@paigy/harness 0.3.5 → 0.3.7
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/dist/cli.js +1148 -1570
- package/dist/main.js +957 -1352
- package/package.json +12 -13
package/dist/main.js
CHANGED
|
@@ -3,8 +3,9 @@ import {
|
|
|
3
3
|
__commonJS,
|
|
4
4
|
__export,
|
|
5
5
|
__require,
|
|
6
|
-
__toESM
|
|
7
|
-
|
|
6
|
+
__toESM,
|
|
7
|
+
require_undici
|
|
8
|
+
} from "./chunk-LS24S3RM.js";
|
|
8
9
|
|
|
9
10
|
// ../../node_modules/.pnpm/qrcode@1.5.4/node_modules/qrcode/lib/can-promise.js
|
|
10
11
|
var require_can_promise = __commonJS({
|
|
@@ -1305,11 +1306,11 @@ var require_dijkstra = __commonJS({
|
|
|
1305
1306
|
var predecessors = {};
|
|
1306
1307
|
var costs = {};
|
|
1307
1308
|
costs[s] = 0;
|
|
1308
|
-
var
|
|
1309
|
-
|
|
1309
|
+
var open = dijkstra.PriorityQueue.make();
|
|
1310
|
+
open.push(s, 0);
|
|
1310
1311
|
var closest, u, v, cost_of_s_to_u, adjacent_nodes, cost_of_e, cost_of_s_to_u_plus_cost_of_e, cost_of_s_to_v, first_visit;
|
|
1311
|
-
while (!
|
|
1312
|
-
closest =
|
|
1312
|
+
while (!open.empty()) {
|
|
1313
|
+
closest = open.pop();
|
|
1313
1314
|
u = closest.value;
|
|
1314
1315
|
cost_of_s_to_u = closest.cost;
|
|
1315
1316
|
adjacent_nodes = graph[u] || {};
|
|
@@ -1321,7 +1322,7 @@ var require_dijkstra = __commonJS({
|
|
|
1321
1322
|
first_visit = typeof costs[v] === "undefined";
|
|
1322
1323
|
if (first_visit || cost_of_s_to_v > cost_of_s_to_u_plus_cost_of_e) {
|
|
1323
1324
|
costs[v] = cost_of_s_to_u_plus_cost_of_e;
|
|
1324
|
-
|
|
1325
|
+
open.push(v, cost_of_s_to_u_plus_cost_of_e);
|
|
1325
1326
|
predecessors[v] = u;
|
|
1326
1327
|
}
|
|
1327
1328
|
}
|
|
@@ -4579,11 +4580,13 @@ import { app, BrowserWindow, dialog, ipcMain } from "electron";
|
|
|
4579
4580
|
import { spawn as spawn2 } from "child_process";
|
|
4580
4581
|
import { readFileSync as readFileSync4 } from "fs";
|
|
4581
4582
|
import { fileURLToPath } from "url";
|
|
4582
|
-
import { dirname as
|
|
4583
|
+
import { dirname as dirname3, join as join5 } from "path";
|
|
4583
4584
|
|
|
4584
4585
|
// ../../packages/sdk/dist/index.js
|
|
4585
4586
|
import { createRequire as __sdkCreateRequire } from "module";
|
|
4586
4587
|
import { createHash, randomUUID } from "crypto";
|
|
4588
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
4589
|
+
import { setTimeout as sleep2 } from "timers/promises";
|
|
4587
4590
|
|
|
4588
4591
|
// ../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/external.js
|
|
4589
4592
|
var external_exports = {};
|
|
@@ -6149,12 +6152,12 @@ ZodString.create = (params) => {
|
|
|
6149
6152
|
...processCreateParams(params)
|
|
6150
6153
|
});
|
|
6151
6154
|
};
|
|
6152
|
-
function floatSafeRemainder(val,
|
|
6155
|
+
function floatSafeRemainder(val, step2) {
|
|
6153
6156
|
const valDecCount = (val.toString().split(".")[1] || "").length;
|
|
6154
|
-
const stepDecCount = (
|
|
6157
|
+
const stepDecCount = (step2.toString().split(".")[1] || "").length;
|
|
6155
6158
|
const decCount = valDecCount > stepDecCount ? valDecCount : stepDecCount;
|
|
6156
6159
|
const valInt = Number.parseInt(val.toFixed(decCount).replace(".", ""));
|
|
6157
|
-
const stepInt = Number.parseInt(
|
|
6160
|
+
const stepInt = Number.parseInt(step2.toFixed(decCount).replace(".", ""));
|
|
6158
6161
|
return valInt % stepInt / 10 ** decCount;
|
|
6159
6162
|
}
|
|
6160
6163
|
var ZodNumber = class _ZodNumber extends ZodType {
|
|
@@ -8631,6 +8634,7 @@ import { randomUUID as randomUUID2 } from "crypto";
|
|
|
8631
8634
|
import { closeSync, existsSync, mkdirSync, openSync, readFileSync, rmSync, statSync, writeFileSync } from "fs";
|
|
8632
8635
|
import { homedir } from "os";
|
|
8633
8636
|
import { join } from "path";
|
|
8637
|
+
var import_undici = __toESM(require_undici(), 1);
|
|
8634
8638
|
var require2 = __sdkCreateRequire(import.meta.url);
|
|
8635
8639
|
var __create = Object.create;
|
|
8636
8640
|
var __defProp = Object.defineProperty;
|
|
@@ -8961,17 +8965,17 @@ var require_nacl_fast = __commonJS2({
|
|
|
8961
8965
|
}
|
|
8962
8966
|
var sigma = new Uint8Array([101, 120, 112, 97, 110, 100, 32, 51, 50, 45, 98, 121, 116, 101, 32, 107]);
|
|
8963
8967
|
function crypto_stream_salsa20_xor(c, cpos, m, mpos, b, n, k) {
|
|
8964
|
-
var
|
|
8968
|
+
var z4 = new Uint8Array(16), x = new Uint8Array(64);
|
|
8965
8969
|
var u, i;
|
|
8966
|
-
for (i = 0; i < 16; i++)
|
|
8967
|
-
for (i = 0; i < 8; i++)
|
|
8970
|
+
for (i = 0; i < 16; i++) z4[i] = 0;
|
|
8971
|
+
for (i = 0; i < 8; i++) z4[i] = n[i];
|
|
8968
8972
|
while (b >= 64) {
|
|
8969
|
-
crypto_core_salsa20(x,
|
|
8973
|
+
crypto_core_salsa20(x, z4, k, sigma);
|
|
8970
8974
|
for (i = 0; i < 64; i++) c[cpos + i] = m[mpos + i] ^ x[i];
|
|
8971
8975
|
u = 1;
|
|
8972
8976
|
for (i = 8; i < 16; i++) {
|
|
8973
|
-
u = u + (
|
|
8974
|
-
|
|
8977
|
+
u = u + (z4[i] & 255) | 0;
|
|
8978
|
+
z4[i] = u & 255;
|
|
8975
8979
|
u >>>= 8;
|
|
8976
8980
|
}
|
|
8977
8981
|
b -= 64;
|
|
@@ -8979,30 +8983,30 @@ var require_nacl_fast = __commonJS2({
|
|
|
8979
8983
|
mpos += 64;
|
|
8980
8984
|
}
|
|
8981
8985
|
if (b > 0) {
|
|
8982
|
-
crypto_core_salsa20(x,
|
|
8986
|
+
crypto_core_salsa20(x, z4, k, sigma);
|
|
8983
8987
|
for (i = 0; i < b; i++) c[cpos + i] = m[mpos + i] ^ x[i];
|
|
8984
8988
|
}
|
|
8985
8989
|
return 0;
|
|
8986
8990
|
}
|
|
8987
8991
|
function crypto_stream_salsa20(c, cpos, b, n, k) {
|
|
8988
|
-
var
|
|
8992
|
+
var z4 = new Uint8Array(16), x = new Uint8Array(64);
|
|
8989
8993
|
var u, i;
|
|
8990
|
-
for (i = 0; i < 16; i++)
|
|
8991
|
-
for (i = 0; i < 8; i++)
|
|
8994
|
+
for (i = 0; i < 16; i++) z4[i] = 0;
|
|
8995
|
+
for (i = 0; i < 8; i++) z4[i] = n[i];
|
|
8992
8996
|
while (b >= 64) {
|
|
8993
|
-
crypto_core_salsa20(x,
|
|
8997
|
+
crypto_core_salsa20(x, z4, k, sigma);
|
|
8994
8998
|
for (i = 0; i < 64; i++) c[cpos + i] = x[i];
|
|
8995
8999
|
u = 1;
|
|
8996
9000
|
for (i = 8; i < 16; i++) {
|
|
8997
|
-
u = u + (
|
|
8998
|
-
|
|
9001
|
+
u = u + (z4[i] & 255) | 0;
|
|
9002
|
+
z4[i] = u & 255;
|
|
8999
9003
|
u >>>= 8;
|
|
9000
9004
|
}
|
|
9001
9005
|
b -= 64;
|
|
9002
9006
|
cpos += 64;
|
|
9003
9007
|
}
|
|
9004
9008
|
if (b > 0) {
|
|
9005
|
-
crypto_core_salsa20(x,
|
|
9009
|
+
crypto_core_salsa20(x, z4, k, sigma);
|
|
9006
9010
|
for (i = 0; i < b; i++) c[cpos + i] = x[i];
|
|
9007
9011
|
}
|
|
9008
9012
|
return 0;
|
|
@@ -9882,12 +9886,12 @@ var require_nacl_fast = __commonJS2({
|
|
|
9882
9886
|
for (a = 0; a < 16; a++) o[a] = c[a];
|
|
9883
9887
|
}
|
|
9884
9888
|
function crypto_scalarmult(q, n, p) {
|
|
9885
|
-
var
|
|
9889
|
+
var z4 = new Uint8Array(32);
|
|
9886
9890
|
var x = new Float64Array(80), r, i;
|
|
9887
9891
|
var a = gf(), b = gf(), c = gf(), d = gf(), e = gf(), f = gf();
|
|
9888
|
-
for (i = 0; i < 31; i++)
|
|
9889
|
-
|
|
9890
|
-
|
|
9892
|
+
for (i = 0; i < 31; i++) z4[i] = n[i];
|
|
9893
|
+
z4[31] = n[31] & 127 | 64;
|
|
9894
|
+
z4[0] &= 248;
|
|
9891
9895
|
unpack25519(x, p);
|
|
9892
9896
|
for (i = 0; i < 16; i++) {
|
|
9893
9897
|
b[i] = x[i];
|
|
@@ -9895,7 +9899,7 @@ var require_nacl_fast = __commonJS2({
|
|
|
9895
9899
|
}
|
|
9896
9900
|
a[0] = d[0] = 1;
|
|
9897
9901
|
for (i = 254; i >= 0; --i) {
|
|
9898
|
-
r =
|
|
9902
|
+
r = z4[i >>> 3] >>> (i & 7) & 1;
|
|
9899
9903
|
sel25519(a, b, r);
|
|
9900
9904
|
sel25519(c, d, r);
|
|
9901
9905
|
A(e, a, c);
|
|
@@ -10892,7 +10896,7 @@ var PROXY_ENV = ["HTTPS_PROXY", "https_proxy", "HTTP_PROXY", "http_proxy"];
|
|
|
10892
10896
|
var agent;
|
|
10893
10897
|
async function proxy() {
|
|
10894
10898
|
if (!PROXY_ENV.some((k) => process.env[k])) return void 0;
|
|
10895
|
-
return agent ??= new (await import("./undici-
|
|
10899
|
+
return agent ??= new (await import("./undici-FEMMVKAA.js")).EnvHttpProxyAgent();
|
|
10896
10900
|
}
|
|
10897
10901
|
var INSTANCE_ID = randomUUID();
|
|
10898
10902
|
var WORKSPACE_ID = (() => {
|
|
@@ -12134,20 +12138,17 @@ var zodToJsonSchema = (schema, options) => {
|
|
|
12134
12138
|
};
|
|
12135
12139
|
var OPTIONS_MIN = 2;
|
|
12136
12140
|
var OPTIONS_MAX = 6;
|
|
12137
|
-
var MISSED_CALL_PLAN = {
|
|
12138
|
-
retry_10m: { kind: "every", minutes: 10 },
|
|
12139
|
-
retry_30m: { kind: "every", minutes: 30 },
|
|
12140
|
-
retry_60m: { kind: "every", minutes: 60 },
|
|
12141
|
-
backoff_gentle: { kind: "at", minutes: [30, 120, 360] },
|
|
12142
|
-
backoff_standard: { kind: "at", minutes: [10, 30, 120] },
|
|
12143
|
-
backoff_aggressive: { kind: "at", minutes: [5, 15, 45] },
|
|
12144
|
-
inbox: { kind: "once" },
|
|
12145
|
-
dismiss: { kind: "grace", minutes: 2 }
|
|
12146
|
-
};
|
|
12147
12141
|
function draft2020(node) {
|
|
12148
12142
|
if (Array.isArray(node)) return node.map(draft2020);
|
|
12149
12143
|
if (node && typeof node === "object") {
|
|
12150
12144
|
const o = node;
|
|
12145
|
+
if (Array.isArray(o.items)) {
|
|
12146
|
+
o.prefixItems = o.items;
|
|
12147
|
+
if ("additionalItems" in o) {
|
|
12148
|
+
o.items = o.additionalItems;
|
|
12149
|
+
delete o.additionalItems;
|
|
12150
|
+
} else delete o.items;
|
|
12151
|
+
}
|
|
12151
12152
|
for (const [excl, lim] of [["exclusiveMinimum", "minimum"], ["exclusiveMaximum", "maximum"]]) {
|
|
12152
12153
|
if (typeof o[excl] === "boolean") {
|
|
12153
12154
|
if (o[excl] === true && typeof o[lim] === "number") {
|
|
@@ -12166,11 +12167,54 @@ function mcpInputSchema(s) {
|
|
|
12166
12167
|
delete schema.$schema;
|
|
12167
12168
|
return draft2020(schema);
|
|
12168
12169
|
}
|
|
12170
|
+
var StartContactSchema = external_exports.object({
|
|
12171
|
+
goalIds: external_exports.tuple([external_exports.string().uuid()]),
|
|
12172
|
+
// THE ASK IS THE QUESTION, not a bulletin with a question at the end (2026-09-16).
|
|
12173
|
+
//
|
|
12174
|
+
// A DecisionNeed is settled only by an answer in the shape THIS ask declares. Bundle news,
|
|
12175
|
+
// findings and a decision into one, and the person answers whichever part engaged them —
|
|
12176
|
+
// which settles nothing, leaves the need open, and gets them asked again. On call 619c5a92
|
|
12177
|
+
// one ask carried a briefing, a side question ("the ring should show the agent's name now —
|
|
12178
|
+
// what did you actually see?") and a one-of-four about how far an erase reaches. The owner
|
|
12179
|
+
// answered the side question. The need stayed open; the card asked again. Of the 45 call
|
|
12180
|
+
// asks carrying a decision, 27 are over 600 characters.
|
|
12181
|
+
//
|
|
12182
|
+
// Sending them separately costs nothing, because a CALL contact joins the call already
|
|
12183
|
+
// happening (`goal/store.ts`, JOIN BEFORE MINTING) — several contacts on one Goal arrive as
|
|
12184
|
+
// one call, planned as its turns, and each keeps its own settleable need.
|
|
12185
|
+
ask: external_exports.string().trim().min(1).max(1e4).describe(
|
|
12186
|
+
"The question, and only what is needed to answer it. News, progress and findings are their own contact \u2014 a call contact JOINS a call already happening, so several arrive as one call. Do not bundle: a DecisionNeed is settled only by an answer in the shape this ask declares, so someone who answers the part that interested them settles nothing and is asked again."
|
|
12187
|
+
),
|
|
12188
|
+
waiting: external_exports.enum(["none", "hard"]).default("none"),
|
|
12189
|
+
channel: external_exports.enum(["notification", "call"]).default("notification"),
|
|
12190
|
+
options: external_exports.array(external_exports.object({ label: external_exports.string().trim().min(1).max(1e3), image: external_exports.string().url().optional(), html: external_exports.string().max(16384).optional() }).strict()).min(2).max(6).optional(),
|
|
12191
|
+
threadId: external_exports.string().uuid().optional().describe("Continue an existing Thread: the threadId a prior Delivery returned. Omit to start a new Thread.")
|
|
12192
|
+
}).strict();
|
|
12193
|
+
var ContactSchema = external_exports.union([StartContactSchema, external_exports.object({ deliveryId: external_exports.string().uuid() }).strict()]);
|
|
12194
|
+
var CONTACT_SCHEMA = { type: "object", ...mcpInputSchema(ContactSchema) };
|
|
12195
|
+
var CONTACT_DESCRIPTION = "Contact the user about exactly one existing Goal: pass goalIds:[goalId], ask, channel:'notification'|'call', and waiting:'none'|'hard'. Options supply choices. Notification returns immediately; collect durable answers with claim_goal/get_goal. On stdio, a Call holds one cancellable ~45s window; continue with ONLY {deliveryId}. Continuation sends nothing and rereads the same durable evidence, including previously read answers. Entries retain authorship and provenance; accepted decisions are separate from quoted speech. Call state open does not mean ringing. Unsupported: soft waiting, multiple Goals/questions, outcome admission, and re-presentation of an existing request. Create a Goal explicitly first; never resend a pending ask to continue waiting.";
|
|
12169
12196
|
var CreateGoalSchema = external_exports.object({
|
|
12170
12197
|
outcome: external_exports.string().trim().min(1).max(1e4),
|
|
12171
12198
|
ownerParticipant: external_exports.string().trim().min(1).optional(),
|
|
12172
|
-
idempotencyKey: external_exports.string().trim().min(1).max(200)
|
|
12199
|
+
idempotencyKey: external_exports.string().trim().min(1).max(200),
|
|
12200
|
+
/** A past conversation this Goal should be read against — History's "new session from this"
|
|
12201
|
+
* (owner, on the call of 2026-09-14: "let's do the reference with the threading"). A
|
|
12202
|
+
* reference only: the owner reads it through `get_thread`, which does its own scoping, and
|
|
12203
|
+
* the writer refuses a thread belonging to another account. */
|
|
12204
|
+
contextThreadId: external_exports.string().uuid().optional(),
|
|
12205
|
+
/** THE GOAL THIS ONE BELONGS UNDER (owner, 2026-09-15: "the ask I gave for the design doc
|
|
12206
|
+
* didn't get created as a child goal of the voice UI goal, which is how it should've
|
|
12207
|
+
* worked"). It could not have been: this door took no parent, so the only route was
|
|
12208
|
+
* create -> claim -> `update_goal`, three calls with a lease in the middle, and every agent
|
|
12209
|
+
* took the short one. The hierarchy has been modelled since Goals existed and had been used
|
|
12210
|
+
* ZERO times in 2,031 of them. Absent, the server judges it against the caller's open Goals
|
|
12211
|
+
* (`apps/api/src/goal/intake.ts`). The writer refuses a Goal belonging to another account. */
|
|
12212
|
+
parentGoalId: external_exports.string().uuid().optional()
|
|
12173
12213
|
});
|
|
12214
|
+
var CreateGoalToolSchema = CreateGoalSchema.extend({
|
|
12215
|
+
idempotencyKey: CreateGoalSchema.shape.idempotencyKey.optional().describe("Optional. One is minted per call; pass your own only so a retry lands on the same Goal.")
|
|
12216
|
+
}).strict();
|
|
12217
|
+
var CREATE_GOAL_DESCRIPTION = "Create a durable Goal for an outcome. Without parentGoalId it is placed against your open Goals: if one already IS this work, that Goal comes back (existing: true) and nothing new is created \u2014 continue it; if the work belongs under one, it is created there (parentGoalId in the receipt); otherwise it is a root. Pass parentGoalId yourself to put it under a specific Goal. Admission only: the owner must claim it before doing work, then update it as it advances. Returns an admission receipt with goalId, current state, revision, ownerParticipant, and the next step; no Goal content or execution lease.";
|
|
12174
12218
|
var UpdateGoalSchema = external_exports.object({
|
|
12175
12219
|
revision: external_exports.number().int().positive(),
|
|
12176
12220
|
changes: external_exports.object({
|
|
@@ -12186,52 +12230,33 @@ var UpdateGoalSchema = external_exports.object({
|
|
|
12186
12230
|
reason: external_exports.string().trim().min(1).max(2e3),
|
|
12187
12231
|
operationId: external_exports.string().uuid().optional()
|
|
12188
12232
|
}).strict();
|
|
12189
|
-
var UpdateGoalToolSchema = UpdateGoalSchema.extend({ goalId: external_exports.string().uuid() }).strict();
|
|
12233
|
+
var UpdateGoalToolSchema = UpdateGoalSchema.omit({ operationId: true }).extend({ goalId: external_exports.string().uuid() }).strict();
|
|
12190
12234
|
var ClaimGoalSchema = external_exports.object({ goalId: external_exports.string().uuid().optional() }).strict();
|
|
12191
|
-
var
|
|
12192
|
-
var
|
|
12193
|
-
|
|
12194
|
-
|
|
12195
|
-
|
|
12196
|
-
|
|
12197
|
-
|
|
12198
|
-
|
|
12199
|
-
|
|
12200
|
-
|
|
12201
|
-
|
|
12202
|
-
|
|
12203
|
-
|
|
12204
|
-
|
|
12205
|
-
|
|
12206
|
-
|
|
12207
|
-
|
|
12208
|
-
|
|
12209
|
-
|
|
12210
|
-
|
|
12211
|
-
|
|
12212
|
-
|
|
12213
|
-
|
|
12214
|
-
|
|
12215
|
-
|
|
12216
|
-
workId: fields.workId.describe(
|
|
12217
|
-
"The durable Work this contact advances. Pass the workId from check_replies or a prior reply when asking for a decision that blocks that work."
|
|
12218
|
-
),
|
|
12219
|
-
goalId: fields.goalId.describe(
|
|
12220
|
-
"The target Goal this contact advances. Use goalId for the Goal model; do not combine it with workId."
|
|
12221
|
-
),
|
|
12222
|
-
// THE WAIT, CONTINUED (owner, 2026-09-06: "await should have been folded into contact").
|
|
12223
|
-
// A contact that rang holds its first window itself; the host caps one tool call at
|
|
12224
|
-
// ~60 s, so keeping the line is another contact — with ONLY this field. Nothing is sent.
|
|
12225
|
-
wait: external_exports.string().uuid().optional().describe(
|
|
12226
|
-
"KEEP WAITING on a live call: the notificationId a previous contact returned. Send it ALONE \u2014 no ask, nothing new goes to the user; contact just holds the next ~45 s window and returns the outcome in `wait`."
|
|
12227
|
-
)
|
|
12228
|
-
});
|
|
12229
|
-
const out = mcpInputSchema(surface);
|
|
12230
|
-
delete out.required;
|
|
12231
|
-
out.anyOf = [{ required: ["ask"] }, { required: ["wait"] }];
|
|
12232
|
-
return out;
|
|
12233
|
-
}
|
|
12234
|
-
var CONTACT_DESCRIPTION = `Reach the user through Paigy \u2014 tell them something, or ask and get their answer. State what you need in \`ask\`, say what happens to your work while you wait in \`waiting\`, and Paigy handles the rest (channel, phrasing, answer format). If the user explicitly asks you to CALL them, send waiting:'hard' and say so in the ask. Returns { notificationId, parentId, workId?, goalId?, entryId?, decisionId?, wait? }. WHEN IT RANG, contact holds the first ~45 s window ITSELF and \`wait\` carries the outcome: { type:'reply', answer } to act on; { type:'partial', inFlight:true, turn } \u2014 what the user is saying to each turn, provisional: use it to PREPARE (fetch, draft, warm the build), never to act irreversibly, they can still revise it until the final reply (partial = intelligence, settled = authorization; if a partial's acts carry a question aimed at you and you know the answer, contact on the SAME parentId right away \u2014 they hear it on the same call); { type:'remind', remindInSeconds } \u2014 schedule a wake-up; { type:'idle' } \u2014 still waiting. TO KEEP WAITING, call contact again with ONLY { wait: notificationId } \u2014 no ask, nothing new is sent; it holds the next scoped ~45 s window (under the 60 s host cap, so it always returns) and never returns another notification's reply. Keep doing that until the reply \u2014 THAT one is the decision \u2014 so the user steps away and comes back to find you already continued; stop only to do other work and check back, or after an unreasonably long stretch worth telling them about. A message delivery has NO \`wait\`: never poll for it \u2014 the reply arrives through check_replies or your wake. Pass parentId to a later contact to continue the conversation. A Goal-targeted contact also returns goalId and entryId for the durable Goal entry. When it rang, the reply also carries { ifMissed: { mode, means } }: what the user's own policy does with a call they don't take ("${STANDARD_MEANS}"), so a no-answer tells you how long to wait before coming back. THREADING REPLACES: a threaded follow-up SUPERSEDES your earlier pending items on that thread \u2014 right for updates to one ask, WRONG for a checklist (send independent to-dos un-threaded). A threaded re-send with IDENTICAL content escalates the pending ask in place. If a reply comes back as {kind:'clarify', chunks:[...]}, the user wants more detail \u2014 contact again on the SAME parentId with an expanded ask. BLOCKED ON A DECISION for existing work? Pass that work's \`workId\`; the reply returns the same workId plus a decisionId, so the answer resumes the right outcome. ONE ASK, ONE ROW: never restate a still-pending ask's question inside a NEW contact (e.g. weaving it into a briefing) \u2014 the whole answer settles on the new row and the original can never receive it. Keep waiting on the original (a live call reads every pending ask out separately, each answer routes to its own row), and use \`needs\` for a genuinely multi-part NEW ask. ANSWERABLE, NOT JUST ASKED: when the reply comes back carrying \`plan.units[].needs\`, that unit asked for something it gave the user no way to answer \u2014 'options' means it posed a choice with nothing to choose from, 'visuals' means it asked about something to look at with nothing to look at. Send it again on the SAME parentId with ${OPTIONS_MIN}-${OPTIONS_MAX} options (or the image), drawn from your own sentence. Paigy will not add them for you: a shape it guessed wrong cannot be undone, and you are the one who knows what the real alternatives are. \`units\` reports WHAT BECAME OF YOUR PROSE \u2014 { kept, raw, why }: how many topics Paigy compressed for delivery, how many kept your exact words, and the reason when it kept them (e.g. 'no_output' = compression produced nothing usable, so the user got your raw sentence). It needs no action and is not an error \u2014 read it only when the delivered wording matters to you; a high \`raw\` count means the user is hearing you verbatim. READING A CALL'S REPLY: it can come back as {kind:'turns', turns:[{prompt,reply}]} \u2014 the ordered log of that call. Read turns[0].reply as the user's main instruction. Usually that's the only turn; if there are more (e.g. an end-of-call 'call me back when it's done / I have a question that blocks me'), read each one in order as a further follow-up instruction, not a single combined one. If they asked for a callback, re-engage in the SAME thread (contact with the reply's parentId) when the task is done or you hit a blocker \u2014 waiting:'hard' for a blocker, waiting:'none' for done. Paigy has no scheduler; the callback is yours to send (use ScheduleWakeup/cron for timing). A call-mapped answer may carry \`intents\` \u2014 next steps the user attached, each { kind, detail } with detail quoting their words. ACT on them, don't just read them: 'defer' ("call me after lunch") \u2192 register it NOW with schedule_callback \u2014 when the intent carries \`dueInSeconds\` (Paigy pre-parsed the spoken time against the user's clock) pass it straight through; otherwise derive it from the detail yourself \u2014 then follow up on the same thread; 'delegate' ("you pick") \u2192 make the call yourself and tell them what you chose; 'channel' ("text me next time") \u2192 honor it on your next contact (channel:'message'); 'question' (an open question aimed back at you that the call couldn't answer) \u2192 you OWE them the answer \u2014 work it out and follow up on the same thread without being asked, the call deliberately skipped "should I call you back?" because the follow-up is implied. \`transcript\` is the user's raw words behind a shaped answer \u2014 read it for hedges and conditions ("yes, IF tests pass") before acting. If your ask declared \`points\`, the reply carries \`covered\` \u2014 the points actually addressed. Compare against what you declared: a missing point is STILL unanswered \u2014 re-ask it (contact on the same parentId) or proceed knowingly partial; never treat a partial answer as complete.`;
|
|
12235
|
+
var GetGoalSchema = external_exports.object({ goalId: external_exports.string().uuid() }).strict();
|
|
12236
|
+
var GET_GOAL_DESCRIPTION = "Read the current authorized Goal brief: state, owner, blockers, open decisions, progress, and the next operation. Foreign or sibling-owned Goals are not disclosed.";
|
|
12237
|
+
var UPDATE_GOAL_DESCRIPTION = "Update an owned Goal at an exact revision. State, ownership, dependencies, children, progress, and review acknowledgement are explicit; stale revisions are rejected. Returns the new revision and a prose summary.";
|
|
12238
|
+
var CLAIM_GOAL_DESCRIPTION = "Claim the oldest runnable or review-pending Goal you own, or pass goalId to claim that Goal. Returns a Goal-scoped brief, current revision, blockers, and the next valid operation. Claiming creates or renews the execution lease.";
|
|
12239
|
+
var CHECK_REPLIES_DESCRIPTION = "Your open Deliveries: every Notification or Call currently addressed to you \u2014 a request the user started toward you, an answer relayed to something you asked, a handoff \u2014 each with its durable Entries, accepted decisions and open decision needs, in the same shape a contact read returns. A pure read with no arguments: nothing is consumed, acknowledged or claimed by reading it, so call it on startup, after a long wait, or whenever you want to know what is outstanding. To act on one, claim its Goal (claim_goal) or reread it with contact({deliveryId}). Your runnable and review-pending Goals come from claim_goal, not from here.";
|
|
12240
|
+
var CheckRepliesSchema = external_exports.object({}).strict();
|
|
12241
|
+
var GetThreadSchema = external_exports.object({
|
|
12242
|
+
parentId: external_exports.string().describe("The Thread to read \u2014 the threadId a Delivery returned, or the parentId of a search hit.")
|
|
12243
|
+
}).strict();
|
|
12244
|
+
var GET_THREAD_DESCRIPTION = "Read the authorized durable Entries on one conversation Thread \u2014 what you wrote there and what was delivered to you, oldest first. Use claim_goal to find the work to resume; use this to rehydrate a Thread that a search hit or a Delivery named.";
|
|
12245
|
+
var SearchThreadsSchema = external_exports.object({
|
|
12246
|
+
q: external_exports.string().describe("What to look for \u2014 plain words or a phrase (e.g. 'the livekit timeout', 'deploy to prod').")
|
|
12247
|
+
}).strict();
|
|
12248
|
+
var SEARCH_THREADS_DESCRIPTION = `Search your PAST conversations before asking \u2014 "have we discussed this before?". Full-text over your own threads (the asks you sent + the user's answers); returns ranked threads with highlighted snippets, NOT rows: { hits: [{ parentId, at, agentLabel, matches: [{ notificationId, role, snippet }] }] }. The loop this exists for: search first \u2192 get_thread the best hit to rehydrate it \u2192 THEN continue or contact, so you answer with receipts ("last week you said ship it") instead of re-asking. Read-only, safe to call anytime; scoped to your own account's threads.`;
|
|
12249
|
+
var AGENT_TOOLS = [
|
|
12250
|
+
{ name: "contact", description: CONTACT_DESCRIPTION, inputSchema: CONTACT_SCHEMA },
|
|
12251
|
+
{ name: "check_replies", description: CHECK_REPLIES_DESCRIPTION, inputSchema: mcpInputSchema(CheckRepliesSchema) },
|
|
12252
|
+
{ name: "get_thread", description: GET_THREAD_DESCRIPTION, inputSchema: mcpInputSchema(GetThreadSchema) },
|
|
12253
|
+
{ name: "search_threads", description: SEARCH_THREADS_DESCRIPTION, inputSchema: mcpInputSchema(SearchThreadsSchema) },
|
|
12254
|
+
{ name: "create_goal", description: CREATE_GOAL_DESCRIPTION, inputSchema: mcpInputSchema(CreateGoalToolSchema) },
|
|
12255
|
+
{ name: "claim_goal", description: CLAIM_GOAL_DESCRIPTION, inputSchema: mcpInputSchema(ClaimGoalSchema) },
|
|
12256
|
+
{ name: "get_goal", description: GET_GOAL_DESCRIPTION, inputSchema: mcpInputSchema(GetGoalSchema) },
|
|
12257
|
+
{ name: "update_goal", description: UPDATE_GOAL_DESCRIPTION, inputSchema: mcpInputSchema(UpdateGoalToolSchema) }
|
|
12258
|
+
];
|
|
12259
|
+
var AGENT_TOOL_NAMES = AGENT_TOOLS.map((t) => t.name);
|
|
12235
12260
|
var ContextSchema = external_exports.object({
|
|
12236
12261
|
title: external_exports.string().min(1).describe("One-line headline of what you need (required, non-empty)."),
|
|
12237
12262
|
description: external_exports.array(external_exports.string().min(1)).describe(
|
|
@@ -12517,16 +12542,6 @@ function deriveAsk(req) {
|
|
|
12517
12542
|
}
|
|
12518
12543
|
var NotifyStatusSchema = external_exports.enum(["pending", "answered", "ignored"]);
|
|
12519
12544
|
var AgentStateSchema = external_exports.enum(["idle", "in_progress", "completed", "needs_input"]);
|
|
12520
|
-
var SetTaskStateSchema = external_exports.object({
|
|
12521
|
-
state: external_exports.enum(["in_progress", "completed", "needs_input"])
|
|
12522
|
-
});
|
|
12523
|
-
var SetWorkStateSchema = external_exports.object({
|
|
12524
|
-
workId: external_exports.string().uuid().optional(),
|
|
12525
|
-
notificationId: external_exports.string().optional(),
|
|
12526
|
-
state: SetTaskStateSchema.shape.state
|
|
12527
|
-
}).refine((value) => Number(Boolean(value.workId)) + Number(Boolean(value.notificationId)) === 1, {
|
|
12528
|
-
message: "exactly one of workId or notificationId is required"
|
|
12529
|
-
});
|
|
12530
12545
|
var TurnSchema = external_exports.object({
|
|
12531
12546
|
prompt: external_exports.string(),
|
|
12532
12547
|
reply: external_exports.string()
|
|
@@ -12656,108 +12671,6 @@ var AwaitItemSchema = external_exports.discriminatedUnion("type", [
|
|
|
12656
12671
|
})
|
|
12657
12672
|
]);
|
|
12658
12673
|
var CallbackTriggerSchema = external_exports.enum(["on_done", "on_blocked", "scheduled"]);
|
|
12659
|
-
var ScheduleCallbackSchema = external_exports.object({
|
|
12660
|
-
parentId: external_exports.string().describe("The thread to call back on (from a prior contact / reply / request)."),
|
|
12661
|
-
goalId: external_exports.string().uuid().optional().describe("The Goal this callback advances; preferred for Goal-owned work."),
|
|
12662
|
-
trigger: CallbackTriggerSchema,
|
|
12663
|
-
dueInSeconds: external_exports.number().int().positive().optional().describe("For 'scheduled' only: how many seconds from now to fire."),
|
|
12664
|
-
note: external_exports.string().optional().describe("What to tell the user when you follow up.")
|
|
12665
|
-
});
|
|
12666
|
-
var PendingRepliesSchema = external_exports.object({
|
|
12667
|
-
replies: external_exports.array(
|
|
12668
|
-
external_exports.object({
|
|
12669
|
-
parentId: external_exports.string(),
|
|
12670
|
-
notificationId: external_exports.string(),
|
|
12671
|
-
workId: external_exports.string().uuid().optional(),
|
|
12672
|
-
decisionId: external_exports.string().uuid().optional(),
|
|
12673
|
-
answer: UserAnswerSchema,
|
|
12674
|
-
/** E2EE: the sealed answer (opaque envelope + plaintext `ignored` hint) when the
|
|
12675
|
-
* pairing is E2EE — the agent opens it and re-derives the real answer. Absent =
|
|
12676
|
-
* plaintext answer (in `answer`). See AwaitItemSchema's reply variant. */
|
|
12677
|
-
sealed: external_exports.lazy(() => SealedAnswerSchema).optional(),
|
|
12678
|
-
/** The call record rendered for THIS agent: the raw words the shaped answer was
|
|
12679
|
-
* mapped from, filtered to its own claims. See AwaitItemSchema's reply variant. */
|
|
12680
|
-
transcript: external_exports.string().optional(),
|
|
12681
|
-
/** Coverage report (#396): which declared `points` this answer addressed. */
|
|
12682
|
-
covered: external_exports.array(external_exports.string()).optional()
|
|
12683
|
-
})
|
|
12684
|
-
),
|
|
12685
|
-
pending: external_exports.array(
|
|
12686
|
-
external_exports.object({ parentId: external_exports.string(), notificationId: external_exports.string(), createdAt: external_exports.string() })
|
|
12687
|
-
),
|
|
12688
|
-
/** WHO YOU ARE on this account (field report 2026-08-28): the name and device the user
|
|
12689
|
-
* sees for this session's identity. From inside a session there was no way to find out —
|
|
12690
|
-
* `pair` with no arguments can HATCH a fresh identity, so it is not a safe probe — and an
|
|
12691
|
-
* agent that cannot tell which agent it is cannot tell whether work addressed to
|
|
12692
|
-
* "Reta" was addressed to it. Absent only for a token with no pairing behind it. */
|
|
12693
|
-
you: external_exports.object({ name: external_exports.string(), device: external_exports.string().nullable(), tokenId: external_exports.string() }).optional(),
|
|
12694
|
-
/** User-initiated requests addressed to this agent; act on them and reply via
|
|
12695
|
-
* contact on the same parentId. Keeps reappearing until you call
|
|
12696
|
-
* set_work_state on its workId (or notificationId once while bootstrapping). */
|
|
12697
|
-
requests: external_exports.array(
|
|
12698
|
-
external_exports.object({
|
|
12699
|
-
parentId: external_exports.string(),
|
|
12700
|
-
notificationId: external_exports.string(),
|
|
12701
|
-
workId: external_exports.string().uuid().optional(),
|
|
12702
|
-
text: external_exports.string(),
|
|
12703
|
-
createdAt: external_exports.string(),
|
|
12704
|
-
/** The user seeded this request with a past conversation — call get_thread on it
|
|
12705
|
-
* FIRST and treat the transcript as prior context (#57/#251). */
|
|
12706
|
-
contextParentId: external_exports.string().optional(),
|
|
12707
|
-
/** STRANDED (field report 2026-08-28): this request was addressed to ANOTHER agent on
|
|
12708
|
-
* the account — the name here — which has not been seen since it landed, so nobody
|
|
12709
|
-
* came for it. Handed to you because you are the session that is here. Take it like
|
|
12710
|
-
* any request (set_work_state claims it, reply with contact on its parentId), and say
|
|
12711
|
-
* whose it was, because the user chose that agent on purpose. */
|
|
12712
|
-
stranded: external_exports.string().optional()
|
|
12713
|
-
})
|
|
12714
|
-
),
|
|
12715
|
-
/** Durable outcomes currently owned by this agent. This is the Work-native queue; flat
|
|
12716
|
-
* notification lists remain during migration so old clients keep their existing view. */
|
|
12717
|
-
work: external_exports.array(external_exports.object({
|
|
12718
|
-
workId: external_exports.string().uuid(),
|
|
12719
|
-
parentId: external_exports.string().optional(),
|
|
12720
|
-
objective: external_exports.string().nullable(),
|
|
12721
|
-
state: external_exports.enum(["active", "blocked", "waiting_external"]),
|
|
12722
|
-
blockedOn: external_exports.array(external_exports.string().uuid()),
|
|
12723
|
-
updatedAt: external_exports.string()
|
|
12724
|
-
})).optional(),
|
|
12725
|
-
/** Callbacks you owe the user that are now DUE (you said you'd follow up when done,
|
|
12726
|
-
* if blocked, or at a time that has passed). Re-surfaced every sweep until you
|
|
12727
|
-
* fulfill one by calling contact on its parentId. */
|
|
12728
|
-
/** Ride-alongs (RideAlongSchema): notes assigned to this agent that no wake could
|
|
12729
|
-
* reach. Same array the contact/await replies carry — one queue, every carrier. */
|
|
12730
|
-
also: external_exports.array(RideAlongSchema).optional(),
|
|
12731
|
-
owedCallbacks: external_exports.array(
|
|
12732
|
-
external_exports.object({ parentId: external_exports.string(), trigger: CallbackTriggerSchema, note: external_exports.string() })
|
|
12733
|
-
),
|
|
12734
|
-
/** Work (either direction) you reported in_progress a while ago and never reported
|
|
12735
|
-
* completed — likely left half-done by this session or a prior one that crashed or
|
|
12736
|
-
* went idle. Report a real state (set_work_state) or continue the work. */
|
|
12737
|
-
stalled: external_exports.array(
|
|
12738
|
-
external_exports.object({ parentId: external_exports.string(), notificationId: external_exports.string(), title: external_exports.string().nullable(), startedAt: external_exports.string() })
|
|
12739
|
-
),
|
|
12740
|
-
/** The queue rail (#614, pending/design.md): the same replies + requests, grouped by
|
|
12741
|
-
* thread and ordered oldest-thread-first, so you work ONE thread at a time — fold all of
|
|
12742
|
-
* a thread's `items` into a single turn rather than interleaving threads. `busy` = the
|
|
12743
|
-
* thread already has a turn in progress (younger than the stall cutoff); let it finish and
|
|
12744
|
-
* ride the next turn. `items` are that thread's replies/requests in arrival order; the
|
|
12745
|
-
* full payload for each is in the flat `replies`/`requests` arrays (matched by
|
|
12746
|
-
* notificationId). Derived, never stored — a crashed agent recomputes it exactly. */
|
|
12747
|
-
threads: external_exports.array(
|
|
12748
|
-
external_exports.object({
|
|
12749
|
-
parentId: external_exports.string(),
|
|
12750
|
-
busy: external_exports.boolean(),
|
|
12751
|
-
items: external_exports.array(
|
|
12752
|
-
external_exports.object({
|
|
12753
|
-
kind: external_exports.enum(["reply", "request"]),
|
|
12754
|
-
notificationId: external_exports.string(),
|
|
12755
|
-
at: external_exports.string()
|
|
12756
|
-
})
|
|
12757
|
-
)
|
|
12758
|
-
})
|
|
12759
|
-
)
|
|
12760
|
-
});
|
|
12761
12674
|
var NotifyResponseSchema = external_exports.object({
|
|
12762
12675
|
notificationId: external_exports.string(),
|
|
12763
12676
|
workId: external_exports.string().uuid().optional(),
|
|
@@ -12820,6 +12733,13 @@ var UserResponseSchema = external_exports.object({
|
|
|
12820
12733
|
});
|
|
12821
12734
|
var VoiceKeySchema = external_exports.enum(["rachel", "george", "jessica", "brian", "lily"]);
|
|
12822
12735
|
var AgendaTurnSchema = external_exports.object({
|
|
12736
|
+
/** THE TURN'S IDENTITY (the first-sentence stream, 2026-09-09): the brain call that wrote
|
|
12737
|
+
* it and its place in that reply — `<brainCallId>:<index>`, with `:p` on the first
|
|
12738
|
+
* sentence a re-plan publishes ahead of the rest. A turn is spoken once, by this id: the
|
|
12739
|
+
* completion of a streamed re-plan carries the published sentence again, and the walk
|
|
12740
|
+
* drops what it already said by identity, never by the API's guess of what was polled.
|
|
12741
|
+
* Absent on plans nothing streams (a ring plan, a floor). */
|
|
12742
|
+
id: external_exports.string().optional(),
|
|
12823
12743
|
/** Twin coverage (#1089): sibling claim ids this asking turn's answer ALSO settles —
|
|
12824
12744
|
* the planner declares duplicates instead of asking them twice. */
|
|
12825
12745
|
coveredIds: external_exports.array(external_exports.string()).optional(),
|
|
@@ -12978,7 +12898,21 @@ var InboxItemSchema = external_exports.object({
|
|
|
12978
12898
|
/** E2EE: the agent's device X25519 public key to seal the user's answer BACK to (the
|
|
12979
12899
|
* sender the phone replies to). Sourced server-side from this item's pairing credential.
|
|
12980
12900
|
* Present only alongside `envelope`; the phone seals via sealAnswer(answer, this, id). */
|
|
12981
|
-
agentX25519: external_exports.string().optional()
|
|
12901
|
+
agentX25519: external_exports.string().optional(),
|
|
12902
|
+
/** THE TARGET FACTS A CARD RENDERS (#1796 point 5, 2026-09-11): the Delivery it is a view of,
|
|
12903
|
+
* that Delivery's kind, the request Entry, the Goals it answers for, the exact DecisionNeed (none
|
|
12904
|
+
* for a request that asks nothing), whether its content is sealed, and that Goal's state. The
|
|
12905
|
+
* answer writer (`POST /api/entries`) and the disposition (`close_delivery`) take their ids from
|
|
12906
|
+
* here. The server projects it (`apps/api/src/inbox/project.ts`); a client never builds it. */
|
|
12907
|
+
communication: external_exports.object({
|
|
12908
|
+
deliveryId: external_exports.string(),
|
|
12909
|
+
kind: external_exports.enum(["notification", "call"]),
|
|
12910
|
+
entryId: external_exports.string(),
|
|
12911
|
+
goalIds: external_exports.array(external_exports.string()),
|
|
12912
|
+
decisionNeedId: external_exports.string().optional(),
|
|
12913
|
+
sealed: external_exports.boolean(),
|
|
12914
|
+
goalState: external_exports.string().optional()
|
|
12915
|
+
}).optional()
|
|
12982
12916
|
});
|
|
12983
12917
|
var SnoozeRequestSchema = external_exports.object({
|
|
12984
12918
|
requestId: external_exports.string(),
|
|
@@ -13214,26 +13148,54 @@ var CreateRequestSchema = external_exports.object({
|
|
|
13214
13148
|
* the agent reads it via get_thread. Must belong to the requesting user. */
|
|
13215
13149
|
contextParentId: external_exports.string().optional()
|
|
13216
13150
|
});
|
|
13217
|
-
var
|
|
13218
|
-
/**
|
|
13219
|
-
|
|
13220
|
-
/**
|
|
13221
|
-
|
|
13222
|
-
/**
|
|
13223
|
-
|
|
13224
|
-
|
|
13225
|
-
|
|
13226
|
-
/**
|
|
13227
|
-
|
|
13228
|
-
|
|
13229
|
-
|
|
13230
|
-
|
|
13231
|
-
|
|
13232
|
-
|
|
13233
|
-
|
|
13234
|
-
|
|
13235
|
-
|
|
13236
|
-
|
|
13151
|
+
var QueueQuestionSchema = external_exports.object({
|
|
13152
|
+
/** The decision need's id — what an answer is accepted against. */
|
|
13153
|
+
id: external_exports.string(),
|
|
13154
|
+
/** The words that were asked, from the request Entry that asked them. */
|
|
13155
|
+
question: external_exports.string(),
|
|
13156
|
+
/** Where it was asked — which is where the ruling goes (`POST /api/entries`). Null only
|
|
13157
|
+
* for a need whose request Entry is carried by no interactive Delivery, which nothing
|
|
13158
|
+
* can answer. */
|
|
13159
|
+
deliveryId: external_exports.string().nullable().default(null),
|
|
13160
|
+
/** The Entry the ruling is about. */
|
|
13161
|
+
aboutId: external_exports.string().nullable().default(null),
|
|
13162
|
+
/** Empty for a free-text question. */
|
|
13163
|
+
options: external_exports.array(OptionSchema).default([]),
|
|
13164
|
+
select: external_exports.enum(["one", "many", "rank", "confirm", "text"]).default("text"),
|
|
13165
|
+
askedAt: external_exports.string(),
|
|
13166
|
+
/** Null while the question is open — which is how the page tells the two apart. */
|
|
13167
|
+
answeredAt: external_exports.string().nullable().default(null),
|
|
13168
|
+
/** The ruling in the person's own words, from the contribution that replied — not the
|
|
13169
|
+
* option id, which is not something anyone reads back. Null while it is open, and null
|
|
13170
|
+
* for a settled question whose reply carried nothing readable. */
|
|
13171
|
+
answer: external_exports.string().nullable().default(null)
|
|
13172
|
+
});
|
|
13173
|
+
var QueueItemSchema = external_exports.object({
|
|
13174
|
+
id: external_exports.string(),
|
|
13175
|
+
/** One-line headline — the first sentence of the outcome. */
|
|
13176
|
+
title: external_exports.string(),
|
|
13177
|
+
/** The outcome in full, verbatim: the person's own words are what an assignee sees. */
|
|
13178
|
+
intent: external_exports.string(),
|
|
13179
|
+
/** `ready` | `active` | `waiting` | `done` | `cancelled`, straight off the Goal. */
|
|
13180
|
+
state: external_exports.string(),
|
|
13181
|
+
/** Who holds it (a participant ref); null when nobody does yet. */
|
|
13182
|
+
assignee: external_exports.string().nullable().default(null),
|
|
13183
|
+
/** What the agent last said it was doing; null if it has said nothing. */
|
|
13184
|
+
progress: external_exports.string().nullable().default(null),
|
|
13185
|
+
reviewPending: external_exports.boolean().default(false),
|
|
13186
|
+
dueAt: external_exports.string().nullable().default(null),
|
|
13187
|
+
/** The Goal this one was opened under; null at the root. */
|
|
13188
|
+
parentGoalId: external_exports.string().nullable().default(null),
|
|
13189
|
+
/** Goals opened under this one — only those the same list holds. */
|
|
13190
|
+
childGoalIds: external_exports.array(external_exports.string()).default([]),
|
|
13191
|
+
/** Goals this one waits on (start or finish gates). */
|
|
13192
|
+
dependencyGoalIds: external_exports.array(external_exports.string()).default([]),
|
|
13193
|
+
/** True while any gate is on a Goal that is not done — the walk draws it dashed. */
|
|
13194
|
+
blocked: external_exports.boolean().default(false),
|
|
13195
|
+
/** Every decision need on it, open or settled — the page decides which to show. */
|
|
13196
|
+
questions: external_exports.array(QueueQuestionSchema).default([]),
|
|
13197
|
+
createdAt: external_exports.string(),
|
|
13198
|
+
updatedAt: external_exports.string().nullable().default(null)
|
|
13237
13199
|
});
|
|
13238
13200
|
var NoteSourceSchema = external_exports.enum(["app", "call"]);
|
|
13239
13201
|
var NoteStatusSchema = external_exports.enum(["open", "assigned", "in_progress", "done"]);
|
|
@@ -13362,6 +13324,8 @@ var AcceptTriageResultSchema = external_exports.object({
|
|
|
13362
13324
|
failed: external_exports.array(external_exports.object({ noteId: external_exports.string(), reason: external_exports.string() }))
|
|
13363
13325
|
});
|
|
13364
13326
|
var DeliveryModeSchema = external_exports.enum(["poll", "self_hosted"]);
|
|
13327
|
+
var WAKE_EVENT = "wake";
|
|
13328
|
+
var wakeChannel = (tokenId) => `wake:${tokenId}`;
|
|
13365
13329
|
var RegisterDeliverySchema = external_exports.object({ mode: DeliveryModeSchema });
|
|
13366
13330
|
var OAuthStartSchema = external_exports.object({
|
|
13367
13331
|
provider: external_exports.enum(["cma"]),
|
|
@@ -13370,15 +13334,29 @@ var OAuthStartSchema = external_exports.object({
|
|
|
13370
13334
|
var DeliveryConfigSchema = external_exports.object({
|
|
13371
13335
|
tokenId: external_exports.string(),
|
|
13372
13336
|
mode: DeliveryModeSchema,
|
|
13373
|
-
/** null when the
|
|
13374
|
-
*
|
|
13337
|
+
/** null when the deployment has no anon key configured. `self_hosted` is then REFUSED
|
|
13338
|
+
* (503 `self_hosted_unavailable`) rather than registered, so a self_hosted config always
|
|
13339
|
+
* carries credentials; only a `poll` registration can come back with null here. */
|
|
13375
13340
|
realtime: external_exports.object({ url: external_exports.string(), anonKey: external_exports.string() }).nullable()
|
|
13376
13341
|
});
|
|
13377
13342
|
var StatusSchema = external_exports.object({
|
|
13378
13343
|
name: external_exports.string(),
|
|
13379
13344
|
sessionMode: external_exports.enum(["default", "all_calls", "silent"]),
|
|
13380
13345
|
/** A phone is registered for push/ring (any push token on the account). */
|
|
13381
|
-
phone: external_exports.boolean()
|
|
13346
|
+
phone: external_exports.boolean(),
|
|
13347
|
+
/** HOW MANY THINGS ARE WAITING ON THIS IDENTITY — replies it never collected and requests
|
|
13348
|
+
* it never picked up. THE SAME NUMBER the harness's wake gate reads
|
|
13349
|
+
* (`pendingSummary.unacknowledged`), from the same function, because a statusline saying
|
|
13350
|
+
* zero while the sweep sees one is two ideas of "waiting".
|
|
13351
|
+
*
|
|
13352
|
+
* Why it is here at all (owner, 2026-09-07): nothing can interrupt an idle agent process
|
|
13353
|
+
* that nobody spawned, so a terminal session only learns of work by asking. The harness
|
|
13354
|
+
* used to paper over that by spawning a SECOND process on the identity; now it stands
|
|
13355
|
+
* back, correctly, and the person sitting at the terminal is the one who can act. A
|
|
13356
|
+
* coffee-beans request sat unread for three days.
|
|
13357
|
+
*
|
|
13358
|
+
* Optional: an older API sends no field, and the statusline then renders exactly as before. */
|
|
13359
|
+
waiting: external_exports.number().int().nonnegative().optional()
|
|
13382
13360
|
});
|
|
13383
13361
|
var EnvelopeRecipientSchema = external_exports.object({
|
|
13384
13362
|
keyId: external_exports.string(),
|
|
@@ -13544,238 +13522,11 @@ var FeedbackOutcomeSchema = external_exports.object({
|
|
|
13544
13522
|
message: external_exports.string(),
|
|
13545
13523
|
childIds: external_exports.array(external_exports.string()).optional()
|
|
13546
13524
|
});
|
|
13547
|
-
var CONTACT_SCHEMA = contactSchemaFrom({
|
|
13548
|
-
ask: NotifyRequestFields.shape.ask,
|
|
13549
|
-
waiting: NotifyRequestFields.shape.waiting,
|
|
13550
|
-
options: NotifyRequestFields.shape.options,
|
|
13551
|
-
channel: NotifyRequestFields.shape.channel,
|
|
13552
|
-
parentId: NotifyRequestFields.shape.parentId,
|
|
13553
|
-
workId: NotifyRequestFields.shape.workId,
|
|
13554
|
-
goalId: NotifyRequestFields.shape.goalId
|
|
13555
|
-
});
|
|
13556
13525
|
var import_tweetnacl = __toESM2(require_nacl_fast(), 1);
|
|
13557
13526
|
var import_tweetnacl2 = __toESM2(require_nacl_fast(), 1);
|
|
13558
13527
|
var import_tweetnacl3 = __toESM2(require_nacl_fast(), 1);
|
|
13559
13528
|
var import_tweetnacl4 = __toESM2(require_nacl_fast(), 1);
|
|
13560
13529
|
var import_tweetnacl5 = __toESM2(require_nacl_fast(), 1);
|
|
13561
|
-
function utf8ToBytes(s) {
|
|
13562
|
-
const out = [];
|
|
13563
|
-
for (let i = 0; i < s.length; i++) {
|
|
13564
|
-
const c = s.charCodeAt(i);
|
|
13565
|
-
if (c < 128) out.push(c);
|
|
13566
|
-
else if (c < 2048) out.push(192 | c >> 6, 128 | c & 63);
|
|
13567
|
-
else if (c >= 55296 && c <= 56319) {
|
|
13568
|
-
const c2 = s.charCodeAt(i + 1);
|
|
13569
|
-
if (c2 >= 56320 && c2 <= 57343) {
|
|
13570
|
-
i++;
|
|
13571
|
-
const cp = 65536 + ((c & 1023) << 10) + (c2 & 1023);
|
|
13572
|
-
out.push(240 | cp >> 18, 128 | cp >> 12 & 63, 128 | cp >> 6 & 63, 128 | cp & 63);
|
|
13573
|
-
} else out.push(239, 191, 189);
|
|
13574
|
-
} else if (c >= 56320 && c <= 57343) out.push(239, 191, 189);
|
|
13575
|
-
else out.push(224 | c >> 12, 128 | c >> 6 & 63, 128 | c & 63);
|
|
13576
|
-
}
|
|
13577
|
-
return Uint8Array.from(out);
|
|
13578
|
-
}
|
|
13579
|
-
function bytesToUtf8(b) {
|
|
13580
|
-
let out = "";
|
|
13581
|
-
for (let i = 0; i < b.length; ) {
|
|
13582
|
-
const c = b[i++];
|
|
13583
|
-
if (c < 128) out += String.fromCharCode(c);
|
|
13584
|
-
else if (c < 224) out += String.fromCharCode((c & 31) << 6 | b[i++] & 63);
|
|
13585
|
-
else if (c < 240) out += String.fromCharCode((c & 15) << 12 | (b[i++] & 63) << 6 | b[i++] & 63);
|
|
13586
|
-
else {
|
|
13587
|
-
const cp = (c & 7) << 18 | (b[i++] & 63) << 12 | (b[i++] & 63) << 6 | b[i++] & 63;
|
|
13588
|
-
const u = cp - 65536;
|
|
13589
|
-
out += String.fromCharCode(55296 + (u >> 10), 56320 + (u & 1023));
|
|
13590
|
-
}
|
|
13591
|
-
}
|
|
13592
|
-
return out;
|
|
13593
|
-
}
|
|
13594
|
-
var B64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
13595
|
-
function toB64(bytes) {
|
|
13596
|
-
let out = "";
|
|
13597
|
-
for (let i = 0; i < bytes.length; i += 3) {
|
|
13598
|
-
const b0 = bytes[i];
|
|
13599
|
-
const has1 = i + 1 < bytes.length;
|
|
13600
|
-
const has2 = i + 2 < bytes.length;
|
|
13601
|
-
const b1 = has1 ? bytes[i + 1] : 0;
|
|
13602
|
-
const b2 = has2 ? bytes[i + 2] : 0;
|
|
13603
|
-
out += B64[b0 >> 2] + B64[(b0 & 3) << 4 | b1 >> 4];
|
|
13604
|
-
out += has1 ? B64[(b1 & 15) << 2 | b2 >> 6] : "=";
|
|
13605
|
-
out += has2 ? B64[b2 & 63] : "=";
|
|
13606
|
-
}
|
|
13607
|
-
return out;
|
|
13608
|
-
}
|
|
13609
|
-
function fromB64Lenient(s) {
|
|
13610
|
-
const clean = s.replace(/[^A-Za-z0-9+/]/g, "");
|
|
13611
|
-
const out = [];
|
|
13612
|
-
for (let i = 0; i < clean.length; i += 4) {
|
|
13613
|
-
const c0 = B64.indexOf(clean[i]);
|
|
13614
|
-
const c1 = B64.indexOf(clean[i + 1] ?? "A");
|
|
13615
|
-
const c2 = clean[i + 2] !== void 0 ? B64.indexOf(clean[i + 2]) : -1;
|
|
13616
|
-
const c3 = clean[i + 3] !== void 0 ? B64.indexOf(clean[i + 3]) : -1;
|
|
13617
|
-
out.push(c0 << 2 | c1 >> 4);
|
|
13618
|
-
if (c2 >= 0) out.push((c1 & 15) << 4 | c2 >> 2);
|
|
13619
|
-
if (c3 >= 0) out.push((c2 & 3) << 6 | c3);
|
|
13620
|
-
}
|
|
13621
|
-
return Uint8Array.from(out);
|
|
13622
|
-
}
|
|
13623
|
-
function fromB64(s) {
|
|
13624
|
-
const bytes = fromB64Lenient(s);
|
|
13625
|
-
if (toB64(bytes) !== s) throw new Error("non-canonical base64");
|
|
13626
|
-
return bytes;
|
|
13627
|
-
}
|
|
13628
|
-
function concat(parts) {
|
|
13629
|
-
let n = 0;
|
|
13630
|
-
for (const p of parts) n += p.length;
|
|
13631
|
-
const out = new Uint8Array(n);
|
|
13632
|
-
let o = 0;
|
|
13633
|
-
for (const p of parts) {
|
|
13634
|
-
out.set(p, o);
|
|
13635
|
-
o += p.length;
|
|
13636
|
-
}
|
|
13637
|
-
return out;
|
|
13638
|
-
}
|
|
13639
|
-
function u16be(n) {
|
|
13640
|
-
return Uint8Array.of(n >>> 8 & 255, n & 255);
|
|
13641
|
-
}
|
|
13642
|
-
function u32be(n) {
|
|
13643
|
-
return Uint8Array.of(n >>> 24 & 255, n >>> 16 & 255, n >>> 8 & 255, n & 255);
|
|
13644
|
-
}
|
|
13645
|
-
function readU32be(b, o) {
|
|
13646
|
-
return (b[o] << 24 | b[o + 1] << 16 | b[o + 2] << 8 | b[o + 3]) >>> 0;
|
|
13647
|
-
}
|
|
13648
|
-
function lp(b) {
|
|
13649
|
-
return concat([u32be(b.length), b]);
|
|
13650
|
-
}
|
|
13651
|
-
function eqCt(a, b) {
|
|
13652
|
-
if (a.length !== b.length) return false;
|
|
13653
|
-
let d = 0;
|
|
13654
|
-
for (let i = 0; i < a.length; i++) d |= a[i] ^ b[i];
|
|
13655
|
-
return d === 0;
|
|
13656
|
-
}
|
|
13657
|
-
var ok = false;
|
|
13658
|
-
function ensureCsprng() {
|
|
13659
|
-
if (ok) return;
|
|
13660
|
-
let a;
|
|
13661
|
-
let b;
|
|
13662
|
-
try {
|
|
13663
|
-
a = import_tweetnacl.default.randomBytes(32);
|
|
13664
|
-
b = import_tweetnacl.default.randomBytes(32);
|
|
13665
|
-
} catch {
|
|
13666
|
-
throw new Error("paigy-crypto: no secure RNG available \u2014 refusing to generate keys (fail-closed)");
|
|
13667
|
-
}
|
|
13668
|
-
if (a.every((x) => x === 0) || eqCt(a, b)) {
|
|
13669
|
-
throw new Error("paigy-crypto: RNG self-test failed (constant/low-entropy output)");
|
|
13670
|
-
}
|
|
13671
|
-
ok = true;
|
|
13672
|
-
}
|
|
13673
|
-
function randomBytes(n) {
|
|
13674
|
-
ensureCsprng();
|
|
13675
|
-
return import_tweetnacl.default.randomBytes(n);
|
|
13676
|
-
}
|
|
13677
|
-
function keyId(publicKeyB64) {
|
|
13678
|
-
return toB64(import_tweetnacl2.default.hash(fromB64(publicKeyB64)).slice(0, 8));
|
|
13679
|
-
}
|
|
13680
|
-
function credentialBody(c) {
|
|
13681
|
-
return concat([
|
|
13682
|
-
lp(utf8ToBytes("paigy-device-credential-v1")),
|
|
13683
|
-
lp(utf8ToBytes(c.deviceId)),
|
|
13684
|
-
lp(utf8ToBytes(c.kind)),
|
|
13685
|
-
lp(fromB64(c.x25519Pub)),
|
|
13686
|
-
lp(fromB64(c.ed25519Pub))
|
|
13687
|
-
]);
|
|
13688
|
-
}
|
|
13689
|
-
function verifyDeviceCredential(cred, uikPublicB64) {
|
|
13690
|
-
const { sig, ...fields } = cred;
|
|
13691
|
-
try {
|
|
13692
|
-
return import_tweetnacl2.default.sign.detached.verify(credentialBody(fields), fromB64(sig), fromB64(uikPublicB64));
|
|
13693
|
-
} catch {
|
|
13694
|
-
return false;
|
|
13695
|
-
}
|
|
13696
|
-
}
|
|
13697
|
-
var ENVELOPE_VERSION = 1;
|
|
13698
|
-
var ENVELOPE_ALG = "x25519-xsalsa20poly1305";
|
|
13699
|
-
function padme(L) {
|
|
13700
|
-
if (L < 2) return L;
|
|
13701
|
-
const E = Math.floor(Math.log2(L));
|
|
13702
|
-
const S = Math.floor(Math.log2(E)) + 1;
|
|
13703
|
-
const lastBits = E - S;
|
|
13704
|
-
if (lastBits <= 0) return L;
|
|
13705
|
-
const mask = (1 << lastBits) - 1;
|
|
13706
|
-
return L + mask & ~mask;
|
|
13707
|
-
}
|
|
13708
|
-
function canonicalHeader(msgId, hdr) {
|
|
13709
|
-
const parts = [
|
|
13710
|
-
u16be(ENVELOPE_VERSION),
|
|
13711
|
-
lp(utf8ToBytes(ENVELOPE_ALG)),
|
|
13712
|
-
lp(msgId),
|
|
13713
|
-
lp(utf8ToBytes(hdr.field)),
|
|
13714
|
-
lp(utf8ToBytes(hdr.kind)),
|
|
13715
|
-
lp(utf8ToBytes(hdr.senderRole)),
|
|
13716
|
-
u32be(hdr.seq),
|
|
13717
|
-
u32be(hdr.recipientKeyIds.length),
|
|
13718
|
-
...hdr.recipientKeyIds.map((k) => lp(utf8ToBytes(k)))
|
|
13719
|
-
];
|
|
13720
|
-
return concat(parts);
|
|
13721
|
-
}
|
|
13722
|
-
function seal(input) {
|
|
13723
|
-
ensureCsprng();
|
|
13724
|
-
if (input.recipients.length === 0) throw new Error("seal: no recipients");
|
|
13725
|
-
const kind = input.kind ?? "";
|
|
13726
|
-
const recs = input.recipients.map((pub) => ({ pub, id: keyId(pub) })).sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
|
|
13727
|
-
const hdr = {
|
|
13728
|
-
field: input.field,
|
|
13729
|
-
kind,
|
|
13730
|
-
senderRole: input.senderRole,
|
|
13731
|
-
recipientKeyIds: recs.map((r) => r.id),
|
|
13732
|
-
seq: input.seq
|
|
13733
|
-
};
|
|
13734
|
-
const msgIdBytes = input.msgId ? fromB64(input.msgId) : randomBytes(16);
|
|
13735
|
-
const cek = randomBytes(32);
|
|
13736
|
-
const canon = canonicalHeader(msgIdBytes, hdr);
|
|
13737
|
-
const plaintext = concat([u32be(canon.length), canon, u32be(input.body.length), input.body]);
|
|
13738
|
-
const padded = new Uint8Array(padme(plaintext.length));
|
|
13739
|
-
padded.set(plaintext);
|
|
13740
|
-
const nonce = randomBytes(24);
|
|
13741
|
-
const ct = import_tweetnacl3.default.secretbox(padded, nonce, cek);
|
|
13742
|
-
const recipients = recs.map((r) => {
|
|
13743
|
-
const eph = import_tweetnacl3.default.box.keyPair();
|
|
13744
|
-
const wnonce = randomBytes(24);
|
|
13745
|
-
const wrap = import_tweetnacl3.default.box(cek, wnonce, fromB64(r.pub), eph.secretKey);
|
|
13746
|
-
return { keyId: r.id, epk: toB64(eph.publicKey), wnonce: toB64(wnonce), wrap: toB64(wrap) };
|
|
13747
|
-
});
|
|
13748
|
-
return {
|
|
13749
|
-
v: ENVELOPE_VERSION,
|
|
13750
|
-
alg: ENVELOPE_ALG,
|
|
13751
|
-
msgId: toB64(msgIdBytes),
|
|
13752
|
-
hdr,
|
|
13753
|
-
recipients,
|
|
13754
|
-
nonce: toB64(nonce),
|
|
13755
|
-
ct: toB64(ct)
|
|
13756
|
-
};
|
|
13757
|
-
}
|
|
13758
|
-
function open(envelope, myKeyId, mySecretKeyB64) {
|
|
13759
|
-
if (envelope.v !== ENVELOPE_VERSION) throw new Error(`unsupported envelope version ${String(envelope.v)}`);
|
|
13760
|
-
if (envelope.alg !== ENVELOPE_ALG) throw new Error(`unsupported alg ${String(envelope.alg)}`);
|
|
13761
|
-
const rec = envelope.recipients.find((r) => r.keyId === myKeyId);
|
|
13762
|
-
if (!rec) throw new Error("not a recipient of this envelope");
|
|
13763
|
-
const cek = import_tweetnacl3.default.box.open(fromB64(rec.wrap), fromB64(rec.wnonce), fromB64(rec.epk), fromB64(mySecretKeyB64));
|
|
13764
|
-
if (!cek) throw new Error("CEK unwrap failed (wrong key or tampered)");
|
|
13765
|
-
const padded = import_tweetnacl3.default.secretbox.open(fromB64(envelope.ct), fromB64(envelope.nonce), cek);
|
|
13766
|
-
if (!padded) throw new Error("body authentication failed (tampered ciphertext)");
|
|
13767
|
-
if (padded.length < 8) throw new Error("malformed sealed plaintext");
|
|
13768
|
-
const canonLen = readU32be(padded, 0);
|
|
13769
|
-
if (canonLen < 0 || 8 + canonLen > padded.length) throw new Error("malformed sealed plaintext");
|
|
13770
|
-
const sealedCanon = padded.slice(4, 4 + canonLen);
|
|
13771
|
-
const bodyLen = readU32be(padded, 4 + canonLen);
|
|
13772
|
-
const bodyStart = 8 + canonLen;
|
|
13773
|
-
if (bodyStart + bodyLen > padded.length) throw new Error("malformed sealed plaintext");
|
|
13774
|
-
const body = padded.slice(bodyStart, bodyStart + bodyLen);
|
|
13775
|
-
const expected = canonicalHeader(fromB64(envelope.msgId), envelope.hdr);
|
|
13776
|
-
if (!eqCt(sealedCanon, expected)) throw new Error("header mismatch (tampered metadata)");
|
|
13777
|
-
return { header: envelope.hdr, body };
|
|
13778
|
-
}
|
|
13779
13530
|
function sessionSlot(sessionId2) {
|
|
13780
13531
|
const id = sessionId2 ?? sessionId();
|
|
13781
13532
|
return `session:${id.slice(0, 8)}`;
|
|
@@ -13883,45 +13634,6 @@ function readKeyFile() {
|
|
|
13883
13634
|
return null;
|
|
13884
13635
|
}
|
|
13885
13636
|
}
|
|
13886
|
-
async function fetchRoster(token) {
|
|
13887
|
-
const res = await reach(`${BACKEND_URL}/api/roster`, { headers: { authorization: `Bearer ${token}` } });
|
|
13888
|
-
if (!res.ok) return null;
|
|
13889
|
-
return await res.json().catch(() => null);
|
|
13890
|
-
}
|
|
13891
|
-
function verifiedRecipients(roster, pinnedUikPub) {
|
|
13892
|
-
return roster.devices.filter((cred) => verifyDeviceCredential(cred, pinnedUikPub)).map((cred) => cred.x25519Pub);
|
|
13893
|
-
}
|
|
13894
|
-
function sealFields(content, recipients) {
|
|
13895
|
-
if (recipients.length === 0) throw new Error("sealFields: no verified recipients");
|
|
13896
|
-
const seq = Math.floor(Date.now() / 1e3);
|
|
13897
|
-
const out = {};
|
|
13898
|
-
for (const field of ["context", "options", "visuals"]) {
|
|
13899
|
-
if (content[field] === void 0) continue;
|
|
13900
|
-
out[field] = seal({
|
|
13901
|
-
body: utf8ToBytes(JSON.stringify(content[field])),
|
|
13902
|
-
field,
|
|
13903
|
-
senderRole: "agent",
|
|
13904
|
-
seq,
|
|
13905
|
-
recipients
|
|
13906
|
-
});
|
|
13907
|
-
}
|
|
13908
|
-
return out;
|
|
13909
|
-
}
|
|
13910
|
-
function openSealedAnswer(sealed, keypair) {
|
|
13911
|
-
const myKeyId = keyId(keypair.x25519.publicKey);
|
|
13912
|
-
const { header, body } = open(sealed.envelope, myKeyId, keypair.x25519.secretKey);
|
|
13913
|
-
if (header.field !== "answer" || header.senderRole !== "user") {
|
|
13914
|
-
throw new Error("sealed answer has the wrong envelope field/role \u2014 refusing (possible replay/misattribution)");
|
|
13915
|
-
}
|
|
13916
|
-
const parsed = UserAnswerSchema.safeParse(JSON.parse(bytesToUtf8(body)));
|
|
13917
|
-
if (!parsed.success) throw new Error("sealed answer body is not a valid UserAnswer");
|
|
13918
|
-
const answer = parsed.data;
|
|
13919
|
-
const bodyIsIgnored = answer.kind === "ignored";
|
|
13920
|
-
if (sealed.ignored !== bodyIsIgnored) {
|
|
13921
|
-
throw new Error("sealed answer tamper: `ignored` hint disagrees with the decrypted body");
|
|
13922
|
-
}
|
|
13923
|
-
return answer;
|
|
13924
|
-
}
|
|
13925
13637
|
var UnpairedError = class extends Error {
|
|
13926
13638
|
constructor() {
|
|
13927
13639
|
super("Not paired with Paigy \u2014 no valid token (the API answered 401). Run the pairing flow, then retry.");
|
|
@@ -13932,85 +13644,47 @@ function ensureAuthed(res) {
|
|
|
13932
13644
|
if (res.status === 401) throw new UnpairedError();
|
|
13933
13645
|
return res;
|
|
13934
13646
|
}
|
|
13935
|
-
|
|
13936
|
-
|
|
13937
|
-
|
|
13938
|
-
|
|
13939
|
-
|
|
13940
|
-
|
|
13941
|
-
|
|
13942
|
-
|
|
13943
|
-
|
|
13944
|
-
|
|
13945
|
-
|
|
13946
|
-
|
|
13947
|
-
|
|
13948
|
-
|
|
13949
|
-
|
|
13950
|
-
|
|
13951
|
-
|
|
13952
|
-
...req.repo !== void 0 ? { repo: req.repo } : {},
|
|
13953
|
-
...req.branch !== void 0 ? { branch: req.branch } : {}
|
|
13954
|
-
};
|
|
13955
|
-
const envelope = sealFields({ context: sealedContext, options: req.options, visuals: req.visuals }, recipients);
|
|
13956
|
-
const { context: _c, options: _o, visuals: _v, repo: _r, branch: _b, ...meta } = req;
|
|
13957
|
-
return { ...meta, envelope };
|
|
13647
|
+
var ApiError = class extends Error {
|
|
13648
|
+
status;
|
|
13649
|
+
body;
|
|
13650
|
+
constructor(what, status, text) {
|
|
13651
|
+
super(`${what} failed: ${status} ${text}`);
|
|
13652
|
+
this.name = "ApiError";
|
|
13653
|
+
this.status = status;
|
|
13654
|
+
let body = void 0;
|
|
13655
|
+
try {
|
|
13656
|
+
body = JSON.parse(text);
|
|
13657
|
+
} catch {
|
|
13658
|
+
}
|
|
13659
|
+
this.body = body;
|
|
13660
|
+
}
|
|
13661
|
+
};
|
|
13662
|
+
async function fail(what, res) {
|
|
13663
|
+
throw new ApiError(what, res.status, await res.text());
|
|
13958
13664
|
}
|
|
13959
|
-
async function
|
|
13665
|
+
async function claimGoal(goalId, opts = {}) {
|
|
13960
13666
|
const token = authToken(opts.token) ?? "";
|
|
13961
|
-
|
|
13962
|
-
|
|
13963
|
-
const shaped = deriveAsk(req);
|
|
13964
|
-
if (!shaped.context || !shaped.select) throw new Error("Goal-scoped contact requires a shaped request or ask");
|
|
13965
|
-
if (shaped.channel === "call" || shaped.urgency === "call") {
|
|
13966
|
-
throw new Error("Goal-scoped call contact is not available yet; use a message contact");
|
|
13967
|
-
}
|
|
13968
|
-
if (shaped.urgency && shaped.urgency !== "inbox") throw new Error("Goal-scoped contact currently supports inbox delivery only");
|
|
13969
|
-
if (shaped.points?.length) throw new Error("Goal-scoped multi-part contact is not available yet; send one decision at a time");
|
|
13970
|
-
const content = [shaped.context.title, ...shaped.context.description].filter(Boolean).join("\n\n");
|
|
13971
|
-
const res2 = ensureAuthed(await reach(`${BACKEND_URL}/api/goals/${encodeURIComponent(req.goalId)}/contact`, {
|
|
13972
|
-
method: "POST",
|
|
13973
|
-
headers: { "content-type": "application/json", authorization: `Bearer ${token}`, "x-paigy-model": "goal-entry-v1" },
|
|
13974
|
-
body: JSON.stringify({
|
|
13975
|
-
operationId: randomUUID2(),
|
|
13976
|
-
threadId: shaped.parentId ?? null,
|
|
13977
|
-
content,
|
|
13978
|
-
select: shaped.select,
|
|
13979
|
-
options: shaped.options ?? [],
|
|
13980
|
-
blocking: shaped.blocking,
|
|
13981
|
-
channel: "message"
|
|
13982
|
-
})
|
|
13983
|
-
}));
|
|
13984
|
-
if (!res2.ok) throw new Error(`goal contact failed: ${res2.status} ${await res2.text()}`);
|
|
13985
|
-
return await res2.json();
|
|
13986
|
-
}
|
|
13987
|
-
const body = await sealForE2ee(req, token);
|
|
13988
|
-
const res = ensureAuthed(await reach(`${BACKEND_URL}/api/notify`, {
|
|
13989
|
-
method: "POST",
|
|
13990
|
-
headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
|
|
13991
|
-
body: JSON.stringify(body)
|
|
13992
|
-
}));
|
|
13993
|
-
if (!res.ok) throw new Error(`notify failed: ${res.status} ${await res.text()}`);
|
|
13667
|
+
const res = ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/goals/claim`, { method: "POST", headers: { "content-type": "application/json", authorization: `Bearer ${token}`, "x-paigy-model": "goal-entry-v1" }, body: JSON.stringify(goalId ? { goalId } : {}) }));
|
|
13668
|
+
if (!res.ok) await fail("claim_goal", res);
|
|
13994
13669
|
return await res.json();
|
|
13995
13670
|
}
|
|
13996
|
-
async function
|
|
13997
|
-
const token = authToken(opts.token);
|
|
13998
|
-
const res = ensureAuthed(await reach(`${BACKEND_URL}/api/
|
|
13999
|
-
|
|
14000
|
-
|
|
14001
|
-
"x-paigy-await-ack": "1"
|
|
14002
|
-
}
|
|
13671
|
+
async function getGoal(goalId, opts = {}) {
|
|
13672
|
+
const token = authToken(opts.token) ?? "";
|
|
13673
|
+
const res = ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/goals/${encodeURIComponent(goalId)}`, {
|
|
13674
|
+
method: "GET",
|
|
13675
|
+
headers: { authorization: `Bearer ${token}`, "x-paigy-model": "goal-entry-v1" }
|
|
14003
13676
|
}));
|
|
14004
|
-
if (!res.ok)
|
|
13677
|
+
if (!res.ok) await fail("get_goal", res);
|
|
14005
13678
|
return await res.json();
|
|
14006
13679
|
}
|
|
14007
|
-
async function
|
|
14008
|
-
const
|
|
14009
|
-
|
|
14010
|
-
}));
|
|
14011
|
-
if (!res.ok)
|
|
13680
|
+
async function updateGoal(goalId, input, opts = {}) {
|
|
13681
|
+
const token = authToken(opts.token) ?? "";
|
|
13682
|
+
const operationId = input.operationId ?? randomUUID2();
|
|
13683
|
+
const res = ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/goals/${encodeURIComponent(goalId)}`, { method: "PATCH", headers: { "content-type": "application/json", authorization: `Bearer ${token}`, "x-paigy-model": "goal-entry-v1" }, body: JSON.stringify({ ...input, operationId }) }));
|
|
13684
|
+
if (!res.ok) await fail("update_goal", res);
|
|
14012
13685
|
return await res.json();
|
|
14013
13686
|
}
|
|
13687
|
+
var AWAIT_WINDOW_MS = 45e3;
|
|
14014
13688
|
var tokenOverride = null;
|
|
14015
13689
|
var authToken = (explicit) => explicit ?? tokenOverride ?? readToken();
|
|
14016
13690
|
async function whoAmI(opts = {}) {
|
|
@@ -14043,6 +13717,15 @@ async function heartbeat(runtime, opts = {}) {
|
|
|
14043
13717
|
}));
|
|
14044
13718
|
if (!res.ok) throw new Error(`heartbeat failed: ${res.status}`);
|
|
14045
13719
|
}
|
|
13720
|
+
async function registerDelivery(mode, opts = {}) {
|
|
13721
|
+
const res = ensureAuthed(await reach(`${BACKEND_URL}/api/delivery`, {
|
|
13722
|
+
method: "POST",
|
|
13723
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${authToken(opts.token)}` },
|
|
13724
|
+
body: JSON.stringify({ mode })
|
|
13725
|
+
}));
|
|
13726
|
+
if (!res.ok) throw new Error(`register_delivery failed: ${res.status} ${await res.text()}`);
|
|
13727
|
+
return await res.json();
|
|
13728
|
+
}
|
|
14046
13729
|
async function listNotes(opts = {}) {
|
|
14047
13730
|
const res = ensureAuthed(await reach(`${BACKEND_URL}/api/notes`, {
|
|
14048
13731
|
headers: { authorization: `Bearer ${authToken(opts.token)}` }
|
|
@@ -14090,293 +13773,155 @@ async function dismissTriage(proposalId, opts = {}) {
|
|
|
14090
13773
|
if (!res.ok) throw new Error(`dismiss_triage failed: ${res.status} ${await res.text()}`);
|
|
14091
13774
|
return await res.json();
|
|
14092
13775
|
}
|
|
14093
|
-
|
|
14094
|
-
|
|
14095
|
-
|
|
14096
|
-
|
|
14097
|
-
|
|
14098
|
-
|
|
14099
|
-
|
|
14100
|
-
|
|
14101
|
-
|
|
14102
|
-
|
|
14103
|
-
|
|
14104
|
-
|
|
14105
|
-
|
|
14106
|
-
|
|
14107
|
-
|
|
14108
|
-
|
|
14109
|
-
|
|
14110
|
-
|
|
14111
|
-
|
|
14112
|
-
|
|
14113
|
-
|
|
14114
|
-
|
|
14115
|
-
|
|
14116
|
-
);
|
|
14117
|
-
|
|
14118
|
-
|
|
14119
|
-
}
|
|
14120
|
-
|
|
14121
|
-
const
|
|
14122
|
-
|
|
14123
|
-
|
|
14124
|
-
const
|
|
14125
|
-
|
|
14126
|
-
return { ...plaintext, answer };
|
|
14127
|
-
}
|
|
14128
|
-
function deliveryOf(item) {
|
|
14129
|
-
if (item.type !== "reply" || !("delivery" in item) || item.delivery === void 0) {
|
|
14130
|
-
return null;
|
|
14131
|
-
}
|
|
14132
|
-
const delivery = item.delivery;
|
|
14133
|
-
if (!delivery || typeof delivery.leaseId !== "string" || !delivery.leaseId || typeof delivery.expiresAt !== "string" || !delivery.expiresAt) {
|
|
14134
|
-
throw new Error(
|
|
14135
|
-
"Paigy returned invalid reply-delivery metadata; the answer was not acknowledged and remains recoverable."
|
|
14136
|
-
);
|
|
14137
|
-
}
|
|
14138
|
-
return delivery;
|
|
14139
|
-
}
|
|
14140
|
-
async function responseDetail(res) {
|
|
13776
|
+
async function contact(input, opts = {}) {
|
|
13777
|
+
const parsed = ContactSchema.parse(input);
|
|
13778
|
+
opts.signal?.throwIfAborted();
|
|
13779
|
+
const send2 = opts.reach ?? reach;
|
|
13780
|
+
const headers = { "content-type": "application/json", authorization: `Bearer ${authToken(opts.token) ?? ""}`, "x-paigy-model": "goal-entry-v1" };
|
|
13781
|
+
let deliveryId;
|
|
13782
|
+
if ("deliveryId" in parsed) deliveryId = parsed.deliveryId;
|
|
13783
|
+
else {
|
|
13784
|
+
if (!opts.reach && readKeyFile()?.e2ee) throw new Error("Goal contact does not support E2EE yet; refusing to send plaintext.");
|
|
13785
|
+
const shaped = deriveAsk(NotifyRequestSchema.parse({ ask: parsed.ask, waiting: parsed.waiting, options: parsed.options }));
|
|
13786
|
+
const res = ensureAuthed(await send2(`${BACKEND_URL}/api/goals/${parsed.goalIds[0]}/contact`, {
|
|
13787
|
+
method: "POST",
|
|
13788
|
+
headers,
|
|
13789
|
+
signal: opts.signal,
|
|
13790
|
+
body: JSON.stringify({ operationId: opts.operationId ?? randomUUID3(), threadId: parsed.threadId ?? null, content: parsed.ask, select: shaped.select, options: shaped.options ?? [], blocking: parsed.waiting === "hard", channel: parsed.channel === "call" ? "call" : "message" })
|
|
13791
|
+
}));
|
|
13792
|
+
if (!res.ok) await fail("goal contact", res);
|
|
13793
|
+
const receipt = await res.json();
|
|
13794
|
+
deliveryId = receipt.deliveryId ?? receipt.callId ?? receipt.notificationId ?? "";
|
|
13795
|
+
if (!deliveryId) throw new Error("Goal contact returned no Delivery identity");
|
|
13796
|
+
}
|
|
13797
|
+
const read = async (signal2) => {
|
|
13798
|
+
const res = ensureAuthed(await send2(`${BACKEND_URL}/api/deliveries/${encodeURIComponent(deliveryId)}`, { headers, signal: signal2 }));
|
|
13799
|
+
if (!res.ok) await fail("read_delivery", res);
|
|
13800
|
+
const delivery = await res.json();
|
|
13801
|
+
return delivery.kind === "notification" ? { ...delivery, message: `${delivery.message}
|
|
13802
|
+
Inbox delivery is asynchronous; use claim_goal/get_goal to collect durable answers. Do not poll this Notification.` } : delivery;
|
|
13803
|
+
};
|
|
13804
|
+
const settled = (d) => d.kind === "notification" || d.state === "closed" || d.answers.length > 0 || d.entries.some((e) => e.kind === "contribution");
|
|
13805
|
+
if (opts.waits === false) return read(opts.signal);
|
|
13806
|
+
const window = AbortSignal.timeout(AWAIT_WINDOW_MS);
|
|
13807
|
+
const signal = opts.signal ? AbortSignal.any([opts.signal, window]) : window;
|
|
13808
|
+
let latest;
|
|
14141
13809
|
try {
|
|
14142
|
-
|
|
14143
|
-
|
|
14144
|
-
|
|
14145
|
-
|
|
14146
|
-
}
|
|
14147
|
-
async function acknowledge(token, leaseId, opts = {}) {
|
|
14148
|
-
const attempts = Math.max(1, Math.min(opts.attempts ?? 3, 5));
|
|
14149
|
-
const backoffMs = Math.max(0, opts.backoffMs ?? 100);
|
|
14150
|
-
const doSleep = opts.doSleep ?? sleep3;
|
|
14151
|
-
let lastError;
|
|
14152
|
-
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
14153
|
-
try {
|
|
14154
|
-
const res = ensureAuthed(await reach(`${BACKEND_URL}/api/await/ack`, {
|
|
14155
|
-
method: "POST",
|
|
14156
|
-
headers: {
|
|
14157
|
-
authorization: `Bearer ${token}`,
|
|
14158
|
-
"content-type": "application/json"
|
|
14159
|
-
},
|
|
14160
|
-
body: JSON.stringify({ leaseId })
|
|
14161
|
-
}));
|
|
14162
|
-
if (res.ok) return;
|
|
14163
|
-
const detail2 = await responseDetail(res);
|
|
14164
|
-
if (res.status === 409) throw new ReplyLeaseExpiredError();
|
|
14165
|
-
const retryable = res.status === 408 || res.status === 425 || res.status === 429 || res.status >= 500;
|
|
14166
|
-
throw new ReplyAckError(
|
|
14167
|
-
`Paigy reply acknowledgement failed: ${res.status}${detail2 ? ` ${detail2}` : ""}`,
|
|
14168
|
-
retryable
|
|
14169
|
-
);
|
|
14170
|
-
} catch (error) {
|
|
14171
|
-
if (error instanceof UnpairedError || error instanceof ReplyLeaseExpiredError || error instanceof ReplyAckError && !error.retryable) {
|
|
14172
|
-
throw error;
|
|
14173
|
-
}
|
|
14174
|
-
lastError = error;
|
|
14175
|
-
if (attempt + 1 < attempts) {
|
|
14176
|
-
await doSleep(backoffMs * 2 ** attempt);
|
|
14177
|
-
}
|
|
13810
|
+
while (true) {
|
|
13811
|
+
signal.throwIfAborted();
|
|
13812
|
+
latest = await read(signal);
|
|
13813
|
+
if (settled(latest)) return latest;
|
|
13814
|
+
await sleep2(5e3, void 0, { signal });
|
|
14178
13815
|
}
|
|
13816
|
+
} catch (error) {
|
|
13817
|
+
opts.signal?.throwIfAborted();
|
|
13818
|
+
if (window.aborted && latest) return latest;
|
|
13819
|
+
throw error;
|
|
14179
13820
|
}
|
|
14180
|
-
const detail = lastError instanceof Error ? lastError.message : String(lastError);
|
|
14181
|
-
throw new Error(
|
|
14182
|
-
`Could not acknowledge the Paigy reply after ${attempts} attempts (${detail}). The answer was not returned to the agent and remains recoverable after the lease expires.`
|
|
14183
|
-
);
|
|
14184
|
-
}
|
|
14185
|
-
async function receiveReply(item, token, opts = {}) {
|
|
14186
|
-
const decoded = decodeReply(item, opts);
|
|
14187
|
-
const delivery = deliveryOf(item);
|
|
14188
|
-
if (!delivery) {
|
|
14189
|
-
if (opts.requireDelivery) {
|
|
14190
|
-
throw new Error("Paigy returned a catch-up reply without a delivery lease; the answer was not exposed.");
|
|
14191
|
-
}
|
|
14192
|
-
return decoded;
|
|
14193
|
-
}
|
|
14194
|
-
await acknowledge(token, delivery.leaseId, {
|
|
14195
|
-
attempts: opts.ackAttempts,
|
|
14196
|
-
backoffMs: opts.ackBackoffMs,
|
|
14197
|
-
doSleep: opts.ackSleep
|
|
14198
|
-
});
|
|
14199
|
-
return decoded;
|
|
14200
13821
|
}
|
|
14201
13822
|
async function checkReplies(opts = {}) {
|
|
14202
|
-
const
|
|
14203
|
-
|
|
14204
|
-
const replies = await Promise.all(pending.replies.map(async (reply) => {
|
|
14205
|
-
const received = await receiveReply(
|
|
14206
|
-
{ type: "reply", ...reply },
|
|
14207
|
-
token,
|
|
14208
|
-
{ requireDelivery: true }
|
|
14209
|
-
);
|
|
14210
|
-
const { type: _type, ...withoutType } = received;
|
|
14211
|
-
return withoutType;
|
|
13823
|
+
const res = ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/deliveries`, {
|
|
13824
|
+
headers: { authorization: `Bearer ${authToken(opts.token) ?? ""}`, "x-paigy-model": "goal-entry-v1" }
|
|
14212
13825
|
}));
|
|
14213
|
-
|
|
14214
|
-
...pending,
|
|
14215
|
-
replies,
|
|
14216
|
-
work: pending.work ?? []
|
|
14217
|
-
};
|
|
14218
|
-
}
|
|
14219
|
-
async function patchState(path, state, opts, operation) {
|
|
14220
|
-
const token = authToken(opts.token) ?? "";
|
|
14221
|
-
const res = ensureAuthed(await reach(
|
|
14222
|
-
`${BACKEND_URL}${path}`,
|
|
14223
|
-
{
|
|
14224
|
-
method: "PATCH",
|
|
14225
|
-
headers: {
|
|
14226
|
-
"content-type": "application/json",
|
|
14227
|
-
authorization: `Bearer ${token}`,
|
|
14228
|
-
"x-paigy-await-ack": "1"
|
|
14229
|
-
},
|
|
14230
|
-
body: JSON.stringify({ state })
|
|
14231
|
-
}
|
|
14232
|
-
));
|
|
14233
|
-
if (!res.ok) {
|
|
14234
|
-
throw new Error(`${operation} failed: ${res.status} ${await res.text()}`);
|
|
14235
|
-
}
|
|
13826
|
+
if (!res.ok) await fail("check_replies", res);
|
|
14236
13827
|
return await res.json();
|
|
14237
13828
|
}
|
|
14238
|
-
|
|
14239
|
-
|
|
14240
|
-
}
|
|
14241
|
-
|
|
14242
|
-
|
|
14243
|
-
var import_qrcode = __toESM(require_lib(), 1);
|
|
14244
|
-
import { homedir as homedir7, hostname } from "os";
|
|
14245
|
-
|
|
14246
|
-
// src/harness/catalog.ts
|
|
14247
|
-
import { spawnSync } from "child_process";
|
|
14248
|
-
import { existsSync as existsSync2 } from "fs";
|
|
14249
|
-
import { homedir as homedir2 } from "os";
|
|
14250
|
-
import { delimiter, dirname, join as join2 } from "path";
|
|
14251
|
-
var CATALOG = [
|
|
14252
|
-
{
|
|
14253
|
-
name: "claude",
|
|
14254
|
-
label: "Claude Code",
|
|
14255
|
-
cli: "claude",
|
|
14256
|
-
adapter: "claude-agent-acp",
|
|
14257
|
-
authProbe: ["claude", "auth", "status"],
|
|
14258
|
-
loginHint: "Run `claude` once in a terminal and sign in.",
|
|
14259
|
-
installCli: "curl -fsSL https://claude.ai/install.sh | bash",
|
|
14260
|
-
installAdapter: "npm install -g @agentclientprotocol/claude-agent-acp"
|
|
14261
|
-
},
|
|
14262
|
-
{
|
|
14263
|
-
name: "codex",
|
|
14264
|
-
label: "Codex",
|
|
14265
|
-
cli: "codex",
|
|
14266
|
-
adapter: "codex-acp",
|
|
14267
|
-
authProbe: ["codex", "login", "status"],
|
|
14268
|
-
loginHint: "Run `codex login` to authenticate.",
|
|
14269
|
-
installCli: "curl -fsSL https://chatgpt.com/codex/install.sh | sh",
|
|
14270
|
-
installAdapter: "npm install -g @agentclientprotocol/codex-acp"
|
|
14271
|
-
},
|
|
14272
|
-
{
|
|
14273
|
-
name: "agy",
|
|
14274
|
-
label: "Antigravity",
|
|
14275
|
-
cli: "agy",
|
|
14276
|
-
authProbe: ["agy", "help"],
|
|
14277
|
-
loginHint: "Install and set up Antigravity (`agy`).",
|
|
14278
|
-
installCli: "curl -fsSL https://antigravity.dev/install.sh | bash"
|
|
14279
|
-
}
|
|
14280
|
-
];
|
|
14281
|
-
function probeDirs(home = homedir2()) {
|
|
14282
|
-
return [
|
|
14283
|
-
dirname(process.execPath),
|
|
14284
|
-
join2(home, ".local", "bin"),
|
|
14285
|
-
"/usr/local/bin",
|
|
14286
|
-
"/opt/homebrew/bin",
|
|
14287
|
-
join2(home, ".npm-global", "bin")
|
|
14288
|
-
];
|
|
14289
|
-
}
|
|
14290
|
-
function which(bin) {
|
|
14291
|
-
const hit = spawnSync("which", [bin], { encoding: "utf8" });
|
|
14292
|
-
const fromPath = hit.status === 0 ? hit.stdout.trim() : "";
|
|
14293
|
-
if (fromPath) return fromPath;
|
|
14294
|
-
for (const dir of probeDirs()) {
|
|
14295
|
-
const candidate = join2(dir, bin);
|
|
14296
|
-
if (existsSync2(candidate)) return candidate;
|
|
14297
|
-
}
|
|
14298
|
-
return null;
|
|
14299
|
-
}
|
|
14300
|
-
function runProbe(argv) {
|
|
14301
|
-
const [cmd, ...args] = argv;
|
|
14302
|
-
if (!cmd) return false;
|
|
14303
|
-
const resolved = which(cmd);
|
|
14304
|
-
if (!resolved) return false;
|
|
14305
|
-
return spawnSync(resolved, args, {
|
|
14306
|
-
timeout: 1e4,
|
|
14307
|
-
stdio: "ignore",
|
|
14308
|
-
env: { ...process.env, PATH: [process.env.PATH, ...probeDirs()].filter(Boolean).join(delimiter) }
|
|
14309
|
-
}).status === 0;
|
|
14310
|
-
}
|
|
14311
|
-
function detect(entry, deps = {}) {
|
|
14312
|
-
const find = deps.which ?? which;
|
|
14313
|
-
const probe = deps.probe ?? runProbe;
|
|
14314
|
-
const base = { name: entry.name, label: entry.label };
|
|
14315
|
-
const cliPath = find(entry.cli);
|
|
14316
|
-
if (!cliPath) return { ...base, status: "missing", hint: `${entry.label} is not installed.`, install: entry.installCli };
|
|
14317
|
-
const adapterPath = entry.adapter ? find(entry.adapter) : null;
|
|
14318
|
-
if (entry.adapter && !adapterPath) {
|
|
14319
|
-
return {
|
|
14320
|
-
...base,
|
|
14321
|
-
status: "adapter-missing",
|
|
14322
|
-
hint: `${entry.label} is installed, but its ACP adapter (${entry.adapter}) is not.`,
|
|
14323
|
-
...entry.installAdapter ? { install: entry.installAdapter } : {}
|
|
14324
|
-
};
|
|
14325
|
-
}
|
|
14326
|
-
const bin = adapterPath ?? cliPath;
|
|
14327
|
-
if (!probe(entry.authProbe)) return { ...base, status: "login", bin, hint: entry.loginHint };
|
|
14328
|
-
return { ...base, status: "ready", bin };
|
|
14329
|
-
}
|
|
14330
|
-
function detectAll(deps = {}) {
|
|
14331
|
-
return CATALOG.map((entry) => detect(entry, deps));
|
|
14332
|
-
}
|
|
14333
|
-
|
|
14334
|
-
// src/host.ts
|
|
14335
|
-
import { readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
14336
|
-
import { homedir as homedir5 } from "os";
|
|
14337
|
-
import { join as join4 } from "path";
|
|
14338
|
-
|
|
14339
|
-
// src/workspaces.ts
|
|
14340
|
-
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
14341
|
-
import { dirname as dirname2, join as join3, resolve } from "path";
|
|
14342
|
-
import { homedir as homedir3 } from "os";
|
|
14343
|
-
function workspacesFile() {
|
|
14344
|
-
return join3(homedir3(), ".paigy", "workspaces.json");
|
|
14345
|
-
}
|
|
14346
|
-
function listWorkspaces(deps) {
|
|
13829
|
+
var HEARTBEAT_MS = 25e3;
|
|
13830
|
+
var RETRY_MS = [1e3, 2e3, 5e3, 1e4, 3e4];
|
|
13831
|
+
var JOIN = { config: { broadcast: { self: false } } };
|
|
13832
|
+
var lines = /* @__PURE__ */ new Map();
|
|
13833
|
+
function frame(line, topic, event, payload) {
|
|
14347
13834
|
try {
|
|
14348
|
-
|
|
14349
|
-
return Array.isArray(parsed) ? parsed.filter((w) => typeof w === "string") : [];
|
|
13835
|
+
line.ws?.send(JSON.stringify({ topic, event, payload, ref: String(++line.ref) }));
|
|
14350
13836
|
} catch {
|
|
14351
|
-
return [];
|
|
14352
13837
|
}
|
|
14353
13838
|
}
|
|
14354
|
-
function
|
|
14355
|
-
|
|
14356
|
-
|
|
14357
|
-
|
|
14358
|
-
all.push(next);
|
|
14359
|
-
mkdirSync2(dirname2(deps.file), { recursive: true });
|
|
14360
|
-
writeFileSync2(deps.file, JSON.stringify(all, null, 2));
|
|
14361
|
-
}
|
|
14362
|
-
return all;
|
|
13839
|
+
function stop(line) {
|
|
13840
|
+
line.open = false;
|
|
13841
|
+
clearInterval(line.heart);
|
|
13842
|
+
clearTimeout(line.retry);
|
|
14363
13843
|
}
|
|
14364
|
-
function
|
|
14365
|
-
const
|
|
14366
|
-
|
|
14367
|
-
|
|
14368
|
-
|
|
13844
|
+
function connect(line) {
|
|
13845
|
+
const base = line.url.replace(/^http/, "ws").replace(/\/+$/, "");
|
|
13846
|
+
const ws = line.ws = new import_undici.WebSocket(`${base}/realtime/v1/websocket?apikey=${encodeURIComponent(line.anonKey)}&vsn=1.0.0`);
|
|
13847
|
+
ws.addEventListener("open", () => {
|
|
13848
|
+
line.open = true;
|
|
13849
|
+
line.attempt = 0;
|
|
13850
|
+
for (const topic of line.topics.keys()) frame(line, `realtime:${topic}`, "phx_join", JOIN);
|
|
13851
|
+
line.heart = setInterval(() => frame(line, "phoenix", "heartbeat", {}), HEARTBEAT_MS);
|
|
13852
|
+
});
|
|
13853
|
+
ws.addEventListener("message", (e) => {
|
|
13854
|
+
let m;
|
|
13855
|
+
try {
|
|
13856
|
+
m = JSON.parse(String(e.data));
|
|
13857
|
+
} catch {
|
|
13858
|
+
return;
|
|
13859
|
+
}
|
|
13860
|
+
if (m.event !== "broadcast" || !m.topic?.startsWith("realtime:")) return;
|
|
13861
|
+
for (const l of line.topics.get(m.topic.slice("realtime:".length)) ?? []) {
|
|
13862
|
+
if (m.payload?.event === l.event) l.onMessage(m.payload.payload);
|
|
13863
|
+
}
|
|
13864
|
+
});
|
|
13865
|
+
let gone = false;
|
|
13866
|
+
const again = () => {
|
|
13867
|
+
if (gone) return;
|
|
13868
|
+
gone = true;
|
|
13869
|
+
stop(line);
|
|
13870
|
+
if (!line.topics.size) return;
|
|
13871
|
+
line.retry = setTimeout(() => connect(line), RETRY_MS[Math.min(line.attempt++, RETRY_MS.length - 1)]);
|
|
13872
|
+
};
|
|
13873
|
+
ws.addEventListener("close", again);
|
|
13874
|
+
ws.addEventListener("error", again);
|
|
14369
13875
|
}
|
|
14370
|
-
function
|
|
14371
|
-
const
|
|
14372
|
-
|
|
13876
|
+
function subscribeRealtime(args) {
|
|
13877
|
+
const key = `${args.url} ${args.anonKey}`;
|
|
13878
|
+
let line = lines.get(key);
|
|
13879
|
+
if (!line) lines.set(key, line = { url: args.url, anonKey: args.anonKey, open: false, attempt: 0, ref: 0, topics: /* @__PURE__ */ new Map() });
|
|
13880
|
+
let heard = line.topics.get(args.topic);
|
|
13881
|
+
if (!heard) {
|
|
13882
|
+
line.topics.set(args.topic, heard = /* @__PURE__ */ new Set());
|
|
13883
|
+
if (line.open) frame(line, `realtime:${args.topic}`, "phx_join", JOIN);
|
|
13884
|
+
}
|
|
13885
|
+
const me = { event: args.event, onMessage: args.onMessage };
|
|
13886
|
+
heard.add(me);
|
|
13887
|
+
if (!line.ws) connect(line);
|
|
13888
|
+
return {
|
|
13889
|
+
close: () => {
|
|
13890
|
+
if (!heard.delete(me) || heard.size) return;
|
|
13891
|
+
line.topics.delete(args.topic);
|
|
13892
|
+
if (line.topics.size) {
|
|
13893
|
+
if (line.open) frame(line, `realtime:${args.topic}`, "phx_leave", {});
|
|
13894
|
+
return;
|
|
13895
|
+
}
|
|
13896
|
+
lines.delete(key);
|
|
13897
|
+
stop(line);
|
|
13898
|
+
try {
|
|
13899
|
+
line.ws?.close();
|
|
13900
|
+
} catch {
|
|
13901
|
+
}
|
|
13902
|
+
}
|
|
13903
|
+
};
|
|
14373
13904
|
}
|
|
14374
|
-
function
|
|
14375
|
-
|
|
14376
|
-
|
|
14377
|
-
|
|
14378
|
-
|
|
14379
|
-
|
|
13905
|
+
async function subscribeWake(onNudge, opts = {}) {
|
|
13906
|
+
const cfg = await registerDelivery("self_hosted", opts);
|
|
13907
|
+
const { url, anonKey } = cfg.realtime;
|
|
13908
|
+
const channel = wakeChannel(cfg.tokenId);
|
|
13909
|
+
const ch = subscribeRealtime({
|
|
13910
|
+
url,
|
|
13911
|
+
anonKey,
|
|
13912
|
+
topic: channel,
|
|
13913
|
+
event: WAKE_EVENT,
|
|
13914
|
+
onMessage: (payload) => onNudge(payload ?? {})
|
|
13915
|
+
});
|
|
13916
|
+
return {
|
|
13917
|
+
tokenId: cfg.tokenId,
|
|
13918
|
+
channel,
|
|
13919
|
+
close: async () => {
|
|
13920
|
+
ch.close();
|
|
13921
|
+
await registerDelivery("poll", opts).catch(() => {
|
|
13922
|
+
});
|
|
13923
|
+
}
|
|
13924
|
+
};
|
|
14380
13925
|
}
|
|
14381
13926
|
|
|
14382
13927
|
// ../../node_modules/.pnpm/zod-to-json-schema@3.25.2_zod@3.25.76/node_modules/zod-to-json-schema/dist/esm/Options.js
|
|
@@ -15666,20 +15211,17 @@ var zodToJsonSchema2 = (schema, options) => {
|
|
|
15666
15211
|
// ../../packages/schema/dist/index.js
|
|
15667
15212
|
var OPTIONS_MIN2 = 2;
|
|
15668
15213
|
var OPTIONS_MAX2 = 6;
|
|
15669
|
-
var MISSED_CALL_PLAN2 = {
|
|
15670
|
-
retry_10m: { kind: "every", minutes: 10 },
|
|
15671
|
-
retry_30m: { kind: "every", minutes: 30 },
|
|
15672
|
-
retry_60m: { kind: "every", minutes: 60 },
|
|
15673
|
-
backoff_gentle: { kind: "at", minutes: [30, 120, 360] },
|
|
15674
|
-
backoff_standard: { kind: "at", minutes: [10, 30, 120] },
|
|
15675
|
-
backoff_aggressive: { kind: "at", minutes: [5, 15, 45] },
|
|
15676
|
-
inbox: { kind: "once" },
|
|
15677
|
-
dismiss: { kind: "grace", minutes: 2 }
|
|
15678
|
-
};
|
|
15679
15214
|
function draft20202(node) {
|
|
15680
15215
|
if (Array.isArray(node)) return node.map(draft20202);
|
|
15681
15216
|
if (node && typeof node === "object") {
|
|
15682
15217
|
const o = node;
|
|
15218
|
+
if (Array.isArray(o.items)) {
|
|
15219
|
+
o.prefixItems = o.items;
|
|
15220
|
+
if ("additionalItems" in o) {
|
|
15221
|
+
o.items = o.additionalItems;
|
|
15222
|
+
delete o.additionalItems;
|
|
15223
|
+
} else delete o.items;
|
|
15224
|
+
}
|
|
15683
15225
|
for (const [excl, lim] of [["exclusiveMinimum", "minimum"], ["exclusiveMaximum", "maximum"]]) {
|
|
15684
15226
|
if (typeof o[excl] === "boolean") {
|
|
15685
15227
|
if (o[excl] === true && typeof o[lim] === "number") {
|
|
@@ -15698,11 +15240,54 @@ function mcpInputSchema2(s) {
|
|
|
15698
15240
|
delete schema.$schema;
|
|
15699
15241
|
return draft20202(schema);
|
|
15700
15242
|
}
|
|
15243
|
+
var StartContactSchema2 = external_exports.object({
|
|
15244
|
+
goalIds: external_exports.tuple([external_exports.string().uuid()]),
|
|
15245
|
+
// THE ASK IS THE QUESTION, not a bulletin with a question at the end (2026-09-16).
|
|
15246
|
+
//
|
|
15247
|
+
// A DecisionNeed is settled only by an answer in the shape THIS ask declares. Bundle news,
|
|
15248
|
+
// findings and a decision into one, and the person answers whichever part engaged them —
|
|
15249
|
+
// which settles nothing, leaves the need open, and gets them asked again. On call 619c5a92
|
|
15250
|
+
// one ask carried a briefing, a side question ("the ring should show the agent's name now —
|
|
15251
|
+
// what did you actually see?") and a one-of-four about how far an erase reaches. The owner
|
|
15252
|
+
// answered the side question. The need stayed open; the card asked again. Of the 45 call
|
|
15253
|
+
// asks carrying a decision, 27 are over 600 characters.
|
|
15254
|
+
//
|
|
15255
|
+
// Sending them separately costs nothing, because a CALL contact joins the call already
|
|
15256
|
+
// happening (`goal/store.ts`, JOIN BEFORE MINTING) — several contacts on one Goal arrive as
|
|
15257
|
+
// one call, planned as its turns, and each keeps its own settleable need.
|
|
15258
|
+
ask: external_exports.string().trim().min(1).max(1e4).describe(
|
|
15259
|
+
"The question, and only what is needed to answer it. News, progress and findings are their own contact \u2014 a call contact JOINS a call already happening, so several arrive as one call. Do not bundle: a DecisionNeed is settled only by an answer in the shape this ask declares, so someone who answers the part that interested them settles nothing and is asked again."
|
|
15260
|
+
),
|
|
15261
|
+
waiting: external_exports.enum(["none", "hard"]).default("none"),
|
|
15262
|
+
channel: external_exports.enum(["notification", "call"]).default("notification"),
|
|
15263
|
+
options: external_exports.array(external_exports.object({ label: external_exports.string().trim().min(1).max(1e3), image: external_exports.string().url().optional(), html: external_exports.string().max(16384).optional() }).strict()).min(2).max(6).optional(),
|
|
15264
|
+
threadId: external_exports.string().uuid().optional().describe("Continue an existing Thread: the threadId a prior Delivery returned. Omit to start a new Thread.")
|
|
15265
|
+
}).strict();
|
|
15266
|
+
var ContactSchema2 = external_exports.union([StartContactSchema2, external_exports.object({ deliveryId: external_exports.string().uuid() }).strict()]);
|
|
15267
|
+
var CONTACT_SCHEMA2 = { type: "object", ...mcpInputSchema2(ContactSchema2) };
|
|
15268
|
+
var CONTACT_DESCRIPTION2 = "Contact the user about exactly one existing Goal: pass goalIds:[goalId], ask, channel:'notification'|'call', and waiting:'none'|'hard'. Options supply choices. Notification returns immediately; collect durable answers with claim_goal/get_goal. On stdio, a Call holds one cancellable ~45s window; continue with ONLY {deliveryId}. Continuation sends nothing and rereads the same durable evidence, including previously read answers. Entries retain authorship and provenance; accepted decisions are separate from quoted speech. Call state open does not mean ringing. Unsupported: soft waiting, multiple Goals/questions, outcome admission, and re-presentation of an existing request. Create a Goal explicitly first; never resend a pending ask to continue waiting.";
|
|
15701
15269
|
var CreateGoalSchema2 = external_exports.object({
|
|
15702
15270
|
outcome: external_exports.string().trim().min(1).max(1e4),
|
|
15703
15271
|
ownerParticipant: external_exports.string().trim().min(1).optional(),
|
|
15704
|
-
idempotencyKey: external_exports.string().trim().min(1).max(200)
|
|
15272
|
+
idempotencyKey: external_exports.string().trim().min(1).max(200),
|
|
15273
|
+
/** A past conversation this Goal should be read against — History's "new session from this"
|
|
15274
|
+
* (owner, on the call of 2026-09-14: "let's do the reference with the threading"). A
|
|
15275
|
+
* reference only: the owner reads it through `get_thread`, which does its own scoping, and
|
|
15276
|
+
* the writer refuses a thread belonging to another account. */
|
|
15277
|
+
contextThreadId: external_exports.string().uuid().optional(),
|
|
15278
|
+
/** THE GOAL THIS ONE BELONGS UNDER (owner, 2026-09-15: "the ask I gave for the design doc
|
|
15279
|
+
* didn't get created as a child goal of the voice UI goal, which is how it should've
|
|
15280
|
+
* worked"). It could not have been: this door took no parent, so the only route was
|
|
15281
|
+
* create -> claim -> `update_goal`, three calls with a lease in the middle, and every agent
|
|
15282
|
+
* took the short one. The hierarchy has been modelled since Goals existed and had been used
|
|
15283
|
+
* ZERO times in 2,031 of them. Absent, the server judges it against the caller's open Goals
|
|
15284
|
+
* (`apps/api/src/goal/intake.ts`). The writer refuses a Goal belonging to another account. */
|
|
15285
|
+
parentGoalId: external_exports.string().uuid().optional()
|
|
15705
15286
|
});
|
|
15287
|
+
var CreateGoalToolSchema2 = CreateGoalSchema2.extend({
|
|
15288
|
+
idempotencyKey: CreateGoalSchema2.shape.idempotencyKey.optional().describe("Optional. One is minted per call; pass your own only so a retry lands on the same Goal.")
|
|
15289
|
+
}).strict();
|
|
15290
|
+
var CREATE_GOAL_DESCRIPTION2 = "Create a durable Goal for an outcome. Without parentGoalId it is placed against your open Goals: if one already IS this work, that Goal comes back (existing: true) and nothing new is created \u2014 continue it; if the work belongs under one, it is created there (parentGoalId in the receipt); otherwise it is a root. Pass parentGoalId yourself to put it under a specific Goal. Admission only: the owner must claim it before doing work, then update it as it advances. Returns an admission receipt with goalId, current state, revision, ownerParticipant, and the next step; no Goal content or execution lease.";
|
|
15706
15291
|
var UpdateGoalSchema2 = external_exports.object({
|
|
15707
15292
|
revision: external_exports.number().int().positive(),
|
|
15708
15293
|
changes: external_exports.object({
|
|
@@ -15718,52 +15303,48 @@ var UpdateGoalSchema2 = external_exports.object({
|
|
|
15718
15303
|
reason: external_exports.string().trim().min(1).max(2e3),
|
|
15719
15304
|
operationId: external_exports.string().uuid().optional()
|
|
15720
15305
|
}).strict();
|
|
15721
|
-
var UpdateGoalToolSchema2 = UpdateGoalSchema2.extend({ goalId: external_exports.string().uuid() }).strict();
|
|
15306
|
+
var UpdateGoalToolSchema2 = UpdateGoalSchema2.omit({ operationId: true }).extend({ goalId: external_exports.string().uuid() }).strict();
|
|
15722
15307
|
var ClaimGoalSchema2 = external_exports.object({ goalId: external_exports.string().uuid().optional() }).strict();
|
|
15723
|
-
var
|
|
15724
|
-
var
|
|
15725
|
-
|
|
15726
|
-
|
|
15727
|
-
|
|
15728
|
-
|
|
15729
|
-
|
|
15730
|
-
|
|
15731
|
-
|
|
15732
|
-
|
|
15733
|
-
|
|
15734
|
-
|
|
15735
|
-
|
|
15736
|
-
|
|
15737
|
-
|
|
15738
|
-
|
|
15739
|
-
|
|
15740
|
-
|
|
15741
|
-
|
|
15742
|
-
|
|
15743
|
-
|
|
15744
|
-
|
|
15745
|
-
|
|
15746
|
-
|
|
15747
|
-
|
|
15748
|
-
|
|
15749
|
-
|
|
15750
|
-
|
|
15751
|
-
|
|
15752
|
-
|
|
15753
|
-
|
|
15754
|
-
|
|
15755
|
-
|
|
15756
|
-
|
|
15757
|
-
|
|
15758
|
-
|
|
15759
|
-
)
|
|
15760
|
-
}
|
|
15761
|
-
|
|
15762
|
-
delete out.required;
|
|
15763
|
-
out.anyOf = [{ required: ["ask"] }, { required: ["wait"] }];
|
|
15764
|
-
return out;
|
|
15308
|
+
var GetGoalSchema2 = external_exports.object({ goalId: external_exports.string().uuid() }).strict();
|
|
15309
|
+
var GET_GOAL_DESCRIPTION2 = "Read the current authorized Goal brief: state, owner, blockers, open decisions, progress, and the next operation. Foreign or sibling-owned Goals are not disclosed.";
|
|
15310
|
+
var UPDATE_GOAL_DESCRIPTION2 = "Update an owned Goal at an exact revision. State, ownership, dependencies, children, progress, and review acknowledgement are explicit; stale revisions are rejected. Returns the new revision and a prose summary.";
|
|
15311
|
+
var CLAIM_GOAL_DESCRIPTION2 = "Claim the oldest runnable or review-pending Goal you own, or pass goalId to claim that Goal. Returns a Goal-scoped brief, current revision, blockers, and the next valid operation. Claiming creates or renews the execution lease.";
|
|
15312
|
+
var CHECK_REPLIES_DESCRIPTION2 = "Your open Deliveries: every Notification or Call currently addressed to you \u2014 a request the user started toward you, an answer relayed to something you asked, a handoff \u2014 each with its durable Entries, accepted decisions and open decision needs, in the same shape a contact read returns. A pure read with no arguments: nothing is consumed, acknowledged or claimed by reading it, so call it on startup, after a long wait, or whenever you want to know what is outstanding. To act on one, claim its Goal (claim_goal) or reread it with contact({deliveryId}). Your runnable and review-pending Goals come from claim_goal, not from here.";
|
|
15313
|
+
var CheckRepliesSchema2 = external_exports.object({}).strict();
|
|
15314
|
+
var GetThreadSchema2 = external_exports.object({
|
|
15315
|
+
parentId: external_exports.string().describe("The Thread to read \u2014 the threadId a Delivery returned, or the parentId of a search hit.")
|
|
15316
|
+
}).strict();
|
|
15317
|
+
var GET_THREAD_DESCRIPTION2 = "Read the authorized durable Entries on one conversation Thread \u2014 what you wrote there and what was delivered to you, oldest first. Use claim_goal to find the work to resume; use this to rehydrate a Thread that a search hit or a Delivery named.";
|
|
15318
|
+
var SearchThreadsSchema2 = external_exports.object({
|
|
15319
|
+
q: external_exports.string().describe("What to look for \u2014 plain words or a phrase (e.g. 'the livekit timeout', 'deploy to prod').")
|
|
15320
|
+
}).strict();
|
|
15321
|
+
var SEARCH_THREADS_DESCRIPTION2 = `Search your PAST conversations before asking \u2014 "have we discussed this before?". Full-text over your own threads (the asks you sent + the user's answers); returns ranked threads with highlighted snippets, NOT rows: { hits: [{ parentId, at, agentLabel, matches: [{ notificationId, role, snippet }] }] }. The loop this exists for: search first \u2192 get_thread the best hit to rehydrate it \u2192 THEN continue or contact, so you answer with receipts ("last week you said ship it") instead of re-asking. Read-only, safe to call anytime; scoped to your own account's threads.`;
|
|
15322
|
+
var AGENT_TOOLS2 = [
|
|
15323
|
+
{ name: "contact", description: CONTACT_DESCRIPTION2, inputSchema: CONTACT_SCHEMA2 },
|
|
15324
|
+
{ name: "check_replies", description: CHECK_REPLIES_DESCRIPTION2, inputSchema: mcpInputSchema2(CheckRepliesSchema2) },
|
|
15325
|
+
{ name: "get_thread", description: GET_THREAD_DESCRIPTION2, inputSchema: mcpInputSchema2(GetThreadSchema2) },
|
|
15326
|
+
{ name: "search_threads", description: SEARCH_THREADS_DESCRIPTION2, inputSchema: mcpInputSchema2(SearchThreadsSchema2) },
|
|
15327
|
+
{ name: "create_goal", description: CREATE_GOAL_DESCRIPTION2, inputSchema: mcpInputSchema2(CreateGoalToolSchema2) },
|
|
15328
|
+
{ name: "claim_goal", description: CLAIM_GOAL_DESCRIPTION2, inputSchema: mcpInputSchema2(ClaimGoalSchema2) },
|
|
15329
|
+
{ name: "get_goal", description: GET_GOAL_DESCRIPTION2, inputSchema: mcpInputSchema2(GetGoalSchema2) },
|
|
15330
|
+
{ name: "update_goal", description: UPDATE_GOAL_DESCRIPTION2, inputSchema: mcpInputSchema2(UpdateGoalToolSchema2) }
|
|
15331
|
+
];
|
|
15332
|
+
var AGENT_TOOL_NAMES2 = AGENT_TOOLS2.map((t) => t.name);
|
|
15333
|
+
function entryWords(entry) {
|
|
15334
|
+
const content = entry.content;
|
|
15335
|
+
if (content && "sealed" in content) return "";
|
|
15336
|
+
const plain = content?.plain;
|
|
15337
|
+
if (typeof plain === "string") return plain;
|
|
15338
|
+
if (plain && typeof plain === "object") {
|
|
15339
|
+
const text = plain.text;
|
|
15340
|
+
if (typeof text === "string") return text;
|
|
15341
|
+
const title = plain.title;
|
|
15342
|
+
const description = plain.description;
|
|
15343
|
+
const parts = [title, ...Array.isArray(description) ? description : []].filter((v) => typeof v === "string");
|
|
15344
|
+
if (parts.length) return parts.join("\n\n");
|
|
15345
|
+
}
|
|
15346
|
+
return entry.sources.map((source) => source.text).join("\n");
|
|
15765
15347
|
}
|
|
15766
|
-
var CONTACT_DESCRIPTION2 = `Reach the user through Paigy \u2014 tell them something, or ask and get their answer. State what you need in \`ask\`, say what happens to your work while you wait in \`waiting\`, and Paigy handles the rest (channel, phrasing, answer format). If the user explicitly asks you to CALL them, send waiting:'hard' and say so in the ask. Returns { notificationId, parentId, workId?, goalId?, entryId?, decisionId?, wait? }. WHEN IT RANG, contact holds the first ~45 s window ITSELF and \`wait\` carries the outcome: { type:'reply', answer } to act on; { type:'partial', inFlight:true, turn } \u2014 what the user is saying to each turn, provisional: use it to PREPARE (fetch, draft, warm the build), never to act irreversibly, they can still revise it until the final reply (partial = intelligence, settled = authorization; if a partial's acts carry a question aimed at you and you know the answer, contact on the SAME parentId right away \u2014 they hear it on the same call); { type:'remind', remindInSeconds } \u2014 schedule a wake-up; { type:'idle' } \u2014 still waiting. TO KEEP WAITING, call contact again with ONLY { wait: notificationId } \u2014 no ask, nothing new is sent; it holds the next scoped ~45 s window (under the 60 s host cap, so it always returns) and never returns another notification's reply. Keep doing that until the reply \u2014 THAT one is the decision \u2014 so the user steps away and comes back to find you already continued; stop only to do other work and check back, or after an unreasonably long stretch worth telling them about. A message delivery has NO \`wait\`: never poll for it \u2014 the reply arrives through check_replies or your wake. Pass parentId to a later contact to continue the conversation. A Goal-targeted contact also returns goalId and entryId for the durable Goal entry. When it rang, the reply also carries { ifMissed: { mode, means } }: what the user's own policy does with a call they don't take ("${STANDARD_MEANS2}"), so a no-answer tells you how long to wait before coming back. THREADING REPLACES: a threaded follow-up SUPERSEDES your earlier pending items on that thread \u2014 right for updates to one ask, WRONG for a checklist (send independent to-dos un-threaded). A threaded re-send with IDENTICAL content escalates the pending ask in place. If a reply comes back as {kind:'clarify', chunks:[...]}, the user wants more detail \u2014 contact again on the SAME parentId with an expanded ask. BLOCKED ON A DECISION for existing work? Pass that work's \`workId\`; the reply returns the same workId plus a decisionId, so the answer resumes the right outcome. ONE ASK, ONE ROW: never restate a still-pending ask's question inside a NEW contact (e.g. weaving it into a briefing) \u2014 the whole answer settles on the new row and the original can never receive it. Keep waiting on the original (a live call reads every pending ask out separately, each answer routes to its own row), and use \`needs\` for a genuinely multi-part NEW ask. ANSWERABLE, NOT JUST ASKED: when the reply comes back carrying \`plan.units[].needs\`, that unit asked for something it gave the user no way to answer \u2014 'options' means it posed a choice with nothing to choose from, 'visuals' means it asked about something to look at with nothing to look at. Send it again on the SAME parentId with ${OPTIONS_MIN2}-${OPTIONS_MAX2} options (or the image), drawn from your own sentence. Paigy will not add them for you: a shape it guessed wrong cannot be undone, and you are the one who knows what the real alternatives are. \`units\` reports WHAT BECAME OF YOUR PROSE \u2014 { kept, raw, why }: how many topics Paigy compressed for delivery, how many kept your exact words, and the reason when it kept them (e.g. 'no_output' = compression produced nothing usable, so the user got your raw sentence). It needs no action and is not an error \u2014 read it only when the delivered wording matters to you; a high \`raw\` count means the user is hearing you verbatim. READING A CALL'S REPLY: it can come back as {kind:'turns', turns:[{prompt,reply}]} \u2014 the ordered log of that call. Read turns[0].reply as the user's main instruction. Usually that's the only turn; if there are more (e.g. an end-of-call 'call me back when it's done / I have a question that blocks me'), read each one in order as a further follow-up instruction, not a single combined one. If they asked for a callback, re-engage in the SAME thread (contact with the reply's parentId) when the task is done or you hit a blocker \u2014 waiting:'hard' for a blocker, waiting:'none' for done. Paigy has no scheduler; the callback is yours to send (use ScheduleWakeup/cron for timing). A call-mapped answer may carry \`intents\` \u2014 next steps the user attached, each { kind, detail } with detail quoting their words. ACT on them, don't just read them: 'defer' ("call me after lunch") \u2192 register it NOW with schedule_callback \u2014 when the intent carries \`dueInSeconds\` (Paigy pre-parsed the spoken time against the user's clock) pass it straight through; otherwise derive it from the detail yourself \u2014 then follow up on the same thread; 'delegate' ("you pick") \u2192 make the call yourself and tell them what you chose; 'channel' ("text me next time") \u2192 honor it on your next contact (channel:'message'); 'question' (an open question aimed back at you that the call couldn't answer) \u2192 you OWE them the answer \u2014 work it out and follow up on the same thread without being asked, the call deliberately skipped "should I call you back?" because the follow-up is implied. \`transcript\` is the user's raw words behind a shaped answer \u2014 read it for hedges and conditions ("yes, IF tests pass") before acting. If your ask declared \`points\`, the reply carries \`covered\` \u2014 the points actually addressed. Compare against what you declared: a missing point is STILL unanswered \u2014 re-ask it (contact on the same parentId) or proceed knowingly partial; never treat a partial answer as complete.`;
|
|
15767
15348
|
var ContextSchema2 = external_exports.object({
|
|
15768
15349
|
title: external_exports.string().min(1).describe("One-line headline of what you need (required, non-empty)."),
|
|
15769
15350
|
description: external_exports.array(external_exports.string().min(1)).describe(
|
|
@@ -15989,38 +15570,11 @@ var NotifyRequestSchema2 = NotifyRequestFields2.superRefine((r, ctx) => {
|
|
|
15989
15570
|
const needsOptions = r.select === "one" || r.select === "many" || r.select === "rank";
|
|
15990
15571
|
if (needsOptions && !r.options?.length)
|
|
15991
15572
|
ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["options"], message: `select:'${r.select}' requires options` });
|
|
15992
|
-
if (!needsOptions && r.options?.length)
|
|
15993
|
-
ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["options"], message: `select:'${r.select}' takes no options` });
|
|
15994
|
-
});
|
|
15995
|
-
function unitsOf2(text) {
|
|
15996
|
-
const spans = [];
|
|
15997
|
-
const re = /\n\s*\n+/g;
|
|
15998
|
-
let cursor = 0;
|
|
15999
|
-
const push = (from, to) => {
|
|
16000
|
-
const slice = text.slice(from, to);
|
|
16001
|
-
const lead = slice.length - slice.trimStart().length;
|
|
16002
|
-
const tail = slice.length - slice.trimEnd().length;
|
|
16003
|
-
if (from + lead < to - tail) spans.push({ start: from + lead, end: to - tail });
|
|
16004
|
-
};
|
|
16005
|
-
for (let m = re.exec(text); m; m = re.exec(text)) {
|
|
16006
|
-
push(cursor, m.index);
|
|
16007
|
-
cursor = m.index + m[0].length;
|
|
16008
|
-
}
|
|
16009
|
-
push(cursor, text.length);
|
|
16010
|
-
return spans;
|
|
16011
|
-
}
|
|
15573
|
+
if (!needsOptions && r.options?.length)
|
|
15574
|
+
ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["options"], message: `select:'${r.select}' takes no options` });
|
|
15575
|
+
});
|
|
16012
15576
|
var NotifyStatusSchema2 = external_exports.enum(["pending", "answered", "ignored"]);
|
|
16013
15577
|
var AgentStateSchema2 = external_exports.enum(["idle", "in_progress", "completed", "needs_input"]);
|
|
16014
|
-
var SetTaskStateSchema2 = external_exports.object({
|
|
16015
|
-
state: external_exports.enum(["in_progress", "completed", "needs_input"])
|
|
16016
|
-
});
|
|
16017
|
-
var SetWorkStateSchema2 = external_exports.object({
|
|
16018
|
-
workId: external_exports.string().uuid().optional(),
|
|
16019
|
-
notificationId: external_exports.string().optional(),
|
|
16020
|
-
state: SetTaskStateSchema2.shape.state
|
|
16021
|
-
}).refine((value) => Number(Boolean(value.workId)) + Number(Boolean(value.notificationId)) === 1, {
|
|
16022
|
-
message: "exactly one of workId or notificationId is required"
|
|
16023
|
-
});
|
|
16024
15578
|
var TurnSchema2 = external_exports.object({
|
|
16025
15579
|
prompt: external_exports.string(),
|
|
16026
15580
|
reply: external_exports.string()
|
|
@@ -16150,108 +15704,6 @@ var AwaitItemSchema2 = external_exports.discriminatedUnion("type", [
|
|
|
16150
15704
|
})
|
|
16151
15705
|
]);
|
|
16152
15706
|
var CallbackTriggerSchema2 = external_exports.enum(["on_done", "on_blocked", "scheduled"]);
|
|
16153
|
-
var ScheduleCallbackSchema2 = external_exports.object({
|
|
16154
|
-
parentId: external_exports.string().describe("The thread to call back on (from a prior contact / reply / request)."),
|
|
16155
|
-
goalId: external_exports.string().uuid().optional().describe("The Goal this callback advances; preferred for Goal-owned work."),
|
|
16156
|
-
trigger: CallbackTriggerSchema2,
|
|
16157
|
-
dueInSeconds: external_exports.number().int().positive().optional().describe("For 'scheduled' only: how many seconds from now to fire."),
|
|
16158
|
-
note: external_exports.string().optional().describe("What to tell the user when you follow up.")
|
|
16159
|
-
});
|
|
16160
|
-
var PendingRepliesSchema2 = external_exports.object({
|
|
16161
|
-
replies: external_exports.array(
|
|
16162
|
-
external_exports.object({
|
|
16163
|
-
parentId: external_exports.string(),
|
|
16164
|
-
notificationId: external_exports.string(),
|
|
16165
|
-
workId: external_exports.string().uuid().optional(),
|
|
16166
|
-
decisionId: external_exports.string().uuid().optional(),
|
|
16167
|
-
answer: UserAnswerSchema2,
|
|
16168
|
-
/** E2EE: the sealed answer (opaque envelope + plaintext `ignored` hint) when the
|
|
16169
|
-
* pairing is E2EE — the agent opens it and re-derives the real answer. Absent =
|
|
16170
|
-
* plaintext answer (in `answer`). See AwaitItemSchema's reply variant. */
|
|
16171
|
-
sealed: external_exports.lazy(() => SealedAnswerSchema2).optional(),
|
|
16172
|
-
/** The call record rendered for THIS agent: the raw words the shaped answer was
|
|
16173
|
-
* mapped from, filtered to its own claims. See AwaitItemSchema's reply variant. */
|
|
16174
|
-
transcript: external_exports.string().optional(),
|
|
16175
|
-
/** Coverage report (#396): which declared `points` this answer addressed. */
|
|
16176
|
-
covered: external_exports.array(external_exports.string()).optional()
|
|
16177
|
-
})
|
|
16178
|
-
),
|
|
16179
|
-
pending: external_exports.array(
|
|
16180
|
-
external_exports.object({ parentId: external_exports.string(), notificationId: external_exports.string(), createdAt: external_exports.string() })
|
|
16181
|
-
),
|
|
16182
|
-
/** WHO YOU ARE on this account (field report 2026-08-28): the name and device the user
|
|
16183
|
-
* sees for this session's identity. From inside a session there was no way to find out —
|
|
16184
|
-
* `pair` with no arguments can HATCH a fresh identity, so it is not a safe probe — and an
|
|
16185
|
-
* agent that cannot tell which agent it is cannot tell whether work addressed to
|
|
16186
|
-
* "Reta" was addressed to it. Absent only for a token with no pairing behind it. */
|
|
16187
|
-
you: external_exports.object({ name: external_exports.string(), device: external_exports.string().nullable(), tokenId: external_exports.string() }).optional(),
|
|
16188
|
-
/** User-initiated requests addressed to this agent; act on them and reply via
|
|
16189
|
-
* contact on the same parentId. Keeps reappearing until you call
|
|
16190
|
-
* set_work_state on its workId (or notificationId once while bootstrapping). */
|
|
16191
|
-
requests: external_exports.array(
|
|
16192
|
-
external_exports.object({
|
|
16193
|
-
parentId: external_exports.string(),
|
|
16194
|
-
notificationId: external_exports.string(),
|
|
16195
|
-
workId: external_exports.string().uuid().optional(),
|
|
16196
|
-
text: external_exports.string(),
|
|
16197
|
-
createdAt: external_exports.string(),
|
|
16198
|
-
/** The user seeded this request with a past conversation — call get_thread on it
|
|
16199
|
-
* FIRST and treat the transcript as prior context (#57/#251). */
|
|
16200
|
-
contextParentId: external_exports.string().optional(),
|
|
16201
|
-
/** STRANDED (field report 2026-08-28): this request was addressed to ANOTHER agent on
|
|
16202
|
-
* the account — the name here — which has not been seen since it landed, so nobody
|
|
16203
|
-
* came for it. Handed to you because you are the session that is here. Take it like
|
|
16204
|
-
* any request (set_work_state claims it, reply with contact on its parentId), and say
|
|
16205
|
-
* whose it was, because the user chose that agent on purpose. */
|
|
16206
|
-
stranded: external_exports.string().optional()
|
|
16207
|
-
})
|
|
16208
|
-
),
|
|
16209
|
-
/** Durable outcomes currently owned by this agent. This is the Work-native queue; flat
|
|
16210
|
-
* notification lists remain during migration so old clients keep their existing view. */
|
|
16211
|
-
work: external_exports.array(external_exports.object({
|
|
16212
|
-
workId: external_exports.string().uuid(),
|
|
16213
|
-
parentId: external_exports.string().optional(),
|
|
16214
|
-
objective: external_exports.string().nullable(),
|
|
16215
|
-
state: external_exports.enum(["active", "blocked", "waiting_external"]),
|
|
16216
|
-
blockedOn: external_exports.array(external_exports.string().uuid()),
|
|
16217
|
-
updatedAt: external_exports.string()
|
|
16218
|
-
})).optional(),
|
|
16219
|
-
/** Callbacks you owe the user that are now DUE (you said you'd follow up when done,
|
|
16220
|
-
* if blocked, or at a time that has passed). Re-surfaced every sweep until you
|
|
16221
|
-
* fulfill one by calling contact on its parentId. */
|
|
16222
|
-
/** Ride-alongs (RideAlongSchema): notes assigned to this agent that no wake could
|
|
16223
|
-
* reach. Same array the contact/await replies carry — one queue, every carrier. */
|
|
16224
|
-
also: external_exports.array(RideAlongSchema2).optional(),
|
|
16225
|
-
owedCallbacks: external_exports.array(
|
|
16226
|
-
external_exports.object({ parentId: external_exports.string(), trigger: CallbackTriggerSchema2, note: external_exports.string() })
|
|
16227
|
-
),
|
|
16228
|
-
/** Work (either direction) you reported in_progress a while ago and never reported
|
|
16229
|
-
* completed — likely left half-done by this session or a prior one that crashed or
|
|
16230
|
-
* went idle. Report a real state (set_work_state) or continue the work. */
|
|
16231
|
-
stalled: external_exports.array(
|
|
16232
|
-
external_exports.object({ parentId: external_exports.string(), notificationId: external_exports.string(), title: external_exports.string().nullable(), startedAt: external_exports.string() })
|
|
16233
|
-
),
|
|
16234
|
-
/** The queue rail (#614, pending/design.md): the same replies + requests, grouped by
|
|
16235
|
-
* thread and ordered oldest-thread-first, so you work ONE thread at a time — fold all of
|
|
16236
|
-
* a thread's `items` into a single turn rather than interleaving threads. `busy` = the
|
|
16237
|
-
* thread already has a turn in progress (younger than the stall cutoff); let it finish and
|
|
16238
|
-
* ride the next turn. `items` are that thread's replies/requests in arrival order; the
|
|
16239
|
-
* full payload for each is in the flat `replies`/`requests` arrays (matched by
|
|
16240
|
-
* notificationId). Derived, never stored — a crashed agent recomputes it exactly. */
|
|
16241
|
-
threads: external_exports.array(
|
|
16242
|
-
external_exports.object({
|
|
16243
|
-
parentId: external_exports.string(),
|
|
16244
|
-
busy: external_exports.boolean(),
|
|
16245
|
-
items: external_exports.array(
|
|
16246
|
-
external_exports.object({
|
|
16247
|
-
kind: external_exports.enum(["reply", "request"]),
|
|
16248
|
-
notificationId: external_exports.string(),
|
|
16249
|
-
at: external_exports.string()
|
|
16250
|
-
})
|
|
16251
|
-
)
|
|
16252
|
-
})
|
|
16253
|
-
)
|
|
16254
|
-
});
|
|
16255
15707
|
var NotifyResponseSchema2 = external_exports.object({
|
|
16256
15708
|
notificationId: external_exports.string(),
|
|
16257
15709
|
workId: external_exports.string().uuid().optional(),
|
|
@@ -16314,6 +15766,13 @@ var UserResponseSchema2 = external_exports.object({
|
|
|
16314
15766
|
});
|
|
16315
15767
|
var VoiceKeySchema2 = external_exports.enum(["rachel", "george", "jessica", "brian", "lily"]);
|
|
16316
15768
|
var AgendaTurnSchema2 = external_exports.object({
|
|
15769
|
+
/** THE TURN'S IDENTITY (the first-sentence stream, 2026-09-09): the brain call that wrote
|
|
15770
|
+
* it and its place in that reply — `<brainCallId>:<index>`, with `:p` on the first
|
|
15771
|
+
* sentence a re-plan publishes ahead of the rest. A turn is spoken once, by this id: the
|
|
15772
|
+
* completion of a streamed re-plan carries the published sentence again, and the walk
|
|
15773
|
+
* drops what it already said by identity, never by the API's guess of what was polled.
|
|
15774
|
+
* Absent on plans nothing streams (a ring plan, a floor). */
|
|
15775
|
+
id: external_exports.string().optional(),
|
|
16317
15776
|
/** Twin coverage (#1089): sibling claim ids this asking turn's answer ALSO settles —
|
|
16318
15777
|
* the planner declares duplicates instead of asking them twice. */
|
|
16319
15778
|
coveredIds: external_exports.array(external_exports.string()).optional(),
|
|
@@ -16472,7 +15931,21 @@ var InboxItemSchema2 = external_exports.object({
|
|
|
16472
15931
|
/** E2EE: the agent's device X25519 public key to seal the user's answer BACK to (the
|
|
16473
15932
|
* sender the phone replies to). Sourced server-side from this item's pairing credential.
|
|
16474
15933
|
* Present only alongside `envelope`; the phone seals via sealAnswer(answer, this, id). */
|
|
16475
|
-
agentX25519: external_exports.string().optional()
|
|
15934
|
+
agentX25519: external_exports.string().optional(),
|
|
15935
|
+
/** THE TARGET FACTS A CARD RENDERS (#1796 point 5, 2026-09-11): the Delivery it is a view of,
|
|
15936
|
+
* that Delivery's kind, the request Entry, the Goals it answers for, the exact DecisionNeed (none
|
|
15937
|
+
* for a request that asks nothing), whether its content is sealed, and that Goal's state. The
|
|
15938
|
+
* answer writer (`POST /api/entries`) and the disposition (`close_delivery`) take their ids from
|
|
15939
|
+
* here. The server projects it (`apps/api/src/inbox/project.ts`); a client never builds it. */
|
|
15940
|
+
communication: external_exports.object({
|
|
15941
|
+
deliveryId: external_exports.string(),
|
|
15942
|
+
kind: external_exports.enum(["notification", "call"]),
|
|
15943
|
+
entryId: external_exports.string(),
|
|
15944
|
+
goalIds: external_exports.array(external_exports.string()),
|
|
15945
|
+
decisionNeedId: external_exports.string().optional(),
|
|
15946
|
+
sealed: external_exports.boolean(),
|
|
15947
|
+
goalState: external_exports.string().optional()
|
|
15948
|
+
}).optional()
|
|
16476
15949
|
});
|
|
16477
15950
|
var SnoozeRequestSchema2 = external_exports.object({
|
|
16478
15951
|
requestId: external_exports.string(),
|
|
@@ -16708,26 +16181,54 @@ var CreateRequestSchema2 = external_exports.object({
|
|
|
16708
16181
|
* the agent reads it via get_thread. Must belong to the requesting user. */
|
|
16709
16182
|
contextParentId: external_exports.string().optional()
|
|
16710
16183
|
});
|
|
16711
|
-
var
|
|
16712
|
-
/**
|
|
16713
|
-
|
|
16714
|
-
/**
|
|
16715
|
-
|
|
16716
|
-
/**
|
|
16717
|
-
|
|
16718
|
-
|
|
16719
|
-
|
|
16720
|
-
/**
|
|
16721
|
-
|
|
16722
|
-
|
|
16723
|
-
|
|
16724
|
-
|
|
16725
|
-
|
|
16726
|
-
|
|
16727
|
-
|
|
16728
|
-
|
|
16729
|
-
|
|
16730
|
-
|
|
16184
|
+
var QueueQuestionSchema2 = external_exports.object({
|
|
16185
|
+
/** The decision need's id — what an answer is accepted against. */
|
|
16186
|
+
id: external_exports.string(),
|
|
16187
|
+
/** The words that were asked, from the request Entry that asked them. */
|
|
16188
|
+
question: external_exports.string(),
|
|
16189
|
+
/** Where it was asked — which is where the ruling goes (`POST /api/entries`). Null only
|
|
16190
|
+
* for a need whose request Entry is carried by no interactive Delivery, which nothing
|
|
16191
|
+
* can answer. */
|
|
16192
|
+
deliveryId: external_exports.string().nullable().default(null),
|
|
16193
|
+
/** The Entry the ruling is about. */
|
|
16194
|
+
aboutId: external_exports.string().nullable().default(null),
|
|
16195
|
+
/** Empty for a free-text question. */
|
|
16196
|
+
options: external_exports.array(OptionSchema2).default([]),
|
|
16197
|
+
select: external_exports.enum(["one", "many", "rank", "confirm", "text"]).default("text"),
|
|
16198
|
+
askedAt: external_exports.string(),
|
|
16199
|
+
/** Null while the question is open — which is how the page tells the two apart. */
|
|
16200
|
+
answeredAt: external_exports.string().nullable().default(null),
|
|
16201
|
+
/** The ruling in the person's own words, from the contribution that replied — not the
|
|
16202
|
+
* option id, which is not something anyone reads back. Null while it is open, and null
|
|
16203
|
+
* for a settled question whose reply carried nothing readable. */
|
|
16204
|
+
answer: external_exports.string().nullable().default(null)
|
|
16205
|
+
});
|
|
16206
|
+
var QueueItemSchema2 = external_exports.object({
|
|
16207
|
+
id: external_exports.string(),
|
|
16208
|
+
/** One-line headline — the first sentence of the outcome. */
|
|
16209
|
+
title: external_exports.string(),
|
|
16210
|
+
/** The outcome in full, verbatim: the person's own words are what an assignee sees. */
|
|
16211
|
+
intent: external_exports.string(),
|
|
16212
|
+
/** `ready` | `active` | `waiting` | `done` | `cancelled`, straight off the Goal. */
|
|
16213
|
+
state: external_exports.string(),
|
|
16214
|
+
/** Who holds it (a participant ref); null when nobody does yet. */
|
|
16215
|
+
assignee: external_exports.string().nullable().default(null),
|
|
16216
|
+
/** What the agent last said it was doing; null if it has said nothing. */
|
|
16217
|
+
progress: external_exports.string().nullable().default(null),
|
|
16218
|
+
reviewPending: external_exports.boolean().default(false),
|
|
16219
|
+
dueAt: external_exports.string().nullable().default(null),
|
|
16220
|
+
/** The Goal this one was opened under; null at the root. */
|
|
16221
|
+
parentGoalId: external_exports.string().nullable().default(null),
|
|
16222
|
+
/** Goals opened under this one — only those the same list holds. */
|
|
16223
|
+
childGoalIds: external_exports.array(external_exports.string()).default([]),
|
|
16224
|
+
/** Goals this one waits on (start or finish gates). */
|
|
16225
|
+
dependencyGoalIds: external_exports.array(external_exports.string()).default([]),
|
|
16226
|
+
/** True while any gate is on a Goal that is not done — the walk draws it dashed. */
|
|
16227
|
+
blocked: external_exports.boolean().default(false),
|
|
16228
|
+
/** Every decision need on it, open or settled — the page decides which to show. */
|
|
16229
|
+
questions: external_exports.array(QueueQuestionSchema2).default([]),
|
|
16230
|
+
createdAt: external_exports.string(),
|
|
16231
|
+
updatedAt: external_exports.string().nullable().default(null)
|
|
16731
16232
|
});
|
|
16732
16233
|
var NoteSourceSchema2 = external_exports.enum(["app", "call"]);
|
|
16733
16234
|
var NoteStatusSchema2 = external_exports.enum(["open", "assigned", "in_progress", "done"]);
|
|
@@ -16864,15 +16365,29 @@ var OAuthStartSchema2 = external_exports.object({
|
|
|
16864
16365
|
var DeliveryConfigSchema2 = external_exports.object({
|
|
16865
16366
|
tokenId: external_exports.string(),
|
|
16866
16367
|
mode: DeliveryModeSchema2,
|
|
16867
|
-
/** null when the
|
|
16868
|
-
*
|
|
16368
|
+
/** null when the deployment has no anon key configured. `self_hosted` is then REFUSED
|
|
16369
|
+
* (503 `self_hosted_unavailable`) rather than registered, so a self_hosted config always
|
|
16370
|
+
* carries credentials; only a `poll` registration can come back with null here. */
|
|
16869
16371
|
realtime: external_exports.object({ url: external_exports.string(), anonKey: external_exports.string() }).nullable()
|
|
16870
16372
|
});
|
|
16871
16373
|
var StatusSchema2 = external_exports.object({
|
|
16872
16374
|
name: external_exports.string(),
|
|
16873
16375
|
sessionMode: external_exports.enum(["default", "all_calls", "silent"]),
|
|
16874
16376
|
/** A phone is registered for push/ring (any push token on the account). */
|
|
16875
|
-
phone: external_exports.boolean()
|
|
16377
|
+
phone: external_exports.boolean(),
|
|
16378
|
+
/** HOW MANY THINGS ARE WAITING ON THIS IDENTITY — replies it never collected and requests
|
|
16379
|
+
* it never picked up. THE SAME NUMBER the harness's wake gate reads
|
|
16380
|
+
* (`pendingSummary.unacknowledged`), from the same function, because a statusline saying
|
|
16381
|
+
* zero while the sweep sees one is two ideas of "waiting".
|
|
16382
|
+
*
|
|
16383
|
+
* Why it is here at all (owner, 2026-09-07): nothing can interrupt an idle agent process
|
|
16384
|
+
* that nobody spawned, so a terminal session only learns of work by asking. The harness
|
|
16385
|
+
* used to paper over that by spawning a SECOND process on the identity; now it stands
|
|
16386
|
+
* back, correctly, and the person sitting at the terminal is the one who can act. A
|
|
16387
|
+
* coffee-beans request sat unread for three days.
|
|
16388
|
+
*
|
|
16389
|
+
* Optional: an older API sends no field, and the statusline then renders exactly as before. */
|
|
16390
|
+
waiting: external_exports.number().int().nonnegative().optional()
|
|
16876
16391
|
});
|
|
16877
16392
|
var EnvelopeRecipientSchema2 = external_exports.object({
|
|
16878
16393
|
keyId: external_exports.string(),
|
|
@@ -17038,15 +16553,146 @@ var FeedbackOutcomeSchema2 = external_exports.object({
|
|
|
17038
16553
|
message: external_exports.string(),
|
|
17039
16554
|
childIds: external_exports.array(external_exports.string()).optional()
|
|
17040
16555
|
});
|
|
17041
|
-
|
|
17042
|
-
|
|
17043
|
-
|
|
17044
|
-
|
|
17045
|
-
|
|
17046
|
-
|
|
17047
|
-
|
|
17048
|
-
|
|
17049
|
-
}
|
|
16556
|
+
|
|
16557
|
+
// src/main.ts
|
|
16558
|
+
var import_qrcode = __toESM(require_lib(), 1);
|
|
16559
|
+
import { homedir as homedir7, hostname } from "os";
|
|
16560
|
+
|
|
16561
|
+
// src/harness/catalog.ts
|
|
16562
|
+
import { spawnSync } from "child_process";
|
|
16563
|
+
import { existsSync as existsSync2 } from "fs";
|
|
16564
|
+
import { homedir as homedir2 } from "os";
|
|
16565
|
+
import { delimiter, dirname, join as join2 } from "path";
|
|
16566
|
+
var CATALOG = [
|
|
16567
|
+
{
|
|
16568
|
+
name: "claude",
|
|
16569
|
+
label: "Claude Code",
|
|
16570
|
+
cli: "claude",
|
|
16571
|
+
adapter: "claude-agent-acp",
|
|
16572
|
+
authProbe: ["claude", "auth", "status"],
|
|
16573
|
+
loginHint: "Run `claude` once in a terminal and sign in.",
|
|
16574
|
+
installCli: "curl -fsSL https://claude.ai/install.sh | bash",
|
|
16575
|
+
installAdapter: "npm install -g @agentclientprotocol/claude-agent-acp"
|
|
16576
|
+
},
|
|
16577
|
+
{
|
|
16578
|
+
name: "codex",
|
|
16579
|
+
label: "Codex",
|
|
16580
|
+
cli: "codex",
|
|
16581
|
+
adapter: "codex-acp",
|
|
16582
|
+
authProbe: ["codex", "login", "status"],
|
|
16583
|
+
loginHint: "Run `codex login` to authenticate.",
|
|
16584
|
+
installCli: "curl -fsSL https://chatgpt.com/codex/install.sh | sh",
|
|
16585
|
+
installAdapter: "npm install -g @agentclientprotocol/codex-acp"
|
|
16586
|
+
},
|
|
16587
|
+
{
|
|
16588
|
+
name: "agy",
|
|
16589
|
+
label: "Antigravity",
|
|
16590
|
+
cli: "agy",
|
|
16591
|
+
authProbe: ["agy", "help"],
|
|
16592
|
+
loginHint: "Install and set up Antigravity (`agy`).",
|
|
16593
|
+
installCli: "curl -fsSL https://antigravity.dev/install.sh | bash"
|
|
16594
|
+
}
|
|
16595
|
+
];
|
|
16596
|
+
function probeDirs(home = homedir2()) {
|
|
16597
|
+
return [
|
|
16598
|
+
dirname(process.execPath),
|
|
16599
|
+
join2(home, ".local", "bin"),
|
|
16600
|
+
"/usr/local/bin",
|
|
16601
|
+
"/opt/homebrew/bin",
|
|
16602
|
+
join2(home, ".npm-global", "bin")
|
|
16603
|
+
];
|
|
16604
|
+
}
|
|
16605
|
+
function which(bin) {
|
|
16606
|
+
const hit = spawnSync("which", [bin], { encoding: "utf8" });
|
|
16607
|
+
const fromPath = hit.status === 0 ? hit.stdout.trim() : "";
|
|
16608
|
+
if (fromPath) return fromPath;
|
|
16609
|
+
for (const dir of probeDirs()) {
|
|
16610
|
+
const candidate = join2(dir, bin);
|
|
16611
|
+
if (existsSync2(candidate)) return candidate;
|
|
16612
|
+
}
|
|
16613
|
+
return null;
|
|
16614
|
+
}
|
|
16615
|
+
function runProbe(argv) {
|
|
16616
|
+
const [cmd, ...args] = argv;
|
|
16617
|
+
if (!cmd) return false;
|
|
16618
|
+
const resolved = which(cmd);
|
|
16619
|
+
if (!resolved) return false;
|
|
16620
|
+
return spawnSync(resolved, args, {
|
|
16621
|
+
timeout: 1e4,
|
|
16622
|
+
stdio: "ignore",
|
|
16623
|
+
env: { ...process.env, PATH: [process.env.PATH, ...probeDirs()].filter(Boolean).join(delimiter) }
|
|
16624
|
+
}).status === 0;
|
|
16625
|
+
}
|
|
16626
|
+
function detect(entry, deps = {}) {
|
|
16627
|
+
const find = deps.which ?? which;
|
|
16628
|
+
const probe = deps.probe ?? runProbe;
|
|
16629
|
+
const base = { name: entry.name, label: entry.label };
|
|
16630
|
+
const cliPath = find(entry.cli);
|
|
16631
|
+
if (!cliPath) return { ...base, status: "missing", hint: `${entry.label} is not installed.`, install: entry.installCli };
|
|
16632
|
+
const adapterPath = entry.adapter ? find(entry.adapter) : null;
|
|
16633
|
+
if (entry.adapter && !adapterPath) {
|
|
16634
|
+
return {
|
|
16635
|
+
...base,
|
|
16636
|
+
status: "adapter-missing",
|
|
16637
|
+
hint: `${entry.label} is installed, but its ACP adapter (${entry.adapter}) is not.`,
|
|
16638
|
+
...entry.installAdapter ? { install: entry.installAdapter } : {}
|
|
16639
|
+
};
|
|
16640
|
+
}
|
|
16641
|
+
const bin = adapterPath ?? cliPath;
|
|
16642
|
+
if (!probe(entry.authProbe)) return { ...base, status: "login", bin, hint: entry.loginHint };
|
|
16643
|
+
return { ...base, status: "ready", bin };
|
|
16644
|
+
}
|
|
16645
|
+
function detectAll(deps = {}) {
|
|
16646
|
+
return CATALOG.map((entry) => detect(entry, deps));
|
|
16647
|
+
}
|
|
16648
|
+
|
|
16649
|
+
// src/host.ts
|
|
16650
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
16651
|
+
import { homedir as homedir5 } from "os";
|
|
16652
|
+
import { join as join4 } from "path";
|
|
16653
|
+
|
|
16654
|
+
// src/workspaces.ts
|
|
16655
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
16656
|
+
import { dirname as dirname2, join as join3, resolve } from "path";
|
|
16657
|
+
import { homedir as homedir3 } from "os";
|
|
16658
|
+
function workspacesFile() {
|
|
16659
|
+
return join3(homedir3(), ".paigy", "workspaces.json");
|
|
16660
|
+
}
|
|
16661
|
+
function listWorkspaces(deps) {
|
|
16662
|
+
try {
|
|
16663
|
+
const parsed = JSON.parse(readFileSync2(deps.file, "utf8"));
|
|
16664
|
+
return Array.isArray(parsed) ? parsed.filter((w) => typeof w === "string") : [];
|
|
16665
|
+
} catch {
|
|
16666
|
+
return [];
|
|
16667
|
+
}
|
|
16668
|
+
}
|
|
16669
|
+
function addWorkspace(dir, deps) {
|
|
16670
|
+
const next = resolve(dir.replace(/^~(?=$|\/)/, homedir3()));
|
|
16671
|
+
const all = listWorkspaces(deps);
|
|
16672
|
+
if (!all.includes(next)) {
|
|
16673
|
+
all.push(next);
|
|
16674
|
+
mkdirSync2(dirname2(deps.file), { recursive: true });
|
|
16675
|
+
writeFileSync2(deps.file, JSON.stringify(all, null, 2));
|
|
16676
|
+
}
|
|
16677
|
+
return all;
|
|
16678
|
+
}
|
|
16679
|
+
function removeWorkspace(dir, deps) {
|
|
16680
|
+
const all = listWorkspaces(deps).filter((w) => w !== dir);
|
|
16681
|
+
mkdirSync2(dirname2(deps.file), { recursive: true });
|
|
16682
|
+
writeFileSync2(deps.file, JSON.stringify(all, null, 2));
|
|
16683
|
+
return all;
|
|
16684
|
+
}
|
|
16685
|
+
function allowed(cwd, deps) {
|
|
16686
|
+
const target = resolve(cwd.replace(/^~(?=$|\/)/, homedir3()));
|
|
16687
|
+
return listWorkspaces(deps).some((w) => target === w || target.startsWith(`${w}/`));
|
|
16688
|
+
}
|
|
16689
|
+
function resolveWakeDir(pinned, deps) {
|
|
16690
|
+
if (pinned && allowed(pinned, deps)) {
|
|
16691
|
+
const dir = resolve(pinned.replace(/^~(?=$|\/)/, homedir3()));
|
|
16692
|
+
if ((deps.exists ?? existsSync3)(dir)) return dir;
|
|
16693
|
+
}
|
|
16694
|
+
return listWorkspaces(deps)[0];
|
|
16695
|
+
}
|
|
17050
16696
|
|
|
17051
16697
|
// src/paigy/activity.ts
|
|
17052
16698
|
function shortenPaths(s) {
|
|
@@ -17060,9 +16706,9 @@ function workLine(event) {
|
|
|
17060
16706
|
const line = shortenPaths([event.tool.trim(), note.trim()].filter(Boolean).join(" \u2014 ").replace(/\s+/g, " "));
|
|
17061
16707
|
return line.length > ACTIVITY_LINE_MAX2 ? `${line.slice(0, ACTIVITY_LINE_MAX2 - 1)}\u2026` : line;
|
|
17062
16708
|
}
|
|
17063
|
-
function pushWork(
|
|
17064
|
-
if (!line ||
|
|
17065
|
-
return [...
|
|
16709
|
+
function pushWork(lines2, line) {
|
|
16710
|
+
if (!line || lines2[lines2.length - 1] === line) return [...lines2];
|
|
16711
|
+
return [...lines2, line].slice(-ACTIVITY_LINES2);
|
|
17066
16712
|
}
|
|
17067
16713
|
function sameTail(a, b) {
|
|
17068
16714
|
return a.length === b.length && a.every((l, i) => l === b[i]);
|
|
@@ -17081,17 +16727,15 @@ function optionFor(options, decision) {
|
|
|
17081
16727
|
return options.find((o) => o.kind === want)?.optionId ?? null;
|
|
17082
16728
|
}
|
|
17083
16729
|
function createAcpDriver(opts) {
|
|
17084
|
-
return new AcpDriver(opts.cwd, opts.mode
|
|
16730
|
+
return new AcpDriver(opts.cwd, opts.mode);
|
|
17085
16731
|
}
|
|
17086
16732
|
var AcpDriver = class {
|
|
17087
|
-
constructor(cwd, mode
|
|
16733
|
+
constructor(cwd, mode) {
|
|
17088
16734
|
this.cwd = cwd;
|
|
17089
16735
|
this.mode = mode;
|
|
17090
|
-
this.mcp = mcp;
|
|
17091
16736
|
}
|
|
17092
16737
|
cwd;
|
|
17093
16738
|
mode;
|
|
17094
|
-
mcp;
|
|
17095
16739
|
nextId = 1;
|
|
17096
16740
|
initId;
|
|
17097
16741
|
sessionNewId;
|
|
@@ -17115,7 +16759,7 @@ var AcpDriver = class {
|
|
|
17115
16759
|
open() {
|
|
17116
16760
|
this.initId = this.nextId++;
|
|
17117
16761
|
return [
|
|
17118
|
-
|
|
16762
|
+
frame2({
|
|
17119
16763
|
id: this.initId,
|
|
17120
16764
|
method: "initialize",
|
|
17121
16765
|
params: {
|
|
@@ -17139,13 +16783,13 @@ var AcpDriver = class {
|
|
|
17139
16783
|
this.pending.delete(id);
|
|
17140
16784
|
const optionId = optionFor(request.options, decision);
|
|
17141
16785
|
return [
|
|
17142
|
-
optionId ?
|
|
16786
|
+
optionId ? frame2({ id: request.id, result: { outcome: { outcome: "selected", optionId } } }) : frame2({ id: request.id, result: { outcome: { outcome: "cancelled" } } })
|
|
17143
16787
|
];
|
|
17144
16788
|
}
|
|
17145
16789
|
/** Cancel anything still blocked — called on stop so the process can exit cleanly. */
|
|
17146
16790
|
close() {
|
|
17147
16791
|
const writes = [...this.pending.values()].map(
|
|
17148
|
-
(r) =>
|
|
16792
|
+
(r) => frame2({ id: r.id, result: { outcome: { outcome: "cancelled" } } })
|
|
17149
16793
|
);
|
|
17150
16794
|
this.pending.clear();
|
|
17151
16795
|
return writes;
|
|
@@ -17175,7 +16819,7 @@ var AcpDriver = class {
|
|
|
17175
16819
|
this.sessionNewId = this.nextId++;
|
|
17176
16820
|
return {
|
|
17177
16821
|
events: [],
|
|
17178
|
-
writes: [
|
|
16822
|
+
writes: [frame2({ id: this.sessionNewId, method: "session/new", params: { cwd: this.cwd, mcpServers: [] } })]
|
|
17179
16823
|
};
|
|
17180
16824
|
}
|
|
17181
16825
|
if (msg.id === this.sessionNewId) {
|
|
@@ -17190,12 +16834,12 @@ var AcpDriver = class {
|
|
|
17190
16834
|
this.sessionId = sessionId2;
|
|
17191
16835
|
const value = this.mode === "bypass" ? "bypassPermissions" : "default";
|
|
17192
16836
|
const writes = [
|
|
17193
|
-
|
|
16837
|
+
frame2({
|
|
17194
16838
|
id: this.nextId++,
|
|
17195
16839
|
method: "session/set_config_option",
|
|
17196
16840
|
params: { sessionId: sessionId2, configId: "mode", value }
|
|
17197
16841
|
}),
|
|
17198
|
-
|
|
16842
|
+
frame2({ id: this.nextId++, method: "session/set_mode", params: { sessionId: sessionId2, modeId: value } })
|
|
17199
16843
|
];
|
|
17200
16844
|
writes.push(...this.flush());
|
|
17201
16845
|
return { events: [], writes };
|
|
@@ -17252,7 +16896,7 @@ var AcpDriver = class {
|
|
|
17252
16896
|
if (msg.method !== "session/request_permission") {
|
|
17253
16897
|
return {
|
|
17254
16898
|
events: [],
|
|
17255
|
-
writes: [
|
|
16899
|
+
writes: [frame2({ id: msg.id, error: { code: -32601, message: `Method not found: ${msg.method}` } })]
|
|
17256
16900
|
};
|
|
17257
16901
|
}
|
|
17258
16902
|
const id = msg.id;
|
|
@@ -17266,7 +16910,7 @@ var AcpDriver = class {
|
|
|
17266
16910
|
// means "don't stall the agent", never "don't tell the user".
|
|
17267
16911
|
events: [{ kind: "turn", role: "agent", text: `auto-approved: ${title}`, tools: [tool] }],
|
|
17268
16912
|
writes: [
|
|
17269
|
-
optionId ?
|
|
16913
|
+
optionId ? frame2({ id, result: { outcome: { outcome: "selected", optionId } } }) : frame2({ id, result: { outcome: { outcome: "cancelled" } } })
|
|
17270
16914
|
]
|
|
17271
16915
|
};
|
|
17272
16916
|
}
|
|
@@ -17283,7 +16927,7 @@ var AcpDriver = class {
|
|
|
17283
16927
|
this.queued = [];
|
|
17284
16928
|
this.promptId = this.nextId++;
|
|
17285
16929
|
return [
|
|
17286
|
-
|
|
16930
|
+
frame2({
|
|
17287
16931
|
id: this.promptId,
|
|
17288
16932
|
method: "session/prompt",
|
|
17289
16933
|
params: { sessionId: this.sessionId, prompt: blocks }
|
|
@@ -17291,7 +16935,7 @@ var AcpDriver = class {
|
|
|
17291
16935
|
];
|
|
17292
16936
|
}
|
|
17293
16937
|
};
|
|
17294
|
-
var
|
|
16938
|
+
var frame2 = (body) => JSON.stringify({ jsonrpc: "2.0", ...body });
|
|
17295
16939
|
|
|
17296
16940
|
// src/harness/session.ts
|
|
17297
16941
|
var ADAPTER_BIN = {
|
|
@@ -17339,12 +16983,12 @@ function startSession(opts) {
|
|
|
17339
16983
|
opts.onEvent({ kind: "error", message: e.message });
|
|
17340
16984
|
opts.onExit?.();
|
|
17341
16985
|
});
|
|
17342
|
-
const driver = createAcpDriver({ cwd, mode: opts.mode
|
|
16986
|
+
const driver = createAcpDriver({ cwd, mode: opts.mode });
|
|
17343
16987
|
let buffer = "";
|
|
17344
16988
|
child.stdout?.on("data", (chunk) => {
|
|
17345
|
-
const { lines, rest } = splitLines(buffer, String(chunk));
|
|
16989
|
+
const { lines: lines2, rest } = splitLines(buffer, String(chunk));
|
|
17346
16990
|
buffer = rest;
|
|
17347
|
-
for (const line of
|
|
16991
|
+
for (const line of lines2) {
|
|
17348
16992
|
const { events, writes } = driver.handleLine(line);
|
|
17349
16993
|
write(writes);
|
|
17350
16994
|
for (const event of events) opts.onEvent(event);
|
|
@@ -17378,57 +17022,60 @@ function endsWithQuestion(text) {
|
|
|
17378
17022
|
}
|
|
17379
17023
|
var POLL_MS = 5e3;
|
|
17380
17024
|
var wait = () => new Promise((r) => setTimeout(r, POLL_MS));
|
|
17381
|
-
|
|
17382
|
-
|
|
17383
|
-
|
|
17384
|
-
|
|
17385
|
-
|
|
17386
|
-
|
|
17387
|
-
|
|
17025
|
+
var ended = (state) => state === "done" || state === "cancelled";
|
|
17026
|
+
function unheard(brief, state) {
|
|
17027
|
+
const seen = state.seen ??= /* @__PURE__ */ new Set();
|
|
17028
|
+
const words2 = [];
|
|
17029
|
+
for (const e of brief.entries ?? []) {
|
|
17030
|
+
if (seen.has(e.entryId) || !e.authorParticipant.startsWith("human:")) continue;
|
|
17031
|
+
seen.add(e.entryId);
|
|
17032
|
+
if (e.aboutId && seen.has(e.aboutId) && brief.answers?.some((a) => a.requestEntryId === e.aboutId)) continue;
|
|
17033
|
+
const text = entryWords(e).trim();
|
|
17034
|
+
if (text) words2.push(text);
|
|
17035
|
+
}
|
|
17036
|
+
return words2;
|
|
17037
|
+
}
|
|
17038
|
+
async function step(session, state, rail) {
|
|
17039
|
+
if (!state.goal) {
|
|
17040
|
+
if (!state.resting) return;
|
|
17041
|
+
const brief2 = await rail.claim();
|
|
17042
|
+
if (!brief2.goalId || brief2.revision === void 0) return;
|
|
17043
|
+
unheard(brief2, state);
|
|
17044
|
+
if (ended(brief2.state)) await rail.update(brief2.goalId, { revision: brief2.revision, changes: { reviewed: true }, reason: "Typed into the session." });
|
|
17045
|
+
else state.goal = { id: brief2.goalId, revision: brief2.revision };
|
|
17046
|
+
send(session, state, brief2.message);
|
|
17047
|
+
return;
|
|
17388
17048
|
}
|
|
17389
|
-
const
|
|
17390
|
-
|
|
17391
|
-
|
|
17392
|
-
|
|
17393
|
-
|
|
17394
|
-
|
|
17395
|
-
|
|
17396
|
-
|
|
17397
|
-
|
|
17398
|
-
|
|
17399
|
-
|
|
17400
|
-
|
|
17401
|
-
|
|
17402
|
-
} catch {
|
|
17403
|
-
}
|
|
17049
|
+
const brief = await rail.read(state.goal.id);
|
|
17050
|
+
state.goal.revision = brief.revision ?? state.goal.revision;
|
|
17051
|
+
for (const answer of brief.answers ?? []) {
|
|
17052
|
+
const resolve3 = state.asks?.get(answer.requestEntryId);
|
|
17053
|
+
if (!resolve3) continue;
|
|
17054
|
+
state.asks.delete(answer.requestEntryId);
|
|
17055
|
+
resolve3(answer.result);
|
|
17056
|
+
}
|
|
17057
|
+
if (ended(brief.state)) {
|
|
17058
|
+
state.goal = void 0;
|
|
17059
|
+
for (const resolve3 of state.asks?.values() ?? []) resolve3(null);
|
|
17060
|
+
state.asks?.clear();
|
|
17061
|
+
return;
|
|
17404
17062
|
}
|
|
17405
|
-
return
|
|
17063
|
+
if (state.asks?.size || !state.resting) return;
|
|
17064
|
+
const words2 = unheard(brief, state);
|
|
17065
|
+
if (words2.length) send(session, state, words2.join("\n\n"));
|
|
17406
17066
|
}
|
|
17407
|
-
|
|
17408
|
-
|
|
17409
|
-
|
|
17410
|
-
return answer?.trim() ? answer : null;
|
|
17067
|
+
function send(session, state, text) {
|
|
17068
|
+
state.resting = false;
|
|
17069
|
+
session.send(text);
|
|
17411
17070
|
}
|
|
17412
|
-
async function pump(session, state,
|
|
17071
|
+
async function pump(session, state, rail, opts) {
|
|
17413
17072
|
for (; ; ) {
|
|
17414
|
-
|
|
17415
|
-
state
|
|
17416
|
-
|
|
17417
|
-
const resolve3 = key !== void 0 ? state.asks?.get(key) : void 0;
|
|
17418
|
-
if (key !== void 0 && resolve3) {
|
|
17419
|
-
state.asks?.delete(key);
|
|
17420
|
-
resolve3(item.answer);
|
|
17421
|
-
continue;
|
|
17422
|
-
}
|
|
17423
|
-
const words2 = spokenText(item.answer);
|
|
17424
|
-
if (words2) {
|
|
17425
|
-
state.resting = false;
|
|
17426
|
-
session.send(words2);
|
|
17427
|
-
}
|
|
17073
|
+
try {
|
|
17074
|
+
await step(session, state, rail);
|
|
17075
|
+
} catch {
|
|
17428
17076
|
}
|
|
17429
|
-
|
|
17430
|
-
|
|
17431
|
-
await (deps.pause ?? wait)();
|
|
17077
|
+
if (!await opts.onIdle()) return;
|
|
17078
|
+
await (opts.pause ?? wait)();
|
|
17432
17079
|
}
|
|
17433
17080
|
}
|
|
17434
17081
|
|
|
@@ -17450,195 +17097,107 @@ var DESTRUCTIVE = [
|
|
|
17450
17097
|
function isDestructive(summary) {
|
|
17451
17098
|
return DESTRUCTIVE.some((re) => re.test(summary));
|
|
17452
17099
|
}
|
|
17453
|
-
function levelFor(event) {
|
|
17454
|
-
switch (event.kind) {
|
|
17455
|
-
case "turn":
|
|
17456
|
-
return "inbox";
|
|
17457
|
-
case "permission":
|
|
17458
|
-
return isDestructive(event.summary) ? "call" : "banner";
|
|
17459
|
-
case "idle":
|
|
17460
|
-
if (endsWithQuestion(event.result)) return "banner";
|
|
17461
|
-
return event.failed ? "push" : "inbox";
|
|
17462
|
-
case "work":
|
|
17463
|
-
return "inbox";
|
|
17464
|
-
// moot — entryFor drops work before a level ever matters
|
|
17465
|
-
case "error":
|
|
17466
|
-
return "inbox";
|
|
17467
|
-
}
|
|
17468
|
-
}
|
|
17469
17100
|
|
|
17470
17101
|
// src/paigy/bridge.ts
|
|
17102
|
+
function railFor(opts) {
|
|
17103
|
+
return {
|
|
17104
|
+
claim: () => claimGoal(void 0, opts),
|
|
17105
|
+
read: (goalId) => getGoal(goalId, opts),
|
|
17106
|
+
update: (goalId, input) => updateGoal(goalId, input, opts),
|
|
17107
|
+
// One read and back: the pump owns the waiting, so a Call must not hold a window here.
|
|
17108
|
+
contact: (input) => contact(input, { ...opts, waits: false })
|
|
17109
|
+
};
|
|
17110
|
+
}
|
|
17471
17111
|
function cancelAsks(state) {
|
|
17472
|
-
for (const resolve3 of state.asks?.values() ?? []) resolve3(
|
|
17112
|
+
for (const resolve3 of state.asks?.values() ?? []) resolve3(null);
|
|
17473
17113
|
state.asks?.clear();
|
|
17474
17114
|
}
|
|
17475
|
-
|
|
17476
|
-
|
|
17477
|
-
|
|
17478
|
-
|
|
17479
|
-
|
|
17480
|
-
|
|
17481
|
-
|
|
17482
|
-
|
|
17483
|
-
case "turn": {
|
|
17484
|
-
const who = event.role === "agent" ? "Agent" : "You";
|
|
17485
|
-
const body = event.text ? event.role === "agent" ? event.text : `${who}: ${event.text}` : `${who} ran ${(event.tools ?? []).join(", ")}.`;
|
|
17486
|
-
return {
|
|
17487
|
-
...common,
|
|
17488
|
-
ask: body
|
|
17489
|
-
// No `select`: the contract refuses it on the `ask` form ("the broker derives it"),
|
|
17490
|
-
// and with no options it derives "text" anyway — the shape this wants, since any
|
|
17491
|
-
// inbox row can be replied to and a reply to history is just the user initiating
|
|
17492
|
-
// (the pump feeds it back in).
|
|
17493
|
-
};
|
|
17494
|
-
}
|
|
17495
|
-
case "permission":
|
|
17496
|
-
return {
|
|
17497
|
-
...common,
|
|
17498
|
-
context: {
|
|
17499
|
-
title: clip(event.summary),
|
|
17500
|
-
description: [
|
|
17501
|
-
`The agent is blocked waiting to run: ${event.summary}`,
|
|
17502
|
-
...event.reason ? [event.reason] : []
|
|
17503
|
-
]
|
|
17504
|
-
},
|
|
17505
|
-
select: "confirm",
|
|
17506
|
-
confirmStyle: "approve",
|
|
17507
|
-
// Literally true, and the inbox uses it: a blocking item gets the badge and the
|
|
17508
|
-
// extra confirm-before-dismiss, which is exactly right for something holding a
|
|
17509
|
-
// real process still.
|
|
17510
|
-
blocking: true
|
|
17511
|
-
};
|
|
17512
|
-
case "idle": {
|
|
17513
|
-
const asking = endsWithQuestion(event.result);
|
|
17514
|
-
const result = event.result?.trim();
|
|
17515
|
-
const norm = (t) => (t ?? "").replace(/\s+/g, " ").trim();
|
|
17516
|
-
if (result && !event.failed && norm(result) === norm(state.lastAgentText)) {
|
|
17517
|
-
if (!asking) return null;
|
|
17518
|
-
const spans = unitsOf2(result);
|
|
17519
|
-
const tail = spans.map((sp) => result.slice(sp.start, sp.end)).reverse().find((t) => t.includes("?"));
|
|
17520
|
-
return { ...common, ask: tail ?? result, blocking: true };
|
|
17521
|
-
}
|
|
17522
|
-
const lead = asking ? "" : event.failed ? "The agent stopped without finishing. " : "Turn complete. ";
|
|
17523
|
-
return {
|
|
17524
|
-
...common,
|
|
17525
|
-
ask: `${lead}${result || (event.failed ? "No result reported." : "Done.")}`,
|
|
17526
|
-
...asking ? { blocking: true } : {}
|
|
17527
|
-
};
|
|
17115
|
+
var ALLOW = "Allow";
|
|
17116
|
+
function decisionFrom(result) {
|
|
17117
|
+
if (result && "kind" in result) {
|
|
17118
|
+
if (result.kind === "one") return result.option.label === ALLOW ? { allow: true } : { allow: false, reason: "Denied from Paigy" };
|
|
17119
|
+
if (result.kind === "confirm") return result.approved ? { allow: true } : { allow: false, reason: "Denied from Paigy" };
|
|
17120
|
+
if (result.kind === "text") {
|
|
17121
|
+
if (/^(yes|y|ok|okay|approve[d]?|allow|go ahead|do it)\b/.test(result.text.trim().toLowerCase())) return { allow: true };
|
|
17122
|
+
return { allow: false, reason: result.text };
|
|
17528
17123
|
}
|
|
17529
|
-
case "work":
|
|
17530
|
-
return null;
|
|
17531
|
-
// log-only by contract — the live texture of the working log
|
|
17532
|
-
case "error":
|
|
17533
|
-
return null;
|
|
17534
|
-
}
|
|
17535
|
-
}
|
|
17536
|
-
function decisionFrom(answer) {
|
|
17537
|
-
if (answer.kind === "confirm") {
|
|
17538
|
-
return answer.approved ? { allow: true } : { allow: false, reason: "Denied from Paigy" };
|
|
17539
|
-
}
|
|
17540
|
-
if (answer.kind === "text") {
|
|
17541
|
-
const said = answer.text.trim().toLowerCase();
|
|
17542
|
-
if (/^(yes|y|ok|okay|approve[d]?|allow|go ahead|do it)\b/.test(said)) return { allow: true };
|
|
17543
|
-
return { allow: false, reason: answer.text };
|
|
17544
|
-
}
|
|
17545
|
-
if (answer.kind === "turns") {
|
|
17546
|
-
const first = answer.turns[0]?.reply ?? "";
|
|
17547
|
-
return decisionFrom({ kind: "text", text: first });
|
|
17548
17124
|
}
|
|
17549
17125
|
return { allow: false, reason: "No approval given" };
|
|
17550
17126
|
}
|
|
17551
|
-
|
|
17552
|
-
|
|
17553
|
-
|
|
17554
|
-
|
|
17555
|
-
|
|
17556
|
-
|
|
17557
|
-
|
|
17558
|
-
|
|
17559
|
-
|
|
17560
|
-
|
|
17127
|
+
function wordsOf(result) {
|
|
17128
|
+
if (!result || !("kind" in result)) return null;
|
|
17129
|
+
switch (result.kind) {
|
|
17130
|
+
case "text":
|
|
17131
|
+
return result.text.trim() || null;
|
|
17132
|
+
case "confirm":
|
|
17133
|
+
return result.approved ? "yes" : "no";
|
|
17134
|
+
case "one":
|
|
17135
|
+
return result.option.label;
|
|
17136
|
+
case "many":
|
|
17137
|
+
case "rank":
|
|
17138
|
+
return result.options.map((o) => o.label).join(", ");
|
|
17561
17139
|
}
|
|
17140
|
+
}
|
|
17141
|
+
async function ask(state, rail, input) {
|
|
17142
|
+
if (!state.goal) return null;
|
|
17562
17143
|
try {
|
|
17563
|
-
const
|
|
17564
|
-
|
|
17565
|
-
|
|
17144
|
+
const sent = await rail.contact({
|
|
17145
|
+
goalIds: [state.goal.id],
|
|
17146
|
+
ask: input.ask,
|
|
17147
|
+
waiting: "hard",
|
|
17148
|
+
channel: input.call ? "call" : "notification",
|
|
17149
|
+
...input.options ? { options: input.options } : {}
|
|
17150
|
+
});
|
|
17151
|
+
const entryId = sent.requestEntryIds[0];
|
|
17152
|
+
if (!entryId) return null;
|
|
17153
|
+
(state.seen ??= /* @__PURE__ */ new Set()).add(entryId);
|
|
17154
|
+
return await new Promise((resolve3) => (state.asks ??= /* @__PURE__ */ new Map()).set(entryId, resolve3));
|
|
17566
17155
|
} catch {
|
|
17567
|
-
return
|
|
17156
|
+
return null;
|
|
17568
17157
|
}
|
|
17569
17158
|
}
|
|
17570
|
-
function
|
|
17571
|
-
|
|
17572
|
-
|
|
17159
|
+
async function askPermission(event, state, rail) {
|
|
17160
|
+
return decisionFrom(await ask(state, rail, {
|
|
17161
|
+
ask: [`The agent is blocked waiting to run: ${event.summary}`, ...event.reason ? [event.reason] : []].join("\n\n"),
|
|
17162
|
+
options: [{ label: ALLOW }, { label: "Deny" }],
|
|
17163
|
+
call: isDestructive(event.summary)
|
|
17164
|
+
}));
|
|
17573
17165
|
}
|
|
17574
|
-
async function
|
|
17575
|
-
|
|
17576
|
-
if (!entry) return { decision: { allow: false, reason: "Could not ask" } };
|
|
17577
|
-
try {
|
|
17578
|
-
const sent = await (deps.submit ?? submitNotification)(NotifyRequestSchema2.parse(entry));
|
|
17579
|
-
(state.mine ??= /* @__PURE__ */ new Set()).add(sent.notificationId);
|
|
17580
|
-
const answer = await awaitAnswer(state, sent.notificationId);
|
|
17581
|
-
return { decision: decisionFrom(answer), parentId: sent.parentId };
|
|
17582
|
-
} catch (e) {
|
|
17583
|
-
return { decision: { allow: false, reason: `Could not reach Paigy: ${e.message}` } };
|
|
17584
|
-
}
|
|
17166
|
+
async function askQuestion(question, state, rail) {
|
|
17167
|
+
return wordsOf(await ask(state, rail, { ask: question }));
|
|
17585
17168
|
}
|
|
17586
|
-
async function
|
|
17587
|
-
const
|
|
17588
|
-
|
|
17589
|
-
|
|
17590
|
-
|
|
17591
|
-
// Prose in — see `entryFor`'s `turn` case. An agent's question is the case most likely
|
|
17592
|
-
// to run long, and its title was a clipped prefix of itself.
|
|
17593
|
-
ask: question,
|
|
17594
|
-
blocking: true,
|
|
17595
|
-
urgency: "banner"
|
|
17596
|
-
};
|
|
17169
|
+
async function finish(result, state, rail) {
|
|
17170
|
+
const goal = state.goal;
|
|
17171
|
+
if (!goal) return;
|
|
17172
|
+
state.goal = void 0;
|
|
17173
|
+
const report = result?.trim() || "Done.";
|
|
17597
17174
|
try {
|
|
17598
|
-
|
|
17599
|
-
|
|
17600
|
-
|
|
17175
|
+
await rail.contact({ goalIds: [goal.id], ask: report, waiting: "none", channel: "notification" });
|
|
17176
|
+
const { revision } = await rail.read(goal.id);
|
|
17177
|
+
await rail.update(goal.id, { revision: revision ?? goal.revision, changes: { state: "done", progress: report.slice(0, 1e4) }, reason: "The agent finished its turn without asking anything." });
|
|
17601
17178
|
} catch {
|
|
17602
|
-
return null;
|
|
17603
17179
|
}
|
|
17604
17180
|
}
|
|
17605
|
-
function
|
|
17606
|
-
|
|
17607
|
-
|
|
17608
|
-
|
|
17609
|
-
|
|
17610
|
-
return answer.label ?? answer.optionId;
|
|
17611
|
-
case "multi":
|
|
17612
|
-
case "ranked":
|
|
17613
|
-
return (answer.labels ?? answer.optionIds).join(", ");
|
|
17614
|
-
case "confirm":
|
|
17615
|
-
return answer.approved ? "yes" : "no";
|
|
17616
|
-
case "clarify":
|
|
17617
|
-
return answer.chunks.join(" ");
|
|
17618
|
-
// A call log: the user's replies are the instruction, the bot's prompts are not.
|
|
17619
|
-
case "turns":
|
|
17620
|
-
return answer.turns.map((t) => t.reply).join(" ").trim() || null;
|
|
17621
|
-
// An auto-answer derived from the user's past decisions — reads like a fast human.
|
|
17622
|
-
case "precedent":
|
|
17623
|
-
return answer.answer;
|
|
17624
|
-
case "ignored":
|
|
17625
|
-
return null;
|
|
17181
|
+
async function afterTurn(event, state, rail) {
|
|
17182
|
+
if (!event.failed && !endsWithQuestion(event.result)) {
|
|
17183
|
+
await finish(event.result, state, rail);
|
|
17184
|
+
state.resting = true;
|
|
17185
|
+
return null;
|
|
17626
17186
|
}
|
|
17187
|
+
const reply = await askQuestion(event.failed ? `The agent stopped without finishing.
|
|
17188
|
+
|
|
17189
|
+
${event.result?.trim() || "No result reported."}` : event.result ?? "", state, rail);
|
|
17190
|
+
if (!reply) state.resting = true;
|
|
17191
|
+
return reply;
|
|
17627
17192
|
}
|
|
17628
|
-
var clip = (s) => (s.length > 120 ? `${s.slice(0, 117)}\u2026` : s) || "(no text)";
|
|
17629
17193
|
|
|
17630
17194
|
// src/run.ts
|
|
17631
17195
|
function runHarness(opts) {
|
|
17632
17196
|
let running = true;
|
|
17633
17197
|
let tail = [];
|
|
17634
|
-
const state = {
|
|
17198
|
+
const state = { resting: true };
|
|
17635
17199
|
let session = null;
|
|
17636
|
-
const
|
|
17637
|
-
const deps = {
|
|
17638
|
-
submit: (req) => submitNotification(req, asMe),
|
|
17639
|
-
check: () => checkReplies(asMe),
|
|
17640
|
-
ack: (id, st) => setTaskState(id, st, asMe)
|
|
17641
|
-
};
|
|
17200
|
+
const rail = railFor({ token: opts.token });
|
|
17642
17201
|
async function handle(event) {
|
|
17643
17202
|
if (event.kind === "error") {
|
|
17644
17203
|
opts.log(`\u26A0 ${event.message}`);
|
|
@@ -17652,8 +17211,7 @@ function runHarness(opts) {
|
|
|
17652
17211
|
return;
|
|
17653
17212
|
}
|
|
17654
17213
|
opts.log(`\u23F8 blocked: ${event.summary} \u2014 asking your phone`);
|
|
17655
|
-
const
|
|
17656
|
-
state.parentId ??= parentId2;
|
|
17214
|
+
const decision = await askPermission(event, state, rail);
|
|
17657
17215
|
session?.respond(event.id, decision);
|
|
17658
17216
|
opts.log(decision.allow ? `\u2713 approved: ${event.summary}` : `\u2717 denied: ${event.summary}`);
|
|
17659
17217
|
return;
|
|
@@ -17663,41 +17221,39 @@ function runHarness(opts) {
|
|
|
17663
17221
|
tail = pushWork(tail, workLine(event));
|
|
17664
17222
|
return;
|
|
17665
17223
|
}
|
|
17666
|
-
const { parentId } = await mirror(event, state, deps);
|
|
17667
|
-
state.parentId ??= parentId;
|
|
17668
17224
|
if (event.kind === "turn") opts.log(`${event.role}: ${event.text.split("\n")[0] ?? ""}`);
|
|
17669
17225
|
if (event.kind === "idle") {
|
|
17670
|
-
state.resting = true;
|
|
17671
17226
|
tail = [];
|
|
17672
17227
|
if (endsWithQuestion(event.result)) {
|
|
17673
17228
|
const local = await opts.localAsk?.question?.(event.result ?? "") ?? null;
|
|
17674
17229
|
if (local?.trim() && session && running) {
|
|
17675
17230
|
opts.log(`you (here): ${local.split("\n")[0] ?? ""}`);
|
|
17231
|
+
state.resting = false;
|
|
17676
17232
|
session.send(local);
|
|
17677
17233
|
return;
|
|
17678
17234
|
}
|
|
17679
17235
|
}
|
|
17680
|
-
const reply = await
|
|
17236
|
+
const reply = await afterTurn(event, state, rail);
|
|
17681
17237
|
if (reply && session && running) {
|
|
17682
17238
|
opts.log(`you: ${reply.split("\n")[0] ?? ""}`);
|
|
17683
17239
|
session.send(reply);
|
|
17684
17240
|
}
|
|
17685
17241
|
}
|
|
17686
17242
|
}
|
|
17687
|
-
const paigyMcp = opts.exclusive && opts.token ? [{ name: "paigy", command: "npx", args: ["-y", "@paigy/mcp@latest"], env: { PAIGY_TOKEN: opts.token } }] : void 0;
|
|
17688
|
-
if (paigyMcp) state.sharedWithAgent = true;
|
|
17689
17243
|
session = startSession({
|
|
17690
17244
|
harness: opts.harness,
|
|
17691
17245
|
cwd: opts.cwd,
|
|
17692
17246
|
mode: opts.mode,
|
|
17693
|
-
...paigyMcp ? { mcp: paigyMcp } : {},
|
|
17694
17247
|
...opts.token ? { token: opts.token } : {},
|
|
17695
17248
|
...opts.bin ? { bin: opts.bin } : {},
|
|
17696
17249
|
...opts.onExit ? { onExit: opts.onExit } : {},
|
|
17697
17250
|
onEvent: (event) => void handle(event).catch((err) => opts.log(`\u26A0 ${err.message}`))
|
|
17698
17251
|
});
|
|
17699
|
-
if (opts.prompt)
|
|
17700
|
-
|
|
17252
|
+
if (opts.prompt) {
|
|
17253
|
+
state.resting = false;
|
|
17254
|
+
session.send(opts.prompt);
|
|
17255
|
+
}
|
|
17256
|
+
void pump(session, state, rail, { onIdle: () => running }).catch(
|
|
17701
17257
|
(e) => opts.log(`\u26A0 conversation loop stopped: ${e.message}`)
|
|
17702
17258
|
);
|
|
17703
17259
|
opts.log(`\u25B6 started ${opts.harness} in ${opts.cwd} (${opts.mode} mode)`);
|
|
@@ -17738,21 +17294,17 @@ function hostElsewhere() {
|
|
|
17738
17294
|
const s = readHostState();
|
|
17739
17295
|
return !!s && s.pid !== process.pid;
|
|
17740
17296
|
}
|
|
17741
|
-
var IDLE_WAKE_MS = 10 * 6e4;
|
|
17742
17297
|
var runKey = (slot) => `slot:${slot}`;
|
|
17743
17298
|
var claimedRun = (sessionId2) => {
|
|
17744
17299
|
const slot = sessionSlot(sessionId2);
|
|
17745
17300
|
return { slot, key: runKey(slot) };
|
|
17746
17301
|
};
|
|
17747
17302
|
async function slotHasWaitingWork(token) {
|
|
17748
|
-
return (await
|
|
17303
|
+
return (await checkReplies({ token })).deliveries.length > 0;
|
|
17749
17304
|
}
|
|
17750
17305
|
var PRESENCE_FRESH_MS = 3 * 6e4;
|
|
17751
|
-
function wakeAction(live,
|
|
17752
|
-
if (live)
|
|
17753
|
-
if (seenAt === void 0) return "skip";
|
|
17754
|
-
return now - seenAt >= IDLE_WAKE_MS ? "prompt" : "skip";
|
|
17755
|
-
}
|
|
17306
|
+
function wakeAction(live, now, serverSeenAt) {
|
|
17307
|
+
if (live) return "skip";
|
|
17756
17308
|
if (serverSeenAt !== void 0 && now - serverSeenAt < PRESENCE_FRESH_MS) return "skip";
|
|
17757
17309
|
return "spawn";
|
|
17758
17310
|
}
|
|
@@ -17780,6 +17332,8 @@ function startHost(opts) {
|
|
|
17780
17332
|
workspaces: listWorkspaces(opts.wsDeps)
|
|
17781
17333
|
}, asHost).catch(() => {
|
|
17782
17334
|
});
|
|
17335
|
+
void listenSlots().then(sweepSlots).catch(() => {
|
|
17336
|
+
});
|
|
17783
17337
|
};
|
|
17784
17338
|
async function claimAndSpawn() {
|
|
17785
17339
|
const specs = await claimSessions(asHost).catch(() => []);
|
|
@@ -17791,8 +17345,7 @@ function startHost(opts) {
|
|
|
17791
17345
|
harness: spec.harness,
|
|
17792
17346
|
cwd: spec.workspace,
|
|
17793
17347
|
mode: "bypass",
|
|
17794
|
-
prompt:
|
|
17795
|
-
exclusive: true,
|
|
17348
|
+
prompt: "",
|
|
17796
17349
|
token: spec.token,
|
|
17797
17350
|
onExit: () => {
|
|
17798
17351
|
runs.delete(key);
|
|
@@ -17812,45 +17365,39 @@ function startHost(opts) {
|
|
|
17812
17365
|
}
|
|
17813
17366
|
}
|
|
17814
17367
|
const SLOT_HARNESS = { "mcp-agent": "claude", codex: "codex", antigravity: "agy" };
|
|
17815
|
-
|
|
17816
|
-
async function sweepSlots() {
|
|
17368
|
+
async function presenceMap() {
|
|
17817
17369
|
const presence = /* @__PURE__ */ new Map();
|
|
17818
17370
|
try {
|
|
17819
17371
|
for (const c of (await listConnections(asHost)).items) {
|
|
17820
17372
|
if (c.lastSeenAt) presence.set(c.id, Date.parse(c.lastSeenAt));
|
|
17821
17373
|
}
|
|
17374
|
+
lastPresence = presence;
|
|
17822
17375
|
} catch {
|
|
17823
17376
|
}
|
|
17824
|
-
|
|
17377
|
+
return presence;
|
|
17378
|
+
}
|
|
17379
|
+
let lastPresence = /* @__PURE__ */ new Map();
|
|
17380
|
+
async function wakeSlot(slot, presence) {
|
|
17381
|
+
{
|
|
17825
17382
|
const harness = SLOT_HARNESS[slot] ?? (slot === "Desktop" ? void 0 : "claude");
|
|
17826
17383
|
const key = runKey(slot);
|
|
17827
|
-
if (!harness)
|
|
17384
|
+
if (!harness) return;
|
|
17828
17385
|
const live = runs.get(key);
|
|
17829
17386
|
const tokenId = slotIdentity(slot).tokenId;
|
|
17830
17387
|
const seenByServer = tokenId ? presence.get(tokenId) : void 0;
|
|
17831
|
-
|
|
17832
|
-
if (action === "skip") continue;
|
|
17388
|
+
if (wakeAction(!!live, Date.now(), seenByServer) === "skip") return;
|
|
17833
17389
|
const token = readToken(slot);
|
|
17834
|
-
if (!token)
|
|
17390
|
+
if (!token) return;
|
|
17835
17391
|
const workspace = resolveWakeDir(slotIdentity(slot).workspace, opts.wsDeps);
|
|
17836
|
-
if (!workspace)
|
|
17837
|
-
if (!await slotHasWaitingWork(token).catch(() => false))
|
|
17392
|
+
if (!workspace) return;
|
|
17393
|
+
if (!await slotHasWaitingWork(token).catch(() => false)) return;
|
|
17838
17394
|
const label = slotName(slot) ?? slot;
|
|
17839
|
-
if (live) {
|
|
17840
|
-
const seen = published.get(key);
|
|
17841
|
-
const quiet = seen ? Math.round((Date.now() - seen.movedAt) / 6e4) : 0;
|
|
17842
|
-
live.run.send(wakePrompt(false));
|
|
17843
|
-
if (seen) seen.movedAt = Date.now();
|
|
17844
|
-
opts.log(`\u25B6 re-woke ${label} \u2014 quiet ${quiet}m with work waiting`);
|
|
17845
|
-
continue;
|
|
17846
|
-
}
|
|
17847
17395
|
const log2 = (line) => opts.log(`[${label}] ${line}`);
|
|
17848
17396
|
const run = runHarness({
|
|
17849
17397
|
harness,
|
|
17850
17398
|
cwd: workspace,
|
|
17851
17399
|
mode: "bypass",
|
|
17852
|
-
prompt:
|
|
17853
|
-
exclusive: true,
|
|
17400
|
+
prompt: "",
|
|
17854
17401
|
token,
|
|
17855
17402
|
// A dead run must not squat the slot — evict so the next wake can respawn.
|
|
17856
17403
|
onExit: () => {
|
|
@@ -17865,20 +17412,54 @@ function startHost(opts) {
|
|
|
17865
17412
|
opts.log(`\u25B6 woke ${label} (${harness}) \u2014 work was waiting in ${workspace}`);
|
|
17866
17413
|
}
|
|
17867
17414
|
}
|
|
17415
|
+
const listening = /* @__PURE__ */ new Map();
|
|
17416
|
+
const joining = /* @__PURE__ */ new Set();
|
|
17417
|
+
async function listenSlots() {
|
|
17418
|
+
const want = new Set(listSlots().filter((slot) => slot !== "Desktop" && !!readToken(slot)));
|
|
17419
|
+
for (const [slot, sub] of [...listening]) {
|
|
17420
|
+
if (want.has(slot)) continue;
|
|
17421
|
+
listening.delete(slot);
|
|
17422
|
+
void sub.close().catch(() => {
|
|
17423
|
+
});
|
|
17424
|
+
}
|
|
17425
|
+
for (const slot of want) {
|
|
17426
|
+
if (listening.has(slot) || joining.has(slot)) continue;
|
|
17427
|
+
const token = readToken(slot);
|
|
17428
|
+
if (!token) continue;
|
|
17429
|
+
joining.add(slot);
|
|
17430
|
+
try {
|
|
17431
|
+
const sub = await subscribeWake(
|
|
17432
|
+
() => void presenceMap().then((presence) => wakeSlot(slot, presence)).catch(() => {
|
|
17433
|
+
}),
|
|
17434
|
+
{ token }
|
|
17435
|
+
);
|
|
17436
|
+
listening.set(slot, sub);
|
|
17437
|
+
opts.log(`\u25C9 listening for ${slotName(slot) ?? slot}`);
|
|
17438
|
+
} catch (e) {
|
|
17439
|
+
opts.log(`\u25CC not listening for ${slotName(slot) ?? slot} \u2014 ${e.message}`);
|
|
17440
|
+
} finally {
|
|
17441
|
+
joining.delete(slot);
|
|
17442
|
+
}
|
|
17443
|
+
}
|
|
17444
|
+
}
|
|
17445
|
+
async function sweepSlots() {
|
|
17446
|
+
const presence = await presenceMap();
|
|
17447
|
+
for (const slot of listSlots()) await wakeSlot(slot, presence);
|
|
17448
|
+
}
|
|
17868
17449
|
const ACTIVITY_MS = 2e3;
|
|
17869
17450
|
const published = /* @__PURE__ */ new Map();
|
|
17870
17451
|
const streamActivity = () => {
|
|
17871
|
-
const publishTail = (token,
|
|
17872
|
-
void heartbeat(void 0, { token, activity: { lines, at: (/* @__PURE__ */ new Date()).toISOString() } }).catch(() => {
|
|
17452
|
+
const publishTail = (token, lines2) => {
|
|
17453
|
+
void heartbeat(void 0, { token, activity: { lines: lines2, at: (/* @__PURE__ */ new Date()).toISOString() } }).catch(() => {
|
|
17873
17454
|
});
|
|
17874
17455
|
};
|
|
17875
17456
|
for (const [key, r] of runs) {
|
|
17876
17457
|
if (!r.token) continue;
|
|
17877
|
-
const
|
|
17458
|
+
const lines2 = r.run.tail();
|
|
17878
17459
|
const was = published.get(key);
|
|
17879
|
-
if (was && sameTail(was.lines,
|
|
17880
|
-
published.set(key, { token: r.token, lines
|
|
17881
|
-
publishTail(r.token,
|
|
17460
|
+
if (was && sameTail(was.lines, lines2)) continue;
|
|
17461
|
+
published.set(key, { token: r.token, lines: lines2 });
|
|
17462
|
+
publishTail(r.token, lines2);
|
|
17882
17463
|
}
|
|
17883
17464
|
for (const [key, was] of published) {
|
|
17884
17465
|
if (runs.has(key)) continue;
|
|
@@ -17896,7 +17477,6 @@ function startHost(opts) {
|
|
|
17896
17477
|
const pulse = setInterval(beat, 6e4);
|
|
17897
17478
|
const spawnPoll = setInterval(() => {
|
|
17898
17479
|
void claimAndSpawn();
|
|
17899
|
-
void sweepSlots();
|
|
17900
17480
|
publish();
|
|
17901
17481
|
}, 5e3);
|
|
17902
17482
|
const activityTick = setInterval(streamActivity, ACTIVITY_MS);
|
|
@@ -17907,6 +17487,12 @@ function startHost(opts) {
|
|
|
17907
17487
|
}
|
|
17908
17488
|
runs.clear();
|
|
17909
17489
|
};
|
|
17490
|
+
const stopListening = async () => {
|
|
17491
|
+
const subs = [...listening.values()];
|
|
17492
|
+
listening.clear();
|
|
17493
|
+
await Promise.all(subs.map((sub) => sub.close().catch(() => {
|
|
17494
|
+
})));
|
|
17495
|
+
};
|
|
17910
17496
|
const api = {
|
|
17911
17497
|
sessions: () => [...runs.entries()].map(([id, r]) => [id, r.label]),
|
|
17912
17498
|
// Every identity this machine holds, plus what it's doing right now. Slots are the
|
|
@@ -17927,7 +17513,16 @@ function startHost(opts) {
|
|
|
17927
17513
|
slot,
|
|
17928
17514
|
tokenId: id.tokenId,
|
|
17929
17515
|
voice: id.voice,
|
|
17930
|
-
|
|
17516
|
+
// RUNNING IS ABOUT THE IDENTITY, NOT ABOUT US (owner, 2026-09-14). This was
|
|
17517
|
+
// `live.has(name)` — "this host spawned it and it has not exited" — which is a fact
|
|
17518
|
+
// about our own process table, not about the agent. An ordinary terminal session
|
|
17519
|
+
// heartbeating every 60 s read as NOT running, and after a host restart so did the
|
|
17520
|
+
// sessions we started ourselves. The server already knows: `last_seen_at` on the
|
|
17521
|
+
// agent's own token, the same evidence `wakeAction` stands back for. So: a run we
|
|
17522
|
+
// hold, OR a process the server heard from inside `PRESENCE_FRESH_MS`.
|
|
17523
|
+
running: live.has(name) || !!id.tokenId && Date.now() - (lastPresence.get(id.tokenId) ?? 0) < PRESENCE_FRESH_MS,
|
|
17524
|
+
// `working` stays ours alone — it means a run WE hold is mid-turn, which is the only
|
|
17525
|
+
// thing a tail can tell us. A process we did not spawn has no tail here to read.
|
|
17931
17526
|
working: live.get(name)?.working() ?? false
|
|
17932
17527
|
};
|
|
17933
17528
|
});
|
|
@@ -17937,6 +17532,7 @@ function startHost(opts) {
|
|
|
17937
17532
|
clearInterval(pulse);
|
|
17938
17533
|
clearInterval(spawnPoll);
|
|
17939
17534
|
stopSessions();
|
|
17535
|
+
void stopListening();
|
|
17940
17536
|
streamActivity();
|
|
17941
17537
|
clearInterval(activityTick);
|
|
17942
17538
|
try {
|
|
@@ -17946,6 +17542,8 @@ function startHost(opts) {
|
|
|
17946
17542
|
}
|
|
17947
17543
|
};
|
|
17948
17544
|
publish();
|
|
17545
|
+
void listenSlots().then(sweepSlots).catch(() => {
|
|
17546
|
+
});
|
|
17949
17547
|
return api;
|
|
17950
17548
|
}
|
|
17951
17549
|
|
|
@@ -18071,8 +17669,8 @@ function agentSignals(notes, connections, now) {
|
|
|
18071
17669
|
}
|
|
18072
17670
|
function noteSignals(notes, connections, now) {
|
|
18073
17671
|
const nameOf = new Map(connections.map((c) => [c.id, c.name]));
|
|
18074
|
-
const
|
|
18075
|
-
return
|
|
17672
|
+
const open = notes.filter((n) => OPEN_STATUSES.includes(n.status));
|
|
17673
|
+
return open.map((n) => {
|
|
18076
17674
|
const superseded = supersededBy(n, notes);
|
|
18077
17675
|
const echo = doneEcho(n, notes);
|
|
18078
17676
|
const token = assigneeToken(n);
|
|
@@ -18165,17 +17763,17 @@ async function available(deps = {}) {
|
|
|
18165
17763
|
return [ollama, ...harnesses];
|
|
18166
17764
|
}
|
|
18167
17765
|
function refusal(options, asked) {
|
|
18168
|
-
const
|
|
17766
|
+
const lines2 = [
|
|
18169
17767
|
asked ? `Can't judge with --via ${asked}: ${options.find((o) => o.provider === asked)?.hint ?? "it isn't available on this machine"}` : "No local judge available \u2014 triage refuses rather than sending your queue to a cloud model.",
|
|
18170
17768
|
"",
|
|
18171
17769
|
"Triage runs the model where your queue already lives (#1106). Your options:"
|
|
18172
17770
|
];
|
|
18173
17771
|
for (const o of options) {
|
|
18174
|
-
|
|
18175
|
-
if (!o.ready && o.hint)
|
|
17772
|
+
lines2.push(` ${o.ready ? "\u2713" : "\u2717"} ${o.provider.padEnd(7)} ${PRIVACY[o.provider]}`);
|
|
17773
|
+
if (!o.ready && o.hint) lines2.push(` ${o.hint}`);
|
|
18176
17774
|
}
|
|
18177
|
-
|
|
18178
|
-
return
|
|
17775
|
+
lines2.push("", "Then re-run: paigy-harness triage [--via ollama|claude|codex|agy]");
|
|
17776
|
+
return lines2.join("\n");
|
|
18179
17777
|
}
|
|
18180
17778
|
var RUNNERS = {
|
|
18181
17779
|
// Ollama speaks HTTP, not a CLI — handled separately in `judgeFor`.
|
|
@@ -18371,27 +17969,27 @@ function suggested(run) {
|
|
|
18371
17969
|
return run.close.length + run.stale.length + run.assign.reduce((n, a) => n + a.notes.length, 0);
|
|
18372
17970
|
}
|
|
18373
17971
|
function renderTable(run) {
|
|
18374
|
-
const
|
|
17972
|
+
const lines2 = [];
|
|
18375
17973
|
const width = Math.max(
|
|
18376
17974
|
24,
|
|
18377
17975
|
...[...run.close, ...run.stale, ...run.assign.flatMap((a) => a.notes)].map((n) => Math.min(n.title.length, 48))
|
|
18378
17976
|
);
|
|
18379
17977
|
const row = (n) => ` ${n.title.slice(0, width).padEnd(width)} ${n.why}`;
|
|
18380
17978
|
if (run.close.length) {
|
|
18381
|
-
|
|
18382
|
-
|
|
17979
|
+
lines2.push(`Paigy thinks you should CLOSE ${plural(run.close.length, "note")}:`);
|
|
17980
|
+
lines2.push(...run.close.map(row), "");
|
|
18383
17981
|
}
|
|
18384
17982
|
if (run.stale.length) {
|
|
18385
|
-
|
|
18386
|
-
|
|
17983
|
+
lines2.push(`Paigy thinks ${plural(run.stale.length, "note")} ${run.stale.length === 1 ? "has" : "have"} gone STALE:`);
|
|
17984
|
+
lines2.push(...run.stale.map(row), "");
|
|
18387
17985
|
}
|
|
18388
17986
|
for (const group of run.assign) {
|
|
18389
|
-
|
|
18390
|
-
|
|
17987
|
+
lines2.push(`Paigy thinks you should ASSIGN ${plural(group.notes.length, "note")} to ${group.agentName}:`);
|
|
17988
|
+
lines2.push(...group.notes.map(row), "");
|
|
18391
17989
|
}
|
|
18392
17990
|
const acted = suggested(run);
|
|
18393
|
-
|
|
18394
|
-
return
|
|
17991
|
+
lines2.push(`Reviewed ${plural(run.reviewed, "open note")} \xB7 ${plural(acted, "suggestion")} \xB7 ${run.reviewed - acted} left alone`);
|
|
17992
|
+
return lines2.join("\n");
|
|
18395
17993
|
}
|
|
18396
17994
|
async function confirm(question) {
|
|
18397
17995
|
if (!process.stdin.isTTY) return false;
|
|
@@ -18514,7 +18112,7 @@ async function brokerView(deps) {
|
|
|
18514
18112
|
}
|
|
18515
18113
|
|
|
18516
18114
|
// src/main.ts
|
|
18517
|
-
var here =
|
|
18115
|
+
var here = dirname3(fileURLToPath(import.meta.url));
|
|
18518
18116
|
var wsDeps = { file: workspacesFile() };
|
|
18519
18117
|
var DEVICE_SLOT = "Desktop";
|
|
18520
18118
|
var deviceToken = () => readToken(DEVICE_SLOT);
|
|
@@ -18589,20 +18187,27 @@ ipcMain.handle("paigy:beacon", async () => {
|
|
|
18589
18187
|
})();
|
|
18590
18188
|
return { qr, code: code.user_code };
|
|
18591
18189
|
});
|
|
18190
|
+
function waitingLine(d) {
|
|
18191
|
+
for (let i = d.entries.length - 1; i >= 0; i -= 1) {
|
|
18192
|
+
const words2 = entryWords(d.entries[i]).trim();
|
|
18193
|
+
if (words2) return words2.split("\n")[0];
|
|
18194
|
+
}
|
|
18195
|
+
return d.message.split("\n")[0] ?? "";
|
|
18196
|
+
}
|
|
18592
18197
|
ipcMain.handle("paigy:agent", async (_e, name) => {
|
|
18593
18198
|
const entry = (host?.roster() ?? readHostState()?.roster ?? []).find((a) => a.name === name);
|
|
18594
18199
|
if (!entry) return null;
|
|
18595
18200
|
const token = entry.slot ? readToken(entry.slot) : "";
|
|
18596
|
-
const
|
|
18201
|
+
const open = token ? await checkReplies({ token }).then((r) => r.deliveries).catch(() => null) : null;
|
|
18597
18202
|
return {
|
|
18598
18203
|
...entry,
|
|
18599
|
-
pending:
|
|
18600
|
-
// The same two facts, from the
|
|
18601
|
-
requests:
|
|
18602
|
-
line:
|
|
18603
|
-
at:
|
|
18204
|
+
pending: open ? {
|
|
18205
|
+
// The same two facts, from the one list: what is waiting, and how much.
|
|
18206
|
+
requests: open.slice(0, 5).map((d) => ({
|
|
18207
|
+
line: waitingLine(d).slice(0, 140),
|
|
18208
|
+
at: d.entries[d.entries.length - 1]?.createdAt ?? ""
|
|
18604
18209
|
})),
|
|
18605
|
-
replies:
|
|
18210
|
+
replies: open.length
|
|
18606
18211
|
} : null
|
|
18607
18212
|
};
|
|
18608
18213
|
});
|
|
@@ -18637,10 +18242,10 @@ ipcMain.handle("paigy:triage-run", async () => {
|
|
|
18637
18242
|
if (!token) return { code: 1, output: "This Mac isn't paired yet \u2014 scan the pairing code first." };
|
|
18638
18243
|
if (sweeping) return { code: 1, output: "A sweep is already running." };
|
|
18639
18244
|
sweeping = true;
|
|
18640
|
-
const
|
|
18245
|
+
const lines2 = [];
|
|
18641
18246
|
const say = (line) => {
|
|
18642
18247
|
for (const l of line.split("\n")) {
|
|
18643
|
-
|
|
18248
|
+
lines2.push(l);
|
|
18644
18249
|
log(l);
|
|
18645
18250
|
}
|
|
18646
18251
|
};
|
|
@@ -18653,11 +18258,11 @@ ipcMain.handle("paigy:triage-run", async () => {
|
|
|
18653
18258
|
submit: (run) => submitTriage(run, { token })
|
|
18654
18259
|
}
|
|
18655
18260
|
);
|
|
18656
|
-
return { code, output:
|
|
18261
|
+
return { code, output: lines2.join("\n") };
|
|
18657
18262
|
} catch (e) {
|
|
18658
18263
|
const message = e instanceof Error ? e.message : String(e);
|
|
18659
18264
|
log(`\u26A0 the sweep failed: ${message}`);
|
|
18660
|
-
return { code: 1, output: [...
|
|
18265
|
+
return { code: 1, output: [...lines2, `\u26A0 the sweep failed: ${message}`].join("\n") };
|
|
18661
18266
|
} finally {
|
|
18662
18267
|
sweeping = false;
|
|
18663
18268
|
}
|