project-guide 2.7.0__tar.gz → 2.7.2__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 (121) hide show
  1. {project_guide-2.7.0 → project_guide-2.7.2}/.gitignore +5 -3
  2. {project_guide-2.7.0 → project_guide-2.7.2}/.project-guide.yml +2 -2
  3. {project_guide-2.7.0 → project_guide-2.7.2}/CHANGELOG.md +38 -0
  4. {project_guide-2.7.0 → project_guide-2.7.2}/PKG-INFO +1 -1
  5. {project_guide-2.7.0 → project_guide-2.7.2}/docs/specs/features.md +9 -1
  6. {project_guide-2.7.0 → project_guide-2.7.2}/docs/specs/project-essentials.md +9 -3
  7. {project_guide-2.7.0 → project_guide-2.7.2}/docs/specs/stories.md +81 -1
  8. {project_guide-2.7.0 → project_guide-2.7.2}/docs/specs/tech-spec.md +16 -6
  9. {project_guide-2.7.0 → project_guide-2.7.2}/project_guide/actions.py +4 -2
  10. {project_guide-2.7.0 → project_guide-2.7.2}/project_guide/cli.py +55 -32
  11. {project_guide-2.7.0 → project_guide-2.7.2}/project_guide/stories.py +4 -1
  12. {project_guide-2.7.0 → project_guide-2.7.2}/project_guide/templates/project-guide/templates/modes/_phase-letters.md +9 -0
  13. {project_guide-2.7.0 → project_guide-2.7.2}/project_guide/version.py +1 -1
  14. {project_guide-2.7.0 → project_guide-2.7.2}/pyproject.toml +1 -1
  15. {project_guide-2.7.0 → project_guide-2.7.2}/tests/test_actions.py +13 -0
  16. {project_guide-2.7.0 → project_guide-2.7.2}/tests/test_cli.py +139 -21
  17. project_guide-2.7.2/tests/test_stories.py +141 -0
  18. {project_guide-2.7.0 → project_guide-2.7.2}/.github/FUNDING.yml +0 -0
  19. {project_guide-2.7.0 → project_guide-2.7.2}/.github/dependabot.yml +0 -0
  20. {project_guide-2.7.0 → project_guide-2.7.2}/.github/workflows/ci.yml +0 -0
  21. {project_guide-2.7.0 → project_guide-2.7.2}/.github/workflows/deploy-docs.yml +0 -0
  22. {project_guide-2.7.0 → project_guide-2.7.2}/.github/workflows/publish.yml +0 -0
  23. {project_guide-2.7.0 → project_guide-2.7.2}/.github/workflows/test.yml +0 -0
  24. {project_guide-2.7.0 → project_guide-2.7.2}/.pyve/config +0 -0
  25. {project_guide-2.7.0 → project_guide-2.7.2}/.tool-versions +0 -0
  26. {project_guide-2.7.0 → project_guide-2.7.2}/CONTRIBUTING.md +0 -0
  27. {project_guide-2.7.0 → project_guide-2.7.2}/LICENSE +0 -0
  28. {project_guide-2.7.0 → project_guide-2.7.2}/README.md +0 -0
  29. {project_guide-2.7.0 → project_guide-2.7.2}/SECURITY.md +0 -0
  30. {project_guide-2.7.0 → project_guide-2.7.2}/docs/site/.gitignore +0 -0
  31. {project_guide-2.7.0 → project_guide-2.7.2}/docs/site/about/changelog.md +0 -0
  32. {project_guide-2.7.0 → project_guide-2.7.2}/docs/site/about/license.md +0 -0
  33. {project_guide-2.7.0 → project_guide-2.7.2}/docs/site/developer-guide/contributing.md +0 -0
  34. {project_guide-2.7.0 → project_guide-2.7.2}/docs/site/developer-guide/development.md +0 -0
  35. {project_guide-2.7.0 → project_guide-2.7.2}/docs/site/developer-guide/testing.md +0 -0
  36. {project_guide-2.7.0 → project_guide-2.7.2}/docs/site/getting-started.md +0 -0
  37. {project_guide-2.7.0 → project_guide-2.7.2}/docs/site/images/project-guide-banner-landing.png +0 -0
  38. {project_guide-2.7.0 → project_guide-2.7.2}/docs/site/images/project-guide-header-readme.png +0 -0
  39. {project_guide-2.7.0 → project_guide-2.7.2}/docs/site/index.html +0 -0
  40. {project_guide-2.7.0 → project_guide-2.7.2}/docs/site/user-guide/commands.md +0 -0
  41. {project_guide-2.7.0 → project_guide-2.7.2}/docs/site/user-guide/configuration.md +0 -0
  42. {project_guide-2.7.0 → project_guide-2.7.2}/docs/site/user-guide/install-options.md +0 -0
  43. {project_guide-2.7.0 → project_guide-2.7.2}/docs/site/user-guide/modes.md +0 -0
  44. {project_guide-2.7.0 → project_guide-2.7.2}/docs/site/user-guide/overrides.md +0 -0
  45. {project_guide-2.7.0 → project_guide-2.7.2}/docs/site/user-guide/workflow.md +0 -0
  46. {project_guide-2.7.0 → project_guide-2.7.2}/docs/specs/.archive/phase-j-modes-plan.md +0 -0
  47. {project_guide-2.7.0 → project_guide-2.7.2}/docs/specs/.archive/phase-k-release-lifecycle-plan.md +0 -0
  48. {project_guide-2.7.0 → project_guide-2.7.2}/docs/specs/.archive/phase-l-no-input-init-plan.md +0 -0
  49. {project_guide-2.7.0 → project_guide-2.7.2}/docs/specs/.archive/phase-m-project-essentials-plan.md +0 -0
  50. {project_guide-2.7.0 → project_guide-2.7.2}/docs/specs/.archive/phase-n-mode-naming-cli-memory-plan.md +0 -0
  51. {project_guide-2.7.0 → project_guide-2.7.2}/docs/specs/.archive/phase-o-pyve-quiet-embedding-subplan.md +0 -0
  52. {project_guide-2.7.0 → project_guide-2.7.2}/docs/specs/.archive/phase-o-quiet-non-interactive-embedding-feature.md +0 -0
  53. {project_guide-2.7.0 → project_guide-2.7.2}/docs/specs/.archive/project-guide-no-input-spec.md +0 -0
  54. {project_guide-2.7.0 → project_guide-2.7.2}/docs/specs/.archive/stories-v1.3.1.md +0 -0
  55. {project_guide-2.7.0 → project_guide-2.7.2}/docs/specs/.archive/stories-v2.0.20.md +0 -0
  56. {project_guide-2.7.0 → project_guide-2.7.2}/docs/specs/.archive/stories-v2.3.9.md +0 -0
  57. {project_guide-2.7.0 → project_guide-2.7.2}/docs/specs/.archive/stories-v2.4.19.md +0 -0
  58. {project_guide-2.7.0 → project_guide-2.7.2}/docs/specs/.archive/stories-v2.5.15.md +0 -0
  59. {project_guide-2.7.0 → project_guide-2.7.2}/docs/specs/.archive/ux-problems-v2.0.10.md +0 -0
  60. {project_guide-2.7.0 → project_guide-2.7.2}/docs/specs/brand-descriptions.md +0 -0
  61. {project_guide-2.7.0 → project_guide-2.7.2}/docs/specs/concept.md +0 -0
  62. {project_guide-2.7.0 → project_guide-2.7.2}/docs/specs/phase-p-auto-heal-plan.md +0 -0
  63. {project_guide-2.7.0 → project_guide-2.7.2}/mkdocs.yml +0 -0
  64. {project_guide-2.7.0 → project_guide-2.7.2}/project-guide-old-template.md +0 -0
  65. {project_guide-2.7.0 → project_guide-2.7.2}/project_guide/__init__.py +0 -0
  66. {project_guide-2.7.0 → project_guide-2.7.2}/project_guide/__main__.py +0 -0
  67. {project_guide-2.7.0 → project_guide-2.7.2}/project_guide/config.py +0 -0
  68. {project_guide-2.7.0 → project_guide-2.7.2}/project_guide/exceptions.py +0 -0
  69. {project_guide-2.7.0 → project_guide-2.7.2}/project_guide/metadata.py +0 -0
  70. {project_guide-2.7.0 → project_guide-2.7.2}/project_guide/render.py +0 -0
  71. {project_guide-2.7.0 → project_guide-2.7.2}/project_guide/runtime.py +0 -0
  72. {project_guide-2.7.0 → project_guide-2.7.2}/project_guide/sync.py +0 -0
  73. {project_guide-2.7.0 → project_guide-2.7.2}/project_guide/templates/.project-guide.yml.template +0 -0
  74. {project_guide-2.7.0 → project_guide-2.7.2}/project_guide/templates/project-guide/.metadata.yml +0 -0
  75. {project_guide-2.7.0 → project_guide-2.7.2}/project_guide/templates/project-guide/README.md +0 -0
  76. {project_guide-2.7.0 → project_guide-2.7.2}/project_guide/templates/project-guide/developer/best-practices-guide.md +0 -0
  77. {project_guide-2.7.0 → project_guide-2.7.2}/project_guide/templates/project-guide/developer/brand-descriptions-guide.md +0 -0
  78. {project_guide-2.7.0 → project_guide-2.7.2}/project_guide/templates/project-guide/developer/codecov-setup-guide.md +0 -0
  79. {project_guide-2.7.0 → project_guide-2.7.2}/project_guide/templates/project-guide/developer/debug-guide.md +0 -0
  80. {project_guide-2.7.0 → project_guide-2.7.2}/project_guide/templates/project-guide/developer/landing-page-guide.md +0 -0
  81. {project_guide-2.7.0 → project_guide-2.7.2}/project_guide/templates/project-guide/developer/production-github-guide.md +0 -0
  82. {project_guide-2.7.0 → project_guide-2.7.2}/project_guide/templates/project-guide/developer/project-guide.md +0 -0
  83. {project_guide-2.7.0 → project_guide-2.7.2}/project_guide/templates/project-guide/developer/python-editable-install.md +0 -0
  84. {project_guide-2.7.0 → project_guide-2.7.2}/project_guide/templates/project-guide/templates/artifacts/brand-descriptions.md +0 -0
  85. {project_guide-2.7.0 → project_guide-2.7.2}/project_guide/templates/project-guide/templates/artifacts/concept.md +0 -0
  86. {project_guide-2.7.0 → project_guide-2.7.2}/project_guide/templates/project-guide/templates/artifacts/features.md +0 -0
  87. {project_guide-2.7.0 → project_guide-2.7.2}/project_guide/templates/project-guide/templates/artifacts/project-essentials.md +0 -0
  88. {project_guide-2.7.0 → project_guide-2.7.2}/project_guide/templates/project-guide/templates/artifacts/pyve-essentials.md +0 -0
  89. {project_guide-2.7.0 → project_guide-2.7.2}/project_guide/templates/project-guide/templates/artifacts/stories.md +0 -0
  90. {project_guide-2.7.0 → project_guide-2.7.2}/project_guide/templates/project-guide/templates/artifacts/tech-spec.md +0 -0
  91. {project_guide-2.7.0 → project_guide-2.7.2}/project_guide/templates/project-guide/templates/llm_entry_point.md +0 -0
  92. {project_guide-2.7.0 → project_guide-2.7.2}/project_guide/templates/project-guide/templates/modes/_header-common.md +0 -0
  93. {project_guide-2.7.0 → project_guide-2.7.2}/project_guide/templates/project-guide/templates/modes/_header-cycle.md +0 -0
  94. {project_guide-2.7.0 → project_guide-2.7.2}/project_guide/templates/project-guide/templates/modes/_header-sequence.md +0 -0
  95. {project_guide-2.7.0 → project_guide-2.7.2}/project_guide/templates/project-guide/templates/modes/archive-stories-mode.md +0 -0
  96. {project_guide-2.7.0 → project_guide-2.7.2}/project_guide/templates/project-guide/templates/modes/code-direct-mode.md +0 -0
  97. {project_guide-2.7.0 → project_guide-2.7.2}/project_guide/templates/project-guide/templates/modes/code-test-first-mode.md +0 -0
  98. {project_guide-2.7.0 → project_guide-2.7.2}/project_guide/templates/project-guide/templates/modes/debug-mode.md +0 -0
  99. {project_guide-2.7.0 → project_guide-2.7.2}/project_guide/templates/project-guide/templates/modes/default-mode.md +0 -0
  100. {project_guide-2.7.0 → project_guide-2.7.2}/project_guide/templates/project-guide/templates/modes/document-brand-mode.md +0 -0
  101. {project_guide-2.7.0 → project_guide-2.7.2}/project_guide/templates/project-guide/templates/modes/document-landing-mode.md +0 -0
  102. {project_guide-2.7.0 → project_guide-2.7.2}/project_guide/templates/project-guide/templates/modes/plan-concept-mode.md +0 -0
  103. {project_guide-2.7.0 → project_guide-2.7.2}/project_guide/templates/project-guide/templates/modes/plan-features-mode.md +0 -0
  104. {project_guide-2.7.0 → project_guide-2.7.2}/project_guide/templates/project-guide/templates/modes/plan-phase-mode.md +0 -0
  105. {project_guide-2.7.0 → project_guide-2.7.2}/project_guide/templates/project-guide/templates/modes/plan-production-phase-mode.md +0 -0
  106. {project_guide-2.7.0 → project_guide-2.7.2}/project_guide/templates/project-guide/templates/modes/plan-stories-mode.md +0 -0
  107. {project_guide-2.7.0 → project_guide-2.7.2}/project_guide/templates/project-guide/templates/modes/plan-tech-spec-mode.md +0 -0
  108. {project_guide-2.7.0 → project_guide-2.7.2}/project_guide/templates/project-guide/templates/modes/refactor-document-mode.md +0 -0
  109. {project_guide-2.7.0 → project_guide-2.7.2}/project_guide/templates/project-guide/templates/modes/refactor-plan-mode.md +0 -0
  110. {project_guide-2.7.0 → project_guide-2.7.2}/project_guide/templates/project-guide/templates/modes/scaffold-project-mode.md +0 -0
  111. {project_guide-2.7.0 → project_guide-2.7.2}/requirements-dev.txt +0 -0
  112. {project_guide-2.7.0 → project_guide-2.7.2}/tests/__init__.py +0 -0
  113. {project_guide-2.7.0 → project_guide-2.7.2}/tests/conftest.py +0 -0
  114. {project_guide-2.7.0 → project_guide-2.7.2}/tests/test_archive_stories_mode.py +0 -0
  115. {project_guide-2.7.0 → project_guide-2.7.2}/tests/test_config.py +0 -0
  116. {project_guide-2.7.0 → project_guide-2.7.2}/tests/test_integration.py +0 -0
  117. {project_guide-2.7.0 → project_guide-2.7.2}/tests/test_metadata.py +0 -0
  118. {project_guide-2.7.0 → project_guide-2.7.2}/tests/test_purge.py +0 -0
  119. {project_guide-2.7.0 → project_guide-2.7.2}/tests/test_render.py +0 -0
  120. {project_guide-2.7.0 → project_guide-2.7.2}/tests/test_runtime.py +0 -0
  121. {project_guide-2.7.0 → project_guide-2.7.2}/tests/test_sync.py +0 -0
