Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 1 addition & 4 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,7 @@

## Verification

- [ ] `npm run lint`
- [ ] `npm run typecheck`
- [ ] `npm test`
- [ ] `npm pack --dry-run --json`
- [ ] `npm run verify`

## Loop Safety Checklist

Expand Down
36 changes: 2 additions & 34 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,37 +31,5 @@ jobs:
- name: Install dependencies
run: npm ci

- name: Lint
run: npm run lint

- name: Typecheck
run: npm run typecheck

- name: Test
run: npm test

- name: Verify package contents
run: |
npm pack --dry-run --json | node -e '
let input = "";
process.stdin.on("data", (chunk) => input += chunk);
process.stdin.on("end", () => {
const pack = JSON.parse(input)[0];
const files = new Set(pack.files.map((file) => file.path));
const required = [
"README.md",
"LICENSE",
"bin/loop.js",
"src/index.js",
"skills/loop/SKILL.md",
".codex-plugin/plugin.json",
"assets/loop-engineering-poster.png",
"assets/loop-engineering-components.png"
];
const missing = required.filter((file) => !files.has(file));
if (missing.length > 0) {
console.error(`Missing package files: ${missing.join(", ")}`);
process.exit(1);
}
});
'
- name: Verify
run: npm run verify
4 changes: 1 addition & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,7 @@ small, reviewable, and safety-first.

```sh
npm install
npm test
npm run lint
npm run typecheck
npm run verify
```

## Pull Requests
Expand Down
16 changes: 10 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ Install Loop once:
```sh
npm install -g github:rlaope/loop
loop --version
loop doctor
loop demo
```

