@intentic/sandbox-contract 1.360.0 → 1.362.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 +9 -0
- package/dist/contracts/agent.contract.d.ts.map +1 -1
- package/dist/contracts/agents.contract.d.ts +21 -0
- package/dist/contracts/agents.contract.d.ts.map +1 -1
- package/dist/contracts/automations.contract.d.ts +2 -0
- package/dist/contracts/automations.contract.d.ts.map +1 -1
- package/dist/contracts/needs.contract.d.ts +7 -0
- package/dist/contracts/needs.contract.d.ts.map +1 -1
- package/dist/contracts/runner.contract.d.ts +3 -0
- package/dist/contracts/runner.contract.d.ts.map +1 -1
- package/dist/contracts/sessions.contract.d.ts +1 -0
- package/dist/contracts/sessions.contract.d.ts.map +1 -1
- package/dist/contracts/system.contract.d.ts +33 -30
- package/dist/contracts/system.contract.d.ts.map +1 -1
- package/dist/events/agent-events.d.ts +4 -0
- package/dist/events/agent-events.d.ts.map +1 -1
- package/dist/events/agent-transcript.d.ts +1 -0
- package/dist/events/agent-transcript.d.ts.map +1 -1
- package/dist/events/land-conflict.d.ts +11 -0
- package/dist/events/land-conflict.d.ts.map +1 -1
- package/dist/events/land-conflict.js +64 -0
- package/dist/events/land-conflict.js.map +1 -1
- package/dist/events/system-events.d.ts +2 -0
- package/dist/events/system-events.d.ts.map +1 -1
- package/dist/events/transcript.d.ts +5 -0
- package/dist/events/transcript.d.ts.map +1 -1
- package/dist/index.d.ts +43 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/models/agent-catalog.d.ts.map +1 -1
- package/dist/models/agent-catalog.js +1 -0
- package/dist/models/agent-catalog.js.map +1 -1
- package/dist/models/agent-lanes.d.ts +8 -3
- package/dist/models/agent-lanes.d.ts.map +1 -1
- package/dist/models/agent-lanes.js +16 -3
- package/dist/models/agent-lanes.js.map +1 -1
- package/dist/models/agent-runtimes.d.ts +7 -1
- package/dist/models/agent-runtimes.d.ts.map +1 -1
- package/dist/models/agent-runtimes.js +17 -8
- package/dist/models/agent-runtimes.js.map +1 -1
- package/dist/models/agent-stack.d.ts +12 -0
- package/dist/models/agent-stack.d.ts.map +1 -0
- package/dist/models/agent-stack.js +19 -0
- package/dist/models/agent-stack.js.map +1 -0
- package/dist/models/module-of.d.ts +3 -0
- package/dist/models/module-of.d.ts.map +1 -0
- package/dist/models/module-of.js +13 -0
- package/dist/models/module-of.js.map +1 -0
- package/dist/models/provider-specs.d.ts +2 -0
- package/dist/models/provider-specs.d.ts.map +1 -1
- package/dist/models/provider-specs.js +2 -0
- package/dist/models/provider-specs.js.map +1 -1
- package/dist/policy/persona-capability.d.ts.map +1 -1
- package/dist/policy/persona-capability.js +2 -1
- package/dist/policy/persona-capability.js.map +1 -1
- package/dist/schemas/agents-list.d.ts +2 -0
- package/dist/schemas/agents-list.d.ts.map +1 -1
- package/dist/schemas/agents.d.ts +3 -0
- package/dist/schemas/agents.d.ts.map +1 -1
- package/dist/schemas/agents.js +5 -1
- package/dist/schemas/agents.js.map +1 -1
- package/dist/schemas/automation-events.d.ts +40 -0
- package/dist/schemas/automation-events.d.ts.map +1 -0
- package/dist/schemas/automation-events.js +20 -0
- package/dist/schemas/automation-events.js.map +1 -0
- package/dist/schemas/automations.d.ts +4 -38
- package/dist/schemas/automations.d.ts.map +1 -1
- package/dist/schemas/automations.js +8 -18
- package/dist/schemas/automations.js.map +1 -1
- package/dist/schemas/device-commands.d.ts.map +1 -1
- package/dist/schemas/device-commands.js.map +1 -1
- package/dist/schemas/hosts.d.ts.map +1 -1
- package/dist/schemas/hosts.js.map +1 -1
- package/dist/schemas/needs.d.ts +8 -0
- package/dist/schemas/needs.d.ts.map +1 -1
- package/dist/schemas/organisation.d.ts.map +1 -1
- package/dist/schemas/organisation.js.map +1 -1
- package/dist/schemas/personas.d.ts +13 -0
- package/dist/schemas/personas.d.ts.map +1 -1
- package/dist/schemas/personas.js +9 -0
- package/dist/schemas/personas.js.map +1 -1
- package/dist/schemas/providers/plan-limits.js +2 -2
- package/dist/schemas/providers/plan-limits.js.map +1 -1
- package/dist/schemas/repo-checks.d.ts +84 -0
- package/dist/schemas/repo-checks.d.ts.map +1 -0
- package/dist/schemas/repo-checks.js +50 -0
- package/dist/schemas/repo-checks.js.map +1 -0
- package/dist/schemas/run-targets.d.ts.map +1 -1
- package/dist/schemas/run-targets.js.map +1 -1
- package/dist/schemas/settings.d.ts +2 -82
- package/dist/schemas/settings.d.ts.map +1 -1
- package/dist/schemas/settings.js +7 -49
- package/dist/schemas/settings.js.map +1 -1
- package/dist/state/daemon-floor.d.ts.map +1 -1
- package/dist/state/daemon-floor.js.map +1 -1
- package/dist/text/preview-guide.d.ts.map +1 -1
- package/dist/text/preview-guide.js.map +1 -1
- package/package.json +13 -13
- package/src/events/land-conflict.test.ts +23 -0
- package/src/events/land-conflict.ts +104 -3
- package/src/models/agent-catalog.test.ts +10 -7
- package/src/models/agent-catalog.ts +1 -0
- package/src/models/agent-lanes.ts +37 -12
- package/src/models/agent-runtimes.ts +50 -14
- package/src/models/agent-stack.test.ts +86 -0
- package/src/models/agent-stack.ts +48 -0
- package/src/models/capability-ledger.test.ts +4 -1
- package/src/models/module-of.ts +20 -0
- package/src/models/provider-specs.test.ts +6 -2
- package/src/models/provider-specs.ts +7 -0
- package/src/policy/persona-capability.ts +10 -3
- package/src/schemas/agents.ts +8 -1
- package/src/schemas/automation-events.ts +35 -0
- package/src/schemas/automations.ts +12 -33
- package/src/schemas/device-commands.ts +5 -17
- package/src/schemas/hosts.ts +4 -27
- package/src/schemas/organisation.ts +4 -16
- package/src/schemas/personas.ts +27 -0
- package/src/schemas/providers/plan-limits.ts +3 -3
- package/src/schemas/repo-checks.ts +81 -0
- package/src/schemas/run-targets.ts +3 -19
- package/src/schemas/settings.ts +9 -88
- package/src/state/daemon-floor.ts +4 -15
- package/src/text/preview-guide.ts +4 -16
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { AgentAttention } from "../schemas/agents.js";
|
|
2
2
|
import type { AutomationApproval } from "../schemas/automations.js";
|
|
3
3
|
import { goesBySelf } from "../schemas/agent-next.js";
|
|
4
|
+
import { automationOf, movedAt, type StackMember, stackHeads } from "./agent-stack.js";
|
|
4
5
|
import { parentOf } from "./agent-starter.js";
|
|
5
6
|
import { type AgentStanding, blocked, heardByParent, onlyOwnerCanAnswer, turnInFlight, turnWorking, watching } from "./agent-standing.js";
|
|
6
7
|
|
|
@@ -24,6 +25,15 @@ export const callsOwner = (
|
|
|
24
25
|
onlyOwnerCanAnswer(child) ||
|
|
25
26
|
(family.child === "attention" && (family.parent !== "active" || child.fresh !== undefined) && !heardByParent(child, family.parentAt));
|
|
26
27
|
|
|
28
|
+
// Whether an automation's earlier run, riding the card of its latest (agent-stack.ts), calls the reader through that card.
|
|
29
|
+
// Nothing supervises a run, so unlike a child's stop (callsOwner) its stop is nobody else's news: what only the reader can
|
|
30
|
+
// give always calls, and any other stop calls until a later run of the same automation has moved since (`headAt`, its
|
|
31
|
+
// movedAt), which is the automation's own account of whether the trouble still stands: a run that failed at 02:30 behind
|
|
32
|
+
// one that went through at 02:40 is history in the tray, and one that failed behind a run failing the same way is news on
|
|
33
|
+
// the card already.
|
|
34
|
+
export const runCallsOwner = (run: AgentStanding & { readonly updatedAt: number }, stack: { readonly run: FleetLane; readonly headAt: number }): boolean =>
|
|
35
|
+
onlyOwnerCanAnswer(run) || (stack.run === "attention" && !heardByParent(run, stack.headAt));
|
|
36
|
+
|
|
27
37
|
export type FleetLane = "attention" | "active" | "finished";
|
|
28
38
|
|
|
29
39
|
// Nothing owed to the user. The one "no attention" block in the app: a client-only card has no daemon account of
|
|
@@ -80,18 +90,18 @@ export const laneOf = (agent: AgentStanding): FleetLane => {
|
|
|
80
90
|
// How many cards the board's Attention lane draws for one fleet, as the board folds child agents under their parents
|
|
81
91
|
// (models/fleet/board/childFold.ts): an agent in Attention is a card of its own, except a child agent whose parent is in the
|
|
82
92
|
// fleet, which rides under its family's one card and lifts that card to Attention only when it calls the reader
|
|
83
|
-
// (callsOwner).
|
|
84
|
-
//
|
|
85
|
-
//
|
|
93
|
+
// (callsOwner). An automation's earlier runs ride the card of its latest the same way (agent-stack.ts, runCallsOwner). A
|
|
94
|
+
// family counts once however many of its children call, and not again when its own card is in Attention already; a
|
|
95
|
+
// child with words left in its composer keeps a card of its own, as on the board. The rail badge's count, for this
|
|
96
|
+
// sandbox's fleet and another's alike, so the badge cannot disagree with the lane it names.
|
|
86
97
|
export const attentionCards = (agents: readonly CallerStanding[]): number => new Set(attentionCalls(agents).map((call) => call.card)).size;
|
|
87
98
|
|
|
88
99
|
// Enough of a fleet's agent to say whether it calls the reader, and through which card.
|
|
89
|
-
export type CallerStanding = AgentStanding &
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
};
|
|
100
|
+
export type CallerStanding = AgentStanding &
|
|
101
|
+
StackMember & {
|
|
102
|
+
readonly startedBy?: string | undefined;
|
|
103
|
+
readonly unsent?: boolean | undefined;
|
|
104
|
+
};
|
|
95
105
|
|
|
96
106
|
// Every agent calling the reader, each with the card it is drawn on: its own, or its family's where it rides one. The
|
|
97
107
|
// count above is the cards; the browser tab's chime wants the callers, so a second child calling under a family already
|
|
@@ -103,12 +113,22 @@ export const attentionCalls = (agents: readonly CallerStanding[]): readonly { re
|
|
|
103
113
|
const id = parentOf(agent.startedBy);
|
|
104
114
|
return id === undefined ? undefined : byId.get(id);
|
|
105
115
|
};
|
|
116
|
+
// A conversation another one started rides its parent, never a stack, whether or not that parent is in this fleet.
|
|
117
|
+
const runOf = (agent: Standing): string | undefined => (parentOf(agent.startedBy) === undefined ? automationOf(agent) : undefined);
|
|
118
|
+
const heads = stackHeads(agents, runOf);
|
|
119
|
+
// The latest run of this one's automation, whose card it rides; undefined for that latest run itself.
|
|
120
|
+
const headIn = (agent: Standing): Standing | undefined => {
|
|
121
|
+
const automation = runOf(agent);
|
|
122
|
+
const head = automation === undefined ? undefined : heads.get(automation);
|
|
123
|
+
return head === agent ? undefined : head;
|
|
124
|
+
};
|
|
125
|
+
const aboveIn = (agent: Standing): Standing | undefined => parentIn(agent) ?? headIn(agent);
|
|
106
126
|
// The card a child rides under: its eldest ancestor in this fleet, walked with a guard against a record naming its
|
|
107
127
|
// own descendant as its parent.
|
|
108
128
|
const familyOf = (agent: Standing): string => {
|
|
109
129
|
const seen = new Set([agent.id]);
|
|
110
130
|
let top = agent;
|
|
111
|
-
for (let above =
|
|
131
|
+
for (let above = aboveIn(top); above !== undefined && !seen.has(above.id); above = aboveIn(top)) {
|
|
112
132
|
seen.add(above.id);
|
|
113
133
|
top = above;
|
|
114
134
|
}
|
|
@@ -117,13 +137,18 @@ export const attentionCalls = (agents: readonly CallerStanding[]): readonly { re
|
|
|
117
137
|
const calls: { readonly id: string; readonly card: string }[] = [];
|
|
118
138
|
for (const agent of agents) {
|
|
119
139
|
const parent = parentIn(agent);
|
|
140
|
+
const above = parent ?? headIn(agent);
|
|
120
141
|
// Words left in a child's composer give it a card of its own (childFold.standsAlone), except a fresh session's:
|
|
121
142
|
// its composer is the person's anyway, and it rides its origin's card whatever it holds.
|
|
122
|
-
if (
|
|
143
|
+
if (above === undefined || (agent.unsent === true && agent.fresh === undefined)) {
|
|
123
144
|
if (laneOf(agent) === "attention") {
|
|
124
145
|
calls.push({ id: agent.id, card: agent.id });
|
|
125
146
|
}
|
|
126
|
-
} else if (
|
|
147
|
+
} else if (
|
|
148
|
+
parent === undefined
|
|
149
|
+
? runCallsOwner(agent, { run: laneOf(agent), headAt: movedAt(above) })
|
|
150
|
+
: callsOwner(agent, { child: laneOf(agent), parent: laneOf(parent), parentAt: parent.updatedAt })
|
|
151
|
+
) {
|
|
127
152
|
calls.push({ id: agent.id, card: familyOf(agent) });
|
|
128
153
|
}
|
|
129
154
|
}
|
|
@@ -6,6 +6,16 @@
|
|
|
6
6
|
// that grants it.
|
|
7
7
|
export type ExecutionBackend = "shell" | "js";
|
|
8
8
|
|
|
9
|
+
// Which origins of skills a runtime's loader reads by itself (AgentCapabilities.skillDiscovery).
|
|
10
|
+
export interface SkillDiscovery {
|
|
11
|
+
readonly workspace: boolean;
|
|
12
|
+
readonly builtIn: boolean;
|
|
13
|
+
readonly kit: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const EVERY_SKILL: SkillDiscovery = { workspace: true, builtIn: true, kit: true };
|
|
17
|
+
const NO_SKILL_LOADER: SkillDiscovery = { workspace: false, builtIn: false, kit: false };
|
|
18
|
+
|
|
9
19
|
export interface AgentCapabilities {
|
|
10
20
|
// Which loop serves the turn; `opencode-gemini` stays separate since adapter health keys off this field.
|
|
11
21
|
readonly runtime: "claude-code" | "codex" | "opencode" | "opencode-gemini" | "acp" | "devin" | "kimi-cli" | "grok-build" | "pi" | "cursor" | "muse";
|
|
@@ -41,8 +51,12 @@ export interface AgentCapabilities {
|
|
|
41
51
|
readonly warm: boolean;
|
|
42
52
|
// How much of the owner's instructions this runtime takes: replace the base, append to it, or no seam at all.
|
|
43
53
|
readonly instructions: "replace" | "append" | "none";
|
|
44
|
-
//
|
|
45
|
-
|
|
54
|
+
// Which of a turn's skills the runtime's own loader finds; the rest are named, with their paths, in the note a fresh
|
|
55
|
+
// session opens with (the sandbox's agent/run/decide/turn-skills.ts), less the ones the turn may not use:
|
|
56
|
+
// workspace - the workspace's loaded skills (.agents/skills, or the .claude/skills projection of them);
|
|
57
|
+
// builtIn - the product's own skills the image bakes (/root/.claude/skills);
|
|
58
|
+
// kit - the worn persona's own skills, which the daemon hands the loader per turn.
|
|
59
|
+
readonly skillDiscovery: SkillDiscovery;
|
|
46
60
|
// Whether the owner's safety policy reaches this runtime, and how:
|
|
47
61
|
// hooks - its own pre-execution hook can park a call and wait for a card (Claude Code only).
|
|
48
62
|
// approval - a vendor approval channel the daemon answers from policy; the vendor picks which calls to raise.
|
|
@@ -69,6 +83,12 @@ export interface AgentCapabilities {
|
|
|
69
83
|
// every tool call the loop makes passes a check the daemon answers, or the loop is built with only the listed tools.
|
|
70
84
|
// A runtime that runs some tool without asking anyone cannot keep it, so a turn carrying a list never starts on one.
|
|
71
85
|
readonly toolAllowlist: boolean;
|
|
86
|
+
// When the owner's per-edit checks (the `file.edited` rules, the byte scan) reach the model: `after-edit` where the
|
|
87
|
+
// loop's own hook answers each edit or write as it lands (Claude Code's PostToolUse, Codex's and Pi's gate), so the
|
|
88
|
+
// model reads them beside the call; `turn-end` where nothing stands after a call, so the daemon reads the files the
|
|
89
|
+
// turn's edit calls touched as it ends and says the findings back as one more prompt, beside the repositories' turn
|
|
90
|
+
// checks (runtimes/decorators/ending-checks.ts).
|
|
91
|
+
readonly editChecks: "after-edit" | "turn-end";
|
|
72
92
|
}
|
|
73
93
|
|
|
74
94
|
// The Claude Code Agent SDK loop, the ceiling every other runtime is measured against: the only one that owns the whole
|
|
@@ -89,7 +109,7 @@ export const CLAUDE_CODE: AgentCapabilities = {
|
|
|
89
109
|
recovery: true,
|
|
90
110
|
warm: true,
|
|
91
111
|
instructions: "replace",
|
|
92
|
-
skillDiscovery:
|
|
112
|
+
skillDiscovery: EVERY_SKILL,
|
|
93
113
|
// The only runtime with its own pre-execution hook, so the only one where a hold can park instead of refusing.
|
|
94
114
|
rulebook: "hooks",
|
|
95
115
|
secrets: "masked",
|
|
@@ -98,6 +118,7 @@ export const CLAUDE_CODE: AgentCapabilities = {
|
|
|
98
118
|
// A PreToolUse hook refuses every call off the list, under bypassPermissions and in subagents too
|
|
99
119
|
// (agent/tools/tool-allowlist.ts).
|
|
100
120
|
toolAllowlist: true,
|
|
121
|
+
editChecks: "after-edit",
|
|
101
122
|
};
|
|
102
123
|
|
|
103
124
|
// Codex app-server: item-level events, the turn's remote MCP servers and the daemon's own over http, per-thread hooks
|
|
@@ -131,7 +152,8 @@ export const CODEX: AgentCapabilities = {
|
|
|
131
152
|
// thread/start's own fields: baseInstructions replaces Codex's base prompt, developerInstructions adds a developer
|
|
132
153
|
// message.
|
|
133
154
|
instructions: "replace",
|
|
134
|
-
|
|
155
|
+
// Codex reads the workspace's `.agents/skills` itself; the built-ins and a persona's kit are named in the note.
|
|
156
|
+
skillDiscovery: { workspace: true, builtIn: false, kit: false },
|
|
135
157
|
// A per-thread PreToolUse hook puts every function tool (the shell, apply_patch, every MCP tool, spawn_agent) to the
|
|
136
158
|
// door's gate before it runs, which may park it on a card (codex-hooks.ts). Codex fails a hook open, so the gate is
|
|
137
159
|
// confirmed connected before the turn starts, a command or patch Codex asks approval for runs only if the gate
|
|
@@ -139,7 +161,7 @@ export const CODEX: AgentCapabilities = {
|
|
|
139
161
|
// connect the gate, so there the rules reach only the commands and patches Codex asks about.
|
|
140
162
|
rulebook: "hooks",
|
|
141
163
|
// Shell commands resolve `{{secret:…}}` in the gate's rewrite and run under a wrapper that masks stored values in
|
|
142
|
-
// their output (
|
|
164
|
+
// their output (mask-script.ts); apply_patch, view_image and hosted tools pass no wrapper.
|
|
143
165
|
secrets: "shell",
|
|
144
166
|
// Its model provider's base_url is the daemon's, written into the per-thread config.
|
|
145
167
|
privacy: "gateway",
|
|
@@ -148,6 +170,7 @@ export const CODEX: AgentCapabilities = {
|
|
|
148
170
|
// too unless listed, and each MCP server is cut to its listed tools (`enabled_tools`). The residual: a known-safe
|
|
149
171
|
// read-only command off the list runs if the in-process gate fails after it connected, and the turn is then stopped.
|
|
150
172
|
toolAllowlist: true,
|
|
173
|
+
editChecks: "after-edit",
|
|
151
174
|
};
|
|
152
175
|
|
|
153
176
|
// OpenCode (the Grok runtime): its own agentic loop and tools, with the turn's remote MCP servers mounted on the shared
|
|
@@ -196,7 +219,9 @@ export const OPENCODE: AgentCapabilities = {
|
|
|
196
219
|
// A prompt that replaces the base (the owner's own under `custom`, a small window's paragraph) runs on the daemon's
|
|
197
220
|
// twin agents, whose base is blank, so it is the whole system text; anything else is added to OpenCode's own base.
|
|
198
221
|
instructions: "replace",
|
|
199
|
-
|
|
222
|
+
// The server's skill roots are the workspace's loaded skills and the built-ins (opencode.ts serverConfig); a persona's
|
|
223
|
+
// kit, which changes per turn on a server shared by every turn, is named in the note.
|
|
224
|
+
skillDiscovery: { workspace: true, builtIn: true, kit: false },
|
|
200
225
|
// OpenCode raises its permission asks for every shell command, a subagent's included (the server's own rules ask about
|
|
201
226
|
// them all, and a child session inherits them); the daemon answers from the rulebook, a hold parked on a card while
|
|
202
227
|
// the turn's inactivity watchdog is held. The JS backend consults the same gate. Not `hooks`: which calls ask is still
|
|
@@ -213,6 +238,7 @@ export const OPENCODE: AgentCapabilities = {
|
|
|
213
238
|
// session inherits them. Every built-in tool has its own permission action (checked against 2.0.26), and a shell entry
|
|
214
239
|
// narrowed to a command is checked against each command's ask (runtimes/opencode/allowlist/opencode-allowlist.ts).
|
|
215
240
|
toolAllowlist: true,
|
|
241
|
+
editChecks: "turn-end",
|
|
216
242
|
};
|
|
217
243
|
|
|
218
244
|
// The same OpenCode loop, serving Gemini instead of xAI, identical abilities. The Claude Code loop announces itself in
|
|
@@ -252,13 +278,14 @@ export const ACP: AgentCapabilities = {
|
|
|
252
278
|
isolation: "cwd",
|
|
253
279
|
commands: true,
|
|
254
280
|
terminals: true,
|
|
255
|
-
// A failure the agent words as a spent allowance or an outgrown window is coded
|
|
256
|
-
//
|
|
257
|
-
|
|
281
|
+
// A failure the agent words as a spent allowance or an outgrown window is coded at the turn's one exit
|
|
282
|
+
// (error-frames.ts codedFailure), and an agent gone silent mid-turn is coded an outage by the shared watchdog
|
|
283
|
+
// (turn-watchdog.ts expiredFrame), so a turn the provider dropped is re-run once it is back, as every loop's is.
|
|
284
|
+
recovery: true,
|
|
258
285
|
warm: false,
|
|
259
286
|
// ACP's calls carry no system field: the agent owns its instructions, the persona note rides the message.
|
|
260
287
|
instructions: "none",
|
|
261
|
-
skillDiscovery:
|
|
288
|
+
skillDiscovery: NO_SKILL_LOADER,
|
|
262
289
|
// In the protocol floor, so every agent has it; which calls it asks about is entirely its own choice. A terminal the
|
|
263
290
|
// daemon runs for it (terminal/create) is asked about here too, unless the permission for it already was.
|
|
264
291
|
rulebook: "approval",
|
|
@@ -267,6 +294,7 @@ export const ACP: AgentCapabilities = {
|
|
|
267
294
|
privacy: "none",
|
|
268
295
|
// Which calls the agent asks permission for is its own choice, and most run reads without asking.
|
|
269
296
|
toolAllowlist: false,
|
|
297
|
+
editChecks: "turn-end",
|
|
270
298
|
};
|
|
271
299
|
|
|
272
300
|
// Devin's own CLI (`devin acp`), the ACP floor with one seam an installed agent lacks: the daemon picks the model, from
|
|
@@ -339,7 +367,8 @@ export const PI: AgentCapabilities = {
|
|
|
339
367
|
// keeps Pi from loading AGENTS.md a second time beside the daemon's own note.
|
|
340
368
|
instructions: "replace",
|
|
341
369
|
// `--skill` per loaded skill, the persona's filter applied, with Pi's own discovery off (`--no-skills`).
|
|
342
|
-
|
|
370
|
+
// Pi's own discovery is off, and every skill the turn may use is handed to it by path (`--skill`, pi-adapter.ts).
|
|
371
|
+
skillDiscovery: EVERY_SKILL,
|
|
343
372
|
// The daemon's extension, loaded with `-e` ahead of any other and whatever the card's command says of discovery, puts
|
|
344
373
|
// every tool call (MCP and codemode calls included) to the turn's own hook socket before it runs, where a held one
|
|
345
374
|
// parks on a card while Pi waits (pi-hooks.ts). A call it cannot ask about is refused, as a handler that throws is,
|
|
@@ -356,6 +385,7 @@ export const PI: AgentCapabilities = {
|
|
|
356
385
|
// The same socket refuses every call off the turn's list, Pi's tools read under the Claude loop's names (bash as
|
|
357
386
|
// Bash, find as Glob) and MCP tools under the same `mcp__<server>__<tool>`; fail closed as above.
|
|
358
387
|
toolAllowlist: true,
|
|
388
|
+
editChecks: "after-edit",
|
|
359
389
|
};
|
|
360
390
|
|
|
361
391
|
// Cursor's own runtime, driven through `@cursor/sdk` from this daemon's process; an embedding surface, so most seams
|
|
@@ -372,7 +402,9 @@ export const CURSOR: AgentCapabilities = {
|
|
|
372
402
|
permissions: "modes",
|
|
373
403
|
// True since the daemon supplies its own ask tool; Cursor's own can fabricate an answer, so it's disallowed.
|
|
374
404
|
questions: true,
|
|
375
|
-
// The turn's remote MCP servers
|
|
405
|
+
// The turn's remote MCP servers, the daemon's own door servers among them (accounts, secrets, watch, deps,
|
|
406
|
+
// diagnostics), plus host callbacks for the three it runs in the daemon's process instead (ask, code, the subagent
|
|
407
|
+
// tools: cursor-tools.ts); everything but a Claude Code plugin checkout.
|
|
376
408
|
mcp: "tools",
|
|
377
409
|
// The JS backend rides Cursor's custom-tool seam rather than an MCP server, and consults the rulebook in its
|
|
378
410
|
// handler, since the hook file only covers the shell.
|
|
@@ -401,7 +433,9 @@ export const CURSOR: AgentCapabilities = {
|
|
|
401
433
|
// Cursor's own loader reads every SKILL.md under `.agents/skills/` of each workspace root it is given: the cwd holds
|
|
402
434
|
// the loaded skills, and the worn persona's kit is laid out as a second root (`local.dirs`); a skill the persona
|
|
403
435
|
// withholds is refused at the read tool, the seam the Claude loop's `Skill(<name>)` denial is (cursor-skills.ts).
|
|
404
|
-
|
|
436
|
+
// The workspace's skills from the turn's own tree and the persona's kit as a projected root (cursor-skills.ts); the
|
|
437
|
+
// built-ins, outside every root it is given, are named in the note.
|
|
438
|
+
skillDiscovery: { workspace: true, builtIn: false, kit: true },
|
|
405
439
|
// The full hook tier, the only foreign runtime to reach it: a hold parks since the vendor waits on the hook.
|
|
406
440
|
rulebook: "hooks",
|
|
407
441
|
// Every shell command runs through the hook script: a `{{secret:name}}` reference resolves into what the script runs once
|
|
@@ -421,6 +455,7 @@ export const CURSOR: AgentCapabilities = {
|
|
|
421
455
|
// cannot place in a turn. A turn carrying a list does not start unless those hooks are installed, and a hook the
|
|
422
456
|
// daemon cannot answer refuses a live turn's call.
|
|
423
457
|
toolAllowlist: true,
|
|
458
|
+
editChecks: "turn-end",
|
|
424
459
|
};
|
|
425
460
|
|
|
426
461
|
// Muse Code, Meta's coding agent, driven over the Muse Session Protocol: one `muse serve` host per turn, born in the turn's
|
|
@@ -460,7 +495,7 @@ export const MUSE: AgentCapabilities = {
|
|
|
460
495
|
instructions: "none",
|
|
461
496
|
// Muse loads a trusted workspace's own skills from its own directories, which are not the daemon's; the daemon names
|
|
462
497
|
// the loaded ones in the opener.
|
|
463
|
-
skillDiscovery:
|
|
498
|
+
skillDiscovery: NO_SKILL_LOADER,
|
|
464
499
|
// Muse's approval channel, answered from the rulebook; which commands Muse runs without asking (its own allow rules)
|
|
465
500
|
// is its choice.
|
|
466
501
|
rulebook: "approval",
|
|
@@ -469,4 +504,5 @@ export const MUSE: AgentCapabilities = {
|
|
|
469
504
|
privacy: "none",
|
|
470
505
|
// Edits and reads run without asking anyone, so no answer the daemon gives can keep a call off a list.
|
|
471
506
|
toolAllowlist: false,
|
|
507
|
+
editChecks: "turn-end",
|
|
472
508
|
};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { AgentStatus } from "../schemas/agents.js";
|
|
2
|
+
import { attentionCalls, attentionCards } from "./agent-lanes.js";
|
|
3
|
+
import { automationOf, stackHeads } from "./agent-stack.js";
|
|
4
|
+
|
|
5
|
+
const none = { plan: false, question: false, permission: false, capability: false, credential: false, conflict: false };
|
|
6
|
+
const LANDED: AgentStatus = `landed`;
|
|
7
|
+
const ERROR: AgentStatus = `error`;
|
|
8
|
+
|
|
9
|
+
type Over = { status?: AgentStatus; updatedAt?: number; startedAt?: number; startedBy?: string; failureCode?: string; attention?: typeof none };
|
|
10
|
+
|
|
11
|
+
// One run of the automation `x-follow`, as the daemon now records it, settled at `updatedAt`.
|
|
12
|
+
const run = (id: string, over: Over = {}) => ({
|
|
13
|
+
id,
|
|
14
|
+
status: LANDED,
|
|
15
|
+
attention: none,
|
|
16
|
+
updatedAt: 0,
|
|
17
|
+
origin: { automationId: `x-follow`, provider: `schedule` },
|
|
18
|
+
...over,
|
|
19
|
+
});
|
|
20
|
+
const person = (id: string, over: Over = {}) => ({ id, status: LANDED, attention: none, updatedAt: 0, ...over });
|
|
21
|
+
|
|
22
|
+
describe("automationOf", () => {
|
|
23
|
+
test("names the automation from the origin, else from the id the daemon minted the run under", () => {
|
|
24
|
+
expect(automationOf({ id: `anything`, origin: { automationId: `x-follow` } })).toBe(`x-follow`);
|
|
25
|
+
// Runs a schedule fired before origins were recorded: `a-<automation>-<base36 stamp>`.
|
|
26
|
+
expect(automationOf({ id: `a-x-follow-mvn5x1jtu` })).toBe(`x-follow`);
|
|
27
|
+
expect(automationOf({ id: `a-nightly-mvn5x1j16` })).toBe(`nightly`);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("names none for a conversation a person started", () => {
|
|
31
|
+
expect(automationOf({ id: `witty-harbor-pknz` })).toBeUndefined();
|
|
32
|
+
expect(automationOf({ id: `home-1a2b3c4d5e6f7a8b` })).toBeUndefined();
|
|
33
|
+
expect(automationOf({ id: `a-` })).toBeUndefined();
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe("stackHeads", () => {
|
|
38
|
+
test("picks each automation's latest run, a running turn by its start so two at work never trade places", () => {
|
|
39
|
+
const heads = stackHeads(
|
|
40
|
+
[run(`r1`, { updatedAt: 100 }), run(`r2`, { updatedAt: 300 }), run(`r3`, { status: `running`, startedAt: 200, updatedAt: 900 })],
|
|
41
|
+
automationOf,
|
|
42
|
+
);
|
|
43
|
+
expect(heads.get(`x-follow`)?.id).toBe(`r2`);
|
|
44
|
+
const working = stackHeads([run(`old`, { status: `running`, startedAt: 100, updatedAt: 990 }), run(`new`, { status: `running`, startedAt: 200, updatedAt: 210 })], automationOf);
|
|
45
|
+
expect(working.get(`x-follow`)?.id).toBe(`new`);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("keeps out whatever the key names no automation for", () => {
|
|
49
|
+
expect(stackHeads([person(`p`, { updatedAt: 5 })], automationOf).size).toBe(0);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// The rail badge counts an automation's runs as the board draws them: one card, its latest run's.
|
|
54
|
+
describe("attentionCards over an automation's runs", () => {
|
|
55
|
+
const failed = { status: ERROR };
|
|
56
|
+
|
|
57
|
+
test("counts nothing for earlier failures a later run has moved past", () => {
|
|
58
|
+
const fleet = [run(`r1`, { ...failed, updatedAt: 100 }), run(`r2`, { ...failed, updatedAt: 200 }), run(`r3`, { updatedAt: 300 })];
|
|
59
|
+
expect(attentionCards(fleet)).toBe(0);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("counts the stack once when its latest run failed, however many before it did", () => {
|
|
63
|
+
const fleet = [run(`r1`, { ...failed, updatedAt: 100 }), run(`r2`, { ...failed, updatedAt: 200 }), run(`r3`, { ...failed, updatedAt: 300 })];
|
|
64
|
+
expect(attentionCards(fleet)).toBe(1);
|
|
65
|
+
expect(attentionCalls(fleet).map((call) => call.card)).toEqual([`r3`]);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("counts an earlier run asking what only the reader can give, through the stack's card", () => {
|
|
69
|
+
const fleet = [run(`r1`, { status: `awaiting`, attention: { ...none, credential: true }, updatedAt: 100 }), run(`r2`, { updatedAt: 300 })];
|
|
70
|
+
expect(attentionCalls(fleet)).toEqual([{ id: `r1`, card: `r2` }]);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("keeps a person's own work out of any stack", () => {
|
|
74
|
+
const fleet = [run(`r1`, { updatedAt: 300 }), person(`mine`, { ...failed, updatedAt: 100 })];
|
|
75
|
+
expect(attentionCalls(fleet)).toEqual([{ id: `mine`, card: `mine` }]);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("lets a run's own child ride its parent run, and so the stack", () => {
|
|
79
|
+
const fleet = [
|
|
80
|
+
run(`r1`, { updatedAt: 100 }),
|
|
81
|
+
run(`r2`, { updatedAt: 300 }),
|
|
82
|
+
person(`helper`, { startedBy: `agent:r1`, status: `awaiting`, attention: { ...none, permission: true }, updatedAt: 150 }),
|
|
83
|
+
];
|
|
84
|
+
expect(attentionCalls(fleet)).toEqual([{ id: `helper`, card: `r2` }]);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// ONE AUTOMATION, ONE CARD. Every fire of an automation that starts an agent opens a conversation of its own, so an
|
|
2
|
+
// automation on a ten-minute clock fills the board with a card per run, each named afresh by the naming pass, until a
|
|
3
|
+
// person's own work is lost among a hundred cards telling one story. So the runs of one automation ride under one card,
|
|
4
|
+
// as a spawned child rides under its parent (agent-lanes.ts attentionCalls, and the editor's childFold.ts, which draws
|
|
5
|
+
// it): the card of its latest run, with the earlier runs as rows in its tray. Which card that is, and which runs ride
|
|
6
|
+
// it, is decided here once, so the board and the daemon's count of what needs the person (`agents.attention`) agree.
|
|
7
|
+
|
|
8
|
+
// Enough of a conversation to say which automation's run it is and when it last moved; a FleetAgent, a roster
|
|
9
|
+
// AgentSummary and a test literal all fit.
|
|
10
|
+
export interface StackMember {
|
|
11
|
+
readonly id: string;
|
|
12
|
+
readonly origin?: { readonly automationId: string } | undefined;
|
|
13
|
+
readonly startedAt?: number | undefined;
|
|
14
|
+
readonly updatedAt: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// How the daemon names a conversation an automation minted (automations/fire/fire-sessions.ts mintConversationId): `a-`,
|
|
18
|
+
// the automation's id cut to 40 characters, `-`, then a base36 stamp with no `-` in it. Read only for a run with no
|
|
19
|
+
// origin, which is every run a schedule fired before the daemon recorded one, so the board stacks those too.
|
|
20
|
+
const MINTED = /^a-(.+)-[0-9a-z]+$/;
|
|
21
|
+
|
|
22
|
+
// The automation whose run this conversation is, or undefined for one a person or another agent started.
|
|
23
|
+
export const automationOf = (agent: Pick<StackMember, "id" | "origin">): string | undefined =>
|
|
24
|
+
agent.origin?.automationId ?? MINTED.exec(agent.id)?.[1];
|
|
25
|
+
|
|
26
|
+
// When a run last moved, as a stack orders its runs: a running turn's start, which holds still for the turn's life so
|
|
27
|
+
// two runs at work at once never trade places on every tick, else its last move.
|
|
28
|
+
export const movedAt = (run: Pick<StackMember, "startedAt" | "updatedAt">): number => run.startedAt ?? run.updatedAt;
|
|
29
|
+
|
|
30
|
+
// Whether `a` is the later of two runs; the id breaks a tie, so every surface picks the same one.
|
|
31
|
+
const later = (a: StackMember, b: StackMember): boolean => movedAt(a) > movedAt(b) || (movedAt(a) === movedAt(b) && a.id > b.id);
|
|
32
|
+
|
|
33
|
+
// The latest run of each automation among `agents`, by the key `stackOf` gives it (the automation, and on a board that
|
|
34
|
+
// holds several sandboxes' cards, the sandbox too); undefined skips a conversation that is no automation's run.
|
|
35
|
+
export const stackHeads = <T extends StackMember>(agents: Iterable<T>, stackOf: (agent: T) => string | undefined): ReadonlyMap<string, T> => {
|
|
36
|
+
const heads = new Map<string, T>();
|
|
37
|
+
for (const agent of agents) {
|
|
38
|
+
const key = stackOf(agent);
|
|
39
|
+
if (key === undefined) {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
const head = heads.get(key);
|
|
43
|
+
if (head === undefined || later(agent, head)) {
|
|
44
|
+
heads.set(key, agent);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return heads;
|
|
48
|
+
};
|
|
@@ -23,7 +23,7 @@ const LEDGER: Record<keyof AgentCapabilities, Backing> = {
|
|
|
23
23
|
isolation: "enforced",
|
|
24
24
|
// system-prompt.ts composes standing instructions against this: replace, append, or a user-message note.
|
|
25
25
|
instructions: "enforced",
|
|
26
|
-
// turn-
|
|
26
|
+
// agent/run/decide/turn-skills.ts names in the opening note only the skills whose origin the loader does not read.
|
|
27
27
|
skillDiscovery: "enforced",
|
|
28
28
|
// turn-plan.ts plans jsExecution only where "js" is declared; without it no Code tool is mounted.
|
|
29
29
|
execution: "enforced",
|
|
@@ -41,6 +41,9 @@ const LEDGER: Record<keyof AgentCapabilities, Backing> = {
|
|
|
41
41
|
// automation ladder passes such rungs over, and agent/tools/tool-allowlist.ts refuses every off-list call on the
|
|
42
42
|
// Claude Code loop.
|
|
43
43
|
toolAllowlist: "enforced",
|
|
44
|
+
// runtimes/decorators/ending-checks.ts reads a "turn-end" loop's edited files as its turn ends and says the
|
|
45
|
+
// owner's per-edit checks back with the repositories' turn checks; an "after-edit" loop's own hook said them.
|
|
46
|
+
editChecks: "enforced",
|
|
44
47
|
|
|
45
48
|
// Descriptive: true of the runtime but nothing consults these; wiring one up moves it to enforced.
|
|
46
49
|
questions: "descriptive",
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { WorkspaceModule } from "../schemas/git/git.js";
|
|
2
|
+
|
|
3
|
+
// Which package a repository-relative path lives in, the one rule every reader buckets files by: the review panels
|
|
4
|
+
// group a change list with it, and the sandbox names the packages a conversation's work falls in for its card, so the
|
|
5
|
+
// two can never put one file in different packages.
|
|
6
|
+
|
|
7
|
+
// Longest matching module dir wins, so a nested package claims its own files before its parent.
|
|
8
|
+
// dir "" claims what nothing deeper claims; the trailing separator keeps `_apps/webx` from matching `_editor/web`.
|
|
9
|
+
export const moduleOf = (path: string, modules: readonly WorkspaceModule[]): WorkspaceModule | undefined => {
|
|
10
|
+
let best: WorkspaceModule | undefined;
|
|
11
|
+
for (const candidate of modules) {
|
|
12
|
+
if (candidate.dir !== `` && !path.startsWith(`${candidate.dir}/`)) {
|
|
13
|
+
continue;
|
|
14
|
+
}
|
|
15
|
+
if (best === undefined || candidate.dir.length > best.dir.length) {
|
|
16
|
+
best = candidate;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return best;
|
|
20
|
+
};
|
|
@@ -155,9 +155,13 @@ test("cursor holds every mode, a tool list, the terminal panel and its own skill
|
|
|
155
155
|
const cursor = capabilitiesOf("cursor", "native");
|
|
156
156
|
expect(cursor.permissions).toBe("modes");
|
|
157
157
|
expect(cursor.terminals).toBe(true);
|
|
158
|
-
expect(cursor.skillDiscovery).
|
|
158
|
+
expect(cursor.skillDiscovery).toEqual({ workspace: true, builtIn: false, kit: true });
|
|
159
159
|
expect(cursor.toolAllowlist).toBe(true);
|
|
160
|
-
expect(limitationsOf(cursor)).toEqual([
|
|
160
|
+
expect(limitationsOf(cursor)).toEqual([
|
|
161
|
+
"no plugins",
|
|
162
|
+
"secrets masked in shell output only, not in files the agent reads",
|
|
163
|
+
"edit checks said as the turn ends, not after each edit",
|
|
164
|
+
]);
|
|
161
165
|
});
|
|
162
166
|
|
|
163
167
|
// Devin runs the ACP floor under its own name: what the floor gained (approvals, questions) it has too, and the effort
|
|
@@ -421,6 +421,13 @@ export const MINTED_PROVIDERS: readonly MintedProvider[] = PROVIDER_SPECS.filter
|
|
|
421
421
|
(spec): spec is Extract<Spec, { auth: { kind: "minted" } }> => spec.auth.kind === "minted",
|
|
422
422
|
).map((spec) => spec.id);
|
|
423
423
|
|
|
424
|
+
// OpenCode's id for Gemini through the translator; not "google", OpenCode's own API-key provider, which bypasses the fleet.
|
|
425
|
+
export const OPENCODE_GEMINI_PROVIDER = "intentic-gemini";
|
|
426
|
+
|
|
427
|
+
// The provider an id names everywhere outside OpenCode's own config. A model running there sees the alias above as its
|
|
428
|
+
// provider and repeats it when it names one (a subagent's spawn or move), so the doors that take such a name read it here.
|
|
429
|
+
export const canonicalProvider = (provider: string): string => (provider === OPENCODE_GEMINI_PROVIDER ? "gemini" : provider);
|
|
430
|
+
|
|
424
431
|
// This provider's CLIProxyAPI id, where it has one; not always ours (grok/xai, gemini/antigravity).
|
|
425
432
|
export const cliProxyIdOf = (provider: string): string | undefined => {
|
|
426
433
|
const auth = providerSpec(provider)?.auth;
|
|
@@ -18,8 +18,13 @@ export interface PersonaShelves {
|
|
|
18
18
|
readonly mcp?: readonly string[] | undefined;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
/**
|
|
22
|
-
|
|
21
|
+
/**
|
|
22
|
+
* The kinds that are someone signed in: a site account, the identity whose browser holds every account born of it, and
|
|
23
|
+
* the owner's own browser (its extension), which holds every session they have. The last one was missing until
|
|
24
|
+
* 2026-10-25, and it fell through to "passes untouched": an X-only persona's unattended run reached the owner's Brave,
|
|
25
|
+
* and an unpinned wake that lost every sandbox account kept it.
|
|
26
|
+
*/
|
|
27
|
+
export const isSignedInAccount = (kind: string): boolean => kind === "browser" || kind === "identity" || kind === "webext";
|
|
23
28
|
|
|
24
29
|
const listed = (list: readonly string[] | undefined, id: string): boolean => list === undefined || list.includes(id);
|
|
25
30
|
|
|
@@ -31,9 +36,11 @@ const listed = (list: readonly string[] | undefined, id: string): boolean => lis
|
|
|
31
36
|
export const personaAllowsCapability = (capability: CapabilityRef, accounts: readonly string[], shelves: PersonaShelves): boolean => {
|
|
32
37
|
switch (capability.kind) {
|
|
33
38
|
// The signed-in browsers: the persona's own `capabilities` list, and the field that predates the shelves. Granting
|
|
34
|
-
// an account (`reddit-work`) grants its hands; granting the identity itself grants the someone.
|
|
39
|
+
// an account (`reddit-work`) grants its hands; granting the identity itself grants the someone. The owner's own
|
|
40
|
+
// browser is named the same way: a persona reaches it only when its list says so.
|
|
35
41
|
case "browser":
|
|
36
42
|
case "identity":
|
|
43
|
+
case "webext":
|
|
37
44
|
return accounts.includes(capability.id);
|
|
38
45
|
// Connector credentials reach the shell; an ungranted id keeps its env vars out of the turn entirely.
|
|
39
46
|
case "cli":
|
package/src/schemas/agents.ts
CHANGED
|
@@ -247,7 +247,7 @@ export const AgentSummarySchema = z.object({
|
|
|
247
247
|
.string()
|
|
248
248
|
.optional()
|
|
249
249
|
.describe(
|
|
250
|
-
"One word for the kind of work the naming pass read in it (fix, audit, redesign). Never part of the displayed name
|
|
250
|
+
"One word for the kind of work the naming pass read in it (fix, audit, redesign). Never part of the displayed name. Absent for a title nothing named an action for.",
|
|
251
251
|
),
|
|
252
252
|
status: AgentStatusSchema.describe(
|
|
253
253
|
"What it is doing. Stopping and stopped are the two halves of somebody pressing stop, because a cancel is not instant; dismissing is the same window for a question waved away, which ends the turn too but owes the user nothing; resuming means the sandbox is already putting right whatever killed the turn; landing means its work is being carried into the workspace right now, and nothing may act on its branch until that settles.",
|
|
@@ -554,6 +554,13 @@ export const AgentSummarySchema = z.object({
|
|
|
554
554
|
})
|
|
555
555
|
.optional()
|
|
556
556
|
.describe("Everything it has written, measured from where it started. Independent of how much has been merged."),
|
|
557
|
+
// Read off the same changes `diff` counts, so the two can't disagree.
|
|
558
|
+
modules: z
|
|
559
|
+
.array(z.string())
|
|
560
|
+
.optional()
|
|
561
|
+
.describe(
|
|
562
|
+
"The packages its changes fall in, most-changed first, at most three: each the folder name of the nearest manifest (package.json, Cargo.toml, pyproject.toml, go.mod) above a changed file, or the repository's name for a file outside every package. Absent until it has changed something.",
|
|
563
|
+
),
|
|
557
564
|
// How much of what this agent landed is still in the working tree, since a discard after landing is invisible to
|
|
558
565
|
// commit-based readings; absent is the steady state and carries no line.
|
|
559
566
|
landedPresence: z
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
// What the daemon emits as the fleet works, for a code chore to react to; no token or route, since only the daemon
|
|
4
|
+
// reads these.
|
|
5
|
+
// turn.settled: fires once per isolated turn regardless of outcome, while the diff is still on screen
|
|
6
|
+
// agent.landed: fires only once work reaches the main tree, including a later manual Land
|
|
7
|
+
// deps.broken/deps.fixed: retired with the check that ran after every land (2026-09-27). Nothing emits them any more;
|
|
8
|
+
// they stay so an automation written for them still reads, and it never fires. Main's failing CI is the `ci` listener's.
|
|
9
|
+
export const WorkspaceEventKindSchema = z.enum(["turn.settled", "agent.landed", "deps.broken", "deps.fixed"]);
|
|
10
|
+
export type WorkspaceEventKind = z.infer<typeof WorkspaceEventKindSchema>;
|
|
11
|
+
// Payload of a workspace-triggered wake, delivered as JSON in $AUTOMATION_PAYLOAD and appended to the prompt.
|
|
12
|
+
// `repos[].from` is each repo's state before the turn; the span runs to the working tree, not a commit, so an errored
|
|
13
|
+
// turn's uncommitted work still shows.
|
|
14
|
+
export const WorkspaceEventSchema = z.object({
|
|
15
|
+
event: WorkspaceEventKindSchema,
|
|
16
|
+
agentId: z.string(),
|
|
17
|
+
title: z.string().optional(),
|
|
18
|
+
branch: z.string(),
|
|
19
|
+
// `ready` is a clean turn held on the branch (auto-land off): the moment right before a deliberate Land, when a
|
|
20
|
+
// pre-land review wants to run.
|
|
21
|
+
outcome: z.enum(["landed", "conflict", "ready", "idle", "error"]),
|
|
22
|
+
repos: z.array(z.object({ repo: z.string(), from: z.string(), dir: z.string() })),
|
|
23
|
+
// Present only for `deps.*` events: which project broke, the command and exit code, a bounded log tail, and
|
|
24
|
+
// `attempt` counting consecutive failures since the last pass.
|
|
25
|
+
deps: z
|
|
26
|
+
.object({
|
|
27
|
+
project: z.string(),
|
|
28
|
+
command: z.string(),
|
|
29
|
+
exitCode: z.number(),
|
|
30
|
+
attempt: z.number(),
|
|
31
|
+
logTail: z.string(),
|
|
32
|
+
})
|
|
33
|
+
.optional(),
|
|
34
|
+
});
|
|
35
|
+
export type WorkspaceEvent = z.infer<typeof WorkspaceEventSchema>;
|