@rubytech/create-realagent-code 0.1.174 → 0.1.175

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/create-realagent-code",
3
- "version": "0.1.174",
3
+ "version": "0.1.175",
4
4
  "description": "Install Real Agent — Built for agents. By agents.",
5
5
  "bin": {
6
6
  "create-realagent-code": "./dist/index.js"
@@ -1279,13 +1279,17 @@ CREATE INDEX postal_address_postcode IF NOT EXISTS
1279
1279
  FOR (a:PostalAddress) ON (a.postalCode);
1280
1280
 
1281
1281
  // ----------------------------------------------------------
1282
- // Voice Mirror (Task 356) — authorial-voice capture + application
1282
+ // Voice Mirror (Task 356 + Task 462) — authorial-voice capture + application
1283
1283
  // ----------------------------------------------------------
1284
- // :VoiceProfile holds the distilled style card for one :AdminUser.
1285
- // One profile per AdminUser; the (accountId, adminUserId) pair is unique.
1284
+ // :VoiceProfile holds the distilled style card for one (AdminUser, format).
1285
+ // Task 462: one profile per (accountId, userId, format); format
1286
+ // {text, email, social-post, article, marketing-copy, note}.
1287
+ // The old (accountId, adminUserId) unique constraint is dropped and replaced.
1288
+ //
1286
1289
  // :VoiceEdit captures operator edits on agent drafts as feedback,
1287
1290
  // stamped with a Haiku-summarised intent; many edits feed back into
1288
- // the next distillation.
1291
+ // the next distillation. VoiceEdit now carries `format` to route
1292
+ // FEEDBACK_FOR edges to the matching per-format :VoiceProfile.
1289
1293
  //
1290
1294
  // `authorshipMode` is an enum property on content-bearing labels
1291
1295
  // (`:KnowledgeDocument`, `:ConversationArchive` (Task 397 — every chat
@@ -1293,30 +1297,62 @@ FOR (a:PostalAddress) ON (a.postalCode);
1293
1297
  // `:Message`, `:SocialPost`) with
1294
1298
  // values `human-only | human-led-agent-assisted | agent-led-human-reviewed
1295
1299
  // | agent-only | unknown`. Legacy nodes default to `unknown` until
1296
- // reviewed via the voice-mirror backfill flow. The index lets the
1297
- // distillation walk filter on `authorshipMode = 'human-only'` cheaply.
1300
+ // reviewed via the voice-mirror backfill flow. The per-label authorship
1301
+ // index lets the distillation walk filter cheaply.
1302
+ //
1303
+ // `format` is an enum property on corpus nodes (same label set) and on
1304
+ // :VoiceProfile nodes. The per-label format index lets per-format corpus
1305
+ // walks and profile reads avoid full-label scans.
1306
+
1307
+ // Task 462 — drop the old (accountId, adminUserId) constraint before
1308
+ // creating the new (accountId, userId, format) three-key constraint.
1309
+ // `IF EXISTS` makes the DROP idempotent on fresh installs.
1310
+ DROP CONSTRAINT voice_profile_admin_unique IF EXISTS;
1298
1311
 
1299
- CREATE CONSTRAINT voice_profile_admin_unique IF NOT EXISTS
1300
- FOR (v:VoiceProfile) REQUIRE (v.accountId, v.adminUserId) IS UNIQUE;
1312
+ CREATE CONSTRAINT voice_profile_format_unique IF NOT EXISTS
1313
+ FOR (v:VoiceProfile) REQUIRE (v.accountId, v.userId, v.format) IS UNIQUE;
1301
1314
 
1302
1315
  CREATE INDEX voice_profile_account IF NOT EXISTS
1303
1316
  FOR (v:VoiceProfile) ON (v.accountId);
1304
1317
 
1305
- CREATE INDEX voice_edit_admin IF NOT EXISTS
1306
- FOR (e:VoiceEdit) ON (e.adminUserId);
1318
+ CREATE INDEX voice_profile_format IF NOT EXISTS
1319
+ FOR (v:VoiceProfile) ON (v.format);
1320
+
1321
+ CREATE INDEX voice_edit_user IF NOT EXISTS
1322
+ FOR (e:VoiceEdit) ON (e.userId);
1307
1323
 
1308
1324
  CREATE INDEX knowledge_document_authorship IF NOT EXISTS
1309
1325
  FOR (k:KnowledgeDocument) ON (k.authorshipMode);
1310
1326
 
1327
+ CREATE INDEX knowledge_document_format IF NOT EXISTS
1328
+ FOR (k:KnowledgeDocument) ON (k.format);
1329
+
1311
1330
  CREATE INDEX conversation_archive_authorship IF NOT EXISTS
1312
1331
  FOR (c:ConversationArchive) ON (c.authorshipMode);
1313
1332
 
1333
+ CREATE INDEX conversation_archive_format IF NOT EXISTS
1334
+ FOR (c:ConversationArchive) ON (c.format);
1335
+
1314
1336
  CREATE INDEX message_authorship IF NOT EXISTS
1315
1337
  FOR (m:Message) ON (m.authorshipMode);
1316
1338
 
1339
+ CREATE INDEX message_format IF NOT EXISTS
1340
+ FOR (m:Message) ON (m.format);
1341
+
1342
+ // contentHash index — deduplication MERGE in voice-ingest-session-text uses
1343
+ // MERGE (n:Message {contentHash: $hash, accountId: $accountId}). Without this
1344
+ // index, each MERGE scans all :Message nodes for the account. The index on
1345
+ // contentHash alone lets neo4j narrow the lookup before applying the
1346
+ // accountId predicate.
1347
+ CREATE INDEX message_content_hash IF NOT EXISTS
1348
+ FOR (m:Message) ON (m.contentHash);
1349
+
1317
1350
  CREATE INDEX social_post_authorship IF NOT EXISTS
1318
1351
  FOR (s:SocialPost) ON (s.authorshipMode);
1319
1352
 
1353
+ CREATE INDEX social_post_format IF NOT EXISTS
1354
+ FOR (s:SocialPost) ON (s.format);
1355
+
1320
1356
  // :Section authorshipMode — the corpus walk includes conversation-archive
1321
1357
  // :Section chunks (parent :ConversationArchive) alongside ordinary
1322
1358
  // document sections under :KnowledgeDocument. Index on the :Section label
@@ -1325,6 +1361,9 @@ FOR (s:SocialPost) ON (s.authorshipMode);
1325
1361
  CREATE INDEX section_authorship IF NOT EXISTS
1326
1362
  FOR (s:Section) ON (s.authorshipMode);
1327
1363
 
1364
+ CREATE INDEX section_format IF NOT EXISTS
1365
+ FOR (s:Section) ON (s.format);
1366
+
1328
1367
  CREATE INDEX social_post_account IF NOT EXISTS
1329
1368
  FOR (s:SocialPost) ON (s.accountId);
1330
1369
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## What It Does
4
4
 
5
- {{productName}} reads emails, posts, and documents that you wrote yourself, and uses them to make sure agent-drafted copy reads like you wrote it — not like generic AI prose.
5
+ {{productName}} reads emails, posts, documents, and your own chat messages, and uses them to make sure agent-drafted copy reads like you wrote it — not like generic AI prose.
6
6
 
7
7
  Anthropic models cannot be fine-tuned. Voice mirror works by feeding the model two things alongside every drafting task:
8
8
 
@@ -11,32 +11,34 @@ Anthropic models cannot be fine-tuned. Voice mirror works by feeding the model t
11
11
 
12
12
  The model conditions on both and produces copy that reads as yours.
13
13
 
14
- ## Setup
14
+ Voice mirror maintains a separate profile for each type of content you write: plain text, email, social posts, articles, notes, and marketing copy. The right profile is applied automatically based on what you're drafting.
15
15
 
16
- ### 1. Tell {{productName}} what you wrote
16
+ ## How Your Voice Is Captured
17
17
 
18
- Voice mirror only learns from content you genuinely authored. Everything else stays unknown until you say otherwise.
18
+ ### Automatically from chat
19
+
20
+ Every admin chat session feeds your writing into the `text` profile automatically. When the session ends, {{productName}} reads the transcript, filters out slash commands, system messages, and large paste blocks, and adds each genuine turn as a corpus entry. This happens in the background with no action needed from you.
21
+
22
+ ### Via backfill — from historical content
23
+
24
+ Use the backfill flow to teach {{productName}} your writing from emails, documents, and social posts you've already written.
19
25
 
20
26
  In the admin chat, ask: **"Start the voice-mirror backfill."**
21
27
 
22
28
  {{productName}} asks which stream to backfill first — discrete documents (emails, posts, PDFs) or chat archives (WhatsApp, Telegram, etc.). **Pick chat archives first if you have any imported.** Chat is where your unguarded voice lives; email is the same voice in dress clothes.
23
29
 
24
30
  - **Chat archives** — tag a whole conversation in one click. {{productName}} doesn't ask about individual messages because chunks within a conversation almost always share the same voice. Options per conversation: yours, mixed (multiple authors on your side — rare), or not yours (e.g. a Slack channel where you only forwarded other people's messages).
