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/workspace-ui.js
CHANGED
|
@@ -16,22 +16,25 @@
|
|
|
16
16
|
* observed" name observed publication records, and deployment is never
|
|
17
17
|
* claimed without one.
|
|
18
18
|
*/
|
|
19
|
+
import { lastIncludedWords } from './memory-use.js';
|
|
19
20
|
import { ACCEPT_NEEDS_REASON, FAILED_CHECK, cantAcceptYetOf, evidenceProblemOf } from "./result-acts.js";
|
|
20
21
|
export { ACCEPT_NEEDS_REASON, cantAcceptYetOf, evidenceProblemOf };
|
|
21
22
|
import { manualReviewOnly, plainReasonWords } from "./proof.js";
|
|
22
23
|
import { CHECK_COMMAND_CHANGED, CHECK_UNREADABLE, OPEN_RESULT, approvedCheckOf, approvedCheckStateOf, plainReasonOf, stageOfCode, taskStatusOf, workToneOf } from "./task-status.js";
|
|
23
24
|
import { whenUtc } from "./when-html.js";
|
|
25
|
+
import { NEEDS } from "./needs-you.js";
|
|
24
26
|
import { html, postForm } from "./html.js";
|
|
25
27
|
export const WORK_VIEWS = [
|
|
26
28
|
{ key: "all", label: "All", hint: "Every task in view, most urgent first.", empty: "Nothing is in progress. Describe work in chat or add a task, and it appears here." },
|
|
27
29
|
{ key: "needs-you", label: "Needs you", hint: "Tasks waiting on an answer, approval, or your inspection.", empty: "Nothing needs you right now. Queued and running work continues on its own." },
|
|
30
|
+
{ key: "ready", label: "Ready", hint: "Results waiting for you to accept or send back.", empty: "Nothing is ready for review." },
|
|
28
31
|
{ key: "running", label: "Running", hint: "Attempts a builder owns right now.", empty: "Nothing is running right now. Approved tasks start when a builder with capacity is connected." },
|
|
29
32
|
{ key: "queued", label: "Queued", hint: "Tasks that start on their own: waiting for a builder, a dependency or a retry.", empty: "Nothing is queued." },
|
|
30
|
-
{ key: "completed", label: "Finished", hint: "Tasks the lead or
|
|
31
|
-
{ key: "archived", label: "Archived", hint: "Tasks
|
|
33
|
+
{ key: "completed", label: "Finished", hint: "Tasks the lead or you accepted. Recorded check results stay available.", empty: "Nothing has finished in this view yet." },
|
|
34
|
+
{ key: "archived", label: "Archived", hint: "Tasks archived by you or automatically. Their history and results are kept.", empty: "Nothing is archived." },
|
|
32
35
|
];
|
|
33
36
|
export function parseWorkView(raw) {
|
|
34
|
-
return raw === "needs-you" || raw === "running" || raw === "queued" || raw === "completed" || raw === "archived" ? raw : "all";
|
|
37
|
+
return raw === "needs-you" || raw === "ready" || raw === "running" || raw === "queued" || raw === "completed" || raw === "archived" ? raw : "all";
|
|
35
38
|
}
|
|
36
39
|
/** The review facts from the store's bounded retry projection — the same
|
|
37
40
|
* reading `diagnoseTaskDispatch` makes, so every surface starts from one
|
|
@@ -141,8 +144,8 @@ function machineVerdictWords(result, check) {
|
|
|
141
144
|
return `the approved check failed against it${exit === null ? "" : ` (exit ${exit})`}`;
|
|
142
145
|
}
|
|
143
146
|
if (problem === "mismatched")
|
|
144
|
-
return "its
|
|
145
|
-
return "its required
|
|
147
|
+
return "its saved files didn't match the record";
|
|
148
|
+
return "its required files were missing";
|
|
146
149
|
}
|
|
147
150
|
/** The approved check passed (task-status.ts approvedCheckStateOf): its own pass, or checks Off covered by a passing
|
|
148
151
|
* release check. A re-run underway keeps the pass it had. */
|
|
@@ -180,7 +183,7 @@ function storedResultStatusOf(result, publication) {
|
|
|
180
183
|
// Acceptance records a person's decision; it neither proves nor
|
|
181
184
|
// disproves anything about the checks. The machine's own verdict is
|
|
182
185
|
// restated as recorded.
|
|
183
|
-
detail: withPublication(`An approver accepted this result by hand, and
|
|
186
|
+
detail: withPublication(`An approver accepted this result by hand, and their reason says why. The checks are unchanged: ${machineVerdictWords(result, check)}.`),
|
|
184
187
|
tone: "done",
|
|
185
188
|
action: { label: "Open the recorded exception", kind: "open-review" },
|
|
186
189
|
};
|
|
@@ -228,16 +231,16 @@ function storedResultStatusOf(result, publication) {
|
|
|
228
231
|
// at adjudication; only a verdict-less no-change reads its two
|
|
229
232
|
// presence facts here.
|
|
230
233
|
return result.recordComplete === false && result.verdict === null
|
|
231
|
-
? { token: "record-incomplete", label: "No-change result, record incomplete", detail: "The build concluded nothing needed to change, but its handoff or
|
|
232
|
-
: { token: "no-change", label: "No changes were needed", detail: withPublication("The build concluded nothing needed to change; its handoff and
|
|
234
|
+
? { token: "record-incomplete", label: "No-change result, record incomplete", detail: "The build concluded nothing needed to change, but its handoff or saved diff is missing.", tone: "problem", action: { label: "Open the record", kind: "open-run" } }
|
|
235
|
+
: { token: "no-change", label: "No changes were needed", detail: withPublication("The build concluded nothing needed to change; its handoff and saved diff are on record."), tone: "done", action: { label: OPEN_RESULT, kind: "open-result" } };
|
|
233
236
|
}
|
|
234
237
|
if (result.verdict === null) {
|
|
235
238
|
return {
|
|
236
239
|
token: "verification-needed",
|
|
237
|
-
label: "
|
|
238
|
-
detail: withPublication("No
|
|
240
|
+
label: "No check result for this build yet",
|
|
241
|
+
detail: withPublication("No check result for this build yet. Run checks on it."),
|
|
239
242
|
tone: "problem",
|
|
240
|
-
action: { label: "
|
|
243
|
+
action: { label: "Run checks", kind: "open-review" },
|
|
241
244
|
};
|
|
242
245
|
}
|
|
243
246
|
// Any verdict short of a pass the check confirms: never "passed" when the check can't be read now.
|
|
@@ -250,8 +253,8 @@ function storedResultStatusOf(result, publication) {
|
|
|
250
253
|
if (result.verdict === "attested" && !passedNow(check)) {
|
|
251
254
|
return {
|
|
252
255
|
token: "agent-attested",
|
|
253
|
-
label: "
|
|
254
|
-
detail: withPublication("No
|
|
256
|
+
label: "Ready · no project check set up",
|
|
257
|
+
detail: withPublication("No project check is set up, so the checks listed are the agent's own report."),
|
|
255
258
|
tone: "neutral",
|
|
256
259
|
action: { label: OPEN_RESULT, kind: "open-result" },
|
|
257
260
|
};
|
|
@@ -322,11 +325,14 @@ const DISPATCH_ACTION_LABELS = {
|
|
|
322
325
|
"inspect-hold": "Review hold",
|
|
323
326
|
"repair-dependency": "Review required task",
|
|
324
327
|
"repair-capability": "Review missing requirement",
|
|
325
|
-
"start-worker": "
|
|
328
|
+
"start-worker": "Start the builder",
|
|
326
329
|
"retry-review": "Review retry options",
|
|
327
330
|
"resume-run": "Review pause",
|
|
328
331
|
};
|
|
329
332
|
export function dispatchActionLabel(dispatch) {
|
|
333
|
+
// One label everywhere for the builder (needs-you.ts NEEDS): Connect a builder, or Start the builder.
|
|
334
|
+
if (dispatch?.action === "start-worker")
|
|
335
|
+
return dispatch.code === "no-worker-registered" ? NEEDS["connect-builder"].action.label : NEEDS["start-builder"].action.label;
|
|
330
336
|
return dispatch?.action == null ? "View task details" : DISPATCH_ACTION_LABELS[dispatch.action];
|
|
331
337
|
}
|
|
332
338
|
/** Result tokens a done task can wear, as the shared stage they mean. A
|
|
@@ -376,7 +382,7 @@ export function reportMismatchesOf(reasons, criteria, changes) {
|
|
|
376
382
|
return [{ text: plainReasonWords(reason), ...place(review[1]), note: null, reason }];
|
|
377
383
|
const unsigned = UNSIGNED_NOTE.exec(reason);
|
|
378
384
|
if (unsigned !== null)
|
|
379
|
-
return [{ text: `The report's note ${unsigned[1]} is about ${unsigned[2]}, which isn't one of the
|
|
385
|
+
return [{ text: `The report's note ${unsigned[1]} is about ${unsigned[2]}, which isn't one of the approved requirements: ${unsigned[3]}`, path: null, lines: null, inChanges: null, note: Number(unsigned[1]), reason }];
|
|
380
386
|
if (FAILED_CHECK.test(reason))
|
|
381
387
|
return [];
|
|
382
388
|
return [{ text: sentenceOf(plainReasonWords(reason)), path: null, lines: null, inChanges: null, note: note === null ? null : Number(note), reason }];
|
|
@@ -419,7 +425,7 @@ export function lastErrorLineOf(log) {
|
|
|
419
425
|
return (stderr >= 0 ? pick(stderr + 1, lines.length) : null) ?? pick(0, stderr >= 0 ? stderr : lines.length);
|
|
420
426
|
}
|
|
421
427
|
/** The result page's own sentence when the decision is on it. */
|
|
422
|
-
export const RESULT_DECISION_SENTENCE = "Review the change, then accept it or
|
|
428
|
+
export const RESULT_DECISION_SENTENCE = "Review the change, then accept it or send it back.";
|
|
423
429
|
export function acceptWordsOf(facts) {
|
|
424
430
|
const plural = (n, one, many) => `${n} ${n === 1 ? one : many}`;
|
|
425
431
|
const parts = [
|
|
@@ -435,7 +441,7 @@ export function acceptWordsOf(facts) {
|
|
|
435
441
|
: facts.publishing === "pull-request" ? "Finishes the task. No pull request opens; you can open one from the task after."
|
|
436
442
|
: facts.publishing === "off" ? "Finishes the task. The branch stays; publishing isn't set up."
|
|
437
443
|
: "Finishes the task. Nothing is published.";
|
|
438
|
-
return { label: ready ? "Accept
|
|
444
|
+
return { label: ready ? "Accept" : "Accept without checks", ready, why: ready ? null : `${joined.charAt(0).toUpperCase()}${joined.slice(1)}.`, effect };
|
|
439
445
|
}
|
|
440
446
|
export function resultHeadlineOf(status) {
|
|
441
447
|
const read = RESULT_NEEDS_YOU.has(status.token) ? { stage: "needs-you", need: "other" }
|
|
@@ -454,8 +460,8 @@ export function workStatusOf(facts, resultDisplay) {
|
|
|
454
460
|
: stageOfCode(status.token, { needsPerson: status.views.includes("needs-you"), planning: facts.dispatch?.code === "running" && facts.dispatch.role === "planner", operatorHold: facts.dispatch?.action === "unhold" });
|
|
455
461
|
const stage = done && read.stage !== "needs-you" && read.stage !== "failed" ? "finished" : read.stage;
|
|
456
462
|
const level = facts.result?.checkLevel ?? null;
|
|
457
|
-
const shared = taskStatusOf({ stage, ...(read.need === undefined ? {} : { need: read.need }), reason: plainReasonOf(stage, status.token, status.detail),
|
|
458
|
-
report: facts.result?.role === "scout", checks: stage !== "finished" ? null : VERIFIED_RESULT.has(status.token) ? { status: "passed", exitCode: null, head: null, level }
|
|
463
|
+
const shared = taskStatusOf({ stage, ...(read.need === undefined ? {} : { need: read.need }), ...(read.wait === undefined ? {} : { wait: read.wait }), reason: plainReasonOf(stage, status.token, status.detail),
|
|
464
|
+
report: facts.result?.role === "scout", ...(facts.machine === true ? { machine: true } : {}), checks: stage !== "finished" ? null : VERIFIED_RESULT.has(status.token) ? { status: "passed", exitCode: null, head: null, level }
|
|
459
465
|
: level === "off" ? { status: "not-run", exitCode: null, head: null, level } : null });
|
|
460
466
|
return { ...status, label: shared.headline, tone: workToneOf(shared.headline), detail: stage === "finished" && level !== "off" ? status.detail : shared.sentence };
|
|
461
467
|
}
|
|
@@ -486,7 +492,7 @@ function workStatusWordsOf(facts, resultDisplay) {
|
|
|
486
492
|
// The control projection knows whether a stop has actually settled.
|
|
487
493
|
// A still-live claim alone cannot distinguish Running from Stopping.
|
|
488
494
|
if (facts.control?.kind === "stopping") {
|
|
489
|
-
return { token: "stopping", label: "Stopping…", detail: facts.control.detail ?? "The attempt is ending. Its work is preserved; resume becomes available after its processes exit.", tone: "attention", action: { label: "View stop details", kind: "open-task" }, views, rank:
|
|
495
|
+
return { token: "stopping", label: "Stopping…", detail: facts.control.detail ?? "The attempt is ending. Its work is preserved; resume becomes available after its processes exit.", tone: "attention", action: { label: "View stop details", kind: "open-task" }, views, rank: 1 };
|
|
490
496
|
}
|
|
491
497
|
if (facts.control?.kind === "paused") {
|
|
492
498
|
return { token: "stopped", label: "Paused", detail: "The stopped attempt's work is preserved. Resuming requires confirmation and checks the current approval again.", tone: "attention", action: { label: "Review pause", kind: "open-task" }, views, rank: 0 };
|
|
@@ -529,7 +535,7 @@ function workStatusWordsOf(facts, resultDisplay) {
|
|
|
529
535
|
}
|
|
530
536
|
/** The counts each view tab wears — from the same rows the page lists. */
|
|
531
537
|
export function workCounts(rows) {
|
|
532
|
-
const counts = { all: 0, "needs-you": 0, running: 0, queued: 0, completed: 0, archived: 0 };
|
|
538
|
+
const counts = { all: 0, "needs-you": 0, ready: 0, running: 0, queued: 0, completed: 0, archived: 0 };
|
|
533
539
|
for (const row of rows)
|
|
534
540
|
for (const view of row.views)
|
|
535
541
|
counts[view] += 1;
|
|
@@ -557,7 +563,7 @@ export function learningHtml(view, csrf, canManage, source) {
|
|
|
557
563
|
const act = (action, label, lesson) => canManage && csrf ? postForm('/settings/learning/change', html `<button type="submit">${label}</button>`, { hidden: { ...base, action, ...(lesson ? { lesson: lesson.id, version: lesson.version, sha: lesson.sha } : {}) } }) : '';
|
|
558
564
|
const evidence = (list, run) => list.map(one => html `<li><a href="/r/${String(one.runId ?? run)}/evidence/${one.artifactId}">Source #${one.artifactId}</a><blockquote>${one.excerpt}</blockquote><code>${one.sha256}</code></li>`);
|
|
559
565
|
const lessons = view.lessons.filter(l => source === undefined || l.source === source);
|
|
560
|
-
const cards = lessons.map(l => html `<article class="card" data-lesson="${l.id}"><p><strong>${l.payload.observation}</strong></p><p>${l.payload.action}</p><p class="meta">${l.payload.kind === 'system' ? 'System suggestion · No change applied' : l.status === 'adopted' ? 'Adopted advice' : l.status === 'disabled' ? 'Disabled' : 'Proposed lesson'}</p><details><summary>Source and use</summary><p><a href="/r/${l.source}">Result #${l.source}</a> · <a href="/r/${l.reviewer}">Review #${l.reviewer}</a></p><p>${l.payload.paths.join(', ')} · ${l.payload.phases.join(', ')} · ${{ darwin: "macOS", win32: "Windows", linux: "Linux" }[l.payload.platform] ?? l.payload.platform}</p><ul>${evidence(l.payload.evidence, l.source)}</ul>${l.payload.kind !== 'project' ? '' : html `<p class="meta">${l.status === 'proposed' ? 'Save permits advisory reuse when enabled and applicable. It does not prove a remedy works.' : 'Disabling affects future runs. Active snapshots, code and approvals stay unchanged.'}</p>${l.status === 'proposed' ? act('adopt', 'Save lesson', l) : ''}`}</details>${l.payload.kind === 'project' && l.status === 'adopted' ? act('disable', 'Disable lesson', l) : ''}</article>`);
|
|
566
|
+
const cards = lessons.map(l => html `<article class="card" data-lesson="${l.id}"><p><strong>${l.payload.observation}</strong></p><p>${l.payload.action}</p><p class="meta">${lastIncludedWords(view.use.get(`lesson:${l.id}`))}</p><p class="meta">${l.payload.kind === 'system' ? 'System suggestion · No change applied' : l.status === 'adopted' ? 'Adopted advice' : l.status === 'disabled' ? 'Disabled' : 'Proposed lesson'}</p><details><summary>Source and use</summary><p><a href="/r/${l.source}">Result #${l.source}</a> · <a href="/r/${l.reviewer}">Review #${l.reviewer}</a></p><p>${l.payload.paths.join(', ')} · ${l.payload.phases.join(', ')} · ${{ darwin: "macOS", win32: "Windows", linux: "Linux" }[l.payload.platform] ?? l.payload.platform}</p><ul>${evidence(l.payload.evidence, l.source)}</ul>${l.payload.kind !== 'project' ? '' : html `<p class="meta">${l.status === 'proposed' ? 'Save permits advisory reuse when enabled and applicable. It does not prove a remedy works.' : 'Disabling affects future runs. Active snapshots, code and approvals stay unchanged.'}</p>${l.status === 'proposed' ? act('adopt', 'Save lesson', l) : ''}`}</details>${l.payload.kind === 'project' && l.status === 'adopted' ? act('disable', 'Disable lesson', l) : ''}</article>`);
|
|
561
567
|
if (source !== undefined)
|
|
562
568
|
return lessons.length ? html `<details class="learning result-learning"><summary>Learned from this task</summary>${cards}<a href="/settings/learning?repo=${encodeURIComponent(view.repo)}">Learning settings</a></details>` : html ``;
|
|
563
569
|
const history = view.events.map(ev => html `<article class="card" data-learning-event="${ev.action}"><p><strong>${{ assessment: { propose: 'Learning suggested', none: 'No lesson needed', unassessed: 'Learning not assessed', invalid: 'Learning assessment invalid' }[ev.after] ?? 'Learning not assessed', proposal: 'Suggestion recorded', adopt: 'Lesson adopted', disable: 'Lesson disabled', reset: 'Learning reset', enable: 'Reuse enabled', pause: 'Reuse paused', reuse: 'Run context saved', failure: 'Learning issue', capture: 'Capture finished' }[ev.action] ?? ev.action}</strong>${ev.action === "reuse" ? ` · ${ev.after}` : ""}</p><p class="meta">${whenUtc(ev.at)} · ${ev.actor}${ev.run === null ? '' : html ` · <a href="/r/${ev.run}">Run #${ev.run}</a>${ev.outcome === null ? "" : ` · ${ev.outcome}`}`}</p><details><summary>Details</summary><p>${ev.before} → ${ev.after}</p><p>${ev.reason}</p>${ev.lesson === null ? '' : html `<p>Lesson #${ev.lesson}</p>`}<ul>${evidence(ev.evidence, ev.run)}</ul>${ev.snapshot === null ? "" : html `<details><summary>Exact context</summary><pre>${ev.snapshot}</pre></details>`}</details></article>`);
|
package/dist/worktree.d.ts
CHANGED
|
@@ -96,7 +96,7 @@ export type LeaseResult = {
|
|
|
96
96
|
};
|
|
97
97
|
export type LeaseFailure = "held" | "dirty" | "git" | "unverified" | "unknown-runner" | "in-use";
|
|
98
98
|
/** Why storage retention left a checkout in place. */
|
|
99
|
-
export type KeptWhy = "has changes" | "has commits" | "unreadable" | "git refused";
|
|
99
|
+
export type KeptWhy = "has changes" | "has commits" | "not on its branch" | "unreadable" | "git refused";
|
|
100
100
|
/**
|
|
101
101
|
* The note a lease leaves in the checkout, naming the process holding it.
|
|
102
102
|
*
|
|
@@ -291,15 +291,24 @@ export declare class WorktreePool {
|
|
|
291
291
|
* or remote. Reads only, through `git` when given (teardown's hardened git).
|
|
292
292
|
*/
|
|
293
293
|
inspect(path: string, git?: Git): Promise<"clean" | KeptWhy>;
|
|
294
|
+
/**
|
|
295
|
+
* Whether a checkout a task may still come back to can go, beyond `inspect`: a later lease of its branch must make the
|
|
296
|
+
* same checkout, and every commit its task recorded must outlive it (git's garbage collection counts a worktree's HEAD
|
|
297
|
+
* and reflog; once they go only a branch holds a commit). So: the folder is the pool's own, the checkout belongs to its
|
|
298
|
+
* project by the plane's record (`boundDirs`, not its `.git` file), its HEAD is its recorded branch, every recorded
|
|
299
|
+
* `heads` commit is on a branch, and no ref lives only in this worktree. Reads only; anything unreadable keeps it.
|
|
300
|
+
*/
|
|
301
|
+
proveRecreatable(row: WorktreeRow, heads: readonly string[]): Promise<"clean" | KeptWhy>;
|
|
294
302
|
/**
|
|
295
303
|
* Keep storage in check: remove the working copies nobody needs any more. Of `rows`, a checkout goes when it is
|
|
296
304
|
* let go, nothing holds it, `wanted` says it may go (asked again right before it goes, so a task coming back or a
|
|
297
305
|
* lease since keeps it) and `inspect` finds it clean, then clean still right before it goes (git refuses an edit
|
|
298
306
|
* made after that). Only the working copy goes: its branch, and so every commit,
|
|
299
307
|
* stays, and a later lease of the same branch makes a new one. At most `max` go per pass. Every git command goes
|
|
300
|
-
* through `git` when given: landed cleanup passes its hardened teardown git (teardown-git.ts).
|
|
308
|
+
* through `git` when given: landed cleanup passes its hardened teardown git (teardown-git.ts). `proof` is asked last,
|
|
309
|
+
* inside the reservation, for what `inspect` doesn't cover (idle cleanup: `proveRecreatable`).
|
|
301
310
|
*/
|
|
302
|
-
prune(rows: readonly WorktreeRow[], wanted: (row: WorktreeRow, fresh: boolean) => boolean | Promise<boolean>, max?: number, git?: Git): Promise<{
|
|
311
|
+
prune(rows: readonly WorktreeRow[], wanted: (row: WorktreeRow, fresh: boolean) => boolean | Promise<boolean>, max?: number, git?: Git, proof?: (row: WorktreeRow) => Promise<"clean" | KeptWhy>): Promise<{
|
|
303
312
|
removed: WorktreeRow[];
|
|
304
313
|
kept: {
|
|
305
314
|
path: string;
|
package/dist/worktree.js
CHANGED
|
@@ -28,6 +28,7 @@ import { hostname } from "node:os";
|
|
|
28
28
|
import { basename, dirname, join, relative, resolve, sep } from "node:path";
|
|
29
29
|
import { run } from "./exec.js";
|
|
30
30
|
import { currentBootId, provenDeadByBootChange } from "./boot-identity.js";
|
|
31
|
+
import { currentMachineId, isThisMachine } from "./machine-identity.js";
|
|
31
32
|
import { HANDOFF_PREFIX, MAILBOX_SUFFIX, looksLikeProtocolFile, readMailbox } from "./evidence.js";
|
|
32
33
|
import { parseHandoff } from "./decision.js";
|
|
33
34
|
import { diskBytes } from "./storage.js";
|
|
@@ -55,7 +56,8 @@ const reservationPath = (path) => `${path}.occupancy`;
|
|
|
55
56
|
function reservation(path) {
|
|
56
57
|
try {
|
|
57
58
|
const note = JSON.parse(readFileSync(reservationPath(path), "utf8"));
|
|
58
|
-
if (Number.isSafeInteger(note.pid) && note.pid > 0 && typeof note.host === "string" && typeof note.token === "string"
|
|
59
|
+
if (Number.isSafeInteger(note.pid) && note.pid > 0 && typeof note.host === "string" && typeof note.token === "string"
|
|
60
|
+
&& (note.machineId === undefined || note.machineId === null || typeof note.machineId === "string"))
|
|
59
61
|
return note;
|
|
60
62
|
}
|
|
61
63
|
catch (error) {
|
|
@@ -66,9 +68,9 @@ function reservation(path) {
|
|
|
66
68
|
return { pid: 0, host: "unknown", bootId: null, token: "unknown" };
|
|
67
69
|
}
|
|
68
70
|
function reservationAlive(note) {
|
|
69
|
-
if (note.pid === 0 || note
|
|
71
|
+
if (note.pid === 0 || !isThisMachine(note))
|
|
70
72
|
return true;
|
|
71
|
-
if (note.bootId !== null && provenDeadByBootChange(note))
|
|
73
|
+
if (note.bootId !== null && provenDeadByBootChange(note, { host: note.host, bootId: currentBootId() }))
|
|
72
74
|
return false;
|
|
73
75
|
try {
|
|
74
76
|
process.kill(note.pid, 0);
|
|
@@ -81,7 +83,7 @@ function reservationAlive(note) {
|
|
|
81
83
|
/** A short store transaction arbitrates marker creation/recovery, never git, a scan or deletion.
|
|
82
84
|
* Atomic rename publishes a complete owner; a crash cannot leave a half-written reservation. */
|
|
83
85
|
function reserveCheckout(store, path) {
|
|
84
|
-
const note = { pid: process.pid, host: hostname(), bootId: currentBootId(), token: randomBytes(12).toString("hex") };
|
|
86
|
+
const note = { pid: process.pid, host: hostname(), bootId: currentBootId(), token: randomBytes(12).toString("hex"), machineId: currentMachineId() };
|
|
85
87
|
const file = reservationPath(path), temp = `${file}.${note.token}`;
|
|
86
88
|
const held = store.transact(() => {
|
|
87
89
|
const prior = reservation(path);
|
|
@@ -155,6 +157,18 @@ function safePoolPath(root, target) {
|
|
|
155
157
|
}
|
|
156
158
|
return true;
|
|
157
159
|
}
|
|
160
|
+
/** The checkout itself, strictly inside the pool as real paths, reached by real folders only: never a link out of it. */
|
|
161
|
+
function insidePool(root, path) {
|
|
162
|
+
if (!safePoolPath(root, path))
|
|
163
|
+
return false;
|
|
164
|
+
try {
|
|
165
|
+
const real = realpathSync(path), base = realpathSync(root);
|
|
166
|
+
return real !== base && real.startsWith(base + sep) && lstatSync(path).isDirectory();
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
158
172
|
function normalisePath(path) {
|
|
159
173
|
return path.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
160
174
|
}
|
|
@@ -178,12 +192,12 @@ export function recordWorktreeProcess(store, path, runner, pid, leaseEpoch) {
|
|
|
178
192
|
// verified boot change nothing of it survives, and the checkout is free
|
|
179
193
|
// without a PID probe that a reused pid could answer wrongly.
|
|
180
194
|
const bootId = currentBootId();
|
|
181
|
-
writeFileSync(join(path, MARKER), `${pid} ${runner} group ${bootId ?? "unknown"} ${hostname()}\n`, "utf8");
|
|
195
|
+
writeFileSync(join(path, MARKER), `${pid} ${runner} group ${bootId ?? "unknown"} ${hostname()} ${currentMachineId() ?? "unknown"}\n`, "utf8");
|
|
182
196
|
}
|
|
183
197
|
/** Read-only occupancy witness shared with stop settlement. */
|
|
184
198
|
export function worktreeProcessOccupancy(path) {
|
|
185
199
|
const reserved = reservation(path);
|
|
186
|
-
if (reserved !== null && (reserved.pid !== process.pid || reserved
|
|
200
|
+
if (reserved !== null && (reserved.pid !== process.pid || !isThisMachine(reserved)) && reservationAlive(reserved))
|
|
187
201
|
return { held: true, by: reserved.pid };
|
|
188
202
|
const note = join(path, MARKER);
|
|
189
203
|
if (!existsSync(note))
|
|
@@ -193,7 +207,9 @@ export function worktreeProcessOccupancy(path) {
|
|
|
193
207
|
if (!Number.isInteger(pid) || pid <= 0)
|
|
194
208
|
return { held: false };
|
|
195
209
|
const noteHost = parts[4];
|
|
196
|
-
|
|
210
|
+
// The sixth word (v150) is the writer's machine id; "unknown", or a note from before it, is matched by name.
|
|
211
|
+
const noteMachine = parts[5] === undefined || parts[5] === "unknown" ? null : parts[5];
|
|
212
|
+
if (noteHost !== undefined && !isThisMachine({ host: noteHost, machineId: noteMachine }))
|
|
197
213
|
return { held: true, by: pid };
|
|
198
214
|
if (pid === process.pid && parts[2] !== "group")
|
|
199
215
|
return { held: false };
|
|
@@ -201,7 +217,7 @@ export function worktreeProcessOccupancy(path) {
|
|
|
201
217
|
// that cannot exist any more; a note without a boot id (legacy) or a
|
|
202
218
|
// boot this host cannot verify keeps the probe below.
|
|
203
219
|
const noteBoot = parts[3];
|
|
204
|
-
if (noteBoot !== undefined && noteHost !== undefined && provenDeadByBootChange({ host: noteHost, bootId: noteBoot }))
|
|
220
|
+
if (noteBoot !== undefined && noteHost !== undefined && provenDeadByBootChange({ host: noteHost, bootId: noteBoot }, { host: noteHost, bootId: currentBootId() }))
|
|
205
221
|
return { held: false };
|
|
206
222
|
// A shell/provider can exit before its descendants. A recorded POSIX
|
|
207
223
|
// process group remains an owner until that whole group has gone.
|
|
@@ -650,7 +666,7 @@ export class WorktreePool {
|
|
|
650
666
|
/** Leave a note naming the process holding this checkout. */
|
|
651
667
|
mark(path, runner, pid = process.pid) {
|
|
652
668
|
try {
|
|
653
|
-
writeFileSync(join(path, MARKER), `${pid} ${runner} process ${currentBootId() ?? "unknown"} ${hostname()}\n`, "utf8");
|
|
669
|
+
writeFileSync(join(path, MARKER), `${pid} ${runner} process ${currentBootId() ?? "unknown"} ${hostname()} ${currentMachineId() ?? "unknown"}\n`, "utf8");
|
|
654
670
|
return true;
|
|
655
671
|
}
|
|
656
672
|
catch {
|
|
@@ -846,15 +862,49 @@ export class WorktreePool {
|
|
|
846
862
|
return "unreadable";
|
|
847
863
|
return own.stdout.trim() === "" ? "clean" : "has commits";
|
|
848
864
|
}
|
|
865
|
+
/**
|
|
866
|
+
* Whether a checkout a task may still come back to can go, beyond `inspect`: a later lease of its branch must make the
|
|
867
|
+
* same checkout, and every commit its task recorded must outlive it (git's garbage collection counts a worktree's HEAD
|
|
868
|
+
* and reflog; once they go only a branch holds a commit). So: the folder is the pool's own, the checkout belongs to its
|
|
869
|
+
* project by the plane's record (`boundDirs`, not its `.git` file), its HEAD is its recorded branch, every recorded
|
|
870
|
+
* `heads` commit is on a branch, and no ref lives only in this worktree. Reads only; anything unreadable keeps it.
|
|
871
|
+
*/
|
|
872
|
+
async proveRecreatable(row, heads) {
|
|
873
|
+
if (row.branch === "unknown" || !insidePool(this.options.root, row.path) || heads.some(one => !/^[0-9a-f]{40,64}$/.test(one)))
|
|
874
|
+
return "unreadable";
|
|
875
|
+
const git = teardownGit(this.runner, { repo: row.repo });
|
|
876
|
+
const head = await git(row.path, [...READ_ONLY, "symbolic-ref", "-q", "HEAD"]);
|
|
877
|
+
if (head.code === 1)
|
|
878
|
+
return "not on its branch";
|
|
879
|
+
if (head.code !== 0)
|
|
880
|
+
return "unreadable";
|
|
881
|
+
if (head.stdout.trim() !== `refs/heads/${row.branch}`)
|
|
882
|
+
return "not on its branch";
|
|
883
|
+
const branch = await git(row.repo, [...READ_ONLY, "rev-parse", "--verify", "--quiet", `refs/heads/${row.branch}^{commit}`]);
|
|
884
|
+
if (branch.code !== 0)
|
|
885
|
+
return "unreadable";
|
|
886
|
+
if (heads.length > 0) {
|
|
887
|
+
const unheld = await git(row.repo, [...READ_ONLY, "rev-list", "-1", ...heads, "--not", "--branches"]);
|
|
888
|
+
if (unheld.code !== 0)
|
|
889
|
+
return "unreadable";
|
|
890
|
+
if (unheld.stdout.trim() !== "")
|
|
891
|
+
return "has commits";
|
|
892
|
+
}
|
|
893
|
+
const own = await git(row.path, [...READ_ONLY, "for-each-ref", "--count=1", "refs/worktree", "refs/bisect", "refs/rewritten"]);
|
|
894
|
+
if (own.code !== 0)
|
|
895
|
+
return "unreadable";
|
|
896
|
+
return own.stdout.trim() === "" ? "clean" : "has commits";
|
|
897
|
+
}
|
|
849
898
|
/**
|
|
850
899
|
* Keep storage in check: remove the working copies nobody needs any more. Of `rows`, a checkout goes when it is
|
|
851
900
|
* let go, nothing holds it, `wanted` says it may go (asked again right before it goes, so a task coming back or a
|
|
852
901
|
* lease since keeps it) and `inspect` finds it clean, then clean still right before it goes (git refuses an edit
|
|
853
902
|
* made after that). Only the working copy goes: its branch, and so every commit,
|
|
854
903
|
* stays, and a later lease of the same branch makes a new one. At most `max` go per pass. Every git command goes
|
|
855
|
-
* through `git` when given: landed cleanup passes its hardened teardown git (teardown-git.ts).
|
|
904
|
+
* through `git` when given: landed cleanup passes its hardened teardown git (teardown-git.ts). `proof` is asked last,
|
|
905
|
+
* inside the reservation, for what `inspect` doesn't cover (idle cleanup: `proveRecreatable`).
|
|
856
906
|
*/
|
|
857
|
-
async prune(rows, wanted, max = Infinity, git = this.teardown) {
|
|
907
|
+
async prune(rows, wanted, max = Infinity, git = this.teardown, proof) {
|
|
858
908
|
const removed = [];
|
|
859
909
|
const kept = [];
|
|
860
910
|
const idle = (row, releasedAt) => row !== null && row.runner === null && row.releasedAt === releasedAt && !this.inUse(row.path).held;
|
|
@@ -888,6 +938,11 @@ export class WorktreePool {
|
|
|
888
938
|
const allowed = wanted(now, true);
|
|
889
939
|
if (!(typeof allowed === "boolean" ? allowed : await allowed))
|
|
890
940
|
continue;
|
|
941
|
+
const proven = proof === undefined ? "clean" : await proof(now);
|
|
942
|
+
if (proven !== "clean") {
|
|
943
|
+
kept.push({ path: row.path, why: proven });
|
|
944
|
+
continue;
|
|
945
|
+
}
|
|
891
946
|
const gone = await this.removeCleanCopy(row, git);
|
|
892
947
|
if (gone !== "removed") {
|
|
893
948
|
kept.push({ path: row.path, why: gone });
|
|
@@ -931,7 +986,7 @@ export class WorktreePool {
|
|
|
931
986
|
return { ok: false, message: `${row.path} is in use` };
|
|
932
987
|
// Held while it slims: another process's lease sees this process in it.
|
|
933
988
|
try {
|
|
934
|
-
writeFileSync(note, `${process.pid} storage-retention slimming ${currentBootId() ?? "unknown"} ${hostname()}\n`, "utf8");
|
|
989
|
+
writeFileSync(note, `${process.pid} storage-retention slimming ${currentBootId() ?? "unknown"} ${hostname()} ${currentMachineId() ?? "unknown"}\n`, "utf8");
|
|
935
990
|
held = true;
|
|
936
991
|
}
|
|
937
992
|
catch {
|
|
@@ -989,7 +1044,7 @@ export class WorktreePool {
|
|
|
989
1044
|
// Held while it goes: another process's lease sees this process in it (the busy map is this process's only).
|
|
990
1045
|
const note = join(row.path, MARKER);
|
|
991
1046
|
try {
|
|
992
|
-
writeFileSync(note, `${process.pid} storage-retention removing ${currentBootId() ?? "unknown"} ${hostname()}\n`, "utf8");
|
|
1047
|
+
writeFileSync(note, `${process.pid} storage-retention removing ${currentBootId() ?? "unknown"} ${hostname()} ${currentMachineId() ?? "unknown"}\n`, "utf8");
|
|
993
1048
|
}
|
|
994
1049
|
catch {
|
|
995
1050
|
return false;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "toolroll",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.94",
|
|
4
4
|
"description": "A control plane for unattended coding agents — queue tasks, walk away, come back to pull requests. Agents build in leased worktrees, never touch your default branch, and interrupt you only for decisions that need a human.",
|
|
5
5
|
"directories": {
|
|
6
6
|
"doc": "docs"
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* /next (Wave E, src/design/KIT.md): everything waiting on a person, one at a time, each in the approval slots with
|
|
3
|
-
* its one act, every act landing back here. The forms post exactly what they always have (the approve form's nonce,
|
|
4
|
-
* digest and password; a decision's choice, confirm and note; a requeue), so every server check is unchanged. The
|
|
5
|
-
* approval shows the password only when the rules ask (approveForm, standard): none with Passwordless or a remembered
|
|
6
|
-
* password the task page's own preview says may approve these exact terms; otherwise the password, required.
|
|
7
|
-
*/
|
|
8
|
-
import { type TaskOrigin } from "../task-source.js";
|
|
9
|
-
import type { Gap } from "../gaps.js";
|
|
10
|
-
import type { Scope } from "../scope.js";
|
|
11
|
-
import type { Decision } from "../store.js";
|
|
12
|
-
import { type Chrome, type Screen } from "./chrome.js";
|
|
13
|
-
import { type PlanContractView, type RouteView } from "./render-tasks.js";
|
|
14
|
-
export type NextItem = {
|
|
15
|
-
key: string;
|
|
16
|
-
kind: "decision";
|
|
17
|
-
decision: Decision & {
|
|
18
|
-
taskId: string;
|
|
19
|
-
};
|
|
20
|
-
} | {
|
|
21
|
-
key: string;
|
|
22
|
-
kind: "approval";
|
|
23
|
-
approval: {
|
|
24
|
-
taskId: string;
|
|
25
|
-
title: string;
|
|
26
|
-
goal: string;
|
|
27
|
-
digest: string;
|
|
28
|
-
proposedAt: string;
|
|
29
|
-
};
|
|
30
|
-
} | {
|
|
31
|
-
key: string;
|
|
32
|
-
kind: "requeue";
|
|
33
|
-
stalled: {
|
|
34
|
-
taskId: string;
|
|
35
|
-
title: string;
|
|
36
|
-
strikes: number;
|
|
37
|
-
incidentCount: number;
|
|
38
|
-
};
|
|
39
|
-
} | {
|
|
40
|
-
key: string;
|
|
41
|
-
kind: "gap";
|
|
42
|
-
gap: Gap;
|
|
43
|
-
} | null;
|
|
44
|
-
export declare function nextPage(chrome: Chrome, data: {
|
|
45
|
-
item: NextItem;
|
|
46
|
-
scope: Scope | null;
|
|
47
|
-
planDocument: string | null;
|
|
48
|
-
planContract?: PlanContractView | null;
|
|
49
|
-
approvalDigest: string | null;
|
|
50
|
-
/** v34: said inside the ceremony when the yes buys a report, not a branch. */
|
|
51
|
-
deliverable?: "branch" | "report";
|
|
52
|
-
/** v48: the agents the yes freezes — the one block every ceremony shows. */
|
|
53
|
-
route: RouteView | null;
|
|
54
|
-
/** v126: where the work came from (an outside event's line of context, never a term agreed to). */
|
|
55
|
-
origin?: TaskOrigin | null;
|
|
56
|
-
/** A remembered password stands in for these exact terms (the task page's preview): one click, no field. */
|
|
57
|
-
remembered?: boolean;
|
|
58
|
-
csrf: string;
|
|
59
|
-
nonce: string;
|
|
60
|
-
remaining: number;
|
|
61
|
-
skipped: string[];
|
|
62
|
-
now: Date;
|
|
63
|
-
}): Screen;
|
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* /next (Wave E, src/design/KIT.md): everything waiting on a person, one at a time, each in the approval slots with
|
|
3
|
-
* its one act, every act landing back here. The forms post exactly what they always have (the approve form's nonce,
|
|
4
|
-
* digest and password; a decision's choice, confirm and note; a requeue), so every server check is unchanged. The
|
|
5
|
-
* approval shows the password only when the rules ask (approveForm, standard): none with Passwordless or a remembered
|
|
6
|
-
* password the task page's own preview says may approve these exact terms; otherwise the password, required.
|
|
7
|
-
*/
|
|
8
|
-
import { originNote } from "../task-source.js";
|
|
9
|
-
import { html, joinHtml, postForm } from "../html.js";
|
|
10
|
-
import { screen, when } from "./chrome.js";
|
|
11
|
-
import { taskHref } from "./http.js";
|
|
12
|
-
import { acceptanceCeremonyHtml, agentsCeremonyHtml, consentClosedHtml, consentDoorOf, executionPlanHtml, planContractHtml, profileWords, runtimeDetailsHtml, } from "./render-tasks.js";
|
|
13
|
-
import { approvalFactsNow, approvalTerms, approveForm, btn, chip, disclose, doc, docHeader, empty, facts, note, phoneHeader, slots } from "./kit.js";
|
|
14
|
-
/** One decision option: a one-click answer (an irreversible one behind its own disclosure, with the confirm the server
|
|
15
|
-
* requires) and its consequence. A note travels from the task's own page. */
|
|
16
|
-
function optionForm(decision, option) {
|
|
17
|
-
const recommended = option.id === decision.recommendation;
|
|
18
|
-
const form = postForm(`/d/${decision.id}/answer`, html `<div class="k-acts">${btn(option.label, { variant: option.reversible ? (recommended ? "primary" : "outline") : "danger", type: "submit" })}</div>\
|
|
19
|
-
<p class="k-note">${recommended ? html `${chip("Recommended", "", { dot: false })} ` : ""}${option.consequence}</p>`, { attrs: { class: "k-form" }, returnTo: "next", hidden: { choice: option.id, confirm: option.reversible ? null : "yes" } });
|
|
20
|
-
return option.reversible ? form : disclose(`${option.label} (can't be undone)`, form);
|
|
21
|
-
}
|
|
22
|
-
export function nextPage(chrome, data) {
|
|
23
|
-
const { item } = data;
|
|
24
|
-
const title = "Needs you";
|
|
25
|
-
if (item === null) {
|
|
26
|
-
const held = data.skipped.length;
|
|
27
|
-
return screen(title, html `${phoneHeader(title)}${doc(docHeader({ title, same: true }), held > 0
|
|
28
|
-
? empty({ title: `Only the ${held} you set aside ${held === 1 ? "is" : "are"} left.`, text: "Look at them again now, or come back later.", action: btn("Look at them again", { sm: true, href: "/next" }) })
|
|
29
|
-
: empty({ title: "Nothing needs you.", text: "Everything is either working or waiting on its own clock.", action: btn("Open Tasks", { sm: true, href: "/work" }) }))}`, { chrome });
|
|
30
|
-
}
|
|
31
|
-
const skipHref = `/next?skip=${encodeURIComponent([...data.skipped, item.key].join(","))}`;
|
|
32
|
-
const header = docHeader({
|
|
33
|
-
title, same: true,
|
|
34
|
-
meta: data.remaining === 1 ? "The last thing waiting on you" : `1 of ${data.remaining} waiting on you`,
|
|
35
|
-
right: btn("Not now", { variant: "ghost", sm: true, href: skipHref }),
|
|
36
|
-
});
|
|
37
|
-
const openTask = (taskId, words = "Open the task") => html `<a class="k-link" href="${taskHref(taskId)}">${words}</a>`;
|
|
38
|
-
let card;
|
|
39
|
-
if (item.kind === "decision") {
|
|
40
|
-
const { decision } = item;
|
|
41
|
-
card = slots({
|
|
42
|
-
wait: true, attrs: { "data-decision": String(decision.id) },
|
|
43
|
-
status: html `${chip("Question", "ask")}<h2>${decision.question}</h2>`,
|
|
44
|
-
text: decision.recap,
|
|
45
|
-
acts: joinHtml(decision.options.map(option => optionForm(decision, option))),
|
|
46
|
-
terms: html `${decision.taskId} · asked ${when(decision.createdAt)} · ${openTask(decision.taskId, "Answer with a note on the task")}`,
|
|
47
|
-
});
|
|
48
|
-
}
|
|
49
|
-
else if (item.kind === "approval" && !consentDoorOf(data.scope, data.route).open) {
|
|
50
|
-
const door = consentDoorOf(data.scope, data.route);
|
|
51
|
-
card = slots({
|
|
52
|
-
wait: true,
|
|
53
|
-
status: html `${chip("Plan", "ask")}<h2>${item.approval.title}</h2>`,
|
|
54
|
-
text: item.approval.taskId,
|
|
55
|
-
acts: consentClosedHtml(item.approval.taskId, door, "next"),
|
|
56
|
-
terms: openTask(item.approval.taskId),
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
|
-
else if (item.kind === "approval") {
|
|
60
|
-
const scope = data.scope;
|
|
61
|
-
const facts_ = approvalFactsNow({ remembered: data.remembered === true });
|
|
62
|
-
const terms = joinHtml([
|
|
63
|
-
data.deliverable === "report" ? note("Research only: a read-only session investigates this goal and delivers a report. No branch; nothing in the repository changes.") : "",
|
|
64
|
-
scope === null || scope.profileState !== "unresolved" ? "" : profileWords(scope),
|
|
65
|
-
originNote(data.origin) === null ? "" : html `<p class="k-note" data-approval-origin>${originNote(data.origin)}</p>`,
|
|
66
|
-
facts([
|
|
67
|
-
["Goal", scope?.goal ?? item.approval.goal],
|
|
68
|
-
["Not this", scope?.outOfScope == null ? "No exclusions" : scope.outOfScope],
|
|
69
|
-
["Touches", scope === null || scope.touches.length === 0 ? "Anything" : scope.touches.join(", ")],
|
|
70
|
-
]),
|
|
71
|
-
scope === null ? "" : acceptanceCeremonyHtml(scope.acceptance),
|
|
72
|
-
// The agents the yes freezes (v48): the same block the task page and chat sign under, before the password.
|
|
73
|
-
agentsCeremonyHtml(data.route),
|
|
74
|
-
scope === null ? "" : runtimeDetailsHtml(scope),
|
|
75
|
-
]);
|
|
76
|
-
const plan = data.planDocument === null ? "" : html `<section class="k-sec"><h3>The plan</h3>${executionPlanHtml(data.planDocument, true)}${planContractHtml(data.planContract ?? null, "ceremony")}</section>`;
|
|
77
|
-
const digest = data.approvalDigest ?? item.approval.digest;
|
|
78
|
-
// The terms ride inside the form, before the password: what the yes covers is on the form that says it.
|
|
79
|
-
card = html `${plan}${slots({
|
|
80
|
-
wait: true, attrs: { "data-approval": digest },
|
|
81
|
-
status: html `${chip("Plan", "ask")}<h2>${item.approval.title}</h2>`,
|
|
82
|
-
text: html `${item.approval.taskId} · ${openTask(item.approval.taskId, "Open the task to change the scope first")}`,
|
|
83
|
-
acts: approveForm({ action: `${taskHref(item.approval.taskId)}/approve`, kind: "standard", facts: facts_, label: "Approve and start", primary: true,
|
|
84
|
-
before: html `<section class="k-sec"><h3>Approve exactly this</h3>${terms}</section>`,
|
|
85
|
-
form: { returnTo: "next", hidden: { nonce: data.nonce, digest }, attrs: { class: "k-form" } } }),
|
|
86
|
-
terms: approvalTerms("standard", facts_),
|
|
87
|
-
})}`;
|
|
88
|
-
}
|
|
89
|
-
else if (item.kind === "requeue") {
|
|
90
|
-
card = slots({
|
|
91
|
-
wait: true,
|
|
92
|
-
status: html `${chip("Stopped", "ask")}<h2>${item.stalled.title}</h2>`,
|
|
93
|
-
text: `Stopped with ${item.stalled.incidentCount} incident${item.stalled.incidentCount === 1 ? "" : "s"}${item.stalled.strikes > 0 ? ` after ${item.stalled.strikes} attempt${item.stalled.strikes === 1 ? "" : "s"}` : ""}. Retrying resolves the incidents, clears the failed attempts and puts it back in line.`,
|
|
94
|
-
acts: postForm(`${taskHref(item.stalled.taskId)}/requeue`, btn("Retry this work", { variant: "primary", type: "submit" }), { returnTo: "next" }),
|
|
95
|
-
terms: html `${item.stalled.taskId} · ${openTask(item.stalled.taskId, "Read the attempts first")}`,
|
|
96
|
-
});
|
|
97
|
-
}
|
|
98
|
-
else {
|
|
99
|
-
const { gap } = item;
|
|
100
|
-
card = slots({
|
|
101
|
-
wait: true,
|
|
102
|
-
status: html `${chip("Missing", "ask")}<h2>Supply ${gap.key}</h2>`,
|
|
103
|
-
text: html `${gap.state}. Filling it starts ${gap.unblocks.length} task${gap.unblocks.length === 1 ? "" : "s"}: ${joinHtml(gap.unblocks.map(one => html `<span class="k-mono">${one}</span>`), ", ")}.`,
|
|
104
|
-
acts: "",
|
|
105
|
-
terms: html `Prove it is filled from a terminal: <code class="k-mono">${gap.verify}</code>`,
|
|
106
|
-
});
|
|
107
|
-
}
|
|
108
|
-
return screen(title, html `${phoneHeader(title)}${doc(header, card)}`, { chrome });
|
|
109
|
-
}
|