toolroll 0.9.85 → 0.9.86
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 +24 -0
- package/dist/agent-fence.d.ts +6 -2
- package/dist/agent-fence.js +3 -2
- package/dist/assignment.js +4 -3
- package/dist/browser/workspace.css +1 -1
- package/dist/browser/workspace.js +31 -29
- package/dist/browser-workspace.d.ts +40 -0
- package/dist/chat-actions.d.ts +11 -1
- package/dist/chat-actions.js +53 -1
- package/dist/chat-channel.js +1 -0
- package/dist/chat-quiet.js +3 -2
- package/dist/claim.d.ts +3 -0
- package/dist/claim.js +4 -1
- package/dist/contracts/chat-actions.d.ts +9 -0
- package/dist/contracts/chat-actions.js +3 -0
- package/dist/contracts/console-api.d.ts +4 -1
- package/dist/contracts/console-api.js +4 -1
- package/dist/contracts/flow-propose.d.ts +4 -1
- package/dist/contracts/flow-propose.js +1 -1
- package/dist/contracts/flow.d.ts +41 -0
- package/dist/contracts/flow.js +4 -2
- package/dist/contracts/lead-tools.d.ts +21 -0
- package/dist/contracts/lead-tools.js +2 -0
- package/dist/contracts/store-json.d.ts +2 -0
- package/dist/contracts/trigger-payloads.d.ts +20 -0
- package/dist/contracts/trigger-payloads.js +4 -0
- package/dist/coordinator.js +1 -0
- package/dist/deploy-gate.js +4 -0
- package/dist/dispatch.d.ts +3 -0
- package/dist/dispatch.js +4 -1
- package/dist/flow-browser.d.ts +103 -0
- package/dist/flow-browser.js +204 -0
- package/dist/flow-code.d.ts +5 -1
- package/dist/flow-code.js +4 -1
- package/dist/flow-engine.js +16 -0
- package/dist/flow-live.js +9 -1
- package/dist/flow-pull-request.js +15 -7
- package/dist/flow-share.js +3 -1
- package/dist/flow-steps.d.ts +9 -1
- package/dist/flow-steps.js +55 -5
- package/dist/flow-triggers.js +55 -0
- package/dist/flows-cli.d.ts +19 -0
- package/dist/flows-cli.js +47 -2
- package/dist/flows-ui.js +11 -2
- package/dist/flows.js +5 -2
- package/dist/lead-tools.js +35 -6
- package/dist/lead-voice.js +16 -5
- package/dist/operate.d.ts +1 -1
- package/dist/operate.js +215 -6
- package/dist/project-delete.js +7 -3
- package/dist/pull-request-flow.js +35 -4
- package/dist/result-follow-ups.js +4 -0
- package/dist/scope.d.ts +5 -0
- package/dist/scope.js +7 -0
- package/dist/server/chat.js +4 -2
- package/dist/server/console.css +29 -0
- package/dist/server/flows.js +15 -0
- package/dist/server/render-pages.d.ts +13 -0
- package/dist/server/render-pages.js +45 -2
- package/dist/server/render-tasks.d.ts +8 -0
- package/dist/server/render-tasks.js +17 -6
- package/dist/server/route-table.js +11 -0
- package/dist/server/task-views.js +5 -0
- package/dist/server/tasks.d.ts +10 -0
- package/dist/server/tasks.js +54 -2
- package/dist/settings-proofs.js +1 -0
- package/dist/store.d.ts +71 -3
- package/dist/store.js +222 -20
- package/dist/surface.js +18 -2
- package/dist/task-archive.d.ts +10 -0
- package/dist/task-archive.js +16 -0
- package/dist/task-control.d.ts +1 -1
- package/dist/tidy.d.ts +130 -0
- package/dist/tidy.js +503 -0
- package/dist/work-index.d.ts +24 -0
- package/dist/work-index.js +52 -30
- package/dist/workspace-ui.d.ts +1 -1
- package/dist/workspace-ui.js +3 -2
- package/package.json +1 -1
package/dist/server/console.css
CHANGED
|
@@ -2742,3 +2742,32 @@ html.so-swap::view-transition-old(*):only-child { animation: so-card-out 160ms e
|
|
|
2742
2742
|
|
|
2743
2743
|
.result-feedback-actions { display: flex; flex-wrap: wrap; gap: .5rem; }
|
|
2744
2744
|
.result-feedback-actions button { min-height: 44px; white-space: nowrap; }
|
|
2745
|
+
|
|
2746
|
+
/* Put away old tasks (docs/plans/task-tidy.md): grouped checklist on the kit's column. */
|
|
2747
|
+
.put-away { max-width: var(--so-settings-width, 760px); }
|
|
2748
|
+
.put-away h1 { margin: 0 0 4px; }
|
|
2749
|
+
.put-away-days { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin: 0 0 16px; }
|
|
2750
|
+
.put-away-days label { display: flex; align-items: center; gap: 8px; margin: 0; font-size: 14px; }
|
|
2751
|
+
.put-away-days input[type=number] { width: 5rem; margin: 0; }
|
|
2752
|
+
.put-away-days button { width: auto; }
|
|
2753
|
+
.put-away-group { margin: 0 0 20px; padding: 0; border: 0; }
|
|
2754
|
+
.put-away-group legend { display: flex; align-items: center; gap: 8px; padding: 0; margin: 0 0 6px; font-weight: 600; font-size: 15px; }
|
|
2755
|
+
.put-away-group legend .count { font-family: var(--so-mono, ui-monospace, monospace); font-size: 11px; font-weight: 500; color: var(--so-muted); }
|
|
2756
|
+
.put-away-group ul { list-style: none; margin: 0; padding: 0; border-top: 1px solid var(--so-line); }
|
|
2757
|
+
.put-away-group li { border-bottom: 1px solid var(--so-line); }
|
|
2758
|
+
.put-away-item { display: grid; grid-template-columns: auto minmax(0, 1fr); column-gap: 10px; align-items: start; margin: 0; padding: 10px 2px; cursor: pointer; }
|
|
2759
|
+
.put-away-item input { margin: 3px 0 0; width: auto; min-height: 0; align-self: start; }
|
|
2760
|
+
.put-away-title { font-size: 13.5px; font-weight: 500; overflow-wrap: anywhere; }
|
|
2761
|
+
.put-away-why { grid-column: 2; font-size: 12.5px; color: var(--so-muted); }
|
|
2762
|
+
.put-away-id { grid-column: 2; font-family: var(--so-mono, ui-monospace, monospace); font-size: 11.5px; color: var(--so-muted); overflow-wrap: anywhere; }
|
|
2763
|
+
.put-away-cli { margin: 12px 0 0; font-size: 13px; color: var(--so-muted); overflow-wrap: anywhere; }
|
|
2764
|
+
.put-away-cli code { font-size: 12px; }
|
|
2765
|
+
.task-archived { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 8px 12px; margin: 0 0 12px; padding: 10px 12px; border-radius: 8px; background: var(--so-raised); }
|
|
2766
|
+
.task-archived p { margin: 0; font-size: 13.5px; }
|
|
2767
|
+
.task-archived form { margin: 0; }
|
|
2768
|
+
.task-archived button { width: auto; }
|
|
2769
|
+
.work-row-status form.inline { margin: 0; }
|
|
2770
|
+
@media (max-width: 900px) {
|
|
2771
|
+
.put-away-item { min-height: 44px; }
|
|
2772
|
+
.put-away-days button, .task-archived button, .work-row-status form.inline button { min-height: 44px; }
|
|
2773
|
+
}
|
package/dist/server/flows.js
CHANGED
|
@@ -21,6 +21,7 @@ import { exportFlow, fetchFlowFile, FlowFileError, importFlow, parseFlowFile, pl
|
|
|
21
21
|
import { addFlowTriggerTo, checkFlowTriggerNow, pressFlowButton, removeFlowTrigger, removeLinearKey, setFlowTriggerOn, renewFlowHook, saveHooksBase, saveLinearKey, saveSigningSecret, setUpFlowHook, changeFlowHookSender, setFlowHookScope, shareFlowButton, stopSharingFlowButton, rawTakesDeliveries, rawAdmitsAnyone } from "../flow-triggers.js";
|
|
22
22
|
import { FLOW_IMPORT_SCRIPT, flowFallbackHtml, flowImportHtml, flowsListHtml, flowView } from "../flows-ui.js";
|
|
23
23
|
import { FLOW_TEMPLATES, FlowContractError, validateFlowDefinition, withZoneNames } from "../flows.js";
|
|
24
|
+
import { revokeFlowBrowser, signFlowBrowser } from "../flow-browser.js";
|
|
24
25
|
import { prepareUnattendedCandidates, parseUnattendedAsk, revokeUnattended, signUnattended, untilWords } from "../flow-unattended.js";
|
|
25
26
|
import { keyStatus } from "../keys.js";
|
|
26
27
|
import { kitPageHtml, kitsGalleryHtml } from "../kits-ui.js";
|
|
@@ -656,6 +657,18 @@ export function createFlowsHandlers(runtime) {
|
|
|
656
657
|
if (action === "unattended-off") {
|
|
657
658
|
return revokeUnattended(store, flow.id, who.name, now) ? settle("It no longer runs on its own. New cards wait for you.") : answer(409, { ok: false, said: "It wasn't running on its own." });
|
|
658
659
|
}
|
|
660
|
+
if (action === "browser") {
|
|
661
|
+
// A person's own step-up (a lead's or remote run's token never gets here: route table), then signFlowBrowser
|
|
662
|
+
// re-reads the flow, its zones and scripts in its one write and refuses unless they are the digest the panel showed.
|
|
663
|
+
const proved = authenticateApprover(who, body.get("password") ?? "", flow.repo);
|
|
664
|
+
if (!proved.ok)
|
|
665
|
+
return answer(403, { ok: false, said: "That password isn't right. Nothing was approved." });
|
|
666
|
+
const signed = signFlowBrowser(store, { flow: flow.id, signer: who.name, proof: proved.method, sawDigest: body.get("digest") ?? "", console: { generation: who.via === "cookie" ? who.session.generation : -1 } }, now);
|
|
667
|
+
return signed.ok ? settle(`Browser allowed. ${signed.grant.zones.length === 1 ? "Its script" : "Its scripts"} can start Chromium now.`) : answer(409, { ok: false, said: signed.message });
|
|
668
|
+
}
|
|
669
|
+
if (action === "browser-off") {
|
|
670
|
+
return revokeFlowBrowser(store, flow.id, who.name, now) ? settle("Browser turned off. Its browser zones wait until someone allows it again.") : answer(409, { ok: false, said: "Browser use wasn't allowed for this flow." });
|
|
671
|
+
}
|
|
659
672
|
if (action === "save") {
|
|
660
673
|
let saved, drawn;
|
|
661
674
|
try {
|
|
@@ -975,6 +988,8 @@ export function createFlowsHandlers(runtime) {
|
|
|
975
988
|
"flow.secrets": flowAct("secrets"),
|
|
976
989
|
"flow.unattended": flowAct("unattended"),
|
|
977
990
|
"flow.unattended-off": flowAct("unattended-off"),
|
|
991
|
+
"flow.browser": flowAct("browser"),
|
|
992
|
+
"flow.browser-off": flowAct("browser-off"),
|
|
978
993
|
"flow.triggers": flowAct("triggers"),
|
|
979
994
|
"flow.card.move": cardAct("move"),
|
|
980
995
|
"flow.card.decide": cardAct("decide"),
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/** Everyday console pages: home, inbox, board, work, projects, fleet and the queue. */
|
|
2
|
+
import { type TidyItem } from "../tidy.js";
|
|
2
3
|
import { type TaskOrigin } from "../task-source.js";
|
|
3
4
|
import { type Html } from "../html.js";
|
|
4
5
|
import { type AssignmentSnapshot } from "../assignment.js";
|
|
@@ -295,6 +296,8 @@ export declare function workPage(chrome: Chrome, data: {
|
|
|
295
296
|
now: Date;
|
|
296
297
|
limits?: BrowserLimits | null;
|
|
297
298
|
state?: TaskState | null;
|
|
299
|
+
/** An approver in a browser session: rows offer Put away (or Restore, archived), and the page offers Put away old tasks. */
|
|
300
|
+
canPutAway?: boolean;
|
|
298
301
|
}): Screen;
|
|
299
302
|
export declare function browsePage(chrome: Chrome, data: {
|
|
300
303
|
at: string;
|
|
@@ -562,3 +565,13 @@ export type PeekAdmission = {
|
|
|
562
565
|
epoch: string;
|
|
563
566
|
entries: ReturnType<typeof parseBaseTreeSnapshot>;
|
|
564
567
|
};
|
|
568
|
+
/** Put away old tasks (docs/plans/task-tidy.md): exactly what would be put away, grouped, each with why, every one
|
|
569
|
+
* ticked; one step-up puts away the ticked ones, bound to this preview's digest. The CLI twin is named beside it. */
|
|
570
|
+
export declare function putAwayPage(chrome: Chrome, data: {
|
|
571
|
+
items: readonly TidyItem[];
|
|
572
|
+
days: number;
|
|
573
|
+
digest: string;
|
|
574
|
+
now: Date;
|
|
575
|
+
problem: string | null;
|
|
576
|
+
releasesSkipped: string | null;
|
|
577
|
+
}): Screen;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { activityMetrics, activityScope, activityLimit } from '../project-activity.js';
|
|
2
2
|
/** Everyday console pages: home, inbox, board, work, projects, fleet and the queue. */
|
|
3
|
+
import { archivedWords, tidyGroups, TIDY_DEFAULT_DAYS } from "../tidy.js";
|
|
4
|
+
import { stepUpForm } from "../settings-kit.js";
|
|
3
5
|
import { originNote } from "../task-source.js";
|
|
4
6
|
import { html, htmlString, joinHtml, postForm, replaceMarkup, reauth } from "../html.js";
|
|
5
7
|
import {} from "../assignment.js";
|
|
@@ -632,14 +634,25 @@ export function workPage(chrome, data) {
|
|
|
632
634
|
return `/work${query.size ? `?${query}` : ''}`;
|
|
633
635
|
};
|
|
634
636
|
const tabs = html `<nav class="work-views" aria-label="Task views">${WORK_VIEWS.map(one => html `<a href="${href(one.key)}"${one.key === data.view ? html ` class="active" aria-current="page"` : null}>${one.label}<span class="count">${data.work.totals[one.key]}</span></a>`)}</nav>`;
|
|
637
|
+
// Put away (or Restore, in Archived): one quiet act per row, never on a task with work going on.
|
|
638
|
+
const tidyAct = (row) => data.canPutAway !== true ? null : row.archived !== undefined ? { action: `${taskHref(row.rootId)}/restore`, label: 'Restore' }
|
|
639
|
+
: row.liveRunId !== null || row.unfinishedRunId !== null || row.status.views.includes('running') ? null : { action: `${taskHref(row.rootId)}/archive`, label: 'Put away' };
|
|
640
|
+
const returnTo = href(data.view, undefined);
|
|
635
641
|
const rowHtml = (row) => {
|
|
642
|
+
if (row.archived !== undefined) {
|
|
643
|
+
const act = tidyAct(row);
|
|
644
|
+
const project = data.multiProject || row.repo === null ? html `<span class="project-label">${row.repo === null ? 'Unplaced' : projectName(row.repo)}</span>` : null;
|
|
645
|
+
return html `<article class="work-row" data-task="${row.rootId}" data-work-status="archived" data-work-views="archived"><div class="work-row-main"><a class="work-title" href="${taskHref(row.rootId)}">${row.title}</a><p class="work-meta">${project}<span>${relativeAge(row.archived.at, data.now)}</span><span class="mono">${row.rootId}</span></p><p class="work-detail" data-archived-by>${archivedWords(row.archived)}</p></div><div class="work-row-status">${act === null ? null : postForm(act.action, html `<button type="submit" class="secondary">${act.label}</button>`, { hidden: { return: returnTo }, attrs: { class: 'inline', 'data-tidy-act': 'restore' } })}</div></article>`;
|
|
646
|
+
}
|
|
636
647
|
const target = row.primaryAction?.target;
|
|
637
648
|
const actionHref = row.primaryAction?.code === 'open-result' && target?.runId != null
|
|
638
649
|
? `/review?result=${encodeURIComponent(target.taskId)}&run=${target.runId}`
|
|
639
650
|
: browserWorkActionHref(row);
|
|
640
651
|
const action = actionHref === null || row.primaryAction === null ? null : html `<a class="work-action" data-primary-action href="${actionHref}">${row.primaryAction.label} →</a>`;
|
|
641
652
|
const project = data.multiProject || row.repo === null ? html `<span class="project-label">${row.repo === null ? 'Unplaced' : projectName(row.repo)}</span>` : null;
|
|
642
|
-
|
|
653
|
+
const act = tidyAct(row);
|
|
654
|
+
const putAway = act === null ? null : postForm(act.action, html `<button type="submit" class="quiet">${act.label}</button>`, { hidden: { return: returnTo }, attrs: { class: 'inline', 'data-tidy-act': 'put-away' } });
|
|
655
|
+
return html `<article class="work-row" data-task="${row.rootId}" data-work-status="${row.status.token}" data-work-views="${row.status.views.join(' ')}"><div class="work-row-main"><a class="work-title" href="${taskHref(row.rootId)}">${row.title}</a><p class="work-meta">${project}<span>${relativeAge(row.updatedAt, data.now)}</span><span class="mono">${row.rootId}</span></p></div><div class="work-row-status">${statusLineHtml(row.status)}${action}${putAway}${row.familyProblem === null ? null : html `<p class="problem">${row.familyProblem}</p>`}${row.status.views.includes('needs-you') && !['write-scope', 'approve-scope'].includes(row.primaryAction?.code ?? '') && row.status.detail !== row.status.label && row.status.detail !== row.familyProblem ? html `<p class="work-detail">${row.status.detail}</p>` : null}${workDiagnosticsHtml(row.status.diagnostics)}</div></article>`;
|
|
643
656
|
};
|
|
644
657
|
const current = WORK_VIEWS.find(one => one.key === data.view);
|
|
645
658
|
const filtered = (data.state ?? null) !== null;
|
|
@@ -654,6 +667,7 @@ export function workPage(chrome, data) {
|
|
|
654
667
|
const pages = !data.previous && data.work.nextCursor === null ? null : html `<nav class="row work-pagination" aria-label="Task pages">${data.previous ? html `<a class="button-link" href="${href(data.view)}">First page</a>` : null}${data.work.nextCursor === null ? null : html `<a class="button-link" rel="next" href="${href(data.view, data.work.nextCursor)}">Next page</a>`}</nav>`;
|
|
655
668
|
const tools = html `<details class="work-tools"><summary>Work tools${CHEVRON_ICON}</summary><nav class="work-tools-menu">${[['/inbox', 'Inbox'], ['/board', 'Board'], ['/board?view=order', 'Order'], ['/recipes', 'Recipes'], ...(chrome.projectScoped ? [] : [['/workbench', 'Portfolio']]), ['/ledger', 'Action ledger']]
|
|
656
669
|
.map(([path, label]) => html `<a href="${path}">${label}</a>`)}</nav></details>`;
|
|
670
|
+
const putAwayLink = data.canPutAway === true && data.view !== 'archived' ? { label: 'Put away old tasks', href: '/work/put-away' } : null;
|
|
657
671
|
const toolLinks = [['/inbox', 'Inbox'], ['/board', 'Board'], ['/board?view=order', 'Order'], ['/recipes', 'Recipes'], ...(chrome.projectScoped ? [] : [['/workbench', 'Portfolio']])];
|
|
658
672
|
const view = {
|
|
659
673
|
kind: 'tasks',
|
|
@@ -667,6 +681,15 @@ export function workPage(chrome, data) {
|
|
|
667
681
|
const actionHref = row.primaryAction?.code === 'open-result' && target?.runId != null
|
|
668
682
|
? `/review?result=${encodeURIComponent(target.taskId)}&run=${target.runId}`
|
|
669
683
|
: browserWorkActionHref(row);
|
|
684
|
+
const act = tidyAct(row);
|
|
685
|
+
const tidy = act === null ? null : { action: act.action, label: act.label, returnTo };
|
|
686
|
+
if (row.archived !== undefined)
|
|
687
|
+
return {
|
|
688
|
+
id: row.rootId, title: row.title, href: taskHref(row.rootId),
|
|
689
|
+
project: data.multiProject || row.repo === null ? (row.repo === null ? 'Unplaced' : projectName(row.repo)) : null,
|
|
690
|
+
age: relativeAge(row.archived.at, data.now), status: { label: 'Archived', tone: 'muted', token: 'archived' },
|
|
691
|
+
ask: null, chip: null, group: 'rest', action: null, detail: archivedWords(row.archived), problem: null, notes: [], tidy,
|
|
692
|
+
};
|
|
670
693
|
const needsYouDetail = (row.status.views.includes('needs-you') || row.status.label === 'Failed') && !['write-scope', 'approve-scope'].includes(row.primaryAction?.code ?? '') && row.status.label !== 'Ready for review' && row.status.detail !== row.status.label && row.status.detail !== row.familyProblem;
|
|
671
694
|
return {
|
|
672
695
|
id: row.rootId, title: row.title, href: taskHref(row.rootId),
|
|
@@ -678,6 +701,7 @@ export function workPage(chrome, data) {
|
|
|
678
701
|
detail: needsYouDetail ? row.status.detail : row.progress ?? null,
|
|
679
702
|
problem: row.familyProblem,
|
|
680
703
|
notes: (row.status.diagnostics ?? []).map(one => `${one.label} · ${one.detail}`),
|
|
704
|
+
tidy,
|
|
681
705
|
};
|
|
682
706
|
}),
|
|
683
707
|
empty: data.work.items.length > 0 ? null : filtered && !data.previous ? { text: `No tasks are ${data.state} in this view.`, action: { label: "Show every state", href: href(data.view, undefined, null) } } : {
|
|
@@ -687,11 +711,12 @@ export function workPage(chrome, data) {
|
|
|
687
711
|
pages: { first: data.previous ? href(data.view) : null, next: data.work.nextCursor === null ? null : href(data.view, data.work.nextCursor) },
|
|
688
712
|
tools: toolLinks.map(([path, label]) => ({ label: label, href: path })),
|
|
689
713
|
newTask: { label: 'New task', href: '/tasks/new' },
|
|
714
|
+
putAway: putAwayLink,
|
|
690
715
|
limits: data.limits ?? null,
|
|
691
716
|
states: [{ label: 'Any state', href: href(data.view, undefined, null), active: (data.state ?? null) === null },
|
|
692
717
|
...TASK_STATES.map(one => ({ label: one.charAt(0).toUpperCase() + one.slice(1), href: href(data.view, undefined, one), active: data.state === one }))],
|
|
693
718
|
};
|
|
694
|
-
return screen('work', html `<div class="work-head"><h1>Tasks</h1>${tools}</div>${limitsHtml(data.limits ?? null)}${tabs}${stateLinks}${list}${pages}`, { chrome, workspace: { view } });
|
|
719
|
+
return screen('work', html `<div class="work-head"><h1>Tasks</h1>${putAwayLink === null ? null : html `<a class="button-link" href="${putAwayLink.href}" data-put-away-old>${putAwayLink.label}</a>`}${tools}</div>${limitsHtml(data.limits ?? null)}${tabs}${stateLinks}${list}${pages}`, { chrome, workspace: { view } });
|
|
695
720
|
}
|
|
696
721
|
export function browsePage(chrome, data) {
|
|
697
722
|
const crumb = data.at === data.root ? projectName(data.root) : `${projectName(data.root)}${data.at.slice(data.root.length)}`;
|
|
@@ -1320,3 +1345,21 @@ export function nextPage(chrome, data) {
|
|
|
1320
1345
|
}
|
|
1321
1346
|
return screen("next", joinHtml([header, card], "\n"), { chrome });
|
|
1322
1347
|
}
|
|
1348
|
+
/** Put away old tasks (docs/plans/task-tidy.md): exactly what would be put away, grouped, each with why, every one
|
|
1349
|
+
* ticked; one step-up puts away the ticked ones, bound to this preview's digest. The CLI twin is named beside it. */
|
|
1350
|
+
export function putAwayPage(chrome, data) {
|
|
1351
|
+
const groups = tidyGroups(data.items, data.days);
|
|
1352
|
+
const count = data.items.length;
|
|
1353
|
+
const daysArg = data.days === TIDY_DEFAULT_DAYS ? "" : ` --days ${data.days}`;
|
|
1354
|
+
const crumbs = html `<nav class="kit-crumbs" aria-label="Breadcrumb"><ol><li><a href="/work">Tasks</a></li><li><span aria-current="page">Put away old tasks</span></li></ol></nav>`;
|
|
1355
|
+
const daysForm = html `<form method="get" action="/work/put-away" class="put-away-days"><label>Also finished tasks untouched for <input type="number" name="days" min="1" max="365" value="${String(data.days)}" inputmode="numeric"> days</label><button type="submit" class="secondary">Update list</button></form>`;
|
|
1356
|
+
const list = joinHtml(groups.map(group => html `<fieldset class="put-away-group" data-tidy-group="${group.rule}"><legend>${group.label}<span class="count">${String(group.items.length)}</span></legend><ul>${group.items.map(item => html `<li><label class="put-away-item" data-tidy-task="${item.taskId}"><input type="checkbox" name="task" value="${item.taskId}" checked><span class="put-away-title">${item.title}</span><span class="put-away-why">${item.reason}</span><span class="put-away-id">${item.taskId}${item.repo === null ? "" : ` · ${projectName(item.repo)}`}${item.members.length > 1 ? ` · ${item.members.length} versions` : ""}</span></label></li>`)}</ul></fieldset>`));
|
|
1357
|
+
const cli = html `<p class="put-away-cli" data-tidy-cli>Or run: <code>toolroll task tidy --apply --digest ${data.digest}${daysArg}</code></p>`;
|
|
1358
|
+
const body = count === 0
|
|
1359
|
+
? html `<p class="kit-state">Nothing to put away right now.</p><p><a href="/work?view=archived">See archived tasks →</a></p>`
|
|
1360
|
+
: stepUpForm({ proof: "tasks-put-away", action: "/work/put-away", id: "put-away", hidden: { digest: data.digest, days: data.days },
|
|
1361
|
+
body: html `${list}${cli}`, save: { label: "Put away selected", idle: `${count} ${count === 1 ? "task" : "tasks"} selected` }, confirm: `Put away the selected tasks. You can restore any of them from Archived.` });
|
|
1362
|
+
return screen("Put away old tasks", html `<div class="put-away">${crumbs}<h1>Put away old tasks</h1>\
|
|
1363
|
+
<p class="kit-state">${count === 0 ? "Old releases, routine runs and fixed CI notices are put away automatically." : `${count} ${count === 1 ? "task" : "tasks"} can be put away. They leave every list and keep their history.`}</p>\
|
|
1364
|
+
${data.problem ? html `<p class="kit-problem" role="alert">${data.problem}</p>` : ""}${data.releasesSkipped === null ? "" : html `<p class="kit-hint">Release tasks were left out: ${data.releasesSkipped}.</p>`}${daysForm}${body}</div>`, { chrome });
|
|
1365
|
+
}
|
|
@@ -438,6 +438,14 @@ export declare function taskBodyParts(data: {
|
|
|
438
438
|
/** Markup the caller rendered (the version history). */
|
|
439
439
|
history?: Html;
|
|
440
440
|
versionLabel?: string | null;
|
|
441
|
+
/** Put away (v145): by whom and why; and whether the viewer can put it away or restore it (an approver's browser). */
|
|
442
|
+
archived?: {
|
|
443
|
+
by: string;
|
|
444
|
+
byKind: "person" | "tidy";
|
|
445
|
+
reason: string;
|
|
446
|
+
at: string;
|
|
447
|
+
} | null;
|
|
448
|
+
canPutAway?: boolean;
|
|
441
449
|
status?: DisplayStatus;
|
|
442
450
|
task: Task;
|
|
443
451
|
/** Shared read-side lifecycle answer; the atomic claim still re-proves it. */
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
/** Task pages: approval, agents, plans, controls, decisions and the task body. */
|
|
2
|
+
import { archivedWords } from "../tidy.js";
|
|
3
|
+
import { whenUtc } from "../when-html.js";
|
|
2
4
|
import { networkWords } from "../egress.js";
|
|
3
5
|
import { releaseAsksAgainWords, releaseTermsSentence } from "../scope.js";
|
|
4
6
|
import { RELEASE_CI_RUNNING_WORDS } from "../release-ci-hold.js";
|
|
@@ -774,6 +776,8 @@ export function approveRefusalWords(reason) {
|
|
|
774
776
|
return gateWords({ verdict: "refuse", reason });
|
|
775
777
|
if (reason === "machine-proof")
|
|
776
778
|
return "not approved: only a person can approve this. Approve it on the task page or from your paired chat";
|
|
779
|
+
if (reason === "archived")
|
|
780
|
+
return "not approved: this task is put away. Restore it first";
|
|
777
781
|
if (reason === "unrouted")
|
|
778
782
|
return "not approved: this scope predates agent routing and its old approval no longer stands — edit and re-file the scope so it is routed under today’s agents, then approve it";
|
|
779
783
|
if (reason === "profile-unresolved")
|
|
@@ -2068,8 +2072,11 @@ export function taskBodyParts(data) {
|
|
|
2068
2072
|
// The title is bare; the receipt or shared task status leads once.
|
|
2069
2073
|
html `<div class="task-title-row"><h1 class="task-main-title">${data.rootTitle ?? task.title}</h1>${data.csrf === "" ? "" : taskViewSwitch(data.rootId ?? task.id, "overview")}</div>`,
|
|
2070
2074
|
data.versionLabel == null ? "" : html `<p class="meta">${data.versionLabel} · <a href="${taskHref(data.rootId ?? task.id)}">Current work</a></p>`,
|
|
2075
|
+
data.archived == null ? "" : html `<div class="task-archived" data-archived><p>${archivedWords(data.archived)} · ${whenUtc(data.archived.at)}</p>${data.canPutAway === true && data.csrf !== ""
|
|
2076
|
+
? postForm(`${taskHref(task.id)}/restore`, html `<button type="submit" class="secondary">Restore</button>`, { hidden: { return: taskHref(task.id) }, attrs: { class: "inline" } }) : ""}</div>`,
|
|
2071
2077
|
(data.history ?? ""),
|
|
2072
|
-
|
|
2078
|
+
// A put-away task's banner is its status: no Needs you card, no act, no approval form (each would be refused).
|
|
2079
|
+
data.archived == null ? statusHtml : "",
|
|
2073
2080
|
controlHtml,
|
|
2074
2081
|
previousResult === null ? "" : html `<details class="task-previous-result"><summary>${previousResult.title}</summary>${previousResult.html}</details>`,
|
|
2075
2082
|
progressCard,
|
|
@@ -2078,7 +2085,7 @@ export function taskBodyParts(data) {
|
|
|
2078
2085
|
mirrorCard,
|
|
2079
2086
|
problemHtml,
|
|
2080
2087
|
needsScopeCard,
|
|
2081
|
-
approvalHtml,
|
|
2088
|
+
data.archived == null ? approvalHtml : "",
|
|
2082
2089
|
html `<details class="task-status-details" id="task-diagnostics"${optionsOpen ? html ` open` : ""}><summary>Task options</summary>${optionsHtml}</details>`,
|
|
2083
2090
|
// Evidence-first (M5.5): what needs you, then what happened — decisions
|
|
2084
2091
|
// and incidents above the attempt ledger and spend, the mechanics
|
|
@@ -2209,7 +2216,8 @@ export function taskBodyParts(data) {
|
|
|
2209
2216
|
// The Details panel: the same facts, grouped and said once.
|
|
2210
2217
|
const lastRunForAgent = liveRun ?? familyRuns.filter(run => run.role !== "reviewer").sort((a, b) => b.id - a.id)[0];
|
|
2211
2218
|
const work = [];
|
|
2212
|
-
|
|
2219
|
+
// A put-away task's one status is Archived (its banner says by whom and why).
|
|
2220
|
+
work.push({ label: "Status", parts: [data.archived != null ? "Archived" : data.assignment != null && assignmentOptions !== null ? assignmentCardOf(data.assignment, assignmentOptions).status.headline : status.label] });
|
|
2213
2221
|
if (data.repo !== null)
|
|
2214
2222
|
work.push({ label: "Project", parts: [projectName(data.repo)] });
|
|
2215
2223
|
if (data.base != null)
|
|
@@ -2320,6 +2328,7 @@ export function taskBodyParts(data) {
|
|
|
2320
2328
|
...(sectionIds.has("waits-for") ? [{ group: "work", label: "Dependencies", kind: "goto", tab: "plan", target: "waits-for" }] : []),
|
|
2321
2329
|
...(sectionIds.has("holds") ? [{ group: "work", label: "Holds", kind: "goto", tab: "plan", target: "holds" }] : []),
|
|
2322
2330
|
...(data.csrf !== "" ? [{ group: "work", label: "Task options", kind: "goto", tab: "plan", target: "task-diagnostics" }] : []),
|
|
2331
|
+
...(data.canPutAway === true && data.csrf !== "" && data.archived == null && !building ? [{ group: "work", label: "Put away", kind: "post", action: `${taskHref(task.id)}/archive`, fields: { return: taskHref(task.id) } }] : []),
|
|
2323
2332
|
{ group: "share", label: "Copy link", kind: "copy" },
|
|
2324
2333
|
...(facts.some(one => one.label === "Audit") ? [{ group: "share", label: "Evidence pack", kind: "link", href: `${taskHref(rootOf)}/evidence` }] : []),
|
|
2325
2334
|
...(data.repo !== null && data.scope !== null ? [{ group: "share", label: "Reuse as recipe", kind: "link", href: `/recipes/from-task?task=${encodeURIComponent(task.id)}` }] : []),
|
|
@@ -2336,10 +2345,12 @@ export function taskBodyParts(data) {
|
|
|
2336
2345
|
{ label: "Ask", href: taskChatHref(data.rootId ?? task.id), active: false },
|
|
2337
2346
|
],
|
|
2338
2347
|
version: data.versionLabel == null ? null : { label: data.versionLabel, current: { label: "Current work", href: taskHref(data.rootId ?? task.id) } },
|
|
2339
|
-
|
|
2348
|
+
archived: data.archived == null ? null : { words: archivedWords(data.archived), at: data.archived.at,
|
|
2349
|
+
restore: data.canPutAway === true && data.csrf !== "" ? { action: `${taskHref(task.id)}/restore`, returnTo: taskHref(task.id) } : null },
|
|
2350
|
+
status: statusCard === null || data.archived != null ? null : inPlaceChecks(failedCard ? { ...statusCard, action: null, status: { ...statusCard.status, sentence: data.failure?.line ?? statusCard.status.sentence } }
|
|
2340
2351
|
: progress !== null || record !== null ? { ...statusCard, action: statusCard.action?.href === record?.href ? null : statusCard.action, status: { ...statusCard.status, sentence: progress?.line ?? statusCard.status.sentence } }
|
|
2341
2352
|
: statusCard.action !== null && rebuild === null && bareSelfLink(statusCard.action.href) ? { ...statusCard, action: null } : statusCard),
|
|
2342
|
-
statusHtml: htmlString(statusHtml),
|
|
2353
|
+
statusHtml: data.archived != null ? "" : htmlString(statusHtml),
|
|
2343
2354
|
failure: failedCard && data.failure != null ? { line: data.failure.line, evidence: data.failure.evidence, suggestion: data.failure.suggestion, link: data.failure.link } : null,
|
|
2344
2355
|
retry,
|
|
2345
2356
|
runChecks,
|
|
@@ -2352,7 +2363,7 @@ export function taskBodyParts(data) {
|
|
|
2352
2363
|
...(data.csrf !== "" && (data.steerRecent?.length ?? 0) > 0 ? { steerRecent: data.steerRecent } : {}),
|
|
2353
2364
|
record,
|
|
2354
2365
|
earlier,
|
|
2355
|
-
approval: fragmentString(approvalHtml),
|
|
2366
|
+
approval: data.archived != null ? "" : fragmentString(approvalHtml),
|
|
2356
2367
|
approvalPlacement: approvalLeads ? "lead" : "plan",
|
|
2357
2368
|
more,
|
|
2358
2369
|
confirmStopped: data.csrf !== "" && data.assignment?.primaryAction?.code === "confirm-stopped" && data.assignment.primaryAction.target.runId !== null
|
|
@@ -127,6 +127,9 @@ get("pages", "projects.page", "/projects", { limited: "unscoped", ...free });
|
|
|
127
127
|
get("pages", "home", "/", { limited: "collection", ...free });
|
|
128
128
|
get("tasks", "inbox", "/inbox", { limited: "collection", ...free });
|
|
129
129
|
get("tasks", "work", "/work", { limited: "collection", ...free });
|
|
130
|
+
// Put away old tasks (task-tidy.md): the preview reads only; putting away is an approver's step-up in a browser session.
|
|
131
|
+
get("tasks", "work.put-away", "/work/put-away", { limited: "collection", ...free, callers: ["cookie"], ...approver("Only an approver can put tasks away.", "/work") });
|
|
132
|
+
post("tasks", "work.put-away.apply", "/work/put-away", { scope: "step-up", limited: "collection", ...approver("Only an approver can put tasks away.", "/work") });
|
|
130
133
|
get("tasks", "next", "/next", { limited: "collection" });
|
|
131
134
|
get("pages", "morning", "/morning");
|
|
132
135
|
get("pages", "workbench", "/workbench", free);
|
|
@@ -270,6 +273,11 @@ for (const one of ["linear-key", "hooks-address", "secrets"])
|
|
|
270
273
|
// "Runs on its own": signing raises authority (the owner's password, a step-up); revoking is one click for any approver.
|
|
271
274
|
post("flows", "flow.unattended", `^\\/flows\\/${FLOW}\\/unattended$`, { scope: "step-up", project: "flow", sample: "/flows/1/unattended", ...EDIT_FLOWS });
|
|
272
275
|
post("flows", "flow.unattended-off", `^\\/flows\\/${FLOW}\\/unattended-off$`, { project: "flow", sample: "/flows/1/unattended-off", ...EDIT_FLOWS });
|
|
276
|
+
// Browser use in a flow's script zones (flow-browser.ts): allowing it widens the sandbox, so a person's own step-up from
|
|
277
|
+
// their browser, carrying the approval digest they read; turning it off lowers authority, one click. Browser only: a
|
|
278
|
+
// lead's or remote run's token never reaches either (the CLI says the same).
|
|
279
|
+
post("flows", "flow.browser", `^\\/flows\\/${FLOW}\\/browser$`, { scope: "step-up", project: "flow", sample: "/flows/1/browser", ...EDIT_FLOWS });
|
|
280
|
+
post("flows", "flow.browser-off", `^\\/flows\\/${FLOW}\\/browser-off$`, { callers: ["cookie"], project: "flow", limited: "resource", sample: "/flows/1/browser-off", ...EDIT_FLOWS });
|
|
273
281
|
post("flows", "flow.triggers", `^\\/flows\\/${FLOW}\\/triggers$`, { project: "flow", limited: "resource", sample: "/flows/1/triggers", ...EDIT_FLOWS });
|
|
274
282
|
for (const one of ["move", "decide", "cancel", "comment", "assign", "watch"])
|
|
275
283
|
post("flows", `flow.card.${one}`, `^\\/flows\\/${FLOW}\\/cards\\/${FLOW}\\/${one}$`, { project: "flow", limited: "resource", sample: `/flows/1/cards/2/${one}`, ...EDIT_FLOWS });
|
|
@@ -408,6 +416,9 @@ post("tasks", "task.act.merge", `^\\/t\\/${TASK}\\/merge$`, { project: "task", l
|
|
|
408
416
|
post("tasks", "task.act.confirm-stopped", `^\\/t\\/${TASK}\\/confirm-stopped$`, { scope: "step-up", project: "task", limited: "resource", sample: "/t/one/confirm-stopped", ...approver("Only an approver can confirm a build stopped.") });
|
|
409
417
|
post("tasks", "task.act.stop-carrying", `^\\/t\\/${TASK}\\/stop-carrying$`, { project: "task", limited: "resource", sample: "/t/one/stop-carrying", ...approver("Only an approver can stop carrying a release.") });
|
|
410
418
|
post("tasks", "task.act.stop", `^\\/t\\/${TASK}\\/stop$`, { project: "task", limited: "resource", sample: "/t/one/stop" });
|
|
419
|
+
// Putting one task away and bringing it back (task-tidy.md): a person's act, so only a browser session.
|
|
420
|
+
post("tasks", "task.act.archive", `^\\/t\\/${TASK}\\/archive$`, { project: "task", limited: "resource", sample: "/t/one/archive", callers: ["cookie"], ...approver("Only an approver can put a task away.") });
|
|
421
|
+
post("tasks", "task.act.restore", `^\\/t\\/${TASK}\\/restore$`, { project: "task", limited: "resource", sample: "/t/one/restore", callers: ["cookie"], ...approver("Only an approver can restore a task.") });
|
|
411
422
|
post("tasks", "task.act.pause", `^\\/t\\/${TASK}\\/pause$`, { project: "task", limited: "resource", sample: "/t/one/pause" });
|
|
412
423
|
post("tasks", "task.act.resume-arm", `^\\/t\\/${TASK}\\/resume-arm$`, { project: "task", limited: "resource", sample: "/t/one/resume-arm" });
|
|
413
424
|
post("tasks", "task.act.resume", `^\\/t\\/${TASK}\\/resume$`, { scope: "step-up", project: "task", limited: "resource", sample: "/t/one/resume" });
|
|
@@ -21,6 +21,7 @@ import { newestPullRequestOf, publishingOf, pullRequestBlocker } from '../pull-r
|
|
|
21
21
|
import { structuredHandoffView, terminalDiffView } from "../result-evidence-readers.js";
|
|
22
22
|
import { resultCheckLevel } from '../result-follow-ups.js';
|
|
23
23
|
import { skillsSnapshotHtml, skillTestFeedbackHtml } from "../skills-ui.js";
|
|
24
|
+
import { archiveOf } from "../store.js";
|
|
24
25
|
import { runActivityOf } from "../task-activity.js";
|
|
25
26
|
import { taskFingerprint } from "../task-live.js";
|
|
26
27
|
import { pullRequestFactOf } from '../task-status.js';
|
|
@@ -792,6 +793,10 @@ export function createTaskViews(runtime) {
|
|
|
792
793
|
const presentedData = { ...data, rootId: family?.root.id ?? taskId, rootTitle: family?.root.title ?? data.task.title,
|
|
793
794
|
...(family === null || data.assignment != null ? {} : { history: familyHistory(family) }),
|
|
794
795
|
versionLabel: family !== null && family.current.id !== taskId ? `Viewing ${family.versions.findIndex(one => one.id === taskId) === 0 ? "Original" : `Revision ${family.versions.findIndex(one => one.id === taskId)}`} · ${family.current.state === "running" ? "A newer revision is running" : "A newer revision is current"}` : null };
|
|
796
|
+
// Put away (v145, task-tidy.md): who or what put it away and why, and whether this person can put it away or restore it.
|
|
797
|
+
const archived = archiveOf(runtime.store, taskId);
|
|
798
|
+
presentedData.archived = archived;
|
|
799
|
+
presentedData.canPutAway = who.via === "cookie" && who.role === "approver";
|
|
795
800
|
if (scopeDraft !== undefined)
|
|
796
801
|
presentedData.scopeDraft = scopeDraft;
|
|
797
802
|
if (cancelDraft !== undefined)
|
package/dist/server/tasks.d.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { type Html } from "../html.js";
|
|
3
3
|
import { type ServerResponse } from "node:http";
|
|
4
4
|
import { type Artifact, type Run, type Store } from "../store.js";
|
|
5
|
+
import type { HandlerContext } from './handler-context.js';
|
|
5
6
|
import type { ServerRuntime } from './runtime.js';
|
|
6
7
|
import { type PeekAdmission } from "./render-pages.js";
|
|
7
8
|
import { type CompletedWorkRow, type RankedReviewRow, type ReviewCockpitView } from "./render-results.js";
|
|
@@ -36,3 +37,12 @@ export declare function createTasksHandlers(runtime: ServerRuntime): {
|
|
|
36
37
|
};
|
|
37
38
|
liveRunIds: (rows: readonly (Pick<Run, "id" | "outcome" | "leaseId" | "taskRef">)[]) => Set<number>;
|
|
38
39
|
};
|
|
40
|
+
/** Put away and Restore one task, and Put away old tasks (docs/plans/task-tidy.md): the preview reads only (GET-quiet);
|
|
41
|
+
* putting away many is an approver's step-up bound to the digest of exactly what the preview showed. Each write
|
|
42
|
+
* re-proves the person's standing inside its own transaction. */
|
|
43
|
+
export declare function createTidyHandlers(runtime: ServerRuntime): {
|
|
44
|
+
"work.put-away": (ctx: HandlerContext) => Promise<void>;
|
|
45
|
+
"work.put-away.apply": (ctx: HandlerContext) => Promise<void>;
|
|
46
|
+
"task.act.archive": (ctx: HandlerContext) => Promise<void>;
|
|
47
|
+
"task.act.restore": (ctx: HandlerContext) => Promise<void>;
|
|
48
|
+
};
|
package/dist/server/tasks.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { parseFeedbackImage, sameFeedbackImage } from "../result-review.js";
|
|
2
|
-
import { dropFeedbackImage, releaseAuthorityOfTask, revokeRelease, runLiveCommands } from "../store.js";
|
|
2
|
+
import { archiveTaskFamily, dropFeedbackImage, releaseAuthorityOfTask, restoreTaskFamily, revokeRelease, runLiveCommands } from "../store.js";
|
|
3
|
+
import { applyTidy, approverStandsNow, familyRepoOf, releaseFactsOf, tidyDays, tidyDigest, tidyPreview, TIDY_DEFAULT_DAYS } from "../tidy.js";
|
|
3
4
|
import { steerCapabilityOf } from "../steer-capabilities.js";
|
|
4
5
|
import { canReveal, localCheckoutOf, revealInFinder } from "../checkout-proof.js";
|
|
5
6
|
/** A form's way back to a flow card ("flow:<flow>:<card>"), read strictly: numbers only, never a path. */
|
|
@@ -67,7 +68,7 @@ import { buildProgressOf, parseWorkView } from "../workspace-ui.js";
|
|
|
67
68
|
import { requestContext } from "./request-context.js";
|
|
68
69
|
import { boardTickScript, oneLineOf, QUEUE_VIEW, refuse, regionScript, screen, transcriptScript, whenTime } from "./chrome.js";
|
|
69
70
|
import { chatReturnWithSaid, matchTaskPath, redirect, respond, reviewHref, safeChatReturn, safeReturn, SAFETY, taskChatHref, taskHref } from "./http.js";
|
|
70
|
-
import { boardBody, boardWorkers, donePage, inboxFingerprints, inboxPage, newTaskPage, nextPage, parseInboxTab, QUEUE_FRONT, queueBody, queueScript, TASK_STATES, workPage } from "./render-pages.js";
|
|
71
|
+
import { putAwayPage, boardBody, boardWorkers, donePage, inboxFingerprints, inboxPage, newTaskPage, nextPage, parseInboxTab, QUEUE_FRONT, queueBody, queueScript, TASK_STATES, workPage } from "./render-pages.js";
|
|
71
72
|
import { runEfficiencyOf } from "../run-efficiency.js";
|
|
72
73
|
import { timingFor } from "../latency-timeline.js";
|
|
73
74
|
import { ACCEPT_ANYWAY_NEEDS_REASON, editorFileHref, proofBundleView, rankReviewQueue, RESULT_REFUSALS, REVIEW_QUEUE_CAP, reviewCockpitDetailParts, reviewCockpitPage, reviewPriorityOf, runFactsFragment, runOutcomeBadge, runPage, RUNS_PAGE, runsPage, withRefusal } from "./render-results.js";
|
|
@@ -278,6 +279,7 @@ export function createTasksHandlers(runtime) {
|
|
|
278
279
|
multiProject: new Set(work.items.map(task => task.repo ?? "")).size > 1,
|
|
279
280
|
now,
|
|
280
281
|
state,
|
|
282
|
+
canPutAway: who.via === "cookie" && who.role === "approver",
|
|
281
283
|
// v105: subscription windows and monthly budgets, for whoever runs the installation.
|
|
282
284
|
limits: store.isInstanceOperator(who.name)
|
|
283
285
|
? limitsView(store.providerLimits(), store.budgets().length === 0 ? [] : store.monthSpendCached(now).budgets, { project: projectName, subagent: id => subagentNamesOf(store.handle).get(id) ?? `Subagent ${id}` }, now)
|
|
@@ -3057,7 +3059,9 @@ export function createTasksHandlers(runtime) {
|
|
|
3057
3059
|
live.add(run.id);
|
|
3058
3060
|
return live;
|
|
3059
3061
|
}
|
|
3062
|
+
const tidyHandlers = createTidyHandlers(runtime);
|
|
3060
3063
|
const registrations = handlersOf("tasks", {
|
|
3064
|
+
...tidyHandlers,
|
|
3061
3065
|
"code.page": showCode("workspace"),
|
|
3062
3066
|
"code.session": showCode("session"),
|
|
3063
3067
|
"code.state": showCode("state"),
|
|
@@ -3154,3 +3158,51 @@ export function createTasksHandlers(runtime) {
|
|
|
3154
3158
|
}
|
|
3155
3159
|
return { registrations, runListPane, peekFragment, taskMutation, queueRegionFor, ciFailingFor, completedRowFor, attemptRowFor, resultRowOf, reviewCockpitViewOf, decisionEvidence, runEvidence, sendArtifact, peekGuards, liveRunIds };
|
|
3156
3160
|
}
|
|
3161
|
+
/** Put away and Restore one task, and Put away old tasks (docs/plans/task-tidy.md): the preview reads only (GET-quiet);
|
|
3162
|
+
* putting away many is an approver's step-up bound to the digest of exactly what the preview showed. Each write
|
|
3163
|
+
* re-proves the person's standing inside its own transaction. */
|
|
3164
|
+
export function createTidyHandlers(runtime) {
|
|
3165
|
+
const { store, sendScreen, chromeFor, authenticateApprover, workAccess, bustBadge } = runtime;
|
|
3166
|
+
async function showPutAway(ctx) {
|
|
3167
|
+
const { url, who, response, now, project } = ctx;
|
|
3168
|
+
const days = tidyDays(url.searchParams.get("days"));
|
|
3169
|
+
if (days === null)
|
|
3170
|
+
return refuse(response, who, 400, "Choose a number of days from 1 to 365.", "/work/put-away");
|
|
3171
|
+
const releases = releaseFactsOf(store);
|
|
3172
|
+
const items = tidyPreview(store, now, { releases, days, access: workAccess() });
|
|
3173
|
+
return sendScreen(response, 200, putAwayPage(chromeFor(project, "work"), { items, days, digest: tidyDigest(items, days), now,
|
|
3174
|
+
problem: url.searchParams.get("problem"), releasesSkipped: releases.problem }));
|
|
3175
|
+
}
|
|
3176
|
+
async function applyPutAway(ctx) {
|
|
3177
|
+
const { who, response, now, posted } = ctx;
|
|
3178
|
+
const body = readForm(posted, CONSOLE_FORMS.tasksPutAway);
|
|
3179
|
+
const days = tidyDays(body.get("days"));
|
|
3180
|
+
const back = (problem) => redirect(response, `/work/put-away?${new URLSearchParams({ ...(days === null || days === TIDY_DEFAULT_DAYS ? {} : { days: String(days) }), problem })}`);
|
|
3181
|
+
if (days === null)
|
|
3182
|
+
return back("Choose a number of days from 1 to 365.");
|
|
3183
|
+
if (!authenticateApprover(who, body.get("password") ?? "").ok)
|
|
3184
|
+
return back("Enter your Toolroll password to put these away.");
|
|
3185
|
+
const done = applyTidy(store, now, { by: who.name, days, digest: body.get("digest") ?? "", only: body.getAll("task"), releases: releaseFactsOf(store), access: workAccess() });
|
|
3186
|
+
if (!done.ok)
|
|
3187
|
+
return back(done.message);
|
|
3188
|
+
bustBadge();
|
|
3189
|
+
return redirect(response, "/work?view=archived");
|
|
3190
|
+
}
|
|
3191
|
+
const oneTask = (kind) => async (ctx) => {
|
|
3192
|
+
const { url, who, response, now, posted } = ctx;
|
|
3193
|
+
const act = matchTaskPath(url.pathname, `/${kind}$`);
|
|
3194
|
+
if (act === null)
|
|
3195
|
+
return refuse(response, who, 404, "There's no page at this address.", "/work");
|
|
3196
|
+
const body = readForm(posted, kind === "archive" ? CONSOLE_FORMS.taskArchive : CONSOLE_FORMS.taskRestore);
|
|
3197
|
+
const standing = (refs) => approverStandsNow(store, who.name, familyRepoOf(store, refs));
|
|
3198
|
+
const done = kind === "archive"
|
|
3199
|
+
? archiveTaskFamily(store, act.taskId, { by: who.name, byKind: "person", rule: null, reason: (body.get("reason") ?? "").trim() || "Put away by hand" }, now, { guard: standing })
|
|
3200
|
+
: restoreTaskFamily(store, act.taskId, who.name, now, standing);
|
|
3201
|
+
if (!done.ok)
|
|
3202
|
+
return refuse(response, who, done.reason === "unknown-task" ? 404 : 409, done.message, taskHref(act.taskId));
|
|
3203
|
+
bustBadge();
|
|
3204
|
+
const to = safeReturn(body.get("return"));
|
|
3205
|
+
return redirect(response, to === "/" ? taskHref(act.taskId) : to);
|
|
3206
|
+
};
|
|
3207
|
+
return { "work.put-away": showPutAway, "work.put-away.apply": applyPutAway, "task.act.archive": oneTask("archive"), "task.act.restore": oneTask("restore") };
|
|
3208
|
+
}
|
package/dist/settings-proofs.js
CHANGED
|
@@ -56,6 +56,7 @@ export const PROOF_INVENTORY = [
|
|
|
56
56
|
{ id: "updates", action: "Update Toolroll", routes: ["/settings/updates", "/settings/updates/seen", "/settings/updates/cancel"], form: "updates", field: "password", proof: "step-up", ...ACCEPT, role: "operator", secrets: [], terms: [] },
|
|
57
57
|
{ id: "storage-cleanup", action: "Choose when checkouts are removed", routes: ["/settings/storage"], form: "storage", field: "password", proof: "step-up", ...ACCEPT, role: "operator", secrets: [], terms: [] },
|
|
58
58
|
{ id: "storage-clean", action: "Clean up checkouts", routes: ["/settings/storage/clean"], form: "storage", field: "password", proof: "step-up", ...ACCEPT, role: "operator", secrets: [], terms: ["preview"], note: "`preview` is the digest of what the page showed; a changed list is refused" },
|
|
59
|
+
{ id: "tasks-put-away", action: "Put away old tasks", routes: ["/work/put-away"], form: "tasksPutAway", field: "password", proof: "step-up", ...ACCEPT, role: "approver", secrets: [], terms: ["digest", "days", "task"], note: "`digest` is the preview the page showed; a changed list, or a ticked task outside it, is refused" },
|
|
59
60
|
{ id: "storage-discard", action: "Discard a checkout", routes: ["/settings/storage/discard"], form: "storage", field: "password", proof: "step-up", ...ACCEPT, role: "operator", secrets: [], terms: ["path"] },
|
|
60
61
|
{ id: "project-live-view", action: "Show a project's commands in the live view", routes: ["/settings/project/live-view"], form: "projectLiveView", field: "password", proof: "step-up", ...ACCEPT, role: "any", handlerRole: "an approver of the project", secrets: [], terms: ["repo", "nonce"] },
|
|
61
62
|
{ id: "project-delete", action: "Delete a project", routes: ["/settings/project/delete"], form: "projectDelete", field: "password", proof: "step-up", ...ACCEPT, role: "operator", secrets: [], terms: ["repo", "step"], note: "the project's name typed too" },
|
package/dist/store.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { type ProjectSandbox } from "./project-sandbox.js";
|
|
|
5
5
|
import type { FencePolicy } from "./agent-fence.js";
|
|
6
6
|
import { type NetworkProfile, type ProjectNetwork, type RunNetworkRecord } from "./egress.js";
|
|
7
7
|
import { type AcpLaunch } from "./acp-attest.js";
|
|
8
|
+
export { refArchived, taskArchived } from "./task-archive.js";
|
|
8
9
|
import { type ChatBinding } from "./chat-core.js";
|
|
9
10
|
import { ChatMessages, TELEGRAM_HOLD_REASONS, TELEGRAM_SKIPPED_ELSEWHERE, TELEGRAM_SKIPPED_OTHER_CHAT, TELEGRAM_SKIPPED_QUIET } from "./chat-messages.js";
|
|
10
11
|
import { isLifecycleNotification, isTelegramProgressNotification, LIFECYCLE_KEY_PREFIX, LIFECYCLE_KINDS, proposalTaskOf, type LifecycleKind } from "./notification-rows.js";
|
|
@@ -232,7 +233,7 @@ export type SealedLedgerEntry = LedgerEntry & {
|
|
|
232
233
|
hash: string;
|
|
233
234
|
} | null;
|
|
234
235
|
};
|
|
235
|
-
export declare const SCHEMA_VERSION =
|
|
236
|
+
export declare const SCHEMA_VERSION = 145;
|
|
236
237
|
/** v118: the tables merged into `credential`, in the order they move (a person's API tokens keep their ids first). */
|
|
237
238
|
export declare const V118_MERGED_TABLES: Readonly<Record<string, CredentialKind>>;
|
|
238
239
|
/**
|
|
@@ -394,6 +395,16 @@ export declare const V143_ADDED_COLUMNS: Readonly<Record<string, Readonly<Record
|
|
|
394
395
|
definition: string;
|
|
395
396
|
saved: string;
|
|
396
397
|
}>>>>;
|
|
398
|
+
/** v145 (docs/plans/task-tidy.md): `task_archive` is a put-away task version. A row hides it from every list and count
|
|
399
|
+
* and refuses every door that would dispatch, claim or approve it; restore deletes the row. Its history, results,
|
|
400
|
+
* approvals and ledger stay exactly as they were. `merge_in_flight` is a direct merge between its last check and GitHub's
|
|
401
|
+
* answer, owned by its token: a task can't be put away under it (a row older than 15 minutes is a crash, and no
|
|
402
|
+
* longer counts). `deploy_in_flight` is a deployment whose candidate passed its gate (30 minutes, refreshed by every
|
|
403
|
+
* phase that proves it again). `flow_step_task` is the tasks a flow step was admitted for, kept whatever the card does
|
|
404
|
+
* next, so a step still running holds them. `branch_check_run` keeps each completed GitHub workflow run (pushed to
|
|
405
|
+
* the watched branch of the watched repository) the "checks failed" trigger saw, green or red, so tidy can tell a
|
|
406
|
+
* failure notice was fixed by a later commit. */
|
|
407
|
+
export declare const V145_SCHEMA = "\nCREATE TABLE IF NOT EXISTS task_archive (\n task_ref INTEGER PRIMARY KEY REFERENCES task_ref(id),\n by TEXT NOT NULL,\n by_kind TEXT NOT NULL CHECK (by_kind IN ('person','tidy')),\n rule TEXT,\n reason TEXT NOT NULL,\n at TEXT NOT NULL\n);\nCREATE TABLE IF NOT EXISTS branch_check_run (\n repo TEXT NOT NULL,\n run_id INTEGER NOT NULL,\n branch TEXT NOT NULL,\n workflow TEXT NOT NULL,\n head_sha TEXT NOT NULL,\n conclusion TEXT NOT NULL CHECK (conclusion IN ('success','failure')),\n run_created_at TEXT NOT NULL,\n observed_at TEXT NOT NULL,\n PRIMARY KEY (repo, run_id)\n);\nCREATE INDEX IF NOT EXISTS branch_check_run_by_workflow ON branch_check_run (repo, branch, workflow, run_created_at);\nCREATE TABLE IF NOT EXISTS merge_in_flight (\n publication INTEGER PRIMARY KEY REFERENCES publication(id),\n task_ref INTEGER NOT NULL REFERENCES task_ref(id),\n token TEXT NOT NULL,\n by TEXT NOT NULL,\n at TEXT NOT NULL\n);\nCREATE TABLE IF NOT EXISTS deploy_in_flight (\n task_ref INTEGER PRIMARY KEY REFERENCES task_ref(id),\n run INTEGER NOT NULL,\n at TEXT NOT NULL\n);\nCREATE TABLE IF NOT EXISTS flow_step_task (\n card INTEGER NOT NULL,\n entry INTEGER NOT NULL,\n task_id TEXT NOT NULL,\n PRIMARY KEY (card, entry, task_id)\n);\n";
|
|
397
408
|
/**
|
|
398
409
|
* v135: one approval per release (docs/plans/release-once.md). `release_authority` is the single source of truth for a
|
|
399
410
|
* release a person approved: one row per generation, changed only by compare-and-swap on `rev`, with a lease whose
|
|
@@ -3745,7 +3756,7 @@ export declare class Store {
|
|
|
3745
3756
|
rejectedRun: number | null;
|
|
3746
3757
|
} | {
|
|
3747
3758
|
ok: false;
|
|
3748
|
-
reason: "unknown-task" | "not-stalled" | "claimed" | "accepted-result" | "published" | "moved";
|
|
3759
|
+
reason: "unknown-task" | "not-stalled" | "claimed" | "accepted-result" | "published" | "moved" | "archived";
|
|
3749
3760
|
};
|
|
3750
3761
|
/**
|
|
3751
3762
|
* Cancel, aware of who might be building right now. A live claim wins:
|
|
@@ -6892,7 +6903,7 @@ export declare class Store {
|
|
|
6892
6903
|
stop: RunStop;
|
|
6893
6904
|
} | {
|
|
6894
6905
|
ok: false;
|
|
6895
|
-
reason: "no-stop" | "wrong-task" | "stopping" | "already-resumed" | "superseded" | "busy" | "review";
|
|
6906
|
+
reason: "no-stop" | "wrong-task" | "stopping" | "already-resumed" | "superseded" | "busy" | "review" | "archived";
|
|
6896
6907
|
detail: string;
|
|
6897
6908
|
};
|
|
6898
6909
|
/** Every stop recorded on a task, newest request first. */
|
|
@@ -8064,3 +8075,60 @@ export declare function dropFeedbackImage(store: Store, runId: number, commentId
|
|
|
8064
8075
|
ok: false;
|
|
8065
8076
|
reason: "not-found" | "image-usable" | "not-authorized";
|
|
8066
8077
|
};
|
|
8078
|
+
/** A put-away task version (v145, docs/plans/task-tidy.md). */
|
|
8079
|
+
export type TaskArchive = {
|
|
8080
|
+
taskRef: number;
|
|
8081
|
+
taskId: string;
|
|
8082
|
+
by: string;
|
|
8083
|
+
byKind: "person" | "tidy";
|
|
8084
|
+
rule: string | null;
|
|
8085
|
+
reason: string;
|
|
8086
|
+
at: string;
|
|
8087
|
+
};
|
|
8088
|
+
/** Who put a task away, and why: a person by name, or tidy by its rule. */
|
|
8089
|
+
export type ArchiveBy = {
|
|
8090
|
+
by: string;
|
|
8091
|
+
byKind: "person" | "tidy";
|
|
8092
|
+
rule: string | null;
|
|
8093
|
+
reason: string;
|
|
8094
|
+
};
|
|
8095
|
+
export type ArchiveRefusal = "unknown-task" | "live" | "archived" | "not-archived" | "blocked";
|
|
8096
|
+
/** The put-away record of this task, or null when it is not put away. */
|
|
8097
|
+
export declare function archiveOf(store: Store, taskId: string): TaskArchive | null;
|
|
8098
|
+
/** Every version of a task's family, by the same proven lineage the task page reads (`taskFamilyOf`: same project, each
|
|
8099
|
+
* revision bound to its parent's own run by its brief). A family whose lineage doesn't prove is just this task. */
|
|
8100
|
+
export declare function taskFamilyRefs(store: Store, taskId: string): {
|
|
8101
|
+
id: number;
|
|
8102
|
+
taskId: string;
|
|
8103
|
+
}[];
|
|
8104
|
+
/** The first family version with something still going on, or null: running, a live newest claim, an unfinished run,
|
|
8105
|
+
* a process Toolroll hasn't seen exit, a build whose processes were never recorded, a stop still settling, a follow-up
|
|
8106
|
+
* check asked for and not finished, a flow step running for its card, a release in flight (a live release approval
|
|
8107
|
+
* that names it, or a deploy or merge reserved, running or unknown), or a pull request merging (an intent claimed or
|
|
8108
|
+
* firing, or a person's merge between its last check and GitHub's answer). */
|
|
8109
|
+
export declare function liveFamilyAttempt(store: Store, refs: readonly number[], now: Date): string | null;
|
|
8110
|
+
/** Puts a task away (v145): every version of its family, in one write transaction that re-checks it has no live
|
|
8111
|
+
* attempt, that its versions are exactly `expect` when the caller showed a person a family (a preview), and that
|
|
8112
|
+
* `guard` (the caller's own rule, re-read under the lock) still allows it. Its state, history, results, approvals and
|
|
8113
|
+
* ledger are untouched; its open notifications are resolved; one ledger row records who and why. */
|
|
8114
|
+
export declare function archiveTaskFamily(store: Store, taskId: string, by: ArchiveBy, now: Date, options?: {
|
|
8115
|
+
expect?: readonly number[];
|
|
8116
|
+
guard?: (refs: readonly number[]) => string | null;
|
|
8117
|
+
}): {
|
|
8118
|
+
ok: true;
|
|
8119
|
+
refs: number[];
|
|
8120
|
+
} | {
|
|
8121
|
+
ok: false;
|
|
8122
|
+
reason: ArchiveRefusal;
|
|
8123
|
+
message: string;
|
|
8124
|
+
};
|
|
8125
|
+
/** Brings a put-away task back (v145): every version of its family, with a ledger row. Tidy never puts a restored
|
|
8126
|
+
* task away again (tidy.ts reads the `task restored` row). */
|
|
8127
|
+
export declare function restoreTaskFamily(store: Store, taskId: string, by: string, now: Date, guard?: (refs: readonly number[]) => string | null): {
|
|
8128
|
+
ok: true;
|
|
8129
|
+
refs: number[];
|
|
8130
|
+
} | {
|
|
8131
|
+
ok: false;
|
|
8132
|
+
reason: ArchiveRefusal;
|
|
8133
|
+
message: string;
|
|
8134
|
+
};
|