25
- - **Documents and posts** — paginated 10 at a time. Tag the whole batch, a subset by number, or per-item if you want a precise label like `human-led-agent-assisted` (you wrote the content, {{productName}} polished).
31
+ - **Documents and posts** — paginated 10 at a time. {{productName}} shows each item with its detected format (email, article, note, social-post). Tag the whole batch, a subset by number, or per-item if you want a precise label like `human-led-agent-assisted` (you wrote the content, {{productName}} polished). Override the detected format if one is wrong.
26
32
 
27
33
  Skip a batch if none qualify, or stop at any time — the next session resumes where you left off.
28
34
 
29
- ### 2. Distil the profile
30
-
31
- Once you have at least ~20 `human-only` items tagged, ask: **"Build my voice profile."**
32
-
33
- {{productName}} reads the corpus, summarises your style as a YAML card, and saves it to the graph. It picks up your sentence rhythms, the constructions you reach for, the words you avoid.
35
+ ## Distilling Your Profile
34
36
 
35
- The profile re-runs automatically when your corpus grows by ≥20% or every 30 days, whichever comes first.
37
+ Once you have corpus entries tagged (or after automatic PTY ingestion fills the `text` profile), ask: **"Build my voice profile."**
36
38
 
37
- ### 3. Done
39
+ {{productName}} reads the corpus for each format, summarises your style as a YAML card, and saves it to the graph. It picks up your sentence rhythms, the constructions you reach for, the words you avoid — separately for email, articles, social posts, and so on.
38
40
 
