@intentic/sandbox-contract 1.165.0 → 1.166.0
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/dist/contracts/agent.contract.d.ts +2 -2
- package/dist/contracts/ci.contract.d.ts +4 -0
- package/dist/contracts/ci.contract.d.ts.map +1 -1
- package/dist/contracts/ci.contract.js +2 -1
- package/dist/contracts/ci.contract.js.map +1 -1
- package/dist/contracts/extensions.contract.d.ts +1 -0
- package/dist/contracts/extensions.contract.d.ts.map +1 -1
- package/dist/contracts/gate.contract.d.ts +47 -0
- package/dist/contracts/gate.contract.d.ts.map +1 -0
- package/dist/contracts/gate.contract.js +9 -0
- package/dist/contracts/gate.contract.js.map +1 -0
- package/dist/contracts/settings.contract.d.ts +8 -0
- package/dist/contracts/settings.contract.d.ts.map +1 -1
- package/dist/contracts/system.contract.d.ts +46 -4
- package/dist/contracts/system.contract.d.ts.map +1 -1
- package/dist/contracts/workspace.contract.d.ts +3 -1
- package/dist/contracts/workspace.contract.d.ts.map +1 -1
- package/dist/events.d.ts +94 -2
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +21 -2
- package/dist/events.js.map +1 -1
- package/dist/index.d.ts +107 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/model-order.d.ts.map +1 -1
- package/dist/model-order.js +25 -6
- package/dist/model-order.js.map +1 -1
- package/dist/schemas.d.ts +79 -1
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +35 -1
- package/dist/schemas.js.map +1 -1
- package/dist/session-names.d.ts +8 -0
- package/dist/session-names.d.ts.map +1 -0
- package/dist/session-names.js +17 -0
- package/dist/session-names.js.map +1 -0
- package/package.json +13 -2
- package/src/contracts/ci.contract.ts +5 -1
- package/src/contracts/gate.contract.ts +19 -0
- package/src/events.ts +54 -7
- package/src/index.ts +3 -0
- package/src/model-order.test.ts +29 -12
- package/src/model-order.ts +49 -17
- package/src/quick-model.test.ts +2 -2
- package/src/schemas.test.ts +10 -0
- package/src/schemas.ts +138 -3
- package/src/session-names.ts +44 -0
package/src/index.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { claudeContract } from "./contracts/claude.contract.js";
|
|
|
10
10
|
import { codexContract } from "./contracts/codex.contract.js";
|
|
11
11
|
import { draftsContract } from "./contracts/drafts.contract.js";
|
|
12
12
|
import { extensionsContract } from "./contracts/extensions.contract.js";
|
|
13
|
+
import { gateContract } from "./contracts/gate.contract.js";
|
|
13
14
|
import { geminiContract } from "./contracts/gemini.contract.js";
|
|
14
15
|
import { gitContract } from "./contracts/git.contract.js";
|
|
15
16
|
import { grokContract } from "./contracts/grok.contract.js";
|
|
@@ -41,6 +42,7 @@ export { claudeContract } from "./contracts/claude.contract.js";
|
|
|
41
42
|
export { codexContract } from "./contracts/codex.contract.js";
|
|
42
43
|
export { draftsContract } from "./contracts/drafts.contract.js";
|
|
43
44
|
export { extensionsContract } from "./contracts/extensions.contract.js";
|
|
45
|
+
export { gateContract } from "./contracts/gate.contract.js";
|
|
44
46
|
export { geminiContract } from "./contracts/gemini.contract.js";
|
|
45
47
|
export { gitContract } from "./contracts/git.contract.js";
|
|
46
48
|
export { grokContract } from "./contracts/grok.contract.js";
|
|
@@ -92,6 +94,7 @@ export const sandboxContract = {
|
|
|
92
94
|
settings: settingsContract,
|
|
93
95
|
intentic: intenticContract,
|
|
94
96
|
gemini: geminiContract,
|
|
97
|
+
gate: gateContract,
|
|
95
98
|
git: gitContract,
|
|
96
99
|
grok: grokContract,
|
|
97
100
|
kimi: kimiContract,
|
package/src/model-order.test.ts
CHANGED
|
@@ -7,37 +7,42 @@ import { compareCheapestFirst, compareModelIds, compareUnrankedModelIds, familyO
|
|
|
7
7
|
* conversations on whichever id sorted first. */
|
|
8
8
|
|
|
9
9
|
// A Codex catalog exactly as an OpenAI-compatible /v1/models hands it over: alphabetical, i.e. meaningless.
|
|
10
|
-
const CODEX = ["gpt-5.1-codex", "gpt-5.4-mini", "gpt-5.5", "gpt-5.6-sol", "gpt-5.6-terra"];
|
|
10
|
+
const CODEX = ["gpt-5.1-codex", "gpt-5.4-mini", "gpt-5.5", "gpt-5.6-luna", "gpt-5.6-sol", "gpt-5.6-terra"];
|
|
11
11
|
|
|
12
12
|
test("ranks the frontier line above the cheap one and the newest release above its predecessors", () => {
|
|
13
13
|
// The base line (no tier word) leads, newest first; the mini rung sinks under all of it regardless of how
|
|
14
14
|
// recently it shipped — which is the whole decision a user makes in this list.
|
|
15
|
-
expect(CODEX.toSorted(compareModelIds)).toEqual(["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.5", "gpt-5.1-codex", "gpt-5.4-mini"]);
|
|
15
|
+
expect(CODEX.toSorted(compareModelIds)).toEqual(["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna", "gpt-5.5", "gpt-5.1-codex", "gpt-5.4-mini"]);
|
|
16
16
|
});
|
|
17
17
|
|
|
18
|
-
test("
|
|
19
|
-
// Arrival order survives only as the tiebreak between two ids the rule ranks equally (the 5.6 siblings), so
|
|
20
|
-
// an alphabetical registry and a reversed one can no longer disagree about which model the group opens on.
|
|
18
|
+
test("orders a release's named tiers strongest-first whichever order the endpoint listed them in", () => {
|
|
21
19
|
for (const arrival of [CODEX.toSorted(), CODEX.toReversed()]) {
|
|
22
20
|
const ordered = arrival.toSorted(compareModelIds);
|
|
23
21
|
|
|
24
|
-
expect(ordered.slice(0,
|
|
22
|
+
expect(ordered.slice(0, 3)).toEqual(["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"]);
|
|
25
23
|
expect(ordered.at(-1)).toBe("gpt-5.4-mini");
|
|
26
24
|
}
|
|
27
25
|
});
|
|
28
26
|
|
|
29
|
-
test("
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
27
|
+
test("keeps release-local tiers below the next generation and above the previous one", () => {
|
|
28
|
+
expect(["gpt-5.6-luna", "gpt-5.5", "gpt-5.7", "gpt-5.6-sol"].toSorted(compareModelIds)).toEqual([
|
|
29
|
+
"gpt-5.7",
|
|
30
|
+
"gpt-5.6-sol",
|
|
31
|
+
"gpt-5.6-luna",
|
|
32
|
+
"gpt-5.5",
|
|
33
|
+
]);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("the Codex release-tier order is stable across catalog refreshes", () => {
|
|
33
37
|
const arrivals = [
|
|
34
38
|
["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"],
|
|
35
39
|
["gpt-5.6-terra", "gpt-5.6-luna", "gpt-5.6-sol"],
|
|
36
40
|
["gpt-5.6-luna", "gpt-5.6-sol", "gpt-5.6-terra"],
|
|
37
41
|
];
|
|
38
|
-
const heads = arrivals.map((arrival) => arrival.toSorted(compareUnrankedModelIds)[0]);
|
|
39
42
|
|
|
40
|
-
|
|
43
|
+
for (const arrival of arrivals) {
|
|
44
|
+
expect(arrival.toSorted(compareUnrankedModelIds)).toEqual(["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"]);
|
|
45
|
+
}
|
|
41
46
|
// Same rule, so ranking still outranks the tiebreak: the mini rung stays at the tail, under every sibling.
|
|
42
47
|
expect(["gpt-5.4-mini", ...arrivals[0]!].toSorted(compareUnrankedModelIds).at(-1)).toBe("gpt-5.4-mini");
|
|
43
48
|
});
|
|
@@ -68,6 +73,14 @@ test("reads each vendor's tier vocabulary, not just Claude's", () => {
|
|
|
68
73
|
]);
|
|
69
74
|
});
|
|
70
75
|
|
|
76
|
+
test("reads Kimi's k-prefixed generation so K3 leads the K2.x catalog", () => {
|
|
77
|
+
const catalog = ["kimi-k2.6", "kimi-k2.7-code-highspeed", "kimi-k3", "kimi-k2.7-code"];
|
|
78
|
+
|
|
79
|
+
expect(releaseOf("kimi-k3")).toEqual({ version: [3], date: 0 });
|
|
80
|
+
expect(familyOf("kimi-k3")).toBe(familyOf("kimi-k2.6"));
|
|
81
|
+
expect(catalog.toSorted(compareUnrankedModelIds)).toEqual(["kimi-k3", "kimi-k2.7-code", "kimi-k2.7-code-highspeed", "kimi-k2.6"]);
|
|
82
|
+
});
|
|
83
|
+
|
|
71
84
|
test("the rightmost tier word wins, because tier words compose", () => {
|
|
72
85
|
// flash-lite is the cheap end of Flash, codex-max the frontier end of Codex — reading the leftmost word
|
|
73
86
|
// instead would file both under the tier they modify.
|
|
@@ -169,6 +182,10 @@ test("finds each vendor's own cheap rung, including a re-served open-weights row
|
|
|
169
182
|
expect(["grok-4", "grok-4-fast"].toSorted(compareCheapestFirst)[0]).toBe("grok-4-fast");
|
|
170
183
|
});
|
|
171
184
|
|
|
185
|
+
test("reads a release-local tier ladder from the cheap end too", () => {
|
|
186
|
+
expect(["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"].toSorted(compareCheapestFirst)[0]).toBe("gpt-5.6-luna");
|
|
187
|
+
});
|
|
188
|
+
|
|
172
189
|
test("falls back on the newest release for a catalog that publishes no cheap tier at all", () => {
|
|
173
190
|
// Kimi names no tier word anywhere, so every row is UNRANKED and the tier term cancels. Serving the newest
|
|
174
191
|
// of what it does publish is the honest answer — there is no cheaper rung to find.
|
package/src/model-order.ts
CHANGED
|
@@ -14,9 +14,11 @@
|
|
|
14
14
|
* every vendor names its models the same way — which is what lets the daemon's four catalog services and the
|
|
15
15
|
* web's picker share one rule instead of each inventing a local one. */
|
|
16
16
|
|
|
17
|
-
// A version-ish segment: digits and dots, optionally
|
|
18
|
-
//
|
|
19
|
-
|
|
17
|
+
// A version-ish segment: digits and dots, optionally prefixed by the vendor's version marker (`4`, `5.1`, `v2`,
|
|
18
|
+
// `k3`, `k2.7`, `20251001`). Kimi is the one provider that fuses the marker with the generation; treating `k3`
|
|
19
|
+
// as a name made the current flagship look unversioned, so K2.x sorted above it. Everything else is a NAME
|
|
20
|
+
// segment and belongs to the family — which is what makes the split below exhaustive.
|
|
21
|
+
const VERSION_SEGMENT = /^(?:v|k)?[\d.]+$/i;
|
|
20
22
|
|
|
21
23
|
// A date stamp rather than a version component: six digits or more (20251001, 250514). The distinction is not
|
|
22
24
|
// cosmetic — claude-opus-4-1-20250805 (Opus 4.1) and claude-opus-4-20250514 (Opus 4.0) compare as (4,1) vs (4)
|
|
@@ -48,7 +50,7 @@ export interface ModelRelease {
|
|
|
48
50
|
export const releaseOf = (id: string): ModelRelease => {
|
|
49
51
|
const numeric = segmentsOf(id)
|
|
50
52
|
.filter((segment) => VERSION_SEGMENT.test(segment))
|
|
51
|
-
.map((segment) => segment.replace(/^
|
|
53
|
+
.map((segment) => segment.replace(/^[vk]/i, ""));
|
|
52
54
|
const stamps = numeric.filter((segment) => DATE_SEGMENT.test(segment)).map(Number);
|
|
53
55
|
return {
|
|
54
56
|
version: numeric
|
|
@@ -109,12 +111,21 @@ const TIER_RANK: Readonly<Record<string, number>> = {
|
|
|
109
111
|
|
|
110
112
|
const UNRANKED = -1;
|
|
111
113
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
114
|
+
/* Some providers name a capability ladder INSIDE one release instead of using the cross-release adjectives
|
|
115
|
+
* above. Codex 5.6's Sol/Terra/Luna rows are that shape: they must remain together ahead of the older 5.5 line,
|
|
116
|
+
* but their order is not an arbitrary id tiebreak — Sol is the strongest, followed by Terra, then Luna. Keeping
|
|
117
|
+
* this as a separate rank lets release recency still win across generations (a future GPT 5.7 base model must
|
|
118
|
+
* not be buried under a recognized 5.6 suffix), while the three siblings sort by their real tier. */
|
|
119
|
+
const RELEASE_TIER_RANK: Readonly<Record<string, number>> = {
|
|
120
|
+
sol: 0,
|
|
121
|
+
terra: 1,
|
|
122
|
+
luna: 2,
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const lastRankOf = (family: string, ranks: Readonly<Record<string, number>>): number => {
|
|
115
126
|
let rank = UNRANKED;
|
|
116
127
|
for (const segment of family.split("-")) {
|
|
117
|
-
const found =
|
|
128
|
+
const found = ranks[segment];
|
|
118
129
|
if (found !== undefined) {
|
|
119
130
|
rank = found;
|
|
120
131
|
}
|
|
@@ -122,18 +133,32 @@ export const tierRankOf = (family: string): number => {
|
|
|
122
133
|
return rank;
|
|
123
134
|
};
|
|
124
135
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
136
|
+
const releaseTierRankOf = (family: string): number => lastRankOf(family, RELEASE_TIER_RANK);
|
|
137
|
+
|
|
138
|
+
// The LAST recognized word wins, because tier words compose and the rightmost is the most specific one:
|
|
139
|
+
// gemini-flash-lite is the cheap end of Flash, gpt-codex-max the frontier end of Codex.
|
|
140
|
+
export const tierRankOf = (family: string): number => lastRankOf(family, TIER_RANK);
|
|
141
|
+
|
|
142
|
+
// The canonical order of two model ids: broad tier first, then release, then a tier declared within that release.
|
|
143
|
+
// Hand it straight to Array#toSorted — that sort is stable, so two ids this rule cannot separate keep the order
|
|
144
|
+
// they arrived in (for Claude, the provider's own).
|
|
145
|
+
export const compareModelIds = (left: string, right: string): number => {
|
|
146
|
+
const leftFamily = familyOf(left);
|
|
147
|
+
const rightFamily = familyOf(right);
|
|
148
|
+
return (
|
|
149
|
+
tierRankOf(leftFamily) - tierRankOf(rightFamily) ||
|
|
150
|
+
compareRelease(releaseOf(left), releaseOf(right)) ||
|
|
151
|
+
releaseTierRankOf(leftFamily) - releaseTierRankOf(rightFamily)
|
|
152
|
+
);
|
|
153
|
+
};
|
|
129
154
|
|
|
130
155
|
/* The order for a catalog its endpoint published as a SET — Codex, Gemini, Kimi and Grok, i.e. everything but
|
|
131
156
|
* Anthropic's ranked list. Falling back on arrival order is what the rule above does with a tie, and for a RANKED
|
|
132
157
|
* catalog that is exactly right: the tie is the provider's own opinion, so claude-opus-5 stays ahead of
|
|
133
158
|
* claude-fable-5. For a set there is no opinion to keep, and the header of this file assumed the leftover order
|
|
134
|
-
* was at least alphabetical — it is not. A subscription
|
|
135
|
-
*
|
|
136
|
-
*
|
|
159
|
+
* was at least alphabetical — it is not. A subscription can hand tied rows back in whatever order its registry
|
|
160
|
+
* iterated THIS request, so the tie decides which model a fresh conversation opens on and can flip between
|
|
161
|
+
* catalog refreshes.
|
|
137
162
|
*
|
|
138
163
|
* So a set breaks its own ties on the id. Which sibling that seats first is arbitrary — but it is the same
|
|
139
164
|
* arbitrary answer every refresh, which is the property `default` actually needs. */
|
|
@@ -151,5 +176,12 @@ export const compareUnrankedModelIds = (left: string, right: string): number =>
|
|
|
151
176
|
* not the efficient rung — and the cheap end is only ever a family whose tier word is actually recognized.
|
|
152
177
|
* Falling off the end of a catalog with no efficient tier at all (Kimi publishes none) is then honest: the
|
|
153
178
|
* newest of what it does publish, chosen by the release tiebreak below. */
|
|
154
|
-
export const compareCheapestFirst = (left: string, right: string): number =>
|
|
155
|
-
|
|
179
|
+
export const compareCheapestFirst = (left: string, right: string): number => {
|
|
180
|
+
const leftFamily = familyOf(left);
|
|
181
|
+
const rightFamily = familyOf(right);
|
|
182
|
+
return (
|
|
183
|
+
tierRankOf(rightFamily) - tierRankOf(leftFamily) ||
|
|
184
|
+
compareRelease(releaseOf(left), releaseOf(right)) ||
|
|
185
|
+
releaseTierRankOf(rightFamily) - releaseTierRankOf(leftFamily)
|
|
186
|
+
);
|
|
187
|
+
};
|
package/src/quick-model.test.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { type QuickModelSource, quickModelKey, resolveQuickModel } from "./quick
|
|
|
9
9
|
const CLAUDE: QuickModelSource = { provider: `claude`, ready: true, models: [`claude-opus-5`, `claude-sonnet-5`, `claude-haiku-4-5-20251001`] };
|
|
10
10
|
const GOOGLE: QuickModelSource = { provider: `gemini`, ready: true, models: [`gemini-3-flash`, `gemini-3-flash-lite`, `gemini-3-pro`] };
|
|
11
11
|
const CODEX: QuickModelSource = { provider: `codex`, ready: true, models: [`gpt-5.4-mini`, `gpt-5.6`] };
|
|
12
|
-
const KIMI: QuickModelSource = { provider: `kimi`, ready: true, models: [`kimi-k2
|
|
12
|
+
const KIMI: QuickModelSource = { provider: `kimi`, ready: true, models: [`kimi-k2.6`, `kimi-k2.7-code`, `kimi-k3`] };
|
|
13
13
|
|
|
14
14
|
const offline = (source: QuickModelSource): QuickModelSource => ({ ...source, ready: false });
|
|
15
15
|
|
|
@@ -72,7 +72,7 @@ test("ignores a malformed pin instead of running an empty model id", () => {
|
|
|
72
72
|
|
|
73
73
|
test("serves the newest of a catalog that publishes no cheap tier at all", () => {
|
|
74
74
|
// Kimi names no tier word anywhere. There is no cheaper rung to find, so the newest row is the honest answer.
|
|
75
|
-
expect(resolveQuickModel([KIMI], ``)).toEqual({ provider: `kimi`, model: `kimi-
|
|
75
|
+
expect(resolveQuickModel([KIMI], ``)).toEqual({ provider: `kimi`, model: `kimi-k3` });
|
|
76
76
|
});
|
|
77
77
|
|
|
78
78
|
test("reports nothing when no account is connected, so the button can say so instead of failing on click", () => {
|
package/src/schemas.test.ts
CHANGED
|
@@ -30,6 +30,10 @@ test("a payload from a build that predates a toggle parses, with the new toggle
|
|
|
30
30
|
autoResumeOnLimit: false,
|
|
31
31
|
resumeAfterOutage: true,
|
|
32
32
|
autoResumeOnRestart: true,
|
|
33
|
+
gateCommand: "",
|
|
34
|
+
gateQuietMs: 20_000,
|
|
35
|
+
gateTimeoutMs: 900_000,
|
|
36
|
+
gateAutoFix: true,
|
|
33
37
|
});
|
|
34
38
|
});
|
|
35
39
|
|
|
@@ -65,6 +69,12 @@ test("an empty object is the full default settings object", () => {
|
|
|
65
69
|
// On: a daemon restart is usually intentic's own doing (an image update, an approved environment
|
|
66
70
|
// change), not the user's decision, so the turn it interrupted resumes rather than staying stuck.
|
|
67
71
|
autoResumeOnRestart: true,
|
|
72
|
+
// Empty disables the landing gate until the owner supplies this workspace's verification command.
|
|
73
|
+
gateCommand: "",
|
|
74
|
+
gateQuietMs: 20_000,
|
|
75
|
+
gateTimeoutMs: 900_000,
|
|
76
|
+
// Once a gate is configured, a red verdict wakes one fixer by default.
|
|
77
|
+
gateAutoFix: true,
|
|
68
78
|
});
|
|
69
79
|
});
|
|
70
80
|
|
package/src/schemas.ts
CHANGED
|
@@ -726,6 +726,34 @@ export const SandboxSettingsSchema = z.object({
|
|
|
726
726
|
* OFF still records the interruption: the fleet card reads `interrupted` (see AgentStatusSchema) and an
|
|
727
727
|
* automation's row shows an `interrupted` run — nothing is re-run, but nothing is silently lost either. */
|
|
728
728
|
autoResumeOnRestart: z.boolean().default(true),
|
|
729
|
+
/* THE LANDING GATE — the check command run over the COMPOSITE of landed work, once the fleet goes quiet.
|
|
730
|
+
* Empty ⇒ no gate at all, which is the default: only the owner knows what verifies this workspace, and a
|
|
731
|
+
* guessed command that fails on a fresh clone would read as the gate finding a bug on its first run.
|
|
732
|
+
*
|
|
733
|
+
* Configuring it is the opt-in, which is why there is no separate enable flag to disagree with it. The
|
|
734
|
+
* command runs in the workspace root through `sh -c`, exactly as a terminal would run it (see gate/gate.ts
|
|
735
|
+
* for why this is NOT an automation guard: a suite outlives GUARD_TIMEOUT_MS, and a timed-out guard reads
|
|
736
|
+
* as "skipped" — a silent green over a suite that never finished). */
|
|
737
|
+
gateCommand: z.string().max(500).default(""),
|
|
738
|
+
/* How long after a land the gate waits before running. A landing burst is the case this exists for: five
|
|
739
|
+
* agents finishing within a minute of each other are five lands, and a gate that ran per land would spend
|
|
740
|
+
* five suites to answer about four trees nobody will ever push. Every land re-arms the timer, so the run
|
|
741
|
+
* happens once, on the tree the user is about to review.
|
|
742
|
+
*
|
|
743
|
+
* It counts from the last LAND and nothing else — explicitly not "until the fleet is idle". Agents here run
|
|
744
|
+
* for hours, so a fleet of twenty with one long runner would never present a quiet moment, and a gate that
|
|
745
|
+
* waited for one would only ever fire when clicked (gate/gate.ts). */
|
|
746
|
+
gateQuietMs: z.number().min(0).max(600_000).default(20_000),
|
|
747
|
+
// Ceiling on one gate run, after which the child is killed and the verdict is `failed` with `timedOut`.
|
|
748
|
+
// Never a pass: a suite that did not finish has not said anything about the tree, and the one thing this
|
|
749
|
+
// gate exists to prevent is a green light nobody earned.
|
|
750
|
+
gateTimeoutMs: z.number().min(60_000).max(3_600_000).default(900_000),
|
|
751
|
+
/* Wake a fixer automatically when the gate goes red, instead of only lighting the badge. ON with a
|
|
752
|
+
* configured command, unlike the other unattended-spend toggles (autoResumeOnLimit), and the difference is
|
|
753
|
+
* that the spend here is the POINT: a red gate whose fix waits for the user to notice has moved the CI
|
|
754
|
+
* round-trip into the workspace without removing it from the user's day. One attempt per verdict, so a
|
|
755
|
+
* command that fails for a reason no agent can fix costs one turn, not a loop (gate/gate.ts). */
|
|
756
|
+
gateAutoFix: z.boolean().default(true),
|
|
729
757
|
});
|
|
730
758
|
export type SandboxSettings = z.infer<typeof SandboxSettingsSchema>;
|
|
731
759
|
|
|
@@ -1215,8 +1243,9 @@ export type WorkspaceClassification = z.infer<typeof WorkspaceClassificationSche
|
|
|
1215
1243
|
// within `text` so clients highlight without re-finding the needle.
|
|
1216
1244
|
export const WorkspaceSearchQuerySchema = z.object({
|
|
1217
1245
|
query: z.string().min(2).max(512),
|
|
1218
|
-
// Search verbs only — anchor/git verbs (outline, context, log, who, …) are CLI-only surface.
|
|
1219
|
-
|
|
1246
|
+
// Search verbs only — anchor/git verbs (outline, context, log, who, …) are CLI-only surface. Natural language
|
|
1247
|
+
// has no verb of its own: `q` classifies the query and answers it semantically when the words call for it.
|
|
1248
|
+
mode: z.enum(["q", "find", "files", "def", "refs", "sym", "ast"]).optional(),
|
|
1220
1249
|
includeIgnored: z.stringbool().optional(),
|
|
1221
1250
|
limit: z.coerce.number().int().positive().optional(),
|
|
1222
1251
|
after: z.string().optional(),
|
|
@@ -1244,6 +1273,9 @@ export const WorkspaceSearchFreshnessSchema = z.object({
|
|
|
1244
1273
|
state: z.enum(["fresh", "building", "stale"]),
|
|
1245
1274
|
ageMs: z.number().optional(),
|
|
1246
1275
|
progress: z.number().optional(),
|
|
1276
|
+
// How many files the index has not caught up with, when it is stale. A count is reportable; "stale" alone
|
|
1277
|
+
// reads as a warning about the answer, which it almost never is.
|
|
1278
|
+
behind: z.number().optional(),
|
|
1247
1279
|
});
|
|
1248
1280
|
export type WorkspaceSearchFreshness = z.infer<typeof WorkspaceSearchFreshnessSchema>;
|
|
1249
1281
|
export const WorkspaceSearchResultSchema = z.object({
|
|
@@ -1255,8 +1287,12 @@ export const WorkspaceSearchResultSchema = z.object({
|
|
|
1255
1287
|
truncated: z.boolean(),
|
|
1256
1288
|
cursor: z.string().optional(),
|
|
1257
1289
|
hint: z.string().optional(),
|
|
1258
|
-
// Code-graph neighbors of the top hits (definition anchors +
|
|
1290
|
+
// Code-graph neighbors of the top hits (definition anchors + the strongest caller of each).
|
|
1259
1291
|
related: z.array(z.string()).optional(),
|
|
1292
|
+
// Ranked `path:line` anchors that placed but were NOT shown, best first — the answer often sits at rank 5–13,
|
|
1293
|
+
// behind groups the budget spent itself on. The text surface has always printed this map; a JSON caller could
|
|
1294
|
+
// not see it, so it had to page through `cursor` to learn what the terminal was told up front.
|
|
1295
|
+
candidates: z.array(z.string()).optional(),
|
|
1260
1296
|
// Run provenance for benchmarking: retrieval stages DISABLED this invocation (absent = full pipeline).
|
|
1261
1297
|
features: z.array(z.string()).optional(),
|
|
1262
1298
|
});
|
|
@@ -2118,9 +2154,17 @@ export const CiRunsResponseSchema = z.object({
|
|
|
2118
2154
|
repos: z.array(CiRepoSchema),
|
|
2119
2155
|
// Newest first, across all mapped repos.
|
|
2120
2156
|
runs: z.array(PipelineRunSchema),
|
|
2157
|
+
// When the owner last opened the pipelines view. Rides the runs response so the rail can decide what is
|
|
2158
|
+
// NEW without a second call — a breakage older than this has already been seen and must not badge again.
|
|
2159
|
+
// Absent ⇒ never opened, so everything counts as unseen.
|
|
2160
|
+
seenAt: z.number().optional(),
|
|
2121
2161
|
});
|
|
2122
2162
|
export type CiRunsResponse = z.infer<typeof CiRunsResponseSchema>;
|
|
2123
2163
|
|
|
2164
|
+
// Stamping the view as read hands back the timestamp it wrote, so the client updates without a refetch.
|
|
2165
|
+
export const CiSeenResponseSchema = z.object({ seenAt: z.number() });
|
|
2166
|
+
export type CiSeenResponse = z.infer<typeof CiSeenResponseSchema>;
|
|
2167
|
+
|
|
2124
2168
|
// rerun/cancel/fix address a run by repo + vendor id; the daemon re-resolves repo → project + token per call,
|
|
2125
2169
|
// so a stale card can't act on a project the workspace no longer maps to.
|
|
2126
2170
|
export const CiRunParamSchema = z.object({ repo: z.string(), runId: z.number() });
|
|
@@ -2130,6 +2174,97 @@ export type CiRunParam = z.infer<typeof CiRunParamSchema>;
|
|
|
2130
2174
|
export const CiFixResponseSchema = z.object({ conversationId: z.string() });
|
|
2131
2175
|
export type CiFixResponse = z.infer<typeof CiFixResponseSchema>;
|
|
2132
2176
|
|
|
2177
|
+
/* ---- the landing gate: the workspace's own verdict on the composite of landed work ----
|
|
2178
|
+
*
|
|
2179
|
+
* WHERE THIS SITS, and why it is not one of the four other places it could:
|
|
2180
|
+
*
|
|
2181
|
+
* A fleet of 5-20 agents lands work into the main tree as UNCOMMITTED changes (agents/land.ts), the user
|
|
2182
|
+
* reviews and commits it by parts, pushes, and CI answers minutes later. This gate front-runs that answer by
|
|
2183
|
+
* asking the same question of the same artifact, before the push.
|
|
2184
|
+
*
|
|
2185
|
+
* NOT inside an agent's turn. An isolated worktree's `node_modules` reads as the MAIN checkout's, so a
|
|
2186
|
+
* monorepo's workspace links resolve cross-package imports to /work's sources rather than the worktree's edited
|
|
2187
|
+
* ones (agents/worktrees.ts). A suite run in a worktree therefore tests the agent's edits against everyone
|
|
2188
|
+
* else's UNEDITED siblings: it invents failures that don't exist and passes changes that break on the
|
|
2189
|
+
* composite, and two agents editing one contract each go green alone and red together. The composite is the
|
|
2190
|
+
* only honest artifact, and it exists in exactly one place — the main working tree.
|
|
2191
|
+
*
|
|
2192
|
+
* NOT at commit. The user commits BY PARTS, and a suite reads the worktree, not the index — so a verdict taken
|
|
2193
|
+
* at a partial commit describes a tree that never gets pushed as such. Commit is where a verdict is DISPLAYED
|
|
2194
|
+
* (ReviewPanel's badge), computed earlier.
|
|
2195
|
+
*
|
|
2196
|
+
* NOT at push. By then HEAD has moved, per-path attribution has expired (agents/origins.ts), and the agents may
|
|
2197
|
+
* be archived with their worktrees reclaimed — so the fix starts cold, in the same position `/ci/fix` is in.
|
|
2198
|
+
* That saves the CI round-trip and none of the context switch.
|
|
2199
|
+
*
|
|
2200
|
+
* So: after the land, before the staging — the one window where the artifact is what CI will see, attribution
|
|
2201
|
+
* is still live, and nobody is waiting on it. */
|
|
2202
|
+
|
|
2203
|
+
/* What the gate has to say about the tree right now.
|
|
2204
|
+
*
|
|
2205
|
+
* idle — no command configured, or nothing has run yet.
|
|
2206
|
+
* armed — work landed; the quiet period is counting down (see gateQuietMs).
|
|
2207
|
+
* running — the check is live. `output` grows as it streams.
|
|
2208
|
+
* passed — exited 0 over `fingerprint`.
|
|
2209
|
+
* failed — exited non-zero, or was killed by gateTimeoutMs (`timedOut`). The state a fix answers.
|
|
2210
|
+
* error — the gate itself could not run: the command was not spawnable. NOT a fix-able failure, because
|
|
2211
|
+
* there is nothing wrong with the code — the gate is misconfigured, and saying "tests failed"
|
|
2212
|
+
* would send an agent hunting a bug that isn't there.
|
|
2213
|
+
* cancelled — the user stopped the run, or the tree moved under it.
|
|
2214
|
+
*/
|
|
2215
|
+
export const GateStatusSchema = z.enum(["idle", "armed", "running", "passed", "failed", "error", "cancelled"]);
|
|
2216
|
+
export type GateStatus = z.infer<typeof GateStatusSchema>;
|
|
2217
|
+
|
|
2218
|
+
// An agent whose landed work the failure implicates. `paths` are its attributed files that the check's own
|
|
2219
|
+
// output NAMED — empty when the output named none of them, which is the honest shape for a failure that could
|
|
2220
|
+
// not be pinpointed (an integration break between two deltas, a suite that prints no paths at all): the agent
|
|
2221
|
+
// is listed because its work is in the tree under test, not because anything accused it.
|
|
2222
|
+
export const GateAgentSchema = z.object({
|
|
2223
|
+
agentId: z.string(),
|
|
2224
|
+
title: z.string().optional(),
|
|
2225
|
+
provider: AgentProviderSchema.optional(),
|
|
2226
|
+
paths: z.array(z.string()),
|
|
2227
|
+
});
|
|
2228
|
+
export type GateAgent = z.infer<typeof GateAgentSchema>;
|
|
2229
|
+
|
|
2230
|
+
// The fix turn one red verdict got. A MAIN-TREE turn, not an isolated conversation, so there is no
|
|
2231
|
+
// conversationId and no fleet card to open — the composite it must reproduce lives in the main working tree and
|
|
2232
|
+
// a fresh worktree branches from HEAD without it. `sessionId` is what makes the run readable after the fact,
|
|
2233
|
+
// the same thing an automation's run record carries for the same reason.
|
|
2234
|
+
export const GateFixSchema = z.object({
|
|
2235
|
+
startedAt: z.number(),
|
|
2236
|
+
sessionId: z.string().optional(),
|
|
2237
|
+
// `running` while the turn streams; `done` when it ended cleanly, whatever the re-check then said;
|
|
2238
|
+
// `error` when the turn itself failed (a provider outage, no credential), which is worth distinguishing
|
|
2239
|
+
// because it is the one case where re-running the fix could still help.
|
|
2240
|
+
outcome: z.enum(["running", "done", "error"]),
|
|
2241
|
+
detail: z.string().optional(),
|
|
2242
|
+
});
|
|
2243
|
+
export type GateFix = z.infer<typeof GateFixSchema>;
|
|
2244
|
+
|
|
2245
|
+
export const GateVerdictSchema = z.object({
|
|
2246
|
+
status: GateStatusSchema,
|
|
2247
|
+
// The command this verdict ran, echoed rather than read back from settings: a verdict read after the
|
|
2248
|
+
// setting changed still has to say what produced it.
|
|
2249
|
+
command: z.string(),
|
|
2250
|
+
startedAt: z.number().optional(),
|
|
2251
|
+
finishedAt: z.number().optional(),
|
|
2252
|
+
exitCode: z.number().optional(),
|
|
2253
|
+
timedOut: z.boolean().optional(),
|
|
2254
|
+
// The check's own output, tail-capped (GATE_OUTPUT_BYTES). The tail, not the head: a suite's verdict and
|
|
2255
|
+
// its failure summary are at the end, and a head-capped buffer of a chatty build is all progress lines.
|
|
2256
|
+
output: z.string(),
|
|
2257
|
+
/* WHICH TREE this verdict is about — HEAD plus the shape of every repo's uncommitted content. Recomputed
|
|
2258
|
+
* on read: when it no longer matches, the verdict is `stale` and the badge says so instead of asserting a
|
|
2259
|
+
* green light over a tree that has since moved. This is what keeps a passed verdict from outliving its
|
|
2260
|
+
* subject when the user edits, discards, or commits half of it. */
|
|
2261
|
+
fingerprint: z.string(),
|
|
2262
|
+
stale: z.boolean(),
|
|
2263
|
+
implicated: z.array(GateAgentSchema),
|
|
2264
|
+
fix: GateFixSchema.optional(),
|
|
2265
|
+
});
|
|
2266
|
+
export type GateVerdict = z.infer<typeof GateVerdictSchema>;
|
|
2267
|
+
|
|
2133
2268
|
// ---- drafts: agent-proposed posts awaiting owner approval (.intentic/drafts/<id>.json) ----
|
|
2134
2269
|
// One JSON file per draft. The AGENT creates drafts with its normal file tools — it can't call daemon routes,
|
|
2135
2270
|
// the same split as the environment proposal — while the daemon edits/deletes them on the owner's behalf, so
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/* THE NAMES OF THE SESSIONS ONE TURN OPENS.
|
|
2
|
+
*
|
|
3
|
+
* A turn that runs Bash gets a tmux session; a turn that browses gets a Chromium the daemon watches over CDP.
|
|
4
|
+
* Both are named off the SDK session id by the same derivation, and both are listed together by
|
|
5
|
+
* GET /system/terminals — so the name is wire vocabulary, not an implementation detail of either half.
|
|
6
|
+
*
|
|
7
|
+
* It lives in the contract because three parties derive it and none of them may guess: the daemon NAMES the
|
|
8
|
+
* session, the web app OPENS it, and an extension that wants to point the user at a turn's live browser has only
|
|
9
|
+
* the fleet roster's `sessionId` to work from. A second copy of this string math anywhere is a drift waiting to
|
|
10
|
+
* happen — the copy would keep resolving to a plausible name that nothing has ever listed.
|
|
11
|
+
*
|
|
12
|
+
* Reached as `@intentic/sandbox-contract/session-names`, NOT through the barrel — the same shape tunnel-ids has,
|
|
13
|
+
* and for the same reason: this module has no dependencies, while the barrel pulls the whole contract (and
|
|
14
|
+
* @intentic/extension-api behind it). One subpath keeps a daemon unit test that only wants a session name from
|
|
15
|
+
* having to resolve the entire wire surface.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
export const WEB_SESSION_PREFIX = "web-";
|
|
19
|
+
export const AGENT_SESSION_PREFIX = "agent-";
|
|
20
|
+
export const JOB_SESSION_PREFIX = "job-";
|
|
21
|
+
export const BROWSER_SESSION_PREFIX = "browser-";
|
|
22
|
+
|
|
23
|
+
/* Eight characters of the SDK session UUID, sanitized to the session-name charset. Eight is what groups a whole
|
|
24
|
+
* turn's work — including its subagents' — under one name, and it clears the name guard
|
|
25
|
+
* (`^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$`) without further escaping. Undefined when the id sanitizes to empty, which
|
|
26
|
+
* is never a valid session name. */
|
|
27
|
+
export const sessionSuffix = (sessionId: string): string | undefined => {
|
|
28
|
+
const id = sessionId.replace(/[^A-Za-z0-9_-]/g, "").slice(0, 8);
|
|
29
|
+
return id === "" ? undefined : id;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// The tmux session one SDK session's Bash commands run in — the same derivation the Bash hook routes commands
|
|
33
|
+
// through, so an emitted `terminal` frame and the live session can't drift.
|
|
34
|
+
export const agentSessionName = (sessionId: string): string | undefined => {
|
|
35
|
+
const suffix = sessionSuffix(sessionId);
|
|
36
|
+
return suffix === undefined ? undefined : `${AGENT_SESSION_PREFIX}${suffix}`;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// The browser session one SDK session drives. Shares the suffix with agentSessionName on purpose: a
|
|
40
|
+
// conversation's shell and its browser read as the pair they are.
|
|
41
|
+
export const browserSessionName = (sessionId: string): string | undefined => {
|
|
42
|
+
const suffix = sessionSuffix(sessionId);
|
|
43
|
+
return suffix === undefined ? undefined : `${BROWSER_SESSION_PREFIX}${suffix}`;
|
|
44
|
+
};
|