@rubytech/create-realagent-code 0.1.144 → 0.1.147

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 (137) 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 -2
  4. package/payload/platform/plugins/admin/hooks/__tests__/session-end-retrospective.test.sh +2 -2
  5. package/payload/platform/plugins/admin/hooks/post-turn-graph-pass.sh +251 -0
  6. package/payload/platform/plugins/admin/hooks/session-end-retrospective.sh +6 -3
  7. package/payload/platform/plugins/admin/skills/datetime/SKILL.md +63 -5
  8. package/payload/platform/plugins/docs/references/internals.md +16 -0
  9. package/payload/platform/plugins/docs/references/memory-guide.md +10 -0
  10. package/payload/platform/plugins/docs/references/platform.md +1 -1
  11. package/payload/platform/plugins/memory/.claude-plugin/plugin.json +1 -1
  12. package/payload/platform/plugins/memory/PLUGIN.md +25 -3
  13. package/payload/platform/plugins/memory/mcp/dist/index.js +309 -11
  14. package/payload/platform/plugins/memory/mcp/dist/index.js.map +1 -1
  15. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/compiled-truth-revision.test.d.ts +2 -0
  16. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/compiled-truth-revision.test.d.ts.map +1 -0
  17. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/compiled-truth-revision.test.js +33 -0
  18. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/compiled-truth-revision.test.js.map +1 -0
  19. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/graph-write-gate-label.test.d.ts +2 -0
  20. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/graph-write-gate-label.test.d.ts.map +1 -0
  21. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/graph-write-gate-label.test.js +30 -0
  22. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/graph-write-gate-label.test.js.map +1 -0
  23. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/phase-prune-revisions.test.d.ts +2 -0
  24. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/phase-prune-revisions.test.d.ts.map +1 -0
  25. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/phase-prune-revisions.test.js +51 -0
  26. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/phase-prune-revisions.test.js.map +1 -0
  27. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/relative-date.test.d.ts +2 -0
  28. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/relative-date.test.d.ts.map +1 -0
  29. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/relative-date.test.js +68 -0
  30. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/relative-date.test.js.map +1 -0
  31. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/timeline-extractor.test.js +41 -0
  32. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/timeline-extractor.test.js.map +1 -1
  33. package/payload/platform/plugins/memory/mcp/dist/lib/compiled-truth-revision.d.ts +43 -0
  34. package/payload/platform/plugins/memory/mcp/dist/lib/compiled-truth-revision.d.ts.map +1 -0
  35. package/payload/platform/plugins/memory/mcp/dist/lib/compiled-truth-revision.js +33 -0
  36. package/payload/platform/plugins/memory/mcp/dist/lib/compiled-truth-revision.js.map +1 -0
  37. package/payload/platform/plugins/memory/mcp/dist/lib/compiled-truth-rewriter.d.ts +18 -5
  38. package/payload/platform/plugins/memory/mcp/dist/lib/compiled-truth-rewriter.d.ts.map +1 -1
  39. package/payload/platform/plugins/memory/mcp/dist/lib/compiled-truth-rewriter.js +64 -17
  40. package/payload/platform/plugins/memory/mcp/dist/lib/compiled-truth-rewriter.js.map +1 -1
  41. package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/index.d.ts +44 -0
  42. package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/index.d.ts.map +1 -0
  43. package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/index.js +14 -0
  44. package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/index.js.map +1 -0
  45. package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-citation-audit.d.ts +26 -0
  46. package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-citation-audit.d.ts.map +1 -0
  47. package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-citation-audit.js +219 -0
  48. package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-citation-audit.js.map +1 -0
  49. package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-dead-edges.d.ts +19 -0
  50. package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-dead-edges.d.ts.map +1 -0
  51. package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-dead-edges.js +39 -0
  52. package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-dead-edges.js.map +1 -0
  53. package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-orphans.d.ts +19 -0
  54. package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-orphans.d.ts.map +1 -0
  55. package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-orphans.js +42 -0
  56. package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-orphans.js.map +1 -0
  57. package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-prune-revisions.d.ts +21 -0
  58. package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-prune-revisions.d.ts.map +1 -0
  59. package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-prune-revisions.js +27 -0
  60. package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-prune-revisions.js.map +1 -0
  61. package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-stale-truth.d.ts +18 -0
  62. package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-stale-truth.d.ts.map +1 -0
  63. package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-stale-truth.js +60 -0
  64. package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-stale-truth.js.map +1 -0
  65. package/payload/platform/plugins/memory/mcp/dist/lib/graph-write-gate.d.ts +20 -3
  66. package/payload/platform/plugins/memory/mcp/dist/lib/graph-write-gate.d.ts.map +1 -1
  67. package/payload/platform/plugins/memory/mcp/dist/lib/graph-write-gate.js +47 -12
  68. package/payload/platform/plugins/memory/mcp/dist/lib/graph-write-gate.js.map +1 -1
  69. package/payload/platform/plugins/memory/mcp/dist/lib/relative-date.d.ts +13 -0
  70. package/payload/platform/plugins/memory/mcp/dist/lib/relative-date.d.ts.map +1 -0
  71. package/payload/platform/plugins/memory/mcp/dist/lib/relative-date.js +191 -0
  72. package/payload/platform/plugins/memory/mcp/dist/lib/relative-date.js.map +1 -0
  73. package/payload/platform/plugins/memory/mcp/dist/lib/timeline-extractor.d.ts +7 -1
  74. package/payload/platform/plugins/memory/mcp/dist/lib/timeline-extractor.d.ts.map +1 -1
  75. package/payload/platform/plugins/memory/mcp/dist/lib/timeline-extractor.js +37 -9
  76. package/payload/platform/plugins/memory/mcp/dist/lib/timeline-extractor.js.map +1 -1
  77. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-compiled-truth-history.test.d.ts +2 -0
  78. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-compiled-truth-history.test.d.ts.map +1 -0
  79. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-compiled-truth-history.test.js +81 -0
  80. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-compiled-truth-history.test.js.map +1 -0
  81. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-report-tools.test.d.ts +2 -0
  82. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-report-tools.test.d.ts.map +1 -0
  83. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-report-tools.test.js +115 -0
  84. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-report-tools.test.js.map +1 -0
  85. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-update-emit.test.js +1 -0
  86. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-update-emit.test.js.map +1 -1
  87. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-write-emit.test.js +1 -0
  88. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-write-emit.test.js.map +1 -1
  89. package/payload/platform/plugins/memory/mcp/dist/tools/memory-compiled-truth-history.d.ts +19 -0
  90. package/payload/platform/plugins/memory/mcp/dist/tools/memory-compiled-truth-history.d.ts.map +1 -0
  91. package/payload/platform/plugins/memory/mcp/dist/tools/memory-compiled-truth-history.js +50 -0
  92. package/payload/platform/plugins/memory/mcp/dist/tools/memory-compiled-truth-history.js.map +1 -0
  93. package/payload/platform/plugins/memory/mcp/dist/tools/memory-dream-run.d.ts +45 -0
  94. package/payload/platform/plugins/memory/mcp/dist/tools/memory-dream-run.d.ts.map +1 -0
  95. package/payload/platform/plugins/memory/mcp/dist/tools/memory-dream-run.js +170 -0
  96. package/payload/platform/plugins/memory/mcp/dist/tools/memory-dream-run.js.map +1 -0
  97. package/payload/platform/plugins/memory/mcp/dist/tools/memory-report-list.d.ts +28 -0
  98. package/payload/platform/plugins/memory/mcp/dist/tools/memory-report-list.d.ts.map +1 -0
  99. package/payload/platform/plugins/memory/mcp/dist/tools/memory-report-list.js +64 -0
  100. package/payload/platform/plugins/memory/mcp/dist/tools/memory-report-list.js.map +1 -0
  101. package/payload/platform/plugins/memory/mcp/dist/tools/memory-report-read-latest.d.ts +24 -0
  102. package/payload/platform/plugins/memory/mcp/dist/tools/memory-report-read-latest.d.ts.map +1 -0
  103. package/payload/platform/plugins/memory/mcp/dist/tools/memory-report-read-latest.js +49 -0
  104. package/payload/platform/plugins/memory/mcp/dist/tools/memory-report-read-latest.js.map +1 -0
  105. package/payload/platform/plugins/memory/mcp/dist/tools/memory-report-write.d.ts +34 -0
  106. package/payload/platform/plugins/memory/mcp/dist/tools/memory-report-write.d.ts.map +1 -0
  107. package/payload/platform/plugins/memory/mcp/dist/tools/memory-report-write.js +99 -0
  108. package/payload/platform/plugins/memory/mcp/dist/tools/memory-report-write.js.map +1 -0
  109. package/payload/platform/plugins/memory/mcp/dist/tools/memory-review-queue.d.ts +29 -0
  110. package/payload/platform/plugins/memory/mcp/dist/tools/memory-review-queue.d.ts.map +1 -0
  111. package/payload/platform/plugins/memory/mcp/dist/tools/memory-review-queue.js +66 -0
  112. package/payload/platform/plugins/memory/mcp/dist/tools/memory-review-queue.js.map +1 -0
  113. package/payload/platform/plugins/memory/mcp/dist/tools/memory-search.d.ts.map +1 -1
  114. package/payload/platform/plugins/memory/mcp/dist/tools/memory-search.js +122 -2
  115. package/payload/platform/plugins/memory/mcp/dist/tools/memory-search.js.map +1 -1
  116. package/payload/platform/plugins/memory/mcp/dist/tools/memory-update.d.ts +49 -13
  117. package/payload/platform/plugins/memory/mcp/dist/tools/memory-update.d.ts.map +1 -1
  118. package/payload/platform/plugins/memory/mcp/dist/tools/memory-update.js +184 -16
  119. package/payload/platform/plugins/memory/mcp/dist/tools/memory-update.js.map +1 -1
  120. package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.d.ts.map +1 -1
  121. package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.js +53 -10
  122. package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.js.map +1 -1
  123. package/payload/platform/plugins/memory/mcp/vitest.config.ts +6 -0
  124. package/payload/platform/plugins/memory/references/schema-base.md +4 -0
  125. package/payload/platform/plugins/memory/skills/conversation-archive/SKILL.md +3 -4
  126. package/payload/platform/plugins/scheduling/skills/briefing/SKILL.md +13 -2
  127. package/payload/platform/scripts/seed-neo4j.sh +1 -1
  128. package/payload/platform/templates/agents/admin/IDENTITY.md +9 -1
  129. package/payload/platform/templates/specialists/agents/database-operator.md +30 -2
  130. package/payload/premium-plugins/real-agent/agents/listing-curator.md +39 -10
  131. package/payload/premium-plugins/real-agent/plugins/brochures/skills/property-brochure/SKILL.md +1 -0
  132. package/payload/premium-plugins/real-agent/plugins/brochures/skills/property-brochure/references/build.md +10 -1
  133. package/payload/premium-plugins/real-agent/plugins/brochures/skills/property-brochure/references/images.md +104 -0
  134. package/payload/server/server.js +195 -91
  135. package/payload/platform/plugins/admin/hooks/signal-detector-stop.sh +0 -309
  136. package/payload/platform/plugins/admin/skills/commitment-followthrough/SKILL.md +0 -60
  137. 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-realagent-code",
