@intentic/sandbox-contract 1.233.0 → 1.234.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 (120) hide show
  1. package/README.md +4 -3
  2. package/dist/agent-catalog.d.ts +2 -1
  3. package/dist/agent-catalog.d.ts.map +1 -1
  4. package/dist/agent-catalog.js +19 -13
  5. package/dist/agent-catalog.js.map +1 -1
  6. package/dist/chores/chores.d.ts.map +1 -1
  7. package/dist/chores/chores.js +36 -1
  8. package/dist/chores/chores.js.map +1 -1
  9. package/dist/chores/probes.d.ts.map +1 -1
  10. package/dist/chores/probes.js +70 -0
  11. package/dist/chores/probes.js.map +1 -1
  12. package/dist/command-classes.d.ts +5 -2
  13. package/dist/command-classes.d.ts.map +1 -1
  14. package/dist/command-classes.js +34 -13
  15. package/dist/command-classes.js.map +1 -1
  16. package/dist/contracts/automations.contract.d.ts +30 -0
  17. package/dist/contracts/automations.contract.d.ts.map +1 -1
  18. package/dist/contracts/chores.contract.d.ts +17 -0
  19. package/dist/contracts/chores.contract.d.ts.map +1 -1
  20. package/dist/contracts/extensions.contract.d.ts +2 -0
  21. package/dist/contracts/extensions.contract.d.ts.map +1 -1
  22. package/dist/contracts/extensions.contract.js.map +1 -1
  23. package/dist/contracts/issues.contract.d.ts +89 -0
  24. package/dist/contracts/issues.contract.d.ts.map +1 -0
  25. package/dist/contracts/issues.contract.js +50 -0
  26. package/dist/contracts/issues.contract.js.map +1 -0
  27. package/dist/contracts/runner.contract.d.ts +102 -102
  28. package/dist/contracts/settings.contract.d.ts +12 -0
  29. package/dist/contracts/settings.contract.d.ts.map +1 -1
  30. package/dist/credential-material.d.ts +2 -0
  31. package/dist/credential-material.d.ts.map +1 -0
  32. package/dist/credential-material.js +36 -0
  33. package/dist/credential-material.js.map +1 -0
  34. package/dist/definition.d.ts +8 -0
  35. package/dist/definition.d.ts.map +1 -1
  36. package/dist/history-state.d.ts.map +1 -1
  37. package/dist/history-state.js +1 -0
  38. package/dist/history-state.js.map +1 -1
  39. package/dist/hostnames.d.ts +2 -0
  40. package/dist/hostnames.d.ts.map +1 -1
  41. package/dist/hostnames.js +3 -1
  42. package/dist/hostnames.js.map +1 -1
  43. package/dist/index.d.ts +268 -116
  44. package/dist/index.d.ts.map +1 -1
  45. package/dist/index.js +5 -0
  46. package/dist/index.js.map +1 -1
  47. package/dist/runtime-state.d.ts +9 -0
  48. package/dist/runtime-state.d.ts.map +1 -1
  49. package/dist/runtime-state.js +3 -0
  50. package/dist/runtime-state.js.map +1 -1
  51. package/dist/schemas/agent.d.ts +6 -0
  52. package/dist/schemas/agent.d.ts.map +1 -1
  53. package/dist/schemas/agent.js +2 -1
  54. package/dist/schemas/agent.js.map +1 -1
  55. package/dist/schemas/automations.d.ts +45 -0
  56. package/dist/schemas/automations.d.ts.map +1 -1
  57. package/dist/schemas/automations.js +6 -1
  58. package/dist/schemas/automations.js.map +1 -1
  59. package/dist/schemas/extension-updates.d.ts +2 -0
  60. package/dist/schemas/extension-updates.d.ts.map +1 -1
  61. package/dist/schemas/extension-updates.js +3 -1
  62. package/dist/schemas/extension-updates.js.map +1 -1
  63. package/dist/schemas/issues.d.ts +324 -0
  64. package/dist/schemas/issues.d.ts.map +1 -0
  65. package/dist/schemas/issues.js +107 -0
  66. package/dist/schemas/issues.js.map +1 -0
  67. package/dist/schemas/maintenance.d.ts +62 -1
  68. package/dist/schemas/maintenance.d.ts.map +1 -1
  69. package/dist/schemas/maintenance.js +22 -1
  70. package/dist/schemas/maintenance.js.map +1 -1
  71. package/dist/schemas/settings.d.ts +6 -0
  72. package/dist/schemas/settings.d.ts.map +1 -1
  73. package/dist/schemas/settings.js +4 -0
  74. package/dist/schemas/settings.js.map +1 -1
  75. package/dist/schemas/terminal.d.ts.map +1 -1
  76. package/dist/schemas/terminal.js.map +1 -1
  77. package/dist/schemas/webext.d.ts +24 -0
  78. package/dist/schemas/webext.d.ts.map +1 -1
  79. package/dist/schemas/webext.js +9 -0
  80. package/dist/schemas/webext.js.map +1 -1
  81. package/dist/webext-links.d.ts +1 -0
  82. package/dist/webext-links.d.ts.map +1 -1
  83. package/dist/webext-links.js +1 -0
  84. package/dist/webext-links.js.map +1 -1
  85. package/dist/workspace-state.d.ts +10 -6
  86. package/dist/workspace-state.d.ts.map +1 -1
  87. package/dist/workspace-state.js +13 -8
  88. package/dist/workspace-state.js.map +1 -1
  89. package/package.json +8 -8
  90. package/src/agent-catalog.test.ts +55 -54
  91. package/src/agent-catalog.ts +29 -22
  92. package/src/capability-ledger.test.ts +11 -2
  93. package/src/chores/chores.test.ts +1 -1
  94. package/src/chores/chores.ts +66 -1
  95. package/src/chores/probes.test.ts +65 -0
  96. package/src/chores/probes.ts +133 -1
  97. package/src/chores/verdict.test.ts +29 -19
  98. package/src/command-classes.test.ts +71 -4
  99. package/src/command-classes.ts +112 -22
  100. package/src/contracts/extensions.contract.ts +3 -2
  101. package/src/contracts/issues.contract.ts +60 -0
  102. package/src/credential-material.test.ts +120 -0
  103. package/src/credential-material.ts +100 -0
  104. package/src/history-state.ts +12 -0
  105. package/src/hostnames.ts +18 -2
  106. package/src/index.ts +5 -0
  107. package/src/routes.test.ts +4 -2
  108. package/src/runtime-state.ts +16 -0
  109. package/src/schemas/agent.ts +15 -2
  110. package/src/schemas/automations.ts +14 -2
  111. package/src/schemas/extension-updates.ts +3 -1
  112. package/src/schemas/issues.ts +279 -0
  113. package/src/schemas/maintenance.ts +51 -1
  114. package/src/schemas/settings.ts +32 -0
  115. package/src/schemas/terminal.ts +6 -4
  116. package/src/schemas/webext.ts +29 -0
  117. package/src/tunnel-ids.test.ts +3 -1
  118. package/src/webext-links.ts +10 -0
  119. package/src/workspace-state.test.ts +3 -1
  120. package/src/workspace-state.ts +27 -37
