@intentic/sandbox-contract 1.309.0 → 1.310.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.
Files changed (101) hide show
  1. package/dist/contracts/settings.contract.d.ts +3 -15
  2. package/dist/contracts/settings.contract.d.ts.map +1 -1
  3. package/dist/index.d.ts +33 -73
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js +0 -3
  6. package/dist/index.js.map +1 -1
  7. package/dist/protocol/upgrade-relay.d.ts +4 -0
  8. package/dist/protocol/upgrade-relay.d.ts.map +1 -0
  9. package/dist/protocol/upgrade-relay.js +25 -0
  10. package/dist/protocol/upgrade-relay.js.map +1 -0
  11. package/dist/schemas/ci.d.ts.map +1 -1
  12. package/dist/schemas/ci.js.map +1 -1
  13. package/dist/schemas/metrics.d.ts +24 -25
  14. package/dist/schemas/metrics.d.ts.map +1 -1
  15. package/dist/schemas/metrics.js +6 -31
  16. package/dist/schemas/metrics.js.map +1 -1
  17. package/dist/schemas/settings.d.ts +9 -29
  18. package/dist/schemas/settings.d.ts.map +1 -1
  19. package/dist/schemas/settings.js +25 -11
  20. package/dist/schemas/settings.js.map +1 -1
  21. package/dist/state/definition.d.ts +8 -20
  22. package/dist/state/definition.d.ts.map +1 -1
  23. package/dist/state/workspace-state.d.ts +0 -7
  24. package/dist/state/workspace-state.d.ts.map +1 -1
  25. package/dist/state/workspace-state.js +0 -8
  26. package/dist/state/workspace-state.js.map +1 -1
  27. package/package.json +16 -5
  28. package/src/chores/chores.test.ts +0 -1
  29. package/src/chores/digest.test.ts +0 -1
  30. package/src/chores/probes.test.ts +0 -1
  31. package/src/chores/stack.test.ts +0 -1
  32. package/src/chores/verdict.test.ts +0 -1
  33. package/src/events/agent-words.test.ts +0 -1
  34. package/src/events/resume.test.ts +0 -1
  35. package/src/events/watch-wake.test.ts +0 -1
  36. package/src/ids/conversation-ids.test.ts +1 -2
  37. package/src/ids/hostnames.test.ts +0 -1
  38. package/src/ids/share-paths.test.ts +0 -1
  39. package/src/ids/tunnel-ids.test.ts +0 -1
  40. package/src/index.ts +0 -3
  41. package/src/models/agent-catalog.test.ts +0 -1
  42. package/src/models/capability-ledger.test.ts +0 -1
  43. package/src/models/model-label.test.ts +0 -1
  44. package/src/models/model-offer.test.ts +0 -1
  45. package/src/models/model-order.test.ts +0 -1
  46. package/src/models/model-pins.test.ts +0 -1
  47. package/src/models/model-roles.test.ts +0 -1
  48. package/src/models/plan-pools.test.ts +0 -1
  49. package/src/models/provider-specs.test.ts +0 -1
  50. package/src/policy/batch-runs.test.ts +0 -1
  51. package/src/policy/command-classes.test.ts +0 -1
  52. package/src/policy/credential-material.test.ts +0 -1
  53. package/src/policy/fence-paths.test.ts +0 -1
  54. package/src/policy/overlay-lint.test.ts +0 -1
  55. package/src/policy/owner-ticket.test.ts +0 -1
  56. package/src/policy/persona-home.test.ts +0 -1
  57. package/src/policy/reserved-servers.test.ts +0 -1
  58. package/src/policy/safety-policy.test.ts +0 -1
  59. package/src/policy/search-globs.test.ts +0 -1
  60. package/src/protocol/container-requirements.test.ts +0 -1
  61. package/src/protocol/ingress-contract.test.ts +0 -1
  62. package/src/protocol/ingress-protocol.test.ts +0 -1
  63. package/src/protocol/peer-dial.test.ts +1 -2
  64. package/src/protocol/peer-mcp-server.test.ts +0 -1
  65. package/src/protocol/routes.test.ts +6 -3
  66. package/src/protocol/tunnel-heartbeat.test.ts +2 -3
  67. package/src/protocol/upgrade-relay.ts +28 -0
  68. package/src/schemas/agent.test.ts +0 -1
  69. package/src/schemas/areas.test.ts +1 -2
  70. package/src/schemas/ci.ts +3 -9
  71. package/src/schemas/context-trim.test.ts +0 -1
  72. package/src/schemas/dev-rebuild-log.test.ts +0 -1
  73. package/src/schemas/git/git.test.ts +0 -1
  74. package/src/schemas/metrics.ts +33 -64
  75. package/src/schemas/settings.test.ts +41 -7
  76. package/src/schemas/settings.ts +48 -44
  77. package/src/schemas/version-seam.test.ts +0 -1
  78. package/src/state/contract-lock.test.ts +0 -1
  79. package/src/state/fix-attempt-plan.test.ts +0 -1
  80. package/src/state/fix-stance.test.ts +0 -1
  81. package/src/state/landed-commit.test.ts +0 -1
  82. package/src/state/runtime-state.test.ts +0 -1
  83. package/src/state/versions.test.ts +0 -1
  84. package/src/state/workspace-state.test.ts +0 -3
  85. package/src/state/workspace-state.ts +0 -14
  86. package/src/text/archives.test.ts +0 -1
  87. package/src/text/documents.test.ts +0 -1
  88. package/src/text/embed.test.ts +2 -3
  89. package/src/text/emoji.test.ts +0 -1
  90. package/src/text/mentions.test.ts +0 -1
  91. package/src/text/path-refs.test.ts +0 -1
  92. package/src/text/title.test.ts +0 -1
  93. package/src/text/transcript-fold.test.ts +12 -3
  94. package/src/text/whisper.test.ts +0 -1
  95. package/src/text/workflow-faults.test.ts +0 -1
  96. package/src/time/zone.test.ts +0 -1
  97. package/dist/contracts/prepush.contract.d.ts +0 -32
  98. package/dist/contracts/prepush.contract.d.ts.map +0 -1
  99. package/dist/contracts/prepush.contract.js +0 -41
  100. package/dist/contracts/prepush.contract.js.map +0 -1
  101. package/src/contracts/prepush.contract.ts +0 -58
