Skip to content
Merged
5 changes: 5 additions & 0 deletions release-please-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@
{
"type": "generic",
"path": "src/bin/akua.ts"
},
{
"type": "json",
"path": "skills/akua/skill-package.json",
"jsonpath": "$.version"
}
]
}
Expand Down
60 changes: 60 additions & 0 deletions skills/akua/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
---
name: akua
description: Guide agents working with Akua documentation, workspaces, authentication, generated CLI commands, structured output, and approval-safe operations. Use for Akua setup, inspection, troubleshooting, or change requests.
---

# Work with Akua

Use the most authoritative available surface for each kind of information. Keep discovery and reads separate from mutations, and report what actually happened.

## Choose the authoritative surface

1. Prefer the Akua docs MCP for current product and API documentation. Treat remembered documentation, copied examples, and model knowledge as potentially stale.
2. Use the Akua platform MCP and its Code Mode for authoritative workspace state. Read the relevant objects before drawing conclusions or proposing changes.
3. Use the canonical `akua` CLI when it is present and the needed behavior is released. Inspect the installed surface instead of assuming a command or install channel exists.

If an authoritative surface is unavailable, say which check could not be made. Do not substitute an inferred live state.

## Inspect the installed CLI

Start with read-only discovery:

```sh
command -v akua
akua --version
akua --help --output agent
akua auth status --output agent
akua commands --output agent
```

Use `akua commands --resource <resource>` or `akua commands --operation-id <operation_id>` to narrow the generated public command registry. Registry presence describes the generated surface; it does not prove that execution is implemented. If a command reports that it is not implemented, return to the docs or platform MCP instead of inventing flags or claiming success.

Agents must never place authentication tokens in tool arguments, process argv, transcripts, or logs. When authentication setup is requested, instruct the human to run `akua auth login --token <token>` privately and locally themselves. Afterward, agents may run `akua auth status --output agent` to confirm the effective source without revealing the credential. Agents may run `akua auth logout` only when explicitly requested.

## Select structured output deliberately

Akua supports `human`, `agent`, `json`, and `quiet` output modes.

- Set `AGENT=true` or `AGENT=<name>` to select agent-oriented output by default.
- Use `--output agent` for compact, self-describing AGENT output with observations, data, and next steps.
- Use `--json` or `--output json` when a program must parse the result.
- Use `--quiet` only when the exit status is sufficient.

Check the exit status and structured error payload before reporting success. Do not parse human prose when a structured mode is available.

## Preserve approvals and mutation boundaries

- Read current workspace state before planning a change. Use explicit workspace and resource identifiers.
- Do not create, update, delete, deploy, or resolve an approval request unless the human explicitly authorized that action.
- Preserve human approval gates. Do not add `--yes`, `--force`, confirmation responses, or approval resolutions merely to make automation continue.
- In agent, JSON, quiet, CI, or other non-interactive environments, never assume an interactive prompt will protect a mutation. Treat a confirmation-required or unsafe-mutation refusal as a stop condition.
- When a dry-run or preview is documented and available, inspect it before asking for mutation approval. Do not claim a preview changed live state.
- After an authorized mutation, read the authoritative state again and report identifiers, observed status, and any remaining approval or wait step.

Generated command discovery, desired configuration, and a submitted request are not evidence that live state changed. State clearly when execution is unavailable, refused, awaiting approval, or unverified.

## Avoid legacy and unavailable paths

Use only the `akua` executable. Do not suggest CNAP-era binaries, commands, module paths, domains, or compatibility aliases.

Do not claim that Akua is publicly installable through npm, Homebrew, `npx`, a skills marketplace, or another channel unless the current Akua docs MCP or a published release explicitly confirms it. If `akua` is not installed, report that fact and use the available MCP surfaces; do not invent installation instructions.
16 changes: 16 additions & 0 deletions skills/akua/skill-package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"schema_version": 1,
"name": "akua",
"version": "0.7.0",
"provenance": {
"repository": "https://github.com/akua-dev/cli",
"path": "skills/akua/SKILL.md",
"revision_source": "git"
},
"files": [
{
"path": "SKILL.md",
"sha256": "abec4823b7e9ea42d9e261ce598442101524e8309a01377f494cf54cdac6c5c3"
}
]
}
277 changes: 277 additions & 0 deletions test/agent-skill.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,277 @@
import { describe, expect, test } from "bun:test";
import { createHash } from "node:crypto";
import { readFileSync } from "node:fs";
import { basename, dirname } from "node:path";

const SKILL_PATH = "skills/akua/SKILL.md";
const PACKAGE_PATH = "skills/akua/skill-package.json";
const SKILL_NAME = "akua";
const SOURCE_REPOSITORY = "https://github.com/akua-dev/cli";
const FRONTMATTER_FIELDS = new Set([
"name",
"description",
"license",
"compatibility",
"metadata",
"allowed-tools",
]);

