Agent guide

How an AI coding agent, or any script, should drive oskac. Oska is built for this loop: every message has a stable code and a precise span, many carry a fix a tool can apply, and the authority report says what a change lets the program do.

The loop

  1. Check. oskac check file.osk --format=json. Read diagnostics.
  2. Apply machine fixes. Apply the fix of a diagnostic that has one, then check again. Repeat until no diagnostic has a fix.
  3. Fix the rest by hand, choosing by code, one diagnostic at a time, first one first.
  4. Run. oskac run file.osk --format=json -- args.... The exit code and standard error say how it ended.
  5. Test. oskac test file.osk --format=json. Write a test for every behavior you add.
  6. Read the authority report. Compare functions and audit from oskac check --format=json with what you intended. A change that widens authority, or adds an audit site, is not finished until you can say why.
  7. Release check. oskac check file.osk --release must pass: no dbg(...) and no todo() left.

You are done when the check, the tests, and the release check all exit with 0, and the authority report is no wider than the task needs.

Machine-readable outputs

CommandOutput with --format=jsonWhere
oskac checkOne object: diagnostics, functions, auditstandard output
oskac testOne object: tests, passed, failed, skipped, diagnosticsstandard output
oskac runThe program's own output, unchanged. Diagnostics and panics are one JSON object per line.standard error
oskac parseThe AST, documented in the AST referencestandard output
oskac lexTokens (the same text as without the flag). A lexical error is a diagnostic object.standard output
oskac --versionOne object: name, versionstandard output

Every diagnostic has the same shape: code, message, span, and sometimes fix. Match on code. The shapes and exit codes are in the CLI reference. If oskac cannot read the file it prints error: cannot read ... to standard error, exits with 1, and prints no JSON, so check for empty standard output before parsing.

Applying fixes

A fix is a message and a list of edits. Each edit replaces the bytes span.start up to span.end with replacement. The offsets count UTF-8 bytes in the file as it was checked.

import json, subprocess

def check(path):
    out = subprocess.run(["oskac", "check", path, "--format=json"], capture_output=True)
    return json.loads(out.stdout)

def apply_fix(path, fix):
    src = open(path, "rb").read()
    for edit in sorted(fix["edits"], key=lambda e: e["span"]["start"], reverse=True):
        start, end = edit["span"]["start"], edit["span"]["end"]
        src = src[:start] + edit["replacement"].encode() + src[end:]
    open(path, "wb").write(src)

def apply_machine_fixes(path):
    while True:
        report = check(path)
        fixable = [d for d in report["diagnostics"] if "fix" in d]
        if not fixable:
            return report
        apply_fix(path, fixable[0]["fix"])  # one fix, then re-check

Rules that keep this safe:

An example. The file declares a struct that holds a capability without saying so:

struct Logger { out: Stdout }

The diagnostic has the fix {"message":"declare it cap struct","edits":[{"span":{"start":0,"end":0},"replacement":"cap "}]}. Applying it, an insertion at byte 0, gives a program that checks:

cap struct Logger { out: Stdout }

Reading the authority report

The report is the review. For each function it says what the function can reach, and it lists every audit site:

fn word_count(docs: DirRead, path: Path) -> Result<Int, IoError> {
    let text = docs.read_to_string(path)?
    Ok(text.split(" ").len())
}

fn main(io: Stdout, fs: Fs) -> Result<(), IoError> {
    let docs = fs.restrict_to("notes").read_only()
    let words = word_count(docs, Path.new("today.txt"))?
    io.println("{words}")?
    Ok(())
}
{
  "diagnostics": [],
  "functions": [
    {"name":"word_count","authority":["DirRead"],"via":[{"param":"docs","capabilities":["DirRead"]}],"pure":false},
    {"name":"main","authority":["Stdout","Fs"],"via":[{"param":"io","capabilities":["Stdout"]},{"param":"fs","capabilities":["Fs"]}],"pure":false}
  ],
  "audit": [],
  "attenuations": [
    {"function":"main","method":"restrict_to","span":{"start":210,"end":233},"source":"Fs","result":"Dir","root":"notes"},
    {"function":"main","method":"read_only","span":{"start":210,"end":245},"source":"Dir","result":"DirRead","root":null}
  ]
}

Before you finish a task, compare this against the report from before your change:

Never silence a diagnostic by weakening authority or trust: do not add capabilities to main, call .assume_trusted(), or call .expose() only to make a message go away. Ask first when the task seems to need it.

Things an agent should know

Telling an agent about the project

Put the loop in the project's instructions file, such as AGENTS.md:

## Working with Oska

- Check: `oskac check <file.osk> --format=json`. Apply a diagnostic's `fix` if it has one, then check again.
- Run: `oskac run <file.osk> -- args`. Test: `oskac test <file.osk>`.
- Before finishing: `oskac check <file.osk> --release` must pass, and read the authority report. Do not widen `main`'s capabilities or add `.assume_trusted()` or `.expose()` without saying why.
- Match on the diagnostic `code`, not the message. `oskac explain <code>` explains a code offline, with an example and its fix. The same entries are at `https://oskalang.dev/codes/<code>` and in `https://oskalang.dev/codes.json`.

Files for agents

These are published on oskalang.dev and are produced from the same docs as the pages you read.