@rubytech/create-maxy-code 0.1.144 → 0.1.146

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 (130) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/neo4j/schema.cypher +154 -4
  3. package/payload/platform/plugins/admin/PLUGIN.md +1 -1
  4. package/payload/platform/plugins/admin/hooks/post-turn-graph-pass.sh +251 -0
  5. package/payload/platform/plugins/admin/skills/datetime/SKILL.md +63 -5
  6. package/payload/platform/plugins/docs/references/internals.md +16 -0
  7. package/payload/platform/plugins/docs/references/memory-guide.md +10 -0
  8. package/payload/platform/plugins/docs/references/platform.md +1 -1
  9. package/payload/platform/plugins/memory/.claude-plugin/plugin.json +1 -1
  10. package/payload/platform/plugins/memory/PLUGIN.md +25 -3
  11. package/payload/platform/plugins/memory/mcp/dist/index.js +309 -11
  12. package/payload/platform/plugins/memory/mcp/dist/index.js.map +1 -1
  13. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/compiled-truth-revision.test.d.ts +2 -0
  14. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/compiled-truth-revision.test.d.ts.map +1 -0
  15. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/compiled-truth-revision.test.js +33 -0
  16. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/compiled-truth-revision.test.js.map +1 -0
  17. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/graph-write-gate-label.test.d.ts +2 -0
  18. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/graph-write-gate-label.test.d.ts.map +1 -0
  19. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/graph-write-gate-label.test.js +30 -0
  20. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/graph-write-gate-label.test.js.map +1 -0
  21. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/phase-prune-revisions.test.d.ts +2 -0
  22. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/phase-prune-revisions.test.d.ts.map +1 -0
  23. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/phase-prune-revisions.test.js +51 -0
  24. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/phase-prune-revisions.test.js.map +1 -0
  25. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/relative-date.test.d.ts +2 -0
  26. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/relative-date.test.d.ts.map +1 -0
  27. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/relative-date.test.js +68 -0
  28. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/relative-date.test.js.map +1 -0
  29. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/timeline-extractor.test.js +41 -0
  30. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/timeline-extractor.test.js.map +1 -1
  31. package/payload/platform/plugins/memory/mcp/dist/lib/compiled-truth-revision.d.ts +43 -0
  32. package/payload/platform/plugins/memory/mcp/dist/lib/compiled-truth-revision.d.ts.map +1 -0
  33. package/payload/platform/plugins/memory/mcp/dist/lib/compiled-truth-revision.js +33 -0
  34. package/payload/platform/plugins/memory/mcp/dist/lib/compiled-truth-revision.js.map +1 -0
  35. package/payload/platform/plugins/memory/mcp/dist/lib/compiled-truth-rewriter.d.ts +18 -5
  36. package/payload/platform/plugins/memory/mcp/dist/lib/compiled-truth-rewriter.d.ts.map +1 -1
  37. package/payload/platform/plugins/memory/mcp/dist/lib/compiled-truth-rewriter.js +64 -17
  38. package/payload/platform/plugins/memory/mcp/dist/lib/compiled-truth-rewriter.js.map +1 -1
  39. package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/index.d.ts +44 -0
  40. package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/index.d.ts.map +1 -0
  41. package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/index.js +14 -0
  42. package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/index.js.map +1 -0
  43. package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-citation-audit.d.ts +26 -0
  44. package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-citation-audit.d.ts.map +1 -0
  45. package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-citation-audit.js +219 -0
  46. package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-citation-audit.js.map +1 -0
  47. package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-dead-edges.d.ts +19 -0
  48. package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-dead-edges.d.ts.map +1 -0
  49. package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-dead-edges.js +39 -0
  50. package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-dead-edges.js.map +1 -0
  51. package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-orphans.d.ts +19 -0
  52. package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-orphans.d.ts.map +1 -0
  53. package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-orphans.js +42 -0
  54. package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-orphans.js.map +1 -0
  55. package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-prune-revisions.d.ts +21 -0
  56. package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-prune-revisions.d.ts.map +1 -0
  57. package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-prune-revisions.js +27 -0
  58. package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-prune-revisions.js.map +1 -0
  59. package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-stale-truth.d.ts +18 -0
  60. package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-stale-truth.d.ts.map +1 -0
  61. package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-stale-truth.js +60 -0
  62. package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-stale-truth.js.map +1 -0
  63. package/payload/platform/plugins/memory/mcp/dist/lib/graph-write-gate.d.ts +20 -3
  64. package/payload/platform/plugins/memory/mcp/dist/lib/graph-write-gate.d.ts.map +1 -1
  65. package/payload/platform/plugins/memory/mcp/dist/lib/graph-write-gate.js +47 -12
  66. package/payload/platform/plugins/memory/mcp/dist/lib/graph-write-gate.js.map +1 -1
  67. package/payload/platform/plugins/memory/mcp/dist/lib/relative-date.d.ts +13 -0
  68. package/payload/platform/plugins/memory/mcp/dist/lib/relative-date.d.ts.map +1 -0
  69. package/payload/platform/plugins/memory/mcp/dist/lib/relative-date.js +191 -0
  70. package/payload/platform/plugins/memory/mcp/dist/lib/relative-date.js.map +1 -0
  71. package/payload/platform/plugins/memory/mcp/dist/lib/timeline-extractor.d.ts +7 -1
  72. package/payload/platform/plugins/memory/mcp/dist/lib/timeline-extractor.d.ts.map +1 -1
  73. package/payload/platform/plugins/memory/mcp/dist/lib/timeline-extractor.js +37 -9
  74. package/payload/platform/plugins/memory/mcp/dist/lib/timeline-extractor.js.map +1 -1
  75. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-compiled-truth-history.test.d.ts +2 -0
  76. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-compiled-truth-history.test.d.ts.map +1 -0
  77. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-compiled-truth-history.test.js +81 -0
  78. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-compiled-truth-history.test.js.map +1 -0
  79. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-report-tools.test.d.ts +2 -0
  80. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-report-tools.test.d.ts.map +1 -0
  81. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-report-tools.test.js +115 -0
  82. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-report-tools.test.js.map +1 -0
  83. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-update-emit.test.js +1 -0
  84. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-update-emit.test.js.map +1 -1
  85. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-write-emit.test.js +1 -0
  86. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-write-emit.test.js.map +1 -1
  87. package/payload/platform/plugins/memory/mcp/dist/tools/memory-compiled-truth-history.d.ts +19 -0
  88. package/payload/platform/plugins/memory/mcp/dist/tools/memory-compiled-truth-history.d.ts.map +1 -0
  89. package/payload/platform/plugins/memory/mcp/dist/tools/memory-compiled-truth-history.js +50 -0
  90. package/payload/platform/plugins/memory/mcp/dist/tools/memory-compiled-truth-history.js.map +1 -0
  91. package/payload/platform/plugins/memory/mcp/dist/tools/memory-dream-run.d.ts +45 -0
  92. package/payload/platform/plugins/memory/mcp/dist/tools/memory-dream-run.d.ts.map +1 -0
  93. package/payload/platform/plugins/memory/mcp/dist/tools/memory-dream-run.js +170 -0
  94. package/payload/platform/plugins/memory/mcp/dist/tools/memory-dream-run.js.map +1 -0
  95. package/payload/platform/plugins/memory/mcp/dist/tools/memory-report-list.d.ts +28 -0
  96. package/payload/platform/plugins/memory/mcp/dist/tools/memory-report-list.d.ts.map +1 -0
  97. package/payload/platform/plugins/memory/mcp/dist/tools/memory-report-list.js +64 -0
  98. package/payload/platform/plugins/memory/mcp/dist/tools/memory-report-list.js.map +1 -0
  99. package/payload/platform/plugins/memory/mcp/dist/tools/memory-report-read-latest.d.ts +24 -0
  100. package/payload/platform/plugins/memory/mcp/dist/tools/memory-report-read-latest.d.ts.map +1 -0
  101. package/payload/platform/plugins/memory/mcp/dist/tools/memory-report-read-latest.js +49 -0
  102. package/payload/platform/plugins/memory/mcp/dist/tools/memory-report-read-latest.js.map +1 -0
  103. package/payload/platform/plugins/memory/mcp/dist/tools/memory-report-write.d.ts +34 -0
  104. package/payload/platform/plugins/memory/mcp/dist/tools/memory-report-write.d.ts.map +1 -0
  105. package/payload/platform/plugins/memory/mcp/dist/tools/memory-report-write.js +99 -0
  106. package/payload/platform/plugins/memory/mcp/dist/tools/memory-report-write.js.map +1 -0
  107. package/payload/platform/plugins/memory/mcp/dist/tools/memory-review-queue.d.ts +29 -0
  108. package/payload/platform/plugins/memory/mcp/dist/tools/memory-review-queue.d.ts.map +1 -0
  109. package/payload/platform/plugins/memory/mcp/dist/tools/memory-review-queue.js +66 -0
  110. package/payload/platform/plugins/memory/mcp/dist/tools/memory-review-queue.js.map +1 -0
  111. package/payload/platform/plugins/memory/mcp/dist/tools/memory-search.d.ts.map +1 -1
  112. package/payload/platform/plugins/memory/mcp/dist/tools/memory-search.js +122 -2
  113. package/payload/platform/plugins/memory/mcp/dist/tools/memory-search.js.map +1 -1
  114. package/payload/platform/plugins/memory/mcp/dist/tools/memory-update.d.ts +49 -13
  115. package/payload/platform/plugins/memory/mcp/dist/tools/memory-update.d.ts.map +1 -1
  116. package/payload/platform/plugins/memory/mcp/dist/tools/memory-update.js +184 -16
  117. package/payload/platform/plugins/memory/mcp/dist/tools/memory-update.js.map +1 -1
  118. package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.d.ts.map +1 -1
  119. package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.js +53 -10
  120. package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.js.map +1 -1
  121. package/payload/platform/plugins/memory/mcp/vitest.config.ts +6 -0
  122. package/payload/platform/plugins/memory/references/schema-base.md +4 -0
  123. package/payload/platform/plugins/memory/skills/conversation-archive/SKILL.md +3 -4
  124. package/payload/platform/plugins/scheduling/skills/briefing/SKILL.md +13 -2
  125. package/payload/platform/scripts/seed-neo4j.sh +1 -1
  126. package/payload/platform/templates/agents/admin/IDENTITY.md +9 -1
  127. package/payload/platform/templates/specialists/agents/database-operator.md +30 -2
  128. package/payload/server/server.js +195 -91
  129. package/payload/platform/plugins/admin/hooks/signal-detector-stop.sh +0 -309
  130. package/payload/platform/templates/specialists/agents/signal-detector.md +0 -129
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/create-maxy-code",
3
- "version": "0.1.144",
3
+ "version": "0.1.146",
4
4
  "description": "Install Maxy — AI for Productive People",
