toolroll 0.9.83 → 0.9.85
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 +69 -0
- package/README.md +94 -1192
- package/dist/agent-fence.d.ts +3 -3
- package/dist/agent-fence.js +3 -3
- package/dist/agent-onboard.d.ts +2 -0
- package/dist/agent-onboard.js +3 -0
- package/dist/assignment-ui.js +5 -1
- package/dist/browser/workspace.css +2 -2
- package/dist/browser/workspace.js +31 -31
- package/dist/browser-crew.js +5 -1
- package/dist/browser-workspace.d.ts +12 -0
- package/dist/chat-channel.d.ts +10 -1
- package/dist/chat-channel.js +35 -5
- package/dist/chat-decide.d.ts +16 -4
- package/dist/chat-decide.js +239 -31
- package/dist/chat-messages.d.ts +5 -0
- package/dist/chat-messages.js +1 -1
- package/dist/chat-quiet.js +3 -2
- package/dist/chat-review.d.ts +4 -0
- package/dist/chat-review.js +13 -4
- package/dist/chat-terms.d.ts +9 -0
- package/dist/chat-terms.js +46 -0
- package/dist/contracts/console-api.d.ts +4 -2
- package/dist/contracts/console-api.js +7 -2
- package/dist/contracts/lead-tools.d.ts +30 -0
- package/dist/contracts/lead-tools.js +3 -0
- package/dist/contracts/youiyoux-request.d.ts +248 -0
- package/dist/contracts/youiyoux-request.js +152 -0
- package/dist/control-setup.d.ts +9 -0
- package/dist/control-setup.js +31 -4
- package/dist/control-ui.d.ts +7 -1
- package/dist/control-ui.js +3 -2
- package/dist/desktop-update.d.ts +1 -1
- package/dist/dispatch.d.ts +15 -0
- package/dist/dispatch.js +26 -4
- package/dist/first-run.d.ts +5 -2
- package/dist/first-run.js +13 -2
- package/dist/flow-cadence.d.ts +8 -2
- package/dist/flow-cadence.js +16 -3
- package/dist/flow-card-task.d.ts +9 -0
- package/dist/flow-card-task.js +15 -6
- package/dist/folder-look.d.ts +3 -0
- package/dist/folder-look.js +8 -4
- package/dist/guides.js +4 -3
- package/dist/integrations-ui.js +9 -4
- package/dist/integrations.d.ts +9 -1
- package/dist/integrations.js +43 -2
- package/dist/landed-cleanup.d.ts +4 -0
- package/dist/landed-cleanup.js +3 -2
- package/dist/lead-capabilities.js +2 -1
- package/dist/lead-cli.js +3 -1
- package/dist/lead-tools.d.ts +2 -0
- package/dist/lead-tools.js +30 -4
- package/dist/lead.d.ts +2 -0
- package/dist/lead.js +3 -3
- package/dist/model-catalog.d.ts +5 -0
- package/dist/model-catalog.js +11 -2
- package/dist/model-choices.d.ts +188 -0
- package/dist/model-choices.js +405 -0
- package/dist/model-names.d.ts +6 -0
- package/dist/model-names.js +34 -0
- package/dist/models-ui.d.ts +69 -5
- package/dist/models-ui.js +53 -10
- package/dist/needs-you.d.ts +8 -0
- package/dist/needs-you.js +8 -0
- package/dist/operate.d.ts +22 -2
- package/dist/operate.js +107 -41
- package/dist/project-activity.d.ts +20 -0
- package/dist/project-activity.js +23 -0
- package/dist/project-admission.d.ts +4 -1
- package/dist/project-admission.js +23 -2
- package/dist/project-cli.d.ts +1 -0
- package/dist/project-cli.js +36 -19
- package/dist/project-identity.d.ts +9 -2
- package/dist/project-identity.js +80 -33
- package/dist/project-knowledge.d.ts +9 -9
- package/dist/project-outcomes.d.ts +119 -0
- package/dist/project-outcomes.js +79 -0
- package/dist/result-actions.d.ts +4 -0
- package/dist/result-actions.js +41 -9
- package/dist/result-review.d.ts +9 -0
- package/dist/result-review.js +45 -5
- package/dist/serve.js +60 -13
- package/dist/server/chat.js +5 -4
- package/dist/server/chrome.d.ts +3 -0
- package/dist/server/chrome.js +5 -2
- package/dist/server/console.css +17 -2
- package/dist/server/flows.js +4 -0
- package/dist/server/http.d.ts +1 -0
- package/dist/server/http.js +6 -0
- package/dist/server/pages.js +53 -7
- package/dist/server/people-tokens.js +40 -20
- package/dist/server/render-chat.js +3 -2
- package/dist/server/render-pages.d.ts +12 -2
- package/dist/server/render-pages.js +36 -16
- package/dist/server/render-people.d.ts +14 -2
- package/dist/server/render-people.js +35 -12
- package/dist/server/render-results.d.ts +2 -0
- package/dist/server/render-results.js +20 -7
- package/dist/server/render-settings.js +1 -0
- package/dist/server/render-tasks.d.ts +1 -0
- package/dist/server/render-tasks.js +22 -19
- package/dist/server/request-context.d.ts +2 -0
- package/dist/server/route-table.js +9 -1
- package/dist/server/settings.d.ts +58 -1
- package/dist/server/settings.js +216 -70
- package/dist/server/signup-gate.d.ts +9 -0
- package/dist/server/signup-gate.js +40 -0
- package/dist/server/task-views.js +6 -0
- package/dist/server/tasks.js +54 -8
- package/dist/settings-kit-script.d.ts +21 -0
- package/dist/settings-kit-script.js +67 -0
- package/dist/settings-kit.d.ts +137 -0
- package/dist/settings-kit.js +186 -0
- package/dist/settings-proofs.d.ts +64 -0
- package/dist/settings-proofs.js +101 -0
- package/dist/setup-guide.js +4 -2
- package/dist/spend-report.d.ts +87 -0
- package/dist/spend-report.js +265 -0
- package/dist/spend-ui.d.ts +29 -8
- package/dist/spend-ui.js +74 -39
- package/dist/store.d.ts +46 -3
- package/dist/store.js +165 -6
- package/dist/task-sizing.d.ts +5 -3
- package/dist/task-sizing.js +4 -4
- package/dist/task-source.d.ts +13 -1
- package/dist/task-source.js +17 -1
- package/dist/telegram-lead.js +7 -5
- package/dist/telegram.js +1 -1
- package/dist/toolroll-update.js +2 -2
- package/dist/webhooks.d.ts +6 -0
- package/dist/webhooks.js +25 -0
- package/dist/work-index.d.ts +4 -0
- package/dist/work-index.js +32 -0
- package/dist/youiyoux-intake.d.ts +234 -0
- package/dist/youiyoux-intake.js +908 -0
- package/package.json +11 -8
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A youiyoux request as Toolroll reads it (youiyoux-intake.ts): the published `youi.request/v1` schema (youiyoux's
|
|
3
|
+
* schema/youi.request.schema.json), held to its own limits, and the `youi.proposal/v1` options file an options request's
|
|
4
|
+
* agent writes (schema/youi.proposal.schema.json). Unknown keys are allowed, as the published schemas allow them;
|
|
5
|
+
* everything Toolroll reads is checked here, and every string is outside text all the same.
|
|
6
|
+
*/
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
import { parseContract } from "./contract.js";
|
|
9
|
+
export const REQUEST_ID = /^r-[0-9a-f]{10}$/;
|
|
10
|
+
/** An options file's id: `p-` and the same 10 hex as the options request it answers. */
|
|
11
|
+
export const PROPOSAL_ID = /^p-[0-9a-f]{10}$/;
|
|
12
|
+
/** The provenance every task filed from a request carries (task_ref.filed_via): immutable, and the seal reads it. */
|
|
13
|
+
export const YOUIYOUX_FILED_VIA = "youiyoux";
|
|
14
|
+
/** The one chat card a pass's filings make for a project (youiyoux-intake.ts), drawn by chat-decide.ts: the new tasks,
|
|
15
|
+
* Approve all and each one's own Approve and Open. */
|
|
16
|
+
export const YOUIYOUX_CARD_KIND = "youiyoux-requests";
|
|
17
|
+
/** Where a person sees and approves everything waiting on them in the console (flow-triggers.ts NEEDS_YOU_HREF). */
|
|
18
|
+
export const YOUIYOUX_HOME_PATH = "/work?view=needs-you";
|
|
19
|
+
const text = (max) => z.string().min(1).max(max);
|
|
20
|
+
const who = z.object({ kind: z.enum(["person", "agent"]), name: text(100) });
|
|
21
|
+
// The published schema's date-time: an ISO timestamp with a zone.
|
|
22
|
+
const when = z.string().max(40).regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/);
|
|
23
|
+
export const YOUIYOUX_EVIDENCE = ["screenshot", "check", "changed-path", "manual-review"];
|
|
24
|
+
/** A component prop as youiyoux names one: what an option may set. */
|
|
25
|
+
const PROP = /^[A-Za-z_$][\w$]{0,63}$/;
|
|
26
|
+
const value = z.union([z.string().max(2000), z.number().finite(), z.boolean()]);
|
|
27
|
+
/** What an options request offers its agent: the one file to write (`.youi/proposals/<proposal>.json`), the props an
|
|
28
|
+
* option may set (enum values from `values`), and whether it may give the component new text. */
|
|
29
|
+
export const youiyouxOfferSchema = z.object({
|
|
30
|
+
proposal: z.string().regex(PROPOSAL_ID),
|
|
31
|
+
file: text(500),
|
|
32
|
+
choices: z.record(z.string().regex(PROP), z.object({
|
|
33
|
+
type: z.enum(["enum", "string", "boolean", "number"]),
|
|
34
|
+
values: z.array(text(200)).max(100).optional(),
|
|
35
|
+
default: value.optional(),
|
|
36
|
+
})).refine(choices => Object.keys(choices).length <= 50, "at most 50 props"),
|
|
37
|
+
text: z.boolean(),
|
|
38
|
+
});
|
|
39
|
+
export const youiyouxTaskSchema = z.object({
|
|
40
|
+
title: text(120),
|
|
41
|
+
goal: text(4000),
|
|
42
|
+
touches: z.array(text(500)).max(50),
|
|
43
|
+
touchesKnown: z.boolean(),
|
|
44
|
+
why: text(500).optional(),
|
|
45
|
+
not: z.array(text(300)).max(10),
|
|
46
|
+
acceptance: z.array(z.object({ statement: text(300), evidence: z.array(z.enum(YOUIYOUX_EVIDENCE)).min(1) })).min(1).max(10),
|
|
47
|
+
/** An options request's: what its agent writes (optionsProblem holds it to the request). */
|
|
48
|
+
offer: youiyouxOfferSchema.optional(),
|
|
49
|
+
});
|
|
50
|
+
export const youiyouxRequestSchema = z.object({
|
|
51
|
+
schema: z.literal("youi.request/v1"),
|
|
52
|
+
id: z.string().regex(REQUEST_ID),
|
|
53
|
+
project: text(200),
|
|
54
|
+
kind: z.enum(["comment", "change", "options"]),
|
|
55
|
+
status: z.enum(["open", "addressed", "closed"]),
|
|
56
|
+
body: text(4000),
|
|
57
|
+
author: who,
|
|
58
|
+
createdAt: when,
|
|
59
|
+
updatedAt: when,
|
|
60
|
+
// Where the person pointed: the page, the component, and (when youiyoux knew it) the file and line that draw it.
|
|
61
|
+
target: z.object({ route: text(300), scope: z.enum(["here", "element"]), name: text(200), file: text(500).optional(), line: z.int().positive().max(Number.MAX_SAFE_INTEGER).optional() }),
|
|
62
|
+
task: youiyouxTaskSchema.optional(),
|
|
63
|
+
thread: z.array(z.unknown()).max(500),
|
|
64
|
+
});
|
|
65
|
+
export function readYouiyouxRequest(raw) {
|
|
66
|
+
return parseContract(youiyouxRequestSchema, raw);
|
|
67
|
+
}
|
|
68
|
+
/** The one file an options request's agent may write: `.youi/proposals/p-<the request's hex>.json`. */
|
|
69
|
+
export const proposalIdOf = (request) => `p-${request.slice(2)}`;
|
|
70
|
+
export const proposalFileOf = (proposal) => `.youi/proposals/${proposal}.json`;
|
|
71
|
+
/** Why an options request can't be filed as youiyoux's contract has it, or null: it carries a task with an offer whose
|
|
72
|
+
* proposal is `p-<its own hex>`, whose file is that proposal's, and the task may touch that one file and nothing else. */
|
|
73
|
+
export function optionsProblem(request) {
|
|
74
|
+
if (request.kind !== "options")
|
|
75
|
+
return null;
|
|
76
|
+
const offer = request.task?.offer;
|
|
77
|
+
if (request.task === undefined || offer === undefined)
|
|
78
|
+
return "it asks for options with no offer";
|
|
79
|
+
const proposal = proposalIdOf(request.id);
|
|
80
|
+
if (offer.proposal !== proposal)
|
|
81
|
+
return `its offer is for ${offer.proposal}, not ${proposal}`;
|
|
82
|
+
if (offer.file !== proposalFileOf(proposal))
|
|
83
|
+
return `its offer's file isn't ${proposalFileOf(proposal)}`;
|
|
84
|
+
if (!request.task.touchesKnown || request.task.touches.length !== 1 || request.task.touches[0] !== offer.file)
|
|
85
|
+
return "its task may touch more than its options file";
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
/** `youi.proposal/v1`: the options an agent puts on youiyoux's Screens canvas (youiyoux's packages/core/src/proposals.ts,
|
|
89
|
+
* mirrored). Toolroll checks only the shape and what it reads; youiyoux checks each option against the component's
|
|
90
|
+
* contract when it reads the file. */
|
|
91
|
+
const OPTION_IDS = ["a", "b", "c", "d"];
|
|
92
|
+
export const PROPOSAL_STATUSES = ["open", "chosen", "dismissed"];
|
|
93
|
+
export const youiyouxProposalSchema = z.object({
|
|
94
|
+
$schema: z.string().max(500).optional(),
|
|
95
|
+
schema: z.literal("youi.proposal/v1"),
|
|
96
|
+
id: z.string().regex(PROPOSAL_ID),
|
|
97
|
+
project: text(200),
|
|
98
|
+
route: text(300),
|
|
99
|
+
target: z.object({
|
|
100
|
+
name: text(200), element: text(200).optional(), file: text(500).optional(),
|
|
101
|
+
line: z.int().positive().max(Number.MAX_SAFE_INTEGER).optional(), column: z.int().nonnegative().max(Number.MAX_SAFE_INTEGER).optional(),
|
|
102
|
+
box: z.object({ x: z.number().finite(), y: z.number().finite(), width: z.number().finite().nonnegative(), height: z.number().finite().nonnegative() }),
|
|
103
|
+
page: z.object({ width: z.number().finite().positive(), height: z.number().finite().positive() }).optional(),
|
|
104
|
+
}),
|
|
105
|
+
ask: text(1000),
|
|
106
|
+
author: who,
|
|
107
|
+
createdAt: when,
|
|
108
|
+
updatedAt: when,
|
|
109
|
+
options: z.array(z.object({
|
|
110
|
+
id: z.enum(OPTION_IDS),
|
|
111
|
+
label: text(60),
|
|
112
|
+
why: text(500),
|
|
113
|
+
set: z.array(z.object({ prop: z.string().regex(PROP), value })).max(20),
|
|
114
|
+
text: text(2000).nullable(),
|
|
115
|
+
}).refine(option => option.set.length > 0 || option.text !== null, "an option changes a prop or the text")).min(2).max(4),
|
|
116
|
+
status: z.enum(PROPOSAL_STATUSES),
|
|
117
|
+
choice: z.enum(OPTION_IDS).optional(),
|
|
118
|
+
decidedBy: who.optional(),
|
|
119
|
+
decidedAt: when.optional(),
|
|
120
|
+
request: z.string().regex(REQUEST_ID).optional(),
|
|
121
|
+
for: z.string().regex(REQUEST_ID),
|
|
122
|
+
}).refine(doc => new Set(doc.options.map(option => option.id)).size === doc.options.length, "each option has its own letter")
|
|
123
|
+
.refine(doc => doc.choice === undefined || doc.options.some(option => option.id === doc.choice), "the choice is one of its options");
|
|
124
|
+
export function readYouiyouxProposal(raw) {
|
|
125
|
+
return parseContract(youiyouxProposalSchema, raw);
|
|
126
|
+
}
|
|
127
|
+
/** What Toolroll writes beside a request (`youi.request-task/v1`): the task it became, how that task stands, and, when
|
|
128
|
+
* the console has an address, the task's link and `home`, where a person sees and approves all waiting work. */
|
|
129
|
+
export const YOUIYOUX_TASK_STATUSES = ["queued", "needs-you", "building", "ready", "done", "failed", "stopped"];
|
|
130
|
+
/** A link youiyoux opens: http or https, at most 2,000 characters, with no user name or password in it. */
|
|
131
|
+
export const youiyouxLinkSchema = z.string().max(2000).refine(value => {
|
|
132
|
+
try {
|
|
133
|
+
const url = new URL(value);
|
|
134
|
+
return (url.protocol === "http:" || url.protocol === "https:") && url.username === "" && url.password === "";
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
}, "an http or https address with no credentials");
|
|
140
|
+
export const youiyouxRequestTaskSchema = z.strictObject({
|
|
141
|
+
schema: z.literal("youi.request-task/v1"),
|
|
142
|
+
request: z.string().regex(REQUEST_ID),
|
|
143
|
+
runner: z.literal("Toolroll"),
|
|
144
|
+
task: z.strictObject({ id: z.string().min(1).max(120), url: youiyouxLinkSchema.nullable() }),
|
|
145
|
+
home: youiyouxLinkSchema.optional(),
|
|
146
|
+
status: z.enum(YOUIYOUX_TASK_STATUSES),
|
|
147
|
+
detail: z.string().max(200),
|
|
148
|
+
updatedAt: when,
|
|
149
|
+
});
|
|
150
|
+
export function readYouiyouxRequestTask(raw) {
|
|
151
|
+
return parseContract(youiyouxRequestTaskSchema, raw);
|
|
152
|
+
}
|
package/dist/control-setup.d.ts
CHANGED
|
@@ -13,6 +13,7 @@ export declare function previewSetup(store: Store, repo: string, inputs: SetupIn
|
|
|
13
13
|
seconds?: never;
|
|
14
14
|
provider?: never;
|
|
15
15
|
model?: never;
|
|
16
|
+
models?: never;
|
|
16
17
|
} | {
|
|
17
18
|
message?: never;
|
|
18
19
|
ok: true;
|
|
@@ -21,6 +22,13 @@ export declare function previewSetup(store: Store, repo: string, inputs: SetupIn
|
|
|
21
22
|
seconds: number;
|
|
22
23
|
provider: "claude" | "cline" | "codex" | "cursor" | "gemini" | "goose" | "opencode" | "openrouter" | "qwen";
|
|
23
24
|
model: string;
|
|
25
|
+
models: {
|
|
26
|
+
rows: (import("./model-choices.js").DiffRow & {
|
|
27
|
+
inherited: import("./model-choices.js").Pair | null;
|
|
28
|
+
})[];
|
|
29
|
+
state: string;
|
|
30
|
+
change: string;
|
|
31
|
+
} | null;
|
|
24
32
|
};
|
|
25
33
|
export declare function approveSetup(store: Store, repo: string, inputs: SetupInputs, fingerprint: string, by: string, token: string, now: Date): {
|
|
26
34
|
ok: false;
|
|
@@ -30,6 +38,7 @@ export declare function approveSetup(store: Store, repo: string, inputs: SetupIn
|
|
|
30
38
|
seconds?: never;
|
|
31
39
|
provider?: never;
|
|
32
40
|
model?: never;
|
|
41
|
+
models?: never;
|
|
33
42
|
} | {
|
|
34
43
|
ok: true;
|
|
35
44
|
};
|
package/dist/control-setup.js
CHANGED
|
@@ -2,6 +2,8 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { hasDisguisedText } from "./decision.js";
|
|
3
3
|
import { authenticateApprover } from "./scope.js";
|
|
4
4
|
import { isProviderId, validateSpec } from "./provider.js";
|
|
5
|
+
import { INSTALLATION_SCOPE } from "./agentconfig.js";
|
|
6
|
+
import { ALL_SLOTS, applyModelChoices, DEFAULT_PRESET, presetChange, previewModelChange, rowAt } from "./model-choices.js";
|
|
5
7
|
export function previewSetup(store, repo, inputs) {
|
|
6
8
|
if (!isProviderId(inputs.provider))
|
|
7
9
|
return { ok: false, message: "Choose an installed provider." };
|
|
@@ -18,19 +20,36 @@ export function previewSetup(store, repo, inputs) {
|
|
|
18
20
|
const seconds = Number(inputs.seconds);
|
|
19
21
|
if (!/^[1-9][0-9]*$/.test(inputs.seconds) || !Number.isInteger(seconds) || seconds < 1 || seconds > 3600)
|
|
20
22
|
return { ok: false, message: "Setup timeout must be 1–3600 seconds." };
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
+
const models = installationModels(store);
|
|
24
|
+
const fingerprint = createHash("sha256").update(JSON.stringify({ repo, inputs, beforeSetup: store.liveWorktreeSetup(repo), beforeConfig: store.phaseConfig(repo, "build"),
|
|
25
|
+
...(models === null ? {} : { models: [models.state, models.change] }) })).digest("hex");
|
|
26
|
+
return { ok: true, fingerprint, command, seconds, provider: inputs.provider, model: inputs.model.trim(), models };
|
|
23
27
|
}
|
|
24
28
|
export function approveSetup(store, repo, inputs, fingerprint, by, token, now) {
|
|
25
29
|
return store.transact(() => {
|
|
26
|
-
|
|
30
|
+
const proved = authenticateApprover(store, by, token, repo);
|
|
31
|
+
if (!proved.ok)
|
|
27
32
|
return { ok: false, message: "Your operator credential is required." };
|
|
28
33
|
const preview = previewSetup(store, repo, inputs);
|
|
29
34
|
if (!preview.ok)
|
|
30
35
|
return preview;
|
|
31
36
|
if (preview.fingerprint !== fingerprint)
|
|
32
37
|
return { ok: false, message: "Setup changed. Review it again." };
|
|
33
|
-
|
|
38
|
+
if (preview.models !== null) {
|
|
39
|
+
// The installation's models, through Settings → Models' one write path (an instance operator's act). The proof
|
|
40
|
+
// above ran in this same transaction, so the generation read here is the one it was checked against.
|
|
41
|
+
if (!store.isInstanceOperator(by))
|
|
42
|
+
return { ok: false, message: "Models for this installation aren't chosen yet. An instance operator sets them up first." };
|
|
43
|
+
const applied = applyModelChoices(store, { scope: INSTALLATION_SCOPE, change: presetChange(DEFAULT_PRESET), expected: { state: preview.models.state, change: preview.models.change }, strict: true, knownProject: () => false }, { actor: by, generation: store.accountOf(by)?.generation ?? -1, method: proved.method, live: () => true }, now);
|
|
44
|
+
if (!applied.ok)
|
|
45
|
+
return { ok: false, message: applied.problem };
|
|
46
|
+
}
|
|
47
|
+
// The project's own builder only when it differs from what it inherits: a project left on the installation's
|
|
48
|
+
// routine builder keeps following the installation's sizes and presets.
|
|
49
|
+
const inherited = store.phaseConfig(INSTALLATION_SCOPE, "build");
|
|
50
|
+
const same = inherited !== null && inherited.provider === preview.provider && inherited.model === preview.model;
|
|
51
|
+
if (!same || store.phaseConfig(repo, "build") !== null)
|
|
52
|
+
store.setPhaseConfig(repo, "build", preview.provider, preview.model, by, now);
|
|
34
53
|
if (preview.command === "")
|
|
35
54
|
store.clearWorktreeSetup(repo, by, now);
|
|
36
55
|
else
|
|
@@ -38,3 +57,11 @@ export function approveSetup(store, repo, inputs, fingerprint, by, token, now) {
|
|
|
38
57
|
return { ok: true };
|
|
39
58
|
});
|
|
40
59
|
}
|
|
60
|
+
// Last in the file, so the password tripwire's function positions (password-freshness.test.ts) stay as reviewed.
|
|
61
|
+
/** A new installation, nothing chosen yet: setup applies the default preset ("Sized by job") with the project's
|
|
62
|
+
* builder, previewed here row by row, so the first task files on exact models for every size. */
|
|
63
|
+
function installationModels(store) {
|
|
64
|
+
if (!ALL_SLOTS.every(slot => rowAt(store, INSTALLATION_SCOPE, slot) === null))
|
|
65
|
+
return null;
|
|
66
|
+
return previewModelChange(store, INSTALLATION_SCOPE, presetChange(DEFAULT_PRESET));
|
|
67
|
+
}
|
package/dist/control-ui.d.ts
CHANGED
|
@@ -28,4 +28,10 @@ export declare function controlSetupHtml(data: {
|
|
|
28
28
|
};
|
|
29
29
|
}): Html;
|
|
30
30
|
/** `fields` may still carry the session's csrf (a caller's habit): postForm writes the one CSRF field. */
|
|
31
|
-
export declare function setupPreviewHtml(inputs: SetupInputs, fields: Record<string, string
|
|
31
|
+
export declare function setupPreviewHtml(inputs: SetupInputs, fields: Record<string, string>, models?: {
|
|
32
|
+
label: string;
|
|
33
|
+
rows: {
|
|
34
|
+
role: string;
|
|
35
|
+
model: string;
|
|
36
|
+
}[];
|
|
37
|
+
} | null): Html;
|
package/dist/control-ui.js
CHANGED
|
@@ -27,7 +27,8 @@ export function controlSetupHtml(data) {
|
|
|
27
27
|
html `<p>Give the agent the project's Toolroll handoff instructions.</p>${postForm("/control/instructions-preview", html `<button>Review instructions</button>`, { hidden: { repo: data.repo } })}`}</section><p><a href="/fleet">Configure planning, repair, and review agents</a> · <a href="/tasks/new">Describe a task</a></p>`;
|
|
28
28
|
}
|
|
29
29
|
/** `fields` may still carry the session's csrf (a caller's habit): postForm writes the one CSRF field. */
|
|
30
|
-
export function setupPreviewHtml(inputs, fields) {
|
|
30
|
+
export function setupPreviewHtml(inputs, fields, models = null) {
|
|
31
31
|
const { csrf: _csrf, ...rest } = fields;
|
|
32
|
-
|
|
32
|
+
const chosen = models === null ? "" : html `<p><strong>Models for every project:</strong> ${models.label}</p><ul>${models.rows.map(row => html `<li>${row.role}: <span class="mono">${row.model}</span></li>`)}</ul><p class="meta">Change them later in Settings → Models.</p>`;
|
|
33
|
+
return html `<h1>Approve project setup</h1><div class="card"><p>${fields.repo ?? ""}</p><p><strong>Builder:</strong> ${inputs.provider} · ${inputs.model}</p>${chosen}<p><strong>Preparation:</strong></p><pre class="recap">${inputs.command || "No preparation command"}</pre><p>Preparation timeout: ${inputs.seconds} seconds.</p><p>The builder default applies to new tasks. Preparation applies to subsequent runs, including already approved tasks. Existing signed task settings and verification rules remain in force.</p>${postForm("/control/setup-approve", html `${reauth(html `<label>Your password<input type="password" name="token" autocomplete="current-password" required></label>`)}<button>Approve project setup</button>`, { hidden: { ...rest, ...inputs } })}</div>`;
|
|
33
34
|
}
|
package/dist/desktop-update.d.ts
CHANGED
|
@@ -97,7 +97,6 @@ export declare function sqliteLock(file: string, busyTimeoutMs?: number): Databa
|
|
|
97
97
|
export declare function lockHeld(file: string): boolean;
|
|
98
98
|
export declare const installationLock: (j: UpdateJournal) => DatabaseSync | null;
|
|
99
99
|
export declare function desktopUpdateStatus(stateDir: string): {
|
|
100
|
-
version?: never;
|
|
101
100
|
active: boolean;
|
|
102
101
|
phase: string;
|
|
103
102
|
detail: string;
|
|
@@ -111,6 +110,7 @@ export declare function desktopUpdateStatus(stateDir: string): {
|
|
|
111
110
|
wasRunning?: never;
|
|
112
111
|
backupPath?: never;
|
|
113
112
|
updateId?: never;
|
|
113
|
+
version?: never;
|
|
114
114
|
buildId?: never;
|
|
115
115
|
workDir?: never;
|
|
116
116
|
} | {
|
package/dist/dispatch.d.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* fleet observations on top for people and agents reading the queue. A
|
|
7
7
|
* diagnosis can go stale; it can never grant a lease.
|
|
8
8
|
*/
|
|
9
|
+
import { type Runner } from "./runner.js";
|
|
9
10
|
import { type ChatSnapshot, type ReviewRequestOrigin, type ReviewRetryState, type Store, type TaskState } from "./store.js";
|
|
10
11
|
export declare const DEFAULT_MAX_OPEN_DECISIONS = 5;
|
|
11
12
|
export type TaskReadinessBlocker = {
|
|
@@ -95,6 +96,20 @@ export declare function dispatchRoleFor(store: Store, taskRef: number, now: Date
|
|
|
95
96
|
/** The first requirement this task fails, in words, or null. */
|
|
96
97
|
export declare function missingCapability(store: Store, taskRef: number, dispatchRepo: string | null, now: Date): string | null;
|
|
97
98
|
/** The read-side lifecycle answer. Project access limits dependency detail, never the readiness gate. */
|
|
99
|
+
/** The builders that may take work in `repo`, as dispatch decides: registered and not retired (`all`), bound to the
|
|
100
|
+
* project (`eligible`; only the assigned one when a task names one), and checking in now (`alive`). The first-run
|
|
101
|
+
* "Builder connected" step reads the same answer, so it is done only when a task there could start. */
|
|
102
|
+
export declare function buildersFor(store: Store, repo: string, now: Date, assignedRunner?: string | null): {
|
|
103
|
+
all: Runner[];
|
|
104
|
+
eligible: Runner[];
|
|
105
|
+
alive: Runner[];
|
|
106
|
+
};
|
|
107
|
+
/** Whether a task in `repo` could start for want of nothing but a person: a live builder bound to it. */
|
|
108
|
+
export declare function builderReadyFor(store: Store, repo: string | null, now: Date): boolean;
|
|
109
|
+
/** The project the first-run "Builder connected" step speaks for, on Chat and the board alike: the open one; else the
|
|
110
|
+
* saved project of the first filed task the viewer may see (All projects); else, only before any task exists, the
|
|
111
|
+
* first managed project. Null when tasks exist but none sits in a project the viewer may see: then nothing says done. */
|
|
112
|
+
export declare function firstRunBuilderRepo(store: Store, selected: string | null, visible: (repo: string | null) => boolean, managed: readonly string[]): string | null;
|
|
98
113
|
export declare function diagnoseTaskDispatch(store: Store, taskId: string, now: Date, readableRepos?: readonly string[] | null): DispatchDiagnosis | null;
|
|
99
114
|
export declare function diagnosisIsDispatchable(diagnosis: DispatchDiagnosis | null): boolean;
|
|
100
115
|
/** Attach the same lifecycle answer to the bounded fleet snapshot used by
|
package/dist/dispatch.js
CHANGED
|
@@ -160,6 +160,31 @@ function approvedProfile(scope) {
|
|
|
160
160
|
return scope.approvedProfile ?? scope.profile ?? null;
|
|
161
161
|
}
|
|
162
162
|
/** The read-side lifecycle answer. Project access limits dependency detail, never the readiness gate. */
|
|
163
|
+
/** The builders that may take work in `repo`, as dispatch decides: registered and not retired (`all`), bound to the
|
|
164
|
+
* project (`eligible`; only the assigned one when a task names one), and checking in now (`alive`). The first-run
|
|
165
|
+
* "Builder connected" step reads the same answer, so it is done only when a task there could start. */
|
|
166
|
+
export function buildersFor(store, repo, now, assignedRunner = null) {
|
|
167
|
+
const all = store.listRunners().filter(one => one.retiredAt === null);
|
|
168
|
+
const bound = all.filter(one => one.repos.includes(repo));
|
|
169
|
+
const eligible = assignedRunner === null ? bound : bound.filter(one => one.name === assignedRunner);
|
|
170
|
+
return { all, eligible, alive: eligible.filter(one => isAlive(one, now)) };
|
|
171
|
+
}
|
|
172
|
+
/** Whether a task in `repo` could start for want of nothing but a person: a live builder bound to it. */
|
|
173
|
+
export function builderReadyFor(store, repo, now) {
|
|
174
|
+
return repo !== null && buildersFor(store, repo, now).alive.length > 0;
|
|
175
|
+
}
|
|
176
|
+
/** The project the first-run "Builder connected" step speaks for, on Chat and the board alike: the open one; else the
|
|
177
|
+
* saved project of the first filed task the viewer may see (All projects); else, only before any task exists, the
|
|
178
|
+
* first managed project. Null when tasks exist but none sits in a project the viewer may see: then nothing says done. */
|
|
179
|
+
export function firstRunBuilderRepo(store, selected, visible, managed) {
|
|
180
|
+
if (selected !== null)
|
|
181
|
+
return selected;
|
|
182
|
+
const placed = store.handle.prepare("SELECT repo FROM task_ref WHERE repo IS NOT NULL ORDER BY id").all();
|
|
183
|
+
const first = placed.find(row => visible(row.repo));
|
|
184
|
+
if (first !== undefined)
|
|
185
|
+
return first.repo;
|
|
186
|
+
return store.hasAnyWork() ? null : managed[0] ?? null;
|
|
187
|
+
}
|
|
163
188
|
export function diagnoseTaskDispatch(store, taskId, now, readableRepos = null) {
|
|
164
189
|
const task = store.getTask(taskId);
|
|
165
190
|
const ref = store.lookupRef(taskId);
|
|
@@ -271,9 +296,7 @@ export function diagnoseTaskDispatch(store, taskId, now, readableRepos = null) {
|
|
|
271
296
|
const gap = missingCapability(store, ref.id, ref.repo, now);
|
|
272
297
|
if (gap !== null)
|
|
273
298
|
return answer("missing-requirement", "waiting", "Missing a requirement", gap, { action: "repair-capability", role });
|
|
274
|
-
const all = store.
|
|
275
|
-
const bound = all.filter(one => one.repos.includes(ref.repo));
|
|
276
|
-
const eligible = ref.assignedRunner === null ? bound : bound.filter(one => one.name === ref.assignedRunner);
|
|
299
|
+
const { all, eligible, alive } = buildersFor(store, ref.repo, now, ref.assignedRunner);
|
|
277
300
|
if (eligible.length === 0) {
|
|
278
301
|
const detail = ref.assignedRunner === null
|
|
279
302
|
? all.length === 0
|
|
@@ -282,7 +305,6 @@ export function diagnoseTaskDispatch(store, taskId, now, readableRepos = null) {
|
|
|
282
305
|
: `This task is assigned to ${ref.assignedRunner}, but that builder is not connected to this project.`;
|
|
283
306
|
return answer("no-worker-registered", "waiting", "Builder not connected", detail, { action: "start-worker", role });
|
|
284
307
|
}
|
|
285
|
-
const alive = eligible.filter(one => isAlive(one, now));
|
|
286
308
|
if (alive.length === 0) {
|
|
287
309
|
return answer("no-worker-online", "waiting", "Builder disconnected", `${eligible.length === 1 ? "The builder for this project has" : "All builders for this project have"} stopped checking in. This task starts automatically when ${eligible.length === 1 ? "it reconnects" : "one reconnects"}.`, { action: "start-worker", role });
|
|
288
310
|
}
|
package/dist/first-run.d.ts
CHANGED
|
@@ -25,7 +25,7 @@ export type FirstRunAction = {
|
|
|
25
25
|
};
|
|
26
26
|
/** `checking`: the answer is not in yet, so the step is shown neither done nor to do. */
|
|
27
27
|
export type FirstRunStep = {
|
|
28
|
-
key: "agent" | "project" | "task";
|
|
28
|
+
key: "agent" | "project" | "builder" | "task";
|
|
29
29
|
title: string;
|
|
30
30
|
done: boolean;
|
|
31
31
|
action: FirstRunAction | null;
|
|
@@ -39,8 +39,11 @@ export type FirstRunFacts = {
|
|
|
39
39
|
firstResultAt: string | null;
|
|
40
40
|
signInCommand?: string;
|
|
41
41
|
};
|
|
42
|
-
/** The
|
|
42
|
+
/** The steps (the builder step is added by withBuilderStep), each done or with the one action that does it; null once the first result has arrived. */
|
|
43
43
|
export declare function firstRunSteps(facts: FirstRunFacts): FirstRunStep[] | null;
|
|
44
|
+
/** The builder step, before the first task: under `toolroll serve` nothing connects a builder, and a task filed then
|
|
45
|
+
* waits forever, so the steps never all read done without one. It leads to Start a builder, which shows the command. */
|
|
46
|
+
export declare function withBuilderStep(steps: FirstRunStep[] | null, connected: boolean): FirstRunStep[] | null;
|
|
44
47
|
export type OpenIssue = {
|
|
45
48
|
number: number;
|
|
46
49
|
title: string;
|
package/dist/first-run.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The first run:
|
|
2
|
+
* The first run: four plain steps to a first result, and three first tasks
|
|
3
3
|
* to try. Everything here is derived from live facts on each render; the
|
|
4
4
|
* list retires permanently with the first Ready result (the
|
|
5
5
|
* 'first-success-at' installation fact). A suggestion only drafts words in
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { run } from "./exec.js";
|
|
9
9
|
import { hardenedGit } from "./teardown-git.js";
|
|
10
10
|
import { githubIssues } from "./issues.js";
|
|
11
|
+
import { START_BUILDER_HREF } from "./needs-you.js";
|
|
11
12
|
export const SIGN_IN_COMMAND = "claude auth login";
|
|
12
13
|
export const SANDBOX_COMMAND = "npx toolroll demo";
|
|
13
14
|
/** How a person starts on a real project (the demo hands off to this). */
|
|
@@ -27,7 +28,7 @@ export function signInCommandFor(states, platform) {
|
|
|
27
28
|
return "codex login";
|
|
28
29
|
return `npm install -g @anthropic-ai/claude-code${platform === "win32" ? ";" : " &&"} ${SIGN_IN_COMMAND}`;
|
|
29
30
|
}
|
|
30
|
-
/** The
|
|
31
|
+
/** The steps (the builder step is added by withBuilderStep), each done or with the one action that does it; null once the first result has arrived. */
|
|
31
32
|
export function firstRunSteps(facts) {
|
|
32
33
|
if (facts.firstResultAt !== null)
|
|
33
34
|
return null;
|
|
@@ -39,6 +40,16 @@ export function firstRunSteps(facts) {
|
|
|
39
40
|
{ key: "task", title: "Your first task", done: facts.hasTask, action: facts.hasTask ? null : { kind: "link", label: "New task", href: "/tasks/new" } },
|
|
40
41
|
];
|
|
41
42
|
}
|
|
43
|
+
/** The builder step, before the first task: under `toolroll serve` nothing connects a builder, and a task filed then
|
|
44
|
+
* waits forever, so the steps never all read done without one. It leads to Start a builder, which shows the command. */
|
|
45
|
+
export function withBuilderStep(steps, connected) {
|
|
46
|
+
if (steps === null)
|
|
47
|
+
return null;
|
|
48
|
+
const builder = { key: "builder", title: "Builder connected", done: connected,
|
|
49
|
+
action: connected ? null : { kind: "link", label: "Start a builder", href: START_BUILDER_HREF } };
|
|
50
|
+
const task = steps.findIndex(one => one.key === "task");
|
|
51
|
+
return task < 0 ? [...steps, builder] : [...steps.slice(0, task), builder, ...steps.slice(task)];
|
|
52
|
+
}
|
|
42
53
|
export const GENERIC_FIRST_TASKS = [
|
|
43
54
|
{ source: "generic", label: "Add a test for an untested function", draft: "Find one function without a test and add a focused test for it." },
|
|
44
55
|
{ source: "generic", label: "Fix a lint warning", draft: "Fix one lint warning without changing behaviour." },
|
package/dist/flow-cadence.d.ts
CHANGED
|
@@ -13,26 +13,32 @@ import { type TriggerIo, type TriggerPass } from "./flow-triggers.js";
|
|
|
13
13
|
import { type ReplyWatch, type ReplyWatchIo } from "./flow-replies.js";
|
|
14
14
|
import { type StepIo, type StepPass } from "./flow-steps.js";
|
|
15
15
|
import type { Store } from "./store.js";
|
|
16
|
+
import { type Git } from "./landed-cleanup.js";
|
|
17
|
+
import { type YouiyouxPass } from "./youiyoux-intake.js";
|
|
16
18
|
/** How often the worker moves cards, checks triggers, reads replies and runs steps, builds or not. */
|
|
17
19
|
export declare const FLOW_EVERY_MS = 15000;
|
|
18
20
|
/** One pass of a kind for a project at a time in this process: a second one asked for while it runs is skipped
|
|
19
21
|
* (`idle`), and the next cadence takes up whatever it would have done. Cards are also claimed in the database,
|
|
20
22
|
* so passes in other processes can't act twice either. */
|
|
21
23
|
export declare function alone<T>(kind: string, project: string, idle: T, run: () => Promise<T>): Promise<T>;
|
|
24
|
+
/** `youiyouxGit`: the hardened git an options request's result is read with (the plane's own runner; gitOf by default). */
|
|
22
25
|
export type FlowIo = {
|
|
23
26
|
triggers: TriggerIo;
|
|
24
27
|
replies: ReplyWatchIo;
|
|
25
28
|
steps: StepIo;
|
|
26
29
|
evidenceRoot?: string;
|
|
30
|
+
youiyouxGit?: Git;
|
|
27
31
|
};
|
|
28
32
|
export type FlowHousekeeping = {
|
|
29
33
|
triggers: TriggerPass;
|
|
34
|
+
youiyoux: YouiyouxPass;
|
|
30
35
|
replies: ReplyWatch;
|
|
31
36
|
steps: StepPass;
|
|
32
37
|
flows: FlowAdvance;
|
|
33
38
|
};
|
|
34
|
-
/** One project's flow pass: triggers (which may start cards),
|
|
35
|
-
* `halted` (a stop or paused admission) skips
|
|
39
|
+
/** One project's flow pass: triggers (which may start cards), youiyoux requests (which file tasks that wait for a
|
|
40
|
+
* person, and move an options request's options to youiyoux's hub and a decision there back), replies (which move waiting cards), steps, then moves. `halted` (a stop or paused admission) skips
|
|
41
|
+
* everything that reaches outside; moves still run. */
|
|
36
42
|
export declare function flowHousekeeping(store: Store, repo: string, clock: () => Date, io: FlowIo, halted: () => boolean): Promise<FlowHousekeeping>;
|
|
37
43
|
/** Move one project's cards. Synchronous, so two can't overlap in a process; each move is checked against the
|
|
38
44
|
* card's visit in its own transaction, so a pass in another process can't move a card twice. */
|
package/dist/flow-cadence.js
CHANGED
|
@@ -12,6 +12,8 @@ import { advanceFlows } from "./flow-engine.js";
|
|
|
12
12
|
import { runFlowTriggers } from "./flow-triggers.js";
|
|
13
13
|
import { watchFlowReplies } from "./flow-replies.js";
|
|
14
14
|
import { runFlowSteps } from "./flow-steps.js";
|
|
15
|
+
import { gitOf } from "./landed-cleanup.js";
|
|
16
|
+
import { NO_YOUIYOUX_PASS, runYouiyoux } from "./youiyoux-intake.js";
|
|
15
17
|
/** How often the worker moves cards, checks triggers, reads replies and runs steps, builds or not. */
|
|
16
18
|
export const FLOW_EVERY_MS = 15_000;
|
|
17
19
|
const running = new Set();
|
|
@@ -30,11 +32,22 @@ export async function alone(kind, project, idle, run) {
|
|
|
30
32
|
running.delete(key);
|
|
31
33
|
}
|
|
32
34
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
+
const NO_REQUESTS = NO_YOUIYOUX_PASS;
|
|
36
|
+
/** One project's flow pass: triggers (which may start cards), youiyoux requests (which file tasks that wait for a
|
|
37
|
+
* person, and move an options request's options to youiyoux's hub and a decision there back), replies (which move waiting cards), steps, then moves. `halted` (a stop or paused admission) skips
|
|
38
|
+
* everything that reaches outside; moves still run. */
|
|
35
39
|
export async function flowHousekeeping(store, repo, clock, io, halted) {
|
|
36
40
|
const triggers = halted() ? { added: 0, checked: 0, problems: [] }
|
|
37
41
|
: await alone("triggers", repo, { added: 0, checked: 0, problems: [] }, () => runFlowTriggers(store, repo, clock(), io.triggers));
|
|
42
|
+
const youiyoux = halted() ? NO_REQUESTS
|
|
43
|
+
: await alone("youiyoux", repo, NO_REQUESTS, async () => {
|
|
44
|
+
try {
|
|
45
|
+
return await runYouiyoux(store, repo, clock(), { dir: io.triggers.dir, env: process.env, git: io.youiyouxGit ?? gitOf(), evidenceRoot: io.evidenceRoot ?? null });
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
return { ...NO_REQUESTS, problems: [`youiyoux: ${error instanceof Error ? error.message : "could not read requests"}`] };
|
|
49
|
+
}
|
|
50
|
+
});
|
|
38
51
|
// One mailbox serves every project, so its reads are one at a time across them all.
|
|
39
52
|
const replies = halted() ? { read: 0, taken: 0, problem: null }
|
|
40
53
|
: await alone("replies", "*", { read: 0, taken: 0, problem: null }, () => watchFlowReplies(store, clock(), io.replies));
|
|
@@ -42,7 +55,7 @@ export async function flowHousekeeping(store, repo, clock, io, halted) {
|
|
|
42
55
|
: await alone("steps", repo, { ran: 0, problems: [] }, () => runFlowSteps(store, repo, clock(), io.steps));
|
|
43
56
|
// Last, so a step that just finished, or a card a trigger just added, moves now.
|
|
44
57
|
const flows = moveCards(store, repo, clock(), io.evidenceRoot);
|
|
45
|
-
return { triggers, replies, steps, flows };
|
|
58
|
+
return { triggers, youiyoux, replies, steps, flows };
|
|
46
59
|
}
|
|
47
60
|
/** Move one project's cards. Synchronous, so two can't overlap in a process; each move is checked against the
|
|
48
61
|
* card's visit in its own transaction, so a pass in another process can't move a card twice. */
|
package/dist/flow-card-task.d.ts
CHANGED
|
@@ -1,2 +1,11 @@
|
|
|
1
1
|
import type { Store } from "./store.js";
|
|
2
|
+
import { type TaskStatus } from "./task-status.js";
|
|
3
|
+
/** A task's status as its page reads it, from the store alone (no evidence files); null when it can't be read. */
|
|
4
|
+
/** `root`: where results' saved evidence is. Without it the receipt is read without its saved checks, so a completion
|
|
5
|
+
* (bound to the receipt read with them) doesn't read as one: a status that must say Complete passes it. */
|
|
6
|
+
export declare function storedTaskStatus(store: Store, taskId: string, repo: string, now: Date, root?: string): {
|
|
7
|
+
status: TaskStatus;
|
|
8
|
+
activeTaskId: string;
|
|
9
|
+
live: ReturnType<Store["getRun"]>;
|
|
10
|
+
} | null;
|
|
2
11
|
export declare function flowCardTaskLine(store: Store, taskId: string, repo: string, now: Date, secrets?: readonly string[]): string;
|
package/dist/flow-card-task.js
CHANGED
|
@@ -19,14 +19,17 @@ function cardReason(text, fallback, secrets) {
|
|
|
19
19
|
return fallback;
|
|
20
20
|
return clean.length > 100 ? `${clean.slice(0, 99).trimEnd()}…` : clean;
|
|
21
21
|
}
|
|
22
|
-
|
|
22
|
+
/** A task's status as its page reads it, from the store alone (no evidence files); null when it can't be read. */
|
|
23
|
+
/** `root`: where results' saved evidence is. Without it the receipt is read without its saved checks, so a completion
|
|
24
|
+
* (bound to the receipt read with them) doesn't read as one: a status that must say Complete passes it. */
|
|
25
|
+
export function storedTaskStatus(store, taskId, repo, now, root) {
|
|
23
26
|
const access = { principal: "operator", repos: [repo], dependencyRepos: [repo] };
|
|
24
|
-
const assignment = assignmentOf(store, taskId, now, access);
|
|
27
|
+
const assignment = assignmentOf(store, taskId, now, access, root);
|
|
25
28
|
if (assignment === null)
|
|
26
|
-
return
|
|
29
|
+
return null;
|
|
27
30
|
const work = taskWorkSummaryOf(store, assignment.activeTaskId, now, access);
|
|
28
31
|
if (work === null)
|
|
29
|
-
return
|
|
32
|
+
return null;
|
|
30
33
|
const live = work.liveRunId === null ? null : store.getRun(work.liveRunId);
|
|
31
34
|
const facts = assignmentStatusFacts(assignment, { work: work.status, planning: live?.role === "planner" });
|
|
32
35
|
// The canvas reads no evidence files. Keep the machine's recorded check visible
|
|
@@ -35,7 +38,13 @@ export function flowCardTaskLine(store, taskId, repo, now, secrets = []) {
|
|
|
35
38
|
// With neither, nothing about the check was read here: the line says nothing about it, never a pass and never an ask.
|
|
36
39
|
if (facts.checks?.status === "unavailable")
|
|
37
40
|
facts.checks = check === null ? null : { ...facts.checks, status: check.status, exitCode: check.exitCode };
|
|
38
|
-
|
|
41
|
+
return { status: taskStatusOf(facts), activeTaskId: assignment.activeTaskId, live };
|
|
42
|
+
}
|
|
43
|
+
export function flowCardTaskLine(store, taskId, repo, now, secrets = []) {
|
|
44
|
+
const read = storedTaskStatus(store, taskId, repo, now);
|
|
45
|
+
if (read === null)
|
|
46
|
+
return "Task unavailable";
|
|
47
|
+
const { status, live } = read;
|
|
39
48
|
if (status.headline === "Needs you") {
|
|
40
49
|
const fallback = status.need?.action.label ?? "Open the task";
|
|
41
50
|
// The question itself is useful; routine approvals are clearer as the short action.
|
|
@@ -44,7 +53,7 @@ export function flowCardTaskLine(store, taskId, repo, now, secrets = []) {
|
|
|
44
53
|
return `Waiting on you: ${what}`;
|
|
45
54
|
}
|
|
46
55
|
if (status.headline === "Building" && live !== null) {
|
|
47
|
-
const ref = store.lookupRef(
|
|
56
|
+
const ref = store.lookupRef(read.activeTaskId);
|
|
48
57
|
const checkpoint = store.latestCheckpointForRun(live.id);
|
|
49
58
|
const progress = buildProgressOf(checkpoint?.taskRef !== ref?.id ? null : checkpoint?.snapshot.milestones.map(one => ({ description: null, state: one.state, note: one.note })));
|
|
50
59
|
if (progress?.stuck != null)
|
package/dist/folder-look.d.ts
CHANGED
|
@@ -51,9 +51,12 @@ export type FolderLookRun = {
|
|
|
51
51
|
};
|
|
52
52
|
/** Starts one look. Production starts a child process; tests pass their own. */
|
|
53
53
|
export type FolderLookRunner = (request: FolderLookRequest, deadlineMs: number) => FolderLookRun;
|
|
54
|
+
/** `started`: when the process that answered was started, in this console's order of starts. A caller that joined a
|
|
55
|
+
* look already under way gets that look's start, never its own, so an older answer always reads as older. */
|
|
54
56
|
export type FolderLook<T> = {
|
|
55
57
|
ok: true;
|
|
56
58
|
value: T;
|
|
59
|
+
started?: number;
|
|
57
60
|
} | {
|
|
58
61
|
ok: false;
|
|
59
62
|
why: "no-answer" | "failed";
|
package/dist/folder-look.js
CHANGED
|
@@ -189,6 +189,8 @@ export const childLook = (request, deadlineMs) => {
|
|
|
189
189
|
/** Looks whose process hasn't ended, by request: callers asking the same thing share one; each holds a child slot
|
|
190
190
|
* until its process is gone. */
|
|
191
191
|
const inFlight = new Map();
|
|
192
|
+
/** Looks started so far: each look's place in that order is fixed when its process starts. */
|
|
193
|
+
let starts = 0;
|
|
192
194
|
/** The same request however its paths were spelled: resolved paths, roots in order. */
|
|
193
195
|
function lookKey(request) {
|
|
194
196
|
const sorted = (paths) => paths.map(one => resolve(one)).sort();
|
|
@@ -215,13 +217,15 @@ export async function lookFolders(request, options = {}) {
|
|
|
215
217
|
if (run === undefined) {
|
|
216
218
|
if (inFlight.size >= FOLDER_LOOK_CHILDREN)
|
|
217
219
|
return { ok: false, why: "no-answer" };
|
|
220
|
+
let started;
|
|
218
221
|
try {
|
|
219
|
-
|
|
222
|
+
started = (options.runner ?? childLook)(request, deadlineMs);
|
|
220
223
|
}
|
|
221
224
|
catch {
|
|
222
225
|
return { ok: false, why: "failed" };
|
|
223
226
|
}
|
|
224
|
-
const mine =
|
|
227
|
+
const mine = { ...started, started: ++starts };
|
|
228
|
+
run = mine;
|
|
225
229
|
inFlight.set(key, mine);
|
|
226
230
|
const forget = () => { if (inFlight.get(key) === mine)
|
|
227
231
|
inFlight.delete(key); };
|
|
@@ -238,7 +242,7 @@ export async function lookFolders(request, options = {}) {
|
|
|
238
242
|
const answer = first.value;
|
|
239
243
|
if (answer === null || typeof answer !== "object" || answer.ok !== true)
|
|
240
244
|
return { ok: false, why: "failed" };
|
|
241
|
-
return { ok: true, value: answer.value };
|
|
245
|
+
return { ok: true, value: answer.value, started: run.started };
|
|
242
246
|
}
|
|
243
247
|
finally {
|
|
244
248
|
clearTimeout(timer);
|
|
@@ -287,7 +291,7 @@ export async function lookIdentities(paths, options = {}) {
|
|
|
287
291
|
if (row !== null && typeof row === "object" && text(row.path) && (row.real === null || text(row.real)))
|
|
288
292
|
out.set(row.path, row.real);
|
|
289
293
|
}
|
|
290
|
-
return { ok: true, value: out };
|
|
294
|
+
return { ok: true, value: out, ...(looked.started === undefined ? {} : { started: looked.started }) };
|
|
291
295
|
}
|
|
292
296
|
/** A project's own `.mcp.json` (parsed object, or null when absent or not one), read off the thread. */
|
|
293
297
|
export async function lookProjectMcp(repo, options = {}) {
|