@g3nomaly/g3n-skills 0.1.0 → 0.1.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.
Files changed (43) hide show
  1. package/.claude-plugin/marketplace.json +3 -2
  2. package/README.md +96 -0
  3. package/package.json +1 -1
  4. package/skills/coding/.claude-plugin/plugin.json +2 -2
  5. package/skills/coding/analyze/SKILL.md +32 -0
  6. package/skills/coding/analyze/agents/openai.yaml +5 -0
  7. package/skills/coding/analyze/references/api.md +27 -0
  8. package/skills/coding/analyze/references/commands.md +54 -0
  9. package/skills/coding/analyze/references/repository.md +13 -0
  10. package/skills/coding/document/SKILL.md +26 -9
  11. package/skills/coding/document/references/typescript.md +44 -3
  12. package/skills/task-manager/decisions/src/paths.ts +13 -1
  13. package/skills/task-manager/decisions/src/read.ts +20 -6
  14. package/skills/task-manager/decisions/src/write.ts +28 -5
  15. package/skills/task-manager/task-manager/hooks/cleanup.ts +6 -0
  16. package/skills/task-manager/task-manager/hooks/guard-tree.ts +31 -4
  17. package/skills/task-manager/task-manager/hooks/sync-tree.ts +1 -1
  18. package/skills/task-manager/task-manager/src/filetree/ensure.ts +15 -3
  19. package/skills/task-manager/task-manager/src/filetree/entries.ts +26 -5
  20. package/skills/task-manager/task-manager/src/filetree/frontmatter.ts +23 -7
  21. package/skills/task-manager/task-manager/src/filetree/generate.ts +31 -6
  22. package/skills/task-manager/task-manager/src/filetree/levels.ts +1 -0
  23. package/skills/task-manager/task-manager/src/filetree/locate.ts +45 -13
  24. package/skills/task-manager/task-manager/src/filetree/paths.ts +30 -6
  25. package/skills/task-manager/task-manager/src/filetree/read.ts +15 -7
  26. package/skills/task-manager/task-manager/src/filetree/remove.ts +7 -4
  27. package/skills/task-manager/task-manager/src/filetree/root.ts +4 -3
  28. package/skills/task-manager/task-manager/src/filetree/status.ts +29 -7
  29. package/skills/task-manager/task-manager/src/hooks/cli.ts +50 -12
  30. package/skills/task-manager/task-manager/src/schemas/config.ts +1 -0
  31. package/skills/task-manager/task-manager/src/schemas/decision.ts +8 -2
  32. package/skills/task-manager/task-manager/src/schemas/phase.ts +6 -0
  33. package/skills/task-manager/task-manager/src/schemas/plan.ts +6 -0
  34. package/skills/task-manager/task-manager/src/schemas/primitives.ts +3 -0
  35. package/skills/task-manager/task-manager/src/schemas/task.ts +7 -0
  36. package/skills/task-manager/task-manager/src/schemas/term.ts +8 -1
  37. package/skills/task-manager/task-manager/src/schemas/validation.ts +31 -7
  38. package/skills/task-manager/task-manager/src/vocabulary/file.ts +37 -4
  39. package/skills/task-manager/task-manager/src/vocabulary/match.ts +29 -7
  40. package/skills/workflow/run/hooks/advance-step.ts +14 -2
  41. package/skills/workflow/run/src/gates.ts +21 -8
  42. package/skills/workflow/run/src/phase.ts +17 -3
  43. package/skills/workflow/run/src/steps.ts +10 -4
@@ -16,10 +16,11 @@
16
16
  {
17
17
  "name": "coding",
18
18
  "source": "./skills/coding",
19
- "description": "How to write, check and document code, per language.",
19
+ "description": "How to write, check and document code, per language, and query a repository with analyze.",
20
20
  "skills": [
21
21
  "./code",
22
- "./document"
22
+ "./document",
23
+ "./analyze"
23
24
  ]
24
25
  },
