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
package/dist/spend-ui.js
CHANGED
|
@@ -1,64 +1,99 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The Spend page (
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* with a step-up.
|
|
2
|
+
* The Spend page (docs/plans/spend-dashboard.md, slice 1): what agent work cost this month, with API dollars, plan
|
|
3
|
+
* usage and unknown cost kept apart and never added together, broken down by project, model, role and person, the
|
|
4
|
+
* runs that cost most, and what isn't recorded here. An instance operator's page. Monthly limits show read-only here
|
|
5
|
+
* and change on Settings → Spend, with a step-up.
|
|
6
6
|
*/
|
|
7
7
|
import { html, postForm, reauth } from "./html.js";
|
|
8
8
|
import { budgetLabel, usd } from "./spend.js";
|
|
9
|
+
import { runHref, taskHref } from "./server/http.js";
|
|
9
10
|
const projectName = (repo) => repo.split("/").filter(Boolean).pop() ?? repo;
|
|
10
11
|
export const SPEND_CSS = `.spend{max-width:960px;min-width:0}.spend-head{display:flex;justify-content:space-between;align-items:baseline;gap:12px;flex-wrap:wrap}.spend-head h1{margin:0}` +
|
|
11
|
-
`.spend-month{display:flex;gap:10px;align-items:baseline;font-size:.875rem
|
|
12
|
+
`.spend-month{display:flex;gap:10px;align-items:baseline;flex-wrap:wrap;font-size:.875rem;margin:0}` +
|
|
12
13
|
`.spend h2{font-size:.9375rem;margin:24px 0 8px}.spend table{width:100%;border-collapse:collapse;font-size:.8125rem}.spend th,.spend td{text-align:left;padding:6px 8px 6px 0;border-bottom:1px solid var(--border);overflow-wrap:anywhere}` +
|
|
13
|
-
`.spend th{font-weight:500;color:var(--muted-foreground)}.spend td.n{text-align:right;font-variant-numeric:tabular-nums
|
|
14
|
+
`.spend th{font-weight:500;color:var(--muted-foreground)}.spend td.n,.spend th.n{text-align:right;font-variant-numeric:tabular-nums}.spend td.n,.spend th.n{white-space:nowrap}.spend table.lines td:first-child{overflow-wrap:break-word;width:40%}` +
|
|
15
|
+
`.spend-figures{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px;margin:16px 0 4px}.spend-figure{border:1px solid var(--border);border-radius:10px;padding:12px 14px;min-width:0}` +
|
|
16
|
+
`.spend-figure h2{margin:0;font-size:.8125rem;font-weight:500;color:var(--muted-foreground)}.spend-figure .value{margin:6px 0 0;font-size:1.5rem;font-weight:600;font-variant-numeric:tabular-nums;letter-spacing:-.01em}` +
|
|
17
|
+
`.spend-figure .meta{margin:4px 0 0}.spend-not-counted ul{margin:0;padding-left:20px;font-size:.8125rem}.spend-not-counted li{margin:2px 0}` +
|
|
14
18
|
`.budget{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:4px 12px;padding:10px 0;border-bottom:1px solid var(--border)}.budget .name{font-weight:500;font-size:.875rem}` +
|
|
15
19
|
`.budget .figures{font-size:.8125rem;font-variant-numeric:tabular-nums;text-align:right}.budget .bar{grid-column:1/-1;height:6px;border-radius:3px;background:var(--muted);overflow:hidden}` +
|
|
16
20
|
`.budget .bar span{display:block;height:100%;background:var(--foreground)}.budget.over .bar span{background:var(--danger)}.budget.over .figures{color:var(--danger)}.budget .meta{grid-column:1/-1}` +
|
|
17
21
|
`.budget details{grid-column:1/-1}.spend form.budget-form{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:8px 12px;align-items:end;margin:8px 0}` +
|
|
18
|
-
`.spend form.budget-form label{display:grid;gap:4px;font-size:.8125rem}.spend form.budget-form .choice{display:flex;gap:8px;align-items:center}.spend form.budget-form button{justify-self:start}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
22
|
+
`.spend form.budget-form label{display:grid;gap:4px;font-size:.8125rem}.spend form.budget-form .choice{display:flex;gap:8px;align-items:center}.spend form.budget-form button{justify-self:start}` +
|
|
23
|
+
`.spend-models dl{display:grid;grid-template-columns:auto minmax(0,1fr);gap:4px 16px;margin:0 0 8px;font-size:.875rem}.spend-models dt{color:var(--muted-foreground)}.spend-models dd{margin:0;overflow-wrap:anywhere}` +
|
|
24
|
+
`@media(max-width:640px){.spend-figures{grid-template-columns:minmax(0,1fr)}.spend table.lines thead{display:none}.spend table.lines tr{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:2px 12px;padding:8px 0;border-bottom:1px solid var(--border)}` +
|
|
25
|
+
`.spend table.lines td{border:0;padding:0;white-space:normal}.spend table.lines td:first-child{grid-column:1/-1;font-weight:500;width:auto}.spend table.lines td.n{text-align:left;color:var(--muted-foreground)}.spend table.lines td.n::before{content:attr(data-label);display:block;font-size:.6875rem}}`;
|
|
26
|
+
const ROLE_WORDS = {
|
|
27
|
+
builder: "Builds", repair: "Repairs", planner: "Plans", reviewer: "Reviews", scout: "Scouting",
|
|
28
|
+
lead: "Lead chat", chat: "Chat", subagent: "Subagents", flow: "Flows", evals: "Evals",
|
|
29
|
+
};
|
|
30
|
+
/** What this page doesn't record, in plain words (docs/plans/spend-dashboard.md §2.2). */
|
|
31
|
+
export const NOT_COUNTED = [
|
|
32
|
+
"Sizing a new task (Claude or OpenRouter)",
|
|
33
|
+
"Memory checks that read finished chat sessions",
|
|
34
|
+
"Coding sessions in Code",
|
|
35
|
+
"A flow draft that failed",
|
|
36
|
+
"A Sort step that failed before saving its answer",
|
|
37
|
+
];
|
|
38
|
+
/** Dollars for the page: a positive amount under a cent reads "<$0.01", never a false $0. */
|
|
39
|
+
export const money = (microusd) => microusd > 0 && microusd < 10_000 ? "<$0.01" : usd(microusd);
|
|
40
|
+
const each = (n, one, many = `${one}s`) => `${n} ${n === 1 ? one : many}`;
|
|
41
|
+
/** A line's plan usage in words: the reported usage, or that none was reported. Never $0 for unreported usage. */
|
|
42
|
+
const usageWords = (line) => line.usageCount === 0 ? "—" : line.usageMicrousd > 0 ? `${money(line.usageMicrousd)}${line.usageUnreported > 0 ? ` + ${line.usageUnreported} not reported` : ""}` : "not reported";
|
|
43
|
+
/** A line's unknown cost in words: how many, and a saved charge as an estimate. Never $0. */
|
|
44
|
+
const unknownWords = (line) => {
|
|
45
|
+
const counted = line.estimatedMicrousd > 0 ? `${money(line.estimatedMicrousd)} estimated` : "";
|
|
46
|
+
return line.unknown > 0 ? `${line.unknown}${counted === "" ? "" : ` · ${counted}`}` : counted === "" ? "—" : counted;
|
|
47
|
+
};
|
|
48
|
+
const apiWords = (line) => line.apiMicrousd > 0 ? money(line.apiMicrousd) : "—";
|
|
49
|
+
function linesTable(title, lines, name, top = 12, detail = () => "") {
|
|
50
|
+
if (lines.length === 0)
|
|
37
51
|
return "";
|
|
38
|
-
return html `<section><h2>${title}</h2><table><tbody>${
|
|
52
|
+
return html `<section><h2>${title}</h2><table class="lines"><thead><tr><th></th><th class="n">API dollars</th><th class="n">Plan usage</th><th class="n">Unknown</th></tr></thead><tbody>${lines.slice(0, top).map(line => html `<tr><td>${name(line.key)}${detail(line) === "" ? "" : html ` <span class="meta">${detail(line)}</span>`}</td><td class="n" data-label="API dollars" data-amount-kind="api">${apiWords(line)}</td><td class="n" data-label="Plan usage" data-amount-kind="subscription">${usageWords(line)}</td><td class="n" data-label="Unknown" data-amount-kind="unknown">${unknownWords(line)}</td></tr>`)}</tbody></table></section>`;
|
|
53
|
+
}
|
|
54
|
+
/** A monthly limit's row; `editor` is Settings → Spend's change form, nothing on the Spend page. */
|
|
55
|
+
function limitRow(budget, nameOfSubagent, editor) {
|
|
56
|
+
const width = Math.min(100, Math.max(0, budget.percent));
|
|
57
|
+
const over = budget.spentMicrousd >= budget.limitMicrousd;
|
|
58
|
+
const label = budgetLabel(budget, budget.scope === "subagent" ? nameOfSubagent(budget.key) : undefined).replace(/'s$/, "");
|
|
59
|
+
return html `<div class="budget${over ? " over" : ""}" data-budget="${budget.id}"><span class="name">${label}</span><span class="figures">${money(budget.spentMicrousd)} of ${usd(budget.limitMicrousd)} · ${budget.percent}%</span><span class="bar" aria-hidden="true"><span style="width:${width}%"></span></span><span class="meta">${over && budget.hardStop ? "Used up: new API work waits until next month or a higher limit." : budget.hardStop ? "Pauses new API work at 100%." : "Alerts only."}${budget.unpriced > 0 ? ` ${budget.unpriced} without a price.` : ""}</span>${editor}</div>`;
|
|
39
60
|
}
|
|
40
61
|
export function spendHtml(view, notice) {
|
|
41
62
|
const note = notice.problem ? html `<p class="problem" role="alert">${notice.problem}</p>` : notice.said ? html `<p role="status">${notice.said}</p>` : "";
|
|
42
|
-
const
|
|
43
|
-
const unpriced = view.items.filter(item => item.microusd === null && (item.tokensIn !== null || item.kind !== "run")).length;
|
|
44
|
-
const counts = { run: view.items.filter(item => item.kind === "run").length, subagent: view.items.filter(item => item.kind === "subagent").length, chat: view.items.filter(item => item.kind === "chat").length, sort: view.items.filter(item => item.kind === "sort").length };
|
|
45
|
-
const parts = [`${counts.run} ${counts.run === 1 ? "run" : "runs"}`, ...(counts.subagent > 0 ? [`${counts.subagent} subagent turns`] : []), ...(counts.chat > 0 ? [`${counts.chat} chat turns`] : []), ...(counts.sort > 0 ? [`${counts.sort} ${counts.sort === 1 ? "sort" : "sorts"}`] : [])];
|
|
63
|
+
const { totals } = view.report;
|
|
46
64
|
const nameOfSubagent = (key) => view.subagentNames.get(Number(key)) ?? `Subagent ${key}`;
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
65
|
+
const usageValue = totals.usageCount === 0 ? "None" : totals.usageMicrousd > 0 ? money(totals.usageMicrousd) : "Not reported";
|
|
66
|
+
const usageMeta = totals.usageCount === 0 ? "No plan usage recorded"
|
|
67
|
+
: totals.usageMicrousd > 0 ? `of usage at API prices${totals.usageUnreported > 0 ? ` · ${each(totals.usageUnreported, "piece")} of work not reported` : ""}` : `${each(totals.usageCount, "piece")} of work`;
|
|
68
|
+
const unknownValue = totals.unknown === 0 ? "None" : String(totals.unknown);
|
|
69
|
+
const unknownMeta = [totals.unknown > 0 ? `${totals.unknown === 1 ? "piece" : "pieces"} of work with billing not proved or cost not recorded` : "Nothing with an unknown cost",
|
|
70
|
+
...(totals.estimatedMicrousd > 0 ? [`counted as ${money(totals.estimatedMicrousd)} (estimated)`] : [])].join(" · ");
|
|
71
|
+
const figures = html `<section class="spend-figures" aria-label="This month">\
|
|
72
|
+
<div class="spend-figure" data-amount-kind="api" data-microusd="${totals.apiMicrousd}"><h2>API dollars</h2><p class="value">${money(totals.apiMicrousd)}</p><p class="meta">Recorded charges to API keys</p></div>\
|
|
73
|
+
<div class="spend-figure" data-amount-kind="subscription" data-microusd="${totals.usageMicrousd}" data-unreported="${totals.usageUnreported}"><h2>Plan usage</h2><p class="value">${usageValue}</p><p class="meta">${usageMeta}</p></div>\
|
|
74
|
+
<div class="spend-figure" data-amount-kind="unknown" data-count="${totals.unknown}" data-estimated-microusd="${totals.estimatedMicrousd}"><h2>Unknown</h2><p class="value">${unknownValue}</p><p class="meta">${unknownMeta}</p></div></section>`;
|
|
75
|
+
const limits = view.budgets === null ? "" : html `<section><h2>Monthly limits</h2>${view.budgets.length === 0 ? html `<p class="meta">No monthly limits. <a href="/settings/spend">Set a limit</a></p>`
|
|
76
|
+
: html `${view.budgets.map(budget => limitRow(budget, nameOfSubagent, ""))}<p class="meta"><a href="/settings/spend">Change limits</a> · Evals never count toward a limit.</p>`}${view.windows}</section>`;
|
|
77
|
+
const runs = view.report.topRuns.length === 0 ? "" : html `<section><h2>Most expensive runs</h2><table><thead><tr><th>Task</th><th>Run</th><th>Model</th><th class="n">API dollars</th></tr></thead><tbody>${view.report.topRuns.map(run => html `<tr data-run="${run.runId}"><td><a href="${taskHref(run.taskId)}">${run.taskId}</a>${run.project === null ? "" : html ` <span class="meta">${projectName(run.project)}</span>`}</td><td><a href="${runHref(run.runId)}">${ROLE_WORDS[run.role]} ${run.runId}</a></td><td>${run.provider} · ${run.model}</td><td class="n">${money(run.microusd)}</td></tr>`)}</tbody></table></section>`;
|
|
78
|
+
const notCounted = html `<section class="spend-not-counted" data-not-counted><h2>Not counted here</h2><ul>${NOT_COUNTED.map(one => html `<li>${one}</li>`)}</ul></section>`;
|
|
79
|
+
return html `<article class="spend"><div class="spend-head"><h1>Spend</h1><p class="spend-month"><a href="/spend?month=${view.previous}">← ${view.previous}</a><strong>${view.month}</strong>${view.next === null ? "" : html `<a href="/spend?month=${view.next}">${view.next} →</a>`}<span class="meta">UTC month</span><a href="/spend?month=${view.month}&format=csv" download>CSV</a></p></div>${note}${figures}${limits}${linesTable("By project", view.report.byProject, projectName)}${linesTable("By model", view.report.byModel, key => key, 12, line => line.chosen.length === 0 ? "" : `chosen as ${line.chosen.join(", ")}`)}${linesTable("By role", view.report.byRole, key => ROLE_WORDS[key] ?? key)}${linesTable("By person", view.report.byPerson, key => key)}${runs}${notCounted}</article>`;
|
|
80
|
+
}
|
|
81
|
+
/** Settings → Spend: the monthly limits editor (moved from the Spend page) and a summary of the models in use. */
|
|
82
|
+
export function spendSettingsHtml(view, notice) {
|
|
83
|
+
const note = notice.problem ? html `<p class="problem" role="alert">${notice.problem}</p>` : notice.said ? html `<p role="status">${notice.said}</p>` : "";
|
|
84
|
+
const nameOfSubagent = (key) => view.subagentNames.get(Number(key)) ?? `Subagent ${key}`;
|
|
85
|
+
const password = reauth(html `<label>Your Toolroll password<input type="password" name="password" autocomplete="current-password"></label>`);
|
|
86
|
+
const budgets = view.budgets.map(budget => limitRow(budget, nameOfSubagent, html `<details><summary>Change</summary>${postForm("/spend/budget", html `<label>Monthly limit (US dollars)<input type="number" name="usd" min="1" step="1" value="${Math.round(budget.limitMicrousd / 1_000_000)}" required></label><label class="choice"><input type="checkbox" name="stop" value="1"${budget.hardStop ? html ` checked` : ""}> Pause new API work at 100%</label>${password}<button type="submit" name="action" value="save">Save</button> <button type="submit" name="action" value="remove">Remove</button>`, { attrs: { class: "budget-form" }, hidden: { target: `${budget.scope}:${budget.key}` } })}</details>`));
|
|
53
87
|
const groups = ["Everything", "Projects", "People", "Subagents"];
|
|
54
88
|
const options = groups.map(group => {
|
|
55
89
|
const members = view.targets.filter(one => one.group === group);
|
|
56
90
|
return members.length === 0 ? "" : html `<optgroup label="${group}">${members.map(one => html `<option value="${one.value}">${one.label}</option>`)}</optgroup>`;
|
|
57
91
|
});
|
|
58
|
-
const add = html `<details${view.budgets.length === 0 ? html ` open` : ""}><summary>Add a
|
|
59
|
-
|
|
92
|
+
const add = html `<details${view.budgets.length === 0 ? html ` open` : ""}><summary>Add a limit</summary>${postForm("/spend/budget", html `<label>For<select name="target" required>${options}</select></label><label>Monthly limit (US dollars)<input type="number" name="usd" min="1" step="1" required></label><label class="choice"><input type="checkbox" name="stop" value="1" checked> Pause new API work at 100%</label>${password}<button type="submit" name="action" value="save">Add limit</button>`, { attrs: { class: "budget-form" } })}</details>`;
|
|
93
|
+
const models = html `<section class="spend-models"><h2>Models</h2>${view.models.length === 0 ? "" : html `<dl>${view.models.map(one => html `<dt>${one.role}</dt><dd>${one.model}</dd>`)}</dl>`}<p><a href="/settings/models">Change models</a></p></section>`;
|
|
94
|
+
return html `<article class="spend"><div class="spend-head"><h1>Spend</h1><p class="spend-month"><a href="/spend">See this month's spend</a></p></div>${note}<section><h2>Monthly limits</h2>${budgets.length === 0 ? html `<p class="meta">No monthly limits yet.</p>` : budgets}${add}</section>${models}</article>`;
|
|
60
95
|
}
|
|
61
|
-
/** One row per piece of spend, for a spreadsheet. */
|
|
96
|
+
/** One row per piece of spend, for a spreadsheet (the command line's `spend --csv`). */
|
|
62
97
|
export function spendCsv(items, subagentNames) {
|
|
63
98
|
const cell = (value) => {
|
|
64
99
|
const text = value === null || value === undefined ? "" : String(value);
|
package/dist/store.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type FeedbackImage } from "./result-review.js";
|
|
1
2
|
import { type WritesMode } from "./writes-mode.js";
|
|
2
3
|
import { type CheckAccess } from "./check-access.js";
|
|
3
4
|
import { type ProjectSandbox } from "./project-sandbox.js";
|
|
@@ -139,6 +140,18 @@ export declare const V141_ADDED_COLUMNS: Readonly<Record<string, Readonly<Record
|
|
|
139
140
|
definition: string;
|
|
140
141
|
saved: string;
|
|
141
142
|
}>>>>;
|
|
143
|
+
/** v142 (youiyoux-intake.ts): youiyoux change requests as tasks. `youiyoux_intake` is one row per project the intake
|
|
144
|
+
* has looked at: a person's on/off choice (`enabled` NULL: automatic, on while the project uses youiyoux) and the last
|
|
145
|
+
* pass's answer (whether the project uses youiyoux, how many request files couldn't be read, and the problem, if any).
|
|
146
|
+
* `youiyoux_request` is one row per request taken, keyed by project and request id: the task it became (NULL: left
|
|
147
|
+
* out, `note` says why), never filed again, the status last written beside it, with the console address its links
|
|
148
|
+
* named ('' for none), and where the person pointed (route, component, file:line), cleaned, for the task page. An
|
|
149
|
+
* options request (`kind`) also keeps the options file its agent writes (`proposal`, p-<its hex>), set when filed and
|
|
150
|
+
* never read from the request again: whether it went to the project's main checkout from which result (`proposal_state`,
|
|
151
|
+
* `proposal_run`, and why not in `proposal_note`; `taken`: a file was already there, so it was left and is never read
|
|
152
|
+
* for a decision), the digest of what was copied apart from what a decision changes (`proposal_digest`), and what a
|
|
153
|
+
* person decided on it in youiyoux's hub (`decided`, `decided_note`: what Toolroll did about it). New tables only. */
|
|
154
|
+
export declare const V142_SCHEMA = "\nCREATE TABLE IF NOT EXISTS youiyoux_intake (\n repo TEXT PRIMARY KEY,\n enabled INTEGER CHECK (enabled IS NULL OR enabled IN (0, 1)),\n uses INTEGER NOT NULL DEFAULT 0 CHECK (uses IN (0, 1)),\n changed_by TEXT,\n changed_at TEXT,\n checked_at TEXT,\n invalid INTEGER NOT NULL DEFAULT 0 CHECK (invalid >= 0),\n problem TEXT\n);\nCREATE TABLE IF NOT EXISTS youiyoux_request (\n repo TEXT NOT NULL,\n request TEXT NOT NULL CHECK (length(request) = 12 AND request GLOB 'r-[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]'),\n task_id TEXT,\n note TEXT,\n filed_at TEXT NOT NULL,\n status TEXT CHECK (status IS NULL OR status IN ('queued','needs-you','building','ready','done','failed','stopped')),\n detail TEXT,\n status_at TEXT,\n written_at TEXT,\n written_base TEXT,\n write_problem TEXT,\n route TEXT,\n component TEXT,\n source_at TEXT,\n kind TEXT NOT NULL DEFAULT 'change' CHECK (kind IN ('change','options')),\n proposal TEXT CHECK (proposal IS NULL OR (length(proposal) = 12 AND proposal GLOB 'p-[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]')),\n proposal_state TEXT CHECK (proposal_state IS NULL OR proposal_state IN ('copied','taken','missing','unsafe','retry')),\n proposal_digest TEXT CHECK (proposal_digest IS NULL OR (length(proposal_digest) = 64 AND proposal_digest NOT GLOB '*[^0-9a-f]*')),\n proposal_run INTEGER,\n proposal_note TEXT,\n proposal_at TEXT,\n decided TEXT CHECK (decided IS NULL OR decided IN ('chosen','dismissed')),\n decided_at TEXT,\n decided_note TEXT,\n PRIMARY KEY (repo, request),\n UNIQUE (task_id),\n CHECK ((kind = 'options') = (proposal IS NOT NULL)),\n CHECK (proposal_state IS NULL OR kind = 'options'),\n CHECK (decided IS NULL OR coalesce(proposal_state = 'copied' AND proposal_digest IS NOT NULL, 0))\n);\n";
|
|
142
155
|
/** A project's "Show commands in the live view" (live-detail.ts): absent means off. */
|
|
143
156
|
export declare const LIVE_DETAIL_SCHEMA = "\nCREATE TABLE IF NOT EXISTS live_detail_setting (\n repo TEXT PRIMARY KEY,\n commands INTEGER NOT NULL CHECK (commands IN (0,1)),\n updated_by TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\n-- Every run a steering note reached, kept as it happens (live-notes.ts): a\n-- note's own attached/mailed fields move to the next attempt, this does not.\nCREATE TABLE IF NOT EXISTS steer_note_run (\n note INTEGER NOT NULL REFERENCES task_steer(id) ON DELETE CASCADE,\n run INTEGER NOT NULL REFERENCES run(id) ON DELETE CASCADE,\n how TEXT NOT NULL CHECK (how IN ('attached','mailed','read')),\n at TEXT NOT NULL,\n PRIMARY KEY (note, run, how)\n);\nCREATE INDEX IF NOT EXISTS steer_note_run_by_run ON steer_note_run (run, note);\n";
|
|
144
157
|
export declare const CHAT_APPROVAL_SCHEMA = "\nCREATE TABLE IF NOT EXISTS chat_approval_setting (\n approver TEXT NOT NULL,\n scope TEXT NOT NULL,\n enabled INTEGER NOT NULL CHECK (enabled IN (0,1)),\n full_access INTEGER NOT NULL DEFAULT 0 CHECK (full_access IN (0,1)),\n attempt_cap_microusd INTEGER CHECK (attempt_cap_microusd IS NULL OR attempt_cap_microusd >= 0),\n generation INTEGER NOT NULL,\n digest TEXT NOT NULL,\n updated_by TEXT NOT NULL,\n updated_at TEXT NOT NULL,\n PRIMARY KEY (approver, scope)\n);\n";
|
|
@@ -219,7 +232,7 @@ export type SealedLedgerEntry = LedgerEntry & {
|
|
|
219
232
|
hash: string;
|
|
220
233
|
} | null;
|
|
221
234
|
};
|
|
222
|
-
export declare const SCHEMA_VERSION =
|
|
235
|
+
export declare const SCHEMA_VERSION = 143;
|
|
223
236
|
/** v118: the tables merged into `credential`, in the order they move (a person's API tokens keep their ids first). */
|
|
224
237
|
export declare const V118_MERGED_TABLES: Readonly<Record<string, CredentialKind>>;
|
|
225
238
|
/**
|
|
@@ -376,6 +389,11 @@ export declare const V140_ADDED_COLUMNS: Readonly<Record<string, Readonly<Record
|
|
|
376
389
|
definition: string;
|
|
377
390
|
saved: string;
|
|
378
391
|
}>>>>;
|
|
392
|
+
/** v143: optional screenshot context on result feedback; existing feedback stays unchanged (saved rows read NULL). */
|
|
393
|
+
export declare const V143_ADDED_COLUMNS: Readonly<Record<string, Readonly<Record<string, {
|
|
394
|
+
definition: string;
|
|
395
|
+
saved: string;
|
|
396
|
+
}>>>>;
|
|
379
397
|
/**
|
|
380
398
|
* v135: one approval per release (docs/plans/release-once.md). `release_authority` is the single source of truth for a
|
|
381
399
|
* release a person approved: one row per generation, changed only by compare-and-swap on `rev`, with a lease whose
|
|
@@ -2406,8 +2424,9 @@ export declare class Store {
|
|
|
2406
2424
|
sealScopeApproval(taskId: string, by: string, now: Date, mutation?: Mutation, basis?: {
|
|
2407
2425
|
kind: "mode" | "chat" | "grant" | "schedule" | "auto-check" | "auto-green";
|
|
2408
2426
|
modeDigest: string;
|
|
2409
|
-
/** A chat seal of a deploy gate: the release card it came from (chat-management.md §0a.5).
|
|
2410
|
-
|
|
2427
|
+
/** A chat seal of a deploy gate: the release card it came from (chat-management.md §0a.5). A chat seal of a task
|
|
2428
|
+
* filed from outside text: `terms`, and the digest of every word the card showed (chat-terms.ts). */
|
|
2429
|
+
card?: "release" | "terms";
|
|
2411
2430
|
cardDigest?: string;
|
|
2412
2431
|
}, approverKind?: ApproverKind): boolean;
|
|
2413
2432
|
/** Whether this task's scope carries a LIVE seal: approved, and the
|
|
@@ -3207,6 +3226,7 @@ export declare class Store {
|
|
|
3207
3226
|
author: string;
|
|
3208
3227
|
/** Ingested comments carry their origin id — the idempotency key (M8.17). */
|
|
3209
3228
|
sourceKey?: string;
|
|
3229
|
+
image?: FeedbackImage | null;
|
|
3210
3230
|
}, now: Date): number | null;
|
|
3211
3231
|
/** Live (unsuperseded, unconsumed) comments on a run's terminal diff. */
|
|
3212
3232
|
liveDiffComments(runId: number): DiffComment[];
|
|
@@ -3405,6 +3425,15 @@ export declare class Store {
|
|
|
3405
3425
|
entry: number;
|
|
3406
3426
|
sourceJson: string;
|
|
3407
3427
|
} | null;
|
|
3428
|
+
/** v142: where a task filed from a youiyoux change or options request came from, as kept when it was filed
|
|
3429
|
+
* (youiyoux-intake.ts; a revision reads its first version's): the request, its kind, and where the person pointed. Null for every other task. */
|
|
3430
|
+
youiyouxOriginOf(taskId: string): {
|
|
3431
|
+
request: string;
|
|
3432
|
+
kind: "change" | "options";
|
|
3433
|
+
route: string | null;
|
|
3434
|
+
component: string | null;
|
|
3435
|
+
at: string | null;
|
|
3436
|
+
} | null;
|
|
3408
3437
|
/**
|
|
3409
3438
|
* Seal a revision in ONE transaction (Codex M5-M8 audit, C-3; contract
|
|
3410
3439
|
* handoff task 2): the SOURCE BINDING is proven here, against live rows,
|
|
@@ -7376,6 +7405,7 @@ export type DiffComment = {
|
|
|
7376
7405
|
id: number;
|
|
7377
7406
|
artifact: number;
|
|
7378
7407
|
artifactSha: string;
|
|
7408
|
+
image?: FeedbackImage | null;
|
|
7379
7409
|
run: number;
|
|
7380
7410
|
path: string | null;
|
|
7381
7411
|
line: number | null;
|
|
@@ -8021,3 +8051,16 @@ export declare function sealStoreMoneyMethods(prototype: object): void;
|
|
|
8021
8051
|
* when the prototype's value is the very reference recorded at the seal. `prototype` is Store's own unless a test
|
|
8022
8052
|
* says otherwise; either way nothing but the recorded references is ever installed. */
|
|
8023
8053
|
export declare function lockStoreMoneyMethods(store: object, prototype?: object): void;
|
|
8054
|
+
/** Takes a saved note's screenshot off without losing the note or its history (v143). Fail closed, in one write
|
|
8055
|
+
* transaction: the note must be live and unconsumed on this run, its screenshot must be genuinely unusable right now
|
|
8056
|
+
* (`unusable` is result-actions.ts feedbackImageProblem, re-read inside the transaction, never the page's say-so), and
|
|
8057
|
+
* the change is recorded in the action ledger under the person who made it. The note is superseded by an exact
|
|
8058
|
+
* imageless copy; the original stays in history. The caller has already proved the person may request changes on
|
|
8059
|
+
* this run (the same authority that seals any saved note into a revision); that standing is re-proved here too. */
|
|
8060
|
+
export declare function dropFeedbackImage(store: Store, runId: number, commentId: number, actor: string, unusable: (image: FeedbackImage) => string | null, now: Date): {
|
|
8061
|
+
ok: true;
|
|
8062
|
+
id: number;
|
|
8063
|
+
} | {
|
|
8064
|
+
ok: false;
|
|
8065
|
+
reason: "not-found" | "image-usable" | "not-authorized";
|
|
8066
|
+
};
|
package/dist/store.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { verifyResultImage } from "./chat-evidence.js";
|
|
2
|
+
import { parseFeedbackImage } from "./result-review.js";
|
|
1
3
|
import { mintSecret } from "./secret-mint.js";
|
|
2
4
|
import { WRITES_DEFAULT } from "./writes-mode.js";
|
|
3
5
|
import { checkAccessOf, widerThan } from "./check-access.js";
|
|
@@ -107,6 +109,8 @@ import { configBase, envValue, namedPath } from "./names.js";
|
|
|
107
109
|
import { PULL_REQUEST_SCHEMA } from "./pull-request-schema.js";
|
|
108
110
|
import { chatTurnAmountOf, combineReportedModels, providerTurnsOf, reportedCostUsdOf, reportedModelOf, runAmountOf } from "./reported-usage.js";
|
|
109
111
|
import { usageOf } from "./run-efficiency.js";
|
|
112
|
+
import { YOUIYOUX_FILED_VIA } from "./contracts/youiyoux-request.js";
|
|
113
|
+
import { shownTermsDigest } from "./chat-terms.js";
|
|
110
114
|
// PR #263 follow-up: the built-ins the Store money seal uses, captured as this module is evaluated, so code that later
|
|
111
115
|
// replaces `Object.defineProperty` and the like can't fool the seal or the lock (lockStoreMoneyMethods refuses when
|
|
112
116
|
// they've been replaced). The four sealed method references are recorded once, at the seal, in SEALED_MONEY_METHODS.
|
|
@@ -717,6 +721,60 @@ export const V141_ADDED_COLUMNS = Object.freeze({
|
|
|
717
721
|
},
|
|
718
722
|
chat_turn: { billing_reported: { definition: "TEXT CHECK (billing_reported IS NULL OR billing_reported IN ('subscription','api-key'))", saved: "NULL" } },
|
|
719
723
|
});
|
|
724
|
+
/** v142 (youiyoux-intake.ts): youiyoux change requests as tasks. `youiyoux_intake` is one row per project the intake
|
|
725
|
+
* has looked at: a person's on/off choice (`enabled` NULL: automatic, on while the project uses youiyoux) and the last
|
|
726
|
+
* pass's answer (whether the project uses youiyoux, how many request files couldn't be read, and the problem, if any).
|
|
727
|
+
* `youiyoux_request` is one row per request taken, keyed by project and request id: the task it became (NULL: left
|
|
728
|
+
* out, `note` says why), never filed again, the status last written beside it, with the console address its links
|
|
729
|
+
* named ('' for none), and where the person pointed (route, component, file:line), cleaned, for the task page. An
|
|
730
|
+
* options request (`kind`) also keeps the options file its agent writes (`proposal`, p-<its hex>), set when filed and
|
|
731
|
+
* never read from the request again: whether it went to the project's main checkout from which result (`proposal_state`,
|
|
732
|
+
* `proposal_run`, and why not in `proposal_note`; `taken`: a file was already there, so it was left and is never read
|
|
733
|
+
* for a decision), the digest of what was copied apart from what a decision changes (`proposal_digest`), and what a
|
|
734
|
+
* person decided on it in youiyoux's hub (`decided`, `decided_note`: what Toolroll did about it). New tables only. */
|
|
735
|
+
export const V142_SCHEMA = `
|
|
736
|
+
CREATE TABLE IF NOT EXISTS youiyoux_intake (
|
|
737
|
+
repo TEXT PRIMARY KEY,
|
|
738
|
+
enabled INTEGER CHECK (enabled IS NULL OR enabled IN (0, 1)),
|
|
739
|
+
uses INTEGER NOT NULL DEFAULT 0 CHECK (uses IN (0, 1)),
|
|
740
|
+
changed_by TEXT,
|
|
741
|
+
changed_at TEXT,
|
|
742
|
+
checked_at TEXT,
|
|
743
|
+
invalid INTEGER NOT NULL DEFAULT 0 CHECK (invalid >= 0),
|
|
744
|
+
problem TEXT
|
|
745
|
+
);
|
|
746
|
+
CREATE TABLE IF NOT EXISTS youiyoux_request (
|
|
747
|
+
repo TEXT NOT NULL,
|
|
748
|
+
request TEXT NOT NULL CHECK (length(request) = 12 AND request GLOB 'r-[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]'),
|
|
749
|
+
task_id TEXT,
|
|
750
|
+
note TEXT,
|
|
751
|
+
filed_at TEXT NOT NULL,
|
|
752
|
+
status TEXT CHECK (status IS NULL OR status IN ('queued','needs-you','building','ready','done','failed','stopped')),
|
|
753
|
+
detail TEXT,
|
|
754
|
+
status_at TEXT,
|
|
755
|
+
written_at TEXT,
|
|
756
|
+
written_base TEXT,
|
|
757
|
+
write_problem TEXT,
|
|
758
|
+
route TEXT,
|
|
759
|
+
component TEXT,
|
|
760
|
+
source_at TEXT,
|
|
761
|
+
kind TEXT NOT NULL DEFAULT 'change' CHECK (kind IN ('change','options')),
|
|
762
|
+
proposal TEXT CHECK (proposal IS NULL OR (length(proposal) = 12 AND proposal GLOB 'p-[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]')),
|
|
763
|
+
proposal_state TEXT CHECK (proposal_state IS NULL OR proposal_state IN ('copied','taken','missing','unsafe','retry')),
|
|
764
|
+
proposal_digest TEXT CHECK (proposal_digest IS NULL OR (length(proposal_digest) = 64 AND proposal_digest NOT GLOB '*[^0-9a-f]*')),
|
|
765
|
+
proposal_run INTEGER,
|
|
766
|
+
proposal_note TEXT,
|
|
767
|
+
proposal_at TEXT,
|
|
768
|
+
decided TEXT CHECK (decided IS NULL OR decided IN ('chosen','dismissed')),
|
|
769
|
+
decided_at TEXT,
|
|
770
|
+
decided_note TEXT,
|
|
771
|
+
PRIMARY KEY (repo, request),
|
|
772
|
+
UNIQUE (task_id),
|
|
773
|
+
CHECK ((kind = 'options') = (proposal IS NOT NULL)),
|
|
774
|
+
CHECK (proposal_state IS NULL OR kind = 'options'),
|
|
775
|
+
CHECK (decided IS NULL OR coalesce(proposal_state = 'copied' AND proposal_digest IS NOT NULL, 0))
|
|
776
|
+
);
|
|
777
|
+
`;
|
|
720
778
|
/** A project's "Show commands in the live view" (live-detail.ts): absent means off. */
|
|
721
779
|
export const LIVE_DETAIL_SCHEMA = `
|
|
722
780
|
CREATE TABLE IF NOT EXISTS live_detail_setting (
|
|
@@ -1030,7 +1088,11 @@ const REGISTERED_AT_FILING = "again, as it was filed";
|
|
|
1030
1088
|
// v141 adds, and changes no saved row: the eval set's records (docs/plans/evals-nightly.md, part C) — campaigns,
|
|
1031
1089
|
// attempts, each attempt's provider dispatches, the baseline pin and eval debt (V141_SCHEMA) — and how a run's or chat
|
|
1032
1090
|
// request's billing was known (V141_ADDED_COLUMNS; saved rows NULL: unknown).
|
|
1033
|
-
|
|
1091
|
+
// v142 adds, and changes no saved row: youiyoux change requests as tasks (V142_SCHEMA, new tables only) — each
|
|
1092
|
+
// project's intake choice and last answer, and each request taken, with the task it became.
|
|
1093
|
+
// v143 adds, and changes no saved row: optional exact-screenshot context on result feedback
|
|
1094
|
+
// (diff_comment.image_json, V143_ADDED_COLUMNS; saved notes read NULL and keep their file/line target).
|
|
1095
|
+
export const SCHEMA_VERSION = 143;
|
|
1034
1096
|
/** v118: the tables merged into `credential`, in the order they move (a person's API tokens keep their ids first). */
|
|
1035
1097
|
export const V118_MERGED_TABLES = Object.freeze({ api_token: "person", coordinator_credential: "coordinator", lead_credential: "lead" });
|
|
1036
1098
|
/** The foreign keys that named a merged table, exactly as their DDL wrote them; each names `credential(id)` instead. */
|
|
@@ -1163,7 +1225,7 @@ const V115_DROPPED_REFERENCES = /\s+REFERENCES (?:contestant|contest|tournament_
|
|
|
1163
1225
|
* unchanged), and adds the nullable column in V125_ADDED_COLUMNS, checked the same way. v126 only adds a new table
|
|
1164
1226
|
* (task_source) and the columns in V126_ADDED_COLUMNS (null or 0), checked the same way.
|
|
1165
1227
|
*/
|
|
1166
|
-
export const UPDATE_SAFE_MIGRATIONS = Object.freeze([108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141]);
|
|
1228
|
+
export const UPDATE_SAFE_MIGRATIONS = Object.freeze([108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143]);
|
|
1167
1229
|
/** v119: the columns it adds, by table, with the value every row saved before it reads (toolroll-update's HISTORY_RULES). */
|
|
1168
1230
|
export const V119_ADDED_COLUMNS = Object.freeze({
|
|
1169
1231
|
task_ref: { planning_generation: { definition: "INTEGER NOT NULL DEFAULT 0", saved: "0" } },
|
|
@@ -1299,6 +1361,10 @@ export const V140_ADDED_COLUMNS = Object.freeze({
|
|
|
1299
1361
|
reported_cost_usd: { definition: "REAL CHECK (reported_cost_usd IS NULL OR reported_cost_usd >= 0)", saved: "NULL" },
|
|
1300
1362
|
},
|
|
1301
1363
|
});
|
|
1364
|
+
/** v143: optional screenshot context on result feedback; existing feedback stays unchanged (saved rows read NULL). */
|
|
1365
|
+
export const V143_ADDED_COLUMNS = Object.freeze({
|
|
1366
|
+
diff_comment: { image_json: { definition: "TEXT", saved: "NULL" } },
|
|
1367
|
+
});
|
|
1302
1368
|
/**
|
|
1303
1369
|
* v135: one approval per release (docs/plans/release-once.md). `release_authority` is the single source of truth for a
|
|
1304
1370
|
* release a person approved: one row per generation, changed only by compare-and-swap on `rev`, with a lease whose
|
|
@@ -4549,6 +4615,13 @@ CREATE INDEX IF NOT EXISTS run_checkpoint_by_task ON run_checkpoint (task_ref, i
|
|
|
4549
4615
|
for (const [column, { definition }] of Object.entries(columns))
|
|
4550
4616
|
addColumn(db, table, column, definition);
|
|
4551
4617
|
}
|
|
4618
|
+
// v142: youiyoux change requests as tasks (new tables only).
|
|
4619
|
+
db.exec(V142_SCHEMA);
|
|
4620
|
+
// v143: optional exact-screenshot context on result feedback (V143_ADDED_COLUMNS: saved notes read NULL).
|
|
4621
|
+
for (const [table, columns] of Object.entries(V143_ADDED_COLUMNS)) {
|
|
4622
|
+
for (const [column, { definition }] of Object.entries(columns))
|
|
4623
|
+
addColumn(db, table, column, definition);
|
|
4624
|
+
}
|
|
4552
4625
|
// Keep the epoch until reclamation succeeds, including a retry after compaction already committed. A new file has
|
|
4553
4626
|
// nothing to reclaim.
|
|
4554
4627
|
if (!fresh)
|
|
@@ -7282,6 +7355,14 @@ export class Store {
|
|
|
7282
7355
|
.get(taskId);
|
|
7283
7356
|
if (writtenByLead !== undefined)
|
|
7284
7357
|
return false;
|
|
7358
|
+
// THE OUTSIDE-REQUEST QUARANTINE (youiyoux-intake.ts): a task filed from a youiyoux change request is text
|
|
7359
|
+
// anyone who can write the project's folder wrote, so only a person who read every word of it approves it.
|
|
7360
|
+
// Its provenance is immutable, so no road around this exists. A mode, a grant, a schedule, auto-check and
|
|
7361
|
+
// auto-green (approvals no person read) never seal it. A person's own ceremony does. So does their paired-
|
|
7362
|
+
// chat tap, only with the digest of every word the card showed (`terms`, chat-terms.ts), recomputed below
|
|
7363
|
+
// from the live scope: a card that cut a word, or terms that changed since, seal nothing.
|
|
7364
|
+
if (basis.kind !== "chat" && this.filedViaOf(taskId) === YOUIYOUX_FILED_VIA)
|
|
7365
|
+
return false;
|
|
7285
7366
|
// CHECK ACCESS (v132, check-access.ts): a scope whose checks reach beyond the fence (sign-ins, or no sandbox at
|
|
7286
7367
|
// all) seals only on a person's password ceremony — never a mode, a grant, a chat confirmation or a carry.
|
|
7287
7368
|
const profileText = this.db.prepare("SELECT profile_json FROM task_scope WHERE task_id = ?").get(taskId)?.["profile_json"];
|
|
@@ -7348,6 +7429,10 @@ export class Store {
|
|
|
7348
7429
|
return false;
|
|
7349
7430
|
if (deployGate && basis?.kind === "chat" && !(basis.card === "release" && typeof basis.cardDigest === "string" && /^[0-9a-f]{64}$/.test(basis.cardDigest)))
|
|
7350
7431
|
return false;
|
|
7432
|
+
// The outside-request quarantine's chat road (above): the card showed every word of these live terms.
|
|
7433
|
+
if (basis?.kind === "chat" && this.filedViaOf(taskId) === YOUIYOUX_FILED_VIA
|
|
7434
|
+
&& !(basis.card === "terms" && typeof basis.cardDigest === "string" && basis.cardDigest === shownTermsDigest(readScope(working))))
|
|
7435
|
+
return false;
|
|
7351
7436
|
// AUTOMATIC RELEASE SEALS (auto-approve-green.md): only on a release task (a candidate and release terms signed
|
|
7352
7437
|
// in), only naming the evidence row the plane wrote for exactly this task, kind, digest and candidate.
|
|
7353
7438
|
const automatic = basis?.kind === "auto-check" || basis?.kind === "auto-green";
|
|
@@ -9535,13 +9620,21 @@ export class Store {
|
|
|
9535
9620
|
if (artifact === undefined || artifact.kind !== "terminal-diff") {
|
|
9536
9621
|
throw new Error(`artifact ${args.artifactId} is not run ${args.runId}'s terminal diff — comments bind to the exact bytes reviewed`);
|
|
9537
9622
|
}
|
|
9623
|
+
const image = parseFeedbackImage(args.image);
|
|
9624
|
+
if (image === false)
|
|
9625
|
+
throw new Error("Invalid feedback image.");
|
|
9626
|
+
if (image !== null) {
|
|
9627
|
+
const shot = this.artifactForRun(args.runId, image.artifact);
|
|
9628
|
+
if (image.run !== args.runId || shot?.kind !== "screenshot" || shot.sha256 !== image.sha256)
|
|
9629
|
+
throw new Error("The feedback image does not belong to this result.");
|
|
9630
|
+
}
|
|
9538
9631
|
// The unique index is the real concurrent guard (audit C-9): a race on
|
|
9539
9632
|
// the same source key lands in DO NOTHING, not in a thrown constraint.
|
|
9540
9633
|
const inserted = this.db
|
|
9541
|
-
.prepare(`INSERT INTO diff_comment (artifact, artifact_sha, run, path, line, note, author, created_at, source_key)
|
|
9542
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
9634
|
+
.prepare(`INSERT INTO diff_comment (artifact, artifact_sha, run, path, line, note, author, created_at, source_key, image_json)
|
|
9635
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
9543
9636
|
ON CONFLICT (source_key) WHERE source_key IS NOT NULL DO NOTHING`)
|
|
9544
|
-
.run(args.artifactId, artifact.sha256, args.runId, args.path, args.line, args.note, args.author, now.toISOString(), args.sourceKey ?? null);
|
|
9637
|
+
.run(args.artifactId, artifact.sha256, args.runId, args.path, args.line, args.note, args.author, now.toISOString(), args.sourceKey ?? null, image === null ? null : JSON.stringify(image));
|
|
9545
9638
|
return Number(inserted.changes) === 0 ? null : Number(inserted.lastInsertRowid);
|
|
9546
9639
|
}
|
|
9547
9640
|
/** Live (unsuperseded, unconsumed) comments on a run's terminal diff. */
|
|
@@ -10229,6 +10322,19 @@ export class Store {
|
|
|
10229
10322
|
const row = this.db.prepare("SELECT flow, card, card_entry, source_json FROM task_source WHERE task_id = ?").get(taskId);
|
|
10230
10323
|
return row === undefined ? null : { flow: Number(row["flow"]), card: Number(row["card"]), entry: Number(row["card_entry"]), sourceJson: String(row["source_json"]) };
|
|
10231
10324
|
}
|
|
10325
|
+
/** v142: where a task filed from a youiyoux change or options request came from, as kept when it was filed
|
|
10326
|
+
* (youiyoux-intake.ts; a revision reads its first version's): the request, its kind, and where the person pointed. Null for every other task. */
|
|
10327
|
+
youiyouxOriginOf(taskId) {
|
|
10328
|
+
const text = (value) => typeof value === "string" && value !== "" ? value : null;
|
|
10329
|
+
let id = taskId;
|
|
10330
|
+
for (let hop = 0; id !== null && hop < 20; hop++) {
|
|
10331
|
+
const row = this.db.prepare("SELECT request, kind, route, component, source_at FROM youiyoux_request WHERE task_id = ?").get(id);
|
|
10332
|
+
if (row !== undefined)
|
|
10333
|
+
return { request: String(row["request"]), kind: row["kind"] === "options" ? "options" : "change", route: text(row["route"]), component: text(row["component"]), at: text(row["source_at"]) };
|
|
10334
|
+
id = this.lookupRef(id)?.revisionOf ?? null;
|
|
10335
|
+
}
|
|
10336
|
+
return null;
|
|
10337
|
+
}
|
|
10232
10338
|
// ---- the revision boundary: ONE field-by-field policy -------------------
|
|
10233
10339
|
/**
|
|
10234
10340
|
* Seal a revision in ONE transaction (Codex M5-M8 audit, C-3; contract
|
|
@@ -10320,7 +10426,14 @@ export class Store {
|
|
|
10320
10426
|
if (ids.size !== args.commentIds.length || live.length !== ids.size) {
|
|
10321
10427
|
throw new RevisionRaced("this annotation batch is no longer whole — read the current comments before sealing it");
|
|
10322
10428
|
}
|
|
10323
|
-
const
|
|
10429
|
+
for (const one of live) {
|
|
10430
|
+
if (one.image == null)
|
|
10431
|
+
continue;
|
|
10432
|
+
const image = sourceRef.repo === null ? null : verifyResultImage(this, args.brief.evidenceRoot, [sourceRef.repo], { taskId: args.source.task, ...one.image });
|
|
10433
|
+
if (one.image.run !== sourceRun.id || image === null || !image.ok)
|
|
10434
|
+
return refuse("brief-custody", "A feedback screenshot is unavailable or changed. The notes were not consumed.");
|
|
10435
|
+
}
|
|
10436
|
+
const exactComment = (one) => ({ id: one["id"], path: one["path"], line: one["line"], note: one["note"], author: one["author"], createdAt: one["createdAt"], image: one["image"] ?? null });
|
|
10324
10437
|
const expected = live.map(one => exactComment({ ...one }));
|
|
10325
10438
|
const described = Array.isArray(named.comments) && named.comments.every(one => one !== null && typeof one === "object" && !Array.isArray(one))
|
|
10326
10439
|
? named.comments.map(one => exactComment(one)) : null;
|
|
@@ -20632,6 +20745,7 @@ function readDiffComment(row) {
|
|
|
20632
20745
|
id: Number(row["id"]),
|
|
20633
20746
|
artifact: Number(row["artifact"]),
|
|
20634
20747
|
artifactSha: String(row["artifact_sha"]),
|
|
20748
|
+
image: readFeedbackImage(row["image_json"]),
|
|
20635
20749
|
run: Number(row["run"]),
|
|
20636
20750
|
path: row["path"] === null ? null : String(row["path"]),
|
|
20637
20751
|
line: row["line"] === null ? null : Number(row["line"]),
|
|
@@ -21510,3 +21624,48 @@ export function lockStoreMoneyMethods(store, prototype = Store.prototype) {
|
|
|
21510
21624
|
throw new Error(`Store.${name} couldn't be locked`);
|
|
21511
21625
|
}
|
|
21512
21626
|
}
|
|
21627
|
+
/** A saved note's screenshot reference (v143), or null; a damaged one refuses rather than reading as none. */
|
|
21628
|
+
function readFeedbackImage(value) {
|
|
21629
|
+
const image = parseFeedbackImage(value);
|
|
21630
|
+
if (image === false)
|
|
21631
|
+
throw new Error("The saved feedback image is damaged.");
|
|
21632
|
+
return image;
|
|
21633
|
+
}
|
|
21634
|
+
/** Takes a saved note's screenshot off without losing the note or its history (v143). Fail closed, in one write
|
|
21635
|
+
* transaction: the note must be live and unconsumed on this run, its screenshot must be genuinely unusable right now
|
|
21636
|
+
* (`unusable` is result-actions.ts feedbackImageProblem, re-read inside the transaction, never the page's say-so), and
|
|
21637
|
+
* the change is recorded in the action ledger under the person who made it. The note is superseded by an exact
|
|
21638
|
+
* imageless copy; the original stays in history. The caller has already proved the person may request changes on
|
|
21639
|
+
* this run (the same authority that seals any saved note into a revision); that standing is re-proved here too. */
|
|
21640
|
+
export function dropFeedbackImage(store, runId, commentId, actor, unusable, now) {
|
|
21641
|
+
return store.transact(() => {
|
|
21642
|
+
// The person's standing is re-read under the write lock: a revocation, a downgrade or a lost project committed
|
|
21643
|
+
// after the request was admitted refuses here (check and write together).
|
|
21644
|
+
const run = store.getRun(runId);
|
|
21645
|
+
const repo = run === null ? null : store.refById(run.taskRef)?.repo ?? null;
|
|
21646
|
+
const account = store.accountOf(actor);
|
|
21647
|
+
if (run === null || account === null || account.revokedAt !== null || account.role !== "approver" || !store.accountCanAccess(actor, repo)) {
|
|
21648
|
+
return { ok: false, reason: "not-authorized" };
|
|
21649
|
+
}
|
|
21650
|
+
const live = store.handle.prepare(`SELECT id, image_json FROM diff_comment WHERE id = ? AND run = ? AND superseded_by IS NULL AND consumed_by IS NULL
|
|
21651
|
+
AND reviewer_run IS NULL AND image_json IS NOT NULL`).get(commentId, runId);
|
|
21652
|
+
if (live === undefined)
|
|
21653
|
+
return { ok: false, reason: "not-found" };
|
|
21654
|
+
const image = readFeedbackImage(live["image_json"]);
|
|
21655
|
+
if (image === null)
|
|
21656
|
+
return { ok: false, reason: "not-found" };
|
|
21657
|
+
const problem = unusable(image);
|
|
21658
|
+
if (problem === null)
|
|
21659
|
+
return { ok: false, reason: "image-usable" };
|
|
21660
|
+
const copy = store.handle.prepare(`INSERT INTO diff_comment (artifact, artifact_sha, run, path, line, note, author, created_at, reviewer_run, severity)
|
|
21661
|
+
SELECT artifact, artifact_sha, run, path, line, note, author, ?, reviewer_run, severity FROM diff_comment WHERE id = ?`).run(now.toISOString(), commentId);
|
|
21662
|
+
const id = Number(copy.lastInsertRowid);
|
|
21663
|
+
const moved = store.handle.prepare("UPDATE diff_comment SET superseded_by = ? WHERE id = ? AND superseded_by IS NULL AND consumed_by IS NULL").run(id, commentId);
|
|
21664
|
+
if (Number(moved.changes) !== 1)
|
|
21665
|
+
throw new Error("The saved note changed while its screenshot was being removed.");
|
|
21666
|
+
store.recordAction({ at: now.toISOString(), actor, repo,
|
|
21667
|
+
taskId: store.externalIdFor(run.taskRef) ?? null, runId, action: "feedback screenshot removed", outcome: "recorded", source: "work",
|
|
21668
|
+
detail: `note ${commentId} kept as note ${id} without screenshot ${image.artifact}: ${problem}` });
|
|
21669
|
+
return { ok: true, id };
|
|
21670
|
+
});
|
|
21671
|
+
}
|
package/dist/task-sizing.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { type SizeAnswer } from "./contracts/task-sizing.js";
|
|
2
2
|
import { type CommandRunner } from "./claude-print.js";
|
|
3
|
-
import { type TaskSizing } from "./phase-routing.js";
|
|
3
|
+
import { type TaskSize, type TaskSizing } from "./phase-routing.js";
|
|
4
4
|
import type { Store } from "./store.js";
|
|
5
5
|
/** The classifier's whole budget, from asking to answer. */
|
|
6
6
|
export declare const SIZING_BUDGET_MS = 5000;
|
|
7
|
-
/** The model `claude -p` sizes with: small and fast. */
|
|
8
|
-
export declare const SIZING_CLAUDE_MODEL = "haiku";
|
|
7
|
+
/** The model `claude -p` sizes with: small and fast, by its exact id (an alias would float to whatever is newest). */
|
|
8
|
+
export declare const SIZING_CLAUDE_MODEL = "claude-haiku-5-5";
|
|
9
9
|
export type SizingInput = {
|
|
10
10
|
title: string;
|
|
11
11
|
goal?: string | undefined;
|
|
@@ -22,6 +22,8 @@ export declare const SUBSTANTIAL_WORK: RegExp;
|
|
|
22
22
|
export declare function planningSignals(input: SizingInput): boolean;
|
|
23
23
|
/** The size the description alone gives — today's planning signals, never risky. */
|
|
24
24
|
export declare function heuristicSizing(input: SizingInput, why?: string | null): TaskSizing;
|
|
25
|
+
export declare const SIZE_MEANS: Record<TaskSize, string>;
|
|
26
|
+
export declare const RISKY_MEANS = "Could a mistake here lose data, break security, sign-in, payments or permissions, change a database schema or migration, or be hard to undo?";
|
|
25
27
|
/** What the classifier reads: the task as data, bounded, keys blanked. */
|
|
26
28
|
export declare function sizingText(input: SizingInput): string;
|
|
27
29
|
/** A small Claude model on this computer's sign-in: no tools, no MCP servers, no repository, credential keys stripped.
|
package/dist/task-sizing.js
CHANGED
|
@@ -30,8 +30,8 @@ import {} from "./phase-routing.js";
|
|
|
30
30
|
import { ALL_CREDENTIAL_ENV } from "./provider.js";
|
|
31
31
|
/** The classifier's whole budget, from asking to answer. */
|
|
32
32
|
export const SIZING_BUDGET_MS = 5_000;
|
|
33
|
-
/** The model `claude -p` sizes with: small and fast. */
|
|
34
|
-
export const SIZING_CLAUDE_MODEL = "haiku";
|
|
33
|
+
/** The model `claude -p` sizes with: small and fast, by its exact id (an alias would float to whatever is newest). */
|
|
34
|
+
export const SIZING_CLAUDE_MODEL = "claude-haiku-5-5";
|
|
35
35
|
/** Structural or broad work — the description signal planning has always read. */
|
|
36
36
|
export const SUBSTANTIAL_WORK = /\b(?:architecture|end[- ]to[- ]end|migrat(?:e|ion)|moderni[sz]e|redesign|refactor|rework|workflow|navigation|accessibility|responsive|performance|security|unif(?:y|ied)|multi[- ](?:step|project|service))\b/i;
|
|
37
37
|
/** The description's own signals: several paths, a long description, or
|
|
@@ -55,12 +55,12 @@ function answerOf(value) {
|
|
|
55
55
|
const read = readSizeAnswer(value);
|
|
56
56
|
return read.ok ? read.value : null;
|
|
57
57
|
}
|
|
58
|
-
const SIZE_MEANS = {
|
|
58
|
+
export const SIZE_MEANS = {
|
|
59
59
|
small: "a contained change a fast model can make directly: copy, a style, a config value, one small function, one or two files",
|
|
60
60
|
medium: "ordinary feature or fix work across a few files that benefits from the everyday model",
|
|
61
61
|
large: "broad or structural work: many files, a new subsystem, a redesign or refactor, cross-cutting behaviour",
|
|
62
62
|
};
|
|
63
|
-
const RISKY_MEANS = "Could a mistake here lose data, break security, sign-in, payments or permissions, change a database schema or migration, or be hard to undo?";
|
|
63
|
+
export const RISKY_MEANS = "Could a mistake here lose data, break security, sign-in, payments or permissions, change a database schema or migration, or be hard to undo?";
|
|
64
64
|
/** What the classifier reads: the task as data, bounded, keys blanked. */
|
|
65
65
|
export function sizingText(input) {
|
|
66
66
|
return [
|