@remnic/core 9.3.688 → 9.3.690

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 (163) hide show
  1. package/dist/access-boundary.d.ts +5 -4
  2. package/dist/access-boundary.js +12 -11
  3. package/dist/access-cli.js +90 -26
  4. package/dist/access-cli.js.map +1 -1
  5. package/dist/access-http.d.ts +4 -3
  6. package/dist/access-http.js +15 -14
  7. package/dist/access-mcp.d.ts +11 -3
  8. package/dist/access-mcp.js +14 -13
  9. package/dist/access-operations.d.ts +11 -5
  10. package/dist/access-operations.js +15 -12
  11. package/dist/{access-service-DmCHJ4cH.d.ts → access-service-Dujr3MTm.d.ts} +62 -2
  12. package/dist/access-service.d.ts +4 -3
  13. package/dist/access-service.js +11 -10
  14. package/dist/access-surface-catalog.d.ts +4 -3
  15. package/dist/access-surface-catalog.js +2 -0
  16. package/dist/access-surface-catalog.js.map +1 -1
  17. package/dist/bootstrap.d.ts +3 -2
  18. package/dist/briefing.d.ts +1 -0
  19. package/dist/briefing.js +3 -2
  20. package/dist/buffer.d.ts +1 -0
  21. package/dist/{catalog-COqWZlZ6.d.ts → catalog-CKPtJ114.d.ts} +1 -1
  22. package/dist/causal-consolidation.js +4 -3
  23. package/dist/causal-consolidation.js.map +1 -1
  24. package/dist/{chunk-B4XVLHJA.js → chunk-2N6UNJSX.js} +2 -2
  25. package/dist/{chunk-NN7QYW5W.js → chunk-46URPRE6.js} +2 -2
  26. package/dist/chunk-46URPRE6.js.map +1 -0
  27. package/dist/{chunk-4N3TFFPH.js → chunk-54PVJDO5.js} +2 -2
  28. package/dist/{chunk-PH3HOKYW.js → chunk-67MMWC74.js} +2 -2
  29. package/dist/{chunk-STOEE37X.js → chunk-BKAMHZYR.js} +2 -2
  30. package/dist/{chunk-B43NZNMG.js → chunk-CE6CBRCV.js} +432 -2240
  31. package/dist/chunk-CE6CBRCV.js.map +1 -0
  32. package/dist/{chunk-RVYD6LR3.js → chunk-CP2NZQLT.js} +15 -4
  33. package/dist/chunk-CP2NZQLT.js.map +1 -0
  34. package/dist/{chunk-BLIWOONZ.js → chunk-CUNTLEJP.js} +4 -12
  35. package/dist/chunk-CUNTLEJP.js.map +1 -0
  36. package/dist/{chunk-2SJCWLQD.js → chunk-CWE74HRG.js} +3 -3
  37. package/dist/{chunk-VX6OBUDW.js → chunk-GR77Z2BM.js} +2 -2
  38. package/dist/{chunk-CTOQEZSN.js → chunk-LXIEXSHU.js} +2 -2
  39. package/dist/{chunk-KFBOZYME.js → chunk-NSVXK7M5.js} +33 -4
  40. package/dist/chunk-NSVXK7M5.js.map +1 -0
  41. package/dist/{chunk-473JIN2U.js → chunk-OMKUJTVJ.js} +37 -5
  42. package/dist/chunk-OMKUJTVJ.js.map +1 -0
  43. package/dist/{chunk-GS55WYRL.js → chunk-PHZKALOE.js} +2 -2
  44. package/dist/{chunk-6O6A6YUO.js → chunk-RGNEARXW.js} +3 -3
  45. package/dist/{chunk-4FE2K57M.js → chunk-RTFAZOIR.js} +2 -2
  46. package/dist/{chunk-UTYBJR7M.js → chunk-SB6CQTKP.js} +2 -2
  47. package/dist/chunk-SVOZFLIQ.js +16 -0
  48. package/dist/chunk-SVOZFLIQ.js.map +1 -0
  49. package/dist/{chunk-2KAYTPPT.js → chunk-TYF3D4MS.js} +5 -5
  50. package/dist/{chunk-WIHPNY65.js → chunk-UD5OKH4J.js} +2 -2
  51. package/dist/{chunk-QANVLERJ.js → chunk-UPIBE2DK.js} +2 -2
  52. package/dist/{chunk-CCWHPGT4.js → chunk-WROKD3XC.js} +45 -30
  53. package/dist/chunk-WROKD3XC.js.map +1 -0
  54. package/dist/{chunk-ZPTISBQU.js → chunk-X5J3JZR3.js} +7 -7
  55. package/dist/{chunk-OV4D5T7V.js → chunk-X7RLU5CR.js} +2 -2
  56. package/dist/{chunk-S2OU5DZY.js → chunk-XKUKJIOY.js} +30 -7
  57. package/dist/chunk-XKUKJIOY.js.map +1 -0
  58. package/dist/{chunk-FUCUR2OZ.js → chunk-Z7KILAOU.js} +603 -9
  59. package/dist/chunk-Z7KILAOU.js.map +1 -0
  60. package/dist/chunk-ZU7N3S6V.js +2190 -0
  61. package/dist/chunk-ZU7N3S6V.js.map +1 -0
  62. package/dist/{chunk-DCWIQFNA.js → chunk-ZUDM75KG.js} +4 -4
  63. package/dist/{cli-D8nZ2MPH.d.ts → cli-BkDp6WNi.d.ts} +2 -2
  64. package/dist/cli.d.ts +5 -4
  65. package/dist/cli.js +32 -30
  66. package/dist/compounding/engine.d.ts +1 -0
  67. package/dist/compounding/engine.js +3 -2
  68. package/dist/connectors/codex-materialize-runner.js +3 -2
  69. package/dist/connectors/index.js +3 -2
  70. package/dist/consolidation-provenance-check.d.ts +1 -0
  71. package/dist/consolidation-undo.d.ts +1 -0
  72. package/dist/contradiction/index.d.ts +1 -0
  73. package/dist/conversation-index/backend.js +2 -2
  74. package/dist/entity-retrieval.d.ts +1 -0
  75. package/dist/entity-retrieval.js +3 -2
  76. package/dist/explicit-capture.d.ts +3 -2
  77. package/dist/index.d.ts +6 -5
  78. package/dist/index.js +47 -44
  79. package/dist/index.js.map +1 -1
  80. package/dist/lcm/engine.js +3 -3
  81. package/dist/lcm/index.js +5 -5
  82. package/dist/maintenance/memory-governance.js +3 -2
  83. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +3 -2
  84. package/dist/maintenance/rebuild-memory-projection.js +4 -3
  85. package/dist/mcp-memory-inspector-app.d.ts +4 -3
  86. package/dist/memory-worth-outcomes.d.ts +1 -0
  87. package/dist/namespaces/migrate.d.ts +2 -1
  88. package/dist/namespaces/migrate.js +10 -9
  89. package/dist/namespaces/search.js +6 -6
  90. package/dist/namespaces/storage.d.ts +2 -1
  91. package/dist/namespaces/storage.js +3 -2
  92. package/dist/operator-toolkit.d.ts +1 -0
  93. package/dist/operator-toolkit.js +13 -11
  94. package/dist/{orchestrator-CA6ouzBn.d.ts → orchestrator-B7ixmUkP.d.ts} +145 -1
  95. package/dist/orchestrator.d.ts +3 -2
  96. package/dist/orchestrator.js +22 -20
  97. package/dist/recall-pipeline-stages.d.ts +167 -0
  98. package/dist/recall-pipeline-stages.js +61 -0
  99. package/dist/recall-pipeline-stages.js.map +1 -0
  100. package/dist/schemas.d.ts +10 -10
  101. package/dist/search/factory.js +5 -5
  102. package/dist/search/index.js +9 -9
  103. package/dist/semantic-consolidation.js +4 -3
  104. package/dist/semantic-rule-promotion.js +3 -2
  105. package/dist/semantic-rule-verifier.js +3 -2
  106. package/dist/storage.d.ts +3 -27
  107. package/dist/storage.js +5 -3
  108. package/dist/structured-attributes.d.ts +29 -0
  109. package/dist/structured-attributes.js +8 -0
  110. package/dist/structured-attributes.js.map +1 -0
  111. package/dist/temporal-supersession.d.ts +1 -0
  112. package/dist/tier-migration.d.ts +1 -0
  113. package/dist/verified-recall.js +3 -2
  114. package/package.json +2 -2
  115. package/src/access-boundary.ts +2 -1
  116. package/src/access-cli.test.ts +40 -0
  117. package/src/access-cli.ts +90 -2
  118. package/src/access-http.ts +35 -6
  119. package/src/access-mcp.ts +34 -0
  120. package/src/access-operations.ts +45 -0
  121. package/src/access-service.ts +60 -0
  122. package/src/access-surface-catalog.test.ts +1 -1
  123. package/src/access-surface-catalog.ts +2 -0
  124. package/src/cli.ts +18 -0
  125. package/src/coding/architecture-card.test.ts +544 -0
  126. package/src/coding/architecture-card.ts +687 -0
  127. package/src/coding/architecture-surfaces.test.ts +579 -0
  128. package/src/coding/architecture-surfaces.ts +457 -0
  129. package/src/maintenance/namespace-maintenance-fanout.test.ts +595 -0
  130. package/src/maintenance/namespace-maintenance-fanout.ts +318 -0
  131. package/src/maintenance/namespace-planner.ts +74 -16
  132. package/src/operator-toolkit.ts +25 -0
  133. package/src/orchestrator.ts +144 -0
  134. package/src/recall-pipeline-stages.test.ts +269 -0
  135. package/src/recall-pipeline-stages.ts +294 -0
  136. package/src/storage.ts +6 -20
  137. package/src/structured-attributes.ts +39 -0
  138. package/dist/chunk-473JIN2U.js.map +0 -1
  139. package/dist/chunk-B43NZNMG.js.map +0 -1
  140. package/dist/chunk-BLIWOONZ.js.map +0 -1
  141. package/dist/chunk-CCWHPGT4.js.map +0 -1
  142. package/dist/chunk-FUCUR2OZ.js.map +0 -1
  143. package/dist/chunk-KFBOZYME.js.map +0 -1
  144. package/dist/chunk-NN7QYW5W.js.map +0 -1
  145. package/dist/chunk-RVYD6LR3.js.map +0 -1
  146. package/dist/chunk-S2OU5DZY.js.map +0 -1
  147. /package/dist/{chunk-B4XVLHJA.js.map → chunk-2N6UNJSX.js.map} +0 -0
  148. /package/dist/{chunk-4N3TFFPH.js.map → chunk-54PVJDO5.js.map} +0 -0
  149. /package/dist/{chunk-PH3HOKYW.js.map → chunk-67MMWC74.js.map} +0 -0
  150. /package/dist/{chunk-STOEE37X.js.map → chunk-BKAMHZYR.js.map} +0 -0
  151. /package/dist/{chunk-2SJCWLQD.js.map → chunk-CWE74HRG.js.map} +0 -0
  152. /package/dist/{chunk-VX6OBUDW.js.map → chunk-GR77Z2BM.js.map} +0 -0
  153. /package/dist/{chunk-CTOQEZSN.js.map → chunk-LXIEXSHU.js.map} +0 -0
  154. /package/dist/{chunk-GS55WYRL.js.map → chunk-PHZKALOE.js.map} +0 -0
  155. /package/dist/{chunk-6O6A6YUO.js.map → chunk-RGNEARXW.js.map} +0 -0
  156. /package/dist/{chunk-4FE2K57M.js.map → chunk-RTFAZOIR.js.map} +0 -0
  157. /package/dist/{chunk-UTYBJR7M.js.map → chunk-SB6CQTKP.js.map} +0 -0
  158. /package/dist/{chunk-2KAYTPPT.js.map → chunk-TYF3D4MS.js.map} +0 -0
  159. /package/dist/{chunk-WIHPNY65.js.map → chunk-UD5OKH4J.js.map} +0 -0
  160. /package/dist/{chunk-QANVLERJ.js.map → chunk-UPIBE2DK.js.map} +0 -0
  161. /package/dist/{chunk-ZPTISBQU.js.map → chunk-X5J3JZR3.js.map} +0 -0
  162. /package/dist/{chunk-OV4D5T7V.js.map → chunk-X7RLU5CR.js.map} +0 -0
  163. /package/dist/{chunk-DCWIQFNA.js.map → chunk-ZUDM75KG.js.map} +0 -0