@@ -13,7 +13,7 @@ import { WorkspaceHotspotSchema, WorkspaceKeyModuleSchema } from "./codebase-hea
13
13
  * signals things the daemon already knows, the resident iq index's health ranking, the package manifests it
14
14
  * reads for the dependency graph, its own node version. Recomputed per request; all of it is cheap. */
15
15
 
16
- export const PROBE_IDS = ["outdated", "audit", "knip", "jscpd", "ui", "bundle"] as const;
16
+ export const PROBE_IDS = ["outdated", "audit", "knip", "jscpd", "ui", "bundle", "mutation"] as const;
17
17
  export const ProbeIdSchema = z.enum(PROBE_IDS);
18
18
  export type ProbeId = z.infer<typeof ProbeIdSchema>;
19
19
  // One dependency the registry has moved past. `kind` is the SEMVER distance, which is the whole reason this is
@@ -165,6 +165,55 @@ export const BundleSchema = z.object({
165
165
  .describe("What is in it, piece by piece."),
166
166
  });
167
167
  export type Bundle = z.infer<typeof BundleSchema>;
168
+ /* WHAT THE SUITE WOULD NOTICE IF THE CODE BROKE. Coverage says a line ran; this says an assertion depended on it.
169
+ *
170
+ * The distinction is the whole reason this probe exists, and it is not theoretical here. Measured on
171
+ * sandbox-contract's chore module — 109 hand-written tests, every line covered — 16 of 58 mutants survived, and
172
+ * one of them flips the zero boundary in `bucketOf` that digest.ts's own comment argues is load-bearing. The test
173
+ * that was supposed to hold it (`expect(bucketOf(0)).not.toBe(bucketOf(1))`) is written in the careful,
174
+ * deliberately un-brittle style, and that is exactly why it cannot see the change: with the boundary moved the two
175
+ * values are still different, so the assertion still passes.
176
+ *
177
+ * That is the failure this measures and nothing else in the repository can. A linter sees the assertion's shape,
178
+ * not its power; a coverage report sees the line, not whether anything checked it. Only killing the code and
179
+ * watching what the suite says distinguishes a test from a test-shaped thing.
180
+ *
181
+ * SURVIVORS, NOT JUST A SCORE. A percentage is a mood; `bucketOf: count <= 0 → count < 0 survives` is a morning's
182
+ * work with the answer already in it. The score decides whether the chore speaks, the survivors are what makes it
183
+ * worth speaking about. */
184
+ export const MutationScoreSchema = z.object({
185
+ score: z
186
+ .number()
187
+ .describe("The share of injected faults the suite caught. Not a coverage figure: coverage says a line ran, this says an assertion depended on it."),
188
+ killed: z.number().int().nonnegative().describe("Faults the suite caught."),
189
+ survived: z.number().int().nonnegative().describe("Faults it did not: code that can be broken with every test still green."),
190
+ /* Mutants that never got a verdict: ones that would not compile, and ones the configuration ignored. Kept
191
+ * apart from both counts above, and OUT of the score, because an unmeasured mutant is not evidence either
192
+ * way — the same conflation `unavailable` exists to prevent one level up.
193
+ *
194
+ * A timeout is deliberately NOT here. Stryker counts it as detected, on the reasoning that a mutant which
195
+ * hangs the suite is one the suite noticed, and this follows Stryker's arithmetic rather than inventing a
196
+ * second definition of the same word: the number on the row has to mean what the tool that produced it
197
+ * means, or the row is quietly arguing with its own evidence. */
198
+ inconclusive: z
199
+ .number()
200
+ .int()
201
+ .nonnegative()
202
+ .describe("Faults it never got a verdict on, because they would not compile or were configured out. Left out of the score entirely, since neither answer is known."),
203
+ /* The worst offenders, named. Capped, and the cap is the point: a survivor list is only useful while it is
204
+ * short enough to act on, and the rest are still there on the next run. */
205
+ survivors: z
206
+ .array(
207
+ z.object({
208
+ file: z.string().describe("Where it is."),
209
+ line: z.number().int().nonnegative().describe("Which line."),
210
+ mutator: z.string().describe("What was changed, in the mutation tool's own vocabulary."),
211
+ replacement: z.string().describe("What it became, so a reader can judge whether it matters without opening the file."),
212
+ }),
213
+ )
214
+ .describe("The surviving faults themselves. A percentage is a mood; a named line with the change that went unnoticed is a morning's work."),
215
+ });
216
+ export type MutationScore = z.infer<typeof MutationScoreSchema>;
168
217
  /* One probe's cached result. The three states are deliberately distinct, because a panel that collapses them
169
218
  * lies about the most important case:
170
219
  * ok the tool ran and reported. `facts` carries its findings, including "nothing found", which is
@@ -187,6 +236,7 @@ export const ProbeFactsSchema = z.discriminatedUnion("id", [
187
236
  z.object({ id: z.literal("jscpd"), duplication: DuplicationSchema }),
188
237
  z.object({ id: z.literal("ui"), scan: UiScanSchema }),
189
238
  z.object({ id: z.literal("bundle"), bundle: BundleSchema }),
239
+ z.object({ id: z.literal("mutation"), mutation: MutationScoreSchema }),
190
240
  ]);
191
241
  export type ProbeFacts = z.infer<typeof ProbeFactsSchema>;
192
242
  export const ProbeResultSchema = z.object({
@@ -422,6 +422,38 @@ export const SandboxSettingsSchema = z.object({
422
422
  .describe(
423
423
  "Whether a version the agent is about to pin is checked against the package's own registry first. Facts only, or facts plus the name of a maintained replacement where the registry agrees the current choice has been abandoned. It tells the agent and lets it decide rather than refusing, because matching a version your project already uses is usually the right answer and a gate would fight it.",
424
424
  ),
425
+ /* DOES THE TEST IT JUST WROTE ACTUALLY TEST THE CHANGE?
426
+ *
427
+ * A model writes a test that passes, and a passing test is the thing it was asked for, so nothing in the loop
428
+ * objects. The failure this catches is the one where the test would have passed BEFORE the change too: it
429
+ * exercises the code without depending on the behaviour that was added, and it will keep passing when that
430
+ * behaviour is broken later. It type-checks, it lints, `pnpm verify` is green, and the suite has grown a test
431
+ * that can never fail.
432
+ *
433
+ * The check is a differential, and it is cheap for one specific reason: in an agent turn we already know
434
+ * exactly which source files changed. So the test is re-run with those files' HEAD contents served in place
435
+ * of the working copies, and the answer is one bit — did it still pass? A test that passes against the old
436
+ * code did not test the new code. This is one-mutant mutation testing where the mutant is "your change,
437
+ * reverted", which is the cheapest useful mutant there is and the only one already known for free.
438
+ *
439
+ * NOTHING IS WRITTEN TO THE TREE. The HEAD copies are served through a vite `load` hook in a generated config,
440
+ * never checked out over the working files — a hook that could leave a half-reverted tree behind on a crash
441
+ * would be trading a whole turn's work for a lint-grade signal.
442
+ *
443
+ * IT REPORTS AND GETS OUT OF THE WAY, for the same reason the freshness check above does. Two legitimate
444
+ * cases pass on HEAD and must not be fought: a test written BEFORE its implementation (it fails now, which
445
+ * the agent can see without help), and a pure refactor, where a test that keeps passing is the entire point.
446
+ * Distinguishing those from a weak test needs intent, so the fact rides back as context and the model decides.
447
+ *
448
+ * Scoped to source changed in the SAME package as the test, because that is what a package's own vitest run
449
+ * loads from source; a cross-package import resolves to the other package's built output, where there is
450
+ * nothing to swap. Off by default like every flag here, and off wires no hook at all. */
451
+ testFaultDetection: z
452
+ .boolean()
453
+ .default(false)
454
+ .describe(
455
+ "After the agent writes a test, re-run it against the code as it was before this turn's changes. A test that still passes did not test the change — it will pass just as happily when that behaviour breaks. Reports what it finds and lets the agent decide, because a test written before its implementation and a pure refactor both pass this honestly.",
456
+ ),
425
457
  outputCleaners: z
