@paigy/harness 0.3.40 → 0.3.42
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 -37
- package/dist/cli.js +28903 -29641
- package/dist/main.js +329 -945
- package/package.json +1 -1
package/dist/main.js
CHANGED
|
@@ -11041,6 +11041,31 @@ var AgendaTurnSchema = external_exports.object({
|
|
|
11041
11041
|
ifSilent: external_exports.boolean().optional()
|
|
11042
11042
|
});
|
|
11043
11043
|
var CLAIM_STALE_MS = 30 * 6e4;
|
|
11044
|
+
var CallQuestionSchema = external_exports.object({
|
|
11045
|
+
id: external_exports.string(),
|
|
11046
|
+
entryId: external_exports.string(),
|
|
11047
|
+
title: external_exports.string(),
|
|
11048
|
+
state: external_exports.enum(["open", "answered"]),
|
|
11049
|
+
answer: external_exports.string().nullable(),
|
|
11050
|
+
/** WHO ASKED IT (owner, 2026-09-23, Goal a345e906: each agenda row wears its agent's face) — the
|
|
11051
|
+
* request Entry's author, as the same three facts the item's own `tokenId`/`name`/`voice`
|
|
11052
|
+
* carry for the call's one agent, so the phone draws it with the same seed. Absent when the
|
|
11053
|
+
* author is not an agent this account holds (unpaired since, or a person). */
|
|
11054
|
+
agent: external_exports.object({ tokenId: external_exports.string(), name: external_exports.string(), voice: VoiceKeySchema.optional() }).optional(),
|
|
11055
|
+
/** ITS OPTIONS, WHEN THERE IS SOMETHING TO SEE (owner, 2026-09-25: "Yes, add it"): the options
|
|
11056
|
+
* its need offers, exactly as its own card carries them, present only when one of them has a
|
|
11057
|
+
* preview (`html` or `image`). The call screen opens them from the agenda row, so a preview is
|
|
11058
|
+
* never re-sent as a second card to be seen mid-call. Words-only options are `choices`. */
|
|
11059
|
+
options: external_exports.array(OptionSchema).optional(),
|
|
11060
|
+
/** ITS OPTIONS WHEN THEY ARE WORDS (owner, 2026-10-02, on a call: "whenever there are pre-made
|
|
11061
|
+
* options … now for a checklist, we should also show them on the screen"; design C, 2026-10-08):
|
|
11062
|
+
* numbered chips above the call's controls, so a checklist is never only something read aloud. A
|
|
11063
|
+
* field of its own so `options` keeps meaning "previews": a phone on an older bundle opens the
|
|
11064
|
+
* preview layout for any `options`, and must see exactly what it did. */
|
|
11065
|
+
choices: external_exports.array(OptionSchema).optional(),
|
|
11066
|
+
/** HOW MANY MAY BE PICKED, beside its `choices`: one, any (`many`) or an order (`rank`). */
|
|
11067
|
+
select: external_exports.enum(["one", "many", "rank"]).optional()
|
|
11068
|
+
});
|
|
11044
11069
|
var InboxItemSchema = external_exports.object({
|
|
11045
11070
|
id: external_exports.string(),
|
|
11046
11071
|
tokenId: external_exports.string().optional(),
|
|
@@ -11082,31 +11107,10 @@ var InboxItemSchema = external_exports.object({
|
|
|
11082
11107
|
* `turn` topic (`asking`, `settled`), because the bot never sees a DecisionNeed id. `title` is
|
|
11083
11108
|
* the card's own concise heading; `answer` the accepted answer in words, null while open. It
|
|
11084
11109
|
* REPLACED `agenda` (turns), which nothing ever filled. */
|
|
11085
|
-
questions: external_exports.array(
|
|
11086
|
-
|
|
11087
|
-
|
|
11088
|
-
|
|
11089
|
-
state: external_exports.enum(["open", "answered"]),
|
|
11090
|
-
answer: external_exports.string().nullable(),
|
|
11091
|
-
/** WHO ASKED IT (owner, 2026-09-23, Goal a345e906: each agenda row wears its agent's face) — the
|
|
11092
|
-
* request Entry's author, as the same three facts the item's own `tokenId`/`name`/`voice`
|
|
11093
|
-
* carry for the call's one agent, so the phone draws it with the same seed. Absent when the
|
|
11094
|
-
* author is not an agent this account holds (unpaired since, or a person). */
|
|
11095
|
-
agent: external_exports.object({ tokenId: external_exports.string(), name: external_exports.string(), voice: VoiceKeySchema.optional() }).optional(),
|
|
11096
|
-
/** ITS OPTIONS, WHEN THERE IS SOMETHING TO SEE (owner, 2026-09-25: "Yes, add it"): the options
|
|
11097
|
-
* its need offers, exactly as its own card carries them, present only when one of them has a
|
|
11098
|
-
* preview (`html` or `image`). The call screen opens them from the agenda row, so a preview is
|
|
11099
|
-
* never re-sent as a second card to be seen mid-call. Words-only options are `choices`. */
|
|
11100
|
-
options: external_exports.array(OptionSchema).optional(),
|
|
11101
|
-
/** ITS OPTIONS WHEN THEY ARE WORDS (owner, 2026-10-02, on a call: "whenever there are pre-made
|
|
11102
|
-
* options … now for a checklist, we should also show them on the screen"; design C, 2026-10-08):
|
|
11103
|
-
* numbered chips above the call's controls, so a checklist is never only something read aloud. A
|
|
11104
|
-
* field of its own so `options` keeps meaning "previews": a phone on an older bundle opens the
|
|
11105
|
-
* preview layout for any `options`, and must see exactly what it did. */
|
|
11106
|
-
choices: external_exports.array(OptionSchema).optional(),
|
|
11107
|
-
/** HOW MANY MAY BE PICKED, beside its `choices`: one, any (`many`) or an order (`rank`). */
|
|
11108
|
-
select: external_exports.enum(["one", "many", "rank"]).optional()
|
|
11109
|
-
})).optional(),
|
|
11110
|
+
questions: external_exports.array(CallQuestionSchema).optional(),
|
|
11111
|
+
/** The same call carrying order, including request Entries with no DecisionNeed as reports.
|
|
11112
|
+
* Additive: questions stays decision-only for every other surface and older bundles. */
|
|
11113
|
+
callItems: external_exports.array(CallQuestionSchema.extend({ state: external_exports.enum(["open", "answered", "report"]) })).optional(),
|
|
11110
11114
|
visuals: external_exports.array(VisualSchema).optional(),
|
|
11111
11115
|
/** The connected agent's name (the single pairing name — user-typed, or the
|
|
11112
11116
|
* agent's suggestion, or a default silly name). */
|
|
@@ -11305,12 +11309,29 @@ var UserSettingsSchema = external_exports.object({
|
|
|
11305
11309
|
* clobber guard as voiceMode: absent = leave unchanged on write. */
|
|
11306
11310
|
broker: BrokerTuningSchema.optional()
|
|
11307
11311
|
});
|
|
11312
|
+
var GoalLatestSchema = external_exports.object({
|
|
11313
|
+
from: external_exports.enum(["person", "agent"]),
|
|
11314
|
+
said: external_exports.string(),
|
|
11315
|
+
at: external_exports.string(),
|
|
11316
|
+
entryId: external_exports.string(),
|
|
11317
|
+
/** ITS SUMMARY IN AT MOST TEN WORDS (owner, 2026-10-08: "the same switch to the more summarized sources
|
|
11318
|
+
* at the work tab top level screen too"): what the talker or filer wrote on the Entry; absent when
|
|
11319
|
+
* none was. The Work row reads it before `said`. */
|
|
11320
|
+
summary: external_exports.string().optional(),
|
|
11321
|
+
/** AN AGENT'S UPDATE OR QUESTION AS ITS UNITS' TITLES (owner, 2026-10-08: "send the 50 chars from the title
|
|
11322
|
+
* of each unit"): each at most 50 characters, in order; absent when it has none. */
|
|
11323
|
+
titles: external_exports.array(external_exports.string()).optional(),
|
|
11324
|
+
/** A PROGRESS NOTE'S ONE POINT ("An agent's progress note -> maybe let's cap it at 200 chars"): the
|
|
11325
|
+
* update's own line, before its units; the row caps it at 200. Absent on anything else. */
|
|
11326
|
+
line: external_exports.string().optional()
|
|
11327
|
+
});
|
|
11308
11328
|
var HistoryWorkSchema = external_exports.object({
|
|
11309
11329
|
id: external_exports.string(),
|
|
11310
11330
|
title: external_exports.string(),
|
|
11311
11331
|
state: external_exports.enum(["done", "cancelled"]),
|
|
11312
11332
|
/** Who held it (`agent:<tokenId>` or `human:<userId>`). */
|
|
11313
|
-
assignee: external_exports.string()
|
|
11333
|
+
assignee: external_exports.string(),
|
|
11334
|
+
latest: GoalLatestSchema.nullable().optional()
|
|
11314
11335
|
});
|
|
11315
11336
|
var HistoryEntrySchema = external_exports.union([
|
|
11316
11337
|
external_exports.object({ at: external_exports.string(), card: InboxItemSchema }),
|
|
@@ -11378,7 +11399,7 @@ var ConnectionSummarySchema = external_exports.object({
|
|
|
11378
11399
|
/** The @paigy/harness this host is running — a machine the self-update has not reached
|
|
11379
11400
|
* shows its age here (`apps/desktop/src/update.ts`). */
|
|
11380
11401
|
version: external_exports.string().optional(),
|
|
11381
|
-
harnesses: external_exports.array(external_exports.object({ name: external_exports.string(), label: external_exports.string(), status: external_exports.string() })).optional(),
|
|
11402
|
+
harnesses: external_exports.array(external_exports.object({ name: external_exports.string(), label: external_exports.string(), status: external_exports.string(), models: external_exports.array(external_exports.string()).optional() })).optional(),
|
|
11382
11403
|
workspaces: external_exports.array(external_exports.string()).optional(),
|
|
11383
11404
|
/** THE GIT REPOS IN THOSE FOLDERS (2026-10-01, Goal 26982211): each granted folder that is a
|
|
11384
11405
|
* repo, and each repo directly inside one, with its `origin` remote. A session started for
|
|
@@ -11538,22 +11559,7 @@ var QueueItemSchema = external_exports.object({
|
|
|
11538
11559
|
* shows the moment it is written, and the agent's reply or next note replaces it by being newer.
|
|
11539
11560
|
* `said` is bounded to 280 characters server-side (a line, not the conversation). Null when the
|
|
11540
11561
|
* Goal carries no readable Entry; optional, so hand-built queues need not spell it. */
|
|
11541
|
-
latest:
|
|
11542
|
-
from: external_exports.enum(["person", "agent"]),
|
|
11543
|
-
said: external_exports.string(),
|
|
11544
|
-
at: external_exports.string(),
|
|
11545
|
-
entryId: external_exports.string(),
|
|
11546
|
-
/** ITS SUMMARY IN AT MOST TEN WORDS (owner, 2026-10-08: "the same switch to the more summarized sources
|
|
11547
|
-
* at the work tab top level screen too"): what the talker or filer wrote on the Entry; absent when
|
|
11548
|
-
* none was. The Work row reads it before `said`. */
|
|
11549
|
-
summary: external_exports.string().optional(),
|
|
11550
|
-
/** AN AGENT'S UPDATE OR QUESTION AS ITS UNITS' TITLES (owner, 2026-10-08: "send the 50 chars from the title
|
|
11551
|
-
* of each unit"): each at most 50 characters, in order; absent when it has none. */
|
|
11552
|
-
titles: external_exports.array(external_exports.string()).optional(),
|
|
11553
|
-
/** A PROGRESS NOTE'S ONE POINT ("An agent's progress note -> maybe let's cap it at 200 chars"): the
|
|
11554
|
-
* update's own line, before its units; the row caps it at 200. Absent on anything else. */
|
|
11555
|
-
line: external_exports.string().optional()
|
|
11556
|
-
}).nullable().optional(),
|
|
11562
|
+
latest: GoalLatestSchema.nullable().optional(),
|
|
11557
11563
|
/** WHEN THIS PERSON LAST PUT A HAND ON IT THEMSELVES (owner, Paigy Goal 16d18f51, 2026-09-30):
|
|
11558
11564
|
* the newest Entry on the Goal they wrote, of any kind — a line they added, a reply to a note, an
|
|
11559
11565
|
* answer to a question, a voice note filed as work. Null when the only hands on it have been its
|
|
@@ -11625,58 +11631,6 @@ var NoteSchema = external_exports.object({
|
|
|
11625
11631
|
lastRideAt: external_exports.string().nullable().default(null),
|
|
11626
11632
|
createdAt: external_exports.string()
|
|
11627
11633
|
});
|
|
11628
|
-
var TriageItemSchema = external_exports.object({
|
|
11629
|
-
noteId: external_exports.string(),
|
|
11630
|
-
/** The note's headline at run time. */
|
|
11631
|
-
title: external_exports.string(),
|
|
11632
|
-
/** WHY, in one short human line, evidence first — this is read on a phone underneath
|
|
11633
|
-
* the note's title: "no movement in 34 days", "worked 3 notes in this repo this week".
|
|
11634
|
-
* Never a model's reasoning transcript, never an id. */
|
|
11635
|
-
why: external_exports.string()
|
|
11636
|
-
});
|
|
11637
|
-
var TriageAssignmentSchema = external_exports.object({
|
|
11638
|
-
/** The agent's token id — what `dispatchNote` resolves and what a request is addressed to. */
|
|
11639
|
-
agent: external_exports.string(),
|
|
11640
|
-
/** Its display name at run time (the name on the hatchling's card). Denormalized for the
|
|
11641
|
-
* same reason as `title`: the card must render from the proposal alone. */
|
|
11642
|
-
agentName: external_exports.string(),
|
|
11643
|
-
notes: external_exports.array(TriageItemSchema)
|
|
11644
|
-
});
|
|
11645
|
-
var TriageStatusSchema = external_exports.enum(["open", "superseded", "dismissed"]);
|
|
11646
|
-
var SubmitTriageSchema = external_exports.object({
|
|
11647
|
-
/** Which runtime judged: "ollama" (inference never left the machine) or a harness the
|
|
11648
|
-
* user already runs under their own credentials ("claude" / "codex" / "agy"). Recorded
|
|
11649
|
-
* so the phone can say where the content went — an unattributed privacy claim is worth
|
|
11650
|
-
* nothing, and #1106's promise is precisely "Paigy's servers never see this". */
|
|
11651
|
-
provider: external_exports.string().min(1).max(60),
|
|
11652
|
-
/** The concrete model when the provider names one (an ollama tag); null otherwise. */
|
|
11653
|
-
model: external_exports.string().max(200).nullable().optional(),
|
|
11654
|
-
/** How many open notes the run actually looked at — the denominator on the phone
|
|
11655
|
-
* ("6 of 50"), and the honest answer to "did it read the whole queue?". */
|
|
11656
|
-
reviewed: external_exports.number().int().min(0).max(1e4).default(0),
|
|
11657
|
-
close: external_exports.array(TriageItemSchema).max(200).default([]),
|
|
11658
|
-
stale: external_exports.array(TriageItemSchema).max(200).default([]),
|
|
11659
|
-
assign: external_exports.array(TriageAssignmentSchema).max(50).default([])
|
|
11660
|
-
});
|
|
11661
|
-
var TriageProposalSchema = SubmitTriageSchema.extend({
|
|
11662
|
-
id: external_exports.string(),
|
|
11663
|
-
runAt: external_exports.string(),
|
|
11664
|
-
status: TriageStatusSchema,
|
|
11665
|
-
model: external_exports.string().nullable().default(null)
|
|
11666
|
-
});
|
|
11667
|
-
var AcceptTriageSchema = external_exports.discriminatedUnion("group", [
|
|
11668
|
-
external_exports.object({ group: external_exports.literal("close"), noteIds: external_exports.array(external_exports.string()).max(200).optional() }),
|
|
11669
|
-
external_exports.object({ group: external_exports.literal("stale"), noteIds: external_exports.array(external_exports.string()).max(200).optional() }),
|
|
11670
|
-
external_exports.object({
|
|
11671
|
-
group: external_exports.literal("assign"),
|
|
11672
|
-
agent: external_exports.string().min(1),
|
|
11673
|
-
noteIds: external_exports.array(external_exports.string()).max(200).optional()
|
|
11674
|
-
})
|
|
11675
|
-
]);
|
|
11676
|
-
var AcceptTriageResultSchema = external_exports.object({
|
|
11677
|
-
accepted: external_exports.array(external_exports.string()),
|
|
11678
|
-
failed: external_exports.array(external_exports.object({ noteId: external_exports.string(), reason: external_exports.string() }))
|
|
11679
|
-
});
|
|
11680
11634
|
var DeliveryModeSchema = external_exports.enum(["poll", "self_hosted"]);
|
|
11681
11635
|
var WAKE_EVENT = "wake";
|
|
11682
11636
|
var wakeChannel = (tokenId) => `wake:${tokenId}`;
|
|
@@ -12164,13 +12118,6 @@ async function registerDelivery(mode, opts = {}) {
|
|
|
12164
12118
|
if (!res.ok) throw new Error(`register_delivery failed: ${res.status} ${await res.text()}`);
|
|
12165
12119
|
return await res.json();
|
|
12166
12120
|
}
|
|
12167
|
-
async function listNotes(opts = {}) {
|
|
12168
|
-
const res = await ensureAuthed(await reach(`${BACKEND_URL}/api/notes`, {
|
|
12169
|
-
headers: { authorization: `Bearer ${authToken(opts.token)}` }
|
|
12170
|
-
}));
|
|
12171
|
-
if (!res.ok) throw new Error(`list_notes failed: ${res.status} ${await res.text()}`);
|
|
12172
|
-
return await res.json();
|
|
12173
|
-
}
|
|
12174
12121
|
async function listConnections(opts = {}) {
|
|
12175
12122
|
const res = await ensureAuthed(await reach(`${BACKEND_URL}/api/tokens`, {
|
|
12176
12123
|
headers: { authorization: `Bearer ${authToken(opts.token)}` }
|
|
@@ -12178,39 +12125,6 @@ async function listConnections(opts = {}) {
|
|
|
12178
12125
|
if (!res.ok) throw new Error(`list_connections failed: ${res.status} ${await res.text()}`);
|
|
12179
12126
|
return await res.json();
|
|
12180
12127
|
}
|
|
12181
|
-
async function submitTriage(run, opts = {}) {
|
|
12182
|
-
const res = await ensureAuthed(await reach(`${BACKEND_URL}/api/triage`, {
|
|
12183
|
-
method: "POST",
|
|
12184
|
-
headers: { "content-type": "application/json", authorization: `Bearer ${authToken(opts.token)}` },
|
|
12185
|
-
body: JSON.stringify(run)
|
|
12186
|
-
}));
|
|
12187
|
-
if (!res.ok) throw new Error(`submit_triage failed: ${res.status} ${await res.text()}`);
|
|
12188
|
-
return await res.json();
|
|
12189
|
-
}
|
|
12190
|
-
async function getTriage(opts = {}) {
|
|
12191
|
-
const res = await ensureAuthed(await reach(`${BACKEND_URL}/api/triage`, {
|
|
12192
|
-
headers: { authorization: `Bearer ${authToken(opts.token)}` }
|
|
12193
|
-
}));
|
|
12194
|
-
if (!res.ok) throw new Error(`get_triage failed: ${res.status} ${await res.text()}`);
|
|
12195
|
-
return await res.json();
|
|
12196
|
-
}
|
|
12197
|
-
async function acceptTriage(proposalId, body, opts = {}) {
|
|
12198
|
-
const res = await ensureAuthed(await reach(`${BACKEND_URL}/api/triage/${encodeURIComponent(proposalId)}/accept`, {
|
|
12199
|
-
method: "POST",
|
|
12200
|
-
headers: { "content-type": "application/json", authorization: `Bearer ${authToken(opts.token)}` },
|
|
12201
|
-
body: JSON.stringify(body)
|
|
12202
|
-
}));
|
|
12203
|
-
if (!res.ok) throw new Error(`accept_triage failed: ${res.status} ${await res.text()}`);
|
|
12204
|
-
return await res.json();
|
|
12205
|
-
}
|
|
12206
|
-
async function dismissTriage(proposalId, opts = {}) {
|
|
12207
|
-
const res = await ensureAuthed(await reach(`${BACKEND_URL}/api/triage/${encodeURIComponent(proposalId)}/dismiss`, {
|
|
12208
|
-
method: "POST",
|
|
12209
|
-
headers: { authorization: `Bearer ${authToken(opts.token)}` }
|
|
12210
|
-
}));
|
|
12211
|
-
if (!res.ok) throw new Error(`dismiss_triage failed: ${res.status} ${await res.text()}`);
|
|
12212
|
-
return await res.json();
|
|
12213
|
-
}
|
|
12214
12128
|
var NOT_SENT = `Recorded as the Goal's progress, not sent: nobody was notified. The person is told only a question, an answer to one they asked you, and what you say once the Goal is done. To close the work, send contact.completions or manage_goals kind:complete with the shared completion report, closing and reporting together. If the person asked you, in so many words, to send them this, send it again with userExplicitlyRequested: "any" on that update.`;
|
|
12215
12129
|
var APPLIED = "Applied; nothing was sent to the person.";
|
|
12216
12130
|
var DEMOTED = "Delivered as a card, not a call: the person's settings do not allow a call right now (silent, quiet hours, or a mode). It has reached them; do not send it again.";
|
|
@@ -12473,7 +12387,7 @@ async function subscribeWake(onNudge, opts = {}) {
|
|
|
12473
12387
|
|
|
12474
12388
|
// src/main.ts
|
|
12475
12389
|
var import_qrcode = __toESM(require_lib(), 1);
|
|
12476
|
-
import { homedir as
|
|
12390
|
+
import { homedir as homedir10, hostname } from "os";
|
|
12477
12391
|
|
|
12478
12392
|
// src/harness/catalog.ts
|
|
12479
12393
|
import { spawnSync } from "child_process";
|
|
@@ -12489,7 +12403,8 @@ var CATALOG = [
|
|
|
12489
12403
|
authProbe: ["claude", "auth", "status"],
|
|
12490
12404
|
loginHint: "Run `claude` once in a terminal and sign in.",
|
|
12491
12405
|
installCli: "curl -fsSL https://claude.ai/install.sh | bash",
|
|
12492
|
-
installAdapter: "npm install -g @agentclientprotocol/claude-agent-acp"
|
|
12406
|
+
installAdapter: "npm install -g @agentclientprotocol/claude-agent-acp",
|
|
12407
|
+
models: ["claude-3-5-sonnet-20241022", "claude-3-5-haiku-20241022", "claude-3-opus-20240229"]
|
|
12493
12408
|
},
|
|
12494
12409
|
{
|
|
12495
12410
|
name: "codex",
|
|
@@ -12499,7 +12414,8 @@ var CATALOG = [
|
|
|
12499
12414
|
authProbe: ["codex", "login", "status"],
|
|
12500
12415
|
loginHint: "Run `codex login` to authenticate.",
|
|
12501
12416
|
installCli: "curl -fsSL https://chatgpt.com/codex/install.sh | sh",
|
|
12502
|
-
installAdapter: "npm install -g @agentclientprotocol/codex-acp"
|
|
12417
|
+
installAdapter: "npm install -g @agentclientprotocol/codex-acp",
|
|
12418
|
+
models: ["gpt-4o", "gpt-4o-mini", "o1", "o1-mini"]
|
|
12503
12419
|
},
|
|
12504
12420
|
{
|
|
12505
12421
|
name: "agy",
|
|
@@ -12509,7 +12425,8 @@ var CATALOG = [
|
|
|
12509
12425
|
adapter: "paigy-agy-acp",
|
|
12510
12426
|
authProbe: ["agy", "help"],
|
|
12511
12427
|
loginHint: "Install and set up Antigravity (`agy`).",
|
|
12512
|
-
installCli: "curl -fsSL https://antigravity.dev/install.sh | bash"
|
|
12428
|
+
installCli: "curl -fsSL https://antigravity.dev/install.sh | bash",
|
|
12429
|
+
models: ["gemini-1.5-pro", "gemini-1.5-flash", "gemini-2.0-pro-exp"]
|
|
12513
12430
|
}
|
|
12514
12431
|
];
|
|
12515
12432
|
function probeDirs(home = homedir3()) {
|
|
@@ -12545,7 +12462,7 @@ function runProbe(argv) {
|
|
|
12545
12462
|
function detect(entry, deps = {}) {
|
|
12546
12463
|
const find = deps.which ?? which;
|
|
12547
12464
|
const probe = deps.probe ?? runProbe;
|
|
12548
|
-
const base = { name: entry.name, label: entry.label };
|
|
12465
|
+
const base = { name: entry.name, label: entry.label, models: entry.models };
|
|
12549
12466
|
const cliPath = find(entry.cli);
|
|
12550
12467
|
if (!cliPath) return { ...base, status: "missing", hint: `${entry.label} is not installed.`, install: entry.installCli };
|
|
12551
12468
|
const adapterPath = entry.adapter ? find(entry.adapter) : null;
|
|
@@ -14944,6 +14861,31 @@ var AgendaTurnSchema2 = external_exports.object({
|
|
|
14944
14861
|
ifSilent: external_exports.boolean().optional()
|
|
14945
14862
|
});
|
|
14946
14863
|
var CLAIM_STALE_MS2 = 30 * 6e4;
|
|
14864
|
+
var CallQuestionSchema2 = external_exports.object({
|
|
14865
|
+
id: external_exports.string(),
|
|
14866
|
+
entryId: external_exports.string(),
|
|
14867
|
+
title: external_exports.string(),
|
|
14868
|
+
state: external_exports.enum(["open", "answered"]),
|
|
14869
|
+
answer: external_exports.string().nullable(),
|
|
14870
|
+
/** WHO ASKED IT (owner, 2026-09-23, Goal a345e906: each agenda row wears its agent's face) — the
|
|
14871
|
+
* request Entry's author, as the same three facts the item's own `tokenId`/`name`/`voice`
|
|
14872
|
+
* carry for the call's one agent, so the phone draws it with the same seed. Absent when the
|
|
14873
|
+
* author is not an agent this account holds (unpaired since, or a person). */
|
|
14874
|
+
agent: external_exports.object({ tokenId: external_exports.string(), name: external_exports.string(), voice: VoiceKeySchema2.optional() }).optional(),
|
|
14875
|
+
/** ITS OPTIONS, WHEN THERE IS SOMETHING TO SEE (owner, 2026-09-25: "Yes, add it"): the options
|
|
14876
|
+
* its need offers, exactly as its own card carries them, present only when one of them has a
|
|
14877
|
+
* preview (`html` or `image`). The call screen opens them from the agenda row, so a preview is
|
|
14878
|
+
* never re-sent as a second card to be seen mid-call. Words-only options are `choices`. */
|
|
14879
|
+
options: external_exports.array(OptionSchema2).optional(),
|
|
14880
|
+
/** ITS OPTIONS WHEN THEY ARE WORDS (owner, 2026-10-02, on a call: "whenever there are pre-made
|
|
14881
|
+
* options … now for a checklist, we should also show them on the screen"; design C, 2026-10-08):
|
|
14882
|
+
* numbered chips above the call's controls, so a checklist is never only something read aloud. A
|
|
14883
|
+
* field of its own so `options` keeps meaning "previews": a phone on an older bundle opens the
|
|
14884
|
+
* preview layout for any `options`, and must see exactly what it did. */
|
|
14885
|
+
choices: external_exports.array(OptionSchema2).optional(),
|
|
14886
|
+
/** HOW MANY MAY BE PICKED, beside its `choices`: one, any (`many`) or an order (`rank`). */
|
|
14887
|
+
select: external_exports.enum(["one", "many", "rank"]).optional()
|
|
14888
|
+
});
|
|
14947
14889
|
var InboxItemSchema2 = external_exports.object({
|
|
14948
14890
|
id: external_exports.string(),
|
|
14949
14891
|
tokenId: external_exports.string().optional(),
|
|
@@ -14985,31 +14927,10 @@ var InboxItemSchema2 = external_exports.object({
|
|
|
14985
14927
|
* `turn` topic (`asking`, `settled`), because the bot never sees a DecisionNeed id. `title` is
|
|
14986
14928
|
* the card's own concise heading; `answer` the accepted answer in words, null while open. It
|
|
14987
14929
|
* REPLACED `agenda` (turns), which nothing ever filled. */
|
|
14988
|
-
questions: external_exports.array(
|
|
14989
|
-
|
|
14990
|
-
|
|
14991
|
-
|
|
14992
|
-
state: external_exports.enum(["open", "answered"]),
|
|
14993
|
-
answer: external_exports.string().nullable(),
|
|
14994
|
-
/** WHO ASKED IT (owner, 2026-09-23, Goal a345e906: each agenda row wears its agent's face) — the
|
|
14995
|
-
* request Entry's author, as the same three facts the item's own `tokenId`/`name`/`voice`
|
|
14996
|
-
* carry for the call's one agent, so the phone draws it with the same seed. Absent when the
|
|
14997
|
-
* author is not an agent this account holds (unpaired since, or a person). */
|
|
14998
|
-
agent: external_exports.object({ tokenId: external_exports.string(), name: external_exports.string(), voice: VoiceKeySchema2.optional() }).optional(),
|
|
14999
|
-
/** ITS OPTIONS, WHEN THERE IS SOMETHING TO SEE (owner, 2026-09-25: "Yes, add it"): the options
|
|
15000
|
-
* its need offers, exactly as its own card carries them, present only when one of them has a
|
|
15001
|
-
* preview (`html` or `image`). The call screen opens them from the agenda row, so a preview is
|
|
15002
|
-
* never re-sent as a second card to be seen mid-call. Words-only options are `choices`. */
|
|
15003
|
-
options: external_exports.array(OptionSchema2).optional(),
|
|
15004
|
-
/** ITS OPTIONS WHEN THEY ARE WORDS (owner, 2026-10-02, on a call: "whenever there are pre-made
|
|
15005
|
-
* options … now for a checklist, we should also show them on the screen"; design C, 2026-10-08):
|
|
15006
|
-
* numbered chips above the call's controls, so a checklist is never only something read aloud. A
|
|
15007
|
-
* field of its own so `options` keeps meaning "previews": a phone on an older bundle opens the
|
|
15008
|
-
* preview layout for any `options`, and must see exactly what it did. */
|
|
15009
|
-
choices: external_exports.array(OptionSchema2).optional(),
|
|
15010
|
-
/** HOW MANY MAY BE PICKED, beside its `choices`: one, any (`many`) or an order (`rank`). */
|
|
15011
|
-
select: external_exports.enum(["one", "many", "rank"]).optional()
|
|
15012
|
-
})).optional(),
|
|
14930
|
+
questions: external_exports.array(CallQuestionSchema2).optional(),
|
|
14931
|
+
/** The same call carrying order, including request Entries with no DecisionNeed as reports.
|
|
14932
|
+
* Additive: questions stays decision-only for every other surface and older bundles. */
|
|
14933
|
+
callItems: external_exports.array(CallQuestionSchema2.extend({ state: external_exports.enum(["open", "answered", "report"]) })).optional(),
|
|
15013
14934
|
visuals: external_exports.array(VisualSchema2).optional(),
|
|
15014
14935
|
/** The connected agent's name (the single pairing name — user-typed, or the
|
|
15015
14936
|
* agent's suggestion, or a default silly name). */
|
|
@@ -15208,12 +15129,29 @@ var UserSettingsSchema2 = external_exports.object({
|
|
|
15208
15129
|
* clobber guard as voiceMode: absent = leave unchanged on write. */
|
|
15209
15130
|
broker: BrokerTuningSchema2.optional()
|
|
15210
15131
|
});
|
|
15132
|
+
var GoalLatestSchema2 = external_exports.object({
|
|
15133
|
+
from: external_exports.enum(["person", "agent"]),
|
|
15134
|
+
said: external_exports.string(),
|
|
15135
|
+
at: external_exports.string(),
|
|
15136
|
+
entryId: external_exports.string(),
|
|
15137
|
+
/** ITS SUMMARY IN AT MOST TEN WORDS (owner, 2026-10-08: "the same switch to the more summarized sources
|
|
15138
|
+
* at the work tab top level screen too"): what the talker or filer wrote on the Entry; absent when
|
|
15139
|
+
* none was. The Work row reads it before `said`. */
|
|
15140
|
+
summary: external_exports.string().optional(),
|
|
15141
|
+
/** AN AGENT'S UPDATE OR QUESTION AS ITS UNITS' TITLES (owner, 2026-10-08: "send the 50 chars from the title
|
|
15142
|
+
* of each unit"): each at most 50 characters, in order; absent when it has none. */
|
|
15143
|
+
titles: external_exports.array(external_exports.string()).optional(),
|
|
15144
|
+
/** A PROGRESS NOTE'S ONE POINT ("An agent's progress note -> maybe let's cap it at 200 chars"): the
|
|
15145
|
+
* update's own line, before its units; the row caps it at 200. Absent on anything else. */
|
|
15146
|
+
line: external_exports.string().optional()
|
|
15147
|
+
});
|
|
15211
15148
|
var HistoryWorkSchema2 = external_exports.object({
|
|
15212
15149
|
id: external_exports.string(),
|
|
15213
15150
|
title: external_exports.string(),
|
|
15214
15151
|
state: external_exports.enum(["done", "cancelled"]),
|
|
15215
15152
|
/** Who held it (`agent:<tokenId>` or `human:<userId>`). */
|
|
15216
|
-
assignee: external_exports.string()
|
|
15153
|
+
assignee: external_exports.string(),
|
|
15154
|
+
latest: GoalLatestSchema2.nullable().optional()
|
|
15217
15155
|
});
|
|
15218
15156
|
var HistoryEntrySchema2 = external_exports.union([
|
|
15219
15157
|
external_exports.object({ at: external_exports.string(), card: InboxItemSchema2 }),
|
|
@@ -15281,7 +15219,7 @@ var ConnectionSummarySchema2 = external_exports.object({
|
|
|
15281
15219
|
/** The @paigy/harness this host is running — a machine the self-update has not reached
|
|
15282
15220
|
* shows its age here (`apps/desktop/src/update.ts`). */
|
|
15283
15221
|
version: external_exports.string().optional(),
|
|
15284
|
-
harnesses: external_exports.array(external_exports.object({ name: external_exports.string(), label: external_exports.string(), status: external_exports.string() })).optional(),
|
|
15222
|
+
harnesses: external_exports.array(external_exports.object({ name: external_exports.string(), label: external_exports.string(), status: external_exports.string(), models: external_exports.array(external_exports.string()).optional() })).optional(),
|
|
15285
15223
|
workspaces: external_exports.array(external_exports.string()).optional(),
|
|
15286
15224
|
/** THE GIT REPOS IN THOSE FOLDERS (2026-10-01, Goal 26982211): each granted folder that is a
|
|
15287
15225
|
* repo, and each repo directly inside one, with its `origin` remote. A session started for
|
|
@@ -15441,22 +15379,7 @@ var QueueItemSchema2 = external_exports.object({
|
|
|
15441
15379
|
* shows the moment it is written, and the agent's reply or next note replaces it by being newer.
|
|
15442
15380
|
* `said` is bounded to 280 characters server-side (a line, not the conversation). Null when the
|
|
15443
15381
|
* Goal carries no readable Entry; optional, so hand-built queues need not spell it. */
|
|
15444
|
-
latest:
|
|
15445
|
-
from: external_exports.enum(["person", "agent"]),
|
|
15446
|
-
said: external_exports.string(),
|
|
15447
|
-
at: external_exports.string(),
|
|
15448
|
-
entryId: external_exports.string(),
|
|
15449
|
-
/** ITS SUMMARY IN AT MOST TEN WORDS (owner, 2026-10-08: "the same switch to the more summarized sources
|
|
15450
|
-
* at the work tab top level screen too"): what the talker or filer wrote on the Entry; absent when
|
|
15451
|
-
* none was. The Work row reads it before `said`. */
|
|
15452
|
-
summary: external_exports.string().optional(),
|
|
15453
|
-
/** AN AGENT'S UPDATE OR QUESTION AS ITS UNITS' TITLES (owner, 2026-10-08: "send the 50 chars from the title
|
|
15454
|
-
* of each unit"): each at most 50 characters, in order; absent when it has none. */
|
|
15455
|
-
titles: external_exports.array(external_exports.string()).optional(),
|
|
15456
|
-
/** A PROGRESS NOTE'S ONE POINT ("An agent's progress note -> maybe let's cap it at 200 chars"): the
|
|
15457
|
-
* update's own line, before its units; the row caps it at 200. Absent on anything else. */
|
|
15458
|
-
line: external_exports.string().optional()
|
|
15459
|
-
}).nullable().optional(),
|
|
15382
|
+
latest: GoalLatestSchema2.nullable().optional(),
|
|
15460
15383
|
/** WHEN THIS PERSON LAST PUT A HAND ON IT THEMSELVES (owner, Paigy Goal 16d18f51, 2026-09-30):
|
|
15461
15384
|
* the newest Entry on the Goal they wrote, of any kind — a line they added, a reply to a note, an
|
|
15462
15385
|
* answer to a question, a voice note filed as work. Null when the only hands on it have been its
|
|
@@ -15528,58 +15451,6 @@ var NoteSchema2 = external_exports.object({
|
|
|
15528
15451
|
lastRideAt: external_exports.string().nullable().default(null),
|
|
15529
15452
|
createdAt: external_exports.string()
|
|
15530
15453
|
});
|
|
15531
|
-
var TriageItemSchema2 = external_exports.object({
|
|
15532
|
-
noteId: external_exports.string(),
|
|
15533
|
-
/** The note's headline at run time. */
|
|
15534
|
-
title: external_exports.string(),
|
|
15535
|
-
/** WHY, in one short human line, evidence first — this is read on a phone underneath
|
|
15536
|
-
* the note's title: "no movement in 34 days", "worked 3 notes in this repo this week".
|
|
15537
|
-
* Never a model's reasoning transcript, never an id. */
|
|
15538
|
-
why: external_exports.string()
|
|
15539
|
-
});
|
|
15540
|
-
var TriageAssignmentSchema2 = external_exports.object({
|
|
15541
|
-
/** The agent's token id — what `dispatchNote` resolves and what a request is addressed to. */
|
|
15542
|
-
agent: external_exports.string(),
|
|
15543
|
-
/** Its display name at run time (the name on the hatchling's card). Denormalized for the
|
|
15544
|
-
* same reason as `title`: the card must render from the proposal alone. */
|
|
15545
|
-
agentName: external_exports.string(),
|
|
15546
|
-
notes: external_exports.array(TriageItemSchema2)
|
|
15547
|
-
});
|
|
15548
|
-
var TriageStatusSchema2 = external_exports.enum(["open", "superseded", "dismissed"]);
|
|
15549
|
-
var SubmitTriageSchema2 = external_exports.object({
|
|
15550
|
-
/** Which runtime judged: "ollama" (inference never left the machine) or a harness the
|
|
15551
|
-
* user already runs under their own credentials ("claude" / "codex" / "agy"). Recorded
|
|
15552
|
-
* so the phone can say where the content went — an unattributed privacy claim is worth
|
|
15553
|
-
* nothing, and #1106's promise is precisely "Paigy's servers never see this". */
|
|
15554
|
-
provider: external_exports.string().min(1).max(60),
|
|
15555
|
-
/** The concrete model when the provider names one (an ollama tag); null otherwise. */
|
|
15556
|
-
model: external_exports.string().max(200).nullable().optional(),
|
|
15557
|
-
/** How many open notes the run actually looked at — the denominator on the phone
|
|
15558
|
-
* ("6 of 50"), and the honest answer to "did it read the whole queue?". */
|
|
15559
|
-
reviewed: external_exports.number().int().min(0).max(1e4).default(0),
|
|
15560
|
-
close: external_exports.array(TriageItemSchema2).max(200).default([]),
|
|
15561
|
-
stale: external_exports.array(TriageItemSchema2).max(200).default([]),
|
|
15562
|
-
assign: external_exports.array(TriageAssignmentSchema2).max(50).default([])
|
|
15563
|
-
});
|
|
15564
|
-
var TriageProposalSchema2 = SubmitTriageSchema2.extend({
|
|
15565
|
-
id: external_exports.string(),
|
|
15566
|
-
runAt: external_exports.string(),
|
|
15567
|
-
status: TriageStatusSchema2,
|
|
15568
|
-
model: external_exports.string().nullable().default(null)
|
|
15569
|
-
});
|
|
15570
|
-
var AcceptTriageSchema2 = external_exports.discriminatedUnion("group", [
|
|
15571
|
-
external_exports.object({ group: external_exports.literal("close"), noteIds: external_exports.array(external_exports.string()).max(200).optional() }),
|
|
15572
|
-
external_exports.object({ group: external_exports.literal("stale"), noteIds: external_exports.array(external_exports.string()).max(200).optional() }),
|
|
15573
|
-
external_exports.object({
|
|
15574
|
-
group: external_exports.literal("assign"),
|
|
15575
|
-
agent: external_exports.string().min(1),
|
|
15576
|
-
noteIds: external_exports.array(external_exports.string()).max(200).optional()
|
|
15577
|
-
})
|
|
15578
|
-
]);
|
|
15579
|
-
var AcceptTriageResultSchema2 = external_exports.object({
|
|
15580
|
-
accepted: external_exports.array(external_exports.string()),
|
|
15581
|
-
failed: external_exports.array(external_exports.object({ noteId: external_exports.string(), reason: external_exports.string() }))
|
|
15582
|
-
});
|
|
15583
15454
|
var DeliveryModeSchema2 = external_exports.enum(["poll", "self_hosted"]);
|
|
15584
15455
|
var RegisterDeliverySchema2 = external_exports.object({ mode: DeliveryModeSchema2 });
|
|
15585
15456
|
var OAuthStartSchema2 = external_exports.object({
|
|
@@ -15836,579 +15707,29 @@ var CallRecapSchema2 = external_exports.object({
|
|
|
15836
15707
|
|
|
15837
15708
|
// src/host.ts
|
|
15838
15709
|
import { readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
|
|
15839
|
-
import {
|
|
15710
|
+
import { execSync } from "child_process";
|
|
15711
|
+
import { randomBytes } from "crypto";
|
|
15712
|
+
import { homedir as homedir9 } from "os";
|
|
15840
15713
|
import { join as join11 } from "path";
|
|
15841
15714
|
|
|
15842
|
-
// src/
|
|
15843
|
-
var OPEN_STATUSES = ["open", "assigned", "in_progress"];
|
|
15844
|
-
var DAY_MS = 864e5;
|
|
15845
|
-
var daysBetween = (from, now) => from ? Math.floor((now - Date.parse(from)) / DAY_MS) : null;
|
|
15846
|
-
function assigneeToken(note2) {
|
|
15847
|
-
const ref = note2.assignee ?? "";
|
|
15848
|
-
return ref.startsWith("agent:") ? ref.slice("agent:".length) : null;
|
|
15849
|
-
}
|
|
15850
|
-
var STOP = /* @__PURE__ */ new Set([
|
|
15851
|
-
"the",
|
|
15852
|
-
"and",
|
|
15853
|
-
"for",
|
|
15854
|
-
"with",
|
|
15855
|
-
"that",
|
|
15856
|
-
"this",
|
|
15857
|
-
"from",
|
|
15858
|
-
"into",
|
|
15859
|
-
"when",
|
|
15860
|
-
"what",
|
|
15861
|
-
"should",
|
|
15862
|
-
"would",
|
|
15863
|
-
"could",
|
|
15864
|
-
"have",
|
|
15865
|
-
"make",
|
|
15866
|
-
"made",
|
|
15867
|
-
"need",
|
|
15868
|
-
"needs",
|
|
15869
|
-
"want",
|
|
15870
|
-
"wants",
|
|
15871
|
-
"please",
|
|
15872
|
-
"also",
|
|
15873
|
-
"then",
|
|
15874
|
-
"than",
|
|
15875
|
-
"them",
|
|
15876
|
-
"they",
|
|
15877
|
-
"there",
|
|
15878
|
-
"here",
|
|
15879
|
-
"about",
|
|
15880
|
-
"some",
|
|
15881
|
-
"more",
|
|
15882
|
-
"most",
|
|
15883
|
-
"just",
|
|
15884
|
-
"like",
|
|
15885
|
-
"want",
|
|
15886
|
-
"will",
|
|
15887
|
-
"does",
|
|
15888
|
-
"done",
|
|
15889
|
-
"your",
|
|
15890
|
-
"yours",
|
|
15891
|
-
"each",
|
|
15892
|
-
"every",
|
|
15893
|
-
"onto"
|
|
15894
|
-
]);
|
|
15895
|
-
function words(text) {
|
|
15896
|
-
return (text.toLowerCase().match(/[a-z0-9][a-z0-9_.-]{2,}/g) ?? []).filter(
|
|
15897
|
-
(w) => w.length >= 4 && !STOP.has(w)
|
|
15898
|
-
);
|
|
15899
|
-
}
|
|
15900
|
-
function repoHints(text) {
|
|
15901
|
-
const hits = /* @__PURE__ */ new Set();
|
|
15902
|
-
for (const m of text.matchAll(/\b([a-z0-9][a-z0-9_.-]*\/[a-z0-9][a-z0-9_.-]*)/gi)) {
|
|
15903
|
-
const hit = m[1];
|
|
15904
|
-
if (hit) hits.add(hit.toLowerCase());
|
|
15905
|
-
}
|
|
15906
|
-
for (const m of text.matchAll(/`([^`]{2,60})`/g)) {
|
|
15907
|
-
const inner = m[1]?.trim().toLowerCase();
|
|
15908
|
-
if (inner && inner.includes("/")) hits.add(inner.split(/\s+/)[0] ?? inner);
|
|
15909
|
-
}
|
|
15910
|
-
return [...hits].slice(0, 6);
|
|
15911
|
-
}
|
|
15912
|
-
function overlap(a, b) {
|
|
15913
|
-
const A = new Set(words(a));
|
|
15914
|
-
const B = new Set(words(b));
|
|
15915
|
-
if (A.size === 0 || B.size === 0) return 0;
|
|
15916
|
-
let shared = 0;
|
|
15917
|
-
for (const w of A) if (B.has(w)) shared++;
|
|
15918
|
-
return shared / (A.size + B.size - shared);
|
|
15919
|
-
}
|
|
15920
|
-
var SUPERSEDE_AT = 0.5;
|
|
15921
|
-
function supersededBy(note2, all) {
|
|
15922
|
-
const mine = `${note2.title} ${note2.intent}`;
|
|
15923
|
-
const at = Date.parse(note2.createdAt);
|
|
15924
|
-
let best = null;
|
|
15925
|
-
for (const other of all) {
|
|
15926
|
-
if (other.id === note2.id) continue;
|
|
15927
|
-
if (Date.parse(other.createdAt) <= at) continue;
|
|
15928
|
-
const score = overlap(mine, `${other.title} ${other.intent}`);
|
|
15929
|
-
if (score >= SUPERSEDE_AT && (!best || score > best.score)) best = { note: other, score };
|
|
15930
|
-
}
|
|
15931
|
-
return best?.note ?? null;
|
|
15932
|
-
}
|
|
15933
|
-
function doneEcho(note2, all) {
|
|
15934
|
-
const mine = `${note2.title} ${note2.intent}`;
|
|
15935
|
-
let best = null;
|
|
15936
|
-
for (const other of all) {
|
|
15937
|
-
if (other.id === note2.id || other.status !== "done") continue;
|
|
15938
|
-
const score = overlap(mine, `${other.title} ${other.intent}`);
|
|
15939
|
-
if (score >= SUPERSEDE_AT && (!best || score > best.score)) best = { note: other, score };
|
|
15940
|
-
}
|
|
15941
|
-
return best?.note ?? null;
|
|
15942
|
-
}
|
|
15943
|
-
var RECENT_DAYS = 7;
|
|
15944
|
-
function agentSignals(notes, connections, now) {
|
|
15945
|
-
return connections.map((c) => {
|
|
15946
|
-
const mine = notes.filter((n) => assigneeToken(n) === c.id);
|
|
15947
|
-
const recent = mine.filter((n) => (daysBetween(n.createdAt, now) ?? Infinity) <= RECENT_DAYS);
|
|
15948
|
-
const repoCount = /* @__PURE__ */ new Map();
|
|
15949
|
-
for (const n of recent) for (const r of repoHints(`${n.title} ${n.intent}`)) repoCount.set(r, (repoCount.get(r) ?? 0) + 1);
|
|
15950
|
-
return {
|
|
15951
|
-
token: c.id,
|
|
15952
|
-
name: c.name,
|
|
15953
|
-
lastSeenDays: daysBetween(c.lastSeenAt ?? null, now),
|
|
15954
|
-
lastContactDays: daysBetween(c.lastContactAt, now),
|
|
15955
|
-
openNotes: mine.filter((n) => n.status !== "done").length,
|
|
15956
|
-
recentNotes: recent.length,
|
|
15957
|
-
repos: [...repoCount.entries()].sort((a, b) => b[1] - a[1]).map(([r]) => r).slice(0, 5),
|
|
15958
|
-
recentTitles: recent.slice(0, 5).map((n) => n.title)
|
|
15959
|
-
};
|
|
15960
|
-
});
|
|
15961
|
-
}
|
|
15962
|
-
function noteSignals(notes, connections, now) {
|
|
15963
|
-
const nameOf = new Map(connections.map((c) => [c.id, c.name]));
|
|
15964
|
-
const open = notes.filter((n) => OPEN_STATUSES.includes(n.status));
|
|
15965
|
-
return open.map((n) => {
|
|
15966
|
-
const superseded = supersededBy(n, notes);
|
|
15967
|
-
const echo = doneEcho(n, notes);
|
|
15968
|
-
const token = assigneeToken(n);
|
|
15969
|
-
return {
|
|
15970
|
-
id: n.id,
|
|
15971
|
-
title: n.title,
|
|
15972
|
-
intent: n.intent,
|
|
15973
|
-
status: n.status,
|
|
15974
|
-
ageDays: daysBetween(n.createdAt, now) ?? 0,
|
|
15975
|
-
idleDays: daysBetween(n.lastRideAt ?? n.createdAt, now) ?? 0,
|
|
15976
|
-
rides: n.rides,
|
|
15977
|
-
assignee: token,
|
|
15978
|
-
assigneeName: token ? nameOf.get(token) ?? null : null,
|
|
15979
|
-
repoHints: repoHints(`${n.title} ${n.intent}`),
|
|
15980
|
-
supersededBy: superseded ? { id: superseded.id, title: superseded.title } : null,
|
|
15981
|
-
doneEcho: echo ? { id: echo.id, title: echo.title } : null
|
|
15982
|
-
};
|
|
15983
|
-
});
|
|
15984
|
-
}
|
|
15985
|
-
function dossier(notes, connections, now) {
|
|
15986
|
-
return {
|
|
15987
|
-
notes: noteSignals(notes, connections, now),
|
|
15988
|
-
agents: agentSignals(notes, connections, now),
|
|
15989
|
-
today: new Date(now).toISOString().slice(0, 10)
|
|
15990
|
-
};
|
|
15991
|
-
}
|
|
15992
|
-
function affinityWhy(note2, agent2) {
|
|
15993
|
-
const shared = note2.repoHints.filter((r) => agent2.repos.includes(r));
|
|
15994
|
-
if (shared.length && agent2.recentNotes)
|
|
15995
|
-
return `worked ${agent2.recentNotes} note${agent2.recentNotes === 1 ? "" : "s"} in ${shared[0]} this week`;
|
|
15996
|
-
if (agent2.recentNotes >= 2) return `took ${agent2.recentNotes} notes this week`;
|
|
15997
|
-
if (agent2.lastSeenDays !== null && agent2.lastSeenDays <= 1) return "active on this machine today";
|
|
15998
|
-
if (agent2.lastContactDays !== null && agent2.lastContactDays <= RECENT_DAYS)
|
|
15999
|
-
return `last spoke ${agent2.lastContactDays === 0 ? "today" : `${agent2.lastContactDays}d ago`}`;
|
|
16000
|
-
return null;
|
|
16001
|
-
}
|
|
16002
|
-
function staleWhy(note2) {
|
|
16003
|
-
if (note2.doneEcho) return `a later note says this shipped: "${note2.doneEcho.title}"`;
|
|
16004
|
-
if (note2.supersededBy) return `superseded by "${note2.supersededBy.title}"`;
|
|
16005
|
-
if (note2.rides >= 3) return `carried on ${note2.rides} calls and still open`;
|
|
16006
|
-
if (note2.idleDays >= 30) return `no movement in ${note2.idleDays} days`;
|
|
16007
|
-
if (note2.idleDays >= 14 && note2.status === "open") return `unassigned for ${note2.idleDays} days`;
|
|
16008
|
-
return null;
|
|
16009
|
-
}
|
|
16010
|
-
|
|
16011
|
-
// src/triage/judge.ts
|
|
16012
|
-
import { spawnSync as spawnSync2 } from "child_process";
|
|
16013
|
-
import { homedir as homedir4 } from "os";
|
|
16014
|
-
import { delimiter as delimiter2 } from "path";
|
|
16015
|
-
var PRIVACY = {
|
|
16016
|
-
ollama: "never leaves this machine",
|
|
16017
|
-
claude: "this machine + your own Claude subscription; Paigy's servers never see it",
|
|
16018
|
-
codex: "this machine + your own Codex subscription; Paigy's servers never see it",
|
|
16019
|
-
agy: "this machine + your own Antigravity account; Paigy's servers never see it"
|
|
16020
|
-
};
|
|
16021
|
-
var OLLAMA_HOST = process.env["OLLAMA_HOST"] ?? "http://localhost:11434";
|
|
16022
|
-
var PREFERRED = ["llama3.2", "qwen2.5", "phi", "gemma", "mistral", "llama3.1", "llama3"];
|
|
16023
|
-
function pickOllamaModel(tags) {
|
|
16024
|
-
if (tags.length === 0) return null;
|
|
16025
|
-
const score = (tag) => {
|
|
16026
|
-
const i = PREFERRED.findIndex((p) => tag.toLowerCase().startsWith(p));
|
|
16027
|
-
return i === -1 ? PREFERRED.length : i;
|
|
16028
|
-
};
|
|
16029
|
-
return [...tags].sort((a, b) => score(a) - score(b) || a.length - b.length)[0] ?? null;
|
|
16030
|
-
}
|
|
16031
|
-
function parseOllamaTags(body) {
|
|
16032
|
-
const models = body?.models;
|
|
16033
|
-
if (!Array.isArray(models)) return [];
|
|
16034
|
-
return models.map((m) => m?.name ?? m?.model).filter((n) => typeof n === "string" && n.length > 0);
|
|
16035
|
-
}
|
|
16036
|
-
async function fetchTags(host2) {
|
|
16037
|
-
try {
|
|
16038
|
-
const res = await fetch(`${host2}/api/tags`, { signal: AbortSignal.timeout(2e3) });
|
|
16039
|
-
if (!res.ok) return null;
|
|
16040
|
-
return parseOllamaTags(await res.json());
|
|
16041
|
-
} catch {
|
|
16042
|
-
return null;
|
|
16043
|
-
}
|
|
16044
|
-
}
|
|
16045
|
-
async function available(deps = {}) {
|
|
16046
|
-
const tags = await (deps.tags ?? fetchTags)(OLLAMA_HOST);
|
|
16047
|
-
const model = tags ? pickOllamaModel(tags) : null;
|
|
16048
|
-
const ollama = tags === null ? { provider: "ollama", ready: false, model: null, hint: `no ollama at ${OLLAMA_HOST} \u2014 install it: https://ollama.com/download` } : model === null ? { provider: "ollama", ready: false, model: null, hint: "ollama is running but has no models \u2014 pull one: ollama pull llama3.2:3b" } : { provider: "ollama", ready: true, model };
|
|
16049
|
-
const detected = (deps.detect ?? detectAll)();
|
|
16050
|
-
const harnesses = CATALOG.map((entry) => {
|
|
16051
|
-
const state = detected.find((d) => d.name === entry.name);
|
|
16052
|
-
const usable = state?.status === "ready" || state?.status === "adapter-missing";
|
|
16053
|
-
return usable ? { provider: entry.name, ready: true, model: null } : { provider: entry.name, ready: false, model: null, hint: state?.hint ?? `${entry.label} is not installed.` };
|
|
16054
|
-
});
|
|
16055
|
-
return [ollama, ...harnesses];
|
|
16056
|
-
}
|
|
16057
|
-
function refusal(options, asked) {
|
|
16058
|
-
const lines2 = [
|
|
16059
|
-
asked ? `Can't judge with --via ${asked}: ${options.find((o) => o.provider === asked)?.hint ?? "it isn't available on this machine"}` : "No local judge available \u2014 triage refuses rather than sending your queue to a cloud model.",
|
|
16060
|
-
"",
|
|
16061
|
-
"Triage runs the model where your queue already lives (#1106). Your options:"
|
|
16062
|
-
];
|
|
16063
|
-
for (const o of options) {
|
|
16064
|
-
lines2.push(` ${o.ready ? "\u2713" : "\u2717"} ${o.provider.padEnd(7)} ${PRIVACY[o.provider]}`);
|
|
16065
|
-
if (!o.ready && o.hint) lines2.push(` ${o.hint}`);
|
|
16066
|
-
}
|
|
16067
|
-
lines2.push("", "Then re-run: paigy-harness triage [--via ollama|claude|codex|agy]");
|
|
16068
|
-
return lines2.join("\n");
|
|
16069
|
-
}
|
|
16070
|
-
var RUNNERS = {
|
|
16071
|
-
// Ollama speaks HTTP, not a CLI — handled separately in `judgeFor`.
|
|
16072
|
-
ollama: null,
|
|
16073
|
-
// `claude -p` is print mode: one prompt, one answer, no session. Verified 2026-08-28.
|
|
16074
|
-
claude: { cli: "claude", argv: () => ["-p"] },
|
|
16075
|
-
// `codex exec` is Codex's documented non-interactive run; it reads the prompt from stdin
|
|
16076
|
-
// when none is given as an argument. It prints its own preamble around the answer, which
|
|
16077
|
-
// is exactly why the parser hunts for the JSON rather than trusting the whole of stdout.
|
|
16078
|
-
codex: { cli: "codex", argv: () => ["exec", "--skip-git-repo-check", "-"] },
|
|
16079
|
-
// `agy --print` (alias -p) is Antigravity's print mode, text output by default.
|
|
16080
|
-
agy: { cli: "agy", argv: () => ["-p"] }
|
|
16081
|
-
};
|
|
16082
|
-
var RUN_TIMEOUT_MS = 5 * 6e4;
|
|
16083
|
-
async function judgeFor(asked, deps = {}) {
|
|
16084
|
-
const options = await available(deps);
|
|
16085
|
-
const chosen = asked ? options.find((o) => o.provider === asked) : options.find((o) => o.ready);
|
|
16086
|
-
if (!chosen?.ready) return { error: refusal(options, asked) };
|
|
16087
|
-
if (chosen.provider === "ollama") {
|
|
16088
|
-
const model = chosen.model;
|
|
16089
|
-
return {
|
|
16090
|
-
provider: "ollama",
|
|
16091
|
-
model,
|
|
16092
|
-
run: async (prompt) => {
|
|
16093
|
-
const res = await fetch(`${OLLAMA_HOST}/api/generate`, {
|
|
16094
|
-
method: "POST",
|
|
16095
|
-
headers: { "content-type": "application/json" },
|
|
16096
|
-
// Non-streaming: we want the whole answer once, and the parser is the same
|
|
16097
|
-
// either way. `temperature: 0` because this is a classification, not prose.
|
|
16098
|
-
body: JSON.stringify({ model, prompt, stream: false, options: { temperature: 0 } }),
|
|
16099
|
-
signal: AbortSignal.timeout(RUN_TIMEOUT_MS)
|
|
16100
|
-
});
|
|
16101
|
-
if (!res.ok) throw new Error(`ollama ${res.status}: ${await res.text()}`);
|
|
16102
|
-
return (await res.json()).response ?? "";
|
|
16103
|
-
}
|
|
16104
|
-
};
|
|
16105
|
-
}
|
|
16106
|
-
const runner = RUNNERS[chosen.provider];
|
|
16107
|
-
if (!runner) return { error: refusal(options, asked) };
|
|
16108
|
-
const bin = which(runner.cli);
|
|
16109
|
-
if (!bin) return { error: refusal(options, asked) };
|
|
16110
|
-
return {
|
|
16111
|
-
provider: chosen.provider,
|
|
16112
|
-
model: null,
|
|
16113
|
-
run: async (prompt) => {
|
|
16114
|
-
const out = spawnSync2(bin, runner.argv(null), {
|
|
16115
|
-
input: prompt,
|
|
16116
|
-
encoding: "utf8",
|
|
16117
|
-
cwd: homedir4(),
|
|
16118
|
-
timeout: RUN_TIMEOUT_MS,
|
|
16119
|
-
maxBuffer: 32 * 1024 * 1024,
|
|
16120
|
-
env: { ...process.env, PATH: [process.env["PATH"], ...probeDirs()].filter(Boolean).join(delimiter2) }
|
|
16121
|
-
});
|
|
16122
|
-
if (out.error) throw new Error(`${runner.cli} failed to run: ${out.error.message}`);
|
|
16123
|
-
if (out.status !== 0 && !out.stdout?.trim())
|
|
16124
|
-
throw new Error(`${runner.cli} exited ${out.status ?? "?"}: ${(out.stderr ?? "").trim().slice(0, 500)}`);
|
|
16125
|
-
return out.stdout ?? "";
|
|
16126
|
-
}
|
|
16127
|
-
};
|
|
16128
|
-
}
|
|
16129
|
-
|
|
16130
|
-
// src/triage/verdict.ts
|
|
16131
|
-
function extractJson(raw) {
|
|
16132
|
-
const fenced = raw.match(/```(?:json)?\s*([\s\S]*?)```/i)?.[1];
|
|
16133
|
-
const body = fenced ?? raw;
|
|
16134
|
-
const start = body.indexOf("[");
|
|
16135
|
-
const end = body.lastIndexOf("]");
|
|
16136
|
-
if (start !== -1 && end > start) return body.slice(start, end + 1);
|
|
16137
|
-
const objStart = body.indexOf("{");
|
|
16138
|
-
const objEnd = body.lastIndexOf("}");
|
|
16139
|
-
if (objStart !== -1 && objEnd > objStart) return body.slice(objStart, objEnd + 1);
|
|
16140
|
-
return null;
|
|
16141
|
-
}
|
|
16142
|
-
var VERDICTS = /* @__PURE__ */ new Set(["keep", "stale", "close", "assign"]);
|
|
16143
|
-
function parseVerdicts(raw, knownIds, knownAgents) {
|
|
16144
|
-
const json3 = extractJson(raw);
|
|
16145
|
-
if (!json3) return { ok: false, error: "no JSON found in the model's answer", raw };
|
|
16146
|
-
let parsed;
|
|
16147
|
-
try {
|
|
16148
|
-
parsed = JSON.parse(json3);
|
|
16149
|
-
} catch (e) {
|
|
16150
|
-
return { ok: false, error: `JSON.parse failed: ${e.message}`, raw };
|
|
16151
|
-
}
|
|
16152
|
-
const rows = Array.isArray(parsed) ? parsed : Array.isArray(parsed?.verdicts) ? parsed.verdicts : typeof parsed?.id === "string" ? [parsed] : null;
|
|
16153
|
-
if (!rows) return { ok: false, error: "the JSON was not an array of verdicts", raw };
|
|
16154
|
-
const verdicts = [];
|
|
16155
|
-
const dropped = [];
|
|
16156
|
-
const seen = /* @__PURE__ */ new Set();
|
|
16157
|
-
for (const row of rows) {
|
|
16158
|
-
const r = row;
|
|
16159
|
-
const id3 = typeof r?.id === "string" ? r.id : "";
|
|
16160
|
-
if (!id3) {
|
|
16161
|
-
dropped.push({ row, reason: "no id" });
|
|
16162
|
-
continue;
|
|
16163
|
-
}
|
|
16164
|
-
if (!knownIds.has(id3)) {
|
|
16165
|
-
dropped.push({ row, reason: "id is not a note in this queue" });
|
|
16166
|
-
continue;
|
|
16167
|
-
}
|
|
16168
|
-
if (seen.has(id3)) {
|
|
16169
|
-
dropped.push({ row, reason: "duplicate verdict for this note" });
|
|
16170
|
-
continue;
|
|
16171
|
-
}
|
|
16172
|
-
const verdict = typeof r?.verdict === "string" ? r.verdict.toLowerCase().trim() : "";
|
|
16173
|
-
if (!VERDICTS.has(verdict)) {
|
|
16174
|
-
dropped.push({ row, reason: `unknown verdict ${JSON.stringify(r?.verdict)}` });
|
|
16175
|
-
continue;
|
|
16176
|
-
}
|
|
16177
|
-
const agent2 = typeof r?.agent === "string" ? r.agent.trim() : null;
|
|
16178
|
-
if (verdict === "assign" && (!agent2 || !knownAgents.has(agent2))) {
|
|
16179
|
-
dropped.push({ row, reason: agent2 ? `no such agent: ${agent2}` : "assign with no agent" });
|
|
16180
|
-
continue;
|
|
16181
|
-
}
|
|
16182
|
-
verdicts.push({
|
|
16183
|
-
id: id3,
|
|
16184
|
-
verdict,
|
|
16185
|
-
agent: verdict === "assign" ? agent2 : null,
|
|
16186
|
-
why: typeof r?.why === "string" ? r.why.trim().slice(0, 200) : null
|
|
16187
|
-
});
|
|
16188
|
-
seen.add(id3);
|
|
16189
|
-
}
|
|
16190
|
-
return { ok: true, verdicts, dropped };
|
|
16191
|
-
}
|
|
16192
|
-
function fillMissing(verdicts, notes) {
|
|
16193
|
-
const said = new Set(verdicts.map((v) => v.id));
|
|
16194
|
-
return [...verdicts, ...notes.filter((n) => !said.has(n.id)).map((n) => ({ id: n.id, verdict: "keep", why: null }))];
|
|
16195
|
-
}
|
|
16196
|
-
function buildPrompt(d) {
|
|
16197
|
-
return [
|
|
16198
|
-
"You are triaging a personal task queue. Today is " + d.today + ".",
|
|
16199
|
-
"",
|
|
16200
|
-
"For EVERY note below, decide one of:",
|
|
16201
|
-
' "keep" \u2014 it is still live work. THE DEFAULT.',
|
|
16202
|
-
' "stale" \u2014 nothing has moved on it for a long time and nothing says it is finished.',
|
|
16203
|
-
' "close" \u2014 there is evidence it is already done or no longer wanted.',
|
|
16204
|
-
' "assign" \u2014 it should be handed to one of the agents listed, and you name which.',
|
|
16205
|
-
"",
|
|
16206
|
-
"The counts and dates below are already computed. Use them; do not recompute them.",
|
|
16207
|
-
"",
|
|
16208
|
-
"AGENTS (assign only to one of these tokens):",
|
|
16209
|
-
JSON.stringify(d.agents, null, 1),
|
|
16210
|
-
"",
|
|
16211
|
-
"NOTES:",
|
|
16212
|
-
JSON.stringify(d.notes, null, 1),
|
|
16213
|
-
"",
|
|
16214
|
-
"Answer with a JSON array and NOTHING else. One object per note:",
|
|
16215
|
-
'[{"id":"<note id>","verdict":"keep|stale|close|assign","agent":"<agent token, only for assign>","why":"<max 12 words>"}]',
|
|
16216
|
-
"",
|
|
16217
|
-
'The "why" is read on a phone under the note title. Evidence first, no ids, no preamble.',
|
|
16218
|
-
'Good: "no movement in 34 days" / "worked 3 notes in this repo this week" / "a later note says this shipped".',
|
|
16219
|
-
"",
|
|
16220
|
-
`When you are not sure, answer "keep". A wrong "keep" costs nothing; a wrong "close" loses the user's work.`
|
|
16221
|
-
].join("\n");
|
|
16222
|
-
}
|
|
16223
|
-
|
|
16224
|
-
// src/triage/triage.ts
|
|
16225
|
-
function toProposal(verdicts, d, meta) {
|
|
16226
|
-
const byId = new Map(d.notes.map((n) => [n.id, n]));
|
|
16227
|
-
const agentById = new Map(d.agents.map((a) => [a.token, a]));
|
|
16228
|
-
const close = [];
|
|
16229
|
-
const stale = [];
|
|
16230
|
-
const assignments = /* @__PURE__ */ new Map();
|
|
16231
|
-
for (const v of verdicts) {
|
|
16232
|
-
const note2 = byId.get(v.id);
|
|
16233
|
-
if (!note2) continue;
|
|
16234
|
-
const item = (why) => ({ noteId: note2.id, title: note2.title, why });
|
|
16235
|
-
if (v.verdict === "close") {
|
|
16236
|
-
close.push(item(staleWhy(note2) ?? v.why ?? "looks finished"));
|
|
16237
|
-
continue;
|
|
16238
|
-
}
|
|
16239
|
-
if (v.verdict === "stale") {
|
|
16240
|
-
stale.push(item(staleWhy(note2) ?? v.why ?? `no movement in ${note2.idleDays} days`));
|
|
16241
|
-
continue;
|
|
16242
|
-
}
|
|
16243
|
-
if (v.verdict !== "assign" || !v.agent) continue;
|
|
16244
|
-
const agent2 = agentById.get(v.agent);
|
|
16245
|
-
if (!agent2) continue;
|
|
16246
|
-
const group = assignments.get(agent2.token) ?? { agent: agent2.token, agentName: agent2.name, notes: [] };
|
|
16247
|
-
group.notes.push(item(affinityWhy(note2, agent2) ?? v.why ?? "best match for this work"));
|
|
16248
|
-
assignments.set(agent2.token, group);
|
|
16249
|
-
}
|
|
16250
|
-
return {
|
|
16251
|
-
provider: meta.provider,
|
|
16252
|
-
model: meta.model,
|
|
16253
|
-
reviewed: d.notes.length,
|
|
16254
|
-
close,
|
|
16255
|
-
stale,
|
|
16256
|
-
assign: [...assignments.values()]
|
|
16257
|
-
};
|
|
16258
|
-
}
|
|
15715
|
+
// src/ago.ts
|
|
16259
15716
|
var plural = (n, one, many = `${one}s`) => `${n} ${n === 1 ? one : many}`;
|
|
16260
|
-
function suggested(run) {
|
|
16261
|
-
return run.close.length + run.stale.length + run.assign.reduce((n, a) => n + a.notes.length, 0);
|
|
16262
|
-
}
|
|
16263
|
-
function renderTable(run) {
|
|
16264
|
-
const lines2 = [];
|
|
16265
|
-
const width = Math.max(
|
|
16266
|
-
24,
|
|
16267
|
-
...[...run.close, ...run.stale, ...run.assign.flatMap((a) => a.notes)].map((n) => Math.min(n.title.length, 48))
|
|
16268
|
-
);
|
|
16269
|
-
const row = (n) => ` ${n.title.slice(0, width).padEnd(width)} ${n.why}`;
|
|
16270
|
-
if (run.close.length) {
|
|
16271
|
-
lines2.push(`Paigy thinks you should CLOSE ${plural(run.close.length, "note")}:`);
|
|
16272
|
-
lines2.push(...run.close.map(row), "");
|
|
16273
|
-
}
|
|
16274
|
-
if (run.stale.length) {
|
|
16275
|
-
lines2.push(`Paigy thinks ${plural(run.stale.length, "note")} ${run.stale.length === 1 ? "has" : "have"} gone STALE:`);
|
|
16276
|
-
lines2.push(...run.stale.map(row), "");
|
|
16277
|
-
}
|
|
16278
|
-
for (const group of run.assign) {
|
|
16279
|
-
lines2.push(`Paigy thinks you should ASSIGN ${plural(group.notes.length, "note")} to ${group.agentName}:`);
|
|
16280
|
-
lines2.push(...group.notes.map(row), "");
|
|
16281
|
-
}
|
|
16282
|
-
const acted = suggested(run);
|
|
16283
|
-
lines2.push(`Reviewed ${plural(run.reviewed, "open note")} \xB7 ${plural(acted, "suggestion")} \xB7 ${run.reviewed - acted} left alone`);
|
|
16284
|
-
return lines2.join("\n");
|
|
16285
|
-
}
|
|
16286
|
-
async function confirm(question) {
|
|
16287
|
-
if (!process.stdin.isTTY) return false;
|
|
16288
|
-
const readline = await import("readline/promises");
|
|
16289
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
16290
|
-
try {
|
|
16291
|
-
return /^(y|yes)$/i.test((await rl.question(`${question} [y/N] `)).trim());
|
|
16292
|
-
} finally {
|
|
16293
|
-
rl.close();
|
|
16294
|
-
}
|
|
16295
|
-
}
|
|
16296
|
-
async function runTriage(opts, deps = {}) {
|
|
16297
|
-
const { log: log2 } = opts;
|
|
16298
|
-
const now = (deps.now ?? Date.now)();
|
|
16299
|
-
const judge = await (deps.judge ?? judgeFor)(opts.via);
|
|
16300
|
-
if ("error" in judge) {
|
|
16301
|
-
log2(judge.error);
|
|
16302
|
-
return 1;
|
|
16303
|
-
}
|
|
16304
|
-
const [{ notes }, { items }] = await Promise.all([
|
|
16305
|
-
(deps.notes ?? listNotes)(),
|
|
16306
|
-
(deps.connections ?? listConnections)()
|
|
16307
|
-
]);
|
|
16308
|
-
const d = dossier(notes, items, now);
|
|
16309
|
-
if (d.notes.length === 0) {
|
|
16310
|
-
log2("Nothing open in the queue \u2014 nothing to triage.");
|
|
16311
|
-
return 0;
|
|
16312
|
-
}
|
|
16313
|
-
if (!opts.json)
|
|
16314
|
-
log2(`Reading ${plural(d.notes.length, "open note")} with ${judge.provider}${judge.model ? ` (${judge.model})` : ""} \u2014 ${PRIVACY[judge.provider]}\u2026`);
|
|
16315
|
-
const raw = await judge.run(buildPrompt(d));
|
|
16316
|
-
const parsed = parseVerdicts(
|
|
16317
|
-
raw,
|
|
16318
|
-
new Set(d.notes.map((n) => n.id)),
|
|
16319
|
-
new Set(d.agents.map((a) => a.token))
|
|
16320
|
-
);
|
|
16321
|
-
if (!parsed.ok) {
|
|
16322
|
-
log2(`The model's answer couldn't be read: ${parsed.error}`);
|
|
16323
|
-
log2("--- raw output ---");
|
|
16324
|
-
log2(parsed.raw.slice(0, 4e3));
|
|
16325
|
-
log2("--- end ---");
|
|
16326
|
-
log2(`Try a different judge: paigy-harness triage --via ${judge.provider === "ollama" ? "claude" : "ollama"}`);
|
|
16327
|
-
return 1;
|
|
16328
|
-
}
|
|
16329
|
-
if (parsed.dropped.length && !opts.json)
|
|
16330
|
-
log2(`(dropped ${plural(parsed.dropped.length, "unusable row")}: ${[...new Set(parsed.dropped.map((x) => x.reason))].join("; ")})`);
|
|
16331
|
-
const run = toProposal(fillMissing(parsed.verdicts, d.notes), d, { provider: judge.provider, model: judge.model });
|
|
16332
|
-
if (opts.json) {
|
|
16333
|
-
log2(JSON.stringify(run, null, 2));
|
|
16334
|
-
return 0;
|
|
16335
|
-
}
|
|
16336
|
-
log2("");
|
|
16337
|
-
log2(renderTable(run));
|
|
16338
|
-
log2("");
|
|
16339
|
-
const proposal = await (deps.submit ?? submitTriage)(run);
|
|
16340
|
-
const acted = suggested(run);
|
|
16341
|
-
log2(acted === 0 ? "Nothing to suggest \u2014 sent, so your phone shows the queue as clean." : `Sent to your phone \u2014 accept it there (${plural(acted, "suggestion")}).`);
|
|
16342
|
-
if (!opts.apply) return 0;
|
|
16343
|
-
const accept = deps.accept ?? ((id3, body) => acceptTriage(id3, body));
|
|
16344
|
-
const groups = [
|
|
16345
|
-
{ body: { group: "close" }, label: `close ${plural(run.close.length, "note")}`, count: run.close.length, preapproved: opts.yes.close },
|
|
16346
|
-
{ body: { group: "stale" }, label: `close ${plural(run.stale.length, "stale note")}`, count: run.stale.length, preapproved: opts.yes.stale },
|
|
16347
|
-
...run.assign.map((a) => ({
|
|
16348
|
-
body: { group: "assign", agent: a.agent },
|
|
16349
|
-
label: `assign ${plural(a.notes.length, "note")} to ${a.agentName}`,
|
|
16350
|
-
count: a.notes.length,
|
|
16351
|
-
preapproved: opts.yes.assign
|
|
16352
|
-
}))
|
|
16353
|
-
];
|
|
16354
|
-
for (const g of groups) {
|
|
16355
|
-
if (g.count === 0) continue;
|
|
16356
|
-
if (!g.preapproved && !await confirm(`${g.label}?`)) {
|
|
16357
|
-
log2(` skipped: ${g.label}`);
|
|
16358
|
-
continue;
|
|
16359
|
-
}
|
|
16360
|
-
const result = await accept(proposal.id, g.body);
|
|
16361
|
-
log2(` \u2713 ${g.label} \u2014 ${result.accepted.length} done${result.failed.length ? `, ${result.failed.length} failed (${result.failed.map((f) => f.reason).join(", ")})` : ""}`);
|
|
16362
|
-
}
|
|
16363
|
-
return 0;
|
|
16364
|
-
}
|
|
16365
|
-
|
|
16366
|
-
// src/broker.ts
|
|
16367
|
-
var plural2 = (n, one, many = `${one}s`) => `${n} ${n === 1 ? one : many}`;
|
|
16368
15717
|
function ago(iso, now) {
|
|
16369
15718
|
const ms = now - Date.parse(iso);
|
|
16370
15719
|
if (!Number.isFinite(ms) || ms < 6e4) return "just now";
|
|
16371
15720
|
const minutes = Math.floor(ms / 6e4);
|
|
16372
|
-
if (minutes < 60) return `${
|
|
15721
|
+
if (minutes < 60) return `${plural(minutes, "minute")} ago`;
|
|
16373
15722
|
const hours = Math.floor(minutes / 60);
|
|
16374
|
-
if (hours < 24) return `${
|
|
16375
|
-
return `${
|
|
16376
|
-
}
|
|
16377
|
-
function standing(queue, proposal, now) {
|
|
16378
|
-
const head2 = queue.length === 0 ? "Nothing open \u2014 the queue is clear" : `Holding ${plural2(queue.length, "open note")}`;
|
|
16379
|
-
if (!proposal) return `${head2} \xB7 no sweep waiting`;
|
|
16380
|
-
const n = suggested(proposal);
|
|
16381
|
-
return n === 0 ? `${head2} \xB7 swept ${ago(proposal.runAt, now)}, nothing to suggest` : `${head2} \xB7 ${plural2(n, "suggestion")} waiting on you, swept ${ago(proposal.runAt, now)}`;
|
|
16382
|
-
}
|
|
16383
|
-
async function brokerView(deps) {
|
|
16384
|
-
const now = (deps.now ?? Date.now)();
|
|
16385
|
-
try {
|
|
16386
|
-
const [{ notes }, { items }, { proposal }] = await Promise.all([deps.notes(), deps.connections(), deps.triage()]);
|
|
16387
|
-
const queue = noteSignals(notes, items, now).map((n) => ({ ...n, why: staleWhy(n) })).sort((a, b) => b.idleDays - a.idleDays || b.ageDays - a.ageDays);
|
|
16388
|
-
return {
|
|
16389
|
-
standing: standing(queue, proposal, now),
|
|
16390
|
-
queue,
|
|
16391
|
-
proposal,
|
|
16392
|
-
voices: Object.fromEntries(items.map((c) => [c.id, c.voice ?? null])),
|
|
16393
|
-
error: null
|
|
16394
|
-
};
|
|
16395
|
-
} catch (e) {
|
|
16396
|
-
return {
|
|
16397
|
-
standing: "Couldn't read the queue just now.",
|
|
16398
|
-
queue: [],
|
|
16399
|
-
proposal: null,
|
|
16400
|
-
voices: {},
|
|
16401
|
-
error: e instanceof Error ? e.message : String(e)
|
|
16402
|
-
};
|
|
16403
|
-
}
|
|
15723
|
+
if (hours < 24) return `${plural(hours, "hour")} ago`;
|
|
15724
|
+
return `${plural(Math.floor(hours / 24), "day")} ago`;
|
|
16404
15725
|
}
|
|
16405
15726
|
|
|
16406
15727
|
// src/workspaces.ts
|
|
16407
15728
|
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readdirSync, readFileSync as readFileSync3, statSync as statSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
16408
15729
|
import { dirname as dirname2, join as join5, resolve } from "path";
|
|
16409
|
-
import { homedir as
|
|
15730
|
+
import { homedir as homedir4 } from "os";
|
|
16410
15731
|
function workspacesFile() {
|
|
16411
|
-
return join5(
|
|
15732
|
+
return join5(homedir4(), ".paigy", "workspaces.json");
|
|
16412
15733
|
}
|
|
16413
15734
|
function listWorkspaces(deps) {
|
|
16414
15735
|
try {
|
|
@@ -16419,7 +15740,7 @@ function listWorkspaces(deps) {
|
|
|
16419
15740
|
}
|
|
16420
15741
|
}
|
|
16421
15742
|
function addWorkspace(dir, deps) {
|
|
16422
|
-
const next = resolve(dir.replace(/^~(?=$|\/)/,
|
|
15743
|
+
const next = resolve(dir.replace(/^~(?=$|\/)/, homedir4()));
|
|
16423
15744
|
const all = listWorkspaces(deps);
|
|
16424
15745
|
if (!all.includes(next)) {
|
|
16425
15746
|
all.push(next);
|
|
@@ -16435,12 +15756,12 @@ function removeWorkspace(dir, deps) {
|
|
|
16435
15756
|
return all;
|
|
16436
15757
|
}
|
|
16437
15758
|
function allowed(cwd, deps) {
|
|
16438
|
-
const target = resolve(cwd.replace(/^~(?=$|\/)/,
|
|
15759
|
+
const target = resolve(cwd.replace(/^~(?=$|\/)/, homedir4()));
|
|
16439
15760
|
return listWorkspaces(deps).some((w) => target === w || target.startsWith(`${w}/`));
|
|
16440
15761
|
}
|
|
16441
15762
|
function resolveWakeDir(pinned, deps) {
|
|
16442
15763
|
if (pinned && allowed(pinned, deps)) {
|
|
16443
|
-
const dir = resolve(pinned.replace(/^~(?=$|\/)/,
|
|
15764
|
+
const dir = resolve(pinned.replace(/^~(?=$|\/)/, homedir4()));
|
|
16444
15765
|
if ((deps.exists ?? existsSync3)(dir)) return dir;
|
|
16445
15766
|
}
|
|
16446
15767
|
return listWorkspaces(deps)[0];
|
|
@@ -16480,7 +15801,7 @@ function listRepos(deps) {
|
|
|
16480
15801
|
// src/holder.ts
|
|
16481
15802
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
16482
15803
|
import { readFileSync as readFileSync4, readdirSync as readdirSync2 } from "fs";
|
|
16483
|
-
import { homedir as
|
|
15804
|
+
import { homedir as homedir5 } from "os";
|
|
16484
15805
|
import { join as join6 } from "path";
|
|
16485
15806
|
function isAlive(pid) {
|
|
16486
15807
|
try {
|
|
@@ -16519,7 +15840,7 @@ function proven(pid, procStart, deps) {
|
|
|
16519
15840
|
});
|
|
16520
15841
|
}
|
|
16521
15842
|
function localHolderPids(deps = {}) {
|
|
16522
|
-
const dir = deps.dir ?? join6(process.env.CLAUDE_CONFIG_DIR?.trim() || join6(
|
|
15843
|
+
const dir = deps.dir ?? join6(process.env.CLAUDE_CONFIG_DIR?.trim() || join6(homedir5(), ".claude"), "sessions");
|
|
16523
15844
|
const list = deps.list ?? ((d) => readdirSync2(d));
|
|
16524
15845
|
const read3 = deps.read ?? ((f) => readFileSync4(f, "utf8"));
|
|
16525
15846
|
const check = { alive: deps.alive ?? isAlive, started: deps.started ?? psStart };
|
|
@@ -16568,7 +15889,7 @@ function descendsFrom(pid, ancestor, parent = psParent) {
|
|
|
16568
15889
|
return false;
|
|
16569
15890
|
}
|
|
16570
15891
|
function localHolders(deps = {}) {
|
|
16571
|
-
const dir = deps.dir ?? join6(process.env.CLAUDE_CONFIG_DIR?.trim() || join6(
|
|
15892
|
+
const dir = deps.dir ?? join6(process.env.CLAUDE_CONFIG_DIR?.trim() || join6(homedir5(), ".claude"), "sessions");
|
|
16572
15893
|
const list = deps.list ?? ((d) => readdirSync2(d));
|
|
16573
15894
|
const read3 = deps.read ?? ((f) => readFileSync4(f, "utf8"));
|
|
16574
15895
|
const check = { alive: deps.alive ?? isAlive, started: deps.started ?? psStart };
|
|
@@ -16661,7 +15982,7 @@ async function endSession(end, deps) {
|
|
|
16661
15982
|
|
|
16662
15983
|
// src/owner.ts
|
|
16663
15984
|
import { closeSync as closeSync2, openSync as openSync2, readSync, readdirSync as readdirSync3 } from "fs";
|
|
16664
|
-
import { homedir as
|
|
15985
|
+
import { homedir as homedir6 } from "os";
|
|
16665
15986
|
import { join as join7 } from "path";
|
|
16666
15987
|
var HEAD_BYTES = 64e3;
|
|
16667
15988
|
function readHead(file) {
|
|
@@ -16678,7 +15999,7 @@ function ownerSession(slot, deps = {}) {
|
|
|
16678
15999
|
const m = /^session:([0-9a-f]{8})$/.exec(slot);
|
|
16679
16000
|
if (!m) return null;
|
|
16680
16001
|
const prefix = m[1];
|
|
16681
|
-
const root = deps.projectsDir ?? join7(
|
|
16002
|
+
const root = deps.projectsDir ?? join7(homedir6(), ".claude", "projects");
|
|
16682
16003
|
const list = deps.list ?? ((dir) => readdirSync3(dir));
|
|
16683
16004
|
const head2 = deps.head ?? readHead;
|
|
16684
16005
|
let folders;
|
|
@@ -16741,8 +16062,8 @@ import { randomUUID as randomUUID4 } from "crypto";
|
|
|
16741
16062
|
// src/harness/session.ts
|
|
16742
16063
|
import { spawn } from "child_process";
|
|
16743
16064
|
import { existsSync as existsSync4, readFileSync as readFileSync5 } from "fs";
|
|
16744
|
-
import { homedir as
|
|
16745
|
-
import { resolve as resolve2, delimiter as
|
|
16065
|
+
import { homedir as homedir8 } from "os";
|
|
16066
|
+
import { resolve as resolve2, delimiter as delimiter2, join as join9 } from "path";
|
|
16746
16067
|
|
|
16747
16068
|
// src/harness/acp.ts
|
|
16748
16069
|
var none = { events: [], writes: [] };
|
|
@@ -17019,7 +16340,7 @@ var frame2 = (body) => JSON.stringify({ jsonrpc: "2.0", ...body });
|
|
|
17019
16340
|
// src/paigy/inbox.ts
|
|
17020
16341
|
import { mkdirSync as mkdirSync3, readdirSync as readdirSync4, renameSync, rmSync as rmSync2, statSync as statSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
17021
16342
|
import { join as join8 } from "path";
|
|
17022
|
-
import { homedir as
|
|
16343
|
+
import { homedir as homedir7 } from "os";
|
|
17023
16344
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
17024
16345
|
var MIDTURN_HOOK = `d="$PAIGY_INBOX"; [ -n "$d" ] || { cat >/dev/null; exit 0; }; set -- "$d"/*.note; [ -e "$1" ] || { cat >/dev/null; exit 0; }; grep '"agent_id"' >/dev/null && exit 0; mkdir -p "$d/taken"; o=""; for f in "$@"; do n=\${f##*/}; mv "$f" "$d/taken/$n" 2>/dev/null || continue; touch "$d/taken/$n"; o="$o\${o:+\\n\\n}$(cat "$d/taken/$n")"; done; [ -n "$o" ] || exit 0; printf '{"hookSpecificOutput":{"hookEventName":"PostToolUse","additionalContext":"%s"}}' "$o"`;
|
|
17025
16346
|
var missing = (error) => error.code === "ENOENT";
|
|
@@ -17062,7 +16383,7 @@ function openInbox(dir) {
|
|
|
17062
16383
|
}
|
|
17063
16384
|
};
|
|
17064
16385
|
}
|
|
17065
|
-
var INBOX_ROOT = join8(
|
|
16386
|
+
var INBOX_ROOT = join8(homedir7(), ".paigy", "inbox");
|
|
17066
16387
|
var alive = (pid) => {
|
|
17067
16388
|
try {
|
|
17068
16389
|
process.kill(pid, 0);
|
|
@@ -17109,14 +16430,15 @@ function splitLines(buffer, chunk) {
|
|
|
17109
16430
|
return { lines: parts.filter((l) => l.trim()), rest };
|
|
17110
16431
|
}
|
|
17111
16432
|
function startSession(opts) {
|
|
17112
|
-
const cwd = resolve2(opts.cwd.replace(/^~(?=$|\/)/,
|
|
16433
|
+
const cwd = resolve2(opts.cwd.replace(/^~(?=$|\/)/, homedir8()));
|
|
17113
16434
|
if (!existsSync4(cwd)) {
|
|
17114
16435
|
queueMicrotask(() => opts.onEvent({ kind: "error", message: `workspace does not exist: ${cwd}` }));
|
|
17115
16436
|
}
|
|
17116
|
-
|
|
16437
|
+
let codexConfig = opts.harness === "codex" ? JSON.parse(process.env.CODEX_CONFIG || "{}") : null;
|
|
16438
|
+
if (opts.model && opts.harness === "codex") codexConfig = { ...codexConfig, model: opts.model };
|
|
17117
16439
|
if (codexConfig) {
|
|
17118
16440
|
const key3 = "mcp_servers.paigy.env_vars";
|
|
17119
|
-
const configPath = join9(process.env.CODEX_HOME || join9(
|
|
16441
|
+
const configPath = join9(process.env.CODEX_HOME || join9(homedir8(), ".codex"), "config.toml");
|
|
17120
16442
|
const saved = existsSync4(configPath) ? readFileSync5(configPath, "utf8") : "";
|
|
17121
16443
|
const registered = /\[mcp_servers\.paigy\][\s\S]*?(?=\n\[|$)/.exec(saved)?.[0];
|
|
17122
16444
|
const configured = codexConfig["mcp_servers.paigy.command"] || codexConfig["mcp_servers.paigy.url"] || codexConfig.mcp_servers?.paigy?.command || codexConfig.mcp_servers?.paigy?.url;
|
|
@@ -17136,9 +16458,11 @@ function startSession(opts) {
|
|
|
17136
16458
|
// PATH knows neither — live catch 2026-08-04: the first slot-wake ENOENT'd).
|
|
17137
16459
|
env: {
|
|
17138
16460
|
...process.env,
|
|
17139
|
-
PATH: [process.env.PATH, ...probeDirs()].filter(Boolean).join(
|
|
16461
|
+
PATH: [process.env.PATH, ...probeDirs()].filter(Boolean).join(delimiter2),
|
|
17140
16462
|
...opts.token ? { PAIGY_TOKEN: opts.token } : {},
|
|
17141
16463
|
...opts.instance ? { PAIGY_INSTANCE_ID: opts.instance } : {},
|
|
16464
|
+
...opts.model && opts.harness === "claude" ? { CLAUDE_MODEL_CONFIG: JSON.stringify({ provider: "anthropic", model: opts.model }) } : {},
|
|
16465
|
+
...opts.model && opts.harness === "agy" ? { AGY_MODEL_CONFIG: JSON.stringify({ model: opts.model }) } : {},
|
|
17142
16466
|
...codexConfig ? { CODEX_CONFIG: JSON.stringify(codexConfig) } : {},
|
|
17143
16467
|
...opts.session ?? opts.resume ? { PAIGY_SESSION_ID: opts.session ?? opts.resume } : {},
|
|
17144
16468
|
// A PUMP DRIVES THIS PROCESS (2026-09-22, #2265): every run `runHarness` starts has the
|
|
@@ -17153,19 +16477,56 @@ function startSession(opts) {
|
|
|
17153
16477
|
stdio: ["pipe", "pipe", "pipe"]
|
|
17154
16478
|
});
|
|
17155
16479
|
let ended2 = false;
|
|
17156
|
-
|
|
17157
|
-
|
|
16480
|
+
let failure;
|
|
16481
|
+
let hush;
|
|
16482
|
+
const pending = /* @__PURE__ */ new Set();
|
|
16483
|
+
const end = (error) => {
|
|
16484
|
+
ended2 = true;
|
|
16485
|
+
failure ??= error;
|
|
16486
|
+
clearTimeout(hush);
|
|
16487
|
+
for (const receipt of pending) receipt.reject(failure);
|
|
16488
|
+
pending.clear();
|
|
16489
|
+
};
|
|
16490
|
+
const broken = (error) => {
|
|
16491
|
+
if (ended2) return;
|
|
16492
|
+
end(new Error(`session_write_failed: ${error.message}`));
|
|
16493
|
+
opts.onEvent({ kind: "error", message: failure.message });
|
|
16494
|
+
child.kill();
|
|
16495
|
+
};
|
|
16496
|
+
child.stdin?.on("error", broken);
|
|
17158
16497
|
const write = (frames) => {
|
|
17159
16498
|
if (ended2) return;
|
|
17160
|
-
for (const f of frames)
|
|
17161
|
-
|
|
17162
|
-
|
|
16499
|
+
for (const f of frames) {
|
|
16500
|
+
if (ended2) break;
|
|
16501
|
+
if (!child.stdin || child.stdin.destroyed || !child.stdin.writable) {
|
|
16502
|
+
broken(new Error("stdin unavailable"));
|
|
16503
|
+
break;
|
|
16504
|
+
}
|
|
16505
|
+
const frame3 = JSON.parse(f);
|
|
16506
|
+
const receipts2 = frame3.method === "session/prompt" ? [...pending].filter((receipt) => !receipt.writing).slice(0, frame3.params.prompt.length) : [];
|
|
16507
|
+
for (const receipt of receipts2) receipt.writing = true;
|
|
16508
|
+
try {
|
|
16509
|
+
child.stdin.write(`${f}
|
|
16510
|
+
`, (error) => {
|
|
16511
|
+
if (error) {
|
|
16512
|
+
broken(error);
|
|
16513
|
+
return;
|
|
16514
|
+
}
|
|
16515
|
+
if (ended2) return;
|
|
16516
|
+
for (const receipt of receipts2) if (pending.delete(receipt)) receipt.resolve();
|
|
16517
|
+
});
|
|
16518
|
+
} catch (error) {
|
|
16519
|
+
broken(error instanceof Error ? error : new Error(String(error)));
|
|
16520
|
+
}
|
|
16521
|
+
}
|
|
16522
|
+
if (frames.length && !ended2) listen();
|
|
17163
16523
|
};
|
|
17164
16524
|
const apply = ({ events, writes }) => {
|
|
16525
|
+
const fatal = events.find((event) => event.kind === "error" && event.fatal);
|
|
16526
|
+
if (fatal?.kind === "error") end(new Error(fatal.message));
|
|
17165
16527
|
write(writes);
|
|
17166
16528
|
for (const event of events) opts.onEvent(event);
|
|
17167
16529
|
};
|
|
17168
|
-
let hush;
|
|
17169
16530
|
const listen = () => {
|
|
17170
16531
|
clearTimeout(hush);
|
|
17171
16532
|
if (!ended2) hush = setTimeout(() => apply(driver.quiet(QUIET_MS)), QUIET_MS);
|
|
@@ -17175,12 +16536,12 @@ function startSession(opts) {
|
|
|
17175
16536
|
if (text) opts.onEvent({ kind: "error", message: text });
|
|
17176
16537
|
});
|
|
17177
16538
|
child.on("exit", (code) => {
|
|
17178
|
-
|
|
17179
|
-
clearTimeout(hush);
|
|
16539
|
+
end(new Error(`session_ended: ${code ?? "signal"}`));
|
|
17180
16540
|
opts.onEvent({ kind: "idle", failed: code !== 0, ...code ? { result: `exited ${code}` } : {} });
|
|
17181
16541
|
opts.onExit?.();
|
|
17182
16542
|
});
|
|
17183
16543
|
child.on("error", (e) => {
|
|
16544
|
+
end(new Error(`session_start_failed: ${e.message}`));
|
|
17184
16545
|
opts.onEvent({ kind: "error", message: e.message });
|
|
17185
16546
|
opts.onExit?.();
|
|
17186
16547
|
});
|
|
@@ -17200,15 +16561,24 @@ function startSession(opts) {
|
|
|
17200
16561
|
write(driver.open());
|
|
17201
16562
|
return {
|
|
17202
16563
|
send(text) {
|
|
17203
|
-
|
|
16564
|
+
const sent = new Promise((resolve3, reject) => {
|
|
16565
|
+
if (ended2) {
|
|
16566
|
+
reject(failure ?? new Error("session_ended"));
|
|
16567
|
+
return;
|
|
16568
|
+
}
|
|
16569
|
+
pending.add({ resolve: resolve3, reject, writing: false });
|
|
16570
|
+
write(driver.send(text));
|
|
16571
|
+
});
|
|
16572
|
+
void sent.catch(() => {
|
|
16573
|
+
});
|
|
16574
|
+
return sent;
|
|
17204
16575
|
},
|
|
17205
16576
|
respond(id3, decision) {
|
|
17206
16577
|
write(driver.respond(id3, decision));
|
|
17207
16578
|
},
|
|
17208
16579
|
stop() {
|
|
17209
16580
|
write(driver.close());
|
|
17210
|
-
|
|
17211
|
-
clearTimeout(hush);
|
|
16581
|
+
end(new Error("session_stopped"));
|
|
17212
16582
|
child.kill();
|
|
17213
16583
|
}
|
|
17214
16584
|
};
|
|
@@ -17368,12 +16738,13 @@ var LIMITS = episodes();
|
|
|
17368
16738
|
// src/paigy/bridge.ts
|
|
17369
16739
|
function railFor(opts) {
|
|
17370
16740
|
return {
|
|
17371
|
-
next: async () => {
|
|
17372
|
-
const waiting = await receive(opts);
|
|
16741
|
+
next: async (alreadyRead) => {
|
|
16742
|
+
const waiting = alreadyRead ?? await receive(opts);
|
|
17373
16743
|
const goalId = waiting.claimable?.goalId ?? waiting.review?.[0]?.goalId;
|
|
17374
16744
|
return goalId ? getGoals([goalId], opts).then(([goal]) => goal) : { state: "idle", message: "" };
|
|
17375
16745
|
},
|
|
17376
16746
|
read: (goalId) => getGoals([goalId], opts).then(([goal]) => goal),
|
|
16747
|
+
delivery: (deliveryId) => readDelivery(deliveryId, opts),
|
|
17377
16748
|
complete: async (goalId, revision) => {
|
|
17378
16749
|
const done = await manageGoals({ changes: [{ kind: "state", goalId, state: "completed", ...revision !== void 0 ? { revision } : {} }] }, opts);
|
|
17379
16750
|
const result = done.results[0];
|
|
@@ -17554,9 +16925,9 @@ function beat() {
|
|
|
17554
16925
|
}
|
|
17555
16926
|
var BEAT_REPORT_MS = 10 * 6e4;
|
|
17556
16927
|
var ended = (state) => state === "done" || state === "cancelled";
|
|
17557
|
-
function unheard(brief, state, since = null, after = -1, reviewSince) {
|
|
16928
|
+
function unheard(brief, state, since = null, after = -1, reviewSince, gathered) {
|
|
17558
16929
|
const seen = state.seen ??= /* @__PURE__ */ new Set();
|
|
17559
|
-
const
|
|
16930
|
+
const words = [];
|
|
17560
16931
|
const pendingAt = reviewSince ? Date.parse(reviewSince) : null;
|
|
17561
16932
|
for (const [i, e] of (brief.entries ?? []).entries()) {
|
|
17562
16933
|
if (seen.has(e.entryId) || !e.authorParticipant.startsWith("human:")) continue;
|
|
@@ -17569,18 +16940,21 @@ function unheard(brief, state, since = null, after = -1, reviewSince) {
|
|
|
17569
16940
|
continue;
|
|
17570
16941
|
}
|
|
17571
16942
|
if (since !== null && Date.parse(e.createdAt) <= Date.parse(since)) continue;
|
|
17572
|
-
seen.add(e.entryId);
|
|
17573
16943
|
if (e.aboutId && seen.has(e.aboutId) && brief.answers?.some((a) => a.requestEntryId === e.aboutId)) continue;
|
|
16944
|
+
seen.add(e.entryId);
|
|
17574
16945
|
const text = entryWords(e).trim();
|
|
17575
|
-
if (text)
|
|
16946
|
+
if (text) {
|
|
16947
|
+
words.push(text);
|
|
16948
|
+
gathered?.add(e.entryId);
|
|
16949
|
+
}
|
|
17576
16950
|
}
|
|
17577
16951
|
if (pendingAt !== null) for (const answer of brief.answers ?? []) {
|
|
17578
16952
|
const key3 = `answer:${answer.decisionNeedId}`;
|
|
17579
16953
|
if (seen.has(key3) || Date.parse(answer.acceptedAt) < pendingAt) continue;
|
|
17580
16954
|
seen.add(key3);
|
|
17581
|
-
|
|
16955
|
+
words.push(`Answer to question ${answer.decisionNeedId}: ${JSON.stringify(answer.result)}`);
|
|
17582
16956
|
}
|
|
17583
|
-
return
|
|
16957
|
+
return words;
|
|
17584
16958
|
}
|
|
17585
16959
|
function sinceTheWork(brief) {
|
|
17586
16960
|
const entries = brief.entries ?? [];
|
|
@@ -17613,12 +16987,28 @@ function named(goalId, title, brief) {
|
|
|
17613
16987
|
const name = line.length > 80 ? `${line.slice(0, 79)}\u2026` : line;
|
|
17614
16988
|
return name ? `On Goal "${name}" (${goalId}):` : `On Goal ${goalId}:`;
|
|
17615
16989
|
}
|
|
17616
|
-
async function gather(state, rail, held) {
|
|
17617
|
-
const looks = /* @__PURE__ */ new Map(
|
|
16990
|
+
async function gather(state, rail, held, alreadyRead, log2) {
|
|
16991
|
+
const looks = /* @__PURE__ */ new Map();
|
|
16992
|
+
if (held && state.goal) looks.set(state.goal.id, { via: "held", brief: held });
|
|
17618
16993
|
const look2 = (goalId, via, title, since) => {
|
|
17619
16994
|
if (!looks.has(goalId)) looks.set(goalId, { via, ...title ? { title } : {}, ...since !== void 0 ? { since } : {} });
|
|
17620
16995
|
};
|
|
17621
|
-
const waiting = await rail.waiting().catch(() => null);
|
|
16996
|
+
const waiting = alreadyRead ?? await rail.waiting().catch(() => null);
|
|
16997
|
+
const direct = [];
|
|
16998
|
+
const staged = new Set(state.seen);
|
|
16999
|
+
const deliveries = /* @__PURE__ */ new Set();
|
|
17000
|
+
for (const d of waiting?.deliveries ?? []) {
|
|
17001
|
+
if (d.goalIds.length || deliveries.has(d.deliveryId) || state.acked?.get(`delivery:${d.deliveryId}`) === "handled") continue;
|
|
17002
|
+
deliveries.add(d.deliveryId);
|
|
17003
|
+
try {
|
|
17004
|
+
const full = await rail.delivery(d.deliveryId);
|
|
17005
|
+
const entries = full.entries.map((e) => ({ id: e.entryId, words: entryWords(e) }));
|
|
17006
|
+
if (!entries.length || entries.some((e) => !e.words.trim())) continue;
|
|
17007
|
+
direct.push({ eventId: d.deliveryId, entries, entryIds: entries.map((e) => e.id), words: [] });
|
|
17008
|
+
} catch (error) {
|
|
17009
|
+
log2?.(`\u26A0 could not read instruction ${d.deliveryId.slice(0, 8)}: ${error.message}`);
|
|
17010
|
+
}
|
|
17011
|
+
}
|
|
17622
17012
|
const listed = /* @__PURE__ */ new Map();
|
|
17623
17013
|
const list = (goalId, eventId) => {
|
|
17624
17014
|
const ids = listed.get(goalId) ?? [];
|
|
@@ -17626,19 +17016,21 @@ async function gather(state, rail, held) {
|
|
|
17626
17016
|
listed.set(goalId, ids);
|
|
17627
17017
|
};
|
|
17628
17018
|
const announced = state.announced ??= /* @__PURE__ */ new Set();
|
|
17629
|
-
if (
|
|
17630
|
-
|
|
17631
|
-
|
|
17632
|
-
|
|
17633
|
-
|
|
17634
|
-
|
|
17635
|
-
|
|
17636
|
-
|
|
17637
|
-
|
|
17638
|
-
|
|
17639
|
-
|
|
17019
|
+
if (held) {
|
|
17020
|
+
if (waiting) {
|
|
17021
|
+
for (const id3 of announced) if (!(waiting.assigned ?? []).some((g) => g.goalId === id3)) announced.delete(id3);
|
|
17022
|
+
}
|
|
17023
|
+
for (const g of waiting?.assigned ?? []) if (g.goalId !== state.goal.id && !announced.has(g.goalId)) look2(g.goalId, "assigned", g.title);
|
|
17024
|
+
for (const g of waiting?.review ?? []) {
|
|
17025
|
+
list(g.goalId, g.eventId);
|
|
17026
|
+
look2(g.goalId, "waiting work", g.title);
|
|
17027
|
+
}
|
|
17028
|
+
for (const d of waiting?.deliveries ?? []) for (const id3 of d.goalIds) {
|
|
17029
|
+
list(id3, d.deliveryId);
|
|
17030
|
+
look2(id3, "waiting work");
|
|
17031
|
+
}
|
|
17640
17032
|
}
|
|
17641
|
-
const moved = movedBelow(held, state);
|
|
17033
|
+
const moved = held ? movedBelow(held, state) : [];
|
|
17642
17034
|
for (const { goal, since } of moved) look2(goal.goalId, "subtree", goal.title, since);
|
|
17643
17035
|
const heard = [];
|
|
17644
17036
|
const read3 = /* @__PURE__ */ new Set();
|
|
@@ -17655,10 +17047,15 @@ async function gather(state, rail, held) {
|
|
|
17655
17047
|
read3.add(goalId);
|
|
17656
17048
|
const fresh = !brief.reviewSince && (found.via === "waiting work" || found.via === "assigned") ? sinceTheWork(brief) : { after: -1 };
|
|
17657
17049
|
if (found.via === "assigned") announced.add(goalId);
|
|
17658
|
-
heard.push({ goalId, ...found, brief, words: unheard(brief, state, brief.reviewSince ? null : since ?? null, fresh.after, brief.reviewSince), ...fresh.note ? { note: fresh.note } : {} });
|
|
17050
|
+
heard.push({ goalId, ...found, brief, words: unheard(brief, state, brief.reviewSince ? null : since ?? null, fresh.after, brief.reviewSince, staged), ...fresh.note ? { note: fresh.note } : {} });
|
|
17659
17051
|
}
|
|
17052
|
+
for (const d of direct) d.words = d.entries.filter((e) => {
|
|
17053
|
+
if (staged.has(e.id)) return false;
|
|
17054
|
+
staged.add(e.id);
|
|
17055
|
+
return true;
|
|
17056
|
+
}).map((e) => e.words);
|
|
17660
17057
|
for (const { goal, since } of moved) if (read3.has(goal.goalId)) state.below.goals.set(goal.goalId, { since, at: goal.updatedAt ?? null });
|
|
17661
|
-
return { heard, listed };
|
|
17058
|
+
return { heard, listed, direct };
|
|
17662
17059
|
}
|
|
17663
17060
|
var GIVEN = "This Goal was given to you, and nobody has started it. Carry on with what you're doing unless it changes that; your first write to it (a contact update naming it) starts it when you take it up.";
|
|
17664
17061
|
var says = (h) => h.words.length > 0 || h.via === "assigned";
|
|
@@ -17676,12 +17073,21 @@ var block = (h) => [
|
|
|
17676
17073
|
].join("\n\n");
|
|
17677
17074
|
var short = (ids) => ids.map((id3) => id3.slice(0, 8)).join(", ");
|
|
17678
17075
|
var given = (heard) => heard.filter((h) => h.via === "assigned").map((h) => h.goalId);
|
|
17679
|
-
async function hear(session, state, rail, held, opts) {
|
|
17076
|
+
async function hear(session, state, rail, held, opts, waiting) {
|
|
17680
17077
|
const log2 = opts?.log;
|
|
17681
|
-
const { heard, listed } = await gather(state, rail, held);
|
|
17078
|
+
const { heard, listed, direct } = await gather(state, rail, held, waiting, log2);
|
|
17682
17079
|
const said = heard.filter(says);
|
|
17683
|
-
|
|
17684
|
-
|
|
17080
|
+
const words = [...direct.flatMap((d) => d.words), ...said.map(block)];
|
|
17081
|
+
if (words.length) {
|
|
17082
|
+
const text = words.join("\n\n");
|
|
17083
|
+
try {
|
|
17084
|
+
await send2(session, state, text);
|
|
17085
|
+
} catch (error) {
|
|
17086
|
+
(state.noted ??= []).push({ name: "", goals: said.map((h) => h.goalId), words: text, eventIds: direct.map((d) => d.eventId), at: Date.now(), cause: "read" });
|
|
17087
|
+
for (const d of direct) for (const id3 of d.entryIds) (state.seen ??= /* @__PURE__ */ new Set()).add(id3);
|
|
17088
|
+
throw error;
|
|
17089
|
+
}
|
|
17090
|
+
for (const d of direct) for (const id3 of d.entryIds) (state.seen ??= /* @__PURE__ */ new Set()).add(id3);
|
|
17685
17091
|
const worded = said.filter((h) => h.words.length);
|
|
17686
17092
|
if (worded.length) log2?.(`\u25B8 typed the person's words from ${worded.map((h) => `${h.goalId.slice(0, 8)} (via ${h.via})`).join(", ")}`);
|
|
17687
17093
|
const goals = given(said);
|
|
@@ -17690,18 +17096,21 @@ async function hear(session, state, rail, held, opts) {
|
|
|
17690
17096
|
if (opts?.tally) opts.tally.announced += goals.length;
|
|
17691
17097
|
}
|
|
17692
17098
|
}
|
|
17099
|
+
await acknowledgeDirect(direct.map((d) => d.eventId), state, rail, log2);
|
|
17693
17100
|
for (const h of heard) {
|
|
17694
17101
|
const events = listed.get(h.goalId);
|
|
17695
17102
|
if (events) await acknowledge2(h.goalId, h.brief, events, state, rail, log2);
|
|
17696
17103
|
}
|
|
17104
|
+
return direct.length > 0 || said.length > 0;
|
|
17697
17105
|
}
|
|
17698
17106
|
var NOTED = "New from the person while you work. Carry on unless it changes what you're doing: it comes back to you as your next turn when this one ends.";
|
|
17699
17107
|
var SEEN_MID_TURN = "While you worked, the person said this, and you saw it mid-task. Act on it now, unless you already have.";
|
|
17700
|
-
async function note(state, rail, held, inbox, how) {
|
|
17701
|
-
const { heard } = await gather(state, rail, held);
|
|
17108
|
+
async function note(state, rail, held, inbox, how, waiting) {
|
|
17109
|
+
const { heard, direct } = await gather(state, rail, held, waiting, how.log);
|
|
17702
17110
|
const said = heard.filter(says);
|
|
17703
|
-
|
|
17704
|
-
|
|
17111
|
+
const parts = [...direct.flatMap((d) => d.words), ...said.map(block)];
|
|
17112
|
+
if (!parts.length) return 0;
|
|
17113
|
+
const words = parts.join("\n\n");
|
|
17705
17114
|
const goals = said.map((h) => h.goalId);
|
|
17706
17115
|
const assigned = given(said);
|
|
17707
17116
|
if (how.tally) how.tally.announced += assigned.length;
|
|
@@ -17709,13 +17118,14 @@ async function note(state, rail, held, inbox, how) {
|
|
|
17709
17118
|
try {
|
|
17710
17119
|
name = inbox.put(`${NOTED}
|
|
17711
17120
|
|
|
17712
|
-
${
|
|
17121
|
+
${words}`);
|
|
17713
17122
|
how.log?.(`\u25B9 noted mid-turn: ${short(goals)}`);
|
|
17714
17123
|
if (assigned.length) how.log?.(`\u25B8 announced assigned ${short(assigned)} (via hook)`);
|
|
17715
17124
|
} catch (error) {
|
|
17716
17125
|
how.log?.(`\u26A0 could not note mid-turn (${error.message}): ${short(goals)} wait for the turn's end`);
|
|
17717
17126
|
}
|
|
17718
|
-
(state.noted ??= []).push({ name, goals, words:
|
|
17127
|
+
(state.noted ??= []).push({ name, goals, words, eventIds: direct.map((d) => d.eventId), at: how.now(), cause: how.cause === "wake" ? "wake" : "read" });
|
|
17128
|
+
for (const d of direct) for (const id3 of d.entryIds) (state.seen ??= /* @__PURE__ */ new Set()).add(id3);
|
|
17719
17129
|
return 1;
|
|
17720
17130
|
}
|
|
17721
17131
|
function receipts(state, inbox, log2) {
|
|
@@ -17730,15 +17140,33 @@ function receipts(state, inbox, log2) {
|
|
|
17730
17140
|
}
|
|
17731
17141
|
return seen;
|
|
17732
17142
|
}
|
|
17733
|
-
function settle(session, state, inbox, log2) {
|
|
17143
|
+
async function settle(session, state, rail, inbox, log2) {
|
|
17734
17144
|
const batches = state.noted ?? [];
|
|
17735
|
-
state.noted = [];
|
|
17736
17145
|
const seen = [];
|
|
17737
17146
|
const unseen = [];
|
|
17738
17147
|
for (const b of batches) (b.name && inbox?.settle(b.name) === "taken" ? seen : unseen).push(b);
|
|
17739
|
-
send2(session, state, [...seen.length ? [SEEN_MID_TURN, ...seen.map((b) => b.words)] : [], ...unseen.map((b) => b.words)].join("\n\n"));
|
|
17148
|
+
await send2(session, state, [...seen.length ? [SEEN_MID_TURN, ...seen.map((b) => b.words)] : [], ...unseen.map((b) => b.words)].join("\n\n"));
|
|
17149
|
+
state.noted = [];
|
|
17150
|
+
await acknowledgeDirect(batches.flatMap((b) => b.eventIds ?? []), state, rail, log2);
|
|
17740
17151
|
log2?.(`\u25B8 typed the person's words from ${short([...new Set(batches.flatMap((b) => b.goals))])} (noted mid-turn: ${seen.length} seen by the hook, ${unseen.length} not)`);
|
|
17741
17152
|
}
|
|
17153
|
+
async function acknowledgeDirect(events, state, rail, log2) {
|
|
17154
|
+
const acked = state.acked ??= /* @__PURE__ */ new Map();
|
|
17155
|
+
const ids = [...new Set(events)].filter((id3) => acked.get(`delivery:${id3}`) !== "handled");
|
|
17156
|
+
for (let at = 0; at < ids.length; at += 100) {
|
|
17157
|
+
const batch = ids.slice(at, at + 100);
|
|
17158
|
+
try {
|
|
17159
|
+
const results = await rail.ack(batch);
|
|
17160
|
+
for (const id3 of batch) {
|
|
17161
|
+
const result = results.find((r) => r.eventId === id3);
|
|
17162
|
+
if (result?.acknowledged) acked.set(`delivery:${id3}`, "handled");
|
|
17163
|
+
else log2?.(`\u26A0 could not acknowledge ${id3.slice(0, 8)}: ${result?.refused ?? "no result"} \u2014 its words are typed in; it stays listed until acknowledged`);
|
|
17164
|
+
}
|
|
17165
|
+
} catch (error) {
|
|
17166
|
+
log2?.(`\u26A0 could not acknowledge instructions: ${error.message} \u2014 their words are typed in; they stay listed until acknowledged`);
|
|
17167
|
+
}
|
|
17168
|
+
}
|
|
17169
|
+
}
|
|
17742
17170
|
async function acknowledge2(goalId, brief, events, state, rail, log2) {
|
|
17743
17171
|
const ids = [.../* @__PURE__ */ new Set([...brief.reviewEventId ? [brief.reviewEventId] : [], ...events])];
|
|
17744
17172
|
if (!ids.length) return;
|
|
@@ -17764,17 +17192,23 @@ async function step(session, state, rail, opts) {
|
|
|
17764
17192
|
state.carryOn = true;
|
|
17765
17193
|
}
|
|
17766
17194
|
await look(session, state, rail, opts);
|
|
17767
|
-
if (state.carryOn && state.resting && !state.asks?.size) send2(session, state, "");
|
|
17195
|
+
if (state.carryOn && state.resting && !state.asks?.size) await send2(session, state, "");
|
|
17768
17196
|
}
|
|
17769
17197
|
async function look(session, state, rail, opts) {
|
|
17770
17198
|
if (opts?.inbox) {
|
|
17771
17199
|
const seen = receipts(state, opts.inbox, opts.log);
|
|
17772
17200
|
if (opts.tally) opts.tally.seen += seen;
|
|
17773
17201
|
}
|
|
17774
|
-
if (state.resting && state.noted?.length && !state.asks?.size) return settle(session, state, opts?.inbox, opts?.log);
|
|
17202
|
+
if (state.resting && state.noted?.length && !state.asks?.size) return settle(session, state, rail, opts?.inbox, opts?.log);
|
|
17775
17203
|
if (!state.goal) {
|
|
17204
|
+
if (state.asks?.size || !state.resting && !opts?.inbox) return;
|
|
17205
|
+
const waiting = await rail.waiting();
|
|
17206
|
+
if (waiting.deliveries.some((d) => !d.goalIds.length && state.acked?.get(`delivery:${d.deliveryId}`) !== "handled")) {
|
|
17207
|
+
const handled = state.resting ? await hear(session, state, rail, void 0, opts, waiting) : await note(state, rail, void 0, opts.inbox, { cause: opts?.cause ?? "safety", now: opts?.now ?? Date.now, log: opts?.log, tally: opts?.tally }, waiting);
|
|
17208
|
+
if (handled) return;
|
|
17209
|
+
}
|
|
17776
17210
|
if (!state.resting) return;
|
|
17777
|
-
let brief2 = await rail.next();
|
|
17211
|
+
let brief2 = await rail.next(waiting);
|
|
17778
17212
|
if (!brief2.goalId || brief2.revision === void 0) {
|
|
17779
17213
|
if (opts?.initialPrompt?.trim()) {
|
|
17780
17214
|
const prompt = opts.initialPrompt.trim();
|
|
@@ -17783,7 +17217,7 @@ async function look(session, state, rail, opts) {
|
|
|
17783
17217
|
brief2 = await rail.next();
|
|
17784
17218
|
if (!brief2.goalId || brief2.revision === void 0) {
|
|
17785
17219
|
state.goal = { id: admitted.goalId, revision: admitted.revision };
|
|
17786
|
-
send2(session, state, [HARNESS_INSTRUCTION, named(admitted.goalId, void 0, { outcome: prompt }), prompt].join("\n\n"));
|
|
17220
|
+
await send2(session, state, [HARNESS_INSTRUCTION, named(admitted.goalId, void 0, { outcome: prompt }), prompt].join("\n\n"));
|
|
17787
17221
|
return;
|
|
17788
17222
|
}
|
|
17789
17223
|
} else {
|
|
@@ -17801,7 +17235,7 @@ async function look(session, state, rail, opts) {
|
|
|
17801
17235
|
const left = brief2.earlier ? brief2.earlier.omitted + brief2.earlier.shortened : 0;
|
|
17802
17236
|
const earlier = left ? `(${left} earlier entries on this Goal are left out or shortened; get_goals with history: true reads them in full.)` : "";
|
|
17803
17237
|
const fullText = [outcome2, earlier, ...said].filter(Boolean).join("\n\n");
|
|
17804
|
-
send2(session, state, [HARNESS_INSTRUCTION, named(brief2.goalId, void 0, brief2), fullText].filter(Boolean).join("\n\n"));
|
|
17238
|
+
await send2(session, state, [HARNESS_INSTRUCTION, named(brief2.goalId, void 0, brief2), fullText].filter(Boolean).join("\n\n"));
|
|
17805
17239
|
if (ended(brief2.state)) await acknowledge2(brief2.goalId, brief2, [], state, rail, opts?.log);
|
|
17806
17240
|
return;
|
|
17807
17241
|
}
|
|
@@ -17830,19 +17264,27 @@ async function look(session, state, rail, opts) {
|
|
|
17830
17264
|
}
|
|
17831
17265
|
await hear(session, state, rail, brief, { ...opts?.log ? { log: opts.log } : {}, ...opts?.tally ? { tally: opts.tally } : {} });
|
|
17832
17266
|
}
|
|
17833
|
-
function send2(session, state, text) {
|
|
17267
|
+
async function send2(session, state, text) {
|
|
17268
|
+
let words = text;
|
|
17269
|
+
if (state.carryOn) {
|
|
17270
|
+
const owed = state.owed;
|
|
17271
|
+
words = [owed, text].filter(Boolean).join("\n\n");
|
|
17272
|
+
text = [CARRY_ON, ...owed ? [OWED, owed] : [], text].filter(Boolean).join("\n\n");
|
|
17273
|
+
}
|
|
17274
|
+
const resting = state.resting;
|
|
17834
17275
|
state.resting = false;
|
|
17835
|
-
|
|
17276
|
+
try {
|
|
17277
|
+
await session.send(text);
|
|
17278
|
+
} catch (error) {
|
|
17279
|
+
state.resting = resting;
|
|
17280
|
+
throw error;
|
|
17281
|
+
}
|
|
17282
|
+
state.typed = words || void 0;
|
|
17836
17283
|
if (state.carryOn) {
|
|
17837
17284
|
state.carryOn = false;
|
|
17838
17285
|
state.carriedOn = true;
|
|
17839
|
-
const owed = state.owed;
|
|
17840
17286
|
state.owed = void 0;
|
|
17841
|
-
words2 = [owed, text].filter(Boolean).join("\n\n");
|
|
17842
|
-
text = [CARRY_ON, ...owed ? [OWED, owed] : [], text].filter(Boolean).join("\n\n");
|
|
17843
17287
|
}
|
|
17844
|
-
state.typed = words2 || void 0;
|
|
17845
|
-
session.send(text);
|
|
17846
17288
|
}
|
|
17847
17289
|
async function pump(session, state, rail, opts) {
|
|
17848
17290
|
const clock5 = opts.beat ?? beat();
|
|
@@ -17982,9 +17424,6 @@ ${event.message}`, state, rail, { log: opts.log });
|
|
|
17982
17424
|
opts.log("\u25A0 stopped");
|
|
17983
17425
|
}
|
|
17984
17426
|
return {
|
|
17985
|
-
send(text) {
|
|
17986
|
-
session?.send(text);
|
|
17987
|
-
},
|
|
17988
17427
|
working: () => running && state.resting !== true,
|
|
17989
17428
|
// Paused by a usage limit, the activity line says so: the quiet place the pause is shown.
|
|
17990
17429
|
tail: () => state.paused ? [pausedLine(state.paused)] : [...tail],
|
|
@@ -18074,7 +17513,7 @@ function selfUpdater(deps) {
|
|
|
18074
17513
|
}
|
|
18075
17514
|
|
|
18076
17515
|
// src/host.ts
|
|
18077
|
-
var HOST_FILE = join11(
|
|
17516
|
+
var HOST_FILE = join11(homedir9(), ".paigy", "host.json");
|
|
18078
17517
|
function wakeHarness(slot, saved, claudeTranscript) {
|
|
18079
17518
|
if (slot === "Desktop") return void 0;
|
|
18080
17519
|
if (saved !== null) return saved === "agy" || saved === "codex" || saved === "claude" ? saved : void 0;
|
|
@@ -18107,7 +17546,7 @@ var claimedRun = (sessionId2) => {
|
|
|
18107
17546
|
const slot = sessionSlot(sessionId2);
|
|
18108
17547
|
return { slot, key: runKey(slot) };
|
|
18109
17548
|
};
|
|
18110
|
-
var launchRun = (spec) => ({ session: spec.sessionId });
|
|
17549
|
+
var launchRun = (spec) => ({ session: spec.sessionId, model: spec.model });
|
|
18111
17550
|
var BOUND_ELSEWHERE_MS = 10 * 6e4;
|
|
18112
17551
|
var boundElsewhere = /* @__PURE__ */ new Map();
|
|
18113
17552
|
var BOUND_SKIP = "is bound to the session it was paired in, which this host cannot name \u2014 run `paigy-harness handoff` as it, in its folder, to record it";
|
|
@@ -18215,7 +17654,7 @@ function startHost(opts) {
|
|
|
18215
17654
|
});
|
|
18216
17655
|
void heartbeat({
|
|
18217
17656
|
version: VERSION,
|
|
18218
|
-
harnesses: detectAll().map((a) => ({ name: a.name, label: a.label, status: a.status })),
|
|
17657
|
+
harnesses: detectAll().map((a) => ({ name: a.name, label: a.label, status: a.status, models: a.models })),
|
|
18219
17658
|
workspaces: listWorkspaces(opts.wsDeps),
|
|
18220
17659
|
// The repos inside them, so a session started for work on one opens IN it (Goal 26982211).
|
|
18221
17660
|
repos: listRepos(opts.wsDeps)
|
|
@@ -18228,6 +17667,17 @@ function startHost(opts) {
|
|
|
18228
17667
|
const specs = await claimSessions(asHost).catch(() => []);
|
|
18229
17668
|
for (const spec of specs) {
|
|
18230
17669
|
const label = spec.name ?? spec.harness;
|
|
17670
|
+
let workspace = spec.workspace;
|
|
17671
|
+
if (spec.worktree) {
|
|
17672
|
+
const suffix = randomBytes(4).toString("hex");
|
|
17673
|
+
const newWt = `/tmp/paigy-${spec.harness}-${suffix}`;
|
|
17674
|
+
try {
|
|
17675
|
+
execSync(`git worktree add ${newWt} -b paigy-${suffix}`, { cwd: workspace, stdio: "ignore" });
|
|
17676
|
+
workspace = newWt;
|
|
17677
|
+
} catch (e) {
|
|
17678
|
+
opts.log(`[${label}] Failed to create worktree, falling back to ${workspace}`);
|
|
17679
|
+
}
|
|
17680
|
+
}
|
|
18231
17681
|
const { slot, key: key3 } = claimedRun(spec.sessionId);
|
|
18232
17682
|
const log2 = (line) => opts.log(`[${label}] ${line}`);
|
|
18233
17683
|
try {
|
|
@@ -18245,7 +17695,7 @@ function startHost(opts) {
|
|
|
18245
17695
|
}
|
|
18246
17696
|
const run = runHarness({
|
|
18247
17697
|
harness: spec.harness,
|
|
18248
|
-
cwd:
|
|
17698
|
+
cwd: workspace,
|
|
18249
17699
|
mode: "bypass",
|
|
18250
17700
|
prompt: "",
|
|
18251
17701
|
token: spec.token,
|
|
@@ -18666,79 +18116,13 @@ ipcMain.handle("paigy:agent", async (_e, name) => {
|
|
|
18666
18116
|
});
|
|
18667
18117
|
ipcMain.handle("paigy:agent-log", (_e, name) => {
|
|
18668
18118
|
try {
|
|
18669
|
-
const all = readFileSync7(join12(
|
|
18119
|
+
const all = readFileSync7(join12(homedir10(), ".paigy", "host.log"), "utf8").split("\n");
|
|
18670
18120
|
const mine = all.filter((l) => l.includes(`[${name}]`) || /[▶✖◉⚠]/.test(l) && l.includes(name));
|
|
18671
18121
|
return mine.slice(-500).join("\n");
|
|
18672
18122
|
} catch {
|
|
18673
18123
|
return "";
|
|
18674
18124
|
}
|
|
18675
18125
|
});
|
|
18676
|
-
ipcMain.handle("paigy:broker", async () => {
|
|
18677
|
-
const token = deviceToken();
|
|
18678
|
-
if (!token)
|
|
18679
|
-
return {
|
|
18680
|
-
standing: "Not paired yet \u2014 scan the code above and Paigy's queue lands here.",
|
|
18681
|
-
queue: [],
|
|
18682
|
-
proposal: null,
|
|
18683
|
-
voices: {},
|
|
18684
|
-
error: "unpaired"
|
|
18685
|
-
};
|
|
18686
|
-
return brokerView({
|
|
18687
|
-
notes: () => listNotes({ token }),
|
|
18688
|
-
connections: () => listConnections({ token }),
|
|
18689
|
-
triage: () => getTriage({ token })
|
|
18690
|
-
});
|
|
18691
|
-
});
|
|
18692
|
-
var sweeping = false;
|
|
18693
|
-
ipcMain.handle("paigy:triage-run", async () => {
|
|
18694
|
-
const token = deviceToken();
|
|
18695
|
-
if (!token) return { code: 1, output: "This Mac isn't paired yet \u2014 scan the pairing code first." };
|
|
18696
|
-
if (sweeping) return { code: 1, output: "A sweep is already running." };
|
|
18697
|
-
sweeping = true;
|
|
18698
|
-
const lines2 = [];
|
|
18699
|
-
const say = (line) => {
|
|
18700
|
-
for (const l of line.split("\n")) {
|
|
18701
|
-
lines2.push(l);
|
|
18702
|
-
log(l);
|
|
18703
|
-
}
|
|
18704
|
-
};
|
|
18705
|
-
try {
|
|
18706
|
-
const code = await runTriage(
|
|
18707
|
-
{ via: null, json: false, apply: false, yes: { close: false, stale: false, assign: false }, log: say },
|
|
18708
|
-
{
|
|
18709
|
-
notes: () => listNotes({ token }),
|
|
18710
|
-
connections: () => listConnections({ token }),
|
|
18711
|
-
submit: (run) => submitTriage(run, { token })
|
|
18712
|
-
}
|
|
18713
|
-
);
|
|
18714
|
-
return { code, output: lines2.join("\n") };
|
|
18715
|
-
} catch (e) {
|
|
18716
|
-
const message = e instanceof Error ? e.message : String(e);
|
|
18717
|
-
log(`\u26A0 the sweep failed: ${message}`);
|
|
18718
|
-
return { code: 1, output: [...lines2, `\u26A0 the sweep failed: ${message}`].join("\n") };
|
|
18719
|
-
} finally {
|
|
18720
|
-
sweeping = false;
|
|
18721
|
-
}
|
|
18722
|
-
});
|
|
18723
|
-
ipcMain.handle("paigy:triage-accept", async (_e, opts) => {
|
|
18724
|
-
const token = deviceToken();
|
|
18725
|
-
if (!token) return { ok: false, error: "This Mac isn't paired yet." };
|
|
18726
|
-
try {
|
|
18727
|
-
return { ok: true, result: await acceptTriage(opts.id, opts.body, { token }) };
|
|
18728
|
-
} catch (e) {
|
|
18729
|
-
return { ok: false, error: e instanceof Error ? e.message : String(e) };
|
|
18730
|
-
}
|
|
18731
|
-
});
|
|
18732
|
-
ipcMain.handle("paigy:triage-dismiss", async (_e, id3) => {
|
|
18733
|
-
const token = deviceToken();
|
|
18734
|
-
if (!token) return { ok: false, error: "This Mac isn't paired yet." };
|
|
18735
|
-
try {
|
|
18736
|
-
await dismissTriage(id3, { token });
|
|
18737
|
-
return { ok: true };
|
|
18738
|
-
} catch (e) {
|
|
18739
|
-
return { ok: false, error: e instanceof Error ? e.message : String(e) };
|
|
18740
|
-
}
|
|
18741
|
-
});
|
|
18742
18126
|
ipcMain.handle("paigy:workspaces", () => listWorkspaces(wsDeps));
|
|
18743
18127
|
ipcMain.handle("paigy:workspace-add", async () => {
|
|
18744
18128
|
const picked = await dialog.showOpenDialog({ properties: ["openDirectory", "createDirectory"] });
|