@karmaniverous/jeeves 0.5.8 → 0.5.10

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/dist/index.js CHANGED
@@ -10,6 +10,7 @@ import cp, { execSync } from 'node:child_process';
10
10
  import { homedir } from 'node:os';
11
11
  import { fileURLToPath } from 'node:url';
12
12
  import { packageDirectorySync } from 'package-directory';
13
+ import Handlebars from 'handlebars';
13
14
 
14
15
  /**
15
16
  * Comment markers for managed content blocks.
@@ -183,14 +184,14 @@ const PLATFORM_COMPONENTS = [
183
184
  * Core library version, inlined at build time.
184
185
  *
185
186
  * @remarks
186
- * The `0.5.7` placeholder is replaced by
187
+ * The `0.5.9` placeholder is replaced by
187
188
  * `@rollup/plugin-replace` during the build with the actual version
188
189
  * from `package.json`. This ensures the correct version survives
189
190
  * when consumers bundle core into their own dist (where runtime
190
191
  * `import.meta.url`-based resolution would find the wrong package.json).
191
192
  */
192
193
  /** The core library version from package.json (inlined at build time). */
193
- const CORE_VERSION = '0.5.7';
194
+ const CORE_VERSION = '0.5.9';
194
195
 
195
196
  /**
196
197
  * Workspace and config root initialization.
@@ -724,6 +725,11 @@ const workspaceCoreConfigSchema = z
724
725
  configRoot: z.string().optional().describe('Platform config root path'),
725
726
  /** OpenClaw gateway URL. */
726
727
  gatewayUrl: z.string().optional().describe('OpenClaw gateway URL'),
728
+ /** Dev repo paths keyed by component name. */
729
+ devRepos: z
730
+ .record(z.string(), z.string())
731
+ .optional()
732
+ .describe('Dev repo paths by component name'),
727
733
  })
728
734
  .partial();
729
735
  /** Memory shared config section. */
@@ -738,13 +744,6 @@ const workspaceMemoryConfigSchema = z
738
744
  .max(1)
739
745
  .optional()
740
746
  .describe('Memory warning threshold'),
741
- /** Staleness threshold in days. */
742
- staleDays: z
743
- .number()
744
- .int()
745
- .positive()
746
- .optional()
747
- .describe('Memory staleness threshold in days'),
748
747
  })
749
748
  .partial();
750
749
  /** Workspace config Zod schema. */
@@ -772,7 +771,6 @@ const WORKSPACE_CONFIG_DEFAULTS = {
772
771
  memory: {
773
772
  budget: 20_000,
774
773
  warningThreshold: 0.8,
775
- staleDays: 30,
776
774
  },
777
775
  };
778
776
  /**
@@ -840,6 +838,11 @@ function generateWorkspaceJsonSchema() {
840
838
  type: 'string',
841
839
  default: WORKSPACE_CONFIG_DEFAULTS.core.gatewayUrl,
842
840
  },
841
+ devRepos: {
842
+ type: 'object',
843
+ additionalProperties: { type: 'string' },
844
+ description: 'Dev repo paths by component name (e.g. { "core": "D:\\\\repos\\\\jeeves" })',
845
+ },
843
846
  },
844
847
  },
845
848
  memory: {
@@ -856,11 +859,6 @@ function generateWorkspaceJsonSchema() {
856
859
  maximum: 1,
857
860
  default: WORKSPACE_CONFIG_DEFAULTS.memory.warningThreshold,
858
861
  },
859
- staleDays: {
860
- type: 'integer',
861
- minimum: 1,
862
- default: WORKSPACE_CONFIG_DEFAULTS.memory.staleDays,
863
- },
864
862
  },
865
863
  },
866
864
  },
@@ -904,7 +902,6 @@ function resolveCliConfig(opts) {
904
902
  memory: {
905
903
  budget: resolveConfigValue(undefined, readNumericEnv('JEEVES_MEMORY_BUDGET'), fileConfig?.memory?.budget, WORKSPACE_CONFIG_DEFAULTS.memory.budget),
906
904
  warningThreshold: resolveConfigValue(undefined, readNumericEnv('JEEVES_MEMORY_WARNING_THRESHOLD'), fileConfig?.memory?.warningThreshold, WORKSPACE_CONFIG_DEFAULTS.memory.warningThreshold),
907
- staleDays: resolveConfigValue(undefined, readNumericEnv('JEEVES_MEMORY_STALE_DAYS'), fileConfig?.memory?.staleDays, WORKSPACE_CONFIG_DEFAULTS.memory.staleDays),
908
905
  },
909
906
  };
910
907
  }
@@ -982,31 +979,31 @@ var hasRequiredExtraTypings;
982
979
  function requireExtraTypings () {
983
980
  if (hasRequiredExtraTypings) return extraTypings.exports;
984
981
  hasRequiredExtraTypings = 1;
985
- (function (module, exports$1) {
982
+ (function (module, exports) {
986
983
  const commander = require$$0;
987
984
 
988
- exports$1 = module.exports = {};
985
+ exports = module.exports = {};
989
986
 
990
987
  // Return a different global program than commander,
991
988
  // and don't also return it as default export.
992
- exports$1.program = new commander.Command();
989
+ exports.program = new commander.Command();
993
990
 
994
991
  /**
995
992
  * Expose classes. The FooT versions are just types, so return Commander original implementations!
996
993
  */
997
994
 
998
- exports$1.Argument = commander.Argument;
999
- exports$1.Command = commander.Command;
1000
- exports$1.CommanderError = commander.CommanderError;
1001
- exports$1.Help = commander.Help;
1002
- exports$1.InvalidArgumentError = commander.InvalidArgumentError;
1003
- exports$1.InvalidOptionArgumentError = commander.InvalidArgumentError; // Deprecated
1004
- exports$1.Option = commander.Option;
995
+ exports.Argument = commander.Argument;
996
+ exports.Command = commander.Command;
997
+ exports.CommanderError = commander.CommanderError;
998
+ exports.Help = commander.Help;
999
+ exports.InvalidArgumentError = commander.InvalidArgumentError;
1000
+ exports.InvalidOptionArgumentError = commander.InvalidArgumentError; // Deprecated
1001
+ exports.Option = commander.Option;
1005
1002
 
1006
- exports$1.createCommand = (name) => new commander.Command(name);
1007
- exports$1.createOption = (flags, description) =>
1003
+ exports.createCommand = (name) => new commander.Command(name);
1004
+ exports.createOption = (flags, description) =>
1008
1005
  new commander.Option(flags, description);
1009
- exports$1.createArgument = (name, description) =>
1006
+ exports.createArgument = (name, description) =>
1010
1007
  new commander.Argument(name, description);
1011
1008
  } (extraTypings, extraTypings.exports));
1012
1009
  return extraTypings.exports;
@@ -1510,7 +1507,158 @@ function buildWithSections(beforeContent, userContent, sections, markers, coreVe
1510
1507
  return parts.join('\n');
1511
1508
  }
1512
1509
 
