@paigy/harness 0.3.39 → 0.3.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -37
- package/dist/cli.js +28803 -29648
- package/dist/main.js +191 -918
- package/package.json +1 -1
package/dist/main.js
CHANGED
|
@@ -9987,45 +9987,6 @@ var OptionInputSchema = external_exports.object({
|
|
|
9987
9987
|
imgUrl: OptionSchema.shape.image
|
|
9988
9988
|
}).strict();
|
|
9989
9989
|
var NIGHT = { from: 23, to: 7 };
|
|
9990
|
-
var ASKING_RULES = `HOW TO ASK:
|
|
9991
|
-
1. Write one question per object. Five questions are five objects in \`questions\`, in one contact, so each can be answered on its own; one question with five parts settles nothing until all five are answered.
|
|
9992
|
-
2. Name the Goal you are working on as the question's \`goalId\`: the question goes onto that Goal and its answer comes back there. It must exist: create it with manage_goals first.
|
|
9993
|
-
3. \`workItBlocks\` only for work you are truly blocked on (it asks for a call); leave it out for a question you can keep working around.
|
|
9994
|
-
4. Never hold the process open with while-loops. A wait that comes back \`expired\` is not a finished conversation: wait again with contact({wait:true}) only while you have nothing else to do. While a Call you placed is open, keep receiving window after window until it ends, even when a window is empty: the person may give follow-ups or instructions. With no open Call, STOP WAITING when a window comes back with nothing new \u2014 the question stays open, and its answer reaches you on the Goal. Otherwise, yield only with a working listener or scheduled wakeup, and collect answers with \`contact({wait:false})\` or \`get_goal\` on that wake.
|
|
9995
|
-
5. One question, one row. Never restate a question that is still waiting inside a new contact: keep waiting on the original, or the answer lands on one copy and the other stays open. Take back one that no longer matters with \`withdrawQuestionIds\`.
|
|
9996
|
-
6. Read the Goal's conversation before asking. Never ask again what was answered or already shipped \u2014 and tell its states apart: \`answered\` carries the choice in \`answer\`, while \`replied\` means they said something and chose none of your options, so that question is still yours to settle and their words are the line beside it.
|
|
9997
|
-
7. A question carries its options when you have them; a free-text question may omit them. Each option\u2019s label must stand on its own and name its choice in full -- never \`All three\` or \`Option 2\`. \`pickMode: "many"\` makes the card a checklist (options they may want several of); \`"one"\`, the default, a pick (one rules out the others); \`"rank"\` an ordering. Each question is sent exactly as written, with no reading in between. They can always answer in their own words, so never add an 'Other' option.
|
|
9998
|
-
8. A diagnosis says when, why and how it happens, then proposes one fix. Never options first.
|
|
9999
|
-
9. GIVE A REPORT ITS REPLY: an update takes \`reply\` too, one line the person can tap instead of typing, written in THEIR voice for THAT report ("Sounds good, I'll test later") \u2014 the same care you give a question's options. What they tap is what you read back, which tells you whether they verified your work or only received it. One line, because a report has one way to close, and it may not change what happens next: a reply that would is a question. Requested delivery and completed-Goal reports require a reply (reply_required); ordinary progress may omit it.`;
|
|
10000
|
-
function draft2020(node) {
|
|
10001
|
-
if (Array.isArray(node)) return node.map(draft2020);
|
|
10002
|
-
if (node && typeof node === "object") {
|
|
10003
|
-
const o = node;
|
|
10004
|
-
if (Array.isArray(o.items)) {
|
|
10005
|
-
o.prefixItems = o.items;
|
|
10006
|
-
if ("additionalItems" in o) {
|
|
10007
|
-
o.items = o.additionalItems;
|
|
10008
|
-
delete o.additionalItems;
|
|
10009
|
-
} else delete o.items;
|
|
10010
|
-
}
|
|
10011
|
-
for (const [excl, lim] of [["exclusiveMinimum", "minimum"], ["exclusiveMaximum", "maximum"]]) {
|
|
10012
|
-
if (typeof o[excl] === "boolean") {
|
|
10013
|
-
if (o[excl] === true && typeof o[lim] === "number") {
|
|
10014
|
-
o[excl] = o[lim];
|
|
10015
|
-
delete o[lim];
|
|
10016
|
-
} else delete o[excl];
|
|
10017
|
-
}
|
|
10018
|
-
}
|
|
10019
|
-
for (const k of Object.keys(o)) o[k] = draft2020(o[k]);
|
|
10020
|
-
return o;
|
|
10021
|
-
}
|
|
10022
|
-
return node;
|
|
10023
|
-
}
|
|
10024
|
-
function mcpInputSchema(s) {
|
|
10025
|
-
const schema = zodToJsonSchema(s, { target: "jsonSchema2019-09", $refStrategy: "none" });
|
|
10026
|
-
delete schema.$schema;
|
|
10027
|
-
return draft2020(schema);
|
|
10028
|
-
}
|
|
10029
9990
|
var ASK_MAX = 250;
|
|
10030
9991
|
var UNITS_MAX = 12;
|
|
10031
9992
|
var UNIT_TITLE_MAX = 50;
|
|
@@ -10075,6 +10036,60 @@ function capRefusals(text, units22, field) {
|
|
|
10075
10036
|
function refuseOverCaps(text, units22, field, ctx) {
|
|
10076
10037
|
for (const { path, message, ...params } of capRefusals(text, units22, field)) ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path, message, params });
|
|
10077
10038
|
}
|
|
10039
|
+
var COMPLETION_MESSAGE_MAX = 50;
|
|
10040
|
+
var CompletionSchema = external_exports.object({
|
|
10041
|
+
goalId: external_exports.string().uuid().describe("The existing Goal to complete."),
|
|
10042
|
+
message: external_exports.string().trim().min(1).max(COMPLETION_MESSAGE_MAX, "completion_message_too_long: at most 50 characters; put detail in units.").describe("Completion headline, at most 50 characters."),
|
|
10043
|
+
units: external_exports.array(UnitInputSchema.extend({
|
|
10044
|
+
title: external_exports.string().trim().min(1).max(UNIT_TITLE_MAX),
|
|
10045
|
+
body: external_exports.string().trim().min(1).max(UNIT_BODY_MAX)
|
|
10046
|
+
})).max(UNITS_MAX).optional().describe("Suggest initial state, changes, current state with verification and limits, and next steps / leftover work with its owner when useful. One idea per unit; omit unnecessary sections."),
|
|
10047
|
+
reply: external_exports.string().trim().min(1).max(OPTION_LABEL_MAX).describe("One acknowledgement in the person's voice, at most 50 characters. Acknowledgement alone requests no follow-up and does not claim they tested the work."),
|
|
10048
|
+
options: external_exports.array(external_exports.object({
|
|
10049
|
+
label: external_exports.string().trim().min(1).max(OPTION_LABEL_MAX),
|
|
10050
|
+
hint: external_exports.string().trim().min(1).max(OPTION_HINT_MAX)
|
|
10051
|
+
}).strict()).max(5).optional().describe("Optional next steps the person may initiate: label (50 characters) and consequence in hint (200). Selecting one acknowledges the report and requests that step from its author; it never reopens this Goal.")
|
|
10052
|
+
}).strict();
|
|
10053
|
+
var CompleteChangeSchema = CompletionSchema.extend({ kind: external_exports.literal("complete") });
|
|
10054
|
+
var ASKING_RULES = `HOW TO ASK:
|
|
10055
|
+
1. Write one question per object. Five questions are five objects in \`questions\`, in one contact, so each can be answered on its own; one question with five parts settles nothing until all five are answered.
|
|
10056
|
+
2. Name the Goal you are working on as the question's \`goalId\`: the question goes onto that Goal and its answer comes back there. It must exist: create it with manage_goals first.
|
|
10057
|
+
3. \`workItBlocks\` only for work you are truly blocked on (it asks for a call); leave it out for a question you can keep working around.
|
|
10058
|
+
4. Never hold the process open with while-loops. A wait that comes back \`expired\` is not a finished conversation: wait again with contact({wait:true}) only while you have nothing else to do. While a Call you placed is open, keep receiving window after window until it ends, even when a window is empty: the person may give follow-ups or instructions. With no open Call, STOP WAITING when a window comes back with nothing new \u2014 the question stays open, and its answer reaches you on the Goal. Otherwise, yield only with a working listener or scheduled wakeup, and collect answers with \`contact({wait:false})\` or \`get_goals\` on that wake.
|
|
10059
|
+
5. One question, one row. Never restate a question that is still waiting inside a new contact: keep waiting on the original, or the answer lands on one copy and the other stays open. Take back one that no longer matters with \`withdrawQuestionIds\`.
|
|
10060
|
+
6. Read the Goal's conversation before asking. Never ask again what was answered or already shipped \u2014 and tell its states apart: \`answered\` carries the choice in \`answer\`, while \`replied\` means they said something and chose none of your options, so that question is still yours to settle and their words are the line beside it.
|
|
10061
|
+
7. A question carries its options when you have them; a free-text question may omit them. Each option\u2019s label must stand on its own and name its choice in full -- never \`All three\` or \`Option 2\`. \`pickMode: "many"\` makes the card a checklist (options they may want several of); \`"one"\`, the default, a pick (one rules out the others); \`"rank"\` an ordering. Each question is sent exactly as written, with no reading in between. They can always answer in their own words, so never add an 'Other' option.
|
|
10062
|
+
8. A diagnosis says when, why and how it happens, then proposes one fix. Never options first.
|
|
10063
|
+
9. GIVE A REPORT ITS REPLY: an update takes \`reply\` too, one line the person can tap instead of typing, written in THEIR voice for THAT report ("Sounds good, I'll test later") \u2014 the same care you give a question's options. What they tap is what you read back, which tells you whether they verified your work or only received it. One line, because a report has one way to close, and it may not change what happens next: a reply that would is a question. Requested delivery and completed-Goal reports require a reply (reply_required); ordinary progress may omit it.`;
|
|
10064
|
+
function draft2020(node) {
|
|
10065
|
+
if (Array.isArray(node)) return node.map(draft2020);
|
|
10066
|
+
if (node && typeof node === "object") {
|
|
10067
|
+
const o = node;
|
|
10068
|
+
if (Array.isArray(o.items)) {
|
|
10069
|
+
o.prefixItems = o.items;
|
|
10070
|
+
if ("additionalItems" in o) {
|
|
10071
|
+
o.items = o.additionalItems;
|
|
10072
|
+
delete o.additionalItems;
|
|
10073
|
+
} else delete o.items;
|
|
10074
|
+
}
|
|
10075
|
+
for (const [excl, lim] of [["exclusiveMinimum", "minimum"], ["exclusiveMaximum", "maximum"]]) {
|
|
10076
|
+
if (typeof o[excl] === "boolean") {
|
|
10077
|
+
if (o[excl] === true && typeof o[lim] === "number") {
|
|
10078
|
+
o[excl] = o[lim];
|
|
10079
|
+
delete o[lim];
|
|
10080
|
+
} else delete o[excl];
|
|
10081
|
+
}
|
|
10082
|
+
}
|
|
10083
|
+
for (const k of Object.keys(o)) o[k] = draft2020(o[k]);
|
|
10084
|
+
return o;
|
|
10085
|
+
}
|
|
10086
|
+
return node;
|
|
10087
|
+
}
|
|
10088
|
+
function mcpInputSchema(s) {
|
|
10089
|
+
const schema = zodToJsonSchema(s, { target: "jsonSchema2019-09", $refStrategy: "none" });
|
|
10090
|
+
delete schema.$schema;
|
|
10091
|
+
return draft2020(schema);
|
|
10092
|
+
}
|
|
10078
10093
|
var questionHandle = external_exports.string().trim().regex(/^[0-9a-fA-F-]{8,36}$/, "a question id, whole or its first eight characters");
|
|
10079
10094
|
var NodeRef = external_exports.object({
|
|
10080
10095
|
type: external_exports.enum(["goal", "question"]),
|
|
@@ -10185,6 +10200,7 @@ var QuestionDependencySchema = external_exports.object({
|
|
|
10185
10200
|
reason: reason.optional()
|
|
10186
10201
|
}).strict().refine(actionFits, ACTION_FITS);
|
|
10187
10202
|
var sendFields = {
|
|
10203
|
+
completions: external_exports.array(CompletionSchema).min(1).max(20).optional().describe("Close existing Goals and deliver their completion reports atomically. Ordinary updates never close work."),
|
|
10188
10204
|
updates: external_exports.array(UpdateInputSchema).min(1).max(20).optional().describe(
|
|
10189
10205
|
"News: progress, findings, a result. An update asks nothing. It reaches the person only when they asked for it (userExplicitlyRequested), when it answers something they said, or once its Goal is done; otherwise it is recorded as the Goal's progress and nobody is notified."
|
|
10190
10206
|
),
|
|
@@ -10207,20 +10223,22 @@ var ContactSchema = external_exports.object({
|
|
|
10207
10223
|
"Only controls when this returns. Sending: false (the default) returns at once; true waits up to about 45 seconds for a response to what you sent. Sending nothing: true (the default) waits up to about 45 seconds for anything addressed to you; false returns what is waiting now. It does not mean work is blocked, set urgency, or ask for a call. `waitOutcome` says which happened: `available`, `expired` (nothing arrived; wait again without sending again), or `not_waited`."
|
|
10208
10224
|
)
|
|
10209
10225
|
}).strict();
|
|
10210
|
-
var sendsAnything = (c) => !!(c.updates?.length || c.questions?.length || c.answers?.length || c.withdrawQuestionIds?.length || c.questionDependencies?.length);
|
|
10226
|
+
var sendsAnything = (c) => !!(c.completions?.length || c.updates?.length || c.questions?.length || c.answers?.length || c.withdrawQuestionIds?.length || c.questionDependencies?.length);
|
|
10211
10227
|
var ContactWireSchema = external_exports.object({
|
|
10212
10228
|
operationId: external_exports.string().uuid(),
|
|
10213
10229
|
...sendFields,
|
|
10214
10230
|
questions: external_exports.array(QuestionWireSchema).min(1).max(20).optional()
|
|
10215
10231
|
}).strict().refine(sendsAnything, "a contact sends at least one update, question, answer, withdrawal or question dependency");
|
|
10216
10232
|
var CONTACT_SCHEMA = { type: "object", ...mcpInputSchema(ContactSchema) };
|
|
10217
|
-
var CONTACT_DESCRIPTION = "Send to and receive from the person: updates, questions, answers to questions addressed to you, withdrawals of your own questions, and what a question holds up. Every Goal you name must already exist: create it with manage_goals first; an unknown Goal is refused and nothing is sent. Every item applies on its own: read `results`, where a failed item names why while the rest still apply.\n\n" + ASKING_RULES + `
|
|
10233
|
+
var CONTACT_DESCRIPTION = "Send to and receive from the person: completions, updates, questions, answers to questions addressed to you, withdrawals of your own questions, and what a question holds up. Every Goal you name must already exist: create it with manage_goals first; an unknown Goal is refused and nothing is sent. Every item applies on its own: read `results`, where a failed item names why while the rest still apply.\n\n" + ASKING_RULES + `
|
|
10218
10234
|
|
|
10219
10235
|
CUT A LONG MESSAGE BEFORE YOU SEND IT: a question, an update's message or an answer is the one point, in at most ${ASK_MAX} characters; everything else it needs goes in 'units', up to ${UNITS_MAX} {title, body} in reading order, one idea each, every title at most ${UNIT_TITLE_MAX} characters and every body at most ${UNIT_BODY_MAX}. The person sees the main text and the titles, and opens a unit to read its body. Over a cap is refused by name (ask_too_long, too_many_units, unit_title_too_long, unit_body_too_long) and nothing is sent.
|
|
10220
10236
|
|
|
10221
|
-
UPDATES ask nothing, so no answer is owed and none should be awaited. An update reaches the person only when they asked you for it (userExplicitlyRequested), when it answers something they said, or once its Goal is done; any other is recorded as the Goal's progress and nobody is notified.
|
|
10237
|
+
UPDATES ask nothing, so no answer is owed and none should be awaited. An update reaches the person only when they asked you for it (userExplicitlyRequested), when it answers something they said, or once its Goal is done; any other is recorded as the Goal's progress and nobody is notified. Ordinary updates never close a Goal. On a Goal whose report card is still open, an update that reaches them is added to that card, with no new push. If the person is already on a call, anything that reaches them joins that call, with no ring.
|
|
10238
|
+
|
|
10239
|
+
COMPLETIONS close and report in one operation: completions:[{goalId,message,units?,reply,options?}]. The same contract works in manage_goals changes with kind:complete. message is at most 50 characters; use units when useful for initial state, changes, current state (verification and limits), and next steps / leftover work with its owner. reply is one acknowledgement in the person's voice. Optional options:[{label,hint}] suggest next steps: a tap acknowledges the report and requests that step from its author. It never reopens the completed Goal; follow-up work needs a new Goal. Closure and the report commit together or neither does. Required children and dependencies must already be finished. No Reviewer rejudges your completion; say only what your evidence supports.
|
|
10222
10240
|
|
|
10223
|
-
RECEIVING: a contact that sends nothing returns \`events\`, a limited batch of what is addressed to you (not a history page): \`question\`, a Question you owe (answer it in answers, with its questionId); \`update\`, something new on one of your Goals (a reply, an answer: read it with
|
|
10241
|
+
RECEIVING: a contact that sends nothing returns \`events\`, a limited batch of what is addressed to you (not a history page): \`question\`, a Question you owe (answer it in answers, with its questionId); \`update\`, something new on one of your Goals (a reply, an answer: read it with get_goals); \`instruction\`, a request or note sent to you. \`hasMore\` says more are waiting. Reading acknowledges nothing: once you have handled events, confirm their eventIds with contact({ackEventIds}), and the next batch can come. contact({}) waits up to about 45 seconds for something to arrive; contact({wait:false}) returns at once. It also lists work given to you that nobody has started (\`assigned\`; your first write to it starts it) and your work gone quiet (\`stalled\`).`;
|
|
10224
10242
|
var CreateGoalSchema = external_exports.object({
|
|
10225
10243
|
outcome: external_exports.string().trim().min(1).max(1e4),
|
|
10226
10244
|
/** The work's NAME (#2115) — one to five words, how a person refers to it out loud ("the night
|
|
@@ -10293,6 +10311,7 @@ var DependencyChange = external_exports.object({
|
|
|
10293
10311
|
var read = { revision: external_exports.number().int().positive().optional() };
|
|
10294
10312
|
var GoalChangeSchema = external_exports.discriminatedUnion("kind", [
|
|
10295
10313
|
CreateChange,
|
|
10314
|
+
CompleteChangeSchema.extend(read),
|
|
10296
10315
|
EditChange.extend(read),
|
|
10297
10316
|
StateChange.extend(read),
|
|
10298
10317
|
AssignChange.extend(read),
|
|
@@ -10310,24 +10329,20 @@ var ManageGoalsSchema = external_exports.object({
|
|
|
10310
10329
|
changes: external_exports.array(GoalChangeSchema).min(1).max(50)
|
|
10311
10330
|
}).strict().superRefine((v, ctx) => editNames(v.changes, ctx));
|
|
10312
10331
|
var ManageGoalsToolSchema = external_exports.object({
|
|
10313
|
-
changes: external_exports.array(external_exports.discriminatedUnion("kind", [CreateChange, EditChange, StateChange, AssignChange, DeferChange, MoveChange, DependencyChange])).min(1).max(50).describe("The changes, applied in order, each on its own.")
|
|
10332
|
+
changes: external_exports.array(external_exports.discriminatedUnion("kind", [CreateChange, CompleteChangeSchema, EditChange, StateChange, AssignChange, DeferChange, MoveChange, DependencyChange])).min(1).max(50).describe("The changes, applied in order, each on its own.")
|
|
10314
10333
|
}).strict().superRefine((v, ctx) => editNames(v.changes, ctx));
|
|
10315
|
-
var MANAGE_GOALS_DESCRIPTION = "Create, edit, assign, organize or close work. Every change here changes Goals; answering, withdrawing or asking Questions stays in contact. Each change applies independently: a refused one names why (`results[i].error`, e.g. goal_revision_conflict, goal_children_open, goal_not_found) and the rest still apply, so read every result: a partial result is never a complete success. Kinds: create (title, outcome, ownerId, parentGoalId, sourceEntryIds) returns the new Goal's id in `results[i].goalId`, in the order requested, to use in later calls (an outcome is one paragraph, at most 500 characters: split more into child Goals with parentGoalId); edit (title and/or outcome); state (open, completed, canceled); assign (ownerId); defer (until: the Goal waits until then and its owner is woken when it passes; null takes it back); move (parentGoalId, or null for a root: only this Goal moves); dependency (add or remove: goalId waits on dependsOnGoalId to start or to complete). A Goal cannot be completed while its required children or dependencies remain open: finish or move them first. Finishing the children does not prove the parent's own work is done. An edit applies at the version you last read: if someone changed the Goal since, it is refused as goal_revision_conflict, so read it again (
|
|
10316
|
-
var
|
|
10317
|
-
|
|
10334
|
+
var MANAGE_GOALS_DESCRIPTION = "Create, edit, assign, organize or close work. Every change here changes Goals; answering, withdrawing or asking Questions stays in contact. Each change applies independently: a refused one names why (`results[i].error`, e.g. goal_revision_conflict, goal_children_open, goal_not_found) and the rest still apply, so read every result: a partial result is never a complete success. Kinds: complete (goalId, message capped at 50 characters, units, reply, optional next-step options {label, hint}) closes and reports atomically, exactly as contact.completions; create (title, outcome, ownerId, parentGoalId, sourceEntryIds) returns the new Goal's id in `results[i].goalId`, in the order requested, to use in later calls (an outcome is one paragraph, at most 500 characters: split more into child Goals with parentGoalId); edit (title and/or outcome); state (open, completed, canceled); assign (ownerId); defer (until: the Goal waits until then and its owner is woken when it passes; null takes it back); move (parentGoalId, or null for a root: only this Goal moves); dependency (add or remove: goalId waits on dependsOnGoalId to start or to complete). A Goal cannot be completed while its required children or dependencies remain open: finish or move them first. Finishing the children does not prove the parent's own work is done. An edit applies at the version you last read: if someone changed the Goal since, it is refused as goal_revision_conflict, so read it again (get_goals) and reconsider. You may change any Goal of your person; a change puts you on it. Returns `ok` (every change applied), `results` per change (applied or failed), and `goals`, each changed Goal's id, state, title, outcome and revision.";
|
|
10335
|
+
var GetGoalsSchema = external_exports.object({
|
|
10336
|
+
goalIds: external_exports.array(external_exports.string().uuid()).min(1).max(10).describe("The Goals to read, in order; one to ten IDs."),
|
|
10318
10337
|
/** Every entry in full. Without it the read carries the person's words, open questions and your
|
|
10319
10338
|
* newest entry, and counts what it left out (`apps/api/src/goal/collapse.ts`). */
|
|
10320
|
-
history: external_exports.boolean().optional()
|
|
10321
|
-
/** WHEN A READ HAS CONFUSED YOU. Adds `diagnosis`: every reader that already answers a question
|
|
10322
|
-
* about this work, each answer attributed to the reader that gave it, and every disagreement
|
|
10323
|
-
* between two of them named. Off by default; `docs/model/goal/diagnose-design.md`. */
|
|
10324
|
-
diagnose: external_exports.boolean().optional()
|
|
10339
|
+
history: external_exports.boolean().optional()
|
|
10325
10340
|
}).strict();
|
|
10326
10341
|
var GetDeliverySchema = external_exports.object({
|
|
10327
10342
|
deliveryId: external_exports.string().uuid().describe("The Delivery id returned by contact or receive.")
|
|
10328
10343
|
}).strict();
|
|
10329
10344
|
var GET_DELIVERY_DESCRIPTION = "Read a Delivery in full: its whole ask, units, options and answers, plus current callState and callOutcome for a Call. Any agent of the same account may read it; another account is not disclosed. Reading never acknowledges or resends anything. Use this after a call or when receive names a Call you are waiting on.";
|
|
10330
|
-
var
|
|
10345
|
+
var GET_GOALS_DESCRIPTION = "Read one to ten Goals by goalIds, returned in the same order under goals (a read never puts you on them). history applies to every requested Goal. A refused read fails the call with its original refusal; no partial result is presented as complete. Each Goal includes its ten most recent other contributors (names, latest entry headlines and times), outcome, state, revision, progress, blockers, the conversation on it (each question with its options and what was decided), `next`, the one step to take, and `more`, where to read further. A question in that conversation reads `open` (nothing yet), `answered` (a choice was made, and `answer` carries it), `replied` (they said something and the read settled the question on their words \u2014 NO option of yours was chosen, and the words are the reply line beside it, so read that before you act), or `closed`. The conversation carries everything the person said, every open question and your newest entry; your older entries are left out and counted \u2014 history: true reads every entry in full. Every Goal of your person is readable, whichever of their agents owns it; another account's Goals are not disclosed.";
|
|
10331
10346
|
var SearchToolSchema = external_exports.object({
|
|
10332
10347
|
query: external_exports.string().trim().min(1).max(500),
|
|
10333
10348
|
types: external_exports.array(external_exports.enum(["entry", "goal", "answer"])).min(1).optional(),
|
|
@@ -10350,9 +10365,9 @@ var AGENT_TOOLS = [
|
|
|
10350
10365
|
{ name: "contact", description: CONTACT_DESCRIPTION, inputSchema: CONTACT_SCHEMA },
|
|
10351
10366
|
{ name: "manage_goals", description: MANAGE_GOALS_DESCRIPTION, inputSchema: mcpInputSchema(ManageGoalsToolSchema) },
|
|
10352
10367
|
{ name: "get_delivery", description: GET_DELIVERY_DESCRIPTION, inputSchema: mcpInputSchema(GetDeliverySchema) },
|
|
10353
|
-
{ name: "
|
|
10368
|
+
{ name: "get_goals", description: GET_GOALS_DESCRIPTION, inputSchema: mcpInputSchema(GetGoalsSchema) },
|
|
10354
10369
|
{ name: "search", description: SEARCH_DESCRIPTION, inputSchema: mcpInputSchema(SearchToolSchema) },
|
|
10355
|
-
{ name: "check_activity", description: "Read your person's agents that contributed in the last 24 hours, newest first: at most 30 agents and their five most recently touched Goals, with their latest entry headline and time. Use
|
|
10370
|
+
{ name: "check_activity", description: "Read your person's agents that contributed in the last 24 hours, newest first: at most 30 agents and their five most recently touched Goals, with their latest entry headline and time. Use get_goals to read a Goal and its other contributors before starting overlapping work. Read-only; no arguments.", inputSchema: mcpInputSchema(CheckActivitySchema) },
|
|
10356
10371
|
{ name: "send_feedback", description: SEND_FEEDBACK_DESCRIPTION, inputSchema: mcpInputSchema(SendFeedbackSchema) }
|
|
10357
10372
|
];
|
|
10358
10373
|
var AGENT_TOOL_NAMES = AGENT_TOOLS.map((t) => t.name);
|
|
@@ -11610,58 +11625,6 @@ var NoteSchema = external_exports.object({
|
|
|
11610
11625
|
lastRideAt: external_exports.string().nullable().default(null),
|
|
11611
11626
|
createdAt: external_exports.string()
|
|
11612
11627
|
});
|
|
11613
|
-
var TriageItemSchema = external_exports.object({
|
|
11614
|
-
noteId: external_exports.string(),
|
|
11615
|
-
/** The note's headline at run time. */
|
|
11616
|
-
title: external_exports.string(),
|
|
11617
|
-
/** WHY, in one short human line, evidence first — this is read on a phone underneath
|
|
11618
|
-
* the note's title: "no movement in 34 days", "worked 3 notes in this repo this week".
|
|
11619
|
-
* Never a model's reasoning transcript, never an id. */
|
|
11620
|
-
why: external_exports.string()
|
|
11621
|
-
});
|
|
11622
|
-
var TriageAssignmentSchema = external_exports.object({
|
|
11623
|
-
/** The agent's token id — what `dispatchNote` resolves and what a request is addressed to. */
|
|
11624
|
-
agent: external_exports.string(),
|
|
11625
|
-
/** Its display name at run time (the name on the hatchling's card). Denormalized for the
|
|
11626
|
-
* same reason as `title`: the card must render from the proposal alone. */
|
|
11627
|
-
agentName: external_exports.string(),
|
|
11628
|
-
notes: external_exports.array(TriageItemSchema)
|
|
11629
|
-
});
|
|
11630
|
-
var TriageStatusSchema = external_exports.enum(["open", "superseded", "dismissed"]);
|
|
11631
|
-
var SubmitTriageSchema = external_exports.object({
|
|
11632
|
-
/** Which runtime judged: "ollama" (inference never left the machine) or a harness the
|
|
11633
|
-
* user already runs under their own credentials ("claude" / "codex" / "agy"). Recorded
|
|
11634
|
-
* so the phone can say where the content went — an unattributed privacy claim is worth
|
|
11635
|
-
* nothing, and #1106's promise is precisely "Paigy's servers never see this". */
|
|
11636
|
-
provider: external_exports.string().min(1).max(60),
|
|
11637
|
-
/** The concrete model when the provider names one (an ollama tag); null otherwise. */
|
|
11638
|
-
model: external_exports.string().max(200).nullable().optional(),
|
|
11639
|
-
/** How many open notes the run actually looked at — the denominator on the phone
|
|
11640
|
-
* ("6 of 50"), and the honest answer to "did it read the whole queue?". */
|
|
11641
|
-
reviewed: external_exports.number().int().min(0).max(1e4).default(0),
|
|
11642
|
-
close: external_exports.array(TriageItemSchema).max(200).default([]),
|
|
11643
|
-
stale: external_exports.array(TriageItemSchema).max(200).default([]),
|
|
11644
|
-
assign: external_exports.array(TriageAssignmentSchema).max(50).default([])
|
|
11645
|
-
});
|
|
11646
|
-
var TriageProposalSchema = SubmitTriageSchema.extend({
|
|
11647
|
-
id: external_exports.string(),
|
|
11648
|
-
runAt: external_exports.string(),
|
|
11649
|
-
status: TriageStatusSchema,
|
|
11650
|
-
model: external_exports.string().nullable().default(null)
|
|
11651
|
-
});
|
|
11652
|
-
var AcceptTriageSchema = external_exports.discriminatedUnion("group", [
|
|
11653
|
-
external_exports.object({ group: external_exports.literal("close"), noteIds: external_exports.array(external_exports.string()).max(200).optional() }),
|
|
11654
|
-
external_exports.object({ group: external_exports.literal("stale"), noteIds: external_exports.array(external_exports.string()).max(200).optional() }),
|
|
11655
|
-
external_exports.object({
|
|
11656
|
-
group: external_exports.literal("assign"),
|
|
11657
|
-
agent: external_exports.string().min(1),
|
|
11658
|
-
noteIds: external_exports.array(external_exports.string()).max(200).optional()
|
|
11659
|
-
})
|
|
11660
|
-
]);
|
|
11661
|
-
var AcceptTriageResultSchema = external_exports.object({
|
|
11662
|
-
accepted: external_exports.array(external_exports.string()),
|
|
11663
|
-
failed: external_exports.array(external_exports.object({ noteId: external_exports.string(), reason: external_exports.string() }))
|
|
11664
|
-
});
|
|
11665
11628
|
var DeliveryModeSchema = external_exports.enum(["poll", "self_hosted"]);
|
|
11666
11629
|
var WAKE_EVENT = "wake";
|
|
11667
11630
|
var wakeChannel = (tokenId) => `wake:${tokenId}`;
|
|
@@ -11867,6 +11830,8 @@ var CallRecapSchema = external_exports.object({
|
|
|
11867
11830
|
status: external_exports.string(),
|
|
11868
11831
|
startedAt: external_exports.string(),
|
|
11869
11832
|
durationMs: external_exports.number().nullable(),
|
|
11833
|
+
/** Bot-clock pickup offset; absent before the timestamp release, null before its report lands. */
|
|
11834
|
+
pickupMs: external_exports.number().nullable().optional(),
|
|
11870
11835
|
agents: external_exports.array(external_exports.object({ id: external_exports.string(), name: external_exports.string().nullable() }))
|
|
11871
11836
|
}),
|
|
11872
11837
|
topics: external_exports.array(external_exports.object({
|
|
@@ -12067,15 +12032,15 @@ async function manageGoals(input, opts = {}) {
|
|
|
12067
12032
|
if (!res.ok) await fail("manage_goals", res);
|
|
12068
12033
|
return await res.json();
|
|
12069
12034
|
}
|
|
12070
|
-
async function
|
|
12035
|
+
async function getGoals(goalIds, opts = {}, read3 = {}) {
|
|
12071
12036
|
const token = authToken(opts.token) ?? "";
|
|
12072
|
-
const
|
|
12073
|
-
|
|
12074
|
-
|
|
12075
|
-
|
|
12037
|
+
const res = await ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/goals/read`, {
|
|
12038
|
+
method: "POST",
|
|
12039
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
|
|
12040
|
+
body: JSON.stringify({ goalIds, ...read3 })
|
|
12076
12041
|
}));
|
|
12077
|
-
if (!res.ok) await fail("
|
|
12078
|
-
return await res.json();
|
|
12042
|
+
if (!res.ok) await fail("get_goals", res);
|
|
12043
|
+
return (await res.json()).goals;
|
|
12079
12044
|
}
|
|
12080
12045
|
var AWAIT_WINDOW_MS = 45e3;
|
|
12081
12046
|
async function whoAmI(opts = {}) {
|
|
@@ -12147,13 +12112,6 @@ async function registerDelivery(mode, opts = {}) {
|
|
|
12147
12112
|
if (!res.ok) throw new Error(`register_delivery failed: ${res.status} ${await res.text()}`);
|
|
12148
12113
|
return await res.json();
|
|
12149
12114
|
}
|
|
12150
|
-
async function listNotes(opts = {}) {
|
|
12151
|
-
const res = await ensureAuthed(await reach(`${BACKEND_URL}/api/notes`, {
|
|
12152
|
-
headers: { authorization: `Bearer ${authToken(opts.token)}` }
|
|
12153
|
-
}));
|
|
12154
|
-
if (!res.ok) throw new Error(`list_notes failed: ${res.status} ${await res.text()}`);
|
|
12155
|
-
return await res.json();
|
|
12156
|
-
}
|
|
12157
12115
|
async function listConnections(opts = {}) {
|
|
12158
12116
|
const res = await ensureAuthed(await reach(`${BACKEND_URL}/api/tokens`, {
|
|
12159
12117
|
headers: { authorization: `Bearer ${authToken(opts.token)}` }
|
|
@@ -12161,40 +12119,7 @@ async function listConnections(opts = {}) {
|
|
|
12161
12119
|
if (!res.ok) throw new Error(`list_connections failed: ${res.status} ${await res.text()}`);
|
|
12162
12120
|
return await res.json();
|
|
12163
12121
|
}
|
|
12164
|
-
|
|
12165
|
-
const res = await ensureAuthed(await reach(`${BACKEND_URL}/api/triage`, {
|
|
12166
|
-
method: "POST",
|
|
12167
|
-
headers: { "content-type": "application/json", authorization: `Bearer ${authToken(opts.token)}` },
|
|
12168
|
-
body: JSON.stringify(run)
|
|
12169
|
-
}));
|
|
12170
|
-
if (!res.ok) throw new Error(`submit_triage failed: ${res.status} ${await res.text()}`);
|
|
12171
|
-
return await res.json();
|
|
12172
|
-
}
|
|
12173
|
-
async function getTriage(opts = {}) {
|
|
12174
|
-
const res = await ensureAuthed(await reach(`${BACKEND_URL}/api/triage`, {
|
|
12175
|
-
headers: { authorization: `Bearer ${authToken(opts.token)}` }
|
|
12176
|
-
}));
|
|
12177
|
-
if (!res.ok) throw new Error(`get_triage failed: ${res.status} ${await res.text()}`);
|
|
12178
|
-
return await res.json();
|
|
12179
|
-
}
|
|
12180
|
-
async function acceptTriage(proposalId, body, opts = {}) {
|
|
12181
|
-
const res = await ensureAuthed(await reach(`${BACKEND_URL}/api/triage/${encodeURIComponent(proposalId)}/accept`, {
|
|
12182
|
-
method: "POST",
|
|
12183
|
-
headers: { "content-type": "application/json", authorization: `Bearer ${authToken(opts.token)}` },
|
|
12184
|
-
body: JSON.stringify(body)
|
|
12185
|
-
}));
|
|
12186
|
-
if (!res.ok) throw new Error(`accept_triage failed: ${res.status} ${await res.text()}`);
|
|
12187
|
-
return await res.json();
|
|
12188
|
-
}
|
|
12189
|
-
async function dismissTriage(proposalId, opts = {}) {
|
|
12190
|
-
const res = await ensureAuthed(await reach(`${BACKEND_URL}/api/triage/${encodeURIComponent(proposalId)}/dismiss`, {
|
|
12191
|
-
method: "POST",
|
|
12192
|
-
headers: { authorization: `Bearer ${authToken(opts.token)}` }
|
|
12193
|
-
}));
|
|
12194
|
-
if (!res.ok) throw new Error(`dismiss_triage failed: ${res.status} ${await res.text()}`);
|
|
12195
|
-
return await res.json();
|
|
12196
|
-
}
|
|
12197
|
-
var NOT_SENT = `Recorded as the Goal's progress, not sent: nobody was notified. The person is told only a question, an answer to one they asked you, and what you say once the Goal is done. To close the work, mark the Goal done first, then send the one update saying what is done and anything they need to do or check. If the person asked you, in so many words, to send them this, send it again with userExplicitlyRequested: "any" on that update.`;
|
|
12122
|
+
var NOT_SENT = `Recorded as the Goal's progress, not sent: nobody was notified. The person is told only a question, an answer to one they asked you, and what you say once the Goal is done. To close the work, send contact.completions or manage_goals kind:complete with the shared completion report, closing and reporting together. If the person asked you, in so many words, to send them this, send it again with userExplicitlyRequested: "any" on that update.`;
|
|
12198
12123
|
var APPLIED = "Applied; nothing was sent to the person.";
|
|
12199
12124
|
var DEMOTED = "Delivered as a card, not a call: the person's settings do not allow a call right now (silent, quiet hours, or a mode). It has reached them; do not send it again.";
|
|
12200
12125
|
async function readDelivery(deliveryId, opts = {}) {
|
|
@@ -12219,6 +12144,7 @@ async function contact(input, opts = {}) {
|
|
|
12219
12144
|
body: JSON.stringify({
|
|
12220
12145
|
operationId: opts.operationId ?? randomUUID2(),
|
|
12221
12146
|
...parsed.updates ? { updates: parsed.updates } : {},
|
|
12147
|
+
...parsed.completions ? { completions: parsed.completions } : {},
|
|
12222
12148
|
...questions ? { questions } : {},
|
|
12223
12149
|
...parsed.answers ? { answers: parsed.answers } : {},
|
|
12224
12150
|
...parsed.withdrawQuestionIds ? { withdrawQuestionIds: parsed.withdrawQuestionIds } : {},
|
|
@@ -12455,7 +12381,7 @@ async function subscribeWake(onNudge, opts = {}) {
|
|
|
12455
12381
|
|
|
12456
12382
|
// src/main.ts
|
|
12457
12383
|
var import_qrcode = __toESM(require_lib(), 1);
|
|
12458
|
-
import { homedir as
|
|
12384
|
+
import { homedir as homedir10, hostname } from "os";
|
|
12459
12385
|
|
|
12460
12386
|
// src/harness/catalog.ts
|
|
12461
12387
|
import { spawnSync } from "child_process";
|
|
@@ -13861,45 +13787,6 @@ var OptionInputSchema2 = external_exports.object({
|
|
|
13861
13787
|
imgUrl: OptionSchema2.shape.image
|
|
13862
13788
|
}).strict();
|
|
13863
13789
|
var NIGHT2 = { from: 23, to: 7 };
|
|
13864
|
-
var ASKING_RULES2 = `HOW TO ASK:
|
|
13865
|
-
1. Write one question per object. Five questions are five objects in \`questions\`, in one contact, so each can be answered on its own; one question with five parts settles nothing until all five are answered.
|
|
13866
|
-
2. Name the Goal you are working on as the question's \`goalId\`: the question goes onto that Goal and its answer comes back there. It must exist: create it with manage_goals first.
|
|
13867
|
-
3. \`workItBlocks\` only for work you are truly blocked on (it asks for a call); leave it out for a question you can keep working around.
|
|
13868
|
-
4. Never hold the process open with while-loops. A wait that comes back \`expired\` is not a finished conversation: wait again with contact({wait:true}) only while you have nothing else to do. While a Call you placed is open, keep receiving window after window until it ends, even when a window is empty: the person may give follow-ups or instructions. With no open Call, STOP WAITING when a window comes back with nothing new \u2014 the question stays open, and its answer reaches you on the Goal. Otherwise, yield only with a working listener or scheduled wakeup, and collect answers with \`contact({wait:false})\` or \`get_goal\` on that wake.
|
|
13869
|
-
5. One question, one row. Never restate a question that is still waiting inside a new contact: keep waiting on the original, or the answer lands on one copy and the other stays open. Take back one that no longer matters with \`withdrawQuestionIds\`.
|
|
13870
|
-
6. Read the Goal's conversation before asking. Never ask again what was answered or already shipped \u2014 and tell its states apart: \`answered\` carries the choice in \`answer\`, while \`replied\` means they said something and chose none of your options, so that question is still yours to settle and their words are the line beside it.
|
|
13871
|
-
7. A question carries its options when you have them; a free-text question may omit them. Each option\u2019s label must stand on its own and name its choice in full -- never \`All three\` or \`Option 2\`. \`pickMode: "many"\` makes the card a checklist (options they may want several of); \`"one"\`, the default, a pick (one rules out the others); \`"rank"\` an ordering. Each question is sent exactly as written, with no reading in between. They can always answer in their own words, so never add an 'Other' option.
|
|
13872
|
-
8. A diagnosis says when, why and how it happens, then proposes one fix. Never options first.
|
|
13873
|
-
9. GIVE A REPORT ITS REPLY: an update takes \`reply\` too, one line the person can tap instead of typing, written in THEIR voice for THAT report ("Sounds good, I'll test later") \u2014 the same care you give a question's options. What they tap is what you read back, which tells you whether they verified your work or only received it. One line, because a report has one way to close, and it may not change what happens next: a reply that would is a question. Requested delivery and completed-Goal reports require a reply (reply_required); ordinary progress may omit it.`;
|
|
13874
|
-
function draft20202(node) {
|
|
13875
|
-
if (Array.isArray(node)) return node.map(draft20202);
|
|
13876
|
-
if (node && typeof node === "object") {
|
|
13877
|
-
const o = node;
|
|
13878
|
-
if (Array.isArray(o.items)) {
|
|
13879
|
-
o.prefixItems = o.items;
|
|
13880
|
-
if ("additionalItems" in o) {
|
|
13881
|
-
o.items = o.additionalItems;
|
|
13882
|
-
delete o.additionalItems;
|
|
13883
|
-
} else delete o.items;
|
|
13884
|
-
}
|
|
13885
|
-
for (const [excl, lim] of [["exclusiveMinimum", "minimum"], ["exclusiveMaximum", "maximum"]]) {
|
|
13886
|
-
if (typeof o[excl] === "boolean") {
|
|
13887
|
-
if (o[excl] === true && typeof o[lim] === "number") {
|
|
13888
|
-
o[excl] = o[lim];
|
|
13889
|
-
delete o[lim];
|
|
13890
|
-
} else delete o[excl];
|
|
13891
|
-
}
|
|
13892
|
-
}
|
|
13893
|
-
for (const k of Object.keys(o)) o[k] = draft20202(o[k]);
|
|
13894
|
-
return o;
|
|
13895
|
-
}
|
|
13896
|
-
return node;
|
|
13897
|
-
}
|
|
13898
|
-
function mcpInputSchema2(s) {
|
|
13899
|
-
const schema = zodToJsonSchema2(s, { target: "jsonSchema2019-09", $refStrategy: "none" });
|
|
13900
|
-
delete schema.$schema;
|
|
13901
|
-
return draft20202(schema);
|
|
13902
|
-
}
|
|
13903
13790
|
var ASK_MAX2 = 250;
|
|
13904
13791
|
var UNITS_MAX2 = 12;
|
|
13905
13792
|
var UNIT_TITLE_MAX2 = 50;
|
|
@@ -13949,6 +13836,60 @@ function capRefusals2(text, units22, field) {
|
|
|
13949
13836
|
function refuseOverCaps2(text, units22, field, ctx) {
|
|
13950
13837
|
for (const { path, message, ...params } of capRefusals2(text, units22, field)) ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path, message, params });
|
|
13951
13838
|
}
|
|
13839
|
+
var COMPLETION_MESSAGE_MAX2 = 50;
|
|
13840
|
+
var CompletionSchema2 = external_exports.object({
|
|
13841
|
+
goalId: external_exports.string().uuid().describe("The existing Goal to complete."),
|
|
13842
|
+
message: external_exports.string().trim().min(1).max(COMPLETION_MESSAGE_MAX2, "completion_message_too_long: at most 50 characters; put detail in units.").describe("Completion headline, at most 50 characters."),
|
|
13843
|
+
units: external_exports.array(UnitInputSchema2.extend({
|
|
13844
|
+
title: external_exports.string().trim().min(1).max(UNIT_TITLE_MAX2),
|
|
13845
|
+
body: external_exports.string().trim().min(1).max(UNIT_BODY_MAX2)
|
|
13846
|
+
})).max(UNITS_MAX2).optional().describe("Suggest initial state, changes, current state with verification and limits, and next steps / leftover work with its owner when useful. One idea per unit; omit unnecessary sections."),
|
|
13847
|
+
reply: external_exports.string().trim().min(1).max(OPTION_LABEL_MAX2).describe("One acknowledgement in the person's voice, at most 50 characters. Acknowledgement alone requests no follow-up and does not claim they tested the work."),
|
|
13848
|
+
options: external_exports.array(external_exports.object({
|
|
13849
|
+
label: external_exports.string().trim().min(1).max(OPTION_LABEL_MAX2),
|
|
13850
|
+
hint: external_exports.string().trim().min(1).max(OPTION_HINT_MAX2)
|
|
13851
|
+
}).strict()).max(5).optional().describe("Optional next steps the person may initiate: label (50 characters) and consequence in hint (200). Selecting one acknowledges the report and requests that step from its author; it never reopens this Goal.")
|
|
13852
|
+
}).strict();
|
|
13853
|
+
var CompleteChangeSchema2 = CompletionSchema2.extend({ kind: external_exports.literal("complete") });
|
|
13854
|
+
var ASKING_RULES2 = `HOW TO ASK:
|
|
13855
|
+
1. Write one question per object. Five questions are five objects in \`questions\`, in one contact, so each can be answered on its own; one question with five parts settles nothing until all five are answered.
|
|
13856
|
+
2. Name the Goal you are working on as the question's \`goalId\`: the question goes onto that Goal and its answer comes back there. It must exist: create it with manage_goals first.
|
|
13857
|
+
3. \`workItBlocks\` only for work you are truly blocked on (it asks for a call); leave it out for a question you can keep working around.
|
|
13858
|
+
4. Never hold the process open with while-loops. A wait that comes back \`expired\` is not a finished conversation: wait again with contact({wait:true}) only while you have nothing else to do. While a Call you placed is open, keep receiving window after window until it ends, even when a window is empty: the person may give follow-ups or instructions. With no open Call, STOP WAITING when a window comes back with nothing new \u2014 the question stays open, and its answer reaches you on the Goal. Otherwise, yield only with a working listener or scheduled wakeup, and collect answers with \`contact({wait:false})\` or \`get_goals\` on that wake.
|
|
13859
|
+
5. One question, one row. Never restate a question that is still waiting inside a new contact: keep waiting on the original, or the answer lands on one copy and the other stays open. Take back one that no longer matters with \`withdrawQuestionIds\`.
|
|
13860
|
+
6. Read the Goal's conversation before asking. Never ask again what was answered or already shipped \u2014 and tell its states apart: \`answered\` carries the choice in \`answer\`, while \`replied\` means they said something and chose none of your options, so that question is still yours to settle and their words are the line beside it.
|
|
13861
|
+
7. A question carries its options when you have them; a free-text question may omit them. Each option\u2019s label must stand on its own and name its choice in full -- never \`All three\` or \`Option 2\`. \`pickMode: "many"\` makes the card a checklist (options they may want several of); \`"one"\`, the default, a pick (one rules out the others); \`"rank"\` an ordering. Each question is sent exactly as written, with no reading in between. They can always answer in their own words, so never add an 'Other' option.
|
|
13862
|
+
8. A diagnosis says when, why and how it happens, then proposes one fix. Never options first.
|
|
13863
|
+
9. GIVE A REPORT ITS REPLY: an update takes \`reply\` too, one line the person can tap instead of typing, written in THEIR voice for THAT report ("Sounds good, I'll test later") \u2014 the same care you give a question's options. What they tap is what you read back, which tells you whether they verified your work or only received it. One line, because a report has one way to close, and it may not change what happens next: a reply that would is a question. Requested delivery and completed-Goal reports require a reply (reply_required); ordinary progress may omit it.`;
|
|
13864
|
+
function draft20202(node) {
|
|
13865
|
+
if (Array.isArray(node)) return node.map(draft20202);
|
|
13866
|
+
if (node && typeof node === "object") {
|
|
13867
|
+
const o = node;
|
|
13868
|
+
if (Array.isArray(o.items)) {
|
|
13869
|
+
o.prefixItems = o.items;
|
|
13870
|
+
if ("additionalItems" in o) {
|
|
13871
|
+
o.items = o.additionalItems;
|
|
13872
|
+
delete o.additionalItems;
|
|
13873
|
+
} else delete o.items;
|
|
13874
|
+
}
|
|
13875
|
+
for (const [excl, lim] of [["exclusiveMinimum", "minimum"], ["exclusiveMaximum", "maximum"]]) {
|
|
13876
|
+
if (typeof o[excl] === "boolean") {
|
|
13877
|
+
if (o[excl] === true && typeof o[lim] === "number") {
|
|
13878
|
+
o[excl] = o[lim];
|
|
13879
|
+
delete o[lim];
|
|
13880
|
+
} else delete o[excl];
|
|
13881
|
+
}
|
|
13882
|
+
}
|
|
13883
|
+
for (const k of Object.keys(o)) o[k] = draft20202(o[k]);
|
|
13884
|
+
return o;
|
|
13885
|
+
}
|
|
13886
|
+
return node;
|
|
13887
|
+
}
|
|
13888
|
+
function mcpInputSchema2(s) {
|
|
13889
|
+
const schema = zodToJsonSchema2(s, { target: "jsonSchema2019-09", $refStrategy: "none" });
|
|
13890
|
+
delete schema.$schema;
|
|
13891
|
+
return draft20202(schema);
|
|
13892
|
+
}
|
|
13952
13893
|
var questionHandle2 = external_exports.string().trim().regex(/^[0-9a-fA-F-]{8,36}$/, "a question id, whole or its first eight characters");
|
|
13953
13894
|
var NodeRef2 = external_exports.object({
|
|
13954
13895
|
type: external_exports.enum(["goal", "question"]),
|
|
@@ -14059,6 +14000,7 @@ var QuestionDependencySchema2 = external_exports.object({
|
|
|
14059
14000
|
reason: reason2.optional()
|
|
14060
14001
|
}).strict().refine(actionFits2, ACTION_FITS2);
|
|
14061
14002
|
var sendFields2 = {
|
|
14003
|
+
completions: external_exports.array(CompletionSchema2).min(1).max(20).optional().describe("Close existing Goals and deliver their completion reports atomically. Ordinary updates never close work."),
|
|
14062
14004
|
updates: external_exports.array(UpdateInputSchema2).min(1).max(20).optional().describe(
|
|
14063
14005
|
"News: progress, findings, a result. An update asks nothing. It reaches the person only when they asked for it (userExplicitlyRequested), when it answers something they said, or once its Goal is done; otherwise it is recorded as the Goal's progress and nobody is notified."
|
|
14064
14006
|
),
|
|
@@ -14081,20 +14023,22 @@ var ContactSchema2 = external_exports.object({
|
|
|
14081
14023
|
"Only controls when this returns. Sending: false (the default) returns at once; true waits up to about 45 seconds for a response to what you sent. Sending nothing: true (the default) waits up to about 45 seconds for anything addressed to you; false returns what is waiting now. It does not mean work is blocked, set urgency, or ask for a call. `waitOutcome` says which happened: `available`, `expired` (nothing arrived; wait again without sending again), or `not_waited`."
|
|
14082
14024
|
)
|
|
14083
14025
|
}).strict();
|
|
14084
|
-
var sendsAnything2 = (c) => !!(c.updates?.length || c.questions?.length || c.answers?.length || c.withdrawQuestionIds?.length || c.questionDependencies?.length);
|
|
14026
|
+
var sendsAnything2 = (c) => !!(c.completions?.length || c.updates?.length || c.questions?.length || c.answers?.length || c.withdrawQuestionIds?.length || c.questionDependencies?.length);
|
|
14085
14027
|
var ContactWireSchema2 = external_exports.object({
|
|
14086
14028
|
operationId: external_exports.string().uuid(),
|
|
14087
14029
|
...sendFields2,
|
|
14088
14030
|
questions: external_exports.array(QuestionWireSchema2).min(1).max(20).optional()
|
|
14089
14031
|
}).strict().refine(sendsAnything2, "a contact sends at least one update, question, answer, withdrawal or question dependency");
|
|
14090
14032
|
var CONTACT_SCHEMA2 = { type: "object", ...mcpInputSchema2(ContactSchema2) };
|
|
14091
|
-
var CONTACT_DESCRIPTION2 = "Send to and receive from the person: updates, questions, answers to questions addressed to you, withdrawals of your own questions, and what a question holds up. Every Goal you name must already exist: create it with manage_goals first; an unknown Goal is refused and nothing is sent. Every item applies on its own: read `results`, where a failed item names why while the rest still apply.\n\n" + ASKING_RULES2 + `
|
|
14033
|
+
var CONTACT_DESCRIPTION2 = "Send to and receive from the person: completions, updates, questions, answers to questions addressed to you, withdrawals of your own questions, and what a question holds up. Every Goal you name must already exist: create it with manage_goals first; an unknown Goal is refused and nothing is sent. Every item applies on its own: read `results`, where a failed item names why while the rest still apply.\n\n" + ASKING_RULES2 + `
|
|
14092
14034
|
|
|
14093
14035
|
CUT A LONG MESSAGE BEFORE YOU SEND IT: a question, an update's message or an answer is the one point, in at most ${ASK_MAX2} characters; everything else it needs goes in 'units', up to ${UNITS_MAX2} {title, body} in reading order, one idea each, every title at most ${UNIT_TITLE_MAX2} characters and every body at most ${UNIT_BODY_MAX2}. The person sees the main text and the titles, and opens a unit to read its body. Over a cap is refused by name (ask_too_long, too_many_units, unit_title_too_long, unit_body_too_long) and nothing is sent.
|
|
14094
14036
|
|
|
14095
|
-
UPDATES ask nothing, so no answer is owed and none should be awaited. An update reaches the person only when they asked you for it (userExplicitlyRequested), when it answers something they said, or once its Goal is done; any other is recorded as the Goal's progress and nobody is notified.
|
|
14037
|
+
UPDATES ask nothing, so no answer is owed and none should be awaited. An update reaches the person only when they asked you for it (userExplicitlyRequested), when it answers something they said, or once its Goal is done; any other is recorded as the Goal's progress and nobody is notified. Ordinary updates never close a Goal. On a Goal whose report card is still open, an update that reaches them is added to that card, with no new push. If the person is already on a call, anything that reaches them joins that call, with no ring.
|
|
14096
14038
|
|
|
14097
|
-
|
|
14039
|
+
COMPLETIONS close and report in one operation: completions:[{goalId,message,units?,reply,options?}]. The same contract works in manage_goals changes with kind:complete. message is at most 50 characters; use units when useful for initial state, changes, current state (verification and limits), and next steps / leftover work with its owner. reply is one acknowledgement in the person's voice. Optional options:[{label,hint}] suggest next steps: a tap acknowledges the report and requests that step from its author. It never reopens the completed Goal; follow-up work needs a new Goal. Closure and the report commit together or neither does. Required children and dependencies must already be finished. No Reviewer rejudges your completion; say only what your evidence supports.
|
|
14040
|
+
|
|
14041
|
+
RECEIVING: a contact that sends nothing returns \`events\`, a limited batch of what is addressed to you (not a history page): \`question\`, a Question you owe (answer it in answers, with its questionId); \`update\`, something new on one of your Goals (a reply, an answer: read it with get_goals); \`instruction\`, a request or note sent to you. \`hasMore\` says more are waiting. Reading acknowledges nothing: once you have handled events, confirm their eventIds with contact({ackEventIds}), and the next batch can come. contact({}) waits up to about 45 seconds for something to arrive; contact({wait:false}) returns at once. It also lists work given to you that nobody has started (\`assigned\`; your first write to it starts it) and your work gone quiet (\`stalled\`).`;
|
|
14098
14042
|
var CreateGoalSchema2 = external_exports.object({
|
|
14099
14043
|
outcome: external_exports.string().trim().min(1).max(1e4),
|
|
14100
14044
|
/** The work's NAME (#2115) — one to five words, how a person refers to it out loud ("the night
|
|
@@ -14167,6 +14111,7 @@ var DependencyChange2 = external_exports.object({
|
|
|
14167
14111
|
var read2 = { revision: external_exports.number().int().positive().optional() };
|
|
14168
14112
|
var GoalChangeSchema2 = external_exports.discriminatedUnion("kind", [
|
|
14169
14113
|
CreateChange2,
|
|
14114
|
+
CompleteChangeSchema2.extend(read2),
|
|
14170
14115
|
EditChange2.extend(read2),
|
|
14171
14116
|
StateChange2.extend(read2),
|
|
14172
14117
|
AssignChange2.extend(read2),
|
|
@@ -14184,24 +14129,20 @@ var ManageGoalsSchema2 = external_exports.object({
|
|
|
14184
14129
|
changes: external_exports.array(GoalChangeSchema2).min(1).max(50)
|
|
14185
14130
|
}).strict().superRefine((v, ctx) => editNames2(v.changes, ctx));
|
|
14186
14131
|
var ManageGoalsToolSchema2 = external_exports.object({
|
|
14187
|
-
changes: external_exports.array(external_exports.discriminatedUnion("kind", [CreateChange2, EditChange2, StateChange2, AssignChange2, DeferChange2, MoveChange2, DependencyChange2])).min(1).max(50).describe("The changes, applied in order, each on its own.")
|
|
14132
|
+
changes: external_exports.array(external_exports.discriminatedUnion("kind", [CreateChange2, CompleteChangeSchema2, EditChange2, StateChange2, AssignChange2, DeferChange2, MoveChange2, DependencyChange2])).min(1).max(50).describe("The changes, applied in order, each on its own.")
|
|
14188
14133
|
}).strict().superRefine((v, ctx) => editNames2(v.changes, ctx));
|
|
14189
|
-
var MANAGE_GOALS_DESCRIPTION2 = "Create, edit, assign, organize or close work. Every change here changes Goals; answering, withdrawing or asking Questions stays in contact. Each change applies independently: a refused one names why (`results[i].error`, e.g. goal_revision_conflict, goal_children_open, goal_not_found) and the rest still apply, so read every result: a partial result is never a complete success. Kinds: create (title, outcome, ownerId, parentGoalId, sourceEntryIds) returns the new Goal's id in `results[i].goalId`, in the order requested, to use in later calls (an outcome is one paragraph, at most 500 characters: split more into child Goals with parentGoalId); edit (title and/or outcome); state (open, completed, canceled); assign (ownerId); defer (until: the Goal waits until then and its owner is woken when it passes; null takes it back); move (parentGoalId, or null for a root: only this Goal moves); dependency (add or remove: goalId waits on dependsOnGoalId to start or to complete). A Goal cannot be completed while its required children or dependencies remain open: finish or move them first. Finishing the children does not prove the parent's own work is done. An edit applies at the version you last read: if someone changed the Goal since, it is refused as goal_revision_conflict, so read it again (
|
|
14190
|
-
var
|
|
14191
|
-
|
|
14134
|
+
var MANAGE_GOALS_DESCRIPTION2 = "Create, edit, assign, organize or close work. Every change here changes Goals; answering, withdrawing or asking Questions stays in contact. Each change applies independently: a refused one names why (`results[i].error`, e.g. goal_revision_conflict, goal_children_open, goal_not_found) and the rest still apply, so read every result: a partial result is never a complete success. Kinds: complete (goalId, message capped at 50 characters, units, reply, optional next-step options {label, hint}) closes and reports atomically, exactly as contact.completions; create (title, outcome, ownerId, parentGoalId, sourceEntryIds) returns the new Goal's id in `results[i].goalId`, in the order requested, to use in later calls (an outcome is one paragraph, at most 500 characters: split more into child Goals with parentGoalId); edit (title and/or outcome); state (open, completed, canceled); assign (ownerId); defer (until: the Goal waits until then and its owner is woken when it passes; null takes it back); move (parentGoalId, or null for a root: only this Goal moves); dependency (add or remove: goalId waits on dependsOnGoalId to start or to complete). A Goal cannot be completed while its required children or dependencies remain open: finish or move them first. Finishing the children does not prove the parent's own work is done. An edit applies at the version you last read: if someone changed the Goal since, it is refused as goal_revision_conflict, so read it again (get_goals) and reconsider. You may change any Goal of your person; a change puts you on it. Returns `ok` (every change applied), `results` per change (applied or failed), and `goals`, each changed Goal's id, state, title, outcome and revision.";
|
|
14135
|
+
var GetGoalsSchema2 = external_exports.object({
|
|
14136
|
+
goalIds: external_exports.array(external_exports.string().uuid()).min(1).max(10).describe("The Goals to read, in order; one to ten IDs."),
|
|
14192
14137
|
/** Every entry in full. Without it the read carries the person's words, open questions and your
|
|
14193
14138
|
* newest entry, and counts what it left out (`apps/api/src/goal/collapse.ts`). */
|
|
14194
|
-
history: external_exports.boolean().optional()
|
|
14195
|
-
/** WHEN A READ HAS CONFUSED YOU. Adds `diagnosis`: every reader that already answers a question
|
|
14196
|
-
* about this work, each answer attributed to the reader that gave it, and every disagreement
|
|
14197
|
-
* between two of them named. Off by default; `docs/model/goal/diagnose-design.md`. */
|
|
14198
|
-
diagnose: external_exports.boolean().optional()
|
|
14139
|
+
history: external_exports.boolean().optional()
|
|
14199
14140
|
}).strict();
|
|
14200
14141
|
var GetDeliverySchema2 = external_exports.object({
|
|
14201
14142
|
deliveryId: external_exports.string().uuid().describe("The Delivery id returned by contact or receive.")
|
|
14202
14143
|
}).strict();
|
|
14203
14144
|
var GET_DELIVERY_DESCRIPTION2 = "Read a Delivery in full: its whole ask, units, options and answers, plus current callState and callOutcome for a Call. Any agent of the same account may read it; another account is not disclosed. Reading never acknowledges or resends anything. Use this after a call or when receive names a Call you are waiting on.";
|
|
14204
|
-
var
|
|
14145
|
+
var GET_GOALS_DESCRIPTION2 = "Read one to ten Goals by goalIds, returned in the same order under goals (a read never puts you on them). history applies to every requested Goal. A refused read fails the call with its original refusal; no partial result is presented as complete. Each Goal includes its ten most recent other contributors (names, latest entry headlines and times), outcome, state, revision, progress, blockers, the conversation on it (each question with its options and what was decided), `next`, the one step to take, and `more`, where to read further. A question in that conversation reads `open` (nothing yet), `answered` (a choice was made, and `answer` carries it), `replied` (they said something and the read settled the question on their words \u2014 NO option of yours was chosen, and the words are the reply line beside it, so read that before you act), or `closed`. The conversation carries everything the person said, every open question and your newest entry; your older entries are left out and counted \u2014 history: true reads every entry in full. Every Goal of your person is readable, whichever of their agents owns it; another account's Goals are not disclosed.";
|
|
14205
14146
|
var SearchToolSchema2 = external_exports.object({
|
|
14206
14147
|
query: external_exports.string().trim().min(1).max(500),
|
|
14207
14148
|
types: external_exports.array(external_exports.enum(["entry", "goal", "answer"])).min(1).optional(),
|
|
@@ -14224,9 +14165,9 @@ var AGENT_TOOLS2 = [
|
|
|
14224
14165
|
{ name: "contact", description: CONTACT_DESCRIPTION2, inputSchema: CONTACT_SCHEMA2 },
|
|
14225
14166
|
{ name: "manage_goals", description: MANAGE_GOALS_DESCRIPTION2, inputSchema: mcpInputSchema2(ManageGoalsToolSchema2) },
|
|
14226
14167
|
{ name: "get_delivery", description: GET_DELIVERY_DESCRIPTION2, inputSchema: mcpInputSchema2(GetDeliverySchema2) },
|
|
14227
|
-
{ name: "
|
|
14168
|
+
{ name: "get_goals", description: GET_GOALS_DESCRIPTION2, inputSchema: mcpInputSchema2(GetGoalsSchema2) },
|
|
14228
14169
|
{ name: "search", description: SEARCH_DESCRIPTION2, inputSchema: mcpInputSchema2(SearchToolSchema2) },
|
|
14229
|
-
{ name: "check_activity", description: "Read your person's agents that contributed in the last 24 hours, newest first: at most 30 agents and their five most recently touched Goals, with their latest entry headline and time. Use
|
|
14170
|
+
{ name: "check_activity", description: "Read your person's agents that contributed in the last 24 hours, newest first: at most 30 agents and their five most recently touched Goals, with their latest entry headline and time. Use get_goals to read a Goal and its other contributors before starting overlapping work. Read-only; no arguments.", inputSchema: mcpInputSchema2(CheckActivitySchema2) },
|
|
14230
14171
|
{ name: "send_feedback", description: SEND_FEEDBACK_DESCRIPTION2, inputSchema: mcpInputSchema2(SendFeedbackSchema2) }
|
|
14231
14172
|
];
|
|
14232
14173
|
var AGENT_TOOL_NAMES2 = AGENT_TOOLS2.map((t) => t.name);
|
|
@@ -15495,58 +15436,6 @@ var NoteSchema2 = external_exports.object({
|
|
|
15495
15436
|
lastRideAt: external_exports.string().nullable().default(null),
|
|
15496
15437
|
createdAt: external_exports.string()
|
|
15497
15438
|
});
|
|
15498
|
-
var TriageItemSchema2 = external_exports.object({
|
|
15499
|
-
noteId: external_exports.string(),
|
|
15500
|
-
/** The note's headline at run time. */
|
|
15501
|
-
title: external_exports.string(),
|
|
15502
|
-
/** WHY, in one short human line, evidence first — this is read on a phone underneath
|
|
15503
|
-
* the note's title: "no movement in 34 days", "worked 3 notes in this repo this week".
|
|
15504
|
-
* Never a model's reasoning transcript, never an id. */
|
|
15505
|
-
why: external_exports.string()
|
|
15506
|
-
});
|
|
15507
|
-
var TriageAssignmentSchema2 = external_exports.object({
|
|
15508
|
-
/** The agent's token id — what `dispatchNote` resolves and what a request is addressed to. */
|
|
15509
|
-
agent: external_exports.string(),
|
|
15510
|
-
/** Its display name at run time (the name on the hatchling's card). Denormalized for the
|
|
15511
|
-
* same reason as `title`: the card must render from the proposal alone. */
|
|
15512
|
-
agentName: external_exports.string(),
|
|
15513
|
-
notes: external_exports.array(TriageItemSchema2)
|
|
15514
|
-
});
|
|
15515
|
-
var TriageStatusSchema2 = external_exports.enum(["open", "superseded", "dismissed"]);
|
|
15516
|
-
var SubmitTriageSchema2 = external_exports.object({
|
|
15517
|
-
/** Which runtime judged: "ollama" (inference never left the machine) or a harness the
|
|
15518
|
-
* user already runs under their own credentials ("claude" / "codex" / "agy"). Recorded
|
|
15519
|
-
* so the phone can say where the content went — an unattributed privacy claim is worth
|
|
15520
|
-
* nothing, and #1106's promise is precisely "Paigy's servers never see this". */
|
|
15521
|
-
provider: external_exports.string().min(1).max(60),
|
|
15522
|
-
/** The concrete model when the provider names one (an ollama tag); null otherwise. */
|
|
15523
|
-
model: external_exports.string().max(200).nullable().optional(),
|
|
15524
|
-
/** How many open notes the run actually looked at — the denominator on the phone
|
|
15525
|
-
* ("6 of 50"), and the honest answer to "did it read the whole queue?". */
|
|
15526
|
-
reviewed: external_exports.number().int().min(0).max(1e4).default(0),
|
|
15527
|
-
close: external_exports.array(TriageItemSchema2).max(200).default([]),
|
|
15528
|
-
stale: external_exports.array(TriageItemSchema2).max(200).default([]),
|
|
15529
|
-
assign: external_exports.array(TriageAssignmentSchema2).max(50).default([])
|
|
15530
|
-
});
|
|
15531
|
-
var TriageProposalSchema2 = SubmitTriageSchema2.extend({
|
|
15532
|
-
id: external_exports.string(),
|
|
15533
|
-
runAt: external_exports.string(),
|
|
15534
|
-
status: TriageStatusSchema2,
|
|
15535
|
-
model: external_exports.string().nullable().default(null)
|
|
15536
|
-
});
|
|
15537
|
-
var AcceptTriageSchema2 = external_exports.discriminatedUnion("group", [
|
|
15538
|
-
external_exports.object({ group: external_exports.literal("close"), noteIds: external_exports.array(external_exports.string()).max(200).optional() }),
|
|
15539
|
-
external_exports.object({ group: external_exports.literal("stale"), noteIds: external_exports.array(external_exports.string()).max(200).optional() }),
|
|
15540
|
-
external_exports.object({
|
|
15541
|
-
group: external_exports.literal("assign"),
|
|
15542
|
-
agent: external_exports.string().min(1),
|
|
15543
|
-
noteIds: external_exports.array(external_exports.string()).max(200).optional()
|
|
15544
|
-
})
|
|
15545
|
-
]);
|
|
15546
|
-
var AcceptTriageResultSchema2 = external_exports.object({
|
|
15547
|
-
accepted: external_exports.array(external_exports.string()),
|
|
15548
|
-
failed: external_exports.array(external_exports.object({ noteId: external_exports.string(), reason: external_exports.string() }))
|
|
15549
|
-
});
|
|
15550
15439
|
var DeliveryModeSchema2 = external_exports.enum(["poll", "self_hosted"]);
|
|
15551
15440
|
var RegisterDeliverySchema2 = external_exports.object({ mode: DeliveryModeSchema2 });
|
|
15552
15441
|
var OAuthStartSchema2 = external_exports.object({
|
|
@@ -15750,6 +15639,8 @@ var CallRecapSchema2 = external_exports.object({
|
|
|
15750
15639
|
status: external_exports.string(),
|
|
15751
15640
|
startedAt: external_exports.string(),
|
|
15752
15641
|
durationMs: external_exports.number().nullable(),
|
|
15642
|
+
/** Bot-clock pickup offset; absent before the timestamp release, null before its report lands. */
|
|
15643
|
+
pickupMs: external_exports.number().nullable().optional(),
|
|
15753
15644
|
agents: external_exports.array(external_exports.object({ id: external_exports.string(), name: external_exports.string().nullable() }))
|
|
15754
15645
|
}),
|
|
15755
15646
|
topics: external_exports.array(external_exports.object({
|
|
@@ -15801,579 +15692,27 @@ var CallRecapSchema2 = external_exports.object({
|
|
|
15801
15692
|
|
|
15802
15693
|
// src/host.ts
|
|
15803
15694
|
import { readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
|
|
15804
|
-
import { homedir as
|
|
15695
|
+
import { homedir as homedir9 } from "os";
|
|
15805
15696
|
import { join as join11 } from "path";
|
|
15806
15697
|
|
|
15807
|
-
// src/
|
|
15808
|
-
var OPEN_STATUSES = ["open", "assigned", "in_progress"];
|
|
15809
|
-
var DAY_MS = 864e5;
|
|
15810
|
-
var daysBetween = (from, now) => from ? Math.floor((now - Date.parse(from)) / DAY_MS) : null;
|
|
15811
|
-
function assigneeToken(note2) {
|
|
15812
|
-
const ref = note2.assignee ?? "";
|
|
15813
|
-
return ref.startsWith("agent:") ? ref.slice("agent:".length) : null;
|
|
15814
|
-
}
|
|
15815
|
-
var STOP = /* @__PURE__ */ new Set([
|
|
15816
|
-
"the",
|
|
15817
|
-
"and",
|
|
15818
|
-
"for",
|
|
15819
|
-
"with",
|
|
15820
|
-
"that",
|
|
15821
|
-
"this",
|
|
15822
|
-
"from",
|
|
15823
|
-
"into",
|
|
15824
|
-
"when",
|
|
15825
|
-
"what",
|
|
15826
|
-
"should",
|
|
15827
|
-
"would",
|
|
15828
|
-
"could",
|
|
15829
|
-
"have",
|
|
15830
|
-
"make",
|
|
15831
|
-
"made",
|
|
15832
|
-
"need",
|
|
15833
|
-
"needs",
|
|
15834
|
-
"want",
|
|
15835
|
-
"wants",
|
|
15836
|
-
"please",
|
|
15837
|
-
"also",
|
|
15838
|
-
"then",
|
|
15839
|
-
"than",
|
|
15840
|
-
"them",
|
|
15841
|
-
"they",
|
|
15842
|
-
"there",
|
|
15843
|
-
"here",
|
|
15844
|
-
"about",
|
|
15845
|
-
"some",
|
|
15846
|
-
"more",
|
|
15847
|
-
"most",
|
|
15848
|
-
"just",
|
|
15849
|
-
"like",
|
|
15850
|
-
"want",
|
|
15851
|
-
"will",
|
|
15852
|
-
"does",
|
|
15853
|
-
"done",
|
|
15854
|
-
"your",
|
|
15855
|
-
"yours",
|
|
15856
|
-
"each",
|
|
15857
|
-
"every",
|
|
15858
|
-
"onto"
|
|
15859
|
-
]);
|
|
15860
|
-
function words(text) {
|
|
15861
|
-
return (text.toLowerCase().match(/[a-z0-9][a-z0-9_.-]{2,}/g) ?? []).filter(
|
|
15862
|
-
(w) => w.length >= 4 && !STOP.has(w)
|
|
15863
|
-
);
|
|
15864
|
-
}
|
|
15865
|
-
function repoHints(text) {
|
|
15866
|
-
const hits = /* @__PURE__ */ new Set();
|
|
15867
|
-
for (const m of text.matchAll(/\b([a-z0-9][a-z0-9_.-]*\/[a-z0-9][a-z0-9_.-]*)/gi)) {
|
|
15868
|
-
const hit = m[1];
|
|
15869
|
-
if (hit) hits.add(hit.toLowerCase());
|
|
15870
|
-
}
|
|
15871
|
-
for (const m of text.matchAll(/`([^`]{2,60})`/g)) {
|
|
15872
|
-
const inner = m[1]?.trim().toLowerCase();
|
|
15873
|
-
if (inner && inner.includes("/")) hits.add(inner.split(/\s+/)[0] ?? inner);
|
|
15874
|
-
}
|
|
15875
|
-
return [...hits].slice(0, 6);
|
|
15876
|
-
}
|
|
15877
|
-
function overlap(a, b) {
|
|
15878
|
-
const A = new Set(words(a));
|
|
15879
|
-
const B = new Set(words(b));
|
|
15880
|
-
if (A.size === 0 || B.size === 0) return 0;
|
|
15881
|
-
let shared = 0;
|
|
15882
|
-
for (const w of A) if (B.has(w)) shared++;
|
|
15883
|
-
return shared / (A.size + B.size - shared);
|
|
15884
|
-
}
|
|
15885
|
-
var SUPERSEDE_AT = 0.5;
|
|
15886
|
-
function supersededBy(note2, all) {
|
|
15887
|
-
const mine = `${note2.title} ${note2.intent}`;
|
|
15888
|
-
const at = Date.parse(note2.createdAt);
|
|
15889
|
-
let best = null;
|
|
15890
|
-
for (const other of all) {
|
|
15891
|
-
if (other.id === note2.id) continue;
|
|
15892
|
-
if (Date.parse(other.createdAt) <= at) continue;
|
|
15893
|
-
const score = overlap(mine, `${other.title} ${other.intent}`);
|
|
15894
|
-
if (score >= SUPERSEDE_AT && (!best || score > best.score)) best = { note: other, score };
|
|
15895
|
-
}
|
|
15896
|
-
return best?.note ?? null;
|
|
15897
|
-
}
|
|
15898
|
-
function doneEcho(note2, all) {
|
|
15899
|
-
const mine = `${note2.title} ${note2.intent}`;
|
|
15900
|
-
let best = null;
|
|
15901
|
-
for (const other of all) {
|
|
15902
|
-
if (other.id === note2.id || other.status !== "done") continue;
|
|
15903
|
-
const score = overlap(mine, `${other.title} ${other.intent}`);
|
|
15904
|
-
if (score >= SUPERSEDE_AT && (!best || score > best.score)) best = { note: other, score };
|
|
15905
|
-
}
|
|
15906
|
-
return best?.note ?? null;
|
|
15907
|
-
}
|
|
15908
|
-
var RECENT_DAYS = 7;
|
|
15909
|
-
function agentSignals(notes, connections, now) {
|
|
15910
|
-
return connections.map((c) => {
|
|
15911
|
-
const mine = notes.filter((n) => assigneeToken(n) === c.id);
|
|
15912
|
-
const recent = mine.filter((n) => (daysBetween(n.createdAt, now) ?? Infinity) <= RECENT_DAYS);
|
|
15913
|
-
const repoCount = /* @__PURE__ */ new Map();
|
|
15914
|
-
for (const n of recent) for (const r of repoHints(`${n.title} ${n.intent}`)) repoCount.set(r, (repoCount.get(r) ?? 0) + 1);
|
|
15915
|
-
return {
|
|
15916
|
-
token: c.id,
|
|
15917
|
-
name: c.name,
|
|
15918
|
-
lastSeenDays: daysBetween(c.lastSeenAt ?? null, now),
|
|
15919
|
-
lastContactDays: daysBetween(c.lastContactAt, now),
|
|
15920
|
-
openNotes: mine.filter((n) => n.status !== "done").length,
|
|
15921
|
-
recentNotes: recent.length,
|
|
15922
|
-
repos: [...repoCount.entries()].sort((a, b) => b[1] - a[1]).map(([r]) => r).slice(0, 5),
|
|
15923
|
-
recentTitles: recent.slice(0, 5).map((n) => n.title)
|
|
15924
|
-
};
|
|
15925
|
-
});
|
|
15926
|
-
}
|
|
15927
|
-
function noteSignals(notes, connections, now) {
|
|
15928
|
-
const nameOf = new Map(connections.map((c) => [c.id, c.name]));
|
|
15929
|
-
const open = notes.filter((n) => OPEN_STATUSES.includes(n.status));
|
|
15930
|
-
return open.map((n) => {
|
|
15931
|
-
const superseded = supersededBy(n, notes);
|
|
15932
|
-
const echo = doneEcho(n, notes);
|
|
15933
|
-
const token = assigneeToken(n);
|
|
15934
|
-
return {
|
|
15935
|
-
id: n.id,
|
|
15936
|
-
title: n.title,
|
|
15937
|
-
intent: n.intent,
|
|
15938
|
-
status: n.status,
|
|
15939
|
-
ageDays: daysBetween(n.createdAt, now) ?? 0,
|
|
15940
|
-
idleDays: daysBetween(n.lastRideAt ?? n.createdAt, now) ?? 0,
|
|
15941
|
-
rides: n.rides,
|
|
15942
|
-
assignee: token,
|
|
15943
|
-
assigneeName: token ? nameOf.get(token) ?? null : null,
|
|
15944
|
-
repoHints: repoHints(`${n.title} ${n.intent}`),
|
|
15945
|
-
supersededBy: superseded ? { id: superseded.id, title: superseded.title } : null,
|
|
15946
|
-
doneEcho: echo ? { id: echo.id, title: echo.title } : null
|
|
15947
|
-
};
|
|
15948
|
-
});
|
|
15949
|
-
}
|
|
15950
|
-
function dossier(notes, connections, now) {
|
|
15951
|
-
return {
|
|
15952
|
-
notes: noteSignals(notes, connections, now),
|
|
15953
|
-
agents: agentSignals(notes, connections, now),
|
|
15954
|
-
today: new Date(now).toISOString().slice(0, 10)
|
|
15955
|
-
};
|
|
15956
|
-
}
|
|
15957
|
-
function affinityWhy(note2, agent2) {
|
|
15958
|
-
const shared = note2.repoHints.filter((r) => agent2.repos.includes(r));
|
|
15959
|
-
if (shared.length && agent2.recentNotes)
|
|
15960
|
-
return `worked ${agent2.recentNotes} note${agent2.recentNotes === 1 ? "" : "s"} in ${shared[0]} this week`;
|
|
15961
|
-
if (agent2.recentNotes >= 2) return `took ${agent2.recentNotes} notes this week`;
|
|
15962
|
-
if (agent2.lastSeenDays !== null && agent2.lastSeenDays <= 1) return "active on this machine today";
|
|
15963
|
-
if (agent2.lastContactDays !== null && agent2.lastContactDays <= RECENT_DAYS)
|
|
15964
|
-
return `last spoke ${agent2.lastContactDays === 0 ? "today" : `${agent2.lastContactDays}d ago`}`;
|
|
15965
|
-
return null;
|
|
15966
|
-
}
|
|
15967
|
-
function staleWhy(note2) {
|
|
15968
|
-
if (note2.doneEcho) return `a later note says this shipped: "${note2.doneEcho.title}"`;
|
|
15969
|
-
if (note2.supersededBy) return `superseded by "${note2.supersededBy.title}"`;
|
|
15970
|
-
if (note2.rides >= 3) return `carried on ${note2.rides} calls and still open`;
|
|
15971
|
-
if (note2.idleDays >= 30) return `no movement in ${note2.idleDays} days`;
|
|
15972
|
-
if (note2.idleDays >= 14 && note2.status === "open") return `unassigned for ${note2.idleDays} days`;
|
|
15973
|
-
return null;
|
|
15974
|
-
}
|
|
15975
|
-
|
|
15976
|
-
// src/triage/judge.ts
|
|
15977
|
-
import { spawnSync as spawnSync2 } from "child_process";
|
|
15978
|
-
import { homedir as homedir4 } from "os";
|
|
15979
|
-
import { delimiter as delimiter2 } from "path";
|
|
15980
|
-
var PRIVACY = {
|
|
15981
|
-
ollama: "never leaves this machine",
|
|
15982
|
-
claude: "this machine + your own Claude subscription; Paigy's servers never see it",
|
|
15983
|
-
codex: "this machine + your own Codex subscription; Paigy's servers never see it",
|
|
15984
|
-
agy: "this machine + your own Antigravity account; Paigy's servers never see it"
|
|
15985
|
-
};
|
|
15986
|
-
var OLLAMA_HOST = process.env["OLLAMA_HOST"] ?? "http://localhost:11434";
|
|
15987
|
-
var PREFERRED = ["llama3.2", "qwen2.5", "phi", "gemma", "mistral", "llama3.1", "llama3"];
|
|
15988
|
-
function pickOllamaModel(tags) {
|
|
15989
|
-
if (tags.length === 0) return null;
|
|
15990
|
-
const score = (tag) => {
|
|
15991
|
-
const i = PREFERRED.findIndex((p) => tag.toLowerCase().startsWith(p));
|
|
15992
|
-
return i === -1 ? PREFERRED.length : i;
|
|
15993
|
-
};
|
|
15994
|
-
return [...tags].sort((a, b) => score(a) - score(b) || a.length - b.length)[0] ?? null;
|
|
15995
|
-
}
|
|
15996
|
-
function parseOllamaTags(body) {
|
|
15997
|
-
const models = body?.models;
|
|
15998
|
-
if (!Array.isArray(models)) return [];
|
|
15999
|
-
return models.map((m) => m?.name ?? m?.model).filter((n) => typeof n === "string" && n.length > 0);
|
|
16000
|
-
}
|
|
16001
|
-
async function fetchTags(host2) {
|
|
16002
|
-
try {
|
|
16003
|
-
const res = await fetch(`${host2}/api/tags`, { signal: AbortSignal.timeout(2e3) });
|
|
16004
|
-
if (!res.ok) return null;
|
|
16005
|
-
return parseOllamaTags(await res.json());
|
|
16006
|
-
} catch {
|
|
16007
|
-
return null;
|
|
16008
|
-
}
|
|
16009
|
-
}
|
|
16010
|
-
async function available(deps = {}) {
|
|
16011
|
-
const tags = await (deps.tags ?? fetchTags)(OLLAMA_HOST);
|
|
16012
|
-
const model = tags ? pickOllamaModel(tags) : null;
|
|
16013
|
-
const ollama = tags === null ? { provider: "ollama", ready: false, model: null, hint: `no ollama at ${OLLAMA_HOST} \u2014 install it: https://ollama.com/download` } : model === null ? { provider: "ollama", ready: false, model: null, hint: "ollama is running but has no models \u2014 pull one: ollama pull llama3.2:3b" } : { provider: "ollama", ready: true, model };
|
|
16014
|
-
const detected = (deps.detect ?? detectAll)();
|
|
16015
|
-
const harnesses = CATALOG.map((entry) => {
|
|
16016
|
-
const state = detected.find((d) => d.name === entry.name);
|
|
16017
|
-
const usable = state?.status === "ready" || state?.status === "adapter-missing";
|
|
16018
|
-
return usable ? { provider: entry.name, ready: true, model: null } : { provider: entry.name, ready: false, model: null, hint: state?.hint ?? `${entry.label} is not installed.` };
|
|
16019
|
-
});
|
|
16020
|
-
return [ollama, ...harnesses];
|
|
16021
|
-
}
|
|
16022
|
-
function refusal(options, asked) {
|
|
16023
|
-
const lines2 = [
|
|
16024
|
-
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.",
|
|
16025
|
-
"",
|
|
16026
|
-
"Triage runs the model where your queue already lives (#1106). Your options:"
|
|
16027
|
-
];
|
|
16028
|
-
for (const o of options) {
|
|
16029
|
-
lines2.push(` ${o.ready ? "\u2713" : "\u2717"} ${o.provider.padEnd(7)} ${PRIVACY[o.provider]}`);
|
|
16030
|
-
if (!o.ready && o.hint) lines2.push(` ${o.hint}`);
|
|
16031
|
-
}
|
|
16032
|
-
lines2.push("", "Then re-run: paigy-harness triage [--via ollama|claude|codex|agy]");
|
|
16033
|
-
return lines2.join("\n");
|
|
16034
|
-
}
|
|
16035
|
-
var RUNNERS = {
|
|
16036
|
-
// Ollama speaks HTTP, not a CLI — handled separately in `judgeFor`.
|
|
16037
|
-
ollama: null,
|
|
16038
|
-
// `claude -p` is print mode: one prompt, one answer, no session. Verified 2026-08-28.
|
|
16039
|
-
claude: { cli: "claude", argv: () => ["-p"] },
|
|
16040
|
-
// `codex exec` is Codex's documented non-interactive run; it reads the prompt from stdin
|
|
16041
|
-
// when none is given as an argument. It prints its own preamble around the answer, which
|
|
16042
|
-
// is exactly why the parser hunts for the JSON rather than trusting the whole of stdout.
|
|
16043
|
-
codex: { cli: "codex", argv: () => ["exec", "--skip-git-repo-check", "-"] },
|
|
16044
|
-
// `agy --print` (alias -p) is Antigravity's print mode, text output by default.
|
|
16045
|
-
agy: { cli: "agy", argv: () => ["-p"] }
|
|
16046
|
-
};
|
|
16047
|
-
var RUN_TIMEOUT_MS = 5 * 6e4;
|
|
16048
|
-
async function judgeFor(asked, deps = {}) {
|
|
16049
|
-
const options = await available(deps);
|
|
16050
|
-
const chosen = asked ? options.find((o) => o.provider === asked) : options.find((o) => o.ready);
|
|
16051
|
-
if (!chosen?.ready) return { error: refusal(options, asked) };
|
|
16052
|
-
if (chosen.provider === "ollama") {
|
|
16053
|
-
const model = chosen.model;
|
|
16054
|
-
return {
|
|
16055
|
-
provider: "ollama",
|
|
16056
|
-
model,
|
|
16057
|
-
run: async (prompt) => {
|
|
16058
|
-
const res = await fetch(`${OLLAMA_HOST}/api/generate`, {
|
|
16059
|
-
method: "POST",
|
|
16060
|
-
headers: { "content-type": "application/json" },
|
|
16061
|
-
// Non-streaming: we want the whole answer once, and the parser is the same
|
|
16062
|
-
// either way. `temperature: 0` because this is a classification, not prose.
|
|
16063
|
-
body: JSON.stringify({ model, prompt, stream: false, options: { temperature: 0 } }),
|
|
16064
|
-
signal: AbortSignal.timeout(RUN_TIMEOUT_MS)
|
|
16065
|
-
});
|
|
16066
|
-
if (!res.ok) throw new Error(`ollama ${res.status}: ${await res.text()}`);
|
|
16067
|
-
return (await res.json()).response ?? "";
|
|
16068
|
-
}
|
|
16069
|
-
};
|
|
16070
|
-
}
|
|
16071
|
-
const runner = RUNNERS[chosen.provider];
|
|
16072
|
-
if (!runner) return { error: refusal(options, asked) };
|
|
16073
|
-
const bin = which(runner.cli);
|
|
16074
|
-
if (!bin) return { error: refusal(options, asked) };
|
|
16075
|
-
return {
|
|
16076
|
-
provider: chosen.provider,
|
|
16077
|
-
model: null,
|
|
16078
|
-
run: async (prompt) => {
|
|
16079
|
-
const out = spawnSync2(bin, runner.argv(null), {
|
|
16080
|
-
input: prompt,
|
|
16081
|
-
encoding: "utf8",
|
|
16082
|
-
cwd: homedir4(),
|
|
16083
|
-
timeout: RUN_TIMEOUT_MS,
|
|
16084
|
-
maxBuffer: 32 * 1024 * 1024,
|
|
16085
|
-
env: { ...process.env, PATH: [process.env["PATH"], ...probeDirs()].filter(Boolean).join(delimiter2) }
|
|
16086
|
-
});
|
|
16087
|
-
if (out.error) throw new Error(`${runner.cli} failed to run: ${out.error.message}`);
|
|
16088
|
-
if (out.status !== 0 && !out.stdout?.trim())
|
|
16089
|
-
throw new Error(`${runner.cli} exited ${out.status ?? "?"}: ${(out.stderr ?? "").trim().slice(0, 500)}`);
|
|
16090
|
-
return out.stdout ?? "";
|
|
16091
|
-
}
|
|
16092
|
-
};
|
|
16093
|
-
}
|
|
16094
|
-
|
|
16095
|
-
// src/triage/verdict.ts
|
|
16096
|
-
function extractJson(raw) {
|
|
16097
|
-
const fenced = raw.match(/```(?:json)?\s*([\s\S]*?)```/i)?.[1];
|
|
16098
|
-
const body = fenced ?? raw;
|
|
16099
|
-
const start = body.indexOf("[");
|
|
16100
|
-
const end = body.lastIndexOf("]");
|
|
16101
|
-
if (start !== -1 && end > start) return body.slice(start, end + 1);
|
|
16102
|
-
const objStart = body.indexOf("{");
|
|
16103
|
-
const objEnd = body.lastIndexOf("}");
|
|
16104
|
-
if (objStart !== -1 && objEnd > objStart) return body.slice(objStart, objEnd + 1);
|
|
16105
|
-
return null;
|
|
16106
|
-
}
|
|
16107
|
-
var VERDICTS = /* @__PURE__ */ new Set(["keep", "stale", "close", "assign"]);
|
|
16108
|
-
function parseVerdicts(raw, knownIds, knownAgents) {
|
|
16109
|
-
const json3 = extractJson(raw);
|
|
16110
|
-
if (!json3) return { ok: false, error: "no JSON found in the model's answer", raw };
|
|
16111
|
-
let parsed;
|
|
16112
|
-
try {
|
|
16113
|
-
parsed = JSON.parse(json3);
|
|
16114
|
-
} catch (e) {
|
|
16115
|
-
return { ok: false, error: `JSON.parse failed: ${e.message}`, raw };
|
|
16116
|
-
}
|
|
16117
|
-
const rows = Array.isArray(parsed) ? parsed : Array.isArray(parsed?.verdicts) ? parsed.verdicts : typeof parsed?.id === "string" ? [parsed] : null;
|
|
16118
|
-
if (!rows) return { ok: false, error: "the JSON was not an array of verdicts", raw };
|
|
16119
|
-
const verdicts = [];
|
|
16120
|
-
const dropped = [];
|
|
16121
|
-
const seen = /* @__PURE__ */ new Set();
|
|
16122
|
-
for (const row of rows) {
|
|
16123
|
-
const r = row;
|
|
16124
|
-
const id3 = typeof r?.id === "string" ? r.id : "";
|
|
16125
|
-
if (!id3) {
|
|
16126
|
-
dropped.push({ row, reason: "no id" });
|
|
16127
|
-
continue;
|
|
16128
|
-
}
|
|
16129
|
-
if (!knownIds.has(id3)) {
|
|
16130
|
-
dropped.push({ row, reason: "id is not a note in this queue" });
|
|
16131
|
-
continue;
|
|
16132
|
-
}
|
|
16133
|
-
if (seen.has(id3)) {
|
|
16134
|
-
dropped.push({ row, reason: "duplicate verdict for this note" });
|
|
16135
|
-
continue;
|
|
16136
|
-
}
|
|
16137
|
-
const verdict = typeof r?.verdict === "string" ? r.verdict.toLowerCase().trim() : "";
|
|
16138
|
-
if (!VERDICTS.has(verdict)) {
|
|
16139
|
-
dropped.push({ row, reason: `unknown verdict ${JSON.stringify(r?.verdict)}` });
|
|
16140
|
-
continue;
|
|
16141
|
-
}
|
|
16142
|
-
const agent2 = typeof r?.agent === "string" ? r.agent.trim() : null;
|
|
16143
|
-
if (verdict === "assign" && (!agent2 || !knownAgents.has(agent2))) {
|
|
16144
|
-
dropped.push({ row, reason: agent2 ? `no such agent: ${agent2}` : "assign with no agent" });
|
|
16145
|
-
continue;
|
|
16146
|
-
}
|
|
16147
|
-
verdicts.push({
|
|
16148
|
-
id: id3,
|
|
16149
|
-
verdict,
|
|
16150
|
-
agent: verdict === "assign" ? agent2 : null,
|
|
16151
|
-
why: typeof r?.why === "string" ? r.why.trim().slice(0, 200) : null
|
|
16152
|
-
});
|
|
16153
|
-
seen.add(id3);
|
|
16154
|
-
}
|
|
16155
|
-
return { ok: true, verdicts, dropped };
|
|
16156
|
-
}
|
|
16157
|
-
function fillMissing(verdicts, notes) {
|
|
16158
|
-
const said = new Set(verdicts.map((v) => v.id));
|
|
16159
|
-
return [...verdicts, ...notes.filter((n) => !said.has(n.id)).map((n) => ({ id: n.id, verdict: "keep", why: null }))];
|
|
16160
|
-
}
|
|
16161
|
-
function buildPrompt(d) {
|
|
16162
|
-
return [
|
|
16163
|
-
"You are triaging a personal task queue. Today is " + d.today + ".",
|
|
16164
|
-
"",
|
|
16165
|
-
"For EVERY note below, decide one of:",
|
|
16166
|
-
' "keep" \u2014 it is still live work. THE DEFAULT.',
|
|
16167
|
-
' "stale" \u2014 nothing has moved on it for a long time and nothing says it is finished.',
|
|
16168
|
-
' "close" \u2014 there is evidence it is already done or no longer wanted.',
|
|
16169
|
-
' "assign" \u2014 it should be handed to one of the agents listed, and you name which.',
|
|
16170
|
-
"",
|
|
16171
|
-
"The counts and dates below are already computed. Use them; do not recompute them.",
|
|
16172
|
-
"",
|
|
16173
|
-
"AGENTS (assign only to one of these tokens):",
|
|
16174
|
-
JSON.stringify(d.agents, null, 1),
|
|
16175
|
-
"",
|
|
16176
|
-
"NOTES:",
|
|
16177
|
-
JSON.stringify(d.notes, null, 1),
|
|
16178
|
-
"",
|
|
16179
|
-
"Answer with a JSON array and NOTHING else. One object per note:",
|
|
16180
|
-
'[{"id":"<note id>","verdict":"keep|stale|close|assign","agent":"<agent token, only for assign>","why":"<max 12 words>"}]',
|
|
16181
|
-
"",
|
|
16182
|
-
'The "why" is read on a phone under the note title. Evidence first, no ids, no preamble.',
|
|
16183
|
-
'Good: "no movement in 34 days" / "worked 3 notes in this repo this week" / "a later note says this shipped".',
|
|
16184
|
-
"",
|
|
16185
|
-
`When you are not sure, answer "keep". A wrong "keep" costs nothing; a wrong "close" loses the user's work.`
|
|
16186
|
-
].join("\n");
|
|
16187
|
-
}
|
|
16188
|
-
|
|
16189
|
-
// src/triage/triage.ts
|
|
16190
|
-
function toProposal(verdicts, d, meta) {
|
|
16191
|
-
const byId = new Map(d.notes.map((n) => [n.id, n]));
|
|
16192
|
-
const agentById = new Map(d.agents.map((a) => [a.token, a]));
|
|
16193
|
-
const close = [];
|
|
16194
|
-
const stale = [];
|
|
16195
|
-
const assignments = /* @__PURE__ */ new Map();
|
|
16196
|
-
for (const v of verdicts) {
|
|
16197
|
-
const note2 = byId.get(v.id);
|
|
16198
|
-
if (!note2) continue;
|
|
16199
|
-
const item = (why) => ({ noteId: note2.id, title: note2.title, why });
|
|
16200
|
-
if (v.verdict === "close") {
|
|
16201
|
-
close.push(item(staleWhy(note2) ?? v.why ?? "looks finished"));
|
|
16202
|
-
continue;
|
|
16203
|
-
}
|
|
16204
|
-
if (v.verdict === "stale") {
|
|
16205
|
-
stale.push(item(staleWhy(note2) ?? v.why ?? `no movement in ${note2.idleDays} days`));
|
|
16206
|
-
continue;
|
|
16207
|
-
}
|
|
16208
|
-
if (v.verdict !== "assign" || !v.agent) continue;
|
|
16209
|
-
const agent2 = agentById.get(v.agent);
|
|
16210
|
-
if (!agent2) continue;
|
|
16211
|
-
const group = assignments.get(agent2.token) ?? { agent: agent2.token, agentName: agent2.name, notes: [] };
|
|
16212
|
-
group.notes.push(item(affinityWhy(note2, agent2) ?? v.why ?? "best match for this work"));
|
|
16213
|
-
assignments.set(agent2.token, group);
|
|
16214
|
-
}
|
|
16215
|
-
return {
|
|
16216
|
-
provider: meta.provider,
|
|
16217
|
-
model: meta.model,
|
|
16218
|
-
reviewed: d.notes.length,
|
|
16219
|
-
close,
|
|
16220
|
-
stale,
|
|
16221
|
-
assign: [...assignments.values()]
|
|
16222
|
-
};
|
|
16223
|
-
}
|
|
15698
|
+
// src/ago.ts
|
|
16224
15699
|
var plural = (n, one, many = `${one}s`) => `${n} ${n === 1 ? one : many}`;
|
|
16225
|
-
function suggested(run) {
|
|
16226
|
-
return run.close.length + run.stale.length + run.assign.reduce((n, a) => n + a.notes.length, 0);
|
|
16227
|
-
}
|
|
16228
|
-
function renderTable(run) {
|
|
16229
|
-
const lines2 = [];
|
|
16230
|
-
const width = Math.max(
|
|
16231
|
-
24,
|
|
16232
|
-
...[...run.close, ...run.stale, ...run.assign.flatMap((a) => a.notes)].map((n) => Math.min(n.title.length, 48))
|
|
16233
|
-
);
|
|
16234
|
-
const row = (n) => ` ${n.title.slice(0, width).padEnd(width)} ${n.why}`;
|
|
16235
|
-
if (run.close.length) {
|
|
16236
|
-
lines2.push(`Paigy thinks you should CLOSE ${plural(run.close.length, "note")}:`);
|
|
16237
|
-
lines2.push(...run.close.map(row), "");
|
|
16238
|
-
}
|
|
16239
|
-
if (run.stale.length) {
|
|
16240
|
-
lines2.push(`Paigy thinks ${plural(run.stale.length, "note")} ${run.stale.length === 1 ? "has" : "have"} gone STALE:`);
|
|
16241
|
-
lines2.push(...run.stale.map(row), "");
|
|
16242
|
-
}
|
|
16243
|
-
for (const group of run.assign) {
|
|
16244
|
-
lines2.push(`Paigy thinks you should ASSIGN ${plural(group.notes.length, "note")} to ${group.agentName}:`);
|
|
16245
|
-
lines2.push(...group.notes.map(row), "");
|
|
16246
|
-
}
|
|
16247
|
-
const acted = suggested(run);
|
|
16248
|
-
lines2.push(`Reviewed ${plural(run.reviewed, "open note")} \xB7 ${plural(acted, "suggestion")} \xB7 ${run.reviewed - acted} left alone`);
|
|
16249
|
-
return lines2.join("\n");
|
|
16250
|
-
}
|
|
16251
|
-
async function confirm(question) {
|
|
16252
|
-
if (!process.stdin.isTTY) return false;
|
|
16253
|
-
const readline = await import("readline/promises");
|
|
16254
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
16255
|
-
try {
|
|
16256
|
-
return /^(y|yes)$/i.test((await rl.question(`${question} [y/N] `)).trim());
|
|
16257
|
-
} finally {
|
|
16258
|
-
rl.close();
|
|
16259
|
-
}
|
|
16260
|
-
}
|
|
16261
|
-
async function runTriage(opts, deps = {}) {
|
|
16262
|
-
const { log: log2 } = opts;
|
|
16263
|
-
const now = (deps.now ?? Date.now)();
|
|
16264
|
-
const judge = await (deps.judge ?? judgeFor)(opts.via);
|
|
16265
|
-
if ("error" in judge) {
|
|
16266
|
-
log2(judge.error);
|
|
16267
|
-
return 1;
|
|
16268
|
-
}
|
|
16269
|
-
const [{ notes }, { items }] = await Promise.all([
|
|
16270
|
-
(deps.notes ?? listNotes)(),
|
|
16271
|
-
(deps.connections ?? listConnections)()
|
|
16272
|
-
]);
|
|
16273
|
-
const d = dossier(notes, items, now);
|
|
16274
|
-
if (d.notes.length === 0) {
|
|
16275
|
-
log2("Nothing open in the queue \u2014 nothing to triage.");
|
|
16276
|
-
return 0;
|
|
16277
|
-
}
|
|
16278
|
-
if (!opts.json)
|
|
16279
|
-
log2(`Reading ${plural(d.notes.length, "open note")} with ${judge.provider}${judge.model ? ` (${judge.model})` : ""} \u2014 ${PRIVACY[judge.provider]}\u2026`);
|
|
16280
|
-
const raw = await judge.run(buildPrompt(d));
|
|
16281
|
-
const parsed = parseVerdicts(
|
|
16282
|
-
raw,
|
|
16283
|
-
new Set(d.notes.map((n) => n.id)),
|
|
16284
|
-
new Set(d.agents.map((a) => a.token))
|
|
16285
|
-
);
|
|
16286
|
-
if (!parsed.ok) {
|
|
16287
|
-
log2(`The model's answer couldn't be read: ${parsed.error}`);
|
|
16288
|
-
log2("--- raw output ---");
|
|
16289
|
-
log2(parsed.raw.slice(0, 4e3));
|
|
16290
|
-
log2("--- end ---");
|
|
16291
|
-
log2(`Try a different judge: paigy-harness triage --via ${judge.provider === "ollama" ? "claude" : "ollama"}`);
|
|
16292
|
-
return 1;
|
|
16293
|
-
}
|
|
16294
|
-
if (parsed.dropped.length && !opts.json)
|
|
16295
|
-
log2(`(dropped ${plural(parsed.dropped.length, "unusable row")}: ${[...new Set(parsed.dropped.map((x) => x.reason))].join("; ")})`);
|
|
16296
|
-
const run = toProposal(fillMissing(parsed.verdicts, d.notes), d, { provider: judge.provider, model: judge.model });
|
|
16297
|
-
if (opts.json) {
|
|
16298
|
-
log2(JSON.stringify(run, null, 2));
|
|
16299
|
-
return 0;
|
|
16300
|
-
}
|
|
16301
|
-
log2("");
|
|
16302
|
-
log2(renderTable(run));
|
|
16303
|
-
log2("");
|
|
16304
|
-
const proposal = await (deps.submit ?? submitTriage)(run);
|
|
16305
|
-
const acted = suggested(run);
|
|
16306
|
-
log2(acted === 0 ? "Nothing to suggest \u2014 sent, so your phone shows the queue as clean." : `Sent to your phone \u2014 accept it there (${plural(acted, "suggestion")}).`);
|
|
16307
|
-
if (!opts.apply) return 0;
|
|
16308
|
-
const accept = deps.accept ?? ((id3, body) => acceptTriage(id3, body));
|
|
16309
|
-
const groups = [
|
|
16310
|
-
{ body: { group: "close" }, label: `close ${plural(run.close.length, "note")}`, count: run.close.length, preapproved: opts.yes.close },
|
|
16311
|
-
{ body: { group: "stale" }, label: `close ${plural(run.stale.length, "stale note")}`, count: run.stale.length, preapproved: opts.yes.stale },
|
|
16312
|
-
...run.assign.map((a) => ({
|
|
16313
|
-
body: { group: "assign", agent: a.agent },
|
|
16314
|
-
label: `assign ${plural(a.notes.length, "note")} to ${a.agentName}`,
|
|
16315
|
-
count: a.notes.length,
|
|
16316
|
-
preapproved: opts.yes.assign
|
|
16317
|
-
}))
|
|
16318
|
-
];
|
|
16319
|
-
for (const g of groups) {
|
|
16320
|
-
if (g.count === 0) continue;
|
|
16321
|
-
if (!g.preapproved && !await confirm(`${g.label}?`)) {
|
|
16322
|
-
log2(` skipped: ${g.label}`);
|
|
16323
|
-
continue;
|
|
16324
|
-
}
|
|
16325
|
-
const result = await accept(proposal.id, g.body);
|
|
16326
|
-
log2(` \u2713 ${g.label} \u2014 ${result.accepted.length} done${result.failed.length ? `, ${result.failed.length} failed (${result.failed.map((f) => f.reason).join(", ")})` : ""}`);
|
|
16327
|
-
}
|
|
16328
|
-
return 0;
|
|
16329
|
-
}
|
|
16330
|
-
|
|
16331
|
-
// src/broker.ts
|
|
16332
|
-
var plural2 = (n, one, many = `${one}s`) => `${n} ${n === 1 ? one : many}`;
|
|
16333
15700
|
function ago(iso, now) {
|
|
16334
15701
|
const ms = now - Date.parse(iso);
|
|
16335
15702
|
if (!Number.isFinite(ms) || ms < 6e4) return "just now";
|
|
16336
15703
|
const minutes = Math.floor(ms / 6e4);
|
|
16337
|
-
if (minutes < 60) return `${
|
|
15704
|
+
if (minutes < 60) return `${plural(minutes, "minute")} ago`;
|
|
16338
15705
|
const hours = Math.floor(minutes / 60);
|
|
16339
|
-
if (hours < 24) return `${
|
|
16340
|
-
return `${
|
|
16341
|
-
}
|
|
16342
|
-
function standing(queue, proposal, now) {
|
|
16343
|
-
const head2 = queue.length === 0 ? "Nothing open \u2014 the queue is clear" : `Holding ${plural2(queue.length, "open note")}`;
|
|
16344
|
-
if (!proposal) return `${head2} \xB7 no sweep waiting`;
|
|
16345
|
-
const n = suggested(proposal);
|
|
16346
|
-
return n === 0 ? `${head2} \xB7 swept ${ago(proposal.runAt, now)}, nothing to suggest` : `${head2} \xB7 ${plural2(n, "suggestion")} waiting on you, swept ${ago(proposal.runAt, now)}`;
|
|
16347
|
-
}
|
|
16348
|
-
async function brokerView(deps) {
|
|
16349
|
-
const now = (deps.now ?? Date.now)();
|
|
16350
|
-
try {
|
|
16351
|
-
const [{ notes }, { items }, { proposal }] = await Promise.all([deps.notes(), deps.connections(), deps.triage()]);
|
|
16352
|
-
const queue = noteSignals(notes, items, now).map((n) => ({ ...n, why: staleWhy(n) })).sort((a, b) => b.idleDays - a.idleDays || b.ageDays - a.ageDays);
|
|
16353
|
-
return {
|
|
16354
|
-
standing: standing(queue, proposal, now),
|
|
16355
|
-
queue,
|
|
16356
|
-
proposal,
|
|
16357
|
-
voices: Object.fromEntries(items.map((c) => [c.id, c.voice ?? null])),
|
|
16358
|
-
error: null
|
|
16359
|
-
};
|
|
16360
|
-
} catch (e) {
|
|
16361
|
-
return {
|
|
16362
|
-
standing: "Couldn't read the queue just now.",
|
|
16363
|
-
queue: [],
|
|
16364
|
-
proposal: null,
|
|
16365
|
-
voices: {},
|
|
16366
|
-
error: e instanceof Error ? e.message : String(e)
|
|
16367
|
-
};
|
|
16368
|
-
}
|
|
15706
|
+
if (hours < 24) return `${plural(hours, "hour")} ago`;
|
|
15707
|
+
return `${plural(Math.floor(hours / 24), "day")} ago`;
|
|
16369
15708
|
}
|
|
16370
15709
|
|
|
16371
15710
|
// src/workspaces.ts
|
|
16372
15711
|
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readdirSync, readFileSync as readFileSync3, statSync as statSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
16373
15712
|
import { dirname as dirname2, join as join5, resolve } from "path";
|
|
16374
|
-
import { homedir as
|
|
15713
|
+
import { homedir as homedir4 } from "os";
|
|
16375
15714
|
function workspacesFile() {
|
|
16376
|
-
return join5(
|
|
15715
|
+
return join5(homedir4(), ".paigy", "workspaces.json");
|
|
16377
15716
|
}
|
|
16378
15717
|
function listWorkspaces(deps) {
|
|
16379
15718
|
try {
|
|
@@ -16384,7 +15723,7 @@ function listWorkspaces(deps) {
|
|
|
16384
15723
|
}
|
|
16385
15724
|
}
|
|
16386
15725
|
function addWorkspace(dir, deps) {
|
|
16387
|
-
const next = resolve(dir.replace(/^~(?=$|\/)/,
|
|
15726
|
+
const next = resolve(dir.replace(/^~(?=$|\/)/, homedir4()));
|
|
16388
15727
|
const all = listWorkspaces(deps);
|
|
16389
15728
|
if (!all.includes(next)) {
|
|
16390
15729
|
all.push(next);
|
|
@@ -16400,12 +15739,12 @@ function removeWorkspace(dir, deps) {
|
|
|
16400
15739
|
return all;
|
|
16401
15740
|
}
|
|
16402
15741
|
function allowed(cwd, deps) {
|
|
16403
|
-
const target = resolve(cwd.replace(/^~(?=$|\/)/,
|
|
15742
|
+
const target = resolve(cwd.replace(/^~(?=$|\/)/, homedir4()));
|
|
16404
15743
|
return listWorkspaces(deps).some((w) => target === w || target.startsWith(`${w}/`));
|
|
16405
15744
|
}
|
|
16406
15745
|
function resolveWakeDir(pinned, deps) {
|
|
16407
15746
|
if (pinned && allowed(pinned, deps)) {
|
|
16408
|
-
const dir = resolve(pinned.replace(/^~(?=$|\/)/,
|
|
15747
|
+
const dir = resolve(pinned.replace(/^~(?=$|\/)/, homedir4()));
|
|
16409
15748
|
if ((deps.exists ?? existsSync3)(dir)) return dir;
|
|
16410
15749
|
}
|
|
16411
15750
|
return listWorkspaces(deps)[0];
|
|
@@ -16445,7 +15784,7 @@ function listRepos(deps) {
|
|
|
16445
15784
|
// src/holder.ts
|
|
16446
15785
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
16447
15786
|
import { readFileSync as readFileSync4, readdirSync as readdirSync2 } from "fs";
|
|
16448
|
-
import { homedir as
|
|
15787
|
+
import { homedir as homedir5 } from "os";
|
|
16449
15788
|
import { join as join6 } from "path";
|
|
16450
15789
|
function isAlive(pid) {
|
|
16451
15790
|
try {
|
|
@@ -16484,7 +15823,7 @@ function proven(pid, procStart, deps) {
|
|
|
16484
15823
|
});
|
|
16485
15824
|
}
|
|
16486
15825
|
function localHolderPids(deps = {}) {
|
|
16487
|
-
const dir = deps.dir ?? join6(process.env.CLAUDE_CONFIG_DIR?.trim() || join6(
|
|
15826
|
+
const dir = deps.dir ?? join6(process.env.CLAUDE_CONFIG_DIR?.trim() || join6(homedir5(), ".claude"), "sessions");
|
|
16488
15827
|
const list = deps.list ?? ((d) => readdirSync2(d));
|
|
16489
15828
|
const read3 = deps.read ?? ((f) => readFileSync4(f, "utf8"));
|
|
16490
15829
|
const check = { alive: deps.alive ?? isAlive, started: deps.started ?? psStart };
|
|
@@ -16533,7 +15872,7 @@ function descendsFrom(pid, ancestor, parent = psParent) {
|
|
|
16533
15872
|
return false;
|
|
16534
15873
|
}
|
|
16535
15874
|
function localHolders(deps = {}) {
|
|
16536
|
-
const dir = deps.dir ?? join6(process.env.CLAUDE_CONFIG_DIR?.trim() || join6(
|
|
15875
|
+
const dir = deps.dir ?? join6(process.env.CLAUDE_CONFIG_DIR?.trim() || join6(homedir5(), ".claude"), "sessions");
|
|
16537
15876
|
const list = deps.list ?? ((d) => readdirSync2(d));
|
|
16538
15877
|
const read3 = deps.read ?? ((f) => readFileSync4(f, "utf8"));
|
|
16539
15878
|
const check = { alive: deps.alive ?? isAlive, started: deps.started ?? psStart };
|
|
@@ -16626,7 +15965,7 @@ async function endSession(end, deps) {
|
|
|
16626
15965
|
|
|
16627
15966
|
// src/owner.ts
|
|
16628
15967
|
import { closeSync as closeSync2, openSync as openSync2, readSync, readdirSync as readdirSync3 } from "fs";
|
|
16629
|
-
import { homedir as
|
|
15968
|
+
import { homedir as homedir6 } from "os";
|
|
16630
15969
|
import { join as join7 } from "path";
|
|
16631
15970
|
var HEAD_BYTES = 64e3;
|
|
16632
15971
|
function readHead(file) {
|
|
@@ -16643,7 +15982,7 @@ function ownerSession(slot, deps = {}) {
|
|
|
16643
15982
|
const m = /^session:([0-9a-f]{8})$/.exec(slot);
|
|
16644
15983
|
if (!m) return null;
|
|
16645
15984
|
const prefix = m[1];
|
|
16646
|
-
const root = deps.projectsDir ?? join7(
|
|
15985
|
+
const root = deps.projectsDir ?? join7(homedir6(), ".claude", "projects");
|
|
16647
15986
|
const list = deps.list ?? ((dir) => readdirSync3(dir));
|
|
16648
15987
|
const head2 = deps.head ?? readHead;
|
|
16649
15988
|
let folders;
|
|
@@ -16706,8 +16045,8 @@ import { randomUUID as randomUUID4 } from "crypto";
|
|
|
16706
16045
|
// src/harness/session.ts
|
|
16707
16046
|
import { spawn } from "child_process";
|
|
16708
16047
|
import { existsSync as existsSync4, readFileSync as readFileSync5 } from "fs";
|
|
16709
|
-
import { homedir as
|
|
16710
|
-
import { resolve as resolve2, delimiter as
|
|
16048
|
+
import { homedir as homedir8 } from "os";
|
|
16049
|
+
import { resolve as resolve2, delimiter as delimiter2, join as join9 } from "path";
|
|
16711
16050
|
|
|
16712
16051
|
// src/harness/acp.ts
|
|
16713
16052
|
var none = { events: [], writes: [] };
|
|
@@ -16984,7 +16323,7 @@ var frame2 = (body) => JSON.stringify({ jsonrpc: "2.0", ...body });
|
|
|
16984
16323
|
// src/paigy/inbox.ts
|
|
16985
16324
|
import { mkdirSync as mkdirSync3, readdirSync as readdirSync4, renameSync, rmSync as rmSync2, statSync as statSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
16986
16325
|
import { join as join8 } from "path";
|
|
16987
|
-
import { homedir as
|
|
16326
|
+
import { homedir as homedir7 } from "os";
|
|
16988
16327
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
16989
16328
|
var MIDTURN_HOOK = `d="$PAIGY_INBOX"; [ -n "$d" ] || { cat >/dev/null; exit 0; }; set -- "$d"/*.note; [ -e "$1" ] || { cat >/dev/null; exit 0; }; grep '"agent_id"' >/dev/null && exit 0; mkdir -p "$d/taken"; o=""; for f in "$@"; do n=\${f##*/}; mv "$f" "$d/taken/$n" 2>/dev/null || continue; touch "$d/taken/$n"; o="$o\${o:+\\n\\n}$(cat "$d/taken/$n")"; done; [ -n "$o" ] || exit 0; printf '{"hookSpecificOutput":{"hookEventName":"PostToolUse","additionalContext":"%s"}}' "$o"`;
|
|
16990
16329
|
var missing = (error) => error.code === "ENOENT";
|
|
@@ -17027,7 +16366,7 @@ function openInbox(dir) {
|
|
|
17027
16366
|
}
|
|
17028
16367
|
};
|
|
17029
16368
|
}
|
|
17030
|
-
var INBOX_ROOT = join8(
|
|
16369
|
+
var INBOX_ROOT = join8(homedir7(), ".paigy", "inbox");
|
|
17031
16370
|
var alive = (pid) => {
|
|
17032
16371
|
try {
|
|
17033
16372
|
process.kill(pid, 0);
|
|
@@ -17074,14 +16413,14 @@ function splitLines(buffer, chunk) {
|
|
|
17074
16413
|
return { lines: parts.filter((l) => l.trim()), rest };
|
|
17075
16414
|
}
|
|
17076
16415
|
function startSession(opts) {
|
|
17077
|
-
const cwd = resolve2(opts.cwd.replace(/^~(?=$|\/)/,
|
|
16416
|
+
const cwd = resolve2(opts.cwd.replace(/^~(?=$|\/)/, homedir8()));
|
|
17078
16417
|
if (!existsSync4(cwd)) {
|
|
17079
16418
|
queueMicrotask(() => opts.onEvent({ kind: "error", message: `workspace does not exist: ${cwd}` }));
|
|
17080
16419
|
}
|
|
17081
16420
|
const codexConfig = opts.harness === "codex" ? JSON.parse(process.env.CODEX_CONFIG || "{}") : null;
|
|
17082
16421
|
if (codexConfig) {
|
|
17083
16422
|
const key3 = "mcp_servers.paigy.env_vars";
|
|
17084
|
-
const configPath = join9(process.env.CODEX_HOME || join9(
|
|
16423
|
+
const configPath = join9(process.env.CODEX_HOME || join9(homedir8(), ".codex"), "config.toml");
|
|
17085
16424
|
const saved = existsSync4(configPath) ? readFileSync5(configPath, "utf8") : "";
|
|
17086
16425
|
const registered = /\[mcp_servers\.paigy\][\s\S]*?(?=\n\[|$)/.exec(saved)?.[0];
|
|
17087
16426
|
const configured = codexConfig["mcp_servers.paigy.command"] || codexConfig["mcp_servers.paigy.url"] || codexConfig.mcp_servers?.paigy?.command || codexConfig.mcp_servers?.paigy?.url;
|
|
@@ -17101,7 +16440,7 @@ function startSession(opts) {
|
|
|
17101
16440
|
// PATH knows neither — live catch 2026-08-04: the first slot-wake ENOENT'd).
|
|
17102
16441
|
env: {
|
|
17103
16442
|
...process.env,
|
|
17104
|
-
PATH: [process.env.PATH, ...probeDirs()].filter(Boolean).join(
|
|
16443
|
+
PATH: [process.env.PATH, ...probeDirs()].filter(Boolean).join(delimiter2),
|
|
17105
16444
|
...opts.token ? { PAIGY_TOKEN: opts.token } : {},
|
|
17106
16445
|
...opts.instance ? { PAIGY_INSTANCE_ID: opts.instance } : {},
|
|
17107
16446
|
...codexConfig ? { CODEX_CONFIG: JSON.stringify(codexConfig) } : {},
|
|
@@ -17336,9 +16675,9 @@ function railFor(opts) {
|
|
|
17336
16675
|
next: async () => {
|
|
17337
16676
|
const waiting = await receive(opts);
|
|
17338
16677
|
const goalId = waiting.claimable?.goalId ?? waiting.review?.[0]?.goalId;
|
|
17339
|
-
return goalId ?
|
|
16678
|
+
return goalId ? getGoals([goalId], opts).then(([goal]) => goal) : { state: "idle", message: "" };
|
|
17340
16679
|
},
|
|
17341
|
-
read: (goalId) =>
|
|
16680
|
+
read: (goalId) => getGoals([goalId], opts).then(([goal]) => goal),
|
|
17342
16681
|
complete: async (goalId, revision) => {
|
|
17343
16682
|
const done = await manageGoals({ changes: [{ kind: "state", goalId, state: "completed", ...revision !== void 0 ? { revision } : {} }] }, opts);
|
|
17344
16683
|
const result = done.results[0];
|
|
@@ -17521,7 +16860,7 @@ var BEAT_REPORT_MS = 10 * 6e4;
|
|
|
17521
16860
|
var ended = (state) => state === "done" || state === "cancelled";
|
|
17522
16861
|
function unheard(brief, state, since = null, after = -1, reviewSince) {
|
|
17523
16862
|
const seen = state.seen ??= /* @__PURE__ */ new Set();
|
|
17524
|
-
const
|
|
16863
|
+
const words = [];
|
|
17525
16864
|
const pendingAt = reviewSince ? Date.parse(reviewSince) : null;
|
|
17526
16865
|
for (const [i, e] of (brief.entries ?? []).entries()) {
|
|
17527
16866
|
if (seen.has(e.entryId) || !e.authorParticipant.startsWith("human:")) continue;
|
|
@@ -17537,15 +16876,15 @@ function unheard(brief, state, since = null, after = -1, reviewSince) {
|
|
|
17537
16876
|
seen.add(e.entryId);
|
|
17538
16877
|
if (e.aboutId && seen.has(e.aboutId) && brief.answers?.some((a) => a.requestEntryId === e.aboutId)) continue;
|
|
17539
16878
|
const text = entryWords(e).trim();
|
|
17540
|
-
if (text)
|
|
16879
|
+
if (text) words.push(text);
|
|
17541
16880
|
}
|
|
17542
16881
|
if (pendingAt !== null) for (const answer of brief.answers ?? []) {
|
|
17543
16882
|
const key3 = `answer:${answer.decisionNeedId}`;
|
|
17544
16883
|
if (seen.has(key3) || Date.parse(answer.acceptedAt) < pendingAt) continue;
|
|
17545
16884
|
seen.add(key3);
|
|
17546
|
-
|
|
16885
|
+
words.push(`Answer to question ${answer.decisionNeedId}: ${JSON.stringify(answer.result)}`);
|
|
17547
16886
|
}
|
|
17548
|
-
return
|
|
16887
|
+
return words;
|
|
17549
16888
|
}
|
|
17550
16889
|
function sinceTheWork(brief) {
|
|
17551
16890
|
const entries = brief.entries ?? [];
|
|
@@ -17666,7 +17005,7 @@ async function note(state, rail, held, inbox, how) {
|
|
|
17666
17005
|
const { heard } = await gather(state, rail, held);
|
|
17667
17006
|
const said = heard.filter(says);
|
|
17668
17007
|
if (!said.length) return 0;
|
|
17669
|
-
const
|
|
17008
|
+
const words = said.map(block).join("\n\n");
|
|
17670
17009
|
const goals = said.map((h) => h.goalId);
|
|
17671
17010
|
const assigned = given(said);
|
|
17672
17011
|
if (how.tally) how.tally.announced += assigned.length;
|
|
@@ -17674,13 +17013,13 @@ async function note(state, rail, held, inbox, how) {
|
|
|
17674
17013
|
try {
|
|
17675
17014
|
name = inbox.put(`${NOTED}
|
|
17676
17015
|
|
|
17677
|
-
${
|
|
17016
|
+
${words}`);
|
|
17678
17017
|
how.log?.(`\u25B9 noted mid-turn: ${short(goals)}`);
|
|
17679
17018
|
if (assigned.length) how.log?.(`\u25B8 announced assigned ${short(assigned)} (via hook)`);
|
|
17680
17019
|
} catch (error) {
|
|
17681
17020
|
how.log?.(`\u26A0 could not note mid-turn (${error.message}): ${short(goals)} wait for the turn's end`);
|
|
17682
17021
|
}
|
|
17683
|
-
(state.noted ??= []).push({ name, goals, words
|
|
17022
|
+
(state.noted ??= []).push({ name, goals, words, at: how.now(), cause: how.cause === "wake" ? "wake" : "read" });
|
|
17684
17023
|
return 1;
|
|
17685
17024
|
}
|
|
17686
17025
|
function receipts(state, inbox, log2) {
|
|
@@ -17764,7 +17103,7 @@ async function look(session, state, rail, opts) {
|
|
|
17764
17103
|
const outcome2 = brief2.outcome?.trim() ?? "";
|
|
17765
17104
|
const said = (pendingWords ?? (brief2.entries ?? []).map((e) => entryWords(e).trim())).filter((w) => w && w.toLowerCase() !== outcome2.toLowerCase());
|
|
17766
17105
|
const left = brief2.earlier ? brief2.earlier.omitted + brief2.earlier.shortened : 0;
|
|
17767
|
-
const earlier = left ? `(${left} earlier entries on this Goal are left out or shortened;
|
|
17106
|
+
const earlier = left ? `(${left} earlier entries on this Goal are left out or shortened; get_goals with history: true reads them in full.)` : "";
|
|
17768
17107
|
const fullText = [outcome2, earlier, ...said].filter(Boolean).join("\n\n");
|
|
17769
17108
|
send2(session, state, [HARNESS_INSTRUCTION, named(brief2.goalId, void 0, brief2), fullText].filter(Boolean).join("\n\n"));
|
|
17770
17109
|
if (ended(brief2.state)) await acknowledge2(brief2.goalId, brief2, [], state, rail, opts?.log);
|
|
@@ -17797,16 +17136,16 @@ async function look(session, state, rail, opts) {
|
|
|
17797
17136
|
}
|
|
17798
17137
|
function send2(session, state, text) {
|
|
17799
17138
|
state.resting = false;
|
|
17800
|
-
let
|
|
17139
|
+
let words = text;
|
|
17801
17140
|
if (state.carryOn) {
|
|
17802
17141
|
state.carryOn = false;
|
|
17803
17142
|
state.carriedOn = true;
|
|
17804
17143
|
const owed = state.owed;
|
|
17805
17144
|
state.owed = void 0;
|
|
17806
|
-
|
|
17145
|
+
words = [owed, text].filter(Boolean).join("\n\n");
|
|
17807
17146
|
text = [CARRY_ON, ...owed ? [OWED, owed] : [], text].filter(Boolean).join("\n\n");
|
|
17808
17147
|
}
|
|
17809
|
-
state.typed =
|
|
17148
|
+
state.typed = words || void 0;
|
|
17810
17149
|
session.send(text);
|
|
17811
17150
|
}
|
|
17812
17151
|
async function pump(session, state, rail, opts) {
|
|
@@ -18039,7 +17378,7 @@ function selfUpdater(deps) {
|
|
|
18039
17378
|
}
|
|
18040
17379
|
|
|
18041
17380
|
// src/host.ts
|
|
18042
|
-
var HOST_FILE = join11(
|
|
17381
|
+
var HOST_FILE = join11(homedir9(), ".paigy", "host.json");
|
|
18043
17382
|
function wakeHarness(slot, saved, claudeTranscript) {
|
|
18044
17383
|
if (slot === "Desktop") return void 0;
|
|
18045
17384
|
if (saved !== null) return saved === "agy" || saved === "codex" || saved === "claude" ? saved : void 0;
|
|
@@ -18631,79 +17970,13 @@ ipcMain.handle("paigy:agent", async (_e, name) => {
|
|
|
18631
17970
|
});
|
|
18632
17971
|
ipcMain.handle("paigy:agent-log", (_e, name) => {
|
|
18633
17972
|
try {
|
|
18634
|
-
const all = readFileSync7(join12(
|
|
17973
|
+
const all = readFileSync7(join12(homedir10(), ".paigy", "host.log"), "utf8").split("\n");
|
|
18635
17974
|
const mine = all.filter((l) => l.includes(`[${name}]`) || /[▶✖◉⚠]/.test(l) && l.includes(name));
|
|
18636
17975
|
return mine.slice(-500).join("\n");
|
|
18637
17976
|
} catch {
|
|
18638
17977
|
return "";
|
|
18639
17978
|
}
|
|
18640
17979
|
});
|
|
18641
|
-
ipcMain.handle("paigy:broker", async () => {
|
|
18642
|
-
const token = deviceToken();
|
|
18643
|
-
if (!token)
|
|
18644
|
-
return {
|
|
18645
|
-
standing: "Not paired yet \u2014 scan the code above and Paigy's queue lands here.",
|
|
18646
|
-
queue: [],
|
|
18647
|
-
proposal: null,
|
|
18648
|
-
voices: {},
|
|
18649
|
-
error: "unpaired"
|
|
18650
|
-
};
|
|
18651
|
-
return brokerView({
|
|
18652
|
-
notes: () => listNotes({ token }),
|
|
18653
|
-
connections: () => listConnections({ token }),
|
|
18654
|
-
triage: () => getTriage({ token })
|
|
18655
|
-
});
|
|
18656
|
-
});
|
|
18657
|
-
var sweeping = false;
|
|
18658
|
-
ipcMain.handle("paigy:triage-run", async () => {
|
|
18659
|
-
const token = deviceToken();
|
|
18660
|
-
if (!token) return { code: 1, output: "This Mac isn't paired yet \u2014 scan the pairing code first." };
|
|
18661
|
-
if (sweeping) return { code: 1, output: "A sweep is already running." };
|
|
18662
|
-
sweeping = true;
|
|
18663
|
-
const lines2 = [];
|
|
18664
|
-
const say = (line) => {
|
|
18665
|
-
for (const l of line.split("\n")) {
|
|
18666
|
-
lines2.push(l);
|
|
18667
|
-
log(l);
|
|
18668
|
-
}
|
|
18669
|
-
};
|
|
18670
|
-
try {
|
|
18671
|
-
const code = await runTriage(
|
|
18672
|
-
{ via: null, json: false, apply: false, yes: { close: false, stale: false, assign: false }, log: say },
|
|
18673
|
-
{
|
|
18674
|
-
notes: () => listNotes({ token }),
|
|
18675
|
-
connections: () => listConnections({ token }),
|
|
18676
|
-
submit: (run) => submitTriage(run, { token })
|
|
18677
|
-
}
|
|
18678
|
-
);
|
|
18679
|
-
return { code, output: lines2.join("\n") };
|
|
18680
|
-
} catch (e) {
|
|
18681
|
-
const message = e instanceof Error ? e.message : String(e);
|
|
18682
|
-
log(`\u26A0 the sweep failed: ${message}`);
|
|
18683
|
-
return { code: 1, output: [...lines2, `\u26A0 the sweep failed: ${message}`].join("\n") };
|
|
18684
|
-
} finally {
|
|
18685
|
-
sweeping = false;
|
|
18686
|
-
}
|
|
18687
|
-
});
|
|
18688
|
-
ipcMain.handle("paigy:triage-accept", async (_e, opts) => {
|
|
18689
|
-
const token = deviceToken();
|
|
18690
|
-
if (!token) return { ok: false, error: "This Mac isn't paired yet." };
|
|
18691
|
-
try {
|
|
18692
|
-
return { ok: true, result: await acceptTriage(opts.id, opts.body, { token }) };
|
|
18693
|
-
} catch (e) {
|
|
18694
|
-
return { ok: false, error: e instanceof Error ? e.message : String(e) };
|
|
18695
|
-
}
|
|
18696
|
-
});
|
|
18697
|
-
ipcMain.handle("paigy:triage-dismiss", async (_e, id3) => {
|
|
18698
|
-
const token = deviceToken();
|
|
18699
|
-
if (!token) return { ok: false, error: "This Mac isn't paired yet." };
|
|
18700
|
-
try {
|
|
18701
|
-
await dismissTriage(id3, { token });
|
|
18702
|
-
return { ok: true };
|
|
18703
|
-
} catch (e) {
|
|
18704
|
-
return { ok: false, error: e instanceof Error ? e.message : String(e) };
|
|
18705
|
-
}
|
|
18706
|
-
});
|
|
18707
17980
|
ipcMain.handle("paigy:workspaces", () => listWorkspaces(wsDeps));
|
|
18708
17981
|
ipcMain.handle("paigy:workspace-add", async () => {
|
|
18709
17982
|
const picked = await dialog.showOpenDialog({ properties: ["openDirectory", "createDirectory"] });
|