@@ -59,9 +59,7 @@ export const SandboxUsageSchema = z.object({
59
59
  .optional()
60
60
  .describe("CPU the whole sandbox used over the window, as a percentage of all it may use (`cores`). Absent on a first reading."),
61
61
  cores: z.number().describe("How many cores the sandbox may use: its CPU quota, or every core it is allowed to run on when it has none."),
62
- memoryBytes: z
63
- .number()
64
- .describe("Memory in use, less the file cache the kernel takes back on demand: the figure that runs into the limit."),
62
+ memoryBytes: z.number().describe("Memory in use, less the file cache the kernel takes back on demand: the figure that runs into the limit."),
65
63
  memoryLimitBytes: z.number().describe("The memory limit: the container's own, or the machine's memory when the container has none."),
66
64
  swapBytes: z.number().optional().describe("Memory pushed out to swap. Absent where the sandbox cannot see its own swap."),
67
65
  diskBytes: z.number().optional().describe("Space used on the volume the workspace lives on. Absent when the volume would not say."),
@@ -70,7 +68,9 @@ export const SandboxUsageSchema = z.object({
70
68
  .tuple([z.number(), z.number(), z.number()])
71
69
  .describe("The load average over 1, 5 and 15 minutes. It is the machine's, so other sandboxes on it count too."),
72
70
  processes: z.number().describe("How many processes are running in the sandbox."),
73
- pressure: PressureMetricsSchema.optional().describe("How much work waited on CPU, memory or disk lately. Absent where the kernel does not report it."),
71
+ pressure: PressureMetricsSchema.optional().describe(
72
+ "How much work waited on CPU, memory or disk lately. Absent where the kernel does not report it.",
73
+ ),
74
74
  });
75
75
  export type SandboxUsage = z.infer<typeof SandboxUsageSchema>;
76
76
 
@@ -109,89 +109,58 @@ export type SandboxMetrics = z.infer<typeof SandboxMetricsSchema>;
109
109
  // What is filling the sandbox's disk, by what it is for (GET /system/storage). Sizes are apparent bytes, and a file
110
110
  // with several hard links counts once, toward the first place the scan met it.
111
111
 
112
- // Every category a byte on the sandbox's volumes can belong to; STORAGE_CLEANABILITY below says which may be cleaned.
113
- export const STORAGE_CATEGORIES = [
114
- // The workspace's own files: repositories, their dependencies, anything the owner or an agent put there.
115
- "workspace",
116
- // Transcripts, runtime session stores, attachments and loop memory of every conversation, archived ones included.
117
- "conversations",
118
- // Each open conversation's own checkout of the repositories, and the dependencies its turns installed over them.
119
- "checkouts",
120
- // The minute-by-minute and per-turn snapshots the History timeline restores from.
121
- "restorePoints",
122
- // The repositories' own git data: every commit and branch, pushed or not.
123
- "repositories",
124
- // Installed versions of the agent runtimes: the one turns use and the one kept for Revert.
125
- "engines",
126
- // Search indexes and caches the sandbox keeps up to date by itself while it runs.
127
- "indexes",
128
- // Installed extensions and their scratch.
129
- "extensions",
130
- // The sandbox's own Docker: images, containers, volumes.
131
- "docker",
132
- // Settings, credentials, sign-ins and the ledgers of what was spent and done.
133
- "state",
134
- // Anything no part of the sandbox claims.
135
- "other",
136
- // Git data and checkouts of repositories deleted from the workspace.
137
- "trash",
138
- // Copies of the sandbox packed for download.
139
- "exports",
140
- // Files agents produced: generated images, reports, harness output.
141
- "artifacts",
142
- // Screenshots and page snapshots the agent's browser took.
143
- "browserCaptures",
144
- // The agent browser's profiles, with the sites it is signed in to.
145
- "browserProfiles",
146
- // Weights downloaded for local models.
147
- "modelWeights",
148
- // The sandbox's own logs and terminal captures.
149
- "logs",
150
- // Scratch space agents and checks leave behind.
151
- "scratch",
152
- // Package managers' content stores, which the next install refills.
153
- "packageStores",
154
- // Build tools' output caches.
155
- "buildCaches",
156
- ] as const;
157
- export const StorageCategoryIdSchema = z.enum(STORAGE_CATEGORIES);
158
- export type StorageCategoryId = z.infer<typeof StorageCategoryIdSchema>;
159
-
160
112
  // none: never removed from here; safe: comes back by itself, removed without asking; confirm: removed only once the
161
113
  // owner has read what it costs.
162
114
  export const StorageCleanabilitySchema = z.enum(["none", "safe", "confirm"]);
163
115
  export type StorageCleanability = z.infer<typeof StorageCleanabilitySchema>;
164
116
 
165
- // Which categories may be cleaned, and which ask first: the daemon's cleaner refuses by it, the editor words its
166
- // confirm by it. State, credentials and live work are `none`; what regenerates is `safe`; the owner's own data asks.
117
+ // Every category a byte on the volumes can belong to, and its cleanability: the cleaner refuses by it, the editor words its confirm by it.
167
118
  export const STORAGE_CLEANABILITY = {
119
+ // The workspace's own files: repositories, their dependencies, anything the owner or an agent put there.
168
120
  workspace: "none",
121
+ // Transcripts, runtime session stores, attachments and loop memory of every conversation, archived ones included.
169
122
  conversations: "none",
170
- // A live conversation's working copy; archiving the conversation is what frees it.
123
+ // Each open conversation's own checkout of the repositories and the dependencies its turns installed; archiving the conversation frees it.
171
124
  checkouts: "none",
172
- // No compaction is safe to offer: `git gc --auto` already runs after every snapshot, and every object is reachable.
125
+ // Per-turn and minute-by-minute snapshots History restores from; no compaction to offer: `git gc --auto` runs after each, all is reachable.
173
126
  restorePoints: "none",
127
+ // The repositories' own git data: every commit and branch, pushed or not.
174
128
  repositories: "none",
175
- // The Engines card keeps two versions and owns which is which; removing one here would strand its Revert.
129
+ // Installed agent runtimes, the one turns use and the one kept for Revert; the Engines card owns which is which, so removing one strands it.
176
130
  engines: "none",
177
- // Held open by what maintains them: removing a file frees nothing until they close it.
131
+ // Search indexes and caches the sandbox keeps up to date while it runs, held open by it: removing a file frees nothing until it closes.
178
132
  indexes: "none",
133
+ // Installed extensions and their scratch.
179
134
  extensions: "none",
135
+ // The sandbox's own Docker: images, containers, volumes.
180
136
  docker: "none",
137
+ // Settings, credentials, sign-ins and the ledgers of what was spent and done.
181
138
  state: "none",
139
+ // Anything no part of the sandbox claims.
182
140
  other: "none",
183
- // Nothing reads it again, but it may hold commits never pushed anywhere.
141
+ // Git data and checkouts of repositories deleted from the workspace; nothing reads it again, but it may hold commits never pushed.
184
142
  trash: "confirm",
143
+ // Copies of the sandbox packed for download.
185
144
  exports: "confirm",
145
+ // Files agents produced: generated images, reports, harness output.
186
146
  artifacts: "confirm",
147
+ // Screenshots and page snapshots the agent's browser took.
187
148
  browserCaptures: "confirm",
149
+ // The agent browser's profiles, with the sites it is signed in to.
188
150
  browserProfiles: "confirm",
151
+ // Weights downloaded for local models.
189
152
  modelWeights: "confirm",
153
+ // The sandbox's own logs and terminal captures.
190
154
  logs: "safe",
155
+ // Scratch space agents and checks leave behind.
191
156
  scratch: "safe",
157
+ // Package managers' content stores, which the next install refills.
192
158
  packageStores: "safe",
159
+ // Build tools' output caches.
193
160
  buildCaches: "safe",
194
- } as const satisfies Record<StorageCategoryId, StorageCleanability>;
161
+ } as const satisfies Record<string, StorageCleanability>;
162
+ export type StorageCategoryId = keyof typeof STORAGE_CLEANABILITY;
163
+ export const StorageCategoryIdSchema = z.enum(Object.keys(STORAGE_CLEANABILITY) as StorageCategoryId[]);
195
164
 
196
165
  export const StorageItemSchema = z.object({
197
166
  path: z.string().describe("Where it is, as an absolute path inside the sandbox."),
@@ -245,11 +214,11 @@ export type StorageCleanInput = z.infer<typeof StorageCleanInputSchema>;
245
214
 
246
215
  export const StorageCleanResultSchema = z.object({
247
216
  category: StorageCategoryIdSchema,
248
- freedBytes: z.number().describe("Space the removals gave back, in bytes. A file that is still linked elsewhere frees nothing and is not counted."),
249
- removed: z.number().describe("How many items were removed."),
250
- kept: z
217
+ freedBytes: z
251
218
  .number()
252
- .describe("How many were left in place: changed too recently, in use by a running program, or no longer this category's."),
219
+ .describe("Space the removals gave back, in bytes. A file that is still linked elsewhere frees nothing and is not counted."),
220
+ removed: z.number().describe("How many items were removed."),
221
+ kept: z.number().describe("How many were left in place: changed too recently, in use by a running program, or no longer this category's."),
253
222
  failed: z.number().describe("How many removals the filesystem refused."),
254
223
  });
255
224
  export type StorageCleanResult = z.infer<typeof StorageCleanResultSchema>;
@@ -1,5 +1,4 @@
1
- import { describe, test, expect } from "bun:test";
2
- import { RuleSchema } from "./settings.js";
1
+ import { RepoChecksFileSchema, RuleSchema, SandboxSettingsSchema } from "./settings.js";
3
2
 
4
3
  // A rule that saves cleanly and silently does nothing is the failure this schema exists to refuse: an action, and a
5
4
  // built-in's name, are checked against the moment they are written at.
@@ -12,14 +11,49 @@ describe(`which built-in stands at which moment`, () => {
12
11
  expect(rule(`turn.ending`, { kind: `builtin`, name: `version-landed` }).success).toBe(false);
13
12
  });
14
13
 
15
- test(`the verifiers stand at the turn's end and not at the landing`, () => {
16
- expect(rule(`turn.ending`, { kind: `builtin`, name: `verify-edits` }).success).toBe(true);
17
- expect(rule(`agent.landed`, { kind: `builtin`, name: `verify-edits` }).success).toBe(false);
14
+ test(`the look stands at the turn's end and not at the landing`, () => {
15
+ expect(rule(`turn.ending`, { kind: `builtin`, name: `verify-ui-edits` }).success).toBe(true);
16
+ expect(rule(`agent.landed`, { kind: `builtin`, name: `verify-ui-edits` }).success).toBe(false);
18
17
  });
19
18
 
20
- test(`the landed moment takes no verdict, command or instruction: the work is already in the tree`, () => {
19
+ test(`the landed moment takes no verdict and no command: the work is already in the tree`, () => {
21
20
  expect(rule(`agent.landed`, { kind: `verdict`, verdict: `allow` }).success).toBe(false);
22
21
  expect(rule(`agent.landed`, { kind: `command`, command: `pnpm test` }).success).toBe(false);
23
- expect(rule(`agent.landed`, { kind: `instruct`, text: `well done` }).success).toBe(false);
22
+ });
23
+
24
+ test(`there is no push moment: the repository's own pre-push hook gates a push`, () => {
25
+ expect(rule(`push.starting`, { kind: `command`, command: `pnpm test` }).success).toBe(false);
26
+ });
27
+ });
28
+
29
+ describe(`where a command lives`, () => {
30
+ test(`settings refuse a command rule, since it belongs to the repository it checks`, () => {
31
+ const command = { id: `lint`, label: `lint`, moment: `file.edited`, action: { kind: `command`, command: `oxlint {file}` } };
32
+ expect(SandboxSettingsSchema.safeParse({ rules: [command] }).success).toBe(false);
33
+ const land = { id: `auto-land`, label: `land`, moment: `agent.finished`, action: { kind: `verdict`, verdict: `allow` } };
34
+ expect(SandboxSettingsSchema.safeParse({ rules: [land] }).success).toBe(true);
35
+ });
36
+
37
+ test(`a repository declares edit, turn and land checks, and no push check`, () => {
38
+ const file = (checks: unknown[]) => RepoChecksFileSchema.safeParse({ checks }).success;
39
+ expect(
40
+ file([
41
+ { when: `edit`, run: `lint {file}` },
42
+ { when: `turn`, run: `pnpm verify:turn` },
43
+ { when: `land`, run: `pnpm verify` },
44
+ ]),
45
+ ).toBe(true);
46
+ expect(file([{ when: `push`, run: `pnpm verify:push` }])).toBe(false);
47
+ });
48
+
49
+ test(`a land check is one per repository and narrows by no path`, () => {
50
+ const file = (checks: unknown[]) => RepoChecksFileSchema.safeParse({ checks }).success;
51
+ expect(
52
+ file([
53
+ { when: `land`, run: `pnpm verify` },
54
+ { when: `land`, run: `pnpm test` },
55
+ ]),
56
+ ).toBe(false);
57
+ expect(file([{ when: `land`, run: `pnpm verify`, paths: [`src/**`] }])).toBe(false);
24
58
  });
25
59
  });
@@ -13,39 +13,28 @@ export const SystemPromptModeSchema = z.enum(["intentic", "claude", "custom"]);
13
13
  export type SystemPromptMode = z.infer<typeof SystemPromptModeSchema>;
14
14
  // Excludes "custom": there is nothing to fetch, it's whatever the owner already typed into the settings field.
15
15
  export const BuiltinPromptSchema = z.object({ base: z.enum(["intentic", "claude"]) });
16
- // Rules: "at this moment, if this is true, do this" — one table replacing three settings that were the same idea built
17
- // three ways; a fourth is now a row here, not a release. Moments are named to match `WorkspaceEventKind`, so folding
18
- // chores in later won't rename what users wrote.
19
-
20
- // Five places the daemon already stops to decide something; this names those decisions rather than inventing new ones.
16
+ // Rules: "at this moment, if this is true, do this". The owner's rules decide (land, hold, version) and switch the
17
+ // daemon's built-in reviews; every command a moment runs is a repository's own check (`<repo>/.intentic/checks.json`),
18
+ // compiled into this same table by the daemon, so no command lives in settings.
21
19
  export const RuleMomentSchema = z.enum([
22
20
  // A command here runs on the just-written file (`{file}` is its path); the cheapest moment to catch a defect.
23
21
  "file.edited",
24
22
  // The assistant is about to stop. A rule here can send it back to work, which is the only moment that can.
25
23
  "turn.ending",
26
- // Code is about to leave the machine. A rule here gates the push on its own exit code.
27
- "push.starting",
28
24
  // An agent's turn is over and its delta is sitting on its branch. A rule here decides whether it lands.
29
25
  "agent.finished",
30
26
  // An agent's delta has just reached the main tree. A rule here decides what becomes of it there.
31
27
  "agent.landed",
32
28
  ]);
33
29
  export type RuleMoment = z.infer<typeof RuleMomentSchema>;
34
- // What a rule does; the split is functional since the three settings this table replaces each needed a different shape.
35
- // command: runs a shell command; its exit code is the verdict.
36
- // instruct: says something to the assistant before it finishes.
37
- // verdict: allows or holds what is about to happen (a pass with nothing to run, told which way to go).
38
- // builtin: invokes a named daemon behaviour the table has no business expressing itself.
39
- // Each reads a record only the daemon keeps, which is what makes it a built-in rather than a command.
40
- // verify-edits: what a turn edited, against what it ran.
41
- // verify-removals: what a turn deleted, against the repo's own history (a `git log` question, not a shell one-liner).
30
+ // command: a repository's check; its exit code is the verdict.
31
+ // verdict: allows or holds what is about to happen.
32
+ // builtin: a daemon behaviour that reads a record only the daemon keeps.
42
33
  // verify-ui-edits: rendered surfaces a turn changed, against whether it ever looked at one.
43
- // verify-tests: whether a touched test's assertions got weaker than at HEAD, or would have passed before its own
44
- // change.
45
34
  // version-landed: commits what a land brought, in the main tree, under the subject drafted for it, and the tree's own
46
35
  // remainder before the next isolated turn starts; the only way a person who never commits keeps every agent seeing
47
36
  // the latest tree, since worktrees are cut from HEAD.
48
- export const RuleBuiltinSchema = z.enum(["verify-edits", "verify-removals", "verify-ui-edits", "verify-tests", "version-landed"]);
37
+ export const RuleBuiltinSchema = z.enum(["verify-ui-edits", "version-landed"]);
49
38
  export type RuleBuiltin = z.infer<typeof RuleBuiltinSchema>;
50
39
  export const RuleActionSchema = z.discriminatedUnion("kind", [
51
40
  z.object({
@@ -54,7 +43,6 @@ export const RuleActionSchema = z.discriminatedUnion("kind", [
54
43
  // Past this, the process group is killed and the run is `failed`, never a silent pass.
55
44
  timeoutMs: z.number().min(60_000).max(3_600_000).default(900_000),
56
45
  }),
57
- z.object({ kind: z.literal("instruct"), text: z.string().min(1).max(4000) }),
58
46
  z.object({ kind: z.literal("verdict"), verdict: z.enum(["allow", "hold"]) }),
59
47
  z.object({ kind: z.literal("builtin"), name: RuleBuiltinSchema }),
60
48
  ]);
@@ -63,8 +51,7 @@ export type RuleAction = z.infer<typeof RuleActionSchema>;
63
51
  // defaults to HELD unless a rule explicitly allows it.
64
52
  export const RuleOutcomeSchema = z.enum(["clean", "error", "conflict", "checks-failed"]);
65
53
  export type RuleOutcome = z.infer<typeof RuleOutcomeSchema>;
66
- // Three keys covering "only this repo" and "skip docs-only changes" without a query language; every key absent means
67
- // the rule always matches.
54
+ // Every key absent means the rule always matches.
68
55
  export const RuleConditionSchema = z.object({
69
56
  // A workspace repo id, or "root". Absent ⇒ any.
70
57
  repo: z.string().min(1).optional(),
@@ -80,15 +67,13 @@ export type RuleCondition = z.infer<typeof RuleConditionSchema>;
80
67
  // validated here, not left to the consumer — the alternative is a rule that saves cleanly and silently does nothing.
81
68
  const MOMENT_ACTIONS: Record<RuleMoment, readonly RuleAction["kind"][]> = {
82
69
  "file.edited": ["command"],
83
- "turn.ending": ["builtin", "instruct", "command"],
84
- "push.starting": ["command"],
70
+ "turn.ending": ["builtin", "command"],
85
71
  "agent.finished": ["verdict"],
86
72
  "agent.landed": ["builtin"],
87
73
  };
88
- // A built-in reads a record only one moment keeps, so it stands at that moment alone: the verifiers read a turn's
89
- // ledgers, the versioner reads a landing's claim.
74
+ // A built-in reads a record only one moment keeps, so it stands at that moment alone.
90
75
  const MOMENT_BUILTINS: Partial<Record<RuleMoment, readonly RuleBuiltin[]>> = {
91
- "turn.ending": ["verify-edits", "verify-removals", "verify-ui-edits", "verify-tests"],
76
+ "turn.ending": ["verify-ui-edits"],
92
77
  "agent.landed": ["version-landed"],
93
78
  };
94
79
  export const RuleSchema = z
@@ -429,14 +414,17 @@ export const SandboxSettingsSchema = z.object({
429
414
  .describe(
430
415
  "Which repositories may run the checks they declare for themselves, and exactly which version of those checks you agreed to. A repository's declaration does nothing until it appears here, the same rule git keeps for hooks, which are never cloned; and a declaration that changes afterwards is held until you look at it again.",
431
416
  ),
432
- // Lives in the owner's own settings, not the workspace: a rule can hold work and gate a push, so it answers to the
433
- // sandbox owner alone. A repository may declare a COMMAND of its own (see `adoptedChecks`), never a verdict.
417
+ // The owner's alone, since a rule can hold work. A command is refused here: it belongs to the repository it checks,
418
+ // in that repository's `.intentic/checks.json`, never in settings.
434
419
  rules: z
435
420
  .array(RuleSchema)
436
421
  .max(50)
437
422
  .default([])
423
+ .refine((rules) => rules.every((rule) => rule.action.kind !== "command"), {
424
+ message: "a command belongs in the repository's own .intentic/checks.json, not in settings",
425
+ })
438
426
  .describe(
439
- "Standing instructions you give the sandbox about its own work: ask for proof before a turn ends, run something before a push, hold or release finished work. Empty is the default and is exactly the behaviour of a fresh sandbox, because each of those defaults is what no rule matched means at its own moment.",
427
+ "Standing decisions about the sandbox's own work: land or hold finished work, save a version of what landed, ask the assistant to look at an interface it changed. Empty is the default and is exactly the behaviour of a fresh sandbox, because each of those defaults is what no rule matched means at its own moment. A command to run is a repository's own check, declared in its .intentic/checks.json.",
440
428
  ),
441
429
  automationFailureLimit: z
442
430
  .number()
@@ -608,27 +596,23 @@ export type SavingsReport = z.infer<typeof SavingsReportSchema>;
608
596
  // spelling for the daemon that reads it, the screen that names it and the demo that mimics it.
609
597
  export const REPO_CHECKS_FILE = `${STATE_DIR}/checks.json`;
610
598
 
611
- // Named for the occasion as a repository would say it, not for the daemon's wire moment: `edit` is `file.edited`,
612
- // `turn` is `turn.ending` and `push` is `push.starting` (rules/repo-checks.ts maps them). Three, because these are the
613
- // occasions whose command a repository actually owns; a verdict moment has nothing here to express.
614
- //
615
- // `edit` is the cheapest of the three and the only one that reaches the model while it still holds the line it wrote:
616
- // the command runs on that one file (`{file}`), and its output rides back in the edit's own response. A repository
617
- // that declares a whole-tree command here pays it per edit, so the command has to take the file.
618
- export const RepoCheckMomentSchema = z.enum(["edit", "turn", "push"]);
599
+ // Named for the occasion as a repository would say it: `edit` is `file.edited`, `turn` is `turn.ending`, and `land` is
600
+ // the daemon's run over the main tree after an install a land caused (rules/repo-checks.ts maps them). A push is gated by
601
+ // the repository's own git pre-push hook, which every push runs, the app's included.
602
+ // `edit` runs on one file (`{file}`) and its output rides back in the edit's own response, so a command declared there is
603
+ // paid per edit and has to take the file.
604
+ export const RepoCheckMomentSchema = z.enum(["edit", "turn", "land"]);
619
605
  export type RepoCheckMoment = z.infer<typeof RepoCheckMomentSchema>;
620
606
 
621
607
  export const RepoCheckSchema = z.object({
622
608
  when: RepoCheckMomentSchema.describe(
623
- "When to run it: `edit` on each file as it is written (`{file}` is its path), `turn` before the assistant finishes, `push` before code leaves the machine.",
609
+ "When to run it: `edit` on each file as it is written (`{file}` is its path), `turn` before the assistant finishes, `land` on the main tree after finished work lands (absent, the package's own verify or test script runs there).",
624
610
  ),
625
611
  run: z.string().min(1).max(500).describe("The command, run in this repository's own directory, so it reads as it would in a terminal there."),
626
612
  label: z.string().min(1).max(80).optional().describe("What to call it on screen. Absent names it after the command."),
627
- // Same ceiling as a rule's own command; past it the process group is killed and the run is a failure, never a
628
- // silent pass.
613
+ // Same ceiling as a rule's own command; past it the process group is killed and the run is a failure.
629
614
  timeoutMs: z.number().min(60_000).max(3_600_000).optional().describe("How long it may take before it is killed and counted as failed."),
630
- // Repo-relative, as anybody reading this file would write them; the daemon prefixes the repo id before matching,
631
- // since a rule's globs are workspace-relative.
615
+ // Repo-relative; the daemon prefixes the repo id before matching, since a rule's globs are workspace-relative.
632
616
  paths: z
633
617
  .array(z.string().min(1))
634
618
  .max(20)
@@ -638,7 +622,16 @@ export const RepoCheckSchema = z.object({
638
622
  export type RepoCheck = z.infer<typeof RepoCheckSchema>;
639
623
 
640
624
  // The file itself. One key, so a second concern can be added later without breaking a file anyone has written.
641
- export const RepoChecksFileSchema = z.object({ checks: z.array(RepoCheckSchema).max(10).default([]) });
625
+ export const RepoChecksFileSchema = z.object({
626
+ checks: z
627
+ .array(RepoCheckSchema)
628
+ .max(10)
629
+ .default([])
630
+ // One land check per repository: it is THE verdict on the main tree the push hook replays, not one of several.
631
+ .refine((checks) => checks.filter((check) => check.when === "land").length <= 1, { message: "a repository declares at most one land check" })
632
+ // A land checks the whole tree an install settled, not a change, so there is nothing for a glob to narrow.
633
+ .refine((checks) => checks.every((check) => check.when !== "land" || check.paths === undefined), { message: "a land check takes no paths" }),
634
+ });
642
635
  export type RepoChecksFile = z.infer<typeof RepoChecksFileSchema>;
643
636
 
644
637
  // One repository, as a screen reads it: what it declares, and where that stands with the owner.
@@ -646,6 +639,11 @@ export const RepoChecksSummarySchema = z.object({
646
639
  repo: z.string().describe('Which repository, by its workspace id ("root" is the workspace itself).'),
647
640
  path: z.string().describe("Where the declaration lives, relative to the workspace, whether or not the file exists yet."),
648
641
  checks: z.array(RepoCheckSchema).describe("What it declares, in the order the file lists them."),
642
+ fired: z
643
+ .array(z.number().nullable())
644
+ .describe(
645
+ "When each declared check last reported something, in the file's order, as epoch milliseconds; null for one that never has, or for the land check, whose verdict the activity feed records instead.",
646
+ ),
649
647
  adopted: z
650
648
  .boolean()
651
649
  .describe("Whether these are running. False means declared and inert: nothing a repository writes runs until the owner switches it on."),
@@ -655,10 +653,16 @@ export const RepoChecksSummarySchema = z.object({
655
653
  "Whether the declaration changed since it was adopted, which holds it until the owner looks again. True only for a repository that was adopted before.",
656
654
  ),
657
655
  error: z.string().optional().describe("Why the file could not be read, when it exists but does not parse. The checks list is empty in that case."),
656
+ landDefault: z
657
+ .string()
658
+ .optional()
659
+ .describe(
660
+ "What runs on the main tree after a land when the file declares no `land` check: the package's own verify or test script. Absent when the repository has neither.",
661
+ ),
658
662
  });
659
663
  export type RepoChecksSummary = z.infer<typeof RepoChecksSummarySchema>;
660
664
  export const RepoChecksListSchema = z.object({
661
- repos: z.array(RepoChecksSummarySchema).describe("Every repository that declares checks, plus any the owner has adopted before, sorted by id."),
665
+ repos: z.array(RepoChecksSummarySchema).describe("Every repository that declares checks or has a package check that runs after a land, in id order."),
662
666
  });
663
667
  export type RepoChecksList = z.infer<typeof RepoChecksListSchema>;
664
668
  export const RepoChecksAdoptSchema = z.object({
@@ -1,4 +1,3 @@
1
- import { test, expect } from "bun:test";
2
1
  import { CapabilitiesListSchema } from "./capabilities.js";
3
2
  import { SandboxSettingsSchema } from "./settings.js";
4
3
  import { SmbNetdiskConfigSchema } from "./netdisk.js";
@@ -1,7 +1,6 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { packageRoot } from "@intentic/constants/node";
4
- import { test, expect } from "bun:test";
5
4
  import { currentLock } from "./contract-lock.js";
6
5
 
7
6
  // contract.lock.json must match the exported schemas; the git-aware half of this check lives in contract-shrink.mjs.
@@ -1,4 +1,3 @@
1
- import { test, expect } from "bun:test";
2
1
  import { ciFixConversationId, fixAttemptId } from "../ids/conversation-ids.js";
3
2
  import type { AgentSummary } from "../schemas/agents.js";
4
3
  import { planFixAttempt } from "./fix-attempt-plan.js";
@@ -1,4 +1,3 @@
1
- import { test, expect } from "bun:test";
2
1
  import { fixStance } from "./fix-stance.js";
3
2
  import type { AgentSummary } from "../schemas/agents.js";
4
3
 
@@ -1,4 +1,3 @@
1
- import { expect, test } from "bun:test";
2
1
  import { declaredTestNote, landedCommitMessage } from "./landed-commit.js";
3
2
 
4
3
  // The push gate reads a weakening's declaration off the commit (assertion-ratchet.mjs), so the trailer must survive
@@ -1,4 +1,3 @@
1
- import { describe, it, expect } from "bun:test";
2
1
  import { RUNTIME_DOMAIN_BINDINGS, staleRuntimeQueryKeys } from "./runtime-state.js";
3
2
 
4
3
  describe(`staleRuntimeQueryKeys`, () => {
@@ -1,4 +1,3 @@
1
- import { test, expect } from "bun:test";
2
1
  import { DEV_VERSION, isBehind, isNewer } from "./versions.js";
3
2
 
4
3
  test("isNewer compares dotted numeric versions", () => {
@@ -1,6 +1,5 @@
1
1
  import { STATE_DIR } from "@intentic/constants";
2
2
  import type { FileContribution } from "@intentic/extension-manifest";
3
- import { describe, it, expect } from "bun:test";
4
3
  import {
5
4
  BACKED_UP_STATE_PATHS,
6
5
  isLockedWorkspacePath,
@@ -346,8 +345,6 @@ describe(`VERSIONED_STATE_PATHS`, () => {
346
345
  // Which commands count as heavy, and how many may run at once; a shared-box decision worth a `git log`
347
346
  // line.
348
347
  `${STATE_DIR}/config/heavy-commands.json`,
349
- // The exclude list carves entries out by name; an unlisted directory is invisible to git and to land.
350
- `${STATE_DIR}/config/hooks/`,
351
348
  `${STATE_DIR}/config/loop-designs.json`,
352
349
  `${STATE_DIR}/config/personas.json`,
353
350
  // A persona's prompt and reachable skills: the text that decides how it behaves, same reason as its card.
@@ -120,20 +120,6 @@ const STATE_FILES = [
120
120
  // Bash command. `carry`: a property of the workspace, not the machine. `versioned`: raising the limit is a decision
121
121
  // worth a diff.
122
122
  { path: ".intentic/config/heavy-commands.json", invalidates: ["settings"], portability: "carry", versioned: true },
123
- // Scripts the rule table runs, one per reader (settings.json points at them per event, e.g. `file.edited`).
124
- // `versioned`: this is code that runs against every file an agent writes, more consequential than the rule naming
125
- // it. `carry`: authored text, no credential. Invalidates nothing: the settings screen renders the rules from
126
- // settings.json, not these scripts.
127
- {
128
- path: ".intentic/config/hooks/",
129
- invalidates: [],
130
- why: "The settings screen renders the rules that name these scripts, out of settings.json; nothing in the browser reads the scripts themselves.",
131
- portability: "carry",
132
- versioned: true,
133
- // The daemon only runs these scripts by path; it never builds them, so there's no `statePath` call for the
134
- // coverage guard to find.
135
- outsideWriter: "the owner or an agent, authoring them; the daemon only ever RUNS one, by the path a rule's command names",
136
- },
137
123
  // Last-fired stamps for the rule table. `derived`: records what happened on this machine, not carried.
138
124
  {
139
125
  path: ".intentic/local/rule-firings.json",
@@ -1,4 +1,3 @@
1
- import { describe, it, expect } from "bun:test";
2
1
  import { archiveFormat, archivePrefixOf, archiveRootOf, archiveStem, isBrowsableArchive, wrapsItsOwnName } from "./archives.js";
3
2
 
4
3
  describe(`archive names`, () => {
@@ -1,4 +1,3 @@
1
- import { describe, test, expect } from "bun:test";
2
1
  import { documentOf, documentTitle, isDocumentPath, isPlanDocumentPath } from "./documents.js";
3
2
  import { PLAN_DOCUMENTS_DIR } from "../state/workspace-state.js";
4
3
 
@@ -1,5 +1,4 @@
1
1
  import { webcrypto } from "node:crypto";
2
- import { test, expect, beforeAll, mock } from "bun:test";
3
2
  import { stubGlobal, unstubAllGlobals } from "@intentic/testing/bun";
4
3
  import { EmbedError, embedFailure, embedUrl, fetchEmbedJson, solveProofOfWork } from "./embed.js";
5
4
 
@@ -40,12 +39,12 @@ test("a refusal carries the server's own sentence and status, and a bodyless one
40
39
  test("a JSON fetch answers the body or throws the refusal", async () => {
41
40
  stubGlobal(
42
41
  "fetch",
43
- mock(async () => new Response(JSON.stringify({ ok: true }), { status: 200 })),
42
+ jest.fn(async () => new Response(JSON.stringify({ ok: true }), { status: 200 })),
44
43
  );
45
44
  expect(await fetchEmbedJson<{ ok: boolean }>("https://x/y")).toEqual({ ok: true });
46
45
  stubGlobal(
47
46
  "fetch",
48
- mock(async () => new Response(JSON.stringify({ error: "rate limited" }), { status: 429 })),
47
+ jest.fn(async () => new Response(JSON.stringify({ error: "rate limited" }), { status: 429 })),
49
48
  );
50
49
  await expect(fetchEmbedJson("https://x/y")).rejects.toMatchObject({ message: "rate limited", status: 429 });
51
50
  unstubAllGlobals();
@@ -1,4 +1,3 @@
1
- import { describe, it, expect } from "bun:test";
2
1
  import { isSingleEmoji } from "./emoji.js";
3
2
 
4
3
  describe("isSingleEmoji", () => {
@@ -1,4 +1,3 @@
1
- import { describe, it, expect } from "bun:test";
2
1
  import { AgentTurnSchema } from "../schemas/agent.js";
3
2
  import { MENTION_LIMIT, mentionedPathTokens, mentionPaths } from "./mentions.js";
4
3
 
@@ -1,4 +1,3 @@
1
- import { describe, test, expect } from "bun:test";
2
1
  import { isTestPath, rankRefCandidates, referenceTails } from "./path-refs.js";
3
2
 
4
3
  describe("referenceTails", () => {
@@ -1,4 +1,3 @@
1
- import { test, expect } from "bun:test";
2
1
  import { AgentTurnSchema } from "../schemas/agent.js";
3
2
  import { deriveTitle } from "./title.js";
4
3
 
@@ -1,5 +1,4 @@
1
1
  import { STATE_DIR } from "@intentic/constants";
2
- import { describe, it, expect } from "bun:test";
3
2
  import type { AgentEvent } from "../events/agent-events.js";
4
3
  import type { TranscriptPatch, TranscriptRow } from "../events/transcript.js";
5
4
  import { watchWakePrompt } from "../events/watch-wake.js";
@@ -213,9 +212,19 @@ describe("foldTurn", () => {
213
212
  });
214
213
  // An outage nobody armed says so, instead of promising the breaker's retry.
215
214
  const unarmed: AgentEvent[] = [
216
- { kind: "error", code: "provider-outage", message: "Anthropic is down.", autoResume: "available", outage: { retryAt: 1 }, retries: { made: 0, max: 6 } },
215
+ {
216
+ kind: "error",
217
+ code: "provider-outage",
218
+ message: "Anthropic is down.",
219
+ autoResume: "available",
220
+ outage: { retryAt: 1 },
221
+ retries: { made: 0, max: 6 },
222
+ },
217
223
  ];
218
- expect(foldOf("hi", unarmed).at(-1)).toEqual({ role: "notice", text: "Anthropic is down. Nothing is retrying it, so the turn is waiting here." });
224
+ expect(foldOf("hi", unarmed).at(-1)).toEqual({
225
+ role: "notice",
226
+ text: "Anthropic is down. Nothing is retrying it, so the turn is waiting here.",
227
+ });
219
228
  });
220
229
 
221
230
  it("writes the turn's own events down as notices", () => {
@@ -1,4 +1,3 @@
1
- import { test, expect } from "bun:test";
2
1
  import { cleanTranscription } from "./whisper.js";
3
2
 
4
3
  test("flattens whisper output onto one line", () => {
@@ -1,4 +1,3 @@
1
- import { test, expect } from "bun:test";
2
1
  import { type Workflow, WorkflowSchema, type WorkflowStep } from "../schemas/workflows.js";
3
2
  import { workflowFaults, workflowRunFaults } from "./workflow-faults.js";
4
3
 
@@ -1,4 +1,3 @@
1
- import { describe, test, expect } from "bun:test";
2
1
  import { asCivilDay, asZone, civilDayIn, cronOptions, isZone, sameClock, utcDayOf, UTC, zoneLabel } from "./zone.js";
3
2
  import type { Zone } from "./zone.js";
4
3