toolroll 0.9.92 → 0.9.94
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 +87 -0
- package/dist/activity-line.d.ts +7 -4
- package/dist/activity-line.js +9 -7
- package/dist/agent-fence.d.ts +4 -0
- package/dist/agent-fence.js +4 -1
- package/dist/approval-authority.d.ts +7 -1
- package/dist/approval-authority.js +11 -2
- package/dist/approval-password.d.ts +31 -0
- package/dist/approval-password.js +26 -0
- package/dist/assignment-status.d.ts +1 -1
- package/dist/assignment-ui.js +3 -3
- package/dist/assignment.js +16 -3
- package/dist/boot-identity.d.ts +2 -1
- package/dist/boot-identity.js +6 -4
- package/dist/bounded-spawn.d.ts +57 -0
- package/dist/bounded-spawn.js +220 -0
- package/dist/browser/terminal.js +42 -42
- package/dist/browser/workspace.css +2 -2
- package/dist/browser/workspace.js +26 -26
- package/dist/browser-crew.js +2 -2
- package/dist/browser-shell.d.ts +1 -1
- package/dist/browser-shell.js +2 -2
- package/dist/browser-workspace.d.ts +48 -7
- package/dist/browser-workspace.js +3 -3
- package/dist/builder-restart.d.ts +21 -0
- package/dist/builder-restart.js +50 -0
- package/dist/builder.d.ts +4 -0
- package/dist/builder.js +73 -25
- package/dist/chat-acceptance.js +1 -1
- package/dist/chat-actions.d.ts +12 -2
- package/dist/chat-actions.js +25 -2
- package/dist/chat-channel.js +11 -0
- package/dist/chat-decide.d.ts +7 -2
- package/dist/chat-decide.js +37 -9
- package/dist/chat-quiet.js +3 -3
- package/dist/chat-voice.d.ts +1 -1
- package/dist/chat-voice.js +2 -2
- package/dist/check-facts.d.ts +126 -0
- package/dist/check-facts.js +318 -0
- package/dist/check-failure.d.ts +22 -3
- package/dist/check-failure.js +57 -13
- package/dist/check-levels-ui.d.ts +4 -0
- package/dist/check-levels-ui.js +10 -1
- package/dist/check-load.d.ts +85 -0
- package/dist/check-load.js +147 -0
- package/dist/check-only-standing.d.ts +36 -0
- package/dist/check-only-standing.js +39 -0
- package/dist/check-only.d.ts +15 -0
- package/dist/check-only.js +99 -0
- package/dist/check-report.d.ts +7 -0
- package/dist/check-report.js +4 -3
- package/dist/check-stamp.d.ts +46 -0
- package/dist/check-stamp.js +93 -0
- package/dist/checkout-cleanup.d.ts +36 -5
- package/dist/checkout-cleanup.js +166 -14
- package/dist/cli-help.d.ts +4 -1
- package/dist/cli-help.js +47 -30
- package/dist/cli.js +16 -1
- package/dist/code-extract.d.ts +44 -0
- package/dist/code-extract.js +88 -0
- package/dist/code-map-acts.d.ts +24 -0
- package/dist/code-map-acts.js +53 -0
- package/dist/code-map-admission.d.ts +52 -0
- package/dist/code-map-admission.js +295 -0
- package/dist/code-map-build.d.ts +63 -0
- package/dist/code-map-build.js +415 -0
- package/dist/code-map-cli.d.ts +153 -0
- package/dist/code-map-cli.js +103 -0
- package/dist/code-map-main.d.ts +1 -0
- package/dist/code-map-main.js +48 -0
- package/dist/code-map-read.d.ts +106 -0
- package/dist/code-map-read.js +171 -0
- package/dist/code-map-report.d.ts +37 -0
- package/dist/code-map-report.js +95 -0
- package/dist/code-map-shape.d.ts +172 -0
- package/dist/code-map-shape.js +327 -0
- package/dist/code-map-store.d.ts +136 -0
- package/dist/code-map-store.js +233 -0
- package/dist/coding-provider.d.ts +2 -0
- package/dist/coding-provider.js +3 -2
- package/dist/coding-update.js +3 -2
- package/dist/coding-workspace.js +4 -4
- package/dist/console-chrome.js +2 -2
- package/dist/containment.d.ts +5 -0
- package/dist/containment.js +14 -1
- package/dist/contracts/chat-actions.d.ts +9 -0
- package/dist/contracts/chat-actions.js +2 -0
- package/dist/contracts/cli.d.ts +52 -0
- package/dist/contracts/cli.js +4 -0
- package/dist/contracts/console-api.d.ts +12 -5
- package/dist/contracts/console-api.js +12 -5
- package/dist/contracts/gateway-tools.d.ts +17 -0
- package/dist/contracts/gateway-tools.js +3 -1
- package/dist/contracts/lead-tools.d.ts +61 -0
- package/dist/contracts/lead-tools.js +10 -1
- package/dist/contracts/project-knowledge.d.ts +38 -5
- package/dist/contracts/project-knowledge.js +11 -5
- package/dist/contracts/project-memory.d.ts +3 -1
- package/dist/contracts/project-memory.js +3 -2
- package/dist/contracts/release-check.d.ts +25 -1
- package/dist/contracts/release-check.js +19 -0
- package/dist/contracts/storage.d.ts +14 -0
- package/dist/contracts/storage.js +3 -1
- package/dist/contracts/store-json.d.ts +2 -0
- package/dist/contracts/youiyoux-request.d.ts +1 -1
- package/dist/contracts/youiyoux-request.js +1 -1
- package/dist/desktop-access.js +6 -2
- package/dist/desktop-update.d.ts +2 -2
- package/dist/desktop-update.js +13 -9
- package/dist/dispatch.js +18 -15
- package/dist/dispose.js +8 -2
- package/dist/easy-start.js +2 -3
- package/dist/eval-hooks.d.ts +55 -0
- package/dist/eval-hooks.js +227 -4
- package/dist/evals.js +2 -1
- package/dist/fast-start.d.ts +136 -0
- package/dist/fast-start.js +431 -0
- package/dist/fast-terms.d.ts +9 -0
- package/dist/fast-terms.js +23 -0
- package/dist/fenced-check-coverage.d.ts +62 -0
- package/dist/fenced-check-coverage.js +54 -0
- package/dist/first-run.js +1 -1
- package/dist/flaky-ci.d.ts +31 -0
- package/dist/flaky-ci.js +149 -0
- package/dist/flaky-cli.d.ts +60 -0
- package/dist/flaky-cli.js +60 -0
- package/dist/flaky.d.ts +83 -0
- package/dist/flaky.js +264 -0
- package/dist/flow-card-task.js +3 -0
- package/dist/flow-triggers.d.ts +1 -1
- package/dist/flow-triggers.js +1 -1
- package/dist/github-import.d.ts +62 -0
- package/dist/github-import.js +146 -0
- package/dist/guides.js +15 -10
- package/dist/invoke.d.ts +5 -1
- package/dist/invoke.js +49 -14
- package/dist/job-object-helper.ps1 +10 -3
- package/dist/keymap.d.ts +41 -0
- package/dist/keymap.js +51 -0
- package/dist/knowledge-cli.js +1 -1
- package/dist/knowledge-ui.d.ts +21 -4
- package/dist/knowledge-ui.js +75 -7
- package/dist/laptop-background.d.ts +16 -0
- package/dist/laptop-background.js +108 -2
- package/dist/lead-context.js +1 -1
- package/dist/lead-contract.d.ts +1 -1
- package/dist/lead-contract.js +2 -2
- package/dist/lead-progress.js +3 -0
- package/dist/lead-status.d.ts +2 -0
- package/dist/lead-status.js +6 -2
- package/dist/lead-tools.js +51 -2
- package/dist/lead.js +1 -1
- package/dist/lesson-notes.d.ts +81 -0
- package/dist/lesson-notes.js +336 -0
- package/dist/limits-ui.js +1 -1
- package/dist/local-owner.d.ts +1 -1
- package/dist/local-owner.js +11 -0
- package/dist/machine-check.d.ts +16 -0
- package/dist/machine-check.js +26 -0
- package/dist/machine-identity.d.ts +75 -0
- package/dist/machine-identity.js +170 -0
- package/dist/mcp-core.js +11 -1
- package/dist/memory-cli.d.ts +300 -1
- package/dist/memory-cli.js +62 -9
- package/dist/memory-pass.d.ts +16 -0
- package/dist/memory-pass.js +67 -3
- package/dist/memory-tidy.d.ts +181 -0
- package/dist/memory-tidy.js +523 -0
- package/dist/memory-use.d.ts +35 -0
- package/dist/memory-use.js +109 -0
- package/dist/merge-checklist.d.ts +55 -0
- package/dist/merge-checklist.js +182 -0
- package/dist/needs-you.d.ts +32 -13
- package/dist/needs-you.js +108 -49
- package/dist/nightly-github.d.ts +2 -0
- package/dist/nightly-github.js +4 -4
- package/dist/operate-remote-arguments.js +6 -1
- package/dist/operate-remote.js +3 -1
- package/dist/operate.d.ts +6 -1
- package/dist/operate.js +540 -70
- package/dist/phase-routing.js +2 -2
- package/dist/plain-schema.d.ts +23 -0
- package/dist/plain-schema.js +94 -0
- package/dist/plain-words.d.ts +20 -0
- package/dist/plain-words.js +66 -0
- package/dist/planner.js +2 -1
- package/dist/process-liveness.d.ts +2 -0
- package/dist/process-liveness.js +61 -17
- package/dist/project-delete-ui.d.ts +7 -2
- package/dist/project-delete-ui.js +12 -1
- package/dist/project-delete.js +13 -2
- package/dist/project-knowledge.d.ts +18 -11
- package/dist/project-knowledge.js +39 -9
- package/dist/project-learning.d.ts +2 -1
- package/dist/project-learning.js +15 -45
- package/dist/project-memory.d.ts +5 -0
- package/dist/project-memory.js +22 -2
- package/dist/proof.js +30 -5
- package/dist/provider-auth.js +1 -1
- package/dist/publish.js +9 -1
- package/dist/pull-request-flow.js +5 -0
- package/dist/push.d.ts +26 -3
- package/dist/push.js +55 -15
- package/dist/release-adapters.d.ts +9 -0
- package/dist/release-adapters.js +87 -1
- package/dist/release-approval.d.ts +191 -0
- package/dist/release-approval.js +382 -0
- package/dist/release-auto.js +26 -17
- package/dist/release-card.d.ts +74 -6
- package/dist/release-card.js +179 -23
- package/dist/release-carry.d.ts +7 -2
- package/dist/release-carry.js +39 -9
- package/dist/release-cli.d.ts +4 -0
- package/dist/release-cli.js +38 -23
- package/dist/release-coverage.js +1 -1
- package/dist/release-next.d.ts +48 -0
- package/dist/release-next.js +161 -0
- package/dist/release.d.ts +50 -1
- package/dist/release.js +118 -11
- package/dist/remote-destination.d.ts +36 -0
- package/dist/remote-destination.js +217 -0
- package/dist/remote-main.d.ts +48 -0
- package/dist/remote-main.js +84 -5
- package/dist/repository-context.d.ts +3 -2
- package/dist/repository-context.js +112 -70
- package/dist/restart-certification.d.ts +1 -0
- package/dist/restart-certification.js +8 -6
- package/dist/result-acts.d.ts +29 -21
- package/dist/result-acts.js +36 -25
- package/dist/result-follow-ups.d.ts +2 -2
- package/dist/result-follow-ups.js +18 -4
- package/dist/result-review.d.ts +1 -1
- package/dist/route-change-check.d.ts +4 -0
- package/dist/route-change-check.js +13 -0
- package/dist/route-change.d.ts +22 -0
- package/dist/route-change.js +89 -0
- package/dist/route-hooks.js +3 -2
- package/dist/route-results.d.ts +6 -1
- package/dist/route-results.js +14 -5
- package/dist/run-closeout.d.ts +1 -1
- package/dist/run-closeout.js +7 -5
- package/dist/runner-process.js +3 -2
- package/dist/runner.js +2 -1
- package/dist/scope.d.ts +6 -1
- package/dist/scope.js +22 -9
- package/dist/serve.d.ts +1 -1
- package/dist/serve.js +24 -18
- package/dist/server/chat.js +14 -12
- package/dist/server/chrome.d.ts +1 -1
- package/dist/server/chrome.js +26 -35
- package/dist/server/console.css +23 -0
- package/dist/server/guards.js +4 -0
- package/dist/server/home.d.ts +6 -0
- package/dist/server/home.js +21 -13
- package/dist/server/http.js +1 -1
- package/dist/server/kit.js +1 -1
- package/dist/server/merge-ready-view.d.ts +10 -0
- package/dist/server/merge-ready-view.js +30 -0
- package/dist/server/pages-assets.d.ts +1 -0
- package/dist/server/pages-assets.js +8 -5
- package/dist/server/pages.js +103 -82
- package/dist/server/render-chat.js +2 -2
- package/dist/server/render-code-map.d.ts +30 -0
- package/dist/server/render-code-map.js +137 -0
- package/dist/server/render-flaky.d.ts +20 -0
- package/dist/server/render-flaky.js +59 -0
- package/dist/server/render-history.js +1 -1
- package/dist/server/render-new-task.d.ts +26 -1
- package/dist/server/render-new-task.js +30 -5
- package/dist/server/render-pages.d.ts +1 -175
- package/dist/server/render-pages.js +38 -313
- package/dist/server/render-projects.d.ts +2 -1
- package/dist/server/render-projects.js +9 -7
- package/dist/server/render-release.d.ts +19 -0
- package/dist/server/render-release.js +84 -0
- package/dist/server/render-results.d.ts +2 -2
- package/dist/server/render-results.js +71 -73
- package/dist/server/render-tasks.d.ts +13 -2
- package/dist/server/render-tasks.js +156 -139
- package/dist/server/retired-routes.d.ts +17 -0
- package/dist/server/retired-routes.js +53 -0
- package/dist/server/route-table.d.ts +4 -1
- package/dist/server/route-table.js +52 -13
- package/dist/server/settings.js +129 -22
- package/dist/server/task-views.js +61 -26
- package/dist/server/tasks.d.ts +17 -6
- package/dist/server/tasks.js +273 -338
- package/dist/server/welcome.js +1 -1
- package/dist/settings-groups-ui.js +1 -1
- package/dist/setup-guide.d.ts +2 -2
- package/dist/stop-proof.d.ts +67 -0
- package/dist/stop-proof.js +295 -0
- package/dist/storage-sweep-process.js +1 -1
- package/dist/storage-sweep.d.ts +5 -3
- package/dist/storage-sweep.js +30 -9
- package/dist/storage-ui.js +28 -17
- package/dist/storage.d.ts +27 -2
- package/dist/storage.js +39 -2
- package/dist/store.d.ts +148 -34
- package/dist/store.js +626 -177
- package/dist/surface.js +46 -5
- package/dist/sweep-quarantine.d.ts +1 -0
- package/dist/sweep-quarantine.js +6 -4
- package/dist/tamper-scan.d.ts +4 -0
- package/dist/tamper-scan.js +90 -0
- package/dist/task-aliases.d.ts +5 -0
- package/dist/task-aliases.js +9 -0
- package/dist/task-control.js +4 -1
- package/dist/task-outcome-cli.d.ts +2 -0
- package/dist/task-outcome-cli.js +36 -17
- package/dist/task-sizing.d.ts +5 -0
- package/dist/task-sizing.js +2 -0
- package/dist/task-source.d.ts +7 -0
- package/dist/task-source.js +14 -0
- package/dist/task-status.d.ts +9 -0
- package/dist/task-status.js +40 -49
- package/dist/teardown-git.d.ts +16 -0
- package/dist/teardown-git.js +63 -5
- package/dist/telegram-progress.js +5 -3
- package/dist/telegram-status.js +6 -4
- package/dist/test-infra.d.ts +8 -0
- package/dist/test-infra.js +18 -0
- package/dist/tidy.js +1 -1
- package/dist/toolroll-update-ui.js +2 -2
- package/dist/toolroll-update.js +18 -5
- package/dist/tui-contract.d.ts +24 -0
- package/dist/tui-source.js +67 -8
- package/dist/tui.js +51 -51
- package/dist/vanished-retry.d.ts +30 -0
- package/dist/vanished-retry.js +174 -0
- package/dist/work-index.d.ts +2 -0
- package/dist/work-index.js +82 -70
- package/dist/work-summary.d.ts +2 -2
- package/dist/work-summary.js +2 -0
- package/dist/workspace-ui.d.ts +4 -2
- package/dist/workspace-ui.js +28 -22
- package/dist/worktree.d.ts +12 -3
- package/dist/worktree.js +68 -13
- package/package.json +1 -1
- package/dist/server/render-next.d.ts +0 -63
- package/dist/server/render-next.js +0 -109
package/dist/server/tasks.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { titleWithin } from "../task-text.js";
|
|
2
2
|
import { parseFeedbackImage, sameFeedbackImage } from "../result-review.js";
|
|
3
|
-
import { archiveTaskFamily, dropFeedbackImage, releaseAuthorityOfTask, restoreTaskFamily, revokeRelease, runLiveCommands } from "../store.js";
|
|
3
|
+
import { archiveTaskFamily, dropFeedbackImage, releaseAuthorityOfTask, restoreTaskFamily, revokeRelease, runCheckStamp, runLiveCommands } from "../store.js";
|
|
4
|
+
import { ranWords } from "../check-stamp.js";
|
|
4
5
|
import { applyTidy, approverStandsNow, familyRepoOf, releaseFactsOf, tidyDays, tidyDigest, tidyPreview, TIDY_DEFAULT_DAYS } from "../tidy.js";
|
|
5
6
|
import { steerCapabilityOf } from "../steer-capabilities.js";
|
|
6
7
|
import { canReveal, localCheckoutOf, revealInFinder } from "../checkout-proof.js";
|
|
@@ -17,7 +18,7 @@ import { DEPRECATED_PAGE } from "../deprecations.js";
|
|
|
17
18
|
/** tasks handlers, moved without changing their route bodies. */
|
|
18
19
|
import { html, htmlString, joinHtml, postForm, textHtml } from "../html.js";
|
|
19
20
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
20
|
-
import { lstatSync, realpathSync } from "node:fs";
|
|
21
|
+
import { existsSync, lstatSync, realpathSync } from "node:fs";
|
|
21
22
|
import {} from "node:http";
|
|
22
23
|
import { withActor } from "../actor.js";
|
|
23
24
|
import { gateWords } from "../approval-policy.js";
|
|
@@ -32,8 +33,10 @@ import { checkResponse, CONSOLE_FORMS, readForm } from "../contracts/console-api
|
|
|
32
33
|
import { hasForbiddenControls, validateNote } from "../decision.js";
|
|
33
34
|
import { builderReadyFor, diagnoseTaskDispatch, firstRunBuilderRepo } from "../dispatch.js";
|
|
34
35
|
import { firstTaskSuggestions, withBuilderStep } from "../first-run.js";
|
|
35
|
-
import { newTaskPage } from "./render-new-task.js";
|
|
36
|
-
import {
|
|
36
|
+
import { fastStartPage, fastTermsRows, importPage, newTaskPage } from "./render-new-task.js";
|
|
37
|
+
import { fileGithubItem, readGithubItem } from "../github-import.js";
|
|
38
|
+
import { allowAutoPublish, autoPublishHeld } from "../release-auto.js";
|
|
39
|
+
import { fastStartBuild, keepGrownChange, prepareFastStart } from "../fast-start.js";
|
|
37
40
|
import { evidencePack } from "../evidence-pack.js";
|
|
38
41
|
import { decisionPage, evidenceRecordPage, runPage } from "./render-history.js";
|
|
39
42
|
import { readVerifiedArtifact, readVerifiedReport, writeEvidenceFile } from "../evidence.js";
|
|
@@ -44,6 +47,7 @@ import { validKeyTail } from "../live-records.js";
|
|
|
44
47
|
import { runNotes } from "../live-notes.js";
|
|
45
48
|
import { latestFinishedAttempt, retryNoteOf } from '../needs-you.js';
|
|
46
49
|
import { REMOTE_MESSAGES } from "../operate-remote.js";
|
|
50
|
+
import { proveRunStopped } from "../stop-proof.js";
|
|
47
51
|
import { aggregateNewNames, observeWorktree, parseBaseTreeSnapshot, PEEK_LIMITS } from "../peek.js";
|
|
48
52
|
import { isTaskSize, PHASES as ROUTE_PHASES } from "../phase-routing.js";
|
|
49
53
|
import { applyModeToNewFiling, authorizePlanUnderMode } from "../plan-auto.js";
|
|
@@ -59,7 +63,7 @@ import { structuredHandoffView, terminalDiffView } from "../result-evidence-read
|
|
|
59
63
|
import { fileAddTestsTask, requestFollowUpChecks } from '../result-follow-ups.js';
|
|
60
64
|
import { commentSourceKey, isRevisionFeedback, parseResultTab, resultReturnTarget } from "../result-review.js";
|
|
61
65
|
import { isAlive as runnerAlive } from "../runner.js";
|
|
62
|
-
import { passwordlessSessionFor, acceptanceLinesToInput, acceptanceToLines, approvalOf, approve as approveScope, freshnessFor, localApprovalEligible, spendingOnlyOnSuccess, ssoNowAvailable, withFreshness, modeFilingCoverage, PLACEHOLDER_RUBRIC, proposeGuarded } from "../scope.js";
|
|
66
|
+
import { passwordlessSessionFor, acceptanceLinesToInput, acceptanceToLines, approvalOf, approve as approveScope, freshnessFor, freshlyApprovable, localApprovalEligible, spendingOnlyOnSuccess, ssoNowAvailable, withFreshness, modeFilingCoverage, PLACEHOLDER_RUBRIC, proposeGuarded } from "../scope.js";
|
|
63
67
|
import { subagentNames as subagentNamesOf } from "../spend.js";
|
|
64
68
|
import { verifiedAuthor } from "../store.js";
|
|
65
69
|
import { runCostWords } from "../summary.js";
|
|
@@ -71,13 +75,18 @@ import { WorkIndexCursorError, workIndexPage } from "../work-index.js";
|
|
|
71
75
|
import { buildProgressOf, parseWorkView } from "../workspace-ui.js";
|
|
72
76
|
import { requestContext } from "./request-context.js";
|
|
73
77
|
import { boardTickScript, oneLineOf, QUEUE_VIEW, refuse, regionScript, screen, transcriptScript, whenTime } from "./chrome.js";
|
|
78
|
+
import { retiredRedirect } from "./retired-routes.js";
|
|
74
79
|
import { chatReturnWithSaid, matchTaskPath, redirect, respond, reviewHref, safeChatReturn, safeReturn, SAFETY, taskChatHref, taskHref } from "./http.js";
|
|
75
|
-
import { putAwayPage, boardBody, boardWorkers,
|
|
80
|
+
import { putAwayPage, boardBody, boardWorkers, QUEUE_FRONT, queueBody, queueScript, TASK_STATES, workPage } from "./render-pages.js";
|
|
76
81
|
import { runEfficiencyOf } from "../run-efficiency.js";
|
|
77
82
|
import { timingFor } from "../latency-timeline.js";
|
|
78
83
|
import { ACCEPT_ANYWAY_NEEDS_REASON, editorFileHref, proofBundleView, rankReviewQueue, RESULT_REFUSALS, REVIEW_QUEUE_CAP, reviewCockpitDetailParts, reviewCockpitPage, reviewPriorityOf, runFactsFragment, runOutcomeBadge, withRefusal } from "./render-results.js";
|
|
79
84
|
import { approvalFormDigest, approveRefusalWords, checkProgressHtml, consentDoorOf, nonceHashOf, PermissionsWouldChange, requirementsFromEditor, resumeDigestOf, taskOf } from "./render-tasks.js";
|
|
80
85
|
import {} from "./session.js";
|
|
86
|
+
import { approveReleaseCard, releaseApprovalCard, releaseSheetHref, releaseTaskOf } from "../release-approval.js";
|
|
87
|
+
import { releaseSheetPage } from "./render-release.js";
|
|
88
|
+
import { approvalFactsNow } from "./kit.js";
|
|
89
|
+
import { localOwnerRefusal } from "../local-owner.js";
|
|
81
90
|
export function createTasksHandlers(runtime) {
|
|
82
91
|
const { identities, store, peekSay, peekCache, PEEK_CACHE_TTL_MS, peekInFlight, PEEK_GLOBAL_INFLIGHT, peekBySession, PEEK_SESSION_INFLIGHT, clock, peekName, PEEK_FRAGMENT_BYTES, peekEvict, evidenceRoot, sendScreen, chromeFor, visible, consumeApprovalNonce, authenticateApprover, options, mintApprovalNonce, taskScreen, unscopedMode, admissionList, planViewOf, revisionViewOf, runIsTaskResult, leadPrincipal, familyOf, armTaskResume, runIsLive, revisionLedgerOf, resultDetailOf, reviewFactsFor, taskRepoOf, runVisible, restricted, codingActorAllowed, codingProjectAllowed, managedRepos, routeViewOf, workAccess, firstRunStepsNow, revisionDocOf, failureOf, dockedConversation, planContractViewOf, familiesInView, explainAttempt, taskChatFocus, familyTasksInView, liveCeiling, projectOf, ceiling, bustBadge, revisionDestination } = runtime;
|
|
83
92
|
/** A row admitted the address but its path parameter does not parse: the console's unknown page. */
|
|
@@ -89,7 +98,7 @@ export function createTasksHandlers(runtime) {
|
|
|
89
98
|
const showCode = (part, posted = false) => async (ctx) => {
|
|
90
99
|
const { url, who, response, project } = ctx;
|
|
91
100
|
if (who.via !== 'cookie' || !codingActorAllowed({ name: who.name, generation: who.session.generation }))
|
|
92
|
-
return refuse(response, who, 403, 'Coding sessions require installation-wide operator access. You can manage project tasks in Work.', '/
|
|
101
|
+
return refuse(response, who, 403, 'Coding sessions require installation-wide operator access. You can manage project tasks in Work.', '/tasks');
|
|
93
102
|
const actor = { name: who.name, generation: who.session.generation };
|
|
94
103
|
const sessionId = part === "workspace" || part === "unknown" ? null : /^\/code\/([a-f0-9]{32})/.exec(url.pathname)[1];
|
|
95
104
|
const read = part === "state" || part === "changes" ? part : null;
|
|
@@ -130,7 +139,7 @@ export function createTasksHandlers(runtime) {
|
|
|
130
139
|
if (!codingActorAllowed(actor))
|
|
131
140
|
return read !== null
|
|
132
141
|
? respond(response, 403, 'application/json', JSON.stringify({ ok: false, error: 'Your access changed. Sign in again.' }))
|
|
133
|
-
: refuse(response, who, 403, 'Your access changed. Sign in again.', '/
|
|
142
|
+
: refuse(response, who, 403, 'Your access changed. Sign in again.', '/tasks');
|
|
134
143
|
const message = error instanceof Error ? error.message : 'The coding workspace is unavailable.';
|
|
135
144
|
if (part === 'ship')
|
|
136
145
|
return sendScreen(response, 409, screen('Review for shipping', html `<h1>Prepare this result</h1><p>${message}</p><a href="/code/${sessionId}">Back to coding</a>`, { chrome: chromeFor(project, 'code') }));
|
|
@@ -139,82 +148,6 @@ export function createTasksHandlers(runtime) {
|
|
|
139
148
|
return sendScreen(response, 404, screen('Coding session unavailable', html `<h1>Session unavailable</h1><p>${message}</p><a href="/code">Open coding workspace</a>`, { chrome: chromeFor(project, 'code') }));
|
|
140
149
|
}
|
|
141
150
|
};
|
|
142
|
-
async function showInbox(ctx) {
|
|
143
|
-
const { url, who, request, response, now, project } = ctx;
|
|
144
|
-
// With no project open in scoped mode this is the ROLL-UP inbox:
|
|
145
|
-
// admission binds inside the bounded queries, every row's repo is
|
|
146
|
-
// re-proved here, and rows render as links only (Codex roll-up
|
|
147
|
-
// review, findings 6–8). Gaps stay per-project — they are derived
|
|
148
|
-
// against one repo's capabilities and roll up dishonestly.
|
|
149
|
-
const rollup = project === null && !unscopedMode;
|
|
150
|
-
const admission = rollup ? admissionList() : null;
|
|
151
|
-
const cancelled = store
|
|
152
|
-
.listCancelledBlockersScoped(project, 10, admission)
|
|
153
|
-
.filter(one => visible(one.repo) && visible(one.blockerRepo));
|
|
154
|
-
const inboxData = {
|
|
155
|
-
csrf: who.via === "cookie" ? who.session.csrf : "",
|
|
156
|
-
revision: who.via === "cookie" ? who.session.projectRevision : 0,
|
|
157
|
-
rollup,
|
|
158
|
-
interactive: project !== null,
|
|
159
|
-
decisions: store.listDecisionsScoped(project).filter(one => visible(one.repo)).slice(0, 10),
|
|
160
|
-
// Each waiting scope wears its consent door (v48 authority repair): a scope whose
|
|
161
|
-
// approval is closed — unreadable route, a route that cannot run,
|
|
162
|
-
// a pre-routing row whose approval lapsed — reads as needing
|
|
163
|
-
// attention, never as something to approve.
|
|
164
|
-
approvals: store.scopesAwaitingApproval(project, 10, admission).filter(one => visible(one.repo)).map(one => {
|
|
165
|
-
const ref = store.lookupRef(one.taskId);
|
|
166
|
-
const scope = store.getScope(one.taskId);
|
|
167
|
-
const door = consentDoorOf(scope, routeViewOf(one.taskId, ref, scope, now, who));
|
|
168
|
-
return { ...one, closed: door.open ? null : door.title };
|
|
169
|
-
}),
|
|
170
|
-
requeueables: store.listRequeueablesScoped(project, now, 10, admission).filter(one => visible(one.repo)),
|
|
171
|
-
needsVerification: store
|
|
172
|
-
.listCompletedWorkScoped(project, 10, admission)
|
|
173
|
-
.filter(one => visible(one.repo) && (one.proofVerdict === "short" || one.proofVerdict === "refuted") && !one.proofAccepted &&
|
|
174
|
-
assignmentOf(store, one.taskId, now, workAccess(), evidenceRoot) === null)
|
|
175
|
-
.map(one => ({
|
|
176
|
-
taskId: one.taskId,
|
|
177
|
-
title: one.title,
|
|
178
|
-
verdict: one.proofVerdict,
|
|
179
|
-
repo: one.repo,
|
|
180
|
-
matrix: one.proofMatrix,
|
|
181
|
-
repairChain: one.runId === null ? null : store.repairChainFor(one.runId),
|
|
182
|
-
})),
|
|
183
|
-
cancelledBlockers: cancelled,
|
|
184
|
-
gaps: project === null ? [] : computeGaps(store, project, now).filter(gap => gap.unblocks.length > 0).slice(0, 10),
|
|
185
|
-
// The builder step speaks for the same project as Chat's (firstRunBuilderRepo), as dispatch decides.
|
|
186
|
-
wizard: withBuilderStep(firstRunStepsNow(now), builderReadyFor(store, firstRunBuilderRepo(store, project, visible, managedRepos()), now)),
|
|
187
|
-
worker: (() => {
|
|
188
|
-
// The one fact the inbox must never hide (install review): with
|
|
189
|
-
// no worker answering, nothing here will ever build, and every
|
|
190
|
-
// approval below is a promise nobody is there to keep.
|
|
191
|
-
const runners = store.listRunners().filter(one => one.retiredAt === null && (!restricted() || one.repos.some(visible)));
|
|
192
|
-
const answering = runners.filter(one => runnerAlive(one, now));
|
|
193
|
-
const lastHeard = runners.map(one => one.heartbeatAt).sort().at(-1) ?? null;
|
|
194
|
-
return { answering: answering.length, registered: runners.length, lastHeard };
|
|
195
|
-
})(),
|
|
196
|
-
now,
|
|
197
|
-
// Console v2: results ready to review and work running now, from the same admitted work index.
|
|
198
|
-
...(() => {
|
|
199
|
-
const row = (one) => ({ taskId: one.rootId, title: one.title, detail: one.status.detail, repo: one.repo });
|
|
200
|
-
const needsYou = workIndexPage(store, now, workAccess(), { view: "needs-you", limit: 40, project, root: evidenceRoot });
|
|
201
|
-
const runningNow = workIndexPage(store, now, workAccess(), { view: "running", limit: 40, project, root: evidenceRoot });
|
|
202
|
-
return { ready: needsYou.items.filter(one => one.assignmentState === "ready-to-check").map(row), running: runningNow.items.map(row) };
|
|
203
|
-
})(),
|
|
204
|
-
};
|
|
205
|
-
// Which tabs hold something new since this browser last looked (the dots on a phone).
|
|
206
|
-
const tab = parseInboxTab(url.searchParams.get("tab")) ?? "all";
|
|
207
|
-
const prints = inboxFingerprints(inboxData);
|
|
208
|
-
const seenWords = /(?:^|;\s*)so-inbox-seen=([0-9a-f]{8}(?:\.[0-9a-f]{8}){3})(?:;|$)/.exec(request.headers.cookie ?? "")?.[1]?.split(".") ?? null;
|
|
209
|
-
const order = ["needs-you", "ready", "running", "all"];
|
|
210
|
-
const seen = Object.fromEntries(order.map((one, index) => [one, seenWords?.[index] ?? ""]));
|
|
211
|
-
const unread = order.filter(one => seenWords !== null && seen[one] !== prints[one]);
|
|
212
|
-
for (const one of order)
|
|
213
|
-
if (tab === "all" || one === tab)
|
|
214
|
-
seen[one] = prints[one];
|
|
215
|
-
response.setHeader("Set-Cookie", `so-inbox-seen=${order.map(one => seen[one] || prints[one]).join(".")}; SameSite=Lax; Path=/; Max-Age=31536000`);
|
|
216
|
-
return sendScreen(response, 200, inboxPage(chromeFor(project, "inbox"), { ...inboxData, tab, unread }));
|
|
217
|
-
}
|
|
218
151
|
async function showWork(ctx) {
|
|
219
152
|
const { url, who, response, now, project } = ctx;
|
|
220
153
|
// The Work destination (workspace package 1): every task in view as
|
|
@@ -227,7 +160,7 @@ export function createTasksHandlers(runtime) {
|
|
|
227
160
|
// The stored-state filter the old task list had (`?state=`): an unknown state is refused, as it was there.
|
|
228
161
|
const wantedState = url.searchParams.get("state");
|
|
229
162
|
if (wantedState !== null && !TASK_STATES.includes(wantedState))
|
|
230
|
-
return refuse(response, who, 400, "no such state", "/
|
|
163
|
+
return refuse(response, who, 400, "no such state", "/tasks");
|
|
231
164
|
const state = wantedState;
|
|
232
165
|
const rollup = project === null && !unscopedMode;
|
|
233
166
|
let work;
|
|
@@ -248,7 +181,7 @@ export function createTasksHandlers(runtime) {
|
|
|
248
181
|
}
|
|
249
182
|
catch (error) {
|
|
250
183
|
if (error instanceof WorkIndexCursorError)
|
|
251
|
-
return refuse(response, who, 400, 'This task page has expired. Open the first page.', '/
|
|
184
|
+
return refuse(response, who, 400, 'This task page has expired. Open the first page.', '/tasks');
|
|
252
185
|
throw error;
|
|
253
186
|
}
|
|
254
187
|
// A failed row says what its latest attempt missed, as its task page does (a stop reason already reads so), from
|
|
@@ -300,179 +233,13 @@ export function createTasksHandlers(runtime) {
|
|
|
300
233
|
});
|
|
301
234
|
// One project's Tasks dock that project's own conversation (v77).
|
|
302
235
|
const projectThread = url.searchParams.has('project') && project !== null && managedRepos().includes(project) ? project : null;
|
|
303
|
-
const docked = projectThread === null ? null : dockedConversation(who, null, projectThread, now, `/
|
|
236
|
+
const docked = projectThread === null ? null : dockedConversation(who, null, projectThread, now, `/tasks?project=${encodeURIComponent(projectThread)}`);
|
|
304
237
|
if (docked !== null)
|
|
305
238
|
page.workspace = { ...page.workspace, conversation: docked, pageHtml: page.body };
|
|
306
239
|
// Without a docked chat the list reads itself: 10 s while any of its tasks is building, else 30 s.
|
|
307
240
|
page.refreshSeconds = work.totals.running > 0 ? 10 : 30;
|
|
308
241
|
return sendScreen(response, 200, page);
|
|
309
242
|
}
|
|
310
|
-
async function showNext(ctx) {
|
|
311
|
-
const { url, who, response, now, project } = ctx;
|
|
312
|
-
// Triage: everything waiting on a person, one at a time, hardest-
|
|
313
|
-
// blocked first — oldest question, then plans and scopes to approve,
|
|
314
|
-
// then stalled work to retry, then requirement gaps. `skip` is a
|
|
315
|
-
// bounded, session-free cursor of keys the operator set aside; every
|
|
316
|
-
// act 303s back here, which is what makes it a flow and not a list.
|
|
317
|
-
const skipped = new Set((url.searchParams.get("skip") ?? "").split(",").filter(one => /^[darg]:[A-Za-z0-9._:-]{1,80}$/.test(one)).slice(0, 20));
|
|
318
|
-
const decisions = store.listDecisionsScoped(project).filter(one => one.state !== "answered");
|
|
319
|
-
const approvals = store.scopesAwaitingApproval(project, 20);
|
|
320
|
-
const requeueables = store.listRequeueablesScoped(project, now, 20);
|
|
321
|
-
const gaps = project === null ? [] : computeGaps(store, project, now).filter(gap => gap.unblocks.length > 0);
|
|
322
|
-
const queue = [
|
|
323
|
-
...decisions.map(decision => ({ key: `d:${decision.id}`, kind: "decision", decision })),
|
|
324
|
-
...approvals.map(approval => ({ key: `a:${approval.taskId}`, kind: "approval", approval })),
|
|
325
|
-
...requeueables.map(stalled => ({ key: `r:${stalled.taskId}`, kind: "requeue", stalled })),
|
|
326
|
-
...gaps.map(gap => ({ key: `g:${gap.key.replace(/[^A-Za-z0-9._:-]/g, "_")}`, kind: "gap", gap })),
|
|
327
|
-
];
|
|
328
|
-
const remaining = queue.filter(one => !skipped.has(one.key));
|
|
329
|
-
const item = remaining[0] ?? null;
|
|
330
|
-
const csrf = who.via === "cookie" ? who.session.csrf : "";
|
|
331
|
-
if (item !== null && item.kind === "approval") {
|
|
332
|
-
// The card restates the digest-bound terms, so the nonce may be
|
|
333
|
-
// minted here — same rule as the task screen, same binding.
|
|
334
|
-
const ref = store.lookupRef(item.approval.taskId);
|
|
335
|
-
const scope = store.getScope(item.approval.taskId);
|
|
336
|
-
const planView = ref !== null && ref.plan === "drafted" ? planViewOf(ref.id) : null;
|
|
337
|
-
const approvalDigest = approvalFormDigest(item.approval.digest, planView?.sha256 ?? null);
|
|
338
|
-
// The same agents block the task page and chat sign under (v48),
|
|
339
|
-
// and the same consent door: closed, it mints no nonce.
|
|
340
|
-
const route = routeViewOf(item.approval.taskId, ref, scope, now, who);
|
|
341
|
-
const nonce = who.via === "cookie" && consentDoorOf(scope, route).open ? mintApprovalNonce(who.name, item.approval.taskId, approvalDigest) : "";
|
|
342
|
-
return sendScreen(response, 200, nextPage(chromeFor(project, "inbox"), {
|
|
343
|
-
item, scope,
|
|
344
|
-
planDocument: planView?.document ?? null,
|
|
345
|
-
planContract: ref === null || planView === null ? null : planContractViewOf(ref.id, scope),
|
|
346
|
-
approvalDigest,
|
|
347
|
-
deliverable: ref?.deliverable ?? "branch",
|
|
348
|
-
route,
|
|
349
|
-
origin: originOf(store, item.approval.taskId, repo => store.accountCanAccess(who.name, repo)),
|
|
350
|
-
// Whether a remembered password may approve these exact terms: the task page's own preview (task-views.ts
|
|
351
|
-
// approvalFresh), read without minting a second nonce. Display only; the approve POST proves it again.
|
|
352
|
-
remembered: nonce !== "" && runtime.taskViewData(item.approval.taskId, who, null, { mintNonce: false })?.approvalFresh != null,
|
|
353
|
-
csrf, nonce, remaining: remaining.length, skipped: [...skipped], now,
|
|
354
|
-
}));
|
|
355
|
-
}
|
|
356
|
-
return sendScreen(response, 200, nextPage(chromeFor(project, "inbox"), {
|
|
357
|
-
item, scope: null, planDocument: null, planContract: null, csrf, nonce: "",
|
|
358
|
-
approvalDigest: null, route: null,
|
|
359
|
-
remaining: remaining.length, skipped: [...skipped], now,
|
|
360
|
-
}));
|
|
361
|
-
}
|
|
362
|
-
async function showBoard(ctx) {
|
|
363
|
-
const { url, who, response, now, project } = ctx;
|
|
364
|
-
// scope=all is the rolled-up view: every project this server was
|
|
365
|
-
// allowed to serve, on one board. The ceiling still rules row by row
|
|
366
|
-
// (rowVisible, the same predicate as every list) — a repo outside
|
|
367
|
-
// the server's configuration never renders a card, whatever the
|
|
368
|
-
// database holds. Unplaced work (repo NULL) appears: it dispatches
|
|
369
|
-
// anywhere, so every board honestly owns it.
|
|
370
|
-
const all = url.searchParams.get("scope") === "all";
|
|
371
|
-
// Roll-up admission happens BEFORE the query limit (Codex round 2,
|
|
372
|
-
// finding 11) — and root ceilings enumerate too, through the STORED
|
|
373
|
-
// repos that pass the ceiling (attended review, finding 3); the
|
|
374
|
-
// per-row visible() re-check below stays either way.
|
|
375
|
-
const admission = all ? admissionList() : null;
|
|
376
|
-
const snapshot = store.boardScoped(all ? null : project, now, 200, admission);
|
|
377
|
-
const admitted = all
|
|
378
|
-
? snapshot.tasks.filter(facts => facts.repo === null || visible(facts.repo))
|
|
379
|
-
: snapshot.tasks;
|
|
380
|
-
const done = all
|
|
381
|
-
? snapshot.done.filter(row => row.repo === null || visible(row.repo))
|
|
382
|
-
: snapshot.done;
|
|
383
|
-
// A blocker may live in a repo this server must not speak about —
|
|
384
|
-
// redact its state before the pure classifier composes a sentence
|
|
385
|
-
// from it (Codex round 2, finding 12). The dependency's NAME stays:
|
|
386
|
-
// the edge belongs to the visible task; the other project's live
|
|
387
|
-
// status does not.
|
|
388
|
-
const cards = admitted.map(facts => classify(facts.blockerRepo !== null && !visible(facts.blockerRepo)
|
|
389
|
-
? { ...facts, blockerState: null }
|
|
390
|
-
: facts, now));
|
|
391
|
-
// "Since you last looked": what concluded between this session's
|
|
392
|
-
// previous full board read and now. Fragment polls never move the
|
|
393
|
-
// anchor — an open tab is not a person looking.
|
|
394
|
-
let delta = null;
|
|
395
|
-
if (who.via === "cookie" && url.searchParams.get("fragment") !== "1") {
|
|
396
|
-
const prev = who.session.sawBoardAt;
|
|
397
|
-
if (prev !== null && now.getTime() - prev > 5 * 60_000) {
|
|
398
|
-
const sinceIso = new Date(prev).toISOString();
|
|
399
|
-
const runs = store.runsSinceScoped(sinceIso, all ? null : project).filter(one => all ? one.taskId !== "" : true);
|
|
400
|
-
delta = {
|
|
401
|
-
agoMinutes: Math.round((now.getTime() - prev) / 60_000),
|
|
402
|
-
built: runs.filter(one => one.outcome === "built" || one.outcome === "no-change").length,
|
|
403
|
-
failed: runs.filter(one => one.outcome === "failed").length,
|
|
404
|
-
questions: store.listDecisionsScoped(all ? null : project).filter(one => one.createdAt >= sinceIso && one.state !== "answered").length,
|
|
405
|
-
};
|
|
406
|
-
if (delta.built === 0 && delta.failed === 0 && delta.questions === 0)
|
|
407
|
-
delta = null;
|
|
408
|
-
}
|
|
409
|
-
who.session.sawBoardAt = now.getTime();
|
|
410
|
-
}
|
|
411
|
-
// A completed task whose proof is short or refuted and not yet
|
|
412
|
-
// accepted (Priority 2) reads "needs verification", not done — the
|
|
413
|
-
// board's once-and-only-once rule holds because this split is the
|
|
414
|
-
// ONE place a done row becomes either lane; the done-lane render
|
|
415
|
-
// below never sees the rows filtered out here.
|
|
416
|
-
const unverifiedDone = done.filter(row => (row.proofVerdict === "short" || row.proofVerdict === "refuted") && !row.proofAccepted);
|
|
417
|
-
const verifiedDone = done.filter(row => !unverifiedDone.includes(row));
|
|
418
|
-
const unverifiedCards = unverifiedDone.map(row => {
|
|
419
|
-
// v40: the SAME chip gains one more word when a repair chain
|
|
420
|
-
// exists for this row's own run — never a second card.
|
|
421
|
-
const chain = row.runId === null ? null : store.repairChainFor(row.runId);
|
|
422
|
-
const repairChain = chain === null
|
|
423
|
-
? null
|
|
424
|
-
: {
|
|
425
|
-
attempt: chain.attempt,
|
|
426
|
-
outcome: chain.outcome,
|
|
427
|
-
approved: chain.draftTask !== null && (store.getScope(chain.draftTask)?.approvedAt ?? null) !== null,
|
|
428
|
-
};
|
|
429
|
-
return attentionCardForUnverifiedDone({
|
|
430
|
-
taskId: row.taskId,
|
|
431
|
-
title: row.title,
|
|
432
|
-
repo: row.repo,
|
|
433
|
-
completedAt: row.completedAt,
|
|
434
|
-
proofVerdict: row.proofVerdict,
|
|
435
|
-
proofMatrix: row.proofMatrix,
|
|
436
|
-
repairChain,
|
|
437
|
-
});
|
|
438
|
-
});
|
|
439
|
-
// Project-level Needs you (evals-nightly.md, part A3): GitHub's nightly lane failures and the plane's own nightly
|
|
440
|
-
// check of main, for the projects this board shows.
|
|
441
|
-
const nightlyCards = nightlyNeedsYou(store, repo => visible(repo) && (all || project === null || repo === project)).map(need => ({
|
|
442
|
-
lane: "attention", taskId: need.key, title: need.title, repo: need.repo, reason: need.reason, href: need.href, claim: null,
|
|
443
|
-
stalledSince: need.since, attempt: null, overdue: false, priority: 0, assignedRunner: null,
|
|
444
|
-
}));
|
|
445
|
-
const laneCards = [...cards, ...unverifiedCards, ...nightlyCards];
|
|
446
|
-
// The worker strip: the workers this board's work can go to — bound to
|
|
447
|
-
// this project, or (rolled up) to any project the ceiling admits.
|
|
448
|
-
const workers = boardWorkers(store.listRunners(), { all, project, visible });
|
|
449
|
-
const body = boardBody({ cards: laneCards, workers, done: verifiedDone, saturated: snapshot.saturated, now, all, project, delta }, pr => store.ciFailureObserved(pr));
|
|
450
|
-
if (url.searchParams.get("fragment") === "1") {
|
|
451
|
-
// The live region alone — the in-page swapper's diet. Same auth,
|
|
452
|
-
// same ceiling, no shell, no scripts (finding 2).
|
|
453
|
-
return respond(response, 200, "text/html; charset=utf-8", body);
|
|
454
|
-
}
|
|
455
|
-
if (url.searchParams.get("view") === "order") {
|
|
456
|
-
// The board's ORDER view (operator request): the same screen, flipped
|
|
457
|
-
// to dispatch order with the queue's drag handles. Reordering and
|
|
458
|
-
// reserving are scheduling, not authority, so this is the one view
|
|
459
|
-
// where a drag does anything; the state view stays a view.
|
|
460
|
-
const csrf = who.via === "cookie" ? who.session.csrf : "";
|
|
461
|
-
const revision = who.via === "cookie" ? who.session.projectRevision : 0;
|
|
462
|
-
const region = queueRegionFor(project, csrf, revision, now);
|
|
463
|
-
return sendScreen(response, 200, screen("board", joinHtml([
|
|
464
|
-
html `<h1>Board</h1>`,
|
|
465
|
-
html `<p class="meta board-view"><a href="/board">state</a> \u00b7 <strong>order</strong> <span class="meta">\u2014 drag to reorder, or onto a worker to reserve; the state view is where cards move on their own</span></p>`,
|
|
466
|
-
html `<div id="queue-region">${region}</div>`,
|
|
467
|
-
html `<p class="meta" id="queue-region-stamp"></p>`,
|
|
468
|
-
], "\n"), { chrome: chromeFor(project, "board"), functional: { script: queueScript(), fetches: true } }));
|
|
469
|
-
}
|
|
470
|
-
const regionBody = html `<div id="board-region">${body}</div><p class="meta" id="board-region-stamp"></p>`;
|
|
471
|
-
return sendScreen(response, 200, screen("board", regionBody, {
|
|
472
|
-
chrome: chromeFor(project, "board", undefined, all ? "board-all" : undefined),
|
|
473
|
-
functional: { script: regionScript("board-region", "1", undefined, { animate: true }) + boardTickScript(), fetches: true },
|
|
474
|
-
}));
|
|
475
|
-
}
|
|
476
243
|
/** A completed (or attempted) row as the review list ranks it: its proof reasons, CI standing, and the assignment
|
|
477
244
|
* when that assignment's receipt is this very row. */
|
|
478
245
|
function reviewRowOf(row, now) {
|
|
@@ -569,9 +336,9 @@ export function createTasksHandlers(runtime) {
|
|
|
569
336
|
const selectedRow = wantedId === null ? ranked[0] ?? null : chosen;
|
|
570
337
|
// An exact link to a result this person can't see (or that never existed) reads the same either way.
|
|
571
338
|
if (expectedRun !== null && wantedId !== null && chosen === null)
|
|
572
|
-
return refuse(response, who, 404, "No such result in your projects.", "/
|
|
339
|
+
return refuse(response, who, 404, "No such result in your projects.", "/tasks");
|
|
573
340
|
if (expectedRun !== null && (!/^[1-9]\d*$/.test(expectedRun) || selectedRow?.runId !== Number(expectedRun))) {
|
|
574
|
-
return sendScreen(response, 409, screen("Result changed", html `<h1>Result changed</h1><p>This acceptance link no longer matches the current result. Review the current task before accepting.</p><p class="refusal-back"><a class="button-link" href="/
|
|
341
|
+
return sendScreen(response, 409, screen("Result changed", html `<h1>Result changed</h1><p>This acceptance link no longer matches the current result. Review the current task before accepting.</p><p class="refusal-back"><a class="button-link" href="/tasks?view=ready">Review results</a></p>`, { chrome: chromeFor(project, "runs") }));
|
|
575
342
|
}
|
|
576
343
|
const selected = picked !== null ? picked.selected : selectedRow === null ? null : reviewCockpitViewOf(selectedRow, who, now);
|
|
577
344
|
// Opening a run's own result page is viewing it: the shell says so with a keyed POST /agents/seen (easy-start N1:
|
|
@@ -602,39 +369,59 @@ export function createTasksHandlers(runtime) {
|
|
|
602
369
|
reviewPage.workspace = { ...reviewPage.workspace, conversation: reviewDocked, pageHtml: reviewPage.body };
|
|
603
370
|
return sendScreen(response, 200, reviewPage);
|
|
604
371
|
}
|
|
605
|
-
/** Finished tasks live in the Tasks list's Finished view now (UI revamp wave B): the old address keeps answering, as a
|
|
606
|
-
* GET alias, with a named project carried along. */
|
|
607
|
-
async function showDone(ctx) {
|
|
608
|
-
const { url, response } = ctx;
|
|
609
|
-
const named = url.searchParams.get("project");
|
|
610
|
-
return redirectGet(response, `/work?view=completed${named === null || named === "" ? "" : `&project=${encodeURIComponent(named)}`}`);
|
|
611
|
-
}
|
|
612
372
|
/** The old task list, retired into /work (docs/plans/reui-polish.md slice 7): its address keeps answering, as a
|
|
613
373
|
* redirect, so links and bookmarks still land. `state` carries over (an unknown one is still refused); `template`
|
|
614
374
|
* goes to the add page, which now pre-fills it. Anything else is dropped. */
|
|
375
|
+
/** Tasks (docs/plans/plain-toolroll.md P1.4): the one list, with List, Board, Ready, Archived and an every-project
|
|
376
|
+
* scope (`scope=all`). `view=board` is the Board, `view=order` the queue's order, `view=done` Finished. */
|
|
615
377
|
async function showTasks(ctx) {
|
|
616
|
-
const { url
|
|
617
|
-
const wanted = url.searchParams.get("state");
|
|
618
|
-
if (wanted !== null && !TASK_STATES.includes(wanted))
|
|
619
|
-
return refuse(response, who, 400, "no such state", "/work");
|
|
378
|
+
const { url } = ctx;
|
|
620
379
|
const template = url.searchParams.get("template");
|
|
621
380
|
if (template !== null)
|
|
622
|
-
return redirect(response, `/tasks/new?template=${encodeURIComponent(template)}`);
|
|
623
|
-
|
|
381
|
+
return redirect(ctx.response, `/tasks/new?template=${encodeURIComponent(template)}`);
|
|
382
|
+
const view = url.searchParams.get("view");
|
|
383
|
+
if (view === "order")
|
|
384
|
+
return showOrder(ctx);
|
|
385
|
+
// The list reads its own keys: Board is its layout, every project its roll-up, Finished its completed view.
|
|
386
|
+
const read = new URL(url.href);
|
|
387
|
+
if (view === "board") {
|
|
388
|
+
read.searchParams.delete("view");
|
|
389
|
+
read.searchParams.set("layout", "board");
|
|
390
|
+
}
|
|
391
|
+
if (view === "done")
|
|
392
|
+
read.searchParams.set("view", "completed");
|
|
393
|
+
if (read.searchParams.get("scope") === "all") {
|
|
394
|
+
read.searchParams.delete("scope");
|
|
395
|
+
read.searchParams.set("all", "1");
|
|
396
|
+
}
|
|
397
|
+
return showWork({ ...ctx, url: read });
|
|
624
398
|
}
|
|
625
|
-
|
|
399
|
+
/** The queue's order view, under Tasks: dispatch order with drag handles (scheduling, never authority). */
|
|
400
|
+
async function showOrder(ctx) {
|
|
626
401
|
const { url, who, response, project } = ctx;
|
|
627
402
|
const csrf = who.via === "cookie" ? who.session.csrf : "";
|
|
628
403
|
const revision = who.via === "cookie" ? who.session.projectRevision : 0;
|
|
629
|
-
|
|
630
|
-
// in THIS project, per worker (the capacity is global), so the header
|
|
631
|
-
// names both and never a ratio.
|
|
632
|
-
if (url.searchParams.get("fragment") === "1") {
|
|
404
|
+
if (url.searchParams.get("fragment") === "1")
|
|
633
405
|
return respond(response, 200, "text/html; charset=utf-8", queueRegionFor(project, csrf, revision, clock()));
|
|
406
|
+
const region = queueRegionFor(project, csrf, revision, clock());
|
|
407
|
+
return sendScreen(response, 200, screen("order", joinHtml([
|
|
408
|
+
html `<h1>Order the queue</h1>`,
|
|
409
|
+
html `<p class="meta board-view"><a href="/tasks?view=board">Board</a> · <strong>order</strong> <span class="meta">— drag to reorder, or onto a builder to keep it for that builder</span></p>`,
|
|
410
|
+
html `<div id="queue-region">${region}</div>`,
|
|
411
|
+
html `<p class="meta" id="queue-region-stamp"></p>`,
|
|
412
|
+
], "\n"), { chrome: chromeFor(project, "work"), functional: { script: queueScript(), fetches: true } }));
|
|
413
|
+
}
|
|
414
|
+
/** /n/<number>: a push's task link, by the task's stored number, never its title. It opens the task's page only
|
|
415
|
+
* inside the viewer's own projects; anything else is the same not-found as an unknown number. */
|
|
416
|
+
async function showNumberedTask(ctx) {
|
|
417
|
+
const { url, who, response } = ctx;
|
|
418
|
+
const number = Number(url.pathname.slice("/n/".length));
|
|
419
|
+
const ref = Number.isSafeInteger(number) ? store.refForId(number) : null;
|
|
420
|
+
const task = ref === null || ref.backend !== "built-in" ? null : ref.externalId;
|
|
421
|
+
if (task === null || ref === null || ref.repo === null || !visible(ref.repo) || !store.accountCanAccess(who.name, ref.repo)) {
|
|
422
|
+
return refuse(response, who, 404, "There's no page at this address.", "/tasks");
|
|
634
423
|
}
|
|
635
|
-
|
|
636
|
-
// keeps answering, as a redirect, so nothing anyone bookmarked 404s.
|
|
637
|
-
return redirect(response, QUEUE_VIEW);
|
|
424
|
+
return redirectGet(response, taskHref(task));
|
|
638
425
|
}
|
|
639
426
|
async function showNewTask(ctx) {
|
|
640
427
|
const picked = (() => { const name = ctx.url.searchParams.get("template"); return name === null ? null : templateByName(name); })();
|
|
@@ -652,7 +439,7 @@ export function createTasksHandlers(runtime) {
|
|
|
652
439
|
return noPage(who, response);
|
|
653
440
|
const family = familyOf(task.taskId);
|
|
654
441
|
if (family === null)
|
|
655
|
-
return refuse(response, who, 404, "no such task", "/
|
|
442
|
+
return refuse(response, who, 404, "no such task", "/tasks");
|
|
656
443
|
const version = url.searchParams.get("version");
|
|
657
444
|
if (version !== null && !family.versions.some(one => one.id === version))
|
|
658
445
|
return refuse(response, who, 404, "That version is not available for this task.", taskHref(family.root.id));
|
|
@@ -704,12 +491,12 @@ export function createTasksHandlers(runtime) {
|
|
|
704
491
|
const access = workAccess();
|
|
705
492
|
const family = store.taskFamilyOf(evidence.taskId, access.repos, access.includeUnplaced);
|
|
706
493
|
if (family === null || !visible(family.root.repo))
|
|
707
|
-
return refuse(response, who, 404, "no such task", "/
|
|
494
|
+
return refuse(response, who, 404, "no such task", "/tasks");
|
|
708
495
|
if (evidence.taskId !== family.root.id)
|
|
709
496
|
return redirect(response, `${taskHref(family.root.id)}/evidence${url.search}`);
|
|
710
497
|
const pack = evidencePack(store, family.root.id, access, who.name, now, evidenceRoot);
|
|
711
498
|
if (pack === null)
|
|
712
|
-
return refuse(response, who, 404, "no such task", "/
|
|
499
|
+
return refuse(response, who, 404, "no such task", "/tasks");
|
|
713
500
|
if (url.searchParams.get("format") === "json") {
|
|
714
501
|
response.writeHead(200, { "content-type": "application/json; charset=utf-8", "content-disposition": `attachment; filename="evidence-${family.root.id.replace(/[^A-Za-z0-9._-]/g, "_")}.json"`,
|
|
715
502
|
"cache-control": "no-store", "x-content-type-options": "nosniff" });
|
|
@@ -855,7 +642,9 @@ export function createTasksHandlers(runtime) {
|
|
|
855
642
|
who.via === "cookie" &&
|
|
856
643
|
who.session.editorLinks === true &&
|
|
857
644
|
// A reviewer run (v29) never had a checkout — no files to open.
|
|
858
|
-
found.worktree !== null
|
|
645
|
+
found.worktree !== null &&
|
|
646
|
+
// Idle cleanup removes the folder and a later run makes it again: no link to a folder that isn't there.
|
|
647
|
+
existsSync(found.worktree)
|
|
859
648
|
? { worktree: found.worktree }
|
|
860
649
|
: null, options.editorLinks !== undefined &&
|
|
861
650
|
options.localRunner !== undefined &&
|
|
@@ -1062,6 +851,30 @@ export function createTasksHandlers(runtime) {
|
|
|
1062
851
|
}
|
|
1063
852
|
// One filing door for every surface (Codex adoption review, finding 7).
|
|
1064
853
|
const after = (body.get("after") ?? "").trim();
|
|
854
|
+
// From an issue or pull request (github-import.ts): read and quoted first; filing is the second press, on what was read.
|
|
855
|
+
const fromLink = (body.get("from") ?? "").trim();
|
|
856
|
+
if (fromLink !== "") {
|
|
857
|
+
if (who.via !== "cookie" || repo === "")
|
|
858
|
+
return newTaskScreen(ctx, 400, "Choose the project this issue belongs to.", draft);
|
|
859
|
+
if (body.get("from-read") !== "1") {
|
|
860
|
+
const read = await readGithubItem(store, { url: fromLink, repo });
|
|
861
|
+
if (!read.ok)
|
|
862
|
+
return newTaskScreen(ctx, 400, read.message, draft);
|
|
863
|
+
return sendScreen(response, 200, importPage(chromeFor(project ?? null, "tasks"), { repo, url: read.item.url, title: read.item.title, body: read.item.body, kind: read.item.kind, number: read.item.number, head: read.item.head }));
|
|
864
|
+
}
|
|
865
|
+
const filed = await fileGithubItem(store, { url: fromLink, repo, actor: who.name, shownHead: body.get("from-head") ?? null }, now);
|
|
866
|
+
if (!filed.ok)
|
|
867
|
+
return newTaskScreen(ctx, filed.reason === "head-moved" ? 409 : 400, filed.message, draft);
|
|
868
|
+
return redirect(response, taskHref(filed.taskId));
|
|
869
|
+
}
|
|
870
|
+
// Small work a person starts themselves: their words become the exact terms, and Build is their yes (fast-start.ts).
|
|
871
|
+
// Anything else, or any doubt, is the ordinary filing below. `plain` is "Plan it first" from that form.
|
|
872
|
+
if (who.via === "cookie" && body.get("plain") !== "1" && repo !== "" && goal === "" && notThis === "" && touchesGiven.length === 0 && !scout && id === "" && after === ""
|
|
873
|
+
&& (qualityMode === null || qualityMode === "default") && (permissionMode === null || permissionMode === "auto")) {
|
|
874
|
+
const fast = await prepareFastStart(store, { actor: who.name, repo, title: typed.trim(), via: "console", ...(permissionMode === null ? {} : { permissionMode }), password: fastPasswordFacts(who) }, now);
|
|
875
|
+
if (fast.eligible)
|
|
876
|
+
return sendScreen(response, 200, fastStartPage(chromeFor(project ?? null, "tasks"), { title: typed.trim(), repo, preview: fast.preview, digest: fast.digest, terms: fastTermsRows(store, fast.scope, repo) }));
|
|
877
|
+
}
|
|
1065
878
|
let chainProblem = null;
|
|
1066
879
|
const made = store.transact(() => {
|
|
1067
880
|
// A project folder's identity changed while this request waited (a root or link now leads elsewhere): what was
|
|
@@ -1241,7 +1054,7 @@ export function createTasksHandlers(runtime) {
|
|
|
1241
1054
|
const id = Number(answer[1]);
|
|
1242
1055
|
const requestedReturn = body.get("return");
|
|
1243
1056
|
const decisionBack = requestedReturn === "next"
|
|
1244
|
-
? "/
|
|
1057
|
+
? "/"
|
|
1245
1058
|
: requestedReturn === null
|
|
1246
1059
|
? `/d/${id}`
|
|
1247
1060
|
: safeChatReturn(requestedReturn);
|
|
@@ -1284,37 +1097,37 @@ export function createTasksHandlers(runtime) {
|
|
|
1284
1097
|
}
|
|
1285
1098
|
return redirect(response, decisionBack);
|
|
1286
1099
|
}
|
|
1287
|
-
async function
|
|
1288
|
-
const { url, who, response,
|
|
1289
|
-
const act = matchTaskPath(url.pathname, "/
|
|
1100
|
+
async function taskRecheckStopped(ctx) {
|
|
1101
|
+
const { url, who, response, posted } = ctx;
|
|
1102
|
+
const act = matchTaskPath(url.pathname, "/recheck-stopped$");
|
|
1290
1103
|
if (act === null)
|
|
1291
1104
|
return noPage(who, response);
|
|
1292
|
-
const body = readForm(posted, CONSOLE_FORMS.
|
|
1293
|
-
//
|
|
1294
|
-
//
|
|
1105
|
+
const body = readForm(posted, CONSOLE_FORMS.recheckStopped);
|
|
1106
|
+
// Check again: Toolroll looks at this computer's processes for the build once more (stop-proof.ts) and settles it
|
|
1107
|
+
// when none of them is the build's. Nobody vouches that a build stopped, so there is nothing to confirm here.
|
|
1295
1108
|
const back = taskHref(act.taskId);
|
|
1296
1109
|
const ref = store.lookupRef(act.taskId);
|
|
1297
1110
|
if (ref === null || !visible(ref.repo))
|
|
1298
|
-
return refuse(response, who, 404, "no such task", "/
|
|
1111
|
+
return refuse(response, who, 404, "no such task", "/tasks");
|
|
1299
1112
|
const named = (body.get("run") ?? "").trim();
|
|
1300
1113
|
const run = /^[0-9]{1,15}$/.test(named) ? store.getRun(Number(named)) : null;
|
|
1301
1114
|
const runTask = run === null ? null : store.externalIdFor(run.taskRef);
|
|
1302
1115
|
if (run === null || runTask === null || familyOf(runTask)?.root.id !== (familyOf(act.taskId)?.root.id ?? act.taskId)) {
|
|
1303
1116
|
return taskScreen(response, who, act.taskId, "That build isn't part of this task.", 409);
|
|
1304
1117
|
}
|
|
1305
|
-
if (!
|
|
1306
|
-
return taskScreen(response, who, act.taskId, "
|
|
1307
|
-
|
|
1308
|
-
// When Toolroll can't check at all, the approver also says they checked: never a one-click confirmation.
|
|
1309
|
-
if (store.stopQuiescenceFact(run.id)?.kind === "unknown" && body.get("checked") !== "yes") {
|
|
1310
|
-
return taskScreen(response, who, act.taskId, `Make sure nothing from build #${run.id} is running, then tick the box to confirm.`, 409);
|
|
1311
|
-
}
|
|
1312
|
-
const settled = store.settleRunWitnessesByApprover({ runId: run.id, by: who.name, why: "Confirmed in the console that nothing from this build is running." }, now);
|
|
1313
|
-
if (!settled.ok) {
|
|
1314
|
-
return taskScreen(response, who, act.taskId, settled.reason === "alive" || settled.reason === "still-running"
|
|
1315
|
-
? `Something from build #${run.id} may still be running, so it can't be confirmed stopped yet.` : `Build #${run.id} can't be confirmed stopped.`, 409);
|
|
1316
|
-
}
|
|
1118
|
+
if (!store.accountCanAccess(who.name, ref.repo))
|
|
1119
|
+
return taskScreen(response, who, act.taskId, "You can't act on this project's tasks.", 403);
|
|
1120
|
+
const proved = await proveRunStopped(store, run.id, clock);
|
|
1317
1121
|
bustBadge();
|
|
1122
|
+
const left = store.stopQuiescenceFact(run.id);
|
|
1123
|
+
if (left !== null) {
|
|
1124
|
+
const words = proved.outcome === "blocked" ? `Build #${proved.run} ${proved.words}.`
|
|
1125
|
+
: left.kind === "alive" || left.kind === "open" ? `Something from build #${left.run} may still be running. Toolroll stops what it proves is the build's on its own.`
|
|
1126
|
+
: left.kind === "elsewhere" ? `Build #${left.run} ran on another computer; only that computer can check it stopped.`
|
|
1127
|
+
: left.kind === "unknown" ? `Toolroll still can't prove build #${left.run} stopped from this computer.`
|
|
1128
|
+
: `Toolroll is still making sure build #${left.run} stopped. This clears on its own.`;
|
|
1129
|
+
return taskScreen(response, who, act.taskId, words, 409);
|
|
1130
|
+
}
|
|
1318
1131
|
const to = resultReturnTarget(body.get("return"), run.id, runTasks(run.taskRef));
|
|
1319
1132
|
return redirect(response, body.get("return") ? to : back);
|
|
1320
1133
|
}
|
|
@@ -1330,7 +1143,7 @@ export function createTasksHandlers(runtime) {
|
|
|
1330
1143
|
return taskScreen(response, who, act.taskId, "The demo merges nothing.", 403);
|
|
1331
1144
|
const ref = store.lookupRef(act.taskId);
|
|
1332
1145
|
if (ref === null || !visible(ref.repo))
|
|
1333
|
-
return refuse(response, who, 404, "no such task", "/
|
|
1146
|
+
return refuse(response, who, 404, "no such task", "/tasks");
|
|
1334
1147
|
const named = (body.get("run") ?? "").trim();
|
|
1335
1148
|
const run = /^[0-9]{1,15}$/.test(named) ? store.getRun(Number(named)) : null;
|
|
1336
1149
|
const runTask = run === null ? null : store.externalIdFor(run.taskRef);
|
|
@@ -1692,7 +1505,7 @@ export function createTasksHandlers(runtime) {
|
|
|
1692
1505
|
const resolved = store.resolveIncident(id, who.name, now);
|
|
1693
1506
|
if (!resolved)
|
|
1694
1507
|
return refuse(response, who, 409, "already resolved, or never open");
|
|
1695
|
-
const back = body.get("return") === "inbox" ? "/
|
|
1508
|
+
const back = body.get("return") === "inbox" ? "/" : openRow === undefined ? "/" : taskHref(openRow.taskId);
|
|
1696
1509
|
return redirect(response, back);
|
|
1697
1510
|
}
|
|
1698
1511
|
/** The compact recent-runs list for the master pane. */
|
|
@@ -1818,10 +1631,10 @@ export function createTasksHandlers(runtime) {
|
|
|
1818
1631
|
function taskMutation(response, who, taskId, verb, posted, now) {
|
|
1819
1632
|
const ref = store.lookupRef(taskId);
|
|
1820
1633
|
if (ref === null || store.getTask(taskId) === null) {
|
|
1821
|
-
return refuse(response, who, 404, "no such task", "/
|
|
1634
|
+
return refuse(response, who, 404, "no such task", "/tasks");
|
|
1822
1635
|
}
|
|
1823
1636
|
if (!visible(ref.repo)) {
|
|
1824
|
-
return refuse(response, who, 404, "no such task", "/
|
|
1637
|
+
return refuse(response, who, 404, "no such task", "/tasks");
|
|
1825
1638
|
}
|
|
1826
1639
|
switch (verb) {
|
|
1827
1640
|
case "steer": {
|
|
@@ -2086,7 +1899,7 @@ export function createTasksHandlers(runtime) {
|
|
|
2086
1899
|
return taskScreen(response, who, taskId, "Retried. The note wasn't saved; add it under Steering.", 409);
|
|
2087
1900
|
}
|
|
2088
1901
|
// Allow-listed return only — never an arbitrary URL from the form.
|
|
2089
|
-
return redirect(response, body.get("return") === "inbox"
|
|
1902
|
+
return redirect(response, body.get("return") === "inbox" || body.get("return") === "next" ? "/" : taskHref(taskId));
|
|
2090
1903
|
}
|
|
2091
1904
|
case "follow-up": {
|
|
2092
1905
|
const body = readForm(posted, CONSOLE_FORMS.taskFollowUp);
|
|
@@ -2454,11 +2267,15 @@ export function createTasksHandlers(runtime) {
|
|
|
2454
2267
|
const body = readForm(posted, CONSOLE_FORMS.taskApprove);
|
|
2455
2268
|
const requestedReturn = body.get("return");
|
|
2456
2269
|
const approvalBack = requestedReturn === "next"
|
|
2457
|
-
? "/
|
|
2270
|
+
? "/"
|
|
2458
2271
|
: requestedReturn === null
|
|
2459
2272
|
? taskHref(taskId)
|
|
2460
2273
|
: safeChatReturn(requestedReturn);
|
|
2274
|
+
// The release sheet's Approve release (trusted-release-reports.md A7): the same door, its own page for words.
|
|
2275
|
+
const sheet = body.get("sheet") === "release";
|
|
2461
2276
|
const approvalProblem = (message, status) => {
|
|
2277
|
+
if (sheet)
|
|
2278
|
+
return releaseSheet.sheetScreen({ who, response, project: null }, taskId, message, status);
|
|
2462
2279
|
if (requestedReturn !== null && requestedReturn !== "next") {
|
|
2463
2280
|
return redirect(response, chatReturnWithSaid(approvalBack, message));
|
|
2464
2281
|
}
|
|
@@ -2484,6 +2301,9 @@ export function createTasksHandlers(runtime) {
|
|
|
2484
2301
|
&& !(who.authority === "local-owner" && localApprovalEligible(store, who.name, taskId).ok)) {
|
|
2485
2302
|
return approvalProblem("approval requires your password, typed again", 400);
|
|
2486
2303
|
}
|
|
2304
|
+
// The sheet binds the release card's digest (the nonce was minted for it): the shared card wrapper decides.
|
|
2305
|
+
if (sheet)
|
|
2306
|
+
return releaseSheet.approveFromSheet({ who, response, project: null }, taskId, digest, token);
|
|
2487
2307
|
const scopeRow = store.getScope(taskId);
|
|
2488
2308
|
if (scopeRow === null)
|
|
2489
2309
|
return approvalProblem("this task has no scope to approve", 409);
|
|
@@ -2948,6 +2768,8 @@ export function createTasksHandlers(runtime) {
|
|
|
2948
2768
|
timing: timingFor(store, run.id, row.taskId),
|
|
2949
2769
|
// How far its checks reached (check-access.ts), display only.
|
|
2950
2770
|
checkAccess: store.runCheckAccess(run.id),
|
|
2771
|
+
// How its checks actually ran, by what was stamped at launch: said only for a result whose checks ran.
|
|
2772
|
+
checkRan: store.artifactsFor(run.id).some(one => one.kind === "check-log") ? ranWords(runCheckStamp(store, run.id)) : null,
|
|
2951
2773
|
reviewRetry: store.reviewRetryStateOf(run.id),
|
|
2952
2774
|
review: reviewFactsFor(run.id),
|
|
2953
2775
|
notes: store.notesForRun(run.id),
|
|
@@ -3078,6 +2900,7 @@ export function createTasksHandlers(runtime) {
|
|
|
3078
2900
|
return live;
|
|
3079
2901
|
}
|
|
3080
2902
|
const tidyHandlers = createTidyHandlers(runtime);
|
|
2903
|
+
const releaseSheet = createReleaseSheetHandlers(runtime);
|
|
3081
2904
|
/** The question page, with a refusal in its box when an answer was refused (the box keeps the choice to make). */
|
|
3082
2905
|
function decisionScreen(ctx, id, back, refusal, code, now, draft) {
|
|
3083
2906
|
const { who, response, project } = ctx;
|
|
@@ -3095,6 +2918,7 @@ export function createTasksHandlers(runtime) {
|
|
|
3095
2918
|
}
|
|
3096
2919
|
const registrations = handlersOf("tasks", {
|
|
3097
2920
|
...tidyHandlers,
|
|
2921
|
+
...releaseSheet.routes,
|
|
3098
2922
|
...createAgentSignalHandlers(runtime),
|
|
3099
2923
|
"code.page": showCode("workspace"),
|
|
3100
2924
|
"code.session": showCode("session"),
|
|
@@ -3105,14 +2929,17 @@ export function createTasksHandlers(runtime) {
|
|
|
3105
2929
|
"code.ship-preview": showCode("ship", true),
|
|
3106
2930
|
"code.other": showCode("unknown"),
|
|
3107
2931
|
// Retired into Tasks and the task page (src/design/KIT.md): each old address still answers, as a redirect.
|
|
3108
|
-
|
|
3109
|
-
"
|
|
3110
|
-
"
|
|
3111
|
-
"
|
|
3112
|
-
"
|
|
3113
|
-
|
|
2932
|
+
// Retired pages (docs/plans/plain-toolroll.md P1.4): permanent redirects, no page handler of their own.
|
|
2933
|
+
"inbox": async (ctx) => retiredRedirect("inbox", ctx.url, ctx.response),
|
|
2934
|
+
"work": async (ctx) => retiredRedirect("work", ctx.url, ctx.response),
|
|
2935
|
+
"next": async (ctx) => retiredRedirect("next", ctx.url, ctx.response),
|
|
2936
|
+
"board": async (ctx) => retiredRedirect("board", ctx.url, ctx.response),
|
|
2937
|
+
// A result keeps its own page; the bare list moved to Tasks' Ready view.
|
|
2938
|
+
"review": async (ctx) => ctx.url.searchParams.has("result") ? showReview(ctx) : retiredRedirect("review", ctx.url, ctx.response),
|
|
2939
|
+
"done": async (ctx) => retiredRedirect("done", ctx.url, ctx.response),
|
|
3114
2940
|
"tasks": showTasks,
|
|
3115
|
-
"queue":
|
|
2941
|
+
"queue": async (ctx) => retiredRedirect("queue", ctx.url, ctx.response),
|
|
2942
|
+
"n.task": showNumberedTask,
|
|
3116
2943
|
"tasks.new": showNewTask,
|
|
3117
2944
|
"task.page": showTask,
|
|
3118
2945
|
"task.evidence": showTaskEvidence,
|
|
@@ -3133,6 +2960,8 @@ export function createTasksHandlers(runtime) {
|
|
|
3133
2960
|
"code.answer-send": codeAct("answer"),
|
|
3134
2961
|
"code.act-other": codeAct("unknown"),
|
|
3135
2962
|
"tasks.add": addTask,
|
|
2963
|
+
"tasks.fast": buildNow,
|
|
2964
|
+
"task.act.keep-grown": keepGrown,
|
|
3136
2965
|
"queue.move": queueMove,
|
|
3137
2966
|
"queue.note": queueNote,
|
|
3138
2967
|
"decision.answer": answerDecision,
|
|
@@ -3155,7 +2984,7 @@ export function createTasksHandlers(runtime) {
|
|
|
3155
2984
|
"task.act.retry-review": taskAct("retry-review"),
|
|
3156
2985
|
"task.act.complete": taskAct("complete"),
|
|
3157
2986
|
"task.act.merge": taskMerge,
|
|
3158
|
-
"task.act.
|
|
2987
|
+
"task.act.recheck-stopped": taskRecheckStopped,
|
|
3159
2988
|
"task.act.stop": taskAct("stop"),
|
|
3160
2989
|
"task.act.stop-carrying": taskAct("stop-carrying"),
|
|
3161
2990
|
"task.act.pause": taskAct("pause"),
|
|
@@ -3194,7 +3023,44 @@ export function createTasksHandlers(runtime) {
|
|
|
3194
3023
|
const startsOn = draft?.values?.get("repo") ?? project ?? chrome.projects?.[0]?.path ?? null;
|
|
3195
3024
|
const found = startsOn === null ? null : runtime.firstTasks.get(startsOn)?.found ?? null;
|
|
3196
3025
|
const ideas = found ?? (runtime.firstRunStepsNow(ctx.now) !== null && !store.hasAnyWork() ? firstTaskSuggestions({ issues: [], todos: [] }) : []);
|
|
3197
|
-
return sendScreen(response, status, newTaskPage(chrome, project, revision, problem, chainable, store.permissionDefault().mode, store.qualityDefault().mode, chrome.projects ?? [], draft, text, ideas));
|
|
3026
|
+
return sendScreen(response, status, newTaskPage(chrome, project, revision, problem, chainable, store.permissionDefault().mode, store.qualityDefault().mode, chrome.projects ?? [], draft, text, ideas, ctx.url.searchParams.get("from")?.slice(0, 300) ?? null));
|
|
3027
|
+
}
|
|
3028
|
+
/** What the password rule reads for this request (approval-password.ts), the same facts the approve forms read. */
|
|
3029
|
+
function fastPasswordFacts(who) {
|
|
3030
|
+
return { passwordless: passwordlessSessionFor(store, who.name), localOwner: who.via === "cookie" && who.authority === "local-owner",
|
|
3031
|
+
freshAt: withFreshness("approve-standard", () => freshnessFor(who.name, Date.now(), true))?.at ?? null, returnTo: "/tasks/new" };
|
|
3032
|
+
}
|
|
3033
|
+
/** Build: the fast start's one write. Every check is made again inside fastStartBuild's transaction. */
|
|
3034
|
+
async function buildNow(ctx) {
|
|
3035
|
+
const { who, response, now, posted } = ctx;
|
|
3036
|
+
const body = readForm(posted, CONSOLE_FORMS.tasksFast);
|
|
3037
|
+
if (who.via !== "cookie")
|
|
3038
|
+
return refuse(response, who, 403, "Only a person starts a build this way.", "/tasks/new");
|
|
3039
|
+
const preview = body.get("preview") ?? "";
|
|
3040
|
+
const row = store.handle.prepare("SELECT repo, title FROM fast_preview WHERE id = ?").get(preview);
|
|
3041
|
+
const title = row === undefined ? "" : String(row["title"]);
|
|
3042
|
+
if (row !== undefined && !visible(String(row["repo"])))
|
|
3043
|
+
return refuse(response, who, 403, "That project is outside your access.", "/projects");
|
|
3044
|
+
const built = spendingOnlyOnSuccess(() => withFreshness("approve-standard", () => fastStartBuild(store, { preview, digest: body.get("digest") ?? "", actor: who.name, via: "console", password: fastPasswordFacts(who),
|
|
3045
|
+
approveNow: (taskId, sawDigest, fast) => approveScope(store, taskId, who.name, now, sawDigest, "", {}, fast) }, now)), result => !result.ok);
|
|
3046
|
+
if (!built.ok)
|
|
3047
|
+
return newTaskScreen(ctx, built.reason === "changed" ? 409 : 403, built.message, { title, goal: "", not: "", touches: "", acceptance: "" });
|
|
3048
|
+
const actionContext = requestContext.getStore();
|
|
3049
|
+
if (actionContext !== undefined)
|
|
3050
|
+
actionContext.createdTask = built.taskId;
|
|
3051
|
+
return redirect(response, taskHref(built.taskId));
|
|
3052
|
+
}
|
|
3053
|
+
/** "Approve it to keep it": a fast start that grew gets a plan for its exact commit, on the ordinary approval. It seals nothing. */
|
|
3054
|
+
async function keepGrown(ctx) {
|
|
3055
|
+
const { url, who, response, now, posted } = ctx;
|
|
3056
|
+
readForm(posted, CONSOLE_FORMS.keepGrown);
|
|
3057
|
+
const act = matchTaskPath(url.pathname, "/keep-grown$");
|
|
3058
|
+
if (act === null)
|
|
3059
|
+
return noPage(who, response);
|
|
3060
|
+
const kept = keepGrownChange(store, act.taskId, now);
|
|
3061
|
+
if (!kept.ok)
|
|
3062
|
+
return taskScreen(response, who, act.taskId, kept.message, 409);
|
|
3063
|
+
return redirect(response, taskHref(act.taskId));
|
|
3198
3064
|
}
|
|
3199
3065
|
return { registrations, runListPane, peekFragment, taskMutation, queueRegionFor, ciFailingFor, completedRowFor, attemptRowFor, resultRowOf, reviewCockpitViewOf, decisionEvidence, runEvidence, sendArtifact, peekGuards, liveRunIds };
|
|
3200
3066
|
}
|
|
@@ -3207,7 +3073,7 @@ export function createTidyHandlers(runtime) {
|
|
|
3207
3073
|
const { url, who, response, now, project } = ctx;
|
|
3208
3074
|
const days = tidyDays(url.searchParams.get("days"));
|
|
3209
3075
|
if (days === null)
|
|
3210
|
-
return refuse(response, who, 400, "Choose a number of days from 1 to 365.", "/
|
|
3076
|
+
return refuse(response, who, 400, "Choose a number of days from 1 to 365.", "/tasks/archive");
|
|
3211
3077
|
const releases = releaseFactsOf(store);
|
|
3212
3078
|
const items = tidyPreview(store, now, { releases, days, access: workAccess() });
|
|
3213
3079
|
return sendScreen(response, 200, putAwayPage(chromeFor(project, "work"), { items, days, digest: tidyDigest(items, days), now,
|
|
@@ -3217,22 +3083,22 @@ export function createTidyHandlers(runtime) {
|
|
|
3217
3083
|
const { who, response, now, posted } = ctx;
|
|
3218
3084
|
const body = readForm(posted, CONSOLE_FORMS.tasksPutAway);
|
|
3219
3085
|
const days = tidyDays(body.get("days"));
|
|
3220
|
-
const back = (problem) => redirect(response, `/
|
|
3086
|
+
const back = (problem) => redirect(response, `/tasks/archive?${new URLSearchParams({ ...(days === null || days === TIDY_DEFAULT_DAYS ? {} : { days: String(days) }), problem })}`);
|
|
3221
3087
|
if (days === null)
|
|
3222
3088
|
return back("Choose a number of days from 1 to 365.");
|
|
3223
3089
|
if (!authenticateApprover(who, body.get("password") ?? "").ok)
|
|
3224
|
-
return back("Enter your Toolroll password to
|
|
3090
|
+
return back("Enter your Toolroll password to archive these.");
|
|
3225
3091
|
const done = applyTidy(store, now, { by: who.name, days, digest: body.get("digest") ?? "", only: body.getAll("task"), releases: releaseFactsOf(store), access: workAccess() });
|
|
3226
3092
|
if (!done.ok)
|
|
3227
3093
|
return back(done.message);
|
|
3228
3094
|
bustBadge();
|
|
3229
|
-
return redirect(response, "/
|
|
3095
|
+
return redirect(response, "/tasks?view=archived");
|
|
3230
3096
|
}
|
|
3231
3097
|
const oneTask = (kind) => async (ctx) => {
|
|
3232
3098
|
const { url, who, response, now, posted } = ctx;
|
|
3233
3099
|
const act = matchTaskPath(url.pathname, `/${kind}$`);
|
|
3234
3100
|
if (act === null)
|
|
3235
|
-
return refuse(response, who, 404, "There's no page at this address.", "/
|
|
3101
|
+
return refuse(response, who, 404, "There's no page at this address.", "/tasks");
|
|
3236
3102
|
const body = readForm(posted, kind === "archive" ? CONSOLE_FORMS.taskArchive : CONSOLE_FORMS.taskRestore);
|
|
3237
3103
|
const standing = (refs) => approverStandsNow(store, who.name, familyRepoOf(store, refs));
|
|
3238
3104
|
const done = kind === "archive"
|
|
@@ -3244,30 +3110,14 @@ export function createTidyHandlers(runtime) {
|
|
|
3244
3110
|
const to = safeReturn(body.get("return"));
|
|
3245
3111
|
return redirect(response, to === "/" ? taskHref(act.taskId) : to);
|
|
3246
3112
|
};
|
|
3247
|
-
return { "
|
|
3113
|
+
return { "tasks.archive": showPutAway, "work.put-away": async (ctx) => retiredRedirect("work.put-away", ctx.url, ctx.response),
|
|
3114
|
+
"work.put-away.apply": applyPutAway, "task.act.archive": oneTask("archive"), "task.act.restore": oneTask("restore") };
|
|
3248
3115
|
}
|
|
3249
3116
|
/** A retired page's GET alias: a plain 302 (the shared 303 helper belongs to POST landings), as pages.ts morningAlias. */
|
|
3250
3117
|
function redirectGet(response, to) {
|
|
3251
3118
|
response.writeHead(302, { ...SAFETY, Location: to });
|
|
3252
3119
|
response.end();
|
|
3253
3120
|
}
|
|
3254
|
-
/**
|
|
3255
|
-
* Where a retired Tasks address goes now (wave A, src/design/KIT.md): Done is Finished. `board` names where the old
|
|
3256
|
-
* Board's state view goes once it is retired (Tasks' Board view; its Order view and its live fragment stay). Null: this
|
|
3257
|
-
* address still answers itself.
|
|
3258
|
-
*/
|
|
3259
|
-
export function retiredTasksAddress(route, url) {
|
|
3260
|
-
// (The Board keeps its own page for now: Tasks' Board view is where the app links; see the wave A report.)
|
|
3261
|
-
const project = url.searchParams.get("project");
|
|
3262
|
-
const keep = project === null || url.searchParams.getAll("project").length !== 1 ? "" : `project=${encodeURIComponent(project)}&`;
|
|
3263
|
-
switch (route) {
|
|
3264
|
-
case "done": return `/work?${keep}view=completed`;
|
|
3265
|
-
case "board":
|
|
3266
|
-
if (url.searchParams.get("view") === "order" || url.searchParams.get("fragment") === "1")
|
|
3267
|
-
return null;
|
|
3268
|
-
return url.searchParams.get("scope") === "all" ? "/work?all=1&layout=board" : `/work?${keep}layout=board`;
|
|
3269
|
-
}
|
|
3270
|
-
}
|
|
3271
3121
|
/** How many cards a Board column shows; the column's count covers them all and its "View all" opens the list. */
|
|
3272
3122
|
export const BOARD_COLUMN_LIMIT = 40;
|
|
3273
3123
|
/**
|
|
@@ -3314,3 +3164,88 @@ export function createAgentSignalHandlers(runtime) {
|
|
|
3314
3164
|
}
|
|
3315
3165
|
return { "agents.seen": agentSeen, "agents.watch": agentWatch };
|
|
3316
3166
|
}
|
|
3167
|
+
/**
|
|
3168
|
+
* One-tap release approval (docs/plans/trusted-release-reports.md, slice A): the sheet (`/t/<id>/release`), the push's
|
|
3169
|
+
* opaque locator (`/approve/<task ref>`, a GET that only redirects to the sheet) and the sheet's approval, which the
|
|
3170
|
+
* task approval POST hands here once its shared checks (cookie session, CSRF and the browser key at admission, the
|
|
3171
|
+
* single-use nonce, a password or a standing proof) passed. The approval itself is the shared card wrapper around the
|
|
3172
|
+
* unchanged approve() (release-approval.ts approveReleaseCard).
|
|
3173
|
+
*/
|
|
3174
|
+
export function createReleaseSheetHandlers(runtime) {
|
|
3175
|
+
const { store, sendScreen, chromeFor, visible, mintApprovalNonce, clock, authenticateApprover } = runtime;
|
|
3176
|
+
const missing = (who, response) => refuse(response, who, 404, "There's no release waiting for approval at this address.", "/work");
|
|
3177
|
+
/** The sheet, with a problem said where it happened. */
|
|
3178
|
+
function sheetScreen(ctx, taskId, problem, status) {
|
|
3179
|
+
const { who, response, project } = ctx;
|
|
3180
|
+
const ref = store.lookupRef(taskId);
|
|
3181
|
+
if (ref === null || !visible(ref.repo) || !store.accountCanAccess(who.name, ref.repo))
|
|
3182
|
+
return missing(who, response);
|
|
3183
|
+
const built = releaseApprovalCard(store, taskId);
|
|
3184
|
+
if (!built.ok)
|
|
3185
|
+
return missing(who, response);
|
|
3186
|
+
const card = built.card;
|
|
3187
|
+
const open = card.approved === null && card.refusal === null;
|
|
3188
|
+
// This computer's local owner can't approve a release: the full sign-in comes first (scope.ts localApprovalEligible).
|
|
3189
|
+
const local = who.via === "cookie" && who.authority === "local-owner" ? localOwnerRefusal(store.accountOf(who.name)?.passwordSet === true, releaseSheetHref(taskId)) : null;
|
|
3190
|
+
const nonce = open && local === null && who.via === "cookie" && who.role === "approver" ? mintApprovalNonce(who.name, taskId, card.digest) : "";
|
|
3191
|
+
const remembered = (() => {
|
|
3192
|
+
if (!open || who.via !== "cookie")
|
|
3193
|
+
return false;
|
|
3194
|
+
const fresh = withFreshness("approve-standard", () => freshnessFor(who.name, Date.now(), true));
|
|
3195
|
+
if (fresh === null)
|
|
3196
|
+
return false;
|
|
3197
|
+
const gate = store.approvalGate(taskId, who.name, "person");
|
|
3198
|
+
return freshlyApprovable(store, taskId, card.scope, gate.verdict === "seal" && !gate.protectedWork);
|
|
3199
|
+
})();
|
|
3200
|
+
return sendScreen(response, status, releaseSheetPage(chromeFor(project ?? ref.repo, "work"), { card, nonce, facts: approvalFactsNow({ remembered }), local, problem, held: who.role === "approver" ? autoPublishHeld(store, taskId) : null }));
|
|
3201
|
+
}
|
|
3202
|
+
async function showSheet(ctx) {
|
|
3203
|
+
const task = matchTaskPath(ctx.url.pathname, "/release$");
|
|
3204
|
+
if (task === null)
|
|
3205
|
+
return missing(ctx.who, ctx.response);
|
|
3206
|
+
return sheetScreen(ctx, task.taskId, null, 200);
|
|
3207
|
+
}
|
|
3208
|
+
/** The push's locator: proved a release in the person's projects from the plane's own state, then only redirected. */
|
|
3209
|
+
async function locate(ctx) {
|
|
3210
|
+
const { who, response, url } = ctx;
|
|
3211
|
+
const raw = /^\/approve\/([1-9][0-9]{0,14})$/.exec(url.pathname)?.[1];
|
|
3212
|
+
const ref = raw === undefined ? null : store.refForId(Number(raw));
|
|
3213
|
+
if (ref === null || ref.backend !== "built-in" || !visible(ref.repo) || !store.accountCanAccess(who.name, ref.repo) || releaseTaskOf(store, ref.externalId) === null)
|
|
3214
|
+
return missing(who, response);
|
|
3215
|
+
return redirect(response, releaseSheetHref(ref.externalId));
|
|
3216
|
+
}
|
|
3217
|
+
/** The sheet's Approve release, after the approval POST's shared checks. */
|
|
3218
|
+
function approveFromSheet(ctx, taskId, cardDigest, token) {
|
|
3219
|
+
const { who, response } = ctx;
|
|
3220
|
+
const now = clock();
|
|
3221
|
+
const answer = spendingOnlyOnSuccess(() => withFreshness("approve-standard", () => approveReleaseCard(store, { taskId, cardDigest, approveNow: scopeDigest => approveScope(store, taskId, who.name, now, scopeDigest, token) })), result => !result.ok && !(result.reason === "approve" && result.result.reason === "second-approver" && !result.result.already));
|
|
3222
|
+
if (answer.ok)
|
|
3223
|
+
return redirect(response, releaseSheetHref(taskId));
|
|
3224
|
+
if (answer.reason !== "approve")
|
|
3225
|
+
return sheetScreen(ctx, taskId, answer.message, answer.reason === "not-release" ? 404 : 409);
|
|
3226
|
+
const refused = answer.result;
|
|
3227
|
+
if (refused.reason === "second-approver")
|
|
3228
|
+
return sheetScreen(ctx, taskId, gateWords({ verdict: "vote", have: refused.have, need: 2, already: refused.already }), 200);
|
|
3229
|
+
if (refused.reason === "policy")
|
|
3230
|
+
return sheetScreen(ctx, taskId, refused.message, 403);
|
|
3231
|
+
return sheetScreen(ctx, taskId, approveRefusalWords(refused.reason), refused.reason === "changed" || refused.reason === "unrouted" ? 409 : 403);
|
|
3232
|
+
}
|
|
3233
|
+
/** A held automatic release goes on: a person only, proved now. The watch then resumes it (release-next.ts). */
|
|
3234
|
+
async function goOn(ctx) {
|
|
3235
|
+
const { who, url, posted, now, response } = ctx;
|
|
3236
|
+
const body = readForm(posted, CONSOLE_FORMS.releaseGoOn);
|
|
3237
|
+
const task = matchTaskPath(url.pathname, "/release/go-on$");
|
|
3238
|
+
if (task === null || who.via !== "cookie")
|
|
3239
|
+
return missing(who, response);
|
|
3240
|
+
const ref = store.lookupRef(task.taskId);
|
|
3241
|
+
if (ref === null || !visible(ref.repo))
|
|
3242
|
+
return missing(who, response);
|
|
3243
|
+
if (!authenticateApprover(who, body.get("password") ?? "", ref.repo).ok)
|
|
3244
|
+
return sheetScreen(ctx, task.taskId, "Enter your Toolroll password to let this release go on.", 403);
|
|
3245
|
+
const allowed = allowAutoPublish(store, { taskId: task.taskId, by: who.name, now });
|
|
3246
|
+
if (!allowed.ok)
|
|
3247
|
+
return sheetScreen(ctx, task.taskId, allowed.why, 409);
|
|
3248
|
+
return redirect(response, releaseSheetHref(task.taskId));
|
|
3249
|
+
}
|
|
3250
|
+
return { routes: { "task.release": showSheet, "task.release.go-on": goOn, "release.locate": locate }, sheetScreen, approveFromSheet };
|
|
3251
|
+
}
|