@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/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()) {
|
|
@@ -23167,8 +23175,82 @@ function detectAll(deps = {}) {
|
|
|
23167
23175
|
return CATALOG.map((entry) => detect(entry, deps));
|
|
23168
23176
|
}
|
|
23169
23177
|
|
|
23178
|
+
// src/cli-args.ts
|
|
23179
|
+
var USAGE = "usage: paigy-harness setup | pair | host [--grant DIR]\u2026 | service | enable-tools [--scope user|project] | hatch NAME [--voice KEY] [--slot SLOT] | handoff | [--harness claude|codex|agy] [--mode bypass|ask] [--cwd DIR] [--identity NAME] [--grace SECONDS] [--doctor] PROMPT\u2026";
|
|
23180
|
+
function parseArgs(argv) {
|
|
23181
|
+
const args = { harness: "claude", mode: "bypass", cwd: process.cwd(), doctor: false, help: false, host: false, pair: false, service: false, setup: false, enableTools: false, scope: "user", grant: [], hatch: null, voice: null, slot: null, identity: null, graceSeconds: 90, prompt: "", handoff: false };
|
|
23182
|
+
const words = [];
|
|
23183
|
+
for (let i = 0; i < argv.length; i++) {
|
|
23184
|
+
const arg = argv[i];
|
|
23185
|
+
const next = () => argv[++i];
|
|
23186
|
+
if (arg === "--harness") {
|
|
23187
|
+
const v = next();
|
|
23188
|
+
if (v !== "claude" && v !== "codex" && v !== "agy") return { error: `--harness must be claude, codex, or agy, got ${v ?? "nothing"}` };
|
|
23189
|
+
args.harness = v;
|
|
23190
|
+
} else if (arg === "--mode") {
|
|
23191
|
+
const v = next();
|
|
23192
|
+
if (v !== "bypass" && v !== "ask") return { error: `--mode must be bypass or ask, got ${v ?? "nothing"}` };
|
|
23193
|
+
args.mode = v;
|
|
23194
|
+
} else if (arg === "--cwd") {
|
|
23195
|
+
const v = next();
|
|
23196
|
+
if (!v) return { error: "--cwd needs a directory" };
|
|
23197
|
+
args.cwd = v;
|
|
23198
|
+
} else if (arg === "--doctor") {
|
|
23199
|
+
args.doctor = true;
|
|
23200
|
+
} else if (arg === "-h" || arg === "--help" || arg === "help") {
|
|
23201
|
+
args.help = true;
|
|
23202
|
+
} else if (arg === "--scope") {
|
|
23203
|
+
const v = next();
|
|
23204
|
+
if (v !== "user" && v !== "project") return { error: `--scope must be user or project, got ${v ?? "nothing"}` };
|
|
23205
|
+
args.scope = v;
|
|
23206
|
+
} else if (arg === "--grace") {
|
|
23207
|
+
const v = Number(next());
|
|
23208
|
+
if (!Number.isFinite(v) || v < 0) return { error: "--grace needs seconds (0 = straight to phone)" };
|
|
23209
|
+
args.graceSeconds = v;
|
|
23210
|
+
} else if (arg === "host") {
|
|
23211
|
+
args.host = true;
|
|
23212
|
+
} else if (arg === "pair") {
|
|
23213
|
+
args.pair = true;
|
|
23214
|
+
} else if (arg === "setup") {
|
|
23215
|
+
args.setup = true;
|
|
23216
|
+
} else if (arg === "service") {
|
|
23217
|
+
args.service = true;
|
|
23218
|
+
} else if (arg === "--grant") {
|
|
23219
|
+
const v = next();
|
|
23220
|
+
if (!v) return { error: "--grant needs a folder to allow" };
|
|
23221
|
+
args.grant.push(v);
|
|
23222
|
+
} else if (arg === "handoff") {
|
|
23223
|
+
args.handoff = true;
|
|
23224
|
+
} else if (arg === "enable-tools") {
|
|
23225
|
+
args.enableTools = true;
|
|
23226
|
+
} else if (arg === "hatch") {
|
|
23227
|
+
const v = next();
|
|
23228
|
+
if (!v) return { error: 'hatch needs a name: paigy-harness hatch "Voice bug hunter"' };
|
|
23229
|
+
args.hatch = v;
|
|
23230
|
+
} else if (arg === "--voice") {
|
|
23231
|
+
args.voice = next() ?? null;
|
|
23232
|
+
} else if (arg === "--slot") {
|
|
23233
|
+
args.slot = next() ?? null;
|
|
23234
|
+
} else if (arg === "--identity") {
|
|
23235
|
+
const v = next();
|
|
23236
|
+
if (!v) return { error: "--identity needs a hatched agent's name" };
|
|
23237
|
+
args.identity = v;
|
|
23238
|
+
} else if (arg?.startsWith("--")) {
|
|
23239
|
+
return { error: `unknown flag ${arg}` };
|
|
23240
|
+
} else if (arg) {
|
|
23241
|
+
words.push(arg);
|
|
23242
|
+
}
|
|
23243
|
+
}
|
|
23244
|
+
args.prompt = words.join(" ");
|
|
23245
|
+
if (args.help) return args;
|
|
23246
|
+
if (!args.doctor && !args.handoff && !args.hatch && !args.host && !args.pair && !args.service && !args.setup && !args.enableTools && !args.prompt) return { error: "a prompt is required (or setup / --doctor / pair / host / service / enable-tools / hatch NAME / handoff)" };
|
|
23247
|
+
return args;
|
|
23248
|
+
}
|
|
23249
|
+
var ENABLE_TOOLS_COMMAND = "npx -y -p @paigy/mcp@latest paigy-enable-tools";
|
|
23250
|
+
|
|
23170
23251
|
// ../../packages/sdk/dist/index.js
|
|
23171
23252
|
import { createRequire as __sdkCreateRequire } from "module";
|
|
23253
|
+
import { randomUUID } from "crypto";
|
|
23172
23254
|
|
|
23173
23255
|
// ../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/external.js
|
|
23174
23256
|
var external_exports = {};
|
|
@@ -27212,7 +27294,8 @@ var coerce = {
|
|
|
27212
27294
|
var NEVER = INVALID;
|
|
27213
27295
|
|
|
27214
27296
|
// ../../packages/sdk/dist/index.js
|
|
27215
|
-
import { existsSync as existsSync2, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
|
|
27297
|
+
import { closeSync, existsSync as existsSync2, mkdirSync, openSync, readFileSync, rmSync, statSync, writeFileSync } from "fs";
|
|
27298
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
27216
27299
|
import { homedir as homedir2 } from "os";
|
|
27217
27300
|
import { join as join2 } from "path";
|
|
27218
27301
|
var require2 = __sdkCreateRequire(import.meta.url);
|
|
@@ -29478,16 +29561,25 @@ async function proxy() {
|
|
|
29478
29561
|
if (!PROXY_ENV.some((k) => process.env[k])) return void 0;
|
|
29479
29562
|
return agent ??= new (await Promise.resolve().then(() => __toESM(require_undici(), 1))).EnvHttpProxyAgent();
|
|
29480
29563
|
}
|
|
29564
|
+
var INSTANCE_ID = randomUUID();
|
|
29565
|
+
var SESSION_ID = process.env.PAIGY_SESSION_ID ?? process.env.CLAUDE_CODE_SESSION_ID ?? INSTANCE_ID;
|
|
29481
29566
|
async function reach(url, init) {
|
|
29482
29567
|
try {
|
|
29483
|
-
|
|
29568
|
+
const headers = {
|
|
29569
|
+
...init?.headers,
|
|
29570
|
+
"x-paigy-instance": INSTANCE_ID,
|
|
29571
|
+
"x-paigy-session": SESSION_ID
|
|
29572
|
+
};
|
|
29573
|
+
return await fetch(url, { ...init, headers, dispatcher: await proxy() });
|
|
29484
29574
|
} catch (e) {
|
|
29485
29575
|
throw new Error(`${NETWORK_MSG} (${e?.message ?? String(e)})`);
|
|
29486
29576
|
}
|
|
29487
29577
|
}
|
|
29488
29578
|
var ContextSchema = external_exports.object({
|
|
29489
29579
|
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)).
|
|
29580
|
+
description: external_exports.array(external_exports.string().min(1)).describe(
|
|
29581
|
+
"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."
|
|
29582
|
+
)
|
|
29491
29583
|
});
|
|
29492
29584
|
var ParticipantSchema = external_exports.object({
|
|
29493
29585
|
kind: external_exports.enum(["human", "agent"]),
|
|
@@ -29634,6 +29726,14 @@ var NotifyRequestSchema = external_exports.object({
|
|
|
29634
29726
|
waiting: external_exports.enum(["none", "soft", "hard"]).optional().describe(
|
|
29635
29727
|
"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
29728
|
),
|
|
29729
|
+
/** Δ9b (#895): HOLD this claim so the sender can correct the plan before anyone is
|
|
29730
|
+
* interrupted. Opt-in per claim, because the fail-open is a minute-granularity cron —
|
|
29731
|
+
* holding by default would charge every quiet claim that minute before any agent could
|
|
29732
|
+
* correct anything. Ignored for `waiting: 'hard'`: a blocking ask rings on what we have,
|
|
29733
|
+
* and the enrichment can still land mid-call (#781 re-plans the unspoken tail). */
|
|
29734
|
+
confirm: external_exports.boolean().optional().describe(
|
|
29735
|
+
"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'."
|
|
29736
|
+
),
|
|
29637
29737
|
/** #575: a RELAY of the user's explicitly stated preference, never the agent's
|
|
29638
29738
|
* choice. Outranks waiting in both directions: 'call' rings even for a
|
|
29639
29739
|
* waiting:'none' "call me when it's done"; 'message' never rings even for
|
|
@@ -29667,7 +29767,7 @@ var NotifyRequestSchema = external_exports.object({
|
|
|
29667
29767
|
if (r.ask !== void 0) {
|
|
29668
29768
|
for (const f of ["context", "select", "points"]) {
|
|
29669
29769
|
if (r[f] !== void 0)
|
|
29670
|
-
ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: [f], message: `the simplified \`ask\` form takes no ${f} \u2014 the broker derives it
|
|
29770
|
+
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.` });
|
|
29671
29771
|
}
|
|
29672
29772
|
return;
|
|
29673
29773
|
}
|
|
@@ -29692,35 +29792,45 @@ function normalizeWaiting(req) {
|
|
|
29692
29792
|
blocking: req.blocking || waiting === "hard"
|
|
29693
29793
|
};
|
|
29694
29794
|
}
|
|
29695
|
-
|
|
29696
|
-
|
|
29697
|
-
|
|
29795
|
+
function unitsOf(text) {
|
|
29796
|
+
const spans = [];
|
|
29797
|
+
const re = /\n\s*\n+/g;
|
|
29798
|
+
let cursor = 0;
|
|
29799
|
+
const push = (from, to) => {
|
|
29800
|
+
const slice = text.slice(from, to);
|
|
29801
|
+
const lead = slice.length - slice.trimStart().length;
|
|
29802
|
+
const tail = slice.length - slice.trimEnd().length;
|
|
29803
|
+
if (from + lead < to - tail) spans.push({ start: from + lead, end: to - tail });
|
|
29804
|
+
};
|
|
29805
|
+
for (let m = re.exec(text); m; m = re.exec(text)) {
|
|
29806
|
+
push(cursor, m.index);
|
|
29807
|
+
cursor = m.index + m[0].length;
|
|
29808
|
+
}
|
|
29809
|
+
push(cursor, text.length);
|
|
29810
|
+
return spans;
|
|
29811
|
+
}
|
|
29812
|
+
function headline(text) {
|
|
29813
|
+
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) ?? "";
|
|
29814
|
+
return (/^[^.!?\n]+[.!?]?/.exec(line)?.[0] ?? line).trim();
|
|
29815
|
+
}
|
|
29816
|
+
function bodyAfterHeadline(text) {
|
|
29698
29817
|
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(" ")];
|
|
29818
|
+
const title = headline(body);
|
|
29819
|
+
const rest = body.slice(title.length).replace(/^[\s.!?—–-]+/, "").trim();
|
|
29820
|
+
if (!rest) return { title, description: [] };
|
|
29821
|
+
const chunks = unitsOf(rest).map((u) => rest.slice(u.start, u.end)).filter(Boolean);
|
|
29822
|
+
return { title, description: chunks.length ? chunks : [rest] };
|
|
29711
29823
|
}
|
|
29712
29824
|
function deriveAsk(req) {
|
|
29713
29825
|
req = normalizeWaiting(req);
|
|
29714
29826
|
if (!req.ask) return req;
|
|
29715
29827
|
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
29828
|
const hinted = req.urgencyHint === "now" ? "call" : req.urgencyHint === "soon" ? "banner" : req.urgencyHint === "whenever" ? "inbox" : req.urgency;
|
|
29719
29829
|
const urgency = req.channel === "call" ? "call" : req.channel === "message" && hinted === "call" ? "banner" : hinted;
|
|
29720
29830
|
const { ask: _ask, needs, urgencyHint: _hint, channel: _channel, ...rest } = req;
|
|
29721
29831
|
return {
|
|
29722
29832
|
...rest,
|
|
29723
|
-
context:
|
|
29833
|
+
context: bodyAfterHeadline(text),
|
|
29724
29834
|
// Options riding alongside the ask (#575: pixels can't be prose) floor to a
|
|
29725
29835
|
// single pick — the model broker may upgrade to many/rank from the wording.
|
|
29726
29836
|
select: req.options?.length ? "one" : "text",
|
|
@@ -29910,6 +30020,40 @@ var NotifyResponseSchema = external_exports.object({
|
|
|
29910
30020
|
answer: UserAnswerSchema.optional(),
|
|
29911
30021
|
answeredAt: external_exports.string().datetime().optional()
|
|
29912
30022
|
});
|
|
30023
|
+
var NotifyPlanUnitSchema = external_exports.object({
|
|
30024
|
+
notificationId: external_exports.string(),
|
|
30025
|
+
/** The unit's own heading, so the agent can tell which of its paragraphs this became. */
|
|
30026
|
+
title: external_exports.string(),
|
|
30027
|
+
/** How loudly this unit was arbitrated to arrive — per unit, which is the point of units. */
|
|
30028
|
+
level: NotifyLevelSchema,
|
|
30029
|
+
/** Answered from something the user already decided: nobody is interrupted, and a trail card
|
|
30030
|
+
* says so. The agent should not wait on this one. */
|
|
30031
|
+
settled: external_exports.literal(true).optional(),
|
|
30032
|
+
/** What this unit would need to be answerable and does not carry (#894). A PROPOSAL to the
|
|
30033
|
+
* agent — nothing here changed the ask, and ignoring it costs nothing. */
|
|
30034
|
+
needs: external_exports.array(external_exports.enum(["options", "visuals"])).optional(),
|
|
30035
|
+
/** The SHAPE the broker would give this unit, for the agent to ratify (#886/#894). The
|
|
30036
|
+
* split layer reads prose and can see that a paragraph is a yes/no or a pick-one — but a
|
|
30037
|
+
* broker that DECIDES that destroys the only fact separating a statement from a real ask
|
|
30038
|
+
* (#731), so it is offered, never applied: the unit is stored `text` until the agent
|
|
30039
|
+
* confirms the shape (POST /notify/:id/confirm). Ignoring it costs nothing. */
|
|
30040
|
+
proposal: external_exports.object({
|
|
30041
|
+
select: SelectShapeSchema,
|
|
30042
|
+
options: external_exports.array(external_exports.object({ label: external_exports.string().min(1) })).optional()
|
|
30043
|
+
}).optional(),
|
|
30044
|
+
/** What the broker READ this unit as wanting from the human (#952 layer 2): a `decision`
|
|
30045
|
+
* between alternatives, an `approval` the agent is blocked on, or `knowledge` it just
|
|
30046
|
+
* needs to know. Reported so the agent can correct a misread the same way it ratifies a
|
|
30047
|
+
* shape — the read RAISES (a decision always asks) and never silences a question the
|
|
30048
|
+
* agent declared (#731, #923). */
|
|
30049
|
+
wants: external_exports.enum(["decision", "approval", "knowledge"]).optional()
|
|
30050
|
+
});
|
|
30051
|
+
var NotifyPlanSchema = external_exports.object({
|
|
30052
|
+
units: external_exports.array(NotifyPlanUnitSchema),
|
|
30053
|
+
/** Which unit is this arrival's ONE interruption (units-design.md D23). Absent means nobody
|
|
30054
|
+
* was interrupted — every unit was either settled or quiet enough to sit in the inbox. */
|
|
30055
|
+
speaks: external_exports.string().optional()
|
|
30056
|
+
});
|
|
29913
30057
|
var UserResponseSchema = external_exports.object({
|
|
29914
30058
|
requestId: external_exports.string(),
|
|
29915
30059
|
answer: UserAnswerSchema,
|
|
@@ -29964,6 +30108,20 @@ var InboxItemSchema = external_exports.object({
|
|
|
29964
30108
|
/** The conversation thread + connection this item lives on. Present on the replied
|
|
29965
30109
|
* detail — they power History's "Continue" / "New session from this" (#57/#251). */
|
|
29966
30110
|
parentId: external_exports.string().optional(),
|
|
30111
|
+
/** THE ARRIVAL this row is one unit of (`notifications.ask_id` → `asks`). A claim is one
|
|
30112
|
+
* arrival and its units are N rows of it, so this — not `parentId` — is what makes a
|
|
30113
|
+
* multi-part notification one thing on screen. The thread is the whole CONVERSATION: it
|
|
30114
|
+
* accumulates every message an agent ever sent, so grouping by it renders a day of
|
|
30115
|
+
* unrelated updates as a single "12-part request". Absent on rows written before the
|
|
30116
|
+
* `asks` table, and on anything that never went through `notify` — both fall back to the
|
|
30117
|
+
* thread, which is what the client did for all rows until now. */
|
|
30118
|
+
askId: external_exports.string().optional(),
|
|
30119
|
+
/** WHERE this unit sat in the message it was cut from (`notifications.seq`). The batch
|
|
30120
|
+
* shares one `created_at` to the microsecond, so without it the author's order is
|
|
30121
|
+
* unrecoverable client-side — a four-paragraph briefing rendered opening-paragraph-last
|
|
30122
|
+
* (live 2026-08-10, D35). The API already orders by it; this lets a reader that
|
|
30123
|
+
* re-sorts (grouping, filtering) put an arrival back in the order it was written. */
|
|
30124
|
+
seq: external_exports.number().int().optional(),
|
|
29967
30125
|
tokenId: external_exports.string().optional(),
|
|
29968
30126
|
status: NotifyStatusSchema,
|
|
29969
30127
|
context: ContextSchema,
|
|
@@ -29974,6 +30132,16 @@ var InboxItemSchema = external_exports.object({
|
|
|
29974
30132
|
* ring/enqueue time. Replaces the condensed line + index-aligned phrased points, which
|
|
29975
30133
|
* between them could not express a call as a sequence. `question: null` is a real turn —
|
|
29976
30134
|
* a status update stays a statement instead of being shaped into a yes/no. */
|
|
30135
|
+
/** Does this claim want an ANSWER, or is it telling you something? Written per row from
|
|
30136
|
+
* `requestAsks` — the agent's own declaration, not a guess. `false` is what earns a card
|
|
30137
|
+
* its acknowledge affordance: without it a status update offers a text box and a dismiss,
|
|
30138
|
+
* and neither of those is "got it" (owner, 2026-08-10). */
|
|
30139
|
+
asks: external_exports.boolean().optional(),
|
|
30140
|
+
/** When a live process last pulsed for this row's agent — the liveness input for
|
|
30141
|
+
* "working requires a pulse" (#928): the list said "Working…" from agent_state alone
|
|
30142
|
+
* while the party called the same dead claim stalled. Absent = no token/no data,
|
|
30143
|
+
* which must never CLAIM stalled. */
|
|
30144
|
+
lastSeenAt: external_exports.string().optional(),
|
|
29977
30145
|
agenda: external_exports.array(AgendaTurnSchema).optional(),
|
|
29978
30146
|
/** On a replied detail (#397): the next steps the user attached to the answer
|
|
29979
30147
|
* ("call back after lunch") — shown so they can see the commitment was captured. */
|
|
@@ -29998,6 +30166,23 @@ var InboxItemSchema = external_exports.object({
|
|
|
29998
30166
|
* client may still flag a stall by age. Drives the inbox error badge + Retry. */
|
|
29999
30167
|
error: external_exports.string().optional(),
|
|
30000
30168
|
clarifies: external_exports.string().optional(),
|
|
30169
|
+
/** The ring ladder ran out while this was still pending — we tried to reach you and
|
|
30170
|
+
* STOPPED trying (`arbitration/arbitrate.ts` `nextRing` → `stop`). Distinct from an
|
|
30171
|
+
* agent with nothing to say, which the roster drew identically until now: "nothing to
|
|
30172
|
+
* say" and "gave up saying it" are opposite situations wearing the same face
|
|
30173
|
+
* (navigation-design.md, gap 1). False for anything that never rang. */
|
|
30174
|
+
gaveUp: external_exports.boolean().default(false),
|
|
30175
|
+
/** Why this arrived the way it did, read back off the delivery receipt (`notify/why.ts`).
|
|
30176
|
+
* Absent for anything never delivered through a push, and for older rows written before
|
|
30177
|
+
* the reason was recorded. Deliberately a debug affordance, shown small (owner,
|
|
30178
|
+
* 2026-08-07) — its real job is to give "this didn't need a call" something to be
|
|
30179
|
+
* feedback ABOUT. */
|
|
30180
|
+
why: external_exports.object({
|
|
30181
|
+
asked: NotifyLevelSchema,
|
|
30182
|
+
got: NotifyLevelSchema,
|
|
30183
|
+
because: external_exports.enum(["unresponsive", "dismissed", "not_permitted", "silent", "coalesced", "agent_capped", "unplanned", "learned_raise"]).optional(),
|
|
30184
|
+
line: external_exports.string()
|
|
30185
|
+
}).optional(),
|
|
30001
30186
|
select: external_exports.enum(["one", "many", "rank", "confirm", "text"]).default("one"),
|
|
30002
30187
|
confirmStyle: external_exports.enum(["yesno", "approve"]).default("yesno").describe(
|
|
30003
30188
|
"Labels for a select:'confirm' paige \u2014 'yesno' (Yes/No) or 'approve' (Approve/Deny). Ignored unless select is 'confirm'."
|
|
@@ -30108,6 +30293,15 @@ var HistoryItemSchema = external_exports.object({
|
|
|
30108
30293
|
/** When you answered the agent's notification (agent→user only). */
|
|
30109
30294
|
humanAckedAt: external_exports.string().nullable()
|
|
30110
30295
|
});
|
|
30296
|
+
var ACTIVITY_LINES = 2;
|
|
30297
|
+
var ACTIVITY_LINE_MAX = 80;
|
|
30298
|
+
var AgentActivitySchema = external_exports.object({
|
|
30299
|
+
/** Oldest first, so the newest line is last — the one that replaces in place. */
|
|
30300
|
+
lines: external_exports.array(external_exports.string().max(ACTIVITY_LINE_MAX)).max(ACTIVITY_LINES),
|
|
30301
|
+
/** When the harness observed this tail. Its own timestamp, not the heartbeat's: a beat
|
|
30302
|
+
* that carries an UNCHANGED tail must not make a stalled agent look like it just moved. */
|
|
30303
|
+
at: external_exports.string().datetime()
|
|
30304
|
+
});
|
|
30111
30305
|
var ConnectionSummarySchema = external_exports.object({
|
|
30112
30306
|
/** The connection = the agent's token id (used to address a request). */
|
|
30113
30307
|
id: external_exports.string(),
|
|
@@ -30119,6 +30313,14 @@ var ConnectionSummarySchema = external_exports.object({
|
|
|
30119
30313
|
provider: external_exports.string().nullable(),
|
|
30120
30314
|
/** The pairing's assigned voice (#462); null = the default voice. */
|
|
30121
30315
|
voice: VoiceKeySchema.nullable(),
|
|
30316
|
+
/** The LOUDEST this agent may ever reach you — a ceiling on `NOTIFY_LADDER`, set by the
|
|
30317
|
+
* user on the agent's own page. null = no ceiling (today's behaviour for every
|
|
30318
|
+
* connection). Android binds importance to a relationship rather than to each message,
|
|
30319
|
+
* and that is the thing our roster could not say: "Marlow may always call me; Otto
|
|
30320
|
+
* never may" (navigation-design.md, gap 2). Clamped in `arbitrateLevel`, so it binds
|
|
30321
|
+
* every surface at once and outranks even `sessionMode: all_calls` — a mode the user
|
|
30322
|
+
* set once must not overrule a rule they set about one agent. */
|
|
30323
|
+
reach: NotifyLevelSchema.nullable().optional(),
|
|
30122
30324
|
createdAt: external_exports.string().datetime(),
|
|
30123
30325
|
/** Most recent notification on this connection, either direction. Null = no contact yet.
|
|
30124
30326
|
* Drives the agents-page recency grouping (Today / This week / …). */
|
|
@@ -30133,10 +30335,49 @@ var ConnectionSummarySchema = external_exports.object({
|
|
|
30133
30335
|
harnesses: external_exports.array(external_exports.object({ name: external_exports.string(), label: external_exports.string(), status: external_exports.string() })).optional(),
|
|
30134
30336
|
workspaces: external_exports.array(external_exports.string()).optional()
|
|
30135
30337
|
}).optional(),
|
|
30338
|
+
/** The tail of this agent's working log, when a harness is driving it — the agent page's
|
|
30339
|
+
* live strip. Absent for anything the desktop harness isn't running (a hatched identity
|
|
30340
|
+
* used straight from a terminal emits no work events; the page says so rather than
|
|
30341
|
+
* drawing an empty box). */
|
|
30342
|
+
activity: AgentActivitySchema.optional(),
|
|
30136
30343
|
/** True = a provider-managed agent running in the provider's cloud (e.g. Anthropic CMA);
|
|
30137
30344
|
* false = a local MCP connection running on the user's computer (Claude Code/Codex/…). */
|
|
30138
30345
|
managed: external_exports.boolean()
|
|
30139
30346
|
});
|
|
30347
|
+
var MoveRingSchema = external_exports.enum(["home", "travels", "retired", "quarantined"]);
|
|
30348
|
+
var MoveSchema = external_exports.object({
|
|
30349
|
+
id: external_exports.string(),
|
|
30350
|
+
/** The reusable question, as distill normalized it. */
|
|
30351
|
+
question: external_exports.string(),
|
|
30352
|
+
/** The operative ruling. Editable by the user (PATCH) — which resets the ledger. */
|
|
30353
|
+
answer: external_exports.string(),
|
|
30354
|
+
/** The user's stated reason, when they gave one. Null = inherently narrow: the judge is
|
|
30355
|
+
* told so, and the ruling only derives essentially the same question in the same scope. */
|
|
30356
|
+
rationale: external_exports.string().nullable(),
|
|
30357
|
+
/** Where the ruling lives: a repo/workspace, or 'global'. */
|
|
30358
|
+
scope: external_exports.string(),
|
|
30359
|
+
ring: MoveRingSchema,
|
|
30360
|
+
/** True = the user pinned it with `always` (travel granted by hand, not by evidence). */
|
|
30361
|
+
pinned: external_exports.boolean(),
|
|
30362
|
+
/** True = a pin the user placed was BROKEN by later counter-evidence. Surfaced so the
|
|
30363
|
+
* break is visible instead of a pin silently disappearing. */
|
|
30364
|
+
pinBroken: external_exports.boolean(),
|
|
30365
|
+
/** When the ruling was distilled. */
|
|
30366
|
+
learnedAt: external_exports.string(),
|
|
30367
|
+
/** Last time it answered an ask. Null = never fired. */
|
|
30368
|
+
lastUsedAt: external_exports.string().nullable(),
|
|
30369
|
+
/** How many asks it has answered. Instrumentation — deliberately NOT an input to the
|
|
30370
|
+
* evidence curve: firing says the question keeps arising, not that the ruling is right. */
|
|
30371
|
+
usedCount: external_exports.number(),
|
|
30372
|
+
/** Ledger: outcomes that said it held up. Saturating — the tenth is worth almost nothing. */
|
|
30373
|
+
confirms: external_exports.number(),
|
|
30374
|
+
/** Ledger: contradictions, in signal units (a full override = 1, weaker signals less).
|
|
30375
|
+
* Linear and priced above the entire confirmation budget, so any full counter wins. */
|
|
30376
|
+
counters: external_exports.number(),
|
|
30377
|
+
/** The agent that asked the question this move came from, when known. Null for a move
|
|
30378
|
+
* distilled from a clarify ruling (those carry no agent) or one whose source rows are gone. */
|
|
30379
|
+
learnedFrom: external_exports.object({ id: external_exports.string(), name: external_exports.string() }).nullable()
|
|
30380
|
+
});
|
|
30140
30381
|
var CreateRequestSchema = external_exports.object({
|
|
30141
30382
|
/** The connection (token id) to send to, from GET /api/tokens. */
|
|
30142
30383
|
tokenId: external_exports.string(),
|
|
@@ -30343,6 +30584,17 @@ var DeviceTokenSchema = external_exports.object({
|
|
|
30343
30584
|
* a default silly name). */
|
|
30344
30585
|
name: external_exports.string(),
|
|
30345
30586
|
device: external_exports.string().nullable(),
|
|
30587
|
+
/** The pairing's assigned voice, cached so the desktop can seed the SAME face the phone
|
|
30588
|
+
* draws — voice is the third ingredient of a hatchling's build (party/traits.ts). */
|
|
30589
|
+
voice: external_exports.string().nullable().optional(),
|
|
30590
|
+
/** The token's server-side id — the face's COLOUR anchor, and the only seed ingredient
|
|
30591
|
+
* that survives a rename. Cached by the host's identity beat. */
|
|
30592
|
+
token_id: external_exports.string().nullable().optional(),
|
|
30593
|
+
/** WHERE this identity works — the folder a wake should land it in. Written by the host
|
|
30594
|
+
* at spawn and by `paigy-harness handoff` from a live terminal. Without it every wake
|
|
30595
|
+
* landed in the FIRST granted workspace and the agent rediscovered its own repo from
|
|
30596
|
+
* the thread each time (host.ts, live catch 2026-08-06 — prompt-papered until now). */
|
|
30597
|
+
workspace: external_exports.string().nullable().optional(),
|
|
30346
30598
|
phone_reveal: PairingRevealSchema.nullable().optional(),
|
|
30347
30599
|
// present once the phone reveals
|
|
30348
30600
|
uik_pub: external_exports.string().nullable().optional()
|
|
@@ -30369,6 +30621,8 @@ var NotificationFeedbackKindSchema = external_exports.enum([
|
|
|
30369
30621
|
// "There should be a picture or design here."
|
|
30370
30622
|
"should_have_called",
|
|
30371
30623
|
// "Don't put this in a banner — ring me for something like this."
|
|
30624
|
+
"should_have_messaged",
|
|
30625
|
+
// the inverse: "that didn't deserve a ring — a message would do."
|
|
30372
30626
|
"other"
|
|
30373
30627
|
// anything else — the note carries it.
|
|
30374
30628
|
]);
|
|
@@ -30665,7 +30919,11 @@ function open(envelope, myKeyId, mySecretKeyB64) {
|
|
|
30665
30919
|
if (!eqCt(sealedCanon, expected)) throw new Error("header mismatch (tampered metadata)");
|
|
30666
30920
|
return { header: envelope.hdr, body };
|
|
30667
30921
|
}
|
|
30668
|
-
|
|
30922
|
+
function sessionSlot(sessionId) {
|
|
30923
|
+
const id = sessionId ?? process.env.PAIGY_SESSION_ID ?? process.env.CLAUDE_CODE_SESSION_ID ?? randomUUID2();
|
|
30924
|
+
return `session:${id.slice(0, 8)}`;
|
|
30925
|
+
}
|
|
30926
|
+
var AGENT_NAME = process.env.PAIGY_AGENT || sessionSlot();
|
|
30669
30927
|
var TOKEN_PATH = join2(homedir2(), ".paigy", "token.json");
|
|
30670
30928
|
var KEY_PATH = join2(homedir2(), ".paigy", "key.json");
|
|
30671
30929
|
function readTokenFile() {
|
|
@@ -30680,10 +30938,45 @@ function readTokenFile() {
|
|
|
30680
30938
|
return {};
|
|
30681
30939
|
}
|
|
30682
30940
|
}
|
|
30683
|
-
function
|
|
30684
|
-
const
|
|
30941
|
+
function withTokenLock(mutate) {
|
|
30942
|
+
const lock = TOKEN_PATH + ".lock";
|
|
30943
|
+
const spin = new Int32Array(new SharedArrayBuffer(4));
|
|
30685
30944
|
mkdirSync(join2(homedir2(), ".paigy"), { recursive: true });
|
|
30686
|
-
|
|
30945
|
+
for (const deadline = Date.now() + 5e3; Date.now() < deadline; ) {
|
|
30946
|
+
try {
|
|
30947
|
+
closeSync(openSync(lock, "wx"));
|
|
30948
|
+
break;
|
|
30949
|
+
} catch {
|
|
30950
|
+
const held = statSync(lock, { throwIfNoEntry: false })?.mtimeMs ?? Date.now();
|
|
30951
|
+
if (Date.now() - held > 5e3) rmSync(lock, { force: true });
|
|
30952
|
+
else Atomics.wait(spin, 0, 0, 25);
|
|
30953
|
+
}
|
|
30954
|
+
}
|
|
30955
|
+
try {
|
|
30956
|
+
return mutate();
|
|
30957
|
+
} finally {
|
|
30958
|
+
rmSync(lock, { force: true });
|
|
30959
|
+
}
|
|
30960
|
+
}
|
|
30961
|
+
function saveToken(token, agent2 = AGENT_NAME) {
|
|
30962
|
+
withTokenLock(() => {
|
|
30963
|
+
const slots = { ...readTokenFile(), [agent2]: token };
|
|
30964
|
+
writeFileSync(TOKEN_PATH, JSON.stringify(slots, null, 2) + "\n", { mode: 384 });
|
|
30965
|
+
});
|
|
30966
|
+
}
|
|
30967
|
+
function updateSlot(agent2, patch) {
|
|
30968
|
+
return withTokenLock(() => {
|
|
30969
|
+
const slots = readTokenFile();
|
|
30970
|
+
const existing = slots[agent2];
|
|
30971
|
+
if (!existing) return false;
|
|
30972
|
+
slots[agent2] = { ...existing, ...patch };
|
|
30973
|
+
writeFileSync(TOKEN_PATH, JSON.stringify(slots, null, 2) + "\n", { mode: 384 });
|
|
30974
|
+
return true;
|
|
30975
|
+
});
|
|
30976
|
+
}
|
|
30977
|
+
function slotIdentity(agent2) {
|
|
30978
|
+
const t = readTokenFile()[agent2];
|
|
30979
|
+
return { name: t?.name ?? null, voice: t?.voice ?? null, tokenId: t?.token_id ?? null, workspace: t?.workspace ?? null };
|
|
30687
30980
|
}
|
|
30688
30981
|
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
30689
30982
|
function readToken(agent2 = AGENT_NAME) {
|
|
@@ -30881,10 +31174,14 @@ async function claimSessions(opts = {}) {
|
|
|
30881
31174
|
return (await res.json()).sessions;
|
|
30882
31175
|
}
|
|
30883
31176
|
async function heartbeat(runtime, opts = {}) {
|
|
31177
|
+
const body = {
|
|
31178
|
+
...runtime !== void 0 ? { runtime } : {},
|
|
31179
|
+
...opts.activity !== void 0 ? { activity: opts.activity } : {}
|
|
31180
|
+
};
|
|
30884
31181
|
const res = ensureAuthed(await reach(`${BACKEND_URL}/api/presence`, {
|
|
30885
31182
|
method: "POST",
|
|
30886
31183
|
headers: { "content-type": "application/json", authorization: `Bearer ${authToken(opts.token)}` },
|
|
30887
|
-
...
|
|
31184
|
+
...Object.keys(body).length > 0 ? { body: JSON.stringify(body) } : {}
|
|
30888
31185
|
}));
|
|
30889
31186
|
if (!res.ok) throw new Error(`heartbeat failed: ${res.status}`);
|
|
30890
31187
|
}
|
|
@@ -30909,7 +31206,9 @@ import { join as join5 } from "path";
|
|
|
30909
31206
|
// ../../packages/schema/dist/index.js
|
|
30910
31207
|
var ContextSchema2 = external_exports.object({
|
|
30911
31208
|
title: external_exports.string().min(1).describe("One-line headline of what you need (required, non-empty)."),
|
|
30912
|
-
description: external_exports.array(external_exports.string().min(1)).
|
|
31209
|
+
description: external_exports.array(external_exports.string().min(1)).describe(
|
|
31210
|
+
"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."
|
|
31211
|
+
)
|
|
30913
31212
|
});
|
|
30914
31213
|
var ParticipantSchema2 = external_exports.object({
|
|
30915
31214
|
kind: external_exports.enum(["human", "agent"]),
|
|
@@ -31056,6 +31355,14 @@ var NotifyRequestSchema2 = external_exports.object({
|
|
|
31056
31355
|
waiting: external_exports.enum(["none", "soft", "hard"]).optional().describe(
|
|
31057
31356
|
"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."
|
|
31058
31357
|
),
|
|
31358
|
+
/** Δ9b (#895): HOLD this claim so the sender can correct the plan before anyone is
|
|
31359
|
+
* interrupted. Opt-in per claim, because the fail-open is a minute-granularity cron —
|
|
31360
|
+
* holding by default would charge every quiet claim that minute before any agent could
|
|
31361
|
+
* correct anything. Ignored for `waiting: 'hard'`: a blocking ask rings on what we have,
|
|
31362
|
+
* and the enrichment can still land mid-call (#781 re-plans the unspoken tail). */
|
|
31363
|
+
confirm: external_exports.boolean().optional().describe(
|
|
31364
|
+
"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'."
|
|
31365
|
+
),
|
|
31059
31366
|
/** #575: a RELAY of the user's explicitly stated preference, never the agent's
|
|
31060
31367
|
* choice. Outranks waiting in both directions: 'call' rings even for a
|
|
31061
31368
|
* waiting:'none' "call me when it's done"; 'message' never rings even for
|
|
@@ -31089,7 +31396,7 @@ var NotifyRequestSchema2 = external_exports.object({
|
|
|
31089
31396
|
if (r.ask !== void 0) {
|
|
31090
31397
|
for (const f of ["context", "select", "points"]) {
|
|
31091
31398
|
if (r[f] !== void 0)
|
|
31092
|
-
ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: [f], message: `the simplified \`ask\` form takes no ${f} \u2014 the broker derives it
|
|
31399
|
+
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.` });
|
|
31093
31400
|
}
|
|
31094
31401
|
return;
|
|
31095
31402
|
}
|
|
@@ -31105,6 +31412,23 @@ var NotifyRequestSchema2 = external_exports.object({
|
|
|
31105
31412
|
if (!needsOptions && r.options?.length)
|
|
31106
31413
|
ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["options"], message: `select:'${r.select}' takes no options` });
|
|
31107
31414
|
});
|
|
31415
|
+
function unitsOf2(text) {
|
|
31416
|
+
const spans = [];
|
|
31417
|
+
const re = /\n\s*\n+/g;
|
|
31418
|
+
let cursor = 0;
|
|
31419
|
+
const push = (from, to) => {
|
|
31420
|
+
const slice = text.slice(from, to);
|
|
31421
|
+
const lead = slice.length - slice.trimStart().length;
|
|
31422
|
+
const tail = slice.length - slice.trimEnd().length;
|
|
31423
|
+
if (from + lead < to - tail) spans.push({ start: from + lead, end: to - tail });
|
|
31424
|
+
};
|
|
31425
|
+
for (let m = re.exec(text); m; m = re.exec(text)) {
|
|
31426
|
+
push(cursor, m.index);
|
|
31427
|
+
cursor = m.index + m[0].length;
|
|
31428
|
+
}
|
|
31429
|
+
push(cursor, text.length);
|
|
31430
|
+
return spans;
|
|
31431
|
+
}
|
|
31108
31432
|
var NotifyStatusSchema2 = external_exports.enum(["pending", "answered", "ignored"]);
|
|
31109
31433
|
var AgentStateSchema2 = external_exports.enum(["idle", "in_progress", "completed", "needs_input"]);
|
|
31110
31434
|
var SetTaskStateSchema2 = external_exports.object({
|
|
@@ -31287,6 +31611,40 @@ var NotifyResponseSchema2 = external_exports.object({
|
|
|
31287
31611
|
answer: UserAnswerSchema2.optional(),
|
|
31288
31612
|
answeredAt: external_exports.string().datetime().optional()
|
|
31289
31613
|
});
|
|
31614
|
+
var NotifyPlanUnitSchema2 = external_exports.object({
|
|
31615
|
+
notificationId: external_exports.string(),
|
|
31616
|
+
/** The unit's own heading, so the agent can tell which of its paragraphs this became. */
|
|
31617
|
+
title: external_exports.string(),
|
|
31618
|
+
/** How loudly this unit was arbitrated to arrive — per unit, which is the point of units. */
|
|
31619
|
+
level: NotifyLevelSchema2,
|
|
31620
|
+
/** Answered from something the user already decided: nobody is interrupted, and a trail card
|
|
31621
|
+
* says so. The agent should not wait on this one. */
|
|
31622
|
+
settled: external_exports.literal(true).optional(),
|
|
31623
|
+
/** What this unit would need to be answerable and does not carry (#894). A PROPOSAL to the
|
|
31624
|
+
* agent — nothing here changed the ask, and ignoring it costs nothing. */
|
|
31625
|
+
needs: external_exports.array(external_exports.enum(["options", "visuals"])).optional(),
|
|
31626
|
+
/** The SHAPE the broker would give this unit, for the agent to ratify (#886/#894). The
|
|
31627
|
+
* split layer reads prose and can see that a paragraph is a yes/no or a pick-one — but a
|
|
31628
|
+
* broker that DECIDES that destroys the only fact separating a statement from a real ask
|
|
31629
|
+
* (#731), so it is offered, never applied: the unit is stored `text` until the agent
|
|
31630
|
+
* confirms the shape (POST /notify/:id/confirm). Ignoring it costs nothing. */
|
|
31631
|
+
proposal: external_exports.object({
|
|
31632
|
+
select: SelectShapeSchema2,
|
|
31633
|
+
options: external_exports.array(external_exports.object({ label: external_exports.string().min(1) })).optional()
|
|
31634
|
+
}).optional(),
|
|
31635
|
+
/** What the broker READ this unit as wanting from the human (#952 layer 2): a `decision`
|
|
31636
|
+
* between alternatives, an `approval` the agent is blocked on, or `knowledge` it just
|
|
31637
|
+
* needs to know. Reported so the agent can correct a misread the same way it ratifies a
|
|
31638
|
+
* shape — the read RAISES (a decision always asks) and never silences a question the
|
|
31639
|
+
* agent declared (#731, #923). */
|
|
31640
|
+
wants: external_exports.enum(["decision", "approval", "knowledge"]).optional()
|
|
31641
|
+
});
|
|
31642
|
+
var NotifyPlanSchema2 = external_exports.object({
|
|
31643
|
+
units: external_exports.array(NotifyPlanUnitSchema2),
|
|
31644
|
+
/** Which unit is this arrival's ONE interruption (units-design.md D23). Absent means nobody
|
|
31645
|
+
* was interrupted — every unit was either settled or quiet enough to sit in the inbox. */
|
|
31646
|
+
speaks: external_exports.string().optional()
|
|
31647
|
+
});
|
|
31290
31648
|
var UserResponseSchema2 = external_exports.object({
|
|
31291
31649
|
requestId: external_exports.string(),
|
|
31292
31650
|
answer: UserAnswerSchema2,
|
|
@@ -31341,6 +31699,20 @@ var InboxItemSchema2 = external_exports.object({
|
|
|
31341
31699
|
/** The conversation thread + connection this item lives on. Present on the replied
|
|
31342
31700
|
* detail — they power History's "Continue" / "New session from this" (#57/#251). */
|
|
31343
31701
|
parentId: external_exports.string().optional(),
|
|
31702
|
+
/** THE ARRIVAL this row is one unit of (`notifications.ask_id` → `asks`). A claim is one
|
|
31703
|
+
* arrival and its units are N rows of it, so this — not `parentId` — is what makes a
|
|
31704
|
+
* multi-part notification one thing on screen. The thread is the whole CONVERSATION: it
|
|
31705
|
+
* accumulates every message an agent ever sent, so grouping by it renders a day of
|
|
31706
|
+
* unrelated updates as a single "12-part request". Absent on rows written before the
|
|
31707
|
+
* `asks` table, and on anything that never went through `notify` — both fall back to the
|
|
31708
|
+
* thread, which is what the client did for all rows until now. */
|
|
31709
|
+
askId: external_exports.string().optional(),
|
|
31710
|
+
/** WHERE this unit sat in the message it was cut from (`notifications.seq`). The batch
|
|
31711
|
+
* shares one `created_at` to the microsecond, so without it the author's order is
|
|
31712
|
+
* unrecoverable client-side — a four-paragraph briefing rendered opening-paragraph-last
|
|
31713
|
+
* (live 2026-08-10, D35). The API already orders by it; this lets a reader that
|
|
31714
|
+
* re-sorts (grouping, filtering) put an arrival back in the order it was written. */
|
|
31715
|
+
seq: external_exports.number().int().optional(),
|
|
31344
31716
|
tokenId: external_exports.string().optional(),
|
|
31345
31717
|
status: NotifyStatusSchema2,
|
|
31346
31718
|
context: ContextSchema2,
|
|
@@ -31351,6 +31723,16 @@ var InboxItemSchema2 = external_exports.object({
|
|
|
31351
31723
|
* ring/enqueue time. Replaces the condensed line + index-aligned phrased points, which
|
|
31352
31724
|
* between them could not express a call as a sequence. `question: null` is a real turn —
|
|
31353
31725
|
* a status update stays a statement instead of being shaped into a yes/no. */
|
|
31726
|
+
/** Does this claim want an ANSWER, or is it telling you something? Written per row from
|
|
31727
|
+
* `requestAsks` — the agent's own declaration, not a guess. `false` is what earns a card
|
|
31728
|
+
* its acknowledge affordance: without it a status update offers a text box and a dismiss,
|
|
31729
|
+
* and neither of those is "got it" (owner, 2026-08-10). */
|
|
31730
|
+
asks: external_exports.boolean().optional(),
|
|
31731
|
+
/** When a live process last pulsed for this row's agent — the liveness input for
|
|
31732
|
+
* "working requires a pulse" (#928): the list said "Working…" from agent_state alone
|
|
31733
|
+
* while the party called the same dead claim stalled. Absent = no token/no data,
|
|
31734
|
+
* which must never CLAIM stalled. */
|
|
31735
|
+
lastSeenAt: external_exports.string().optional(),
|
|
31354
31736
|
agenda: external_exports.array(AgendaTurnSchema2).optional(),
|
|
31355
31737
|
/** On a replied detail (#397): the next steps the user attached to the answer
|
|
31356
31738
|
* ("call back after lunch") — shown so they can see the commitment was captured. */
|
|
@@ -31375,6 +31757,23 @@ var InboxItemSchema2 = external_exports.object({
|
|
|
31375
31757
|
* client may still flag a stall by age. Drives the inbox error badge + Retry. */
|
|
31376
31758
|
error: external_exports.string().optional(),
|
|
31377
31759
|
clarifies: external_exports.string().optional(),
|
|
31760
|
+
/** The ring ladder ran out while this was still pending — we tried to reach you and
|
|
31761
|
+
* STOPPED trying (`arbitration/arbitrate.ts` `nextRing` → `stop`). Distinct from an
|
|
31762
|
+
* agent with nothing to say, which the roster drew identically until now: "nothing to
|
|
31763
|
+
* say" and "gave up saying it" are opposite situations wearing the same face
|
|
31764
|
+
* (navigation-design.md, gap 1). False for anything that never rang. */
|
|
31765
|
+
gaveUp: external_exports.boolean().default(false),
|
|
31766
|
+
/** Why this arrived the way it did, read back off the delivery receipt (`notify/why.ts`).
|
|
31767
|
+
* Absent for anything never delivered through a push, and for older rows written before
|
|
31768
|
+
* the reason was recorded. Deliberately a debug affordance, shown small (owner,
|
|
31769
|
+
* 2026-08-07) — its real job is to give "this didn't need a call" something to be
|
|
31770
|
+
* feedback ABOUT. */
|
|
31771
|
+
why: external_exports.object({
|
|
31772
|
+
asked: NotifyLevelSchema2,
|
|
31773
|
+
got: NotifyLevelSchema2,
|
|
31774
|
+
because: external_exports.enum(["unresponsive", "dismissed", "not_permitted", "silent", "coalesced", "agent_capped", "unplanned", "learned_raise"]).optional(),
|
|
31775
|
+
line: external_exports.string()
|
|
31776
|
+
}).optional(),
|
|
31378
31777
|
select: external_exports.enum(["one", "many", "rank", "confirm", "text"]).default("one"),
|
|
31379
31778
|
confirmStyle: external_exports.enum(["yesno", "approve"]).default("yesno").describe(
|
|
31380
31779
|
"Labels for a select:'confirm' paige \u2014 'yesno' (Yes/No) or 'approve' (Approve/Deny). Ignored unless select is 'confirm'."
|
|
@@ -31485,6 +31884,15 @@ var HistoryItemSchema2 = external_exports.object({
|
|
|
31485
31884
|
/** When you answered the agent's notification (agent→user only). */
|
|
31486
31885
|
humanAckedAt: external_exports.string().nullable()
|
|
31487
31886
|
});
|
|
31887
|
+
var ACTIVITY_LINES2 = 2;
|
|
31888
|
+
var ACTIVITY_LINE_MAX2 = 80;
|
|
31889
|
+
var AgentActivitySchema2 = external_exports.object({
|
|
31890
|
+
/** Oldest first, so the newest line is last — the one that replaces in place. */
|
|
31891
|
+
lines: external_exports.array(external_exports.string().max(ACTIVITY_LINE_MAX2)).max(ACTIVITY_LINES2),
|
|
31892
|
+
/** When the harness observed this tail. Its own timestamp, not the heartbeat's: a beat
|
|
31893
|
+
* that carries an UNCHANGED tail must not make a stalled agent look like it just moved. */
|
|
31894
|
+
at: external_exports.string().datetime()
|
|
31895
|
+
});
|
|
31488
31896
|
var ConnectionSummarySchema2 = external_exports.object({
|
|
31489
31897
|
/** The connection = the agent's token id (used to address a request). */
|
|
31490
31898
|
id: external_exports.string(),
|
|
@@ -31496,6 +31904,14 @@ var ConnectionSummarySchema2 = external_exports.object({
|
|
|
31496
31904
|
provider: external_exports.string().nullable(),
|
|
31497
31905
|
/** The pairing's assigned voice (#462); null = the default voice. */
|
|
31498
31906
|
voice: VoiceKeySchema2.nullable(),
|
|
31907
|
+
/** The LOUDEST this agent may ever reach you — a ceiling on `NOTIFY_LADDER`, set by the
|
|
31908
|
+
* user on the agent's own page. null = no ceiling (today's behaviour for every
|
|
31909
|
+
* connection). Android binds importance to a relationship rather than to each message,
|
|
31910
|
+
* and that is the thing our roster could not say: "Marlow may always call me; Otto
|
|
31911
|
+
* never may" (navigation-design.md, gap 2). Clamped in `arbitrateLevel`, so it binds
|
|
31912
|
+
* every surface at once and outranks even `sessionMode: all_calls` — a mode the user
|
|
31913
|
+
* set once must not overrule a rule they set about one agent. */
|
|
31914
|
+
reach: NotifyLevelSchema2.nullable().optional(),
|
|
31499
31915
|
createdAt: external_exports.string().datetime(),
|
|
31500
31916
|
/** Most recent notification on this connection, either direction. Null = no contact yet.
|
|
31501
31917
|
* Drives the agents-page recency grouping (Today / This week / …). */
|
|
@@ -31510,10 +31926,49 @@ var ConnectionSummarySchema2 = external_exports.object({
|
|
|
31510
31926
|
harnesses: external_exports.array(external_exports.object({ name: external_exports.string(), label: external_exports.string(), status: external_exports.string() })).optional(),
|
|
31511
31927
|
workspaces: external_exports.array(external_exports.string()).optional()
|
|
31512
31928
|
}).optional(),
|
|
31929
|
+
/** The tail of this agent's working log, when a harness is driving it — the agent page's
|
|
31930
|
+
* live strip. Absent for anything the desktop harness isn't running (a hatched identity
|
|
31931
|
+
* used straight from a terminal emits no work events; the page says so rather than
|
|
31932
|
+
* drawing an empty box). */
|
|
31933
|
+
activity: AgentActivitySchema2.optional(),
|
|
31513
31934
|
/** True = a provider-managed agent running in the provider's cloud (e.g. Anthropic CMA);
|
|
31514
31935
|
* false = a local MCP connection running on the user's computer (Claude Code/Codex/…). */
|
|
31515
31936
|
managed: external_exports.boolean()
|
|
31516
31937
|
});
|
|
31938
|
+
var MoveRingSchema2 = external_exports.enum(["home", "travels", "retired", "quarantined"]);
|
|
31939
|
+
var MoveSchema2 = external_exports.object({
|
|
31940
|
+
id: external_exports.string(),
|
|
31941
|
+
/** The reusable question, as distill normalized it. */
|
|
31942
|
+
question: external_exports.string(),
|
|
31943
|
+
/** The operative ruling. Editable by the user (PATCH) — which resets the ledger. */
|
|
31944
|
+
answer: external_exports.string(),
|
|
31945
|
+
/** The user's stated reason, when they gave one. Null = inherently narrow: the judge is
|
|
31946
|
+
* told so, and the ruling only derives essentially the same question in the same scope. */
|
|
31947
|
+
rationale: external_exports.string().nullable(),
|
|
31948
|
+
/** Where the ruling lives: a repo/workspace, or 'global'. */
|
|
31949
|
+
scope: external_exports.string(),
|
|
31950
|
+
ring: MoveRingSchema2,
|
|
31951
|
+
/** True = the user pinned it with `always` (travel granted by hand, not by evidence). */
|
|
31952
|
+
pinned: external_exports.boolean(),
|
|
31953
|
+
/** True = a pin the user placed was BROKEN by later counter-evidence. Surfaced so the
|
|
31954
|
+
* break is visible instead of a pin silently disappearing. */
|
|
31955
|
+
pinBroken: external_exports.boolean(),
|
|
31956
|
+
/** When the ruling was distilled. */
|
|
31957
|
+
learnedAt: external_exports.string(),
|
|
31958
|
+
/** Last time it answered an ask. Null = never fired. */
|
|
31959
|
+
lastUsedAt: external_exports.string().nullable(),
|
|
31960
|
+
/** How many asks it has answered. Instrumentation — deliberately NOT an input to the
|
|
31961
|
+
* evidence curve: firing says the question keeps arising, not that the ruling is right. */
|
|
31962
|
+
usedCount: external_exports.number(),
|
|
31963
|
+
/** Ledger: outcomes that said it held up. Saturating — the tenth is worth almost nothing. */
|
|
31964
|
+
confirms: external_exports.number(),
|
|
31965
|
+
/** Ledger: contradictions, in signal units (a full override = 1, weaker signals less).
|
|
31966
|
+
* Linear and priced above the entire confirmation budget, so any full counter wins. */
|
|
31967
|
+
counters: external_exports.number(),
|
|
31968
|
+
/** The agent that asked the question this move came from, when known. Null for a move
|
|
31969
|
+
* distilled from a clarify ruling (those carry no agent) or one whose source rows are gone. */
|
|
31970
|
+
learnedFrom: external_exports.object({ id: external_exports.string(), name: external_exports.string() }).nullable()
|
|
31971
|
+
});
|
|
31517
31972
|
var CreateRequestSchema2 = external_exports.object({
|
|
31518
31973
|
/** The connection (token id) to send to, from GET /api/tokens. */
|
|
31519
31974
|
tokenId: external_exports.string(),
|
|
@@ -31720,6 +32175,17 @@ var DeviceTokenSchema2 = external_exports.object({
|
|
|
31720
32175
|
* a default silly name). */
|
|
31721
32176
|
name: external_exports.string(),
|
|
31722
32177
|
device: external_exports.string().nullable(),
|
|
32178
|
+
/** The pairing's assigned voice, cached so the desktop can seed the SAME face the phone
|
|
32179
|
+
* draws — voice is the third ingredient of a hatchling's build (party/traits.ts). */
|
|
32180
|
+
voice: external_exports.string().nullable().optional(),
|
|
32181
|
+
/** The token's server-side id — the face's COLOUR anchor, and the only seed ingredient
|
|
32182
|
+
* that survives a rename. Cached by the host's identity beat. */
|
|
32183
|
+
token_id: external_exports.string().nullable().optional(),
|
|
32184
|
+
/** WHERE this identity works — the folder a wake should land it in. Written by the host
|
|
32185
|
+
* at spawn and by `paigy-harness handoff` from a live terminal. Without it every wake
|
|
32186
|
+
* landed in the FIRST granted workspace and the agent rediscovered its own repo from
|
|
32187
|
+
* the thread each time (host.ts, live catch 2026-08-06 — prompt-papered until now). */
|
|
32188
|
+
workspace: external_exports.string().nullable().optional(),
|
|
31723
32189
|
phone_reveal: PairingRevealSchema2.nullable().optional(),
|
|
31724
32190
|
// present once the phone reveals
|
|
31725
32191
|
uik_pub: external_exports.string().nullable().optional()
|
|
@@ -31746,6 +32212,8 @@ var NotificationFeedbackKindSchema2 = external_exports.enum([
|
|
|
31746
32212
|
// "There should be a picture or design here."
|
|
31747
32213
|
"should_have_called",
|
|
31748
32214
|
// "Don't put this in a banner — ring me for something like this."
|
|
32215
|
+
"should_have_messaged",
|
|
32216
|
+
// the inverse: "that didn't deserve a ring — a message would do."
|
|
31749
32217
|
"other"
|
|
31750
32218
|
// anything else — the note carries it.
|
|
31751
32219
|
]);
|
|
@@ -31860,6 +32328,9 @@ function levelFor(event) {
|
|
|
31860
32328
|
case "idle":
|
|
31861
32329
|
if (endsWithQuestion(event.result)) return "banner";
|
|
31862
32330
|
return event.failed ? "push" : "inbox";
|
|
32331
|
+
case "work":
|
|
32332
|
+
return "inbox";
|
|
32333
|
+
// moot — entryFor drops work before a level ever matters
|
|
31863
32334
|
case "error":
|
|
31864
32335
|
return "inbox";
|
|
31865
32336
|
}
|
|
@@ -31880,16 +32351,14 @@ function entryFor(event, state = {}) {
|
|
|
31880
32351
|
switch (event.kind) {
|
|
31881
32352
|
case "turn": {
|
|
31882
32353
|
const who = event.role === "agent" ? "Agent" : "You";
|
|
31883
|
-
const
|
|
31884
|
-
const description = [event.text, event.tools?.length ? `Tools: ${event.tools.join(", ")}` : null].filter((c) => Boolean(c));
|
|
32354
|
+
const body = event.text ? event.role === "agent" ? event.text : `${who}: ${event.text}` : `${who} ran ${(event.tools ?? []).join(", ")}.`;
|
|
31885
32355
|
return {
|
|
31886
32356
|
...common,
|
|
31887
|
-
|
|
31888
|
-
//
|
|
31889
|
-
//
|
|
31890
|
-
//
|
|
31891
|
-
//
|
|
31892
|
-
select: "text"
|
|
32357
|
+
ask: body
|
|
32358
|
+
// No `select`: the contract refuses it on the `ask` form ("the broker derives it"),
|
|
32359
|
+
// and with no options it derives "text" anyway — the shape this wants, since any
|
|
32360
|
+
// inbox row can be replied to and a reply to history is just the user initiating
|
|
32361
|
+
// (the pump feeds it back in).
|
|
31893
32362
|
};
|
|
31894
32363
|
}
|
|
31895
32364
|
case "permission":
|
|
@@ -31911,16 +32380,24 @@ function entryFor(event, state = {}) {
|
|
|
31911
32380
|
};
|
|
31912
32381
|
case "idle": {
|
|
31913
32382
|
const asking = endsWithQuestion(event.result);
|
|
32383
|
+
const result = event.result?.trim();
|
|
32384
|
+
const norm = (t) => (t ?? "").replace(/\s+/g, " ").trim();
|
|
32385
|
+
if (result && !event.failed && norm(result) === norm(state.lastAgentText)) {
|
|
32386
|
+
if (!asking) return null;
|
|
32387
|
+
const spans = unitsOf2(result);
|
|
32388
|
+
const tail = spans.map((sp) => result.slice(sp.start, sp.end)).reverse().find((t) => t.includes("?"));
|
|
32389
|
+
return { ...common, ask: tail ?? result, blocking: true };
|
|
32390
|
+
}
|
|
32391
|
+
const lead = asking ? "" : event.failed ? "The agent stopped without finishing. " : "Turn complete. ";
|
|
31914
32392
|
return {
|
|
31915
32393
|
...common,
|
|
31916
|
-
|
|
31917
|
-
title: asking ? clip(firstLine(event.result ?? "")) : event.failed ? "The agent stopped without finishing" : "Turn complete",
|
|
31918
|
-
description: [event.result || (event.failed ? "No result reported." : "Done.")]
|
|
31919
|
-
},
|
|
31920
|
-
select: "text",
|
|
32394
|
+
ask: `${lead}${result || (event.failed ? "No result reported." : "Done.")}`,
|
|
31921
32395
|
...asking ? { blocking: true } : {}
|
|
31922
32396
|
};
|
|
31923
32397
|
}
|
|
32398
|
+
case "work":
|
|
32399
|
+
return null;
|
|
32400
|
+
// log-only by contract — the live texture of the working log
|
|
31924
32401
|
case "error":
|
|
31925
32402
|
return null;
|
|
31926
32403
|
}
|
|
@@ -31942,9 +32419,16 @@ function decisionFrom(answer) {
|
|
|
31942
32419
|
}
|
|
31943
32420
|
async function mirror(event, state, deps = {}) {
|
|
31944
32421
|
const entry = entryFor(event, state);
|
|
32422
|
+
if (event.kind === "turn" && event.role === "agent" && event.text) state.lastAgentText = event.text;
|
|
31945
32423
|
if (!entry) return {};
|
|
32424
|
+
let req;
|
|
32425
|
+
try {
|
|
32426
|
+
req = NotifyRequestSchema2.parse(entry);
|
|
32427
|
+
} catch (e) {
|
|
32428
|
+
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)}`);
|
|
32429
|
+
return {};
|
|
32430
|
+
}
|
|
31946
32431
|
try {
|
|
31947
|
-
const req = NotifyRequestSchema2.parse(entry);
|
|
31948
32432
|
const { notificationId, parentId } = await (deps.submit ?? submitNotification)(req);
|
|
31949
32433
|
(state.mine ??= /* @__PURE__ */ new Set()).add(notificationId);
|
|
31950
32434
|
return { parentId, notificationId };
|
|
@@ -31973,8 +32457,9 @@ async function askQuestion(question, state, deps = {}) {
|
|
|
31973
32457
|
...state.parentId ? { parentId: state.parentId } : {},
|
|
31974
32458
|
...state.repo ? { repo: state.repo } : {},
|
|
31975
32459
|
...state.branch ? { branch: state.branch } : {},
|
|
31976
|
-
|
|
31977
|
-
|
|
32460
|
+
// Prose in — see `entryFor`'s `turn` case. An agent's question is the case most likely
|
|
32461
|
+
// to run long, and its title was a clipped prefix of itself.
|
|
32462
|
+
ask: question,
|
|
31978
32463
|
blocking: true,
|
|
31979
32464
|
urgency: "banner"
|
|
31980
32465
|
};
|
|
@@ -31988,6 +32473,9 @@ async function askQuestion(question, state, deps = {}) {
|
|
|
31988
32473
|
}
|
|
31989
32474
|
async function nudgeSetup(label, hint, deps = {}) {
|
|
31990
32475
|
const entry = {
|
|
32476
|
+
// Keeps `context`: the title here SUMMARISES rather than repeating — "<label> needs
|
|
32477
|
+
// setup" is not a prefix of the hint — which is exactly the case a hand-written context
|
|
32478
|
+
// is for. `ask` would derive a title from the hint's first sentence and lose the label.
|
|
31991
32479
|
context: { title: clip(`${label} needs setup`), description: [hint] },
|
|
31992
32480
|
select: "text",
|
|
31993
32481
|
urgency: "push"
|
|
@@ -32020,7 +32508,6 @@ function spokenText(answer) {
|
|
|
32020
32508
|
return null;
|
|
32021
32509
|
}
|
|
32022
32510
|
}
|
|
32023
|
-
var firstLine = (s) => s.split("\n")[0] ?? s;
|
|
32024
32511
|
var clip = (s) => (s.length > 120 ? `${s.slice(0, 117)}\u2026` : s) || "(no text)";
|
|
32025
32512
|
|
|
32026
32513
|
// src/harness/session.ts
|
|
@@ -32055,6 +32542,14 @@ var AcpDriver = class {
|
|
|
32055
32542
|
queued = [];
|
|
32056
32543
|
/** Options of each unanswered permission request, keyed by its JSON-RPC id. */
|
|
32057
32544
|
pending = /* @__PURE__ */ new Map();
|
|
32545
|
+
/** Where the REPORT starts in `text` — everything before the LAST tool call is working
|
|
32546
|
+
* narration ("Now the API endpoints." → runs a tool), and it used to ship: the chunks
|
|
32547
|
+
* concatenate with no separator, so the owner's phone got "…find the repo.Now I have
|
|
32548
|
+
* the full picture. Writing the migration.Now…" as the opening paragraph of a finished
|
|
32549
|
+
* task (live, 2026-08-11 — "looks like a working log"). The narration's audience is the
|
|
32550
|
+
* terminal and host.log; what the agent composed AFTER its last tool call is the part
|
|
32551
|
+
* addressed to a human, and that is what leaves the machine. */
|
|
32552
|
+
reportFrom = 0;
|
|
32058
32553
|
/** The turn being streamed: text accumulates, tools append, both flush on stopReason. */
|
|
32059
32554
|
text = "";
|
|
32060
32555
|
tools = [];
|
|
@@ -32150,18 +32645,20 @@ var AcpDriver = class {
|
|
|
32150
32645
|
if (msg.id === this.promptId) {
|
|
32151
32646
|
this.promptId = void 0;
|
|
32152
32647
|
const events = [];
|
|
32153
|
-
|
|
32648
|
+
const report = this.text.slice(this.reportFrom).trim() || this.text.trim();
|
|
32649
|
+
if (report || this.tools.length) {
|
|
32154
32650
|
events.push({
|
|
32155
32651
|
kind: "turn",
|
|
32156
32652
|
role: "agent",
|
|
32157
|
-
text:
|
|
32653
|
+
text: report,
|
|
32158
32654
|
...this.tools.length ? { tools: [...this.tools] } : {}
|
|
32159
32655
|
});
|
|
32160
32656
|
}
|
|
32161
|
-
const result =
|
|
32657
|
+
const result = report;
|
|
32162
32658
|
const failed = msg.error !== void 0 || msg.result?.stopReason === "refusal";
|
|
32163
32659
|
this.text = "";
|
|
32164
32660
|
this.tools = [];
|
|
32661
|
+
this.reportFrom = 0;
|
|
32165
32662
|
const writes = this.flush();
|
|
32166
32663
|
if (!writes.length) {
|
|
32167
32664
|
events.push({
|
|
@@ -32185,7 +32682,9 @@ var AcpDriver = class {
|
|
|
32185
32682
|
case "tool_call": {
|
|
32186
32683
|
const title = update.title?.trim() || update.kind || "tool";
|
|
32187
32684
|
this.tools.push(title);
|
|
32188
|
-
|
|
32685
|
+
const note = this.text.slice(this.reportFrom).trim();
|
|
32686
|
+
this.reportFrom = this.text.length;
|
|
32687
|
+
return { events: [{ kind: "work", tool: title, ...note ? { note } : {} }], writes: [] };
|
|
32189
32688
|
}
|
|
32190
32689
|
default:
|
|
32191
32690
|
return none;
|
|
@@ -32239,7 +32738,8 @@ var frame = (body) => JSON.stringify({ jsonrpc: "2.0", ...body });
|
|
|
32239
32738
|
// src/harness/session.ts
|
|
32240
32739
|
var ADAPTER_BIN = {
|
|
32241
32740
|
claude: "claude-agent-acp",
|
|
32242
|
-
codex: "codex-acp"
|
|
32741
|
+
codex: "codex-acp",
|
|
32742
|
+
agy: "agy"
|
|
32243
32743
|
};
|
|
32244
32744
|
function splitLines(buffer, chunk) {
|
|
32245
32745
|
const combined = buffer + chunk;
|
|
@@ -32307,9 +32807,30 @@ function startSession(opts) {
|
|
|
32307
32807
|
};
|
|
32308
32808
|
}
|
|
32309
32809
|
|
|
32810
|
+
// src/paigy/activity.ts
|
|
32811
|
+
function shortenPaths(s) {
|
|
32812
|
+
return s.replace(/(?<![\w.@+-])(?:\/[\w.@+-]+){3,}/g, (p) => {
|
|
32813
|
+
const parts = p.split("/").filter(Boolean);
|
|
32814
|
+
return `\u2026/${parts.slice(-2).join("/")}`;
|
|
32815
|
+
});
|
|
32816
|
+
}
|
|
32817
|
+
function workLine(event) {
|
|
32818
|
+
const note = (event.note ?? "").split("\n").find((l) => l.trim()) ?? "";
|
|
32819
|
+
const line = shortenPaths([event.tool.trim(), note.trim()].filter(Boolean).join(" \u2014 ").replace(/\s+/g, " "));
|
|
32820
|
+
return line.length > ACTIVITY_LINE_MAX2 ? `${line.slice(0, ACTIVITY_LINE_MAX2 - 1)}\u2026` : line;
|
|
32821
|
+
}
|
|
32822
|
+
function pushWork(lines, line) {
|
|
32823
|
+
if (!line || lines[lines.length - 1] === line) return [...lines];
|
|
32824
|
+
return [...lines, line].slice(-ACTIVITY_LINES2);
|
|
32825
|
+
}
|
|
32826
|
+
function sameTail(a, b) {
|
|
32827
|
+
return a.length === b.length && a.every((l, i) => l === b[i]);
|
|
32828
|
+
}
|
|
32829
|
+
|
|
32310
32830
|
// src/run.ts
|
|
32311
32831
|
function runHarness(opts) {
|
|
32312
32832
|
let running = true;
|
|
32833
|
+
let tail = [];
|
|
32313
32834
|
const state = { ...opts.exclusive ? { exclusive: true } : {} };
|
|
32314
32835
|
let session = null;
|
|
32315
32836
|
const asMe = { token: opts.token };
|
|
@@ -32337,11 +32858,17 @@ function runHarness(opts) {
|
|
|
32337
32858
|
opts.log(decision.allow ? `\u2713 approved: ${event.summary}` : `\u2717 denied: ${event.summary}`);
|
|
32338
32859
|
return;
|
|
32339
32860
|
}
|
|
32861
|
+
if (event.kind === "work") {
|
|
32862
|
+
opts.log(`\u2699 ${event.tool}${event.note ? ` \u2014 ${event.note.split("\n")[0] ?? ""}` : ""}`);
|
|
32863
|
+
tail = pushWork(tail, workLine(event));
|
|
32864
|
+
return;
|
|
32865
|
+
}
|
|
32340
32866
|
const { parentId } = await mirror(event, state, deps);
|
|
32341
32867
|
state.parentId ??= parentId;
|
|
32342
32868
|
if (event.kind === "turn") opts.log(`${event.role}: ${event.text.split("\n")[0] ?? ""}`);
|
|
32343
32869
|
if (event.kind === "idle") {
|
|
32344
32870
|
state.resting = true;
|
|
32871
|
+
tail = [];
|
|
32345
32872
|
if (endsWithQuestion(event.result)) {
|
|
32346
32873
|
const local = await opts.localAsk?.question?.(event.result ?? "") ?? null;
|
|
32347
32874
|
if (local?.trim() && session && running) {
|
|
@@ -32379,8 +32906,10 @@ function runHarness(opts) {
|
|
|
32379
32906
|
session?.send(text);
|
|
32380
32907
|
},
|
|
32381
32908
|
working: () => running && state.resting !== true,
|
|
32909
|
+
tail: () => [...tail],
|
|
32382
32910
|
stop() {
|
|
32383
32911
|
running = false;
|
|
32912
|
+
tail = [];
|
|
32384
32913
|
cancelAsks(state);
|
|
32385
32914
|
session?.stop();
|
|
32386
32915
|
session = null;
|
|
@@ -32419,22 +32948,39 @@ function addWorkspace(dir, deps) {
|
|
|
32419
32948
|
}
|
|
32420
32949
|
return all;
|
|
32421
32950
|
}
|
|
32951
|
+
function allowed(cwd, deps) {
|
|
32952
|
+
const target = resolve2(cwd.replace(/^~(?=$|\/)/, homedir4()));
|
|
32953
|
+
return listWorkspaces(deps).some((w) => target === w || target.startsWith(`${w}/`));
|
|
32954
|
+
}
|
|
32955
|
+
function resolveWakeDir(pinned, deps) {
|
|
32956
|
+
if (pinned && allowed(pinned, deps)) {
|
|
32957
|
+
const dir = resolve2(pinned.replace(/^~(?=$|\/)/, homedir4()));
|
|
32958
|
+
if ((deps.exists ?? existsSync4)(dir)) return dir;
|
|
32959
|
+
}
|
|
32960
|
+
return listWorkspaces(deps)[0];
|
|
32961
|
+
}
|
|
32422
32962
|
|
|
32423
32963
|
// src/host.ts
|
|
32424
32964
|
var HOST_FILE = join4(homedir5(), ".paigy", "host.json");
|
|
32425
32965
|
function startHost(opts) {
|
|
32426
32966
|
const runs = /* @__PURE__ */ new Map();
|
|
32427
32967
|
const asHost = { token: opts.token };
|
|
32428
|
-
const
|
|
32968
|
+
const refreshIdentities = async () => {
|
|
32429
32969
|
for (const slot of listSlots()) {
|
|
32430
32970
|
const token = readToken(slot);
|
|
32431
32971
|
if (!token) continue;
|
|
32432
32972
|
const me = await whoAmI({ token }).catch(() => null);
|
|
32433
|
-
if (me
|
|
32973
|
+
if (!me) continue;
|
|
32974
|
+
const have = slotIdentity(slot);
|
|
32975
|
+
if (me.name !== have.name || (me.voice ?? null) !== have.voice || (me.tokenId ?? null) !== have.tokenId) {
|
|
32976
|
+
updateSlot(slot, { ...me.name ? { name: me.name } : {}, voice: me.voice ?? null, token_id: me.tokenId ?? null });
|
|
32977
|
+
}
|
|
32434
32978
|
}
|
|
32435
32979
|
};
|
|
32436
32980
|
const beat = () => {
|
|
32437
|
-
void
|
|
32981
|
+
void refreshIdentities();
|
|
32982
|
+
for (const r of runs.values()) if (r.token) void heartbeat(void 0, { token: r.token }).catch(() => {
|
|
32983
|
+
});
|
|
32438
32984
|
void heartbeat({
|
|
32439
32985
|
harnesses: detectAll().map((a) => ({ name: a.name, label: a.label, status: a.status })),
|
|
32440
32986
|
workspaces: listWorkspaces(opts.wsDeps)
|
|
@@ -32459,25 +33005,28 @@ function startHost(opts) {
|
|
|
32459
33005
|
},
|
|
32460
33006
|
log
|
|
32461
33007
|
});
|
|
32462
|
-
runs.set(spec.sessionId, { run, label });
|
|
33008
|
+
runs.set(spec.sessionId, { run, label, token: spec.token });
|
|
33009
|
+
void heartbeat(void 0, { token: spec.token }).catch(() => {
|
|
33010
|
+
});
|
|
33011
|
+
void refreshIdentities();
|
|
32463
33012
|
try {
|
|
32464
|
-
saveToken({ access_token: spec.token, name: label, device: null },
|
|
33013
|
+
saveToken({ access_token: spec.token, name: label, device: null, workspace: spec.workspace }, sessionSlot(spec.sessionId));
|
|
32465
33014
|
} catch {
|
|
32466
33015
|
}
|
|
32467
33016
|
opts.log(`\u25B6 phone-launched ${label} (${spec.harness}) in ${spec.workspace}`);
|
|
32468
33017
|
}
|
|
32469
33018
|
}
|
|
32470
|
-
const SLOT_HARNESS = { "mcp-agent": "claude", codex: "codex" };
|
|
32471
|
-
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.";
|
|
33019
|
+
const SLOT_HARNESS = { "mcp-agent": "claude", codex: "codex", antigravity: "agy" };
|
|
33020
|
+
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.";
|
|
32472
33021
|
async function sweepSlots() {
|
|
32473
|
-
const workspace = listWorkspaces(opts.wsDeps)[0];
|
|
32474
|
-
if (!workspace) return;
|
|
32475
33022
|
for (const slot of listSlots()) {
|
|
32476
33023
|
const harness = SLOT_HARNESS[slot] ?? (slot === "Desktop" ? void 0 : "claude");
|
|
32477
33024
|
const key = `slot:${slot}`;
|
|
32478
33025
|
if (!harness || runs.has(key)) continue;
|
|
32479
33026
|
const token = readToken(slot);
|
|
32480
33027
|
if (!token) continue;
|
|
33028
|
+
const workspace = resolveWakeDir(slotIdentity(slot).workspace, opts.wsDeps);
|
|
33029
|
+
if (!workspace) continue;
|
|
32481
33030
|
const work = await checkReplies({ token }).catch(() => null);
|
|
32482
33031
|
if (!work || work.requests.length === 0 && work.replies.length === 0) continue;
|
|
32483
33032
|
const label = slotName(slot) ?? slot;
|
|
@@ -32496,10 +33045,33 @@ function startHost(opts) {
|
|
|
32496
33045
|
},
|
|
32497
33046
|
log
|
|
32498
33047
|
});
|
|
32499
|
-
runs.set(key, { run, label });
|
|
33048
|
+
runs.set(key, { run, label, token });
|
|
33049
|
+
void heartbeat(void 0, { token }).catch(() => {
|
|
33050
|
+
});
|
|
32500
33051
|
opts.log(`\u25B6 woke ${label} (${harness}) \u2014 work was waiting in ${workspace}`);
|
|
32501
33052
|
}
|
|
32502
33053
|
}
|
|
33054
|
+
const ACTIVITY_MS = 2e3;
|
|
33055
|
+
const published = /* @__PURE__ */ new Map();
|
|
33056
|
+
const streamActivity = () => {
|
|
33057
|
+
const publishTail = (token, lines) => {
|
|
33058
|
+
void heartbeat(void 0, { token, activity: { lines, at: (/* @__PURE__ */ new Date()).toISOString() } }).catch(() => {
|
|
33059
|
+
});
|
|
33060
|
+
};
|
|
33061
|
+
for (const [key, r] of runs) {
|
|
33062
|
+
if (!r.token) continue;
|
|
33063
|
+
const lines = r.run.tail();
|
|
33064
|
+
const was = published.get(key);
|
|
33065
|
+
if (was && sameTail(was.lines, lines)) continue;
|
|
33066
|
+
published.set(key, { token: r.token, lines });
|
|
33067
|
+
publishTail(r.token, lines);
|
|
33068
|
+
}
|
|
33069
|
+
for (const [key, was] of published) {
|
|
33070
|
+
if (runs.has(key)) continue;
|
|
33071
|
+
published.delete(key);
|
|
33072
|
+
if (was.lines.length > 0) publishTail(was.token, []);
|
|
33073
|
+
}
|
|
33074
|
+
};
|
|
32503
33075
|
const publish = () => {
|
|
32504
33076
|
try {
|
|
32505
33077
|
writeFileSync3(HOST_FILE, JSON.stringify({ pid: process.pid, at: Date.now(), roster: api.roster() }));
|
|
@@ -32513,6 +33085,7 @@ function startHost(opts) {
|
|
|
32513
33085
|
void sweepSlots();
|
|
32514
33086
|
publish();
|
|
32515
33087
|
}, 5e3);
|
|
33088
|
+
const activityTick = setInterval(streamActivity, ACTIVITY_MS);
|
|
32516
33089
|
const stopSessions = () => {
|
|
32517
33090
|
for (const { run, label } of runs.values()) {
|
|
32518
33091
|
run.stop();
|
|
@@ -32529,18 +33102,29 @@ function startHost(opts) {
|
|
|
32529
33102
|
const live = new Map([...runs.values()].map((r) => [r.label, r.run]));
|
|
32530
33103
|
const names = /* @__PURE__ */ new Map();
|
|
32531
33104
|
for (const key of listSlots()) if (key !== "Desktop") names.set(slotName(key) ?? key, key);
|
|
32532
|
-
for (const label of live.keys()) if (!names.has(label)) names.set(label,
|
|
32533
|
-
return [...names.
|
|
32534
|
-
|
|
32535
|
-
|
|
32536
|
-
|
|
32537
|
-
|
|
33105
|
+
for (const label of live.keys()) if (!names.has(label)) names.set(label, null);
|
|
33106
|
+
return [...names.entries()].map(([name, slot]) => {
|
|
33107
|
+
const id = slot ? slotIdentity(slot) : { voice: null, tokenId: null };
|
|
33108
|
+
return {
|
|
33109
|
+
name,
|
|
33110
|
+
// The slot key rides along so the window's agent page can speak AS this agent —
|
|
33111
|
+
// its pending reads (`checkReplies`, the same pure read the sweep does) need the
|
|
33112
|
+
// slot's own token, and the display name is not a key.
|
|
33113
|
+
slot,
|
|
33114
|
+
tokenId: id.tokenId,
|
|
33115
|
+
voice: id.voice,
|
|
33116
|
+
running: live.has(name),
|
|
33117
|
+
working: live.get(name)?.working() ?? false
|
|
33118
|
+
};
|
|
33119
|
+
});
|
|
32538
33120
|
},
|
|
32539
33121
|
stopSessions,
|
|
32540
33122
|
stop() {
|
|
32541
33123
|
clearInterval(pulse);
|
|
32542
33124
|
clearInterval(spawnPoll);
|
|
32543
33125
|
stopSessions();
|
|
33126
|
+
streamActivity();
|
|
33127
|
+
clearInterval(activityTick);
|
|
32544
33128
|
try {
|
|
32545
33129
|
writeFileSync3(HOST_FILE, JSON.stringify({ pid: process.pid, at: 0, roster: [] }));
|
|
32546
33130
|
} catch {
|
|
@@ -32552,72 +33136,38 @@ function startHost(opts) {
|
|
|
32552
33136
|
}
|
|
32553
33137
|
|
|
32554
33138
|
// src/cli.ts
|
|
32555
|
-
function parseArgs(argv) {
|
|
32556
|
-
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: "" };
|
|
32557
|
-
const words = [];
|
|
32558
|
-
for (let i = 0; i < argv.length; i++) {
|
|
32559
|
-
const arg = argv[i];
|
|
32560
|
-
const next = () => argv[++i];
|
|
32561
|
-
if (arg === "--harness") {
|
|
32562
|
-
const v = next();
|
|
32563
|
-
if (v !== "claude" && v !== "codex") return { error: `--harness must be claude or codex, got ${v ?? "nothing"}` };
|
|
32564
|
-
args.harness = v;
|
|
32565
|
-
} else if (arg === "--mode") {
|
|
32566
|
-
const v = next();
|
|
32567
|
-
if (v !== "bypass" && v !== "ask") return { error: `--mode must be bypass or ask, got ${v ?? "nothing"}` };
|
|
32568
|
-
args.mode = v;
|
|
32569
|
-
} else if (arg === "--cwd") {
|
|
32570
|
-
const v = next();
|
|
32571
|
-
if (!v) return { error: "--cwd needs a directory" };
|
|
32572
|
-
args.cwd = v;
|
|
32573
|
-
} else if (arg === "--doctor") {
|
|
32574
|
-
args.doctor = true;
|
|
32575
|
-
} else if (arg === "--grace") {
|
|
32576
|
-
const v = Number(next());
|
|
32577
|
-
if (!Number.isFinite(v) || v < 0) return { error: "--grace needs seconds (0 = straight to phone)" };
|
|
32578
|
-
args.graceSeconds = v;
|
|
32579
|
-
} else if (arg === "host") {
|
|
32580
|
-
args.host = true;
|
|
32581
|
-
} else if (arg === "pair") {
|
|
32582
|
-
args.pair = true;
|
|
32583
|
-
} else if (arg === "setup") {
|
|
32584
|
-
args.setup = true;
|
|
32585
|
-
} else if (arg === "service") {
|
|
32586
|
-
args.service = true;
|
|
32587
|
-
} else if (arg === "--grant") {
|
|
32588
|
-
const v = next();
|
|
32589
|
-
if (!v) return { error: "--grant needs a folder to allow" };
|
|
32590
|
-
args.grant.push(v);
|
|
32591
|
-
} else if (arg === "hatch") {
|
|
32592
|
-
const v = next();
|
|
32593
|
-
if (!v) return { error: 'hatch needs a name: paigy-harness hatch "Voice bug hunter"' };
|
|
32594
|
-
args.hatch = v;
|
|
32595
|
-
} else if (arg === "--voice") {
|
|
32596
|
-
args.voice = next() ?? null;
|
|
32597
|
-
} else if (arg === "--slot") {
|
|
32598
|
-
args.slot = next() ?? null;
|
|
32599
|
-
} else if (arg === "--identity") {
|
|
32600
|
-
const v = next();
|
|
32601
|
-
if (!v) return { error: "--identity needs a hatched agent's name" };
|
|
32602
|
-
args.identity = v;
|
|
32603
|
-
} else if (arg?.startsWith("--")) {
|
|
32604
|
-
return { error: `unknown flag ${arg}` };
|
|
32605
|
-
} else if (arg) {
|
|
32606
|
-
words.push(arg);
|
|
32607
|
-
}
|
|
32608
|
-
}
|
|
32609
|
-
args.prompt = words.join(" ");
|
|
32610
|
-
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)" };
|
|
32611
|
-
return args;
|
|
32612
|
-
}
|
|
32613
33139
|
var STATUS_MARK = { ready: "\u2713", login: "\u25D0", "adapter-missing": "\u25D0", missing: "\u2717" };
|
|
32614
33140
|
async function main() {
|
|
32615
33141
|
const parsed = parseArgs(process.argv.slice(2));
|
|
32616
33142
|
if ("error" in parsed) {
|
|
32617
33143
|
console.error(`paigy-harness: ${parsed.error}`);
|
|
32618
|
-
console.error(
|
|
33144
|
+
console.error(USAGE);
|
|
32619
33145
|
process.exit(2);
|
|
32620
33146
|
}
|
|
33147
|
+
if (parsed.help) {
|
|
33148
|
+
console.log(USAGE);
|
|
33149
|
+
console.log("");
|
|
33150
|
+
console.log(" setup pair this machine, set up every agent found here, install the host service");
|
|
33151
|
+
console.log(" pair pair this machine only (QR / code)");
|
|
33152
|
+
console.log(" host host sessions your phone launches (--grant DIR to allow a folder)");
|
|
33153
|
+
console.log(" service install the host as a login service (macOS)");
|
|
33154
|
+
console.log(" enable-tools allowlist Paigy's tools in Claude Code so they don't prompt each time");
|
|
33155
|
+
console.log(" hatch NAME mint a sibling identity from this machine's credential");
|
|
33156
|
+
console.log(" handoff pin this terminal's identity here so your phone can resume it");
|
|
33157
|
+
console.log(" --doctor report which agent CLIs are installed and ready");
|
|
33158
|
+
console.log("");
|
|
33159
|
+
console.log('Anything else is a PROMPT: paigy-harness --harness codex "fix the tests"');
|
|
33160
|
+
return;
|
|
33161
|
+
}
|
|
33162
|
+
if (parsed.enableTools) {
|
|
33163
|
+
try {
|
|
33164
|
+
execSync(`${ENABLE_TOOLS_COMMAND} --scope ${parsed.scope}`, { stdio: "inherit", shell: "/bin/sh" });
|
|
33165
|
+
} catch {
|
|
33166
|
+
console.error(`Couldn't run it \u2014 try directly: ${ENABLE_TOOLS_COMMAND} --scope ${parsed.scope}`);
|
|
33167
|
+
process.exit(1);
|
|
33168
|
+
}
|
|
33169
|
+
return;
|
|
33170
|
+
}
|
|
32621
33171
|
if (parsed.doctor) {
|
|
32622
33172
|
for (const a of detectAll()) {
|
|
32623
33173
|
console.log(`${STATUS_MARK[a.status]} ${a.label}${a.hint ? ` \u2014 ${a.hint}` : ""}${a.install ? `
|
|
@@ -32676,9 +33226,12 @@ async function main() {
|
|
|
32676
33226
|
const TERMINAL_AGENTS = [
|
|
32677
33227
|
// Wire = MCP registration + the plugin (skills, hooks, idle-escalation — the
|
|
32678
33228
|
// piece that makes a LIVE session notice your requests without polling).
|
|
32679
|
-
|
|
32680
|
-
|
|
32681
|
-
|
|
33229
|
+
// The slot goes in the MCP config as PAIGY_AGENT, always. A registration without it
|
|
33230
|
+
// used to fall through to a shared default slot, so every unconfigured session on the
|
|
33231
|
+
// machine spoke as whoever paired into it last (strict identity, 2026-08-07).
|
|
33232
|
+
{ 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" },
|
|
33233
|
+
{ cli: "codex", name: "Codex", slot: "codex", wire: "codex mcp add paigy --env PAIGY_AGENT=codex -- npx -y @paigy/mcp@latest" },
|
|
33234
|
+
{ cli: "agy", name: "Antigravity", slot: "antigravity" }
|
|
32682
33235
|
];
|
|
32683
33236
|
overrideToken(readToken("Desktop") || null);
|
|
32684
33237
|
for (const t of TERMINAL_AGENTS) {
|
|
@@ -32697,6 +33250,14 @@ async function main() {
|
|
|
32697
33250
|
} catch {
|
|
32698
33251
|
}
|
|
32699
33252
|
}
|
|
33253
|
+
if (which("claude")) {
|
|
33254
|
+
try {
|
|
33255
|
+
execSync(ENABLE_TOOLS_COMMAND, { stdio: "ignore", shell: "/bin/sh" });
|
|
33256
|
+
console.log("\u2713 Paigy's tools allowlisted in Claude Code (they won't prompt each time)");
|
|
33257
|
+
} catch {
|
|
33258
|
+
console.log(` allowlist it later with: paigy-harness enable-tools`);
|
|
33259
|
+
}
|
|
33260
|
+
}
|
|
32700
33261
|
try {
|
|
32701
33262
|
const settings = join5(homedir6(), ".claude", "settings.json");
|
|
32702
33263
|
if (which("claude") && existsSync5(settings)) {
|
|
@@ -32753,6 +33314,9 @@ async function main() {
|
|
|
32753
33314
|
</array>
|
|
32754
33315
|
<key>RunAtLoad</key><true/>
|
|
32755
33316
|
<key>KeepAlive</key><true/>
|
|
33317
|
+
<key>SoftResourceLimits</key><dict>
|
|
33318
|
+
<key>NumberOfFiles</key><integer>65536</integer>
|
|
33319
|
+
</dict>
|
|
32756
33320
|
<key>StandardOutPath</key><string>${join5(homedir6(), ".paigy", "host.log")}</string>
|
|
32757
33321
|
<key>StandardErrorPath</key><string>${join5(homedir6(), ".paigy", "host.log")}</string>
|
|
32758
33322
|
</dict></plist>
|
|
@@ -32763,12 +33327,33 @@ async function main() {
|
|
|
32763
33327
|
writeFileSync4(path, plist);
|
|
32764
33328
|
execSync(`launchctl unload ${path} 2>/dev/null; launchctl load ${path}`, { shell: "/bin/sh" });
|
|
32765
33329
|
console.log(`\u2713 host installed as a login service (${path}) \u2014 logs at ~/.paigy/host.log`);
|
|
33330
|
+
console.log(" file limit raised to 65536 for the host and every session it spawns");
|
|
32766
33331
|
return;
|
|
32767
33332
|
}
|
|
32768
33333
|
if (!readToken("Desktop") && !readToken()) {
|
|
32769
33334
|
console.error("Not paired. Run `paigy-harness pair` (or `npx -y -p @paigy/mcp paigy-mcp-onboard`), then retry.");
|
|
32770
33335
|
process.exit(1);
|
|
32771
33336
|
}
|
|
33337
|
+
if (parsed.handoff) {
|
|
33338
|
+
const slot = AGENT_NAME;
|
|
33339
|
+
if (!readToken(slot)) {
|
|
33340
|
+
console.error(`This terminal holds no Paigy identity (slot "${slot}" is empty) \u2014 pair or hatch first.`);
|
|
33341
|
+
process.exit(1);
|
|
33342
|
+
}
|
|
33343
|
+
const cwd = process.cwd();
|
|
33344
|
+
if (!updateSlot(slot, { workspace: cwd })) {
|
|
33345
|
+
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.`);
|
|
33346
|
+
process.exit(1);
|
|
33347
|
+
}
|
|
33348
|
+
const name = slotName(slot) ?? slot;
|
|
33349
|
+
console.log(`\u2713 ${name} is handed off \u2014 wakes in ${cwd.replace(homedir6(), "~")}`);
|
|
33350
|
+
if (!allowed(cwd, { file: workspacesFile() })) {
|
|
33351
|
+
console.log(` \u26A0 that folder isn't on the allow-list, so wakes will land in the first granted`);
|
|
33352
|
+
console.log(` workspace instead \u2014 add it in the desktop app, or: paigy-harness host --grant ${cwd}`);
|
|
33353
|
+
}
|
|
33354
|
+
console.log(` Write to ${name} from your phone and the host resumes it there, thread as memory.`);
|
|
33355
|
+
return;
|
|
33356
|
+
}
|
|
32772
33357
|
if (parsed.hatch) {
|
|
32773
33358
|
overrideToken(readToken("Desktop") || null);
|
|
32774
33359
|
const minted = await hatch(parsed.hatch, parsed.voice ?? null);
|
|
@@ -32859,9 +33444,6 @@ async function main() {
|
|
|
32859
33444
|
});
|
|
32860
33445
|
}
|
|
32861
33446
|
void main();
|
|
32862
|
-
export {
|
|
32863
|
-
parseArgs
|
|
32864
|
-
};
|
|
32865
33447
|
/*! Bundled license information:
|
|
32866
33448
|
|
|
32867
33449
|
undici/lib/web/fetch/body.js:
|