napkinstack 0.1.0__tar.gz → 0.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.
- napkinstack-0.3.0/PKG-INFO +147 -0
- napkinstack-0.3.0/README.md +132 -0
- {napkinstack-0.1.0 → napkinstack-0.3.0}/pyproject.toml +2 -2
- {napkinstack-0.1.0 → napkinstack-0.3.0}/pyproject.toml.orig +2 -2
- napkinstack-0.3.0/src/napkinstack/__init__.py +5 -0
- napkinstack-0.3.0/src/napkinstack/cli.py +116 -0
- napkinstack-0.3.0/src/napkinstack/discovery.py +53 -0
- napkinstack-0.3.0/src/napkinstack/doctor.py +314 -0
- {napkinstack-0.1.0 → napkinstack-0.3.0}/src/napkinstack/fitness/boundaries.py +46 -46
- napkinstack-0.3.0/src/napkinstack/fitness/manifests.py +204 -0
- napkinstack-0.3.0/src/napkinstack/fitness/plan.py +221 -0
- napkinstack-0.3.0/src/napkinstack/fitness/pr_scope.sh +79 -0
- napkinstack-0.3.0/src/napkinstack/modules.py +140 -0
- napkinstack-0.3.0/src/napkinstack/project.py +170 -0
- napkinstack-0.3.0/src/napkinstack/pull_request.py +242 -0
- napkinstack-0.3.0/src/napkinstack/skills.py +159 -0
- napkinstack-0.3.0/src/napkinstack/templates/module/AGENTS.md +36 -0
- napkinstack-0.3.0/src/napkinstack/templates/module/MANIFEST.yaml +82 -0
- napkinstack-0.3.0/src/napkinstack/templates/module/README.md +27 -0
- napkinstack-0.1.0/PKG-INFO +0 -139
- napkinstack-0.1.0/README.md +0 -124
- napkinstack-0.1.0/src/napkinstack/__init__.py +0 -5
- napkinstack-0.1.0/src/napkinstack/cli.py +0 -103
- napkinstack-0.1.0/src/napkinstack/doctor.py +0 -278
- napkinstack-0.1.0/src/napkinstack/fitness/manifests.py +0 -198
- napkinstack-0.1.0/src/napkinstack/fitness/pr_scope.sh +0 -79
- napkinstack-0.1.0/src/napkinstack/modules.py +0 -132
- napkinstack-0.1.0/src/napkinstack/project.py +0 -170
- napkinstack-0.1.0/src/napkinstack/skills.py +0 -159
- napkinstack-0.1.0/src/napkinstack/templates/module/AGENTS.md +0 -36
- napkinstack-0.1.0/src/napkinstack/templates/module/MANIFEST.yaml +0 -77
- napkinstack-0.1.0/src/napkinstack/templates/module/README.md +0 -27
- {napkinstack-0.1.0 → napkinstack-0.3.0}/LICENSE +0 -0
- {napkinstack-0.1.0 → napkinstack-0.3.0}/src/napkinstack/fitness/__init__.py +0 -0
- {napkinstack-0.1.0 → napkinstack-0.3.0}/src/napkinstack/templates/module/docs/adr/.gitkeep +0 -0
- {napkinstack-0.1.0 → napkinstack-0.3.0}/src/napkinstack/templates/module/src/.gitkeep +0 -0
- {napkinstack-0.1.0 → napkinstack-0.3.0}/src/napkinstack/templates/module/tests/.gitkeep +0 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: napkinstack
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Engineering framework for several teams and their agents working in one repository: modules, contracts, guardrails in CI.
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Requires-Dist: pyyaml>=6.0
|
|
8
|
+
Requires-Dist: copier==9.18.2
|
|
9
|
+
Requires-Dist: pre-commit==4.6.2
|
|
10
|
+
Requires-Python: >=3.12
|
|
11
|
+
Project-URL: Homepage, https://github.com/NapkinStack/engineering-os
|
|
12
|
+
Project-URL: Source, https://github.com/NapkinStack/engineering-os
|
|
13
|
+
Project-URL: Issues, https://github.com/NapkinStack/engineering-os/issues
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
# NapkinStack
|
|
17
|
+
|
|
18
|
+
An engineering framework for several teams and their agents working in one repository:
|
|
19
|
+
modules, contracts, guardrails in CI. On the Django or Rails model, one command creates
|
|
20
|
+
the project, which then receives new versions on demand; no application stack is imposed.
|
|
21
|
+
Positioning and vocabulary: [`PRODUCT.md`](PRODUCT.md) §1.
|
|
22
|
+
|
|
23
|
+
> **Status: v0.2.0, published** ([PyPI](https://pypi.org/project/napkinstack/)), English
|
|
24
|
+
> throughout. A first pilot project, private, starts from it and puts it to the test before
|
|
25
|
+
> the rest. Tracking: [engine roadmap](docs/governance/plans/2026-09-15-engine-v0.1.0.md),
|
|
26
|
+
> [move to English](docs/governance/plans/2026-09-16-english-migration.md),
|
|
27
|
+
> [`docs/governance/workstreams.md`](docs/governance/workstreams.md).
|
|
28
|
+
|
|
29
|
+
## A project's journey
|
|
30
|
+
|
|
31
|
+
```mermaid
|
|
32
|
+
flowchart LR
|
|
33
|
+
I["Install<br/>uv tool install"]:::cmd --> N["nstack init"]:::cmd
|
|
34
|
+
N --> G["Publish on GitHub<br/>apply the checklist"]:::human
|
|
35
|
+
G --> D["nstack doctor<br/>read-only"]:::cmd
|
|
36
|
+
D --> M["nstack new-module"]:::cmd
|
|
37
|
+
M --> W["Work in pull requests<br/>the team and its agent"]:::human
|
|
38
|
+
W --> U["nstack update<br/>merged branch"]:::cmd
|
|
39
|
+
U --> P["PR reviewed<br/>validated by CI"]:::human
|
|
40
|
+
P -->|"next version"| U
|
|
41
|
+
|
|
42
|
+
classDef cmd fill:#1f2937,color:#fff
|
|
43
|
+
classDef human fill:#065f46,color:#fff
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
**Legend** — grey: a NapkinStack command · green: the team's action. Decision:
|
|
47
|
+
[PDR-0001](docs/pdr/0001-create-a-project-and-receive-updates.md).
|
|
48
|
+
|
|
49
|
+
The project owns its skeleton and adapts it freely. Every new version reaches it on
|
|
50
|
+
demand, merged with its adaptations; the conflicts are left to the team.
|
|
51
|
+
|
|
52
|
+
```mermaid
|
|
53
|
+
flowchart LR
|
|
54
|
+
V1["Skeleton v0.1<br/>common base"]:::ref --> F{"Three-way<br/>merge"}
|
|
55
|
+
V2["Skeleton v0.2<br/>NapkinStack fixes"]:::ns --> F
|
|
56
|
+
PR["Project<br/>the team's adaptations"]:::team --> F
|
|
57
|
+
F -->|"different lines"| B["Update branch<br/>fixes + adaptations"]:::ok
|
|
58
|
+
F -->|"same line changed"| X["Conflict marked<br/>commit refused"]:::ko
|
|
59
|
+
|
|
60
|
+
classDef ref fill:#374151,color:#fff
|
|
61
|
+
classDef ns fill:#1e3a8a,color:#fff
|
|
62
|
+
classDef team fill:#065f46,color:#fff
|
|
63
|
+
classDef ok fill:#065f46,color:#fff
|
|
64
|
+
classDef ko fill:#7c2d12,color:#fff
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**Legend** — grey: the version the project came from · blue: the new version · green: the
|
|
68
|
+
team's work and the accepted result · red: a conflict left to the team.
|
|
69
|
+
|
|
70
|
+
## Install
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
uv tool install napkinstack --with-executables-from pre-commit # prerequisites: uv and git
|
|
74
|
+
nstack init my-project
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Each project then pins its version and changes it through `nstack update`. Every published
|
|
78
|
+
version carries a provenance attestation, visible on PyPI, tying it to the workflow and
|
|
79
|
+
the commit of this repository
|
|
80
|
+
([ADR-0002](docs/adr/0002-distribute-napkinstack-on-pypi.md)).
|
|
81
|
+
|
|
82
|
+
## The commands
|
|
83
|
+
|
|
84
|
+
| Command | Role |
|
|
85
|
+
|---|---|
|
|
86
|
+
| `nstack init <folder>` | Creates the project: skeleton, git repository, initial commit, GitHub checklist |
|
|
87
|
+
| `nstack doctor` | Checks the workstation and the GitHub settings, read-only |
|
|
88
|
+
| `nstack new-module <name> <owner> <criticality>` | Creates a module, with no imposed stack |
|
|
89
|
+
| `nstack check`, `test`, `bootstrap` `[module]`; `nstack run <module>` | Run the commands declared in the module's manifest |
|
|
90
|
+
| `nstack discover <idea-file>` | Starts a discovery for your agent: the idea kept, its document created |
|
|
91
|
+
| `nstack plan` | The discovery, the charter and the cycles: formats, one cycle at a time, closures |
|
|
92
|
+
| `nstack fitness` | Manifests, boundaries between modules, skills, plan |
|
|
93
|
+
| `nstack pr-scope` | One PR = one module, review budget |
|
|
94
|
+
| `nstack e2e [module]` | Runs the module's end-to-end scenarios, when declared |
|
|
95
|
+
| `nstack pr-check` | The test sheet and the cycle, read from the pull request description |
|
|
96
|
+
| `nstack skills` | Exposes the playbooks as skills for the agent |
|
|
97
|
+
| `nstack update` | Lays the new version on a branch to review |
|
|
98
|
+
|
|
99
|
+
**Prerequisites**: uv and git. The guardrails really block on a public GitHub repository,
|
|
100
|
+
or on a private one under the Team or Pro plan; on a private repository on the Free plan
|
|
101
|
+
CI informs without blocking
|
|
102
|
+
([a clarification of PDR-0001](docs/pdr/0001-create-a-project-and-receive-updates.md)).
|
|
103
|
+
|
|
104
|
+
**AI**: NapkinStack embeds none. The team's agent (Claude Code, Codex, Copilot…) reads the
|
|
105
|
+
kernel and the playbooks, runs the commands, and CI accepts or refuses its proposals
|
|
106
|
+
exactly as it would any other contributor's.
|
|
107
|
+
|
|
108
|
+
## This repository
|
|
109
|
+
|
|
110
|
+
```mermaid
|
|
111
|
+
flowchart LR
|
|
112
|
+
S["skeleton/<br/>the project skeleton"]:::shipped -->|"copier.yml"| P["A team's project"]:::project
|
|
113
|
+
E["src/napkinstack/<br/>the nstack engine"]:::shipped -.->|"pinned version"| P
|
|
114
|
+
A["PRODUCT.md · docs/governance/<br/>platform/ · this repository's CI"]:::internal
|
|
115
|
+
|
|
116
|
+
classDef shipped fill:#1e3a8a,color:#fff
|
|
117
|
+
classDef project fill:#065f46,color:#fff
|
|
118
|
+
classDef internal fill:#374151,color:#fff
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
**Legend** — blue: shipped to projects · green: a generated project, which owns its
|
|
122
|
+
skeleton · grey: developing NapkinStack itself, never copied (PDR-0001 R6). Solid line:
|
|
123
|
+
generation; dotted: a versioned dependency.
|
|
124
|
+
|
|
125
|
+
| Path | Role |
|
|
126
|
+
|---|---|
|
|
127
|
+
| `skeleton/` | What every project receives: kernel, playbooks, handbook, CI, hooks |
|
|
128
|
+
| `copier.yml` | The questions asked at creation (a Copier template, ADR-0001) |
|
|
129
|
+
| `src/napkinstack/` | The engine, the `nstack` command |
|
|
130
|
+
| `platform/` | The engine module's envelope: manifest, runbook, tests |
|
|
131
|
+
| `PRODUCT.md`, `docs/governance/` | The working context for NapkinStack itself |
|
|
132
|
+
| `docs/adr/`, `docs/pdr/` | NapkinStack's decisions |
|
|
133
|
+
|
|
134
|
+
## Developing NapkinStack
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
uv sync # prerequisite: uv
|
|
138
|
+
uv run pre-commit install
|
|
139
|
+
uv run nstack fitness # this repository's guardrails
|
|
140
|
+
uv run bash platform/tests/run.sh # the oracle: every guardrail proves it can fail
|
|
141
|
+
uv run nstack init /tmp/trial --source . --ref HEAD # a trial project from the working tree
|
|
142
|
+
uv run nstack doctor --root /tmp/trial # workstation and GitHub settings, read-only
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Contributing: [`CONTRIBUTING.md`](CONTRIBUTING.md), after [`PRODUCT.md`](PRODUCT.md).
|
|
146
|
+
|
|
147
|
+
Licence: [MIT](LICENSE).
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# NapkinStack
|
|
2
|
+
|
|
3
|
+
An engineering framework for several teams and their agents working in one repository:
|
|
4
|
+
modules, contracts, guardrails in CI. On the Django or Rails model, one command creates
|
|
5
|
+
the project, which then receives new versions on demand; no application stack is imposed.
|
|
6
|
+
Positioning and vocabulary: [`PRODUCT.md`](PRODUCT.md) §1.
|
|
7
|
+
|
|
8
|
+
> **Status: v0.2.0, published** ([PyPI](https://pypi.org/project/napkinstack/)), English
|
|
9
|
+
> throughout. A first pilot project, private, starts from it and puts it to the test before
|
|
10
|
+
> the rest. Tracking: [engine roadmap](docs/governance/plans/2026-09-15-engine-v0.1.0.md),
|
|
11
|
+
> [move to English](docs/governance/plans/2026-09-16-english-migration.md),
|
|
12
|
+
> [`docs/governance/workstreams.md`](docs/governance/workstreams.md).
|
|
13
|
+
|
|
14
|
+
## A project's journey
|
|
15
|
+
|
|
16
|
+
```mermaid
|
|
17
|
+
flowchart LR
|
|
18
|
+
I["Install<br/>uv tool install"]:::cmd --> N["nstack init"]:::cmd
|
|
19
|
+
N --> G["Publish on GitHub<br/>apply the checklist"]:::human
|
|
20
|
+
G --> D["nstack doctor<br/>read-only"]:::cmd
|
|
21
|
+
D --> M["nstack new-module"]:::cmd
|
|
22
|
+
M --> W["Work in pull requests<br/>the team and its agent"]:::human
|
|
23
|
+
W --> U["nstack update<br/>merged branch"]:::cmd
|
|
24
|
+
U --> P["PR reviewed<br/>validated by CI"]:::human
|
|
25
|
+
P -->|"next version"| U
|
|
26
|
+
|
|
27
|
+
classDef cmd fill:#1f2937,color:#fff
|
|
28
|
+
classDef human fill:#065f46,color:#fff
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
**Legend** — grey: a NapkinStack command · green: the team's action. Decision:
|
|
32
|
+
[PDR-0001](docs/pdr/0001-create-a-project-and-receive-updates.md).
|
|
33
|
+
|
|
34
|
+
The project owns its skeleton and adapts it freely. Every new version reaches it on
|
|
35
|
+
demand, merged with its adaptations; the conflicts are left to the team.
|
|
36
|
+
|
|
37
|
+
```mermaid
|
|
38
|
+
flowchart LR
|
|
39
|
+
V1["Skeleton v0.1<br/>common base"]:::ref --> F{"Three-way<br/>merge"}
|
|
40
|
+
V2["Skeleton v0.2<br/>NapkinStack fixes"]:::ns --> F
|
|
41
|
+
PR["Project<br/>the team's adaptations"]:::team --> F
|
|
42
|
+
F -->|"different lines"| B["Update branch<br/>fixes + adaptations"]:::ok
|
|
43
|
+
F -->|"same line changed"| X["Conflict marked<br/>commit refused"]:::ko
|
|
44
|
+
|
|
45
|
+
classDef ref fill:#374151,color:#fff
|
|
46
|
+
classDef ns fill:#1e3a8a,color:#fff
|
|
47
|
+
classDef team fill:#065f46,color:#fff
|
|
48
|
+
classDef ok fill:#065f46,color:#fff
|
|
49
|
+
classDef ko fill:#7c2d12,color:#fff
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
**Legend** — grey: the version the project came from · blue: the new version · green: the
|
|
53
|
+
team's work and the accepted result · red: a conflict left to the team.
|
|
54
|
+
|
|
55
|
+
## Install
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
uv tool install napkinstack --with-executables-from pre-commit # prerequisites: uv and git
|
|
59
|
+
nstack init my-project
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Each project then pins its version and changes it through `nstack update`. Every published
|
|
63
|
+
version carries a provenance attestation, visible on PyPI, tying it to the workflow and
|
|
64
|
+
the commit of this repository
|
|
65
|
+
([ADR-0002](docs/adr/0002-distribute-napkinstack-on-pypi.md)).
|
|
66
|
+
|
|
67
|
+
## The commands
|
|
68
|
+
|
|
69
|
+
| Command | Role |
|
|
70
|
+
|---|---|
|
|
71
|
+
| `nstack init <folder>` | Creates the project: skeleton, git repository, initial commit, GitHub checklist |
|
|
72
|
+
| `nstack doctor` | Checks the workstation and the GitHub settings, read-only |
|
|
73
|
+
| `nstack new-module <name> <owner> <criticality>` | Creates a module, with no imposed stack |
|
|
74
|
+
| `nstack check`, `test`, `bootstrap` `[module]`; `nstack run <module>` | Run the commands declared in the module's manifest |
|
|
75
|
+
| `nstack discover <idea-file>` | Starts a discovery for your agent: the idea kept, its document created |
|
|
76
|
+
| `nstack plan` | The discovery, the charter and the cycles: formats, one cycle at a time, closures |
|
|
77
|
+
| `nstack fitness` | Manifests, boundaries between modules, skills, plan |
|
|
78
|
+
| `nstack pr-scope` | One PR = one module, review budget |
|
|
79
|
+
| `nstack e2e [module]` | Runs the module's end-to-end scenarios, when declared |
|
|
80
|
+
| `nstack pr-check` | The test sheet and the cycle, read from the pull request description |
|
|
81
|
+
| `nstack skills` | Exposes the playbooks as skills for the agent |
|
|
82
|
+
| `nstack update` | Lays the new version on a branch to review |
|
|
83
|
+
|
|
84
|
+
**Prerequisites**: uv and git. The guardrails really block on a public GitHub repository,
|
|
85
|
+
or on a private one under the Team or Pro plan; on a private repository on the Free plan
|
|
86
|
+
CI informs without blocking
|
|
87
|
+
([a clarification of PDR-0001](docs/pdr/0001-create-a-project-and-receive-updates.md)).
|
|
88
|
+
|
|
89
|
+
**AI**: NapkinStack embeds none. The team's agent (Claude Code, Codex, Copilot…) reads the
|
|
90
|
+
kernel and the playbooks, runs the commands, and CI accepts or refuses its proposals
|
|
91
|
+
exactly as it would any other contributor's.
|
|
92
|
+
|
|
93
|
+
## This repository
|
|
94
|
+
|
|
95
|
+
```mermaid
|
|
96
|
+
flowchart LR
|
|
97
|
+
S["skeleton/<br/>the project skeleton"]:::shipped -->|"copier.yml"| P["A team's project"]:::project
|
|
98
|
+
E["src/napkinstack/<br/>the nstack engine"]:::shipped -.->|"pinned version"| P
|
|
99
|
+
A["PRODUCT.md · docs/governance/<br/>platform/ · this repository's CI"]:::internal
|
|
100
|
+
|
|
101
|
+
classDef shipped fill:#1e3a8a,color:#fff
|
|
102
|
+
classDef project fill:#065f46,color:#fff
|
|
103
|
+
classDef internal fill:#374151,color:#fff
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
**Legend** — blue: shipped to projects · green: a generated project, which owns its
|
|
107
|
+
skeleton · grey: developing NapkinStack itself, never copied (PDR-0001 R6). Solid line:
|
|
108
|
+
generation; dotted: a versioned dependency.
|
|
109
|
+
|
|
110
|
+
| Path | Role |
|
|
111
|
+
|---|---|
|
|
112
|
+
| `skeleton/` | What every project receives: kernel, playbooks, handbook, CI, hooks |
|
|
113
|
+
| `copier.yml` | The questions asked at creation (a Copier template, ADR-0001) |
|
|
114
|
+
| `src/napkinstack/` | The engine, the `nstack` command |
|
|
115
|
+
| `platform/` | The engine module's envelope: manifest, runbook, tests |
|
|
116
|
+
| `PRODUCT.md`, `docs/governance/` | The working context for NapkinStack itself |
|
|
117
|
+
| `docs/adr/`, `docs/pdr/` | NapkinStack's decisions |
|
|
118
|
+
|
|
119
|
+
## Developing NapkinStack
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
uv sync # prerequisite: uv
|
|
123
|
+
uv run pre-commit install
|
|
124
|
+
uv run nstack fitness # this repository's guardrails
|
|
125
|
+
uv run bash platform/tests/run.sh # the oracle: every guardrail proves it can fail
|
|
126
|
+
uv run nstack init /tmp/trial --source . --ref HEAD # a trial project from the working tree
|
|
127
|
+
uv run nstack doctor --root /tmp/trial # workstation and GitHub settings, read-only
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Contributing: [`CONTRIBUTING.md`](CONTRIBUTING.md), after [`PRODUCT.md`](PRODUCT.md).
|
|
131
|
+
|
|
132
|
+
Licence: [MIT](LICENSE).
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "napkinstack"
|
|
3
|
-
version = "0.
|
|
4
|
-
description = "
|
|
3
|
+
version = "0.3.0"
|
|
4
|
+
description = "Engineering framework for several teams and their agents working in one repository: modules, contracts, guardrails in CI."
|
|
5
5
|
requires-python = ">=3.12"
|
|
6
6
|
license = "MIT"
|
|
7
7
|
license-files = ["LICENSE"]
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "napkinstack"
|
|
3
|
-
version = "0.
|
|
4
|
-
description = "
|
|
3
|
+
version = "0.3.0"
|
|
4
|
+
description = "Engineering framework for several teams and their agents working in one repository: modules, contracts, guardrails in CI."
|
|
5
5
|
requires-python = ">=3.12"
|
|
6
6
|
license = "MIT"
|
|
7
7
|
license-files = ["LICENSE"]
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""Single entry point `nstack` (workstream C1, PDR-0001)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import os
|
|
7
|
+
import subprocess
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from napkinstack import __version__, discovery, doctor, modules, pull_request, skills
|
|
11
|
+
from napkinstack.fitness import boundaries, manifests, plan
|
|
12
|
+
|
|
13
|
+
PACKAGE = Path(__file__).resolve().parent
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _root(value: str) -> Path:
|
|
17
|
+
root = Path(value).resolve()
|
|
18
|
+
if not root.is_dir():
|
|
19
|
+
raise argparse.ArgumentTypeError(f"root not found: {value}")
|
|
20
|
+
return root
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _script(relative: str, *args: str, root: Path) -> int:
|
|
24
|
+
env = {**os.environ, "NSTACK_ROOT": str(root)}
|
|
25
|
+
return subprocess.run(["bash", str(PACKAGE / relative), *args], cwd=root, env=env).returncode
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _fitness(root: Path) -> int:
|
|
29
|
+
results = [manifests.run(root), boundaries.run(root), skills.run(root, check_only=True), plan.run(root)]
|
|
30
|
+
return 1 if any(results) else 0
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _add(sub, name: str, help_: str, func) -> argparse.ArgumentParser:
|
|
34
|
+
parser = sub.add_parser(name, help=help_)
|
|
35
|
+
parser.add_argument("--root", type=_root, default=Path.cwd(),
|
|
36
|
+
help="project root (default: current folder)")
|
|
37
|
+
parser.set_defaults(func=func)
|
|
38
|
+
return parser
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _init(args: argparse.Namespace) -> int:
|
|
42
|
+
from napkinstack import project # Copier is only loaded for init and update
|
|
43
|
+
|
|
44
|
+
answers = {"project_name": args.project_name, "github_repo": args.github_repo,
|
|
45
|
+
"owner_team": args.owner_team}
|
|
46
|
+
return project.init(args.destination, answers, args.source or project.SOURCE,
|
|
47
|
+
args.ref or project.default_ref())
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _update(args: argparse.Namespace) -> int:
|
|
51
|
+
from napkinstack import project
|
|
52
|
+
|
|
53
|
+
return project.update(args.root, args.ref or project.default_ref())
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
57
|
+
parser = argparse.ArgumentParser(prog="nstack", description="NapkinStack engine.")
|
|
58
|
+
parser.add_argument("--version", action="version", version=f"nstack {__version__}")
|
|
59
|
+
sub = parser.add_subparsers(dest="command", required=True, metavar="command")
|
|
60
|
+
_add(sub, "manifests", "manifests, lifecycles, deprecations (M1-M9)",
|
|
61
|
+
lambda a: manifests.run(a.root))
|
|
62
|
+
_add(sub, "boundaries", "declared graph against real graph (B1-B5)",
|
|
63
|
+
lambda a: boundaries.run(a.root))
|
|
64
|
+
_add(sub, "plan", "the discovery, the charter and the cycles (C1-C7)", lambda a: plan.run(a.root))
|
|
65
|
+
sk = _add(sub, "skills", "generates or checks the skills (S1-S4)",
|
|
66
|
+
lambda a: skills.run(a.root, check_only=a.check))
|
|
67
|
+
sk.add_argument("--check", action="store_true", help="check without writing")
|
|
68
|
+
_add(sub, "fitness", "manifests + boundaries + skills + plan",
|
|
69
|
+
lambda a: _fitness(a.root))
|
|
70
|
+
_add(sub, "doctor", "diagnoses the workstation and the GitHub settings, read-only (PDR-0001)",
|
|
71
|
+
lambda a: doctor.run(a.root))
|
|
72
|
+
nm = _add(sub, "new-module", "creates a module and its guardrails, with no imposed stack",
|
|
73
|
+
lambda a: modules.create(a.root, a.name, a.owner, a.criticality, a.user_facing))
|
|
74
|
+
nm.add_argument("name", help="module name, kebab-case")
|
|
75
|
+
nm.add_argument("owner", help="GitHub team, organisation/team, or a user when the project has "
|
|
76
|
+
"no organisation")
|
|
77
|
+
nm.add_argument("criticality", choices=["prototype", "standard", "high", "critical"])
|
|
78
|
+
nm.add_argument("--user-facing", action="store_true",
|
|
79
|
+
help="a user sees this module: its pull requests carry a test sheet")
|
|
80
|
+
for verb, help_text in (("bootstrap", "prepares one module, or all of them (commands.bootstrap)"),
|
|
81
|
+
("check", "format, lint, types of one module, or all (commands.check)"),
|
|
82
|
+
("test", "tests of one module, or of all of them (commands.test)"),
|
|
83
|
+
("e2e", "end-to-end scenarios of one module, or of all (commands.e2e)")):
|
|
84
|
+
vb = _add(sub, verb, help_text, lambda a, v=verb: modules.run_verb(a.root, v, a.module))
|
|
85
|
+
vb.add_argument("module", nargs="?", help="module name (default: all)")
|
|
86
|
+
rn = _add(sub, "run", "starts a module locally (commands.run)",
|
|
87
|
+
lambda a: modules.run_verb(a.root, "run", a.module))
|
|
88
|
+
rn.add_argument("module")
|
|
89
|
+
ps = _add(sub, "pr-scope", "one PR = one module, review budget (P1-P2)",
|
|
90
|
+
lambda a: _script("fitness/pr_scope.sh", a.base, root=a.root))
|
|
91
|
+
ps.add_argument("--base", default="origin/main")
|
|
92
|
+
pc = _add(sub, "pr-check", "test sheet and cycle, read from the pull request description (T1-T5, K1-K4)",
|
|
93
|
+
lambda a: pull_request.run(a.root, a.base, a.body_file))
|
|
94
|
+
pc.add_argument("--base", default="origin/main")
|
|
95
|
+
pc.add_argument("--body-file", type=Path, help="the description, when PR_BODY is not set")
|
|
96
|
+
ds = _add(sub, "discover", "starts a discovery from an idea file, for the team's agent (PDR-0002)",
|
|
97
|
+
lambda a: discovery.run(a.root, a.idea))
|
|
98
|
+
ds.add_argument("idea", type=Path, help="the idea, a .md or .txt file")
|
|
99
|
+
ini = sub.add_parser("init", help="creates a project from the skeleton (PDR-0001)")
|
|
100
|
+
ini.add_argument("destination", type=Path, help="project folder, missing or empty")
|
|
101
|
+
ini.add_argument("--project-name", help="project name (asked when absent)")
|
|
102
|
+
ini.add_argument("--github-repo", help="GitHub repository, organisation/name (asked when absent)")
|
|
103
|
+
ini.add_argument("--owner-team", help="owner of the foundation: organisation/team, or a user "
|
|
104
|
+
"when the project has no organisation (asked when absent)")
|
|
105
|
+
ini.add_argument("--source", help="template: URL or path (default: the NapkinStack repository)")
|
|
106
|
+
ini.add_argument("--ref", help="skeleton version, tag vX.Y.Z (default: the one of nstack)")
|
|
107
|
+
ini.set_defaults(func=_init)
|
|
108
|
+
up = _add(sub, "update", "merges a NapkinStack version onto a branch to review (PDR-0001)",
|
|
109
|
+
_update)
|
|
110
|
+
up.add_argument("--ref", help="target version, tag vX.Y.Z (default: the one of nstack)")
|
|
111
|
+
return parser
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def main(argv: list[str] | None = None) -> int:
|
|
115
|
+
args = build_parser().parse_args(argv)
|
|
116
|
+
return args.func(args)
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""
|
|
2
|
+
nstack discover — starts a project's discovery from an idea file (PDR-0002, extension).
|
|
3
|
+
|
|
4
|
+
Deterministic, no model called: the idea is kept in docs/project/inputs/, the discovery
|
|
5
|
+
document is created from its template, and the instruction for the team's agent is
|
|
6
|
+
printed. The conversation itself happens in the agent (playbooks/discovery.md).
|
|
7
|
+
|
|
8
|
+
Usage : nstack discover <idea-file> [--root ROOT]
|
|
9
|
+
Output: 0 when the discovery is started, 1 otherwise.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import shutil
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
from napkinstack.fitness.plan import DISCOVERY, PROJECT
|
|
18
|
+
|
|
19
|
+
INPUTS = PROJECT / "inputs"
|
|
20
|
+
TEMPLATE = PROJECT / "_DISCOVERY_TEMPLATE.md"
|
|
21
|
+
SUFFIXES = {".md", ".txt"}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def run(root: Path, idea: Path) -> int:
|
|
25
|
+
if not (root / TEMPLATE).is_file():
|
|
26
|
+
print(f"FAIL [discover] {TEMPLATE} not found in {root}: not a project created by nstack init, "
|
|
27
|
+
"or one older than the discovery.\n Action: run it at the project root, or update "
|
|
28
|
+
"the project (nstack update).")
|
|
29
|
+
return 1
|
|
30
|
+
if not idea.is_file() or idea.suffix.lower() not in SUFFIXES:
|
|
31
|
+
print(f"FAIL [discover] {idea}: a Markdown or text file expected.\n Action: write the idea "
|
|
32
|
+
"in a .md or .txt file — a few lines are enough.")
|
|
33
|
+
return 1
|
|
34
|
+
if (root / DISCOVERY).exists():
|
|
35
|
+
print(f"FAIL [discover] {DISCOVERY} already exists: one discovery per project.\n Action: "
|
|
36
|
+
"continue it in your agent (playbooks/discovery.md), as a new round of the same document.")
|
|
37
|
+
return 1
|
|
38
|
+
kept = root / INPUTS / idea.name
|
|
39
|
+
if kept.exists() and kept.resolve() != idea.resolve():
|
|
40
|
+
print(f"FAIL [discover] {INPUTS / idea.name} already exists.\n Action: rename the idea file.")
|
|
41
|
+
return 1
|
|
42
|
+
kept.parent.mkdir(parents=True, exist_ok=True)
|
|
43
|
+
if kept.resolve() != idea.resolve():
|
|
44
|
+
shutil.copyfile(idea, kept)
|
|
45
|
+
source = (INPUTS / idea.name).as_posix()
|
|
46
|
+
template = (root / TEMPLATE).read_text(encoding="utf-8")
|
|
47
|
+
(root / DISCOVERY).write_text(template.replace("<idea file>", source), encoding="utf-8")
|
|
48
|
+
print(f"Discovery started: {source} kept, {DISCOVERY} created.")
|
|
49
|
+
print("\nNext, in your agent:")
|
|
50
|
+
print(f" Follow playbooks/discovery.md on {source}: interview me, one question at a time.")
|
|
51
|
+
print("Then, in another session, have the document challenged (stage 5); the decider decides: "
|
|
52
|
+
"go, clarify or kill.")
|
|
53
|
+
return 0
|