toolroll 0.9.88 → 0.9.90
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 +65 -0
- package/README.md +16 -10
- package/THIRD_PARTY_NOTICES.md +37 -0
- package/dist/agent-board.d.ts +9 -0
- package/dist/agent-board.js +20 -0
- package/dist/agent-fence.js +20 -2
- package/dist/approval-authority.d.ts +7 -0
- package/dist/approval-authority.js +13 -2
- package/dist/browser/THIRD_PARTY_NOTICES.txt +692 -28
- package/dist/browser/terminal.js +42 -42
- package/dist/browser/workspace.css +1 -1
- package/dist/browser/workspace.js +30 -30
- package/dist/browser-workspace.d.ts +13 -0
- package/dist/builder.js +5 -2
- package/dist/chat-actions.d.ts +5 -0
- package/dist/chat-actions.js +7 -1
- package/dist/chat-channel.js +11 -1
- package/dist/chat-delivery.js +2 -0
- package/dist/chat-task-actions.d.ts +1 -0
- package/dist/check-access.d.ts +3 -0
- package/dist/check-access.js +4 -1
- package/dist/cli-help.d.ts +1 -1
- package/dist/cli-help.js +6 -2
- package/dist/cli.d.ts +6 -0
- package/dist/cli.js +23 -0
- package/dist/coding-ui.js +4 -2
- package/dist/connect-cli.js +2 -1
- package/dist/console-instance.d.ts +34 -1
- package/dist/console-instance.js +124 -15
- package/dist/contracts/cli.d.ts +72 -0
- package/dist/contracts/cli.js +13 -0
- package/dist/contracts/console-api.d.ts +9 -2
- package/dist/contracts/console-api.js +9 -2
- package/dist/desktop-host.js +8 -3
- package/dist/desktop-update.js +2 -1
- package/dist/dispatch.d.ts +3 -0
- package/dist/dispatch.js +22 -2
- package/dist/easy-start-args.d.ts +13 -0
- package/dist/easy-start-args.js +24 -0
- package/dist/easy-start.d.ts +77 -0
- package/dist/easy-start.js +321 -0
- package/dist/egress.js +5 -0
- package/dist/first-run.d.ts +77 -0
- package/dist/first-run.js +45 -0
- package/dist/flow-starters.js +3 -0
- package/dist/flow-triggers.js +4 -0
- package/dist/folder-gate.d.ts +32 -0
- package/dist/folder-gate.js +43 -0
- package/dist/guides.js +1 -1
- package/dist/invoke.js +11 -6
- package/dist/laptop-background.d.ts +171 -0
- package/dist/laptop-background.js +550 -0
- package/dist/laptop-service.d.ts +111 -0
- package/dist/laptop-service.js +268 -0
- package/dist/lead-cli.js +2 -1
- package/dist/lead-doors.d.ts +6 -0
- package/dist/lead-doors.js +20 -2
- package/dist/live-records.d.ts +1 -1
- package/dist/live-records.js +3 -12
- package/dist/local-owner.d.ts +124 -0
- package/dist/local-owner.js +212 -0
- package/dist/login-secrets.d.ts +60 -0
- package/dist/login-secrets.js +195 -0
- package/dist/mcp-oauth.d.ts +4 -0
- package/dist/mcp-oauth.js +13 -7
- package/dist/model-choices.d.ts +43 -0
- package/dist/model-choices.js +80 -0
- package/dist/operate.d.ts +46 -1
- package/dist/operate.js +461 -40
- package/dist/person-proof.js +3 -0
- package/dist/project-access.js +15 -1
- package/dist/project-admission.d.ts +6 -1
- package/dist/project-admission.js +44 -7
- package/dist/project-identity.d.ts +8 -0
- package/dist/project-identity.js +43 -0
- package/dist/project-knowledge.d.ts +7 -7
- package/dist/project.d.ts +1 -0
- package/dist/project.js +6 -0
- package/dist/proposal.js +11 -1
- package/dist/quick-start.d.ts +3 -1
- package/dist/quick-start.js +6 -1
- package/dist/recipes.js +12 -1
- package/dist/release-cli.js +2 -1
- package/dist/runner-process.d.ts +9 -0
- package/dist/runner-process.js +18 -0
- package/dist/sample-project.d.ts +31 -0
- package/dist/sample-project.js +141 -0
- package/dist/scope.d.ts +24 -2
- package/dist/scope.js +74 -3
- package/dist/serve.js +157 -28
- package/dist/server/chat.js +10 -3
- package/dist/server/chrome.d.ts +10 -2
- package/dist/server/chrome.js +7 -5
- package/dist/server/console.css +10 -0
- package/dist/server/guards.d.ts +3 -0
- package/dist/server/guards.js +22 -2
- package/dist/server/http.d.ts +22 -0
- package/dist/server/http.js +58 -0
- package/dist/server/live.js +4 -14
- package/dist/server/mutation-key.d.ts +70 -0
- package/dist/server/mutation-key.js +142 -0
- package/dist/server/pages.js +12 -9
- package/dist/server/people-tokens.js +240 -20
- package/dist/server/remote.d.ts +2 -0
- package/dist/server/remote.js +19 -4
- package/dist/server/render-new-task.js +5 -1
- package/dist/server/render-pages.d.ts +2 -1
- package/dist/server/render-pages.js +1 -1
- package/dist/server/render-people.d.ts +36 -0
- package/dist/server/render-people.js +65 -1
- package/dist/server/render-projects.js +4 -2
- package/dist/server/render-tasks.d.ts +12 -0
- package/dist/server/render-tasks.js +16 -6
- package/dist/server/request-context.d.ts +6 -0
- package/dist/server/route-table.d.ts +16 -0
- package/dist/server/route-table.js +121 -0
- package/dist/server/runtime.d.ts +6 -4
- package/dist/server/session.d.ts +31 -1
- package/dist/server/session.js +12 -2
- package/dist/server/settings.js +63 -8
- package/dist/server/task-views.js +10 -1
- package/dist/server/tasks.d.ts +10 -0
- package/dist/server/tasks.js +59 -9
- package/dist/server/terminals.js +15 -2
- package/dist/server/welcome.d.ts +22 -16
- package/dist/server/welcome.js +150 -52
- package/dist/settings-groups-ui.d.ts +7 -1
- package/dist/settings-groups-ui.js +23 -2
- package/dist/settings-proofs.js +1 -0
- package/dist/setup-guide.d.ts +2 -2
- package/dist/setup-guide.js +6 -0
- package/dist/store.d.ts +124 -1
- package/dist/store.js +245 -12
- package/dist/surface.d.ts +10 -0
- package/dist/surface.js +40 -1
- package/dist/task-text.d.ts +6 -0
- package/dist/task-text.js +17 -0
- package/dist/teardown-git.js +16 -1
- package/dist/telegram-lead.js +2 -0
- package/dist/terminal-text.d.ts +11 -0
- package/dist/terminal-text.js +26 -0
- package/dist/toolroll-update.d.ts +8 -1
- package/dist/toolroll-update.js +151 -4
- package/dist/tui-contract.d.ts +118 -0
- package/dist/tui-contract.js +9 -0
- package/dist/tui-source.d.ts +14 -0
- package/dist/tui-source.js +182 -0
- package/dist/tui.js +129 -0
- package/dist/up-processes.d.ts +13 -1
- package/dist/up-processes.js +13 -3
- package/dist/welcome-facts.d.ts +18 -0
- package/dist/welcome-facts.js +79 -0
- package/dist/youiyoux-intake.js +11 -1
- package/package.json +4 -2
package/dist/person-proof.js
CHANGED
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
const METHOD_IS_PERSON = {
|
|
4
4
|
"password-now": true, "sso-now": true, "fresh-password": true, "fresh-sso": true, "session": true, "cli-login": true,
|
|
5
5
|
"remote-token": false, "lead-token": false,
|
|
6
|
+
// Easy-start C1: the laptop's own owner in an allowlisted act — a person, standing only while local mode stays on at
|
|
7
|
+
// the generation it was given under (approval-authority.ts localSealStandsSql).
|
|
8
|
+
"local-owner": true,
|
|
6
9
|
};
|
|
7
10
|
/** Proofs that aren't credential methods: the paired-chat tap, and the phone plan's passkey ceremony. */
|
|
8
11
|
const OTHER_PERSON_PROOFS = new Set(["chat", "passkey"]);
|
package/dist/project-access.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
2
|
import { isAbsolute, resolve } from "node:path";
|
|
3
3
|
import { canonicalProject } from "./project.js";
|
|
4
|
+
import { FolderUnavailable } from "./folder-gate.js";
|
|
4
5
|
export function normalizeProjectAccess(value) {
|
|
5
6
|
if (value === null)
|
|
6
7
|
return null;
|
|
@@ -26,7 +27,20 @@ export function readProjectAccess(value) {
|
|
|
26
27
|
}
|
|
27
28
|
}
|
|
28
29
|
export function projectAccessAllows(access, repo) {
|
|
29
|
-
|
|
30
|
+
if (access === null)
|
|
31
|
+
return true;
|
|
32
|
+
if (repo === null)
|
|
33
|
+
return false;
|
|
34
|
+
// A folder the console's gate holds back (folder-gate.ts) is unavailable: access to it is denied, never decided by
|
|
35
|
+
// its spelling.
|
|
36
|
+
try {
|
|
37
|
+
return access.includes(canonicalProject(repo) ?? resolve(repo));
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
if (error instanceof FolderUnavailable)
|
|
41
|
+
return false;
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
30
44
|
}
|
|
31
45
|
/** SQL: projectAccessAllows for an `approver` row (`alias`) and a project path expression, for the dispatch belts. A
|
|
32
46
|
* null list reaches every project; an unreadable one reaches none. */
|
|
@@ -42,9 +42,11 @@ export type Released = {
|
|
|
42
42
|
removed: boolean;
|
|
43
43
|
};
|
|
44
44
|
export declare const PROJECT_ADDED = "project added";
|
|
45
|
+
/** A folder's real path (a folder), null (not one), or "no-answer" (it didn't answer in time). */
|
|
46
|
+
export type FolderResolver = (path: string) => Promise<string | null | "no-answer">;
|
|
45
47
|
export declare const PROJECT_REMOVED = "project removed";
|
|
46
48
|
/** The checkout root a folder belongs to, canonical; never a shell. */
|
|
47
|
-
export declare function proveProjectRoot(path: string, exec?: ProjectExec): Promise<{
|
|
49
|
+
export declare function proveProjectRoot(path: string, exec?: ProjectExec, resolveFolder?: FolderResolver): Promise<{
|
|
48
50
|
ok: true;
|
|
49
51
|
repo: string;
|
|
50
52
|
} | AdmissionFailure>;
|
|
@@ -63,6 +65,9 @@ export declare function admitProject(store: Store, input: {
|
|
|
63
65
|
/** The caller's authority, asked again about the proved repo inside the locked registry update and inside the
|
|
64
66
|
* database transaction (the console: nothing it proved before an await moved, and the repo is still visible). */
|
|
65
67
|
stillAllowed?: (repo: string) => boolean;
|
|
68
|
+
/** How the folder and its checkout root are resolved (the console: off its thread, under a deadline). Absent:
|
|
69
|
+
* canonicalProject. */
|
|
70
|
+
resolveFolder?: FolderResolver;
|
|
66
71
|
}): Promise<Admitted | AdmissionFailure>;
|
|
67
72
|
/**
|
|
68
73
|
* Remove one project from the lists and the builder. Its tasks, results and
|
|
@@ -17,6 +17,7 @@ import { run } from "./exec.js";
|
|
|
17
17
|
import { canonicalProject, projectName, sameRepo } from "./project.js";
|
|
18
18
|
import { remoteUrlProblem } from "./remote-main.js";
|
|
19
19
|
import { addRepos, loadProjectRegistry, removeRepos, updateRepos } from "./repos.js";
|
|
20
|
+
import { FolderUnavailable } from "./folder-gate.js";
|
|
20
21
|
const ADMISSION_GIT = {
|
|
21
22
|
envAllowlist: ["PATH", "HOME", "TMPDIR", "LANG", "LC_ALL"],
|
|
22
23
|
env: { GIT_CONFIG_NOSYSTEM: "1", GIT_CONFIG_GLOBAL: "/dev/null", GIT_NO_REPLACE_OBJECTS: "1" },
|
|
@@ -24,15 +25,23 @@ const ADMISSION_GIT = {
|
|
|
24
25
|
export const PROJECT_ADDED = "project added";
|
|
25
26
|
export const PROJECT_REMOVED = "project removed";
|
|
26
27
|
/** The checkout root a folder belongs to, canonical; never a shell. */
|
|
27
|
-
export async function proveProjectRoot(path, exec = run) {
|
|
28
|
+
export async function proveProjectRoot(path, exec = run, resolveFolder) {
|
|
28
29
|
if (path.trim() === "" || path.length > 4096 || /[\x00-\x1f]/.test(path)) {
|
|
29
30
|
return { ok: false, reason: "unavailable", message: "Choose a valid project folder." };
|
|
30
31
|
}
|
|
31
|
-
|
|
32
|
+
// The console resolves both folders off its thread (project-identity.ts resolveNow): its thread never waits on one.
|
|
33
|
+
const canon = async (one) => resolveFolder === undefined ? canonicalProject(one) : resolveFolder(one);
|
|
34
|
+
const didntAnswer = (one) => ({ ok: false, reason: "unavailable", message: `${one} didn't answer. It may be on a drive that's disconnected, or waiting on macOS; try again in a moment.` });
|
|
35
|
+
const canonical = await canon(path);
|
|
36
|
+
if (canonical === "no-answer")
|
|
37
|
+
return didntAnswer(path);
|
|
32
38
|
if (canonical === null)
|
|
33
39
|
return { ok: false, reason: "unavailable", message: `${path} isn't a folder on this computer.` };
|
|
34
40
|
const top = await exec("git", ["rev-parse", "--show-toplevel"], { cwd: canonical, timeoutMs: 5_000, ...ADMISSION_GIT });
|
|
35
|
-
const
|
|
41
|
+
const topReal = top.code === 0 ? await canon(top.stdout.trim()) : null;
|
|
42
|
+
if (topReal === "no-answer")
|
|
43
|
+
return didntAnswer(top.stdout.trim());
|
|
44
|
+
const repo = topReal;
|
|
36
45
|
if (repo === null)
|
|
37
46
|
return { ok: false, reason: "not-git", message: `${path} isn't a Git repository. Run git init there first, or choose a folder inside one.` };
|
|
38
47
|
return { ok: true, repo };
|
|
@@ -48,7 +57,7 @@ async function originOf(repo, exec) {
|
|
|
48
57
|
}
|
|
49
58
|
/** Registry first, then the project and its audit records in one transaction. Re-adds preserve trust. */
|
|
50
59
|
export async function admitProject(store, input) {
|
|
51
|
-
const proved = await proveProjectRoot(input.path, input.exec);
|
|
60
|
+
const proved = await proveProjectRoot(input.path, input.exec, input.resolveFolder);
|
|
52
61
|
if (!proved.ok)
|
|
53
62
|
return proved;
|
|
54
63
|
const { repo } = proved;
|
|
@@ -110,12 +119,40 @@ export async function admitProject(store, input) {
|
|
|
110
119
|
* saved project changes nothing.
|
|
111
120
|
*/
|
|
112
121
|
export async function releaseProject(store, input) {
|
|
113
|
-
|
|
122
|
+
// Removing only narrows: a folder the console's gate holds back (folder-gate.ts) is removed by its exact saved path,
|
|
123
|
+
// and matches no other spelling.
|
|
124
|
+
// Latched for the whole removal: once its folder was unavailable, a later answer (during an await below) never
|
|
125
|
+
// widens it to other spellings.
|
|
126
|
+
const unavailable = (error) => error instanceof FolderUnavailable;
|
|
127
|
+
let asked, exactOnly = false;
|
|
128
|
+
try {
|
|
129
|
+
asked = canonicalProject(input.path) ?? resolve(input.path);
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
if (!unavailable(error))
|
|
133
|
+
throw error;
|
|
134
|
+
asked = resolve(input.path);
|
|
135
|
+
exactOnly = true;
|
|
136
|
+
}
|
|
137
|
+
const same = (one) => {
|
|
138
|
+
if (exactOnly)
|
|
139
|
+
return false;
|
|
140
|
+
try {
|
|
141
|
+
return sameRepo(one, asked);
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
if (unavailable(error)) {
|
|
145
|
+
exactOnly = true;
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
throw error;
|
|
149
|
+
}
|
|
150
|
+
};
|
|
114
151
|
const loaded = input.registryFile === null ? { repos: [] } : await loadProjectRegistry(input.registryFile);
|
|
115
152
|
if ("error" in loaded)
|
|
116
153
|
return { ok: false, reason: "registry", message: loaded.error };
|
|
117
|
-
const entries = loaded.repos.filter(one => one === asked ||
|
|
118
|
-
const rows = store.listProjects().filter(one => one.path === asked ||
|
|
154
|
+
const entries = loaded.repos.filter(one => one === asked || same(one));
|
|
155
|
+
const rows = store.listProjects().filter(one => one.path === asked || same(one.path)).map(one => one.path);
|
|
119
156
|
const repo = entries[0] ?? rows[0] ?? asked;
|
|
120
157
|
if (entries.length === 0 && rows.length === 0)
|
|
121
158
|
return { ok: true, repo, name: projectName(repo), removed: false };
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
*
|
|
17
17
|
* An embedder or test that passes no identities gets `syncProjectIdentities`: the old synchronous reads, unchanged.
|
|
18
18
|
*/
|
|
19
|
+
import { type FolderGate } from "./folder-gate.js";
|
|
19
20
|
import { type FolderLookOptions } from "./folder-look.js";
|
|
20
21
|
export type ProjectIdentities = {
|
|
21
22
|
/** For display and grouping only: the last known identity, else the path as saved (resolved lexically). Reads
|
|
@@ -36,9 +37,16 @@ export type ProjectIdentities = {
|
|
|
36
37
|
/** Moves whenever a known identity changes or goes: a decision made before an await is re-checked against it right
|
|
37
38
|
* before its write. */
|
|
38
39
|
generation: () => number;
|
|
40
|
+
/** Whether a synchronous read inside `path` may happen on the console's thread now (folder-gate.ts): the folder it is
|
|
41
|
+
* in answered a look within the last few minutes, and no look at it since went unanswered. Reads nothing. */
|
|
42
|
+
answering: (path: string) => boolean;
|
|
39
43
|
};
|
|
40
44
|
/** How many folders a pass looks at side by side when they're looked at one by one (within FOLDER_LOOK_CHILDREN). */
|
|
41
45
|
export declare const IDENTITY_LOOKS = 4;
|
|
42
46
|
export declare function projectIdentities(look?: FolderLookOptions, now?: () => number): ProjectIdentities;
|
|
43
47
|
/** The synchronous reads, as before: for embedders and tests that pass no identities. */
|
|
44
48
|
export declare function syncProjectIdentities(): ProjectIdentities;
|
|
49
|
+
/** The console's folder gate (folder-gate.ts): its own data folders (the database's, evidence, build checkouts) are read
|
|
50
|
+
* freely when the console names them; a path read from a file a build can write (`pointer`), and anything else, only
|
|
51
|
+
* while `answering` vouches for where it leads. */
|
|
52
|
+
export declare function consoleFolderGate(identities: ProjectIdentities, own: readonly (string | undefined)[]): FolderGate;
|
package/dist/project-identity.js
CHANGED
|
@@ -17,10 +17,14 @@
|
|
|
17
17
|
* An embedder or test that passes no identities gets `syncProjectIdentities`: the old synchronous reads, unchanged.
|
|
18
18
|
*/
|
|
19
19
|
import { resolve } from "node:path";
|
|
20
|
+
import { insideFolder } from "./folder-gate.js";
|
|
20
21
|
import { lookIdentities } from "./folder-look.js";
|
|
21
22
|
import { canonicalProject } from "./project.js";
|
|
22
23
|
/** How long an answer stands before the background pass resolves the path again. */
|
|
23
24
|
const STALE_MS = 60_000;
|
|
25
|
+
/** How long an answer vouches that its folder answers, for `answering`: past it (the background pass every 30 s
|
|
26
|
+
* renews it each minute), a synchronous read waits for the next look. */
|
|
27
|
+
const ANSWERING_MS = 3 * STALE_MS;
|
|
24
28
|
/** How many folders a pass looks at side by side when they're looked at one by one (within FOLDER_LOOK_CHILDREN). */
|
|
25
29
|
export const IDENTITY_LOOKS = 4;
|
|
26
30
|
export function projectIdentities(look = {}, now = Date.now) {
|
|
@@ -137,6 +141,37 @@ export function projectIdentities(look = {}, now = Date.now) {
|
|
|
137
141
|
passes = pass.then(() => undefined, () => undefined);
|
|
138
142
|
return pass;
|
|
139
143
|
},
|
|
144
|
+
answering: path => {
|
|
145
|
+
const when = now();
|
|
146
|
+
const recent = (at) => when - at < ANSWERING_MS;
|
|
147
|
+
// Decided on real paths only. First the spelling: a path under an answered spelling (the longest) is read where
|
|
148
|
+
// that answer says it leads, and only while that answer is recent, since a link can be pointed elsewhere.
|
|
149
|
+
let real = resolve(path);
|
|
150
|
+
let spelled = null;
|
|
151
|
+
for (const [key, answer] of answers) {
|
|
152
|
+
if (answer.real !== null && insideFolder(real, key) && (spelled === null || key.length > spelled.key.length))
|
|
153
|
+
spelled = { key, real: answer.real, at: answer.at };
|
|
154
|
+
}
|
|
155
|
+
if (spelled !== null) {
|
|
156
|
+
if (!recent(spelled.at))
|
|
157
|
+
return false;
|
|
158
|
+
real = resolve(spelled.real, `.${real.slice(spelled.key.length)}`);
|
|
159
|
+
}
|
|
160
|
+
// A folder whose last look didn't answer holds back every read inside where it leads, whatever older answer stands.
|
|
161
|
+
for (const key of suspect)
|
|
162
|
+
if (insideFolder(real, answers.get(key)?.real ?? key))
|
|
163
|
+
return false;
|
|
164
|
+
// Otherwise the nearest real folder an answer names decides, by its newest answer; "/" vouches for nothing below it.
|
|
165
|
+
let nearest = null;
|
|
166
|
+
for (const answer of answers.values()) {
|
|
167
|
+
const folder = answer.real;
|
|
168
|
+
if (folder === null || (folder === "/" && real !== "/") || !insideFolder(real, folder))
|
|
169
|
+
continue;
|
|
170
|
+
if (nearest === null || folder.length > nearest.folder.length || (folder.length === nearest.folder.length && answer.at > nearest.at))
|
|
171
|
+
nearest = { folder, at: answer.at };
|
|
172
|
+
}
|
|
173
|
+
return nearest !== null && recent(nearest.at);
|
|
174
|
+
},
|
|
140
175
|
};
|
|
141
176
|
}
|
|
142
177
|
/** The synchronous reads, as before: for embedders and tests that pass no identities. */
|
|
@@ -149,5 +184,13 @@ export function syncProjectIdentities() {
|
|
|
149
184
|
resolveNow: async (path) => canonicalProject(path),
|
|
150
185
|
generation: () => 0,
|
|
151
186
|
refresh: async () => true,
|
|
187
|
+
answering: () => true,
|
|
152
188
|
};
|
|
153
189
|
}
|
|
190
|
+
/** The console's folder gate (folder-gate.ts): its own data folders (the database's, evidence, build checkouts) are read
|
|
191
|
+
* freely when the console names them; a path read from a file a build can write (`pointer`), and anything else, only
|
|
192
|
+
* while `answering` vouches for where it leads. */
|
|
193
|
+
export function consoleFolderGate(identities, own) {
|
|
194
|
+
const folders = [...new Set(own.filter((one) => typeof one === "string" && one !== "").map(one => resolve(one)))];
|
|
195
|
+
return (path, pointer) => (!pointer && folders.some(folder => insideFolder(path, folder))) || identities.answering(path);
|
|
196
|
+
}
|
|
@@ -22,19 +22,18 @@ export type KnowledgeView = ReturnType<typeof knowledgeView>;
|
|
|
22
22
|
export declare function knowledgeVersion(store: Store, repo: string, actor: string, revision: number): Knowledge;
|
|
23
23
|
/** Chat reads only an admitted project. Keep the full source in the project UI. */
|
|
24
24
|
export declare function conversationKnowledge(store: Store, repo: string, actor: string, reference?: string, decision?: number): {
|
|
25
|
-
|
|
25
|
+
content?: never;
|
|
26
26
|
revision: number;
|
|
27
27
|
stale: boolean;
|
|
28
28
|
notice: string;
|
|
29
29
|
decision?: never;
|
|
30
30
|
title?: never;
|
|
31
|
-
|
|
31
|
+
history?: never;
|
|
32
32
|
instructions?: never;
|
|
33
33
|
references?: never;
|
|
34
34
|
decisions?: never;
|
|
35
35
|
} | {
|
|
36
|
-
|
|
37
|
-
history?: never;
|
|
36
|
+
content?: never;
|
|
38
37
|
stale?: never;
|
|
39
38
|
decision: {
|
|
40
39
|
repo: string;
|
|
@@ -52,26 +51,27 @@ export declare function conversationKnowledge(store: Store, repo: string, actor:
|
|
|
52
51
|
};
|
|
53
52
|
notice: string;
|
|
54
53
|
title?: never;
|
|
55
|
-
|
|
54
|
+
revision?: never;
|
|
55
|
+
history?: never;
|
|
56
56
|
instructions?: never;
|
|
57
57
|
references?: never;
|
|
58
58
|
decisions?: never;
|
|
59
59
|
} | {
|
|
60
|
-
history?: never;
|
|
61
60
|
stale?: never;
|
|
62
61
|
decision?: never;
|
|
63
62
|
revision: number;
|
|
64
63
|
title: string;
|
|
65
64
|
content: string;
|
|
66
65
|
notice: string;
|
|
66
|
+
history?: never;
|
|
67
67
|
instructions?: never;
|
|
68
68
|
references?: never;
|
|
69
69
|
decisions?: never;
|
|
70
70
|
} | {
|
|
71
|
+
content?: never;
|
|
71
72
|
stale?: never;
|
|
72
73
|
decision?: never;
|
|
73
74
|
title?: never;
|
|
74
|
-
content?: never;
|
|
75
75
|
revision: number;
|
|
76
76
|
history: {
|
|
77
77
|
revision: number;
|
package/dist/project.d.ts
CHANGED
|
@@ -16,6 +16,7 @@ export type ProjectExec = (file: string, args: readonly string[], options?: RunO
|
|
|
16
16
|
* One canonicalization for every path that becomes project identity:
|
|
17
17
|
* startup config, a session open, task placement. Comparisons elsewhere
|
|
18
18
|
* fall back to raw equality for legacy rows written before this existed.
|
|
19
|
+
* Throws FolderUnavailable when the console's folder gate holds the read back (folder-gate.ts).
|
|
19
20
|
*/
|
|
20
21
|
export declare function canonicalProject(path: string): string | null;
|
|
21
22
|
/** Two repo identities, canonical when possible, raw-equal as the legacy fallback. */
|
package/dist/project.js
CHANGED
|
@@ -13,12 +13,18 @@
|
|
|
13
13
|
import { realpathSync, statSync } from "node:fs";
|
|
14
14
|
import { basename, resolve, sep } from "node:path";
|
|
15
15
|
import { run } from "./exec.js";
|
|
16
|
+
import { folderAnswers, FolderUnavailable } from "./folder-gate.js";
|
|
16
17
|
/**
|
|
17
18
|
* One canonicalization for every path that becomes project identity:
|
|
18
19
|
* startup config, a session open, task placement. Comparisons elsewhere
|
|
19
20
|
* fall back to raw equality for legacy rows written before this existed.
|
|
21
|
+
* Throws FolderUnavailable when the console's folder gate holds the read back (folder-gate.ts).
|
|
20
22
|
*/
|
|
21
23
|
export function canonicalProject(path) {
|
|
24
|
+
// In the console, a folder that hasn't answered a look lately is not read on its thread (folder-gate.ts). That is
|
|
25
|
+
// "unavailable", never "not a folder": it throws, so no caller falls back to the path's spelling because of it.
|
|
26
|
+
if (!folderAnswers(path))
|
|
27
|
+
throw new FolderUnavailable(resolve(path));
|
|
22
28
|
try {
|
|
23
29
|
const real = realpathSync(resolve(path));
|
|
24
30
|
return statSync(real).isDirectory() ? real : null;
|
package/dist/proposal.js
CHANGED
|
@@ -30,6 +30,7 @@ export { validateTaskText } from "./task-text.js";
|
|
|
30
30
|
import { resolve } from "node:path";
|
|
31
31
|
import { canonicalProject } from "./project.js";
|
|
32
32
|
import { heuristicSizing, planningSignals, refineFiledSizing } from "./task-sizing.js";
|
|
33
|
+
import { FolderUnavailable } from "./folder-gate.js";
|
|
33
34
|
/** Provenance tokens are part of the audit surface: lowercase, bounded,
|
|
34
35
|
* nothing that could render as anything but itself. */
|
|
35
36
|
const FILED_VIA = /^[a-z][a-z0-9:-]{0,63}$/;
|
|
@@ -75,7 +76,16 @@ function checkRepo(given, admitted) {
|
|
|
75
76
|
// directory resolves through symlinks; a path that does not exist yet
|
|
76
77
|
// still normalizes, because filing must not depend on this machine
|
|
77
78
|
// seeing every repo the plane knows about.
|
|
78
|
-
|
|
79
|
+
let canonical;
|
|
80
|
+
// A folder the console's gate holds back (folder-gate.ts) is unavailable: refused, never filed by its spelling.
|
|
81
|
+
try {
|
|
82
|
+
canonical = canonicalProject(given) ?? resolve(given);
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
if (error instanceof FolderUnavailable)
|
|
86
|
+
return refuse("bad-repo", "that repository's folder didn't answer — it may be on a drive that's disconnected; try again in a moment");
|
|
87
|
+
throw error;
|
|
88
|
+
}
|
|
79
89
|
if (admitted !== undefined && !admitted.includes(canonical)) {
|
|
80
90
|
return refuse("outside-ceiling", "that repository is outside what this surface was configured to show");
|
|
81
91
|
}
|
package/dist/quick-start.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type AttachSeams } from "./console-instance.js";
|
|
1
|
+
import { type AttachProof, type AttachSeams } from "./console-instance.js";
|
|
2
2
|
export type QuickStartSeams = {
|
|
3
3
|
databaseFile: string;
|
|
4
4
|
cwd: string;
|
|
@@ -20,5 +20,7 @@ export type QuickStartSeams = {
|
|
|
20
20
|
};
|
|
21
21
|
/** Whether argv is a bare `toolroll` (nothing, or only --json). */
|
|
22
22
|
export declare const isBareInvocation: (argv: readonly string[]) => boolean;
|
|
23
|
+
/** This database's console, proved live without creating, migrating or changing anything (also easy-start.ts's). */
|
|
24
|
+
export declare function proveConsoleReadOnly(databaseFile: string, build: string, attach?: AttachSeams): Promise<AttachProof>;
|
|
23
25
|
/** null: not this command (outside a repository) — the caller scans as before. */
|
|
24
26
|
export declare function runQuickStart(argv: readonly string[], write: (line: string) => void, seams: QuickStartSeams): Promise<number | null>;
|
package/dist/quick-start.js
CHANGED
|
@@ -45,7 +45,12 @@ function databasePresence(file) {
|
|
|
45
45
|
return error.code === "ENOENT" ? "absent" : "unknown";
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
|
-
|
|
48
|
+
function prove(seams) {
|
|
49
|
+
return proveConsoleReadOnly(seams.databaseFile, seams.build, seams.attach ?? {});
|
|
50
|
+
}
|
|
51
|
+
/** This database's console, proved live without creating, migrating or changing anything (also easy-start.ts's). */
|
|
52
|
+
export async function proveConsoleReadOnly(databaseFile, build, attach = {}) {
|
|
53
|
+
const seams = { databaseFile, build, attach };
|
|
49
54
|
// Read only, never created or migrated, never cleaned up: proving changes nothing. "None" only when nothing of this
|
|
50
55
|
// build can be running on it (no database yet, or one older than this build, which `up` brings up to date); a
|
|
51
56
|
// database that can't be read, is mid-upgrade or is newer than this build can't be checked, so it is refused.
|
package/dist/recipes.js
CHANGED
|
@@ -12,6 +12,7 @@ import { applyModeToNewFiling } from "./plan-auto.js";
|
|
|
12
12
|
import { canonicalProject } from "./project.js";
|
|
13
13
|
import { RECIPE_FORMAT, RECIPE_LIMITS, readRecipeDocument } from "./contracts/recipes.js";
|
|
14
14
|
import { resolve } from "node:path";
|
|
15
|
+
import { FolderUnavailable } from "./folder-gate.js";
|
|
15
16
|
export const RECIPE_SCHEMA = `
|
|
16
17
|
CREATE TABLE IF NOT EXISTS workflow_recipe (
|
|
17
18
|
id TEXT NOT NULL, revision INTEGER NOT NULL CHECK(revision > 0),
|
|
@@ -47,7 +48,17 @@ export class RecipeError extends Error {
|
|
|
47
48
|
}
|
|
48
49
|
}
|
|
49
50
|
const honest = (text) => !hasForbiddenControls(text) && !hasDisguisedText(text);
|
|
50
|
-
const recipeProject = (repo) =>
|
|
51
|
+
const recipeProject = (repo) => {
|
|
52
|
+
// A project folder the console's gate holds back (folder-gate.ts) is unavailable, never matched by its spelling.
|
|
53
|
+
try {
|
|
54
|
+
return canonicalProject(repo) ?? resolve(repo);
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
if (error instanceof FolderUnavailable)
|
|
58
|
+
throw new RecipeError("This project's folder didn't answer. Try again in a moment.", 503);
|
|
59
|
+
throw error;
|
|
60
|
+
}
|
|
61
|
+
};
|
|
51
62
|
/** What a refusal at each top-level field tells the person, after the path-named line. */
|
|
52
63
|
const FIELD_ADVICE = {
|
|
53
64
|
name: "Name, goal, exclusions, and allowed paths must be readable text.", description: "Name, goal, exclusions, and allowed paths must be readable text.",
|
package/dist/release-cli.js
CHANGED
|
@@ -297,7 +297,8 @@ function installedGateWorld() {
|
|
|
297
297
|
db.exec("PRAGMA busy_timeout=5000");
|
|
298
298
|
return db;
|
|
299
299
|
},
|
|
300
|
-
|
|
300
|
+
// The laptop service too: two services on one database is a conflict the gate names, never one it skips.
|
|
301
|
+
servicePlists: () => ["com.toolroll.browser", "com.standing-orders.browser", "com.toolroll.up"].map(label => join(agents, `${label}.plist`))
|
|
301
302
|
.filter(path => existsSync(path)).map(path => ({ path, text: readFileSync(path, "utf8") })),
|
|
302
303
|
};
|
|
303
304
|
}
|
package/dist/runner-process.d.ts
CHANGED
|
@@ -65,3 +65,12 @@ export declare function recordedHolderProvedGone(file: string, holder: {
|
|
|
65
65
|
gone: false;
|
|
66
66
|
why: string;
|
|
67
67
|
};
|
|
68
|
+
/**
|
|
69
|
+
* Whether a record is authentic for exactly this registration on this host (the checks holderProvedGone makes before
|
|
70
|
+
* it looks at any process): sealed with the stored credential hash, for this runner, host, registration and
|
|
71
|
+
* credential. Null when it stands; otherwise why not. It says nothing about which processes still run.
|
|
72
|
+
*/
|
|
73
|
+
export declare function recordStandsFor(record: RunnerProcessRecord | null, holder: {
|
|
74
|
+
runner: Runner;
|
|
75
|
+
credentialHash: string;
|
|
76
|
+
}, host: string): string | null;
|
package/dist/runner-process.js
CHANGED
|
@@ -217,3 +217,21 @@ export function recordedHolderProvedGone(file, holder, host, probe = realProcess
|
|
|
217
217
|
return { gone: false, why: "its process record changed during the exit proof" };
|
|
218
218
|
return proof;
|
|
219
219
|
}
|
|
220
|
+
/**
|
|
221
|
+
* Whether a record is authentic for exactly this registration on this host (the checks holderProvedGone makes before
|
|
222
|
+
* it looks at any process): sealed with the stored credential hash, for this runner, host, registration and
|
|
223
|
+
* credential. Null when it stands; otherwise why not. It says nothing about which processes still run.
|
|
224
|
+
*/
|
|
225
|
+
export function recordStandsFor(record, holder, host) {
|
|
226
|
+
if (record === null)
|
|
227
|
+
return "no readable record of its processes";
|
|
228
|
+
const expected = sealRunnerProcessRecord(record, holder.credentialHash).signature;
|
|
229
|
+
if (typeof record.signature !== "string" || !/^[a-f0-9]{64}$/.test(record.signature)
|
|
230
|
+
|| !timingSafeEqual(Buffer.from(record.signature, "hex"), Buffer.from(expected, "hex")))
|
|
231
|
+
return "its process record is not authenticated";
|
|
232
|
+
if (record.runner !== holder.runner.name || record.host !== holder.runner.host || record.host !== host)
|
|
233
|
+
return "its record is for another worker or machine";
|
|
234
|
+
if (record.registeredAt !== holder.runner.registeredAt || record.credential !== credentialDigest(holder.credentialHash))
|
|
235
|
+
return "its record is from an earlier start";
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type ExecResult, type RunOptions } from "./exec.js";
|
|
2
|
+
import type { Store } from "./store.js";
|
|
3
|
+
export type GitRun = (file: string, args: readonly string[], options: RunOptions) => Promise<ExecResult>;
|
|
4
|
+
export type SampleOutcome = {
|
|
5
|
+
ok: true;
|
|
6
|
+
path: string;
|
|
7
|
+
created: boolean;
|
|
8
|
+
} | {
|
|
9
|
+
ok: false;
|
|
10
|
+
reason: "no-root" | "exists" | "failed";
|
|
11
|
+
message: string;
|
|
12
|
+
};
|
|
13
|
+
/** Make (or find again) the sample project in `root`, the folder projects go in. */
|
|
14
|
+
export declare function createSampleProject(root: string | null, git?: GitRun): Promise<SampleOutcome>;
|
|
15
|
+
/** The sample's own check: its one test file, run by Node's test runner. Fixed here; never anyone's input. */
|
|
16
|
+
export declare const SAMPLE_CHECK_COMMAND = "node --test";
|
|
17
|
+
/** Who the sample's check is recorded as approved by: Toolroll itself, never the person who clicked (no step-up). */
|
|
18
|
+
export declare const SAMPLE_CHECK_ACTOR = "toolroll";
|
|
19
|
+
/**
|
|
20
|
+
* The sample project's check, set as part of making it, so its first result reads checked: the fixed command above,
|
|
21
|
+
* recorded as Toolroll's (SAMPLE_CHECK_ACTOR), with the person who added the sample named in the ledger line. Only
|
|
22
|
+
* for a sample this request just made (the caller passes createSampleProject's `created: true`; a folder found again
|
|
23
|
+
* is never armed), only for the folder this made (our marker in its .git), and only while the project has never had
|
|
24
|
+
* a check at all (no verify_command row, live or revoked, for it or its quick check): a check someone cleared stays
|
|
25
|
+
* cleared, and one someone approved is never replaced. It takes no command: this is not a way to set a project's
|
|
26
|
+
* check. True when it set one now.
|
|
27
|
+
*/
|
|
28
|
+
export declare function registerSampleCheck(store: Pick<Store, "transact" | "handle" | "setVerifyCommand" | "recordAction">, made: {
|
|
29
|
+
path: string;
|
|
30
|
+
created: boolean;
|
|
31
|
+
}, repo: string, addedBy: string, now: Date): boolean;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The sample project the welcome offers ("Try a sample project"): a small real git repository — a README, one function
|
|
3
|
+
* and its test — made in the project folder, so a first task has something true to change. It is registered like any
|
|
4
|
+
* other project by its caller (project-admission.ts).
|
|
5
|
+
*
|
|
6
|
+
* The folder is claimed with an exclusive mkdir; a folder already there is used again only when it is ours (a real
|
|
7
|
+
* directory, not a link, holding our marker inside its .git), and refused otherwise: nothing in someone else's folder
|
|
8
|
+
* is touched. Git runs hardened (no system or global config, no hooks or hook templates, no fsmonitor, no signing, no
|
|
9
|
+
* prompts, an allowlisted environment), and a folder this call made is removed again when making it fails.
|
|
10
|
+
*/
|
|
11
|
+
import { lstatSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
12
|
+
import { isAbsolute, join } from "node:path";
|
|
13
|
+
import { run } from "./exec.js";
|
|
14
|
+
import { SAMPLE_PROJECT_NAME } from "./first-run.js";
|
|
15
|
+
import { ENV, ENV_ALLOWLIST } from "./remote-main.js";
|
|
16
|
+
import { TEARDOWN_FLAGS } from "./teardown-git.js";
|
|
17
|
+
/** Inside the sample's .git: says the folder is the one this made (never tracked, so the work tree stays clean). */
|
|
18
|
+
const MARKER = "toolroll-sample";
|
|
19
|
+
const MARKER_TEXT = "Made by Toolroll's welcome as the sample project.\n";
|
|
20
|
+
const FILES = {
|
|
21
|
+
"README.md": `# Toolroll sample
|
|
22
|
+
|
|
23
|
+
A tiny project to try Toolroll on. \`src/price.js\` has one function, and \`test/price.test.js\` checks it.
|
|
24
|
+
|
|
25
|
+
Run the tests with \`npm test\` (Node.js 22 or newer, nothing to install).
|
|
26
|
+
|
|
27
|
+
Things to ask for:
|
|
28
|
+
|
|
29
|
+
- Round prices to whole cents and add a test for it.
|
|
30
|
+
- Refuse a negative discount with a clear error.
|
|
31
|
+
- Add a short usage example to this README.
|
|
32
|
+
`,
|
|
33
|
+
"package.json": `${JSON.stringify({ name: "toolroll-sample", version: "1.0.0", private: true, type: "module", scripts: { test: "node --test" } }, null, 2)}\n`,
|
|
34
|
+
"src/price.js": `/** The price after a percentage discount: priceWithDiscount(80, 25) is 60. */
|
|
35
|
+
export function priceWithDiscount(price, percent) {
|
|
36
|
+
return price - (price * percent) / 100;
|
|
37
|
+
}
|
|
38
|
+
`,
|
|
39
|
+
"test/price.test.js": `import assert from "node:assert/strict";
|
|
40
|
+
import { test } from "node:test";
|
|
41
|
+
import { priceWithDiscount } from "../src/price.js";
|
|
42
|
+
|
|
43
|
+
test("takes the discount off the price", () => {
|
|
44
|
+
assert.equal(priceWithDiscount(80, 25), 60);
|
|
45
|
+
});
|
|
46
|
+
`,
|
|
47
|
+
".gitignore": "node_modules/\n",
|
|
48
|
+
};
|
|
49
|
+
/** Git for the sample: fixed settings first, an allowlisted environment with no system or global config. */
|
|
50
|
+
const SAMPLE_FLAGS = [
|
|
51
|
+
...TEARDOWN_FLAGS, "-c", "commit.gpgsign=false", "-c", "tag.gpgsign=false", "-c", "init.templateDir=",
|
|
52
|
+
"-c", "user.name=Toolroll", "-c", "user.email=sample@toolroll.invalid", "-c", "core.autocrlf=false",
|
|
53
|
+
];
|
|
54
|
+
function kindOf(path) {
|
|
55
|
+
try {
|
|
56
|
+
const seen = lstatSync(path);
|
|
57
|
+
return seen.isDirectory() && !seen.isSymbolicLink() ? "dir" : "other";
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
return error.code === "ENOENT" ? "missing" : "other";
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/** Ours: a real folder whose .git is a real folder holding our marker, a regular file with our words. */
|
|
64
|
+
function isOurs(target) {
|
|
65
|
+
if (kindOf(target) !== "dir" || kindOf(join(target, ".git")) !== "dir")
|
|
66
|
+
return false;
|
|
67
|
+
const marker = join(target, ".git", MARKER);
|
|
68
|
+
try {
|
|
69
|
+
const seen = lstatSync(marker);
|
|
70
|
+
return seen.isFile() && seen.size === Buffer.byteLength(MARKER_TEXT) && readFileSync(marker, "utf8") === MARKER_TEXT;
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/** Make (or find again) the sample project in `root`, the folder projects go in. */
|
|
77
|
+
export async function createSampleProject(root, git = run) {
|
|
78
|
+
if (root === null || root === "" || !isAbsolute(root) || kindOf(root) !== "dir") {
|
|
79
|
+
return { ok: false, reason: "no-root", message: "There's no project folder to put the sample in." };
|
|
80
|
+
}
|
|
81
|
+
const target = join(root, SAMPLE_PROJECT_NAME);
|
|
82
|
+
if (isOurs(target))
|
|
83
|
+
return { ok: true, path: target, created: false };
|
|
84
|
+
try {
|
|
85
|
+
mkdirSync(target); // the claim: exclusive, and never through a link
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return { ok: false, reason: "exists", message: `A folder named ${SAMPLE_PROJECT_NAME} is already there, so nothing was changed.` };
|
|
89
|
+
}
|
|
90
|
+
const fail = () => {
|
|
91
|
+
try {
|
|
92
|
+
rmSync(target, { recursive: true, force: true });
|
|
93
|
+
}
|
|
94
|
+
catch { /* left for a person to remove */ }
|
|
95
|
+
return { ok: false, reason: "failed", message: "The sample project couldn't be made. Nothing was added." };
|
|
96
|
+
};
|
|
97
|
+
try {
|
|
98
|
+
for (const [file, body] of Object.entries(FILES)) {
|
|
99
|
+
const path = join(target, file);
|
|
100
|
+
mkdirSync(join(path, ".."), { recursive: true });
|
|
101
|
+
writeFileSync(path, body, { flag: "wx" });
|
|
102
|
+
}
|
|
103
|
+
const options = { cwd: target, env: ENV, envAllowlist: ENV_ALLOWLIST, timeoutMs: 30_000 };
|
|
104
|
+
for (const args of [["init", "-q", "--template=", "-b", "main"], ["add", "--", "."], ["commit", "-q", "--no-verify", "-m", "Sample project"]]) {
|
|
105
|
+
const ran = await git("git", [...SAMPLE_FLAGS, ...args], options);
|
|
106
|
+
if (ran.code !== 0)
|
|
107
|
+
return fail();
|
|
108
|
+
}
|
|
109
|
+
writeFileSync(join(target, ".git", MARKER), MARKER_TEXT, { flag: "wx", mode: 0o600 });
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
return fail();
|
|
113
|
+
}
|
|
114
|
+
return { ok: true, path: target, created: true };
|
|
115
|
+
}
|
|
116
|
+
/** The sample's own check: its one test file, run by Node's test runner. Fixed here; never anyone's input. */
|
|
117
|
+
export const SAMPLE_CHECK_COMMAND = "node --test";
|
|
118
|
+
const SAMPLE_CHECK_TIMEOUT_MS = 60_000;
|
|
119
|
+
/** Who the sample's check is recorded as approved by: Toolroll itself, never the person who clicked (no step-up). */
|
|
120
|
+
export const SAMPLE_CHECK_ACTOR = "toolroll";
|
|
121
|
+
/**
|
|
122
|
+
* The sample project's check, set as part of making it, so its first result reads checked: the fixed command above,
|
|
123
|
+
* recorded as Toolroll's (SAMPLE_CHECK_ACTOR), with the person who added the sample named in the ledger line. Only
|
|
124
|
+
* for a sample this request just made (the caller passes createSampleProject's `created: true`; a folder found again
|
|
125
|
+
* is never armed), only for the folder this made (our marker in its .git), and only while the project has never had
|
|
126
|
+
* a check at all (no verify_command row, live or revoked, for it or its quick check): a check someone cleared stays
|
|
127
|
+
* cleared, and one someone approved is never replaced. It takes no command: this is not a way to set a project's
|
|
128
|
+
* check. True when it set one now.
|
|
129
|
+
*/
|
|
130
|
+
export function registerSampleCheck(store, made, repo, addedBy, now) {
|
|
131
|
+
if (!made.created || !isAbsolute(repo) || !isOurs(repo))
|
|
132
|
+
return false;
|
|
133
|
+
return store.transact(() => {
|
|
134
|
+
if (store.handle.prepare("SELECT 1 FROM verify_command WHERE repo IN (?, ?) LIMIT 1").get(repo, `quick:${repo}`) !== undefined)
|
|
135
|
+
return false;
|
|
136
|
+
store.setVerifyCommand({ repo, command: SAMPLE_CHECK_COMMAND, timeoutMs: SAMPLE_CHECK_TIMEOUT_MS, approvedBy: SAMPLE_CHECK_ACTOR }, now);
|
|
137
|
+
store.recordAction({ at: now.toISOString(), actor: SAMPLE_CHECK_ACTOR, repo, taskId: null, runId: null, action: "check approved", outcome: "approved", source: "policy",
|
|
138
|
+
detail: `the sample project's own test (${SAMPLE_CHECK_COMMAND}), set by Toolroll when ${addedBy} added the sample` });
|
|
139
|
+
return true;
|
|
140
|
+
});
|
|
141
|
+
}
|