sessionmemory 0.3.1__tar.gz → 0.4.0__tar.gz

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 (36) hide show
  1. {sessionmemory-0.3.1 → sessionmemory-0.4.0}/PKG-INFO +6 -3
  2. {sessionmemory-0.3.1 → sessionmemory-0.4.0}/README.md +5 -2
  3. {sessionmemory-0.3.1 → sessionmemory-0.4.0}/pyproject.toml +1 -1
  4. {sessionmemory-0.3.1 → sessionmemory-0.4.0}/pyproject.toml.orig +1 -1
  5. {sessionmemory-0.3.1 → sessionmemory-0.4.0}/src/sessionmemory/commands/new.py +45 -5
  6. sessionmemory-0.4.0/src/sessionmemory/lib/backlog.py +134 -0
  7. {sessionmemory-0.3.1 → sessionmemory-0.4.0}/src/sessionmemory/lib/bootstrap.py +2 -1
  8. {sessionmemory-0.3.1 → sessionmemory-0.4.0}/src/sessionmemory/lib/config.py +5 -2
  9. {sessionmemory-0.3.1 → sessionmemory-0.4.0}/src/sessionmemory/lib/doctor.py +30 -2
  10. {sessionmemory-0.3.1 → sessionmemory-0.4.0}/src/sessionmemory/lib/field.py +6 -9
  11. {sessionmemory-0.3.1 → sessionmemory-0.4.0}/src/sessionmemory/lib/inject.py +7 -9
  12. {sessionmemory-0.3.1 → sessionmemory-0.4.0}/src/sessionmemory/__init__.py +0 -0
  13. {sessionmemory-0.3.1 → sessionmemory-0.4.0}/src/sessionmemory/cli.py +0 -0
  14. {sessionmemory-0.3.1 → sessionmemory-0.4.0}/src/sessionmemory/commands/__init__.py +0 -0
  15. {sessionmemory-0.3.1 → sessionmemory-0.4.0}/src/sessionmemory/commands/_common.py +0 -0
  16. {sessionmemory-0.3.1 → sessionmemory-0.4.0}/src/sessionmemory/commands/delete.py +0 -0
  17. {sessionmemory-0.3.1 → sessionmemory-0.4.0}/src/sessionmemory/commands/doctor.py +0 -0
  18. {sessionmemory-0.3.1 → sessionmemory-0.4.0}/src/sessionmemory/commands/export.py +0 -0
  19. {sessionmemory-0.3.1 → sessionmemory-0.4.0}/src/sessionmemory/commands/init.py +0 -0
  20. {sessionmemory-0.3.1 → sessionmemory-0.4.0}/src/sessionmemory/commands/inject.py +0 -0
  21. {sessionmemory-0.3.1 → sessionmemory-0.4.0}/src/sessionmemory/commands/log.py +0 -0
  22. {sessionmemory-0.3.1 → sessionmemory-0.4.0}/src/sessionmemory/commands/project.py +0 -0
  23. {sessionmemory-0.3.1 → sessionmemory-0.4.0}/src/sessionmemory/commands/reindex.py +0 -0
  24. {sessionmemory-0.3.1 → sessionmemory-0.4.0}/src/sessionmemory/commands/search.py +0 -0
  25. {sessionmemory-0.3.1 → sessionmemory-0.4.0}/src/sessionmemory/lib/__init__.py +0 -0
  26. {sessionmemory-0.3.1 → sessionmemory-0.4.0}/src/sessionmemory/lib/atomic.py +0 -0
  27. {sessionmemory-0.3.1 → sessionmemory-0.4.0}/src/sessionmemory/lib/embed.py +0 -0
  28. {sessionmemory-0.3.1 → sessionmemory-0.4.0}/src/sessionmemory/lib/export.py +0 -0
  29. {sessionmemory-0.3.1 → sessionmemory-0.4.0}/src/sessionmemory/lib/fieldindex.py +0 -0
  30. {sessionmemory-0.3.1 → sessionmemory-0.4.0}/src/sessionmemory/lib/frontmatter.py +0 -0
  31. {sessionmemory-0.3.1 → sessionmemory-0.4.0}/src/sessionmemory/lib/gitinfo.py +0 -0
  32. {sessionmemory-0.3.1 → sessionmemory-0.4.0}/src/sessionmemory/lib/ids.py +0 -0
  33. {sessionmemory-0.3.1 → sessionmemory-0.4.0}/src/sessionmemory/lib/log.py +0 -0
  34. {sessionmemory-0.3.1 → sessionmemory-0.4.0}/src/sessionmemory/lib/paths.py +0 -0
  35. {sessionmemory-0.3.1 → sessionmemory-0.4.0}/src/sessionmemory/lib/registry.py +0 -0
  36. {sessionmemory-0.3.1 → sessionmemory-0.4.0}/src/sessionmemory/lib/resolve.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: sessionmemory