interface SkillPackage {
schema_version: number;
name: string;
version: string;
provenance: {
repository: string;
path: string;
revision_source: string;
};
files: Array<{
path: string;
sha256: string;
}>;
}

describe("canonical Akua agent skill", () => {
test("is a valid, release-versioned Agent Skills artifact", () => {
const skill = readFileSync(SKILL_PATH, "utf8");
const skillPackage = readSkillPackage(PACKAGE_PATH);
const rootPackage = JSON.parse(readFileSync("package.json", "utf8")) as { version: string };

expect(() => validateFrontmatter(SKILL_PATH, skill)).not.toThrow();
expect(() => validateSkillPackage(skillPackage, skill, rootPackage.version)).not.toThrow();
});

test("directs authentication setup through the human without exposing tokens", () => {
const skill = readFileSync(SKILL_PATH, "utf8");

expect(skill).toContain(
"Agents must never place authentication tokens in tool arguments, process argv, transcripts, or logs.",
);
expect(skill).toContain(
"instruct the human to run `akua auth login --token <token>` privately and locally themselves",
);
expect(skill).toContain("agents may run `akua auth status --output agent`");
});

test("accepts standard optional frontmatter fields in any key order", () => {
const reordered = skillWithFrontmatter(`allowed-tools: Bash(git:*) Read
metadata:
author: akua-dev
version: "1.0"
description: Use when working with Akua.
license: Apache-2.0
name: ${SKILL_NAME}
compatibility: Requires Bun 1.3.7 or newer`);

expect(() => validateFrontmatter(SKILL_PATH, reordered)).not.toThrow();
});

test("rejects unknown frontmatter fields", () => {
const unknown = skillWithFrontmatter(`name: ${SKILL_NAME}
description: Use when working with Akua.
version: 1.0.0`);

expect(() => validateFrontmatter(SKILL_PATH, unknown)).toThrow("unknown frontmatter field: version");
});

test("rejects invalid optional frontmatter field values", () => {
const invalidFields = [
"license: []",
"compatibility: 1",
`compatibility: ${"x".repeat(501)}`,
"metadata: []",
"metadata:\n version: 1",
"allowed-tools: []",
];

for (const field of invalidFields) {
const invalid = skillWithFrontmatter(`name: ${SKILL_NAME}
description: Use when working with Akua.
${field}`);

expect(() => validateFrontmatter(SKILL_PATH, invalid)).toThrow();
}
});

test("rejects invalid descriptions", () => {
const missing = skillWithFrontmatter(`name: ${SKILL_NAME}`);
const blank = skillWithFrontmatter(`name: ${SKILL_NAME}
description: " "`);

expect(() => validateFrontmatter(SKILL_PATH, missing)).toThrow("description must be a non-empty string");
expect(() => validateFrontmatter(SKILL_PATH, blank)).toThrow("description must be a non-empty string");
});

test("rejects invalid YAML frontmatter syntax", () => {
const invalidYaml = `---\nname: ${SKILL_NAME}\ndescription: [unterminated\n---\n# Akua\n`;

expect(() => validateFrontmatter(SKILL_PATH, invalidYaml)).toThrow();
});

test("rejects skill path and name drift", () => {
const drifted = `---\nname: another-skill\ndescription: Use when working with Akua.\n---\n# Akua\n`;

expect(() => validateFrontmatter(SKILL_PATH, drifted)).toThrow("must match its parent directory");
});

test("rejects task-shaped public skill identities", () => {
const taskShapedPath = "skills/agent-skill-compliance-task/SKILL.md";
const taskShaped = `---\nname: agent-skill-compliance-task\ndescription: Use when working with Akua.\n---\n# Akua\n`;

expect(() => validateFrontmatter(taskShapedPath, taskShaped)).toThrow("must use the canonical Akua identity");
});

test("rejects stale version and provenance metadata", () => {
const skill = validSkill();
const staleVersion = { ...validSkillPackage(skill), version: "0.0.0" };
const staleProvenance = {
...validSkillPackage(skill),
provenance: { ...validSkillPackage(skill).provenance, repository: "https://github.com/akua-dev/skills" },
};

expect(() => validateSkillPackage(staleVersion, skill, "1.2.3")).toThrow("version must match");
expect(() => validateSkillPackage(staleProvenance, skill, "1.2.3")).toThrow("provenance must identify");
});

test("rejects non-deterministic or stale package contents", () => {
const skill = validSkill();
const generatedAt = { ...validSkillPackage(skill), generated_at: new Date().toISOString() };
const staleDigest = {
...validSkillPackage(skill),
files: [{ path: "SKILL.md", sha256: "0".repeat(64) }],
};

expect(() => validateSkillPackage(generatedAt, skill, "1.2.3")).toThrow("package keys");
expect(() => validateSkillPackage(staleDigest, skill, "1.2.3")).toThrow("deterministic SKILL.md digest");
});

test("release automation keeps skill package version metadata current", () => {
const config = JSON.parse(readFileSync("release-please-config.json", "utf8")) as {
packages?: Record<string, { "extra-files"?: unknown[] }>;
};

expect(config.packages?.["."]?.["extra-files"]).toContainEqual({
type: "json",
path: PACKAGE_PATH,
jsonpath: "$.version",
});
});
});

