toolroll 0.9.41 → 0.9.42
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 +18 -0
- package/dist/cli.js +3 -1
- package/dist/coding-types.d.ts +2 -24
- package/dist/coding-workspace.js +12 -11
- package/dist/contracts/cli.d.ts +736 -0
- package/dist/contracts/cli.js +223 -0
- package/dist/contracts/coding-activity.d.ts +64 -0
- package/dist/contracts/coding-activity.js +58 -0
- package/dist/contracts/console-api.d.ts +483 -0
- package/dist/contracts/console-api.js +273 -0
- package/dist/contracts/diff-stat.d.ts +58 -0
- package/dist/contracts/diff-stat.js +59 -0
- package/dist/contracts/integration-metadata.d.ts +99 -0
- package/dist/contracts/integration-metadata.js +101 -0
- package/dist/contracts/model-catalog.d.ts +21 -0
- package/dist/contracts/model-catalog.js +14 -0
- package/dist/contracts/native-census.d.ts +42 -0
- package/dist/contracts/native-census.js +36 -0
- package/dist/contracts/observation-request.d.ts +37 -0
- package/dist/contracts/observation-request.js +25 -0
- package/dist/contracts/provider.d.ts +30 -0
- package/dist/contracts/provider.js +15 -0
- package/dist/contracts/recipes.d.ts +166 -0
- package/dist/contracts/recipes.js +52 -0
- package/dist/contracts/retention.d.ts +42 -0
- package/dist/contracts/retention.js +24 -0
- package/dist/contracts/storage.d.ts +23 -0
- package/dist/contracts/storage.js +10 -0
- package/dist/contracts/store-json.d.ts +315 -0
- package/dist/contracts/store-json.js +196 -0
- package/dist/contracts/trigger-payloads.d.ts +369 -0
- package/dist/contracts/trigger-payloads.js +144 -0
- package/dist/contracts/update-journal.d.ts +254 -0
- package/dist/contracts/update-journal.js +155 -0
- package/dist/contracts/verification-view.d.ts +93 -0
- package/dist/contracts/verification-view.js +43 -0
- package/dist/control-setup.d.ts +1 -1
- package/dist/desktop-update.d.ts +7 -40
- package/dist/desktop-update.js +36 -8
- package/dist/evidence.d.ts +3 -19
- package/dist/flow-triggers.js +61 -60
- package/dist/mcp-connect.js +37 -22
- package/dist/model-catalog.js +8 -6
- package/dist/observations.d.ts +2 -6
- package/dist/observations.js +13 -9
- package/dist/operate.js +2 -1
- package/dist/process-recovery-native.d.ts +2 -13
- package/dist/process-recovery-native.js +8 -5
- package/dist/project-tools.d.ts +6 -24
- package/dist/project-tools.js +22 -18
- package/dist/provider.d.ts +3 -8
- package/dist/provider.js +15 -11
- package/dist/recipes.d.ts +3 -28
- package/dist/recipes.js +79 -52
- package/dist/retention.d.ts +4 -5
- package/dist/retention.js +5 -4
- package/dist/serve.js +142 -28
- package/dist/storage.d.ts +2 -1
- package/dist/storage.js +12 -9
- package/dist/store.d.ts +3 -15
- package/dist/store.js +80 -158
- package/dist/surface.d.ts +6 -54
- package/dist/surface.js +6 -1
- package/dist/toolroll-update.d.ts +8 -99
- package/dist/toolroll-update.js +28 -18
- package/dist/verification-evidence.js +27 -12
- package/package.json +1 -1
package/dist/desktop-update.js
CHANGED
|
@@ -12,6 +12,7 @@ import { activeUpdateWork, freezeUpdateGate, installUpdateGate, removeUpdateGate
|
|
|
12
12
|
import { currentDesktopAccess } from "./desktop-access.js";
|
|
13
13
|
import { assertCodingUpdateStopped, backupCodingCatalog, codingCatalogExists, releaseStaleCodingOwner } from "./coding-update.js";
|
|
14
14
|
import { processMayBeAlive } from "./process-liveness.js";
|
|
15
|
+
import { readDesktopRecoveryView, readDesktopUpdateJournal, supervisorPidsOf, updateRequestIs } from "./contracts/update-journal.js";
|
|
15
16
|
/** Loaded on first use (as backup.ts and store.ts do), so modules that only import this one (the console,
|
|
16
17
|
* and tests that load it in a browser-like environment) never need `node:sqlite` itself. */
|
|
17
18
|
function sqlite() {
|
|
@@ -60,13 +61,40 @@ function privateFile(file) {
|
|
|
60
61
|
if (!stat.isFile() || stat.isSymbolicLink() || (process.platform !== "win32" && ((stat.mode & 0o077) !== 0 || stat.uid !== process.getuid?.())))
|
|
61
62
|
throw Error("The update record must be an owner-only regular file.");
|
|
62
63
|
}
|
|
64
|
+
/** The refusal the hand-written reader gave for the field a saved journal first gets wrong, so callers and people read
|
|
65
|
+
* the same words; fields it never checked read as an invalid record. */
|
|
66
|
+
function journalRefusal(issue) {
|
|
67
|
+
const [field, inner] = issue.path.split(/[.[]/);
|
|
68
|
+
switch (field) {
|
|
69
|
+
case "old":
|
|
70
|
+
if (inner === undefined || inner === "path")
|
|
71
|
+
return "The saved update record is invalid. Preserve it and the app backups; nothing was changed.";
|
|
72
|
+
return "The saved update paths or build identities are invalid. Nothing was changed.";
|
|
73
|
+
case "next":
|
|
74
|
+
case "databaseFile":
|
|
75
|
+
case "configHash":
|
|
76
|
+
case "wasRunning":
|
|
77
|
+
case "backupPath":
|
|
78
|
+
case "backupHash":
|
|
79
|
+
return "The saved update paths or build identities are invalid. Nothing was changed.";
|
|
80
|
+
case "replacementOccurred": return "The recorded replacement state is invalid. Nothing was changed.";
|
|
81
|
+
case "codingCatalogExpected": return "The recorded coding catalog presence is invalid. Nothing was changed.";
|
|
82
|
+
case "stoppedPids": return "The recorded service processes are invalid. Nothing was changed.";
|
|
83
|
+
case "codingBackupPath":
|
|
84
|
+
case "codingBackupHash": return "The retained coding backup paths or identity are invalid. Nothing was changed.";
|
|
85
|
+
default: return "The saved update record is invalid. Preserve it and the app backups; nothing was changed.";
|
|
86
|
+
}
|
|
87
|
+
}
|
|
63
88
|
export function readUpdateJournal(stateDir, retainedReceipt) {
|
|
64
89
|
const file = retainedReceipt ?? journalPath(stateDir);
|
|
65
90
|
if (!existsSync(file))
|
|
66
91
|
return null;
|
|
67
92
|
privateFile(file);
|
|
68
|
-
const
|
|
69
|
-
if (
|
|
93
|
+
const read = readDesktopUpdateJournal(JSON.parse(readFileSync(file, "utf8")));
|
|
94
|
+
if (!read.ok)
|
|
95
|
+
throw Error(`${journalRefusal(read.issues[0])} (${read.issues.map(issue => issue.line).join("; ")})`);
|
|
96
|
+
const j = read.value;
|
|
97
|
+
if (!/^[a-f0-9-]{36}$/.test(j.id) || j.stateDir !== resolve(stateDir) || !isAbsolute(j.old?.path ?? "") || j.workDir !== join(dirname(j.old.path), ".standing-orders-updates", j.id) || !/^[a-zA-Z0-9.-]+$/.test(j.label) || !["install", "restore"].includes(j.intended) || !["prepared", "draining", "backing-up", "stopping", "installing", "verifying", "rolling-back", "releasing", "complete", "restored", "cancelled", "needs-attention"].includes(j.phase))
|
|
70
98
|
throw Error("The saved update record is invalid. Preserve it and the app backups; nothing was changed.");
|
|
71
99
|
if (retainedReceipt && retainedReceipt !== join(j.workDir, "receipt.json"))
|
|
72
100
|
throw Error("The retained recovery receipt is at the wrong path.");
|
|
@@ -374,7 +402,7 @@ export function desktopUpdateStatus(stateDir) {
|
|
|
374
402
|
let recovery = null;
|
|
375
403
|
try {
|
|
376
404
|
privateFile(join(j.workDir, "recovery.json"));
|
|
377
|
-
recovery = JSON.parse(readFileSync(join(j.workDir, "recovery.json"), "utf8"));
|
|
405
|
+
recovery = readDesktopRecoveryView(JSON.parse(readFileSync(join(j.workDir, "recovery.json"), "utf8")));
|
|
378
406
|
}
|
|
379
407
|
catch { /* A legacy/manual update has no guardian. */ }
|
|
380
408
|
const wantsAutomatic = recovery?.id === j.id && ["armed", "running", "backoff"].includes(recovery.state ?? "") && !terminal(j.phase);
|
|
@@ -389,8 +417,7 @@ export function updateStopRequested(j) {
|
|
|
389
417
|
if (!existsSync(file))
|
|
390
418
|
return false;
|
|
391
419
|
privateFile(file);
|
|
392
|
-
|
|
393
|
-
if (request.id !== j.id || request.action !== "stop")
|
|
420
|
+
if (!updateRequestIs(JSON.parse(readFileSync(file, "utf8")), j.id, "stop"))
|
|
394
421
|
throw Error("The saved stop request is invalid. Automatic starts are refused.");
|
|
395
422
|
return true;
|
|
396
423
|
}
|
|
@@ -410,7 +437,9 @@ export function requestUpdateRestore(stateDir) {
|
|
|
410
437
|
function restoreRequested(j) {
|
|
411
438
|
try {
|
|
412
439
|
const request = JSON.parse(readFileSync(join(j.workDir, "request.json"), "utf8"));
|
|
413
|
-
|
|
440
|
+
if (request === null)
|
|
441
|
+
throw Error("The saved restore request is invalid.");
|
|
442
|
+
return updateRequestIs(request, j.id, "restore");
|
|
414
443
|
}
|
|
415
444
|
catch (error) {
|
|
416
445
|
if (error.code === "ENOENT")
|
|
@@ -543,8 +572,7 @@ function assertDesktopCodingStopped(j, db) {
|
|
|
543
572
|
/** The service's own processes, from its supervisor's status file: read before the stop, never after. */
|
|
544
573
|
function servicePids(stateDir) {
|
|
545
574
|
try {
|
|
546
|
-
|
|
547
|
-
return [status.supervisorPid, status.controllerPid].filter((pid) => Number.isSafeInteger(pid) && Number(pid) > 1);
|
|
575
|
+
return supervisorPidsOf(JSON.parse(readFileSync(join(stateDir, "controller-supervisor.json"), "utf8")));
|
|
548
576
|
}
|
|
549
577
|
catch {
|
|
550
578
|
return [];
|
package/dist/evidence.d.ts
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
import { type ParsedReport, type ReportImage } from "./scout-report.js";
|
|
17
17
|
import { type ParsedProof } from "./proof.js";
|
|
18
18
|
import { type HandoffArtifact } from "./contracts/handoff.js";
|
|
19
|
+
import type { DiffStat } from "./contracts/diff-stat.js";
|
|
19
20
|
export type { HandoffArtifact } from "./contracts/handoff.js";
|
|
20
21
|
import type { Artifact, Store } from "./store.js";
|
|
21
22
|
import type { ExecResult } from "./exec.js";
|
|
@@ -229,25 +230,8 @@ export declare function quarantineMailboxes(worktree: string, root: string | nul
|
|
|
229
230
|
export declare function captureParkEvidence(store: Store, git: (file: string, args: readonly string[], options?: {
|
|
230
231
|
cwd?: string;
|
|
231
232
|
}) => Promise<ExecResult>, worktree: string, baseRevision: string | null, root: string, runId: number, now: Date): Promise<number[]>;
|
|
232
|
-
/**
|
|
233
|
-
export type DiffStatFile
|
|
234
|
-
path: string;
|
|
235
|
-
additions: number | null;
|
|
236
|
-
deletions: number | null;
|
|
237
|
-
renamedFrom?: string;
|
|
238
|
-
};
|
|
239
|
-
export type DiffStat = {
|
|
240
|
-
schema: 1;
|
|
241
|
-
base: string;
|
|
242
|
-
head: string;
|
|
243
|
-
fileCount: number;
|
|
244
|
-
additions: number;
|
|
245
|
-
deletions: number;
|
|
246
|
-
binaryCount: number;
|
|
247
|
-
files: DiffStatFile[];
|
|
248
|
-
/** True when the file list was cut to fit the cap — counts stay complete. */
|
|
249
|
-
filesTruncated: boolean;
|
|
250
|
-
};
|
|
233
|
+
/** The terminal diff-stat and its file rows: src/contracts/diff-stat.ts. */
|
|
234
|
+
export type { DiffStat, DiffStatFile } from "./contracts/diff-stat.js";
|
|
251
235
|
/**
|
|
252
236
|
* Parse `git diff --numstat -z` output. NUL-delimited, so filenames are never
|
|
253
237
|
* guessed out of patch headers (Codex roadmap review, scope cut A.3): plain
|
package/dist/flow-triggers.js
CHANGED
|
@@ -42,6 +42,7 @@ import { mailboxAccess, mailCursorOf, mailCursorText, readThroughImap } from "./
|
|
|
42
42
|
import { readFlowSecrets } from "./flow-secrets.js";
|
|
43
43
|
import { tmpdir } from "node:os";
|
|
44
44
|
import { envValue } from "./names.js";
|
|
45
|
+
import { linearLabelNames, readFormSubmission, readGithubEvent, readGithubIssue, readGithubIssueEventList, readGithubIssueList, readGithubRun, readGithubRunList, readInboundMail, readLinearAnswer, readLinearEvent, readLinearIssue, readWebhookPayload } from "./contracts/trigger-payloads.js";
|
|
45
46
|
export { FLOW_TRIGGER_KINDS };
|
|
46
47
|
export const CHAT_APP_NAMES = { slack: "Slack", discord: "Discord", teams: "Teams", telegram: "Telegram" };
|
|
47
48
|
/** What each kind is called on the canvas. */
|
|
@@ -771,7 +772,7 @@ async function fetchMail(store, config, trigger, io, now) {
|
|
|
771
772
|
if (!read.ok)
|
|
772
773
|
return { ok: false, problem: read.said };
|
|
773
774
|
const own = signed.address.toLowerCase();
|
|
774
|
-
const items = read.mails.filter(mail => mailMatches(config, mail)).map(mail => {
|
|
775
|
+
const items = read.mails.map(readInboundMail).filter(mail => mailMatches(config, mail)).map(mail => {
|
|
775
776
|
const key = `mail:${read.at.validity}:${mail.uid}`, at = mailCursorText({ validity: read.at.validity, uid: mail.uid });
|
|
776
777
|
const label = `Email from ${mail.from || "an unknown sender"}`.slice(0, 120);
|
|
777
778
|
// Machines and this account itself never start cards: a flow that answers mail mustn't answer them.
|
|
@@ -793,44 +794,42 @@ async function fetchMail(store, config, trigger, io, now) {
|
|
|
793
794
|
: read.renumbered ? "The mail server renumbered this folder; watching it from now." : undefined;
|
|
794
795
|
return { ok: true, items, cursor: mailCursorText(read.at), more: read.more, ...(said === undefined ? {} : { said }) };
|
|
795
796
|
}
|
|
796
|
-
const text = (value) => typeof value === "string" ? value : "";
|
|
797
|
-
const record = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
798
797
|
/** A GitHub issue or pull request as a card, or why it is left out; null when it doesn't match. */
|
|
799
798
|
export function githubItem(config, raw, what) {
|
|
800
|
-
const item =
|
|
801
|
-
const number = Number(item
|
|
799
|
+
const item = readGithubIssue(raw);
|
|
800
|
+
const number = Number(item.number);
|
|
802
801
|
if (!Number.isSafeInteger(number))
|
|
803
802
|
return null;
|
|
804
|
-
if (what === "issue" && item
|
|
803
|
+
if (what === "issue" && item.pull_request !== undefined)
|
|
805
804
|
return null;
|
|
806
|
-
if (what === "pull" && item
|
|
805
|
+
if (what === "pull" && item.draft === true)
|
|
807
806
|
return null;
|
|
808
|
-
const labels =
|
|
807
|
+
const labels = (item.labels ?? []).map(one => (one.name ?? "").toLowerCase());
|
|
809
808
|
if (config.label !== null && !labels.includes(config.label.toLowerCase()))
|
|
810
809
|
return null;
|
|
811
810
|
const label = what === "issue" ? `GitHub issue #${number}` : `Pull request #${number}`;
|
|
812
|
-
const key = `${what}:${number}`, at =
|
|
813
|
-
if (config.from === "team" && !TEAM.has(
|
|
811
|
+
const key = `${what}:${number}`, at = item.updated_at || null;
|
|
812
|
+
if (config.from === "team" && !TEAM.has(item.author_association ?? ""))
|
|
814
813
|
return { key, skip: "opened by someone without write access", label, at };
|
|
815
|
-
const login =
|
|
816
|
-
const branches = what === "pull" ? ` (${
|
|
817
|
-
return { key, at, title:
|
|
818
|
-
description: `From ${what === "issue" ? "GitHub issue" : "pull request"} #${number} by @${login}${branches}:\n\n${
|
|
819
|
-
source: { kind: "github", label, url:
|
|
814
|
+
const login = item.user?.login || "someone";
|
|
815
|
+
const branches = what === "pull" ? ` (${item.head?.ref ?? ""} → ${item.base?.ref ?? ""})` : "";
|
|
816
|
+
return { key, at, title: item.title || label,
|
|
817
|
+
description: `From ${what === "issue" ? "GitHub issue" : "pull request"} #${number} by @${login}${branches}:\n\n${item.body ?? ""}`.trim().slice(0, 4000),
|
|
818
|
+
source: { kind: "github", label, url: item.html_url || null } };
|
|
820
819
|
}
|
|
821
820
|
/** A failed workflow run as a card: one card per failing commit on the watched branch. */
|
|
822
821
|
export function githubRunItem(config, raw) {
|
|
823
|
-
const run =
|
|
824
|
-
if (
|
|
822
|
+
const run = readGithubRun(raw);
|
|
823
|
+
if (run.conclusion !== "failure" || (config.branch !== null && (run.head_branch ?? "") !== config.branch))
|
|
825
824
|
return null;
|
|
826
|
-
const sha =
|
|
825
|
+
const sha = run.head_sha ?? "";
|
|
827
826
|
if (sha === "")
|
|
828
827
|
return null;
|
|
829
|
-
const name =
|
|
830
|
-
const commit =
|
|
831
|
-
return { key: `checks:${sha}`, at:
|
|
832
|
-
description: `From GitHub: “${name}” failed on ${config.branch} at ${sha.slice(0, 7)}${commit === "" ? "" : ` (${commit})`}.\nRun: ${
|
|
833
|
-
source: { kind: "github", label: `Failed check on ${config.branch}`, url:
|
|
828
|
+
const name = run.name || "Checks";
|
|
829
|
+
const commit = run.display_title ?? "";
|
|
830
|
+
return { key: `checks:${sha}`, at: run.created_at || null, title: `Checks failed on ${config.branch}: ${name}`,
|
|
831
|
+
description: `From GitHub: “${name}” failed on ${config.branch} at ${sha.slice(0, 7)}${commit === "" ? "" : ` (${commit})`}.\nRun: ${run.html_url ?? ""}`,
|
|
832
|
+
source: { kind: "github", label: `Failed check on ${config.branch}`, url: run.html_url || null } };
|
|
834
833
|
}
|
|
835
834
|
async function fetchGitHub(config, trigger, gh) {
|
|
836
835
|
const since = trigger.cursor ?? trigger.createdAt;
|
|
@@ -861,20 +860,19 @@ async function fetchGitHub(config, trigger, gh) {
|
|
|
861
860
|
}
|
|
862
861
|
const created = Date.parse(trigger.createdAt);
|
|
863
862
|
if (config.watch === "checks") {
|
|
864
|
-
const runs = (
|
|
863
|
+
const runs = (readGithubRunList(body).workflow_runs ?? []).filter(one => Date.parse(one.created_at ?? "") > created);
|
|
865
864
|
const items = runs.reverse().map(one => githubRunItem(config, one)).filter((one) => one !== null);
|
|
866
865
|
return { ok: true, items: unique(items), cursor: trigger.cursor };
|
|
867
866
|
}
|
|
868
|
-
const list = Array.isArray(body) ? body : [];
|
|
869
867
|
if (labelEvents) {
|
|
870
|
-
const labeled =
|
|
871
|
-
&& Date.parse(
|
|
872
|
-
const items = labeled.map(one => { const item = githubItem(config,
|
|
873
|
-
return { ok: true, items: unique(items), cursor: labeled.map(one =>
|
|
868
|
+
const labeled = readGithubIssueEventList(body).filter(one => one.event === "labeled" && (one.label?.name ?? "").toLowerCase() === config.label.toLowerCase()
|
|
869
|
+
&& Date.parse(one.created_at ?? "") > Date.parse(since)).reverse();
|
|
870
|
+
const items = labeled.map(one => { const item = githubItem(config, one.issue, "issue"); return item === null ? null : { ...item, at: one.created_at || null }; }).filter((one) => one !== null);
|
|
871
|
+
return { ok: true, items: unique(items), cursor: labeled.map(one => one.created_at ?? "").at(-1) ?? trigger.cursor };
|
|
874
872
|
}
|
|
875
873
|
// New issues and pull requests: opened after the trigger was added, oldest first; the key keeps each to one card.
|
|
876
874
|
const what = config.watch === "pulls" ? "pull" : "issue";
|
|
877
|
-
const items =
|
|
875
|
+
const items = readGithubIssueList(body).filter(one => Date.parse(one.created_at ?? "") > created).reverse().map(one => githubItem(config, one, what)).filter((one) => one !== null);
|
|
878
876
|
return { ok: true, items, cursor: trigger.cursor };
|
|
879
877
|
}
|
|
880
878
|
const unique = (items) => items.filter((item, index) => items.findIndex(one => one.key === item.key) === index);
|
|
@@ -882,21 +880,20 @@ export const LINEAR_URL = "https://api.linear.app/graphql";
|
|
|
882
880
|
export const LINEAR_QUERY = "query FlowTrigger($filter: IssueFilter) { issues(filter: $filter, first: 50, orderBy: updatedAt) { nodes { identifier title description url updatedAt state { name } team { key } labels { nodes { name } } } } }";
|
|
883
881
|
/** A Linear issue as a card; null when it doesn't match the trigger. */
|
|
884
882
|
export function linearItem(config, raw) {
|
|
885
|
-
const issue =
|
|
886
|
-
const identifier =
|
|
883
|
+
const issue = readLinearIssue(raw);
|
|
884
|
+
const identifier = issue.identifier ?? "";
|
|
887
885
|
if (identifier === "")
|
|
888
886
|
return null;
|
|
889
|
-
const
|
|
890
|
-
|
|
891
|
-
if (config.team !== null && text(record(issue["team"])["key"]).toUpperCase() !== config.team)
|
|
887
|
+
const labels = linearLabelNames(issue).map(one => one.toLowerCase());
|
|
888
|
+
if (config.team !== null && (issue.team?.key ?? "").toUpperCase() !== config.team)
|
|
892
889
|
return null;
|
|
893
|
-
if (config.state !== null &&
|
|
890
|
+
if (config.state !== null && (issue.state?.name ?? "").toLowerCase() !== config.state.toLowerCase())
|
|
894
891
|
return null;
|
|
895
892
|
if (config.label !== null && !labels.includes(config.label.toLowerCase()))
|
|
896
893
|
return null;
|
|
897
|
-
return { key: `linear:${identifier}`, at:
|
|
898
|
-
description: `From Linear ${identifier}:\n\n${
|
|
899
|
-
source: { kind: "linear", label: `Linear ${identifier}`, url:
|
|
894
|
+
return { key: `linear:${identifier}`, at: issue.updatedAt || null, title: issue.title || identifier,
|
|
895
|
+
description: `From Linear ${identifier}:\n\n${issue.description ?? ""}`.trim().slice(0, 4000),
|
|
896
|
+
source: { kind: "linear", label: `Linear ${identifier}`, url: issue.url || null } };
|
|
900
897
|
}
|
|
901
898
|
async function fetchLinear(config, trigger, io) {
|
|
902
899
|
const key = readLinearKey(io.dir);
|
|
@@ -917,22 +914,22 @@ async function fetchLinear(config, trigger, io) {
|
|
|
917
914
|
catch {
|
|
918
915
|
return { ok: false, problem: "Couldn't reach Linear." };
|
|
919
916
|
}
|
|
920
|
-
let
|
|
917
|
+
let raw;
|
|
921
918
|
try {
|
|
922
|
-
|
|
919
|
+
raw = await response.json();
|
|
923
920
|
}
|
|
924
921
|
catch {
|
|
925
922
|
return { ok: false, problem: `Linear answered ${response.status} and it couldn't be read.` };
|
|
926
923
|
}
|
|
927
|
-
const
|
|
924
|
+
const body = readLinearAnswer(raw);
|
|
925
|
+
const errors = (body.errors ?? []).map(one => one.message ?? "");
|
|
928
926
|
if (response.status === 401 || errors.some(one => /authenticat|api key/i.test(one)))
|
|
929
927
|
return { ok: false, problem: "Linear didn't accept the key. Add a new one on this flow's Triggers panel." };
|
|
930
928
|
if (!response.ok || errors.length > 0)
|
|
931
929
|
return { ok: false, problem: `Linear said: ${(errors[0] ?? `status ${response.status}`).slice(0, 160)}` };
|
|
932
|
-
const
|
|
933
|
-
const issues = (Array.isArray(nodes) ? nodes : []).sort((a, b) => text(record(a)["updatedAt"]).localeCompare(text(record(b)["updatedAt"])));
|
|
930
|
+
const issues = (body.data?.issues?.nodes ?? []).sort((a, b) => (a.updatedAt ?? "").localeCompare(b.updatedAt ?? ""));
|
|
934
931
|
const items = issues.map(one => linearItem(config, one)).filter((one) => one !== null);
|
|
935
|
-
return { ok: true, items, cursor: issues.map(one =>
|
|
932
|
+
return { ok: true, items, cursor: issues.map(one => one.updatedAt ?? "").filter(one => one !== "").at(-1) ?? trigger.cursor };
|
|
936
933
|
}
|
|
937
934
|
const header = (delivery, name) => { const value = delivery.headers[name]; return Array.isArray(value) ? value[0] ?? "" : value ?? ""; };
|
|
938
935
|
function sameHex(expected, given) {
|
|
@@ -963,8 +960,8 @@ export function receiveFlowHook(store, token, delivery, dir, now) {
|
|
|
963
960
|
const signature = header(delivery, "x-hub-signature-256");
|
|
964
961
|
if (secret === null || !sameHex(`sha256=${createHmac("sha256", secret).update(delivery.body).digest("hex")}`, signature))
|
|
965
962
|
return { status: 401, said: "Signature doesn't match." };
|
|
966
|
-
const event = header(delivery, "x-github-event"), body =
|
|
967
|
-
const repository =
|
|
963
|
+
const event = header(delivery, "x-github-event"), body = readGithubEvent(payload), action = body.action ?? "";
|
|
964
|
+
const repository = body.repository?.full_name ?? "";
|
|
968
965
|
if (event === "ping") {
|
|
969
966
|
note("GitHub connected.");
|
|
970
967
|
return { status: 200, said: "Connected." };
|
|
@@ -972,10 +969,10 @@ export function receiveFlowHook(store, token, delivery, dir, now) {
|
|
|
972
969
|
if (repository.toLowerCase() !== config.repo.toLowerCase())
|
|
973
970
|
return { status: 202, said: "Not this repository." };
|
|
974
971
|
// A label trigger takes the moment its label is added (or an issue opened with it); a plain one takes new issues.
|
|
975
|
-
const labeledNow = action === "labeled" && config.label !== null &&
|
|
976
|
-
const found = config.watch === "issues" && event === "issues" && (action === "opened" || labeledNow) ? githubItem(config, body
|
|
977
|
-
: config.watch === "pulls" && event === "pull_request" && ["opened", "ready_for_review", "labeled", "reopened"].includes(action) ? githubItem(config, body
|
|
978
|
-
: config.watch === "checks" && event === "workflow_run" && action === "completed" ? githubRunItem(config, body
|
|
972
|
+
const labeledNow = action === "labeled" && config.label !== null && (body.label?.name ?? "").toLowerCase() === config.label.toLowerCase();
|
|
973
|
+
const found = config.watch === "issues" && event === "issues" && (action === "opened" || labeledNow) ? githubItem(config, body.issue, "issue")
|
|
974
|
+
: config.watch === "pulls" && event === "pull_request" && ["opened", "ready_for_review", "labeled", "reopened"].includes(action) ? githubItem(config, body.pull_request, "pull")
|
|
975
|
+
: config.watch === "checks" && event === "workflow_run" && action === "completed" ? githubRunItem(config, body.workflow_run) : null;
|
|
979
976
|
items = found === null ? [] : [found];
|
|
980
977
|
}
|
|
981
978
|
else if (config.kind === "linear") {
|
|
@@ -984,23 +981,26 @@ export function receiveFlowHook(store, token, delivery, dir, now) {
|
|
|
984
981
|
return { status: 401, said: "Paste Linear's signing secret on the flow's Triggers panel first." };
|
|
985
982
|
if (!sameHex(createHmac("sha256", secret).update(delivery.body).digest("hex"), header(delivery, "linear-signature")))
|
|
986
983
|
return { status: 401, said: "Signature doesn't match." };
|
|
987
|
-
const body =
|
|
984
|
+
const body = readLinearEvent(payload), sent = Number(body.webhookTimestamp);
|
|
988
985
|
if (!Number.isFinite(sent) || Math.abs(now.getTime() - sent) > 5 * 60_000)
|
|
989
986
|
return { status: 401, said: "Too old." };
|
|
990
|
-
if (
|
|
987
|
+
if (body.type !== "Issue" || !["create", "update"].includes(body.action ?? ""))
|
|
991
988
|
return { status: 202, said: "Ignored." };
|
|
992
989
|
// A state trigger fires when the issue arrives in that state, not on every later edit there.
|
|
993
|
-
const moved =
|
|
994
|
-
const found = moved ? linearItem(config, body
|
|
990
|
+
const moved = body.action === "create" || body.updatedFrom?.stateId !== undefined || config.state === null;
|
|
991
|
+
const found = moved ? linearItem(config, body.data) : null;
|
|
995
992
|
items = found === null ? [] : [found];
|
|
996
993
|
}
|
|
997
994
|
else if (config.kind === "webhook") {
|
|
998
|
-
|
|
995
|
+
// Any JSON, kept as sent: the trigger's own paths are picked from it, and it is shown whole when they find no text.
|
|
996
|
+
const body = readWebhookPayload(payload);
|
|
997
|
+
const record = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
998
|
+
const pick = (path) => path === null ? null : path.split(".").reduce((node, part) => record(node)[part], body);
|
|
999
999
|
const title = pick(config.titleField ?? "title") ?? pick("summary") ?? pick("message");
|
|
1000
1000
|
const bodyText = pick(config.bodyField ?? "description") ?? pick("body") ?? pick("text");
|
|
1001
1001
|
const id = header(delivery, "x-request-id") || header(delivery, "x-delivery-id") || header(delivery, "idempotency-key") || createHash("sha256").update(delivery.body).digest("hex");
|
|
1002
1002
|
items = [{ key: `hook:${id.slice(0, 120)}`, at: null, title: typeof title === "string" && title.trim() !== "" ? title : config.title,
|
|
1003
|
-
description: `From a webhook:\n\n${typeof bodyText === "string" ? bodyText : JSON.stringify(
|
|
1003
|
+
description: `From a webhook:\n\n${typeof bodyText === "string" ? bodyText : JSON.stringify(body, null, 2)}`.slice(0, 4000),
|
|
1004
1004
|
source: { kind: "webhook", label: "Webhook", url: null } }];
|
|
1005
1005
|
}
|
|
1006
1006
|
if (items.length === 0)
|
|
@@ -1081,13 +1081,14 @@ export function receiveFlowForm(store, token, fields, now) {
|
|
|
1081
1081
|
const again = `<p><a href="${esc(FORM_PATH + token)}">Send another</a></p>`;
|
|
1082
1082
|
const thanks = formHtml(config.label, `<p>Thanks — it's been sent.</p>${again}`);
|
|
1083
1083
|
// A filled trap, or a page posted faster than a person could, gets thanks and makes nothing.
|
|
1084
|
-
const
|
|
1085
|
-
|
|
1084
|
+
const form = readFormSubmission(fields, config.questions.length);
|
|
1085
|
+
const shown = Number(form.t);
|
|
1086
|
+
if ((form.website ?? "") !== "" || !Number.isFinite(shown) || now.getTime() - shown < 2000 || now.getTime() - shown > 86_400_000)
|
|
1086
1087
|
return { status: 200, html: thanks };
|
|
1087
1088
|
const recent = store.handle.prepare("SELECT COUNT(*) AS n FROM flow_trigger_event WHERE trigger = ? AND key LIKE 'form:%' AND at > ?").get(trigger.id, new Date(now.getTime() - 3_600_000).toISOString());
|
|
1088
1089
|
if (Number(recent?.["n"] ?? 0) >= FORM_CARDS_PER_HOUR)
|
|
1089
1090
|
return { status: 429, html: formHtml(config.label, `<p>Too many sent in the last hour. Try again later.</p>`) };
|
|
1090
|
-
const answers =
|
|
1091
|
+
const answers = form.answers.map((answer, index) => (answer ?? "").replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, "").trim().slice(0, index === 0 ? 200 : 3000));
|
|
1091
1092
|
if (answers[0] === "")
|
|
1092
1093
|
return { status: 400, html: formHtml(config.label, `<p>Answer “${esc(config.questions[0])}” first.</p>${again}`) };
|
|
1093
1094
|
const details = config.questions.slice(1).map((question, index) => answers[index + 1] === "" ? null : `${question}\n${answers[index + 1]}`).filter(one => one !== null).join("\n\n");
|
package/dist/mcp-connect.js
CHANGED
|
@@ -16,6 +16,7 @@ import { homedir } from "node:os";
|
|
|
16
16
|
import { basename } from "node:path";
|
|
17
17
|
import { LOCAL_APPS, addToolTo, catalogTool, localAppOf, projectToolsOf, readToolSecrets, setToolSecrets, testToolOf } from "./project-tools.js";
|
|
18
18
|
import { envValue } from "./names.js";
|
|
19
|
+
import { SCOPE_TOKEN, readAuthorizationServer, readProtectedResource, readRegistration, readTokenResponse } from "./contracts/integration-metadata.js";
|
|
19
20
|
/**
|
|
20
21
|
* Services that connect this way: their servers speak streamable HTTP and
|
|
21
22
|
* advertise registering clients on the spot. Advertising isn't accepting:
|
|
@@ -202,8 +203,7 @@ const GRANTED = "OAUTH_GRANTED_SCOPE";
|
|
|
202
203
|
export const CONNECT_CALLBACK = "/settings/tools/connected";
|
|
203
204
|
const VISIT_MS = 15 * 60_000, RENEW_RETRY_MS = 5 * 60_000;
|
|
204
205
|
const renewTried = new Map();
|
|
205
|
-
/**
|
|
206
|
-
const SCOPE_TOKEN = /^[\x21\x23-\x5b\x5d-\x7e]{1,128}$/;
|
|
206
|
+
/** The most scope text a sign-in address carries (addresses stay well under 8 KB). */
|
|
207
207
|
const SCOPE_PARAM_LIMIT = 6_000;
|
|
208
208
|
const json = async (fetcher, url) => {
|
|
209
209
|
try {
|
|
@@ -238,25 +238,29 @@ export async function discoverSignIn(mcpUrl, fetcher = fetch) {
|
|
|
238
238
|
catch {
|
|
239
239
|
pointer = null;
|
|
240
240
|
}
|
|
241
|
-
const
|
|
241
|
+
const found = (pointer !== null && https(pointer) ? await json(fetcher, pointer) : null)
|
|
242
242
|
?? await json(fetcher, `${mcp.origin}/.well-known/oauth-protected-resource${mcp.pathname === "/" ? "" : mcp.pathname}`)
|
|
243
243
|
?? await json(fetcher, `${mcp.origin}/.well-known/oauth-protected-resource`);
|
|
244
|
-
const
|
|
244
|
+
const resource = found === null ? {} : readProtectedResource(found);
|
|
245
|
+
const issuers = (resource.authorization_servers ?? []).filter(https);
|
|
245
246
|
const issuer = new URL(issuers[0] ?? mcp.origin);
|
|
246
247
|
const path = issuer.pathname === "/" ? "" : issuer.pathname.replace(/\/$/, "");
|
|
247
|
-
const
|
|
248
|
+
const answered = await json(fetcher, `${issuer.origin}/.well-known/oauth-authorization-server${path}`)
|
|
248
249
|
?? await json(fetcher, `${issuer.origin}/.well-known/oauth-authorization-server`)
|
|
249
250
|
?? (path === "" ? null : await json(fetcher, `${issuer.origin}${path}/.well-known/openid-configuration`))
|
|
250
251
|
?? await json(fetcher, `${issuer.origin}/.well-known/openid-configuration`);
|
|
251
|
-
if (
|
|
252
|
+
if (answered === null)
|
|
252
253
|
return null;
|
|
253
|
-
const
|
|
254
|
-
|
|
254
|
+
const meta = readAuthorizationServer(answered);
|
|
255
|
+
const { authorization_endpoint: authorize, token_endpoint: token, registration_endpoint: register } = meta;
|
|
256
|
+
if (!https(authorize) || !https(token) || !https(register))
|
|
257
|
+
return null;
|
|
258
|
+
if (!(meta.code_challenge_methods_supported ?? ["S256"]).includes("S256"))
|
|
255
259
|
return null;
|
|
256
260
|
// Ask for every scope the server lists for this resource: a token missing one the server itself requires is
|
|
257
261
|
// refused after a successful sign-in (PostHog lists 155 and needs user:read, the 140th). Only well-formed scope
|
|
258
262
|
// tokens, and at most what fits a sign-in address.
|
|
259
|
-
const listed =
|
|
263
|
+
const listed = resource.scopes_supported ?? [];
|
|
260
264
|
const scopes = [];
|
|
261
265
|
for (const one of listed) {
|
|
262
266
|
if (scopes.join(" ").length + one.length + 1 > SCOPE_PARAM_LIMIT)
|
|
@@ -264,8 +268,8 @@ export async function discoverSignIn(mcpUrl, fetcher = fetch) {
|
|
|
264
268
|
scopes.push(one);
|
|
265
269
|
}
|
|
266
270
|
// A token is for the resource the server names (Stripe's has no trailing slash), when it names one.
|
|
267
|
-
const named = resource
|
|
268
|
-
return { authorize
|
|
271
|
+
const named = resource.resource;
|
|
272
|
+
return { authorize, token, register, scopes, resource: https(named) ? named : null };
|
|
269
273
|
}
|
|
270
274
|
/**
|
|
271
275
|
* Start connecting a service: find its sign-in, register this console as a
|
|
@@ -287,11 +291,14 @@ export async function startConnect(input, fetcher = fetch, now = Date.now()) {
|
|
|
287
291
|
// Figma's answer (Oct 2026): it registers only apps it has approved. Said before reading a body that may not be JSON.
|
|
288
292
|
if (registered.status === 401 || registered.status === 403)
|
|
289
293
|
return { ok: false, said: `${service.label} doesn't let other apps sign in this way yet.${INSTEAD[service.id] === undefined ? "" : ` ${INSTEAD[service.id]}`}` };
|
|
290
|
-
const body = await registered.json();
|
|
291
|
-
|
|
294
|
+
const body = readRegistration(await registered.json());
|
|
295
|
+
// A `null` answer that says it registered is no answer, as it always read.
|
|
296
|
+
if (registered.ok && body === null)
|
|
297
|
+
return { ok: false, said: `${service.label} couldn't be reached.` };
|
|
298
|
+
if (!registered.ok || body?.client_id === undefined)
|
|
292
299
|
return { ok: false, said: `${service.label} didn't let Toolroll register (HTTP ${registered.status}).` };
|
|
293
300
|
clientId = body.client_id;
|
|
294
|
-
clientSecret =
|
|
301
|
+
clientSecret = body.client_secret ?? null;
|
|
295
302
|
}
|
|
296
303
|
catch {
|
|
297
304
|
return { ok: false, said: `${service.label} couldn't be reached.` };
|
|
@@ -327,9 +334,9 @@ export function connectedSpec(serviceId) {
|
|
|
327
334
|
secrets: [ACCESS, REFRESH, CLIENT, CLIENT_SECRET, TOKEN_URL, EXPIRES, RESOURCE].map(name => ({ name, optional: name !== ACCESS })), about: `${service.label}: ${service.about} Connected by signing in.` };
|
|
328
335
|
}
|
|
329
336
|
const tokensFrom = (body, now) => ({
|
|
330
|
-
access:
|
|
331
|
-
refresh:
|
|
332
|
-
expires:
|
|
337
|
+
access: body.access_token ?? null,
|
|
338
|
+
refresh: body.refresh_token ?? null,
|
|
339
|
+
expires: body.expires_in === undefined ? null : new Date(now + body.expires_in * 1000).toISOString(),
|
|
333
340
|
});
|
|
334
341
|
/**
|
|
335
342
|
* Finish connecting: trade the code for tokens, add the tool to the project
|
|
@@ -347,9 +354,13 @@ export async function finishConnect(store, visit, code, now, options = {}) {
|
|
|
347
354
|
try {
|
|
348
355
|
const answer = await fetcher(visit.token, { method: "POST", headers: { "content-type": "application/x-www-form-urlencoded", accept: "application/json" }, signal: AbortSignal.timeout(15_000),
|
|
349
356
|
body: new URLSearchParams({ grant_type: "authorization_code", code, redirect_uri: visit.redirect, client_id: visit.clientId, code_verifier: visit.verifier, resource: visit.resource, ...(visit.clientSecret === null ? {} : { client_secret: visit.clientSecret }) }) });
|
|
350
|
-
|
|
357
|
+
const read = readTokenResponse(await answer.json());
|
|
358
|
+
// An answer of `null` reads as none, like one that isn't JSON.
|
|
359
|
+
if (read === null)
|
|
360
|
+
return { ok: false, said: `${service.label} couldn't be reached to finish the sign-in.` };
|
|
361
|
+
body = read;
|
|
351
362
|
if (!answer.ok)
|
|
352
|
-
return { ok: false, said: `${service.label} didn't finish the sign-in (${
|
|
363
|
+
return { ok: false, said: `${service.label} didn't finish the sign-in (${body.error ?? `HTTP ${answer.status}`}).` };
|
|
353
364
|
}
|
|
354
365
|
catch {
|
|
355
366
|
return { ok: false, said: `${service.label} couldn't be reached to finish the sign-in.` };
|
|
@@ -363,7 +374,7 @@ export async function finishConnect(store, visit, code, now, options = {}) {
|
|
|
363
374
|
return { ok: false, said: added.message };
|
|
364
375
|
}
|
|
365
376
|
// The grant: what the answer says, or what was asked for when it says nothing; never one from an earlier sign-in.
|
|
366
|
-
const granted =
|
|
377
|
+
const granted = body.scopeSaid ? scopeList(body.scope) : scopeList(visit.scope ?? null);
|
|
367
378
|
setToolSecrets(visit.repo, spec.name, { [ACCESS]: tokens.access, [CLIENT]: visit.clientId, [TOKEN_URL]: visit.token, [RESOURCE]: visit.resource, [GRANTED]: granted?.join(" ") ?? "",
|
|
368
379
|
...(tokens.refresh === null ? {} : { [REFRESH]: tokens.refresh }), ...(tokens.expires === null ? {} : { [EXPIRES]: tokens.expires }), ...(visit.clientSecret === null ? {} : { [CLIENT_SECRET]: visit.clientSecret }) }, home);
|
|
369
380
|
// The success line names the project it went to: the page and chat may be looking at another.
|
|
@@ -394,7 +405,11 @@ export async function refreshConnections(store, repos, now, options = {}) {
|
|
|
394
405
|
try {
|
|
395
406
|
const answer = await fetcher(values[TOKEN_URL], { method: "POST", headers: { "content-type": "application/x-www-form-urlencoded", accept: "application/json" }, signal: AbortSignal.timeout(15_000),
|
|
396
407
|
body: new URLSearchParams({ grant_type: "refresh_token", refresh_token: values[REFRESH], client_id: values[CLIENT], ...(values[CLIENT_SECRET] === undefined ? {} : { client_secret: values[CLIENT_SECRET] }), ...(values[RESOURCE] === undefined ? {} : { resource: values[RESOURCE] }) }) });
|
|
397
|
-
const body = await answer.json();
|
|
408
|
+
const body = readTokenResponse(await answer.json());
|
|
409
|
+
if (body === null) {
|
|
410
|
+
report.problems.push(`${tool.name}: couldn't reach it to renew its sign-in`);
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
398
413
|
const tokens = tokensFrom(body, now.getTime());
|
|
399
414
|
if (!answer.ok || tokens.access === null) {
|
|
400
415
|
report.problems.push(`${tool.name}: its sign-in ran out; connect it again on the Tools page`);
|
|
@@ -402,7 +417,7 @@ export async function refreshConnections(store, repos, now, options = {}) {
|
|
|
402
417
|
}
|
|
403
418
|
// A refresh carries the same grant or a narrower one, and says so when it changes; said badly, it is unknown.
|
|
404
419
|
setToolSecrets(repo, tool.name, { [ACCESS]: tokens.access, ...(tokens.refresh === null ? {} : { [REFRESH]: tokens.refresh }), ...(tokens.expires === null ? {} : { [EXPIRES]: tokens.expires }),
|
|
405
|
-
...(
|
|
420
|
+
...(body.scopeSaid ? { [GRANTED]: scopeList(body.scope)?.join(" ") ?? "" } : {}) }, home);
|
|
406
421
|
renewTried.delete(key);
|
|
407
422
|
report.refreshed++;
|
|
408
423
|
}
|
package/dist/model-catalog.js
CHANGED
|
@@ -11,6 +11,7 @@ import { accessSync, constants, lstatSync, readFileSync, realpathSync } from "no
|
|
|
11
11
|
import { homedir } from "node:os";
|
|
12
12
|
import { delimiter, join } from "node:path";
|
|
13
13
|
import { fetchOpenRouterModels } from "./openrouter-models.js";
|
|
14
|
+
import { codexModelSchema, codexModelsCacheSchema, npmLatestSchema } from "./contracts/model-catalog.js";
|
|
14
15
|
import { readCappedBody } from "./converse.js";
|
|
15
16
|
import { validModelId } from "./provider.js";
|
|
16
17
|
import { activeUpdateWork } from "./desktop-update-gate.js";
|
|
@@ -73,10 +74,11 @@ export function codexCatalog(home = homedir()) {
|
|
|
73
74
|
const path = join(home, ".codex", "models_cache.json");
|
|
74
75
|
if (lstatSync(path).size >= 2_000_000)
|
|
75
76
|
return [];
|
|
76
|
-
const data = JSON.parse(readFileSync(path, "utf8"));
|
|
77
|
-
return (
|
|
78
|
-
|
|
79
|
-
|
|
77
|
+
const data = codexModelsCacheSchema.safeParse(JSON.parse(readFileSync(path, "utf8")));
|
|
78
|
+
return (data.success ? data.data.models : []).flatMap(entry => {
|
|
79
|
+
const one = codexModelSchema.safeParse(entry);
|
|
80
|
+
return one.success ? [one.data] : [];
|
|
81
|
+
}).slice(0, 30).map(one => ({ id: one.slug, name: typeof one.display_name === "string" ? one.display_name.slice(0, 80) : one.slug }));
|
|
80
82
|
}
|
|
81
83
|
catch {
|
|
82
84
|
return [];
|
|
@@ -255,8 +257,8 @@ async function latestVersion(pkg, fetcher) {
|
|
|
255
257
|
const body = await readCappedBody(response, 2_000_000);
|
|
256
258
|
if (body === null)
|
|
257
259
|
return null;
|
|
258
|
-
const
|
|
259
|
-
return
|
|
260
|
+
const latest = npmLatestSchema.safeParse(JSON.parse(new TextDecoder().decode(body)));
|
|
261
|
+
return latest.success ? latest.data.version : null;
|
|
260
262
|
}
|
|
261
263
|
catch {
|
|
262
264
|
return null;
|
package/dist/observations.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Runner } from "./builder.js";
|
|
2
2
|
import type { Store } from "./store.js";
|
|
3
|
+
import { type ObservationCase } from "./contracts/observation-request.js";
|
|
3
4
|
export declare const OBSERVATION_MAILBOX = "STANDING-ORDERS-OBSERVATIONS.json";
|
|
4
5
|
export declare const OBSERVATION_CAPTURE = "machine focused observations v1";
|
|
5
6
|
export declare const OBSERVATION_FILE = "REVIEW-OBSERVATIONS.json";
|
|
@@ -18,12 +19,7 @@ export type ObservationBrief = {
|
|
|
18
19
|
gateDigest: string;
|
|
19
20
|
};
|
|
20
21
|
export declare function focusedTestCommandSupported(command: string): boolean;
|
|
21
|
-
export type ObservationCase
|
|
22
|
-
criterion: string;
|
|
23
|
-
at: "base" | "head";
|
|
24
|
-
testPath: string;
|
|
25
|
-
testName: string;
|
|
26
|
-
};
|
|
22
|
+
export type { ObservationCase };
|
|
27
23
|
export declare function originalTaskBase(store: Store, taskId: string): string | null;
|
|
28
24
|
/** The existing sealed revision brief identifies this narrower kind of work.
|
|
29
25
|
* A title, agent-authored mailbox, or handoff can never elect gate reuse. */
|