toolroll 0.9.85 → 0.9.87
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 +83 -0
- package/dist/agent-fence.d.ts +6 -2
- package/dist/agent-fence.js +3 -2
- package/dist/agents-board.d.ts +12 -0
- package/dist/agents-board.js +26 -0
- package/dist/approval-rules-ui.d.ts +4 -1
- package/dist/approval-rules-ui.js +31 -15
- package/dist/assignment-ui.js +1 -1
- package/dist/assignment.js +4 -3
- package/dist/backup-ui.d.ts +5 -4
- package/dist/backup-ui.js +65 -32
- package/dist/browser/terminal.css +1 -1
- package/dist/browser/terminal.js +57 -54
- package/dist/browser/workspace.css +2 -2
- package/dist/browser/workspace.js +35 -68
- package/dist/browser-crew.js +1 -1
- package/dist/browser-workspace.d.ts +363 -177
- package/dist/browser-workspace.js +10 -9
- 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-controls.d.ts +1 -1
- package/dist/chat-controls.js +1 -1
- package/dist/chat-quiet.js +3 -2
- package/dist/chat-settings-page.d.ts +29 -9
- package/dist/chat-settings-page.js +59 -28
- package/dist/check-levels-ui.d.ts +2 -1
- package/dist/check-levels-ui.js +67 -58
- 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/cli.d.ts +43 -0
- package/dist/contracts/cli.js +4 -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/credentials-ui.d.ts +5 -3
- package/dist/credentials-ui.js +58 -31
- package/dist/deploy-gate.js +4 -0
- package/dist/design/kit.css +685 -0
- package/dist/design/kit.d.ts +208 -0
- package/dist/design/kit.js +177 -0
- package/dist/design/tokens.css +19 -1
- package/dist/discord-settings.js +8 -4
- package/dist/dispatch.d.ts +3 -0
- package/dist/dispatch.js +4 -1
- package/dist/export-ui.d.ts +6 -4
- package/dist/export-ui.js +18 -8
- package/dist/flow-browser.d.ts +103 -0
- package/dist/flow-browser.js +204 -0
- package/dist/flow-changed.d.ts +14 -0
- package/dist/flow-changed.js +53 -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-gallery-ui.d.ts +41 -4
- package/dist/flow-gallery-ui.js +95 -63
- 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-starters-ui.d.ts +3 -9
- package/dist/flow-starters-ui.js +12 -23
- package/dist/flow-steps.d.ts +9 -1
- package/dist/flow-steps.js +55 -5
- package/dist/flow-triggers.js +55 -0
- package/dist/flow-unattended-words.d.ts +20 -0
- package/dist/flow-unattended-words.js +24 -0
- package/dist/flow-unattended.d.ts +9 -5
- package/dist/flow-unattended.js +45 -23
- package/dist/flows-cli.d.ts +19 -0
- package/dist/flows-cli.js +54 -5
- package/dist/flows-ui.d.ts +12 -6
- package/dist/flows-ui.js +152 -32
- package/dist/flows.js +5 -2
- package/dist/integrations-ui.d.ts +6 -4
- package/dist/integrations-ui.js +34 -40
- package/dist/integrations.d.ts +2 -2
- package/dist/integrations.js +4 -4
- package/dist/kits-ui.d.ts +8 -10
- package/dist/kits-ui.js +29 -33
- package/dist/kits.js +1 -1
- package/dist/knowledge-ui.js +76 -19
- package/dist/lead-tools.js +35 -6
- package/dist/lead-voice.js +16 -5
- package/dist/ledger-view.d.ts +2 -0
- package/dist/ledger-view.js +41 -38
- package/dist/models-ui.d.ts +8 -3
- package/dist/models-ui.js +82 -59
- package/dist/monitoring-ui.d.ts +4 -2
- package/dist/monitoring-ui.js +44 -13
- package/dist/needs-you.d.ts +1 -1
- package/dist/needs-you.js +8 -0
- package/dist/operate-remote-arguments.js +1 -0
- package/dist/operate-remote.js +1 -1
- package/dist/operate.d.ts +1 -1
- package/dist/operate.js +249 -6
- package/dist/passwordless.d.ts +1 -1
- package/dist/passwordless.js +1 -1
- package/dist/people-audit-ui.d.ts +2 -1
- package/dist/people-audit-ui.js +25 -23
- package/dist/policy-ui.d.ts +3 -2
- package/dist/policy-ui.js +27 -21
- package/dist/policy.d.ts +3 -0
- package/dist/policy.js +14 -1
- package/dist/project-delete-ui.d.ts +4 -2
- package/dist/project-delete-ui.js +135 -126
- package/dist/project-delete.js +7 -3
- package/dist/project-knowledge.d.ts +10 -10
- package/dist/publish.d.ts +11 -0
- package/dist/publish.js +16 -3
- package/dist/pull-request-flow.js +35 -4
- package/dist/pull-request-ui.d.ts +2 -1
- package/dist/pull-request-ui.js +39 -28
- package/dist/recipe-ui.d.ts +4 -2
- package/dist/recipe-ui.js +14 -4
- package/dist/release-adapters.d.ts +3 -0
- package/dist/release-adapters.js +22 -2
- package/dist/release-cli.js +81 -9
- package/dist/release.d.ts +21 -0
- package/dist/release.js +40 -4
- package/dist/releases.js +1 -1
- package/dist/request-budget-ui.d.ts +4 -2
- package/dist/request-budget-ui.js +30 -19
- package/dist/result-follow-ups.js +4 -0
- package/dist/retention-ui.d.ts +4 -3
- package/dist/retention-ui.js +15 -12
- package/dist/scope.d.ts +8 -0
- package/dist/scope.js +13 -0
- package/dist/serve.d.ts +0 -1
- package/dist/serve.js +51 -7
- package/dist/server/agents-computers.d.ts +9 -0
- package/dist/server/agents-computers.js +67 -0
- package/dist/server/chat.js +20 -18
- package/dist/server/chrome.d.ts +1 -1
- package/dist/server/chrome.js +19 -9
- package/dist/server/console.css +31 -2
- package/dist/server/flows.js +79 -36
- package/dist/server/home.d.ts +35 -0
- package/dist/server/home.js +104 -0
- package/dist/server/http.d.ts +3 -0
- package/dist/server/http.js +5 -0
- package/dist/server/kit-standalone.d.ts +22 -0
- package/dist/server/kit-standalone.js +69 -0
- package/dist/server/kit.d.ts +405 -0
- package/dist/server/kit.js +361 -0
- package/dist/server/pages.d.ts +2 -1
- package/dist/server/pages.js +213 -174
- package/dist/server/people-tokens.js +26 -85
- package/dist/server/render-history.d.ts +51 -0
- package/dist/server/render-history.js +228 -0
- package/dist/server/render-new-task.d.ts +29 -0
- package/dist/server/render-new-task.js +96 -0
- package/dist/server/render-next.d.ts +63 -0
- package/dist/server/render-next.js +109 -0
- package/dist/server/render-pages.d.ts +35 -190
- package/dist/server/render-pages.js +154 -563
- package/dist/server/render-people.d.ts +24 -3
- package/dist/server/render-people.js +93 -72
- package/dist/server/render-projects.d.ts +103 -0
- package/dist/server/render-projects.js +247 -0
- package/dist/server/render-results.d.ts +5 -24
- package/dist/server/render-results.js +48 -185
- package/dist/server/render-settings.d.ts +5 -74
- package/dist/server/render-settings.js +102 -493
- package/dist/server/render-tasks.d.ts +43 -5
- package/dist/server/render-tasks.js +131 -91
- package/dist/server/route-table.js +31 -5
- package/dist/server/settings.js +276 -255
- package/dist/server/task-views.d.ts +8 -0
- package/dist/server/task-views.js +90 -2
- package/dist/server/tasks.d.ts +18 -0
- package/dist/server/tasks.js +148 -41
- package/dist/server/terminals.js +1 -1
- package/dist/server/welcome.d.ts +22 -0
- package/dist/server/welcome.js +69 -0
- package/dist/settings-groups-ui.d.ts +279 -0
- package/dist/settings-groups-ui.js +615 -0
- package/dist/settings-kit.d.ts +42 -6
- package/dist/settings-kit.js +69 -7
- package/dist/settings-proofs.js +1 -0
- package/dist/setup-guide.d.ts +2 -2
- package/dist/skills-ui.js +59 -7
- package/dist/slack-settings.js +12 -5
- package/dist/spend-ui.d.ts +45 -26
- package/dist/spend-ui.js +124 -56
- package/dist/sso-ui.d.ts +5 -2
- package/dist/sso-ui.js +63 -31
- package/dist/storage-ui.d.ts +3 -2
- package/dist/storage-ui.js +56 -50
- package/dist/store.d.ts +182 -5
- package/dist/store.js +512 -38
- package/dist/surface.js +23 -3
- 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/teams-settings.d.ts +2 -1
- package/dist/teams-settings.js +24 -41
- package/dist/telegram-settings.d.ts +0 -4
- package/dist/telegram-settings.js +52 -25
- package/dist/tidy.d.ts +130 -0
- package/dist/tidy.js +503 -0
- package/dist/toolroll-update-ui.d.ts +8 -4
- package/dist/toolroll-update-ui.js +55 -45
- package/dist/tools-ui.d.ts +5 -3
- package/dist/tools-ui.js +107 -42
- package/dist/work-index.d.ts +24 -0
- package/dist/work-index.js +56 -30
- package/dist/workspace-ui.d.ts +1 -1
- package/dist/workspace-ui.js +8 -4
- package/package.json +1 -1
package/dist/operate.js
CHANGED
|
@@ -28,6 +28,8 @@ import { idleStorageSweep, sweepSchedule, sweepScheduleWords } from "./storage-s
|
|
|
28
28
|
import { lastSweep, saveSweep, sweepDetails, sweepWords as storageSweepWords } from "./storage-sweep.js";
|
|
29
29
|
import { removeShared, sharedDepsRoot, sharedUse, unusedShared } from "./shared-deps.js";
|
|
30
30
|
import { cleanLandedBacklog, gitOf, landedBacklog, reconcileLanded, BACKLOG_DAYS } from "./landed-cleanup.js";
|
|
31
|
+
import { accessOf, applyTidy, approverStandsNow, withinLens, familyRepoOf, releaseFactsFor, runTidy, tidyDays, tidyDigest, tidyGroups, tidyPreview, TIDY_DEFAULT_DAYS, TIDY_PASS_LIMIT } from "./tidy.js";
|
|
32
|
+
import { personProof } from "./person-proof.js";
|
|
31
33
|
import { RemoteMains } from "./remote-main.js";
|
|
32
34
|
import { checkoutPlan, cleanCheckouts, discardCheckout, finishedCheckouts, slimKeptCheckouts, whyWords } from "./checkout-cleanup.js";
|
|
33
35
|
import { closeOutCheckouts, closeOutRuns, stoppedWords } from "./run-closeout.js";
|
|
@@ -105,7 +107,8 @@ import { taskReviewBrief, renderReviewBrief } from "./task-review-brief.js";
|
|
|
105
107
|
import { homedir, hostname } from "node:os";
|
|
106
108
|
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
107
109
|
import { releaseStalledUpdate, waitingUpdate } from "./toolroll-update.js";
|
|
108
|
-
import {
|
|
110
|
+
import { dur } from "./design/kit.js";
|
|
111
|
+
import { openStore, openStoreNoMigrate, archiveTaskFamily, restoreTaskFamily, shipChecksForTask, elapsedSeconds, CONCURRENT_WRITER_WAIT_MS, isDatabaseBusy, databasePath, BUILT_IN, parseCapabilityKey, verifiedAuthor, isDigestTime, predecessorWatches, RESULT_SCREENSHOTS, lifecycleOf, reportLifecycle, beatLifecycle, } from "./store.js";
|
|
109
112
|
import { createHash, randomBytes, randomInt, randomUUID } from "node:crypto";
|
|
110
113
|
import { claimInstance, clearInstance, openInstanceSocket } from "./console-instance.js";
|
|
111
114
|
import { authorizePlanUnderMode } from "./plan-auto.js";
|
|
@@ -273,7 +276,11 @@ export const OPERATE_HELP = `toolroll — operating the queue
|
|
|
273
276
|
the lead passes it as --token, or TOOLROLL_LEAD_TOKEN, and its work pings nobody
|
|
274
277
|
toolroll task ask <id> --person <name> --why "<text>"
|
|
275
278
|
hand a task to a person: it pings them, and it is theirs
|
|
276
|
-
toolroll task list [--view <v>] [--limit <n>] [--cursor <c>] paginated saved task status
|
|
279
|
+
toolroll task list [--view <v>] [--limit <n>] [--cursor <c>] paginated saved task status (--view archived: put away)
|
|
280
|
+
toolroll task archive <id...> [--reason <why>] put tasks away: out of every list, history kept (a person's act)
|
|
281
|
+
toolroll task restore <id...> bring put-away tasks back exactly as they were
|
|
282
|
+
toolroll task tidy --preview [--days N] what Put away old tasks would put away, and why (read-only)
|
|
283
|
+
toolroll task tidy --apply --digest <d> [--except <id,...>] put away exactly that list
|
|
277
284
|
toolroll task show <id>
|
|
278
285
|
toolroll task wait <id> [--timeout <seconds>]
|
|
279
286
|
wait for this attempt to finish or need a person
|
|
@@ -338,6 +345,7 @@ export const OPERATE_HELP = `toolroll — operating the queue
|
|
|
338
345
|
[--anyway] merge without waiting for the full check
|
|
339
346
|
toolroll task checks <id> [--level quick|full]
|
|
340
347
|
run checks on the result's exact commit now
|
|
348
|
+
toolroll task ship <id> its pull request's checks, each with its state and how long it took
|
|
341
349
|
toolroll task add-tests <id> file a small task to write tests for that change
|
|
342
350
|
toolroll task revise <id> --feedback "requested change"
|
|
343
351
|
toolroll assignment show <task> root, current work and exact handoff
|
|
@@ -663,7 +671,7 @@ export const TASK_ACTIONS = [
|
|
|
663
671
|
"add", "list", "show", "wait", "state", "block", "unblock", "next", "steer", "pause", "continue", "assign",
|
|
664
672
|
"reopen", "scope", "approve", "hold", "unhold", "require", "requeue", "regate", "plan",
|
|
665
673
|
"review", "accept", "repair", "route", "stop", "resume", "complete", "revise", "merge", "ask",
|
|
666
|
-
"checks", "add-tests",
|
|
674
|
+
"checks", "add-tests", "archive", "restore", "tidy", "ship",
|
|
667
675
|
];
|
|
668
676
|
export const PUBLISH_ACTIONS = ["setup", "grant", "revoke", "status", "unblock", "rearm", "merge", "refire"];
|
|
669
677
|
export const CONFIG_ACTIONS = ["show", "set", "clear"];
|
|
@@ -707,6 +715,8 @@ export const OPERATE_VALUE_FLAGS = new Set([
|
|
|
707
715
|
"starter",
|
|
708
716
|
// flows: templates, steps, scripts and cards.
|
|
709
717
|
"template", "steps", "about", "language", "timeout-minutes", "body", "description", "zone", "option",
|
|
718
|
+
// task tidy --apply: the previewed tasks to leave out.
|
|
719
|
+
"except",
|
|
710
720
|
"token-env", "after", "repair-max-attempts", "consumer", "batch", "feedback", "source", "view", "cursor", "why", "supersedes", "decision", "sessions", "timeout",
|
|
711
721
|
// pings follow responsibility: replacements.
|
|
712
722
|
"replaced-by", "replaces",
|
|
@@ -757,6 +767,8 @@ export const OPERATE_BOOLEAN_FLAGS = new Set([
|
|
|
757
767
|
"password-stdin",
|
|
758
768
|
// task steer: pause the running Claude build, then send the note.
|
|
759
769
|
"now",
|
|
770
|
+
// task tidy: look first (--preview), then put away exactly what was shown (--apply --digest).
|
|
771
|
+
"preview", "apply",
|
|
760
772
|
]);
|
|
761
773
|
/** `project network` reads --remove <hosts> and --local any|none as values, where every other verb's --remove and
|
|
762
774
|
* --local are switches. */
|
|
@@ -1564,6 +1576,8 @@ function claimCommand(positional, flags, context) {
|
|
|
1564
1576
|
return fail(write, json, "claim", "sweep-limit", `its sweep already has ${result.limit} running at once — it starts when one finishes`, EXIT.refused, { sweep: result.sweep, limit: result.limit });
|
|
1565
1577
|
if (result.reason === "watch-custody")
|
|
1566
1578
|
return fail(write, json, "claim", "watch-custody", "This worker no longer holds the project; restart the watch", EXIT.refused);
|
|
1579
|
+
if (result.reason === "archived")
|
|
1580
|
+
return fail(write, json, "claim", "archived", `${id} is put away — restore it with \`toolroll task restore ${id}\` first`, EXIT.refused);
|
|
1567
1581
|
if (result.reason === "draining")
|
|
1568
1582
|
return fail(write, json, "claim", "draining", `Toolroll is restarting (draining): ${runner} takes no new work until it is back (by ${result.until})`, EXIT.refused, { until: result.until });
|
|
1569
1583
|
return fail(write, json, "claim", result.reason, `held by ${result.by} until ${result.until}`, EXIT.refused, { holder: result.by, until: result.until });
|
|
@@ -3562,6 +3576,13 @@ async function reconcileCommand(flags, context) {
|
|
|
3562
3576
|
remotes.close();
|
|
3563
3577
|
}
|
|
3564
3578
|
const landedGone = landed.steps.filter(one => one.outcome === "removed");
|
|
3579
|
+
// Tidy (task-tidy.md): old routine runs a newer run replaces, release tasks already published or replaced, and CI
|
|
3580
|
+
// notices main has fixed are put away, at most a bounded number a pass, each with its reason in the ledger.
|
|
3581
|
+
let tidied = [];
|
|
3582
|
+
try {
|
|
3583
|
+
tidied = runTidy(store, clock(), releaseFactsFor(context.databaseFile), TIDY_PASS_LIMIT, { principal: "operator", repos: [repo], includeUnplaced: false });
|
|
3584
|
+
}
|
|
3585
|
+
catch { /* the next pass tries again */ }
|
|
3565
3586
|
// Checkout cleanup (Settings → Storage): finished work's clean checkouts go as the setting says (by default when
|
|
3566
3587
|
// the task is complete or cancelled); their branches stay. Each removal is in the ledger.
|
|
3567
3588
|
// Completing or cancelling a task stops anything still running from its checkout first.
|
|
@@ -3605,6 +3626,7 @@ async function reconcileCommand(flags, context) {
|
|
|
3605
3626
|
slimmed.slimmed.length === 0 &&
|
|
3606
3627
|
landed.completed.length === 0 &&
|
|
3607
3628
|
landedGone.length === 0 &&
|
|
3629
|
+
tidied.length === 0 &&
|
|
3608
3630
|
liveSwept.removed.length === 0;
|
|
3609
3631
|
return succeed(write, json, "reconcile", {
|
|
3610
3632
|
recovered,
|
|
@@ -3616,6 +3638,7 @@ async function reconcileCommand(flags, context) {
|
|
|
3616
3638
|
processesStopped: [...closedRuns.map(one => ({ run: one.runId, pids: one.done.stopped.map(row => row.pid) })), ...closedCheckouts.map(one => ({ checkout: one.path, pids: one.done.stopped.map(row => row.pid) }))],
|
|
3617
3639
|
checkoutsSlimmed: slimmed.slimmed.map(one => one.path),
|
|
3618
3640
|
landed: { completed: landed.completed, steps: landed.steps },
|
|
3641
|
+
tidied: tidied.map(one => ({ id: one.taskId, rule: one.rule, reason: one.reason })),
|
|
3619
3642
|
liveViewsSwept: liveSwept.removed.length,
|
|
3620
3643
|
retention: retention === null ? null : { counts: retention.counts, freedBytes: retention.freed },
|
|
3621
3644
|
storageSweep: storageSwept,
|
|
@@ -3630,6 +3653,7 @@ async function reconcileCommand(flags, context) {
|
|
|
3630
3653
|
...closedRuns.map(one => `Run #${one.runId} ended; stopped ${stoppedWords(one.done)}.`),
|
|
3631
3654
|
...closedCheckouts.map(one => `${one.path}: its task finished; stopped ${stoppedWords(one.done)}.`),
|
|
3632
3655
|
...(landed.completed.length === 0 ? [] : [`Marked ${landed.completed.length} landed result(s) complete: ${landed.completed.join(", ")}.`]),
|
|
3656
|
+
...(tidied.length === 0 ? [] : [`Put away ${tidied.length} old task(s): ${tidied.map(one => one.taskId).join(", ")}.`]),
|
|
3633
3657
|
...(landedGone.length === 0 ? [] : [`Removed landed work: ${landedGone.filter(one => one.kind === "checkout").length} checkout(s), ${landedGone.filter(one => one.kind === "branch").length} local branch(es).`]),
|
|
3634
3658
|
...(slimmed.slimmed.length === 0 ? [] : [`Dropped dependencies and build output from ${slimmed.slimmed.length} kept checkout(s), about ${bytesWords(slimmed.slimmed.reduce((sum, one) => sum + one.bytes, 0))}.`]),
|
|
3635
3659
|
...(liveSwept.removed.length === 0 ? [] : [`Cleared ${liveSwept.removed.length} finished live view(s).`]),
|
|
@@ -4112,6 +4136,7 @@ async function requeueTask(positional, flags, context) {
|
|
|
4112
4136
|
published: `${id}'s last result is published; revise it through its pull request`,
|
|
4113
4137
|
// Only an Approve & build again asks for an exact build; the CLI never does.
|
|
4114
4138
|
moved: `${id} changed since its page was read; read it again`,
|
|
4139
|
+
archived: `${id} is put away — restore it with \`toolroll task restore ${id}\` first`,
|
|
4115
4140
|
};
|
|
4116
4141
|
return fail(write, json, "task requeue", result.reason, why[result.reason], EXIT.refused);
|
|
4117
4142
|
}
|
|
@@ -8563,10 +8588,18 @@ function taskAction(action, rest, flags, context) {
|
|
|
8563
8588
|
return addTask(rest, flags, context);
|
|
8564
8589
|
case "checks":
|
|
8565
8590
|
return runTaskChecks(rest, flags, context);
|
|
8591
|
+
case "ship":
|
|
8592
|
+
return taskShipCommand(rest, context);
|
|
8566
8593
|
case "add-tests":
|
|
8567
8594
|
return addTestsTask(rest, flags, context);
|
|
8568
8595
|
case "list":
|
|
8569
8596
|
return listTasks(flags, context);
|
|
8597
|
+
case "archive":
|
|
8598
|
+
return archiveTasksCommand(rest, flags, context);
|
|
8599
|
+
case "restore":
|
|
8600
|
+
return restoreTasksCommand(rest, flags, context);
|
|
8601
|
+
case "tidy":
|
|
8602
|
+
return tidyTasksCommand(rest, flags, context);
|
|
8570
8603
|
case "show":
|
|
8571
8604
|
return showTask(rest, context);
|
|
8572
8605
|
case "wait":
|
|
@@ -8988,9 +9021,9 @@ function listTasks(flags, context) {
|
|
|
8988
9021
|
const limit = rawLimit === undefined ? 40 : Number(rawLimit);
|
|
8989
9022
|
if (wanted !== undefined && !STATES.includes(wanted))
|
|
8990
9023
|
return fail(write, json, 'task list', 'usage', `--state takes one of ${STATES.join(', ')}`, EXIT.usage);
|
|
8991
|
-
if (rawView !== undefined && !['all', 'needs-you', 'running', 'completed'].includes(rawView) ||
|
|
9024
|
+
if (rawView !== undefined && !['all', 'needs-you', 'running', 'queued', 'completed', 'archived'].includes(rawView) ||
|
|
8992
9025
|
!Number.isSafeInteger(limit) || limit < 1 || limit > 100) {
|
|
8993
|
-
return fail(write, json, 'task list', 'usage', '--view takes all, needs-you, running or
|
|
9026
|
+
return fail(write, json, 'task list', 'usage', '--view takes all, needs-you, running, queued, completed or archived; --limit takes 1–100', EXIT.usage);
|
|
8994
9027
|
}
|
|
8995
9028
|
// A remote person limited to some projects (remoteLensOf) pages and counts only theirs.
|
|
8996
9029
|
const lens = context.principal?.lens ?? null;
|
|
@@ -9017,7 +9050,8 @@ function listTasks(flags, context) {
|
|
|
9017
9050
|
write(wanted === undefined ? 'The task list is empty.' : `Nothing is ${wanted}.`);
|
|
9018
9051
|
const width = Math.max(0, ...tasks.map(task => task.id.length));
|
|
9019
9052
|
for (const task of tasks)
|
|
9020
|
-
write(` ${task.id.padEnd(width)} ${task.status.label.padEnd(16)} ${task.title}`
|
|
9053
|
+
write(task.archived === undefined ? ` ${task.id.padEnd(width)} ${task.status.label.padEnd(16)} ${task.title}`
|
|
9054
|
+
: ` ${task.id.padEnd(width)} ${'Archived'.padEnd(16)} ${task.title} — by ${task.archived.byKind === 'tidy' ? 'tidy' : task.archived.by}: ${task.archived.reason}`);
|
|
9021
9055
|
if (page.nextCursor !== null)
|
|
9022
9056
|
write(`Next page: --cursor ${page.nextCursor}`);
|
|
9023
9057
|
return EXIT.ok;
|
|
@@ -10149,6 +10183,8 @@ function describeApproveFailure(reason, id) {
|
|
|
10149
10183
|
// Approval policy, phase 1: the lead's token (or a remote API token) proves a credential, not a person.
|
|
10150
10184
|
if (reason === "machine-proof")
|
|
10151
10185
|
return "only a person can approve this — approve it on the task page or from your paired chat";
|
|
10186
|
+
if (reason === "archived")
|
|
10187
|
+
return `${id} is put away — restore it with \`toolroll task restore ${id}\` first`;
|
|
10152
10188
|
if (reason === "second-approver")
|
|
10153
10189
|
return `your approval of ${id} is recorded; it is protected work, so a second person needs to approve it (toolroll task approve ${id} as them)`;
|
|
10154
10190
|
if (reason === "no-approvers") {
|
|
@@ -12754,3 +12790,210 @@ export function consoleIdentitiesInBackground(identities, paths) {
|
|
|
12754
12790
|
export function writeModelRow(store, proved, scope, knownProject, slot, to, now, extra = []) {
|
|
12755
12791
|
return applyModelChoices(store, { scope, change: slot === null ? [] : [{ slot, to }], expected: null, strict: false, knownProject, extra }, { ...proved, live: () => true }, now);
|
|
12756
12792
|
}
|
|
12793
|
+
/** Putting away and restoring are a person's acts (docs/plans/task-tidy.md): never the lead's token, never a remote
|
|
12794
|
+
* API token, never a machine proof. Null when a person is acting. */
|
|
12795
|
+
function refuseMachineTidy(context, command, method) {
|
|
12796
|
+
if (machineActing() || process.env["TOOLROLL_LEAD_TOKEN"] !== undefined || (method !== undefined && !personProof(method))) {
|
|
12797
|
+
return fail(context.write, context.json, command, "person-only", "Only a person puts tasks away or restores them. Use the console or your own login.", EXIT.refused);
|
|
12798
|
+
}
|
|
12799
|
+
return null;
|
|
12800
|
+
}
|
|
12801
|
+
/** `toolroll task archive <id...> [--reason <text>]`: put tasks away by hand (v145). Each keeps its history, results and
|
|
12802
|
+
* approvals; nothing can run, claim or approve it until it is restored. Credentialed, a person's act. */
|
|
12803
|
+
async function archiveTasksCommand(positional, flags, context) {
|
|
12804
|
+
const { store, write, json, clock } = context;
|
|
12805
|
+
const command = "task archive";
|
|
12806
|
+
const ids = [...new Set(positional)];
|
|
12807
|
+
if (ids.length === 0 || ids.length > 100)
|
|
12808
|
+
return fail(write, json, command, "usage", "`toolroll task archive <id...> [--reason <why>] --as <you> --token <t>` — 1 to 100 tasks", EXIT.usage);
|
|
12809
|
+
const machine = refuseMachineTidy(context, command);
|
|
12810
|
+
if (machine !== null)
|
|
12811
|
+
return machine;
|
|
12812
|
+
const demo = refuseDemo(context, command);
|
|
12813
|
+
if (demo !== null)
|
|
12814
|
+
return demo;
|
|
12815
|
+
const acting = await askCredentials(flags, context);
|
|
12816
|
+
// The password is proved against a project the person can see (the first task's); each task's own project is
|
|
12817
|
+
// re-proved inside its put-away transaction.
|
|
12818
|
+
const verified = acting === null ? null : authenticateApprover(store, acting.name, acting.token, firstVisibleRepo(store, acting.name, ids));
|
|
12819
|
+
if (acting === null || !verified?.ok)
|
|
12820
|
+
return refuseApprover(context, command, verified, "putting a task away takes `--as <you> --token <t>` — it is recorded against your name", ids[0]);
|
|
12821
|
+
const personal = refuseMachineTidy(context, command, verified.method);
|
|
12822
|
+
if (personal !== null)
|
|
12823
|
+
return personal;
|
|
12824
|
+
const reason = text(flags, "reason") ?? "Put away by hand";
|
|
12825
|
+
const archived = [], refused = [];
|
|
12826
|
+
for (const id of ids) {
|
|
12827
|
+
const ref = store.lookupRef(id);
|
|
12828
|
+
if (ref === null || store.getTask(id) === null || !store.accountCanAccess(acting.name, ref.repo)) {
|
|
12829
|
+
refused.push({ id, reason: "unknown-task", message: `No task ${id}.` });
|
|
12830
|
+
continue;
|
|
12831
|
+
}
|
|
12832
|
+
const done = archiveTaskFamily(store, id, { by: acting.name, byKind: "person", rule: null, reason }, clock(), { guard: refs => approverStandsNow(store, acting.name, familyRepoOf(store, refs)) });
|
|
12833
|
+
if (done.ok)
|
|
12834
|
+
archived.push({ id, versions: done.refs.length });
|
|
12835
|
+
else
|
|
12836
|
+
refused.push({ id, reason: done.reason, message: done.message });
|
|
12837
|
+
}
|
|
12838
|
+
const lines = () => [
|
|
12839
|
+
...archived.map(one => `Put away ${one.id}${one.versions > 1 ? ` (${one.versions} versions)` : ""}.`),
|
|
12840
|
+
...refused.map(one => `Not put away: ${one.message}`),
|
|
12841
|
+
...(archived.length === 0 ? [] : [`Restore with \`toolroll task restore ${archived.map(one => one.id).join(" ")}\`.`]),
|
|
12842
|
+
];
|
|
12843
|
+
if (refused.length === 0)
|
|
12844
|
+
return succeed(write, json, command, { archived, refused }, lines);
|
|
12845
|
+
if (json)
|
|
12846
|
+
return fail(write, json, command, refused[0].reason, refused[0].message, EXIT.refused, { archived, refused });
|
|
12847
|
+
write(lines().join("\n"));
|
|
12848
|
+
return EXIT.refused;
|
|
12849
|
+
}
|
|
12850
|
+
/** `toolroll task restore <id...>`: bring put-away tasks back exactly as they were (v145). Credentialed, a person's act. */
|
|
12851
|
+
async function restoreTasksCommand(positional, flags, context) {
|
|
12852
|
+
const { store, write, json, clock } = context;
|
|
12853
|
+
const command = "task restore";
|
|
12854
|
+
const ids = [...new Set(positional)];
|
|
12855
|
+
if (ids.length === 0 || ids.length > 100)
|
|
12856
|
+
return fail(write, json, command, "usage", "`toolroll task restore <id...> --as <you> --token <t>` — 1 to 100 tasks", EXIT.usage);
|
|
12857
|
+
const machine = refuseMachineTidy(context, command);
|
|
12858
|
+
if (machine !== null)
|
|
12859
|
+
return machine;
|
|
12860
|
+
const demo = refuseDemo(context, command);
|
|
12861
|
+
if (demo !== null)
|
|
12862
|
+
return demo;
|
|
12863
|
+
const acting = await askCredentials(flags, context);
|
|
12864
|
+
const verified = acting === null ? null : authenticateApprover(store, acting.name, acting.token, firstVisibleRepo(store, acting.name, ids));
|
|
12865
|
+
if (acting === null || !verified?.ok)
|
|
12866
|
+
return refuseApprover(context, command, verified, "restoring takes `--as <you> --token <t>` — it is recorded against your name", ids[0]);
|
|
12867
|
+
const personal = refuseMachineTidy(context, command, verified.method);
|
|
12868
|
+
if (personal !== null)
|
|
12869
|
+
return personal;
|
|
12870
|
+
const restored = [], refused = [];
|
|
12871
|
+
for (const id of ids) {
|
|
12872
|
+
const ref = store.lookupRef(id);
|
|
12873
|
+
if (ref === null || store.getTask(id) === null || !store.accountCanAccess(acting.name, ref.repo)) {
|
|
12874
|
+
refused.push({ id, reason: "unknown-task", message: `No task ${id}.` });
|
|
12875
|
+
continue;
|
|
12876
|
+
}
|
|
12877
|
+
const done = restoreTaskFamily(store, id, acting.name, clock(), refs => approverStandsNow(store, acting.name, familyRepoOf(store, refs)));
|
|
12878
|
+
if (done.ok)
|
|
12879
|
+
restored.push(id);
|
|
12880
|
+
else
|
|
12881
|
+
refused.push({ id, reason: done.reason, message: done.message });
|
|
12882
|
+
}
|
|
12883
|
+
const lines = () => [...restored.map(id => `Restored ${id}.`), ...refused.map(one => `Not restored: ${one.message}`)];
|
|
12884
|
+
if (refused.length === 0)
|
|
12885
|
+
return succeed(write, json, command, { restored, refused }, lines);
|
|
12886
|
+
if (json)
|
|
12887
|
+
return fail(write, json, command, refused[0].reason, refused[0].message, EXIT.refused, { restored, refused });
|
|
12888
|
+
write(lines().join("\n"));
|
|
12889
|
+
return EXIT.refused;
|
|
12890
|
+
}
|
|
12891
|
+
/** `toolroll task tidy --preview [--days N]` (read-only, no credential) and `toolroll task tidy --apply --digest <d>
|
|
12892
|
+
* [--days N] [--except <id,...>]` (credentialed, a person's act): the console's Put away old tasks, word for word,
|
|
12893
|
+
* with the same preview and digest. Apply puts away exactly what the preview with that digest showed. */
|
|
12894
|
+
async function tidyTasksCommand(positional, flags, context) {
|
|
12895
|
+
const { store, write, json, clock } = context;
|
|
12896
|
+
const command = "task tidy";
|
|
12897
|
+
const preview = flag(flags, "preview"), apply = flag(flags, "apply");
|
|
12898
|
+
const days = tidyDays(text(flags, "days"));
|
|
12899
|
+
const digest = text(flags, "digest");
|
|
12900
|
+
const except = (text(flags, "except") ?? "").split(",").map(one => one.trim()).filter(one => one !== "");
|
|
12901
|
+
const usage = "`toolroll task tidy --preview [--days N]`, then `toolroll task tidy --apply --digest <d> [--days N] [--except <id,...>]`";
|
|
12902
|
+
if (positional.length > 0 || preview === apply || days === null || (preview && (digest !== undefined || except.length > 0)) || (apply && digest === undefined))
|
|
12903
|
+
return fail(write, json, command, "usage", usage, EXIT.usage);
|
|
12904
|
+
const releases = releaseFactsFor(context.databaseFile);
|
|
12905
|
+
// One scope for the preview and the apply: `--as <you>` previews exactly your projects (no password: it reads), and
|
|
12906
|
+
// the apply reads the same person's projects, so an unchanged list keeps its digest.
|
|
12907
|
+
// Whose lists: `--as <you>`, else this machine's saved login (`toolroll up`), else everything (an unrestricted
|
|
12908
|
+
// person's list; a person limited to some projects previews with --as).
|
|
12909
|
+
const previewAs = text(flags, "as") ?? readLoginFile(join(dirname(context.databaseFile), UP_LOGIN_FILE))?.name;
|
|
12910
|
+
const daysArg = `${days === TIDY_DEFAULT_DAYS ? "" : ` --days ${days}`}${previewAs === undefined ? "" : ` --as ${previewAs}`}`;
|
|
12911
|
+
// `--as` only ever narrows: the named person's projects within the caller's own lens, never beyond it.
|
|
12912
|
+
const lens = context.principal?.lens ?? null;
|
|
12913
|
+
const own = { principal: "operator", repos: lens, includeUnplaced: lens === null };
|
|
12914
|
+
const access = previewAs !== undefined ? withinLens(accessOf(store, previewAs), lens) : own;
|
|
12915
|
+
if (preview) {
|
|
12916
|
+
const items = tidyPreview(store, clock(), { releases, days, access });
|
|
12917
|
+
const shown = tidyDigest(items, days);
|
|
12918
|
+
const groups = tidyGroups(items, days);
|
|
12919
|
+
const next = `toolroll task tidy --apply --digest ${shown}${daysArg}`;
|
|
12920
|
+
return succeed(write, json, command, { preview: true, days, digest: shown, count: items.length, groups, next: items.length === 0 ? null : next,
|
|
12921
|
+
...(releases.problem === null ? {} : { releasesSkipped: releases.problem }) }, () => items.length === 0
|
|
12922
|
+
? ["Nothing to put away."]
|
|
12923
|
+
: [`Put away old tasks: ${items.length} task${items.length === 1 ? "" : "s"}.`,
|
|
12924
|
+
...groups.flatMap(group => ["", `${group.label} (${group.items.length})`, ...group.items.map(one => ` ${one.taskId} ${one.reason}${one.members.length > 1 ? ` (${one.members.length} versions)` : ""}`)]),
|
|
12925
|
+
...(releases.problem === null ? [] : ["", `Release tasks were left out: ${releases.problem}.`]),
|
|
12926
|
+
"", `To put these away, run: ${next}`, "Leave some out with --except <id,...>.",
|
|
12927
|
+
...(previewAs === undefined ? ["Limited to some projects? Preview with --as <you> so the list is yours."] : [])]);
|
|
12928
|
+
}
|
|
12929
|
+
const machine = refuseMachineTidy(context, command);
|
|
12930
|
+
if (machine !== null)
|
|
12931
|
+
return machine;
|
|
12932
|
+
const demo = refuseDemo(context, command);
|
|
12933
|
+
if (demo !== null)
|
|
12934
|
+
return demo;
|
|
12935
|
+
const acting = await askCredentials(flags, context);
|
|
12936
|
+
const verified = acting === null ? null : authenticateApprover(store, acting.name, acting.token, firstVisibleRepo(store, acting.name, []));
|
|
12937
|
+
if (acting === null || !verified?.ok)
|
|
12938
|
+
return refuseApprover(context, command, verified, "putting tasks away takes `--as <you> --token <t>` — it is recorded against your name", undefined);
|
|
12939
|
+
const personal = refuseMachineTidy(context, command, verified.method);
|
|
12940
|
+
if (personal !== null)
|
|
12941
|
+
return personal;
|
|
12942
|
+
// The person's own view of the lists, from the verified credential (never a flag), within the caller's lens: the
|
|
12943
|
+
// same scope `--preview --as <them>` read.
|
|
12944
|
+
const mine = withinLens(accessOf(store, acting.name), lens);
|
|
12945
|
+
const shownItems = tidyPreview(store, clock(), { releases, days, access: mine });
|
|
12946
|
+
const only = except.length === 0 ? null : shownItems.map(one => one.taskId).filter(id => !except.includes(id));
|
|
12947
|
+
if (except.some(id => !shownItems.some(one => one.taskId === id)))
|
|
12948
|
+
return fail(write, json, command, "outside", "--except names a task that isn't in the list. Check it again with --preview.", EXIT.refused);
|
|
12949
|
+
const done = applyTidy(store, clock(), { by: acting.name, days, digest: digest, only, releases, access: mine });
|
|
12950
|
+
if (!done.ok) {
|
|
12951
|
+
const again = `toolroll task tidy --apply --digest ${done.digest}${daysArg}`;
|
|
12952
|
+
return fail(write, json, command, done.reason, done.reason === "changed" ? `${done.message} Look again with \`toolroll task tidy --preview${daysArg}\`.` : done.message, EXIT.refused, { digest: done.digest, count: done.items.length, ...(done.reason === "changed" ? { next: done.items.length === 0 ? null : again } : {}) });
|
|
12953
|
+
}
|
|
12954
|
+
return succeed(write, json, command, { archived: done.archived.map(one => ({ id: one.taskId, rule: one.rule, reason: one.reason })), skipped: done.skipped }, () => [
|
|
12955
|
+
`Put away ${done.archived.length} task${done.archived.length === 1 ? "" : "s"}.`,
|
|
12956
|
+
...done.skipped.map(one => `Not put away: ${one.message}`),
|
|
12957
|
+
"Find them with `toolroll task list --view archived`; bring one back with `toolroll task restore <id>`.",
|
|
12958
|
+
]);
|
|
12959
|
+
}
|
|
12960
|
+
/** The project a put-away or restore proves the person's password against: the first named task's, when they can see
|
|
12961
|
+
* it; otherwise the first project they can see at all (null when none: an unrestricted person, or none to prove). */
|
|
12962
|
+
function firstVisibleRepo(store, name, ids) {
|
|
12963
|
+
for (const id of ids) {
|
|
12964
|
+
const repo = store.lookupRef(id)?.repo ?? null;
|
|
12965
|
+
if (repo !== null && store.accountCanAccess(name, repo))
|
|
12966
|
+
return repo;
|
|
12967
|
+
}
|
|
12968
|
+
return store.accountCanAccess(name, null) ? null : store.knownRepos().find(repo => store.accountCanAccess(name, repo)) ?? null;
|
|
12969
|
+
}
|
|
12970
|
+
/** The check states as `task ship` says them. */
|
|
12971
|
+
const SHIP_CHECK_WORDS = { queued: "queued", running: "running", passed: "passed", failed: "failed", skipped: "skipped", cancelled: "cancelled", neutral: "neutral" };
|
|
12972
|
+
/**
|
|
12973
|
+
* `toolroll task ship <task>` (read-only) — the task's pull request checks as the plane last observed them for that
|
|
12974
|
+
* head (store.ts ci_check, v146): each check's state and how long it took, the Ship view's own facts.
|
|
12975
|
+
*/
|
|
12976
|
+
function taskShipCommand(positional, context) {
|
|
12977
|
+
const { store, write, json } = context;
|
|
12978
|
+
const command = "task ship";
|
|
12979
|
+
const id = positional[0];
|
|
12980
|
+
if (id === undefined || positional.length > 1)
|
|
12981
|
+
return fail(write, json, command, "usage", "`toolroll task ship <task>`", EXIT.usage);
|
|
12982
|
+
const ref = store.lookupRef(id);
|
|
12983
|
+
const lens = context.principal?.lens ?? null;
|
|
12984
|
+
// A task outside the caller's projects is no task here, the same answer as one that doesn't exist.
|
|
12985
|
+
if (ref === null || store.getTask(id) === null || (lens !== null && (ref.repo === null || !lens.includes(ref.repo))))
|
|
12986
|
+
return fail(write, json, command, "unknown-task", `no task \`${id}\``, EXIT.refused);
|
|
12987
|
+
const ship = shipChecksForTask(store, id);
|
|
12988
|
+
if (ship === null)
|
|
12989
|
+
return fail(write, json, command, "no-pull-request", `${id} has no pull request with observed checks yet`, EXIT.refused);
|
|
12990
|
+
const now = (context.clock ?? (() => context.now))();
|
|
12991
|
+
const checks = ship.checks.map(one => ({ ...one, seconds: elapsedSeconds(one.startedAt, one.completedAt, now) }));
|
|
12992
|
+
const passed = checks.filter(one => one.state === "passed").length;
|
|
12993
|
+
// The overall answer is the observation's own (the one merges read), never a sum of the rows.
|
|
12994
|
+
const overall = ship.observed === "failing" ? "failing" : ship.observed === "passing" ? "passing" : ship.observed === "running" ? "running" : "not read";
|
|
12995
|
+
return succeed(write, json, command, { task: id, pullRequest: ship.prNumber, githubRepo: ship.githubRepo, head: ship.headSha, observedAt: ship.observedAt, status: ship.observed, checks }, () => [
|
|
12996
|
+
`Pull request #${ship.prNumber} (${ship.githubRepo}) at ${ship.headSha.slice(0, 7)}: checks ${overall} · ${passed} of ${checks.length} passed`,
|
|
12997
|
+
...checks.map(one => ` ${(one.workflow === null ? one.name : `${one.workflow} / ${one.name}`).padEnd(32)} ${(SHIP_CHECK_WORDS[one.state] ?? one.state).padEnd(9)} ${one.seconds === null ? "–" : dur(one.seconds)}`),
|
|
12998
|
+
]);
|
|
12999
|
+
}
|
package/dist/passwordless.d.ts
CHANGED
|
@@ -20,4 +20,4 @@ export declare const passwordlessOn: (store: Store) => boolean;
|
|
|
20
20
|
export declare function setPasswordless(store: Store, on: boolean, by: string, now: Date): {
|
|
21
21
|
changed: boolean;
|
|
22
22
|
};
|
|
23
|
-
export declare const PASSWORDLESS_TRADE = "Anyone holding a signed-in browser \u2014 an unlocked laptop, a phone, a stolen session \u2014 can then do everything you can without a second check. Signing in, session limits (12 hours idle, 7 days), signing out and ending sessions remain your protection. Creating or widening a way in (passwords, sign-in methods, API tokens, connected apps, people, workers, chat pairing, webhook secrets) still asks for your password.";
|
|
23
|
+
export declare const PASSWORDLESS_TRADE = "Anyone holding a signed-in browser \u2014 an unlocked laptop, a phone, a stolen session \u2014 can then do everything you can without a second check. Signing in, session limits (12 hours idle, 7 days), signing out and ending sessions remain your protection. Creating or widening a way in (passwords, sign-in methods, API tokens, connected apps, people, workers, chat pairing, webhook secrets), or widening what a project's builds and checks reach (its network, where builds write, its home folder and its check access), still asks for your password.";
|
package/dist/passwordless.js
CHANGED
|
@@ -23,4 +23,4 @@ export function setPasswordless(store, on, by, now) {
|
|
|
23
23
|
store.bumpWake();
|
|
24
24
|
return { changed: true };
|
|
25
25
|
}
|
|
26
|
-
export const PASSWORDLESS_TRADE = "Anyone holding a signed-in browser — an unlocked laptop, a phone, a stolen session — can then do everything you can without a second check. Signing in, session limits (12 hours idle, 7 days), signing out and ending sessions remain your protection. Creating or widening a way in (passwords, sign-in methods, API tokens, connected apps, people, workers, chat pairing, webhook secrets) still asks for your password.";
|
|
26
|
+
export const PASSWORDLESS_TRADE = "Anyone holding a signed-in browser — an unlocked laptop, a phone, a stolen session — can then do everything you can without a second check. Signing in, session limits (12 hours idle, 7 days), signing out and ending sessions remain your protection. Creating or widening a way in (passwords, sign-in methods, API tokens, connected apps, people, workers, chat pairing, webhook secrets), or widening what a project's builds and checks reach (its network, where builds write, its home folder and its check access), still asks for your password.";
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { type AuditAction } from "./remote-audit.js";
|
|
2
2
|
import type { ApiTokenRow } from "./store.js";
|
|
3
3
|
import { type Html } from "./html.js";
|
|
4
|
-
|
|
4
|
+
/** The old look; the page is built from the kit now. Kept for chrome.ts, which still concatenates it. */
|
|
5
|
+
export declare const PEOPLE_AUDIT_CSS = "";
|
|
5
6
|
export type PersonAuditView = {
|
|
6
7
|
name: string;
|
|
7
8
|
tokens: readonly ApiTokenRow[];
|
package/dist/people-audit-ui.js
CHANGED
|
@@ -3,36 +3,38 @@
|
|
|
3
3
|
* (remote-audit.ts). A token's name filters the actions to it (`token-name`: a `token` query is always refused). Read-only: making, revoking and rotating tokens stay
|
|
4
4
|
* in Settings → Sessions & tokens. Never a token's secret or a ledger line's raw detail.
|
|
5
5
|
*/
|
|
6
|
+
import { settingsSubPage } from "./settings-groups-ui.js";
|
|
6
7
|
import { basename } from "node:path";
|
|
7
8
|
import { tokenStatus } from "./remote-audit.js";
|
|
8
|
-
import { html } from "./html.js";
|
|
9
|
+
import { html, joinHtml } from "./html.js";
|
|
10
|
+
import { acts, btn, chip, note, settingsRecord, settingsSection } from "./server/kit.js";
|
|
9
11
|
const stamp = (value) => `${value.slice(0, 16).replace("T", " ")} UTC`;
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
`.person-audit .row-item:last-child{border-bottom:0}.person-audit .row-item p{margin:0}.person-audit .row-item .meta{font-size:.85rem}` +
|
|
13
|
-
`.person-audit a{display:inline-flex;align-items:center;min-height:44px}.person-audit .row-item a{min-height:32px}.person-audit a:focus-visible{outline:2px solid var(--so-accent);outline-offset:2px;border-radius:4px}` +
|
|
14
|
-
`.person-audit .outcome{display:inline-block;font-size:.8rem;font-weight:600;padding:1px 8px;border-radius:999px;margin-right:8px}` +
|
|
15
|
-
`.person-audit .outcome-ok{background:var(--so-success-soft);color:var(--so-success)}.person-audit .outcome-refused{background:var(--so-warning-soft);color:var(--so-ink)}` +
|
|
16
|
-
`.person-audit .outcome-error{background:var(--so-danger-soft);color:var(--so-danger)}` +
|
|
17
|
-
`@media(max-width:600px){.person-audit .row-item a{min-height:44px}}`;
|
|
12
|
+
/** The old look; the page is built from the kit now. Kept for chrome.ts, which still concatenates it. */
|
|
13
|
+
export const PEOPLE_AUDIT_CSS = "";
|
|
18
14
|
export const personHref = (name, more = {}) => `/people?${new URLSearchParams({ person: name, ...more }).toString()}`;
|
|
19
15
|
export function personAuditHtml(view) {
|
|
20
|
-
const tokens =
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
16
|
+
const tokens = settingsSection({ id: "person-tokens", title: "API tokens", lead: "Make, revoke and rotate tokens in Sessions and tokens.",
|
|
17
|
+
body: view.tokens.length === 0 ? note("No API tokens.") : joinHtml(view.tokens.map(one => {
|
|
18
|
+
const status = tokenStatus(one, view.now);
|
|
19
|
+
return settingsRecord({
|
|
20
|
+
title: html `<a class="k-link" href="${personHref(view.name, { "token-name": one.name })}">${one.name}</a>`,
|
|
21
|
+
sub: `${status === "active" ? "Active" : status === "expired" ? "Expired" : "Revoked"} · ${one.access === "act" ? "Can act" : "Read only"} · last used ${one.lastUsedAt === null ? "never" : stamp(one.lastUsedAt)}`,
|
|
22
|
+
attrs: { "data-token-name": one.name },
|
|
23
|
+
});
|
|
24
|
+
})) });
|
|
25
|
+
const filtered = view.token === null ? undefined : btn("All tokens", { sm: true, variant: "ghost", href: personHref(view.name) });
|
|
26
|
+
const older = view.nextCursor === null ? "" : acts(btn("Older actions", { sm: true, href: personHref(view.name, { ...(view.token === null ? {} : { "token-name": view.token }), before: view.nextCursor }) }));
|
|
27
|
+
const activity = settingsSection({ id: "person-activity", title: "Remote activity", ...(view.token === null ? {} : { state: ` · with ${view.token}` }), ...(filtered ? { right: filtered } : {}),
|
|
28
|
+
lead: "What this person did through the toolroll command and agents (MCP), newest first.",
|
|
29
|
+
body: joinHtml([view.actions.length === 0 ? note(`No remote actions${view.token === null ? "" : " with this token"}.`) : joinHtml(view.actions.map(actionHtml)), older]) });
|
|
30
|
+
return settingsSubPage("access", view.name, joinHtml([note(html `<a class="k-link" href="/people">All people</a>`), tokens, activity]));
|
|
31
31
|
}
|
|
32
32
|
function actionHtml(action) {
|
|
33
|
-
const
|
|
33
|
+
const outcome = action.outcome === "ok" ? chip("OK", "done") : action.outcome === "refused" ? chip("Refused", "") : chip("Error", "failed");
|
|
34
34
|
const detail = [stamp(action.at), action.token ?? "unknown token", action.source === "mcp" ? "Agent (MCP)" : "CLI",
|
|
35
35
|
...(action.repo === null ? [] : [basename(action.repo)]), ...(action.reason === null ? [] : [action.reason])];
|
|
36
|
-
return
|
|
37
|
-
${action.taskId === null ? "" : html ` · <a href="/t/${encodeURIComponent(action.taskId)}">${action.taskId}</a>`}
|
|
36
|
+
return settingsRecord({
|
|
37
|
+
title: html `${outcome} ${action.name}${action.taskId === null ? "" : html ` · <a class="k-link" href="/t/${encodeURIComponent(action.taskId)}">${action.taskId}</a>`}`,
|
|
38
|
+
sub: detail.join(" · "), attrs: { "data-action": action.id },
|
|
39
|
+
});
|
|
38
40
|
}
|
package/dist/policy-ui.d.ts
CHANGED
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
import { type Html } from "./html.js";
|
|
8
8
|
import type { LedgerEntry } from "./action-ledger.js";
|
|
9
9
|
import { type SavedPolicy } from "./policy.js";
|
|
10
|
-
|
|
10
|
+
/** The old look; the page is built from the kit now. Kept for chrome.ts, which still concatenates it. */
|
|
11
|
+
export declare const POLICY_CSS = "";
|
|
11
12
|
export type PolicyView = {
|
|
12
13
|
policy: SavedPolicy;
|
|
13
14
|
history: LedgerEntry[];
|
|
@@ -15,7 +16,7 @@ export type PolicyView = {
|
|
|
15
16
|
/** The project tools here, as a hint for the tools list. */
|
|
16
17
|
toolNames: string[];
|
|
17
18
|
};
|
|
18
|
-
export declare function policyHtml(view: PolicyView,
|
|
19
|
+
export declare function policyHtml(view: PolicyView, outcome: {
|
|
19
20
|
said?: string | null;
|
|
20
21
|
problem?: string | null;
|
|
21
22
|
}): Html;
|
package/dist/policy-ui.js
CHANGED
|
@@ -4,33 +4,39 @@
|
|
|
4
4
|
* action ledger. Anyone signed in reads it; an instance operator changes it
|
|
5
5
|
* with their password.
|
|
6
6
|
*/
|
|
7
|
-
import { html,
|
|
7
|
+
import { html, joinHtml } from "./html.js";
|
|
8
8
|
import { whenUtc } from "./when-html.js";
|
|
9
9
|
import { LEVEL_HINTS, LEVEL_NAMES, PERMISSION_LEVELS, POLICY_PROVIDERS, PROVIDER_NAMES, policyParts } from "./policy.js";
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
`.policy textarea{width:100%;min-height:72px;font-family:var(--font-mono);font-size:.8125rem;box-sizing:border-box}.policy .step-up{margin-top:18px}.policy button[type=submit]{margin-top:14px}` +
|
|
16
|
-
`.policy details{margin-top:28px}.policy summary{cursor:pointer;min-height:44px;display:flex;align-items:center;font-weight:600;font-size:.875rem}` +
|
|
17
|
-
`.policy ol{list-style:none;padding:0;margin:0}.policy li{padding:10px 0;border-bottom:1px solid var(--border);font-size:.8125rem;overflow-wrap:anywhere}.policy li .what{display:block;font-size:.875rem}`;
|
|
18
|
-
/** A ledger entry as one line of history: when, who, what, before → after. */
|
|
10
|
+
import { disclose, facts, formRow, infoLine, note, notice, radios, settingsRecord, settingsSection, textarea } from "./server/kit.js";
|
|
11
|
+
import { kitSectionForm } from "./settings-kit.js";
|
|
12
|
+
/** The old look; the page is built from the kit now. Kept for chrome.ts, which still concatenates it. */
|
|
13
|
+
export const POLICY_CSS = "";
|
|
14
|
+
/** A ledger entry as one record of history: what, before → after; who; when. */
|
|
19
15
|
function historyItem(entry) {
|
|
20
16
|
const what = entry.action.replace(/^organisation policy: /, "");
|
|
21
17
|
const [before, after] = (entry.detail ?? "").split(" → ");
|
|
22
|
-
return html
|
|
18
|
+
return settingsRecord({ title: html `${what.charAt(0).toUpperCase() + what.slice(1)}: ${before ?? ""} → <b>${after ?? ""}</b>`, sub: entry.actor, when: whenUtc(entry.at) });
|
|
23
19
|
}
|
|
24
|
-
export function policyHtml(view,
|
|
25
|
-
const
|
|
20
|
+
export function policyHtml(view, outcome) {
|
|
21
|
+
const answer = outcome.problem ? notice(outcome.problem) : outcome.said ? infoLine(outcome.said) : "";
|
|
26
22
|
const parts = policyParts(view.policy);
|
|
27
|
-
const changed = view.policy.updatedBy === null ? "" :
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
if (!view.canChange)
|
|
31
|
-
return
|
|
32
|
-
|
|
33
|
-
|
|
23
|
+
const changed = view.policy.updatedBy === null ? "" : ` · changed by ${view.policy.updatedBy} on ${(view.policy.updatedAt ?? "").slice(0, 10)}`;
|
|
24
|
+
const history = view.history.length === 0 ? "" : settingsSection({ id: "policy-history", title: "History",
|
|
25
|
+
body: disclose(`${view.history.length} ${view.history.length === 1 ? "change" : "changes"}`, joinHtml(view.history.map(historyItem))) });
|
|
26
|
+
if (!view.canChange) {
|
|
27
|
+
return joinHtml([answer, settingsSection({ id: "policy", title: "Organisation policy", state: changed, lead: "What every task may use, in every project.",
|
|
28
|
+
body: joinHtml([facts([["Providers", parts.providers === "any" ? "Any" : parts.providers], ["Models", parts.models === "any" ? "Any" : parts.models],
|
|
29
|
+
["Tools", parts.tools === "any" ? "Any" : parts.tools], ["Ceiling", parts.ceiling]]), note("An instance operator sets the policy.")]) }), history]);
|
|
30
|
+
}
|
|
31
|
+
const providers = html `<div class="k-checks" role="group" aria-label="Providers">${POLICY_PROVIDERS.map(one => html `<label for="policy-provider-${one}"><input type="checkbox" id="policy-provider-${one}" name="provider" value="${one}"${view.policy.providers === null || view.policy.providers.includes(one) ? html ` checked` : ""}> ${PROVIDER_NAMES[one]}</label>`)}</div>`;
|
|
34
32
|
const toolHint = view.toolNames.length === 0 ? "" : ` Tools here: ${view.toolNames.slice(0, 12).join(", ")}${view.toolNames.length > 12 ? "…" : ""}.`;
|
|
35
|
-
return
|
|
33
|
+
return joinHtml([answer, settingsSection({ id: "policy", title: "Organisation policy", state: changed, lead: "What every task may use, in every project.",
|
|
34
|
+
body: kitSectionForm({ proof: "policy", action: "/settings/policy", id: "policy-form", gate: true, save: { label: "Save policy" }, confirm: "Change the policy",
|
|
35
|
+
body: joinHtml([
|
|
36
|
+
formRow("Providers", "Work runs only on the ticked ones. All of them allows any.", providers, "wide"),
|
|
37
|
+
formRow("Models", "One per line; end with * to allow a family. Blank allows any.", textarea({ id: "policy-models", name: "models", value: (view.policy.models ?? []).join("\n"), rows: 3, attrs: { spellcheck: "false", placeholder: "Any model", "aria-label": "Models" } }), "top"),
|
|
38
|
+
formRow("Project tools", `One per line. Blank allows any.${toolHint}`, textarea({ id: "policy-tools", name: "tools", value: (view.policy.tools ?? []).join("\n"), rows: 3, attrs: { spellcheck: "false", placeholder: "Any tool", "aria-label": "Project tools" } }), "top"),
|
|
39
|
+
formRow("Permission ceiling", "The most any task may be allowed to do.", radios({ name: "ceiling", legend: "Permission ceiling", current: view.policy.ceiling,
|
|
40
|
+
items: [...PERMISSION_LEVELS].reverse().map(level => ({ value: level, label: LEVEL_NAMES[level], text: LEVEL_HINTS[level] })) }), "wide"),
|
|
41
|
+
]) }) }), history]);
|
|
36
42
|
}
|
package/dist/policy.d.ts
CHANGED
|
@@ -87,3 +87,6 @@ export declare function checkPolicy(input: {
|
|
|
87
87
|
ok: false;
|
|
88
88
|
problem: string;
|
|
89
89
|
};
|
|
90
|
+
/** The providers ticked on the policy form: every provider the policy knows ticked (and nothing unknown) means "any"
|
|
91
|
+
* (null), so a provider added later is allowed too; anything else is the list as ticked, for checkPolicy to check. */
|
|
92
|
+
export declare function chosenProviders(chosen: readonly string[]): string[] | null;
|
package/dist/policy.js
CHANGED
|
@@ -224,7 +224,7 @@ export function checkPolicy(input) {
|
|
|
224
224
|
if (input.providers !== null) {
|
|
225
225
|
const unknown = input.providers.filter(one => policyProvider(one.toLowerCase()) === null);
|
|
226
226
|
if (unknown.length > 0)
|
|
227
|
-
return { ok: false, problem: `${unknown.join(", ")} isn't a provider here. Choose from
|
|
227
|
+
return { ok: false, problem: `${unknown.join(", ")} isn't a provider here. Choose from ${providerChoices()}.` };
|
|
228
228
|
providers = POLICY_PROVIDERS.filter(one => input.providers.some(given => policyProvider(given.toLowerCase()) === one));
|
|
229
229
|
if (providers.length === 0)
|
|
230
230
|
return { ok: false, problem: "Allow at least one provider, or allow any." };
|
|
@@ -248,3 +248,16 @@ export function checkPolicy(input) {
|
|
|
248
248
|
return { ok: false, problem: tools.problem };
|
|
249
249
|
return { ok: true, policy: { providers, models, tools, ceiling: input.ceiling } };
|
|
250
250
|
}
|
|
251
|
+
/** Every provider the policy knows, in plain names: "Claude, Codex, … and Goose". */
|
|
252
|
+
function providerChoices() {
|
|
253
|
+
const names = POLICY_PROVIDERS.map(one => PROVIDER_NAMES[one]);
|
|
254
|
+
return `${names.slice(0, -1).join(", ")} and ${names[names.length - 1]}`;
|
|
255
|
+
}
|
|
256
|
+
/** The providers ticked on the policy form: every provider the policy knows ticked (and nothing unknown) means "any"
|
|
257
|
+
* (null), so a provider added later is allowed too; anything else is the list as ticked, for checkPolicy to check. */
|
|
258
|
+
export function chosenProviders(chosen) {
|
|
259
|
+
const known = new Set(chosen.map(one => policyProvider(one.toLowerCase())));
|
|
260
|
+
if (known.has(null))
|
|
261
|
+
return [...chosen];
|
|
262
|
+
return POLICY_PROVIDERS.every(one => known.has(one)) ? null : [...chosen];
|
|
263
|
+
}
|