@@ -32,6 +32,8 @@ build/
32
32
  /site/
33
33
 
34
34
  # project-guide
35
- docs/project-guide/**
36
- !docs/project-guide/go.md
37
- docs/project-guide/**/*.bak.*
35
+ /docs/project-guide/.metadata.yml
36
+ /docs/project-guide/README.md
37
+ /docs/project-guide/developer/
38
+ /docs/project-guide/templates/
39
+ /docs/project-guide/**/*.bak.*
@@ -1,8 +1,8 @@
1
1
  version: '2.0'
2
- installed_version: 2.6.0
2
+ installed_version: 2.7.2
3
3
  target_dir: docs/project-guide
4
4
  metadata_file: .metadata.yml
5
- current_mode: default
5
+ current_mode: debug
6
6
  test_first: false
7
7
  pyve_version: pyve version 2.6.2
8
8
  project_name: project-guide
@@ -7,6 +7,44 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [2.7.2] - 2026-05-19
11
+
12
+ Bug fix: `project-guide git-push` (and version/phase detection) silently dropped sub-numbered story IDs (`J.m.1`, `J.m.2`, …). When the latest `[Done]` story used the sub-numbered form, the wrapper fell back to the previous bare-letter heading and reported it as "already committed," blocking the push of the new story.
13
+
14
+ ### Fixed
15
+ - **`project_guide/stories.py:_STORY_RE`** — extended the story-ID character class from `[A-Z]\.[a-z]+` to `[A-Z]\.[a-z]+(?:\.\d+)?` so headings of the form `### Story J.m.1: ...` parse correctly. Without this, `_read_done_stories()` silently filtered such headings out and the wrapper picked the wrong "last [Done] story."
16
+ - **`project_guide/cli.py:_COMMIT_SUBJECT_STORY_ID_RE`** — extended in the same shape so the already-committed check recognizes commit subjects like `J.m.1: ...`. Without this fix the duplicate-detection path would have a second-layer hole even after the stories.md side was fixed.
17
+ - **`project_guide/actions.py:_VERSION_RE`** — extended in the same shape so `detect_latest_version()` no longer under-reports the highest version when the latest story uses the sub-numbered form.
18
+
19
+ ### Documented
20
+ - **`_phase-letters.md`** — added a "Sub-numbered stories" subsection covering both use cases: pre-implementation split (`J.m` was planned but split into `J.m.1`, `J.m.2` before any work; bare `J.m` heading dropped) and post-implementation follow-up (`J.m` shipped, then a bug or follow-on feature lands as `J.m.1` before proceeding to `J.n`). Flat single-level only — no cascading like `J.m.1.1`.
21
+
22
+ ### Tests
23
+ - New `tests/test_stories.py` covering `_STORY_RE`, `_read_done_stories()`, and `derive_commit_message()` — `stories.py` had no direct unit tests prior to this story (the test-coverage gap that let the bug ship in P.k).
24
+ - New `tests/test_cli.py` cases for both `git-push` scenarios (post-impl follow-up with bare `J.m` present; pre-impl split with no bare `J.m`) and the round-trip "sub-numbered story is already committed" path.
25
+ - New `tests/test_actions.py` case asserting `detect_latest_version()` picks up the version on a sub-numbered heading.
26
+
27
+ ## [2.7.1] - 2026-05-11
28
+
29
+ Compatibility fix for IDE-integrated LLM @-mention / fuzzy-search. The v2.6.0–v2.7.0 gitignore block used a clean `<target>/**` + `!<target>/go.md` negation pair — correct per the `.gitignore` spec, but **several IDE tools** (Cursor, parts of the VS Code fork ecosystem, certain LSP-based search backends) implement a subset of gitignore semantics that does not honor re-include negation. Those tools applied the broad `**` rule, hid `go.md`, and defeated the IDE-LLM-visibility constraint that's the entire reason `go.md` is tracked. P.l switches to a negation-free explicit-list form so simplistic parsers handle it reliably.
30
+
31
+ ### Changed
32
+ - **`project_guide/cli.py:_build_project_guide_block()`** — rewritten to enumerate the bundled template root (`_get_package_template_dir()`) and emit one anchored `/<target>/<child>[/]` line per top-level entry other than `go.md`, plus a trailing `/<target>/**/*.bak.*` defensive catch-all for top-level backups. The list is dynamic — new top-level files/directories in the bundled tree are picked up automatically by both the writer and the test helper that mirrors it. Default install now writes:
33
+ ```
34
+ # project-guide
35
+ /docs/project-guide/.metadata.yml
36
+ /docs/project-guide/README.md
37
+ /docs/project-guide/developer/
38
+ /docs/project-guide/templates/
39
+ /docs/project-guide/**/*.bak.*
40
+ ```
41
+ - **`project_guide/cli.py`** — `_recognized_block_lines()` replaced with `_is_recognized_block_line(line, target_dir)` predicate. Accepts the v2.7.1+ form (any line anchored at `/<target>/`) plus every prior legacy line (`<target>/**`, `!<target>/go.md`, `<target>/**/*.bak.*`, `<target>/go.md`). Existing v2.6.x/v2.7.0 installs heal cleanly to the v2.7.1 form on `init --force`; no multi-step migration required.
42
+ - **Tests** — `_EXPECTED_GITIGNORE_BLOCK` constant replaced with `_expected_gitignore_block()` helper that mirrors the writer's enumeration; new `test_init_force_rewrites_v261_three_line_block_to_explicit_list` for the v2.6.1/v2.7.0 → v2.7.1 migration path; the foreign-block test now seeds a line not anchored at `/<target>/` so the predicate correctly flags it as foreign.
43
+ - **Docs** — updated the gitignore prose in `docs/specs/features.md`, `docs/specs/tech-spec.md`, and `docs/specs/project-essentials.md` to document the three-version evolution (v2.6.0 → v2.6.1 → v2.7.1) and the "do not simplify back to negation" warning for future maintainers.
44
+
45
+ ### Migration
46
+ None required at the consumer level. The v2.6.x/v2.7.0 negation form and the v2.7.1 explicit-list form produce identical git-tracking outcomes (only `go.md` is tracked). The behavior change is purely in what `_ensure_gitignore_entry()` writes. Consumers whose IDE handles negation correctly may keep their existing block indefinitely; only consumers hitting the IDE bug actually benefit from running `project-guide init --force` to adopt the new form.
47
+
10
48
  ## [2.7.0] - 2026-05-11