426
458
  .string()
427
459
  .default("")
@@ -10,10 +10,12 @@ import { z } from "zod";
10
10
  // Bash commands run in (live-watchable, AI-marked in the UI; running:false once every window is a finished
11
11
  // command's dead pane, which is what lets the panel sweep it), `job` = a job-* session the daemon's terminal
12
12
  // runner executes user-triggered flows in (capability adds, infra check), `process` = a managed background
13
- // process riding a panel session (an extension's declared processes, dockerd), surfaced in the panel's
14
- // background-processes popover with read-only log views, never as a killable tab; running is the actual
15
- // process (a lingering shell after a crash reads false). A process row that maps to an installed extension's
16
- // declared process carries extensionId+processName, the address for its /extensions start/stop routes. The
13
+ // process: a supervised daemon child for an extension's declared processes (name `svc-<key>`, whose
14
+ // "terminal" is a tail of the service's log file), or a tmux-riding one for dockerd and the local model
15
+ // servers, which outlive daemon restarts on purpose. Surfaced in the panel's background-processes popover
16
+ // with read-only log views, never as a killable tab; running is the actual process. A process row that maps
17
+ // to an installed extension's declared process carries extensionId+processName, the address for its
18
+ // /extensions start/stop routes. The
17
19
  // `{name}` kill-route param is a bare string validated in the handler (a bad name is a BAD_REQUEST) since the
