npx tsx state/lib/projects.ts .md file to compare - side-by-side diff against projects
projects
What it does for you
Organizes your work into projects with their own files and instructions.
What it produces
A recent result, so you can see the kind of work it returns.
loading…
How to get it
These run inside the Snappy workspace. Want this working in your business? I set skills like this up with you, in one focused week.
For developers how this skill is built, graded, and how it runs
at a glance- the short version
what's inside - the parts that make up a skill 3/4 present
A skill is just a few plain-text files. Only the main one is required. The rest are optional, added as the work needs them. This is what the skill is made of; how it runs is just below.
state/skills/projects/SKILL.md
present
state/lib/projects.ts
present
state/bin/projects/
not present
state/skills/projects/AGENTS.md
present
how it runs - the shared frame every skill uses 3/5 present
Every skill runs the same way. One part does the work, a separate part checks it, and a short loader hands the AI exactly what it needs for the job. Anything this skill doesn't use shows a one-line note saying why, on purpose, not by accident.
state/log/evals.ndjson - Atomic write only. Every write to project.json, instructions.md, and .index.json uses temp+rename. Partial files corrupt reads. Never write directly to final path.
- Slug is sticky. updateProject patches name and instructions only. Folder name never changes.
- Index is source of truth for location. Resolve via index, not by constructing join(PROJECTS_BASE, slug) — custom locations break that.
- ~/Documents/Snappy/Projects/ created on first call if missing. Do not pre-check in agent code.
- Corrupt rows silently skipped. listProjects drops missing/unparseable entries. Read-only, never repairs.
- Slug is pure ASCII kebab-case. slugify(name) is zero-dep. Falls back to uuid.substring(0,8) for empty/non-ASCII.
- +1 more in AGENTS.md →
what it has learned - fixes written back in over time sample
When a run hits something this skill didn't handle, the fix gets written back into the skill so it doesn't happen again. FIXED means it was corrected on the spot. LOGGED means it's queued for a bigger rewrite. Either way, the skill gets a little better and never makes the same mistake twice.
- Loading feedback rows…
how the work flows- step by step
SKILL.md- the skill, written out in plain English
projects
A Project is a first-class workspace unit: a named folder on disk containing project.json (metadata), instructions.md (freeform prose injected into threads), and files/ (user-dropped or agent-written files). Projects are indexed at ~/Documents/Snappy/Projects/.index.json (id → location). Threads and tasks attach to a project via projectId.
The lib at state/lib/projects.ts is the action scope. Server route wiring (/projects, /projects/:id, /projects/:id/files) is a separate brief.
Steps
listProjects(): Project[]- read the index, load each project.json and
instructions.md, compute file_count from files/, return sorted created_at desc. Corrupt or missing entries are silently skipped.
getProject(id): Project | null- lookup by id from the index. Returns
null if not found or corrupt.
createProject({ name, instructions?, location? }): Project- mint a v4
uuid, derive slug with slugify(name) (kebab-case ASCII). If ~/Documents/Snappy/Projects/<slug>/ already exists, append -2, -3 etc. until unique. Create project.json + instructions.md + files/ atomically (temp+rename). Update .index.json atomically last.
updateProject(id, patch): Project- rewriteproject.jsonand/or
instructions.md atomically, bump updated_at. Slug is sticky - only name and instructions are patchable.
deleteProject(id): { ok: boolean }- remove the folder recursively,
remove the index entry. Atomic index rewrite.
listProjectFiles(id): { name, size, mtime }[]- list files inside
<location>/files/, sorted by name. Returns [] when project not found.
Library API
state/lib/projects.ts exports all six functions and the Project type. Importable from any TS agent; also runnable as a CLI.
export type Project = {
id: string;
slug: string;
name: string;
instructions: string;
created_at: string;
updated_at: string;
location: string;
file_count: number;
};
export function listProjects(): Project[];
export function getProject(id: string): Project | null;
export function createProject(args: { name: string; instructions?: string; location?: string }): Project;
export function updateProject(id: string, patch: Partial<Pick<Project,"name"|"instructions">>): Project;
export function deleteProject(id: string): { ok: boolean };
export function listProjectFiles(id: string): { name: string; size: number; mtime: string }[];
export function slugify(name: string): string;
CLI:
npx tsx state/lib/projects.ts list
npx tsx state/lib/projects.ts create "My Project" "Optional instructions"
npx tsx state/lib/projects.ts get <id>
npx tsx state/lib/projects.ts update <id> "New Name"
npx tsx state/lib/projects.ts delete <id>
npx tsx state/lib/projects.ts files <id>
Storage shape
~/Documents/Snappy/Projects/
.index.json # [{id, location}] — atomic map
<slug>/
project.json # {id, slug, name, created_at, updated_at}
instructions.md # freeform prose, may be empty
files/ # user-owned files, empty at creation
~/Documents/Snappy/Projects/ is created on first call if missing.
Critical Rules
- Atomic writes only. Every write to
project.json,instructions.md,
and .index.json uses temp+rename. No partial files ever visible to readers.
- Slug is sticky.
updateProjectonly patchesnameandinstructions.
The slug is derived at creation time and never mutated.
- Slugify is a pure function. No third-party libs. Input: display name.
Output: kebab-case ASCII. Falls back to uuid.substring(0,8) for empty/ non-ASCII input.
- Index is source of truth for location. Projects can be stored outside
PROJECTS_BASE when location is supplied to createProject. Never assume <slug> == join(PROJECTS_BASE, slug) - always check the index.
~/Documents/Snappy/Projects/is local per-machine. Not gitignored
(user content), but not synced by the snappy-os sync layer either. A future sync hook is the right axis - do not move storage.
Eval rubric
criteria:
- name: index_integrity
kind: deterministic
score: 0/0.5/1
check: >
After createProject + deleteProject, the .index.json contains exactly
the surviving project ids. No dangling entries, no missing entries.
Corrupt (missing folder) entries are tolerated at read time but new
writes never produce them.
- name: atomic_write
kind: deterministic
score: 0/0.5/1
check: >
No .tmp file is visible to a concurrent listProjects() call during a
createProject() or updateProject() write. Temp files use the
`<filename>.tmp` convention and are renamed to final in a single syscall.
- name: slug_uniqueness
kind: deterministic
score: 0/0.5/1
check: >
createProject("Foo") twice results in folders test-foo/ and test-foo-2/
(or matching pattern), never two projects in the same folder. The slug
in project.json matches the folder name.
| Outcome | Score |
|---|---|
| All three criteria pass end-to-end | 1.0 |
| Atomic write holds but index has a stale entry | 0.5 |
| Import fails, CLI crashes, or partial file visible | 0.0 |
Files
state/lib/projects.ts- the API (importable + CLI).state/skills/projects/SKILL.md- this file (verb registration).state/skills/projects/AGENTS.md- per-turn loader.~/Documents/Snappy/Projects/- on-disk storage (per-machine, not gitignored).
AGENTS.md- what the AI loads when this skill comes up
projects - loader
Per-turn rules for the projects skill. Full reference: state/skills/projects/SKILL.md. Storage: ~/Documents/Snappy/Projects/<slug>/ (per-machine, not gitignored).
Critical Rules
- Atomic write only. Every write to
project.json,instructions.md, and.index.jsonuses temp+rename. Partial files corrupt reads. Never write directly to final path. - Slug is sticky.
updateProjectpatchesnameandinstructionsonly. Folder name never changes. - Index is source of truth for location. Resolve via index, not by constructing
join(PROJECTS_BASE, slug)- custom locations break that. ~/Documents/Snappy/Projects/created on first call if missing. Do not pre-check in agent code.- Corrupt rows silently skipped.
listProjectsdrops missing/unparseable entries. Read-only, never repairs. - Slug is pure ASCII kebab-case.
slugify(name)is zero-dep. Falls back touuid.substring(0,8)for empty/non-ASCII. - No new external deps. Only Node fs, crypto, path, os.
Commands
| Action | Command |
|---|---|
| Import | import { listProjects, createProject, ... } from "./state/lib/projects.ts" |
| CLI list | npx tsx state/lib/projects.ts list |
| CLI create | npx tsx state/lib/projects.ts create "<name>" "[instructions]" |
| CLI get | npx tsx state/lib/projects.ts get <id> |
| CLI update | npx tsx state/lib/projects.ts update <id> "<new-name>" |
| CLI delete | npx tsx state/lib/projects.ts delete <id> |
| CLI files | npx tsx state/lib/projects.ts files <id> |
Self-Test
An agent reading this should correctly:
- [ ] Atomic write (temp+rename) on every write - never directly to final path.
- [ ] Slug sticky - updateProject never renames folder.
- [ ] Resolve location via index, not slug construction.
- [ ] Skip corrupt rows silently in listProjects.
- [ ] Know storage is local per-machine user content (not synced).
- [ ] Call slugify() as zero-dep pure ASCII function.
Storage
~/Documents/Snappy/Projects/
.index.json [{id, location}, ...]
<slug>/
project.json {id, slug, name, created_at, updated_at}
instructions.md freeform prose
files/ user or agent files, empty at creation
API surface
listProjects(): Project[] - read index, load manifests, compute file_count, sort desc by created_at. Silently skip corrupt entries.
getProject(id): Project | null - lookup by id from index. Null if not found.
createProject({name, instructions?, location?}): Project - mint uuid, slugify name, find unique folder (append -2/-3 if collision), write project.json + instructions.md + files/ atomically, update index last.
updateProject(id, patch): Project - patch name and/or instructions only. Bumps updated_at. Slug never changes. Throws if not found.
deleteProject(id): {ok: boolean} - remove folder recursively, remove index entry atomically.
listProjectFiles(id): {name, size, mtime}[] - list files/ entries. Returns [] if project not found or empty.
<!-- kernel-ok: historical attribution for the PID-loader pattern. -->
Self-correcting loader (PID feedback)
This is the rule that makes loaders learn. Ported from snappy-kernel, where it demonstrably worked: the snappy-dom-cartographer skill mapped Skool admin UI once, every later ship-agent inherited the full map for free.
Before you finish, do two things:
1. Fix gaps (P - proportional). MANDATORY. If this loader didn't cover your case - if you had to read another file, run an undocumented command, work around a wrong selector, or discover a quirk - you MUST attempt an Edit to this AGENTS.md before you log.
- Default action: edit this file. Surgical. Add the missing line, table
row, or code example where it belongs. No restructuring.
- Only
LOGGEDis allowed when: the fix needs >10 lines, spans multiple
files, or requires a structural rewrite. In that case the state/regen/drain.sh queue picks it up asynchronously.
- "I didn't have time" / "it's minor" / "the next agent will figure it out"
are NOT valid reasons. Minor gaps compound - that's the whole point of this loop.
- Concrete example of a valid P-fix: discover
agent-browser screenshot
takes path positionally not as --output <path>. One line in a table. Edit it in. Do not just log it.
- The goal: the next agent never has to leave the loader.
2. Log the result.
echo "[$(date -u +%FT%TZ)] <slug>: <what was missing or fixed> [FIXED|LOGGED] action_kind=<kind>" >> state/log/loader-feedback.log
<slug>MUST be the literal folder name of this loader
(state/skills/<slug>/AGENTS.md). The class token between [ts] and : is the producer slug, the writeback class, AND the grade class - they must be equal so state/lib/controller-tune.ts can pair the brief.
FIXED= you patched this loader inline (P-fix).LOGGED= too large for inline; the PostToolUse enqueue + Stop-hook drain
will rewrite the loader from scratch on next session-end.
action_kindis the SECOND pairing predicate (added 2026-04-27, task #327).
Pick the value that describes what you actually did - same slug, different action_kind means the writeback satisfies a different brief layer:
shape-ok- only frontmatter-shape verification passed (rare from
a human; usually emitted by the lint, not a loader echo)
skill-ran- the skill ran end-to-end and an eval row landed
in state/log/evals.ndjson
loader-rewritten- you EDITED this AGENTS.md inline (the FIXED case),
OR the regen drain rewrote it
pattern-elevated- you promoted a recurring failure to a Critical Rule
(rule fix or new-skill scaffold) If you LOGGED (couldn't fix inline), omit action_kind - the inferrer will pick it up from your body keywords.
Do not skip this. Every agent run must leave the system better than it found it. The loader is the setpoint; you are the sensor; the gap is the error signal; closing the gap is the correction.
<!-- kernel-ok: historical attribution for the PID-loader pattern. -->
Self-correcting loader (PID feedback)
This is the rule that makes loaders learn. Ported from snappy-kernel, where it demonstrably worked: the snappy-dom-cartographer skill mapped Skool admin UI once, every later ship-agent inherited the full map for free.
Before you finish, do two things:
1. Fix gaps (P - proportional). MANDATORY. If this loader didn't cover your case - if you had to read another file, run an undocumented command, work around a wrong selector, or discover a quirk - you MUST attempt an Edit to this AGENTS.md before you log.
- Default action: edit this file. Surgical. Add the missing line, table
row, or code example where it belongs. No restructuring.
- Only
LOGGEDis allowed when: the fix needs >10 lines, spans multiple
files, or requires a structural rewrite. In that case the state/regen/drain.sh queue picks it up asynchronously.
- "I didn't have time" / "it's minor" / "the next agent will figure it out"
are NOT valid reasons. Minor gaps compound - that's the whole point of this loop.
- Concrete example of a valid P-fix: discover
agent-browser screenshot
takes path positionally not as --output <path>. One line in a table. Edit it in. Do not just log it.
- The goal: the next agent never has to leave the loader.
2. Log the result.
echo "[$(date -u +%FT%TZ)] projects: <what was missing or fixed> [FIXED|LOGGED] action_kind=<kind>" >> state/log/loader-feedback.log
FIXED= you patched this loader inline (P-fix).LOGGED= too large for inline; the PostToolUse enqueue + Stop-hook drain
will rewrite the loader from scratch on next session-end.
action_kind:shape-ok|skill-ran|loader-rewritten|pattern-elevated
Do not skip this. Every agent run must leave the system better than it found it. The loader is the setpoint; you are the sensor; the gap is the error signal; closing the gap is the correction.
api.ts- the code it can call
#!/usr/bin/env npx tsx
/**
* state/lib/projects.ts -- First-class Project abstraction for snappy-os.
*
* A Project = folder on disk with project.json (metadata), instructions.md
* (freeform prose), and files/ (user-owned content). Each project has a unique
* id (uuid v4) and slug (kebab-case, url-safe, ASCII). Threads can attach via
* projectId. This module provides full CRUD primitives.
*
* Storage: ~/Documents/Snappy/Projects/<slug>/ containing:
* - project.json: the manifest (id, slug, name, created_at, updated_at)
* - instructions.md: the prose (may be empty)
* - files/: empty dir at creation, user-populated
*
* Index: ~/Documents/Snappy/Projects/.index.json maps id → location.
*
* All writes are atomic (temp + rename) — mirrors artifacts.ts discipline.
* No external deps: only Node fs, crypto, path, os.
*
* import { listProjects, createProject } from "../lib/projects.ts";
* const projects = listProjects();
* const p = createProject({ name: "My Project", instructions: "Focus on X" });
*/
import {
existsSync,
mkdirSync,
readFileSync,
readdirSync,
renameSync,
writeFileSync,
rmSync,
statSync,
} from "fs";
import { dirname, join } from "path";
import { homedir } from "os";
import { fileURLToPath } from "url";
import { randomUUID } from "crypto";
const HERE = dirname(fileURLToPath(import.meta.url));
// ROOT kept for potential future relative-path usage, unused currently
const _ROOT = join(HERE, "..", "..");
const PROJECTS_BASE = join(homedir(), "Documents", "Snappy", "Projects");
const INDEX_PATH = join(PROJECTS_BASE, ".index.json");
// ─── Types ─────────────────────────────────────────────────────────────────
export type Project = {
id: string; // uuid v4
slug: string; // url-safe ASCII kebab-case, sticky after creation
name: string;
instructions: string; // contents of instructions.md, may be empty
created_at: string; // ISO 8601
updated_at: string; // ISO 8601
location: string; // absolute path to the project folder
file_count: number; // computed at list/get time from files/ dir
};
type IndexEntry = {
id: string;
location: string;
};
// ─── Slugify — pure function, no third-party deps ──────────────────────────
/**
* Convert a display name to a url-safe ASCII kebab-case slug.
* Falls back to a short uuid prefix for empty or fully non-ASCII names.
*/
export function slugify(name: string): string {
if (!name || !name.trim()) return randomUUID().substring(0, 8);
const slug = name
.trim()
.toLowerCase()
.replace(/[^\w\s-]/g, "") // strip special chars (keep word chars, space, hyphen)
.replace(/\s+/g, "-") // spaces to hyphens
.replace(/-+/g, "-") // collapse runs
.replace(/^-|-$/g, ""); // trim edge hyphens
return slug || randomUUID().substring(0, 8);
}
// ─── Ensure base dir exists ────────────────────────────────────────────────
function ensureBase(): void {
if (!existsSync(PROJECTS_BASE)) {
mkdirSync(PROJECTS_BASE, { recursive: true });
}
}
// ─── Index helpers (atomic) ────────────────────────────────────────────────
function readIndex(): Map<string, IndexEntry> {
ensureBase();
if (!existsSync(INDEX_PATH)) return new Map();
try {
const raw = readFileSync(INDEX_PATH, "utf-8");
const entries: IndexEntry[] = JSON.parse(raw);
return new Map(entries.map((e) => [e.id, e]));
} catch {
return new Map(); // corrupt index: return empty, rewrite on next write
}
}
function writeIndex(index: Map<string, IndexEntry>): void {
ensureBase();
const entries = Array.from(index.values());
const tmp = INDEX_PATH + ".tmp";
writeFileSync(tmp, JSON.stringify(entries, null, 2) + "\n", "utf-8");
renameSync(tmp, INDEX_PATH);
}
// ─── Project-folder helpers ────────────────────────────────────────────────
type Manifest = {
id: string;
slug: string;
name: string;
created_at: string;
updated_at: string;
};
function readManifest(location: string): Manifest | null {
try {
const raw = readFileSync(join(location, "project.json"), "utf-8");
const m = JSON.parse(raw) as Manifest;
if (typeof m?.id === "string" && typeof m?.slug === "string") return m;
return null;
} catch {
return null;
}
}
function writeManifest(location: string, m: Manifest): void {
const fp = join(location, "project.json");
const tmp = fp + ".tmp";
writeFileSync(tmp, JSON.stringify(m, null, 2) + "\n", "utf-8");
renameSync(tmp, fp);
}
function readInstructions(location: string): string {
try {
return readFileSync(join(location, "instructions.md"), "utf-8");
} catch {
return "";
}
}
function writeInstructions(location: string, content: string): void {
const fp = join(location, "instructions.md");
const tmp = fp + ".tmp";
writeFileSync(tmp, content, "utf-8");
renameSync(tmp, fp);
}
function countFiles(location: string): number {
const filesDir = join(location, "files");
if (!existsSync(filesDir)) return 0;
try {
return readdirSync(filesDir).filter((f) => !f.startsWith(".")).length;
} catch {
return 0;
}
}
function hydrateProject(m: Manifest, location: string): Project {
return {
id: m.id,
slug: m.slug,
name: m.name,
instructions: readInstructions(location),
created_at: m.created_at,
updated_at: m.updated_at,
location,
file_count: countFiles(location),
};
}
// ─── Slug-uniqueness guard ─────────────────────────────────────────────────
function findUniqueSlug(base: string): string {
let candidate = base;
let n = 2;
while (existsSync(join(PROJECTS_BASE, candidate))) {
candidate = `${base}-${n}`;
n++;
}
return candidate;
}
// ─── Public API ────────────────────────────────────────────────────────────
/**
* List every project sorted by created_at desc. Corrupt/missing entries
* are silently skipped — never throws.
*/
export function listProjects(): Project[] {
ensureBase();
const index = readIndex();
const out: Project[] = [];
for (const entry of index.values()) {
if (!existsSync(entry.location)) continue;
const m = readManifest(entry.location);
if (!m) continue;
out.push(hydrateProject(m, entry.location));
}
out.sort((a, b) =>
a.created_at < b.created_at ? 1 : a.created_at > b.created_at ? -1 : 0
);
return out;
}
/**
* Get a single project by id. Returns null when not found or corrupt.
*/
export function getProject(id: string): Project | null {
ensureBase();
const entry = readIndex().get(id);
if (!entry || !existsSync(entry.location)) return null;
const m = readManifest(entry.location);
if (!m) return null;
return hydrateProject(m, entry.location);
}
/**
* Create a new project. Mints a v4 uuid for id, derives slug from name.
* If a folder with that slug already exists, appends -2, -3, etc. until unique.
* Atomic: temp+rename for manifest and instructions; index updated last.
*/
export function createProject(args: {
name: string;
instructions?: string;
location?: string;
}): Project {
const name = (args.name ?? "").trim();
if (!name) throw new Error("createProject: name is required");
ensureBase();
const baseSlug = slugify(name);
const slug = args.location ? baseSlug : findUniqueSlug(baseSlug);
const location = args.location
? join(args.location) // caller-supplied path
: join(PROJECTS_BASE, slug);
if (!existsSync(location)) mkdirSync(location, { recursive: true });
const filesDir = join(location, "files");
if (!existsSync(filesDir)) mkdirSync(filesDir, { recursive: true });
const id = randomUUID();
const now = new Date().toISOString();
const manifest: Manifest = { id, slug, name, created_at: now, updated_at: now };
writeManifest(location, manifest);
writeInstructions(location, args.instructions ?? "");
// Update index atomically (after folder is written, so a crash before this
// leaves an orphaned folder that list() will skip rather than a dangling index)
const index = readIndex();
index.set(id, { id, location });
writeIndex(index);
return hydrateProject(manifest, location);
}
/**
* Update name and/or instructions. Slug is sticky (never changes after creation).
* Bumps updated_at. Throws if the project is not found.
*/
export function updateProject(
id: string,
patch: Partial<Pick<Project, "name" | "instructions">>
): Project {
const entry = readIndex().get(id);
if (!entry || !existsSync(entry.location)) {
throw new Error(`updateProject: project ${id} not found`);
}
const m = readManifest(entry.location);
if (!m) throw new Error(`updateProject: corrupt manifest for ${id}`);
const updatedManifest: Manifest = {
...m,
name: patch.name?.trim() ?? m.name,
updated_at: new Date().toISOString(),
};
writeManifest(entry.location, updatedManifest);
if (typeof patch.instructions === "string") {
writeInstructions(entry.location, patch.instructions);
}
return hydrateProject(updatedManifest, entry.location);
}
/**
* Delete a project by id. Removes the folder recursively and the index entry.
* Returns { ok: true } on success, { ok: false } if not found.
*/
export function deleteProject(id: string): { ok: boolean } {
const index = readIndex();
const entry = index.get(id);
if (!entry) return { ok: false };
try {
if (existsSync(entry.location)) {
rmSync(entry.location, { recursive: true, force: true });
}
index.delete(id);
writeIndex(index);
return { ok: true };
} catch {
return { ok: false };
}
}
/**
* List files inside the project's files/ subdirectory.
* Returns [] when the project is not found or files/ is empty.
*/
export function listProjectFiles(
id: string
): { name: string; size: number; mtime: string }[] {
const entry = readIndex().get(id);
if (!entry || !existsSync(entry.location)) return [];
const filesDir = join(entry.location, "files");
if (!existsSync(filesDir)) return [];
try {
return readdirSync(filesDir, { withFileTypes: true })
.filter((e) => e.isFile() && !e.name.startsWith("."))
.map((e) => {
const fp = join(filesDir, e.name);
const st = statSync(fp);
return { name: e.name, size: st.size, mtime: st.mtime.toISOString() };
})
.sort((a, b) => a.name.localeCompare(b.name));
} catch {
return [];
}
}
// ─── CLI ───────────────────────────────────────────────────────────────────
// usage: npx tsx state/lib/projects.ts list|create <name>|get <id>|delete <id>|files <id>|update <id> <name>
if (import.meta.url === `file://${process.argv[1]}`) {
const cmd = process.argv[2] ?? "list";
if (cmd === "list") {
console.log(JSON.stringify(listProjects(), null, 2));
} else if (cmd === "create") {
const name = process.argv[3];
if (!name) {
console.error("usage: projects.ts create <name> [instructions]");
process.exit(2);
}
const instructions = process.argv[4];
console.log(JSON.stringify(createProject({ name, instructions }), null, 2));
} else if (cmd === "get") {
const id = process.argv[3];
if (!id) { console.error("usage: projects.ts get <id>"); process.exit(2); }
console.log(JSON.stringify(getProject(id), null, 2));
} else if (cmd === "update") {
const id = process.argv[3];
const newName = process.argv[4];
if (!id || !newName) {
console.error("usage: projects.ts update <id> <new-name>");
process.exit(2);
}
console.log(JSON.stringify(updateProject(id, { name: newName }), null, 2));
} else if (cmd === "delete") {
const id = process.argv[3];
if (!id) { console.error("usage: projects.ts delete <id>"); process.exit(2); }
console.log(JSON.stringify(deleteProject(id), null, 2));
} else if (cmd === "files") {
const id = process.argv[3];
if (!id) { console.error("usage: projects.ts files <id>"); process.exit(2); }
console.log(JSON.stringify(listProjectFiles(id), null, 2));
} else {
console.error(
"usage: projects.ts [list|create <name>|get <id>|delete <id>|files <id>|update <id> <name>]"
);
process.exit(2);
}
}
scripts- helper scripts it can run
prose-only skill - 4 inline code blocks live in SKILL.md above (no state/bin/ sidecar yet).
how we check it- the checks, plus the last 10 runs
no recent runs logged - the eval contract is declared but nothing has been graded yet