@paigy/harness 0.2.8 → 0.3.0
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 +582 -78
- package/dist/main.js +598 -72
- package/package.json +4 -3
package/dist/cli.js
CHANGED
|
@@ -23112,6 +23112,14 @@ var CATALOG = [
|
|
|
23112
23112
|
loginHint: "Run `codex login` to authenticate.",
|
|
23113
23113
|
installCli: "curl -fsSL https://chatgpt.com/codex/install.sh | sh",
|
|
23114
23114
|
installAdapter: "npm install -g @agentclientprotocol/codex-acp"
|
|
23115
|
+
},
|
|
23116
|
+
{
|
|
23117
|
+
name: "agy",
|
|
23118
|
+
label: "Antigravity",
|
|
23119
|
+
cli: "agy",
|
|
23120
|
+
authProbe: ["agy", "help"],
|
|
23121
|
+
loginHint: "Install and set up Antigravity (`agy`).",
|
|
23122
|
+
installCli: "curl -fsSL https://antigravity.dev/install.sh | bash"
|
|
23115
23123
|
}
|
|
23116
23124
|
];
|
|
23117
23125
|
function probeDirs(home = homedir()) {
|
|
@@ -23169,6 +23177,7 @@ function detectAll(deps = {}) {
|
|
|
23169
23177
|
|
|
23170
23178
|
// ../../packages/sdk/dist/index.js
|
|
23171
23179
|
import { createRequire as __sdkCreateRequire } from "module";
|
|
23180
|
+
import { randomUUID } from "crypto";
|
|
23172
23181
|
|
|
23173
23182
|
// ../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/external.js
|
|
23174
23183
|
var external_exports = {};
|
|
@@ -27212,7 +27221,8 @@ var coerce = {
|
|
|
27212
27221
|
var NEVER = INVALID;
|
|
27213
27222
|
|
|
27214
27223
|
// ../../packages/sdk/dist/index.js
|
|
27215
|
-
import { existsSync as existsSync2, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
|
|
27224
|
+
import { closeSync, existsSync as existsSync2, mkdirSync, openSync, readFileSync, rmSync, statSync, writeFileSync } from "fs";
|
|
27225
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
27216
27226
|
import { homedir as homedir2 } from "os";
|
|
27217
27227
|
import { join as join2 } from "path";
|
|
27218
27228
|
var require2 = __sdkCreateRequire(import.meta.url);
|
|
@@ -29478,16 +29488,25 @@ async function proxy() {
|
|
|
29478
29488
|
if (!PROXY_ENV.some((k) => process.env[k])) return void 0;
|
|
29479
29489
|
return agent ??= new (await Promise.resolve().then(() => __toESM(require_undici(), 1))).EnvHttpProxyAgent();
|
|
29480
29490
|
}
|
|
29491
|
+
var INSTANCE_ID = randomUUID();
|
|
29492
|
+
var SESSION_ID = process.env.PAIGY_SESSION_ID ?? process.env.CLAUDE_CODE_SESSION_ID ?? INSTANCE_ID;
|
|
29481
29493
|
async function reach(url, init) {
|
|
29482
29494
|
try {
|
|
29483
|
-
|
|
29495
|
+
const headers = {
|
|
29496
|
+
...init?.headers,
|
|
29497
|
+
"x-paigy-instance": INSTANCE_ID,
|
|
29498
|
+
"x-paigy-session": SESSION_ID
|
|
29499
|
+
};
|
|
29500
|
+
return await fetch(url, { ...init, headers, dispatcher: await proxy() });
|
|
29484
29501
|
} catch (e) {
|
|
29485
29502
|
throw new Error(`${NETWORK_MSG} (${e?.message ?? String(e)})`);
|
|
29486
29503
|
}
|
|
29487
29504
|
}
|
|
29488
29505
|
var ContextSchema = external_exports.object({
|
|
29489
29506
|
title: external_exports.string().min(1).describe("One-line headline of what you need (required, non-empty)."),
|
|
29490
|
-
description: external_exports.array(external_exports.string().min(1)).
|
|
29507
|
+
description: external_exports.array(external_exports.string().min(1)).describe(
|
|
29508
|
+
"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."
|
|
29509
|
+
)
|
|
29491
29510
|
});
|
|
29492
29511
|
var ParticipantSchema = external_exports.object({
|
|
29493
29512
|
kind: external_exports.enum(["human", "agent"]),
|
|
@@ -29634,6 +29653,14 @@ var NotifyRequestSchema = external_exports.object({
|
|
|
29634
29653
|
waiting: external_exports.enum(["none", "soft", "hard"]).optional().describe(
|
|
29635
29654
|
"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."
|
|
29636
29655
|
),
|
|
29656
|
+
/** Δ9b (#895): HOLD this claim so the sender can correct the plan before anyone is
|
|
29657
|
+
* interrupted. Opt-in per claim, because the fail-open is a minute-granularity cron —
|
|
29658
|
+
* holding by default would charge every quiet claim that minute before any agent could
|
|
29659
|
+
* correct anything. Ignored for `waiting: 'hard'`: a blocking ask rings on what we have,
|
|
29660
|
+
* and the enrichment can still land mid-call (#781 re-plans the unspoken tail). */
|
|
29661
|
+
confirm: external_exports.boolean().optional().describe(
|
|
29662
|
+
"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'."
|
|
29663
|
+
),
|
|
29637
29664
|
/** #575: a RELAY of the user's explicitly stated preference, never the agent's
|
|
29638
29665
|
* choice. Outranks waiting in both directions: 'call' rings even for a
|
|
29639
29666
|
* waiting:'none' "call me when it's done"; 'message' never rings even for
|
|
@@ -29692,35 +29719,45 @@ function normalizeWaiting(req) {
|
|
|
29692
29719
|
blocking: req.blocking || waiting === "hard"
|
|
29693
29720
|
};
|
|
29694
29721
|
}
|
|
29695
|
-
|
|
29696
|
-
|
|
29697
|
-
|
|
29722
|
+
function unitsOf(text) {
|
|
29723
|
+
const spans = [];
|
|
29724
|
+
const re = /\n\s*\n+/g;
|
|
29725
|
+
let cursor = 0;
|
|
29726
|
+
const push = (from, to) => {
|
|
29727
|
+
const slice = text.slice(from, to);
|
|
29728
|
+
const lead = slice.length - slice.trimStart().length;
|
|
29729
|
+
const tail = slice.length - slice.trimEnd().length;
|
|
29730
|
+
if (from + lead < to - tail) spans.push({ start: from + lead, end: to - tail });
|
|
29731
|
+
};
|
|
29732
|
+
for (let m = re.exec(text); m; m = re.exec(text)) {
|
|
29733
|
+
push(cursor, m.index);
|
|
29734
|
+
cursor = m.index + m[0].length;
|
|
29735
|
+
}
|
|
29736
|
+
push(cursor, text.length);
|
|
29737
|
+
return spans;
|
|
29738
|
+
}
|
|
29739
|
+
function headline(text) {
|
|
29740
|
+
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) ?? "";
|
|
29741
|
+
return (/^[^.!?\n]+[.!?]?/.exec(line)?.[0] ?? line).trim();
|
|
29742
|
+
}
|
|
29743
|
+
function bodyAfterHeadline(text) {
|
|
29698
29744
|
const body = text.trim();
|
|
29699
|
-
|
|
29700
|
-
const
|
|
29701
|
-
|
|
29702
|
-
|
|
29703
|
-
|
|
29704
|
-
if (!s) continue;
|
|
29705
|
-
const last = chunks[chunks.length - 1];
|
|
29706
|
-
if (last !== void 0 && `${last} ${s}`.length <= max) chunks[chunks.length - 1] = `${last} ${s}`;
|
|
29707
|
-
else chunks.push(s);
|
|
29708
|
-
}
|
|
29709
|
-
if (chunks.length <= cap) return chunks;
|
|
29710
|
-
return [...chunks.slice(0, cap - 1), chunks.slice(cap - 1).join(" ")];
|
|
29745
|
+
const title = headline(body);
|
|
29746
|
+
const rest = body.slice(title.length).replace(/^[\s.!?—–-]+/, "").trim();
|
|
29747
|
+
if (!rest) return { title, description: [] };
|
|
29748
|
+
const chunks = unitsOf(rest).map((u) => rest.slice(u.start, u.end)).filter(Boolean);
|
|
29749
|
+
return { title, description: chunks.length ? chunks : [rest] };
|
|
29711
29750
|
}
|
|
29712
29751
|
function deriveAsk(req) {
|
|
29713
29752
|
req = normalizeWaiting(req);
|
|
29714
29753
|
if (!req.ask) return req;
|
|
29715
29754
|
const text = req.ask.trim();
|
|
29716
|
-
const firstSentence = (/^[^.!?\n]+[.!?]?/.exec(text)?.[0] ?? text).trim();
|
|
29717
|
-
const title = firstSentence.length > 90 ? `${firstSentence.slice(0, 87).trimEnd()}\u2026` : firstSentence;
|
|
29718
29755
|
const hinted = req.urgencyHint === "now" ? "call" : req.urgencyHint === "soon" ? "banner" : req.urgencyHint === "whenever" ? "inbox" : req.urgency;
|
|
29719
29756
|
const urgency = req.channel === "call" ? "call" : req.channel === "message" && hinted === "call" ? "banner" : hinted;
|
|
29720
29757
|
const { ask: _ask, needs, urgencyHint: _hint, channel: _channel, ...rest } = req;
|
|
29721
29758
|
return {
|
|
29722
29759
|
...rest,
|
|
29723
|
-
context:
|
|
29760
|
+
context: bodyAfterHeadline(text),
|
|
29724
29761
|
// Options riding alongside the ask (#575: pixels can't be prose) floor to a
|
|
29725
29762
|
// single pick — the model broker may upgrade to many/rank from the wording.
|
|
29726
29763
|
select: req.options?.length ? "one" : "text",
|
|
@@ -29910,6 +29947,34 @@ var NotifyResponseSchema = external_exports.object({
|
|
|
29910
29947
|
answer: UserAnswerSchema.optional(),
|
|
29911
29948
|
answeredAt: external_exports.string().datetime().optional()
|
|
29912
29949
|
});
|
|
29950
|
+
var NotifyPlanUnitSchema = external_exports.object({
|
|
29951
|
+
notificationId: external_exports.string(),
|
|
29952
|
+
/** The unit's own heading, so the agent can tell which of its paragraphs this became. */
|
|
29953
|
+
title: external_exports.string(),
|
|
29954
|
+
/** How loudly this unit was arbitrated to arrive — per unit, which is the point of units. */
|
|
29955
|
+
level: NotifyLevelSchema,
|
|
29956
|
+
/** Answered from something the user already decided: nobody is interrupted, and a trail card
|
|
29957
|
+
* says so. The agent should not wait on this one. */
|
|
29958
|
+
settled: external_exports.literal(true).optional(),
|
|
29959
|
+
/** What this unit would need to be answerable and does not carry (#894). A PROPOSAL to the
|
|
29960
|
+
* agent — nothing here changed the ask, and ignoring it costs nothing. */
|
|
29961
|
+
needs: external_exports.array(external_exports.enum(["options", "visuals"])).optional(),
|
|
29962
|
+
/** The SHAPE the broker would give this unit, for the agent to ratify (#886/#894). The
|
|
29963
|
+
* split layer reads prose and can see that a paragraph is a yes/no or a pick-one — but a
|
|
29964
|
+
* broker that DECIDES that destroys the only fact separating a statement from a real ask
|
|
29965
|
+
* (#731), so it is offered, never applied: the unit is stored `text` until the agent
|
|
29966
|
+
* confirms the shape (POST /notify/:id/confirm). Ignoring it costs nothing. */
|
|
29967
|
+
proposal: external_exports.object({
|
|
29968
|
+
select: SelectShapeSchema,
|
|
29969
|
+
options: external_exports.array(external_exports.object({ label: external_exports.string().min(1) })).optional()
|
|
29970
|
+
}).optional()
|
|
29971
|
+
});
|
|
29972
|
+
var NotifyPlanSchema = external_exports.object({
|
|
29973
|
+
units: external_exports.array(NotifyPlanUnitSchema),
|
|
29974
|
+
/** Which unit is this arrival's ONE interruption (units-design.md D23). Absent means nobody
|
|
29975
|
+
* was interrupted — every unit was either settled or quiet enough to sit in the inbox. */
|
|
29976
|
+
speaks: external_exports.string().optional()
|
|
29977
|
+
});
|
|
29913
29978
|
var UserResponseSchema = external_exports.object({
|
|
29914
29979
|
requestId: external_exports.string(),
|
|
29915
29980
|
answer: UserAnswerSchema,
|
|
@@ -29964,6 +30029,20 @@ var InboxItemSchema = external_exports.object({
|
|
|
29964
30029
|
/** The conversation thread + connection this item lives on. Present on the replied
|
|
29965
30030
|
* detail — they power History's "Continue" / "New session from this" (#57/#251). */
|
|
29966
30031
|
parentId: external_exports.string().optional(),
|
|
30032
|
+
/** THE ARRIVAL this row is one unit of (`notifications.ask_id` → `asks`). A claim is one
|
|
30033
|
+
* arrival and its units are N rows of it, so this — not `parentId` — is what makes a
|
|
30034
|
+
* multi-part notification one thing on screen. The thread is the whole CONVERSATION: it
|
|
30035
|
+
* accumulates every message an agent ever sent, so grouping by it renders a day of
|
|
30036
|
+
* unrelated updates as a single "12-part request". Absent on rows written before the
|
|
30037
|
+
* `asks` table, and on anything that never went through `notify` — both fall back to the
|
|
30038
|
+
* thread, which is what the client did for all rows until now. */
|
|
30039
|
+
askId: external_exports.string().optional(),
|
|
30040
|
+
/** WHERE this unit sat in the message it was cut from (`notifications.seq`). The batch
|
|
30041
|
+
* shares one `created_at` to the microsecond, so without it the author's order is
|
|
30042
|
+
* unrecoverable client-side — a four-paragraph briefing rendered opening-paragraph-last
|
|
30043
|
+
* (live 2026-08-10, D35). The API already orders by it; this lets a reader that
|
|
30044
|
+
* re-sorts (grouping, filtering) put an arrival back in the order it was written. */
|
|
30045
|
+
seq: external_exports.number().int().optional(),
|
|
29967
30046
|
tokenId: external_exports.string().optional(),
|
|
29968
30047
|
status: NotifyStatusSchema,
|
|
29969
30048
|
context: ContextSchema,
|
|
@@ -29974,6 +30053,16 @@ var InboxItemSchema = external_exports.object({
|
|
|
29974
30053
|
* ring/enqueue time. Replaces the condensed line + index-aligned phrased points, which
|
|
29975
30054
|
* between them could not express a call as a sequence. `question: null` is a real turn —
|
|
29976
30055
|
* a status update stays a statement instead of being shaped into a yes/no. */
|
|
30056
|
+
/** Does this claim want an ANSWER, or is it telling you something? Written per row from
|
|
30057
|
+
* `requestAsks` — the agent's own declaration, not a guess. `false` is what earns a card
|
|
30058
|
+
* its acknowledge affordance: without it a status update offers a text box and a dismiss,
|
|
30059
|
+
* and neither of those is "got it" (owner, 2026-08-10). */
|
|
30060
|
+
asks: external_exports.boolean().optional(),
|
|
30061
|
+
/** When a live process last pulsed for this row's agent — the liveness input for
|
|
30062
|
+
* "working requires a pulse" (#928): the list said "Working…" from agent_state alone
|
|
30063
|
+
* while the party called the same dead claim stalled. Absent = no token/no data,
|
|
30064
|
+
* which must never CLAIM stalled. */
|
|
30065
|
+
lastSeenAt: external_exports.string().optional(),
|
|
29977
30066
|
agenda: external_exports.array(AgendaTurnSchema).optional(),
|
|
29978
30067
|
/** On a replied detail (#397): the next steps the user attached to the answer
|
|
29979
30068
|
* ("call back after lunch") — shown so they can see the commitment was captured. */
|
|
@@ -29998,6 +30087,23 @@ var InboxItemSchema = external_exports.object({
|
|
|
29998
30087
|
* client may still flag a stall by age. Drives the inbox error badge + Retry. */
|
|
29999
30088
|
error: external_exports.string().optional(),
|
|
30000
30089
|
clarifies: external_exports.string().optional(),
|
|
30090
|
+
/** The ring ladder ran out while this was still pending — we tried to reach you and
|
|
30091
|
+
* STOPPED trying (`arbitration/arbitrate.ts` `nextRing` → `stop`). Distinct from an
|
|
30092
|
+
* agent with nothing to say, which the roster drew identically until now: "nothing to
|
|
30093
|
+
* say" and "gave up saying it" are opposite situations wearing the same face
|
|
30094
|
+
* (navigation-design.md, gap 1). False for anything that never rang. */
|
|
30095
|
+
gaveUp: external_exports.boolean().default(false),
|
|
30096
|
+
/** Why this arrived the way it did, read back off the delivery receipt (`notify/why.ts`).
|
|
30097
|
+
* Absent for anything never delivered through a push, and for older rows written before
|
|
30098
|
+
* the reason was recorded. Deliberately a debug affordance, shown small (owner,
|
|
30099
|
+
* 2026-08-07) — its real job is to give "this didn't need a call" something to be
|
|
30100
|
+
* feedback ABOUT. */
|
|
30101
|
+
why: external_exports.object({
|
|
30102
|
+
asked: NotifyLevelSchema,
|
|
30103
|
+
got: NotifyLevelSchema,
|
|
30104
|
+
because: external_exports.enum(["unresponsive", "dismissed", "not_permitted", "silent", "coalesced", "agent_capped", "unplanned", "learned_raise"]).optional(),
|
|
30105
|
+
line: external_exports.string()
|
|
30106
|
+
}).optional(),
|
|
30001
30107
|
select: external_exports.enum(["one", "many", "rank", "confirm", "text"]).default("one"),
|
|
30002
30108
|
confirmStyle: external_exports.enum(["yesno", "approve"]).default("yesno").describe(
|
|
30003
30109
|
"Labels for a select:'confirm' paige \u2014 'yesno' (Yes/No) or 'approve' (Approve/Deny). Ignored unless select is 'confirm'."
|
|
@@ -30119,6 +30225,14 @@ var ConnectionSummarySchema = external_exports.object({
|
|
|
30119
30225
|
provider: external_exports.string().nullable(),
|
|
30120
30226
|
/** The pairing's assigned voice (#462); null = the default voice. */
|
|
30121
30227
|
voice: VoiceKeySchema.nullable(),
|
|
30228
|
+
/** The LOUDEST this agent may ever reach you — a ceiling on `NOTIFY_LADDER`, set by the
|
|
30229
|
+
* user on the agent's own page. null = no ceiling (today's behaviour for every
|
|
30230
|
+
* connection). Android binds importance to a relationship rather than to each message,
|
|
30231
|
+
* and that is the thing our roster could not say: "Marlow may always call me; Otto
|
|
30232
|
+
* never may" (navigation-design.md, gap 2). Clamped in `arbitrateLevel`, so it binds
|
|
30233
|
+
* every surface at once and outranks even `sessionMode: all_calls` — a mode the user
|
|
30234
|
+
* set once must not overrule a rule they set about one agent. */
|
|
30235
|
+
reach: NotifyLevelSchema.nullable().optional(),
|
|
30122
30236
|
createdAt: external_exports.string().datetime(),
|
|
30123
30237
|
/** Most recent notification on this connection, either direction. Null = no contact yet.
|
|
30124
30238
|
* Drives the agents-page recency grouping (Today / This week / …). */
|
|
@@ -30137,6 +30251,40 @@ var ConnectionSummarySchema = external_exports.object({
|
|
|
30137
30251
|
* false = a local MCP connection running on the user's computer (Claude Code/Codex/…). */
|
|
30138
30252
|
managed: external_exports.boolean()
|
|
30139
30253
|
});
|
|
30254
|
+
var MoveRingSchema = external_exports.enum(["home", "travels", "retired", "quarantined"]);
|
|
30255
|
+
var MoveSchema = external_exports.object({
|
|
30256
|
+
id: external_exports.string(),
|
|
30257
|
+
/** The reusable question, as distill normalized it. */
|
|
30258
|
+
question: external_exports.string(),
|
|
30259
|
+
/** The operative ruling. Editable by the user (PATCH) — which resets the ledger. */
|
|
30260
|
+
answer: external_exports.string(),
|
|
30261
|
+
/** The user's stated reason, when they gave one. Null = inherently narrow: the judge is
|
|
30262
|
+
* told so, and the ruling only derives essentially the same question in the same scope. */
|
|
30263
|
+
rationale: external_exports.string().nullable(),
|
|
30264
|
+
/** Where the ruling lives: a repo/workspace, or 'global'. */
|
|
30265
|
+
scope: external_exports.string(),
|
|
30266
|
+
ring: MoveRingSchema,
|
|
30267
|
+
/** True = the user pinned it with `always` (travel granted by hand, not by evidence). */
|
|
30268
|
+
pinned: external_exports.boolean(),
|
|
30269
|
+
/** True = a pin the user placed was BROKEN by later counter-evidence. Surfaced so the
|
|
30270
|
+
* break is visible instead of a pin silently disappearing. */
|
|
30271
|
+
pinBroken: external_exports.boolean(),
|
|
30272
|
+
/** When the ruling was distilled. */
|
|
30273
|
+
learnedAt: external_exports.string(),
|
|
30274
|
+
/** Last time it answered an ask. Null = never fired. */
|
|
30275
|
+
lastUsedAt: external_exports.string().nullable(),
|
|
30276
|
+
/** How many asks it has answered. Instrumentation — deliberately NOT an input to the
|
|
30277
|
+
* evidence curve: firing says the question keeps arising, not that the ruling is right. */
|
|
30278
|
+
usedCount: external_exports.number(),
|
|
30279
|
+
/** Ledger: outcomes that said it held up. Saturating — the tenth is worth almost nothing. */
|
|
30280
|
+
confirms: external_exports.number(),
|
|
30281
|
+
/** Ledger: contradictions, in signal units (a full override = 1, weaker signals less).
|
|
30282
|
+
* Linear and priced above the entire confirmation budget, so any full counter wins. */
|
|
30283
|
+
counters: external_exports.number(),
|
|
30284
|
+
/** The agent that asked the question this move came from, when known. Null for a move
|
|
30285
|
+
* distilled from a clarify ruling (those carry no agent) or one whose source rows are gone. */
|
|
30286
|
+
learnedFrom: external_exports.object({ id: external_exports.string(), name: external_exports.string() }).nullable()
|
|
30287
|
+
});
|
|
30140
30288
|
var CreateRequestSchema = external_exports.object({
|
|
30141
30289
|
/** The connection (token id) to send to, from GET /api/tokens. */
|
|
30142
30290
|
tokenId: external_exports.string(),
|
|
@@ -30343,6 +30491,17 @@ var DeviceTokenSchema = external_exports.object({
|
|
|
30343
30491
|
* a default silly name). */
|
|
30344
30492
|
name: external_exports.string(),
|
|
30345
30493
|
device: external_exports.string().nullable(),
|
|
30494
|
+
/** The pairing's assigned voice, cached so the desktop can seed the SAME face the phone
|
|
30495
|
+
* draws — voice is the third ingredient of a hatchling's build (party/traits.ts). */
|
|
30496
|
+
voice: external_exports.string().nullable().optional(),
|
|
30497
|
+
/** The token's server-side id — the face's COLOUR anchor, and the only seed ingredient
|
|
30498
|
+
* that survives a rename. Cached by the host's identity beat. */
|
|
30499
|
+
token_id: external_exports.string().nullable().optional(),
|
|
30500
|
+
/** WHERE this identity works — the folder a wake should land it in. Written by the host
|
|
30501
|
+
* at spawn and by `paigy-harness handoff` from a live terminal. Without it every wake
|
|
30502
|
+
* landed in the FIRST granted workspace and the agent rediscovered its own repo from
|
|
30503
|
+
* the thread each time (host.ts, live catch 2026-08-06 — prompt-papered until now). */
|
|
30504
|
+
workspace: external_exports.string().nullable().optional(),
|
|
30346
30505
|
phone_reveal: PairingRevealSchema.nullable().optional(),
|
|
30347
30506
|
// present once the phone reveals
|
|
30348
30507
|
uik_pub: external_exports.string().nullable().optional()
|
|
@@ -30369,6 +30528,8 @@ var NotificationFeedbackKindSchema = external_exports.enum([
|
|
|
30369
30528
|
// "There should be a picture or design here."
|
|
30370
30529
|
"should_have_called",
|
|
30371
30530
|
// "Don't put this in a banner — ring me for something like this."
|
|
30531
|
+
"should_have_messaged",
|
|
30532
|
+
// the inverse: "that didn't deserve a ring — a message would do."
|
|
30372
30533
|
"other"
|
|
30373
30534
|
// anything else — the note carries it.
|
|
30374
30535
|
]);
|
|
@@ -30665,7 +30826,11 @@ function open(envelope, myKeyId, mySecretKeyB64) {
|
|
|
30665
30826
|
if (!eqCt(sealedCanon, expected)) throw new Error("header mismatch (tampered metadata)");
|
|
30666
30827
|
return { header: envelope.hdr, body };
|
|
30667
30828
|
}
|
|
30668
|
-
|
|
30829
|
+
function sessionSlot(sessionId) {
|
|
30830
|
+
const id = sessionId ?? process.env.PAIGY_SESSION_ID ?? process.env.CLAUDE_CODE_SESSION_ID ?? randomUUID2();
|
|
30831
|
+
return `session:${id.slice(0, 8)}`;
|
|
30832
|
+
}
|
|
30833
|
+
var AGENT_NAME = process.env.PAIGY_AGENT || sessionSlot();
|
|
30669
30834
|
var TOKEN_PATH = join2(homedir2(), ".paigy", "token.json");
|
|
30670
30835
|
var KEY_PATH = join2(homedir2(), ".paigy", "key.json");
|
|
30671
30836
|
function readTokenFile() {
|
|
@@ -30680,10 +30845,45 @@ function readTokenFile() {
|
|
|
30680
30845
|
return {};
|
|
30681
30846
|
}
|
|
30682
30847
|
}
|
|
30683
|
-
function
|
|
30684
|
-
const
|
|
30848
|
+
function withTokenLock(mutate) {
|
|
30849
|
+
const lock = TOKEN_PATH + ".lock";
|
|
30850
|
+
const spin = new Int32Array(new SharedArrayBuffer(4));
|
|
30685
30851
|
mkdirSync(join2(homedir2(), ".paigy"), { recursive: true });
|
|
30686
|
-
|
|
30852
|
+
for (const deadline = Date.now() + 5e3; Date.now() < deadline; ) {
|
|
30853
|
+
try {
|
|
30854
|
+
closeSync(openSync(lock, "wx"));
|
|
30855
|
+
break;
|
|
30856
|
+
} catch {
|
|
30857
|
+
const held = statSync(lock, { throwIfNoEntry: false })?.mtimeMs ?? Date.now();
|
|
30858
|
+
if (Date.now() - held > 5e3) rmSync(lock, { force: true });
|
|
30859
|
+
else Atomics.wait(spin, 0, 0, 25);
|
|
30860
|
+
}
|
|
30861
|
+
}
|
|
30862
|
+
try {
|
|
30863
|
+
return mutate();
|
|
30864
|
+
} finally {
|
|
30865
|
+
rmSync(lock, { force: true });
|
|
30866
|
+
}
|
|
30867
|
+
}
|
|
30868
|
+
function saveToken(token, agent2 = AGENT_NAME) {
|
|
30869
|
+
withTokenLock(() => {
|
|
30870
|
+
const slots = { ...readTokenFile(), [agent2]: token };
|
|
30871
|
+
writeFileSync(TOKEN_PATH, JSON.stringify(slots, null, 2) + "\n", { mode: 384 });
|
|
30872
|
+
});
|
|
30873
|
+
}
|
|
30874
|
+
function updateSlot(agent2, patch) {
|
|
30875
|
+
return withTokenLock(() => {
|
|
30876
|
+
const slots = readTokenFile();
|
|
30877
|
+
const existing = slots[agent2];
|
|
30878
|
+
if (!existing) return false;
|
|
30879
|
+
slots[agent2] = { ...existing, ...patch };
|
|
30880
|
+
writeFileSync(TOKEN_PATH, JSON.stringify(slots, null, 2) + "\n", { mode: 384 });
|
|
30881
|
+
return true;
|
|
30882
|
+
});
|
|
30883
|
+
}
|
|
30884
|
+
function slotIdentity(agent2) {
|
|
30885
|
+
const t = readTokenFile()[agent2];
|
|
30886
|
+
return { name: t?.name ?? null, voice: t?.voice ?? null, tokenId: t?.token_id ?? null, workspace: t?.workspace ?? null };
|
|
30687
30887
|
}
|
|
30688
30888
|
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
30689
30889
|
function readToken(agent2 = AGENT_NAME) {
|
|
@@ -30693,6 +30893,9 @@ function readToken(agent2 = AGENT_NAME) {
|
|
|
30693
30893
|
function listSlots() {
|
|
30694
30894
|
return Object.keys(readTokenFile());
|
|
30695
30895
|
}
|
|
30896
|
+
function slotName(agent2) {
|
|
30897
|
+
return readTokenFile()[agent2]?.name ?? null;
|
|
30898
|
+
}
|
|
30696
30899
|
async function requestCode(suggestedName = AGENT_NAME, e2ee) {
|
|
30697
30900
|
const res = await reach(`${BACKEND_URL}/api/device/code`, {
|
|
30698
30901
|
method: "POST",
|
|
@@ -30859,6 +31062,16 @@ async function hatch(name, voice = null) {
|
|
|
30859
31062
|
if (!res.ok) throw new Error(`hatch failed: ${res.status} ${await res.text()}`);
|
|
30860
31063
|
return await res.json();
|
|
30861
31064
|
}
|
|
31065
|
+
async function whoAmI(opts = {}) {
|
|
31066
|
+
try {
|
|
31067
|
+
const res = await reach(`${BACKEND_URL}/api/identity`, {
|
|
31068
|
+
headers: { authorization: `Bearer ${authToken(opts.token)}` }
|
|
31069
|
+
});
|
|
31070
|
+
return res.ok ? await res.json() : null;
|
|
31071
|
+
} catch {
|
|
31072
|
+
return null;
|
|
31073
|
+
}
|
|
31074
|
+
}
|
|
30862
31075
|
async function claimSessions(opts = {}) {
|
|
30863
31076
|
const res = ensureAuthed(await reach(`${BACKEND_URL}/api/sessions/claim`, {
|
|
30864
31077
|
headers: { authorization: `Bearer ${authToken(opts.token)}` }
|
|
@@ -30888,15 +31101,17 @@ async function setTaskState(notificationId, state, opts = {}) {
|
|
|
30888
31101
|
// src/cli.ts
|
|
30889
31102
|
var import_qrcode = __toESM(require_lib(), 1);
|
|
30890
31103
|
import { hostname } from "os";
|
|
30891
|
-
import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as
|
|
31104
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
|
|
30892
31105
|
import { execSync } from "child_process";
|
|
30893
|
-
import { homedir as
|
|
30894
|
-
import { join as
|
|
31106
|
+
import { homedir as homedir6 } from "os";
|
|
31107
|
+
import { join as join5 } from "path";
|
|
30895
31108
|
|
|
30896
31109
|
// ../../packages/schema/dist/index.js
|
|
30897
31110
|
var ContextSchema2 = external_exports.object({
|
|
30898
31111
|
title: external_exports.string().min(1).describe("One-line headline of what you need (required, non-empty)."),
|
|
30899
|
-
description: external_exports.array(external_exports.string().min(1)).
|
|
31112
|
+
description: external_exports.array(external_exports.string().min(1)).describe(
|
|
31113
|
+
"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."
|
|
31114
|
+
)
|
|
30900
31115
|
});
|
|
30901
31116
|
var ParticipantSchema2 = external_exports.object({
|
|
30902
31117
|
kind: external_exports.enum(["human", "agent"]),
|
|
@@ -31043,6 +31258,14 @@ var NotifyRequestSchema2 = external_exports.object({
|
|
|
31043
31258
|
waiting: external_exports.enum(["none", "soft", "hard"]).optional().describe(
|
|
31044
31259
|
"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."
|
|
31045
31260
|
),
|
|
31261
|
+
/** Δ9b (#895): HOLD this claim so the sender can correct the plan before anyone is
|
|
31262
|
+
* interrupted. Opt-in per claim, because the fail-open is a minute-granularity cron —
|
|
31263
|
+
* holding by default would charge every quiet claim that minute before any agent could
|
|
31264
|
+
* correct anything. Ignored for `waiting: 'hard'`: a blocking ask rings on what we have,
|
|
31265
|
+
* and the enrichment can still land mid-call (#781 re-plans the unspoken tail). */
|
|
31266
|
+
confirm: external_exports.boolean().optional().describe(
|
|
31267
|
+
"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'."
|
|
31268
|
+
),
|
|
31046
31269
|
/** #575: a RELAY of the user's explicitly stated preference, never the agent's
|
|
31047
31270
|
* choice. Outranks waiting in both directions: 'call' rings even for a
|
|
31048
31271
|
* waiting:'none' "call me when it's done"; 'message' never rings even for
|
|
@@ -31092,6 +31315,23 @@ var NotifyRequestSchema2 = external_exports.object({
|
|
|
31092
31315
|
if (!needsOptions && r.options?.length)
|
|
31093
31316
|
ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["options"], message: `select:'${r.select}' takes no options` });
|
|
31094
31317
|
});
|
|
31318
|
+
function unitsOf2(text) {
|
|
31319
|
+
const spans = [];
|
|
31320
|
+
const re = /\n\s*\n+/g;
|
|
31321
|
+
let cursor = 0;
|
|
31322
|
+
const push = (from, to) => {
|
|
31323
|
+
const slice = text.slice(from, to);
|
|
31324
|
+
const lead = slice.length - slice.trimStart().length;
|
|
31325
|
+
const tail = slice.length - slice.trimEnd().length;
|
|
31326
|
+
if (from + lead < to - tail) spans.push({ start: from + lead, end: to - tail });
|
|
31327
|
+
};
|
|
31328
|
+
for (let m = re.exec(text); m; m = re.exec(text)) {
|
|
31329
|
+
push(cursor, m.index);
|
|
31330
|
+
cursor = m.index + m[0].length;
|
|
31331
|
+
}
|
|
31332
|
+
push(cursor, text.length);
|
|
31333
|
+
return spans;
|
|
31334
|
+
}
|
|
31095
31335
|
var NotifyStatusSchema2 = external_exports.enum(["pending", "answered", "ignored"]);
|
|
31096
31336
|
var AgentStateSchema2 = external_exports.enum(["idle", "in_progress", "completed", "needs_input"]);
|
|
31097
31337
|
var SetTaskStateSchema2 = external_exports.object({
|
|
@@ -31274,6 +31514,34 @@ var NotifyResponseSchema2 = external_exports.object({
|
|
|
31274
31514
|
answer: UserAnswerSchema2.optional(),
|
|
31275
31515
|
answeredAt: external_exports.string().datetime().optional()
|
|
31276
31516
|
});
|
|
31517
|
+
var NotifyPlanUnitSchema2 = external_exports.object({
|
|
31518
|
+
notificationId: external_exports.string(),
|
|
31519
|
+
/** The unit's own heading, so the agent can tell which of its paragraphs this became. */
|
|
31520
|
+
title: external_exports.string(),
|
|
31521
|
+
/** How loudly this unit was arbitrated to arrive — per unit, which is the point of units. */
|
|
31522
|
+
level: NotifyLevelSchema2,
|
|
31523
|
+
/** Answered from something the user already decided: nobody is interrupted, and a trail card
|
|
31524
|
+
* says so. The agent should not wait on this one. */
|
|
31525
|
+
settled: external_exports.literal(true).optional(),
|
|
31526
|
+
/** What this unit would need to be answerable and does not carry (#894). A PROPOSAL to the
|
|
31527
|
+
* agent — nothing here changed the ask, and ignoring it costs nothing. */
|
|
31528
|
+
needs: external_exports.array(external_exports.enum(["options", "visuals"])).optional(),
|
|
31529
|
+
/** The SHAPE the broker would give this unit, for the agent to ratify (#886/#894). The
|
|
31530
|
+
* split layer reads prose and can see that a paragraph is a yes/no or a pick-one — but a
|
|
31531
|
+
* broker that DECIDES that destroys the only fact separating a statement from a real ask
|
|
31532
|
+
* (#731), so it is offered, never applied: the unit is stored `text` until the agent
|
|
31533
|
+
* confirms the shape (POST /notify/:id/confirm). Ignoring it costs nothing. */
|
|
31534
|
+
proposal: external_exports.object({
|
|
31535
|
+
select: SelectShapeSchema2,
|
|
31536
|
+
options: external_exports.array(external_exports.object({ label: external_exports.string().min(1) })).optional()
|
|
31537
|
+
}).optional()
|
|
31538
|
+
});
|
|
31539
|
+
var NotifyPlanSchema2 = external_exports.object({
|
|
31540
|
+
units: external_exports.array(NotifyPlanUnitSchema2),
|
|
31541
|
+
/** Which unit is this arrival's ONE interruption (units-design.md D23). Absent means nobody
|
|
31542
|
+
* was interrupted — every unit was either settled or quiet enough to sit in the inbox. */
|
|
31543
|
+
speaks: external_exports.string().optional()
|
|
31544
|
+
});
|
|
31277
31545
|
var UserResponseSchema2 = external_exports.object({
|
|
31278
31546
|
requestId: external_exports.string(),
|
|
31279
31547
|
answer: UserAnswerSchema2,
|
|
@@ -31328,6 +31596,20 @@ var InboxItemSchema2 = external_exports.object({
|
|
|
31328
31596
|
/** The conversation thread + connection this item lives on. Present on the replied
|
|
31329
31597
|
* detail — they power History's "Continue" / "New session from this" (#57/#251). */
|
|
31330
31598
|
parentId: external_exports.string().optional(),
|
|
31599
|
+
/** THE ARRIVAL this row is one unit of (`notifications.ask_id` → `asks`). A claim is one
|
|
31600
|
+
* arrival and its units are N rows of it, so this — not `parentId` — is what makes a
|
|
31601
|
+
* multi-part notification one thing on screen. The thread is the whole CONVERSATION: it
|
|
31602
|
+
* accumulates every message an agent ever sent, so grouping by it renders a day of
|
|
31603
|
+
* unrelated updates as a single "12-part request". Absent on rows written before the
|
|
31604
|
+
* `asks` table, and on anything that never went through `notify` — both fall back to the
|
|
31605
|
+
* thread, which is what the client did for all rows until now. */
|
|
31606
|
+
askId: external_exports.string().optional(),
|
|
31607
|
+
/** WHERE this unit sat in the message it was cut from (`notifications.seq`). The batch
|
|
31608
|
+
* shares one `created_at` to the microsecond, so without it the author's order is
|
|
31609
|
+
* unrecoverable client-side — a four-paragraph briefing rendered opening-paragraph-last
|
|
31610
|
+
* (live 2026-08-10, D35). The API already orders by it; this lets a reader that
|
|
31611
|
+
* re-sorts (grouping, filtering) put an arrival back in the order it was written. */
|
|
31612
|
+
seq: external_exports.number().int().optional(),
|
|
31331
31613
|
tokenId: external_exports.string().optional(),
|
|
31332
31614
|
status: NotifyStatusSchema2,
|
|
31333
31615
|
context: ContextSchema2,
|
|
@@ -31338,6 +31620,16 @@ var InboxItemSchema2 = external_exports.object({
|
|
|
31338
31620
|
* ring/enqueue time. Replaces the condensed line + index-aligned phrased points, which
|
|
31339
31621
|
* between them could not express a call as a sequence. `question: null` is a real turn —
|
|
31340
31622
|
* a status update stays a statement instead of being shaped into a yes/no. */
|
|
31623
|
+
/** Does this claim want an ANSWER, or is it telling you something? Written per row from
|
|
31624
|
+
* `requestAsks` — the agent's own declaration, not a guess. `false` is what earns a card
|
|
31625
|
+
* its acknowledge affordance: without it a status update offers a text box and a dismiss,
|
|
31626
|
+
* and neither of those is "got it" (owner, 2026-08-10). */
|
|
31627
|
+
asks: external_exports.boolean().optional(),
|
|
31628
|
+
/** When a live process last pulsed for this row's agent — the liveness input for
|
|
31629
|
+
* "working requires a pulse" (#928): the list said "Working…" from agent_state alone
|
|
31630
|
+
* while the party called the same dead claim stalled. Absent = no token/no data,
|
|
31631
|
+
* which must never CLAIM stalled. */
|
|
31632
|
+
lastSeenAt: external_exports.string().optional(),
|
|
31341
31633
|
agenda: external_exports.array(AgendaTurnSchema2).optional(),
|
|
31342
31634
|
/** On a replied detail (#397): the next steps the user attached to the answer
|
|
31343
31635
|
* ("call back after lunch") — shown so they can see the commitment was captured. */
|
|
@@ -31362,6 +31654,23 @@ var InboxItemSchema2 = external_exports.object({
|
|
|
31362
31654
|
* client may still flag a stall by age. Drives the inbox error badge + Retry. */
|
|
31363
31655
|
error: external_exports.string().optional(),
|
|
31364
31656
|
clarifies: external_exports.string().optional(),
|
|
31657
|
+
/** The ring ladder ran out while this was still pending — we tried to reach you and
|
|
31658
|
+
* STOPPED trying (`arbitration/arbitrate.ts` `nextRing` → `stop`). Distinct from an
|
|
31659
|
+
* agent with nothing to say, which the roster drew identically until now: "nothing to
|
|
31660
|
+
* say" and "gave up saying it" are opposite situations wearing the same face
|
|
31661
|
+
* (navigation-design.md, gap 1). False for anything that never rang. */
|
|
31662
|
+
gaveUp: external_exports.boolean().default(false),
|
|
31663
|
+
/** Why this arrived the way it did, read back off the delivery receipt (`notify/why.ts`).
|
|
31664
|
+
* Absent for anything never delivered through a push, and for older rows written before
|
|
31665
|
+
* the reason was recorded. Deliberately a debug affordance, shown small (owner,
|
|
31666
|
+
* 2026-08-07) — its real job is to give "this didn't need a call" something to be
|
|
31667
|
+
* feedback ABOUT. */
|
|
31668
|
+
why: external_exports.object({
|
|
31669
|
+
asked: NotifyLevelSchema2,
|
|
31670
|
+
got: NotifyLevelSchema2,
|
|
31671
|
+
because: external_exports.enum(["unresponsive", "dismissed", "not_permitted", "silent", "coalesced", "agent_capped", "unplanned", "learned_raise"]).optional(),
|
|
31672
|
+
line: external_exports.string()
|
|
31673
|
+
}).optional(),
|
|
31365
31674
|
select: external_exports.enum(["one", "many", "rank", "confirm", "text"]).default("one"),
|
|
31366
31675
|
confirmStyle: external_exports.enum(["yesno", "approve"]).default("yesno").describe(
|
|
31367
31676
|
"Labels for a select:'confirm' paige \u2014 'yesno' (Yes/No) or 'approve' (Approve/Deny). Ignored unless select is 'confirm'."
|
|
@@ -31483,6 +31792,14 @@ var ConnectionSummarySchema2 = external_exports.object({
|
|
|
31483
31792
|
provider: external_exports.string().nullable(),
|
|
31484
31793
|
/** The pairing's assigned voice (#462); null = the default voice. */
|
|
31485
31794
|
voice: VoiceKeySchema2.nullable(),
|
|
31795
|
+
/** The LOUDEST this agent may ever reach you — a ceiling on `NOTIFY_LADDER`, set by the
|
|
31796
|
+
* user on the agent's own page. null = no ceiling (today's behaviour for every
|
|
31797
|
+
* connection). Android binds importance to a relationship rather than to each message,
|
|
31798
|
+
* and that is the thing our roster could not say: "Marlow may always call me; Otto
|
|
31799
|
+
* never may" (navigation-design.md, gap 2). Clamped in `arbitrateLevel`, so it binds
|
|
31800
|
+
* every surface at once and outranks even `sessionMode: all_calls` — a mode the user
|
|
31801
|
+
* set once must not overrule a rule they set about one agent. */
|
|
31802
|
+
reach: NotifyLevelSchema2.nullable().optional(),
|
|
31486
31803
|
createdAt: external_exports.string().datetime(),
|
|
31487
31804
|
/** Most recent notification on this connection, either direction. Null = no contact yet.
|
|
31488
31805
|
* Drives the agents-page recency grouping (Today / This week / …). */
|
|
@@ -31501,6 +31818,40 @@ var ConnectionSummarySchema2 = external_exports.object({
|
|
|
31501
31818
|
* false = a local MCP connection running on the user's computer (Claude Code/Codex/…). */
|
|
31502
31819
|
managed: external_exports.boolean()
|
|
31503
31820
|
});
|
|
31821
|
+
var MoveRingSchema2 = external_exports.enum(["home", "travels", "retired", "quarantined"]);
|
|
31822
|
+
var MoveSchema2 = external_exports.object({
|
|
31823
|
+
id: external_exports.string(),
|
|
31824
|
+
/** The reusable question, as distill normalized it. */
|
|
31825
|
+
question: external_exports.string(),
|
|
31826
|
+
/** The operative ruling. Editable by the user (PATCH) — which resets the ledger. */
|
|
31827
|
+
answer: external_exports.string(),
|
|
31828
|
+
/** The user's stated reason, when they gave one. Null = inherently narrow: the judge is
|
|
31829
|
+
* told so, and the ruling only derives essentially the same question in the same scope. */
|
|
31830
|
+
rationale: external_exports.string().nullable(),
|
|
31831
|
+
/** Where the ruling lives: a repo/workspace, or 'global'. */
|
|
31832
|
+
scope: external_exports.string(),
|
|
31833
|
+
ring: MoveRingSchema2,
|
|
31834
|
+
/** True = the user pinned it with `always` (travel granted by hand, not by evidence). */
|
|
31835
|
+
pinned: external_exports.boolean(),
|
|
31836
|
+
/** True = a pin the user placed was BROKEN by later counter-evidence. Surfaced so the
|
|
31837
|
+
* break is visible instead of a pin silently disappearing. */
|
|
31838
|
+
pinBroken: external_exports.boolean(),
|
|
31839
|
+
/** When the ruling was distilled. */
|
|
31840
|
+
learnedAt: external_exports.string(),
|
|
31841
|
+
/** Last time it answered an ask. Null = never fired. */
|
|
31842
|
+
lastUsedAt: external_exports.string().nullable(),
|
|
31843
|
+
/** How many asks it has answered. Instrumentation — deliberately NOT an input to the
|
|
31844
|
+
* evidence curve: firing says the question keeps arising, not that the ruling is right. */
|
|
31845
|
+
usedCount: external_exports.number(),
|
|
31846
|
+
/** Ledger: outcomes that said it held up. Saturating — the tenth is worth almost nothing. */
|
|
31847
|
+
confirms: external_exports.number(),
|
|
31848
|
+
/** Ledger: contradictions, in signal units (a full override = 1, weaker signals less).
|
|
31849
|
+
* Linear and priced above the entire confirmation budget, so any full counter wins. */
|
|
31850
|
+
counters: external_exports.number(),
|
|
31851
|
+
/** The agent that asked the question this move came from, when known. Null for a move
|
|
31852
|
+
* distilled from a clarify ruling (those carry no agent) or one whose source rows are gone. */
|
|
31853
|
+
learnedFrom: external_exports.object({ id: external_exports.string(), name: external_exports.string() }).nullable()
|
|
31854
|
+
});
|
|
31504
31855
|
var CreateRequestSchema2 = external_exports.object({
|
|
31505
31856
|
/** The connection (token id) to send to, from GET /api/tokens. */
|
|
31506
31857
|
tokenId: external_exports.string(),
|
|
@@ -31707,6 +32058,17 @@ var DeviceTokenSchema2 = external_exports.object({
|
|
|
31707
32058
|
* a default silly name). */
|
|
31708
32059
|
name: external_exports.string(),
|
|
31709
32060
|
device: external_exports.string().nullable(),
|
|
32061
|
+
/** The pairing's assigned voice, cached so the desktop can seed the SAME face the phone
|
|
32062
|
+
* draws — voice is the third ingredient of a hatchling's build (party/traits.ts). */
|
|
32063
|
+
voice: external_exports.string().nullable().optional(),
|
|
32064
|
+
/** The token's server-side id — the face's COLOUR anchor, and the only seed ingredient
|
|
32065
|
+
* that survives a rename. Cached by the host's identity beat. */
|
|
32066
|
+
token_id: external_exports.string().nullable().optional(),
|
|
32067
|
+
/** WHERE this identity works — the folder a wake should land it in. Written by the host
|
|
32068
|
+
* at spawn and by `paigy-harness handoff` from a live terminal. Without it every wake
|
|
32069
|
+
* landed in the FIRST granted workspace and the agent rediscovered its own repo from
|
|
32070
|
+
* the thread each time (host.ts, live catch 2026-08-06 — prompt-papered until now). */
|
|
32071
|
+
workspace: external_exports.string().nullable().optional(),
|
|
31710
32072
|
phone_reveal: PairingRevealSchema2.nullable().optional(),
|
|
31711
32073
|
// present once the phone reveals
|
|
31712
32074
|
uik_pub: external_exports.string().nullable().optional()
|
|
@@ -31733,6 +32095,8 @@ var NotificationFeedbackKindSchema2 = external_exports.enum([
|
|
|
31733
32095
|
// "There should be a picture or design here."
|
|
31734
32096
|
"should_have_called",
|
|
31735
32097
|
// "Don't put this in a banner — ring me for something like this."
|
|
32098
|
+
"should_have_messaged",
|
|
32099
|
+
// the inverse: "that didn't deserve a ring — a message would do."
|
|
31736
32100
|
"other"
|
|
31737
32101
|
// anything else — the note carries it.
|
|
31738
32102
|
]);
|
|
@@ -31847,6 +32211,9 @@ function levelFor(event) {
|
|
|
31847
32211
|
case "idle":
|
|
31848
32212
|
if (endsWithQuestion(event.result)) return "banner";
|
|
31849
32213
|
return event.failed ? "push" : "inbox";
|
|
32214
|
+
case "work":
|
|
32215
|
+
return "inbox";
|
|
32216
|
+
// moot — entryFor drops work before a level ever matters
|
|
31850
32217
|
case "error":
|
|
31851
32218
|
return "inbox";
|
|
31852
32219
|
}
|
|
@@ -31867,16 +32234,14 @@ function entryFor(event, state = {}) {
|
|
|
31867
32234
|
switch (event.kind) {
|
|
31868
32235
|
case "turn": {
|
|
31869
32236
|
const who = event.role === "agent" ? "Agent" : "You";
|
|
31870
|
-
const
|
|
31871
|
-
const description = [event.text, event.tools?.length ? `Tools: ${event.tools.join(", ")}` : null].filter((c) => Boolean(c));
|
|
32237
|
+
const body = event.text ? event.role === "agent" ? event.text : `${who}: ${event.text}` : `${who} ran ${(event.tools ?? []).join(", ")}.`;
|
|
31872
32238
|
return {
|
|
31873
32239
|
...common,
|
|
31874
|
-
|
|
31875
|
-
//
|
|
31876
|
-
//
|
|
31877
|
-
//
|
|
31878
|
-
//
|
|
31879
|
-
select: "text"
|
|
32240
|
+
ask: body
|
|
32241
|
+
// No `select`: the contract refuses it on the `ask` form ("the broker derives it"),
|
|
32242
|
+
// and with no options it derives "text" anyway — the shape this wants, since any
|
|
32243
|
+
// inbox row can be replied to and a reply to history is just the user initiating
|
|
32244
|
+
// (the pump feeds it back in).
|
|
31880
32245
|
};
|
|
31881
32246
|
}
|
|
31882
32247
|
case "permission":
|
|
@@ -31898,16 +32263,24 @@ function entryFor(event, state = {}) {
|
|
|
31898
32263
|
};
|
|
31899
32264
|
case "idle": {
|
|
31900
32265
|
const asking = endsWithQuestion(event.result);
|
|
32266
|
+
const result = event.result?.trim();
|
|
32267
|
+
const norm = (t) => (t ?? "").replace(/\s+/g, " ").trim();
|
|
32268
|
+
if (result && !event.failed && norm(result) === norm(state.lastAgentText)) {
|
|
32269
|
+
if (!asking) return null;
|
|
32270
|
+
const spans = unitsOf2(result);
|
|
32271
|
+
const tail = spans.map((sp) => result.slice(sp.start, sp.end)).reverse().find((t) => t.includes("?"));
|
|
32272
|
+
return { ...common, ask: tail ?? result, blocking: true };
|
|
32273
|
+
}
|
|
32274
|
+
const lead = asking ? "" : event.failed ? "The agent stopped without finishing. " : "Turn complete. ";
|
|
31901
32275
|
return {
|
|
31902
32276
|
...common,
|
|
31903
|
-
|
|
31904
|
-
title: asking ? clip(firstLine(event.result ?? "")) : event.failed ? "The agent stopped without finishing" : "Turn complete",
|
|
31905
|
-
description: [event.result || (event.failed ? "No result reported." : "Done.")]
|
|
31906
|
-
},
|
|
31907
|
-
select: "text",
|
|
32277
|
+
ask: `${lead}${result || (event.failed ? "No result reported." : "Done.")}`,
|
|
31908
32278
|
...asking ? { blocking: true } : {}
|
|
31909
32279
|
};
|
|
31910
32280
|
}
|
|
32281
|
+
case "work":
|
|
32282
|
+
return null;
|
|
32283
|
+
// log-only by contract — the live texture of the working log
|
|
31911
32284
|
case "error":
|
|
31912
32285
|
return null;
|
|
31913
32286
|
}
|
|
@@ -31929,9 +32302,16 @@ function decisionFrom(answer) {
|
|
|
31929
32302
|
}
|
|
31930
32303
|
async function mirror(event, state, deps = {}) {
|
|
31931
32304
|
const entry = entryFor(event, state);
|
|
32305
|
+
if (event.kind === "turn" && event.role === "agent" && event.text) state.lastAgentText = event.text;
|
|
31932
32306
|
if (!entry) return {};
|
|
32307
|
+
let req;
|
|
32308
|
+
try {
|
|
32309
|
+
req = NotifyRequestSchema2.parse(entry);
|
|
32310
|
+
} catch (e) {
|
|
32311
|
+
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)}`);
|
|
32312
|
+
return {};
|
|
32313
|
+
}
|
|
31933
32314
|
try {
|
|
31934
|
-
const req = NotifyRequestSchema2.parse(entry);
|
|
31935
32315
|
const { notificationId, parentId } = await (deps.submit ?? submitNotification)(req);
|
|
31936
32316
|
(state.mine ??= /* @__PURE__ */ new Set()).add(notificationId);
|
|
31937
32317
|
return { parentId, notificationId };
|
|
@@ -31960,8 +32340,9 @@ async function askQuestion(question, state, deps = {}) {
|
|
|
31960
32340
|
...state.parentId ? { parentId: state.parentId } : {},
|
|
31961
32341
|
...state.repo ? { repo: state.repo } : {},
|
|
31962
32342
|
...state.branch ? { branch: state.branch } : {},
|
|
31963
|
-
|
|
31964
|
-
|
|
32343
|
+
// Prose in — see `entryFor`'s `turn` case. An agent's question is the case most likely
|
|
32344
|
+
// to run long, and its title was a clipped prefix of itself.
|
|
32345
|
+
ask: question,
|
|
31965
32346
|
blocking: true,
|
|
31966
32347
|
urgency: "banner"
|
|
31967
32348
|
};
|
|
@@ -31975,6 +32356,9 @@ async function askQuestion(question, state, deps = {}) {
|
|
|
31975
32356
|
}
|
|
31976
32357
|
async function nudgeSetup(label, hint, deps = {}) {
|
|
31977
32358
|
const entry = {
|
|
32359
|
+
// Keeps `context`: the title here SUMMARISES rather than repeating — "<label> needs
|
|
32360
|
+
// setup" is not a prefix of the hint — which is exactly the case a hand-written context
|
|
32361
|
+
// is for. `ask` would derive a title from the hint's first sentence and lose the label.
|
|
31978
32362
|
context: { title: clip(`${label} needs setup`), description: [hint] },
|
|
31979
32363
|
select: "text",
|
|
31980
32364
|
urgency: "push"
|
|
@@ -32007,7 +32391,6 @@ function spokenText(answer) {
|
|
|
32007
32391
|
return null;
|
|
32008
32392
|
}
|
|
32009
32393
|
}
|
|
32010
|
-
var firstLine = (s) => s.split("\n")[0] ?? s;
|
|
32011
32394
|
var clip = (s) => (s.length > 120 ? `${s.slice(0, 117)}\u2026` : s) || "(no text)";
|
|
32012
32395
|
|
|
32013
32396
|
// src/harness/session.ts
|
|
@@ -32042,6 +32425,14 @@ var AcpDriver = class {
|
|
|
32042
32425
|
queued = [];
|
|
32043
32426
|
/** Options of each unanswered permission request, keyed by its JSON-RPC id. */
|
|
32044
32427
|
pending = /* @__PURE__ */ new Map();
|
|
32428
|
+
/** Where the REPORT starts in `text` — everything before the LAST tool call is working
|
|
32429
|
+
* narration ("Now the API endpoints." → runs a tool), and it used to ship: the chunks
|
|
32430
|
+
* concatenate with no separator, so the owner's phone got "…find the repo.Now I have
|
|
32431
|
+
* the full picture. Writing the migration.Now…" as the opening paragraph of a finished
|
|
32432
|
+
* task (live, 2026-08-11 — "looks like a working log"). The narration's audience is the
|
|
32433
|
+
* terminal and host.log; what the agent composed AFTER its last tool call is the part
|
|
32434
|
+
* addressed to a human, and that is what leaves the machine. */
|
|
32435
|
+
reportFrom = 0;
|
|
32045
32436
|
/** The turn being streamed: text accumulates, tools append, both flush on stopReason. */
|
|
32046
32437
|
text = "";
|
|
32047
32438
|
tools = [];
|
|
@@ -32137,18 +32528,20 @@ var AcpDriver = class {
|
|
|
32137
32528
|
if (msg.id === this.promptId) {
|
|
32138
32529
|
this.promptId = void 0;
|
|
32139
32530
|
const events = [];
|
|
32140
|
-
|
|
32531
|
+
const report = this.text.slice(this.reportFrom).trim() || this.text.trim();
|
|
32532
|
+
if (report || this.tools.length) {
|
|
32141
32533
|
events.push({
|
|
32142
32534
|
kind: "turn",
|
|
32143
32535
|
role: "agent",
|
|
32144
|
-
text:
|
|
32536
|
+
text: report,
|
|
32145
32537
|
...this.tools.length ? { tools: [...this.tools] } : {}
|
|
32146
32538
|
});
|
|
32147
32539
|
}
|
|
32148
|
-
const result =
|
|
32540
|
+
const result = report;
|
|
32149
32541
|
const failed = msg.error !== void 0 || msg.result?.stopReason === "refusal";
|
|
32150
32542
|
this.text = "";
|
|
32151
32543
|
this.tools = [];
|
|
32544
|
+
this.reportFrom = 0;
|
|
32152
32545
|
const writes = this.flush();
|
|
32153
32546
|
if (!writes.length) {
|
|
32154
32547
|
events.push({
|
|
@@ -32172,7 +32565,9 @@ var AcpDriver = class {
|
|
|
32172
32565
|
case "tool_call": {
|
|
32173
32566
|
const title = update.title?.trim() || update.kind || "tool";
|
|
32174
32567
|
this.tools.push(title);
|
|
32175
|
-
|
|
32568
|
+
const note = this.text.slice(this.reportFrom).trim();
|
|
32569
|
+
this.reportFrom = this.text.length;
|
|
32570
|
+
return { events: [{ kind: "work", tool: title, ...note ? { note } : {} }], writes: [] };
|
|
32176
32571
|
}
|
|
32177
32572
|
default:
|
|
32178
32573
|
return none;
|
|
@@ -32226,7 +32621,8 @@ var frame = (body) => JSON.stringify({ jsonrpc: "2.0", ...body });
|
|
|
32226
32621
|
// src/harness/session.ts
|
|
32227
32622
|
var ADAPTER_BIN = {
|
|
32228
32623
|
claude: "claude-agent-acp",
|
|
32229
|
-
codex: "codex-acp"
|
|
32624
|
+
codex: "codex-acp",
|
|
32625
|
+
agy: "agy"
|
|
32230
32626
|
};
|
|
32231
32627
|
function splitLines(buffer, chunk) {
|
|
32232
32628
|
const combined = buffer + chunk;
|
|
@@ -32324,6 +32720,10 @@ function runHarness(opts) {
|
|
|
32324
32720
|
opts.log(decision.allow ? `\u2713 approved: ${event.summary}` : `\u2717 denied: ${event.summary}`);
|
|
32325
32721
|
return;
|
|
32326
32722
|
}
|
|
32723
|
+
if (event.kind === "work") {
|
|
32724
|
+
opts.log(`\u2699 ${event.tool}${event.note ? ` \u2014 ${event.note.split("\n")[0] ?? ""}` : ""}`);
|
|
32725
|
+
return;
|
|
32726
|
+
}
|
|
32327
32727
|
const { parentId } = await mirror(event, state, deps);
|
|
32328
32728
|
state.parentId ??= parentId;
|
|
32329
32729
|
if (event.kind === "turn") opts.log(`${event.role}: ${event.text.split("\n")[0] ?? ""}`);
|
|
@@ -32365,6 +32765,7 @@ function runHarness(opts) {
|
|
|
32365
32765
|
send(text) {
|
|
32366
32766
|
session?.send(text);
|
|
32367
32767
|
},
|
|
32768
|
+
working: () => running && state.resting !== true,
|
|
32368
32769
|
stop() {
|
|
32369
32770
|
running = false;
|
|
32370
32771
|
cancelAsks(state);
|
|
@@ -32375,6 +32776,11 @@ function runHarness(opts) {
|
|
|
32375
32776
|
};
|
|
32376
32777
|
}
|
|
32377
32778
|
|
|
32779
|
+
// src/host.ts
|
|
32780
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
32781
|
+
import { homedir as homedir5 } from "os";
|
|
32782
|
+
import { join as join4 } from "path";
|
|
32783
|
+
|
|
32378
32784
|
// src/workspaces.ts
|
|
32379
32785
|
import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
32380
32786
|
import { dirname as dirname3, join as join3, resolve as resolve2 } from "path";
|
|
@@ -32400,12 +32806,39 @@ function addWorkspace(dir, deps) {
|
|
|
32400
32806
|
}
|
|
32401
32807
|
return all;
|
|
32402
32808
|
}
|
|
32809
|
+
function allowed(cwd, deps) {
|
|
32810
|
+
const target = resolve2(cwd.replace(/^~(?=$|\/)/, homedir4()));
|
|
32811
|
+
return listWorkspaces(deps).some((w) => target === w || target.startsWith(`${w}/`));
|
|
32812
|
+
}
|
|
32813
|
+
function resolveWakeDir(pinned, deps) {
|
|
32814
|
+
if (pinned && allowed(pinned, deps)) {
|
|
32815
|
+
const dir = resolve2(pinned.replace(/^~(?=$|\/)/, homedir4()));
|
|
32816
|
+
if ((deps.exists ?? existsSync4)(dir)) return dir;
|
|
32817
|
+
}
|
|
32818
|
+
return listWorkspaces(deps)[0];
|
|
32819
|
+
}
|
|
32403
32820
|
|
|
32404
32821
|
// src/host.ts
|
|
32822
|
+
var HOST_FILE = join4(homedir5(), ".paigy", "host.json");
|
|
32405
32823
|
function startHost(opts) {
|
|
32406
32824
|
const runs = /* @__PURE__ */ new Map();
|
|
32407
32825
|
const asHost = { token: opts.token };
|
|
32826
|
+
const refreshIdentities = async () => {
|
|
32827
|
+
for (const slot of listSlots()) {
|
|
32828
|
+
const token = readToken(slot);
|
|
32829
|
+
if (!token) continue;
|
|
32830
|
+
const me = await whoAmI({ token }).catch(() => null);
|
|
32831
|
+
if (!me) continue;
|
|
32832
|
+
const have = slotIdentity(slot);
|
|
32833
|
+
if (me.name !== have.name || (me.voice ?? null) !== have.voice || (me.tokenId ?? null) !== have.tokenId) {
|
|
32834
|
+
updateSlot(slot, { ...me.name ? { name: me.name } : {}, voice: me.voice ?? null, token_id: me.tokenId ?? null });
|
|
32835
|
+
}
|
|
32836
|
+
}
|
|
32837
|
+
};
|
|
32408
32838
|
const beat = () => {
|
|
32839
|
+
void refreshIdentities();
|
|
32840
|
+
for (const r of runs.values()) if (r.token) void heartbeat(void 0, { token: r.token }).catch(() => {
|
|
32841
|
+
});
|
|
32409
32842
|
void heartbeat({
|
|
32410
32843
|
harnesses: detectAll().map((a) => ({ name: a.name, label: a.label, status: a.status })),
|
|
32411
32844
|
workspaces: listWorkspaces(opts.wsDeps)
|
|
@@ -32430,28 +32863,32 @@ function startHost(opts) {
|
|
|
32430
32863
|
},
|
|
32431
32864
|
log
|
|
32432
32865
|
});
|
|
32433
|
-
runs.set(spec.sessionId, { run, label });
|
|
32866
|
+
runs.set(spec.sessionId, { run, label, token: spec.token });
|
|
32867
|
+
void heartbeat(void 0, { token: spec.token }).catch(() => {
|
|
32868
|
+
});
|
|
32869
|
+
void refreshIdentities();
|
|
32434
32870
|
try {
|
|
32435
|
-
saveToken({ access_token: spec.token, name: label, device: null },
|
|
32871
|
+
saveToken({ access_token: spec.token, name: label, device: null, workspace: spec.workspace }, sessionSlot(spec.sessionId));
|
|
32436
32872
|
} catch {
|
|
32437
32873
|
}
|
|
32438
32874
|
opts.log(`\u25B6 phone-launched ${label} (${spec.harness}) in ${spec.workspace}`);
|
|
32439
32875
|
}
|
|
32440
32876
|
}
|
|
32441
|
-
const SLOT_HARNESS = { "mcp-agent": "claude", codex: "codex" };
|
|
32442
|
-
const WAKE_PROMPT = "You were woken because Paigy work is waiting for you. Call check_replies
|
|
32877
|
+
const SLOT_HARNESS = { "mcp-agent": "claude", codex: "codex", antigravity: "agy" };
|
|
32878
|
+
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.";
|
|
32443
32879
|
async function sweepSlots() {
|
|
32444
|
-
const workspace = listWorkspaces(opts.wsDeps)[0];
|
|
32445
|
-
if (!workspace) return;
|
|
32446
32880
|
for (const slot of listSlots()) {
|
|
32447
32881
|
const harness = SLOT_HARNESS[slot] ?? (slot === "Desktop" ? void 0 : "claude");
|
|
32448
32882
|
const key = `slot:${slot}`;
|
|
32449
32883
|
if (!harness || runs.has(key)) continue;
|
|
32450
32884
|
const token = readToken(slot);
|
|
32451
32885
|
if (!token) continue;
|
|
32886
|
+
const workspace = resolveWakeDir(slotIdentity(slot).workspace, opts.wsDeps);
|
|
32887
|
+
if (!workspace) continue;
|
|
32452
32888
|
const work = await checkReplies({ token }).catch(() => null);
|
|
32453
32889
|
if (!work || work.requests.length === 0 && work.replies.length === 0) continue;
|
|
32454
|
-
const
|
|
32890
|
+
const label = slotName(slot) ?? slot;
|
|
32891
|
+
const log = (line) => opts.log(`[${label}] ${line}`);
|
|
32455
32892
|
const run = runHarness({
|
|
32456
32893
|
harness,
|
|
32457
32894
|
cwd: workspace,
|
|
@@ -32462,19 +32899,28 @@ function startHost(opts) {
|
|
|
32462
32899
|
// A dead run must not squat the slot — evict so the next wake can respawn.
|
|
32463
32900
|
onExit: () => {
|
|
32464
32901
|
runs.delete(key);
|
|
32465
|
-
opts.log(`\u2716 ${
|
|
32902
|
+
opts.log(`\u2716 ${label} ended`);
|
|
32466
32903
|
},
|
|
32467
32904
|
log
|
|
32468
32905
|
});
|
|
32469
|
-
runs.set(key, { run, label
|
|
32470
|
-
|
|
32906
|
+
runs.set(key, { run, label, token });
|
|
32907
|
+
void heartbeat(void 0, { token }).catch(() => {
|
|
32908
|
+
});
|
|
32909
|
+
opts.log(`\u25B6 woke ${label} (${harness}) \u2014 work was waiting in ${workspace}`);
|
|
32471
32910
|
}
|
|
32472
32911
|
}
|
|
32912
|
+
const publish = () => {
|
|
32913
|
+
try {
|
|
32914
|
+
writeFileSync3(HOST_FILE, JSON.stringify({ pid: process.pid, at: Date.now(), roster: api.roster() }));
|
|
32915
|
+
} catch {
|
|
32916
|
+
}
|
|
32917
|
+
};
|
|
32473
32918
|
beat();
|
|
32474
32919
|
const pulse = setInterval(beat, 6e4);
|
|
32475
32920
|
const spawnPoll = setInterval(() => {
|
|
32476
32921
|
void claimAndSpawn();
|
|
32477
32922
|
void sweepSlots();
|
|
32923
|
+
publish();
|
|
32478
32924
|
}, 5e3);
|
|
32479
32925
|
const stopSessions = () => {
|
|
32480
32926
|
for (const { run, label } of runs.values()) {
|
|
@@ -32483,27 +32929,56 @@ function startHost(opts) {
|
|
|
32483
32929
|
}
|
|
32484
32930
|
runs.clear();
|
|
32485
32931
|
};
|
|
32486
|
-
|
|
32932
|
+
const api = {
|
|
32487
32933
|
sessions: () => [...runs.entries()].map(([id, r]) => [id, r.label]),
|
|
32934
|
+
// Every identity this machine holds, plus what it's doing right now. Slots are the
|
|
32935
|
+
// roster (hatched agents, the terminal tools, claimed sessions); a run makes one of
|
|
32936
|
+
// them live, and a run mid-turn makes it working.
|
|
32937
|
+
roster: () => {
|
|
32938
|
+
const live = new Map([...runs.values()].map((r) => [r.label, r.run]));
|
|
32939
|
+
const names = /* @__PURE__ */ new Map();
|
|
32940
|
+
for (const key of listSlots()) if (key !== "Desktop") names.set(slotName(key) ?? key, key);
|
|
32941
|
+
for (const label of live.keys()) if (!names.has(label)) names.set(label, null);
|
|
32942
|
+
return [...names.entries()].map(([name, slot]) => {
|
|
32943
|
+
const id = slot ? slotIdentity(slot) : { voice: null, tokenId: null };
|
|
32944
|
+
return {
|
|
32945
|
+
name,
|
|
32946
|
+
// The slot key rides along so the window's agent page can speak AS this agent —
|
|
32947
|
+
// its pending reads (`checkReplies`, the same pure read the sweep does) need the
|
|
32948
|
+
// slot's own token, and the display name is not a key.
|
|
32949
|
+
slot,
|
|
32950
|
+
tokenId: id.tokenId,
|
|
32951
|
+
voice: id.voice,
|
|
32952
|
+
running: live.has(name),
|
|
32953
|
+
working: live.get(name)?.working() ?? false
|
|
32954
|
+
};
|
|
32955
|
+
});
|
|
32956
|
+
},
|
|
32488
32957
|
stopSessions,
|
|
32489
32958
|
stop() {
|
|
32490
32959
|
clearInterval(pulse);
|
|
32491
32960
|
clearInterval(spawnPoll);
|
|
32492
32961
|
stopSessions();
|
|
32962
|
+
try {
|
|
32963
|
+
writeFileSync3(HOST_FILE, JSON.stringify({ pid: process.pid, at: 0, roster: [] }));
|
|
32964
|
+
} catch {
|
|
32965
|
+
}
|
|
32493
32966
|
}
|
|
32494
32967
|
};
|
|
32968
|
+
publish();
|
|
32969
|
+
return api;
|
|
32495
32970
|
}
|
|
32496
32971
|
|
|
32497
32972
|
// src/cli.ts
|
|
32498
32973
|
function parseArgs(argv) {
|
|
32499
|
-
const args = { harness: "claude", mode: "bypass", cwd: process.cwd(), doctor: false, host: false, pair: false, service: false, setup: false, grant: [], hatch: null, voice: null, slot: null, identity: null, graceSeconds: 90, prompt: "" };
|
|
32974
|
+
const args = { harness: "claude", mode: "bypass", cwd: process.cwd(), doctor: false, host: false, pair: false, service: false, setup: false, grant: [], hatch: null, voice: null, slot: null, identity: null, graceSeconds: 90, prompt: "", handoff: false };
|
|
32500
32975
|
const words = [];
|
|
32501
32976
|
for (let i = 0; i < argv.length; i++) {
|
|
32502
32977
|
const arg = argv[i];
|
|
32503
32978
|
const next = () => argv[++i];
|
|
32504
32979
|
if (arg === "--harness") {
|
|
32505
32980
|
const v = next();
|
|
32506
|
-
if (v !== "claude" && v !== "codex") return { error: `--harness must be claude or
|
|
32981
|
+
if (v !== "claude" && v !== "codex" && v !== "agy") return { error: `--harness must be claude, codex, or agy, got ${v ?? "nothing"}` };
|
|
32507
32982
|
args.harness = v;
|
|
32508
32983
|
} else if (arg === "--mode") {
|
|
32509
32984
|
const v = next();
|
|
@@ -32531,6 +33006,8 @@ function parseArgs(argv) {
|
|
|
32531
33006
|
const v = next();
|
|
32532
33007
|
if (!v) return { error: "--grant needs a folder to allow" };
|
|
32533
33008
|
args.grant.push(v);
|
|
33009
|
+
} else if (arg === "handoff") {
|
|
33010
|
+
args.handoff = true;
|
|
32534
33011
|
} else if (arg === "hatch") {
|
|
32535
33012
|
const v = next();
|
|
32536
33013
|
if (!v) return { error: 'hatch needs a name: paigy-harness hatch "Voice bug hunter"' };
|
|
@@ -32550,7 +33027,7 @@ function parseArgs(argv) {
|
|
|
32550
33027
|
}
|
|
32551
33028
|
}
|
|
32552
33029
|
args.prompt = words.join(" ");
|
|
32553
|
-
if (!args.doctor && !args.hatch && !args.host && !args.pair && !args.service && !args.setup && !args.prompt) return { error: "a prompt is required (or setup / --doctor / pair / host / service / hatch NAME)" };
|
|
33030
|
+
if (!args.doctor && !args.handoff && !args.hatch && !args.host && !args.pair && !args.service && !args.setup && !args.prompt) return { error: "a prompt is required (or setup / --doctor / pair / host / service / hatch NAME / handoff)" };
|
|
32554
33031
|
return args;
|
|
32555
33032
|
}
|
|
32556
33033
|
var STATUS_MARK = { ready: "\u2713", login: "\u25D0", "adapter-missing": "\u25D0", missing: "\u2717" };
|
|
@@ -32558,7 +33035,7 @@ async function main() {
|
|
|
32558
33035
|
const parsed = parseArgs(process.argv.slice(2));
|
|
32559
33036
|
if ("error" in parsed) {
|
|
32560
33037
|
console.error(`paigy-harness: ${parsed.error}`);
|
|
32561
|
-
console.error("usage: paigy-harness setup | pair | host [--grant DIR]\u2026 | service | hatch NAME [--voice KEY] [--slot SLOT] | [--harness claude|codex] [--mode bypass|ask] [--cwd DIR] [--identity NAME] [--grace SECONDS] [--doctor] PROMPT\u2026");
|
|
33038
|
+
console.error("usage: paigy-harness setup | pair | host [--grant DIR]\u2026 | service | hatch NAME [--voice KEY] [--slot SLOT] | handoff | [--harness claude|codex|agy] [--mode bypass|ask] [--cwd DIR] [--identity NAME] [--grace SECONDS] [--doctor] PROMPT\u2026");
|
|
32562
33039
|
process.exit(2);
|
|
32563
33040
|
}
|
|
32564
33041
|
if (parsed.doctor) {
|
|
@@ -32603,7 +33080,7 @@ async function main() {
|
|
|
32603
33080
|
const wsDeps = { file: workspacesFile() };
|
|
32604
33081
|
for (const dir of parsed.grant) addWorkspace(dir, wsDeps);
|
|
32605
33082
|
if (!listWorkspaces(wsDeps).length) {
|
|
32606
|
-
const guess =
|
|
33083
|
+
const guess = join5(homedir6(), "projects");
|
|
32607
33084
|
if (existsSync5(guess)) {
|
|
32608
33085
|
addWorkspace(guess, wsDeps);
|
|
32609
33086
|
console.log(`\u2713 workspace granted: ${guess} (change with --grant DIR)`);
|
|
@@ -32619,9 +33096,12 @@ async function main() {
|
|
|
32619
33096
|
const TERMINAL_AGENTS = [
|
|
32620
33097
|
// Wire = MCP registration + the plugin (skills, hooks, idle-escalation — the
|
|
32621
33098
|
// piece that makes a LIVE session notice your requests without polling).
|
|
32622
|
-
|
|
32623
|
-
|
|
32624
|
-
|
|
33099
|
+
// The slot goes in the MCP config as PAIGY_AGENT, always. A registration without it
|
|
33100
|
+
// used to fall through to a shared default slot, so every unconfigured session on the
|
|
33101
|
+
// machine spoke as whoever paired into it last (strict identity, 2026-08-07).
|
|
33102
|
+
{ cli: "claude", name: "Claude Code", slot: "mcp-agent", wire: "claude mcp add --scope user -e PAIGY_AGENT=mcp-agent paigy -- npx -y @paigy/mcp@latest && claude plugin marketplace add paigy-ai/mcp; claude plugin install paigy@paigy" },
|
|
33103
|
+
{ cli: "codex", name: "Codex", slot: "codex", wire: "codex mcp add paigy --env PAIGY_AGENT=codex -- npx -y @paigy/mcp@latest" },
|
|
33104
|
+
{ cli: "agy", name: "Antigravity", slot: "antigravity" }
|
|
32625
33105
|
];
|
|
32626
33106
|
overrideToken(readToken("Desktop") || null);
|
|
32627
33107
|
for (const t of TERMINAL_AGENTS) {
|
|
@@ -32641,12 +33121,12 @@ async function main() {
|
|
|
32641
33121
|
}
|
|
32642
33122
|
}
|
|
32643
33123
|
try {
|
|
32644
|
-
const settings =
|
|
33124
|
+
const settings = join5(homedir6(), ".claude", "settings.json");
|
|
32645
33125
|
if (which("claude") && existsSync5(settings)) {
|
|
32646
|
-
const cfg = JSON.parse(
|
|
33126
|
+
const cfg = JSON.parse(readFileSync4(settings, "utf8"));
|
|
32647
33127
|
if (!cfg["statusLine"]) {
|
|
32648
33128
|
cfg["statusLine"] = { type: "command", command: "npx -y -p @paigy/mcp@latest paigy-statusline" };
|
|
32649
|
-
|
|
33129
|
+
writeFileSync4(settings, JSON.stringify(cfg, null, 2));
|
|
32650
33130
|
console.log("\u2713 statusline wired");
|
|
32651
33131
|
}
|
|
32652
33132
|
}
|
|
@@ -32696,22 +33176,46 @@ async function main() {
|
|
|
32696
33176
|
</array>
|
|
32697
33177
|
<key>RunAtLoad</key><true/>
|
|
32698
33178
|
<key>KeepAlive</key><true/>
|
|
32699
|
-
<key>
|
|
32700
|
-
|
|
33179
|
+
<key>SoftResourceLimits</key><dict>
|
|
33180
|
+
<key>NumberOfFiles</key><integer>65536</integer>
|
|
33181
|
+
</dict>
|
|
33182
|
+
<key>StandardOutPath</key><string>${join5(homedir6(), ".paigy", "host.log")}</string>
|
|
33183
|
+
<key>StandardErrorPath</key><string>${join5(homedir6(), ".paigy", "host.log")}</string>
|
|
32701
33184
|
</dict></plist>
|
|
32702
33185
|
`;
|
|
32703
|
-
const dir =
|
|
33186
|
+
const dir = join5(homedir6(), "Library", "LaunchAgents");
|
|
32704
33187
|
mkdirSync3(dir, { recursive: true });
|
|
32705
|
-
const path =
|
|
32706
|
-
|
|
33188
|
+
const path = join5(dir, "ai.paigy.harness.plist");
|
|
33189
|
+
writeFileSync4(path, plist);
|
|
32707
33190
|
execSync(`launchctl unload ${path} 2>/dev/null; launchctl load ${path}`, { shell: "/bin/sh" });
|
|
32708
33191
|
console.log(`\u2713 host installed as a login service (${path}) \u2014 logs at ~/.paigy/host.log`);
|
|
33192
|
+
console.log(" file limit raised to 65536 for the host and every session it spawns");
|
|
32709
33193
|
return;
|
|
32710
33194
|
}
|
|
32711
33195
|
if (!readToken("Desktop") && !readToken()) {
|
|
32712
33196
|
console.error("Not paired. Run `paigy-harness pair` (or `npx -y -p @paigy/mcp paigy-mcp-onboard`), then retry.");
|
|
32713
33197
|
process.exit(1);
|
|
32714
33198
|
}
|
|
33199
|
+
if (parsed.handoff) {
|
|
33200
|
+
const slot = AGENT_NAME;
|
|
33201
|
+
if (!readToken(slot)) {
|
|
33202
|
+
console.error(`This terminal holds no Paigy identity (slot "${slot}" is empty) \u2014 pair or hatch first.`);
|
|
33203
|
+
process.exit(1);
|
|
33204
|
+
}
|
|
33205
|
+
const cwd = process.cwd();
|
|
33206
|
+
if (!updateSlot(slot, { workspace: cwd })) {
|
|
33207
|
+
console.error(`Slot "${slot}" isn't in this machine's slot file \u2014 this session was spawned by the host, which already owns its wake dir. Run handoff from the terminal that hatched the identity.`);
|
|
33208
|
+
process.exit(1);
|
|
33209
|
+
}
|
|
33210
|
+
const name = slotName(slot) ?? slot;
|
|
33211
|
+
console.log(`\u2713 ${name} is handed off \u2014 wakes in ${cwd.replace(homedir6(), "~")}`);
|
|
33212
|
+
if (!allowed(cwd, { file: workspacesFile() })) {
|
|
33213
|
+
console.log(` \u26A0 that folder isn't on the allow-list, so wakes will land in the first granted`);
|
|
33214
|
+
console.log(` workspace instead \u2014 add it in the desktop app, or: paigy-harness host --grant ${cwd}`);
|
|
33215
|
+
}
|
|
33216
|
+
console.log(` Write to ${name} from your phone and the host resumes it there, thread as memory.`);
|
|
33217
|
+
return;
|
|
33218
|
+
}
|
|
32715
33219
|
if (parsed.hatch) {
|
|
32716
33220
|
overrideToken(readToken("Desktop") || null);
|
|
32717
33221
|
const minted = await hatch(parsed.hatch, parsed.voice ?? null);
|