pi-simocracy 0.6.2 โ 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 +10 -1
- package/docs/SIM_AUTHORED_PROPOSALS.md +47 -12
- package/docs/SIM_AUTHORED_SKILLS.md +221 -0
- package/package.json +1 -1
- package/src/index.ts +288 -2
- package/src/simocracy.ts +28 -0
- package/src/writes.ts +193 -0
package/README.md
CHANGED
|
@@ -39,7 +39,8 @@ pi install npm:pi-simocracy
|
|
|
39
39
|
| `simocracy_chat` | Send one message to a sim and get a quoted reply, **without** changing the active session persona. Needs `OPENROUTER_API_KEY`. |
|
|
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
|
-
| `simocracy_post_proposal` | Submit a new funding proposal (`org.hypercerts.claim.activity`) **as the loaded sim**. Writes the proposal
|
|
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).
|
|
@@ -12,7 +12,7 @@ subject collection.
|
|
|
12
12
|
## TL;DR
|
|
13
13
|
|
|
14
14
|
When pi submits a proposal on behalf of a loaded sim, it writes
|
|
15
|
-
**
|
|
15
|
+
**three** records to the user's PDS:
|
|
16
16
|
|
|
17
17
|
1. **`org.hypercerts.claim.activity`** โ the proposal itself, in the
|
|
18
18
|
exact shape simocracy.org's `ProposalFormDialog` already writes
|
|
@@ -20,16 +20,27 @@ When pi submits a proposal on behalf of a loaded sim, it writes
|
|
|
20
20
|
`workScope` / `contributors` / `image`, `createdAt`). Old readers
|
|
21
21
|
(Hyperindexer, the existing webapp) see this as a regular
|
|
22
22
|
user-authored proposal โ graceful degradation.
|
|
23
|
-
2. **`org.simocracy.
|
|
23
|
+
2. **`org.simocracy.proposalContext`** โ sidecar binding the proposal
|
|
24
|
+
to its parent gathering (StrongRef to `org.simocracy.gathering`)
|
|
25
|
+
or to a Frontier Tower SF floor (`floorNumber` integer). Required
|
|
26
|
+
for visibility: post-Phase-5, simocracy.org's `/proposals` feed
|
|
27
|
+
sources its seed set from `org.simocracy.proposalContext` records,
|
|
28
|
+
not from a hashtag scan, so a proposal without this sidecar is
|
|
29
|
+
invisible. Lives in the proposer's PDS so the resolver's tier-1
|
|
30
|
+
(proposer) > tier-2 (facilitator backfill) precedence works.
|
|
31
|
+
3. **`org.simocracy.history`** โ sidecar record with `type:
|
|
24
32
|
"proposal"`, `subjectUri` pointing at the proposal we just wrote,
|
|
25
33
|
`simUris[]` / `simNames[]` declaring which sim spoke. New `type`
|
|
26
34
|
value, but the lexicon's `type` field is free-form string and the
|
|
27
35
|
indexer already accepts new event types as they appear.
|
|
28
36
|
|
|
29
|
-
Renderers that understand the join (simocracy.org,
|
|
30
|
-
change
|
|
37
|
+
Renderers that understand the history join (simocracy.org, since
|
|
38
|
+
the planned change landed) display a sim badge on the proposal card;
|
|
31
39
|
renderers that don't keep showing it as a regular user proposal.
|
|
32
|
-
**Zero hypercerts lexicon changes
|
|
40
|
+
**Zero hypercerts lexicon changes.** One new Simocracy lexicon was
|
|
41
|
+
added for proposalContext (see `simocracy-v2/lexicons/org/simocracy/proposalContext.json`)
|
|
42
|
+
so every Simocracy-bound proposal carries a typed back-pointer to
|
|
43
|
+
its parent.
|
|
33
44
|
|
|
34
45
|
---
|
|
35
46
|
|
|
@@ -67,6 +78,17 @@ Use it as-is.
|
|
|
67
78
|
Implemented by `simocracy_post_proposal` in `src/index.ts`:
|
|
68
79
|
|
|
69
80
|
```ts
|
|
81
|
+
// Tool input โ exactly one of gatheringUri / ftcSfFloor must be set.
|
|
82
|
+
// gatheringUri is fetched ahead of any writes so a typo or unreachable
|
|
83
|
+
// PDS surfaces *before* an orphaned proposal lands in the user's repo.
|
|
84
|
+
const gatheringRef =
|
|
85
|
+
gatheringUri
|
|
86
|
+
? await getRecordRefFromPds(parsed.did, "org.simocracy.gathering", parsed.rkey)
|
|
87
|
+
: undefined;
|
|
88
|
+
const resolvedContext = gatheringRef
|
|
89
|
+
? { kind: "gathering" as const, uri: gatheringRef.uri, cid: gatheringRef.cid }
|
|
90
|
+
: { kind: "ftc-sf" as const, floorNumber: ftcSfFloor };
|
|
91
|
+
|
|
70
92
|
// 1. The proposal โ same shape as ProposalFormDialog writes today.
|
|
71
93
|
const proposal = await createProposal({
|
|
72
94
|
agent, did,
|
|
@@ -78,7 +100,17 @@ const proposal = await createProposal({
|
|
|
78
100
|
image: { $type: "org.hypercerts.defs#uri", uri: imageUri ?? DEFAULT_BANNER },
|
|
79
101
|
});
|
|
80
102
|
|
|
81
|
-
// 2. The
|
|
103
|
+
// 2. The parent-context sidecar โ required for visibility on /proposals.
|
|
104
|
+
// A failure here doesn't roll back the proposal but is surfaced as
|
|
105
|
+
// a `contextSidecarWarning` so the LLM can retry just the sidecar.
|
|
106
|
+
await createProposalContext({
|
|
107
|
+
agent, did,
|
|
108
|
+
proposalUri: proposal.uri,
|
|
109
|
+
proposalCid: proposal.cid,
|
|
110
|
+
context: resolvedContext,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// 3. The sim-attribution sidecar โ required, since attribution is the whole
|
|
82
114
|
// point of this tool.
|
|
83
115
|
await createProposalHistory({
|
|
84
116
|
agent, did,
|
|
@@ -190,9 +222,12 @@ chat / hearing / sprocess events โ no new indexer queries needed.
|
|
|
190
222
|
## Status
|
|
191
223
|
|
|
192
224
|
- โ
Implemented in pi-simocracy `simocracy_post_proposal` (this repo)
|
|
193
|
-
-
|
|
194
|
-
-
|
|
195
|
-
|
|
196
|
-
The proposal + history
|
|
197
|
-
|
|
198
|
-
|
|
225
|
+
- โ
Renderer changes shipped in simocracy-v2
|
|
226
|
+
- โ
`org.simocracy.proposalContext` lexicon shipped (1 new Simocracy lexicon, 0 hypercerts changes)
|
|
227
|
+
|
|
228
|
+
The proposal + proposalContext + history triple is being written
|
|
229
|
+
today. Every pi-authored proposal carries both the sim badge
|
|
230
|
+
(history sidecar) and the parent-gathering binding
|
|
231
|
+
(proposalContext sidecar), so it surfaces correctly on
|
|
232
|
+
`simocracy.org/proposals` and under its parent gathering page โ no
|
|
233
|
+
migration, no backfill needed for new submissions.
|
|
@@ -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.
|
|
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
|
|
@@ -98,6 +106,7 @@ import {
|
|
|
98
106
|
fetchSkillMd,
|
|
99
107
|
resolveHandle,
|
|
100
108
|
parseAtUri,
|
|
109
|
+
getRecordRefFromPds,
|
|
101
110
|
type AgentsRecord,
|
|
102
111
|
type SimMatch,
|
|
103
112
|
type StyleRecord,
|
|
@@ -129,7 +138,10 @@ import {
|
|
|
129
138
|
createComment,
|
|
130
139
|
createCommentHistory,
|
|
131
140
|
createProposal,
|
|
141
|
+
createProposalContext,
|
|
132
142
|
createProposalHistory,
|
|
143
|
+
createSkill,
|
|
144
|
+
createSkillHistory,
|
|
133
145
|
createStyle,
|
|
134
146
|
findRkeyForSim,
|
|
135
147
|
getAuthenticatedAgent,
|
|
@@ -137,6 +149,7 @@ import {
|
|
|
137
149
|
NotSimOwnerError,
|
|
138
150
|
updateAgents,
|
|
139
151
|
updateStyle,
|
|
152
|
+
type ProposalContextTarget,
|
|
140
153
|
} from "./writes.ts";
|
|
141
154
|
|
|
142
155
|
// ---------------------------------------------------------------------------
|
|
@@ -787,6 +800,47 @@ const PostProposalToolParams = Type.Object({
|
|
|
787
800
|
"https URL for the cover image. Defaults to the Simocracy banner if omitted. Image upload from disk is not supported โ pass a URL or leave blank.",
|
|
788
801
|
}),
|
|
789
802
|
),
|
|
803
|
+
// Parent-context binding โ effectively required for the proposal to
|
|
804
|
+
// be discoverable on simocracy.org (post-Phase-5 the read paths only
|
|
805
|
+
// surface proposals that have a sidecar). Exactly one of the two must
|
|
806
|
+
// be passed; the execute handler enforces that.
|
|
807
|
+
gatheringUri: Type.Optional(
|
|
808
|
+
Type.String({
|
|
809
|
+
description:
|
|
810
|
+
"AT-URI of the parent gathering this proposal belongs to (an `org.simocracy.gathering` record). Pass exactly one of `gatheringUri` or `ftcSfFloor`. Get the URI by calling `simocracy_lookup_record` first โ the LLM should resolve a fuzzy gathering name to a real AT-URI before submitting. Without a parent context, the proposal will be invisible on simocracy.org's `/proposals` feed.",
|
|
811
|
+
pattern: "^at://[^/]+/org\\.simocracy\\.gathering/[^/]+$",
|
|
812
|
+
}),
|
|
813
|
+
),
|
|
814
|
+
ftcSfFloor: Type.Optional(
|
|
815
|
+
Type.Integer({
|
|
816
|
+
description:
|
|
817
|
+
"Frontier Tower SF floor number this proposal belongs to (1โ14, with the static configuration in simocracy-v2's `lib/ftc-sf-data.ts`). Pass exactly one of `gatheringUri` or `ftcSfFloor`. Use this for proposals submitted to the Frontier Tower SF agentic funding experiment instead of a regular gathering.",
|
|
818
|
+
minimum: 1,
|
|
819
|
+
maximum: 14,
|
|
820
|
+
}),
|
|
821
|
+
),
|
|
822
|
+
});
|
|
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
|
+
}),
|
|
790
844
|
});
|
|
791
845
|
|
|
792
846
|
const LookupRecordToolParams = Type.Object({
|
|
@@ -1492,11 +1546,21 @@ export default async function simocracy(pi: ExtensionAPI) {
|
|
|
1492
1546
|
name: "simocracy_post_proposal",
|
|
1493
1547
|
label: "Submit a Simocracy proposal as the loaded sim",
|
|
1494
1548
|
description:
|
|
1495
|
-
"Submit a new funding proposal to Simocracy on behalf of the currently loaded sim. The sim should write the title + shortDescription + description in their own voice (their persona is already in your system prompt). Writes
|
|
1549
|
+
"Submit a new funding proposal to Simocracy on behalf of the currently loaded sim. The sim should write the title + shortDescription + description in their own voice (their persona is already in your system prompt). Writes THREE records to the user's PDS: (1) the proposal itself, (2) an `org.simocracy.proposalContext` sidecar binding the proposal to its parent gathering / FtC SF floor (required for the proposal to appear in simocracy.org's `/proposals` feed), and (3) an `org.simocracy.history` sidecar attributing the draft to the loaded sim. Use this when the user asks the sim to draft, propose, or submit a proposal โ e.g. \"Mr Meow, propose a cat sanctuary\" or \"draft a proposal for solar panels\". You MUST pass exactly one of `gatheringUri` (to bind the proposal to a gathering) or `ftcSfFloor` (to bind to a Frontier Tower SF floor); call `simocracy_lookup_record` first to resolve a gathering name to its AT-URI. Pass `budgetItems` if a budget request was discussed; pass `workScope` for tag-style categorization; pass `contributors` for credited humans. Image is optional and URL-only (the default Simocracy banner is used otherwise). Requires /sim login + a loaded sim the user owns.",
|
|
1496
1550
|
parameters: PostProposalToolParams,
|
|
1497
1551
|
async execute(
|
|
1498
1552
|
_id,
|
|
1499
|
-
{
|
|
1553
|
+
{
|
|
1554
|
+
title,
|
|
1555
|
+
shortDescription,
|
|
1556
|
+
description,
|
|
1557
|
+
workScope,
|
|
1558
|
+
contributors,
|
|
1559
|
+
budgetItems,
|
|
1560
|
+
imageUri,
|
|
1561
|
+
gatheringUri,
|
|
1562
|
+
ftcSfFloor,
|
|
1563
|
+
},
|
|
1500
1564
|
) {
|
|
1501
1565
|
if (!loadedSim) {
|
|
1502
1566
|
throw new Error(
|
|
@@ -1520,6 +1584,64 @@ export default async function simocracy(pi: ExtensionAPI) {
|
|
|
1520
1584
|
throw new Error(`ATProto auth failed: ${(err as Error).message}`);
|
|
1521
1585
|
}
|
|
1522
1586
|
|
|
1587
|
+
// Parent-context validation โ exactly one of gatheringUri / ftcSfFloor.
|
|
1588
|
+
// Without one, the proposal is invisible on /proposals, so we require
|
|
1589
|
+
// it up-front rather than letting the LLM ship orphaned proposals.
|
|
1590
|
+
const hasGathering = gatheringUri !== undefined;
|
|
1591
|
+
const hasFloor = ftcSfFloor !== undefined;
|
|
1592
|
+
if (hasGathering && hasFloor) {
|
|
1593
|
+
throw new Error(
|
|
1594
|
+
"Pass exactly one of `gatheringUri` (for a gathering) or `ftcSfFloor` (for FtC SF), not both.",
|
|
1595
|
+
);
|
|
1596
|
+
}
|
|
1597
|
+
if (!hasGathering && !hasFloor) {
|
|
1598
|
+
throw new Error(
|
|
1599
|
+
"A parent context is required: pass `gatheringUri` (an AT-URI to an `org.simocracy.gathering` โ use `simocracy_lookup_record` to resolve a name) or `ftcSfFloor` (a Frontier Tower SF floor number 1โ14). Without one, the proposal will be invisible on simocracy.org's /proposals feed.",
|
|
1600
|
+
);
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
// Resolve gathering URI โ StrongRef now (before any writes) so a
|
|
1604
|
+
// typo or unreachable PDS surfaces *before* we put a half-orphaned
|
|
1605
|
+
// proposal record into the user's repo.
|
|
1606
|
+
let resolvedContext: ProposalContextTarget;
|
|
1607
|
+
if (hasGathering) {
|
|
1608
|
+
let gatheringDid: string;
|
|
1609
|
+
let gatheringRkey: string;
|
|
1610
|
+
try {
|
|
1611
|
+
const parsed = parseAtUri(gatheringUri!);
|
|
1612
|
+
if (parsed.collection !== "org.simocracy.gathering") {
|
|
1613
|
+
throw new Error(
|
|
1614
|
+
`gatheringUri must point at an org.simocracy.gathering record (got ${parsed.collection}).`,
|
|
1615
|
+
);
|
|
1616
|
+
}
|
|
1617
|
+
gatheringDid = parsed.did;
|
|
1618
|
+
gatheringRkey = parsed.rkey;
|
|
1619
|
+
} catch (err) {
|
|
1620
|
+
throw new Error(
|
|
1621
|
+
`gatheringUri is not a valid AT-URI: ${(err as Error).message}`,
|
|
1622
|
+
);
|
|
1623
|
+
}
|
|
1624
|
+
let gatheringRef: { uri: string; cid: string };
|
|
1625
|
+
try {
|
|
1626
|
+
gatheringRef = await getRecordRefFromPds(
|
|
1627
|
+
gatheringDid,
|
|
1628
|
+
"org.simocracy.gathering",
|
|
1629
|
+
gatheringRkey,
|
|
1630
|
+
);
|
|
1631
|
+
} catch (err) {
|
|
1632
|
+
throw new Error(
|
|
1633
|
+
`Couldn't fetch gathering record from owner's PDS โ verify the URI is correct and the gathering still exists. ${(err as Error).message}`,
|
|
1634
|
+
);
|
|
1635
|
+
}
|
|
1636
|
+
resolvedContext = {
|
|
1637
|
+
kind: "gathering",
|
|
1638
|
+
uri: gatheringRef.uri,
|
|
1639
|
+
cid: gatheringRef.cid,
|
|
1640
|
+
};
|
|
1641
|
+
} else {
|
|
1642
|
+
resolvedContext = { kind: "ftc-sf", floorNumber: ftcSfFloor! };
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1523
1645
|
// Resolve the cover image โ either an LLM-supplied https URL or
|
|
1524
1646
|
// the simocracy.org default banner. Reject non-https schemes
|
|
1525
1647
|
// (data:, javascript:, file://) defensively even though the only
|
|
@@ -1573,6 +1695,26 @@ export default async function simocracy(pi: ExtensionAPI) {
|
|
|
1573
1695
|
throw new Error(`Proposal write failed: ${(err as Error).message}`);
|
|
1574
1696
|
}
|
|
1575
1697
|
|
|
1698
|
+
// Parent-context sidecar โ written FIRST after the proposal so a
|
|
1699
|
+
// visibility failure is loud and immediate. If this fails the
|
|
1700
|
+
// proposal is already on the user's PDS but won't surface on
|
|
1701
|
+
// /proposals; we surface a warning so the LLM can retry just this
|
|
1702
|
+
// sidecar instead of re-submitting the whole proposal.
|
|
1703
|
+
let contextSidecarUri: string | undefined;
|
|
1704
|
+
let contextSidecarWarning: string | undefined;
|
|
1705
|
+
try {
|
|
1706
|
+
const ctxWrite = await createProposalContext({
|
|
1707
|
+
agent: pdsAgent,
|
|
1708
|
+
did: auth.did,
|
|
1709
|
+
proposalUri: proposal.uri,
|
|
1710
|
+
proposalCid: proposal.cid,
|
|
1711
|
+
context: resolvedContext,
|
|
1712
|
+
});
|
|
1713
|
+
contextSidecarUri = ctxWrite.uri;
|
|
1714
|
+
} catch (err) {
|
|
1715
|
+
contextSidecarWarning = `Parent-context sidecar failed: ${(err as Error).message}`;
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1576
1718
|
let sidecarUri: string | undefined;
|
|
1577
1719
|
let sidecarWarning: string | undefined;
|
|
1578
1720
|
try {
|
|
@@ -1593,12 +1735,25 @@ export default async function simocracy(pi: ExtensionAPI) {
|
|
|
1593
1735
|
sidecarWarning = `Sim-attribution sidecar failed: ${(err as Error).message}`;
|
|
1594
1736
|
}
|
|
1595
1737
|
|
|
1738
|
+
const parentLabel =
|
|
1739
|
+
resolvedContext.kind === "gathering"
|
|
1740
|
+
? `gathering ${resolvedContext.uri}`
|
|
1741
|
+
: `FtC SF floor ${resolvedContext.floorNumber}`;
|
|
1596
1742
|
const lines = [
|
|
1597
1743
|
`Submitted proposal as ${loadedSim.name}${loadedSim.handle ? ` (@${loadedSim.handle})` : ""}:`,
|
|
1598
1744
|
` title: ${title}`,
|
|
1599
1745
|
` proposal URI: ${proposal.uri}`,
|
|
1746
|
+
` parent: ${parentLabel}`,
|
|
1600
1747
|
` image: ${imageRef.uri}`,
|
|
1601
1748
|
];
|
|
1749
|
+
if (contextSidecarUri) {
|
|
1750
|
+
lines.push(` context: ${contextSidecarUri} (org.simocracy.proposalContext sidecar)`);
|
|
1751
|
+
} else if (contextSidecarWarning) {
|
|
1752
|
+
lines.push(` WARNING: ${contextSidecarWarning}`);
|
|
1753
|
+
lines.push(
|
|
1754
|
+
` The proposal is posted but will be invisible on /proposals until a proposalContext sidecar is written.`,
|
|
1755
|
+
);
|
|
1756
|
+
}
|
|
1602
1757
|
if (sidecarUri) {
|
|
1603
1758
|
lines.push(` attribution: ${sidecarUri} (org.simocracy.history sidecar)`);
|
|
1604
1759
|
} else if (sidecarWarning) {
|
|
@@ -1621,6 +1776,137 @@ export default async function simocracy(pi: ExtensionAPI) {
|
|
|
1621
1776
|
budgetItemCount: budgetItems?.length ?? 0,
|
|
1622
1777
|
simUri: loadedSim.uri,
|
|
1623
1778
|
simName: loadedSim.name,
|
|
1779
|
+
parent:
|
|
1780
|
+
resolvedContext.kind === "gathering"
|
|
1781
|
+
? { kind: "gathering", uri: resolvedContext.uri, cid: resolvedContext.cid }
|
|
1782
|
+
: { kind: "ftc-sf", floorNumber: resolvedContext.floorNumber },
|
|
1783
|
+
contextSidecarUri,
|
|
1784
|
+
contextSidecarWarning,
|
|
1785
|
+
sidecarUri,
|
|
1786
|
+
sidecarWarning,
|
|
1787
|
+
},
|
|
1788
|
+
};
|
|
1789
|
+
},
|
|
1790
|
+
});
|
|
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,
|
|
1624
1910
|
sidecarUri,
|
|
1625
1911
|
sidecarWarning,
|
|
1626
1912
|
},
|
package/src/simocracy.ts
CHANGED
|
@@ -252,6 +252,34 @@ export async function getRecordFromPds<T>(did: string, collection: string, rkey:
|
|
|
252
252
|
return json.value as T;
|
|
253
253
|
}
|
|
254
254
|
|
|
255
|
+
/**
|
|
256
|
+
* Read a record's StrongRef ({uri, cid}) directly from the owner's PDS.
|
|
257
|
+
*
|
|
258
|
+
* Same wire call as `getRecordFromPds` but returns the metadata needed to
|
|
259
|
+
* reference the record from another record (e.g. an `org.simocracy.proposalContext`
|
|
260
|
+
* sidecar's `subject` or `context.gathering` field). The body is intentionally
|
|
261
|
+
* dropped โ callers that need it should use `getRecordFromPds` instead.
|
|
262
|
+
*/
|
|
263
|
+
export async function getRecordRefFromPds(
|
|
264
|
+
did: string,
|
|
265
|
+
collection: string,
|
|
266
|
+
rkey: string,
|
|
267
|
+
): Promise<{ uri: string; cid: string }> {
|
|
268
|
+
const pds = await resolvePds(did);
|
|
269
|
+
const url =
|
|
270
|
+
`${pds.replace(/\/+$/, "")}/xrpc/com.atproto.repo.getRecord` +
|
|
271
|
+
`?repo=${encodeURIComponent(did)}` +
|
|
272
|
+
`&collection=${encodeURIComponent(collection)}` +
|
|
273
|
+
`&rkey=${encodeURIComponent(rkey)}`;
|
|
274
|
+
const res = await fetch(url);
|
|
275
|
+
if (!res.ok) throw new Error(`PDS getRecord failed: ${res.status}`);
|
|
276
|
+
const json = (await res.json()) as { uri?: string; cid?: string };
|
|
277
|
+
if (!json.uri || !json.cid) {
|
|
278
|
+
throw new Error(`PDS getRecord returned no uri/cid: ${url}`);
|
|
279
|
+
}
|
|
280
|
+
return { uri: json.uri, cid: json.cid };
|
|
281
|
+
}
|
|
282
|
+
|
|
255
283
|
/** List records by paging com.atproto.repo.listRecords on a PDS. */
|
|
256
284
|
export async function listRecordsFromPds<T>(did: string, collection: string): Promise<Array<{ uri: string; cid: string; value: T }>> {
|
|
257
285
|
const pds = await resolvePds(did);
|
package/src/writes.ts
CHANGED
|
@@ -130,6 +130,8 @@ const COLLECTION_STYLE = "org.simocracy.style";
|
|
|
130
130
|
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
|
+
const COLLECTION_PROPOSAL_CONTEXT = "org.simocracy.proposalContext";
|
|
134
|
+
const COLLECTION_SKILL = "org.simocracy.skill";
|
|
133
135
|
|
|
134
136
|
/**
|
|
135
137
|
* Defense-in-depth: every write helper below verifies the target
|
|
@@ -455,6 +457,86 @@ export async function createProposal(opts: {
|
|
|
455
457
|
};
|
|
456
458
|
}
|
|
457
459
|
|
|
460
|
+
/**
|
|
461
|
+
* Discriminated parent target for an `org.simocracy.proposalContext`
|
|
462
|
+
* sidecar. Mirrors the lexicon's `context` union (#gatheringContext
|
|
463
|
+
* vs #ftcSfContext); see `simocracy-v2/lexicons/org/simocracy/proposalContext.json`.
|
|
464
|
+
*/
|
|
465
|
+
export type ProposalContextTarget =
|
|
466
|
+
| { kind: "gathering"; uri: string; cid: string }
|
|
467
|
+
| { kind: "ftc-sf"; floorNumber: number };
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* POST `org.simocracy.proposalContext` (parent-context sidecar).
|
|
471
|
+
*
|
|
472
|
+
* Binds a proposal to its parent container โ either an
|
|
473
|
+
* `org.simocracy.gathering` record (via StrongRef) or a static FtC SF
|
|
474
|
+
* floor number. Without this sidecar, simocracy.org's read paths
|
|
475
|
+
* (post-Phase-5) won't surface the proposal on `/proposals` or under
|
|
476
|
+
* the gathering / floor it belongs to. The sidecar is therefore
|
|
477
|
+
* effectively required for any proposal submitted via this tool.
|
|
478
|
+
*
|
|
479
|
+
* Same trust model as `org.simocracy.history` โ lives in the
|
|
480
|
+
* proposer's own PDS so the resolver's tier-1 (proposer-PDS) >
|
|
481
|
+
* tier-2 (facilitator-PDS) precedence rule lets a backfill record
|
|
482
|
+
* be silently superseded if the proposer ever re-saves.
|
|
483
|
+
*
|
|
484
|
+
* No sim-ownership precondition: a proposal isn't sim-owned, and
|
|
485
|
+
* the sidecar references the proposal + parent gathering, not the
|
|
486
|
+
* sim. The OAuth precondition ($DID matches signed-in DID and the
|
|
487
|
+
* agent is authenticated) is enforced upstream by
|
|
488
|
+
* `assertCanWriteToSim` at the tool entry point.
|
|
489
|
+
*/
|
|
490
|
+
export async function createProposalContext(opts: {
|
|
491
|
+
agent: Agent;
|
|
492
|
+
did: string;
|
|
493
|
+
proposalUri: string;
|
|
494
|
+
proposalCid: string;
|
|
495
|
+
context: ProposalContextTarget;
|
|
496
|
+
}): Promise<{ uri: string; cid: string; rkey: string }> {
|
|
497
|
+
let context: Record<string, unknown>;
|
|
498
|
+
if (opts.context.kind === "gathering") {
|
|
499
|
+
if (!opts.context.uri || !opts.context.cid) {
|
|
500
|
+
throw new Error(
|
|
501
|
+
"Gathering parent context requires both uri and cid (a full StrongRef).",
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
context = {
|
|
505
|
+
$type: `${COLLECTION_PROPOSAL_CONTEXT}#gatheringContext`,
|
|
506
|
+
gathering: { uri: opts.context.uri, cid: opts.context.cid },
|
|
507
|
+
};
|
|
508
|
+
} else {
|
|
509
|
+
if (
|
|
510
|
+
!Number.isInteger(opts.context.floorNumber) ||
|
|
511
|
+
opts.context.floorNumber < 1
|
|
512
|
+
) {
|
|
513
|
+
throw new Error(
|
|
514
|
+
`FtC SF floor number must be a positive integer (got ${opts.context.floorNumber}).`,
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
context = {
|
|
518
|
+
$type: `${COLLECTION_PROPOSAL_CONTEXT}#ftcSfContext`,
|
|
519
|
+
floorNumber: opts.context.floorNumber,
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
const record = {
|
|
523
|
+
$type: COLLECTION_PROPOSAL_CONTEXT,
|
|
524
|
+
subject: { uri: opts.proposalUri, cid: opts.proposalCid },
|
|
525
|
+
context,
|
|
526
|
+
createdAt: new Date().toISOString(),
|
|
527
|
+
};
|
|
528
|
+
const res = await opts.agent.com.atproto.repo.createRecord({
|
|
529
|
+
repo: opts.did,
|
|
530
|
+
collection: COLLECTION_PROPOSAL_CONTEXT,
|
|
531
|
+
record,
|
|
532
|
+
});
|
|
533
|
+
return {
|
|
534
|
+
uri: res.data.uri,
|
|
535
|
+
cid: res.data.cid,
|
|
536
|
+
rkey: res.data.uri.split("/").pop() ?? "",
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
|
|
458
540
|
/**
|
|
459
541
|
* Sim-attribution sidecar for a proposal.
|
|
460
542
|
*
|
|
@@ -513,6 +595,117 @@ export async function createProposalHistory(opts: {
|
|
|
513
595
|
};
|
|
514
596
|
}
|
|
515
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
|
+
|
|
516
709
|
/**
|
|
517
710
|
* Best-effort lookup of an existing rkey by listing the collection
|
|
518
711
|
* and finding the record whose `sim.uri` matches. Used by the Apply
|