feat: add Archipelago agent skills and app starter

This commit is contained in:
archipelago
2026-10-09 07:31:06 -04:00
parent 940b28dd9b
commit 820df9a196
21 changed files with 484 additions and 0 deletions
+49
View File
@@ -0,0 +1,49 @@
---
name: archipelago-app
description: Build, package, test, and update a manifest-driven Archipelago app using the real app developer contract, rootless runtime, and lifecycle acceptance flow.
metadata:
short-description: Build a real Archipelago app
---
# Archipelago app development
Use this skill when creating or changing an Archipelago app, manifest, container
build, app integration, credentials, signer flow, or app preview. Always load
`archipelago-design` for newly authored UI.
Read [app-contract.md](references/app-contract.md),
`docs/app-developer-guide.md`, and `docs/app-manifest-spec.md` before coding.
## Required loop
1. Create a dedicated worktree or project directory and choose the smallest
useful app scope. Prefer the maintained starter and its pinned dependencies.
2. Implement the app as a manifest, rootless container, persistent data path,
truthful health/readiness check, declared interface, and tests. Never add a
per-app Rust installer or rootful/Docker-socket shortcut.
3. Use generated secrets or declared secret files; never put credentials in a
manifest, image, logs, URL, or frontend bundle. Keep production wallets and
app databases outside development fixtures.
4. Validate the manifest and build context, then run the app through install,
start, stop, restart, manager restart, uninstall with data preservation, and
reinstall. Verify the real My Apps/Services launch path, not only a direct
port.
5. For UI, exercise standalone and embedded modes at 320, 390, 768, and 1440px
plus phone landscape, keyboard navigation, loading/empty/error/retry/success,
safe areas, and reduced motion.
6. Report source, disposable-node, actual-node, and release-artifact evidence
separately. Catalog publication is a separate request.
Important runtime facts:
- `/opt/archipelago/apps` is rebuilt from the runtime payload at backend start;
staging only there will be lost. Follow the developer guide's payload path.
- Signed catalog entries take precedence over disk manifests for catalog apps.
- Installed does not mean ready or launchable; use health and readiness evidence.
- App interfaces describe the service behind the gate. Do not hard-code a node
IP, scheme, app path, or host frame URL into app code.
## References
- [app contract and acceptance](references/app-contract.md)
- [design routing](../archipelago-design/SKILL.md)
@@ -0,0 +1,7 @@
interface:
display_name: "Archipelago app"
short_description: "Build a real Archipelago app"
brand_color: "#F7931A"
default_prompt: "Use $archipelago-app to build and validate this Archipelago app."
policy:
allow_implicit_invocation: true
@@ -0,0 +1,17 @@
# App contract
Start with `docs/app-developer-guide.md` and `docs/app-manifest-spec.md`; those
files are authoritative for fields and launch behavior. Validate with:
```bash
./scripts/validate-app-manifest.sh apps/<id>/manifest.yml
python3 scripts/generate-app-catalog.py
python3 scripts/check-app-catalog-drift.py --release --strict
```
Use pinned image versions, read-only root, no-new-privileges, minimal
capabilities, rootless Podman, declared persistent data under
`/var/lib/archipelago/<id>`, health checks, generated/declared secrets, and
truthful interfaces. Test install/start/stop/restart/manager-restart/uninstall
with data preservation/reinstall on a disposable node. Do not replace the
signed catalog to make a local test app appear.
@@ -0,0 +1,41 @@
---
name: archipelago-design
description: Create or change Archipelago UI using the shared semantic tokens, components, responsive patterns, accessibility states, and embedded/standalone host contract.
metadata:
short-description: Keep Archipelago UI consistent
---
# Archipelago design system
Use this skill for any new or changed Archipelago UI, including app screens,
Terminal, setup flows, dialogs, dashboards, and app wrappers. Read
[design-contract.md](references/design-contract.md) before implementation.
## Rules
- Find and reuse the closest shared component and pattern before creating one.
- Use semantic `--archy-*` tokens and the pinned kit version. A bespoke color,
font, radius, shadow, or z-index needs a documented reason.
- Preserve the dark baseline, readable surfaces, 4px spacing rhythm, bottom
action placement, 44px touch targets, visible focus, and safe-area behavior.
- Keep terminal/editor/tmux keys inside the terminal focus boundary; generic
modal Escape/arrow handlers must not consume them.
- Implement loading, empty, offline, denied, validation-error, retry, disabled,
and confirmed-success states. Status color must have text or icon support.
- Test standalone and embedded modes. Embedded apps do not duplicate the host
wallpaper or navigation and must work without a host handshake.
## Workflow
1. Read the reference screen and source; choose the matching component/pattern.
2. Build with the shared kit and local assets, not runtime dashboard CSS or CDN
fonts. Keep host RPC, signer, and credential bridges behind typed adapters.
3. Render the gallery/preview at required viewports and inspect screenshots and
keyboard behavior. Check contrast on the real composited surface.
4. Record deliberate exceptions and update the kit only when a reusable need is
demonstrated. Do not silently accept visual-diff changes.
## References
- [design contract](references/design-contract.md)
- [existing component map](references/component-map.md)
@@ -0,0 +1,7 @@
interface:
display_name: "Archipelago design"
short_description: "Keep Archipelago UI consistent"
brand_color: "#F7931A"
default_prompt: "Use $archipelago-design to implement this UI in Archipelago's design system."
policy:
allow_implicit_invocation: true
@@ -0,0 +1,15 @@
# Existing component map
Reuse these first:
- Structure: `BaseModal.vue`, `BackButton.vue`, `EmptyState.vue`,
`SkeletonCard.vue`.
- Controls: `ToggleSwitch.vue`, `PasswordRevealInput.vue`,
`AppSearchField.vue`, `CopyButton.vue`.
- Feedback: `ToastStack.vue`, `ContainerStatus.vue`, existing upload/progress
components.
- Completion: `PaymentSuccessPane.vue`, `IdentitySuccessPane.vue`.
- App flows: `AppLauncherOverlay.vue`, `AppCredentialInterstitial.vue`.
The terminal must have an explicit keyboard ownership boundary and must not be
wrapped in the generic arrow-key modal behavior without adapting it.
@@ -0,0 +1,18 @@
# Design contract
The current baseline is defined by `neode-ui/src/style.css`,
`neode-ui/tailwind.config.js`, `BaseModal.vue`, `AppSearchField.vue`,
`EmptyState.vue`, `SkeletonCard.vue`, and `PaymentSuccessPane.vue`. Preserve the
dark canvas, glass-card surface, 4px spacing scale, semantic orange accent,
local licensed fonts, bottom card actions, 40px desktop/52px mobile search,
44px touch targets, visible focus, dynamic viewport, safe-area and embedded
canvas rules while the shared kit is extracted.
Use semantic tokens, not raw values. New controls must document keyboard,
focus, disabled, loading, validation, error, retry, success, responsive, and
reduced-motion behavior. No essential action may be hover-only. Use the existing
dialog footer/content split and restore focus after close.
Visual acceptance covers 320/390/768/1440px, phone landscape, enlarged text,
standalone/embedded modes, dark native controls, no-blur fallback, and actual
Companion WebView behavior where applicable.
+53
View File
@@ -0,0 +1,53 @@
---
name: archipelago
description: Configure, troubleshoot, and safely modify an Archipelago node or its developer environment using the repository's typed operations, ownership rules, and recovery workflow.
metadata:
short-description: Work safely on Archipelago systems
---
# Archipelago system work
Use this skill for node configuration, terminal/developer setup, service
diagnosis, network and SSH work, supported app operations, and system changes.
For app implementation load `archipelago-app`; for any new or changed UI also
load `archipelago-design`.
Read [system-map.md](references/system-map.md) before acting. Read `AGENTS.md`
and the relevant project documentation in the current checkout.
## Workflow
1. Identify the node, repository/worktree, owner, and whether the request is
planning, source work, a disposable test, or a live-node operation.
2. Inspect current state before changing it. Prefer `archy`/Archipelago RPC
operations and existing scripts over direct edits or ad-hoc service commands.
3. Preserve wallets, app data, credentials, user uninstall decisions, and
unrelated services. Back up only the scoped state before a mutation.
4. Make the smallest reversible change. Keep generated state separate from
user overrides; never edit generated or packaged files when an owned source
or managed override exists.
5. Verify the actual result and report source tests, disposable integration,
live-node acceptance, and packaged-artifact checks separately.
For repository work, use a dedicated worktree and focused branch. For backend
unit tests use `scripts/test-backend-isolated.sh`; do not run unrestricted
`cargo test` on a node with installed apps. Do not publish OTA, ISO, catalog,
or Git mirrors from this skill unless that publication is explicitly requested
and every release gate is satisfied.
## Terminal and sessions
Treat terminal close as detach. Resume the existing named session; never create
a duplicate shell or replay a lost command. A reboot can restore workspace and
Codex conversation metadata but cannot restore the old process. Distinguish
running, detached, ended, and interrupted states in user-facing output.
Keep credentials, wallet material, terminal output, and command contents out of
diagnostics and support bundles. A terminal is a privileged capability: verify
the authenticated owner, origin, attachment grant, and account boundary before
any PTY bytes flow.
## References
- [system map and safe recipes](references/system-map.md)
- [app and design routing](references/routing.md)
@@ -0,0 +1,7 @@
interface:
display_name: "Archipelago system"
short_description: "Work safely on Archipelago systems"
brand_color: "#F7931A"
default_prompt: "Use $archipelago to inspect and safely change this Archipelago node."
policy:
allow_implicit_invocation: true
@@ -0,0 +1,10 @@
# Skill routing
Load `archipelago-app` for manifests, containers, app previews, lifecycle,
credentials, signer integration, or app packaging. Load `archipelago-design` for
any newly authored or changed UI. For backend-only work, use only the system
skill and the relevant project docs.
Planning stays planning. A source change, node mutation, deployment, or
publication requires that explicit scope from the user. A skill supplies
workflow knowledge; it does not grant additional privileges.
@@ -0,0 +1,19 @@
# System map and safe recipes
The native backend is `core/archipelago`; the Vue dashboard is `neode-ui`; ISO
and first-boot material lives under `image-recipe` and `scripts`; app manifests
are under `apps/<id>/manifest.yml`. Read `CLAUDE.md`, `AGENTS.md`, and the current
release checklist before release work.
Use the existing typed RPC and orchestration layers for app lifecycle, network,
wallet, identity, and service operations. Inspect a matching handler before
adding an endpoint. Preserve the existing session cookie, CSRF, Origin, and
app-gate protections.
For source tests, run frontend checks from `neode-ui`. Backend unit tests run
only through `scripts/test-backend-isolated.sh`; live host checks must be named
and explicit. Do not touch real wallet/payment/channel state for a test fixture.
Developer changes belong in a dedicated worktree. Keep generated catalog,
runtime payload, release artifacts, and local node state separate until the
request explicitly includes packaging or deployment.
@@ -0,0 +1,5 @@
node_modules/
dist/
.env
.env.*
!.env.example
@@ -0,0 +1,16 @@
FROM node:22-alpine AS build
WORKDIR /src
COPY package.json vite.config.ts index.html ./
COPY src ./src
RUN npm install --ignore-scripts && npm run build
FROM nginx:1.27-alpine
COPY --from=build /src/dist /usr/share/nginx/html
COPY <<'EOF' /etc/nginx/conf.d/default.conf
server {
listen 8080;
root /usr/share/nginx/html;
index index.html;
location / { try_files $uri $uri/ /index.html; }
}
EOF
@@ -0,0 +1,11 @@
# Archipelago app starter
This is a small standalone Vue/Vite app that demonstrates Archipelago's initial
design contract: semantic `--archy-*` tokens, dark native controls, responsive
cards, bottom actions, visible focus, 44px targets, and loading/empty/error/
success states. It intentionally has no dashboard dependency or host handshake.
Run `npm install && npm run dev` from this directory. Before packaging, copy the
source into a project, replace the demo state with a real API, pin dependencies,
and validate the manifest using the repository app developer guide. The Docker
file is a starter build example; it is not a substitute for lifecycle acceptance.
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#0a0a0a" />
<title>Archipelago app</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
@@ -0,0 +1,48 @@
app:
id: archipelago-app-starter
name: Archipelago App Starter
version: 0.1.0
description: A minimal Archipelago design-system starter with responsive states.
container:
build:
context: .
dockerfile: Dockerfile
tag: localhost/archipelago-app-starter:0.1.0
resources:
cpu_limit: 1
memory_limit: 128Mi
disk_limit: 256Mi
security:
capabilities: []
readonly_root: true
no_new_privileges: true
network_policy: isolated
ports:
- host: 8180
container: 8080
protocol: tcp
bind: 127.0.0.1
auth: gated
volumes:
- type: bind
source: /var/lib/archipelago/archipelago-app-starter
target: /data
options: [rw]
health_check:
type: http
endpoint: http://127.0.0.1:8080
path: /
interval: 30s
timeout: 5s
retries: 3
interfaces:
main:
name: Archipelago App Starter
description: Responsive design-system starter
type: ui
port: 8180
protocol: http
path: /
metadata:
category: development
tier: optional
@@ -0,0 +1,18 @@
{
"name": "archipelago-app-starter",
"private": true,
"version": "0.1.0",
"type": "module",
"scripts": {
"dev": "vite --host 127.0.0.1",
"build": "vite build",
"preview": "vite preview --host 127.0.0.1"
},
"dependencies": {
"@vitejs/plugin-vue": "^6.0.1",
"vite": "^7.2.2",
"typescript": "~5.9.3",
"vue": "^3.5.24"
},
"devDependencies": {}
}
@@ -0,0 +1,67 @@
<template>
<main class="archy-shell" aria-labelledby="page-title">
<header class="archy-header">
<div>
<p class="archy-eyebrow">ARCHIPELAGO APP</p>
<h1 id="page-title">Project workspace</h1>
<p class="archy-muted">A small, useful starting point for a node app.</p>
</div>
<button class="archy-button archy-button-secondary" type="button" @click="reload">
Refresh
</button>
</header>
<section class="archy-card" aria-labelledby="items-title">
<div class="archy-section-heading">
<div>
<p class="archy-eyebrow">YOUR DATA</p>
<h2 id="items-title">Items</h2>
</div>
<button class="archy-button" type="button" @click="addItem">Add item</button>
</div>
<div v-if="status === 'loading'" class="archy-state" role="status">Loading items…</div>
<div v-else-if="status === 'error'" class="archy-state archy-state-error" role="alert">
<strong>Items are unavailable.</strong>
<span>Check the app service and try again.</span>
<button class="archy-button archy-button-secondary" type="button" @click="reload">Try again</button>
</div>
<div v-else-if="items.length === 0" class="archy-state">
<strong>No items yet.</strong>
<span>Create one to see the app's normal success state.</span>
<button class="archy-button archy-button-secondary" type="button" @click="addItem">Create first item</button>
</div>
<ul v-else class="archy-list" aria-live="polite">
<li v-for="item in items" :key="item.id" class="archy-list-row">
<div>
<strong>{{ item.name }}</strong>
<span class="archy-muted">{{ item.detail }}</span>
</div>
<span class="archy-status">Ready</span>
</li>
</ul>
</section>
<p class="archy-footnote">This starter is standalone-safe and does not require a host handshake.</p>
</main>
</template>
<script setup lang="ts">
import { ref } from 'vue'
type Item = { id: number; name: string; detail: string }
const status = ref<'ready' | 'loading' | 'error'>('ready')
const items = ref<Item[]>([])
let nextId = 1
function addItem() {
status.value = 'ready'
items.value.push({ id: nextId, name: `Item ${nextId}`, detail: 'Persist this through your app API.' })
nextId += 1
}
function reload() {
status.value = 'loading'
window.setTimeout(() => { status.value = 'ready' }, 250)
}
</script>
@@ -0,0 +1,5 @@
import { createApp } from 'vue'
import App from './App.vue'
import './styles.css'
createApp(App).mount('#app')
@@ -0,0 +1,54 @@
:root {
color-scheme: dark;
--archy-canvas: #0a0a0a;
--archy-surface: rgba(0, 0, 0, 0.65);
--archy-surface-muted: rgba(255, 255, 255, 0.06);
--archy-border: rgba(255, 255, 255, 0.18);
--archy-text: rgba(255, 255, 255, 0.92);
--archy-muted: rgba(255, 255, 255, 0.58);
--archy-accent: #fb923c;
--archy-success: #4ade80;
--archy-danger: #f87171;
--archy-radius-card: 16px;
--archy-radius-control: 12px;
--archy-shadow: 0 8px 24px rgba(0, 0, 0, 0.45);
--archy-space-1: 4px;
--archy-space-2: 8px;
--archy-space-3: 12px;
--archy-space-4: 16px;
--archy-space-6: 24px;
--archy-space-8: 32px;
font-family: Avenir Next, system-ui, sans-serif;
background: var(--archy-canvas);
color: var(--archy-text);
}
* { box-sizing: border-box; }
body { margin: 0; min-width: 320px; background: var(--archy-canvas); }
button { font: inherit; }
button:focus-visible { outline: 2px solid var(--archy-accent); outline-offset: 3px; }
.archy-shell { width: min(100% - 32px, 960px); margin: 0 auto; padding: 40px 0 56px; }
.archy-header, .archy-section-heading { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--archy-space-4); }
.archy-header { margin-bottom: var(--archy-space-8); }
.archy-eyebrow { margin: 0 0 var(--archy-space-2); color: var(--archy-accent); font-size: 0.72rem; font-weight: 800; letter-spacing: 0.12em; }
h1, h2 { margin: 0; line-height: 1.15; }
h1 { font-size: clamp(1.8rem, 5vw, 2.8rem); }
h2 { font-size: 1.25rem; }
.archy-muted, .archy-footnote { color: var(--archy-muted); }
.archy-card { padding: var(--archy-space-6); background: var(--archy-surface); border: 1px solid var(--archy-border); border-radius: var(--archy-radius-card); box-shadow: var(--archy-shadow); }
.archy-section-heading { align-items: center; margin-bottom: var(--archy-space-6); }
.archy-button { min-height: 44px; padding: 10px 18px; border: 1px solid rgba(251, 146, 60, 0.35); border-radius: var(--archy-radius-control); background: rgba(251, 146, 60, 0.2); color: #fed7aa; cursor: pointer; }
.archy-button:hover { background: rgba(251, 146, 60, 0.3); }
.archy-button-secondary { border-color: var(--archy-border); background: var(--archy-surface-muted); color: var(--archy-text); }
.archy-state { display: grid; gap: var(--archy-space-3); padding: var(--archy-space-6); border-radius: var(--archy-radius-control); background: var(--archy-surface-muted); color: var(--archy-muted); }
.archy-state strong { color: var(--archy-text); }
.archy-state-error { border: 1px solid rgba(248, 113, 113, 0.35); }
.archy-state-error strong { color: var(--archy-danger); }
.archy-list { display: grid; gap: var(--archy-space-2); padding: 0; margin: 0; list-style: none; }
.archy-list-row { display: flex; align-items: center; justify-content: space-between; gap: var(--archy-space-4); padding: var(--archy-space-4); border-radius: var(--archy-radius-control); background: var(--archy-surface-muted); }
.archy-list-row div { display: grid; gap: var(--archy-space-1); min-width: 0; }
.archy-status { color: var(--archy-success); font-size: 0.8rem; }
.archy-footnote { margin: var(--archy-space-4) 0 0; font-size: 0.8rem; }
@media (max-width: 560px) { .archy-shell { width: min(100% - 24px, 960px); padding-top: 24px; } .archy-header { flex-direction: column; } .archy-header .archy-button { width: 100%; } .archy-card { padding: var(--archy-space-4); } .archy-section-heading { align-items: stretch; flex-direction: column; } .archy-section-heading .archy-button { width: 100%; } }
@media (prefers-reduced-motion: reduce) { *, *::before, *::after { scroll-behavior: auto !important; transition-duration: 0.01ms !important; animation-duration: 0.01ms !important; } }
@@ -0,0 +1,4 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({ plugins: [vue()] })