3
- Version: 0.3.1
3
+ Version: 0.4.0
4
4
  Summary: Durable memory for coding agents, one folder of searchable pages per project.
5
5
  Author: Nathaniel Landau
6
6
  Author-email: Nathaniel Landau <github@natelandau.com>
@@ -215,8 +215,11 @@ away. The project's folder has `learnings/` and `logs/`, searched by meaning, be
215
215
  - Past sessions, one page each: `sessionmemory search "<words>" --logs`.
216
216
  - Open work: read `backlog.md`. An item is one line under a `## <kind>` heading
217
217
  (feat, fix, refactor, perf, docs, test, build, ci), sized S, M, or L:
218
- `- [ ] [S] <imperative description> - <YYYY-MM-DD> [#topic]`. Add, tick, or
219
- delete lines directly. If the file is missing, create it with a `# Backlog` heading.
218
+ `- [S] <imperative description> - <YYYY-MM-DD> [#topic]`. Add one with
219
+ `sessionmemory new backlog --kind <kind> --size <S|M|L> --title "..." --topic <topic> --cwd .`,
220
+ which creates the file or heading when missing. Delete a finished line, and one
221
+ that will never be done, directly; never tick or annotate it. Git history is the
222
+ record of what was finished.
220
223
  - Specs and plans: `sessionmemory new spec|plan --title "..." --cwd .` creates the file
221
224
  and prints its path. Edit it directly after that.
222
225
  - Learnings are captured at session end, not by you mid-session. When the user asks
@@ -200,8 +200,11 @@ away. The project's folder has `learnings/` and `logs/`, searched by meaning, be
200
200
  - Past sessions, one page each: `sessionmemory search "<words>" --logs`.
201
201
  - Open work: read `backlog.md`. An item is one line under a `## <kind>` heading
202
202
  (feat, fix, refactor, perf, docs, test, build, ci), sized S, M, or L:
203
- `- [ ] [S] <imperative description> - <YYYY-MM-DD> [#topic]`. Add, tick, or
204
- delete lines directly. If the file is missing, create it with a `# Backlog` heading.
203
+ `- [S] <imperative description> - <YYYY-MM-DD> [#topic]`. Add one with
204
+ `sessionmemory new backlog --kind <kind> --size <S|M|L> --title "..." --topic <topic> --cwd .`,
205
+ which creates the file or heading when missing. Delete a finished line, and one
206
+ that will never be done, directly; never tick or annotate it. Git history is the
207
+ record of what was finished.
205
208
  - Specs and plans: `sessionmemory new spec|plan --title "..." --cwd .` creates the file
206
209
  and prints its path. Edit it directly after that.
207
210
  - Learnings are captured at session end, not by you mid-session. When the user asks
@@ -11,7 +11,7 @@ description = "Durable memory for coding agents, one folder of searchable pages
11
11
  name = "sessionmemory"
12
12
  readme = "README.md"
13
13
  requires-python = ">=3.13,<3.15"
14
- version = "0.3.1"
14
+ version = "0.4.0"
15
15
 
16
16
  [[project.authors]]
17
17
  name = "Nathaniel Landau"
@@ -12,7 +12,7 @@
12
12
  name = "sessionmemory"
13
13
  readme = "README.md"
14
14
  requires-python = ">=3.13,<3.15"
15
- version = "0.3.1"
15
+ version = "0.4.0"
16
16
 
17
17
  [project.scripts]
18
18
  sessionmemory = "sessionmemory.cli:main"
@@ -1,7 +1,8 @@
1
- """Commands that create a page or a document and print the path to write into."""
1
+ """Commands that create a page, a document, or a backlog item and print what was written."""
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import enum
5
6
  from pathlib import Path # noqa: TC003
6
7
 
7
8
  import typer
@@ -14,10 +15,15 @@ from sessionmemory.commands._common import (
14
15
  require_vault,
15
16
  resolve_body,
16
17
  )
