New:Socket for Asana Is Now Available.Learn more
Get Started

gatecraft

Package Overview
Dependencies
Maintainers
1
Versions
4
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

gatecraft - npm Package Compare versions

Comparing version
1.0.1
to
1.1.0
+127
src/commands/prompt.js
'use strict';
const ui = require('../lib/ui.js');
const fsx = require('../lib/fsx.js');
const sections = require('../lib/sections.js');
// `gatecraft prompt <name>` — print one prompt from the library.
//
// PROMPTS.md holds 62 independent prompts across ~28k tokens. Exactly one of them
// is relevant to any given task, and the whole point of a prompt is to be pasted,
// so the retrieval cost should be a command rather than a file read plus a search.
const FILE = 'PROMPTS.md';
function help() {
ui.out(`${ui.color.bold('gatecraft prompt')} — print one prompt from the library
${ui.color.bold('USAGE')}
gatecraft prompt # list all 62
gatecraft prompt write-an-adr # print one
gatecraft prompt adr --md # raw markdown, for piping
gatecraft prompt --category backend # list one category
${ui.color.bold('OPTIONS')}`);
ui.table([
['--md', 'Raw markdown with no terminal formatting'],
['--category <name>', 'Restrict the listing to one category'],
['--dir <path>', 'Read from a specific project install'],
]);
ui.out(`
${ui.color.bold('WHY A COMMAND')}
${ui.color.dim('A prompt exists to be pasted. Reading 62 of them to reach one wastes the')}
${ui.color.dim('context the prompt was going to be used in. Project prompts go in')}
${ui.color.dim('.ai/prompts/ — this command reads the framework library.')}
`);
return 0;
}
/** Prompts are mostly fenced blocks meant to be copied; keep them verbatim. */
function render(body) {
let fenced = false;
for (const line of fsx.lines(body)) {
if (/^\s*(```|~~~)/.test(line)) {
fenced = !fenced;
ui.out(` ${ui.color.dim(line)}`);
continue;
}
if (!fenced && /^\*\*.+\*\*/.test(line)) ui.out(` ${ui.color.bold(line.replace(/\*\*/g, ''))}`);
else ui.out(line ? ` ${line}` : '');
}
}
function list(all, source, category) {
const shown = category
? all.filter((p) => sections.slugify(String(p.category || '')).includes(sections.slugify(String(category))))
: all;
if (!shown.length) {
ui.fail(`no category matching "${category}"`);
const names = [...new Set(all.map((p) => p.category).filter(Boolean))];
ui.out(`\nCategories: ${names.map((n) => ui.color.cyan(n)).join(', ')}`);
return 1;
}
ui.step(`Prompts ${ui.color.dim(source === 'project' ? '(from this project)' : '(framework defaults)')}`);
// Width across every row, not per category, so the second column is a straight
// edge down the whole listing. Padding is applied to the raw slug — colour codes
// are invisible but not zero-length, and padding the coloured string shears it.
//
// Capped, because the longest slug is nearly twice the median: letting one
// outlier set the column would push every title past 80 characters and wrap the
// whole listing. Two names overflow the cap and take a single space instead.
const MAX_COL = 44;
const width = Math.min(MAX_COL, shown.reduce((m, p) => Math.max(m, p.slug.length), 0));
let current = null;
for (const p of shown) {
if (p.category !== current) {
current = p.category;
ui.out(`\n ${ui.color.bold(current || 'Uncategorised')}`);
}
const pad = ' '.repeat(Math.max(0, width - p.slug.length));
ui.out(` ${ui.color.cyan(p.slug)}${pad} ${ui.color.dim(p.title)}`);
}
ui.out(`\n ${ui.color.dim('gatecraft prompt <name> print one')}`);
ui.out(` ${ui.color.dim('gatecraft prompt <name> --md raw markdown')}\n`);
return 0;
}
async function run({ flags, args }) {
const { file, source } = sections.locate(flags, FILE);
const all = sections.parseEntries(fsx.read(file));
if (!args[0]) return list(all, source, typeof flags.category === 'string' ? flags.category : null);
const { found, matches } = sections.resolve(all, args[0]);
if (!found) {
if (matches.length > 1) {
ui.fail(`"${args[0]}" matches ${matches.length} prompts`);
ui.out('');
ui.table(matches.map((p) => [p.slug, `${p.title}${p.category ? ui.color.dim(` (${p.category})`) : ''}`]));
return 1;
}
ui.fail(`no prompt named "${args[0]}"`);
ui.out(`\nRun ${ui.color.cyan('gatecraft prompt')} to see all ${all.length}.`);
return 1;
}
if (flags.md) {
process.stdout.write(`### ${found.title}\n\n${found.body}\n`);
return 0;
}
ui.out('');
ui.out(`${ui.color.bold(found.title)}${found.category ? ` ${ui.color.dim(found.category)}` : ''}`);
ui.out('');
render(found.body);
ui.out('');
ui.out(` ${ui.color.dim('Fill every {{placeholder}} before sending. An unfilled prompt gets a')}`);
ui.out(` ${ui.color.dim('generic answer, which is the failure this library exists to prevent.')}`);
ui.out('');
return 0;
}
module.exports = { run, help };
'use strict';
const ui = require('../lib/ui.js');
const fsx = require('../lib/fsx.js');
const sections = require('../lib/sections.js');
// `gatecraft standard <topic>` — print one section of STANDARDS.md.
//
// STANDARDS.md is 25 sections and ~11k tokens. An agent about to write a database
// migration needs section 12, and reading the other 24 to find it is context spent
// on nothing. `--md` exists so the section can be piped straight into a prompt.
const FILE = 'STANDARDS.md';
const SUFFIX = /\s+standards$/i;
function help() {
ui.out(`${ui.color.bold('gatecraft standard')} — print one section of the engineering standards
${ui.color.bold('USAGE')}
gatecraft standard # list all 25
gatecraft standard security # print one
gatecraft standard 12 # by number
gatecraft standard api --md # raw markdown, for piping
${ui.color.bold('OPTIONS')}`);
ui.table([
['--md', 'Raw markdown with no terminal formatting'],
['--dir <path>', 'Read from a specific project install'],
]);
ui.out(`
${ui.color.bold('WHY A COMMAND')}
${ui.color.dim('The standards are a reference work, not a document you read front to back.')}
${ui.color.dim('Reading all 25 sections to apply one costs context an agent needed for the')}
${ui.color.dim('work itself. Project overrides go in .ai/standards/, never in this file.')}
`);
return 0;
}
/** Bold the `**Topic — MUST**` group headers; leave everything else as written. */
function render(body) {
let fenced = false;
for (const line of fsx.lines(body)) {
if (/^\s*(```|~~~)/.test(line)) {
fenced = !fenced;
ui.out(` ${ui.color.dim(line)}`);
continue;
}
if (!fenced && /^\*\*.+\*\*\s*$/.test(line)) ui.out(` ${ui.color.bold(line.replace(/\*\*/g, ''))}`);
else ui.out(line ? ` ${line}` : '');
}
}
async function run({ flags, args }) {
const { file, source } = sections.locate(flags, FILE);
const all = sections.parseNumbered(fsx.read(file), { stripSuffix: SUFFIX });
if (!args[0]) {
ui.step(`Standards ${ui.color.dim(source === 'project' ? '(from this project)' : '(framework defaults)')}`);
ui.table(all.map((s) => [`${String(s.number).padStart(2)} ${s.slug}`, s.title]));
ui.out(`\n ${ui.color.dim('gatecraft standard <topic> print one')}`);
ui.out(` ${ui.color.dim('gatecraft standard <topic> --md raw markdown')}\n`);
return 0;
}
const { found, matches } = sections.resolve(all, args[0]);
if (!found) {
if (matches.length > 1) {
ui.fail(`"${args[0]}" matches ${matches.length} standards`);
ui.out('');
ui.table(matches.map((s) => [s.slug, s.title]));
return 1;
}
ui.fail(`no standard named "${args[0]}"`);
ui.out(`\nRun ${ui.color.cyan('gatecraft standard')} to see all ${all.length}.`);
return 1;
}
if (flags.md) {
process.stdout.write(`## ${found.number}. ${found.title}\n\n${found.body}\n`);
return 0;
}
ui.out('');
ui.out(`${ui.color.bold(`${found.number}. ${found.title}`)}`);
ui.out('');
render(found.body);
ui.out('');
ui.out(` ${ui.color.dim('MUST is not negotiable. Record a deliberate exception in')}`);
ui.out(` ${ui.color.dim('.ai/PROJECT_CONTEXT.md#12-overrides-and-exceptions with an owner and a reason.')}`);
ui.out('');
return 0;
}
module.exports = { run, help };
'use strict';
const path = require('path');
const fsx = require('./fsx.js');
const paths = require('./paths.js');
const payload = require('./payload.js');
const links = require('./links.js');
// Addressing one section of a payload document from the command line.
//
// The framework is deliberately large, and every document in it is a reference
// work rather than something read front to back. STANDARDS.md is 976 lines and an
// agent implementing an endpoint needs section 13; PROMPTS.md is 2,804 lines and
// holds 62 independent prompts. Reading the whole file to reach one section costs
// thousands of tokens of context an agent could have spent on the actual work,
// and on a small context window it is the difference between fitting and not.
//
// So: one parser for the two shapes these documents use, and one resolver, shared
// by `checklist`, `standard`, and `prompt` so the three behave identically. The
// alternative — line offsets in the docs — rots the moment anything is inserted.
/** A heading turned into something typeable: `10. Security standards` -> `security`. */
function slugify(title, stripSuffix) {
const base = stripSuffix ? title.replace(stripSuffix, '') : title;
return base
.trim()
.toLowerCase()
.replace(/[^\w]+/g, '-')
.replace(/^-+|-+$/g, '');
}
/** Trailing `---` rules separate sections in the payload; they are not content. */
function finish(section) {
section.body = section.lines.join('\n').replace(/\n*---\s*$/, '').trim();
section.items = (section.body.match(/^- \[ \]/gm) || []).length;
delete section.lines;
return section;
}
/**
* Sections of the form `## N. Title` — the shape CHECKLISTS.md and STANDARDS.md
* use. Fenced code is skipped, because a `## ` inside a fence is sample output,
* not a heading.
*/
function parseNumbered(text, { stripSuffix = null } = {}) {
const out = [];
let current = null;
let fenced = false;
for (const line of fsx.lines(text)) {
if (/^\s*(```|~~~)/.test(line)) fenced = !fenced;
const m = !fenced && /^##\s+(\d+)\.\s+(.+?)\s*$/.exec(line);
if (m) {
current = {
number: Number(m[1]),
title: m[2],
slug: slugify(m[2], stripSuffix),
anchor: links.slug(`${m[1]}. ${m[2]}`),
lines: [],
};
out.push(current);
continue;
}
if (current) current.lines.push(line);
}
return out.map(finish);
}
/**
* Entries of the form `### Name` grouped under `## N. Category` — the shape
* PROMPTS.md, TEMPLATES.md, and PLAYBOOKS.md use. Each entry carries its parent
* category so an ambiguous query can be disambiguated by where it lives.
*/
function parseEntries(text) {
const out = [];
let category = null;
let categoryNumber = 0;
let current = null;
let fenced = false;
for (const line of fsx.lines(text)) {
if (/^\s*(```|~~~)/.test(line)) fenced = !fenced;
if (!fenced) {
const cat = /^##\s+(?:(\d+)\.\s+)?(.+?)\s*$/.exec(line);
if (cat && !line.startsWith('###')) {
categoryNumber = cat[1] ? Number(cat[1]) : categoryNumber + 1;
category = cat[2];
current = null;
continue;
}
const entry = /^###\s+(.+?)\s*$/.exec(line);
if (entry) {
current = {
number: out.length + 1,
title: entry[1],
category,
categoryNumber,
slug: slugify(entry[1]),
anchor: links.slug(entry[1]),
lines: [],
};
out.push(current);
continue;
}
}
if (current) current.lines.push(line);
}
return out.map(finish);
}
/**
* Find the one section a query means.
*
* Exact slug beats prefix beats substring, and a query matching several returns
* them for the caller to print rather than guessing — silently picking the first
* of four security standards is worse than asking again.
*/
function resolve(all, raw) {
const query = String(raw || '').toLowerCase().replace(/[^\w]+/g, '-').replace(/^-+|-+$/g, '');
if (!query) return { query, found: null, matches: [] };
if (/^\d+$/.test(query)) {
const byNumber = all.find((s) => s.number === Number(query));
if (byNumber) return { query, found: byNumber, matches: [byNumber] };
}
const exact = all.find((s) => s.slug === query);
if (exact) return { query, found: exact, matches: [exact] };
const prefix = all.filter((s) => s.slug.startsWith(query));
const pool = prefix.length
? prefix
: all.filter((s) => s.slug.includes(query) || s.title.toLowerCase().includes(query.replace(/-/g, ' ')));
return { query, found: pool.length === 1 ? pool[0] : null, matches: pool };
}
/**
* Prefer the copy installed in the project, since `.ai/standards/` overrides and
* a pinned framework version both live there. Fall back to the packaged payload
* so these commands still work outside an installed project.
*/
function locate(flags, file) {
const root = flags.dir ? path.resolve(flags.dir) : paths.findProjectRoot();
const installed = path.join(paths.paths(root).ai, file);
if (fsx.exists(installed)) return { file: installed, source: 'project' };
return { file: path.join(payload.source(), file), source: 'framework' };
}
module.exports = { slugify, parseNumbered, parseEntries, resolve, locate };
+8
-5
#!/bin/sh
# gatecraft installer — for projects without Node, or without a Node you want to use.
#
# curl -fsSL https://gatecraft.dev/install.sh | sh
# curl -fsSL https://gatecraft.dev/install.sh | sh -s -- --version 1.2.0
# The short https://gatecraft.dev/install.sh URL is not live yet; use the raw
# GitHub URL until that domain is registered and serving this file.
#
# curl -fsSL https://raw.githubusercontent.com/Eric20Junior/gatecraft/main/install.sh | sh
# curl -fsSL https://raw.githubusercontent.com/Eric20Junior/gatecraft/main/install.sh | sh -s -- --version 1.2.0
#
# What this does: downloads the gatecraft release tarball, extracts the framework

@@ -50,4 +53,4 @@ # payload into ./.ai, adds `.ai/` to .gitignore, and writes an AGENTS.md bootstrap

${B}USAGE${R}
curl -fsSL https://gatecraft.dev/install.sh | sh
curl -fsSL https://gatecraft.dev/install.sh | sh -s -- [options]
curl -fsSL https://raw.githubusercontent.com/Eric20Junior/gatecraft/main/install.sh | sh
curl -fsSL https://raw.githubusercontent.com/Eric20Junior/gatecraft/main/install.sh | sh -s -- [options]

@@ -130,3 +133,3 @@ ${B}OPTIONS${R}

die "gatecraft is already installed here.
Upgrade with: curl -fsSL https://gatecraft.dev/install.sh | sh -s -- --force
Upgrade with: curl -fsSL https://raw.githubusercontent.com/Eric20Junior/gatecraft/main/install.sh | sh -s -- --force
Or, with Node: npx gatecraft upgrade"

@@ -133,0 +136,0 @@ fi

{
"name": "gatecraft",
"version": "1.0.1",
"version": "1.1.0",
"description": "The AI Engineering Operating System — install a complete engineering loop, quality gates, and 26 specialist roles into any repository. Hidden, gitignored, zero dependencies.",

@@ -5,0 +5,0 @@ "keywords": [

@@ -29,2 +29,32 @@ # CHANGELOG.md — History of the Gatecraft

## [1.1.0] — 2026-08-06
Two retrieval commands, so an agent can pull one section instead of reading a whole
document. `PROMPTS.md` is 114 KB and `STANDARDS.md` is 46 KB; an agent that needed one
prompt had to load all 62 and search, which wastes context on small-window models and
risks truncation on any of them. `gatecraft checklist` already solved this for
checklists — this extends the same pattern to standards and prompts.
**What you must do.** Nothing is required; both commands are additive and no document,
section number, or anchor changed. To let your agent use them, run
`npx gatecraft@latest upgrade` to pick up the new `AGENTS.md` bootstrap section that
names them. If you customized `AGENTS.md` outside the managed marker block, your edits
are preserved.
### Added
- `gatecraft standard <topic>` prints one section of `STANDARDS.md`. Run it bare to
list the 25 topics. `--md` emits raw markdown for piping into a prompt.
- `gatecraft prompt <name>` prints one prompt from `PROMPTS.md`. Run it bare to list
all 62 grouped by category, or `--category <name>` to list one category.
- Both resolve a query by section number, exact slug, unique prefix, then substring,
and read your installed `.ai/` copy before the packaged defaults — so local edits to
a standard are what you get back. An ambiguous query lists the matches and exits
non-zero rather than guessing.
- `AGENTS.md` gained a "Read one section, not the whole document" section listing all
three retrieval commands, with a fallback for agents that cannot run shell commands.
Previously none of them were mentioned in the bootstrap at all.
---
## [1.0.1] — 2026-08-05

@@ -31,0 +61,0 @@

# AI Engineering Operating System (Gatecraft)
Version 1.0.1 — see [VERSION.md](VERSION.md) and [CHANGELOG.md](CHANGELOG.md).
Version 1.1.0 — see [VERSION.md](VERSION.md) and [CHANGELOG.md](CHANGELOG.md).

@@ -5,0 +5,0 @@ The Gatecraft is a technology-agnostic operating system for AI coding agents and the

# VERSION.md — Versioning and Compatibility
**Current version: 1.0.1**
**Current version: 1.1.0**

@@ -151,2 +151,3 @@ This file governs the version of the AI Engineering Operating System itself — the

| 1.0.1 | 15 | 12 | Unchanged | Installer fixes only. No document, directory, or kernel change; no override can be affected. |
| 1.1.0 | 15 | 12 | Unchanged | Retrieval commands (`standard`, `prompt`) and a bootstrap section pointing at them. No document, directory, section number, or anchor changed; no override can be affected. |

@@ -153,0 +154,0 @@ Record each release here. The columns are the things an override can depend on, so a

@@ -57,2 +57,25 @@ <div align="center">

The loop is the part worth seeing. Solid arrows are the happy path; dotted arrows
are the gates sending work backwards, which is the behaviour an agent left to
itself does not have.
```mermaid
flowchart LR
A(Understand) --> B(Research) --> C(Plan) --> D(Design)
D --> E(Implement) --> F{{Review}} --> G{{Critique}}
G --> H(Improve) --> I{{Validate}} --> J{{Test}}
J --> K(Document) --> L{{Evaluate}}
L --> M([Done])
F -.->|defect found| E
I -.->|no evidence| H
J -.->|test fails| E
L -.->|score below 90| C
classDef gate fill:#fde68a,stroke:#b45309,color:#1c1917
classDef done fill:#bbf7d0,stroke:#15803d,color:#1c1917
class F,G,I,J,L gate
class M done
```
## Install

@@ -69,2 +92,6 @@

<!-- Hidden until gatecraft.dev is registered and serving install.sh. Restore this
section once the domain is live; install.sh itself is unchanged and still ships
in the package.
**Without Node:**

@@ -79,2 +106,4 @@

-->
**Globally, if you install into projects often:**

@@ -220,5 +249,18 @@

| `gatecraft checklist [name]` | Print a quality gate. `--md` to pipe into a PR. |
| `gatecraft standard [topic]` | Print one of the 25 standards sections, not all 25. |
| `gatecraft prompt [name]` | Print one of the 62 prompts, ready to fill in and send. |
| `gatecraft eject` | Keep the files, drop the tooling. No lock-in. |
| `gatecraft uninstall` | Remove everything, including our `.gitignore` and `AGENTS.md` blocks. |
The three retrieval commands exist for the same reason: these documents are
reference works, and an agent that reads all of `STANDARDS.md` to apply one
section has spent ~11,000 tokens of its context to get ~1,200 tokens of answer.
`gatecraft standard security --md` returns the section alone.
```sh
gatecraft standard api --md # one section, not the other 24
gatecraft prompt write-an-adr --md # one prompt, not the other 61
gatecraft prompt --category backend # what is available, before choosing
```
## Upgrades will not eat your work

@@ -225,0 +267,0 @@

@@ -12,2 +12,4 @@ 'use strict';

checklist: { file: './commands/checklist.js', blurb: 'Print a quality gate checklist to run' },
standard: { file: './commands/standard.js', blurb: 'Print one section of the engineering standards' },
prompt: { file: './commands/prompt.js', blurb: 'Print one prompt from the library' },
uninstall: { file: './commands/uninstall.js', blurb: 'Remove the framework and everything it added' },

@@ -30,3 +32,3 @@ eject: { file: './commands/eject.js', blurb: 'Stop managing the install; keep the files, commit them' },

// `--dir` without this list would silently swallow the next argument.
const VALUED = new Set(['dir', 'name']);
const VALUED = new Set(['dir', 'name', 'category']);

@@ -33,0 +35,0 @@ function parse(argv) {

'use strict';
const path = require('path');
const ui = require('../lib/ui.js');
const fsx = require('../lib/fsx.js');
const paths = require('../lib/paths.js');
const payload = require('../lib/payload.js');
const links = require('../lib/links.js');
const sections = require('../lib/sections.js');

@@ -16,2 +13,5 @@ // `gatecraft checklist <name>` exists so a gate can be run without opening a file.

const FILE = 'CHECKLISTS.md';
const SUFFIX = /\s+checklist$/i;
function help() {

@@ -39,47 +39,7 @@ ui.out(`${ui.color.bold('gatecraft checklist')} — print a quality gate checklist

/** Split CHECKLISTS.md into its `## N. Name checklist` sections. */
function parse(text) {
const out = [];
const lines = fsx.lines(text);
let current = null;
let fenced = false;
for (const line of lines) {
if (/^\s*(```|~~~)/.test(line)) fenced = !fenced;
const m = !fenced && /^##\s+(\d+)\.\s+(.+?)\s*$/.exec(line);
if (m) {
current = {
number: Number(m[1]),
title: m[2],
slug: m[2].replace(/\s+checklist$/i, '').toLowerCase().replace(/[^\w]+/g, '-').replace(/^-|-$/g, ''),
anchor: links.slug(`${m[1]}. ${m[2]}`),
lines: [],
};
out.push(current);
continue;
}
if (current) current.lines.push(line);
}
for (const c of out) {
c.body = c.lines.join('\n').replace(/\n*---\s*$/, '').trim();
c.items = (c.body.match(/^- \[ \]/gm) || []).length;
delete c.lines;
}
return out;
}
function locate(flags) {
const root = flags.dir ? path.resolve(flags.dir) : paths.findProjectRoot();
const installed = path.join(paths.paths(root).ai, 'CHECKLISTS.md');
if (fsx.exists(installed)) return { file: installed, source: 'project' };
return { file: path.join(payload.source(), 'CHECKLISTS.md'), source: 'framework' };
}
async function run({ flags, args }) {
const { file, source } = locate(flags);
const all = parse(fsx.read(file));
const query = (args[0] || '').toLowerCase().replace(/[^\w]+/g, '-');
const { file, source } = sections.locate(flags, FILE);
const all = sections.parseNumbered(fsx.read(file), { stripSuffix: SUFFIX });
if (!query) {
if (!args[0]) {
ui.step(`Checklists ${ui.color.dim(source === 'project' ? '(from this project)' : '(framework defaults)')}`);

@@ -92,12 +52,9 @@ ui.table(all.map((c) => [`${String(c.number).padStart(2)} ${c.slug}`, `${c.items} items — ${c.title}`]));

const byNumber = /^\d+$/.test(query) ? all.find((c) => c.number === Number(query)) : null;
const exact = all.find((c) => c.slug === query);
const partial = all.filter((c) => c.slug.includes(query) || c.title.toLowerCase().includes(args[0].toLowerCase()));
const found = byNumber || exact || (partial.length === 1 ? partial[0] : null);
const { found, matches } = sections.resolve(all, args[0]);
if (!found) {
if (partial.length > 1) {
ui.fail(`"${args[0]}" matches ${partial.length} checklists`);
if (matches.length > 1) {
ui.fail(`"${args[0]}" matches ${matches.length} checklists`);
ui.out('');
ui.table(partial.map((c) => [c.slug, c.title]));
ui.table(matches.map((c) => [c.slug, c.title]));
return 1;

@@ -129,2 +86,2 @@ }

module.exports = { run, help, parse };
module.exports = { run, help };

@@ -45,2 +45,16 @@ 'use strict';

### Read one section, not the whole document
\`STANDARDS.md\`, \`CHECKLISTS.md\`, and \`PROMPTS.md\` are reference works. Pull the
section you need instead of reading the file and searching it:
\`\`\`sh
gatecraft standard security --md # one of 25 sections
gatecraft checklist release --md # one of 20 gates
gatecraft prompt write-an-adr --md # one of 62 prompts
\`\`\`
Run any of the three with no argument to see what exists. If \`gatecraft\` is not on
PATH, use \`npx gatecraft\`, or read the file — but read only the section.
### The loop is not optional

@@ -47,0 +61,0 @@