project-kit-cli 1.1.0__tar.gz → 1.3.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 (108) hide show
  1. project_kit_cli-1.1.0/README.md → project_kit_cli-1.3.0/PKG-INFO +40 -6
  2. project_kit_cli-1.1.0/PKG-INFO → project_kit_cli-1.3.0/README.md +19 -25
  3. {project_kit_cli-1.1.0 → project_kit_cli-1.3.0}/pyproject.toml +5 -1
  4. {project_kit_cli-1.1.0 → project_kit_cli-1.3.0}/pyproject.toml.orig +5 -1
  5. project_kit_cli-1.3.0/src/project_kit/actions.py +459 -0
  6. project_kit_cli-1.3.0/src/project_kit/assets/docs/automate.md +86 -0
  7. project_kit_cli-1.3.0/src/project_kit/assets/skills/pk-automate/SKILL.md +61 -0
  8. {project_kit_cli-1.1.0 → project_kit_cli-1.3.0}/src/project_kit/assets/skills/pk-next/SKILL.md +18 -0
  9. {project_kit_cli-1.1.0 → project_kit_cli-1.3.0}/src/project_kit/assets/skills/pk-provision/SKILL.md +2 -1
  10. {project_kit_cli-1.1.0 → project_kit_cli-1.3.0}/src/project_kit/assets/skills/pk-quiet/SKILL.md +3 -0
  11. {project_kit_cli-1.1.0 → project_kit_cli-1.3.0}/src/project_kit/assets/skills/pk-start/SKILL.md +7 -3
  12. project_kit_cli-1.3.0/src/project_kit/assets/subagents/pk-provisioner.md +19 -0
  13. project_kit_cli-1.3.0/src/project_kit/assets/subagents/pk-runner.md +19 -0
  14. project_kit_cli-1.3.0/src/project_kit/assets/subagents/pk-tester.md +24 -0
  15. project_kit_cli-1.3.0/src/project_kit/claude_hooks.py +141 -0
  16. {project_kit_cli-1.1.0 → project_kit_cli-1.3.0}/src/project_kit/cli.py +180 -4
  17. {project_kit_cli-1.1.0 → project_kit_cli-1.3.0}/src/project_kit/config.py +13 -0
  18. {project_kit_cli-1.1.0 → project_kit_cli-1.3.0}/src/project_kit/context_files.py +11 -0
  19. {project_kit_cli-1.1.0 → project_kit_cli-1.3.0}/src/project_kit/doctor.py +7 -5
  20. {project_kit_cli-1.1.0 → project_kit_cli-1.3.0}/src/project_kit/entry.py +17 -3
  21. {project_kit_cli-1.1.0 → project_kit_cli-1.3.0}/src/project_kit/help.py +2 -0
  22. {project_kit_cli-1.1.0 → project_kit_cli-1.3.0}/src/project_kit/installer.py +13 -6
  23. project_kit_cli-1.3.0/src/project_kit/models.py +153 -0
  24. project_kit_cli-1.1.0/src/project_kit/claude_hooks.py +0 -100
  25. {project_kit_cli-1.1.0 → project_kit_cli-1.3.0}/LICENSE +0 -0
  26. {project_kit_cli-1.1.0 → project_kit_cli-1.3.0}/src/project_kit/__init__.py +0 -0
  27. {project_kit_cli-1.1.0 → project_kit_cli-1.3.0}/src/project_kit/__main__.py +0 -0
  28. {project_kit_cli-1.1.0 → project_kit_cli-1.3.0}/src/project_kit/agents.py +0 -0
  29. {project_kit_cli-1.1.0 → project_kit_cli-1.3.0}/src/project_kit/ai_attribution.py +0 -0
  30. {project_kit_cli-1.1.0 → project_kit_cli-1.3.0}/src/project_kit/analyzers.py +0 -0
  31. {project_kit_cli-1.1.0 → project_kit_cli-1.3.0}/src/project_kit/approval.py +0 -0
  32. {project_kit_cli-1.1.0 → project_kit_cli-1.3.0}/src/project_kit/assets/docs/agents.md +0 -0
  33. {project_kit_cli-1.1.0 → project_kit_cli-1.3.0}/src/project_kit/assets/docs/backlog-schema.md +0 -0
  34. {project_kit_cli-1.1.0 → project_kit_cli-1.3.0}/src/project_kit/assets/docs/example.md +0 -0
  35. {project_kit_cli-1.1.0 → project_kit_cli-1.3.0}/src/project_kit/assets/docs/flow.md +0 -0
  36. {project_kit_cli-1.1.0 → project_kit_cli-1.3.0}/src/project_kit/assets/docs/handoff.md +0 -0
  37. {project_kit_cli-1.1.0 → project_kit_cli-1.3.0}/src/project_kit/assets/docs/provisioning.md +0 -0
  38. {project_kit_cli-1.1.0 → project_kit_cli-1.3.0}/src/project_kit/assets/docs/quickstart.md +0 -0
  39. {project_kit_cli-1.1.0 → project_kit_cli-1.3.0}/src/project_kit/assets/docs/quiet.md +0 -0
  40. {project_kit_cli-1.1.0 → project_kit_cli-1.3.0}/src/project_kit/assets/docs/session.md +0 -0
  41. {project_kit_cli-1.1.0 → project_kit_cli-1.3.0}/src/project_kit/assets/docs/skills.md +0 -0
  42. {project_kit_cli-1.1.0 → project_kit_cli-1.3.0}/src/project_kit/assets/docs/statusline.md +0 -0
  43. {project_kit_cli-1.1.0 → project_kit_cli-1.3.0}/src/project_kit/assets/docs/upgrade.md +0 -0
  44. {project_kit_cli-1.1.0 → project_kit_cli-1.3.0}/src/project_kit/assets/hooks/commit-msg +0 -0
  45. {project_kit_cli-1.1.0 → project_kit_cli-1.3.0}/src/project_kit/assets/project/skills-catalog.yml +0 -0
  46. {project_kit_cli-1.1.0 → project_kit_cli-1.3.0}/src/project_kit/assets/project/workflow.yml +0 -0
  47. {project_kit_cli-1.1.0 → project_kit_cli-1.3.0}/src/project_kit/assets/skills/pk-amend/SKILL.md +0 -0
  48. {project_kit_cli-1.1.0 → project_kit_cli-1.3.0}/src/project_kit/assets/skills/pk-decide/SKILL.md +0 -0
  49. {project_kit_cli-1.1.0 → project_kit_cli-1.3.0}/src/project_kit/assets/skills/pk-discover/SKILL.md +0 -0
  50. {project_kit_cli-1.1.0 → project_kit_cli-1.3.0}/src/project_kit/assets/skills/pk-foundations/SKILL.md +0 -0
  51. {project_kit_cli-1.1.0 → project_kit_cli-1.3.0}/src/project_kit/assets/skills/pk-grill/SKILL.md +0 -0
  52. {project_kit_cli-1.1.0 → project_kit_cli-1.3.0}/src/project_kit/assets/skills/pk-handoff/SKILL.md +0 -0
  53. {project_kit_cli-1.1.0 → project_kit_cli-1.3.0}/src/project_kit/assets/skills/pk-import/SKILL.md +0 -0
  54. {project_kit_cli-1.1.0 → project_kit_cli-1.3.0}/src/project_kit/assets/skills/pk-plan/SKILL.md +0 -0
  55. {project_kit_cli-1.1.0 → project_kit_cli-1.3.0}/src/project_kit/assets/skills/pk-skills/SKILL.md +0 -0
  56. {project_kit_cli-1.1.0 → project_kit_cli-1.3.0}/src/project_kit/assets/skills/pk-verify/SKILL.md +0 -0
  57. {project_kit_cli-1.1.0 → project_kit_cli-1.3.0}/src/project_kit/assets/skills/pk-workflow/SKILL.md +0 -0
  58. {project_kit_cli-1.1.0 → project_kit_cli-1.3.0}/src/project_kit/assets/subagents/pk-reviewer.md +0 -0
  59. {project_kit_cli-1.1.0 → project_kit_cli-1.3.0}/src/project_kit/assets/templates/adr.md +0 -0
  60. {project_kit_cli-1.1.0 → project_kit_cli-1.3.0}/src/project_kit/assets/templates/backlog.example.json +0 -0
  61. {project_kit_cli-1.1.0 → project_kit_cli-1.3.0}/src/project_kit/assets/templates/brief.md +0 -0
  62. {project_kit_cli-1.1.0 → project_kit_cli-1.3.0}/src/project_kit/assets/templates/constitution-input.md +0 -0
  63. {project_kit_cli-1.1.0 → project_kit_cli-1.3.0}/src/project_kit/assets/templates/development-workflow.md +0 -0
  64. {project_kit_cli-1.1.0 → project_kit_cli-1.3.0}/src/project_kit/assets/templates/github/ISSUE_TEMPLATE/adr.yml +0 -0
  65. {project_kit_cli-1.1.0 → project_kit_cli-1.3.0}/src/project_kit/assets/templates/github/ISSUE_TEMPLATE/bug.yml +0 -0
  66. {project_kit_cli-1.1.0 → project_kit_cli-1.3.0}/src/project_kit/assets/templates/github/ISSUE_TEMPLATE/config.yml +0 -0
  67. {project_kit_cli-1.1.0 → project_kit_cli-1.3.0}/src/project_kit/assets/templates/github/ISSUE_TEMPLATE/epic.yml +0 -0
  68. {project_kit_cli-1.1.0 → project_kit_cli-1.3.0}/src/project_kit/assets/templates/github/ISSUE_TEMPLATE/feature.yml +0 -0
  69. {project_kit_cli-1.1.0 → project_kit_cli-1.3.0}/src/project_kit/assets/templates/github/pull_request_template.md +0 -0
  70. {project_kit_cli-1.1.0 → project_kit_cli-1.3.0}/src/project_kit/assets/templates/gitlab/issue_templates/adr.md +0 -0
  71. {project_kit_cli-1.1.0 → project_kit_cli-1.3.0}/src/project_kit/assets/templates/gitlab/issue_templates/bug.md +0 -0
  72. {project_kit_cli-1.1.0 → project_kit_cli-1.3.0}/src/project_kit/assets/templates/gitlab/issue_templates/epic.md +0 -0
  73. {project_kit_cli-1.1.0 → project_kit_cli-1.3.0}/src/project_kit/assets/templates/gitlab/issue_templates/feature.md +0 -0
  74. {project_kit_cli-1.1.0 → project_kit_cli-1.3.0}/src/project_kit/assets/templates/gitlab/merge_request_templates/Default.md +0 -0
  75. {project_kit_cli-1.1.0 → project_kit_cli-1.3.0}/src/project_kit/assets/templates/interview.md +0 -0
  76. {project_kit_cli-1.1.0 → project_kit_cli-1.3.0}/src/project_kit/backlog.py +0 -0
  77. {project_kit_cli-1.1.0 → project_kit_cli-1.3.0}/src/project_kit/fsutil.py +0 -0
  78. {project_kit_cli-1.1.0 → project_kit_cli-1.3.0}/src/project_kit/gh.py +0 -0
  79. {project_kit_cli-1.1.0 → project_kit_cli-1.3.0}/src/project_kit/glab.py +0 -0
  80. {project_kit_cli-1.1.0 → project_kit_cli-1.3.0}/src/project_kit/handoff.py +0 -0
  81. {project_kit_cli-1.1.0 → project_kit_cli-1.3.0}/src/project_kit/labels.py +0 -0
  82. {project_kit_cli-1.1.0 → project_kit_cli-1.3.0}/src/project_kit/logview.py +0 -0
  83. {project_kit_cli-1.1.0 → project_kit_cli-1.3.0}/src/project_kit/manifest.py +0 -0
  84. {project_kit_cli-1.1.0 → project_kit_cli-1.3.0}/src/project_kit/migrate.py +0 -0
  85. {project_kit_cli-1.1.0 → project_kit_cli-1.3.0}/src/project_kit/next_step.py +0 -0
  86. {project_kit_cli-1.1.0 → project_kit_cli-1.3.0}/src/project_kit/peek.py +0 -0
  87. {project_kit_cli-1.1.0 → project_kit_cli-1.3.0}/src/project_kit/procs.py +0 -0
  88. {project_kit_cli-1.1.0 → project_kit_cli-1.3.0}/src/project_kit/project_choice.py +0 -0
  89. {project_kit_cli-1.1.0 → project_kit_cli-1.3.0}/src/project_kit/providers/__init__.py +0 -0
  90. {project_kit_cli-1.1.0 → project_kit_cli-1.3.0}/src/project_kit/providers/github.py +0 -0
  91. {project_kit_cli-1.1.0 → project_kit_cli-1.3.0}/src/project_kit/providers/gitlab.py +0 -0
  92. {project_kit_cli-1.1.0 → project_kit_cli-1.3.0}/src/project_kit/provision.py +0 -0
  93. {project_kit_cli-1.1.0 → project_kit_cli-1.3.0}/src/project_kit/provision_gitlab.py +0 -0
  94. {project_kit_cli-1.1.0 → project_kit_cli-1.3.0}/src/project_kit/provision_issues.py +0 -0
  95. {project_kit_cli-1.1.0 → project_kit_cli-1.3.0}/src/project_kit/quiet.py +0 -0
  96. {project_kit_cli-1.1.0 → project_kit_cli-1.3.0}/src/project_kit/redact.py +0 -0
  97. {project_kit_cli-1.1.0 → project_kit_cli-1.3.0}/src/project_kit/schema.py +0 -0
  98. {project_kit_cli-1.1.0 → project_kit_cli-1.3.0}/src/project_kit/skills.py +0 -0
  99. {project_kit_cli-1.1.0 → project_kit_cli-1.3.0}/src/project_kit/sources.py +0 -0
  100. {project_kit_cli-1.1.0 → project_kit_cli-1.3.0}/src/project_kit/state.py +0 -0
  101. {project_kit_cli-1.1.0 → project_kit_cli-1.3.0}/src/project_kit/statusline.py +0 -0
  102. {project_kit_cli-1.1.0 → project_kit_cli-1.3.0}/src/project_kit/templating.py +0 -0
  103. {project_kit_cli-1.1.0 → project_kit_cli-1.3.0}/src/project_kit/trace.py +0 -0
  104. {project_kit_cli-1.1.0 → project_kit_cli-1.3.0}/src/project_kit/uninstall.py +0 -0
  105. {project_kit_cli-1.1.0 → project_kit_cli-1.3.0}/src/project_kit/updates.py +0 -0
  106. {project_kit_cli-1.1.0 → project_kit_cli-1.3.0}/src/project_kit/upgrade.py +0 -0
  107. {project_kit_cli-1.1.0 → project_kit_cli-1.3.0}/src/project_kit/userdocs.py +0 -0
  108. {project_kit_cli-1.1.0 → project_kit_cli-1.3.0}/src/project_kit/workflow.py +0 -0
