toolroll 0.9.92 → 0.9.93
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 +19 -0
- package/dist/assignment-status.d.ts +1 -1
- package/dist/assignment-ui.js +2 -2
- package/dist/assignment.js +11 -3
- package/dist/browser/workspace.css +1 -1
- package/dist/browser/workspace.js +30 -30
- package/dist/browser-crew.js +2 -2
- package/dist/browser-workspace.d.ts +3 -5
- package/dist/builder.js +21 -2
- package/dist/check-facts.d.ts +124 -0
- package/dist/check-facts.js +272 -0
- package/dist/check-report.d.ts +5 -0
- package/dist/check-report.js +4 -3
- package/dist/cli.js +5 -0
- package/dist/contracts/console-api.d.ts +2 -2
- package/dist/contracts/console-api.js +2 -2
- package/dist/contracts/release-check.d.ts +25 -1
- package/dist/contracts/release-check.js +19 -0
- package/dist/desktop-update.d.ts +2 -2
- package/dist/desktop-update.js +13 -9
- package/dist/eval-hooks.d.ts +55 -0
- package/dist/eval-hooks.js +227 -4
- package/dist/flaky.d.ts +17 -0
- package/dist/flaky.js +9 -0
- package/dist/needs-you.d.ts +7 -7
- package/dist/needs-you.js +12 -10
- package/dist/operate.js +138 -18
- package/dist/project-delete.js +5 -1
- package/dist/project-knowledge.d.ts +6 -6
- package/dist/push.d.ts +10 -3
- package/dist/push.js +22 -6
- package/dist/release-adapters.js +11 -1
- package/dist/release-approval.d.ts +191 -0
- package/dist/release-approval.js +382 -0
- package/dist/release-auto.js +13 -4
- package/dist/release-card.d.ts +73 -4
- package/dist/release-card.js +168 -23
- package/dist/release-cli.js +9 -1
- package/dist/release.d.ts +30 -0
- package/dist/release.js +42 -3
- package/dist/result-acts.d.ts +3 -3
- package/dist/result-acts.js +1 -1
- package/dist/server/guards.js +1 -0
- package/dist/server/pages-assets.js +1 -1
- package/dist/server/render-release.d.ts +17 -0
- package/dist/server/render-release.js +84 -0
- package/dist/server/render-results.d.ts +0 -2
- package/dist/server/render-results.js +4 -6
- package/dist/server/render-tasks.js +3 -3
- package/dist/server/route-table.d.ts +1 -1
- package/dist/server/route-table.js +7 -1
- package/dist/server/tasks.d.ts +15 -0
- package/dist/server/tasks.js +102 -20
- package/dist/setup-guide.d.ts +2 -2
- package/dist/stop-proof.d.ts +67 -0
- package/dist/stop-proof.js +293 -0
- package/dist/store.d.ts +70 -31
- package/dist/store.js +335 -95
- package/dist/surface.js +1 -1
- package/dist/task-outcome-cli.js +19 -0
- package/dist/toolroll-update-ui.js +2 -2
- package/dist/toolroll-update.js +1 -0
- package/dist/work-index.js +7 -9
- package/dist/work-summary.d.ts +2 -2
- package/package.json +1 -1
package/dist/browser-crew.js
CHANGED
|
@@ -15,8 +15,8 @@ export function browserWorkActionHref(item) {
|
|
|
15
15
|
return '/settings/agents#providers';
|
|
16
16
|
if (action.code === 'answer-agent' && runId !== null)
|
|
17
17
|
return `/agents?run=${runId}`;
|
|
18
|
-
if (action.code === '
|
|
19
|
-
return `/t/${encodeURIComponent(item.rootId)}#
|
|
18
|
+
if (action.code === 'recheck-stopped')
|
|
19
|
+
return `/t/${encodeURIComponent(item.rootId)}#recheck-stopped`;
|
|
20
20
|
if (action.code === 'open-result' && runId !== null)
|
|
21
21
|
return chatResultHref(taskId, runId);
|
|
22
22
|
if (action.code === 'open-pr' && /^https:\/\/github\.com\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+\/pull\/\d+$/.test(item.publicationUrl ?? ''))
|
|
@@ -381,11 +381,10 @@ export type BrowserTaskView = {
|
|
|
381
381
|
statusHtml: string;
|
|
382
382
|
/** The approval ceremony or its updated terms, exactly as signed. */
|
|
383
383
|
approval: string;
|
|
384
|
-
/**
|
|
385
|
-
|
|
384
|
+
/** Check again (a build Toolroll could not prove stopped from this computer), when the status asks for it. */
|
|
385
|
+
recheckStopped?: {
|
|
386
386
|
action: string;
|
|
387
387
|
run: number;
|
|
388
|
-
checked?: boolean;
|
|
389
388
|
} | null;
|
|
390
389
|
/** Build again, in place, when the status asks for it (a result built to an earlier plan). */
|
|
391
390
|
rebuild?: {
|
|
@@ -703,7 +702,7 @@ export type BrowserResultDecision = {
|
|
|
703
702
|
why: string | null;
|
|
704
703
|
};
|
|
705
704
|
};
|
|
706
|
-
/** A Needs you action: a link, (
|
|
705
|
+
/** A Needs you action: a link, (Check again, Build again) one
|
|
707
706
|
* button, or (on the result itself) Accept, which records the person's acceptance; `note` asks why
|
|
708
707
|
* when the evidence disagrees. */
|
|
709
708
|
export type BrowserNeedAction = {
|
|
@@ -713,7 +712,6 @@ export type BrowserNeedAction = {
|
|
|
713
712
|
action: string;
|
|
714
713
|
run: number;
|
|
715
714
|
returnTo: string;
|
|
716
|
-
checked?: boolean;
|
|
717
715
|
} | null;
|
|
718
716
|
accept?: {
|
|
719
717
|
action: string;
|
package/dist/builder.js
CHANGED
|
@@ -5,6 +5,7 @@ import { checkoutGit } from "./teardown-git.js";
|
|
|
5
5
|
import { isAcpProvider } from "./acp-agents.js";
|
|
6
6
|
import { OBSERVATION_MAILBOX, observationBrief, parseObservationCases, collectObservations } from "./observations.js";
|
|
7
7
|
import { checkCommandFor, effectiveCheckLevel, recordRunCheckLevel } from "./check-levels.js";
|
|
8
|
+
import { captureCheckConditions, learnRequestFor, recordCheckFacts } from "./check-facts.js";
|
|
8
9
|
import { queueBatchCheck } from "./batch-checks.js";
|
|
9
10
|
import { batchesFullCheck } from "./batch-policy.js";
|
|
10
11
|
import { skillsContext } from "./project-skills.js";
|
|
@@ -2324,9 +2325,12 @@ async function settleProof(captured, statArtifactId, sealedHead) {
|
|
|
2324
2325
|
let checkAttempt = null;
|
|
2325
2326
|
let releaseFacts = null;
|
|
2326
2327
|
const verification = { last: null };
|
|
2328
|
+
// What the check ran under, captured as its command starts (check-facts.ts): advisory, recorded at disposition.
|
|
2329
|
+
const checkConditions = { last: null };
|
|
2327
2330
|
const checkLog = [];
|
|
2328
2331
|
const checkOutcomes = [];
|
|
2329
2332
|
const checkSuites = [];
|
|
2333
|
+
let settledReport = null;
|
|
2330
2334
|
const noteCheckSuite = (name, result) => {
|
|
2331
2335
|
checkSuites.push({
|
|
2332
2336
|
name,
|
|
@@ -2382,7 +2386,9 @@ async function settleProof(captured, statArtifactId, sealedHead) {
|
|
|
2382
2386
|
const release = decision.authority !== null || releaseCheckScope(store.getScope(request.taskId));
|
|
2383
2387
|
try {
|
|
2384
2388
|
checkAttempt = prepareCheckAttempt(store, root, { runId, repo, head: sealedHead, command: configured, access, authority: decision.authority,
|
|
2385
|
-
...(decision.mode === "targeted" ? { plan: decision.plan, snapshot: decision.snapshot } : {}), watch: decision.watch, release
|
|
2389
|
+
...(decision.mode === "targeted" ? { plan: decision.plan, snapshot: decision.snapshot } : {}), watch: decision.watch, release,
|
|
2390
|
+
// Advisory only (docs/plans/learning-loops.md): the project's tracked identities, a separate handshake field.
|
|
2391
|
+
learn: learnRequestFor(store, repo, now()).learn }, now());
|
|
2386
2392
|
}
|
|
2387
2393
|
catch (error) {
|
|
2388
2394
|
// A recorded last release that can't be verified (eval-harness review 2, finding 4): nothing runs.
|
|
@@ -2404,6 +2410,9 @@ async function settleProof(captured, statArtifactId, sealedHead) {
|
|
|
2404
2410
|
const checkProgress = new CheckProgressTracker(snapshot => progressWrites.push(snapshot));
|
|
2405
2411
|
const runVerification = async (label, missing = null) => {
|
|
2406
2412
|
const streamed = { stdout: false, stderr: false };
|
|
2413
|
+
// The conditions it starts under (verified tree, command, access, inherited environment): advisory, never authority.
|
|
2414
|
+
if (missing === null)
|
|
2415
|
+
checkConditions.last = captureCheckConditions({ repo, head: sealedHead, command: configured, access: checkLaunchOf(store, runId, captured.effective.profile).access, envNames: SETUP_ENV_ALLOWLIST });
|
|
2407
2416
|
// The check runs under the stop watch (v52), owned by this run.
|
|
2408
2417
|
const result = missing !== null
|
|
2409
2418
|
? { code: 127, stdout: "", stderr: `Toolroll did not start the check: ${missing} wasn't found.`, timedOut: false, notFound: false }
|
|
@@ -2624,7 +2633,7 @@ async function settleProof(captured, statArtifactId, sealedHead) {
|
|
|
2624
2633
|
const last = verification.last;
|
|
2625
2634
|
let sealedReport = null;
|
|
2626
2635
|
if (checkAttempt !== null && last !== null && !last.notFound) {
|
|
2627
|
-
sealedReport = sealCheckOutcome(store, root, checkAttempt, { stdout: last.stdout, exitCode: last.timedOut ? null : last.code }, now());
|
|
2636
|
+
sealedReport = settledReport = sealCheckOutcome(store, root, checkAttempt, { stdout: last.stdout, exitCode: last.timedOut ? null : last.code }, now());
|
|
2628
2637
|
// Where the check's time went (latency-timeline.ts): each part's start and end, display only.
|
|
2629
2638
|
keepCheckTimeline(store, { runId, taskId: request.taskId, report: sealedReport, stdout: last.stdout, now: now() });
|
|
2630
2639
|
}
|
|
@@ -2683,6 +2692,16 @@ async function settleProof(captured, statArtifactId, sealedHead) {
|
|
|
2683
2692
|
exitCode: checkExitCode,
|
|
2684
2693
|
suites,
|
|
2685
2694
|
}, now());
|
|
2695
|
+
// What the check ran under and reported, every failure in it, each tagged by source (check-facts.ts): advisory, never
|
|
2696
|
+
// read for this outcome, a verdict or an approval.
|
|
2697
|
+
// A failure to record them is said on the run and never reaches the check's outcome.
|
|
2698
|
+
try {
|
|
2699
|
+
recordCheckFacts(store, root, { runId, taskRef: captured.taskRef, repo, conditions: checkConditions.last, receipt: { status: checkStatus }, report: settledReport,
|
|
2700
|
+
attempt: checkAttempt === null ? null : { nonce: checkAttempt.nonce, learn: checkAttempt.learn }, stdout: verification.last?.stdout ?? null, now: now() });
|
|
2701
|
+
}
|
|
2702
|
+
catch (error) {
|
|
2703
|
+
store.addRunNote(runId, "Toolroll", `This check's facts for the flaky-test list weren't recorded (${redactSecretText(error instanceof Error ? error.message : String(error))}); its result is unchanged.`, now());
|
|
2704
|
+
}
|
|
2686
2705
|
// 4. The sealed diff-stat was restated above, before the correction; it
|
|
2687
2706
|
// is re-read now, after the gate, and the cached facts are adjudicated
|
|
2688
2707
|
// only when the artifact still states exactly them. The gate receipt
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { type CheckReport } from "./contracts/release-check.js";
|
|
2
|
+
import { type Store, type VerifyCommand } from "./store.js";
|
|
3
|
+
/** The sources a result can come from, and the states it can have. */
|
|
4
|
+
export declare const CHECK_RESULT_SOURCES: readonly ["receipt", "report", "log", "learn"];
|
|
5
|
+
export type CheckResultSource = typeof CHECK_RESULT_SOURCES[number];
|
|
6
|
+
export type CheckResultState = "failed" | "passed" | "skipped" | "passed-on-retry";
|
|
7
|
+
/** The receipt's whole-check identity, and the one a failed check with nothing else named records. */
|
|
8
|
+
export declare const WHOLE_CHECK = "check";
|
|
9
|
+
export declare const UNEXPLAINED = "unexplained";
|
|
10
|
+
/** How many result rows one check records at most (a report's own bounds are far above any real check). */
|
|
11
|
+
export declare const MAX_RESULTS_PER_CHECK = 20000;
|
|
12
|
+
/** How old a tracked observation may be and still be asked about (docs/plans/learning-loops.md, 90-day window). */
|
|
13
|
+
export declare const OBSERVATION_WATCH_MS: number;
|
|
14
|
+
/** What a check ran under, captured when its command started. `tree` null: its commit couldn't be read from verified
|
|
15
|
+
* objects, and nothing about the check is comparable. */
|
|
16
|
+
export type CheckConditions = {
|
|
17
|
+
head: string;
|
|
18
|
+
tree: string | null;
|
|
19
|
+
commandDigest: string;
|
|
20
|
+
access: string;
|
|
21
|
+
harnessDigest: string | null;
|
|
22
|
+
lockfile: string | null;
|
|
23
|
+
conditionsDigest: string | null;
|
|
24
|
+
conditionsClass: string | null;
|
|
25
|
+
problem: string | null;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* The conditions a check's command starts under: its commit's verified tree facts, the approved command's digest, its
|
|
29
|
+
* access, and the values of `envNames` (the variables the check inherits, builder.ts SETUP_ENV_ALLOWLIST) as this
|
|
30
|
+
* process holds them now — what the command is given. Hashed with checkContextOf's fields; the class reads
|
|
31
|
+
* `<platform>-<arch>-node<major>`. Values are hashed, never stored.
|
|
32
|
+
*/
|
|
33
|
+
export declare function captureCheckConditions(input: {
|
|
34
|
+
repo: string | null;
|
|
35
|
+
head: string;
|
|
36
|
+
command: VerifyCommand;
|
|
37
|
+
access: string;
|
|
38
|
+
envNames: readonly string[];
|
|
39
|
+
env?: NodeJS.ProcessEnv;
|
|
40
|
+
}): CheckConditions;
|
|
41
|
+
/** Why the task's check ran: a release's or the nightly's purpose, read from the plane's own records. */
|
|
42
|
+
export declare function checkPurposeOf(store: Store, taskRef: number): "release" | "nightly" | "ordinary";
|
|
43
|
+
/** The advisory learning line of a check's output, read strictly: exactly one line carrying this run's nonce, at most
|
|
44
|
+
* 64 KiB, before the report, matching its contract, naming only requested exact cases, each once. Anything else is
|
|
45
|
+
* not recorded, with why; that loses learning evidence only. */
|
|
46
|
+
export declare function learningLinesOf(stdout: string, nonce: string, asked: readonly string[]): {
|
|
47
|
+
ok: true;
|
|
48
|
+
results: {
|
|
49
|
+
identity: string;
|
|
50
|
+
state: "passed" | "failed" | "skipped";
|
|
51
|
+
}[];
|
|
52
|
+
} | {
|
|
53
|
+
ok: false;
|
|
54
|
+
why: string;
|
|
55
|
+
};
|
|
56
|
+
type Row = {
|
|
57
|
+
identity: string;
|
|
58
|
+
state: CheckResultState;
|
|
59
|
+
source: CheckResultSource;
|
|
60
|
+
};
|
|
61
|
+
/** The report's lines as results: every case it names; a file, group, version or part only when it didn't simply pass. */
|
|
62
|
+
export declare function reportResults(report: CheckReport): Row[];
|
|
63
|
+
export type CheckFactsInput = {
|
|
64
|
+
runId: number;
|
|
65
|
+
taskRef: number;
|
|
66
|
+
repo: string | null;
|
|
67
|
+
/** Captured when the command started; null when no command started (nothing comparable was run). */
|
|
68
|
+
conditions: CheckConditions | null;
|
|
69
|
+
/** What the plane recorded as the check's own outcome (run_check), unchanged. */
|
|
70
|
+
receipt: {
|
|
71
|
+
status: "passed" | "failed" | "not-run";
|
|
72
|
+
};
|
|
73
|
+
/** The sealed report, when the check gave a valid one. */
|
|
74
|
+
report: CheckReport | null;
|
|
75
|
+
/** The check's handshake facts, when it had one: its nonce and what it was asked about for learning. */
|
|
76
|
+
attempt: {
|
|
77
|
+
nonce: string;
|
|
78
|
+
learn: readonly string[];
|
|
79
|
+
} | null;
|
|
80
|
+
/** The check's final command output, for the learning line (the same bytes the report was read from). */
|
|
81
|
+
stdout: string | null;
|
|
82
|
+
now: Date;
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* Record what a settled check ran under and reported, once (first write wins), in one transaction: its fact row and
|
|
86
|
+
* every result row. Called at disposition by the builder right after it records the check's own outcome. Advisory:
|
|
87
|
+
* nothing reads these for a verdict, a rerun or an approval.
|
|
88
|
+
*/
|
|
89
|
+
export declare function recordCheckFacts(store: Store, root: string, input: CheckFactsInput): void;
|
|
90
|
+
/** A check's recorded facts, or null. */
|
|
91
|
+
export type CheckFact = {
|
|
92
|
+
run: number;
|
|
93
|
+
repo: string | null;
|
|
94
|
+
taskRef: number;
|
|
95
|
+
head: string | null;
|
|
96
|
+
tree: string | null;
|
|
97
|
+
commandDigest: string | null;
|
|
98
|
+
access: string | null;
|
|
99
|
+
harnessDigest: string | null;
|
|
100
|
+
lockfile: string | null;
|
|
101
|
+
conditionsDigest: string | null;
|
|
102
|
+
conditionsClass: string | null;
|
|
103
|
+
purpose: "release" | "nightly" | "ordinary";
|
|
104
|
+
learnState: "recorded" | "not-recorded" | "not-asked";
|
|
105
|
+
learnWhy: string | null;
|
|
106
|
+
recordedAt: string;
|
|
107
|
+
};
|
|
108
|
+
export declare function checkFactOf(store: Store, runId: number): CheckFact | null;
|
|
109
|
+
/** A check's recorded results, by source then identity. */
|
|
110
|
+
export declare function checkResultsOf(store: Store, runId: number): {
|
|
111
|
+
identity: string;
|
|
112
|
+
state: CheckResultState;
|
|
113
|
+
source: CheckResultSource;
|
|
114
|
+
}[];
|
|
115
|
+
/**
|
|
116
|
+
* What a project's Full checks are asked about for learning (`learn`): its tracked identities (learning_watch) — an
|
|
117
|
+
* accepted note's until it ends, an observation's for 90 days — exact cases only, the most recently tracked first, at
|
|
118
|
+
* most 200. Those left out are counted (`leftOut`): a coverage gap, said, never hidden.
|
|
119
|
+
*/
|
|
120
|
+
export declare function learnRequestFor(store: Store, repo: string, now: Date): {
|
|
121
|
+
learn: string[];
|
|
122
|
+
leftOut: number;
|
|
123
|
+
};
|
|
124
|
+
export {};
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Check facts (docs/plans/learning-loops.md, "Foundation: check facts"): what each settled check ran under and what it
|
|
3
|
+
* reported, recorded at disposition, never trusted for authority. The only writer of `check_fact` and `check_result`
|
|
4
|
+
* (the v149 migration aside); release-auto's learning scan (eval-hooks.ts learningWriteProblem) needs a person for
|
|
5
|
+
* SQL that writes them from anywhere else. A policy path (release-auto.ts EVAL_POLICY): any change needs a person.
|
|
6
|
+
*
|
|
7
|
+
* - Conditions are captured when the check's command starts (captureCheckConditions, called from the builder's
|
|
8
|
+
* runVerification), held in memory and passed here: the commit's tree read through verified objects (never the
|
|
9
|
+
* checkout), the approved command's digest, the check's access, the harness digest and lockfile, and the values of
|
|
10
|
+
* every environment variable the check inherits (the setup allowlist), hashed with checkContextOf's fields as
|
|
11
|
+
* `conditions_digest`. Not re-read later.
|
|
12
|
+
* - Results are tagged by source: `receipt` (the plane's own whole-check result: trusted for "the check passed or
|
|
13
|
+
* failed", nothing finer), `report` (the candidate-written report's lines), `log` (Vitest's FAIL lines in the sealed
|
|
14
|
+
* log, failures only, file level) and `learn` (the advisory learning line). Every failure is recorded; the check's
|
|
15
|
+
* blocking failure is the check's and is never changed here.
|
|
16
|
+
* - Explicit execution evidence is a `report` or `learn` line naming an identity. No line is no evidence: never
|
|
17
|
+
* skipped, removed or passed.
|
|
18
|
+
* - Fail closed: an unreadable tree, or conditions never captured (the command never started), records the fact row
|
|
19
|
+
* with no conditions and no results at all.
|
|
20
|
+
*
|
|
21
|
+
* Nothing here changes a check's outcome, a verdict, an approval or a rerun: every reader of a check's outcome reads
|
|
22
|
+
* the receipt and run_check, which this module never writes.
|
|
23
|
+
*/
|
|
24
|
+
import { createHash } from "node:crypto";
|
|
25
|
+
import { failingTestsOf } from "./gate-failure.js";
|
|
26
|
+
import { checkContextOf, projectGitDir, treeFactsOf } from "./check-report.js";
|
|
27
|
+
import { readVerifiedArtifact } from "./evidence.js";
|
|
28
|
+
import { CHECK_REPORT_PREFIX, isExactCase, learnLineSchema, learnRequest, LEARN_MAX_BYTES, LEARN_PREFIX } from "./contracts/release-check.js";
|
|
29
|
+
import { nightlyTaskRef, releaseTaskRef } from "./store.js";
|
|
30
|
+
/** The sources a result can come from, and the states it can have. */
|
|
31
|
+
export const CHECK_RESULT_SOURCES = Object.freeze(["receipt", "report", "log", "learn"]);
|
|
32
|
+
/** The receipt's whole-check identity, and the one a failed check with nothing else named records. */
|
|
33
|
+
export const WHOLE_CHECK = "check";
|
|
34
|
+
export const UNEXPLAINED = "unexplained";
|
|
35
|
+
/** How many result rows one check records at most (a report's own bounds are far above any real check). */
|
|
36
|
+
export const MAX_RESULTS_PER_CHECK = 20_000;
|
|
37
|
+
/** How old a tracked observation may be and still be asked about (docs/plans/learning-loops.md, 90-day window). */
|
|
38
|
+
export const OBSERVATION_WATCH_MS = 90 * 24 * 60 * 60 * 1000;
|
|
39
|
+
const sha256 = (value) => createHash("sha256").update(value).digest("hex");
|
|
40
|
+
/**
|
|
41
|
+
* The conditions a check's command starts under: its commit's verified tree facts, the approved command's digest, its
|
|
42
|
+
* access, and the values of `envNames` (the variables the check inherits, builder.ts SETUP_ENV_ALLOWLIST) as this
|
|
43
|
+
* process holds them now — what the command is given. Hashed with checkContextOf's fields; the class reads
|
|
44
|
+
* `<platform>-<arch>-node<major>`. Values are hashed, never stored.
|
|
45
|
+
*/
|
|
46
|
+
export function captureCheckConditions(input) {
|
|
47
|
+
const none = (problem) => ({ head: input.head, tree: null, commandDigest: input.command.digest, access: input.access, harnessDigest: null, lockfile: null, conditionsDigest: null, conditionsClass: null, problem });
|
|
48
|
+
if (input.repo === null)
|
|
49
|
+
return none("the check's project isn't recorded");
|
|
50
|
+
const gitDir = projectGitDir(input.repo);
|
|
51
|
+
if (gitDir === null)
|
|
52
|
+
return none("the project's Git directory can't be read");
|
|
53
|
+
let read;
|
|
54
|
+
// Advisory: whatever happens reading the tree, the check itself is never stopped by it.
|
|
55
|
+
try {
|
|
56
|
+
read = treeFactsOf(gitDir, input.head);
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
return none(`the checked commit can't be read from verified objects (${error instanceof Error ? error.message : String(error)})`);
|
|
60
|
+
}
|
|
61
|
+
if (!read.ok)
|
|
62
|
+
return none(`the checked commit can't be read from verified objects (${read.problem})`);
|
|
63
|
+
const context = checkContextOf(read.facts, input.command, input.access);
|
|
64
|
+
const env = input.env ?? process.env;
|
|
65
|
+
const environment = Object.fromEntries([...new Set(input.envNames)].sort().map(name => [name, env[name] ?? null]));
|
|
66
|
+
const digest = sha256(JSON.stringify({ version: 1, context, environment }));
|
|
67
|
+
const major = /^v?(\d+)/.exec(context.node)?.[1] ?? "unknown";
|
|
68
|
+
return { head: input.head, tree: read.facts.tree, commandDigest: input.command.digest, access: input.access, harnessDigest: read.facts.harnessDigest, lockfile: read.facts.lockfile,
|
|
69
|
+
conditionsDigest: digest, conditionsClass: `${context.platform}-${context.arch}-node${major}`, problem: null };
|
|
70
|
+
}
|
|
71
|
+
/** Why the task's check ran: a release's or the nightly's purpose, read from the plane's own records. */
|
|
72
|
+
export function checkPurposeOf(store, taskRef) {
|
|
73
|
+
if (nightlyTaskRef(store, taskRef))
|
|
74
|
+
return "nightly";
|
|
75
|
+
if (releaseTaskRef(store, taskRef))
|
|
76
|
+
return "release";
|
|
77
|
+
const terms = store.handle.prepare(`SELECT s.release_terms_json, s.release_check_json FROM task_scope s JOIN task_ref r ON r.external_id = s.task_id AND r.backend = 'built-in' WHERE r.id = ?`).get(taskRef);
|
|
78
|
+
return terms !== undefined && (terms["release_terms_json"] != null || terms["release_check_json"] != null) ? "release" : "ordinary";
|
|
79
|
+
}
|
|
80
|
+
/** The advisory learning line of a check's output, read strictly: exactly one line carrying this run's nonce, at most
|
|
81
|
+
* 64 KiB, before the report, matching its contract, naming only requested exact cases, each once. Anything else is
|
|
82
|
+
* not recorded, with why; that loses learning evidence only. */
|
|
83
|
+
export function learningLinesOf(stdout, nonce, asked) {
|
|
84
|
+
const lines = stdout.split("\n");
|
|
85
|
+
let reportAt = -1;
|
|
86
|
+
for (let at = lines.length - 1; at >= 0; at--)
|
|
87
|
+
if (lines[at].startsWith(CHECK_REPORT_PREFIX)) {
|
|
88
|
+
reportAt = at;
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
const ours = [];
|
|
92
|
+
let oversized = false, others = false, unreadable = false;
|
|
93
|
+
lines.forEach((line, at) => {
|
|
94
|
+
if (!line.startsWith(LEARN_PREFIX))
|
|
95
|
+
return;
|
|
96
|
+
if (Buffer.byteLength(line) > LEARN_MAX_BYTES) {
|
|
97
|
+
oversized = true;
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
let value;
|
|
101
|
+
try {
|
|
102
|
+
value = JSON.parse(line.slice(LEARN_PREFIX.length));
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
unreadable = true;
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (value === null || typeof value !== "object" || value["nonce"] !== nonce) {
|
|
109
|
+
others = true;
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
ours.push({ at, value });
|
|
113
|
+
});
|
|
114
|
+
if (ours.length === 0)
|
|
115
|
+
return { ok: false, why: oversized ? "the learning line was over 64 KiB" : unreadable ? "the learning line couldn't be read" : others ? "no learning line carried this run's nonce" : "the check printed no learning line" };
|
|
116
|
+
if (ours.length > 1)
|
|
117
|
+
return { ok: false, why: "the check printed more than one learning line" };
|
|
118
|
+
if (oversized)
|
|
119
|
+
return { ok: false, why: "a learning line was over 64 KiB" };
|
|
120
|
+
const only = ours[0];
|
|
121
|
+
if (reportAt >= 0 && only.at > reportAt)
|
|
122
|
+
return { ok: false, why: "the learning line came after the report" };
|
|
123
|
+
const read = learnLineSchema.safeParse(only.value);
|
|
124
|
+
if (!read.success)
|
|
125
|
+
return { ok: false, why: "the learning line doesn't match its contract" };
|
|
126
|
+
const wanted = new Set(asked);
|
|
127
|
+
const seen = new Set();
|
|
128
|
+
for (const one of read.data.results) {
|
|
129
|
+
if (!wanted.has(one.identity) || !isExactCase(one.identity))
|
|
130
|
+
return { ok: false, why: "the learning line named an identity it wasn't asked about" };
|
|
131
|
+
if (seen.has(one.identity))
|
|
132
|
+
return { ok: false, why: "the learning line named an identity twice" };
|
|
133
|
+
seen.add(one.identity);
|
|
134
|
+
}
|
|
135
|
+
return { ok: true, results: read.data.results };
|
|
136
|
+
}
|
|
137
|
+
const RANK = { passed: 0, skipped: 1, "passed-on-retry": 2, failed: 3 };
|
|
138
|
+
/** The report's lines as results: every case it names; a file, group, version or part only when it didn't simply pass. */
|
|
139
|
+
export function reportResults(report) {
|
|
140
|
+
const rows = [];
|
|
141
|
+
const add = (identity, state) => rows.push({ identity, state, source: "report" });
|
|
142
|
+
for (const one of report.units?.identities ?? [])
|
|
143
|
+
add(`unit:${one.file}::${one.name}`, one.state === "passed" && one.flaky ? "passed-on-retry" : one.state);
|
|
144
|
+
for (const one of report.units?.files ?? []) {
|
|
145
|
+
if (one.state !== "passed")
|
|
146
|
+
add(`file:${one.file}`, one.state);
|
|
147
|
+
else if (one.flaky)
|
|
148
|
+
add(`file:${one.file}`, "passed-on-retry");
|
|
149
|
+
}
|
|
150
|
+
for (const run of report.journeys)
|
|
151
|
+
for (const group of run.groups) {
|
|
152
|
+
if (group.state === "failed")
|
|
153
|
+
add(`group:${run.script}:${run.kind}:${group.group}`, "failed");
|
|
154
|
+
else if (group.state === "passed" && group.retried)
|
|
155
|
+
add(`group:${run.script}:${run.kind}:${group.group}`, "passed-on-retry");
|
|
156
|
+
if (group.state === "not-run")
|
|
157
|
+
continue;
|
|
158
|
+
for (const journey of group.journeys)
|
|
159
|
+
add(`journey:${run.script}:${run.kind}:${group.group}::${journey.name}`, journey.state === "passed" && group.retried ? "passed-on-retry" : journey.state);
|
|
160
|
+
}
|
|
161
|
+
for (const one of report.upgrades?.versions ?? [])
|
|
162
|
+
if (one.state === "failed")
|
|
163
|
+
add(`upgrade:${one.version}`, "failed");
|
|
164
|
+
for (const part of report.parts)
|
|
165
|
+
if (part.selected && part.ran && (part.code !== 0 || part.timedOut))
|
|
166
|
+
add(`part:${part.part}`, "failed");
|
|
167
|
+
return rows;
|
|
168
|
+
}
|
|
169
|
+
/** The sealed log's FAIL lines (Vitest), failures only, file level; nothing when the log's own count disagrees. */
|
|
170
|
+
function logResults(store, root, runId) {
|
|
171
|
+
const logs = store.artifactsFor(runId).filter(one => one.kind === "check-log");
|
|
172
|
+
if (logs.length !== 1)
|
|
173
|
+
return [];
|
|
174
|
+
const read = readVerifiedArtifact(root, logs[0]);
|
|
175
|
+
if (!read.ok)
|
|
176
|
+
return [];
|
|
177
|
+
const failing = failingTestsOf(read.content.toString("utf8")) ?? [];
|
|
178
|
+
return failing.filter(one => /^(?!\/)(?!.*(?:^|\/)\.\.?(?:\/|$))[^\u0000-\u001f\\]{1,512}$/.test(one.file)).map(one => ({ identity: `file:${one.file}`, state: "failed", source: "log" }));
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Record what a settled check ran under and reported, once (first write wins), in one transaction: its fact row and
|
|
182
|
+
* every result row. Called at disposition by the builder right after it records the check's own outcome. Advisory:
|
|
183
|
+
* nothing reads these for a verdict, a rerun or an approval.
|
|
184
|
+
*/
|
|
185
|
+
export function recordCheckFacts(store, root, input) {
|
|
186
|
+
const conditions = input.conditions;
|
|
187
|
+
const comparable = conditions !== null && conditions.tree !== null && conditions.conditionsDigest !== null;
|
|
188
|
+
const asked = input.attempt === null ? [] : learnRequest(input.attempt.learn);
|
|
189
|
+
let learnState = asked.length === 0 ? "not-asked" : "not-recorded";
|
|
190
|
+
let learnWhy = null;
|
|
191
|
+
const rows = [];
|
|
192
|
+
if (comparable) {
|
|
193
|
+
if (input.receipt.status !== "not-run")
|
|
194
|
+
rows.push({ identity: WHOLE_CHECK, state: input.receipt.status, source: "receipt" });
|
|
195
|
+
if (input.report !== null)
|
|
196
|
+
rows.push(...reportResults(input.report));
|
|
197
|
+
rows.push(...logResults(store, root, input.runId));
|
|
198
|
+
}
|
|
199
|
+
if (asked.length > 0) {
|
|
200
|
+
if (!comparable)
|
|
201
|
+
learnWhy = conditions === null ? "the check's command never started" : conditions.problem ?? "the check's conditions weren't recorded";
|
|
202
|
+
else if (input.stdout === null || input.attempt === null)
|
|
203
|
+
learnWhy = "the check's output wasn't kept";
|
|
204
|
+
else {
|
|
205
|
+
const learned = learningLinesOf(input.stdout, input.attempt.nonce, asked);
|
|
206
|
+
if (!learned.ok)
|
|
207
|
+
learnWhy = learned.why;
|
|
208
|
+
else {
|
|
209
|
+
learnState = "recorded";
|
|
210
|
+
for (const one of learned.results)
|
|
211
|
+
rows.push({ identity: one.identity, state: one.state, source: "learn" });
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if (comparable && input.receipt.status === "failed" && !rows.some(one => one.source !== "receipt" && one.state === "failed"))
|
|
216
|
+
rows.push({ identity: UNEXPLAINED, state: "failed", source: "receipt" });
|
|
217
|
+
// One row per (identity, source), its worst state; bounded.
|
|
218
|
+
const kept = new Map();
|
|
219
|
+
for (const row of rows) {
|
|
220
|
+
if (row.identity.length < 1 || row.identity.length > 2600)
|
|
221
|
+
continue;
|
|
222
|
+
const key = `${row.source}\u0000${row.identity}`;
|
|
223
|
+
const before = kept.get(key);
|
|
224
|
+
if (before === undefined || RANK[row.state] > RANK[before.state])
|
|
225
|
+
kept.set(key, row);
|
|
226
|
+
}
|
|
227
|
+
const at = input.now.toISOString();
|
|
228
|
+
store.transact(() => {
|
|
229
|
+
const inserted = store.handle.prepare(`INSERT INTO check_fact (run, repo, task_ref, head, tree, command_digest, access, harness_digest, lockfile, conditions_digest,
|
|
230
|
+
conditions_class, purpose, learn_state, learn_why, recorded_at) SELECT id, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? FROM run WHERE id = ?
|
|
231
|
+
ON CONFLICT (run) DO NOTHING`).run(input.repo, input.taskRef, conditions?.head ?? null, comparable ? conditions.tree : null, conditions?.commandDigest ?? null, conditions?.access ?? null, comparable ? conditions.harnessDigest : null, comparable ? conditions.lockfile : null, comparable ? conditions.conditionsDigest : null, comparable ? conditions.conditionsClass : null, checkPurposeOf(store, input.taskRef), learnState, learnState === "not-recorded" ? learnWhy ?? "not recorded" : null, at, input.runId);
|
|
232
|
+
if (Number(inserted.changes) !== 1)
|
|
233
|
+
return;
|
|
234
|
+
const insert = store.handle.prepare("INSERT INTO check_result (run, identity, state, source, recorded_at) VALUES (?, ?, ?, ?, ?)");
|
|
235
|
+
for (const row of [...kept.values()].slice(0, MAX_RESULTS_PER_CHECK))
|
|
236
|
+
insert.run(input.runId, row.identity, row.state, row.source, at);
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
export function checkFactOf(store, runId) {
|
|
240
|
+
const row = store.handle.prepare("SELECT * FROM check_fact WHERE run = ?").get(runId);
|
|
241
|
+
if (row === undefined)
|
|
242
|
+
return null;
|
|
243
|
+
const text = (key) => row[key] === null ? null : String(row[key]);
|
|
244
|
+
return { run: Number(row["run"]), repo: text("repo"), taskRef: Number(row["task_ref"]), head: text("head"), tree: text("tree"), commandDigest: text("command_digest"), access: text("access"),
|
|
245
|
+
harnessDigest: text("harness_digest"), lockfile: text("lockfile"), conditionsDigest: text("conditions_digest"), conditionsClass: text("conditions_class"),
|
|
246
|
+
purpose: String(row["purpose"]), learnState: String(row["learn_state"]), learnWhy: text("learn_why"), recordedAt: String(row["recorded_at"]) };
|
|
247
|
+
}
|
|
248
|
+
/** A check's recorded results, by source then identity. */
|
|
249
|
+
export function checkResultsOf(store, runId) {
|
|
250
|
+
return store.handle.prepare("SELECT identity, state, source FROM check_result WHERE run = ? ORDER BY source, identity").all(runId)
|
|
251
|
+
.map(row => ({ identity: String(row["identity"]), state: String(row["state"]), source: String(row["source"]) }));
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* What a project's Full checks are asked about for learning (`learn`): its tracked identities (learning_watch) — an
|
|
255
|
+
* accepted note's until it ends, an observation's for 90 days — exact cases only, the most recently tracked first, at
|
|
256
|
+
* most 200. Those left out are counted (`leftOut`): a coverage gap, said, never hidden.
|
|
257
|
+
*/
|
|
258
|
+
export function learnRequestFor(store, repo, now) {
|
|
259
|
+
const since = new Date(now.getTime() - OBSERVATION_WATCH_MS).toISOString();
|
|
260
|
+
let tracked;
|
|
261
|
+
// Advisory: a read that fails asks about nothing, and the check runs as it would have.
|
|
262
|
+
try {
|
|
263
|
+
tracked = store.handle.prepare(`SELECT identity, MAX(since) AS latest FROM learning_watch
|
|
264
|
+
WHERE repo = ? AND ((reason = 'note' AND until IS NULL) OR (reason = 'observation' AND since >= ?))
|
|
265
|
+
GROUP BY identity ORDER BY latest DESC, identity`).all(repo, since).map(row => String(row["identity"])).filter(isExactCase);
|
|
266
|
+
}
|
|
267
|
+
catch {
|
|
268
|
+
return { learn: [], leftOut: 0 };
|
|
269
|
+
}
|
|
270
|
+
const learn = learnRequest(tracked);
|
|
271
|
+
return { learn, leftOut: tracked.length - learn.length };
|
|
272
|
+
}
|
package/dist/check-report.d.ts
CHANGED
|
@@ -88,6 +88,8 @@ export type PreparedAttempt = {
|
|
|
88
88
|
snapshot: ObligationSnapshot | null;
|
|
89
89
|
/** The identities the check was asked to report one by one (required ones). */
|
|
90
90
|
watch: string[];
|
|
91
|
+
/** The exact cases the check was asked about for learning only (docs/plans/learning-loops.md): never gate evidence. */
|
|
92
|
+
learn: string[];
|
|
91
93
|
};
|
|
92
94
|
/** A Full check's preparation refused (eval-harness review 2, finding 4): the plane recorded a release whose commit it
|
|
93
95
|
* can't verify now, so the check's obligations can't be decided. The check isn't run (builder.ts: `baseline-unverifiable`). */
|
|
@@ -112,6 +114,9 @@ export declare function prepareCheckAttempt(store: Store, root: string, input: {
|
|
|
112
114
|
/** A release's check (signed release terms or a release authority): CI never stands in for its unit suite. Sealed in
|
|
113
115
|
* the inventory (whose digest the attempt row holds); only an explicit false lets CI stand in. */
|
|
114
116
|
release?: boolean;
|
|
117
|
+
/** Exact cases asked about for learning only (docs/plans/learning-loops.md): a separate handshake field, at most 200,
|
|
118
|
+
* that never joins `watch`, the inventory or anything the check owes. */
|
|
119
|
+
learn?: readonly string[];
|
|
115
120
|
}, now: Date): PreparedAttempt | null;
|
|
116
121
|
/** Seal one JSON payload as a structured-output artifact under its capture label. */
|
|
117
122
|
export declare function sealJson(store: Store, root: string, runId: number, name: string, value: unknown, capture: string, now: Date): number;
|
package/dist/check-report.js
CHANGED
|
@@ -21,7 +21,7 @@ import { createHash, randomBytes } from "node:crypto";
|
|
|
21
21
|
import { lstatSync, realpathSync } from "node:fs";
|
|
22
22
|
import { join } from "node:path";
|
|
23
23
|
import { readVerifiedCommit, verifiedBlobs, verifiedTreeEntries } from "./commit-extract.js";
|
|
24
|
-
import { CHECK_PARTS, CHECK_REPORT_MAX_BYTES, CHECK_REPORT_PREFIX, readCheckReport, } from "./contracts/release-check.js";
|
|
24
|
+
import { CHECK_PARTS, CHECK_REPORT_MAX_BYTES, CHECK_REPORT_PREFIX, learnRequest, readCheckReport, } from "./contracts/release-check.js";
|
|
25
25
|
import { readVerifiedArtifact, storeEvidence } from "./evidence.js";
|
|
26
26
|
import { releaseBaselineRecord } from "./release-card.js";
|
|
27
27
|
import { checkAttemptOf, recordCheckAttempt } from "./store.js";
|
|
@@ -219,8 +219,9 @@ export function prepareCheckAttempt(store, root, input, now) {
|
|
|
219
219
|
// The command's own limit rides along, so the check ends itself (logs and report printed) before the plane stops it.
|
|
220
220
|
// `reportBytes`: the largest report this plane reads back once sealed. A plane that doesn't say (0.9.77 and older) reads
|
|
221
221
|
// only 64 KiB, so the check gives it no larger report rather than one that poisons the receipt (scripts/release-check.mjs).
|
|
222
|
-
const
|
|
223
|
-
|
|
222
|
+
const learn = learnRequest(input.learn ?? []);
|
|
223
|
+
const stdin = JSON.stringify({ nonce, mode, plan, watch: [...(input.watch ?? [])], limitMs: input.command.timeoutMs, reportBytes: CHECK_ARTIFACT_CAP, ...(plan === null ? { owed, ciSubstitute } : {}), ...(learn.length > 0 ? { learn } : {}) });
|
|
224
|
+
return { runId: input.runId, nonce, stdin, mode, inventory, context, plan, snapshot, watch: [...(input.watch ?? [])], learn };
|
|
224
225
|
}
|
|
225
226
|
/** Seal one JSON payload as a structured-output artifact under its capture label. */
|
|
226
227
|
export function sealJson(store, root, runId, name, value, capture, now) {
|
package/dist/cli.js
CHANGED
|
@@ -113,6 +113,8 @@ export const OPERATE_COMMANDS = new Set([
|
|
|
113
113
|
"evals",
|
|
114
114
|
// Only `lead token` reaches here; the central service's lead verbs answer first.
|
|
115
115
|
"lead",
|
|
116
|
+
// Only `release approve` reaches here: every other `release` form is release-cli.ts, routed before this set.
|
|
117
|
+
"release",
|
|
116
118
|
"onboard",
|
|
117
119
|
"status",
|
|
118
120
|
"ready",
|
|
@@ -344,6 +346,9 @@ async function dispatch(argv, write, mainOptions) {
|
|
|
344
346
|
}
|
|
345
347
|
if (first === "update")
|
|
346
348
|
return (await import("./toolroll-update-cli.js")).runUpdateCommand(rest, write, mainOptions.update);
|
|
349
|
+
// `toolroll release approve`: a person's approval, with the operate commands' own credential prompt and context.
|
|
350
|
+
if (first === "release" && rest[0] === "approve")
|
|
351
|
+
return runOperate("release", rest, write, mainOptions.operate ?? {});
|
|
347
352
|
if (first === "release") {
|
|
348
353
|
const { runReleaseCommand } = await import("./release-cli.js");
|
|
349
354
|
return runReleaseCommand(rest, write, { releaseLease: lease => runOperate("worker", ["release", ...lease], write, mainOptions.operate ?? {}), ...mainOptions.release });
|
|
@@ -150,7 +150,7 @@ export declare const CONSOLE_FORMS: {
|
|
|
150
150
|
readonly peopleRevoke: FormContract<"name" | "token">;
|
|
151
151
|
readonly runnerRetire: FormContract<"name" | "token">;
|
|
152
152
|
readonly decisionAnswer: FormContract<"choice" | "confirm" | "note" | "return">;
|
|
153
|
-
readonly
|
|
153
|
+
readonly recheckStopped: FormContract<"return" | "run">;
|
|
154
154
|
readonly merge: FormContract<"anyway" | "run" | "token">;
|
|
155
155
|
readonly onboard: FormContract<"big-ok" | "nonce" | "repo" | "root" | "token">;
|
|
156
156
|
readonly pushSubscribe: FormContract<"auth" | "endpoint" | "p256dh" | "token">;
|
|
@@ -199,7 +199,7 @@ export declare const CONSOLE_FORMS: {
|
|
|
199
199
|
readonly taskCancel: FormContract<"reason">;
|
|
200
200
|
readonly taskRoute: FormContract<"agent" | "clear-phase" | "model" | "phase" | "provider" | "return" | "risky" | "sawDigest" | "size">;
|
|
201
201
|
readonly taskScope: FormContract<"acceptance" | "budget-microusd" | "budget-usd" | "goal" | "goal-brief" | "not" | "permission-mode" | "quality-mode" | "requirement" | "requirement-new" | "sawDigest" | "touches">;
|
|
202
|
-
readonly taskApprove: FormContract<"digest" | "nonce" | "rebuild" | "return" | "token">;
|
|
202
|
+
readonly taskApprove: FormContract<"digest" | "nonce" | "rebuild" | "return" | "sheet" | "token">;
|
|
203
203
|
readonly taskAcceptProof: FormContract<"note" | "return" | "run">;
|
|
204
204
|
readonly taskComplete: FormContract<"accept" | "note" | "publish" | "receipt" | "return" | "run">;
|
|
205
205
|
readonly taskStop: FormContract<"return" | "run">;
|
|
@@ -165,7 +165,7 @@ export const CONSOLE_FORMS = {
|
|
|
165
165
|
peopleRevoke: formContract("POST /people/revoke", ["token", "name"]),
|
|
166
166
|
runnerRetire: formContract("POST /fleet/runner/retire", ["token", "name"]),
|
|
167
167
|
decisionAnswer: formContract("POST /d/<id>/answer", ["return", "choice", "confirm", "note"]),
|
|
168
|
-
|
|
168
|
+
recheckStopped: formContract("POST /t/<task>/recheck-stopped", ["run", "return"]),
|
|
169
169
|
merge: formContract("POST /t/<task>/merge", ["run", "token", "anyway"]),
|
|
170
170
|
onboard: formContract("POST /projects/onboard-preview, /projects/onboard-confirm", ["repo", "root", "token", "nonce", "big-ok"]),
|
|
171
171
|
pushSubscribe: formContract("POST /push/subscribe", ["token", "endpoint", "p256dh", "auth"]),
|
|
@@ -217,7 +217,7 @@ export const CONSOLE_FORMS = {
|
|
|
217
217
|
taskCancel: formContract("POST /t/<task>/cancel", ["reason"]),
|
|
218
218
|
taskRoute: formContract("POST /t/<task>/route", ["size", "risky", "phase", "clear-phase", "agent", "provider", "model", "sawDigest", "return"]),
|
|
219
219
|
taskScope: formContract("POST /t/<task>/scope", ["sawDigest", "permission-mode", "quality-mode", "budget-usd", "budget-microusd", "requirement", "requirement-new", "goal-brief", "goal", "not", "touches", "acceptance"]),
|
|
220
|
-
taskApprove: formContract("POST /t/<task>/approve", ["return", "digest", "token", "nonce", "rebuild"]),
|
|
220
|
+
taskApprove: formContract("POST /t/<task>/approve", ["return", "digest", "token", "nonce", "rebuild", "sheet"]),
|
|
221
221
|
taskAcceptProof: formContract("POST /t/<task>/accept-proof", ["run", "note", "return"]),
|
|
222
222
|
taskComplete: formContract("POST /t/<task>/complete", ["receipt", "run", "accept", "note", "publish", "return"]),
|
|
223
223
|
taskStop: formContract("POST /t/<task>/stop", ["run", "return"]),
|