25
26
  {
package/README.md ADDED
@@ -0,0 +1,96 @@
1
+ # @g3nomaly/g3n-skills
2
+
3
+ The g3n skills for Claude Code and Codex: 28 skills in 8 groups. This package holds the skills and has no CLI. Install them with [`@g3nomaly/skills`](https://www.npmjs.com/package/@g3nomaly/skills), which reads this package as its built-in source, or read it as a Claude Code plugin marketplace.
4
+
5
+ Some skills carry their own hooks. A hook is a TypeScript file in the skill's `hooks/` folder, and the agent runs it with `bun`. The task-manager hooks keep plans, phases and tasks as Markdown files that the agent reaches only through those hooks. The workflow hooks decide when a step of a phase is finished.
6
+
7
+ ## Requirements
8
+
9
+ - [Bun](https://bun.sh), to run the hooks
10
+ - A git repository to install into, because `@g3nomaly/skills` anchors a project at its repository root
11
+ - [`@g3nomaly/analyze`](https://www.npmjs.com/package/@g3nomaly/analyze) for the `analyze` skill, which runs `bunx @g3nomaly/analyze`
12
+
13
+ ## Install with @g3nomaly/skills
14
+
15
+ Run these from the root of your project:
16
+
17
+ ```sh
18
+ bunx @g3nomaly/skills init # pick Claude Code, Codex or both
19
+ bunx @g3nomaly/skills ls --source g3n-skills # lists the skills this package offers
20
+ bunx @g3nomaly/skills add workflow # installs a group, with every skill it depends on
21
+ bunx @g3nomaly/skills add unslop # installs one skill
22
+ ```
23
+
24
+ You don't register this package. `g3n-skills` is built into the installer and cannot be removed.
25
+
26
+ Each skill is copied to `.claude/skills/<skill>/`, or `.agents/skills/<skill>/` for Codex, next to the skills it depends on. The installer records it in `.claude/skills.json` or `.agents/skills.json`. Commit the skills and that file if your team should get the same skills.
27
+
28
+ A name that is both a group and a skill, such as `grilling` or `task-manager`, installs the whole group. `add` and `update` also run `bun install` in each skill that ships a `package.json`. Today that is only `task-manager`, which pins zod.
29
+
30
+ ```sh
31
+ bunx @g3nomaly/skills update # every installed skill
32
+ bunx @g3nomaly/skills update --dry-run # shows what would change
33
+ bunx @g3nomaly/skills remove grill-me
34
+ bunx @g3nomaly/skills ls --installed
35
+ ```
36
+
37
+ ## As a Claude Code plugin marketplace
38
+
39
+ The package ships `.claude-plugin/marketplace.json`, a marketplace named `g3n` with one plugin per group in the table below.
40
+
41
+ Installed as a plugin, the task-manager group registers two lifecycle hooks. `sync-tree` runs at `SessionStart`, and `guard-tree` runs before `Read`, `Grep` and `Glob`. With `@g3nomaly/skills` you get neither, and the task-manager skill tells the agent to run `sync-tree` itself.
42
+
43
+ There is one known gap. Claude Code caches each plugin in its own folder, so the workflow hooks can't import the task-manager code they need. Install the workflow group with `@g3nomaly/skills` for now.
44
+
45
+ ## The skills
46
+
47
+ Skills marked _manual_ run only when you type their command, such as `/grill-me`. The agent loads the others when the task calls for them.
48
+
49
+ | Group | Skill | What it does |
50
+ |---|---|---|
51
+ | `agent` | `bootstrap` | Session start checks (read `CLAUDE.md`, compare the file tree, state the branch) and the agent's boundaries |
52
+ | `agent` | `worktrees` | Work in a git worktree on its home branch and land it with a local squash merge |
53
+ | `coding` | `code` | Rules for writing and checking code, with a toolchain reference per language |
54
+ | `coding` | `document` | Docstrings and comments for functions and classes, and where the reasons behind code go |
55
+ | `coding` | `analyze` | Query a repository's model with `g3n-analyze` and rebuild its documentation site in `analyze_out/` |
56
+ | `grilling` | `grilling` | Interview the user relentlessly to stress-test a plan, decision or idea |
57
+ | `grilling` | `grill-me` | _Manual._ Starts a `/grilling` session |
58
+ | `grilling` | `grill-with-docs` | _Manual._ A grilling session that writes ADRs and a glossary as it goes |
59
+ | `grilling` | `domain-modeling` | Pin down a project's terms and record its architectural decisions |
60
+ | `speech` | `emphasise` | What bold, italics and backticks mean in Markdown that an agent reads |
61
+ | `speech` | `unslop` | Cut AI tells from any writing |
62
+ | `task-manager` | `task-manager` | Query plans, phases and tasks and change their status through hooks |
63
+ | `task-manager` | `vocabulary` | One word per concept, stored in the task-manager tree, so names don't drift between sessions |
64
+ | `task-manager` | `decisions` | Record and look up a plan's decisions as numbered files inside the plan |
65
+ | `using-hooks` | `hook-pre-tool-use` | _Manual._ Writing a `PreToolUse` hook, which can allow, deny or escalate a tool call |
66
+ | `using-hooks` | `hook-post-tool-use` | _Manual._ Writing a `PostToolUse` hook, which reacts to a finished tool call |
67
+ | `using-hooks` | `hook-stop` | _Manual._ Writing a `Stop` hook, which can refuse to let the agent end its turn |
68
+ | `using-hooks` | `hook-user-prompt-submit` | _Manual._ Writing a `UserPromptSubmit` hook, which can add context to a prompt or reject it |
69
+ | `workflow` | `run` | Work one phase through plan, execute, review and sync, one fresh agent per step |
70
+ | `workflow` | `plan` | Turn a phase into task files an agent can execute without asking anything |
71
+ | `workflow` | `execute` | Carry out a phase's tasks, each in its own worktree, and hand each one to review |
72
+ | `workflow` | `review` | Check each in-review task against its requirements, then complete it or send it back |
73
+ | `workflow` | `sync` | Merge a reviewed phase's branches, remove its worktrees, record its decisions and terms, and close it |
74
+ | `workflow` | `orchestrator` | _Manual._ Run a whole plan phase by phase, starting each phase once the phases it depends on are done |
75
+ | `workflow` | `worktree` | Set up, merge and remove the git worktrees tasks are worked in, one per task |
76
+ | `writing` | `writing-great-hooks` | Add, change or debug a hook script or a lifecycle hook |
77
+ | `writing` | `writing-great-skills` | Write or edit a skill, its `SKILL.md`, references and hooks |
78
+ | `writing` | `writing-great-tasks` | Write a task that doesn't leave its executor guessing about scope |
79
+
80
+ The task-manager hooks store the tree in `docs/task-manager` by default. Set `skills.settings.filetreeRoot` in `.g3n.config.json` to move it.
81
+
82
+ ## Development
83
+
84
+ Run `bun install` at the repository root first, then every command from this directory.
85
+
86
+ ```sh
87
+ bun run build # regenerates .claude-plugin/marketplace.json here and at the repository root
88
+ bun run typecheck
89
+ bun test
90
+ ```
91
+
92
+ Run `bun run build` after adding a group or a skill, and commit both marketplace files.
93
+
94
+ ## License
95
+
96
+ [Elastic License 2.0](https://www.elastic.co/licensing/elastic-license) (`Elastic-2.0`).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@g3nomaly/g3n-skills",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "The g3n skills, each with its own hooks: a skill source for @g3nomaly/skills and a Claude Code plugin marketplace.",
5
5
  "type": "module",
6
6
  "files": [
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "coding",
3
- "description": "How to write, check and document code, per language.",
3
+ "description": "How to write, check and document code, per language, and query a repository with analyze.",
4
4
  "version": "0.1.0",
5
5
  "license": "UNLICENSED",
6
- "skills": ["./code", "./document"]
6
+ "skills": ["./code", "./document", "./analyze"]
7
7
  }
@@ -0,0 +1,32 @@
1
+ ---
2
+ name: analyze
3
+ description: Query and update the linked model of a repository with g3n-analyze: find code, docs, skills, decisions and tasks, see what uses or links to what, and rebuild the documentation site in analyze_out/. Use when asked where something is, what uses a type or function, what a decision affects, or to run g3n-analyze or regenerate the docs.
4
+ ---
5
+
6
+ `g3n-analyze` scans a repository into one model of **nodes** and **edges**, and answers queries on it. Run it from the repository root as `bunx @g3nomaly/analyze <command>`; in the g3n monorepo, `bun packages/analyze/dist/cli.js <command>` after `bun run build` in `packages/analyze`.
7
+
8
+ ## Query
9
+
10
+ 1. **Find** the node: `search <words>`. It matches names first, then meaning. Done when a hit's `id` names what you want.
11
+ 2. **Read** it: `show <id>`. Text, signature, parameters, fields and every edge.
12
+ 3. **Follow** its edges: `refs <id>`. Add `--incoming` for what uses it, `--outgoing` for what it uses, `--edge <kind>` to keep one kind, such as `type` or `affects`.
13
+ 4. **Narrow** any of these with `--kind`, `--section` and `-n`. `list --kind <kind>` lists every node of a kind.
14
+
15
+ Add `--json` when a script reads the output. A `<node>` may be an `id`, a file path or a name; a name that fits several nodes prints the candidates.
16
+
17
+ A _member_, a parameter, field, operation or definition, is found by search but returns its _owner_: the function, the type or the file. Its exact `id` opens the member itself.
18
+
19
+ Flags, kinds and edge kinds: [`references/commands.md`](references/commands.md).
20
+
21
+ ## Update
22
+
23
+ - **Queries rescan by themselves.** The scan is cached in `analyze_out/cache/` and redone when a file changes. `--no-cache` forces it.
24
+ - **Rebuild the site** with `create_docs`. It writes `analyze_out/index.html` and nothing else.
25
+ - **Change what is scanned** in `.analyzeignore` at the root, written with the defaults on the first run. `--exclude <glob>` skips more for one run.
26
+ - **Never edit `analyze_out/`.** It is output.
27
+
28
+ What makes a repository show more, such as glossaries, decisions and doc tags: [`references/repository.md`](references/repository.md).
29
+
30
+ ## From code
31
+
32
+ Hooks and scripts call the same queries as a typed library, without the CLI: [`references/api.md`](references/api.md).
@@ -0,0 +1,5 @@
1
+ interface:
2
+ display_name: "Analyze"
3
+ short_description: "Query and rebuild the linked model of a repository with g3n-analyze"
4
+ policy:
5
+ allow_implicit_invocation: true
@@ -0,0 +1,27 @@
1
+ # Library
2
+
3
+ `@g3nomaly/analyze` exports the queries as functions. They take the CLI options as fields, never print, and throw `OptionsError`, `QueryError` or `SourceError`.
4
+
5
+ ```ts
6
+ import { createDocs, list, refs, search, show } from '@g3nomaly/analyze';
7
+
8
+ const repo = '/home/me/app';
9
+ const { hits } = await search({ repo, query: 'route price', limit: 5 });
10
+ const node = await show({ repo, target: hits[0].id });
11
+ const users = await refs({ repo, target: 'choose_route', incoming: true, edges: ['type'] });
12
+ const tasks = await list({ repo, kinds: ['task'] });
13
+ await createDocs({ repo });
14
+ ```
15
+
16
+ | Field | Functions | CLI flag |
17
+ |---|---|---|
18
+ | `repo`, `skillDirs`, `docDirs`, `excludes`, `embeddings` | all | `--repo`, `--skills`, `--docs`, `--exclude`, `--no-embeddings` |
19
+ | `from`, `cache` | queries | `--from`, `--no-cache` |
20
+ | `kinds`, `section` | queries | `--kind`, `--section` |
21
+ | `query`, `by`, `limit` | `search` | `<query>`, `--by`, `-n` |
22
+ | `target` | `show`, `refs` | `<node>` |
23
+ | `incoming`, `outgoing`, `edges` | `refs` | `--incoming`, `--outgoing`, `--edge` |
24
+ | `title` | `createDocs` | `--title` |
25
+ | `onWarning` | all | — |
26
+
27
+ Warnings go to `onWarning` and are dropped without it.
@@ -0,0 +1,54 @@
1
+ # Commands
2
+
3
+ | Command | Does |
4
+ |---|---|
5
+ | `create_docs` | Writes `analyze_out/index.html`, one offline HTML file |
6
+ | `search <query>` | Finds nodes by words and by meaning |
7
+ | `show <node>` | Prints one node in full |
8
+ | `refs <node>` | Lists its edges, both ways |
9
+ | `list` | Lists nodes in display order |
10
+
11
+ ## Flags
12
+
13
+ | Flag | Commands | Does |
14
+ |---|---|---|
15
+ | `--repo <dir>` | all | Repository to read. Default: the working directory |
16
+ | `--skills <dir>`, `--docs <dir>` | all | Read skills or docs from this directory only. Repeatable |
17
+ | `--exclude <glob>` | all | Skip matching paths. Repeatable |
18
+ | `--no-embeddings` | all | No search by meaning |
19
+ | `--title <text>` | `create_docs` | Site title |
20
+ | `--from <file>` | queries | Query a built `index.html` instead of scanning |
21
+ | `--no-cache` | queries | Scan again even when nothing changed |
22
+ | `--json` | queries | JSON instead of text |
23
+ | `--kind <kind>` | queries | Only this kind. Repeatable |
24
+ | `--section <name>` | queries | `skills`, `docs`, `code`, `database` or `tasks` |
25
+ | `-n, --limit <n>` | `search`, `list` | Most results |
26
+ | `--by <way>` | `search` | `name`, `meaning` or `both` |
27
+ | `--incoming`, `--outgoing` | `refs` | One direction only |
28
+ | `--edge <kind>` | `refs` | Only this edge kind. Repeatable |
29
+
30
+ Each flag has an `ANALYZE_*` variable, such as `ANALYZE_REPO`. A flag wins over its variable.
31
+
32
+ ## Kinds
33
+
34
+ - **Structure**: `folder`, `package`, `file`
35
+ - **Code**: `namespace`, `class`, `interface`, `struct`, `enum`, `trait`, `type`, `function` (methods included)
36
+ - **Docs and data**: `term`, `decision`, `table`, `query`
37
+ - **Tasks**: `plan`, `phase`, `task`
38
+ - **Members**, returned as their owner: `operation`, `definition`, `parameter`, `field`
39
+
40
+ The JSON of a node has its `kind` and its `type`, the finer sort, such as `method` or `api`.
41
+
42
+ ## Edge kinds
43
+
44
+ | Edge | From → to |
45
+ |---|---|
46
+ | `type` | parameter or field → its type |
47
+ | `returns`, `throws` | function → type it returns or throws |
48
+ | `extends` | type → type it extends or implements |
49
+ | `mentions` | page or description → node it links to |
50
+ | `uses` | text → glossary term |
51
+ | `affects` | decision → node it is about |
52
+ | `references` | table → table of its foreign key |
53
+ | `reads`, `writes` | query → table |
54
+ | `names`, `depends` | plan, phase or task → node it names, task it waits for |
@@ -0,0 +1,13 @@
1
+ # What analyze reads
2
+
3
+ | Shows as | Write |
4
+ |---|---|
5
+ | **Skill** | A `SKILL.md` inside an AI directory: `.claude`, `.codex`, `.cursor`, `.gemini`, `.agents`, `.windsurf`, `.opencode`. Elsewhere it is a doc |
6
+ | **Package** | A directory with a manifest: `package.json`, `pyproject.toml`, `Cargo.toml`, `go.mod`, `pom.xml`, `build.gradle`, `composer.json`, `.csproj`, `.gemspec`, `Gemfile`. A declared executable makes it an app |
7
+ | **Glossary** | A `glossary.md`. Each term is a node, and its first use in any text links to it |
8
+ | **Decisions** | A `decisions.md`. Each decision is a node; a link or inline code in it that names a node becomes an `affects` edge |
9
+ | **Code docs** | Doc comments. `@param`, `@returns`, `@throws` and `@property` attach to the parameter, return value, error and field |
10
+ | **Database** | `.sql` files. One with `CREATE TABLE` is a schema, any other a query |
11
+ | **Tasks** | The task-manager tree in `docs/task-manager`, or the directory `.g3n.config.json` names |
12
+
13
+ Inline code that names a type or function, such as `` `choose_route` ``, links to it from any text.
@@ -4,33 +4,50 @@ description: Docstrings and comments for functions and classes, and where the re
4
4
  disable-model-invocation: false
5
5
  ---
6
6
 
7
- Documentation describes the code as it is now: _what_ it does and _how_. The _why_ goes in `decisions.md`.
7
+ Documentation describes _what_ code does and _how_. The _why_ goes in `decisions.md`.
8
+
9
+ These rules apply to existing docstrings too. **Rewrite docstrings that breaks them when applicable**, and never copy the style of one that does.
8
10
 
9
11
  ## Files
10
12
 
11
- Line 1 of every file is a comment with its relative path, such as `// path/to/file.ts`. No file-level docstring.
13
+ Line 1 of every file is a comment with its relative path. No file-level docstring.
14
+
15
+ ## Docstrings
16
+
17
+ Functions, classes and other type declarations get a docstring; the language reference lists which. **A property, field or enum member is a tag in the docstring of its owner**, not a docstring of its own.
18
+
19
+ A _tag_ is a marked line in a docstring that documents one thing, such as `@param` in TypeScript.
20
+
21
+ **What a tag can say goes in the docstring, never in a code comment.** Any tag the language has is fine.
12
22
 
13
23
  ## Functions
14
24
 
15
- Every function gets a docstring that says what it does in one or two sentences.
25
+ The function its docstring is declarative of _what_ / _how_ it works. Use @brief to summarize the function in one line or two lines.
16
26
 
17
- `@param` and `@returns` important. Add one when the type alone hides the meaning, such as the form of an id, whether a path is absolute or relative to what, the unit of a number, or when the result can be empty. Leave them out when the name and the types already make it clear.
27
+ Tag a parameter or the return value when its name and type hide the meaning. Custom types are always tagged.
18
28
 
19
29
  ## Classes
20
30
 
21
31
  A class docstring says:
22
32
 
23
- - what the class does and what it holds,
24
- - where it is used: the modules that create it and the ones that consume it,
25
- - how to create one, as an `@example`, when the constructor takes more than the obvious.
33
+ - what it does and holds,
34
+ - which modules create and consume it,
35
+ - how to create one.
36
+
37
+ ## Other types
38
+
39
+ The summary says what the type _holds_. Tag its properties under the same rule as function parameters.
26
40
 
27
41
  ## Comments
28
42
 
29
- An inline comment explains how a piece of code works when the code itself does not show it. Use sparingly!
43
+ An inline comment explains how code works when neither the code nor a tag shows it. Use sparingly!
30
44
 
31
45
  ## Decisions
32
46
 
33
- The reason code is the way it is goes in `decisions.md` as an entry. Decisions are never written directly in the code.
47
+ A decision is a choice **the user** made, with its _why_, never the _how_. A choice made while implementing is not one.
48
+
49
+ - **Never add an entry without the user's explicit approval.**
50
+ - Decisions are never written in the code.
34
51
 
35
52
  ## Languages
36
53
 
@@ -1,6 +1,10 @@
1
1
  # TypeScript
2
2
 
3
- TSDoc: a `/** */` block directly above the declaration. The first sentence is the summary. Tags are `@param name - text` and `@returns text`.
3
+ TSDoc: a `/** */` block directly above the declaration.
4
+
5
+ - **Declarations with a docstring**: functions, classes, interfaces, type aliases and enums.
6
+ - **Summary**: `/** Adds two numbers. */` on one line. A longer block opens with `@brief` and the summary.
7
+ - **Tags**: `@param name - text`, `@returns text`, and for the members of a class, interface, type or enum `@param name - text` or `@property name - text`. Any other TSDoc tag that fits is fine: `@throws {Type} When ...`, `@example`, `@see`, `@deprecated`.
4
8
 
5
9
  ```ts
6
10
  /** Adds two numbers. */
@@ -45,9 +49,20 @@ export class FileValidationError extends Error {
45
49
  // ...
46
50
  }
47
51
  }
48
- ```
49
52
 
50
- Exported types and interfaces get a one-line docstring. A field gets its own `/** */` line only when its name and type leave its meaning open.
53
+ /**
54
+ * @brief Entries a set of requests resolved to, and the requests that did not resolve.
55
+ *
56
+ * @property entries - Resolved entries by key, e.g. `emphasise`.
57
+ * @property unknown - Requests no source offers, e.g. `ghost@mine`.
58
+ * @property notes - Why an entry nobody named is in the set, e.g. `{ vocabulary: "dep of plan" }`.
59
+ */
60
+ export interface Resolution {
61
+ entries: Map<string, SourceOffer>;
62
+ unknown: string[];
63
+ notes: Record<string, string>;
64
+ }
65
+ ```
51
66
 
52
67
  ## Bad
53
68
 
@@ -84,6 +99,32 @@ export function add(a: number, b: number): number
84
99
  ```
85
100
  The tags repeat the signature. The summary line is enough.
86
101
 
102
+ ```ts
103
+ /** Entries one request names. */
104
+ function matchRequest(request: EntryRequest, offers: readonly SourceOffer[]): Match
105
+ ```
106
+ A noun phrase that names the result, and no tags. The summary says what the function does: _Finds the entries one request names_. `EntryRequest`, `SourceOffer` and `Match` are custom types, so `request`, `offers` and the return value each need a tag.
107
+
108
+ ```ts
109
+ /** Entries a set of requests resolved to. */
110
+ export interface Resolution {
111
+ /** Resolved entries by key. */
112
+ entries: Map<string, SourceOffer>;
113
+ /** Requests no source offers. */
114
+ unknown: string[];
115
+ }
116
+ ```
117
+ A docstring on each property. Properties go in the interface's docstring as `@property` tags, as in `Resolution` above.
118
+
119
+ ```ts
120
+ /**
121
+ * Resolves the dependencies of one entry against every offer.
122
+ *
123
+ * @param preferred - Source to take a key from when several offer it.
124
+ */
125
+ ```
126
+ More than one line without `@brief`. It opens with `@brief Resolves the dependencies...`.
127
+
87
128
  ```ts
88
129
  /** Loads a user by id. Uses a surrogate key because auth ids changed format in 2024. */
89
130
  ```
@@ -3,11 +3,23 @@ import { join } from "node:path";
3
3
  import { DECISIONS_DIR } from "@task-manager/schemas/decision";
4
4
  import { MARKDOWN } from "@task-manager/filetree/levels";
5
5
 
6
- /** Folder holding a plan's decision files. */
6
+ /**
7
+ * @brief Finds the folder holding a plan's decision files.
8
+ *
9
+ * @param planDir - Plan folder.
10
+ * @returns Path of the plan's `decisions/` folder.
11
+ */
7
12
  export function decisionsDirFor(planDir: string): string {
8
13
  return join(planDir, DECISIONS_DIR);
9
14
  }
10
15
 
16
+ /**
17
+ * @brief Builds the path of one decision file in a plan.
18
+ *
19
+ * @param planDir - Plan folder.
20
+ * @param id - File name without extension, e.g. `0001-use-jwt`.
21
+ * @returns Path of the `<id>.md` file.
22
+ */
11
23
  export function decisionPathFor(planDir: string, id: string): string {
12
24
  return join(decisionsDirFor(planDir), `${id}${MARKDOWN}`);
13
25
  }
@@ -6,14 +6,25 @@ import { MARKDOWN } from "@task-manager/filetree/levels";
6
6
  import { readDocument } from "@task-manager/filetree/frontmatter";
7
7
  import { decisionPathFor, decisionsDirFor } from "./paths";
8
8
 
9
- /** One decision as it sits on disk: where it was read from, its fields, its prose. */
9
+ /**
10
+ * @brief Holds one decision as it sits on disk: where it was read from, its fields, its prose.
11
+ *
12
+ * @property file - Path of the decision's Markdown file.
13
+ * @property meta - Fields from the frontmatter, checked against `DecisionSchema`.
14
+ * @property body - Prose after the closing fence of the frontmatter.
15
+ */
10
16
  export interface DecisionDocument {
11
17
  file: string;
12
18
  meta: Decision;
13
19
  body: string;
14
20
  }
15
21
 
16
- /** Ids of a plan's decisions, in numbered order. A plan with no `decisions/` folder has none. */
22
+ /**
23
+ * @brief Lists the ids of a plan's decisions, in numbered order.
24
+ *
25
+ * @param planDir - Plan folder holding the `decisions/` folder.
26
+ * @returns File names without extension; none when the plan has no `decisions/` folder.
27
+ */
17
28
  export async function listDecisionIds(planDir: string): Promise<string[]> {
18
29
  let files;
19
30
 
@@ -31,11 +42,14 @@ export async function listDecisionIds(planDir: string): Promise<string[]> {
31
42
  }
32
43
 
33
44
  /**
34
- * Reads one decision of a plan and checks it against its schema.
45
+ * @brief Reads one decision of a plan and checks it against its schema.
35
46
  *
36
- * @param id file name without extension, e.g. `0001-use-jwt`
37
- * @throws FileValidationError when a field fails its schema, or when the `id`
38
- * inside the file disagrees with the file it was read from
47
+ * @param planDir - Plan folder holding the `decisions/` folder.
48
+ * @param id - File name without extension, e.g. `0001-use-jwt`.
49
+ * @returns The decision's path, its validated fields and its prose.
50
+ * @throws {Error} When no decision file has that `id`.
51
+ * @throws {FileValidationError} When a field fails its schema, or when the `id`
52
+ * inside the file disagrees with the file it was read from.
39
53
  */
40
54
  export async function readDecision(planDir: string, id: string): Promise<DecisionDocument> {
41
55
  const file = decisionPathFor(planDir, id);
@@ -6,15 +6,28 @@ import { setStatusLine } from "@task-manager/filetree/status";
6
6
  import { decisionPathFor, decisionsDirFor } from "./paths";
7
7
  import { listDecisionIds, readDecision, type DecisionDocument } from "./read";
8
8
 
9
- /** What `addDecision` needs from its caller; the id and date are worked out here. */
9
+ /**
10
+ * @brief Holds what `addDecision` needs from its caller; the id and date are worked out in `addDecision`.
11
+ *
12
+ * @property title - Title of the decision, also the source of its id slug.
13
+ * @property status - Status the new decision starts with.
14
+ * @property supersedes - Id of the earlier decision this one replaces; that file is moved to `superseded`.
15
+ * @property body - Decision text, written below the frontmatter.
16
+ */
10
17
  export interface NewDecision {
11
18
  title: string;
12
19
  status: DecisionStatus;
13
- /** Id of the earlier decision this one replaces; that file is moved to `superseded` */
14
20
  supersedes?: string;
15
21
  body: string;
16
22
  }
17
23
 
24
+ /**
25
+ * @brief Turns a title into a lowercase, dash-separated slug for a decision id.
26
+ *
27
+ * @param title - Decision title, e.g. `Use JWT for sessions`.
28
+ * @returns Slug, e.g. `use-jwt-for-sessions`.
29
+ * @throws {Error} When the title has no letters or digits.
30
+ */
18
31
  function slugFor(title: string): string {
19
32
  const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
20
33
  if (slug === "") throw new Error(`title "${title}" has no letters or digits to build an id from`);
@@ -22,7 +35,14 @@ function slugFor(title: string): string {
22
35
  return slug;
23
36
  }
24
37
 
25
- /** Next free id in a plan: one above the highest number in use, then the title as a slug. */
38
+ /**
39
+ * @brief Builds the next free decision id in a plan: one above the highest number in use, then the title as a slug.
40
+ *
41
+ * @param planDir - Plan folder.
42
+ * @param title - Title the id's slug is made from.
43
+ * @returns Id such as `0005-use-jwt`.
44
+ * @throws {Error} When every decision number up to 9999 is in use.
45
+ */
26
46
  export async function nextDecisionId(planDir: string, title: string): Promise<string> {
27
47
  const numbers = (await listDecisionIds(planDir)).map((id) => Number.parseInt(id.slice(0, 4), 10));
28
48
  const next = Math.max(0, ...numbers.filter((n) => Number.isInteger(n))) + 1;
@@ -33,10 +53,13 @@ export async function nextDecisionId(planDir: string, title: string): Promise<st
33
53
  }
34
54
 
35
55
  /**
36
- * Writes a new decision file into a plan, numbered after the ones already
56
+ * @brief Writes a new decision file into a plan, numbered after the ones already
37
57
  * there and dated today. Called by the `add-decision` hook.
38
58
  *
39
- * @throws Error when `supersedes` names no decision of this plan
59
+ * @param planDir - Plan folder.
60
+ * @param decision - Fields of the new decision; `supersedes` names the one it replaces.
61
+ * @returns The decision as read back from the file it was written to.
62
+ * @throws {Error} When `supersedes` names no decision of this plan.
40
63
  */
41
64
  export async function addDecision(planDir: string, decision: NewDecision): Promise<DecisionDocument> {
42
65
  const replaced = decision.supersedes === undefined
@@ -3,6 +3,12 @@ import { generateTree, listAllEntries, readEntry, removeEntry, type EntryLocatio
3
3
  import type { TaskStatus } from "../src/schemas";
4
4
  import { hasFlag, printJson, resolveRoot, runHook } from "../src/hooks";
5
5
 
6
+ /**
7
+ * @brief Holds one entry to delete, with the status that marked it for deletion.
8
+ *
9
+ * @property location - Where the entry sits, as `listAllEntries` reports it.
10
+ * @property status - Status that matched: `completed`, or `blocked` with `--blocked`.
11
+ */
6
12
  interface Removal {
7
13
  location: EntryLocation;
8
14
  status: TaskStatus;
@@ -6,13 +6,25 @@ import { resolveFiletreeRoot } from "../src/filetree";
6
6
  const GUARDED_TOOLS = ["Read", "Grep", "Glob"];
7
7
  const PATH_FIELDS = ["file_path", "path", "notebook_path"];
8
8
 
9
+ /**
10
+ * @brief Holds the fields of a `PreToolUse` payload the guard reads.
11
+ *
12
+ * @property tool_name - Name of the tool being called, e.g. `Read`.
13
+ * @property tool_input - Arguments of the call, empty when the payload has none.
14
+ * @property cwd - Directory the session runs in, the base for relative paths.
15
+ */
9
16
  interface Payload {
10
17
  tool_name: string;
11
18
  tool_input: Record<string, unknown>;
12
19
  cwd: string;
13
20
  }
14
21
 
15
- /** The fields of a `PreToolUse` payload the guard reads, or `null` when the JSON is not one. */
22
+ /**
23
+ * @brief Reads the fields of a `PreToolUse` payload the guard needs.
24
+ *
25
+ * @param raw - JSON text read from stdin.
26
+ * @returns The payload, or `null` when the JSON is not one.
27
+ */
16
28
  function readPayload(raw: string): Payload | null {
17
29
  const parsed: unknown = JSON.parse(raw);
18
30
  if (typeof parsed !== "object" || parsed === null) return null;
@@ -29,7 +41,13 @@ function readPayload(raw: string): Payload | null {
29
41
  };
30
42
  }
31
43
 
32
- /** The path the call names, made absolute against the session's directory. */
44
+ /**
45
+ * @brief Finds the path a tool call names, made absolute against the session's directory.
46
+ *
47
+ * @param input - Arguments of the call.
48
+ * @param cwd - Session directory that relative paths resolve against.
49
+ * @returns The absolute path, or `null` when the call names no path.
50
+ */
33
51
  function targetPath(input: Record<string, unknown>, cwd: string): string | null {
34
52
  for (const field of PATH_FIELDS) {
35
53
  const value = input[field];
@@ -41,12 +59,21 @@ function targetPath(input: Record<string, unknown>, cwd: string): string | null
41
59
  return null;
42
60
  }
43
61
 
44
- /** Whether `path` is `folder` itself or anything below it; both absolute. */
62
+ /**
63
+ * @brief Checks whether `path` is `folder` itself or anything below it.
64
+ *
65
+ * @param path - Absolute path to check.
66
+ * @param folder - Absolute folder.
67
+ */
45
68
  function isInside(path: string, folder: string): boolean {
46
69
  return path === folder || path.startsWith(`${folder}${sep}`);
47
70
  }
48
71
 
49
- /** Prints the `PreToolUse` answer that denies the call, with `reason` shown to the agent. */
72
+ /**
73
+ * @brief Prints the `PreToolUse` answer that denies the call.
74
+ *
75
+ * @param reason - Text shown to the agent.
76
+ */
50
77
  function deny(reason: string): void {
51
78
  console.log(
52
79
  JSON.stringify({
@@ -2,7 +2,7 @@
2
2
  import { ensureTree, generateTree } from "../src/filetree";
3
3
  import { readFlag, resolveRoot, runHook } from "../src/hooks";
4
4
 
5
- /** The session payload, read only for the directory the session opened in. */
5
+ /** Reads the directory the session opened in from the session payload on stdin. */
6
6
  async function projectFromPayload(): Promise<string | undefined> {
7
7
  if (process.stdin.isTTY) return undefined;
8
8