toolroll 0.9.92 → 0.9.93
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 +19 -0
- package/dist/assignment-status.d.ts +1 -1
- package/dist/assignment-ui.js +2 -2
- package/dist/assignment.js +11 -3
- package/dist/browser/workspace.css +1 -1
- package/dist/browser/workspace.js +30 -30
- package/dist/browser-crew.js +2 -2
- package/dist/browser-workspace.d.ts +3 -5
- package/dist/builder.js +21 -2
- package/dist/check-facts.d.ts +124 -0
- package/dist/check-facts.js +272 -0
- package/dist/check-report.d.ts +5 -0
- package/dist/check-report.js +4 -3
- package/dist/cli.js +5 -0
- package/dist/contracts/console-api.d.ts +2 -2
- package/dist/contracts/console-api.js +2 -2
- package/dist/contracts/release-check.d.ts +25 -1
- package/dist/contracts/release-check.js +19 -0
- package/dist/desktop-update.d.ts +2 -2
- package/dist/desktop-update.js +13 -9
- package/dist/eval-hooks.d.ts +55 -0
- package/dist/eval-hooks.js +227 -4
- package/dist/flaky.d.ts +17 -0
- package/dist/flaky.js +9 -0
- package/dist/needs-you.d.ts +7 -7
- package/dist/needs-you.js +12 -10
- package/dist/operate.js +138 -18
- package/dist/project-delete.js +5 -1
- package/dist/project-knowledge.d.ts +6 -6
- package/dist/push.d.ts +10 -3
- package/dist/push.js +22 -6
- package/dist/release-adapters.js +11 -1
- package/dist/release-approval.d.ts +191 -0
- package/dist/release-approval.js +382 -0
- package/dist/release-auto.js +13 -4
- package/dist/release-card.d.ts +73 -4
- package/dist/release-card.js +168 -23
- package/dist/release-cli.js +9 -1
- package/dist/release.d.ts +30 -0
- package/dist/release.js +42 -3
- package/dist/result-acts.d.ts +3 -3
- package/dist/result-acts.js +1 -1
- package/dist/server/guards.js +1 -0
- package/dist/server/pages-assets.js +1 -1
- package/dist/server/render-release.d.ts +17 -0
- package/dist/server/render-release.js +84 -0
- package/dist/server/render-results.d.ts +0 -2
- package/dist/server/render-results.js +4 -6
- package/dist/server/render-tasks.js +3 -3
- package/dist/server/route-table.d.ts +1 -1
- package/dist/server/route-table.js +7 -1
- package/dist/server/tasks.d.ts +15 -0
- package/dist/server/tasks.js +102 -20
- package/dist/setup-guide.d.ts +2 -2
- package/dist/stop-proof.d.ts +67 -0
- package/dist/stop-proof.js +293 -0
- package/dist/store.d.ts +70 -31
- package/dist/store.js +335 -95
- package/dist/surface.js +1 -1
- package/dist/task-outcome-cli.js +19 -0
- package/dist/toolroll-update-ui.js +2 -2
- package/dist/toolroll-update.js +1 -0
- package/dist/work-index.js +7 -9
- package/dist/work-summary.d.ts +2 -2
- package/package.json +1 -1
|
@@ -233,7 +233,9 @@ export declare const obligationSnapshotSchema: z.ZodObject<{
|
|
|
233
233
|
}, z.core.$strict>>;
|
|
234
234
|
}, z.core.$strict>;
|
|
235
235
|
export type ObligationSnapshot = z.infer<typeof obligationSnapshotSchema>;
|
|
236
|
-
/** What the plane writes on the check's stdin before closing it.
|
|
236
|
+
/** What the plane writes on the check's stdin before closing it. `learn` (docs/plans/learning-loops.md) is advisory and
|
|
237
|
+
* apart from everything else: exact case identities the check is asked to say it ran, on their own line
|
|
238
|
+
* (LEARN_PREFIX), never in the report and never part of what the check owes. */
|
|
237
239
|
export type CheckHandshake = {
|
|
238
240
|
nonce: string;
|
|
239
241
|
mode: "full" | "targeted";
|
|
@@ -241,7 +243,29 @@ export type CheckHandshake = {
|
|
|
241
243
|
watch: string[];
|
|
242
244
|
owed?: CheckPart[];
|
|
243
245
|
ciSubstitute?: boolean;
|
|
246
|
+
learn?: string[];
|
|
244
247
|
};
|
|
248
|
+
/** The advisory learning line a check prints before its report: `TOOLROLL-LEARN <json>`, at most 64 KiB, holding only
|
|
249
|
+
* the requested identities that ran. Never part of the report: buildReport, reportLine and fullCheckProblem don't read
|
|
250
|
+
* it, and a missing, malformed or oversized one loses learning evidence only. */
|
|
251
|
+
export declare const LEARN_PREFIX = "TOOLROLL-LEARN ";
|
|
252
|
+
export declare const LEARN_MAX_BYTES: number;
|
|
253
|
+
export declare const LEARN_MAX_IDENTITIES = 200;
|
|
254
|
+
export declare const isExactCase: (identity: unknown) => identity is string;
|
|
255
|
+
/** What the plane asks a check to report for learning: exact cases only, first seen first, at most 200. */
|
|
256
|
+
export declare function learnRequest(identities: readonly unknown[]): string[];
|
|
257
|
+
export declare const learnLineSchema: z.ZodObject<{
|
|
258
|
+
nonce: z.ZodString;
|
|
259
|
+
results: z.ZodArray<z.ZodObject<{
|
|
260
|
+
identity: z.ZodString;
|
|
261
|
+
state: z.ZodEnum<{
|
|
262
|
+
failed: "failed";
|
|
263
|
+
passed: "passed";
|
|
264
|
+
skipped: "skipped";
|
|
265
|
+
}>;
|
|
266
|
+
}, z.core.$strict>>;
|
|
267
|
+
}, z.core.$strict>;
|
|
268
|
+
export type LearnLine = z.infer<typeof learnLineSchema>;
|
|
245
269
|
/** A unit test: its file and full name. */
|
|
246
270
|
export declare const unitIdentity: (file: string, testName: string) => string;
|
|
247
271
|
/** A unit file. */
|
|
@@ -146,6 +146,25 @@ export const obligationSnapshotSchema = versioned(1, {
|
|
|
146
146
|
base: z.int().min(1),
|
|
147
147
|
earlier: z.array(z.strictObject({ run: z.int().min(1), reportDigest: z.string().max(64).nullable(), failed: z.array(z.string().min(1).max(2600)).max(5000) })).max(200),
|
|
148
148
|
});
|
|
149
|
+
// ------------------------------------------------------------------ advisory learning lines (docs/plans/learning-loops.md)
|
|
150
|
+
/** The advisory learning line a check prints before its report: `TOOLROLL-LEARN <json>`, at most 64 KiB, holding only
|
|
151
|
+
* the requested identities that ran. Never part of the report: buildReport, reportLine and fullCheckProblem don't read
|
|
152
|
+
* it, and a missing, malformed or oversized one loses learning evidence only. */
|
|
153
|
+
export const LEARN_PREFIX = "TOOLROLL-LEARN ";
|
|
154
|
+
export const LEARN_MAX_BYTES = 64 * 1024;
|
|
155
|
+
export const LEARN_MAX_IDENTITIES = 200;
|
|
156
|
+
/** An exact case: a unit test (`unit:<file>::<name>`) or a journey (`journey:<script>:<kind>:<group>::<name>`), never a
|
|
157
|
+
* file, group, part or version; one line, no control characters, at most 2600 characters. */
|
|
158
|
+
const EXACT_CASE = /^(?:unit:[^\u0000-\u001f\\:][^\u0000-\u001f\\]*?::[^\u0000-\u001f]+|journey:(?:app|flows):(?:scripted|real):[A-Za-z0-9._/-]{1,200}::[^\u0000-\u001f]+)$/;
|
|
159
|
+
export const isExactCase = (identity) => typeof identity === "string" && identity.length <= 2600 && EXACT_CASE.test(identity);
|
|
160
|
+
/** What the plane asks a check to report for learning: exact cases only, first seen first, at most 200. */
|
|
161
|
+
export function learnRequest(identities) {
|
|
162
|
+
return [...new Set(identities.filter(isExactCase))].slice(0, LEARN_MAX_IDENTITIES);
|
|
163
|
+
}
|
|
164
|
+
export const learnLineSchema = z.strictObject({
|
|
165
|
+
nonce: hex64,
|
|
166
|
+
results: z.array(z.strictObject({ identity: z.string().min(1).max(2600), state: z.enum(["passed", "failed", "skipped"]) })).max(LEARN_MAX_IDENTITIES),
|
|
167
|
+
});
|
|
149
168
|
// ------------------------------------------------------------------ identities
|
|
150
169
|
/** A unit test: its file and full name. */
|
|
151
170
|
export const unitIdentity = (file, testName) => `unit:${file}::${testName}`;
|
package/dist/desktop-update.d.ts
CHANGED
|
@@ -74,8 +74,8 @@ export declare function desktopUpdateWaitingOf(databaseFile: string, inTheWay: (
|
|
|
74
74
|
/** The same settling reconcile the background worker runs: a finished run's leftover record of a process that never
|
|
75
75
|
* started settles once the run's process groups are proven gone. A record nothing can prove either way stays. */
|
|
76
76
|
export declare function settleLeftoverRecords(db: DatabaseSync, now: Date): number;
|
|
77
|
-
/** Store.stopQuiescenceProblem's reasons, said for a person: no internal terms
|
|
78
|
-
*
|
|
77
|
+
/** Store.stopQuiescenceProblem's reasons, said for a person: no internal terms. Nobody vouches that a run stopped:
|
|
78
|
+
* Toolroll proves it itself (stop-proof.ts), and `toolroll run settle` only has it look again now. */
|
|
79
79
|
export declare function plainLingering(fallbackRun: number, problem: string): LingeringRun;
|
|
80
80
|
export declare function previewDesktopUpdate(stateDir: string, installed: string, candidate: string, label: string, hooks?: UpdateHooks): Promise<{
|
|
81
81
|
old: DesktopBundle;
|
package/dist/desktop-update.js
CHANGED
|
@@ -193,11 +193,11 @@ export function lingeringWork(db) {
|
|
|
193
193
|
const lingering = lingeringRun(db);
|
|
194
194
|
return lingering && lingeringWords(lingering);
|
|
195
195
|
}
|
|
196
|
-
export const lingeringWords = (l) => l.action ? `${l.on}.
|
|
196
|
+
export const lingeringWords = (l) => l.action ? `${l.on}. To have Toolroll check again, run: ${l.action}` : `${l.on}.`;
|
|
197
197
|
/** What an update says when it stops waiting on `l`: what is in the way, the command, and that new work resumed. */
|
|
198
198
|
export const stoppedWaitingWords = (l) => `Stopped waiting after 2 minutes. ${l.on}. ${clearWords(l)} New work resumed; nothing was changed.`;
|
|
199
199
|
/** The next step once an update has stopped on `l`. */
|
|
200
|
-
export const clearWords = (l) => l.action ? `
|
|
200
|
+
export const clearWords = (l) => l.action ? `To have Toolroll check again, run ${l.action}, then update again.` : "Update again once it has stopped.";
|
|
201
201
|
export function updateWaitingWords(o) {
|
|
202
202
|
const { app, version, stopped, lingering: l } = o;
|
|
203
203
|
const update = `${app ? "App update" : "Update"} to ${version}`;
|
|
@@ -206,7 +206,7 @@ export function updateWaitingWords(o) {
|
|
|
206
206
|
const words = `${update} is waiting for ${on}. New work is paused.`;
|
|
207
207
|
return { app, version, stopped: false, run: null, on, action: null, words, short: words };
|
|
208
208
|
}
|
|
209
|
-
const next = l.action ? `
|
|
209
|
+
const next = l.action ? ` To have Toolroll check again, run ${l.action}${stopped ? ", then update again" : ""}.` : stopped ? " Update again once it has stopped." : "";
|
|
210
210
|
return { app, version, stopped, run: l.run, on: l.on, action: l.action,
|
|
211
211
|
words: `${update} ${stopped ? "stopped waiting" : "is waiting"}: ${l.on}.${next}`,
|
|
212
212
|
short: `${update} ${stopped ? `stopped: run #${l.run} is in the way` : `is waiting on run #${l.run}`}.${next}` };
|
|
@@ -284,18 +284,22 @@ export function desktopUpdateWaitingOf(databaseFile, inTheWay, stateDirs = [dirn
|
|
|
284
284
|
export function settleLeftoverRecords(db, now) {
|
|
285
285
|
return new Store(db).settleUnspawnedWitnesses(now);
|
|
286
286
|
}
|
|
287
|
-
/** Store.stopQuiescenceProblem's reasons, said for a person: no internal terms
|
|
288
|
-
*
|
|
287
|
+
/** Store.stopQuiescenceProblem's reasons, said for a person: no internal terms. Nobody vouches that a run stopped:
|
|
288
|
+
* Toolroll proves it itself (stop-proof.ts), and `toolroll run settle` only has it look again now. */
|
|
289
289
|
export function plainLingering(fallbackRun, problem) {
|
|
290
290
|
const run = Number(/run #(\d+)/.exec(problem)?.[1] ?? fallbackRun);
|
|
291
|
-
const settle = `toolroll run settle ${run}
|
|
291
|
+
const settle = `toolroll run settle ${run}`;
|
|
292
292
|
const pid = /process (\d+)/.exec(problem)?.[1];
|
|
293
293
|
if (/incomplete spawn witness|before its process witness was recorded/.test(problem))
|
|
294
|
-
return { run, on: `Run #${run} finished,
|
|
295
|
-
if (/belongs to another host|another host/.test(problem))
|
|
294
|
+
return { run, on: `Run #${run} finished, and Toolroll is making sure none of its processes is still running`, action: settle };
|
|
295
|
+
if (/belongs to another host|another host|another computer/.test(problem))
|
|
296
296
|
return { run, on: `Run #${run} finished on another computer, and Toolroll can only confirm its processes ended there`, action: `${settle} (on that computer)` };
|
|
297
|
+
if (/can't be proven stopped: .* is using its folder/.test(problem))
|
|
298
|
+
return { run, on: `Run #${run} finished, but something Toolroll didn't start is using its folder`, action: settle };
|
|
299
|
+
if (/can't be checked on this computer/.test(problem))
|
|
300
|
+
return { run, on: `Run #${run} finished, but Toolroll can't check this computer's processes`, action: settle };
|
|
297
301
|
if (/incomplete native containment custody|cannot be proven empty/.test(problem))
|
|
298
|
-
return { run, on: `Run #${run} finished, but Toolroll can't confirm its sandbox is empty`, action:
|
|
302
|
+
return { run, on: `Run #${run} finished, but Toolroll can't confirm its sandbox is empty`, action: null };
|
|
299
303
|
if (/still has members/.test(problem))
|
|
300
304
|
return { run, on: `Run #${run} finished, but processes are still running in its sandbox`, action: null };
|
|
301
305
|
if (/workspace is still held/.test(problem))
|
package/dist/eval-hooks.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type ts from "typescript-parser";
|
|
1
2
|
/** One required call: in this file, inside this top-level function, a call to this name, the name bound as `from`
|
|
2
3
|
* says (a module specifier its un-renamed named import comes from; `self`: the file's own top-level function; `local`:
|
|
3
4
|
* declared inside `inside`). `args`: the call's arguments, as text, must stay as the base has them. */
|
|
@@ -9,12 +10,17 @@ export type EvalHook = {
|
|
|
9
10
|
args?: true;
|
|
10
11
|
why: string;
|
|
11
12
|
};
|
|
13
|
+
/** `fn` may also be `outer>inner` (docs/plans/learning-loops.md, Protection 3): a `const inner = (…) => …` (or function
|
|
14
|
+
* expression) or `function inner`, bound once in the file and never reassigned, inside the top-level function `outer`
|
|
15
|
+
* and in no other function between them. `strict`: a base that holds the file but can't resolve the pin is a problem,
|
|
16
|
+
* never skipped (a base without the file at all is a project that isn't Toolroll, and holds none). */
|
|
12
17
|
/** One function whose body (its text, as the candidate has it) must stay as the base has it: a top-level function, or
|
|
13
18
|
* `Class#method`, a method of a top-level class. */
|
|
14
19
|
export type EvalPinned = {
|
|
15
20
|
file: string;
|
|
16
21
|
fn: string;
|
|
17
22
|
why: string;
|
|
23
|
+
strict?: true;
|
|
18
24
|
};
|
|
19
25
|
export declare const EVAL_HOOKS: readonly EvalHook[];
|
|
20
26
|
export declare const EVAL_PINNED: readonly EvalPinned[];
|
|
@@ -37,3 +43,52 @@ export declare const EVAL_HOOK_FILES: readonly string[];
|
|
|
37
43
|
* files the verified diff says exist at the candidate, so no bytes is a failed read, never a deletion.
|
|
38
44
|
*/
|
|
39
45
|
export declare function storeRewriteProblem(paths: readonly string[], candidate: ((file: string) => Buffer | null) | null, base: ((file: string) => Buffer | null) | null, pinned?: readonly string[]): string | null;
|
|
46
|
+
/** The calls that record a check's facts: at disposition, and as the check's command starts. */
|
|
47
|
+
export declare const LEARNING_HOOKS: readonly EvalHook[];
|
|
48
|
+
/** The bodies a release can't change by itself, every one strict: what a run is told from project memory, how a memory
|
|
49
|
+
* proposal and a decision are written, and where a check's conditions are captured. */
|
|
50
|
+
export declare const LEARNING_PINNED: readonly EvalPinned[];
|
|
51
|
+
/** Every file the learning manifest reads. */
|
|
52
|
+
export declare const LEARNING_HOOK_FILES: readonly string[];
|
|
53
|
+
/** Each protected table and its only writer modules (the v149 migration in store.ts aside). The decision tables'
|
|
54
|
+
* existing writers stay theirs: project-memory.ts records and retires, project-delete.ts deletes a whole project. */
|
|
55
|
+
export declare const LEARNING_WRITERS: Readonly<Record<string, readonly string[]>>;
|
|
56
|
+
/** One SQL-shaped construct in a file: its static text (a placeholder `${}` where an expression sits), the protected
|
|
57
|
+
* tables it writes, and the top-level declaration it's in ("" for none). `unreadable`: a write whose table name is
|
|
58
|
+
* split around an expression; `bare`: a string that is only a protected table's name (SQL assembled elsewhere). */
|
|
59
|
+
type LearningConstruct = {
|
|
60
|
+
text: string;
|
|
61
|
+
tables: string[];
|
|
62
|
+
inside: string;
|
|
63
|
+
unreadable: boolean;
|
|
64
|
+
bare: boolean;
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* Every construct in the file that could write a protected table: each string literal, template (its static parts)
|
|
68
|
+
* and `+` concatenation (its literal operands), whatever it's passed to — `prepare`, `exec`, a renamed `prepare`, a
|
|
69
|
+
* helper. A write verb next to a protected table's name, a table name split around an expression next to a write
|
|
70
|
+
* word, and a string that is only a protected table's name are each found.
|
|
71
|
+
*/
|
|
72
|
+
export declare function learningConstructs(source: ts.SourceFile): LearningConstruct[];
|
|
73
|
+
/**
|
|
74
|
+
* Why a changed file writes a learning-loops table (or a decision merge or restore) from a place that may not, or null
|
|
75
|
+
* (docs/plans/learning-loops.md, Protection 4). Every changed JavaScript or TypeScript file but tests is parsed; a
|
|
76
|
+
* construct its writer module doesn't own is new unless the base's copy of that file already had exactly that text
|
|
77
|
+
* (counted by its sha256, as storeRewriteProblem does). A file that can't be read or parsed needs a person. It reads
|
|
78
|
+
* text the candidate wrote, never runs it: it raises the bar for review; a person reviewing these files is the backstop.
|
|
79
|
+
*/
|
|
80
|
+
export declare function learningWriteProblem(paths: readonly string[], candidate: ((file: string) => Buffer | null) | null, base: ((file: string) => Buffer | null) | null): string | null;
|
|
81
|
+
/**
|
|
82
|
+
* Why a candidate needs a person for learning loops, or null: a required call dropped, a pinned body changed (strict
|
|
83
|
+
* pins included), or a changed file writing a protected table from outside its writers. With `manifest.base`, only what
|
|
84
|
+
* the base holds is required, as evalHookProblem does — except a strict pin whose file the base holds.
|
|
85
|
+
*/
|
|
86
|
+
export declare function learningHookProblem(manifest: {
|
|
87
|
+
candidate: (file: string) => Buffer | null;
|
|
88
|
+
base?: (file: string) => Buffer | null;
|
|
89
|
+
}, changed: {
|
|
90
|
+
paths: readonly string[];
|
|
91
|
+
candidate: ((file: string) => Buffer | null) | null;
|
|
92
|
+
base: ((file: string) => Buffer | null) | null;
|
|
93
|
+
}): string | null;
|
|
94
|
+
export {};
|
package/dist/eval-hooks.js
CHANGED
|
@@ -36,6 +36,7 @@ export const EVAL_HOOKS = Object.freeze([
|
|
|
36
36
|
{ file: "src/release-auto.ts", inside: "changeProblem", calls: "evalHookProblem", from: "./eval-hooks.js", why: "this check itself" },
|
|
37
37
|
{ file: "src/release-auto.ts", inside: "changeProblem", calls: "autoPathProblem", from: "self", why: "policy paths need a person" },
|
|
38
38
|
{ file: "src/release-auto.ts", inside: "changeProblem", calls: "storeRewriteProblem", from: "./eval-hooks.js", why: "no changed file rewrites a pinned Store method" },
|
|
39
|
+
{ file: "src/release-auto.ts", inside: "changeProblem", calls: "learningHookProblem", from: "./eval-hooks.js", why: "learning loops' hooks, pins and writers (docs/plans/learning-loops.md)" },
|
|
39
40
|
// The invocation gateway (invoke.ts): every agent run an eval world spawns takes a slot.
|
|
40
41
|
{ file: "src/invoke.ts", inside: "invokeAgent", calls: "takeEvalDispatch", from: "./eval-gate.js", why: "every agent dispatch in an eval world takes a slot" },
|
|
41
42
|
// PR #263 review: what the run's billing basis and its unknown cost are computed from is held too, not only the call.
|
|
@@ -227,7 +228,7 @@ function boundAs(source, name, from, inside) {
|
|
|
227
228
|
if (from === "self")
|
|
228
229
|
return compiler.isFunctionDeclaration(only) && only.parent === source ? null : `${name} isn't its own top-level function`;
|
|
229
230
|
if (from === "local")
|
|
230
|
-
return only.pos >= inside.pos && only.end <= inside.end && !compiler.isParameter(only) ? null : `${name} isn't declared inside
|
|
231
|
+
return only.pos >= inside.pos && only.end <= inside.end && !compiler.isParameter(only) ? null : `${name} isn't declared inside its function`;
|
|
231
232
|
if (!compiler.isImportSpecifier(only) || only.propertyName !== undefined || only.isTypeOnly)
|
|
232
233
|
return `${name} isn't imported by its own name`;
|
|
233
234
|
const clause = only.parent.parent;
|
|
@@ -254,6 +255,46 @@ function heldFunction(source, name) {
|
|
|
254
255
|
const fn = topFunction(source, name);
|
|
255
256
|
return fn === null || boundAs(source, name, "self", fn) !== null ? null : fn;
|
|
256
257
|
}
|
|
258
|
+
/** `outer>inner`: the one function-like `inner` declared inside the held top-level `outer` (no other function between
|
|
259
|
+
* them), bound once in the file and never reassigned — a `function inner` or a `const inner =` arrow or function
|
|
260
|
+
* expression. Null otherwise. */
|
|
261
|
+
function nestedFunction(source, spec) {
|
|
262
|
+
const compiler = typescript();
|
|
263
|
+
const [outerName, innerName, ...rest] = spec.split(">");
|
|
264
|
+
if (outerName === undefined || innerName === undefined || rest.length > 0 || outerName === "" || innerName === "")
|
|
265
|
+
return null;
|
|
266
|
+
const outer = heldFunction(source, outerName);
|
|
267
|
+
if (outer === null || outer.body === undefined)
|
|
268
|
+
return null;
|
|
269
|
+
const all = bindingsOf(source, innerName);
|
|
270
|
+
if (all.length !== 1)
|
|
271
|
+
return null;
|
|
272
|
+
const only = all[0];
|
|
273
|
+
let fn = null;
|
|
274
|
+
if (compiler.isFunctionDeclaration(only) && only.body !== undefined)
|
|
275
|
+
fn = only;
|
|
276
|
+
else if (compiler.isVariableDeclaration(only) && only.initializer !== undefined && (compiler.isArrowFunction(only.initializer) || compiler.isFunctionExpression(only.initializer))
|
|
277
|
+
&& compiler.isVariableDeclarationList(only.parent) && (only.parent.flags & compiler.NodeFlags.Const) !== 0)
|
|
278
|
+
fn = only.initializer;
|
|
279
|
+
if (fn === null)
|
|
280
|
+
return null;
|
|
281
|
+
// Directly inside `outer`: the first function-like ancestor of the declaration is `outer` itself.
|
|
282
|
+
for (let at = only.parent; at !== undefined; at = at.parent) {
|
|
283
|
+
if (at === outer)
|
|
284
|
+
return fn;
|
|
285
|
+
if (compiler.isFunctionLike(at) || compiler.isClassLike(at))
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
290
|
+
/** A held function by its manifest name: top-level, `Class#method` or `outer>inner`. */
|
|
291
|
+
function heldNode(source, spec) {
|
|
292
|
+
if (spec.includes(">"))
|
|
293
|
+
return nestedFunction(source, spec);
|
|
294
|
+
if (spec.includes("#"))
|
|
295
|
+
return classMethod(source, spec);
|
|
296
|
+
return heldFunction(source, spec);
|
|
297
|
+
}
|
|
257
298
|
/** A reader that parses each file once. */
|
|
258
299
|
function parser(read) {
|
|
259
300
|
const seen = new Map();
|
|
@@ -265,7 +306,7 @@ function hookIn(tree, hook) {
|
|
|
265
306
|
const parsed = tree(hook.file);
|
|
266
307
|
if ("problem" in parsed)
|
|
267
308
|
return { missing: `${parsed.problem}, and it holds a required call (${hook.why})` };
|
|
268
|
-
const fn = heldFunction(parsed.source, hook.inside);
|
|
309
|
+
const fn = (hook.inside.includes(">") ? nestedFunction(parsed.source, hook.inside) : heldFunction(parsed.source, hook.inside));
|
|
269
310
|
if (fn === null || fn.body === undefined)
|
|
270
311
|
return { missing: `${hook.file} no longer has ${hook.inside} as one function, which must call ${hook.calls} (${hook.why})` };
|
|
271
312
|
const found = callsOf(fn.body, hook.calls);
|
|
@@ -281,7 +322,7 @@ function pinnedText(tree, pin) {
|
|
|
281
322
|
const parsed = tree(pin.file);
|
|
282
323
|
if ("problem" in parsed)
|
|
283
324
|
return null;
|
|
284
|
-
const fn =
|
|
325
|
+
const fn = heldNode(parsed.source, pin.fn);
|
|
285
326
|
if (fn === null)
|
|
286
327
|
return null;
|
|
287
328
|
// The static block's pin holds the class header too: its name, modifiers and heritage, as the base has them.
|
|
@@ -308,8 +349,12 @@ export function evalHookProblem(candidate, base, hooks = EVAL_HOOKS, pinned = EV
|
|
|
308
349
|
}
|
|
309
350
|
for (const pin of pinned) {
|
|
310
351
|
const before = was === null ? null : pinnedText(was, pin);
|
|
311
|
-
if (was !== null && before === null)
|
|
352
|
+
if (was !== null && before === null) {
|
|
353
|
+
// A strict pin the base holds the file of but can't resolve is a problem (learning loops, Protection 2), never a skip.
|
|
354
|
+
if (pin.strict === true && base(pin.file) !== null)
|
|
355
|
+
return `${pin.file} at the base has no ${pin.fn} as one function, which is pinned strictly (${pin.why})`;
|
|
312
356
|
continue;
|
|
357
|
+
}
|
|
313
358
|
const after = pinnedText(now, pin);
|
|
314
359
|
if (after === null)
|
|
315
360
|
return `${pin.file} no longer has ${pin.fn} as one function (${pin.why})`;
|
|
@@ -475,3 +520,181 @@ export function storeRewriteProblem(paths, candidate, base, pinned = PINNED_STOR
|
|
|
475
520
|
}
|
|
476
521
|
return null;
|
|
477
522
|
}
|
|
523
|
+
// ---- learning loops (docs/plans/learning-loops.md, "Safety", Protection 2-4) ----------------------------------------
|
|
524
|
+
/** The calls that record a check's facts: at disposition, and as the check's command starts. */
|
|
525
|
+
export const LEARNING_HOOKS = Object.freeze([
|
|
526
|
+
{ file: "src/builder.ts", inside: "settleProof", calls: "recordCheckFacts", from: "./check-facts.js", why: "every settled check records its facts" },
|
|
527
|
+
{ file: "src/builder.ts", inside: "settleProof>runVerification", calls: "captureCheckConditions", from: "./check-facts.js", why: "a check's conditions are captured as its command starts" },
|
|
528
|
+
]);
|
|
529
|
+
/** The bodies a release can't change by itself, every one strict: what a run is told from project memory, how a memory
|
|
530
|
+
* proposal and a decision are written, and where a check's conditions are captured. */
|
|
531
|
+
export const LEARNING_PINNED = Object.freeze([
|
|
532
|
+
{ file: "src/project-knowledge.ts", fn: "knowledgeContext", strict: true, why: "the decisions and references a run is given, and their snapshot" },
|
|
533
|
+
{ file: "src/project-learning.ts", fn: "learningContext", strict: true, why: "the lessons a run is given, and their snapshot" },
|
|
534
|
+
{ file: "src/memory-pass.ts", fn: "decideProposal", strict: true, why: "a memory proposal's one writer" },
|
|
535
|
+
{ file: "src/project-memory.ts", fn: "recordDecision", strict: true, why: "how a decision is recorded and what it supersedes" },
|
|
536
|
+
{ file: "src/project-memory.ts", fn: "retireDecision", strict: true, why: "how a decision is retired" },
|
|
537
|
+
{ file: "src/builder.ts", fn: "settleProof>runVerification", strict: true, why: "the check's command and the conditions it starts under" },
|
|
538
|
+
]);
|
|
539
|
+
/** Every file the learning manifest reads. */
|
|
540
|
+
export const LEARNING_HOOK_FILES = Object.freeze([...new Set([...LEARNING_HOOKS.map(hook => hook.file), ...LEARNING_PINNED.map(pin => pin.file)])]);
|
|
541
|
+
/** Each protected table and its only writer modules (the v149 migration in store.ts aside). The decision tables'
|
|
542
|
+
* existing writers stay theirs: project-memory.ts records and retires, project-delete.ts deletes a whole project. */
|
|
543
|
+
export const LEARNING_WRITERS = Object.freeze({
|
|
544
|
+
check_fact: ["src/check-facts.ts"], check_result: ["src/check-facts.ts"],
|
|
545
|
+
local_observation: ["src/flaky.ts"], ci_flaky_observation: ["src/flaky-ci.ts"], ci_pairing_rejected: ["src/flaky-ci.ts"],
|
|
546
|
+
learning_watch: ["src/flaky.ts", "src/lesson-notes.ts"],
|
|
547
|
+
lesson_note: ["src/lesson-notes.ts"], lesson_note_tally: ["src/lesson-notes.ts"],
|
|
548
|
+
memory_use: ["src/memory-use.ts"],
|
|
549
|
+
memory_tidy_proposal: ["src/memory-tidy.ts"], decision_merge: ["src/memory-tidy.ts"],
|
|
550
|
+
project_decision: ["src/memory-tidy.ts", "src/project-memory.ts", "src/project-delete.ts"],
|
|
551
|
+
decision_change: ["src/memory-tidy.ts", "src/project-memory.ts", "src/project-delete.ts"],
|
|
552
|
+
});
|
|
553
|
+
/** store.ts's v149 migration: the only place outside the writers that may name these tables in SQL. */
|
|
554
|
+
const LEARNING_MIGRATION = Object.freeze({ "src/store.ts": ["V149_SCHEMA", "V149_TABLES", "V149_REPO_TABLES"] });
|
|
555
|
+
const PROTECTED = Object.keys(LEARNING_WRITERS);
|
|
556
|
+
const tableWrite = (table) => new RegExp(String.raw `(?:\b(?:(?:INSERT(?:\s+OR\s+\w+)?|REPLACE)\s+INTO|UPDATE(?:\s+OR\s+\w+)?|DELETE\s+FROM)\s+(?:main\s*\.\s*)?["\`\[]?${table}["\`\]]?(?!\w)`
|
|
557
|
+
+ String.raw `|\b(?:DROP\s+(?:TABLE|TRIGGER|INDEX|VIEW)(?:\s+IF\s+EXISTS)?|ALTER\s+TABLE)\s+(?:main\s*\.\s*)?["\`\[]?${table})`, "i");
|
|
558
|
+
const TABLE_WRITES = PROTECTED.map(table => ({ table, write: tableWrite(table) }));
|
|
559
|
+
const WRITE_WORD = /\b(?:INSERT|UPDATE|DELETE|REPLACE|DROP|ALTER)\b/i;
|
|
560
|
+
const BARE_NAME = new RegExp(String.raw `^\s*(?:main\s*\.\s*)?["\`\[]?(?:${PROTECTED.join("|")})["\`\]]?\s*$`, "i");
|
|
561
|
+
/**
|
|
562
|
+
* Every construct in the file that could write a protected table: each string literal, template (its static parts)
|
|
563
|
+
* and `+` concatenation (its literal operands), whatever it's passed to — `prepare`, `exec`, a renamed `prepare`, a
|
|
564
|
+
* helper. A write verb next to a protected table's name, a table name split around an expression next to a write
|
|
565
|
+
* word, and a string that is only a protected table's name are each found.
|
|
566
|
+
*/
|
|
567
|
+
export function learningConstructs(source) {
|
|
568
|
+
const compiler = typescript();
|
|
569
|
+
const found = [];
|
|
570
|
+
const topOf = (node) => {
|
|
571
|
+
let at = node;
|
|
572
|
+
while (at.parent !== undefined && !compiler.isSourceFile(at.parent))
|
|
573
|
+
at = at.parent;
|
|
574
|
+
if (compiler.isFunctionDeclaration(at) || compiler.isClassDeclaration(at))
|
|
575
|
+
return at.name?.text ?? "";
|
|
576
|
+
if (compiler.isVariableStatement(at))
|
|
577
|
+
return at.declarationList.declarations.map(one => compiler.isIdentifier(one.name) ? one.name.text : "").join(",");
|
|
578
|
+
return "";
|
|
579
|
+
};
|
|
580
|
+
const fragmentRisk = (parts) => {
|
|
581
|
+
// Around each placeholder: the identifier characters just before and just after it.
|
|
582
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
583
|
+
const before = /[A-Za-z0-9_]*$/.exec(parts[i])[0].toLowerCase(), after = /^[A-Za-z0-9_]*/.exec(parts[i + 1])[0].toLowerCase();
|
|
584
|
+
if (before === "" && after === "")
|
|
585
|
+
continue;
|
|
586
|
+
if (PROTECTED.some(table => (before !== "" && table.startsWith(before) && table !== before) || (after !== "" && table.endsWith(after) && table !== after)
|
|
587
|
+
|| (before !== "" && after !== "" && table.startsWith(before) && table.endsWith(after))))
|
|
588
|
+
return true;
|
|
589
|
+
}
|
|
590
|
+
return false;
|
|
591
|
+
};
|
|
592
|
+
const consider = (node, parts) => {
|
|
593
|
+
const text = parts.join("${}");
|
|
594
|
+
const tables = TABLE_WRITES.filter(one => one.write.test(text)).map(one => one.table);
|
|
595
|
+
const unreadable = parts.length > 1 && WRITE_WORD.test(text) && fragmentRisk(parts);
|
|
596
|
+
const bare = parts.length === 1 && BARE_NAME.test(text);
|
|
597
|
+
if (tables.length > 0 || unreadable || bare)
|
|
598
|
+
found.push({ text: text.replace(/\s+/g, " ").trim(), tables, inside: topOf(node), unreadable, bare });
|
|
599
|
+
};
|
|
600
|
+
const isPlus = (node) => compiler.isBinaryExpression(node) && node.operatorToken.kind === compiler.SyntaxKind.PlusToken;
|
|
601
|
+
const flatten = (node, into) => {
|
|
602
|
+
let at = node;
|
|
603
|
+
while (compiler.isParenthesizedExpression(at))
|
|
604
|
+
at = at.expression;
|
|
605
|
+
if (isPlus(at)) {
|
|
606
|
+
flatten(at.left, into);
|
|
607
|
+
flatten(at.right, into);
|
|
608
|
+
}
|
|
609
|
+
else
|
|
610
|
+
into.push(at);
|
|
611
|
+
};
|
|
612
|
+
const visit = (node) => {
|
|
613
|
+
if (isPlus(node) && !(node.parent !== undefined && (isPlus(node.parent) || (compiler.isParenthesizedExpression(node.parent) && node.parent.parent !== undefined && isPlus(node.parent.parent))))) {
|
|
614
|
+
const operands = [];
|
|
615
|
+
flatten(node, operands);
|
|
616
|
+
if (operands.some(one => compiler.isStringLiteralLike(one) || compiler.isTemplateExpression(one))) {
|
|
617
|
+
const parts = [""];
|
|
618
|
+
for (const one of operands) {
|
|
619
|
+
if (compiler.isStringLiteralLike(one))
|
|
620
|
+
parts[parts.length - 1] += one.text;
|
|
621
|
+
else if (compiler.isTemplateExpression(one)) {
|
|
622
|
+
parts[parts.length - 1] += one.head.text;
|
|
623
|
+
for (const span of one.templateSpans)
|
|
624
|
+
parts.push(span.literal.text);
|
|
625
|
+
}
|
|
626
|
+
else
|
|
627
|
+
parts.push("");
|
|
628
|
+
}
|
|
629
|
+
if (parts.length > 1 || operands.length > 1)
|
|
630
|
+
consider(node, parts);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
if (compiler.isStringLiteralLike(node))
|
|
634
|
+
consider(node, [node.text]);
|
|
635
|
+
else if (compiler.isTemplateExpression(node))
|
|
636
|
+
consider(node, [node.head.text, ...node.templateSpans.map(span => span.literal.text)]);
|
|
637
|
+
compiler.forEachChild(node, visit);
|
|
638
|
+
};
|
|
639
|
+
visit(source);
|
|
640
|
+
return found;
|
|
641
|
+
}
|
|
642
|
+
/** Whether this construct may stand in this file: its writer module for every table it writes, or the migration's own
|
|
643
|
+
* declarations in store.ts. An unreadable or bare construct stands only in a writer of some protected table. */
|
|
644
|
+
function learningAllowed(file, one) {
|
|
645
|
+
if ((LEARNING_MIGRATION[file] ?? []).some(name => one.inside.split(",").includes(name)))
|
|
646
|
+
return true;
|
|
647
|
+
if (one.tables.length > 0)
|
|
648
|
+
return one.tables.every(table => LEARNING_WRITERS[table].includes(file));
|
|
649
|
+
return Object.values(LEARNING_WRITERS).some(writers => writers.includes(file));
|
|
650
|
+
}
|
|
651
|
+
/**
|
|
652
|
+
* Why a changed file writes a learning-loops table (or a decision merge or restore) from a place that may not, or null
|
|
653
|
+
* (docs/plans/learning-loops.md, Protection 4). Every changed JavaScript or TypeScript file but tests is parsed; a
|
|
654
|
+
* construct its writer module doesn't own is new unless the base's copy of that file already had exactly that text
|
|
655
|
+
* (counted by its sha256, as storeRewriteProblem does). A file that can't be read or parsed needs a person. It reads
|
|
656
|
+
* text the candidate wrote, never runs it: it raises the bar for review; a person reviewing these files is the backstop.
|
|
657
|
+
*/
|
|
658
|
+
export function learningWriteProblem(paths, candidate, base) {
|
|
659
|
+
const scanned = paths.filter(path => /\.(?:[cm]?[jt]sx?)$/.test(path) && !/\.test\.[cm]?[jt]sx?$/.test(path) && !path.startsWith("test/")
|
|
660
|
+
&& !path.startsWith("node_modules/") && !path.includes("/node_modules/"));
|
|
661
|
+
if (scanned.length === 0)
|
|
662
|
+
return null;
|
|
663
|
+
if (candidate === null || base === null)
|
|
664
|
+
return "the changed files can't be read";
|
|
665
|
+
const key = (text) => createHash("sha256").update(text).digest("hex");
|
|
666
|
+
for (const path of scanned) {
|
|
667
|
+
const bytes = candidate(path);
|
|
668
|
+
if (bytes === null)
|
|
669
|
+
return `${path} can't be read`;
|
|
670
|
+
const now = parse(path, bytes);
|
|
671
|
+
if ("problem" in now)
|
|
672
|
+
return now.problem;
|
|
673
|
+
const before = base(path);
|
|
674
|
+
const was = before === null ? null : parse(path, before);
|
|
675
|
+
const kept = new Map();
|
|
676
|
+
if (was !== null && !("problem" in was))
|
|
677
|
+
for (const one of learningConstructs(was.source))
|
|
678
|
+
kept.set(key(one.text), (kept.get(key(one.text)) ?? 0) + 1);
|
|
679
|
+
for (const one of learningConstructs(now.source)) {
|
|
680
|
+
if (learningAllowed(path, one))
|
|
681
|
+
continue;
|
|
682
|
+
const left = kept.get(key(one.text)) ?? 0;
|
|
683
|
+
if (left > 0) {
|
|
684
|
+
kept.set(key(one.text), left - 1);
|
|
685
|
+
continue;
|
|
686
|
+
}
|
|
687
|
+
const what = one.unreadable ? "builds a learning-loops table's name around an expression in SQL" : one.bare ? "names a learning-loops table outside its writers" : "writes learning-loops rows outside their writers";
|
|
688
|
+
return `${path} ${what} (\`${one.text.slice(0, 80)}\`)`;
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
return null;
|
|
692
|
+
}
|
|
693
|
+
/**
|
|
694
|
+
* Why a candidate needs a person for learning loops, or null: a required call dropped, a pinned body changed (strict
|
|
695
|
+
* pins included), or a changed file writing a protected table from outside its writers. With `manifest.base`, only what
|
|
696
|
+
* the base holds is required, as evalHookProblem does — except a strict pin whose file the base holds.
|
|
697
|
+
*/
|
|
698
|
+
export function learningHookProblem(manifest, changed) {
|
|
699
|
+
return evalHookProblem(manifest.candidate, manifest.base, LEARNING_HOOKS, LEARNING_PINNED) ?? learningWriteProblem(changed.paths, changed.candidate, changed.base);
|
|
700
|
+
}
|
package/dist/flaky.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mixed outcomes under recorded conditions (docs/plans/learning-loops.md, "Flaky tests"): advisory only. Nothing here
|
|
3
|
+
* changes a check's outcome, a verdict, an approval or a rerun; rerunning stays a person's `task regate`.
|
|
4
|
+
*
|
|
5
|
+
* Slice 1 holds the tracked identities an observation adds (`learning_watch`, reason `observation`): every Full check
|
|
6
|
+
* with a handshake asks about them for 90 days (check-facts.ts learnRequestFor). Lesson notes (lesson-notes.ts) add
|
|
7
|
+
* their own. This module and lesson-notes.ts are the only writers of `learning_watch`; release-auto's learning scan
|
|
8
|
+
* (eval-hooks.ts learningWriteProblem) needs a person for any other. A policy path (release-auto.ts EVAL_POLICY).
|
|
9
|
+
*/
|
|
10
|
+
import type { Store } from "./store.js";
|
|
11
|
+
/** Track an identity a local observation named (one row per observation; first write wins, never rewritten). */
|
|
12
|
+
export declare function watchObservation(store: Store, input: {
|
|
13
|
+
repo: string;
|
|
14
|
+
identity: string;
|
|
15
|
+
observation: number;
|
|
16
|
+
now: Date;
|
|
17
|
+
}): boolean;
|
package/dist/flaky.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Track an identity a local observation named (one row per observation; first write wins, never rewritten). */
|
|
2
|
+
export function watchObservation(store, input) {
|
|
3
|
+
if (input.repo.trim() === "" || input.identity.length < 1 || input.identity.length > 2600 || !Number.isSafeInteger(input.observation) || input.observation < 1) {
|
|
4
|
+
throw new Error("a tracked identity needs its project, a bounded identity and the observation that named it");
|
|
5
|
+
}
|
|
6
|
+
const added = store.handle.prepare(`INSERT INTO learning_watch (repo, identity, reason, source_ref, since, until) VALUES (?, ?, 'observation', ?, ?, NULL)
|
|
7
|
+
ON CONFLICT (repo, identity, reason, source_ref) DO NOTHING`).run(input.repo, input.identity, `observation:${input.observation}`, input.now.toISOString());
|
|
8
|
+
return Number(added.changes) === 1;
|
|
9
|
+
}
|
package/dist/needs-you.d.ts
CHANGED
|
@@ -6,11 +6,11 @@
|
|
|
6
6
|
* The words never name the machine's internals; `plainSentence` keeps a
|
|
7
7
|
* recorded reason only when it is free of them. */
|
|
8
8
|
/** What a person is asked for. */
|
|
9
|
-
export type NeedKey = "approval" | "card" | "answer" | "questions" | "sign-in" | "
|
|
9
|
+
export type NeedKey = "approval" | "card" | "answer" | "questions" | "sign-in" | "stop-unproven" | "hold" | "choose-project" | "define-task" | "fix-request" | "choose-agent" | "fix-dependency" | "add-requirement" | "connect-builder" | "start-builder" | "vanished" | "review-result" | "rebuild" | "replan" | "earlier-version" | "other";
|
|
10
10
|
/** What the work waits for when no person can act. */
|
|
11
|
-
export type WaitKey = "build-stopping" | "build-finishing" | "other-computer" | "card-reply" | "card-time" | "card-ci";
|
|
11
|
+
export type WaitKey = "build-stopping" | "build-finishing" | "build-checking" | "other-computer" | "card-reply" | "card-time" | "card-ci";
|
|
12
12
|
/** The action's code: the WorkAction that owns the act, or one of the acts a surface renders itself. */
|
|
13
|
-
export type NeedActionCode = "approve-scope" | "approve-card" | "answer-decision" | "inspect-decisions" | "sign-in" | "
|
|
13
|
+
export type NeedActionCode = "approve-scope" | "approve-card" | "answer-decision" | "inspect-decisions" | "sign-in" | "recheck-stopped" | "inspect-hold" | "place-task" | "write-scope" | "select-agent" | "repair-dependency" | "repair-capability" | "start-worker" | "reconcile-run" | "open-result" | "retry-task" | "inspect-task";
|
|
14
14
|
export type NeedAction = {
|
|
15
15
|
code: NeedActionCode;
|
|
16
16
|
label: string;
|
|
@@ -76,14 +76,14 @@ export declare const hasInternalWords: (text: string) => boolean;
|
|
|
76
76
|
* it reads plainly, otherwise the need's own words. */
|
|
77
77
|
export declare function needSentence(key: NeedKey, reason: string | null, context?: NeedContext): string;
|
|
78
78
|
export declare function waitSentence(key: WaitKey | undefined, reason: string | null, context?: NeedContext): string;
|
|
79
|
-
/** A finished run Toolroll can't prove stopped (store.stopQuiescenceFact):
|
|
80
|
-
*
|
|
81
|
-
*
|
|
79
|
+
/** A finished run Toolroll can't yet prove stopped (store.stopQuiescenceFact). Nobody is asked to confirm it:
|
|
80
|
+
* `unprovable` is Toolroll's own check still to run (stop-proof.ts), which settles by itself; `unknown` is that this
|
|
81
|
+
* computer can't look, or something Toolroll didn't start is in the build's folder — held, with nothing to vouch for. */
|
|
82
82
|
export declare function processNeedOf(fact: {
|
|
83
83
|
run: number;
|
|
84
84
|
kind: "open" | "alive" | "elsewhere" | "unprovable" | "unknown" | "settling";
|
|
85
85
|
} | null): {
|
|
86
|
-
need: "
|
|
86
|
+
need: "stop-unproven";
|
|
87
87
|
build: number;
|
|
88
88
|
} | {
|
|
89
89
|
wait: WaitKey;
|
package/dist/needs-you.js
CHANGED
|
@@ -22,9 +22,10 @@ export const NEEDS = {
|
|
|
22
22
|
answer: { sentence: () => "Answer the question so the work can continue.", action: { code: "answer-decision", label: "Answer the question" }, useReason: always },
|
|
23
23
|
questions: { sentence: () => "Too many questions are open. Answer one so this task can start.", action: { code: "inspect-decisions", label: "Answer questions" } },
|
|
24
24
|
"sign-in": { sentence: context => `${context.provider ?? "Your agent"} needs you to sign in again. The task starts on its own after.`, action: { code: "sign-in", label: "Sign in again" }, useReason: reason => /sign in|API key/i.test(reason) },
|
|
25
|
-
|
|
26
|
-
//
|
|
27
|
-
|
|
25
|
+
// Toolroll proves a build stopped itself (stop-proof.ts); nobody is asked to vouch for it. Only when this computer
|
|
26
|
+
// can't look, or something Toolroll didn't start is using the build's folder, is the work held: the person can close
|
|
27
|
+
// what's theirs and have it look again, nothing more.
|
|
28
|
+
"stop-unproven": { sentence: context => `Toolroll can't prove ${build(context)} stopped from this computer, so the work is held. Close anything of yours that is using its folder, then check again.`, action: { code: "recheck-stopped", label: "Check again" } },
|
|
28
29
|
hold: { sentence: () => "A hold is keeping this task from starting. Review it to release it.", action: { code: "inspect-hold", label: "Review hold" } },
|
|
29
30
|
"choose-project": { sentence: () => "Choose a project so a builder can start.", action: { code: "place-task", label: "Choose a project" } },
|
|
30
31
|
"define-task": { sentence: () => "Say what the task should do so it can be planned.", action: { code: "write-scope", label: "Define the task" } },
|
|
@@ -70,7 +71,7 @@ export function askChipOf(read) {
|
|
|
70
71
|
return "Question";
|
|
71
72
|
if (read.need === "hold")
|
|
72
73
|
return "On hold";
|
|
73
|
-
if (read.need === "vanished" || read.need === "
|
|
74
|
+
if (read.need === "vanished" || read.need === "stop-unproven")
|
|
74
75
|
return "Stopped";
|
|
75
76
|
if (read.need === "sign-in")
|
|
76
77
|
return "Sign in";
|
|
@@ -100,11 +101,12 @@ export const NEED_ASK = {
|
|
|
100
101
|
approval: "decide", card: "decide", answer: "decide", questions: "decide", "choose-project": "decide", "define-task": "decide",
|
|
101
102
|
"fix-request": "decide", "choose-agent": "decide",
|
|
102
103
|
"review-result": "review", rebuild: "review", replan: "decide",
|
|
103
|
-
"sign-in": "unblock", "
|
|
104
|
+
"sign-in": "unblock", "stop-unproven": "unblock", hold: "unblock", "fix-dependency": "unblock",
|
|
104
105
|
"add-requirement": "unblock", "connect-builder": "unblock", "start-builder": "unblock", vanished: "unblock", "earlier-version": "unblock", other: "unblock",
|
|
105
106
|
};
|
|
106
107
|
export const WAITS = {
|
|
107
108
|
"build-finishing": () => "Finishing up; this clears on its own.",
|
|
109
|
+
"build-checking": context => `Making sure ${build(context)} stopped. This clears on its own.`,
|
|
108
110
|
"build-stopping": context => `Waiting for ${build(context)} to stop. Nothing is needed from you.`,
|
|
109
111
|
"other-computer": context => `Waiting for the computer that ran ${build(context)} to confirm it stopped.`,
|
|
110
112
|
"card-reply": () => "Waiting for a reply.",
|
|
@@ -130,18 +132,18 @@ export function waitSentence(key, reason, context = {}) {
|
|
|
130
132
|
const plain = reason?.trim() || null;
|
|
131
133
|
return plain !== null && !hasInternalWords(plain) ? plain : "Waiting for the work to continue on its own.";
|
|
132
134
|
}
|
|
133
|
-
/** A finished run Toolroll can't prove stopped (store.stopQuiescenceFact):
|
|
134
|
-
*
|
|
135
|
-
*
|
|
135
|
+
/** A finished run Toolroll can't yet prove stopped (store.stopQuiescenceFact). Nobody is asked to confirm it:
|
|
136
|
+
* `unprovable` is Toolroll's own check still to run (stop-proof.ts), which settles by itself; `unknown` is that this
|
|
137
|
+
* computer can't look, or something Toolroll didn't start is in the build's folder — held, with nothing to vouch for. */
|
|
136
138
|
export function processNeedOf(fact) {
|
|
137
139
|
if (fact === null)
|
|
138
140
|
return null;
|
|
139
141
|
if (fact.kind === "settling")
|
|
140
142
|
return { wait: "build-finishing", build: fact.run };
|
|
141
143
|
if (fact.kind === "unprovable")
|
|
142
|
-
return {
|
|
144
|
+
return { wait: "build-checking", build: fact.run };
|
|
143
145
|
if (fact.kind === "unknown")
|
|
144
|
-
return { need: "
|
|
146
|
+
return { need: "stop-unproven", build: fact.run };
|
|
145
147
|
return { wait: fact.kind === "elsewhere" ? "other-computer" : "build-stopping", build: fact.run };
|
|
146
148
|
}
|
|
147
149
|
/** Dispatch and work-index codes that wait on a person, as their need. */
|