toolroll 0.9.14 → 0.9.16
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 +34 -0
- package/dist/assignment-brief.d.ts +10 -0
- package/dist/assignment-brief.js +49 -26
- package/dist/assignment-ui.d.ts +1 -0
- package/dist/assignment-ui.js +6 -4
- package/dist/assignment.d.ts +34 -1
- package/dist/assignment.js +86 -7
- package/dist/browser/workspace.css +2 -2
- package/dist/browser/workspace.js +38 -38
- package/dist/browser-crew.d.ts +2 -1
- package/dist/browser-crew.js +3 -1
- package/dist/browser-workspace.d.ts +93 -18
- package/dist/chat-actions.d.ts +1 -1
- package/dist/chat-actions.js +6 -4
- package/dist/chat-ask.d.ts +28 -0
- package/dist/chat-ask.js +77 -0
- package/dist/chat-channel.js +6 -1
- package/dist/chat-controls.d.ts +8 -0
- package/dist/chat-controls.js +3 -0
- package/dist/chat-delivery-state.d.ts +7 -2
- package/dist/chat-delivery-state.js +15 -2
- package/dist/chat-delivery.js +20 -3
- package/dist/chat-quiet.js +4 -2
- package/dist/chat-voice.d.ts +6 -9
- package/dist/chat-voice.js +11 -5
- package/dist/demo.js +82 -2
- package/dist/disclosure.d.ts +6 -0
- package/dist/disclosure.js +13 -0
- package/dist/discord-chat.js +10 -0
- package/dist/lead-commitments.d.ts +84 -0
- package/dist/lead-commitments.js +208 -0
- package/dist/lead-context.d.ts +32 -3
- package/dist/lead-context.js +133 -19
- package/dist/lead-follow.js +3 -0
- package/dist/lead-identity.d.ts +23 -0
- package/dist/lead-identity.js +33 -0
- package/dist/lead-status.js +2 -2
- package/dist/lead-voice.d.ts +12 -9
- package/dist/lead-voice.js +25 -16
- package/dist/ledger-view.js +3 -2
- package/dist/mate-cli.d.ts +1 -1
- package/dist/mate-cli.js +8 -2
- package/dist/mate-contract.d.ts +1 -1
- package/dist/mate-contract.js +11 -4
- package/dist/mate-progress.js +5 -0
- package/dist/mate-tools.d.ts +18 -1
- package/dist/mate-tools.js +250 -6
- package/dist/mate.d.ts +9 -0
- package/dist/mate.js +34 -38
- package/dist/needs-you.d.ts +48 -0
- package/dist/needs-you.js +93 -1
- package/dist/operate.js +2 -1
- package/dist/project-delete.js +3 -1
- package/dist/result-acts.d.ts +63 -5
- package/dist/result-acts.js +53 -3
- package/dist/result-review.d.ts +4 -3
- package/dist/result-review.js +7 -3
- package/dist/retention.js +6 -0
- package/dist/serve.d.ts +3 -0
- package/dist/serve.js +1220 -694
- package/dist/slack-chat.js +11 -1
- package/dist/store.d.ts +51 -0
- package/dist/store.js +115 -1
- package/dist/surface.js +2 -2
- package/dist/task-status.d.ts +26 -19
- package/dist/task-status.js +59 -21
- package/dist/team-runtime.js +2 -1
- package/dist/teams-chat.js +4 -1
- package/dist/telegram-mate.d.ts +4 -1
- package/dist/telegram-mate.js +17 -1
- package/dist/telegram-progress.js +3 -2
- package/dist/telegram.js +43 -3
- package/dist/test-temp.js +1 -1
- package/dist/transitions-recipes.d.ts +1 -1
- package/dist/transitions-recipes.js +5 -9
- package/dist/when-html.d.ts +24 -2
- package/dist/when-html.js +98 -11
- package/dist/work-index.d.ts +5 -1
- package/dist/work-index.js +22 -9
- package/dist/work-summary.d.ts +1 -1
- package/dist/workspace-motion.js +3 -3
- package/dist/workspace-ui.d.ts +44 -20
- package/dist/workspace-ui.js +63 -37
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,39 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.9.16 — 2026-10-03
|
|
4
|
+
|
|
5
|
+
- **Name your lead.** Settings → Lead takes a name and a short persona. The
|
|
6
|
+
lead uses them wherever it speaks.
|
|
7
|
+
- **The lead knows who it's talking to.** Every turn it sees your first name,
|
|
8
|
+
today's date, the channel it's on, what needs you, and your projects by name
|
|
9
|
+
with their decisions.
|
|
10
|
+
- **It checks before it speaks.** It says what it checked and marks guesses.
|
|
11
|
+
It searches project memory before recommending anything, and it checks an
|
|
12
|
+
integration before promising work that needs it.
|
|
13
|
+
- **Questions with buttons.** When it needs an answer, it asks one question
|
|
14
|
+
with tap-to-answer options in the console, Telegram, Slack, Discord and
|
|
15
|
+
Teams.
|
|
16
|
+
- **It keeps its promises.** "I'll tell you when the checks pass" is recorded
|
|
17
|
+
and reported once, on the chat where you asked. Corrections become a card
|
|
18
|
+
you confirm, and it remembers them from the next reply.
|
|
19
|
+
- Accepting a result that doesn't match its report needs a reason, enforced by
|
|
20
|
+
the server. Every time on a page shows in your time zone.
|
|
21
|
+
|
|
22
|
+
## 0.9.15 — 2026-10-02
|
|
23
|
+
|
|
24
|
+
- **Accept finishes the task.** One step, Accept and finish, after the
|
|
25
|
+
evidence. Your own checks come first, and the count updates as you answer.
|
|
26
|
+
- **Failures say what failed.** A failed task names the requirement or check
|
|
27
|
+
it missed and suggests what to change, prefilled in Retry. Failed builds get
|
|
28
|
+
a real result page. Builds show "Step 2 of 6" and say when they're stuck.
|
|
29
|
+
- **One set of words.** The same state names in the list, task page, result
|
|
30
|
+
page and Crew. Project checks and PR CI are labelled apart, and every time
|
|
31
|
+
shows in your own time zone.
|
|
32
|
+
- **Approving says what you allow.** A plain "You're allowing" line sits above
|
|
33
|
+
Approve, your password says what it signs, and Edit plan edits in place.
|
|
34
|
+
- **Safer deploys.** An interrupted deploy puts the old service and its coding
|
|
35
|
+
store back.
|
|
36
|
+
|
|
3
37
|
## 0.9.14 — 2026-10-02
|
|
4
38
|
|
|
5
39
|
- **Results and failures say what went wrong.** A result whose report doesn't
|
|
@@ -53,6 +53,7 @@ export type AssignmentCatchUp = {
|
|
|
53
53
|
decisions: {
|
|
54
54
|
id: number;
|
|
55
55
|
claim: string;
|
|
56
|
+
why: string;
|
|
56
57
|
}[];
|
|
57
58
|
};
|
|
58
59
|
}[];
|
|
@@ -65,6 +66,15 @@ export type AssignmentCatchUp = {
|
|
|
65
66
|
notes: string[];
|
|
66
67
|
};
|
|
67
68
|
};
|
|
69
|
+
/** A reason's first sentence, or its first line. */
|
|
70
|
+
export declare function firstSentenceOf(text: string): string;
|
|
71
|
+
/** A project's active decisions, newest first: each one's title and its reason in one line (first sentence or
|
|
72
|
+
* line). Memory absent on an older database reads as no decisions. */
|
|
73
|
+
export declare function activeDecisionsOf(store: Store, repo: string, text?: (value: string, cap: number) => string): {
|
|
74
|
+
id: number;
|
|
75
|
+
claim: string;
|
|
76
|
+
why: string;
|
|
77
|
+
}[];
|
|
68
78
|
/** A current snapshot, never a receipt to acknowledge. Full assignment reads
|
|
69
79
|
* remain the place to inspect exact saved work and obtain its current digest. */
|
|
70
80
|
export declare function assignmentCatchUp(store: Store, now: Date, access: AssignmentAccess, query?: {
|
package/dist/assignment-brief.js
CHANGED
|
@@ -7,6 +7,26 @@ import { publicChatText } from "./chat-display.js";
|
|
|
7
7
|
const sha = (value) => createHash("sha256").update(value).digest("hex");
|
|
8
8
|
const MAX_BYTES = 16_000;
|
|
9
9
|
const RECENT_MS = 7 * 24 * 60 * 60_000;
|
|
10
|
+
/** A sentence's end: not an abbreviation's full stop ("e.g.", "i.e.", "etc."), and followed by a new sentence. */
|
|
11
|
+
const SENTENCE_END = /(?<!\b(?:e\.g|i\.e|etc|vs|cf|approx|incl|Mr|Mrs|Ms|Dr|St|No))[.!?](?=\s+["'“(]?[\p{Lu}\p{N}])/u;
|
|
12
|
+
/** A reason's first sentence, or its first line. */
|
|
13
|
+
export function firstSentenceOf(text) {
|
|
14
|
+
const line = text.split("\n")[0].trim();
|
|
15
|
+
const end = SENTENCE_END.exec(line);
|
|
16
|
+
return end === null ? line : line.slice(0, end.index + 1);
|
|
17
|
+
}
|
|
18
|
+
/** A project's active decisions, newest first: each one's title and its reason in one line (first sentence or
|
|
19
|
+
* line). Memory absent on an older database reads as no decisions. */
|
|
20
|
+
export function activeDecisionsOf(store, repo, text = publicChatText) {
|
|
21
|
+
try {
|
|
22
|
+
return store.handle.prepare("SELECT id,claim,why FROM project_decision WHERE repo=? AND status='active' ORDER BY id DESC LIMIT 8").all(repo).map(row => ({
|
|
23
|
+
id: Number(row["id"]), claim: text(String(row["claim"]), 160), why: text(firstSentenceOf(String(row["why"])), 160)
|
|
24
|
+
}));
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
}
|
|
10
30
|
/** A current snapshot, never a receipt to acknowledge. Full assignment reads
|
|
11
31
|
* remain the place to inspect exact saved work and obtain its current digest. */
|
|
12
32
|
export function assignmentCatchUp(store, now, access, query = {}, evidenceRoot) {
|
|
@@ -26,7 +46,7 @@ export function assignmentCatchUp(store, now, access, query = {}, evidenceRoot)
|
|
|
26
46
|
return result;
|
|
27
47
|
const repos = query.repo === undefined ? access.repos : [query.repo];
|
|
28
48
|
const includeUnplaced = query.repo === undefined && access.principal === "operator" && access.includeUnplaced === true;
|
|
29
|
-
const scopedAccess = access.principal === "operator" ? { principal: "operator", repos, includeUnplaced } : { principal: "coordinator", repos: repos ?? [] };
|
|
49
|
+
const scopedAccess = access.principal === "operator" ? { principal: "operator", repos, includeUnplaced, ...(access.viewer == null ? {} : { viewer: access.viewer }) } : { principal: "coordinator", repos: repos ?? [] };
|
|
30
50
|
const text = (value, cap) => {
|
|
31
51
|
const rendered = publicChatText(value, cap);
|
|
32
52
|
if (rendered !== value)
|
|
@@ -86,37 +106,40 @@ export function assignmentCatchUp(store, now, access, query = {}, evidenceRoot)
|
|
|
86
106
|
result.omissions.decisions += Math.max(0, count - decisions.length);
|
|
87
107
|
}
|
|
88
108
|
// Project knowledge is admitted in SQL before any payload or title read.
|
|
89
|
-
|
|
109
|
+
// A project with settled decisions but no saved instructions yet still has knowledge to bring.
|
|
110
|
+
const known = store.handle.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name='project_decision'").get() === undefined ? "SELECT repo FROM project_knowledge"
|
|
111
|
+
: "SELECT repo FROM project_knowledge UNION SELECT repo FROM project_decision WHERE status='active'";
|
|
112
|
+
const projectRows = store.handle.prepare(`SELECT repo FROM (${known}) WHERE ? = 1 OR repo IN (SELECT value FROM json_each(?)) ORDER BY repo LIMIT 9`)
|
|
90
113
|
.all(repos === null ? 1 : 0, JSON.stringify(repos ?? []));
|
|
91
|
-
const projectCount = Number(store.handle.prepare(
|
|
114
|
+
const projectCount = Number(store.handle.prepare(`SELECT COUNT(*) AS n FROM (${known}) WHERE ? = 1 OR repo IN (SELECT value FROM json_each(?))`).get(repos === null ? 1 : 0, JSON.stringify(repos ?? []))?.["n"] ?? 0);
|
|
92
115
|
result.omissions.projects = Math.max(0, projectCount - 8);
|
|
93
116
|
for (const project of projectRows.slice(0, 8)) {
|
|
94
117
|
const repo = String(project["repo"]);
|
|
95
118
|
const knowledge = { status: "unavailable", revision: null, identity: null, sha256: null, instructions: "", sources: [], decisions: [] };
|
|
96
119
|
const row = store.handle.prepare('SELECT * FROM project_knowledge WHERE repo=?').get(repo);
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
if (typeof
|
|
107
|
-
throw Error("invalid stored
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
+
if (row === undefined)
|
|
121
|
+
knowledge.status = "none";
|
|
122
|
+
else
|
|
123
|
+
try {
|
|
124
|
+
const payload = String(row["payload"]), revision = Number(row["revision"]), identity = String(row["identity"]);
|
|
125
|
+
const history = store.handle.prepare('SELECT sha FROM knowledge_change WHERE repo=? AND identity=? AND revision=?').get(repo, identity, revision);
|
|
126
|
+
if (Buffer.byteLength(payload) > 160_000 || !Number.isSafeInteger(revision) || revision < 1 || sha(payload) !== row["sha"] || history?.["sha"] !== row["sha"])
|
|
127
|
+
throw Error("invalid stored context");
|
|
128
|
+
const saved = JSON.parse(payload);
|
|
129
|
+
if (typeof saved.instructions !== "string" || !Array.isArray(saved.references) || saved.references.length > 12)
|
|
130
|
+
throw Error("invalid stored context");
|
|
131
|
+
const sources = saved.references.map((one) => {
|
|
132
|
+
if (typeof one.id !== "string" || typeof one.title !== "string" || typeof one.content !== "string")
|
|
133
|
+
throw Error("invalid stored source");
|
|
134
|
+
const id = text(one.id, 80), path = typeof one.path === "string" ? text(one.path, 160) : null;
|
|
135
|
+
return { id, title: text(one.title, 120), kind: path === null ? "saved-note" : "repository-document", path,
|
|
136
|
+
sourceRevision: typeof one.sourceRevision === "string" && /^[a-f0-9]{40,64}$/.test(one.sourceRevision) ? one.sourceRevision : null,
|
|
137
|
+
sourceSha: typeof one.sourceSha === "string" && /^[a-f0-9]{40,64}$/.test(one.sourceSha) ? one.sourceSha : null };
|
|
138
|
+
});
|
|
139
|
+
Object.assign(knowledge, { status: "stored", revision, identity: /^[a-f0-9]{64}$/.test(identity) ? identity : null, sha256: String(row["sha"]), instructions: text(saved.instructions, 600), sources });
|
|
140
|
+
}
|
|
141
|
+
catch { /* Integrity failures are explicit; never replay an older version as current. */ }
|
|
142
|
+
knowledge.decisions = activeDecisionsOf(store, repo, text);
|
|
120
143
|
result.projects.push({ repo, knowledge });
|
|
121
144
|
if (!fits()) {
|
|
122
145
|
result.projects.pop();
|
package/dist/assignment-ui.d.ts
CHANGED
|
@@ -60,6 +60,7 @@ export type AssignmentCard = {
|
|
|
60
60
|
export declare function assignmentCardOf(assignment: AssignmentSnapshot, options?: AssignmentCardOptions): AssignmentCard;
|
|
61
61
|
export declare function assignmentSummaryHtml(assignment: AssignmentSnapshot, options?: AssignmentCardOptions & {
|
|
62
62
|
compact?: boolean;
|
|
63
|
+
sentence?: string | undefined;
|
|
63
64
|
}): string;
|
|
64
65
|
/** Shared assignment layout; actions remain comfortable on touch screens. */
|
|
65
66
|
export declare const ASSIGNMENT_CSS = ".assignment-summary{min-width:0;overflow-wrap:anywhere}.assignment-summary .assignment-state{font-weight:600;margin:0}.assignment-summary h2.assignment-state{font-size:1.125rem;color:var(--foreground)}.assignment-summary>.assignment-attempts,.assignment-summary>.assignment-notices{border:0;box-shadow:none;padding:0;margin:.25rem 0 0;background:transparent}.assignment-summary>.button-link{margin-top:.7rem;min-height:44px}.assignment-summary>.work-action{display:inline-flex;margin-inline-start:.6rem;min-height:44px;align-items:center}.assignment-detail{margin:.55rem 0}.assignment-attempts summary,.assignment-notices summary{min-height:44px;display:list-item;align-content:center;cursor:pointer}.assignment-notices summary{font-size:.8125rem;color:var(--foreground)}.assignment-notices p{margin:.25rem 0 .5rem}.assignment-attempts ol{padding-left:1.4rem}.assignment-attempts li{margin:.4rem 0}.assignment-attempts li a{display:inline-flex;align-items:center;min-height:44px;margin-right:.7rem}.assignment-attempts li p{margin:0 0 .4rem}";
|
package/dist/assignment-ui.js
CHANGED
|
@@ -72,10 +72,12 @@ function savedMaterialNotice(detail, assignment) {
|
|
|
72
72
|
}
|
|
73
73
|
export function assignmentCardOf(assignment, options = {}) {
|
|
74
74
|
const receipt = assignment.receipt;
|
|
75
|
-
|
|
75
|
+
// The checks live on the one result page (a failed build has one too), where Run checks runs in place; never Chat,
|
|
76
|
+
// which would only lead back to this card.
|
|
77
|
+
const checksPage = receipt === null ? null : `/review?result=${encodeURIComponent(receipt.taskId)}&run=${receipt.runId}&tab=checks`;
|
|
78
|
+
const links = receipt === null ? {} : { result: options.resultHref ?? chatResultHref(receipt.taskId, receipt.runId), checks: checksPage,
|
|
76
79
|
pullRequest: `/t/${encodeURIComponent(assignment.rootId)}#merge`,
|
|
77
|
-
|
|
78
|
-
...(receipt.completionKind === 'research-report' ? {} : { runChecks: `${chatResultHref(receipt.taskId, receipt.runId, 'checks')}#follow-ups` }) };
|
|
80
|
+
...(receipt.completionKind === 'research-report' ? {} : { runChecks: `${checksPage}#follow-ups` }) };
|
|
79
81
|
const presentation = assignmentPresentationOf(assignment, { ...options, links });
|
|
80
82
|
const { status, diagnostics, taskStatus } = presentation;
|
|
81
83
|
const href = assignment.primaryAction?.code === 'open-result' && options.resultHref !== undefined ? options.resultHref : assignmentActionHref(assignment);
|
|
@@ -100,7 +102,7 @@ export function assignmentSummaryHtml(assignment, options = {}) {
|
|
|
100
102
|
const tone = card.status.tone;
|
|
101
103
|
return `<section class="${options.compact ? 'assignment-summary' : 'card assignment-summary'}" aria-label="assignment progress" data-assignment="${escape(assignment.rootId)}" data-work-status="${card.token}" data-tone="${card.tone}" data-headline="${escape(card.status.headline)}" data-headline-tone="${tone}"${options.compact ? '' : ' data-task-status'}>` +
|
|
102
104
|
(options.compact ? `<span class="status-line" data-work-status="${card.token}" data-tone="${card.tone}"><i class="status-dot" aria-hidden="true"></i><span class="status-label">${card.label}</span></span>` : `<h2 class="assignment-state status-headline"><i aria-hidden="true"></i>${escape(card.label)}</h2>`) +
|
|
103
|
-
`<p class="meta assignment-detail status-sentence">${escape(card.status.sentence)}</p>` +
|
|
105
|
+
`<p class="meta assignment-detail status-sentence">${escape(options.sentence ?? card.status.sentence)}</p>` +
|
|
104
106
|
(card.action === null ? '' : `<a class="${options.compact ? 'work-action' : 'button-link'}" href="${escape(card.action.href)}"${card.action.openResult ? ' data-open-result' : ''}${options.compact ? '' : ' data-primary-action'}>${escape(card.action.label)}${options.compact ? ' →' : ''}</a>`) +
|
|
105
107
|
(options.compact ? '' : statusDetailsHtml(card.status)) +
|
|
106
108
|
statusWhyHtml(card.status, card.reasons, card.diagnostics) +
|
package/dist/assignment.d.ts
CHANGED
|
@@ -109,8 +109,12 @@ export type AssignmentSnapshot = {
|
|
|
109
109
|
wait: WaitKey;
|
|
110
110
|
build: number | null;
|
|
111
111
|
} | null;
|
|
112
|
-
/** The person's lead took it on (lead-voice.ts): "
|
|
112
|
+
/** The person's lead took it on (lead-voice.ts): "<name> is on it" until done, handed on, or two quiet hours. */
|
|
113
113
|
lead?: LeadClaim | null;
|
|
114
|
+
/** How many earlier versions of this task are still queued or running; absent when none. */
|
|
115
|
+
earlierActive?: number;
|
|
116
|
+
/** Of those, how many are running rather than only queued; present with earlierActive. */
|
|
117
|
+
earlierRunning?: number;
|
|
114
118
|
};
|
|
115
119
|
/** Status-first handoff for routine reads. Fetch get_assignment only when
|
|
116
120
|
* inspecting its exact evidence; do not repeat a full proof in every poll. */
|
|
@@ -180,6 +184,35 @@ export declare function checkAssignment(store: Store, taskId: string, receiptDig
|
|
|
180
184
|
/** The signed-in user can mark the same exact receipt handled without taking
|
|
181
185
|
* lead ownership. This records review, never check success or new authority. */
|
|
182
186
|
export declare function checkAssignmentAsOperator(store: Store, taskId: string, receiptDigest: string, who: VerifiedApprover, now: Date, root?: string): MutationResult;
|
|
187
|
+
/** The acceptance's own ledger act, as a separate Accept request records it. */
|
|
188
|
+
export declare const ACCEPT_ACTION = "task accept-proof";
|
|
189
|
+
/** Only a person's check stands between this result and done: its proof is short for that alone, and nobody accepted it. */
|
|
190
|
+
export declare function personCheckPending(receipt: AssignmentReceipt | null): boolean;
|
|
191
|
+
type Finished = {
|
|
192
|
+
ok: true;
|
|
193
|
+
} | {
|
|
194
|
+
ok: false;
|
|
195
|
+
reason: string;
|
|
196
|
+
message: string;
|
|
197
|
+
};
|
|
198
|
+
/** An acceptance this receipt owes before it finishes, by the result page's own rule: the person's own check
|
|
199
|
+
* (no reason needed), or an exception a report that doesn't match its changes needs a reason for. Null when
|
|
200
|
+
* Accept and finish is a plain completion: nothing is owed (no proof, a failed check, or a verified result),
|
|
201
|
+
* or it was already accepted. */
|
|
202
|
+
export declare function owedAcceptance(receipt: AssignmentReceipt | null): "person-check" | "exception" | null;
|
|
203
|
+
/** Accept and finish: the person's acceptance of the exact result they read and its completion, in one
|
|
204
|
+
* transaction. Whether an acceptance is owed is decided here, against the receipt as it stands: only a
|
|
205
|
+
* person's own check or a reasoned exception records one (with its ledger act); an exception without its
|
|
206
|
+
* reason is refused; anything else is the plain completion Mark complete always was. The completion names the receipt the acceptance produced, under the
|
|
207
|
+
* same digest a separate Accept then Mark complete leave. `receiptDigest` is the receipt as read, before
|
|
208
|
+
* accepting (`runId`, when named, must be its run); any other change refuses both, and a refused completion
|
|
209
|
+
* keeps no acceptance. `finish` completes the receipt (Mark complete by default; Complete and open a pull
|
|
210
|
+
* request passes its own). */
|
|
211
|
+
export declare function acceptAndCompleteAsOperator(store: Store, taskId: string, input: {
|
|
212
|
+
runId: number | null;
|
|
213
|
+
receiptDigest: string;
|
|
214
|
+
note: string | null;
|
|
215
|
+
}, who: VerifiedApprover, now: Date, root?: string, finish?: (digest: string) => Finished): Finished;
|
|
183
216
|
/** The ordinary worker reconciles at most 50 owned roots per pass. A lost
|
|
184
217
|
* wakeup is recovered after restart; immutable notification keys dedupe it. */
|
|
185
218
|
export declare function syncAssignmentHandoffs(store: Store, now: Date, repos: readonly string[], root?: string): void;
|
package/dist/assignment.js
CHANGED
|
@@ -14,10 +14,12 @@ import { verificationEvidence } from "./verification-evidence.js";
|
|
|
14
14
|
import { reproveApprover } from "./principal.js";
|
|
15
15
|
import { noteAssignmentStatus } from "./assignment-status.js";
|
|
16
16
|
import { historicalAssessmentReason } from "./assignment-presentation.js";
|
|
17
|
+
import { manualReviewOnly } from "./proof.js";
|
|
18
|
+
import { ACCEPT_NEEDS_REASON, cantAcceptYetOf } from "./result-acts.js";
|
|
17
19
|
import { runCheckLevel } from "./check-levels.js";
|
|
18
20
|
import { followUpChecksOf, withFollowUps } from "./result-follow-ups.js";
|
|
19
21
|
import { buildReviewOf, findingWords } from "./review-switch.js";
|
|
20
|
-
import { NEEDS, WAITS, processNeedOf } from "./needs-you.js";
|
|
22
|
+
import { NEEDS, WAITS, processNeedOf, resultHoldUpSentence } from "./needs-you.js";
|
|
21
23
|
import { assignmentStageOf } from "./task-status.js";
|
|
22
24
|
import { leadClaimOf } from "./lead-voice.js";
|
|
23
25
|
/** Status-first handoff for routine reads. Fetch get_assignment only when
|
|
@@ -140,9 +142,11 @@ export function assignmentOf(store, taskId, now, access, root) {
|
|
|
140
142
|
return decision === null ? [] : [{ taskId: version.id, decision }];
|
|
141
143
|
});
|
|
142
144
|
// A finished task label cannot hide a lease or an unfinished process record.
|
|
143
|
-
const
|
|
144
|
-
(version.
|
|
145
|
-
|
|
145
|
+
const busy = (version) => version.state === "running" || store.currentLiveLease(version.refId, now) !== null ||
|
|
146
|
+
store.runsFor(version.refId).some(run => run.outcome === null || store.stopQuiescenceProblem(run.id) !== null);
|
|
147
|
+
const earlierActive = family.versions.filter(version => version.id !== current.id && (version.state === "queued" || busy(version)));
|
|
148
|
+
// Of those, how many have work under way (the rest only wait in the queue).
|
|
149
|
+
const earlierRunning = earlierActive.filter(busy).length;
|
|
146
150
|
const unfinished = store.runsFor(current.refId).find(run => run.outcome === null) ?? null;
|
|
147
151
|
const attempts = family.versions.map(version => {
|
|
148
152
|
const summary = version.id === current.id ? work : taskWorkSummaryOf(store, version.id, now, access);
|
|
@@ -246,7 +250,7 @@ export function assignmentOf(store, taskId, now, access, root) {
|
|
|
246
250
|
detail = completionKind === "research-report" ? "The research report is ready for the lead to read."
|
|
247
251
|
: completionKind === "accepted-exception" ? "An operator accepted this result with its recorded limitations. The lead can inspect that decision; the recorded checks are unchanged."
|
|
248
252
|
: receipt.checks.detail;
|
|
249
|
-
primaryAction = { code: "open-result", label: completionKind === "research-report" ? "Read report" :
|
|
253
|
+
primaryAction = { code: "open-result", label: completionKind === "research-report" ? "Read report" : "Open result", target: { taskId: current.id, runId: result.id, decisionId: null }, access: "read", retry: "read-again" };
|
|
250
254
|
const checked = store.handle.prepare("SELECT actor,at FROM action_ledger WHERE task_id = ? AND run_id = ? AND action = ? AND outcome = ? AND source = 'work' ORDER BY id DESC")
|
|
251
255
|
.all(family.root.id, result.id, CHECK_ACTION, receipt.digest).find(row => /^(operator|coordinator|lead):.+/.test(String(row["actor"])));
|
|
252
256
|
// This ledger fact was authorized when written. Credential rotation,
|
|
@@ -269,7 +273,11 @@ export function assignmentOf(store, taskId, now, access, root) {
|
|
|
269
273
|
detail = result?.role === "scout"
|
|
270
274
|
? completionKind === null ? "The research report is missing or incomplete. Inspect the saved report before checking this handoff."
|
|
271
275
|
: "The saved report is awaiting resolution of its current scope or hold."
|
|
272
|
-
:
|
|
276
|
+
: resultHoldUpSentence({ hold: store.activeHolds(current.refId, now)[0]?.reason ?? null,
|
|
277
|
+
planChanged: result !== null && scope !== null && !!result.scopeDigest && scope.digest !== result.scopeDigest,
|
|
278
|
+
unapproved: scope === null || scope.termsProblem != null || !approvalOf(scope).approved, question: questions.length > 0,
|
|
279
|
+
running: store.currentLiveLease(current.refId, now) !== null, unfinished: unfinished !== null,
|
|
280
|
+
noCommit: result === null || completionKind === null });
|
|
273
281
|
// Built to an earlier plan: the result page can't resolve it (accepting would leave it stuck), building again can.
|
|
274
282
|
if (current.state === "done" && result !== null && scope !== null && scope.termsProblem == null && approvalOf(scope).approved && !!result.scopeDigest && scope.digest !== result.scopeDigest &&
|
|
275
283
|
store.activeHolds(current.refId, now).length === 0 && unfinished === null && store.currentLiveLease(current.refId, now) === null && store.finalResultReason(result.id) === null) {
|
|
@@ -341,7 +349,7 @@ export function assignmentOf(store, taskId, now, access, root) {
|
|
|
341
349
|
return { version: 1, rootId: family.root.id, activeTaskId: current.id, repo: current.repo, title: family.root.title,
|
|
342
350
|
state, detail, primaryAction, attention: [...new Set(attention)], attempts, owner, receipt, savedContext, completion, handoff,
|
|
343
351
|
publication: publication === null ? null : { state: publication.state, prUrl: publication.prUrl, remoteState: publication.remoteState },
|
|
344
|
-
review, deployment: { status: "not-recorded" }, ...(need === null ? {} : { need }), ...(lead === null ? {} : { lead }) };
|
|
352
|
+
review, deployment: { status: "not-recorded" }, ...(need === null ? {} : { need }), ...(lead === null ? {} : { lead }), ...(earlierActive.length === 0 ? {} : { earlierActive: earlierActive.length, earlierRunning }) };
|
|
345
353
|
}
|
|
346
354
|
function admittedOwner(store, taskId, owner, now, root) {
|
|
347
355
|
if (owner.kind !== "coordinator")
|
|
@@ -445,6 +453,77 @@ export function checkAssignmentAsOperator(store, taskId, receiptDigest, who, now
|
|
|
445
453
|
return acknowledgeCurrent(store, current, receiptDigest, `operator:${who.name}`, now, root, access);
|
|
446
454
|
}));
|
|
447
455
|
}
|
|
456
|
+
/** The acceptance's own ledger act, as a separate Accept request records it. */
|
|
457
|
+
export const ACCEPT_ACTION = "task accept-proof";
|
|
458
|
+
/** Only a person's check stands between this result and done: its proof is short for that alone, and nobody accepted it. */
|
|
459
|
+
export function personCheckPending(receipt) {
|
|
460
|
+
return receipt !== null && receipt.proofAcceptance === null && manualReviewOnly(receipt.proof === null ? null : { verdict: receipt.proof.verdict, reasons: receipt.proof.reasons, matrix: receipt.proof.matrix });
|
|
461
|
+
}
|
|
462
|
+
class Undone extends Error {
|
|
463
|
+
result;
|
|
464
|
+
constructor(result) {
|
|
465
|
+
super(result.message);
|
|
466
|
+
this.result = result;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
/** An acceptance this receipt owes before it finishes, by the result page's own rule: the person's own check
|
|
470
|
+
* (no reason needed), or an exception a report that doesn't match its changes needs a reason for. Null when
|
|
471
|
+
* Accept and finish is a plain completion: nothing is owed (no proof, a failed check, or a verified result),
|
|
472
|
+
* or it was already accepted. */
|
|
473
|
+
export function owedAcceptance(receipt) {
|
|
474
|
+
if (receipt === null || receipt.proofAcceptance !== null)
|
|
475
|
+
return null;
|
|
476
|
+
if (personCheckPending(receipt))
|
|
477
|
+
return "person-check";
|
|
478
|
+
return cantAcceptYetOf(receipt.proof?.verdict ?? null, receipt.proof?.reasons ?? [], false) === ACCEPT_NEEDS_REASON ? "exception" : null;
|
|
479
|
+
}
|
|
480
|
+
/** Accept and finish: the person's acceptance of the exact result they read and its completion, in one
|
|
481
|
+
* transaction. Whether an acceptance is owed is decided here, against the receipt as it stands: only a
|
|
482
|
+
* person's own check or a reasoned exception records one (with its ledger act); an exception without its
|
|
483
|
+
* reason is refused; anything else is the plain completion Mark complete always was. The completion names the receipt the acceptance produced, under the
|
|
484
|
+
* same digest a separate Accept then Mark complete leave. `receiptDigest` is the receipt as read, before
|
|
485
|
+
* accepting (`runId`, when named, must be its run); any other change refuses both, and a refused completion
|
|
486
|
+
* keeps no acceptance. `finish` completes the receipt (Mark complete by default; Complete and open a pull
|
|
487
|
+
* request passes its own). */
|
|
488
|
+
export function acceptAndCompleteAsOperator(store, taskId, input, who, now, root = evidenceRoot(homedir()), finish = digest => checkAssignmentAsOperator(store, taskId, digest, who, now, root)) {
|
|
489
|
+
try {
|
|
490
|
+
return withActor(currentActor() ?? { account: who.name, lead: false }, () => store.transact(() => {
|
|
491
|
+
if (!reproveApprover(store, who).ok)
|
|
492
|
+
return { ok: false, reason: "unauthenticated", message: "Sign in again before accepting this result." };
|
|
493
|
+
const access = { principal: "operator", repos: who.repos };
|
|
494
|
+
const before = assignmentOf(store, taskId, now, access, root);
|
|
495
|
+
if (before === null || !store.accountCanAccess(who.name, before.repo))
|
|
496
|
+
return { ok: false, reason: "not-found", message: "No assignment is available in your projects." };
|
|
497
|
+
const receipt = before.receipt;
|
|
498
|
+
if (receipt === null || (input.runId !== null && receipt.runId !== input.runId) || !/^[a-f0-9]{64}$/.test(input.receiptDigest) || receipt.digest !== input.receiptDigest) {
|
|
499
|
+
return { ok: false, reason: "stale", message: "This result changed. Open the current result before accepting it." };
|
|
500
|
+
}
|
|
501
|
+
// Already complete, or nothing owed: the plain completion of the receipt as read.
|
|
502
|
+
const owed = before.state === "ready-to-check" ? owedAcceptance(receipt) : null;
|
|
503
|
+
// An exception is accepted only with its reason: never a completion that skips it.
|
|
504
|
+
if (owed === "exception" && input.note === null)
|
|
505
|
+
return { ok: false, reason: "needs-reason", message: `${ACCEPT_NEEDS_REASON}.` };
|
|
506
|
+
let digest = receipt.digest;
|
|
507
|
+
if (owed !== null) {
|
|
508
|
+
store.acceptProof(receipt.runId, who.name, input.note, now);
|
|
509
|
+
store.recordAction({ at: now.toISOString(), actor: who.name, repo: before.repo, taskId, runId: null, action: ACCEPT_ACTION, outcome: "accepted", source: "request" });
|
|
510
|
+
const accepted = assignmentOf(store, taskId, now, access, root)?.receipt ?? null;
|
|
511
|
+
if (accepted === null || accepted.runId !== receipt.runId)
|
|
512
|
+
throw new Undone({ ok: false, reason: "stale", message: "This result changed. Open the current result before accepting it." });
|
|
513
|
+
digest = accepted.digest;
|
|
514
|
+
}
|
|
515
|
+
const done = finish(digest);
|
|
516
|
+
if (!done.ok)
|
|
517
|
+
throw new Undone(done);
|
|
518
|
+
return done;
|
|
519
|
+
}));
|
|
520
|
+
}
|
|
521
|
+
catch (error) {
|
|
522
|
+
if (error instanceof Undone)
|
|
523
|
+
return error.result;
|
|
524
|
+
throw error;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
448
527
|
function noteAssignmentHandoff(store, assignment, now) {
|
|
449
528
|
if (!assignment.owner?.active || assignment.handoff === null || assignment.handoff.acknowledged)
|
|
450
529
|
return;
|