toolroll 0.9.18 → 0.9.20
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/CHANGELOG.md +18 -0
- package/dist/browser/workspace.js +15 -15
- package/dist/browser-workspace.d.ts +2 -0
- package/dist/chat-delivery.js +10 -2
- package/dist/discord-chat.js +8 -3
- package/dist/flow-triggers.d.ts +7 -0
- package/dist/flow-triggers.js +17 -5
- package/dist/flows-ui.js +2 -2
- package/dist/invoke.d.ts +3 -0
- package/dist/invoke.js +1 -0
- package/dist/plane-review.d.ts +15 -0
- package/dist/plane-review.js +62 -20
- package/dist/provider.d.ts +8 -0
- package/dist/provider.js +5 -1
- package/dist/result-shots.d.ts +2 -0
- package/dist/result-shots.js +5 -0
- package/dist/scout-report.d.ts +93 -0
- package/dist/scout-report.js +63 -0
- package/dist/scout.js +163 -61
- package/dist/slack-chat.js +8 -3
- package/dist/telegram.js +17 -5
- package/package.json +1 -1
|
@@ -912,6 +912,8 @@ export type BrowserFlowTrigger = {
|
|
|
912
912
|
status: string | null;
|
|
913
913
|
statusAt: string | null;
|
|
914
914
|
failing: boolean;
|
|
915
|
+
/** Where the status points a person (a plane review's long waits: Needs you). */
|
|
916
|
+
statusLink: BrowserLink | null;
|
|
915
917
|
button: {
|
|
916
918
|
label: string;
|
|
917
919
|
questions: string[];
|
package/dist/chat-delivery.js
CHANGED
|
@@ -677,13 +677,21 @@ function planResultShots(options, binding, notification, repos, id, now) {
|
|
|
677
677
|
if (plan.kind === "none" || notification.taskId === null || notification.run === null)
|
|
678
678
|
return;
|
|
679
679
|
const where = { task: notification.taskId, run: notification.run };
|
|
680
|
-
// The result's message here: its finished-work batch (quiet),
|
|
680
|
+
// The result's message here: its finished-work batch (quiet), its progress card (every step), or the result's own
|
|
681
|
+
// notice. The transport threads the screenshots under it once it is posted, and holds them until then.
|
|
681
682
|
const follows = (() => {
|
|
682
683
|
const batch = notification.taskRef === null ? null : store.chatBatchMessageFor(`${state.channel}:${binding.id}`, notification.taskRef, notification.run);
|
|
683
684
|
if (batch !== null)
|
|
684
685
|
return Number(batch);
|
|
685
686
|
const card = state.prepare("SELECT part FROM chat_progress WHERE binding=? AND run=?").get(binding.id, notification.run);
|
|
686
|
-
|
|
687
|
+
if (card !== undefined)
|
|
688
|
+
return Number(card.part);
|
|
689
|
+
for (const one of store.handle.prepare("SELECT id FROM notification WHERE source_run=? AND recipient IS NULL AND id<? ORDER BY id DESC").all(notification.run, notification.id)) {
|
|
690
|
+
const part = state.prepare("SELECT id FROM chat_part WHERE event=? ORDER BY id LIMIT 1").get(chatHash(`${state.channel}:notice:${binding.id}:${Number(one["id"])}`));
|
|
691
|
+
if (part !== undefined)
|
|
692
|
+
return Number(part.id);
|
|
693
|
+
}
|
|
694
|
+
return null;
|
|
687
695
|
})();
|
|
688
696
|
const image = (artifact, sha256) => ({ taskId: where.task, run: where.run, artifact, sha256 });
|
|
689
697
|
const parts = plan.kind === "line" ? [{ text: plan.text, ...where }]
|
package/dist/discord-chat.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { chatQuestionButtons } from "./teammate-question.js";
|
|
3
3
|
import { chatAskButtons } from "./chat-ask.js";
|
|
4
4
|
import { chatFlowButtons } from "./chat-flow.js";
|
|
5
|
-
import { refuseResultShots, resultShotsPruned } from "./result-shots.js";
|
|
5
|
+
import { refuseResultShots, resultShotsPruned, shotWaitsUntil } from "./result-shots.js";
|
|
6
6
|
import { channelInbox } from "./chat-inbox.js";
|
|
7
7
|
import { roomCommand } from "./chat-rooms.js";
|
|
8
8
|
import { ChatState, ChatDeliveryError, chatHash, } from "./chat-delivery-state.js";
|
|
@@ -210,8 +210,13 @@ export async function deliverDiscordPart(options) {
|
|
|
210
210
|
state.prepare("UPDATE chat_part SET state='dropped',problem='Removed by retention' WHERE id=?").run(row.id);
|
|
211
211
|
return true;
|
|
212
212
|
}
|
|
213
|
-
// A screenshot sent with a result replies to that result's message once it is placed.
|
|
214
|
-
const
|
|
213
|
+
// A screenshot sent with a result replies to that result's message once it is placed, and waits until then.
|
|
214
|
+
const result = content.shot?.follows == null ? undefined : state.prepare("SELECT message,state,next_at FROM chat_part WHERE id=?").get(content.shot.follows);
|
|
215
|
+
if (result !== undefined && !discordId(result["message"]) && result["state"] === "pending") {
|
|
216
|
+
state.prepare("UPDATE chat_part SET next_at=? WHERE id=?").run(shotWaitsUntil(result["next_at"], now), row.id);
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
219
|
+
const follows = result?.["message"];
|
|
215
220
|
const thread = discordId(follows) ? String(follows) : event.thread;
|
|
216
221
|
if (content.image) {
|
|
217
222
|
const image = verifyResultImage(store, options.evidenceRoot, repos, content.image);
|
package/dist/flow-triggers.d.ts
CHANGED
|
@@ -199,6 +199,13 @@ export declare function firePlaneReview(store: Store, trigger: FlowTriggerRow, c
|
|
|
199
199
|
joined: number;
|
|
200
200
|
said: string;
|
|
201
201
|
}>;
|
|
202
|
+
/** Where a person sees everything that waits on them. */
|
|
203
|
+
export declare const NEEDS_YOU_HREF = "/work?view=needs-you";
|
|
204
|
+
/** A plane review whose summary mentions long waits on a person links to Needs you. */
|
|
205
|
+
export declare function planeStatusLink(config: TriggerConfig | null, status: string | null): {
|
|
206
|
+
label: string;
|
|
207
|
+
href: string;
|
|
208
|
+
} | null;
|
|
202
209
|
/** v96: a schedule (a teammate's routine) makes its card now, once per press, and keeps its own times. */
|
|
203
210
|
export declare function runScheduleNow(store: Store, trigger: FlowTriggerRow, actor: string, now: Date): {
|
|
204
211
|
ok: true;
|
package/dist/flow-triggers.js
CHANGED
|
@@ -16,7 +16,9 @@
|
|
|
16
16
|
* - plane-review — every day at a set time (07:30 in the computer's time zone unless it says),
|
|
17
17
|
* the plane's last 24 hours read straight from the store (plane-review.ts): one card
|
|
18
18
|
* per distinct problem worth fixing, and the same problem on a later day joins its
|
|
19
|
-
* card while that card is still open. A clean day makes nothing.
|
|
19
|
+
* card while that card is still open. A clean day makes nothing. Waits only a person
|
|
20
|
+
* can end make no card; ones over three days are mentioned once in its summary line,
|
|
21
|
+
* linked to Needs you. Its row is kept as a
|
|
20
22
|
* 'schedule' (the table's kinds are fixed); its settings say what it is.
|
|
21
23
|
*
|
|
22
24
|
* GitHub and Linear are checked by the worker's pass — `gh` and one HTTPS
|
|
@@ -615,10 +617,14 @@ export async function firePlaneReview(store, trigger, config, now, scheduled) {
|
|
|
615
617
|
if (flow === null || flow.state !== "active")
|
|
616
618
|
return done("The flow isn't active.");
|
|
617
619
|
// Loaded when a review first runs: the reader reaches the integrations and task status, which reach back to these triggers.
|
|
618
|
-
const { problemCard, problemText, reviewPlane } = await import("./plane-review.js");
|
|
620
|
+
const { longPersonWaits, longWaitWords, problemCard, problemText, reviewPlane } = await import("./plane-review.js");
|
|
619
621
|
// Only the projects the flow's owner may see: a flow's cards are read by everyone on its project.
|
|
620
622
|
const owner = store.accountOf(flow.owner);
|
|
621
|
-
const
|
|
623
|
+
const canSee = (repo) => owner === null || (repo !== null && store.accountCanAccess(flow.owner, repo)) || (repo === null && owner.projects === null);
|
|
624
|
+
const problems = reviewPlane(store, now, canSee);
|
|
625
|
+
// Long waits on a person are said here once, linked to Needs you (planeStatusLink), and file nothing.
|
|
626
|
+
const waited = longWaitWords(longPersonWaits(store, now, canSee));
|
|
627
|
+
const said = (words) => waited === null ? words : `${words} ${waited}`;
|
|
622
628
|
const timeZone = schedule !== null && schedule.kind !== "every" ? schedule.timezone ?? "UTC" : "UTC";
|
|
623
629
|
const day = new Intl.DateTimeFormat("en-CA", { year: "numeric", month: "2-digit", day: "2-digit", timeZone }).format(now);
|
|
624
630
|
const dayWords = new Intl.DateTimeFormat("en-US", { month: "short", day: "numeric", timeZone }).format(now);
|
|
@@ -646,10 +652,16 @@ export async function firePlaneReview(store, trigger, config, now, scheduled) {
|
|
|
646
652
|
skipped.push(made.note);
|
|
647
653
|
}
|
|
648
654
|
if (problems.length === 0)
|
|
649
|
-
return done("A clean day: nothing to fix.");
|
|
655
|
+
return done(said("A clean day: nothing to fix."));
|
|
650
656
|
const parts = [added > 0 ? `Added ${plural(added, "card")}.` : null, joined > 0 ? `${plural(joined, "problem")} came back and joined ${joined === 1 ? "its card" : "their cards"}.` : null,
|
|
651
657
|
skipped.length > 0 ? `Left out ${plural(skipped.length, "problem")}: ${skipped[0]}.` : null].filter(one => one !== null);
|
|
652
|
-
return done(parts.length === 0 ? "Nothing new since this morning's review." : parts.join(" "), added, joined);
|
|
658
|
+
return done(said(parts.length === 0 ? "Nothing new since this morning's review." : parts.join(" ")), added, joined);
|
|
659
|
+
}
|
|
660
|
+
/** Where a person sees everything that waits on them. */
|
|
661
|
+
export const NEEDS_YOU_HREF = "/work?view=needs-you";
|
|
662
|
+
/** A plane review whose summary mentions long waits on a person links to Needs you. */
|
|
663
|
+
export function planeStatusLink(config, status) {
|
|
664
|
+
return config?.kind === "plane-review" && status !== null && /waited over \d+ days for you\.$/.test(status) ? { label: "Needs you", href: NEEDS_YOU_HREF } : null;
|
|
653
665
|
}
|
|
654
666
|
/** v96: a schedule (a teammate's routine) makes its card now, once per press, and keeps its own times. */
|
|
655
667
|
export function runScheduleNow(store, trigger, actor, now) {
|
package/dist/flows-ui.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { draftFor, flowDefinitionOf } from "./flow-engine.js";
|
|
2
|
-
import { describeTrigger, triggerHeadline, FLOW_TRIGGER_KINDS, FLOW_TRIGGER_WORDS, githubRepoOf, HOOK_PATH, hookReady, readHooksBase, readLinearKey, takesDeliveries, triggerConfigOf } from "./flow-triggers.js";
|
|
2
|
+
import { describeTrigger, triggerHeadline, FLOW_TRIGGER_KINDS, FLOW_TRIGGER_WORDS, githubRepoOf, HOOK_PATH, hookReady, planeStatusLink, readHooksBase, readLinearKey, takesDeliveries, triggerConfigOf } from "./flow-triggers.js";
|
|
3
3
|
import { deciderOf, FLOW_COLORS, FLOW_KIND_WORDS, FLOW_STAGE_KINDS } from "./flows.js";
|
|
4
4
|
import { flowInsights, troubleWords } from "./flow-insights.js";
|
|
5
5
|
import { parseSortDecision, sortChip } from "./flow-sort.js";
|
|
@@ -152,7 +152,7 @@ export function flowView(store, flow, viewer, selectedCard, setup = { dir: null,
|
|
|
152
152
|
return {
|
|
153
153
|
id: trigger.id, kind: trigger.kind, words: config === null ? "This trigger can't be read." : describeTrigger(config, store),
|
|
154
154
|
...(config === null ? { name: "Trigger", detail: "Can't be read" } : triggerHeadline(config, store)),
|
|
155
|
-
zone: title(config?.zone ?? definition?.start ?? ""), zoneId: config?.zone ?? definition?.start ?? "", state: trigger.state, status: trigger.lastOutcome, statusAt: trigger.lastAt, failing: trigger.failures > 0,
|
|
155
|
+
zone: title(config?.zone ?? definition?.start ?? ""), zoneId: config?.zone ?? definition?.start ?? "", state: trigger.state, status: trigger.lastOutcome, statusLink: planeStatusLink(config, trigger.lastOutcome), statusAt: trigger.lastAt, failing: trigger.failures > 0,
|
|
156
156
|
button: config?.kind === "button" ? { label: config.label, questions: config.questions } : null,
|
|
157
157
|
hook: config !== null && takesDeliveries(config) ? { ready: hookReady(trigger, setup.dir), needsSecret: config.kind === "linear" } : null,
|
|
158
158
|
checkable: ((config?.kind === "github" || config?.kind === "linear") && config.delivery === "poll") || config?.kind === "email" || (config?.kind === "schedule" && config.script !== undefined) || config?.kind === "plane-review",
|
package/dist/invoke.d.ts
CHANGED
|
@@ -39,6 +39,9 @@ export type AgentOutcome = {
|
|
|
39
39
|
sessionId: string | null;
|
|
40
40
|
/** The agent's spoken conclusion — diagnostics, never the handoff. */
|
|
41
41
|
finalMessage: string | null;
|
|
42
|
+
/** Claude's `structured_output` alone (see ParsedEnvelope) — the only
|
|
43
|
+
* field a structured handback may be read from. */
|
|
44
|
+
structuredOutput?: string | null;
|
|
42
45
|
/** The harness's own account of the ending, when its dialect has one. */
|
|
43
46
|
ending?: AgentEnding | null;
|
|
44
47
|
usage: ProviderUsage;
|
package/dist/invoke.js
CHANGED
|
@@ -550,6 +550,7 @@ export async function invokeAgent(store, runId, spec, invocation, options) {
|
|
|
550
550
|
notFound: result.notFound,
|
|
551
551
|
sessionId: envelope.sessionId,
|
|
552
552
|
finalMessage: envelope.finalMessage,
|
|
553
|
+
structuredOutput: envelope.structuredOutput ?? null,
|
|
553
554
|
ending: envelope.ending ?? null,
|
|
554
555
|
usage: {
|
|
555
556
|
tokensIn: envelope.tokensIn,
|
package/dist/plane-review.d.ts
CHANGED
|
@@ -11,6 +11,8 @@ export type PlaneProblem = {
|
|
|
11
11
|
evidence: string[];
|
|
12
12
|
};
|
|
13
13
|
export declare const REVIEW_HOURS = 24;
|
|
14
|
+
/** A person's wait this long is mentioned in the review's summary line. */
|
|
15
|
+
export declare const LONG_WAIT_DAYS = 3;
|
|
14
16
|
export declare const RUN_CAUSES: readonly ["provider", "sign-in", "check", "timeout", "no-handoff", "stuck-lease", "orphaned", "no-change", "other"];
|
|
15
17
|
export type RunCause = (typeof RUN_CAUSES)[number];
|
|
16
18
|
/** Anything shaped like a key is hidden; one short line. */
|
|
@@ -30,6 +32,19 @@ export declare function runCause(run: {
|
|
|
30
32
|
* project counts only when every project may be seen).
|
|
31
33
|
*/
|
|
32
34
|
export declare function reviewPlane(store: Store, now: Date, canSee?: (repo: string | null) => boolean): PlaneProblem[];
|
|
35
|
+
/**
|
|
36
|
+
* Results nobody has opened, and tasks waiting on a person's answer, approval or hold, for over `LONG_WAIT_DAYS`
|
|
37
|
+
* before `now`. Counted for the review's summary line only; they are never problems.
|
|
38
|
+
*/
|
|
39
|
+
export declare function longPersonWaits(store: Store, now: Date, canSee?: (repo: string | null) => boolean): {
|
|
40
|
+
results: number;
|
|
41
|
+
tasks: number;
|
|
42
|
+
};
|
|
43
|
+
/** "3 results have waited over 3 days for you." — or null when nothing has. */
|
|
44
|
+
export declare function longWaitWords(waits: {
|
|
45
|
+
results: number;
|
|
46
|
+
tasks: number;
|
|
47
|
+
}): string | null;
|
|
33
48
|
/** A problem as a card's details (or a day's note on a card it joins). */
|
|
34
49
|
export declare function problemText(problem: PlaneProblem, day: string): string;
|
|
35
50
|
/** The card's opening details: the day's evidence, and what it is. */
|
package/dist/plane-review.js
CHANGED
|
@@ -6,7 +6,9 @@
|
|
|
6
6
|
* - failed or no-change runs, grouped by cause (provider error, sign-in
|
|
7
7
|
* expiry, check failure, timeout, quit without handoff, stuck lease,
|
|
8
8
|
* orphaned process, no change, other);
|
|
9
|
-
* - tasks
|
|
9
|
+
* - tasks stuck for over a day on something the system must fix (unfinished
|
|
10
|
+
* work no worker holds, repeated failures, an incident hold), grouped by
|
|
11
|
+
* what they wait for;
|
|
10
12
|
* - sign-in and plan-limit pauses, per provider;
|
|
11
13
|
* - chat replies that couldn't be delivered, per app;
|
|
12
14
|
* - integrations that are Broken (integration_check);
|
|
@@ -17,6 +19,13 @@
|
|
|
17
19
|
* problem (flow-triggers.ts joins it to its card), and carries counts, run
|
|
18
20
|
* ids and short evidence excerpts with anything shaped like a secret hidden.
|
|
19
21
|
* A clean day is no problems at all. Only projects the reader may see count.
|
|
22
|
+
*
|
|
23
|
+
* Waits only a person can end — a result to open, a question to answer, a
|
|
24
|
+
* plan or task to approve, a hold to lift, a scope to write — are never
|
|
25
|
+
* problems: each card starts a find-the-cause → fix → pull request flow, and
|
|
26
|
+
* no code change opens a result for someone. Needs you and chat already ask
|
|
27
|
+
* the person. `longPersonWaits` counts the ones over `LONG_WAIT_DAYS` so the
|
|
28
|
+
* review's own summary line can mention them once, filing nothing.
|
|
20
29
|
*/
|
|
21
30
|
import { scanForSecrets } from "./evidence.js";
|
|
22
31
|
import { scrubIntegrationText } from "./integrations.js";
|
|
@@ -24,8 +33,10 @@ import { taskWaitSnapshot } from "./lead-status.js";
|
|
|
24
33
|
export const REVIEW_HOURS = 24;
|
|
25
34
|
const EVIDENCE_LINES = 5;
|
|
26
35
|
const WAITING_DAYS = 1;
|
|
27
|
-
/** A
|
|
28
|
-
const
|
|
36
|
+
/** A person's wait this long is mentioned in the review's summary line. */
|
|
37
|
+
export const LONG_WAIT_DAYS = 3;
|
|
38
|
+
/** What a person, not the code, must do next (lead-status.ts `taskWaitSnapshot`). The rest have a system cause. */
|
|
39
|
+
const PERSON_NEXT = new Set(["Open result", "Answer the task's question", "Answer question", "Remove hold", "Review plan", "Resume or close attempt", "Choose a result", "Approve plan", "Add scope", "Approve task"]);
|
|
29
40
|
export const RUN_CAUSES = ["provider", "sign-in", "check", "timeout", "no-handoff", "stuck-lease", "orphaned", "no-change", "other"];
|
|
30
41
|
const CAUSE_TITLES = {
|
|
31
42
|
provider: "Runs failed on a provider error",
|
|
@@ -140,24 +151,12 @@ export function reviewPlane(store, now, canSee = () => true) {
|
|
|
140
151
|
const run = Number(row["run"]);
|
|
141
152
|
add("run/orphaned", CAUSE_TITLES.orphaned, ["run", "runs"], { run, evidence: `Run ${run} (task ${str(row["task"]) ?? "?"}): process ${str(row["pid"]) ?? "?"} on ${excerpt(str(row["host"]), 40)} was still running after it finished` });
|
|
142
153
|
}
|
|
143
|
-
// Tasks
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
const tasks = store.handle.prepare(`SELECT task.id, task.title, task.updated_at, ref.repo FROM task JOIN task_ref ref ON ref.backend = 'built-in' AND ref.external_id = task.id
|
|
147
|
-
WHERE task.updated_at <= ? AND (task.state IN ('queued', 'running') OR (task.state = 'done' AND task.updated_at >= ?)) ORDER BY task.updated_at LIMIT 500`).all(waited, readyFrom);
|
|
148
|
-
for (const row of tasks) {
|
|
149
|
-
if (!canSee(str(row["repo"])))
|
|
150
|
-
continue;
|
|
151
|
-
// A version a later revision replaced never waits on its own.
|
|
152
|
-
const family = store.taskFamilyOf(String(row["id"]), null, true);
|
|
153
|
-
if (family !== null && family.current.id !== String(row["id"]))
|
|
154
|
-
continue;
|
|
155
|
-
const snapshot = taskWaitSnapshot(store, String(row["id"]), now);
|
|
156
|
-
if (snapshot === null || (snapshot.reason !== "needs-person" && snapshot.reason !== "ready"))
|
|
154
|
+
// Tasks stuck for over a day on a system cause, by what they wait for. Waits on a person are never cards.
|
|
155
|
+
for (const wait of openWaits(store, new Date(now.getTime() - WAITING_DAYS * 86_400_000), now, canSee)) {
|
|
156
|
+
if (wait.person)
|
|
157
157
|
continue;
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
evidence: `Task ${String(row["id"])} “${excerpt(str(row["title"]), 80)}”: ${plural(days, "day")} waiting` });
|
|
158
|
+
add(`waiting/${keyPart(wait.next)}`, `Tasks waited over a day: ${wait.next}`, ["task", "tasks"], { run: wait.run,
|
|
159
|
+
evidence: `Task ${wait.id} “${excerpt(wait.title, 80)}”: ${plural(wait.days, "day")} waiting` });
|
|
161
160
|
}
|
|
162
161
|
// Sign-in and plan-limit pauses.
|
|
163
162
|
if (tableExists(store, "provider_auth_pause")) {
|
|
@@ -227,6 +226,49 @@ export function reviewPlane(store, now, canSee = () => true) {
|
|
|
227
226
|
summary: `${plural(bucket.count, bucket.noun[0], bucket.noun[1])} in the last ${REVIEW_HOURS} hours${runsList.length === 0 ? "" : ` (${runsList.length === 1 ? "run" : "runs"} ${runsList.slice(0, 12).join(", ")}${runsList.length > 12 ? ", …" : ""})`}.` };
|
|
228
227
|
}).sort((a, b) => b.count - a.count || a.key.localeCompare(b.key));
|
|
229
228
|
}
|
|
229
|
+
/** Queued or running tasks last touched by `before` that wait, and on what; replaced versions never wait on their own. */
|
|
230
|
+
function openWaits(store, before, now, canSee) {
|
|
231
|
+
const rows = store.handle.prepare(`SELECT task.id, task.title, task.updated_at, ref.repo FROM task JOIN task_ref ref ON ref.backend = 'built-in' AND ref.external_id = task.id
|
|
232
|
+
WHERE task.updated_at <= ? AND task.state IN ('queued', 'running') ORDER BY task.updated_at LIMIT 500`).all(before.toISOString());
|
|
233
|
+
return waitsOf(store, rows, now, canSee);
|
|
234
|
+
}
|
|
235
|
+
function waitsOf(store, rows, now, canSee) {
|
|
236
|
+
const waits = [];
|
|
237
|
+
for (const row of rows) {
|
|
238
|
+
if (!canSee(str(row["repo"])))
|
|
239
|
+
continue;
|
|
240
|
+
const id = String(row["id"]);
|
|
241
|
+
const family = store.taskFamilyOf(id, null, true);
|
|
242
|
+
if (family !== null && family.current.id !== id)
|
|
243
|
+
continue;
|
|
244
|
+
const snapshot = taskWaitSnapshot(store, id, now);
|
|
245
|
+
if (snapshot === null || (snapshot.reason !== "needs-person" && snapshot.reason !== "ready"))
|
|
246
|
+
continue;
|
|
247
|
+
waits.push({ id, title: str(row["title"]), next: snapshot.next, run: snapshot.run, days: Math.floor((now.getTime() - Date.parse(String(row["updated_at"]))) / 86_400_000),
|
|
248
|
+
person: snapshot.reason === "ready" || PERSON_NEXT.has(snapshot.next) });
|
|
249
|
+
}
|
|
250
|
+
return waits;
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Results nobody has opened, and tasks waiting on a person's answer, approval or hold, for over `LONG_WAIT_DAYS`
|
|
254
|
+
* before `now`. Counted for the review's summary line only; they are never problems.
|
|
255
|
+
*/
|
|
256
|
+
export function longPersonWaits(store, now, canSee = () => true) {
|
|
257
|
+
const before = new Date(now.getTime() - LONG_WAIT_DAYS * 86_400_000);
|
|
258
|
+
// Newest first, so years of Complete work never crowds out this week's unopened results.
|
|
259
|
+
const finished = store.handle.prepare(`SELECT task.id, task.title, task.updated_at, ref.repo FROM task JOIN task_ref ref ON ref.backend = 'built-in' AND ref.external_id = task.id
|
|
260
|
+
WHERE task.updated_at <= ? AND task.state = 'done' ORDER BY task.updated_at DESC LIMIT 500`).all(before.toISOString());
|
|
261
|
+
const results = waitsOf(store, finished, now, canSee).filter(wait => wait.person).length;
|
|
262
|
+
const tasks = openWaits(store, before, now, canSee).filter(wait => wait.person).length;
|
|
263
|
+
return { results, tasks };
|
|
264
|
+
}
|
|
265
|
+
/** "3 results have waited over 3 days for you." — or null when nothing has. */
|
|
266
|
+
export function longWaitWords(waits) {
|
|
267
|
+
const parts = [waits.results > 0 ? plural(waits.results, "result") : null, waits.tasks > 0 ? plural(waits.tasks, "task") : null].filter(one => one !== null);
|
|
268
|
+
if (parts.length === 0)
|
|
269
|
+
return null;
|
|
270
|
+
return `${parts.join(" and ")} ${waits.results + waits.tasks === 1 ? "has" : "have"} waited over ${LONG_WAIT_DAYS} days for you.`;
|
|
271
|
+
}
|
|
230
272
|
/** A problem as a card's details (or a day's note on a card it joins). */
|
|
231
273
|
export function problemText(problem, day) {
|
|
232
274
|
return [`${day}: ${problem.summary}`, ...problem.evidence.map(line => `- ${line}`)].join("\n");
|
package/dist/provider.d.ts
CHANGED
|
@@ -68,6 +68,10 @@ export type Invocation = {
|
|
|
68
68
|
* Codex renders it as its own sandbox profile; Claude as deny rules for
|
|
69
69
|
* its file tools (its shell is fenced by the macOS sandbox around it). */
|
|
70
70
|
fence?: readonly string[];
|
|
71
|
+
/** Claude's structured-output floor for a non-review phase: the scout's
|
|
72
|
+
* report rides the terminal result event instead of a file plan mode
|
|
73
|
+
* would refuse to write. Ignored by providers without `--json-schema`. */
|
|
74
|
+
jsonSchema?: Readonly<Record<string, unknown>>;
|
|
71
75
|
};
|
|
72
76
|
export type ProviderRunner = (file: string, args: readonly string[], options?: RunOptions) => Promise<ExecResult>;
|
|
73
77
|
/** The harness's own account of how a turn ended. */
|
|
@@ -86,6 +90,10 @@ export type ParsedEnvelope = {
|
|
|
86
90
|
protocolError: string | null;
|
|
87
91
|
/** The agent's spoken conclusion — diagnostics only, never the handoff. */
|
|
88
92
|
finalMessage: string | null;
|
|
93
|
+
/** Claude's schema-validated `structured_output` alone, re-serialized —
|
|
94
|
+
* never the prose result, even when that prose is JSON. Absent or null
|
|
95
|
+
* when the turn ran without `--json-schema` or returned none. */
|
|
96
|
+
structuredOutput?: string | null;
|
|
89
97
|
/** How the harness said the turn ended (claude: the result's subtype and
|
|
90
98
|
* turn count) — absent on dialects that carry no such record. */
|
|
91
99
|
ending?: AgentEnding | null;
|
package/dist/provider.js
CHANGED
|
@@ -229,7 +229,8 @@ const claudeArgv = (invocation) => [
|
|
|
229
229
|
// Headless (run 2085): a wakeup, cron job or monitor needs a later
|
|
230
230
|
// turn that a -p process never gets. Denied at the harness too.
|
|
231
231
|
"--disallowedTools", HEADLESS_DISALLOWED_TOOLS.join(","),
|
|
232
|
-
...(invocation.toolArgv ?? []), ...(invocation.fence !== undefined && invocation.fence.length > 0 ? ["--settings", claudeFenceSettings(invocation.fence)] : [])
|
|
232
|
+
...(invocation.toolArgv ?? []), ...(invocation.fence !== undefined && invocation.fence.length > 0 ? ["--settings", claudeFenceSettings(invocation.fence)] : []),
|
|
233
|
+
...(invocation.jsonSchema === undefined ? [] : ["--json-schema", JSON.stringify(invocation.jsonSchema)])
|
|
233
234
|
]),
|
|
234
235
|
];
|
|
235
236
|
/**
|
|
@@ -276,6 +277,9 @@ function claudeEnvelopeOf(result, sessionFromInit, initObserved, initSessionConf
|
|
|
276
277
|
? "the Claude init event and terminal result announced different session ids"
|
|
277
278
|
: null,
|
|
278
279
|
finalMessage: claudeFinalMessage(result),
|
|
280
|
+
structuredOutput: result === null || result.structured_output === undefined || result.structured_output === null
|
|
281
|
+
? null
|
|
282
|
+
: JSON.stringify(result.structured_output),
|
|
279
283
|
ending: result === null ? null : {
|
|
280
284
|
subtype: typeof result.subtype === "string" ? result.subtype : null,
|
|
281
285
|
turns: typeof result.num_turns === "number" && result.num_turns >= 0 ? result.num_turns : null,
|
package/dist/result-shots.d.ts
CHANGED
|
@@ -39,6 +39,8 @@ export type ResultShotsPlan =
|
|
|
39
39
|
* no wait in between, or asks again.
|
|
40
40
|
*/
|
|
41
41
|
export declare function resultShotsFor(store: Store, evidenceRoot: string | undefined, repos: readonly string[], row: Pick<Notification, "kind" | "recipient" | "taskId" | "taskRef" | "run">, sent?: ReadonlySet<number>): ResultShotsPlan;
|
|
42
|
+
/** A screenshot whose result message is not posted yet waits for that message's own next try (at least a second). */
|
|
43
|
+
export declare function shotWaitsUntil(resultNextAt: unknown, now: Date): string;
|
|
42
44
|
/** Retention removed this result's files: its screenshots go quietly, never as a complaint. */
|
|
43
45
|
export declare function resultShotsPruned(evidenceRoot: string, run: number): boolean;
|
|
44
46
|
/**
|
package/dist/result-shots.js
CHANGED
|
@@ -102,6 +102,11 @@ export function resultShotsFor(store, evidenceRoot, repos, row, sent = new Set()
|
|
|
102
102
|
const remaining = shots.filter(one => !sent.has(one.artifact));
|
|
103
103
|
return remaining.length === 0 ? { kind: "none", why: "sent" } : { kind: "send", taskId: row.taskId, run: row.run, shots: remaining };
|
|
104
104
|
}
|
|
105
|
+
/** A screenshot whose result message is not posted yet waits for that message's own next try (at least a second). */
|
|
106
|
+
export function shotWaitsUntil(resultNextAt, now) {
|
|
107
|
+
const soonest = new Date(now.getTime() + 1_000).toISOString();
|
|
108
|
+
return typeof resultNextAt === "string" && resultNextAt > soonest ? resultNextAt : soonest;
|
|
109
|
+
}
|
|
105
110
|
/** Retention removed this result's files: its screenshots go quietly, never as a complaint. */
|
|
106
111
|
export function resultShotsPruned(evidenceRoot, run) {
|
|
107
112
|
return existsSync(join(evidenceRoot, String(run), RETENTION_NOTE));
|
package/dist/scout-report.d.ts
CHANGED
|
@@ -39,4 +39,97 @@ export declare const REPORT_LIMITS: {
|
|
|
39
39
|
readonly followUpTitle: 200;
|
|
40
40
|
readonly followUpGoal: 2000;
|
|
41
41
|
};
|
|
42
|
+
/**
|
|
43
|
+
* The scout's handback for Claude's `--json-schema` (run 2334's fix): plan
|
|
44
|
+
* mode only lets a session write its own plan file, so a Claude scout
|
|
45
|
+
* returns a report — or a question for the operator — as structured output
|
|
46
|
+
* on the terminal result event. Shape only, never the validator: byte caps,
|
|
47
|
+
* one-line titles and control characters stay `parseReport`'s and
|
|
48
|
+
* `parseDecision`'s, applied to the re-serialized body.
|
|
49
|
+
*/
|
|
50
|
+
export declare const SCOUT_OUTPUT_JSON_SCHEMA: {
|
|
51
|
+
readonly type: "object";
|
|
52
|
+
readonly properties: {
|
|
53
|
+
readonly kind: {
|
|
54
|
+
readonly type: "string";
|
|
55
|
+
readonly enum: readonly ["report", "question"];
|
|
56
|
+
};
|
|
57
|
+
readonly report: {
|
|
58
|
+
readonly type: "object";
|
|
59
|
+
readonly properties: {
|
|
60
|
+
readonly title: {
|
|
61
|
+
readonly type: "string";
|
|
62
|
+
};
|
|
63
|
+
readonly summary: {
|
|
64
|
+
readonly type: "string";
|
|
65
|
+
};
|
|
66
|
+
readonly report: {
|
|
67
|
+
readonly type: "string";
|
|
68
|
+
};
|
|
69
|
+
readonly followUps: {
|
|
70
|
+
readonly type: "array";
|
|
71
|
+
readonly items: {
|
|
72
|
+
readonly type: "object";
|
|
73
|
+
readonly properties: {
|
|
74
|
+
readonly title: {
|
|
75
|
+
readonly type: "string";
|
|
76
|
+
};
|
|
77
|
+
readonly goal: {
|
|
78
|
+
readonly type: "string";
|
|
79
|
+
};
|
|
80
|
+
};
|
|
81
|
+
readonly required: readonly ["title", "goal"];
|
|
82
|
+
readonly additionalProperties: false;
|
|
83
|
+
};
|
|
84
|
+
};
|
|
85
|
+
};
|
|
86
|
+
readonly required: readonly ["title", "summary", "report"];
|
|
87
|
+
readonly additionalProperties: false;
|
|
88
|
+
};
|
|
89
|
+
readonly decision: {
|
|
90
|
+
readonly type: "object";
|
|
91
|
+
readonly properties: {
|
|
92
|
+
readonly urgency: {
|
|
93
|
+
readonly type: "string";
|
|
94
|
+
readonly enum: readonly ["blocking"];
|
|
95
|
+
};
|
|
96
|
+
readonly recap: {
|
|
97
|
+
readonly type: "string";
|
|
98
|
+
};
|
|
99
|
+
readonly question: {
|
|
100
|
+
readonly type: "string";
|
|
101
|
+
};
|
|
102
|
+
readonly options: {
|
|
103
|
+
readonly type: "array";
|
|
104
|
+
readonly items: {
|
|
105
|
+
readonly type: "object";
|
|
106
|
+
readonly properties: {
|
|
107
|
+
readonly id: {
|
|
108
|
+
readonly type: "string";
|
|
109
|
+
};
|
|
110
|
+
readonly label: {
|
|
111
|
+
readonly type: "string";
|
|
112
|
+
};
|
|
113
|
+
readonly consequence: {
|
|
114
|
+
readonly type: "string";
|
|
115
|
+
};
|
|
116
|
+
readonly reversible: {
|
|
117
|
+
readonly type: "boolean";
|
|
118
|
+
};
|
|
119
|
+
};
|
|
120
|
+
readonly required: readonly ["id", "label", "consequence", "reversible"];
|
|
121
|
+
readonly additionalProperties: false;
|
|
122
|
+
};
|
|
123
|
+
};
|
|
124
|
+
readonly recommendation: {
|
|
125
|
+
readonly type: "string";
|
|
126
|
+
};
|
|
127
|
+
};
|
|
128
|
+
readonly required: readonly ["urgency", "recap", "question", "options", "recommendation"];
|
|
129
|
+
readonly additionalProperties: false;
|
|
130
|
+
};
|
|
131
|
+
};
|
|
132
|
+
readonly required: readonly ["kind"];
|
|
133
|
+
readonly additionalProperties: false;
|
|
134
|
+
};
|
|
42
135
|
export declare function parseReport(raw: string): ReportParseResult;
|
package/dist/scout-report.js
CHANGED
|
@@ -18,6 +18,69 @@ export const REPORT_LIMITS = {
|
|
|
18
18
|
followUpTitle: 200,
|
|
19
19
|
followUpGoal: 2_000,
|
|
20
20
|
};
|
|
21
|
+
const REPORT_SHAPE = {
|
|
22
|
+
type: "object",
|
|
23
|
+
properties: {
|
|
24
|
+
title: { type: "string" },
|
|
25
|
+
summary: { type: "string" },
|
|
26
|
+
report: { type: "string" },
|
|
27
|
+
followUps: {
|
|
28
|
+
type: "array",
|
|
29
|
+
items: {
|
|
30
|
+
type: "object",
|
|
31
|
+
properties: { title: { type: "string" }, goal: { type: "string" } },
|
|
32
|
+
required: ["title", "goal"],
|
|
33
|
+
additionalProperties: false,
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
required: ["title", "summary", "report"],
|
|
38
|
+
additionalProperties: false,
|
|
39
|
+
};
|
|
40
|
+
/** The park mailbox's decision, the same fields `parseDecision` reads. */
|
|
41
|
+
const DECISION_SHAPE = {
|
|
42
|
+
type: "object",
|
|
43
|
+
properties: {
|
|
44
|
+
urgency: { type: "string", enum: ["blocking"] },
|
|
45
|
+
recap: { type: "string" },
|
|
46
|
+
question: { type: "string" },
|
|
47
|
+
options: {
|
|
48
|
+
type: "array",
|
|
49
|
+
items: {
|
|
50
|
+
type: "object",
|
|
51
|
+
properties: {
|
|
52
|
+
id: { type: "string" },
|
|
53
|
+
label: { type: "string" },
|
|
54
|
+
consequence: { type: "string" },
|
|
55
|
+
reversible: { type: "boolean" },
|
|
56
|
+
},
|
|
57
|
+
required: ["id", "label", "consequence", "reversible"],
|
|
58
|
+
additionalProperties: false,
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
recommendation: { type: "string" },
|
|
62
|
+
},
|
|
63
|
+
required: ["urgency", "recap", "question", "options", "recommendation"],
|
|
64
|
+
additionalProperties: false,
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* The scout's handback for Claude's `--json-schema` (run 2334's fix): plan
|
|
68
|
+
* mode only lets a session write its own plan file, so a Claude scout
|
|
69
|
+
* returns a report — or a question for the operator — as structured output
|
|
70
|
+
* on the terminal result event. Shape only, never the validator: byte caps,
|
|
71
|
+
* one-line titles and control characters stay `parseReport`'s and
|
|
72
|
+
* `parseDecision`'s, applied to the re-serialized body.
|
|
73
|
+
*/
|
|
74
|
+
export const SCOUT_OUTPUT_JSON_SCHEMA = {
|
|
75
|
+
type: "object",
|
|
76
|
+
properties: {
|
|
77
|
+
kind: { type: "string", enum: ["report", "question"] },
|
|
78
|
+
report: REPORT_SHAPE,
|
|
79
|
+
decision: DECISION_SHAPE,
|
|
80
|
+
},
|
|
81
|
+
required: ["kind"],
|
|
82
|
+
additionalProperties: false,
|
|
83
|
+
};
|
|
21
84
|
function refuse(reason, message) {
|
|
22
85
|
return { ok: false, problems: [{ reason, message }] };
|
|
23
86
|
}
|