toolroll 0.9.58 → 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 +18 -0
- package/README.md +8 -6
- package/dist/action-ledger.js +10 -1
- 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 +78 -27
- package/dist/chat-channel.d.ts +1 -0
- package/dist/chat-channel.js +4 -1
- package/dist/chat-delivery.js +2 -0
- 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/contract-changes.d.ts +52 -0
- package/dist/contract-changes.js +61 -0
- package/dist/decision.d.ts +4 -0
- package/dist/decision.js +8 -0
- package/dist/deploy-lock.d.ts +8 -0
- package/dist/deploy-lock.js +45 -0
- package/dist/design/tokens.css +234 -0
- package/dist/discord.js +2 -1
- package/dist/dispatch.js +1 -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 +4 -1
- package/dist/lead-contract.js +1 -1
- package/dist/lead-doors.js +8 -2
- package/dist/lead-follow.d.ts +2 -0
- package/dist/lead-follow.js +138 -88
- package/dist/lead-tools.js +11 -3
- package/dist/mailbox.js +3 -2
- package/dist/mcp-core.js +1 -1
- package/dist/needs-you.js +3 -1
- package/dist/operate.js +38 -17
- package/dist/password-bearer.d.ts +4 -4
- package/dist/password-bearer.js +2 -2
- package/dist/planner-source.d.ts +23 -53
- package/dist/planner-source.js +43 -68
- package/dist/provider.d.ts +8 -0
- package/dist/provider.js +12 -1
- package/dist/release-gate.d.ts +2 -3
- package/dist/release-gate.js +44 -16
- package/dist/scope.js +14 -0
- 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 +12 -2
- package/dist/server/render-tasks.d.ts +2 -0
- package/dist/server/render-tasks.js +30 -22
- package/dist/server/task-views.js +1 -0
- package/dist/server/tasks.js +3 -1
- 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 +85 -8
- package/dist/store.js +203 -15
- package/dist/surface.js +2 -2
- package/dist/sweep-gate.d.ts +10 -0
- package/dist/sweep-gate.js +18 -0
- package/dist/task-status.js +3 -1
- package/dist/team-updates.js +3 -3
- package/dist/telegram-lead.js +6 -0
- package/dist/telegram.js +9 -7
- package/dist/toolroll-update.js +20 -1
- package/package.json +21 -21
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The running build's steering mailbox (v120). A note an operator files
|
|
3
|
+
* while a build runs used to wait for the next attempt; now the builder's
|
|
4
|
+
* pulse writes it into one nonce-named folder in the worktree, as a numbered
|
|
5
|
+
* file the agent reads and acknowledges by moving it into `handled/` — the
|
|
6
|
+
* same small contract a person's own inbox keeps:
|
|
7
|
+
*
|
|
8
|
+
* - The durable record is the delivery. A note is mailed (written into the
|
|
9
|
+
* folder) and read (moved by the agent); nothing else counts. A hook
|
|
10
|
+
* reminding the agent, or a file the pulse wrote, proves nothing.
|
|
11
|
+
* - Numbers are never reused: the next one is past every name in the folder
|
|
12
|
+
* and in `handled/`, and past every one this pulse has used.
|
|
13
|
+
* - Whatever is still unread when the attempt ends keeps today's road: the
|
|
14
|
+
* note stays pending, the next attempt's brief quotes it, and a task that
|
|
15
|
+
* ends first supersedes it.
|
|
16
|
+
*
|
|
17
|
+
* Every step is bookkeeping, and bookkeeping never fails a build: a folder
|
|
18
|
+
* the agent removed is made again, one it replaced with anything but a real
|
|
19
|
+
* directory is never written through, and an error waits for the next beat.
|
|
20
|
+
*
|
|
21
|
+
* Two boundaries, because the folder lives where the agent and anything it
|
|
22
|
+
* runs can write:
|
|
23
|
+
* - Every genuine note carries this attempt's key, which only the brief and
|
|
24
|
+
* the notes themselves name. A file in the folder without it was not
|
|
25
|
+
* written by the operator, and the brief tells the agent to act on none.
|
|
26
|
+
* (The brief rides the agent's argv, so a process that reads argv could
|
|
27
|
+
* learn it: this keeps ordinary repository code from posing as the
|
|
28
|
+
* operator, not a determined local process.)
|
|
29
|
+
* - The pulse writes only while the folder still resolves to this worktree's
|
|
30
|
+
* own folder, before and after each write, so swapping it for a link can
|
|
31
|
+
* never make the plane write a note somewhere else.
|
|
32
|
+
*/
|
|
33
|
+
import { lstatSync, mkdirSync, readdirSync, realpathSync, renameSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
|
|
34
|
+
import { join } from "node:path";
|
|
35
|
+
import { fence } from "./brief-fence.js";
|
|
36
|
+
export const STEER_HANDLED = "handled";
|
|
37
|
+
const NOTE_NAME = /^(\d+)\.json$/;
|
|
38
|
+
export const steerNoteName = (number) => `${String(number).padStart(3, "0")}.json`;
|
|
39
|
+
const isRealDirectory = (path) => {
|
|
40
|
+
try {
|
|
41
|
+
return lstatSync(path).isDirectory();
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
/** Make the folder and its handled/ inside the worktree; null when it cannot be made. */
|
|
48
|
+
export function openSteerMailbox(worktree, folder, runId, taskRef, key) {
|
|
49
|
+
let realDir;
|
|
50
|
+
try {
|
|
51
|
+
realDir = join(realpathSync(worktree), folder);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
const box = { runId, taskRef, folder, dir: join(worktree, folder), realDir, key, last: 0, waiting: new Map() };
|
|
57
|
+
return ensureFolder(box) ? box : null;
|
|
58
|
+
}
|
|
59
|
+
/** The folder is still this worktree's own, not a link to somewhere else. */
|
|
60
|
+
const stillInside = (box) => {
|
|
61
|
+
try {
|
|
62
|
+
return realpathSync(box.dir) === box.realDir;
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
function ensureFolder(box) {
|
|
69
|
+
for (const path of [box.dir, join(box.dir, STEER_HANDLED)]) {
|
|
70
|
+
try {
|
|
71
|
+
mkdirSync(path, { mode: 0o700 });
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
if (error.code !== "EEXIST")
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
// Something the agent put in the folder's place is never written through.
|
|
78
|
+
if (!isRealDirectory(path))
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
return stillInside(box);
|
|
82
|
+
}
|
|
83
|
+
function highestNumber(box) {
|
|
84
|
+
let highest = box.last;
|
|
85
|
+
for (const path of [box.dir, join(box.dir, STEER_HANDLED)]) {
|
|
86
|
+
let names;
|
|
87
|
+
try {
|
|
88
|
+
names = readdirSync(path);
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
for (const name of names) {
|
|
94
|
+
const number = NOTE_NAME.exec(name)?.[1];
|
|
95
|
+
if (number !== undefined)
|
|
96
|
+
highest = Math.max(highest, Number(number));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return highest;
|
|
100
|
+
}
|
|
101
|
+
/** Every waiting note the agent moved into handled/ is read, by this run. */
|
|
102
|
+
function collectAcknowledgements(store, box, now) {
|
|
103
|
+
if (box.waiting.size === 0)
|
|
104
|
+
return;
|
|
105
|
+
const handled = join(box.dir, STEER_HANDLED);
|
|
106
|
+
if (!isRealDirectory(box.dir) || !isRealDirectory(handled))
|
|
107
|
+
return;
|
|
108
|
+
for (const [number, noteId] of [...box.waiting]) {
|
|
109
|
+
let moved;
|
|
110
|
+
try {
|
|
111
|
+
moved = lstatSync(join(handled, steerNoteName(number))).isFile();
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
moved = false;
|
|
115
|
+
}
|
|
116
|
+
if (!moved)
|
|
117
|
+
continue;
|
|
118
|
+
box.waiting.delete(number);
|
|
119
|
+
store.ackSteerNote(noteId, box.runId, now);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/** Remove a name this pulse wrote, only while the folder is still this worktree's own; a link goes, never its target. */
|
|
123
|
+
function removeOwn(box, name) {
|
|
124
|
+
if (!stillInside(box))
|
|
125
|
+
return;
|
|
126
|
+
try {
|
|
127
|
+
unlinkSync(join(box.dir, name));
|
|
128
|
+
}
|
|
129
|
+
catch { /* never written, or the agent already took it */ }
|
|
130
|
+
}
|
|
131
|
+
/** Write each note this run has not seen yet: a temp name, then the number. */
|
|
132
|
+
function mailNotes(store, box, now) {
|
|
133
|
+
const notes = store.mailableSteerNotes(box.taskRef, box.runId);
|
|
134
|
+
if (notes.length === 0 || !ensureFolder(box))
|
|
135
|
+
return;
|
|
136
|
+
for (const note of notes) {
|
|
137
|
+
if (!stillInside(box))
|
|
138
|
+
return;
|
|
139
|
+
const number = highestNumber(box) + 1;
|
|
140
|
+
const name = steerNoteName(number);
|
|
141
|
+
const temp = `.${name}.tmp`;
|
|
142
|
+
const body = { version: 1, number, key: box.key, filedAt: note.createdAt, note: fence(note.note) };
|
|
143
|
+
try {
|
|
144
|
+
writeFileSync(join(box.dir, temp), `${JSON.stringify(body, null, 2)}\n`, { flag: "wx", mode: 0o600 });
|
|
145
|
+
renameSync(join(box.dir, temp), join(box.dir, name));
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
removeOwn(box, temp);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
// Checked again after the write: a folder swapped for a link mid-write
|
|
152
|
+
// leaves the note unrecorded, deletes nothing, and mails nothing more this beat.
|
|
153
|
+
if (!stillInside(box))
|
|
154
|
+
return;
|
|
155
|
+
box.last = number;
|
|
156
|
+
let recorded;
|
|
157
|
+
try {
|
|
158
|
+
recorded = store.markSteerMailed(note.id, box.runId, now);
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
recorded = false;
|
|
162
|
+
}
|
|
163
|
+
if (recorded) {
|
|
164
|
+
box.waiting.set(number, note.id);
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
// The task ended or the note settled between the read and the write, or
|
|
168
|
+
// the mailing could not be recorded: an unrecorded file would only be
|
|
169
|
+
// mailed twice, so it goes, and the next beat tries again.
|
|
170
|
+
removeOwn(box, name);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
/** One beat: record what the agent read, then mail what is new while the agent runs. */
|
|
175
|
+
export function pulseSteerMailbox(store, box, now, agentRunning) {
|
|
176
|
+
collectAcknowledgements(store, box, now);
|
|
177
|
+
if (agentRunning)
|
|
178
|
+
mailNotes(store, box, now);
|
|
179
|
+
}
|
|
180
|
+
/** The attempt is over: one last read, then the folder goes. Unread notes stay pending for the next attempt. */
|
|
181
|
+
export function closeSteerMailbox(store, box, now) {
|
|
182
|
+
try {
|
|
183
|
+
collectAcknowledgements(store, box, now);
|
|
184
|
+
}
|
|
185
|
+
catch { /* the notes stay pending */ }
|
|
186
|
+
try {
|
|
187
|
+
rmSync(box.dir, { recursive: true, force: true });
|
|
188
|
+
}
|
|
189
|
+
catch { /* the next attempt's sweep removes it */ }
|
|
190
|
+
}
|
|
191
|
+
const shellQuote = (value) => `'${value.replace(/'/g, `'\\''`)}'`;
|
|
192
|
+
/**
|
|
193
|
+
* Claude's doorbell: a synchronous PostToolUse hook that lists the folder and,
|
|
194
|
+
* while a note is unread, hands the agent one line of context between tool
|
|
195
|
+
* calls. It only says notes are waiting — it never quotes one, and its firing
|
|
196
|
+
* is never an acknowledgement. Rendered into the run's own --settings.
|
|
197
|
+
*/
|
|
198
|
+
export function claudeSteerHooks(dir, folder) {
|
|
199
|
+
const context = {
|
|
200
|
+
hookSpecificOutput: {
|
|
201
|
+
hookEventName: "PostToolUse",
|
|
202
|
+
additionalContext: `Operator steering waiting: %s unread note(s) in ${folder}/. Read them in number order now, act on each within the agreed scope, then move each into ${folder}/${STEER_HANDLED}/.`,
|
|
203
|
+
},
|
|
204
|
+
};
|
|
205
|
+
const command = `set -- ${shellQuote(dir)}/[0-9]*.json; [ -e "$1" ] || exit 0; printf ${shellQuote(`${JSON.stringify(context)}\n`)} "$#"`;
|
|
206
|
+
return { PostToolUse: [{ matcher: "*", hooks: [{ type: "command", command }] }] };
|
|
207
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a steering note stands, in the one set of words the task page, the
|
|
3
|
+
* CLI and the lead all use (v120). A note is read only when the agent proves
|
|
4
|
+
* it: the stream's receipt of a brief that quoted it, or the agent moving it
|
|
5
|
+
* to handled/ in a running build's steering folder. Everything else is
|
|
6
|
+
* either waiting for the agent that is running now, or for the next attempt.
|
|
7
|
+
*/
|
|
8
|
+
import type { SteerNote } from "./store.js";
|
|
9
|
+
export type SteerStanding = "read" | "waiting" | "next-attempt" | "ended" | "unverified";
|
|
10
|
+
/** `liveBuild`: the task has a build whose agent is running and reads its steering folder (Store.steeringLiveBuild). */
|
|
11
|
+
export declare function steerStandingOf(note: SteerNote, liveBuild: boolean): SteerStanding;
|
|
12
|
+
/** The words, and the moment the agent read it when it has: each surface renders that time its own way. */
|
|
13
|
+
export declare function steerStandingParts(note: SteerNote, liveBuild: boolean): {
|
|
14
|
+
words: string;
|
|
15
|
+
readAt: string | null;
|
|
16
|
+
};
|
|
17
|
+
export declare function steerStandingWords(note: SteerNote, liveBuild: boolean, time: (iso: string) => string): string;
|
|
18
|
+
/** Where a note filed now goes, said before or right after filing it. */
|
|
19
|
+
export declare function steerDestinationWords(liveBuild: boolean): string;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/** `liveBuild`: the task has a build whose agent is running and reads its steering folder (Store.steeringLiveBuild). */
|
|
2
|
+
export function steerStandingOf(note, liveBuild) {
|
|
3
|
+
if (note.authorshipState !== "verified")
|
|
4
|
+
return "unverified";
|
|
5
|
+
if (note.supersededAt !== null)
|
|
6
|
+
return "ended";
|
|
7
|
+
if (note.deliveredAt !== null)
|
|
8
|
+
return "read";
|
|
9
|
+
return liveBuild ? "waiting" : "next-attempt";
|
|
10
|
+
}
|
|
11
|
+
/** The words, and the moment the agent read it when it has: each surface renders that time its own way. */
|
|
12
|
+
export function steerStandingParts(note, liveBuild) {
|
|
13
|
+
switch (steerStandingOf(note, liveBuild)) {
|
|
14
|
+
case "read": return { words: "Read by the agent at", readAt: note.ackedAt ?? note.deliveredAt };
|
|
15
|
+
case "waiting": return { words: "Waiting for the agent to read it", readAt: null };
|
|
16
|
+
case "next-attempt": return { words: "Will reach the next attempt", readAt: null };
|
|
17
|
+
case "ended": return { words: "The task ended before the agent read it", readAt: null };
|
|
18
|
+
case "unverified": return { words: "Saved before notes required a sign-in; never sent", readAt: null };
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export function steerStandingWords(note, liveBuild, time) {
|
|
22
|
+
const { words, readAt } = steerStandingParts(note, liveBuild);
|
|
23
|
+
return readAt === null ? words : `${words} ${time(readAt)}`;
|
|
24
|
+
}
|
|
25
|
+
/** Where a note filed now goes, said before or right after filing it. */
|
|
26
|
+
export function steerDestinationWords(liveBuild) {
|
|
27
|
+
return liveBuild
|
|
28
|
+
? "The running agent reads it when it next checks for notes."
|
|
29
|
+
: "The next attempt reads it before starting.";
|
|
30
|
+
}
|
package/dist/store.d.ts
CHANGED
|
@@ -152,7 +152,7 @@ export type SealedLedgerEntry = LedgerEntry & {
|
|
|
152
152
|
hash: string;
|
|
153
153
|
} | null;
|
|
154
154
|
};
|
|
155
|
-
export declare const SCHEMA_VERSION =
|
|
155
|
+
export declare const SCHEMA_VERSION = 120;
|
|
156
156
|
/** v118: the tables merged into `credential`, in the order they move (a person's API tokens keep their ids first). */
|
|
157
157
|
export declare const V118_MERGED_TABLES: Readonly<Record<string, CredentialKind>>;
|
|
158
158
|
/**
|
|
@@ -207,9 +207,22 @@ export declare const V115_DROPPED_TABLES: readonly string[];
|
|
|
207
207
|
* teammates' to its subagents' (SQLite's RENAME, every row, id and link carried), renames their teammate column, and rewrites four internal kinds from 'teammate' to 'subagent' in place; the rehearsal checks
|
|
208
208
|
* each renamed table's rows arrived under its new name. v118 (D7) is checked the same way: every row of api_token,
|
|
209
209
|
* coordinator_credential and lead_credential moves into credential under its kind in one transaction that counts them
|
|
210
|
-
* before the old tables drop, and the references that named them name credential in place, their rows untouched.
|
|
210
|
+
* before the old tables drop, and the references that named them name credential in place, their rows untouched. v119 only adds four columns
|
|
211
|
+
* (planning_generation on task_ref, run and plan_revision, defaulting to 0, and decision.superseded_reason, null);
|
|
212
|
+
* the rehearsal checks every saved row reads the declared default (HISTORY_RULES' added columns). v120 only adds
|
|
213
|
+
* three nullable task_steer columns (the running build's steering folder), checked the same way.
|
|
211
214
|
*/
|
|
212
215
|
export declare const UPDATE_SAFE_MIGRATIONS: readonly number[];
|
|
216
|
+
/** v119: the columns it adds, by table, with the value every row saved before it reads (toolroll-update's HISTORY_RULES). */
|
|
217
|
+
export declare const V119_ADDED_COLUMNS: Readonly<Record<string, Readonly<Record<string, {
|
|
218
|
+
definition: string;
|
|
219
|
+
saved: string;
|
|
220
|
+
}>>>>;
|
|
221
|
+
/** v120: the columns it adds, by table, with the value every row saved before it reads. */
|
|
222
|
+
export declare const V120_ADDED_COLUMNS: Readonly<Record<string, Readonly<Record<string, {
|
|
223
|
+
definition: string;
|
|
224
|
+
saved: string;
|
|
225
|
+
}>>>>;
|
|
213
226
|
/** Whether a database settled at `version` reaches this build's schema through update-safe migrations alone. */
|
|
214
227
|
export declare function updateSafeSchema(version: number | null): boolean;
|
|
215
228
|
export type OAuthClientRow = {
|
|
@@ -330,6 +343,9 @@ export type TaskRef = {
|
|
|
330
343
|
plan: "requested" | "drafted" | null;
|
|
331
344
|
/** Planning failures, counted apart from build strikes by design. */
|
|
332
345
|
planStrikes: number;
|
|
346
|
+
/** v119: which planning context is active. A rewritten scope advances it; plans, revisions and answers from an
|
|
347
|
+
* earlier generation stay as history, never as the next planner's context. */
|
|
348
|
+
planningGeneration: number;
|
|
333
349
|
/** What comes back (v34): a branch to publish, or a report to read.
|
|
334
350
|
* Set at filing and never rewritten — a scout's strikes are the task's. */
|
|
335
351
|
deliverable: "branch" | "report";
|
|
@@ -1050,6 +1066,8 @@ export type Run = {
|
|
|
1050
1066
|
taskRef: number;
|
|
1051
1067
|
leaseId: string;
|
|
1052
1068
|
runner: string;
|
|
1069
|
+
/** v119: the task's planning generation when this run opened (0 for every run before it). */
|
|
1070
|
+
planningGeneration: number;
|
|
1053
1071
|
/** Explicit watch ownership for claimless review runs; null on cron/legacy reviews. */
|
|
1054
1072
|
watchIncarnation?: string | null;
|
|
1055
1073
|
/** v50: a ROOT reviewer's attempt ordinal for its source run (1..3);
|
|
@@ -1125,6 +1143,9 @@ export type RunPhase = (typeof RUN_PHASES)[number];
|
|
|
1125
1143
|
* One operator steering note (arc 1, v22). Attached ≠ delivered: attachment
|
|
1126
1144
|
* says which build the note rode toward; delivery settles only on the
|
|
1127
1145
|
* stream's own receipt. Superseded = the task ended with it still pending.
|
|
1146
|
+
* v120: a note filed while a build runs is mailed into that build's steering
|
|
1147
|
+
* folder (mailed), and is read only when the agent moves it to handled/
|
|
1148
|
+
* (acked, which also settles it delivered to that build).
|
|
1128
1149
|
*/
|
|
1129
1150
|
export type SteerNote = {
|
|
1130
1151
|
/** v24 (ruling 11): "verified" = credentialed authorship; anything else is history, never brief-bound. */
|
|
@@ -1139,6 +1160,9 @@ export type SteerNote = {
|
|
|
1139
1160
|
attachedAt: string | null;
|
|
1140
1161
|
deliveredAt: string | null;
|
|
1141
1162
|
supersededAt: string | null;
|
|
1163
|
+
mailedRun: number | null;
|
|
1164
|
+
mailedAt: string | null;
|
|
1165
|
+
ackedAt: string | null;
|
|
1142
1166
|
};
|
|
1143
1167
|
/** One phone enrollment for web push (arc 3, v23). A row per ACTIVATION —
|
|
1144
1168
|
* retirement is history, never deletion. */
|
|
@@ -1194,7 +1218,12 @@ export type Decision = {
|
|
|
1194
1218
|
answeredVia: "cli" | "web" | "telegram" | "slack" | "discord" | "teams" | null;
|
|
1195
1219
|
choice: string | null;
|
|
1196
1220
|
note: string | null;
|
|
1221
|
+
/** v119: closed without an answer (state 'answered', no choice) because its task's scope changed or the task was
|
|
1222
|
+
* cancelled. null is every real answer and every question still waiting. */
|
|
1223
|
+
supersededReason: DecisionClosure | null;
|
|
1197
1224
|
};
|
|
1225
|
+
/** Why a question closed without an answer (v119). */
|
|
1226
|
+
export type DecisionClosure = "scope-changed" | "cancelled";
|
|
1198
1227
|
/** Evidence, by reference. `key` is relative to the evidence root, never absolute. */
|
|
1199
1228
|
export type Artifact = {
|
|
1200
1229
|
id: number;
|
|
@@ -3446,6 +3475,24 @@ export declare class Store {
|
|
|
3446
3475
|
ok: false;
|
|
3447
3476
|
reason: "live-claim";
|
|
3448
3477
|
};
|
|
3478
|
+
/**
|
|
3479
|
+
* A rewritten scope starts planning again (v119), in one transaction: the task's planning generation advances, a
|
|
3480
|
+
* requested or drafted plan becomes requested with no strikes, and what the old goal produced leaves the active
|
|
3481
|
+
* planning context — its open questions close (typed 'scope-changed', never a fake answer, their holds and pages
|
|
3482
|
+
* with them) and a revision still waiting for a person is rejected with its hold. The old runs, plans, revisions,
|
|
3483
|
+
* answers and ledger rows all stay: generation-bound reads simply stop offering them to the next planner. Only a
|
|
3484
|
+
* task already planning restarts; false (nothing changed) for any other.
|
|
3485
|
+
*/
|
|
3486
|
+
restartPlanning(taskRef: number, now: Date): boolean;
|
|
3487
|
+
/**
|
|
3488
|
+
* Close a task's open and expired questions without answering them (v119): typed with why, no choice, their
|
|
3489
|
+
* holds lifted and their pages resolved, so they leave every decision queue. Answered questions are never touched.
|
|
3490
|
+
* `belowGeneration` limits it to questions asked in earlier planning generations. Caller holds the transaction.
|
|
3491
|
+
*/
|
|
3492
|
+
private closeQuestionsLocked;
|
|
3493
|
+
/** The number the next plan revision of a task takes: one past every revision it ever had, in any planning
|
|
3494
|
+
* generation, so a restarted plan's revisions never collide with the history they follow. */
|
|
3495
|
+
nextPlanRevisionNumber(taskRef: number): number;
|
|
3449
3496
|
/** Move the planning state; null clears it. Caller owns the transaction story. */
|
|
3450
3497
|
setPlanState(taskRef: number, state: "requested" | "drafted" | null): void;
|
|
3451
3498
|
addPlanStrike(taskRef: number): number;
|
|
@@ -4475,7 +4522,8 @@ export declare class Store {
|
|
|
4475
4522
|
* reads in the order the questions arrived. "unanswered" is open + expired:
|
|
4476
4523
|
* expiry makes a decision louder, never gone.
|
|
4477
4524
|
*/
|
|
4478
|
-
/** The newest planner run's plan document for a task, when one exists
|
|
4525
|
+
/** The newest planner run's plan document for a task, when one exists in its current planning generation (v119:
|
|
4526
|
+
* a plan drafted for a scope since rewritten is history, read by its artifact id). */
|
|
4479
4527
|
latestPlanArtifact(taskRef: number): Artifact | null;
|
|
4480
4528
|
/** The newest plan-contract INGESTION record on one of this task's
|
|
4481
4529
|
* planner runs (contract handoff, task 1): what the draft proposed
|
|
@@ -4508,8 +4556,9 @@ export declare class Store {
|
|
|
4508
4556
|
changedFields: readonly string[];
|
|
4509
4557
|
status: PlanRevisionStatus;
|
|
4510
4558
|
}, now: Date): number;
|
|
4511
|
-
/** Every revision for a task
|
|
4512
|
-
* reader can show without a second table
|
|
4559
|
+
/** Every revision for a task in its current planning generation, newest
|
|
4560
|
+
* first — the immutable history a reader can show without a second table
|
|
4561
|
+
* (v119: an earlier generation's rows stay, read by id). */
|
|
4513
4562
|
listPlanRevisions(taskRef: number): PlanRevision[];
|
|
4514
4563
|
/** The current plan revision: the newest row an operator (or an
|
|
4515
4564
|
* auto-applied plan-only proposal) has actually put in force. Null for
|
|
@@ -4566,7 +4615,8 @@ export declare class Store {
|
|
|
4566
4615
|
checkpointHistory(run: number): RunCheckpoint[];
|
|
4567
4616
|
/** The newest scout run's report for a task (v34), when one exists. */
|
|
4568
4617
|
latestReportArtifact(taskRef: number): Artifact | null;
|
|
4569
|
-
/** Answered questions for one task, newest first — the planner's memory.
|
|
4618
|
+
/** Answered questions for one task, newest first — the planner's memory. Only real answers from its current
|
|
4619
|
+
* planning generation (v119): a question closed unanswered, or asked about a scope since rewritten, is history. */
|
|
4570
4620
|
answeredDecisionsFor(taskId: string, limit?: number): {
|
|
4571
4621
|
question: string;
|
|
4572
4622
|
choice: string | null;
|
|
@@ -4638,6 +4688,33 @@ export declare class Store {
|
|
|
4638
4688
|
* first receipt wins, and a replay changes nothing.
|
|
4639
4689
|
*/
|
|
4640
4690
|
settleSteerDelivered(runId: number, now: Date): number;
|
|
4691
|
+
/**
|
|
4692
|
+
* The task's build whose agent is running right now and reads its steering
|
|
4693
|
+
* folder (v120): the current live lease's open builder run, in its
|
|
4694
|
+
* agent-running phase, whose steering folder is open. Null otherwise — a
|
|
4695
|
+
* note then waits for the next attempt. Display state, never evidence.
|
|
4696
|
+
*/
|
|
4697
|
+
steeringLiveBuild(taskRef: number, now: Date): number | null;
|
|
4698
|
+
/** This run made its steering folder (v120): its agent can read a note filed now. */
|
|
4699
|
+
markSteerFolderOpened(runId: number, now: Date): void;
|
|
4700
|
+
/** This run's steering folder is gone (v120): a note filed now reaches the next attempt. */
|
|
4701
|
+
markSteerFolderClosed(runId: number, now: Date): void;
|
|
4702
|
+
/**
|
|
4703
|
+
* The notes a RUNNING build may still read (v120): verified, pending, not
|
|
4704
|
+
* already quoted in this run's brief, and not already in its folder. The
|
|
4705
|
+
* pulse mails exactly these, in filing order.
|
|
4706
|
+
*/
|
|
4707
|
+
mailableSteerNotes(taskRef: number, runId: number): SteerNote[];
|
|
4708
|
+
/** The pulse wrote this note into the run's steering folder. Mailing is not
|
|
4709
|
+
* reading: delivery settles only on the agent's own acknowledgement. */
|
|
4710
|
+
markSteerMailed(noteId: number, runId: number, now: Date): boolean;
|
|
4711
|
+
/**
|
|
4712
|
+
* The agent moved this note to handled/ (v120): it was read, by THIS run.
|
|
4713
|
+
* Acknowledgement is the only road from mailed to delivered — a hook firing
|
|
4714
|
+
* or a file written proves nothing. First acknowledgement wins; a note the
|
|
4715
|
+
* task already ended, or mailed to another run since, is left alone.
|
|
4716
|
+
*/
|
|
4717
|
+
ackSteerNote(noteId: number, runId: number, now: Date): boolean;
|
|
4641
4718
|
/** A finished task's pending notes settle superseded — shown, typed, done. */
|
|
4642
4719
|
supersedeSteerNotes(taskRef: number, now: Date): number;
|
|
4643
4720
|
/** The answers a run was given, exactly as it was given them. */
|
|
@@ -4668,7 +4745,7 @@ export declare class Store {
|
|
|
4668
4745
|
duplicate?: boolean;
|
|
4669
4746
|
} | {
|
|
4670
4747
|
ok: false;
|
|
4671
|
-
reason: "unknown-decision" | "bad-option" | "already-answered" | "bad-note";
|
|
4748
|
+
reason: "unknown-decision" | "bad-option" | "already-answered" | "closed" | "bad-note";
|
|
4672
4749
|
};
|
|
4673
4750
|
/**
|
|
4674
4751
|
* The answer's body, for callers already holding the transaction — the
|
|
@@ -4689,7 +4766,7 @@ export declare class Store {
|
|
|
4689
4766
|
duplicate?: boolean;
|
|
4690
4767
|
} | {
|
|
4691
4768
|
ok: false;
|
|
4692
|
-
reason: "unknown-decision" | "bad-option" | "already-answered" | "bad-note";
|
|
4769
|
+
reason: "unknown-decision" | "bad-option" | "already-answered" | "closed" | "bad-note";
|
|
4693
4770
|
};
|
|
4694
4771
|
saveArtifact(artifact: {
|
|
4695
4772
|
run: number;
|