5
5
  "bin": {
6
6
  "create-maxy-code": "./dist/index.js"
@@ -363,11 +363,19 @@ ON EACH [n.name, n.firstName, n.lastName, n.givenName, n.familyName,
363
363
  n.subject, n.bodyPreview, n.fromName, n.fromAddress, n.agentAddress, n.screeningReason,
364
364
  n.authority, n.contactValue, n.toolName,
365
365
  n.displayName, n.slug, n.role,
366
- n.compiledTruth];
366
+ n.compiledTruth, n.compiledTruthPublic];
367
367
  // `compiledTruth` (Task 306) is the authoritative-summary property the
368
368
  // compiled-truth + timeline knowledge model writes on Person/Company/Concept.
369
369
  // Indexed here so Task 308's compiledTruth boost path also benefits from BM25
370
370
  // hits against the summary text. No-op until 306 writes the property.
371
+ //
372
+ // `compiledTruthPublic` (Task 392) is the customer-facing twin on the four
373
+ // public-twin labels (Organization | Concept | Project | LocalBusiness). The
374
+ // public-agent's `memory-search` projection swaps `compiledTruth` →
375
+ // `compiledTruthPublic` on the way out for scope='public' reads (see
376
+ // platform/plugins/memory/mcp/src/tools/memory-search.ts). Both fields stay
377
+ // in the BM25 union so admin queries rank on the operator's view and public
378
+ // queries rank on the customer-facing summary.
371
379
 
372
380
  // Project node — a standalone creative-output node distinct from
373
381
  // :Section. Anchored via (:UserProfile)-[:CREATED]->(:Project), with optional
@@ -1328,15 +1336,27 @@ OPTIONS {
1328
1336
  };
1329
1337
 
1330
1338
  // ----------------------------------------------------------
1331
- // Compiled-truth + timeline knowledge model (Task 306) —
1339
+ // Compiled-truth + timeline knowledge model (Task 306, extended Task 392) —
1332
1340
  // gbrain's "compiled truth above the rule, append-only timeline
1333
- // below" pattern. Every :Person / :Organization / :Concept carries
1341
+ // below" pattern. Every entity subject in the admin-twin set
1342
+ // (:Person | :Organization | :Concept | :Project | :LocalBusiness) carries
1334
1343
  // an agent-rewritten summary (`compiledTruth`, ≤2000 chars) plus
1335
1344
  // a history of dated `:TimelineEvent` rows path-anchored via
1336
1345
  // `(entity)-[:HAS_TIMELINE_EVENT]->(:TimelineEvent)`. The
1337
1346
  // compiledTruth is what the agent reads when it needs a summary;
1338
1347
  // the timeline is what it cites and appends to.
1339
1348
  //
1349
+ // Public-twin (Task 392): four of the five subject labels also carry
1350
+ // `compiledTruthPublic` (≤2000 chars) — the customer-facing summary
1351
+ // the public agent surfaces. The public-twin set is
1352
+ // :Organization | :Concept | :Project | :LocalBusiness. :Person is
1353
+ // deliberately excluded: an operator's notes on a contact are private
1354
+ // by definition and have no neutral customer-facing form. The
1355
+ // public-agent `memory-search` projection (scope='public') rewrites
1356
+ // the returned property bag so `compiledTruth` is renamed to
1357
+ // `compiledTruthPublic` on public-twin labels and stripped entirely
1358
+ // on :Person.
1359
+ //
1340
1360
  // Naming:
1341
1361
  // :TimelineEvent — distinct from :Event (scheduling plugin's
1342
1362
  // schema:Event with eventId-unique). Collisions were the reason
