toolroll 0.9.59 → 0.9.60
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 +11 -0
- package/README.md +8 -6
- package/dist/assignment-adapters.d.ts +1 -1
- package/dist/assignment-adapters.js +1 -1
- package/dist/assignment-delivery.d.ts +16 -0
- package/dist/assignment-delivery.js +54 -28
- package/dist/assignment-status.d.ts +13 -4
- package/dist/assignment-status.js +70 -39
- package/dist/assignment.d.ts +6 -2
- package/dist/assignment.js +13 -8
- package/dist/bin.js +11 -2
- package/dist/brief-fence.d.ts +9 -0
- package/dist/brief-fence.js +22 -0
- package/dist/browser/workspace.css +2 -2
- package/dist/browser/workspace.js +19 -19
- package/dist/builder.js +74 -25
- package/dist/chat-channel.js +1 -1
- package/dist/cli-fast.d.ts +8 -0
- package/dist/cli-fast.js +18 -0
- package/dist/cli-help.d.ts +5 -0
- package/dist/cli-help.js +91 -0
- package/dist/cli.d.ts +6 -5
- package/dist/cli.js +17 -96
- package/dist/design/tokens.css +234 -0
- package/dist/discord.js +2 -1
- package/dist/evidence.d.ts +22 -0
- package/dist/evidence.js +76 -17
- package/dist/flow-actions.js +2 -1
- package/dist/guides.js +14 -11
- package/dist/lead-cli.js +1 -1
- package/dist/lead-contract.js +1 -1
- package/dist/lead-doors.js +5 -2
- package/dist/lead-follow.d.ts +2 -0
- package/dist/lead-follow.js +138 -88
- package/dist/lead-tools.js +9 -2
- package/dist/mailbox.js +3 -2
- package/dist/mcp-core.js +1 -1
- package/dist/operate.js +17 -7
- package/dist/provider.d.ts +8 -0
- package/dist/provider.js +12 -1
- package/dist/serve.js +3 -2
- package/dist/server/chrome.d.ts +6 -3
- package/dist/server/chrome.js +8 -4
- package/dist/server/console.css +9 -64
- package/dist/server/render-chat.js +2 -2
- package/dist/server/render-tasks.d.ts +2 -0
- package/dist/server/render-tasks.js +11 -13
- package/dist/server/task-views.js +1 -0
- package/dist/slack-socket.d.ts +12 -0
- package/dist/slack-socket.js +29 -0
- package/dist/slack.d.ts +0 -7
- package/dist/slack.js +2 -24
- package/dist/steer-mailbox.d.ts +39 -0
- package/dist/steer-mailbox.js +207 -0
- package/dist/steer-status.d.ts +19 -0
- package/dist/steer-status.js +30 -0
- package/dist/store.d.ts +41 -2
- package/dist/store.js +95 -3
- package/dist/surface.js +2 -2
- package/dist/sweep-gate.d.ts +10 -0
- package/dist/sweep-gate.js +18 -0
- package/dist/team-updates.js +3 -3
- package/dist/toolroll-update.js +14 -2
- package/package.json +21 -21
package/dist/lead-follow.js
CHANGED
|
@@ -1,13 +1,28 @@
|
|
|
1
1
|
/** Durable crew updates for the built-in lead. Idle scans use no model.
|
|
2
|
-
*
|
|
2
|
+
* The lead is an ordinary consumer of the assignment inbox: its scan records
|
|
3
|
+
* status events under its follow grant (assignment-status.ts), and it reads
|
|
4
|
+
* and acknowledges them through the same saved pages every coordinator uses
|
|
5
|
+
* (assignment-delivery.ts). Replays reuse the saved lead request; they never
|
|
6
|
+
* dispatch a task. */
|
|
3
7
|
import { createHash } from 'node:crypto';
|
|
4
8
|
import { LEAD_FOLLOW_MESSAGE } from './store.js';
|
|
5
9
|
import { isVerifiedApprover, reproveApprover, verifyApproverStanding } from './principal.js';
|
|
6
10
|
import { assignmentOf } from './assignment.js';
|
|
11
|
+
import { ASSIGNMENT_STATUS_ACTION, ASSIGNMENT_STATUS_BASELINE, followActor, LEAD_WAKE_STATES, noteFollowStatus } from './assignment-status.js';
|
|
12
|
+
import { acknowledgeLeadFollowDelivery, leadFollowInbox } from './assignment-delivery.js';
|
|
13
|
+
import { publicChatText } from './chat-display.js';
|
|
7
14
|
import { runLeadTurn } from './lead.js';
|
|
8
15
|
import { updateAdmissionPaused } from './desktop-update-gate.js';
|
|
9
16
|
import { checkLeadCommitments } from './lead-commitments.js';
|
|
17
|
+
import { sweepIfMoved } from './sweep-gate.js';
|
|
10
18
|
const CONFIG = 'lead follow configured';
|
|
19
|
+
/** What follow status reads while nothing is waiting on the lead. */
|
|
20
|
+
const FOLLOW_READY = 'The lead will respond when a result or decision is ready, or a task is cancelled.';
|
|
21
|
+
const WAITING = 'Crew updates are waiting:';
|
|
22
|
+
const FAILED = `${WAITING} the last update check failed`;
|
|
23
|
+
/** The built-in lead's one inbox per grant. */
|
|
24
|
+
const CONSUMER = 'lead';
|
|
25
|
+
const PAGE = 20;
|
|
11
26
|
const hash = (value) => createHash('sha256').update(JSON.stringify(value)).digest('hex');
|
|
12
27
|
export function configureLeadFollow(store, who, session, thread, enabled, now) {
|
|
13
28
|
if (!isVerifiedApprover(who) || !reproveApprover(store, who).ok || session.approver !== who.name || thread.approver !== who.name ||
|
|
@@ -46,7 +61,7 @@ export function leadFollowStatus(store, actor) {
|
|
|
46
61
|
if (!grant?.enabled || !authorized(store, grant, grant.repos))
|
|
47
62
|
return { enabled: false, detail: 'Automatic crew updates are off.' };
|
|
48
63
|
const row = store.handle.prepare("SELECT outcome FROM action_ledger WHERE actor=? AND action=? AND source='work' ORDER BY id DESC LIMIT 1").get(actor, `lead delivery status:${grant.id}`);
|
|
49
|
-
return { enabled: true, detail: row ? String(row['outcome']) :
|
|
64
|
+
return { enabled: true, detail: row ? String(row['outcome']) : FOLLOW_READY };
|
|
50
65
|
}
|
|
51
66
|
function authorized(store, grant, admitted) {
|
|
52
67
|
const latest = grantFor(store, grant.actor);
|
|
@@ -59,63 +74,81 @@ function authorized(store, grant, admitted) {
|
|
|
59
74
|
const binding = liveBinding(store, proved.who, grant.session, grant.thread);
|
|
60
75
|
return binding ? { who: proved.who, ...binding } : null;
|
|
61
76
|
}
|
|
62
|
-
/** Observe bounded pages; first sight of
|
|
63
|
-
*
|
|
77
|
+
/** Observe bounded pages; first sight of work that has not moved since follow
|
|
78
|
+
* was enabled is only a baseline, recorded so the next change is compared to it. Every later transition — cancellation too —
|
|
79
|
+
* becomes an immutable status event in the grant's own inbox. */
|
|
64
80
|
function observe(store, grant, now, root) {
|
|
65
81
|
store.transact(() => {
|
|
66
82
|
const cursor = `lead:${grant.id}:scan`, after = store.serviceCursor(cursor);
|
|
67
83
|
const roots = store.handle.prepare(`SELECT id,external_id FROM task_ref WHERE backend='built-in' AND revision_of IS NULL
|
|
68
84
|
AND repo IN (SELECT value FROM json_each(?)) AND id>? ORDER BY id LIMIT 50`).all(JSON.stringify(grant.repos), after);
|
|
85
|
+
const since = baselineOf(store, grant);
|
|
69
86
|
for (const row of roots) {
|
|
70
87
|
const a = assignmentOf(store, String(row['external_id']), now, { principal: 'operator', repos: grant.repos }, root);
|
|
71
88
|
if (!a || !a.repo)
|
|
72
89
|
continue;
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
continue;
|
|
79
|
-
store.recordAction({ at: now.toISOString(), actor: grant.actor, repo: a.repo, taskId: a.rootId, runId: a.receipt?.runId ?? null, action, outcome: digest, source: 'work' });
|
|
80
|
-
// Scope, question, result and completion writes do not necessarily
|
|
81
|
-
// update task.updated_at. A transition between enabling follow and
|
|
82
|
-
// the first scan must not be mistaken for a pre-existing baseline.
|
|
83
|
-
const scope = store.getScope(a.activeTaskId);
|
|
84
|
-
const question = observation.decision === null ? null : store.getDecision(observation.decision);
|
|
85
|
-
const changedAfterGrant = [store.getTask(a.activeTaskId)?.updatedAt, scope?.proposedAt, scope?.approvedAt,
|
|
86
|
-
question?.createdAt, a.completion?.at, ...a.attempts.map(attempt => attempt.runId === null ? null : store.getRun(attempt.runId)?.finishedAt)]
|
|
87
|
-
.some(at => typeof at === 'string' && at >= grant.at);
|
|
88
|
-
if ((old || changedAfterGrant) && ['ready-to-check', 'needs-decision', 'complete'].includes(a.state)) {
|
|
89
|
-
store.recordAction({ at: now.toISOString(), actor: grant.actor, repo: a.repo, taskId: a.rootId, runId: a.receipt?.runId ?? null,
|
|
90
|
-
action: `lead update:${grant.id}`, outcome: JSON.stringify(observation), source: 'work' });
|
|
91
|
-
}
|
|
90
|
+
const seen = store.handle.prepare("SELECT 1 FROM action_ledger WHERE task_id=? AND action IN (?,?) AND source='work' AND actor=? LIMIT 1")
|
|
91
|
+
.get(a.rootId, ASSIGNMENT_STATUS_ACTION, ASSIGNMENT_STATUS_BASELINE, followActor(grant.id)) !== undefined;
|
|
92
|
+
const undelivered = !seen && store.handle.prepare("SELECT 1 FROM action_ledger WHERE actor=? AND action=? AND task_id=? AND source='work' AND id>? LIMIT 1")
|
|
93
|
+
.get(grant.actor, `lead update:${grant.id}`, a.rootId, store.serviceCursor(`lead:${grant.id}:events`)) !== undefined;
|
|
94
|
+
noteFollowStatus(store, grant.id, a, now, seen || undelivered || changedSince(store, a, since) ? ASSIGNMENT_STATUS_ACTION : ASSIGNMENT_STATUS_BASELINE);
|
|
92
95
|
}
|
|
93
96
|
store.setServiceCursor(cursor, roots.length < 50 ? 0 : Number(roots.at(-1)['id']), now);
|
|
94
97
|
});
|
|
95
98
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
99
|
+
/** Scope, question, result and completion writes do not necessarily update
|
|
100
|
+
* task.updated_at. A transition between enabling follow and the first scan
|
|
101
|
+
* must not be mistaken for a pre-existing baseline. */
|
|
102
|
+
function changedSince(store, a, since) {
|
|
103
|
+
const scope = store.getScope(a.activeTaskId);
|
|
104
|
+
const decision = a.primaryAction?.target.decisionId ?? null;
|
|
105
|
+
const question = decision === null ? null : store.getDecision(decision);
|
|
106
|
+
return [store.getTask(a.activeTaskId)?.updatedAt, scope?.proposedAt, scope?.approvedAt,
|
|
107
|
+
question?.createdAt, a.completion?.at, ...a.attempts.map(attempt => attempt.runId === null ? null : store.getRun(attempt.runId)?.finishedAt)]
|
|
108
|
+
.some(at => typeof at === 'string' && at >= since);
|
|
109
|
+
}
|
|
110
|
+
/** When follow began counting news: the grant itself, or, for a grant that
|
|
111
|
+
* predates this inbox, the newest crew update its old private batches actually
|
|
112
|
+
* handed the lead — that is not news again. Updates recorded past that point
|
|
113
|
+
* were never received, so observe still treats their tasks as news. */
|
|
114
|
+
function baselineOf(store, grant) {
|
|
115
|
+
const received = store.serviceCursor(`lead:${grant.id}:events`);
|
|
116
|
+
if (received === 0)
|
|
117
|
+
return grant.at;
|
|
118
|
+
const row = store.handle.prepare("SELECT at FROM action_ledger WHERE id<=? AND actor=? AND action=? AND source='work' ORDER BY id DESC LIMIT 1")
|
|
119
|
+
.get(received, grant.actor, `lead update:${grant.id}`);
|
|
120
|
+
return row !== undefined && String(row['at']) > grant.at ? String(row['at']) : grant.at;
|
|
121
|
+
}
|
|
122
|
+
/** What wakes the lead from one page: each task's latest result, question, completion or cancellation. */
|
|
123
|
+
function wakeUpdates(events) {
|
|
124
|
+
const latest = new Map();
|
|
125
|
+
for (const event of events) {
|
|
126
|
+
if ('unavailable' in event || !LEAD_WAKE_STATES.includes(event.assignment.state))
|
|
127
|
+
continue;
|
|
128
|
+
const a = event.assignment;
|
|
129
|
+
latest.set(a.rootId, { id: event.id, task: a.rootId, repo: event.project, title: a.title.slice(0, 200), state: a.state, detail: a.detail.slice(0, 400),
|
|
130
|
+
result: a.result?.runId ?? null, checks: a.result?.checks.status ?? null, decision: a.primaryAction?.target.decisionId ?? null });
|
|
131
|
+
}
|
|
132
|
+
return [...latest.values()];
|
|
133
|
+
}
|
|
134
|
+
/** The lead's follow status line, written once per distinct status. */
|
|
135
|
+
const lastDeliveryStatus = (store, grant) => store.handle.prepare("SELECT outcome FROM action_ledger WHERE actor=? AND action=? ORDER BY id DESC LIMIT 1").get(grant.actor, `lead delivery status:${grant.id}`)?.['outcome'];
|
|
136
|
+
function noteDeliveryStatus(store, grant, status, now) {
|
|
137
|
+
if (lastDeliveryStatus(store, grant) !== status)
|
|
138
|
+
store.recordAction({ at: now.toISOString(), actor: grant.actor, repo: null, taskId: null, runId: null, action: `lead delivery status:${grant.id}`, outcome: status, source: 'work' });
|
|
139
|
+
}
|
|
140
|
+
const enabledGrants = (store) => store.handle.prepare("SELECT a.* FROM action_ledger a WHERE a.action=? AND a.source='work' AND a.id=(SELECT MAX(b.id) FROM action_ledger b WHERE b.actor=a.actor AND b.action=a.action AND b.source=a.source) ORDER BY a.id")
|
|
141
|
+
.all(CONFIG).map(readGrant).filter((one) => one !== null && one.enabled);
|
|
142
|
+
const failureStatus = (error) => `${FAILED} (${publicChatText(error instanceof Error ? error.message : String(error), 200)}). It retries in a few seconds.`;
|
|
143
|
+
/** With no page left unacknowledged, an earlier fault or refusal is no longer waiting. */
|
|
144
|
+
function settle(store, grant, now) {
|
|
145
|
+
if (String(lastDeliveryStatus(store, grant) ?? '').startsWith(WAITING))
|
|
146
|
+
noteDeliveryStatus(store, grant, FOLLOW_READY, now);
|
|
147
|
+
}
|
|
148
|
+
/** A follow pass that failed outright says so where each enabled conversation reads its follow status. */
|
|
149
|
+
export function recordLeadFollowFailure(store, error, now) {
|
|
150
|
+
for (const grant of enabledGrants(store))
|
|
151
|
+
noteDeliveryStatus(store, grant, failureStatus(error), now);
|
|
119
152
|
}
|
|
120
153
|
export async function runLeadFollowPass(input) {
|
|
121
154
|
const { store } = input, clock = input.clock ?? (() => new Date());
|
|
@@ -124,48 +157,65 @@ export async function runLeadFollowPass(input) {
|
|
|
124
157
|
store.sweepStaleLeadTurns(clock());
|
|
125
158
|
// The lead's own promises are kept whether or not automatic crew updates are on: no model, one line when met.
|
|
126
159
|
checkLeadCommitments(store, clock(), input.evidenceRoot);
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
if (!batch)
|
|
135
|
-
continue;
|
|
136
|
-
// A replay after process exit reads the saved turn; even a failed turn is
|
|
137
|
-
// terminal for this delivery. Human chat remains available for follow-up.
|
|
138
|
-
const saved = store.leadRequestReceipt(grant.session, batch.request);
|
|
139
|
-
let turn = saved ? store.getLeadTurn(saved.turn) : null;
|
|
140
|
-
if (!turn) {
|
|
141
|
-
const provider = input.provider();
|
|
142
|
-
if (!provider)
|
|
143
|
-
continue;
|
|
144
|
-
const result = await (input.runTurn ?? runLeadTurn)({ store, ...access, ...provider, evidenceRoot: input.evidenceRoot, clock,
|
|
145
|
-
requestId: batch.request, message: LEAD_FOLLOW_MESSAGE,
|
|
146
|
-
context: batch.context, revalidate: async () => !updateAdmissionPaused(store.raw()) && authorized(store, grant, input.repos()) !== null ? { ok: true } : { ok: false, reason: 'access-changed' },
|
|
147
|
-
...(input.fetcher ? { fetcher: input.fetcher } : {}), ...(input.subscriptionRunner ? { subscriptionRunner: input.subscriptionRunner } : {}) });
|
|
148
|
-
if (!result.ok && 'refused' in result) {
|
|
149
|
-
const status = `Crew updates are waiting: ${result.message}`;
|
|
150
|
-
const last = store.handle.prepare("SELECT outcome FROM action_ledger WHERE actor=? AND action=? ORDER BY id DESC LIMIT 1").get(grant.actor, `lead delivery status:${grant.id}`);
|
|
151
|
-
if (last?.['outcome'] !== status)
|
|
152
|
-
store.recordAction({ at: clock().toISOString(), actor: grant.actor, repo: null, taskId: null, runId: null, action: `lead delivery status:${grant.id}`, outcome: status, source: 'work' });
|
|
153
|
-
continue;
|
|
154
|
-
}
|
|
155
|
-
turn = store.getLeadTurn(result.turn);
|
|
160
|
+
for (const grant of enabledGrants(store)) {
|
|
161
|
+
// One conversation's fault never holds up another's updates, and it is said where follow status is read.
|
|
162
|
+
try {
|
|
163
|
+
await followGrant(input, grant, clock);
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
noteDeliveryStatus(store, grant, failureStatus(error), clock());
|
|
156
167
|
}
|
|
157
|
-
if (!turn || turn.state === 'queued' || turn.state === 'running')
|
|
158
|
-
continue;
|
|
159
|
-
store.transact(() => {
|
|
160
|
-
// Concurrent passes may finish the same receipt, but ACK once.
|
|
161
|
-
if (store.serviceCursor(`lead:${grant.id}:delivery`) >= batch.id)
|
|
162
|
-
return;
|
|
163
|
-
store.setServiceCursor(`lead:${grant.id}:delivery`, batch.id, clock());
|
|
164
|
-
store.setServiceCursor(`lead:${grant.id}:events`, batch.through, clock());
|
|
165
|
-
const status = turn.state === 'answered' ? 'Crew updates handled.' : 'The lead response stopped. Open chat to continue; crew work was not rerun.';
|
|
166
|
-
store.recordAction({ at: clock().toISOString(), actor: grant.actor, repo: null, taskId: null, runId: null, action: `lead delivery status:${grant.id}`, outcome: status, source: 'work' });
|
|
167
|
-
if (turn.state !== 'answered' && authorized(store, grant, input.repos()))
|
|
168
|
-
store.appendLeadMessage({ thread: grant.thread, turn: null, role: 'assistant', text: status }, clock());
|
|
169
|
-
});
|
|
170
168
|
}
|
|
171
169
|
}
|
|
170
|
+
async function followGrant(input, grant, clock) {
|
|
171
|
+
const { store } = input;
|
|
172
|
+
const access = authorized(store, grant, input.repos());
|
|
173
|
+
if (!access || updateAdmissionPaused(store.raw()))
|
|
174
|
+
return;
|
|
175
|
+
const now = clock();
|
|
176
|
+
sweepIfMoved(store, `lead:${grant.id}:scan`, now, () => observe(store, grant, now, input.evidenceRoot));
|
|
177
|
+
const page = leadFollowInbox(store, grant, { consumer: CONSUMER, limit: PAGE }, clock());
|
|
178
|
+
if (!page.ok)
|
|
179
|
+
return noteDeliveryStatus(store, grant, `Crew updates are waiting: ${page.message}`, clock());
|
|
180
|
+
const batch = page.batch;
|
|
181
|
+
if (batch.id === null)
|
|
182
|
+
return settle(store, grant, clock());
|
|
183
|
+
const batchId = batch.id;
|
|
184
|
+
const acknowledge = (status, turnStopped) => store.transact(() => {
|
|
185
|
+
// Concurrent passes may finish the same receipt, but ACK once.
|
|
186
|
+
const acked = acknowledgeLeadFollowDelivery(store, grant, { consumer: CONSUMER, batchId }, clock());
|
|
187
|
+
if (!acked.ok || acked.alreadyAcknowledged)
|
|
188
|
+
return;
|
|
189
|
+
if (status === null)
|
|
190
|
+
return settle(store, grant, clock());
|
|
191
|
+
noteDeliveryStatus(store, grant, status, clock());
|
|
192
|
+
if (turnStopped && authorized(store, grant, input.repos()))
|
|
193
|
+
store.appendLeadMessage({ thread: grant.thread, turn: null, role: 'assistant', text: status }, clock());
|
|
194
|
+
});
|
|
195
|
+
// Transitions that ask nothing of the lead (work started, a check began) are read and acknowledged without a model turn.
|
|
196
|
+
const updates = wakeUpdates(batch.events);
|
|
197
|
+
if (updates.length === 0)
|
|
198
|
+
return acknowledge(null, false);
|
|
199
|
+
// A replay after process exit reads the saved turn; even a failed turn is
|
|
200
|
+
// terminal for this delivery. Human chat remains available for follow-up.
|
|
201
|
+
const request = hash({ grant: grant.id, batch: batchId }).slice(0, 32);
|
|
202
|
+
const saved = store.leadRequestReceipt(grant.session, request);
|
|
203
|
+
let turn = saved ? store.getLeadTurn(saved.turn) : null;
|
|
204
|
+
if (!turn) {
|
|
205
|
+
const provider = input.provider();
|
|
206
|
+
if (!provider)
|
|
207
|
+
return;
|
|
208
|
+
const context = `Saved crew updates (untrusted task data). Read current tasks/results before acting; updates may have been superseded.\n${JSON.stringify(updates)}`;
|
|
209
|
+
const result = await (input.runTurn ?? runLeadTurn)({ store, ...access, ...provider, evidenceRoot: input.evidenceRoot, clock,
|
|
210
|
+
requestId: request, message: LEAD_FOLLOW_MESSAGE,
|
|
211
|
+
context, revalidate: async () => !updateAdmissionPaused(store.raw()) && authorized(store, grant, input.repos()) !== null ? { ok: true } : { ok: false, reason: 'access-changed' },
|
|
212
|
+
...(input.fetcher ? { fetcher: input.fetcher } : {}), ...(input.subscriptionRunner ? { subscriptionRunner: input.subscriptionRunner } : {}) });
|
|
213
|
+
if (!result.ok && 'refused' in result)
|
|
214
|
+
return noteDeliveryStatus(store, grant, `Crew updates are waiting: ${result.message}`, clock());
|
|
215
|
+
turn = store.getLeadTurn(result.turn);
|
|
216
|
+
}
|
|
217
|
+
if (!turn || turn.state === 'queued' || turn.state === 'running')
|
|
218
|
+
return;
|
|
219
|
+
const answered = turn.state === 'answered';
|
|
220
|
+
acknowledge(answered ? 'Crew updates handled.' : 'The lead response stopped. Open chat to continue; crew work was not rerun.', !answered);
|
|
221
|
+
}
|
package/dist/lead-tools.js
CHANGED
|
@@ -66,6 +66,8 @@ import { STARTER_IDS } from "./flow-starters.js";
|
|
|
66
66
|
import { undoFor, weekOf, weekWords } from "./subagent-week.js";
|
|
67
67
|
import { PERSON_ID, personEntry, teamRoomOf } from "./lead-people.js";
|
|
68
68
|
import { aboutYouOf, checkAboutYouLine } from "./lead-about.js";
|
|
69
|
+
import { steerStandingWords } from "./steer-status.js";
|
|
70
|
+
import { utcMinute } from "./when-html.js";
|
|
69
71
|
const REPO_ID = /^r[0-9]{1,3}$/;
|
|
70
72
|
const TASK_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
71
73
|
const ISO_STAMP = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{1,3})?Z$/;
|
|
@@ -1492,7 +1494,7 @@ const LEAD_TOOL_HANDLERS = {
|
|
|
1492
1494
|
},
|
|
1493
1495
|
},
|
|
1494
1496
|
get_task: {
|
|
1495
|
-
description: "Read currentExecution, state, dispatch, history, scope, dependencies, holds, queue, attempts and
|
|
1497
|
+
description: "Read currentExecution, state, dispatch, history, scope, dependencies, holds, queue, attempts, decisions and whether the agent has read your notes before acting.",
|
|
1496
1498
|
handle: (ctx, args) => {
|
|
1497
1499
|
const taskId = taskIdOf(args);
|
|
1498
1500
|
if (taskId === null)
|
|
@@ -1538,6 +1540,11 @@ const LEAD_TOOL_HANDLERS = {
|
|
|
1538
1540
|
attempts: runs.length,
|
|
1539
1541
|
lastAttempt: runs[0] === undefined ? null : { build: runs[0].id, outcome: runs[0].outcome ?? "unfinished", worker: runs[0].runner, tools: ctx.store.runTools(runs[0].id), secretsFenced: ctx.store.runFence(runs[0].id)?.method ?? null },
|
|
1540
1542
|
decisionsOpen,
|
|
1543
|
+
// Operator steering (v120), in the task page's words: read (and when), waiting, or for the next attempt.
|
|
1544
|
+
steering: (() => {
|
|
1545
|
+
const live = ctx.store.steeringLiveBuild(ref.id, ctx.now) !== null;
|
|
1546
|
+
return ctx.store.listSteerNotes(ref.id).slice(-5).map(one => ({ note: one.id, filedAt: one.createdAt, state: steerStandingWords(one, live, utcMinute) }));
|
|
1547
|
+
})(),
|
|
1541
1548
|
},
|
|
1542
1549
|
};
|
|
1543
1550
|
},
|
|
@@ -1691,7 +1698,7 @@ const LEAD_TOOL_HANDLERS = {
|
|
|
1691
1698
|
},
|
|
1692
1699
|
},
|
|
1693
1700
|
propose_steer: {
|
|
1694
|
-
description: "
|
|
1701
|
+
description: "Send guidance within existing scope: a running build's agent reads it when it next checks for notes, otherwise the next attempt does. It never stops work.",
|
|
1695
1702
|
handle: (ctx, args) => {
|
|
1696
1703
|
const taskId = taskIdOf(args);
|
|
1697
1704
|
const ref = taskId === null ? null : admittedRef(ctx, taskId);
|
package/dist/mailbox.js
CHANGED
|
@@ -15,8 +15,6 @@
|
|
|
15
15
|
* answering machines. The card keeps the message's ID so a Send email step
|
|
16
16
|
* replying to the sender stays in the same thread.
|
|
17
17
|
*/
|
|
18
|
-
import { ImapFlow } from "imapflow";
|
|
19
|
-
import { simpleParser } from "mailparser";
|
|
20
18
|
import { googleAccessToken, googleConnected } from "./google-mail.js";
|
|
21
19
|
import { readEmailSettings } from "./email-settings.js";
|
|
22
20
|
const BODY_CHARS = 4000;
|
|
@@ -72,6 +70,8 @@ const header = (headers, name) => {
|
|
|
72
70
|
};
|
|
73
71
|
/** One raw message as plain words, and whether a machine sent it. */
|
|
74
72
|
export async function readMail(uid, source) {
|
|
73
|
+
// The mail SDKs load when mail is read, not with every command that imports this module.
|
|
74
|
+
const { simpleParser } = await import("mailparser");
|
|
75
75
|
const parsed = await simpleParser(source, { skipImageLinks: true, skipTextToHtml: true, skipTextLinks: true });
|
|
76
76
|
const sender = parsed.from?.value[0];
|
|
77
77
|
const from = (sender?.address ?? "").toLowerCase();
|
|
@@ -103,6 +103,7 @@ function problemOf(error) {
|
|
|
103
103
|
}
|
|
104
104
|
/** The production reader: one read-only IMAP visit. */
|
|
105
105
|
export const readThroughImap = async (access, folder, after, limit) => {
|
|
106
|
+
const { ImapFlow } = await import("imapflow");
|
|
106
107
|
const client = new ImapFlow({
|
|
107
108
|
host: access.host, port: access.port, secure: access.secure, logger: false, disableAutoIdle: true,
|
|
108
109
|
auth: access.accessToken !== undefined ? { user: access.user, accessToken: access.accessToken } : { user: access.user, pass: access.password ?? "" },
|
package/dist/mcp-core.js
CHANGED
|
@@ -56,7 +56,7 @@ const CONTRACT_GUIDE = [
|
|
|
56
56
|
"recap, list_decisions, get_decision, and queue are the plane's own read queries (shared with the operator's mate). get_decision shows each option's consequence, never the builder's recommendation.",
|
|
57
57
|
"propose_next, propose_reserve, propose_hold, propose_unhold, propose_scope, propose_cancel, and propose_answer write a PROPOSAL row and nothing else: an approver confirms it on the console or the CLI, and a stale one refuses there. Proposals share your hourly filing rate and hold at most 20 pending per credential; they expire after seven days.",
|
|
58
58
|
"Approve, steer, answer, pick, mint, configure, merge: those verbs do not exist on this surface, by construction.",
|
|
59
|
-
"Claim one assignment, then
|
|
59
|
+
"Claim one assignment, then receive its changes with get_assignment_inbox and acknowledge each handled batch with acknowledge_assignment_delivery; inspect get_assignment when a current result or decision needs attention. list_assignment_updates is only the history of handoffs sent to people. claim_assignment records its lead; acknowledge_assignment records that lead's check of an exact ready receipt. Neither operation grants authority, accepts proof, answers decisions, approves work, publishes, or deploys.",
|
|
60
60
|
].join("\n");
|
|
61
61
|
/** An assignment tool: the adapter reads its call with the same schema `toolroll assignment` does. */
|
|
62
62
|
const assignmentHandler = (operation) => (ctx, args) => {
|
package/dist/operate.js
CHANGED
|
@@ -10,6 +10,8 @@ import { runBatchChecks } from "./batch-checks.js";
|
|
|
10
10
|
import { BATCH_HINT, BATCH_WINDOW_MS, projectBatchChecks, setProjectBatchChecks } from "./batch-policy.js";
|
|
11
11
|
import { buildReviewPass } from "./build-review.js";
|
|
12
12
|
import { buildReviewLines, buildReviewOf } from "./review-switch.js";
|
|
13
|
+
import { steerDestinationWords, steerStandingOf, steerStandingWords } from "./steer-status.js";
|
|
14
|
+
import { utcMinute } from "./when-html.js";
|
|
13
15
|
import { parseProtectedPaths } from "./approval-policy.js";
|
|
14
16
|
import { rulesSummary } from "./approval-rules-ui.js";
|
|
15
17
|
import { evidencePack, exportDay, ledgerExportChunks, standaloneEvidenceHtml } from "./evidence-pack.js";
|
|
@@ -289,7 +291,7 @@ export const OPERATE_HELP = `toolroll — operating the queue
|
|
|
289
291
|
toolroll task add-tests <id> file a small task to write tests for that change
|
|
290
292
|
toolroll task revise <id> --feedback "requested change"
|
|
291
293
|
toolroll assignment show <task> root, current work and exact handoff
|
|
292
|
-
toolroll assignment updates
|
|
294
|
+
toolroll assignment updates history of handoffs sent to people (--after <cursor>)
|
|
293
295
|
toolroll assignment brief catch up from the local database
|
|
294
296
|
toolroll assignment inbox --consumer <name> receive saved status changes
|
|
295
297
|
toolroll assignment ack --consumer <name> --batch <id> acknowledge delivery
|
|
@@ -306,9 +308,10 @@ export const OPERATE_HELP = `toolroll — operating the queue
|
|
|
306
308
|
is still required); --undo puts it
|
|
307
309
|
back in filing order
|
|
308
310
|
toolroll task steer <id> --note "..."
|
|
309
|
-
guidance for the
|
|
310
|
-
reads
|
|
311
|
-
|
|
311
|
+
guidance for the agent — a running
|
|
312
|
+
build reads it when it next checks
|
|
313
|
+
for notes, otherwise the next
|
|
314
|
+
attempt does
|
|
312
315
|
toolroll task assign <id> --runner <name> | --anyone
|
|
313
316
|
reserve it for one worker (it joins
|
|
314
317
|
the back of that worker's queue) or
|
|
@@ -9005,6 +9008,7 @@ function showTask(positional, context) {
|
|
|
9005
9008
|
const listed = workIndexTask(store, id, now, access, context.evidenceRoot);
|
|
9006
9009
|
const shared = listed === null ? null : { headline: listed.status.label, sentence: listed.status.detail };
|
|
9007
9010
|
const primaryAction = listed === null ? work?.primaryAction ?? null : listed.primaryAction;
|
|
9011
|
+
const steeringLive = store.steeringLiveBuild(ref.id, now) !== null;
|
|
9008
9012
|
const detail = {
|
|
9009
9013
|
task,
|
|
9010
9014
|
work: work === null || listed === null ? work : { ...work, status: { ...work.status, label: listed.status.label, detail: listed.status.detail, tone: listed.status.tone }, primaryAction },
|
|
@@ -9044,6 +9048,9 @@ function showTask(positional, context) {
|
|
|
9044
9048
|
stops: store.stopsForTask(ref.id),
|
|
9045
9049
|
// The pull request Complete opened (any version), with its CI state and merge commit — the console's view.
|
|
9046
9050
|
pullRequest: newestPullRequestOf(store, store.taskFamilyOf(id, null, true)?.versions.map(one => one.id) ?? [id]),
|
|
9051
|
+
// Operator steering (v120), in the task page's words: read (and when), waiting, or for the next attempt.
|
|
9052
|
+
steering: store.listSteerNotes(ref.id).map(one => ({ id: one.id, author: one.author, createdAt: one.createdAt, note: one.note,
|
|
9053
|
+
standing: steerStandingOf(one, steeringLive), words: steerStandingWords(one, steeringLive, utcMinute) })),
|
|
9047
9054
|
};
|
|
9048
9055
|
return succeed(write, json, "task show", detail, () => [
|
|
9049
9056
|
`${task.id} ${task.state}${ref.deliverable === "report" ? " (scout — delivers a report)" : ""}`,
|
|
@@ -9078,6 +9085,7 @@ function showTask(positional, context) {
|
|
|
9078
9085
|
? []
|
|
9079
9086
|
: [` position ${detail.position.position} of ${detail.position.total}${detail.position.column === null ? " in the shared queue" : ` in ${detail.position.column}'s queue`}`]),
|
|
9080
9087
|
...(detail.hold === null ? [] : [` held: ${detail.hold.reason}`]),
|
|
9088
|
+
...detail.steering.slice(-5).map(one => ` note #${one.id} to the agent: ${one.words}`),
|
|
9081
9089
|
...(detail.claim === null ? [] : [` claimed by ${detail.claim.runner} until ${detail.claim.expiresAt}`]),
|
|
9082
9090
|
...(detail.dispatch === null ? [] : [` dispatch: ${detail.dispatch.summary} — ${detail.dispatch.detail}`]),
|
|
9083
9091
|
...(detail.control.kind === "none"
|
|
@@ -9433,7 +9441,7 @@ async function steerTask(positional, flags, context) {
|
|
|
9433
9441
|
const id = positional[0];
|
|
9434
9442
|
const note = text(flags, "note");
|
|
9435
9443
|
if (id === undefined || note === undefined) {
|
|
9436
|
-
return fail(write, json, "task steer", "usage", "`toolroll task steer <id> --note \"...\" --as <you> --token <t>` — steering speaks with the operator's voice, so it takes your credential; the note
|
|
9444
|
+
return fail(write, json, "task steer", "usage", "`toolroll task steer <id> --note \"...\" --as <you> --token <t>` — steering speaks with the operator's voice, so it takes your credential; a running build's agent reads the note when it next checks for notes, otherwise the next attempt's brief quotes it, fenced, inside the approved scope", EXIT.usage);
|
|
9437
9445
|
}
|
|
9438
9446
|
// Ruling 11: authorship derives from a VERIFIED principal, never a flag.
|
|
9439
9447
|
// Missing credentials are usage (the invocation is incomplete); present
|
|
@@ -9455,8 +9463,10 @@ async function steerTask(positional, flags, context) {
|
|
|
9455
9463
|
: (filed.problem ?? "that note will not store");
|
|
9456
9464
|
return fail(write, json, "task steer", filed.reason, detail, EXIT.refused);
|
|
9457
9465
|
}
|
|
9458
|
-
|
|
9459
|
-
|
|
9466
|
+
const steered = store.lookupRef(id);
|
|
9467
|
+
const live = steered !== null && store.steeringLiveBuild(steered.id, context.now) !== null;
|
|
9468
|
+
return succeed(write, json, "task steer", { task: id, note: filed.id, standing: live ? "waiting" : "next-attempt" }, () => [
|
|
9469
|
+
`Noted. ${steerDestinationWords(live)} \`task show ${id}\` says when it has been read.`,
|
|
9460
9470
|
]);
|
|
9461
9471
|
}
|
|
9462
9472
|
/** The mirror of block: stop waiting. Removal cannot create a cycle. */
|
package/dist/provider.d.ts
CHANGED
|
@@ -64,6 +64,14 @@ export type Invocation = {
|
|
|
64
64
|
* Codex renders it as its own sandbox profile; Claude as deny rules for
|
|
65
65
|
* its file tools (its shell is fenced by the macOS sandbox around it). */
|
|
66
66
|
fence?: readonly string[];
|
|
67
|
+
/** A running build's steering folder (steer-mailbox.ts): the absolute path
|
|
68
|
+
* and the worktree-relative name its brief uses. Claude renders a
|
|
69
|
+
* PostToolUse hook that says when a note is waiting; every other provider
|
|
70
|
+
* relies on the brief's own rule to look. */
|
|
71
|
+
steerFolder?: {
|
|
72
|
+
dir: string;
|
|
73
|
+
name: string;
|
|
74
|
+
};
|
|
67
75
|
/** Claude's structured-output floor for a non-review phase: the scout's
|
|
68
76
|
* report rides the terminal result event instead of a file plan mode
|
|
69
77
|
* would refuse to write. Ignored by providers without `--json-schema`. */
|
package/dist/provider.js
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
* shell the model itself launches (Codex provider review, Q5).
|
|
20
20
|
*/
|
|
21
21
|
import { claudeFenceSettings, codexFenceArgv } from "./agent-fence.js";
|
|
22
|
+
import { claudeSteerHooks } from "./steer-mailbox.js";
|
|
22
23
|
import {} from "./exec.js";
|
|
23
24
|
import { runStreamJsonl, runClaudeStreamJsonl, runGeminiStreamJsonl } from "./exec.js";
|
|
24
25
|
import { scanForSecrets } from "./evidence.js";
|
|
@@ -198,10 +199,20 @@ const claudeArgv = (invocation) => [
|
|
|
198
199
|
...(invocation.allowedTools === undefined || invocation.allowedTools.length === 0 ? [] : ["--allowedTools", invocation.allowedTools.join(",")]),
|
|
199
200
|
...(invocation.toolArgv ?? []),
|
|
200
201
|
...(invocation.extraMcpServers === undefined ? [] : ["--mcp-config", JSON.stringify({ mcpServers: invocation.extraMcpServers })]),
|
|
201
|
-
...(invocation
|
|
202
|
+
...claudeSettingsArgv(invocation),
|
|
202
203
|
...(invocation.jsonSchema === undefined ? [] : ["--json-schema", JSON.stringify(invocation.jsonSchema)])
|
|
203
204
|
]),
|
|
204
205
|
];
|
|
206
|
+
/** One --settings for a build: the fence's deny rules and the steering doorbell, whichever the run has. */
|
|
207
|
+
function claudeSettingsArgv(invocation) {
|
|
208
|
+
const fenced = invocation.fence !== undefined && invocation.fence.length > 0;
|
|
209
|
+
if (!fenced && invocation.steerFolder === undefined)
|
|
210
|
+
return [];
|
|
211
|
+
return ["--settings", JSON.stringify({
|
|
212
|
+
...(fenced ? JSON.parse(claudeFenceSettings(invocation.fence)) : {}),
|
|
213
|
+
...(invocation.steerFolder === undefined ? {} : { hooks: claudeSteerHooks(invocation.steerFolder.dir, invocation.steerFolder.name) }),
|
|
214
|
+
})];
|
|
215
|
+
}
|
|
205
216
|
/**
|
|
206
217
|
* The turn's own words, preferring the schema-validated structured field
|
|
207
218
|
* (present only under `--json-schema`) over the plain result string — the
|
package/dist/serve.js
CHANGED
|
@@ -7,7 +7,7 @@ import { CodingWorkspace } from './coding-workspace.js';
|
|
|
7
7
|
import { CONSOLE_FORMS, readForm } from "./contracts/console-api.js";
|
|
8
8
|
import { createFlowRooms, flowFingerprint } from "./flow-live.js";
|
|
9
9
|
import {} from "./google-mail.js";
|
|
10
|
-
import { configureLeadFollow, runLeadFollowPass } from './lead-follow.js';
|
|
10
|
+
import { configureLeadFollow, recordLeadFollowFailure, runLeadFollowPass } from './lead-follow.js';
|
|
11
11
|
import { leadNameOf } from "./lead-identity.js";
|
|
12
12
|
import { createLiveBus, followWorkspace } from "./live-bus.js";
|
|
13
13
|
import { startMaintenance } from './maintenance.js';
|
|
@@ -1570,7 +1570,8 @@ export function createDecisionServer(options) {
|
|
|
1570
1570
|
run: () => runLeadFollowPass({ store, repos: managedRepos, evidenceRoot, clock,
|
|
1571
1571
|
provider: () => { const enabled = chatEnablement(); return enabled.ok ? { config: enabled.config, key: enabled.key } : null; },
|
|
1572
1572
|
fetcher: chatFetcher, ...(options.subscriptionChatRunner ? { subscriptionRunner: options.subscriptionChatRunner } : {}) }),
|
|
1573
|
-
|
|
1573
|
+
// Pending delivery is durable and is read again on the next pass; a failure is said where follow status is read.
|
|
1574
|
+
onError: error => recordLeadFollowFailure(store, error, clock()),
|
|
1574
1575
|
});
|
|
1575
1576
|
});
|
|
1576
1577
|
// Keep native process custody until shutdown has verified agent/tool exit.
|
package/dist/server/chrome.d.ts
CHANGED
|
@@ -42,9 +42,12 @@ export declare const DEMO_BANNER_SHORT = "Demo: sample projects. Nothing calls a
|
|
|
42
42
|
* inset into it. Semantic names only; every surface (server pages, the
|
|
43
43
|
* React workspace, the pre-script fallback) reads these.
|
|
44
44
|
*
|
|
45
|
-
* The
|
|
46
|
-
*
|
|
47
|
-
*
|
|
45
|
+
* The palette above, in light, dark and pinned themes, is Signal's generated
|
|
46
|
+
* tokens.css (src/design, built by youi from the design-language repo); the
|
|
47
|
+
* console's own rules follow in console.css, shipped beside this module.
|
|
48
|
+
* Their bytes are the served sheet's (WORKSPACE_STYLE hashes them). The React
|
|
49
|
+
* workspace's stylesheet loads after this one and never redefines a token, so
|
|
50
|
+
* a person's accent (accentHead) still wins over the generated default. */
|
|
48
51
|
export declare const STYLE: string;
|
|
49
52
|
/** Appearance: a three-way segmented switch, one tap per choice. */
|
|
50
53
|
export declare const THEME_CONTROLS_CSS = ".task-repo select{width:100%;min-height:2.75rem;font-size:1rem}.task-repo-add{margin:.35rem .1rem .5rem}.task-repo-add a{display:inline-flex;align-items:center;min-height:2.25rem}details.result-request-open.result-request-form>summary{border:0;background:transparent;padding:.5rem 0;min-height:2.75rem;font-weight:600;display:list-item;list-style:revert}details.result-request-open.result-request-form>summary::-webkit-details-marker{display:revert}form.js-autosave button[type=submit]{display:none}.provider-row{border-bottom:1px solid var(--so-line);padding:.35rem 0}.provider-row:first-of-type{border-top:1px solid var(--so-line)}.provider-head{display:flex;align-items:center;gap:.75rem;margin:.4rem 0 0}.provider-status{display:inline-flex;align-items:center;gap:.4rem;color:var(--so-muted);font-size:.875rem}.provider-status i{width:.5rem;height:.5rem;border-radius:50%;background:var(--so-muted)}.provider-status--ok i{background:var(--so-success)}.provider-status--warn i{background:var(--so-attention)}.provider-status--off i{background:transparent;border:1.5px solid var(--so-muted)}details.provider-manage>summary{cursor:pointer;color:var(--so-accent-text);font-size:.875rem;min-height:2.5rem;display:list-item;padding-block:.5rem}.card.props .row{display:grid;gap:.1rem;margin:0 0 .75rem}.card.props .row>.meta{display:block;font-size:.75rem}.card.props .row>.meta::first-letter{text-transform:uppercase}.card.props .row>.mono{font-family:var(--font-sans);font-size:.875rem}.card.props .row>.mono .seal{font-family:var(--font-mono);font-size:.8125rem}details.evidence-files{margin:1rem 0}details.evidence-files>summary{cursor:pointer;min-height:2.75rem;display:list-item;padding-block:.7rem;font-weight:600}details.evidence-files ul{list-style:none;margin:0;padding:0}details.evidence-files li{display:flex;justify-content:space-between;gap:1rem;padding:.5rem 0;border-bottom:1px solid var(--so-line)}.result-action .result-feedback-link{display:inline-flex;align-items:center;min-height:2.5rem;padding:.5rem 1rem;border:1px solid var(--so-input-line);border-radius:.5rem;background:var(--so-paper);color:var(--so-ink);font-weight:600;text-decoration:none}@media(hover:hover) and (pointer:fine){.result-action .result-feedback-link:hover{background:var(--so-raised)}}.so-sr-only{position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}.verdict{margin:.5rem 0 .75rem}.verdict-chips{display:flex;flex-wrap:wrap;gap:.4rem;list-style:none;padding:0;margin:0}.verdict-chip{display:inline-flex;align-items:center;gap:.3rem;min-height:1.75rem;padding:.2rem .65rem;border-radius:999px;font-size:.8125rem;font-weight:600;background:var(--so-neutral-soft);color:var(--so-neutral-ink)}.verdict-chip svg{width:.9rem;height:.9rem}.verdict-chip--success{background:var(--so-success-soft);color:var(--so-success)}.verdict-chip--danger{background:var(--so-danger-soft);color:var(--so-danger)}.verdict-chip--warning{background:var(--so-warning-soft);color:var(--so-warning)}.verdict-chip--info{background:var(--so-info-soft);color:var(--so-info)}.verdict-by{margin:.4rem 0 0}details.result-request-open{margin:.5rem 0}details.result-request-open>summary{display:inline-flex;align-items:center;min-height:2.5rem;padding:.5rem 1rem;border:1px solid var(--so-input-line);border-radius:.5rem;background:var(--so-paper);color:var(--so-ink);font-weight:600;cursor:pointer;list-style:none}details.result-request-open>summary::-webkit-details-marker{display:none}details.result-request-open[open]>summary{margin-bottom:.75rem}.settings-tiles{display:grid;gap:1.25rem;margin:0 0 2rem}.settings-tiles h2{margin:0 0 .5rem;font-size:.875rem;font-weight:600;color:var(--so-muted)}.settings-tiles section>div{display:grid;grid-template-columns:repeat(auto-fill,minmax(8.5rem,1fr));gap:.5rem}.settings-tiles a>span{display:grid;gap:.1rem;min-width:0}.settings-tiles .provider-status{font-weight:400;font-size:.8125rem}.settings-tiles a{display:flex;align-items:center;gap:.6rem;min-height:3rem;padding:.65rem .8rem;border:1px solid var(--so-line);border-radius:.625rem;background:var(--so-paper);color:var(--so-ink);text-decoration:none;font-weight:550;font-size:.875rem}@media(hover:hover) and (pointer:fine){.settings-tiles a:hover{border-color:var(--so-input-line);background:var(--so-raised)}}.settings-tiles a>svg{width:1.1rem;height:1.1rem;flex-shrink:0;color:var(--so-accent-text)}details.settings-more{margin:.25rem 0 1.25rem}details.settings-more>summary{cursor:pointer;min-height:2.75rem;display:list-item;padding-block:.7rem;font-weight:550}details.settings-more>summary .meta{font-weight:400;margin-left:.35rem}.settings-changed{margin-top:-.25rem}.appearance{margin:0 0 28px}.appearance h2{margin:0 0 10px}.theme-switch{display:inline-flex;flex-wrap:nowrap;max-width:100%;gap:4px;padding:4px;margin:0;border:1px solid var(--so-line);border-radius:10px;background:var(--so-raised)}.theme-switch .theme-choice,.so-native-region .theme-switch .theme-choice{flex:1 1 0;width:auto;white-space:nowrap;min-height:40px;padding:8px 16px;border:0;border-radius:7px;background:transparent;color:var(--so-muted);font:inherit;font-weight:550;box-shadow:none;cursor:pointer}@media(hover:hover) and (pointer:fine){.theme-switch .theme-choice:hover{color:var(--so-ink)}}.theme-switch .theme-choice[aria-pressed=\"true\"]{background:var(--so-paper);color:var(--so-ink);box-shadow:0 1px 2px rgb(0 0 0 / .1)}.appearance .meta{margin:8px 0 0}@media(max-width:600px){.theme-switch .theme-choice{min-height:44px}}.update-notes{white-space:pre-wrap;overflow-wrap:anywhere;font:inherit;max-height:18rem;overflow:auto}";
|
package/dist/server/chrome.js
CHANGED
|
@@ -127,10 +127,14 @@ export const DEMO_BANNER_SHORT = "Demo: sample projects. Nothing calls a model o
|
|
|
127
127
|
* inset into it. Semantic names only; every surface (server pages, the
|
|
128
128
|
* React workspace, the pre-script fallback) reads these.
|
|
129
129
|
*
|
|
130
|
-
* The
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
|
|
130
|
+
* The palette above, in light, dark and pinned themes, is Signal's generated
|
|
131
|
+
* tokens.css (src/design, built by youi from the design-language repo); the
|
|
132
|
+
* console's own rules follow in console.css, shipped beside this module.
|
|
133
|
+
* Their bytes are the served sheet's (WORKSPACE_STYLE hashes them). The React
|
|
134
|
+
* workspace's stylesheet loads after this one and never redefines a token, so
|
|
135
|
+
* a person's accent (accentHead) still wins over the generated default. */
|
|
136
|
+
export const STYLE = readFileSync(join(import.meta.dirname, "..", "design", "tokens.css"), "utf8") +
|
|
137
|
+
readFileSync(join(import.meta.dirname, "console.css"), "utf8");
|
|
134
138
|
/** Appearance: a three-way segmented switch, one tap per choice. */
|
|
135
139
|
export const THEME_CONTROLS_CSS = `.task-repo select{width:100%;min-height:2.75rem;font-size:1rem}.task-repo-add{margin:.35rem .1rem .5rem}.task-repo-add a{display:inline-flex;align-items:center;min-height:2.25rem}details.result-request-open.result-request-form>summary{border:0;background:transparent;padding:.5rem 0;min-height:2.75rem;font-weight:600;display:list-item;list-style:revert}details.result-request-open.result-request-form>summary::-webkit-details-marker{display:revert}form.js-autosave button[type=submit]{display:none}.provider-row{border-bottom:1px solid var(--so-line);padding:.35rem 0}.provider-row:first-of-type{border-top:1px solid var(--so-line)}.provider-head{display:flex;align-items:center;gap:.75rem;margin:.4rem 0 0}.provider-status{display:inline-flex;align-items:center;gap:.4rem;color:var(--so-muted);font-size:.875rem}.provider-status i{width:.5rem;height:.5rem;border-radius:50%;background:var(--so-muted)}.provider-status--ok i{background:var(--so-success)}.provider-status--warn i{background:var(--so-attention)}.provider-status--off i{background:transparent;border:1.5px solid var(--so-muted)}details.provider-manage>summary{cursor:pointer;color:var(--so-accent-text);font-size:.875rem;min-height:2.5rem;display:list-item;padding-block:.5rem}.card.props .row{display:grid;gap:.1rem;margin:0 0 .75rem}.card.props .row>.meta{display:block;font-size:.75rem}.card.props .row>.meta::first-letter{text-transform:uppercase}.card.props .row>.mono{font-family:var(--font-sans);font-size:.875rem}.card.props .row>.mono .seal{font-family:var(--font-mono);font-size:.8125rem}details.evidence-files{margin:1rem 0}details.evidence-files>summary{cursor:pointer;min-height:2.75rem;display:list-item;padding-block:.7rem;font-weight:600}details.evidence-files ul{list-style:none;margin:0;padding:0}details.evidence-files li{display:flex;justify-content:space-between;gap:1rem;padding:.5rem 0;border-bottom:1px solid var(--so-line)}.result-action .result-feedback-link{display:inline-flex;align-items:center;min-height:2.5rem;padding:.5rem 1rem;border:1px solid var(--so-input-line);border-radius:.5rem;background:var(--so-paper);color:var(--so-ink);font-weight:600;text-decoration:none}@media(hover:hover) and (pointer:fine){.result-action .result-feedback-link:hover{background:var(--so-raised)}}.so-sr-only{position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}.verdict{margin:.5rem 0 .75rem}.verdict-chips{display:flex;flex-wrap:wrap;gap:.4rem;list-style:none;padding:0;margin:0}.verdict-chip{display:inline-flex;align-items:center;gap:.3rem;min-height:1.75rem;padding:.2rem .65rem;border-radius:999px;font-size:.8125rem;font-weight:600;background:var(--so-neutral-soft);color:var(--so-neutral-ink)}.verdict-chip svg{width:.9rem;height:.9rem}.verdict-chip--success{background:var(--so-success-soft);color:var(--so-success)}.verdict-chip--danger{background:var(--so-danger-soft);color:var(--so-danger)}.verdict-chip--warning{background:var(--so-warning-soft);color:var(--so-warning)}.verdict-chip--info{background:var(--so-info-soft);color:var(--so-info)}.verdict-by{margin:.4rem 0 0}details.result-request-open{margin:.5rem 0}details.result-request-open>summary{display:inline-flex;align-items:center;min-height:2.5rem;padding:.5rem 1rem;border:1px solid var(--so-input-line);border-radius:.5rem;background:var(--so-paper);color:var(--so-ink);font-weight:600;cursor:pointer;list-style:none}details.result-request-open>summary::-webkit-details-marker{display:none}details.result-request-open[open]>summary{margin-bottom:.75rem}.settings-tiles{display:grid;gap:1.25rem;margin:0 0 2rem}.settings-tiles h2{margin:0 0 .5rem;font-size:.875rem;font-weight:600;color:var(--so-muted)}.settings-tiles section>div{display:grid;grid-template-columns:repeat(auto-fill,minmax(8.5rem,1fr));gap:.5rem}.settings-tiles a>span{display:grid;gap:.1rem;min-width:0}.settings-tiles .provider-status{font-weight:400;font-size:.8125rem}.settings-tiles a{display:flex;align-items:center;gap:.6rem;min-height:3rem;padding:.65rem .8rem;border:1px solid var(--so-line);border-radius:.625rem;background:var(--so-paper);color:var(--so-ink);text-decoration:none;font-weight:550;font-size:.875rem}@media(hover:hover) and (pointer:fine){.settings-tiles a:hover{border-color:var(--so-input-line);background:var(--so-raised)}}.settings-tiles a>svg{width:1.1rem;height:1.1rem;flex-shrink:0;color:var(--so-accent-text)}details.settings-more{margin:.25rem 0 1.25rem}details.settings-more>summary{cursor:pointer;min-height:2.75rem;display:list-item;padding-block:.7rem;font-weight:550}details.settings-more>summary .meta{font-weight:400;margin-left:.35rem}.settings-changed{margin-top:-.25rem}.appearance{margin:0 0 28px}.appearance h2{margin:0 0 10px}.theme-switch{display:inline-flex;flex-wrap:nowrap;max-width:100%;gap:4px;padding:4px;margin:0;border:1px solid var(--so-line);border-radius:10px;background:var(--so-raised)}.theme-switch .theme-choice,.so-native-region .theme-switch .theme-choice{flex:1 1 0;width:auto;white-space:nowrap;min-height:40px;padding:8px 16px;border:0;border-radius:7px;background:transparent;color:var(--so-muted);font:inherit;font-weight:550;box-shadow:none;cursor:pointer}@media(hover:hover) and (pointer:fine){.theme-switch .theme-choice:hover{color:var(--so-ink)}}.theme-switch .theme-choice[aria-pressed="true"]{background:var(--so-paper);color:var(--so-ink);box-shadow:0 1px 2px rgb(0 0 0 / .1)}.appearance .meta{margin:8px 0 0}@media(max-width:600px){.theme-switch .theme-choice{min-height:44px}}.update-notes{white-space:pre-wrap;overflow-wrap:anywhere;font:inherit;max-height:18rem;overflow:auto}`;
|
|
136
140
|
/** The page CSS this module writes itself (not the imported modules'), for the stylesheet contract tests. */
|