11
49
 
12
50
  New top-level `project-guide git-push` command — a thin wrapper over [gitbetter](https://github.com/pointmatic/gitbetter)'s `git-push` that auto-derives the commit subject from the most-recently-completed-and-not-yet-committed story in `docs/specs/stories.md`. Developer-lane convenience only; the LLM still does not initiate commits.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: project-guide
3
- Version: 2.7.0
3
+ Version: 2.7.2
4
4
  Summary: Stay organized and in control with adaptive LLM workflow prompts.
5
5
  Project-URL: Homepage, https://github.com/pointmatic/project-guide
6
6
  Project-URL: Documentation, https://pointmatic.github.io/project-guide/
@@ -376,7 +376,15 @@ The bundled `templates/artifacts/pyve-essentials.md` artifact covers: two-enviro
376
376
  - `.project-guide.yml` is absent (let `init` bootstrap; the hook does not error).
377
377
  - The config fails to load (schema mismatch, parse error) — the subcommand surfaces the error with its own guidance.
378
378
 
379
- **Inverted gitignore policy.** `init`'s gitignore writer produces a canonical 3-line block under a `# project-guide` header: ignore everything under `target_dir` *except* `go.md` (tightened from the original 4-line v2.6.0 form in v2.6.1 — the explicit `.bak.*` line was redundant with the broader `**` rule). The remaining template tree is bundled static data that `heal` repopulates on first invocation. This eliminates the ~35-file install footprint from consumer-repo `git status` and PR reviews. Consumers migrating from a pre-Phase-P install run `project-guide init --force` to refresh the gitignore block; `git rm --cached` is the manual cleanup for previously tracked files. v2.6.0 installs heal to the v2.6.1 3-line form on the next `init --force`.
379
+ **Inverted gitignore policy.** `init`'s gitignore writer produces a canonical block under a `# project-guide` header that ignores everything under `target_dir` *except* `go.md`. The block has gone through three shapes (P.d P.j P.l):
380
+
381
+ - **v2.6.0 (P.d):** 4-line negation form (`<target>/**` + `!<target>/go.md` + redundant `<target>/**/*.bak.*`).
382
+ - **v2.6.1 (P.j):** 3-line negation form — dropped the redundant `.bak.*` line.
383
+ - **v2.7.1 (P.l):** **negation-free explicit-list form** — lists every top-level entry under `target_dir` other than `go.md`, plus a `<target>/**/*.bak.*` catch-all for top-level backups. The list is generated dynamically from the bundled template tree, so new top-level files/subdirectories added in future releases are picked up automatically.
384
+
385
+ P.l abandoned the negation form because several IDE-integrated tools (Cursor, parts of the VS Code fork ecosystem, certain LSP-based search backends) implement a subset of `.gitignore` semantics that does not honor re-include negation — they apply the broad `**` rule, hide `go.md` from @-mention / fuzzy-search, and defeat the IDE-LLM-visibility constraint that's the whole reason `go.md` is tracked.
386
+
387
+ Consumers migrating from a pre-Phase-P install run `project-guide init --force` to refresh the gitignore block; `git rm --cached` is the manual cleanup for previously tracked files. Existing v2.6.x/v2.7.0 installs heal to the v2.7.1 explicit-list form on the next `init --force` — every prior shape stays recognized by `_is_recognized_block_line()`. The track-only-`go.md` policy is unchanged across all three shapes; only the syntax that expresses it differs.
380
388
 
381
389
  ### FR-15: Story-Aware `git-push` Wrapper (gitbetter integration)
382
390
 
@@ -63,11 +63,17 @@ The hook is silent in the steady state (no drift → no output). It is recursion
63
63
 
64
64
  **Skip conditions:** the hook returns silently when `PROJECT_GUIDE_HEALING=1` is set, when `.project-guide.yml` is absent (let `init` bootstrap; `heal` would error otherwise), or when config load / drift detection fails. Missing `.project-guide.yml` is a hard error from the **`heal` subcommand itself** but a silent skip from the **hook** — the original subcommand surfaces the missing-config error with its own guidance.
65
65
 
66
- ### Inverted gitignore policy (added v2.6.0, tightened v2.6.1)
66
+ ### Inverted gitignore policy (added v2.6.0, tightened v2.6.1, IDE-compat reshape v2.7.1)
67
67
 
68
- `init`'s gitignore writer produces a canonical 3-line block: ignore everything under `target_dir` *except* `go.md` (Story P.d wrote the inversion; Story P.j / v2.6.1 dropped a redundant `.bak.*` line that the original block carried over from the pre-P.d shape). The remaining template tree is bundled static data that `heal` repopulates on first invocation in a fresh clone, so tracking it in the consumer repo would just add ~35 files of noise to `git status` and PR reviews.
68
+ `init`'s gitignore writer produces a canonical block that ignores everything under `target_dir` *except* `go.md`. The policy has gone through three shapes:
69
69
 
70
- **Idempotent rewrite:** `_ensure_gitignore_entry()` recognizes the v2.6.1 canonical block plus prior forms (the v2.6.0 4-line block, the pre-P.d `.bak.*`-only block, and the legacy `<target>/go.md` line) and rewrites all of them cleanly to the v2.6.1 3-line form. A foreign hand-customized block under a `# project-guide` header is left untouched with a stderr warning; migrate manually or run `init --force`.
70
+ - **v2.6.0 (P.d):** 4-line negation form (`<target>/**` + `!<target>/go.md` + redundant `<target>/**/*.bak.*`).
71
+ - **v2.6.1 (P.j):** 3-line negation form — dropped the redundant `.bak.*` line.
72
+ - **v2.7.1 (P.l):** **negation-free explicit-list form** — lists every top-level entry under `target_dir` other than `go.md`, plus a `<target>/**/*.bak.*` defensive catch-all. The list is dynamically enumerated from the bundled template root at write time, so new top-level files/directories added in future stories are picked up automatically.
73
+
74
+ P.l abandoned negation because several IDE-integrated tools (Cursor, parts of the VS Code fork ecosystem, certain LSP-based search backends) implement a subset of `.gitignore` semantics that does **not** honor re-include negation — they apply the broad `**` rule, hide `go.md` from @-mention / fuzzy-search, and defeat the IDE-LLM-visibility constraint the policy is trying to enforce. **Future maintainers: do not "simplify" back to `**` + `!` — the regression is invisible from git's perspective but breaks the IDE workflow.**
75
+
76
+ **Idempotent rewrite:** `_ensure_gitignore_entry()` uses `_is_recognized_block_line(line, target_dir)` as its "ours-vs-foreign" predicate. It accepts anything anchored at `/<target>/` (the v2.7.1+ form) plus every prior legacy form (v2.6.1 3-line, v2.6.0 4-line, pre-P.d `.bak.*`-only, legacy `<target>/go.md`). Any block whose lines all satisfy the predicate is rewritten cleanly to the current canonical form. A foreign hand-customized block under a `# project-guide` header is left untouched with a stderr warning; migrate manually or run `init --force`.
71
77
 
72
78
  ### IDE-LLM visibility constraint (added v2.6.0)
73
79
 
@@ -34,7 +34,7 @@ Collapse the `docs/project-guide/` install footprint in consumer repos to a sing
34
34
 
35
35
  **Implementation order:** `heal` core (P.a) → auto-hook (P.b) → `--no-input` semantics (P.c) → gitignore template flip (P.d). Production hardening items (P.e–P.h) are independent and can ship in any order. P.i is the doc-only story that bundles the v2.6.0 release.
36
36
 
37
- **Version cadence:** phase-bundled — stories P.a–P.i ran unversioned and shipped together as **v2.6.0** (P.i owned the single bundled bump). **Stories P.j and P.k were added post-bundle** and follow standard per-story cadence: **P.j → v2.6.1** (patch — gitignore-block tightening, a fix-up to P.d), **P.k → v2.7.0** (minor — new `git-push` wrapper command). Phases can be extended after their bundled release, but new stories then follow the standard per-story cadence rather than rejoining the (already-shipped) bundle.
37
+ **Version cadence:** phase-bundled — stories P.a–P.i ran unversioned and shipped together as **v2.6.0** (P.i owned the single bundled bump). **Stories P.j, P.k, and P.l were added post-bundle** and follow standard per-story cadence: **P.j → v2.6.1** (patch — gitignore-block tightening, a fix-up to P.d), **P.k → v2.7.0** (minor — new `git-push` wrapper command), **P.l → v2.7.1** (patch — switch the gitignore block from negation to explicit-list form for IDE compatibility). Phases can be extended after their bundled release, but new stories then follow the standard per-story cadence rather than rejoining the (already-shipped) bundle.
38
38
 
39
39
  ### Story P.a: Heal command with drift detection and create-missing semantics [Done]
40
40
 
@@ -262,6 +262,86 @@ This is a developer-lane convenience command. **The LLM still does not initiate
262
262
  - A parallel `project-guide git-tag` wrapper. gitbetter has one but the project's release process already uses `bump-version` + raw `git tag`, so the value is lower.
263
263
  - Pre-flight `pyve test` / `ruff check` gates before invoking gitbetter. The developer runs those during the story's normal cycle before marking `[Done]`, so re-running them at push time is redundant.
264
264
 
265
+ ### Story P.l: v2.7.1 Negation-free gitignore for IDE LLM compatibility [Done]
266
+
267
+ Switch the canonical `# project-guide` gitignore block from the v2.6.1/v2.7.0 negation form (`<target>/**` + `!<target>/go.md`) to a **negation-free explicit-list** form. Motivation: several IDE-integrated tools (Cursor, parts of the VS Code fork ecosystem, certain LSP-based search backends) implement a subset of `.gitignore` semantics that does not honor re-include negation — they apply the broad `**` rule, hide `go.md` from @-mention and fuzzy-search, and defeat the IDE-LLM-visibility constraint that's the entire reason `go.md` is tracked.
268
+
269
+ The new canonical form lists every gitignored top-level entry explicitly so no negation is required. The list is **dynamically generated** from the bundled template root, so new top-level files or subdirectories added in future stories are picked up automatically — no manual maintenance.
270
+
271
+ - [x] In `project_guide/cli.py:_build_project_guide_block()`: rewrite to enumerate `_get_package_template_dir()` and emit one `/<target>/<child>[/]` line per top-level entry except `go.md`, plus the existing `/<target>/**/*.bak.*` backup-catch-all. New canonical form for the default install layout:
272
+ ```
273
+ # project-guide
274
+ /docs/project-guide/.metadata.yml
275
+ /docs/project-guide/README.md
276
+ /docs/project-guide/developer/
277
+ /docs/project-guide/templates/
278
+ /docs/project-guide/**/*.bak.*
279
+ ```
280
+ Use a leading slash to anchor each rule at repo root; trailing slash on directories. Children sorted for deterministic output.
281
+ - [x] In `project_guide/cli.py`: update the recognized-block check to accept every form we've ever written: *(replaced `_recognized_block_lines()` with a `_is_recognized_block_line(line, target_dir)` predicate — cleaner because the v2.7.1+ "anything anchored at `/<target>/`" rule isn't expressible as a fixed set)*
282
+ - [x] New v2.7.1+ form: any line starting with `/<target>/`
283
+ - [x] v2.6.1 form: `<target>/**`, `!<target>/go.md`
284
+ - [x] v2.6.0 form: `<target>/**`, `!<target>/go.md`, `<target>/**/*.bak.*`
285
+ - [x] pre-P.d form: `<target>/**/*.bak.*` only
286
+ - [x] Legacy `<target>/go.md` line
287
+ - [x] In `tests/test_cli.py`:
288
+ - [x] Compute the expected block from the bundled tree via `_expected_gitignore_block()` helper instead of a hardcoded constant
289
+ - [x] Add `test_init_force_rewrites_v261_three_line_block_to_explicit_list` for the v2.6.1/v2.7.0 → v2.7.1 migration
290
+ - [x] Renamed the v2.6.0 → v2.7.1 migration test to `test_init_force_rewrites_v260_four_line_block_to_explicit_list` (was the v2.6.1-form rewrite test)
291
+ - [x] Foreign-block warning test updated to use a line not anchored at `/<target>/` so the new predicate flags it correctly
292
+ - [x] Idempotency test passes against the new canonical form (the seed builds from `_expected_gitignore_block()`)
293
+ - [x] All prior P.j/P.d migration tests continue to pass
294
+ - [x] Doc updates (bundled in this story):
295
+ - [x] `docs/specs/features.md` FR-14: amended the "Inverted gitignore policy" paragraph with the three-version evolution (v2.6.0 → v2.6.1 → v2.7.1) and the IDE-compat rationale
296
+ - [x] `docs/specs/tech-spec.md` § `.gitignore Management`: replaced the negation code block with the explicit-list form, added a "Why explicit list instead of `**` + `!go.md`?" paragraph plus an explicit "do not simplify back" warning, and a "Why the trailing `<target>/**/*.bak.*` line?" paragraph
297
+ - [x] `docs/specs/project-essentials.md`: amended the "Inverted gitignore policy" sub-section to cover the v2.7.1 form change and the "future maintainers: do not simplify back to `**` + `!`" warning
298
+ - [x] `README.md`: no user-facing prose changes needed (Quick Start footnote still reads correctly — "ignored except go.md")
299
+ - [x] Bump `project_guide/version.py`: `__version__ = "2.7.1"`
300
+ - [x] Bump `pyproject.toml`: `version = "2.7.1"`
301
+ - [x] Add `## [2.7.1] - <date>` CHANGELOG entry framed as a compatibility fix
302
+
303
+ **Migration:** none required by consumers. The new and old blocks produce identical git-tracking outcomes (only `go.md` is tracked). The behavior change is purely in what `_ensure_gitignore_entry()` writes. Existing v2.6.x/v2.7.0 installs heal to the v2.7.1 form on `init --force` because all prior shapes remain recognized. Consumers whose IDE handles negation correctly can keep their existing block indefinitely — only consumers hitting the IDE bug actually need to migrate.
304
+
305
+ **Why dynamic enumeration:** hardcoding the install footprint in `_build_project_guide_block()` means every new top-level template file or subdirectory requires a writer-code update. Enumerating `_get_package_template_dir()` at write time keeps the canonical block in sync with the bundled tree automatically. The trade-off is that the writer now reads from the package — not a real concern since `init` already does the same to copy the template tree.
306
+
307
+ **Out of scope:**
308
+ - An opt-back-in flag (`--gitignore-style=negation`). YAGNI until someone asks; the new form is strictly better for the documented IDE-LLM-visibility constraint.
309
+ - A `project-guide check` integrity rule that detects "consumer has tracked-but-should-be-ignored files under `target_dir`". Defer until there's a second integrity rule worth shipping (see Future > Integrity & Validation).
310
+
311
+ ### Story P.m: v2.7.2 Recognize sub-numbered story IDs (`J.m.1`) in regex sites [Done]
312
+
313
+ Reported bug: in a consumer project with the heading sequence `… J.l [Done], J.m.1 [Done]`, `project-guide git-push` printed `"Story J.l is already committed. Use 'git-push' directly for any follow-up commit."` and refused to push, even though `J.m.1` was the actual just-completed story.
314
+
315
+ **Root cause:** three regexes encoded the story-ID shape as `[A-Z]\.[a-z]+`, which silently fails to match the sub-numbered form. `stories.py:_STORY_RE` was the proximate cause — `_read_done_stories()` filtered the `J.m.1` heading out entirely, leaving `J.l` as the "last `[Done]`," and the commit-subject check then correctly observed that `J.l` had been committed. The other two sites (`cli.py:_COMMIT_SUBJECT_STORY_ID_RE`, `actions.py:_VERSION_RE`) had the same hole and were fixed in the same story to avoid a half-fix where a future code path re-introduced the bug from a different angle.
316
+
317
+ **Sub-numbered form** — `<phase>.<letter>.<digit>+`, flat single-level only (no cascading like `J.m.1.1`). Two use cases per the developer's intent:
318
+
319
+ - **Pre-implementation split:** `J.m` is planned but its scope is judged too large before any work begins; the heading is split into `J.m.1`, `J.m.2` and the bare `J.m` heading is dropped. Sequence: `… J.l, J.m.1, J.m.2, J.n, …`.
320
+ - **Post-implementation follow-up:** `J.m` ships, then a bug or follow-on feature must land before proceeding to `J.n`; the follow-up is added as `J.m.1` (and may cascade to `J.m.2`, `J.m.3`, …). Sequence: `… J.l, J.m, J.m.1, J.m.2, …, J.n, …`.
321
+
322
+ Both scenarios are exercised by the new test suite.
323
+
324
+ - [x] `project_guide/stories.py:_STORY_RE` — extend capture group to `[A-Z]\.[a-z]+(?:\.\d+)?` with comment cross-referencing `_phase-letters.md`
325
+ - [x] `project_guide/cli.py:_COMMIT_SUBJECT_STORY_ID_RE` — matching extension; updated comment to reflect the new shape so future readers see why the two regexes must move together
326
+ - [x] `project_guide/actions.py:_VERSION_RE` — matching extension; `detect_latest_version()` now picks up versions on sub-numbered headings
327
+ - [x] `project_guide/templates/project-guide/templates/modes/_phase-letters.md` — new "Sub-numbered stories" subsection documenting both scenarios and the flat-only constraint
328
+ - [x] Ran `project-guide update` to propagate the template change into `docs/project-guide/templates/modes/_phase-letters.md` (installed copy)
329
+ - [x] New `tests/test_stories.py` — first direct unit-test coverage for `stories.py` (the gap that let the bug ship in P.k). Covers `_STORY_RE` matches (plain, sub-numbered, multi-digit sub-number), `_read_done_stories()` for both scenarios, `derive_commit_message()` with sub-numbered ID, and the round-trip through `cli._COMMIT_SUBJECT_STORY_ID_RE`
330
+ - [x] `tests/test_cli.py` — new cases for both `git-push` scenarios (post-impl follow-up; pre-impl split with no bare `J.m`) and the "sub-numbered story is already committed" path that surfaces the second-layer regex hole
331
+ - [x] `tests/test_actions.py` — new case asserting `detect_latest_version()` picks up the version on a sub-numbered heading
332
+ - [x] Bump `project_guide/version.py`: `__version__ = "2.7.2"`
333
+ - [x] Bump `pyproject.toml`: `version = "2.7.2"`
334
+ - [x] Add `## [2.7.2] - 2026-05-19` entry to `CHANGELOG.md` framed as a bug fix with test-coverage backfill
335
+
336
+ **Migration:** none. The change is purely permissive — existing plain-letter IDs (`J.l`, `J.m`) continue to match. Consumers who never used sub-numbered IDs see no behavior change; consumers who did will see `git-push` and `detect_latest_version()` start handling those headings correctly.
337
+
338
+ **Why the docs change rides this story:** the sub-numbered form was already in use by consumers but undocumented in `_phase-letters.md`. Extending the regex without documenting the form would leave the next maintainer staring at an unexplained `(?:\.\d+)?` tail. Doc + code travel together.
339
+
340
+ **Out of scope:**
341
+ - Multi-level cascading (`J.m.1.1`). The developer's intent is flat; YAGNI until a consumer asks. Adding it later is a strict superset (`(?:\.\d+)+` instead of `(?:\.\d+)?`) and would be backwards compatible.
342
+ - A `project-guide check` rule that warns when a sub-numbered ID appears without a preceding plain-letter heading in non-pre-split contexts. The two valid scenarios cover everything seen in the wild; defer until there's evidence of misuse.
343
+ - Sub-numbered phase letters (`AA.1`). Phases don't carry the same pre-impl-split / post-impl-followup workflow that motivates the story-level sub-numbering, and no consumer has asked.
344
+
265
345
  ---
