@intentic/sandbox-contract 1.169.0 → 1.171.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/agent-catalog.d.ts +1 -0
- package/dist/agent-catalog.d.ts.map +1 -1
- package/dist/agent-catalog.js +2 -1
- package/dist/agent-catalog.js.map +1 -1
- package/dist/chores/chores.d.ts +45 -0
- package/dist/chores/chores.d.ts.map +1 -0
- package/dist/chores/chores.js +487 -0
- package/dist/chores/chores.js.map +1 -0
- package/dist/chores/digest.d.ts +3 -0
- package/dist/chores/digest.d.ts.map +1 -0
- package/dist/chores/digest.js +0 -0
- package/dist/chores/digest.js.map +1 -0
- package/dist/chores/index.d.ts +10 -0
- package/dist/chores/index.d.ts.map +1 -0
- package/dist/chores/index.js +6 -0
- package/dist/chores/index.js.map +1 -0
- package/dist/chores/probes.d.ts +15 -0
- package/dist/chores/probes.d.ts.map +1 -0
- package/dist/chores/probes.js +177 -0
- package/dist/chores/probes.js.map +1 -0
- package/dist/chores/prompt.d.ts +14 -0
- package/dist/chores/prompt.d.ts.map +1 -0
- package/dist/chores/prompt.js +12 -0
- package/dist/chores/prompt.js.map +1 -0
- package/dist/chores/verdict.d.ts +20 -0
- package/dist/chores/verdict.d.ts.map +1 -0
- package/dist/chores/verdict.js +59 -0
- package/dist/chores/verdict.js.map +1 -0
- package/dist/contracts/agent.contract.d.ts +22 -0
- package/dist/contracts/agent.contract.d.ts.map +1 -1
- package/dist/contracts/agent.contract.js +2 -1
- package/dist/contracts/agent.contract.js.map +1 -1
- package/dist/contracts/agents.contract.d.ts +2 -0
- package/dist/contracts/agents.contract.d.ts.map +1 -1
- package/dist/contracts/automations.contract.d.ts +54 -0
- package/dist/contracts/automations.contract.d.ts.map +1 -1
- package/dist/contracts/chores.contract.d.ts +151 -0
- package/dist/contracts/chores.contract.d.ts.map +1 -0
- package/dist/contracts/chores.contract.js +8 -0
- package/dist/contracts/chores.contract.js.map +1 -0
- package/dist/contracts/extensions.contract.d.ts +7 -0
- package/dist/contracts/extensions.contract.d.ts.map +1 -1
- package/dist/contracts/extensions.contract.js +2 -1
- package/dist/contracts/extensions.contract.js.map +1 -1
- package/dist/contracts/system.contract.d.ts +14 -14
- package/dist/contracts/workspace.contract.d.ts +11 -24
- package/dist/contracts/workspace.contract.d.ts.map +1 -1
- package/dist/events.d.ts +10 -0
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +7 -1
- package/dist/events.js.map +1 -1
- package/dist/hostnames.d.ts +0 -1
- package/dist/hostnames.d.ts.map +1 -1
- package/dist/hostnames.js +0 -1
- package/dist/hostnames.js.map +1 -1
- package/dist/index.d.ts +261 -38
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/schemas.d.ts +618 -40
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +145 -2
- package/dist/schemas.js.map +1 -1
- package/dist/tunnel-ids.d.ts +2 -0
- package/dist/tunnel-ids.d.ts.map +1 -1
- package/dist/tunnel-ids.js +2 -0
- package/dist/tunnel-ids.js.map +1 -1
- package/dist/workspace-state.d.ts.map +1 -1
- package/dist/workspace-state.js +9 -0
- package/dist/workspace-state.js.map +1 -1
- package/package.json +14 -2
- package/src/agent-catalog.test.ts +32 -1
- package/src/agent-catalog.ts +25 -7
- package/src/chores/chores.ts +837 -0
- package/src/chores/digest.test.ts +30 -0
- package/src/chores/digest.ts +0 -0
- package/src/chores/index.ts +9 -0
- package/src/chores/probes.test.ts +166 -0
- package/src/chores/probes.ts +273 -0
- package/src/chores/prompt.ts +64 -0
- package/src/chores/verdict.test.ts +394 -0
- package/src/chores/verdict.ts +167 -0
- package/src/contracts/agent.contract.ts +14 -1
- package/src/contracts/chores.contract.ts +23 -0
- package/src/contracts/extensions.contract.ts +5 -0
- package/src/events.ts +15 -2
- package/src/hostnames.ts +4 -6
- package/src/index.ts +3 -0
- package/src/schemas.ts +427 -19
- package/src/tunnel-ids.test.ts +49 -0
- package/src/tunnel-ids.ts +24 -0
- package/src/workspace-state.ts +13 -0
package/src/hostnames.ts
CHANGED
|
@@ -43,18 +43,16 @@ export const CATCH_ALL = { service: "http_status:404" } as const;
|
|
|
43
43
|
// covers exactly one level), where <panel> is `<repo>` or `<repo>--<app>` and <sandboxId> pins the hostname to
|
|
44
44
|
// this sandbox (the shared intentic zone hosts many sandboxes; without the id two users' panels would collide).
|
|
45
45
|
// Port-forward scheme: `port-<slot>-<sandboxId>.<zone>` — the same shape with a `port-` prefix, where <slot>
|
|
46
|
-
// is one of the sandbox's
|
|
47
|
-
// intentic-provided path's minted routes bounded and warm while dev servers churn ephemeral
|
|
46
|
+
// is one of the sandbox's forward slots (portSlotsFromToken in ./tunnel-ids), not the port number itself:
|
|
47
|
+
// slots keep the intentic-provided path's minted routes bounded and warm while dev servers churn ephemeral
|
|
48
|
+
// ports. The slot labels are salted with the connect token rather than being the letters a…h, so a forwarded
|
|
49
|
+
// port's hostname is not derivable from the (public) sandbox id alone — see tunnel-ids for why that matters.
|
|
48
50
|
//
|
|
49
51
|
// A *label* is the first-DNS-label prefix before `-<sandboxId>` (`preview-<panel>` / `port-<slot>`) — the unit
|
|
50
52
|
// the platform's /sandbox/preview-route mints, so one endpoint serves both schemes.
|
|
51
53
|
export const previewLabel = (panel: string): string => `preview-${panel}`;
|
|
52
54
|
export const portLabel = (slot: string): string => `port-${slot}`;
|
|
53
55
|
|
|
54
|
-
// The fixed per-sandbox forward slots. Eight is deliberate: enough for a monorepo's worth of concurrent dev
|
|
55
|
-
// servers, and the hard cap on preview DNS records a sandbox can ever cost the shared intentic zone.
|
|
56
|
-
export const PORT_SLOTS = ["a", "b", "c", "d", "e", "f", "g", "h"] as const;
|
|
57
|
-
|
|
58
56
|
// The hostname a label resolves to — what the platform's /sandbox/preview-route mints from the label alone.
|
|
59
57
|
export const labelHostname = (label: string, id: string, zone: string): string => `${label}-${id}.${zone}`;
|
|
60
58
|
export const previewHostname = (panel: string, id: string, zone: string): string => labelHostname(previewLabel(panel), id, zone);
|
package/src/index.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { agentContract } from "./contracts/agent.contract.js";
|
|
|
5
5
|
import { agentsContract } from "./contracts/agents.contract.js";
|
|
6
6
|
import { automationsContract } from "./contracts/automations.contract.js";
|
|
7
7
|
import { capabilitiesContract } from "./contracts/capabilities.contract.js";
|
|
8
|
+
import { choresContract } from "./contracts/chores.contract.js";
|
|
8
9
|
import { ciContract } from "./contracts/ci.contract.js";
|
|
9
10
|
import { claudeContract } from "./contracts/claude.contract.js";
|
|
10
11
|
import { codexContract } from "./contracts/codex.contract.js";
|
|
@@ -37,6 +38,7 @@ export { agentContract } from "./contracts/agent.contract.js";
|
|
|
37
38
|
export { agentsContract } from "./contracts/agents.contract.js";
|
|
38
39
|
export { automationsContract } from "./contracts/automations.contract.js";
|
|
39
40
|
export { capabilitiesContract } from "./contracts/capabilities.contract.js";
|
|
41
|
+
export { choresContract } from "./contracts/chores.contract.js";
|
|
40
42
|
export { ciContract } from "./contracts/ci.contract.js";
|
|
41
43
|
export { claudeContract } from "./contracts/claude.contract.js";
|
|
42
44
|
export { codexContract } from "./contracts/codex.contract.js";
|
|
@@ -86,6 +88,7 @@ export const sandboxContract = {
|
|
|
86
88
|
agents: agentsContract,
|
|
87
89
|
automations: automationsContract,
|
|
88
90
|
capabilities: capabilitiesContract,
|
|
91
|
+
chores: choresContract,
|
|
89
92
|
ci: ciContract,
|
|
90
93
|
claude: claudeContract,
|
|
91
94
|
codex: codexContract,
|
package/src/schemas.ts
CHANGED
|
@@ -122,6 +122,12 @@ export const AgentTurnSchema = z
|
|
|
122
122
|
// 'acceptEdits' auto-accepts file edits; 'bypassPermissions' runs everything. The agent can move
|
|
123
123
|
// itself between modes mid-turn (EnterPlanMode/ExitPlanMode), which rides back as a `mode` frame.
|
|
124
124
|
permissionMode: PermissionModeSchema.optional(),
|
|
125
|
+
/* Narrows the turn to these tool names (the SDK option of the same name — not to be confused with the
|
|
126
|
+
* daemon's MCP `tools`, which are servers). Absent ⇒ every tool the runtime has, which is what an
|
|
127
|
+
* owner-driven chat wants. Set by the automation dispatchers from Automation.allowedTools: a wake driven
|
|
128
|
+
* by an OUTSIDE message runs bypassPermissions like any other automation turn, so for a public Doorbell
|
|
129
|
+
* this list is the actual boundary — prompt wording is advice, an empty toolbox is not. */
|
|
130
|
+
allowedTools: z.array(z.string().min(1)).optional(),
|
|
125
131
|
effort: z.string().optional(),
|
|
126
132
|
thinking: z.boolean().optional(),
|
|
127
133
|
// The opt-in editor context chip: what the user is looking at, folded into the prompt daemon-side.
|
|
@@ -286,6 +292,7 @@ export type AgentSummary = z.infer<typeof AgentSummarySchema>;
|
|
|
286
292
|
// mutated agents back. The browser drops any roster older than the newest it has applied, and holds its own
|
|
287
293
|
// pending change until a roster at or past the revision that applied it arrives. See useAgents.ts.
|
|
288
294
|
export const AgentsListSchema = z.object({ agents: z.array(AgentSummarySchema), rev: z.number() });
|
|
295
|
+
export type AgentsList = z.infer<typeof AgentsListSchema>;
|
|
289
296
|
export const AgentIdSchema = z.object({ id: z.string().min(1) });
|
|
290
297
|
// archive's input: the agents to take off the board. Absent `ids` ⇒ every finished agent that is archivable
|
|
291
298
|
// right now (the lane header's "Clear"); unarchive always names its ids (a restore, or a bulk archive's undo).
|
|
@@ -353,6 +360,11 @@ export const LandConflictSchema = z.object({
|
|
|
353
360
|
repo: z.string(),
|
|
354
361
|
paths: z.array(LandConflictPathSchema),
|
|
355
362
|
clean: z.number(),
|
|
363
|
+
// The branch the user's checkout is on — the thing the agent has to rebase onto. Carried because only the
|
|
364
|
+
// daemon can see it: an isolated turn's worktree is mounted over the agent's whole view, so the resolution
|
|
365
|
+
// errand could otherwise only tell it to go and read the name off `git worktree list`. Absent on a detached
|
|
366
|
+
// HEAD or a vanished checkout, where there is no name to give.
|
|
367
|
+
mainBranch: z.string().optional(),
|
|
356
368
|
});
|
|
357
369
|
export type LandConflict = z.infer<typeof LandConflictSchema>;
|
|
358
370
|
|
|
@@ -414,7 +426,7 @@ export type UsageWindow = z.infer<typeof UsageWindowSchema>;
|
|
|
414
426
|
// An account's headroom: EVERY window the provider reports, read together, plus when the reading was taken.
|
|
415
427
|
// All of them, not the binding one, because "which pool is binding" changes between turns and a reader
|
|
416
428
|
// comparing accounts needs the same pools on every row. How the reading is TAKEN is per provider and stops at
|
|
417
|
-
// the daemon's readers: Claude's rides the turn's own stream, ChatGPT's and
|
|
429
|
+
// the daemon's readers: Claude's rides the turn's own stream, ChatGPT's, Google's and Kimi's are pulled through
|
|
418
430
|
// CLIProxyAPI's credential-scoped management call. All of them are control requests, so none costs tokens.
|
|
419
431
|
//
|
|
420
432
|
// Within one window utilization only climbs, so an un-reset window stays a valid FLOOR however old it is; past
|
|
@@ -426,13 +438,45 @@ export const AccountUsageSchema = z.object({
|
|
|
426
438
|
});
|
|
427
439
|
export type AccountUsage = z.infer<typeof AccountUsageSchema>;
|
|
428
440
|
|
|
441
|
+
/* THE LAST TIME A PROVIDER ACTUALLY REFUSED A TURN — the other half of "can I run on this", and the half no
|
|
442
|
+
* meter can supply.
|
|
443
|
+
*
|
|
444
|
+
* A snapshot above is POLLED and therefore always a floor: read at turn end (Claude) or on a five-minute sweep
|
|
445
|
+
* (the routed subscriptions), and account-wide, so every other client on the plan spends the same pools without
|
|
446
|
+
* this sandbox hearing about it. A refusal is the opposite kind of fact — observed, exact, and timestamped by
|
|
447
|
+
* the only event that proves the plan said no. Between them they answer a question neither can alone: a green
|
|
448
|
+
* meter beside "refused a turn 4 minutes ago" means the reading is stale, not that the account has room.
|
|
449
|
+
*
|
|
450
|
+
* Keyed by PROVIDER rather than by account, because that is the resolution the daemon honestly has. A native
|
|
451
|
+
* Claude turn knows which account served it and names it; a routed turn does not — CLIProxyAPI picks the auth
|
|
452
|
+
* file itself and only refuses once every credential it holds is cooling down, which makes the refusal a fact
|
|
453
|
+
* about the provider in the first place.
|
|
454
|
+
*
|
|
455
|
+
* `kind` is read off what the provider SAID, not off the frame code the harness filed it under, because those
|
|
456
|
+
* two disagree: Kimi answers a spent plan with `403 You've reached your usage limit for this billing cycle`,
|
|
457
|
+
* which the CLI prints under "Failed to authenticate" and the stream codes as a refused credential. Sending
|
|
458
|
+
* someone to reconnect a perfectly good account is the cost of believing the code over the sentence. */
|
|
459
|
+
export const ProviderRefusalSchema = z.object({
|
|
460
|
+
// Epoch MS, matching AccountUsage.measuredAt — the two are read side by side.
|
|
461
|
+
at: z.number(),
|
|
462
|
+
kind: z.enum(["limit", "auth"]),
|
|
463
|
+
// The provider's own sentence, verbatim. It is the only part that says WHICH pool or WHICH credential.
|
|
464
|
+
message: z.string(),
|
|
465
|
+
// The account that was serving, when the daemon knows it (native turns only — see above).
|
|
466
|
+
account: z.string().optional(),
|
|
467
|
+
});
|
|
468
|
+
export type ProviderRefusal = z.infer<typeof ProviderRefusalSchema>;
|
|
469
|
+
|
|
470
|
+
export const ProviderRefusalsSchema = z.object({ refusals: z.record(z.string(), ProviderRefusalSchema) });
|
|
471
|
+
export type ProviderRefusals = z.infer<typeof ProviderRefusalsSchema>;
|
|
472
|
+
|
|
429
473
|
// One connected subscription in the translator. `name` is CLIProxyAPI's auth-file name — the stable store key a
|
|
430
474
|
// disconnect addresses — and `label` the sign-in identity it reported (the account email, else the file name).
|
|
431
475
|
export const TranslatorAccountSchema = z.object({
|
|
432
476
|
name: z.string(),
|
|
433
477
|
label: z.string(),
|
|
434
478
|
// The same headroom an OauthAccount carries, on the same field, for the same reason: the account rows are
|
|
435
|
-
// one list to the reader. Optional because a provider whose quota this sandbox cannot read (Grok
|
|
479
|
+
// one list to the reader. Optional because a provider whose quota this sandbox cannot read (Grok) —
|
|
436
480
|
// or one that did not answer — must still render as the connected account it is, with a dot instead of a
|
|
437
481
|
// ring.
|
|
438
482
|
usage: AccountUsageSchema.optional(),
|
|
@@ -552,6 +596,7 @@ export const OauthAccountSchema = z.object({
|
|
|
552
596
|
});
|
|
553
597
|
export type OauthAccount = z.infer<typeof OauthAccountSchema>;
|
|
554
598
|
export const OauthAccountListSchema = z.object({ accounts: z.array(OauthAccountSchema) });
|
|
599
|
+
export type OauthAccountList = z.infer<typeof OauthAccountListSchema>;
|
|
555
600
|
// Address one account of a provider (disconnect, and the turn's `account`).
|
|
556
601
|
export const AccountIdSchema = z.object({ id: z.string().min(1) });
|
|
557
602
|
// Rename one account of a provider whose credential the sandbox owns (Claude, Kimi). Blank ⇒ the daemon falls
|
|
@@ -631,6 +676,7 @@ export const SessionSummarySchema = z.object({
|
|
|
631
676
|
// own heading is noise, not evidence. See AgentMatchSchema for the same field on the fleet's side.
|
|
632
677
|
snippet: z.string().optional(),
|
|
633
678
|
});
|
|
679
|
+
export type SessionSummary = z.infer<typeof SessionSummarySchema>;
|
|
634
680
|
export const SessionsListSchema = z.object({ sessions: z.array(SessionSummarySchema) });
|
|
635
681
|
|
|
636
682
|
// ---- settings: per-sandbox agent settings (.intentic/settings.json) ----
|
|
@@ -1222,24 +1268,37 @@ export type FileDiff = z.infer<typeof FileDiffSchema>;
|
|
|
1222
1268
|
|
|
1223
1269
|
// ---- workspace tree + files ----
|
|
1224
1270
|
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1271
|
+
/* One node of the full /work filesystem tree the agent sees (untracked + generated files included), distinct
|
|
1272
|
+
* from the git-tracked listing. `path` is root-relative with forward slashes so it feeds straight back to the
|
|
1273
|
+
* file route.
|
|
1274
|
+
*
|
|
1275
|
+
* Recursive, and the type is declared rather than inferred. Zod's getter form does infer one, but it collapses
|
|
1276
|
+
* to `{}` below the first level of nesting — so `entry.children[0].name` type-checked as an index-signature
|
|
1277
|
+
* read on both sides of the wire, and the tree walker's own suite was reading a `hidden` field off entries
|
|
1278
|
+
* that has never existed there without the compiler minding. The interface is the contract; the schema
|
|
1279
|
+
* validates against it, and z.ZodType makes a divergence between the two an error here. */
|
|
1280
|
+
export interface WorkspaceTreeEntry {
|
|
1281
|
+
readonly name: string;
|
|
1282
|
+
readonly path: string;
|
|
1283
|
+
readonly type: "file" | "dir";
|
|
1284
|
+
readonly size?: number | undefined;
|
|
1285
|
+
// Ignored-by-tooling (node_modules, .git, .gitignore'd paths, browser profiles): the client grays the row.
|
|
1286
|
+
readonly ignored?: boolean | undefined;
|
|
1287
|
+
// A DIR without `children` was listed but not descended into — because it's ignored, or because the walk's
|
|
1288
|
+
// breadth-first budget stopped above it. Either way the client lazy-loads it via /workspace/children on
|
|
1289
|
+
// expand, so "not loaded yet" and "empty directory" (`children: []`) stay distinguishable.
|
|
1290
|
+
readonly children?: readonly WorkspaceTreeEntry[] | undefined;
|
|
1291
|
+
}
|
|
1292
|
+
export const WorkspaceTreeEntrySchema: z.ZodType<WorkspaceTreeEntry> = z.object({
|
|
1229
1293
|
name: z.string(),
|
|
1230
1294
|
path: z.string(),
|
|
1231
1295
|
type: z.enum(["file", "dir"]),
|
|
1232
1296
|
size: z.number().optional(),
|
|
1233
|
-
// Ignored-by-tooling (node_modules, .git, .gitignore'd paths, browser profiles): the client grays the row.
|
|
1234
1297
|
ignored: z.boolean().optional(),
|
|
1235
|
-
// A DIR without `children` was listed but not descended into — because it's ignored, or because the walk's
|
|
1236
|
-
// breadth-first budget stopped above it. Either way the client lazy-loads it via /workspace/children on
|
|
1237
|
-
// expand, so "not loaded yet" and "empty directory" (`children: []`) stay distinguishable.
|
|
1238
1298
|
get children() {
|
|
1239
1299
|
return z.array(WorkspaceTreeEntrySchema).optional();
|
|
1240
1300
|
},
|
|
1241
1301
|
});
|
|
1242
|
-
export type WorkspaceTreeEntry = z.infer<typeof WorkspaceTreeEntrySchema>;
|
|
1243
1302
|
export const WorkspaceTreeSchema = z.object({
|
|
1244
1303
|
root: z.string(),
|
|
1245
1304
|
tree: z.array(WorkspaceTreeEntrySchema),
|
|
@@ -1300,14 +1359,20 @@ export type WorkspaceClassification = z.infer<typeof WorkspaceClassificationSche
|
|
|
1300
1359
|
// The workspace-search wire shape — shared by the daemon's /workspace/search route and the web client.
|
|
1301
1360
|
// (Implementation detail, not part of the contract: the daemon backs this route with a resident in-process iq
|
|
1302
1361
|
// engine; the engine is interchangeable behind this shape.) Groups are relevance-ranked (best first, never path
|
|
1303
|
-
// order); each hit carries the match-reason tags the fused engines contributed
|
|
1304
|
-
//
|
|
1362
|
+
// order); each hit carries the match-reason tags the fused engines contributed, and the char spans within `text`
|
|
1363
|
+
// that matched, so clients highlight without re-finding the needle.
|
|
1305
1364
|
export const WorkspaceSearchQuerySchema = z.object({
|
|
1306
1365
|
query: z.string().min(2).max(512),
|
|
1307
1366
|
// Search verbs only — anchor/git verbs (outline, context, log, who, …) are CLI-only surface. Natural language
|
|
1308
1367
|
// has no verb of its own: `q` classifies the query and answers it semantically when the words call for it.
|
|
1309
1368
|
mode: z.enum(["q", "find", "files", "def", "refs", "sym", "ast"]).optional(),
|
|
1310
1369
|
includeIgnored: z.stringbool().optional(),
|
|
1370
|
+
// How `find` reads the query — the three switches every editor's search box has (VSCode: Aa, ab, .*).
|
|
1371
|
+
// `literal` treats it as fixed text instead of a regex; `caseSensitive` off means case-INSENSITIVE, not
|
|
1372
|
+
// ripgrep's smart case.
|
|
1373
|
+
literal: z.stringbool().optional(),
|
|
1374
|
+
word: z.stringbool().optional(),
|
|
1375
|
+
caseSensitive: z.stringbool().optional(),
|
|
1311
1376
|
limit: z.coerce.number().int().positive().optional(),
|
|
1312
1377
|
after: z.string().optional(),
|
|
1313
1378
|
});
|
|
@@ -1316,11 +1381,14 @@ export const WorkspaceSearchTagSchema = z.object({
|
|
|
1316
1381
|
score: z.number().optional(),
|
|
1317
1382
|
});
|
|
1318
1383
|
export type WorkspaceSearchTag = z.infer<typeof WorkspaceSearchTagSchema>;
|
|
1384
|
+
export const WorkspaceSearchSpanSchema = z.object({ start: z.number(), end: z.number() });
|
|
1385
|
+
export type WorkspaceSearchSpan = z.infer<typeof WorkspaceSearchSpanSchema>;
|
|
1319
1386
|
export const WorkspaceSearchHitSchema = z.object({
|
|
1320
1387
|
line: z.number(),
|
|
1321
1388
|
text: z.string(),
|
|
1322
|
-
|
|
1323
|
-
|
|
1389
|
+
// Every matched span in `text`, in order — a text search marks all of them, the way an editor does. Empty
|
|
1390
|
+
// where the LINE is the match and no span of it is (a semantic or definition hit reports none).
|
|
1391
|
+
spans: z.array(WorkspaceSearchSpanSchema),
|
|
1324
1392
|
tags: z.array(WorkspaceSearchTagSchema),
|
|
1325
1393
|
// Enclosing symbol ("createWidget (fn)") — parent-document context so the reader often needs no follow-up.
|
|
1326
1394
|
context: z.string().optional(),
|
|
@@ -1342,12 +1410,19 @@ export type WorkspaceSearchFreshness = z.infer<typeof WorkspaceSearchFreshnessSc
|
|
|
1342
1410
|
export const WorkspaceSearchResultSchema = z.object({
|
|
1343
1411
|
mode: z.string(),
|
|
1344
1412
|
total: z.number(),
|
|
1413
|
+
// Files the query matched in total, which `groups` reports only for the page it carries — the count a
|
|
1414
|
+
// results panel puts beside the hit total ("218 results in 61 files").
|
|
1415
|
+
files: z.number(),
|
|
1345
1416
|
shown: z.number(),
|
|
1346
1417
|
groups: z.array(WorkspaceSearchGroupSchema),
|
|
1347
1418
|
freshness: WorkspaceSearchFreshnessSchema,
|
|
1348
1419
|
truncated: z.boolean(),
|
|
1349
1420
|
cursor: z.string().optional(),
|
|
1350
1421
|
hint: z.string().optional(),
|
|
1422
|
+
// What the engine did with the query that the query did not ask for — a pattern rerun as literal text
|
|
1423
|
+
// because it is not valid regex, grep-style escapes rewritten, a language filter that matched no files. The
|
|
1424
|
+
// text surface has always printed this above the results; a JSON caller could not see it at all.
|
|
1425
|
+
note: z.string().optional(),
|
|
1351
1426
|
// Code-graph neighbors of the top hits (definition anchors + the strongest caller of each).
|
|
1352
1427
|
related: z.array(z.string()).optional(),
|
|
1353
1428
|
// Ranked `path:line` anchors that placed but were NOT shown, best first — the answer often sits at rank 5–13,
|
|
@@ -1961,6 +2036,10 @@ export const ExtensionSummarySchema = z.object({
|
|
|
1961
2036
|
// Image-baked first-party extension (no git checkout, not removable) vs a git-installed capability — the
|
|
1962
2037
|
// web hides the uninstall affordance for baked ones.
|
|
1963
2038
|
builtin: z.boolean(),
|
|
2039
|
+
// The owner's switch (.intentic/extension-enablement.json). A disabled extension is still listed — that's
|
|
2040
|
+
// what makes it switchable back on — but the daemon wires none of its contributions up and the web host
|
|
2041
|
+
// doesn't activate it.
|
|
2042
|
+
enabled: z.boolean(),
|
|
1964
2043
|
});
|
|
1965
2044
|
export type ExtensionSummary = z.infer<typeof ExtensionSummarySchema>;
|
|
1966
2045
|
export const ExtensionsListSchema = z.object({ extensions: z.array(ExtensionSummarySchema) });
|
|
@@ -1977,6 +2056,9 @@ export const ExtensionSettingsInputSchema = z.object({
|
|
|
1977
2056
|
id: z.string(),
|
|
1978
2057
|
settings: z.record(z.string(), z.union([z.string(), z.number(), z.boolean()])),
|
|
1979
2058
|
});
|
|
2059
|
+
// Flip one extension on or off. Persisted by publisher.name (like settings), so the choice outlives the
|
|
2060
|
+
// checkout; the daemon's immediate half of the flip — declared processes — converges in the same handler.
|
|
2061
|
+
export const ExtensionEnabledInputSchema = z.object({ id: z.string(), enabled: z.boolean() });
|
|
1980
2062
|
// One declared background process (contributes.processes) — status/start/stop, addressed by the capability
|
|
1981
2063
|
// entry id + the manifest's process name. Undeclared names are NOT_FOUND, the manifest-honesty rule again.
|
|
1982
2064
|
export const ExtensionProcessParamSchema = z.object({ id: z.string(), name: z.string() });
|
|
@@ -2064,12 +2146,130 @@ export const TriggerSchema = z.discriminatedUnion("kind", [
|
|
|
2064
2146
|
]);
|
|
2065
2147
|
export type Trigger = z.infer<typeof TriggerSchema>;
|
|
2066
2148
|
|
|
2149
|
+
/* The Doorbell widget's settings — everything about the embeddable chat that isn't the automation's prompt.
|
|
2150
|
+
* Present only on `webchat` listener automations; the trigger keeps `allowedOrigins` because that one is the
|
|
2151
|
+
* admission gate the message route reads, not a rendering choice.
|
|
2152
|
+
*
|
|
2153
|
+
* Split deliberately into what the WIDGET may read (title/greeting/accent/position/access/googleClientId/
|
|
2154
|
+
* turnstileSiteKey — all public by construction, they ship to a stranger's browser) and what only the daemon
|
|
2155
|
+
* may read (turnstileSecret). GET /webchat/<id>/config serves the first group by naming it, never by omitting
|
|
2156
|
+
* the second: a field added here is invisible to the widget until it is listed there. */
|
|
2157
|
+
export const WebchatConfigSchema = z.object({
|
|
2158
|
+
// `public` admits anyone; `google` refuses a message that carries no verifiable Google ID token. Absent ⇒
|
|
2159
|
+
// public — a Doorbell with no access setting is the anonymous support box it looks like.
|
|
2160
|
+
access: z.enum(["public", "google"]).optional(),
|
|
2161
|
+
// Ask an anonymous visitor for a display name before the first message. Cosmetic: the name is typed, so it
|
|
2162
|
+
// reaches the model as untrusted `displayName`, never as identity.
|
|
2163
|
+
requireName: z.boolean().optional(),
|
|
2164
|
+
/* The bot ceiling. `turnstile` is Cloudflare's (invisible, needs the site's own keys); `pow` is a
|
|
2165
|
+
* hashcash-style challenge the daemon issues and the widget solves in a worker, so a site with no
|
|
2166
|
+
* Cloudflare account still has something. Absent ⇒ off: the origin allowlist and the rate limit are then
|
|
2167
|
+
* the whole boundary, which is the right default for an internal or invite-only page. */
|
|
2168
|
+
antiBot: z.enum(["turnstile", "pow"]).optional(),
|
|
2169
|
+
turnstileSiteKey: z.string().optional(),
|
|
2170
|
+
turnstileSecret: z.string().optional(),
|
|
2171
|
+
// The site's OWN Google OAuth web client id. It cannot be intentic's: Google Identity Services only issues
|
|
2172
|
+
// a token to an authorized JavaScript origin, and intentic's client can't list every customer domain.
|
|
2173
|
+
googleClientId: z.string().optional(),
|
|
2174
|
+
// Widget chrome. `accent` is any CSS colour the host page can render; `position` picks the launcher corner.
|
|
2175
|
+
title: z.string().max(80).optional(),
|
|
2176
|
+
greeting: z.string().max(500).optional(),
|
|
2177
|
+
accent: z.string().max(40).optional(),
|
|
2178
|
+
position: z.enum(["top-right", "top-left", "bottom-right", "bottom-left"]).optional(),
|
|
2179
|
+
/* Two ceilings on top of the route's fixed per-minute window, because a public endpoint's real exposure is
|
|
2180
|
+
* cost, not request rate: `dailyMessageMax` caps the whole automation per UTC day, `conversationMessageMax`
|
|
2181
|
+
* caps one visitor thread for its lifetime.
|
|
2182
|
+
*
|
|
2183
|
+
* `dailyMessageMax` absent ⇒ WEBCHAT_DAILY_MAX_DEFAULT, not uncapped. Every message here is an agent turn
|
|
2184
|
+
* billed to the owner, and the per-minute window bounds the RATE without bounding the DAY — twenty a minute,
|
|
2185
|
+
* sustained, is tens of thousands of turns before anyone notices. A Doorbell nobody configured should not be
|
|
2186
|
+
* able to spend that, so the safe number is the one you get for free and the owner raises it deliberately.
|
|
2187
|
+
* `conversationMessageMax` stays optional-means-uncapped: it is per visitor thread, which the daily ceiling
|
|
2188
|
+
* already bounds in aggregate. */
|
|
2189
|
+
dailyMessageMax: z.number().int().positive().optional(),
|
|
2190
|
+
conversationMessageMax: z.number().int().positive().optional(),
|
|
2191
|
+
// (WEBCHAT_DAILY_MAX_DEFAULT, below the schema, is the number `dailyMessageMax` falls back to.)
|
|
2192
|
+
// How long a visitor thread keeps resuming the same conversation before the next message starts a fresh
|
|
2193
|
+
// one. Absent ⇒ WEBCHAT_SESSION_TTL_MS (the daemon's default).
|
|
2194
|
+
sessionTtlMinutes: z.number().int().positive().optional(),
|
|
2195
|
+
});
|
|
2196
|
+
export type WebchatConfig = z.infer<typeof WebchatConfigSchema>;
|
|
2197
|
+
|
|
2198
|
+
/* The daily agent-turn ceiling a Doorbell gets when its owner sets none. Lives here rather than beside the
|
|
2199
|
+
* route that enforces it because both ends need the number: the daemon to apply it, and the automation editor
|
|
2200
|
+
* to show the owner what they are already protected by (an invisible limit is one people hit and file as a bug).
|
|
2201
|
+
*
|
|
2202
|
+
* 200 is chosen to be irrelevant to real support traffic and decisive against a script. A Doorbell answering
|
|
2203
|
+
* two hundred questions in one UTC day is a busy one; a scripted flood reaches that in ten seconds and then
|
|
2204
|
+
* stops costing anything. */
|
|
2205
|
+
export const WEBCHAT_DAILY_MAX_DEFAULT = 200;
|
|
2206
|
+
|
|
2207
|
+
/* ---- the widget wire: three shapes GET /webchat/<id>/config, GET …/challenge and POST …/message speak ----
|
|
2208
|
+
*
|
|
2209
|
+
* They live here, beside the stored config they derive from, because the Doorbell widget is a SECOND client of
|
|
2210
|
+
* this daemon — a bundle running on a stranger's page — and the reason this package exists is that both ends of
|
|
2211
|
+
* a wire read one definition. The widget imports these as types only (`import type`), so zod never reaches a
|
|
2212
|
+
* visitor's browser. */
|
|
2213
|
+
|
|
2214
|
+
// What the widget is told about itself. Fully RESOLVED — every default is applied daemon-side, so the widget
|
|
2215
|
+
// carries no fallback logic and one place decides what an unset field means. Everything here is public by
|
|
2216
|
+
// construction: it ships to any browser that can reach the endpoint from an allowed origin.
|
|
2217
|
+
export const WebchatPublicConfigSchema = z.object({
|
|
2218
|
+
automationId: z.string(),
|
|
2219
|
+
title: z.string(),
|
|
2220
|
+
greeting: z.string(),
|
|
2221
|
+
accent: z.string(),
|
|
2222
|
+
position: z.enum(["top-right", "top-left", "bottom-right", "bottom-left"]),
|
|
2223
|
+
access: z.enum(["public", "google"]),
|
|
2224
|
+
requireName: z.boolean(),
|
|
2225
|
+
// "off" is spelled out rather than left absent: the widget branches on this, and a missing field that means
|
|
2226
|
+
// "no challenge" is the kind of default that turns one serialization bug into an open door.
|
|
2227
|
+
antiBot: z.enum(["turnstile", "pow", "off"]),
|
|
2228
|
+
turnstileSiteKey: z.string().optional(),
|
|
2229
|
+
googleClientId: z.string().optional(),
|
|
2230
|
+
});
|
|
2231
|
+
export type WebchatPublicConfig = z.infer<typeof WebchatPublicConfigSchema>;
|
|
2232
|
+
|
|
2233
|
+
// A proof-of-work challenge: find a nonce whose SHA-256 of `${salt}:${nonce}` starts with `difficulty` zero
|
|
2234
|
+
// bits. Issued per visitor conversation, spent on its first message.
|
|
2235
|
+
export const WebchatChallengeSchema = z.object({ salt: z.string(), difficulty: z.number().int().positive() });
|
|
2236
|
+
export type WebchatChallenge = z.infer<typeof WebchatChallengeSchema>;
|
|
2237
|
+
|
|
2238
|
+
// One visitor message. `conversationId` is the widget's own localStorage id — it threads the visitor's messages
|
|
2239
|
+
// into ONE sandbox conversation, so it is the thread key, not a secret (anyone can mint one; the origin
|
|
2240
|
+
// allowlist, the challenge and the rate limit are the gate).
|
|
2241
|
+
export const WebchatMessageSchema = z.object({
|
|
2242
|
+
conversationId: z.string().min(1).max(200),
|
|
2243
|
+
content: z.string().min(1),
|
|
2244
|
+
// What the visitor TYPED as their name. Never identity — it reaches the model tagged as unverified, and a
|
|
2245
|
+
// signed-in visitor's verified name comes from the ID token instead.
|
|
2246
|
+
displayName: z.string().max(200).optional(),
|
|
2247
|
+
// A Google ID token from the site's own client id, verified daemon-side against Google's JWKS.
|
|
2248
|
+
idToken: z.string().optional(),
|
|
2249
|
+
// The anti-bot answer, whichever kind the config asked for. Checked once per conversation, not per message.
|
|
2250
|
+
turnstileToken: z.string().optional(),
|
|
2251
|
+
powNonce: z.string().optional(),
|
|
2252
|
+
// The widget's own transcript, sent ONLY on the first message of a thread — after that the sandbox
|
|
2253
|
+
// conversation resumes and carries its own context.
|
|
2254
|
+
history: z
|
|
2255
|
+
.array(z.object({ author: z.string().optional(), content: z.string() }))
|
|
2256
|
+
.max(50)
|
|
2257
|
+
.optional(),
|
|
2258
|
+
});
|
|
2259
|
+
export type WebchatMessage = z.infer<typeof WebchatMessageSchema>;
|
|
2260
|
+
|
|
2067
2261
|
export const AutomationSchema = z.object({
|
|
2068
2262
|
id: entryId,
|
|
2069
2263
|
trigger: TriggerSchema,
|
|
2070
2264
|
// Shell command run in the workspace root before waking; exit 0 ⇒ wake, non-zero ⇒ the run is "skipped".
|
|
2071
2265
|
guard: z.string().min(1).optional(),
|
|
2072
2266
|
prompt: z.string().min(1),
|
|
2267
|
+
// The Doorbell widget's settings — `webchat` listener automations only, ignored on every other trigger.
|
|
2268
|
+
webchat: WebchatConfigSchema.optional(),
|
|
2269
|
+
// The tool names this automation's wake may call (AgentTurnSchema.allowedTools). The reason it exists: a
|
|
2270
|
+
// webchat automation is driven by strangers, and an automation turn runs bypassPermissions by default — so
|
|
2271
|
+
// the allowlist, not the prompt's wording, is what bounds what an injected instruction can reach.
|
|
2272
|
+
allowedTools: z.array(z.string().min(1)).optional(),
|
|
2073
2273
|
// Which provider adapter serves the wake; absent ⇒ claude. Same dispatch as a chat turn (AgentTurnSchema.agent).
|
|
2074
2274
|
agent: AgentProviderSchema.optional(),
|
|
2075
2275
|
// Which harness (agentic loop) runs the wake; absent ⇒ native. Same semantics as AgentTurnSchema.harness.
|
|
@@ -2427,10 +2627,9 @@ export type PortForwardResult = z.infer<typeof PortForwardResultSchema>;
|
|
|
2427
2627
|
// it lists from /system/browsers with the pages it has open (BrowserSessionSchema below).
|
|
2428
2628
|
//
|
|
2429
2629
|
// `activityAt` (epoch ms of the session's last output) and `exitCode` (the LAST window's exit status, absent
|
|
2430
|
-
// while that pane still lives)
|
|
2431
|
-
//
|
|
2432
|
-
//
|
|
2433
|
-
// never as 1970.
|
|
2630
|
+
// while that pane still lives) describe a session beyond "it exists": the panel's work popover orders its live
|
|
2631
|
+
// rows by the one and dates them off it, and the daemon's retention sweep ages sessions out by the same clock.
|
|
2632
|
+
// 0 is "tmux didn't say" — treated as unknown by both, never as 1970.
|
|
2434
2633
|
export const TerminalSessionSchema = z.object({
|
|
2435
2634
|
name: z.string(),
|
|
2436
2635
|
label: z.string().optional(),
|
|
@@ -2626,6 +2825,7 @@ export const InfoSchema = z.object({
|
|
|
2626
2825
|
latest: z.string().optional(),
|
|
2627
2826
|
updateAvailable: z.boolean().optional(),
|
|
2628
2827
|
});
|
|
2828
|
+
export type Info = z.infer<typeof InfoSchema>;
|
|
2629
2829
|
|
|
2630
2830
|
// A daemon-minted session (system.session): the steady-state browser credential, exchanged for a verified
|
|
2631
2831
|
// Google ID token so Google UI is a sign-in moment instead of an hourly renewal. `expiresAt` is epoch ms —
|
|
@@ -2932,3 +3132,211 @@ export const PushConfigQuerySchema = z.object({ endpoint: z.string().url().optio
|
|
|
2932
3132
|
// the question being asked. A count separates "your OS swallowed it" from "nothing was sent at all".
|
|
2933
3133
|
export const PushTestSchema = z.object({ delivered: z.number().int().nonnegative() });
|
|
2934
3134
|
export type PushTest = z.infer<typeof PushTestSchema>;
|
|
3135
|
+
|
|
3136
|
+
// ---- maintenance: the standing evidence a chore is decided from ----
|
|
3137
|
+
|
|
3138
|
+
/* THE DAEMON SERVES FACTS; THE BROWSER DECIDES. Everything below is measurement — what a tool reported, what the
|
|
3139
|
+
* manifests say, when a chore last ran. Not one field here says "you should do something", and that is the whole
|
|
3140
|
+
* boundary: which chore is DUE is computed by @intentic/sandbox-contract/chores, which both the Maintenance view and its rail
|
|
3141
|
+
* badge run, so the number on the tile and the reason in the panel can never disagree. Put the verdict on the wire
|
|
3142
|
+
* instead and a daemon one image behind would be quietly arguing with the browser about what needs doing.
|
|
3143
|
+
*
|
|
3144
|
+
* The split inside the evidence is by COST, not by subject:
|
|
3145
|
+
* probes subprocesses (pnpm outdated, pnpm audit, knip, jscpd) — minutes, so they are cached on disk with a
|
|
3146
|
+
* TTL and refreshed by a background runner. A route hit never waits on one.
|
|
3147
|
+
* signals things the daemon already knows — the resident iq index's health ranking, the package manifests it
|
|
3148
|
+
* reads for the dependency graph, its own node version. Recomputed per request; all of it is cheap. */
|
|
3149
|
+
|
|
3150
|
+
export const PROBE_IDS = ["outdated", "audit", "knip", "jscpd"] as const;
|
|
3151
|
+
export const ProbeIdSchema = z.enum(PROBE_IDS);
|
|
3152
|
+
export type ProbeId = z.infer<typeof ProbeIdSchema>;
|
|
3153
|
+
|
|
3154
|
+
// One dependency the registry has moved past. `kind` is the SEMVER distance, which is the whole reason this is
|
|
3155
|
+
// not one number: forty patch releases behind is a morning's work and one major is a project.
|
|
3156
|
+
export const OutdatedPackageSchema = z.object({
|
|
3157
|
+
name: z.string(),
|
|
3158
|
+
current: z.string(),
|
|
3159
|
+
latest: z.string(),
|
|
3160
|
+
kind: z.enum(["major", "minor", "patch"]),
|
|
3161
|
+
// "dependencies" / "devDependencies" / "optionalDependencies" — a dev-only major is a different risk.
|
|
3162
|
+
section: z.string(),
|
|
3163
|
+
});
|
|
3164
|
+
export type OutdatedPackage = z.infer<typeof OutdatedPackageSchema>;
|
|
3165
|
+
|
|
3166
|
+
// One advisory, reduced to what a decision needs. No CVSS vector and no reference list: those are for reading on
|
|
3167
|
+
// the advisory page, and carrying them would put a kilobyte of prose per finding on every poll of this route.
|
|
3168
|
+
export const AdvisorySchema = z.object({
|
|
3169
|
+
name: z.string(),
|
|
3170
|
+
severity: z.enum(["critical", "high", "moderate", "low", "info"]),
|
|
3171
|
+
title: z.string(),
|
|
3172
|
+
// The range that fixes it, when the advisory names one. Absent ⇒ no patch published yet, which is the case
|
|
3173
|
+
// where a chore must NOT offer to bump and say so instead.
|
|
3174
|
+
patched: z.string().optional(),
|
|
3175
|
+
// Whether it reaches a production dependency path. A build-time-only tool's transitive CVE is a different
|
|
3176
|
+
// problem, and the chore's prompt says so rather than treating every advisory alike.
|
|
3177
|
+
dev: z.boolean(),
|
|
3178
|
+
});
|
|
3179
|
+
export type Advisory = z.infer<typeof AdvisorySchema>;
|
|
3180
|
+
|
|
3181
|
+
// knip's counts, by the kind of thing it found unreachable. Counts plus a sample rather than the full list: the
|
|
3182
|
+
// agent re-runs knip itself against the live tree (a list from a probe hours old would send it at files that are
|
|
3183
|
+
// already gone), so what travels here only has to be enough to decide whether the turn is worth starting.
|
|
3184
|
+
export const DeadCodeSchema = z.object({
|
|
3185
|
+
files: z.number().int().nonnegative(),
|
|
3186
|
+
exports: z.number().int().nonnegative(),
|
|
3187
|
+
types: z.number().int().nonnegative(),
|
|
3188
|
+
dependencies: z.number().int().nonnegative(),
|
|
3189
|
+
devDependencies: z.number().int().nonnegative(),
|
|
3190
|
+
// A handful of the unreferenced files, for the panel to show instead of asking the reader to take "31" on faith.
|
|
3191
|
+
sample: z.array(z.string()),
|
|
3192
|
+
});
|
|
3193
|
+
export type DeadCode = z.infer<typeof DeadCodeSchema>;
|
|
3194
|
+
|
|
3195
|
+
// jscpd's headline plus the biggest clones. `percentage` is of scanned lines, which is the figure a threshold is
|
|
3196
|
+
// worth setting against — a clone COUNT grows with the repo and would mean something different every quarter.
|
|
3197
|
+
export const DuplicationSchema = z.object({
|
|
3198
|
+
percentage: z.number(),
|
|
3199
|
+
clones: z.number().int().nonnegative(),
|
|
3200
|
+
top: z.array(z.object({ lines: z.number().int().nonnegative(), first: z.string(), second: z.string() })),
|
|
3201
|
+
});
|
|
3202
|
+
export type Duplication = z.infer<typeof DuplicationSchema>;
|
|
3203
|
+
|
|
3204
|
+
/* One probe's cached result. The three states are deliberately distinct, because a panel that collapses them
|
|
3205
|
+
* lies about the most important case:
|
|
3206
|
+
* ok the tool ran and reported. `facts` carries its findings — including "nothing found", which is
|
|
3207
|
+
* a real answer and the one that keeps a chore quiet.
|
|
3208
|
+
* unavailable the tool is not part of this repo (knip is not a devDependency, there is no lockfile to audit).
|
|
3209
|
+
* Not a failure and not evidence of health: the chore renders as unmeasured, and can never badge.
|
|
3210
|
+
* failed the tool ran and broke — a network-less audit, a jscpd that ran out of memory. Says so, with
|
|
3211
|
+
* the tail of what it printed, rather than reading as "clean".
|
|
3212
|
+
* Merging `unavailable` into `ok`-with-zeros is how a maintenance surface ends up reporting a green repository
|
|
3213
|
+
* it has never actually measured. */
|
|
3214
|
+
export const ProbeStateSchema = z.enum(["ok", "unavailable", "failed"]);
|
|
3215
|
+
export type ProbeState = z.infer<typeof ProbeStateSchema>;
|
|
3216
|
+
|
|
3217
|
+
// The findings, discriminated by which probe produced them. Absent while the probe has never completed, and on
|
|
3218
|
+
// `unavailable`/`failed` — a reader must go through `state` to reach facts, so there is no shape in which a
|
|
3219
|
+
// missing measurement can be mistaken for a zero.
|
|
3220
|
+
export const ProbeFactsSchema = z.discriminatedUnion("id", [
|
|
3221
|
+
z.object({ id: z.literal("outdated"), packages: z.array(OutdatedPackageSchema) }),
|
|
3222
|
+
z.object({ id: z.literal("audit"), advisories: z.array(AdvisorySchema) }),
|
|
3223
|
+
z.object({ id: z.literal("knip"), deadCode: DeadCodeSchema }),
|
|
3224
|
+
z.object({ id: z.literal("jscpd"), duplication: DuplicationSchema }),
|
|
3225
|
+
]);
|
|
3226
|
+
export type ProbeFacts = z.infer<typeof ProbeFactsSchema>;
|
|
3227
|
+
|
|
3228
|
+
export const ProbeResultSchema = z.object({
|
|
3229
|
+
id: ProbeIdSchema,
|
|
3230
|
+
state: ProbeStateSchema,
|
|
3231
|
+
// When the probe last COMPLETED — the age the panel shows, and what the runner's TTL is measured from.
|
|
3232
|
+
ranAt: z.number(),
|
|
3233
|
+
// How long it took. Shown because a seven-minute jscpd is why the tier-2 refresh is weekly, and a reader
|
|
3234
|
+
// deciding whether to force a refresh deserves to know what they are asking for.
|
|
3235
|
+
tookMs: z.number().int().nonnegative(),
|
|
3236
|
+
facts: ProbeFactsSchema.optional(),
|
|
3237
|
+
// Why it is unavailable, or how it failed — the tail of the tool's own output. Never invented here.
|
|
3238
|
+
reason: z.string().optional(),
|
|
3239
|
+
});
|
|
3240
|
+
export type ProbeResult = z.infer<typeof ProbeResultSchema>;
|
|
3241
|
+
|
|
3242
|
+
// One workspace package as its manifest declares it — what the daemon already reads to build the dependency
|
|
3243
|
+
// graph, carried through so chores can reason about the repo's own shape without a probe. `documented` is the
|
|
3244
|
+
// one derived field: whether docs/architecture/<dir>/doc.md exists, a stat per package.
|
|
3245
|
+
export const ChorePackageSchema = z.object({
|
|
3246
|
+
dir: z.string(),
|
|
3247
|
+
name: z.string(),
|
|
3248
|
+
// The manifest's `engines` map, verbatim — the runtime chore compares it against what the daemon is running.
|
|
3249
|
+
engines: z.record(z.string(), z.string()).optional(),
|
|
3250
|
+
dependencies: z.array(z.string()),
|
|
3251
|
+
devDependencies: z.array(z.string()),
|
|
3252
|
+
documented: z.boolean(),
|
|
3253
|
+
});
|
|
3254
|
+
export type ChorePackage = z.infer<typeof ChorePackageSchema>;
|
|
3255
|
+
|
|
3256
|
+
/* The cheap half of the evidence: what the daemon knows without starting anything. `hotspots` and `keyModules`
|
|
3257
|
+
* are the same rankings GET /workspace/health serves, capped tighter — a chore only ever asks whether a file has
|
|
3258
|
+
* ENTERED the top of the ranking, so a leaderboard is enough and a full report per repo per poll is not. */
|
|
3259
|
+
/* WHAT THIS REPOSITORY IS MADE OF — the facts that decide whether a chore is a QUESTION worth asking of it at
|
|
3260
|
+
* all, as opposed to whether the answer happens to be yes.
|
|
3261
|
+
*
|
|
3262
|
+
* The distinction is the difference between a maintenance surface that reads as attentive and one that reads as
|
|
3263
|
+
* generic. "Re-read the documentation against the code" in a repository with no documentation is not a chore
|
|
3264
|
+
* that is currently clear — it is a chore that will never make sense here, and showing it teaches the owner that
|
|
3265
|
+
* this list was written by someone who had not looked. Same for a Docker chore with no Dockerfile, or a CI chore
|
|
3266
|
+
* with no pipeline.
|
|
3267
|
+
*
|
|
3268
|
+
* These are all paths, deliberately: presence of a FILE is checkable, cheap, and cannot be argued with, which is
|
|
3269
|
+
* the same evidence-over-identity rule the extension activation facts follow. Every field is a list rather than a
|
|
3270
|
+
* boolean where the paths themselves are worth showing — a chore that says "not applicable: no Dockerfile" is
|
|
3271
|
+
* useful, and one that says "3 Dockerfiles: ./Dockerfile, _apps/web/Dockerfile, …" is more so. */
|
|
3272
|
+
export const ChoreShapeSchema = z.object({
|
|
3273
|
+
// Architecture documents that actually exist (docs/architecture/**/doc.md), capped — the count is what
|
|
3274
|
+
// matters, and the drift survey needs to know there is something to re-read.
|
|
3275
|
+
docs: z.array(z.string()),
|
|
3276
|
+
dockerfiles: z.array(z.string()),
|
|
3277
|
+
// CI pipeline definitions: .github/workflows/*.yml, .gitlab-ci.yml, and the other single-file conventions.
|
|
3278
|
+
ci: z.array(z.string()),
|
|
3279
|
+
// Whether dependencies are resolved to a lockfile — what makes an audit mean anything.
|
|
3280
|
+
lockfile: z.boolean(),
|
|
3281
|
+
// A package.json at the repo root. The gate for every chore whose subject is the JavaScript dependency tree:
|
|
3282
|
+
// a Rust or Go repository has no majors to be behind on and no engines field to be pinned by, and offering it
|
|
3283
|
+
// those chores would be this surface guessing at what it is looking at.
|
|
3284
|
+
packageManifest: z.boolean(),
|
|
3285
|
+
});
|
|
3286
|
+
export type ChoreShape = z.infer<typeof ChoreShapeSchema>;
|
|
3287
|
+
|
|
3288
|
+
export const ChoreSignalsSchema = z.object({
|
|
3289
|
+
packages: z.array(ChorePackageSchema),
|
|
3290
|
+
shape: ChoreShapeSchema,
|
|
3291
|
+
hotspots: z.array(WorkspaceHotspotSchema),
|
|
3292
|
+
keyModules: z.array(WorkspaceKeyModuleSchema),
|
|
3293
|
+
totals: z.object({ files: z.number(), symbols: z.number(), complexity: z.number(), hotspots: z.number() }),
|
|
3294
|
+
// Whether the index these rankings came from is current. A chore must not fire on a half-built index, and
|
|
3295
|
+
// this is how the browser knows to hold its verdict rather than act on a partial ranking.
|
|
3296
|
+
indexed: z.boolean(),
|
|
3297
|
+
});
|
|
3298
|
+
export type ChoreSignals = z.infer<typeof ChoreSignalsSchema>;
|
|
3299
|
+
|
|
3300
|
+
// What a finished chore turn left behind — written by the agent, read back to decide whether the chore is still
|
|
3301
|
+
// due. `clean` is the load-bearing one: an agent that looked and found the tool's findings to be false positives
|
|
3302
|
+
// must be able to say so, or the next poll starts the same turn again forever.
|
|
3303
|
+
export const ChoreOutcomeSchema = z.enum(["acted", "reported", "clean"]);
|
|
3304
|
+
export type ChoreOutcome = z.infer<typeof ChoreOutcomeSchema>;
|
|
3305
|
+
|
|
3306
|
+
/* One chore's history in one repo. The DIGEST is what makes this a debounce rather than a suppression: it is a
|
|
3307
|
+
* hash of the evidence that was standing when the turn ran, so a chore whose evidence has since changed is due
|
|
3308
|
+
* again on its own merits while one whose evidence is unchanged stays quiet — with the run still visible in the
|
|
3309
|
+
* panel, saying when it ran and what it concluded. Nothing here can hide a chore from the view; it only decides
|
|
3310
|
+
* whether the rail is allowed to speak. */
|
|
3311
|
+
export const ChoreLedgerEntrySchema = z.object({
|
|
3312
|
+
repo: z.string(),
|
|
3313
|
+
chore: z.string(),
|
|
3314
|
+
ranAt: z.number(),
|
|
3315
|
+
runId: z.string(),
|
|
3316
|
+
outcome: ChoreOutcomeSchema,
|
|
3317
|
+
digest: z.string(),
|
|
3318
|
+
// Set by the owner from the panel — the chore stays visible and stays out of the badge until this passes.
|
|
3319
|
+
// Distinct from opting out, which is the absence of the chore from `enabled` in the sandbox's settings.
|
|
3320
|
+
snoozedUntil: z.number().optional(),
|
|
3321
|
+
});
|
|
3322
|
+
export type ChoreLedgerEntry = z.infer<typeof ChoreLedgerEntrySchema>;
|
|
3323
|
+
|
|
3324
|
+
// GET /chores — every discovered repo's standing evidence, plus the ledger, in one read. One route rather than
|
|
3325
|
+
// one per repo because the rail badge scans ALL of them on a timer, and N requests a minute to answer "is
|
|
3326
|
+
// anything due" is the kind of poll that shows up in a battery graph.
|
|
3327
|
+
export const ChoresReportSchema = z.object({
|
|
3328
|
+
repos: z.array(z.object({ repo: z.string(), probes: z.array(ProbeResultSchema), signals: ChoreSignalsSchema })),
|
|
3329
|
+
ledger: z.array(ChoreLedgerEntrySchema),
|
|
3330
|
+
// The daemon's own runtime, for the chore that asks whether this sandbox is running something end-of-life.
|
|
3331
|
+
// Read off the process rather than a manifest: what is INSTALLED is the fact that matters, and an `engines`
|
|
3332
|
+
// range is a wish.
|
|
3333
|
+
node: z.string(),
|
|
3334
|
+
});
|
|
3335
|
+
export type ChoresReport = z.infer<typeof ChoresReportSchema>;
|
|
3336
|
+
|
|
3337
|
+
// POST /chores/probe — force one probe to re-run now, ahead of its TTL. Returns immediately; the runner does the
|
|
3338
|
+
// work and the next GET /chores carries the result, the same shape the panel already polls.
|
|
3339
|
+
export const ChoreProbeRequestSchema = z.object({ repo: z.string().min(1), id: ProbeIdSchema });
|
|
3340
|
+
// POST /chores/ledger — record a run, or snooze. Written daemon-side rather than by the browser so a chore turn
|
|
3341
|
+
// started from anywhere (the panel, an automation, the agent itself) lands in one ledger.
|
|
3342
|
+
export const ChoreLedgerWriteSchema = ChoreLedgerEntrySchema;
|