@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.
- package/.env.example +40 -15
- package/.xai.key +1 -0
- package/PLUGINS.md +169 -53
- package/README.md +38 -32
- package/SPEC.md +366 -179
- package/bin/digest.js +1097 -0
- package/biome/no-fallbacks.grit +2 -2
- package/gemini.key +1 -0
- package/lang/en.json +10 -1
- package/migrations/001_initial_schema.sql +9 -2
- package/package.json +19 -8
- package/service.js +1 -0
- package/src/agent/AgentLoop.js +76 -26
- package/src/agent/ContextAssembler.js +2 -0
- package/src/agent/Entries.js +238 -60
- package/src/agent/ProjectAgent.js +44 -0
- package/src/agent/TurnExecutor.js +99 -30
- package/src/agent/XmlParser.js +206 -111
- package/src/agent/errors.js +35 -0
- package/src/agent/known_queries.sql +1 -1
- package/src/agent/known_store.sql +3 -42
- package/src/agent/materializeContext.js +30 -1
- package/src/agent/runs.sql +8 -18
- package/src/agent/tokens.js +0 -1
- package/src/agent/turns.sql +1 -0
- package/src/hooks/Hooks.js +26 -0
- package/src/hooks/RummyContext.js +12 -1
- package/src/lib/hedberg/README.md +60 -0
- package/src/lib/hedberg/hedberg.js +60 -0
- package/src/lib/hedberg/marker.js +158 -0
- package/src/{plugins → lib}/hedberg/matcher.js +1 -2
- package/src/llm/LlmProvider.js +41 -3
- package/src/llm/openaiStream.js +17 -0
- package/src/plugins/ask_user/ask_user.js +12 -2
- package/src/plugins/ask_user/ask_userDoc.md +1 -5
- package/src/plugins/budget/README.md +29 -24
- package/src/plugins/budget/budget.js +166 -110
- package/src/plugins/cli/README.md +3 -4
- package/src/plugins/cli/cli.js +31 -5
- package/src/plugins/cloudflare/cloudflare.js +136 -0
- package/src/plugins/cp/cp.js +41 -4
- package/src/plugins/cp/cpDoc.md +5 -6
- package/src/plugins/engine/engine.sql +1 -1
- package/src/plugins/env/README.md +5 -4
- package/src/plugins/env/env.js +7 -4
- package/src/plugins/env/envDoc.md +7 -8
- package/src/plugins/error/error.js +56 -15
- package/src/plugins/file/README.md +12 -3
- package/src/plugins/file/file.js +2 -2
- package/src/plugins/get/get.js +59 -36
- package/src/plugins/get/getDoc.md +10 -34
- package/src/plugins/google/google.js +115 -0
- package/src/plugins/hedberg/hedberg.js +13 -56
- package/src/plugins/helpers.js +66 -12
- package/src/plugins/index.js +1 -2
- package/src/plugins/instructions/README.md +44 -47
- package/src/plugins/instructions/instructions-system.md +44 -0
- package/src/plugins/instructions/instructions-user.md +53 -0
- package/src/plugins/instructions/instructions.js +58 -189
- package/src/plugins/known/README.md +6 -7
- package/src/plugins/known/known.js +24 -30
- package/src/plugins/log/log.js +41 -32
- package/src/plugins/mv/mv.js +40 -1
- package/src/plugins/mv/mvDoc.md +1 -8
- package/src/plugins/ollama/ollama.js +4 -3
- package/src/plugins/openai/openai.js +4 -3
- package/src/plugins/openrouter/openrouter.js +14 -4
- package/src/plugins/persona/README.md +11 -13
- package/src/plugins/persona/default.md +29 -0
- package/src/plugins/persona/persona.js +10 -66
- package/src/plugins/policy/policy.js +23 -22
- package/src/plugins/prompt/README.md +37 -27
- package/src/plugins/prompt/prompt.js +13 -19
- package/src/plugins/rm/rm.js +18 -0
- package/src/plugins/rm/rmDoc.md +5 -6
- package/src/plugins/rpc/rpc.js +3 -3
- package/src/plugins/set/set.js +205 -323
- package/src/plugins/set/setDoc.md +47 -17
- package/src/plugins/sh/README.md +6 -5
- package/src/plugins/sh/sh.js +8 -5
- package/src/plugins/sh/shDoc.md +7 -8
- package/src/plugins/skill/README.md +37 -14
- package/src/plugins/skill/skill.js +200 -101
- package/src/plugins/skill/skillDoc.js +3 -0
- package/src/plugins/skill/skillDoc.md +9 -0
- package/src/plugins/stream/README.md +7 -6
- package/src/plugins/stream/finalize.js +100 -0
- package/src/plugins/stream/stream.js +13 -45
- package/src/plugins/telemetry/telemetry.js +27 -4
- package/src/plugins/think/think.js +2 -3
- package/src/plugins/think/thinkDoc.md +2 -4
- package/src/plugins/unknown/README.md +1 -1
- package/src/plugins/unknown/unknown.js +17 -19
- package/src/plugins/update/update.js +4 -51
- package/src/plugins/update/updateDoc.md +21 -6
- package/src/plugins/xai/xai.js +68 -102
- package/src/plugins/yolo/yolo.js +102 -75
- package/src/sql/functions/hedmatch.js +1 -1
- package/src/sql/functions/hedreplace.js +1 -1
- package/src/sql/functions/hedsearch.js +1 -1
- package/src/sql/functions/slugify.js +16 -2
- package/BENCH_ENVIRONMENT.md +0 -230
- package/CLIENT_INTERFACE.md +0 -396
- package/last_run.txt +0 -5617
- package/scriptify/ask_run.js +0 -77
- package/scriptify/cache_probe.js +0 -66
- package/scriptify/cache_probe_grok.js +0 -74
- package/src/agent/budget.js +0 -33
- package/src/agent/config.js +0 -38
- package/src/plugins/hedberg/README.md +0 -71
- package/src/plugins/hedberg/docs.md +0 -0
- package/src/plugins/hedberg/edits.js +0 -55
- package/src/plugins/hedberg/normalize.js +0 -17
- package/src/plugins/hedberg/sed.js +0 -49
- package/src/plugins/instructions/instructions.md +0 -34
- package/src/plugins/instructions/instructions_104.md +0 -8
- package/src/plugins/instructions/instructions_105.md +0 -39
- package/src/plugins/instructions/instructions_106.md +0 -22
- package/src/plugins/instructions/instructions_107.md +0 -17
- package/src/plugins/instructions/instructions_108.md +0 -0
- package/src/plugins/known/knownDoc.js +0 -3
- package/src/plugins/known/knownDoc.md +0 -8
- package/src/plugins/unknown/unknownDoc.js +0 -3
- package/src/plugins/unknown/unknownDoc.md +0 -11
- package/turns/cli_1777462658211/turn_001.txt +0 -772
- package/turns/cli_1777462658211/turn_002.txt +0 -606
- package/turns/cli_1777462658211/turn_003.txt +0 -667
- package/turns/cli_1777462658211/turn_004.txt +0 -297
- package/turns/cli_1777462658211/turn_005.txt +0 -301
- package/turns/cli_1777462658211/turn_006.txt +0 -262
- package/turns/cli_1777465095132/turn_001.txt +0 -715
- package/turns/cli_1777465095132/turn_002.txt +0 -236
- package/turns/cli_1777465095132/turn_003.txt +0 -287
- package/turns/cli_1777465095132/turn_004.txt +0 -694
- package/turns/cli_1777465095132/turn_005.txt +0 -422
- package/turns/cli_1777465095132/turn_006.txt +0 -365
- package/turns/cli_1777465095132/turn_007.txt +0 -885
- package/turns/cli_1777465095132/turn_008.txt +0 -1277
- package/turns/cli_1777465095132/turn_009.txt +0 -736
- /package/src/{plugins → lib}/hedberg/patterns.js +0 -0
|
@@ -1,22 +1,52 @@
|
|
|
1
|
-
## <set path="
|
|
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
|
-
|
|
4
|
-
|
|
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
|
-
|
|
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:
|
|
16
|
-
|
|
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:
|
|
19
|
-
|
|
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
|
-
|
|
22
|
-
|
|
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. -->
|
package/src/plugins/sh/README.md
CHANGED
|
@@ -7,7 +7,7 @@ after the proposal is accepted.
|
|
|
7
7
|
## Registration
|
|
8
8
|
|
|
9
9
|
- **Tool**: `sh`
|
|
10
|
-
- **Scheme**: `sh` — `category: "
|
|
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=`
|
|
26
|
-
proposal acceptance, grow via the `stream`
|
|
27
|
-
via `stream/completed`. Render inside
|
|
28
|
-
|
|
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
|
package/src/plugins/sh/sh.js
CHANGED
|
@@ -8,8 +8,11 @@ export default class Sh {
|
|
|
8
8
|
|
|
9
9
|
constructor(core) {
|
|
10
10
|
this.#core = core;
|
|
11
|
-
//
|
|
12
|
-
|
|
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?.
|
|
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,
|
|
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,
|
|
62
|
+
attributes: { ...entry.attributes, tags: entry.attributes.command },
|
|
60
63
|
loopId,
|
|
61
64
|
});
|
|
62
65
|
}
|
package/src/plugins/sh/shDoc.md
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
## <sh>[command]</sh> - Run a shell command with side effects
|
|
2
2
|
|
|
3
|
-
Example:
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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** —
|
|
9
|
+
- **skill.js** — `<skill>` tag handler + `skill://` scheme.
|
|
10
|
+
- **skillDoc.md** — model-facing tooldoc.
|
|
10
11
|
|
|
11
|
-
##
|
|
12
|
+
## Tag
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
- **Projections**: visible → body; summarized → empty.
|
|
14
|
+
`<skill path="[path-or-url]"/>`
|
|
15
15
|
|
|
16
|
-
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
|
2
|
-
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
27
|
-
|
|
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
|
|
30
|
-
turn
|
|
31
|
-
path:
|
|
80
|
+
runId,
|
|
81
|
+
turn,
|
|
82
|
+
path: skillPath,
|
|
32
83
|
body,
|
|
33
84
|
state: "resolved",
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
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
|
|
106
|
-
const
|
|
107
|
-
if (
|
|
108
|
-
return
|
|
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
|
|
112
|
-
const
|
|
113
|
-
|
|
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
|
-
|
|
118
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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,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=`
|
|
18
|
-
|
|
19
|
-
`stream/
|
|
20
|
-
|
|
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 **
|
|
79
|
-
`{dataBase}_1`, `{dataBase}_2`, etc. at status=102, category=
|
|
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
|