1513
- var skillContent = `---
1510
+ var codingContent = `---
1511
+ name: coding
1512
+ description: Engineering standards for all code work. Use when writing code, reviewing PRs, spawning coding sub-agents, or making architectural decisions in any project (not just Jeeves). Covers design-first development, schema-first patterns, testing, STAN workflow, dependency management, and pre-PR checklist.
1513
+ ---
1514
+
1515
+ # Engineering Standards
1516
+
1517
+ These standards apply to ALL code work — whether done directly or via sub-agents.
1518
+ When spawning sub-agents for coding tasks, include the relevant rules in the task prompt.
1519
+ Sub-agents don't inherit your context — if you don't pass the rules, they don't exist.
1520
+
1521
+ ---
1522
+
1523
+ ## Design-First Development
1524
+
1525
+ 1. **Iterate on design until convergence** — Summarize requirements, propose approach, raise questions BEFORE writing code.
1526
+ 2. **Services-first architecture** — Core logic in services behind ports; adapters thin; side effects at boundaries.
1527
+ 3. **Schema-first** — Runtime schema (Zod) is source of truth; TypeScript types derived via \`z.infer<>\`; validation centralized. Plain TypeScript \`interface\` declarations for config surfaces are not acceptable.
1528
+ 4. **300 LOC hard limit** — If a file would exceed 300 lines, stop and decompose first. No exceptions.
1529
+ 5. **Avoid \`any\`** — Prefer \`unknown\` + narrowing; if unavoidable, narrowest scope + rationale.
1530
+ 6. **Test pairing** — Every non-trivial module gets a \`*.test.ts\`.
1531
+ 7. **Open-source first** — Prefer established deps over home-grown solutions. Search npm/GitHub before building anything non-trivial.
1532
+
1533
+ ## Module Design
1534
+
1535
+ - **Single Responsibility** applies to modules as well as functions.
1536
+ - Prefer many small modules over a few large ones.
1537
+ - Keep module boundaries explicit and cohesive; avoid "kitchen-sink" files.
1538
+ - Co-locate tests with modules for discoverability.
1539
+
1540
+ ## Config Surfaces
1541
+
1542
+ - Define config with **Zod schemas** — never bare TypeScript interfaces.
1543
+ - Derive types: \`type MyConfig = z.infer<typeof myConfigSchema>\`
1544
+ - Generate **JSON Schema** from Zod for IDE DX (\`\$schema\` pointer in config files).
1545
+ - Validate at load time — fail fast with clear error messages.
1546
+ - \`init\` commands generate config with \`\$schema\` pointer already in place.
1547
+
1548
+ ## Testing
1549
+
1550
+ - **Unit tests** for pure services (no fs/process/network).
1551
+ - **Integration tests** for adapters/seams (minimal end-to-end slices).
1552
+ - Exercise happy paths AND representative error paths.
1553
+ - Table-driven cases encouraged for exhaustive coverage.
1554
+ - Keep coverage meaningful — prefer covering branches/decisions over chasing 100% lines.
1555
+
1556
+ ## STAN-Enabled Repos
1557
+
1558
+ When working in a repo with \`.stan/\`:
1559
+ - Run \`stan run --sequential --no-archive\` **before** each commit. Scripts must pass before you commit. Sequential runs are preferred to limit side effects. Archives are not needed (you won't use them).
1560
+ - **Push after every commit.** Don't accumulate unpushed local commits. Jason needs to be able to see your work at any time.
1561
+ - All scripts must pass before claiming work is complete.
1562
+ - Read \`.stan/output/<script>.txt\` for evidence on failures.
1563
+ - When creating stan scripts, eliminate colorized output where possible (e.g. \`--no-color\`, \`NO_COLOR=1\`) to reduce noise in script output files.
1564
+
1565
+ ## Cross-Package Verification
1566
+
1567
+ When changes affect exports consumed by another repo:
1568
+ - Standalone scripts passing ≠ "ready for review."
1569
+ - Use \`npm link\` or equivalent to verify the consumer builds against your changes.
1570
+ - Only claim completion when BOTH repos pass.
1571
+
1572
+ ## Dependencies: Latest Versions Required (HARD GATE)
1573
+
1574
+ **NEVER use a superseded version of ANY dependency without direct human authorization.** When adding a new dependency — or creating a new project — ALWAYS check the latest stable version and use it. This applies to runtime deps, dev deps, and peer deps alike.
1575
+
1576
+ - Before \`npm install <package>\`: run \`npm view <package> version\` (or check npmjs.com) to confirm you're installing the current major.
1577
+ - Before spawning sub-agents that install packages: include the latest version in the task prompt, or instruct the sub-agent to verify latest before installing.
1578
+ - If the latest major has known breaking issues that block adoption, flag it to the human — don't silently pin an old major.
1579
+
1580
+ LLMs are trained on stale data. Your training cutoff means you will default to old versions of everything. **Assume your version knowledge is wrong** and verify before every install.
1581
+
1582
+ *Earned: 2026-05-12, created the jeeves-tools repo with Zod 3 despite Zod 4 being available since mid-2025. Shipped 84 commits on the old major before catching it.*
1583
+
1584
+ ## Dependencies: Local Over Global
1585
+
1586
+ - **Dev dependencies belong in the project, not the global environment.** Install with \`npm install --save-dev\`, not \`npm install -g\`.
1587
+ - This guarantees reproducibility across machines and CI. Global installs mask environment differences that cause "works on my machine" failures.
1588
+ - **Rare exceptions:** Tools that are genuinely machine-level utilities (e.g. \`stan-cli\`). If in doubt, install locally.
1589
+ ## Dependency Failures
1590
+
1591
+ When a third-party dependency is broken:
1592
+ 1. Summarize the failure concisely.
1593
+ 2. Enumerate options: switch dependency → fix upstream → temporary pin → shim (last resort).
1594
+ 3. Recommend with rationale.
1595
+ 4. Do NOT immediately code around the problem.
1596
+
1597
+ ## CHANGELOG
1598
+
1599
+ - **Do not manually update CHANGELOG.md** — it is generated as part of the release process (e.g. via \`standard-version\`, \`changesets\`, or equivalent). Conventional commit messages are the input; the tooling produces the output.
1600
+
1601
+ ## Pre-PR Checklist (HARD GATE)
1602
+
1603
+ **Before creating ANY PR, run the full verification sequence. No exceptions.**
1604
+
1605
+ 1. \`stan run --sequential --no-archive\` if \`.stan/\` exists — this is the canonical check suite
1606
+ 2. In monorepos: run checks **from each package directory**, not just the root. Root-level runs may mask package-level failures due to config resolution differences.
1607
+ 3. Exercise the release path: check \`release-it\` hooks (or equivalent) in each releasable package — run the same commands (\`lint\`, \`typecheck\`, \`test\`, \`build\`) from the same cwd the release process uses.
1608
+
1609
+ If any step fails, fix it before committing. Do NOT create the PR and "note" the failures. Do NOT claim pre-existing failures without having actually run the commands first. Skipping this sequence is how we ship broken code and fabricate diagnoses.
1610
+
1611
+ - **Compare against canonical template** (\`karmaniverous/npm-package-template-ts\`) before any npm package PR. If the project is behind the template, update it to conform. If the template is behind the project, raise the issue with Jason for template upkeep.
1612
+ - **Run \`ncu --peer\`** before any PR. Review the output. Update safe patches/minors. **Flag major version bumps for discussion** — never auto-apply \`ncu -u\` without reading what changed. Peer dep conflicts must be resolved, not ignored.
1613
+ - When spawning sub-agents, include \`ncu --peer\` in the quality gate commands: \`ncu --peer && npm run lint && npm run typecheck && npm run build && npm test\`. The sub-agent should report \`ncu\` output and only apply updates that don't involve major bumps or peer conflicts.
1614
+ - **Resolve ALL script warnings.** It is NOT acceptable to release code with outstanding warnings. They exist for a reason — fix them.
1615
+ - **Typecheck/lint rules apply to ALL authored code**, including configs at project root (\`eslint.config.ts\`, \`rollup.config.ts\`, \`vitest.config.ts\`, etc.). Only generated code (e.g. typedoc output) should be excepted from code quality checks.
1616
+ - **Never disable lint/typecheck rules** without surfacing it for discussion first. Disabled rules are a major code smell. If a rule must be disabled, document the rationale inline at the point of suppression.
1617
+ - **Multiple tsconfigs are a code smell.** Sometimes needed, but often they paper over poor configuration choices. Fix root causes rather than adding tsconfig variants.
1618
+ - **In a TS repo, all scripts should be authored in TS** (not JS). Prefer execution with \`tsx\`.
1619
+ - **Clean-room verify before claiming "all green."** Run \`rimraf node_modules && npm install && npm run build\` (or equivalent) to catch issues masked by cached state. If someone reports an error you can't reproduce, assume your cache is lying — not that they're wrong.
1620
+ - Verify build, test, and lint pass after updates.
1621
+
1622
+ ## Dev Workspace
1623
+
1624
+ - **Clone location:** \`D:\\repos\\{org-or-userid}\\{repo}\` (e.g. \`D:\\repos\\karmaniverous\\jeeves-watcher\`)
1625
+ - D drive is the dev workspace. Do not clone repos elsewhere.
1626
+ - Fresh clones are preferred over copying existing checkouts — \`npm install\` from registry is faster than disk-copying \`node_modules\`.
1627
+ - D drive is NOT indexed by jeeves-watcher. Dev work stays off the archive.
1628
+
1629
+ ## GitHub Auth (HARD GATE)
1630
+
1631
+ - **ALL GitHub operations use \`jgs-jeeves\` auth.** Set \`GH_TOKEN\` before any \`gh\` CLI command:
1632
+ \`\`\`powershell
1633
+ \$env:GH_TOKEN = (Get-Content "J:\\config\\credentials\\github\\jgs-jeeves.token" -Raw).Trim()
1634
+ \`\`\`
1635
+ - Never write to GitHub as \`karmaniverous\` — that's Jason's account.
1636
+ - If \`jgs-jeeves\` lacks permissions, **escalate to Jason** rather than falling back to \`karmaniverous\`.
1637
+
1638
+ ## Issue Hygiene
1639
+
1640
+ - **Always comment rationale when closing an issue without resolution** (duplicate, won't-fix, obsolete). The close action alone doesn't explain why.
1641
+ - Reference the replacement issue/PR when closing as duplicate.
1642
+
1643
+ ## Code Style
1644
+
1645
+ - Prettier is source of truth for formatting.
1646
+ - Keep imports sorted per repo tooling.
1647
+ - Avoid dead code.
1648
+ - TSDoc \`@module\` or \`@packageDocumentation\` on every non-test module.
1649
+ - First 160 chars of module doc should be high-signal: what it does, IO/side effects, traversal hints.
1650
+
1651
+ ## eslint-disable is a HARD GATE
1652
+
1653
+ Never disable lint/typecheck rules without surfacing it for discussion first. Fix the code, don't suppress the warning. For test mocks, use properly typed partial objects (\`Partial<RealType>\`, typed \`MockReply\` interfaces) instead of \`any\`. Tests are code.
1654
+
1655
+ ## Git Merge Policy
1656
+
1657
+ - **No squash merges.** Preserve commit history.
1658
+ - **PR reviewer:** When creating a PR under \`jgs-jeeves\` auth, always add \`karmaniverous\` (Jason) as a reviewer.
1659
+ `;
1660
+
1661
+ var jeevesContent = `---
1514
1662
  name: jeeves
1515
1663
  description: Jeeves platform architecture, data flow, component interaction, scripts repo, and coordination knowledge. Use when making architectural decisions, coordinating across components, checking platform health, managing service lifecycle, or working with the scripts repo.
1516
1664
  ---
@@ -1574,7 +1722,7 @@ Managed blocks are stationary after initial insertion. Cleanup detection uses Ja
1574
1722
 
1575
1723
  \`jeeves.config.json\` at workspace root provides shared defaults:
1576
1724
  - Precedence: CLI flags → env vars → file → defaults
1577
- - Namespaced: \`core.*\` (workspace, configRoot, gatewayUrl) and \`memory.*\` (budget, warningThreshold, staleDays)
1725
+ - Namespaced: \`core.*\` (workspace, configRoot, gatewayUrl, devRepos) and \`memory.*\` (budget, warningThreshold)
1578
1726
  - Inspect with \`jeeves config [jsonpath]\`
1579
1727
 
1580
1728
  ## HEARTBEAT Protocol
@@ -1634,26 +1782,304 @@ When a workspace file exceeds the warning threshold, a \`## {filename}\` alert a
1634
1782
  Each file heading follows the same declined/active lifecycle as component headings. Users can decline alerts by changing the heading to \`## {filename}: declined\` (e.g., \`## AGENTS.md: declined\`).
1635
1783
  `;