function validateFrontmatter(path: string, source: string): void {
const match = source.match(/^---\n([\s\S]*?)\n---\n([\s\S]+)$/);
if (!match) {
throw new Error("SKILL.md must contain YAML frontmatter followed by Markdown");
}

const parsed: unknown = Bun.YAML.parse(match[1]);
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
throw new Error("frontmatter must be a YAML mapping");
}

const fields = parsed as Record<string, unknown>;
const unknownFields = Object.keys(fields).filter((field) => !FRONTMATTER_FIELDS.has(field));
if (unknownFields.length > 0) {
throw new Error(`unknown frontmatter field: ${unknownFields.join(", ")}`);
}

const name = fields.name;
const description = fields.description;
if (typeof name !== "string" || !name) {
throw new Error("skill name must be a non-empty string");
}
if (typeof description !== "string" || !description.trim()) {
throw new Error("skill description must be a non-empty string");
}
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name) || name.length > 64) {
throw new Error("skill name must follow the Agent Skills naming rules");
}
if (name !== basename(dirname(path))) {
throw new Error("skill name must match its parent directory");
}
if (name !== SKILL_NAME) {
throw new Error("skill name must use the canonical Akua identity");
}
if (description.length > 1024) {
throw new Error("skill description must not exceed 1024 characters");
}

validateOptionalString(fields, "license");
validateOptionalString(fields, "allowed-tools");

const compatibility = validateOptionalString(fields, "compatibility");
if (compatibility !== undefined && (compatibility.length === 0 || compatibility.length > 500)) {
throw new Error("skill compatibility must contain 1-500 characters");
}

const metadata = fields.metadata;
if (metadata !== undefined) {
if (typeof metadata !== "object" || metadata === null || Array.isArray(metadata)) {
throw new Error("skill metadata must be a string-to-string mapping");
}
if (Object.values(metadata).some((value) => typeof value !== "string")) {
throw new Error("skill metadata must be a string-to-string mapping");
}
}
}

function validateOptionalString(fields: Record<string, unknown>, field: string): string | undefined {
const value = fields[field];
if (value !== undefined && typeof value !== "string") {
throw new Error(`skill ${field} must be a string`);
}
return value;
}

function validateSkillPackage(skillPackage: SkillPackage, skill: string, rootVersion: string): void {
if (Object.keys(skillPackage).join(",") !== "schema_version,name,version,provenance,files") {
throw new Error("package keys must be stable and exclude generated timestamps");
}
if (skillPackage.schema_version !== 1 || skillPackage.name !== SKILL_NAME) {
throw new Error("package schema and name must identify the canonical skill");
}
if (skillPackage.version !== rootVersion) {
throw new Error("skill package version must match the CLI release version");
}
if (
JSON.stringify(skillPackage.provenance) !==
JSON.stringify({ repository: SOURCE_REPOSITORY, path: SKILL_PATH, revision_source: "git" })
) {
throw new Error("package provenance must identify the CLI-owned source and containing git revision");
}

const expectedFiles = [{ path: "SKILL.md", sha256: sha256(skill) }];
if (JSON.stringify(skillPackage.files) !== JSON.stringify(expectedFiles)) {
throw new Error("package files must contain the deterministic SKILL.md digest");
}
}

function readSkillPackage(path: string): SkillPackage {
return JSON.parse(readFileSync(path, "utf8")) as SkillPackage;
}

function sha256(value: string): string {
return createHash("sha256").update(value).digest("hex");
}

function validSkill(): string {
return skillWithFrontmatter(`name: ${SKILL_NAME}\ndescription: Use when working with Akua.`);
}

function skillWithFrontmatter(frontmatter: string): string {
return `---\n${frontmatter}\n---\n# Akua\n`;
}

function validSkillPackage(skill: string): SkillPackage {
return {
schema_version: 1,
name: SKILL_NAME,
version: "1.2.3",
provenance: {
repository: SOURCE_REPOSITORY,
path: SKILL_PATH,
revision_source: "git",
},
files: [{ path: "SKILL.md", sha256: sha256(skill) }],
};
}
6 changes: 6 additions & 0 deletions test/release-please-config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ interface ReleasePleaseConfig {
"extra-files"?: Array<{
type?: string;
path?: string;
jsonpath?: string;
}>;
}>;
}
Expand All @@ -27,6 +28,11 @@ describe("release-please configuration", () => {
type: "generic",
path: "src/bin/akua.ts",
},
{
type: "json",
path: "skills/akua/skill-package.json",
jsonpath: "$.version",
},
],
});
expect(config["include-component-in-tag"]).toBe(false);
Expand Down
Loading