@possumtech/rummy 2.1.0 → 2.2.1

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.
Files changed (140) hide show
  1. package/.env.example +40 -15
  2. package/.xai.key +1 -0
  3. package/PLUGINS.md +169 -53
  4. package/README.md +38 -32
  5. package/SPEC.md +366 -179
  6. package/bin/digest.js +1097 -0
  7. package/biome/no-fallbacks.grit +2 -2
  8. package/gemini.key +1 -0
  9. package/lang/en.json +10 -1
  10. package/migrations/001_initial_schema.sql +9 -2
  11. package/package.json +19 -8
  12. package/service.js +1 -0
  13. package/src/agent/AgentLoop.js +76 -26
  14. package/src/agent/ContextAssembler.js +2 -0
  15. package/src/agent/Entries.js +238 -60
  16. package/src/agent/ProjectAgent.js +44 -0
  17. package/src/agent/TurnExecutor.js +99 -30
  18. package/src/agent/XmlParser.js +206 -111
  19. package/src/agent/errors.js +35 -0
  20. package/src/agent/known_queries.sql +1 -1
  21. package/src/agent/known_store.sql +3 -42
  22. package/src/agent/materializeContext.js +30 -1
  23. package/src/agent/runs.sql +8 -18
  24. package/src/agent/tokens.js +0 -1
  25. package/src/agent/turns.sql +1 -0
  26. package/src/hooks/Hooks.js +26 -0
  27. package/src/hooks/RummyContext.js +12 -1
  28. package/src/lib/hedberg/README.md +60 -0
  29. package/src/lib/hedberg/hedberg.js +60 -0
  30. package/src/lib/hedberg/marker.js +158 -0
  31. package/src/{plugins → lib}/hedberg/matcher.js +1 -2
  32. package/src/llm/LlmProvider.js +41 -3
  33. package/src/llm/openaiStream.js +17 -0
  34. package/src/plugins/ask_user/ask_user.js +12 -2
  35. package/src/plugins/ask_user/ask_userDoc.md +1 -5
  36. package/src/plugins/budget/README.md +29 -24
  37. package/src/plugins/budget/budget.js +166 -110
  38. package/src/plugins/cli/README.md +3 -4
  39. package/src/plugins/cli/cli.js +31 -5
  40. package/src/plugins/cloudflare/cloudflare.js +136 -0
  41. package/src/plugins/cp/cp.js +41 -4
  42. package/src/plugins/cp/cpDoc.md +5 -6
  43. package/src/plugins/engine/engine.sql +1 -1
  44. package/src/plugins/env/README.md +5 -4
  45. package/src/plugins/env/env.js +7 -4
  46. package/src/plugins/env/envDoc.md +7 -8
  47. package/src/plugins/error/error.js +56 -15
  48. package/src/plugins/file/README.md +12 -3
  49. package/src/plugins/file/file.js +2 -2
  50. package/src/plugins/get/get.js +59 -36
  51. package/src/plugins/get/getDoc.md +10 -34
  52. package/src/plugins/google/google.js +115 -0
  53. package/src/plugins/hedberg/hedberg.js +13 -56
  54. package/src/plugins/helpers.js +66 -12
  55. package/src/plugins/index.js +1 -2
  56. package/src/plugins/instructions/README.md +44 -47
  57. package/src/plugins/instructions/instructions-system.md +44 -0
  58. package/src/plugins/instructions/instructions-user.md +53 -0
  59. package/src/plugins/instructions/instructions.js +58 -189
  60. package/src/plugins/known/README.md +6 -7
  61. package/src/plugins/known/known.js +24 -30
  62. package/src/plugins/log/log.js +41 -32
  63. package/src/plugins/mv/mv.js +40 -1
  64. package/src/plugins/mv/mvDoc.md +1 -8
  65. package/src/plugins/ollama/ollama.js +4 -3
  66. package/src/plugins/openai/openai.js +4 -3
  67. package/src/plugins/openrouter/openrouter.js +14 -4
  68. package/src/plugins/persona/README.md +11 -13
  69. package/src/plugins/persona/default.md +29 -0
  70. package/src/plugins/persona/persona.js +10 -66
  71. package/src/plugins/policy/policy.js +23 -22
  72. package/src/plugins/prompt/README.md +37 -27
  73. package/src/plugins/prompt/prompt.js +13 -19
  74. package/src/plugins/rm/rm.js +18 -0
  75. package/src/plugins/rm/rmDoc.md +5 -6
  76. package/src/plugins/rpc/rpc.js +3 -3
  77. package/src/plugins/set/set.js +205 -323
  78. package/src/plugins/set/setDoc.md +47 -17
  79. package/src/plugins/sh/README.md +6 -5
  80. package/src/plugins/sh/sh.js +8 -5
  81. package/src/plugins/sh/shDoc.md +7 -8
  82. package/src/plugins/skill/README.md +37 -14
  83. package/src/plugins/skill/skill.js +200 -101
  84. package/src/plugins/skill/skillDoc.js +3 -0
  85. package/src/plugins/skill/skillDoc.md +9 -0
  86. package/src/plugins/stream/README.md +7 -6
  87. package/src/plugins/stream/finalize.js +100 -0
  88. package/src/plugins/stream/stream.js +13 -45
  89. package/src/plugins/telemetry/telemetry.js +27 -4
  90. package/src/plugins/think/think.js +2 -3
  91. package/src/plugins/think/thinkDoc.md +2 -4
  92. package/src/plugins/unknown/README.md +1 -1
  93. package/src/plugins/unknown/unknown.js +17 -19
  94. package/src/plugins/update/update.js +4 -51
  95. package/src/plugins/update/updateDoc.md +21 -6
  96. package/src/plugins/xai/xai.js +68 -102
  97. package/src/plugins/yolo/yolo.js +102 -75
  98. package/src/sql/functions/hedmatch.js +1 -1
  99. package/src/sql/functions/hedreplace.js +1 -1
  100. package/src/sql/functions/hedsearch.js +1 -1
  101. package/src/sql/functions/slugify.js +16 -2
  102. package/BENCH_ENVIRONMENT.md +0 -230
  103. package/CLIENT_INTERFACE.md +0 -396
  104. package/last_run.txt +0 -5617
  105. package/scriptify/ask_run.js +0 -77
  106. package/scriptify/cache_probe.js +0 -66
  107. package/scriptify/cache_probe_grok.js +0 -74
  108. package/src/agent/budget.js +0 -33
  109. package/src/agent/config.js +0 -38
  110. package/src/plugins/hedberg/README.md +0 -71
  111. package/src/plugins/hedberg/docs.md +0 -0
  112. package/src/plugins/hedberg/edits.js +0 -55
  113. package/src/plugins/hedberg/normalize.js +0 -17
  114. package/src/plugins/hedberg/sed.js +0 -49
  115. package/src/plugins/instructions/instructions.md +0 -34
  116. package/src/plugins/instructions/instructions_104.md +0 -8
  117. package/src/plugins/instructions/instructions_105.md +0 -39
  118. package/src/plugins/instructions/instructions_106.md +0 -22
  119. package/src/plugins/instructions/instructions_107.md +0 -17
  120. package/src/plugins/instructions/instructions_108.md +0 -0
  121. package/src/plugins/known/knownDoc.js +0 -3
  122. package/src/plugins/known/knownDoc.md +0 -8
  123. package/src/plugins/unknown/unknownDoc.js +0 -3
  124. package/src/plugins/unknown/unknownDoc.md +0 -11
  125. package/turns/cli_1777462658211/turn_001.txt +0 -772
  126. package/turns/cli_1777462658211/turn_002.txt +0 -606
  127. package/turns/cli_1777462658211/turn_003.txt +0 -667
  128. package/turns/cli_1777462658211/turn_004.txt +0 -297
  129. package/turns/cli_1777462658211/turn_005.txt +0 -301
  130. package/turns/cli_1777462658211/turn_006.txt +0 -262
  131. package/turns/cli_1777465095132/turn_001.txt +0 -715
  132. package/turns/cli_1777465095132/turn_002.txt +0 -236
  133. package/turns/cli_1777465095132/turn_003.txt +0 -287
  134. package/turns/cli_1777465095132/turn_004.txt +0 -694
  135. package/turns/cli_1777465095132/turn_005.txt +0 -422
  136. package/turns/cli_1777465095132/turn_006.txt +0 -365
  137. package/turns/cli_1777465095132/turn_007.txt +0 -885
  138. package/turns/cli_1777465095132/turn_008.txt +0 -1277
  139. package/turns/cli_1777465095132/turn_009.txt +0 -736
  140. /package/src/{plugins → lib}/hedberg/patterns.js +0 -0