@@ -522,3 +522,43 @@ test("access-cli rejects adjacent option-looking values", async () => {
522
522
 
523
523
  assert.match(output, /missing required option: --content/);
524
524
  });
525
+
526
+ // ──────────────────────────────────────────────────────────────────────────
527
+ // architecture command — wired through the boundary operation (#1548 PR3)
528
+ // ──────────────────────────────────────────────────────────────────────────
529
+
530
+ test("access-cli architecture is a known command (unknown options rejected, not unsupported-command)", async () => {
531
+ const output = await captureRunCliFailure(["architecture", "--typo", "x"]);
532
+ assert.match(output, /unknown option: --typo/);
533
+ });
534
+
535
+ test("access-cli architecture accepts its documented value options", async () => {
536
+ // Valid option spellings parse cleanly; the invocation only fails once it
537
+ // reaches the runtime (no OpenClaw config here), proving the options were
538
+ // accepted by parseArgs rather than rejected as unknown.
539
+ const output = await captureRunCliFailure([
540
+ "architecture",
541
+ "--subcommand",
542
+ "get",
543
+ "--session-key",
544
+ "s1",
545
+ "--namespace",
546
+ "ns",
547
+ "--project-tag",
548
+ "myproj",
549
+ ]);
550
+ assert.doesNotMatch(output, /unknown option/);
551
+ assert.doesNotMatch(output, /unsupported command/);
552
+ });
553
+
554
+ test("access-cli usage lists the architecture command and its options", async () => {
555
+ const output = await captureRunCliFailure(["architecture", "--typo"]);
556
+ assert.match(output, /engram-access architecture \[options\]/);
557
+ assert.match(output, /Architecture options:/);
558
+ assert.match(output, /--subcommand <get\|refresh>/);
559
+ });
560
+
561
+ test("access-cli still rejects truly unknown commands as unsupported", async () => {
562
+ const output = await captureRunCliFailure(["bogus-command"]);
563
+ assert.match(output, /unsupported command/);
564
+ });
package/src/access-cli.ts CHANGED
@@ -15,7 +15,7 @@ import { projectTagProjectId } from "./coding/coding-namespace.js";
15
15
 
