@sema-agent/core 7.25.0 → 7.26.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 (75) hide show
  1. package/CHANGELOG.md +81 -0
  2. package/dist/core/arg-summary.d.ts +0 -7
  3. package/dist/core/arg-summary.js +2 -1
  4. package/dist/core/ask-origin.d.ts +84 -0
  5. package/dist/core/ask-origin.js +31 -3
  6. package/dist/core/auto-compaction.d.ts +5 -0
  7. package/dist/core/auto-compaction.js +7 -0
  8. package/dist/core/auto-mode-directive.d.ts +45 -0
  9. package/dist/core/auto-mode-directive.js +47 -0
  10. package/dist/core/checkpoint-store.d.ts +10 -1
  11. package/dist/core/engine-notice.d.ts +10 -0
  12. package/dist/core/gate-lanes.js +18 -10
  13. package/dist/core/governance-codes.d.ts +1 -1
  14. package/dist/core/governance-codes.js +3 -0
  15. package/dist/core/hooks.d.ts +3 -0
  16. package/dist/core/mcp-probe.d.ts +122 -0
  17. package/dist/core/mcp-probe.js +132 -0
  18. package/dist/core/memory-engine/engine.d.ts +6 -1
  19. package/dist/core/memory-engine/engine.js +30 -2
  20. package/dist/core/memory-engine/index.d.ts +1 -0
  21. package/dist/core/memory-engine/index.js +1 -0
  22. package/dist/core/memory-engine/shadowing.d.ts +58 -0
  23. package/dist/core/memory-engine/shadowing.js +40 -0
  24. package/dist/core/memory-engine/tools.d.ts +33 -1
  25. package/dist/core/memory-engine/tools.js +136 -31
  26. package/dist/core/memory-engine/types.d.ts +10 -0
  27. package/dist/core/memory.d.ts +60 -0
  28. package/dist/core/memory.js +47 -2
  29. package/dist/core/permission-rule-store.d.ts +51 -8
  30. package/dist/core/permission-rule-store.js +112 -65
  31. package/dist/core/reminder-mint.d.ts +41 -0
  32. package/dist/core/reminder-mint.js +55 -0
  33. package/dist/core/runner/compaction-restate.d.ts +9 -0
  34. package/dist/core/runner/compaction-restate.js +20 -0
  35. package/dist/core/runner/contracts.d.ts +18 -1
  36. package/dist/core/runner/denial-limit-arms.js +1 -1
  37. package/dist/core/runner/permission-rule-lanes.d.ts +5 -17
  38. package/dist/core/runner/permission-rule-lanes.js +5 -23
  39. package/dist/core/runner/prepare-ask-lane.js +2 -0
  40. package/dist/core/runner/prepare-context-lane.d.ts +4 -0
  41. package/dist/core/runner/prepare-context-lane.js +7 -2
  42. package/dist/core/runner/prepare-inherited-gate.d.ts +5 -1
  43. package/dist/core/runner/prepare-inherited-gate.js +4 -2
  44. package/dist/core/runner/prepare-memory.js +47 -6
  45. package/dist/core/runner/prepare-park-ask.js +1 -0
  46. package/dist/core/runner/prepare-policy-chain.js +8 -4
  47. package/dist/core/runner/prepare-suspend-saga.d.ts +2 -0
  48. package/dist/core/runner/prepare-suspend-saga.js +3 -1
  49. package/dist/core/runner/prepare-task.js +7 -4
  50. package/dist/core/runner/run-leg.js +9 -1
  51. package/dist/core/runner/run-recovery-lanes.js +2 -2
  52. package/dist/core/runner/run-terminal-adoption.js +2 -2
  53. package/dist/core/runner/run-turn-boundary.js +2 -2
  54. package/dist/core/store-contracts/permission-rule-write-contract.d.ts +20 -0
  55. package/dist/core/store-contracts/permission-rule-write-contract.js +77 -0
  56. package/dist/core/task-result.d.ts +11 -0
  57. package/dist/core/task-spec.d.ts +31 -0
  58. package/dist/core/tool-catalog-entries.js +3 -3
  59. package/dist/core/tool-policy.d.ts +38 -0
  60. package/dist/core/tool-policy.js +35 -24
  61. package/dist/core/types.d.ts +1 -1
  62. package/dist/index.d.ts +7 -2
  63. package/dist/index.js +6 -1
  64. package/dist/stores/file/permission-rule-store.js +48 -29
  65. package/dist/tools/fs/bash-lexer.d.ts +13 -0
  66. package/dist/tools/fs/bash-lexer.js +61 -0
  67. package/dist/tools/fs/bash-program-position.d.ts +32 -3
  68. package/dist/tools/fs/bash-program-position.js +33 -2
  69. package/dist/tools/fs/bash-readonly-classifier.d.ts +6 -4
  70. package/dist/tools/fs/bash-readonly-classifier.js +55 -18
  71. package/dist/tools/fs/shell-read-plan.js +9 -11
  72. package/package.json +1 -1
  73. package/test/export-surface.snapshot.json +29 -1
  74. package/wire-changes/core@7.25.1.json +64 -0
  75. package/wire-changes/core@7.26.0.json +695 -0
@@ -6,6 +6,7 @@ import { formatMemoryAge } from "../memory-recall.js";
6
6
  import { committedOriginOf } from "./frontmatter.js";
7
7
  import { MEMORY_EXPOSURE_BANNER, MEMORY_EXPOSURE_HANDLE_TAG, MEMORY_PROVENANCE_INDEX_SENTENCE, MEMORY_PROVENANCE_SEARCH_SENTENCE, MEMORY_SEARCH_BAND_ORDER_HEADER } from "./provenance-wording.js";
8
8
  import { deriveSupersededSet, memorySupersededNote, MEMORY_SEARCH_SUPERSEDED_TAG } from "./consolidation.js";
9
+ import { deriveShadowedSet, memoryShadowedNote, shadowNamesOf, MEMORY_SEARCH_SHADOWED_TAG } from "./shadowing.js";
9
10
  import { toolFace } from "../tool-catalog-entries.js";
10
11
  export const MEMORY_SEARCH_TOOL_NAME = "memory_search";
11
12
  export const MEMORY_GET_TOOL_NAME = "memory_get";
