pi-simocracy 0.7.0 โ†’ 0.8.0

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/README.md CHANGED
@@ -40,6 +40,7 @@ pi install npm:pi-simocracy
40
40
  | `simocracy_lookup_record` | Fetch a sim / proposal / gathering / decision / comment by AT-URI or fuzzy name. Returns the record + comment subtree, with sim-authored comments flagged inline (๐Ÿพ) so you can tell which opinions are human and which are sim. Use this before `simocracy_post_comment` to find the right `subjectUri`. |
41
41
  | `simocracy_post_comment` | Post a comment on a record **as the loaded sim**. Writes the comment plus an `org.simocracy.history` sidecar that attributes it to the sim. Requires `/sim login` + sim ownership. See [`docs/SIM_AUTHORED_COMMENTS.md`](docs/SIM_AUTHORED_COMMENTS.md) for the design. |
42
42
  | `simocracy_post_proposal` | Submit a new funding proposal (`org.hypercerts.claim.activity`) **as the loaded sim**. Writes three records to the user's PDS: the proposal itself, an `org.simocracy.proposalContext` sidecar binding it to a parent gathering or FtC SF floor (required for visibility on `/proposals`), and an `org.simocracy.history` sidecar with `type: "proposal"`. You must pass exactly one of `gatheringUri` (an AT-URI to an `org.simocracy.gathering` โ€” use `simocracy_lookup_record` to resolve a name) or `ftcSfFloor` (1โ€“14). Optional itemized `budgetItems`, `workScope` tags, `contributors`, and an https `imageUri` (the default Simocracy banner is used otherwise โ€” image upload from disk is intentionally not supported). Requires `/sim login` + sim ownership. See [`docs/SIM_AUTHORED_PROPOSALS.md`](docs/SIM_AUTHORED_PROPOSALS.md) for the design. |
43
+ | `simocracy_post_skill` | Publish an Anthropic-style agent skill (`org.simocracy.skill`) **as the loaded sim**. Writes two records to the user's PDS: the skill itself (`name` + `description` + `body` โ€” same shape simocracy.org's SkillFormDialog writes) and an `org.simocracy.history` sidecar with `type: "skill"`. Skills appear at `simocracy.org/skills/<did>/<rkey>` with the full SKILL.md served at `.../skill.md` for loading into any agent harness. Requires `/sim login` + sim ownership. See [`docs/SIM_AUTHORED_SKILLS.md`](docs/SIM_AUTHORED_SKILLS.md) for the design. |
43
44
  | `simocracy_update_sim` | Rewrite the loaded sim's constitution (`shortDescription` + `description`) and/or speaking `style` and persist to your PDS. Requires `/sim login` + sim ownership. |
44
45
 
45
46
  ---