16
16
  const OPENCLAW_REMNIC_PLUGIN_IDS = ["openclaw-remnic", "openclaw-engram"] as const;
17
17
 
18
- type CommandName = "browse" | "store" | "decision";
18
+ type CommandName = "browse" | "store" | "decision" | "architecture";
19
19
 
20
20
  type ParsedArgs = {
21
21
  command: CommandName;
@@ -137,6 +137,7 @@ function usage(): string {
137
137
  " engram-access browse [options]",
138
138
  " engram-access store [options]",
139
139
  " engram-access decision [options]",
140
+ " engram-access architecture [options]",
140
141
  "",
141
142
  "Browse options:",
142
143
  " --namespace <name>",
@@ -176,6 +177,14 @@ function usage(): string {
176
177
  " --entity-ref <ref> (repeatable)",
177
178
  " --project-tag <tag> (attach coding context for this invocation)",
178
179
  " --supersedes-id <id> (alias for --id on supersede)",
180
+ "",
181
+ "Architecture options:",
182
+ " --subcommand <get|refresh>",
183
+ " --namespace <name>",
184
+ " --session-key <key>",
185
+ " --principal <principal>",
186
+ " --project-tag <tag> (attach coding context for this invocation)",
187
+ " --repo-root <path> (repo to scan for refresh; defaults to the current directory)",
179
188
  ].join("\n");
180
189
  }
181
190
 
@@ -228,6 +237,17 @@ const COMMAND_SPECS: Record<CommandName, CommandSpec> = {
228
237
  ]),
