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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,7 @@ Install or load this repository as a Codex plugin source, then invoke:

```text
$loop <objective>
$Loop <objective>
```

The skill tells the agent to record durable state, check repository boundaries,
Expand Down
2 changes: 1 addition & 1 deletion docs/compatibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

| Surface | MVP behavior | Notes |
| --- | --- | --- |
| Codex `$loop` | Shipped first. | `$loop <objective>` maps to the Codex skill in `skills/loop/SKILL.md`. |
| Codex `$loop` / `$Loop` | Shipped first. | `$loop <objective>` and the display-case `$Loop <objective>` map to the Codex skill in `skills/loop/SKILL.md`. |
| Loop Agent Console TUI | Shipped local baseline. | `loop` with no arguments opens a terminal console for run selection, wiki/log reading, verification notes, lineage-preserving follow-up intent, dashboard open, and Codex resume actions when a session id is known. |
| Loop CLI Codex agent | Prototype. | `loop run --agent codex "prompt"` launches `codex exec` after Loop state and safety checks. |
| Loop CLI Claude Code agent | Prototype. | `loop run --agent claudecode "prompt"` launches `claude --print` after Loop state and safety checks. |
Expand Down
3 changes: 2 additions & 1 deletion skills/loop/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: loop
description: Run a Loop Engineering workflow for a coding-agent objective with durable state, budget/stop rules, isolation checks, and human verification boundaries. Use when the user invokes $loop or asks the agent to keep iterating safely until a goal is complete.
description: Run a Loop Engineering workflow for a coding-agent objective with durable state, budget/stop rules, isolation checks, and human verification boundaries. Use when the user invokes $loop, $Loop, or asks the agent to keep iterating safely until a goal is complete.
---

# Loop
Expand All @@ -13,6 +13,7 @@ isolate, act, verify, persist, then stop or escalate.
Use this skill for:

- `$loop <objective>`
- `$Loop <objective>`
- "run a loop for this"
- "keep iterating until this goal is complete"

Expand Down
62 changes: 62 additions & 0 deletions test/adapter.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ import { createServer as createNetServer } from "node:net";
import { join, resolve } from "node:path";
import { tmpdir } from "node:os";

/** @param {string} command */
function commandExists(command) {
const result = spawnSync(command, ["--version"], { encoding: "utf8" });
return !(result.error && "code" in result.error && result.error.code === "ENOENT");
}

test("Codex plugin manifest points at skills", async () => {
const manifest = JSON.parse(await readFile(".codex-plugin/plugin.json", "utf8"));

Expand All @@ -20,11 +26,67 @@ test("$loop skill describes the six Loop Engineering components", async () => {
const skill = await readFile("skills/loop/SKILL.md", "utf8");

assert.match(skill, /\$loop/);
assert.match(skill, /\$Loop/);
for (const component of ["Automations", "Worktrees", "Skills", "Plugins/connectors", "Sub-agents", "Memory"]) {
assert.match(skill, new RegExp(component.replace("/", "\\/")));
}
});

test("Codex can install the Loop plugin and render $Loop skill context", {
skip: commandExists("codex") ? false : "codex CLI is not installed"
}, async () => {
const root = await mkdtemp(join(tmpdir(), "loop-codex-plugin-"));
const codexHome = join(root, "codex-home");
const marketplace = join(root, "marketplace");
const pluginRoot = join(marketplace, "plugins", "loop");
await mkdir(join(marketplace, ".agents", "plugins"), { recursive: true });
await mkdir(join(pluginRoot, ".codex-plugin"), { recursive: true });
await mkdir(join(pluginRoot, "skills", "loop"), { recursive: true });
await mkdir(codexHome, { recursive: true });
await writeFile(
join(pluginRoot, ".codex-plugin", "plugin.json"),
await readFile(".codex-plugin/plugin.json", "utf8")
);
await writeFile(
join(pluginRoot, "skills", "loop", "SKILL.md"),
await readFile("skills/loop/SKILL.md", "utf8")
);
await writeFile(join(marketplace, ".agents", "plugins", "marketplace.json"), `${JSON.stringify({
name: "loop-local-test",
interface: { displayName: "Loop local test" },
plugins: [{
name: "loop",
source: { source: "local", path: "./plugins/loop" },
policy: { installation: "AVAILABLE", authentication: "ON_INSTALL" },
category: "Productivity"
}]
}, null, 2)}\n`);
const env = { ...process.env, CODEX_HOME: codexHome };

const addMarketplace = spawnSync("codex", ["plugin", "marketplace", "add", marketplace], {
env,
encoding: "utf8"
});
assert.equal(addMarketplace.status, 0, addMarketplace.stderr || addMarketplace.stdout);
const addPlugin = spawnSync("codex", ["plugin", "add", "loop@loop-local-test"], {
env,
encoding: "utf8"
});
assert.equal(addPlugin.status, 0, addPlugin.stderr || addPlugin.stdout);
const pluginList = execFileSync("codex", ["plugin", "list"], { env, encoding: "utf8" });
const promptInput = execFileSync(
"codex",
["debug", "prompt-input", "$Loop 현재 대시보드 품질 루프를 검증해줘"],
{ env, encoding: "utf8" }
);

assert.match(pluginList, /loop@loop-local-test\s+installed, enabled/);
assert.match(promptInput, /loop:loop/);
assert.match(promptInput, /Available plugins/);
assert.match(promptInput, /`Loop`/);
assert.match(promptInput, /\$Loop 현재 대시보드 품질 루프를 검증해줘/);
});

test("CLI dry-run writes durable state without source edits", async () => {
const stateDir = await mkdtemp(join(tmpdir(), "loop-cli-state-"));
const output = execFileSync(
Expand Down