1636
1784
 
1785
+ var operationsContent = `---
1786
+ name: operations
1787
+ description: Operational knowledge for a Jeeves installation. Covers date formatting utilities, email pipeline architecture, curation signal protocol, label taxonomy, and data flow patterns. Use when working with date formatting, email scripts, debugging email pipeline issues, or understanding how human email actions are interpreted.
1788
+ ---
1789
+
1790
+ # Operations
1791
+
1792
+ Operational knowledge for the Jeeves platform. Covers email pipeline architecture, curation protocols, and operational conventions.
1793
+
1794
+ ## Date Formatting
1795
+
1796
+ A \`date-fns\` wrapper lives at \`{configRoot}/jeeves-core/scripts/src/lib/dates.ts\`. It provides:
1797
+
1798
+ | Export | Purpose |
1799
+ |--------|---------|
1800
+ | \`dayOfWeek(dateStr)\` | Full weekday name for an ISO date string (e.g. \`'Monday'\`) |
1801
+ | \`formatDate(dateStr, fmt)\` | Format with any date-fns pattern |
1802
+ | \`relativeDays(dateStr, refStr?)\` | Human-friendly relative description (\`'today'\`, \`'tomorrow'\`, \`'3 days ago'\`) |
1803
+ | \`parseISO\` / \`format\` | Re-exported from date-fns for direct use |
1804
+
1805
+ ### Gateway session usage
1806
+
1807
+ From a gateway session, call via \`exec\`:
1808
+
1809
+ \`\`\`
1810
+ node -e "import { dayOfWeek } from './src/lib/dates.js'; console.log(dayOfWeek('2026-05-11'));"
1811
+ \`\`\`
1812
+
1813
+ with \`workdir: {configRoot}/jeeves-core/scripts\`.
1814
+
1815
+ Or use the simpler inline form when the full wrapper isn't needed:
1816
+
1817
+ \`\`\`
1818
+ node -e "import { format, parseISO } from 'date-fns'; console.log(format(parseISO('2026-05-11'), 'EEEE'));"
1819
+ \`\`\`
1820
+
1821
+ with \`workdir: {configRoot}/jeeves-core/scripts\` (so date-fns resolves from \`node_modules\`).
1822
+
1823
+ ### Hard gate
1824
+
1825
+ NEVER state a day of the week without computing it first. LLMs cannot do day-of-week arithmetic reliably.
1826
+
1827
+ ## Email Curation Signal Protocol
1828
+
1829
+ Defines how human email actions in Gmail are interpreted by Jeeves email processes.
1830
+
1831
+ ### Human Signals
1832
+
1833
+ | Signal | Meaning | Action |
1834
+ |--------|---------|--------|
1835
+ | Label added | Human is adjusting Jeeves classification | Update domain process inputs to reflect new classification |
1836
+ | Label removed | Human is adjusting Jeeves classification (removal) | Update domain process inputs to reflect removed classification |
1837
+ | Archived → Inbox | Human wants to keep this email in sight | Add \`watch\` label via update queue |
1838
+ | Starred / Flagged | Elevated attention — email is important in context | Domain processes should weight higher |
1839
+ | Moved to Spam | Confirmed spam — human classified as junk | Learn from classification for future triage |
1840
+ | Removed from Spam | False positive — human rescued from spam | Process as normal email, learn from false positive |
1841
+
1842
+ ### Watch Label
1843
+
1844
+ The \`watch\` label has special semantics:
1845
+ - When present, never auto-archive the email
1846
+ - When a watched email lands in archive (by anyone), remove the watch label
1847
+ - Added automatically when a human moves an archived email back to inbox
1848
+
1849
+ ### Label Taxonomy
1850
+
1851
+ Labels applied by Jeeves processes fall into two categories:
1852
+
1853
+ **Mechanical labels** (applied by domain extractors with high confidence):
1854
+ - \`meeting\` — meeting-related email (invite, notes, transcript)
1855
+ - \`finance\` — financial email (receipt, invoice, billing, statement)
1856
+
1857
+ **Reasoning labels** (applied by Update Email Meta, requiring cross-domain context):
1858
+ - \`project/<name>\` — associated with a known project
1859
+ - \`todo\` — requires action or work from the user
1860
+ - \`reply\` — someone is waiting on a response
1861
+ - \`alert\` — automated notification from a service
1862
+ - \`readme\` — newsletter or subscribed informational content
1863
+
1864
+ ### Labeling Principles
1865
+
1866
+ 1. Label at the earliest point where confidence is high enough
1867
+ 2. Domain extractors label what they know with certainty
1868
+ 3. Update Email Meta labels what requires cross-domain context
1869
+ 4. A thread can have multiple labels
1870
+ 5. Do not re-label threads that already have the label
1871
+ 6. **Prefer false negatives over false positives**
1872
+
1873
+ ### Poll Scope
1874
+
1875
+ Query: \`newer_than:1d in:anywhere\`
1876
+
1877
+ Must include spam and trash to detect human curation signals (e.g., moving to/from spam).
1878
+
1879
+ ## Email Pipeline Architecture
1880
+
1881
+ ### Directory Layout
1882
+
1883
+ \`\`\`
1884
+ {configRoot}/jeeves-core/email-config.json — pipeline configuration (accounts, buckets)
1885
+ {workspace}/../email/threads/{account}/ — canonical email archive (thread.json + per-message JSONs)
1886
+ \`\`\`
1887
+
1888
+ ### Data Flow
1889
+
1890
+ 1. **Poll** (\`email/poll.ts\`) — searches Gmail for recent threads, classifies, enqueues important ones for metadata fetch
1891
+ 2. **Fetch** (\`email/email-fetch.ts\`) — fetches full thread metadata from Gmail, creates/updates \`thread.json\` cache, enqueues for body download
1892
+ 3. **Download** (\`email/download.ts\`) — downloads full message bodies, writes per-message JSONs to \`threads/{account}/{threadId}/\`
1893
+ 4. **Drain Updates** (\`email/drain-updates.ts\`) — applies label changes and other queued updates back to Gmail
1894
+ 5. **Meta synthesis** — jeeves-meta synthesizes email archives into searchable summaries
1895
+
1896
+ ### thread.json (Cache Format)
1897
+
1898
+ Each \`threads/{account}/{threadId}/thread.json\` contains:
1899
+ - \`threadId\`, \`account\`, \`subject\`, \`participants\`
1900
+ - \`messages\` — record of \`{ messageId → { from, to, cc, date, internalDateMs, labels, snippet, attachments } }\`
1901
+ - \`provenance\` — label change history
1902
+ - \`cachedAt\`, \`updatedAt\`
1903
+
1904
+ ### Per-Message JSONs
1905
+
1906
+ Each \`threads/{account}/{threadId}/{messageId}.json\` contains full message data:
1907
+ - \`messageId\`, \`threadId\`, \`account\`, \`subject\`, \`from\`, \`to\`, \`cc\`
1908
+ - \`date\` (RFC 2822), \`internalDateMs\` (epoch ms)
1909
+ - \`labels\`, \`body\`, \`attachments\`, \`downloadedAt\`
1910
+ `;
1911
+
1912
+ var playbooksContent = `---
1913
+ name: playbooks
1914
+ description: >
1915
+ Reusable operational workflow patterns for the Jeeves platform. Use when asked to
1916
+ set up a daily briefing for a person or team, create standing meeting ops (notes +
1917
+ agenda generation), replicate an existing workflow pattern for a new context, or
1918
+ understand how recurring intelligence/ops workflows are structured. Covers the
1919
+ full stack: content directory, TASK files, standing orders, runner jobs, dispatcher
1920
+ scripts, Slack channel integration, and meta synthesis.
1921
+ ---
1922
+
1923
+ # Playbooks
1924
+
1925
+ Proven, replicable operational patterns. Each playbook describes what it does, what
1926
+ infrastructure it needs, and how to instantiate a new instance.
1927
+
1928
+ ## Common Infrastructure
1929
+
1930
+ All playbooks share these building blocks:
1931
+
1932
+ | Component | Purpose |
1933
+ |-----------|---------|
1934
+ | **Content directory** | \`{workspace}/../<silo>/<domain>/\` — stores output files, \`.meta/\`, standing orders |
1935
+ | **TASK file** | Markdown prompt that defines the LLM session's entire job |
1936
+ | **Standing orders** | \`standing-orders.md\` — append-only file for persistent stakeholder preferences |
1937
+ | **Dispatcher script** | TypeScript in \`{configRoot}/jeeves-core/scripts/src/\` — reads TASK, spawns worker |
1938
+ | **Runner job** | jeeves-runner job with cron schedule, timezone, and rrstack |
1939
+ | **Slack channel** | Delivery surface — summary posts, quick-link pins, feedback loop |
1940
+ | **Meta entity** | \`.meta/\` directory seeded so jeeves-meta synthesizes context over time |
1941
+
1942
+ ### Dispatcher Pattern
1943
+
1944
+ All dispatchers use \`taskFileDispatcher\` from \`dispatchers/lib/task-file-dispatcher.ts\`:
1945
+
1946
+ \`\`\`typescript
1947
+ import { taskFileDispatcher } from '../dispatchers/lib/task-file-dispatcher.js';
1948
+
1949
+ taskFileDispatcher({
1950
+ scriptName: '<silo>/<job-name>',
1951
+ jobId: '<runner-job-id>',
1952
+ taskFile: '<path-to-TASK.md>',
1953
+ timeout: 600,
1954
+ injectDateContext: true,
1955
+ dateTimezone: '<IANA timezone>',
1956
+ });
1957
+ \`\`\`
1958
+
1959
+ \`injectDateContext: true\` prepends an authoritative date line so the LLM knows today's date.
1960
+
1961
+ ### Standing Orders Convention
1962
+
1963
+ - Append-only — never modify existing entries
1964
+ - TASK files instruct the LLM to read standing orders at Step 0
1965
+ - TASK files instruct the LLM to append new persistent preferences from channel feedback
1966
+ - Include initial configuration section with participants, timezones, channel rules
1967
+
1968
+ ## Available Playbook Patterns
1969
+
1970
+ | Pattern | Description |
1971
+ |---------|-------------|
1972
+ | **Daily Briefing** | Recurring intelligence or action-item report for a stakeholder |
1973
+ | **Standing Meeting Ops** | Post-meeting notes + next-day agenda generation for a recurring meeting |
1974
+
1975
+ ## Instantiation Checklist
1976
+
1977
+ When creating a new playbook instance:
1978
+
1979
+ 1. Choose the appropriate pattern from the table above
1980
+ 2. Create the content directory with \`.meta/\` and \`standing-orders.md\`
1981
+ 3. Write the TASK file(s) — adapt from an existing instance. Ensure Step 0 reads feedback from the *delivery channel* (where output is posted), not only a DM
1982
+ 4. Write the dispatcher script(s) in \`{configRoot}/jeeves-core/scripts/src/<silo>/\`
1983
+ 5. Register the runner job(s) with appropriate cron, timezone, rrstack
1984
+ 6. Set up the Slack channel — pin a quick-links message if the pattern calls for it
1985
+ 7. Seed \`.meta/\` so meta synthesis begins
1986
+ 8. Test with \`--dry-run\` before going live
1987
+ `;
1988
+
1989
+ var slackBotProvisionerContent = `---
1990
+ name: slack-bot-provisioner
1991
+ description: Provision a new Slack bot identity for Clawdbot on a fresh server. Guides through Slack App creation steps, collects tokens, writes local config/env, and verifies connectivity.
1992
+ ---
1993
+
1994
+ # Slack bot provisioner (per-bot server)
1995
+
1996
+ Use this when you are setting up **a new Clawdbot instance** that should have **its own Slack bot identity** (one Slack App per bot), and you want a repeatable guided setup.
1997
+
1998
+ This skill assumes:
1999
+ - The user is a Slack workspace admin.
2000
+ - Each bot runs on its own server with its own Gateway config.
2001
+
2002
+ ## What can be automated vs not
2003
+
2004
+ **Automated (this skill):**
2005
+ - Create local folders.
2006
+ - Write a \`slack.env\` (bot token, signing secret).
2007
+ - Patch the Clawdbot gateway config to enable Slack for this instance (user approves).
2008
+ - Run a connectivity test (send a message to a channel).
2009
+
2010
+ **Not fully automatable (Slack-side):**
2011
+ - Creating/installing the Slack App and granting scopes (UI/OAuth).
2012
+ - Verifying event subscription URLs (requires public HTTPS endpoint).
2013
+
2014
+ ## Recommended mode
2015
+ Start with **outbound-only** (post messages) and expand to event subscriptions later.
2016
+
2017
+ ## Quick start
2018
+
2019
+ 1) Have the user do the Slack UI steps in \`references/slack-app-checklist.md\`.
2020
+ 2) Run the provisioning script:
2021
+
2022
+ - PowerShell:
2023
+ - \`powershell -NoProfile -ExecutionPolicy Bypass -File scripts/provision.ps1\`
2024
+
2025
+ The script will prompt for:
2026
+ - bot name
2027
+ - Slack bot token (\`xoxb-...\`)
2028
+ - Slack signing secret
2029
+ - (optional) test channel id
2030
+
2031
+ ## Files
2032
+ - Script: \`scripts/provision.ps1\`
2033
+ - Reference checklist: \`references/slack-app-checklist.md\`
2034
+ - Reference scopes: \`references/scopes.md\`
2035
+
2036
+ ## Secrets (best practices)
2037
+ - **Do not** store secrets inside the skill folder (skills are meant to be shareable/publishable).
2038
+ - Store secrets **per-instance** in the Clawdbot runtime directory (recommended):
2039
+ - \`C:\\Users\\Administrator\\.clawdbot\\credentials\\...\`
2040
+ - Prefer environment variables / local credential files loaded by the Gateway/service manager.
2041
+ - Never paste Slack secrets into public chats.
2042
+
2043
+ ## Safety notes
2044
+ - Store secrets per-instance. Do not reuse tokens across bot identities.
2045
+ - Avoid bot-to-bot loops: bots should ignore messages from other bots by default.
2046
+ `;
2047
+
1637
2048
  /**
1638
- * Skill seeding: write the `jeeves` workspace skill unconditionally.
2049
+ * Skill seeding: write all bundled platform skills to the workspace.
1639
2050
  *
1640
2051
  * @remarks
1641
- * The skill file is entirely generated — no user-authored content (Decision 48).
1642
- * Every installer (core CLI and component plugins) writes it unconditionally.
2052
+ * Skill files are entirely generated — no user-authored content (Decision 48).
2053
+ * Every installer (core CLI and component plugins) writes them unconditionally.
1643
2054
  * Content is inlined at build time via `rollup-plugin-md.ts`.
2055
+ *
2056
+ * @module
1644
2057
  */
