@paigy/harness 0.2.5 → 0.2.12
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 +108 -22
- package/dist/main.js +128 -19
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -29763,7 +29763,12 @@ var IntentSchema = external_exports.object({
|
|
|
29763
29763
|
* "after lunch") against the agent machine's clock — the user's — and attaches
|
|
29764
29764
|
* the seconds, ready to pass straight to schedule_callback. Absent when the
|
|
29765
29765
|
* detail didn't parse (the agent interprets it) or the kind isn't defer. */
|
|
29766
|
-
dueInSeconds: external_exports.number().int().positive().optional()
|
|
29766
|
+
dueInSeconds: external_exports.number().int().positive().optional(),
|
|
29767
|
+
/** Feedback only (#812): WHICH failure the complaint names — typed by the mapper that
|
|
29768
|
+
* already read the utterance, so `feedback_from_call.kind` stops defaulting to
|
|
29769
|
+
* 'other' on every row. A table that records that something was wrong and nothing
|
|
29770
|
+
* about what cannot answer "is the bot looping less this week?". */
|
|
29771
|
+
fault: external_exports.enum(["loop", "unanswered", "overridden", "misheard", "slow", "other"]).optional()
|
|
29767
29772
|
});
|
|
29768
29773
|
var AwaitItemSchema = external_exports.discriminatedUnion("type", [
|
|
29769
29774
|
external_exports.object({
|
|
@@ -29933,6 +29938,11 @@ var AgendaTurnSchema = external_exports.object({
|
|
|
29933
29938
|
claimId: external_exports.string().optional(),
|
|
29934
29939
|
/** The claim's voice key (#462) — the OUTBOUND identity, audible who-is-asking. */
|
|
29935
29940
|
voice: external_exports.string().optional(),
|
|
29941
|
+
/** The claim's AGENT NAME (#838) — the spoken identity. A voice alone doesn't say
|
|
29942
|
+
* whose request this is: an item that folded in from another agent arrived as a bare
|
|
29943
|
+
* non-sequitur ("First real production sign-in is yours to make whenever you want.")
|
|
29944
|
+
* and the owner answered "What?". The bot names the agent before its first turn. */
|
|
29945
|
+
agent: external_exports.string().optional(),
|
|
29936
29946
|
select: SelectShapeSchema.optional(),
|
|
29937
29947
|
options: external_exports.array(OptionSchema.omit({ id: true })).optional(),
|
|
29938
29948
|
/** The seat was satisfied by the call's own ACCOUNT (replan-design.md, user_info): the
|
|
@@ -30683,6 +30693,9 @@ function readToken(agent2 = AGENT_NAME) {
|
|
|
30683
30693
|
function listSlots() {
|
|
30684
30694
|
return Object.keys(readTokenFile());
|
|
30685
30695
|
}
|
|
30696
|
+
function slotName(agent2) {
|
|
30697
|
+
return readTokenFile()[agent2]?.name ?? null;
|
|
30698
|
+
}
|
|
30686
30699
|
async function requestCode(suggestedName = AGENT_NAME, e2ee) {
|
|
30687
30700
|
const res = await reach(`${BACKEND_URL}/api/device/code`, {
|
|
30688
30701
|
method: "POST",
|
|
@@ -30849,6 +30862,16 @@ async function hatch(name, voice = null) {
|
|
|
30849
30862
|
if (!res.ok) throw new Error(`hatch failed: ${res.status} ${await res.text()}`);
|
|
30850
30863
|
return await res.json();
|
|
30851
30864
|
}
|
|
30865
|
+
async function whoAmI(opts = {}) {
|
|
30866
|
+
try {
|
|
30867
|
+
const res = await reach(`${BACKEND_URL}/api/identity`, {
|
|
30868
|
+
headers: { authorization: `Bearer ${authToken(opts.token)}` }
|
|
30869
|
+
});
|
|
30870
|
+
return res.ok ? await res.json() : null;
|
|
30871
|
+
} catch {
|
|
30872
|
+
return null;
|
|
30873
|
+
}
|
|
30874
|
+
}
|
|
30852
30875
|
async function claimSessions(opts = {}) {
|
|
30853
30876
|
const res = ensureAuthed(await reach(`${BACKEND_URL}/api/sessions/claim`, {
|
|
30854
30877
|
headers: { authorization: `Bearer ${authToken(opts.token)}` }
|
|
@@ -30878,10 +30901,10 @@ async function setTaskState(notificationId, state, opts = {}) {
|
|
|
30878
30901
|
// src/cli.ts
|
|
30879
30902
|
var import_qrcode = __toESM(require_lib(), 1);
|
|
30880
30903
|
import { hostname } from "os";
|
|
30881
|
-
import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as
|
|
30904
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
|
|
30882
30905
|
import { execSync } from "child_process";
|
|
30883
|
-
import { homedir as
|
|
30884
|
-
import { join as
|
|
30906
|
+
import { homedir as homedir6 } from "os";
|
|
30907
|
+
import { join as join5 } from "path";
|
|
30885
30908
|
|
|
30886
30909
|
// ../../packages/schema/dist/index.js
|
|
30887
30910
|
var ContextSchema2 = external_exports.object({
|
|
@@ -31117,7 +31140,12 @@ var IntentSchema2 = external_exports.object({
|
|
|
31117
31140
|
* "after lunch") against the agent machine's clock — the user's — and attaches
|
|
31118
31141
|
* the seconds, ready to pass straight to schedule_callback. Absent when the
|
|
31119
31142
|
* detail didn't parse (the agent interprets it) or the kind isn't defer. */
|
|
31120
|
-
dueInSeconds: external_exports.number().int().positive().optional()
|
|
31143
|
+
dueInSeconds: external_exports.number().int().positive().optional(),
|
|
31144
|
+
/** Feedback only (#812): WHICH failure the complaint names — typed by the mapper that
|
|
31145
|
+
* already read the utterance, so `feedback_from_call.kind` stops defaulting to
|
|
31146
|
+
* 'other' on every row. A table that records that something was wrong and nothing
|
|
31147
|
+
* about what cannot answer "is the bot looping less this week?". */
|
|
31148
|
+
fault: external_exports.enum(["loop", "unanswered", "overridden", "misheard", "slow", "other"]).optional()
|
|
31121
31149
|
});
|
|
31122
31150
|
var AwaitItemSchema2 = external_exports.discriminatedUnion("type", [
|
|
31123
31151
|
external_exports.object({
|
|
@@ -31287,6 +31315,11 @@ var AgendaTurnSchema2 = external_exports.object({
|
|
|
31287
31315
|
claimId: external_exports.string().optional(),
|
|
31288
31316
|
/** The claim's voice key (#462) — the OUTBOUND identity, audible who-is-asking. */
|
|
31289
31317
|
voice: external_exports.string().optional(),
|
|
31318
|
+
/** The claim's AGENT NAME (#838) — the spoken identity. A voice alone doesn't say
|
|
31319
|
+
* whose request this is: an item that folded in from another agent arrived as a bare
|
|
31320
|
+
* non-sequitur ("First real production sign-in is yours to make whenever you want.")
|
|
31321
|
+
* and the owner answered "What?". The bot names the agent before its first turn. */
|
|
31322
|
+
agent: external_exports.string().optional(),
|
|
31290
31323
|
select: SelectShapeSchema2.optional(),
|
|
31291
31324
|
options: external_exports.array(OptionSchema2.omit({ id: true })).optional(),
|
|
31292
31325
|
/** The seat was satisfied by the call's own ACCOUNT (replan-design.md, user_info): the
|
|
@@ -31755,7 +31788,8 @@ async function drainInput(state, deps = {}) {
|
|
|
31755
31788
|
return [];
|
|
31756
31789
|
}
|
|
31757
31790
|
const ours = (parentId) => state.exclusive || parentId === state.parentId;
|
|
31758
|
-
const
|
|
31791
|
+
const opening = !!state.exclusive && !state.parentId;
|
|
31792
|
+
const claimable = (notificationId) => opening || state.resting === true || !state.sharedWithAgent || (state.mine?.has(notificationId) ?? false);
|
|
31759
31793
|
const mine = [
|
|
31760
31794
|
...work.replies.filter((r) => ours(r.parentId) && claimable(r.notificationId)).map((r) => ({ notificationId: r.notificationId, parentId: r.parentId, answer: r.answer })),
|
|
31761
31795
|
...work.requests.filter((r) => ours(r.parentId) && claimable(r.notificationId)).map((r) => ({ notificationId: r.notificationId, parentId: r.parentId, answer: { kind: "text", text: r.text } }))
|
|
@@ -31788,7 +31822,10 @@ async function pump(session, state, deps = {}) {
|
|
|
31788
31822
|
continue;
|
|
31789
31823
|
}
|
|
31790
31824
|
const words = spokenText(item.answer);
|
|
31791
|
-
if (words)
|
|
31825
|
+
if (words) {
|
|
31826
|
+
state.resting = false;
|
|
31827
|
+
session.send(words);
|
|
31828
|
+
}
|
|
31792
31829
|
}
|
|
31793
31830
|
const keepGoing = await deps.onIdle?.() ?? false;
|
|
31794
31831
|
if (!keepGoing) return;
|
|
@@ -32304,6 +32341,7 @@ function runHarness(opts) {
|
|
|
32304
32341
|
state.parentId ??= parentId;
|
|
32305
32342
|
if (event.kind === "turn") opts.log(`${event.role}: ${event.text.split("\n")[0] ?? ""}`);
|
|
32306
32343
|
if (event.kind === "idle") {
|
|
32344
|
+
state.resting = true;
|
|
32307
32345
|
if (endsWithQuestion(event.result)) {
|
|
32308
32346
|
const local = await opts.localAsk?.question?.(event.result ?? "") ?? null;
|
|
32309
32347
|
if (local?.trim() && session && running) {
|
|
@@ -32340,6 +32378,7 @@ function runHarness(opts) {
|
|
|
32340
32378
|
send(text) {
|
|
32341
32379
|
session?.send(text);
|
|
32342
32380
|
},
|
|
32381
|
+
working: () => running && state.resting !== true,
|
|
32343
32382
|
stop() {
|
|
32344
32383
|
running = false;
|
|
32345
32384
|
cancelAsks(state);
|
|
@@ -32350,6 +32389,11 @@ function runHarness(opts) {
|
|
|
32350
32389
|
};
|
|
32351
32390
|
}
|
|
32352
32391
|
|
|
32392
|
+
// src/host.ts
|
|
32393
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
32394
|
+
import { homedir as homedir5 } from "os";
|
|
32395
|
+
import { join as join4 } from "path";
|
|
32396
|
+
|
|
32353
32397
|
// src/workspaces.ts
|
|
32354
32398
|
import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
32355
32399
|
import { dirname as dirname3, join as join3, resolve as resolve2 } from "path";
|
|
@@ -32377,10 +32421,20 @@ function addWorkspace(dir, deps) {
|
|
|
32377
32421
|
}
|
|
32378
32422
|
|
|
32379
32423
|
// src/host.ts
|
|
32424
|
+
var HOST_FILE = join4(homedir5(), ".paigy", "host.json");
|
|
32380
32425
|
function startHost(opts) {
|
|
32381
32426
|
const runs = /* @__PURE__ */ new Map();
|
|
32382
32427
|
const asHost = { token: opts.token };
|
|
32428
|
+
const refreshNames = async () => {
|
|
32429
|
+
for (const slot of listSlots()) {
|
|
32430
|
+
const token = readToken(slot);
|
|
32431
|
+
if (!token) continue;
|
|
32432
|
+
const me = await whoAmI({ token }).catch(() => null);
|
|
32433
|
+
if (me?.name && me.name !== slotName(slot)) saveToken({ access_token: token, name: me.name, device: null }, slot);
|
|
32434
|
+
}
|
|
32435
|
+
};
|
|
32383
32436
|
const beat = () => {
|
|
32437
|
+
void refreshNames();
|
|
32384
32438
|
void heartbeat({
|
|
32385
32439
|
harnesses: detectAll().map((a) => ({ name: a.name, label: a.label, status: a.status })),
|
|
32386
32440
|
workspaces: listWorkspaces(opts.wsDeps)
|
|
@@ -32406,11 +32460,15 @@ function startHost(opts) {
|
|
|
32406
32460
|
log
|
|
32407
32461
|
});
|
|
32408
32462
|
runs.set(spec.sessionId, { run, label });
|
|
32463
|
+
try {
|
|
32464
|
+
saveToken({ access_token: spec.token, name: label, device: null }, `session:${spec.sessionId.slice(0, 8)}`);
|
|
32465
|
+
} catch {
|
|
32466
|
+
}
|
|
32409
32467
|
opts.log(`\u25B6 phone-launched ${label} (${spec.harness}) in ${spec.workspace}`);
|
|
32410
32468
|
}
|
|
32411
32469
|
}
|
|
32412
32470
|
const SLOT_HARNESS = { "mcp-agent": "claude", codex: "codex" };
|
|
32413
|
-
const WAKE_PROMPT = "You were woken because Paigy work is waiting for you. Call check_replies
|
|
32471
|
+
const WAKE_PROMPT = "You were woken because Paigy work is waiting for you. This is a fresh session with your existing identity, so you may already have work in flight that you can't remember. Call check_replies FIRST, then get_thread on each conversation it returns and read it before you touch anything: it holds what you were doing, where (a repo or worktree may not be this folder), and what the owner already decided. Then handle what's waiting and follow your paigy instructions to stay in the loop.";
|
|
32414
32472
|
async function sweepSlots() {
|
|
32415
32473
|
const workspace = listWorkspaces(opts.wsDeps)[0];
|
|
32416
32474
|
if (!workspace) return;
|
|
@@ -32422,7 +32480,8 @@ function startHost(opts) {
|
|
|
32422
32480
|
if (!token) continue;
|
|
32423
32481
|
const work = await checkReplies({ token }).catch(() => null);
|
|
32424
32482
|
if (!work || work.requests.length === 0 && work.replies.length === 0) continue;
|
|
32425
|
-
const
|
|
32483
|
+
const label = slotName(slot) ?? slot;
|
|
32484
|
+
const log = (line) => opts.log(`[${label}] ${line}`);
|
|
32426
32485
|
const run = runHarness({
|
|
32427
32486
|
harness,
|
|
32428
32487
|
cwd: workspace,
|
|
@@ -32433,19 +32492,26 @@ function startHost(opts) {
|
|
|
32433
32492
|
// A dead run must not squat the slot — evict so the next wake can respawn.
|
|
32434
32493
|
onExit: () => {
|
|
32435
32494
|
runs.delete(key);
|
|
32436
|
-
opts.log(`\u2716 ${
|
|
32495
|
+
opts.log(`\u2716 ${label} ended`);
|
|
32437
32496
|
},
|
|
32438
32497
|
log
|
|
32439
32498
|
});
|
|
32440
|
-
runs.set(key, { run, label
|
|
32441
|
-
opts.log(`\u25B6 woke ${
|
|
32499
|
+
runs.set(key, { run, label });
|
|
32500
|
+
opts.log(`\u25B6 woke ${label} (${harness}) \u2014 work was waiting in ${workspace}`);
|
|
32442
32501
|
}
|
|
32443
32502
|
}
|
|
32503
|
+
const publish = () => {
|
|
32504
|
+
try {
|
|
32505
|
+
writeFileSync3(HOST_FILE, JSON.stringify({ pid: process.pid, at: Date.now(), roster: api.roster() }));
|
|
32506
|
+
} catch {
|
|
32507
|
+
}
|
|
32508
|
+
};
|
|
32444
32509
|
beat();
|
|
32445
32510
|
const pulse = setInterval(beat, 6e4);
|
|
32446
32511
|
const spawnPoll = setInterval(() => {
|
|
32447
32512
|
void claimAndSpawn();
|
|
32448
32513
|
void sweepSlots();
|
|
32514
|
+
publish();
|
|
32449
32515
|
}, 5e3);
|
|
32450
32516
|
const stopSessions = () => {
|
|
32451
32517
|
for (const { run, label } of runs.values()) {
|
|
@@ -32454,15 +32520,35 @@ function startHost(opts) {
|
|
|
32454
32520
|
}
|
|
32455
32521
|
runs.clear();
|
|
32456
32522
|
};
|
|
32457
|
-
|
|
32523
|
+
const api = {
|
|
32458
32524
|
sessions: () => [...runs.entries()].map(([id, r]) => [id, r.label]),
|
|
32525
|
+
// Every identity this machine holds, plus what it's doing right now. Slots are the
|
|
32526
|
+
// roster (hatched agents, the terminal tools, claimed sessions); a run makes one of
|
|
32527
|
+
// them live, and a run mid-turn makes it working.
|
|
32528
|
+
roster: () => {
|
|
32529
|
+
const live = new Map([...runs.values()].map((r) => [r.label, r.run]));
|
|
32530
|
+
const names = /* @__PURE__ */ new Map();
|
|
32531
|
+
for (const key of listSlots()) if (key !== "Desktop") names.set(slotName(key) ?? key, key);
|
|
32532
|
+
for (const label of live.keys()) if (!names.has(label)) names.set(label, label);
|
|
32533
|
+
return [...names.keys()].map((name) => ({
|
|
32534
|
+
name,
|
|
32535
|
+
running: live.has(name),
|
|
32536
|
+
working: live.get(name)?.working() ?? false
|
|
32537
|
+
}));
|
|
32538
|
+
},
|
|
32459
32539
|
stopSessions,
|
|
32460
32540
|
stop() {
|
|
32461
32541
|
clearInterval(pulse);
|
|
32462
32542
|
clearInterval(spawnPoll);
|
|
32463
32543
|
stopSessions();
|
|
32544
|
+
try {
|
|
32545
|
+
writeFileSync3(HOST_FILE, JSON.stringify({ pid: process.pid, at: 0, roster: [] }));
|
|
32546
|
+
} catch {
|
|
32547
|
+
}
|
|
32464
32548
|
}
|
|
32465
32549
|
};
|
|
32550
|
+
publish();
|
|
32551
|
+
return api;
|
|
32466
32552
|
}
|
|
32467
32553
|
|
|
32468
32554
|
// src/cli.ts
|
|
@@ -32574,7 +32660,7 @@ async function main() {
|
|
|
32574
32660
|
const wsDeps = { file: workspacesFile() };
|
|
32575
32661
|
for (const dir of parsed.grant) addWorkspace(dir, wsDeps);
|
|
32576
32662
|
if (!listWorkspaces(wsDeps).length) {
|
|
32577
|
-
const guess =
|
|
32663
|
+
const guess = join5(homedir6(), "projects");
|
|
32578
32664
|
if (existsSync5(guess)) {
|
|
32579
32665
|
addWorkspace(guess, wsDeps);
|
|
32580
32666
|
console.log(`\u2713 workspace granted: ${guess} (change with --grant DIR)`);
|
|
@@ -32612,12 +32698,12 @@ async function main() {
|
|
|
32612
32698
|
}
|
|
32613
32699
|
}
|
|
32614
32700
|
try {
|
|
32615
|
-
const settings =
|
|
32701
|
+
const settings = join5(homedir6(), ".claude", "settings.json");
|
|
32616
32702
|
if (which("claude") && existsSync5(settings)) {
|
|
32617
|
-
const cfg = JSON.parse(
|
|
32703
|
+
const cfg = JSON.parse(readFileSync4(settings, "utf8"));
|
|
32618
32704
|
if (!cfg["statusLine"]) {
|
|
32619
32705
|
cfg["statusLine"] = { type: "command", command: "npx -y -p @paigy/mcp@latest paigy-statusline" };
|
|
32620
|
-
|
|
32706
|
+
writeFileSync4(settings, JSON.stringify(cfg, null, 2));
|
|
32621
32707
|
console.log("\u2713 statusline wired");
|
|
32622
32708
|
}
|
|
32623
32709
|
}
|
|
@@ -32667,14 +32753,14 @@ async function main() {
|
|
|
32667
32753
|
</array>
|
|
32668
32754
|
<key>RunAtLoad</key><true/>
|
|
32669
32755
|
<key>KeepAlive</key><true/>
|
|
32670
|
-
<key>StandardOutPath</key><string>${
|
|
32671
|
-
<key>StandardErrorPath</key><string>${
|
|
32756
|
+
<key>StandardOutPath</key><string>${join5(homedir6(), ".paigy", "host.log")}</string>
|
|
32757
|
+
<key>StandardErrorPath</key><string>${join5(homedir6(), ".paigy", "host.log")}</string>
|
|
32672
32758
|
</dict></plist>
|
|
32673
32759
|
`;
|
|
32674
|
-
const dir =
|
|
32760
|
+
const dir = join5(homedir6(), "Library", "LaunchAgents");
|
|
32675
32761
|
mkdirSync3(dir, { recursive: true });
|
|
32676
|
-
const path =
|
|
32677
|
-
|
|
32762
|
+
const path = join5(dir, "ai.paigy.harness.plist");
|
|
32763
|
+
writeFileSync4(path, plist);
|
|
32678
32764
|
execSync(`launchctl unload ${path} 2>/dev/null; launchctl load ${path}`, { shell: "/bin/sh" });
|
|
32679
32765
|
console.log(`\u2713 host installed as a login service (${path}) \u2014 logs at ~/.paigy/host.log`);
|
|
32680
32766
|
return;
|
package/dist/main.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import { createRequire as __cr } from 'node:module'; const require = __cr(import.meta.url);
|
|
1
2
|
import {
|
|
2
3
|
__commonJS,
|
|
3
4
|
__export,
|
|
4
5
|
__require,
|
|
5
6
|
__toESM
|
|
6
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-OBWUN26Y.js";
|
|
7
8
|
|
|
8
9
|
// ../../node_modules/.pnpm/qrcode@1.5.4/node_modules/qrcode/lib/can-promise.js
|
|
9
10
|
var require_can_promise = __commonJS({
|
|
@@ -4577,7 +4578,7 @@ var require_lib = __commonJS({
|
|
|
4577
4578
|
import { app, BrowserWindow, dialog, ipcMain } from "electron";
|
|
4578
4579
|
import { spawn as spawn2 } from "child_process";
|
|
4579
4580
|
import { fileURLToPath } from "url";
|
|
4580
|
-
import { dirname as dirname4, join as
|
|
4581
|
+
import { dirname as dirname4, join as join5 } from "path";
|
|
4581
4582
|
|
|
4582
4583
|
// ../../packages/sdk/dist/index.js
|
|
4583
4584
|
import { createRequire as __sdkCreateRequire } from "module";
|
|
@@ -10888,7 +10889,7 @@ var PROXY_ENV = ["HTTPS_PROXY", "https_proxy", "HTTP_PROXY", "http_proxy"];
|
|
|
10888
10889
|
var agent;
|
|
10889
10890
|
async function proxy() {
|
|
10890
10891
|
if (!PROXY_ENV.some((k) => process.env[k])) return void 0;
|
|
10891
|
-
return agent ??= new (await import("./undici-
|
|
10892
|
+
return agent ??= new (await import("./undici-GCFUZISI.js")).EnvHttpProxyAgent();
|
|
10892
10893
|
}
|
|
10893
10894
|
async function reach(url, init) {
|
|
10894
10895
|
try {
|
|
@@ -11175,7 +11176,12 @@ var IntentSchema = external_exports.object({
|
|
|
11175
11176
|
* "after lunch") against the agent machine's clock — the user's — and attaches
|
|
11176
11177
|
* the seconds, ready to pass straight to schedule_callback. Absent when the
|
|
11177
11178
|
* detail didn't parse (the agent interprets it) or the kind isn't defer. */
|
|
11178
|
-
dueInSeconds: external_exports.number().int().positive().optional()
|
|
11179
|
+
dueInSeconds: external_exports.number().int().positive().optional(),
|
|
11180
|
+
/** Feedback only (#812): WHICH failure the complaint names — typed by the mapper that
|
|
11181
|
+
* already read the utterance, so `feedback_from_call.kind` stops defaulting to
|
|
11182
|
+
* 'other' on every row. A table that records that something was wrong and nothing
|
|
11183
|
+
* about what cannot answer "is the bot looping less this week?". */
|
|
11184
|
+
fault: external_exports.enum(["loop", "unanswered", "overridden", "misheard", "slow", "other"]).optional()
|
|
11179
11185
|
});
|
|
11180
11186
|
var AwaitItemSchema = external_exports.discriminatedUnion("type", [
|
|
11181
11187
|
external_exports.object({
|
|
@@ -11345,6 +11351,11 @@ var AgendaTurnSchema = external_exports.object({
|
|
|
11345
11351
|
claimId: external_exports.string().optional(),
|
|
11346
11352
|
/** The claim's voice key (#462) — the OUTBOUND identity, audible who-is-asking. */
|
|
11347
11353
|
voice: external_exports.string().optional(),
|
|
11354
|
+
/** The claim's AGENT NAME (#838) — the spoken identity. A voice alone doesn't say
|
|
11355
|
+
* whose request this is: an item that folded in from another agent arrived as a bare
|
|
11356
|
+
* non-sequitur ("First real production sign-in is yours to make whenever you want.")
|
|
11357
|
+
* and the owner answered "What?". The bot names the agent before its first turn. */
|
|
11358
|
+
agent: external_exports.string().optional(),
|
|
11348
11359
|
select: SelectShapeSchema.optional(),
|
|
11349
11360
|
options: external_exports.array(OptionSchema.omit({ id: true })).optional(),
|
|
11350
11361
|
/** The seat was satisfied by the call's own ACCOUNT (replan-design.md, user_info): the
|
|
@@ -12095,6 +12106,9 @@ function readToken(agent2 = AGENT_NAME) {
|
|
|
12095
12106
|
function listSlots() {
|
|
12096
12107
|
return Object.keys(readTokenFile());
|
|
12097
12108
|
}
|
|
12109
|
+
function slotName(agent2) {
|
|
12110
|
+
return readTokenFile()[agent2]?.name ?? null;
|
|
12111
|
+
}
|
|
12098
12112
|
async function requestCode(suggestedName = AGENT_NAME, e2ee) {
|
|
12099
12113
|
const res = await reach(`${BACKEND_URL}/api/device/code`, {
|
|
12100
12114
|
method: "POST",
|
|
@@ -12249,6 +12263,16 @@ async function checkReplies(opts = {}) {
|
|
|
12249
12263
|
}
|
|
12250
12264
|
var tokenOverride = null;
|
|
12251
12265
|
var authToken = (explicit) => explicit ?? tokenOverride ?? readToken();
|
|
12266
|
+
async function whoAmI(opts = {}) {
|
|
12267
|
+
try {
|
|
12268
|
+
const res = await reach(`${BACKEND_URL}/api/identity`, {
|
|
12269
|
+
headers: { authorization: `Bearer ${authToken(opts.token)}` }
|
|
12270
|
+
});
|
|
12271
|
+
return res.ok ? await res.json() : null;
|
|
12272
|
+
} catch {
|
|
12273
|
+
return null;
|
|
12274
|
+
}
|
|
12275
|
+
}
|
|
12252
12276
|
async function claimSessions(opts = {}) {
|
|
12253
12277
|
const res = ensureAuthed(await reach(`${BACKEND_URL}/api/sessions/claim`, {
|
|
12254
12278
|
headers: { authorization: `Bearer ${authToken(opts.token)}` }
|
|
@@ -12359,6 +12383,11 @@ function detectAll(deps = {}) {
|
|
|
12359
12383
|
return CATALOG.map((entry) => detect(entry, deps));
|
|
12360
12384
|
}
|
|
12361
12385
|
|
|
12386
|
+
// src/host.ts
|
|
12387
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
12388
|
+
import { homedir as homedir5 } from "os";
|
|
12389
|
+
import { join as join4 } from "path";
|
|
12390
|
+
|
|
12362
12391
|
// src/workspaces.ts
|
|
12363
12392
|
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
12364
12393
|
import { dirname as dirname2, join as join3, resolve } from "path";
|
|
@@ -12909,7 +12938,12 @@ var IntentSchema2 = external_exports.object({
|
|
|
12909
12938
|
* "after lunch") against the agent machine's clock — the user's — and attaches
|
|
12910
12939
|
* the seconds, ready to pass straight to schedule_callback. Absent when the
|
|
12911
12940
|
* detail didn't parse (the agent interprets it) or the kind isn't defer. */
|
|
12912
|
-
dueInSeconds: external_exports.number().int().positive().optional()
|
|
12941
|
+
dueInSeconds: external_exports.number().int().positive().optional(),
|
|
12942
|
+
/** Feedback only (#812): WHICH failure the complaint names — typed by the mapper that
|
|
12943
|
+
* already read the utterance, so `feedback_from_call.kind` stops defaulting to
|
|
12944
|
+
* 'other' on every row. A table that records that something was wrong and nothing
|
|
12945
|
+
* about what cannot answer "is the bot looping less this week?". */
|
|
12946
|
+
fault: external_exports.enum(["loop", "unanswered", "overridden", "misheard", "slow", "other"]).optional()
|
|
12913
12947
|
});
|
|
12914
12948
|
var AwaitItemSchema2 = external_exports.discriminatedUnion("type", [
|
|
12915
12949
|
external_exports.object({
|
|
@@ -13079,6 +13113,11 @@ var AgendaTurnSchema2 = external_exports.object({
|
|
|
13079
13113
|
claimId: external_exports.string().optional(),
|
|
13080
13114
|
/** The claim's voice key (#462) — the OUTBOUND identity, audible who-is-asking. */
|
|
13081
13115
|
voice: external_exports.string().optional(),
|
|
13116
|
+
/** The claim's AGENT NAME (#838) — the spoken identity. A voice alone doesn't say
|
|
13117
|
+
* whose request this is: an item that folded in from another agent arrived as a bare
|
|
13118
|
+
* non-sequitur ("First real production sign-in is yours to make whenever you want.")
|
|
13119
|
+
* and the owner answered "What?". The bot names the agent before its first turn. */
|
|
13120
|
+
agent: external_exports.string().optional(),
|
|
13082
13121
|
select: SelectShapeSchema2.optional(),
|
|
13083
13122
|
options: external_exports.array(OptionSchema2.omit({ id: true })).optional(),
|
|
13084
13123
|
/** The seat was satisfied by the call's own ACCOUNT (replan-design.md, user_info): the
|
|
@@ -13547,7 +13586,8 @@ async function drainInput(state, deps = {}) {
|
|
|
13547
13586
|
return [];
|
|
13548
13587
|
}
|
|
13549
13588
|
const ours = (parentId) => state.exclusive || parentId === state.parentId;
|
|
13550
|
-
const
|
|
13589
|
+
const opening = !!state.exclusive && !state.parentId;
|
|
13590
|
+
const claimable = (notificationId) => opening || state.resting === true || !state.sharedWithAgent || (state.mine?.has(notificationId) ?? false);
|
|
13551
13591
|
const mine = [
|
|
13552
13592
|
...work.replies.filter((r) => ours(r.parentId) && claimable(r.notificationId)).map((r) => ({ notificationId: r.notificationId, parentId: r.parentId, answer: r.answer })),
|
|
13553
13593
|
...work.requests.filter((r) => ours(r.parentId) && claimable(r.notificationId)).map((r) => ({ notificationId: r.notificationId, parentId: r.parentId, answer: { kind: "text", text: r.text } }))
|
|
@@ -13580,7 +13620,10 @@ async function pump(session, state, deps = {}) {
|
|
|
13580
13620
|
continue;
|
|
13581
13621
|
}
|
|
13582
13622
|
const words = spokenText(item.answer);
|
|
13583
|
-
if (words)
|
|
13623
|
+
if (words) {
|
|
13624
|
+
state.resting = false;
|
|
13625
|
+
session.send(words);
|
|
13626
|
+
}
|
|
13584
13627
|
}
|
|
13585
13628
|
const keepGoing = await deps.onIdle?.() ?? false;
|
|
13586
13629
|
if (!keepGoing) return;
|
|
@@ -13801,6 +13844,7 @@ function runHarness(opts) {
|
|
|
13801
13844
|
state.parentId ??= parentId;
|
|
13802
13845
|
if (event.kind === "turn") opts.log(`${event.role}: ${event.text.split("\n")[0] ?? ""}`);
|
|
13803
13846
|
if (event.kind === "idle") {
|
|
13847
|
+
state.resting = true;
|
|
13804
13848
|
if (endsWithQuestion(event.result)) {
|
|
13805
13849
|
const local = await opts.localAsk?.question?.(event.result ?? "") ?? null;
|
|
13806
13850
|
if (local?.trim() && session && running) {
|
|
@@ -13837,6 +13881,7 @@ function runHarness(opts) {
|
|
|
13837
13881
|
send(text) {
|
|
13838
13882
|
session?.send(text);
|
|
13839
13883
|
},
|
|
13884
|
+
working: () => running && state.resting !== true,
|
|
13840
13885
|
stop() {
|
|
13841
13886
|
running = false;
|
|
13842
13887
|
cancelAsks(state);
|
|
@@ -13848,10 +13893,38 @@ function runHarness(opts) {
|
|
|
13848
13893
|
}
|
|
13849
13894
|
|
|
13850
13895
|
// src/host.ts
|
|
13896
|
+
var HOST_FILE = join4(homedir5(), ".paigy", "host.json");
|
|
13897
|
+
function readHostState() {
|
|
13898
|
+
try {
|
|
13899
|
+
const s = JSON.parse(readFileSync3(HOST_FILE, "utf8"));
|
|
13900
|
+
if (Date.now() - s.at > 2e4) return null;
|
|
13901
|
+
try {
|
|
13902
|
+
process.kill(s.pid, 0);
|
|
13903
|
+
} catch {
|
|
13904
|
+
return null;
|
|
13905
|
+
}
|
|
13906
|
+
return s;
|
|
13907
|
+
} catch {
|
|
13908
|
+
return null;
|
|
13909
|
+
}
|
|
13910
|
+
}
|
|
13911
|
+
function hostElsewhere() {
|
|
13912
|
+
const s = readHostState();
|
|
13913
|
+
return !!s && s.pid !== process.pid;
|
|
13914
|
+
}
|
|
13851
13915
|
function startHost(opts) {
|
|
13852
13916
|
const runs = /* @__PURE__ */ new Map();
|
|
13853
13917
|
const asHost = { token: opts.token };
|
|
13918
|
+
const refreshNames = async () => {
|
|
13919
|
+
for (const slot of listSlots()) {
|
|
13920
|
+
const token = readToken(slot);
|
|
13921
|
+
if (!token) continue;
|
|
13922
|
+
const me = await whoAmI({ token }).catch(() => null);
|
|
13923
|
+
if (me?.name && me.name !== slotName(slot)) saveToken({ access_token: token, name: me.name, device: null }, slot);
|
|
13924
|
+
}
|
|
13925
|
+
};
|
|
13854
13926
|
const beat = () => {
|
|
13927
|
+
void refreshNames();
|
|
13855
13928
|
void heartbeat({
|
|
13856
13929
|
harnesses: detectAll().map((a) => ({ name: a.name, label: a.label, status: a.status })),
|
|
13857
13930
|
workspaces: listWorkspaces(opts.wsDeps)
|
|
@@ -13877,11 +13950,15 @@ function startHost(opts) {
|
|
|
13877
13950
|
log: log2
|
|
13878
13951
|
});
|
|
13879
13952
|
runs.set(spec.sessionId, { run, label });
|
|
13953
|
+
try {
|
|
13954
|
+
saveToken({ access_token: spec.token, name: label, device: null }, `session:${spec.sessionId.slice(0, 8)}`);
|
|
13955
|
+
} catch {
|
|
13956
|
+
}
|
|
13880
13957
|
opts.log(`\u25B6 phone-launched ${label} (${spec.harness}) in ${spec.workspace}`);
|
|
13881
13958
|
}
|
|
13882
13959
|
}
|
|
13883
13960
|
const SLOT_HARNESS = { "mcp-agent": "claude", codex: "codex" };
|
|
13884
|
-
const WAKE_PROMPT = "You were woken because Paigy work is waiting for you. Call check_replies
|
|
13961
|
+
const WAKE_PROMPT = "You were woken because Paigy work is waiting for you. This is a fresh session with your existing identity, so you may already have work in flight that you can't remember. Call check_replies FIRST, then get_thread on each conversation it returns and read it before you touch anything: it holds what you were doing, where (a repo or worktree may not be this folder), and what the owner already decided. Then handle what's waiting and follow your paigy instructions to stay in the loop.";
|
|
13885
13962
|
async function sweepSlots() {
|
|
13886
13963
|
const workspace = listWorkspaces(opts.wsDeps)[0];
|
|
13887
13964
|
if (!workspace) return;
|
|
@@ -13893,7 +13970,8 @@ function startHost(opts) {
|
|
|
13893
13970
|
if (!token) continue;
|
|
13894
13971
|
const work = await checkReplies({ token }).catch(() => null);
|
|
13895
13972
|
if (!work || work.requests.length === 0 && work.replies.length === 0) continue;
|
|
13896
|
-
const
|
|
13973
|
+
const label = slotName(slot) ?? slot;
|
|
13974
|
+
const log2 = (line) => opts.log(`[${label}] ${line}`);
|
|
13897
13975
|
const run = runHarness({
|
|
13898
13976
|
harness,
|
|
13899
13977
|
cwd: workspace,
|
|
@@ -13904,19 +13982,26 @@ function startHost(opts) {
|
|
|
13904
13982
|
// A dead run must not squat the slot — evict so the next wake can respawn.
|
|
13905
13983
|
onExit: () => {
|
|
13906
13984
|
runs.delete(key);
|
|
13907
|
-
opts.log(`\u2716 ${
|
|
13985
|
+
opts.log(`\u2716 ${label} ended`);
|
|
13908
13986
|
},
|
|
13909
13987
|
log: log2
|
|
13910
13988
|
});
|
|
13911
|
-
runs.set(key, { run, label
|
|
13912
|
-
opts.log(`\u25B6 woke ${
|
|
13989
|
+
runs.set(key, { run, label });
|
|
13990
|
+
opts.log(`\u25B6 woke ${label} (${harness}) \u2014 work was waiting in ${workspace}`);
|
|
13913
13991
|
}
|
|
13914
13992
|
}
|
|
13993
|
+
const publish = () => {
|
|
13994
|
+
try {
|
|
13995
|
+
writeFileSync3(HOST_FILE, JSON.stringify({ pid: process.pid, at: Date.now(), roster: api.roster() }));
|
|
13996
|
+
} catch {
|
|
13997
|
+
}
|
|
13998
|
+
};
|
|
13915
13999
|
beat();
|
|
13916
14000
|
const pulse = setInterval(beat, 6e4);
|
|
13917
14001
|
const spawnPoll = setInterval(() => {
|
|
13918
14002
|
void claimAndSpawn();
|
|
13919
14003
|
void sweepSlots();
|
|
14004
|
+
publish();
|
|
13920
14005
|
}, 5e3);
|
|
13921
14006
|
const stopSessions = () => {
|
|
13922
14007
|
for (const { run, label } of runs.values()) {
|
|
@@ -13925,15 +14010,35 @@ function startHost(opts) {
|
|
|
13925
14010
|
}
|
|
13926
14011
|
runs.clear();
|
|
13927
14012
|
};
|
|
13928
|
-
|
|
14013
|
+
const api = {
|
|
13929
14014
|
sessions: () => [...runs.entries()].map(([id, r]) => [id, r.label]),
|
|
14015
|
+
// Every identity this machine holds, plus what it's doing right now. Slots are the
|
|
14016
|
+
// roster (hatched agents, the terminal tools, claimed sessions); a run makes one of
|
|
14017
|
+
// them live, and a run mid-turn makes it working.
|
|
14018
|
+
roster: () => {
|
|
14019
|
+
const live = new Map([...runs.values()].map((r) => [r.label, r.run]));
|
|
14020
|
+
const names = /* @__PURE__ */ new Map();
|
|
14021
|
+
for (const key of listSlots()) if (key !== "Desktop") names.set(slotName(key) ?? key, key);
|
|
14022
|
+
for (const label of live.keys()) if (!names.has(label)) names.set(label, label);
|
|
14023
|
+
return [...names.keys()].map((name) => ({
|
|
14024
|
+
name,
|
|
14025
|
+
running: live.has(name),
|
|
14026
|
+
working: live.get(name)?.working() ?? false
|
|
14027
|
+
}));
|
|
14028
|
+
},
|
|
13930
14029
|
stopSessions,
|
|
13931
14030
|
stop() {
|
|
13932
14031
|
clearInterval(pulse);
|
|
13933
14032
|
clearInterval(spawnPoll);
|
|
13934
14033
|
stopSessions();
|
|
14034
|
+
try {
|
|
14035
|
+
writeFileSync3(HOST_FILE, JSON.stringify({ pid: process.pid, at: 0, roster: [] }));
|
|
14036
|
+
} catch {
|
|
14037
|
+
}
|
|
13935
14038
|
}
|
|
13936
14039
|
};
|
|
14040
|
+
publish();
|
|
14041
|
+
return api;
|
|
13937
14042
|
}
|
|
13938
14043
|
|
|
13939
14044
|
// src/main.ts
|
|
@@ -13945,22 +14050,23 @@ var win = null;
|
|
|
13945
14050
|
function log(message) {
|
|
13946
14051
|
win?.webContents.send("paigy:log", message);
|
|
13947
14052
|
}
|
|
13948
|
-
var ICON =
|
|
14053
|
+
var ICON = join5(here, "..", "assets", "icon.png");
|
|
13949
14054
|
function createWindow() {
|
|
13950
14055
|
win = new BrowserWindow({
|
|
13951
14056
|
width: 720,
|
|
13952
14057
|
height: 640,
|
|
13953
14058
|
icon: ICON,
|
|
13954
14059
|
// Windows/Linux window icon; macOS uses the dock icon below
|
|
13955
|
-
webPreferences: { preload:
|
|
14060
|
+
webPreferences: { preload: join5(here, "preload.cjs"), contextIsolation: true, nodeIntegration: false }
|
|
13956
14061
|
});
|
|
13957
|
-
void win.loadFile(
|
|
14062
|
+
void win.loadFile(join5(here, "..", "renderer", "index.html"));
|
|
13958
14063
|
}
|
|
13959
14064
|
var host = null;
|
|
13960
14065
|
app.whenReady().then(() => {
|
|
13961
14066
|
if (process.platform === "darwin") app.dock?.setIcon(ICON);
|
|
13962
14067
|
createWindow();
|
|
13963
|
-
if (deviceToken()) host = startHost({ wsDeps, token: deviceToken(), log });
|
|
14068
|
+
if (deviceToken() && !hostElsewhere()) host = startHost({ wsDeps, token: deviceToken(), log });
|
|
14069
|
+
else if (hostElsewhere()) log("\u25C9 this Mac is already hosting (the background service) \u2014 showing its agents");
|
|
13964
14070
|
app.on("activate", () => {
|
|
13965
14071
|
if (BrowserWindow.getAllWindows().length === 0) createWindow();
|
|
13966
14072
|
});
|
|
@@ -13978,7 +14084,10 @@ ipcMain.handle("paigy:status", () => ({
|
|
|
13978
14084
|
// (works, but the beacon upgrades it); none = show the beacon.
|
|
13979
14085
|
paired: Boolean(deviceToken()),
|
|
13980
14086
|
device: Boolean(readToken(DEVICE_SLOT)),
|
|
13981
|
-
sessions: host?.sessions().map(([, label]) => label) ?? []
|
|
14087
|
+
sessions: host?.sessions().map(([, label]) => label) ?? [],
|
|
14088
|
+
// The party (companion.md / party design.md): who lives on this machine and what
|
|
14089
|
+
// each one is doing THIS SECOND — the one thing the laptop knows better than the phone.
|
|
14090
|
+
roster: host?.roster() ?? readHostState()?.roster ?? []
|
|
13982
14091
|
}));
|
|
13983
14092
|
var beaconLive = false;
|
|
13984
14093
|
ipcMain.handle("paigy:beacon", async () => {
|
|
@@ -13993,7 +14102,7 @@ ipcMain.handle("paigy:beacon", async () => {
|
|
|
13993
14102
|
saveToken(token, DEVICE_SLOT);
|
|
13994
14103
|
beaconLive = false;
|
|
13995
14104
|
host?.stop();
|
|
13996
|
-
host = startHost({ wsDeps, token: deviceToken(), log });
|
|
14105
|
+
if (!hostElsewhere()) host = startHost({ wsDeps, token: deviceToken(), log });
|
|
13997
14106
|
win?.webContents.send("paigy:paired", { name: token.name });
|
|
13998
14107
|
log(`\u2713 paired as "${token.name}" \u2014 this machine is now launchable from your phone`);
|
|
13999
14108
|
return;
|