toolroll 0.9.6 → 0.9.8
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 +64 -1
- package/dist/actor.d.ts +21 -0
- package/dist/actor.js +48 -0
- package/dist/assignment-adapters.d.ts +2 -0
- package/dist/assignment-adapters.js +2 -0
- package/dist/assignment-ui.js +3 -1
- package/dist/assignment.d.ts +15 -0
- package/dist/assignment.js +80 -16
- package/dist/browser/workspace.css +2 -2
- package/dist/browser/workspace.js +28 -24
- package/dist/browser-workspace.d.ts +117 -2
- package/dist/build-review.d.ts +38 -0
- package/dist/build-review.js +341 -0
- package/dist/builder.d.ts +1 -0
- package/dist/builder.js +33 -13
- package/dist/chat-actions.js +2 -1
- package/dist/chat-delivery.js +2 -0
- package/dist/chat-quiet.js +11 -1
- package/dist/chat-task-actions.d.ts +13 -0
- package/dist/chat-task-actions.js +19 -1
- package/dist/check-levels-ui.d.ts +26 -0
- package/dist/check-levels-ui.js +84 -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 +6 -0
- package/dist/demo-chat.js +4 -1
- package/dist/dispatch.js +9 -3
- package/dist/dispose.d.ts +12 -3
- package/dist/dispose.js +19 -5
- package/dist/exec.d.ts +5 -0
- package/dist/exec.js +30 -4
- 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/invoke.js +5 -0
- package/dist/lead-context.js +6 -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/modes.d.ts +3 -1
- package/dist/modes.js +3 -1
- package/dist/operate.d.ts +4 -1
- package/dist/operate.js +711 -98
- package/dist/process-custody.js +8 -0
- 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/provider.js +16 -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/retention-ui.d.ts +3 -1
- package/dist/retention-ui.js +4 -4
- package/dist/retention.d.ts +13 -6
- package/dist/retention.js +50 -12
- package/dist/review-switch.d.ts +41 -0
- package/dist/review-switch.js +88 -0
- package/dist/reviewer.d.ts +24 -2
- package/dist/reviewer.js +49 -1
- package/dist/scope.js +11 -0
- package/dist/serve.d.ts +211 -2
- package/dist/serve.js +1038 -145
- package/dist/store.d.ts +112 -4
- package/dist/store.js +447 -27
- package/dist/surface.js +36 -9
- 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 +13 -3
- package/dist/task-status.js +38 -16
- package/dist/team-cli.js +3 -0
- package/dist/telegram-status.js +2 -2
- package/dist/verification-evidence.js +21 -1
- package/dist/webhooks.js +5 -0
- package/dist/work-index.js +39 -6
- package/dist/work-summary.d.ts +0 -4
- package/dist/work-summary.js +4 -1
- package/dist/workspace-ui.d.ts +6 -1
- package/dist/workspace-ui.js +14 -11
- package/package.json +1 -1
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",
|
|
@@ -210,6 +213,7 @@ export const OPERATE_COMMANDS = new Set([
|
|
|
210
213
|
"restore",
|
|
211
214
|
"export",
|
|
212
215
|
"policy",
|
|
216
|
+
"review",
|
|
213
217
|
"assignment",
|
|
214
218
|
"knowledge",
|
|
215
219
|
"memory",
|
|
@@ -240,6 +244,7 @@ export const OPERATE_COMMANDS = new Set([
|
|
|
240
244
|
"bridge",
|
|
241
245
|
"publish",
|
|
242
246
|
"reconcile",
|
|
247
|
+
"run",
|
|
243
248
|
"routine",
|
|
244
249
|
"flows",
|
|
245
250
|
"config", "chat", "proposals", "mode", "people", "keys",
|
|
@@ -1275,6 +1280,7 @@ async function runDemoCommand(argv, write) {
|
|
|
1275
1280
|
write(` sandbox ${sandbox}`);
|
|
1276
1281
|
write("");
|
|
1277
1282
|
write("Open Chat and ask for a change, like \"fix the flaky refund test\".");
|
|
1283
|
+
write("For your own project, run `npx toolroll up` in its folder.");
|
|
1278
1284
|
write("");
|
|
1279
1285
|
write(keep
|
|
1280
1286
|
? "Ctrl-C stops the server; the sandbox is KEPT. It stays a demo forever — every spending command refuses it."
|
package/dist/demo-chat.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* rendered as plain server HTML with native forms. One small script keeps a
|
|
4
4
|
* running build's progress live; without it the page still works by reload.
|
|
5
5
|
*/
|
|
6
|
+
import { START_COMMAND } from "./first-run.js";
|
|
6
7
|
import { headlineOf } from "./task-status.js";
|
|
7
8
|
const SUGGESTIONS = ["Fix the flaky refund test", "Rewrite the empty Payouts page", "Put the new invoice view behind a flag"];
|
|
8
9
|
const esc = (text) => text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
@@ -100,7 +101,9 @@ export function demoThreadHtml(exchanges, csrf, resultOf) {
|
|
|
100
101
|
if (exchange.state === "ready" || exchange.state === "complete" || exchange.state === "sent-back")
|
|
101
102
|
parts.push(resultCard(exchange, csrf, resultOf(exchange)));
|
|
102
103
|
if (exchange.state === "complete") {
|
|
103
|
-
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>`);
|
|
104
107
|
}
|
|
105
108
|
return `<article class="demo-turn" id="demo-${exchange.id}">${parts.join("")}</article>`;
|
|
106
109
|
}).join("");
|
package/dist/dispatch.js
CHANGED
|
@@ -12,6 +12,8 @@ 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";
|
|
16
|
+
import { buildReviewOf } from "./review-switch.js";
|
|
15
17
|
import { BUILT_IN, parseCapabilityKey } from "./store.js";
|
|
16
18
|
export const DEFAULT_MAX_OPEN_DECISIONS = 5;
|
|
17
19
|
/**
|
|
@@ -186,7 +188,7 @@ export function diagnoseTaskDispatch(store, taskId, now) {
|
|
|
186
188
|
const reviewFacts = reviewFactsOf(retry, runner => {
|
|
187
189
|
const reviewer = store.getRunner(runner)?.runner;
|
|
188
190
|
return reviewer !== undefined && isAlive(reviewer, now);
|
|
189
|
-
});
|
|
191
|
+
}, result !== undefined && buildReviewOf(store, result.id)?.state === "pending");
|
|
190
192
|
// A no-change conclusion owes no proof beyond its handoff and sealed
|
|
191
193
|
// diff (the task page's own "attested floor"); with both on record it
|
|
192
194
|
// is complete, not verification-needed.
|
|
@@ -210,8 +212,12 @@ export function diagnoseTaskDispatch(store, taskId, now) {
|
|
|
210
212
|
? `${status.detail} Its independent review succeeded on attempt ${review.attempt} of ${review.cap}.`
|
|
211
213
|
: status.detail, { action: "open-result", review });
|
|
212
214
|
}
|
|
213
|
-
if (task.state === "cancelled")
|
|
214
|
-
|
|
215
|
+
if (task.state === "cancelled") {
|
|
216
|
+
// Replaced, never "Cancelled": a successor carries the work on.
|
|
217
|
+
const successor = store.replacementOf(taskId);
|
|
218
|
+
return successor === null ? answer("cancelled", "terminal", "Cancelled", "Nothing else will run for this task.")
|
|
219
|
+
: answer("cancelled", "terminal", replacedWords(successor), `${replacedWords(successor)}. Nothing else will run for this task.`);
|
|
220
|
+
}
|
|
215
221
|
if (task.state === "failed")
|
|
216
222
|
return answer("failed", "terminal", "Needs a retry", "The last attempt stopped; review its incident, then retry it.", { action: "retry-task" });
|
|
217
223
|
if (store.hasLiveClaim(ref.id, now))
|
package/dist/dispose.d.ts
CHANGED
|
@@ -73,13 +73,22 @@ 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;
|
|
79
87
|
};
|
|
80
|
-
/**
|
|
81
|
-
*
|
|
82
|
-
|
|
88
|
+
/** A committed build whose approved check passed gets the project's one
|
|
89
|
+
* automatic review when its review switch is on (build-review.ts). No-change
|
|
90
|
+
* results and failed checks go straight to the person, as before. */
|
|
91
|
+
export declare function maybeRequestAutoReview(store: Store, repo: string, runId: number, committed: boolean, noChange: boolean, now: Date, root?: string): void;
|
|
83
92
|
export declare function disposeBuildOutcome(context: DisposeContext, result: BuildResult): Disposition;
|
|
84
93
|
/**
|
|
85
94
|
* The stale-approval HOLD, shared by the post-build disposition above and
|
package/dist/dispose.js
CHANGED
|
@@ -14,10 +14,12 @@ 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
|
-
import { readVerifiedArtifact, writeEvidenceFile } from "./evidence.js";
|
|
20
|
+
import { evidenceRoot, readVerifiedArtifact, writeEvidenceFile } from "./evidence.js";
|
|
21
|
+
import { queueBuildReview } from "./build-review.js";
|
|
22
|
+
import { homedir } from "node:os";
|
|
21
23
|
import {} from "./proof.js";
|
|
22
24
|
import { verificationEvidence, failedVerificationEvidence } from "./verification-evidence.js";
|
|
23
25
|
import { classifyGateFailure, describeGateFailure, gateFailureSummary } from "./gate-failure.js";
|
|
@@ -55,9 +57,14 @@ const STANDALONE_BROKE_REASONS = new Set([
|
|
|
55
57
|
"timeout",
|
|
56
58
|
"git",
|
|
57
59
|
]);
|
|
58
|
-
/**
|
|
59
|
-
*
|
|
60
|
-
|
|
60
|
+
/** A committed build whose approved check passed gets the project's one
|
|
61
|
+
* automatic review when its review switch is on (build-review.ts). No-change
|
|
62
|
+
* results and failed checks go straight to the person, as before. */
|
|
63
|
+
export function maybeRequestAutoReview(store, repo, runId, committed, noChange, now, root = evidenceRoot(homedir())) {
|
|
64
|
+
if (!committed || noChange)
|
|
65
|
+
return;
|
|
66
|
+
queueBuildReview(store, repo, runId, root, now);
|
|
67
|
+
}
|
|
61
68
|
export function disposeBuildOutcome(context, result) {
|
|
62
69
|
// Stop and every terminal side effect compete under the same SQLite
|
|
63
70
|
// write lock. A separate preliminary transaction leaves a completion gap.
|
|
@@ -238,6 +245,7 @@ function disposeBuildOutcomeLocked(context, result) {
|
|
|
238
245
|
// proves the credential, clearing any quota stamp.
|
|
239
246
|
store.resetStrikes(taskRef);
|
|
240
247
|
store.clearQuota(runner, provider, model ?? "");
|
|
248
|
+
maybeRequestAutoReview(store, repo, runId, result.committed, result.noChange === true, clock(), context.evidenceRoot);
|
|
241
249
|
return { kind: "built", committed: result.committed, noChange: result.noChange === true };
|
|
242
250
|
}
|
|
243
251
|
store.transact(() => {
|
|
@@ -338,6 +346,12 @@ function disposeBuildOutcomeLocked(context, result) {
|
|
|
338
346
|
});
|
|
339
347
|
return { kind: "malformed", sealed: sealed.ok };
|
|
340
348
|
}
|
|
349
|
+
// A stop no operator asked for is the SERVICE stopping (the fence above
|
|
350
|
+
// already settled every task stop): hand the attempt back, never strike it.
|
|
351
|
+
if (result.reason === "stopped" && policy === "tick" && leaseId !== undefined && store.getRun(runId)?.role === "builder") {
|
|
352
|
+
const handed = finalizeHandBackFenced(store, { leaseId, runId, taskId, message: result.message, now: clock() });
|
|
353
|
+
return { kind: "handed-back", requeued: handed.requeued };
|
|
354
|
+
}
|
|
341
355
|
if (TICK_FAILURE_REASONS.has(result.reason)) {
|
|
342
356
|
// One fenced transaction decides what the failure means. The
|
|
343
357
|
// classification trusts only what the machine itself observed.
|
package/dist/exec.d.ts
CHANGED
|
@@ -113,6 +113,11 @@ export type RunOptions = ProcessTreeObserver & {
|
|
|
113
113
|
/** Fires once the child exists, with its pid (the process-group id when
|
|
114
114
|
* processGroup is set) — the slot ledger records it (v14 finding 26). */
|
|
115
115
|
onSpawn?: (pid: number) => void;
|
|
116
|
+
/** Fires instead of onSpawn when a spawn that beforeSpawn admitted made no
|
|
117
|
+
* process at all (the OS call threw — ENOENT, EAGAIN — returned no pid, or
|
|
118
|
+
* its OS object could not be made), so its reserved witness can be settled
|
|
119
|
+
* as never started in the same step. Never fires once a child exists. */
|
|
120
|
+
onSpawnFailed?: () => void;
|
|
116
121
|
/** Fires before any target spawn, so a crash cannot lose its OS object. */
|
|
117
122
|
onContainer?: (info: {
|
|
118
123
|
backend: ContainmentBackendId;
|
package/dist/exec.js
CHANGED
|
@@ -84,9 +84,24 @@ function spawnContained(file, args, spawnOptions, label, bag, contain = true) {
|
|
|
84
84
|
throw new ContainmentRefusal(effective.refusal);
|
|
85
85
|
if (bag.beforeSpawn?.() === false)
|
|
86
86
|
throw new Error("the attempt stopped before this process could spawn");
|
|
87
|
-
|
|
88
|
-
|
|
87
|
+
// From here a witness may be reserved: every road that makes no process
|
|
88
|
+
// says so through onSpawnFailed before it returns or throws.
|
|
89
|
+
const neverStarted = () => { try {
|
|
90
|
+
bag.onSpawnFailed?.();
|
|
91
|
+
}
|
|
92
|
+
catch { /* the reservation stays unproven */ } };
|
|
93
|
+
let made;
|
|
94
|
+
try {
|
|
95
|
+
made = effective === null ? { container: null } : createContainer(effective, label);
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
neverStarted();
|
|
99
|
+
throw error;
|
|
100
|
+
}
|
|
101
|
+
if ("refused" in made) {
|
|
102
|
+
neverStarted();
|
|
89
103
|
throw new ContainmentRefusal(made.refused);
|
|
104
|
+
}
|
|
90
105
|
const container = made.container;
|
|
91
106
|
try {
|
|
92
107
|
if (container !== null)
|
|
@@ -94,6 +109,7 @@ function spawnContained(file, args, spawnOptions, label, bag, contain = true) {
|
|
|
94
109
|
}
|
|
95
110
|
catch (error) {
|
|
96
111
|
container?.release();
|
|
112
|
+
neverStarted();
|
|
97
113
|
throw error;
|
|
98
114
|
}
|
|
99
115
|
let launch;
|
|
@@ -102,6 +118,7 @@ function spawnContained(file, args, spawnOptions, label, bag, contain = true) {
|
|
|
102
118
|
}
|
|
103
119
|
catch (error) {
|
|
104
120
|
container?.release();
|
|
121
|
+
neverStarted();
|
|
105
122
|
throw error;
|
|
106
123
|
}
|
|
107
124
|
let child;
|
|
@@ -123,6 +140,7 @@ function spawnContained(file, args, spawnOptions, label, bag, contain = true) {
|
|
|
123
140
|
}
|
|
124
141
|
catch { }
|
|
125
142
|
container?.release();
|
|
143
|
+
neverStarted();
|
|
126
144
|
throw error;
|
|
127
145
|
}
|
|
128
146
|
if (container !== null) {
|
|
@@ -143,6 +161,12 @@ function spawnContained(file, args, spawnOptions, label, bag, contain = true) {
|
|
|
143
161
|
if (child.pid !== undefined) {
|
|
144
162
|
bag.onSpawn?.(child.pid);
|
|
145
163
|
}
|
|
164
|
+
else if (container === null) {
|
|
165
|
+
// No pid: the OS refused the spawn (ENOENT, EACCES) and only an
|
|
166
|
+
// 'error' event follows. A contained spawn settles once its object is
|
|
167
|
+
// proven empty, and its transport's return finishes the witness.
|
|
168
|
+
neverStarted();
|
|
169
|
+
}
|
|
146
170
|
}
|
|
147
171
|
catch (error) {
|
|
148
172
|
throw new SpawnCustodyFailure(child, error);
|
|
@@ -201,7 +225,7 @@ function containmentOf(container, empty, attachFailure) {
|
|
|
201
225
|
}
|
|
202
226
|
/** The streaming transports' spawn: contained when the spawn is a provider (process group), plain otherwise. */
|
|
203
227
|
function spawnStream(file, args, options, stdio, childEnv) {
|
|
204
|
-
return spawnContained(file, args, { cwd: options.cwd, env: childEnv, stdio, detached: options.processGroup === true && process.platform !== "win32" }, options.owner ?? "stream", { beforeSpawn: options.beforeSpawn, onSpawn: options.onSpawn, onContainer: options.onContainer, onContainerEmpty: options.onContainerEmpty, onUnknown: options.onUnknown, fence: options.fence }, options.processGroup === true);
|
|
228
|
+
return spawnContained(file, args, { cwd: options.cwd, env: childEnv, stdio, detached: options.processGroup === true && process.platform !== "win32" }, options.owner ?? "stream", { beforeSpawn: options.beforeSpawn, onSpawn: options.onSpawn, onSpawnFailed: options.onSpawnFailed, onContainer: options.onContainer, onContainerEmpty: options.onContainerEmpty, onUnknown: options.onUnknown, fence: options.fence }, options.processGroup === true);
|
|
205
229
|
}
|
|
206
230
|
/** The refusal-before-spawn shape shared by every containable transport. */
|
|
207
231
|
function refusedResult(error) {
|
|
@@ -506,6 +530,7 @@ export function run(file, args, options = {}) {
|
|
|
506
530
|
...(cwd === undefined ? {} : { cwd }),
|
|
507
531
|
...(childEnv === undefined ? {} : { childEnv }),
|
|
508
532
|
...(options.onSpawn === undefined ? {} : { onSpawn: options.onSpawn }),
|
|
533
|
+
...(options.onSpawnFailed === undefined ? {} : { onSpawnFailed: options.onSpawnFailed }),
|
|
509
534
|
...(options.owner === undefined ? {} : { owner: options.owner }),
|
|
510
535
|
...(options.beforeSpawn === undefined ? {} : { beforeSpawn: options.beforeSpawn }),
|
|
511
536
|
...(options.onStdout === undefined ? {} : { onStdout: options.onStdout }),
|
|
@@ -553,7 +578,7 @@ function runBufferedGroup(file, args, bag) {
|
|
|
553
578
|
let container = null;
|
|
554
579
|
let attached;
|
|
555
580
|
try {
|
|
556
|
-
const spawned = spawnContained(file, args, { cwd: bag.cwd, env: bag.childEnv, stdio: [bag.stdin === undefined ? "ignore" : "pipe", "pipe", "pipe"], detached: process.platform !== "win32" }, bag.owner ?? "buffered", { beforeSpawn: bag.beforeSpawn, onSpawn: bag.onSpawn, onContainer: bag.onContainer, onContainerEmpty: bag.onContainerEmpty, onUnknown: bag.onUnknown, fence: bag.fence });
|
|
581
|
+
const spawned = spawnContained(file, args, { cwd: bag.cwd, env: bag.childEnv, stdio: [bag.stdin === undefined ? "ignore" : "pipe", "pipe", "pipe"], detached: process.platform !== "win32" }, bag.owner ?? "buffered", { beforeSpawn: bag.beforeSpawn, onSpawn: bag.onSpawn, onSpawnFailed: bag.onSpawnFailed, onContainer: bag.onContainer, onContainerEmpty: bag.onContainerEmpty, onUnknown: bag.onUnknown, fence: bag.fence });
|
|
557
582
|
child = spawned.child;
|
|
558
583
|
container = spawned.container;
|
|
559
584
|
attached = spawned.attached;
|
|
@@ -1578,6 +1603,7 @@ export function startClaudeHeldSession(file, args, options) {
|
|
|
1578
1603
|
}, options.owner ?? "held", {
|
|
1579
1604
|
beforeSpawn: options.beforeSpawn,
|
|
1580
1605
|
onSpawn: options.onSpawn,
|
|
1606
|
+
onSpawnFailed: options.onSpawnFailed,
|
|
1581
1607
|
onContainer: options.onContainer,
|
|
1582
1608
|
onContainerEmpty: options.onContainerEmpty,
|
|
1583
1609
|
onUnknown: options.onUnknown,
|
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" | "reviewing" | "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" || read.stage === "reviewing" ? { 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
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The flow gallery in the console (Flows → New and Settings → Flows): cards grouped by what they're for, each with
|
|
3
|
+
* its promise, a small drawing of its zones, what it needs and "Use this". A template's page asks only what it
|
|
4
|
+
* needs, previews in plain words what it will do and never do, and creates it — the same answers it previewed.
|
|
5
|
+
*/
|
|
6
|
+
import { type GalleryAnswers, type GalleryPreview, type GalleryTemplate } from "./flow-gallery.js";
|
|
7
|
+
import type { FlowDefinition } from "./flows.js";
|
|
8
|
+
export declare const GALLERY_CSS: string;
|
|
9
|
+
/** A small drawing of a flow's zones where they sit on its canvas, with its paths; a person's decisions stand out. */
|
|
10
|
+
export declare function zonesDiagram(definition: FlowDefinition, label: string): string;
|
|
11
|
+
/** The gallery: every template, grouped, each with one "Use this". `repo` carries a chosen project to the template's page. */
|
|
12
|
+
export declare function galleryHtml(input: {
|
|
13
|
+
repo: string | null;
|
|
14
|
+
canUse: boolean;
|
|
15
|
+
}): string;
|
|
16
|
+
/** A template's page: its questions, then what it will do and never do, then Create. */
|
|
17
|
+
export declare function galleryUseHtml(input: {
|
|
18
|
+
template: GalleryTemplate;
|
|
19
|
+
projects: readonly {
|
|
20
|
+
path: string;
|
|
21
|
+
name: string;
|
|
22
|
+
}[];
|
|
23
|
+
repo: string;
|
|
24
|
+
answers: GalleryAnswers;
|
|
25
|
+
name: string;
|
|
26
|
+
preview: GalleryPreview | null;
|
|
27
|
+
problem: string | null;
|
|
28
|
+
csrf: string;
|
|
29
|
+
diagram: FlowDefinition | null;
|
|
30
|
+
}): string;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The flow gallery in the console (Flows → New and Settings → Flows): cards grouped by what they're for, each with
|
|
3
|
+
* its promise, a small drawing of its zones, what it needs and "Use this". A template's page asks only what it
|
|
4
|
+
* needs, previews in plain words what it will do and never do, and creates it — the same answers it previewed.
|
|
5
|
+
*/
|
|
6
|
+
import { BLANK, GALLERY, GALLERY_GROUPS, galleryDiagram, OUTDATED_COMMANDS } from "./flow-gallery.js";
|
|
7
|
+
const e = (value) => String(value ?? "").replace(/[&<>"']/g, c => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
8
|
+
export const GALLERY_CSS = `.gallery{max-width:1120px;min-width:0}.gallery h2{margin:28px 0 10px;font-size:.9375rem}.gallery h2:first-of-type{margin-top:8px}` +
|
|
9
|
+
`.gallery-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(250px,1fr));gap:12px}` +
|
|
10
|
+
`.gallery-card{display:flex;flex-direction:column;gap:8px;padding:16px;border:1px solid var(--so-line);border-radius:10px;background:var(--so-paper);min-width:0}` +
|
|
11
|
+
`.gallery-card h3{margin:0;font-size:.9375rem}.gallery-card p{margin:0;overflow-wrap:anywhere}.gallery-card .gallery-promise{line-height:1.5;flex:1}` +
|
|
12
|
+
`.gallery-zones{display:block;width:100%;height:56px;border-radius:8px;background:var(--so-raised)}.gallery-zones rect{fill:var(--so-paper);stroke:var(--so-input-line);stroke-width:1.5}` +
|
|
13
|
+
`.gallery-zones rect[data-person="true"]{fill:var(--so-signal-soft,var(--so-raised));stroke:var(--so-ink)}.gallery-zones line{stroke:var(--so-input-line);stroke-width:1.5}` +
|
|
14
|
+
`.gallery-needs{display:flex;flex-wrap:wrap;gap:4px;list-style:none;padding:0;margin:0}.gallery-needs li{font-size:11.5px;font-weight:500;line-height:18px;padding:1px 6px;border-radius:5px;background:var(--so-neutral-soft,var(--so-raised));color:var(--so-neutral-ink,var(--so-muted))}` +
|
|
15
|
+
// An outline action in a list (the Quiet List Rule), over the workspace's ink default for .button-link.
|
|
16
|
+
`.gallery .gallery-card a.button-link{align-self:flex-start;min-height:36px;background:var(--so-paper);color:var(--so-ink);border:1px solid var(--so-input-line)}` +
|
|
17
|
+
`.gallery-blank{margin-top:20px}` +
|
|
18
|
+
`.gallery-use{max-width:720px;min-width:0}.gallery-use .gallery-zones{height:88px;margin:4px 0 12px}.gallery-use form{display:grid;gap:12px;margin:0}.gallery-use label{display:grid;gap:4px;font-weight:500}` +
|
|
19
|
+
`.gallery-use label small{font-weight:400;color:var(--so-muted)}` +
|
|
20
|
+
`.gallery-use input:not([type=hidden]),.gallery-use select{box-sizing:border-box;width:100%;max-width:100%;min-height:32px;padding:0 10px;border:1px solid var(--so-input-line);border-radius:8px;background:var(--so-paper);color:var(--so-ink);font:inherit}` +
|
|
21
|
+
`.gallery-preview{display:grid;gap:8px;padding:16px;border:1px solid var(--so-line);border-radius:10px;background:var(--so-paper);min-width:0}.gallery-preview h2{margin:0;font-size:.9375rem}` +
|
|
22
|
+
`.gallery-preview ul{margin:0;padding-left:18px;line-height:1.55}.gallery-preview p{margin:0;overflow-wrap:anywhere}.gallery-preview .gallery-never{font-weight:600}` +
|
|
23
|
+
`.gallery-preview details ol{margin:6px 0 0;padding-left:20px;white-space:pre-line;font-size:.875rem;line-height:1.5;color:var(--so-muted)}.gallery-preview details li{margin:0 0 6px;overflow-wrap:anywhere}` +
|
|
24
|
+
`.gallery-preview summary{cursor:pointer;min-height:36px;display:flex;align-items:center;font-weight:500}` +
|
|
25
|
+
`.gallery-actions{display:flex;flex-wrap:wrap;gap:8px;margin:0}.gallery-actions button{min-height:36px;white-space:nowrap}` +
|
|
26
|
+
`.gallery-use .gallery-actions button[value=create]{background:var(--primary);color:var(--primary-foreground);border-color:var(--primary);font-weight:600}.gallery-heading{margin:36px 0 0;font-size:1.05rem}` +
|
|
27
|
+
`@media(max-width:760px){.gallery-grid{grid-template-columns:minmax(0,1fr)}.gallery .gallery-card a.button-link{min-height:44px;align-self:stretch;justify-content:center}` +
|
|
28
|
+
`.gallery-use input:not([type=hidden]),.gallery-use select{font-size:16px;min-height:44px}.gallery-actions button{flex:1 1 auto;min-height:44px}}`;
|
|
29
|
+
/** A small drawing of a flow's zones where they sit on its canvas, with its paths; a person's decisions stand out. */
|
|
30
|
+
export function zonesDiagram(definition, label) {
|
|
31
|
+
const zones = definition.stages.map(one => one.zone);
|
|
32
|
+
const minX = Math.min(...zones.map(one => one.x)), minY = Math.min(...zones.map(one => one.y));
|
|
33
|
+
const maxX = Math.max(...zones.map(one => one.x + one.w)), maxY = Math.max(...zones.map(one => one.y + one.h));
|
|
34
|
+
const pad = 40;
|
|
35
|
+
const box = `${minX - pad} ${minY - pad} ${maxX - minX + pad * 2} ${maxY - minY + pad * 2}`;
|
|
36
|
+
const centre = (id) => { const at = definition.stages.find(one => one.id === id)?.zone; return at === undefined ? null : { x: at.x + at.w / 2, y: at.y + at.h / 2 }; };
|
|
37
|
+
const lines = definition.stages.flatMap(stage => [stage.next, ...(stage.sort?.answers.map(one => one.to) ?? []), ...(stage.routes?.map(one => one.to) ?? [])].map(to => {
|
|
38
|
+
const from = centre(stage.id), end = centre(to);
|
|
39
|
+
return from === null || end === null ? "" : `<line x1="${from.x}" y1="${from.y}" x2="${end.x}" y2="${end.y}" vector-effect="non-scaling-stroke"/>`;
|
|
40
|
+
})).join("");
|
|
41
|
+
const rects = definition.stages.map(one => `<rect x="${one.zone.x}" y="${one.zone.y}" width="${one.zone.w}" height="${one.zone.h}" rx="36" data-person="${one.kind === "approval"}" vector-effect="non-scaling-stroke"/>`).join("");
|
|
42
|
+
return `<svg class="gallery-zones" viewBox="${box}" preserveAspectRatio="xMidYMid meet" role="img" aria-label="${e(label)}">${lines}${rects}</svg>`;
|
|
43
|
+
}
|
|
44
|
+
const needsList = (template) => template.needs.length === 0 ? `<ul class="gallery-needs"><li>Nothing to connect</li></ul>`
|
|
45
|
+
: `<ul class="gallery-needs" aria-label="Needs">${template.needs.map(one => `<li>${e(one)}</li>`).join("")}</ul>`;
|
|
46
|
+
/** The gallery: every template, grouped, each with one "Use this". `repo` carries a chosen project to the template's page. */
|
|
47
|
+
export function galleryHtml(input) {
|
|
48
|
+
const href = (id) => `/flows/new/${id}${input.repo === null ? "" : `?repo=${encodeURIComponent(input.repo)}`}`;
|
|
49
|
+
const card = (template) => {
|
|
50
|
+
const definition = galleryDiagram(template);
|
|
51
|
+
return `<article class="gallery-card" data-template="${e(template.id)}"><h3>${e(template.name)}</h3><p class="gallery-promise">${e(template.promise)}</p>` +
|
|
52
|
+
zonesDiagram(definition, `Zones: ${definition.stages.map(one => one.title).join(", ")}`) + needsList(template) +
|
|
53
|
+
(input.canUse ? `<a class="button-link" href="${e(href(template.id))}" aria-label="Use ${e(template.name)}">Use this</a>` : "") + `</article>`;
|
|
54
|
+
};
|
|
55
|
+
const groups = GALLERY_GROUPS.map(group => `<h2 id="gallery-${group.id}">${e(group.label)}</h2><div class="gallery-grid" data-group="${group.id}">${GALLERY.filter(one => one.group === group.id).map(card).join("")}</div>`).join("");
|
|
56
|
+
return `<section class="gallery">${groups}${input.canUse ? `<p class="gallery-blank meta"><a href="${e(href(BLANK.id))}">Start from a blank flow</a></p>` : `<p class="meta">An approver creates flows.</p>`}</section>`;
|
|
57
|
+
}
|
|
58
|
+
/** A template's page: its questions, then what it will do and never do, then Create. */
|
|
59
|
+
export function galleryUseHtml(input) {
|
|
60
|
+
const { template } = input;
|
|
61
|
+
const field = (ask) => {
|
|
62
|
+
const value = input.answers[ask.key] ?? ask.default;
|
|
63
|
+
const hint = ask.hint === undefined ? "" : ` <small>${e(ask.hint)}</small>`;
|
|
64
|
+
if (ask.key === "outdated")
|
|
65
|
+
return `<label>${e(ask.label)}<select name="outdated">${Object.entries(OUTDATED_COMMANDS).map(([id, one]) => `<option value="${id}"${id === value ? " selected" : ""}>${e(one.label)}</option>`).join("")}</select></label>`;
|
|
66
|
+
return `<label>${e(ask.label)}${hint}<input name="${ask.key}" value="${e(value)}" required maxlength="${ask.key === "command" ? 500 : 100}"${ask.key === "command" ? ' spellcheck="false" autocapitalize="off"' : ""}></label>`;
|
|
67
|
+
};
|
|
68
|
+
const project = input.projects.length > 1
|
|
69
|
+
? `<label>Project<select name="repo">${input.projects.map(one => `<option value="${e(one.path)}"${one.path === input.repo ? " selected" : ""}>${e(one.name)}</option>`).join("")}</select></label>`
|
|
70
|
+
: `<input type="hidden" name="repo" value="${e(input.repo)}">`;
|
|
71
|
+
const preview = input.preview === null ? "" : `<section class="gallery-preview" aria-labelledby="gallery-will">` +
|
|
72
|
+
`<h2 id="gallery-will">What it will do</h2><ul>${input.preview.built.does.map(one => `<li>${e(one)}</li>`).join("")}</ul>` +
|
|
73
|
+
`<p class="gallery-never">${e(input.preview.built.never)}</p>` +
|
|
74
|
+
(input.preview.startsFrom.length === 0 ? `<p class="meta">Cards start when you add them.</p>` : input.preview.startsFrom.map(one => `<p class="meta">Starts from: ${e(one)}</p>`).join("")) +
|
|
75
|
+
`<details><summary>Every step</summary><ol>${input.preview.steps.map(one => `<li>${e(one.replace(/^\d+\.\s/, ""))}</li>`).join("")}</ol></details></section>`;
|
|
76
|
+
const asks = template.asks.length > 0 || input.projects.length > 1;
|
|
77
|
+
return `<section class="gallery-use" data-gallery-template="${e(template.id)}">` +
|
|
78
|
+
(input.problem === null ? "" : `<p class="problem" role="alert">${e(input.problem)}</p>`) +
|
|
79
|
+
`<p>${e(template.promise)}</p>${input.diagram === null ? "" : zonesDiagram(input.diagram, `Zones: ${input.diagram.stages.map(one => one.title).join(", ")}`)}${needsList(template)}` +
|
|
80
|
+
`<form method="post" action="/flows/new/${e(template.id)}" data-gallery-use><input type="hidden" name="csrf" value="${e(input.csrf)}">` +
|
|
81
|
+
`<input type="hidden" name="previewed" value="${e(input.preview?.digest ?? "")}">${project}${template.asks.map(field).join("")}` +
|
|
82
|
+
`<label>Name<input name="name" value="${e(input.name)}" maxlength="80"></label>${preview}` +
|
|
83
|
+
`<p class="gallery-actions">${input.preview === null ? "" : `<button type="submit" name="intent" value="create">Create flow</button>`}${asks || input.preview === null ? `<button type="submit" name="intent" value="preview" class="secondary">${input.preview === null ? "Preview" : "Update preview"}</button>` : ""}</p>` +
|
|
84
|
+
`</form><p class="meta"><a href="/flows/new">All templates</a></p></section>`;
|
|
85
|
+
}
|