@intentic/sandbox-contract 1.309.0 → 1.311.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 (200) hide show
  1. package/README.md +38 -159
  2. package/dist/contracts/accounts.contract.d.ts +3 -0
  3. package/dist/contracts/accounts.contract.d.ts.map +1 -1
  4. package/dist/contracts/agent.contract.d.ts +15 -0
  5. package/dist/contracts/agent.contract.d.ts.map +1 -1
  6. package/dist/contracts/agents.contract.d.ts +596 -1
  7. package/dist/contracts/agents.contract.d.ts.map +1 -1
  8. package/dist/contracts/agents.contract.js +13 -2
  9. package/dist/contracts/agents.contract.js.map +1 -1
  10. package/dist/contracts/capabilities.contract.d.ts +2 -0
  11. package/dist/contracts/capabilities.contract.d.ts.map +1 -1
  12. package/dist/contracts/device.contract.d.ts +1 -0
  13. package/dist/contracts/device.contract.d.ts.map +1 -1
  14. package/dist/contracts/runner.contract.d.ts +45 -0
  15. package/dist/contracts/runner.contract.d.ts.map +1 -1
  16. package/dist/contracts/settings.contract.d.ts +11 -15
  17. package/dist/contracts/settings.contract.d.ts.map +1 -1
  18. package/dist/contracts/system.contract.d.ts +59 -0
  19. package/dist/contracts/system.contract.d.ts.map +1 -1
  20. package/dist/events/agent-events.d.ts +20 -0
  21. package/dist/events/agent-events.d.ts.map +1 -1
  22. package/dist/events/agent-events.js +6 -1
  23. package/dist/events/agent-events.js.map +1 -1
  24. package/dist/events/system-events.d.ts +48 -0
  25. package/dist/events/system-events.d.ts.map +1 -1
  26. package/dist/events/transcript.d.ts +5 -0
  27. package/dist/events/transcript.d.ts.map +1 -1
  28. package/dist/events/transcript.js +9 -1
  29. package/dist/events/transcript.js.map +1 -1
  30. package/dist/front/generated/wire.d.ts +74 -0
  31. package/dist/front/generated/wire.d.ts.map +1 -0
  32. package/dist/front/generated/wire.js +2 -0
  33. package/dist/front/generated/wire.js.map +1 -0
  34. package/dist/index.d.ts +717 -74
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +1 -3
  37. package/dist/index.js.map +1 -1
  38. package/dist/policy/command-run.d.ts +1 -1
  39. package/dist/policy/command-run.d.ts.map +1 -1
  40. package/dist/policy/command-run.js +7 -4
  41. package/dist/policy/command-run.js.map +1 -1
  42. package/dist/protocol/ingress-protocol.d.ts.map +1 -1
  43. package/dist/protocol/ingress-protocol.js +15 -0
  44. package/dist/protocol/ingress-protocol.js.map +1 -1
  45. package/dist/protocol/upgrade-relay.d.ts +4 -0
  46. package/dist/protocol/upgrade-relay.d.ts.map +1 -0
  47. package/dist/protocol/upgrade-relay.js +25 -0
  48. package/dist/protocol/upgrade-relay.js.map +1 -0
  49. package/dist/schemas/agents.d.ts +72 -3
  50. package/dist/schemas/agents.d.ts.map +1 -1
  51. package/dist/schemas/agents.js +10 -7
  52. package/dist/schemas/agents.js.map +1 -1
  53. package/dist/schemas/automations.d.ts +24 -0
  54. package/dist/schemas/automations.d.ts.map +1 -1
  55. package/dist/schemas/capabilities.d.ts +3 -0
  56. package/dist/schemas/capabilities.d.ts.map +1 -1
  57. package/dist/schemas/capabilities.js +4 -0
  58. package/dist/schemas/capabilities.js.map +1 -1
  59. package/dist/schemas/ci.d.ts.map +1 -1
  60. package/dist/schemas/ci.js.map +1 -1
  61. package/dist/schemas/devices.d.ts +32 -6
  62. package/dist/schemas/devices.d.ts.map +1 -1
  63. package/dist/schemas/devices.js +8 -6
  64. package/dist/schemas/devices.js.map +1 -1
  65. package/dist/schemas/keep-warm.d.ts +68 -0
  66. package/dist/schemas/keep-warm.d.ts.map +1 -0
  67. package/dist/schemas/keep-warm.js +74 -0
  68. package/dist/schemas/keep-warm.js.map +1 -0
  69. package/dist/schemas/metrics.d.ts +24 -25
  70. package/dist/schemas/metrics.d.ts.map +1 -1
  71. package/dist/schemas/metrics.js +6 -31
  72. package/dist/schemas/metrics.js.map +1 -1
  73. package/dist/schemas/providers/provider-oauth.d.ts +3 -0
  74. package/dist/schemas/providers/provider-oauth.d.ts.map +1 -1
  75. package/dist/schemas/providers/provider-oauth.js +4 -0
  76. package/dist/schemas/providers/provider-oauth.js.map +1 -1
  77. package/dist/schemas/providers/usage.d.ts +6 -0
  78. package/dist/schemas/providers/usage.d.ts.map +1 -1
  79. package/dist/schemas/providers/usage.js +4 -0
  80. package/dist/schemas/providers/usage.js.map +1 -1
  81. package/dist/schemas/settings.d.ts +13 -29
  82. package/dist/schemas/settings.d.ts.map +1 -1
  83. package/dist/schemas/settings.js +48 -11
  84. package/dist/schemas/settings.js.map +1 -1
  85. package/dist/state/definition.d.ts +24 -20
  86. package/dist/state/definition.d.ts.map +1 -1
  87. package/dist/state/fix-stance.d.ts.map +1 -1
  88. package/dist/state/fix-stance.js +1 -0
  89. package/dist/state/fix-stance.js.map +1 -1
  90. package/dist/state/runtime-state.d.ts +3 -0
  91. package/dist/state/runtime-state.d.ts.map +1 -1
  92. package/dist/state/runtime-state.js +1 -0
  93. package/dist/state/runtime-state.js.map +1 -1
  94. package/dist/state/workspace-state.d.ts +0 -7
  95. package/dist/state/workspace-state.d.ts.map +1 -1
  96. package/dist/state/workspace-state.js +0 -8
  97. package/dist/state/workspace-state.js.map +1 -1
  98. package/dist/text/transcript-fold.d.ts +2 -0
  99. package/dist/text/transcript-fold.d.ts.map +1 -1
  100. package/dist/text/transcript-fold.js +20 -7
  101. package/dist/text/transcript-fold.js.map +1 -1
  102. package/dist/time/zone.d.ts +1 -0
  103. package/dist/time/zone.d.ts.map +1 -1
  104. package/dist/time/zone.js +5 -0
  105. package/dist/time/zone.js.map +1 -1
  106. package/package.json +27 -5
  107. package/src/chores/chores.test.ts +0 -1
  108. package/src/chores/digest.test.ts +0 -1
  109. package/src/chores/probes.test.ts +0 -1
  110. package/src/chores/stack.test.ts +0 -1
  111. package/src/chores/verdict.test.ts +0 -1
  112. package/src/contracts/agents.contract.ts +14 -2
  113. package/src/events/agent-events.ts +9 -1
  114. package/src/events/agent-words.test.ts +0 -1
  115. package/src/events/resume.test.ts +0 -1
  116. package/src/events/transcript.ts +14 -3
  117. package/src/events/watch-wake.test.ts +0 -1
  118. package/src/front/generated/wire.ts +73 -0
  119. package/src/ids/conversation-ids.test.ts +1 -2
  120. package/src/ids/hostnames.fixture.json +20 -0
  121. package/src/ids/hostnames.test.ts +20 -1
  122. package/src/ids/share-paths.test.ts +0 -1
  123. package/src/ids/tunnel-ids.test.ts +0 -1
  124. package/src/index.ts +1 -3
  125. package/src/models/agent-catalog.test.ts +0 -1
  126. package/src/models/capability-ledger.test.ts +0 -1
  127. package/src/models/model-label.test.ts +0 -1
  128. package/src/models/model-offer.test.ts +0 -1
  129. package/src/models/model-order.test.ts +0 -1
  130. package/src/models/model-pins.test.ts +0 -1
  131. package/src/models/model-roles.test.ts +0 -1
  132. package/src/models/plan-pools.test.ts +0 -1
  133. package/src/models/provider-specs.test.ts +0 -1
  134. package/src/policy/batch-runs.test.ts +0 -1
  135. package/src/policy/command-classes.test.ts +0 -1
  136. package/src/policy/command-run.test.ts +35 -0
  137. package/src/policy/command-run.ts +10 -5
  138. package/src/policy/credential-material.test.ts +0 -1
  139. package/src/policy/fence-paths.test.ts +0 -1
  140. package/src/policy/overlay-lint.test.ts +0 -1
  141. package/src/policy/owner-ticket.test.ts +0 -1
  142. package/src/policy/persona-home.test.ts +0 -1
  143. package/src/policy/reserved-servers.test.ts +0 -1
  144. package/src/policy/safety-policy.test.ts +0 -1
  145. package/src/policy/search-globs.test.ts +0 -1
  146. package/src/protocol/container-requirements.test.ts +0 -1
  147. package/src/protocol/ingress-contract.test.ts +0 -1
  148. package/src/protocol/ingress-protocol.test.ts +45 -2
  149. package/src/protocol/ingress-protocol.ts +25 -4
  150. package/src/protocol/peer-dial.test.ts +1 -2
  151. package/src/protocol/peer-mcp-server.test.ts +0 -1
  152. package/src/protocol/routes.test.ts +6 -3
  153. package/src/protocol/tunnel-heartbeat.test.ts +2 -3
  154. package/src/protocol/upgrade-relay.ts +28 -0
  155. package/src/schemas/agent.test.ts +0 -1
  156. package/src/schemas/agents.ts +14 -7
  157. package/src/schemas/areas.test.ts +1 -2
  158. package/src/schemas/capabilities.ts +6 -0
  159. package/src/schemas/ci.ts +3 -9
  160. package/src/schemas/context-trim.test.ts +0 -1
  161. package/src/schemas/dev-rebuild-log.test.ts +0 -1
  162. package/src/schemas/devices.ts +16 -9
  163. package/src/schemas/git/git.test.ts +0 -1
  164. package/src/schemas/keep-warm.test.ts +68 -0
  165. package/src/schemas/keep-warm.ts +124 -0
  166. package/src/schemas/metrics.ts +33 -64
  167. package/src/schemas/providers/provider-oauth.ts +6 -0
  168. package/src/schemas/providers/usage.ts +7 -0
  169. package/src/schemas/settings.test.ts +41 -7
  170. package/src/schemas/settings.ts +78 -44
  171. package/src/schemas/version-seam.test.ts +0 -1
  172. package/src/state/contract-lock.test.ts +0 -1
  173. package/src/state/fix-attempt-plan.test.ts +0 -1
  174. package/src/state/fix-stance.test.ts +14 -1
  175. package/src/state/fix-stance.ts +2 -0
  176. package/src/state/landed-commit.test.ts +0 -1
  177. package/src/state/runtime-state.test.ts +0 -1
  178. package/src/state/runtime-state.ts +4 -0
  179. package/src/state/versions.test.ts +0 -1
  180. package/src/state/workspace-state.test.ts +0 -3
  181. package/src/state/workspace-state.ts +0 -14
  182. package/src/text/archives.test.ts +0 -1
  183. package/src/text/documents.test.ts +0 -1
  184. package/src/text/embed.test.ts +2 -3
  185. package/src/text/emoji.test.ts +0 -1
  186. package/src/text/mentions.test.ts +0 -1
  187. package/src/text/path-refs.test.ts +0 -1
  188. package/src/text/title.test.ts +0 -1
  189. package/src/text/transcript-fold.test.ts +32 -4
  190. package/src/text/transcript-fold.ts +39 -9
  191. package/src/text/whisper.test.ts +0 -1
  192. package/src/text/workflow-faults.test.ts +0 -1
  193. package/src/time/zone.test.ts +0 -1
  194. package/src/time/zone.ts +7 -0
  195. package/src/webext/index.ts +1 -1
  196. package/dist/contracts/prepush.contract.d.ts +0 -32
  197. package/dist/contracts/prepush.contract.d.ts.map +0 -1
  198. package/dist/contracts/prepush.contract.js +0 -41
  199. package/dist/contracts/prepush.contract.js.map +0 -1
  200. package/src/contracts/prepush.contract.ts +0 -58