@@ -1,22 +1,52 @@
1
- ## <set path="[path/to/file]">[content or edit]</set> - Create, edit, or update a file or entry
1
+ ## <set path="{path}" tags="{topical,searchable,folksonomic,internal,tags}">[content or edit]</set> - Create, edit, or update an entry or file
2
2
 
3
- Example: <set path="known://project/milestones" visibility="summarized" summary="milestone,deadline,2026"/>
4
- <!-- Visibility control first most unique capability of set. -->
3
+ * The <set/> command requires HEREDOC string literal syntax
4
+ * The <set/> command's SEARCH/REPLACE string literal syntax uses HEREDOC instead of git conflict markers
5
+ * The `{SEARCH|REPLACE|NEW|APPEND|PREPEND|DELETE} Operative Labels determine the type of edit
5
6
 
6
- Example: <set path="src/app.js">
7
- <<<<<<< SEARCH
8
- old text
9
- =======
10
- new text
11
- >>>>>>> REPLACE
12
- </set>
13
- <!-- SEARCH/REPLACE block — primary edit pattern for existing files. -->
7
+ YOU MAY add additional characters to the Operative Labels to avoid collisions
14
8
 
15
- Example: <set path="src/config.js">s/port = 3000/port = 8080/g;s/We're almost done/We're done./g;</set>
16
- <!-- Sed syntax: chained s/old/new/ patterns with semicolons. -->
9
+ Example:
10
+ <set path="src/main.go" tags="go,source,unlinted"><<SEARCH
11
+ exact
12
+ text
13
+ to be
14
+ replaced
15
+ SEARCH<<REPLACE
16
+ new
17
+ replacement
18
+ text
19
+ REPLACE</set>
20
+ <!-- SEARCH/REPLACE: surgical edit, fuzzy on whitespace. Multiple pairs in one body apply in order. -->
17
21
 
18
- Example: <set path="example.md">Full file content here</set>
19
- <!-- Create: body contents are entire file. -->
22
+ Example:
23
+ <set path="src/main.go"><<NEW
24
+ package main
25
+
26
+ func main() {}
27
+ NEW</set>
28
+ <!-- NEW: create with body content. -->
20
29
 
21
- YOU MUST NOT use <sh></sh> or <env></env> to list, create, read, or edit files — use <get></get> and <set></set>
22
- <!-- Reinforces at the decision point — model reading setDoc for file ops sees the prohibition here, not just buried in shDoc/envDoc which it may not be reading. -->
30
+ Example:
31
+ <set path="known://plan" tags="plan,project,todo"><<APPEND
32
+ - [ ] new task
33
+ APPEND</set>
34
+ <!-- APPEND adds to the end; PREPEND to the start. -->
35
+
36
+ Example:
37
+ <set path="known://plan" tags="docs"><<PREPEND0
38
+ Documenting the <<PREPEND label
39
+ PREPEND0</set>
40
+ <!-- APPEND adds to the end; PREPEND to the start. -->
41
+
42
+ Example:
43
+ <set path="src/main.go"><<DELETE
44
+ deprecated_function()
45
+ DELETE</set>
46
+ <!-- DELETE: remove a literal-matching region. -->
47
+
48
+ Example:
49
+ <set path="docs/guide.md" tags="docs"><<GUIDE
50
+ The pair is <<SEARCH ... SEARCH<<REPLACE ... REPLACE.
51
+ GUIDE</set>
52
+ <!-- Any IDENT brackets opaque body. Use a custom IDENT (GUIDE, EOF, DOC, file paths, etc.) for bodies that contain `<<` literally. -->
@@ -7,7 +7,7 @@ after the proposal is accepted.
7
7
  ## Registration
8
8
 
9
9
  - **Tool**: `sh`
10
- - **Scheme**: `sh` — `category: "data"` (channels only; see below)
10
+ - **Scheme**: `sh` — `category: "logging"` (channels are time-indexed activity, not state)
11
11
  - **Handler**: Upserts the proposal entry at status 202 (proposed). The
12
12
  client must approve execution.
13
13
 
@@ -22,10 +22,11 @@ record, one data payload:
22
22
  and finalized by `stream/completed` with exit code + duration. Renders
23
23
  inside the `<log>` block as `<sh>`.
24
24
  - **Data channels**: `sh://turn_N/{slug}_1` (stdout), `sh://turn_N/{slug}_2`
