@paigy/harness 0.3.40 → 0.3.41
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 +28652 -29536
- package/dist/main.js +35 -797
- package/package.json +1 -1
package/dist/main.js
CHANGED
|
@@ -11625,58 +11625,6 @@ var NoteSchema = external_exports.object({
|
|
|
11625
11625
|
lastRideAt: external_exports.string().nullable().default(null),
|
|
11626
11626
|
createdAt: external_exports.string()
|
|
11627
11627
|
});
|
|
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
11628
|
var DeliveryModeSchema = external_exports.enum(["poll", "self_hosted"]);
|
|
11681
11629
|
var WAKE_EVENT = "wake";
|
|
11682
11630
|
var wakeChannel = (tokenId) => `wake:${tokenId}`;
|
|
@@ -12164,13 +12112,6 @@ async function registerDelivery(mode, opts = {}) {
|
|
|
12164
12112
|
if (!res.ok) throw new Error(`register_delivery failed: ${res.status} ${await res.text()}`);
|
|
12165
12113
|
return await res.json();
|
|
12166
12114
|
}
|
|
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
12115
|
async function listConnections(opts = {}) {
|
|
12175
12116
|
const res = await ensureAuthed(await reach(`${BACKEND_URL}/api/tokens`, {
|
|
12176
12117
|
headers: { authorization: `Bearer ${authToken(opts.token)}` }
|
|
@@ -12178,39 +12119,6 @@ async function listConnections(opts = {}) {
|
|
|
12178
12119
|
if (!res.ok) throw new Error(`list_connections failed: ${res.status} ${await res.text()}`);
|
|
12179
12120
|
return await res.json();
|
|
12180
12121
|
}
|
|
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
12122
|
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
12123
|
var APPLIED = "Applied; nothing was sent to the person.";
|
|
12216
12124
|
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 +12381,7 @@ async function subscribeWake(onNudge, opts = {}) {
|
|
|
12473
12381
|
|
|
12474
12382
|
// src/main.ts
|
|
12475
12383
|
var import_qrcode = __toESM(require_lib(), 1);
|
|
12476
|
-
import { homedir as
|
|
12384
|
+
import { homedir as homedir10, hostname } from "os";
|
|
12477
12385
|
|
|
12478
12386
|
// src/harness/catalog.ts
|
|
12479
12387
|
import { spawnSync } from "child_process";
|
|
@@ -15528,58 +15436,6 @@ var NoteSchema2 = external_exports.object({
|
|
|
15528
15436
|
lastRideAt: external_exports.string().nullable().default(null),
|
|
15529
15437
|
createdAt: external_exports.string()
|
|
15530
15438
|
});
|
|
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
15439
|
var DeliveryModeSchema2 = external_exports.enum(["poll", "self_hosted"]);
|
|
15584
15440
|
var RegisterDeliverySchema2 = external_exports.object({ mode: DeliveryModeSchema2 });
|
|
15585
15441
|
var OAuthStartSchema2 = external_exports.object({
|
|
@@ -15836,579 +15692,27 @@ var CallRecapSchema2 = external_exports.object({
|
|
|
15836
15692
|
|
|
15837
15693
|
// src/host.ts
|
|
15838
15694
|
import { readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
|
|
15839
|
-
import { homedir as
|
|
15695
|
+
import { homedir as homedir9 } from "os";
|
|
15840
15696
|
import { join as join11 } from "path";
|
|
15841
15697
|
|
|
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
|
-
}
|
|
15698
|
+
// src/ago.ts
|
|
16259
15699
|
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
15700
|
function ago(iso, now) {
|
|
16369
15701
|
const ms = now - Date.parse(iso);
|
|
16370
15702
|
if (!Number.isFinite(ms) || ms < 6e4) return "just now";
|
|
16371
15703
|
const minutes = Math.floor(ms / 6e4);
|
|
16372
|
-
if (minutes < 60) return `${
|
|
15704
|
+
if (minutes < 60) return `${plural(minutes, "minute")} ago`;
|
|
16373
15705
|
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
|
-
}
|
|
15706
|
+
if (hours < 24) return `${plural(hours, "hour")} ago`;
|
|
15707
|
+
return `${plural(Math.floor(hours / 24), "day")} ago`;
|
|
16404
15708
|
}
|
|
16405
15709
|
|
|
16406
15710
|
// src/workspaces.ts
|
|
16407
15711
|
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readdirSync, readFileSync as readFileSync3, statSync as statSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
16408
15712
|
import { dirname as dirname2, join as join5, resolve } from "path";
|
|
16409
|
-
import { homedir as
|
|
15713
|
+
import { homedir as homedir4 } from "os";
|
|
16410
15714
|
function workspacesFile() {
|
|
16411
|
-
return join5(
|
|
15715
|
+
return join5(homedir4(), ".paigy", "workspaces.json");
|
|
16412
15716
|
}
|
|
16413
15717
|
function listWorkspaces(deps) {
|
|
16414
15718
|
try {
|
|
@@ -16419,7 +15723,7 @@ function listWorkspaces(deps) {
|
|
|
16419
15723
|
}
|
|
16420
15724
|
}
|
|
16421
15725
|
function addWorkspace(dir, deps) {
|
|
16422
|
-
const next = resolve(dir.replace(/^~(?=$|\/)/,
|
|
15726
|
+
const next = resolve(dir.replace(/^~(?=$|\/)/, homedir4()));
|
|
16423
15727
|
const all = listWorkspaces(deps);
|
|
16424
15728
|
if (!all.includes(next)) {
|
|
16425
15729
|
all.push(next);
|
|
@@ -16435,12 +15739,12 @@ function removeWorkspace(dir, deps) {
|
|
|
16435
15739
|
return all;
|
|
16436
15740
|
}
|
|
16437
15741
|
function allowed(cwd, deps) {
|
|
16438
|
-
const target = resolve(cwd.replace(/^~(?=$|\/)/,
|
|
15742
|
+
const target = resolve(cwd.replace(/^~(?=$|\/)/, homedir4()));
|
|
16439
15743
|
return listWorkspaces(deps).some((w) => target === w || target.startsWith(`${w}/`));
|
|
16440
15744
|
}
|
|
16441
15745
|
function resolveWakeDir(pinned, deps) {
|
|
16442
15746
|
if (pinned && allowed(pinned, deps)) {
|
|
16443
|
-
const dir = resolve(pinned.replace(/^~(?=$|\/)/,
|
|
15747
|
+
const dir = resolve(pinned.replace(/^~(?=$|\/)/, homedir4()));
|
|
16444
15748
|
if ((deps.exists ?? existsSync3)(dir)) return dir;
|
|
16445
15749
|
}
|
|
16446
15750
|
return listWorkspaces(deps)[0];
|
|
@@ -16480,7 +15784,7 @@ function listRepos(deps) {
|
|
|
16480
15784
|
// src/holder.ts
|
|
16481
15785
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
16482
15786
|
import { readFileSync as readFileSync4, readdirSync as readdirSync2 } from "fs";
|
|
16483
|
-
import { homedir as
|
|
15787
|
+
import { homedir as homedir5 } from "os";
|
|
16484
15788
|
import { join as join6 } from "path";
|
|
16485
15789
|
function isAlive(pid) {
|
|
16486
15790
|
try {
|
|
@@ -16519,7 +15823,7 @@ function proven(pid, procStart, deps) {
|
|
|
16519
15823
|
});
|
|
16520
15824
|
}
|
|
16521
15825
|
function localHolderPids(deps = {}) {
|
|
16522
|
-
const dir = deps.dir ?? join6(process.env.CLAUDE_CONFIG_DIR?.trim() || join6(
|
|
15826
|
+
const dir = deps.dir ?? join6(process.env.CLAUDE_CONFIG_DIR?.trim() || join6(homedir5(), ".claude"), "sessions");
|
|
16523
15827
|
const list = deps.list ?? ((d) => readdirSync2(d));
|
|
16524
15828
|
const read3 = deps.read ?? ((f) => readFileSync4(f, "utf8"));
|
|
16525
15829
|
const check = { alive: deps.alive ?? isAlive, started: deps.started ?? psStart };
|
|
@@ -16568,7 +15872,7 @@ function descendsFrom(pid, ancestor, parent = psParent) {
|
|
|
16568
15872
|
return false;
|
|
16569
15873
|
}
|
|
16570
15874
|
function localHolders(deps = {}) {
|
|
16571
|
-
const dir = deps.dir ?? join6(process.env.CLAUDE_CONFIG_DIR?.trim() || join6(
|
|
15875
|
+
const dir = deps.dir ?? join6(process.env.CLAUDE_CONFIG_DIR?.trim() || join6(homedir5(), ".claude"), "sessions");
|
|
16572
15876
|
const list = deps.list ?? ((d) => readdirSync2(d));
|
|
16573
15877
|
const read3 = deps.read ?? ((f) => readFileSync4(f, "utf8"));
|
|
16574
15878
|
const check = { alive: deps.alive ?? isAlive, started: deps.started ?? psStart };
|
|
@@ -16661,7 +15965,7 @@ async function endSession(end, deps) {
|
|
|
16661
15965
|
|
|
16662
15966
|
// src/owner.ts
|
|
16663
15967
|
import { closeSync as closeSync2, openSync as openSync2, readSync, readdirSync as readdirSync3 } from "fs";
|
|
16664
|
-
import { homedir as
|
|
15968
|
+
import { homedir as homedir6 } from "os";
|
|
16665
15969
|
import { join as join7 } from "path";
|
|
16666
15970
|
var HEAD_BYTES = 64e3;
|
|
16667
15971
|
function readHead(file) {
|
|
@@ -16678,7 +15982,7 @@ function ownerSession(slot, deps = {}) {
|
|
|
16678
15982
|
const m = /^session:([0-9a-f]{8})$/.exec(slot);
|
|
16679
15983
|
if (!m) return null;
|
|
16680
15984
|
const prefix = m[1];
|
|
16681
|
-
const root = deps.projectsDir ?? join7(
|
|
15985
|
+
const root = deps.projectsDir ?? join7(homedir6(), ".claude", "projects");
|
|
16682
15986
|
const list = deps.list ?? ((dir) => readdirSync3(dir));
|
|
16683
15987
|
const head2 = deps.head ?? readHead;
|
|
16684
15988
|
let folders;
|
|
@@ -16741,8 +16045,8 @@ import { randomUUID as randomUUID4 } from "crypto";
|
|
|
16741
16045
|
// src/harness/session.ts
|
|
16742
16046
|
import { spawn } from "child_process";
|
|
16743
16047
|
import { existsSync as existsSync4, readFileSync as readFileSync5 } from "fs";
|
|
16744
|
-
import { homedir as
|
|
16745
|
-
import { resolve as resolve2, delimiter as
|
|
16048
|
+
import { homedir as homedir8 } from "os";
|
|
16049
|
+
import { resolve as resolve2, delimiter as delimiter2, join as join9 } from "path";
|
|
16746
16050
|
|
|
16747
16051
|
// src/harness/acp.ts
|
|
16748
16052
|
var none = { events: [], writes: [] };
|
|
@@ -17019,7 +16323,7 @@ var frame2 = (body) => JSON.stringify({ jsonrpc: "2.0", ...body });
|
|
|
17019
16323
|
// src/paigy/inbox.ts
|
|
17020
16324
|
import { mkdirSync as mkdirSync3, readdirSync as readdirSync4, renameSync, rmSync as rmSync2, statSync as statSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
17021
16325
|
import { join as join8 } from "path";
|
|
17022
|
-
import { homedir as
|
|
16326
|
+
import { homedir as homedir7 } from "os";
|
|
17023
16327
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
17024
16328
|
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
16329
|
var missing = (error) => error.code === "ENOENT";
|
|
@@ -17062,7 +16366,7 @@ function openInbox(dir) {
|
|
|
17062
16366
|
}
|
|
17063
16367
|
};
|
|
17064
16368
|
}
|
|
17065
|
-
var INBOX_ROOT = join8(
|
|
16369
|
+
var INBOX_ROOT = join8(homedir7(), ".paigy", "inbox");
|
|
17066
16370
|
var alive = (pid) => {
|
|
17067
16371
|
try {
|
|
17068
16372
|
process.kill(pid, 0);
|
|
@@ -17109,14 +16413,14 @@ function splitLines(buffer, chunk) {
|
|
|
17109
16413
|
return { lines: parts.filter((l) => l.trim()), rest };
|
|
17110
16414
|
}
|
|
17111
16415
|
function startSession(opts) {
|
|
17112
|
-
const cwd = resolve2(opts.cwd.replace(/^~(?=$|\/)/,
|
|
16416
|
+
const cwd = resolve2(opts.cwd.replace(/^~(?=$|\/)/, homedir8()));
|
|
17113
16417
|
if (!existsSync4(cwd)) {
|
|
17114
16418
|
queueMicrotask(() => opts.onEvent({ kind: "error", message: `workspace does not exist: ${cwd}` }));
|
|
17115
16419
|
}
|
|
17116
16420
|
const codexConfig = opts.harness === "codex" ? JSON.parse(process.env.CODEX_CONFIG || "{}") : null;
|
|
17117
16421
|
if (codexConfig) {
|
|
17118
16422
|
const key3 = "mcp_servers.paigy.env_vars";
|
|
17119
|
-
const configPath = join9(process.env.CODEX_HOME || join9(
|
|
16423
|
+
const configPath = join9(process.env.CODEX_HOME || join9(homedir8(), ".codex"), "config.toml");
|
|
17120
16424
|
const saved = existsSync4(configPath) ? readFileSync5(configPath, "utf8") : "";
|
|
17121
16425
|
const registered = /\[mcp_servers\.paigy\][\s\S]*?(?=\n\[|$)/.exec(saved)?.[0];
|
|
17122
16426
|
const configured = codexConfig["mcp_servers.paigy.command"] || codexConfig["mcp_servers.paigy.url"] || codexConfig.mcp_servers?.paigy?.command || codexConfig.mcp_servers?.paigy?.url;
|
|
@@ -17136,7 +16440,7 @@ function startSession(opts) {
|
|
|
17136
16440
|
// PATH knows neither — live catch 2026-08-04: the first slot-wake ENOENT'd).
|
|
17137
16441
|
env: {
|
|
17138
16442
|
...process.env,
|
|
17139
|
-
PATH: [process.env.PATH, ...probeDirs()].filter(Boolean).join(
|
|
16443
|
+
PATH: [process.env.PATH, ...probeDirs()].filter(Boolean).join(delimiter2),
|
|
17140
16444
|
...opts.token ? { PAIGY_TOKEN: opts.token } : {},
|
|
17141
16445
|
...opts.instance ? { PAIGY_INSTANCE_ID: opts.instance } : {},
|
|
17142
16446
|
...codexConfig ? { CODEX_CONFIG: JSON.stringify(codexConfig) } : {},
|
|
@@ -17556,7 +16860,7 @@ var BEAT_REPORT_MS = 10 * 6e4;
|
|
|
17556
16860
|
var ended = (state) => state === "done" || state === "cancelled";
|
|
17557
16861
|
function unheard(brief, state, since = null, after = -1, reviewSince) {
|
|
17558
16862
|
const seen = state.seen ??= /* @__PURE__ */ new Set();
|
|
17559
|
-
const
|
|
16863
|
+
const words = [];
|
|
17560
16864
|
const pendingAt = reviewSince ? Date.parse(reviewSince) : null;
|
|
17561
16865
|
for (const [i, e] of (brief.entries ?? []).entries()) {
|
|
17562
16866
|
if (seen.has(e.entryId) || !e.authorParticipant.startsWith("human:")) continue;
|
|
@@ -17572,15 +16876,15 @@ function unheard(brief, state, since = null, after = -1, reviewSince) {
|
|
|
17572
16876
|
seen.add(e.entryId);
|
|
17573
16877
|
if (e.aboutId && seen.has(e.aboutId) && brief.answers?.some((a) => a.requestEntryId === e.aboutId)) continue;
|
|
17574
16878
|
const text = entryWords(e).trim();
|
|
17575
|
-
if (text)
|
|
16879
|
+
if (text) words.push(text);
|
|
17576
16880
|
}
|
|
17577
16881
|
if (pendingAt !== null) for (const answer of brief.answers ?? []) {
|
|
17578
16882
|
const key3 = `answer:${answer.decisionNeedId}`;
|
|
17579
16883
|
if (seen.has(key3) || Date.parse(answer.acceptedAt) < pendingAt) continue;
|
|
17580
16884
|
seen.add(key3);
|
|
17581
|
-
|
|
16885
|
+
words.push(`Answer to question ${answer.decisionNeedId}: ${JSON.stringify(answer.result)}`);
|
|
17582
16886
|
}
|
|
17583
|
-
return
|
|
16887
|
+
return words;
|
|
17584
16888
|
}
|
|
17585
16889
|
function sinceTheWork(brief) {
|
|
17586
16890
|
const entries = brief.entries ?? [];
|
|
@@ -17701,7 +17005,7 @@ async function note(state, rail, held, inbox, how) {
|
|
|
17701
17005
|
const { heard } = await gather(state, rail, held);
|
|
17702
17006
|
const said = heard.filter(says);
|
|
17703
17007
|
if (!said.length) return 0;
|
|
17704
|
-
const
|
|
17008
|
+
const words = said.map(block).join("\n\n");
|
|
17705
17009
|
const goals = said.map((h) => h.goalId);
|
|
17706
17010
|
const assigned = given(said);
|
|
17707
17011
|
if (how.tally) how.tally.announced += assigned.length;
|
|
@@ -17709,13 +17013,13 @@ async function note(state, rail, held, inbox, how) {
|
|
|
17709
17013
|
try {
|
|
17710
17014
|
name = inbox.put(`${NOTED}
|
|
17711
17015
|
|
|
17712
|
-
${
|
|
17016
|
+
${words}`);
|
|
17713
17017
|
how.log?.(`\u25B9 noted mid-turn: ${short(goals)}`);
|
|
17714
17018
|
if (assigned.length) how.log?.(`\u25B8 announced assigned ${short(assigned)} (via hook)`);
|
|
17715
17019
|
} catch (error) {
|
|
17716
17020
|
how.log?.(`\u26A0 could not note mid-turn (${error.message}): ${short(goals)} wait for the turn's end`);
|
|
17717
17021
|
}
|
|
17718
|
-
(state.noted ??= []).push({ name, goals, words
|
|
17022
|
+
(state.noted ??= []).push({ name, goals, words, at: how.now(), cause: how.cause === "wake" ? "wake" : "read" });
|
|
17719
17023
|
return 1;
|
|
17720
17024
|
}
|
|
17721
17025
|
function receipts(state, inbox, log2) {
|
|
@@ -17832,16 +17136,16 @@ async function look(session, state, rail, opts) {
|
|
|
17832
17136
|
}
|
|
17833
17137
|
function send2(session, state, text) {
|
|
17834
17138
|
state.resting = false;
|
|
17835
|
-
let
|
|
17139
|
+
let words = text;
|
|
17836
17140
|
if (state.carryOn) {
|
|
17837
17141
|
state.carryOn = false;
|
|
17838
17142
|
state.carriedOn = true;
|
|
17839
17143
|
const owed = state.owed;
|
|
17840
17144
|
state.owed = void 0;
|
|
17841
|
-
|
|
17145
|
+
words = [owed, text].filter(Boolean).join("\n\n");
|
|
17842
17146
|
text = [CARRY_ON, ...owed ? [OWED, owed] : [], text].filter(Boolean).join("\n\n");
|
|
17843
17147
|
}
|
|
17844
|
-
state.typed =
|
|
17148
|
+
state.typed = words || void 0;
|
|
17845
17149
|
session.send(text);
|
|
17846
17150
|
}
|
|
17847
17151
|
async function pump(session, state, rail, opts) {
|
|
@@ -18074,7 +17378,7 @@ function selfUpdater(deps) {
|
|
|
18074
17378
|
}
|
|
18075
17379
|
|
|
18076
17380
|
// src/host.ts
|
|
18077
|
-
var HOST_FILE = join11(
|
|
17381
|
+
var HOST_FILE = join11(homedir9(), ".paigy", "host.json");
|
|
18078
17382
|
function wakeHarness(slot, saved, claudeTranscript) {
|
|
18079
17383
|
if (slot === "Desktop") return void 0;
|
|
18080
17384
|
if (saved !== null) return saved === "agy" || saved === "codex" || saved === "claude" ? saved : void 0;
|
|
@@ -18666,79 +17970,13 @@ ipcMain.handle("paigy:agent", async (_e, name) => {
|
|
|
18666
17970
|
});
|
|
18667
17971
|
ipcMain.handle("paigy:agent-log", (_e, name) => {
|
|
18668
17972
|
try {
|
|
18669
|
-
const all = readFileSync7(join12(
|
|
17973
|
+
const all = readFileSync7(join12(homedir10(), ".paigy", "host.log"), "utf8").split("\n");
|
|
18670
17974
|
const mine = all.filter((l) => l.includes(`[${name}]`) || /[▶✖◉⚠]/.test(l) && l.includes(name));
|
|
18671
17975
|
return mine.slice(-500).join("\n");
|
|
18672
17976
|
} catch {
|
|
18673
17977
|
return "";
|
|
18674
17978
|
}
|
|
18675
17979
|
});
|
|
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
17980
|
ipcMain.handle("paigy:workspaces", () => listWorkspaces(wsDeps));
|
|
18743
17981
|
ipcMain.handle("paigy:workspace-add", async () => {
|
|
18744
17982
|
const picked = await dialog.showOpenDialog({ properties: ["openDirectory", "createDirectory"] });
|