toolroll 0.9.81 → 0.9.83
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 +35 -0
- package/dist/cli.js +1 -0
- package/dist/commit-extract.d.ts +18 -0
- package/dist/commit-extract.js +39 -1
- package/dist/converse.d.ts +12 -0
- package/dist/converse.js +18 -2
- package/dist/deploy-gate.js +8 -0
- package/dist/desktop-update.d.ts +1 -1
- package/dist/eval-gate.d.ts +51 -0
- package/dist/eval-gate.js +199 -0
- package/dist/eval-hooks.d.ts +39 -0
- package/dist/eval-hooks.js +477 -0
- package/dist/eval-oracle-main.d.ts +1 -0
- package/dist/eval-oracle-main.js +31 -0
- package/dist/eval-oracle.d.ts +60 -0
- package/dist/eval-oracle.js +217 -0
- package/dist/eval-world.d.ts +17 -0
- package/dist/eval-world.js +97 -0
- package/dist/evals.d.ts +299 -0
- package/dist/evals.js +964 -0
- package/dist/exec.js +15 -1
- package/dist/first-tasks-child.d.ts +1 -0
- package/dist/first-tasks-child.js +11 -0
- package/dist/flow-draft.js +4 -0
- package/dist/flow-sort.js +4 -0
- package/dist/folder-look.d.ts +107 -0
- package/dist/folder-look.js +302 -0
- package/dist/github-jobs.d.ts +25 -0
- package/dist/github-jobs.js +22 -0
- package/dist/invoke.d.ts +1 -1
- package/dist/invoke.js +14 -0
- package/dist/lead.js +4 -1
- package/dist/learning-child.d.ts +1 -0
- package/dist/learning-child.js +34 -0
- package/dist/learning-look.d.ts +25 -0
- package/dist/learning-look.js +72 -0
- package/dist/memory-pass.js +4 -0
- package/dist/nightly-check.d.ts +4 -1
- package/dist/nightly-check.js +5 -2
- package/dist/nightly-github.d.ts +316 -0
- package/dist/nightly-github.js +1022 -0
- package/dist/operate.d.ts +123 -1
- package/dist/operate.js +344 -29
- package/dist/project-identity.d.ts +37 -0
- package/dist/project-identity.js +106 -0
- package/dist/project-knowledge.d.ts +4 -4
- package/dist/project-tools.d.ts +4 -1
- package/dist/project-tools.js +7 -2
- package/dist/project.d.ts +4 -1
- package/dist/project.js +11 -4
- package/dist/provider.d.ts +3 -0
- package/dist/provider.js +9 -2
- package/dist/release-adapters.d.ts +21 -21
- package/dist/release-adapters.js +52 -19
- package/dist/release-auto.js +26 -0
- package/dist/release-card.js +3 -0
- package/dist/release-cli.js +59 -3
- package/dist/release-view.d.ts +3 -0
- package/dist/release-view.js +2 -0
- package/dist/release.d.ts +16 -0
- package/dist/release.js +16 -0
- package/dist/reported-usage.d.ts +69 -0
- package/dist/reported-usage.js +97 -0
- package/dist/repos.d.ts +7 -0
- package/dist/repos.js +57 -0
- package/dist/serve.js +35 -8
- package/dist/server/chat.js +4 -0
- package/dist/server/flows.js +9 -3
- package/dist/server/guards.d.ts +4 -0
- package/dist/server/guards.js +12 -3
- package/dist/server/http.d.ts +11 -0
- package/dist/server/http.js +3 -0
- package/dist/server/pages.js +62 -88
- package/dist/server/render-pages.d.ts +2 -16
- package/dist/server/render-pages.js +1 -55
- package/dist/server/render-tasks.js +1 -0
- package/dist/server/runtime.d.ts +4 -0
- package/dist/server/runtime.js +1 -0
- package/dist/server/settings.js +14 -3
- package/dist/server/tasks.js +15 -4
- package/dist/store.d.ts +109 -1
- package/dist/store.js +394 -4
- package/dist/subagents.js +4 -0
- package/dist/subscription-chat.d.ts +2 -0
- package/dist/subscription-chat.js +52 -13
- package/dist/task-sizing.js +4 -0
- package/dist/teardown-git.d.ts +9 -2
- package/dist/teardown-git.js +56 -33
- package/dist/up-processes.d.ts +13 -0
- package/dist/up-processes.js +6 -2
- package/evals/cases/catalog/case.json +25 -0
- package/evals/cases/catalog/fixture/README.md +3 -0
- package/evals/cases/catalog/fixture/package.json +8 -0
- package/evals/cases/catalog/fixture/test/catalog.test.mjs +4 -0
- package/evals/cases/catalog/oracle.mjs +23 -0
- package/evals/cases/catalog/probe.mjs +10 -0
- package/evals/cases/catalog/source.md +9 -0
- package/evals/cases/csv-quoting/case.json +23 -0
- package/evals/cases/csv-quoting/fixture/README.md +3 -0
- package/evals/cases/csv-quoting/fixture/csv.js +1 -0
- package/evals/cases/csv-quoting/fixture/package.json +8 -0
- package/evals/cases/csv-quoting/fixture/test/csv.test.mjs +5 -0
- package/evals/cases/csv-quoting/oracle.mjs +14 -0
- package/evals/cases/csv-quoting/probe.mjs +9 -0
- package/evals/cases/csv-quoting/source.md +9 -0
- package/evals/cases/dedupe/case.json +23 -0
- package/evals/cases/dedupe/fixture/README.md +3 -0
- package/evals/cases/dedupe/fixture/dedupe.js +1 -0
- package/evals/cases/dedupe/fixture/package.json +8 -0
- package/evals/cases/dedupe/fixture/test/dedupe.test.mjs +5 -0
- package/evals/cases/dedupe/oracle.mjs +16 -0
- package/evals/cases/dedupe/probe.mjs +13 -0
- package/evals/cases/dedupe/source.md +9 -0
- package/evals/cases/endpoint/case.json +25 -0
- package/evals/cases/endpoint/fixture/README.md +3 -0
- package/evals/cases/endpoint/fixture/package.json +8 -0
- package/evals/cases/endpoint/fixture/test/endpoint.test.mjs +4 -0
- package/evals/cases/endpoint/oracle.mjs +27 -0
- package/evals/cases/endpoint/probe.mjs +10 -0
- package/evals/cases/endpoint/source.md +11 -0
- package/evals/cases/math-divide/case.json +23 -0
- package/evals/cases/math-divide/fixture/README.md +3 -0
- package/evals/cases/math-divide/fixture/package.json +8 -0
- package/evals/cases/math-divide/fixture/src/math.js +1 -0
- package/evals/cases/math-divide/fixture/test/math.test.js +5 -0
- package/evals/cases/math-divide/oracle.mjs +13 -0
- package/evals/cases/math-divide/probe.mjs +9 -0
- package/evals/cases/math-divide/source.md +9 -0
- package/evals/cases/math-multiply/case.json +23 -0
- package/evals/cases/math-multiply/fixture/README.md +3 -0
- package/evals/cases/math-multiply/fixture/package.json +8 -0
- package/evals/cases/math-multiply/fixture/src/math.js +2 -0
- package/evals/cases/math-multiply/fixture/test/math.test.js +5 -0
- package/evals/cases/math-multiply/oracle.mjs +12 -0
- package/evals/cases/math-multiply/probe.mjs +9 -0
- package/evals/cases/math-multiply/source.md +9 -0
- package/evals/cases/math-subtract/case.json +23 -0
- package/evals/cases/math-subtract/fixture/README.md +3 -0
- package/evals/cases/math-subtract/fixture/package.json +8 -0
- package/evals/cases/math-subtract/fixture/src/math.js +1 -0
- package/evals/cases/math-subtract/fixture/test/math.test.js +5 -0
- package/evals/cases/math-subtract/oracle.mjs +12 -0
- package/evals/cases/math-subtract/probe.mjs +9 -0
- package/evals/cases/math-subtract/source.md +9 -0
- package/evals/cases/merge-config/case.json +23 -0
- package/evals/cases/merge-config/fixture/README.md +3 -0
- package/evals/cases/merge-config/fixture/config.js +1 -0
- package/evals/cases/merge-config/fixture/package.json +8 -0
- package/evals/cases/merge-config/fixture/test/config.test.mjs +5 -0
- package/evals/cases/merge-config/oracle.mjs +28 -0
- package/evals/cases/merge-config/probe.mjs +15 -0
- package/evals/cases/merge-config/source.md +9 -0
- package/evals/cases/paginate/case.json +23 -0
- package/evals/cases/paginate/fixture/README.md +3 -0
- package/evals/cases/paginate/fixture/package.json +8 -0
- package/evals/cases/paginate/fixture/paginate.js +1 -0
- package/evals/cases/paginate/fixture/test/paginate.test.mjs +5 -0
- package/evals/cases/paginate/oracle.mjs +18 -0
- package/evals/cases/paginate/probe.mjs +13 -0
- package/evals/cases/paginate/source.md +9 -0
- package/evals/cases/round2-asks/case.json +24 -0
- package/evals/cases/round2-asks/fixture/README.md +3 -0
- package/evals/cases/round2-asks/fixture/package.json +8 -0
- package/evals/cases/round2-asks/fixture/src/math.js +1 -0
- package/evals/cases/round2-asks/fixture/test/math.test.js +5 -0
- package/evals/cases/round2-asks/source.md +11 -0
- package/evals/manifest.json +15 -0
- package/package.json +2 -1
package/dist/release-cli.js
CHANGED
|
@@ -21,6 +21,7 @@ import { carryRefusalWords, proveCarry, setAskEveryReleaseCommit, setUnsandboxed
|
|
|
21
21
|
import { authenticateApprover, machineActing, personProof, proveCredential } from "./scope.js";
|
|
22
22
|
import { allowAutoPublish, setReleaseApproval } from "./release-auto.js";
|
|
23
23
|
import { createNightlyCheckFlow } from "./nightly-check.js";
|
|
24
|
+
import { aliasNightlyLane, optInGithubNightly, retireNightlyLane } from "./nightly-github.js";
|
|
24
25
|
import { parseSchedule } from "./flow-schedule.js";
|
|
25
26
|
export const RELEASE_HELP = `toolroll release <branch> — release Toolroll itself, from a pushed branch with an open pull request
|
|
26
27
|
|
|
@@ -58,6 +59,12 @@ toolroll release nightly --repo <path> --as <you> --token <password> [--schedule
|
|
|
58
59
|
— the full release check of main's head every night, approved once with your password; it never deploys,
|
|
59
60
|
merges or completes, and a failure tells you and holds composed releases until a full check passes
|
|
60
61
|
|
|
62
|
+
toolroll release nightly --github --repo <path> --as <you> --token <password> — read GitHub's Nightly runs on main
|
|
63
|
+
for this project (its recorded GitHub remote): a lane that fails its tests holds every deploy of the project, a
|
|
64
|
+
release's merge and its tag until a later nightly passes it, or the release's own CI re-ran it after the failure and
|
|
65
|
+
passed. --lane-alias <lane-id> <new name> says a renamed job is the same lane; --lane-retire <lane-id> retires a
|
|
66
|
+
lane no required check names any more (both with your password)
|
|
67
|
+
|
|
61
68
|
toolroll release ask-every-commit on|off --repo <path> [--as <you> --token <password>] — this project's releases ask
|
|
62
69
|
for every later commit (on), or carry test-only commits again (off: your password, never the lead's token)
|
|
63
70
|
|
|
@@ -91,8 +98,8 @@ A runner's lease is returned with \`toolroll worker release <lease>\`.`;
|
|
|
91
98
|
/** A runner lease id (claim.ts mints randomUUID()). */
|
|
92
99
|
export const LEASE_SHAPED = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
93
100
|
export const LEASE_DEPRECATION = "`toolroll release <lease>` is now `toolroll worker release <lease>`; the old form goes in the next minor version.";
|
|
94
|
-
const VALUE_FLAGS = new Set(["--repo", "--limit", "--tap", "--release-gate", "--to", "--request", "--as", "--token", "--schedule", "--stage"]);
|
|
95
|
-
const BARE_FLAGS = new Set(["--new", "--json", "--help", "--yes", "--no-carry", "--start", "--recover-deploy"]);
|
|
101
|
+
const VALUE_FLAGS = new Set(["--repo", "--limit", "--tap", "--release-gate", "--to", "--request", "--as", "--token", "--schedule", "--stage", "--lane-retire"]);
|
|
102
|
+
const BARE_FLAGS = new Set(["--new", "--json", "--help", "--yes", "--no-carry", "--start", "--recover-deploy", "--github"]);
|
|
96
103
|
export async function runReleaseCommand(args, write, deps = {}) {
|
|
97
104
|
// The flag, not a secret's value that happens to read "--json".
|
|
98
105
|
// A secret is never exactly one of these names (the loop below refuses that as a missing value).
|
|
@@ -104,12 +111,21 @@ export async function runReleaseCommand(args, write, deps = {}) {
|
|
|
104
111
|
};
|
|
105
112
|
const positional = [];
|
|
106
113
|
const limits = {};
|
|
107
|
-
let stage;
|
|
114
|
+
let stage, laneRetire, laneAlias;
|
|
108
115
|
let repo, tap, gate, to, request, as, token, scheduleFlag;
|
|
109
116
|
for (let i = 0; i < args.length; i += 1) {
|
|
110
117
|
const raw = args[i];
|
|
111
118
|
if (BARE_FLAGS.has(raw))
|
|
112
119
|
continue;
|
|
120
|
+
// `--lane-alias <lane-id> <new name>`: two values, neither a flag of this command.
|
|
121
|
+
if (raw === "--lane-alias") {
|
|
122
|
+
const [id, name] = [args[i + 1], args[i + 2]];
|
|
123
|
+
if (id === undefined || name === undefined || id.startsWith("--") || name.startsWith("--"))
|
|
124
|
+
return refuse("usage", "--lane-alias takes <lane-id> <new name>.", 2);
|
|
125
|
+
laneAlias = [id, name];
|
|
126
|
+
i += 2;
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
113
129
|
// `--flag=value` for every value flag; a secret (--token) is also the next argument even when it starts with "--".
|
|
114
130
|
const equals = raw.startsWith("--") ? raw.indexOf("=") : -1;
|
|
115
131
|
const arg = equals === -1 ? raw : raw.slice(0, equals);
|
|
@@ -139,6 +155,8 @@ export async function runReleaseCommand(args, write, deps = {}) {
|
|
|
139
155
|
gate = value;
|
|
140
156
|
else if (arg === "--stage")
|
|
141
157
|
stage = value;
|
|
158
|
+
else if (arg === "--lane-retire")
|
|
159
|
+
laneRetire = value;
|
|
142
160
|
else {
|
|
143
161
|
const match = /^([a-z]+)=([0-9]+)$/.exec(value);
|
|
144
162
|
if (match === null || !(match[1] in RELEASE_LIMITS) || Number(match[2]) < 1)
|
|
@@ -169,6 +187,13 @@ export async function runReleaseCommand(args, write, deps = {}) {
|
|
|
169
187
|
return refuse("usage", "--stage goes with --recover-deploy.", 2);
|
|
170
188
|
if (bare.has("--yes"))
|
|
171
189
|
return refuse("usage", "--yes goes with --release-gate <id> or --recover-deploy; a release needs no confirmation beyond the owner's approval.", 2);
|
|
190
|
+
if (bare.has("--github") || laneAlias !== undefined || laneRetire !== undefined) {
|
|
191
|
+
if (positional[0] !== "nightly" || positional.length !== 1)
|
|
192
|
+
return refuse("usage", "--github, --lane-alias and --lane-retire go with `toolroll release nightly`.", 2);
|
|
193
|
+
if ([bare.has("--github"), laneAlias !== undefined, laneRetire !== undefined].filter(Boolean).length !== 1 || scheduleFlag !== undefined || bare.has("--start"))
|
|
194
|
+
return refuse("usage", "Use one of --github, --lane-alias or --lane-retire, without --schedule or --start.", 2);
|
|
195
|
+
return runGithubNightlyCommand({ repo: repo === undefined ? undefined : resolve(deps.cwd ?? process.cwd(), repo), as, token, alias: laneAlias, retire: laneRetire }, write, json, deps);
|
|
196
|
+
}
|
|
172
197
|
if (positional[0] === "nightly" && positional.length === 1)
|
|
173
198
|
return runNightlyCommand({ repo: repo === undefined ? undefined : resolve(deps.cwd ?? process.cwd(), repo), as, token, schedule: scheduleFlag, start: bare.has("--start") }, write, json, deps);
|
|
174
199
|
if (["carry", "revoke", "ask-every-commit", "unsandboxed-carry", "auto-approve", "publish-ok"].includes(positional[0] ?? "") && positional.length === 2) {
|
|
@@ -463,3 +488,34 @@ async function runNightlyCommand(flags, write, json, deps) {
|
|
|
463
488
|
store.close();
|
|
464
489
|
}
|
|
465
490
|
}
|
|
491
|
+
/** `toolroll release nightly --github | --lane-alias | --lane-retire`: GitHub's nightly as release debt
|
|
492
|
+
* (nightly-github.ts), each a person's step proved with their password now. */
|
|
493
|
+
async function runGithubNightlyCommand(flags, write, json, deps) {
|
|
494
|
+
const answer = (ok, fields, words, code) => { write(json ? checkedEnvelopeJson({ ok, command: "release nightly", ...fields }) : words); return code; };
|
|
495
|
+
if (flags.repo === undefined)
|
|
496
|
+
return answer(false, { reason: "usage", message: "Name the project with --repo <path>." }, "Name the project with --repo <path>.", 2);
|
|
497
|
+
if (flags.as === undefined || flags.token === undefined)
|
|
498
|
+
return answer(false, { reason: "usage", message: "This needs your password: add --as <you> --token <password>." }, "This needs your password: add --as <you> --token <password>.", 2);
|
|
499
|
+
if (machineActing() || process.env["TOOLROLL_LEAD_TOKEN"] !== undefined || process.env["TOOLROLL_REMOTE_TOKEN"] !== undefined)
|
|
500
|
+
return answer(false, { reason: "person-only", message: "Only a person changes how GitHub's nightly holds releases." }, "Only a person changes how GitHub's nightly holds releases.", 1);
|
|
501
|
+
const store = openStore(deps.database ?? databasePath(process.env, homedir()));
|
|
502
|
+
const now = (deps.now ?? (() => new Date()))();
|
|
503
|
+
try {
|
|
504
|
+
if (flags.alias !== undefined) {
|
|
505
|
+
const [laneId, name] = flags.alias;
|
|
506
|
+
const done = aliasNightlyLane(store, { repo: flags.repo, laneId, name, as: flags.as, token: flags.token }, now);
|
|
507
|
+
return done.ok ? answer(true, { lane: laneId, name }, `Jobs named "${name}" now count for lane ${laneId}.`, 0) : answer(false, { reason: "refused", message: done.message }, done.message, 1);
|
|
508
|
+
}
|
|
509
|
+
if (flags.retire !== undefined) {
|
|
510
|
+
const done = retireNightlyLane(store, { repo: flags.repo, laneId: flags.retire, as: flags.as, token: flags.token }, now);
|
|
511
|
+
return done.ok ? answer(true, { lane: flags.retire, closed: done.closed }, `Retired lane ${flags.retire}${done.closed > 0 ? `; ${done.closed} open failure${done.closed === 1 ? "" : "s"} closed` : ""}.`, 0)
|
|
512
|
+
: answer(false, { reason: "refused", message: done.message }, done.message, 1);
|
|
513
|
+
}
|
|
514
|
+
const made = optInGithubNightly(store, { repo: flags.repo, as: flags.as, token: flags.token }, now);
|
|
515
|
+
return made.ok ? answer(true, { github: made.slug, lanes: made.lanes }, `GitHub's nightly for ${made.slug} now holds this project's deploys, release merges and tags while a lane fails (${made.lanes} lanes).`, 0)
|
|
516
|
+
: answer(false, { reason: "refused", message: made.message }, made.message, 1);
|
|
517
|
+
}
|
|
518
|
+
finally {
|
|
519
|
+
store.close();
|
|
520
|
+
}
|
|
521
|
+
}
|
package/dist/release-view.d.ts
CHANGED
|
@@ -30,5 +30,8 @@ export type ReleaseView = {
|
|
|
30
30
|
outcome: string | null;
|
|
31
31
|
} | null;
|
|
32
32
|
openMainFailures: number;
|
|
33
|
+
/** GitHub's nightly (nightly-github.ts): each open lane failure, held or exempted for this release's tip, and a
|
|
34
|
+
* warning when it couldn't be read or hasn't run lately. */
|
|
35
|
+
githubNightly: string[];
|
|
33
36
|
};
|
|
34
37
|
export declare function releaseViewOf(store: Store, taskId: string, now: Date, obligations?: (authority: number) => string[]): ReleaseView | null;
|
package/dist/release-view.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* main; for a carried task, whose release approval it was carried from. Read-only, from the store.
|
|
5
5
|
*/
|
|
6
6
|
import { liveReleaseOf } from "./scope.js";
|
|
7
|
+
import { nightlyReleaseLines } from "./nightly-github.js";
|
|
7
8
|
import { releaseAuthorityOfTask, releaseCarries } from "./store.js";
|
|
8
9
|
export function releaseViewOf(store, taskId, now, obligations = () => []) {
|
|
9
10
|
const found = releaseAuthorityOfTask(store, taskId);
|
|
@@ -23,5 +24,6 @@ export function releaseViewOf(store, taskId, now, obligations = () => []) {
|
|
|
23
24
|
obligations: obligations(a.id),
|
|
24
25
|
nightly: nightly === undefined ? null : { at: String(nightly["at"]), sha: String(nightly["sha"]), outcome: nightly["outcome"] == null ? null : String(nightly["outcome"]) },
|
|
25
26
|
openMainFailures: open,
|
|
27
|
+
githubNightly: nightlyReleaseLines(store, { repo: a.repo, taskId: a.tipTask, candidate: a.tipSha, now }),
|
|
26
28
|
};
|
|
27
29
|
}
|
package/dist/release.d.ts
CHANGED
|
@@ -307,6 +307,22 @@ export type ReleaseAuthorityAdapter = {
|
|
|
307
307
|
/** Releases that approve themselves when green (docs/plans/auto-approve-green.md): the plane's own seals, each proven
|
|
308
308
|
* again inside the plane. Absent: a plane that can't, and every release asks a person as before. */
|
|
309
309
|
auto?: AutoReleaseAdapter;
|
|
310
|
+
/** GitHub's nightly lane debt (nightly-github.ts, docs/plans/evals-nightly.md part A) right before a deploy, the merge
|
|
311
|
+
* and the tag: one bounded read of GitHub (20 s), this candidate's exemptions read again, then the plane's stored
|
|
312
|
+
* debt. A warning never holds; a hold stops the release. Absent: a plane that can't read it. */
|
|
313
|
+
nightly?(input: {
|
|
314
|
+
taskId: string;
|
|
315
|
+
candidate: string;
|
|
316
|
+
pr: number;
|
|
317
|
+
phase: "deploy" | "merge" | "tag";
|
|
318
|
+
}): Promise<{
|
|
319
|
+
ok: true;
|
|
320
|
+
warning: string | null;
|
|
321
|
+
} | {
|
|
322
|
+
ok: false;
|
|
323
|
+
why: string;
|
|
324
|
+
warning: string | null;
|
|
325
|
+
}>;
|
|
310
326
|
};
|
|
311
327
|
/** The plane's automatic release approval, as the coordinator asks it. Every answer is the plane's own: the coordinator
|
|
312
328
|
* hands it ids, never evidence (the plane reads GitHub CI itself for `approve`). */
|
package/dist/release.js
CHANGED
|
@@ -570,6 +570,16 @@ export async function runRelease(options, adapters) {
|
|
|
570
570
|
if (held !== null)
|
|
571
571
|
throw new Stop("approval", `${held}. It was merged; nothing more is tagged, published or sent to Homebrew until a person lets it go on: \`toolroll release publish-ok ${j.task.id} --as <you> --token <password>\`, then rerun.`);
|
|
572
572
|
};
|
|
573
|
+
/** GitHub's nightly lane debt before a deploy, the merge or the tag (evals-nightly.md, part A2). */
|
|
574
|
+
const nightlyHolds = async (phase) => {
|
|
575
|
+
if (rel?.nightly === undefined)
|
|
576
|
+
return;
|
|
577
|
+
const answer = await rel.nightly({ taskId: j.task.id, candidate: j.candidate.sha, pr: j.pr, phase });
|
|
578
|
+
if (answer.warning !== null)
|
|
579
|
+
say(` ${answer.warning}`);
|
|
580
|
+
if (!answer.ok)
|
|
581
|
+
throw new Stop("nightly-debt", `${answer.why} Nothing was ${phase === "deploy" ? "deployed" : phase === "merge" ? "merged" : "tagged or published"}; rerun the release once it passes.`);
|
|
582
|
+
};
|
|
573
583
|
/** identity(), on a plane that keeps release authority; a legacy plane keeps the old rules. */
|
|
574
584
|
const watch = async (action) => { if (rel !== undefined)
|
|
575
585
|
await identity(action, true); };
|
|
@@ -1000,6 +1010,7 @@ export async function runRelease(options, adapters) {
|
|
|
1000
1010
|
await watch("deploy");
|
|
1001
1011
|
// Every deploy reservation, whatever the journal saved: CI must still be green on the exact commit.
|
|
1002
1012
|
await autoCiHolds("the deploy");
|
|
1013
|
+
await nightlyHolds("deploy");
|
|
1003
1014
|
const reserved = await rel.reserve({ authority: j.authority.id, kind: "deploy", taskId: j.task.id, candidate: j.candidate.sha, epoch });
|
|
1004
1015
|
if (!reserved.ok)
|
|
1005
1016
|
throw new Stop("release", `The deploy couldn't be reserved: ${reserved.why}`);
|
|
@@ -1017,6 +1028,7 @@ export async function runRelease(options, adapters) {
|
|
|
1017
1028
|
await pinned();
|
|
1018
1029
|
// Every deploy issued, whatever the journal saved (a retry after a rollback included): CI still green.
|
|
1019
1030
|
await autoCiHolds("the deploy");
|
|
1031
|
+
await nightlyHolds("deploy");
|
|
1020
1032
|
let operation, secret = "";
|
|
1021
1033
|
if (rel !== undefined && j.authority && epoch !== null) {
|
|
1022
1034
|
const reserved = await rel.reserve({ authority: j.authority.id, kind: "deploy", taskId: j.task.id, candidate: j.candidate.sha, epoch });
|
|
@@ -1110,6 +1122,9 @@ export async function runRelease(options, adapters) {
|
|
|
1110
1122
|
const verdict = prChecksVerdict(await adapters.github.checks(j.github, j.candidate.sha), j.candidate.sha, REQUIRED_PR_CHECKS, { ...autoCap(), workflow: REQUIRED_PR_CHECKS_WORKFLOW });
|
|
1111
1123
|
if (verdict.state !== "passed")
|
|
1112
1124
|
await ciFailedAfterDeploy(`Pull request checks are no longer all passing on ${j.candidate.sha.slice(0, 7)}`);
|
|
1125
|
+
// GitHub's nightly lane debt (a bounded read), before main's last read so it doesn't widen that window; the
|
|
1126
|
+
// reservation below checks the stored debt again in its own transaction.
|
|
1127
|
+
await nightlyHolds("merge");
|
|
1113
1128
|
// Main once more, from GitHub, the last thing before the merge is reserved: moved since the comparison, it
|
|
1114
1129
|
// stops here (finding 4). GitHub's own up-to-date rule refuses the rest of the window.
|
|
1115
1130
|
const now = await adapters.github.mainHead(j.github);
|
|
@@ -1166,6 +1181,7 @@ export async function runRelease(options, adapters) {
|
|
|
1166
1181
|
throw new Stop("conflict", `${j.tag} already points at ${at.slice(0, 7)}, not the merge ${merge.sha.slice(0, 7)}.`);
|
|
1167
1182
|
if (at === null) {
|
|
1168
1183
|
await autoCiHolds("the tag");
|
|
1184
|
+
await nightlyHolds("tag");
|
|
1169
1185
|
intend("tag");
|
|
1170
1186
|
await adapters.github.createTag(j.github, j.tag, merge.sha);
|
|
1171
1187
|
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { Billing } from "./spend.js";
|
|
2
|
+
/** Several reported models are kept as their sorted ids joined by this (it has spaces, so no one id can equal it):
|
|
3
|
+
* a run whose calls ran different models never reads as any one of them. */
|
|
4
|
+
export declare const MODELS_JOIN = " + ";
|
|
5
|
+
export declare function reportedModelOf(value: unknown): string | null;
|
|
6
|
+
/** The one model a Claude result's `modelUsage` names; null when it names none, several, or anything malformed. */
|
|
7
|
+
export declare function soleReportedModel(modelUsage: unknown): string | null;
|
|
8
|
+
/** Two reports of the same piece of work (a resumed turn, a second init): unknown if either is, the model when they
|
|
9
|
+
* agree, otherwise every model named, sorted (never one chosen). */
|
|
10
|
+
export declare function combineReportedModels(a: string | null, b: string | null): string | null;
|
|
11
|
+
export declare function providerTurnsOf(value: unknown): number | null;
|
|
12
|
+
export declare function reportedCostUsdOf(value: unknown): number | null;
|
|
13
|
+
/** One provider call's own report. `tokensIn` is everything the model read (cached included) when the provider
|
|
14
|
+
* reported every part of it (run-efficiency.ts usageOf), else null. */
|
|
15
|
+
export type ProviderReport = {
|
|
16
|
+
modelReported: string | null;
|
|
17
|
+
providerTurns: number | null;
|
|
18
|
+
reportedCostUsd: number | null;
|
|
19
|
+
tokensIn: number | null;
|
|
20
|
+
cacheReadTokens: number | null;
|
|
21
|
+
cacheWriteTokens: number | null;
|
|
22
|
+
};
|
|
23
|
+
/** What the session's own `system`/`init` events named: the one model they agree on, or that they disagree. */
|
|
24
|
+
export type ClaudeInitModel = {
|
|
25
|
+
model: string | null;
|
|
26
|
+
conflict: boolean;
|
|
27
|
+
};
|
|
28
|
+
/** A Claude primary `result` (streamed or buffered `--output-format json`), with what the session's inits named. The
|
|
29
|
+
* init's model is the session's own; inits that disagree leave it unknown; with no init naming one, the result's
|
|
30
|
+
* `modelUsage` counts only when it names exactly one model. */
|
|
31
|
+
export declare function claudeResultReport(result: Record<string, unknown>, init?: ClaudeInitModel): ProviderReport;
|
|
32
|
+
/** Codex's `turn.completed` usage: its input already counts what came from cache; it reports no cache writes, no
|
|
33
|
+
* model and no cost in `exec --json`, so those stay unknown. */
|
|
34
|
+
export declare function codexUsageReport(usage: unknown): ProviderReport;
|
|
35
|
+
/** One of the three amounts, never added to another. */
|
|
36
|
+
export type ReportedAmount = {
|
|
37
|
+
kind: "api";
|
|
38
|
+
microusd: number;
|
|
39
|
+
}
|
|
40
|
+
/** The provider's own reported cost of work a subscription covers: usage, not billed (null: it reported none). */
|
|
41
|
+
| {
|
|
42
|
+
kind: "subscription";
|
|
43
|
+
reportedUsd: number | null;
|
|
44
|
+
} | {
|
|
45
|
+
kind: "unknown";
|
|
46
|
+
};
|
|
47
|
+
/** A run's amount, from its fixed billing and its priced spend (run_spend). Subscription work reads its provider's
|
|
48
|
+
* reported cost as usage, never as dollars; billed work without a reported or priced cost is unknown. v141 (Codex
|
|
49
|
+
* round 1 #4, #10): billing this computer's default guessed (`basis` not evidence) is unknown, and so is a run whose
|
|
50
|
+
* cost the provider never reported (its `cost_usd` then holds a conservative charge, never a measurement). */
|
|
51
|
+
export declare function runAmountOf(run: {
|
|
52
|
+
billing: Billing | null;
|
|
53
|
+
spendMicrousd: number | null;
|
|
54
|
+
costUsd: number | null;
|
|
55
|
+
basis?: "evidence" | "default" | null;
|
|
56
|
+
costUnknown?: boolean;
|
|
57
|
+
}): ReportedAmount;
|
|
58
|
+
/** A chat request's amount: a subscription chat's reported cost is usage; an API chat's settled charge is dollars,
|
|
59
|
+
* unless its spend is unknown (then the amount is, whatever was settled against the reservation). v141 (Codex round 1
|
|
60
|
+
* #4): a subscription harness's request is subscription usage only when its own events said it was billed to the plan
|
|
61
|
+
* (`billingReported`); one they said used a key is API dollars at its reported cost; one they said nothing of is
|
|
62
|
+
* unknown. Absent `billingReported` keeps the older reading for callers that predate it. */
|
|
63
|
+
export declare function chatTurnAmountOf(turn: {
|
|
64
|
+
provider: string;
|
|
65
|
+
settledMicrousd: number | null;
|
|
66
|
+
unknownSpend: boolean;
|
|
67
|
+
reportedCostUsd: number | null;
|
|
68
|
+
billingReported?: "subscription" | "api-key" | null;
|
|
69
|
+
}): ReportedAmount;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a provider itself reported about one call (docs/plans/evals-nightly.md, part B1): the exact model, its turn
|
|
3
|
+
* count, its reported cost and its cache usage. Read only from the provider's own events and response bodies (Claude's
|
|
4
|
+
* session `system`/`init` and primary `result`, Codex's `turn.completed`, an API's response body), never from text an
|
|
5
|
+
* agent or a candidate wrote. Anything unreported or malformed is unknown (null), never 0.
|
|
6
|
+
*
|
|
7
|
+
* Three amounts are kept apart and never summed into one number: API dollars (work billed by use, with a reported or
|
|
8
|
+
* priced cost), subscription usage (the provider's own reported cost of work a plan covers: not billed), and unknown.
|
|
9
|
+
*/
|
|
10
|
+
import { usageOf } from "./run-efficiency.js";
|
|
11
|
+
/** A model id as a provider reports it: printable ASCII with no spaces, at most 200 characters. Anything else is
|
|
12
|
+
* unknown, never trimmed into something that could read as a different model. */
|
|
13
|
+
const MODEL_ID = /^[\x21-\x7e]{1,200}$/;
|
|
14
|
+
/** Several reported models are kept as their sorted ids joined by this (it has spaces, so no one id can equal it):
|
|
15
|
+
* a run whose calls ran different models never reads as any one of them. */
|
|
16
|
+
export const MODELS_JOIN = " + ";
|
|
17
|
+
export function reportedModelOf(value) {
|
|
18
|
+
return typeof value === "string" && MODEL_ID.test(value) ? value : null;
|
|
19
|
+
}
|
|
20
|
+
/** The one model a Claude result's `modelUsage` names; null when it names none, several, or anything malformed. */
|
|
21
|
+
export function soleReportedModel(modelUsage) {
|
|
22
|
+
if (typeof modelUsage !== "object" || modelUsage === null || Array.isArray(modelUsage))
|
|
23
|
+
return null;
|
|
24
|
+
const keys = Object.keys(modelUsage);
|
|
25
|
+
return keys.length === 1 ? reportedModelOf(keys[0]) : null;
|
|
26
|
+
}
|
|
27
|
+
/** Two reports of the same piece of work (a resumed turn, a second init): unknown if either is, the model when they
|
|
28
|
+
* agree, otherwise every model named, sorted (never one chosen). */
|
|
29
|
+
export function combineReportedModels(a, b) {
|
|
30
|
+
if (a === null || b === null)
|
|
31
|
+
return null;
|
|
32
|
+
if (a === b)
|
|
33
|
+
return a;
|
|
34
|
+
return [...new Set([...a.split(MODELS_JOIN), ...b.split(MODELS_JOIN)])].sort().join(MODELS_JOIN);
|
|
35
|
+
}
|
|
36
|
+
export function providerTurnsOf(value) {
|
|
37
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : null;
|
|
38
|
+
}
|
|
39
|
+
export function reportedCostUsdOf(value) {
|
|
40
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : null;
|
|
41
|
+
}
|
|
42
|
+
/** A Claude primary `result` (streamed or buffered `--output-format json`), with what the session's inits named. The
|
|
43
|
+
* init's model is the session's own; inits that disagree leave it unknown; with no init naming one, the result's
|
|
44
|
+
* `modelUsage` counts only when it names exactly one model. */
|
|
45
|
+
export function claudeResultReport(result, init = { model: null, conflict: false }) {
|
|
46
|
+
const usage = usageOf({ provider: "claude", model: null, tokensIn: null, tokensOut: null, usageJson: usageJsonOf(result["usage"]) });
|
|
47
|
+
return {
|
|
48
|
+
modelReported: init.conflict ? null : init.model ?? soleReportedModel(result["modelUsage"]),
|
|
49
|
+
providerTurns: providerTurnsOf(result["num_turns"]),
|
|
50
|
+
reportedCostUsd: reportedCostUsdOf(result["total_cost_usd"]),
|
|
51
|
+
tokensIn: usage.in,
|
|
52
|
+
cacheReadTokens: usage.cacheRead,
|
|
53
|
+
cacheWriteTokens: usage.cacheWrite,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/** Codex's `turn.completed` usage: its input already counts what came from cache; it reports no cache writes, no
|
|
57
|
+
* model and no cost in `exec --json`, so those stay unknown. */
|
|
58
|
+
export function codexUsageReport(usage) {
|
|
59
|
+
const read = usageOf({ provider: "codex", model: null, tokensIn: null, tokensOut: null, usageJson: usageJsonOf(usage) });
|
|
60
|
+
return { modelReported: null, providerTurns: null, reportedCostUsd: null, tokensIn: read.in, cacheReadTokens: read.cacheRead, cacheWriteTokens: null };
|
|
61
|
+
}
|
|
62
|
+
function usageJsonOf(usage) {
|
|
63
|
+
return typeof usage === "object" && usage !== null && !Array.isArray(usage) ? JSON.stringify(usage) : null;
|
|
64
|
+
}
|
|
65
|
+
/** A run's amount, from its fixed billing and its priced spend (run_spend). Subscription work reads its provider's
|
|
66
|
+
* reported cost as usage, never as dollars; billed work without a reported or priced cost is unknown. v141 (Codex
|
|
67
|
+
* round 1 #4, #10): billing this computer's default guessed (`basis` not evidence) is unknown, and so is a run whose
|
|
68
|
+
* cost the provider never reported (its `cost_usd` then holds a conservative charge, never a measurement). */
|
|
69
|
+
export function runAmountOf(run) {
|
|
70
|
+
if (run.basis !== undefined && run.basis !== "evidence")
|
|
71
|
+
return { kind: "unknown" };
|
|
72
|
+
if (run.costUnknown === true)
|
|
73
|
+
return { kind: "unknown" };
|
|
74
|
+
if (run.billing === "subscription")
|
|
75
|
+
return { kind: "subscription", reportedUsd: reportedCostUsdOf(run.costUsd) };
|
|
76
|
+
if (run.billing === "api-key" && run.spendMicrousd !== null && Number.isSafeInteger(run.spendMicrousd) && run.spendMicrousd >= 0) {
|
|
77
|
+
return { kind: "api", microusd: run.spendMicrousd };
|
|
78
|
+
}
|
|
79
|
+
return { kind: "unknown" };
|
|
80
|
+
}
|
|
81
|
+
const SUBSCRIPTION_CHAT = new Set(["claude-subscription", "codex-subscription"]);
|
|
82
|
+
/** A chat request's amount: a subscription chat's reported cost is usage; an API chat's settled charge is dollars,
|
|
83
|
+
* unless its spend is unknown (then the amount is, whatever was settled against the reservation). v141 (Codex round 1
|
|
84
|
+
* #4): a subscription harness's request is subscription usage only when its own events said it was billed to the plan
|
|
85
|
+
* (`billingReported`); one they said used a key is API dollars at its reported cost; one they said nothing of is
|
|
86
|
+
* unknown. Absent `billingReported` keeps the older reading for callers that predate it. */
|
|
87
|
+
export function chatTurnAmountOf(turn) {
|
|
88
|
+
if (SUBSCRIPTION_CHAT.has(turn.provider)) {
|
|
89
|
+
if (turn.billingReported === undefined || turn.billingReported === "subscription")
|
|
90
|
+
return { kind: "subscription", reportedUsd: reportedCostUsdOf(turn.reportedCostUsd) };
|
|
91
|
+
const cost = reportedCostUsdOf(turn.reportedCostUsd);
|
|
92
|
+
return turn.billingReported === "api-key" && cost !== null ? { kind: "api", microusd: Math.round(cost * 1_000_000) } : { kind: "unknown" };
|
|
93
|
+
}
|
|
94
|
+
if (turn.unknownSpend || turn.settledMicrousd === null || !Number.isSafeInteger(turn.settledMicrousd) || turn.settledMicrousd < 0)
|
|
95
|
+
return { kind: "unknown" };
|
|
96
|
+
return { kind: "api", microusd: turn.settledMicrousd };
|
|
97
|
+
}
|
package/dist/repos.d.ts
CHANGED
|
@@ -57,4 +57,11 @@ export declare function updateProjectRegistry(file: string, transform: (registry
|
|
|
57
57
|
} | RegistryUpdateFailure>;
|
|
58
58
|
export declare function addRepos(existing: readonly string[], incoming: readonly string[]): string[];
|
|
59
59
|
export declare function removeRepos(existing: readonly string[], targets: readonly string[]): string[];
|
|
60
|
+
/** Beside the saved list: how many times each line has been removed from it (written under its lock). */
|
|
61
|
+
export declare function registryRemovalsFile(file: string): string;
|
|
62
|
+
/**
|
|
63
|
+
* How many times each saved line has been removed, as recorded under the saved list's lock. A file that can't be read
|
|
64
|
+
* as one answers null: its caller treats every line as changed (fail closed).
|
|
65
|
+
*/
|
|
66
|
+
export declare function registryRemovals(file: string): Record<string, number> | null;
|
|
60
67
|
export {};
|
package/dist/repos.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* to edit it by hand or check it into their dotfiles — which also means it is
|
|
11
11
|
* untrusted input and gets validated like any other.
|
|
12
12
|
*/
|
|
13
|
+
import { closeSync as closeFd, constants as fsConstants, fstatSync, openSync as openFd, readSync, renameSync as renameFile, writeFileSync } from "node:fs";
|
|
13
14
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
14
15
|
import { dirname } from "node:path";
|
|
15
16
|
import { configBase, namedPath } from "./names.js";
|
|
@@ -213,6 +214,11 @@ export async function updateProjectRegistry(file, transform) {
|
|
|
213
214
|
held = false; // abandon: no unlink, no publication
|
|
214
215
|
return { ok: false, reason: "abandoned", message: "the registry lock aged out before the update could publish — nothing changed; try again" };
|
|
215
216
|
}
|
|
217
|
+
// Each line this update removes counts one more removal, written BEFORE the list is published: whoever holds a
|
|
218
|
+
// start's authority for a saved line (upDeferredRetry) sees that it was removed, even if it was saved again since.
|
|
219
|
+
const gone = current.repos.filter(one => !next.repos.includes(one));
|
|
220
|
+
if (gone.length > 0)
|
|
221
|
+
bumpRegistryRemovals(file, gone, `${process.pid}.${token.slice(0, 8)}`);
|
|
216
222
|
const temp = `${file}.${process.pid}.${token.slice(0, 8)}.tmp`;
|
|
217
223
|
const fd = openSync(temp, "wx");
|
|
218
224
|
writeSync(fd, `${JSON.stringify({ version: CONFIG_VERSION, ...next }, null, 2)}
|
|
@@ -267,3 +273,54 @@ function readProjectRegistry(parsed) {
|
|
|
267
273
|
function sortUnique(repos) {
|
|
268
274
|
return [...new Set(repos)].sort();
|
|
269
275
|
}
|
|
276
|
+
/** Beside the saved list: how many times each line has been removed from it (written under its lock). */
|
|
277
|
+
export function registryRemovalsFile(file) {
|
|
278
|
+
return `${file}.removals.json`;
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* How many times each saved line has been removed, as recorded under the saved list's lock. A file that can't be read
|
|
282
|
+
* as one answers null: its caller treats every line as changed (fail closed).
|
|
283
|
+
*/
|
|
284
|
+
export function registryRemovals(file) {
|
|
285
|
+
let fd;
|
|
286
|
+
try {
|
|
287
|
+
fd = openFd(registryRemovalsFile(file), fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW | fsConstants.O_NONBLOCK);
|
|
288
|
+
}
|
|
289
|
+
catch (error) {
|
|
290
|
+
return error.code === "ENOENT" ? {} : null;
|
|
291
|
+
}
|
|
292
|
+
try {
|
|
293
|
+
const held = fstatSync(fd);
|
|
294
|
+
if (!held.isFile() || held.size > 4 * 1024 * 1024)
|
|
295
|
+
return null;
|
|
296
|
+
const buffer = Buffer.alloc(held.size);
|
|
297
|
+
const read = held.size === 0 ? 0 : readSync(fd, buffer, 0, buffer.length, 0);
|
|
298
|
+
const parsed = JSON.parse(buffer.toString("utf8", 0, read));
|
|
299
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
|
|
300
|
+
return null;
|
|
301
|
+
const out = {};
|
|
302
|
+
for (const [line, count] of Object.entries(parsed)) {
|
|
303
|
+
if (!Number.isSafeInteger(count) || count < 0)
|
|
304
|
+
return null;
|
|
305
|
+
out[line] = count;
|
|
306
|
+
}
|
|
307
|
+
return out;
|
|
308
|
+
}
|
|
309
|
+
catch {
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
finally {
|
|
313
|
+
closeFd(fd);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
/** One more removal for each of `lines`, written whole and renamed into place. Called only under the list's lock. */
|
|
317
|
+
function bumpRegistryRemovals(file, lines, tag) {
|
|
318
|
+
const kept = registryRemovals(file);
|
|
319
|
+
// An unreadable record starts again from the clock, so no count read before can come round again.
|
|
320
|
+
const counts = kept ?? {};
|
|
321
|
+
for (const line of lines)
|
|
322
|
+
counts[line] = kept === null ? Date.now() : (counts[line] ?? 0) + 1;
|
|
323
|
+
const temp = `${registryRemovalsFile(file)}.${tag}.tmp`;
|
|
324
|
+
writeFileSync(temp, `${JSON.stringify(counts)}\n`, { flag: "wx", mode: 0o600 });
|
|
325
|
+
renameFile(temp, registryRemovalsFile(file));
|
|
326
|
+
}
|
package/dist/serve.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { secretFolder } from "./tool-secret-store.js";
|
|
2
2
|
import { acceptedIdentities, recoverLearning } from "./project-learning.js";
|
|
3
|
+
import { syncProjectIdentities } from "./project-identity.js";
|
|
4
|
+
import { firstTasksOffThread, recoverLearningOffThread } from "./learning-look.js";
|
|
3
5
|
import { loadOrCreateVapidKeys } from "./push.js";
|
|
4
6
|
import { checkoutPlan } from "./checkout-cleanup.js";
|
|
5
7
|
import { latestReleaseFor } from "./server/render-settings.js";
|
|
@@ -38,7 +40,8 @@ import { asReadOnlyRequest } from './read-only-request.js';
|
|
|
38
40
|
import { connectionsOf, localConnectOf, oneClickOf } from "./mcp-connect.js";
|
|
39
41
|
import { MOBILE_VIEWPORT_SCRIPT } from "./mobile-viewport.js";
|
|
40
42
|
import { projectAuthority } from "./project-access.js";
|
|
41
|
-
import { discoverTools, projectToolsOf, secretsSetFor, TOOL_CATALOG, toolSecretStorage, toolStanding } from "./project-tools.js";
|
|
43
|
+
import { discoverTools, projectToolsOf, savedProjectMcp, secretsSetFor, TOOL_CATALOG, toolSecretStorage, toolStanding } from "./project-tools.js";
|
|
44
|
+
import { lookProjectMcp } from "./folder-look.js";
|
|
42
45
|
import { toolRefusal } from "./policy.js";
|
|
43
46
|
import { isEdgeAddress, refuseEdge } from "./server/edge-refusal.js";
|
|
44
47
|
import { announcePasswordBearer, isPasswordBearer, PASSWORD_BEARER_REFUSED, passwordBearerAccepted } from "./password-bearer.js";
|
|
@@ -166,6 +169,9 @@ import { loadConsoleUrl } from "./webhooks.js";
|
|
|
166
169
|
import { reviewFactsOf } from "./workspace-ui.js";
|
|
167
170
|
import { WorktreePool } from "./worktree.js";
|
|
168
171
|
export function createDecisionServer(options) {
|
|
172
|
+
// Identities come from what the console's start resolved off its thread (startConsole); a path that didn't answer
|
|
173
|
+
// stays unresolved, kept restrictive. Without them (embedders, tests) the old synchronous reads apply.
|
|
174
|
+
const identities = options.identities ?? syncProjectIdentities();
|
|
169
175
|
const { projectFamilyPeek, familiesInView, familyTasksInView, revisionDestination, runIsLive, runIsTaskResult, revisionViewOf, taskViewData, routeViewOf, taskChatFocus, pullRequestTargetOf, failureOf, explainAttempt, taskScreen, armTaskResume, planViewOf, planContractViewOf, revisionDocOf, revisionLedgerOf, resultDetailOf } = createTaskViews({
|
|
170
176
|
get store() { return store; },
|
|
171
177
|
get projectCounts() { return projectCounts; },
|
|
@@ -188,6 +194,7 @@ export function createDecisionServer(options) {
|
|
|
188
194
|
const { authenticateApprover, authenticateCredential, authorizeMutation, projectOf, projectRequestAllowed, identify, lookupSession, restricted, visible, admissionList, codingActorAllowed, codingProjectAllowed, workAccess, familyOf, taskRepoOf, runVisible } = createSharedGuards({
|
|
189
195
|
get store() { return store; },
|
|
190
196
|
get liveCeiling() { return liveCeiling; },
|
|
197
|
+
identities,
|
|
191
198
|
get codingProjects() { return codingProjects; },
|
|
192
199
|
get unscopedMode() { return unscopedMode; },
|
|
193
200
|
get managedRepos() { return managedRepos; },
|
|
@@ -448,18 +455,20 @@ export function createDecisionServer(options) {
|
|
|
448
455
|
const approvalNonces = new Map();
|
|
449
456
|
// Resolve startup authority once. Missing paths remain restrictive.
|
|
450
457
|
// Native additions come only from the controller after its admission checks.
|
|
451
|
-
const { ceiling, unresolved: unresolvedRepos } = resolveCeiling([...(options.repo === undefined ? [] : [options.repo]), ...(options.repos ?? [])], options.projectRoots ?? []);
|
|
458
|
+
const { ceiling: startCeiling, unresolved: unresolvedRepos } = resolveCeiling([...(options.repo === undefined ? [] : [options.repo]), ...(options.repos ?? [])], options.projectRoots ?? [], options.identities === undefined ? undefined : identities.known);
|
|
459
|
+
const ceiling = { ...startCeiling, canon: identities.strict };
|
|
452
460
|
/** The project every fresh session opens with: the sole configured repo, else none. */
|
|
453
461
|
const defaultProject = ceiling.repos.length === 1 && ceiling.roots.length === 0 ? ceiling.repos[0] : null;
|
|
454
462
|
/** No ceiling configured at all: the legacy trust-everything mode, named. */
|
|
455
|
-
|
|
463
|
+
// An `up` console (it is handed its admitted set) is never unscoped: with every saved project away it is empty.
|
|
464
|
+
const unscopedMode = ceiling.repos.length === 0 && ceiling.roots.length === 0 && options.admittedRepos === undefined;
|
|
456
465
|
/** Per-row visibility under the ceiling — the authorization question for reads. */
|
|
457
466
|
/** The exact repositories this console serves now: `up`'s live admitted set when it supplies one, else startup's. */
|
|
458
467
|
const exactRepos = () => options.admittedRepos?.() ?? ceiling.repos;
|
|
459
468
|
const liveCeiling = () => {
|
|
460
469
|
const repos = [...exactRepos(), ...(options.additionalProjectRepos?.() ?? [])];
|
|
461
470
|
// An admitted set that is empty for now is an empty ceiling, never the legacy unscoped mode.
|
|
462
|
-
return { repos: repos.length === 0 && ceiling.roots.length === 0 && !unscopedMode ? [NO_PROJECT] : repos, roots: ceiling.roots };
|
|
471
|
+
return { repos: repos.length === 0 && ceiling.roots.length === 0 && !unscopedMode ? [NO_PROJECT] : repos, roots: ceiling.roots, canon: identities.strict };
|
|
463
472
|
};
|
|
464
473
|
/** The explicit, currently proved project list. The callback is supplied
|
|
465
474
|
* only by `up`, after it has independently checked git-ness and the root
|
|
@@ -479,7 +488,7 @@ export function createDecisionServer(options) {
|
|
|
479
488
|
const repos = [];
|
|
480
489
|
const gone = goneRepos();
|
|
481
490
|
for (const path of [...exactRepos(), ...(options.currentRepos?.() ?? [])]) {
|
|
482
|
-
const canonical =
|
|
491
|
+
const canonical = identities.hot(path);
|
|
483
492
|
if (seen.has(canonical) || !visible(canonical) || gone.has(canonical))
|
|
484
493
|
continue;
|
|
485
494
|
seen.add(canonical);
|
|
@@ -568,7 +577,8 @@ export function createDecisionServer(options) {
|
|
|
568
577
|
// A service that connects by signing in (or an app's own Connect) is offered only that way.
|
|
569
578
|
catalog: TOOL_CATALOG.filter(one => !names.has(one.name) && oneClickOf(one.name) === null && localConnectOf(one.name) === null),
|
|
570
579
|
connections: connectionsOf(store, repo, toolHome), kit, wanted,
|
|
571
|
-
|
|
580
|
+
// The project's own .mcp.json as the background pass last read it off the console's thread (never read here).
|
|
581
|
+
found: discoverTools(repo, codex, toolHome, process.env, options.identities === undefined ? undefined : savedProjectMcp.get(repo) ?? null).filter(one => !names.has(one.spec.name)),
|
|
572
582
|
storage: toolSecretStorage(),
|
|
573
583
|
};
|
|
574
584
|
};
|
|
@@ -1027,7 +1037,7 @@ export function createDecisionServer(options) {
|
|
|
1027
1037
|
}
|
|
1028
1038
|
const known = new Set(["/recipes/prepare", "/recipes/preview", "/recipes/import", "/recipes/save", "/recipes/launch", "/tasks/add", "/queue/move", "/queue/note", "/people/invite", "/people/invite-revoke", "/people/revoke", "/people/projects", "/projects/select", "/projects/open", "/mode/confirm", "/mode/sign", "/mode/revoke", "/settings/chat-approval/save", "/settings/chat-approval/off", "/settings/project/live-view"]);
|
|
1029
1039
|
const placed = url.pathname === "/tasks/add" ? body?.get("repo")?.trim() : null;
|
|
1030
|
-
return { repo: url.pathname.startsWith("/people/") ? null : placed ?
|
|
1040
|
+
return { repo: url.pathname.startsWith("/people/") ? null : placed ? identities.strict(placed) ?? placed : projectOf(who, request) ?? null, taskId: null, runId: null,
|
|
1031
1041
|
action: known.has(url.pathname) ? url.pathname.slice(1).replaceAll("/", " ") : "console request" };
|
|
1032
1042
|
}
|
|
1033
1043
|
async function handleGet(route, url, who, request, response) {
|
|
@@ -1955,6 +1965,8 @@ export function createDecisionServer(options) {
|
|
|
1955
1965
|
for (const provider of PROVIDER_IDS.filter(isKeyedProvider).filter(one => one !== "claude" && one !== "codex"))
|
|
1956
1966
|
waits.push(connectionCheck(provider).catch(() => undefined));
|
|
1957
1967
|
}
|
|
1968
|
+
// Project identities, resolved off the thread: what pages asked about, and answers gone stale.
|
|
1969
|
+
waits.push(identities.refresh([...exactRepos(), ...(options.currentRepos?.() ?? []), ...store.listProjects().map(one => one.path), ...store.knownRepos(), ...ceiling.roots]));
|
|
1958
1970
|
integrations.refreshIfDue();
|
|
1959
1971
|
const sso = ssoSettings();
|
|
1960
1972
|
if (sso !== null && (ssoProvider === null || ssoProvider.issuer !== sso.issuer || Date.now() - ssoProvider.at >= 10 * 60_000))
|
|
@@ -1991,13 +2003,22 @@ export function createDecisionServer(options) {
|
|
|
1991
2003
|
continue;
|
|
1992
2004
|
const reading = { until: Date.now() + 10 * 60_000, found: entry?.found ?? null, reading: true };
|
|
1993
2005
|
firstTasks.set(repo, reading);
|
|
1994
|
-
|
|
2006
|
+
// A test's runner reads in-process; otherwise in a process of its own (learning-look.ts), so the hardened
|
|
2007
|
+
// git's synchronous reads of the project's folder never hold the console's thread.
|
|
2008
|
+
waits.push((options.firstTaskRunner !== undefined ? findFirstTasks(repo, options.firstTaskRunner) : firstTasksOffThread(repo)).then(found => { reading.found = found; }, () => { }).finally(() => { reading.reading = false; }));
|
|
1995
2009
|
}
|
|
1996
2010
|
}
|
|
1997
2011
|
// Learning recovered from reviewed runs (and a project's saved rows carried to its current identity), which
|
|
1998
2012
|
// Settings → Learning and Knowledge used to do as they opened.
|
|
1999
2013
|
if (lastChecks.due("learning", 10 * 60_000)) {
|
|
2000
2014
|
waits.push(lastChecks.run("learning", async () => {
|
|
2015
|
+
// Its reads of each project's Git folder are synchronous, so with a database file they run in a process of
|
|
2016
|
+
// their own (learning-look.ts): a folder that doesn't answer costs its own recovery, never the console. An
|
|
2017
|
+
// in-memory database (a test) can't be shared, so it is read here.
|
|
2018
|
+
if (databaseFile !== null) {
|
|
2019
|
+
await recoverLearningOffThread({ databaseFile, evidenceRoot, repos: managedRepos() }, options.learningRunner === undefined ? {} : { runner: options.learningRunner });
|
|
2020
|
+
return;
|
|
2021
|
+
}
|
|
2001
2022
|
for (const repo of managedRepos()) {
|
|
2002
2023
|
try {
|
|
2003
2024
|
acceptedIdentities(store, repo);
|
|
@@ -2012,6 +2033,11 @@ export function createDecisionServer(options) {
|
|
|
2012
2033
|
}
|
|
2013
2034
|
// What Codex has configured, per project, for Settings → Tools to offer.
|
|
2014
2035
|
for (const repo of managedRepos()) {
|
|
2036
|
+
// The project's own .mcp.json, read off the console's thread (folder-look.ts) for the Tools page and the lead.
|
|
2037
|
+
if (options.identities !== undefined && lastChecks.due(`project-mcp:${repo}`, 10 * 60_000)) {
|
|
2038
|
+
waits.push(lastChecks.run(`project-mcp:${repo}`, async () => { const looked = await lookProjectMcp(repo); if (looked.ok)
|
|
2039
|
+
savedProjectMcp.set(repo, looked.value); return looked.ok; }));
|
|
2040
|
+
}
|
|
2015
2041
|
if (lastChecks.due(`codex-tools:${repo}`, 10 * 60_000))
|
|
2016
2042
|
waits.push(lastChecks.run(`codex-tools:${repo}`, () => codexServers(repo)));
|
|
2017
2043
|
}
|
|
@@ -2220,6 +2246,7 @@ export function createDecisionServer(options) {
|
|
|
2220
2246
|
get revisionLedgerOf() { return revisionLedgerOf; },
|
|
2221
2247
|
get firstTasks() { return firstTasks; },
|
|
2222
2248
|
get authenticateCredential() { return authenticateCredential; },
|
|
2249
|
+
get identities() { return identities; },
|
|
2223
2250
|
};
|
|
2224
2251
|
const tasksHandlers = createTasksHandlers(handlerRuntime);
|
|
2225
2252
|
const pagesHandlers = createPagesHandlers(handlerRuntime);
|