toolroll 0.9.5 → 0.9.7
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 +49 -0
- package/dist/actor.d.ts +21 -0
- package/dist/actor.js +48 -0
- package/dist/assignment-adapters.d.ts +1 -0
- package/dist/assignment-presentation.d.ts +13 -2
- package/dist/assignment-presentation.js +22 -9
- package/dist/assignment-ui.d.ts +10 -15
- package/dist/assignment-ui.js +19 -18
- package/dist/assignment.d.ts +9 -0
- package/dist/assignment.js +62 -13
- package/dist/browser/workspace.css +2 -2
- package/dist/browser/workspace.js +29 -24
- package/dist/browser-workspace.d.ts +120 -12
- package/dist/builder.d.ts +1 -0
- package/dist/builder.js +33 -13
- package/dist/chat-acceptance.js +2 -2
- package/dist/chat-actions.js +2 -1
- package/dist/chat-delivery.js +2 -0
- package/dist/chat-quiet.js +30 -19
- package/dist/chat-task-actions.d.ts +13 -0
- package/dist/chat-task-actions.js +19 -1
- package/dist/check-levels-ui.d.ts +21 -0
- package/dist/check-levels-ui.js +70 -0
- package/dist/check-levels.d.ts +67 -0
- package/dist/check-levels.js +171 -0
- package/dist/claim.d.ts +22 -0
- package/dist/claim.js +43 -0
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +4 -0
- package/dist/demo-chat.d.ts +0 -5
- package/dist/demo-chat.js +14 -3
- package/dist/dispatch.js +7 -2
- package/dist/dispose.d.ts +8 -0
- package/dist/dispose.js +7 -1
- package/dist/export.js +3 -2
- package/dist/first-run.d.ts +34 -0
- package/dist/first-run.js +37 -1
- package/dist/flow-gallery-ui.d.ts +30 -0
- package/dist/flow-gallery-ui.js +85 -0
- package/dist/flow-gallery.d.ts +111 -0
- package/dist/flow-gallery.js +445 -0
- package/dist/flow-share.d.ts +102 -0
- package/dist/flow-share.js +614 -0
- package/dist/flow-starters.d.ts +2 -0
- package/dist/flow-starters.js +1 -1
- package/dist/flow-steps.js +7 -0
- package/dist/flow-triggers.js +2 -0
- package/dist/flows-cli.d.ts +56 -0
- package/dist/flows-cli.js +109 -3
- package/dist/flows-ui.d.ts +15 -1
- package/dist/flows-ui.js +31 -6
- package/dist/guides.js +27 -2
- package/dist/lead-context.js +6 -2
- package/dist/lead-status.d.ts +7 -0
- package/dist/lead-status.js +11 -2
- package/dist/mate-doors.d.ts +0 -15
- package/dist/mate-doors.js +5 -1
- package/dist/mate-tools.js +9 -4
- package/dist/models-ui.js +1 -1
- package/dist/operate.d.ts +4 -1
- package/dist/operate.js +601 -85
- package/dist/project-concurrency.d.ts +62 -0
- package/dist/project-concurrency.js +116 -0
- package/dist/project-delete-ui.d.ts +7 -0
- package/dist/project-delete-ui.js +19 -2
- package/dist/project-delete.js +4 -0
- package/dist/pull-request-flow.d.ts +9 -0
- package/dist/pull-request-flow.js +21 -3
- package/dist/result-follow-ups.d.ts +101 -0
- package/dist/result-follow-ups.js +333 -0
- package/dist/scope.js +11 -0
- package/dist/serve.d.ts +211 -2
- package/dist/serve.js +1070 -204
- package/dist/store.d.ts +76 -3
- package/dist/store.js +311 -22
- package/dist/surface.js +31 -8
- package/dist/tailnet.d.ts +11 -0
- package/dist/tailnet.js +47 -0
- package/dist/task-outcome-cli.js +2 -2
- package/dist/task-status.d.ts +186 -0
- package/dist/task-status.js +335 -0
- package/dist/team-cli.js +3 -0
- package/dist/telegram-progress.js +62 -100
- package/dist/telegram-status.js +21 -8
- package/dist/verification-evidence.js +21 -1
- package/dist/webhooks.js +5 -0
- package/dist/work-index.js +60 -11
- package/dist/work-summary.d.ts +0 -4
- package/dist/work-summary.js +2 -0
- package/dist/workspace-ui.d.ts +10 -0
- package/dist/workspace-ui.js +33 -2
- package/package.json +1 -1
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { Store, VerifyCommand } from "./store.js";
|
|
2
|
+
export declare const CHECK_LEVELS: readonly ["quick", "full", "off"];
|
|
3
|
+
export type CheckLevel = (typeof CHECK_LEVELS)[number];
|
|
4
|
+
export declare const isCheckLevel: (value: unknown) => value is CheckLevel;
|
|
5
|
+
export declare const CHECK_LEVEL_WORDS: Readonly<Record<CheckLevel, string>>;
|
|
6
|
+
export declare const CHECK_LEVEL_HINTS: Readonly<Record<CheckLevel, string>>;
|
|
7
|
+
/** Ledger actions. The ledger is append-only, so the newest entry is the setting. */
|
|
8
|
+
export declare const PROJECT_LEVEL_ACTION = "check level changed";
|
|
9
|
+
export declare const TASK_LEVEL_ACTION = "task checks chosen";
|
|
10
|
+
export declare const RUN_LEVEL_ACTION = "checks used";
|
|
11
|
+
/** The quick command's grant lives beside the full one under its own key. */
|
|
12
|
+
export declare const quickVerifyKey: (repo: string) => string;
|
|
13
|
+
export declare function liveQuickCommand(store: Store, repo: string): VerifyCommand | null;
|
|
14
|
+
/** Ledger reads go through the repo index: `repo = ?` (or IS NULL for unplaced work). */
|
|
15
|
+
export declare const repoClause: (repo: string | null) => [string, string[]];
|
|
16
|
+
export declare function repoOfRun(store: Store, runId: number): string | null;
|
|
17
|
+
export type ProjectCheckLevel = {
|
|
18
|
+
level: CheckLevel;
|
|
19
|
+
setBy: string | null;
|
|
20
|
+
at: string | null;
|
|
21
|
+
};
|
|
22
|
+
/** A project's level. With nothing recorded it is Full: a project that existed
|
|
23
|
+
* before levels keeps exactly the check it had. New projects are stamped Quick
|
|
24
|
+
* when they are added (stampNewProjectLevel). */
|
|
25
|
+
export declare function projectCheckLevel(store: Store, repo: string): ProjectCheckLevel;
|
|
26
|
+
/** An approver's act: set the project's level. The ledger keeps before → after. */
|
|
27
|
+
export declare function setProjectCheckLevel(store: Store, repo: string, level: CheckLevel, by: string, now: Date): {
|
|
28
|
+
changed: boolean;
|
|
29
|
+
before: CheckLevel;
|
|
30
|
+
};
|
|
31
|
+
/** A project added from now on starts on Quick. One that already has a level,
|
|
32
|
+
* or an approved check from before levels existed, is left exactly as it is. */
|
|
33
|
+
export declare function stampNewProjectLevel(store: Store, repo: string, now: Date): boolean;
|
|
34
|
+
/** A task's own choice, if it made one; a revision inherits the task it revises. */
|
|
35
|
+
export declare function taskCheckLevel(store: Store, taskId: string): CheckLevel | null;
|
|
36
|
+
/** A filing choice: this task's checks, overriding the project. Fixed once its
|
|
37
|
+
* plan is approved, so what the approver saw is what runs. */
|
|
38
|
+
export declare function setTaskCheckLevel(store: Store, taskId: string, level: CheckLevel, by: string, now: Date): {
|
|
39
|
+
ok: true;
|
|
40
|
+
} | {
|
|
41
|
+
ok: false;
|
|
42
|
+
message: string;
|
|
43
|
+
};
|
|
44
|
+
export type EffectiveCheckLevel = {
|
|
45
|
+
level: CheckLevel;
|
|
46
|
+
from: "task" | "project";
|
|
47
|
+
};
|
|
48
|
+
export declare function effectiveCheckLevel(store: Store, repo: string | null, taskId: string | null): EffectiveCheckLevel;
|
|
49
|
+
/** The command a level runs. Quick without its own command runs the full one
|
|
50
|
+
* (and says so: the level that ran is Full). Off runs nothing. */
|
|
51
|
+
export declare function checkCommandFor(store: Store, repo: string, level: CheckLevel): {
|
|
52
|
+
level: CheckLevel;
|
|
53
|
+
command: VerifyCommand | null;
|
|
54
|
+
};
|
|
55
|
+
/** The builder records which level a run used, so its result says it honestly. */
|
|
56
|
+
export declare function recordRunCheckLevel(store: Store, run: {
|
|
57
|
+
id: number;
|
|
58
|
+
taskId: string;
|
|
59
|
+
repo: string | null;
|
|
60
|
+
}, level: CheckLevel, from: EffectiveCheckLevel["from"], now: Date): void;
|
|
61
|
+
/** The level a finished run's checks used; null for a run from before levels. */
|
|
62
|
+
export declare function runCheckLevel(store: Store, runId: number): CheckLevel | null;
|
|
63
|
+
/** A starting point for the quick command, from the project's own scripts.
|
|
64
|
+
* It is only a suggestion: a person approves the exact command. */
|
|
65
|
+
export declare function suggestQuickCommand(repo: string): string | null;
|
|
66
|
+
/** "skip the tests", "run the full checks": a filing request in chat, as a level. */
|
|
67
|
+
export declare function checkLevelFromWords(text: string): CheckLevel | null;
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/** Check levels: how much of the project check runs after a build.
|
|
2
|
+
*
|
|
3
|
+
* - Quick: a fast check while work is in progress (typecheck plus the tests
|
|
4
|
+
* near the change). New projects start here.
|
|
5
|
+
* - Full: the project's approved check, as before. Projects that already
|
|
6
|
+
* had a check keep it.
|
|
7
|
+
* - Off: no check. The result reads Ready for review, with "Checks: Off for this project" underneath.
|
|
8
|
+
*
|
|
9
|
+
* Nothing here needs a new table. A project's level and a task's choice are
|
|
10
|
+
* append-only ledger entries (the newest one wins, and the ledger already
|
|
11
|
+
* says who changed what, when). The quick command is a second approved
|
|
12
|
+
* verification grant with its own digest, kept under a `quick:` key beside
|
|
13
|
+
* the full one, so approving it is the same ceremony as `verify set`. */
|
|
14
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
import { approvalOf } from "./scope.js";
|
|
17
|
+
export const CHECK_LEVELS = ["quick", "full", "off"];
|
|
18
|
+
export const isCheckLevel = (value) => typeof value === "string" && CHECK_LEVELS.includes(value);
|
|
19
|
+
export const CHECK_LEVEL_WORDS = { quick: "Quick", full: "Full", off: "Off" };
|
|
20
|
+
export const CHECK_LEVEL_HINTS = {
|
|
21
|
+
quick: "A fast check while building: typecheck and the tests near the change.",
|
|
22
|
+
full: "The project's full check after every build.",
|
|
23
|
+
off: "No check. Results are Ready for review, and say no check ran.",
|
|
24
|
+
};
|
|
25
|
+
/** Ledger actions. The ledger is append-only, so the newest entry is the setting. */
|
|
26
|
+
export const PROJECT_LEVEL_ACTION = "check level changed";
|
|
27
|
+
export const TASK_LEVEL_ACTION = "task checks chosen";
|
|
28
|
+
export const RUN_LEVEL_ACTION = "checks used";
|
|
29
|
+
/** The quick command's grant lives beside the full one under its own key. */
|
|
30
|
+
export const quickVerifyKey = (repo) => `quick:${repo}`;
|
|
31
|
+
export function liveQuickCommand(store, repo) {
|
|
32
|
+
return store.liveVerifyCommand(quickVerifyKey(repo));
|
|
33
|
+
}
|
|
34
|
+
/** Ledger reads go through the repo index: `repo = ?` (or IS NULL for unplaced work). */
|
|
35
|
+
export const repoClause = (repo) => repo === null ? ["repo IS NULL", []] : ["repo = ?", [repo]];
|
|
36
|
+
export function repoOfRun(store, runId) {
|
|
37
|
+
const run = store.getRun(runId);
|
|
38
|
+
return run === null ? null : store.refById(run.taskRef)?.repo ?? null;
|
|
39
|
+
}
|
|
40
|
+
function newest(store, sql, ...params) {
|
|
41
|
+
const row = store.handle.prepare(sql).get(...params);
|
|
42
|
+
return row === undefined ? null : { actor: String(row["actor"]), at: String(row["at"]), outcome: String(row["outcome"]) };
|
|
43
|
+
}
|
|
44
|
+
/** A project's level. With nothing recorded it is Full: a project that existed
|
|
45
|
+
* before levels keeps exactly the check it had. New projects are stamped Quick
|
|
46
|
+
* when they are added (stampNewProjectLevel). */
|
|
47
|
+
export function projectCheckLevel(store, repo) {
|
|
48
|
+
const row = newest(store, "SELECT actor, at, outcome FROM action_ledger WHERE repo = ? AND task_id IS NULL AND action = ? ORDER BY id DESC LIMIT 1", repo, PROJECT_LEVEL_ACTION);
|
|
49
|
+
if (row === null || !isCheckLevel(row.outcome))
|
|
50
|
+
return { level: "full", setBy: null, at: null };
|
|
51
|
+
return { level: row.outcome, setBy: row.actor, at: row.at };
|
|
52
|
+
}
|
|
53
|
+
/** An approver's act: set the project's level. The ledger keeps before → after. */
|
|
54
|
+
export function setProjectCheckLevel(store, repo, level, by, now) {
|
|
55
|
+
const before = projectCheckLevel(store, repo);
|
|
56
|
+
if (before.level === level && before.setBy !== null)
|
|
57
|
+
return { changed: false, before: before.level };
|
|
58
|
+
store.recordAction({ at: now.toISOString(), actor: by, repo, taskId: null, runId: null, action: PROJECT_LEVEL_ACTION, outcome: level, source: "policy",
|
|
59
|
+
detail: `${CHECK_LEVEL_WORDS[before.level]} → ${CHECK_LEVEL_WORDS[level]}` });
|
|
60
|
+
return { changed: before.level !== level, before: before.level };
|
|
61
|
+
}
|
|
62
|
+
/** A project added from now on starts on Quick. One that already has a level,
|
|
63
|
+
* or an approved check from before levels existed, is left exactly as it is. */
|
|
64
|
+
export function stampNewProjectLevel(store, repo, now) {
|
|
65
|
+
const known = newest(store, "SELECT actor, at, outcome FROM action_ledger WHERE repo = ? AND task_id IS NULL AND action = ? LIMIT 1", repo, PROJECT_LEVEL_ACTION);
|
|
66
|
+
if (known !== null)
|
|
67
|
+
return false;
|
|
68
|
+
const hadCheck = store.handle.prepare("SELECT 1 FROM verify_command WHERE repo = ? LIMIT 1").get(repo) !== undefined;
|
|
69
|
+
if (hadCheck)
|
|
70
|
+
return false;
|
|
71
|
+
store.recordAction({ at: now.toISOString(), actor: "system", repo, taskId: null, runId: null, action: PROJECT_LEVEL_ACTION, outcome: "quick", source: "policy",
|
|
72
|
+
detail: "New project → Quick" });
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
/** A task's own choice, if it made one; a revision inherits the task it revises. */
|
|
76
|
+
export function taskCheckLevel(store, taskId) {
|
|
77
|
+
let id = taskId;
|
|
78
|
+
for (let depth = 0; id !== null && depth < 64; depth++) {
|
|
79
|
+
const ref = store.lookupRef(id);
|
|
80
|
+
const [where, params] = repoClause(ref?.repo ?? null);
|
|
81
|
+
const row = newest(store, `SELECT actor, at, outcome FROM action_ledger WHERE ${where} AND task_id = ? AND action = ? ORDER BY id DESC LIMIT 1`, ...params, id, TASK_LEVEL_ACTION);
|
|
82
|
+
if (row !== null && isCheckLevel(row.outcome))
|
|
83
|
+
return row.outcome;
|
|
84
|
+
id = ref?.revisionOf ?? null;
|
|
85
|
+
}
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
/** A filing choice: this task's checks, overriding the project. Fixed once its
|
|
89
|
+
* plan is approved, so what the approver saw is what runs. */
|
|
90
|
+
export function setTaskCheckLevel(store, taskId, level, by, now) {
|
|
91
|
+
const ref = store.lookupRef(taskId);
|
|
92
|
+
if (ref === null)
|
|
93
|
+
return { ok: false, message: `No task ${taskId}.` };
|
|
94
|
+
if (approvalOf(store.getScope(taskId)).approved) {
|
|
95
|
+
return { ok: false, message: "This task's plan is already approved, so its checks are fixed. File a new task to change them." };
|
|
96
|
+
}
|
|
97
|
+
if (taskCheckLevel(store, taskId) === level)
|
|
98
|
+
return { ok: true };
|
|
99
|
+
store.recordAction({ at: now.toISOString(), actor: by, repo: ref.repo, taskId, runId: null, action: TASK_LEVEL_ACTION, outcome: level, source: "request",
|
|
100
|
+
detail: `Checks for this task: ${CHECK_LEVEL_WORDS[level]}` });
|
|
101
|
+
return { ok: true };
|
|
102
|
+
}
|
|
103
|
+
export function effectiveCheckLevel(store, repo, taskId) {
|
|
104
|
+
const own = taskId === null ? null : taskCheckLevel(store, taskId);
|
|
105
|
+
if (own !== null)
|
|
106
|
+
return { level: own, from: "task" };
|
|
107
|
+
return { level: repo === null ? "full" : projectCheckLevel(store, repo).level, from: "project" };
|
|
108
|
+
}
|
|
109
|
+
/** The command a level runs. Quick without its own command runs the full one
|
|
110
|
+
* (and says so: the level that ran is Full). Off runs nothing. */
|
|
111
|
+
export function checkCommandFor(store, repo, level) {
|
|
112
|
+
if (level === "off")
|
|
113
|
+
return { level: "off", command: null };
|
|
114
|
+
if (level === "quick") {
|
|
115
|
+
const quick = liveQuickCommand(store, repo);
|
|
116
|
+
if (quick !== null)
|
|
117
|
+
return { level: "quick", command: quick };
|
|
118
|
+
}
|
|
119
|
+
return { level: "full", command: store.liveVerifyCommand(repo) };
|
|
120
|
+
}
|
|
121
|
+
/** The builder records which level a run used, so its result says it honestly. */
|
|
122
|
+
export function recordRunCheckLevel(store, run, level, from, now) {
|
|
123
|
+
store.recordAction({ at: now.toISOString(), actor: "system", repo: run.repo, taskId: run.taskId, runId: run.id, action: RUN_LEVEL_ACTION, outcome: level, source: "work",
|
|
124
|
+
detail: `${CHECK_LEVEL_WORDS[level]} checks (${from === "task" ? "this task's choice" : "project setting"})` });
|
|
125
|
+
}
|
|
126
|
+
/** The level a finished run's checks used; null for a run from before levels. */
|
|
127
|
+
export function runCheckLevel(store, runId) {
|
|
128
|
+
const [where, params] = repoClause(repoOfRun(store, runId));
|
|
129
|
+
const row = newest(store, `SELECT actor, at, outcome FROM action_ledger WHERE ${where} AND run_id = ? AND action = ? ORDER BY id DESC LIMIT 1`, ...params, runId, RUN_LEVEL_ACTION);
|
|
130
|
+
return row !== null && isCheckLevel(row.outcome) ? row.outcome : null;
|
|
131
|
+
}
|
|
132
|
+
/** A starting point for the quick command, from the project's own scripts.
|
|
133
|
+
* It is only a suggestion: a person approves the exact command. */
|
|
134
|
+
export function suggestQuickCommand(repo) {
|
|
135
|
+
const file = join(repo, "package.json");
|
|
136
|
+
if (!existsSync(file))
|
|
137
|
+
return null;
|
|
138
|
+
let scripts = {};
|
|
139
|
+
let deps = {};
|
|
140
|
+
try {
|
|
141
|
+
const parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
142
|
+
scripts = parsed.scripts ?? {};
|
|
143
|
+
deps = { ...(parsed.dependencies ?? {}), ...(parsed.devDependencies ?? {}) };
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
const has = (name) => typeof scripts[name] === "string";
|
|
149
|
+
const runner = existsSync(join(repo, "pnpm-lock.yaml")) ? "pnpm" : existsSync(join(repo, "yarn.lock")) ? "yarn" : "npm";
|
|
150
|
+
const run = (name) => runner === "npm" ? `npm run ${name}` : `${runner} ${name}`;
|
|
151
|
+
const exec = runner === "npm" ? "npx" : `${runner} exec`;
|
|
152
|
+
const typecheck = ["typecheck", "type-check", "check-types", "tsc"].find(has) ?? null;
|
|
153
|
+
const changed = "$(git diff --name-only HEAD~1)";
|
|
154
|
+
const tests = "vitest" in deps ? `${exec} vitest related --run ${changed}`
|
|
155
|
+
: "jest" in deps ? `${exec} jest --findRelatedTests --passWithNoTests ${changed}` : null;
|
|
156
|
+
const parts = [typecheck === null ? null : run(typecheck), tests].filter((one) => one !== null);
|
|
157
|
+
if (parts.length > 0)
|
|
158
|
+
return parts.join(" && ");
|
|
159
|
+
return has("lint") ? run("lint") : null;
|
|
160
|
+
}
|
|
161
|
+
/** "skip the tests", "run the full checks": a filing request in chat, as a level. */
|
|
162
|
+
export function checkLevelFromWords(text) {
|
|
163
|
+
const said = text.toLowerCase().replace(/[’']/g, "'");
|
|
164
|
+
if (/\b(skip|without|no|don't run|do not run|turn off|switch off)\b[^.!?\n]{0,20}\b(the )?(tests|checks)\b/.test(said) || /\b(tests|checks) off\b/.test(said))
|
|
165
|
+
return "off";
|
|
166
|
+
if (/\b(full|all the|every|complete)\s+(test|check)s?\b/.test(said))
|
|
167
|
+
return "full";
|
|
168
|
+
if (/\bquick\s+(test|check)s?\b/.test(said))
|
|
169
|
+
return "quick";
|
|
170
|
+
return null;
|
|
171
|
+
}
|
package/dist/claim.d.ts
CHANGED
|
@@ -411,6 +411,28 @@ export declare function finalizeInterruptedFenced(store: Store, args: {
|
|
|
411
411
|
committed?: boolean;
|
|
412
412
|
now: Date;
|
|
413
413
|
}): InterruptSeal;
|
|
414
|
+
/**
|
|
415
|
+
* Hand an attempt back because the SERVICE stopped (its builder is
|
|
416
|
+
* stopping or restarting), not because anyone stopped the task: one fenced
|
|
417
|
+
* transaction releases the claim as `interrupted`, ends the run (and every
|
|
418
|
+
* open run it owns) as `failed` / `interrupted` — the words dead-runner
|
|
419
|
+
* recovery writes, so the next attempt inherits the preserved work in the
|
|
420
|
+
* same checkout exactly as after a crash — and returns the task to the
|
|
421
|
+
* queue with no strike, no backoff and no hold, so the next builder pass
|
|
422
|
+
* resumes it. A lease the world moved past still ends the run, touching no
|
|
423
|
+
* task state.
|
|
424
|
+
*/
|
|
425
|
+
export declare function finalizeHandBackFenced(store: Store, args: {
|
|
426
|
+
leaseId: string;
|
|
427
|
+
runId: number;
|
|
428
|
+
taskId: string;
|
|
429
|
+
message: string;
|
|
430
|
+
now: Date;
|
|
431
|
+
}): {
|
|
432
|
+
ok: true;
|
|
433
|
+
requeued: boolean;
|
|
434
|
+
fenced: boolean;
|
|
435
|
+
};
|
|
414
436
|
/**
|
|
415
437
|
* THE STOP FENCE at settlement (v52): when a stop applies to the run —
|
|
416
438
|
* its own or an owning ancestor's — seal the attempt as interrupted and
|
package/dist/claim.js
CHANGED
|
@@ -1003,6 +1003,49 @@ export function finalizeInterruptedFenced(store, args) {
|
|
|
1003
1003
|
return { ok: true, stopRun, requeued: Number(requeued.changes) > 0, fenced: false };
|
|
1004
1004
|
});
|
|
1005
1005
|
}
|
|
1006
|
+
/**
|
|
1007
|
+
* Hand an attempt back because the SERVICE stopped (its builder is
|
|
1008
|
+
* stopping or restarting), not because anyone stopped the task: one fenced
|
|
1009
|
+
* transaction releases the claim as `interrupted`, ends the run (and every
|
|
1010
|
+
* open run it owns) as `failed` / `interrupted` — the words dead-runner
|
|
1011
|
+
* recovery writes, so the next attempt inherits the preserved work in the
|
|
1012
|
+
* same checkout exactly as after a crash — and returns the task to the
|
|
1013
|
+
* queue with no strike, no backoff and no hold, so the next builder pass
|
|
1014
|
+
* resumes it. A lease the world moved past still ends the run, touching no
|
|
1015
|
+
* task state.
|
|
1016
|
+
*/
|
|
1017
|
+
export function finalizeHandBackFenced(store, args) {
|
|
1018
|
+
const { leaseId, runId, taskId, now } = args;
|
|
1019
|
+
const db = store.handle;
|
|
1020
|
+
return inTransaction(store, () => {
|
|
1021
|
+
const run = store.getRun(runId);
|
|
1022
|
+
if (run === null || run.leaseId !== leaseId || run.outcome !== null) {
|
|
1023
|
+
throw new Error(`run ${runId} is not ${leaseId}'s open attempt — a hand-back seals exactly one`);
|
|
1024
|
+
}
|
|
1025
|
+
const { changes } = db
|
|
1026
|
+
.prepare(`UPDATE claim SET released_at = ?, released_by = 'interrupted'
|
|
1027
|
+
WHERE lease_id = ? AND released_at IS NULL AND ${NOT_SUPERSEDED}`)
|
|
1028
|
+
.run(now.toISOString(), leaseId);
|
|
1029
|
+
const fenced = Number(changes) === 0;
|
|
1030
|
+
if (args.message.trim() !== "")
|
|
1031
|
+
store.recordOutcomeFacts(runId, { handoff: args.message });
|
|
1032
|
+
for (const owned of store.ownedRunsOf(runId)) {
|
|
1033
|
+
if (owned === runId)
|
|
1034
|
+
continue;
|
|
1035
|
+
const child = store.getRun(owned);
|
|
1036
|
+
if (child !== null && child.outcome === null)
|
|
1037
|
+
store.finishRun(owned, { outcome: "failed", reason: "interrupted", now });
|
|
1038
|
+
}
|
|
1039
|
+
store.finishRun(runId, { outcome: "failed", reason: "interrupted", now });
|
|
1040
|
+
if (fenced)
|
|
1041
|
+
return { ok: true, requeued: false, fenced: true };
|
|
1042
|
+
const requeued = db
|
|
1043
|
+
.prepare("UPDATE task SET state = 'queued', updated_at = ? WHERE id = ? AND state = 'running'")
|
|
1044
|
+
.run(now.toISOString(), taskId);
|
|
1045
|
+
store.bumpWake();
|
|
1046
|
+
return { ok: true, requeued: Number(requeued.changes) > 0, fenced: false };
|
|
1047
|
+
});
|
|
1048
|
+
}
|
|
1006
1049
|
/**
|
|
1007
1050
|
* THE STOP FENCE at settlement (v52): when a stop applies to the run —
|
|
1008
1051
|
* its own or an owning ancestor's — seal the attempt as interrupted and
|
package/dist/cli.d.ts
CHANGED
|
@@ -39,7 +39,7 @@ export type ParseResult = {
|
|
|
39
39
|
error: string;
|
|
40
40
|
};
|
|
41
41
|
type Write = (line: string) => void;
|
|
42
|
-
export declare const HELP = "toolroll \u2014 a control plane for unattended coding agents\n\nUsage\n toolroll [path...] report what is in flight\n toolroll pulls report what is waiting on a person\n toolroll graph report which work graph is already here\n toolroll repos list connected repositories, and how to adjust\n toolroll repos add <path> connect one (no path: the repo you are in)\n toolroll repos remove <path>\n toolroll repos add-from-github <owner/name> --root <dir>\n preview, then clone and connect (--yes)\n toolroll link put `toolroll` on your PATH\n toolroll unlink take it off again\n toolroll update update to the latest release: verified, drained, undoable\n (preview first; --yes, --now, --at HH:MM, --rollback)\n toolroll contract the machine contract: envelope version + capabilities\n (--commands dumps the declared command guide)\n toolroll skills install --claude-code [--dir <path>]\n install this binary's guides for Claude Code (preview first)\n toolroll skills install teach a repo's agents this queue exists (preview first)\n toolroll skills list the guides this exact binary serves\n toolroll skills get <name> print one guide (version-matched, never stale)\n toolroll demo a seeded throwaway sandbox \u2014 see it working in 90 seconds\n toolroll up app + builder for every saved project \u2014 the normal start\n toolroll onboard make your agent Toolroll's lead: add this repo, install its skill (--yes), offer starter flows (--starter)\n toolroll status running, queued, ready results, release check and plan windows\n toolroll integrations which integrations work, and what to do about the ones that don't (--json)\n toolroll session native coding sessions through the running service\n toolroll connect save a private connection to your central service\n toolroll lead named leads on the connected service\n toolroll conversation shared and private conversations on that service\n toolroll chat --lead <id> --conversation <id> central chat (use --local for local chat)\n\nOperating the queue \u2014 `toolroll task` prints the whole surface,\nand any queue command + --help prints it too\n toolroll approver add <name>\n mint the credential that lets a person say yes\n toolroll ready what could be dispatched right now\n toolroll check-progress <run> current or final approved check progress\n toolroll task wait <id> [--timeout <seconds>]\n wait for this attempt to finish or need a person\n toolroll task add <title> queue work\n toolroll task scope <id> --goal <text>\n state what success is; approval binds to it\n toolroll task approve <id>\n the yes \u2014 nothing builds without one\n toolroll claim <id> --runner <name>\n toolroll heartbeat <lease> / release <lease> / reap\n toolroll tick --runner <name> --token <t> --repo <path>\n one unattended pass over the ready set\n toolroll flows list | show <id> | create | edit | trigger | script | card | archive\n set up and inspect flows (writes preview until --yes)\n toolroll chat --as <you> --token <t>\n talk to the mate: one conversation across every\n project; it proposes, you confirm (--say \"\u2026\" for one turn)\n toolroll serve --repo <path> advanced: run only the console\n (--editor vscode with --runner: file links\n open in VS Code on the device you browse from)\n toolroll watch / daemon install advanced: run only the builder\n toolroll reconcile recover what the last stretch left behind\n\nWith nothing connected it reports everything it can find below the working\ndirectory. Once you connect repositories it reports those instead.\n\nOptions\n --all report everything discoverable, ignoring connected repos\n --depth <n> directory levels to descend (default: 6)\n --hidden include dot-directories, which are skipped by default\n --dirty also read each working tree for uncommitted files\n --local branches only: no network, no pull requests, no issues\n --json emit a machine-readable envelope instead of a report\n -o <file> also write that envelope to a file (requires --json)\n -h, --help show this\n\nNothing is written, installed, or configured. Every repository is read\nthrough git with --no-optional-locks, so a scan never contends for the\nindex lock with an editor you have open.\n\n--dirty is off by default because reading a working tree is unbounded\nwork: on a large or cloud-synced repo `git status` can take minutes,\nwhere listing branches takes milliseconds.\n\nlink shows what it would do and changes nothing; add --yes to apply it,\nor --to <dir> to choose where. It never uses sudo, never leaves your\nhome directory, and never touches a file it did not create.";
|
|
42
|
+
export declare const HELP = "toolroll \u2014 a control plane for unattended coding agents\n\nUsage\n toolroll [path...] report what is in flight\n toolroll pulls report what is waiting on a person\n toolroll graph report which work graph is already here\n toolroll repos list connected repositories, and how to adjust\n toolroll repos add <path> connect one (no path: the repo you are in)\n toolroll repos remove <path>\n toolroll repos add-from-github <owner/name> --root <dir>\n preview, then clone and connect (--yes)\n toolroll link put `toolroll` on your PATH\n toolroll unlink take it off again\n toolroll update update to the latest release: verified, drained, undoable\n (preview first; --yes, --now, --at HH:MM, --rollback)\n toolroll contract the machine contract: envelope version + capabilities\n (--commands dumps the declared command guide)\n toolroll skills install --claude-code [--dir <path>]\n install this binary's guides for Claude Code (preview first)\n toolroll skills install teach a repo's agents this queue exists (preview first)\n toolroll skills list the guides this exact binary serves\n toolroll skills get <name> print one guide (version-matched, never stale)\n toolroll demo a seeded throwaway sandbox \u2014 see it working in 90 seconds\n toolroll up app + builder for every saved project \u2014 the normal start\n toolroll onboard make your agent Toolroll's lead: add this repo, install its skill (--yes), offer starter flows (--starter)\n toolroll status running, queued, ready results, release check and plan windows\n toolroll integrations which integrations work, and what to do about the ones that don't (--json)\n toolroll session native coding sessions through the running service\n toolroll connect save a private connection to your central service\n toolroll lead named leads on the connected service\n toolroll lead token mint your lead agent's credential: its own work pings nobody\n toolroll conversation shared and private conversations on that service\n toolroll chat --lead <id> --conversation <id> central chat (use --local for local chat)\n\nOperating the queue \u2014 `toolroll task` prints the whole surface,\nand any queue command + --help prints it too\n toolroll approver add <name>\n mint the credential that lets a person say yes\n toolroll ready what could be dispatched right now\n toolroll check-progress <run> current or final approved check progress\n toolroll task wait <id> [--timeout <seconds>]\n wait for this attempt to finish or need a person\n toolroll task add <title> queue work\n toolroll task scope <id> --goal <text>\n state what success is; approval binds to it\n toolroll task approve <id>\n the yes \u2014 nothing builds without one\n toolroll claim <id> --runner <name>\n toolroll heartbeat <lease> / release <lease> / reap\n toolroll tick --runner <name> --token <t> --repo <path>\n one unattended pass over the ready set\n toolroll flows list | show <id> | create | edit | trigger | script | card | archive\n set up and inspect flows (writes preview until --yes)\n toolroll chat --as <you> --token <t>\n talk to the mate: one conversation across every\n project; it proposes, you confirm (--say \"\u2026\" for one turn)\n toolroll serve --repo <path> advanced: run only the console\n (--editor vscode with --runner: file links\n open in VS Code on the device you browse from)\n toolroll watch / daemon install advanced: run only the builder\n toolroll reconcile recover what the last stretch left behind\n\nWith nothing connected it reports everything it can find below the working\ndirectory. Once you connect repositories it reports those instead.\n\nOptions\n --all report everything discoverable, ignoring connected repos\n --depth <n> directory levels to descend (default: 6)\n --hidden include dot-directories, which are skipped by default\n --dirty also read each working tree for uncommitted files\n --local branches only: no network, no pull requests, no issues\n --json emit a machine-readable envelope instead of a report\n -o <file> also write that envelope to a file (requires --json)\n -h, --help show this\n\nNothing is written, installed, or configured. Every repository is read\nthrough git with --no-optional-locks, so a scan never contends for the\nindex lock with an editor you have open.\n\n--dirty is off by default because reading a working tree is unbounded\nwork: on a large or cloud-synced repo `git status` can take minutes,\nwhere listing branches takes milliseconds.\n\nlink shows what it would do and changes nothing; add --yes to apply it,\nor --to <dir> to choose where. It never uses sudo, never leaves your\nhome directory, and never touches a file it did not create.";
|
|
43
43
|
export declare function parseArgs(argv: readonly string[]): ParseResult;
|
|
44
44
|
/** The commands that operate the queue rather than report on the world. */
|
|
45
45
|
/** Every verb the CLI routes OUTSIDE runOperate. Exported (with
|
package/dist/cli.js
CHANGED
|
@@ -65,6 +65,7 @@ Usage
|
|
|
65
65
|
toolroll session native coding sessions through the running service
|
|
66
66
|
toolroll connect save a private connection to your central service
|
|
67
67
|
toolroll lead named leads on the connected service
|
|
68
|
+
toolroll lead token mint your lead agent's credential: its own work pings nobody
|
|
68
69
|
toolroll conversation shared and private conversations on that service
|
|
69
70
|
toolroll chat --lead <id> --conversation <id> central chat (use --local for local chat)
|
|
70
71
|
|
|
@@ -192,6 +193,8 @@ export const TOP_LEVEL_COMMANDS = [
|
|
|
192
193
|
];
|
|
193
194
|
export const OPERATE_COMMANDS = new Set([
|
|
194
195
|
"up",
|
|
196
|
+
// Only `lead token` reaches here; the central service's lead verbs answer first.
|
|
197
|
+
"lead",
|
|
195
198
|
"onboard",
|
|
196
199
|
"status",
|
|
197
200
|
"ready",
|
|
@@ -1275,6 +1278,7 @@ async function runDemoCommand(argv, write) {
|
|
|
1275
1278
|
write(` sandbox ${sandbox}`);
|
|
1276
1279
|
write("");
|
|
1277
1280
|
write("Open Chat and ask for a change, like \"fix the flaky refund test\".");
|
|
1281
|
+
write("For your own project, run `npx toolroll up` in its folder.");
|
|
1278
1282
|
write("");
|
|
1279
1283
|
write(keep
|
|
1280
1284
|
? "Ctrl-C stops the server; the sandbox is KEPT. It stays a demo forever — every spending command refuses it."
|
package/dist/demo-chat.d.ts
CHANGED
|
@@ -1,8 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Chat in `toolroll demo`: the scripted lead's conversation (src/demo.ts),
|
|
3
|
-
* rendered as plain server HTML with native forms. One small script keeps a
|
|
4
|
-
* running build's progress live; without it the page still works by reload.
|
|
5
|
-
*/
|
|
6
1
|
import type { DemoExchange } from "./demo.js";
|
|
7
2
|
/** What a finished exchange's stored result holds, read back from its evidence. */
|
|
8
3
|
export type DemoResultView = {
|
package/dist/demo-chat.js
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chat in `toolroll demo`: the scripted lead's conversation (src/demo.ts),
|
|
3
|
+
* rendered as plain server HTML with native forms. One small script keeps a
|
|
4
|
+
* running build's progress live; without it the page still works by reload.
|
|
5
|
+
*/
|
|
6
|
+
import { START_COMMAND } from "./first-run.js";
|
|
7
|
+
import { headlineOf } from "./task-status.js";
|
|
1
8
|
const SUGGESTIONS = ["Fix the flaky refund test", "Rewrite the empty Payouts page", "Put the new invoice view behind a flag"];
|
|
2
9
|
const esc = (text) => text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
3
10
|
const hidden = (csrf) => `<input type="hidden" name="csrf" value="${esc(csrf)}">`;
|
|
@@ -52,9 +59,11 @@ function diffHtml(diff) {
|
|
|
52
59
|
function resultCard(exchange, csrf, result) {
|
|
53
60
|
if (result === null)
|
|
54
61
|
return `<section class="card demo-result"><p class="meta">This result's saved evidence is unavailable.</p></section>`;
|
|
55
|
-
const status = exchange.state === "complete" ? `<span class="badge demo-complete">Complete</span>`
|
|
56
|
-
: exchange.state === "sent-back" ? `<span class="badge">Sent back</span>` : `<span class="badge demo-ready">Ready</span>`;
|
|
57
62
|
const passed = result.checks.status === "passed";
|
|
63
|
+
// The shared headline (task-status.ts): the same words as every real task.
|
|
64
|
+
const headline = headlineOf({ stage: exchange.state === "complete" ? "complete" : exchange.state === "sent-back" ? "building" : "finished",
|
|
65
|
+
checks: { status: passed ? "passed" : "failed", exitCode: null, head: null } });
|
|
66
|
+
const status = `<span class="badge${headline === "Complete" ? " demo-complete" : headline === "Ready for review" ? " demo-ready" : ""}" data-headline="${headline}">${headline}</span>`;
|
|
58
67
|
const summary = `${result.files} file${result.files === 1 ? "" : "s"} changed · +${result.additions} −${result.deletions} · ` +
|
|
59
68
|
`<span class="${passed ? "demo-pass" : "demo-fail"}">${esc(result.checks.detail)}</span>`;
|
|
60
69
|
const actions = exchange.state === "ready"
|
|
@@ -92,7 +101,9 @@ export function demoThreadHtml(exchanges, csrf, resultOf) {
|
|
|
92
101
|
if (exchange.state === "ready" || exchange.state === "complete" || exchange.state === "sent-back")
|
|
93
102
|
parts.push(resultCard(exchange, csrf, resultOf(exchange)));
|
|
94
103
|
if (exchange.state === "complete") {
|
|
95
|
-
parts.push(`<div class="demo-said demo-lead"><p class="demo-who">Lead</p><p>Done. That's the whole loop: ask, approve, Ready, Complete. Ask for something else whenever you like.</p></div>`
|
|
104
|
+
parts.push(`<div class="demo-said demo-lead"><p class="demo-who">Lead</p><p>Done. That's the whole loop: ask, approve, Ready, Complete. Ask for something else whenever you like.</p></div>`, `<section class="card demo-handoff" data-demo-handoff aria-label="Your own project"><h2>Now try it on your own project</h2>` +
|
|
105
|
+
`<p>In your repository's folder, run:</p><pre class="demo-command"><code>${esc(START_COMMAND)}</code></pre>` +
|
|
106
|
+
`<p class="meta">It opens in your browser, already signed in.</p></section>`);
|
|
96
107
|
}
|
|
97
108
|
return `<article class="demo-turn" id="demo-${exchange.id}">${parts.join("")}</article>`;
|
|
98
109
|
}).join("");
|
package/dist/dispatch.js
CHANGED
|
@@ -12,6 +12,7 @@ import { approvalOf } from "./scope.js";
|
|
|
12
12
|
import { plannerSourceProblemOf } from "./planner-source.js";
|
|
13
13
|
import { authPauseOf, authWaitOf, providerName, signInReason } from "./provider-auth.js";
|
|
14
14
|
import { REVIEW_TOKENS, resultStatusOf, reviewFactsOf } from "./workspace-ui.js";
|
|
15
|
+
import { replacedWords } from "./task-status.js";
|
|
15
16
|
import { BUILT_IN, parseCapabilityKey } from "./store.js";
|
|
16
17
|
export const DEFAULT_MAX_OPEN_DECISIONS = 5;
|
|
17
18
|
/**
|
|
@@ -210,8 +211,12 @@ export function diagnoseTaskDispatch(store, taskId, now) {
|
|
|
210
211
|
? `${status.detail} Its independent review succeeded on attempt ${review.attempt} of ${review.cap}.`
|
|
211
212
|
: status.detail, { action: "open-result", review });
|
|
212
213
|
}
|
|
213
|
-
if (task.state === "cancelled")
|
|
214
|
-
|
|
214
|
+
if (task.state === "cancelled") {
|
|
215
|
+
// Replaced, never "Cancelled": a successor carries the work on.
|
|
216
|
+
const successor = store.replacementOf(taskId);
|
|
217
|
+
return successor === null ? answer("cancelled", "terminal", "Cancelled", "Nothing else will run for this task.")
|
|
218
|
+
: answer("cancelled", "terminal", replacedWords(successor), `${replacedWords(successor)}. Nothing else will run for this task.`);
|
|
219
|
+
}
|
|
215
220
|
if (task.state === "failed")
|
|
216
221
|
return answer("failed", "terminal", "Needs a retry", "The last attempt stopped; review its incident, then retry it.", { action: "retry-task" });
|
|
217
222
|
if (store.hasLiveClaim(ref.id, now))
|
package/dist/dispose.d.ts
CHANGED
|
@@ -73,6 +73,14 @@ export type Disposition = {
|
|
|
73
73
|
kind: "stopped";
|
|
74
74
|
stopRun: number;
|
|
75
75
|
requeued: boolean;
|
|
76
|
+
}
|
|
77
|
+
/** The service stopped or restarted while the attempt ran (no one stopped
|
|
78
|
+
* the task): its work is preserved, the run ends as interrupted, and the
|
|
79
|
+
* task is back in the queue — no strike, no backoff — so the next builder
|
|
80
|
+
* pass resumes it from the kept work. */
|
|
81
|
+
| {
|
|
82
|
+
kind: "handed-back";
|
|
83
|
+
requeued: boolean;
|
|
76
84
|
} | {
|
|
77
85
|
kind: "invariant";
|
|
78
86
|
reason: string;
|
package/dist/dispose.js
CHANGED
|
@@ -14,7 +14,7 @@ import { originalTaskBase, focusedTestCommandSupported } from "./observations.js
|
|
|
14
14
|
* inlined before.
|
|
15
15
|
*/
|
|
16
16
|
import { createHash } from "node:crypto";
|
|
17
|
-
import { completeFenced, finalizeFailureFenced, finalizeMalformedFenced, finalizeParkFenced, interruptIfStopped, release, } from "./claim.js";
|
|
17
|
+
import { completeFenced, finalizeFailureFenced, finalizeHandBackFenced, finalizeMalformedFenced, finalizeParkFenced, interruptIfStopped, release, } from "./claim.js";
|
|
18
18
|
import { bodyHashOf, publicationBody } from "./publish.js";
|
|
19
19
|
import { modeTermsFromJson } from "./modes.js";
|
|
20
20
|
import { readVerifiedArtifact, writeEvidenceFile } from "./evidence.js";
|
|
@@ -338,6 +338,12 @@ function disposeBuildOutcomeLocked(context, result) {
|
|
|
338
338
|
});
|
|
339
339
|
return { kind: "malformed", sealed: sealed.ok };
|
|
340
340
|
}
|
|
341
|
+
// A stop no operator asked for is the SERVICE stopping (the fence above
|
|
342
|
+
// already settled every task stop): hand the attempt back, never strike it.
|
|
343
|
+
if (result.reason === "stopped" && policy === "tick" && leaseId !== undefined && store.getRun(runId)?.role === "builder") {
|
|
344
|
+
const handed = finalizeHandBackFenced(store, { leaseId, runId, taskId, message: result.message, now: clock() });
|
|
345
|
+
return { kind: "handed-back", requeued: handed.requeued };
|
|
346
|
+
}
|
|
341
347
|
if (TICK_FAILURE_REASONS.has(result.reason)) {
|
|
342
348
|
// One fenced transaction decides what the failure means. The
|
|
343
349
|
// classification trusts only what the machine itself observed.
|
package/dist/export.js
CHANGED
|
@@ -33,7 +33,7 @@ const SECRET_TABLES = new Set(["web_session", "ceremony_nonce", "telegram_pairin
|
|
|
33
33
|
const DERIVED_TABLE = /^(sqlite_|memory_search)/;
|
|
34
34
|
/** Columns that hold a secret (or what stands in for one): never exported. */
|
|
35
35
|
const SECRET_COLUMNS = new Set([
|
|
36
|
-
"api_token.secret_hash", "approver.credential_hash", "coordinator_credential.credential_hash", "runner.credential_hash", "invite.token_hash",
|
|
36
|
+
"api_token.secret_hash", "lead_credential.secret_hash", "approver.credential_hash", "coordinator_credential.credential_hash", "runner.credential_hash", "invite.token_hash",
|
|
37
37
|
"flow_trigger.hook_hash", "held_session.cookie", "push_subscription.endpoint", "push_subscription.p256dh", "push_subscription.auth",
|
|
38
38
|
"chat_turn.credential_key", "mate_session.credential_key", "mate_turn.credential_key", "bridge_lease.push_url", "workflow_preview.token",
|
|
39
39
|
"quota.credential_fp",
|
|
@@ -56,7 +56,7 @@ export function categoryOf(table) {
|
|
|
56
56
|
[/^teammate/, "teammates"],
|
|
57
57
|
[/^(flow|routine|workflow_)/, "flows"],
|
|
58
58
|
[/^(chat_|mate_|team_|telegram_|slack_|discord_|teams_|notification|push_|bridge_lease)/, "chats"],
|
|
59
|
-
[/^(approver|api_token|invite|sso_identity|coordinator_)/, "people"],
|
|
59
|
+
[/^(approver|api_token|lead_credential|invite|sso_identity|coordinator_)/, "people"],
|
|
60
60
|
[/^(run|artifact|claim|worktree$|execution_slot|contest|tournament_terms|fallback_cycle|fallback_transition|held_session|session_turn|attended_authorization|criterion_review|diff_comment|proof_|review_request|repair_chain|incident|publication$|merge_|side_spend|knowledge_snapshot|learning_snapshot|skill_snapshot)/, "runs"],
|
|
61
61
|
[/^(task|hold$|plan_|scope_|tool_seal|decision|external_|skill_test)/, "tasks"],
|
|
62
62
|
[/^(project|approval_policy|capability|verify_command|worktree_setup|backend_grant|intake_grant|publication_grant|operating_mode|knowledge_|learning_|memory_|skill_)/, "projects"],
|
|
@@ -67,6 +67,7 @@ export function categoryOf(table) {
|
|
|
67
67
|
/** Extra shapes beside the evidence scanner's: this installation's own tokens and password hashes, and bearer headers. */
|
|
68
68
|
const EXTRA_SHAPES = [
|
|
69
69
|
/\bso_[a-f0-9]{12}_[A-Za-z0-9_-]{43}/g,
|
|
70
|
+
/\blt_[a-f0-9]{12}_[A-Za-z0-9_-]{43}/g,
|
|
70
71
|
/\bscrypt\$[^\s"',;]+/g,
|
|
71
72
|
/\bBearer\s+[A-Za-z0-9._~+/-]{16,}=*/gi,
|
|
72
73
|
/\bsk-[A-Za-z0-9_-]{20,}/g,
|
package/dist/first-run.d.ts
CHANGED
|
@@ -1,6 +1,20 @@
|
|
|
1
1
|
import type { Runner } from "./backend.js";
|
|
2
2
|
export declare const SIGN_IN_COMMAND = "claude auth login";
|
|
3
3
|
export declare const SANDBOX_COMMAND = "npx toolroll demo";
|
|
4
|
+
/** How a person starts on a real project (the demo hands off to this). */
|
|
5
|
+
export declare const START_COMMAND = "npx toolroll up";
|
|
6
|
+
/** How it works, once, above the composer until the first request. */
|
|
7
|
+
export declare const HOW_IT_WORKS = "Ask for a change. The lead writes a short plan; you approve it; an agent builds it on its own branch; it's Ready when your tests pass.";
|
|
8
|
+
type SignInState = "connected" | "signed-out" | "not-installed" | "unverified" | "key-present" | "key-works" | "key-refused" | "missing-key";
|
|
9
|
+
/**
|
|
10
|
+
* The one command that gets an agent signed in on this computer: sign in to the CLI that's installed (Claude Code
|
|
11
|
+
* first, then Codex), or install Claude Code and sign in, written for this operating system's shell. Unknown states
|
|
12
|
+
* (not checked yet) get the install-and-sign-in command, which is right on a fresh machine and harmless otherwise.
|
|
13
|
+
*/
|
|
14
|
+
export declare function signInCommandFor(states: {
|
|
15
|
+
claude: SignInState;
|
|
16
|
+
codex: SignInState;
|
|
17
|
+
} | null, platform: NodeJS.Platform): string;
|
|
4
18
|
export type FirstRunAction = {
|
|
5
19
|
kind: "link";
|
|
6
20
|
label: string;
|
|
@@ -23,6 +37,7 @@ export type FirstRunFacts = {
|
|
|
23
37
|
projects: number;
|
|
24
38
|
hasTask: boolean;
|
|
25
39
|
firstResultAt: string | null;
|
|
40
|
+
signInCommand?: string;
|
|
26
41
|
};
|
|
27
42
|
/** The three steps, each done or with the one action that does it; null once the first result has arrived. */
|
|
28
43
|
export declare function firstRunSteps(facts: FirstRunFacts): FirstRunStep[] | null;
|
|
@@ -58,3 +73,22 @@ export declare const TODO_GREP_ARGS: readonly string[];
|
|
|
58
73
|
export declare function findFirstTasks(repo: string, runner?: Runner): Promise<FirstTaskSuggestion[]>;
|
|
59
74
|
/** "First result in 7 min": how long after the installation began its first Ready result arrived. */
|
|
60
75
|
export declare function firstResultWords(since: string, at: string): string | null;
|
|
76
|
+
/** The first task's five steps, as a person reads them. */
|
|
77
|
+
export declare const JOURNEY_STEPS: readonly [readonly ["plan", "Plan"], readonly ["approve", "You approve"], readonly ["build", "Build"], readonly ["checks", "Checks"], readonly ["ready", "Ready"]];
|
|
78
|
+
export type JourneyKey = (typeof JOURNEY_STEPS)[number][0];
|
|
79
|
+
/** `stuck`: the step it stopped at (Failed, Stopped, or waiting on something other than the approval). */
|
|
80
|
+
export type JourneyStep = {
|
|
81
|
+
key: JourneyKey;
|
|
82
|
+
label: string;
|
|
83
|
+
state: "done" | "current" | "stuck" | "todo";
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
86
|
+
* Where the first task stands on Plan → You approve → Build → Checks → Ready, from the one stage every surface reads
|
|
87
|
+
* (task-status.ts). Waiting for a worker before approval is still the plan; after approval it is the build. `built`:
|
|
88
|
+
* the build finished with a result.
|
|
89
|
+
*/
|
|
90
|
+
export declare function firstTaskJourney(read: {
|
|
91
|
+
stage: "queued" | "planning" | "needs-you" | "building" | "checking" | "finished" | "complete" | "failed" | "stopped";
|
|
92
|
+
need?: string | undefined;
|
|
93
|
+
}, approved: boolean, built?: boolean): JourneyStep[];
|
|
94
|
+
export {};
|
package/dist/first-run.js
CHANGED
|
@@ -9,6 +9,23 @@ import { run } from "./exec.js";
|
|
|
9
9
|
import { githubIssues } from "./issues.js";
|
|
10
10
|
export const SIGN_IN_COMMAND = "claude auth login";
|
|
11
11
|
export const SANDBOX_COMMAND = "npx toolroll demo";
|
|
12
|
+
/** How a person starts on a real project (the demo hands off to this). */
|
|
13
|
+
export const START_COMMAND = "npx toolroll up";
|
|
14
|
+
/** How it works, once, above the composer until the first request. */
|
|
15
|
+
export const HOW_IT_WORKS = "Ask for a change. The lead writes a short plan; you approve it; an agent builds it on its own branch; it's Ready when your tests pass.";
|
|
16
|
+
/**
|
|
17
|
+
* The one command that gets an agent signed in on this computer: sign in to the CLI that's installed (Claude Code
|
|
18
|
+
* first, then Codex), or install Claude Code and sign in, written for this operating system's shell. Unknown states
|
|
19
|
+
* (not checked yet) get the install-and-sign-in command, which is right on a fresh machine and harmless otherwise.
|
|
20
|
+
*/
|
|
21
|
+
export function signInCommandFor(states, platform) {
|
|
22
|
+
const installed = (state) => state !== undefined && state !== "not-installed";
|
|
23
|
+
if (installed(states?.claude))
|
|
24
|
+
return SIGN_IN_COMMAND;
|
|
25
|
+
if (installed(states?.codex))
|
|
26
|
+
return "codex login";
|
|
27
|
+
return `npm install -g @anthropic-ai/claude-code${platform === "win32" ? ";" : " &&"} ${SIGN_IN_COMMAND}`;
|
|
28
|
+
}
|
|
12
29
|
/** The three steps, each done or with the one action that does it; null once the first result has arrived. */
|
|
13
30
|
export function firstRunSteps(facts) {
|
|
14
31
|
if (facts.firstResultAt !== null)
|
|
@@ -16,7 +33,7 @@ export function firstRunSteps(facts) {
|
|
|
16
33
|
const project = facts.projects > 0;
|
|
17
34
|
return [
|
|
18
35
|
facts.agentSignedIn === null ? { key: "agent", title: "Agent signed in", done: false, action: null, checking: true }
|
|
19
|
-
: { key: "agent", title: "Agent signed in", done: facts.agentSignedIn, action: facts.agentSignedIn ? null : { kind: "command", command: SIGN_IN_COMMAND } },
|
|
36
|
+
: { key: "agent", title: "Agent signed in", done: facts.agentSignedIn, action: facts.agentSignedIn ? null : { kind: "command", command: facts.signInCommand ?? SIGN_IN_COMMAND } },
|
|
20
37
|
{ key: "project", title: "Project added", done: project, action: project ? null : { kind: "link", label: "Add a project", href: "/projects" } },
|
|
21
38
|
{ key: "task", title: "Your first task", done: facts.hasTask, action: facts.hasTask ? null : { kind: "link", label: "New task", href: "/tasks/new" } },
|
|
22
39
|
];
|
|
@@ -103,3 +120,22 @@ export function firstResultWords(since, at) {
|
|
|
103
120
|
}
|
|
104
121
|
return `First result in ${Math.round(minutes / (24 * 60))} days`;
|
|
105
122
|
}
|
|
123
|
+
/** The first task's five steps, as a person reads them. */
|
|
124
|
+
export const JOURNEY_STEPS = [["plan", "Plan"], ["approve", "You approve"], ["build", "Build"], ["checks", "Checks"], ["ready", "Ready"]];
|
|
125
|
+
/**
|
|
126
|
+
* Where the first task stands on Plan → You approve → Build → Checks → Ready, from the one stage every surface reads
|
|
127
|
+
* (task-status.ts). Waiting for a worker before approval is still the plan; after approval it is the build. `built`:
|
|
128
|
+
* the build finished with a result.
|
|
129
|
+
*/
|
|
130
|
+
export function firstTaskJourney(read, approved, built = false) {
|
|
131
|
+
const at = read.stage === "finished" || read.stage === "complete" ? { index: 5, stuck: false }
|
|
132
|
+
: read.stage === "checking" ? { index: 3, stuck: false }
|
|
133
|
+
: read.stage === "building" ? { index: 2, stuck: false }
|
|
134
|
+
: read.stage === "needs-you" && read.need === "approval" ? { index: 1, stuck: false }
|
|
135
|
+
: read.stage === "planning" ? { index: 0, stuck: false }
|
|
136
|
+
: read.stage === "queued" ? { index: approved ? 2 : 0, stuck: false }
|
|
137
|
+
// A build that finished but isn't Ready (its checks failed or didn't run, or it needs a decision) stopped at Checks.
|
|
138
|
+
: { index: built ? 3 : approved ? 2 : 0, stuck: true };
|
|
139
|
+
return JOURNEY_STEPS.map(([key, label], index) => ({ key, label,
|
|
140
|
+
state: index < at.index ? "done" : index === at.index ? (at.stuck ? "stuck" : "current") : "todo" }));
|
|
141
|
+
}
|