266
346
 
267
347
  ## Future
@@ -128,7 +128,7 @@ project-guide/
128
128
  | `purge` | Remove all project-guide files with confirmation |
129
129
 
130
130
  **Key functions:**
131
- - `_ensure_gitignore_entry(target_dir)` — writes the canonical `# project-guide` block: ignore everything under `target_dir` except `go.md` (3-line form as of P.j / v2.6.1). Idempotent. Recognized prior blocks (pre-P.d `.bak.*`-only form, v2.6.0 4-line form with the redundant `.bak.*` line, legacy `<target>/go.md` line) are rewritten cleanly to the v2.6.1 3-line form; foreign hand-customized content under a `# project-guide` header is left alone with a stderr warning.
131
+ - `_ensure_gitignore_entry(target_dir)` — writes the canonical `# project-guide` block: ignore everything under `target_dir` except `go.md` (negation-free explicit-list form as of P.l / v2.7.1, dynamically enumerated from the bundled template root). Idempotent. Recognized prior blocks (pre-P.d `.bak.*`-only form, v2.6.0 4-line form, v2.6.1/v2.7.0 3-line negation form, legacy `<target>/go.md` line) are rewritten cleanly to the v2.7.1 explicit-list form; foreign hand-customized content under a `# project-guide` header is left alone with a stderr warning. The recognized-line check is `_is_recognized_block_line(line, target_dir)` — accepts anything anchored at `/<target>/` plus the legacy negation entries.
132
132
  - `_copy_template_tree(src, dest, force)` — recursive copy preserving structure
