@intentic/sandbox-contract 1.359.2 → 1.361.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 +10 -0
- package/dist/events/land-conflict.d.ts.map +1 -1
- package/dist/events/land-conflict.js +47 -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.ts +78 -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
|
@@ -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>;
|
|
@@ -4,6 +4,8 @@ import { AgentOriginSchema, ModelPinSchema } from "./agent.js";
|
|
|
4
4
|
import { entryId } from "./internal.js";
|
|
5
5
|
import { IssuesConfigSchema } from "./issues.js";
|
|
6
6
|
import { ZoneSchema } from "../time/zone.js";
|
|
7
|
+
import { WorkspaceEventKindSchema } from "./automation-events.js";
|
|
8
|
+
export { WorkspaceEventKindSchema, WorkspaceEventSchema, type WorkspaceEventKind, type WorkspaceEvent } from "./automation-events.js";
|
|
7
9
|
// An automation wakes the agent: the daemon fires each enabled one on its trigger, runs the optional guard command
|
|
8
10
|
// (non-zero exit skips the wake), then runs one turn with the prompt. The manifest is user config; run history is
|
|
9
11
|
// daemon-recorded.
|
|
@@ -23,39 +25,6 @@ import { ZoneSchema } from "../time/zone.js";
|
|
|
23
25
|
// pushes one branch per agent
|
|
24
26
|
// workspace: fires from the sandbox's own codebase (WorkspaceEventKindSchema)
|
|
25
27
|
|
|
26
|
-
// What the daemon emits as the fleet works, for a code chore to react to; no token or route, since only the daemon
|
|
27
|
-
// reads these.
|
|
28
|
-
// turn.settled: fires once per isolated turn regardless of outcome, while the diff is still on screen
|
|
29
|
-
// agent.landed: fires only once work reaches the main tree, including a later manual Land
|
|
30
|
-
// deps.broken/deps.fixed: retired with the check that ran after every land (2026-09-27). Nothing emits them any more;
|
|
31
|
-
// they stay so an automation written for them still reads, and it never fires. Main's failing CI is the `ci` listener's.
|
|
32
|
-
export const WorkspaceEventKindSchema = z.enum(["turn.settled", "agent.landed", "deps.broken", "deps.fixed"]);
|
|
33
|
-
export type WorkspaceEventKind = z.infer<typeof WorkspaceEventKindSchema>;
|
|
34
|
-
// Payload of a workspace-triggered wake, delivered as JSON in $AUTOMATION_PAYLOAD and appended to the prompt.
|
|
35
|
-
// `repos[].from` is each repo's state before the turn; the span runs to the working tree, not a commit, so an errored
|
|
36
|
-
// turn's uncommitted work still shows.
|
|
37
|
-
export const WorkspaceEventSchema = z.object({
|
|
38
|
-
event: WorkspaceEventKindSchema,
|
|
39
|
-
agentId: z.string(),
|
|
40
|
-
title: z.string().optional(),
|
|
41
|
-
branch: z.string(),
|
|
42
|
-
// `ready` is a clean turn held on the branch (auto-land off): the moment right before a deliberate Land, when a
|
|
43
|
-
// pre-land review wants to run.
|
|
44
|
-
outcome: z.enum(["landed", "conflict", "ready", "idle", "error"]),
|
|
45
|
-
repos: z.array(z.object({ repo: z.string(), from: z.string(), dir: z.string() })),
|
|
46
|
-
// Present only for `deps.*` events: which project broke, the command and exit code, a bounded log tail, and
|
|
47
|
-
// `attempt` counting consecutive failures since the last pass.
|
|
48
|
-
deps: z
|
|
49
|
-
.object({
|
|
50
|
-
project: z.string(),
|
|
51
|
-
command: z.string(),
|
|
52
|
-
exitCode: z.number(),
|
|
53
|
-
attempt: z.number(),
|
|
54
|
-
logTail: z.string(),
|
|
55
|
-
})
|
|
56
|
-
.optional(),
|
|
57
|
-
});
|
|
58
|
-
export type WorkspaceEvent = z.infer<typeof WorkspaceEventSchema>;
|
|
59
28
|
export const TriggerSchema = z.discriminatedUnion("kind", [
|
|
60
29
|
z.object({
|
|
61
30
|
kind: z.literal("schedule").describe("On a clock."),
|
|
@@ -446,6 +415,16 @@ export const AutomationSchema = z.object({
|
|
|
446
415
|
.boolean()
|
|
447
416
|
.optional()
|
|
448
417
|
.describe("This automation is a maintenance job, which is what files it under chores rather than among ordinary automations."),
|
|
418
|
+
// Pure filing, like `chore`: the daemon never reads it. Absent, the panel files it by its persona instead, so a set
|
|
419
|
+
// of jobs one role runs reads as one shelf without anybody naming it.
|
|
420
|
+
group: z
|
|
421
|
+
.string()
|
|
422
|
+
.min(1)
|
|
423
|
+
.max(40)
|
|
424
|
+
.optional()
|
|
425
|
+
.describe(
|
|
426
|
+
"Which shelf of the Automations panel it is filed on, like 'Marketing' or 'Releases'. Automations naming the same group are listed together. Absent files it under its persona, or with the code chores.",
|
|
427
|
+
),
|
|
449
428
|
enabled: z.boolean().describe("Whether it fires at all."),
|
|
450
429
|
});
|
|
451
430
|
export type Automation = z.infer<typeof AutomationSchema>;
|
|
@@ -1,21 +1,9 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
|
|
3
|
-
// Closed
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// `dev-restart`, `dev-rebuild` and `sync-install` are the three whose argv the daemon fills from what only it knows — the
|
|
8
|
-
// dev checkout on that machine, a freshly minted pairing token — which is also why none takes a path or a token from the
|
|
9
|
-
// caller. The two dev commands are also why a machine-side op is the wrong shape for them: this door carries a COMMAND
|
|
10
|
-
// STRING to a tool every released agent already has, so a sandbox can drive a machine whose agent predates the feature.
|
|
11
|
-
// `dev-rebuild-log` is the read side of `dev-rebuild`: the build is detached out there, so its log is the only place its
|
|
12
|
-
// progress exists, and polling this is what turns a fired-and-forgotten command into something with a progress bar.
|
|
13
|
-
// `sync-clean` removes the build output left in directories the sandbox has deleted, which is what stops those
|
|
14
|
-
// deletions from ever landing. It deletes only content the session already ignores, so it is the one command here whose
|
|
15
|
-
// worst outcome is a rebuild — which is why it is a button and not a turn.
|
|
16
|
-
// `mirror-ignore`/`mirror-unignore` are `mirror-off`'s per-port form, and the reason they exist: a port number that is
|
|
17
|
-
// permanently taken on one machine's localhost has no answer in an all-or-nothing switch, so the choice lives where the
|
|
18
|
-
// conflict does — one port, one device, durable — instead of muting every port the pairing serves.
|
|
3
|
+
// Closed names let the daemon build argv without granting arbitrary shell access; dev/sync commands never accept caller paths or tokens.
|
|
4
|
+
// Use existing run_command for older-agent compatibility; poll dev-rebuild-log because rebuilds are detached.
|
|
5
|
+
// sync-unpair terminates sessions/revokes enrollment; sync-clean removes ignored build residue only.
|
|
6
|
+
// mirror-ignore/unignore persists per-device port choices without muting all mirrored ports.
|
|
19
7
|
export const DeviceCommandSchema = z.enum([
|
|
20
8
|
"mirror-off",
|
|
21
9
|
"mirror-on",
|
|
@@ -55,7 +43,7 @@ export const DeviceLocalDirSchema = z
|
|
|
55
43
|
.string()
|
|
56
44
|
.min(1)
|
|
57
45
|
.max(4096)
|
|
58
|
-
// The curly quotes as escapes: bun's transpiler rewrites a literal one in a regex to its escape and
|
|
46
|
+
// The curly quotes as escapes: bun's transpiler rewrites a literal one in a regex to its escape and tsc keeps it,
|
|
59
47
|
// so spelled literally the pattern reads differently from source and from dist, and contract.lock.json can match
|
|
60
48
|
// only one of them.
|
|
61
49
|
.regex(/^(?:~|\/|[A-Za-z]:[\\/])[^"'`$;|&\n\r\u201C\u201D\u201E\u2018\u2019\u201A\u201B]*$/);
|