3
- "version": "0.1.144",
3
+ "version": "0.1.147",
4
4
  "description": "Install Real Agent — Built for agents. By agents.",
5
5
  "bin": {
6
6
  "create-realagent-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);
@@ -129,7 +129,6 @@ Tools are available via the `admin` MCP server.
129
129
  | Plain-English explanation | User asks to explain, define, or pre-empts ("explain in plain English", "what does X mean", "define X", "I don't understand", "what is this"), or admin is about to return a reply containing a term not in the operator's prior turn | `skills/plainly/SKILL.md` |
130
130
  | Manage admin users | User asks to add, remove, or list admins on this account, or change an admin PIN | `skills/admin-user-management/SKILL.md` |
131
131
  | Session reset / continue / resume | User asks to clear the session, start fresh, continue the last session, or pick up where they left off | `skills/session-management/SKILL.md` |
132
- | Commitment follow-through | A `<commitment-detected>` block appears in the prompt; the owner has just made a commitment that needs a backing mechanism | `skills/commitment-followthrough/SKILL.md` |
133
132
  | Show or download a file | User asks to view, attach, or download a file or document, or the current turn produces a document the owner needs to review | `skills/file-presentation/SKILL.md` |
134
133
  | Upgrade Maxy | User asks to upgrade, update, install the latest version, or accepts an upgrade offer | `skills/upgrade/SKILL.md` |
135
134
 
@@ -140,7 +139,7 @@ Tools are available via the `admin` MCP server.
140
139
  - `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
140
  - `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
141
  - `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).
142
+ - `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
143
  - `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
144
 
146
145
  ## Session identifiers (Task 135)
@@ -294,7 +294,7 @@ write_transcript "$T_RELEASE" \
294
294
  "assistant|hello|" \
295
295
  "user|/end|" \
296
296
  "assistant|running retrospective|" \
297
- "assistant||tool_use:session-retrospective-mark-complete"
297
+ "assistant||tool_use:mcp__admin__session-retrospective-mark-complete"
298
298
  ENV_REL=$(envelope_for "$OP_ID" "$T_RELEASE")
299
299
  : > "$REQ_LOG"
300
300
  run_hook "admin" "" "$ENV_REL"
@@ -325,7 +325,7 @@ fi
325
325
  T_OTHER=$(mktemp); TMPFILES+=("$T_OTHER")
326
326
  write_transcript "$T_OTHER" \
327
327
  "user|/end|" \
328
- "assistant||tool_use:session-retrospective-mark-complete"
328
+ "assistant||tool_use:mcp__admin__session-retrospective-mark-complete"
329
329
  # Active gate inspects only T_ACTIVE (no sentinel inside).
330
330
  T_ACTIVE=$(mktemp); TMPFILES+=("$T_ACTIVE")
331
331
  write_transcript "$T_ACTIVE" "user|/end|"
@@ -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
@@ -20,7 +20,10 @@
20
20
  #
21
21
  # Sentinel detection. Completion is recognised by greping the operator's
22
22
  # own JSONL (transcript_path on the Stop envelope) for a `tool_use` block
23
- # whose `name` is exactly `session-retrospective-mark-complete`. The
23
+ # whose `name` is exactly `mcp__admin__session-retrospective-mark-complete`
24
+ # — that is the prefixed form Claude Code writes for MCP tools in the
25
+ # JSONL; the unprefixed name only appears in the operator-facing
26
+ # instruction text below. The
24
27
  # sentinel-grep doubles as the re-entry guard: every Stop that fires after
25
28
  # the agent's sentinel call sees the sentinel and exits 0 with
26
29
  # `gate-released`, so the agent's wrap-up turn never re-blocks.
@@ -128,12 +131,12 @@ fi
128
131
  # 1. The latest end-intent token in the most recent real-user message,
129
132
  # or empty if no real user message matches.
130
133
  # 2. Whether any `tool_use` block in the transcript names the sentinel
131
- # tool `session-retrospective-mark-complete`.
134
+ # tool `mcp__admin__session-retrospective-mark-complete`.
132
135
  # Output one line: <intent-token-or-empty>\t<sentinel-present-yes|no>
133
136
  INSPECTION=$(TRANSCRIPT_PATH="$TRANSCRIPT_PATH" python3 - <<'PY'
134
137
  import os, json
135
138
 
136
- SENTINEL_TOOL = "session-retrospective-mark-complete"
139
+ SENTINEL_TOOL = "mcp__admin__session-retrospective-mark-complete"
137
140
  INTENT_TOKENS = ("/end", "/archive", "end session", "archive this session")
138
141
 
139
142
  def is_real_user(rec):
@@ -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.