133
133
  - `_migrate_config_if_needed()` — renames legacy `.project-guides.yml`
134
134
  - `_apply_heal(config, config_path)` — apply pending template syncs and re-render `go.md`. Sets `PROJECT_GUIDE_HEALING=1` in `os.environ` before doing any writes so nested subprocess invocations don't re-enter the auto-hook.
@@ -270,18 +270,28 @@ class ModeDefinition:
270
270
 
271
271
  ### `.gitignore` Management
272
272
 
273
- `init` writes a canonical 3-line block under a `# project-guide` comment header (Story P.d, tightened in P.j / v2.6.1):
273
+ `init` writes a canonical **negation-free explicit-list** block under a `# project-guide` comment header (Story P.d P.j P.l). For the default install layout the block is:
274
+
274
275
  ```
275
276
  # project-guide
276
- docs/project-guide/**
277
- !docs/project-guide/go.md
277
+ /docs/project-guide/.metadata.yml
278
+ /docs/project-guide/README.md
279
+ /docs/project-guide/developer/
280
+ /docs/project-guide/templates/
281
+ /docs/project-guide/**/*.bak.*
278
282
  ```
279
283
 
284
+ The list is **dynamically generated** at write time by enumerating the bundled template root (`_get_package_template_dir()`) and emitting one anchored line per top-level child other than `go.md`. New top-level files or subdirectories added in future releases are picked up automatically — no manual writer update required.
285
+
280
286
  **Why this shape:** every file under `target_dir` except `go.md` is bundled static data that `heal` (FR-14) repopulates on first invocation, so tracking the full template tree in the consumer repo would just add ~35 files of noise to `git status` and PR reviews. `go.md` itself **must remain tracked** because IDE-integrated LLMs (Cursor, Claude Code, etc.) typically hide gitignored files from the LLM's view, and the LLM's instruction to `Read docs/project-guide/go.md` requires the file to be visible. The repo-history value of `go.md` is incidental — the file churns on every mode switch — and that churn is the acceptable cost for LLM visibility.
