toolroll 0.9.3 → 0.9.5
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 +26 -0
- package/README.md +5 -4
- package/dist/assignment-presentation.js +4 -2
- package/dist/assignment-ui.js +2 -1
- package/dist/browser/workspace.css +1 -1
- package/dist/browser/workspace.js +9 -9
- package/dist/browser-workspace.d.ts +10 -0
- package/dist/cli.js +6 -6
- package/dist/containment.d.ts +5 -0
- package/dist/containment.js +9 -0
- package/dist/demo-chat.d.ts +37 -0
- package/dist/demo-chat.js +150 -0
- package/dist/demo.d.ts +98 -0
- package/dist/demo.js +525 -21
- package/dist/flow-engine.js +22 -11
- package/dist/flow-pull-request.js +26 -3
- package/dist/flow-steps.js +12 -2
- package/dist/guides.js +17 -0
- package/dist/operate.d.ts +1 -1
- package/dist/operate.js +40 -13
- package/dist/proof.d.ts +9 -0
- package/dist/proof.js +25 -1
- package/dist/publish.d.ts +15 -0
- package/dist/publish.js +41 -1
- package/dist/pull-request-flow.d.ts +2 -0
- package/dist/pull-request-flow.js +17 -3
- package/dist/serve.d.ts +2 -0
- package/dist/serve.js +201 -28
- package/dist/store.d.ts +2 -0
- package/dist/store.js +9 -5
- package/dist/surface.js +7 -1
- package/dist/task-outcome-cli.d.ts +18 -0
- package/dist/task-outcome-cli.js +75 -2
- package/dist/workspace-ui.js +1 -1
- package/package.json +1 -1
package/dist/flow-engine.js
CHANGED
|
@@ -241,21 +241,32 @@ function workStage(store, flow, stage, card, now, options, outcome, onward) {
|
|
|
241
241
|
wait(`${stage.title}'s instructions are too long for a task. Open the flow and shorten them.`);
|
|
242
242
|
return;
|
|
243
243
|
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
244
|
+
// Filed exactly once however many passes advance this project: the card is claimed (still this entry, in this zone,
|
|
245
|
+
// with no task) in the same transaction that files its task, so another pass finds it claimed and leaves it.
|
|
246
|
+
const filed = store.transact(() => {
|
|
247
|
+
const fresh = store.getFlowCard(card.id);
|
|
248
|
+
if (fresh === null || fresh.state !== "active" || fresh.entry !== card.entry || fresh.stage !== stage.id || fresh.task !== null)
|
|
249
|
+
return null;
|
|
250
|
+
const made = fileTaskProposal(store, {
|
|
251
|
+
title: (report ? `${stage.title}: ${card.title}` : card.title).slice(0, 200),
|
|
252
|
+
repo: flow.repo,
|
|
253
|
+
goal,
|
|
254
|
+
filedVia: `flow:${flow.id}`, filedBy: filerFor(card.createdBy), // who made the card asked for the work (an owner can be reassigned by anyone)
|
|
255
|
+
deliverable: report ? "report" : "branch",
|
|
256
|
+
planning: report ? "skip" : stage.planning ?? "auto",
|
|
257
|
+
acceptance: report ? ACCEPTANCE.report : ACCEPTANCE.task,
|
|
258
|
+
admittedRepos: [flow.repo],
|
|
259
|
+
}, now);
|
|
260
|
+
if (made.ok)
|
|
261
|
+
store.updateFlowCard(card.id, { task: made.id, ...(report || fresh.primaryTask !== null ? {} : { primaryTask: made.id }), waiting: "Filed as a task" }, now);
|
|
262
|
+
return made;
|
|
263
|
+
});
|
|
264
|
+
if (filed === null)
|
|
265
|
+
return;
|
|
254
266
|
if (!filed.ok) {
|
|
255
267
|
wait(/[.!?]$/.test(filed.message) ? filed.message : `${filed.message}.`);
|
|
256
268
|
return;
|
|
257
269
|
}
|
|
258
|
-
store.updateFlowCard(card.id, { task: filed.id, ...(report || card.primaryTask !== null ? {} : { primaryTask: filed.id }), waiting: "Filed as a task" }, now);
|
|
259
270
|
outcome.filed.push(filed.id);
|
|
260
271
|
return;
|
|
261
272
|
}
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
import { createHash } from "node:crypto";
|
|
16
16
|
import { assignmentOf } from "./assignment.js";
|
|
17
17
|
import { cardFollowers, notifyPeople } from "./flow-people.js";
|
|
18
|
+
import { adoptPublishedPullRequest } from "./publish.js";
|
|
18
19
|
import { failingCheckOf, mergePullRequest, owePullRequest, pullRequestBlocker, pullRequestFollowOf } from "./pull-request-flow.js";
|
|
19
20
|
import { requestResultChanges } from "./result-actions.js";
|
|
20
21
|
import { revisionSourceOf } from "./result-review.js";
|
|
@@ -56,6 +57,15 @@ export async function pullRequestStep(store, flow, definition, stage, card, now,
|
|
|
56
57
|
body: `${said}${stage.onFail === null ? "" : `\n\nIt's back in ${titleOf(stage.onFail)}${revision === null ? "" : `, as revision ${revision}`}.`}` }, now);
|
|
57
58
|
return true;
|
|
58
59
|
};
|
|
60
|
+
// Publishing that can't proceed is not the build's fault: rebuilding wouldn't change it, so the card waits here,
|
|
61
|
+
// saying what to do, and the people on it hear once. It carries on by itself once the cause is fixed.
|
|
62
|
+
const stuck = (said) => {
|
|
63
|
+
if (!waiting(said))
|
|
64
|
+
return false;
|
|
65
|
+
const people = card.owner === null ? cardFollowers(store, card) : [card.owner];
|
|
66
|
+
notifyPeople(store, card, people, null, { key: `pr-stuck:${card.entry}`, subject: `${stage.title} is waiting on you for “${card.title}”`, attention: true, body: said }, now);
|
|
67
|
+
return true;
|
|
68
|
+
};
|
|
59
69
|
// With no failure path, a card that didn't pass waits for a person to move it (a move is a fresh visit).
|
|
60
70
|
if (stage.onFail === null && card.waiting !== null && card.waiting.endsWith(" Fix it, then move the card to try again."))
|
|
61
71
|
return false;
|
|
@@ -68,14 +78,27 @@ export async function pullRequestStep(store, flow, definition, stage, card, now,
|
|
|
68
78
|
if (publication === null || pullRequestFollowOf(store, publication.id) === null) {
|
|
69
79
|
if (publication === null) {
|
|
70
80
|
const blocked = pullRequestBlocker(store, runId);
|
|
81
|
+
// Only a result that can't be published as built goes back to be built again.
|
|
82
|
+
if (blocked !== null && (blocked === "This result has no commit to publish." || blocked.includes("looks like a secret")))
|
|
83
|
+
return failed(blocked);
|
|
84
|
+
if (blocked === "Pull requests aren't set up for this project.")
|
|
85
|
+
return stuck("Pull requests aren't set up for this project. Turn them on in Projects → Pull requests (or run toolroll publish setup); the card carries on by itself.");
|
|
71
86
|
if (blocked !== null)
|
|
72
|
-
return
|
|
87
|
+
return stuck(blocked);
|
|
73
88
|
}
|
|
74
89
|
publication = owePullRequest(store, runId, `flow ${flow.name}`, card.id, now);
|
|
75
90
|
}
|
|
91
|
+
if (publication.state === "failed") {
|
|
92
|
+
const said = `Couldn't open the pull request: ${publication.lastError ?? "GitHub refused it"}. The commit is safe locally. Fix that, then move the card back to Build for a fresh result.`;
|
|
93
|
+
if (card.waiting === said)
|
|
94
|
+
return false;
|
|
95
|
+
// A pull request opened for this commit anyway (another pass, or a person) is the card's pull request.
|
|
96
|
+
if (await adoptPublishedPullRequest(store, publication, io.gh, () => now))
|
|
97
|
+
publication = store.publicationForRun(runId);
|
|
98
|
+
else
|
|
99
|
+
return stuck(said);
|
|
100
|
+
}
|
|
76
101
|
const pr = publication.prNumber === null ? "the pull request" : `PR #${publication.prNumber}`;
|
|
77
|
-
if (publication.state === "failed")
|
|
78
|
-
return failed(`Couldn't open the pull request: ${publication.lastError ?? "GitHub refused it"}. The commit is safe locally.`);
|
|
79
102
|
if (publication.state !== "opened")
|
|
80
103
|
return waiting(publication.lastError === null ? "Opening the pull request…" : `Opening the pull request. Retrying: ${publication.lastError}`);
|
|
81
104
|
const link = publication.prUrl ?? pr;
|
package/dist/flow-steps.js
CHANGED
|
@@ -137,7 +137,7 @@ export async function runFlowSteps(store, repo, now, io) {
|
|
|
137
137
|
store.updateFlowCard(card.id, { waiting }, now);
|
|
138
138
|
continue;
|
|
139
139
|
}
|
|
140
|
-
if (!store
|
|
140
|
+
if (!claimStep(store, card, { stage: stage.id, kind: stage.kind, script: script?.name ?? null, scriptVersion: script?.version ?? null }, now))
|
|
141
141
|
continue;
|
|
142
142
|
pass.ran++;
|
|
143
143
|
store.updateFlowCard(card.id, { waiting: { check: "Running its check…", sort: "Sorting…", draft: "Writing the draft…", request: "Calling the address…", email: "Sending the email…", tool: "Using the tool…" }[stage.kind] ?? "Updating the issue…" }, now);
|
|
@@ -164,6 +164,16 @@ export async function runFlowSteps(store, repo, now, io) {
|
|
|
164
164
|
}
|
|
165
165
|
return pass;
|
|
166
166
|
}
|
|
167
|
+
/** Claim this card's visit to its step zone, only while the card is still there on that visit: a pass that read the card
|
|
168
|
+
* before another pass (or a person) moved it never runs the step for a visit that's over. */
|
|
169
|
+
function claimStep(store, card, step, now) {
|
|
170
|
+
return store.transact(() => {
|
|
171
|
+
const fresh = store.getFlowCard(card.id);
|
|
172
|
+
if (fresh === null || fresh.state !== "active" || fresh.entry !== card.entry || fresh.stage !== step.stage)
|
|
173
|
+
return false;
|
|
174
|
+
return store.claimFlowStep({ card: card.id, entry: card.entry, ...step }, now);
|
|
175
|
+
});
|
|
176
|
+
}
|
|
167
177
|
/** One teammate turn, when one is due for this card: claimed like any step, retried on failure, and waiting while its question is open. */
|
|
168
178
|
async function teammateStep(store, flow, definition, stage, card, now, io, pass) {
|
|
169
179
|
// v102: on a protected project an AI teammate never decides an approval; the engine asks the person.
|
|
@@ -183,7 +193,7 @@ async function teammateStep(store, flow, definition, stage, card, now, io, pass)
|
|
|
183
193
|
// Waiting on a person: its own question, or (v94) a tool call they approve first.
|
|
184
194
|
if (store.openTeammateQuestionOn(card.id, card.entry) !== null)
|
|
185
195
|
return true;
|
|
186
|
-
if (!store
|
|
196
|
+
if (!claimStep(store, card, { stage: stage.id, kind: "teammate", script: null, scriptVersion: null }, now))
|
|
187
197
|
return true;
|
|
188
198
|
pass.ran++;
|
|
189
199
|
const started = Date.now();
|
package/dist/guides.js
CHANGED
|
@@ -79,6 +79,23 @@ ${AUTHORITY_LINE}
|
|
|
79
79
|
detail says why; \`toolroll sync\` refreshes trackers.
|
|
80
80
|
- \`contest-open\`: a tournament is running on the task; a person picks.
|
|
81
81
|
|
|
82
|
+
## Complete, pull request, merge
|
|
83
|
+
|
|
84
|
+
The console's Complete and Merge, from a terminal. Both are a person's
|
|
85
|
+
acts behind their sign-in (\`--as <you> --token <t>\`, or the remembered
|
|
86
|
+
login); run them only when the person asked for exactly that.
|
|
87
|
+
|
|
88
|
+
- \`task complete <id> --digest <receipt> --pull-request\` marks the exact
|
|
89
|
+
inspected result complete and opens its pull request from that commit
|
|
90
|
+
under the project's publishing setup. \`pull-requests-off\` names the
|
|
91
|
+
setup command (\`publish setup --repo <path> --yes\`).
|
|
92
|
+
- CI is watched on GitHub. \`task show <id> --json\` carries
|
|
93
|
+
\`pullRequest\`: \`prUrl\`, \`state\` (opening, waiting, running, ready,
|
|
94
|
+
failing, merged, closed, failed) and \`mergeCommit\`.
|
|
95
|
+
- \`task merge <id>\` merges it once checks pass, by the project's method
|
|
96
|
+
(squash by default), deletes the branch and records who merged.
|
|
97
|
+
\`checks\` means they are running or failing; the message says which.
|
|
98
|
+
|
|
82
99
|
## Flows
|
|
83
100
|
|
|
84
101
|
A flow is a process cards move through, drawn as zones. The terminal
|
package/dist/operate.d.ts
CHANGED
|
@@ -91,7 +91,7 @@ type Args = {
|
|
|
91
91
|
* exports are behavior, not commentary — and the declared command guide
|
|
92
92
|
* (surface.ts) is tested for exact equality against them.
|
|
93
93
|
*/
|
|
94
|
-
export declare const TASK_ACTIONS: readonly ["add", "list", "show", "wait", "state", "block", "unblock", "next", "steer", "assign", "reopen", "scope", "approve", "hold", "unhold", "require", "requeue", "regate", "plan", "review", "accept", "repair", "route", "stop", "resume", "complete", "revise"];
|
|
94
|
+
export declare const TASK_ACTIONS: readonly ["add", "list", "show", "wait", "state", "block", "unblock", "next", "steer", "assign", "reopen", "scope", "approve", "hold", "unhold", "require", "requeue", "regate", "plan", "review", "accept", "repair", "route", "stop", "resume", "complete", "revise", "merge"];
|
|
95
95
|
export declare const PUBLISH_ACTIONS: readonly ["setup", "grant", "revoke", "status", "unblock", "rearm", "merge", "refire"];
|
|
96
96
|
export declare const CONFIG_ACTIONS: readonly ["show", "set", "clear"];
|
|
97
97
|
export declare const APPROVER_ACTIONS: readonly ["list", "add"];
|
package/dist/operate.js
CHANGED
|
@@ -40,7 +40,7 @@ import { runKnowledgeCommand } from "./knowledge-cli.js";
|
|
|
40
40
|
import { FLOWS_VALUE_FLAGS, runFlowsCommand } from "./flows-cli.js";
|
|
41
41
|
import { runAssignmentCommand } from "./assignment-adapters.js";
|
|
42
42
|
import { applyProjectProfile, runProjectCommand } from "./project-cli.js";
|
|
43
|
-
import { runTaskOutcomeCommand } from "./task-outcome-cli.js";
|
|
43
|
+
import { pullRequestLines, runTaskMergeCommand, runTaskOutcomeCommand } from "./task-outcome-cli.js";
|
|
44
44
|
/**
|
|
45
45
|
* The commands that actually move work: authoring tasks, and the claim loop.
|
|
46
46
|
*
|
|
@@ -138,7 +138,7 @@ import { verifyApproverByPassword } from "./principal.js";
|
|
|
138
138
|
import { authorizedProject, canonicalProject, projectName, resolveCeiling } from "./project.js";
|
|
139
139
|
import { tally, spendLine } from "./summary.js";
|
|
140
140
|
import { bodyHashOf, describePublicationGrant, observeChecks, publicationBody, publishPass, sweepMerges, } from "./publish.js";
|
|
141
|
-
import { checkPublishing, publishingOf, savePublishing, followPullRequests } from "./pull-request-flow.js";
|
|
141
|
+
import { checkPublishing, publishingOf, savePublishing, followPullRequests, newestPullRequestOf } from "./pull-request-flow.js";
|
|
142
142
|
import { starterOf, startersFor, switchOnStarter } from "./flow-starters.js";
|
|
143
143
|
import { acquire, acquireFallback, acquireIfReady, completeFenced, finalizeFailureFenced, finalizeMalformedFenced, finalizeParkFenced, finalizePlanFenced, finalizePlanFailureFenced, finalizeScoutFenced, finalizeScoutFailureFenced, heartbeat, release, reap, currentClaim, DEFAULT_LEASE_MS, SYNC_MAX_AGE_MS, acquireContinuation, } from "./claim.js";
|
|
144
144
|
import { disposeBuildOutcome, holdStaleApproval, regateTask } from "./dispose.js";
|
|
@@ -178,7 +178,7 @@ import { scout as scoutTask } from "./scout.js";
|
|
|
178
178
|
import { profileDigestOf, chainDigestOf, entryDigestOf } from "./scope.js";
|
|
179
179
|
import { PROVIDER_KEY_ENV, SUBSCRIPTION_CAPABLE, clearProviderKey, keyStatus, readAuthMode, readAuthModeStrict, readProviderKey, saveProviderKey, setAuthMode, verifyProviderKey, verdictWords } from "./keys.js";
|
|
180
180
|
import { run, terminateLiveProviders, run as execRun } from "./exec.js";
|
|
181
|
-
import { containmentStatus, currentContainment, describeContainment, resolveContainment } from "./containment.js";
|
|
181
|
+
import { containmentNotice, containmentStatus, currentContainment, describeContainment, resolveContainment } from "./containment.js";
|
|
182
182
|
import { readPulls } from "./pulls.js";
|
|
183
183
|
import { startMaintenance } from "./maintenance.js";
|
|
184
184
|
import { livePin, modelWatchPass } from "./model-catalog.js";
|
|
@@ -235,7 +235,10 @@ export const OPERATE_HELP = `toolroll — operating the queue
|
|
|
235
235
|
toolroll ledger checkpoint record the chain's head to copy off this machine (instance operator)
|
|
236
236
|
toolroll ledger export --from <YYYY-MM-DD> --to <YYYY-MM-DD> [--out <file>]
|
|
237
237
|
every sealed entry in the range, with an evidence pack per task
|
|
238
|
-
toolroll task complete <id> mark the current result complete (--digest for JSON/agents)
|
|
238
|
+
toolroll task complete <id> mark the current result complete (--digest for JSON/agents);
|
|
239
|
+
[--pull-request] --pull-request also opens its pull request
|
|
240
|
+
toolroll task merge <id> --as <you> --token <t>
|
|
241
|
+
merge the task's pull request once checks pass
|
|
239
242
|
toolroll task revise <id> --feedback "requested change"
|
|
240
243
|
toolroll assignment show <task> root, current work and exact handoff
|
|
241
244
|
toolroll assignment updates durable updates (--after <cursor>)
|
|
@@ -364,7 +367,9 @@ External trackers — build what a tracker nominates, under local approvals
|
|
|
364
367
|
refuses to spawn where none exists
|
|
365
368
|
(macOS) instead of downgrading.
|
|
366
369
|
Also read by watch, tick and
|
|
367
|
-
daemon install.
|
|
370
|
+
daemon install. --verbose prints
|
|
371
|
+
the exact containment status (a
|
|
372
|
+
service log always has it).
|
|
368
373
|
toolroll reconcile --repo <path> the morning sweep: recover dead
|
|
369
374
|
runners, reap expired leases, adopt
|
|
370
375
|
or forget orphaned worktrees. Run it
|
|
@@ -554,7 +559,7 @@ Exit codes
|
|
|
554
559
|
export const TASK_ACTIONS = [
|
|
555
560
|
"add", "list", "show", "wait", "state", "block", "unblock", "next", "steer", "assign",
|
|
556
561
|
"reopen", "scope", "approve", "hold", "unhold", "require", "requeue", "regate", "plan",
|
|
557
|
-
"review", "accept", "repair", "route", "stop", "resume", "complete", "revise",
|
|
562
|
+
"review", "accept", "repair", "route", "stop", "resume", "complete", "revise", "merge",
|
|
558
563
|
];
|
|
559
564
|
export const PUBLISH_ACTIONS = ["setup", "grant", "revoke", "status", "unblock", "rearm", "merge", "refire"];
|
|
560
565
|
export const CONFIG_ACTIONS = ["show", "set", "clear"];
|
|
@@ -607,6 +612,10 @@ export const OPERATE_BOOLEAN_FLAGS = new Set([
|
|
|
607
612
|
"zip",
|
|
608
613
|
// Settings → Integrations: the last checks, without checking again.
|
|
609
614
|
"saved",
|
|
615
|
+
// task complete: also open the result's pull request.
|
|
616
|
+
"pull-request",
|
|
617
|
+
// up: the exact start-up details (containment) in the terminal too.
|
|
618
|
+
"verbose",
|
|
610
619
|
]);
|
|
611
620
|
export function parseOperateArgs(argv, ownValues = new Set()) {
|
|
612
621
|
const positional = [];
|
|
@@ -7126,7 +7135,8 @@ async function runWatchLoop(args) {
|
|
|
7126
7135
|
}
|
|
7127
7136
|
// What this runner's spawns actually get, in one honest line: native
|
|
7128
7137
|
// and which backend, observed, or a required policy that will refuse.
|
|
7129
|
-
|
|
7138
|
+
for (const line of args.containmentLines?.(currentContainment()) ?? [`watch: ${describeContainment(currentContainment())}`])
|
|
7139
|
+
progress(line);
|
|
7130
7140
|
// The night is a row, not "the last 24 hours": everything this watch does
|
|
7131
7141
|
// attributes to this episode by runner and window, and `brief
|
|
7132
7142
|
// --latest-watch` bounds itself to exactly it.
|
|
@@ -8152,6 +8162,18 @@ async function upCommand(flags, context) {
|
|
|
8152
8162
|
return copy;
|
|
8153
8163
|
};
|
|
8154
8164
|
const prefix = (repo) => (activeRepos.size > 1 ? `[${projectName(repo)}] ` : "");
|
|
8165
|
+
// Containment at start: one plain sentence, once, for a person at a
|
|
8166
|
+
// terminal; the exact status line with --verbose, in a service log (no
|
|
8167
|
+
// terminal), and always when a required policy will refuse spawns.
|
|
8168
|
+
const containmentDetail = flags.has("verbose") || !(context.upSeams?.terminal ?? process.stdout.isTTY === true);
|
|
8169
|
+
let containmentSaid = false;
|
|
8170
|
+
const containmentLines = (effective) => {
|
|
8171
|
+
const notice = containmentNotice(effective);
|
|
8172
|
+
if (notice !== null && !containmentSaid)
|
|
8173
|
+
progress(notice);
|
|
8174
|
+
containmentSaid = true;
|
|
8175
|
+
return containmentDetail || effective.refusal !== null ? [`watch: ${describeContainment(effective)}`] : [];
|
|
8176
|
+
};
|
|
8155
8177
|
const loopResults = new Map();
|
|
8156
8178
|
const launchRepo = async (repo, bind) => {
|
|
8157
8179
|
if (loopResults.has(repo) || stopping)
|
|
@@ -8179,6 +8201,7 @@ async function upCommand(flags, context) {
|
|
|
8179
8201
|
progress: line => progress(`${prefix(repo)}${line}`),
|
|
8180
8202
|
isStopping: () => stopping,
|
|
8181
8203
|
onFollowController: controller => followControllers.push(controller),
|
|
8204
|
+
containmentLines,
|
|
8182
8205
|
onReady: () => {
|
|
8183
8206
|
markReady();
|
|
8184
8207
|
if (bind)
|
|
@@ -8318,10 +8341,10 @@ async function upCommand(flags, context) {
|
|
|
8318
8341
|
}
|
|
8319
8342
|
write(repos.length === 0
|
|
8320
8343
|
? ` builder ${runnerName} is ready — add a local folder or GitHub repository in Projects`
|
|
8321
|
-
: ` builder
|
|
8344
|
+
: ` builder watching ${repos.length === 1 ? repos[0] : `${repos.length} repositories`}`);
|
|
8322
8345
|
if (projectRoots.length > 0)
|
|
8323
8346
|
write(` projects new repositories under ${projectRoots.length === 1 ? projectRoots[0] : `${projectRoots.length} saved folders`} connect automatically`);
|
|
8324
|
-
write("
|
|
8347
|
+
write(" Chat shows the steps to your first result.");
|
|
8325
8348
|
write(" Ctrl-C stops Toolroll on this machine.");
|
|
8326
8349
|
}
|
|
8327
8350
|
if (!json && !flags.has("no-open") && terminal)
|
|
@@ -8330,7 +8353,7 @@ async function upCommand(flags, context) {
|
|
|
8330
8353
|
// 10. Supervise to the end.
|
|
8331
8354
|
await stopped;
|
|
8332
8355
|
await registrySupervisor;
|
|
8333
|
-
|
|
8356
|
+
await Promise.all([...loopResults.values()]);
|
|
8334
8357
|
clearInterval(runnerHeartbeat);
|
|
8335
8358
|
if (runTimer !== undefined)
|
|
8336
8359
|
clearTimeout(runTimer);
|
|
@@ -8363,9 +8386,7 @@ async function upCommand(flags, context) {
|
|
|
8363
8386
|
process.stderr.write(`up: ${fatal}\n`);
|
|
8364
8387
|
return EXIT.failed;
|
|
8365
8388
|
}
|
|
8366
|
-
|
|
8367
|
-
const built = results.reduce((sum, one) => sum + one.result.built, 0);
|
|
8368
|
-
progress(`up: stopped cleanly — ${ticks} pass(es), ${built} with work. Run \`toolroll up\` anywhere on this machine to reconnect every saved project.`);
|
|
8389
|
+
progress("Toolroll stopped. Run `toolroll up` anywhere on this machine to start it again with every saved project.");
|
|
8369
8390
|
return EXIT.ok;
|
|
8370
8391
|
}
|
|
8371
8392
|
// ---- the telegram bridge ---------------------------------------------------
|
|
@@ -9325,6 +9346,9 @@ function taskCommand(positional, flags, context) {
|
|
|
9325
9346
|
const authenticated = verifyApproverByPassword(context.store, acting.name, acting.token, context.store.knownRepos().filter(repo => context.store.accountCanAccess(acting.name, repo)));
|
|
9326
9347
|
return authenticated.ok ? authenticated.who : null;
|
|
9327
9348
|
} });
|
|
9349
|
+
case "merge":
|
|
9350
|
+
return runTaskMergeCommand(rest, flags, { store: context.store, json: context.json, write: context.write, clock: context.clock,
|
|
9351
|
+
demo: context.store.isDemo(), credentials: () => askCredentials(flags, context), ...(context.publishExec === undefined ? {} : { exec: context.publishExec }) });
|
|
9328
9352
|
case "add":
|
|
9329
9353
|
return addTask(rest, flags, context);
|
|
9330
9354
|
case "list":
|
|
@@ -9744,6 +9768,8 @@ function showTask(positional, context) {
|
|
|
9744
9768
|
// Paused (resume), or the review-retry door — and every stop on record.
|
|
9745
9769
|
control: taskControlOf(store, ref.id, now),
|
|
9746
9770
|
stops: store.stopsForTask(ref.id),
|
|
9771
|
+
// The pull request Complete opened (any version), with its CI state and merge commit — the console's view.
|
|
9772
|
+
pullRequest: newestPullRequestOf(store, store.taskFamilyOf(id, null, true)?.versions.map(one => one.id) ?? [id]),
|
|
9747
9773
|
};
|
|
9748
9774
|
return succeed(write, json, "task show", detail, () => [
|
|
9749
9775
|
`${task.id} ${task.state}${ref.deliverable === "report" ? " (scout — delivers a report)" : ""}`,
|
|
@@ -9772,6 +9798,7 @@ function showTask(positional, context) {
|
|
|
9772
9798
|
...detail.report.report.followUps.map((one, index) => ` follow-up ${index + 1}: ${one.title}`),
|
|
9773
9799
|
]
|
|
9774
9800
|
: [` report: ${detail.report.problem} (run ${detail.report.run})`]),
|
|
9801
|
+
...pullRequestLines(detail.pullRequest),
|
|
9775
9802
|
...(detail.blockedBy.length > 0 ? [` waits for ${detail.blockedBy.join(", ")}`] : []),
|
|
9776
9803
|
...(detail.position === null
|
|
9777
9804
|
? []
|
package/dist/proof.d.ts
CHANGED
|
@@ -332,6 +332,15 @@ export declare function manualReviewOnly(proof: {
|
|
|
332
332
|
reasons: readonly string[];
|
|
333
333
|
matrix?: readonly CriterionMatrixRow[];
|
|
334
334
|
} | null): boolean;
|
|
335
|
+
/** The criterion id a stored "needs a person's check" reason names, or null
|
|
336
|
+
* for any other reason. The stored words stay as recorded; pages say them
|
|
337
|
+
* through `personCheckWords`. */
|
|
338
|
+
export declare function manualReviewCriterionOf(reason: string): string | null;
|
|
339
|
+
/** A stored verification reason as a person would say it, for pages and
|
|
340
|
+
* cards. The record keeps its exact words; unknown reasons pass through. */
|
|
341
|
+
export declare function plainReasonWords(reason: string): string;
|
|
342
|
+
/** A requirement only a person can confirm, as a person would say it. */
|
|
343
|
+
export declare function personCheckWords(statement: string | null): string;
|
|
335
344
|
export type CriterionMatrixRow = {
|
|
336
345
|
/** Controller-captured evidence for direct goal assessment. Kept separately
|
|
337
346
|
* from the final row state: available evidence is not a satisfied goal. */
|
package/dist/proof.js
CHANGED
|
@@ -509,7 +509,31 @@ export function manualReviewOnly(proof) {
|
|
|
509
509
|
return proof?.verdict === "short" && (proof.matrix === undefined || (proof.matrix.some(row => row.state === "manual-review")
|
|
510
510
|
&& proof.matrix.every(row => row.state === "pass" || row.state === "manual-review")))
|
|
511
511
|
&& proof.reasons.length > 0
|
|
512
|
-
&& proof.reasons.every(reason =>
|
|
512
|
+
&& proof.reasons.every(reason => manualReviewCriterionOf(reason) !== null);
|
|
513
|
+
}
|
|
514
|
+
/** The criterion id a stored "needs a person's check" reason names, or null
|
|
515
|
+
* for any other reason. The stored words stay as recorded; pages say them
|
|
516
|
+
* through `personCheckWords`. */
|
|
517
|
+
export function manualReviewCriterionOf(reason) {
|
|
518
|
+
return /^criterion "([^"]+)" requires manual-review evidence — an operator must accept it before this can verify$/.exec(reason)?.[1] ?? null;
|
|
519
|
+
}
|
|
520
|
+
/** A stored verification reason as a person would say it, for pages and
|
|
521
|
+
* cards. The record keeps its exact words; unknown reasons pass through. */
|
|
522
|
+
export function plainReasonWords(reason) {
|
|
523
|
+
if (manualReviewCriterionOf(reason) !== null)
|
|
524
|
+
return "A requirement needs your own check.";
|
|
525
|
+
const unassigned = /^caveat [0-9]+ names no criterion — every caveat is an exception to exactly one signed criterion, named by its exact id \(an unrelated idea belongs in the handoff's follow-ups\): ([\s\S]*)$/.exec(reason);
|
|
526
|
+
if (unassigned !== null)
|
|
527
|
+
return `The agent left a note without saying which requirement it affects: ${unassigned[1]}`;
|
|
528
|
+
const reviewed = /^(?:reviewer:)?([^\s"]+) (contradicts|needs more evidence for) criterion "[^"]+": ([\s\S]*)$/i.exec(reason);
|
|
529
|
+
if (reviewed !== null)
|
|
530
|
+
return `${reviewed[1]} ${reviewed[2].toLowerCase() === "contradicts" ? "says a requirement is not met" : "isn't sure a requirement is met"}: ${reviewed[3]}`;
|
|
531
|
+
return reason;
|
|
532
|
+
}
|
|
533
|
+
/** A requirement only a person can confirm, as a person would say it. */
|
|
534
|
+
export function personCheckWords(statement) {
|
|
535
|
+
const plain = statement?.trim() ?? "";
|
|
536
|
+
return plain === "" ? "You check this one yourself." : `You check this one: ${plain}`;
|
|
513
537
|
}
|
|
514
538
|
/** Set equality, exactly — the "must equal the complete sealed git diff
|
|
515
539
|
* exactly" rule (v39) needs both directions, unlike the legacy subset
|
package/dist/publish.d.ts
CHANGED
|
@@ -25,6 +25,15 @@ import type { Publication, PublicationGrant, Store } from "./store.js";
|
|
|
25
25
|
export declare const BODY_TEMPLATE_VERSION = 2;
|
|
26
26
|
/** After this many failed attempts a publication stops retrying and pages instead. */
|
|
27
27
|
export declare const MAX_PUBLISH_ATTEMPTS = 5;
|
|
28
|
+
/**
|
|
29
|
+
* The one grant lookup for a task's publication: its project is the task's repo, and that project's live grant
|
|
30
|
+
* governs it. The publisher, Complete → pull request and a flow's Pull request zone all ask here, so none of them
|
|
31
|
+
* can judge a publication by another project's grant. `fallback`: the repo to assume for a task with none recorded.
|
|
32
|
+
*/
|
|
33
|
+
export declare function publicationGrantOf(store: Store, taskRef: number, fallback?: string | null): {
|
|
34
|
+
repo: string | null;
|
|
35
|
+
grant: PublicationGrant | null;
|
|
36
|
+
};
|
|
28
37
|
export type PublishExec = (file: string, args: readonly string[], options?: RunOptions) => Promise<ExecResult>;
|
|
29
38
|
/** The grant's terms, in the words the approver must be shown before agreeing. */
|
|
30
39
|
export declare function describePublicationGrant(grant: {
|
|
@@ -65,6 +74,12 @@ export declare function permitsPublication(grant: PublicationGrant | null, publi
|
|
|
65
74
|
ok: false;
|
|
66
75
|
message: string;
|
|
67
76
|
};
|
|
77
|
+
/**
|
|
78
|
+
* A publication given up on whose pull request exists anyway — a pass that gave up while another opened it, or a
|
|
79
|
+
* person who opened it by hand: the PR on its exact head and base, carrying its exact commit, is adopted as opened
|
|
80
|
+
* (merged or closed ones too; the next observation records which). Whether one was found.
|
|
81
|
+
*/
|
|
82
|
+
export declare function adoptPublishedPullRequest(store: Store, publication: Publication, exec: PublishExec, clock: () => Date): Promise<boolean>;
|
|
68
83
|
export type CheckReport = {
|
|
69
84
|
observed: number;
|
|
70
85
|
failing: number;
|
package/dist/publish.js
CHANGED
|
@@ -31,6 +31,15 @@ export const BODY_TEMPLATE_VERSION = 2;
|
|
|
31
31
|
/** After this many failed attempts a publication stops retrying and pages instead. */
|
|
32
32
|
export const MAX_PUBLISH_ATTEMPTS = 5;
|
|
33
33
|
const EXEC_TIMEOUT_MS = 60_000;
|
|
34
|
+
/**
|
|
35
|
+
* The one grant lookup for a task's publication: its project is the task's repo, and that project's live grant
|
|
36
|
+
* governs it. The publisher, Complete → pull request and a flow's Pull request zone all ask here, so none of them
|
|
37
|
+
* can judge a publication by another project's grant. `fallback`: the repo to assume for a task with none recorded.
|
|
38
|
+
*/
|
|
39
|
+
export function publicationGrantOf(store, taskRef, fallback = null) {
|
|
40
|
+
const repo = store.refById(taskRef)?.repo ?? fallback;
|
|
41
|
+
return { repo, grant: repo === null ? null : store.publicationGrantFor(repo) };
|
|
42
|
+
}
|
|
34
43
|
/** The grant's terms, in the words the approver must be shown before agreeing. */
|
|
35
44
|
export function describePublicationGrant(grant) {
|
|
36
45
|
return [
|
|
@@ -107,9 +116,15 @@ export async function publishPass(store, options) {
|
|
|
107
116
|
concede(store, publication, "the accepted diff carries a detected secret — rewrite the branch before anything publishes", report, clock);
|
|
108
117
|
continue;
|
|
109
118
|
}
|
|
119
|
+
// Each project's watch publishes its own project's work: the push runs in
|
|
120
|
+
// that checkout, under that project's grant. Another project's pass must
|
|
121
|
+
// not touch it — judging it by the wrong grant once recorded "no live
|
|
122
|
+
// grant" errors until it gave up, while its own pass opened the PR.
|
|
110
123
|
// The grant is re-read per publication, live: revocation is immediate,
|
|
111
124
|
// and an intent created under a grant that has since died goes nowhere.
|
|
112
|
-
const grant = store.
|
|
125
|
+
const { repo, grant } = publicationGrantOf(store, publication.taskRef, options.repo);
|
|
126
|
+
if (repo !== options.repo)
|
|
127
|
+
continue;
|
|
113
128
|
const verdict = permitsPublication(grant, publication);
|
|
114
129
|
if (!verdict.ok) {
|
|
115
130
|
report.problems.push(`publication ${publication.id}: ${verdict.message}`);
|
|
@@ -243,6 +258,31 @@ async function openOrAdopt(store, publication, grant, exec, clock) {
|
|
|
243
258
|
rmSync(dir, { recursive: true, force: true });
|
|
244
259
|
}
|
|
245
260
|
}
|
|
261
|
+
/**
|
|
262
|
+
* A publication given up on whose pull request exists anyway — a pass that gave up while another opened it, or a
|
|
263
|
+
* person who opened it by hand: the PR on its exact head and base, carrying its exact commit, is adopted as opened
|
|
264
|
+
* (merged or closed ones too; the next observation records which). Whether one was found.
|
|
265
|
+
*/
|
|
266
|
+
export async function adoptPublishedPullRequest(store, publication, exec, clock) {
|
|
267
|
+
if (publication.state !== "failed")
|
|
268
|
+
return false;
|
|
269
|
+
const listed = await exec("gh", ["pr", "list", "--repo", publication.githubRepo, "--head", publication.head, "--base", publication.base, "--state", "all", "--json", "number,url,headRefOid"], { timeoutMs: EXEC_TIMEOUT_MS });
|
|
270
|
+
if (listed.code !== 0)
|
|
271
|
+
return false;
|
|
272
|
+
let found;
|
|
273
|
+
try {
|
|
274
|
+
found = JSON.parse(listed.stdout || "[]");
|
|
275
|
+
}
|
|
276
|
+
catch {
|
|
277
|
+
return false;
|
|
278
|
+
}
|
|
279
|
+
const pr = Array.isArray(found) ? found.find(one => one.headRefOid === publication.headSha && typeof one.number === "number" && typeof one.url === "string") : undefined;
|
|
280
|
+
if (pr === undefined)
|
|
281
|
+
return false;
|
|
282
|
+
store.markPublicationOpened(publication.id, pr.number, pr.url, clock());
|
|
283
|
+
enqueueOpened(store, publication, pr.url, clock);
|
|
284
|
+
return true;
|
|
285
|
+
}
|
|
246
286
|
/**
|
|
247
287
|
* Read CI for every PR this control plane opened — structured rollup and the
|
|
248
288
|
* exact head OID, never a command's exit status (finding 18). Episodes are
|
|
@@ -169,4 +169,6 @@ export type PullRequestView = {
|
|
|
169
169
|
};
|
|
170
170
|
/** The pull request a result opened through Complete, in plain words; null when it opened none. */
|
|
171
171
|
export declare function pullRequestViewOf(store: Store, runId: number): PullRequestView | null;
|
|
172
|
+
/** The newest pull request any version of a task opened through Complete (versions oldest first), or null. */
|
|
173
|
+
export declare function newestPullRequestOf(store: Store, versions: readonly string[]): PullRequestView | null;
|
|
172
174
|
export {};
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import { createHash } from "node:crypto";
|
|
18
18
|
import { run as execRun } from "./exec.js";
|
|
19
|
-
import { bodyHashOf, publicationBody } from "./publish.js";
|
|
19
|
+
import { bodyHashOf, publicationBody, publicationGrantOf } from "./publish.js";
|
|
20
20
|
import { failingChecks, summarizeChecks } from "./pulls.js";
|
|
21
21
|
import { assignmentOf, checkAssignmentAsOperator } from "./assignment.js";
|
|
22
22
|
import { requestResultChanges } from "./result-actions.js";
|
|
@@ -137,7 +137,7 @@ export function pullRequestBlocker(store, runId) {
|
|
|
137
137
|
const ref = run === null ? null : store.refById(run.taskRef);
|
|
138
138
|
if (run === null || ref === null)
|
|
139
139
|
return "No such result.";
|
|
140
|
-
const grant =
|
|
140
|
+
const { grant } = publicationGrantOf(store, run.taskRef);
|
|
141
141
|
if (grant === null || !grant.capabilities.includes("push-branch") || !grant.capabilities.includes("open-pr"))
|
|
142
142
|
return "Pull requests aren't set up for this project.";
|
|
143
143
|
if (run.headRevision === null || run.committed === false)
|
|
@@ -184,7 +184,7 @@ export function owePullRequest(store, runId, actor, flowCard, now) {
|
|
|
184
184
|
return store.transact(() => {
|
|
185
185
|
const run = store.getRun(runId);
|
|
186
186
|
const ref = store.refById(run.taskRef);
|
|
187
|
-
const grant = store.
|
|
187
|
+
const grant = publicationGrantOf(store, run.taskRef).grant;
|
|
188
188
|
let publication = store.publicationForRun(run.id);
|
|
189
189
|
if (publication === null) {
|
|
190
190
|
const id = store.createPublicationIntent({
|
|
@@ -473,3 +473,17 @@ export function pullRequestViewOf(store, runId) {
|
|
|
473
473
|
return { ...base, state: "waiting", label: "Waiting for checks", detail: "No CI checks have reported on GitHub yet." };
|
|
474
474
|
}
|
|
475
475
|
}
|
|
476
|
+
/** The newest pull request any version of a task opened through Complete (versions oldest first), or null. */
|
|
477
|
+
export function newestPullRequestOf(store, versions) {
|
|
478
|
+
for (const id of [...versions].reverse()) {
|
|
479
|
+
const ref = store.lookupRef(id);
|
|
480
|
+
if (ref === null)
|
|
481
|
+
continue;
|
|
482
|
+
for (const run of store.runsFor(ref.id)) {
|
|
483
|
+
const view = pullRequestViewOf(store, run.id);
|
|
484
|
+
if (view !== null)
|
|
485
|
+
return view;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
return null;
|
|
489
|
+
}
|
package/dist/serve.d.ts
CHANGED
|
@@ -18,6 +18,8 @@ import type { RevisionLineage } from "./store.js";
|
|
|
18
18
|
import type { SubscriptionMateRunner } from "./subscription-chat.js";
|
|
19
19
|
import { type InstallMethod } from "./install-method.js";
|
|
20
20
|
export type ServeOptions = {
|
|
21
|
+
/** `toolroll demo`: the scripted lead that answers Chat instead of a model. */
|
|
22
|
+
demoLead?: import("./demo.js").DemoLead;
|
|
21
23
|
/** Tests: the bin whose real path says how Toolroll was installed (Settings → Updates' command). */
|
|
22
24
|
installBin?: string;
|
|
23
25
|
/** Native coding workspace injection for isolated integration tests. */
|