toolroll 0.9.74 → 0.9.76
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 +80 -0
- package/dist/agent-fence.js +8 -0
- package/dist/approval-authority.js +6 -2
- package/dist/approval-rules-ui.d.ts +11 -0
- package/dist/approval-rules-ui.js +13 -1
- package/dist/assignment.d.ts +3 -0
- package/dist/assignment.js +27 -8
- package/dist/batch-checks.js +3 -1
- package/dist/browser/workspace.js +27 -27
- package/dist/browser-workspace.d.ts +1 -1
- package/dist/builder.js +1 -1
- package/dist/chat-acceptance.d.ts +4 -1
- package/dist/chat-acceptance.js +13 -3
- package/dist/cli.js +1 -1
- package/dist/coding-handoff.js +4 -3
- package/dist/coding-ui.js +2 -2
- package/dist/coding-workspace.js +23 -9
- package/dist/contracts/console-api.d.ts +6 -1
- package/dist/contracts/console-api.js +11 -0
- package/dist/control-ui.d.ts +3 -2
- package/dist/control-ui.js +7 -2
- package/dist/demo.js +5 -3
- package/dist/deploy-gate.d.ts +3 -0
- package/dist/deploy-gate.js +3 -2
- package/dist/desktop-update.d.ts +4 -1
- package/dist/desktop-update.js +43 -9
- package/dist/discover.js +6 -1
- package/dist/first-run.d.ts +8 -5
- package/dist/first-run.js +42 -7
- package/dist/flow-card-task.js +3 -2
- package/dist/flow-unattended.d.ts +2 -0
- package/dist/flow-unattended.js +12 -1
- package/dist/git-incident.d.ts +49 -0
- package/dist/git-incident.js +69 -0
- package/dist/git-views.d.ts +20 -0
- package/dist/git-views.js +120 -0
- package/dist/grant.js +3 -1
- package/dist/integrations.d.ts +4 -3
- package/dist/integrations.js +5 -6
- package/dist/lead-contract.d.ts +1 -1
- package/dist/lead-contract.js +2 -2
- package/dist/lead-follow.js +3 -1
- package/dist/openrouter-models.d.ts +5 -2
- package/dist/openrouter-models.js +7 -4
- package/dist/operate.js +9 -1
- package/dist/planner.js +5 -1
- package/dist/project-knowledge.js +1 -1
- package/dist/project-learning.js +65 -7
- package/dist/project-tools.js +19 -1
- package/dist/pull-request-flow.js +4 -0
- package/dist/pull-request-ui.d.ts +1 -1
- package/dist/pull-request-ui.js +5 -1
- package/dist/push.d.ts +2 -0
- package/dist/push.js +24 -19
- package/dist/read-only-request.d.ts +4 -0
- package/dist/read-only-request.js +15 -0
- package/dist/release-adapters.d.ts +24 -2
- package/dist/release-adapters.js +136 -10
- package/dist/release-authority.d.ts +46 -0
- package/dist/release-authority.js +64 -0
- package/dist/release-auto.d.ts +153 -0
- package/dist/release-auto.js +496 -0
- package/dist/release-cli.js +56 -2
- package/dist/release-coverage.js +1 -1
- package/dist/release.d.ts +105 -1
- package/dist/release.js +229 -9
- package/dist/repository-context.js +5 -2
- package/dist/result-follow-ups.js +23 -2
- package/dist/result-review.js +5 -1
- package/dist/scope.d.ts +31 -2
- package/dist/scope.js +44 -3
- package/dist/scout.js +5 -1
- package/dist/serve.js +162 -6
- package/dist/server/agents-live-filed.d.ts +17 -0
- package/dist/server/agents-live-filed.js +97 -0
- package/dist/server/agents-live-room.js +4 -1
- package/dist/server/chat.js +31 -23
- package/dist/server/http.d.ts +5 -0
- package/dist/server/last-checks.d.ts +17 -0
- package/dist/server/last-checks.js +22 -0
- package/dist/server/pages.js +18 -4
- package/dist/server/people-tokens.js +7 -4
- package/dist/server/render-pages.d.ts +2 -1
- package/dist/server/render-pages.js +28 -24
- package/dist/server/render-results.d.ts +11 -2
- package/dist/server/render-results.js +54 -12
- package/dist/server/render-settings.js +3 -3
- package/dist/server/render-tasks.d.ts +10 -0
- package/dist/server/render-tasks.js +30 -17
- package/dist/server/route-table.js +17 -1
- package/dist/server/runtime.d.ts +16 -1
- package/dist/server/settings.js +111 -31
- package/dist/server/task-views.js +8 -1
- package/dist/server/tasks.js +11 -2
- package/dist/storage-ui.d.ts +2 -0
- package/dist/storage-ui.js +1 -1
- package/dist/storage.js +2 -1
- package/dist/store.d.ts +4 -1
- package/dist/store.js +52 -13
- package/dist/subagent-work.js +3 -0
- package/dist/task-base.js +1 -1
- package/dist/task-status.d.ts +28 -1
- package/dist/task-status.js +93 -11
- package/dist/teardown-git.d.ts +59 -5
- package/dist/teardown-git.js +1022 -90
- package/dist/telegram-progress.js +4 -4
- package/dist/telegram-status.js +8 -3
- package/dist/test-infra.d.ts +26 -0
- package/dist/test-infra.js +321 -0
- package/dist/tool-secret-store.js +8 -0
- package/dist/toolroll-update-ui.d.ts +2 -0
- package/dist/toolroll-update-ui.js +4 -3
- package/dist/toolroll-update.js +10 -9
- package/dist/work-index.js +29 -12
- package/dist/workspace-ui.d.ts +5 -0
- package/dist/workspace-ui.js +38 -10
- package/dist/worktree.d.ts +4 -0
- package/dist/worktree.js +13 -7
- package/package.json +1 -1
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import { assignmentOf } from "./assignment.js";
|
|
8
8
|
import { chatResultHref, chatControlHref } from "./chat-controls.js";
|
|
9
9
|
import { phoneText, projectLabel } from "./telegram-status.js";
|
|
10
|
-
import { assignmentStatusFacts, headlineEmoji, pullRequestFactOf, requirementsOf, statusDetailLines, unverifiedWhenRefuted, taskStatusOf } from "./task-status.js";
|
|
10
|
+
import { approvedCheckOf, assignmentStatusFacts, checksFactOf, headlineEmoji, pullRequestFactOf, requirementsOf, statusDetailLines, unverifiedWhenRefuted, taskStatusOf } from "./task-status.js";
|
|
11
11
|
import { leadClaimOf } from "./lead-voice.js";
|
|
12
12
|
import { leadNameOf } from "./lead-identity.js";
|
|
13
13
|
import { chatTitle } from "./chat-voice.js";
|
|
@@ -46,10 +46,10 @@ export function telegramProgressCard(store, run, taskId, project, now = new Date
|
|
|
46
46
|
const proof = store.proofVerdictFor(run.id);
|
|
47
47
|
const machine = proof?.machineVerdict ?? proof?.verdict ?? null;
|
|
48
48
|
const exit = failedCheckExit(proof?.reasons ?? []);
|
|
49
|
-
|
|
49
|
+
// The one answer (task-status.ts approvedCheckOf) when a saved check was read; a verdict alone claims no pass.
|
|
50
|
+
const checks = recorded !== null ? approvedCheckOf(machine, proof?.reasons ?? [], checksFactOf(recorded, run.headRevision))
|
|
50
51
|
: exit !== null ? { status: "failed", exitCode: exit, head: run.headRevision }
|
|
51
|
-
: machine === "
|
|
52
|
-
: machine === "attested" ? { status: "not-run", exitCode: null, head: null } : null;
|
|
52
|
+
: machine === "attested" ? { status: "not-run", exitCode: null, head: null } : null;
|
|
53
53
|
facts = { stage: "finished", report: run.role === "scout", checks, requirements: unverifiedWhenRefuted(requirementsOf(proof?.matrix), proof?.verdict),
|
|
54
54
|
...(pullRequest === undefined ? {} : { pullRequest }) };
|
|
55
55
|
}
|
package/dist/telegram-status.js
CHANGED
|
@@ -4,8 +4,8 @@ import { manualReviewOnly } from "./proof.js";
|
|
|
4
4
|
* No provider calls, repository access, new workflow state, or inferred success. */
|
|
5
5
|
import { diagnoseTaskDispatch, withDispatchDiagnoses } from "./dispatch.js";
|
|
6
6
|
import { familyCompleted } from "./result-completion.js";
|
|
7
|
-
import { assignmentStatusFacts, plainReasonOf, stageOfDispatch, taskStatusOf } from "./task-status.js";
|
|
8
|
-
import { assignmentOf } from "./assignment.js";
|
|
7
|
+
import { approvedCheckOf, approvedCheckStateOf, assignmentStatusFacts, checksFactOf, plainReasonOf, stageOfDispatch, taskStatusOf } from "./task-status.js";
|
|
8
|
+
import { assignmentChecksForRun, assignmentOf } from "./assignment.js";
|
|
9
9
|
import { CHAT_CONTROLS, chatControlHref, chatResultHref } from "./chat-controls.js";
|
|
10
10
|
export function phoneCommand(text) {
|
|
11
11
|
// Slash forms everywhere; the bare words too, because Slack and Discord
|
|
@@ -257,7 +257,12 @@ export function phoneTaskView(store, repos, id, now, root) {
|
|
|
257
257
|
const proof = store.proofVerdictFor(result.id);
|
|
258
258
|
link = taskLinkFor(id, d, { run: result.id, verdict: proof !== null });
|
|
259
259
|
const accepted = store.proofAcceptance(result.id) !== null;
|
|
260
|
-
|
|
260
|
+
// Did the approved check pass (task-status.ts approvedCheckOf): read with the saved files, else no claim at all.
|
|
261
|
+
const checkNow = root === undefined || proof?.verdict !== "verified" ? null
|
|
262
|
+
: approvedCheckStateOf(approvedCheckOf(proof.verdict, proof.reasons, checksFactOf(assignmentChecksForRun(store, root, result.id, now), result.headRevision)));
|
|
263
|
+
const verifiedWords = root === undefined ? "Open the result to see its checks" : checkNow === "passed" ? "Checks passed at completion"
|
|
264
|
+
: checkNow === "failed" ? "Checks failed" : checkNow === "running" ? "Checks running" : "The check result couldn't be read";
|
|
265
|
+
const proofWords = { verified: verifiedWords, attested: "Checks reported by the agent, not run by Toolroll", short: "Required saved material is missing", refuted: "The saved result conflicts with the approved scope" };
|
|
261
266
|
lines.push(`Checks: ${proof === null ? "No completion record saved" : manualReviewOnly(proof) ? accepted ? "Accepted by a person; the recorded checks are unchanged" : "A person must inspect this result; no recorded check failed" : proofWords[proof.verdict]}.`);
|
|
262
267
|
if (manualReviewOnly(proof) && !accepted)
|
|
263
268
|
lines.push('Reply “Send the result summary” for the checks, notes and screenshots.');
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** The product's entry points: the CLI binary, the processes it starts by file name, and the browser bundles. */
|
|
2
|
+
export declare const PRODUCT_ENTRIES: readonly string[];
|
|
3
|
+
/** A specifier reached from the tests that names no repository file and no known package: `prefix` is the repository
|
|
4
|
+
* path it might name (a folder or file stem), or null when it might name anything. */
|
|
5
|
+
export type Unresolved = {
|
|
6
|
+
from: string;
|
|
7
|
+
specifier: string | null;
|
|
8
|
+
prefix: string | null;
|
|
9
|
+
};
|
|
10
|
+
export type TestInfrastructure = {
|
|
11
|
+
infra: Set<string>;
|
|
12
|
+
unresolved: Unresolved[];
|
|
13
|
+
/** What the product's entry points reach (trusted product code, by Alex's decision). */
|
|
14
|
+
product: Set<string>;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* The infrastructure among `files` (path → text: the tree's sources, and its tsconfig.json and package.json): reachable
|
|
18
|
+
* from a test root — every test file, every file under `test/`, every vitest config and its setup files — not itself a
|
|
19
|
+
* test, and not reachable from PRODUCT_ENTRIES; plus every specifier reached from a test root that couldn't be resolved.
|
|
20
|
+
*/
|
|
21
|
+
export declare function testInfrastructure(files: ReadonlyMap<string, string>, allPaths?: ReadonlySet<string>): TestInfrastructure;
|
|
22
|
+
/**
|
|
23
|
+
* Whether a changed path is something an unresolved specifier might name. A computed one (no prefix) might name anything
|
|
24
|
+
* the other rules don't already trust: any source file that is neither a test nor product code.
|
|
25
|
+
*/
|
|
26
|
+
export declare function mightName(open: Unresolved, path: string, found: TestInfrastructure): boolean;
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test infrastructure, found structurally (docs/plans/auto-approve-green.md): every non-test source file that the tests
|
|
3
|
+
* or the test runner's own setup import, directly or through other files, and that the product's entry points never
|
|
4
|
+
* reach. Changing one changes how tests check things — an automatic release approval leaves that to a person.
|
|
5
|
+
*
|
|
6
|
+
* Imports are read with the TypeScript compiler (typescript-parser, a runtime dependency): `preProcessFile` for static,
|
|
7
|
+
* dynamic and `require` specifiers (comments never count), and the syntax tree for dynamic imports or requires whose
|
|
8
|
+
* specifier isn't a plain string. Specifiers resolve as the build resolves them: relative paths (`./x.js` → `./x.ts`),
|
|
9
|
+
* the project's tsconfig `paths`, and package.json `imports`. Fail closed: a specifier reached from the tests that
|
|
10
|
+
* resolves to no file and to no known package is "unresolved", with what it might name, and the caller refuses when the
|
|
11
|
+
* change touches anything it might name.
|
|
12
|
+
*/
|
|
13
|
+
import { createRequire } from "node:module";
|
|
14
|
+
import { builtinModules } from "node:module";
|
|
15
|
+
let compiler;
|
|
16
|
+
const typescript = () => compiler ??= createRequire(import.meta.url)("typescript-parser");
|
|
17
|
+
const SOURCE = /\.(?:[cm]?[jt]sx?)$/;
|
|
18
|
+
const TEST = /\.test\.[cm]?[jt]sx?$/;
|
|
19
|
+
const VITEST_CONFIG = /(^|\/)vitest\.config\.[cm]?[jt]s$/;
|
|
20
|
+
const EXTENSIONS = ["ts", "tsx", "mts", "cts", "js", "mjs", "cjs", "jsx"];
|
|
21
|
+
/** The product's entry points: the CLI binary, the processes it starts by file name, and the browser bundles. */
|
|
22
|
+
export const PRODUCT_ENTRIES = Object.freeze([
|
|
23
|
+
"src/bin.ts", "src/cli.ts", "src/controller-service.ts", "src/desktop-host.ts", "src/sandbox-launch.ts", "src/tool-launcher.ts",
|
|
24
|
+
"src/browser/app.tsx", "src/browser/terminal-app.tsx",
|
|
25
|
+
]);
|
|
26
|
+
const join = (dir, spec) => {
|
|
27
|
+
const parts = dir === "" ? [] : dir.split("/");
|
|
28
|
+
for (const part of spec.split("/")) {
|
|
29
|
+
if (part === "." || part === "")
|
|
30
|
+
continue;
|
|
31
|
+
if (part === "..") {
|
|
32
|
+
if (parts.length === 0)
|
|
33
|
+
return null;
|
|
34
|
+
parts.pop();
|
|
35
|
+
}
|
|
36
|
+
else
|
|
37
|
+
parts.push(part);
|
|
38
|
+
}
|
|
39
|
+
return parts.join("/");
|
|
40
|
+
};
|
|
41
|
+
const dirOf = (path) => path.split("/").slice(0, -1).join("/");
|
|
42
|
+
/** A repository path, tried as the build tries it (with or without its extension, or as a folder's index). */
|
|
43
|
+
function fileAt(target, files) {
|
|
44
|
+
const stem = target.replace(/\.(?:[cm]?js|jsx)$/, "");
|
|
45
|
+
const tries = [target, ...EXTENSIONS.map(ext => `${stem}.${ext}`), ...EXTENSIONS.map(ext => `${target}/index.${ext}`)];
|
|
46
|
+
return tries.find(one => files.has(one)) ?? null;
|
|
47
|
+
}
|
|
48
|
+
/** The project's resolution rules, read from its own tsconfig.json and package.json (as text in the same tree). */
|
|
49
|
+
function resolverFor(files, names, allPaths, aliases) {
|
|
50
|
+
const tsc = typescript();
|
|
51
|
+
let paths = {}, baseUrl = "";
|
|
52
|
+
const config = files.get("tsconfig.json");
|
|
53
|
+
if (config !== undefined) {
|
|
54
|
+
const parsed = tsc.parseConfigFileTextToJson("tsconfig.json", config).config;
|
|
55
|
+
paths = parsed?.compilerOptions?.paths ?? {};
|
|
56
|
+
baseUrl = join("", parsed?.compilerOptions?.baseUrl ?? ".") ?? "";
|
|
57
|
+
}
|
|
58
|
+
let pkg = {};
|
|
59
|
+
try {
|
|
60
|
+
pkg = JSON.parse(files.get("package.json") ?? "{}");
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
pkg = {};
|
|
64
|
+
}
|
|
65
|
+
const packages = new Set([pkg.dependencies, pkg.devDependencies, pkg.peerDependencies, pkg.optionalDependencies].flatMap(one => Object.keys(one ?? {})));
|
|
66
|
+
// The package's own name resolves to itself: never "some installed package".
|
|
67
|
+
const own = typeof pkg.name === "string" ? pkg.name : null;
|
|
68
|
+
const builtins = new Set(builtinModules);
|
|
69
|
+
const pattern = (key, spec) => {
|
|
70
|
+
if (!key.includes("*"))
|
|
71
|
+
return key === spec ? "" : null;
|
|
72
|
+
const [head, tail] = key.split("*");
|
|
73
|
+
return spec.startsWith(head) && spec.endsWith(tail) && spec.length >= head.length + tail.length ? spec.slice(head.length, spec.length - tail.length) : null;
|
|
74
|
+
};
|
|
75
|
+
return (from, spec) => {
|
|
76
|
+
// vitest's own aliases come first, as vitest applies them.
|
|
77
|
+
for (const alias of aliases) {
|
|
78
|
+
if (spec !== alias.find && !spec.startsWith(`${alias.find}/`))
|
|
79
|
+
continue;
|
|
80
|
+
const target = join(alias.replacement, spec.slice(alias.find.length).replace(/^\//, ""));
|
|
81
|
+
const file = target === null ? null : fileAt(target, names);
|
|
82
|
+
return file !== null ? { file } : { unresolved: target };
|
|
83
|
+
}
|
|
84
|
+
if (spec.startsWith("./") || spec.startsWith("../")) {
|
|
85
|
+
const target = join(dirOf(from), spec);
|
|
86
|
+
const file = target === null ? null : fileAt(target, names);
|
|
87
|
+
if (file !== null)
|
|
88
|
+
return { file };
|
|
89
|
+
// A file that isn't source (a stylesheet, JSON, an asset) is a leaf: it imports nothing.
|
|
90
|
+
if (target !== null && allPaths.has(target))
|
|
91
|
+
return { external: true };
|
|
92
|
+
return { unresolved: target };
|
|
93
|
+
}
|
|
94
|
+
for (const [key, targets] of Object.entries(paths)) {
|
|
95
|
+
const star = pattern(key, spec);
|
|
96
|
+
if (star === null)
|
|
97
|
+
continue;
|
|
98
|
+
for (const one of targets) {
|
|
99
|
+
const target = join(baseUrl, one.replace("*", star));
|
|
100
|
+
const file = target === null ? null : fileAt(target, names);
|
|
101
|
+
if (file !== null)
|
|
102
|
+
return { file };
|
|
103
|
+
}
|
|
104
|
+
return { unresolved: join(baseUrl, (targets[0] ?? "").replace("*", star)) };
|
|
105
|
+
}
|
|
106
|
+
if (spec.startsWith("#")) {
|
|
107
|
+
for (const [key, value] of Object.entries(pkg.imports ?? {})) {
|
|
108
|
+
const star = pattern(key, spec);
|
|
109
|
+
if (star === null)
|
|
110
|
+
continue;
|
|
111
|
+
const target = typeof value === "string" ? value : typeof value?.default === "string" ? value.default : null;
|
|
112
|
+
if (target === null)
|
|
113
|
+
return { unresolved: null };
|
|
114
|
+
const path = join("", target.replace("*", star));
|
|
115
|
+
const file = path === null ? null : fileAt(path, names) ?? fileAt(path.replace(/^dist\//, "src/"), names);
|
|
116
|
+
return file !== null ? { file } : { unresolved: path };
|
|
117
|
+
}
|
|
118
|
+
return { unresolved: null };
|
|
119
|
+
}
|
|
120
|
+
const bare = spec.startsWith("node:") ? spec.slice(5) : spec;
|
|
121
|
+
const name = bare.startsWith("@") ? bare.split("/").slice(0, 2).join("/") : bare.split("/")[0];
|
|
122
|
+
if (name === own)
|
|
123
|
+
return { unresolved: null };
|
|
124
|
+
if (spec.startsWith("node:") || builtins.has(name) || packages.has(name))
|
|
125
|
+
return { external: true };
|
|
126
|
+
// A package the manifest doesn't declare: what it names can't be known here.
|
|
127
|
+
return { unresolved: null };
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
/** The module-loading helpers vitest offers: each takes a specifier first. */
|
|
131
|
+
const VI_LOADERS = new Set(["mock", "doMock", "unmock", "doUnmock", "importActual", "importMock"]);
|
|
132
|
+
/**
|
|
133
|
+
* Every specifier a file names: string ones (preProcessFile: static, dynamic, `require`; comments don't count); every
|
|
134
|
+
* call that loads a module from its first argument — `require`, `import()`, `createRequire(…)(…)` and any name bound to
|
|
135
|
+
* `createRequire(…)`, `vi.*`/`vitest.*` mock and import helpers — whose argument isn't a plain string is computed (`null`,
|
|
136
|
+
* or the template's literal head); and `soft`: every other relative string literal, an edge when it names a repo file.
|
|
137
|
+
*/
|
|
138
|
+
function specifiersOf(path, text) {
|
|
139
|
+
const tsc = typescript();
|
|
140
|
+
const literal = tsc.preProcessFile(text, true, true).importedFiles.map(one => one.fileName);
|
|
141
|
+
const computed = [], soft = [];
|
|
142
|
+
const source = tsc.createSourceFile(path, text, tsc.ScriptTarget.Latest, false, /x$/.test(path) ? tsc.ScriptKind.TSX : tsc.ScriptKind.TS);
|
|
143
|
+
// Names bound to createRequire(…) anywhere in the file (a loader by another name).
|
|
144
|
+
const requires = new Set(["require"]);
|
|
145
|
+
const isCreateRequire = (node) => tsc.isCallExpression(node) && ((tsc.isIdentifier(node.expression) && node.expression.text === "createRequire")
|
|
146
|
+
|| (tsc.isPropertyAccessExpression(node.expression) && node.expression.name.text === "createRequire"));
|
|
147
|
+
const bind = (node) => {
|
|
148
|
+
if (tsc.isVariableDeclaration(node) && tsc.isIdentifier(node.name) && node.initializer !== undefined && isCreateRequire(node.initializer))
|
|
149
|
+
requires.add(node.name.text);
|
|
150
|
+
if (tsc.isBinaryExpression(node) && node.operatorToken.kind === tsc.SyntaxKind.EqualsToken && tsc.isIdentifier(node.left) && isCreateRequire(node.right))
|
|
151
|
+
requires.add(node.left.text);
|
|
152
|
+
tsc.forEachChild(node, bind);
|
|
153
|
+
};
|
|
154
|
+
bind(source);
|
|
155
|
+
const loader = (callee) => callee.kind === tsc.SyntaxKind.ImportKeyword
|
|
156
|
+
|| (tsc.isIdentifier(callee) && requires.has(callee.text))
|
|
157
|
+
|| isCreateRequire(callee)
|
|
158
|
+
|| (tsc.isPropertyAccessExpression(callee) && tsc.isIdentifier(callee.expression) && (callee.expression.text === "vi" || callee.expression.text === "vitest") && VI_LOADERS.has(callee.name.text))
|
|
159
|
+
|| (tsc.isPropertyAccessExpression(callee) && callee.name.text === "require");
|
|
160
|
+
const visit = (node) => {
|
|
161
|
+
if (tsc.isCallExpression(node)) {
|
|
162
|
+
const first = node.arguments[0];
|
|
163
|
+
if (loader(node.expression) && first !== undefined) {
|
|
164
|
+
if (tsc.isStringLiteral(first) || tsc.isNoSubstitutionTemplateLiteral(first))
|
|
165
|
+
literal.push(first.text);
|
|
166
|
+
else
|
|
167
|
+
computed.push(tsc.isTemplateExpression(first) && (first.head.text.startsWith("./") || first.head.text.startsWith("../")) ? first.head.text : null);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if ((tsc.isStringLiteral(node) || tsc.isNoSubstitutionTemplateLiteral(node)) && (node.text.startsWith("./") || node.text.startsWith("../")))
|
|
171
|
+
soft.push(node.text);
|
|
172
|
+
tsc.forEachChild(node, visit);
|
|
173
|
+
};
|
|
174
|
+
visit(source);
|
|
175
|
+
return { literal, computed, soft };
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* The vitest configs' `resolve.alias` and `test.alias` (string keys to string paths, as an object or a list of
|
|
179
|
+
* `{ find, replacement }`), and whether any alias couldn't be read statically.
|
|
180
|
+
*/
|
|
181
|
+
function vitestAliases(files) {
|
|
182
|
+
const tsc = typescript(), aliases = [], unreadable = [];
|
|
183
|
+
for (const [path, text] of files) {
|
|
184
|
+
if (!VITEST_CONFIG.test(path))
|
|
185
|
+
continue;
|
|
186
|
+
const source = tsc.createSourceFile(path, text, tsc.ScriptTarget.Latest, false, tsc.ScriptKind.TS);
|
|
187
|
+
const text_ = (node) => node !== undefined && (tsc.isStringLiteral(node) || tsc.isNoSubstitutionTemplateLiteral(node)) ? node.text : null;
|
|
188
|
+
const visit = (node) => {
|
|
189
|
+
if (tsc.isPropertyAssignment(node) && (tsc.isIdentifier(node.name) || tsc.isStringLiteral(node.name)) && node.name.text === "alias") {
|
|
190
|
+
const value = node.initializer;
|
|
191
|
+
const add = (find, replacement) => {
|
|
192
|
+
if (find === null || replacement === null || !(replacement.startsWith("./") || replacement.startsWith("../"))) {
|
|
193
|
+
unreadable.push(path);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
const target = join(dirOf(path), replacement);
|
|
197
|
+
if (target === null)
|
|
198
|
+
unreadable.push(path);
|
|
199
|
+
else
|
|
200
|
+
aliases.push({ find, replacement: target });
|
|
201
|
+
};
|
|
202
|
+
if (tsc.isObjectLiteralExpression(value)) {
|
|
203
|
+
for (const property of value.properties) {
|
|
204
|
+
if (!tsc.isPropertyAssignment(property)) {
|
|
205
|
+
unreadable.push(path);
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
add(tsc.isIdentifier(property.name) || tsc.isStringLiteral(property.name) ? property.name.text : null, text_(property.initializer));
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
else if (tsc.isArrayLiteralExpression(value)) {
|
|
212
|
+
for (const element of value.elements) {
|
|
213
|
+
if (!tsc.isObjectLiteralExpression(element)) {
|
|
214
|
+
unreadable.push(path);
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
const field = (name) => element.properties.find((one) => tsc.isPropertyAssignment(one) && (tsc.isIdentifier(one.name) || tsc.isStringLiteral(one.name)) && one.name.text === name)?.initializer;
|
|
218
|
+
add(text_(field("find")), text_(field("replacement")));
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
else
|
|
222
|
+
unreadable.push(path);
|
|
223
|
+
}
|
|
224
|
+
tsc.forEachChild(node, visit);
|
|
225
|
+
};
|
|
226
|
+
visit(source);
|
|
227
|
+
}
|
|
228
|
+
return { aliases, unreadable };
|
|
229
|
+
}
|
|
230
|
+
/** The vitest configs' setupFiles and globalSetup entries (string literals), as repository paths. */
|
|
231
|
+
function setupRoots(files, names) {
|
|
232
|
+
const tsc = typescript(), roots = [], unresolved = [];
|
|
233
|
+
for (const [path, text] of files) {
|
|
234
|
+
if (!VITEST_CONFIG.test(path))
|
|
235
|
+
continue;
|
|
236
|
+
const source = tsc.createSourceFile(path, text, tsc.ScriptTarget.Latest, false, tsc.ScriptKind.TS);
|
|
237
|
+
const visit = (node) => {
|
|
238
|
+
if (tsc.isPropertyAssignment(node) && (tsc.isIdentifier(node.name) || tsc.isStringLiteral(node.name)) && (node.name.text === "setupFiles" || node.name.text === "globalSetup")) {
|
|
239
|
+
const values = tsc.isArrayLiteralExpression(node.initializer) ? [...node.initializer.elements] : [node.initializer];
|
|
240
|
+
for (const value of values) {
|
|
241
|
+
if (!tsc.isStringLiteral(value)) {
|
|
242
|
+
unresolved.push({ from: path, specifier: null, prefix: null });
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
// vitest resolves them from its root: the config's own folder, or the repository root a config names.
|
|
246
|
+
const found = [dirOf(path), ""].map(dir => { const target = join(dir, value.text); return target === null ? null : fileAt(target, names); }).find(one => one !== null) ?? null;
|
|
247
|
+
if (found !== null)
|
|
248
|
+
roots.push(found);
|
|
249
|
+
else
|
|
250
|
+
unresolved.push({ from: path, specifier: value.text, prefix: join("", value.text) });
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
tsc.forEachChild(node, visit);
|
|
254
|
+
};
|
|
255
|
+
visit(source);
|
|
256
|
+
}
|
|
257
|
+
return { roots, unresolved };
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* The infrastructure among `files` (path → text: the tree's sources, and its tsconfig.json and package.json): reachable
|
|
261
|
+
* from a test root — every test file, every file under `test/`, every vitest config and its setup files — not itself a
|
|
262
|
+
* test, and not reachable from PRODUCT_ENTRIES; plus every specifier reached from a test root that couldn't be resolved.
|
|
263
|
+
*/
|
|
264
|
+
export function testInfrastructure(files, allPaths = new Set(files.keys())) {
|
|
265
|
+
const names = new Set([...files.keys()].filter(path => SOURCE.test(path)));
|
|
266
|
+
const vitest = vitestAliases(files);
|
|
267
|
+
const resolve = resolverFor(files, names, allPaths, vitest.aliases);
|
|
268
|
+
const edges = new Map(), loose = new Map();
|
|
269
|
+
for (const path of names) {
|
|
270
|
+
const { literal, computed, soft } = specifiersOf(path, files.get(path));
|
|
271
|
+
const targets = new Set(), open = [];
|
|
272
|
+
for (const spec of literal) {
|
|
273
|
+
const found = resolve(path, spec);
|
|
274
|
+
if ("file" in found)
|
|
275
|
+
targets.add(found.file);
|
|
276
|
+
else if ("unresolved" in found)
|
|
277
|
+
open.push({ from: path, specifier: spec, prefix: found.unresolved });
|
|
278
|
+
}
|
|
279
|
+
for (const head of computed)
|
|
280
|
+
open.push({ from: path, specifier: head, prefix: head === null ? null : join(dirOf(path), head) });
|
|
281
|
+
// Any other relative string that names a repository source file is an edge (a path handed to a loader by any name).
|
|
282
|
+
for (const spec of soft) {
|
|
283
|
+
const found = resolve(path, spec);
|
|
284
|
+
if ("file" in found)
|
|
285
|
+
targets.add(found.file);
|
|
286
|
+
}
|
|
287
|
+
edges.set(path, [...targets]);
|
|
288
|
+
loose.set(path, open);
|
|
289
|
+
}
|
|
290
|
+
const setup = setupRoots(files, names);
|
|
291
|
+
const testRoots = [...[...names].filter(path => TEST.test(path) || path.startsWith("test/") || VITEST_CONFIG.test(path)), ...setup.roots];
|
|
292
|
+
const reach = (roots) => {
|
|
293
|
+
const seen = new Set(), todo = [...roots];
|
|
294
|
+
while (todo.length > 0) {
|
|
295
|
+
const at = todo.pop();
|
|
296
|
+
if (seen.has(at))
|
|
297
|
+
continue;
|
|
298
|
+
seen.add(at);
|
|
299
|
+
for (const next of edges.get(at) ?? [])
|
|
300
|
+
if (!seen.has(next))
|
|
301
|
+
todo.push(next);
|
|
302
|
+
}
|
|
303
|
+
return seen;
|
|
304
|
+
};
|
|
305
|
+
const product = reach(PRODUCT_ENTRIES.filter(path => names.has(path)));
|
|
306
|
+
const tested = reach(testRoots);
|
|
307
|
+
const infra = new Set([...tested].filter(path => !TEST.test(path) && !product.has(path)));
|
|
308
|
+
const unresolved = [...setup.unresolved, ...vitest.unreadable.map(from => ({ from, specifier: null, prefix: null })), ...[...tested].flatMap(path => loose.get(path) ?? [])];
|
|
309
|
+
return { infra, unresolved, product };
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Whether a changed path is something an unresolved specifier might name. A computed one (no prefix) might name anything
|
|
313
|
+
* the other rules don't already trust: any source file that is neither a test nor product code.
|
|
314
|
+
*/
|
|
315
|
+
export function mightName(open, path, found) {
|
|
316
|
+
if (open.prefix === null)
|
|
317
|
+
return SOURCE.test(path) && !TEST.test(path) && !found.product.has(path);
|
|
318
|
+
const prefix = open.prefix.replace(/\/$/, "");
|
|
319
|
+
const stem = path.replace(/\.[^/.]+$/, "");
|
|
320
|
+
return path === prefix || stem === prefix.replace(/\.(?:[cm]?js|jsx)$/, "") || path.startsWith(`${prefix}/`) || (open.specifier !== null && open.specifier.endsWith("/") === false && path.startsWith(prefix));
|
|
321
|
+
}
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
* database records which key Toolroll made: a folder, key, link or lock that
|
|
28
28
|
* something else put there first is refused, never used.
|
|
29
29
|
*/
|
|
30
|
+
import { inReadOnlyRequest } from "./read-only-request.js";
|
|
30
31
|
import { spawnSync } from "node:child_process";
|
|
31
32
|
import { createCipheriv, createDecipheriv, createHash, randomBytes } from "node:crypto";
|
|
32
33
|
import { closeSync, constants, fstatSync, fsyncSync, linkSync, lstatSync, mkdirSync, openSync, readSync, readdirSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, writeSync } from "node:fs";
|
|
@@ -541,6 +542,10 @@ export function secretFolder(state) {
|
|
|
541
542
|
const known = folders.get(root);
|
|
542
543
|
if (known !== undefined && unchanged(known))
|
|
543
544
|
return known;
|
|
545
|
+
// A page visit prepares nothing (docs/plans/get-side-effects.md): the console prepares its folder as it starts, and
|
|
546
|
+
// an action checks a changed one again. Until then the page says secrets can't be read right now.
|
|
547
|
+
if (inReadOnlyRequest())
|
|
548
|
+
throw new SecretStoreError(NOT_READY);
|
|
544
549
|
// Changed since it was checked (or never seen): checked against the same record (the database's, for Toolroll's
|
|
545
550
|
// own folder; this process's, for a test's), and refused if it doesn't match.
|
|
546
551
|
const named = pending.get(root);
|
|
@@ -797,6 +802,9 @@ export function readHeldSecret(state, name, file) {
|
|
|
797
802
|
const backend = secretBackend();
|
|
798
803
|
if (backend === null)
|
|
799
804
|
return readOwnFile(path);
|
|
805
|
+
// A page visit moves nothing and takes no lock (docs/plans/get-side-effects.md): the keychain, else the old file.
|
|
806
|
+
if (inReadOnlyRequest())
|
|
807
|
+
return readSealed(backend, folder, heldItem(folder, name)) ?? readOwnFile(path);
|
|
800
808
|
return withSecretLock(folder, `held.${name}`, () => {
|
|
801
809
|
moveHeldIn(backend, folder, name, path);
|
|
802
810
|
return readSealed(backend, folder, heldItem(folder, name));
|
|
@@ -65,9 +65,10 @@ export function updatesHtml(view, notice) {
|
|
|
65
65
|
: view.method.kind === "source" ? html `<h2>Toolroll ${view.current}</h2><p class="meta">This runs from a source checkout. Update it with git.</p>`
|
|
66
66
|
: newer && view.method.kind === "desktop" ? html `<h2>Toolroll ${latest} is available</h2><p class="meta">You have ${view.current}. Update it from the Toolroll app.</p>`
|
|
67
67
|
: newer ? html `<h2>Toolroll ${latest} is available</h2><p class="meta">You have ${view.current}. Running work finishes first, and your current version is kept so you can go back.</p>`
|
|
68
|
-
: "off" in view.latest ? html `<h2>Toolroll ${view.current}</h2><p class="meta">Update checks are off.</p
|
|
69
|
-
:
|
|
70
|
-
: html `<h2>Toolroll ${view.current}
|
|
68
|
+
: "off" in view.latest ? html `<h2>Toolroll ${view.current}</h2><p class="meta">Update checks are off.</p>${postForm("/settings/updates/check", html `<button type="submit">Check now</button>`, { attrs: { class: "update-actions" } })}`
|
|
69
|
+
: "pending" in view.latest ? html `<h2>Toolroll ${view.current}</h2><p class="meta">Not checked for a newer release yet.</p>${postForm("/settings/updates/check", html `<button type="submit">Check now</button>`, { attrs: { class: "update-actions" } })}`
|
|
70
|
+
: latest === null ? html `<h2>Toolroll ${view.current}</h2><p class="meta">Couldn’t check for a newer release: ${"problem" in view.latest ? view.latest.problem : ""}</p>`
|
|
71
|
+
: html `<h2>Toolroll ${view.current} is up to date</h2>`;
|
|
71
72
|
const form = newer && !["npx", "source", "desktop"].includes(view.method.kind)
|
|
72
73
|
? postForm("/settings/updates", html `${reauth(html `<label class="step-up">Your Toolroll password<input type="password" name="password" autocomplete="current-password" required></label>`)}<div class="update-actions"><button type="submit" name="when" value="now" class="primary">Update now</button><button type="submit" name="when" value="when-idle">When idle</button><button type="submit" name="when" value="tonight">Tonight (03:00)</button></div>`, { hidden: { version: latest } })
|
|
73
74
|
: "";
|
package/dist/toolroll-update.js
CHANGED
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
* previous runtime and its backups. Every update, rollback, refusal and
|
|
23
23
|
* failure is a ledger entry.
|
|
24
24
|
*/
|
|
25
|
+
import { inReadOnlyRequest } from "./read-only-request.js";
|
|
25
26
|
import { createHash, randomUUID, verify as signatureValid, X509Certificate } from "node:crypto";
|
|
26
27
|
import { spawnSync } from "node:child_process";
|
|
27
28
|
import { accessSync, chmodSync, closeSync, constants, copyFileSync, existsSync, fsyncSync, lstatSync, mkdirSync, openSync, readdirSync, readFileSync, readlinkSync, realpathSync, renameSync, rmSync, statfsSync, statSync, symlinkSync, writeFileSync } from "node:fs";
|
|
@@ -29,7 +30,7 @@ import { homedir, userInfo } from "node:os";
|
|
|
29
30
|
import { basename, dirname, isAbsolute, join, resolve, sep } from "node:path";
|
|
30
31
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
31
32
|
import { createRequire } from "node:module";
|
|
32
|
-
import { clearWords, durableJson, LINGERING_LIMIT_MS, lingeringRun, lingeringWords, settleLeftoverRecords, sqliteLock, desktopUpdateWaitingOf, stoppedWaitingWords, updateWaitingWords, verifiedDatabaseBackup } from "./desktop-update.js";
|
|
33
|
+
import { clearWords, durableJson, LINGERING_LIMIT_MS, lingeringRun, lingeringWords, settleLeftoverRecords, sqliteLock, lockHeld, desktopUpdateWaitingOf, stoppedWaitingWords, updateWaitingWords, verifiedDatabaseBackup } from "./desktop-update.js";
|
|
33
34
|
import { activeUpdateWork, freezeUpdateGate, installUpdateGate, removeUpdateGate, updateAdmissionPaused, updateGateOwned } from "./desktop-update-gate.js";
|
|
34
35
|
import { processMayBeAlive } from "./process-liveness.js";
|
|
35
36
|
import { removeTerminalUpdateGate } from "./terminal-catalog.js";
|
|
@@ -1445,11 +1446,8 @@ export function runtimeUpdateStatus(stateDir) {
|
|
|
1445
1446
|
j = null;
|
|
1446
1447
|
}
|
|
1447
1448
|
let running = false;
|
|
1448
|
-
if (j && !runtimeUpdateTerminal(j.phase))
|
|
1449
|
-
|
|
1450
|
-
running = lock === null;
|
|
1451
|
-
lock?.close();
|
|
1452
|
-
}
|
|
1449
|
+
if (j && !runtimeUpdateTerminal(j.phase))
|
|
1450
|
+
running = lockHeld(join(j.stageDir, "worker.sqlite"));
|
|
1453
1451
|
const whatsNew = j && j.kind === "update" && j.phase === "complete" && !j.seen ? { version: j.to.version, notes: j.notes ?? [] } : null;
|
|
1454
1452
|
let last = null;
|
|
1455
1453
|
try {
|
|
@@ -1525,10 +1523,13 @@ export function releaseStalledUpdate(stateDir, now) {
|
|
|
1525
1523
|
* waiting update, `toolroll update`'s or the desktop app's. */
|
|
1526
1524
|
export function waitingUpdate(databaseFile, inTheWay, now, desktopStates) {
|
|
1527
1525
|
const stateDir = dirname(databaseFile);
|
|
1528
|
-
|
|
1529
|
-
|
|
1526
|
+
// A page visit only reads (docs/plans/get-side-effects.md): the console's background pass releases a stalled update.
|
|
1527
|
+
if (!inReadOnlyRequest()) {
|
|
1528
|
+
try {
|
|
1529
|
+
releaseStalledUpdate(stateDir, now);
|
|
1530
|
+
}
|
|
1531
|
+
catch { /* an unreadable record is shown as it is */ }
|
|
1530
1532
|
}
|
|
1531
|
-
catch { /* an unreadable record is shown as it is */ }
|
|
1532
1533
|
return updateWaitingOf(stateDir, inTheWay) ?? desktopUpdateWaitingOf(databaseFile, inTheWay, desktopStates);
|
|
1533
1534
|
}
|
|
1534
1535
|
export function markWhatsNewSeen(stateDir) {
|
package/dist/work-index.js
CHANGED
|
@@ -10,7 +10,7 @@ import { readProjectAccess } from './project-access.js';
|
|
|
10
10
|
import { pauseOnTaskProviders, providerName, signInReason } from './provider-auth.js';
|
|
11
11
|
import { scopeTermsProblem, stopFactOf } from './store.js';
|
|
12
12
|
import { assignmentChecksForRun } from './assignment.js';
|
|
13
|
-
import { checkBackingOf, completionBlockersOf, leadOnIt, plainReasonOf, replacedWords, requirementsOf, stageOfCode, taskStatusOf, unverifiedWhenRefuted, workToneOf } from './task-status.js';
|
|
13
|
+
import { approvedCheckOf, checkBackingOf, completionBlockersOf, leadOnIt, plainReasonOf, replacedWords, requirementsOf, stageOfCode, taskStatusOf, unverifiedWhenRefuted, workToneOf } from './task-status.js';
|
|
14
14
|
import { releaseCoverageOf } from './release-coverage.js';
|
|
15
15
|
import { buildReviewOf } from './review-switch.js';
|
|
16
16
|
import { shownVerdictOf } from './proof.js';
|
|
@@ -429,7 +429,7 @@ function readWorkIndexPage(store, now, access, options, taskId = null) {
|
|
|
429
429
|
// automatic review and, with checks Off, the release check that covers it.
|
|
430
430
|
const memo = new Map();
|
|
431
431
|
const readiness = (row, checks) => resultReadinessFacts(store, now, options.root, row, checks, memo);
|
|
432
|
-
return { items: page.map(row => itemOf(row, access.principal, probe, leadName, readiness)), projects, totals: rows[0] === undefined ? { ...EMPTY } : counts(rows[0]),
|
|
432
|
+
return { items: page.map(row => itemOf(row, access.principal, probe, leadName, readiness, options.root !== undefined)), projects, totals: rows[0] === undefined ? { ...EMPTY } : counts(rows[0]),
|
|
433
433
|
groups: rows[0] === undefined ? { ...NO_GROUPS } : groupCounts(rows[0]), nextCursor, limit, view };
|
|
434
434
|
}
|
|
435
435
|
export function workCountsByProject(store, now, access) {
|
|
@@ -489,22 +489,36 @@ function listChecksOf(own, level, follows) {
|
|
|
489
489
|
return null;
|
|
490
490
|
return { status: read.status, exitCode: null, head: null, level: read.level, running: read.running };
|
|
491
491
|
}
|
|
492
|
+
/** A check as a read without the evidence files may show it: only what never claims a pass it can't prove. */
|
|
493
|
+
function unreadChecksOf(checks) {
|
|
494
|
+
if (checks === null)
|
|
495
|
+
return null;
|
|
496
|
+
return checks.status === 'failed' || (checks.level === 'off' && checks.status !== 'passed' && checks.release == null) ? checks : null;
|
|
497
|
+
}
|
|
492
498
|
/** A ready result's requirements, acceptance and completion blockers from its saved records, as assignmentOf reads them. */
|
|
493
499
|
function resultReadinessFacts(store, now, root, row, recorded, memo) {
|
|
494
500
|
const run = n(row, 'result_id'), repo = s(row, 'repo');
|
|
495
501
|
if (!run)
|
|
496
|
-
return { checks: recorded };
|
|
502
|
+
return { checks: root === undefined ? unreadChecksOf(recorded) : recorded };
|
|
497
503
|
const report = s(row, 'result_role') === 'scout';
|
|
498
|
-
//
|
|
499
|
-
|
|
504
|
+
// Read its saved receipt, as the page does: a recorded result (or a verdict alone) is not a check that can still be
|
|
505
|
+
// read, and the row must never say passed while the task page says the check couldn't be read.
|
|
506
|
+
const read = root !== undefined && !report ? assignmentChecksForRun(store, root, run, now) : null;
|
|
507
|
+
// A recorded failure is never lost because its saved files can't be read now (fail closed: negatives stand).
|
|
508
|
+
const saved = read !== null && recorded?.status === 'failed' && (read.status === 'unavailable' || read.status === 'not-run')
|
|
509
|
+
? { ...read, status: 'failed', exitCode: recorded.exitCode } : read;
|
|
500
510
|
const listed = saved === null ? recorded : { status: saved.status, exitCode: saved.exitCode, head: s(row, 'result_head'),
|
|
501
511
|
...(saved.level == null ? {} : { level: saved.level }), ...(saved.running == null ? {} : { running: saved.running }), ...(saved.batch == null ? {} : { batch: saved.batch }) };
|
|
502
|
-
const
|
|
512
|
+
const covered = listed?.level === 'off' && listed.status !== 'passed' && listed.status !== 'failed'
|
|
503
513
|
? { ...listed, release: releaseCoverageOf(store, root, repo, s(row, 'result_head'), { memo }) } : listed;
|
|
504
514
|
const proof = store.proofVerdictFor(run), accepted = store.proofAcceptance(run) !== null;
|
|
515
|
+
// The approved check as the task page reads it (task-status.ts): a recorded pass with no readable check doesn't count.
|
|
516
|
+
// Without the evidence root nothing can be read, so the row makes no claim about the check at all: a recorded
|
|
517
|
+
// failure or checks Off still say so; a pass, or a missing check, is the result's own page to say.
|
|
518
|
+
const checks = report ? covered : root === undefined ? unreadChecksOf(covered) : approvedCheckOf(proof?.verdict ?? null, proof?.reasons ?? [], covered);
|
|
505
519
|
const review = buildReviewOf(store, run);
|
|
506
520
|
const high = review === null || review.sentBackAs !== null || review.state !== 'reviewed' ? 0 : review.high.length;
|
|
507
|
-
const checkRequired = requiredCheckCommandFor(store, repo, String(row['id']), checks?.level) !== null;
|
|
521
|
+
const checkRequired = root === undefined && checks === null ? false : requiredCheckCommandFor(store, repo, String(row['id']), checks?.level) !== null;
|
|
508
522
|
const backing = checkBackingOf(checks);
|
|
509
523
|
const requirements = requirementsOf(proof?.matrix, backing);
|
|
510
524
|
const verdict = shownVerdictOf(proof?.verdict ?? null, { checkFailed: !report && checks?.status === 'failed', highFindings: high, reviewPending: review?.state === 'pending',
|
|
@@ -512,7 +526,7 @@ function resultReadinessFacts(store, now, root, row, recorded, memo) {
|
|
|
512
526
|
const blockers = completionBlockersOf({ report, checks, checkRequired, matrix: proof?.matrix, verdict: proof?.verdict ?? null, accepted, high });
|
|
513
527
|
return { checks, requirements: unverifiedWhenRefuted(requirements, verdict), ...(accepted ? { accepted } : {}), ...(blockers.length === 0 ? {} : { blockers }) };
|
|
514
528
|
}
|
|
515
|
-
function itemOf(row, principal, probe, leadName, readiness) {
|
|
529
|
+
function itemOf(row, principal, probe, leadName, readiness, readable = false) {
|
|
516
530
|
const code = String(row['code']), id = String(row['id']);
|
|
517
531
|
const custody = code === 'process-needs-attention' || (code === 'result-needs-attention' && n(row, 'custody_unresolved') === 1);
|
|
518
532
|
const process = custody && probe !== undefined ? processNeedOf(probe(id)) : null;
|
|
@@ -624,15 +638,18 @@ function itemOf(row, principal, probe, leadName, readiness) {
|
|
|
624
638
|
: coded.stage === 'needs-you' && (coded.need === undefined || coded.need === 'other') && INDEX_NEED[code] !== undefined ? { stage: 'needs-you', need: INDEX_NEED[code] } : coded;
|
|
625
639
|
const checkStatus = s(row, 'check_status'), verdict = s(row, 'proof_verdict');
|
|
626
640
|
const finished = reading.stage === 'finished' || reading.stage === 'complete';
|
|
627
|
-
|
|
628
|
-
const
|
|
629
|
-
const
|
|
641
|
+
// Only a recorded check is a pass; a verdict alone isn't one (task-status.ts approvedCheckOf reads it with the receipt).
|
|
642
|
+
const own = checkStatus === 'failed' || n(row, 'proof_failed_check') === 1 ? 'failed' : checkStatus === 'passed' ? 'passed' : null;
|
|
643
|
+
const recordedChecks = !finished ? null : listChecksOf(own, s(row, 'check_level'), row['check_follows']);
|
|
644
|
+
// A read without the evidence files claims no pass (unreadChecksOf); the readiness read below says the rest.
|
|
645
|
+
const listedChecks = readable ? recordedChecks : unreadChecksOf(recordedChecks);
|
|
646
|
+
const ready = reading.stage === 'finished' && readiness !== undefined ? readiness(row, recordedChecks) : null;
|
|
630
647
|
// Toolroll marked it complete because its work landed: the reason, said once.
|
|
631
648
|
const landed = code === 'complete' ? landedReasonOf(s(row, 'checked_actor'), s(row, 'checked_detail')) : null;
|
|
632
649
|
const shared = taskStatusOf({ stage: reading.stage, ...(reading.need === undefined ? {} : { need: reading.need }), ...(reading.wait === undefined ? {} : { wait: reading.wait }),
|
|
633
650
|
...(process === null ? {} : { needContext: { build: process.build } }),
|
|
634
651
|
reason: finished ? null : plainReasonOf(reading.stage, code, detail || null), report: s(row, 'result_role') === 'scout',
|
|
635
|
-
checks: ready
|
|
652
|
+
checks: ready !== null ? ready.checks : listedChecks,
|
|
636
653
|
...(ready === null ? {} : { requirements: ready.requirements ?? null, ...(ready.accepted ? { accepted: true } : {}), ...(ready.blockers === undefined ? {} : { blockers: ready.blockers }) }),
|
|
637
654
|
completedBy: code !== 'complete' || landed !== null ? null : n(row, 'completed_by_lead') === 1 ? 'the lead' : String(row['checked_actor']).replace(/^(?:operator|coordinator|lead):/, ''),
|
|
638
655
|
...(landed === null ? {} : { landed }),
|
package/dist/workspace-ui.d.ts
CHANGED
|
@@ -22,6 +22,7 @@ export { ACCEPT_NEEDS_REASON, cantAcceptYetOf, evidenceProblemOf, type EvidenceP
|
|
|
22
22
|
import { type ProofVerdict } from "./proof.js";
|
|
23
23
|
import type { ReviewRetryState, TaskState } from "./store.js";
|
|
24
24
|
import type { TaskControlView } from "./task-control.js";
|
|
25
|
+
import { type ChecksFact } from "./task-status.js";
|
|
25
26
|
import { type Html } from "./html.js";
|
|
26
27
|
/** The Work destination's views — shortcuts over the same rows, never a
|
|
27
28
|
* persisted state. All is the default. */
|
|
@@ -53,6 +54,10 @@ export type ResultFacts = {
|
|
|
53
54
|
* per run so an older selected result keeps its own words; undefined
|
|
54
55
|
* when the caller did not read it, null when never requested. */
|
|
55
56
|
review?: ReviewFacts | null;
|
|
57
|
+
/** The saved approved check as read now (assignmentChecksForRun), for `approvedCheckOf`: the words say the check
|
|
58
|
+
* passed only when this reading does. Undefined: the caller read no check (a list or dispatch read), so the words
|
|
59
|
+
* name only what the verdict recorded when the build finished. */
|
|
60
|
+
checks?: ChecksFact | null;
|
|
56
61
|
};
|
|
57
62
|
/** The result's review facts as `reviewRetryStateOf` records them, plus
|
|
58
63
|
* the one liveness fact the words depend on. */
|