281
287
 
282
- **Why not the explicit `.bak.*` line that v2.6.0 shipped?** It was carried over from the pre-P.d block during the policy inversion but is functionally redundant: the `<target>/**` rule already ignores backups produced by sync/heal under that subtree. P.j dropped the line; existing v2.6.0 installs heal cleanly to the 3-line form on `init --force` because `_recognized_block_lines()` still lists the v2.6.0 entry.
288
+ **Why explicit list instead of `**` + `!go.md`?** The cleaner negation form (used in v2.6.0–v2.7.0) is correct per the `.gitignore` spec, but several IDE-integrated tools (Cursor, parts of the VS Code fork ecosystem, certain LSP-based search backends) implement a subset of `.gitignore` semantics that does **not** honor re-include negation — they apply the broad `**` rule, hide `go.md` from @-mention search, and defeat the IDE-LLM-visibility constraint the policy is trying to enforce. P.l (v2.7.1) switched to the explicit-list form so no negation is required; tools with simplistic parsers handle anchored line-per-entry patterns reliably. Future maintainers: do **not** "simplify" back to `**` + `!` — the regression is invisible from git's perspective but breaks the IDE workflow that motivates tracking `go.md` in the first place.
289
+
290
+ **Why the trailing `<target>/**/*.bak.*` line?** Defensive coverage for backup files that `apply_file_update()` writes next to top-level synced files (e.g., `<target>/README.md.bak.<timestamp>`). Subdirectory backups are already covered by the per-directory entries; this catch-all is a simple recursive glob with no negation, so the IDEs handle it cleanly.
291
+
292
+ **Existing-block detection:** `_ensure_gitignore_entry()` is idempotent. The recognized-line predicate `_is_recognized_block_line(line, target_dir)` accepts any line starting with `/<target>/` (the v2.7.1+ anchor) plus the legacy negation-form lines (`<target>/**`, `!<target>/go.md`, `<target>/**/*.bak.*`, `<target>/go.md`). A block whose every non-empty line satisfies the predicate is rewritten cleanly to the current canonical form; any line that fails the predicate marks the block as hand-customized — the writer leaves it untouched and emits a stderr warning. A `.gitignore` with no `# project-guide` header gets the canonical block appended (separated by a blank line).
283
293
 