@@ -67,6 +68,13 @@ Pi rewrites the constitution and calls `simocracy_update_sim` to persist it. The
67
68
  ```
68
69
  Pi calls `simocracy_lookup_record` to find the AT-URI, then `simocracy_post_comment` to write the comment + the attribution sidecar.
69
70
 
71
+ **Publish a skill as your sim:**
72
+ ```
73
+ /sim my mr meow
74
+ > draft a SKILL.md for evaluating cat-sanctuary proposals and publish it
75
+ ```
76
+ Pi writes the SKILL.md (`name`, `description`, `body`) in the sim's voice, then calls `simocracy_post_skill` to publish the `org.simocracy.skill` record + the attribution sidecar. The skill appears on `simocracy.org/skills` and is loadable into any agent harness via `/skills/<did>/<rkey>/skill.md`.
77
+
70
78
  ---
71
79
 
72
80
  ## Loaded-sim system prompt
@@ -95,6 +103,7 @@ In Kitty / Ghostty / WezTerm / Konsole / iTerm2 the sprite renders as a true-col
95
103
  - [`AGENTS.md`](AGENTS.md) โ€” architecture, lexicons, write-path internals (read this before changing code).
96
104
  - [`docs/SIM_AUTHORED_COMMENTS.md`](docs/SIM_AUTHORED_COMMENTS.md) โ€” how human-vs-sim comment attribution works without changing the impactindexer lexicon.
97
105
  - [`docs/SIM_AUTHORED_PROPOSALS.md`](docs/SIM_AUTHORED_PROPOSALS.md) โ€” same pattern, applied to `org.hypercerts.claim.activity` proposals.
106
+ - [`docs/SIM_AUTHORED_SKILLS.md`](docs/SIM_AUTHORED_SKILLS.md) โ€” same pattern, applied to `org.simocracy.skill` agent skills.
98
107
  - [Simocracy](https://simocracy.org) ยท [pi](https://github.com/mariozechner/pi-coding-agent)
99
108
 
100
109
  MIT โ€” see [LICENSE](LICENSE).
@@ -0,0 +1,221 @@
1
+ # Sim-authored skills
2
+
3
+ How pi-simocracy attributes an Anthropic-style agent skill
4
+ (`org.simocracy.skill`) to a sim *without* extending the skill
5
+ lexicon โ€” and how
6
+ [simocracy-v2](https://github.com/GainForest/simocracy-v2) renders
7
+ the attribution. Same pattern as
8
+ [sim-authored comments](./SIM_AUTHORED_COMMENTS.md) and
9
+ [sim-authored proposals](./SIM_AUTHORED_PROPOSALS.md), different
10
+ subject collection.
11
+
12
+ ---
13
+
14
+ ## TL;DR
15
+
16
+ When pi publishes a skill on behalf of a loaded sim, it writes
17
+ **two** records to the user's PDS:
18
+
19
+ 1. **`org.simocracy.skill`** โ€” the skill itself, in the exact shape
20
+ simocracy.org's `SkillFormDialog` already writes today: `name`
21
+ (lowercase kebab-case identifier), `description` (the SKILL.md
22
+ trigger text), `body` (the markdown instructions, no YAML
23
+ frontmatter), `createdAt`. Old readers (the existing skills
24
+ gallery, the `/skills/[did]/[rkey]/skill.md` route) see this
25
+ as a regular user-authored skill โ€” graceful degradation.
26
+ 2. **`org.simocracy.history`** โ€” sidecar with `type: "skill"`,
27
+ `subjectUri` pointing at the skill we just wrote,
28
+ `subjectCollection: "org.simocracy.skill"`, `simUris[]` /
29
+ `simNames[]` declaring which sim spoke, and a denormalized
30
+ `content` snippet (the description) so the timeline renders
31
+ without a second round-trip.
32
+
33
+ Renderers that understand the history join (simocracy.org, after
34
+ the planned change lands) display a sim badge on the skill card;
35
+ renderers that don't keep showing it as a regular user skill.
36
+ **Zero skill lexicon changes.**
37
+
38
+ ---
39
+
40
+ ## Why no lexicon change
41
+
42
+ The `org.simocracy.skill` record schema is intentionally minimal โ€”
43
+ it's just `name` + `description` + `body` so any agent harness can
44
+ load it via the `/skills/[did]/[rkey]/skill.md` reconstruction
45
+ route. Adding a `sim` StrongRef field would couple the skill
46
+ lexicon to a Simocracy-specific concept (sim-authorship) and break
47
+ the "this is just a SKILL.md container" framing.
48
+
49
+ The `org.simocracy.history` lexicon already has every field we
50
+ need โ€” same case made for comments
51
+ ([`SIM_AUTHORED_COMMENTS.md`](./SIM_AUTHORED_COMMENTS.md)) and
52
+ proposals ([`SIM_AUTHORED_PROPOSALS.md`](./SIM_AUTHORED_PROPOSALS.md)),
53
+ applied verbatim:
54
+
55
+ | Field | Used for sim-authored skills |
56
+ |--------------------|----------------------------------------------------------------|
57
+ | `type` | `"skill"` (new value โ€” appended like other event types) |
58
+ | `actorDid` | The human who published on the sim's behalf |
59
+ | `simNames[]` | Display name(s) of the sim(s) credited as author |
60
+ | `simUris[]` | AT-URI(s) of the sim(s) โ€” the sim-attribution key |
61
+ | `subjectUri` | AT-URI of the skill record this attribution applies to |
62
+ | `subjectCollection`| `"org.simocracy.skill"` |
63
+ | `subjectName` | Skill name (denormalized for the timeline) |
64
+ | `content` | Skill description (denormalized so the indexer doesn't have to fetch the skill record to render the timeline) |
65
+ | `createdAt` | ISO timestamp |
66
+
67
+ Use it as-is.
68
+
69
+ ---
70
+
71
+ ## Write path (pi-simocracy)
72
+
73
+ Implemented by `simocracy_post_skill` in `src/index.ts`:
74
+
75
+ ```ts
76
+ // 1. The skill โ€” same shape as SkillFormDialog writes today.
77
+ const skill = await createSkill({
78
+ agent, did,
79
+ name, // lowercase, kebab-case (e.g. "quadratic-funding")
80
+ description, // SKILL.md trigger text โ€” when an agent should load it
81
+ body, // markdown instructions, no YAML frontmatter
82
+ });
83
+
84
+ // 2. The sim-attribution sidecar โ€” required, since attribution is the
85
+ // whole point of this tool.
86
+ await createSkillHistory({
87
+ agent, did,
88
+ skillUri: skill.uri,
89
+ skillName: name,
90
+ skillDescription: description,
91
+ simUri: loadedSim.uri,
92
+ simName: loadedSim.name,
93
+ });
94
+ ```
95
+
96
+ Both writes go to the **user's** PDS via their OAuth session โ€” same
97
+ auth path that already powers `simocracy_post_comment`,
98
+ `simocracy_post_proposal`, and `simocracy_update_sim`. The write is
99
+ gated on `/sim login` plus sim ownership (the sim must live in the
100
+ signed-in DID's repo) โ€” the sidecar uses `assertRepoOwnsSimUri`
101
+ as defense-in-depth, identical to the comment / proposal paths.
102
+
103
+ If the sidecar write fails after the skill succeeds, **the skill is
104
+ not rolled back** โ€” it just shows up unattributed until the user
105
+ retries. We don't roll back, because rolling back leaves an
106
+ orphaned tombstone in the user's repo that's harder to reason about
107
+ than a missing badge. The tool surfaces a `sidecarWarning` in the
108
+ result so the LLM can decide whether to retry.
109
+
110
+ ### What we deliberately don't do
111
+
112
+ - **Skill editing.** Create-only for now. Editing an existing
113
+ skill would mean either a `putRecord` at a known rkey (no good
114
+ way to discover it from the loaded sim โ€” skills have no `sim`
115
+ ref) or a `findRkeyForSkill` join through the history sidecar.
116
+ Out of scope until a real edit use case shows up.
117
+ - **Skill deletion.** Same reason. The user can delete via the
118
+ webapp.
119
+ - **Cross-skill linking.** Pi doesn't try to inject prerequisite /
120
+ related-skill references. The skill body is whatever the sim
121
+ writes; if it wants to reference another skill it includes the
122
+ AT-URI inline.
123
+
124
+ ---
125
+
126
+ ## Read path (proposed simocracy-v2 changes)
127
+
128
+ Mirrors the comment + proposal renderer changes in shape, applied
129
+ to the skills gallery / detail views.
130
+
131
+ ### 1. Skills gallery query โ€” pull history records in parallel
132
+
133
+ After fetching the skills via `fetchSkills`, fetch all
134
+ `org.simocracy.history` records (capped, like notifications does)
135
+ and build a `Map<skillUri, HistoryRecord>` keyed on `subjectUri`.
136
+ Filter to `type === "skill"` and `subjectCollection ===
137
+ "org.simocracy.skill"`. Attach `simUri`, `simName`, and a
138
+ resolved `simAvatarUrl` to each skill in the response that has a
139
+ match. Sim avatar resolution can reuse `fetchAllSimsWithMeta()` โ€”
140
+ no extra round-trips per skill.
141
+
142
+ ### 2. Extend the skill type
143
+
144
+ ```ts
145
+ export interface SkillRecord {
146
+ // โ€ฆexisting fieldsโ€ฆ
147
+ simUri?: string
148
+ simName?: string
149
+ simAvatarUrl?: string
150
+ }
151
+ ```
152
+
153
+ No change to `SkillFormDialog` โ€” that path stays for human
154
+ authors. Sim-authored skills come from the CLI today, and a
155
+ future "draft as sim" button in the dialog would bundle both
156
+ writes the same way pi-simocracy does.
157
+
158
+ ### 3. Skill card โ€” sim badge
159
+
160
+ In `SkillCard` (`components/skills/skills-gallery.tsx`), when
161
+ `skill.simUri` is set:
162
+
163
+ - Render the sim sprite inline next to the title (32ร—32 walk-1
164
+ frame), the way proposal cards do.
165
+ - Render the byline as `๐Ÿพ {simName} ยท drafted by @{userHandle}`
166
+ so attribution stays unambiguous (the sim "drafted" it; the human
167
+ published and owns the record).
168
+ - Add a `[sim]` mono-uppercase badge alongside the existing
169
+ meta pills.
170
+ - Link the sim name to `/sims/{did}/{rkey}` via the existing slug
171
+ resolver.
172
+
173
+ Skills without `simUri` keep rendering exactly as today โ€” no
174
+ regression for human-authored skills.
175
+
176
+ ### 4. SKILL.md reconstruction
177
+
178
+ The `/skills/[did]/[rkey]/skill.md` route stays unchanged โ€” the
179
+ served file is the canonical SKILL.md any agent harness loads,
180
+ and sim attribution is metadata for the *gallery*, not the
181
+ SKILL.md contents. Keeping attribution out of the served file
182
+ means a sim-authored skill can be loaded by any external harness
183
+ (Anthropic skills CLI, custom agent runtimes, โ€ฆ) without that
184
+ harness needing to understand Simocracy lexicons.
185
+
186
+ ---
187
+
188
+ ## Querying sim-authored skills
189
+
190
+ For "show me everything my sim has published":
191
+
192
+ ```ts
193
+ const histories = await fetchHistory() // existing helper
194
+ const mySimSkills = histories.filter(h =>
195
+ h.event.type === "skill" &&
196
+ h.event.simUris?.includes(mySimUri)
197
+ )
198
+ ```
199
+
200
+ Each result has `subjectUri` (the skill URI) and `content`
201
+ (denormalized description). Resolve the skill URI for the full
202
+ record. Same query shape the notifications system already uses
203
+ for chat / comment / proposal events โ€” no new indexer queries
204
+ needed.
205
+
206
+ ---
207
+
208
+ ## Status
209
+
210
+ - โœ… Implemented in pi-simocracy `simocracy_post_skill` (this repo)
211
+ - โณ Renderer changes pending in simocracy-v2 (sim badge on skill
212
+ cards, history-sidecar join in `fetchSkills` / skill detail page)
213
+ - โœ… No new lexicons โ€” uses the existing `org.simocracy.skill` and
214
+ `org.simocracy.history` records as-is
215
+
216
+ The skill + history pair is being written today. Every pi-authored
217
+ skill carries the sim badge (history sidecar) so once
218
+ simocracy-v2's renderer change lands the attribution surfaces
219
+ automatically โ€” no migration, no backfill needed for new
220
+ submissions. Pre-renderer, pi-authored skills appear identically
221
+ to human-authored skills on `simocracy.org/skills`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-simocracy",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "Pi extension: load a Simocracy sim into your chat โ€” see its pixel-art sprite render inline in the terminal and roleplay with it.",
5
5
  "type": "module",
6
6
  "author": "David Dao <david@gainforest.earth> (https://github.com/daviddao)",
package/src/index.ts CHANGED
@@ -61,6 +61,14 @@
61
61
  * `type: "proposal"`. Same write pattern
62
62
  * as `simocracy_post_comment`. Requires
63
63
  * /sim login + sim ownership.
64
+ * - `simocracy_post_skill` Publish an Anthropic-style agent skill
65
+ * (`org.simocracy.skill`) on behalf of
66
+ * the loaded sim, plus an
67
+ * `org.simocracy.history` sidecar with
68
+ * `type: "skill"`. Same write pattern as
69
+ * `simocracy_post_comment` /
70
+ * `simocracy_post_proposal`. Requires
71
+ * /sim login + sim ownership.
64
72
  * - `simocracy_lookup_record` Look up a sim / proposal / gathering /
65
73
  * decision / comment by AT-URI or fuzzy
66
74
  * name and return its details + comment
@@ -132,6 +140,8 @@ import {
132
140
  createProposal,
133
141
  createProposalContext,
134
142
  createProposalHistory,
143
+ createSkill,
144
+ createSkillHistory,
135
145
  createStyle,
136
146
  findRkeyForSim,
137
147
  getAuthenticatedAgent,
@@ -811,6 +821,28 @@ const PostProposalToolParams = Type.Object({
811
821
  ),
812
822
  });
813
823
 
824
+ const PostSkillToolParams = Type.Object({
825
+ name: Type.String({
826
+ description:
827
+ "Skill identifier โ€” lowercase, kebab-case (e.g. `quadratic-funding`). Maps to the `name` field of the SKILL.md YAML frontmatter. Required, max 100 characters.",
828
+ minLength: 1,
829
+ maxLength: 100,
830
+ pattern: "^[a-z][a-z0-9-]*$",
831
+ }),
832
+ description: Type.String({
833
+ description:
834
+ "Skill triggering description โ€” what the skill does AND when an agent should load it. Maps to the `description` field of the SKILL.md YAML frontmatter; this is the primary signal an agent uses to decide whether to load the skill, so make it specific and a little pushy. Required, max 1024 characters.",
835
+ minLength: 1,
836
+ maxLength: 1024,
837
+ }),
838
+ body: Type.String({
839
+ description:
840
+ "Markdown body of SKILL.md, without YAML frontmatter โ€” the actual instructions. Anthropic recommends keeping this under ~500 lines. Required, max 50000 characters.",
841
+ minLength: 1,
842
+ maxLength: 500000,
843
+ }),
844
+ });
845
+
814
846
  const LookupRecordToolParams = Type.Object({
815
847
  query: Type.String({
816
848
  description:
@@ -1757,6 +1789,131 @@ export default async function simocracy(pi: ExtensionAPI) {
1757
1789
  },
1758
1790
  });
1759
1791
 
1792
+ // -------------------------------------------------------------------------
1793
+ // Tool: simocracy_post_skill
1794
+ //
1795
+ // Publish an Anthropic-style agent skill (`org.simocracy.skill`) on
1796
+ // behalf of the loaded sim. Two records are written to the user's
1797
+ // PDS, mirroring `simocracy_post_comment` / `simocracy_post_proposal`:
1798
+ //
1799
+ // 1. org.simocracy.skill the skill record itself, in the same
1800
+ // wire shape simocracy.org's `SkillFormDialog` writes today, so
1801
+ // it renders identically on /skills.
1802
+ // 2. org.simocracy.history sidecar with type="skill",
1803
+ // simUris=[loadedSim], subjectUri=<skill uri>. Renderers that
1804
+ // understand the join show the sim badge; others see a regular
1805
+ // user-authored skill โ€” graceful degradation, zero lexicon
1806
+ // changes.
1807
+ //
1808
+ // See `docs/SIM_AUTHORED_SKILLS.md` for the design rationale.
1809
+ // -------------------------------------------------------------------------
1810
+ pi.registerTool({
1811
+ name: "simocracy_post_skill",
1812
+ label: "Publish a Simocracy skill as the loaded sim",
1813
+ description:
1814
+ "Publish an Anthropic-style agent skill (`org.simocracy.skill`) on behalf of the currently loaded sim. The sim should write the `name` (lowercase kebab-case identifier), `description` (what the skill does AND when an agent should load it โ€” the primary trigger signal), and `body` (the markdown instructions, without YAML frontmatter) in their own voice; their persona is already in your system prompt. Writes TWO records to the user's PDS: (1) the `org.simocracy.skill` record itself in the same shape simocracy.org's SkillFormDialog writes today, and (2) an `org.simocracy.history` sidecar with `type: \"skill\"` attributing the skill to the loaded sim. Use this when the user asks the sim to write, publish, or share a skill โ€” e.g. \"Mr Meow, draft a skill for evaluating cat-sanctuary proposals\" or \"publish a skill on quadratic funding\". The skill appears on simocracy.org/skills and can be loaded into any agent harness via /skills/<did>/<rkey>/skill.md. Anthropic recommends keeping `body` under ~500 lines. Requires /sim login + a loaded sim the user owns.",
1815
+ parameters: PostSkillToolParams,
1816
+ async execute(_id, { name, description, body }) {
1817
+ if (!loadedSim) {
1818
+ throw new Error(
1819
+ "No sim loaded. Call simocracy_load_sim first โ€” skills are published on behalf of a specific sim.",
1820
+ );
1821
+ }
1822
+ let auth;
1823
+ try {
1824
+ auth = await assertCanWriteToSim(loadedSim, {
1825
+ action: "publish a skill as",
1826
+ });
1827
+ } catch (err) {
1828
+ if (err instanceof NotSignedInError || err instanceof NotSimOwnerError) {
1829
+ throw new Error(err.message);
1830
+ }
1831
+ throw err;
1832
+ }
1833
+ let pdsAgent;
1834
+ try {
1835
+ ({ agent: pdsAgent } = await getAuthenticatedAgent());
1836
+ } catch (err) {
1837
+ if (err instanceof NotSignedInError) throw new Error(err.message);
1838
+ throw new Error(`ATProto auth failed: ${(err as Error).message}`);
1839
+ }
1840
+
1841
+ let skill;
1842
+ try {
1843
+ skill = await createSkill({
1844
+ agent: pdsAgent,
1845
+ did: auth.did,
1846
+ name,
1847
+ description,
1848
+ body,
1849
+ });
1850
+ } catch (err) {
1851
+ throw new Error(`Skill write failed: ${(err as Error).message}`);
1852
+ }
1853
+
1854
+ let sidecarUri: string | undefined;
1855
+ let sidecarWarning: string | undefined;
1856
+ try {
1857
+ const history = await createSkillHistory({
1858
+ agent: pdsAgent,
1859
+ did: auth.did,
1860
+ skillUri: skill.uri,
1861
+ skillName: name,
1862
+ skillDescription: description,
1863
+ simUri: loadedSim.uri,
1864
+ simName: loadedSim.name,
1865
+ });
1866
+ sidecarUri = history.uri;
1867
+ } catch (err) {
1868
+ // Don't roll back โ€” the skill is already on the user's PDS.
1869
+ // The sidecar can be re-written later. Surface the warning so
1870
+ // the LLM can decide whether to retry.
1871
+ sidecarWarning = `Sim-attribution sidecar failed: ${(err as Error).message}`;
1872
+ }
1873
+
1874
+ // Public URLs the LLM can hand back to the user. The skill page
1875
+ // is served by simocracy-v2's /skills/[did]/[rkey] route; the
1876
+ // .md endpoint reconstructs the full SKILL.md (frontmatter + body)
1877
+ // for any agent harness that wants to load it.
1878
+ const did = encodeURIComponent(loadedSim.did);
1879
+ const rkey = encodeURIComponent(skill.rkey);
1880
+ const pageUrl = `https://www.simocracy.org/skills/${did}/${rkey}`;
1881
+ const skillMdUrl = `https://www.simocracy.org/skills/${did}/${rkey}/skill.md`;
1882
+
1883
+ const lines = [
1884
+ `Published skill as ${loadedSim.name}${loadedSim.handle ? ` (@${loadedSim.handle})` : ""}:`,
1885
+ ` name: ${name}`,
1886
+ ` skill URI: ${skill.uri}`,
1887
+ ` page: ${pageUrl}`,
1888
+ ` SKILL.md: ${skillMdUrl}`,
1889
+ ];
1890
+ if (sidecarUri) {
1891
+ lines.push(` attribution: ${sidecarUri} (org.simocracy.history sidecar)`);
1892
+ } else if (sidecarWarning) {
1893
+ lines.push(` WARNING: ${sidecarWarning}`);
1894
+ lines.push(
1895
+ ` The skill is published but will appear unattributed until a history sidecar is written.`,
1896
+ );
1897
+ }
1898
+ return {
1899
+ content: [{ type: "text" as const, text: lines.join("\n") }],
1900
+ details: {
1901
+ skillUri: skill.uri,
1902
+ skillRkey: skill.rkey,
1903
+ skillCid: skill.cid,
1904
+ name,
1905
+ description,
1906
+ pageUrl,
1907
+ skillMdUrl,
1908
+ simUri: loadedSim.uri,
1909
+ simName: loadedSim.name,
1910
+ sidecarUri,
1911
+ sidecarWarning,
1912
+ },
1913
+ };
1914
+ },
1915
+ });
1916
+
1760
1917
  // -------------------------------------------------------------------------
