toolroll 0.9.59 → 0.9.60
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 +11 -0
- package/README.md +8 -6
- package/dist/assignment-adapters.d.ts +1 -1
- package/dist/assignment-adapters.js +1 -1
- package/dist/assignment-delivery.d.ts +16 -0
- package/dist/assignment-delivery.js +54 -28
- package/dist/assignment-status.d.ts +13 -4
- package/dist/assignment-status.js +70 -39
- package/dist/assignment.d.ts +6 -2
- package/dist/assignment.js +13 -8
- package/dist/bin.js +11 -2
- package/dist/brief-fence.d.ts +9 -0
- package/dist/brief-fence.js +22 -0
- package/dist/browser/workspace.css +2 -2
- package/dist/browser/workspace.js +19 -19
- package/dist/builder.js +74 -25
- package/dist/chat-channel.js +1 -1
- package/dist/cli-fast.d.ts +8 -0
- package/dist/cli-fast.js +18 -0
- package/dist/cli-help.d.ts +5 -0
- package/dist/cli-help.js +91 -0
- package/dist/cli.d.ts +6 -5
- package/dist/cli.js +17 -96
- package/dist/design/tokens.css +234 -0
- package/dist/discord.js +2 -1
- package/dist/evidence.d.ts +22 -0
- package/dist/evidence.js +76 -17
- package/dist/flow-actions.js +2 -1
- package/dist/guides.js +14 -11
- package/dist/lead-cli.js +1 -1
- package/dist/lead-contract.js +1 -1
- package/dist/lead-doors.js +5 -2
- package/dist/lead-follow.d.ts +2 -0
- package/dist/lead-follow.js +138 -88
- package/dist/lead-tools.js +9 -2
- package/dist/mailbox.js +3 -2
- package/dist/mcp-core.js +1 -1
- package/dist/operate.js +17 -7
- package/dist/provider.d.ts +8 -0
- package/dist/provider.js +12 -1
- package/dist/serve.js +3 -2
- package/dist/server/chrome.d.ts +6 -3
- package/dist/server/chrome.js +8 -4
- package/dist/server/console.css +9 -64
- package/dist/server/render-chat.js +2 -2
- package/dist/server/render-tasks.d.ts +2 -0
- package/dist/server/render-tasks.js +11 -13
- package/dist/server/task-views.js +1 -0
- package/dist/slack-socket.d.ts +12 -0
- package/dist/slack-socket.js +29 -0
- package/dist/slack.d.ts +0 -7
- package/dist/slack.js +2 -24
- package/dist/steer-mailbox.d.ts +39 -0
- package/dist/steer-mailbox.js +207 -0
- package/dist/steer-status.d.ts +19 -0
- package/dist/steer-status.js +30 -0
- package/dist/store.d.ts +41 -2
- package/dist/store.js +95 -3
- package/dist/surface.js +2 -2
- package/dist/sweep-gate.d.ts +10 -0
- package/dist/sweep-gate.js +18 -0
- package/dist/team-updates.js +3 -3
- package/dist/toolroll-update.js +14 -2
- package/package.json +21 -21
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/* @generated by youi from the Signal design language v1.0.0. Do not edit: change the tokens in design-language and run `youi sync`. */
|
|
2
|
+
/* Built with Terrazzo (DTCG 2025.10 resolver). */
|
|
3
|
+
:root {
|
|
4
|
+
color-scheme: light;
|
|
5
|
+
--so-accent: #171717;
|
|
6
|
+
--so-accent-hover: #383838;
|
|
7
|
+
--so-accent-text: #171717;
|
|
8
|
+
--so-attention: var(--so-signal);
|
|
9
|
+
--so-attention-soft: var(--so-signal-soft);
|
|
10
|
+
--so-code-bg: #fafafa;
|
|
11
|
+
/* Vermilion: failure and irreversible acts. */
|
|
12
|
+
--so-danger: #c4320a;
|
|
13
|
+
--so-danger-soft: #feebe7;
|
|
14
|
+
--so-ease-out: cubic-bezier(0.23, 1, 0.32, 1);
|
|
15
|
+
--so-font-mono: "Geist Mono", ui-monospace, "SF Mono", "SFMono-Regular", "Menlo", "Consolas", monospace;
|
|
16
|
+
--so-font-sans: "Geist", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
17
|
+
/* Frame Grey: the frame the sidebar sits on, and the page background of server pages. */
|
|
18
|
+
--so-ground: #efefef;
|
|
19
|
+
/* Live Blue: something is running right now. */
|
|
20
|
+
--so-info: #0d74ce;
|
|
21
|
+
--so-info-soft: #e6f4fe;
|
|
22
|
+
/* Ink: all body text, and the colour of every act: primary buttons, the New task button, switches and radio cards when on. */
|
|
23
|
+
--so-ink: #171717;
|
|
24
|
+
/* Control Line: the boundary of fields, the composer, and the switch track when off. */
|
|
25
|
+
--so-input-line: #d4d4d4;
|
|
26
|
+
/* Hairline: every divider. */
|
|
27
|
+
--so-line: #e6e6e6;
|
|
28
|
+
--so-live: #0d74ce;
|
|
29
|
+
/* Muted: meta lines, fact keys, ghost buttons, placeholders, idle tab counts. */
|
|
30
|
+
--so-muted: #666666;
|
|
31
|
+
--so-nav-current: #ffffff;
|
|
32
|
+
--so-nav-current-ink: #171717;
|
|
33
|
+
--so-nav-hover: #e4e4e4;
|
|
34
|
+
/* Nav Ink: idle sidebar rows and their hover fill on the frame. */
|
|
35
|
+
--so-nav-ink: #525252;
|
|
36
|
+
--so-neutral-ink: #525252;
|
|
37
|
+
/* Neutral Badge: the badge for anything that is a fact and not a claim on the person, such as queued, running or built today in a project peek, or a count in a fold. */
|
|
38
|
+
--so-neutral-soft: #f0f0f0;
|
|
39
|
+
--so-on-accent: #ffffff;
|
|
40
|
+
--so-on-attention: var(--so-on-signal);
|
|
41
|
+
--so-on-signal: #ffffff;
|
|
42
|
+
--so-overlay: rgb(0 0 0 / .32);
|
|
43
|
+
/* Paper: every sheet, card, field, menu and dialog. */
|
|
44
|
+
--so-paper: #ffffff;
|
|
45
|
+
--so-phone-block: 14px;
|
|
46
|
+
--so-phone-card: 12px;
|
|
47
|
+
--so-phone-gap: 12px;
|
|
48
|
+
--so-phone-gutter: 16px;
|
|
49
|
+
--so-phone-row: 8px;
|
|
50
|
+
--so-pill-shadow: 0px 0px 0px 1px rgb(0 0 0 / .06), 0px 1px 2px 0px rgb(0 0 0 / .06);
|
|
51
|
+
--so-radius-badge: 5px;
|
|
52
|
+
--so-radius-card: 10px;
|
|
53
|
+
--so-radius-full: 9999px;
|
|
54
|
+
--so-radius-md: 8px;
|
|
55
|
+
--so-radius-sheet: 12px;
|
|
56
|
+
--so-radius-sm: 6px;
|
|
57
|
+
/* Raised: segmented-tab tracks, notices, row hover fills, and the muted well behind a "next step" block or a neutral problem line. */
|
|
58
|
+
--so-raised: #f5f5f5;
|
|
59
|
+
--so-scroll: #cfcfcf;
|
|
60
|
+
/* Selection: text selection highlight, with ink text on it. */
|
|
61
|
+
--so-selection: #d4d4d4;
|
|
62
|
+
--so-shadow-overlay: 0px 0px 0px 1px rgb(0 0 0 / .08), 0px 24px 48px -12px rgb(0 0 0 / .22);
|
|
63
|
+
--so-sheet-shadow: 0px 0px 0px 1px rgb(0 0 0 / .06), 0px 1px 2px 0px rgb(0 0 0 / .04), 0px 4px 12px -6px rgb(0 0 0 / .06);
|
|
64
|
+
--so-sidebar: #efefef;
|
|
65
|
+
/* Ink accent: the default accent, used for what waits on a person. */
|
|
66
|
+
--so-signal: #171717;
|
|
67
|
+
--so-signal-hover: #383838;
|
|
68
|
+
/* Ink wash: the fill behind attention badge text and the 3px focus halo around a focused field or composer — one step stronger than the neutral badge fill, so a waiting badge still reads first. */
|
|
69
|
+
--so-signal-soft: #e8e8e8;
|
|
70
|
+
/* Soft: hover fills on outline and ghost buttons, menu items and suggestions. */
|
|
71
|
+
--so-soft: #f2f2f2;
|
|
72
|
+
--so-space-2xl: 24px;
|
|
73
|
+
--so-space-3xl: 28px;
|
|
74
|
+
--so-space-lg: 16px;
|
|
75
|
+
--so-space-md: 12px;
|
|
76
|
+
--so-space-sm: 8px;
|
|
77
|
+
--so-space-xl: 20px;
|
|
78
|
+
--so-space-xs: 4px;
|
|
79
|
+
/* Built Green: done and passed, used for "Checks passed", "Open now" and a finished live step. */
|
|
80
|
+
--so-success: #218358;
|
|
81
|
+
--so-success-soft: #e6f6eb;
|
|
82
|
+
--so-typography-badge: var(--so-typography-badge-font-weight) var(--so-typography-badge-font-size)/var(--so-typography-badge-line-height) var(--so-typography-badge-font-family);
|
|
83
|
+
--so-typography-badge-font-family: var(--so-font-sans);
|
|
84
|
+
--so-typography-badge-font-size: 11.5px;
|
|
85
|
+
--so-typography-badge-font-weight: 500;
|
|
86
|
+
--so-typography-badge-line-height: 1.5652;
|
|
87
|
+
--so-typography-body: var(--so-typography-body-font-weight) var(--so-typography-body-font-size)/var(--so-typography-body-line-height) var(--so-typography-body-font-family);
|
|
88
|
+
--so-typography-body-font-family: var(--so-font-sans);
|
|
89
|
+
--so-typography-body-font-size: 13px;
|
|
90
|
+
--so-typography-body-font-weight: 400;
|
|
91
|
+
--so-typography-body-line-height: 1.5;
|
|
92
|
+
--so-typography-body-reading: var(--so-typography-body-reading-font-weight) var(--so-typography-body-reading-font-size)/var(--so-typography-body-reading-line-height) var(--so-typography-body-reading-font-family);
|
|
93
|
+
--so-typography-body-reading-font-family: var(--so-font-sans);
|
|
94
|
+
--so-typography-body-reading-font-size: 14px;
|
|
95
|
+
--so-typography-body-reading-font-weight: 400;
|
|
96
|
+
--so-typography-body-reading-line-height: 1.7;
|
|
97
|
+
--so-typography-caption: var(--so-typography-caption-font-weight) var(--so-typography-caption-font-size)/var(--so-typography-caption-line-height) var(--so-typography-caption-font-family);
|
|
98
|
+
--so-typography-caption-font-family: var(--so-font-sans);
|
|
99
|
+
--so-typography-caption-font-size: 12px;
|
|
100
|
+
--so-typography-caption-font-weight: 500;
|
|
101
|
+
--so-typography-caption-line-height: 1.5;
|
|
102
|
+
--so-typography-display: var(--so-typography-display-font-weight) var(--so-typography-display-font-size)/var(--so-typography-display-line-height) var(--so-typography-display-font-family);
|
|
103
|
+
--so-typography-display-font-family: var(--so-font-sans);
|
|
104
|
+
--so-typography-display-font-size: 26px;
|
|
105
|
+
--so-typography-display-font-weight: 600;
|
|
106
|
+
--so-typography-display-letter-spacing: -0.65px;
|
|
107
|
+
--so-typography-display-line-height: 1.25;
|
|
108
|
+
--so-typography-headline: var(--so-typography-headline-font-weight) var(--so-typography-headline-font-size)/var(--so-typography-headline-line-height) var(--so-typography-headline-font-family);
|
|
109
|
+
--so-typography-headline-font-family: var(--so-font-sans);
|
|
110
|
+
--so-typography-headline-font-size: 18px;
|
|
111
|
+
--so-typography-headline-font-weight: 600;
|
|
112
|
+
--so-typography-headline-line-height: 1.375;
|
|
113
|
+
--so-typography-label: var(--so-typography-label-font-weight) var(--so-typography-label-font-size)/var(--so-typography-label-line-height) var(--so-typography-label-font-family);
|
|
114
|
+
--so-typography-label-font-family: var(--so-font-sans);
|
|
115
|
+
--so-typography-label-font-size: 13px;
|
|
116
|
+
--so-typography-label-font-weight: 500;
|
|
117
|
+
--so-typography-label-line-height: 1.2;
|
|
118
|
+
--so-typography-mono: var(--so-typography-mono-font-weight) var(--so-typography-mono-font-size)/var(--so-typography-mono-line-height) var(--so-typography-mono-font-family);
|
|
119
|
+
--so-typography-mono-count: var(--so-typography-mono-count-font-weight) var(--so-typography-mono-count-font-size)/var(--so-typography-mono-count-line-height) var(--so-typography-mono-count-font-family);
|
|
120
|
+
--so-typography-mono-count-font-family: var(--so-font-mono);
|
|
121
|
+
--so-typography-mono-count-font-size: 11px;
|
|
122
|
+
--so-typography-mono-count-font-weight: 500;
|
|
123
|
+
--so-typography-mono-count-line-height: 1;
|
|
124
|
+
--so-typography-mono-font-family: var(--so-font-mono);
|
|
125
|
+
--so-typography-mono-font-size: 12px;
|
|
126
|
+
--so-typography-mono-font-weight: 400;
|
|
127
|
+
--so-typography-mono-line-height: 1.5;
|
|
128
|
+
--so-typography-title: var(--so-typography-title-font-weight) var(--so-typography-title-font-size)/var(--so-typography-title-line-height) var(--so-typography-title-font-family);
|
|
129
|
+
--so-typography-title-font-family: var(--so-font-sans);
|
|
130
|
+
--so-typography-title-font-size: 15px;
|
|
131
|
+
--so-typography-title-font-weight: 600;
|
|
132
|
+
--so-typography-title-letter-spacing: -0.15px;
|
|
133
|
+
--so-typography-title-line-height: 1.4;
|
|
134
|
+
--so-typography-title-sm: var(--so-typography-title-sm-font-weight) var(--so-typography-title-sm-font-size)/var(--so-typography-title-sm-line-height) var(--so-typography-title-sm-font-family);
|
|
135
|
+
--so-typography-title-sm-font-family: var(--so-font-sans);
|
|
136
|
+
--so-typography-title-sm-font-size: 14px;
|
|
137
|
+
--so-typography-title-sm-font-weight: 600;
|
|
138
|
+
--so-typography-title-sm-letter-spacing: -0.14px;
|
|
139
|
+
--so-typography-title-sm-line-height: 1.3;
|
|
140
|
+
--so-user-bubble: #f2f2f2;
|
|
141
|
+
/* Amber: setup trouble and caution. */
|
|
142
|
+
--so-warning: #ab6400;
|
|
143
|
+
--so-warning-soft: #fff4d5;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
@media (prefers-color-scheme: dark) {
|
|
147
|
+
:root:not([data-theme="light"]) {
|
|
148
|
+
color-scheme: dark;
|
|
149
|
+
--so-accent: #ededed;
|
|
150
|
+
--so-accent-hover: #ffffff;
|
|
151
|
+
--so-accent-text: #ededed;
|
|
152
|
+
--so-code-bg: #111111;
|
|
153
|
+
--so-danger: #ff977d;
|
|
154
|
+
--so-danger-soft: rgb(255 151 125 / .12);
|
|
155
|
+
--so-ground: #0b0b0b;
|
|
156
|
+
--so-info: #70b8ff;
|
|
157
|
+
--so-info-soft: rgb(112 184 255 / .12);
|
|
158
|
+
--so-ink: #ededed;
|
|
159
|
+
--so-input-line: #363636;
|
|
160
|
+
--so-line: #262626;
|
|
161
|
+
--so-live: #70b8ff;
|
|
162
|
+
--so-muted: #a1a1a1;
|
|
163
|
+
--so-nav-current: #1f1f1f;
|
|
164
|
+
--so-nav-current-ink: #ededed;
|
|
165
|
+
--so-nav-hover: #171717;
|
|
166
|
+
--so-nav-ink: #a1a1a1;
|
|
167
|
+
--so-neutral-ink: #b4b4b4;
|
|
168
|
+
--so-neutral-soft: #1f1f1f;
|
|
169
|
+
--so-on-accent: #0a0a0a;
|
|
170
|
+
--so-on-signal: #0a0a0a;
|
|
171
|
+
--so-overlay: rgb(0 0 0 / .6);
|
|
172
|
+
--so-paper: #161616;
|
|
173
|
+
--so-pill-shadow: 0px 0px 0px 1px #2a2a2a, 0px 1px 2px 0px rgb(0 0 0 / .5);
|
|
174
|
+
--so-raised: #1c1c1c;
|
|
175
|
+
--so-scroll: #333333;
|
|
176
|
+
--so-selection: #3a3a3a;
|
|
177
|
+
--so-shadow-overlay: 0px 0px 0px 1px #2e2e2e, 0px 24px 48px -12px rgb(0 0 0 / .7);
|
|
178
|
+
--so-sheet-shadow: 0px 0px 0px 1px #262626, 0px 1px 2px 0px rgb(0 0 0 / .4);
|
|
179
|
+
--so-sidebar: #0b0b0b;
|
|
180
|
+
--so-signal: #ededed;
|
|
181
|
+
--so-signal-hover: #ffffff;
|
|
182
|
+
--so-signal-soft: #2e2e2e;
|
|
183
|
+
--so-soft: #1f1f1f;
|
|
184
|
+
--so-success: #3dd68c;
|
|
185
|
+
--so-success-soft: rgb(61 214 140 / .12);
|
|
186
|
+
--so-user-bubble: #202020;
|
|
187
|
+
--so-warning: #ffca16;
|
|
188
|
+
--so-warning-soft: rgb(255 202 22 / .12);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
:root[data-theme="dark"] {
|
|
193
|
+
color-scheme: dark;
|
|
194
|
+
--so-accent: #ededed;
|
|
195
|
+
--so-accent-hover: #ffffff;
|
|
196
|
+
--so-accent-text: #ededed;
|
|
197
|
+
--so-code-bg: #111111;
|
|
198
|
+
--so-danger: #ff977d;
|
|
199
|
+
--so-danger-soft: rgb(255 151 125 / .12);
|
|
200
|
+
--so-ground: #0b0b0b;
|
|
201
|
+
--so-info: #70b8ff;
|
|
202
|
+
--so-info-soft: rgb(112 184 255 / .12);
|
|
203
|
+
--so-ink: #ededed;
|
|
204
|
+
--so-input-line: #363636;
|
|
205
|
+
--so-line: #262626;
|
|
206
|
+
--so-live: #70b8ff;
|
|
207
|
+
--so-muted: #a1a1a1;
|
|
208
|
+
--so-nav-current: #1f1f1f;
|
|
209
|
+
--so-nav-current-ink: #ededed;
|
|
210
|
+
--so-nav-hover: #171717;
|
|
211
|
+
--so-nav-ink: #a1a1a1;
|
|
212
|
+
--so-neutral-ink: #b4b4b4;
|
|
213
|
+
--so-neutral-soft: #1f1f1f;
|
|
214
|
+
--so-on-accent: #0a0a0a;
|
|
215
|
+
--so-on-signal: #0a0a0a;
|
|
216
|
+
--so-overlay: rgb(0 0 0 / .6);
|
|
217
|
+
--so-paper: #161616;
|
|
218
|
+
--so-pill-shadow: 0px 0px 0px 1px #2a2a2a, 0px 1px 2px 0px rgb(0 0 0 / .5);
|
|
219
|
+
--so-raised: #1c1c1c;
|
|
220
|
+
--so-scroll: #333333;
|
|
221
|
+
--so-selection: #3a3a3a;
|
|
222
|
+
--so-shadow-overlay: 0px 0px 0px 1px #2e2e2e, 0px 24px 48px -12px rgb(0 0 0 / .7);
|
|
223
|
+
--so-sheet-shadow: 0px 0px 0px 1px #262626, 0px 1px 2px 0px rgb(0 0 0 / .4);
|
|
224
|
+
--so-sidebar: #0b0b0b;
|
|
225
|
+
--so-signal: #ededed;
|
|
226
|
+
--so-signal-hover: #ffffff;
|
|
227
|
+
--so-signal-soft: #2e2e2e;
|
|
228
|
+
--so-soft: #1f1f1f;
|
|
229
|
+
--so-success: #3dd68c;
|
|
230
|
+
--so-success-soft: rgb(61 214 140 / .12);
|
|
231
|
+
--so-user-bubble: #202020;
|
|
232
|
+
--so-warning: #ffca16;
|
|
233
|
+
--so-warning-soft: rgb(255 202 22 / .12);
|
|
234
|
+
}
|
package/dist/discord.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
/** A single leased Discord Gateway connection, hosted by the ordinary worker. */
|
|
2
2
|
import { randomBytes } from "node:crypto";
|
|
3
3
|
import { setTimeout as sleep } from "node:timers/promises";
|
|
4
|
-
import { Client, GatewayIntentBits, Partials } from "discord.js";
|
|
5
4
|
import { loadDiscordCredentials, discordApi, object, discordId, } from "./discord-api.js";
|
|
6
5
|
import { ChatState, chatHash } from "./chat-delivery-state.js";
|
|
7
6
|
import { receiveDiscord, processDiscordEvent, deliverDiscordPart, planDiscordNotifications, planDiscordRooms, } from "./discord-chat.js";
|
|
@@ -39,6 +38,8 @@ export async function followDiscord(options) {
|
|
|
39
38
|
};
|
|
40
39
|
const problem = (message) => state.setProblem(credentials.installation, owner, message);
|
|
41
40
|
const api = discordApi(credentials.botToken);
|
|
41
|
+
// The SDK loads once Discord is set up, not with every command that could host the follower.
|
|
42
|
+
const { Client, GatewayIntentBits, Partials } = await import("discord.js");
|
|
42
43
|
const client = new Client({
|
|
43
44
|
// Guild messages and their content are needed for rooms; the Message
|
|
44
45
|
// Content intent must be enabled for the app in Discord's developer portal.
|
package/dist/evidence.d.ts
CHANGED
|
@@ -47,6 +47,10 @@ export declare const PROGRESS_PREFIX = "STANDING-ORDERS-PROGRESS-";
|
|
|
47
47
|
* (adaptive execution plans): a terminal file like park or proof, written
|
|
48
48
|
* at most once, read once at settlement. */
|
|
49
49
|
export declare const PROPOSAL_PREFIX = "STANDING-ORDERS-PROPOSAL-";
|
|
50
|
+
/** The running build's steering mailbox: a FOLDER, not a file. The pulse
|
|
51
|
+
* writes each operator note into it as NNN.json (a temp name, then a
|
|
52
|
+
* rename); the agent acknowledges a note by moving it into `handled/`. */
|
|
53
|
+
export declare const STEER_PREFIX = "STANDING-ORDERS-STEER-";
|
|
50
54
|
export declare const MAILBOX_SUFFIX = ".json";
|
|
51
55
|
/** v105: left in a run's evidence folder when the retention setting removed its files (retention.ts). */
|
|
52
56
|
export declare const RETENTION_NOTE = "REMOVED-BY-RETENTION";
|
|
@@ -78,6 +82,11 @@ export declare function looksLikeMailbox(name: string): boolean;
|
|
|
78
82
|
* The pre-rename NIGHTORDERS- prefix stays recognized here — a worktree cut
|
|
79
83
|
* down before 2026-08-13 may still hold one, and cleanup has no vintage. */
|
|
80
84
|
export declare function looksLikeProtocolFile(name: string): boolean;
|
|
85
|
+
/** A steering mailbox folder, or anything in it, as a directory listing or
|
|
86
|
+
* `git status` names it: an untracked folder is reported with a trailing
|
|
87
|
+
* slash, or file by file where a repository shows every untracked file. */
|
|
88
|
+
export declare function looksLikeSteerFolder(name: string): boolean;
|
|
89
|
+
export declare function steerFolderName(): string;
|
|
81
90
|
export declare function handoffName(): string;
|
|
82
91
|
export declare function planFileName(): string;
|
|
83
92
|
/** The build's optional proof manifest (Priority 2), named exactly like
|
|
@@ -208,6 +217,19 @@ export declare function readVerifiedArtifact(root: string, artifact: Artifact):
|
|
|
208
217
|
ok: false;
|
|
209
218
|
problem: string;
|
|
210
219
|
};
|
|
220
|
+
/**
|
|
221
|
+
* Whether an artifact's bytes still verify, without returning them. Status
|
|
222
|
+
* sweeps ask this of every artifact of every run, so a file proved earlier and
|
|
223
|
+
* not touched since (same artifact row, device, inode, size, mtime and ctime
|
|
224
|
+
* on the descriptor just opened) costs an fstat, not a re-read and SHA-256.
|
|
225
|
+
* Every path check still runs on each call; a changed or new file is hashed.
|
|
226
|
+
*/
|
|
227
|
+
export declare function verifyArtifact(root: string, artifact: Artifact): {
|
|
228
|
+
ok: true;
|
|
229
|
+
} | {
|
|
230
|
+
ok: false;
|
|
231
|
+
problem: string;
|
|
232
|
+
};
|
|
211
233
|
/** The file alone, no row — quarantines use this; they belong to no live run. */
|
|
212
234
|
export declare function writeEvidenceFile(root: string, runId: number, name: string, content: Buffer): string;
|
|
213
235
|
/**
|
package/dist/evidence.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* store holds keys relative to that root, never absolute paths.
|
|
15
15
|
*/
|
|
16
16
|
import { createHash, randomBytes } from "node:crypto";
|
|
17
|
-
import { closeSync, existsSync, constants, fstatSync, mkdirSync, openSync, readdirSync, readSync, realpathSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
17
|
+
import { closeSync, existsSync, constants, fstatSync, mkdirSync, openSync, readdirSync, readSync, realpathSync, rmSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
18
18
|
import { join, sep } from "node:path";
|
|
19
19
|
import { namedPath } from "./names.js";
|
|
20
20
|
import { LIMITS } from "./decision.js";
|
|
@@ -50,6 +50,10 @@ export const PROGRESS_PREFIX = "STANDING-ORDERS-PROGRESS-";
|
|
|
50
50
|
* (adaptive execution plans): a terminal file like park or proof, written
|
|
51
51
|
* at most once, read once at settlement. */
|
|
52
52
|
export const PROPOSAL_PREFIX = "STANDING-ORDERS-PROPOSAL-";
|
|
53
|
+
/** The running build's steering mailbox: a FOLDER, not a file. The pulse
|
|
54
|
+
* writes each operator note into it as NNN.json (a temp name, then a
|
|
55
|
+
* rename); the agent acknowledges a note by moving it into `handled/`. */
|
|
56
|
+
export const STEER_PREFIX = "STANDING-ORDERS-STEER-";
|
|
53
57
|
export const MAILBOX_SUFFIX = ".json";
|
|
54
58
|
/** v105: left in a run's evidence folder when the retention setting removed its files (retention.ts). */
|
|
55
59
|
export const RETENTION_NOTE = "REMOVED-BY-RETENTION";
|
|
@@ -128,6 +132,8 @@ export function looksLikeMailbox(name) {
|
|
|
128
132
|
* The pre-rename NIGHTORDERS- prefix stays recognized here — a worktree cut
|
|
129
133
|
* down before 2026-08-13 may still hold one, and cleanup has no vintage. */
|
|
130
134
|
export function looksLikeProtocolFile(name) {
|
|
135
|
+
if (looksLikeSteerFolder(name))
|
|
136
|
+
return true;
|
|
131
137
|
return ((name.startsWith(MAILBOX_PREFIX) ||
|
|
132
138
|
name.startsWith(HANDOFF_PREFIX) ||
|
|
133
139
|
name.startsWith(PLAN_PREFIX) ||
|
|
@@ -141,6 +147,15 @@ export function looksLikeProtocolFile(name) {
|
|
|
141
147
|
name.startsWith("NIGHTORDERS-")) &&
|
|
142
148
|
name.endsWith(MAILBOX_SUFFIX));
|
|
143
149
|
}
|
|
150
|
+
/** A steering mailbox folder, or anything in it, as a directory listing or
|
|
151
|
+
* `git status` names it: an untracked folder is reported with a trailing
|
|
152
|
+
* slash, or file by file where a repository shows every untracked file. */
|
|
153
|
+
export function looksLikeSteerFolder(name) {
|
|
154
|
+
return /^STANDING-ORDERS-STEER-[0-9a-f]{16}(?:\/.*)?$/.test(name);
|
|
155
|
+
}
|
|
156
|
+
export function steerFolderName() {
|
|
157
|
+
return `${STEER_PREFIX}${randomBytes(8).toString("hex")}`;
|
|
158
|
+
}
|
|
144
159
|
export function handoffName() {
|
|
145
160
|
return `${HANDOFF_PREFIX}${randomBytes(8).toString("hex")}${MAILBOX_SUFFIX}`;
|
|
146
161
|
}
|
|
@@ -412,6 +427,29 @@ export function storeEvidence(store, root, runId, kind, name, content, capture,
|
|
|
412
427
|
// Recovery uses this read boundary for retained receipts and handoffs: inspecting
|
|
413
428
|
// bytes must never reseal a missing artifact or turn a saved failure into a pass.
|
|
414
429
|
export function readVerifiedArtifact(root, artifact) {
|
|
430
|
+
return withArtifactFile(root, artifact, readAndHash);
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Whether an artifact's bytes still verify, without returning them. Status
|
|
434
|
+
* sweeps ask this of every artifact of every run, so a file proved earlier and
|
|
435
|
+
* not touched since (same artifact row, device, inode, size, mtime and ctime
|
|
436
|
+
* on the descriptor just opened) costs an fstat, not a re-read and SHA-256.
|
|
437
|
+
* Every path check still runs on each call; a changed or new file is hashed.
|
|
438
|
+
*/
|
|
439
|
+
export function verifyArtifact(root, artifact) {
|
|
440
|
+
return withArtifactFile(root, artifact, (fd, stat, memoKey) => {
|
|
441
|
+
if (VERIFIED_FILES.get(memoKey) === fileIdentity(artifact, stat))
|
|
442
|
+
return { ok: true };
|
|
443
|
+
const read = readAndHash(fd, stat, memoKey, artifact);
|
|
444
|
+
return read.ok ? { ok: true } : read;
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
/** Files proved by an earlier read, keyed by artifact and resolved path. Bounded; the oldest proof is forgotten first. */
|
|
448
|
+
const VERIFIED_FILES = new Map();
|
|
449
|
+
const VERIFIED_FILES_LIMIT = 20_000;
|
|
450
|
+
const fileIdentity = (artifact, stat) => [artifact.sha256, artifact.bytesStored, stat.dev, stat.ino, stat.size, stat.mtimeNs, stat.ctimeNs].join(":");
|
|
451
|
+
/** The checks both readers share, on the descriptor `use` then reads. */
|
|
452
|
+
function withArtifactFile(root, artifact, use) {
|
|
415
453
|
const segments = artifact.key.split("/");
|
|
416
454
|
const wellFormed = segments.length > 0 &&
|
|
417
455
|
segments.every(segment => segment.length > 0 &&
|
|
@@ -447,32 +485,40 @@ export function readVerifiedArtifact(root, artifact) {
|
|
|
447
485
|
return { ok: false, problem: "cannot open the file without following a link" };
|
|
448
486
|
}
|
|
449
487
|
try {
|
|
450
|
-
const stat = fstatSync(fd);
|
|
488
|
+
const stat = fstatSync(fd, { bigint: true });
|
|
451
489
|
if (!stat.isFile())
|
|
452
490
|
return { ok: false, problem: "not a regular file" };
|
|
453
491
|
if (Number(stat.size) !== artifact.bytesStored) {
|
|
454
492
|
return { ok: false, problem: "the file's size no longer matches its record" };
|
|
455
493
|
}
|
|
456
|
-
|
|
457
|
-
let offset = 0;
|
|
458
|
-
while (offset < buffer.length) {
|
|
459
|
-
const read = readSync(fd, buffer, offset, buffer.length - offset, offset);
|
|
460
|
-
if (read <= 0)
|
|
461
|
-
break;
|
|
462
|
-
offset += read;
|
|
463
|
-
}
|
|
464
|
-
if (offset !== buffer.length)
|
|
465
|
-
return { ok: false, problem: "the file ended early" };
|
|
466
|
-
const digest = createHash("sha256").update(buffer).digest("hex");
|
|
467
|
-
if (digest !== artifact.sha256) {
|
|
468
|
-
return { ok: false, problem: "the file no longer hashes to its record" };
|
|
469
|
-
}
|
|
470
|
-
return { ok: true, content: buffer };
|
|
494
|
+
return use(fd, stat, `${artifact.id}\0${resolved}`, artifact);
|
|
471
495
|
}
|
|
472
496
|
finally {
|
|
473
497
|
closeSync(fd);
|
|
474
498
|
}
|
|
475
499
|
}
|
|
500
|
+
/** Read the whole file from the open descriptor; only bytes matching the row's SHA-256 leave, and only they are remembered as proved. */
|
|
501
|
+
function readAndHash(fd, stat, memoKey, artifact) {
|
|
502
|
+
VERIFIED_FILES.delete(memoKey);
|
|
503
|
+
const buffer = Buffer.alloc(artifact.bytesStored);
|
|
504
|
+
let offset = 0;
|
|
505
|
+
while (offset < buffer.length) {
|
|
506
|
+
const read = readSync(fd, buffer, offset, buffer.length - offset, offset);
|
|
507
|
+
if (read <= 0)
|
|
508
|
+
break;
|
|
509
|
+
offset += read;
|
|
510
|
+
}
|
|
511
|
+
if (offset !== buffer.length)
|
|
512
|
+
return { ok: false, problem: "the file ended early" };
|
|
513
|
+
const digest = createHash("sha256").update(buffer).digest("hex");
|
|
514
|
+
if (digest !== artifact.sha256) {
|
|
515
|
+
return { ok: false, problem: "the file no longer hashes to its record" };
|
|
516
|
+
}
|
|
517
|
+
if (VERIFIED_FILES.size >= VERIFIED_FILES_LIMIT)
|
|
518
|
+
VERIFIED_FILES.delete(VERIFIED_FILES.keys().next().value);
|
|
519
|
+
VERIFIED_FILES.set(memoKey, fileIdentity(artifact, stat));
|
|
520
|
+
return { ok: true, content: buffer };
|
|
521
|
+
}
|
|
476
522
|
/** The file alone, no row — quarantines use this; they belong to no live run. */
|
|
477
523
|
export function writeEvidenceFile(root, runId, name, content) {
|
|
478
524
|
const dir = join(root, String(runId));
|
|
@@ -502,6 +548,19 @@ export function quarantineMailboxes(worktree, root, runId) {
|
|
|
502
548
|
if (!looksLikeProtocolFile(name))
|
|
503
549
|
continue;
|
|
504
550
|
const path = join(worktree, name);
|
|
551
|
+
// An earlier attempt's steering folder holds copies of notes the
|
|
552
|
+
// database already keeps: removed whole (a symlink is removed, never
|
|
553
|
+
// followed), never quarantined.
|
|
554
|
+
if (looksLikeSteerFolder(name)) {
|
|
555
|
+
try {
|
|
556
|
+
rmSync(path, { recursive: true, force: true });
|
|
557
|
+
swept.push(name);
|
|
558
|
+
}
|
|
559
|
+
catch {
|
|
560
|
+
// Its name carries an old nonce; nothing reads it again.
|
|
561
|
+
}
|
|
562
|
+
continue;
|
|
563
|
+
}
|
|
505
564
|
if (root !== null && runId !== null) {
|
|
506
565
|
const read = readMailbox(path);
|
|
507
566
|
if (read.ok) {
|
package/dist/flow-actions.js
CHANGED
|
@@ -21,7 +21,6 @@
|
|
|
21
21
|
import { createHash } from "node:crypto";
|
|
22
22
|
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
23
23
|
import { join } from "node:path";
|
|
24
|
-
import nodemailer from "nodemailer";
|
|
25
24
|
import { redactSecretAssignments } from "./builder.js";
|
|
26
25
|
import { redactSecretLines, scanForSecrets } from "./evidence.js";
|
|
27
26
|
import { fillFlowText } from "./flows.js";
|
|
@@ -132,6 +131,8 @@ export async function sendingAccount(dir, fetcher) {
|
|
|
132
131
|
}
|
|
133
132
|
/** The production sender: the operator's mail server through nodemailer. */
|
|
134
133
|
export const sendThroughServer = async (settings, mail) => {
|
|
134
|
+
// nodemailer loads when mail is sent, not with every command that imports this module.
|
|
135
|
+
const { default: nodemailer } = await import("nodemailer");
|
|
135
136
|
const transport = nodemailer.createTransport({
|
|
136
137
|
// A password never travels unencrypted: with a sign-in, the connection must be (or become) TLS.
|
|
137
138
|
host: settings.host, port: settings.port, secure: settings.secure, requireTLS: !settings.secure && settings.user !== "",
|
package/dist/guides.js
CHANGED
|
@@ -208,22 +208,25 @@ runner never pushes, merges, or opens pull requests itself.
|
|
|
208
208
|
const steering = {
|
|
209
209
|
name: "steering",
|
|
210
210
|
title: "Steering a task",
|
|
211
|
-
oneLiner: "an operator ceremony: credentialed notes
|
|
211
|
+
oneLiner: "an operator ceremony: credentialed notes a running build reads when it next checks for notes, or the next attempt — agents cannot file one",
|
|
212
212
|
content: `# Steering a task
|
|
213
213
|
|
|
214
214
|
${AUTHORITY_LINE}
|
|
215
215
|
|
|
216
216
|
Steering is an OPERATOR ceremony. \`task steer <id> --note "..." --as
|
|
217
|
-
<you> --token <t>\` files one note (≤4000 characters) for the
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
217
|
+
<you> --token <t>\` files one note (≤4000 characters) for the task's
|
|
218
|
+
agent, and the credential is the point: the note reaches the agent as
|
|
219
|
+
operator guidance, so only a verified person may put words there.
|
|
220
|
+
Anonymous or agent-authored notes are refused outright.
|
|
221
|
+
|
|
222
|
+
- While a build's agent is running, the note is put in its steering
|
|
223
|
+
folder; the agent reads it when it next checks for notes and moves it
|
|
224
|
+
to handled/, which is the only thing that marks it read. Otherwise —
|
|
225
|
+
or if the agent finishes before reading it — the next attempt's brief
|
|
226
|
+
quotes it under an OPERATOR STEERING heading.
|
|
227
|
+
- Each note says where it stands, the same on the task page, in
|
|
228
|
+
\`task show\` and to the lead: waiting for the agent to read it, read
|
|
229
|
+
by the agent at a time, or will reach the next attempt.
|
|
227
230
|
- A note is guidance INSIDE the approved scope. It cannot widen scope,
|
|
228
231
|
approve anything, or replace the ceremony a change of scope needs.
|
|
229
232
|
- Refusals: missing credential is usage; a wrong credential is
|
package/dist/lead-cli.js
CHANGED
|
@@ -71,7 +71,7 @@ export function proposalLines(proposals, repos, ordinal = (_p) => 0) {
|
|
|
71
71
|
? `${t("operation") === "revise" ? "request a revision" : "save feedback"} for ${t("taskTitle") || t("task")}: ${t("note")}${Array.isArray(payload["notes"]) && payload["notes"].length > 0 ? ` (saved notes: ${payload["notes"].join(", ")})` : ""}`
|
|
72
72
|
: one.kind === "control"
|
|
73
73
|
? (isChatControl(payload["control"]) ? `${CHAT_CONTROLS[payload["control"]].label}: ${chatControlHref(payload["control"], t("task"), payload["run"], payload["project"])} (open in the console)` : "control unavailable")
|
|
74
|
-
: one.kind === "steer" ? `
|
|
74
|
+
: one.kind === "steer" ? `guidance for ${t("task")}'s agent: ${t("note")}`
|
|
75
75
|
: one.kind === "agents" ? `change agents for ${t("task")}: ${t("role")} ${t("provider")} ${t("model")} ${t("risk")}`
|
|
76
76
|
: one.kind === "repair" ? `${t("operation")} dependency ${t("blocker")} for ${t("task")}`
|
|
77
77
|
:
|
package/dist/lead-contract.js
CHANGED
|
@@ -20,7 +20,7 @@ export const LEAD_CONTRACT = [
|
|
|
20
20
|
"Talk like a colleague in everyday words: task titles, 'the changes', 'the checks', 'try again'. Never say execution, run, scope, digest, dispatch or lane to the operator; say what happens next and what they can do.",
|
|
21
21
|
"Read get_result for the exact execution/run; page nextFeedbackOffset. propose_review revise requests changes; note saves feedback. Use read notes only; clarify ambiguous intent. Ready is a saved result, not a reviewer stage. Historical missing assessments never require rerunning work. Failed checks and missing files remain visible; completion never deploys or changes checks.",
|
|
22
22
|
"For screenshots, call get_result_images for that exact execution and run. On Telegram say they follow, never that they were delivered; elsewhere name result and image count. Limit 8/reply. If nextImageOffset exists, name remaining count; call it again with that offset or with the image ids it listed when asked. Select a newer revision explicitly; never describe unselected images or expose paths.",
|
|
23
|
-
"propose_steer
|
|
23
|
+
"propose_steer reaches a running build when it next checks for notes, else the next attempt; get_task steering says if it was read. propose_scope changes scope. propose_task_action uses control.run for stop/resume and supports retry, plan, dependencies. Requested stop is not stopped; ending chat differs. For protected actions read get_actions, use propose_action and full secure review. get_action_status proves saved outcomes; show_control only opens controls.",
|
|
24
24
|
"For a task's models/risk, read get_agents and answer in its words; for defaults, CLI versions and new models, read get_models; never an agent that is not listed. Changes require renewed approval. Before propose_answer read get_decision in an earlier step, including every option and consequence. Irreversible answers require explicit confirmation; cards retain consequences and recommendations.",
|
|
25
25
|
"For intake, a plain-language outcome is enough to draft a task. Infer title, narrow goal, safe non-goals and testable criteria; leave touches empty for discovery. Do not ask the operator for a title, paths, implementation details, acceptance wording, model, budget or safely inferable fields. Ask at most three questions with defaults (one at a time with ask_owner) for material ambiguity, conflicting goals or unresolved irreversible/public/security/data-loss/migration choices. With 'use your judgment', use reversible defaults; real blockers may still arise.",
|
|
26
26
|
"Set propose_task planning to 'required' for broad, risky or plan-first work, 'skip' for explicitly small direct builds, otherwise 'auto'. report:true investigates without a branch; follow-ups require confirmed proposals.",
|
package/dist/lead-doors.js
CHANGED
|
@@ -15,6 +15,7 @@ import { executeSharedAction, sharedActionPayload, sharedActionAllowsChallenge,
|
|
|
15
15
|
* silent re-read.
|
|
16
16
|
*/
|
|
17
17
|
import { closedQuestionWords } from "./decision.js";
|
|
18
|
+
import { steerDestinationWords } from "./steer-status.js";
|
|
18
19
|
import { isCheckLevel, setTaskCheckLevel } from "./check-levels.js";
|
|
19
20
|
import { leadTurnKeepsProposals, verifiedAuthor } from "./store.js";
|
|
20
21
|
import { isVerifiedApprover, reproveApprover } from "./principal.js";
|
|
@@ -58,7 +59,7 @@ const VIA_WORDS = {
|
|
|
58
59
|
};
|
|
59
60
|
const CARD_WORDS = {
|
|
60
61
|
task: "New task", next: "Queue priority", reserve: "Worker assignment", hold: "Pause work", unhold: "Resume work",
|
|
61
|
-
steer: "Guidance for the
|
|
62
|
+
steer: "Guidance for the agent", scope: "Scope revision", answer: "Decision answer", repair: "Waiting task",
|
|
62
63
|
agents: "Agents change", task_action: "Task update", action: "Action",
|
|
63
64
|
};
|
|
64
65
|
/** A card about a task, confirmed anywhere but that task's own chat (the
|
|
@@ -421,7 +422,9 @@ function executeProposal(store, actor, kind, payload, now, options) {
|
|
|
421
422
|
return refuse("refused", said);
|
|
422
423
|
}
|
|
423
424
|
const taskName = payloadString(payload, "taskTitle") ?? taskId;
|
|
424
|
-
|
|
425
|
+
const steered = store.lookupRef(taskId);
|
|
426
|
+
const live = steered !== null && store.steeringLiveBuild(steered.id, now) !== null;
|
|
427
|
+
return { ok: true, kind, said: `Guidance saved for ${taskName}. ${steerDestinationWords(live)}`, taskId };
|
|
425
428
|
}
|
|
426
429
|
if (kind === "repair") {
|
|
427
430
|
const blocker = payloadString(payload, "blocker");
|
package/dist/lead-follow.d.ts
CHANGED
|
@@ -6,6 +6,8 @@ export declare function leadFollowStatus(store: Store, actor: string): {
|
|
|
6
6
|
enabled: boolean;
|
|
7
7
|
detail: string;
|
|
8
8
|
};
|
|
9
|
+
/** A follow pass that failed outright says so where each enabled conversation reads its follow status. */
|
|
10
|
+
export declare function recordLeadFollowFailure(store: Store, error: unknown, now: Date): void;
|
|
9
11
|
export type LeadFollowInput = {
|
|
10
12
|
store: Store;
|
|
11
13
|
repos: () => readonly string[];
|