284
- **Existing-block detection:** `_ensure_gitignore_entry()` is idempotent. A recognized prior block (the v2.6.1 canonical form, the v2.6.0 4-line form, or the pre-P.d `.bak.*`-only form) is rewritten cleanly to the v2.6.1 3-line form. A foreign hand-customized block under a `# project-guide` header is left untouched with a stderr warning. A `.gitignore` with no `# project-guide` header gets the canonical block appended (separated by a blank line). Migration for pre-Phase-P consumer repos: `project-guide init --force` rewrites the block; `git rm --cached` is the manual cleanup for already-tracked files.
294
+ **Migration:** `project-guide init --force` rewrites prior blocks (pre-P.d `.bak.*`-only, v2.6.0 4-line, v2.6.1/v2.7.0 3-line) to the v2.7.1 explicit-list form in place. `git rm --cached` remains the manual cleanup for files already tracked under the old policy.
285
295
 
286
296
  ---
287
297
 
@@ -127,9 +127,11 @@ class Artifact:
127
127
  action=action,
128
128
  )
129
129
 
130
- # Matches `### Story <Phase>.<sub>: vMAJOR.MINOR.PATCH ...`
130
+ # Matches `### Story <Phase>.<sub>: vMAJOR.MINOR.PATCH ...` — the optional
131
+ # `(?:\.\d+)?` tail covers sub-numbered IDs (`J.m.1`, `J.m.2`, …) per the
132
+ # `_phase-letters.md` "Sub-numbered stories" rule.
131
133
  _VERSION_RE = re.compile(
132
- r"^###\s+Story\s+[A-Za-z]+\.[a-z]+:\s+v(\d+)\.(\d+)\.(\d+)",
134
+ r"^###\s+Story\s+[A-Za-z]+\.[a-z]+(?:\.\d+)?:\s+v(\d+)\.(\d+)\.(\d+)",
133
135
  re.M,
134
136
  )
135
137
 
@@ -312,39 +312,62 @@ _GITIGNORE_HEADER = "# project-guide"
312
312
  def _build_project_guide_block(target_dir: str) -> str:
313
313
  """Build the canonical project-guide gitignore block.
314
314
 
315
- Policy (Story P.d, tightened in P.j): everything under ``target_dir`` is
316
- gitignored except ``go.md`` (which the LLM reads, and IDE-integrated
317
- LLMs typically hide gitignored files from the LLM's view). The remaining
318
- template tree including ``.bak.*`` backup files — is bundled static
319
- data that ``heal`` repopulates on first invocation, so it does not need
320
- to be tracked in the consumer repo. P.j dropped the explicit ``.bak.*``
321
- line because the broad ``**`` rule already covers it; the old line is
322
- still recognized as ours so v2.6.0 installs heal to this shape on
323
- ``init --force``.
315
+ Policy (Story P.d, tightened in P.j, reshaped in P.l): everything under
316
+ ``target_dir`` is gitignored except ``go.md``. ``go.md`` must remain
317
+ tracked because IDE-integrated LLMs (Cursor, parts of the VS Code fork
318
+ ecosystem, several LSP-based search backends) typically hide gitignored
319
+ files from the LLM's @-mention / fuzzy-search view.
320
+
321
+ P.l (v2.7.1) abandons the cleaner ``<target>/**`` + ``!<target>/go.md``
322
+ shape because several of those same IDEs implement a subset of
323
+ ``.gitignore`` semantics that does not honor re-include negation —
324
+ they apply the broad ``**`` rule, hide ``go.md``, and defeat the
325
+ visibility constraint the policy is trying to enforce. The new form
326
+ lists every top-level entry under ``target_dir`` explicitly so no
327
+ negation is required. The list is enumerated from the bundled template
328
+ tree at write time, so future additions to the install footprint are
329
+ picked up automatically.
330
+
331
+ The trailing ``<target>/**/*.bak.*`` rule defensively ignores backup
332
+ files that ``apply_file_update`` writes next to top-level synced files
333
+ (subdirectory backups are already covered by the per-directory entries).
324
334
  """