Create or enter the project you want the coding agent to work on:
Expand Down Expand Up @@ -115,6 +117,12 @@ If you want to try Loop without installing it first:
npm exec --yes --package github:rlaope/loop -- loop "Build a darkwear luxury exhibition site"
```

Before a first real run, `loop doctor` checks the local Node.js, git, package,
repo-boundary, and optional agent CLI readiness without writing `.loop`,
launching agents, starting the dashboard, or calling the network. `loop demo`
prints a small command catalog for common first-run, explicit-agent, and
dry-run follow-up workflows; it is also read-only.

If Loop says the git root does not match, you probably passed an explicit
`--expected-root` that does not match the current project. Run Loop from the
folder you want the agent to edit, or pass the intended root explicitly.
Expand Down Expand Up @@ -201,9 +209,7 @@ To verify the package:

```sh
npm install
npm test
npm run lint
npm run typecheck
npm run verify
```

After the package is published to npm, the shorter registry form will be:
Expand Down Expand Up @@ -252,9 +258,7 @@ observable and bounded.

```sh
npm install
npm test
npm run lint
npm run typecheck
npm run verify
```

Build the loop, stay the engineer.
28 changes: 27 additions & 1 deletion bin/loop.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
dashboardUrl,
deleteRunState,
deleteWikiNote,
doctorExitCode,
getDashboardStatus,
evaluatePolicyGate,
listRunStates,
Expand All @@ -27,9 +28,12 @@ import {
readRunState,
readWikiNote,
recordBudgetActivity,
renderDemoGuide,
renderDoctorReport,
renderWikiList,
runLogPath,
runAgentProcess,
runDoctorChecks,
runLoopTui,
scriptPathFromImportMetaUrl,
serveWikiDashboard,
Expand All @@ -44,7 +48,7 @@ import {
} from "../src/index.js";

const rawArgs = process.argv.slice(2);
const knownCommands = new Set(["run", "wiki", "status", "runs", "logs"]);
const knownCommands = new Set(["run", "wiki", "status", "runs", "logs", "doctor", "demo"]);
const command = knownCommands.has(rawArgs[0]) ? rawArgs[0] : undefined;
const args = command ? rawArgs.slice(1) : rawArgs;
const packageJson = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
Expand Down Expand Up @@ -1017,6 +1021,28 @@ if (command === "wiki") {
await handleWikiCommand();
}

if (command === "doctor") {
try {
const result = runDoctorChecks({
cwd: process.cwd(),
packageJson,
expectedRoot: valueFor("--expected-root"),
expectedRemote: valueFor("--expected-remote")
});
process.stdout.write(renderDoctorReport(result));
process.exit(doctorExitCode(result));
} catch (error) {
process.stderr.write(`${errorMessage(error)}\n\n`);
printHelp(process.stderr);
process.exit(1);
}
}

if (command === "demo") {
process.stdout.write(renderDemoGuide());
process.exit(0);
}

let objective;
let stateDir;
try {
Expand Down
13 changes: 13 additions & 0 deletions docs/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@ and a prototype `loop run` surface for Codex and Claude Code. Rich automation,
native command adapters, external sync, and hosted knowledge storage remain
future work.

## Public Alpha Readiness

- Keep `loop doctor` read-only and local-only so first-time users can inspect
Node.js, git, repo-boundary, package, and optional agent CLI readiness before
launching a loop.
- Keep `loop demo` as a read-only command catalog. It must not write `.loop`,
launch agents, start dashboards, or make network calls.
- Keep `npm run verify` as the contributor and CI quality gate for lint,
typecheck, tests, and package-content validation.

## Claude Code Adapter

- Harden the `loop run --agent claudecode` prototype with parity tests against
Expand Down Expand Up @@ -47,3 +57,6 @@ future work.
- A write-capable loop must fail closed on unknown policy modes, missing
approvals, unsafe worktree state, missing repo-boundary evidence, and
exhausted budgets.
- The public alpha CLI must expose `loop doctor`, `loop demo`, `loop --help`,
`loop --version`, `loop status`, `loop runs`, `loop logs`, and `loop wiki`
with matching README examples and local tests.
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,9 @@
"scripts": {
"lint": "node scripts/lint.mjs",
"test": "node --test",
"typecheck": "tsc --noEmit"
"typecheck": "tsc --noEmit",
"verify:package": "npm pack --dry-run --json | node scripts/verify-package.mjs",
"verify": "npm run lint && npm run typecheck && npm test && npm run verify:package"
},
"keywords": [
"ai",
Expand Down
64 changes: 64 additions & 0 deletions scripts/verify-package.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
#!/usr/bin/env node

const requiredFiles = [
"README.md",
"LICENSE",
"bin/loop.js",
"src/index.js",
"skills/loop/SKILL.md",
".codex-plugin/plugin.json",
"assets/loop-engineering-poster.png",
"assets/loop-engineering-components.png"
];

/**
* @param {string} message
*/
function fail(message) {
console.error(message);
process.exit(1);
}

let input = "";
for await (const chunk of process.stdin) {
input += String(chunk);
}

let parsed;
try {
parsed = JSON.parse(input);
} catch (error) {
fail(`Could not parse npm pack output: ${error instanceof Error ? error.message : String(error)}`);
}

if (!Array.isArray(parsed) || parsed.length === 0) {
fail("npm pack output did not include a package entry.");
}

const pack = parsed[0];
if (!pack || typeof pack !== "object" || !Array.isArray(pack.files)) {
fail("npm pack output did not include a files list.");
}

/**
* @param {unknown} file
*/
function filePath(file) {
if (!file || typeof file !== "object" || !("path" in file)) {
fail("npm pack file entry did not include a path.");
}
const path = /** @type {{ path: unknown }} */ (file).path;
if (typeof path !== "string") {
fail("npm pack file entry did not include a path.");
}
return path;
}

const files = new Set(pack.files.map(filePath));
const missing = requiredFiles.filter((file) => !files.has(file));

if (missing.length > 0) {
fail(`Missing package files: ${missing.join(", ")}`);
}

console.log(`Package content verified: ${requiredFiles.length} required files present.`);
61 changes: 61 additions & 0 deletions src/core/demo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
export const demoWorkflows = [
{
title: "Darkwear luxury exhibition site",
description: "Start a fresh local project, let Loop create a git boundary, and watch the agent run.",
commands: [
"mkdir darkwear-exhibit",
"cd darkwear-exhibit",
"loop doctor",
"loop \"Build a darkwear luxury exhibition site MVP\"",
"loop status",
"loop logs --follow",
"loop wiki"
]
},
{
title: "Explicit Codex run",
description: "Skip the agent picker when you already know which coding agent should receive the objective.",
commands: [
"loop run --agent codex \"Build a quiet SaaS metrics dashboard MVP\"",
"loop runs",
"loop wiki list"
]
},
{
title: "Safe planning and follow-up",
description: "Record a dry-run plan first, then continue with a scoped write-capable loop when the goal is clear.",
commands: [
"loop --dry-run --objective \"Audit failing tests and propose the smallest safe fix plan\"",
"loop wiki read <note-id>",
"loop run --agent codex --parent-run <run-id> \"Fix the failing test with the smallest safe change\""
]
}
];

export function renderDemoGuide() {
const lines = [
"Loop Demo",
"",
"This command prints examples only. It does not write .loop, launch agents, start services, or call the network.",
""
];

for (const workflow of demoWorkflows) {
lines.push(`## ${workflow.title}`);
lines.push(workflow.description);
lines.push("");
lines.push("```sh");
lines.push(...workflow.commands);
lines.push("```");
lines.push("");
}

lines.push("Most users start with:");
lines.push("");
lines.push("```sh");
lines.push("loop \"Build the thing you want\"");
lines.push("```");
lines.push("");

return `${lines.join("\n")}\n`;
}
Loading