18
20
  // same charset gates a `tmux kill-session -t` shell-out. The agent's BROWSER is deliberately NOT one of these
19
21
  // kinds: a Chromium with its own tab strip is a surface in its own right, not a pane in the terminal panel, so
@@ -77,3 +77,32 @@ export const WebExtSessionImportSchema = z.object({
77
77
  cookies: z.array(WebExtCookieSchema).min(1).max(300),
78
78
  });
79
79
  export type WebExtSessionImport = z.infer<typeof WebExtSessionImportSchema>;
80
+
81
+ /* ---- and the same door in the other direction: LENDING a sandbox session to the person's own browser ----
82
+ *
83
+ * The case this answers is the one no amount of streaming quality can: a passkey bound to an authenticator the
84
+ * person physically holds, a hardware second factor that has to be touched, an employer's SSO that checks the
85
+ * device. Driving the sandbox's browser remotely is not a worse experience on those sites, it is an impossible
86
+ * one — so the session goes to the browser the account was actually enrolled on, the person finishes the step
87
+ * as themselves, and `connect_site` hands it back.
88
+ *
89
+ * Same door and same rule as the import above: the cookies travel on the extension's own HTTPS request, never
90
+ * as a socket answer, because socket answers are MCP results and MCP results land in the model's context. */
91
+ export const WebExtSessionExportSchema = z.object({
92
+ // The `browser`-kind capability to lend FROM. Named by the agent out of the roster it can already read.
93
+ account: z.string().min(1),
94
+ // The registrable domain to lend, and the reason this cannot lend a whole profile: a profile holds every
95
+ // account its owner ever connected, and handing all of it over because one site got stuck is the kind of
96
+ // over-broad grant that is invisible until it matters.
97
+ domain: z.string().min(1),
98
+ });
99
+ export type WebExtSessionExport = z.infer<typeof WebExtSessionExportSchema>;
100
+
101
+ export const WebExtSessionExportResultSchema = z.object({
102
+ ok: z.boolean(),
103
+ // What the owner reads and the agent reads back. Never carries a cookie name or a value.
104
+ message: z.string(),
105
+ // Only ever read by the extension, which writes it straight into this browser's own cookie store.
106
+ cookies: z.array(WebExtCookieSchema).optional(),
107
+ });
108
+ export type WebExtSessionExportResult = z.infer<typeof WebExtSessionExportResultSchema>;
@@ -42,7 +42,9 @@ test("slots are not derivable from the sandbox id: only from the token behind it
42
42
  expect(portSlotsFromToken(OTHER)).not.toEqual(portSlotsFromToken(TOKEN));
43
43
  // No slot leaks the id it will be paired with in `port-<slot>-<sandboxId>`.
44
44
  const id = sandboxIdFromToken(TOKEN);
45
- expect(id).toBeDefined();
45
+ // Load-bearing, and not merely tidy: `not.toContain(undefined)` is true of every slot list ever produced,
46
+ // so without a real id the leak check below would pass on a function that had stopped deriving one at all.
47
+ expect(id).toEqual(expect.any(String));
46
48
  expect(portSlotsFromToken(TOKEN)).not.toContain(id);
47
49
  // And none of them is the old fixed alphabet, which is what made the hostnames guessable.
48
50
  expect(portSlotsFromToken(TOKEN).some((slot) => slot.length === 1)).toBe(false);
@@ -23,6 +23,16 @@ export const webextConnectUrl = (sandboxUrl: string): string => `${sandboxUrl.re
23
23
  * and the one thing that would be catastrophic to leak is the one thing the model never sees. */
24
24
  export const webextSessionUrl = (sandboxUrl: string): string => `${sandboxUrl.replace(/\/$/, "")}/system/webext/session`;
25
25
 
26
+ /* The same door in the other direction: where the extension asks for a sandbox account's session to be LENT to
27
+ * this browser, so the owner can finish a step no remote browser can do — a passkey, a hardware second factor,
28
+ * an employer's SSO that checks the device.
29
+ *
30
+ * A separate path rather than a verb on the one above, because the two are different acts with different
31
+ * refusals ("no such account" versus "that account is not signed in there") and a single endpoint doing both
32
+ * would have to be read twice to know which. Same bearer, and the same rule about the socket: the cookies come
33
+ * back on THIS request, never as an MCP result. */
34
+ export const webextLendUrl = (sandboxUrl: string): string => `${sandboxUrl.replace(/\/$/, "")}/system/webext/lend`;
35
+
26
36
  /* ---- the pairing code: the one string that travels from the sandbox's card into the extension ----
27
37
  *
28
38
  * A connected computer is paired by a shell one-liner, which can carry two values in two environment variables
@@ -162,7 +162,9 @@ describe(`WORKSPACE_STATE_FILES`, () => {
162
162
  // the difference visible at review time.
163
163
  for (const file of WORKSPACE_STATE_FILES) {
164
164
  if (file.invalidates.length === 0) {
165
- expect(file.why, `${file.path} invalidates nothing and must say why`).toBeTruthy();
165
+ // A string with something in it. Truthiness would accept the number 1 or an object, and the
166
+ // failure this guards against — a `why` left blank to quiet the check — reads as whitespace.
167
+ expect(file.why, `${file.path} invalidates nothing and must say why`).toEqual(expect.stringMatching(/\S/));
166
168
  } else {
167
169
  expect(file.why, `${file.path} invalidates queries, so \`why\` is dead weight`).toBeUndefined();