1761
1918
  // Tool: simocracy_lookup_record
1762
1919
  //
package/src/writes.ts CHANGED
@@ -131,6 +131,7 @@ const COLLECTION_COMMENT = "org.impactindexer.review.comment";
131
131
  const COLLECTION_HISTORY = "org.simocracy.history";
132
132
  const COLLECTION_PROPOSAL = "org.hypercerts.claim.activity";
133
133
  const COLLECTION_PROPOSAL_CONTEXT = "org.simocracy.proposalContext";
134
+ const COLLECTION_SKILL = "org.simocracy.skill";
134
135
 
135
136
  /**
136
137
  * Defense-in-depth: every write helper below verifies the target
@@ -594,6 +595,117 @@ export async function createProposalHistory(opts: {
594
595
  };
595
596
  }
596
597
 
598
+ /**
599
+ * POST `org.simocracy.skill` (an Anthropic-style agent skill).
600
+ *
601
+ * The lexicon stores the SKILL.md frontmatter (`name`, `description`)
602
+ * as separate fields and the markdown body in `body`, so the indexer
603
+ * can filter on metadata cheaply without parsing markdown. The full
604
+ * SKILL.md is reconstructed at serve time by simocracy.org's
605
+ * `/skills/[did]/[rkey]/skill.md` route.
606
+ *
607
+ * Skills are NOT 1:1 with sims โ€” the lexicon has no `sim` ref. They
608
+ * live in the *user's* own PDS exactly the way simocracy.org's
609
+ * `SkillFormDialog` writes them today, so a sim-authored skill
610
+ * renders identically to a human-authored skill on the gallery.
611
+ * Sim attribution is a sidecar `org.simocracy.history` record
612
+ * written by `createSkillHistory` below โ€” same pattern as comments
613
+ * and proposals. See `docs/SIM_AUTHORED_SKILLS.md` for the design.
614
+ */
615
+ export async function createSkill(opts: {
616
+ agent: Agent;
617
+ did: string;
618
+ name: string;
619
+ description: string;
620
+ body: string;
621
+ }): Promise<{ uri: string; cid: string; rkey: string }> {
622
+ const name = opts.name.trim();
623
+ if (!name) throw new Error("Skill name is required.");
624
+ const description = opts.description.trim();
625
+ if (!description) throw new Error("Skill description is required.");
626
+ const body = opts.body.trim();
627
+ if (!body) throw new Error("Skill body is required.");
628
+ // Lexicon caps (mirrored from lexicons/org/simocracy/skill.json):
629
+ // name โ‰ค 100 graphemes (maxLength 1000)
630
+ // description โ‰ค 1024 graphemes (maxLength 10000)
631
+ // body โ‰ค 50000 graphemes (maxLength 500000)
632
+ // Slice on JS string length is a conservative approximation of grapheme
633
+ // count โ€” it never exceeds the limit, occasionally trims early on rare
634
+ // multi-codepoint clusters. Same approach used elsewhere in this module.
635
+ const record = {
636
+ $type: COLLECTION_SKILL,
637
+ name: name.slice(0, 1000),
638
+ description: description.slice(0, 10000),
639
+ body: body.slice(0, 500000),
640
+ createdAt: new Date().toISOString(),
641
+ };
642
+ const res = await opts.agent.com.atproto.repo.createRecord({
643
+ repo: opts.did,
644
+ collection: COLLECTION_SKILL,
645
+ record,
646
+ });
647
+ return {
648
+ uri: res.data.uri,
649
+ cid: res.data.cid,
650
+ rkey: res.data.uri.split("/").pop() ?? "",
651
+ };
652
+ }
653
+
654
+ /**
655
+ * Sim-attribution sidecar for a skill.
656
+ *
657
+ * Mirrors `createCommentHistory` and `createProposalHistory` exactly โ€”
658
+ * same `org.simocracy.history` lexicon, same join-key shape, just
659
+ * `type: "skill"` and `subjectCollection: "org.simocracy.skill"`.
660
+ * The lexicon's `type` field is free-form string and the indexer
661
+ * already accepts new event types as they appear (history.json
662
+ * documents this explicitly).
663
+ *
664
+ * Writes to the *user's* own PDS โ€” the attribution is an event the
665
+ * user triggered and naturally belongs in their history.
666
+ */
667
+ export async function createSkillHistory(opts: {
668
+ agent: Agent;
669
+ did: string;
670
+ skillUri: string;
671
+ skillName: string;
672
+ skillDescription: string;
673
+ simUri: string;
674
+ simName: string;
675
+ }): Promise<{ uri: string; cid: string; rkey: string }> {
676
+ // Defense-in-depth: the sim must live in the same repo we're writing
677
+ // to. The skill record itself isn't sim-owned, but the history
678
+ // sidecar *claims attribution to* a sim โ€” only the sim's owner can
679
+ // make that claim.
680
+ assertRepoOwnsSimUri(opts.did, opts.simUri);
681
+ const skillName = opts.skillName.trim();
682
+ const description = opts.skillDescription.trim();
683
+ const record: Record<string, unknown> = {
684
+ $type: COLLECTION_HISTORY,
685
+ type: "skill",
686
+ actorDid: opts.did,
687
+ simNames: [opts.simName].slice(0, 10),
688
+ simUris: [opts.simUri].slice(0, 10),
689
+ subjectUri: opts.skillUri,
690
+ subjectCollection: COLLECTION_SKILL,
691
+ subjectName: skillName.slice(0, 500),
692
+ createdAt: new Date().toISOString(),
693
+ };
694
+ if (description) {
695
+ record.content = description.slice(0, 5000);
696
+ }
697
+ const res = await opts.agent.com.atproto.repo.createRecord({
698
+ repo: opts.did,
699
+ collection: COLLECTION_HISTORY,
700
+ record,
701
+ });
702
+ return {
703
+ uri: res.data.uri,
704
+ cid: res.data.cid,
705
+ rkey: res.data.uri.split("/").pop() ?? "",
706
+ };
707
+ }
708
+
597
709
  /**
598
710
  * Best-effort lookup of an existing rkey by listing the collection
599
711
  * and finding the record whose `sim.uri` matches. Used by the Apply