@@ -1357,8 +1377,19 @@ OPTIONS {
1357
1377
  // `compiledTruth` source:
1358
1378
  // compiledTruthSource = "rewriter" (default) — compiled-truth-rewriter
1359
1379
  // library wrote it.
1360
- // compiledTruthSource = "operator" — operator overrode via the graph UI.
1380
+ // compiledTruthSource = "operator" — operator overrode via the graph UI
1381
+ // (legacy admin-server only) or via memory-update direct-write.
1361
1382
  // Rewriter refuses to overwrite for 7 days unless forceRewrite=true.
1383
+ //
1384
+ // `compiledTruthPublic` source (Task 392):
1385
+ // compiledTruthPublicSource = "rewriter" (default) — compiled-truth-rewriter
1386
+ // `mode: 'public'` pass wrote it. Fires inline after a successful admin
1387
+ // pass when the label is in the public-twin set and the admin pass was
1388
+ // not a no-op.
1389
+ // compiledTruthPublicSource = "operator" — operator overrode via chat
1390
+ // (memory-update `compiledTruthPublic` direct-write argument). Public
1391
+ // rewriter refuses to overwrite for 7 days unless forceRewrite=true.
1392
+ // Cooldown is independent of the admin cooldown.
1362
1393
  // ----------------------------------------------------------
1363
1394
 
1364
1395
  CREATE CONSTRAINT timeline_event_id_unique IF NOT EXISTS
@@ -1392,3 +1423,122 @@ FOR (o:Organization) ON (o.compiledTruthUpdatedAt);
1392
1423
 
1393
1424
  CREATE INDEX concept_compiled_truth_updated_at IF NOT EXISTS
1394
1425
  FOR (c:Concept) ON (c.compiledTruthUpdatedAt);
1426
+
1427
+ // Admin-twin extension (Task 392) — :Project and :LocalBusiness joined the
1428
+ // admin-twin set when the public-twin landed. Same stale-detection index.
1429
+ CREATE INDEX project_compiled_truth_updated_at IF NOT EXISTS
1430
+ FOR (p:Project) ON (p.compiledTruthUpdatedAt);
1431
+
1432
+ CREATE INDEX local_business_compiled_truth_updated_at IF NOT EXISTS
1433
+ FOR (b:LocalBusiness) ON (b.compiledTruthUpdatedAt);
1434
+
1435
+ // Public-twin stale-detection indexes (Task 392). Same role as the admin
1436
+ // indexes above — find public-twin nodes whose public summary lags the
1437
+ // admin summary by N days. :Person omitted by design (no public twin).
1438
+ CREATE INDEX organization_compiled_truth_public_updated_at IF NOT EXISTS
1439
+ FOR (o:Organization) ON (o.compiledTruthPublicUpdatedAt);
1440
+
1441
+ CREATE INDEX concept_compiled_truth_public_updated_at IF NOT EXISTS
1442
+ FOR (c:Concept) ON (c.compiledTruthPublicUpdatedAt);
1443
+
1444
+ CREATE INDEX project_compiled_truth_public_updated_at IF NOT EXISTS
1445
+ FOR (p:Project) ON (p.compiledTruthPublicUpdatedAt);
1446
+
1447
+ CREATE INDEX local_business_compiled_truth_public_updated_at IF NOT EXISTS
1448
+ FOR (b:LocalBusiness) ON (b.compiledTruthPublicUpdatedAt);
1449
+
1450
+ // ----------------------------------------------------------
1451
+ // :CompiledTruthRevision (Task 391) — audit history of every
1452
+ // `compiledTruth` value an entity has ever held. Captured in the
1453
+ // same transaction as the rewriter SET so revision history can
1454
+ // never desync from the live property. Pruned by dream-cycle
1455
+ // phase 5 (keep newest 20 per entity) — never written-and-pruned
1456
+ // in the same TX as the main rewrite.
1457
+ //
1458
+ // Read-only after write: graph-write-gate rejects property
1459
+ // updates and label-anchored MERGE on this label. The dream-cycle
1460
+ // prune is the only mutation path.
1461
+ //
1462
+ // MERGE doctrine: writers MUST path-anchor from the entity to
1463
+ // defend cross-account collisions:
1464
+ // MATCH (e) WHERE elementId(e) = $entityId AND e.accountId = $accountId
1465
+ // MERGE (e)-[:HAS_REVISION]->(r:CompiledTruthRevision {revisionId: $id})
1466
+ // Never label-only MERGE on :CompiledTruthRevision.
1467
+ //
1468
+ // `source` enum (exact, no catch-all):
1469
+ // 'rewriter' — compiled-truth-rewriter library wrote the truth.
1470
+ // 'operator' — operator overrode via a sanctioned write route.
1471
+ // ----------------------------------------------------------
1472
+
1473
+ CREATE CONSTRAINT compiled_truth_revision_id_unique IF NOT EXISTS
1474
+ FOR (r:CompiledTruthRevision) REQUIRE r.revisionId IS UNIQUE;
1475
+
1476
+ CREATE INDEX compiled_truth_revision_account IF NOT EXISTS
1477
+ FOR (r:CompiledTruthRevision) ON (r.accountId);
1478
+
1479
+ CREATE INDEX compiled_truth_revision_written_at IF NOT EXISTS
1480
+ FOR (r:CompiledTruthRevision) ON (r.writtenAt);
1481
+
1482
+ // ----------------------------------------------------------
1483
+ // :Report node — durable, keyword-routed output of admin
1484
+ // workflows (daily briefing, dream cycle, ad-hoc analyses).
1485
+ // Task 332. Append-only by design: reports are not edited
1486
+ // after write. Parented to the active :AdminConversation
1487
+ // (via :PRODUCED, injected at write time) or to the
1488
+ // account's :AdminUser when no conversation context exists.
1489
+ // `keywords` is a string array; `sourceWorkflow` names the
1490
+ // originating skill or scheduled workflow (e.g.
1491
+ // "daily-briefing", "dream-cycle"). Embedded so the existing
1492
+ // `memory-search` hybrid pipeline can surface reports
1493
+ // alongside other knowledge.
1494
+ //
1495
+ // Dream-cycle queues (Task 327): :OrphanCandidate and
1496
+ // :CitationProposal hold phase-1 and phase-4 candidates the
1497
+ // operator triages via `memory-review-queue`. Edges to
1498
+ // `:Trashed` targets get a `staleTarget: true` property
1499
+ // from phase 3 (audit preserved; no deletion). :TimelineEvent
1500
+ // citations are written additively as `citations: [string]`
1501
+ // — no new label needed.
1502
+ // ----------------------------------------------------------
1503
+
1504
+ CREATE CONSTRAINT report_id_unique IF NOT EXISTS
1505
+ FOR (r:Report) REQUIRE r.reportId IS UNIQUE;
1506
+
1507
+ CREATE INDEX report_account IF NOT EXISTS
1508
+ FOR (r:Report) ON (r.accountId);
1509
+
1510
+ CREATE INDEX report_occurred_at IF NOT EXISTS
1511
+ FOR (r:Report) ON (r.occurredAt);
1512
+
1513
+ CREATE INDEX report_source_workflow IF NOT EXISTS
1514
+ FOR (r:Report) ON (r.sourceWorkflow);
1515
+
1516
+ CREATE INDEX report_title IF NOT EXISTS
1517
+ FOR (r:Report) ON (r.title);
1518
+
1519
+ CREATE VECTOR INDEX report_embedding IF NOT EXISTS
1520
+ FOR (r:Report) ON (r.embedding)
1521
+ OPTIONS {
1522
+ indexConfig: {
1523
+ `vector.dimensions`: 768,
1524
+ `vector.similarity_function`: 'cosine'
1525
+ }
1526
+ };
1527
+
1528
+ CREATE CONSTRAINT orphan_candidate_id_unique IF NOT EXISTS
1529
+ FOR (o:OrphanCandidate) REQUIRE o.orphanCandidateId IS UNIQUE;
1530
+
1531
+ CREATE INDEX orphan_candidate_account IF NOT EXISTS
1532
+ FOR (o:OrphanCandidate) ON (o.accountId);
1533
+
1534
+ CREATE INDEX orphan_candidate_target IF NOT EXISTS
1535
+ FOR (o:OrphanCandidate) ON (o.targetElementId);
1536
+
1537
+ CREATE CONSTRAINT citation_proposal_id_unique IF NOT EXISTS
1538
+ FOR (p:CitationProposal) REQUIRE p.citationProposalId IS UNIQUE;
1539
+
1540
+ CREATE INDEX citation_proposal_account IF NOT EXISTS
1541
+ FOR (p:CitationProposal) ON (p.accountId);
1542
+
1543
+ CREATE INDEX citation_proposal_target IF NOT EXISTS
1544
+ FOR (p:CitationProposal) ON (p.timelineEventId);
@@ -140,7 +140,7 @@ Tools are available via the `admin` MCP server.
140
140
  - `hooks/webfetch-preflight.mjs` — short-circuits WebFetch on JS-SPA shells with a structured `WEBFETCH_CANNOT_READ_JS_SPA` error so the agent surfaces a loud failure to the owner instead of paying the 60s extraction timeout. Fail-open on any internal error.
141
141
  - `hooks/askuserquestion-investigate-gate.sh` — PreToolUse matcher=`AskUserQuestion`. Blocks the question (exit 2) when no read-only investigation tool has fired since the latest real user turn in the session JSONL. The structural fix for the failure class where the agent fabricates a menu before evidence-gathering (session `c085ec2c-46fb-4b73-8865-68cf85866ea8` 2026-05-22 — "change remote access password" → invented options "Admin PIN / Cloudflare tunnel / WiFi password" with zero prior tool_use; post-correction the agent immediately fired `remote-auth-status` → `ToolSearch` → `remote-auth-set-password`, proving it knew the moves). **Allowlist** (exact, with trailing `__<tool>` suffix-match for namespaced `mcp__plugin_<plugin>_<server>__<tool>` aliases): `ToolSearch`, `Grep`, `Glob`, `Read`, `LS`, `NotebookRead`, `Bash`, `WebFetch`, `WebSearch`, plus the read-only admin / memory MCP tools (`*-status`, `*-list`, `*-read`, `skill-find`, `memory-find-candidates`, `profile-read`, `conversation-list`, `memory-list-attachments`, `memory-read-attachment`). **Block message:** `Blocked: AskUserQuestion requires at least one investigation tool (ToolSearch, Grep, Read, *-list, *-status, *-read, skill-find, ...) earlier in this turn. Search the operator's literal phrase first.` **Log line** (stderr, one per call): `[ask-gate] decision=<allow|block> sessionId=<id8> seen=<csv|-> reason=<allowlist-hit|no-investigation|fail-open-no-transcript|fail-open-parse-error>`. **Fail-open** on missing transcript or parse error — nudges, never bricks the UI.
142
142
  - `hooks/session-end-retrospective.sh` — **Stop hook.** Fires on every assistant end_turn but exits 0 with `trigger-skipped reason=<role-not-admin|is-specialist|empty-stdin|missing-transcript|no-intent-match>` on every path EXCEPT when the operator's latest real-user message carries an end-intent token (`/end`, `/archive`, `end session`, `archive this session`, case-insensitive, must match the whole message or a standalone line — not embedded in prose). On that path the hook blocks (exit 2 + structured retrospective instruction on stderr per Stop-hook contract) and emits `[session-retrospective] gate-blocked sessionId=<id> reason=sentinel-absent` until the admin agent calls the `session-retrospective-mark-complete` MCP sentinel tool. Completion is recognised by greping the operator's own JSONL (`transcript_path` on the Stop envelope) for a `tool_use` block whose `name` equals that exact string — never by parsing prose ([[feedback_no_stdout_parsing_for_control_flow]], [[feedback_doctrine_paragraph_is_not_a_gate]]). The sentinel-grep doubles as the re-entry guard: every Stop after the sentinel call sees it and exits 0 with `gate-released sessionId=<id>`. Recursion guard: any session whose `MAXY_SPECIALIST` env is set (specialist subagent dispatched via Task tool) skips the gate. The Sidebar archive button POSTs `/:sessionId/archive` directly to the manager which kills the PTY — no Stop fires after that, so clicking archive is the operator's current escape hatch (opt-out remains deferred per the task spec). All emits go through `POST /api/admin/log-ingest`; the only stderr writer is the gate-blocked path (the instruction block the agent reads). The retrospective itself runs as one or more additional turns inside the operator's existing admin session — Task-tool delegation to `database-operator` is the existing IDENTITY.md "Recording" route, not a parallel admin session ([[feedback_deterministic_means_remove_llm]]).
143
- - `hooks/signal-detector-stop.sh` — **Stop hook.** Task 303gbrain port. Fires on every admin-agent end_turn alongside `session-end-retrospective.sh` (both registered on the Stop matcher). Inspects the operator's JSONL transcript and decides whether to dispatch the `signal-detector` specialist for background brain-compounding. Throttle gates: 3 new real-user turns since the most recent in-transcript Task call with `subagent_type=signal-detector` (overridable via `MAXY_SIGNAL_DETECTOR_MIN_NEW_TURNS`) AND ≥30 000 ms since the latest real-user message's `timestamp` (overridable via `MAXY_SIGNAL_DETECTOR_MIN_IDLE_MS`). Below either threshold exit 0 with `trigger-skipped reason=below-{turn,idle}-threshold`. Recursion guard mirrors `session-end-retrospective.sh`: any session with `MAXY_SPECIALIST` set on the PTY env is skipped, plus the in-transcript Task-call grep means the very next Stop after the agent's dispatch turn sees the call and exits 0 (`reason=already-dispatched`). Gate-blocked path emits exit 2 + stderr directive `[system: signal-detector-stop-hook]` per Stop-hook contract; the admin IDENTITY.md "Stop-hook system directives" section binds the agent's response shape (perform one `Task` dispatch, emit a single-space ack, stop). First-fire bootstrap: idempotently copies the template from `$MAXY_PLATFORM_ROOT/templates/specialists/agents/signal-detector.md` into `$ACCOUNT_DIR/specialists/agents/` and appends the AGENTS.md routing line so the specialist is available platform-core without onboarding-flow surgery. All emits go through `POST /api/admin/log-ingest` under tag `signal-detector-hook`. Operator-channel doctrine: by design, only the admin agent's stop fires this hook (public/visitor channels never do high-value signal flows through the operator, not the visitor).
143
+ - `hooks/post-turn-graph-pass.sh` — **Stop hook.** Task 412supersedes the original signal-detector-stop hook (Task 303) which had three structural defects (idle gate measured assistant response duration not operator pause, recursion guard greped the wrong tool name so `newUserTurnsSinceLastRun` never reset, single-excerpt throttle dropped 2 of every 3 operator turns). Fires on every admin-agent `end_turn` alongside `session-end-retrospective.sh`. **No throttle, no idle gate, no in-transcript recursion-guard grep.** One dispatch per `end_turn`, every time. Walks the operator's JSONL once, collects every assistant `text` block and every non-`tool_result` user message as `[role: text]` pairs (oldest first), then `GET /api/admin/post-turn-context?conversationId&accountId` to fetch the actual content of every node already written under this conversationId, and dispatches `database-operator` via exit 2 + stderr directive `[system: post-turn-graph-pass-stop-hook]` per Stop-hook contract. The dispatch prompt carries two interpolated blocks `<conversation>` and `<prior-writes>` — and the specialist body's background-pass mode reads both verbatim and decides what new graph rows the conversation supports (mentions, ideas, learnings, contacts, tasks, anything the schema admits). Duplicate writes between this background pass and the admin agent's discretionary inline `database-operator` dispatch are accepted at write time; cleanup is delegated to the dream-cycle / hygiene sweeps (Tasks 410, 411). Remaining early exits (the only two): `reason=role-not-admin` (the hook fires only on admin-agent stops by doctrine) and `reason=is-specialist` (`MAXY_SPECIALIST` env-stamped on every subagent PTY blocks the hook from recursing inside the specialist's own stop). All emits go through `POST /api/admin/log-ingest` under tag `post-turn-graph-pass`. The canonical fire line is `dispatch sessionId=<id> conversationId=<id> conversationTurns=<n> priorWritesCount=<n> contextBytes=<n> ms=<n>`; failure to see a matching `db-op: writes=<n> ms=<n>` line in the admin-resume stream within 30 s means the specialist crashed or `memory-write` rejected silently.
144
144
  - `hooks/turn-completed-graph-write.sh` — **Dormant since Task 214** — the Stop-hook registration is no longer written to account settings.json; admin delegates graph writes to `database-operator` via the Task tool. The script, envelope walker, loopback `/api/admin/claude-sessions` 127.0.0.1 bypass, `[turn-recorder]` emitters, recorder-auto-archive subscriber, and `initialMessage` envelope spec are preserved as reusable infrastructure for any future autonomous post-turn flow. Historical contract (still describes the dormant path): Stop hook fired once per completed admin-agent turn. Gates on `MAXY_SESSION_ROLE=admin` + `MAXY_SPECIALIST!=database-operator` so it never recurses into the recorder PTY or fires on public sessions. **Task 147** — the recorder is spawned via the same route Sidebar uses. ONE POST to `POST /api/admin/claude-sessions`, body carries `{specialist:'database-operator', model:'haiku', initialMessage:<json-envelope>, adminSessionId:<op>}` — no synthetic `senderId: 'turn-recorder'` marker. The Hono wrapper bypasses cookie auth on this exact method+path when the request originates from `127.0.0.1` (same trust boundary the claude-session-manager itself relies on), looks up the operator's senderId from the manager's `/<adminSessionId>/meta`, and forwards a Sidebar-shape spawn body. The `/recorder-spawn` sibling route is gone. The hook reads its UI port from `MAXY_UI_INTERNAL_PORT` (stamped on the manager systemd unit) — no fallback; absence emits `[turn-recorder] spawn-failed reason=missing-env env=MAXY_UI_INTERNAL_PORT` to stderr instead of silently 19199-ing. **Task 177** — `initialMessage` is a JSON-stringified envelope; the schema lives in [`platform/plugins/docs/references/admin-session.md`](../docs/references/admin-session.md) under "`initialMessage` JSON envelope (Task 177)". Top-level keys exactly: `turns`, `conversationId`, `accountId`, `occurredAt`. `turns` is the full conversation transcript, oldest first, newest last, with each entry `{ role: "user"|"assistant", text, ts, toolCalls? }`. No windowing, no truncation. Multi-record assistant messages collapse on `message.id`. `tool_use` and matching `tool_result` blocks attach as a single `toolCalls` entry on the owning assistant turn; the user record carrying only the `tool_result` does not create a separate user turn. No leading instruction prose. `toolCalls[].input` and `toolCalls[].output` are native JSON values, never re-stringified. (Replaces Task 175's `(operatorMessage, assistantReply)` pair, which asserted a temporal Q→A relationship the walker never enforced.) One observability line `[turn-recorder] envelope sessionId=<op> turnsCount=<n> userTurns=<n> assistantTurns=<n> toolCallTurns=<n>` precedes `spawn-request`. The envelope rides on the `/spawn` body as a trailing positional argv to `claude`, so the database-operator session's JSONL first `role=user` line is the JSON object verbatim. No separate `POST /:id/input` call, no bracketed-paste, no keystroke injection. The recorder-auto-archive subscriber stops the recorder PTY as soon as its JSONL contains an assistant message with `stop_reason === "end_turn"`. **Task 129** — every emit goes through `POST /api/admin/log-ingest` so the lines land in `server.log` keyed by the operator session id. The chain is `trigger` → `spawn-request` → `spawn-success` → `input-delivered` → `tool-call` × N → `tool-result` × N → `write-complete` → `auto-archive`; each gated-off path emits one `trigger-skipped reason=<role-not-admin|is-recorder|empty-stdin|missing-transcript|conversation-empty>` line. Failure modes — `spawn-failed`, `tool-surface-missing`, `input-failed`, `write-empty`, `auto-archive reason=stale-recorder` — each emit one named line; absence is itself a defect.
145
145
 
146
146
  ## Session identifiers (Task 135)
@@ -0,0 +1,251 @@
1
+ #!/usr/bin/env bash
2
+ # Stop hook — post-turn graph pass. Task 412.
3
+ #
4
+ # Fires on every admin-agent assistant end_turn. Dispatches the
5
+ # `database-operator` specialist with the full conversation (as
6
+ # `[role: text]` pairs) plus the actual content of every node already
7
+ # written under this `conversationId`. The specialist decides what new
8
+ # graph rows the conversation supports and writes them.
9
+ #
10
+ # No throttle, no idle gate, no recursion-guard transcript grep. The two
11
+ # remaining early exits are doctrinal:
12
+ # - MAXY_SESSION_ROLE must equal "admin" → reason=role-not-admin
13
+ # - MAXY_SPECIALIST must be empty → reason=is-specialist
14
+ # (Same `MAXY_SPECIALIST` env-stamp model that blocks the hook from
15
+ # recursing inside a subagent's own stop. The specialist sees the
16
+ # operator's transcript via the dispatch prompt, not via its own PTY.)
17
+ #
18
+ # Duplicate writes between this background pass and the admin agent's
19
+ # discretionary inline `database-operator` dispatch are accepted at
20
+ # write time; cleanup is delegated to the dream-cycle / hygiene sweeps
21
+ # (Tasks 410, 411).
22
+ #
23
+ # Input: Claude Code's Stop hook stdin shape
24
+ # { "session_id": "<intrinsic>", "transcript_path": "<jsonl path>", ... }
25
+ #
26
+ # Output: exit 2 + the dispatch directive on stderr per Stop-hook
27
+ # contract. The admin IDENTITY.md "Stop-hook system directives" section
28
+ # binds the agent's response: perform the single Agent dispatch, emit a
29
+ # single-space ack, stop.
30
+
31
+ set -uo pipefail
32
+
33
+ UI_PORT="${MAXY_UI_INTERNAL_PORT:-}"
34
+ if [ -z "$UI_PORT" ]; then
35
+ echo "[post-turn-graph-pass] trigger-skipped sessionId=unknown reason=missing-env env=MAXY_UI_INTERNAL_PORT" >&2
36
+ exit 0
37
+ fi
38
+ UI_BASE="http://127.0.0.1:${UI_PORT}"
39
+ LOG_INGEST_URL="${UI_BASE}/api/admin/log-ingest"
40
+ CONTEXT_URL="${UI_BASE}/api/admin/post-turn-context"
41
+
42
+ emit_log() {
43
+ local line="$1"
44
+ curl -sS -o /dev/null -X POST \
45
+ -H 'Content-Type: application/json' \
46
+ --max-time 2 \
47
+ --data "$(python3 -c '
48
+ import sys, json
49
+ print(json.dumps({"tag": "post-turn-graph-pass", "level": "info", "line": sys.argv[1]}))
50
+ ' "$line")" \
51
+ "$LOG_INGEST_URL" 2>/dev/null || true
52
+ }
53
+
54
+ INPUT=""
55
+ if [ ! -t 0 ]; then
56
+ INPUT=$(cat)
57
+ fi
58
+
59
+ PARSED=$(printf '%s' "$INPUT" | python3 -c '
60
+ import sys, json
61
+ try:
62
+ d = json.load(sys.stdin)
63
+ sid = d.get("session_id", "") or ""
64
+ tpath = d.get("transcript_path", "") or ""
65
+ print(f"{sid}\t{tpath}")
66
+ except Exception:
67
+ print("\t")
68
+ ' 2>/dev/null)
69
+ SESSION_ID="${PARSED%% *}"
70
+ TRANSCRIPT_PATH="${PARSED#* }"
71
+ SESSION_ID_REPORTED="${SESSION_ID:-unknown}"
72
+
73
+ if [ "${MAXY_SESSION_ROLE:-}" != "admin" ]; then
74
+ emit_log "trigger-skipped sessionId=${SESSION_ID_REPORTED} reason=role-not-admin"
75
+ exit 0
76
+ fi
77
+ if [ -n "${MAXY_SPECIALIST:-}" ]; then
78
+ emit_log "trigger-skipped sessionId=${SESSION_ID_REPORTED} reason=is-specialist specialist=${MAXY_SPECIALIST}"
79
+ exit 0
80
+ fi
81
+ if [ -z "$INPUT" ]; then
82
+ emit_log "trigger-skipped sessionId=${SESSION_ID_REPORTED} reason=empty-stdin"
83
+ exit 0
84
+ fi
85
+ if [ -z "$SESSION_ID" ] || [ -z "$TRANSCRIPT_PATH" ] || [ ! -f "$TRANSCRIPT_PATH" ]; then
86
+ emit_log "trigger-skipped sessionId=${SESSION_ID_REPORTED} reason=missing-transcript"
87
+ exit 0
88
+ fi
89
+
90
+ CONVERSATION_ID="${CONVERSATION_NODE_ID:-}"
91
+
92
+ # Walk the transcript and emit the conversation as `[role: text]` pairs,
93
+ # oldest first. User turns whose only content is `tool_result` blocks are
94
+ # skipped (they carry the Stop-hook feedback envelope and other tool
95
+ # plumbing, not operator intent). Multi-block assistant turns concatenate
96
+ # their `text` blocks; `tool_use` blocks are dropped.
97
+ CONVERSATION=$(TRANSCRIPT_PATH="$TRANSCRIPT_PATH" python3 - <<'PY'
98
+ import os, json, sys
99
+
100
+ def assistant_text(rec):
101
+ msg = rec.get("message", {}) or {}
102
+ if msg.get("role") != "assistant":
103
+ return ""
104
+ content = msg.get("content")
105
+ if isinstance(content, str):
106
+ return content
107
+ if isinstance(content, list):
108
+ parts = []
109
+ for b in content:
110
+ if isinstance(b, dict) and b.get("type") == "text":
111
+ t = b.get("text")
112
+ if isinstance(t, str):
113
+ parts.append(t)
114
+ return "".join(parts)
115
+ return ""
116
+
117
+ def user_text(rec):
118
+ msg = rec.get("message", {}) or {}
119
+ if msg.get("role") != "user":
120
+ return None
121
+ content = msg.get("content")
122
+ if isinstance(content, str):
123
+ return content
124
+ if isinstance(content, list):
125
+ parts = []
126
+ saw_non_tool_result = False
127
+ for b in content:
128
+ if not isinstance(b, dict):
129
+ continue
130
+ btype = b.get("type")
131
+ if btype == "tool_result":
132
+ continue
133
+ saw_non_tool_result = True
134
+ if btype == "text":
135
+ t = b.get("text")
136
+ if isinstance(t, str):
137
+ parts.append(t)
138
+ if not saw_non_tool_result:
139
+ return None
140
+ return "".join(parts)
141
+ return None
142
+
143
+ path = os.environ["TRANSCRIPT_PATH"]
144
+ out_lines = []
145
+ try:
146
+ with open(path, "r", encoding="utf-8") as f:
147
+ for raw in f:
148
+ raw = raw.strip()
149
+ if not raw:
150
+ continue
151
+ try:
152
+ rec = json.loads(raw)
153
+ except Exception:
154
+ continue
155
+ if not isinstance(rec, dict):
156
+ continue
157
+ rtype = rec.get("type")
158
+ if rtype == "user":
159
+ t = user_text(rec)
160
+ if t is None:
161
+ continue
162
+ out_lines.append(f"[user: {t.strip()}]")
163
+ elif rtype == "assistant":
164
+ t = assistant_text(rec)
165
+ if not t.strip():
166
+ continue
167
+ out_lines.append(f"[assistant: {t.strip()}]")
168
+ except Exception as e:
169
+ sys.stderr.write(f"transcript-walk-failed: {e}\n")
170
+ sys.exit(0)
171
+
172
+ print("\n\n".join(out_lines))
173
+ PY
174
+ )
175
+ CONVERSATION_TURNS=$(printf '%s\n' "$CONVERSATION" | grep -c '^\[' || true)
176
+ CONTEXT_BYTES=${#CONVERSATION}
177
+
178
+ # Fetch the actual content of every node already written under this
179
+ # conversation, account-scoped. The route runs loopback-only — the hook
180
+ # is in-process on the device, same trust boundary as `/log-ingest`.
181
+ PRIOR_WRITES="(none)"
182
+ PRIOR_WRITES_COUNT=0
183
+ if [ -n "$CONVERSATION_ID" ]; then
184
+ CONTEXT_JSON=$(curl -sS --max-time 5 -G \
185
+ --data-urlencode "conversationId=${CONVERSATION_ID}" \
186
+ --data-urlencode "accountId=${ACCOUNT_ID:-}" \
187
+ "$CONTEXT_URL" 2>/dev/null || echo '')
188
+ if [ -n "$CONTEXT_JSON" ]; then
189
+ PARSED_CTX=$(printf '%s' "$CONTEXT_JSON" | python3 -c '
190
+ import sys, json
191
+ try:
192
+ d = json.load(sys.stdin)
193
+ except Exception:
194
+ print("0\t")
195
+ sys.exit(0)
196
+ writes = d.get("writes") or []
197
+ if not isinstance(writes, list):
198
+ print("0\t")
199
+ sys.exit(0)
200
+ lines = []
201
+ for w in writes:
202
+ if not isinstance(w, dict):
203
+ continue
204
+ eid = w.get("elementId") or ""
205
+ labels = w.get("labels") or []
206
+ props = w.get("properties") or {}
207
+ if isinstance(labels, list):
208
+ labels_str = ":".join(str(x) for x in labels)
209
+ else:
210
+ labels_str = str(labels)
211
+ try:
212
+ props_str = json.dumps(props, ensure_ascii=False, sort_keys=True)
213
+ except Exception:
214
+ props_str = "{}"
215
+ lines.append(f"- ({eid}) :{labels_str} {props_str}")
216
+ print(f"{len(writes)}\t" + "\n".join(lines))
217
+ ' 2>/dev/null)
218
+ PRIOR_WRITES_COUNT="${PARSED_CTX%% *}"
219
+ PRIOR_WRITES_BODY="${PARSED_CTX#* }"
220
+ if [ -n "$PRIOR_WRITES_BODY" ]; then
221
+ PRIOR_WRITES="$PRIOR_WRITES_BODY"
222
+ fi
223
+ fi
224
+ fi
225
+
226
+ emit_log "dispatch sessionId=${SESSION_ID} conversationId=${CONVERSATION_ID:-<unset>} conversationTurns=${CONVERSATION_TURNS} priorWritesCount=${PRIOR_WRITES_COUNT} contextBytes=${CONTEXT_BYTES} ms=0"
227
+
228
+ # Emit the dispatch directive on stderr per Stop-hook contract.
229
+ cat >&2 <<DIRECTIVE
230
+ [system: post-turn-graph-pass-stop-hook]
231
+ Dispatch the database-operator specialist NOW using the Task tool. Do not respond to the operator; this is a background graph-write pass and the operator does not see this turn. After the specialist returns its one-line summary, emit a single space character and stop.
232
+
233
+ Agent dispatch parameters:
234
+ subagent_type: database-operator
235
+ prompt: |
236
+ accountId: ${ACCOUNT_ID:-<unset>}
237
+ sessionId: ${SESSION_ID}
238
+ conversationId: ${CONVERSATION_ID:-<unset>}
239
+
240
+ Background post-turn graph pass. Given the conversation below and the list of nodes already written under this conversationId, derive any new graph rows the conversation supports (mentions, ideas, learnings, contacts, tasks — anything the schema admits) and write them. Skip writes the prior list already covers; rely on MERGE semantics for the rest. Return one line in the form: db-op: writes=<n> ms=<n>
241
+
242
+ <conversation>
243
+ ${CONVERSATION}
244
+ </conversation>
245
+
246
+ <prior-writes>
247
+ ${PRIOR_WRITES}
248
+ </prior-writes>
249
+ DIRECTIVE
250
+
251
+ exit 2
@@ -1,14 +1,16 @@
1
1
  ---
2
2
  name: datetime
3
3
  description: >
4
- Timezone queries: current time in any city or timezone, time conversion between
5
- zones, UTC offset, and DST status. Trigger phrases: "what time is it in",
6
- "convert time", "timezone", "time difference", "DST".
4
+ Timezone queries (current time, conversion, UTC offset, DST) and
5
+ relative-date arithmetic (last Tuesday, two weeks ago, Q3 last year).
6
+ Trigger phrases: "what time is it in", "convert time", "timezone",
7
+ "time difference", "DST", "last X", "N days/weeks/months ago",
8
+ "this/next/previous X", "Qn YYYY", "Qn last year".
7
9
  ---
8
10
 
9
11
  # Datetime & Timezone Queries
10
12
 
11
- Answer timezone-related questions using the server's `<datetime>` context and Node.js `Intl.DateTimeFormat` for cross-zone lookups.
13
+ Answer timezone-related questions and relative-date arithmetic using Node.js — never freelance the math. Relative dates and quarter forms must go through the deterministic one-liner in this skill; the same algorithm runs server-side in the memory plugin's `relative-date.ts`, so chat answers stay consistent with timeline rows the operator sees in the graph.
12
14
 
13
15
  ## When to Activate
14
16
 
@@ -16,6 +18,7 @@ Answer timezone-related questions using the server's `<datetime>` context and No
16
18
  - User asks to convert a time between timezones
17
19
  - User asks about UTC offset, time difference, or DST status for a location
18
20
  - User asks "what time is it?" with a location qualifier
21
+ - User uses a relative date: "last Tuesday", "two weeks ago", "yesterday", "this Christmas", "Q3 last year", "Q1 2025"
19
22
 
20
23
  For simple "what time is it?" without a location, answer directly from the `<datetime>` block in the system prompt — no tool use needed.
21
24
 
@@ -84,8 +87,63 @@ For less common cities, use Bash to search the IANA database:
84
87
  node -e "console.log(Intl.supportedValuesOf('timeZone').filter(z => z.toLowerCase().includes('SEARCH_TERM'.toLowerCase())).join('\n'))"
85
88
  ```
86
89
 
90
+ ## Relative date arithmetic
91
+
92
+ When the operator references a relative date — "last Tuesday", "two weeks ago", "yesterday", "this Christmas", "Q3 2024", "Q1 last year" — never compute the result by hand. The agent loses track of weekday offsets, leap-day arithmetic, and quarter-start months. Use this deterministic one-liner. The reference timestamp defaults to now; pass an ISO string as the second argument to anchor against an earlier moment (e.g. when the operator's message timestamp matters).
93
+
94
+ ```bash
95
+ node -e "
96
+ const REF = process.argv[2] ? new Date(process.argv[2]) : new Date();
97
+ const TXT = process.argv[1];
98
+ const WEEKDAYS = {sun:0,sunday:0,mon:1,monday:1,tue:2,tues:2,tuesday:2,wed:3,weds:3,wednesday:3,thu:4,thur:4,thurs:4,thursday:4,fri:5,friday:5,sat:6,saturday:6};
99
+ const NUMS = {one:1,two:2,three:3,four:4,five:5,six:6,seven:7,eight:8,nine:9,ten:10,eleven:11,twelve:12};
100
+ const sod = d => { const o=new Date(d); o.setUTCHours(0,0,0,0); return o; };
101
+ const addD = (d,n) => { const o=new Date(d); o.setUTCDate(o.getUTCDate()+n); return o; };
102
+ const addM = (d,n) => { const o=new Date(d); o.setUTCMonth(o.getUTCMonth()+n); return o; };
103
+ const addY = (d,n) => { const o=new Date(d); o.setUTCFullYear(o.getUTCFullYear()+n); return o; };
104
+ const base = sod(REF);
105
+ let m;
106
+ if (/\\bevery(\\s+other)?\\s+/i.test(TXT)) { console.log('null (recurring)'); process.exit(0); }
107
+ if (m = TXT.match(/\\b(yesterday|today|tomorrow)\\b/i)) {
108
+ const w = m[1].toLowerCase();
109
+ console.log((w==='yesterday'?addD(base,-1):w==='tomorrow'?addD(base,1):base).toISOString());
110
+ } else if (m = TXT.match(/\\b(last|next|this)\\s+([A-Za-z]+)\\b/i)) {
111
+ const q = m[1].toLowerCase(), t = WEEKDAYS[m[2].toLowerCase()];
112
+ if (t === undefined) { console.log('null'); process.exit(0); }
113
+ const dow = base.getUTCDay();
114
+ let delta;
115
+ if (q==='last') { delta = t-dow; if (delta>=0) delta-=7; }
116
+ else if (q==='next') { delta = t-dow; if (delta<=0) delta+=7; }
117
+ else { const id = dow===0?7:dow, it = t===0?7:t; delta = it-id; }
118
+ console.log(addD(base, delta).toISOString());
119
+ } else if (m = TXT.match(/\\b(a|an|\\d+|one|two|three|four|five|six|seven|eight|nine|ten|eleven|twelve)\\s+(day|week|month|year)s?\\s+ago\\b/i)) {
120
+ const w = m[1].toLowerCase();
121
+ const n = (w==='a'||w==='an')?1:(NUMS[w]??Number(w));
122
+ const u = m[2].toLowerCase();
123
+ const d = u==='day'?addD(base,-n):u==='week'?addD(base,-7*n):u==='month'?addM(base,-n):addY(base,-n);
124
+ console.log(d.toISOString());
125
+ } else if (TXT.match(/\\b(?:this\\s+)?Christmas\\b/i)) {
126
+ console.log(new Date(Date.UTC(base.getUTCFullYear(),11,25)).toISOString());
127
+ } else if (TXT.match(/\\b(?:this\\s+)?New\\s+Year(?:'s)?(?:\\s+Day)?\\b/i)) {
128
+ console.log(new Date(Date.UTC(base.getUTCFullYear(),0,1)).toISOString());
129
+ } else if (m = TXT.match(/\\bQ([1-4])\\s+(\\d{2}|\\d{4})\\b/i)) {
130
+ let y = Number(m[2]); if (y<100) y+=2000;
131
+ console.log(new Date(Date.UTC(y,(Number(m[1])-1)*3,1)).toISOString());
132
+ } else if (m = TXT.match(/\\bQ([1-4])\\s+(last|this|next)\\s+year\\b/i)) {
133
+ const refY = base.getUTCFullYear();
134
+ const y = m[2].toLowerCase()==='last'?refY-1:m[2].toLowerCase()==='next'?refY+1:refY;
135
+ console.log(new Date(Date.UTC(y,(Number(m[1])-1)*3,1)).toISOString());
136
+ } else { console.log('null'); }
137
+ " "PHRASE" "OPTIONAL_REF_ISO"
138
+ ```
139
+
140
+ Replace `PHRASE` with the relative phrase ("last Tuesday", "Q3 2024") and optionally `OPTIONAL_REF_ISO` with the ISO reference timestamp.
141
+
142
+ The algorithm mirrors `platform/plugins/memory/mcp/src/lib/relative-date.ts` — the server-side parser the timeline extractor uses. Recurring forms ("every other Friday") return `null` by design. Phrases that don't match return `null`.
143
+
87
144
  ## Boundaries
88
145
 
89
- - Answers timezone questions only — not scheduling, reminders, or alarms.
146
+ - Answers timezone questions and relative-date arithmetic only — not scheduling, reminders, or alarms.
90
147
  - Uses the server's system clock. If the server clock is wrong, answers will be wrong.
91
148
  - Historical timezone data (e.g., "what time was it in Tokyo on 15 March 1990?") depends on the Node.js ICU dataset and may be inaccurate for dates before 1970.
149
+ - Relative-date parser handles single-date phrases only; multi-date ranges ("from March through May") are out of scope.
@@ -297,6 +297,22 @@ This makes the failure visible to the agent, which can relay it to the user. Deg
297
297
 
298
298
  ---
299
299
 
300
+ ## Reports — durable workflow output (Task 332)
301
+
302
+ The `:Report` label is the platform's durable shape for workflow output the operator may want back later — daily briefings, dream cycle runs, ad-hoc analyses. Three MCP tools own the surface, all on the memory plugin:
303
+
304
+ - `memory-report-write` — append-only writer. Validates body ≤ 10,000 chars, embeds title+body, and CREATEs a `:Report` node. Idempotent on `(accountId, title, occurredAt-within-same-minute)` — a second call with the same title in the same minute returns the existing node instead of duplicating. Parented to the active `:Conversation` via `:PRODUCED` when `CONVERSATION_NODE_ID` is set (the chat-driven default); falls back to the account's `:AdminUser` so the graph-hierarchy doctrine holds even outside a conversation.
305
+ - `memory-report-read-latest` — fetches the newest `:Report` (default `limit=1`) tagged with a given keyword. The expected route for any operator phrasing of "latest X", "last night's X", "show me X report".
306
+ - `memory-report-list` — metadata-only paginated listing (newest first), with optional `keyword` and `sourceWorkflow` filters. Use to scan the catalogue without paying for full bodies.
307
+
308
+ Every operation emits one log line: `[reports] op=<write|read-latest|list> reportId=<short> keywords=<csv> ms=<n>` (with `idempotent=1` on a write that resolved to an existing node, `hits=<n>` on reads, `total=<n>` on list).
309
+
310
+ Routing is not classifier-side. The admin agent's `IDENTITY.md` carries the rule under **Recalling reports**: "latest <X>" / "last night's <X>" / "show me <X> report" → first tool call is `memory-report-read-latest`. The intent classifier (Task 304's `retrievalClass`) already differentiates temporal vs entity vs event reads; reports route off the literal phrase, not a new class.
311
+
312
+ The first caller is the `briefing` skill (`platform/plugins/scheduling/skills/briefing/SKILL.md`), which persists each run as a `:Report` with `title: "Daily briefing <YYYY-MM-DD>"`, `keywords: ["daily-briefing", "<YYYY-MM-DD>"]`, `sourceWorkflow: "daily-briefing"`. Dream-cycle (Task 327) and ad-hoc analyses are expected to follow the same pattern.
313
+
314
+ ---
315
+
300
316
  ## Graph Expansion
301
317
 
302
318
  After the top results are selected (by combined score or by LLM ranking), each result node is expanded by traversing its immediate relationships.
@@ -116,6 +116,16 @@ Every node also carries a provenance stamp — which agent wrote it, in which se
116
116
 
117
117
  **Two write surfaces, one substrate.** General agents write through schema-aware helpers — {{productName}} can record a new contact, a new commitment, a new preference without ever typing a database query, and the helper enforces the connection-and-provenance rule above structurally. The graph-steward role (the specialist {{productName}} dispatches when you ask for graph hygiene — "merge those two duplicate contacts," "wire those four tasks to the meeting," "rename the legacy label across the graph") additionally has a raw Cypher write tool for the multi-step operations the helpers cannot express. The steward role internalises the same connection-and-provenance discipline in its prompt; a post-write audit emits a warning on every breach so the same rules apply to both surfaces. Both paths feed the same hourly orphan trend and the same forensic provenance fields — read-side, you cannot tell the two apart, and that is the point.
118
118
 
119
+ ## Public-facing summaries for customer-readable subjects
120
+
121
+ Some entities in your graph are knowable by people outside your team — companies you work with, projects you've delivered, the business itself. For those entities ({{productName}} treats `:Organization`, `:Concept`, `:Project`, and `:LocalBusiness` this way), {{productName}} maintains two summaries: a private one only you and your specialist agents see, and a customer-facing public one your public agents are allowed to surface.
122
+
123
+ Whenever {{productName}} updates the private summary on one of these entities, it automatically rewrites the public summary in the same step using a separate prompt that strips operator-voice ("needs follow-up", "action: chase next week"), internal sentiment, and anything that reads like a note-to-self. The two summaries stay in lockstep without you doing anything.
124
+
125
+ If you want to write the public summary yourself — for instance, because the auto-generated version misses something you want customers to see — just tell {{productName}} the wording you want for the public summary on that entity, and {{productName}} will write it directly. It stays locked in for seven days; after that, the next automatic refresh can take over again, unless you re-pin it.
126
+
127
+ People entries (`:Person`) are deliberately excluded from this dual-summary system. Notes about contacts are private by definition and never get a public-facing form.
128
+
119
129
  ## Privacy
120
130
 
121
131
  All memory is stored on your local Raspberry Pi. The Neo4j database never leaves your network. {{productName}} does not sync memory to any cloud service or third party.
@@ -54,7 +54,7 @@ Roles are installed during setup and listed when {{productName}} introduces itse
54
54
 
55
55
  {{productName}} maintains a graph database (Neo4j) of everything you've told it. People, conversations, preferences, and context are stored as connected nodes. When you ask {{productName}} something, it searches this graph to retrieve relevant context before responding.
56
56
 
57
- **The recording loop.** A background recorder watches every completed admin-agent turn. Once your turn finishes and the reply is on screen, a Stop hook fires the `database-operator` specialist as a single-shot headless Claude Code session: the filtered transcript is written to the recorder's stdin and stdin is closed, so claude processes one turn, writes through the wrapped graph writers, and exits. The row archives the moment claude exits no long-lived hidden PTY. The admin agent itself does not write to the graph. The recorder runs on a cheap fast model so the recording adds nothing to your perceived latency, and its session never appears in the sidebar or anywhere on the operator UI. To disable it for an account, remove the `Stop` hook entry from that account's `~/.<brand>/.claude/settings.json`; there is no toggle.
57
+ **The recording loop.** Once your turn finishes and the reply is on screen, a Stop hook (`post-turn-graph-pass.sh`) fires the `database-operator` specialist as a background dispatch. The dispatch carries the full conversation as `[role: text]` pairs plus the list of every node already written under this conversation, fetched via `GET /api/admin/post-turn-context`. The specialist decides what new graph rows the conversation supports — mentions of named entities, ideas, learnings, contacts, tasks, anything the schema admitsand writes them. Duplicate writes between this background pass and any inline write the admin agent dispatched mid-turn are accepted and cleaned up by the dream-cycle / hygiene sweeps. There is no throttle and no idle gate: one dispatch per `end_turn`. The admin agent can also dispatch `database-operator` inline at its own discretion when a write must complete before the assistant response ends. To disable the background pass for an account, remove the `post-turn-graph-pass.sh` Stop-hook entry from that account's `~/.<brand>/.claude/settings.json`; there is no toggle.
58
58
 
59
59
  The memory graph is stored on your Pi. It never leaves your network.
60
60