@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.
- package/package.json +1 -1
- package/payload/platform/neo4j/schema.cypher +154 -4
- package/payload/platform/plugins/admin/PLUGIN.md +1 -2
- package/payload/platform/plugins/admin/hooks/__tests__/session-end-retrospective.test.sh +2 -2
- package/payload/platform/plugins/admin/hooks/post-turn-graph-pass.sh +251 -0
- package/payload/platform/plugins/admin/hooks/session-end-retrospective.sh +6 -3
- package/payload/platform/plugins/admin/skills/datetime/SKILL.md +63 -5
- package/payload/platform/plugins/docs/references/internals.md +16 -0
- package/payload/platform/plugins/docs/references/memory-guide.md +10 -0
- package/payload/platform/plugins/docs/references/platform.md +1 -1
- package/payload/platform/plugins/memory/.claude-plugin/plugin.json +1 -1
- package/payload/platform/plugins/memory/PLUGIN.md +25 -3
- package/payload/platform/plugins/memory/mcp/dist/index.js +309 -11
- package/payload/platform/plugins/memory/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/compiled-truth-revision.test.d.ts +2 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/compiled-truth-revision.test.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/compiled-truth-revision.test.js +33 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/compiled-truth-revision.test.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/graph-write-gate-label.test.d.ts +2 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/graph-write-gate-label.test.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/graph-write-gate-label.test.js +30 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/graph-write-gate-label.test.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/phase-prune-revisions.test.d.ts +2 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/phase-prune-revisions.test.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/phase-prune-revisions.test.js +51 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/phase-prune-revisions.test.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/relative-date.test.d.ts +2 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/relative-date.test.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/relative-date.test.js +68 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/relative-date.test.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/timeline-extractor.test.js +41 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/timeline-extractor.test.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/compiled-truth-revision.d.ts +43 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/compiled-truth-revision.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/compiled-truth-revision.js +33 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/compiled-truth-revision.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/compiled-truth-rewriter.d.ts +18 -5
- package/payload/platform/plugins/memory/mcp/dist/lib/compiled-truth-rewriter.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/compiled-truth-rewriter.js +64 -17
- package/payload/platform/plugins/memory/mcp/dist/lib/compiled-truth-rewriter.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/index.d.ts +44 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/index.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/index.js +14 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/index.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-citation-audit.d.ts +26 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-citation-audit.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-citation-audit.js +219 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-citation-audit.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-dead-edges.d.ts +19 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-dead-edges.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-dead-edges.js +39 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-dead-edges.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-orphans.d.ts +19 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-orphans.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-orphans.js +42 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-orphans.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-prune-revisions.d.ts +21 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-prune-revisions.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-prune-revisions.js +27 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-prune-revisions.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-stale-truth.d.ts +18 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-stale-truth.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-stale-truth.js +60 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-stale-truth.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/graph-write-gate.d.ts +20 -3
- package/payload/platform/plugins/memory/mcp/dist/lib/graph-write-gate.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/graph-write-gate.js +47 -12
- package/payload/platform/plugins/memory/mcp/dist/lib/graph-write-gate.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/relative-date.d.ts +13 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/relative-date.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/relative-date.js +191 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/relative-date.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/timeline-extractor.d.ts +7 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/timeline-extractor.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/timeline-extractor.js +37 -9
- package/payload/platform/plugins/memory/mcp/dist/lib/timeline-extractor.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-compiled-truth-history.test.d.ts +2 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-compiled-truth-history.test.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-compiled-truth-history.test.js +81 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-compiled-truth-history.test.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-report-tools.test.d.ts +2 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-report-tools.test.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-report-tools.test.js +115 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-report-tools.test.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-update-emit.test.js +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-update-emit.test.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-write-emit.test.js +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-write-emit.test.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-compiled-truth-history.d.ts +19 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-compiled-truth-history.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-compiled-truth-history.js +50 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-compiled-truth-history.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-dream-run.d.ts +45 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-dream-run.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-dream-run.js +170 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-dream-run.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-report-list.d.ts +28 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-report-list.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-report-list.js +64 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-report-list.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-report-read-latest.d.ts +24 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-report-read-latest.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-report-read-latest.js +49 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-report-read-latest.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-report-write.d.ts +34 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-report-write.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-report-write.js +99 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-report-write.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-review-queue.d.ts +29 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-review-queue.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-review-queue.js +66 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-review-queue.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-search.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-search.js +122 -2
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-search.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-update.d.ts +49 -13
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-update.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-update.js +184 -16
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-update.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.js +53 -10
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/vitest.config.ts +6 -0
- package/payload/platform/plugins/memory/references/schema-base.md +4 -0
- package/payload/platform/plugins/memory/skills/conversation-archive/SKILL.md +3 -4
- package/payload/platform/plugins/scheduling/skills/briefing/SKILL.md +13 -2
- package/payload/platform/scripts/seed-neo4j.sh +1 -1
- package/payload/platform/templates/agents/admin/IDENTITY.md +9 -1
- package/payload/platform/templates/specialists/agents/database-operator.md +30 -2
- package/payload/premium-plugins/real-agent/agents/listing-curator.md +39 -10
- package/payload/premium-plugins/real-agent/plugins/brochures/skills/property-brochure/SKILL.md +1 -0
- package/payload/premium-plugins/real-agent/plugins/brochures/skills/property-brochure/references/build.md +10 -1
- package/payload/premium-plugins/real-agent/plugins/brochures/skills/property-brochure/references/images.md +104 -0
- package/payload/server/server.js +195 -91
- package/payload/platform/plugins/admin/hooks/signal-detector-stop.sh +0 -309
- package/payload/platform/plugins/admin/skills/commitment-followthrough/SKILL.md +0 -60
- package/payload/platform/templates/specialists/agents/signal-detector.md +0 -129
package/package.json
CHANGED
|
@@ -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
|
|
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/
|
|
142
|
+
- `hooks/post-turn-graph-pass.sh` — **Stop hook.** Task 412 — supersedes 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:
|
|
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:
|
|
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 `
|
|
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 `
|
|
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 = "
|
|
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
|
|
5
|
-
|
|
6
|
-
"
|
|
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
|
|
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.
|