honestdocs 0.1.1 → 0.1.3
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 +8 -0
- package/guide/METHOD.md +3 -3
- package/guide/agents-snippet.md +8 -8
- package/guide/before-you-build.md +4 -4
- package/guide/docs-readme.md +11 -6
- package/guide/skill/SKILL.md +11 -11
- package/package.json +3 -3
- package/src/commands/check.mjs +7 -6
- package/src/commands/done.mjs +1 -1
- package/src/commands/guide.mjs +6 -2
- package/src/commands/init.mjs +16 -10
- package/src/commands/new.mjs +9 -3
- package/src/commands/status.mjs +2 -2
- package/src/config.mjs +19 -2
- package/src/render.mjs +1 -1
package/README.md
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
</p>
|
|
11
11
|
|
|
12
12
|
<p align="center">
|
|
13
|
+
<a href="https://www.npmjs.com/package/honestdocs"><img alt="npm version" src="https://img.shields.io/npm/v/honestdocs?label=npm&color=1f2328"></a>
|
|
13
14
|
<a href="LICENSE"><img alt="MIT license" src="https://img.shields.io/badge/license-MIT-16a34a"></a>
|
|
14
15
|
<img alt="Node 20+ or Bun" src="https://img.shields.io/badge/node-%E2%89%A520%20%C2%B7%20bun-1f2328">
|
|
15
16
|
<img alt="Zero dependencies" src="https://img.shields.io/badge/dependencies-0-1f2328">
|
|
@@ -287,6 +288,7 @@ Markdown, so your own documents are never overwritten.
|
|
|
287
288
|
"rules": "Standing rules every change must keep"
|
|
288
289
|
},
|
|
289
290
|
"trailer": "Doc", // the commit trailer key
|
|
291
|
+
"command": "npx honestdocs", // how this project runs it (see below)
|
|
290
292
|
"codeRepos": ["."], // where trailer commits are read from
|
|
291
293
|
"templates": ".honestdocs/_templates", // optional: your own <type>.md templates
|
|
292
294
|
"types": { // optional: add or remove types
|
|
@@ -298,6 +300,12 @@ Markdown, so your own documents are never overwritten.
|
|
|
298
300
|
}
|
|
299
301
|
```
|
|
300
302
|
|
|
303
|
+
`command` is what honestdocs writes into `AGENTS.md`, the skill and its own messages
|
|
304
|
+
whenever it tells someone to run it. `init` records the runner that started it —
|
|
305
|
+
`bunx honestdocs` under Bun, `pnpm dlx honestdocs` under pnpm, otherwise `npx honestdocs` —
|
|
306
|
+
and never a bare `honestdocs`, which exists only where someone installed it globally.
|
|
307
|
+
Change it, then `init --agents` (and `--skill`) to rewrite those sections.
|
|
308
|
+
|
|
301
309
|
Templates may use `{{title}}`, `{{id}}` and `{{date}}`. Write the bodies in any language;
|
|
302
310
|
filenames and frontmatter stay plain ASCII so every tool can read them.
|
|
303
311
|
|
package/guide/METHOD.md
CHANGED
|
@@ -18,7 +18,7 @@ what keep it right. The `honestdocs` command enforces the ones a machine can che
|
|
|
18
18
|
- **D2 A field someone has to remember to update will be wrong.** So people write four
|
|
19
19
|
fields — `title`, `status`, `supersedes`, `related` — and everything else is derived:
|
|
20
20
|
`progress` from the checklist, `files` from git, `superseded-by` from the newer
|
|
21
|
-
document, `updated` from git. `
|
|
21
|
+
document, `updated` from git. `{{command}} index` derives; nobody types those.
|
|
22
22
|
|
|
23
23
|
- **D3 Write it down before building.** Non-trivial work starts with a document: a design
|
|
24
24
|
for "how should this work", a plan for "what are the steps", research for "what is
|
|
@@ -32,7 +32,7 @@ what keep it right. The `honestdocs` command enforces the ones a machine can che
|
|
|
32
32
|
- **D5 Write down what was rejected.** Every template has *Alternatives considered*. The
|
|
33
33
|
option that lost, and why, is what stops the same argument three months later — and
|
|
34
34
|
stops an AI agent from proposing it again as if it were new. Before proposing a design,
|
|
35
|
-
search for it: `
|
|
35
|
+
search for it: `{{command}} search <words> --section alternatives`.
|
|
36
36
|
|
|
37
37
|
- **D6 One document replaces another from the newer side.** Write
|
|
38
38
|
`supersedes: [<old id>]` in the new document; the old one is marked automatically.
|
|
@@ -48,7 +48,7 @@ what keep it right. The `honestdocs` command enforces the ones a machine can che
|
|
|
48
48
|
truths.
|
|
49
49
|
|
|
50
50
|
- **D9 A rule that can be a test is one.** Rules are numbered (`R1`, `R2`, …) so they can
|
|
51
|
-
be cited in a review. A rule names the test that proves it, and `
|
|
51
|
+
be cited in a review. A rule names the test that proves it, and `{{command}} check`
|
|
52
52
|
fails when that test file is gone. A rule that lives only in prose is the one that gets
|
|
53
53
|
forgotten.
|
|
54
54
|
|
package/guide/agents-snippet.md
CHANGED
|
@@ -4,19 +4,19 @@ This project keeps its decisions, plans, research, postmortems and standing rule
|
|
|
4
4
|
`{{root}}/`, managed by `honestdocs`. The code is the truth: where a document and the code
|
|
5
5
|
disagree, believe the code and correct the document's status.
|
|
6
6
|
|
|
7
|
-
- **Starting a session:** run `
|
|
7
|
+
- **Starting a session:** run `{{command}} brief` — what is in progress, its next unticked
|
|
8
8
|
items, what changed lately, which rules are in force.
|
|
9
|
-
- **Before building anything non-trivial:** run `
|
|
9
|
+
- **Before building anything non-trivial:** run `{{command}} search <words>` (add
|
|
10
10
|
`--section alternatives` to see what was already rejected, and why), read the rules a
|
|
11
|
-
change touches (`
|
|
12
|
-
`
|
|
11
|
+
change touches (`{{command}} rules`), then open a document:
|
|
12
|
+
`{{command}} new <area> <slug> --type design|plan|research --title "…"`.
|
|
13
13
|
- **While building:** put `{{trailer}}: <id>` on its own line at the end of every commit
|
|
14
14
|
message that does the work. Tick the document's checklist as items land; add dated
|
|
15
15
|
notes to its Log — they are the hand-over to the next session.
|
|
16
|
-
- **Never type** `progress`, `files`, `superseded-by` or `updated` — `
|
|
16
|
+
- **Never type** `progress`, `files`, `superseded-by` or `updated` — `{{command}} index`
|
|
17
17
|
derives them. To replace a document, write `supersedes: [<old id>]` in the new one.
|
|
18
|
-
- **Finishing:** `
|
|
19
|
-
then `
|
|
18
|
+
- **Finishing:** `{{command}} done <id>` (or `--abandoned`, with the reason in the Log),
|
|
19
|
+
then `{{command}} check`. A change is not finished while a document or rule it affects
|
|
20
20
|
still describes the old behaviour.
|
|
21
21
|
- Ids are filenames and never change. Add `--json` to any query command for
|
|
22
|
-
machine-readable output. The full method: `
|
|
22
|
+
machine-readable output. The full method: `{{command}} guide`.
|
|
@@ -18,7 +18,7 @@ is finished.
|
|
|
18
18
|
- **B2** A change is not finished while a document or rule it affects still describes the
|
|
19
19
|
old behaviour.
|
|
20
20
|
- **B3** A rule that can be checked by a test names that test on a `Test:` line, so
|
|
21
|
-
`
|
|
21
|
+
`{{command}} check` notices when it disappears.
|
|
22
22
|
|
|
23
23
|
## The checklist
|
|
24
24
|
|
|
@@ -27,18 +27,18 @@ public API, a setting — each pointing at the rule document that governs it. St
|
|
|
27
27
|
add a line the first time something is forgotten._
|
|
28
28
|
|
|
29
29
|
**Any change**
|
|
30
|
-
- ☐ Searched for earlier decisions: `
|
|
30
|
+
- ☐ Searched for earlier decisions: `{{command}} search <words> --section alternatives`.
|
|
31
31
|
- ☐ The documents and rules it affects are updated in the same batch (B2).
|
|
32
32
|
|
|
33
33
|
**Before saying it works**
|
|
34
34
|
- ☐ The project's tests pass.
|
|
35
|
-
- ☐ `
|
|
35
|
+
- ☐ `{{command}} index && {{command}} check` passes.
|
|
36
36
|
|
|
37
37
|
## Keeping this folder right
|
|
38
38
|
|
|
39
39
|
- A rule changes ⇒ edit its document here, in place, and add a line to its Log.
|
|
40
40
|
- A new cross-cutting concern ⇒ a new rule document
|
|
41
|
-
(`
|
|
41
|
+
(`{{command}} new rules <slug> --type rule --status current --title "…"`), its rules
|
|
42
42
|
numbered with a fresh letter, and its lines added to the checklist above.
|
|
43
43
|
|
|
44
44
|
## Log
|
package/guide/docs-readme.md
CHANGED
|
@@ -14,11 +14,16 @@ the code disagree, believe the code — then come back and correct the document'
|
|
|
14
14
|
## Working with them
|
|
15
15
|
|
|
16
16
|
```sh
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
17
|
+
# what is under way
|
|
18
|
+
{{command}} brief
|
|
19
|
+
# start a document
|
|
20
|
+
{{command}} new <area> <slug> --type plan --title "…"
|
|
21
|
+
# link the work to it
|
|
22
|
+
git commit -m "…" -m "{{trailer}}: <id>"
|
|
23
|
+
# close it
|
|
24
|
+
{{command}} done <id>
|
|
25
|
+
# derive, regenerate, validate
|
|
26
|
+
{{command}} index && {{command}} check
|
|
22
27
|
```
|
|
23
28
|
|
|
24
29
|
Write by hand only `title`, `status`, `supersedes` and `related`. The rest is derived —
|
|
@@ -26,4 +31,4 @@ Write by hand only `title`, `status`, `supersedes` and `related`. The rest is de
|
|
|
26
31
|
`superseded-by` from the newer document, `updated` from git — because a field someone has
|
|
27
32
|
to remember to update will be wrong.
|
|
28
33
|
|
|
29
|
-
The method in full: `
|
|
34
|
+
The method in full: `{{command}} guide`.
|
package/guide/skill/SKILL.md
CHANGED
|
@@ -12,7 +12,7 @@ and the code disagree, believe the code and correct the document.
|
|
|
12
12
|
## When a session starts
|
|
13
13
|
|
|
14
14
|
```sh
|
|
15
|
-
|
|
15
|
+
{{command}} brief
|
|
16
16
|
```
|
|
17
17
|
|
|
18
18
|
Work in progress with its next unticked items, what is planned, what changed lately,
|
|
@@ -21,16 +21,16 @@ the hand-over from the last session.
|
|
|
21
21
|
|
|
22
22
|
## Before building
|
|
23
23
|
|
|
24
|
-
1. **Has this been weighed before?** `
|
|
24
|
+
1. **Has this been weighed before?** `{{command}} search <words>`; with
|
|
25
25
|
`--section alternatives` it shows only what was considered and rejected, with the
|
|
26
26
|
reason. Superseded and abandoned documents are included on purpose. Do not propose a
|
|
27
27
|
rejected option again without saying what has changed.
|
|
28
|
-
2. **Which rules apply?** `
|
|
28
|
+
2. **Which rules apply?** `{{command}} rules` lists the numbered rules in force and the
|
|
29
29
|
tests that prove them. Go through the `before-you-build` checklist in `{{root}}/rules/`.
|
|
30
30
|
3. **Open a document** for anything non-trivial:
|
|
31
31
|
|
|
32
32
|
```sh
|
|
33
|
-
|
|
33
|
+
{{command}} new <area> <slug> --type design --title "Token rotation"
|
|
34
34
|
```
|
|
35
35
|
|
|
36
36
|
Types: `design` (how it should work), `plan` (ordered steps, checklist), `research`
|
|
@@ -46,26 +46,26 @@ the hand-over from the last session.
|
|
|
46
46
|
{{trailer}}: 2026-11-02-token-rotation
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
-
That is how `
|
|
49
|
+
That is how `{{command}} index` learns which files the document touched. Several ids:
|
|
50
50
|
comma-separated or one line each.
|
|
51
51
|
- Tick checklist items (`- [x]`) as they land. Add dated notes to the Log.
|
|
52
52
|
- Never write `progress`, `files`, `superseded-by` or `updated` by hand. Write only
|
|
53
53
|
`title`, `status`, `supersedes`, `related`.
|
|
54
54
|
- Replacing a document: `supersedes: [<old id>]` in the **new** one, then
|
|
55
|
-
`
|
|
55
|
+
`{{command}} index` marks the old one.
|
|
56
56
|
- A rule changed: edit its rule document in place and add a Log line — never a second
|
|
57
57
|
document beside it.
|
|
58
58
|
|
|
59
59
|
## Finishing
|
|
60
60
|
|
|
61
61
|
```sh
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
62
|
+
{{command}} done <id> # refuses while checklist items are unticked
|
|
63
|
+
{{command}} done <id> --abandoned # dropped on purpose; the reason goes in the Log
|
|
64
|
+
{{command}} status <id> current # living documents: draft | current | retired
|
|
65
|
+
{{command}} index && {{command}} check
|
|
66
66
|
```
|
|
67
67
|
|
|
68
68
|
`check` exits 1 on anything that makes the set lie. A change is not finished while a
|
|
69
69
|
document or rule it affects still describes the old behaviour.
|
|
70
70
|
|
|
71
|
-
Every query command takes `--json`. The method in full: `
|
|
71
|
+
Every query command takes `--json`. The method in full: `{{command}} guide`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "honestdocs",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Development documents that stay true — decisions, designs, plans, research, postmortems and standing rules, kept honest for people and AI agents.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"documentation",
|
|
@@ -36,8 +36,8 @@
|
|
|
36
36
|
},
|
|
37
37
|
"scripts": {
|
|
38
38
|
"test": "node --test",
|
|
39
|
-
"
|
|
40
|
-
"
|
|
39
|
+
"prepublishOnly": "node scripts/npm-readme.mjs pack",
|
|
40
|
+
"postpublish": "node scripts/npm-readme.mjs restore",
|
|
41
41
|
"assets": "node scripts/build-assets.mjs",
|
|
42
42
|
"docs:index": "node bin/honestdocs.mjs index",
|
|
43
43
|
"docs:check": "node bin/honestdocs.mjs check"
|
package/src/commands/check.mjs
CHANGED
|
@@ -37,6 +37,7 @@ function runCheck(config, { fix = false } = {}) {
|
|
|
37
37
|
const fixed = [];
|
|
38
38
|
const err = (d, msg) => errors.push({ file: d.rel, msg });
|
|
39
39
|
const warnAt = (d, msg) => warnings.push({ file: d.rel, msg });
|
|
40
|
+
const cmd = config.command;
|
|
40
41
|
|
|
41
42
|
const ids = new Map();
|
|
42
43
|
for (const d of docs) {
|
|
@@ -47,7 +48,7 @@ function runCheck(config, { fix = false } = {}) {
|
|
|
47
48
|
for (const d of docs) {
|
|
48
49
|
const m = d.meta;
|
|
49
50
|
if (!d.hasMeta) {
|
|
50
|
-
err(d,
|
|
51
|
+
err(d, `no frontmatter — start documents with \`${cmd} new\`, or add the block by hand`);
|
|
51
52
|
continue;
|
|
52
53
|
}
|
|
53
54
|
if (!m.id) err(d, "missing `id`");
|
|
@@ -91,7 +92,7 @@ function runCheck(config, { fix = false } = {}) {
|
|
|
91
92
|
warnAt(d, "superseded, but nothing says by what — set `supersedes` on the newer document");
|
|
92
93
|
}
|
|
93
94
|
if ((m.status === "done" || m.status === "abandoned") && !m.completed && m.legacy !== true) {
|
|
94
|
-
warnAt(d, `${m.status} but \`completed\` is empty — close documents with
|
|
95
|
+
warnAt(d, `${m.status} but \`completed\` is empty — close documents with \`${cmd} done\``);
|
|
95
96
|
}
|
|
96
97
|
if (m.status === "done") {
|
|
97
98
|
const p = checklistProgress(d.body);
|
|
@@ -127,8 +128,8 @@ function runCheck(config, { fix = false } = {}) {
|
|
|
127
128
|
for (const ch of derive(config, docs.filter((d) => d.hasMeta && d.meta.id === d.id), facts)) {
|
|
128
129
|
for (const k of new Set([...Object.keys(ch.before), ...Object.keys(ch.meta)])) {
|
|
129
130
|
if (JSON.stringify(ch.before[k]) === JSON.stringify(ch.meta[k])) continue;
|
|
130
|
-
const msg = `\`${k}\` is ${JSON.stringify(ch.before[k] ?? "")}, should be ${JSON.stringify(ch.meta[k])} — run
|
|
131
|
-
if (k === "status") err(ch.doc, `replaced by ${ch.meta["superseded-by"]} but still says "${ch.before.status}" — run
|
|
131
|
+
const msg = `\`${k}\` is ${JSON.stringify(ch.before[k] ?? "")}, should be ${JSON.stringify(ch.meta[k])} — run \`${cmd} index\``;
|
|
132
|
+
if (k === "status") err(ch.doc, `replaced by ${ch.meta["superseded-by"]} but still says "${ch.before.status}" — run \`${cmd} index\``);
|
|
132
133
|
else if (k === "progress" || k === "superseded-by") err(ch.doc, msg);
|
|
133
134
|
else warnAt(ch.doc, msg);
|
|
134
135
|
}
|
|
@@ -138,9 +139,9 @@ function runCheck(config, { fix = false } = {}) {
|
|
|
138
139
|
// so a commit can never make it stale on its own.
|
|
139
140
|
const indexFile = join(config.docsDir, "INDEX.md");
|
|
140
141
|
const expected = renderIndex(config, docs.filter((d) => d.hasMeta));
|
|
141
|
-
if (!existsSync(indexFile)) errors.push({ file: "INDEX.md", msg:
|
|
142
|
+
if (!existsSync(indexFile)) errors.push({ file: "INDEX.md", msg: `missing — run \`${cmd} index\`` });
|
|
142
143
|
else if (readFileSync(indexFile, "utf8") !== expected) {
|
|
143
|
-
errors.push({ file: "INDEX.md", msg:
|
|
144
|
+
errors.push({ file: "INDEX.md", msg: `stale: frontmatter changed since it was generated — run \`${cmd} index\`` });
|
|
144
145
|
}
|
|
145
146
|
|
|
146
147
|
return { documents: docs.length, git: facts.available ? "ok" : facts.reason, errors, warnings, fixed };
|
package/src/commands/done.mjs
CHANGED
|
@@ -29,7 +29,7 @@ export function run(argv) {
|
|
|
29
29
|
|
|
30
30
|
if (lifecycleOf(config, doc.meta.type) !== "work") {
|
|
31
31
|
die(`${id} is a ${doc.meta.type} — a living document is never finished. ` +
|
|
32
|
-
|
|
32
|
+
`Keep it current, or retire it: \`${config.command} status ${id} retired\``);
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
const status = args.abandoned ? "abandoned" : "done";
|
package/src/commands/guide.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { UsageError, parseArgs } from "../cli.mjs";
|
|
2
|
-
import { DEFAULT_ROOT, findConfig, loadConfig } from "../config.mjs";
|
|
2
|
+
import { DEFAULT_ROOT, findConfig, loadConfig, runnerCommand } from "../config.mjs";
|
|
3
3
|
import { fill, packageFile } from "../templates.mjs";
|
|
4
4
|
|
|
5
5
|
export const summary = "print the method, the agent instructions or the skill";
|
|
@@ -24,6 +24,10 @@ export function run(argv) {
|
|
|
24
24
|
const topic = args._[0] ?? "method";
|
|
25
25
|
if (!TOPICS[topic]) throw new UsageError(`unknown topic "${topic}" — expected: ${Object.keys(TOPICS).join(", ")}`);
|
|
26
26
|
const config = findConfig() ? loadConfig() : null;
|
|
27
|
-
const values = {
|
|
27
|
+
const values = {
|
|
28
|
+
root: config?.root ?? DEFAULT_ROOT,
|
|
29
|
+
trailer: config?.trailer ?? "Doc",
|
|
30
|
+
command: config?.command ?? runnerCommand(),
|
|
31
|
+
};
|
|
28
32
|
console.log(fill(packageFile("guide", ...TOPICS[topic]), values).trimEnd());
|
|
29
33
|
}
|
package/src/commands/init.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { dirname, join, relative, resolve } from "node:path";
|
|
3
3
|
import { c, die, ok, parseArgs, say } from "../cli.mjs";
|
|
4
|
-
import { CONFIG_FILE, CONFIG_IN_ROOT, DEFAULT_AREAS, DEFAULT_ROOT, findConfig, loadConfig } from "../config.mjs";
|
|
4
|
+
import { CONFIG_FILE, CONFIG_IN_ROOT, DEFAULT_AREAS, DEFAULT_ROOT, findConfig, loadConfig, runnerCommand } from "../config.mjs";
|
|
5
5
|
import { today } from "../docs.mjs";
|
|
6
6
|
import { stringify } from "../frontmatter.mjs";
|
|
7
7
|
import { fill, packageFile } from "../templates.mjs";
|
|
@@ -68,8 +68,12 @@ function upsertBlock(file, block) {
|
|
|
68
68
|
return "appended";
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
|
|
72
|
-
|
|
71
|
+
/** What the guide texts are filled with: where the documents live, and how to run the tool. */
|
|
72
|
+
const guideValues = (config) => ({ root: config.root, trailer: config.trailer, command: config.command });
|
|
73
|
+
|
|
74
|
+
function installAgentBits(config, args) {
|
|
75
|
+
const base = config.base;
|
|
76
|
+
const values = guideValues(config);
|
|
73
77
|
if (args.agents || args["agents-file"]) {
|
|
74
78
|
const file = resolve(base, args["agents-file"] ?? "AGENTS.md");
|
|
75
79
|
const how = upsertBlock(file, fill(packageFile("guide", "agents-snippet.md"), values));
|
|
@@ -96,7 +100,7 @@ export function run(argv) {
|
|
|
96
100
|
if (!args.agents && !args["agents-file"] && !args.skill) {
|
|
97
101
|
die(`already set up: ${existing.file} — re-run with --agents or --skill to refresh those`);
|
|
98
102
|
}
|
|
99
|
-
installAgentBits(config
|
|
103
|
+
installAgentBits(config, args);
|
|
100
104
|
return;
|
|
101
105
|
}
|
|
102
106
|
|
|
@@ -117,6 +121,7 @@ export function run(argv) {
|
|
|
117
121
|
title: args.title ?? "Development documents",
|
|
118
122
|
areas,
|
|
119
123
|
trailer: "Doc",
|
|
124
|
+
command: runnerCommand(),
|
|
120
125
|
codeRepos: args["code-repo"].length ? args["code-repo"] : ["."],
|
|
121
126
|
};
|
|
122
127
|
const configPath = root === DEFAULT_ROOT ? CONFIG_IN_ROOT : CONFIG_FILE;
|
|
@@ -129,7 +134,7 @@ export function run(argv) {
|
|
|
129
134
|
|
|
130
135
|
const readme = join(config.docsDir, "README.md");
|
|
131
136
|
if (!existsSync(readme)) {
|
|
132
|
-
writeFileSync(readme, fill(packageFile("guide", "docs-readme.md"),
|
|
137
|
+
writeFileSync(readme, fill(packageFile("guide", "docs-readme.md"), guideValues(config)));
|
|
133
138
|
ok(`${root}/README.md`);
|
|
134
139
|
}
|
|
135
140
|
|
|
@@ -150,17 +155,18 @@ export function run(argv) {
|
|
|
150
155
|
related: [],
|
|
151
156
|
files: [],
|
|
152
157
|
};
|
|
153
|
-
writeFileSync(rulesFile, stringify(meta) + fill(packageFile("guide", "before-you-build.md"),
|
|
158
|
+
writeFileSync(rulesFile, stringify(meta) + fill(packageFile("guide", "before-you-build.md"), guideValues(config)));
|
|
154
159
|
ok(`${root}/rules/${id}.md`);
|
|
155
160
|
|
|
156
|
-
installAgentBits(config
|
|
161
|
+
installAgentBits(config, args);
|
|
157
162
|
report(config, runIndex(config), false);
|
|
158
163
|
|
|
159
164
|
say("");
|
|
160
165
|
const area = Object.keys(config.areas).find((a) => a !== "rules") ?? "rules";
|
|
166
|
+
const cmd = config.command;
|
|
161
167
|
say("Next:");
|
|
162
|
-
say(c.dim(` start a document
|
|
168
|
+
say(c.dim(` start a document ${cmd} new ${area} my-first-design --title "…"`));
|
|
163
169
|
say(c.dim(` link work to it git commit -m "…" -m "${config.trailer}: <id>"`));
|
|
164
|
-
say(c.dim(
|
|
165
|
-
if (!args.agents && !args["agents-file"]) say(c.dim(
|
|
170
|
+
say(c.dim(` keep it honest ${cmd} index && ${cmd} check`));
|
|
171
|
+
if (!args.agents && !args["agents-file"]) say(c.dim(` teach your agents ${cmd} init --agents`));
|
|
166
172
|
}
|
package/src/commands/new.mjs
CHANGED
|
@@ -2,8 +2,9 @@ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
|
2
2
|
import { join, relative } from "node:path";
|
|
3
3
|
import { UsageError, c, die, emitJson, ok, parseArgs, say } from "../cli.mjs";
|
|
4
4
|
import { loadConfig, statusesFor } from "../config.mjs";
|
|
5
|
-
import { loadDocs, slugify, today } from "../docs.mjs";
|
|
5
|
+
import { checklistProgress, loadDocs, slugify, today } from "../docs.mjs";
|
|
6
6
|
import { stringify } from "../frontmatter.mjs";
|
|
7
|
+
import { renderIndex } from "../render.mjs";
|
|
7
8
|
import { fill, templateFor } from "../templates.mjs";
|
|
8
9
|
|
|
9
10
|
export const summary = "start a document";
|
|
@@ -70,7 +71,9 @@ export function run(argv) {
|
|
|
70
71
|
related: args.related,
|
|
71
72
|
files: [],
|
|
72
73
|
};
|
|
73
|
-
const
|
|
74
|
+
const body = fill(templateFor(config, type), { title: args.title, id, date: today() });
|
|
75
|
+
meta.progress = checklistProgress(body);
|
|
76
|
+
const content = stringify(meta) + body;
|
|
74
77
|
const file = join(config.docsDir, area, `${id}.md`);
|
|
75
78
|
const shown = relative(config.base, file).split("\\").join("/");
|
|
76
79
|
|
|
@@ -82,10 +85,13 @@ export function run(argv) {
|
|
|
82
85
|
if (existsSync(file)) die(`${shown} already exists`);
|
|
83
86
|
mkdirSync(join(config.docsDir, area), { recursive: true });
|
|
84
87
|
writeFileSync(file, content);
|
|
88
|
+
// A new document has no commits yet, so nothing git knows can change: rendering the
|
|
89
|
+
// index is enough for `check` to pass, without the git pass a full `index` makes.
|
|
90
|
+
writeFileSync(join(config.docsDir, "INDEX.md"), renderIndex(config, loadDocs(config).filter((d) => d.hasMeta)));
|
|
85
91
|
|
|
86
92
|
if (args.json) return emitJson({ id, path: shown, type, status });
|
|
87
93
|
ok(c.dim(`${type} · ${area} · ${status}`));
|
|
88
94
|
say(c.dim(` Put "${config.trailer}: ${id}" in the commits that do this work —`));
|
|
89
|
-
say(c.dim(
|
|
95
|
+
say(c.dim(` that is how \`${config.command} index\` learns which files it touched.`));
|
|
90
96
|
console.log(shown);
|
|
91
97
|
}
|
package/src/commands/status.mjs
CHANGED
|
@@ -28,9 +28,9 @@ export function run(argv) {
|
|
|
28
28
|
const allowed = statusesFor(config, doc.meta.type);
|
|
29
29
|
if (!allowed) die(`${doc.rel} has an unknown type "${doc.meta.type}"`);
|
|
30
30
|
if (status === "superseded") {
|
|
31
|
-
die(
|
|
31
|
+
die(`set \`supersedes: [${id}]\` on the document that replaces it, then run \`${config.command} index\``);
|
|
32
32
|
}
|
|
33
|
-
if (status === "done" || status === "abandoned") die(`use
|
|
33
|
+
if (status === "done" || status === "abandoned") die(`use \`${config.command} done ${id}${status === "abandoned" ? " --abandoned" : ""}\` — it checks the checklist first`);
|
|
34
34
|
if (!allowed[status]) {
|
|
35
35
|
die(`"${status}" is not a status for a ${doc.meta.type} (${lifecycleOf(config, doc.meta.type)}) — expected one of: ${Object.keys(allowed).filter((s) => s !== "superseded").join(", ")}`);
|
|
36
36
|
}
|
package/src/config.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Finding and reading honestdocs.config.json.
|
|
1
|
+
// Finding and reading the config: .honestdocs/config.json, or honestdocs.config.json.
|
|
2
2
|
//
|
|
3
3
|
// The config is found by walking up from the working directory, the way git finds .git,
|
|
4
4
|
// so every command works from anywhere inside the project. Everything a project may want
|
|
@@ -52,6 +52,22 @@ const DEFAULT_TYPES = {
|
|
|
52
52
|
rule: { lifecycle: "living", description: "Numbered rules every change must keep, each naming its test where it can" },
|
|
53
53
|
};
|
|
54
54
|
|
|
55
|
+
/**
|
|
56
|
+
* How a project runs honestdocs, for every command the tool prints or writes into
|
|
57
|
+
* AGENTS.md. A bare `honestdocs` exists only after a global install, on one machine; a
|
|
58
|
+
* project set up with `npx honestdocs init` has no such command, and an agent told to run
|
|
59
|
+
* `honestdocs brief` fails on its first step. So `init` records the package runner that
|
|
60
|
+
* started it — never the bare name — and a config without one means npx.
|
|
61
|
+
*/
|
|
62
|
+
export const DEFAULT_COMMAND = "npx honestdocs";
|
|
63
|
+
|
|
64
|
+
export function runnerCommand(env = process.env) {
|
|
65
|
+
const agent = env.npm_config_user_agent ?? "";
|
|
66
|
+
if (agent.startsWith("bun/")) return "bunx honestdocs";
|
|
67
|
+
if (agent.startsWith("pnpm/")) return "pnpm dlx honestdocs";
|
|
68
|
+
return DEFAULT_COMMAND;
|
|
69
|
+
}
|
|
70
|
+
|
|
55
71
|
export const DEFAULT_AREAS = {
|
|
56
72
|
general: "Anything that does not belong to a narrower area yet",
|
|
57
73
|
rules: "Standing rules every change must keep — read before building, updated in place",
|
|
@@ -82,7 +98,7 @@ export function findConfig(start = process.cwd()) {
|
|
|
82
98
|
*/
|
|
83
99
|
export function loadConfig(start = process.cwd()) {
|
|
84
100
|
const found = findConfig(start);
|
|
85
|
-
if (!found) throw new Failure(`no ${CONFIG_IN_ROOT} here or above — run
|
|
101
|
+
if (!found) throw new Failure(`no ${CONFIG_IN_ROOT} here or above — run \`${runnerCommand()} init\` first`);
|
|
86
102
|
let raw;
|
|
87
103
|
try {
|
|
88
104
|
raw = JSON.parse(readFileSync(found.file, "utf8"));
|
|
@@ -133,6 +149,7 @@ function normalise(raw, base, file = join(base, CONFIG_FILE)) {
|
|
|
133
149
|
areas,
|
|
134
150
|
types,
|
|
135
151
|
trailer,
|
|
152
|
+
command: typeof raw.command === "string" && raw.command.trim() ? raw.command.trim() : DEFAULT_COMMAND,
|
|
136
153
|
codeRepos,
|
|
137
154
|
templatesDir: raw.templates ? resolve(base, raw.templates) : null,
|
|
138
155
|
recent: Number.isInteger(raw.recent) && raw.recent > 0 ? raw.recent : 25,
|
package/src/render.mjs
CHANGED
|
@@ -33,7 +33,7 @@ export function renderIndex(config, docs) {
|
|
|
33
33
|
const lines = [
|
|
34
34
|
`# ${config.title}`,
|
|
35
35
|
"",
|
|
36
|
-
|
|
36
|
+
`> Generated by \`${config.command} index\`. Do not edit — change the document's own frontmatter`,
|
|
37
37
|
"> and run it again.",
|
|
38
38
|
">",
|
|
39
39
|
"> **The code is the truth.** A document records what was decided at the time. Where the",
|