168
170
  }
@@ -301,6 +301,24 @@ const STATE_FILES = [
301
301
  why: "Declared by the intentic.automations extension's contributes.files, `automation-approvals` is its query key, not core's.",
302
302
  portability: "carry",
303
303
  },
304
+ /* The bug-report inbox, one file per fingerprint, written by the daemon's issues-store as reports arrive
305
+ * from the owner's own sites and apps, and rendered by the intentic.issues extension. Its invalidation is
306
+ * the extension's own (`issues`), declared in that manifest, the automations shape exactly.
307
+ *
308
+ * `carry` rather than `local`: an issue is a fact about the PRODUCT, not about this container. A workspace
309
+ * exported and restored elsewhere that arrived claiming nothing had ever crashed would have thrown away
310
+ * the one record that says which bug is worth fixing first, and the counts are the whole of that record.
311
+ *
312
+ * NOT `versioned`, and the drafts entry is the contrast worth reading: a draft is authored, reviewable and
313
+ * goes out under the owner's name, so it earns a diff. An issue is machine-recorded telemetry whose count
314
+ * moves on every crash: tracking it would put a commit's worth of churn in `git log` per bad afternoon,
315
+ * and nothing in it is a decision anybody made. */
316
+ {
317
+ path: ".intentic/records/issues/",
318
+ invalidates: [],
319
+ why: "Declared by the intentic.issues extension's contributes.files, `issues` is its query key, not core's.",
320
+ portability: "carry",
321
+ },
304
322
  /* The maintenance ledger and probe evidence, written by the daemon's chores-store and rendered by the
305
323
  * intentic.maintenance extension, the automations shape exactly: the path is the daemon's, the query keys
306
324
  * (`maintenance-report`, `maintenance-runs`) are the extension's own contributes.files. Point-in-time
@@ -359,6 +377,12 @@ const STATE_FILES = [
359
377
  why: "Which origins have loaded a Front Desk's widget, written on a 30s flush timer while a customer's site serves page views. The install panel that renders it fetches on open and polls itself while it is on screen, which is the whole window in which the answer changes for anyone. Pushing instead would bill every connected browser a refetch per flush, for a panel almost nobody has open.",
360
378
  portability: "carry",
361
379
  },
380
+ {
381
+ path: ".intentic/records/issue-installs.json",
382
+ invalidates: [],
383
+ why: "The same probe for the bug reporter's script, on the same flush timer and read by the same kind of panel, so it is outside the push path for the same reason the Front Desk's is.",
384
+ portability: "carry",
385
+ },
362
386
  {
363
387
  path: ".intentic/records/thread-sessions.json",
364
388
  invalidates: [],
@@ -805,34 +829,6 @@ export const REPORTED_MANIFEST_PATHS: readonly string[] = WORKSPACE_STATE_FILES.
805
829
  // them relative at the last moment, and normalizing at each call site is the one that eventually gets forgotten.
806
830
  export const isReportedManifest = (relPath: string): boolean => REPORTED_MANIFEST_PATHS.includes(relPath.replaceAll("\\", "/"));
807
831
 
808
- /* Old directory names are never read or migrated. Keep that finite set in one quarantine record so access,
809
- * export, and search cannot reinterpret abandoned machine state as ordinary workspace content after a rename.
810
- * `artifacts` still carry; the distinction here tells portability only which retired roots are secrets or
811
- * derived, and tells the state janitor which it may DELETE: a retired `derived` root is a rebuildable cache
812
- * by its own classification, so leaving 466 MB of abandoned model where only a manual `rm` reaches it was
813
- * quarantine doing half its job. Secret and artifact roots stay until an owner removes them by hand: deleting
814
- * content is not the janitor's call, only deleting what the class already says is disposable. */
815
- export const RETIRED_WORKSPACE_STATE_DIRS = {
816
- /* The flat spellings, from before the state dir was grouped into its five folders. Every entry in the table
817
- * used to sit directly under `.intentic/`, and a sandbox that predates the move still has those directories
818
- * on disk with real contents in them. They are quarantined rather than migrated, the workspace rule is fresh
819
- * state and no compatibility layers, but quarantine is exactly what they need, because the alternative is
820
- * worse than leaving them: `auth` at the old spelling is a real credential store that the classifier no
821
- * longer recognises, so without these names it would read as ordinary workspace content and be indexed by
822
- * search, carried by an export and copied down by the backup. Being listed here keeps the old copy as
823
- * untouchable as the new one, and lets the janitor delete the rebuildable half. */
824
- secret: ["claude", "codex", "kimi", "opencode", "cliproxy", "auth", "ci.json"],
825
- /* The ownership records at their OLD flat spelling, and a bucket of their own rather than a few more names
826
- * in `secret`, because the two classes differ on exactly the thing that matters for a leftover. A `secret`
827
- * travels when the owner opts in at export; an `identity` never travels at all, since a list of who may
828
- * drive this sandbox arriving in another one is how a source hands itself the target's ownership. Filed under
829
- * `secret` these would have become carryable by ticking a box, which is the one outcome their live entries
830
- * are written to prevent. */
831
- identity: ["owner.json", "members.json", "workspace.json", "control-tokens.json"],
832
- derived: ["iq", "extensions-runtime", "whisper", "browser/output", "browser", "cache", "runtime", "tmp", ".pnpm-store", "extensions", "verify"],
833
- artifacts: ["attachments", "acceptance", "loops", "workflow-runs", "transcripts", "artifacts", "sessions"],
834
- } as const;
835
-
836
832
  /* THE DAEMON'S OWN CONTROL PLANE, the entries directly under the workspace root's `.intentic/` that the file
837
833
  * API refuses to read, write, move or delete for anyone, the owner included (workspace/workspace-files.ts holds
838
834
  * the enforcement and the full reasoning for each name).
@@ -884,15 +880,9 @@ export const isLockedWorkspacePath = (relPath: string): boolean => {
884
880
  if (segments[0] !== STATE_DIR) {
885
881
  return false;
886
882
  }
887
- /* Two shapes, because two layouts exist on disk. A current entry is `<group>/<name>`; a leftover from before
888
- * the regrouping is a bare name directly under the state dir, and the quarantine record is what still knows
889
- * those are credentials. Both are locked, a stale `auth/` full of real tokens is no safer to open than the
890
- * live one, and the whole point of quarantining the old spellings was that nothing downstream reclassifies
891
- * them as ordinary content. */
892
- return (
893
- LOCKED_STATE_ENTRIES.has(segments.slice(1, 3).join("/")) ||
894
- (segments.length >= 2 && RETIRED_WORKSPACE_STATE_DIRS.secret.includes(segments[1] as never))
895
- );
883
+ // Two segments, which covers both spellings in the set above: a grouped entry (`secrets/auth`) matches as
884
+ // written, and a bare root entry (`claude.json`) joins to itself because there is no second segment to add.
885
+ return LOCKED_STATE_ENTRIES.has(segments.slice(1, 3).join("/"));
896
886
  };
897
887
 
898
888
  /* THE LOCKED ENTRIES THE ROOT REPO TRACKS, refused by the file API, and diffable anyway.