229
238
  flagOptions: new Set(),
230
239
  },
240
+ architecture: {
241
+ valueOptions: new Set([
242
+ "subcommand",
243
+ "namespace",
244
+ "session-key",
245
+ "principal",
246
+ "project-tag",
247
+ "repo-root",
248
+ ]),
249
+ flagOptions: new Set(),
250
+ },
231
251
  };
232
252
  const BROWSE_SORT_VALUES = Object.freeze([
233
253
  "updated_desc",
@@ -238,9 +258,19 @@ const BROWSE_SORT_VALUES = Object.freeze([
238
258
 
239
259
  type BrowseSort = (typeof BROWSE_SORT_VALUES)[number];
240
260
 
261
+ /**
262
+ * Type guard for {@link CommandName}. Enumerating the union (rather than a
263
+ * bare `value in COMMAND_SPECS`) is what lets TypeScript NARROW the string
264
+ * to `CommandName` — the `in` operator alone yields TS2322 on the
265
+ * `command: commandRaw` assignment below (codex review P2).
266
+ */
267
+ function isCommandName(value: string): value is CommandName {
268
+ return value === "browse" || value === "store" || value === "decision" || value === "architecture";
269
+ }
270
+
241
271
  function parseArgs(argv: string[]): ParsedArgs {
242
272
  const [commandRaw, ...rest] = argv;
243
- if (commandRaw !== "browse" && commandRaw !== "store" && commandRaw !== "decision") {
273
+ if (!isCommandName(commandRaw)) {
244
274
  throw new UsageError("unsupported-command");
245
275
  }
246
276
  const spec = COMMAND_SPECS[commandRaw];
@@ -552,6 +582,60 @@ async function runDecision(args: ParsedArgs, preferredId?: string): Promise<void
552
582
  console.log(JSON.stringify(output.result, null, 2));
553
583
  }
554
584
 
585
+ /**
586
+ * Architecture-card surface (issue #1548 Track A PR 3). Dispatches through
587
+ * the same `coding_architecture` operation as the MCP tool and HTTP route —
588
+ * one validation boundary, three transports.
589
+ */
590
+ async function runArchitecture(args: ParsedArgs, preferredId?: string): Promise<void> {
591
+ const subcommand = requireOption(args, "subcommand");
592
+ const { config, service } = buildRuntime(preferredId);
593
+ // Same coding-context attachment as runDecision (review P2): the CLI's
594
+ // fresh Orchestrator has an empty session coding-context map, so attach
595
+ // a context BEFORE dispatching when --project-tag + --session-key are
596
+ // given, so the gate passes and writes resolve to the right namespace.
597
+ const projectTag = getLastOption(args, "project-tag");
598
+ const sessionKey = getLastOption(args, "session-key");
599
+ if (sessionKey && sessionKey.trim().length > 0) {
600
+ // refresh scans codingContext.rootPath, so it must be a REAL checkout
601
+ // path. Default to the CWD (or an explicit --repo-root) so a one-shot
602
+ // CLI refresh scans the caller's repo, not a non-existent tag path.
603
+ const repoRoot = expandOptionalPath(getLastOption(args, "repo-root")) ?? process.cwd();
604
+ // --project-tag supplies the context id (namespace); when absent, derive
605
+ // a default from the repo-root basename so refresh works with just
606
+ // --session-key + --repo-root (codex review).
607
+ const projectId =
608
+ projectTag && projectTag.trim().length > 0
609
+ ? projectTagProjectId(projectTag.trim())
610
+ : projectTagProjectId(path.basename(repoRoot));
611
+ service.setCodingContext({
612
+ sessionKey,
613
+ codingContext: {
614
+ projectId,
615
+ branch: null,
616
+ rootPath: repoRoot,
617
+ defaultBranch: null,
618
+ },
619
+ });
620
+ }
621
+ const op = getOperation("coding_architecture");
622
+ if (!op) {
623
+ throw new Error("access-boundary: operation not registered: coding_architecture");
624
+ }
625
+ const output = (await op.run(
626
+ {
627
+ subcommand,
628
+ namespace: getLastOption(args, "namespace"),
629
+ sessionKey,
630
+ },
631
+ {
632
+ service,
633
+ authenticatedPrincipal: getLastOption(args, "principal") ?? config.agentAccessHttp.principal,
634
+ },
635
+ )) as { result: unknown };
636
+ console.log(JSON.stringify(output.result, null, 2));
637
+ }
638
+
555
639
  export async function main(
556
640
  argv: string[] = process.argv.slice(2),
557
641
  options: AccessCliOptions = {},
@@ -565,6 +649,10 @@ export async function main(
565
649
  await runDecision(args, options.preferredId);
566
650
  return;
567
651
  }
652
+ if (args.command === "architecture") {
653
+ await runArchitecture(args, options.preferredId);
654
+ return;
655
+ }
568
656
  await runStore(args, options.preferredId);
569
657
  }
570
658
 
@@ -316,6 +316,7 @@ export class EngramAccessHttpServer {
316
316
  citationsAutoDetect: options.citationsAutoDetect,
317
317
  emitLegacyTools: options.emitLegacyTools,
318
318
  codingDecisionVisible: this.service.decisionRecordSurfaceVisible,
319
+ architectureCardVisible: this.service.architectureCardSurfaceVisible,
319
320
  });
320
321
  }
321
322
 
@@ -1361,6 +1362,30 @@ export class EngramAccessHttpServer {
1361
1362
  return;
1362
1363
  }
1363
1364
 
1365
+ if (req.method === "POST" && pathname === "/engram/v1/coding/architecture") {
1366
+ // Migrated through the access boundary (issue #1525/#1548 PR3):
1367
+ // refresh persists the card, so it is gated by the 30/min write quota;
1368
+ // get is a pure read and stays uncounted.
1369
+ const body = await this.readJsonBody(req);
1370
+ const isWriteSubcommand = body.subcommand === "refresh";
1371
+ if (isWriteSubcommand) {
1372
+ this.ensureWriteRateLimitAvailable();
1373
+ }
1374
+ const op = getOperation("coding_architecture");
1375
+ if (!op) {
1376
+ throw new EngramAccessInputError("access-boundary: operation not registered: coding_architecture");
1377
+ }
1378
+ const output = (await op.run(body, {
1379
+ service: this.service,
1380
+ authenticatedPrincipal: this.resolveRequestPrincipal(req),
1381
+ })) as { result: unknown };
1382
+ if (isWriteSubcommand) {
1383
+ this.recordWriteRateLimitHit();
1384
+ }
1385
+ this.respondJson(res, 200, output.result);
1386
+ return;
1387
+ }
1388
+
1364
1389
  if (req.method === "POST" && pathname === "/engram/v1/suggestions") {
1365
1390
  const body = await this.readValidatedBody(req, "suggestionSubmit");
1366
1391
  const request = {
@@ -2213,13 +2238,16 @@ export class EngramAccessHttpServer {
2213
2238
  typeof toolArgs === "object" &&
2214
2239
  !Array.isArray(toolArgs) &&
2215
2240
  (toolArgs as { dryRun?: unknown }).dryRun === true;
2241
+ const toolArgsSubcommand =
2242
+ toolArgs !== null && typeof toolArgs === "object" && !Array.isArray(toolArgs) && "subcommand" in toolArgs
2243
+ ? toolArgs.subcommand
2244
+ : undefined;
2216
2245
  const codingDecisionWrite =
2217
2246
  (toolName === "engram.coding_decision" || toolName === "remnic.coding_decision") &&
2218
- toolArgs !== null &&
2219
- typeof toolArgs === "object" &&
2220
- !Array.isArray(toolArgs) &&
2221
- ((toolArgs as { subcommand?: unknown }).subcommand === "record" ||
2222
- (toolArgs as { subcommand?: unknown }).subcommand === "supersede");
2247
+ (toolArgsSubcommand === "record" || toolArgsSubcommand === "supersede");
2248
+ const codingArchitectureWrite =
2249
+ (toolName === "engram.coding_architecture" || toolName === "remnic.coding_architecture") &&
2250
+ toolArgsSubcommand === "refresh";
2223
2251
  const isMcpWrite =
2224
2252
  request.method === "tools/call" &&
2225
2253
  (
@@ -2248,7 +2276,8 @@ export class EngramAccessHttpServer {
2248
2276
  toolName === "remnic.memory_action_apply"
2249
2277
  )
2250
2278
  ) ||
2251
- codingDecisionWrite
2279
+ codingDecisionWrite ||
2280
+ codingArchitectureWrite
2252
2281
  );
2253
2282
  if (isMcpWrite) {
2254
2283
  this.ensureWriteRateLimitAvailable();
package/src/access-mcp.ts CHANGED
@@ -110,6 +110,7 @@ const MCP_MIGRATED_OPERATIONS: Readonly<Record<string, OperationName>> = {
110
110
  "engram.memory_search": "memory_search",
111
111
  "engram.memory_store": "memory_store",
112
112
  "engram.coding_decision": "coding_decision",
113
+ "engram.coding_architecture": "coding_architecture",
113
114
  };
114
115
 
115
116
  function resolveChatGptInspectorRecallSessionKey(
@@ -324,6 +325,12 @@ export class EngramMcpServer {
324
325
  * pre-feature.
325
326
  */
326
327
  private readonly codingDecisionVisible: boolean;
328
+ /**
329
+ * Whether the `coding_architecture` tool should appear in `tools/list`.
330
+ * Gated on `codingKnowledge.enabled && codingKnowledge.architectureCard`
331
+ * (issue #1548 Track A PR 3, rule 39).
332
+ */
333
+ private readonly architectureCardVisible: boolean;
327
334
 
328
335
  constructor(
329
336
  private readonly service: EngramAccessService,
@@ -333,12 +340,14 @@ export class EngramMcpServer {
333
340
  citationsAutoDetect?: boolean;
334
341
  emitLegacyTools?: boolean;
335
342
  codingDecisionVisible?: boolean;
343
+ architectureCardVisible?: boolean;
336
344
  } = {},
337
345
  ) {
338
346
  this.citationsEnabled = options.citationsEnabled === true;
339
347
  this.citationsAutoDetect = options.citationsAutoDetect !== false;
340
348
  this.emitLegacyTools = options.emitLegacyTools !== false;
341
349
  this.codingDecisionVisible = options.codingDecisionVisible === true;
350
+ this.architectureCardVisible = options.architectureCardVisible === true;
342
351
  this.authenticatedPrincipal =
343
352
  options.principal?.trim() ||
344
353
  readEnvVar("OPENCLAW_ENGRAM_ACCESS_PRINCIPAL")?.trim() ||
@@ -1999,6 +2008,31 @@ export class EngramMcpServer {
1999
2008
  );
2000
2009
  this.tools = [...this.tools, ...codingDecisionTools];
2001
2010
  }
2011
+ if (this.architectureCardVisible) {
2012
+ const architectureTools = withToolAliases(
2013
+ {
2014
+ name: "engram.coding_architecture",
2015
+ description:
2016
+ "Get or refresh the architecture card for the session's coding namespace (issue #1548 Track A PR 3). Subcommands: get, refresh.",
2017
+ inputSchema: {
2018
+ type: "object",
2019
+ properties: {
2020
+ subcommand: {
2021
+ type: "string",
2022
+ enum: ["get", "refresh"],
2023
+ description: "Which architecture-card operation to run.",
2024
+ },
2025
+ sessionKey: { type: "string", description: "Session identifier whose coding context scopes the operation." },
2026
+ namespace: { type: "string", description: "Optional explicit namespace (overrides coding-context overlay)." },
2027
+ },
2028
+ required: ["subcommand"],
2029
+ additionalProperties: false,
2030
+ },
2031
+ },
2032
+ this.emitLegacyTools,
2033
+ );
2034
+ this.tools = [...this.tools, ...architectureTools];
2035
+ }
2002
2036
  }
2003
2037
 
2004
2038
  /** Get clientInfo for a specific MCP session. Returns undefined for non-MCP requests. */
@@ -25,6 +25,10 @@ import {
25
25
  type DecisionSurfaceRequest,
26
26
  type DecisionSurfaceResponse,
27
27
  } from "./coding/decision-surfaces.js";
28
+ import {
29
+ type ArchitectureSurfaceRequest,
30
+ type ArchitectureSurfaceResponse,
31
+ } from "./coding/architecture-surfaces.js";
28
32
 
29
33
  // ---------------------------------------------------------------------------
30
34
  // memory_get — fetch one memory by id
@@ -206,6 +210,46 @@ export const codingDecisionOperation = defineOperation<
206
210
  },
207
211
  });
208
212
 
213
+ // ---------------------------------------------------------------------------
214
+ // coding_architecture — architecture-card surfaces (issue #1548 Track A PR 3)
215
+ // ---------------------------------------------------------------------------
216
+
217
+ const codingArchitectureSchema = z.preprocess(
218
+ (data) => {
219
+ if (data !== null && typeof data === "object" && !Array.isArray(data)) {
220
+ const obj = data as Record<string, unknown>;
221
+ const cleaned: Record<string, unknown> = {};
222
+ for (const [key, value] of Object.entries(obj)) {
223
+ if (value !== null) cleaned[key] = value;
224
+ }
225
+ return cleaned;
226
+ }
227
+ return data;
228
+ },
229
+ z.object({
230
+ subcommand: z.enum(["get", "refresh"]),
231
+ sessionKey: z.string().optional(),
232
+ namespace: z.string().optional(),
233
+ }),
234
+ );
235
+
236
+ export type CodingArchitectureInput = ArchitectureSurfaceRequest;
237
+ export type CodingArchitectureOutput = { result: ArchitectureSurfaceResponse };
238
+
239
+ export const codingArchitectureOperation = defineOperation<
240
+ CodingArchitectureInput,
241
+ CodingArchitectureOutput
242
+ >({
243
+ name: "coding_architecture",
244
+ description:
245
+ "Get or refresh the architecture card for the session's coding namespace (issue #1548 Track A PR 3).",
246
+ schema: codingArchitectureSchema as z.ZodType<CodingArchitectureInput>,
247
+ handler: async (input, ctx) => {
248
+ const result = await ctx.service.codingArchitecture(input, ctx.authenticatedPrincipal);
249
+ return { result };
250
+ },
251
+ });
252
+
209
253
  // ---------------------------------------------------------------------------
210
254
  // Surface registration map — what each transport calls the pilot ops
211
255
  // ---------------------------------------------------------------------------
@@ -220,4 +264,5 @@ export const REGISTERED_OPERATIONS = [
220
264
  memorySearchOperation.spec.name,
221
265
  memoryStoreOperation.spec.name,
222
266
  codingDecisionOperation.spec.name,
267
+ codingArchitectureOperation.spec.name,
223
268
  ] as const;
@@ -19,6 +19,16 @@ import {
19
19
  type DecisionSurfaceRequest,
20
20
  type DecisionSurfaceResponse,
21
21
  } from "./coding/decision-surfaces.js";
22
+ import {
23
+ handleCodingArchitecture,
24
+ type ArchitectureSurfaceRequest,
25
+ type ArchitectureSurfaceResponse,
26
+ type ArchitectureSurfaceStorage,
27
+ createArchitectureVersioningHook,
28
+ ARCHITECTURE_CARD_TAG,
29
+ } from "./coding/architecture-surfaces.js";
30
+ import { buildArchitectureCard, createArchitectureCardSummariser } from "./coding/architecture-card.js";
31
+ import { createVersion } from "./page-versioning.js";
22
32
  import { WorkStorage } from "./work/storage.js";
23
33
  import {
24
34
  exportWorkBoardMarkdown,
@@ -4485,6 +4495,56 @@ export class EngramAccessService {
4485
4495
  });
4486
4496
  }
4487
4497
 
4498
+ /** Whether the coding_architecture tool should appear in tools/list (rule 39). */
4499
+ get architectureCardSurfaceVisible(): boolean {
4500
+ return this.orchestrator.config.codingKnowledge?.enabled === true
4501
+ && this.orchestrator.config.codingKnowledge?.architectureCard === true;
4502
+ }
4503
+ /**
4504
+ * Thin delegate — handler logic in coding/architecture-surfaces.ts (#1548 PR3).
4505
+ * All three surfaces (MCP/HTTP/CLI) arrive here via the boundary operation.
4506
+ * Namespace resolution uses the SAME coding-scoped path as decision records.
4507
+ */
4508
+ async codingArchitecture(
4509
+ request: ArchitectureSurfaceRequest,
4510
+ authenticatedPrincipal?: string,
4511
+ ): Promise<ArchitectureSurfaceResponse> {
4512
+ const resolvedConfig = this.orchestrator.config;
4513
+ return handleCodingArchitecture(request, {
4514
+ codingKnowledge: this.orchestrator.config.codingKnowledge,
4515
+ getCodingContext: (sk) => this.orchestrator.getCodingContextForSession(sk),
4516
+ resolveStorage: async (req) => {
4517
+ const isWrite = req.subcommand === "refresh";
4518
+ const ns = isWrite
4519
+ ? await this.resolveCodingScopedWriteNamespace({
4520
+ namespace: req.namespace,
4521
+ sessionKey: req.sessionKey,
4522
+ authenticatedPrincipal,
4523
+ })
4524
+ : await this.resolveCodingScopedReadableNamespace({
4525
+ namespace: req.namespace,
4526
+ sessionKey: req.sessionKey,
4527
+ authenticatedPrincipal,
4528
+ });
4529
+ const storage = await this.orchestrator.getStorage(ns);
4530
+ return Object.assign(storage, { namespace: ns }) as ArchitectureSurfaceStorage;
4531
+ },
4532
+ buildCard: async (repoRoot) => buildArchitectureCard(repoRoot, {
4533
+ llmSummary: this.orchestrator.config.codingKnowledge?.architectureCardLlmSummary === true,
4534
+ summariser: createArchitectureCardSummariser(this.fallbackLlmRef ?? this.localLlmRef),
4535
+ }),
4536
+ versioning: createArchitectureVersioningHook(
4537
+ resolvedConfig.versioningEnabled === true,
4538
+ resolvedConfig.versioningMaxPerPage,
4539
+ resolvedConfig.versioningSidecarDir,
4540
+ resolvedConfig.memoryDir,
4541
+ (p) => nodeFs.readFile(p, "utf-8"),
4542
+ createVersion,
4543
+ ),
4544
+ throwInputError: (msg) => { throw new EngramAccessInputError(msg); },
4545
+ });
4546
+ }
4547
+
4488
4548
  async memoryBrowse(
4489
4549
  request: EngramAccessMemoryBrowseRequest = {},
4490
4550
  ): Promise<EngramAccessMemoryBrowseResponse> {
@@ -67,7 +67,7 @@ function shortToolName(advertised: string): string {
67
67
  /** Spin up a server with emitLegacyTools=true and read the deduped short names. */
68
68
  async function liveMcpToolShortNames(): Promise<ReadonlySet<string>> {
69
69
  const stub = { briefingEnabled: true } as unknown as EngramAccessService;
70
- const server = new EngramMcpServer(stub, { emitLegacyTools: true, codingDecisionVisible: true });
70
+ const server = new EngramMcpServer(stub, { emitLegacyTools: true, codingDecisionVisible: true, architectureCardVisible: true });
71
71
  const response = await server.handleRequest({ jsonrpc: "2.0", id: 1, method: "tools/list" });
72
72
  const result = (response as { result?: { tools?: Array<{ name: string }> } }).result;
73
73
  const names = new Set<string>();
@@ -73,6 +73,7 @@ export const MCP_TOOLS: readonly McpToolEntry[] = [
73
73
  { tool: "memory_timeline", operation: null },
74
74
  { tool: "memory_store", operation: "memory_store" },
75
75
  { tool: "coding_decision", operation: "coding_decision" },
76
+ { tool: "coding_architecture", operation: "coding_architecture" },
76
77
  { tool: "suggestion_submit", operation: null },
77
78
  { tool: "entity_get", operation: null },
78
79
  { tool: "review_queue_list", operation: null },
@@ -170,6 +171,7 @@ export const HTTP_ROUTES: readonly HttpRouteEntry[] = [
170
171
  { method: "GET", pathname: "/engram/v1/lcm/status", operation: null },
171
172
  { method: "POST", pathname: "/engram/v1/memories", operation: "memory_store" },
172
173
  { method: "POST", pathname: "/engram/v1/coding/decisions", operation: "coding_decision" },
174
+ { method: "POST", pathname: "/engram/v1/coding/architecture", operation: "coding_architecture" },
173
175
  { method: "POST", pathname: "/engram/v1/suggestions", operation: null },
174
176
  { method: "GET", pathname: "/engram/v1/memories", operation: null },
175
177
  { method: "GET", pathname: "/engram/v1/memories/:id", operation: "memory_get" },
package/src/cli.ts CHANGED
@@ -57,6 +57,7 @@ import {
57
57
  } from "./maintenance/rebuild-memory-projection.js";
58
58
  import { rebuildObservations } from "./maintenance/rebuild-observations.js";
59
59
  import { migrateObservations } from "./maintenance/migrate-observations.js";
60
+ import { formatNamespaceMaintenanceHealthText } from "./maintenance/namespace-maintenance-fanout.js";
60
61
  import {
61
62
  listNamespaces,
62
63
  runNamespaceMigration,
@@ -2801,6 +2802,7 @@ export async function runAccessMcpServeCliCommand(
2801
2802
  principal: options.principal,
2802
2803
  emitLegacyTools: options.emitLegacyTools,
2803
2804
  codingDecisionVisible: service.decisionRecordSurfaceVisible,
2805
+ architectureCardVisible: service.architectureCardSurfaceVisible,
2804
2806
  });
2805
2807
  await server.runStdio(options.stdin ?? process.stdin, options.stdout ?? process.stdout);
2806
2808
  return { ok: true };
@@ -4292,6 +4294,22 @@ export function registerCli(
4292
4294
  }
4293
4295
  });
4294
4296
 
4297
+ namespacesCmd
4298
+ .command("maintenance")
4299
+ .description(
4300
+ "Show per-namespace maintenance status (issue #1500). Reports which namespaces were maintained, skipped, or failed for each standard job.",
4301
+ )
4302
+ .option("--json", "Emit machine-readable JSON only")
4303
+ .action(async (...args: unknown[]) => {
4304
+ const options = (args[0] ?? {}) as Record<string, unknown>;
4305
+ const summary = await orchestrator.readNamespaceMaintenanceHealth();
4306
+ if (reportHasMachineReadableOutput(options)) {
4307
+ console.log(JSON.stringify(summary, null, 2));
4308
+ } else {
4309
+ console.log(formatNamespaceMaintenanceHealthText(summary));
4310
+ }
4311
+ });
4312
+
4295
4313
  cmd
4296
4314
  .command("export")
4297
4315
  .description("Export Remnic memory to JSON, Markdown bundle, or SQLite")