@@ -1,7 +1,32 @@
1
+ Metadata-Version: 2.4
2
+ Name: project-kit-cli
3
+ Version: 1.3.0
4
+ Summary: Project definition and dev-workflow kit that hands off to Spec Kit
5
+ Keywords: project-management,spec-kit,claude-code,workflow,github
6
+ Author: Miguel Tomás
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Environment :: Console
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.14
15
+ Classifier: Topic :: Software Development
16
+ Requires-Dist: pyyaml>=6.0.3
17
+ Requires-Python: >=3.12
18
+ Project-URL: Homepage, https://miguelmatg.github.io/project-kit-site/
19
+ Project-URL: Documentation, https://miguelmatg.github.io/project-kit-site/
20
+ Description-Content-Type: text/markdown
21
+
1
22
  # project-kit
2
23
 
3
24
  [![PyPI](https://img.shields.io/pypi/v/project-kit-cli)](https://pypi.org/project/project-kit-cli/)
4
25
  [![License: MIT](https://img.shields.io/pypi/l/project-kit-cli)](https://pypi.org/project/project-kit-cli/)
26
+ [![Website](https://img.shields.io/badge/website-project--kit-0a66e4)](https://miguelmatg.github.io/project-kit-site/)
27
+
28
+ **Website:** [miguelmatg.github.io/project-kit-site](https://miguelmatg.github.io/project-kit-site/),
29
+ with the phases, what to ask your agent and the commands, in English and Spanish.
5
30
 
6
31
  **Everything that happens before the first line of code.** project-kit guides you, with an AI
7
32
  agent, through defining a project, deciding its stack and architecture, agreeing how the team
@@ -82,8 +107,13 @@ Outside the sequence: `/pk-next` (runs the sequence for you), `/pk-grill` (hard
82
107
  contradictions, offered before each approval), `/pk-import` (start from existing documents),
83
108
  `/pk-skills`, `/pk-amend`, `/pk-verify` and `/pk-quiet` (runs long commands and reads long logs
84
109
  through `pjkit run`, `logs` and `peek` so that only what failed reaches the AI; it pays off with
85
- long output, see `pjkit docs quiet`). Each phase ends with `pjkit approve <phase>`, which only
86
- you run. A small project can skip three phases with `pjkit init --mode quick`.
110
+ long output, see `pjkit docs quiet`) and `/pk-automate` (turns code the AI keeps writing into a
111
+ saved script it runs instead). Each phase ends with `pjkit approve <phase>`, which only you run.
112
+ A small project can skip three phases with `pjkit init --mode quick`.
113
+
114
+ In Claude Code, work that needs no judgment goes to subagents with their own context and, if you
115
+ want, a smaller model: `pk-tester` (tests and checks), `pk-provisioner` (an approved provision),
116
+ `pk-runner` (saved scripts) and `pk-reviewer` (verification). See `pjkit docs automate`.
87
117
 
88
118
  ## Commands
89
119
 
@@ -102,22 +132,26 @@ you run. A small project can skip three phases with `pjkit init --mode quick`.
102
132
  | `pjkit uninstall [--apply] [--purge]` | Undo `init`: remove what project-kit installed and you did not edit |
103
133
  | `pjkit run -- <command>` | Run a command and print a small report instead of all its output (`logs`, `peek` and `stats` go with it) |
104
134
  | `pjkit mode [quick\|full]` | Show or change how many phases the project goes through |
135
+ | `pjkit models [set]` | Which model each Claude Code subagent uses |
136
+ | `pjkit actions [scripts]` | Code the AI wrote again and again, and the scripts saved instead |
105
137
  | `pjkit context` | Write or refresh the project block in `CLAUDE.md` / `AGENTS.md` / `GEMINI.md` |
106
138
  | `pjkit skills list\|show\|mark` | Recommended third-party skills and what the project decided |
107
139
  | `pjkit statusline [--install]` | A line for Claude Code's status bar: model, context, limits, project phase |
108
- | `pjkit hook install\|remove` | Tell Claude Code the project's state at the start of each session |
140
+ | `pjkit hook install\|remove` | Tell Claude Code the project's state at the start of each session, and remember the code it writes |
109
141
  | `pjkit docs [topic]` | The guides |
110
142
  | `pjkit doctor` / `pjkit help` | Check the tools / a visual guide |
111
143
 
112
144
  Useful `init` options: `--from <path>`, `--mode quick|full`, `--no-context`, `--project-name`,
113
145
  `--language` (of the generated documents), `--agents claude,codex,...`, `--agent claude|none`,
114
- `--provider github|gitlab|none` and `--ai-coauthor keep|remove`.
146
+ `--provider github|gitlab|none`, `--ai-coauthor keep|remove` and `--models recommended|inherit`.
115
147
 
116
148
  ## What it adds to your repository
117
149
 
118
150
  ```
119
151
  .project-kit/ config.yml, state.json, workflow.yml, templates/, handoff/
120
152
  .claude/skills/pk-*/ the skills (.agents/skills for the other agents)
153
+ .claude/agents/pk-* the subagents (Claude Code only)
154
+ .project-kit/scripts/ scripts saved with /pk-automate (catalog: .project-kit/scripts.yml)
121
155
  docs/project/ brief.md, roadmap.md (created by the phases)
122
156
  docs/adr/ one file per decision (created by the phases)
123
157
  .githooks/commit-msg only if you removed the AI co-author
@@ -129,8 +163,8 @@ date and `pjkit upgrade --apply` brings it up to date without touching what you
129
163
 
130
164
  ## Documentation
131
165
 
132
- `pjkit docs` lists every guide: `quickstart`, `example`, `session`, `flow`, `quiet`, `upgrade`,
133
- `provisioning`, `handoff`, `backlog-schema`, `skills`, `statusline` and `agents`.
166
+ `pjkit docs` lists every guide: `quickstart`, `example`, `session`, `flow`, `quiet`, `automate`,
167
+ `upgrade`, `provisioning`, `handoff`, `backlog-schema`, `skills`, `statusline` and `agents`.
134
168
 
135
169
  ## License
136
170
 
@@ -1,26 +1,11 @@
1
- Metadata-Version: 2.4
2
- Name: project-kit-cli
3
- Version: 1.1.0
4
- Summary: Project definition and dev-workflow kit that hands off to Spec Kit
5
- Keywords: project-management,spec-kit,claude-code,workflow,github
6
- Author: Miguel Tomás
7
- License-Expression: MIT
8
- License-File: LICENSE
9
- Classifier: Development Status :: 3 - Alpha
10
- Classifier: Environment :: Console
11
- Classifier: Intended Audience :: Developers
12
- Classifier: Operating System :: OS Independent
13
- Classifier: Programming Language :: Python :: 3.12
14
- Classifier: Programming Language :: Python :: 3.14
15
- Classifier: Topic :: Software Development
16
- Requires-Dist: pyyaml>=6.0.3
17
- Requires-Python: >=3.12
18
- Description-Content-Type: text/markdown
19
-
20
1
  # project-kit
21
2
 
22
3
  [![PyPI](https://img.shields.io/pypi/v/project-kit-cli)](https://pypi.org/project/project-kit-cli/)
23
4
  [![License: MIT](https://img.shields.io/pypi/l/project-kit-cli)](https://pypi.org/project/project-kit-cli/)
5
+ [![Website](https://img.shields.io/badge/website-project--kit-0a66e4)](https://miguelmatg.github.io/project-kit-site/)
6
+
7
+ **Website:** [miguelmatg.github.io/project-kit-site](https://miguelmatg.github.io/project-kit-site/),
8
+ with the phases, what to ask your agent and the commands, in English and Spanish.
24
9
 
25
10
  **Everything that happens before the first line of code.** project-kit guides you, with an AI
26
11
  agent, through defining a project, deciding its stack and architecture, agreeing how the team
@@ -101,8 +86,13 @@ Outside the sequence: `/pk-next` (runs the sequence for you), `/pk-grill` (hard
101
86
  contradictions, offered before each approval), `/pk-import` (start from existing documents),
102
87
  `/pk-skills`, `/pk-amend`, `/pk-verify` and `/pk-quiet` (runs long commands and reads long logs
103
88
  through `pjkit run`, `logs` and `peek` so that only what failed reaches the AI; it pays off with
104
- long output, see `pjkit docs quiet`). Each phase ends with `pjkit approve <phase>`, which only
105
- you run. A small project can skip three phases with `pjkit init --mode quick`.
89
+ long output, see `pjkit docs quiet`) and `/pk-automate` (turns code the AI keeps writing into a
90
+ saved script it runs instead). Each phase ends with `pjkit approve <phase>`, which only you run.
91
+ A small project can skip three phases with `pjkit init --mode quick`.
92
+
93
+ In Claude Code, work that needs no judgment goes to subagents with their own context and, if you
94
+ want, a smaller model: `pk-tester` (tests and checks), `pk-provisioner` (an approved provision),
95
+ `pk-runner` (saved scripts) and `pk-reviewer` (verification). See `pjkit docs automate`.
106
96
 
107
97
  ## Commands
108
98
 
@@ -121,22 +111,26 @@ you run. A small project can skip three phases with `pjkit init --mode quick`.
121
111
  | `pjkit uninstall [--apply] [--purge]` | Undo `init`: remove what project-kit installed and you did not edit |
122
112
  | `pjkit run -- <command>` | Run a command and print a small report instead of all its output (`logs`, `peek` and `stats` go with it) |
123
113
  | `pjkit mode [quick\|full]` | Show or change how many phases the project goes through |
114
+ | `pjkit models [set]` | Which model each Claude Code subagent uses |
115
+ | `pjkit actions [scripts]` | Code the AI wrote again and again, and the scripts saved instead |
124
116
  | `pjkit context` | Write or refresh the project block in `CLAUDE.md` / `AGENTS.md` / `GEMINI.md` |
125
117
  | `pjkit skills list\|show\|mark` | Recommended third-party skills and what the project decided |
126
118
  | `pjkit statusline [--install]` | A line for Claude Code's status bar: model, context, limits, project phase |
127
- | `pjkit hook install\|remove` | Tell Claude Code the project's state at the start of each session |
119
+ | `pjkit hook install\|remove` | Tell Claude Code the project's state at the start of each session, and remember the code it writes |
128
120
  | `pjkit docs [topic]` | The guides |
129
121
  | `pjkit doctor` / `pjkit help` | Check the tools / a visual guide |
130
122
 
131
123
  Useful `init` options: `--from <path>`, `--mode quick|full`, `--no-context`, `--project-name`,
132
124
  `--language` (of the generated documents), `--agents claude,codex,...`, `--agent claude|none`,
133
- `--provider github|gitlab|none` and `--ai-coauthor keep|remove`.
125
+ `--provider github|gitlab|none`, `--ai-coauthor keep|remove` and `--models recommended|inherit`.
134
126
 
135
127
  ## What it adds to your repository
136
128
 
137
129
  ```
138
130
  .project-kit/ config.yml, state.json, workflow.yml, templates/, handoff/
139
131
  .claude/skills/pk-*/ the skills (.agents/skills for the other agents)
132
+ .claude/agents/pk-* the subagents (Claude Code only)
133
+ .project-kit/scripts/ scripts saved with /pk-automate (catalog: .project-kit/scripts.yml)
140
134
  docs/project/ brief.md, roadmap.md (created by the phases)
141
135
  docs/adr/ one file per decision (created by the phases)
142
136
  .githooks/commit-msg only if you removed the AI co-author
@@ -148,8 +142,8 @@ date and `pjkit upgrade --apply` brings it up to date without touching what you
148
142
 
149
143
  ## Documentation
150
144
 
151
- `pjkit docs` lists every guide: `quickstart`, `example`, `session`, `flow`, `quiet`, `upgrade`,
152
- `provisioning`, `handoff`, `backlog-schema`, `skills`, `statusline` and `agents`.
145
+ `pjkit docs` lists every guide: `quickstart`, `example`, `session`, `flow`, `quiet`, `automate`,
146
+ `upgrade`, `provisioning`, `handoff`, `backlog-schema`, `skills`, `statusline` and `agents`.
153
147
 
154
148
  ## License
155
149
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "project-kit-cli"
3
- version = "1.1.0"
3
+ version = "1.3.0"
4
4
  description = "Project definition and dev-workflow kit that hands off to Spec Kit"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -27,6 +27,10 @@ dependencies = ["pyyaml>=6.0.3"]
27
27
  [[project.authors]]
28
28
  name = "Miguel Tomás"
29
29
 
30
+ [project.urls]
31
+ Homepage = "https://miguelmatg.github.io/project-kit-site/"
32
+ Documentation = "https://miguelmatg.github.io/project-kit-site/"
33
+
30
34
  [project.scripts]
31
35
  pjkit = "project_kit.entry:main"
32
36
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "project-kit-cli"
3
- version = "1.1.0"
3
+ version = "1.3.0"
4
4
  description = "Project definition and dev-workflow kit that hands off to Spec Kit"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -21,6 +21,10 @@ dependencies = [
21
21
  "pyyaml>=6.0.3",
22
22
  ]
23
23
 
24
+ [project.urls]
25
+ Homepage = "https://miguelmatg.github.io/project-kit-site/"
26
+ Documentation = "https://miguelmatg.github.io/project-kit-site/"
27
+
24
28
  [project.scripts]
25
29
  pjkit = "project_kit.entry:main"
26
30
 
@@ -0,0 +1,459 @@
1
+ """The memory of actions: notice when the AI writes the same code again and again.
2
+
3
+ An agent asked for the same kind of thing (resize an image, read a web page, convert a file)
4
+ writes much the same script every time, and every time pays for it in tokens. Claude Code calls
5
+ ``pjkit hook post-tool-use`` after each Bash, PowerShell and Write call (see ``claude_hooks``);
6
+ this module keeps, for every piece of code that looks like a script, what it uses: the modules
7
+ it imports, the functions and methods it calls, the commands and long options it runs. Two
8
+ pieces of code that use much the same are the same action, even when the model named its
9
+ variables differently or changed the URL, the path or the size this time. When a group
10
+ has been written ``REPEATS`` times, the hook tells the agent once, and the ``pk-automate`` skill
11
+ offers the user to save it as a script. What the user decided is remembered: a declined group is
12
+ never offered again, a saved one is listed at the start of each session so the agent runs it
13
+ instead of writing it.
14
+
15
+ The memory is local (``.project-kit/actions/``, ignored by git). The scripts and their catalog
16
+ (``.project-kit/scripts/`` and ``.project-kit/scripts.yml``) are the project's and are versioned.
17
+
18
+ The hook runs after every command, so this module imports nothing heavy: no yaml, no config.
19
+ Two hooks running at once can lose one occurrence; a count that is one short does no harm.
20
+ """
21
+
22
+ import hashlib
23
+ import json
24
+ import re
25
+ from dataclasses import asdict, dataclass, field
26
+ from datetime import UTC, datetime
27
+ from pathlib import Path
28
+ from typing import Any
29
+
30
+ from project_kit.fsutil import atomic_write_text
31
+
32
+ PROJECT_DIR = ".project-kit" # config.PROJECT_DIR, not imported: config needs yaml
33
+ MEMORY = Path(PROJECT_DIR) / "actions" / "memory.json"
34
+ SCRIPTS_DIR = Path(PROJECT_DIR) / "scripts"
35
+ CATALOG = Path(PROJECT_DIR) / "scripts.yml"
36
+ HOOK_COMMAND = "pjkit hook post-tool-use"
37
+
38
+ # How many times something is written before it is offered as a script.
39
+ REPEATS = 3
40
+ # The same thing again in the same session within this time is a retry, not a new occurrence.
41
+ RETRY_SECONDS = 300
42
+ # How alike two pieces of code must be to be "the same action": the share of what they use in
43
+ # common (Jaccard). Code that uses fewer than MIN_FEATURES things only matches its exact shape.
44
+ SIMILAR = 0.6
45
+ MIN_FEATURES = 4
46
+ MAX_GROUPS = 300
47
+ MAX_EXAMPLE = 4000
48
+
49
+ NEW, OFFERED, SAVED, DECLINED = "new", "offered", "saved", "declined"
50
+ STATUSES = (NEW, OFFERED, SAVED, DECLINED)
51
+
52
+ _SCRIPT_SUFFIXES = {".py", ".js", ".mjs", ".cjs", ".ts", ".sh", ".bash", ".ps1", ".rb", ".pl"}
53
+ _SCRATCH_PARTS = {"tmp", "temp", "scratch", "scratchpad"}
54
+ _INLINE = re.compile(
55
+ r"(?:^|[\s;&|(])(?:python3?|py|uv run(?: --\S+)* python3?|node|deno|ruby|perl|php|bun)"
56
+ r"(?:\.exe)?\s+(?:-c|-e|--eval|eval|-)(?:\s|$)|<<-?\s*['\"]?\w+|@['\"]\s*$",
57
+ re.MULTILINE,
58
+ )
59
+
60
+ _URL = re.compile(r"\b[a-z][a-z0-9+.-]*://\S+", re.IGNORECASE)
61
+ _STRING = re.compile(
62
+ r'"""[\s\S]*?"""|\'\'\'[\s\S]*?\'\'\'|"(?:\\.|[^"\\\n])*"|\'(?:\\.|[^\'\\\n])*\''
63
+ )
64
+ _COMMENT = re.compile(r"(?m)^\s*(?:#|//|REM\b).*$")
65
+ _PATH = re.compile(r"(?<![\w.])(?:[A-Za-z]:)?(?:\.{0,2}[/\\])[\w.@~+-]+(?:[/\\][\w.@~+-]*)*")
66
+ _NUMBER = re.compile(r"\b\d+(?:\.\d+)?\b")
67
+ _TOKEN = re.compile(r"\w+|[^\w\s]")
68
+
69
+ # What a piece of code uses, read from the code itself: no parser, so it works for any language.
70
+ _IMPORT = re.compile(
71
+ r"""\bimport\s+([\w.]+(?:\s*,\s*[\w.]+)*)|\bfrom\s+([\w.]+)\s+import"""
72
+ r"""|require\(\s*['"]([^'"]+)|\bfrom\s+['"]([^'"]+)['"]|Import-Module\s+([\w.-]+)"""
73
+ )
74
+ _ATTR = re.compile(r"\.\s*([A-Za-z_]\w*)")
75
+ _CALL = re.compile(r"\b([A-Za-z_][\w-]*)\s*\(")
76
+ _PIPED = re.compile(r"(?:^|\||&&|;|\$\()\s*([a-z][\w-]*)")
77
+ _FLAG = re.compile(r"(?<![\w-])(--[a-z][\w-]*)")
78
+ # Used by nearly every script, so they say nothing about what this one does.
79
+ _COMMON = frozenset(
80
+ "print len open str int float bool list dict set tuple range enumerate isinstance sorted "
81
+ "format join split strip append extend items keys values read write close exit main self "
82
+ "import from if for while in not and or return with def class function const let var echo "
83
+ "cd cat python python3 py node uv run".split()
84
+ )
85
+
86
+
87
+ @dataclass
88
+ class Group:
89
+ id: str
90
+ kind: str # bash | write
91
+ count: int
92
+ first: str
93
+ last: str
94
+ session: str
95
+ status: str = NEW
96
+ script: str = "" # name in the catalog, once saved
97
+ example: str = "" # the latest occurrence, without secrets, cut at MAX_EXAMPLE
98
+ shape: str = "" # hash of the normalized code
99
+ features: list[str] = field(default_factory=list)
100
+
101
+
102
+ @dataclass
103
+ class Memory:
104
+ groups: list[Group] = field(default_factory=list)
105
+
106
+ def find(self, group_id: str) -> Group | None:
107
+ return next((g for g in self.groups if g.id == group_id), None)
108
+
109
+
110
+ @dataclass(frozen=True)
111
+ class Action:
112
+ kind: str
113
+ code: str
114
+
115
+
116
+ def project_root(start: Path) -> Path | None:
117
+ for folder in (start, *start.parents):
118
+ if (folder / PROJECT_DIR / "config.yml").is_file():
119
+ return folder
120
+ return None
121
+
122
+
123
+ # --- what counts as code ---------------------------------------------------------------------
124
+
125
+
126
+ def _lines(text: str) -> int:
127
+ return sum(1 for line in text.splitlines() if line.strip())
128
+
129
+
130
+ def _is_scratch(path: Path, root: Path) -> bool:
131
+ """A script written outside the project or in a temporary folder: a throwaway, not the
132
+ project's own code (which is edited over and over on purpose)."""
133
+ try:
134
+ rel = path.resolve().relative_to(root.resolve())
135
+ except ValueError:
136
+ return True
137
+ return any(part.lower() in _SCRATCH_PARTS for part in rel.parts[:-1])
138
+
139
+
140
+ def extract(payload: dict[str, Any], root: Path) -> Action | None:
141
+ """The code in a tool call, if it looks like a script someone may want to keep."""
142
+ tool = payload.get("tool_name")
143
+ data = payload.get("tool_input")
144
+ if not isinstance(data, dict):
145
+ return None
146
+ if tool in ("Bash", "PowerShell"):
147
+ code = data.get("command")
148
+ if not isinstance(code, str):
149
+ return None
150
+ stripped = code.strip()
151
+ if stripped.startswith("pjkit") or SCRIPTS_DIR.as_posix() in code.replace("\\", "/"):
152
+ return None # already one of ours
153
+ inline = bool(_INLINE.search(code)) and len(stripped) >= 80
154
+ return Action("bash", code) if inline or _lines(code) >= 4 else None
155
+ if tool == "Write":
156
+ path, code = data.get("file_path"), data.get("content")
157
+ if not (isinstance(path, str) and isinstance(code, str)):
158
+ return None
159
+ target = Path(path)
160
+ if target.suffix.lower() not in _SCRIPT_SUFFIXES or _lines(code) < 5:
161
+ return None
162
+ if SCRIPTS_DIR.as_posix() in path.replace("\\", "/"):
163
+ return None
164
+ return Action("write", code) if _is_scratch(target, root) else None
165
+ return None
166
+
167
+
168
+ # --- what "the same" means -------------------------------------------------------------------
169
+
170
+
171
+ def normalize(code: str) -> str:
172
+ """The shape of the code: the values that change from one request to the next are gone."""
173
+ text = _URL.sub(" U ", code)
174
+ text = _STRING.sub(" S ", text)
175
+ text = _COMMENT.sub("", text)
176
+ text = _PATH.sub(" P ", text)
177
+ text = _NUMBER.sub("0", text)
178
+ return " ".join(_TOKEN.findall(text))
179
+
180
+
181
+ def shape_hash(code: str) -> str:
182
+ return hashlib.sha1(normalize(code).encode()).hexdigest()[:8]
183
+
184
+
185
+ def features(code: str) -> list[str]:
186
+ """What the code uses: modules, called functions and methods, commands, long options."""
187
+ found = set()
188
+ for match in _IMPORT.finditer(code):
189
+ names = next(g for g in match.groups() if g)
190
+ for name in names.split(","):
191
+ found.add("import:" + re.split(r"[./]", name.strip().lstrip("@"))[0])
192
+ body = _COMMENT.sub("", _STRING.sub(" S ", _URL.sub(" U ", code)))
193
+ first = body.strip().splitlines()[0] if body.strip() else ""
194
+ found |= {"." + name for name in _ATTR.findall(body)}
195
+ found |= {name + "()" for name in _CALL.findall(body)}
196
+ found |= {"$" + name for name in _PIPED.findall(first)}
197
+ found |= set(_FLAG.findall(body))
198
+ return sorted(f for f in found if f.split(":")[-1].strip(".()$").lower() not in _COMMON)
199
+
200
+
201
+ def similarity(a: list[str], b: list[str]) -> float:
202
+ sa, sb = set(a), set(b)
203
+ if len(sa) < MIN_FEATURES or len(sb) < MIN_FEATURES:
204
+ return 0.0
205
+ return len(sa & sb) / len(sa | sb)
206
+
207
+
208
+ def _same(group: "Group", shape: str, uses: list[str]) -> float:
209
+ return 1.0 if group.shape == shape else similarity(uses, group.features)
210
+
211
+
212
+ # --- the memory ------------------------------------------------------------------------------
213
+
214
+
215
+ def load(root: Path) -> Memory:
216
+ try:
217
+ data = json.loads((root / MEMORY).read_text(encoding="utf-8"))
218
+ groups = [Group(**g) for g in data["groups"]]
219
+ except (OSError, ValueError, KeyError, TypeError):
220
+ return Memory() # missing or damaged: start again, it is only a memory
221
+ return Memory([g for g in groups if g.status in STATUSES])
222
+
223
+
224
+ def save(root: Path, memory: Memory) -> None:
225
+ folder = root / MEMORY.parent
226
+ folder.mkdir(parents=True, exist_ok=True)
227
+ ignore = folder / ".gitignore"
228
+ if not ignore.exists():
229
+ ignore.write_text("*\n", encoding="utf-8")
230
+ if len(memory.groups) > MAX_GROUPS: # forget what was seen once, oldest first
231
+ once = sorted((g for g in memory.groups if g.count == 1 and g.status == NEW),
232
+ key=lambda g: g.last) # fmt: skip
233
+ drop = {id(g) for g in once[: len(memory.groups) - MAX_GROUPS]}
234
+ memory.groups = [g for g in memory.groups if id(g) not in drop]
235
+ body = {"schema_version": 1, "groups": [asdict(g) for g in memory.groups]}
236
+ atomic_write_text(root / MEMORY, json.dumps(body, ensure_ascii=False) + "\n")
237
+
238
+
239
+ def _redact(text: str) -> str:
240
+ from project_kit.redact import redact # only when something is recorded
241
+
242
+ return redact(text)
243
+
244
+
245
+ def record(root: Path, action: Action, session: str, now: datetime) -> tuple[Group, bool]:
246
+ """Add one occurrence. Returns its group and whether it has just become worth offering
247
+ (it then counts as offered, so it is offered once)."""
248
+ memory = load(root)
249
+ shape, uses = shape_hash(action.code), features(action.code)
250
+ stamp = now.isoformat(timespec="seconds")
251
+ # Any kind: the same script may be written to a file one day and run inline the next.
252
+ best = max(memory.groups, key=lambda g: _same(g, shape, uses), default=None)
253
+ if best is None or _same(best, shape, uses) < SIMILAR:
254
+ best = Group(shape, action.kind, 0, stamp, stamp, session, shape=shape, features=uses)
255
+ while memory.find(best.id): # same shape as a group that grew apart from it: rare
256
+ best.id = hashlib.sha1(best.id.encode()).hexdigest()[:8]
257
+ memory.groups.append(best)
258
+ retry = (
259
+ best.count > 0
260
+ and best.session == session
261
+ and (now - datetime.fromisoformat(best.last)).total_seconds() < RETRY_SECONDS
262
+ )
263
+ if not retry:
264
+ best.count += 1
265
+ best.last, best.session = stamp, session
266
+ best.example = _redact(action.code)[:MAX_EXAMPLE]
267
+ offer = best.status == NEW and best.count >= REPEATS
268
+ if offer:
269
+ best.status = OFFERED
270
+ save(root, memory)
271
+ return best, offer
272
+
273
+
274
+ def notice(group: Group) -> str:
275
+ return (
276
+ f"project-kit: this code (action {group.id}) has now been written {group.count} times "
277
+ "in this project. When the user's current request is done, follow the pk-automate skill "
278
+ "to offer to save it as a script, so that next time it is run instead of written again."
279
+ )
280
+
281
+
282
+ def post_tool_use(payload: dict[str, Any], cwd: Path, now: datetime | None = None) -> str:
283
+ """What the hook prints: Claude Code's JSON with a notice, or nothing. Never raises."""
284
+ try:
285
+ root = project_root(cwd)
286
+ action = extract(payload, root) if root else None
287
+ if root is None or action is None:
288
+ return ""
289
+ session = str(payload.get("session_id") or "")
290
+ group, offer = record(root, action, session, now or datetime.now(UTC))
291
+ if not offer:
292
+ return ""
293
+ except Exception: # noqa: BLE001 - a hook must never break the agent's work
294
+ return ""
295
+ return json.dumps(
296
+ {"hookSpecificOutput": {"hookEventName": "PostToolUse", "additionalContext": notice(group)}}
297
+ )
298
+
299
+
300
+ # --- what the user decided -------------------------------------------------------------------
301
+
302
+
303
+ class ActionsError(ValueError):
304
+ """An unknown action, a missing script, a bad catalog."""
305
+
306
+
307
+ def set_status(root: Path, group_id: str, status: str, script: str = "") -> Group:
308
+ memory = load(root)
309
+ group = memory.find(group_id)
310
+ if group is None:
311
+ raise ActionsError(f"No action {group_id!r}; `pjkit actions list --all` shows them.")
312
+ group.status, group.script = status, script
313
+ if status == NEW:
314
+ group.count = 0 # "not now": offered again after REPEATS more
315
+ save(root, memory)
316
+ return group
317
+
318
+
319
+ def pending(memory: Memory) -> list[Group]:
320
+ return [g for g in memory.groups if g.status == OFFERED]
321
+
322
+
323
+ def repeated(memory: Memory, *, everything: bool = False) -> list[Group]:
324
+ shown = [g for g in memory.groups if everything or g.count >= 2 or g.status != NEW]
325
+ return sorted(shown, key=lambda g: (-g.count, g.last))
326
+
327
+
328
+ def summary(group: Group, width: int = 60) -> str:
329
+ """What the code uses, which says more than its first line (often ``python - <<EOF``)."""
330
+ modules = [f[7:] for f in group.features if f.startswith("import:")]
331
+ calls = [f[:-2] for f in group.features if f.endswith("()")]
332
+ text = ", ".join(modules) + (": " if modules and calls else "") + ", ".join(calls)
333
+ if not text:
334
+ text = next((x.strip() for x in group.example.splitlines() if x.strip()), "")
335
+ return text if len(text) <= width else text[: width - 3] + "..."
336
+
337
+
338
+ def render_list(groups: list[Group]) -> str:
339
+ if not groups:
340
+ return "Nothing has been written more than once yet."
341
+ lines = [f"{'id':<9} {'times':>5} {'status':<9} {'kind':<6} uses"]
342
+ for g in groups:
343
+ status = f"{g.status}:{g.script}" if g.status == SAVED else g.status
344
+ lines.append(f"{g.id:<9} {g.count:>5} {status:<9} {g.kind:<6} {summary(g)}")
345
+ lines.append(
346
+ "\n`pjkit actions show <id>` shows the code. The pk-automate skill turns one into a script."
347
+ )
348
+ return "\n".join(lines)
349
+
350
+
351
+ def render_group(group: Group) -> str:
352
+ head = (
353
+ f"Action {group.id}: {group.kind}, written {group.count} time(s), {group.status}"
354
+ + (f" (script {group.script})" if group.script else "")
355
+ + f"; first {group.first}, last {group.last}."
356
+ )
357
+ return f"{head}\nThe latest one (secrets removed):\n\n{group.example}"
358
+
359
+
360
+ # --- the catalog of saved scripts ------------------------------------------------------------
361
+
362
+
363
+ @dataclass(frozen=True)
364
+ class Script:
365
+ name: str
366
+ file: str # relative to SCRIPTS_DIR
367
+ description: str
368
+ usage: str
369
+ action: str = "" # the group it came from, if any
370
+
371
+
372
+ _NAME = re.compile(r"[a-z0-9][a-z0-9-]{0,47}")
373
+
374
+
375
+ def load_catalog(root: Path) -> list[Script]:
376
+ import yaml # not at module level: the hook path must stay light
377
+
378
+ path = root / CATALOG
379
+ if not path.exists():
380
+ return []
381
+ try:
382
+ data = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
383
+ except yaml.YAMLError as exc:
384
+ raise ActionsError(f"scripts.yml is not valid YAML: {exc}") from exc
385
+ entries = data.get("scripts", []) if isinstance(data, dict) else None
386
+ if not isinstance(entries, list):
387
+ raise ActionsError("scripts.yml must have a list under `scripts`")
388
+ scripts = []
389
+ for entry in entries:
390
+ if not isinstance(entry, dict) or not all(
391
+ isinstance(entry.get(k), str) for k in ("name", "file", "description", "usage")
392
+ ):
393
+ raise ActionsError("every script in scripts.yml needs name, file, description, usage")
394
+ scripts.append(Script(**{k: str(entry.get(k, "")) for k in Script.__dataclass_fields__}))
395
+ return scripts
396
+
397
+
398
+ def save_catalog(root: Path, scripts: list[Script]) -> None:
399
+ import yaml
400
+
401
+ body = {
402
+ "schema_version": 1,
403
+ "scripts": [{k: v for k, v in asdict(s).items() if v} for s in scripts],
404
+ }
405
+ header = (
406
+ "# Scripts saved so the AI runs them instead of writing the same code again.\n"
407
+ "# Kept by `pjkit actions save`; safe to edit by hand.\n"
408
+ )
409
+ text = yaml.safe_dump(body, sort_keys=False, allow_unicode=True, width=100)
410
+ atomic_write_text(root / CATALOG, header + text)
411
+
412
+
413
+ def add_script(root: Path, script: Script) -> None:
414
+ """Add (or replace, by name) a script in the catalog; mark its action as saved."""
415
+ if not _NAME.fullmatch(script.name):
416
+ raise ActionsError(f"{script.name!r}: use lowercase letters, digits and dashes")
417
+ file = (root / SCRIPTS_DIR / script.file).resolve()
418
+ if not file.is_relative_to((root / SCRIPTS_DIR).resolve()) or not file.is_file():
419
+ raise ActionsError(f"{(SCRIPTS_DIR / script.file).as_posix()} does not exist")
420
+ if not script.description.strip() or not script.usage.strip():
421
+ raise ActionsError("a script needs a description and a usage line")
422
+ if script.action:
423
+ set_status(root, script.action, SAVED, script.name)
424
+ scripts = [s for s in load_catalog(root) if s.name != script.name] + [script]
425
+ save_catalog(root, sorted(scripts, key=lambda s: s.name))
426
+
427
+
428
+ def render_catalog(scripts: list[Script]) -> str:
429
+ if not scripts:
430
+ return "No scripts saved yet."
431
+ lines = []
432
+ for s in scripts:
433
+ lines += [f"{s.name}: {s.description}", f" {s.usage}"]
434
+ return "\n".join(lines)
435
+
436
+
437
+ def session_lines(root: Path, limit: int = 12) -> list[str]:
438
+ """What the start of a session needs to know: the saved scripts and what awaits a decision."""
439
+ lines = []
440
+ try:
441
+ scripts = load_catalog(root)
442
+ except ActionsError:
443
+ scripts = []
444
+ if scripts:
445
+ lines.append(
446
+ "Saved scripts: before writing code for one of these, run the script instead "
447
+ "(usage: `pjkit actions scripts`; in Claude Code, through the pk-runner subagent):"
448
+ )
449
+ lines += [f"- {s.name}: {s.description}" for s in scripts[:limit]]
450
+ if len(scripts) > limit:
451
+ lines.append(f"- and {len(scripts) - limit} more.")
452
+ waiting = pending(load(root))
453
+ if waiting:
454
+ ids = ", ".join(g.id for g in waiting)
455
+ lines.append(
456
+ f"Code written again and again is waiting for the user's decision ({ids}): at a "
457
+ "natural pause, follow the pk-automate skill."
458
+ )
459
+ return lines