25
- (stderr) — scheme=`sh`, category=`data`. Created at status=102 on
26
- proposal acceptance, grow via the `stream` RPC, transition to 200/500
27
- via `stream/completed`. Render inside `<visible>` as `<sh>` when
28
- promoted; listed in `<summarized>` otherwise.
25
+ (stderr) — scheme=`sh`, category=`logging` (time-indexed activity).
26
+ Created at status=102 on proposal acceptance, grow via the `stream`
27
+ RPC, transition to 200/500 via `stream/completed`. Render inside
28
+ `<log>` adjacent to their parent `<sh>` action entry; visibility
29
+ controls whether the body is full or compact, not which block.
29
30
 
30
31
  The `sh` scheme exists **only** for the data channels. The proposal/log
31
32
  entry itself is in the unified `log://` namespace along with every
@@ -8,8 +8,11 @@ export default class Sh {
8
8
 
9
9
  constructor(core) {
10
10
  this.#core = core;
11
- // data scheme = streamed stdout/stderr; audit lives in log://. SPEC #streaming_entries.
12
- core.registerScheme({ category: "data" });
11
+ // Streaming stdout/stderr is time-indexed activity output, not
12
+ // topic-indexed state — category="logging" so it renders in <log>
13
+ // adjacent to its action entry, not in <summary>/<visible> next
14
+ // to knowns and files. SPEC #streaming_entries.
15
+ core.registerScheme({ category: "logging" });
13
16
  core.on("handler", this.handler.bind(this));
14
17
  core.on("visible", this.full.bind(this));
15
18
  core.on("summarized", this.summary.bind(this));
@@ -25,7 +28,7 @@ export default class Sh {
25
28
  if (m?.[1] !== "sh") return;
26
29
  let command = "";
27
30
  if (ctx.attrs?.command) command = ctx.attrs.command;
28
- else if (ctx.attrs?.summary) command = ctx.attrs.summary;
31
+ else if (ctx.attrs?.tags) command = ctx.attrs.tags;
29
32
  const turn = (await ctx.db.get_run_by_id.get({ id: ctx.runId })).next_turn;
30
33
  const dataBase = logPathToDataBase(ctx.path);
31
34
  for (const ch of [1, 2]) {
@@ -36,7 +39,7 @@ export default class Sh {
36
39
  body: "",
37
40
  state: "streaming",
38
41
  visibility: "summarized",
39
- attributes: { command, summary: command, channel: ch },
42
+ attributes: { command, tags: command, channel: ch },
40
43
  });
41
44
  }
42
45
  await ctx.entries.set({
@@ -56,7 +59,7 @@ export default class Sh {
56
59
  path: entry.resultPath,
57
60
  body: "",
58
61
  state: "proposed",
59
- attributes: { ...entry.attributes, summary: entry.attributes.command },
62
+ attributes: { ...entry.attributes, tags: entry.attributes.command },
60
63
  loopId,
61
64
  });
62
65
  }
@@ -1,13 +1,12 @@
1
1
  ## <sh>[command]</sh> - Run a shell command with side effects
2
2
 
3
- Example: <sh>npm install express</sh>
4
- <!-- Package install. Real side-effect command. -->
5
-
6
- Example: <sh>npm test</sh>
7
- <!-- Test execution. Another common side-effect action. -->
3
+ Example:
4
+ <sh><<EOF
5
+ npm install express
6
+ npm test 2>&1 | tee npm.log
7
+ EOF</sh>
8
+ Example: <get path="sh://turn_N/*" line="-50"/>
9
+ <!-- Heredoc body is opaque — embed multi-line scripts, redirects, and special characters without escaping. Output is addressable: every <sh> result lives at sh://turn_N/<slug>. Slice with line/limit instead of re-running. -->
8
10
 
9
11
  YOU MUST NOT use <sh></sh> to read, create, or edit files — use <get></get> and <set></set>
10
- <!-- Forces file operations through the entry system. -->
11
-
12
12
  YOU MUST use <env></env> for commands without side effects
13
- <!-- Reinforces the env/sh split. Read = env, mutate = sh. -->
@@ -1,23 +1,46 @@
1
1
  # skill {#skill_plugin}
2
2
 
3
- Runtime skill management. A skill is a markdown file that gets
4
- attached to a run as a `skill://<name>` entry. Models see skills
5
- like any other entry.
3
+ Drop-in deep skills: a single markdown file, a folder of markdown files,
4
+ or a `.zip` archive local or URL — archived under `skill://<name>/...`
5
+ for the run.
6
6
 
7
7
  ## Files
8
8
 
9
- - **skill.js** — RPC registration and skill file loading.
9
+ - **skill.js** — `<skill>` tag handler + `skill://` scheme.
10
+ - **skillDoc.md** — model-facing tooldoc.
10
11
 
11
- ## Registration
12
+ ## Tag
12
13
 
13
- - **Scheme**: `skill` (category: `data`)
14
- - **Projections**: visible → body; summarized → empty.
14
+ `<skill path="[path-or-url]"/>`
15
15
 
16
- ## RPC Methods
16
+ - Single `.md` file → archived at `skill://<basename>` (summarized).
17
+ - Folder → walk `*.md`; index file (`index.md`) → `skill://<foldername>`
18
+ (summarized); rest → `skill://<foldername>/<relpath-without-.md>`
19
+ (archived). `index.md` segments collapse: `foo/index.md` becomes
20
+ `skill://<foldername>/foo`.
21
+ - `.zip` → unpack `*.md`; same layout as folder. Top-level archive
22
+ folder is stripped (`example/index.md` inside `example.zip` ↦
23
+ `skill://example`).
24
+ - URL → fetch. `.zip` extension or `Content-Type: application/zip`
25
+ triggers zip unpack; otherwise treated as a single markdown file.
17
26
 
18
- | Method | Params | Notes |
19
- |--------|--------|-------|
20
- | `skill/add` | `{ run, name }` | Load `${RUMMY_HOME}/skills/<name>.md` and attach it to the run as `skill://<name>`. |
21
- | `skill/remove` | `{ run, name }` | Remove the skill entry from the run. |
22
- | `getSkills` | `{ run }` | Skills active on a run. |
23
- | `listSkills` | | Available skill files on disk. |
27
+ Relative paths resolve against the project root. Absolute paths used
28
+ as-is.
29
+
30
+ ## Authoring
31
+
32
+ Skill files reference each other with absolute `skill://...` URIs:
33
+ `[next](skill://playbook/next)`. No relative-link rewriting at archive
34
+ time — the contract is explicit so navigation works the same regardless
35
+ of how the skill was packaged.
36
+
37
+ ## Visibility
38
+
39
+ - Index page → `summarized` (model sees a header in summary; pulls
40
+ full body via `<get>`).
41
+ - All other pages → `archived` (out of context until promoted).
42
+
43
+ ## Re-emit
44
+
45
+ Re-emitting `<skill path="..."/>` overwrites prior entries — source may
46
+ have changed mid-run.
@@ -1,130 +1,229 @@
1
- import fs from "node:fs/promises";
2
- import { join } from "node:path";
1
+ import {
2
+ mkdtemp,
3
+ readdir,
4
+ readFile,
5
+ rm,
6
+ stat,
7
+ writeFile,
8
+ } from "node:fs/promises";
9
+ import { tmpdir } from "node:os";
10
+ import { basename, extname, isAbsolute, join, relative } from "node:path";
11
+ import { open as openZip } from "yauzl-promise";
12
+ import docs from "./skillDoc.js";
3
13
 
4
14
  export default class Skill {
5
15
  #core;
6
16
 
7
17
  constructor(core) {
8
18
  this.#core = core;
9
- core.registerScheme({
10
- name: "skill",
11
- category: "data",
12
- });
19
+ core.registerScheme({ name: "skill", category: "data" });
13
20
  core.hooks.tools.onView("skill", (entry) => entry.body, "visible");
14
21
  core.hooks.tools.onView("skill", () => "", "summarized");
15
22
 
16
- const r = core.hooks.rpc.registry;
17
-
18
- r.register("skill/add", {
19
- handler: async (params, ctx) => {
20
- if (!params.name) throw new Error("name is required");
21
- if (!params.run) throw new Error("run is required");
22
-
23
- const runRow = await ctx.db.get_run_by_alias.get({ alias: params.run });
24
- if (!runRow) throw new Error(`Run not found: ${params.run}`);
23
+ core.on("handler", this.handler.bind(this));
24
+ core.filter("instructions.toolDocs", async (docsMap) => {
25
+ docsMap.skill = docs;
26
+ return docsMap;
27
+ });
28
+ }
25
29
 
26
- const body = await loadFile("skills", params.name);
27
- const store = ctx.projectAgent.entries;
30
+ async handler(entry, rummy) {
31
+ const {
32
+ entries: store,
33
+ sequence: turn,
34
+ runId,
35
+ loopId,
36
+ db,
37
+ projectId,
38
+ } = rummy;
39
+ const path = entry.attributes.path;
40
+ if (!path) {
41
+ await store.set({
42
+ runId,
43
+ turn,
44
+ loopId,
45
+ path: entry.resultPath,
46
+ body: 'Missing required "path" on <skill>.',
47
+ state: "failed",
48
+ outcome: "validation",
49
+ });
50
+ return;
51
+ }
52
+
53
+ const projectRoot = await projectRootFor(db, projectId);
54
+ let resolved;
55
+ try {
56
+ resolved = await resolveSource(path, projectRoot);
57
+ } catch (err) {
58
+ await store.set({
59
+ runId,
60
+ turn,
61
+ loopId,
62
+ path: entry.resultPath,
63
+ body: err.message,
64
+ state: "failed",
65
+ outcome: "not_found",
66
+ attributes: { path },
67
+ });
68
+ return;
69
+ }
70
+
71
+ const { name, files, cleanup } = resolved;
72
+ try {
73
+ let added = 0;
74
+ for (const { relPath, body } of files) {
75
+ const isRoot = relPath === "";
76
+ const skillPath = isRoot
77
+ ? `skill://${name}`
78
+ : `skill://${name}/${relPath}`;
28
79
  await store.set({
29
- runId: runRow.id,
30
- turn: 0,
31
- path: `skill://${params.name}`,
80
+ runId,
81
+ turn,
82
+ path: skillPath,
32
83
  body,
33
84
  state: "resolved",
34
- attributes: {
35
- name: params.name,
36
- source: filePath("skills", params.name),
37
- },
85
+ visibility: isRoot ? "summarized" : "archived",
86
+ attributes: { source: path },
87
+ loopId,
38
88
  });
89
+ added += 1;
90
+ }
91
+ await store.set({
92
+ runId,
93
+ turn,
94
+ loopId,
95
+ path: entry.resultPath,
96
+ body: `skill '${name}' added: ${added} entr${added === 1 ? "y" : "ies"} at skill://${name}${added > 1 ? "/*" : ""}`,
97
+ state: "resolved",
98
+ attributes: { path, name, count: added },
99
+ });
100
+ } finally {
101
+ if (cleanup) await cleanup();
102
+ }
103
+ }
104
+ }
39
105
 
40
- return { status: "ok", skill: params.name };
41
- },
42
- description:
43
- "Add a skill to a run. Reads from RUMMY_HOME/skills/{name}.md.",
44
- params: {
45
- run: "string — run alias",
46
- name: "string — skill name (filename without .md)",
47
- },
48
- requiresInit: true,
49
- });
106
+ async function projectRootFor(db, projectId) {
107
+ if (!projectId) return null;
108
+ const project = await db.get_project_by_id.get({ id: projectId });
109
+ return project.project_root;
110
+ }
50
111
 
51
- r.register("skill/remove", {
52
- handler: async (params, ctx) => {
53
- if (!params.name) throw new Error("name is required");
54
- if (!params.run) throw new Error("run is required");
55
-
56
- const runRow = await ctx.db.get_run_by_alias.get({ alias: params.run });
57
- if (!runRow) throw new Error(`Run not found: ${params.run}`);
58
-
59
- const store = ctx.projectAgent.entries;
60
- await store.rm({ runId: runRow.id, path: `skill://${params.name}` });
61
-
62
- return { status: "ok" };
63
- },
64
- description: "Remove a skill from a run.",
65
- params: {
66
- run: "string — run alias",
67
- name: "string — skill name",
68
- },
69
- requiresInit: true,
70
- });
112
+ function isUrl(p) {
113
+ return /^https?:\/\//.test(p);
114
+ }
71
115
 
72
- r.register("getSkills", {
73
- handler: async (params, ctx) => {
74
- if (!params.run) throw new Error("run is required");
75
-
76
- const runRow = await ctx.db.get_run_by_alias.get({ alias: params.run });
77
- if (!runRow) throw new Error(`Run not found: ${params.run}`);
78
-
79
- const store = ctx.projectAgent.entries;
80
- const entries = await store.getEntriesByPattern(
81
- runRow.id,
82
- "skill://*",
83
- null,
84
- );
85
- return entries.map((e) => ({
86
- name: e.path.replace("skill://", ""),
87
- status: e.status,
88
- }));
89
- },
90
- description: "List skills active on a run. Returns [{ name, status }].",
91
- params: { run: "string — run alias" },
92
- requiresInit: true,
93
- });
116
+ async function resolveSource(rawPath, projectRoot) {
117
+ if (isUrl(rawPath)) return resolveUrl(rawPath);
118
+ const absPath = isAbsolute(rawPath)
119
+ ? rawPath
120
+ : projectRoot
121
+ ? join(projectRoot, rawPath)
122
+ : rawPath;
123
+ const st = await stat(absPath).catch(() => null);
124
+ if (!st) throw new Error(`skill source not found: ${rawPath}`);
125
+
126
+ if (st.isDirectory()) {
127
+ const name = basename(absPath);
128
+ const files = await walkFolder(absPath);
129
+ return { name, files, cleanup: null };
130
+ }
131
+ if (extname(absPath).toLowerCase() === ".zip") {
132
+ const name = basename(absPath, extname(absPath));
133
+ const files = await extractZipToFiles(absPath);
134
+ return { name, files, cleanup: null };
135
+ }
136
+ const name = basename(absPath, extname(absPath));
137
+ const body = await readFile(absPath, "utf8");
138
+ return { name, files: [{ relPath: "", body }], cleanup: null };
139
+ }
94
140
 
95
- r.register("listSkills", {
96
- handler: async () => listAvailable("skills"),
97
- description: "List available skill files. Returns [{ name, path }].",
98
- requiresInit: true,
99
- });
141
+ async function resolveUrl(url) {
142
+ const u = new URL(url);
143
+ const pathBase = basename(u.pathname);
144
+ const ext = extname(pathBase).toLowerCase();
145
+ const res = await fetch(url);
146
+ if (!res.ok) throw new Error(`skill fetch failed (${res.status}): ${url}`);
147
+ const ctype = res.headers.get("content-type");
148
+ const isZip = ext === ".zip" || ctype?.includes("application/zip");
149
+ if (isZip) {
150
+ const buf = Buffer.from(await res.arrayBuffer());
151
+ const tmp = await mkdtemp(join(tmpdir(), "rummy-skill-"));
152
+ const zipPath = join(tmp, "src.zip");
153
+ await writeFile(zipPath, buf);
154
+ const files = await extractZipToFiles(zipPath);
155
+ const name = basename(pathBase, ext);
156
+ return {
157
+ name,
158
+ files,
159
+ cleanup: () => rm(tmp, { recursive: true, force: true }),
160
+ };
161
+ }
162
+ const body = await res.text();
163
+ const name = basename(pathBase, ext);
164
+ return { name, files: [{ relPath: "", body }], cleanup: null };
165
+ }
166
+
167
+ async function walkFolder(root) {
168
+ const out = [];
169
+ for await (const file of walk(root)) {
170
+ if (extname(file).toLowerCase() !== ".md") continue;
171
+ const body = await readFile(file, "utf8");
172
+ out.push({ relPath: relPathFor(root, file), body });
173
+ }
174
+ return out;
175
+ }
176
+
177
+ async function* walk(dir) {
178
+ const dirents = await readdir(dir, { withFileTypes: true });
179
+ for (const e of dirents) {
180
+ const full = join(dir, e.name);
181
+ if (e.isDirectory()) yield* walk(full);
182
+ else yield full;
183
+ }
184
+ }
100
185
 
101
- // Persona methods extracted to persona plugin.
186
+ async function extractZipToFiles(zipPath) {
187
+ const zip = await openZip(zipPath);
188
+ const out = [];
189
+ try {
190
+ for await (const entry of zip) {
191
+ if (entry.filename.endsWith("/")) continue;
192
+ if (extname(entry.filename).toLowerCase() !== ".md") continue;
193
+ const stream = await entry.openReadStream();
194
+ const chunks = [];
195
+ for await (const chunk of stream) chunks.push(chunk);
196
+ const body = Buffer.concat(chunks).toString("utf8");
197
+ const stripped = stripTopFolder(entry.filename);
198
+ out.push({ relPath: relPathFromArchive(stripped), body });
199
+ }
200
+ } finally {
201
+ await zip.close();
102
202
  }
203
+ return out;
103
204
  }
104
205
 
105
- function configDir(subfolder) {
106
- const home = process.env.RUMMY_HOME;
107
- if (home) return join(home, subfolder);
108
- return null;
206
+ function stripTopFolder(p) {
207
+ const idx = p.indexOf("/");
208
+ if (idx === -1) return p;
209
+ return p.slice(idx + 1);
109
210
  }
110
211
 
111
- function filePath(subfolder, name) {
112
- const dir = configDir(subfolder);
113
- if (!dir) return null;
114
- return join(dir, `${name}.md`);
212
+ function relPathFor(root, full) {
213
+ const rel = relative(root, full).replaceAll("\\", "/");
214
+ return mapToSkillRel(rel);
115
215
  }
116
216
 
117
- async function loadFile(subfolder, name) {
118
- const path = filePath(subfolder, name);
119
- if (!path) throw new Error("RUMMY_HOME not configured");
120
- return fs.readFile(path, "utf8");
217
+ function relPathFromArchive(rel) {
218
+ return mapToSkillRel(rel);
121
219
  }
122
220
 
123
- async function listAvailable(subfolder) {
124
- const dir = configDir(subfolder);
125
- if (!dir) return [];
126
- const files = await fs.readdir(dir);
127
- return files
128
- .filter((f) => f.endsWith(".md"))
129
- .map((f) => ({ name: f.replace(".md", ""), path: join(dir, f) }));
221
+ // "index.md" → "" (root)
222
+ // "foo.md" → "foo"
223
+ // "foo/index.md" → "foo"
224
+ // "foo/bar.md" → "foo/bar"
225
+ function mapToSkillRel(rel) {
226
+ const noExt = rel.replace(/\.md$/i, "");
227
+ if (noExt === "index") return "";
228
+ return noExt.replace(/\/index$/, "");
130
229
  }
@@ -0,0 +1,3 @@
1
+ import { loadDoc } from "../helpers.js";
2
+
3
+ export default loadDoc(import.meta.url, "skillDoc.md");
@@ -0,0 +1,9 @@
1
+ ## <skill path="[path-or-url]"/> - Drop in a deep skill
2
+
3
+ Example: <skill path="docs/refactoring.md"/>
4
+ <!-- Single-file skill: archived at skill://refactoring (summarized). -->
5
+ Example: <skill path="docs/playbook/"/>
6
+ <!-- Folder skill: index.md → skill://playbook (summarized). All other *.md → skill://playbook/<relpath> (archived). Navigate via <get skill://playbook/<page>>. -->
7
+ Example: <skill path="https://example.com/team-skill.zip"/>
8
+ <!-- URL skill: fetch + unpack. .zip or Content-Type: application/zip → multi-file deep skill. Otherwise single file. -->
9
+ <!-- Inside a multi-file skill, link sister pages with absolute URIs: [next](skill://playbook/next). -->
@@ -14,10 +14,11 @@ A streaming action lives in **two namespaces** by design:
14
14
  client resolves. Renders inside `<log>`.
15
15
  - **Data channels** (payload): `{action}://turn_N/{slug}_1`,
16
16
  `{action}://turn_N/{slug}_2`, ... — scheme=`{action}` (sh, env, ...),
17
- category=`data`. Created at status=102 on proposal acceptance. Grow
18
- via `stream`; terminal via `stream/completed` / `stream/aborted` /
19
- `stream/cancel`. Render inside `<visible>` (or `<summarized>` if
20
- demoted).
17
+ category=`logging` (time-indexed activity, not topic-indexed state).
18
+ Created at status=102 on proposal acceptance. Grow via `stream`;
19
+ terminal via `stream/completed` / `stream/aborted` / `stream/cancel`.
20
+ Render inside `<log>` adjacent to their parent action entry; visibility
21
+ controls body projection (full vs compact), not section assignment.
21
22
 
22
23
  The stream RPC `path` param is always the **log-entry path** (the
23
24
  `log://...` path the client discovers via `getEntries` after a
@@ -75,8 +76,8 @@ A streaming producer plugin:
75
76
  `TurnExecutor` builds the path via `logPath`; the producer's
76
77
  `handler` just persists it).
77
78
  2. On `proposal.accepted`, derives the data base
78
- (`logPathToDataBase(ctx.path)`) and creates **data entries** at
79
- `{dataBase}_1`, `{dataBase}_2`, etc. at status=102, category=data,
79
+ (`logPathToDataBase(ctx.path)`) and creates **channel entries** at
80
+ `{dataBase}_1`, `{dataBase}_2`, etc. at status=102, category=logging,
80
81
  visibility=summarized, empty body. Then rewrites the log entry body
81
82
  to reference the channel paths.
82
83
  3. Client or external producer calls the `stream` RPC with chunks as