@yegor256/dogent 0.6.0 → 0.6.2
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 +5 -2
- package/package.json +4 -1
- package/src/args.js +15 -12
- package/src/dogent.js +32 -15
- package/src/openai.js +14 -2
- package/src/oracle.js +7 -4
- package/src/rules/empty.js +4 -1
- package/src/rules/index.js +2 -0
- package/src/rules/line-length.js +4 -1
- package/src/rules/redundant.js +92 -0
- package/src/usage.js +52 -0
package/README.md
CHANGED
|
@@ -29,7 +29,7 @@ In short: `agnix` lints the harness, `dogent` lints the prompt.
|
|
|
29
29
|
Run it on any manifesto file, no installation required:
|
|
30
30
|
|
|
31
31
|
```bash
|
|
32
|
-
npx @yegor256/dogent@0.
|
|
32
|
+
npx @yegor256/dogent@0.6.0 CLAUDE.md
|
|
33
33
|
```
|
|
34
34
|
|
|
35
35
|
Lint several files at once:
|
|
@@ -90,6 +90,9 @@ It sends the manifesto together with one instruction per rule,
|
|
|
90
90
|
then prints any violation the model reports for ambiguity,
|
|
91
91
|
weak phrasing, and instructions that only pretend to be commands.
|
|
92
92
|
The model defaults to `gpt-4o-mini`; override it with `OPENAI_MODEL`.
|
|
93
|
+
After the report, `dogent` prints a one-line usage summary to standard error,
|
|
94
|
+
naming the model, the tokens sent and received, and an estimated cost,
|
|
95
|
+
for example `OpenAI: gpt-4o-mini, 1234 sent, 567 received, ~$0.0005`.
|
|
93
96
|
|
|
94
97
|
```bash
|
|
95
98
|
export OPENAI_API_KEY=...
|
|
@@ -150,7 +153,7 @@ Reference `dogent` as a remote hook in `.pre-commit-config.yaml`:
|
|
|
150
153
|
```yaml
|
|
151
154
|
repos:
|
|
152
155
|
- repo: https://github.com/yegor256/dogent
|
|
153
|
-
rev: 0.
|
|
156
|
+
rev: 0.6.0
|
|
154
157
|
hooks:
|
|
155
158
|
- id: dogent
|
|
156
159
|
```
|
package/package.json
CHANGED
package/src/args.js
CHANGED
|
@@ -5,32 +5,35 @@
|
|
|
5
5
|
|
|
6
6
|
'use strict';
|
|
7
7
|
|
|
8
|
+
const minimist = require('minimist');
|
|
9
|
+
|
|
8
10
|
/**
|
|
9
11
|
* Args.
|
|
10
12
|
*
|
|
11
|
-
* The command-line arguments handed to dogent. It
|
|
12
|
-
* recognized options and the manifesto paths that remain.
|
|
13
|
-
* flag switches the report to SARIF, while `--offline` forbids
|
|
14
|
-
* the LLM even when a token sits in the environment.
|
|
13
|
+
* The command-line arguments handed to dogent. It leans on minimist to split
|
|
14
|
+
* the raw argv into recognized options and the manifesto paths that remain.
|
|
15
|
+
* The `--sarif` flag switches the report to SARIF, while `--offline` forbids
|
|
16
|
+
* any talk to the LLM even when a token sits in the environment. Everything
|
|
17
|
+
* after a `--` separator counts as a path, never as an option.
|
|
15
18
|
*/
|
|
16
19
|
class Args {
|
|
17
|
-
constructor(argv, flags = ['
|
|
18
|
-
this.argv = argv;
|
|
20
|
+
constructor(argv, flags = ['sarif', 'offline']) {
|
|
19
21
|
this.flags = flags;
|
|
22
|
+
this.parsed = minimist(argv, {boolean: flags, '--': true});
|
|
20
23
|
}
|
|
21
24
|
sarif() {
|
|
22
|
-
return this.
|
|
25
|
+
return this.parsed.sarif === true;
|
|
23
26
|
}
|
|
24
27
|
offline() {
|
|
25
|
-
return this.
|
|
28
|
+
return this.parsed.offline === true;
|
|
26
29
|
}
|
|
27
30
|
paths() {
|
|
28
|
-
return this.
|
|
31
|
+
return this.parsed._.concat(this.parsed['--']).map(String);
|
|
29
32
|
}
|
|
30
33
|
unknown() {
|
|
31
|
-
return this.
|
|
32
|
-
(
|
|
33
|
-
|
|
34
|
+
return Object.keys(this.parsed)
|
|
35
|
+
.filter((key) => key !== '_' && key !== '--' && !this.flags.includes(key))
|
|
36
|
+
.map((key) => `${key.length === 1 ? '-' : '--'}${key}`);
|
|
34
37
|
}
|
|
35
38
|
}
|
|
36
39
|
|
package/src/dogent.js
CHANGED
|
@@ -13,6 +13,7 @@ const Report = require('./report');
|
|
|
13
13
|
const Sources = require('./sources');
|
|
14
14
|
const Openai = require('./openai');
|
|
15
15
|
const Oracle = require('./oracle');
|
|
16
|
+
const Usage = require('./usage');
|
|
16
17
|
const rules = require('./rules');
|
|
17
18
|
|
|
18
19
|
const args = new Args(process.argv.slice(2));
|
|
@@ -41,27 +42,43 @@ documents.forEach((document) => {
|
|
|
41
42
|
});
|
|
42
43
|
});
|
|
43
44
|
const key = process.env.OPENAI_API_KEY;
|
|
45
|
+
const audit = async (docs) => {
|
|
46
|
+
const oracle = new Oracle(
|
|
47
|
+
rules(),
|
|
48
|
+
new Openai(
|
|
49
|
+
key,
|
|
50
|
+
process.env.OPENAI_MODEL || 'gpt-4o-mini',
|
|
51
|
+
(url, options) => globalThis.fetch(url, options)
|
|
52
|
+
)
|
|
53
|
+
);
|
|
54
|
+
const replies = await Promise.all(docs.map((doc) => oracle.violations(doc)));
|
|
55
|
+
return replies.reduce(
|
|
56
|
+
(acc, reply) => ({
|
|
57
|
+
extra: acc.extra.concat(reply.found),
|
|
58
|
+
usage: acc.usage.plus(reply.usage)
|
|
59
|
+
}),
|
|
60
|
+
{extra: [], usage: new Usage('', 0, 0)}
|
|
61
|
+
);
|
|
62
|
+
};
|
|
63
|
+
const finish = (usage) => {
|
|
64
|
+
const report = new Report('dogent', found);
|
|
65
|
+
process.stdout.write(`${sarif ? JSON.stringify(report.sarif(), null, 2) : report.text()}\n`);
|
|
66
|
+
if (usage !== null) {
|
|
67
|
+
process.stderr.write(`${usage.text()}\n`);
|
|
68
|
+
}
|
|
69
|
+
process.exit(report.count() > 0 ? 1 : 0);
|
|
70
|
+
};
|
|
44
71
|
(async () => {
|
|
72
|
+
let usage = null;
|
|
45
73
|
if (found.length === 0 && key && !args.offline()) {
|
|
46
74
|
try {
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
key,
|
|
51
|
-
process.env.OPENAI_MODEL || 'gpt-4o-mini',
|
|
52
|
-
(url, options) => globalThis.fetch(url, options)
|
|
53
|
-
)
|
|
54
|
-
);
|
|
55
|
-
const extra = await Promise.all(
|
|
56
|
-
documents.map((document) => oracle.violations(document))
|
|
57
|
-
);
|
|
58
|
-
extra.forEach((bag) => bag.forEach((violation) => found.push(violation)));
|
|
75
|
+
const result = await audit(documents);
|
|
76
|
+
result.extra.forEach((violation) => found.push(violation));
|
|
77
|
+
({usage} = result);
|
|
59
78
|
} catch (error) {
|
|
60
79
|
process.stderr.write(`AI verification failed: ${error.message}\n`);
|
|
61
80
|
process.exit(2);
|
|
62
81
|
}
|
|
63
82
|
}
|
|
64
|
-
|
|
65
|
-
process.stdout.write(`${sarif ? JSON.stringify(report.sarif(), null, 2) : report.text()}\n`);
|
|
66
|
-
process.exit(report.count() > 0 ? 1 : 0);
|
|
83
|
+
finish(usage);
|
|
67
84
|
})();
|
package/src/openai.js
CHANGED
|
@@ -5,11 +5,14 @@
|
|
|
5
5
|
|
|
6
6
|
'use strict';
|
|
7
7
|
|
|
8
|
+
const Usage = require('./usage');
|
|
9
|
+
|
|
8
10
|
/**
|
|
9
11
|
* Openai.
|
|
10
12
|
*
|
|
11
13
|
* A thin adapter over the OpenAI chat-completions endpoint. Sends one
|
|
12
|
-
* prompt, demands a JSON object back, and returns the assistant text
|
|
14
|
+
* prompt, demands a JSON object back, and returns the assistant text
|
|
15
|
+
* paired with a Usage tally of the model and the tokens it consumed.
|
|
13
16
|
* The transport is injected so the class runs in tests without a socket.
|
|
14
17
|
*/
|
|
15
18
|
class Openai {
|
|
@@ -38,7 +41,16 @@ class Openai {
|
|
|
38
41
|
if (!response.ok) {
|
|
39
42
|
throw new Error(`OpenAI request rejected with status ${response.status}`);
|
|
40
43
|
}
|
|
41
|
-
|
|
44
|
+
const body = await response.json();
|
|
45
|
+
const usage = body.usage || {};
|
|
46
|
+
return {
|
|
47
|
+
content: body.choices[0].message.content,
|
|
48
|
+
usage: new Usage(
|
|
49
|
+
this.model,
|
|
50
|
+
usage.prompt_tokens || 0,
|
|
51
|
+
usage.completion_tokens || 0
|
|
52
|
+
)
|
|
53
|
+
};
|
|
42
54
|
}
|
|
43
55
|
}
|
|
44
56
|
|
package/src/oracle.js
CHANGED
|
@@ -13,7 +13,8 @@ const Answer = require('./answer');
|
|
|
13
13
|
*
|
|
14
14
|
* The AI second opinion. Wraps the rules and a chat endpoint, builds one
|
|
15
15
|
* prompt from a document, asks the endpoint, and parses the reply into
|
|
16
|
-
* violations
|
|
16
|
+
* violations paired with the token usage the model reported. Mirrors a
|
|
17
|
+
* rule, but consults a model instead of guessing.
|
|
17
18
|
*/
|
|
18
19
|
class Oracle {
|
|
19
20
|
constructor(rules, chat) {
|
|
@@ -21,9 +22,11 @@ class Oracle {
|
|
|
21
22
|
this.chat = chat;
|
|
22
23
|
}
|
|
23
24
|
async violations(document) {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
const reply = await this.chat.answer(new Prompt(this.rules, document).text());
|
|
26
|
+
return {
|
|
27
|
+
found: new Answer(reply.content).violations(),
|
|
28
|
+
usage: reply.usage
|
|
29
|
+
};
|
|
27
30
|
}
|
|
28
31
|
}
|
|
29
32
|
|
package/src/rules/empty.js
CHANGED
|
@@ -15,13 +15,16 @@ const Region = require('../region');
|
|
|
15
15
|
* A heading is empty when it is immediately followed by another
|
|
16
16
|
* heading or by end-of-file — no prose, bullets, or snippet sits
|
|
17
17
|
* between them.
|
|
18
|
+
*
|
|
19
|
+
* The check is standalone and deterministic, so prompt() returns an
|
|
20
|
+
* empty string and the AI oracle never re-checks this rule.
|
|
18
21
|
*/
|
|
19
22
|
class Empty {
|
|
20
23
|
constructor() {
|
|
21
24
|
this.id = 'empty';
|
|
22
25
|
}
|
|
23
26
|
prompt() {
|
|
24
|
-
return
|
|
27
|
+
return '';
|
|
25
28
|
}
|
|
26
29
|
violations(document) {
|
|
27
30
|
const uri = document.uri();
|
package/src/rules/index.js
CHANGED
|
@@ -15,6 +15,7 @@ const Command = require('./command');
|
|
|
15
15
|
const Punctuation = require('./punctuation');
|
|
16
16
|
const Frontmatter = require('./frontmatter');
|
|
17
17
|
const DeadImport = require('./dead-import');
|
|
18
|
+
const Redundant = require('./redundant');
|
|
18
19
|
|
|
19
20
|
module.exports = () => [
|
|
20
21
|
new Grouped(),
|
|
@@ -26,6 +27,7 @@ module.exports = () => [
|
|
|
26
27
|
new Command(),
|
|
27
28
|
new Punctuation(),
|
|
28
29
|
new DeadImport(),
|
|
30
|
+
new Redundant(),
|
|
29
31
|
new Frontmatter(
|
|
30
32
|
'SKILL.md',
|
|
31
33
|
['name', 'description'],
|
package/src/rules/line-length.js
CHANGED
|
@@ -13,6 +13,9 @@ const Region = require('../region');
|
|
|
13
13
|
*
|
|
14
14
|
* Demands that every instruction and every heading stay within a
|
|
15
15
|
* maximum width. Code snippets are exempt, since code is not prose.
|
|
16
|
+
*
|
|
17
|
+
* The check is standalone and deterministic, so prompt() returns an
|
|
18
|
+
* empty string and the AI oracle never re-checks this rule.
|
|
16
19
|
*/
|
|
17
20
|
class LineLength {
|
|
18
21
|
constructor(max) {
|
|
@@ -20,7 +23,7 @@ class LineLength {
|
|
|
20
23
|
this.max = max;
|
|
21
24
|
}
|
|
22
25
|
prompt() {
|
|
23
|
-
return
|
|
26
|
+
return '';
|
|
24
27
|
}
|
|
25
28
|
violations(document) {
|
|
26
29
|
const uri = document.uri();
|
|
@@ -0,0 +1,92 @@
|
|
|
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
|
+
const PHRASES = [
|
|
12
|
+
'be helpful',
|
|
13
|
+
'be helpful and accurate',
|
|
14
|
+
'be accurate',
|
|
15
|
+
'be concise',
|
|
16
|
+
'be clear',
|
|
17
|
+
'be polite',
|
|
18
|
+
'be professional',
|
|
19
|
+
'write clean code',
|
|
20
|
+
'write good code',
|
|
21
|
+
'write readable code',
|
|
22
|
+
'write maintainable code',
|
|
23
|
+
'follow best practices',
|
|
24
|
+
'follow industry best practices',
|
|
25
|
+
'use best practices',
|
|
26
|
+
'apply best practices',
|
|
27
|
+
'use meaningful variable names',
|
|
28
|
+
'use descriptive variable names',
|
|
29
|
+
'use good variable names',
|
|
30
|
+
'handle errors properly',
|
|
31
|
+
'handle errors gracefully',
|
|
32
|
+
'handle exceptions properly',
|
|
33
|
+
'avoid bugs',
|
|
34
|
+
'avoid mistakes',
|
|
35
|
+
'think step by step',
|
|
36
|
+
'do your best',
|
|
37
|
+
'try your best',
|
|
38
|
+
'pay attention to detail'
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Redundant.
|
|
43
|
+
*
|
|
44
|
+
* Flags a line that restates default model behavior, like
|
|
45
|
+
* "Be helpful and accurate" or "Write clean code". Such filler
|
|
46
|
+
* burns the context budget and drowns the project-specific
|
|
47
|
+
* guidance the manifesto exists to carry.
|
|
48
|
+
*
|
|
49
|
+
* @todo #15:60min Promote the standalone heuristic into a
|
|
50
|
+
* proper AI-oracle check when `OPENAI_API_KEY` is present, so
|
|
51
|
+
* redundancy detection covers paraphrases beyond the curated
|
|
52
|
+
* blacklist below. The hybrid pattern from `src/rules/command.js`
|
|
53
|
+
* is the model: keep the deterministic check as the default,
|
|
54
|
+
* let the oracle catch the rest.
|
|
55
|
+
*/
|
|
56
|
+
class Redundant {
|
|
57
|
+
constructor(phrases = PHRASES) {
|
|
58
|
+
this.id = 'redundant';
|
|
59
|
+
this.phrases = phrases;
|
|
60
|
+
}
|
|
61
|
+
prompt() {
|
|
62
|
+
return `${this.id}: flag any line that restates default agent behavior already known to the model, not a project-specific instruction`;
|
|
63
|
+
}
|
|
64
|
+
violations(document) {
|
|
65
|
+
const uri = document.uri();
|
|
66
|
+
return document.walk({
|
|
67
|
+
header: () => [],
|
|
68
|
+
prose: (text, line) => this.judge(text, line, uri),
|
|
69
|
+
snippet: () => [],
|
|
70
|
+
bullets: () => [],
|
|
71
|
+
frontmatter: () => []
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
judge(text, line, uri) {
|
|
75
|
+
const clean = text
|
|
76
|
+
.replace(/^\s*(?:[-*+]|\d+\.)\s+/u, '')
|
|
77
|
+
.replace(/[.!?]+\s*$/u, '')
|
|
78
|
+
.trim()
|
|
79
|
+
.toLowerCase();
|
|
80
|
+
if (!this.phrases.includes(clean)) {
|
|
81
|
+
return [];
|
|
82
|
+
}
|
|
83
|
+
return [new Violation(
|
|
84
|
+
this.id,
|
|
85
|
+
'error',
|
|
86
|
+
'generic instruction, model already knows this',
|
|
87
|
+
new Region(uri, line, 1)
|
|
88
|
+
)];
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
module.exports = Redundant;
|
package/src/usage.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* SPDX-FileCopyrightText: Copyright (c) 2026 Yegor Bugayenko
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Price table.
|
|
10
|
+
*
|
|
11
|
+
* United States dollars per one million tokens, sent and received, for
|
|
12
|
+
* every model dogent knows. An unknown model falls back to zero, so the
|
|
13
|
+
* summary still prints token counts without inventing a price.
|
|
14
|
+
*/
|
|
15
|
+
const PRICES = {
|
|
16
|
+
'gpt-4o-mini': {input: 0.15, output: 0.6},
|
|
17
|
+
'gpt-4o': {input: 2.5, output: 10},
|
|
18
|
+
'gpt-4.1-nano': {input: 0.1, output: 0.4},
|
|
19
|
+
'gpt-4.1-mini': {input: 0.4, output: 1.6},
|
|
20
|
+
'gpt-4.1': {input: 2, output: 8}
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Usage.
|
|
25
|
+
*
|
|
26
|
+
* One immutable tally of an OpenAI exchange: the model, the tokens sent,
|
|
27
|
+
* and the tokens received. Sums itself with another tally and renders a
|
|
28
|
+
* single human summary line, complete with an estimated dollar cost.
|
|
29
|
+
*/
|
|
30
|
+
class Usage {
|
|
31
|
+
constructor(model, sent, received) {
|
|
32
|
+
this.model = model;
|
|
33
|
+
this.sent = sent;
|
|
34
|
+
this.received = received;
|
|
35
|
+
}
|
|
36
|
+
plus(other) {
|
|
37
|
+
return new Usage(
|
|
38
|
+
this.model || other.model,
|
|
39
|
+
this.sent + other.sent,
|
|
40
|
+
this.received + other.received
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
cost() {
|
|
44
|
+
const price = PRICES[this.model] || {input: 0, output: 0};
|
|
45
|
+
return this.sent / 1e6 * price.input + this.received / 1e6 * price.output;
|
|
46
|
+
}
|
|
47
|
+
text() {
|
|
48
|
+
return `OpenAI: ${this.model}, ${this.sent} sent, ${this.received} received, ~$${this.cost().toFixed(4)}`;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
module.exports = Usage;
|