39
- That's it. Every agent-drafted email, social post, brochure paragraph, and prospectus section from this point conditions on your voice. You don't need to ask for it.
41
+ The profile re-runs automatically when your corpus grows by ≥20% for any format or every 30 days, whichever comes first.
40
42
 
41
43
  ## Feedback Loop
42
44
 
@@ -50,11 +52,11 @@ Voice mirror is on by default for every drafting skill. To opt out for one, set
50
52
 
51
53
  ## What It Won't Do
52
54
 
53
- - **Blend voices** — one profile per person, no "Joel + Neo combined".
55
+ - **Blend voices** — one profile set per person, no "Joel + Neo combined".
54
56
  - **Copy public figures** — voice mirror only learns from your own writing.
55
57
  - **Clone audio** — text only, no speech synthesis.
56
- - **Guess** — content stays `unknown` until you mark it. {{productName}} never auto-classifies your historical writing.
58
+ - **Guess** — historical content stays `unknown` until you mark it. {{productName}} never auto-classifies your writing. (Automatic ingestion applies only to your live PTY sessions where authorship is certain.)
57
59
 
58
60
  ## Status
59
61
 
60
- Voice mirror shipped end-to-end. The schema, the four tools (`voice-tag-content`, `voice-distil-profile`, `voice-retrieve-conditioning`, `voice-record-feedback`), and wiring into the three live drafting surfaces (email composition, property brochures, investor data room) are all live. When no voice profile exists on the account, every drafting skill degrades gracefully — the output matches what it was before voice mirror was installed.
62
+ Voice mirror is live end-to-end. Six corpus formats (text, email, social-post, article, note, marketing-copy), five tools (`voice-tag-content`, `voice-distil-profile`, `voice-retrieve-conditioning`, `voice-record-feedback`, `voice-ingest-session-text`), automatic PTY-turn ingestion via SessionEnd hook, and wiring into the three live drafting surfaces (email composition, property brochures, investor data room) are all live. When no voice profile exists on the account for a given format, every drafting skill degrades gracefully — the output matches what it was before voice mirror was installed.
@@ -58,6 +58,7 @@ mkdir -p "$ACCOUNT_DIR/agents/admin" "$ACCOUNT_DIR/agents/public" "$ACCOUNT_DIR/
58
58
  # to locate scripts relative to the platform installation, not the account dir.