2058
+ /** Map of skill directory name to inlined content. */
2059
+ const BUNDLED_SKILLS = {
2060
+ jeeves: jeevesContent,
2061
+ coding: codingContent,
2062
+ 'slack-bot-provisioner': slackBotProvisionerContent,
2063
+ operations: operationsContent,
2064
+ playbooks: playbooksContent,
2065
+ };
1645
2066
  /**
1646
- * Seed the jeeves workspace skill file.
2067
+ * Seed all bundled platform skills into the workspace.
2068
+ *
2069
+ * @remarks
2070
+ * Writes each skill to `{workspace}/skills/{name}/SKILL.md`, creating
2071
+ * directories as needed. Overwrites existing content unconditionally.
1647
2072
  *
1648
2073
  * @param workspacePath - Workspace root directory.
1649
2074
  */
1650
- function seedSkill(workspacePath) {
1651
- const skillDir = join(workspacePath, SKILLS_DIR, JEEVES_SKILL_DIR);
1652
- if (!existsSync(skillDir)) {
1653
- mkdirSync(skillDir, { recursive: true });
2075
+ function seedSkills(workspacePath) {
2076
+ for (const [name, content] of Object.entries(BUNDLED_SKILLS)) {
2077
+ const skillDir = join(workspacePath, SKILLS_DIR, name);
2078
+ if (!existsSync(skillDir)) {
2079
+ mkdirSync(skillDir, { recursive: true });
2080
+ }
2081
+ writeFileSync(join(skillDir, 'SKILL.md'), content, 'utf-8');
1654
2082
  }
1655
- const skillPath = join(skillDir, 'SKILL.md');
1656
- writeFileSync(skillPath, skillContent, 'utf-8');
1657
2083
  }
1658
2084
 
1659
2085
  /**
@@ -2879,11 +3305,11 @@ function createPluginCli(options) {
2879
3305
  console.log(' ⚠ Could not write HEARTBEAT entry');
2880
3306
  }
2881
3307
  try {
2882
- seedSkill(ws);
2883
- console.log(' ✓ Jeeves skill seeded');
3308
+ seedSkills(ws);
3309
+ console.log(' ✓ Platform skills seeded');
2884
3310
  }
2885
3311
  catch {
2886
- console.log(' ⚠ Could not seed Jeeves skill');
3312
+ console.log(' ⚠ Could not seed platform skills');
2887
3313
  }
2888
3314
  }
2889
3315
  }
@@ -3214,11 +3640,6 @@ function createServiceCli(descriptor) {
3214
3640
  });
3215
3641
  // Apply custom CLI commands if provided
3216
3642
  if (descriptor.customCliCommands) {
3217
- // Cast required: @commander-js/extra-typings Command has generic type
3218
- // parameters that don't align with the descriptor's base Command type.
3219
- // The descriptor can't know the parent Command's exact generic parameters
3220
- // at definition time. The cast is safe — customCliCommands only adds
3221
- // subcommands and doesn't depend on the parent's generic state.
3222
3643
  descriptor.customCliCommands(program);
3223
3644
  }
3224
3645
  return program;
@@ -3503,7 +3924,7 @@ Periodic checks (email, calendar, mentions) belong in jeeves-runner scripts, not
3503
3924
  ## Platform Surface Conventions
3504
3925
 
3505
3926
  **Slack:**
3506
- - No threaded replies by default
3927
+ - Never initiate a threaded reply. Only reply within a thread started by a human.
3507
3928
  - Use \`<#C…>\` for channel references
3508
3929
 
3509
3930
  **Table formatting:** On channels that do not support Markdown tables (Slack, Discord, WhatsApp, IRC), use code-block tables with aligned columns. Markdown tables are only safe in contexts that render them (GitHub, jeeves-server, files).
@@ -3709,8 +4130,8 @@ Never manually edit \`~/.openclaw/extensions/\`. Always use the CLI commands abo
3709
4130
 
3710
4131
  ### Reference Templates
3711
4132
 
3712
- <!-- IF_TEMPLATES -->
3713
- Reference templates are available at \`__TEMPLATE_PATH__\`:
4133
+ {{#if templatePath}}
4134
+ Reference templates are available at \`{{templatePath}}\`:
3714
4135
 
3715
4136
  | Template | Purpose |
3716
4137
  |----------|---------|
@@ -3718,9 +4139,39 @@ Reference templates are available at \`__TEMPLATE_PATH__\`:
3718
4139
  | \`spec-to-code-guide.md\` | The spec-to-code development practice — 7-stage iterative process, convergence loops, release gates |
3719
4140
 
3720
4141
  Read these templates when creating new specs, onboarding to new projects, or when asked about the development process.
3721
- <!-- ELSE_TEMPLATES -->
4142
+ {{else}}
3722
4143
  > Reference templates not yet installed. Run \`npx @karmaniverous/jeeves install\` to seed templates.
3723
- <!-- ENDIF_TEMPLATES -->
4144
+ {{/if}}
4145
+
4146
+ ### Post-Upgrade Maintenance
4147
+
4148
+ After updating OpenClaw (\`npm install -g openclaw@latest\` or equivalent), reinstall all Jeeves component plugins to repair install state:
4149
+
4150
+ \`\`\`bash
4151
+ npx @karmaniverous/jeeves install
4152
+ npx @karmaniverous/jeeves-runner-openclaw install
4153
+ npx @karmaniverous/jeeves-watcher-openclaw install
4154
+ npx @karmaniverous/jeeves-server-openclaw install
4155
+ npx @karmaniverous/jeeves-meta-openclaw install
4156
+ \`\`\`
4157
+
4158
+ Then restart the gateway. Plugin installers copy dist files and patch config; reinstalling after an OpenClaw update ensures the extensions directory stays consistent.
4159
+
4160
+ ### Source Code Preference
4161
+
4162
+ {{#if devRepos}}
4163
+ When investigating, debugging, or analyzing Jeeves components, always read TypeScript source from dev repos — never compiled \`dist/\` from the global npm install. Dev repos:
4164
+
4165
+ | Component | Dev Repo |
4166
+ |-----------|----------|
4167
+ {{#each devRepos}}
4168
+ | {{@key}} | \`{{this}}\` |
4169
+ {{/each}}
4170
+
4171
+ Built code is minified, harder to reason about, and wastes context. Always \`git pull\` before analysis.
4172
+ {{else}}
4173
+ > Dev repo paths not configured. Add \`core.devRepos\` to \`jeeves.config.json\` to enable source code preference guidance.
4174
+ {{/if}}
3724
4175
  `;
3725
4176
 
3726
4177
  /**
@@ -3732,6 +4183,10 @@ Read these templates when creating new specs, onboarding to new projects, or whe
3732
4183
  * Platform template with live data, and writes managed sections using
3733
4184
  * `updateManagedSection`.
3734
4185
  */
4186
+ /** Compiled Handlebars template for the Platform section (cached at module level). */
4187
+ const compiledPlatformTemplate = Handlebars.compile(toolsPlatformTemplate, {
4188
+ noEscape: true,
4189
+ });
3735
4190
  /**
3736
4191
  * Resolve the package's content directory for template file copying.
3737
4192
  *
@@ -3775,23 +4230,13 @@ function copyTemplates(coreConfigDir) {
3775
4230
  cpSync(sourceDir, destDir, { recursive: true });
3776
4231
  }
3777
4232
  /**
3778
- * Render the Platform template using simple string replacement.
4233
+ * Render the Platform template using Handlebars.
3779
4234
  *
3780
- * @param templatePath - Path to the templates directory.
4235
+ * @param context - Template context with optional templatePath and devRepos.
3781
4236
  * @returns Rendered platform content string.
3782
4237
  */
3783
- function renderPlatformTemplate(templatePath) {
3784
- const templatesAvailable = existsSync(templatePath);
3785
- let content = toolsPlatformTemplate;
3786
- // Handle <!-- IF_TEMPLATES --> ... <!-- ELSE_TEMPLATES --> ... <!-- ENDIF_TEMPLATES --> block
3787
- const ifRegex = /<!-- IF_TEMPLATES -->([\s\S]*?)<!-- ELSE_TEMPLATES -->([\s\S]*?)<!-- ENDIF_TEMPLATES -->/;
3788
- const match = ifRegex.exec(content);
3789
- if (match) {
3790
- content = content.replace(match[0], templatesAvailable ? match[1] : match[2]);
3791
- }
3792
- // Replace __TEMPLATE_PATH__ with the actual path
3793
- content = content.replace(/__TEMPLATE_PATH__/g, templatePath);
3794
- return content;
4238
+ function renderPlatformTemplate(context) {
4239
+ return compiledPlatformTemplate(context);
3795
4240
  }
3796
4241
  /**
3797
4242
  * Refresh platform content: SOUL.md, AGENTS.md, and TOOLS.md Platform section.
@@ -3799,7 +4244,7 @@ function renderPlatformTemplate(templatePath) {
3799
4244
  * @param options - Configuration for the refresh cycle.
3800
4245
  */
3801
4246
  async function refreshPlatformContent(options) {
3802
- const { coreVersion, componentName, componentVersion, servicePackage, pluginPackage, stalenessThresholdMs, } = options;
4247
+ const { coreVersion, componentName, componentVersion, servicePackage, pluginPackage, stalenessThresholdMs, workspaceConfig, } = options;
3803
4248
  const workspacePath = getWorkspacePath();
3804
4249
  const coreConfigDir = getCoreConfigDir();
3805
4250
  // 1. Write calling component's version entry
@@ -3813,7 +4258,11 @@ async function refreshPlatformContent(options) {
3813
4258
  }
3814
4259
  // 2. Render Platform template
3815
4260
  const templatePath = join(coreConfigDir, TEMPLATES_DIR);
3816
- const platformContent = renderPlatformTemplate(templatePath);
4261
+ const wsConfig = workspaceConfig ?? loadWorkspaceConfig(workspacePath);
4262
+ const platformContent = renderPlatformTemplate({
4263
+ templatePath: existsSync(templatePath) ? templatePath : undefined,
4264
+ devRepos: wsConfig?.core?.devRepos,
4265
+ });
3817
4266
  // 3. Write TOOLS.md Platform section
3818
4267
  const toolsPath = join(workspacePath, WORKSPACE_FILES.tools);
3819
4268
  await updateManagedSection(toolsPath, platformContent, {
@@ -3941,46 +4390,21 @@ function scanAndEscalateCleanup(targets, gatewayUrl, pendingCleanups) {
3941
4390
  }
3942
4391
 
3943
4392
  /**
3944
- * Memory budget accounting and staleness detection for MEMORY.md.
4393
+ * Memory budget accounting for MEMORY.md.
3945
4394
  *
3946
4395
  * @remarks
3947
- * Scans MEMORY.md for ISO date patterns in H2/H3 headings and bullet items.
3948
- * Reports character count against a configured budget, warning threshold state,
3949
- * and stale section candidates. Does not auto-delete: review remains
3950
- * human- or agent-mediated (Decision 42).
3951
- */
3952
- /** ISO date pattern: YYYY-MM-DD. */
3953
- const ISO_DATE_RE = /\b(\d{4}-\d{2}-\d{2})\b/g;
3954
- /** H2 heading pattern used to split sections. */
3955
- const H2_RE = /^## /m;
3956
- /**
3957
- * Extract the most recent ISO date from a string.
3958
- *
3959
- * @param text - Text to scan for dates.
3960
- * @returns The most recent date found, or undefined.
4396
+ * Reports character count against a configured budget and warning threshold
4397
+ * state. Does not auto-delete: review remains human- or agent-mediated
4398
+ * (Decision 42). Staleness detection removed in v0.5.9 (Decision 45).
3961
4399
  */
3962
- function extractMostRecentDate(text) {
3963
- const matches = text.match(ISO_DATE_RE);
3964
- if (!matches)
3965
- return undefined;
3966
- let latest;
3967
- for (const match of matches) {
3968
- const d = new Date(match + 'T00:00:00Z');
3969
- if (!Number.isNaN(d.getTime())) {
3970
- if (!latest || d > latest)
3971
- latest = d;
3972
- }
3973
- }
3974
- return latest;
3975
- }
3976
4400
  /**
3977
- * Analyze MEMORY.md for budget and staleness.
4401
+ * Analyze MEMORY.md for budget health.
3978
4402
  *
3979
4403
  * @param options - Analysis configuration.
3980
4404
  * @returns Memory hygiene result.
3981
4405
  */
3982
4406
  function analyzeMemory(options) {
3983
- const { workspacePath, budget, warningThreshold, staleDays } = options;
4407
+ const { workspacePath, budget, warningThreshold } = options;
3984
4408
  const memoryPath = join(workspacePath, WORKSPACE_FILES.memory);
3985
4409
  if (!existsSync(memoryPath)) {
3986
4410
  return {
@@ -3990,8 +4414,6 @@ function analyzeMemory(options) {
3990
4414
  usage: 0,
3991
4415
  warning: false,
3992
4416
  overBudget: false,
3993
- staleCandidates: 0,
3994
- staleSectionNames: [],
3995
4417
  };
3996
4418
  }
3997
4419
  const content = readFileSync(memoryPath, 'utf-8');
@@ -3999,21 +4421,6 @@ function analyzeMemory(options) {
3999
4421
  const usage = budget > 0 ? charCount / budget : charCount > 0 ? Infinity : 0;
4000
4422
  const warning = usage >= warningThreshold;
4001
4423
  const overBudget = usage > 1;
4002
- // Split into H2 sections and scan for staleness
4003
- const sections = content.split(H2_RE).slice(1); // skip content before first H2
4004
- const now = Date.now();
4005
- const thresholdMs = staleDays * 24 * 60 * 60 * 1000;
4006
- const staleSectionNames = [];
4007
- for (const section of sections) {
4008
- const sectionName = section.split('\n')[0]?.trim() ?? '';
4009
- const recentDate = extractMostRecentDate(section);
4010
- // Sections without dates are evergreen — never flagged (Decision 47)
4011
- if (!recentDate)
4012
- continue;
4013
- if (now - recentDate.getTime() > thresholdMs) {
4014
- staleSectionNames.push(sectionName);
4015
- }
4016
- }
4017
4424
  return {
4018
4425
  exists: true,
4019
4426
  charCount,
@@ -4021,8 +4428,6 @@ function analyzeMemory(options) {
4021
4428
  usage,
4022
4429
  warning,
4023
4430
  overBudget,
4024
- staleCandidates: staleSectionNames.length,
4025
- staleSectionNames,
4026
4431
  };
4027
4432
  }
4028
4433
 
@@ -4049,20 +4454,14 @@ function checkMemoryHealth(options) {
4049
4454
  const result = analyzeMemory(options);
4050
4455
  if (!result.exists)
4051
4456
  return undefined;
4052
- if (!result.warning && result.staleCandidates === 0)
4457
+ if (!result.warning)
4053
4458
  return undefined;
4054
- const lines = [];
4055
- if (result.warning) {
4056
- const pct = Math.round(result.usage * 100);
4057
- lines.push(`- Budget: ${result.charCount.toLocaleString()} / ${result.budget.toLocaleString()} chars (${String(pct)}%).${result.overBudget ? ' **Over budget.**' : ' Consider reviewing.'}`);
4058
- }
4059
- if (result.staleCandidates > 0) {
4060
- lines.push(`- ${String(result.staleCandidates)} stale section${result.staleCandidates === 1 ? '' : 's'}: ${result.staleSectionNames.join(', ')}`);
4061
- }
4459
+ const pct = Math.round(result.usage * 100);
4460
+ const content = `- Budget: ${result.charCount.toLocaleString()} / ${result.budget.toLocaleString()} chars (${String(pct)}%).${result.overBudget ? ' **Over budget.**' : ' Consider reviewing.'}`;
4062
4461
  return {
4063
4462
  name: MEMORY_HEARTBEAT_NAME,
4064
4463
  declined: false,
4065
- content: lines.join('\n'),
4464
+ content,
4066
4465
  };
4067
4466
  }
4068
4467
 
@@ -4302,9 +4701,8 @@ function getServiceUrl(serviceName, consumerName) {
4302
4701
  if (coreUrl)
4303
4702
  return coreUrl;
4304
4703
  // 3. Fall back to port constants
4305
- const port = DEFAULT_PORTS[serviceName];
4306
- if (port !== undefined) {
4307
- return `http://127.0.0.1:${String(port)}`;
4704
+ if (serviceName in DEFAULT_PORTS) {
4705
+ return `http://127.0.0.1:${String(DEFAULT_PORTS[serviceName])}`;
4308
4706
  }
4309
4707
  throw new Error(`jeeves-core: unknown service "${serviceName}" and no config found`);
4310
4708
  }
@@ -4628,6 +5026,7 @@ function readFileOrEmpty(filePath) {
4628
5026
  */
4629
5027
  async function runHeartbeatCycle(options) {
4630
5028
  const { workspacePath, coreConfigDir, configRoot } = options;
5029
+ const wsConfig = options.workspaceConfig ?? loadWorkspaceConfig(workspacePath);
4631
5030
  const heartbeatPath = join(workspacePath, WORKSPACE_FILES.heartbeat);
4632
5031
  try {
4633
5032
  const existingContent = readFileOrEmpty(heartbeatPath);
@@ -4640,14 +5039,11 @@ async function runHeartbeatCycle(options) {
4640
5039
  });
4641
5040
  // Memory hygiene check (Decision 49)
4642
5041
  if (!declinedNames.has(MEMORY_HEARTBEAT_NAME)) {
4643
- const wsConfig = loadWorkspaceConfig(workspacePath);
4644
5042
  const memoryEntry = checkMemoryHealth({
4645
5043
  workspacePath,
4646
5044
  budget: wsConfig?.memory?.budget ?? WORKSPACE_CONFIG_DEFAULTS.memory.budget,
4647
5045
  warningThreshold: wsConfig?.memory?.warningThreshold ??
4648
5046
  WORKSPACE_CONFIG_DEFAULTS.memory.warningThreshold,
4649
- staleDays: wsConfig?.memory?.staleDays ??
4650
- WORKSPACE_CONFIG_DEFAULTS.memory.staleDays,
4651
5047
  });
4652
5048
  if (memoryEntry)
4653
5049
  entries.push(memoryEntry);
@@ -4701,6 +5097,7 @@ class ComponentWriter {
4701
5097
  gatewayUrl;
4702
5098
  pendingCleanups = new Set();
4703
5099
  cyclePromise;
5100
+ stopped = false;
4704
5101
  /** @internal */
4705
5102
  constructor(component, options) {
4706
5103
  this.component = component;
@@ -4726,6 +5123,7 @@ class ComponentWriter {
4726
5123
  * contention on startup.
4727
5124
  */
4728
5125
  start() {
5126
+ this.stopped = false;
4729
5127
  if (this.isRunning)
4730
5128
  return;
4731
5129
  // Random jitter up to one full interval to spread initial writes
@@ -4738,6 +5136,7 @@ class ComponentWriter {
4738
5136
  }
4739
5137
  /** Stop the writer timer. */
4740
5138
  stop() {
5139
+ this.stopped = true;
4741
5140
  if (this.jitterTimeout) {
4742
5141
  clearTimeout(this.jitterTimeout);
4743
5142
  this.jitterTimeout = undefined;
@@ -4751,7 +5150,7 @@ class ComponentWriter {
4751
5150
  this.timer = setTimeout(() => {
4752
5151
  this.timer = undefined;
4753
5152
  void this.cycle().finally(() => {
4754
- if (this.isRunning)
5153
+ if (!this.stopped)
4755
5154
  this.scheduleNextCycle(intervalMs, intervalMs);
4756
5155
  });
4757
5156
  }, delayMs);
@@ -4786,15 +5185,18 @@ class ComponentWriter {
4786
5185
  markers: TOOLS_MARKERS,
4787
5186
  coreVersion: CORE_VERSION,
4788
5187
  });
4789
- // 2. Platform content maintenance
5188
+ // 2. Load workspace config once for the entire cycle
5189
+ const workspaceConfig = loadWorkspaceConfig(workspacePath);
5190
+ // 3. Platform content maintenance
4790
5191
  await refreshPlatformContent({
4791
5192
  coreVersion: CORE_VERSION,
4792
5193
  componentName: this.component.name,
4793
5194
  componentVersion: this.component.version,
4794
5195
  servicePackage: this.component.servicePackage,
4795
5196
  pluginPackage: this.component.pluginPackage,
5197
+ workspaceConfig,
4796
5198
  });
4797
- // 3. Cleanup escalation
5199
+ // 4. Cleanup escalation
4798
5200
  if (this.gatewayUrl) {
4799
5201
  scanAndEscalateCleanup([
4800
5202
  { filePath: toolsPath, markerIdentity: 'TOOLS' },
@@ -4808,11 +5210,12 @@ class ComponentWriter {
4808
5210
  },
4809
5211
  ], this.gatewayUrl, this.pendingCleanups);
4810
5212
  }
4811
- // 4. HEARTBEAT orchestration
5213
+ // 5. HEARTBEAT orchestration
4812
5214
  await runHeartbeatCycle({
4813
5215
  workspacePath,
4814
5216
  coreConfigDir: getCoreConfigDir(),
4815
5217
  configRoot: getConfigRoot(),
5218
+ workspaceConfig,
4816
5219
  });
4817
5220
  });
4818
5221
  }
@@ -5007,8 +5410,26 @@ async function seedContent(options) {
5007
5410
  content: `- ${NOT_INSTALLED_ALERTS[name]}`,
5008
5411
  }));
5009
5412
  await writeHeartbeatSection(heartbeatPath, entries);
5010
- // Seed jeeves workspace skill (Decision 48: overwrite-on-install)
5011
- seedSkill(getWorkspacePath());
5413
+ // Seed all bundled platform skills (Decision 48: overwrite-on-install)
5414
+ seedSkills(getWorkspacePath());
5415
+ }
5416
+
5417
+ /**
5418
+ * Backward-compatible re-export of `seedSkills`.
5419
+ *
5420
+ * @remarks
5421
+ * Delegates to `seedSkills` which seeds all bundled platform skills.
5422
+ * Retained for API compatibility with existing component plugins.
5423
+ *
5424
+ * @module
5425
+ */
5426
+ /**
5427
+ * Seed all bundled platform skills into the workspace.
5428
+ *
5429
+ * @param workspacePath - Workspace root directory.
5430
+ */
5431
+ function seedSkill(workspacePath) {
5432
+ seedSkills(workspacePath);
5012
5433
  }
5013
5434
 
5014
5435
  /**
@@ -5385,4 +5806,4 @@ async function getChannelWorkspace(channelId, token, options) {
5385
5806
  return teamId;
5386
5807
  }
5387
5808
 
5388
- export { AGENTS_MARKERS, CLEANUP_FLAG, COMPONENT_CONFIG_PREFIX, COMPONENT_VERSIONS_FILE, CONFIG_FILE, CORE_CONFIG_DIR, CORE_VERSION, ComponentWriter, DEFAULT_BIND_ADDRESS, DEFAULT_CORE_VERSION, DEFAULT_PORTS, HEARTBEAT_HEADING, JEEVES_SKILL_DIR, MEMORY_HEARTBEAT_NAME, META_PORT, PLATFORM_COMPONENTS, REGISTRY_CACHE_FILE, RUNNER_PORT, SECTION_IDS, SECTION_ORDER, SERVER_PORT, SKILLS_DIR, SOUL_MARKERS, STALENESS_THRESHOLD_MS, STALE_LOCK_MS, TEMPLATES_DIR, TOOLS_MARKERS, VERSION_STAMP_PATTERN, WATCHER_PORT, WORKSPACE_CONFIG_DEFAULTS, WORKSPACE_CONFIG_FILE, WORKSPACE_FILES, analyzeMemory, appendJsonl, atomicWrite, buildEffectiveConfig, buildHeartbeatSection, checkMemoryHealth, checkNodeVersion, checkRegistryVersion, connectionFail, coreConfigSchema, createAsyncContentCache, createComponentWriter, createConfigApplyHandler, createConfigQueryHandler, createGoogleAuth, createPluginCli, createPluginToolset, createServiceCli, createServiceManager, createStatusHandler, ensureDir, extractMostRecentDate, fail, fetchJson, fetchWithTimeout, formatBeginMarker, formatEndMarker, generateJsonSchema, generateWorkspaceJsonSchema, getArg, getBindAddress, getChannelWorkspace, getComponentConfigDir, getComponentConfigPath, getConfigRoot, getCoreConfigDir, getCoreConfigFile, getEffectiveServiceName, getErrorMessage, getPackageRoot, getPackageVersion, getServiceState, getServiceUrl, getWorkspacePath, init, isPrime, isTransientError, jaccard, jeevesComponentDescriptorSchema, loadEnvFile, loadWorkspaceConfig, needsCleanup, nowIso, ok, orchestrateHeartbeat, parseArgs, parseHeartbeat, parseManaged, patchConfig, postJson, readComponentVersions, readJson, readJsonl, refreshPlatformContent, registerComponentConfigPath, removeComponentVersion, removeManagedSection, resetInit, resolveConfigPath, resolveConfigValue, resolveOpenClawHome, resolveOptionalPluginSetting, resolvePluginSetting, resolveWorkspacePath, run, runScript, runWithRetry, saveCache, seedContent, seedSkill, shingles, shouldWrite, sleepAsync, sleepMs, updateManagedSection, uuid, withFileLock, workspaceConfigSchema, writeComponentVersion, writeHeartbeatSection, writeJsonAtomic, writeJsonl };
5809
+ export { AGENTS_MARKERS, CLEANUP_FLAG, COMPONENT_CONFIG_PREFIX, COMPONENT_VERSIONS_FILE, CONFIG_FILE, CORE_CONFIG_DIR, CORE_VERSION, ComponentWriter, DEFAULT_BIND_ADDRESS, DEFAULT_CORE_VERSION, DEFAULT_PORTS, HEARTBEAT_HEADING, JEEVES_SKILL_DIR, MEMORY_HEARTBEAT_NAME, META_PORT, PLATFORM_COMPONENTS, REGISTRY_CACHE_FILE, RUNNER_PORT, SECTION_IDS, SECTION_ORDER, SERVER_PORT, SKILLS_DIR, SOUL_MARKERS, STALENESS_THRESHOLD_MS, STALE_LOCK_MS, TEMPLATES_DIR, TOOLS_MARKERS, VERSION_STAMP_PATTERN, WATCHER_PORT, WORKSPACE_CONFIG_DEFAULTS, WORKSPACE_CONFIG_FILE, WORKSPACE_FILES, analyzeMemory, appendJsonl, atomicWrite, buildEffectiveConfig, buildHeartbeatSection, checkMemoryHealth, checkNodeVersion, checkRegistryVersion, connectionFail, coreConfigSchema, createAsyncContentCache, createComponentWriter, createConfigApplyHandler, createConfigQueryHandler, createGoogleAuth, createPluginCli, createPluginToolset, createServiceCli, createServiceManager, createStatusHandler, ensureDir, fail, fetchJson, fetchWithTimeout, formatBeginMarker, formatEndMarker, generateJsonSchema, generateWorkspaceJsonSchema, getArg, getBindAddress, getChannelWorkspace, getComponentConfigDir, getComponentConfigPath, getConfigRoot, getCoreConfigDir, getCoreConfigFile, getEffectiveServiceName, getErrorMessage, getPackageRoot, getPackageVersion, getServiceState, getServiceUrl, getWorkspacePath, init, isPrime, isTransientError, jaccard, jeevesComponentDescriptorSchema, loadEnvFile, loadWorkspaceConfig, needsCleanup, nowIso, ok, orchestrateHeartbeat, parseArgs, parseHeartbeat, parseManaged, patchConfig, postJson, readComponentVersions, readJson, readJsonl, refreshPlatformContent, registerComponentConfigPath, removeComponentVersion, removeManagedSection, resetInit, resolveConfigPath, resolveConfigValue, resolveOpenClawHome, resolveOptionalPluginSetting, resolvePluginSetting, resolveWorkspacePath, run, runScript, runWithRetry, saveCache, seedContent, seedSkill, seedSkills, shingles, shouldWrite, sleepAsync, sleepMs, updateManagedSection, uuid, withFileLock, workspaceConfigSchema, writeComponentVersion, writeHeartbeatSection, writeJsonAtomic, writeJsonl };