@yegor256/dogent 0.10.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +87 -12
- package/package.json +1 -1
- package/src/args.js +18 -3
- package/src/defaults.js +47 -0
- package/src/dogent.js +19 -3
- package/src/openai.js +8 -5
- package/src/prompt.js +0 -4
- package/src/rules/ambiguous-or.js +58 -0
- package/src/rules/conditional.js +55 -0
- package/src/rules/consistent.js +1 -1
- package/src/rules/default.js +60 -0
- package/src/rules/description-length.js +64 -0
- package/src/rules/description-voice.js +67 -0
- package/src/rules/duplicate-section.js +65 -0
- package/src/rules/emoji.js +60 -0
- package/src/rules/example-format.js +32 -0
- package/src/rules/external-link.js +57 -0
- package/src/rules/fence-language.js +55 -0
- package/src/rules/hidden-char.js +61 -0
- package/src/rules/homoglyph.js +82 -0
- package/src/rules/index.js +40 -0
- package/src/rules/inline-code.js +79 -0
- package/src/rules/jargon.js +14 -4
- package/src/rules/meta-reference.js +57 -0
- package/src/rules/placement.js +62 -0
- package/src/rules/quantifier.js +63 -0
- package/src/rules/scope.js +31 -0
- package/src/rules/transition.js +59 -0
- package/src/rules/units.js +81 -0
- package/src/rules/weak-verb.js +62 -0
- package/src/version.js +2 -2
package/README.md
CHANGED
|
@@ -13,23 +13,47 @@ Vague, bloated, or ambiguous instructions make agents behave unpredictably.
|
|
|
13
13
|
`dogent` enforces a clear, command-style discipline
|
|
14
14
|
so every line earns its place.
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
16
|
+
## Alternatives
|
|
17
|
+
|
|
18
|
+
Several tools sit near this problem, yet none lint manifesto prose
|
|
19
|
+
the way we do.
|
|
20
|
+
Most rewrite prompts for you or score a file, while we enforce
|
|
21
|
+
line-level discipline and fail the build when one line breaks it.
|
|
22
|
+
|
|
23
|
+
- [agent-sh/agnix](https://github.com/agent-sh/agnix)
|
|
24
|
+
validates the harness — frontmatter schema, hook JSON, MCP config, tools —
|
|
25
|
+
while we lint the prose, asking if each line is a tight command.
|
|
26
|
+
|
|
27
|
+
- [AgentLinter](https://agentlinter.com)
|
|
28
|
+
lints the same files but scores them 0–100 across eight dimensions,
|
|
29
|
+
while we run a strict pass/fail gate, not a scorer.
|
|
30
|
+
|
|
31
|
+
- [microsoft/SkillOpt](https://github.com/microsoft/SkillOpt)
|
|
32
|
+
rewrites `skill.md` against benchmarks to raise an agent's task score,
|
|
33
|
+
while we never rewrite, need no benchmarks, and judge discipline.
|
|
34
|
+
|
|
35
|
+
- [linshenkx/prompt-optimizer](https://github.com/linshenkx/prompt-optimizer)
|
|
36
|
+
rewrites a prompt through an LLM in the browser, round by round,
|
|
37
|
+
while we lint manifesto files offline in CI.
|
|
38
|
+
|
|
39
|
+
- [DSPy](https://github.com/stanfordnlp/dspy)
|
|
40
|
+
tunes prompts as programs to maximize a metric on a dataset,
|
|
41
|
+
while we lint hand-written manifestos, demanding clarity over a score.
|
|
42
|
+
|
|
43
|
+
- Prompt platforms like [LangSmith](https://www.langchain.com/langsmith),
|
|
44
|
+
[PromptLayer](https://www.promptlayer.com), and
|
|
45
|
+
[Braintrust](https://www.braintrust.dev)
|
|
46
|
+
version, trace, and evaluate prompts from runtime data,
|
|
47
|
+
while we lint statically and offline, needing no traces or account.
|
|
26
48
|
|
|
27
49
|
## Usage
|
|
28
50
|
|
|
51
|
+
[Watch the demo](https://github.com/user-attachments/assets/0f05c975-2bc8-4a95-9c3a-76a78fe5d655)
|
|
52
|
+
|
|
29
53
|
Run it on any manifesto file, no installation required:
|
|
30
54
|
|
|
31
55
|
```bash
|
|
32
|
-
npx @yegor256/dogent@0.
|
|
56
|
+
npx @yegor256/dogent@0.10.0 SKILL.md
|
|
33
57
|
```
|
|
34
58
|
|
|
35
59
|
Point it at a directory to lint the default manifestos it holds
|
|
@@ -106,6 +130,11 @@ It sends the manifesto together with one instruction per rule,
|
|
|
106
130
|
then prints any violation the model reports for ambiguity,
|
|
107
131
|
weak phrasing, and instructions that only pretend to be commands.
|
|
108
132
|
The model defaults to `gpt-4o-mini`; override it with `OPENAI_MODEL`.
|
|
133
|
+
Requests go to `https://api.openai.com/v1` by default;
|
|
134
|
+
set `OPENAI_BASE_URL` to any OpenAI-compatible endpoint instead,
|
|
135
|
+
such as a local vLLM, Ollama, or LocalAI server, or a private gateway.
|
|
136
|
+
For a keyless local server, set `OPENAI_API_KEY` to any placeholder,
|
|
137
|
+
since the server ignores it yet `dogent` calls the LLM only when it is set.
|
|
109
138
|
After the report, `dogent` prints a one-line usage summary to standard error,
|
|
110
139
|
naming the model, the tokens sent and received, and an estimated cost,
|
|
111
140
|
for example `OpenAI: gpt-4o-mini, 1234 sent, 567 received, ~$0.0005`.
|
|
@@ -115,6 +144,15 @@ export OPENAI_API_KEY=...
|
|
|
115
144
|
npx @yegor256/dogent CLAUDE.md
|
|
116
145
|
```
|
|
117
146
|
|
|
147
|
+
Point it at a local model the same way:
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
export OPENAI_BASE_URL=http://localhost:11434/v1
|
|
151
|
+
export OPENAI_MODEL=llama3
|
|
152
|
+
export OPENAI_API_KEY=dummy
|
|
153
|
+
npx @yegor256/dogent CLAUDE.md
|
|
154
|
+
```
|
|
155
|
+
|
|
118
156
|
Pass `--offline` to keep `dogent` away from the LLM,
|
|
119
157
|
even when `OPENAI_API_KEY` is present in the environment:
|
|
120
158
|
|
|
@@ -131,6 +169,43 @@ Pass `--suppress` to silence a rule by its id. Repeat the option or
|
|
|
131
169
|
npx @yegor256/dogent --suppress=name-matches-dir,line-length CLAUDE.md
|
|
132
170
|
```
|
|
133
171
|
|
|
172
|
+
Pass `--openai-http-header` to add one `Name: Value` header to every OpenAI
|
|
173
|
+
call, handy for a gateway that wants its own token beside the API key.
|
|
174
|
+
Repeat the option to add several headers:
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
npx @yegor256/dogent \
|
|
178
|
+
--openai-http-header='X-Api-Key: secret' \
|
|
179
|
+
--openai-http-header='X-Tenant: acme' \
|
|
180
|
+
CLAUDE.md
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
A header value often holds a secret, so keep it out of your shell history
|
|
184
|
+
by listing the option in a `.dogent` file (see below) instead.
|
|
185
|
+
|
|
186
|
+
## Defaults file
|
|
187
|
+
|
|
188
|
+
Tired of repeating the same flags on every run?
|
|
189
|
+
Drop a `.dogent` file beside your project, and `dogent` reads its options
|
|
190
|
+
as defaults before it touches the command line.
|
|
191
|
+
The file holds one option per line, written exactly as you would type it,
|
|
192
|
+
with the value after a space (for example `--suppress name-matches-dir`).
|
|
193
|
+
A blank line is ignored, and a line that starts with `#` is a comment:
|
|
194
|
+
|
|
195
|
+
```text
|
|
196
|
+
# project defaults for dogent
|
|
197
|
+
--offline
|
|
198
|
+
--suppress name-matches-dir
|
|
199
|
+
--suppress line-length
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
`dogent` looks first in the current directory, then in your home directory,
|
|
203
|
+
and reads the first `.dogent` it finds.
|
|
204
|
+
The file only supplies defaults, so any option you type on the command line
|
|
205
|
+
overrides the same option in the file.
|
|
206
|
+
Thus `npx @yegor256/dogent --sarif .` still prints SARIF even when the file
|
|
207
|
+
says nothing about it, and a hand-typed flag always wins.
|
|
208
|
+
|
|
134
209
|
## GitHub Actions
|
|
135
210
|
|
|
136
211
|
Because `dogent` runs through `npx`, no extra action is needed.
|
|
@@ -176,7 +251,7 @@ Reference `dogent` as a remote hook in `.pre-commit-config.yaml`:
|
|
|
176
251
|
```yaml
|
|
177
252
|
repos:
|
|
178
253
|
- repo: https://github.com/yegor256/dogent
|
|
179
|
-
rev: 0.
|
|
254
|
+
rev: 0.10.0
|
|
180
255
|
hooks:
|
|
181
256
|
- id: dogent
|
|
182
257
|
```
|
package/package.json
CHANGED
package/src/args.js
CHANGED
|
@@ -16,14 +16,16 @@ const minimist = require('minimist');
|
|
|
16
16
|
* any talk to the LLM even when a token sits in the environment. The `--help`
|
|
17
17
|
* flag, also spelled `-h`, asks for the usage banner. The `--version` flag
|
|
18
18
|
* asks for the release number. The `--suppress` option names a rule to
|
|
19
|
-
* silence; repeat it or join names with commas to silence many at once.
|
|
20
|
-
*
|
|
19
|
+
* silence; repeat it or join names with commas to silence many at once. The
|
|
20
|
+
* `--openai-http-header` option adds one `Name: Value` header to every OpenAI
|
|
21
|
+
* call; repeat it to add many. Everything after a `--` separator counts as a
|
|
22
|
+
* path, never as an option.
|
|
21
23
|
*/
|
|
22
24
|
class Args {
|
|
23
25
|
constructor(
|
|
24
26
|
argv,
|
|
25
27
|
flags = ['sarif', 'offline', 'help', 'version'],
|
|
26
|
-
options = ['suppress']
|
|
28
|
+
options = ['suppress', 'openai-http-header']
|
|
27
29
|
) {
|
|
28
30
|
this.flags = flags;
|
|
29
31
|
this.options = options;
|
|
@@ -50,6 +52,19 @@ class Args {
|
|
|
50
52
|
.map((name) => name.trim())
|
|
51
53
|
.filter((name) => name !== '');
|
|
52
54
|
}
|
|
55
|
+
headers() {
|
|
56
|
+
return [].concat(this.parsed['openai-http-header'] || [])
|
|
57
|
+
.map(String)
|
|
58
|
+
.map((line) => line.trim())
|
|
59
|
+
.filter((line) => line !== '')
|
|
60
|
+
.reduce((acc, line) => {
|
|
61
|
+
const at = line.indexOf(':');
|
|
62
|
+
if (at < 0) {
|
|
63
|
+
throw new Error(`Malformed header "${line}", expected "Name: Value"`);
|
|
64
|
+
}
|
|
65
|
+
return {...acc, [line.slice(0, at).trim()]: line.slice(at + 1).trim()};
|
|
66
|
+
}, {});
|
|
67
|
+
}
|
|
53
68
|
paths() {
|
|
54
69
|
return this.parsed._.concat(this.parsed['--']).map(String);
|
|
55
70
|
}
|
package/src/defaults.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* SPDX-FileCopyrightText: Copyright (c) 2026 Yegor Bugayenko
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const os = require('os');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Defaults.
|
|
14
|
+
*
|
|
15
|
+
* The default command-line options pulled from a `.dogent` file. Dogent looks
|
|
16
|
+
* first in the current directory, then in the user home, and reads the first
|
|
17
|
+
* file it finds. Each line names one option, written exactly as typed on the
|
|
18
|
+
* command line, with its value after a space. A blank line vanishes, and a
|
|
19
|
+
* line that opens with `#` reads as a comment. These options sit ahead of the
|
|
20
|
+
* real argv, so any flag typed by hand overrides the file.
|
|
21
|
+
*/
|
|
22
|
+
class Defaults {
|
|
23
|
+
constructor(
|
|
24
|
+
dirs = [process.cwd(), os.homedir()],
|
|
25
|
+
exists = (file) => fs.existsSync(file),
|
|
26
|
+
reader = (file) => fs.readFileSync(file, 'utf8')
|
|
27
|
+
) {
|
|
28
|
+
this.dirs = dirs;
|
|
29
|
+
this.exists = exists;
|
|
30
|
+
this.reader = reader;
|
|
31
|
+
}
|
|
32
|
+
argv() {
|
|
33
|
+
const file = this.dirs
|
|
34
|
+
.map((dir) => path.join(dir, '.dogent'))
|
|
35
|
+
.find((candidate) => this.exists(candidate));
|
|
36
|
+
if (!file) {
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
return this.reader(file)
|
|
40
|
+
.split('\n')
|
|
41
|
+
.map((line) => line.trim())
|
|
42
|
+
.filter((line) => line !== '' && !line.startsWith('#'))
|
|
43
|
+
.flatMap((line) => line.split(/\s+/u));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
module.exports = Defaults;
|
package/src/dogent.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
const fs = require('fs');
|
|
10
10
|
const Args = require('./args');
|
|
11
|
+
const Defaults = require('./defaults');
|
|
11
12
|
const Markdown = require('./markdown');
|
|
12
13
|
const Report = require('./report');
|
|
13
14
|
const Sources = require('./sources');
|
|
@@ -18,7 +19,7 @@ const prettyMs = require('pretty-ms');
|
|
|
18
19
|
const version = require('./version');
|
|
19
20
|
const rules = require('./rules');
|
|
20
21
|
|
|
21
|
-
const args = new Args(process.argv.slice(2));
|
|
22
|
+
const args = new Args(new Defaults().argv().concat(process.argv.slice(2)));
|
|
22
23
|
const sarif = args.sarif();
|
|
23
24
|
const banner = 'Usage: dogent [--sarif] [--offline] [--suppress=RULE,...] <file.md|dir>...';
|
|
24
25
|
if (args.version()) {
|
|
@@ -33,8 +34,12 @@ if (args.help()) {
|
|
|
33
34
|
' --sarif render the report as SARIF JSON\n' +
|
|
34
35
|
' --offline never call the LLM, even when a token exists\n' +
|
|
35
36
|
' --suppress silence a rule by id; repeat or comma-join to silence many\n' +
|
|
37
|
+
' --openai-http-header add a "Name: Value" header to OpenAI calls\n' +
|
|
36
38
|
' --version show the version and exit\n' +
|
|
37
|
-
' --help show this help and exit\n'
|
|
39
|
+
' --help show this help and exit\n\n' +
|
|
40
|
+
'Defaults:\n' +
|
|
41
|
+
' A .dogent file in the current directory or home holds default\n' +
|
|
42
|
+
' options, one per line; options typed by hand override it.\n'
|
|
38
43
|
);
|
|
39
44
|
process.exit(0);
|
|
40
45
|
}
|
|
@@ -44,6 +49,13 @@ if (unknown.length > 0) {
|
|
|
44
49
|
process.stderr.write(`${banner}\n`);
|
|
45
50
|
process.exit(2);
|
|
46
51
|
}
|
|
52
|
+
let headers = {};
|
|
53
|
+
try {
|
|
54
|
+
headers = args.headers();
|
|
55
|
+
} catch (error) {
|
|
56
|
+
process.stderr.write(`${error.message}\n`);
|
|
57
|
+
process.exit(2);
|
|
58
|
+
}
|
|
47
59
|
const paths = args.paths();
|
|
48
60
|
if (paths.length === 0) {
|
|
49
61
|
process.stderr.write(`${banner}\n`);
|
|
@@ -72,7 +84,11 @@ const audit = async (docs) => {
|
|
|
72
84
|
new Openai(
|
|
73
85
|
key,
|
|
74
86
|
process.env.OPENAI_MODEL || 'gpt-4o-mini',
|
|
75
|
-
|
|
87
|
+
process.env.OPENAI_BASE_URL || 'https://api.openai.com/v1',
|
|
88
|
+
(url, options) => globalThis.fetch(
|
|
89
|
+
url,
|
|
90
|
+
{...options, headers: {...options.headers, ...headers}}
|
|
91
|
+
)
|
|
76
92
|
)
|
|
77
93
|
);
|
|
78
94
|
const replies = await Promise.all(docs.map((doc) => oracle.violations(doc)));
|
package/src/openai.js
CHANGED
|
@@ -10,20 +10,23 @@ const Usage = require('./usage');
|
|
|
10
10
|
/**
|
|
11
11
|
* Openai.
|
|
12
12
|
*
|
|
13
|
-
* A thin adapter over
|
|
14
|
-
* prompt, demands a JSON object back, and returns the assistant
|
|
15
|
-
* paired with a Usage tally of the model and the tokens it consumed.
|
|
13
|
+
* A thin adapter over an OpenAI-compatible chat-completions endpoint.
|
|
14
|
+
* Sends one prompt, demands a JSON object back, and returns the assistant
|
|
15
|
+
* text paired with a Usage tally of the model and the tokens it consumed.
|
|
16
|
+
* The base URL is configurable, so the same adapter reaches OpenAI itself
|
|
17
|
+
* or any compatible server, such as vLLM, Ollama, or a private gateway.
|
|
16
18
|
* The transport is injected so the class runs in tests without a socket.
|
|
17
19
|
*/
|
|
18
20
|
class Openai {
|
|
19
|
-
constructor(key, model, transport) {
|
|
21
|
+
constructor(key, model, base, transport) {
|
|
20
22
|
this.key = key;
|
|
21
23
|
this.model = model;
|
|
24
|
+
this.base = base;
|
|
22
25
|
this.transport = transport;
|
|
23
26
|
}
|
|
24
27
|
async answer(prompt) {
|
|
25
28
|
const response = await this.transport(
|
|
26
|
-
|
|
29
|
+
`${this.base.replace(/\/+$/u, '')}/chat/completions`,
|
|
27
30
|
{
|
|
28
31
|
method: 'POST',
|
|
29
32
|
headers: {
|
package/src/prompt.js
CHANGED
|
@@ -41,10 +41,6 @@ class Prompt {
|
|
|
41
41
|
'report nothing and return {"results": []}. Never invent a',
|
|
42
42
|
'violation to seem useful, and never flag a line merely for',
|
|
43
43
|
'sitting in a plain, on-topic section.',
|
|
44
|
-
'One clash is never a false alarm: when two lines give the same',
|
|
45
|
-
'order twice, or demand opposite things, report each offending',
|
|
46
|
-
'line with high confidence, even while every other check stays',
|
|
47
|
-
'conservative. A clean file has no such clash and stays silent.',
|
|
48
44
|
'Report a violation only when you are certain it breaks a check.',
|
|
49
45
|
'Give every result a "confidence" number from 0 to 1, your own',
|
|
50
46
|
'probability that the violation is real, and omit any result you',
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* SPDX-FileCopyrightText: Copyright (c) 2026 Yegor Bugayenko
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
const Violation = require('../violation');
|
|
9
|
+
const Region = require('../region');
|
|
10
|
+
const mask = require('../mask');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* AmbiguousOr.
|
|
14
|
+
*
|
|
15
|
+
* An "either-or" choice left to the reader is an instruction the agent
|
|
16
|
+
* cannot follow without guessing. This flags the literal "and/or" and a
|
|
17
|
+
* two-term slashed alternative like "tabs/spaces", each demanding the
|
|
18
|
+
* author pick one branch and state exactly which option applies. Inline
|
|
19
|
+
* code is masked first, so a backticked path such as `src/app` never
|
|
20
|
+
* matches. Subtler ambiguities stay with the oracle; prompt() defers
|
|
21
|
+
* the open-ended either-or judgement to it.
|
|
22
|
+
*/
|
|
23
|
+
class AmbiguousOr {
|
|
24
|
+
constructor() {
|
|
25
|
+
this.id = 'ambiguous-or';
|
|
26
|
+
}
|
|
27
|
+
prompt() {
|
|
28
|
+
return `${this.id}: flag either-or ambiguity beyond "and/or" and slashed alternatives, and state exactly which option applies`;
|
|
29
|
+
}
|
|
30
|
+
violations(document) {
|
|
31
|
+
const uri = document.uri();
|
|
32
|
+
return document.walk({
|
|
33
|
+
header: () => [],
|
|
34
|
+
prose: (text, line) => this.scan(text, line, uri),
|
|
35
|
+
snippet: () => [],
|
|
36
|
+
bullets: () => [],
|
|
37
|
+
frontmatter: () => []
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
scan(text, line, uri) {
|
|
41
|
+
const masked = mask(text);
|
|
42
|
+
const pattern = /\band\/or\b|(?<![\w./])[A-Za-z]{2,}\/[A-Za-z]{2,}(?![\w/]|\.\w)/giu;
|
|
43
|
+
const found = [];
|
|
44
|
+
let hit = pattern.exec(masked);
|
|
45
|
+
while (hit !== null) {
|
|
46
|
+
found.push(new Violation(
|
|
47
|
+
this.id,
|
|
48
|
+
'warning',
|
|
49
|
+
`ambiguous "${hit[0]}", state exactly which option applies`,
|
|
50
|
+
new Region(uri, line, hit.index + 1)
|
|
51
|
+
));
|
|
52
|
+
hit = pattern.exec(masked);
|
|
53
|
+
}
|
|
54
|
+
return found;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
module.exports = AmbiguousOr;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* SPDX-FileCopyrightText: Copyright (c) 2026 Yegor Bugayenko
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
const Violation = require('../violation');
|
|
9
|
+
const Region = require('../region');
|
|
10
|
+
const mask = require('../mask');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Conditional.
|
|
14
|
+
*
|
|
15
|
+
* Demands that branching never collapse onto one line. A line carrying
|
|
16
|
+
* more than one condition keyword (if, unless, when, else, otherwise)
|
|
17
|
+
* spells out a whole branch tree at once, so each case must split into
|
|
18
|
+
* its own command. Distinct from simple, which weighs clause depth, and
|
|
19
|
+
* from atomic, which counts instructions; this one targets branching
|
|
20
|
+
* alone. A lone guard keeps just one keyword and stays clean.
|
|
21
|
+
*/
|
|
22
|
+
class Conditional {
|
|
23
|
+
constructor() {
|
|
24
|
+
this.id = 'conditional';
|
|
25
|
+
}
|
|
26
|
+
prompt() {
|
|
27
|
+
return `${this.id}: flag implicit branching that carries no keyword, and split each case into its own command`;
|
|
28
|
+
}
|
|
29
|
+
violations(document) {
|
|
30
|
+
const uri = document.uri();
|
|
31
|
+
return document.walk({
|
|
32
|
+
header: () => [],
|
|
33
|
+
prose: (text, line) => this.judge(text, line, uri),
|
|
34
|
+
snippet: () => [],
|
|
35
|
+
bullets: () => [],
|
|
36
|
+
frontmatter: () => []
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
judge(text, line, uri) {
|
|
40
|
+
const clean = mask(text);
|
|
41
|
+
const hits = clean.match(/\b(?:if|unless|when|else|otherwise)\b/giu);
|
|
42
|
+
if (hits === null || hits.length < 2) {
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
const column = clean.search(/\b(?:if|unless|when|else|otherwise)\b/iu);
|
|
46
|
+
return [new Violation(
|
|
47
|
+
this.id,
|
|
48
|
+
'warning',
|
|
49
|
+
'multi-branch conditional, split each case into its own command',
|
|
50
|
+
new Region(uri, line, column + 1)
|
|
51
|
+
)];
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
module.exports = Conditional;
|
package/src/rules/consistent.js
CHANGED
|
@@ -20,7 +20,7 @@ class Consistent {
|
|
|
20
20
|
this.id = 'consistent';
|
|
21
21
|
}
|
|
22
22
|
prompt() {
|
|
23
|
-
return `${this.id}: flag an instruction that repeats another instruction word for word, or that
|
|
23
|
+
return `${this.id}: flag an instruction that repeats another instruction word for word, or that logically contradicts another instruction about the very same subject, where one line orders exactly what another forbids; ignore lines that merely share a theme but govern different concerns, since complementary instructions never clash`;
|
|
24
24
|
}
|
|
25
25
|
violations() {
|
|
26
26
|
return [];
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* SPDX-FileCopyrightText: Copyright (c) 2026 Yegor Bugayenko
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
const Violation = require('../violation');
|
|
9
|
+
const Region = require('../region');
|
|
10
|
+
const mask = require('../mask');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Default.
|
|
14
|
+
*
|
|
15
|
+
* Demands that optional behavior names its default. A line marking work
|
|
16
|
+
* as optional through "optionally", "you may", or "feel free to" leaves
|
|
17
|
+
* the agent guessing what happens when it declines, so the line must
|
|
18
|
+
* state a default. A line that already declares one through "by
|
|
19
|
+
* default", "defaults to", or "otherwise" passes untouched. Its prompt
|
|
20
|
+
* hands subtler optionality with no stated default to the AI oracle.
|
|
21
|
+
*/
|
|
22
|
+
class Default {
|
|
23
|
+
constructor() {
|
|
24
|
+
this.id = 'default';
|
|
25
|
+
}
|
|
26
|
+
prompt() {
|
|
27
|
+
return `${this.id}: flag optionality that names no default even without a listed marker, and state the default`;
|
|
28
|
+
}
|
|
29
|
+
violations(document) {
|
|
30
|
+
const uri = document.uri();
|
|
31
|
+
return document.walk({
|
|
32
|
+
header: () => [],
|
|
33
|
+
prose: (text, line) => this.scan(text, line, uri),
|
|
34
|
+
snippet: () => [],
|
|
35
|
+
bullets: () => [],
|
|
36
|
+
frontmatter: () => []
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
scan(text, line, uri) {
|
|
40
|
+
const masked = mask(text);
|
|
41
|
+
if ((/\b(?:by default|default to|defaults to|otherwise)\b/iu).test(masked)) {
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
const found = [];
|
|
45
|
+
const regex = /\b(?:optionally|you may|you can|if you want|feel free to|as an option)\b/giu;
|
|
46
|
+
let hit = regex.exec(masked);
|
|
47
|
+
while (hit !== null) {
|
|
48
|
+
found.push(new Violation(
|
|
49
|
+
this.id,
|
|
50
|
+
'warning',
|
|
51
|
+
`optional behavior "${hit[0]}" has no default, state it`,
|
|
52
|
+
new Region(uri, line, hit.index + 1)
|
|
53
|
+
));
|
|
54
|
+
hit = regex.exec(masked);
|
|
55
|
+
}
|
|
56
|
+
return found;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
module.exports = Default;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* SPDX-FileCopyrightText: Copyright (c) 2026 Yegor Bugayenko
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
const Violation = require('../violation');
|
|
9
|
+
const Region = require('../region');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* DescriptionLength.
|
|
13
|
+
*
|
|
14
|
+
* Demands that a SKILL.md description stay within a sane size. The
|
|
15
|
+
* loader keeps every description in context, so an overgrown one wastes
|
|
16
|
+
* the budget that the instructions need. Flags a value longer than the
|
|
17
|
+
* ceiling and a value that is empty, leaving the wording itself to
|
|
18
|
+
* sibling rules.
|
|
19
|
+
*
|
|
20
|
+
* The check is standalone and deterministic, so prompt() returns an
|
|
21
|
+
* empty string and the AI oracle never re-checks this rule.
|
|
22
|
+
*/
|
|
23
|
+
class DescriptionLength {
|
|
24
|
+
constructor() {
|
|
25
|
+
this.id = 'description-length';
|
|
26
|
+
this.ceiling = 1024;
|
|
27
|
+
}
|
|
28
|
+
prompt() {
|
|
29
|
+
return '';
|
|
30
|
+
}
|
|
31
|
+
violations(document) {
|
|
32
|
+
const uri = document.uri();
|
|
33
|
+
if (uri.replace(/^.*\//u, '') !== 'SKILL.md') {
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
const pairs = document.walk({
|
|
37
|
+
header: () => [],
|
|
38
|
+
prose: () => [],
|
|
39
|
+
snippet: () => [],
|
|
40
|
+
bullets: () => [],
|
|
41
|
+
frontmatter: (keys) => keys
|
|
42
|
+
});
|
|
43
|
+
const found = pairs.filter((pair) => pair.key === 'description');
|
|
44
|
+
if (found.length === 0) {
|
|
45
|
+
return [];
|
|
46
|
+
}
|
|
47
|
+
return this.judge(found[0], uri);
|
|
48
|
+
}
|
|
49
|
+
judge(pair, uri) {
|
|
50
|
+
const {value} = pair;
|
|
51
|
+
if (value.trim() === '') {
|
|
52
|
+
return [this.flag('description is empty, write a concise capability statement', pair.row, uri)];
|
|
53
|
+
}
|
|
54
|
+
if (value.length > this.ceiling) {
|
|
55
|
+
return [this.flag(`description is ${value.length} chars, keep it under ${this.ceiling}`, pair.row, uri)];
|
|
56
|
+
}
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
flag(message, row, uri) {
|
|
60
|
+
return new Violation(this.id, 'warning', message, new Region(uri, row, 1));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
module.exports = DescriptionLength;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* SPDX-FileCopyrightText: Copyright (c) 2026 Yegor Bugayenko
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
const Violation = require('../violation');
|
|
9
|
+
const Region = require('../region');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* DescriptionVoice.
|
|
13
|
+
*
|
|
14
|
+
* Demands that a SKILL.md description stay in the third person, reading
|
|
15
|
+
* as a capability statement like "Extracts tables ..." rather than a
|
|
16
|
+
* first- or second-person sentence like "I extract ..." or "You can
|
|
17
|
+
* use ...". A standalone checker flags first- and second-person
|
|
18
|
+
* pronouns as whole words, after dropping the trigger clause that opens
|
|
19
|
+
* with "Use when" so a legitimate "Use when ..." phrase stays clean.
|
|
20
|
+
* Distinct from description-triggers, which checks that a "when" clause
|
|
21
|
+
* exists, and from description-length, which checks the size; this one
|
|
22
|
+
* checks the grammatical voice. Its prompt hands subtler voice
|
|
23
|
+
* judgement to the AI oracle.
|
|
24
|
+
*/
|
|
25
|
+
class DescriptionVoice {
|
|
26
|
+
constructor() {
|
|
27
|
+
this.id = 'description-voice';
|
|
28
|
+
this.pronoun = /\b(?:I|we|you|your|my|our)\b/giu;
|
|
29
|
+
}
|
|
30
|
+
prompt() {
|
|
31
|
+
return `${this.id}: in a SKILL.md, flag a description written in first or second person and demand a third-person capability statement`;
|
|
32
|
+
}
|
|
33
|
+
violations(document) {
|
|
34
|
+
const uri = document.uri();
|
|
35
|
+
if (uri.replace(/^.*\//u, '') !== 'SKILL.md') {
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
const pairs = document.walk({
|
|
39
|
+
header: () => [],
|
|
40
|
+
prose: () => [],
|
|
41
|
+
snippet: () => [],
|
|
42
|
+
bullets: () => [],
|
|
43
|
+
frontmatter: (keys) => keys
|
|
44
|
+
});
|
|
45
|
+
const found = pairs.filter((pair) => pair.key === 'description');
|
|
46
|
+
if (found.length === 0) {
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
return this.judge(found[0], uri);
|
|
50
|
+
}
|
|
51
|
+
judge(pair, uri) {
|
|
52
|
+
const text = pair.value.replace(/use when.*$/isu, '');
|
|
53
|
+
this.pronoun.lastIndex = 0;
|
|
54
|
+
const hit = this.pronoun.exec(text);
|
|
55
|
+
if (hit === null) {
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
58
|
+
return [new Violation(
|
|
59
|
+
this.id,
|
|
60
|
+
'warning',
|
|
61
|
+
`description must be third person, not "${hit[0]}"`,
|
|
62
|
+
new Region(uri, pair.row, 1)
|
|
63
|
+
)];
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = DescriptionVoice;
|