59
59
  ACCOUNT_SETTINGS="$ACCOUNT_DIR/.claude/settings.json"
60
60
  HOOKS_PATH="\$PLATFORM_ROOT/plugins/admin/hooks"
61
+ WRITER_CRAFT_HOOKS_PATH="\$PLATFORM_ROOT/../premium-plugins/writer-craft/hooks"
61
62
  cat > "$ACCOUNT_SETTINGS" << SETTINGS_EOF
62
63
  {
63
64
  "hooks": {
@@ -146,6 +147,13 @@ cat > "$ACCOUNT_SETTINGS" << SETTINGS_EOF
146
147
  { "type": "command", "command": "bash $HOOKS_PATH/session-end-retrospective.sh" }
147
148
  ]
148
149
  }
150
+ ],
151
+ "SessionEnd": [
152
+ {
153
+ "hooks": [
154
+ { "type": "command", "command": "bash $WRITER_CRAFT_HOOKS_PATH/voice-session-end-text.sh" }
155
+ ]
156
+ }
149
157
  ]
150
158
  }
151
159
  }
@@ -326,26 +326,45 @@ def main() -> int:
326
326
  )
327
327
  pairs_checked += 1
328
328
 
329
- # Built-in-tool rule + WebFetch rule: sweep all specialist templates directly.
330
- # These rules are orthogonal to the skill-pairing loop above — they check the
331
- # specialist's own prompt body for directives that require built-in CC tools.
329
+ # Built-in-tool rule + WebFetch rule: sweep all agent template .md files.
330
+ # Covers platform specialists (SPECIALISTS_DIR) and premium-plugin agent
331
+ # templates (premium-plugins/*/agents/). These rules are orthogonal to the
332
+ # skill-pairing loop above — they check each agent's own prompt body for
333
+ # directives that require built-in CC tools.
332
334
  known_skills = collect_skill_names()
333
335
  builtin_checked = 0
336
+
337
+ agent_template_dirs: list[str] = []
334
338
  if os.path.isdir(SPECIALISTS_DIR):
335
- for fname in sorted(os.listdir(SPECIALISTS_DIR)):
339
+ agent_template_dirs.append(SPECIALISTS_DIR)
340
+ pp_root = os.path.join(REPO_ROOT, "premium-plugins")
341
+ if os.path.isdir(pp_root):
342
+ for plugin in sorted(os.listdir(pp_root)):
343
+ adir = os.path.join(pp_root, plugin, "agents")
344
+ if os.path.isdir(adir):
345
+ agent_template_dirs.append(adir)
346
+
347
+ for adir in agent_template_dirs:
348
+ for fname in sorted(os.listdir(adir)):
336
349
  if not fname.endswith(".md"):
337
350
  continue
338
- specialist = fname[:-3]
339
- spath = os.path.join(SPECIALISTS_DIR, fname)
340
- tools = specialist_tools(specialist)
341
- if tools is None:
351
+ spath = os.path.join(adir, fname)
352
+ fm = parse_frontmatter(spath)
353
+ if fm is None:
342
354
  continue
355
+ specialist = fm.get("name") or fname[:-3]
356
+ raw = fm.get("tools", "")
357
+ if isinstance(raw, list):
358
+ tools = {t for t in raw if t}
359
+ else:
360
+ tools = {t.strip() for t in str(raw).split(",") if t.strip()}
343
361
  body = specialist_body(spath)
344
362
  for err in check_builtin_tool_rule(specialist, body, tools, known_skills):
345
363
  errors.append(err)
346
364
  for err in check_webfetch_rule(specialist, body, tools):
347
365
  errors.append(err)
348
366
  builtin_checked += 1
367
+
349
368
  summary.append(
350
369
  f"[verify] builtin-tool-check specialists_checked={builtin_checked}"
351
370
  )