mirror of
https://github.com/Narcooo/inkos.git
synced 2026-08-28 23:02:03 +08:00
feat(skills): ship professional story skill pack
This commit is contained in:
@@ -273,4 +273,24 @@ describe.sequential("publish packaging", () => {
|
||||
await rm(packDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("packs the built-in professional skills with core", { timeout: STUDIO_PACK_TEST_TIMEOUT_MS }, async () => {
|
||||
const packDir = await mkdtemp(join(tmpdir(), "inkos-core-pack-"));
|
||||
|
||||
try {
|
||||
const coreDir = resolve(workspaceRoot, "packages", "core");
|
||||
const tarballPath = await packPackage(coreDir, packDir);
|
||||
const tarArgs = [...tarForceLocalArgs(), "-tf"];
|
||||
const archiveListing = execFileSync("tar", [...tarArgs, tarballPath], {
|
||||
cwd: workspaceRoot,
|
||||
encoding: "utf-8",
|
||||
});
|
||||
|
||||
expect(archiveListing).toContain("package/skills/inkos-long-writing/SKILL.md");
|
||||
expect(archiveListing).toContain("package/skills/inkos-story-review/references/review-matrix.md");
|
||||
expect(archiveListing).toContain("package/skills/inkos-story-cover/SKILL.md");
|
||||
} finally {
|
||||
await rm(packDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -41,7 +41,8 @@
|
||||
"files": [
|
||||
"dist",
|
||||
"!dist/__tests__",
|
||||
"genres"
|
||||
"genres",
|
||||
"skills"
|
||||
],
|
||||
"license": "AGPL-3.0-only",
|
||||
"repository": {
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
---
|
||||
name: inkos-long-market-research
|
||||
description: 长篇网文市场、榜单、平台趋势与对标研究。Use for evidence-based long-form fiction market research, not for ordinary drafting.
|
||||
---
|
||||
# Long-form market research
|
||||
|
||||
Use this skill when the user wants current market evidence, platform differences, comparable works, audience expectations, or topic selection for a long-form project.
|
||||
|
||||
- Clarify the market, platform, audience, language, and time window only when they materially affect the answer.
|
||||
- Use `research_web` for current claims. Separate observed evidence, interpretation, and creative recommendation.
|
||||
- Use `ingest_material` for user-provided reports or URLs and `retrieve_material` for already archived evidence.
|
||||
- Do not treat rankings, popularity, or one successful book as a writing formula. Extract mechanisms and uncertainty.
|
||||
- Research never mutates book canon. If the user later wants a source available during chapter writing, archive it and explicitly bind it with `manage_book_reference` in the active book.
|
||||
- Respond in the user's language.
|
||||
|
||||
For a detailed evidence and deliverable rubric, load `references/research-rubric.md` with `use_skill` only when needed.
|
||||
@@ -0,0 +1,19 @@
|
||||
# Long-form market research rubric
|
||||
|
||||
## Evidence
|
||||
|
||||
- Record source, publication date, observation window, platform, and whether a number is first-party, third-party, or inferred.
|
||||
- Distinguish durable reader needs from a temporary chart pattern.
|
||||
- Compare at least two sources when making a market-level claim.
|
||||
- State missing evidence rather than filling it with genre common sense.
|
||||
|
||||
## Deliverable
|
||||
|
||||
1. Market boundary and user objective.
|
||||
2. Observed signals with sources.
|
||||
3. Audience and reading-context implications.
|
||||
4. Transferable story mechanisms.
|
||||
5. Risks, saturation, and counter-signals.
|
||||
6. Two or three differentiated directions, not a single mandatory formula.
|
||||
|
||||
Do not copy titles, character configurations, signature scenes, or wording from a comparable work.
|
||||
@@ -0,0 +1,17 @@
|
||||
---
|
||||
name: inkos-long-story-analysis
|
||||
description: 长篇小说拆稿、文风分析与可迁移机制提炼。Use when analyzing a full novel or long sample without copying its expression.
|
||||
---
|
||||
# Long-form story analysis
|
||||
|
||||
Use this skill when the user asks to deconstruct, compare, learn from, or continue from a long novel or substantial sample.
|
||||
|
||||
- Preserve the source as traceable material with `ingest_material` when it comes from a file, PDF, URL, or upload.
|
||||
- Analyze semantic mechanisms: reader promise, conflict escalation, character motivation, information release, scene function, emotional payoff, volume rhythm, prose behavior, and continuity load.
|
||||
- Keep source evidence pointers. Separate direct observation from inference.
|
||||
- Extract reusable craft, not names, wording, scene order, or signature combinations.
|
||||
- If the user explicitly wants the active book to consult this source later, call `manage_book_reference(action="bind")` with the user's natural-language purposes. Binding is guidance, never canon.
|
||||
- If the user only asks a question, answer it; do not start writing or editing a book.
|
||||
- Respond in the user's language.
|
||||
|
||||
Load `references/analysis-lens.md` only for a full decomposition or comparison deliverable.
|
||||
@@ -0,0 +1,16 @@
|
||||
# Long-form decomposition lens
|
||||
|
||||
## Layers
|
||||
|
||||
- Promise: what continuing experience the reader expects.
|
||||
- Story engine: the repeatable pressure, choice, consequence, and reward loop.
|
||||
- Structure: opening contract, arc transitions, volume goals, midpoint changes, and payoff placement.
|
||||
- Character: desire, fear, tactic, relationship movement, voice, and irreversible choices.
|
||||
- Information: what the reader, protagonist, and opposition know at each stage.
|
||||
- Scene craft: entry pressure, concrete action, evidence, turn, consequence, and exit tension.
|
||||
- Prose behavior: viewpoint distance, sentence movement, dialogue subtext, detail selection, and paragraph rhythm.
|
||||
- Long-horizon state: facts, promises, hooks, unresolved costs, and continuity burdens.
|
||||
|
||||
## Output discipline
|
||||
|
||||
Anchor important claims to a chapter or excerpt. End with transferable principles, failure conditions, and several recombination options. Never output a disguised rewrite of the source.
|
||||
@@ -0,0 +1,17 @@
|
||||
---
|
||||
name: inkos-long-writing
|
||||
description: 长篇小说建书、续写、方向调整与章节协作。Use for long-form creation while preserving author intent, canon, and review gates.
|
||||
---
|
||||
# Long-form writing
|
||||
|
||||
Use this skill for creating, planning, continuing, or substantially revising a long-form book.
|
||||
|
||||
- Conversation comes first. A discussion, question, or request for options is not permission to write or edit.
|
||||
- In general chat, propose the appropriate action and wait for confirmation. In an active book, use the existing `sub_agent` writer, auditor, or reviser rather than drafting chapter prose in chat.
|
||||
- Preserve the authority order: established facts and published chapters; durable book rules; current author direction; model defaults and genre convention.
|
||||
- When sources are bound to the book, use them only for the purposes the user stated. Never let a reference override author intent or canon, and never reproduce source wording.
|
||||
- Make each scene change conflict, evidence, emotion, relationship, knowledge, or consequence. Do not pad to satisfy length.
|
||||
- Report the real tool result and review status. Never claim a chapter was saved without a successful artifact result.
|
||||
- Respond in the user's language.
|
||||
|
||||
Load `references/collaboration-protocol.md` when deciding between discussion, writing, revision, and canon change.
|
||||
@@ -0,0 +1,15 @@
|
||||
# Long-form collaboration protocol
|
||||
|
||||
## Decide the operation semantically
|
||||
|
||||
- Explore or compare directions: answer in conversation.
|
||||
- Create a new book: discuss the core, then propose confirmation.
|
||||
- Write the next chapter or several sequential chapters: use the writer pipeline after explicit execution intent.
|
||||
- Diagnose an existing chapter: use the auditor and show concrete findings.
|
||||
- Rewrite or restructure an existing chapter: use the reviser with the user's instruction and appropriate revision mode.
|
||||
- Change durable setting: explain conflicts, then use the truth-editing path after explicit authorization.
|
||||
- Retcon published history: stop and present the conflict and affected scope before editing.
|
||||
|
||||
## Completion
|
||||
|
||||
Completion comes from persisted artifacts and tool results, not assistant prose. Preserve failed review details and let the user choose whether to revise, accept, or change the standard.
|
||||
@@ -0,0 +1,17 @@
|
||||
---
|
||||
name: inkos-short-market-research
|
||||
description: 商业短篇市场、平台样本、标题与移动端阅读趋势研究。Use for evidence-based short-fiction market research.
|
||||
---
|
||||
# Short-fiction market research
|
||||
|
||||
Use this skill when the user asks what short fiction is working, how platforms differ, or which commercial direction to test.
|
||||
|
||||
- Use `research_web` for current platform and market claims; archive user-provided samples with `ingest_material`.
|
||||
- Study titles, openings, pressure chains, evidence chains, emotional gaps, reversals, payoffs, chapter titles, and mobile-reading density.
|
||||
- Separate a durable mechanism from a temporary surface trend.
|
||||
- Do not turn benchmark research into plagiarism detection or a fixed tag table.
|
||||
- Recommend several recombinations with different characters, causes, evidence, and consequences.
|
||||
- Research does not authorize generation. If the user wants a short produced, move through the normal confirmation flow.
|
||||
- Respond in the user's language.
|
||||
|
||||
Load `references/short-market-rubric.md` for a full benchmark report.
|
||||
@@ -0,0 +1,16 @@
|
||||
# Short-fiction benchmark rubric
|
||||
|
||||
Evaluate real samples on:
|
||||
|
||||
- title promise and mobile thumbnail readability;
|
||||
- first-screen pressure and unanswered question;
|
||||
- protagonist desire expressed through action;
|
||||
- antagonist or pressure-source motivation;
|
||||
- evidence, misunderstanding, and information asymmetry;
|
||||
- emotional deficit before payoff;
|
||||
- causal setup before reversal;
|
||||
- scene-by-scene anti-padding value;
|
||||
- chapter-end continuation pressure;
|
||||
- ending payoff and residual emotion.
|
||||
|
||||
The report should explain why a mechanism works, when it fails, and how to recombine it without preserving the source's expression.
|
||||
@@ -0,0 +1,16 @@
|
||||
---
|
||||
name: inkos-short-story-analysis
|
||||
description: 商业短篇拆稿、情绪链、证据链和反转机制分析。Use to deconstruct a short story into transferable craft.
|
||||
---
|
||||
# Short-story analysis
|
||||
|
||||
Use this skill when the user supplies a complete short story or sample and asks why it works, how it is structured, or what can be learned from it.
|
||||
|
||||
- Read the whole supplied story when possible; do not infer the ending from the opening.
|
||||
- Preserve traceability with `ingest_material` for uploaded files, PDFs, URLs, or long text assets.
|
||||
- Analyze the story core, character motivation, pressure chain, evidence chain, emotional movement, information gap, reversal setup, payoff, opening, chapter titles, and mobile prose density.
|
||||
- Quote sparingly and point to evidence. Extract methods rather than replacement prose.
|
||||
- Do not silently turn analysis into a new short-fiction run.
|
||||
- Respond in the user's language.
|
||||
|
||||
Load `references/deconstruction-template.md` for a complete Markdown analysis.
|
||||
@@ -0,0 +1,14 @@
|
||||
# Short-story decomposition template
|
||||
|
||||
1. One-sentence story core and reader promise.
|
||||
2. Character table: desire, wound, tactic, misbelief, cost, and change.
|
||||
3. Opening hook: concrete event, pressure, and missing answer.
|
||||
4. Scene chain: input pressure, action, new evidence, emotional movement, consequence.
|
||||
5. Evidence and information chain.
|
||||
6. Reversal chain: setup, reader expectation, reveal, causal validity, payoff.
|
||||
7. Emotional curve and reader deficit/reward.
|
||||
8. Title and chapter-title functions.
|
||||
9. Prose mechanics and mobile readability.
|
||||
10. Transferable mechanisms, anti-patterns, and original recombination directions.
|
||||
|
||||
Do not reduce the result to labels. Each conclusion needs source evidence and an explanation of causality.
|
||||
@@ -0,0 +1,18 @@
|
||||
---
|
||||
name: inkos-short-writing
|
||||
description: 12–18章商业短篇的构思、一次写完、整篇审改与包装。Use for confirmed standalone short-fiction production.
|
||||
---
|
||||
# Short-fiction writing
|
||||
|
||||
Use this skill when the user wants a complete standalone commercial short story, not a long serial or a discussion-only answer.
|
||||
|
||||
- Preserve the user's premise, emotional target, taboo, viewpoint, and ending preference. Ask only for a missing choice that would materially change the story.
|
||||
- In chat or short mode, use the normal proposal and confirmation flow before `short_fiction_run` or cover generation.
|
||||
- Build one main line and at most one supporting reversal line. Prefer 12–18 focused chapters and complete scenes over a synopsis montage.
|
||||
- Express personality and values through action. Every scene must move conflict, causality, evidence, emotion, payoff, or relationship.
|
||||
- Set up reversals and rewards through visible evidence; do not use sudden stupidity or unexplained luck.
|
||||
- Treat title, chapter titles, opening, full manuscript, synopsis, selling points, and cover direction as one coherent product.
|
||||
- Report partial failures honestly; a cover failure must not erase a successful manuscript.
|
||||
- Respond in the user's language.
|
||||
|
||||
Load `references/production-checklist.md` before a full production run or whole-story revision.
|
||||
@@ -0,0 +1,19 @@
|
||||
# Short-fiction production checklist
|
||||
|
||||
## Before writing
|
||||
|
||||
- Core conflict can sustain a complete short story.
|
||||
- Protagonist has an active desire and constrained tactic.
|
||||
- Pressure source has a credible interest, fear, or misunderstanding.
|
||||
- Opening creates a concrete emotional or practical deficit.
|
||||
- Evidence and setup exist before each major reversal.
|
||||
- Ending delivers the promised emotional result and cost.
|
||||
|
||||
## During whole-story review
|
||||
|
||||
- No scene exists only to explain.
|
||||
- Later chapters do not collapse into summary.
|
||||
- Chapter transitions preserve causality.
|
||||
- Names, facts, objects, and evidence remain consistent.
|
||||
- Mobile paragraphs remain readable without turning prose into fragments.
|
||||
- Title and synopsis promise only events the manuscript actually delivers.
|
||||
@@ -0,0 +1,17 @@
|
||||
---
|
||||
name: inkos-story-cover
|
||||
description: 根据作品、平台与用户视觉要求设计或生成封面。Use for story-cover direction and confirmed cover generation.
|
||||
---
|
||||
# Story cover
|
||||
|
||||
Use this skill when the user wants cover strategy, a cover prompt, generation, or regeneration.
|
||||
|
||||
- Start from the actual work: title, genre, protagonist, central conflict, emotional promise, platform, and display size.
|
||||
- Treat the user's visual instructions as authoritative. Do not impose a fixed InkOS house style, watermark, frame, collage, typography, or text prohibition.
|
||||
- Distinguish a mobile book cover, cinematic key art, interactive-world scene image, character image, and item image. They serve different purposes.
|
||||
- In chat, discuss or propose the action. Generate only through the available `generate_cover` confirmation path.
|
||||
- Do not promise textual accuracy from an image model. If deterministic title layout is available, separate background generation from typography.
|
||||
- Keep the cover faithful to the finished content; do not advertise events absent from the story.
|
||||
- Respond in the user's language.
|
||||
|
||||
Load `references/cover-brief.md` when converting a manuscript into a production-ready visual brief.
|
||||
@@ -0,0 +1,16 @@
|
||||
# Cover brief
|
||||
|
||||
Capture only decisions supported by the user or work:
|
||||
|
||||
- artifact type and target platform;
|
||||
- aspect ratio and thumbnail context;
|
||||
- title treatment requirements, if any;
|
||||
- focal subject, expression, pose, and viewpoint;
|
||||
- one central conflict signal;
|
||||
- one or two legible genre objects;
|
||||
- palette, material, lighting, and atmosphere;
|
||||
- prohibited elements;
|
||||
- continuity references for recurring characters or places;
|
||||
- whether typography is image-generated or applied deterministically later.
|
||||
|
||||
Evaluate at final size and thumbnail size. A beautiful image that hides genre, conflict, or title hierarchy may still be a poor cover.
|
||||
@@ -0,0 +1,16 @@
|
||||
---
|
||||
name: inkos-story-deslop
|
||||
description: 识别并修复空泛、模板化、总结腔和其他AI写作痕迹,同时保留作者声音。Use for semantic prose cleanup, not keyword deletion.
|
||||
---
|
||||
# Semantic prose cleanup
|
||||
|
||||
Use this skill when the user says prose feels generic, mechanical, over-explained, repetitive, or AI-generated.
|
||||
|
||||
- Diagnose by reading function and effect, not by counting banned words or applying global replacements.
|
||||
- Distinguish a real defect from a legitimate voice choice. Repetition can be rhythm; abstraction can be intentional; short sentences can be pressure.
|
||||
- Look for unsupported conclusions, emotion labels without scene evidence, generic transitions, symmetrical canned phrasing, repeated interpretation, decorative detail, summary replacing scenes, and dialogue that only transfers information.
|
||||
- In an active book, audit first when scope is unclear. Use the reviser for an authorized rewrite; do not paste a replacement chapter into chat and claim it was saved.
|
||||
- Preserve plot facts, viewpoint, character voice, evidence, pacing function, and strong original lines.
|
||||
- Respond in the user's language.
|
||||
|
||||
Load `references/semantic-cleanup.md` for a passage-level diagnosis or revision brief.
|
||||
@@ -0,0 +1,13 @@
|
||||
# Semantic cleanup rubric
|
||||
|
||||
Ask of every suspect passage:
|
||||
|
||||
1. What story function should this passage perform?
|
||||
2. Is that function visible through action, image, evidence, choice, or consequence?
|
||||
3. Does the narrator explain an emotion already demonstrated?
|
||||
4. Could the sentence fit any character or story unchanged?
|
||||
5. Is detail carrying evidence, relationship, pressure, or voice?
|
||||
6. Is dialogue motivated, or is it disguised exposition?
|
||||
7. Does sentence rhythm match the scene's physical and emotional movement?
|
||||
|
||||
Repair by restoring concrete cause and character-specific behavior. Do not mechanically delete transitions, metaphors, three-part lists, or every occurrence of a flagged word.
|
||||
@@ -0,0 +1,19 @@
|
||||
---
|
||||
name: inkos-story-import
|
||||
description: 导入已有小说、逆向重建设定与续写工程,并区分母本参考。Use for importing manuscripts or attaching external canon safely.
|
||||
---
|
||||
# Story import and reconstruction
|
||||
|
||||
Use this skill when the user wants an existing manuscript to become an editable, continuable InkOS project.
|
||||
|
||||
- Determine the user's intent before acting:
|
||||
- `import_chapters`: the text becomes real chapters in the active book and InkOS reconstructs state.
|
||||
- `ingest_material`: the text remains reference material only.
|
||||
- `manage_book_reference`: an archived source should guide future chapters for user-stated purposes.
|
||||
- Preserve chapter order with natural chapter-number ordering. Confirm the continuation point when the active book already has chapters.
|
||||
- Never treat a user's own manuscript as an imitation target by default.
|
||||
- After import, report imported chapter count, detected gaps, reconstructed facts, uncertainties, and the next safe continuation point.
|
||||
- Do not invent missing canon to make the project look complete. Mark uncertainty for later confirmation.
|
||||
- Respond in the user's language.
|
||||
|
||||
Load `references/reconstruction-rubric.md` for large or inconsistent manuscripts.
|
||||
@@ -0,0 +1,13 @@
|
||||
# Manuscript reconstruction rubric
|
||||
|
||||
Reconstruct only what the text supports:
|
||||
|
||||
- chapter order and titles;
|
||||
- character identity, voice, goals, relationships, and last known state;
|
||||
- world rules and exceptions;
|
||||
- timeline and location changes;
|
||||
- revealed facts versus reader-facing uncertainty;
|
||||
- planted, advanced, resolved, and dormant hooks;
|
||||
- current conflict, unresolved costs, and immediate continuation pressure.
|
||||
|
||||
For contradictions, retain both evidence pointers and mark the conflict. Do not silently choose one version. Imported chapter prose is authoritative unless the user explicitly requests a rewrite or retcon.
|
||||
@@ -0,0 +1,16 @@
|
||||
---
|
||||
name: inkos-story-review
|
||||
description: 按题材、目标读者和用户标准审稿,展示具体问题并协作修订。Use for chapter or manuscript review with transparent criteria.
|
||||
---
|
||||
# Story review
|
||||
|
||||
Use this skill when the user wants diagnosis, scoring, comparison, approval, or revision advice for existing prose.
|
||||
|
||||
- First identify the applicable standard from genre, target audience, tone, platform, and explicit user preference. Everyday comedy, literary fiction, romance, mystery, and commercial serials should not share one logic-density threshold.
|
||||
- In an active book, use the auditor for persisted chapters. Show concrete issue descriptions, severity, evidence, and likely reader impact.
|
||||
- A parser or model-format failure is an audit failure, not evidence that the prose is bad and not permission to rewrite it.
|
||||
- Do not revise merely because an audit failed. Revise when the user asks, or when the confirmed workflow authorizes it; report whether the revised artifact was actually applied.
|
||||
- Preserve the user's voice and successful passages. Prefer the smallest scope that resolves the real defect.
|
||||
- Respond in the user's language.
|
||||
|
||||
Load `references/review-matrix.md` for a full review or when standards are disputed.
|
||||
@@ -0,0 +1,21 @@
|
||||
# Adaptive review matrix
|
||||
|
||||
Always inspect:
|
||||
|
||||
- user intent and durable constraints;
|
||||
- continuity and causal coherence;
|
||||
- character motivation and voice;
|
||||
- scene function and consequence;
|
||||
- information and evidence consistency;
|
||||
- prose clarity and viewpoint stability;
|
||||
- promised versus delivered emotion.
|
||||
|
||||
Adjust by form:
|
||||
|
||||
- Comedy may accept coincidence or exaggeration when setup, rhythm, and character logic support the joke.
|
||||
- Romance prioritizes relationship movement, subtext, and emotional credibility.
|
||||
- Mystery requires evidence visibility, fair inference, and controlled information release.
|
||||
- Commercial serials require continuation pressure without sacrificing scene completeness.
|
||||
- Literary work may trade plot velocity for perception, language, or thematic movement, but not for empty repetition.
|
||||
|
||||
For every blocking issue, include evidence, impact, and a repair direction. Separate objective contradiction from taste preference.
|
||||
@@ -4,11 +4,26 @@ import { delimiter, join } from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import {
|
||||
createSkillRegistry,
|
||||
loadAvailableAgentSkills,
|
||||
loadBuiltinAgentSkills,
|
||||
loadConfiguredAgentSkills,
|
||||
loadExternalAgentSkills,
|
||||
parseAgentSkillDocument,
|
||||
} from "../skills/index.js";
|
||||
|
||||
const BUILTIN_SKILL_IDS = [
|
||||
"inkos-long-market-research",
|
||||
"inkos-long-story-analysis",
|
||||
"inkos-long-writing",
|
||||
"inkos-short-market-research",
|
||||
"inkos-short-story-analysis",
|
||||
"inkos-short-writing",
|
||||
"inkos-story-cover",
|
||||
"inkos-story-deslop",
|
||||
"inkos-story-import",
|
||||
"inkos-story-review",
|
||||
] as const;
|
||||
|
||||
describe("external skill loader", () => {
|
||||
let root: string;
|
||||
|
||||
@@ -20,6 +35,56 @@ describe("external skill loader", () => {
|
||||
await import("node:fs/promises").then(({ rm }) => rm(root, { recursive: true, force: true }));
|
||||
});
|
||||
|
||||
it("loads the built-in professional story skill pack", async () => {
|
||||
const loaded = await loadBuiltinAgentSkills();
|
||||
|
||||
expect(loaded.diagnostics).toEqual([]);
|
||||
expect(loaded.skills.map((skill) => skill.id)).toEqual(BUILTIN_SKILL_IDS);
|
||||
expect(loaded.skills).toEqual(expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: "inkos-long-writing",
|
||||
source: "builtin",
|
||||
body: expect.stringContaining("established facts"),
|
||||
baseDir: expect.stringMatching(/skills[\\/]inkos-long-writing$/),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: "inkos-story-review",
|
||||
source: "builtin",
|
||||
body: expect.stringContaining("parser or model-format failure"),
|
||||
}),
|
||||
]));
|
||||
});
|
||||
|
||||
it("lets a project skill replace a built-in skill with the same id", async () => {
|
||||
const skillDir = join(root, ".agents", "skills", "inkos-story-review");
|
||||
await mkdir(skillDir, { recursive: true });
|
||||
await writeFile(
|
||||
join(skillDir, "SKILL.md"),
|
||||
[
|
||||
"---",
|
||||
"name: inkos-story-review",
|
||||
"description: Project-specific review standard.",
|
||||
"---",
|
||||
"Use the project's own review standard.",
|
||||
].join("\n"),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const loaded = await loadAvailableAgentSkills({
|
||||
projectRoot: root,
|
||||
env: {},
|
||||
homeDir: join(root, "home"),
|
||||
});
|
||||
const registry = createSkillRegistry({ skills: loaded.skills });
|
||||
|
||||
expect(registry.getSkill("inkos-story-review")).toMatchObject({
|
||||
source: "project",
|
||||
body: "Use the project's own review standard.",
|
||||
baseDir: skillDir,
|
||||
});
|
||||
expect(registry.listSkills()).toHaveLength(BUILTIN_SKILL_IDS.length);
|
||||
});
|
||||
|
||||
it("loads only the standard AgentSkills discovery fields and body", async () => {
|
||||
const skillDir = join(root, "detective-play");
|
||||
await mkdir(join(skillDir, "scripts"), { recursive: true });
|
||||
|
||||
@@ -65,7 +65,7 @@ import type { TranscriptEvent, TranscriptRole } from "../interaction/session-tra
|
||||
import type { PlayMode, SessionKind } from "../interaction/session.js";
|
||||
import type { ActionPayload, ActionSource, RequestedIntent } from "../interaction/action-envelope.js";
|
||||
import type { ContextCompressionCallback } from "../models/context-compression.js";
|
||||
import { createSkillRegistry, loadConfiguredAgentSkills } from "../skills/index.js";
|
||||
import { createSkillRegistry, loadAvailableAgentSkills } from "../skills/index.js";
|
||||
import { assertSafeBookId } from "../utils/book-id.js";
|
||||
import { PlayStore } from "../play/play-store.js";
|
||||
import { isLlmStubEnabled, stubAgentStream } from "./llm-stub.js";
|
||||
@@ -1018,7 +1018,7 @@ async function runAgentSessionUnlocked(
|
||||
const requestedIntent = config.requestedIntent;
|
||||
const actionPayload = config.actionPayload;
|
||||
const actionPayloadKey = actionPayloadCacheKey(actionPayload);
|
||||
const configuredSkills = await loadConfiguredAgentSkills({ projectRoot });
|
||||
const configuredSkills = await loadAvailableAgentSkills({ projectRoot });
|
||||
const skillRegistry = createSkillRegistry({ skills: configuredSkills.skills });
|
||||
const skillResolution = skillRegistry.resolveSkills({
|
||||
requestedSkills: config.requestedSkills,
|
||||
|
||||
@@ -139,6 +139,8 @@ export {
|
||||
export {
|
||||
AgentSkillSchema,
|
||||
createSkillRegistry,
|
||||
loadAvailableAgentSkills,
|
||||
loadBuiltinAgentSkills,
|
||||
loadConfiguredAgentSkills,
|
||||
loadExternalAgentSkills,
|
||||
parseAgentSkillDocument,
|
||||
@@ -146,6 +148,7 @@ export {
|
||||
type CreateSkillRegistryOptions,
|
||||
type ExternalSkillDiagnostic,
|
||||
type LoadConfiguredAgentSkillsInput,
|
||||
type LoadAvailableAgentSkillsResult,
|
||||
type LoadExternalAgentSkillsInput,
|
||||
type LoadExternalAgentSkillsResult,
|
||||
type ParseAgentSkillDocumentOptions,
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { fileURLToPath } from "node:url";
|
||||
import {
|
||||
loadConfiguredAgentSkills,
|
||||
loadExternalAgentSkills,
|
||||
type ExternalSkillDiagnostic,
|
||||
type LoadConfiguredAgentSkillsInput,
|
||||
} from "./external-loader.js";
|
||||
import type { AgentSkill } from "./types.js";
|
||||
|
||||
export interface LoadAvailableAgentSkillsResult {
|
||||
readonly skills: ReadonlyArray<AgentSkill>;
|
||||
readonly diagnostics: ReadonlyArray<ExternalSkillDiagnostic>;
|
||||
}
|
||||
|
||||
export async function loadBuiltinAgentSkills(
|
||||
builtinRoot = builtinSkillsRoot(),
|
||||
): Promise<LoadAvailableAgentSkillsResult> {
|
||||
return loadExternalAgentSkills({
|
||||
externalDirs: [builtinRoot],
|
||||
source: "builtin",
|
||||
});
|
||||
}
|
||||
|
||||
export async function loadAvailableAgentSkills(
|
||||
input: LoadConfiguredAgentSkillsInput,
|
||||
): Promise<LoadAvailableAgentSkillsResult> {
|
||||
const [builtin, configured] = await Promise.all([
|
||||
loadBuiltinAgentSkills(),
|
||||
loadConfiguredAgentSkills(input),
|
||||
]);
|
||||
return {
|
||||
// Registry de-duplication is last-write-wins, so project/user skills can
|
||||
// intentionally replace an InkOS default with the same AgentSkills id.
|
||||
skills: [...builtin.skills, ...configured.skills],
|
||||
diagnostics: [...builtin.diagnostics, ...configured.diagnostics],
|
||||
};
|
||||
}
|
||||
|
||||
function builtinSkillsRoot(): string {
|
||||
return fileURLToPath(new URL("../../skills", import.meta.url));
|
||||
}
|
||||
@@ -8,6 +8,11 @@ export {
|
||||
type LoadExternalAgentSkillsResult,
|
||||
type ParseAgentSkillDocumentOptions,
|
||||
} from "./external-loader.js";
|
||||
export {
|
||||
loadAvailableAgentSkills,
|
||||
loadBuiltinAgentSkills,
|
||||
type LoadAvailableAgentSkillsResult,
|
||||
} from "./builtin-loader.js";
|
||||
export { createSkillRegistry, type CreateSkillRegistryOptions } from "./registry.js";
|
||||
export {
|
||||
AgentSkillSchema,
|
||||
|
||||
@@ -5,7 +5,7 @@ export const AgentSkillSchema = z.object({
|
||||
name: z.string().min(1).max(64),
|
||||
description: z.string().min(1).max(1024),
|
||||
body: z.string().default(""),
|
||||
source: z.enum(["project", "user", "external"]).default("external"),
|
||||
source: z.enum(["builtin", "project", "user", "external"]).default("external"),
|
||||
baseDir: z.string().min(1).optional(),
|
||||
}).strict();
|
||||
export type AgentSkill = z.infer<typeof AgentSkillSchema>;
|
||||
|
||||
@@ -15,7 +15,7 @@ describe("Studio skill endpoints", () => {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("lists project and external SKILL.md folders without implicit built-ins", async () => {
|
||||
it("lists built-in professional skills alongside editable project skills", async () => {
|
||||
await mkdir(join(root, ".agents", "skills", "detective-play"), { recursive: true });
|
||||
await writeFile(
|
||||
join(root, ".agents", "skills", "detective-play", "SKILL.md"),
|
||||
@@ -34,7 +34,11 @@ describe("Studio skill endpoints", () => {
|
||||
const json = await res.json() as { skills: Array<{ id: string; source: string; editable: boolean; body?: string }> };
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(json.skills.map((skill) => skill.source)).not.toContain("builtin");
|
||||
expect(json.skills).toContainEqual(expect.objectContaining({
|
||||
id: "inkos-long-writing",
|
||||
source: "builtin",
|
||||
editable: false,
|
||||
}));
|
||||
expect(json.skills).toContainEqual(expect.objectContaining({
|
||||
id: "detective-play",
|
||||
source: "project",
|
||||
@@ -44,6 +48,40 @@ describe("Studio skill endpoints", () => {
|
||||
expect(json.skills.find((skill) => skill.id === "detective-play")).not.toHaveProperty("whenToUse");
|
||||
});
|
||||
|
||||
it("lets an imported project skill override a built-in skill", async () => {
|
||||
const app = createStudioServer({} as never, root);
|
||||
const manifest = Buffer.from([
|
||||
"---",
|
||||
"name: inkos-story-review",
|
||||
"description: Review with this project's acceptance standard.",
|
||||
"---",
|
||||
"Apply the project-specific review standard.",
|
||||
].join("\n")).toString("base64");
|
||||
|
||||
const imported = await app.request("/api/v1/skills/import", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
files: [
|
||||
{ path: "inkos-story-review/SKILL.md", dataUrl: `data:text/markdown;base64,${manifest}` },
|
||||
],
|
||||
}),
|
||||
});
|
||||
expect(imported.status).toBe(200);
|
||||
|
||||
const listed = await app.request("/api/v1/skills");
|
||||
const json = await listed.json() as {
|
||||
skills: Array<{ id: string; source: string; editable: boolean; body: string }>;
|
||||
};
|
||||
const reviewSkills = json.skills.filter((skill) => skill.id === "inkos-story-review");
|
||||
|
||||
expect(reviewSkills).toEqual([expect.objectContaining({
|
||||
source: "project",
|
||||
editable: true,
|
||||
body: "Apply the project-specific review standard.",
|
||||
})]);
|
||||
});
|
||||
|
||||
it("does not expose the legacy JSON skill create or update protocol", async () => {
|
||||
const app = createStudioServer({} as never, root);
|
||||
|
||||
|
||||
@@ -352,6 +352,7 @@ vi.mock("@actalk/inkos-core", async (importOriginal) => {
|
||||
normalizeSkillIdList: actual.normalizeSkillIdList,
|
||||
createSkillRegistry: actual.createSkillRegistry,
|
||||
loadConfiguredAgentSkills: actual.loadConfiguredAgentSkills,
|
||||
loadAvailableAgentSkills: actual.loadAvailableAgentSkills,
|
||||
createTranslationCreateTool: actual.createTranslationCreateTool,
|
||||
createLLMTranslationModel: createLLMTranslationModelMock,
|
||||
createTranslationProjectFromFile: actual.createTranslationProjectFromFile,
|
||||
|
||||
@@ -71,7 +71,7 @@ import {
|
||||
inferLanguage,
|
||||
ingestMaterial,
|
||||
createSkillRegistry,
|
||||
loadConfiguredAgentSkills,
|
||||
loadAvailableAgentSkills,
|
||||
parseAgentSkillDocument,
|
||||
getBuiltinPrompt,
|
||||
listBuiltinPromptPacks,
|
||||
@@ -934,7 +934,7 @@ async function importStudioSkillFolder(
|
||||
}
|
||||
|
||||
async function loadStudioSkills(root: string) {
|
||||
const configured = await loadConfiguredAgentSkills({ projectRoot: root });
|
||||
const configured = await loadAvailableAgentSkills({ projectRoot: root });
|
||||
const projectSkillIds = await listProjectSkillIds(root);
|
||||
const registry = createSkillRegistry({ skills: configured.skills });
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user