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
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One step to approve a new route (docs/plans/plain-toolroll.md P2.5). There are no route classes: an approval stays
|
|
3
|
+
* bound to its exact route, and dispatch runs on no other bytes. When the route a task would dispatch on differs from the
|
|
4
|
+
* sealed one, Toolroll files the same words again on today's route (nothing is sealed) and keeps one "what changed" line,
|
|
5
|
+
* bound to that route's digest, for the one Approve. A legacy row with no route gets the same.
|
|
6
|
+
*
|
|
7
|
+
* approve() reads routeChangeProblem: a line that doesn't describe the route being approved refuses "changed".
|
|
8
|
+
*/
|
|
9
|
+
import { providerName } from "./provider-auth.js";
|
|
10
|
+
import { legOf, routeDigestOf, routeFromJson } from "./phase-routing.js";
|
|
11
|
+
import { deployGateScope, releaseCheckScope } from "./scope.js";
|
|
12
|
+
import { BUILT_IN } from "./store.js";
|
|
13
|
+
const ROLE = { plan: "planner", build: "builder", repair: "repair agent", review: "reviewer" };
|
|
14
|
+
const named = (one) => `${providerName(one.provider)} ${one.model ?? "(its default model)"}`;
|
|
15
|
+
const billing = (mode) => mode === "api-key" ? "API key" : mode === "subscription" ? "subscription" : null;
|
|
16
|
+
/** The sentence for what changed between the seal and the proposal. */
|
|
17
|
+
export function routeChangeWords(sealed, next) {
|
|
18
|
+
const changes = [];
|
|
19
|
+
for (const phase of ["build", "plan", "repair", "review"]) {
|
|
20
|
+
const now = legOf(next.route, phase);
|
|
21
|
+
const was = sealed.route !== null ? legOf(sealed.route, phase) : phase === "build" && sealed.profile != null ? { provider: sealed.profile.provider, model: "model" in sealed.profile ? (sealed.profile.model ?? null) : null } : null;
|
|
22
|
+
if (was === null) {
|
|
23
|
+
if (phase === "build")
|
|
24
|
+
changes.push(`The builder is ${named(now)}`);
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
if (was.provider === now.provider && was.model === now.model)
|
|
28
|
+
continue;
|
|
29
|
+
const line = `changes from ${named(was)} to ${named(now)}`;
|
|
30
|
+
// The repair agent is the builder's usual twin: one change is one sentence.
|
|
31
|
+
if (changes.some(one => one.endsWith(line)))
|
|
32
|
+
continue;
|
|
33
|
+
changes.push(`${changes.length === 0 ? "The" : "the"} ${ROLE[phase]} ${line}`);
|
|
34
|
+
}
|
|
35
|
+
const from = billing(sealed.authMode), to = billing(next.authMode);
|
|
36
|
+
const paid = from !== null && to !== null && from !== to ? ` (${from} → ${to})` : "";
|
|
37
|
+
if (changes.length === 0)
|
|
38
|
+
return "The agents' settings changed since you approved this. Approve to continue.";
|
|
39
|
+
return `${changes.join("; ")}${paid}. Approve to continue.`;
|
|
40
|
+
}
|
|
41
|
+
/** File the same words on today's route, and keep the line. Null: nothing to propose (a release, nothing changed, or no
|
|
42
|
+
* route Toolroll can name), and the task stays as it is. It seals nothing. */
|
|
43
|
+
export function proposeNewRoute(store, taskRef, taskId, now) {
|
|
44
|
+
return store.transact(() => {
|
|
45
|
+
const before = store.getScope(taskId);
|
|
46
|
+
if (before === null || deployGateScope(before) || releaseCheckScope(before))
|
|
47
|
+
return null;
|
|
48
|
+
const sealedRoute = routeFromJson(before.approvedRouteJson ?? null);
|
|
49
|
+
const refiled = store.refileScope(taskId, now);
|
|
50
|
+
if (refiled === null || refiled.profileState === "unresolved")
|
|
51
|
+
return null;
|
|
52
|
+
const route = routeFromJson(refiled.proposedRouteJson ?? null);
|
|
53
|
+
if (route === null)
|
|
54
|
+
return null;
|
|
55
|
+
if (refiled.approvedDigest === refiled.digest)
|
|
56
|
+
return null;
|
|
57
|
+
// How the last attempt billed, against how the new builder would: said when they differ.
|
|
58
|
+
const lastBilled = store.runsFor(taskRef).find(one => one.role === "builder")?.authMode ?? null;
|
|
59
|
+
const billed = store.agentsFor([legOf(route, "build").provider])[0]?.billing ?? null;
|
|
60
|
+
const words = routeChangeWords({ route: sealedRoute, profile: before.approvedProfile ?? null, authMode: lastBilled }, { route, authMode: billed });
|
|
61
|
+
store.handle.prepare(`INSERT INTO route_change (task_ref, scope_digest, to_route_digest, words, created_at) VALUES (?, ?, ?, ?, ?)
|
|
62
|
+
ON CONFLICT (task_ref) DO UPDATE SET scope_digest = excluded.scope_digest, to_route_digest = excluded.to_route_digest, words = excluded.words, created_at = excluded.created_at`)
|
|
63
|
+
.run(taskRef, refiled.digest, routeDigestOf(route), words, now.toISOString());
|
|
64
|
+
return { words };
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
/** The line for this task's waiting plan, or null. Only while it describes exactly the plan up for approval. */
|
|
68
|
+
export function routeChangeOf(store, taskId) {
|
|
69
|
+
const ref = store.lookupRef(taskId);
|
|
70
|
+
if (ref === null)
|
|
71
|
+
return null;
|
|
72
|
+
const row = store.handle.prepare("SELECT scope_digest, to_route_digest, words FROM route_change WHERE task_ref = ?").get(ref.id);
|
|
73
|
+
const scope = store.getScope(taskId);
|
|
74
|
+
if (row === undefined || scope === null || scope.digest !== row["scope_digest"] || scope.approvedDigest === scope.digest)
|
|
75
|
+
return null;
|
|
76
|
+
const route = routeFromJson(scope.proposedRouteJson ?? null);
|
|
77
|
+
return route !== null && routeDigestOf(route) === row["to_route_digest"] ? String(row["words"]) : null;
|
|
78
|
+
}
|
|
79
|
+
/** Each sweep: an unapproved row from before routing gets its route, and the same one Approve. A standing old approval
|
|
80
|
+
* is left alone (the stale check reaches it if its route ever differs). */
|
|
81
|
+
export function routeLegacyPlans(store, now) {
|
|
82
|
+
const rows = store.handle.prepare(`SELECT r.id AS ref, r.external_id AS task FROM task_scope s JOIN task_ref r ON r.external_id = s.task_id AND r.backend = ? JOIN task t ON t.id = r.external_id
|
|
83
|
+
WHERE s.route_era IS NULL AND t.state = 'queued' AND (s.approved_digest IS NULL OR s.approved_digest <> s.digest) LIMIT 20`).all(BUILT_IN);
|
|
84
|
+
let routed = 0;
|
|
85
|
+
for (const row of rows)
|
|
86
|
+
if (proposeNewRoute(store, Number(row["ref"]), String(row["task"]), now) !== null)
|
|
87
|
+
routed++;
|
|
88
|
+
return routed;
|
|
89
|
+
}
|
package/dist/route-hooks.js
CHANGED
|
@@ -80,8 +80,9 @@ const VERB = String.raw `(?:INSERT(?:\s+OR\s+\w+)?\s+INTO|REPLACE\s+INTO|UPDATE(
|
|
|
80
80
|
const writes = (tables) => new RegExp(String.raw `\b${VERB}\s+(?:main\s*\.\s*)?["\`\[]?(?:${tables.join("|")})["\`\]]?(?![\w])`, "i");
|
|
81
81
|
const ROUTING_WRITE = writes([...ROUTING_TABLES, "route_rule_one_winner"]);
|
|
82
82
|
const MODELS_WRITE = writes(MODELS_TABLES);
|
|
83
|
-
/** The two columns only route-store.ts writes: any write statement that names one.
|
|
84
|
-
|
|
83
|
+
/** The two columns only route-store.ts writes: any write statement that names one. A lesson note keeps its own copy of
|
|
84
|
+
* how a revision was asked for: that one INSERT into its own table is not this column, and only it is left out. */
|
|
85
|
+
const COLUMN_WRITE = /\b(?:INSERT(?!\s+INTO\s+lesson_note\b)|REPLACE|UPDATE)\b[^]*\b(?:exec_class|revision_origin)\b/i;
|
|
85
86
|
/** Each string or template text in the file that writes a protected row, with the function it's in ("" at the top
|
|
86
87
|
* level): `Class#method`, or the top-level function. */
|
|
87
88
|
export function protectedWrites(file, source) {
|
package/dist/route-results.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { INELIGIBLE_REASONS, type RecordReason, type RecordState, type ResultRecordView, type Selection } from "./contracts/route-results.js";
|
|
2
|
-
import { type Store } from "./store.js";
|
|
2
|
+
import { type Run, type Store } from "./store.js";
|
|
3
3
|
export { cohortOf, EXEC_CLASS_OF_REASON, execClassOf, type CohortInput, type CohortRow } from "./route-store.js";
|
|
4
4
|
/** How long an accepted result waits before it counts, and how long a result may wait for an answer. */
|
|
5
5
|
export declare const MATURE_AFTER_MS: number;
|
|
@@ -39,6 +39,11 @@ export type RecordValue = {
|
|
|
39
39
|
/** The family is quiet, or past ACCUMULATE_FOR_MS: with a final verdict, the record closes. */
|
|
40
40
|
accumulationEnded: boolean;
|
|
41
41
|
};
|
|
42
|
+
/** Whether the first built run among `runs` passed its own check; null when none is recorded. The code map's report reads
|
|
43
|
+
* this too, so the two never disagree on what a first check is. */
|
|
44
|
+
export declare function firstCheckPassedOf(store: Store, runs: readonly Pick<Run, "id" | "outcome">[]): boolean | null;
|
|
45
|
+
/** How many of these task versions are a person's revision. */
|
|
46
|
+
export declare function personRevisionsOf(store: Store, refs: readonly number[]): number;
|
|
42
47
|
/**
|
|
43
48
|
* A task family's record, evaluated now from its rows (null: no code-writing run, or a family whose lineage can't be
|
|
44
49
|
* proved). The order is the plan's: what can't be attributed to one model and one cohort first, then a live attempt
|
package/dist/route-results.js
CHANGED
|
@@ -54,6 +54,17 @@ function classOfRun(run, kept) {
|
|
|
54
54
|
return "model";
|
|
55
55
|
}
|
|
56
56
|
const FAILED_OUTCOMES = new Set(["failed", "interrupted"]);
|
|
57
|
+
/** Whether the first built run among `runs` passed its own check; null when none is recorded. The code map's report reads
|
|
58
|
+
* this too, so the two never disagree on what a first check is. */
|
|
59
|
+
export function firstCheckPassedOf(store, runs) {
|
|
60
|
+
const first = runs.find(run => run.outcome === "built");
|
|
61
|
+
const status = first === undefined ? null : store.runCheckFor(first.id)?.status ?? null;
|
|
62
|
+
return status === "passed" ? true : status === "failed" ? false : null;
|
|
63
|
+
}
|
|
64
|
+
/** How many of these task versions are a person's revision. */
|
|
65
|
+
export function personRevisionsOf(store, refs) {
|
|
66
|
+
return Number(store.handle.prepare("SELECT COUNT(*) AS n FROM task_ref WHERE id IN (SELECT value FROM json_each(?)) AND revision_origin = 'person'").get(JSON.stringify(refs))?.["n"] ?? 0);
|
|
67
|
+
}
|
|
57
68
|
/**
|
|
58
69
|
* A task family's record, evaluated now from its rows (null: no code-writing run, or a family whose lineage can't be
|
|
59
70
|
* proved). The order is the plan's: what can't be attributed to one model and one cohort first, then a live attempt
|
|
@@ -106,10 +117,8 @@ export function evaluateFamily(store, rootTaskId, evidenceRoot, now) {
|
|
|
106
117
|
else
|
|
107
118
|
agentTurns += run.agentTurns;
|
|
108
119
|
}
|
|
109
|
-
const
|
|
110
|
-
const
|
|
111
|
-
const personRevisions = store.handle.prepare("SELECT COUNT(*) AS n FROM task_ref WHERE id IN (SELECT value FROM json_each(?)) AND revision_origin = 'person'")
|
|
112
|
-
.get(JSON.stringify(family.versions.filter(one => one.id !== family.root.id).map(one => one.refId)))?.["n"];
|
|
120
|
+
const firstCheck = firstCheckPassedOf(store, runs);
|
|
121
|
+
const personRevisions = personRevisionsOf(store, family.versions.filter(one => one.id !== family.root.id).map(one => one.refId));
|
|
113
122
|
const quiet = family.versions.every(one => one.state === "done" || one.state === "cancelled" || one.state === "failed") && runs.every(run => run.outcome !== null);
|
|
114
123
|
let modelFailures = 0, infraFailures = 0;
|
|
115
124
|
for (const run of runs) {
|
|
@@ -129,7 +138,7 @@ export function evaluateFamily(store, rootTaskId, evidenceRoot, now) {
|
|
|
129
138
|
firstStartedAt: first?.startedAt ?? runs[0].startedAt, acceptedAt: null, decidedAt: null, modelFailures, infraFailures,
|
|
130
139
|
apiMicrousd, subscriptionRuns, subscriptionTokens, unknownRuns, remediationRuns,
|
|
131
140
|
selections: attributed.map(one => one.selection).filter((one) => one !== null), settled: false,
|
|
132
|
-
personRevisions
|
|
141
|
+
personRevisions, firstCheckPassed: firstCheck,
|
|
133
142
|
agentTurns, turnsUnknownRuns,
|
|
134
143
|
accumulationEnded: quiet || now.getTime() >= Date.parse(first?.startedAt ?? runs[0].startedAt) + ACCUMULATE_FOR_MS,
|
|
135
144
|
};
|
package/dist/run-closeout.d.ts
CHANGED
package/dist/run-closeout.js
CHANGED
|
@@ -15,9 +15,9 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import { execFileSync } from "node:child_process";
|
|
17
17
|
import { existsSync, readdirSync, readlinkSync, realpathSync } from "node:fs";
|
|
18
|
-
import { hostname } from "node:os";
|
|
19
18
|
import { basename, sep } from "node:path";
|
|
20
19
|
import { ownedProcessCount, runOwnerTag } from "./exec.js";
|
|
20
|
+
import { localWitnessSql } from "./store.js";
|
|
21
21
|
import { sleep as defaultSleep } from "./sleep.js";
|
|
22
22
|
/** How long a process has after SIGTERM before SIGKILL. */
|
|
23
23
|
export const STOP_GRACE_MS = 10_000;
|
|
@@ -138,10 +138,11 @@ export async function closeOutRuns(store, clock, options = {}) {
|
|
|
138
138
|
if (process.platform === "win32")
|
|
139
139
|
return [];
|
|
140
140
|
const db = store.handle;
|
|
141
|
+
const local = localWitnessSql(db, "p"), mine = localWitnessSql(db);
|
|
141
142
|
const runs = db.prepare(`SELECT DISTINCT p.run AS run FROM run_process p JOIN run r ON r.id = p.run JOIN task_ref t ON t.id = r.task_ref
|
|
142
|
-
WHERE p.exited_at IS NULL AND p.pid IS NOT NULL AND p.containment IS NULL AND p.container IS NULL AND
|
|
143
|
+
WHERE p.exited_at IS NULL AND p.pid IS NOT NULL AND p.containment IS NULL AND p.container IS NULL AND ${local.sql}
|
|
143
144
|
AND r.outcome IS NOT NULL${options.repo === undefined ? "" : " AND t.repo = ?"} ORDER BY p.run`)
|
|
144
|
-
.all(...[
|
|
145
|
+
.all(...[...local.params, ...(options.repo === undefined ? [] : [options.repo])]).map(row => Number(row["run"]));
|
|
145
146
|
const out = [];
|
|
146
147
|
if (runs.length === 0)
|
|
147
148
|
return out;
|
|
@@ -151,7 +152,7 @@ export async function closeOutRuns(store, clock, options = {}) {
|
|
|
151
152
|
if (ownedProcessCount(runOwnerTag(store, runId)) > 0)
|
|
152
153
|
continue;
|
|
153
154
|
const witnesses = db.prepare(`SELECT pid, process_group, observed_at FROM run_process
|
|
154
|
-
WHERE run = ? AND exited_at IS NULL AND pid IS NOT NULL AND containment IS NULL AND container IS NULL AND
|
|
155
|
+
WHERE run = ? AND exited_at IS NULL AND pid IS NOT NULL AND containment IS NULL AND container IS NULL AND ${mine.sql}`).all(runId, ...mine.params)
|
|
155
156
|
.map(row => ({ pid: Number(row["pid"]), group: Number(row["process_group"]) === 1, observedAt: Date.parse(String(row["observed_at"])) }))
|
|
156
157
|
.filter(one => Number.isFinite(one.observedAt));
|
|
157
158
|
if (witnesses.length === 0)
|
|
@@ -252,8 +253,9 @@ export function checkoutTargets(census, inside, roots, guard) {
|
|
|
252
253
|
function finishedRunProcesses(store, census) {
|
|
253
254
|
const byPid = new Map(census.map(row => [row.pid, row]));
|
|
254
255
|
const roots = new Set();
|
|
256
|
+
const local = localWitnessSql(store.handle, "p");
|
|
255
257
|
const rows = store.handle.prepare(`SELECT p.pid AS pid, p.observed_at AS observed_at FROM run_process p JOIN run r ON r.id = p.run
|
|
256
|
-
WHERE p.exited_at IS NULL AND p.pid IS NOT NULL AND
|
|
258
|
+
WHERE p.exited_at IS NULL AND p.pid IS NOT NULL AND ${local.sql} AND r.outcome IS NOT NULL`).all(...local.params);
|
|
257
259
|
for (const row of rows) {
|
|
258
260
|
const now = byPid.get(Number(row["pid"]));
|
|
259
261
|
const observedAt = Date.parse(String(row["observed_at"]));
|
package/dist/runner-process.js
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* The record is the plane's own state (the folder that holds the database). It grants nothing by itself: it can
|
|
14
14
|
* only let a name whose every recorded holder has exited be taken over early.
|
|
15
15
|
*/
|
|
16
|
+
import { sameMachineHost } from "./machine-identity.js";
|
|
16
17
|
import { createHash, createHmac, randomUUID, timingSafeEqual } from "node:crypto";
|
|
17
18
|
import { execFileSync } from "node:child_process";
|
|
18
19
|
import { closeSync, fstatSync, lstatSync, mkdirSync, openSync, readSync, realpathSync, renameSync, rmSync, writeFileSync, fsyncSync, constants } from "node:fs";
|
|
@@ -195,7 +196,7 @@ export function holderProvedGone(record, holder, host, probe = realProcessProbe)
|
|
|
195
196
|
if (typeof record.signature !== "string" || !/^[a-f0-9]{64}$/.test(record.signature)
|
|
196
197
|
|| !timingSafeEqual(Buffer.from(record.signature, "hex"), Buffer.from(expected, "hex")))
|
|
197
198
|
return { gone: false, why: "its process record is not authenticated" };
|
|
198
|
-
if (record.runner !== holder.runner.name || record.host !== holder.runner.host || record.host
|
|
199
|
+
if (record.runner !== holder.runner.name || record.host !== holder.runner.host || !sameMachineHost(record.host, host))
|
|
199
200
|
return { gone: false, why: "its record is for another worker or machine" };
|
|
200
201
|
if (record.registeredAt !== holder.runner.registeredAt || record.credential !== credentialDigest(holder.credentialHash))
|
|
201
202
|
return { gone: false, why: "its record is from an earlier start" };
|
|
@@ -229,7 +230,7 @@ export function recordStandsFor(record, holder, host) {
|
|
|
229
230
|
if (typeof record.signature !== "string" || !/^[a-f0-9]{64}$/.test(record.signature)
|
|
230
231
|
|| !timingSafeEqual(Buffer.from(record.signature, "hex"), Buffer.from(expected, "hex")))
|
|
231
232
|
return "its process record is not authenticated";
|
|
232
|
-
if (record.runner !== holder.runner.name || record.host !== holder.runner.host || record.host
|
|
233
|
+
if (record.runner !== holder.runner.name || record.host !== holder.runner.host || !sameMachineHost(record.host, host))
|
|
233
234
|
return "its record is for another worker or machine";
|
|
234
235
|
if (record.registeredAt !== holder.runner.registeredAt || record.credential !== credentialDigest(holder.credentialHash))
|
|
235
236
|
return "its record is from an earlier start";
|
package/dist/runner.js
CHANGED
|
@@ -380,7 +380,8 @@ export function recoverDead(store, now, livenessMs = DEFAULT_LIVENESS_MS) {
|
|
|
380
380
|
const claims = store.releaseClaimsOf(candidate.name, now, requeued);
|
|
381
381
|
const worktrees = store.releaseWorktreesOf(candidate.name, now);
|
|
382
382
|
const settled = store.recoverRunnerWork(candidate.name, now);
|
|
383
|
-
|
|
383
|
+
// Only what recovery left in the queue: a vanished build it stopped for a person isn't requeued (vanished-retry.ts).
|
|
384
|
+
return { runner: candidate.name, claims, worktrees, runs: settled.runs, requeued: [...new Set([...requeued, ...settled.requeued])].filter(id => store.getTask(id)?.state === "queued") };
|
|
384
385
|
});
|
|
385
386
|
if (recovery !== null)
|
|
386
387
|
recovered.push(recovery);
|
package/dist/scope.d.ts
CHANGED
|
@@ -793,7 +793,12 @@ export declare function fileAndSealUnderMode(store: Store, input: ScopeInput & {
|
|
|
793
793
|
reason: "no-mode" | "not-signer" | "not-covered" | "coordinator-filed" | "acceptance-required" | "profile-unresolved";
|
|
794
794
|
detail?: string;
|
|
795
795
|
};
|
|
796
|
-
export declare function approve(store: Store, taskId: string, by: string, now: Date, sawDigest: string, token: string, mutation?: Mutation
|
|
796
|
+
export declare function approve(store: Store, taskId: string, by: string, now: Date, sawDigest: string, token: string, mutation?: Mutation,
|
|
797
|
+
/** A fast start (fast-start.ts): Build is this same yes, with its marker in the proof. Only that road passes it. */
|
|
798
|
+
fastStart?: {
|
|
799
|
+
rule: string;
|
|
800
|
+
at: string;
|
|
801
|
+
}): ApproveResult;
|
|
797
802
|
/** The ledger act that records how an approval's person was proved (password-now, sso-now, fresh-password, fresh-sso). */
|
|
798
803
|
export declare const APPROVAL_PROOF = "approval proved";
|
|
799
804
|
/** The proof row's detail: when the person was proved, the generation, and the exact digest they said yes to. */
|
package/dist/scope.js
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
export { ACCEPTANCE_LIMITS, EVIDENCE_KINDS } from "./contracts/acceptance-terms.js";
|
|
3
3
|
import { mintSecret } from "./secret-mint.js";
|
|
4
4
|
import { passwordlessOn } from "./passwordless.js";
|
|
5
|
+
import { routeChangeProblem } from "./route-change-check.js";
|
|
6
|
+
import { issueImported } from "./task-source.js";
|
|
5
7
|
import { currentPasswordSource, passwordGuardOf, provenPasswordAccount } from "./sign-in-guard.js";
|
|
6
8
|
import { validateScopeText } from "./task-text.js";
|
|
7
9
|
import { contractChangesOf } from "./contract-changes.js";
|
|
@@ -1070,7 +1072,9 @@ export function fileAndSealUnderMode(store, input) {
|
|
|
1070
1072
|
return { ok: true, scope: store.getScope(input.taskId), basis: "mode" };
|
|
1071
1073
|
});
|
|
1072
1074
|
}
|
|
1073
|
-
export function approve(store, taskId, by, now, sawDigest, token, mutation = {}
|
|
1075
|
+
export function approve(store, taskId, by, now, sawDigest, token, mutation = {},
|
|
1076
|
+
/** A fast start (fast-start.ts): Build is this same yes, with its marker in the proof. Only that road passes it. */
|
|
1077
|
+
fastStart) {
|
|
1074
1078
|
// Read and write in one transaction, re-reading inside it — the credential
|
|
1075
1079
|
// check included, so a token rotated between authentication and the write
|
|
1076
1080
|
// cannot leave an approval signed by an authority that no longer exists.
|
|
@@ -1089,7 +1093,7 @@ export function approve(store, taskId, by, now, sawDigest, token, mutation = {})
|
|
|
1089
1093
|
// localApprovalEligible, read again here inside the seal's transaction).
|
|
1090
1094
|
const local = authenticated.method === "local-owner" ? currentLocalOwner() : null;
|
|
1091
1095
|
if (authenticated.method === "local-owner") {
|
|
1092
|
-
if (local === null || local.act !== "approve-plan")
|
|
1096
|
+
if (local === null || (local.act !== "approve-plan" && !(local.act === "build-now" && fastStart !== undefined)))
|
|
1093
1097
|
return { ok: false, reason: "policy", message: localOwnerRefusal(store.accountOf(by)?.passwordSet === true, "/").message };
|
|
1094
1098
|
const eligible = localApprovalEligible(store, by, taskId);
|
|
1095
1099
|
if (!eligible.ok)
|
|
@@ -1106,6 +1110,12 @@ export function approve(store, taskId, by, now, sawDigest, token, mutation = {})
|
|
|
1106
1110
|
// the new one in silence — which is the whole failure this guards.
|
|
1107
1111
|
if (sawDigest !== scope.digest)
|
|
1108
1112
|
return { ok: false, reason: "changed" };
|
|
1113
|
+
// The "what changed" line a person read describes this exact route, or this yes isn't to what they read (route-change.ts).
|
|
1114
|
+
if (routeChangeProblem(store, taskId, scope))
|
|
1115
|
+
return { ok: false, reason: "changed" };
|
|
1116
|
+
// A fast start is never a task from outside (github-import.ts): that takes the ordinary ceremony.
|
|
1117
|
+
if (fastStart !== undefined && issueImported(store, taskId))
|
|
1118
|
+
return { ok: false, reason: "policy", message: "A task from outside takes the ordinary approval." };
|
|
1109
1119
|
// An unresolved scope cannot say exactly what would run, so nobody can
|
|
1110
1120
|
// agree to it (foundations finding 16) — restatement is the road.
|
|
1111
1121
|
if (scope.profileState === "unresolved") {
|
|
@@ -1144,7 +1154,7 @@ export function approve(store, taskId, by, now, sawDigest, token, mutation = {})
|
|
|
1144
1154
|
// How the person was proved, beside every yes they give (a vote kept for protected work, or the seal): a password
|
|
1145
1155
|
// typed now, a re-check now, or a remembered one — with when, the credential generation and the exact digest.
|
|
1146
1156
|
const proved = () => store.recordAction({ at: now.toISOString(), actor: by, repo: store.lookupRef(taskId)?.repo ?? null, taskId, runId: null, action: APPROVAL_PROOF,
|
|
1147
|
-
outcome: authenticated.method, source: "work", detail: approvalProofDetail(authenticated.at, store.accountOf(by)?.generation ?? 0, scope.digest, local === null ? undefined : { generation: local.localGeneration, transport: local.transport }) });
|
|
1157
|
+
outcome: authenticated.method, source: "work", detail: (fastStart === undefined ? approvalProofDetail : fastProofDetail(fastStart))(authenticated.at, store.accountOf(by)?.generation ?? 0, scope.digest, local === null ? undefined : { generation: local.localGeneration, transport: local.transport }) });
|
|
1148
1158
|
if (gate.verdict === "vote") {
|
|
1149
1159
|
// The same person again adds nothing: no new vote, no proof row (and the approval roads refund a re-check).
|
|
1150
1160
|
if (gate.already)
|
|
@@ -1185,6 +1195,9 @@ export function approve(store, taskId, by, now, sawDigest, token, mutation = {})
|
|
|
1185
1195
|
export const APPROVAL_PROOF = "approval proved";
|
|
1186
1196
|
/** The proof row's detail: when the person was proved, the generation, and the exact digest they said yes to. */
|
|
1187
1197
|
export const approvalProofDetail = (at, generation, digest, local) => `${new Date(at).toISOString()} · generation ${generation}${local === undefined ? "" : ` · local ${local.generation} ${local.transport}`} · digest ${digest}`;
|
|
1198
|
+
/** A fast start's proof: the v1 record proofDetailOf reads, with the marker. `local` keeps the legacy words
|
|
1199
|
+
* localSealStandsSql reads, so a local owner's seal stands on the same rule. */
|
|
1200
|
+
const fastProofDetail = (fastStart) => (at, generation, digest, local) => JSON.stringify({ v: 1, at: new Date(at).toISOString(), generation, digest, fastStart, ...(local === undefined ? {} : { local: `proof · local ${local.generation} ${local.transport} · seal` }) });
|
|
1188
1201
|
/** The proof of THIS approval: the approver's own row for these exact bytes (never another approval's words). */
|
|
1189
1202
|
export function approvalProofOf(store, taskId, by, digest) {
|
|
1190
1203
|
const rows = store.handle.prepare("SELECT outcome, detail FROM action_ledger WHERE task_id = ? AND action = ? AND actor = ? ORDER BY id DESC LIMIT 50").all(taskId, APPROVAL_PROOF, by);
|
|
@@ -1321,8 +1334,8 @@ export function acceptanceWords(criteria) {
|
|
|
1321
1334
|
if (criteria.length === 0)
|
|
1322
1335
|
return [];
|
|
1323
1336
|
return [
|
|
1324
|
-
`
|
|
1325
|
-
...criteria.slice(1).map(c => ` ${c.id}: ${c.statement} [
|
|
1337
|
+
` done when ${criteria[0].id}: ${criteria[0].statement} [shown by: ${criteria[0].evidence.join(", ")}]`,
|
|
1338
|
+
...criteria.slice(1).map(c => ` ${c.id}: ${c.statement} [shown by: ${c.evidence.join(", ")}]`),
|
|
1326
1339
|
];
|
|
1327
1340
|
}
|
|
1328
1341
|
/** The route's approval-card lines (v47): the WORKING route the digest
|
|
@@ -1340,8 +1353,8 @@ evidence = null) {
|
|
|
1340
1353
|
return [
|
|
1341
1354
|
` goal ${scope.goal}`,
|
|
1342
1355
|
...(scope.outOfScope === null ? [] : [` not this ${scope.outOfScope}`]),
|
|
1343
|
-
...(scope.candidate ? [`
|
|
1344
|
-
...(scope.touches.length === 0 ? [] : [`
|
|
1356
|
+
...(scope.candidate ? [` commit ${scope.candidate.slice(0, 7)} — checked out by Toolroll, no agent runs`] : []),
|
|
1357
|
+
...(scope.touches.length === 0 ? [] : [` files ${scope.touches.join(", ")}`]),
|
|
1345
1358
|
...acceptanceWords(scope.acceptance),
|
|
1346
1359
|
...(scope.budgetMicrousd === null
|
|
1347
1360
|
? []
|
|
@@ -1358,7 +1371,7 @@ evidence = null) {
|
|
|
1358
1371
|
...(scope.profile === null || scope.profile === undefined ? [] : ((terms) => [` reads ${terms.reads}`, ...(terms.mach === null ? [] : [` services ${terms.mach}`])])(sandboxTermsWords(scope.profile))),
|
|
1359
1372
|
...releaseTermsWords(scope.releaseTerms ?? null),
|
|
1360
1373
|
...(scope.releaseCheck == null ? [] : [` release ${releaseCheckSentence(scope.releaseCheck)}`]),
|
|
1361
|
-
`
|
|
1374
|
+
` version ${scope.digest.slice(0, 8)} (the full plan version is in --json)`,
|
|
1362
1375
|
` approved ${describeApproval(approval)}`,
|
|
1363
1376
|
];
|
|
1364
1377
|
}
|
|
@@ -1394,7 +1407,7 @@ function describeApproval(approval) {
|
|
|
1394
1407
|
if (approval.approved)
|
|
1395
1408
|
return `yes, by ${approval.by} at ${approval.at}`;
|
|
1396
1409
|
if (approval.reason === "changed") {
|
|
1397
|
-
return "no —
|
|
1410
|
+
return "no — edited after approval; approve it again";
|
|
1398
1411
|
}
|
|
1399
1412
|
return "no — nothing will build this until somebody agrees to it";
|
|
1400
1413
|
}
|
package/dist/serve.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ export declare function createDecisionServer(options: ServeOptions): DecisionSer
|
|
|
4
4
|
export { PAGE_CSS, pinnedTheme, SENSITIVE_INPUT } from "./server/chrome.js";
|
|
5
5
|
export { redactedPath, SHUTDOWN_WAIT_MS, type ServeOptions } from "./server/http.js";
|
|
6
6
|
export { TASK_COMPOSER_MODES, type TaskComposerMode } from "./server/render-chat.js";
|
|
7
|
-
export {
|
|
7
|
+
export { queueScript } from "./server/render-pages.js";
|
|
8
8
|
export { ssoStepUps, wrongHostPage } from "./server/render-people.js";
|
|
9
9
|
export { diffFileAnchor, editorFileHref, needActionOf, orderChangedFiles, plainConclusionOf, rankReviewQueue, reviewFilePriority, reviewPriorityOf, runFactsFragment, withinSignedTouches, type ReviewFileRow, type ReviewPriority, type ReviewQueueFacts } from "./server/render-results.js";
|
|
10
10
|
export { decisionAnswerScript, earlierVersionsWords, revisionLineageWords, type RouteView } from "./server/render-tasks.js";
|
package/dist/serve.js
CHANGED
|
@@ -108,6 +108,7 @@ import { keepSlowRequest, Timeline } from "./latency-timeline.js";
|
|
|
108
108
|
import { readFileSync } from "node:fs";
|
|
109
109
|
import { createServer } from "node:http";
|
|
110
110
|
import { homedir, hostname } from "node:os";
|
|
111
|
+
import { sameMachineHost } from "./machine-identity.js";
|
|
111
112
|
import { dirname, join } from "node:path";
|
|
112
113
|
import { pinnedAccent } from "./accent-colors.js";
|
|
113
114
|
import { CHAT_KEY_ENV, credentialKeyOf, fetchOpenRouterCatalog, isSubscriptionChatProvider, priceForConfig, subscriptionCredentialKey } from "./converse.js";
|
|
@@ -1109,7 +1110,7 @@ export function createDecisionServer(options) {
|
|
|
1109
1110
|
// A project link is a read context, not a session-changing operation.
|
|
1110
1111
|
// Prove it against both admission and the known project catalog before
|
|
1111
1112
|
// collection queries; unknown/foreign paths reveal no work.
|
|
1112
|
-
if ((route.id === 'work' || route.id === 'system') && url.searchParams.has('project')) {
|
|
1113
|
+
if ((route.id === 'tasks' || route.id === 'work' || route.id === 'system') && url.searchParams.has('project')) {
|
|
1113
1114
|
const wanted = url.searchParams.get('project') ?? '';
|
|
1114
1115
|
if (url.searchParams.getAll('project').length !== 1 || !visible(wanted) ||
|
|
1115
1116
|
![...managedRepos(), ...store.listProjects().map(one => one.path)].includes(wanted)) {
|
|
@@ -1128,7 +1129,9 @@ export function createDecisionServer(options) {
|
|
|
1128
1129
|
project = wanted;
|
|
1129
1130
|
}
|
|
1130
1131
|
// All projects, asked for by name on Tasks (its project filter): the rolled-up list, admission-bound as ever.
|
|
1131
|
-
|
|
1132
|
+
// Tasks (P1.4) says every project as scope=all; an old link's all=1 still reads so.
|
|
1133
|
+
if (route.id === 'tasks' && !url.searchParams.has('project') && ((url.searchParams.get('scope') === 'all' && url.searchParams.getAll('scope').length === 1)
|
|
1134
|
+
|| (url.searchParams.get('all') === '1' && url.searchParams.getAll('all').length === 1)))
|
|
1132
1135
|
project = null;
|
|
1133
1136
|
// Every page's Tasks count covers this one project view, whatever project the page itself shows.
|
|
1134
1137
|
const facts = requestContext.getStore();
|
|
@@ -1145,7 +1148,7 @@ export function createDecisionServer(options) {
|
|
|
1145
1148
|
const namedProject = url.searchParams.get("project");
|
|
1146
1149
|
if (url.searchParams.getAll("result").length !== 1 || url.searchParams.getAll("run").length > 1 || url.searchParams.getAll("project").length > 1 ||
|
|
1147
1150
|
(namedProject !== null && (ref === null || !visible(ref.repo) || namedProject !== ref.repo))) {
|
|
1148
|
-
return refuse(response, who, 404, "No such result in your projects.", "/
|
|
1151
|
+
return refuse(response, who, 404, "No such result in your projects.", "/tasks");
|
|
1149
1152
|
}
|
|
1150
1153
|
if (who.via === "cookie" && ref !== null && visible(ref.repo))
|
|
1151
1154
|
project = ref.repo;
|
|
@@ -1171,7 +1174,7 @@ export function createDecisionServer(options) {
|
|
|
1171
1174
|
const needsProject = who.via === "cookie" && project === null && !unscopedMode &&
|
|
1172
1175
|
route.needsProject &&
|
|
1173
1176
|
!(route.id === "review" && url.searchParams.has("result")) &&
|
|
1174
|
-
!(route.id === "board" && url.searchParams.get("scope") === "all");
|
|
1177
|
+
!(route.id === "board" && url.searchParams.get("scope") === "all") && !(route.id === "tasks" && url.searchParams.get("scope") === "all");
|
|
1175
1178
|
if (needsProject)
|
|
1176
1179
|
return redirect(response, `/projects?return=${encodeURIComponent(safeReturn(url.pathname + url.search))}`);
|
|
1177
1180
|
return dispatchConsole(route, { url, who, request, response, route, now, project, chosenProject, posted: new URLSearchParams() });
|
|
@@ -1201,9 +1204,10 @@ export function createDecisionServer(options) {
|
|
|
1201
1204
|
const here = hostname();
|
|
1202
1205
|
const registered = store.listRunners().filter(one => one.retiredAt === null);
|
|
1203
1206
|
const workers = registered.filter(one => runnerAlive(one, now));
|
|
1204
|
-
|
|
1207
|
+
// This machine under any of its own names (machine-identity.ts): a hostname that moved with the network is still here.
|
|
1208
|
+
if (workers.some(one => !sameMachineHost(one.host, here) && store.providerReadiness(one.name).some(seen => seen.state === "ready")))
|
|
1205
1209
|
return true;
|
|
1206
|
-
return options.localRunner !== undefined || registered.length === 0 || workers.some(one => one.host
|
|
1210
|
+
return options.localRunner !== undefined || registered.length === 0 || workers.some(one => sameMachineHost(one.host, here)) ? localAgentSignedIn() : false;
|
|
1207
1211
|
}
|
|
1208
1212
|
let localSignIn = null;
|
|
1209
1213
|
let localSignInChecking = null;
|
|
@@ -1495,15 +1499,15 @@ export function createDecisionServer(options) {
|
|
|
1495
1499
|
function paletteIndexTag(project) {
|
|
1496
1500
|
const admitted = project === null ? admissionList() : null;
|
|
1497
1501
|
const entries = [
|
|
1498
|
-
{ label: "tasks", href: "/
|
|
1499
|
-
{ label: "needs you", href: "/
|
|
1500
|
-
{ label: "
|
|
1501
|
-
{ label: "board", href: "/board
|
|
1502
|
+
{ label: "tasks", href: "/tasks" },
|
|
1503
|
+
{ label: "needs you", href: "/tasks?view=needs-you" },
|
|
1504
|
+
{ label: "home", href: "/" },
|
|
1505
|
+
{ label: "board", href: "/tasks?view=board&scope=all" },
|
|
1502
1506
|
{ label: "queue", href: QUEUE_VIEW },
|
|
1503
|
-
{ label: "
|
|
1507
|
+
{ label: "every project", href: "/tasks?scope=all" },
|
|
1504
1508
|
{ label: "flows", href: "/flows" },
|
|
1505
|
-
{ label: "
|
|
1506
|
-
{ label: "review
|
|
1509
|
+
{ label: "finished", href: "/tasks?view=done" },
|
|
1510
|
+
{ label: "ready for review", href: "/tasks?view=ready" },
|
|
1507
1511
|
{ label: "fleet", href: "/fleet" },
|
|
1508
1512
|
{ label: "activity", href: "/activity" },
|
|
1509
1513
|
{ label: "action ledger", href: "/ledger" },
|
|
@@ -1518,8 +1522,8 @@ export function createDecisionServer(options) {
|
|
|
1518
1522
|
entries.push({ label: `${one.id} — ${one.title}`, href: `/t/${encodeURIComponent(one.id)}` });
|
|
1519
1523
|
}
|
|
1520
1524
|
const saturated = open.length > 200;
|
|
1521
|
-
const safeEntries = restricted() ? entries.filter(one => ["/
|
|
1522
|
-
return jsonScript(saturated ? [...safeEntries, { label: "… more in Tasks", href: "/
|
|
1525
|
+
const safeEntries = restricted() ? entries.filter(one => ["/tasks", "/tasks?view=needs-you", "/", "/tasks?view=board", "/flows", "/tasks?view=done", "/tasks?view=ready", "/runs", "/projects", "/ledger"].includes(one.href) || one.href.startsWith("/t/")) : entries;
|
|
1526
|
+
return jsonScript(saturated ? [...safeEntries, { label: "… more in Tasks", href: "/tasks" }] : safeEntries, { id: "palette-index" });
|
|
1523
1527
|
}
|
|
1524
1528
|
/**
|
|
1525
1529
|
* The palette cache (arc 4, finding 6), and its honest contract: a
|
|
@@ -1710,7 +1714,7 @@ export function createDecisionServer(options) {
|
|
|
1710
1714
|
needsYou: { count: needsYou, saturated: s.chrome.inboxSaturated === true, scope: tasksProject ?? null,
|
|
1711
1715
|
label: s.chrome.inboxLabel ?? `${needsYou} ${needsYou === 1 ? 'needs' : 'need'} you`,
|
|
1712
1716
|
// Every project, said by name (all=1), so a session's own open project never narrows the list the count opens.
|
|
1713
|
-
href: tasksProject === null ? '/
|
|
1717
|
+
href: tasksProject === null ? '/tasks?scope=all&view=needs-you' : `/tasks?project=${encodeURIComponent(tasksProject)}&view=needs-you` },
|
|
1714
1718
|
chats,
|
|
1715
1719
|
// The same count per project (the rail's project rows), read from the same work index totals.
|
|
1716
1720
|
...(() => { if (sensitive)
|
|
@@ -1753,7 +1757,9 @@ export function createDecisionServer(options) {
|
|
|
1753
1757
|
// A page with its own view keeps its script; the chat page runs the result script.
|
|
1754
1758
|
const functional = conversation === null || extras.view ? (s.functional?.script ?? '') : RESULT_REVIEW_SCRIPT;
|
|
1755
1759
|
const document = shell(s.title, s.body, { chrome: s.chrome, sensitive });
|
|
1756
|
-
|
|
1760
|
+
// The keys and their list (keymap.ts) work here as on every chromed page; the list sits beside the app React owns.
|
|
1761
|
+
// A sensitive page keeps to its own script.
|
|
1762
|
+
return page(response, status, browserWorkspaceDocument(document, workspace, nonce, functional + MOBILE_VIEWPORT_SCRIPT + keyFill + (sensitive ? '' : chromeScript()), sensitive ? html `` : KBD_HELP), nonce, true);
|
|
1757
1763
|
}
|
|
1758
1764
|
// A one-time secret on screen (a token, an invite link, a pairing code): the
|
|
1759
1765
|
// page carries no script of any kind, so it leaves the workspace for a
|
|
@@ -2485,7 +2491,7 @@ export function createDecisionServer(options) {
|
|
|
2485
2491
|
export { PAGE_CSS, pinnedTheme, SENSITIVE_INPUT } from "./server/chrome.js";
|
|
2486
2492
|
export { redactedPath, SHUTDOWN_WAIT_MS } from "./server/http.js";
|
|
2487
2493
|
export { TASK_COMPOSER_MODES } from "./server/render-chat.js";
|
|
2488
|
-
export {
|
|
2494
|
+
export { queueScript } from "./server/render-pages.js";
|
|
2489
2495
|
export { ssoStepUps, wrongHostPage } from "./server/render-people.js";
|
|
2490
2496
|
export { diffFileAnchor, editorFileHref, needActionOf, orderChangedFiles, plainConclusionOf, rankReviewQueue, reviewFilePriority, reviewPriorityOf, runFactsFragment, withinSignedTouches } from "./server/render-results.js";
|
|
2491
2497
|
export { decisionAnswerScript, earlierVersionsWords, revisionLineageWords } from "./server/render-tasks.js";
|
package/dist/server/chat.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { freshFacts,
|
|
1
|
+
import { freshFacts, freshnessFor, spendingOnlyOnSuccess, withFreshness } from "../scope.js";
|
|
2
|
+
import { approvalPasswordField } from "../approval-password.js";
|
|
2
3
|
import { CHAT_ACTION_LOCAL_ACTS, localOwnerRefusal } from "../local-owner.js";
|
|
3
|
-
import { localApprovalEligible } from "../scope.js";
|
|
4
4
|
import { handlersOf } from './handler-registry.js';
|
|
5
5
|
import { DEPRECATED_PAGE } from "../deprecations.js";
|
|
6
6
|
/** chat handlers, moved without changing their route bodies. */
|
|
@@ -170,7 +170,7 @@ export function createChatHandlers(runtime) {
|
|
|
170
170
|
const requestedRevision = url.searchParams.get("revision");
|
|
171
171
|
if (requestedRevision !== null) {
|
|
172
172
|
if (focusTask === null || !focusTask.family.versions.some(one => one.id === requestedRevision) || url.searchParams.has("result")) {
|
|
173
|
-
return refuse(response, who, 404, "That revision is not available for this task.", "/
|
|
173
|
+
return refuse(response, who, 404, "That revision is not available for this task.", "/tasks");
|
|
174
174
|
}
|
|
175
175
|
if (requestedRevision !== focusTask.executionId)
|
|
176
176
|
return redirect(response, `${taskHref(focusTask.id)}?version=${encodeURIComponent(requestedRevision)}`);
|
|
@@ -402,20 +402,22 @@ export function createChatHandlers(runtime) {
|
|
|
402
402
|
const evidenceLink = action.operation === 'result_accept' ? html `<a href="/r/${Number(action.request['run'])}">Inspect this result</a>` : '';
|
|
403
403
|
// Cockpit B2: approving standard work may stand on a password remembered on this session, exactly as on the task
|
|
404
404
|
// page (the same rule approve() applies when the form comes back); the field then may be left empty.
|
|
405
|
+
// The one password rule every approve form reads (approval-password.ts).
|
|
405
406
|
const freshSince = (() => {
|
|
406
407
|
const task = typeof action.request['task'] === 'string' ? action.request['task'] : null;
|
|
407
408
|
if (action.operation !== 'scope_approve' || task === null)
|
|
408
409
|
return null;
|
|
409
410
|
const scope = store.getScope(task), fresh = withFreshness('approve-standard', () => freshnessFor(who.name, Date.now(), true));
|
|
410
|
-
if (scope === null
|
|
411
|
+
if (scope === null)
|
|
411
412
|
return null;
|
|
412
|
-
const
|
|
413
|
-
return
|
|
413
|
+
const field = approvalPasswordField(store, { name: who.name, taskId: task, scope, passwordless: false, localOwner: false, freshAt: fresh?.at ?? null, returnTo: '/chat' });
|
|
414
|
+
return field.field === 'remembered' ? new Date(field.since) : null;
|
|
414
415
|
})();
|
|
415
416
|
// Easy-start C5: a laptop's local owner confirms its own acts without a password (an approval only when it is
|
|
416
417
|
// eligible: localApprovalEligible, the rule the seal applies again); anything else says how to get a password.
|
|
417
418
|
const reviewTask = typeof action.request['task'] === 'string' ? action.request['task'] : null;
|
|
418
|
-
const
|
|
419
|
+
const localScope = reviewTask === null ? null : store.getScope(reviewTask);
|
|
420
|
+
const localOk = who.authority === 'local-owner' && CHAT_ACTION_LOCAL_ACTS[action.operation] !== null && (action.operation !== 'scope_approve' || (reviewTask !== null && localScope !== null && approvalPasswordField(store, { name: who.name, taskId: reviewTask, scope: localScope, passwordless: false, localOwner: true, freshAt: null, returnTo: '/chat' }).field === 'none'));
|
|
419
421
|
const localWords = who.authority === 'local-owner' && !localOk ? localOwnerRefusal(store.accountOf(who.name)?.passwordSet === true, '/chat') : null;
|
|
420
422
|
const passwordField = !CHAT_ACTIONS[action.operation].password || localOk ? '' : localWords !== null ? html `<p class="approval-password-note">${localWords.message} <a href="${localWords.href}">${localWords.action}</a></p>` : reauth(freshSince === null
|
|
421
423
|
? html `<label>Your password<input type="password" name="token" autocomplete="current-password" required></label>`
|
|
@@ -1471,11 +1473,11 @@ export function createChatHandlers(runtime) {
|
|
|
1471
1473
|
const all = view("all"), needs = view("needs-you"), done = view("completed");
|
|
1472
1474
|
const weekAgo = new Date(now.getTime() - 7 * 86_400_000).toISOString();
|
|
1473
1475
|
const counts = [
|
|
1474
|
-
{ key: "working", label: "Working now", value: agents.length, href: "/
|
|
1476
|
+
{ key: "working", label: "Working now", value: agents.length, href: "/tasks?view=running" },
|
|
1475
1477
|
// Each by its shared headline, as the Tasks list words it: a result whose checks failed waits on you, it isn't ready.
|
|
1476
|
-
{ key: "waiting", label: "Waiting on you", value: needs.items.filter(one => one.status.label !== "Ready for review").length, href: "/
|
|
1477
|
-
{ key: "ready", label: "Ready to review", value: needs.items.filter(one => one.status.label === "Ready for review").length, href: "/
|
|
1478
|
-
{ key: "done", label: "Done this week", value: done.items.filter(one => (one.completion?.at ?? one.updatedAt) >= weekAgo).length, href: "/
|
|
1478
|
+
{ key: "waiting", label: "Waiting on you", value: needs.items.filter(one => one.status.label !== "Ready for review").length, href: "/tasks?view=needs-you" },
|
|
1479
|
+
{ key: "ready", label: "Ready to review", value: needs.items.filter(one => one.status.label === "Ready for review").length, href: "/tasks?view=needs-you" },
|
|
1480
|
+
{ key: "done", label: "Done this week", value: done.items.filter(one => (one.completion?.at ?? one.updatedAt) >= weekAgo).length, href: "/tasks?view=completed" },
|
|
1479
1481
|
];
|
|
1480
1482
|
const tabOf = (one) => one.assignmentState === "ready-to-check" ? one.status.label === "Ready for review" ? "ready" : "needs-you"
|
|
1481
1483
|
: one.assignmentState === "needs-decision" ? "needs-you" : one.assignmentState === "working" || one.assignmentState === "checking" ? "running" : "finished";
|
|
@@ -1494,7 +1496,7 @@ export function createChatHandlers(runtime) {
|
|
|
1494
1496
|
const lead = activity === null ? null : { name: activity.name, doing: activity.doing, at: activity.at,
|
|
1495
1497
|
href: leadTask == null || !admitted(leadTask.repo) ? null : taskHref(familyOf(activity.taskId)?.root.id ?? activity.taskId) };
|
|
1496
1498
|
return {
|
|
1497
|
-
agents, counts, catchUp, allHref: "/
|
|
1499
|
+
agents, counts, catchUp, allHref: "/tasks", ...(lead === null ? {} : { lead }),
|
|
1498
1500
|
planUse: (windows?.tiles ?? []).map(one => ({ name: one.name, window: one.window, percent: one.percent, detail: one.detail, tone: one.tone })),
|
|
1499
1501
|
};
|
|
1500
1502
|
}
|
package/dist/server/chrome.d.ts
CHANGED
|
@@ -264,7 +264,7 @@ export declare const TO_FRONT_ICON: Html;
|
|
|
264
264
|
* wear an icon each; the foot's list stays text, the way Linear's does. */
|
|
265
265
|
export declare const strokeIcon: (paths: Html | string) => Html;
|
|
266
266
|
/** Where the queue lives now: the board, flipped to dispatch order. */
|
|
267
|
-
export declare const QUEUE_VIEW = "/
|
|
267
|
+
export declare const QUEUE_VIEW = "/tasks?view=order";
|
|
268
268
|
export declare const FOLDER_PATHS: Html;
|
|
269
269
|
export declare const CHAT_PATHS: Html;
|
|
270
270
|
export declare const WORK_PATHS: Html;
|