325
- return (
326
- f"{_GITIGNORE_HEADER}\n"
327
- f"{target_dir}/**\n"
328
- f"!{target_dir}/go.md\n"
329
- )
330
-
331
-
332
- def _recognized_block_lines(target_dir: str) -> set[str]:
333
- """Lines that mark an existing block as one we wrote (safe to replace).
334
-
335
- Any block whose every non-empty line is in this set can be safely
336
- rewritten to the canonical form. A block containing anything outside
337
- this set has been hand-customized; we warn and leave it alone.
335
+ pkg_root = _get_package_template_dir()
336
+ entries: list[str] = [_GITIGNORE_HEADER]
337
+ for child in sorted(pkg_root.iterdir(), key=lambda p: p.name):
338
+ if child.name == "go.md":
339
+ continue
340
+ suffix = "/" if child.is_dir() else ""
341
+ entries.append(f"/{target_dir}/{child.name}{suffix}")
342
+ entries.append(f"/{target_dir}/**/*.bak.*")
343
+ return "\n".join(entries) + "\n"
344
+
345
+
346
+ def _is_recognized_block_line(line: str, target_dir: str) -> bool:
347
+ """Return True when ``line`` is one we plausibly wrote in any past version.
348
+
349
+ A block whose every non-empty line satisfies this predicate is treated
350
+ as ours and rewritten cleanly to the current canonical form; a block
351
+ containing anything that fails this predicate is left untouched with a
352
+ warning. Recognized forms (newest first):
353
+
354
+ - **v2.7.1+ explicit-list form (Story P.l):** any line starting with
355
+ ``/<target>/``. The leading slash anchors at repo root; we never
356
+ write unanchored lines, and there is nothing else we plausibly
357
+ generate under that anchor.
358
+ - **v2.6.1 form (Story P.j):** ``<target>/**`` and ``!<target>/go.md``.
359
+ - **v2.6.0 form (Story P.d):** the v2.6.1 lines plus ``<target>/**/*.bak.*``.
360
+ - **pre-P.d form:** ``<target>/**/*.bak.*`` only.
361
+ - **Legacy variants:** ``<target>/go.md`` (incorrectly gitignored, if
362
+ it ever appeared in the wild).
338
363
  """
339
- return {
340
- # Canonical lines as of P.j (v2.6.1).
364
+ if line.startswith(f"/{target_dir}/"):
365
+ return True
366
+ return line in {
341
367
  f"{target_dir}/**",
342
368
  f"!{target_dir}/go.md",
343
- # v2.6.0 canonical form (Story P.d) — kept recognized so v2.6.0
344
- # installs heal to the P.j 3-line form on `init --force`.
345
369
  f"{target_dir}/**/*.bak.*",
346
- # Legacy / pre-P.d variants we know about.
347
- f"{target_dir}/go.md", # incorrectly gitignored go.md, if it ever appeared
370
+ f"{target_dir}/go.md",
348
371
  }
349
372
 
350
373
 
@@ -388,8 +411,7 @@ def _ensure_gitignore_entry(target_dir: str) -> None:
388
411
  block_end += 1
389
412
 
390
413
  block_body = [lines[i].strip() for i in range(header_idx + 1, block_end)]
391
- recognized = _recognized_block_lines(target_dir)
392
- foreign = [bl for bl in block_body if bl and bl not in recognized]
414
+ foreign = [bl for bl in block_body if bl and not _is_recognized_block_line(bl, target_dir)]
393
415
 
394
416
  if foreign:
395
417
  click.secho(
@@ -1604,8 +1626,9 @@ def heal(no_input: bool):
1604
1626
 
1605
1627
  # Regex matching the story-ID prefix of a commit-subject line — used to detect
1606
1628
  # which stories have already been committed (Story P.k). Mirrors the
1607
- # `[A-Z]\.[a-z]+` shape of `_STORY_RE` in `stories.py`.
1608
- _COMMIT_SUBJECT_STORY_ID_RE = __import__("re").compile(r"^([A-Z]\.[a-z]+):\s")
1629
+ # `[A-Z]\.[a-z]+(?:\.\d+)?` shape of `_STORY_RE` in `stories.py` (the optional
1630
+ # `.\d+` tail covers sub-numbered IDs like `J.m.1`).
1631
+ _COMMIT_SUBJECT_STORY_ID_RE = __import__("re").compile(r"^([A-Z]\.[a-z]+(?:\.\d+)?):\s")
1609
1632
 
1610
1633
 
1611
1634
  def _get_committed_story_ids() -> set[str]:
@@ -17,8 +17,11 @@ from dataclasses import dataclass, field
17
17
  from pathlib import Path
18
18
 
19
19
  # Matches: ### Story N.a: v2.4.0 Some Title [Done]
20
+ # The optional `(?:\.\d+)?` tail captures sub-numbered IDs (`J.m.1`, `J.m.2`, …)
21
+ # used for pre-implementation splits or post-implementation follow-ups. See
22
+ # `_phase-letters.md` (Sub-numbered stories).
20
23
  _STORY_RE = re.compile(
21
- r"^### Story ([A-Z]\.[a-z]+): (.+) \[(Done|In Progress|Planned)\]\s*$",
24
+ r"^### Story ([A-Z]\.[a-z]+(?:\.\d+)?): (.+) \[(Done|In Progress|Planned)\]\s*$",
22
25
  re.MULTILINE,
23
26
  )
24
27
 
@@ -14,6 +14,15 @@ Within a phase, stories use lowercase letters following the same scheme: `A.a`,
14
14
 
15
15
  Examples: `A.a`, `A.b`, …, `A.z`, `A.aa`, `A.ab`, ….
16
16
 
17
+ ### Sub-numbered stories
18
+
19
+ A story may carry an optional numeric suffix — `J.m.1`, `J.m.2`, … — appended after the sub-letter. Sub-numbers are flat (no cascading like `J.m.1.1`) and start at `1`. Two situations use them:
20
+
21
+ - **Pre-implementation split.** When `J.m` is planned but its scope is judged too large before any work begins, the heading is split into `J.m.1`, `J.m.2`, … and the bare `J.m` heading is dropped. Sequence becomes `…, J.l, J.m.1, J.m.2, J.n, …`.
22
+ - **Post-implementation follow-up.** When `J.m` ships but a bug or follow-on feature must land before proceeding to `J.n`, the follow-up is added as `J.m.1` (and may cascade to `J.m.2`, `J.m.3`, …). Sequence becomes `…, J.l, J.m, J.m.1, J.m.2, …, J.n, …`.
23
+
24
+ Sub-numbered stories follow normal Version Cadence: each one that ships code takes its own bump.
25
+
17
26
  ### Continuing across archive boundaries
18
27
 
19
28
  When `stories.md` is archived (via `archive_stories` mode), the fresh `stories.md` starts empty — but phase letters do **not** reset. To determine the next phase letter:
@@ -12,4 +12,4 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
- __version__ = "2.7.0"
15
+ __version__ = "2.7.2"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "project-guide"
3
- version = "2.7.0"
3
+ version = "2.7.2"
4
4
  description = "Stay organized and in control with adaptive LLM workflow prompts."
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
@@ -150,6 +150,19 @@ def test_detect_latest_version_raises_when_none_found():
150
150
  detect_latest_version(text)
151
151
 
152
152
 
153
+ def test_detect_latest_version_recognizes_subnumbered_story_id():
154
+ """Sub-numbered story IDs (J.m.1, used for pre-impl splits or post-impl
155
+ follow-ups) must contribute to the latest-version computation. The
156
+ plain-letter regex silently dropped them, causing `detect_latest_version`
157
+ to under-report the highest version when the latest story used the
158
+ `.NN` form."""
159
+ text = (
160
+ "### Story J.l: v0.68.0 prior story [Done]\n\n"
161
+ "### Story J.m.1: v0.70.0 follow-up after J.m [Done]\n"
162
+ )
163
+ assert detect_latest_version(text) == (0, 70, 0)
164
+
165
+
153
166
  # ---------------------------------------------------------------------------
154
167
  # detect_latest_phase_letter
155
168
  # ---------------------------------------------------------------------------