toolroll 0.9.49 → 0.9.50
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 +8 -0
- package/dist/cli-http.js +3 -0
- package/dist/flow-triggers.js +7 -0
- package/dist/mcp-http.js +6 -11
- package/dist/mcp-oauth.js +5 -21
- package/dist/mcp-person.d.ts +0 -9
- package/dist/mcp-person.js +2 -1
- package/dist/operate.js +4 -1
- package/dist/serve.d.ts +2 -605
- package/dist/serve.js +423 -23783
- package/dist/server/chat.d.ts +41 -0
- package/dist/server/chat.js +1458 -0
- package/dist/server/edge-refusal.d.ts +5 -0
- package/dist/server/edge-refusal.js +56 -0
- package/dist/server/flows.d.ts +12 -0
- package/dist/server/flows.js +832 -0
- package/dist/server/guards.d.ts +72 -0
- package/dist/server/guards.js +357 -0
- package/dist/server/handler-context.d.ts +22 -0
- package/dist/server/handler-context.js +1 -0
- package/dist/server/handler-registry.d.ts +21 -0
- package/dist/server/handler-registry.js +34 -0
- package/dist/server/pages-assets.d.ts +10 -0
- package/dist/server/pages-assets.js +103 -0
- package/dist/server/pages.d.ts +11 -0
- package/dist/server/pages.js +910 -0
- package/dist/server/people-tokens.d.ts +20 -0
- package/dist/server/people-tokens.js +694 -0
- package/dist/server/remote-hooks.d.ts +29 -0
- package/dist/server/remote-hooks.js +130 -0
- package/dist/server/remote.d.ts +59 -0
- package/dist/server/remote.js +185 -0
- package/dist/server/route-policy.d.ts +18 -0
- package/dist/server/route-policy.js +30 -0
- package/dist/server/route-table.d.ts +74 -0
- package/dist/server/route-table.js +350 -0
- package/dist/server/runtime.d.ts +280 -0
- package/dist/server/runtime.js +25 -0
- package/dist/server/settings.d.ts +22 -0
- package/dist/server/settings.js +2388 -0
- package/dist/server/shared.d.ts +3334 -0
- package/dist/server/shared.js +12708 -0
- package/dist/server/task-views.d.ts +82 -0
- package/dist/server/task-views.js +1099 -0
- package/dist/server/tasks.d.ts +87 -0
- package/dist/server/tasks.js +3579 -0
- package/dist/session-http.js +3 -0
- package/dist/team-http.d.ts +5 -1
- package/dist/team-http.js +6 -2
- package/dist/teams.js +3 -0
- package/package.json +1 -1
|
@@ -0,0 +1,1099 @@
|
|
|
1
|
+
import { assignmentPresentationOf, historicalAssessmentReason } from '../assignment-presentation.js';
|
|
2
|
+
import { assignmentStatusOf, assignmentWithEvidence } from '../assignment-ui.js';
|
|
3
|
+
import { assignmentOf } from '../assignment.js';
|
|
4
|
+
import { knowledgeContextHtml } from "../knowledge-ui.js";
|
|
5
|
+
import { failedAttemptSentence, failingCheckSuggestion, GENERAL_SUGGESTION, isInternalErrorReason, latestFinishedAttempt, missedRequirementLine, missedRequirementSuggestion, NO_REASON_RECORDED, stopSuggestionOf } from '../needs-you.js';
|
|
6
|
+
import { planAutoPending } from "../plan-auto.js";
|
|
7
|
+
import { readKnowledgeSnapshot } from "../project-knowledge.js";
|
|
8
|
+
import { learningView } from "../project-learning.js";
|
|
9
|
+
import { readSkillsSnapshot, skillTestResult } from "../project-skills.js";
|
|
10
|
+
import { newestPullRequestOf, publishingOf, pullRequestBlocker } from '../pull-request-flow.js';
|
|
11
|
+
import { structuredHandoffView, terminalDiffView } from "../result-evidence-readers.js";
|
|
12
|
+
import { resultCheckLevel } from '../result-follow-ups.js';
|
|
13
|
+
import { skillsSnapshotHtml, skillTestFeedbackHtml } from "../skills-ui.js";
|
|
14
|
+
import { runActivityOf } from "../task-activity.js";
|
|
15
|
+
import { taskFingerprint } from "../task-live.js";
|
|
16
|
+
import { pullRequestFactOf } from '../task-status.js';
|
|
17
|
+
import { openWorkDecisionOf } from "../work-summary.js";
|
|
18
|
+
import { learningHtml } from "../workspace-ui.js";
|
|
19
|
+
import { randomBytes } from "node:crypto";
|
|
20
|
+
import {} from "node:http";
|
|
21
|
+
import { agentChoicesFor, INSTALLATION_SCOPE, routeOfTask } from "../agentconfig.js";
|
|
22
|
+
import { rulesSummary } from "../approval-rules-ui.js";
|
|
23
|
+
import { nonceHashOf } from "../contest.js";
|
|
24
|
+
import { hasForbiddenControls } from "../decision.js";
|
|
25
|
+
import { diagnoseTaskDispatch } from "../dispatch.js";
|
|
26
|
+
import { readVerifiedArtifact, readVerifiedReport } from "../evidence.js";
|
|
27
|
+
import { computeGaps } from "../gaps.js";
|
|
28
|
+
import { modeTermsFromJson } from "../modes.js";
|
|
29
|
+
import { projectRoute } from "../phase-routing.js";
|
|
30
|
+
import { milestonesOf, parseExecutionPlanDocument } from "../plan.js";
|
|
31
|
+
import { contractChangesOf, decodePlanContractRecord, describeContractChanges } from "../planner-source.js";
|
|
32
|
+
import { projectName } from "../project.js";
|
|
33
|
+
import { isRevisionFeedback } from "../result-review.js";
|
|
34
|
+
import { buildReviewOf } from "../review-switch.js";
|
|
35
|
+
import { isAlive as runnerAlive } from "../runner.js";
|
|
36
|
+
import { approvalOf } from "../scope.js";
|
|
37
|
+
import { approvalFormDigest, attendedWatchWords, chatResultHref, completionReceiptView, consentDoorOf, decisionsFor, diffFileAnchor, escape, evidenceLinksFor, followUpsFor, oneLineOf, orderChangedFiles, parseReviewDiff, proofBundleView, publicationFactsOf, receiptStatusOf, refuse, resumeCeremonyPage, resumeDigestOf, reviewHref, screen, taskBody, taskChatHref, taskHref, taskPage, withinSignedTouches } from "./shared.js";
|
|
38
|
+
import { budgetHoldWords, budgetLabel, monthOf } from "../spend.js";
|
|
39
|
+
import {} from "../store.js";
|
|
40
|
+
import { taskControlOf } from "../task-control.js";
|
|
41
|
+
import { lastErrorLineOf, missedRequirementOf, workStatusOf } from "../workspace-ui.js";
|
|
42
|
+
export function createTaskViews(runtime) {
|
|
43
|
+
/** The first task's way to Ready shows until the first Ready result, and stays on the installation's first task. */
|
|
44
|
+
function guided(rootId, now) {
|
|
45
|
+
if (runtime.store.isDemo())
|
|
46
|
+
return false;
|
|
47
|
+
if (runtime.store.firstSuccessAt(now) === null)
|
|
48
|
+
return true;
|
|
49
|
+
const first = runtime.store.handle.prepare("SELECT id FROM task ORDER BY created_at, rowid LIMIT 1").get();
|
|
50
|
+
return first !== undefined && String(first["id"]) === rootId;
|
|
51
|
+
}
|
|
52
|
+
function projectFamilyPeek(repo, now) {
|
|
53
|
+
const row = runtime.projectCounts(now).find(one => one.repo === repo);
|
|
54
|
+
return { waiting: row?.totals['needs-you'] ?? 0, running: row?.totals.running ?? 0,
|
|
55
|
+
queued: row?.queued ?? 0, doneRecently: row?.doneRecently ?? 0 };
|
|
56
|
+
}
|
|
57
|
+
function familiesInView(project, options = {}) {
|
|
58
|
+
return runtime.store.taskFamiliesAdmitted(project === null ? runtime.admissionList() : runtime.visible(project) ? [project] : [], runtime.visible(null), options);
|
|
59
|
+
}
|
|
60
|
+
function familyTasksInView(project, state, limit = 200) {
|
|
61
|
+
return familiesInView(project, { ...(state === undefined ? {} : { states: [state] }), limit })
|
|
62
|
+
.map(family => ({ ...family.current, id: family.root.id, title: family.root.title }));
|
|
63
|
+
}
|
|
64
|
+
function revisionDestination(child, back) {
|
|
65
|
+
const root = runtime.familyOf(child)?.root.id ?? child;
|
|
66
|
+
if (!back.startsWith("/chat?"))
|
|
67
|
+
return `${taskHref(root)}?version=${encodeURIComponent(child)}`;
|
|
68
|
+
const conversation = new URL(back, 'http://localhost').searchParams.get('conversation');
|
|
69
|
+
return `${taskChatHref(root)}&revision=${encodeURIComponent(child)}${conversation ? '&conversation=' + encodeURIComponent(conversation) : ''}`;
|
|
70
|
+
}
|
|
71
|
+
function earlierLiveVersions(family, now) {
|
|
72
|
+
const ids = family.versions.filter(one => one.id !== family.current.id).map(one => one.id);
|
|
73
|
+
if (ids.length === 0)
|
|
74
|
+
return [];
|
|
75
|
+
return runtime.store.taskActivityCandidates(family.root.repo === null ? [] : [family.root.repo], family.root.repo === null, now, ids)
|
|
76
|
+
.filter(task => workRowOf(task, now).status.views.includes("running")).map(task => task.id);
|
|
77
|
+
}
|
|
78
|
+
function familyHistory(family) {
|
|
79
|
+
const warning = family.problem === null ? "" : `<p class="problem" data-history-problem>${escape(family.problem)}</p>`;
|
|
80
|
+
const otherActive = new Set([...family.otherActive.map(one => one.id), ...earlierLiveVersions(family, runtime.clock())]).size;
|
|
81
|
+
const active = otherActive === 0 ? "" : `<p class="problem" data-other-active>${otherActive} earlier version${otherActive === 1 ? " is" : "s are"} still waiting or running. Review History.</p>`;
|
|
82
|
+
if (family.versions.length < 2)
|
|
83
|
+
return warning;
|
|
84
|
+
const versions = family.versions.map((version, index) => {
|
|
85
|
+
const label = index === 0 ? "Original" : `Revision ${index}`;
|
|
86
|
+
const status = workRowOf(version, runtime.clock()).status;
|
|
87
|
+
const runs = runtime.store.runsFor(version.refId).filter(runIsTaskResult);
|
|
88
|
+
const href = `${taskHref(family.root.id)}?version=${encodeURIComponent(version.id)}`;
|
|
89
|
+
return `<li data-history-version="${escape(version.id)}"><a href="${href}">${label}</a>${version.id === family.current.id ? " · Current" : ""} · ${escape(status.label)}` +
|
|
90
|
+
runs.map(run => `<a href="${escape(run.outcome === "built" || run.outcome === "no-change" ? chatResultHref(family.root.id, run.id) : `/r/${run.id}`)}">Build #${run.id}${run.outcome === "built" ? "" : ` · ${escape(run.outcome === "no-change" ? "No changes" : run.outcome === "failed" ? "Failed" : run.outcome ?? "Unfinished")}`}</a>`).join("") + `</li>`;
|
|
91
|
+
});
|
|
92
|
+
return warning + active + `<details class="task-history" data-root-task="${escape(family.root.id)}"><summary>History</summary><ol>${versions.join("")}</ol></details>`;
|
|
93
|
+
}
|
|
94
|
+
/** Keep detailed receipt diagnostics; shared assignment reads own readiness. */
|
|
95
|
+
function freshAssignment(assignment, receipt) {
|
|
96
|
+
if (assignment === null)
|
|
97
|
+
return null;
|
|
98
|
+
const value = assignmentWithEvidence(assignment, receipt === null ? null : receiptStatusOf(receipt), receipt?.runId ?? null);
|
|
99
|
+
return receipt === null ? value : { ...value, attention: [...new Set([...value.attention, ...receipt.facts.evidenceProblems, ...receipt.caveats.filter(reason => !historicalAssessmentReason(reason))])] };
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* One Work row's facts (workspace package 1), from the records the task
|
|
103
|
+
* page already reads: the dispatch diagnosis, the latest finished
|
|
104
|
+
* builder/scout run with its machine verdict and any acceptance, the
|
|
105
|
+
* run's publication, and the live claim. The words come from the pure
|
|
106
|
+
* projection so this row, the task page, the focused chat, and the
|
|
107
|
+
* review cockpit cannot disagree about the same run.
|
|
108
|
+
*/
|
|
109
|
+
function workRowOf(task, now, receipt) {
|
|
110
|
+
const ref = runtime.store.lookupRef(task.id);
|
|
111
|
+
const runs = ref === null ? [] : runtime.store.runsFor(ref.id);
|
|
112
|
+
const latest = runs.find(runIsTaskResult) ?? null;
|
|
113
|
+
const verdict = latest === null ? null : runtime.store.proofVerdictFor(latest.id);
|
|
114
|
+
const live = runs.find(one => runIsLive(one)) ?? null;
|
|
115
|
+
const facts = {
|
|
116
|
+
id: task.id,
|
|
117
|
+
title: task.title,
|
|
118
|
+
repo: task.repo,
|
|
119
|
+
state: task.state,
|
|
120
|
+
updatedAt: task.updatedAt,
|
|
121
|
+
dispatch: diagnoseTaskDispatch(runtime.store, task.id, now),
|
|
122
|
+
result: task.state !== "done"
|
|
123
|
+
? null
|
|
124
|
+
: latest === null
|
|
125
|
+
? null
|
|
126
|
+
: {
|
|
127
|
+
runId: latest.id,
|
|
128
|
+
role: latest.role,
|
|
129
|
+
outcome: latest.outcome,
|
|
130
|
+
verdict: verdict?.verdict ?? null,
|
|
131
|
+
reasons: verdict?.reasons ?? [],
|
|
132
|
+
accepted: runtime.store.proofAcceptance(latest.id) !== null,
|
|
133
|
+
checkLevel: resultCheckLevel(runtime.store, latest.id, now),
|
|
134
|
+
review: runtime.reviewFactsFor(latest.id),
|
|
135
|
+
...(latest.outcome !== "no-change"
|
|
136
|
+
? {}
|
|
137
|
+
: { recordComplete: (() => { const kinds = new Set(runtime.store.artifactsFor(latest.id).map(one => one.kind)); return kinds.has("handoff") && kinds.has("terminal-diff"); })() }),
|
|
138
|
+
},
|
|
139
|
+
publication: latest === null ? null : publicationFactsOf(runtime.store.publicationForRun(latest.id)),
|
|
140
|
+
liveRunId: live === null ? null : live.id,
|
|
141
|
+
openDecision: ref === null ? null : openWorkDecisionOf(runtime.store, ref.id, now),
|
|
142
|
+
unfinishedRunId: runs.find(one => one.outcome === null && one.role !== "reviewer")?.id ?? null,
|
|
143
|
+
control: ref === null ? { kind: "none" } : taskControlOf(runtime.store, ref.id, now),
|
|
144
|
+
};
|
|
145
|
+
// Read the same evidence-health projection as the receipt, including
|
|
146
|
+
// damaged files discovered after a stored verdict was written.
|
|
147
|
+
const result = receipt === undefined && task.state === "done" && latest !== null && (latest.outcome === "built" || latest.outcome === "no-change")
|
|
148
|
+
? completionReceiptView(runtime.store, latest, runtime.store.artifactsFor(latest.id), runtime.evidenceRoot, runtime.reviewFactsFor(latest.id)) : receipt;
|
|
149
|
+
const status = workStatusOf(facts, result == null ? undefined : receiptStatusOf(result));
|
|
150
|
+
const earlierLive = task.family === undefined ? [] : earlierLiveVersions(task.family, now);
|
|
151
|
+
if (earlierLive.length > 0 && !status.views.includes("running"))
|
|
152
|
+
status.views = [...status.views, "running"];
|
|
153
|
+
const otherActive = new Set([...(task.family?.otherActive.map(one => one.id) ?? []), ...earlierLive]).size;
|
|
154
|
+
const recordedAssignment = task.family === undefined ? null : assignmentOf(runtime.store, task.id, now, runtime.workAccess(), runtime.evidenceRoot);
|
|
155
|
+
const assignment = freshAssignment(recordedAssignment, result ?? null);
|
|
156
|
+
const assignmentStatus = assignment === null ? status : assignmentStatusOf(assignment, status);
|
|
157
|
+
if (status.views.includes("running") && !assignmentStatus.views.includes("running"))
|
|
158
|
+
assignmentStatus.views = [...assignmentStatus.views, "running"];
|
|
159
|
+
return { ...facts, executionId: task.id, id: task.family?.root.id ?? task.id, title: task.family?.root.title ?? task.title,
|
|
160
|
+
familyNotice: task.family?.problem ?? (otherActive ? `${otherActive} earlier version${otherActive === 1 ? " is" : "s are"} still waiting or running. Open History.` : null),
|
|
161
|
+
status: assignment === null ? status : { ...assignmentStatus, diagnostics: assignmentPresentationOf(assignment, { workStatus: status, diagnostics: [...(status.diagnostics ?? []), ...(status.tone === "problem" && status.detail !== assignment.detail ? [status] : [])] }).diagnostics }, assignment, assignmentProblem: status.tone === "problem", resultRunId: latest === null ? null : latest.id };
|
|
162
|
+
}
|
|
163
|
+
/** The compact task list for the master pane, the current row marked. */
|
|
164
|
+
function taskListPane(project, currentId) {
|
|
165
|
+
const rows = familyTasksInView(project, undefined, 100);
|
|
166
|
+
if (currentId !== null && !rows.some(one => one.id === currentId)) {
|
|
167
|
+
const family = runtime.familyOf(currentId);
|
|
168
|
+
if (family !== null && (project === null || family.root.repo === null || family.root.repo === project))
|
|
169
|
+
rows.push({ ...family.current, id: family.root.id, title: family.root.title });
|
|
170
|
+
}
|
|
171
|
+
const items = rows
|
|
172
|
+
.map(task => `<a class="item${task.id === currentId ? " current" : ""}" href="${taskHref(task.id)}">` +
|
|
173
|
+
`<span class="t">${escape(task.title)}</span></a>`)
|
|
174
|
+
.join("\n");
|
|
175
|
+
return `<h2>Tasks</h2>\n${items === "" ? `<p class="meta">None yet</p>` : items}`;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* The one liveness fact (round-4 findings 14/15): a run is being built
|
|
179
|
+
* right now iff its outcome is null AND its lease is the task's current
|
|
180
|
+
* live claim \u2014 maximum generation, unreleased, strictly unexpired. Every
|
|
181
|
+
* "running" label, poller, and watch link derives from this; nothing
|
|
182
|
+
* ever infers liveness from a null outcome alone.
|
|
183
|
+
*/
|
|
184
|
+
function runIsLive(run) {
|
|
185
|
+
return run.outcome === null && runtime.store.currentLiveLease(run.taskRef, runtime.clock()) === run.leaseId;
|
|
186
|
+
}
|
|
187
|
+
/** Planner, reviewer, and structured-output correction bookkeeping can
|
|
188
|
+
* finish around a build but are never the task's delivered result. */
|
|
189
|
+
const runIsTaskResult = (run) => run.finishedAt !== null && (run.role === "builder" || run.role === "scout");
|
|
190
|
+
/** The task screen, shared by the GET and by every refusal that re-renders it. */
|
|
191
|
+
/**
|
|
192
|
+
* What a revision task's approval must restate (M6.8): the exact comment
|
|
193
|
+
* batch, read back through the verified artifact path. A brief that no
|
|
194
|
+
* longer verifies is a named problem on the screen — approving against
|
|
195
|
+
* bytes nobody can prove is not approving anything.
|
|
196
|
+
*/
|
|
197
|
+
function revisionViewOf(ref) {
|
|
198
|
+
if (ref === null || ref.revisionBriefArtifact === null)
|
|
199
|
+
return null;
|
|
200
|
+
const artifact = runtime.store.getArtifact(ref.revisionBriefArtifact);
|
|
201
|
+
if (artifact === null)
|
|
202
|
+
return { problem: "the revision brief is missing from this build's records" };
|
|
203
|
+
const read = readVerifiedArtifact(runtime.evidenceRoot, artifact);
|
|
204
|
+
if (!read.ok)
|
|
205
|
+
return { problem: `the revision brief no longer verifies — ${read.problem}` };
|
|
206
|
+
try {
|
|
207
|
+
const parsed = JSON.parse(read.content.toString("utf8"));
|
|
208
|
+
return {
|
|
209
|
+
sourceTask: String(parsed.sourceTask ?? "?"),
|
|
210
|
+
sourceRun: Number(parsed.sourceRun ?? 0),
|
|
211
|
+
kind: parsed.kind === "ci-repair" ? "ci-repair" : parsed.kind === "criterion-repair" ? "criterion-repair" : "annotations",
|
|
212
|
+
lineage: runtime.store.revisionLineageOf(ref.externalId, runtime.clock()),
|
|
213
|
+
comments: (parsed.comments ?? []).slice(0, 100).map(one => ({
|
|
214
|
+
path: one.path === null || one.path === undefined ? null : String(one.path),
|
|
215
|
+
line: one.line === null || one.line === undefined ? null : Number(one.line),
|
|
216
|
+
note: String(one.note ?? ""),
|
|
217
|
+
author: String(one.author ?? "?"),
|
|
218
|
+
})),
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
return { problem: "the revision brief did not parse" };
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
/** The task view's facts, shared by the full screen and the workbench
|
|
226
|
+
* pane (attended A1): one assembly, one authorization story. */
|
|
227
|
+
function taskViewData(taskId, who, problem, render = {}) {
|
|
228
|
+
const found = runtime.store.getTask(taskId);
|
|
229
|
+
if (found === null)
|
|
230
|
+
return null;
|
|
231
|
+
const ref = runtime.store.lookupRef(taskId);
|
|
232
|
+
if (ref !== null && !runtime.visible(ref.repo))
|
|
233
|
+
return null;
|
|
234
|
+
const now = runtime.clock();
|
|
235
|
+
const scope = runtime.store.getScope(taskId);
|
|
236
|
+
const revision = revisionViewOf(ref);
|
|
237
|
+
// A broken revision brief blocks the whole approval surface (audit
|
|
238
|
+
// IV-3): no nonce is minted over a batch nobody can verify.
|
|
239
|
+
const revisionBroken = revision !== null && "problem" in revision;
|
|
240
|
+
// A tournament task's yes covers BOTH documents (finding 31): where
|
|
241
|
+
// race terms are filed, the digest being shown — and bound by the
|
|
242
|
+
// nonce — is the joint fingerprint, never the scope's alone.
|
|
243
|
+
const raceTerms = ref === null ? null : runtime.store.activeTournamentTerms(ref.id);
|
|
244
|
+
const planView = ref === null ? null : planViewOf(ref.id);
|
|
245
|
+
const approvalDigest = scope === null ? null : approvalFormDigest(scope.digest, raceTerms?.raceDigest ?? null, planView?.sha256 ?? null);
|
|
246
|
+
// The nonce is minted at render, per viewer, bound to the digest being
|
|
247
|
+
// shown — the browser approval flow starts here and nowhere else.
|
|
248
|
+
// …and only through an OPEN consent door (v48): an unreadable route,
|
|
249
|
+
// a route that cannot run, or a pre-routing row whose approval lapsed
|
|
250
|
+
// gets recovery copy, never a nonce.
|
|
251
|
+
const routeView = routeViewOf(taskId, ref, scope, now, who);
|
|
252
|
+
// A read-only refresh (package 2) reads the same facts WITHOUT minting:
|
|
253
|
+
// a five-second poll must never churn the bounded nonce store, and a
|
|
254
|
+
// fragment carries no password form to bind a nonce to.
|
|
255
|
+
const nonce = who.via === "cookie" && scope !== null && approvalDigest !== null && !approvalOf(scope).approved && !revisionBroken && ref?.plan !== "requested" && consentDoorOf(scope, routeView).open
|
|
256
|
+
? render.mintNonce === false ? "unminted" : runtime.mintApprovalNonce(who.name, taskId, approvalDigest)
|
|
257
|
+
: "";
|
|
258
|
+
const runs = ref === null ? [] : runtime.store.runsFor(ref.id);
|
|
259
|
+
const completion = (() => {
|
|
260
|
+
// v40 fix: a reviewer run finishes AFTER the build it reviews and
|
|
261
|
+
// carries no proof verdict of its own — without this filter, its
|
|
262
|
+
// "no-change" outcome would hijack the task's own completion card
|
|
263
|
+
// the instant a review lands, showing "proof missing" for a proof
|
|
264
|
+
// that is right there on the builder's run.
|
|
265
|
+
const latest = runs.find(runIsTaskResult);
|
|
266
|
+
if (latest === undefined)
|
|
267
|
+
return null;
|
|
268
|
+
const artifacts = runtime.store.artifactsFor(latest.id);
|
|
269
|
+
const verdict = runtime.store.proofVerdictFor(latest.id);
|
|
270
|
+
const review = runtime.reviewFactsFor(latest.id);
|
|
271
|
+
return {
|
|
272
|
+
runId: latest.id,
|
|
273
|
+
outcome: latest.outcome,
|
|
274
|
+
hasTerminalDiff: artifacts.some(one => one.kind === "terminal-diff"),
|
|
275
|
+
hasHandoff: artifacts.some(one => one.kind === "handoff"),
|
|
276
|
+
// The closed machine-authored verdict (Priority 2), computed once
|
|
277
|
+
// at completion by adjudicate() and never re-inferred here — null
|
|
278
|
+
// only for a run that predates the proof system.
|
|
279
|
+
proofVerdict: verdict?.verdict ?? null,
|
|
280
|
+
machineVerdict: verdict?.machineVerdict ?? null,
|
|
281
|
+
proofReasons: verdict?.reasons ?? [],
|
|
282
|
+
proofMatrix: verdict?.matrix ?? [],
|
|
283
|
+
proofMatrixLinks: evidenceLinksFor(artifacts),
|
|
284
|
+
proofAccepted: runtime.store.proofAcceptance(latest.id) !== null,
|
|
285
|
+
qualityMode: latest.qualityMode ?? "default",
|
|
286
|
+
// Either direction: the ORIGINAL task's page finds the chain by
|
|
287
|
+
// its own latest run (the one that triggered a draft); a DRAFT
|
|
288
|
+
// task's page finds the SAME chain by being named as the draft.
|
|
289
|
+
repairChain: runtime.store.repairChainFor(latest.id) ?? runtime.store.repairChainForDraft(taskId),
|
|
290
|
+
// v50: the bounded review-retry history of this exact result —
|
|
291
|
+
// the same projection the CLI and dispatch diagnosis read.
|
|
292
|
+
reviewRetry: runtime.store.reviewRetryStateOf(latest.id),
|
|
293
|
+
// The same review facts every other surface projects for this run.
|
|
294
|
+
review,
|
|
295
|
+
receipt: latest.outcome === "built" || latest.outcome === "no-change"
|
|
296
|
+
? completionReceiptView(runtime.store, latest, artifacts, runtime.evidenceRoot, review)
|
|
297
|
+
: null,
|
|
298
|
+
};
|
|
299
|
+
})();
|
|
300
|
+
// Adaptive execution plans (v44): the plan-revision ledger and live
|
|
301
|
+
// milestone projection, both null for a task with no plan at all.
|
|
302
|
+
// taskChatFocus below reads the SAME two calls, so the task page and
|
|
303
|
+
// focused chat always render identical facts (c2).
|
|
304
|
+
const planRevisions = ref === null ? null : revisionLedgerOf(ref.id);
|
|
305
|
+
const milestoneProgress = ref === null ? null : progressOf(ref.id, planRevisions?.current?.document ?? null);
|
|
306
|
+
const family = runtime.familyOf(taskId);
|
|
307
|
+
const recordedAssignment = family?.current.id !== taskId ? null : assignmentOf(runtime.store, taskId, now, runtime.workAccess(), runtime.evidenceRoot);
|
|
308
|
+
const assignment = freshAssignment(recordedAssignment, completion?.receipt ?? null);
|
|
309
|
+
const progressRun = runs.find(one => runIsLive(one))?.id ?? completion?.runId ?? null;
|
|
310
|
+
return {
|
|
311
|
+
guide: guided(family?.root.id ?? taskId, now),
|
|
312
|
+
assignment,
|
|
313
|
+
checkProgress: progressRun === null ? null : runtime.store.checkProgress(progressRun),
|
|
314
|
+
task: found,
|
|
315
|
+
status: workRowOf({ ...found, repo: ref?.repo ?? null }, now, completion?.receipt).status,
|
|
316
|
+
dispatch: diagnoseTaskDispatch(runtime.store, taskId, now),
|
|
317
|
+
// v52: the exact-run control — the same projection the focused
|
|
318
|
+
// chat and `task show` read.
|
|
319
|
+
control: ref === null ? { kind: "none" } : taskControlOf(runtime.store, ref.id, now),
|
|
320
|
+
canRetryReview: who.via === "cookie" && who.role === "approver",
|
|
321
|
+
strikes: ref?.strikes ?? 0,
|
|
322
|
+
filer: runtime.store.taskFiler(taskId),
|
|
323
|
+
// v105: a waiting task whose monthly budget is used up says so.
|
|
324
|
+
budgetHold: (() => {
|
|
325
|
+
if (ref === null || found.state !== "queued")
|
|
326
|
+
return null;
|
|
327
|
+
// Only work billed to an API key waits on a budget (its approved agent's, or Claude's by default).
|
|
328
|
+
const base = runtime.store.approvedChainOf(taskId)?.[0];
|
|
329
|
+
const hold = runtime.store.budgetGate(now)({ ...runtime.store.budgetSubject(ref.id),
|
|
330
|
+
agents: base !== undefined ? [{ provider: base.profile.provider, billing: base.authMode }] : runtime.store.agentsFor([runtime.store.getScope(taskId)?.approvedProfile?.provider ?? "claude"]) });
|
|
331
|
+
if (hold.over === null)
|
|
332
|
+
return null;
|
|
333
|
+
return hold.why === "unpriced" ? `${budgetHoldWords(hold, monthOf(now).name)}; this waits until then`
|
|
334
|
+
: `${budgetLabel(hold.over)} monthly budget is used up, so this waits until next month or a higher budget`;
|
|
335
|
+
})(),
|
|
336
|
+
// Sprint 8: a waiting task the organisation policy stops says which rule, or that it runs lowered.
|
|
337
|
+
policyHold: (() => {
|
|
338
|
+
if (ref === null || found.state !== "queued")
|
|
339
|
+
return null;
|
|
340
|
+
const base = runtime.store.approvedChainOf(taskId)?.[0]?.profile ?? runtime.store.getScope(taskId)?.approvedProfile ?? null;
|
|
341
|
+
if (base === null)
|
|
342
|
+
return null;
|
|
343
|
+
const verdict = runtime.store.runPolicy(base);
|
|
344
|
+
return !verdict.ok ? `${verdict.message.replace(/ An instance operator can change it in Settings → Policy\.$/, "")} It waits until the policy allows it.` : verdict.lowered;
|
|
345
|
+
})(),
|
|
346
|
+
approvalRules: (() => {
|
|
347
|
+
const repo = ref?.repo ?? null;
|
|
348
|
+
if (repo === null)
|
|
349
|
+
return null;
|
|
350
|
+
const rules = runtime.store.approvalRules(repo);
|
|
351
|
+
if (!rules.notRequester && !rules.protectProject && rules.protectedPaths.length === 0)
|
|
352
|
+
return null;
|
|
353
|
+
const gate = runtime.store.approvalGate(taskId, "\u0000nobody", "person");
|
|
354
|
+
return { words: rulesSummary(rules), votes: runtime.store.approvalVotes(taskId).map(one => one.approver), needsTwo: gate.verdict === "vote" || (gate.verdict === "seal" && gate.protectedWork) };
|
|
355
|
+
})(),
|
|
356
|
+
plan: ref?.plan ?? null,
|
|
357
|
+
planAuto: planAutoPending(runtime.store, taskId, now),
|
|
358
|
+
planDocument: planView?.document ?? null,
|
|
359
|
+
planSha: planView?.sha256 ?? null,
|
|
360
|
+
planContract: ref === null || planView === null ? null : planContractViewOf(ref.id, scope),
|
|
361
|
+
deliverable: ref?.deliverable ?? "branch",
|
|
362
|
+
report: ref === null ? null : readVerifiedReport(runtime.store, runtime.evidenceRoot, ref.id),
|
|
363
|
+
revision,
|
|
364
|
+
planRevisions,
|
|
365
|
+
milestoneProgress,
|
|
366
|
+
// v40: the fallback for a task page with no completed run yet (a
|
|
367
|
+
// freshly drafted, unapproved repair) — completion's own branches
|
|
368
|
+
// cover every case once a run exists.
|
|
369
|
+
repairChain: completion !== null ? null : runtime.store.repairChainForDraft(taskId),
|
|
370
|
+
repo: ref?.repo ?? null,
|
|
371
|
+
filedVia: runtime.store.filedViaOf(taskId),
|
|
372
|
+
coordinator: (() => {
|
|
373
|
+
const who = runtime.store.coordinatorProvenanceOf(taskId);
|
|
374
|
+
if (who === null)
|
|
375
|
+
return null;
|
|
376
|
+
// RELATIVE age (round-2 finding 5): "12m ago" reads at a glance;
|
|
377
|
+
// an absolute stamp makes the operator do arithmetic.
|
|
378
|
+
const ago = who.filedAt === null
|
|
379
|
+
? null
|
|
380
|
+
: (() => {
|
|
381
|
+
const minutes = Math.max(0, Math.round((now.getTime() - new Date(who.filedAt).getTime()) / 60_000));
|
|
382
|
+
if (minutes < 60)
|
|
383
|
+
return `${minutes}m ago`;
|
|
384
|
+
if (minutes < 60 * 24)
|
|
385
|
+
return `${Math.round(minutes / 60)}h ago`;
|
|
386
|
+
return `${Math.round(minutes / (60 * 24))}d ago`;
|
|
387
|
+
})();
|
|
388
|
+
return { label: who.label, filedAgo: ago };
|
|
389
|
+
})(),
|
|
390
|
+
holds: ref === null ? [] : runtime.store.activeHolds(ref.id, now),
|
|
391
|
+
contest: (() => {
|
|
392
|
+
if (ref === null)
|
|
393
|
+
return null;
|
|
394
|
+
const open = runtime.store.contestNeedingOperator(ref.id);
|
|
395
|
+
return open === null ? null : { id: open.id, state: open.state, agents: runtime.store.contestants(open.id).length, kind: open.kind };
|
|
396
|
+
})(),
|
|
397
|
+
claimed: ref === null ? false : runtime.store.hasLiveClaim(ref.id, now),
|
|
398
|
+
// The chain, both directions of trust: blockers outside the ceiling
|
|
399
|
+
// are named but wear no state and no link (same redaction the board
|
|
400
|
+
// applies to blockerState).
|
|
401
|
+
waitsFor: runtime.store.blockers(taskId).map(blockerId => {
|
|
402
|
+
const blockerRef = runtime.store.lookupRef(blockerId);
|
|
403
|
+
const admitted = blockerRef !== null && runtime.visible(blockerRef.repo);
|
|
404
|
+
const blocker = admitted ? runtime.store.getTask(blockerId) : null;
|
|
405
|
+
return { id: !admitted && runtime.restricted() ? "restricted task" : blockerId, title: blocker === null ? null : blocker.title, state: blocker === null ? null : blocker.state, admitted };
|
|
406
|
+
}),
|
|
407
|
+
// Candidates a "wait for" or replacement select may offer: this
|
|
408
|
+
// TASK's own project, even when the sidebar is in all-project mode.
|
|
409
|
+
// That keeps repair complete without ever mixing unrelated projects.
|
|
410
|
+
waitCandidates: (() => {
|
|
411
|
+
if (who.via !== "cookie")
|
|
412
|
+
return [];
|
|
413
|
+
const candidateRepo = ref?.repo ?? who.session.project;
|
|
414
|
+
return candidateRepo === null
|
|
415
|
+
? []
|
|
416
|
+
: runtime.store
|
|
417
|
+
.listTasksScoped(candidateRepo, undefined, 100, null)
|
|
418
|
+
.filter(one => one.id !== taskId && (one.state === "queued" || one.state === "running") && runtime.visible(one.repo))
|
|
419
|
+
.map(one => ({ id: one.id, title: one.title }));
|
|
420
|
+
})(),
|
|
421
|
+
// The one liveness fact, computed here where the store is: the run
|
|
422
|
+
// whose lease is the task's CURRENT claim — not merely the first
|
|
423
|
+
// unfinished run (round-4 finding, A1).
|
|
424
|
+
liveRunId: (() => {
|
|
425
|
+
if (ref === null)
|
|
426
|
+
return null;
|
|
427
|
+
const found = runtime.store.runsFor(ref.id).find(one => runIsLive(one));
|
|
428
|
+
return found === undefined ? null : found.id;
|
|
429
|
+
})(),
|
|
430
|
+
worker: (() => {
|
|
431
|
+
const runners = runtime.store.listRunners().filter(one => one.retiredAt === null);
|
|
432
|
+
const eligible = ref?.repo === null || ref?.repo === undefined
|
|
433
|
+
? []
|
|
434
|
+
: runners.filter(one => one.repos.includes(ref.repo));
|
|
435
|
+
const answering = eligible.filter(one => runnerAlive(one, now));
|
|
436
|
+
return {
|
|
437
|
+
answering: answering.length,
|
|
438
|
+
registered: eligible.length,
|
|
439
|
+
totalRegistered: runtime.restricted() ? eligible.length : runners.length,
|
|
440
|
+
lastHeard: eligible.map(one => one.heartbeatAt).sort().at(-1) ?? null,
|
|
441
|
+
};
|
|
442
|
+
})(),
|
|
443
|
+
gaps: ref?.repo === null || ref?.repo === undefined
|
|
444
|
+
? []
|
|
445
|
+
: computeGaps(runtime.store, ref.repo, now).filter(one => one.unblocks.includes(taskId) || one.alsoBlocks.includes(taskId)),
|
|
446
|
+
peekable: runtime.options.localRunner !== undefined,
|
|
447
|
+
position: runtime.store.queuePosition(taskId),
|
|
448
|
+
mirror: runtime.store.mirrorByTask(taskId),
|
|
449
|
+
scope,
|
|
450
|
+
raceTerms,
|
|
451
|
+
approvalDigest,
|
|
452
|
+
// The phase route (v47): one projection for the card, the ceremony,
|
|
453
|
+
// and the focused chat, with the readiness this task's runners report.
|
|
454
|
+
route: routeView,
|
|
455
|
+
canEditRoute: who.role === "approver",
|
|
456
|
+
spendDefaults: runtime.store.getSpendDefaults(),
|
|
457
|
+
permissionDefault: runtime.store.permissionDefault().mode,
|
|
458
|
+
permissionMode: ref?.permissionMode ?? null,
|
|
459
|
+
qualityDefault: runtime.store.qualityDefault().mode,
|
|
460
|
+
qualityMode: ref?.qualityMode ?? null,
|
|
461
|
+
publication: (() => {
|
|
462
|
+
// The latest publication across this task's runs, with its
|
|
463
|
+
// OBSERVED CI state (audit SD-5): the reviewer learns PR and CI
|
|
464
|
+
// here instead of spelunking run pages.
|
|
465
|
+
if (ref === null)
|
|
466
|
+
return null;
|
|
467
|
+
for (const one of runtime.store.runsFor(ref.id)) {
|
|
468
|
+
const found = runtime.store.publicationForRun(one.id);
|
|
469
|
+
if (found !== null)
|
|
470
|
+
return found;
|
|
471
|
+
}
|
|
472
|
+
return null;
|
|
473
|
+
})(),
|
|
474
|
+
runs,
|
|
475
|
+
completion,
|
|
476
|
+
decisions: ref === null ? [] : runtime.store.decisionsForTask(ref.id),
|
|
477
|
+
incidents: ref === null ? [] : runtime.store.incidentsForTask(ref.id),
|
|
478
|
+
coordinatorProposals: (() => {
|
|
479
|
+
if (ref === null || ref.repo === null || who.role !== "approver")
|
|
480
|
+
return null;
|
|
481
|
+
runtime.store.sweepCoordinatorProposals(now);
|
|
482
|
+
const rows = runtime.store.listCoordinatorProposals({ repos: [ref.repo], taskId });
|
|
483
|
+
return { rows, decisions: decisionsFor(runtime.store, rows), now };
|
|
484
|
+
})(),
|
|
485
|
+
steering: ref === null ? [] : runtime.store.listSteerNotes(ref.id),
|
|
486
|
+
// "publishes as" reads ONLY publicationGrantFor(repo) — the grant
|
|
487
|
+
// the publisher would act under — never listGrants(), which is
|
|
488
|
+
// dispatch authority (slice 1c).
|
|
489
|
+
grant: ref === null || ref.repo === null ? null : runtime.store.publicationGrantFor(ref.repo),
|
|
490
|
+
csrf: who.via === "cookie" ? who.session.csrf : "",
|
|
491
|
+
nonce,
|
|
492
|
+
problem,
|
|
493
|
+
attended: (() => {
|
|
494
|
+
if (runtime.restricted() || runtime.options.attended === undefined || ref === null || who.via !== "cookie" || runtime.store.isDemo())
|
|
495
|
+
return null;
|
|
496
|
+
const open = runtime.store.openAuthorizationFor(ref.id);
|
|
497
|
+
if (open !== null) {
|
|
498
|
+
const spent = runtime.store.authorizationSpendMicrousd(open.id);
|
|
499
|
+
const turnsUsed = open.attemptRun === null ? 0 : runtime.store.sessionTurnsOf(open.attemptRun).length;
|
|
500
|
+
const state = attendedWatchWords(open.lastBeatAt, now, open.absoluteExpiry);
|
|
501
|
+
return {
|
|
502
|
+
canMint: false,
|
|
503
|
+
open: {
|
|
504
|
+
id: open.id,
|
|
505
|
+
state,
|
|
506
|
+
expiresAt: open.absoluteExpiry,
|
|
507
|
+
turnsUsed,
|
|
508
|
+
cap: open.maxSessionTurns,
|
|
509
|
+
spentMicrousd: spent,
|
|
510
|
+
budgetMicrousd: open.budgetMicrousd,
|
|
511
|
+
running: open.attemptRun !== null,
|
|
512
|
+
},
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
const canMint = scope !== null &&
|
|
516
|
+
!approvalOf(scope).approved &&
|
|
517
|
+
scope.profileState === "resolved" &&
|
|
518
|
+
(scope.profile?.provider ?? "") === "claude" &&
|
|
519
|
+
ref.repo !== null &&
|
|
520
|
+
runtime.store.activeTournamentTerms(ref.id) === null &&
|
|
521
|
+
runtime.store.getTask(taskId)?.state === "queued";
|
|
522
|
+
if (!canMint)
|
|
523
|
+
return { canMint, open: null };
|
|
524
|
+
const pinnedModel = scope?.profile?.provider === "claude" ? scope.profile.model : "";
|
|
525
|
+
const configured = ["plan", "build", "repair", "review"]
|
|
526
|
+
.map(phase => runtime.store.phaseConfig(INSTALLATION_SCOPE, phase))
|
|
527
|
+
.filter((row) => row !== null && row.provider === "claude" && row.model !== null)
|
|
528
|
+
.map(row => row.model);
|
|
529
|
+
const models = [...new Set([pinnedModel, ...configured])].filter(model => model !== "");
|
|
530
|
+
const liveMode = ref.repo === null ? null : runtime.store.activeMode(ref.repo, now);
|
|
531
|
+
const modeTerms = liveMode === null ? null : modeTermsFromJson(liveMode.termsJson);
|
|
532
|
+
return {
|
|
533
|
+
canMint,
|
|
534
|
+
mint: {
|
|
535
|
+
models,
|
|
536
|
+
pinnedModel,
|
|
537
|
+
posture: modeTerms?.permissionDefault === "escalated" ? "bypassPermissions" : "auto",
|
|
538
|
+
quick: modeTerms?.quickMint === true && liveMode !== null && liveMode.signedBy === who.name,
|
|
539
|
+
},
|
|
540
|
+
open: null,
|
|
541
|
+
};
|
|
542
|
+
})(),
|
|
543
|
+
now,
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
/** The Agents view (v47): the task's route from the ONE resolver dispatch
|
|
547
|
+
* uses — sealed, proposed, live, a proven pre-routing profile, or
|
|
548
|
+
* unreadable — projected with the readiness its runners have reported,
|
|
549
|
+
* and the edit posture an approver's controls need. The scope digest
|
|
550
|
+
* rides along so every change form can CAS against exactly what was
|
|
551
|
+
* rendered (an empty value means "I saw no scope"). */
|
|
552
|
+
function routeViewOf(taskId, ref, scope, now, who) {
|
|
553
|
+
if (ref === null)
|
|
554
|
+
return null;
|
|
555
|
+
const routed = routeOfTask(runtime.store, taskId, ref, now);
|
|
556
|
+
if (routed === null)
|
|
557
|
+
return null;
|
|
558
|
+
const live = runtime.store.hasLiveClaim(ref.id, now);
|
|
559
|
+
const raced = runtime.store.activeTournamentTerms(ref.id) !== null;
|
|
560
|
+
const shared = {
|
|
561
|
+
riskLevel: ref.riskLevel ?? scope?.riskLevel ?? "routine",
|
|
562
|
+
overrides: ref.routeOverrides ?? [],
|
|
563
|
+
editable: who.role === "approver" && !live && !raced,
|
|
564
|
+
editableWhy: who.role !== "approver" ? "your login can watch — choosing agents is an approver's act" : live ? "this task is running — its agents cannot change under a live claim" : raced ? "tournament terms are on file — its lanes decide the agents" : null,
|
|
565
|
+
replanOnPlanChange: ref.plan === "drafted" && scope !== null && !approvalOf(scope).approved,
|
|
566
|
+
digest: scope?.digest ?? null,
|
|
567
|
+
choices: agentChoicesFor(runtime.store, ref.repo, routed.kind === "route" ? routed.route : null),
|
|
568
|
+
};
|
|
569
|
+
if (routed.kind === "route") {
|
|
570
|
+
return { kind: "route", source: routed.source, projection: projectRoute(routed.route, runtime.store.readinessLookupFor(ref.repo, ref.assignedRunner, now)), legacy: null, problem: null, ...shared };
|
|
571
|
+
}
|
|
572
|
+
if (routed.kind === "legacy") {
|
|
573
|
+
return { kind: "legacy", source: null, projection: null, legacy: { provider: routed.profile.provider, model: routed.profile.model, repairModel: routed.profile.repairModel === "inherit" ? routed.profile.model : routed.profile.repairModel, approved: routed.approved }, problem: null, ...shared };
|
|
574
|
+
}
|
|
575
|
+
return { kind: "unreadable", source: null, projection: null, legacy: null, problem: routed.problem, ...shared };
|
|
576
|
+
}
|
|
577
|
+
/** Resolve a task-scoped chat lens without trusting its query string.
|
|
578
|
+
* Only an admitted task becomes model context or visible page copy. */
|
|
579
|
+
function taskChatFocus(taskId, now, who, options = {}) {
|
|
580
|
+
if (taskId === null || taskId.length === 0 || taskId.length > 64 || hasForbiddenControls(taskId))
|
|
581
|
+
return null;
|
|
582
|
+
const family = runtime.familyOf(taskId);
|
|
583
|
+
if (family === null)
|
|
584
|
+
return null;
|
|
585
|
+
taskId = family.current.id;
|
|
586
|
+
const task = family.current;
|
|
587
|
+
const ref = runtime.store.lookupRef(taskId);
|
|
588
|
+
const scope = runtime.store.getScope(taskId);
|
|
589
|
+
// The focused chat is a lens over the task page's own assembled facts.
|
|
590
|
+
// Reusing that projection keeps approval nonces, joint race digests,
|
|
591
|
+
// revision verification, decisions, and result evidence on one source
|
|
592
|
+
// of truth instead of growing a chat-only lifecycle.
|
|
593
|
+
const view = who === undefined || family.problem !== null ? null : taskViewData(taskId, who, null, options);
|
|
594
|
+
const runs = view?.runs ?? runtime.store.runsFor(ref.id);
|
|
595
|
+
const latest = runs.find(runIsTaskResult) ?? null;
|
|
596
|
+
const approval = approvalOf(scope);
|
|
597
|
+
const live = runs.find(one => runIsLive(one)) ?? null;
|
|
598
|
+
const planRevisions = view?.planRevisions ?? revisionLedgerOf(ref.id);
|
|
599
|
+
return {
|
|
600
|
+
id: family.root.id,
|
|
601
|
+
executionId: task.id,
|
|
602
|
+
family,
|
|
603
|
+
guide: guided(family.root.id, now),
|
|
604
|
+
history: familyHistory(family),
|
|
605
|
+
title: family.root.title,
|
|
606
|
+
state: task.state,
|
|
607
|
+
status: view?.status ?? workRowOf({ ...task, repo: ref.repo }, now).status,
|
|
608
|
+
assignment: view?.assignment ?? assignmentOf(runtime.store, task.id, now, runtime.workAccess(), runtime.evidenceRoot),
|
|
609
|
+
project: ref.repo === null ? null : projectName(ref.repo),
|
|
610
|
+
now,
|
|
611
|
+
dispatch: diagnoseTaskDispatch(runtime.store, taskId, now),
|
|
612
|
+
scope: scope === null ? "none" : approval.approved ? "approved" : "needs approval",
|
|
613
|
+
plan: ref.plan,
|
|
614
|
+
// Adaptive execution plans (v44): the identical ledger/progress
|
|
615
|
+
// projection the task page renders (c2) — reused from `view` when
|
|
616
|
+
// available, computed fresh only when this call has no `who`.
|
|
617
|
+
planRevisions,
|
|
618
|
+
milestoneProgress: view?.milestoneProgress ?? progressOf(ref.id, planRevisions?.current?.document ?? null),
|
|
619
|
+
// What a failed task missed, said as the task page says it.
|
|
620
|
+
failure: task.state === "failed" ? failureOf(family.versions.flatMap(version => runtime.store.runsFor(version.refId)), view?.assignment ?? assignmentOf(runtime.store, task.id, now, runtime.workAccess(), runtime.evidenceRoot)) : null,
|
|
621
|
+
claimed: view?.claimed ?? runtime.store.hasLiveClaim(ref.id, now),
|
|
622
|
+
liveRun: live === null ? null : { id: live.id, runner: live.runner, startedAt: live.startedAt, phase: live.phase },
|
|
623
|
+
checkProgress: view?.checkProgress ?? (live === null && latest === null ? null : runtime.store.checkProgress((live ?? latest).id)),
|
|
624
|
+
control: view?.control ?? taskControlOf(runtime.store, ref.id, now),
|
|
625
|
+
route: view?.route ?? null,
|
|
626
|
+
approval: view === null || who?.role !== "approver" || scope === null || approval.approved
|
|
627
|
+
? null
|
|
628
|
+
: {
|
|
629
|
+
scope,
|
|
630
|
+
nonce: view.nonce,
|
|
631
|
+
digest: view.approvalDigest ?? scope.digest,
|
|
632
|
+
planDocument: view.planDocument,
|
|
633
|
+
planContract: view.planContract ?? null,
|
|
634
|
+
deliverable: view.deliverable ?? "branch",
|
|
635
|
+
raceTerms: view.raceTerms ?? null,
|
|
636
|
+
revision: view.revision ?? null,
|
|
637
|
+
coordinator: view.coordinator ?? null,
|
|
638
|
+
repairChain: view.repairChain ?? null,
|
|
639
|
+
},
|
|
640
|
+
decisions: (view?.decisions ?? runtime.store.decisionsForTask(ref.id))
|
|
641
|
+
.filter(one => one.state === "open" || one.state === "expired")
|
|
642
|
+
.map(one => ({ ...one, taskId: task.id, repo: ref.repo })),
|
|
643
|
+
publication: view?.publication ?? null,
|
|
644
|
+
result: view?.completion?.receipt !== undefined && view.completion.receipt !== null
|
|
645
|
+
? view.completion.receipt
|
|
646
|
+
: latest === null || (latest.outcome !== "built" && latest.outcome !== "no-change")
|
|
647
|
+
? null
|
|
648
|
+
: completionReceiptView(runtime.store, latest, runtime.store.artifactsFor(latest.id), runtime.evidenceRoot, runtime.reviewFactsFor(latest.id)),
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
/** The newest pull request any version of this task opened through Complete; else, for a result completed
|
|
652
|
+
* without one, the offer to open it (an approver's browser session only). */
|
|
653
|
+
/** Where "Complete and open a pull request" would open one for this result, or null when it isn't offered. */
|
|
654
|
+
function pullRequestTargetOf(runId) {
|
|
655
|
+
const run = runtime.store.getRun(runId);
|
|
656
|
+
const publishing = publishingOf(runtime.store, run === null ? null : runtime.store.refById(run.taskRef)?.repo ?? null);
|
|
657
|
+
if (!publishing.on || publishing.legacy || runtime.store.publicationForRun(runId) !== null || pullRequestBlocker(runtime.store, runId) !== null)
|
|
658
|
+
return null;
|
|
659
|
+
return `${publishing.githubRepo} into ${publishing.base}`;
|
|
660
|
+
}
|
|
661
|
+
function taskPullRequestOf(taskId, who) {
|
|
662
|
+
const family = runtime.familyOf(taskId);
|
|
663
|
+
const rootId = family?.root.id ?? taskId;
|
|
664
|
+
const repo = runtime.store.lookupRef(taskId)?.repo ?? null;
|
|
665
|
+
const publishing = publishingOf(runtime.store, repo);
|
|
666
|
+
const target = publishing.on ? publishing.base : null;
|
|
667
|
+
const view = newestPullRequestOf(runtime.store, family === null ? [taskId] : family.versions.map(one => one.id));
|
|
668
|
+
if (view !== null) {
|
|
669
|
+
const publication = runtime.store.publicationForRun(view.runId);
|
|
670
|
+
const fact = publication === null ? null : { ...pullRequestFactOf({ ...publication, merged: view.state === "merged" }),
|
|
671
|
+
note: view.state === "merged" ? `${view.detail}${view.mergeCommit === null ? "" : ` Merge commit ${view.mergeCommit.slice(0, 12)}.`}` : null };
|
|
672
|
+
return { taskId: rootId, view: { ...view, canMerge: view.canMerge && who.role === "approver" && who.via === "cookie" }, offer: null, target, fact };
|
|
673
|
+
}
|
|
674
|
+
if (!publishing.on || who.via !== "cookie" || who.role !== "approver")
|
|
675
|
+
return null;
|
|
676
|
+
const assignment = assignmentOf(runtime.store, taskId, runtime.clock(), runtime.workAccess(), runtime.evidenceRoot);
|
|
677
|
+
const receipt = assignment?.receipt ?? null;
|
|
678
|
+
if (assignment?.state !== "complete" || receipt === null || runtime.store.publicationForRun(receipt.runId) !== null || pullRequestBlocker(runtime.store, receipt.runId) !== null)
|
|
679
|
+
return null;
|
|
680
|
+
return { taskId: rootId, view: null, offer: { taskId: receipt.taskId, runId: receipt.runId, digest: receipt.digest }, target };
|
|
681
|
+
}
|
|
682
|
+
/** What went wrong with the latest finished attempt (whatever its outcome, never an older failure). */
|
|
683
|
+
function failureOf(runs, assignment, readLog = true) {
|
|
684
|
+
const last = latestFinishedAttempt(runs);
|
|
685
|
+
if (last === null)
|
|
686
|
+
return { kind: "reason", line: NO_REASON_RECORDED, evidence: null, suggestion: GENERAL_SUGGESTION, link: null };
|
|
687
|
+
const full = runtime.store.getRun(last.id);
|
|
688
|
+
return full === null ? { kind: "reason", line: failedAttemptSentence(last.reason), evidence: null, suggestion: stopSuggestionOf(last.reason), link: null }
|
|
689
|
+
: explainAttempt(full, assignment?.receipt?.runId === last.id ? assignment.receipt : null, readLog);
|
|
690
|
+
}
|
|
691
|
+
/** One finished attempt's failure in plain words: a failing check's last error line (linked to that line in its
|
|
692
|
+
* saved log), else the first signed requirement it missed with the evidence line behind it, else its recorded reason
|
|
693
|
+
* in plain words (or that none was recorded). Each comes with one suggestion of what to change, and a link to the
|
|
694
|
+
* attempt's own result page, where its changes, checks and failure are; machine output reads as an internal error,
|
|
695
|
+
* its detail behind a link to that line of the attempt's record. `readLog` false (a list's rows) reads no saved
|
|
696
|
+
* check log: the requirement or the reason, from the database alone. */
|
|
697
|
+
function explainAttempt(run, receipt, readLog = true) {
|
|
698
|
+
const taskId = runtime.store.externalIdFor(run.taskRef) ?? "";
|
|
699
|
+
const page = `${reviewHref(taskId)}&run=${run.id}`;
|
|
700
|
+
const see = taskId === "" ? null : { label: `See build #${run.id}`, href: page };
|
|
701
|
+
const artifacts = runtime.store.artifactsFor(run.id);
|
|
702
|
+
const logArtifact = !readLog ? null : receipt?.checks.status === "failed" && receipt.checks.logArtifactId !== null
|
|
703
|
+
? artifacts.find(one => one.id === receipt.checks.logArtifactId) ?? null
|
|
704
|
+
: [...artifacts].reverse().find(one => one.kind === "check-log") ?? null;
|
|
705
|
+
if (logArtifact !== null) {
|
|
706
|
+
let read = null;
|
|
707
|
+
try {
|
|
708
|
+
read = readVerifiedArtifact(runtime.evidenceRoot, logArtifact);
|
|
709
|
+
}
|
|
710
|
+
catch {
|
|
711
|
+
read = null;
|
|
712
|
+
}
|
|
713
|
+
const text = read !== null && read.ok ? read.content.toString("utf8") : null;
|
|
714
|
+
const exit = text === null ? null : /^\(exit (-?[0-9]+)\)$/m.exec(text);
|
|
715
|
+
const failedCheck = receipt?.checks.status === "failed" || (exit !== null && exit !== undefined && Number(exit[1]) !== 0);
|
|
716
|
+
const found = text !== null && failedCheck ? lastErrorLineOf(text) : null;
|
|
717
|
+
if (found !== null) {
|
|
718
|
+
const line = oneLineOf(found.text, 140);
|
|
719
|
+
return { kind: "check", line: `The check failed: ${line}`, evidence: null, suggestion: failingCheckSuggestion(line),
|
|
720
|
+
link: taskId === "" ? null : { label: `Check output, line ${found.line}`, href: `${page}&tab=checks#check-log-L${found.line}` } };
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
const verdict = runtime.store.proofVerdictFor(run.id);
|
|
724
|
+
const missed = verdict === null ? null : missedRequirementOf(verdict.matrix);
|
|
725
|
+
if (missed !== null) {
|
|
726
|
+
return { kind: "requirement", line: missedRequirementLine(missed.statement), evidence: missed.evidence === null ? null : oneLineOf(missed.evidence, 240),
|
|
727
|
+
suggestion: missedRequirementSuggestion(missed.statement), link: see };
|
|
728
|
+
}
|
|
729
|
+
const internal = isInternalErrorReason(run.reason);
|
|
730
|
+
return { kind: "reason", line: failedAttemptSentence(run.reason), evidence: null, suggestion: stopSuggestionOf(run.reason),
|
|
731
|
+
link: internal ? { label: "The recorded error", href: `/r/${run.id}?record=1#run-reason-detail` } : see };
|
|
732
|
+
}
|
|
733
|
+
/** Run checks in place on a result's commit: an approver's browser session, outside the demo, when the project has a
|
|
734
|
+
* check and none is waiting or running on it. Comes back to `returnTo`. */
|
|
735
|
+
function runChecksHere(runId, who, returnTo) {
|
|
736
|
+
if (runId === null || who.via !== "cookie" || who.role !== "approver" || runtime.store.isDemo())
|
|
737
|
+
return null;
|
|
738
|
+
const run = runtime.store.getRun(runId);
|
|
739
|
+
const followUps = run === null ? null : followUpsFor(runtime.store, runtime.evidenceRoot, run, runtime.clock());
|
|
740
|
+
if (followUps === null || !(followUps.quick || followUps.full) || followUps.checks.some(one => one.state === "waiting" || one.state === "running"))
|
|
741
|
+
return null;
|
|
742
|
+
return { action: `/r/${runId}/checks`, level: followUps.full ? "full" : "quick", returnTo };
|
|
743
|
+
}
|
|
744
|
+
function taskScreen(response, who, taskId, problem, status, scopeDraft, cancelDraft, editPlan = false) {
|
|
745
|
+
const admittedFamily = runtime.familyOf(taskId);
|
|
746
|
+
if (admittedFamily?.problem != null)
|
|
747
|
+
return runtime.sendScreen(response, status, screen(admittedFamily.root.title, `<h1>${escape(admittedFamily.root.title)}</h1><p>${escape(admittedFamily.current.state)}</p><p class="problem" data-history-problem>${escape(admittedFamily.problem)}</p>`, { chrome: runtime.chromeFor(admittedFamily.root.repo, "tasks") }));
|
|
748
|
+
const data = taskViewData(taskId, who, problem);
|
|
749
|
+
if (data === null)
|
|
750
|
+
return refuse(response, who, 404, "no such task", "/tasks");
|
|
751
|
+
const family = runtime.familyOf(taskId);
|
|
752
|
+
const presentedData = { ...data, rootId: family?.root.id ?? taskId, rootTitle: family?.root.title ?? data.task.title,
|
|
753
|
+
history: family === null || data.assignment != null ? "" : familyHistory(family),
|
|
754
|
+
versionLabel: family !== null && family.current.id !== taskId ? `Viewing ${family.versions.findIndex(one => one.id === taskId) === 0 ? "Original" : `Revision ${family.versions.findIndex(one => one.id === taskId)}`} · ${family.current.state === "running" ? "A newer revision is running" : "A newer revision is current"}` : null };
|
|
755
|
+
if (scopeDraft !== undefined)
|
|
756
|
+
presentedData.scopeDraft = scopeDraft;
|
|
757
|
+
if (cancelDraft !== undefined)
|
|
758
|
+
presentedData.cancelDraft = cancelDraft;
|
|
759
|
+
if (editPlan)
|
|
760
|
+
presentedData.editPlan = true;
|
|
761
|
+
presentedData.pullRequest = taskPullRequestOf(taskId, who);
|
|
762
|
+
// The thread reads the whole family: the original, its revisions, and every attempt across them.
|
|
763
|
+
if (family !== null && family.problem === null) {
|
|
764
|
+
presentedData.family = { root: { id: family.root.id, title: family.root.title, createdAt: family.root.createdAt, goal: runtime.store.getScope(family.root.id)?.goal ?? null },
|
|
765
|
+
versions: family.versions.map(one => ({ id: one.id, title: one.title, state: one.state })),
|
|
766
|
+
runs: family.versions.flatMap(version => version.id === data.task.id ? data.runs.map(run => ({ ...run, taskId: version.id })) : runtime.store.runsFor(version.refId).map(run => ({ ...run, taskId: version.id }))) };
|
|
767
|
+
}
|
|
768
|
+
// What went wrong reads the family's latest finished attempt, as the Tasks row does.
|
|
769
|
+
presentedData.failure = failureOf(presentedData.family?.runs ?? data.runs, data.assignment ?? null);
|
|
770
|
+
presentedData.runChecks = runChecksHere(data.assignment?.receipt?.runId ?? null, who, taskHref(taskId));
|
|
771
|
+
const liveRun = data.liveRunId == null ? undefined : data.runs.find(one => one.id === data.liveRunId);
|
|
772
|
+
if (liveRun !== undefined)
|
|
773
|
+
presentedData.activity = runActivityOf(runtime.store, liveRun, runtime.clock());
|
|
774
|
+
presentedData.demo = runtime.store.isDemo();
|
|
775
|
+
const paneProject = runtime.restricted() ? runtime.store.lookupRef(taskId)?.repo ?? null : who.via === "cookie" ? who.session.project : null;
|
|
776
|
+
const page = taskPage(paneProject === null && !runtime.unscopedMode
|
|
777
|
+
? runtime.chromeFor(paneProject, "tasks")
|
|
778
|
+
: runtime.chromeFor(paneProject, "tasks", taskListPane(paneProject, family?.root.id ?? taskId)), presentedData);
|
|
779
|
+
// The task's own conversation, docked beside the page (v77).
|
|
780
|
+
const focus = taskChatFocus(family?.root.id ?? taskId, runtime.clock(), who, { mintNonce: false });
|
|
781
|
+
const docked = focus === null ? null : runtime.dockedConversation(who, focus, null, runtime.clock(), taskHref(focus.id));
|
|
782
|
+
// The conversation is the page's own thread here, so the page drops its Ask tab;
|
|
783
|
+
// only an approver is offered a road to message the agent.
|
|
784
|
+
const taskView = page.workspace?.view?.kind === "task" ? { ...page.workspace.view, ...(docked === null ? {} : { tabs: [] }), ...(who.role === "approver" ? {} : { chatHref: null }),
|
|
785
|
+
// A signed-in page hears the moment the task changes, and who else has it open.
|
|
786
|
+
...(who.via === "cookie" ? { live: { href: `${taskHref(family?.root.id ?? taskId)}/live`, at: (() => { try {
|
|
787
|
+
return taskFingerprint(runtime.store, family?.root.id ?? taskId, runtime.clock());
|
|
788
|
+
}
|
|
789
|
+
catch {
|
|
790
|
+
return null;
|
|
791
|
+
} })() } } : {}) } : page.workspace?.view;
|
|
792
|
+
if (docked !== null)
|
|
793
|
+
page.workspace = { ...page.workspace, ...(taskView === undefined ? {} : { view: taskView }), conversation: docked, pageHtml: page.body };
|
|
794
|
+
else if (taskView !== undefined && page.workspace !== undefined)
|
|
795
|
+
page.workspace = { ...page.workspace, view: taskView };
|
|
796
|
+
page.refreshSeconds = runtime.liveRefreshSeconds();
|
|
797
|
+
return runtime.sendScreen(response, status, page);
|
|
798
|
+
}
|
|
799
|
+
/** Shared by task controls and chat proposals; both callers authorize the task first. */
|
|
800
|
+
function armTaskResume(response, who, taskId, named, returnTo, now) {
|
|
801
|
+
// The resume ceremony's first half (v52): minted by THIS POST, never
|
|
802
|
+
// a GET. The nonce is bound to the exact run, this approver, and a
|
|
803
|
+
// digest of the facts the confirmation restates — the run, its
|
|
804
|
+
// settlement, and the scope's current approval — so a page read
|
|
805
|
+
// before the approval changed cannot resume under the new terms.
|
|
806
|
+
if (who.via !== "cookie")
|
|
807
|
+
return refuse(response, who, 403, "resuming a task is a browser session's act");
|
|
808
|
+
if (who.role !== "approver")
|
|
809
|
+
return taskScreen(response, who, taskId, "your login can watch — resuming an attempt is an approver's act", 403);
|
|
810
|
+
if (runtime.store.isDemo())
|
|
811
|
+
return taskScreen(response, who, taskId, "the demo authorizes nothing", 403);
|
|
812
|
+
const ref = runtime.store.lookupRef(taskId);
|
|
813
|
+
if (ref === null)
|
|
814
|
+
return taskScreen(response, who, taskId, "That task is not available.", 404);
|
|
815
|
+
if (!/^[0-9]{1,15}$/.test(named))
|
|
816
|
+
return taskScreen(response, who, taskId, "which attempt? the resume names the exact stopped run", 400);
|
|
817
|
+
const runId = Number(named);
|
|
818
|
+
const stop = runtime.store.stopOf(runId);
|
|
819
|
+
const run = runtime.store.getRun(runId);
|
|
820
|
+
if (stop === null || run === null || stop.taskRef !== ref.id)
|
|
821
|
+
return taskScreen(response, who, taskId, `run #${runId} is not a stopped attempt of this task`, 409);
|
|
822
|
+
if (run.role === "reviewer")
|
|
823
|
+
return taskScreen(response, who, taskId, `run #${runId} is a historical review run — nothing reruns it; open the saved result instead`, 409);
|
|
824
|
+
if (stop.resumedAt !== null)
|
|
825
|
+
return taskScreen(response, who, taskId, `run #${runId} was already resumed by ${escape(stop.resumedBy ?? "?")}`, 409);
|
|
826
|
+
const control = taskControlOf(runtime.store, ref.id, now);
|
|
827
|
+
if (control.kind !== "paused" || control.run !== runId) {
|
|
828
|
+
return taskScreen(response, who, taskId, control.kind === "stopping" ? `run #${runId} is still stopping — its processes are not yet established gone` : `run #${runId} is no longer the attempt a resume can name — reload and decide against the current state`, 409);
|
|
829
|
+
}
|
|
830
|
+
const digest = resumeDigestOf(taskId, runId, stop.settledAt ?? "", runtime.store.getScope(taskId));
|
|
831
|
+
const nonceValue = randomBytes(18).toString("base64url");
|
|
832
|
+
const minted = runtime.store.mintCeremonyNonce({ hash: nonceHashOf(nonceValue), approver: who.name, subject: "run-resume", subjectId: runId, digest, ttlMs: 15 * 60_000 }, now);
|
|
833
|
+
if (!minted.ok)
|
|
834
|
+
return taskScreen(response, who, taskId, "too many unfinished confirmations are open — finish or let them expire", 429);
|
|
835
|
+
const gate = diagnoseTaskDispatch(runtime.store, taskId, now);
|
|
836
|
+
return runtime.sendScreen(response, 200, resumeCeremonyPage(runtime.chromeFor(who.session.project, "tasks"), {
|
|
837
|
+
taskId, taskTitle: runtime.store.getTask(taskId)?.title ?? taskId, control, nonceValue, csrf: who.session.csrf,
|
|
838
|
+
returnTo,
|
|
839
|
+
gate: gate !== null && gate.code === "stopped" ? null : gate,
|
|
840
|
+
approved: (() => { const scope = runtime.store.getScope(taskId); return scope !== null && approvalOf(scope).approved; })(),
|
|
841
|
+
}));
|
|
842
|
+
}
|
|
843
|
+
/** The newest plan document for a task, verified before a byte renders.
|
|
844
|
+
* Its hash is also the plan revision the approval form was minted for. */
|
|
845
|
+
function planViewOf(taskRef) {
|
|
846
|
+
const artifact = runtime.store.latestPlanArtifact(taskRef);
|
|
847
|
+
if (artifact === null)
|
|
848
|
+
return null;
|
|
849
|
+
try {
|
|
850
|
+
const verified = readVerifiedArtifact(runtime.evidenceRoot, artifact);
|
|
851
|
+
return verified.ok ? { document: verified.content.toString("utf8"), sha256: artifact.sha256, run: artifact.run } : null;
|
|
852
|
+
}
|
|
853
|
+
catch {
|
|
854
|
+
return null;
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
/**
|
|
858
|
+
* The plan-contract record of the newest drafted plan (contract handoff,
|
|
859
|
+
* task 1): what the planner was filed, what it proposed, its explicit
|
|
860
|
+
* amendment, and every mechanical change between the two — verified
|
|
861
|
+
* before a byte renders, and marked `current` only while the proposed
|
|
862
|
+
* terms are still exactly the scope row (an operator's later edit turns
|
|
863
|
+
* the record into history, never a claim about the row).
|
|
864
|
+
*/
|
|
865
|
+
function planContractViewOf(taskRef, scope) {
|
|
866
|
+
const artifact = runtime.store.latestPlanContractArtifact(taskRef);
|
|
867
|
+
if (artifact === null)
|
|
868
|
+
return null;
|
|
869
|
+
try {
|
|
870
|
+
const verified = readVerifiedArtifact(runtime.evidenceRoot, artifact);
|
|
871
|
+
if (!verified.ok)
|
|
872
|
+
return { run: artifact.run, problem: `the contract record no longer verifies — ${verified.problem}` };
|
|
873
|
+
const record = decodePlanContractRecord(verified.content);
|
|
874
|
+
if (record === null)
|
|
875
|
+
return { run: artifact.run, problem: "the contract record is not the JSON it was sealed as" };
|
|
876
|
+
return {
|
|
877
|
+
run: artifact.run,
|
|
878
|
+
sourceDigest: record.sourceDigest,
|
|
879
|
+
filed: record.filed !== null,
|
|
880
|
+
amendment: record.amendment,
|
|
881
|
+
changes: record.changes,
|
|
882
|
+
changeWords: describeContractChanges(record.changes),
|
|
883
|
+
current: scope !== null && contractChangesOf(record.proposed, scope).length === 0,
|
|
884
|
+
revision: runtime.store.revisionSourceOf(taskRef) !== null,
|
|
885
|
+
};
|
|
886
|
+
}
|
|
887
|
+
catch {
|
|
888
|
+
return { run: artifact.run, problem: "the contract record could not be read" };
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
function revisionDocOf(row) {
|
|
892
|
+
const artifact = runtime.store.getArtifact(row.artifact);
|
|
893
|
+
if (artifact === null)
|
|
894
|
+
return null;
|
|
895
|
+
try {
|
|
896
|
+
const verified = readVerifiedArtifact(runtime.evidenceRoot, artifact);
|
|
897
|
+
if (!verified.ok)
|
|
898
|
+
return null;
|
|
899
|
+
return {
|
|
900
|
+
id: row.id,
|
|
901
|
+
revision: row.revision,
|
|
902
|
+
status: row.status,
|
|
903
|
+
reason: row.reason,
|
|
904
|
+
evidenceLink: row.evidenceLink,
|
|
905
|
+
author: row.author,
|
|
906
|
+
kind: row.kind,
|
|
907
|
+
authorityKind: row.authorityKind,
|
|
908
|
+
changedFields: row.changedFields,
|
|
909
|
+
document: verified.content.toString("utf8"),
|
|
910
|
+
sha256: artifact.sha256,
|
|
911
|
+
createdAt: row.createdAt,
|
|
912
|
+
resolvedAt: row.resolvedAt,
|
|
913
|
+
resolvedBy: row.resolvedBy,
|
|
914
|
+
};
|
|
915
|
+
}
|
|
916
|
+
catch {
|
|
917
|
+
return null;
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
/**
|
|
921
|
+
* The adaptive-plan ledger for one task: the revision currently in force,
|
|
922
|
+
* any revision still 'blocked' awaiting an operator's accept or reject,
|
|
923
|
+
* and the full immutable history. A task with no `plan_revision` rows at
|
|
924
|
+
* all — every task filed before this feature, or one whose first
|
|
925
|
+
* revision is still 'blocked' — reads `current` as a read-only
|
|
926
|
+
* projection off the existing planner artifact (`planViewOf`), never
|
|
927
|
+
* written back as a row: c1's "older builds with no checkpoints remain
|
|
928
|
+
* readable."
|
|
929
|
+
*/
|
|
930
|
+
function revisionLedgerOf(taskRef) {
|
|
931
|
+
const history = runtime.store.listPlanRevisions(taskRef).map(revisionDocOf).filter((view) => view !== null);
|
|
932
|
+
const currentRow = runtime.store.currentPlanRevision(taskRef);
|
|
933
|
+
const latestRow = runtime.store.latestPlanRevision(taskRef);
|
|
934
|
+
const pending = latestRow !== null && latestRow.status === "blocked" ? revisionDocOf(latestRow) : null;
|
|
935
|
+
let current = currentRow === null ? null : revisionDocOf(currentRow);
|
|
936
|
+
if (current === null) {
|
|
937
|
+
const legacy = planViewOf(taskRef);
|
|
938
|
+
current =
|
|
939
|
+
legacy === null
|
|
940
|
+
? null
|
|
941
|
+
: {
|
|
942
|
+
id: null,
|
|
943
|
+
revision: 1,
|
|
944
|
+
status: "applied",
|
|
945
|
+
reason: "the plan the operator approved",
|
|
946
|
+
evidenceLink: null,
|
|
947
|
+
author: "planner",
|
|
948
|
+
kind: "initial",
|
|
949
|
+
authorityKind: "plan-only",
|
|
950
|
+
changedFields: [],
|
|
951
|
+
document: legacy.document,
|
|
952
|
+
sha256: legacy.sha256,
|
|
953
|
+
createdAt: "",
|
|
954
|
+
resolvedAt: null,
|
|
955
|
+
resolvedBy: null,
|
|
956
|
+
};
|
|
957
|
+
}
|
|
958
|
+
return { current, pending, history };
|
|
959
|
+
}
|
|
960
|
+
/** The live milestone projection for a task: every milestone in the
|
|
961
|
+
* CURRENT revision's document, in order, each carrying the newest
|
|
962
|
+
* checkpoint's state for its exact id — a checkpoint filed against an
|
|
963
|
+
* older, differently-worded revision simply has no matching ids, so its
|
|
964
|
+
* milestones fall back to "pending" rather than showing stale progress
|
|
965
|
+
* under a plan that no longer says that. */
|
|
966
|
+
function progressOf(taskRef, currentDocument) {
|
|
967
|
+
if (currentDocument === null)
|
|
968
|
+
return null;
|
|
969
|
+
const parsed = parseExecutionPlanDocument(currentDocument);
|
|
970
|
+
if (!parsed.ok)
|
|
971
|
+
return null;
|
|
972
|
+
const milestones = milestonesOf(parsed.document);
|
|
973
|
+
const checkpoint = runtime.store.latestCheckpointForTask(taskRef);
|
|
974
|
+
const byId = new Map((checkpoint?.snapshot.milestones ?? []).map(entry => [entry.id, entry]));
|
|
975
|
+
return milestones.map(milestone => {
|
|
976
|
+
const entry = byId.get(milestone.id);
|
|
977
|
+
return { id: milestone.id, description: milestone.description, state: entry?.state ?? "pending", note: entry?.note ?? null };
|
|
978
|
+
});
|
|
979
|
+
}
|
|
980
|
+
/**
|
|
981
|
+
* The review cockpit's projection of ONE completed task (Priority 5):
|
|
982
|
+
* approved intent from the scope and plan already on file, the result
|
|
983
|
+
* run's sealed artifacts through the same verified readers the run page
|
|
984
|
+
* uses, the stored verdict and matrix, the contest and publication
|
|
985
|
+
* rows. Nothing here is a new record: a manual completion (no run), a
|
|
986
|
+
* legacy result (no proof verdict), and a broken artifact each read as
|
|
987
|
+
* exactly what they are. Only the selected result is enriched — the
|
|
988
|
+
* queue rows carry the done list's own facts and nothing more.
|
|
989
|
+
*/
|
|
990
|
+
/**
|
|
991
|
+
* The result detail (workspace package 3): every verified record one
|
|
992
|
+
* finished run's result presentation reads, assembled once for the run
|
|
993
|
+
* page, the review cockpit, and the chat's result view. Read-only.
|
|
994
|
+
*/
|
|
995
|
+
function resultDetailOf(run, who, now) {
|
|
996
|
+
const taskId = runtime.store.externalIdFor(run.taskRef) ?? "?";
|
|
997
|
+
const scope = runtime.store.getScope(taskId);
|
|
998
|
+
const artifacts = runtime.store.artifactsFor(run.id);
|
|
999
|
+
const terminal = terminalDiffView(artifacts, runtime.evidenceRoot);
|
|
1000
|
+
const proof = proofBundleView(runtime.store, run, artifacts, runtime.evidenceRoot);
|
|
1001
|
+
const handoff = structuredHandoffView(artifacts, runtime.evidenceRoot);
|
|
1002
|
+
const review = runtime.reviewFactsFor(run.id);
|
|
1003
|
+
const receipt = completionReceiptView(runtime.store, run, artifacts, runtime.evidenceRoot, review);
|
|
1004
|
+
// File anchors come from the PARSED sealed patch — a stat path with no
|
|
1005
|
+
// hunk in the patch gets no anchor, never a dangling one.
|
|
1006
|
+
const patch = terminal?.patch ?? null;
|
|
1007
|
+
const patchOk = patch !== null && !("problem" in patch);
|
|
1008
|
+
const parsedPaths = patchOk ? parseReviewDiff(patch.text).files.filter(one => one.hunks.length > 0).map(one => one.path) : [];
|
|
1009
|
+
const fileAnchors = new Map(parsedPaths.map(path => [path, diffFileAnchor(path)]));
|
|
1010
|
+
const statFiles = terminal?.stat !== null && terminal?.stat !== undefined && !("problem" in terminal.stat) ? terminal.stat.files : [];
|
|
1011
|
+
const cited = new Set();
|
|
1012
|
+
for (const criterion of proof?.matrix ?? [])
|
|
1013
|
+
for (const answer of criterion.answered)
|
|
1014
|
+
if (answer.kind === "changed-path")
|
|
1015
|
+
cited.add(answer.ref);
|
|
1016
|
+
const touches = scope?.touches ?? [];
|
|
1017
|
+
const files = orderChangedFiles(statFiles.map(file => ({
|
|
1018
|
+
path: file.path,
|
|
1019
|
+
additions: file.additions,
|
|
1020
|
+
deletions: file.deletions,
|
|
1021
|
+
renamedFrom: file.renamedFrom ?? null,
|
|
1022
|
+
anchor: fileAnchors.get(file.path) ?? null,
|
|
1023
|
+
outsideTouches: touches.length > 0 && !withinSignedTouches(file.path, touches),
|
|
1024
|
+
cited: cited.has(file.path),
|
|
1025
|
+
})), cited.size > 0);
|
|
1026
|
+
const publication = runtime.store.publicationForRun(run.id);
|
|
1027
|
+
let learning = "", skillTest = false;
|
|
1028
|
+
const learningRepo = runtime.store.refForId(run.taskRef)?.repo;
|
|
1029
|
+
if (learningRepo && runtime.visible(learningRepo)) {
|
|
1030
|
+
try {
|
|
1031
|
+
learning = learningHtml(learningView(runtime.store, runtime.evidenceRoot, learningRepo, who.name), who.via === "cookie" ? who.session.csrf : "", who.role === "approver", run.id);
|
|
1032
|
+
}
|
|
1033
|
+
catch { /* Learning failures remain available in Settings; the result is independent. */ }
|
|
1034
|
+
try {
|
|
1035
|
+
const test = skillTestResult(runtime.store, run.id, who.name);
|
|
1036
|
+
if (test) {
|
|
1037
|
+
skillTest = true;
|
|
1038
|
+
learning = skillTestFeedbackHtml(test, who.via === 'cookie' ? who.session.csrf : '', who.role === 'approver') + learning;
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
catch {
|
|
1042
|
+
learning = '<p role="alert">The saved skill test could not be verified.</p>' + learning;
|
|
1043
|
+
}
|
|
1044
|
+
try {
|
|
1045
|
+
learning = skillsSnapshotHtml(readSkillsSnapshot(runtime.store, run.id)) + knowledgeContextHtml(readKnowledgeSnapshot(runtime.store, run.id)) + learning;
|
|
1046
|
+
}
|
|
1047
|
+
catch {
|
|
1048
|
+
learning = '<p class="problem" role="alert">The context saved for this run could not be verified.</p>' + learning;
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
return {
|
|
1052
|
+
learning,
|
|
1053
|
+
skillTest,
|
|
1054
|
+
taskId,
|
|
1055
|
+
rootId: runtime.familyOf(taskId)?.root.id ?? taskId,
|
|
1056
|
+
history: (() => { const family = runtime.familyOf(taskId); return family === null ? "" : familyHistory(family); })(),
|
|
1057
|
+
run,
|
|
1058
|
+
receipt,
|
|
1059
|
+
assignment: (() => {
|
|
1060
|
+
const value = assignmentOf(runtime.store, taskId, now, runtime.workAccess(), runtime.evidenceRoot);
|
|
1061
|
+
return value?.receipt?.runId === run.id && value.activeTaskId === taskId ? freshAssignment(value, receipt) : null;
|
|
1062
|
+
})(),
|
|
1063
|
+
handoff,
|
|
1064
|
+
proof,
|
|
1065
|
+
terminal,
|
|
1066
|
+
publication,
|
|
1067
|
+
pullRequestTo: pullRequestTargetOf(run.id),
|
|
1068
|
+
publishing: pullRequestTargetOf(run.id) !== null ? "pull-request" : publishingOf(runtime.store, runtime.store.refById(run.taskRef)?.repo ?? null).on ? "other" : "off",
|
|
1069
|
+
ciFailing: publication !== null && publication.prNumber !== null && runtime.store.hasOpenCiEpisode(publication.githubRepo, publication.prNumber),
|
|
1070
|
+
files,
|
|
1071
|
+
outsideTouches: files.filter(one => one.outsideTouches).map(one => one.path),
|
|
1072
|
+
fileAnchors,
|
|
1073
|
+
comments: runtime.store.liveDiffComments(run.id).filter(isRevisionFeedback),
|
|
1074
|
+
pastComments: runtime.store.allDiffComments(run.id).filter(one => one.reviewerRun === null && (one.consumedBy !== null || one.supersededBy !== null)),
|
|
1075
|
+
reviewerFindings: runtime.store.allDiffComments(run.id).filter(one => one.reviewerRun !== null),
|
|
1076
|
+
automaticReview: buildReviewOf(runtime.store, run.id),
|
|
1077
|
+
revisions: runtime.store.revisionsFromRun(run.id).flatMap(one => {
|
|
1078
|
+
const child = runtime.store.getTask(one.id);
|
|
1079
|
+
if (child === null || !runtime.visible(runtime.store.lookupRef(one.id)?.repo ?? null))
|
|
1080
|
+
return [];
|
|
1081
|
+
// The same projection the Work list and the child's own page wear.
|
|
1082
|
+
const status = workRowOf({ ...child, repo: runtime.store.lookupRef(one.id)?.repo ?? null }, now).status;
|
|
1083
|
+
return [{ id: one.id, title: one.title, state: one.state, approved: approvalOf(runtime.store.getScope(one.id)).approved, standing: status.label, tone: status.tone }];
|
|
1084
|
+
}),
|
|
1085
|
+
sourceDigest: scope?.digest ?? null,
|
|
1086
|
+
followUps: followUpsFor(runtime.store, runtime.evidenceRoot, run, now),
|
|
1087
|
+
route: runtime.store.runRoute(run.id),
|
|
1088
|
+
// Editor links (arc 6): the deployment capability, THIS machine's
|
|
1089
|
+
// runner owning the run, the session's own device-side yes, and a
|
|
1090
|
+
// checkout to open — or nothing renders.
|
|
1091
|
+
editor: runtime.options.editorLinks !== undefined && runtime.options.localRunner !== undefined && run.runner === runtime.options.localRunner && who.via === "cookie" && who.session.editorLinks === true && run.worktree !== null
|
|
1092
|
+
? { worktree: run.worktree }
|
|
1093
|
+
: null,
|
|
1094
|
+
signedCriteria: scope?.acceptance.length ?? 0,
|
|
1095
|
+
canAnnotate: who.via === "cookie" && patchOk && patch.text.trim() !== "",
|
|
1096
|
+
};
|
|
1097
|
+
}
|
|
1098
|
+
return { guided, projectFamilyPeek, familiesInView, familyTasksInView, revisionDestination, earlierLiveVersions, familyHistory, freshAssignment, workRowOf, taskListPane, runIsLive, runIsTaskResult, revisionViewOf, taskViewData, routeViewOf, taskChatFocus, pullRequestTargetOf, taskPullRequestOf, failureOf, explainAttempt, runChecksHere, taskScreen, armTaskResume, planViewOf, planContractViewOf, revisionDocOf, revisionLedgerOf, progressOf, resultDetailOf };
|
|
1099
|
+
}
|