@paigy/harness 0.2.12 → 0.3.1
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 +29 -1
- package/dist/cli.js +714 -132
- package/dist/main.js +886 -347
- package/package.json +15 -13
package/dist/main.js
CHANGED
|
@@ -4577,11 +4577,13 @@ var require_lib = __commonJS({
|
|
|
4577
4577
|
// src/main.ts
|
|
4578
4578
|
import { app, BrowserWindow, dialog, ipcMain } from "electron";
|
|
4579
4579
|
import { spawn as spawn2 } from "child_process";
|
|
4580
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
4580
4581
|
import { fileURLToPath } from "url";
|
|
4581
4582
|
import { dirname as dirname4, join as join5 } from "path";
|
|
4582
4583
|
|
|
4583
4584
|
// ../../packages/sdk/dist/index.js
|
|
4584
4585
|
import { createRequire as __sdkCreateRequire } from "module";
|
|
4586
|
+
import { randomUUID } from "crypto";
|
|
4585
4587
|
|
|
4586
4588
|
// ../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/external.js
|
|
4587
4589
|
var external_exports = {};
|
|
@@ -8625,7 +8627,8 @@ var coerce = {
|
|
|
8625
8627
|
var NEVER = INVALID;
|
|
8626
8628
|
|
|
8627
8629
|
// ../../packages/sdk/dist/index.js
|
|
8628
|
-
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
|
|
8630
|
+
import { closeSync, existsSync, mkdirSync, openSync, readFileSync, rmSync, statSync, writeFileSync } from "fs";
|
|
8631
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
8629
8632
|
import { homedir } from "os";
|
|
8630
8633
|
import { join } from "path";
|
|
8631
8634
|
var require2 = __sdkCreateRequire(import.meta.url);
|
|
@@ -10891,16 +10894,25 @@ async function proxy() {
|
|
|
10891
10894
|
if (!PROXY_ENV.some((k) => process.env[k])) return void 0;
|
|
10892
10895
|
return agent ??= new (await import("./undici-GCFUZISI.js")).EnvHttpProxyAgent();
|
|
10893
10896
|
}
|
|
10897
|
+
var INSTANCE_ID = randomUUID();
|
|
10898
|
+
var SESSION_ID = process.env.PAIGY_SESSION_ID ?? process.env.CLAUDE_CODE_SESSION_ID ?? INSTANCE_ID;
|
|
10894
10899
|
async function reach(url, init) {
|
|
10895
10900
|
try {
|
|
10896
|
-
|
|
10901
|
+
const headers = {
|
|
10902
|
+
...init?.headers,
|
|
10903
|
+
"x-paigy-instance": INSTANCE_ID,
|
|
10904
|
+
"x-paigy-session": SESSION_ID
|
|
10905
|
+
};
|
|
10906
|
+
return await fetch(url, { ...init, headers, dispatcher: await proxy() });
|
|
10897
10907
|
} catch (e) {
|
|
10898
10908
|
throw new Error(`${NETWORK_MSG} (${e?.message ?? String(e)})`);
|
|
10899
10909
|
}
|
|
10900
10910
|
}
|
|
10901
10911
|
var ContextSchema = external_exports.object({
|
|
10902
10912
|
title: external_exports.string().min(1).describe("One-line headline of what you need (required, non-empty)."),
|
|
10903
|
-
description: external_exports.array(external_exports.string().min(1)).
|
|
10913
|
+
description: external_exports.array(external_exports.string().min(1)).describe(
|
|
10914
|
+
"Semantic chunks of detail (each a standalone, non-empty piece). The user can select chunks to ask you to expand. MAY BE EMPTY: a claim whose whole content is its heading \u2014 a single sentence \u2014 has no body, and saying so beats repeating the heading underneath itself. That repeat is what `min(1)` used to force, at 2x the storage, with every reader subtracting it back out at render time."
|
|
10915
|
+
)
|
|
10904
10916
|
});
|
|
10905
10917
|
var ParticipantSchema = external_exports.object({
|
|
10906
10918
|
kind: external_exports.enum(["human", "agent"]),
|
|
@@ -11047,6 +11059,14 @@ var NotifyRequestSchema = external_exports.object({
|
|
|
11047
11059
|
waiting: external_exports.enum(["none", "soft", "hard"]).optional().describe(
|
|
11048
11060
|
"With `ask`: what happens to your work while you wait. 'none' = you're just informing the user. 'soft' = you'd like an answer but can keep working. 'hard' = you are stopped until they answer (reaches them urgently and escalates to a real phone call if unanswered). Replaces urgencyHint + blocking \u2014 send this one field."
|
|
11049
11061
|
),
|
|
11062
|
+
/** Δ9b (#895): HOLD this claim so the sender can correct the plan before anyone is
|
|
11063
|
+
* interrupted. Opt-in per claim, because the fail-open is a minute-granularity cron —
|
|
11064
|
+
* holding by default would charge every quiet claim that minute before any agent could
|
|
11065
|
+
* correct anything. Ignored for `waiting: 'hard'`: a blocking ask rings on what we have,
|
|
11066
|
+
* and the enrichment can still land mid-call (#781 re-plans the unspoken tail). */
|
|
11067
|
+
confirm: external_exports.boolean().optional().describe(
|
|
11068
|
+
"Hold this one so you can correct the plan before the user is interrupted. The response comes back with `held: true` and the plan; POST the confirm route to release it (with options/visuals/urgency corrections, or nothing at all). If you never do, it is announced anyway a couple of minutes later. Ignored when waiting is 'hard'."
|
|
11069
|
+
),
|
|
11050
11070
|
/** #575: a RELAY of the user's explicitly stated preference, never the agent's
|
|
11051
11071
|
* choice. Outranks waiting in both directions: 'call' rings even for a
|
|
11052
11072
|
* waiting:'none' "call me when it's done"; 'message' never rings even for
|
|
@@ -11080,7 +11100,7 @@ var NotifyRequestSchema = external_exports.object({
|
|
|
11080
11100
|
if (r.ask !== void 0) {
|
|
11081
11101
|
for (const f of ["context", "select", "points"]) {
|
|
11082
11102
|
if (r[f] !== void 0)
|
|
11083
|
-
ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: [f], message: `the simplified \`ask\` form takes no ${f} \u2014 the broker derives it
|
|
11103
|
+
ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: [f], message: `the simplified \`ask\` form takes no ${f} \u2014 the broker derives the answer shape from your prose. Drop ${f} and say it in \`ask\` instead ("should I\u2026" for approve/deny, "which of these\u2026" for a pick), passing \`options\` when you're offering concrete alternatives.` });
|
|
11084
11104
|
}
|
|
11085
11105
|
return;
|
|
11086
11106
|
}
|
|
@@ -11105,35 +11125,45 @@ function normalizeWaiting(req) {
|
|
|
11105
11125
|
blocking: req.blocking || waiting === "hard"
|
|
11106
11126
|
};
|
|
11107
11127
|
}
|
|
11108
|
-
|
|
11109
|
-
|
|
11110
|
-
|
|
11128
|
+
function unitsOf(text) {
|
|
11129
|
+
const spans = [];
|
|
11130
|
+
const re = /\n\s*\n+/g;
|
|
11131
|
+
let cursor = 0;
|
|
11132
|
+
const push = (from, to) => {
|
|
11133
|
+
const slice = text.slice(from, to);
|
|
11134
|
+
const lead = slice.length - slice.trimStart().length;
|
|
11135
|
+
const tail = slice.length - slice.trimEnd().length;
|
|
11136
|
+
if (from + lead < to - tail) spans.push({ start: from + lead, end: to - tail });
|
|
11137
|
+
};
|
|
11138
|
+
for (let m = re.exec(text); m; m = re.exec(text)) {
|
|
11139
|
+
push(cursor, m.index);
|
|
11140
|
+
cursor = m.index + m[0].length;
|
|
11141
|
+
}
|
|
11142
|
+
push(cursor, text.length);
|
|
11143
|
+
return spans;
|
|
11144
|
+
}
|
|
11145
|
+
function headline(text) {
|
|
11146
|
+
const line = text.split("\n").map((l) => l.trim()).filter((l) => l && !/^`{3,}/.test(l)).map((l) => l.replace(/^(?:#{1,6}|[-*>]|\d{1,3}\.)\s+/, "")).find(Boolean) ?? "";
|
|
11147
|
+
return (/^[^.!?\n]+[.!?]?/.exec(line)?.[0] ?? line).trim();
|
|
11148
|
+
}
|
|
11149
|
+
function bodyAfterHeadline(text) {
|
|
11111
11150
|
const body = text.trim();
|
|
11112
|
-
|
|
11113
|
-
const
|
|
11114
|
-
|
|
11115
|
-
|
|
11116
|
-
|
|
11117
|
-
if (!s) continue;
|
|
11118
|
-
const last = chunks[chunks.length - 1];
|
|
11119
|
-
if (last !== void 0 && `${last} ${s}`.length <= max) chunks[chunks.length - 1] = `${last} ${s}`;
|
|
11120
|
-
else chunks.push(s);
|
|
11121
|
-
}
|
|
11122
|
-
if (chunks.length <= cap) return chunks;
|
|
11123
|
-
return [...chunks.slice(0, cap - 1), chunks.slice(cap - 1).join(" ")];
|
|
11151
|
+
const title = headline(body);
|
|
11152
|
+
const rest = body.slice(title.length).replace(/^[\s.!?—–-]+/, "").trim();
|
|
11153
|
+
if (!rest) return { title, description: [] };
|
|
11154
|
+
const chunks = unitsOf(rest).map((u) => rest.slice(u.start, u.end)).filter(Boolean);
|
|
11155
|
+
return { title, description: chunks.length ? chunks : [rest] };
|
|
11124
11156
|
}
|
|
11125
11157
|
function deriveAsk(req) {
|
|
11126
11158
|
req = normalizeWaiting(req);
|
|
11127
11159
|
if (!req.ask) return req;
|
|
11128
11160
|
const text = req.ask.trim();
|
|
11129
|
-
const firstSentence = (/^[^.!?\n]+[.!?]?/.exec(text)?.[0] ?? text).trim();
|
|
11130
|
-
const title = firstSentence.length > 90 ? `${firstSentence.slice(0, 87).trimEnd()}\u2026` : firstSentence;
|
|
11131
11161
|
const hinted = req.urgencyHint === "now" ? "call" : req.urgencyHint === "soon" ? "banner" : req.urgencyHint === "whenever" ? "inbox" : req.urgency;
|
|
11132
11162
|
const urgency = req.channel === "call" ? "call" : req.channel === "message" && hinted === "call" ? "banner" : hinted;
|
|
11133
11163
|
const { ask: _ask, needs, urgencyHint: _hint, channel: _channel, ...rest } = req;
|
|
11134
11164
|
return {
|
|
11135
11165
|
...rest,
|
|
11136
|
-
context:
|
|
11166
|
+
context: bodyAfterHeadline(text),
|
|
11137
11167
|
// Options riding alongside the ask (#575: pixels can't be prose) floor to a
|
|
11138
11168
|
// single pick — the model broker may upgrade to many/rank from the wording.
|
|
11139
11169
|
select: req.options?.length ? "one" : "text",
|
|
@@ -11323,6 +11353,40 @@ var NotifyResponseSchema = external_exports.object({
|
|
|
11323
11353
|
answer: UserAnswerSchema.optional(),
|
|
11324
11354
|
answeredAt: external_exports.string().datetime().optional()
|
|
11325
11355
|
});
|
|
11356
|
+
var NotifyPlanUnitSchema = external_exports.object({
|
|
11357
|
+
notificationId: external_exports.string(),
|
|
11358
|
+
/** The unit's own heading, so the agent can tell which of its paragraphs this became. */
|
|
11359
|
+
title: external_exports.string(),
|
|
11360
|
+
/** How loudly this unit was arbitrated to arrive — per unit, which is the point of units. */
|
|
11361
|
+
level: NotifyLevelSchema,
|
|
11362
|
+
/** Answered from something the user already decided: nobody is interrupted, and a trail card
|
|
11363
|
+
* says so. The agent should not wait on this one. */
|
|
11364
|
+
settled: external_exports.literal(true).optional(),
|
|
11365
|
+
/** What this unit would need to be answerable and does not carry (#894). A PROPOSAL to the
|
|
11366
|
+
* agent — nothing here changed the ask, and ignoring it costs nothing. */
|
|
11367
|
+
needs: external_exports.array(external_exports.enum(["options", "visuals"])).optional(),
|
|
11368
|
+
/** The SHAPE the broker would give this unit, for the agent to ratify (#886/#894). The
|
|
11369
|
+
* split layer reads prose and can see that a paragraph is a yes/no or a pick-one — but a
|
|
11370
|
+
* broker that DECIDES that destroys the only fact separating a statement from a real ask
|
|
11371
|
+
* (#731), so it is offered, never applied: the unit is stored `text` until the agent
|
|
11372
|
+
* confirms the shape (POST /notify/:id/confirm). Ignoring it costs nothing. */
|
|
11373
|
+
proposal: external_exports.object({
|
|
11374
|
+
select: SelectShapeSchema,
|
|
11375
|
+
options: external_exports.array(external_exports.object({ label: external_exports.string().min(1) })).optional()
|
|
11376
|
+
}).optional(),
|
|
11377
|
+
/** What the broker READ this unit as wanting from the human (#952 layer 2): a `decision`
|
|
11378
|
+
* between alternatives, an `approval` the agent is blocked on, or `knowledge` it just
|
|
11379
|
+
* needs to know. Reported so the agent can correct a misread the same way it ratifies a
|
|
11380
|
+
* shape — the read RAISES (a decision always asks) and never silences a question the
|
|
11381
|
+
* agent declared (#731, #923). */
|
|
11382
|
+
wants: external_exports.enum(["decision", "approval", "knowledge"]).optional()
|
|
11383
|
+
});
|
|
11384
|
+
var NotifyPlanSchema = external_exports.object({
|
|
11385
|
+
units: external_exports.array(NotifyPlanUnitSchema),
|
|
11386
|
+
/** Which unit is this arrival's ONE interruption (units-design.md D23). Absent means nobody
|
|
11387
|
+
* was interrupted — every unit was either settled or quiet enough to sit in the inbox. */
|
|
11388
|
+
speaks: external_exports.string().optional()
|
|
11389
|
+
});
|
|
11326
11390
|
var UserResponseSchema = external_exports.object({
|
|
11327
11391
|
requestId: external_exports.string(),
|
|
11328
11392
|
answer: UserAnswerSchema,
|
|
@@ -11377,6 +11441,20 @@ var InboxItemSchema = external_exports.object({
|
|
|
11377
11441
|
/** The conversation thread + connection this item lives on. Present on the replied
|
|
11378
11442
|
* detail — they power History's "Continue" / "New session from this" (#57/#251). */
|
|
11379
11443
|
parentId: external_exports.string().optional(),
|
|
11444
|
+
/** THE ARRIVAL this row is one unit of (`notifications.ask_id` → `asks`). A claim is one
|
|
11445
|
+
* arrival and its units are N rows of it, so this — not `parentId` — is what makes a
|
|
11446
|
+
* multi-part notification one thing on screen. The thread is the whole CONVERSATION: it
|
|
11447
|
+
* accumulates every message an agent ever sent, so grouping by it renders a day of
|
|
11448
|
+
* unrelated updates as a single "12-part request". Absent on rows written before the
|
|
11449
|
+
* `asks` table, and on anything that never went through `notify` — both fall back to the
|
|
11450
|
+
* thread, which is what the client did for all rows until now. */
|
|
11451
|
+
askId: external_exports.string().optional(),
|
|
11452
|
+
/** WHERE this unit sat in the message it was cut from (`notifications.seq`). The batch
|
|
11453
|
+
* shares one `created_at` to the microsecond, so without it the author's order is
|
|
11454
|
+
* unrecoverable client-side — a four-paragraph briefing rendered opening-paragraph-last
|
|
11455
|
+
* (live 2026-08-10, D35). The API already orders by it; this lets a reader that
|
|
11456
|
+
* re-sorts (grouping, filtering) put an arrival back in the order it was written. */
|
|
11457
|
+
seq: external_exports.number().int().optional(),
|
|
11380
11458
|
tokenId: external_exports.string().optional(),
|
|
11381
11459
|
status: NotifyStatusSchema,
|
|
11382
11460
|
context: ContextSchema,
|
|
@@ -11387,6 +11465,16 @@ var InboxItemSchema = external_exports.object({
|
|
|
11387
11465
|
* ring/enqueue time. Replaces the condensed line + index-aligned phrased points, which
|
|
11388
11466
|
* between them could not express a call as a sequence. `question: null` is a real turn —
|
|
11389
11467
|
* a status update stays a statement instead of being shaped into a yes/no. */
|
|
11468
|
+
/** Does this claim want an ANSWER, or is it telling you something? Written per row from
|
|
11469
|
+
* `requestAsks` — the agent's own declaration, not a guess. `false` is what earns a card
|
|
11470
|
+
* its acknowledge affordance: without it a status update offers a text box and a dismiss,
|
|
11471
|
+
* and neither of those is "got it" (owner, 2026-08-10). */
|
|
11472
|
+
asks: external_exports.boolean().optional(),
|
|
11473
|
+
/** When a live process last pulsed for this row's agent — the liveness input for
|
|
11474
|
+
* "working requires a pulse" (#928): the list said "Working…" from agent_state alone
|
|
11475
|
+
* while the party called the same dead claim stalled. Absent = no token/no data,
|
|
11476
|
+
* which must never CLAIM stalled. */
|
|
11477
|
+
lastSeenAt: external_exports.string().optional(),
|
|
11390
11478
|
agenda: external_exports.array(AgendaTurnSchema).optional(),
|
|
11391
11479
|
/** On a replied detail (#397): the next steps the user attached to the answer
|
|
11392
11480
|
* ("call back after lunch") — shown so they can see the commitment was captured. */
|
|
@@ -11411,6 +11499,23 @@ var InboxItemSchema = external_exports.object({
|
|
|
11411
11499
|
* client may still flag a stall by age. Drives the inbox error badge + Retry. */
|
|
11412
11500
|
error: external_exports.string().optional(),
|
|
11413
11501
|
clarifies: external_exports.string().optional(),
|
|
11502
|
+
/** The ring ladder ran out while this was still pending — we tried to reach you and
|
|
11503
|
+
* STOPPED trying (`arbitration/arbitrate.ts` `nextRing` → `stop`). Distinct from an
|
|
11504
|
+
* agent with nothing to say, which the roster drew identically until now: "nothing to
|
|
11505
|
+
* say" and "gave up saying it" are opposite situations wearing the same face
|
|
11506
|
+
* (navigation-design.md, gap 1). False for anything that never rang. */
|
|
11507
|
+
gaveUp: external_exports.boolean().default(false),
|
|
11508
|
+
/** Why this arrived the way it did, read back off the delivery receipt (`notify/why.ts`).
|
|
11509
|
+
* Absent for anything never delivered through a push, and for older rows written before
|
|
11510
|
+
* the reason was recorded. Deliberately a debug affordance, shown small (owner,
|
|
11511
|
+
* 2026-08-07) — its real job is to give "this didn't need a call" something to be
|
|
11512
|
+
* feedback ABOUT. */
|
|
11513
|
+
why: external_exports.object({
|
|
11514
|
+
asked: NotifyLevelSchema,
|
|
11515
|
+
got: NotifyLevelSchema,
|
|
11516
|
+
because: external_exports.enum(["unresponsive", "dismissed", "not_permitted", "silent", "coalesced", "agent_capped", "unplanned", "learned_raise"]).optional(),
|
|
11517
|
+
line: external_exports.string()
|
|
11518
|
+
}).optional(),
|
|
11414
11519
|
select: external_exports.enum(["one", "many", "rank", "confirm", "text"]).default("one"),
|
|
11415
11520
|
confirmStyle: external_exports.enum(["yesno", "approve"]).default("yesno").describe(
|
|
11416
11521
|
"Labels for a select:'confirm' paige \u2014 'yesno' (Yes/No) or 'approve' (Approve/Deny). Ignored unless select is 'confirm'."
|
|
@@ -11521,6 +11626,15 @@ var HistoryItemSchema = external_exports.object({
|
|
|
11521
11626
|
/** When you answered the agent's notification (agent→user only). */
|
|
11522
11627
|
humanAckedAt: external_exports.string().nullable()
|
|
11523
11628
|
});
|
|
11629
|
+
var ACTIVITY_LINES = 2;
|
|
11630
|
+
var ACTIVITY_LINE_MAX = 80;
|
|
11631
|
+
var AgentActivitySchema = external_exports.object({
|
|
11632
|
+
/** Oldest first, so the newest line is last — the one that replaces in place. */
|
|
11633
|
+
lines: external_exports.array(external_exports.string().max(ACTIVITY_LINE_MAX)).max(ACTIVITY_LINES),
|
|
11634
|
+
/** When the harness observed this tail. Its own timestamp, not the heartbeat's: a beat
|
|
11635
|
+
* that carries an UNCHANGED tail must not make a stalled agent look like it just moved. */
|
|
11636
|
+
at: external_exports.string().datetime()
|
|
11637
|
+
});
|
|
11524
11638
|
var ConnectionSummarySchema = external_exports.object({
|
|
11525
11639
|
/** The connection = the agent's token id (used to address a request). */
|
|
11526
11640
|
id: external_exports.string(),
|
|
@@ -11532,6 +11646,14 @@ var ConnectionSummarySchema = external_exports.object({
|
|
|
11532
11646
|
provider: external_exports.string().nullable(),
|
|
11533
11647
|
/** The pairing's assigned voice (#462); null = the default voice. */
|
|
11534
11648
|
voice: VoiceKeySchema.nullable(),
|
|
11649
|
+
/** The LOUDEST this agent may ever reach you — a ceiling on `NOTIFY_LADDER`, set by the
|
|
11650
|
+
* user on the agent's own page. null = no ceiling (today's behaviour for every
|
|
11651
|
+
* connection). Android binds importance to a relationship rather than to each message,
|
|
11652
|
+
* and that is the thing our roster could not say: "Marlow may always call me; Otto
|
|
11653
|
+
* never may" (navigation-design.md, gap 2). Clamped in `arbitrateLevel`, so it binds
|
|
11654
|
+
* every surface at once and outranks even `sessionMode: all_calls` — a mode the user
|
|
11655
|
+
* set once must not overrule a rule they set about one agent. */
|
|
11656
|
+
reach: NotifyLevelSchema.nullable().optional(),
|
|
11535
11657
|
createdAt: external_exports.string().datetime(),
|
|
11536
11658
|
/** Most recent notification on this connection, either direction. Null = no contact yet.
|
|
11537
11659
|
* Drives the agents-page recency grouping (Today / This week / …). */
|
|
@@ -11546,10 +11668,49 @@ var ConnectionSummarySchema = external_exports.object({
|
|
|
11546
11668
|
harnesses: external_exports.array(external_exports.object({ name: external_exports.string(), label: external_exports.string(), status: external_exports.string() })).optional(),
|
|
11547
11669
|
workspaces: external_exports.array(external_exports.string()).optional()
|
|
11548
11670
|
}).optional(),
|
|
11671
|
+
/** The tail of this agent's working log, when a harness is driving it — the agent page's
|
|
11672
|
+
* live strip. Absent for anything the desktop harness isn't running (a hatched identity
|
|
11673
|
+
* used straight from a terminal emits no work events; the page says so rather than
|
|
11674
|
+
* drawing an empty box). */
|
|
11675
|
+
activity: AgentActivitySchema.optional(),
|
|
11549
11676
|
/** True = a provider-managed agent running in the provider's cloud (e.g. Anthropic CMA);
|
|
11550
11677
|
* false = a local MCP connection running on the user's computer (Claude Code/Codex/…). */
|
|
11551
11678
|
managed: external_exports.boolean()
|
|
11552
11679
|
});
|
|
11680
|
+
var MoveRingSchema = external_exports.enum(["home", "travels", "retired", "quarantined"]);
|
|
11681
|
+
var MoveSchema = external_exports.object({
|
|
11682
|
+
id: external_exports.string(),
|
|
11683
|
+
/** The reusable question, as distill normalized it. */
|
|
11684
|
+
question: external_exports.string(),
|
|
11685
|
+
/** The operative ruling. Editable by the user (PATCH) — which resets the ledger. */
|
|
11686
|
+
answer: external_exports.string(),
|
|
11687
|
+
/** The user's stated reason, when they gave one. Null = inherently narrow: the judge is
|
|
11688
|
+
* told so, and the ruling only derives essentially the same question in the same scope. */
|
|
11689
|
+
rationale: external_exports.string().nullable(),
|
|
11690
|
+
/** Where the ruling lives: a repo/workspace, or 'global'. */
|
|
11691
|
+
scope: external_exports.string(),
|
|
11692
|
+
ring: MoveRingSchema,
|
|
11693
|
+
/** True = the user pinned it with `always` (travel granted by hand, not by evidence). */
|
|
11694
|
+
pinned: external_exports.boolean(),
|
|
11695
|
+
/** True = a pin the user placed was BROKEN by later counter-evidence. Surfaced so the
|
|
11696
|
+
* break is visible instead of a pin silently disappearing. */
|
|
11697
|
+
pinBroken: external_exports.boolean(),
|
|
11698
|
+
/** When the ruling was distilled. */
|
|
11699
|
+
learnedAt: external_exports.string(),
|
|
11700
|
+
/** Last time it answered an ask. Null = never fired. */
|
|
11701
|
+
lastUsedAt: external_exports.string().nullable(),
|
|
11702
|
+
/** How many asks it has answered. Instrumentation — deliberately NOT an input to the
|
|
11703
|
+
* evidence curve: firing says the question keeps arising, not that the ruling is right. */
|
|
11704
|
+
usedCount: external_exports.number(),
|
|
11705
|
+
/** Ledger: outcomes that said it held up. Saturating — the tenth is worth almost nothing. */
|
|
11706
|
+
confirms: external_exports.number(),
|
|
11707
|
+
/** Ledger: contradictions, in signal units (a full override = 1, weaker signals less).
|
|
11708
|
+
* Linear and priced above the entire confirmation budget, so any full counter wins. */
|
|
11709
|
+
counters: external_exports.number(),
|
|
11710
|
+
/** The agent that asked the question this move came from, when known. Null for a move
|
|
11711
|
+
* distilled from a clarify ruling (those carry no agent) or one whose source rows are gone. */
|
|
11712
|
+
learnedFrom: external_exports.object({ id: external_exports.string(), name: external_exports.string() }).nullable()
|
|
11713
|
+
});
|
|
11553
11714
|
var CreateRequestSchema = external_exports.object({
|
|
11554
11715
|
/** The connection (token id) to send to, from GET /api/tokens. */
|
|
11555
11716
|
tokenId: external_exports.string(),
|
|
@@ -11756,6 +11917,17 @@ var DeviceTokenSchema = external_exports.object({
|
|
|
11756
11917
|
* a default silly name). */
|
|
11757
11918
|
name: external_exports.string(),
|
|
11758
11919
|
device: external_exports.string().nullable(),
|
|
11920
|
+
/** The pairing's assigned voice, cached so the desktop can seed the SAME face the phone
|
|
11921
|
+
* draws — voice is the third ingredient of a hatchling's build (party/traits.ts). */
|
|
11922
|
+
voice: external_exports.string().nullable().optional(),
|
|
11923
|
+
/** The token's server-side id — the face's COLOUR anchor, and the only seed ingredient
|
|
11924
|
+
* that survives a rename. Cached by the host's identity beat. */
|
|
11925
|
+
token_id: external_exports.string().nullable().optional(),
|
|
11926
|
+
/** WHERE this identity works — the folder a wake should land it in. Written by the host
|
|
11927
|
+
* at spawn and by `paigy-harness handoff` from a live terminal. Without it every wake
|
|
11928
|
+
* landed in the FIRST granted workspace and the agent rediscovered its own repo from
|
|
11929
|
+
* the thread each time (host.ts, live catch 2026-08-06 — prompt-papered until now). */
|
|
11930
|
+
workspace: external_exports.string().nullable().optional(),
|
|
11759
11931
|
phone_reveal: PairingRevealSchema.nullable().optional(),
|
|
11760
11932
|
// present once the phone reveals
|
|
11761
11933
|
uik_pub: external_exports.string().nullable().optional()
|
|
@@ -11782,6 +11954,8 @@ var NotificationFeedbackKindSchema = external_exports.enum([
|
|
|
11782
11954
|
// "There should be a picture or design here."
|
|
11783
11955
|
"should_have_called",
|
|
11784
11956
|
// "Don't put this in a banner — ring me for something like this."
|
|
11957
|
+
"should_have_messaged",
|
|
11958
|
+
// the inverse: "that didn't deserve a ring — a message would do."
|
|
11785
11959
|
"other"
|
|
11786
11960
|
// anything else — the note carries it.
|
|
11787
11961
|
]);
|
|
@@ -12078,7 +12252,11 @@ function open(envelope, myKeyId, mySecretKeyB64) {
|
|
|
12078
12252
|
if (!eqCt(sealedCanon, expected)) throw new Error("header mismatch (tampered metadata)");
|
|
12079
12253
|
return { header: envelope.hdr, body };
|
|
12080
12254
|
}
|
|
12081
|
-
|
|
12255
|
+
function sessionSlot(sessionId) {
|
|
12256
|
+
const id = sessionId ?? process.env.PAIGY_SESSION_ID ?? process.env.CLAUDE_CODE_SESSION_ID ?? randomUUID2();
|
|
12257
|
+
return `session:${id.slice(0, 8)}`;
|
|
12258
|
+
}
|
|
12259
|
+
var AGENT_NAME = process.env.PAIGY_AGENT || sessionSlot();
|
|
12082
12260
|
var TOKEN_PATH = join(homedir(), ".paigy", "token.json");
|
|
12083
12261
|
var KEY_PATH = join(homedir(), ".paigy", "key.json");
|
|
12084
12262
|
function readTokenFile() {
|
|
@@ -12093,10 +12271,45 @@ function readTokenFile() {
|
|
|
12093
12271
|
return {};
|
|
12094
12272
|
}
|
|
12095
12273
|
}
|
|
12096
|
-
function
|
|
12097
|
-
const
|
|
12274
|
+
function withTokenLock(mutate) {
|
|
12275
|
+
const lock = TOKEN_PATH + ".lock";
|
|
12276
|
+
const spin = new Int32Array(new SharedArrayBuffer(4));
|
|
12098
12277
|
mkdirSync(join(homedir(), ".paigy"), { recursive: true });
|
|
12099
|
-
|
|
12278
|
+
for (const deadline = Date.now() + 5e3; Date.now() < deadline; ) {
|
|
12279
|
+
try {
|
|
12280
|
+
closeSync(openSync(lock, "wx"));
|
|
12281
|
+
break;
|
|
12282
|
+
} catch {
|
|
12283
|
+
const held = statSync(lock, { throwIfNoEntry: false })?.mtimeMs ?? Date.now();
|
|
12284
|
+
if (Date.now() - held > 5e3) rmSync(lock, { force: true });
|
|
12285
|
+
else Atomics.wait(spin, 0, 0, 25);
|
|
12286
|
+
}
|
|
12287
|
+
}
|
|
12288
|
+
try {
|
|
12289
|
+
return mutate();
|
|
12290
|
+
} finally {
|
|
12291
|
+
rmSync(lock, { force: true });
|
|
12292
|
+
}
|
|
12293
|
+
}
|
|
12294
|
+
function saveToken(token, agent2 = AGENT_NAME) {
|
|
12295
|
+
withTokenLock(() => {
|
|
12296
|
+
const slots = { ...readTokenFile(), [agent2]: token };
|
|
12297
|
+
writeFileSync(TOKEN_PATH, JSON.stringify(slots, null, 2) + "\n", { mode: 384 });
|
|
12298
|
+
});
|
|
12299
|
+
}
|
|
12300
|
+
function updateSlot(agent2, patch) {
|
|
12301
|
+
return withTokenLock(() => {
|
|
12302
|
+
const slots = readTokenFile();
|
|
12303
|
+
const existing = slots[agent2];
|
|
12304
|
+
if (!existing) return false;
|
|
12305
|
+
slots[agent2] = { ...existing, ...patch };
|
|
12306
|
+
writeFileSync(TOKEN_PATH, JSON.stringify(slots, null, 2) + "\n", { mode: 384 });
|
|
12307
|
+
return true;
|
|
12308
|
+
});
|
|
12309
|
+
}
|
|
12310
|
+
function slotIdentity(agent2) {
|
|
12311
|
+
const t = readTokenFile()[agent2];
|
|
12312
|
+
return { name: t?.name ?? null, voice: t?.voice ?? null, tokenId: t?.token_id ?? null, workspace: t?.workspace ?? null };
|
|
12100
12313
|
}
|
|
12101
12314
|
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
12102
12315
|
function readToken(agent2 = AGENT_NAME) {
|
|
@@ -12282,10 +12495,14 @@ async function claimSessions(opts = {}) {
|
|
|
12282
12495
|
return (await res.json()).sessions;
|
|
12283
12496
|
}
|
|
12284
12497
|
async function heartbeat(runtime, opts = {}) {
|
|
12498
|
+
const body = {
|
|
12499
|
+
...runtime !== void 0 ? { runtime } : {},
|
|
12500
|
+
...opts.activity !== void 0 ? { activity: opts.activity } : {}
|
|
12501
|
+
};
|
|
12285
12502
|
const res = ensureAuthed(await reach(`${BACKEND_URL}/api/presence`, {
|
|
12286
12503
|
method: "POST",
|
|
12287
12504
|
headers: { "content-type": "application/json", authorization: `Bearer ${authToken(opts.token)}` },
|
|
12288
|
-
...
|
|
12505
|
+
...Object.keys(body).length > 0 ? { body: JSON.stringify(body) } : {}
|
|
12289
12506
|
}));
|
|
12290
12507
|
if (!res.ok) throw new Error(`heartbeat failed: ${res.status}`);
|
|
12291
12508
|
}
|
|
@@ -12301,7 +12518,7 @@ async function setTaskState(notificationId, state, opts = {}) {
|
|
|
12301
12518
|
|
|
12302
12519
|
// src/main.ts
|
|
12303
12520
|
var import_qrcode = __toESM(require_lib(), 1);
|
|
12304
|
-
import { hostname } from "os";
|
|
12521
|
+
import { homedir as homedir6, hostname } from "os";
|
|
12305
12522
|
|
|
12306
12523
|
// src/harness/catalog.ts
|
|
12307
12524
|
import { spawnSync } from "child_process";
|
|
@@ -12328,6 +12545,14 @@ var CATALOG = [
|
|
|
12328
12545
|
loginHint: "Run `codex login` to authenticate.",
|
|
12329
12546
|
installCli: "curl -fsSL https://chatgpt.com/codex/install.sh | sh",
|
|
12330
12547
|
installAdapter: "npm install -g @agentclientprotocol/codex-acp"
|
|
12548
|
+
},
|
|
12549
|
+
{
|
|
12550
|
+
name: "agy",
|
|
12551
|
+
label: "Antigravity",
|
|
12552
|
+
cli: "agy",
|
|
12553
|
+
authProbe: ["agy", "help"],
|
|
12554
|
+
loginHint: "Install and set up Antigravity (`agy`).",
|
|
12555
|
+
installCli: "curl -fsSL https://antigravity.dev/install.sh | bash"
|
|
12331
12556
|
}
|
|
12332
12557
|
];
|
|
12333
12558
|
function probeDirs(home = homedir2()) {
|
|
@@ -12419,295 +12644,24 @@ function removeWorkspace(dir, deps) {
|
|
|
12419
12644
|
writeFileSync2(deps.file, JSON.stringify(all, null, 2));
|
|
12420
12645
|
return all;
|
|
12421
12646
|
}
|
|
12422
|
-
|
|
12423
|
-
|
|
12424
|
-
|
|
12425
|
-
import { existsSync as existsSync4 } from "fs";
|
|
12426
|
-
import { homedir as homedir4 } from "os";
|
|
12427
|
-
import { resolve as resolve2, delimiter as delimiter2 } from "path";
|
|
12428
|
-
|
|
12429
|
-
// src/harness/acp.ts
|
|
12430
|
-
var none = { events: [], writes: [] };
|
|
12431
|
-
function optionFor(options, decision) {
|
|
12432
|
-
const want = decision.allow ? "allow_once" : "reject_once";
|
|
12433
|
-
return options.find((o) => o.kind === want)?.optionId ?? null;
|
|
12434
|
-
}
|
|
12435
|
-
function createAcpDriver(opts) {
|
|
12436
|
-
return new AcpDriver(opts.cwd, opts.mode, opts.mcp ?? []);
|
|
12647
|
+
function allowed(cwd, deps) {
|
|
12648
|
+
const target = resolve(cwd.replace(/^~(?=$|\/)/, homedir3()));
|
|
12649
|
+
return listWorkspaces(deps).some((w) => target === w || target.startsWith(`${w}/`));
|
|
12437
12650
|
}
|
|
12438
|
-
|
|
12439
|
-
|
|
12440
|
-
|
|
12441
|
-
|
|
12442
|
-
this.mcp = mcp;
|
|
12443
|
-
}
|
|
12444
|
-
cwd;
|
|
12445
|
-
mode;
|
|
12446
|
-
mcp;
|
|
12447
|
-
nextId = 1;
|
|
12448
|
-
initId;
|
|
12449
|
-
sessionNewId;
|
|
12450
|
-
promptId;
|
|
12451
|
-
sessionId;
|
|
12452
|
-
queued = [];
|
|
12453
|
-
/** Options of each unanswered permission request, keyed by its JSON-RPC id. */
|
|
12454
|
-
pending = /* @__PURE__ */ new Map();
|
|
12455
|
-
/** The turn being streamed: text accumulates, tools append, both flush on stopReason. */
|
|
12456
|
-
text = "";
|
|
12457
|
-
tools = [];
|
|
12458
|
-
/** The opening frame. Everything after is driven by responses in `handleLine`. */
|
|
12459
|
-
open() {
|
|
12460
|
-
this.initId = this.nextId++;
|
|
12461
|
-
return [
|
|
12462
|
-
frame({
|
|
12463
|
-
id: this.initId,
|
|
12464
|
-
method: "initialize",
|
|
12465
|
-
params: {
|
|
12466
|
-
protocolVersion: 2,
|
|
12467
|
-
// We are not an editor: no file services offered, the agent uses its own.
|
|
12468
|
-
clientCapabilities: { fs: { readTextFile: false, writeTextFile: false } },
|
|
12469
|
-
clientInfo: { name: "paigy-desktop", version: "0.0.0" }
|
|
12470
|
-
}
|
|
12471
|
-
})
|
|
12472
|
-
];
|
|
12473
|
-
}
|
|
12474
|
-
/** Queue a prompt; it goes out when the session exists and no turn is running. */
|
|
12475
|
-
send(text) {
|
|
12476
|
-
this.queued.push(text);
|
|
12477
|
-
return this.flush();
|
|
12478
|
-
}
|
|
12479
|
-
/** Answer a PermissionEvent (ask mode). The id is the request's JSON-RPC id as a string. */
|
|
12480
|
-
respond(id, decision) {
|
|
12481
|
-
const request = this.pending.get(id);
|
|
12482
|
-
if (!request) return [];
|
|
12483
|
-
this.pending.delete(id);
|
|
12484
|
-
const optionId = optionFor(request.options, decision);
|
|
12485
|
-
return [
|
|
12486
|
-
optionId ? frame({ id: request.id, result: { outcome: { outcome: "selected", optionId } } }) : frame({ id: request.id, result: { outcome: { outcome: "cancelled" } } })
|
|
12487
|
-
];
|
|
12488
|
-
}
|
|
12489
|
-
/** Cancel anything still blocked — called on stop so the process can exit cleanly. */
|
|
12490
|
-
close() {
|
|
12491
|
-
const writes = [...this.pending.values()].map(
|
|
12492
|
-
(r) => frame({ id: r.id, result: { outcome: { outcome: "cancelled" } } })
|
|
12493
|
-
);
|
|
12494
|
-
this.pending.clear();
|
|
12495
|
-
return writes;
|
|
12496
|
-
}
|
|
12497
|
-
handleLine(line) {
|
|
12498
|
-
const trimmed = line.trim();
|
|
12499
|
-
if (!trimmed) return none;
|
|
12500
|
-
let msg;
|
|
12501
|
-
try {
|
|
12502
|
-
msg = JSON.parse(trimmed);
|
|
12503
|
-
} catch {
|
|
12504
|
-
return none;
|
|
12505
|
-
}
|
|
12506
|
-
if (msg.method !== void 0) {
|
|
12507
|
-
return msg.id !== void 0 ? this.handleRequest(msg) : this.handleNotification(msg);
|
|
12508
|
-
}
|
|
12509
|
-
if (msg.id !== void 0) return this.handleResponse(msg);
|
|
12510
|
-
return none;
|
|
12511
|
-
}
|
|
12512
|
-
// ── responses to our requests ──
|
|
12513
|
-
handleResponse(msg) {
|
|
12514
|
-
if (msg.id === this.initId) {
|
|
12515
|
-
this.initId = void 0;
|
|
12516
|
-
if (msg.error) {
|
|
12517
|
-
return { events: [{ kind: "error", message: `initialize failed: ${msg.error.message}` }], writes: [] };
|
|
12518
|
-
}
|
|
12519
|
-
this.sessionNewId = this.nextId++;
|
|
12520
|
-
return {
|
|
12521
|
-
events: [],
|
|
12522
|
-
writes: [frame({ id: this.sessionNewId, method: "session/new", params: { cwd: this.cwd, mcpServers: this.mcp } })]
|
|
12523
|
-
};
|
|
12524
|
-
}
|
|
12525
|
-
if (msg.id === this.sessionNewId) {
|
|
12526
|
-
this.sessionNewId = void 0;
|
|
12527
|
-
const sessionId = msg.result?.sessionId;
|
|
12528
|
-
if (!sessionId) {
|
|
12529
|
-
return {
|
|
12530
|
-
events: [{ kind: "error", message: `session/new failed: ${msg.error?.message ?? "no sessionId"}` }],
|
|
12531
|
-
writes: []
|
|
12532
|
-
};
|
|
12533
|
-
}
|
|
12534
|
-
this.sessionId = sessionId;
|
|
12535
|
-
const value = this.mode === "bypass" ? "bypassPermissions" : "default";
|
|
12536
|
-
const writes = [
|
|
12537
|
-
frame({
|
|
12538
|
-
id: this.nextId++,
|
|
12539
|
-
method: "session/set_config_option",
|
|
12540
|
-
params: { sessionId, configId: "mode", value }
|
|
12541
|
-
}),
|
|
12542
|
-
frame({ id: this.nextId++, method: "session/set_mode", params: { sessionId, modeId: value } })
|
|
12543
|
-
];
|
|
12544
|
-
writes.push(...this.flush());
|
|
12545
|
-
return { events: [], writes };
|
|
12546
|
-
}
|
|
12547
|
-
if (msg.id === this.promptId) {
|
|
12548
|
-
this.promptId = void 0;
|
|
12549
|
-
const events = [];
|
|
12550
|
-
if (this.text.trim() || this.tools.length) {
|
|
12551
|
-
events.push({
|
|
12552
|
-
kind: "turn",
|
|
12553
|
-
role: "agent",
|
|
12554
|
-
text: this.text.trim(),
|
|
12555
|
-
...this.tools.length ? { tools: [...this.tools] } : {}
|
|
12556
|
-
});
|
|
12557
|
-
}
|
|
12558
|
-
const result = this.text.trim();
|
|
12559
|
-
const failed = msg.error !== void 0 || msg.result?.stopReason === "refusal";
|
|
12560
|
-
this.text = "";
|
|
12561
|
-
this.tools = [];
|
|
12562
|
-
const writes = this.flush();
|
|
12563
|
-
if (!writes.length) {
|
|
12564
|
-
events.push({
|
|
12565
|
-
kind: "idle",
|
|
12566
|
-
...result ? { result } : {},
|
|
12567
|
-
...failed ? { failed: true } : {}
|
|
12568
|
-
});
|
|
12569
|
-
}
|
|
12570
|
-
return { events, writes };
|
|
12571
|
-
}
|
|
12572
|
-
return none;
|
|
12573
|
-
}
|
|
12574
|
-
// ── the agent talking to us ──
|
|
12575
|
-
handleNotification(msg) {
|
|
12576
|
-
if (msg.method !== "session/update") return none;
|
|
12577
|
-
const update = msg.params?.update;
|
|
12578
|
-
switch (update?.sessionUpdate) {
|
|
12579
|
-
case "agent_message_chunk":
|
|
12580
|
-
this.text += update.content?.text ?? "";
|
|
12581
|
-
return none;
|
|
12582
|
-
case "tool_call": {
|
|
12583
|
-
const title = update.title?.trim() || update.kind || "tool";
|
|
12584
|
-
this.tools.push(title);
|
|
12585
|
-
return none;
|
|
12586
|
-
}
|
|
12587
|
-
default:
|
|
12588
|
-
return none;
|
|
12589
|
-
}
|
|
12590
|
-
}
|
|
12591
|
-
handleRequest(msg) {
|
|
12592
|
-
if (msg.method !== "session/request_permission") {
|
|
12593
|
-
return {
|
|
12594
|
-
events: [],
|
|
12595
|
-
writes: [frame({ id: msg.id, error: { code: -32601, message: `Method not found: ${msg.method}` } })]
|
|
12596
|
-
};
|
|
12597
|
-
}
|
|
12598
|
-
const id = msg.id;
|
|
12599
|
-
const options = msg.params?.options ?? [];
|
|
12600
|
-
const title = msg.params?.toolCall?.title?.trim() || "a tool call";
|
|
12601
|
-
const tool = msg.params?.toolCall?.kind ?? "tool";
|
|
12602
|
-
if (this.mode === "bypass") {
|
|
12603
|
-
const optionId = optionFor(options, { allow: true }) ?? optionFor(options, { allow: false });
|
|
12604
|
-
return {
|
|
12605
|
-
// The decision still goes through Paigy — as history, not a question. Bypass
|
|
12606
|
-
// means "don't stall the agent", never "don't tell the user".
|
|
12607
|
-
events: [{ kind: "turn", role: "agent", text: `auto-approved: ${title}`, tools: [tool] }],
|
|
12608
|
-
writes: [
|
|
12609
|
-
optionId ? frame({ id, result: { outcome: { outcome: "selected", optionId } } }) : frame({ id, result: { outcome: { outcome: "cancelled" } } })
|
|
12610
|
-
]
|
|
12611
|
-
};
|
|
12612
|
-
}
|
|
12613
|
-
this.pending.set(String(id), { id, options });
|
|
12614
|
-
return {
|
|
12615
|
-
events: [{ kind: "permission", id: String(id), tool, summary: title }],
|
|
12616
|
-
writes: []
|
|
12617
|
-
};
|
|
12618
|
-
}
|
|
12619
|
-
/** Send the queued prompts as one turn, if the agent can take one right now. */
|
|
12620
|
-
flush() {
|
|
12621
|
-
if (!this.sessionId || this.promptId !== void 0 || !this.queued.length) return [];
|
|
12622
|
-
const blocks = this.queued.map((text) => ({ type: "text", text }));
|
|
12623
|
-
this.queued = [];
|
|
12624
|
-
this.promptId = this.nextId++;
|
|
12625
|
-
return [
|
|
12626
|
-
frame({
|
|
12627
|
-
id: this.promptId,
|
|
12628
|
-
method: "session/prompt",
|
|
12629
|
-
params: { sessionId: this.sessionId, prompt: blocks }
|
|
12630
|
-
})
|
|
12631
|
-
];
|
|
12651
|
+
function resolveWakeDir(pinned, deps) {
|
|
12652
|
+
if (pinned && allowed(pinned, deps)) {
|
|
12653
|
+
const dir = resolve(pinned.replace(/^~(?=$|\/)/, homedir3()));
|
|
12654
|
+
if ((deps.exists ?? existsSync3)(dir)) return dir;
|
|
12632
12655
|
}
|
|
12633
|
-
|
|
12634
|
-
var frame = (body) => JSON.stringify({ jsonrpc: "2.0", ...body });
|
|
12635
|
-
|
|
12636
|
-
// src/harness/session.ts
|
|
12637
|
-
var ADAPTER_BIN = {
|
|
12638
|
-
claude: "claude-agent-acp",
|
|
12639
|
-
codex: "codex-acp"
|
|
12640
|
-
};
|
|
12641
|
-
function splitLines(buffer, chunk) {
|
|
12642
|
-
const combined = buffer + chunk;
|
|
12643
|
-
const parts = combined.split("\n");
|
|
12644
|
-
const rest = parts.pop() ?? "";
|
|
12645
|
-
return { lines: parts.filter((l) => l.trim()), rest };
|
|
12646
|
-
}
|
|
12647
|
-
function startSession(opts) {
|
|
12648
|
-
const cwd = resolve2(opts.cwd.replace(/^~(?=$|\/)/, homedir4()));
|
|
12649
|
-
if (!existsSync4(cwd)) {
|
|
12650
|
-
queueMicrotask(() => opts.onEvent({ kind: "error", message: `workspace does not exist: ${cwd}` }));
|
|
12651
|
-
}
|
|
12652
|
-
const child = (opts.spawnFn ?? spawn)(opts.bin ?? ADAPTER_BIN[opts.harness], [], {
|
|
12653
|
-
cwd,
|
|
12654
|
-
// The adapter shells out to its vendor CLI (`claude`, `codex`), and a GUI- or
|
|
12655
|
-
// launchd-launched process's PATH won't have it — probe dirs plus the RUNNING
|
|
12656
|
-
// node's own bin dir (nvm installs the adapters next to node; launchd's bare
|
|
12657
|
-
// PATH knows neither — live catch 2026-08-04: the first slot-wake ENOENT'd).
|
|
12658
|
-
env: {
|
|
12659
|
-
...process.env,
|
|
12660
|
-
PATH: [process.env.PATH, ...probeDirs()].filter(Boolean).join(delimiter2),
|
|
12661
|
-
...opts.token ? { PAIGY_TOKEN: opts.token } : {}
|
|
12662
|
-
},
|
|
12663
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
12664
|
-
});
|
|
12665
|
-
const write = (frames) => {
|
|
12666
|
-
for (const f of frames) child.stdin?.write(`${f}
|
|
12667
|
-
`);
|
|
12668
|
-
};
|
|
12669
|
-
child.stderr?.on("data", (chunk) => {
|
|
12670
|
-
const text = String(chunk).trim();
|
|
12671
|
-
if (text) opts.onEvent({ kind: "error", message: text });
|
|
12672
|
-
});
|
|
12673
|
-
child.on("exit", (code) => {
|
|
12674
|
-
opts.onEvent({ kind: "idle", failed: code !== 0, ...code ? { result: `exited ${code}` } : {} });
|
|
12675
|
-
opts.onExit?.();
|
|
12676
|
-
});
|
|
12677
|
-
child.on("error", (e) => {
|
|
12678
|
-
opts.onEvent({ kind: "error", message: e.message });
|
|
12679
|
-
opts.onExit?.();
|
|
12680
|
-
});
|
|
12681
|
-
const driver = createAcpDriver({ cwd, mode: opts.mode, ...opts.mcp ? { mcp: opts.mcp } : {} });
|
|
12682
|
-
let buffer = "";
|
|
12683
|
-
child.stdout?.on("data", (chunk) => {
|
|
12684
|
-
const { lines, rest } = splitLines(buffer, String(chunk));
|
|
12685
|
-
buffer = rest;
|
|
12686
|
-
for (const line of lines) {
|
|
12687
|
-
const { events, writes } = driver.handleLine(line);
|
|
12688
|
-
write(writes);
|
|
12689
|
-
for (const event of events) opts.onEvent(event);
|
|
12690
|
-
}
|
|
12691
|
-
});
|
|
12692
|
-
write(driver.open());
|
|
12693
|
-
return {
|
|
12694
|
-
send(text) {
|
|
12695
|
-
write(driver.send(text));
|
|
12696
|
-
},
|
|
12697
|
-
respond(id, decision) {
|
|
12698
|
-
write(driver.respond(id, decision));
|
|
12699
|
-
},
|
|
12700
|
-
stop() {
|
|
12701
|
-
write(driver.close());
|
|
12702
|
-
child.kill();
|
|
12703
|
-
}
|
|
12704
|
-
};
|
|
12656
|
+
return listWorkspaces(deps)[0];
|
|
12705
12657
|
}
|
|
12706
12658
|
|
|
12707
12659
|
// ../../packages/schema/dist/index.js
|
|
12708
12660
|
var ContextSchema2 = external_exports.object({
|
|
12709
12661
|
title: external_exports.string().min(1).describe("One-line headline of what you need (required, non-empty)."),
|
|
12710
|
-
description: external_exports.array(external_exports.string().min(1)).
|
|
12662
|
+
description: external_exports.array(external_exports.string().min(1)).describe(
|
|
12663
|
+
"Semantic chunks of detail (each a standalone, non-empty piece). The user can select chunks to ask you to expand. MAY BE EMPTY: a claim whose whole content is its heading \u2014 a single sentence \u2014 has no body, and saying so beats repeating the heading underneath itself. That repeat is what `min(1)` used to force, at 2x the storage, with every reader subtracting it back out at render time."
|
|
12664
|
+
)
|
|
12711
12665
|
});
|
|
12712
12666
|
var ParticipantSchema2 = external_exports.object({
|
|
12713
12667
|
kind: external_exports.enum(["human", "agent"]),
|
|
@@ -12854,6 +12808,14 @@ var NotifyRequestSchema2 = external_exports.object({
|
|
|
12854
12808
|
waiting: external_exports.enum(["none", "soft", "hard"]).optional().describe(
|
|
12855
12809
|
"With `ask`: what happens to your work while you wait. 'none' = you're just informing the user. 'soft' = you'd like an answer but can keep working. 'hard' = you are stopped until they answer (reaches them urgently and escalates to a real phone call if unanswered). Replaces urgencyHint + blocking \u2014 send this one field."
|
|
12856
12810
|
),
|
|
12811
|
+
/** Δ9b (#895): HOLD this claim so the sender can correct the plan before anyone is
|
|
12812
|
+
* interrupted. Opt-in per claim, because the fail-open is a minute-granularity cron —
|
|
12813
|
+
* holding by default would charge every quiet claim that minute before any agent could
|
|
12814
|
+
* correct anything. Ignored for `waiting: 'hard'`: a blocking ask rings on what we have,
|
|
12815
|
+
* and the enrichment can still land mid-call (#781 re-plans the unspoken tail). */
|
|
12816
|
+
confirm: external_exports.boolean().optional().describe(
|
|
12817
|
+
"Hold this one so you can correct the plan before the user is interrupted. The response comes back with `held: true` and the plan; POST the confirm route to release it (with options/visuals/urgency corrections, or nothing at all). If you never do, it is announced anyway a couple of minutes later. Ignored when waiting is 'hard'."
|
|
12818
|
+
),
|
|
12857
12819
|
/** #575: a RELAY of the user's explicitly stated preference, never the agent's
|
|
12858
12820
|
* choice. Outranks waiting in both directions: 'call' rings even for a
|
|
12859
12821
|
* waiting:'none' "call me when it's done"; 'message' never rings even for
|
|
@@ -12887,7 +12849,7 @@ var NotifyRequestSchema2 = external_exports.object({
|
|
|
12887
12849
|
if (r.ask !== void 0) {
|
|
12888
12850
|
for (const f of ["context", "select", "points"]) {
|
|
12889
12851
|
if (r[f] !== void 0)
|
|
12890
|
-
ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: [f], message: `the simplified \`ask\` form takes no ${f} \u2014 the broker derives it
|
|
12852
|
+
ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: [f], message: `the simplified \`ask\` form takes no ${f} \u2014 the broker derives the answer shape from your prose. Drop ${f} and say it in \`ask\` instead ("should I\u2026" for approve/deny, "which of these\u2026" for a pick), passing \`options\` when you're offering concrete alternatives.` });
|
|
12891
12853
|
}
|
|
12892
12854
|
return;
|
|
12893
12855
|
}
|
|
@@ -12903,6 +12865,23 @@ var NotifyRequestSchema2 = external_exports.object({
|
|
|
12903
12865
|
if (!needsOptions && r.options?.length)
|
|
12904
12866
|
ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["options"], message: `select:'${r.select}' takes no options` });
|
|
12905
12867
|
});
|
|
12868
|
+
function unitsOf2(text) {
|
|
12869
|
+
const spans = [];
|
|
12870
|
+
const re = /\n\s*\n+/g;
|
|
12871
|
+
let cursor = 0;
|
|
12872
|
+
const push = (from, to) => {
|
|
12873
|
+
const slice = text.slice(from, to);
|
|
12874
|
+
const lead = slice.length - slice.trimStart().length;
|
|
12875
|
+
const tail = slice.length - slice.trimEnd().length;
|
|
12876
|
+
if (from + lead < to - tail) spans.push({ start: from + lead, end: to - tail });
|
|
12877
|
+
};
|
|
12878
|
+
for (let m = re.exec(text); m; m = re.exec(text)) {
|
|
12879
|
+
push(cursor, m.index);
|
|
12880
|
+
cursor = m.index + m[0].length;
|
|
12881
|
+
}
|
|
12882
|
+
push(cursor, text.length);
|
|
12883
|
+
return spans;
|
|
12884
|
+
}
|
|
12906
12885
|
var NotifyStatusSchema2 = external_exports.enum(["pending", "answered", "ignored"]);
|
|
12907
12886
|
var AgentStateSchema2 = external_exports.enum(["idle", "in_progress", "completed", "needs_input"]);
|
|
12908
12887
|
var SetTaskStateSchema2 = external_exports.object({
|
|
@@ -13085,6 +13064,40 @@ var NotifyResponseSchema2 = external_exports.object({
|
|
|
13085
13064
|
answer: UserAnswerSchema2.optional(),
|
|
13086
13065
|
answeredAt: external_exports.string().datetime().optional()
|
|
13087
13066
|
});
|
|
13067
|
+
var NotifyPlanUnitSchema2 = external_exports.object({
|
|
13068
|
+
notificationId: external_exports.string(),
|
|
13069
|
+
/** The unit's own heading, so the agent can tell which of its paragraphs this became. */
|
|
13070
|
+
title: external_exports.string(),
|
|
13071
|
+
/** How loudly this unit was arbitrated to arrive — per unit, which is the point of units. */
|
|
13072
|
+
level: NotifyLevelSchema2,
|
|
13073
|
+
/** Answered from something the user already decided: nobody is interrupted, and a trail card
|
|
13074
|
+
* says so. The agent should not wait on this one. */
|
|
13075
|
+
settled: external_exports.literal(true).optional(),
|
|
13076
|
+
/** What this unit would need to be answerable and does not carry (#894). A PROPOSAL to the
|
|
13077
|
+
* agent — nothing here changed the ask, and ignoring it costs nothing. */
|
|
13078
|
+
needs: external_exports.array(external_exports.enum(["options", "visuals"])).optional(),
|
|
13079
|
+
/** The SHAPE the broker would give this unit, for the agent to ratify (#886/#894). The
|
|
13080
|
+
* split layer reads prose and can see that a paragraph is a yes/no or a pick-one — but a
|
|
13081
|
+
* broker that DECIDES that destroys the only fact separating a statement from a real ask
|
|
13082
|
+
* (#731), so it is offered, never applied: the unit is stored `text` until the agent
|
|
13083
|
+
* confirms the shape (POST /notify/:id/confirm). Ignoring it costs nothing. */
|
|
13084
|
+
proposal: external_exports.object({
|
|
13085
|
+
select: SelectShapeSchema2,
|
|
13086
|
+
options: external_exports.array(external_exports.object({ label: external_exports.string().min(1) })).optional()
|
|
13087
|
+
}).optional(),
|
|
13088
|
+
/** What the broker READ this unit as wanting from the human (#952 layer 2): a `decision`
|
|
13089
|
+
* between alternatives, an `approval` the agent is blocked on, or `knowledge` it just
|
|
13090
|
+
* needs to know. Reported so the agent can correct a misread the same way it ratifies a
|
|
13091
|
+
* shape — the read RAISES (a decision always asks) and never silences a question the
|
|
13092
|
+
* agent declared (#731, #923). */
|
|
13093
|
+
wants: external_exports.enum(["decision", "approval", "knowledge"]).optional()
|
|
13094
|
+
});
|
|
13095
|
+
var NotifyPlanSchema2 = external_exports.object({
|
|
13096
|
+
units: external_exports.array(NotifyPlanUnitSchema2),
|
|
13097
|
+
/** Which unit is this arrival's ONE interruption (units-design.md D23). Absent means nobody
|
|
13098
|
+
* was interrupted — every unit was either settled or quiet enough to sit in the inbox. */
|
|
13099
|
+
speaks: external_exports.string().optional()
|
|
13100
|
+
});
|
|
13088
13101
|
var UserResponseSchema2 = external_exports.object({
|
|
13089
13102
|
requestId: external_exports.string(),
|
|
13090
13103
|
answer: UserAnswerSchema2,
|
|
@@ -13139,6 +13152,20 @@ var InboxItemSchema2 = external_exports.object({
|
|
|
13139
13152
|
/** The conversation thread + connection this item lives on. Present on the replied
|
|
13140
13153
|
* detail — they power History's "Continue" / "New session from this" (#57/#251). */
|
|
13141
13154
|
parentId: external_exports.string().optional(),
|
|
13155
|
+
/** THE ARRIVAL this row is one unit of (`notifications.ask_id` → `asks`). A claim is one
|
|
13156
|
+
* arrival and its units are N rows of it, so this — not `parentId` — is what makes a
|
|
13157
|
+
* multi-part notification one thing on screen. The thread is the whole CONVERSATION: it
|
|
13158
|
+
* accumulates every message an agent ever sent, so grouping by it renders a day of
|
|
13159
|
+
* unrelated updates as a single "12-part request". Absent on rows written before the
|
|
13160
|
+
* `asks` table, and on anything that never went through `notify` — both fall back to the
|
|
13161
|
+
* thread, which is what the client did for all rows until now. */
|
|
13162
|
+
askId: external_exports.string().optional(),
|
|
13163
|
+
/** WHERE this unit sat in the message it was cut from (`notifications.seq`). The batch
|
|
13164
|
+
* shares one `created_at` to the microsecond, so without it the author's order is
|
|
13165
|
+
* unrecoverable client-side — a four-paragraph briefing rendered opening-paragraph-last
|
|
13166
|
+
* (live 2026-08-10, D35). The API already orders by it; this lets a reader that
|
|
13167
|
+
* re-sorts (grouping, filtering) put an arrival back in the order it was written. */
|
|
13168
|
+
seq: external_exports.number().int().optional(),
|
|
13142
13169
|
tokenId: external_exports.string().optional(),
|
|
13143
13170
|
status: NotifyStatusSchema2,
|
|
13144
13171
|
context: ContextSchema2,
|
|
@@ -13149,6 +13176,16 @@ var InboxItemSchema2 = external_exports.object({
|
|
|
13149
13176
|
* ring/enqueue time. Replaces the condensed line + index-aligned phrased points, which
|
|
13150
13177
|
* between them could not express a call as a sequence. `question: null` is a real turn —
|
|
13151
13178
|
* a status update stays a statement instead of being shaped into a yes/no. */
|
|
13179
|
+
/** Does this claim want an ANSWER, or is it telling you something? Written per row from
|
|
13180
|
+
* `requestAsks` — the agent's own declaration, not a guess. `false` is what earns a card
|
|
13181
|
+
* its acknowledge affordance: without it a status update offers a text box and a dismiss,
|
|
13182
|
+
* and neither of those is "got it" (owner, 2026-08-10). */
|
|
13183
|
+
asks: external_exports.boolean().optional(),
|
|
13184
|
+
/** When a live process last pulsed for this row's agent — the liveness input for
|
|
13185
|
+
* "working requires a pulse" (#928): the list said "Working…" from agent_state alone
|
|
13186
|
+
* while the party called the same dead claim stalled. Absent = no token/no data,
|
|
13187
|
+
* which must never CLAIM stalled. */
|
|
13188
|
+
lastSeenAt: external_exports.string().optional(),
|
|
13152
13189
|
agenda: external_exports.array(AgendaTurnSchema2).optional(),
|
|
13153
13190
|
/** On a replied detail (#397): the next steps the user attached to the answer
|
|
13154
13191
|
* ("call back after lunch") — shown so they can see the commitment was captured. */
|
|
@@ -13173,6 +13210,23 @@ var InboxItemSchema2 = external_exports.object({
|
|
|
13173
13210
|
* client may still flag a stall by age. Drives the inbox error badge + Retry. */
|
|
13174
13211
|
error: external_exports.string().optional(),
|
|
13175
13212
|
clarifies: external_exports.string().optional(),
|
|
13213
|
+
/** The ring ladder ran out while this was still pending — we tried to reach you and
|
|
13214
|
+
* STOPPED trying (`arbitration/arbitrate.ts` `nextRing` → `stop`). Distinct from an
|
|
13215
|
+
* agent with nothing to say, which the roster drew identically until now: "nothing to
|
|
13216
|
+
* say" and "gave up saying it" are opposite situations wearing the same face
|
|
13217
|
+
* (navigation-design.md, gap 1). False for anything that never rang. */
|
|
13218
|
+
gaveUp: external_exports.boolean().default(false),
|
|
13219
|
+
/** Why this arrived the way it did, read back off the delivery receipt (`notify/why.ts`).
|
|
13220
|
+
* Absent for anything never delivered through a push, and for older rows written before
|
|
13221
|
+
* the reason was recorded. Deliberately a debug affordance, shown small (owner,
|
|
13222
|
+
* 2026-08-07) — its real job is to give "this didn't need a call" something to be
|
|
13223
|
+
* feedback ABOUT. */
|
|
13224
|
+
why: external_exports.object({
|
|
13225
|
+
asked: NotifyLevelSchema2,
|
|
13226
|
+
got: NotifyLevelSchema2,
|
|
13227
|
+
because: external_exports.enum(["unresponsive", "dismissed", "not_permitted", "silent", "coalesced", "agent_capped", "unplanned", "learned_raise"]).optional(),
|
|
13228
|
+
line: external_exports.string()
|
|
13229
|
+
}).optional(),
|
|
13176
13230
|
select: external_exports.enum(["one", "many", "rank", "confirm", "text"]).default("one"),
|
|
13177
13231
|
confirmStyle: external_exports.enum(["yesno", "approve"]).default("yesno").describe(
|
|
13178
13232
|
"Labels for a select:'confirm' paige \u2014 'yesno' (Yes/No) or 'approve' (Approve/Deny). Ignored unless select is 'confirm'."
|
|
@@ -13283,6 +13337,15 @@ var HistoryItemSchema2 = external_exports.object({
|
|
|
13283
13337
|
/** When you answered the agent's notification (agent→user only). */
|
|
13284
13338
|
humanAckedAt: external_exports.string().nullable()
|
|
13285
13339
|
});
|
|
13340
|
+
var ACTIVITY_LINES2 = 2;
|
|
13341
|
+
var ACTIVITY_LINE_MAX2 = 80;
|
|
13342
|
+
var AgentActivitySchema2 = external_exports.object({
|
|
13343
|
+
/** Oldest first, so the newest line is last — the one that replaces in place. */
|
|
13344
|
+
lines: external_exports.array(external_exports.string().max(ACTIVITY_LINE_MAX2)).max(ACTIVITY_LINES2),
|
|
13345
|
+
/** When the harness observed this tail. Its own timestamp, not the heartbeat's: a beat
|
|
13346
|
+
* that carries an UNCHANGED tail must not make a stalled agent look like it just moved. */
|
|
13347
|
+
at: external_exports.string().datetime()
|
|
13348
|
+
});
|
|
13286
13349
|
var ConnectionSummarySchema2 = external_exports.object({
|
|
13287
13350
|
/** The connection = the agent's token id (used to address a request). */
|
|
13288
13351
|
id: external_exports.string(),
|
|
@@ -13294,6 +13357,14 @@ var ConnectionSummarySchema2 = external_exports.object({
|
|
|
13294
13357
|
provider: external_exports.string().nullable(),
|
|
13295
13358
|
/** The pairing's assigned voice (#462); null = the default voice. */
|
|
13296
13359
|
voice: VoiceKeySchema2.nullable(),
|
|
13360
|
+
/** The LOUDEST this agent may ever reach you — a ceiling on `NOTIFY_LADDER`, set by the
|
|
13361
|
+
* user on the agent's own page. null = no ceiling (today's behaviour for every
|
|
13362
|
+
* connection). Android binds importance to a relationship rather than to each message,
|
|
13363
|
+
* and that is the thing our roster could not say: "Marlow may always call me; Otto
|
|
13364
|
+
* never may" (navigation-design.md, gap 2). Clamped in `arbitrateLevel`, so it binds
|
|
13365
|
+
* every surface at once and outranks even `sessionMode: all_calls` — a mode the user
|
|
13366
|
+
* set once must not overrule a rule they set about one agent. */
|
|
13367
|
+
reach: NotifyLevelSchema2.nullable().optional(),
|
|
13297
13368
|
createdAt: external_exports.string().datetime(),
|
|
13298
13369
|
/** Most recent notification on this connection, either direction. Null = no contact yet.
|
|
13299
13370
|
* Drives the agents-page recency grouping (Today / This week / …). */
|
|
@@ -13308,10 +13379,49 @@ var ConnectionSummarySchema2 = external_exports.object({
|
|
|
13308
13379
|
harnesses: external_exports.array(external_exports.object({ name: external_exports.string(), label: external_exports.string(), status: external_exports.string() })).optional(),
|
|
13309
13380
|
workspaces: external_exports.array(external_exports.string()).optional()
|
|
13310
13381
|
}).optional(),
|
|
13382
|
+
/** The tail of this agent's working log, when a harness is driving it — the agent page's
|
|
13383
|
+
* live strip. Absent for anything the desktop harness isn't running (a hatched identity
|
|
13384
|
+
* used straight from a terminal emits no work events; the page says so rather than
|
|
13385
|
+
* drawing an empty box). */
|
|
13386
|
+
activity: AgentActivitySchema2.optional(),
|
|
13311
13387
|
/** True = a provider-managed agent running in the provider's cloud (e.g. Anthropic CMA);
|
|
13312
13388
|
* false = a local MCP connection running on the user's computer (Claude Code/Codex/…). */
|
|
13313
13389
|
managed: external_exports.boolean()
|
|
13314
13390
|
});
|
|
13391
|
+
var MoveRingSchema2 = external_exports.enum(["home", "travels", "retired", "quarantined"]);
|
|
13392
|
+
var MoveSchema2 = external_exports.object({
|
|
13393
|
+
id: external_exports.string(),
|
|
13394
|
+
/** The reusable question, as distill normalized it. */
|
|
13395
|
+
question: external_exports.string(),
|
|
13396
|
+
/** The operative ruling. Editable by the user (PATCH) — which resets the ledger. */
|
|
13397
|
+
answer: external_exports.string(),
|
|
13398
|
+
/** The user's stated reason, when they gave one. Null = inherently narrow: the judge is
|
|
13399
|
+
* told so, and the ruling only derives essentially the same question in the same scope. */
|
|
13400
|
+
rationale: external_exports.string().nullable(),
|
|
13401
|
+
/** Where the ruling lives: a repo/workspace, or 'global'. */
|
|
13402
|
+
scope: external_exports.string(),
|
|
13403
|
+
ring: MoveRingSchema2,
|
|
13404
|
+
/** True = the user pinned it with `always` (travel granted by hand, not by evidence). */
|
|
13405
|
+
pinned: external_exports.boolean(),
|
|
13406
|
+
/** True = a pin the user placed was BROKEN by later counter-evidence. Surfaced so the
|
|
13407
|
+
* break is visible instead of a pin silently disappearing. */
|
|
13408
|
+
pinBroken: external_exports.boolean(),
|
|
13409
|
+
/** When the ruling was distilled. */
|
|
13410
|
+
learnedAt: external_exports.string(),
|
|
13411
|
+
/** Last time it answered an ask. Null = never fired. */
|
|
13412
|
+
lastUsedAt: external_exports.string().nullable(),
|
|
13413
|
+
/** How many asks it has answered. Instrumentation — deliberately NOT an input to the
|
|
13414
|
+
* evidence curve: firing says the question keeps arising, not that the ruling is right. */
|
|
13415
|
+
usedCount: external_exports.number(),
|
|
13416
|
+
/** Ledger: outcomes that said it held up. Saturating — the tenth is worth almost nothing. */
|
|
13417
|
+
confirms: external_exports.number(),
|
|
13418
|
+
/** Ledger: contradictions, in signal units (a full override = 1, weaker signals less).
|
|
13419
|
+
* Linear and priced above the entire confirmation budget, so any full counter wins. */
|
|
13420
|
+
counters: external_exports.number(),
|
|
13421
|
+
/** The agent that asked the question this move came from, when known. Null for a move
|
|
13422
|
+
* distilled from a clarify ruling (those carry no agent) or one whose source rows are gone. */
|
|
13423
|
+
learnedFrom: external_exports.object({ id: external_exports.string(), name: external_exports.string() }).nullable()
|
|
13424
|
+
});
|
|
13315
13425
|
var CreateRequestSchema2 = external_exports.object({
|
|
13316
13426
|
/** The connection (token id) to send to, from GET /api/tokens. */
|
|
13317
13427
|
tokenId: external_exports.string(),
|
|
@@ -13518,6 +13628,17 @@ var DeviceTokenSchema2 = external_exports.object({
|
|
|
13518
13628
|
* a default silly name). */
|
|
13519
13629
|
name: external_exports.string(),
|
|
13520
13630
|
device: external_exports.string().nullable(),
|
|
13631
|
+
/** The pairing's assigned voice, cached so the desktop can seed the SAME face the phone
|
|
13632
|
+
* draws — voice is the third ingredient of a hatchling's build (party/traits.ts). */
|
|
13633
|
+
voice: external_exports.string().nullable().optional(),
|
|
13634
|
+
/** The token's server-side id — the face's COLOUR anchor, and the only seed ingredient
|
|
13635
|
+
* that survives a rename. Cached by the host's identity beat. */
|
|
13636
|
+
token_id: external_exports.string().nullable().optional(),
|
|
13637
|
+
/** WHERE this identity works — the folder a wake should land it in. Written by the host
|
|
13638
|
+
* at spawn and by `paigy-harness handoff` from a live terminal. Without it every wake
|
|
13639
|
+
* landed in the FIRST granted workspace and the agent rediscovered its own repo from
|
|
13640
|
+
* the thread each time (host.ts, live catch 2026-08-06 — prompt-papered until now). */
|
|
13641
|
+
workspace: external_exports.string().nullable().optional(),
|
|
13521
13642
|
phone_reveal: PairingRevealSchema2.nullable().optional(),
|
|
13522
13643
|
// present once the phone reveals
|
|
13523
13644
|
uik_pub: external_exports.string().nullable().optional()
|
|
@@ -13544,6 +13665,8 @@ var NotificationFeedbackKindSchema2 = external_exports.enum([
|
|
|
13544
13665
|
// "There should be a picture or design here."
|
|
13545
13666
|
"should_have_called",
|
|
13546
13667
|
// "Don't put this in a banner — ring me for something like this."
|
|
13668
|
+
"should_have_messaged",
|
|
13669
|
+
// the inverse: "that didn't deserve a ring — a message would do."
|
|
13547
13670
|
"other"
|
|
13548
13671
|
// anything else — the note carries it.
|
|
13549
13672
|
]);
|
|
@@ -13564,6 +13687,323 @@ var FeedbackOutcomeSchema2 = external_exports.object({
|
|
|
13564
13687
|
childIds: external_exports.array(external_exports.string()).optional()
|
|
13565
13688
|
});
|
|
13566
13689
|
|
|
13690
|
+
// src/paigy/activity.ts
|
|
13691
|
+
function shortenPaths(s) {
|
|
13692
|
+
return s.replace(/(?<![\w.@+-])(?:\/[\w.@+-]+){3,}/g, (p) => {
|
|
13693
|
+
const parts = p.split("/").filter(Boolean);
|
|
13694
|
+
return `\u2026/${parts.slice(-2).join("/")}`;
|
|
13695
|
+
});
|
|
13696
|
+
}
|
|
13697
|
+
function workLine(event) {
|
|
13698
|
+
const note = (event.note ?? "").split("\n").find((l) => l.trim()) ?? "";
|
|
13699
|
+
const line = shortenPaths([event.tool.trim(), note.trim()].filter(Boolean).join(" \u2014 ").replace(/\s+/g, " "));
|
|
13700
|
+
return line.length > ACTIVITY_LINE_MAX2 ? `${line.slice(0, ACTIVITY_LINE_MAX2 - 1)}\u2026` : line;
|
|
13701
|
+
}
|
|
13702
|
+
function pushWork(lines, line) {
|
|
13703
|
+
if (!line || lines[lines.length - 1] === line) return [...lines];
|
|
13704
|
+
return [...lines, line].slice(-ACTIVITY_LINES2);
|
|
13705
|
+
}
|
|
13706
|
+
function sameTail(a, b) {
|
|
13707
|
+
return a.length === b.length && a.every((l, i) => l === b[i]);
|
|
13708
|
+
}
|
|
13709
|
+
|
|
13710
|
+
// src/harness/session.ts
|
|
13711
|
+
import { spawn } from "child_process";
|
|
13712
|
+
import { existsSync as existsSync4 } from "fs";
|
|
13713
|
+
import { homedir as homedir4 } from "os";
|
|
13714
|
+
import { resolve as resolve2, delimiter as delimiter2 } from "path";
|
|
13715
|
+
|
|
13716
|
+
// src/harness/acp.ts
|
|
13717
|
+
var none = { events: [], writes: [] };
|
|
13718
|
+
function optionFor(options, decision) {
|
|
13719
|
+
const want = decision.allow ? "allow_once" : "reject_once";
|
|
13720
|
+
return options.find((o) => o.kind === want)?.optionId ?? null;
|
|
13721
|
+
}
|
|
13722
|
+
function createAcpDriver(opts) {
|
|
13723
|
+
return new AcpDriver(opts.cwd, opts.mode, opts.mcp ?? []);
|
|
13724
|
+
}
|
|
13725
|
+
var AcpDriver = class {
|
|
13726
|
+
constructor(cwd, mode, mcp = []) {
|
|
13727
|
+
this.cwd = cwd;
|
|
13728
|
+
this.mode = mode;
|
|
13729
|
+
this.mcp = mcp;
|
|
13730
|
+
}
|
|
13731
|
+
cwd;
|
|
13732
|
+
mode;
|
|
13733
|
+
mcp;
|
|
13734
|
+
nextId = 1;
|
|
13735
|
+
initId;
|
|
13736
|
+
sessionNewId;
|
|
13737
|
+
promptId;
|
|
13738
|
+
sessionId;
|
|
13739
|
+
queued = [];
|
|
13740
|
+
/** Options of each unanswered permission request, keyed by its JSON-RPC id. */
|
|
13741
|
+
pending = /* @__PURE__ */ new Map();
|
|
13742
|
+
/** Where the REPORT starts in `text` — everything before the LAST tool call is working
|
|
13743
|
+
* narration ("Now the API endpoints." → runs a tool), and it used to ship: the chunks
|
|
13744
|
+
* concatenate with no separator, so the owner's phone got "…find the repo.Now I have
|
|
13745
|
+
* the full picture. Writing the migration.Now…" as the opening paragraph of a finished
|
|
13746
|
+
* task (live, 2026-08-11 — "looks like a working log"). The narration's audience is the
|
|
13747
|
+
* terminal and host.log; what the agent composed AFTER its last tool call is the part
|
|
13748
|
+
* addressed to a human, and that is what leaves the machine. */
|
|
13749
|
+
reportFrom = 0;
|
|
13750
|
+
/** The turn being streamed: text accumulates, tools append, both flush on stopReason. */
|
|
13751
|
+
text = "";
|
|
13752
|
+
tools = [];
|
|
13753
|
+
/** The opening frame. Everything after is driven by responses in `handleLine`. */
|
|
13754
|
+
open() {
|
|
13755
|
+
this.initId = this.nextId++;
|
|
13756
|
+
return [
|
|
13757
|
+
frame({
|
|
13758
|
+
id: this.initId,
|
|
13759
|
+
method: "initialize",
|
|
13760
|
+
params: {
|
|
13761
|
+
protocolVersion: 2,
|
|
13762
|
+
// We are not an editor: no file services offered, the agent uses its own.
|
|
13763
|
+
clientCapabilities: { fs: { readTextFile: false, writeTextFile: false } },
|
|
13764
|
+
clientInfo: { name: "paigy-desktop", version: "0.0.0" }
|
|
13765
|
+
}
|
|
13766
|
+
})
|
|
13767
|
+
];
|
|
13768
|
+
}
|
|
13769
|
+
/** Queue a prompt; it goes out when the session exists and no turn is running. */
|
|
13770
|
+
send(text) {
|
|
13771
|
+
this.queued.push(text);
|
|
13772
|
+
return this.flush();
|
|
13773
|
+
}
|
|
13774
|
+
/** Answer a PermissionEvent (ask mode). The id is the request's JSON-RPC id as a string. */
|
|
13775
|
+
respond(id, decision) {
|
|
13776
|
+
const request = this.pending.get(id);
|
|
13777
|
+
if (!request) return [];
|
|
13778
|
+
this.pending.delete(id);
|
|
13779
|
+
const optionId = optionFor(request.options, decision);
|
|
13780
|
+
return [
|
|
13781
|
+
optionId ? frame({ id: request.id, result: { outcome: { outcome: "selected", optionId } } }) : frame({ id: request.id, result: { outcome: { outcome: "cancelled" } } })
|
|
13782
|
+
];
|
|
13783
|
+
}
|
|
13784
|
+
/** Cancel anything still blocked — called on stop so the process can exit cleanly. */
|
|
13785
|
+
close() {
|
|
13786
|
+
const writes = [...this.pending.values()].map(
|
|
13787
|
+
(r) => frame({ id: r.id, result: { outcome: { outcome: "cancelled" } } })
|
|
13788
|
+
);
|
|
13789
|
+
this.pending.clear();
|
|
13790
|
+
return writes;
|
|
13791
|
+
}
|
|
13792
|
+
handleLine(line) {
|
|
13793
|
+
const trimmed = line.trim();
|
|
13794
|
+
if (!trimmed) return none;
|
|
13795
|
+
let msg;
|
|
13796
|
+
try {
|
|
13797
|
+
msg = JSON.parse(trimmed);
|
|
13798
|
+
} catch {
|
|
13799
|
+
return none;
|
|
13800
|
+
}
|
|
13801
|
+
if (msg.method !== void 0) {
|
|
13802
|
+
return msg.id !== void 0 ? this.handleRequest(msg) : this.handleNotification(msg);
|
|
13803
|
+
}
|
|
13804
|
+
if (msg.id !== void 0) return this.handleResponse(msg);
|
|
13805
|
+
return none;
|
|
13806
|
+
}
|
|
13807
|
+
// ── responses to our requests ──
|
|
13808
|
+
handleResponse(msg) {
|
|
13809
|
+
if (msg.id === this.initId) {
|
|
13810
|
+
this.initId = void 0;
|
|
13811
|
+
if (msg.error) {
|
|
13812
|
+
return { events: [{ kind: "error", message: `initialize failed: ${msg.error.message}` }], writes: [] };
|
|
13813
|
+
}
|
|
13814
|
+
this.sessionNewId = this.nextId++;
|
|
13815
|
+
return {
|
|
13816
|
+
events: [],
|
|
13817
|
+
writes: [frame({ id: this.sessionNewId, method: "session/new", params: { cwd: this.cwd, mcpServers: this.mcp } })]
|
|
13818
|
+
};
|
|
13819
|
+
}
|
|
13820
|
+
if (msg.id === this.sessionNewId) {
|
|
13821
|
+
this.sessionNewId = void 0;
|
|
13822
|
+
const sessionId = msg.result?.sessionId;
|
|
13823
|
+
if (!sessionId) {
|
|
13824
|
+
return {
|
|
13825
|
+
events: [{ kind: "error", message: `session/new failed: ${msg.error?.message ?? "no sessionId"}` }],
|
|
13826
|
+
writes: []
|
|
13827
|
+
};
|
|
13828
|
+
}
|
|
13829
|
+
this.sessionId = sessionId;
|
|
13830
|
+
const value = this.mode === "bypass" ? "bypassPermissions" : "default";
|
|
13831
|
+
const writes = [
|
|
13832
|
+
frame({
|
|
13833
|
+
id: this.nextId++,
|
|
13834
|
+
method: "session/set_config_option",
|
|
13835
|
+
params: { sessionId, configId: "mode", value }
|
|
13836
|
+
}),
|
|
13837
|
+
frame({ id: this.nextId++, method: "session/set_mode", params: { sessionId, modeId: value } })
|
|
13838
|
+
];
|
|
13839
|
+
writes.push(...this.flush());
|
|
13840
|
+
return { events: [], writes };
|
|
13841
|
+
}
|
|
13842
|
+
if (msg.id === this.promptId) {
|
|
13843
|
+
this.promptId = void 0;
|
|
13844
|
+
const events = [];
|
|
13845
|
+
const report = this.text.slice(this.reportFrom).trim() || this.text.trim();
|
|
13846
|
+
if (report || this.tools.length) {
|
|
13847
|
+
events.push({
|
|
13848
|
+
kind: "turn",
|
|
13849
|
+
role: "agent",
|
|
13850
|
+
text: report,
|
|
13851
|
+
...this.tools.length ? { tools: [...this.tools] } : {}
|
|
13852
|
+
});
|
|
13853
|
+
}
|
|
13854
|
+
const result = report;
|
|
13855
|
+
const failed = msg.error !== void 0 || msg.result?.stopReason === "refusal";
|
|
13856
|
+
this.text = "";
|
|
13857
|
+
this.tools = [];
|
|
13858
|
+
this.reportFrom = 0;
|
|
13859
|
+
const writes = this.flush();
|
|
13860
|
+
if (!writes.length) {
|
|
13861
|
+
events.push({
|
|
13862
|
+
kind: "idle",
|
|
13863
|
+
...result ? { result } : {},
|
|
13864
|
+
...failed ? { failed: true } : {}
|
|
13865
|
+
});
|
|
13866
|
+
}
|
|
13867
|
+
return { events, writes };
|
|
13868
|
+
}
|
|
13869
|
+
return none;
|
|
13870
|
+
}
|
|
13871
|
+
// ── the agent talking to us ──
|
|
13872
|
+
handleNotification(msg) {
|
|
13873
|
+
if (msg.method !== "session/update") return none;
|
|
13874
|
+
const update = msg.params?.update;
|
|
13875
|
+
switch (update?.sessionUpdate) {
|
|
13876
|
+
case "agent_message_chunk":
|
|
13877
|
+
this.text += update.content?.text ?? "";
|
|
13878
|
+
return none;
|
|
13879
|
+
case "tool_call": {
|
|
13880
|
+
const title = update.title?.trim() || update.kind || "tool";
|
|
13881
|
+
this.tools.push(title);
|
|
13882
|
+
const note = this.text.slice(this.reportFrom).trim();
|
|
13883
|
+
this.reportFrom = this.text.length;
|
|
13884
|
+
return { events: [{ kind: "work", tool: title, ...note ? { note } : {} }], writes: [] };
|
|
13885
|
+
}
|
|
13886
|
+
default:
|
|
13887
|
+
return none;
|
|
13888
|
+
}
|
|
13889
|
+
}
|
|
13890
|
+
handleRequest(msg) {
|
|
13891
|
+
if (msg.method !== "session/request_permission") {
|
|
13892
|
+
return {
|
|
13893
|
+
events: [],
|
|
13894
|
+
writes: [frame({ id: msg.id, error: { code: -32601, message: `Method not found: ${msg.method}` } })]
|
|
13895
|
+
};
|
|
13896
|
+
}
|
|
13897
|
+
const id = msg.id;
|
|
13898
|
+
const options = msg.params?.options ?? [];
|
|
13899
|
+
const title = msg.params?.toolCall?.title?.trim() || "a tool call";
|
|
13900
|
+
const tool = msg.params?.toolCall?.kind ?? "tool";
|
|
13901
|
+
if (this.mode === "bypass") {
|
|
13902
|
+
const optionId = optionFor(options, { allow: true }) ?? optionFor(options, { allow: false });
|
|
13903
|
+
return {
|
|
13904
|
+
// The decision still goes through Paigy — as history, not a question. Bypass
|
|
13905
|
+
// means "don't stall the agent", never "don't tell the user".
|
|
13906
|
+
events: [{ kind: "turn", role: "agent", text: `auto-approved: ${title}`, tools: [tool] }],
|
|
13907
|
+
writes: [
|
|
13908
|
+
optionId ? frame({ id, result: { outcome: { outcome: "selected", optionId } } }) : frame({ id, result: { outcome: { outcome: "cancelled" } } })
|
|
13909
|
+
]
|
|
13910
|
+
};
|
|
13911
|
+
}
|
|
13912
|
+
this.pending.set(String(id), { id, options });
|
|
13913
|
+
return {
|
|
13914
|
+
events: [{ kind: "permission", id: String(id), tool, summary: title }],
|
|
13915
|
+
writes: []
|
|
13916
|
+
};
|
|
13917
|
+
}
|
|
13918
|
+
/** Send the queued prompts as one turn, if the agent can take one right now. */
|
|
13919
|
+
flush() {
|
|
13920
|
+
if (!this.sessionId || this.promptId !== void 0 || !this.queued.length) return [];
|
|
13921
|
+
const blocks = this.queued.map((text) => ({ type: "text", text }));
|
|
13922
|
+
this.queued = [];
|
|
13923
|
+
this.promptId = this.nextId++;
|
|
13924
|
+
return [
|
|
13925
|
+
frame({
|
|
13926
|
+
id: this.promptId,
|
|
13927
|
+
method: "session/prompt",
|
|
13928
|
+
params: { sessionId: this.sessionId, prompt: blocks }
|
|
13929
|
+
})
|
|
13930
|
+
];
|
|
13931
|
+
}
|
|
13932
|
+
};
|
|
13933
|
+
var frame = (body) => JSON.stringify({ jsonrpc: "2.0", ...body });
|
|
13934
|
+
|
|
13935
|
+
// src/harness/session.ts
|
|
13936
|
+
var ADAPTER_BIN = {
|
|
13937
|
+
claude: "claude-agent-acp",
|
|
13938
|
+
codex: "codex-acp",
|
|
13939
|
+
agy: "agy"
|
|
13940
|
+
};
|
|
13941
|
+
function splitLines(buffer, chunk) {
|
|
13942
|
+
const combined = buffer + chunk;
|
|
13943
|
+
const parts = combined.split("\n");
|
|
13944
|
+
const rest = parts.pop() ?? "";
|
|
13945
|
+
return { lines: parts.filter((l) => l.trim()), rest };
|
|
13946
|
+
}
|
|
13947
|
+
function startSession(opts) {
|
|
13948
|
+
const cwd = resolve2(opts.cwd.replace(/^~(?=$|\/)/, homedir4()));
|
|
13949
|
+
if (!existsSync4(cwd)) {
|
|
13950
|
+
queueMicrotask(() => opts.onEvent({ kind: "error", message: `workspace does not exist: ${cwd}` }));
|
|
13951
|
+
}
|
|
13952
|
+
const child = (opts.spawnFn ?? spawn)(opts.bin ?? ADAPTER_BIN[opts.harness], [], {
|
|
13953
|
+
cwd,
|
|
13954
|
+
// The adapter shells out to its vendor CLI (`claude`, `codex`), and a GUI- or
|
|
13955
|
+
// launchd-launched process's PATH won't have it — probe dirs plus the RUNNING
|
|
13956
|
+
// node's own bin dir (nvm installs the adapters next to node; launchd's bare
|
|
13957
|
+
// PATH knows neither — live catch 2026-08-04: the first slot-wake ENOENT'd).
|
|
13958
|
+
env: {
|
|
13959
|
+
...process.env,
|
|
13960
|
+
PATH: [process.env.PATH, ...probeDirs()].filter(Boolean).join(delimiter2),
|
|
13961
|
+
...opts.token ? { PAIGY_TOKEN: opts.token } : {}
|
|
13962
|
+
},
|
|
13963
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
13964
|
+
});
|
|
13965
|
+
const write = (frames) => {
|
|
13966
|
+
for (const f of frames) child.stdin?.write(`${f}
|
|
13967
|
+
`);
|
|
13968
|
+
};
|
|
13969
|
+
child.stderr?.on("data", (chunk) => {
|
|
13970
|
+
const text = String(chunk).trim();
|
|
13971
|
+
if (text) opts.onEvent({ kind: "error", message: text });
|
|
13972
|
+
});
|
|
13973
|
+
child.on("exit", (code) => {
|
|
13974
|
+
opts.onEvent({ kind: "idle", failed: code !== 0, ...code ? { result: `exited ${code}` } : {} });
|
|
13975
|
+
opts.onExit?.();
|
|
13976
|
+
});
|
|
13977
|
+
child.on("error", (e) => {
|
|
13978
|
+
opts.onEvent({ kind: "error", message: e.message });
|
|
13979
|
+
opts.onExit?.();
|
|
13980
|
+
});
|
|
13981
|
+
const driver = createAcpDriver({ cwd, mode: opts.mode, ...opts.mcp ? { mcp: opts.mcp } : {} });
|
|
13982
|
+
let buffer = "";
|
|
13983
|
+
child.stdout?.on("data", (chunk) => {
|
|
13984
|
+
const { lines, rest } = splitLines(buffer, String(chunk));
|
|
13985
|
+
buffer = rest;
|
|
13986
|
+
for (const line of lines) {
|
|
13987
|
+
const { events, writes } = driver.handleLine(line);
|
|
13988
|
+
write(writes);
|
|
13989
|
+
for (const event of events) opts.onEvent(event);
|
|
13990
|
+
}
|
|
13991
|
+
});
|
|
13992
|
+
write(driver.open());
|
|
13993
|
+
return {
|
|
13994
|
+
send(text) {
|
|
13995
|
+
write(driver.send(text));
|
|
13996
|
+
},
|
|
13997
|
+
respond(id, decision) {
|
|
13998
|
+
write(driver.respond(id, decision));
|
|
13999
|
+
},
|
|
14000
|
+
stop() {
|
|
14001
|
+
write(driver.close());
|
|
14002
|
+
child.kill();
|
|
14003
|
+
}
|
|
14004
|
+
};
|
|
14005
|
+
}
|
|
14006
|
+
|
|
13567
14007
|
// src/paigy/conversation.ts
|
|
13568
14008
|
function endsWithQuestion(text) {
|
|
13569
14009
|
if (!text) return false;
|
|
@@ -13658,6 +14098,9 @@ function levelFor(event) {
|
|
|
13658
14098
|
case "idle":
|
|
13659
14099
|
if (endsWithQuestion(event.result)) return "banner";
|
|
13660
14100
|
return event.failed ? "push" : "inbox";
|
|
14101
|
+
case "work":
|
|
14102
|
+
return "inbox";
|
|
14103
|
+
// moot — entryFor drops work before a level ever matters
|
|
13661
14104
|
case "error":
|
|
13662
14105
|
return "inbox";
|
|
13663
14106
|
}
|
|
@@ -13678,16 +14121,14 @@ function entryFor(event, state = {}) {
|
|
|
13678
14121
|
switch (event.kind) {
|
|
13679
14122
|
case "turn": {
|
|
13680
14123
|
const who = event.role === "agent" ? "Agent" : "You";
|
|
13681
|
-
const
|
|
13682
|
-
const description = [event.text, event.tools?.length ? `Tools: ${event.tools.join(", ")}` : null].filter((c) => Boolean(c));
|
|
14124
|
+
const body = event.text ? event.role === "agent" ? event.text : `${who}: ${event.text}` : `${who} ran ${(event.tools ?? []).join(", ")}.`;
|
|
13683
14125
|
return {
|
|
13684
14126
|
...common,
|
|
13685
|
-
|
|
13686
|
-
//
|
|
13687
|
-
//
|
|
13688
|
-
//
|
|
13689
|
-
//
|
|
13690
|
-
select: "text"
|
|
14127
|
+
ask: body
|
|
14128
|
+
// No `select`: the contract refuses it on the `ask` form ("the broker derives it"),
|
|
14129
|
+
// and with no options it derives "text" anyway — the shape this wants, since any
|
|
14130
|
+
// inbox row can be replied to and a reply to history is just the user initiating
|
|
14131
|
+
// (the pump feeds it back in).
|
|
13691
14132
|
};
|
|
13692
14133
|
}
|
|
13693
14134
|
case "permission":
|
|
@@ -13709,16 +14150,24 @@ function entryFor(event, state = {}) {
|
|
|
13709
14150
|
};
|
|
13710
14151
|
case "idle": {
|
|
13711
14152
|
const asking = endsWithQuestion(event.result);
|
|
14153
|
+
const result = event.result?.trim();
|
|
14154
|
+
const norm = (t) => (t ?? "").replace(/\s+/g, " ").trim();
|
|
14155
|
+
if (result && !event.failed && norm(result) === norm(state.lastAgentText)) {
|
|
14156
|
+
if (!asking) return null;
|
|
14157
|
+
const spans = unitsOf2(result);
|
|
14158
|
+
const tail = spans.map((sp) => result.slice(sp.start, sp.end)).reverse().find((t) => t.includes("?"));
|
|
14159
|
+
return { ...common, ask: tail ?? result, blocking: true };
|
|
14160
|
+
}
|
|
14161
|
+
const lead = asking ? "" : event.failed ? "The agent stopped without finishing. " : "Turn complete. ";
|
|
13712
14162
|
return {
|
|
13713
14163
|
...common,
|
|
13714
|
-
|
|
13715
|
-
title: asking ? clip(firstLine(event.result ?? "")) : event.failed ? "The agent stopped without finishing" : "Turn complete",
|
|
13716
|
-
description: [event.result || (event.failed ? "No result reported." : "Done.")]
|
|
13717
|
-
},
|
|
13718
|
-
select: "text",
|
|
14164
|
+
ask: `${lead}${result || (event.failed ? "No result reported." : "Done.")}`,
|
|
13719
14165
|
...asking ? { blocking: true } : {}
|
|
13720
14166
|
};
|
|
13721
14167
|
}
|
|
14168
|
+
case "work":
|
|
14169
|
+
return null;
|
|
14170
|
+
// log-only by contract — the live texture of the working log
|
|
13722
14171
|
case "error":
|
|
13723
14172
|
return null;
|
|
13724
14173
|
}
|
|
@@ -13740,9 +14189,16 @@ function decisionFrom(answer) {
|
|
|
13740
14189
|
}
|
|
13741
14190
|
async function mirror(event, state, deps = {}) {
|
|
13742
14191
|
const entry = entryFor(event, state);
|
|
14192
|
+
if (event.kind === "turn" && event.role === "agent" && event.text) state.lastAgentText = event.text;
|
|
13743
14193
|
if (!entry) return {};
|
|
14194
|
+
let req;
|
|
14195
|
+
try {
|
|
14196
|
+
req = NotifyRequestSchema2.parse(entry);
|
|
14197
|
+
} catch (e) {
|
|
14198
|
+
console.error(`paigy: mirror entry failed the contract \u2014 a bridge bug, not the network: ${e instanceof Error ? e.message.slice(0, 300) : String(e)}`);
|
|
14199
|
+
return {};
|
|
14200
|
+
}
|
|
13744
14201
|
try {
|
|
13745
|
-
const req = NotifyRequestSchema2.parse(entry);
|
|
13746
14202
|
const { notificationId, parentId } = await (deps.submit ?? submitNotification)(req);
|
|
13747
14203
|
(state.mine ??= /* @__PURE__ */ new Set()).add(notificationId);
|
|
13748
14204
|
return { parentId, notificationId };
|
|
@@ -13771,8 +14227,9 @@ async function askQuestion(question, state, deps = {}) {
|
|
|
13771
14227
|
...state.parentId ? { parentId: state.parentId } : {},
|
|
13772
14228
|
...state.repo ? { repo: state.repo } : {},
|
|
13773
14229
|
...state.branch ? { branch: state.branch } : {},
|
|
13774
|
-
|
|
13775
|
-
|
|
14230
|
+
// Prose in — see `entryFor`'s `turn` case. An agent's question is the case most likely
|
|
14231
|
+
// to run long, and its title was a clipped prefix of itself.
|
|
14232
|
+
ask: question,
|
|
13776
14233
|
blocking: true,
|
|
13777
14234
|
urgency: "banner"
|
|
13778
14235
|
};
|
|
@@ -13807,12 +14264,12 @@ function spokenText(answer) {
|
|
|
13807
14264
|
return null;
|
|
13808
14265
|
}
|
|
13809
14266
|
}
|
|
13810
|
-
var firstLine = (s) => s.split("\n")[0] ?? s;
|
|
13811
14267
|
var clip = (s) => (s.length > 120 ? `${s.slice(0, 117)}\u2026` : s) || "(no text)";
|
|
13812
14268
|
|
|
13813
14269
|
// src/run.ts
|
|
13814
14270
|
function runHarness(opts) {
|
|
13815
14271
|
let running = true;
|
|
14272
|
+
let tail = [];
|
|
13816
14273
|
const state = { ...opts.exclusive ? { exclusive: true } : {} };
|
|
13817
14274
|
let session = null;
|
|
13818
14275
|
const asMe = { token: opts.token };
|
|
@@ -13840,11 +14297,17 @@ function runHarness(opts) {
|
|
|
13840
14297
|
opts.log(decision.allow ? `\u2713 approved: ${event.summary}` : `\u2717 denied: ${event.summary}`);
|
|
13841
14298
|
return;
|
|
13842
14299
|
}
|
|
14300
|
+
if (event.kind === "work") {
|
|
14301
|
+
opts.log(`\u2699 ${event.tool}${event.note ? ` \u2014 ${event.note.split("\n")[0] ?? ""}` : ""}`);
|
|
14302
|
+
tail = pushWork(tail, workLine(event));
|
|
14303
|
+
return;
|
|
14304
|
+
}
|
|
13843
14305
|
const { parentId } = await mirror(event, state, deps);
|
|
13844
14306
|
state.parentId ??= parentId;
|
|
13845
14307
|
if (event.kind === "turn") opts.log(`${event.role}: ${event.text.split("\n")[0] ?? ""}`);
|
|
13846
14308
|
if (event.kind === "idle") {
|
|
13847
14309
|
state.resting = true;
|
|
14310
|
+
tail = [];
|
|
13848
14311
|
if (endsWithQuestion(event.result)) {
|
|
13849
14312
|
const local = await opts.localAsk?.question?.(event.result ?? "") ?? null;
|
|
13850
14313
|
if (local?.trim() && session && running) {
|
|
@@ -13882,8 +14345,10 @@ function runHarness(opts) {
|
|
|
13882
14345
|
session?.send(text);
|
|
13883
14346
|
},
|
|
13884
14347
|
working: () => running && state.resting !== true,
|
|
14348
|
+
tail: () => [...tail],
|
|
13885
14349
|
stop() {
|
|
13886
14350
|
running = false;
|
|
14351
|
+
tail = [];
|
|
13887
14352
|
cancelAsks(state);
|
|
13888
14353
|
session?.stop();
|
|
13889
14354
|
session = null;
|
|
@@ -13915,16 +14380,22 @@ function hostElsewhere() {
|
|
|
13915
14380
|
function startHost(opts) {
|
|
13916
14381
|
const runs = /* @__PURE__ */ new Map();
|
|
13917
14382
|
const asHost = { token: opts.token };
|
|
13918
|
-
const
|
|
14383
|
+
const refreshIdentities = async () => {
|
|
13919
14384
|
for (const slot of listSlots()) {
|
|
13920
14385
|
const token = readToken(slot);
|
|
13921
14386
|
if (!token) continue;
|
|
13922
14387
|
const me = await whoAmI({ token }).catch(() => null);
|
|
13923
|
-
if (me
|
|
14388
|
+
if (!me) continue;
|
|
14389
|
+
const have = slotIdentity(slot);
|
|
14390
|
+
if (me.name !== have.name || (me.voice ?? null) !== have.voice || (me.tokenId ?? null) !== have.tokenId) {
|
|
14391
|
+
updateSlot(slot, { ...me.name ? { name: me.name } : {}, voice: me.voice ?? null, token_id: me.tokenId ?? null });
|
|
14392
|
+
}
|
|
13924
14393
|
}
|
|
13925
14394
|
};
|
|
13926
14395
|
const beat = () => {
|
|
13927
|
-
void
|
|
14396
|
+
void refreshIdentities();
|
|
14397
|
+
for (const r of runs.values()) if (r.token) void heartbeat(void 0, { token: r.token }).catch(() => {
|
|
14398
|
+
});
|
|
13928
14399
|
void heartbeat({
|
|
13929
14400
|
harnesses: detectAll().map((a) => ({ name: a.name, label: a.label, status: a.status })),
|
|
13930
14401
|
workspaces: listWorkspaces(opts.wsDeps)
|
|
@@ -13949,25 +14420,28 @@ function startHost(opts) {
|
|
|
13949
14420
|
},
|
|
13950
14421
|
log: log2
|
|
13951
14422
|
});
|
|
13952
|
-
runs.set(spec.sessionId, { run, label });
|
|
14423
|
+
runs.set(spec.sessionId, { run, label, token: spec.token });
|
|
14424
|
+
void heartbeat(void 0, { token: spec.token }).catch(() => {
|
|
14425
|
+
});
|
|
14426
|
+
void refreshIdentities();
|
|
13953
14427
|
try {
|
|
13954
|
-
saveToken({ access_token: spec.token, name: label, device: null },
|
|
14428
|
+
saveToken({ access_token: spec.token, name: label, device: null, workspace: spec.workspace }, sessionSlot(spec.sessionId));
|
|
13955
14429
|
} catch {
|
|
13956
14430
|
}
|
|
13957
14431
|
opts.log(`\u25B6 phone-launched ${label} (${spec.harness}) in ${spec.workspace}`);
|
|
13958
14432
|
}
|
|
13959
14433
|
}
|
|
13960
|
-
const SLOT_HARNESS = { "mcp-agent": "claude", codex: "codex" };
|
|
13961
|
-
const WAKE_PROMPT = "You were woken because Paigy work is waiting for you. This is a fresh session with your existing identity, so you may already have work in flight that you can't remember. Call check_replies FIRST, then get_thread on each conversation it returns and read it before you touch anything: it holds what you were doing, where (a repo or worktree may not be this folder), and what the owner already decided. Then handle what's waiting and follow your paigy instructions to stay in the loop.";
|
|
14434
|
+
const SLOT_HARNESS = { "mcp-agent": "claude", codex: "codex", antigravity: "agy" };
|
|
14435
|
+
const WAKE_PROMPT = "You were woken because Paigy work is waiting for you. This is a fresh session with your existing identity, so you may already have work in flight that you can't remember. Call check_replies FIRST, then get_thread on each conversation it returns and read it before you touch anything: it holds what you were doing, where (a repo or worktree may not be this folder), and what the owner already decided. Then handle what's waiting and follow your paigy instructions to stay in the loop. Speak to the owner at exactly TWO moments: when you are BLOCKED on a decision only they can make (contact with waiting:'hard', the decision as the ask, options if you have real ones), and when you are DONE (one short report: what shipped, how you verified it, anything you flagged). Progress is never a contact \u2014 call set_task_state('in_progress') when you pick work up and the app shows you working; narrate to the terminal, not to the human.";
|
|
13962
14436
|
async function sweepSlots() {
|
|
13963
|
-
const workspace = listWorkspaces(opts.wsDeps)[0];
|
|
13964
|
-
if (!workspace) return;
|
|
13965
14437
|
for (const slot of listSlots()) {
|
|
13966
14438
|
const harness = SLOT_HARNESS[slot] ?? (slot === "Desktop" ? void 0 : "claude");
|
|
13967
14439
|
const key = `slot:${slot}`;
|
|
13968
14440
|
if (!harness || runs.has(key)) continue;
|
|
13969
14441
|
const token = readToken(slot);
|
|
13970
14442
|
if (!token) continue;
|
|
14443
|
+
const workspace = resolveWakeDir(slotIdentity(slot).workspace, opts.wsDeps);
|
|
14444
|
+
if (!workspace) continue;
|
|
13971
14445
|
const work = await checkReplies({ token }).catch(() => null);
|
|
13972
14446
|
if (!work || work.requests.length === 0 && work.replies.length === 0) continue;
|
|
13973
14447
|
const label = slotName(slot) ?? slot;
|
|
@@ -13986,10 +14460,33 @@ function startHost(opts) {
|
|
|
13986
14460
|
},
|
|
13987
14461
|
log: log2
|
|
13988
14462
|
});
|
|
13989
|
-
runs.set(key, { run, label });
|
|
14463
|
+
runs.set(key, { run, label, token });
|
|
14464
|
+
void heartbeat(void 0, { token }).catch(() => {
|
|
14465
|
+
});
|
|
13990
14466
|
opts.log(`\u25B6 woke ${label} (${harness}) \u2014 work was waiting in ${workspace}`);
|
|
13991
14467
|
}
|
|
13992
14468
|
}
|
|
14469
|
+
const ACTIVITY_MS = 2e3;
|
|
14470
|
+
const published = /* @__PURE__ */ new Map();
|
|
14471
|
+
const streamActivity = () => {
|
|
14472
|
+
const publishTail = (token, lines) => {
|
|
14473
|
+
void heartbeat(void 0, { token, activity: { lines, at: (/* @__PURE__ */ new Date()).toISOString() } }).catch(() => {
|
|
14474
|
+
});
|
|
14475
|
+
};
|
|
14476
|
+
for (const [key, r] of runs) {
|
|
14477
|
+
if (!r.token) continue;
|
|
14478
|
+
const lines = r.run.tail();
|
|
14479
|
+
const was = published.get(key);
|
|
14480
|
+
if (was && sameTail(was.lines, lines)) continue;
|
|
14481
|
+
published.set(key, { token: r.token, lines });
|
|
14482
|
+
publishTail(r.token, lines);
|
|
14483
|
+
}
|
|
14484
|
+
for (const [key, was] of published) {
|
|
14485
|
+
if (runs.has(key)) continue;
|
|
14486
|
+
published.delete(key);
|
|
14487
|
+
if (was.lines.length > 0) publishTail(was.token, []);
|
|
14488
|
+
}
|
|
14489
|
+
};
|
|
13993
14490
|
const publish = () => {
|
|
13994
14491
|
try {
|
|
13995
14492
|
writeFileSync3(HOST_FILE, JSON.stringify({ pid: process.pid, at: Date.now(), roster: api.roster() }));
|
|
@@ -14003,6 +14500,7 @@ function startHost(opts) {
|
|
|
14003
14500
|
void sweepSlots();
|
|
14004
14501
|
publish();
|
|
14005
14502
|
}, 5e3);
|
|
14503
|
+
const activityTick = setInterval(streamActivity, ACTIVITY_MS);
|
|
14006
14504
|
const stopSessions = () => {
|
|
14007
14505
|
for (const { run, label } of runs.values()) {
|
|
14008
14506
|
run.stop();
|
|
@@ -14019,18 +14517,29 @@ function startHost(opts) {
|
|
|
14019
14517
|
const live = new Map([...runs.values()].map((r) => [r.label, r.run]));
|
|
14020
14518
|
const names = /* @__PURE__ */ new Map();
|
|
14021
14519
|
for (const key of listSlots()) if (key !== "Desktop") names.set(slotName(key) ?? key, key);
|
|
14022
|
-
for (const label of live.keys()) if (!names.has(label)) names.set(label,
|
|
14023
|
-
return [...names.
|
|
14024
|
-
|
|
14025
|
-
|
|
14026
|
-
|
|
14027
|
-
|
|
14520
|
+
for (const label of live.keys()) if (!names.has(label)) names.set(label, null);
|
|
14521
|
+
return [...names.entries()].map(([name, slot]) => {
|
|
14522
|
+
const id = slot ? slotIdentity(slot) : { voice: null, tokenId: null };
|
|
14523
|
+
return {
|
|
14524
|
+
name,
|
|
14525
|
+
// The slot key rides along so the window's agent page can speak AS this agent —
|
|
14526
|
+
// its pending reads (`checkReplies`, the same pure read the sweep does) need the
|
|
14527
|
+
// slot's own token, and the display name is not a key.
|
|
14528
|
+
slot,
|
|
14529
|
+
tokenId: id.tokenId,
|
|
14530
|
+
voice: id.voice,
|
|
14531
|
+
running: live.has(name),
|
|
14532
|
+
working: live.get(name)?.working() ?? false
|
|
14533
|
+
};
|
|
14534
|
+
});
|
|
14028
14535
|
},
|
|
14029
14536
|
stopSessions,
|
|
14030
14537
|
stop() {
|
|
14031
14538
|
clearInterval(pulse);
|
|
14032
14539
|
clearInterval(spawnPoll);
|
|
14033
14540
|
stopSessions();
|
|
14541
|
+
streamActivity();
|
|
14542
|
+
clearInterval(activityTick);
|
|
14034
14543
|
try {
|
|
14035
14544
|
writeFileSync3(HOST_FILE, JSON.stringify({ pid: process.pid, at: 0, roster: [] }));
|
|
14036
14545
|
} catch {
|
|
@@ -14045,7 +14554,7 @@ function startHost(opts) {
|
|
|
14045
14554
|
var here = dirname4(fileURLToPath(import.meta.url));
|
|
14046
14555
|
var wsDeps = { file: workspacesFile() };
|
|
14047
14556
|
var DEVICE_SLOT = "Desktop";
|
|
14048
|
-
var deviceToken = () => readToken(DEVICE_SLOT)
|
|
14557
|
+
var deviceToken = () => readToken(DEVICE_SLOT);
|
|
14049
14558
|
var win = null;
|
|
14050
14559
|
function log(message) {
|
|
14051
14560
|
win?.webContents.send("paigy:log", message);
|
|
@@ -14053,8 +14562,13 @@ function log(message) {
|
|
|
14053
14562
|
var ICON = join5(here, "..", "assets", "icon.png");
|
|
14054
14563
|
function createWindow() {
|
|
14055
14564
|
win = new BrowserWindow({
|
|
14056
|
-
|
|
14057
|
-
|
|
14565
|
+
// Sized for the working log, which is now the window's centerpiece; the layout is a
|
|
14566
|
+
// single fluid column (max-width 1080, logs wrap at any token), so any size between
|
|
14567
|
+
// the min and a full screen holds without a horizontal scrollbar.
|
|
14568
|
+
width: 880,
|
|
14569
|
+
height: 820,
|
|
14570
|
+
minWidth: 560,
|
|
14571
|
+
minHeight: 520,
|
|
14058
14572
|
icon: ICON,
|
|
14059
14573
|
// Windows/Linux window icon; macOS uses the dock icon below
|
|
14060
14574
|
webPreferences: { preload: join5(here, "preload.cjs"), contextIsolation: true, nodeIntegration: false }
|
|
@@ -14112,6 +14626,31 @@ ipcMain.handle("paigy:beacon", async () => {
|
|
|
14112
14626
|
})();
|
|
14113
14627
|
return { qr, code: code.user_code };
|
|
14114
14628
|
});
|
|
14629
|
+
ipcMain.handle("paigy:agent", async (_e, name) => {
|
|
14630
|
+
const entry = (host?.roster() ?? readHostState()?.roster ?? []).find((a) => a.name === name);
|
|
14631
|
+
if (!entry) return null;
|
|
14632
|
+
const token = entry.slot ? readToken(entry.slot) : "";
|
|
14633
|
+
const pending = token ? await checkReplies({ token }).catch(() => null) : null;
|
|
14634
|
+
return {
|
|
14635
|
+
...entry,
|
|
14636
|
+
pending: pending ? {
|
|
14637
|
+
requests: pending.requests.slice(0, 5).map((r) => ({
|
|
14638
|
+
line: (r.text.split("\n").find(Boolean) ?? "").slice(0, 140),
|
|
14639
|
+
at: r.createdAt
|
|
14640
|
+
})),
|
|
14641
|
+
replies: pending.replies.length
|
|
14642
|
+
} : null
|
|
14643
|
+
};
|
|
14644
|
+
});
|
|
14645
|
+
ipcMain.handle("paigy:agent-log", (_e, name) => {
|
|
14646
|
+
try {
|
|
14647
|
+
const all = readFileSync4(join5(homedir6(), ".paigy", "host.log"), "utf8").split("\n");
|
|
14648
|
+
const mine = all.filter((l) => l.includes(`[${name}]`) || /[▶✖◉⚠]/.test(l) && l.includes(name));
|
|
14649
|
+
return mine.slice(-500).join("\n");
|
|
14650
|
+
} catch {
|
|
14651
|
+
return "";
|
|
14652
|
+
}
|
|
14653
|
+
});
|
|
14115
14654
|
ipcMain.handle("paigy:workspaces", () => listWorkspaces(wsDeps));
|
|
14116
14655
|
ipcMain.handle("paigy:workspace-add", async () => {
|
|
14117
14656
|
const picked = await dialog.showOpenDialog({ properties: ["openDirectory", "createDirectory"] });
|