@intentic/sandbox-contract 1.167.0 → 1.169.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 +20 -2
- package/dist/agent-catalog.d.ts.map +1 -1
- package/dist/agent-catalog.js +80 -4
- package/dist/agent-catalog.js.map +1 -1
- package/dist/contracts/agent.contract.d.ts +36 -8
- package/dist/contracts/agent.contract.d.ts.map +1 -1
- package/dist/contracts/agent.contract.js +1 -2
- package/dist/contracts/agent.contract.js.map +1 -1
- package/dist/contracts/agents.contract.d.ts +37 -39
- package/dist/contracts/agents.contract.d.ts.map +1 -1
- package/dist/contracts/extensions.contract.d.ts +4 -0
- package/dist/contracts/extensions.contract.d.ts.map +1 -1
- package/dist/contracts/prepush.contract.d.ts +25 -0
- package/dist/contracts/prepush.contract.d.ts.map +1 -0
- package/dist/contracts/prepush.contract.js +8 -0
- package/dist/contracts/prepush.contract.js.map +1 -0
- package/dist/contracts/sessions.contract.d.ts +1 -39
- package/dist/contracts/sessions.contract.d.ts.map +1 -1
- package/dist/contracts/settings.contract.d.ts +8 -10
- package/dist/contracts/settings.contract.d.ts.map +1 -1
- package/dist/contracts/system.contract.d.ts +56 -0
- package/dist/contracts/system.contract.d.ts.map +1 -1
- package/dist/contracts/system.contract.js +7 -2
- package/dist/contracts/system.contract.js.map +1 -1
- package/dist/contracts/translator.contract.d.ts +36 -0
- package/dist/contracts/translator.contract.d.ts.map +1 -1
- package/dist/events.d.ts +88 -159
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +37 -4
- package/dist/events.js.map +1 -1
- package/dist/index.d.ts +218 -157
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/quick-model.d.ts +1 -0
- package/dist/quick-model.d.ts.map +1 -1
- package/dist/quick-model.js +1 -1
- package/dist/quick-model.js.map +1 -1
- package/dist/schemas.d.ts +176 -69
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +48 -40
- package/dist/schemas.js.map +1 -1
- package/dist/title.d.ts.map +1 -1
- package/dist/title.js.map +1 -1
- package/dist/workspace-state.d.ts +9 -0
- package/dist/workspace-state.d.ts.map +1 -0
- package/dist/workspace-state.js +61 -0
- package/dist/workspace-state.js.map +1 -0
- package/package.json +2 -2
- package/src/agent-catalog.test.ts +118 -0
- package/src/agent-catalog.ts +179 -15
- package/src/contracts/agent.contract.ts +1 -14
- package/src/contracts/prepush.contract.ts +16 -0
- package/src/contracts/system.contract.ts +15 -1
- package/src/events.test.ts +32 -0
- package/src/events.ts +105 -27
- package/src/index.ts +4 -3
- package/src/quick-model.ts +4 -2
- package/src/schemas.test.ts +13 -18
- package/src/schemas.ts +179 -152
- package/src/title.ts +6 -2
- package/src/workspace-state.test.ts +129 -0
- package/src/workspace-state.ts +150 -0
- package/dist/contracts/gate.contract.d.ts +0 -47
- package/dist/contracts/gate.contract.d.ts.map +0 -1
- package/dist/contracts/gate.contract.js +0 -9
- package/dist/contracts/gate.contract.js.map +0 -1
- package/src/contracts/gate.contract.ts +0 -19
package/src/schemas.ts
CHANGED
|
@@ -262,6 +262,14 @@ export const AgentSummarySchema = z.object({
|
|
|
262
262
|
// Completed turns and lifetime tool calls — the card's msgs/tools counters.
|
|
263
263
|
turns: z.number().optional(),
|
|
264
264
|
toolUses: z.number().optional(),
|
|
265
|
+
/* The agents THIS agent started (SubagentSessionSchema), live and lifetime. Absent ⇒ it has never delegated,
|
|
266
|
+
* which is most agents — so the card's chip appears on content rather than reading "0" down the board.
|
|
267
|
+
*
|
|
268
|
+
* It earns a place on a card because a fleet card is the answer to "what is this agent up to", and an agent
|
|
269
|
+
* running five children looked exactly like an agent running none: the work was real, the spend was real, and
|
|
270
|
+
* the board said nothing. The tokens are NOT folded into the parent's cost — a child's spend is its own, and
|
|
271
|
+
* the Subagents area is where it is attributed. */
|
|
272
|
+
subagents: z.object({ running: z.number(), total: z.number() }).optional(),
|
|
265
273
|
// The agent's cumulative output (base → branch tip across every repo), refreshed on each land —
|
|
266
274
|
// the card's "12 files · +412 −96" readout. Independent of what has landed.
|
|
267
275
|
diff: z.object({ files: z.number(), insertions: z.number(), deletions: z.number() }).optional(),
|
|
@@ -383,9 +391,52 @@ export const AgentLandSchema = z.object({ id: z.string().min(1), mode: LandModeS
|
|
|
383
391
|
export const KeyedProviderSchema = z.enum(["codex", "grok", "kimi", "gemini"]);
|
|
384
392
|
export type KeyedProvider = z.infer<typeof KeyedProviderSchema>;
|
|
385
393
|
|
|
394
|
+
// ---- plan-limit usage ----
|
|
395
|
+
// Declared ABOVE both account shapes because both carry it: headroom is one idea in this product, not a Claude
|
|
396
|
+
// idea that other providers imitate. A native account (OauthAccount) and a routed subscription
|
|
397
|
+
// (TranslatorAccount) differ in who holds the credential and how the reading is taken — never in what a
|
|
398
|
+
// reading IS — so every surface that draws a percentage reads this one type and no other.
|
|
399
|
+
|
|
400
|
+
// One plan-limit pool. `kind` is the provider's own key ('five_hour' | 'seven_day' | 'seven_day_opus' |
|
|
401
|
+
// 'seven_day_sonnet' | 'model:Fable' | …) rather than an enum we'd have to keep in step with the provider: an
|
|
402
|
+
// unrecognised pool is shown under its raw key, which is far better than being silently folded into a
|
|
403
|
+
// neighbour. `label` is the provider's OWN display name where it supplies one (the per-model buckets do) — it
|
|
404
|
+
// wins over anything we'd infer, because the model names in a plan's limits are the provider's to rename.
|
|
405
|
+
// `resetsAt` is epoch SECONDS (matching the SDK's frame).
|
|
406
|
+
export const UsageWindowSchema = z.object({
|
|
407
|
+
kind: z.string(),
|
|
408
|
+
label: z.string().optional(),
|
|
409
|
+
utilization: z.number(), // 0-100
|
|
410
|
+
resetsAt: z.number().optional(),
|
|
411
|
+
});
|
|
412
|
+
export type UsageWindow = z.infer<typeof UsageWindowSchema>;
|
|
413
|
+
|
|
414
|
+
// An account's headroom: EVERY window the provider reports, read together, plus when the reading was taken.
|
|
415
|
+
// All of them, not the binding one, because "which pool is binding" changes between turns and a reader
|
|
416
|
+
// 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 Google's are pulled through
|
|
418
|
+
// CLIProxyAPI's credential-scoped management call. All of them are control requests, so none costs tokens.
|
|
419
|
+
//
|
|
420
|
+
// Within one window utilization only climbs, so an un-reset window stays a valid FLOOR however old it is; past
|
|
421
|
+
// its `resetsAt` it describes a pool that no longer exists and the store drops it. `measuredAt` is epoch MS
|
|
422
|
+
// (matching connectedAt) — deliberately a different unit from the windows' seconds.
|
|
423
|
+
export const AccountUsageSchema = z.object({
|
|
424
|
+
windows: z.array(UsageWindowSchema),
|
|
425
|
+
measuredAt: z.number(),
|
|
426
|
+
});
|
|
427
|
+
export type AccountUsage = z.infer<typeof AccountUsageSchema>;
|
|
428
|
+
|
|
386
429
|
// One connected subscription in the translator. `name` is CLIProxyAPI's auth-file name — the stable store key a
|
|
387
430
|
// disconnect addresses — and `label` the sign-in identity it reported (the account email, else the file name).
|
|
388
|
-
export const TranslatorAccountSchema = z.object({
|
|
431
|
+
export const TranslatorAccountSchema = z.object({
|
|
432
|
+
name: z.string(),
|
|
433
|
+
label: z.string(),
|
|
434
|
+
// 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, Kimi) —
|
|
436
|
+
// or one that did not answer — must still render as the connected account it is, with a dot instead of a
|
|
437
|
+
// ring.
|
|
438
|
+
usage: AccountUsageSchema.optional(),
|
|
439
|
+
});
|
|
389
440
|
export type TranslatorAccount = z.infer<typeof TranslatorAccountSchema>;
|
|
390
441
|
// Which routed-provider subscriptions are connected in the translator, per provider — a LIST per provider, not
|
|
391
442
|
// a flag: CLIProxyAPI holds any number of auth files per provider side by side and balances requests across
|
|
@@ -453,14 +504,8 @@ export const SteerSchema = z
|
|
|
453
504
|
// True cancel for the conversation's in-flight turn — aborts the agent daemon-side, unlike closing the
|
|
454
505
|
// /agent fetch (which sends no cancel frame).
|
|
455
506
|
export const StopTurnSchema = z.object({ conversationId: z.string().min(1) });
|
|
456
|
-
|
|
457
|
-
//
|
|
458
|
-
// action a spent allowance offers when the sandbox holds more than one; omitted, the turn re-runs on
|
|
459
|
-
// whatever served it (a plain "try again now"). NOT_FOUND when nothing is pending (the failure was already
|
|
460
|
-
// superseded by a fresh turn, or the daemon restarted).
|
|
461
|
-
export const ResumeLimitSchema = z.object({ conversationId: z.string().min(1), account: z.string().min(1).optional() });
|
|
462
|
-
|
|
463
|
-
// ---- claude subscription usage ----
|
|
507
|
+
|
|
508
|
+
// ---- claude rate-limit gate ----
|
|
464
509
|
// The GATE signal: whether the provider is letting turns through right now, and — when it is refusing — which
|
|
465
510
|
// window is binding and when it lifts. This is the SDK's rate_limit_event, mapped one-to-one, and it is only
|
|
466
511
|
// ever about the CURRENT moment. It is deliberately NOT the thing the headroom displays read: the event names a
|
|
@@ -474,34 +519,6 @@ export const RateLimitInfoSchema = z.object({
|
|
|
474
519
|
});
|
|
475
520
|
export type RateLimitInfo = z.infer<typeof RateLimitInfoSchema>;
|
|
476
521
|
|
|
477
|
-
// One plan-limit pool. `kind` is the provider's own key ('five_hour' | 'seven_day' | 'seven_day_opus' |
|
|
478
|
-
// 'seven_day_sonnet' | 'model:Fable' | …) rather than an enum we'd have to keep in step with the provider: an
|
|
479
|
-
// unrecognised pool is shown under its raw key, which is far better than being silently folded into a
|
|
480
|
-
// neighbour. `label` is the provider's OWN display name where it supplies one (the per-model buckets do) — it
|
|
481
|
-
// wins over anything we'd infer, because the model names in a plan's limits are the provider's to rename.
|
|
482
|
-
// `resetsAt` is epoch SECONDS (matching the SDK's frame).
|
|
483
|
-
export const UsageWindowSchema = z.object({
|
|
484
|
-
kind: z.string(),
|
|
485
|
-
label: z.string().optional(),
|
|
486
|
-
utilization: z.number(), // 0-100
|
|
487
|
-
resetsAt: z.number().optional(),
|
|
488
|
-
});
|
|
489
|
-
export type UsageWindow = z.infer<typeof UsageWindowSchema>;
|
|
490
|
-
|
|
491
|
-
// An account's headroom: EVERY window the provider reports, read together, plus when the reading was taken.
|
|
492
|
-
// All of them, not the binding one, because "which pool is binding" changes between turns and a reader
|
|
493
|
-
// comparing accounts needs the same pools on every row. Sourced from the CLI's own usage endpoint at turn end
|
|
494
|
-
// (see claudeUsageWindows) — a control request, so it costs no tokens.
|
|
495
|
-
//
|
|
496
|
-
// Within one window utilization only climbs, so an un-reset window stays a valid FLOOR however old it is; past
|
|
497
|
-
// its `resetsAt` it describes a pool that no longer exists and the store drops it. `measuredAt` is epoch MS
|
|
498
|
-
// (matching connectedAt) — deliberately a different unit from the windows' seconds.
|
|
499
|
-
export const AccountUsageSchema = z.object({
|
|
500
|
-
windows: z.array(UsageWindowSchema),
|
|
501
|
-
measuredAt: z.number(),
|
|
502
|
-
});
|
|
503
|
-
export type AccountUsage = z.infer<typeof AccountUsageSchema>;
|
|
504
|
-
|
|
505
522
|
// ---- provider oauth ----
|
|
506
523
|
// Claude uses the PKCE authorize-URL + paste-back handshake (start → exchange). Codex uses OpenAI's device-code
|
|
507
524
|
// flow (start → poll): the browser signs in at verificationUri and enters userCode; the daemon polls until done.
|
|
@@ -528,8 +545,9 @@ export const OauthAccountSchema = z.object({
|
|
|
528
545
|
needsReauth: z.boolean().optional(),
|
|
529
546
|
detail: z.string().optional(),
|
|
530
547
|
// The account's last known subscription-usage snapshot, so the picker can show what's left on each account
|
|
531
|
-
// before the user commits a turn to one.
|
|
532
|
-
//
|
|
548
|
+
// before the user commits a turn to one. Absent until a reading exists for it — an unmeasured account reads
|
|
549
|
+
// as unknown, never 0%. Claude is the provider that fills it here, because its stream reports the windows;
|
|
550
|
+
// the routed subscriptions carry the identical field on TranslatorAccount, filled by a pulled reading.
|
|
533
551
|
usage: AccountUsageSchema.optional(),
|
|
534
552
|
});
|
|
535
553
|
export type OauthAccount = z.infer<typeof OauthAccountSchema>;
|
|
@@ -669,11 +687,6 @@ export const BuiltinPromptSchema = z.object({ base: z.enum(["intentic", "claude"
|
|
|
669
687
|
// surface fail to parse the moment a toggle is added — which reaches the user as a page of switches that are
|
|
670
688
|
// silently dead, not as an error. It also means an older on-disk manifest keeps the owner's other picks rather
|
|
671
689
|
// than being discarded whole.
|
|
672
|
-
// Build-wide kill switch for usage-limit auto-resume. Keep the setting and implementation in place while the
|
|
673
|
-
// feature is disabled, but clamp every daemon response and settings write to OFF so a persisted `true` from an
|
|
674
|
-
// older build cannot keep spending a newly reset allowance. The web also reads this constant to render the
|
|
675
|
-
// control unavailable and to distrust a stale daemon that still reports a scheduled resume.
|
|
676
|
-
export const USAGE_LIMIT_AUTO_RESUME_ENABLED: boolean = false;
|
|
677
690
|
|
|
678
691
|
export const SandboxSettingsSchema = z.object({
|
|
679
692
|
stableSystemPrompt: z.boolean().default(false),
|
|
@@ -746,18 +759,12 @@ export const SandboxSettingsSchema = z.object({
|
|
|
746
759
|
* automation-opened agents (Discord, webhooks, email) finish turns with no browser in the room — a
|
|
747
760
|
* browser-held preference could not govern them. Per-agent override: AgentSummarySchema.autoLand. */
|
|
748
761
|
autoLand: z.boolean().default(true),
|
|
749
|
-
// Latent opt-in for re-running a turn after the Claude subscription's usage window resets. It defaults off
|
|
750
|
-
// because an unattended retry spends the fresh allowance without the user in the room, and the build-wide
|
|
751
|
-
// gate above currently clamps even an older saved opt-in off while the feature is unavailable.
|
|
752
|
-
autoResumeOnLimit: z
|
|
753
|
-
.boolean()
|
|
754
|
-
.default(false)
|
|
755
|
-
.overwrite((enabled) => USAGE_LIMIT_AUTO_RESUME_ENABLED && enabled),
|
|
756
762
|
/* When a turn dies because the MODEL PROVIDER was failing (500/502/503, a 529 at capacity, a dropped
|
|
757
763
|
* socket), re-run it on an escalating backoff until it goes through or the attempts are spent.
|
|
758
764
|
*
|
|
759
|
-
* Defaults ON,
|
|
760
|
-
*
|
|
765
|
+
* Defaults ON, and a spent Claude allowance is the counter-example that explains why: that one is the
|
|
766
|
+
* user's own budget, and resuming into a freshly reset window spends something they may have been saving —
|
|
767
|
+
* so a usage limit stops the turn and says when it resets, and nothing re-runs it. An
|
|
761
768
|
* outage resume spends nothing the dead turn had not already committed, resolves in minutes rather than
|
|
762
769
|
* hours, and — the deciding argument — the turns hurt worst by it are the ones with nobody in the room
|
|
763
770
|
* (automation wakes, Discord, webhooks), which no browser-held preference could ever rescue. It is the same
|
|
@@ -765,8 +772,8 @@ export const SandboxSettingsSchema = z.object({
|
|
|
765
772
|
resumeAfterOutage: z.boolean().default(true),
|
|
766
773
|
/* When the daemon dies under a running turn, re-run that turn once it is back (agent/turn-journal.ts records
|
|
767
774
|
* every in-flight turn; the boot pass in agent/turn-resume.ts re-runs what survived). ON by default, where
|
|
768
|
-
*
|
|
769
|
-
* budget, while a restart is usually intentic's OWN doing — the container is recreated on every update,
|
|
775
|
+
* a spent usage limit re-runs nothing, and the difference is who broke the turn: a spent allowance is the
|
|
776
|
+
* user's own budget, while a restart is usually intentic's OWN doing — the container is recreated on every update,
|
|
770
777
|
* every environment approval and every dev-sandbox.sh swap. Approving the Dockerfile change an agent asked
|
|
771
778
|
* for must not cost the run that asked for it, and a user who just clicked Approve is in the room expecting
|
|
772
779
|
* the work to continue, not a second button.
|
|
@@ -774,34 +781,26 @@ export const SandboxSettingsSchema = z.object({
|
|
|
774
781
|
* OFF still records the interruption: the fleet card reads `interrupted` (see AgentStatusSchema) and an
|
|
775
782
|
* automation's row shows an `interrupted` run — nothing is re-run, but nothing is silently lost either. */
|
|
776
783
|
autoResumeOnRestart: z.boolean().default(true),
|
|
777
|
-
/* THE
|
|
778
|
-
* Empty ⇒ no
|
|
779
|
-
* guessed command that fails on a fresh clone would read as the
|
|
784
|
+
/* THE PRE-PUSH CHECK — the command run when the user pushes, before anything leaves the machine.
|
|
785
|
+
* Empty ⇒ no check at all, which is the default: only the owner knows what verifies this workspace, and a
|
|
786
|
+
* guessed command that fails on a fresh clone would read as the check finding a bug on its first run.
|
|
780
787
|
*
|
|
781
788
|
* Configuring it is the opt-in, which is why there is no separate enable flag to disagree with it. The
|
|
782
|
-
* command runs in the workspace root through `sh -c`, exactly as a terminal would run it
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
*
|
|
789
|
+
* command runs in the workspace root through `sh -c`, exactly as a terminal would run it. */
|
|
790
|
+
prepushCommand: z.string().max(500).default(""),
|
|
791
|
+
// Ceiling on one run, after which the child's whole process group is killed and the result is `failed` with
|
|
792
|
+
// `timedOut`. Never a pass: a suite that did not finish has said nothing about the tree, and the one thing
|
|
793
|
+
// this check exists to prevent is a green light nobody earned.
|
|
794
|
+
prepushTimeoutMs: z.number().min(60_000).max(3_600_000).default(900_000),
|
|
795
|
+
/* WHAT THE SUGGESTED FIX SESSION RUNS ON when the check fails — `${provider}:${model}` (quickModelKey), and
|
|
796
|
+
* the reasoning effort beside it. Both empty ⇒ whatever the chat composer would have started with, which is
|
|
797
|
+
* the right default precisely because it is the model the user already chose to work with.
|
|
790
798
|
*
|
|
791
|
-
*
|
|
792
|
-
*
|
|
793
|
-
*
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
// Never a pass: a suite that did not finish has not said anything about the tree, and the one thing this
|
|
797
|
-
// gate exists to prevent is a green light nobody earned.
|
|
798
|
-
gateTimeoutMs: z.number().min(60_000).max(3_600_000).default(900_000),
|
|
799
|
-
/* Wake a fixer automatically when the gate goes red, instead of only lighting the badge. ON with a
|
|
800
|
-
* configured command, unlike the other unattended-spend toggles (autoResumeOnLimit), and the difference is
|
|
801
|
-
* that the spend here is the POINT: a red gate whose fix waits for the user to notice has moved the CI
|
|
802
|
-
* round-trip into the workspace without removing it from the user's day. One attempt per verdict, so a
|
|
803
|
-
* command that fails for a reason no agent can fix costs one turn, not a loop (gate/gate.ts). */
|
|
804
|
-
gateAutoFix: z.boolean().default(true),
|
|
799
|
+
* Pinned here rather than resolved cheapest-first like `quickModel`: this is not a one-click helper, it is a
|
|
800
|
+
* turn that has to read a failing suite and repair it, so the choice is the user's and worth stating. The
|
|
801
|
+
* suggestion dialog seeds from these and stays editable — the setting decides where the dialog OPENS. */
|
|
802
|
+
prepushFixModel: z.string().default(""),
|
|
803
|
+
prepushFixEffort: z.string().default(""),
|
|
805
804
|
});
|
|
806
805
|
export type SandboxSettings = z.infer<typeof SandboxSettingsSchema>;
|
|
807
806
|
|
|
@@ -2236,95 +2235,51 @@ export type CiRunParam = z.infer<typeof CiRunParamSchema>;
|
|
|
2236
2235
|
export const CiFixResponseSchema = z.object({ conversationId: z.string() });
|
|
2237
2236
|
export type CiFixResponse = z.infer<typeof CiFixResponseSchema>;
|
|
2238
2237
|
|
|
2239
|
-
/* ---- the
|
|
2240
|
-
*
|
|
2241
|
-
* WHERE THIS SITS, and why it is not one of the four other places it could:
|
|
2242
|
-
*
|
|
2243
|
-
* A fleet of 5-20 agents lands work into the main tree as UNCOMMITTED changes (agents/land.ts), the user
|
|
2244
|
-
* reviews and commits it by parts, pushes, and CI answers minutes later. This gate front-runs that answer by
|
|
2245
|
-
* asking the same question of the same artifact, before the push.
|
|
2238
|
+
/* ---- the pre-push check: the workspace's own answer to "would this push go red" ----
|
|
2246
2239
|
*
|
|
2247
|
-
*
|
|
2248
|
-
*
|
|
2249
|
-
*
|
|
2250
|
-
*
|
|
2251
|
-
* composite, and two agents editing one contract each go green alone and red together. The composite is the
|
|
2252
|
-
* only honest artifact, and it exists in exactly one place — the main working tree.
|
|
2240
|
+
* WHERE THIS SITS. A fleet of 5-20 agents lands work into the main tree, the user reviews and commits it by
|
|
2241
|
+
* parts, pushes, and CI answers minutes later. The check front-runs that answer at the push itself — the last
|
|
2242
|
+
* moment before the work leaves the machine, and the first moment at which what will be pushed is finally
|
|
2243
|
+
* settled.
|
|
2253
2244
|
*
|
|
2254
|
-
*
|
|
2255
|
-
*
|
|
2256
|
-
*
|
|
2245
|
+
* WHY THE PUSH AND NOT THE LAND, which is where this used to run. A post-land verdict is about a tree that
|
|
2246
|
+
* keeps moving: the user commits by parts, another agent lands, an edit arrives — so the verdict spent its life
|
|
2247
|
+
* either stale or being recomputed, and needed a content fingerprint, a staleness rule and a badge to say which.
|
|
2248
|
+
* All of that machinery existed to answer a question the push asks for free, because at the push there is
|
|
2249
|
+
* exactly one artifact and the user is standing in front of it waiting.
|
|
2257
2250
|
*
|
|
2258
|
-
*
|
|
2259
|
-
*
|
|
2260
|
-
*
|
|
2261
|
-
*
|
|
2262
|
-
* So: after the land, before the staging — the one window where the artifact is what CI will see, attribution
|
|
2263
|
-
* is still live, and nobody is waiting on it. */
|
|
2251
|
+
* SO THERE IS NO STORED VERDICT AND NOTHING IS POLLED AT REST. A run exists while it runs, reports to the
|
|
2252
|
+
* dialog that started it, and is gone. Nothing survives a daemon restart because nothing needs to: the next
|
|
2253
|
+
* push asks again. */
|
|
2264
2254
|
|
|
2265
|
-
/*
|
|
2255
|
+
/* Where a run is.
|
|
2266
2256
|
*
|
|
2267
|
-
* idle —
|
|
2268
|
-
* armed — work landed; the quiet period is counting down (see gateQuietMs).
|
|
2257
|
+
* idle — nothing has run in this daemon's life, or the last run was cleared.
|
|
2269
2258
|
* running — the check is live. `output` grows as it streams.
|
|
2270
|
-
* passed — exited 0
|
|
2271
|
-
* failed — exited non-zero, or was killed by
|
|
2272
|
-
* error — the
|
|
2273
|
-
* there is nothing wrong with the code — the
|
|
2259
|
+
* passed — exited 0. The push goes.
|
|
2260
|
+
* failed — exited non-zero, or was killed by prepushTimeoutMs (`timedOut`). The state a fix answers.
|
|
2261
|
+
* error — the check could not run at all: the command was not spawnable. NOT a fix-able failure, because
|
|
2262
|
+
* there is nothing wrong with the code — the command is misconfigured, and saying "tests failed"
|
|
2274
2263
|
* would send an agent hunting a bug that isn't there.
|
|
2275
|
-
* cancelled — the user stopped the run
|
|
2264
|
+
* cancelled — the user stopped the run.
|
|
2276
2265
|
*/
|
|
2277
|
-
export const
|
|
2278
|
-
export type
|
|
2279
|
-
|
|
2280
|
-
// An agent whose landed work the failure implicates. `paths` are its attributed files that the check's own
|
|
2281
|
-
// output NAMED — empty when the output named none of them, which is the honest shape for a failure that could
|
|
2282
|
-
// not be pinpointed (an integration break between two deltas, a suite that prints no paths at all): the agent
|
|
2283
|
-
// is listed because its work is in the tree under test, not because anything accused it.
|
|
2284
|
-
export const GateAgentSchema = z.object({
|
|
2285
|
-
agentId: z.string(),
|
|
2286
|
-
title: z.string().optional(),
|
|
2287
|
-
provider: AgentProviderSchema.optional(),
|
|
2288
|
-
paths: z.array(z.string()),
|
|
2289
|
-
});
|
|
2290
|
-
export type GateAgent = z.infer<typeof GateAgentSchema>;
|
|
2291
|
-
|
|
2292
|
-
// The fix turn one red verdict got. It is a workspace conversation because the composite it must reproduce
|
|
2293
|
-
// lives as uncommitted content in the main working tree; it still has the same stable registry identity as any
|
|
2294
|
-
// other conversation.
|
|
2295
|
-
export const GateFixSchema = z.object({
|
|
2296
|
-
startedAt: z.number(),
|
|
2297
|
-
conversationId: z.string(),
|
|
2298
|
-
// `running` while the turn streams; `done` when it ended cleanly, whatever the re-check then said;
|
|
2299
|
-
// `error` when the turn itself failed (a provider outage, no credential), which is worth distinguishing
|
|
2300
|
-
// because it is the one case where re-running the fix could still help.
|
|
2301
|
-
outcome: z.enum(["running", "done", "error"]),
|
|
2302
|
-
detail: z.string().optional(),
|
|
2303
|
-
});
|
|
2304
|
-
export type GateFix = z.infer<typeof GateFixSchema>;
|
|
2266
|
+
export const PrepushStatusSchema = z.enum(["idle", "running", "passed", "failed", "error", "cancelled"]);
|
|
2267
|
+
export type PrepushStatus = z.infer<typeof PrepushStatusSchema>;
|
|
2305
2268
|
|
|
2306
|
-
export const
|
|
2307
|
-
status:
|
|
2308
|
-
// The command this
|
|
2269
|
+
export const PrepushRunSchema = z.object({
|
|
2270
|
+
status: PrepushStatusSchema,
|
|
2271
|
+
// The command this run executed, echoed rather than read back from settings: a result read after the
|
|
2309
2272
|
// setting changed still has to say what produced it.
|
|
2310
2273
|
command: z.string(),
|
|
2311
2274
|
startedAt: z.number().optional(),
|
|
2312
2275
|
finishedAt: z.number().optional(),
|
|
2313
2276
|
exitCode: z.number().optional(),
|
|
2314
2277
|
timedOut: z.boolean().optional(),
|
|
2315
|
-
// The check's own output, tail-capped (
|
|
2278
|
+
// The check's own output, tail-capped (PREPUSH_OUTPUT_BYTES). The TAIL, not the head: a suite's verdict and
|
|
2316
2279
|
// its failure summary are at the end, and a head-capped buffer of a chatty build is all progress lines.
|
|
2317
2280
|
output: z.string(),
|
|
2318
|
-
/* WHICH TREE this verdict is about — HEAD plus the shape of every repo's uncommitted content. Recomputed
|
|
2319
|
-
* on read: when it no longer matches, the verdict is `stale` and the badge says so instead of asserting a
|
|
2320
|
-
* green light over a tree that has since moved. This is what keeps a passed verdict from outliving its
|
|
2321
|
-
* subject when the user edits, discards, or commits half of it. */
|
|
2322
|
-
fingerprint: z.string(),
|
|
2323
|
-
stale: z.boolean(),
|
|
2324
|
-
implicated: z.array(GateAgentSchema),
|
|
2325
|
-
fix: GateFixSchema.optional(),
|
|
2326
2281
|
});
|
|
2327
|
-
export type
|
|
2282
|
+
export type PrepushRun = z.infer<typeof PrepushRunSchema>;
|
|
2328
2283
|
|
|
2329
2284
|
// ---- drafts: agent-proposed posts awaiting owner approval (.intentic/drafts/<id>.json) ----
|
|
2330
2285
|
// One JSON file per draft. The AGENT creates drafts with its normal file tools — it can't call daemon routes,
|
|
@@ -2507,7 +2462,7 @@ export const BrowserPageSchema = z.object({
|
|
|
2507
2462
|
// The page's own title. Absent mid-navigation, which is exactly when a tab still needs to render.
|
|
2508
2463
|
title: z.string().optional(),
|
|
2509
2464
|
url: z.string(),
|
|
2510
|
-
// The page the agent last drove. Exactly one page
|
|
2465
|
+
// The page the agent last drove — on a finished session, the one it ended on. Exactly one page has it.
|
|
2511
2466
|
active: z.boolean(),
|
|
2512
2467
|
});
|
|
2513
2468
|
export const BrowserSessionSchema = z.object({
|
|
@@ -2521,6 +2476,9 @@ export const BrowserSessionSchema = z.object({
|
|
|
2521
2476
|
// still lists for a while, with the pages it had — the record of where the agent went.
|
|
2522
2477
|
running: z.boolean(),
|
|
2523
2478
|
activityAt: z.number(),
|
|
2479
|
+
// When that Chromium went away, for the "closed 20m ago" line a finished session leads with. Absent while
|
|
2480
|
+
// running, which is the same fact as `running` — but the view needs the timestamp, not just the flag.
|
|
2481
|
+
finishedAt: z.number().optional(),
|
|
2524
2482
|
pages: z.array(BrowserPageSchema),
|
|
2525
2483
|
});
|
|
2526
2484
|
export type BrowserPage = z.infer<typeof BrowserPageSchema>;
|
|
@@ -2529,6 +2487,75 @@ export const BrowsersListSchema = z.object({ sessions: z.array(BrowserSessionSch
|
|
|
2529
2487
|
export type BrowsersList = z.infer<typeof BrowsersListSchema>;
|
|
2530
2488
|
export const BrowserNameParamSchema = z.object({ name: z.string() });
|
|
2531
2489
|
|
|
2490
|
+
/* ---- subagents: the agents an agent starts ----
|
|
2491
|
+
*
|
|
2492
|
+
* The third thing a turn spawns that the operator can be shown, after its shell and its browser — and the only
|
|
2493
|
+
* one that is itself an agent. Two kinds land in this one list, because from outside they are the same fact
|
|
2494
|
+
* (another agent, working, that you did not start):
|
|
2495
|
+
* • `subagent` — the SDK's Agent/Task tool. The daemon learns of it from the SubagentStart/SubagentStop hooks
|
|
2496
|
+
* and the task_* stream messages, joined on `toolUseId`.
|
|
2497
|
+
* • `codex` / `grok` — a CLI the agent drove from its own Bash (agent/delegation.ts). Detected in the Bash
|
|
2498
|
+
* PreToolUse hook, bound to its thread/session id from the command's output.
|
|
2499
|
+
*
|
|
2500
|
+
* `id` IS THE SPAWNING TOOL CALL'S id — the Agent card's, or the Bash card's for a delegation. It is the one key
|
|
2501
|
+
* every source already carries (the SDK's subagent meta, its task_* messages, and the `parentToolUseId` the
|
|
2502
|
+
* client nests inner frames under), so nothing has to be correlated: a card links to its subagent with the id it
|
|
2503
|
+
* already has, and the subagent points back at the card the same way. The ids the transcripts are actually READ
|
|
2504
|
+
* with — the SDK's agent id, a Codex thread, an OpenCode session — stay daemon-side, because no surface asks a
|
|
2505
|
+
* question they answer.
|
|
2506
|
+
*
|
|
2507
|
+
* WHAT A KIND CHANGES, and it is only ever the live view: a subagent has no process of its own to look at, so
|
|
2508
|
+
* watching it means reading its transcript. A delegation runs in a tmux window, so it has both — `terminal`
|
|
2509
|
+
* names it, and the card keeps its existing "Watch in terminal" beside the transcript door. */
|
|
2510
|
+
export const SubagentKindSchema = z.enum(["subagent", "codex", "grok"]);
|
|
2511
|
+
export type SubagentKind = z.infer<typeof SubagentKindSchema>;
|
|
2512
|
+
|
|
2513
|
+
// running/pending are live; the rest are terminal. Deliberately the SDK's own task vocabulary
|
|
2514
|
+
// (SDKTaskUpdatedMessage.patch.status) rather than AgentStatus: this is not a fleet card's lifecycle (no
|
|
2515
|
+
// draft/landed/conflict), and mapping the two would invent states neither side reports.
|
|
2516
|
+
export const SubagentStatusSchema = z.enum(["pending", "running", "completed", "failed", "killed", "paused"]);
|
|
2517
|
+
export type SubagentStatus = z.infer<typeof SubagentStatusSchema>;
|
|
2518
|
+
|
|
2519
|
+
export const SubagentSessionSchema = z.object({
|
|
2520
|
+
id: z.string(),
|
|
2521
|
+
kind: SubagentKindSchema,
|
|
2522
|
+
// The conversation whose turn spawned this — what the area groups its rows by, and the way back to the chat
|
|
2523
|
+
// the card lives in.
|
|
2524
|
+
conversationId: z.string(),
|
|
2525
|
+
// What it is and what it was asked to do: the subagent type (`Explore`, `general-purpose`) or the delegated
|
|
2526
|
+
// provider's model, and the caller's one-line description. The area's row and the card's title read as
|
|
2527
|
+
// `Explore · Locate claimIndexer definition`.
|
|
2528
|
+
agentType: z.string().optional(),
|
|
2529
|
+
description: z.string().optional(),
|
|
2530
|
+
model: z.string().optional(),
|
|
2531
|
+
// How deep in the spawn tree (1 = spawned by the turn itself). From the SDK's meta.json; a subagent may
|
|
2532
|
+
// itself delegate, and a flat list that cannot say so reads as though the turn started all of them.
|
|
2533
|
+
spawnDepth: z.number().optional(),
|
|
2534
|
+
// Backgrounded: the parent went on working instead of waiting for it. This is the whole reason the list
|
|
2535
|
+
// exists — a backgrounded child used to be invisible until its result landed, sometimes minutes later.
|
|
2536
|
+
background: z.boolean().optional(),
|
|
2537
|
+
status: SubagentStatusSchema,
|
|
2538
|
+
startedAt: z.number(),
|
|
2539
|
+
endedAt: z.number().optional(),
|
|
2540
|
+
activityAt: z.number(),
|
|
2541
|
+
// What it has spent and done so far (task_progress). Tokens are the child's own, so a parent's cost line and
|
|
2542
|
+
// the sum of its children's are two different true numbers.
|
|
2543
|
+
tokens: z.number().optional(),
|
|
2544
|
+
toolUses: z.number().optional(),
|
|
2545
|
+
lastTool: z.string().optional(),
|
|
2546
|
+
// Its report — the last assistant message (SubagentStop) or the task summary. The answer to "what did it
|
|
2547
|
+
// conclude?" without opening the transcript, which is the question a finished child is read for.
|
|
2548
|
+
summary: z.string().optional(),
|
|
2549
|
+
error: z.string().optional(),
|
|
2550
|
+
// A delegation's live view: the tmux session its command runs in. Absent for an SDK subagent, which has no
|
|
2551
|
+
// process of its own to attach to.
|
|
2552
|
+
terminal: z.string().optional(),
|
|
2553
|
+
});
|
|
2554
|
+
export type SubagentSession = z.infer<typeof SubagentSessionSchema>;
|
|
2555
|
+
export const SubagentsListSchema = z.object({ sessions: z.array(SubagentSessionSchema) });
|
|
2556
|
+
export type SubagentsList = z.infer<typeof SubagentsListSchema>;
|
|
2557
|
+
export const SubagentIdParamSchema = z.object({ id: z.string() });
|
|
2558
|
+
|
|
2532
2559
|
// ---- environment: the overlay Dockerfile extending the sandbox image ----
|
|
2533
2560
|
// The approved file is DAEMON-COMPOSED: pinned FROM + capability fragments + the owner-approved custom section.
|
|
2534
2561
|
// The agent writes the proposal file (.intentic/environment.Dockerfile — custom-section content only, no FROM)
|
package/src/title.ts
CHANGED
|
@@ -15,8 +15,12 @@
|
|
|
15
15
|
* names a turn that arrived without one — an automation, a Discord message, a webchat visitor (agents-
|
|
16
16
|
* registry.ts). One rule, because two would let the same prompt open under two different names depending on
|
|
17
17
|
* where it entered. Nothing here calls a model: the title has to exist before the first frame comes back.
|
|
18
|
-
*
|
|
19
|
-
*
|
|
18
|
+
*
|
|
19
|
+
* Which is exactly the ceiling on it. Cutting is not naming — what comes out is the user's own sentence,
|
|
20
|
+
* shortened, and a column of those is scannable only where the users' sentences happened to differ early. The
|
|
21
|
+
* name a conversation ends up WEARING is written a second or two later by a model that reads the same prompt
|
|
22
|
+
* and answers in the fleet board's own shape (the daemon's title-namer.ts). This is the title that holds the
|
|
23
|
+
* tab until that arrives, and the one it keeps if nothing is connected to write a better one. */
|
|
20
24
|
|
|
21
25
|
// The registry's title budget (agents-registry MAX_TITLE_LENGTH, the rename input's maxlength) — the widest
|
|
22
26
|
// any surface stores. Every surface truncates in CSS to its own width, so the clamp here is a storage cap,
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import type { FileContribution } from "@intentic/extension-api";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { staleQueryKeys, WORKSPACE_STATE_FILES } from "./workspace-state.js";
|
|
4
|
+
|
|
5
|
+
// What the automations extension declares in its manifest, and what the memory extension WOULD declare if the
|
|
6
|
+
// watcher reported its files. Literals rather than the real manifests: an extension package importing this one is
|
|
7
|
+
// the dependency direction, so reaching back for them here would invert it. The real manifests are checked
|
|
8
|
+
// against this rule where they are loaded — web's fileBindings.test.ts and the daemon's file-bindings.test.ts.
|
|
9
|
+
const AUTOMATIONS: readonly FileContribution[] = [
|
|
10
|
+
{ path: `.intentic/automations.json`, invalidates: [`automations`] },
|
|
11
|
+
{ path: `.intentic/approvals/`, invalidates: [`automation-approvals`] },
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
describe(`staleQueryKeys`, () => {
|
|
15
|
+
it(`maps a manifest write to the queries it makes stale`, () => {
|
|
16
|
+
expect(staleQueryKeys([`.intentic/capabilities.json`], [])).toEqual([`capabilities`, `environment`, `panels`]);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it(`matches a name family and a one-file-per-entry directory through one prefix each`, () => {
|
|
20
|
+
// environment.Dockerfile, environment.custom.Dockerfile, environment.approved.Dockerfile — one entry.
|
|
21
|
+
expect(staleQueryKeys([`.intentic/environment.custom.Dockerfile`], [])).toEqual([`environment`]);
|
|
22
|
+
expect(staleQueryKeys([`.intentic/drafts/post-1.json`], [])).toEqual([`drafts`]);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it(`refreshes the Drafts view when the AGENT writes a draft`, () => {
|
|
26
|
+
// The regression this table was reorganized around: the drafts skill writes these files directly, so
|
|
27
|
+
// there is no browser mutation to hang an invalidate on — the watcher push is the only signal, and it
|
|
28
|
+
// used to be dropped on the floor.
|
|
29
|
+
expect(staleQueryKeys([`.intentic/drafts/post-1.json`], [])).toEqual([`drafts`]);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it(`ignores unrelated churn under .intentic/`, () => {
|
|
33
|
+
// The amplification that turned an iq index rebuild into an endless request storm: a prefix test on
|
|
34
|
+
// `.intentic/` alone would invalidate every one of these queries for each index write.
|
|
35
|
+
expect(staleQueryKeys([`.intentic/iq/index.db`, `.intentic/claude/projects/p/session.jsonl`], [])).toEqual([]);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it(`ignores a store's own temp file while it is mid-swap`, () => {
|
|
39
|
+
// jsonFile writes `.<name>.<pid>.tmp` beside the target precisely so the atomic rename can't be read as
|
|
40
|
+
// a write to the target itself. A trailing-tag temp would prefix-match and bill an extra refetch.
|
|
41
|
+
expect(staleQueryKeys([`.intentic/.settings.json.42.tmp`], [])).toEqual([]);
|
|
42
|
+
expect(staleQueryKeys([`.intentic/settings.json`], [])).toEqual([`settings`]);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it(`ignores ordinary workspace edits`, () => {
|
|
46
|
+
expect(staleQueryKeys([`src/main.ts`, `README.md`], [])).toEqual([]);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it(`dedupes keys across a batch that touches several manifests`, () => {
|
|
50
|
+
// A capability add recomposes the overlay, so both entries claim `environment` — one refetch, not two.
|
|
51
|
+
expect(staleQueryKeys([`.intentic/capabilities.json`, `.intentic/environment.Dockerfile`], [])).toEqual([
|
|
52
|
+
`capabilities`,
|
|
53
|
+
`environment`,
|
|
54
|
+
`panels`,
|
|
55
|
+
]);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it(`invalidates an extension's queries from its own declaration`, () => {
|
|
59
|
+
expect(staleQueryKeys([`.intentic/automations.json`], AUTOMATIONS)).toEqual([`automations`]);
|
|
60
|
+
expect(staleQueryKeys([`.intentic/approvals/a1.json`], AUTOMATIONS)).toEqual([`automation-approvals`]);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it(`makes nothing stale for an extension that is not running`, () => {
|
|
64
|
+
// The reason the live set is passed in rather than read off the installed list: `automations` is the
|
|
65
|
+
// extension's query key, so with the extension gone there is no cache entry for it to be about. The core
|
|
66
|
+
// table used to carry these two keys itself, and would have kept invalidating them either way.
|
|
67
|
+
expect(staleQueryKeys([`.intentic/automations.json`, `.intentic/approvals/a1.json`], [])).toEqual([]);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it(`lets an extension claim a path the core table deliberately ignores`, () => {
|
|
71
|
+
// The two lists are unioned flat, not layered: a narrow extension entry under a broad core entry that
|
|
72
|
+
// invalidates nothing must still fire. Without this, every path beneath one of the daemon's
|
|
73
|
+
// machine-state prefixes would be unreachable to extensions.
|
|
74
|
+
const nested: readonly FileContribution[] = [{ path: `.intentic/claude/projects/p/memory/`, invalidates: [`memory`] }];
|
|
75
|
+
expect(staleQueryKeys([`.intentic/claude/projects/p/memory/note.md`], nested)).toEqual([`memory`]);
|
|
76
|
+
// …and a sibling under the same core prefix stays ignored.
|
|
77
|
+
expect(staleQueryKeys([`.intentic/claude/projects/p/session.jsonl`], nested)).toEqual([]);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it(`dedupes a key two extensions both claim`, () => {
|
|
81
|
+
const twice: readonly FileContribution[] = [
|
|
82
|
+
{ path: `.intentic/automations.json`, invalidates: [`automations`] },
|
|
83
|
+
{ path: `.intentic/automations.json`, invalidates: [`automations`] },
|
|
84
|
+
];
|
|
85
|
+
expect(staleQueryKeys([`.intentic/automations.json`], twice)).toEqual([`automations`]);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe(`WORKSPACE_STATE_FILES`, () => {
|
|
90
|
+
it(`declares every entry under .intentic/, root-relative and forward-slash`, () => {
|
|
91
|
+
for (const file of WORKSPACE_STATE_FILES) {
|
|
92
|
+
expect(file.path.startsWith(`.intentic/`), file.path).toBe(true);
|
|
93
|
+
expect(file.path.includes(`\\`), file.path).toBe(false);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it(`states a reason for every entry that invalidates nothing`, () => {
|
|
98
|
+
// An empty `invalidates` is a real answer (daemon machine state, a deliberately-polled surface, a path
|
|
99
|
+
// whose query keys belong to an extension), but a SILENT one is indistinguishable from the omission this
|
|
100
|
+
// table exists to prevent — which is exactly how drafts went missing. Requiring the reason is what makes
|
|
101
|
+
// the difference visible at review time.
|
|
102
|
+
for (const file of WORKSPACE_STATE_FILES) {
|
|
103
|
+
if (file.invalidates.length === 0) {
|
|
104
|
+
expect(file.why, `${file.path} invalidates nothing and must say why`).toBeTruthy();
|
|
105
|
+
} else {
|
|
106
|
+
expect(file.why, `${file.path} invalidates queries, so \`why\` is dead weight`).toBeUndefined();
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it(`keeps directory entries slash-terminated so they cannot swallow a sibling`, () => {
|
|
112
|
+
// `.intentic/drafts` without the slash would also prefix-match a future `.intentic/drafts-archive.json`.
|
|
113
|
+
for (const file of WORKSPACE_STATE_FILES.filter((entry) => entry.invalidates.length > 0)) {
|
|
114
|
+
const isFamilyPrefix = file.path.endsWith(`.`);
|
|
115
|
+
const isFile = file.path.endsWith(`.json`) || file.path.endsWith(`.Dockerfile`);
|
|
116
|
+
expect(isFile || isFamilyPrefix || file.path.endsWith(`/`), file.path).toBe(true);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it(`has no entry that prefix-matches another, so one write can't be billed twice`, () => {
|
|
121
|
+
for (const file of WORKSPACE_STATE_FILES) {
|
|
122
|
+
const overlapping = WORKSPACE_STATE_FILES.filter((other) => other !== file && other.path.startsWith(file.path));
|
|
123
|
+
expect(
|
|
124
|
+
overlapping.map((other) => other.path),
|
|
125
|
+
`${file.path} is a prefix of another entry`,
|
|
126
|
+
).toEqual([]);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
});
|