toolroll 0.9.101 → 0.9.103
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 +13 -0
- package/dist/browser/terminal.js +9 -9
- package/dist/browser/workspace.css +1 -1
- package/dist/browser/workspace.js +38 -38
- package/dist/browser-workspace.d.ts +51 -14
- package/dist/browser-workspace.js +5 -3
- package/dist/flow-engine.d.ts +3 -0
- package/dist/flow-engine.js +4 -1
- package/dist/flows-ui.d.ts +31 -1
- package/dist/flows-ui.js +40 -12
- package/dist/page-work-contract.d.ts +21 -0
- package/dist/page-work-contract.js +5 -0
- package/dist/project-knowledge.d.ts +9 -9
- package/dist/release-approval.d.ts +9 -20
- package/dist/release-approval.js +14 -53
- package/dist/release-refusal.d.ts +32 -0
- package/dist/release-refusal.js +73 -0
- package/dist/run-efficiency.d.ts +3 -2
- package/dist/run-efficiency.js +4 -3
- package/dist/scope.js +6 -0
- package/dist/serve.js +51 -9
- package/dist/server/chrome.d.ts +10 -1
- package/dist/server/chrome.js +9 -0
- package/dist/server/console.css +19 -4
- package/dist/server/home-asks.d.ts +45 -0
- package/dist/server/home-asks.js +108 -0
- package/dist/server/home.d.ts +37 -14
- package/dist/server/home.js +113 -63
- package/dist/server/http.d.ts +6 -0
- package/dist/server/http.js +8 -0
- package/dist/server/in-place.d.ts +15 -0
- package/dist/server/in-place.js +32 -0
- package/dist/server/pages.js +36 -3
- package/dist/server/render-tasks.js +14 -3
- package/dist/server/request-context.d.ts +14 -0
- package/dist/server/request-context.js +3 -0
- package/dist/server/task-views.js +6 -3
- package/dist/server/tasks.d.ts +8 -2
- package/dist/server/tasks.js +31 -17
- package/dist/work-index.d.ts +19 -1
- package/dist/work-index.js +40 -14
- package/package.json +1 -1
|
@@ -9,6 +9,7 @@ export { browserCrewFromIndex, browserWorkActionHref } from './browser-crew.js';
|
|
|
9
9
|
import type { Store } from './store.js';
|
|
10
10
|
import type { WorkSummaryAccess } from './work-summary.js';
|
|
11
11
|
import type { StatusTone, StepRow } from './workspace-ui.js';
|
|
12
|
+
import type { PageWork } from './page-work-contract.js';
|
|
12
13
|
import type { Ask, AskChip } from './needs-you.js';
|
|
13
14
|
import type { WorkIndexGroup } from './work-index.js';
|
|
14
15
|
import type { AssignmentCard } from './assignment-ui.js';
|
|
@@ -486,9 +487,12 @@ export type BrowserTaskView = {
|
|
|
486
487
|
}[];
|
|
487
488
|
} | null;
|
|
488
489
|
/** Server cards that may need a person now (stop/resume, scope prompt, plan, live attempt). */
|
|
490
|
+
/** `instance`: what the block is about now (the attempt card: its build). The page installs a block's HTML once;
|
|
491
|
+
* another instance replaces it. */
|
|
489
492
|
lead: {
|
|
490
493
|
key: string;
|
|
491
494
|
html: string;
|
|
495
|
+
instance?: string;
|
|
492
496
|
}[];
|
|
493
497
|
questions: string;
|
|
494
498
|
facts: BrowserTaskFact[];
|
|
@@ -549,6 +553,8 @@ export type BrowserTaskView = {
|
|
|
549
553
|
/** When a remembered password stops counting (ISO); the page words it in the viewer's zone. */
|
|
550
554
|
rememberedUntil: string | null;
|
|
551
555
|
planHtml: string;
|
|
556
|
+
/** The plan sheet alone (no editor, no form): what Home shows beside its Approve (live-pages-and-home.md B2.1). */
|
|
557
|
+
sheetHtml?: string;
|
|
552
558
|
/** The terms line in words, when the rule isn't one of the kit's (easy-start C5: a laptop's own owner). */
|
|
553
559
|
termsWords?: string;
|
|
554
560
|
} | null;
|
|
@@ -1613,6 +1619,15 @@ export type BrowserWorkspace = {
|
|
|
1613
1619
|
user: string;
|
|
1614
1620
|
csrf: string;
|
|
1615
1621
|
sensitive: boolean;
|
|
1622
|
+
/** True when this page may replace another without a document load (docs/plans/live-pages-and-home.md A1.1): the server
|
|
1623
|
+
* sets it on every workspace it sends; absent (an older server, a fixture) is false. */
|
|
1624
|
+
inPlace?: boolean;
|
|
1625
|
+
/** What the app runs for this page in place of a page script (docs/plans/task-page-in-app.md): data, a closed list.
|
|
1626
|
+
* Present only when the app covers the page's whole script; the document then ships no page script string. */
|
|
1627
|
+
work?: readonly PageWork[];
|
|
1628
|
+
/** This document ships a page script string (set on a document load only). Its listeners live as long as the
|
|
1629
|
+
* document, so the app never changes pages in place from it, and never runs `work` in it. */
|
|
1630
|
+
scripted?: boolean;
|
|
1616
1631
|
/** Passwordless is on for this signed-in browser: ceremony password fields are left out (docs/plans/passwordless.md). */
|
|
1617
1632
|
passwordless?: boolean;
|
|
1618
1633
|
/** The session's open project (its path), or null: the sidebar's project picker names it. */
|
|
@@ -1688,13 +1703,14 @@ export type BrowserWorkspace = {
|
|
|
1688
1703
|
command: string;
|
|
1689
1704
|
};
|
|
1690
1705
|
};
|
|
1691
|
-
/** Home ("/"):
|
|
1692
|
-
*
|
|
1706
|
+
/** Home ("/", docs/plans/live-pages-and-home.md B1): the heading said once, one quiet line for what runs and waits, what
|
|
1707
|
+
* needs this person grouped by what is asked (each group with its exact count), the selected one open beside the list,
|
|
1708
|
+
* and the lead's last message above the message box. With nothing waiting: what runs and what finished today. */
|
|
1693
1709
|
export type BrowserHomeView = {
|
|
1694
1710
|
kind: 'home';
|
|
1695
|
-
/** "
|
|
1711
|
+
/** "19 tasks need you", "Nothing needs you", read from the same total as the rail's count. */
|
|
1696
1712
|
say: string;
|
|
1697
|
-
/** The project in view (a path) or every project (null), and the
|
|
1713
|
+
/** The project in view (a path) or every project (null), and the project filter's choices (null with one project). */
|
|
1698
1714
|
project: string | null;
|
|
1699
1715
|
scopes: {
|
|
1700
1716
|
key: string;
|
|
@@ -1702,29 +1718,43 @@ export type BrowserHomeView = {
|
|
|
1702
1718
|
href: string;
|
|
1703
1719
|
active: boolean;
|
|
1704
1720
|
}[] | null;
|
|
1721
|
+
/** The work index totals, each with the Tasks list it opens. */
|
|
1705
1722
|
counts: {
|
|
1706
1723
|
key: 'needs' | 'running' | 'queued' | 'finished';
|
|
1707
1724
|
label: string;
|
|
1708
1725
|
value: number;
|
|
1709
1726
|
href: string;
|
|
1710
1727
|
}[];
|
|
1711
|
-
/**
|
|
1728
|
+
/** What needs you, by what is asked, in this order: release, approve, answer, flow, wrong, review, setup, other
|
|
1729
|
+
* (anything none of the others fits; docs/plans/home-other-asks.md). `count` is the exact total for that ask, past the
|
|
1730
|
+
* rows listed. */
|
|
1712
1731
|
groups: {
|
|
1713
|
-
key:
|
|
1732
|
+
key: BrowserHomeAsk;
|
|
1714
1733
|
label: string;
|
|
1734
|
+
count: number;
|
|
1715
1735
|
rows: BrowserHomeRow[];
|
|
1716
1736
|
}[];
|
|
1717
|
-
/**
|
|
1718
|
-
|
|
1737
|
+
/** What runs, then what waits its turn. */
|
|
1738
|
+
working: BrowserHomeRow[];
|
|
1739
|
+
/** What was completed today: the exact figure, the newest rows, and the list that has them all. */
|
|
1740
|
+
finishedToday: {
|
|
1719
1741
|
count: number;
|
|
1742
|
+
rows: BrowserHomeRow[];
|
|
1720
1743
|
href: string;
|
|
1721
|
-
}
|
|
1744
|
+
};
|
|
1722
1745
|
newTask: string;
|
|
1723
1746
|
/** The lead's message box, where chat is allowed for this person: a POST to /chat in this scope. */
|
|
1724
1747
|
ask: {
|
|
1725
1748
|
project: string | null;
|
|
1726
1749
|
placeholder: string;
|
|
1727
1750
|
} | null;
|
|
1751
|
+
/** The lead's last message in this person's main thread, as one plain line that opens the chat; null when there is
|
|
1752
|
+
* none or this person may not read that thread. */
|
|
1753
|
+
lastLead: {
|
|
1754
|
+
text: string;
|
|
1755
|
+
age: string;
|
|
1756
|
+
href: string;
|
|
1757
|
+
} | null;
|
|
1728
1758
|
empty: {
|
|
1729
1759
|
title: string;
|
|
1730
1760
|
text: string;
|
|
@@ -1732,19 +1762,25 @@ export type BrowserHomeView = {
|
|
|
1732
1762
|
/** While the first run is open (no first result yet): the welcome's address, linked as Finish setup. */
|
|
1733
1763
|
setup?: string | null;
|
|
1734
1764
|
};
|
|
1765
|
+
/** What a waiting row asks (work-index.ts WorkAskGroup), plus a flow card waiting on you and a first-run step. */
|
|
1766
|
+
export type BrowserHomeAsk = 'release' | 'approve' | 'answer' | 'flow' | 'wrong' | 'review' | 'setup' | 'other';
|
|
1735
1767
|
export type BrowserHomeRow = {
|
|
1768
|
+
/** A task row's family id; a row that isn't a task is namespaced (`release:<task>`, `flow:<card>`, `setup:<step>`). */
|
|
1736
1769
|
id: string;
|
|
1737
1770
|
title: string;
|
|
1738
1771
|
href: string;
|
|
1739
1772
|
project: string | null;
|
|
1740
1773
|
age: string;
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1774
|
+
/** Which list it is in: it needs you, it runs, it waits its turn, or it finished. */
|
|
1775
|
+
state: 'needs' | 'running' | 'queued' | 'done';
|
|
1776
|
+
/** What it asks of you (its group), or null when it doesn't wait on you. */
|
|
1777
|
+
ask: BrowserHomeAsk | null;
|
|
1778
|
+
/** Why, in one short line: only a row where something went wrong. */
|
|
1779
|
+
reason: string | null;
|
|
1745
1780
|
/** One line: what it asks, or what the agent is doing now. */
|
|
1746
1781
|
line: string | null;
|
|
1747
|
-
/** The one act that resolves it (a link), or null.
|
|
1782
|
+
/** The one act that resolves it (a link), or null. A release, a flow card or a setup step is never acted on at
|
|
1783
|
+
* Home: its one link opens its own page. */
|
|
1748
1784
|
action: {
|
|
1749
1785
|
label: string;
|
|
1750
1786
|
href: string;
|
|
@@ -1933,3 +1969,4 @@ export type MergeReadyView = {
|
|
|
1933
1969
|
act: MergeReadyAct | null;
|
|
1934
1970
|
}[];
|
|
1935
1971
|
};
|
|
1972
|
+
export type { PageWork } from './page-work-contract.js';
|
|
@@ -97,11 +97,13 @@ export function browserProjectsOf(projects) {
|
|
|
97
97
|
export function browserNavigationOf(path, project = null, needsYou = 0, tasksProject = project, needsYouLabel) {
|
|
98
98
|
const pathname = path.split('?')[0].split('#')[0];
|
|
99
99
|
const under = (...roots) => roots.some(root => pathname === root || pathname.startsWith(`${root}/`));
|
|
100
|
-
// The rail (UI revamp, wave B): Home carries the one Needs-you count
|
|
101
|
-
// under
|
|
100
|
+
// The rail (UI revamp, wave B; Chat under Home, live-pages-and-home.md B1): Home carries the one Needs-you count and
|
|
101
|
+
// an agent's question lives under it; the lead chat is its own item; a task's pages under Tasks, every computer and
|
|
102
|
+
// terminal under Agents, every setting under Settings.
|
|
102
103
|
return [
|
|
103
|
-
{ label: 'Home', href: '/', active: pathname === '/' ||
|
|
104
|
+
{ label: 'Home', href: '/', active: pathname === '/' || under('/d', '/activity', '/morning'),
|
|
104
105
|
...(needsYou > 0 ? { count: needsYou, ...(needsYouLabel === undefined ? {} : { countLabel: needsYouLabel }) } : {}) },
|
|
106
|
+
{ label: 'Chat', href: '/chat', active: under('/chat') },
|
|
105
107
|
{ label: 'Tasks', href: `/tasks${tasksProject === null ? '' : `?project=${encodeURIComponent(tasksProject)}`}`,
|
|
106
108
|
active: under('/tasks', '/t', '/r', '/review') },
|
|
107
109
|
{ label: 'Flows', href: '/flows', active: under('/flows', '/recipes', '/kits', '/routines') },
|
package/dist/flow-engine.d.ts
CHANGED
|
@@ -9,6 +9,9 @@ export type FlowAdvance = {
|
|
|
9
9
|
export declare function flowDefinitionOf(flow: FlowRow): FlowDefinition | null;
|
|
10
10
|
/** Where a card's link opens: the flow's canvas with the card selected. */
|
|
11
11
|
export declare const flowCardHref: (flow: number, card: number) => string;
|
|
12
|
+
/** A reply wait that timed out with nowhere to send the card asks its owner (or the flow's owner) to move it on: the
|
|
13
|
+
* card's waiting words end with this (flows-ui.ts flowCardWaitsOn reads it). */
|
|
14
|
+
export declare const NO_REPLY_MOVE_ON = "Move the card on when you're ready.";
|
|
12
15
|
/** Where a confirmed flow action may point: a flow's canvas, maybe with one card open. */
|
|
13
16
|
export declare const FLOW_HREF: RegExp;
|
|
14
17
|
/** Advance every active card in one project's flows. */
|
package/dist/flow-engine.js
CHANGED
|
@@ -36,6 +36,9 @@ export function flowDefinitionOf(flow) {
|
|
|
36
36
|
}
|
|
37
37
|
/** Where a card's link opens: the flow's canvas with the card selected. */
|
|
38
38
|
export const flowCardHref = (flow, card) => `/flows/${flow}?card=${card}`;
|
|
39
|
+
/** A reply wait that timed out with nowhere to send the card asks its owner (or the flow's owner) to move it on: the
|
|
40
|
+
* card's waiting words end with this (flows-ui.ts flowCardWaitsOn reads it). */
|
|
41
|
+
export const NO_REPLY_MOVE_ON = "Move the card on when you're ready.";
|
|
39
42
|
/** Where a confirmed flow action may point: a flow's canvas, maybe with one card open. */
|
|
40
43
|
export const FLOW_HREF = /^\/flows\/[1-9][0-9]{0,9}(\?card=[1-9][0-9]{0,9})?$/;
|
|
41
44
|
const ACCEPTANCE = FLOW_ACCEPTANCE;
|
|
@@ -173,7 +176,7 @@ function advanceCard(store, flow, definition, card, now, options, outcome) {
|
|
|
173
176
|
outcome.moved++;
|
|
174
177
|
return;
|
|
175
178
|
}
|
|
176
|
-
const waiting = `No reply after ${words}.
|
|
179
|
+
const waiting = `No reply after ${words}. ${NO_REPLY_MOVE_ON}`;
|
|
177
180
|
if (card.waiting !== waiting) {
|
|
178
181
|
store.updateFlowCard(card.id, { waiting }, now);
|
|
179
182
|
notifyPeople(store, card, [card.owner ?? flow.owner], null, { key: `no-reply:${card.entry}`, subject: `No reply to “${card.title}” after ${words}`, body: `It's waiting in ${stage.title}.`, attention: true }, now);
|
package/dist/flows-ui.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { BrowserFlowView } from "./browser-workspace.js";
|
|
2
|
-
import
|
|
2
|
+
import { type FlowStage } from "./flows.js";
|
|
3
|
+
import type { FlowCardRow, FlowRow, Store, SubagentQuestionRow } from "./store.js";
|
|
3
4
|
import { type FlowFile, type FlowImportPlan } from "./flow-share.js";
|
|
4
5
|
import { type Html } from "./html.js";
|
|
5
6
|
/** The flow page's fallback rules, plus the Flows list and the import preview (kit pieces; only layout here). */
|
|
@@ -24,6 +25,35 @@ export declare function flowImportHtml(input: {
|
|
|
24
25
|
previewed: string;
|
|
25
26
|
problem: string | null;
|
|
26
27
|
}): Html;
|
|
28
|
+
/** Why a card waits on this person now (docs/plans/home-other-asks.md part 4, v2.3): the one rule the flow page's
|
|
29
|
+
* Decide and Home's Flow decisions both read, so they never disagree. Each needs approver standing:
|
|
30
|
+
* - `decide`: an approval zone it is theirs to decide (the zone's decider, the flow's owner for "to owner", any approver
|
|
31
|
+
* when none is named), and no subagent of the zone deciding it first;
|
|
32
|
+
* - `choose`: a "Person chooses" zone waiting on them (flow-send.ts flowChoiceAt);
|
|
33
|
+
* - `answer`: its subagent's open question or tool-call request asked of them;
|
|
34
|
+
* - `browser`: a check zone waiting for browser use, which only an approver with every project may allow;
|
|
35
|
+
* - `reply`: a reply wait that ran out with nowhere to go, asking the card's owner (else the flow's) to move it on.
|
|
36
|
+
* `reads` passes what the caller already read (a missing key is read here). */
|
|
37
|
+
export type FlowCardWait = {
|
|
38
|
+
decide: boolean;
|
|
39
|
+
choose: boolean;
|
|
40
|
+
answer: boolean;
|
|
41
|
+
browser: boolean;
|
|
42
|
+
reply: boolean;
|
|
43
|
+
};
|
|
44
|
+
export declare function flowCardWaitsOn(store: Store, flow: FlowRow, card: FlowCardRow, viewer: {
|
|
45
|
+
name: string;
|
|
46
|
+
approver: boolean;
|
|
47
|
+
}, reads?: {
|
|
48
|
+
stage?: FlowStage | undefined;
|
|
49
|
+
asked?: SubagentQuestionRow | null;
|
|
50
|
+
choosing?: {
|
|
51
|
+
person: string;
|
|
52
|
+
} | null;
|
|
53
|
+
browser?: {
|
|
54
|
+
status: string;
|
|
55
|
+
} | null;
|
|
56
|
+
}): FlowCardWait;
|
|
27
57
|
/** One flow's canvas for one person. */
|
|
28
58
|
export declare function flowView(store: Store, flow: FlowRow, viewer: {
|
|
29
59
|
name: string;
|
package/dist/flows-ui.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** Flows in the console: the list of a person's flows, and one flow's canvas
|
|
2
2
|
* (the React view's data, plus a plain fallback page the view replaces). */
|
|
3
3
|
import { liveAttemptOf, steerControlsFor } from "./steer-controls.js";
|
|
4
|
-
import { draftFor, flowDefinitionOf } from "./flow-engine.js";
|
|
4
|
+
import { draftFor, flowDefinitionOf, NO_REPLY_MOVE_ON } from "./flow-engine.js";
|
|
5
5
|
import { describeStandingOrder } from "./flow-schedule.js";
|
|
6
6
|
import { describeTrigger, triggerHeadline, FLOW_TRIGGER_KINDS, FLOW_TRIGGER_WORDS, githubRepoOf, HOOK_PATH, hookReady, planeStatusLink, readHooksBase, readLinearKey, senderOf, takesDeliveries, triggerConfigOf } from "./flow-triggers.js";
|
|
7
7
|
import { SENDER_WORDS } from "./webhook-signing.js";
|
|
@@ -14,7 +14,7 @@ import { parseSortDecision, sortChip } from "./flow-sort.js";
|
|
|
14
14
|
import { flowSecretNames, readEmailSettings, sendingReady } from "./flow-actions.js";
|
|
15
15
|
import { projectToolsOf, secretsSetFor, toolStanding } from "./project-tools.js";
|
|
16
16
|
import { flowFingerprint } from "./flow-live.js";
|
|
17
|
-
import { flowChoiceAt, readFlowSend } from "./flow-send.js";
|
|
17
|
+
import { flowChoiceAt, flowPersonOf, readFlowSend } from "./flow-send.js";
|
|
18
18
|
import { itemCaption, whyLines } from "./flow-items.js";
|
|
19
19
|
import { FLOW_FILE_MAX_BYTES } from "./flow-share.js";
|
|
20
20
|
import { googleConnected } from "./google-mail.js";
|
|
@@ -224,6 +224,37 @@ const historyText = (event, title, sorts = new Set()) => {
|
|
|
224
224
|
default: return `Moved to ${title(event.toStage)}${who}${note}`;
|
|
225
225
|
}
|
|
226
226
|
};
|
|
227
|
+
export function flowCardWaitsOn(store, flow, card, viewer, reads = {}) {
|
|
228
|
+
const none = { decide: false, choose: false, answer: false, browser: false, reply: false };
|
|
229
|
+
if (!viewer.approver || card.state !== "active")
|
|
230
|
+
return none;
|
|
231
|
+
const stage = "stage" in reads ? reads.stage : flowDefinitionOf(flow)?.stages.find(one => one.id === card.stage);
|
|
232
|
+
// Its subagent's open question is the person's to answer whatever zone it is in, even one since removed from the flow
|
|
233
|
+
// (the flow page shows it and takes the answer).
|
|
234
|
+
const asked = "asked" in reads ? reads.asked ?? null : store.openSubagentQuestionOn(card.id, card.entry);
|
|
235
|
+
const answer = asked !== null && asked.state === "open" && asked.askedOf === viewer.name && store.getSubagent(asked.subagent) !== null;
|
|
236
|
+
if (stage === undefined)
|
|
237
|
+
return { ...none, answer };
|
|
238
|
+
const decider = stage.kind === "approval" ? deciderOf(stage, flow) : null;
|
|
239
|
+
// While its subagent is deciding (v92), the card isn't anyone else's to decide yet.
|
|
240
|
+
const subagentDeciding = stage.subagent !== undefined && card.waiting !== null && card.waiting.endsWith(" is deciding");
|
|
241
|
+
const choosing = stage.kind !== "choose" ? null : "choosing" in reads ? reads.choosing ?? null : flowChoiceAt(store, card.id, card.entry);
|
|
242
|
+
const browser = stage.kind !== "check" || stage.browser !== true ? null : "browser" in reads ? reads.browser ?? null : browserViewOf(store, flow);
|
|
243
|
+
const wait = stage.wait ?? { for: "reply" };
|
|
244
|
+
return {
|
|
245
|
+
decide: stage.kind === "approval" && (decider === null || decider === viewer.name) && !subagentDeciding,
|
|
246
|
+
choose: choosing !== null && choosing.person === viewer.name,
|
|
247
|
+
answer,
|
|
248
|
+
browser: browserWaitOn(card, stage, browser) && store.accountOf(viewer.name)?.projects === null,
|
|
249
|
+
reply: stage.kind === "wait" && wait.for === "reply" && card.waiting !== null && card.waiting.startsWith("No reply after ") && card.waiting.endsWith(NO_REPLY_MOVE_ON)
|
|
250
|
+
&& flowPersonOf(card, flow) === viewer.name,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
/** A check zone's card waiting for browser use (flow-browser.ts): the flow's browser isn't allowed and the card says so. */
|
|
254
|
+
function browserWaitOn(card, stage, browser) {
|
|
255
|
+
return card.state === "active" && stage?.kind === "check" && stage.browser === true && browser !== null && browser.status !== "allowed"
|
|
256
|
+
&& card.waiting !== null && card.waiting.startsWith(`${stage.title} starts a browser`);
|
|
257
|
+
}
|
|
227
258
|
/** One flow's canvas for one person. */
|
|
228
259
|
export function flowView(store, flow, viewer, selectedCard, setup = { dir: null, repos: [] }) {
|
|
229
260
|
const definition = flowDefinitionOf(flow);
|
|
@@ -249,10 +280,12 @@ export function flowView(store, flow, viewer, selectedCard, setup = { dir: null,
|
|
|
249
280
|
const canReadTask = taskRepo !== null && (taskRepo === flow.repo || (setup.repos.includes(taskRepo) && store.accountCanAccess(viewer.name, taskRepo)));
|
|
250
281
|
const discussion = store.flowComments(card.id);
|
|
251
282
|
const watchers = store.flowCardWatchers(card.id);
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
const
|
|
283
|
+
// Its open question: one of its own, or (v94) a tool call waiting for approval.
|
|
284
|
+
const asked = card.state === "active" ? store.openSubagentQuestionOn(card.id, card.entry) : null;
|
|
285
|
+
// "Person chooses" (flow-send.ts): what was sent, and the options, for the person it waits on; the latest send otherwise.
|
|
286
|
+
const choosing = card.state === "active" && stage?.kind === "choose" ? flowChoiceAt(store, card.id, card.entry) : null;
|
|
287
|
+
// Who may decide it here: the one rule Home reads too (flowCardWaitsOn).
|
|
288
|
+
const canDecide = flowCardWaitsOn(store, flow, card, viewer, { stage, asked, choosing, browser: browserNow }).decide;
|
|
256
289
|
// History reads moves and ownership together, newest first.
|
|
257
290
|
const owned = discussion.filter(one => one.kind === "owner").map(one => ({ text: one.body === "" ? `${one.author} left it without an owner` : one.body === one.author ? `${one.author} took it on` : `${one.author} made ${one.body} the owner`, at: one.at }));
|
|
258
291
|
const moves = store.flowEvents(card.id).map(event => ({ text: historyText(event, title, sortZones), at: event.at }));
|
|
@@ -266,8 +299,6 @@ export function flowView(store, flow, viewer, selectedCard, setup = { dir: null,
|
|
|
266
299
|
at: until.toISOString(),
|
|
267
300
|
label: stage.wait !== undefined && stage.wait.for !== "hours" ? (stage.wait.for === "reply" ? "No reply by" : "Moves on at") : stage.limit.to !== null ? `Moves to ${title(stage.limit.to)} at` : "Reminder at",
|
|
268
301
|
};
|
|
269
|
-
// Its open question: one of its own, or (v94) a tool call waiting for approval.
|
|
270
|
-
const asked = card.state === "active" ? store.openSubagentQuestionOn(card.id, card.entry) : null;
|
|
271
302
|
const pending = asked?.toolCall == null ? null : store.subagentCall(asked.toolCall);
|
|
272
303
|
// A decision its subagent handed over carries what the subagent said (v92).
|
|
273
304
|
const turn = card.state === "active" && stage?.kind === "approval" && stage.subagent !== undefined ? store.flowStepRun(card.id, card.entry) : null;
|
|
@@ -284,8 +315,6 @@ export function flowView(store, flow, viewer, selectedCard, setup = { dir: null,
|
|
|
284
315
|
}
|
|
285
316
|
}
|
|
286
317
|
const asker = asked === null || asked.state !== "open" ? null : store.getSubagent(asked.subagent);
|
|
287
|
-
// "Person chooses" (flow-send.ts): what was sent, and the options, for the person it waits on; the latest send otherwise.
|
|
288
|
-
const choosing = card.state === "active" && stage?.kind === "choose" ? flowChoiceAt(store, card.id, card.entry) : null;
|
|
289
318
|
const latest = store.flowSends(card.id)[0] ?? null;
|
|
290
319
|
const sentContent = latest === null ? null : readFlowSend(latest.contentJson);
|
|
291
320
|
// On the card itself: where it came from (the card's title is right above), and only the links the panel doesn't already have.
|
|
@@ -295,8 +324,7 @@ export function flowView(store, flow, viewer, selectedCard, setup = { dir: null,
|
|
|
295
324
|
// After research: the same numbered items the message carried, each beside its screenshot.
|
|
296
325
|
...(content.items === undefined ? {} : { items: content.items.map((item, index) => ({ number: index + 1, title: item.title, lines: whyLines(item.why), source: item.source, url: item.url,
|
|
297
326
|
image: item.shot === null || content.shots === null ? null : { src: `/r/${content.shots.run}/evidence/${item.shot}`, caption: itemCaption(index + 1, item) } })) }) });
|
|
298
|
-
const browserWait = card
|
|
299
|
-
&& card.waiting !== null && card.waiting.startsWith(`${stage.title} starts a browser`);
|
|
327
|
+
const browserWait = browserWaitOn(card, stage, browser);
|
|
300
328
|
return {
|
|
301
329
|
id: card.id, title: card.title, description: card.description, stage: card.stage, state: card.state,
|
|
302
330
|
waiting: card.state === "active" && card.task !== null && (stage?.kind === "task" || stage?.kind === "report")
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One behaviour the app runs for the page on screen instead of a page script string (docs/plans/task-page-in-app.md).
|
|
3
|
+
* `answer`: a question's inline answer (render-tasks.ts decisionAnswerScript). `region`: a server-drawn region kept
|
|
4
|
+
* current from a build's own fragment (chrome.ts regionScript), `/r/<run>?fragment=check|peek`. `result`: reviewing the
|
|
5
|
+
* result the page shows (result-review.ts RESULT_REVIEW_SCRIPT with render-results.ts reviewEvidenceScript). No code,
|
|
6
|
+
* selector or address travels here: the app runs only the kinds it knows.
|
|
7
|
+
*/
|
|
8
|
+
export type PageWork = {
|
|
9
|
+
kind: "answer";
|
|
10
|
+
} | {
|
|
11
|
+
kind: "result";
|
|
12
|
+
} | {
|
|
13
|
+
kind: "region";
|
|
14
|
+
region: "check-progress" | "run-peek";
|
|
15
|
+
run: number;
|
|
16
|
+
};
|
|
17
|
+
/** The work the app and the server agree on. A workspace read says it with `x-toolroll-work`; a reader that does not
|
|
18
|
+
* say exactly this version (an older tab) gets the page as a document with its script string, as before. A hint, never
|
|
19
|
+
* authority. It moves whenever a kind is added: 2 added `result`. */
|
|
20
|
+
export declare const PAGE_WORK_VERSION = 2;
|
|
21
|
+
export declare const PAGE_WORK_HEADER = "x-toolroll-work";
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** The work the app and the server agree on. A workspace read says it with `x-toolroll-work`; a reader that does not
|
|
2
|
+
* say exactly this version (an older tab) gets the page as a document with its script string, as before. A hint, never
|
|
3
|
+
* authority. It moves whenever a kind is added: 2 added `result`. */
|
|
4
|
+
export const PAGE_WORK_VERSION = 2;
|
|
5
|
+
export const PAGE_WORK_HEADER = "x-toolroll-work";
|
|
@@ -22,19 +22,19 @@ export type KnowledgeView = ReturnType<typeof knowledgeView>;
|
|
|
22
22
|
export declare function knowledgeVersion(store: Store, repo: string, actor: string, revision: number): Knowledge;
|
|
23
23
|
/** Chat reads only an admitted project. Keep the full source in the project UI. */
|
|
24
24
|
export declare function conversationKnowledge(store: Store, repo: string, actor: string, reference?: string, decision?: number): {
|
|
25
|
-
|
|
25
|
+
title?: never;
|
|
26
|
+
content?: never;
|
|
26
27
|
revision: number;
|
|
27
28
|
stale: boolean;
|
|
28
29
|
notice: string;
|
|
29
|
-
|
|
30
|
-
content?: never;
|
|
30
|
+
history?: never;
|
|
31
31
|
instructions?: never;
|
|
32
32
|
references?: never;
|
|
33
33
|
decisions?: never;
|
|
34
34
|
waiting?: never;
|
|
35
35
|
} | {
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
title?: never;
|
|
37
|
+
content?: never;
|
|
38
38
|
stale?: never;
|
|
39
39
|
decision: {
|
|
40
40
|
repo: string;
|
|
@@ -52,27 +52,27 @@ export declare function conversationKnowledge(store: Store, repo: string, actor:
|
|
|
52
52
|
};
|
|
53
53
|
afterwards?: string;
|
|
54
54
|
notice: string;
|
|
55
|
-
|
|
56
|
-
|
|
55
|
+
revision?: never;
|
|
56
|
+
history?: never;
|
|
57
57
|
instructions?: never;
|
|
58
58
|
references?: never;
|
|
59
59
|
decisions?: never;
|
|
60
60
|
waiting?: never;
|
|
61
61
|
} | {
|
|
62
|
-
history?: never;
|
|
63
62
|
stale?: never;
|
|
64
63
|
revision: number;
|
|
65
64
|
title: string;
|
|
66
65
|
content: string;
|
|
67
66
|
notice: string;
|
|
67
|
+
history?: never;
|
|
68
68
|
instructions?: never;
|
|
69
69
|
references?: never;
|
|
70
70
|
decisions?: never;
|
|
71
71
|
waiting?: never;
|
|
72
72
|
} | {
|
|
73
|
-
stale?: never;
|
|
74
73
|
title?: never;
|
|
75
74
|
content?: never;
|
|
75
|
+
stale?: never;
|
|
76
76
|
revision: number;
|
|
77
77
|
history: {
|
|
78
78
|
revision: number;
|
|
@@ -1,22 +1,8 @@
|
|
|
1
1
|
import { type PullRequestReader, type ReleaseCard, type ReleaseSeal, type TermLine } from "./release-card.js";
|
|
2
2
|
import { type ApproveResult, type Scope } from "./scope.js";
|
|
3
3
|
import { type Notification, type Store } from "./store.js";
|
|
4
|
-
|
|
5
|
-
export
|
|
6
|
-
/** One check's newest attempt, as the coordinator's own gh read it. A re-run is a new check run id. */
|
|
7
|
-
export type ReleaseCiRun = {
|
|
8
|
-
id: number | null;
|
|
9
|
-
name: string;
|
|
10
|
-
status: string;
|
|
11
|
-
conclusion: string | null;
|
|
12
|
-
};
|
|
13
|
-
export type ReleaseCiSnapshot = {
|
|
14
|
-
candidate: string;
|
|
15
|
-
at: string;
|
|
16
|
-
state: "passed" | "running" | "failed";
|
|
17
|
-
failed: string[];
|
|
18
|
-
runs: ReleaseCiRun[];
|
|
19
|
-
};
|
|
4
|
+
import { type ReleaseCiSnapshot } from "./release-refusal.js";
|
|
5
|
+
export { ciWords, releaseApprovalRefusal, releaseCiOf, RELEASE_CI_SEEN_ACTION, type ReleaseCiRun, type ReleaseCiSnapshot } from "./release-refusal.js";
|
|
20
6
|
/**
|
|
21
7
|
* Record what the coordinator just read of the pull request's CI on `candidate` (its verdict and every check's newest
|
|
22
8
|
* attempt), when it differs from the last reading recorded: the sheet and the CLI show it, and the card digest binds
|
|
@@ -35,13 +21,9 @@ export declare function recordReleaseCi(store: Store, input: {
|
|
|
35
21
|
}[];
|
|
36
22
|
now: Date;
|
|
37
23
|
}): boolean;
|
|
38
|
-
/** The coordinator's latest CI reading for this task on exactly `candidate`, or null (not read yet, or another commit). */
|
|
39
|
-
export declare function releaseCiOf(store: Store, taskId: string, candidate: string): ReleaseCiSnapshot | null;
|
|
40
24
|
/** What of a CI reading the card binds: its state and each check's attempt, whether it finished, and how. Queued and
|
|
41
25
|
* in progress read alike (neither is finished); the time it was read is not bound. */
|
|
42
26
|
export declare function ciBinding(ci: ReleaseCiSnapshot | null): unknown;
|
|
43
|
-
/** GitHub CI in one line, as the sheet and the CLI say it. */
|
|
44
|
-
export declare function ciWords(ci: ReleaseCiSnapshot | null, candidate: string): string;
|
|
45
27
|
export type ReleaseApprovalCard = {
|
|
46
28
|
taskId: string;
|
|
47
29
|
taskRef: number;
|
|
@@ -189,3 +171,10 @@ export declare function waitingJournals(stateDir: string, expectedFile: (branch:
|
|
|
189
171
|
journals: WaitingJournal[];
|
|
190
172
|
unreadable: string[];
|
|
191
173
|
};
|
|
174
|
+
/** A release as Home names it (docs/plans/home-other-asks.md, part 1): its version, and why it can't take a yes now
|
|
175
|
+
* in the sheet's own words (`refusalOf`, the one source), or null when this isn't a release. Reads only; Home never
|
|
176
|
+
* approves one, it links to the sheet. */
|
|
177
|
+
export declare function releaseAskOf(store: Store, taskId: string): {
|
|
178
|
+
version: string;
|
|
179
|
+
refusal: string | null;
|
|
180
|
+
} | null;
|
package/dist/release-approval.js
CHANGED
|
@@ -19,20 +19,19 @@ import { lstatSync, readdirSync, readFileSync, realpathSync } from "node:fs";
|
|
|
19
19
|
import { join } from "node:path";
|
|
20
20
|
import { permissionPlainWords } from "./chat-terms.js";
|
|
21
21
|
import { approvalTermsOf, releaseAccessWords, releaseCardOf, releaseSealOf } from "./release-card.js";
|
|
22
|
-
import { releaseCiHoldOf, releaseCiHoldWords } from "./release-ci-hold.js";
|
|
23
22
|
import { approvalOf } from "./scope.js";
|
|
24
23
|
import { releaseProgressFor } from "./store.js";
|
|
25
24
|
import { taskArchived } from "./task-archive.js";
|
|
26
25
|
import { cleanTerminalLine } from "./terminal-text.js";
|
|
26
|
+
import { ciName, ciWords, refusalOf, releaseCiOf, RELEASE_CI_SEEN_ACTION } from "./release-refusal.js";
|
|
27
|
+
// The CI reading and the refusals live in release-refusal.ts, so scope.ts approve() can read them without a cycle.
|
|
28
|
+
export { ciWords, releaseApprovalRefusal, releaseCiOf, RELEASE_CI_SEEN_ACTION } from "./release-refusal.js";
|
|
27
29
|
const SHA = /^[0-9a-f]{40}$/;
|
|
28
30
|
const DIGEST = /^[0-9a-f]{64}$/;
|
|
29
31
|
/** A scope digest as scope.ts writes it. */
|
|
30
32
|
const SCOPE_DIGEST = /^[0-9a-f]{32,64}$/;
|
|
31
33
|
const sha256 = (text) => createHash("sha256").update(text).digest("hex");
|
|
32
34
|
// ---- GitHub CI as the coordinator read it -------------------------------------------------------------------------
|
|
33
|
-
/** The ledger act holding the coordinator's latest reading of the pull request's CI on the exact candidate. */
|
|
34
|
-
export const RELEASE_CI_SEEN_ACTION = "release CI observed";
|
|
35
|
-
const ciName = (value) => cleanTerminalLine(String(value ?? "")).trim().slice(0, 120);
|
|
36
35
|
/**
|
|
37
36
|
* Record what the coordinator just read of the pull request's CI on `candidate` (its verdict and every check's newest
|
|
38
37
|
* attempt), when it differs from the last reading recorded: the sheet and the CLI show it, and the card digest binds
|
|
@@ -58,60 +57,11 @@ export function recordReleaseCi(store, input) {
|
|
|
58
57
|
action: RELEASE_CI_SEEN_ACTION, outcome: input.state, source: "work", detail });
|
|
59
58
|
return true;
|
|
60
59
|
}
|
|
61
|
-
/** The coordinator's latest CI reading for this task on exactly `candidate`, or null (not read yet, or another commit). */
|
|
62
|
-
export function releaseCiOf(store, taskId, candidate) {
|
|
63
|
-
const row = store.handle.prepare("SELECT detail, at FROM action_ledger WHERE task_id = ? AND action = ? ORDER BY id DESC LIMIT 1").get(taskId, RELEASE_CI_SEEN_ACTION);
|
|
64
|
-
if (row === undefined)
|
|
65
|
-
return null;
|
|
66
|
-
let parsed;
|
|
67
|
-
try {
|
|
68
|
-
parsed = JSON.parse(String(row["detail"]));
|
|
69
|
-
}
|
|
70
|
-
catch {
|
|
71
|
-
return null;
|
|
72
|
-
}
|
|
73
|
-
const one = parsed;
|
|
74
|
-
if (one.v !== 1 || one.candidate !== candidate || !["passed", "running", "failed"].includes(String(one.state)) || !Array.isArray(one.failed) || !Array.isArray(one.runs))
|
|
75
|
-
return null;
|
|
76
|
-
return { candidate, at: String(row["at"]), state: one.state, failed: one.failed.map(ciName),
|
|
77
|
-
runs: one.runs.map(run => { const r = run; return { id: typeof r["id"] === "number" ? r["id"] : null, name: ciName(r["name"]), status: ciName(r["status"]), conclusion: r["conclusion"] == null ? null : ciName(r["conclusion"]) }; }) };
|
|
78
|
-
}
|
|
79
60
|
/** What of a CI reading the card binds: its state and each check's attempt, whether it finished, and how. Queued and
|
|
80
61
|
* in progress read alike (neither is finished); the time it was read is not bound. */
|
|
81
62
|
export function ciBinding(ci) {
|
|
82
63
|
return ci === null ? null : { candidate: ci.candidate, state: ci.state, failed: ci.failed, runs: ci.runs.map(run => [run.id, run.name, run.status === "completed", run.conclusion]) };
|
|
83
64
|
}
|
|
84
|
-
/** GitHub CI in one line, as the sheet and the CLI say it. */
|
|
85
|
-
export function ciWords(ci, candidate) {
|
|
86
|
-
const short = candidate.slice(0, 7);
|
|
87
|
-
if (ci === null)
|
|
88
|
-
return `GitHub CI on ${short} hasn't been read yet.`;
|
|
89
|
-
if (ci.state === "passed")
|
|
90
|
-
return `GitHub CI passed on ${short}.`;
|
|
91
|
-
if (ci.state === "failed")
|
|
92
|
-
return `GitHub CI failed on ${short}: ${ci.failed.join(", ") || "a check"}.`;
|
|
93
|
-
const finished = ci.runs.filter(run => run.status === "completed").length;
|
|
94
|
-
return `GitHub CI is running on ${short}: ${finished} of ${ci.runs.length} checks finished.`;
|
|
95
|
-
}
|
|
96
|
-
/** The release check failed on this task already (the coordinator's record): nobody approves it again here. */
|
|
97
|
-
function checkFailed(store, taskId) {
|
|
98
|
-
return store.handle.prepare("SELECT 1 FROM release_step WHERE task_id = ? AND step = 'check' AND state = 'failed' LIMIT 1").get(taskId) !== undefined;
|
|
99
|
-
}
|
|
100
|
-
/** Why a release can't be approved now, from the plane's own state, or null. */
|
|
101
|
-
function refusalOf(store, taskId, scope, ci) {
|
|
102
|
-
if (taskArchived(store, taskId))
|
|
103
|
-
return "This release is put away. Restore it before approving.";
|
|
104
|
-
const hold = releaseCiHoldOf(store, taskId);
|
|
105
|
-
if (hold !== null)
|
|
106
|
-
return releaseCiHoldWords(hold);
|
|
107
|
-
if (ci?.state === "failed")
|
|
108
|
-
return `${ciWords(ci, scope.candidate)} Fix the branch or rerun the failed job on GitHub, then release again.`;
|
|
109
|
-
if (checkFailed(store, taskId))
|
|
110
|
-
return "The release check already failed on this commit. Fix the branch and release the new commit.";
|
|
111
|
-
if (scope.approvedAt !== null && scope.approvedDigest !== scope.digest)
|
|
112
|
-
return "The release's terms changed after an earlier approval. Read them again.";
|
|
113
|
-
return null;
|
|
114
|
-
}
|
|
115
65
|
/** A release task from the plane's own state: a sealed form (terms or a release check's purpose) whose repository is the
|
|
116
66
|
* task's project and whose approved commit is the scope's candidate. Never from a title or a task id. */
|
|
117
67
|
export function releaseTaskOf(store, taskId) {
|
|
@@ -380,3 +330,14 @@ export function waitingJournals(stateDir, expectedFile) {
|
|
|
380
330
|
}
|
|
381
331
|
return { journals, unreadable };
|
|
382
332
|
}
|
|
333
|
+
/** A release as Home names it (docs/plans/home-other-asks.md, part 1): its version, and why it can't take a yes now
|
|
334
|
+
* in the sheet's own words (`refusalOf`, the one source), or null when this isn't a release. Reads only; Home never
|
|
335
|
+
* approves one, it links to the sheet. */
|
|
336
|
+
export function releaseAskOf(store, taskId) {
|
|
337
|
+
const found = releaseTaskOf(store, taskId);
|
|
338
|
+
if (found === null)
|
|
339
|
+
return null;
|
|
340
|
+
const { scope, seal } = found;
|
|
341
|
+
const refusal = approvalOf(scope).approved ? null : refusalOf(store, taskId, scope, releaseCiOf(store, taskId, scope.candidate));
|
|
342
|
+
return { version: seal.identity.packageVersion, refusal };
|
|
343
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { Scope } from "./scope.js";
|
|
2
|
+
import type { Store } from "./store.js";
|
|
3
|
+
/** The ledger act holding the coordinator's latest reading of the pull request's CI on the exact candidate. */
|
|
4
|
+
export declare const RELEASE_CI_SEEN_ACTION = "release CI observed";
|
|
5
|
+
/** One check's newest attempt, as the coordinator's own gh read it. A re-run is a new check run id. */
|
|
6
|
+
export type ReleaseCiRun = {
|
|
7
|
+
id: number | null;
|
|
8
|
+
name: string;
|
|
9
|
+
status: string;
|
|
10
|
+
conclusion: string | null;
|
|
11
|
+
};
|
|
12
|
+
export type ReleaseCiSnapshot = {
|
|
13
|
+
candidate: string;
|
|
14
|
+
at: string;
|
|
15
|
+
state: "passed" | "running" | "failed";
|
|
16
|
+
failed: string[];
|
|
17
|
+
runs: ReleaseCiRun[];
|
|
18
|
+
};
|
|
19
|
+
export declare const ciName: (value: unknown) => string;
|
|
20
|
+
/** The coordinator's latest CI reading for this task on exactly `candidate`, or null (not read yet, or another commit). */
|
|
21
|
+
export declare function releaseCiOf(store: Store, taskId: string, candidate: string): ReleaseCiSnapshot | null;
|
|
22
|
+
/** GitHub CI in one line, as the sheet and the CLI say it. */
|
|
23
|
+
export declare function ciWords(ci: ReleaseCiSnapshot | null, candidate: string): string;
|
|
24
|
+
/** Why a release can't be approved now, from the plane's own state, or null. */
|
|
25
|
+
export declare function refusalOf(store: Store, taskId: string, scope: Scope, ci: ReleaseCiSnapshot | null): string | null;
|
|
26
|
+
/**
|
|
27
|
+
* The release refusals every approval road shares (scope.ts approve(), read inside its transaction): for a scope
|
|
28
|
+
* sealing release terms or a release check's purpose, the sheet's own reasons in the sheet's own words, so the task
|
|
29
|
+
* page, Home's in-place yes, chat and `toolroll task approve` refuse exactly what `/t/<id>/release` refuses. Null for
|
|
30
|
+
* every other scope, and for a release nothing stops. The sheet's card digest binding stays the sheet's.
|
|
31
|
+
*/
|
|
32
|
+
export declare function releaseApprovalRefusal(store: Store, taskId: string, scope: Scope): string | null;
|