17
- from sessionmemory.lib import field, paths
18
- from sessionmemory.lib.config import now
18
+ from sessionmemory.lib import backlog, field, paths
19
+ from sessionmemory.lib.config import now, today
19
20
 
20
- app = typer.Typer(no_args_is_help=True, help="Create a learning, spec, or plan.")
21
+ app = typer.Typer(no_args_is_help=True, help="Create a learning, spec, plan, or backlog item.")
22
+
23
+ # Typer lists an Enum's members in --help and rejects anything else at parse time, so the
24
+ # allowed sets are spelled once, in lib/backlog, and mirrored here as choices.
25
+ Kind = enum.Enum("Kind", {kind: kind for kind in backlog.KINDS}, type=str)
26
+ Size = enum.Enum("Size", {size: size for size in backlog.SIZES}, type=str)
21
27
 
22
28
  TITLE = typer.Option(..., "--title", help="The title.")
23
29
  SUMMARY = typer.Option(..., "--summary", help="One sentence a search result shows.")
@@ -25,6 +31,10 @@ BODY = typer.Option("", "--body", help="Markdown body.")
25
31
  BODY_FILE = typer.Option(None, "--body-file", help="Read the body from a file, or stdin for '-'.")
26
32
  CWD = typer.Option(None, "--cwd", help="Directory to resolve the project from.")
27
33
  JSON = typer.Option(False, "--json", help="Emit JSON instead of prose.") # noqa: FBT003
34
+ KIND = typer.Option(..., "--kind", help="The commit type heading the item goes under.")
35
+ SIZE = typer.Option(..., "--size", help="Effort: S, M, or L.")
36
+ DESCRIPTION = typer.Option(..., "--title", help="The imperative description of the work.")
37
+ TOPIC = typer.Option(None, "--topic", help="A topic tag, without the leading hash.")
28
38
 
29
39
 
30
40
  def _report(path: Path, *, as_json: bool, uuid: str | None = None) -> None:
@@ -62,7 +72,7 @@ def new_learning(
62
72
 
63
73
  def _new_document(folder: Path, title: str, body: str, *, as_json: bool) -> None:
64
74
  try:
65
- path = field.new_document(folder, title=title, body=body, now=now())
75
+ path = field.new_document(folder, title=title, body=body, now=now(), day=today())
66
76
  except field.PageError as error:
67
77
  fail(str(error))
68
78
  _report(path, as_json=as_json)
@@ -100,3 +110,33 @@ def new_plan(
100
110
  _new_document(
101
111
  paths.plans_dir(vault, slug), title, resolve_body(body, body_file), as_json=as_json
102
112
  )
113
+
114
+
115
+ @app.command("backlog", help="Add one open item to this project's backlog.md.")
116
+ def new_backlog(
117
+ kind: Kind = KIND,
118
+ size: Size = SIZE,
119
+ title: str = DESCRIPTION,
120
+ topic: str | None = TOPIC,
121
+ cwd: Path | None = CWD,
122
+ *,
123
+ as_json: bool = JSON,
124
+ ) -> None:
125
+ """Append an open item under its kind heading, creating the file or heading as needed.
126
+
127
+ Raises:
128
+ typer.Exit: The title or topic cannot be written as a well-formed line.
129
+ """
130
+ vault = require_vault()
131
+ slug = require_project(vault, cwd)
132
+ path = paths.backlog_path(vault, slug)
133
+ try:
134
+ line = backlog.add_item(
135
+ path, kind=kind.value, size=size.value, description=title, topic=topic, today=today()
136
+ )
137
+ except backlog.BacklogError as error:
138
+ fail(str(error))
139
+ if as_json:
140
+ emit_json({"path": str(path), "line": line})
141
+ return
142
+ pp.success(f"added to {path.name}", details=[line, str(path)])
@@ -0,0 +1,134 @@
1
+ """Append an item to a project's backlog and recognize one.
2
+
3
+ This is the one place the item line's shape is known. A backlog line carries a kind that
4
+ must match a `## <kind>` heading, a size from a fixed set, a date, and a topic tag, and
5
+ the inject count, the doctor check, and the backlog skill all depend on that shape being
6
+ exact. The line has no checkbox: finished work is deleted, and git history is the
7
+ record of it, so there is nothing to tick. Deleting a line needs no validation and
8
+ stays a direct edit.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import re
14
+ from typing import TYPE_CHECKING
15
+
16
+ from sessionmemory.lib import atomic
17
+
18
+ if TYPE_CHECKING:
19
+ from pathlib import Path
20
+
21
+ KINDS = ("feat", "fix", "refactor", "perf", "docs", "test", "build", "ci")
22
+ SIZES = ("S", "M", "L")
23
+ HEADING = "# Backlog"
24
+ ITEM_PATTERN = re.compile(
25
+ rf"^- \[(?:{'|'.join(SIZES)})\] \S.* - \d{{4}}-\d{{2}}-\d{{2}}(?: \[#[^\s\]]+\])?$"
26
+ )
27
+ _SECTION_PREFIX = "## "
28
+
29
+
30
+ class BacklogError(ValueError):
31
+ """An item field that cannot be written as a well-formed line."""
32
+
33
+
34
+ def format_item(*, kind: str, size: str, description: str, topic: str | None, today: str) -> str:
35
+ """Return the checklist line for one open item, validating every field.
36
+
37
+ Args:
38
+ kind (str): One of `KINDS`; the heading the line belongs under.
39
+ size (str): One of `SIZES`.
40
+ description (str): The imperative description, a single line.
41
+ topic (str | None): The tag written as `[#topic]`, or None for no tag. A leading
42
+ `#` is accepted and dropped.
43
+ today (str): The date as `YYYY-MM-DD`.
44
+
45
+ Returns:
46
+ str: The line, without a trailing newline.
47
+
48
+ Raises:
49
+ BacklogError: A field is empty, spans lines, contains whitespace where a tag
50
+ cannot, or names an unknown kind or size.
51
+ """
52
+ if kind not in KINDS:
53
+ msg = f"kind must be one of {', '.join(KINDS)}, not {kind!r}"
54
+ raise BacklogError(msg)
55
+ if size not in SIZES:
56
+ msg = f"size must be one of {', '.join(SIZES)}, not {size!r}"
57
+ raise BacklogError(msg)
58
+ description = description.strip()
59
+ if not description:
60
+ msg = "the description is empty"
61
+ raise BacklogError(msg)
62
+ if "\n" in description:
63
+ msg = "the description must be a single line"
64
+ raise BacklogError(msg)
65
+ line = f"- [{size}] {description} - {today}"
66
+ if topic is None:
67
+ return line
68
+ topic = topic.strip().removeprefix("#")
69
+ if not topic:
70
+ msg = "the topic is empty"
71
+ raise BacklogError(msg)
72
+ if any(character.isspace() for character in topic):
73
+ msg = "the topic cannot contain whitespace"
74
+ raise BacklogError(msg)
75
+ return f"{line} [#{topic}]"
76
+
77
+
78
+ def is_item(line: str) -> bool:
79
+ """Return whether `line` is an open item, the only shape the session start counts.
80
+
81
+ Args:
82
+ line (str): One line of `backlog.md`; leading whitespace is ignored.
83
+
84
+ Returns:
85
+ bool: True for `- [S|M|L] <description> - <YYYY-MM-DD>` with an optional
86
+ `[#topic]`, and False for anything else, a checkbox included.
87
+ """
88
+ return ITEM_PATTERN.match(line.lstrip()) is not None
89
+
90
+
91
+ def _insert(lines: list[str], kind: str, item: str) -> list[str]:
92
+ """Place `item` at the end of the `## kind` section, or open that section last."""
93
+ heading = f"{_SECTION_PREFIX}{kind}"
94
+ start = next((i for i, line in enumerate(lines) if line.rstrip() == heading), None)
95
+ if start is None:
96
+ return [*lines, "", heading, "", item]
97
+ end = next(
98
+ (i for i in range(start + 1, len(lines)) if lines[i].startswith(_SECTION_PREFIX)),
99
+ len(lines),
100
+ )
101
+ last = end
102
+ while last > start + 1 and not lines[last - 1].strip():
103
+ last -= 1
104
+ if last == start + 1:
105
+ return [*lines[:last], "", item, *lines[last:]]
106
+ return [*lines[:last], item, *lines[last:]]
107
+
108
+
109
+ def add_item(
110
+ path: Path, *, kind: str, size: str, description: str, topic: str | None, today: str
111
+ ) -> str:
112
+ """Append one open item under its kind heading and return the line written.
113
+
114
+ A missing file starts with `# Backlog`. An existing file keeps whatever heads it,
115
+ since the `# Backlog` heading is never inserted into a file someone else structured.
116
+
117
+ Args:
118
+ path (Path): The project's `backlog.md`.
119
+ kind (str): One of `KINDS`.
120
+ size (str): One of `SIZES`.
121
+ description (str): The imperative description.
122
+ topic (str | None): The tag, or None for none.
123
+ today (str): The date as `YYYY-MM-DD`.
124
+
125
+ Returns:
126
+ str: The line written, without a trailing newline.
127
+
128
+ Raises:
129
+ BacklogError: A field cannot be written as a well-formed line.
130
+ """
131
+ item = format_item(kind=kind, size=size, description=description, topic=topic, today=today)
132
+ lines = path.read_text(encoding="utf-8").rstrip().splitlines() if path.is_file() else [HEADING]
133
+ atomic.write_text(path, "\n".join(_insert(lines, kind=kind, item=item)) + "\n")
134
+ return item
@@ -57,7 +57,8 @@ derived from the pages beside it, gitignored, and rebuilt by `sessionmemory rein
57
57
  `_system/vault.toml` is the marker that tells the CLI this directory is a vault.
58
58
 
59
59
  Pages are created with `sessionmemory new learning` and searched with
60
- `sessionmemory search`. Everything else is an ordinary file you read and edit directly.
60
+ `sessionmemory search`, and a backlog item is added with `sessionmemory new backlog`.
61
+ Everything else is an ordinary file you read and edit directly.
61
62
  The format follows the memoryfield spec: https://github.com/calpaterson/memoryfield-spec
62
63
  """
63
64
 
@@ -83,12 +83,15 @@ def _configured_root() -> str | None:
83
83
 
84
84
 
85
85
  def today() -> str:
86
- """Return today's date as an ISO string.
86
+ """Return today's local date as an ISO string.
87
+
88
+ A date a person reads, in a filename or a checklist line, follows their clock; only
89
+ the frontmatter timestamps from `now` are UTC, which the memoryfield spec asks for.
87
90
 
88
91
  Returns:
89
92
  str: Today's date.
90
93
  """
91
- return datetime.datetime.now(tz=datetime.UTC).date().isoformat()
94
+ return datetime.datetime.now().astimezone().date().isoformat()
92
95
 
93
96
 
94
97
  def now() -> str:
@@ -1,4 +1,4 @@
1
- """The six things that can be wrong with a vault, stated without severity.
1
+ """The seven things that can be wrong with a vault, stated without severity.
2
2
 
3
3
  Every check is a suggestion. The article's argument holds: an irrelevant or imperfect
4
4
  page is never surfaced by semantic search, so nothing here fails a build. There is no
@@ -13,7 +13,7 @@ from dataclasses import dataclass
13
13
  from pathlib import Path
14
14
  from typing import TYPE_CHECKING
15
15
 
16
- from sessionmemory.lib import field, fieldindex, paths, registry
16
+ from sessionmemory.lib import backlog, field, fieldindex, paths, registry
17
17
  from sessionmemory.lib.frontmatter import (
18
18
  FrontmatterError,
19
19
  MissingFrontmatterError,
@@ -139,6 +139,33 @@ def dead_projects(vault: Path, _embedder: Embedder) -> list[Finding]:
139
139
  ]
140
140
 
141
141
 
142
+ _TICKED = "- [x]"
143
+ _SHAPE = "- [S|M|L] <description> - <YYYY-MM-DD> [#topic]"
144
+
145
+
146
+ def uncounted_backlog_lines(vault: Path, _embedder: Embedder) -> list[Finding]:
147
+ """Report a top-level bullet in a backlog that the session start does not count.
148
+
149
+ A ticked line is finished work that should have been deleted; anything else is a
150
+ line that does not match the item shape. An indented bullet is a note under an
151
+ item and is left alone.
152
+ """
153
+ findings = []
154
+ for slug in paths.iter_project_slugs(vault):
155
+ path = paths.backlog_path(vault, slug)
156
+ if not path.is_file():
157
+ continue
158
+ for number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), start=1):
159
+ if not line.startswith("- ") or backlog.is_item(line):
160
+ continue
161
+ if line.startswith(_TICKED):
162
+ message = f"line {number}: ticked; finished work is deleted, not ticked"
163
+ else:
164
+ message = f"line {number}: not counted as open; the shape is `{_SHAPE}`"
165
+ findings.append(Finding("backlog", str(path), message))
166
+ return findings
167
+
168
+
142
169
  def _is_stale(directory: Path, embedder: Embedder) -> str | None:
143
170
  index = fieldindex.index_path(directory, embedder)
144
171
  if not index.is_file():
@@ -177,6 +204,7 @@ CHECKS: tuple[Callable[[Path, Embedder], list[Finding]], ...] = (
177
204
  malformed_frontmatter,
178
205
  unquoted_datetimes,
179
206
  dead_projects,
207
+ uncounted_backlog_lines,
180
208
  stale_indexes,
181
209
  )
182
210
 
@@ -179,21 +179,18 @@ def new_page(directory: Path, *, title: str, summary: str, body: str, now: str)
179
179
  return _create(directory, meta, body, stem=None)
180
180
 
181
181
 
182
- def new_document(
183
- directory: Path, *, title: str, body: str, now: str, stem: str | None = None
184
- ) -> Path:
185
- """Create a spec, plan, or log: a titled, dated file that is not a memory page.
182
+ def new_document(directory: Path, *, title: str, body: str, now: str, day: str) -> Path:
183
+ """Create a spec or plan: a titled, dated file that is not a memory page.
186
184
 
187
- The filename leads with the creation date unless the caller fixes the stem, so a
188
- directory listing reads in the order the documents were written.
185
+ The filename leads with `day`, so a directory listing reads in the order the
186
+ documents were written. `day` is passed rather than sliced from `now` because the
187
+ timestamp is UTC and the date a person reads in a filename follows their own clock.
189
188
 
190
189
  Raises:
191
190
  PageError: If the title yields no slug or no name is free.
192
191
  """
193
- if stem is None:
194
- stem = dated_stem(title, now[: len("2026-01-01")])
195
192
  meta = {"title": title, "created": now, "updated": now}
196
- return _create(directory, meta, body, stem=stem)
193
+ return _create(directory, meta, body, stem=dated_stem(title, day))
197
194
 
198
195
 
199
196
  def dated_stem(title: str, day: str) -> str:
@@ -10,12 +10,11 @@ from __future__ import annotations
10
10
  from dataclasses import dataclass
11
11
  from typing import TYPE_CHECKING
12
12
 
13
- from sessionmemory.lib import field, paths
13
+ from sessionmemory.lib import backlog, field, paths
14
14
 
15
15
  if TYPE_CHECKING:
16
16
  from pathlib import Path
17
17
 
18
- OPEN_ITEM = "- [ ]"
19
18
  _LEADING_PUNCTUATION = "`'\"([{<*_~"
20
19
 
21
20
 
@@ -43,11 +42,7 @@ def _titles(directory: Path) -> tuple[str, ...]:
43
42
  def _open_backlog(path: Path) -> int:
44
43
  if not path.is_file():
45
44
  return 0
46
- return sum(
47
- 1
48
- for line in path.read_text(encoding="utf-8").splitlines()
49
- if line.lstrip().startswith(OPEN_ITEM)
50
- )
45
+ return sum(1 for line in path.read_text(encoding="utf-8").splitlines() if backlog.is_item(line))
51
46
 
52
47
 
53
48
  def build(vault: Path, slug: str) -> Injection:
@@ -76,8 +71,11 @@ away. The project's folder has `learnings/` and `logs/`, searched by meaning, be
76
71
  - Past sessions, one page each: `{command} search "<words>" --logs`.
77
72
  - Open work: read `backlog.md`. An item is one line under a `## <kind>` heading
78
73
  (feat, fix, refactor, perf, docs, test, build, ci), sized S, M, or L:
79
- `- [ ] [S] <imperative description> - <YYYY-MM-DD> [#topic]`. Add, tick, or
80
- delete lines directly. If the file is missing, create it with a `# Backlog` heading.
74
+ `- [S] <imperative description> - <YYYY-MM-DD> [#topic]`. Add one with
75
+ `{command} new backlog --kind <kind> --size <S|M|L> --title "..." --topic <topic> --cwd .`,
76
+ which creates the file or heading when missing. Delete a finished line, and one
77
+ that will never be done, directly; never tick or annotate it. Git history is the
78
+ record of what was finished.
81
79
  - Specs and plans: `{command} new spec|plan --title "..." --cwd .` creates the file
82
80
  and prints its path. Edit it directly after that.
83
81
  - Learnings are captured at session end, not by you mid-session. When the user asks