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
9 changes: 9 additions & 0 deletions skills/loop-engineering/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@ manually and label the run as a non-deterministic fallback.
The script emits `budget_exhausted` when the declared ceiling is consumed.
8. Run `show`; continue only while `terminal_status` is `running`.

While state is `running` and the next action is authorized, begin the next cycle
immediately in the same invocation. Do not yield an intermediate result or ask
again. Yield only for a terminal outcome, user interruption, or real runtime
boundary.

If later evidence contradicts a recorded fact, use `annotate --evidence
"<correction>"`. Preserve the audit trail; do not reopen or hand-edit terminal
state.
Expand All @@ -58,6 +63,10 @@ Use `finish` for `blocked`, `needs_human`, `cancelled`, or
`continue_scheduled`. Never translate those states or `budget_exhausted` into
`complete`.

After intervention clears a resumable terminal condition, use `resume` to
create a bound successor state, replay the blocked check, and continue while
the successor is `running`. Never reopen the predecessor.

## Preserve durable context

When the installed `worklog` protocol is available, hydrate resume context
Expand Down
27 changes: 25 additions & 2 deletions skills/loop-engineering/references/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,26 @@ terminal_status: running
```

Run one discriminating check per hypothesis. Serialize edits and test after each
one. End `complete` only with three passing runs; otherwise end
`budget_exhausted` with failures and the safest next action.
one. Continue immediately between hypotheses while state is `running`; do not
return an intermediate handoff. End `complete` only with three passing runs;
otherwise end `budget_exhausted` with failures and the safest next action.
When the terminal run itself consumes a declared unit, use `finish --consume 1`;
never also `advance` for that same unit.

If a fresh check later contradicts evidence in a saved terminal run, append the
correction and revalidate the state:

```bash
python3 <skill-dir>/scripts/loop_state.py annotate \
--state <state-file> \
--evidence "Correction: isolated rerun reproduced the timing failure"
python3 <skill-dir>/scripts/loop_state.py validate --state <state-file>
python3 <skill-dir>/scripts/loop_state.py show --state <state-file>
```

Confirm the terminal status and consumed budget remain unchanged. The earlier
evidence remains in history. Start a new authorized state file for further work;
do not reopen the terminal run.

## 2. Worklog-backed delegation

Expand Down Expand Up @@ -56,6 +74,11 @@ primitive, schedule bounded checks and end each run `continue_scheduled` until
CI evidence satisfies the goal. If no such primitive is available, end
`needs_human` and name the missing capability.

At each verified wakeup, use `resume` to create a successor bound to the prior
`continue_scheduled` state, replay the CI check, and keep cycling. If a
credential or authority blocker requires intervention, checkpoint, ask for that
specific intervention, then resume a successor and replay the blocked check.

Do not claim that a timer or stop hook exists merely because another host
supports one.

Expand Down
22 changes: 19 additions & 3 deletions skills/loop-engineering/references/hosts.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,38 +9,54 @@ tracking.
- Use only standard `name` and `description` frontmatter in `SKILL.md`.
- Discover capabilities before invoking them.
- Preserve the same run contract and terminal statuses on every host.
- During an active invocation, keep cycling while state is `running`; progress
updates are not pause points.
- Across invocations, continue only through a verified host recurrence
primitive. A prompt cannot manufacture background execution.
- After intervention or a scheduled wakeup, the agent creates a successor state
bound to the terminal predecessor and replays the stopping check before
continuing. Treat supplied intervention as pending until that check passes.

## Codex

- Discover shared skills from `~/.agents/skills/`.
- Use the current task plan and available subagent tools for in-session tracking
and bounded delegation.
- Keep issuing tool calls in the current task while state is `running`; do not
yield merely because one cycle ended.
- Invoke the installed `worklog` skill for durable context and checkpoints.
- For recurrence, use an available Codex automation or `loop-orchestrator`; if
neither is callable, end `needs_human`.
neither is callable, end `needs_human`. Each heartbeat wakes the agent, which
resumes from the prior `continue_scheduled` state instead of reopening it.

## Claude Code

- Discover personal skills from `~/.claude/skills/`.
- Use the available task tracker and Agent tool for in-session tracking and
bounded delegation.
- Continue the current tool/agent sequence while state is `running`; do not end
the response between authorized cycles.
- Invoke `/worklog context <slug> --for=compact` before cold delegation and
pass the returned pack directly. Use `/worklog sync` for the protocol's
confirmation/checkpoint boundary.
- Use Claude's real `/loop`, scheduled task, or hook capability only when exposed
and authorized. Otherwise end `needs_human`.
and authorized. Each recurrence wakes the agent to resume a bound successor.
Otherwise end `needs_human`.

## Cursor

- Discover user skills from `~/.agents/skills/` or Cursor's native
`~/.cursor/skills/`; project-local alternatives may use `.agents/skills/` or
`.cursor/skills/`.
- Use Cursor todos and subagents when exposed.
- Continue the active agent run while state is `running`; a todo update alone
is not a reason to pause.
- Invoke the installed worklog skill or its documented helper commands for
durable context. If unavailable, use one durable project tracker and label the
fallback.
- Use a configured Cursor automation or hook only after verifying it exists and
has a bounded stop rule. Otherwise end `needs_human`.
has a bounded stop rule. Each recurrence wakes the agent to resume a bound
successor. Otherwise end `needs_human`.

## Compatibility rule

Expand Down
37 changes: 34 additions & 3 deletions skills/loop-engineering/references/protocol.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,47 @@ Use `scripts/loop_state.py`; do not reimplement its state machine or hand-edit
the JSON.

- `init` creates a `running` state and refuses overwrite unless `--force`.
- `advance` records a failed or nonterminal cycle, consumes positive budget,
and atomically changes the state to `budget_exhausted` at the ceiling.
- `advance` records a failed or nonterminal cycle, consumes one budget unit by
default, and atomically changes the state to `budget_exhausted` at the
ceiling. Use `--consume N` only when one recorded cycle represents `N`
declared units; it cannot exceed the remaining budget.
- `resume --state <terminal> --new-state <successor>` starts a fresh running
state after `blocked`, `needs_human`, `budget_exhausted`, or
`continue_scheduled`. It inherits the goal and cumulative budget, requires a
next action, and binds the successor to the predecessor path, status, and
SHA-256. Use `--extend-budget N` only with explicit authorization; exhausted
predecessors require an extension. It rejects `running`, `complete`, and
`cancelled`.
- `finish` records exactly one terminal outcome. `complete` requires
`--verification` naming tool output or an artifact.
`--verification` naming tool output or an artifact. It leaves budget
unchanged by default; use `--consume N` when the terminal cycle spent `N`
declared units. Every executed cycle must be accounted exactly once by
`advance` or `finish`.
- `annotate` appends corrected evidence without reopening terminal state or
changing the consumed budget.
- `validate` checks the schema and transition invariants.
- `show` prints the five-field contract; `--json` returns the full history.

Every write is atomic. A failed transition leaves the previous state unchanged.
Malformed CLI usage exits `2` with a `usage:` error. A well-formed command
rejected by the state contract exits `3` with a `loop-state:` error.

## Continuous execution and intervention

- While state is `running`, execute the next authorized cycle immediately. Do
not stop at an intermediate progress update or ask for permission already
granted by the effect boundary.
- On `blocked` or `needs_human`, checkpoint durable context and ask for the
smallest specific intervention. Include the predecessor state path and the
exact check that will prove the blocker cleared.
- After intervention, create a successor with `resume`, record the intervention
as supplied but pending verification, replay that check, append verified
clearing evidence, and keep iterating while the successor remains `running`.
- On `continue_scheduled`, a verified scheduler wakes the agent; the agent
creates the successor and replays the stopping check. Active-turn
continuation and background recurrence are distinct.
- A user may authorize a successor after `budget_exhausted`; do not silently
grant more budget. Never resume `complete` or `cancelled`.

## Effect boundary

Expand Down
104 changes: 95 additions & 9 deletions skills/loop-engineering/scripts/loop_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from __future__ import annotations

import argparse
import hashlib
import json
import os
import pathlib
Expand All @@ -22,6 +23,12 @@
"cancelled",
"continue_scheduled",
}
RESUMABLE_STATUSES = {
"blocked",
"needs_human",
"budget_exhausted",
"continue_scheduled",
}
ALL_STATUSES = {"running", *TERMINAL_STATUSES}


Expand Down Expand Up @@ -154,14 +161,8 @@ def command_init(args: argparse.Namespace) -> dict[str, Any]:
def command_advance(args: argparse.Namespace) -> dict[str, Any]:
state = read_state(args.state)
require_running(state)
remaining = state["budget"]["limit"] - state["budget"]["used"]
if args.consume > remaining:
raise StateError(
f"transition consumes {args.consume} {state['budget']['unit']}; "
f"only {remaining} remain"
)
consume_budget(state, args.consume)
state["progress_evidence"].extend(args.evidence)
state["budget"]["used"] += args.consume
state["next_action"] = args.next_action
if state["budget"]["used"] == state["budget"]["limit"]:
state["terminal_status"] = "budget_exhausted"
Expand All @@ -170,11 +171,78 @@ def command_advance(args: argparse.Namespace) -> dict[str, Any]:
return state


def consume_budget(state: dict[str, Any], amount: int) -> None:
remaining = state["budget"]["limit"] - state["budget"]["used"]
if amount > remaining:
raise StateError(
f"transition consumes {amount} {state['budget']['unit']}; "
f"only {remaining} remain"
)
state["budget"]["used"] += amount


def command_resume(args: argparse.Namespace) -> dict[str, Any]:
if args.new_state.exists():
raise StateError(f"refusing to overwrite existing state: {args.new_state}")
if args.state.resolve() == args.new_state.resolve():
raise StateError("resume requires a distinct --new-state path")
predecessor = read_state(args.state)
status = predecessor["terminal_status"]
if status not in RESUMABLE_STATUSES:
raise StateError(f"cannot resume state with status: {status}")
if (
predecessor["budget"]["used"] == predecessor["budget"]["limit"]
and args.extend_budget == 0
):
raise StateError(
"resume requires --extend-budget when predecessor budget is exhausted"
)

digest = hashlib.sha256(args.state.read_bytes()).hexdigest()
predecessor_reference = {
"path": str(args.state.resolve()),
"sha256": digest,
"terminal_status": status,
}
resume_evidence = [
(f"Resumed from {predecessor_reference['path']} ({status}, sha256={digest})"),
*args.evidence,
]
state: dict[str, Any] = {
"schema_version": SCHEMA_VERSION,
"goal": predecessor["goal"],
"progress_evidence": resume_evidence,
"budget": {
"unit": predecessor["budget"]["unit"],
"limit": predecessor["budget"]["limit"] + args.extend_budget,
"used": predecessor["budget"]["used"],
},
"next_action": args.next_action,
"terminal_status": "running",
"allowed_effects": [
*predecessor.get("allowed_effects", []),
*args.allowed_effect,
],
"approval_boundary": (
args.approval_boundary
if args.approval_boundary is not None
else predecessor.get("approval_boundary", "")
),
"verification": "",
"predecessor": predecessor_reference,
"history": [],
}
append_history(state, f"resumed:{status}", resume_evidence, args.next_action)
write_state(args.new_state, state)
return state


def command_finish(args: argparse.Namespace) -> dict[str, Any]:
state = read_state(args.state)
require_running(state)
if args.status == "complete" and not args.verification:
raise StateError("complete requires --verification from a tool or artifact")
consume_budget(state, args.consume)
state["progress_evidence"].extend(args.evidence)
state["terminal_status"] = args.status
if args.next_action is not None:
Expand Down Expand Up @@ -240,12 +308,26 @@ def build_parser() -> argparse.ArgumentParser:
advance.add_argument("--consume", type=int, default=1)
advance.set_defaults(handler=command_advance)

resume = subparsers.add_parser(
"resume",
help="start a running successor from a resumable terminal state",
)
add_state_argument(resume)
resume.add_argument("--new-state", type=pathlib.Path, required=True)
resume.add_argument("--evidence", action="append", required=True)
resume.add_argument("--extend-budget", type=int, default=0)
resume.add_argument("--next-action", required=True)
resume.add_argument("--allowed-effect", action="append", default=[])
resume.add_argument("--approval-boundary")
resume.set_defaults(handler=command_resume)

finish = subparsers.add_parser("finish", help="record a terminal outcome")
add_state_argument(finish)
finish.add_argument("--status", choices=sorted(TERMINAL_STATUSES), required=True)
finish.add_argument("--evidence", action="append", required=True)
finish.add_argument("--verification")
finish.add_argument("--next-action")
finish.add_argument("--consume", type=int, default=0)
finish.set_defaults(handler=command_finish)

annotate = subparsers.add_parser(
Expand All @@ -271,13 +353,17 @@ def build_parser() -> argparse.ArgumentParser:
def main() -> int:
parser = build_parser()
args = parser.parse_args()
if getattr(args, "consume", 1) < 1:
if args.command == "advance" and args.consume < 1:
parser.error("--consume must be a positive integer")
if args.command == "finish" and args.consume < 0:
parser.error("--consume must be zero or a positive integer")
if args.command == "resume" and args.extend_budget < 0:
parser.error("--extend-budget must be zero or a positive integer")
try:
state = args.handler(args)
except StateError as exc:
print(f"loop-state: {exc}", file=sys.stderr)
return 2
return 3
if args.command == "show" and not args.json:
print(summary(state))
else:
Expand Down
Loading
Loading