@@ -69,9 +70,10 @@ const INDEX_DESCRIPTION = [
69
70
  "",
70
71
  `When to use it: when ${MEMORY_SEARCH_TOOL_NAME} comes up empty but the fact should exist, when the ` +
71
72
  `injected memory index says it was truncated, or to survey what is stored at all. The listing is ` +
72
- `complete for the ACTIVE set: an entry absent across all pages is either not mounted or superseded ` +
73
- `by a newer consolidated entry (superseded rows are hidden by default, their count is always ` +
74
- `disclosed, and includeSuperseded lists them too). The order is structural (scope, then name), NOT ` +
73
+ `complete for the ACTIVE set: an entry absent across all pages is either not mounted, superseded ` +
74
+ `by a newer consolidated entry, or shadowed by a same-named entry in this session's write home ` +
75
+ `(both are hidden by default, both counts are always disclosed, and includeSuperseded / ` +
76
+ `includeShadowed list them too). The order is structural (scope, then name), NOT ` +
75
77
  `by relevance — when checking whether something exists, page through to the last page before ` +
76
78
  `concluding it does not; the row you need may sit at the end, and early pages full of routine ` +
77
79
  `entries say nothing about the later ones. Pages read the live store: on a multi-page pass, send ` +
@@ -173,12 +175,56 @@ export function createMemoryEngineTools(opts) {
173
175
  const scopeSet = new Set(plane.scopes);
174
176
  return (await plane.backend.getByIds(ids)).filter((e) => scopeSet.has(e.scope) && e.frontmatter.deleted !== true);
175
177
  };
176
- const supersededOf = async (plane, exclusions) => {
178
+ const servedHeaders = async (plane) => {
177
179
  if (plane.scopes.length === 0)
178
- return new Map();
180
+ return [];
179
181
  const face = plane.backend.retrievalView?.() ?? plane.backend;
180
- const headers = await face.listHeaders(plane.scopes);
181
- return deriveSupersededSet(headers, { excluded: (id) => exclusions?.has(id) === true });
182
+ return face.listHeaders(plane.scopes);
183
+ };
184
+ const supersededOf = async (plane, exclusions) => deriveSupersededSet(await servedHeaders(plane), { excluded: (id) => exclusions?.has(id) === true });
185
+ const planeFaceOf = async (plane, excluded) => {
186
+ const headers = await servedHeaders(plane);
187
+ return { headers, superseded: deriveSupersededSet(headers, { excluded: (id) => excluded?.has(id) === true }) };
188
+ };
189
+ const shadowNamesAcross = async (exclusions, faces = []) => {
190
+ const out = new Map();
191
+ const writeScope = opts.writeScope;
192
+ if (writeScope === undefined || writeScope === null)
193
+ return out;
194
+ for (let i = 0; i < planes.length; i++) {
195
+ const plane = planes[i];
196
+ if (!plane.scopes.includes(writeScope))
197
+ continue;
198
+ const committed = await plane.committedHeaders?.();
199
+ if (committed === undefined)
200
+ continue;
201
+ const face = faces[i] ?? (await planeFaceOf(plane, exclusions[i]));
202
+ const excluded = exclusions[i];
203
+ const standing = committed.filter((h) => excluded?.has(h.id) !== true && !face.superseded.has(h.id));
204
+ for (const [name, id] of shadowNamesOf(standing, writeScope)) {
205
+ const prior = out.get(name);
206
+ if (prior === undefined || id < prior)
207
+ out.set(name, id);
208
+ }
209
+ }
210
+ return out;
211
+ };
212
+ const shadowedOf = (headers, names) => names.size === 0 ? new Map() : deriveShadowedSet(headers, opts.writeScope ?? null, { shadowNames: names });
213
+ const pruneUndeliverableCarriers = async (maps) => {
214
+ const carriers = new Set();
215
+ for (const m of maps)
216
+ for (const id of m.values())
217
+ carriers.add(id);
218
+ if (carriers.size === 0)
219
+ return [...maps];
220
+ const ids = [...carriers];
221
+ const live = new Set();
222
+ for (const plane of planes)
223
+ for (const e of await getWithinScopes(plane, ids))
224
+ live.add(e.id);
225
+ if (live.size === carriers.size)
226
+ return [...maps];
227
+ return maps.map((m) => new Map([...m].filter(([, carrier]) => live.has(carrier))));
182
228
  };
183
229
  const searchTool = {
184
230
  name: MEMORY_SEARCH_TOOL_NAME,
@@ -190,9 +236,12 @@ export function createMemoryEngineTools(opts) {
190
236
  includeSuperseded: Type.Optional(Type.Boolean({
191
237
  description: "Also return entries a newer consolidated entry has superseded (kept as evidence; hidden from results by default). Superseded hits are tagged, with the superseding entry's id.",
192
238
  })),
239
+ includeShadowed: Type.Optional(Type.Boolean({
240
+ description: "Also return entries a same-named entry in this session's write home shadows (kept as evidence; hidden from results by default). Shadowed hits are tagged, with the shadowing entry's id.",
241
+ })),
193
242
  }, { additionalProperties: false }),
194
243
  execute: async (args, ctx) => {
195
- const { query, limit: rawLimit, includeSuperseded } = args;
244
+ const { query, limit: rawLimit, includeSuperseded, includeShadowed } = args;
196
245
  const signal = ctx.signal;
197
246
  if (query.trim() === "") {
198
247
  return refusedSearch("empty_query", "query must be non-empty — pass the concrete keywords a past memory entry would contain.");
@@ -208,11 +257,15 @@ export function createMemoryEngineTools(opts) {
208
257
  }
209
258
  }
210
259
  const supersededMaps = [];
260
+ const shadowedMaps = [];
211
261
  try {
212
- for (let i = 0; i < planes.length; i++) {
213
- const plane = planes[i];
214
- supersededMaps.push(plane === undefined ? new Map() : await supersededOf(plane, exclusions[i]));
215
- }
262
+ const faces = [];
263
+ for (let i = 0; i < planes.length; i++)
264
+ faces.push(await planeFaceOf(planes[i], exclusions[i]));
265
+ for (const face of faces)
266
+ supersededMaps.push(face.superseded);
267
+ const names = await shadowNamesAcross(exclusions, faces);
268
+ shadowedMaps.push(...(await pruneUndeliverableCarriers(faces.map((f) => shadowedOf(f.headers, names)))));
216
269
  }
217
270
  catch (err) {
218
271
  if (signal?.aborted === true)
@@ -226,13 +279,16 @@ export function createMemoryEngineTools(opts) {
226
279
  if (plane.scopes.length === 0)
227
280
  continue;
228
281
  const supersededSlots = includeSuperseded === true ? 0 : supersededMaps[i]?.size ?? 0;
229
- const askFor = (carry ? limit + (exclusions[i]?.size ?? 0) : limit) + supersededSlots;
282
+ const shadowedSlots = includeShadowed === true ? 0 : shadowedMaps[i]?.size ?? 0;
283
+ const askFor = (carry ? limit + (exclusions[i]?.size ?? 0) : limit) + supersededSlots + shadowedSlots;
230
284
  const hits = await plane.backend.search(query, plane.scopes, { limit: askFor, ...(carry ? { exposureBands: true } : {}) });
231
285
  for (const h of hits) {
232
286
  if (exclusions[i]?.has(h.id))
233
287
  continue;
234
288
  if (includeSuperseded !== true && supersededMaps[i]?.has(h.id))
235
289
  continue;
290
+ if (includeShadowed !== true && shadowedMaps[i]?.has(h.id))
291
+ continue;
236
292
  merged.push({ ...h, planeIndex: i });
237
293
  }
238
294
  }
@@ -292,11 +348,13 @@ export function createMemoryEngineTools(opts) {
292
348
  const h = live[i];
293
349
  const exposed = exposedFlags[i];
294
350
  const supersededBy = supersededMaps[h.planeIndex]?.get(h.id);
295
- const supersededSuffix = supersededBy !== undefined ? ` ${MEMORY_SEARCH_SUPERSEDED_TAG}` : "";
351
+ const shadowedBy = shadowedMaps[h.planeIndex]?.get(h.id);
352
+ const tags = `${supersededBy !== undefined ? ` ${MEMORY_SEARCH_SUPERSEDED_TAG}` : ""}${shadowedBy !== undefined ? ` ${MEMORY_SEARCH_SHADOWED_TAG}` : ""}`;
353
+ const offFace = { ...(supersededBy !== undefined ? { supersededBy } : {}), ...(shadowedBy !== undefined ? { shadowedBy } : {}) };
296
354
  if (exposed) {
297
- hits.push({ exposure: "external", id: h.id, scope: h.scope, score: h.score, mtimeMs: h.mtimeMs, sizeBytes: h.sizeBytes, ...(supersededBy !== undefined ? { supersededBy } : {}) });
355
+ hits.push({ exposure: "external", id: h.id, scope: h.scope, score: h.score, mtimeMs: h.mtimeMs, sizeBytes: h.sizeBytes, ...offFace });
298
356
  lines.push("");
299
- lines.push(`${i + 1}. [mem:${h.id}] ${MEMORY_EXPOSURE_HANDLE_TAG}${supersededSuffix} (scope ${inlineUntrusted(h.scope, 80)}, score ${h.score.toFixed(3)}, ${ageOf(now, h.mtimeMs)}) — read it with ${MEMORY_GET_TOOL_NAME} id ${h.id}`);
357
+ lines.push(`${i + 1}. [mem:${h.id}] ${MEMORY_EXPOSURE_HANDLE_TAG}${tags} (scope ${inlineUntrusted(h.scope, 80)}, score ${h.score.toFixed(3)}, ${ageOf(now, h.mtimeMs)}) — read it with ${MEMORY_GET_TOOL_NAME} id ${h.id}`);
300
358
  continue;
301
359
  }
302
360
  hits.push({
@@ -308,11 +366,11 @@ export function createMemoryEngineTools(opts) {
308
366
  score: h.score,
309
367
  mtimeMs: h.mtimeMs,
310
368
  sizeBytes: h.sizeBytes,
311
- ...(supersededBy !== undefined ? { supersededBy } : {}),
369
+ ...offFace,
312
370
  });
313
371
  const hook = h.description ? ` — ${inlineUntrusted(h.description, 200)}` : "";
314
372
  lines.push("");
315
- lines.push(`${i + 1}. ${entryPath(h.scope, h.slug)}${supersededSuffix}${hook} (id ${h.id}, score ${h.score.toFixed(3)}, ${ageOf(now, h.mtimeMs)})`);
373
+ lines.push(`${i + 1}. ${entryPath(h.scope, h.slug)}${tags}${hook} (id ${h.id}, score ${h.score.toFixed(3)}, ${ageOf(now, h.mtimeMs)})`);
316
374
  const body = (entryById.get(h.id)?.body ?? "").trim();
317
375
  if (body !== "")
318
376
  lines.push(delimitUntrusted(`memory entry ${h.slug}`, body, MEMORY_SEARCH_SNIPPET_CAP));
@@ -349,6 +407,7 @@ export function createMemoryEngineTools(opts) {
349
407
  const lineCursor = Math.max(0, Math.floor(rawLineCursor ?? 0) || 0);
350
408
  let entry;
351
409
  let entryPlane;
410
+ let entryHeader;
352
411
  let mtimeMs;
353
412
  try {
354
413
  if (id !== undefined) {
@@ -365,7 +424,8 @@ export function createMemoryEngineTools(opts) {
365
424
  `If the id might be misremembered: ${MEMORY_SEARCH_TOOL_NAME} finds entries by keyword, and ` +
366
425
  `${MEMORY_INDEX_TOOL_NAME} lists every mounted entry with its exact id and slug.`, "not_found", { id });
367
426
  }
368
- mtimeMs = (await entryPlane.backend.listHeaders(entryPlane.scopes)).find((h) => h.id === entry.id)?.mtimeMs;
427
+ entryHeader = (await entryPlane.backend.listHeaders(entryPlane.scopes)).find((h) => h.id === entry.id);
428
+ mtimeMs = entryHeader?.mtimeMs;
369
429
  }
370
430
  else {
371
431
  const matches = [];
@@ -388,6 +448,17 @@ export function createMemoryEngineTools(opts) {
388
448
  `If the slug is misremembered: ${MEMORY_SEARCH_TOOL_NAME} matches keywords; ${MEMORY_INDEX_TOOL_NAME} lists exact slugs.` +
389
449
  pollutionSentence(), "not_found", { slug: slug, ...(scope !== undefined ? { scope } : {}) });
390
450
  }
451
+ try {
452
+ const names = await shadowNamesAcross(planes.map((plane) => plane.challengeExclusions?.()));
453
+ const [shadowed] = await pruneUndeliverableCarriers([shadowedOf(matches.map((m) => m.header), names)]);
454
+ const standing = matches.filter((m) => shadowed?.has(m.header.id) !== true);
455
+ if (standing.length > 0 && standing.length < matches.length) {
456
+ matches.length = 0;
457
+ matches.push(...standing);
458
+ }
459
+ }
460
+ catch {
461
+ }
391
462
  if (matches.length > 1) {
392
463
  const candidates = matches.map((m) => ({ scope: m.header.scope, slug: m.header.slug, id: m.header.id }));
393
464
  const listing = candidates.map((c) => `- scope ${JSON.stringify(inlineUntrusted(c.scope, 80))}, id ${c.id}`).join("\n");
@@ -400,7 +471,8 @@ export function createMemoryEngineTools(opts) {
400
471
  }
401
472
  entry = found;
402
473
  entryPlane = matches[0].plane;
403
- mtimeMs = matches[0].header.mtimeMs;
474
+ entryHeader = matches[0].header;
475
+ mtimeMs = entryHeader.mtimeMs;
404
476
  }
405
477
  }
406
478
  catch (err) {
@@ -436,6 +508,17 @@ export function createMemoryEngineTools(opts) {
436
508
  supersededBy = undefined;
437
509
  }
438
510
  }
511
+ let shadowedBy;
512
+ if (entryHeader !== undefined) {
513
+ try {
514
+ const names = await shadowNamesAcross(planes.map((plane) => plane.challengeExclusions?.()));
515
+ const [pruned] = await pruneUndeliverableCarriers([shadowedOf([entryHeader], names)]);
516
+ shadowedBy = pruned?.get(entry.id);
517
+ }
518
+ catch {
519
+ shadowedBy = undefined;
520
+ }
521
+ }
439
522
  try {
440
523
  entryPlane?.recordRetrieved([entry.id]);
441
524
  }
@@ -450,6 +533,7 @@ export function createMemoryEngineTools(opts) {
450
533
  `Memory entry ${entryPath(entry.scope, entry.slug)} (id ${entry.id}${mtimeMs !== undefined ? `, ${ageOf(now, mtimeMs)}` : ""})`,
451
534
  ...(exposed ? [MEMORY_EXPOSURE_BANNER] : []),
452
535
  ...(supersededBy !== undefined ? [memorySupersededNote(supersededBy)] : []),
536
+ ...(shadowedBy !== undefined ? [memoryShadowedNote(shadowedBy)] : []),
453
537
  ...(fm.name !== undefined ? [`name: ${inlineUntrusted(fm.name, 120)}`] : []),
454
538
  ...(fm.description !== undefined ? [`description: ${inlineUntrusted(fm.description, 200)}`] : []),
455
539
  ...(fm.type !== undefined ? [`type: ${inlineUntrusted(fm.type, 40)}`] : []),
@@ -494,6 +578,7 @@ export function createMemoryEngineTools(opts) {
494
578
  ...(nextLineCursor !== undefined ? { nextLineCursor } : {}),
495
579
  ...(exposed ? { exposure: "external" } : {}),
496
580
  ...(supersededBy !== undefined ? { supersededBy } : {}),
581
+ ...(shadowedBy !== undefined ? { shadowedBy } : {}),
497
582
  };
498
583
  return { content: head.join("\n"), details };
499
584
  }
@@ -536,6 +621,7 @@ export function createMemoryEngineTools(opts) {
536
621
  ...(nextLineCursor !== undefined ? { nextLineCursor } : {}),
537
622
  ...(exposed ? { exposure: "external" } : {}),
538
623
  ...(supersededBy !== undefined ? { supersededBy } : {}),
624
+ ...(shadowedBy !== undefined ? { shadowedBy } : {}),
539
625
  };
540
626
  return { content: head.join("\n"), details };
541
627
  },
@@ -558,9 +644,12 @@ export function createMemoryEngineTools(opts) {
558
644
  includeSuperseded: Type.Optional(Type.Boolean({
559
645
  description: "Also list entries a newer consolidated entry has superseded (kept as evidence; hidden from the listing by default). Superseded rows are tagged, with the superseding entry's id.",
560
646
  })),
647
+ includeShadowed: Type.Optional(Type.Boolean({
648
+ description: "Also list entries a same-named entry in this session's write home shadows (kept as evidence; hidden from the listing by default). Shadowed rows are tagged, with the shadowing entry's id.",
649
+ })),
561
650
  }, { additionalProperties: false }),
562
651
  execute: async (args, ctx) => {
563
- const { offset: rawOffset, limit: rawLimit, listingRev: givenListingRev, scope, includeSuperseded, } = args;
652
+ const { offset: rawOffset, limit: rawLimit, listingRev: givenListingRev, scope, includeSuperseded, includeShadowed, } = args;
564
653
  const signal = ctx.signal;
565
654
  const offset = Math.max(0, Math.floor(rawOffset ?? 0) || 0);
566
655
  const limit = Math.max(1, Math.min(MEMORY_INDEX_MAX_PAGE_ROWS, Math.floor(rawLimit ?? MEMORY_INDEX_PAGE_ROWS) || MEMORY_INDEX_PAGE_ROWS));
@@ -583,14 +672,20 @@ export function createMemoryEngineTools(opts) {
583
672
  }
584
673
  const rows = [];
585
674
  let supersededHidden = 0;
675
+ let shadowedHidden = 0;
586
676
  try {
677
+ const faces = [];
678
+ for (let i = 0; i < planes.length; i++)
679
+ faces.push(await planeFaceOf(planes[i], exclusions[i]));
680
+ const names = await shadowNamesAcross(exclusions, faces);
681
+ const shadowedMaps = await pruneUndeliverableCarriers(faces.map((f) => shadowedOf(f.headers, names)));
587
682
  for (let i = 0; i < planes.length; i++) {
588
683
  const plane = planes[i];
589
684
  if (plane.scopes.length === 0)
590
685
  continue;
591
- const face = plane.backend.retrievalView?.() ?? plane.backend;
592
- const headers = await face.listHeaders(plane.scopes);
593
- const superseded = deriveSupersededSet(headers, { excluded: (id) => exclusions[i]?.has(id) === true });
686
+ const headers = faces[i].headers;
687
+ const superseded = faces[i].superseded;
688
+ const shadowed = shadowedMaps[i];
594
689
  const scopeSet = new Set(plane.scopes);
595
690
  for (const h of headers) {
596
691
  if (!scopeSet.has(h.scope))
@@ -604,7 +699,12 @@ export function createMemoryEngineTools(opts) {
604
699
  supersededHidden++;
605
700
  continue;
606
701
  }
607
- rows.push({ planeIndex: i, header: h, ...(supersededBy !== undefined ? { supersededBy } : {}) });
702
+ const shadowedBy = shadowed.get(h.id);
703
+ if (shadowedBy !== undefined && includeShadowed !== true) {
704
+ shadowedHidden++;
705
+ continue;
706
+ }
707
+ rows.push({ planeIndex: i, header: h, ...(supersededBy !== undefined ? { supersededBy } : {}), ...(shadowedBy !== undefined ? { shadowedBy } : {}) });
608
708
  }
609
709
  }
610
710
  }
@@ -629,13 +729,16 @@ export function createMemoryEngineTools(opts) {
629
729
  return a.header.id < b.header.id ? -1 : a.header.id > b.header.id ? 1 : 0;
630
730
  });
631
731
  const total = rows.length;
632
- const supersededNote = supersededHidden > 0 && includeSuperseded !== true
732
+ const supersededNote = (supersededHidden > 0 && includeSuperseded !== true
633
733
  ? ` ${supersededHidden} superseded entr${supersededHidden === 1 ? "y is" : "ies are"} hidden — pass includeSuperseded to list them.`
634
- : "";
734
+ : "") +
735
+ (shadowedHidden > 0 && includeShadowed !== true
736
+ ? ` ${shadowedHidden} shadowed entr${shadowedHidden === 1 ? "y is" : "ies are"} hidden — pass includeShadowed to list them.`
737
+ : "");
635
738
  if (total === 0) {
636
739
  const where = scope !== undefined ? ` in scope ${inlineUntrusted(scope, 80)}` : "";
637
- const details = { outcome: "ok", entries: [], offset: 0, shown: 0, total: 0, ...(supersededHidden > 0 ? { supersededHidden } : {}) };
638
- return { content: `No ${supersededHidden > 0 ? "active " : ""}memory entries are mounted${where} in this session.${supersededNote}` + pollutionSentence(), details };
740
+ const details = { outcome: "ok", entries: [], offset: 0, shown: 0, total: 0, ...(supersededHidden > 0 ? { supersededHidden } : {}), ...(shadowedHidden > 0 ? { shadowedHidden } : {}) };
741
+ return { content: `No ${supersededHidden > 0 || shadowedHidden > 0 ? "active " : ""}memory entries are mounted${where} in this session.${supersededNote}` + pollutionSentence(), details };
639
742
  }
640
743
  if (offset >= total) {
641
744
  return refusedIndex("offset_past_end", `offset ${offset} is past the end — the listing has ${total} row${total === 1 ? "" : "s"}.`);
@@ -686,7 +789,7 @@ export function createMemoryEngineTools(opts) {
686
789
  }
687
790
  const h = r.header;
688
791
  const exposed = carry && (h.exposure === "external" || (pageEntryById.has(h.id) && exposedEntry(pageEntryById.get(h.id))));
689
- const supersededSuffix = r.supersededBy !== undefined ? ` ${MEMORY_SEARCH_SUPERSEDED_TAG}` : "";
792
+ const supersededSuffix = `${r.supersededBy !== undefined ? ` ${MEMORY_SEARCH_SUPERSEDED_TAG}` : ""}${r.shadowedBy !== undefined ? ` ${MEMORY_SEARCH_SHADOWED_TAG}` : ""}`;
690
793
  const line = exposed
691
794
  ? `${offset + consumed + 1}. [mem:${h.id}] ${MEMORY_EXPOSURE_HANDLE_TAG}${supersededSuffix} (scope ${inlineUntrusted(h.scope, 80)}, ${ageOf(now, h.mtimeMs)}) — read it with ${MEMORY_GET_TOOL_NAME} id ${h.id}`
692
795
  : `${offset + consumed + 1}. ${entryPath(h.scope, h.slug)}${supersededSuffix}${h.description ? ` — ${inlineUntrusted(h.description, 200)}` : ""} (id ${h.id}, ${ageOf(now, h.mtimeMs)})`;
@@ -699,7 +802,7 @@ export function createMemoryEngineTools(opts) {
699
802
  bytes += lineBytes;
700
803
  consumed++;
701
804
  if (exposed) {
702
- entries.push({ exposure: "external", id: h.id, scope: h.scope, mtimeMs: h.mtimeMs, sizeBytes: h.sizeBytes, ...(r.supersededBy !== undefined ? { supersededBy: r.supersededBy } : {}) });
805
+ entries.push({ exposure: "external", id: h.id, scope: h.scope, mtimeMs: h.mtimeMs, sizeBytes: h.sizeBytes, ...(r.supersededBy !== undefined ? { supersededBy: r.supersededBy } : {}), ...(r.shadowedBy !== undefined ? { shadowedBy: r.shadowedBy } : {}) });
703
806
  }
704
807
  else {
705
808
  entries.push({
@@ -711,6 +814,7 @@ export function createMemoryEngineTools(opts) {
711
814
  mtimeMs: h.mtimeMs,
712
815
  sizeBytes: h.sizeBytes,
713
816
  ...(r.supersededBy !== undefined ? { supersededBy: r.supersededBy } : {}),
817
+ ...(r.shadowedBy !== undefined ? { shadowedBy: r.shadowedBy } : {}),
714
818
  });
715
819
  }
716
820
  }
@@ -728,6 +832,7 @@ export function createMemoryEngineTools(opts) {
728
832
  total,
729
833
  listingRev,
730
834
  ...(supersededHidden > 0 && includeSuperseded !== true ? { supersededHidden } : {}),
835
+ ...(shadowedHidden > 0 && includeShadowed !== true ? { shadowedHidden } : {}),
731
836
  ...(end < total || capTripped ? { nextOffset: end } : {}),
732
837
  };
733
838
  return { content: [head, ...lines, ...(footer !== undefined ? [footer] : [])].join("\n"), details };
@@ -438,6 +438,16 @@ export interface MemorySessionHandle {
438
438
  * clean-entry mis-labeling). Absent on handles minted by engines predating the field ⇒ consumers
439
439
  * treat as "unknown", which the discipline seat maps to the historic over-teaching side. */
440
440
  markedEntriesPresent?: boolean;
441
+ /** The names the SESSION's write home carries (name → the carrying entry's id), supplied by the
442
+ * caller when the write home lives on a DIFFERENT plane than this one (the dual-root read-only
443
+ * plane). Absent on a plane holding the write scope itself (the rule reads this plane's own
444
+ * headers) and in a session with no write home, where the shadow set is empty. */
445
+ shadowNames?: ReadonlyMap<string, string>;
446
+ /** How many entries of each scope this plane's index rebuild retired from the default read face
447
+ * by write-home name — the mount-time snapshot the runner reports per scope. Only non-zero
448
+ * scopes have a key; the tool faces derive their own LIVE answer per call, so a stale snapshot
449
+ * can mis-report a number, never mis-serve an entry. */
450
+ shadowedByScope?: ReadonlyMap<string, number>;
441
451
  /** Lines of the derived MEMORY.md at materialize time (the index-cleared fuse baseline). */
442
452
  indexBaselineLines: number;
443
453
  /** 评审 M6 — the derived index TEXT as of materialize. `inject` reads THIS when no on-disk
@@ -1,3 +1,4 @@
1
+ import type { EngineNotice } from "./engine-notice.js";
1
2
  /**
2
3
  * A scored memory candidate returned by {@link MemoryStore.searchScored} (design/41). Carries a
3
4
  * **stable id** (for {@link MemoryStore.update}/{@link MemoryStore.delete}) and a similarity `score`.
@@ -525,7 +526,33 @@ export interface NormalizedMemorySpec {
525
526
  scopeOrigins?: Readonly<Record<string, "deployment" | "request">>;
526
527
  /** See {@link MemorySpecInput.capture} (design/383 §2.1 — the session capture opt-out). */
527
528
  capture?: "off";
529
+ /** See {@link MemorySpecInput.projectKey} — carried verbatim; a normalized spec never invents it. */
530
+ projectKey?: MemoryProjectKey;
528
531
  }
532
+ /**
533
+ * The closed vocabulary a deployment states a MISSING project key with: the working root is not a
534
+ * repository, it is a bare repository, or it could not be read. Purely descriptive — the engine
535
+ * branches on presence (`absent` vs `scope`), never on which word; the word rides the operator
536
+ * notice so a deployment can tell "no project here" from "I could not look".
537
+ */
538
+ export declare const MEMORY_PROJECT_KEY_ABSENT_REASONS: readonly ["not_git", "bare", "unreadable"];
539
+ export type MemoryProjectKeyAbsentReason = (typeof MEMORY_PROJECT_KEY_ABSENT_REASONS)[number];
540
+ /**
541
+ * The assembly point's STATEMENT about this task's project identity — the key it derived for the
542
+ * working root, or the reason there is none. Exactly one arm is spelled.
543
+ *
544
+ * It is a DECLARATION, not a second way to configure memory: the layering is still `scopes` and
545
+ * `writeScope`, and this key only lets the engine refuse the one misconfiguration those two cannot
546
+ * express — an assembler that derived a project key and then left `writeScope` to its default, so
547
+ * every entry the session writes lands in the shared layer instead of the project's.
548
+ */
549
+ export type MemoryProjectKey = {
550
+ scope: string;
551
+ absent?: never;
552
+ } | {
553
+ absent: MemoryProjectKeyAbsentReason;
554
+ scope?: never;
555
+ };
529
556
  /** The memory spec accepted on `TaskSpec.memory`. RB-479-B② (ruled 2026-08-02): ONE form —
530
557
  * the legacy singular `scope` field is REMOVED; `scopes` (ordered read layers) is the only spelling.
531
558
  * An input still carrying an own `scope` property is REFUSED by {@link normalizeMemorySpec}
@@ -573,6 +600,24 @@ export type MemorySpecInput = {
573
600
  * prepare, strictly BEFORE any record lands (a denied declaration leaves zero record residue).
574
601
  */
575
602
  capture?: "off";
603
+ /**
604
+ * The assembly point's statement about this task's PROJECT IDENTITY ({@link MemoryProjectKey}).
605
+ * Two things happen to it and nothing else: a `{ scope }` arm must EQUAL `writeScope` (a
606
+ * read-only layering excepted), or the task refuses to start with
607
+ * `config.memory_write_scope_mismatch`; an `{ absent }` arm mints one operator notice
608
+ * (`memory.project_scope_absent`) naming the reason, and the layering is used as written.
609
+ *
610
+ * Why this rather than "derive writeScope from it": the layering is the deployment's to compose,
611
+ * and a key that quietly re-pointed the write face would be a second configuration channel. What
612
+ * it closes is the SILENT direction — an omitted `writeScope` defaults to the LAST scope, so an
613
+ * assembler that derived a project key and forgot to pin the write face writes every entry of
614
+ * every project into the shared layer, and nothing says so. Absent ⇒ the spec reads as before.
615
+ *
616
+ * Any other shape — both arms, neither, an empty `scope`, a reason outside
617
+ * {@link MEMORY_PROJECT_KEY_ABSENT_REASONS} — is REFUSED loudly
618
+ * (`config.memory_project_key_spelling`), never folded to "no key stated".
619
+ */
620
+ projectKey?: MemoryProjectKey;
576
621
  };
577
622
  /**
578
623
  * Normalize {@link import("./types.js").TaskSpec.memory} into ONE canonical
@@ -592,6 +637,21 @@ export type MemorySpecInput = {
592
637
  *
593
638
  */
594
639
  export declare function normalizeMemorySpec(input: MemorySpecInput | undefined): NormalizedMemorySpec | undefined;
640
+ /**
641
+ * The operator disclosure for a declared-absent project key (`memory.project_scope_absent`,
642
+ * audience `operator`): the deployment stated it could derive no project identity for this working
643
+ * root, so the layering it sent is used exactly as written — typically writing into a layer shared
644
+ * with every other project. The notice states the deployment's own reason word and the root it was
645
+ * about; whether that reading is correct is a question about how the deployment resolves working
646
+ * roots, which is why nobody is judged here. One per prepared leg, at the declaration — not at the
647
+ * mount, because the declaration is already the fact.
648
+ */
649
+ export declare function memoryProjectScopeAbsentNotice(input: {
650
+ reason: MemoryProjectKeyAbsentReason;
651
+ taskRoot: string;
652
+ sessionId: string;
653
+ writeScope: string | null;
654
+ }): EngineNotice;
595
655
  /** Max bytes of memory injected into the prompt; larger is truncated with a marker. (CodeWhale: 100 KiB)
596
656
  * Exported so the inject-all de-dup seeding (prepare-task.ts) can detect truncation and NOT seed dropped
597
657
  * (truncated-out) note ids as "already surfaced" — see the MINOR-1 audit fix there. */
@@ -363,6 +363,7 @@ export function parseNoteTimestamp(ts) {
363
363
  const ms = Date.parse(`${ts.replace(" ", "T")}:00Z`);
364
364
  return Number.isFinite(ms) ? { mtimeMs: ms } : { mtimeMs: 0, timestampMissing: true };
365
365
  }
366
+ export const MEMORY_PROJECT_KEY_ABSENT_REASONS = ["not_git", "bare", "unreadable"];
366
367
  export function normalizeMemorySpec(input) {
367
368
  if (!input)
368
369
  return undefined;
@@ -385,6 +386,7 @@ export function normalizeMemorySpec(input) {
385
386
  e.code = "config.memory_capture_spelling";
386
387
  throw e;
387
388
  }
389
+ const projectKey = readProjectKey(input);
388
390
  const raw = input.scopes ?? [];
389
391
  const scopes = [];
390
392
  const seen = new Set();
@@ -421,13 +423,56 @@ export function normalizeMemorySpec(input) {
421
423
  ...(input.execIsExternalContent === true ? { execIsExternalContent: true } : {}),
422
424
  };
423
425
  const origins = input.scopeOrigins !== undefined ? { scopeOrigins: input.scopeOrigins } : {};
426
+ if (projectKey?.scope !== undefined && writeScope !== null && writeScope !== projectKey.scope) {
427
+ const e = new Error(`TaskSpec.memory.projectKey declares project scope ${JSON.stringify(projectKey.scope)} while writeScope is ${JSON.stringify(writeScope)} — ` +
428
+ `every entry this session writes would land in ${JSON.stringify(writeScope)} instead of the project's own layer. ` +
429
+ `Pin writeScope to the project scope (or drop projectKey if the layering is deliberate). Refusing to start mis-addressed.`);
430
+ e.code = "config.memory_write_scope_mismatch";
431
+ throw e;
432
+ }
424
433
  const capture = input.capture === "off" ? { capture: "off" } : {};
434
+ const project = projectKey !== undefined ? { projectKey } : {};
425
435
  if (input.scopeContract === "v2") {
426
436
  for (const key of writeScope !== null ? [...scopes, writeScope] : scopes)
427
437
  parseScopeKey(key);
428
- return { scopes, writeScope, enabled: input.enabled !== false, scopeContract: "v2", ...contentSafety, ...origins, ...capture };
438
+ return { scopes, writeScope, enabled: input.enabled !== false, scopeContract: "v2", ...contentSafety, ...origins, ...capture, ...project };
439
+ }
440
+ return { scopes, writeScope, enabled: input.enabled !== false, ...contentSafety, ...origins, ...capture, ...project };
441
+ }
442
+ function readProjectKey(input) {
443
+ const raw = input.projectKey;
444
+ if (raw === undefined)
445
+ return undefined;
446
+ const refuse = (why) => {
447
+ const e = new Error(`TaskSpec.memory.projectKey must be exactly { scope: "<non-empty>" } or { absent: ${MEMORY_PROJECT_KEY_ABSENT_REASONS.map((r) => JSON.stringify(r)).join(" | ")} } — ${why}. ` +
448
+ `Refusing to guess whether this deployment has a project identity.`);
449
+ e.code = "config.memory_project_key_spelling";
450
+ throw e;
451
+ };
452
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw))
453
+ return refuse(`got ${raw === null ? "null" : Array.isArray(raw) ? "an array" : typeof raw}`);
454
+ const scope = raw.scope;
455
+ const absent = raw.absent;
456
+ if ((scope === undefined) === (absent === undefined))
457
+ return refuse(absent === undefined ? "neither arm was spelled" : "both arms were spelled");
458
+ if (absent !== undefined) {
459
+ if (typeof absent !== "string" || !MEMORY_PROJECT_KEY_ABSENT_REASONS.includes(absent)) {
460
+ return refuse(`the absence reason ${typeof absent === "string" ? JSON.stringify(absent.slice(0, 64)) : typeof absent} is not one of the stated words`);
461
+ }
462
+ return { absent: absent };
429
463
  }
430
- return { scopes, writeScope, enabled: input.enabled !== false, ...contentSafety, ...origins, ...capture };
464
+ if (typeof scope !== "string" || scope.trim() === "")
465
+ return refuse(typeof scope === "string" ? "the project scope is blank" : `the project scope is ${typeof scope}`);
466
+ return { scope: scope.trim() };
467
+ }
468
+ export function memoryProjectScopeAbsentNotice(input) {
469
+ return {
470
+ code: "memory.project_scope_absent",
471
+ message: `No project identity was derived for this session's working root (${input.reason}), so its memory layering is used as declared: ` +
472
+ `${input.writeScope === null ? "the session writes nothing (read-only layering)" : `entries this session writes go to the shared layer ${JSON.stringify(input.writeScope)}`}. ` +
473
+ `If this root should have its own project memory, fix how the deployment resolves it — nothing here retries.`,
474
+ detail: { reason: input.reason, taskRoot: input.taskRoot, sessionId: input.sessionId, ...(input.writeScope !== null ? { writeScope: input.writeScope } : {}) },
475
+ };
431
476
  }
432
477
  export const MAX_MEMORY_BYTES = 100 * 1024;
433
478
  export const MEMORY_WRAPPER_TAGS = ["user_memory"];
@@ -221,6 +221,19 @@ export interface RuleSyncJoinDelta {
221
221
  * calling layer, and closes identically for every caller.
222
222
  */
223
223
  export type RuleWriteDelta = RuleAddDelta | RuleTightenAddDelta | RuleDeleteDelta | RuleSyncJoinDelta;
224
+ /**
225
+ * The refusal every backend owes a delta whose `kind` is outside {@link RuleWriteDelta} — minted here so
226
+ * the two bundled backends (and an out-of-repo twin) answer an unnamed operation with ONE sentence
227
+ * instead of each guessing.
228
+ *
229
+ * The parameter is `never` on purpose: the only way to reach it is from a `default` arm the type system
230
+ * has already proved unreachable, so the factory doubles as the compile-time fence (a fifth arm added to
231
+ * the union stops satisfying `never` and reds at every backend's switch, naming the missing word). The
232
+ * runtime half is not decoration — a delta arrives as DATA (a foreign caller, a replayed record, a
233
+ * deserialized join), and the alternative to throwing is folding an unnamed kind as whichever arm
234
+ * an if/else chain happens to end with: a write nobody defined, landed under a name nobody checked.
235
+ */
236
+ export declare function unknownRuleWriteDeltaError(delta: never): Error;
224
237
  /**
225
238
  * The backend write face. Exported as part of the BACKEND CONTRACT (ruled 2026-08-10) so an
226
239
  * out-of-repo store implementation hangs the same face the file backend does, instead of mirroring the
@@ -531,6 +544,22 @@ export declare function ruleStoreChecksum(payload: unknown): Promise<string>;
531
544
  */
532
545
  export declare const REMOVAL_LIVENESS_WORDS: readonly ["yes", "no", "unknown"];
533
546
  export type RemovalLiveness = (typeof REMOVAL_LIVENESS_WORDS)[number];
547
+ /**
548
+ * What a terminal write FAILURE may claim about the bytes — the failure half of the honesty
549
+ * {@link RemovalLiveness} keeps on the success half, spelled ONCE and read by every write entry here.
550
+ *
551
+ * `"no"` = the store ITSELF answered: a face that cannot write at all, a target the type face refuses,
552
+ * a read that failed before any write, a refused preflight, an epoch seal, a compare-and-set that lost
553
+ * every attempt. Each of those is the store saying nothing landed, so saying it is a fact.
554
+ * `"unknown"` = the apply THREW, which proves nothing either way (a backend can commit and then lose
555
+ * its acknowledgement), or the read-back that would have settled it could not be taken.
556
+ *
557
+ * The word exists because the caller's two branches — retry, or reconcile with `list()` — are not
558
+ * derivable from the failure PROSE. A consumer reading the strength out of the sentence is parsing a
559
+ * message written for a person, and every such reader has to be updated by hand when the sentence is.
560
+ */
561
+ export declare const RULE_WRITE_COMMITTED_WORDS: readonly ["no", "unknown"];
562
+ export type RuleWriteCommitted = (typeof RULE_WRITE_COMMITTED_WORDS)[number];
534
563
  /**
535
564
  * The result of a removal.
536
565
  *
@@ -548,21 +577,32 @@ export type RemoveResult =
548
577
  rev: number;
549
578
  stillLive: RemovalLiveness;
550
579
  }
551
- /** Nothing to remove — including a repeated delete. No empty tombstone is minted. */
580
+ /** Nothing to remove — including a repeated delete. No empty tombstone is minted. `stillLive` is the
581
+ * SAME read-back the `removed` arm reports, taken at the same point in the call ({@link
582
+ * readRuleLiveness}): "there was nothing to remove" is a statement about the snapshot this call read,
583
+ * and an approval landing after it leaves the rule standing. Both 200-shaped arms therefore carry the
584
+ * word, from one source. It is a LATER OBSERVATION, not an atomic absence: `rev` is the revision the
585
+ * decision was taken at, the word is what a second look saw, and the store may change again right
586
+ * after — `yes` beside a `no-op` is the honest pair, not a contradiction. */
552
587
  | {
553
588
  status: "no-op";
554
589
  rev: number;
590
+ stillLive: RemovalLiveness;
555
591
  }
556
- /** Terminal failure, in one of two strengths the `error` text distinguishes — word for word the dual of
557
- * {@link AddResult}'s. When the STORE answered every attempt (the compare-and-set simply lost), the
558
- * read-back confirms nothing was written and the text says so plainly. When the apply THREW, a missing
592
+ /** Terminal failure, in one of two strengths — word for word the dual of {@link AddResult}'s, and
593
+ * `committed` is the word for it ({@link RuleWriteCommitted}), so the strength is READ, not parsed out
594
+ * of the sentence. When the STORE answered every attempt (the compare-and-set simply lost), nothing
595
+ * landed: `committed: "no"`, and the read-back confirms it in the text. When the apply THREW, a missing
559
596
  * tombstone cannot tell a write that never landed from one whose evidence a paired collection erased
560
597
  * (`collectBelowFrontier` recycles a tombstone together with the adds it covered), so that outcome is
561
- * reported INDETERMINATE and the caller reconciles with `list()` rather than retrying blind. When the
562
- * read-back itself failed, `error` says so and the caller reconciles with `list()` too. */
598
+ * `committed: "unknown"`, reported INDETERMINATE, and the caller reconciles with `list()` rather than
599
+ * retrying blind. When the read-back itself failed, the word is the one the apply earned — a throw
600
+ * leaves it `unknown`, an answered compare-and-set stays `no`, because an unreadable store cannot
601
+ * un-answer what the store already said. */
563
602
  | {
564
603
  status: "failed";
565
604
  error: string;
605
+ committed: RuleWriteCommitted;
566
606
  };
567
607
  /**
568
608
  * Remove a persisted rule.
@@ -602,7 +642,7 @@ export type RuleAddLanding = {
602
642
  | {
603
643
  landed: false;
604
644
  reason: string;
605
- committed: "no" | "unknown";
645
+ committed: RuleWriteCommitted;
606
646
  };
607
647
  /**
608
648
  * Land ONE add — the terminal write both doors take (the consent protocol's redemption and the
@@ -652,10 +692,13 @@ export type AddResult =
652
692
  * survives the tombstone a concurrent removal laid on it — but a sync collection can erase even
653
693
  * that, so a dot it cannot find is reported INDETERMINATE rather than as a confirmed nothing, and
654
694
  * the caller reconciles with `list()` instead of retrying blind (a blind retry mints a fresh dot and
655
- * can reinstate a restriction somebody had just removed). */
695
+ * can reinstate a restriction somebody had just removed). `committed` ({@link RuleWriteCommitted}) is
696
+ * which of the two this is, as a WORD — the landing's own answer, passed through rather than re-derived,
697
+ * so this entry and the consent protocol's door cannot disagree about a write they both took. */
656
698
  | {
657
699
  status: "failed";
658
700
  error: string;
701
+ committed: RuleWriteCommitted;
659
702
  };
660
703
  /**
661
704
  * Add a persisted rule in ONE step, with no approval record.