wellactually

Writing principles

Findings

A handler yields what it found, one finding at a time. A finding says where the advice applies, and the plugin quotes that place to the agent.

What a finding can say#

Nothing in a finding is required.

ts
yield { line: 12 };                              // one line
yield { line: 10, endLine: 18 };                 // an if/else over several lines
yield { line: 12, match: "late" };               // the part of the line that matters
yield { match: "late final", depth: "pointer" }; // a keyword in a prompt
yield {};                                        // the file itself, or just fire
yield { line: 12, file: "assets/hero.jpg" };     // another file, named on line 12
FieldWhat it means
lineThe 1-based line of the file, or of what a command printed. The agent is shown that line.
endLineThe last line of a finding that spans several. Needs line.
matchThe exact text that triggered. It has to occur word for word in the event, on the lines the finding names, and may not span lines.
fileAnother file than the one of the event, as a path from the project root or an absolute one. It has to exist, or occur word for word in the event.
depth"pointer" shows only the summary. Leave it out for the whole advice.

A finding carries no text of its own#

The plugin writes the sentence the agent reads, from the event and the finding:

- line 2, "DateTime.now()": print(DateTime.now());

A detector cannot add sentences of its own, and a match that does not occur in the event is left out. That is what keeps a detector from smuggling instructions into an agent's context.

Lines#

  • On onWrite and onRead, line counts in the file.
  • On onOutput, it counts in what the command printed.
  • A prompt and a command have no lines to point at, so their findings have no line. The types say so: yield { line: 1 } in onPrompt does not compile.

A finding that names a line the file does not have loses that detail and still fires. In a test, detect() throws instead, so you notice.

The file itself#

A finding without a line in onWrite or onRead points at the file. Use it when the file itself is the problem: its name, where it lives, something it lacks. Do not pick line 1 to stand in for it.

ts
onWrite({
  globs: ["**/lib/**/*.dart"],
  *detect(event) {
    if (event.file.isNew && /[A-Z]/.test(event.file.name)) {
      yield {};
    }
  },
});

Another file#

file points at a different file than the one of the event: the jpg that should be a webp, the test that already exists. line and match stay about the event, so they say where the other file was named.

The agent is shown the path, so it has to be a file or folder that exists, or a path that occurs word for word in the event, like the output of a command that is about to run. Any other file is left out, and the finding still fires.

ts
onWrite({
  globs: ["**/*.dart"],
  *detect(event) {
    for (const written of event.written) {
      const image = /["']([\w/.-]+\.(?:png|jpe?g))["']/.exec(written.text)?.[1];
      if (image) {
        yield { line: written.line, match: image, file: image };
      }
    }
  },
});

Pointer or full advice#

The first time a principle fires in a session, the agent reads its whole advice. After that, and for every depth: "pointer" finding, it reads only the title and the summary.

Use pointer when a word matched and nothing is known to be wrong yet, as for a topic in a prompt. mentioned() does that for you.

How many#

A principle reports at most 20 findings for one event. More than that are dropped, and in a test detect() says so.