toolroll 0.9.73 → 0.9.75
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 +63 -0
- package/THIRD_PARTY_NOTICES.md +40 -0
- package/dist/agent-turns.js +5 -0
- package/dist/agents-live.d.ts +31 -0
- package/dist/agents-live.js +43 -0
- package/dist/assignment.d.ts +3 -0
- package/dist/assignment.js +27 -8
- package/dist/browser/THIRD_PARTY_NOTICES.txt +40 -0
- package/dist/browser/workspace.css +2 -2
- package/dist/browser/workspace.js +31 -30
- package/dist/browser-workspace.d.ts +7 -1
- package/dist/builder.js +1 -1
- package/dist/chat-acceptance.d.ts +4 -1
- package/dist/chat-acceptance.js +13 -3
- package/dist/chat-continuity.js +10 -0
- package/dist/chat-quiet.js +3 -3
- package/dist/cli.js +1 -1
- package/dist/coding-handoff.js +4 -3
- package/dist/coding-ui.js +2 -2
- package/dist/coding-workspace.js +16 -4
- package/dist/contracts/console-api.d.ts +5 -1
- package/dist/contracts/console-api.js +10 -0
- package/dist/control-ui.d.ts +3 -2
- package/dist/control-ui.js +7 -2
- package/dist/demo.js +5 -3
- package/dist/deploy-gate.js +5 -2
- package/dist/desktop-update.d.ts +4 -1
- package/dist/desktop-update.js +43 -9
- package/dist/dispose.js +35 -2
- package/dist/exec.js +13 -16
- package/dist/first-run.d.ts +20 -1
- package/dist/first-run.js +71 -0
- package/dist/flow-card-task.js +3 -2
- package/dist/flow-unattended.d.ts +2 -0
- package/dist/flow-unattended.js +12 -1
- package/dist/integrations.d.ts +4 -3
- package/dist/integrations.js +5 -6
- package/dist/invoke.d.ts +13 -0
- package/dist/invoke.js +21 -12
- package/dist/knowledge-ui.d.ts +4 -1
- package/dist/knowledge-ui.js +10 -5
- package/dist/lead-contract.d.ts +1 -1
- package/dist/lead-contract.js +2 -2
- package/dist/lead-follow.js +3 -1
- package/dist/lead-progress.d.ts +4 -1
- package/dist/lead-progress.js +3 -1
- package/dist/lead.js +4 -0
- package/dist/live-bus.d.ts +2 -0
- package/dist/live-bus.js +1 -0
- package/dist/live-children-read.d.ts +52 -0
- package/dist/live-children-read.js +333 -0
- package/dist/live-children.d.ts +73 -0
- package/dist/live-children.js +318 -0
- package/dist/live-detail.d.ts +4 -3
- package/dist/live-detail.js +9 -5
- package/dist/live-input.js +7 -8
- package/dist/live-main-read.d.ts +19 -0
- package/dist/live-main-read.js +91 -0
- package/dist/live-open.d.ts +28 -0
- package/dist/live-open.js +125 -0
- package/dist/live-records.d.ts +10 -1
- package/dist/live-records.js +16 -5
- package/dist/live.d.ts +54 -22
- package/dist/live.js +218 -69
- package/dist/openrouter-models.d.ts +5 -2
- package/dist/openrouter-models.js +7 -4
- package/dist/operate.js +5 -2
- package/dist/peek-cli.d.ts +10 -3
- package/dist/peek-cli.js +17 -8
- package/dist/planner.js +1 -1
- package/dist/project-learning.js +44 -6
- package/dist/project-tools.js +19 -1
- package/dist/provider-limits.d.ts +3 -1
- package/dist/provider-limits.js +5 -2
- package/dist/provider.js +6 -12
- package/dist/pull-request-ui.d.ts +1 -1
- package/dist/pull-request-ui.js +5 -1
- package/dist/push.d.ts +2 -0
- package/dist/push.js +24 -19
- package/dist/read-only-request.d.ts +4 -0
- package/dist/read-only-request.js +15 -0
- package/dist/release-adapters.js +4 -0
- package/dist/release.d.ts +3 -0
- package/dist/release.js +3 -0
- package/dist/repository-context-ui.js +2 -1
- package/dist/repository-context.js +6 -1
- package/dist/result-follow-ups.js +20 -1
- package/dist/result-review.js +5 -1
- package/dist/run-efficiency.d.ts +3 -3
- package/dist/run-efficiency.js +20 -8
- package/dist/scout.d.ts +3 -0
- package/dist/scout.js +5 -3
- package/dist/serve.js +196 -29
- package/dist/server/agents-live-filed.d.ts +17 -0
- package/dist/server/agents-live-filed.js +97 -0
- package/dist/server/agents-live-lead.d.ts +47 -0
- package/dist/server/agents-live-lead.js +116 -0
- package/dist/server/agents-live-room.d.ts +23 -0
- package/dist/server/agents-live-room.js +91 -0
- package/dist/server/chat.js +115 -53
- package/dist/server/chrome.js +2 -2
- package/dist/server/console.css +68 -40
- package/dist/server/http.d.ts +5 -0
- package/dist/server/last-checks.d.ts +17 -0
- package/dist/server/last-checks.js +22 -0
- package/dist/server/live.d.ts +4 -0
- package/dist/server/live.js +42 -2
- package/dist/server/pages.js +98 -4
- package/dist/server/people-tokens.js +7 -4
- package/dist/server/render-chat.d.ts +15 -1
- package/dist/server/render-chat.js +14 -7
- package/dist/server/render-pages.d.ts +10 -5
- package/dist/server/render-pages.js +83 -74
- package/dist/server/render-results.d.ts +11 -2
- package/dist/server/render-results.js +54 -12
- package/dist/server/render-settings.js +3 -3
- package/dist/server/render-tasks.js +13 -3
- package/dist/server/route-table.js +19 -2
- package/dist/server/runtime.d.ts +17 -2
- package/dist/server/settings.js +85 -32
- package/dist/server/task-views.js +1 -2
- package/dist/server/tasks.js +70 -43
- package/dist/storage-ui.d.ts +2 -0
- package/dist/storage-ui.js +1 -1
- package/dist/storage.js +2 -1
- package/dist/store.d.ts +34 -1
- package/dist/store.js +108 -11
- package/dist/stream-scope.d.ts +18 -0
- package/dist/stream-scope.js +28 -0
- package/dist/subagent-work.js +3 -0
- package/dist/subscription-chat.js +5 -3
- package/dist/task-status.d.ts +28 -1
- package/dist/task-status.js +93 -11
- package/dist/teardown-git.d.ts +27 -1
- package/dist/teardown-git.js +185 -39
- package/dist/telegram-progress.js +4 -4
- package/dist/telegram-status.js +8 -3
- package/dist/tool-secret-store.js +8 -0
- package/dist/toolroll-update-ui.d.ts +2 -0
- package/dist/toolroll-update-ui.js +4 -3
- package/dist/toolroll-update.js +10 -9
- package/dist/work-index.js +29 -12
- package/dist/workspace-ui.d.ts +5 -0
- package/dist/workspace-ui.js +38 -10
- package/package.json +1 -1
package/dist/project-learning.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/** Optional, project-local advice. Core review and approval never depend on it. */
|
|
2
|
+
import { inReadOnlyRequest } from "./read-only-request.js";
|
|
2
3
|
import { createHash } from 'node:crypto';
|
|
3
4
|
import { execFileSync } from 'node:child_process';
|
|
4
|
-
import { realpathSync, statSync } from 'node:fs';
|
|
5
|
-
import { resolve } from 'node:path';
|
|
5
|
+
import { closeSync, constants as fsConstants, existsSync, fstatSync, lstatSync, openSync, readSync, realpathSync, statSync } from 'node:fs';
|
|
6
|
+
import { join, resolve } from 'node:path';
|
|
6
7
|
import { readVerifiedArtifact, scanForSecrets } from './evidence.js';
|
|
7
8
|
import { normalizeStructuredJson } from "./structured-output.js";
|
|
8
9
|
import { parseReviewContext, reviewContextCustodyProblem } from "./review-context.js";
|
|
@@ -50,7 +51,8 @@ const git = (repo, args, seen) => {
|
|
|
50
51
|
const known = seen?.get(key);
|
|
51
52
|
if (known !== undefined)
|
|
52
53
|
return known;
|
|
53
|
-
|
|
54
|
+
// Bounded, and no hook or fsmonitor program: it also runs from the console's background pass.
|
|
55
|
+
const out = execFileSync('git', ['--no-optional-locks', '-c', 'core.fsmonitor=false', '-c', 'core.hooksPath=/dev/null', '-C', repo, ...args], { encoding: 'utf8', maxBuffer: 2 * 1024 * 1024, timeout: 10_000, stdio: ['ignore', 'pipe', 'pipe'] }).trimEnd();
|
|
54
56
|
seen?.set(key, out);
|
|
55
57
|
return out;
|
|
56
58
|
};
|
|
@@ -62,9 +64,42 @@ export const legacyIdentityOf = (repo, common, st) => learningSha(JSON.stringify
|
|
|
62
64
|
function identityFacts(repo, seen) {
|
|
63
65
|
if (canonicalProject(repo) !== repo)
|
|
64
66
|
throw new Error('Project identity changed.');
|
|
65
|
-
|
|
67
|
+
// A page visit starts no git (docs/plans/get-side-effects.md): it reads the same answer from the folder itself.
|
|
68
|
+
const common = realpathSync(resolve(repo, inReadOnlyRequest() ? commonDirOnDisk(repo) : git(repo, ['rev-parse', '--git-common-dir'], seen)));
|
|
66
69
|
return { common, st: statSync(common) };
|
|
67
70
|
}
|
|
71
|
+
/** The shared Git folder as git finds it for a checkout: `.git` itself, or the folder a `.git` file names, through its
|
|
72
|
+
* `commondir` when it is a linked worktree. Throws when the folder holds neither. */
|
|
73
|
+
function commonDirOnDisk(repo) {
|
|
74
|
+
const dotGit = join(repo, '.git');
|
|
75
|
+
if (lstatSync(dotGit).isDirectory())
|
|
76
|
+
return dotGit;
|
|
77
|
+
const named = /^gitdir: (.+)$/m.exec(smallFile(dotGit))?.[1]?.trim();
|
|
78
|
+
if (!named)
|
|
79
|
+
throw new Error('Project identity is unavailable.');
|
|
80
|
+
const gitDir = resolve(repo, named);
|
|
81
|
+
if (!lstatSync(gitDir).isDirectory())
|
|
82
|
+
throw new Error('Project identity is unavailable.');
|
|
83
|
+
const commondir = join(gitDir, 'commondir');
|
|
84
|
+
if (!existsSync(commondir))
|
|
85
|
+
return gitDir;
|
|
86
|
+
return resolve(gitDir, smallFile(commondir).trim());
|
|
87
|
+
}
|
|
88
|
+
/** A small plain file's text, read through one descriptor that follows no link and never waits (a FIFO, a device or a
|
|
89
|
+
* file swapped in between is refused, never read), at most 4 KiB. */
|
|
90
|
+
function smallFile(path) {
|
|
91
|
+
const fd = openSync(path, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW | fsConstants.O_NONBLOCK);
|
|
92
|
+
try {
|
|
93
|
+
const st = fstatSync(fd);
|
|
94
|
+
if (!st.isFile() || st.size > 4096)
|
|
95
|
+
throw new Error('Project identity is unavailable.');
|
|
96
|
+
const buffer = Buffer.alloc(4096);
|
|
97
|
+
return buffer.subarray(0, readSync(fd, buffer, 0, 4096, 0)).toString('utf8');
|
|
98
|
+
}
|
|
99
|
+
finally {
|
|
100
|
+
closeSync(fd);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
68
103
|
export function learningIdentity(repo, seen) {
|
|
69
104
|
const { common, st } = identityFacts(repo, seen);
|
|
70
105
|
return identityOf(repo, common, st);
|
|
@@ -114,7 +149,8 @@ export function reconcileIdentity(store, repo, seen, now = new Date()) {
|
|
|
114
149
|
if (!done)
|
|
115
150
|
reconciled.set(store, done = new Set());
|
|
116
151
|
const key = `${repo}\n${legacy}`;
|
|
117
|
-
|
|
152
|
+
// Carrying saved rows to the current identity is a write: never a page visit's (the background pass does it).
|
|
153
|
+
if (legacy !== current && !done.has(key) && !inReadOnlyRequest()) {
|
|
118
154
|
if (!store.handle.prepare('SELECT 1 FROM project_identity_carry WHERE repo=? AND from_identity=? AND to_identity=?').get(repo, legacy, current)
|
|
119
155
|
&& store.handle.prepare(STORED_IDENTITY).get(repo, legacy)) {
|
|
120
156
|
// Damaged knowledge history keeps its own error; retry on the next read.
|
|
@@ -413,7 +449,9 @@ function supported(store, root, lesson, seen) {
|
|
|
413
449
|
}
|
|
414
450
|
export function learningView(store, root, repo, actor, before = 0) {
|
|
415
451
|
admission(store, repo, actor);
|
|
416
|
-
|
|
452
|
+
// Recovering learning from reviewed runs writes: the console's background pass and each run's start do it.
|
|
453
|
+
if (!inReadOnlyRequest())
|
|
454
|
+
recoverLearning(store, root, repo);
|
|
417
455
|
const identity = learningIdentity(repo), accepted = acceptedIdentities(store, repo);
|
|
418
456
|
const p = store.handle.prepare('SELECT * FROM learning_policy WHERE repo=?').get(repo);
|
|
419
457
|
const revision = Number(p?.['revision'] ?? 0), enabled = accepted.has(String(p?.['identity'])) && p?.['enabled'] === 1;
|
package/dist/project-tools.js
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
* the agent. A tool connected by signing in gives a run only an access token
|
|
20
20
|
* of its own (oauth-run-token.ts).
|
|
21
21
|
*/
|
|
22
|
+
import { inReadOnlyRequest } from "./read-only-request.js";
|
|
22
23
|
import { toolRefusal } from "./policy.js";
|
|
23
24
|
import { createHash } from "node:crypto";
|
|
24
25
|
import { spawn } from "node:child_process";
|
|
@@ -319,6 +320,23 @@ function moveIn(backend, folder, repo, tool) {
|
|
|
319
320
|
export function readToolSecrets(repo, tool, state) {
|
|
320
321
|
const folder = secretFolder(state);
|
|
321
322
|
const backend = secretBackend();
|
|
323
|
+
// A page visit moves nothing (docs/plans/get-side-effects.md): it reads the store, else the file not yet moved in.
|
|
324
|
+
if (inReadOnlyRequest()) {
|
|
325
|
+
const stored = readStored(backend, folder, repo, tool);
|
|
326
|
+
if (stored !== null)
|
|
327
|
+
return stored;
|
|
328
|
+
for (const file of [legacyFile(folder, repo, tool), backend === null ? null : toolFile(folder, repo, tool, ".json")]) {
|
|
329
|
+
if (file === null || !existsSync(file))
|
|
330
|
+
continue;
|
|
331
|
+
try {
|
|
332
|
+
const values = readSecretFile(file);
|
|
333
|
+
if (values !== null)
|
|
334
|
+
return values;
|
|
335
|
+
}
|
|
336
|
+
catch { /* kept as it is, and never used */ }
|
|
337
|
+
}
|
|
338
|
+
return {};
|
|
339
|
+
}
|
|
322
340
|
moveIn(backend, folder, repo, tool);
|
|
323
341
|
return readStored(backend, folder, repo, tool) ?? {};
|
|
324
342
|
}
|
|
@@ -1067,7 +1085,7 @@ export function secretsSetFor(repo, tool, state) {
|
|
|
1067
1085
|
if (names === null) {
|
|
1068
1086
|
const values = readToolSecrets(repo, tool.name, state);
|
|
1069
1087
|
names = Object.keys(values).filter(name => values[name] !== "");
|
|
1070
|
-
if (backend !== null)
|
|
1088
|
+
if (backend !== null && !inReadOnlyRequest())
|
|
1071
1089
|
writeNames(folder, repo, tool.name, names);
|
|
1072
1090
|
}
|
|
1073
1091
|
return tool.secrets.filter(one => names.includes(one.name)).map(one => one.name);
|
|
@@ -24,7 +24,9 @@ export type LimitReading = {
|
|
|
24
24
|
billing?: Billing;
|
|
25
25
|
};
|
|
26
26
|
export declare const record: (value: unknown) => Record<string, unknown> | null;
|
|
27
|
-
/** A Claude stream event's limits, or null when it isn't
|
|
27
|
+
/** A Claude stream event's limits, or null when it isn't the session's own `rate_limit_event` or says nothing usable.
|
|
28
|
+
* A subagent's (parent_tool_use_id set) is never read: the session reports its own windows each turn, and a
|
|
29
|
+
* subagent-tagged reading can't be told apart from one shaped by the subagent's own traffic (stream-scope.ts). */
|
|
28
30
|
export declare function claudeLimitsOf(event: unknown): LimitReading | null;
|
|
29
31
|
/** Codex's `account/rateLimits/read` answer as a reading, or null. */
|
|
30
32
|
export declare function codexLimitsOf(result: unknown): LimitReading | null;
|
package/dist/provider-limits.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { topLevelEvent } from "./stream-scope.js";
|
|
1
2
|
const WINDOW_NAME = /^[a-z][a-z0-9_]{0,39}$/;
|
|
2
3
|
const CLAUDE_MINUTES = { five_hour: 300, seven_day: 10_080, seven_day_opus: 10_080, seven_day_sonnet: 10_080 };
|
|
3
4
|
const finite = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
@@ -8,10 +9,12 @@ const epoch = (value) => {
|
|
|
8
9
|
return seconds === null || seconds < 1_000_000_000 || seconds > 10_000_000_000 ? null : new Date(seconds * 1000).toISOString();
|
|
9
10
|
};
|
|
10
11
|
export const record = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
|
|
11
|
-
/** A Claude stream event's limits, or null when it isn't
|
|
12
|
+
/** A Claude stream event's limits, or null when it isn't the session's own `rate_limit_event` or says nothing usable.
|
|
13
|
+
* A subagent's (parent_tool_use_id set) is never read: the session reports its own windows each turn, and a
|
|
14
|
+
* subagent-tagged reading can't be told apart from one shaped by the subagent's own traffic (stream-scope.ts). */
|
|
12
15
|
export function claudeLimitsOf(event) {
|
|
13
16
|
const body = record(event);
|
|
14
|
-
if (body === null || body["type"] !== "rate_limit_event")
|
|
17
|
+
if (body === null || body["type"] !== "rate_limit_event" || !topLevelEvent(body))
|
|
15
18
|
return null;
|
|
16
19
|
const info = record(body["rate_limit_info"]);
|
|
17
20
|
if (info === null)
|
package/dist/provider.js
CHANGED
|
@@ -29,6 +29,7 @@ import { REVIEW_OUTPUT_LIMITS } from "./structured-output.js";
|
|
|
29
29
|
import { ACP_AGENTS, isAcpProvider, withAcp } from "./acp-agents.js";
|
|
30
30
|
import { acpParse } from "./acp-client.js";
|
|
31
31
|
import { agentSpecSchema, MODEL_ID, modelIdSchema, PROVIDER_ID_VALUES, providerIdSchema } from "./contracts/provider.js";
|
|
32
|
+
import { primaryResultEvent, topLevelEvent } from "./stream-scope.js";
|
|
32
33
|
export const PROVIDER_IDS = PROVIDER_ID_VALUES;
|
|
33
34
|
export function isProviderId(value) {
|
|
34
35
|
return providerIdSchema.safeParse(value).success;
|
|
@@ -292,17 +293,6 @@ function claudeFinalMessage(result) {
|
|
|
292
293
|
}
|
|
293
294
|
return typeof result.result === "string" ? result.result : null;
|
|
294
295
|
}
|
|
295
|
-
/**
|
|
296
|
-
* The primary-result allowlist (arc 1 finding 10): only an absent origin or
|
|
297
|
-
* an explicit human origin can be the main query's accounting envelope.
|
|
298
|
-
* Every other kind — task-notification, channel, peer, coordinator, and
|
|
299
|
-
* anything the SDK grows later — fails closed as a non-primary result.
|
|
300
|
-
*/
|
|
301
|
-
function claudePrimaryOrigin(event) {
|
|
302
|
-
if (event.origin === undefined || event.origin === null)
|
|
303
|
-
return true;
|
|
304
|
-
return typeof event.origin === "object" && String(event.origin["kind"] ?? "") === "human";
|
|
305
|
-
}
|
|
306
296
|
function claudeEnvelopeOf(result, sessionFromInit, initObserved, initSessionConflict = false) {
|
|
307
297
|
const input = result?.usage?.input_tokens;
|
|
308
298
|
const output = result?.usage?.output_tokens;
|
|
@@ -378,6 +368,10 @@ function claudeParse(stdout) {
|
|
|
378
368
|
}
|
|
379
369
|
if (event === null || typeof event !== "object")
|
|
380
370
|
continue;
|
|
371
|
+
// A subagent's object (parent_tool_use_id set) is never the session's envelope, init, result or overflow witness
|
|
372
|
+
// (stream-scope.ts). The transport's own witness carries no parent and still counts.
|
|
373
|
+
if (!topLevelEvent(event))
|
|
374
|
+
continue;
|
|
381
375
|
const type = event["type"];
|
|
382
376
|
if (type === undefined) {
|
|
383
377
|
// Legacy buffered envelope: one JSON object, no event framing.
|
|
@@ -404,7 +398,7 @@ function claudeParse(stdout) {
|
|
|
404
398
|
// replacement for the primary result's usage/accounting envelope.
|
|
405
399
|
transportFailure = result;
|
|
406
400
|
}
|
|
407
|
-
if (primary === null &&
|
|
401
|
+
if (primary === null && primaryResultEvent(event))
|
|
408
402
|
primary = result;
|
|
409
403
|
}
|
|
410
404
|
}
|
package/dist/pull-request-ui.js
CHANGED
|
@@ -22,8 +22,12 @@ export function pullRequestSettingsHtml(view) {
|
|
|
22
22
|
const check = view.check;
|
|
23
23
|
if (check === null)
|
|
24
24
|
return html `${head}<section class="card pr-settings"><p><strong>Off</strong></p><p class="meta">An approver can turn on pull requests.</p></section>`;
|
|
25
|
+
// The checks ask git and GitHub, so they run when asked, never on opening the page (docs/plans/get-side-effects.md).
|
|
26
|
+
const checkButton = (label) => postForm("/settings/pull-requests/check", html `<button type="submit"${label === "Check again" ? html ` class="secondary"` : ""}>${label}</button>`, { hidden: { repo: view.repo } });
|
|
27
|
+
if (check === "unchecked")
|
|
28
|
+
return html `${head}<section class="card pr-settings"><p><strong>Off</strong></p><p class="meta">Check this project's GitHub setup to see what turning on would do.</p>${checkButton("Check setup")}</section>`;
|
|
25
29
|
if (!check.ok) {
|
|
26
|
-
return html `${head}<section class="card pr-settings"><p><strong>Can’t turn on yet</strong></p><p>${check.message}</p
|
|
30
|
+
return html `${head}<section class="card pr-settings"><p><strong>Can’t turn on yet</strong></p><p>${check.message}</p>${checkButton("Check again")}</section>`;
|
|
27
31
|
}
|
|
28
32
|
const plan = check.plan;
|
|
29
33
|
return html `${head}${post("on", html `<p><strong>Ready to turn on</strong></p><ul class="pr-terms"><li>Pull requests open on <span class="mono">${plan.githubRepo}</span> into <span class="mono">${plan.base}</span>${plan.account === null ? "" : `, as ${plan.account}`}.</li><li>Complete offers “Complete and open a pull request”, from the exact completed commit. A flow's Pull request zone can open one too.</li><li>Merging squashes and deletes the branch. A person merges with their password; a flow merges only when its zone says so, after a person approved the card.</li></ul>${password}<button type="submit">Turn on pull requests</button>`, { github: plan.githubRepo, base: plan.base })}`;
|
package/dist/push.d.ts
CHANGED
|
@@ -31,6 +31,8 @@ export type VapidKeys = {
|
|
|
31
31
|
fingerprint: string;
|
|
32
32
|
};
|
|
33
33
|
export declare function vapidFingerprint(publicKey: string): string;
|
|
34
|
+
/** Load the saved pair only (null when none is saved yet): what a page reads (docs/plans/get-side-effects.md). */
|
|
35
|
+
export declare function loadVapidKeys(dir: string): VapidKeys | null;
|
|
34
36
|
/**
|
|
35
37
|
* Mint-or-load, atomically across processes (finding 29): exclusive
|
|
36
38
|
* creation; the loser of a race re-reads the winner's file; and a loaded
|
package/dist/push.js
CHANGED
|
@@ -35,6 +35,10 @@ const fromB64url = (text) => Buffer.from(text, "base64url");
|
|
|
35
35
|
export function vapidFingerprint(publicKey) {
|
|
36
36
|
return createHash("sha256").update(publicKey, "utf8").digest("hex").slice(0, 16);
|
|
37
37
|
}
|
|
38
|
+
/** Load the saved pair only (null when none is saved yet): what a page reads (docs/plans/get-side-effects.md). */
|
|
39
|
+
export function loadVapidKeys(dir) {
|
|
40
|
+
return readVapidKeys(join(dir, VAPID_FILE));
|
|
41
|
+
}
|
|
38
42
|
/**
|
|
39
43
|
* Mint-or-load, atomically across processes (finding 29): exclusive
|
|
40
44
|
* creation; the loser of a race re-reads the winner's file; and a loaded
|
|
@@ -43,25 +47,7 @@ export function vapidFingerprint(publicKey) {
|
|
|
43
47
|
*/
|
|
44
48
|
export function loadOrCreateVapidKeys(dir) {
|
|
45
49
|
const path = join(dir, VAPID_FILE);
|
|
46
|
-
const read = () =>
|
|
47
|
-
let raw;
|
|
48
|
-
try {
|
|
49
|
-
raw = readFileSync(path, "utf8");
|
|
50
|
-
}
|
|
51
|
-
catch {
|
|
52
|
-
return null;
|
|
53
|
-
}
|
|
54
|
-
const parsed = JSON.parse(raw);
|
|
55
|
-
if (typeof parsed.publicKey !== "string" || typeof parsed.privateKey !== "string") {
|
|
56
|
-
throw new Error(`${VAPID_FILE} is not a key file`);
|
|
57
|
-
}
|
|
58
|
-
const check = createECDH("prime256v1");
|
|
59
|
-
check.setPrivateKey(fromB64url(parsed.privateKey));
|
|
60
|
-
if (b64url(check.getPublicKey()) !== parsed.publicKey) {
|
|
61
|
-
throw new Error(`${VAPID_FILE} holds a mismatched key pair — refusing to sign with it`);
|
|
62
|
-
}
|
|
63
|
-
return { publicKey: parsed.publicKey, privateKey: parsed.privateKey, fingerprint: vapidFingerprint(parsed.publicKey) };
|
|
64
|
-
};
|
|
50
|
+
const read = () => readVapidKeys(path);
|
|
65
51
|
const existing = read();
|
|
66
52
|
if (existing !== null)
|
|
67
53
|
return existing;
|
|
@@ -87,6 +73,25 @@ export function loadOrCreateVapidKeys(dir) {
|
|
|
87
73
|
return winner;
|
|
88
74
|
}
|
|
89
75
|
}
|
|
76
|
+
function readVapidKeys(path) {
|
|
77
|
+
let raw;
|
|
78
|
+
try {
|
|
79
|
+
raw = readFileSync(path, "utf8");
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
const parsed = JSON.parse(raw);
|
|
85
|
+
if (typeof parsed.publicKey !== "string" || typeof parsed.privateKey !== "string") {
|
|
86
|
+
throw new Error(`${VAPID_FILE} is not a key file`);
|
|
87
|
+
}
|
|
88
|
+
const check = createECDH("prime256v1");
|
|
89
|
+
check.setPrivateKey(fromB64url(parsed.privateKey));
|
|
90
|
+
if (b64url(check.getPublicKey()) !== parsed.publicKey) {
|
|
91
|
+
throw new Error(`${VAPID_FILE} holds a mismatched key pair — refusing to sign with it`);
|
|
92
|
+
}
|
|
93
|
+
return { publicKey: parsed.publicKey, privateKey: parsed.privateKey, fingerprint: vapidFingerprint(parsed.publicKey) };
|
|
94
|
+
}
|
|
90
95
|
// ---- RFC 8291 encryption ----------------------------------------------------
|
|
91
96
|
/**
|
|
92
97
|
* The deterministic core, exported for the Appendix A vector test: given
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A page visit only reads (docs/plans/get-side-effects.md). The server runs every GET and HEAD inside this scope, and
|
|
3
|
+
* the few store reads that used to tidy up as they went (closing an operating mode past its expiry) check it and
|
|
4
|
+
* leave the tidying to the background pass and the paths that act.
|
|
5
|
+
*/
|
|
6
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
7
|
+
const scope = new AsyncLocalStorage();
|
|
8
|
+
/** Run a GET or HEAD request's work. */
|
|
9
|
+
export function asReadOnlyRequest(work) {
|
|
10
|
+
return scope.run(true, work);
|
|
11
|
+
}
|
|
12
|
+
/** Whether the current work answers a GET or HEAD request. */
|
|
13
|
+
export function inReadOnlyRequest() {
|
|
14
|
+
return scope.getStore() === true;
|
|
15
|
+
}
|
package/dist/release-adapters.js
CHANGED
|
@@ -115,6 +115,9 @@ export function gateStateOf(answer, accounts = []) {
|
|
|
115
115
|
const failed = newest !== undefined && newest["finishedAt"] != null && newest["outcome"] !== "built" && newest["outcome"] !== "no-change" && Number(newest["id"]) !== resultRun
|
|
116
116
|
? `run ${String(newest["id"])} ended ${String(newest["outcome"] ?? "without an outcome")}` : null;
|
|
117
117
|
const worktree = runs.find(run => run["id"] === resultRun)?.["worktree"];
|
|
118
|
+
// Queued again or still building past the result shown (a regate): that result is the one before the rerun.
|
|
119
|
+
const state = object(answer["task"])["state"];
|
|
120
|
+
const pending = resultRun !== null && (state === "queued" || state === "running" || (newest !== undefined && newest["finishedAt"] == null && Number(newest["id"]) > resultRun));
|
|
118
121
|
return {
|
|
119
122
|
exists: true,
|
|
120
123
|
candidate: textOr(scope["candidate"]),
|
|
@@ -131,6 +134,7 @@ export function gateStateOf(answer, accounts = []) {
|
|
|
131
134
|
: checks["status"] === "passed" && checks["level"] !== "full" ? "not-full" : String(checks["status"] ?? "not-run"),
|
|
132
135
|
},
|
|
133
136
|
failed,
|
|
137
|
+
pending,
|
|
134
138
|
completion: textOr(completion["digest"]) === null ? null : { actor: String(completion["actor"]), at: String(completion["at"]), digest: String(completion["digest"]) },
|
|
135
139
|
};
|
|
136
140
|
}
|
package/dist/release.d.ts
CHANGED
|
@@ -90,6 +90,9 @@ export type GateState = {
|
|
|
90
90
|
} | null;
|
|
91
91
|
/** A finished build that left no result to complete (it failed before the check), in words. */
|
|
92
92
|
failed: string | null;
|
|
93
|
+
/** A newer attempt is queued or running (`task regate` after a failed check): the result shown is the one before it.
|
|
94
|
+
* Absent from a plane too old to say. */
|
|
95
|
+
pending?: boolean;
|
|
93
96
|
completion: {
|
|
94
97
|
actor: string;
|
|
95
98
|
at: string;
|
package/dist/release.js
CHANGED
|
@@ -535,6 +535,9 @@ export async function runRelease(options, adapters) {
|
|
|
535
535
|
await watch("dispatch");
|
|
536
536
|
const gate = await adapters.toolroll.gate(j.task.id);
|
|
537
537
|
await approved(gate);
|
|
538
|
+
// A rerun of the same approved commit is on its way: the failure before it is not this release's answer.
|
|
539
|
+
if (gate.pending === true && gate.result?.check !== "passed")
|
|
540
|
+
return null;
|
|
538
541
|
if (gate.failed !== null)
|
|
539
542
|
throw new Stop("check-failed", `The release build of ${j.task.id} failed: ${gate.failed}. Fix it and release the new commit with --new.`);
|
|
540
543
|
if (gate.result === null || gate.result.check === "running" || gate.result.check === "pending")
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { html, postForm } from "./html.js";
|
|
2
|
+
import { framedSection } from './knowledge-ui.js';
|
|
2
3
|
export function repositoryContextHtml(repo, query, result, csrf) {
|
|
3
4
|
const root = `/settings/knowledge?repo=${encodeURIComponent(repo)}`;
|
|
4
|
-
return html `<
|
|
5
|
+
return framedSection('Relevant code', html `<form class="field-row" method="get" action="/settings/knowledge"><input type="hidden" name="repo" value="${repo}"><label>Find code<input name="q" value="${query}" maxlength="1000" placeholder="A feature, file or symbol"></label><button type="submit">Search</button></form>${result === null ? '' : html `<p class="meta">${result.index.engine === 'typescript-ast' ? 'Code relationships' : 'Source search'}${result.mode === 'impact' ? ' · possible change impact' : ''}</p>${result.excerpts.length ? result.excerpts.map(one => html `<details><summary>${one.file}:${one.line}${one.symbol ? ` · ${one.symbol}` : ''}</summary><p class="meta">${one.reason}</p><pre>${one.text}</pre><a href="${root}&mode=impact&q=${encodeURIComponent(one.file)}">What this may affect</a></details>`) : html `<p>No matching source was found in this bounded search.</p>`}${result.relationships.length ? html `<details><summary>Related files</summary><ul>${result.relationships.map(one => html `<li>${one.from} → ${one.to} · ${one.kind}</li>`)}</ul></details>` : ''}<details><summary>Context details</summary><p>${result.index.status}${result.checkout ? ` · checkout ${result.checkout.head.slice(0, 8)}` : ''}</p>${result.warnings.map(one => html `<p>${one}</p>`)}</details>`}${csrf ? html `<details><summary>Refresh code index</summary>${postForm("/settings/knowledge/refresh", html `<p>Builds a local map of TypeScript and JavaScript imports. Uses no model.</p><button type="submit">Refresh index</button>`, { hidden: { repo } })}</details>` : ''}`, 'repository-context');
|
|
5
6
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/** Optional repository context. Callers admit project access; this module grants no authority.
|
|
2
2
|
* The index is disposable. Every read hashes the current bounded tracked source set;
|
|
3
3
|
* a missing/changed/unreadable index falls back to ordinary text search, never a task gate. */
|
|
4
|
+
import { inReadOnlyRequest } from './read-only-request.js';
|
|
5
|
+
import { checkoutGitSync } from './teardown-git.js';
|
|
4
6
|
import { createHash, randomUUID } from 'node:crypto';
|
|
5
7
|
import { execFileSync } from 'node:child_process';
|
|
6
8
|
import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
@@ -12,7 +14,10 @@ const CODE = /\.(?:[cm]?[jt]sx?)$/i;
|
|
|
12
14
|
const TEXT = /\.(?:[cm]?[jt]sx?|md|txt|css|html|json)$/i;
|
|
13
15
|
const MAX_NODES = 16_000, MAX_EDGES = 12_000;
|
|
14
16
|
const sha = (value) => createHash('sha256').update(value).digest('hex');
|
|
15
|
-
|
|
17
|
+
// Read-only and bounded: no index refresh, no hooks, no fsmonitor program.
|
|
18
|
+
// On a page read (Settings -> Knowledge's search), the hardened checkout git: no filter, hook, program or network.
|
|
19
|
+
const git = (repo, args) => inReadOnlyRequest() ? checkoutGitSync(repo, args, { timeoutMs: 10_000, maxBuffer: 2_000_000 }).trimEnd()
|
|
20
|
+
: execFileSync('git', ['--no-optional-locks', '-c', 'core.fsmonitor=false', '-c', 'core.hooksPath=/dev/null', '-C', repo, ...args], { encoding: 'utf8', maxBuffer: 2_000_000, timeout: 10_000, stdio: ['ignore', 'pipe', 'pipe'] }).trimEnd();
|
|
16
21
|
const inside = (root, path) => { const r = relative(root, path); return r !== '..' && !r.startsWith('..' + sep) && !isAbsolute(r); };
|
|
17
22
|
function capture(request) {
|
|
18
23
|
const repo = realpathSync(request.repo);
|
|
@@ -46,6 +46,25 @@ export function followUpChecksOf(store, runId, now = new Date(), root) {
|
|
|
46
46
|
catch { /* an unreadable receipt upgrades nothing */ }
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
|
+
// A pass stands only while its saved log still verifies and the command it ran is still the approved one, the same
|
|
50
|
+
// bar the build's own check receipt meets (verification-evidence.ts): otherwise it can't be read as a pass now.
|
|
51
|
+
const repo = root === undefined ? null : repoOfRun(store, runId);
|
|
52
|
+
const logVerifies = (artifactId) => {
|
|
53
|
+
if (root === undefined || artifactId === null)
|
|
54
|
+
return false;
|
|
55
|
+
const artifact = store.getArtifact(artifactId);
|
|
56
|
+
return artifact !== null && artifact.run === runId && artifact.kind === "structured-output" && artifact.capture === FOLLOW_UP_LOG && artifact.captureStatus !== "failed" && readVerifiedArtifact(root, artifact).ok;
|
|
57
|
+
};
|
|
58
|
+
const stillApproved = (level, digest) => {
|
|
59
|
+
if (repo === null)
|
|
60
|
+
return false;
|
|
61
|
+
try {
|
|
62
|
+
return checkCommandFor(store, repo, level).command?.digest === digest;
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
};
|
|
49
68
|
return rows.filter(row => row.action === CHECKS_REQUESTED && (row.outcome === "quick" || row.outcome === "full")).map(request => {
|
|
50
69
|
const [head = "", why = "person", digest = "", recordedBase = ""] = (request.detail ?? "").split(" · ");
|
|
51
70
|
const started = rows.find(row => row.action === CHECKS_STARTED && row.outcome === String(request.id)) ?? null;
|
|
@@ -59,7 +78,7 @@ export function followUpChecksOf(store, runId, now = new Date(), root) {
|
|
|
59
78
|
// With the evidence at hand, only a sealed receipt can say passed: for this result's own commit, and,
|
|
60
79
|
// for a batch, a batch that names this result.
|
|
61
80
|
if (status === "passed" && root !== undefined && (sealed === null || !sealed.ran || sealed.exitCode !== 0 || (sealed.head !== null && sealed.head !== head)
|
|
62
|
-
|| (base.why === "batch" && sealed.batch === null))) {
|
|
81
|
+
|| (base.why === "batch" && sealed.batch === null) || !logVerifies(sealed.log) || !stillApproved(base.level, digest))) {
|
|
63
82
|
return { ...base, state: "not-run", note: "The saved check result could not be verified." };
|
|
64
83
|
}
|
|
65
84
|
const exit = /exit (\d+)/.exec(finished.detail ?? "");
|
package/dist/result-review.js
CHANGED
|
@@ -28,7 +28,11 @@ export function evidenceResultStatusOf(result, publication, evidence) {
|
|
|
28
28
|
stored = {
|
|
29
29
|
token: "evidence-damaged",
|
|
30
30
|
label: "Result saved, but some saved material is unavailable",
|
|
31
|
-
|
|
31
|
+
// Never "passed" when the check itself can't be read now (task-status.ts approvedCheckOf).
|
|
32
|
+
detail: recorded.token === "check-unreadable" ? `Some saved material is missing, damaged, or could not be read. ${recorded.detail}`
|
|
33
|
+
: `Some saved material is missing, damaged, or could not be read. ${result.verdict === "verified" && result.checks !== undefined && ["ready-to-review", "pr-opened", "merge-observed", "pr-closed"].includes(recorded.token)
|
|
34
|
+
? "The machine's verdict at completion is unchanged: the approved check passed against it then."
|
|
35
|
+
: result.verdict === "verified" ? "Open the result to see its checks." : `The machine's verdict at completion is unchanged: ${recorded.label.toLowerCase()}.`}`,
|
|
32
36
|
tone: "problem",
|
|
33
37
|
action: { label: "See what is missing", kind: "open-result" },
|
|
34
38
|
};
|
package/dist/run-efficiency.d.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* kept 24 hours) are read once, bounded, through the descriptor-safe live reader, for step counts and observed step
|
|
9
9
|
* times. No command text is ever stored here: repeats are counted, never quoted.
|
|
10
10
|
*/
|
|
11
|
-
import type
|
|
11
|
+
import { type Run, type Store } from "./store.js";
|
|
12
12
|
import type { LiveActivityKind, LiveRecord } from "./live-records.js";
|
|
13
13
|
export declare const RUN_EFFICIENCY_VERSION = 3;
|
|
14
14
|
/** Budgets for one derivation: never more than the live file's own cap, and a bounded record count. */
|
|
@@ -62,10 +62,10 @@ export declare function usageOf(run: Pick<Run, "provider" | "model" | "tokensIn"
|
|
|
62
62
|
* others their times are partly made up (Codex reports a step when it completes, ACP agents may report late), so no
|
|
63
63
|
* duration is claimed for them. */
|
|
64
64
|
export declare function stepsOf(records: Iterable<LiveRecord>, truncated: string | null, measured?: boolean): RunEfficiency["steps"];
|
|
65
|
-
/** Derive a finished run's How it ran from its records. */
|
|
65
|
+
/** Derive a finished run's How it ran from its records, read under the run's own captured "Show commands" answer. */
|
|
66
66
|
export declare function deriveRunEfficiency(run: Run & {
|
|
67
67
|
usageJson?: string | null;
|
|
68
|
-
}, evidenceRoot: string, now: Date): RunEfficiency;
|
|
68
|
+
}, evidenceRoot: string, now: Date, commands?: boolean): RunEfficiency;
|
|
69
69
|
/**
|
|
70
70
|
* A finished run's How it ran: the saved one when it was derived by this version, else derived now and saved (once:
|
|
71
71
|
* a row whose steps were complete or have expired is final). An unfinished run is derived but never saved.
|
package/dist/run-efficiency.js
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How a run went (docs/plans/run-efficiency.md): derived from the run's own records, display only. It never changes a
|
|
3
|
+
* completion, an approval, a charge or a check verdict, and it claims nothing its sources don't hold — an unreported
|
|
4
|
+
* number is unknown (null, shown as a dash), never zero.
|
|
5
|
+
*
|
|
6
|
+
* Sources, and what each can say: the run row (wall times, tokens, the provider's raw usage with its cache counts, the
|
|
7
|
+
* agent's turns where reported) is kept with the run; the live records (`<evidence>/live/<run>.jsonl`, display-only,
|
|
8
|
+
* kept 24 hours) are read once, bounded, through the descriptor-safe live reader, for step counts and observed step
|
|
9
|
+
* times. No command text is ever stored here: repeats are counted, never quoted.
|
|
10
|
+
*/
|
|
11
|
+
import { runLiveCommands } from "./store.js";
|
|
1
12
|
import { closeSync, constants, fstatSync, lstatSync, openSync, readSync } from "node:fs";
|
|
2
13
|
import { join } from "node:path";
|
|
3
14
|
import { LIVE_RETAIN_MS, legacyLiveLogPath, liveLogPath, liveWindowRecords } from "./live.js";
|
|
@@ -95,7 +106,7 @@ const noSteps = (state, reason) => ({ state, reason, total: null, failed: null,
|
|
|
95
106
|
* directory), measured once, and never more than the budget read whatever the file does meanwhile; then parsed line by
|
|
96
107
|
* line. A file over the budget, in the older text format, with lines that don't read, or cut or capped while the agent
|
|
97
108
|
* ran, says its steps are incomplete. Null when there is no step file at all. */
|
|
98
|
-
function readRecords(evidenceRoot, runId) {
|
|
109
|
+
function readRecords(evidenceRoot, runId, commands) {
|
|
99
110
|
try {
|
|
100
111
|
const dir = lstatSync(join(evidenceRoot, "live"));
|
|
101
112
|
if (!dir.isDirectory() || dir.isSymbolicLink())
|
|
@@ -154,10 +165,10 @@ function readRecords(evidenceRoot, runId) {
|
|
|
154
165
|
whole.length = EFFICIENCY_RECORDS;
|
|
155
166
|
const keys = { inKey: false };
|
|
156
167
|
// One record per line: a whole line that doesn't read is a record lost.
|
|
157
|
-
const read = liveWindowRecords({ text: whole.length === 0 ? "" : `${whole.join("\n")}\n`, format: "records" }, keys);
|
|
168
|
+
const read = liveWindowRecords({ text: whole.length === 0 ? "" : `${whole.join("\n")}\n`, format: "records" }, commands, keys);
|
|
158
169
|
const lost = whole.length - read.length;
|
|
159
170
|
// A finished run's last line without its newline is read like any other; one that doesn't read is a record lost.
|
|
160
|
-
const tail = unfinished.trim() === "" ? [] : liveWindowRecords({ text: `${unfinished}\n`, format: "records" }, keys);
|
|
171
|
+
const tail = unfinished.trim() === "" ? [] : liveWindowRecords({ text: `${unfinished}\n`, format: "records" }, commands, keys);
|
|
161
172
|
const records = [...read, ...tail];
|
|
162
173
|
const lostAll = lost + (unfinished.trim() !== "" && tail.length === 0 ? 1 : 0);
|
|
163
174
|
if (records.length === 0 && text.trim() !== "")
|
|
@@ -170,10 +181,10 @@ function readRecords(evidenceRoot, runId) {
|
|
|
170
181
|
: null;
|
|
171
182
|
return { records, truncated };
|
|
172
183
|
}
|
|
173
|
-
/** Derive a finished run's How it ran from its records. */
|
|
174
|
-
export function deriveRunEfficiency(run, evidenceRoot, now) {
|
|
184
|
+
/** Derive a finished run's How it ran from its records, read under the run's own captured "Show commands" answer. */
|
|
185
|
+
export function deriveRunEfficiency(run, evidenceRoot, now, commands = false) {
|
|
175
186
|
const finished = run.finishedAt;
|
|
176
|
-
const read = readRecords(evidenceRoot, run.id);
|
|
187
|
+
const read = readRecords(evidenceRoot, run.id, commands);
|
|
177
188
|
const steps = read !== null && "unreadable" in read ? noSteps("truncated", read.unreadable)
|
|
178
189
|
: read !== null ? stepsOf(read.records, read.truncated, run.provider === "claude")
|
|
179
190
|
: finished !== null && now.getTime() - Date.parse(finished) > LIVE_RETAIN_MS ? noSteps("expired", "Step details are kept for a day after a run; this one's have expired.")
|
|
@@ -196,8 +207,9 @@ export function runEfficiencyOf(store, evidenceRoot, runId, now) {
|
|
|
196
207
|
return null;
|
|
197
208
|
const usageJson = store.handle.prepare("SELECT usage_json FROM run WHERE id = ?").get(runId)?.["usage_json"];
|
|
198
209
|
const full = { ...run, usageJson: typeof usageJson === "string" ? usageJson : null };
|
|
210
|
+
const commands = runLiveCommands(store, runId);
|
|
199
211
|
if (run.finishedAt === null)
|
|
200
|
-
return deriveRunEfficiency(full, evidenceRoot, now);
|
|
212
|
+
return deriveRunEfficiency(full, evidenceRoot, now, commands);
|
|
201
213
|
let saved;
|
|
202
214
|
try {
|
|
203
215
|
saved = store.handle.prepare("SELECT parser_version, payload_json FROM run_efficiency WHERE run = ?").get(runId);
|
|
@@ -211,7 +223,7 @@ export function runEfficiencyOf(store, evidenceRoot, runId, now) {
|
|
|
211
223
|
}
|
|
212
224
|
catch { /* derived again below */ }
|
|
213
225
|
}
|
|
214
|
-
const derived = deriveRunEfficiency(full, evidenceRoot, now);
|
|
226
|
+
const derived = deriveRunEfficiency(full, evidenceRoot, now, commands);
|
|
215
227
|
const payload = JSON.stringify(derived);
|
|
216
228
|
// A row too large for its column (it can't be: the counts are bounded) is shown, never saved.
|
|
217
229
|
// Saving is a convenience: a store that can't take the row (read-only, busy) still shows what was derived.
|
package/dist/scout.d.ts
CHANGED
|
@@ -131,6 +131,9 @@ export declare function readReportImage(folder: string, file: string): {
|
|
|
131
131
|
};
|
|
132
132
|
/** The shorten turn's ask: the fields over their limits, and the same report again within them. */
|
|
133
133
|
export declare function shortenReportBrief(over: readonly ReportProblem[], reportFile: string, structured: boolean): string;
|
|
134
|
+
/** Whether a stream event shows the session writing its plan-mode plan
|
|
135
|
+
* file (~/.claude/plans/…) or handing a plan to ExitPlanMode. */
|
|
136
|
+
export declare function touchesPlanFile(event: Record<string, unknown>): boolean;
|
|
134
137
|
/** A cited link without its secrets: credential-shaped query values and path parts become REDACTED, a fragment
|
|
135
138
|
* that carries one is dropped, and a link still showing one keeps only its origin. Unchanged when it has none. */
|
|
136
139
|
export declare function scrubUrl(text: string): {
|
package/dist/scout.js
CHANGED
|
@@ -37,6 +37,7 @@ import { grantOf, researchToolsOf } from "./mcp-connect.js";
|
|
|
37
37
|
import { startScoutProxy } from "./scout-net.js";
|
|
38
38
|
import * as browserCheck from "./scout-browser.js";
|
|
39
39
|
import { invokeAgent } from "./invoke.js";
|
|
40
|
+
import { topLevelEvent } from "./stream-scope.js";
|
|
40
41
|
const GIT = "git";
|
|
41
42
|
const AGENT_ENV_DENYLIST = [...TELEGRAM_TOKEN_ENVS];
|
|
42
43
|
const DEFAULT_SCOUT_TIMEOUT_MS = 20 * 60_000;
|
|
@@ -298,7 +299,7 @@ async function scoutWith(store, request, outputDir) {
|
|
|
298
299
|
}
|
|
299
300
|
// The live window (peek): the same transcript file the builder keeps,
|
|
300
301
|
// so `toolroll peek` and the run page can watch this session too.
|
|
301
|
-
const liveLog = openLiveLog(root, request.runId, liveLogContext(store, taskRef, worktree));
|
|
302
|
+
const liveLog = openLiveLog(root, request.runId, liveLogContext(store, request.runId, taskRef, worktree, clock()));
|
|
302
303
|
// Claude's report rides the terminal result event (run 2334's fix): plan
|
|
303
304
|
// mode only lets the session write its own plan file. Codex has no
|
|
304
305
|
// structured output and keeps the mailbox file.
|
|
@@ -706,8 +707,9 @@ export function shortenReportBrief(over, reportFile, structured) {
|
|
|
706
707
|
}
|
|
707
708
|
/** Whether a stream event shows the session writing its plan-mode plan
|
|
708
709
|
* file (~/.claude/plans/…) or handing a plan to ExitPlanMode. */
|
|
709
|
-
function touchesPlanFile(event) {
|
|
710
|
-
|
|
710
|
+
export function touchesPlanFile(event) {
|
|
711
|
+
// The session's own calls only: a subagent's (parent_tool_use_id set) is not the scout's plan (stream-scope.ts).
|
|
712
|
+
if (event["type"] !== "assistant" || !topLevelEvent(event))
|
|
711
713
|
return false;
|
|
712
714
|
const message = event["message"];
|
|
713
715
|
if (typeof message !== "object" || message === null)
|