@@ -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
@@ -402,6 +387,36 @@ export const SandboxSettingsSchema = z.object({
402
387
  .describe(
403
388
  "When a spent usage limit moves a turn to another account, carry the provider session (the model keeps everything, and re-reads all of it once on the other account) while the conversation's context is under this many tokens; at or above it, start a fresh session with the sandbox's measured brief instead. Zero always starts fresh.",
404
389
  ),
390
+ // Off by default: every refresh spends the account's own allowance on a conversation nobody is using yet.
391
+ keepWarm: z
392
+ .boolean()
393
+ .default(false)
394
+ .describe(
395
+ "Keep a Claude conversation's prompt cache warm after each turn a person asked for, so coming back to it hours later costs a cache read instead of re-sending everything. Each refresh re-reads the cached context at the cache price and adds nothing to the conversation. Any one conversation can be kept warm or let cool by hand either way.",
396
+ ),
397
+ keepWarmHours: z
398
+ .number()
399
+ .min(1)
400
+ .max(8)
401
+ .default(4)
402
+ .describe(
403
+ "How long an idle conversation is kept warm after its last turn, in hours. Shortened where refreshing would cost more than the cold resume it saves, and at midnight where the agent runs, when the date in its prompt changes.",
404
+ ),
405
+ keepWarmMinTokens: z
406
+ .number()
407
+ .int()
408
+ .min(0)
409
+ .default(100_000)
410
+ .describe("Only conversations at least this large, in tokens, are kept warm by themselves: a small one is cheap to re-read anyway."),
411
+ keepWarmReserve: z
412
+ .number()
413
+ .int()
414
+ .min(0)
415
+ .max(90)
416
+ .default(15)
417
+ .describe(
418
+ "How much of an account's usage limit, in percent, keeping conversations warm must leave untouched for real work. Refreshing stops once any limit that account's model spends is fuller than that.",
419
+ ),
405
420
  // Worth it since the container is recreated on every update or environment approval — otherwise approving a
406
421
  // Dockerfile change costs the run that asked for it.
407
422
  // What happens to breakage found after the work that caused it has left the turn: a land that turns the main tree's own
@@ -429,14 +444,17 @@ export const SandboxSettingsSchema = z.object({
429
444
  .describe(
430
445
  "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
446
  ),
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.
447
+ // The owner's alone, since a rule can hold work. A command is refused here: it belongs to the repository it checks,
448
+ // in that repository's `.intentic/checks.json`, never in settings.
434
449
  rules: z
435
450
  .array(RuleSchema)
436
451
  .max(50)
437
452
  .default([])
453
+ .refine((rules) => rules.every((rule) => rule.action.kind !== "command"), {
454
+ message: "a command belongs in the repository's own .intentic/checks.json, not in settings",
455
+ })
438
456
  .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.",
457
+ "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
458
  ),
441
459
  automationFailureLimit: z
442
460
  .number()
@@ -608,27 +626,23 @@ export type SavingsReport = z.infer<typeof SavingsReportSchema>;
608
626
  // spelling for the daemon that reads it, the screen that names it and the demo that mimics it.
609
627
  export const REPO_CHECKS_FILE = `${STATE_DIR}/checks.json`;
610
628
 
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"]);
629
+ // Named for the occasion as a repository would say it: `edit` is `file.edited`, `turn` is `turn.ending`, and `land` is
630
+ // 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
631
+ // the repository's own git pre-push hook, which every push runs, the app's included.
632
+ // `edit` runs on one file (`{file}`) and its output rides back in the edit's own response, so a command declared there is
633
+ // paid per edit and has to take the file.
634
+ export const RepoCheckMomentSchema = z.enum(["edit", "turn", "land"]);
619
635
  export type RepoCheckMoment = z.infer<typeof RepoCheckMomentSchema>;
620
636
 
621
637
  export const RepoCheckSchema = z.object({
622
638
  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.",
639
+ "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
640
  ),
625
641
  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
642
  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.
643
+ // Same ceiling as a rule's own command; past it the process group is killed and the run is a failure.
629
644
  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.
645
+ // Repo-relative; the daemon prefixes the repo id before matching, since a rule's globs are workspace-relative.
632
646
  paths: z
633
647
  .array(z.string().min(1))
634
648
  .max(20)
@@ -638,7 +652,16 @@ export const RepoCheckSchema = z.object({
638
652
  export type RepoCheck = z.infer<typeof RepoCheckSchema>;
639
653
 
640
654
  // 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([]) });
655
+ export const RepoChecksFileSchema = z.object({
656
+ checks: z
657
+ .array(RepoCheckSchema)
658
+ .max(10)
659
+ .default([])
660
+ // One land check per repository: it is THE verdict on the main tree the push hook replays, not one of several.
661
+ .refine((checks) => checks.filter((check) => check.when === "land").length <= 1, { message: "a repository declares at most one land check" })
662
+ // A land checks the whole tree an install settled, not a change, so there is nothing for a glob to narrow.
663
+ .refine((checks) => checks.every((check) => check.when !== "land" || check.paths === undefined), { message: "a land check takes no paths" }),
664
+ });
642
665
  export type RepoChecksFile = z.infer<typeof RepoChecksFileSchema>;
643
666
 
644
667
  // One repository, as a screen reads it: what it declares, and where that stands with the owner.
@@ -646,6 +669,11 @@ export const RepoChecksSummarySchema = z.object({
646
669
  repo: z.string().describe('Which repository, by its workspace id ("root" is the workspace itself).'),
647
670
  path: z.string().describe("Where the declaration lives, relative to the workspace, whether or not the file exists yet."),
648
671
  checks: z.array(RepoCheckSchema).describe("What it declares, in the order the file lists them."),
672
+ fired: z
673
+ .array(z.number().nullable())
674
+ .describe(
675
+ "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.",
676
+ ),
649
677
  adopted: z
650
678
  .boolean()
651
679
  .describe("Whether these are running. False means declared and inert: nothing a repository writes runs until the owner switches it on."),
@@ -655,10 +683,16 @@ export const RepoChecksSummarySchema = z.object({
655
683
  "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
684
  ),
657
685
  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."),
686
+ landDefault: z
687
+ .string()
688
+ .optional()
689
+ .describe(
690
+ "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.",
691
+ ),
658
692
  });
659
693
  export type RepoChecksSummary = z.infer<typeof RepoChecksSummarySchema>;
660
694
  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."),
695
+ repos: z.array(RepoChecksSummarySchema).describe("Every repository that declares checks or has a package check that runs after a land, in id order."),
662
696
  });
663
697
  export type RepoChecksList = z.infer<typeof RepoChecksListSchema>;
664
698
  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
 
@@ -46,6 +45,20 @@ test("held work reads as a fix to review, whether the fleet called it ready or l
46
45
  expect(fixStance(agent({ status: `idle`, diff: { files: 4, insertions: 120, deletions: 8 } })).kind).toBe(`ready`);
47
46
  });
48
47
 
48
+ test.each([
49
+ ["holding work", { files: 4, insertions: 120, deletions: 8 }],
50
+ ["holding nothing yet", undefined],
51
+ ] as const)("a turn that ended waiting on its own watch, %s, is still working", (_case, diff) => {
52
+ const watches = [{ id: `watch-a1b2`, note: `CI on the pushed branch`, intervalSeconds: 60, deadlineAt: 9_000 }];
53
+ expect(fixStance(agent({ status: `idle`, watches, ...(diff === undefined ? {} : { diff }) }))).toStrictEqual({
54
+ kind: `working`,
55
+ ongoing: true,
56
+ retry: false,
57
+ label: `Agent working`,
58
+ hint: `An agent is already working on this failure. Open the conversation to watch it.`,
59
+ });
60
+ });
61
+
49
62
  // Diff present doesn't change a failed status to ready; it only earns a mention in the hint.
50
63
  test("an agent that failed after writing files still reads as failed", () => {
51
64
  const stance = fixStance(agent({ status: `error`, diff: { files: 2, insertions: 34, deletions: 6 } }));
@@ -130,6 +130,8 @@ const RULES: readonly ((agent: AgentSummary) => FixStance | undefined)[] = [
130
130
  ? needsYou("Needs you", "The fix agent has stopped and is waiting on you.")
131
131
  : undefined,
132
132
  (agent) => (IN_FLIGHT.has(agent.status) ? WORKING : undefined),
133
+ // An armed watch wakes it again by itself, so neither what it holds nor holding nothing is its last word yet.
134
+ (agent) => ((agent.watches?.length ?? 0) > 0 ? WORKING : undefined),
133
135
  (agent) => (holdingWork(agent) ? READY : undefined),
134
136
  (agent) => (agent.status === "landed" ? LANDED : undefined),
135
137
  ];
@@ -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`, () => {
@@ -27,6 +27,10 @@ const RUNTIME_DOMAINS = [
27
27
  // The agent's Chromiums and open pages, daemon-held, minted from its own browser tool-call hooks.
28
28
  { domain: "browsers", invalidates: [["browsers"]] },
29
29
 
30
+ // A connection's status as its last probe found it: pushed when a probe behind a served answer finds another one, and
31
+ // when a browser sign-in is saved or cleared, which is what moves an account's card off "log in".
32
+ { domain: "capabilities", invalidates: [["capabilities"]] },
33
+
30
34
  // Daemon-held; changes continuously (tokens, tool uses), so it's rate-limited rather than pushed per mutation.
31
35
  { domain: "subagents", invalidates: [["subagents"]] },
32
36
 
@@ -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", () => {
@@ -430,7 +439,7 @@ describe("patches", () => {
430
439
  "append",
431
440
  "tool",
432
441
  "tool",
433
- "tool",
442
+ "toolThinking",
434
443
  "tool",
435
444
  "replace",
436
445
  "append",
@@ -442,6 +451,25 @@ describe("patches", () => {
442
451
  ]);
443
452
  });
444
453
 
454
+ it("send a delegation's card without its subtree or thinking, and a client keeps the ones it has", () => {
455
+ const events: AgentEvent[] = [
456
+ { kind: "tool_call", id: "task-1", name: "Agent", category: "other", status: "in_progress" },
457
+ { kind: "tool_call", id: "t2", name: "Read", category: "read", status: "in_progress", parentToolUseId: "task-1" },
458
+ { kind: "thinking", text: "in", parentToolUseId: "task-1" },
459
+ { kind: "thinking", text: "ner", parentToolUseId: "task-1" },
460
+ { kind: "tool_call_update", id: "task-1", status: "completed" },
461
+ ];
462
+ const { folded, applied, patches } = replay(openingOf("go"), events);
463
+ expect(applied).toEqual(folded);
464
+ expect(patches.filter((patch) => patch.op === "toolThinking")).toEqual([
465
+ { op: "toolThinking", index: 1, id: "task-1", text: "in" },
466
+ { op: "toolThinking", index: 1, id: "task-1", text: "ner" },
467
+ ]);
468
+ const update = patches.at(-1);
469
+ expect(update?.op === "tool" ? update.tool : undefined).toEqual({ id: "task-1", name: "Agent", category: "other", status: "completed" });
470
+ expect(applied[1]?.tools?.[0]).toMatchObject({ status: "completed", thinking: "inner", children: [{ id: "t2" }] });
471
+ });
472
+
445
473
  // The other half of the wake's delivery: a live turn takes it as a steer. It is the daemon's own words either way,
446
474
  // so it must not become a user row here, and must not leave a rewind anchor pointing at a message nobody sent.
447
475
  it("writes a watch's wake into a live turn as a notice, and never as a steer to rewind to", () => {
@@ -11,6 +11,7 @@ import {
11
11
  type TranscriptTool,
12
12
  } from "../events/transcript.js";
13
13
  import { contextTrimLine } from "../schemas/context-trim.js";
14
+ import { keptWarmLine } from "../schemas/keep-warm.js";
14
15
  import { turnedAwayCode } from "../policy/turned-away.js";
15
16
  import { mentionedPathTokens } from "./mentions.js";
16
17
  import { unspokenPromptRow } from "../events/agent-words.js";
@@ -21,6 +22,9 @@ import { unspokenPromptRow } from "../events/agent-words.js";
21
22
 
22
23
  export type TurnEnding = "settled" | "stopped";
23
24
 
25
+ // A compaction's row, live and restored from the provider's store alike.
26
+ export const COMPACTED_NOTICE = "Context compacted to free up space.";
27
+
24
28
  // Where a tool card lives: its row, and the parent card it nests under when it's a helper's own call.
25
29
  interface CardPlace {
26
30
  readonly tool: TranscriptTool;
@@ -32,6 +36,9 @@ interface CardPlace {
32
36
  const defined = <T extends object>(value: T): Partial<T> =>
33
37
  Object.fromEntries(Object.entries(value).filter(([, field]) => field !== undefined)) as Partial<T>;
34
38
 
39
+ // A card's own fields: what a `tool` patch carries, so a delegation's growing subtree never rides one.
40
+ const ownFields = ({ children: _children, thinking: _thinking, ...own }: TranscriptTool): TranscriptTool => own;
41
+
35
42
  const cardOf = (event: Extract<AgentEvent, { kind: "tool_call" }>): TranscriptTool => ({
36
43
  id: event.id,
37
44
  name: event.name,
@@ -296,7 +303,16 @@ export class TranscriptFold {
296
303
  }
297
304
  case "usage": {
298
305
  // Lands on the last assistant bubble and closes it: a steered turn's stream can carry several turns.
299
- const { kind: _kind, account: _account, cacheReadTokens: _read, cacheCreationTokens: _written, ...usage } = event;
306
+ const {
307
+ kind: _kind,
308
+ account: _account,
309
+ cacheReadTokens: _read,
310
+ cacheCreationTokens: _written,
311
+ openingCacheReadTokens: _openingRead,
312
+ openingCacheCreationTokens: _openingWritten,
313
+ promptFingerprint: _fingerprint,
314
+ ...usage
315
+ } = event;
300
316
  const index = this.rows.findLastIndex((row) => row.role === "assistant");
301
317
  const closed = this.closeBubble();
302
318
  if (index === -1) {
@@ -318,6 +334,11 @@ export class TranscriptFold {
318
334
  case "preamble":
319
335
  // Collapses the daemon's preamble notes onto the user row; an empty note list is not a disclosure.
320
336
  return event.notes.length === 0 ? [] : this.stampOpener((row) => (row.notes = [...event.notes]));
337
+ case "prompt_cache": {
338
+ // Only a conversation the sandbox kept warm gets a row: every other turn's cache is its own business.
339
+ const line = keptWarmLine(event);
340
+ return line === undefined ? [] : this.pushRow({ role: "notice", text: line });
341
+ }
321
342
  case "context_trim":
322
343
  // A row of its own rather than a stamp on the message: what was left out is not part of what was sent,
323
344
  // and the fold beside the message only ever lists notes that actually rode.
@@ -327,7 +348,7 @@ export class TranscriptFold {
327
348
  case "landed":
328
349
  return this.pushRow(landedRow(event));
329
350
  case "compact":
330
- return this.pushRow({ role: "notice", text: `Context compacted to free up space.` });
351
+ return this.pushRow({ role: "notice", text: COMPACTED_NOTICE });
331
352
  case "error":
332
353
  // Keeps a refusal (no prose from the provider) from reading as a session that ended mid-question.
333
354
  // A refusal that ran nothing takes its message back out ahead of the notice standing in for it.
@@ -519,9 +540,8 @@ export class TranscriptFold {
519
540
  return [];
520
541
  }
521
542
  if (event.kind === "thinking") {
522
- return this.patchCard(parent, (tool) => {
523
- tool.thinking = `${tool.thinking ?? ""}${event.text}`;
524
- });
543
+ place.tool.thinking = `${place.tool.thinking ?? ""}${event.text}`;
544
+ return [{ op: "toolThinking", index: place.row, id: parent, text: event.text }];
525
545
  }
526
546
  if (event.kind === "tool_call") {
527
547
  const child = cardOf(event);
@@ -590,7 +610,7 @@ export class TranscriptFold {
590
610
  return [];
591
611
  }
592
612
  mutate(place.tool);
593
- return [{ op: "tool", index: place.row, tool: structuredClone(place.tool), ...(place.parent === undefined ? {} : { parent: place.parent }) }];
613
+ return [{ op: "tool", index: place.row, tool: structuredClone(ownFields(place.tool)), ...(place.parent === undefined ? {} : { parent: place.parent }) }];
594
614
  }
595
615
 
596
616
  private stampOpener(mutate: (row: TranscriptRow) => void): TranscriptPatch[] {
@@ -637,15 +657,25 @@ export const applyTranscriptPatch = (rows: readonly TranscriptRow[], patch: Tran
637
657
  return rows.map((row, index) => (index === patch.index ? { ...row, text: `${row.text}${patch.text}` } : row));
638
658
  case "thinking":
639
659
  return rows.map((row, index) => (index === patch.index ? { ...row, thinking: `${row.thinking ?? ""}${patch.text}` } : row));
660
+ case "toolThinking":
661
+ return rows.map((row, index) => (index === patch.index ? { ...row, tools: [...appendToolThinking(row.tools ?? [], patch.id, patch.text)] } : row));
640
662
  case "tool":
641
663
  return rows.map((row, index) => (index === patch.index ? { ...row, tools: upsertTool(row.tools ?? [], patch.tool, patch.parent) } : row));
642
664
  }
643
665
  };
644
666
 
645
- // Replaces the tool with this id anywhere in the tree, or appends it under `parent`, or at top level with no parent or
646
- // no match.
667
+ // Appends a delegated subagent's reasoning onto the card with this id, wherever it nests.
668
+ export const appendToolThinking = (tools: readonly TranscriptTool[], id: string, text: string): readonly TranscriptTool[] =>
669
+ mapTool(tools, id, (card) => ({ ...card, thinking: `${card.thinking ?? ""}${text}` }));
670
+
671
+ // Replaces the tool with this id anywhere in the tree, keeping the nested calls and thinking it already holds, or appends
672
+ // it under `parent`, or at top level with no parent or no match.
647
673
  export const upsertTool = (tools: readonly TranscriptTool[], tool: TranscriptTool, parent: string | undefined): TranscriptTool[] => {
648
- const replaced = mapTool(tools, tool.id, () => tool);
674
+ const replaced = mapTool(tools, tool.id, (current) => ({
675
+ ...tool,
676
+ ...(current.children === undefined ? {} : { children: current.children }),
677
+ ...(current.thinking === undefined ? {} : { thinking: current.thinking }),
678
+ }));
649
679
  if (replaced !== tools) {
650
680
  return [...replaced];
651
681
  }
@@ -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