project-kit-cli 0.1.0__py3-none-any.whl
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.
- project_kit/__init__.py +8 -0
- project_kit/__main__.py +5 -0
- project_kit/agents.py +81 -0
- project_kit/ai_attribution.py +69 -0
- project_kit/approval.py +36 -0
- project_kit/assets/hooks/commit-msg +14 -0
- project_kit/assets/project/workflow.yml +63 -0
- project_kit/assets/skills/pk-decide/SKILL.md +63 -0
- project_kit/assets/skills/pk-discover/SKILL.md +57 -0
- project_kit/assets/skills/pk-handoff/SKILL.md +43 -0
- project_kit/assets/skills/pk-plan/SKILL.md +55 -0
- project_kit/assets/skills/pk-provision/SKILL.md +45 -0
- project_kit/assets/skills/pk-start/SKILL.md +47 -0
- project_kit/assets/skills/pk-workflow/SKILL.md +51 -0
- project_kit/assets/templates/adr.md +32 -0
- project_kit/assets/templates/backlog.example.json +59 -0
- project_kit/assets/templates/brief.md +77 -0
- project_kit/assets/templates/constitution-input.md +78 -0
- project_kit/assets/templates/development-workflow.md +51 -0
- project_kit/assets/templates/github/ISSUE_TEMPLATE/adr.yml +21 -0
- project_kit/assets/templates/github/ISSUE_TEMPLATE/bug.yml +25 -0
- project_kit/assets/templates/github/ISSUE_TEMPLATE/config.yml +1 -0
- project_kit/assets/templates/github/ISSUE_TEMPLATE/epic.yml +15 -0
- project_kit/assets/templates/github/ISSUE_TEMPLATE/feature.yml +22 -0
- project_kit/assets/templates/github/pull_request_template.md +21 -0
- project_kit/assets/templates/interview.md +68 -0
- project_kit/backlog.py +312 -0
- project_kit/cli.py +286 -0
- project_kit/config.py +139 -0
- project_kit/doctor.py +187 -0
- project_kit/fsutil.py +27 -0
- project_kit/gh.py +152 -0
- project_kit/handoff.py +360 -0
- project_kit/help.py +152 -0
- project_kit/installer.py +100 -0
- project_kit/labels.py +17 -0
- project_kit/project_choice.py +127 -0
- project_kit/providers/__init__.py +1 -0
- project_kit/providers/github.py +329 -0
- project_kit/provision.py +260 -0
- project_kit/provision_issues.py +360 -0
- project_kit/state.py +115 -0
- project_kit/templating.py +23 -0
- project_kit/workflow.py +168 -0
- project_kit_cli-0.1.0.dist-info/METADATA +97 -0
- project_kit_cli-0.1.0.dist-info/RECORD +49 -0
- project_kit_cli-0.1.0.dist-info/WHEEL +4 -0
- project_kit_cli-0.1.0.dist-info/entry_points.txt +3 -0
- project_kit_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
project_kit/state.py
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
""".project-kit/state.json: where the project is and what exists in the provider.
|
|
2
|
+
|
|
3
|
+
The ``provider_ids`` map (``kind -> local_id -> provider_id``) is what makes provisioning
|
|
4
|
+
resumable: after a partial failure, items already created are looked up here instead of
|
|
5
|
+
being created again.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from datetime import UTC, datetime
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from project_kit.config import PROJECT_DIR
|
|
15
|
+
from project_kit.fsutil import atomic_write_text
|
|
16
|
+
|
|
17
|
+
STATE_VERSION = 1
|
|
18
|
+
PHASES = ("init", "discover", "decide", "workflow", "plan", "provision", "handoff")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class StateError(ValueError):
|
|
22
|
+
"""The state file is unreadable or invalid."""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def state_path(root: Path) -> Path:
|
|
26
|
+
return root / PROJECT_DIR / "state.json"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class State:
|
|
31
|
+
phase: str = "init"
|
|
32
|
+
approvals: dict[str, str] = field(default_factory=dict)
|
|
33
|
+
provider_ids: dict[str, dict[str, str]] = field(default_factory=dict)
|
|
34
|
+
|
|
35
|
+
def validate(self) -> None:
|
|
36
|
+
problems = []
|
|
37
|
+
if self.phase not in PHASES:
|
|
38
|
+
problems.append(f"phase must be one of {', '.join(PHASES)}, got {self.phase!r}")
|
|
39
|
+
for phase, when in self.approvals.items():
|
|
40
|
+
if phase not in PHASES:
|
|
41
|
+
problems.append(f"approvals has unknown phase {phase!r}")
|
|
42
|
+
if not isinstance(when, str):
|
|
43
|
+
problems.append(f"approvals.{phase} must be a timestamp string")
|
|
44
|
+
for kind, ids in self.provider_ids.items():
|
|
45
|
+
if not isinstance(ids, dict) or not all(
|
|
46
|
+
isinstance(k, str) and isinstance(v, str) for k, v in ids.items()
|
|
47
|
+
):
|
|
48
|
+
problems.append(f"provider_ids.{kind} must map local ids to provider ids (strings)")
|
|
49
|
+
if problems:
|
|
50
|
+
raise StateError("Invalid state: " + "; ".join(problems))
|
|
51
|
+
|
|
52
|
+
def approve(self, phase: str, when: datetime | None = None) -> None:
|
|
53
|
+
if phase not in PHASES:
|
|
54
|
+
raise StateError(f"Unknown phase {phase!r}")
|
|
55
|
+
self.approvals[phase] = (when or datetime.now(UTC)).isoformat(timespec="seconds")
|
|
56
|
+
|
|
57
|
+
def is_approved(self, phase: str) -> bool:
|
|
58
|
+
return phase in self.approvals
|
|
59
|
+
|
|
60
|
+
def set_id(self, kind: str, local_id: str, provider_id: str) -> None:
|
|
61
|
+
self.provider_ids.setdefault(kind, {})[local_id] = provider_id
|
|
62
|
+
|
|
63
|
+
def get_id(self, kind: str, local_id: str) -> str | None:
|
|
64
|
+
return self.provider_ids.get(kind, {}).get(local_id)
|
|
65
|
+
|
|
66
|
+
def to_dict(self) -> dict[str, Any]:
|
|
67
|
+
return {
|
|
68
|
+
"version": STATE_VERSION,
|
|
69
|
+
"phase": self.phase,
|
|
70
|
+
"approvals": self.approvals,
|
|
71
|
+
"provider_ids": self.provider_ids,
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def parse_state(text: str) -> State:
|
|
76
|
+
try:
|
|
77
|
+
data = json.loads(text)
|
|
78
|
+
except json.JSONDecodeError as exc:
|
|
79
|
+
raise StateError(
|
|
80
|
+
f"state.json is not valid JSON ({exc}). It is written atomically, so it was "
|
|
81
|
+
"probably edited by hand; fix or delete it."
|
|
82
|
+
) from exc
|
|
83
|
+
if not isinstance(data, dict):
|
|
84
|
+
raise StateError("state.json must contain an object")
|
|
85
|
+
|
|
86
|
+
version = data.get("version")
|
|
87
|
+
if version != STATE_VERSION:
|
|
88
|
+
hint = (
|
|
89
|
+
" Upgrade project-kit." if isinstance(version, int) and version > STATE_VERSION else ""
|
|
90
|
+
)
|
|
91
|
+
raise StateError(f"Unsupported state.json version {version!r}.{hint}")
|
|
92
|
+
|
|
93
|
+
state = State(
|
|
94
|
+
phase=data.get("phase", "init"),
|
|
95
|
+
approvals=data.get("approvals", {}),
|
|
96
|
+
provider_ids=data.get("provider_ids", {}),
|
|
97
|
+
)
|
|
98
|
+
state.validate()
|
|
99
|
+
return state
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def load_state(root: Path) -> State:
|
|
103
|
+
"""Load the state, or a fresh one if the project has none yet."""
|
|
104
|
+
try:
|
|
105
|
+
text = state_path(root).read_text(encoding="utf-8")
|
|
106
|
+
except FileNotFoundError:
|
|
107
|
+
return State()
|
|
108
|
+
return parse_state(text)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def save_state(root: Path, state: State) -> None:
|
|
112
|
+
state.validate()
|
|
113
|
+
atomic_write_text(
|
|
114
|
+
state_path(root), json.dumps(state.to_dict(), indent=2, sort_keys=True) + "\n"
|
|
115
|
+
)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Minimal ``{{variable}}`` substitution for the templates project-kit ships.
|
|
2
|
+
|
|
3
|
+
Deliberately strict: an unknown variable is an error, so a typo in a template that the
|
|
4
|
+
project has overridden is reported instead of leaking ``{{...}}`` into generated files.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import re
|
|
8
|
+
|
|
9
|
+
_VARIABLE = re.compile(r"\{\{\s*([A-Za-z_][\w.]*)\s*\}\}")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TemplateError(ValueError):
|
|
13
|
+
"""The template uses a variable that was not provided."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def render(text: str, variables: dict[str, str]) -> str:
|
|
17
|
+
unknown = sorted({name for name in _VARIABLE.findall(text) if name not in variables})
|
|
18
|
+
if unknown:
|
|
19
|
+
known = ", ".join(sorted(variables))
|
|
20
|
+
raise TemplateError(
|
|
21
|
+
f"Unknown template variable(s): {', '.join(unknown)}. Available: {known}."
|
|
22
|
+
)
|
|
23
|
+
return _VARIABLE.sub(lambda m: variables[m.group(1)], text)
|
project_kit/workflow.py
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
""".project-kit/workflow.yml: the ordered phases of the project-definition flow.
|
|
2
|
+
|
|
3
|
+
Modelled on Spec Kit's ``workflow.yml``: a list of steps, each one with an optional review
|
|
4
|
+
gate. The skills and ``project-kit status`` read it to know what comes next, and
|
|
5
|
+
``project-kit approve`` uses it to refuse a gate while an earlier enabled gate is still open.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from importlib.resources import files
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
import yaml
|
|
14
|
+
|
|
15
|
+
from project_kit.config import PROJECT_DIR
|
|
16
|
+
from project_kit.state import PHASES, State
|
|
17
|
+
|
|
18
|
+
SCHEMA_VERSION = "1.0"
|
|
19
|
+
_STEP_KEYS = {"id", "title", "skill", "gate", "enabled", "produces"}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class WorkflowError(ValueError):
|
|
23
|
+
"""The workflow file is missing, unreadable or invalid."""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def workflow_path(root: Path) -> Path:
|
|
27
|
+
return root / PROJECT_DIR / "workflow.yml"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class Step:
|
|
32
|
+
id: str
|
|
33
|
+
title: str
|
|
34
|
+
skill: str
|
|
35
|
+
gate: bool = True
|
|
36
|
+
enabled: bool = True
|
|
37
|
+
produces: tuple[str, ...] = ()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class Workflow:
|
|
42
|
+
steps: list[Step] = field(default_factory=list)
|
|
43
|
+
|
|
44
|
+
def enabled_steps(self) -> list[Step]:
|
|
45
|
+
return [s for s in self.steps if s.enabled]
|
|
46
|
+
|
|
47
|
+
def open_gates_before(self, phase: str, state: State) -> list[Step]:
|
|
48
|
+
"""Enabled steps with a gate, earlier than ``phase``, that nobody has approved yet."""
|
|
49
|
+
later = PHASES.index(phase)
|
|
50
|
+
return [
|
|
51
|
+
s
|
|
52
|
+
for s in self.enabled_steps()
|
|
53
|
+
if s.gate and PHASES.index(s.id) < later and not state.is_approved(s.id)
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
def next_step(self, state: State) -> Step | None:
|
|
57
|
+
"""First enabled step that is not done. ``init`` is done once the project exists."""
|
|
58
|
+
for step in self.enabled_steps():
|
|
59
|
+
if step.id != "init" and not state.is_approved(step.id):
|
|
60
|
+
return step
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _flag(raw: dict[str, Any], key: str, where: str, problems: list[str]) -> bool:
|
|
65
|
+
value = raw.get(key, True)
|
|
66
|
+
if not isinstance(value, bool):
|
|
67
|
+
problems.append(f"{where}.{key} must be true or false")
|
|
68
|
+
return True
|
|
69
|
+
return value
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def parse_workflow(text: str) -> Workflow:
|
|
73
|
+
try:
|
|
74
|
+
data = yaml.safe_load(text)
|
|
75
|
+
except yaml.YAMLError as exc:
|
|
76
|
+
raise WorkflowError(f"workflow.yml is not valid YAML: {exc}") from exc
|
|
77
|
+
if not isinstance(data, dict):
|
|
78
|
+
raise WorkflowError("workflow.yml must contain a mapping")
|
|
79
|
+
if str(data.get("schema_version")) != SCHEMA_VERSION:
|
|
80
|
+
raise WorkflowError(f"Unsupported workflow.yml schema_version; expected {SCHEMA_VERSION!r}")
|
|
81
|
+
raw_steps = data.get("steps")
|
|
82
|
+
if not isinstance(raw_steps, list) or not raw_steps:
|
|
83
|
+
raise WorkflowError("workflow.yml needs a non-empty `steps` list")
|
|
84
|
+
|
|
85
|
+
problems: list[str] = []
|
|
86
|
+
steps: list[Step] = []
|
|
87
|
+
for n, raw in enumerate(raw_steps, 1):
|
|
88
|
+
where = f"steps[{n}]"
|
|
89
|
+
if not isinstance(raw, dict):
|
|
90
|
+
problems.append(f"{where} must be a mapping")
|
|
91
|
+
continue
|
|
92
|
+
unknown = set(raw) - _STEP_KEYS
|
|
93
|
+
if unknown:
|
|
94
|
+
problems.append(f"{where} has unknown key(s): {', '.join(sorted(map(str, unknown)))}")
|
|
95
|
+
step_id = raw.get("id")
|
|
96
|
+
if step_id not in PHASES:
|
|
97
|
+
problems.append(f"{where}.id must be one of {', '.join(PHASES)}, got {step_id!r}")
|
|
98
|
+
continue
|
|
99
|
+
produces = raw.get("produces") or []
|
|
100
|
+
if not isinstance(produces, list) or not all(isinstance(p, str) for p in produces):
|
|
101
|
+
problems.append(f"{where}.produces must be a list of paths")
|
|
102
|
+
produces = []
|
|
103
|
+
title, skill = raw.get("title"), raw.get("skill")
|
|
104
|
+
for key, value in (("title", title), ("skill", skill)):
|
|
105
|
+
if not isinstance(value, str) or not value.strip():
|
|
106
|
+
problems.append(f"{where}.{key} must be a non-empty string")
|
|
107
|
+
steps.append(
|
|
108
|
+
Step(
|
|
109
|
+
id=step_id,
|
|
110
|
+
title=title if isinstance(title, str) else "",
|
|
111
|
+
skill=skill if isinstance(skill, str) else "",
|
|
112
|
+
gate=_flag(raw, "gate", where, problems),
|
|
113
|
+
enabled=_flag(raw, "enabled", where, problems),
|
|
114
|
+
produces=tuple(produces),
|
|
115
|
+
)
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
ids = [s.id for s in steps]
|
|
119
|
+
if len(set(ids)) != len(ids):
|
|
120
|
+
problems.append("steps must not repeat an id")
|
|
121
|
+
if [i for i in PHASES if i in ids] != ids:
|
|
122
|
+
problems.append(f"steps must follow this order: {', '.join(PHASES)}")
|
|
123
|
+
if problems:
|
|
124
|
+
raise WorkflowError("Invalid workflow.yml: " + "; ".join(problems))
|
|
125
|
+
return Workflow(steps)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def default_workflow() -> Workflow:
|
|
129
|
+
text = (files("project_kit") / "assets" / "project" / "workflow.yml").read_text("utf-8")
|
|
130
|
+
return parse_workflow(text)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def load_workflow(root: Path) -> Workflow:
|
|
134
|
+
"""The project's workflow, or the shipped one for projects created before it existed."""
|
|
135
|
+
path = workflow_path(root)
|
|
136
|
+
if not path.exists():
|
|
137
|
+
return default_workflow()
|
|
138
|
+
return parse_workflow(path.read_text(encoding="utf-8"))
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def step_marks(workflow: Workflow, state: State) -> list[tuple[Step, str, str]]:
|
|
142
|
+
"""Each step with its mark (``x`` done, ``>`` next, ``-`` skipped, blank pending) and note."""
|
|
143
|
+
next_step = workflow.next_step(state)
|
|
144
|
+
marks = []
|
|
145
|
+
for step in workflow.steps:
|
|
146
|
+
if not step.enabled:
|
|
147
|
+
mark, note = "-", "skipped (disabled in workflow.yml)"
|
|
148
|
+
elif step.id == "init" or state.is_approved(step.id):
|
|
149
|
+
mark, note = "x", "done"
|
|
150
|
+
elif step is next_step:
|
|
151
|
+
mark, note = ">", "next"
|
|
152
|
+
else:
|
|
153
|
+
mark, note = " ", "pending"
|
|
154
|
+
marks.append((step, mark, note))
|
|
155
|
+
return marks
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def render_status(workflow: Workflow, state: State, project_name: str) -> str:
|
|
159
|
+
next_step = workflow.next_step(state)
|
|
160
|
+
lines = [f"Project: {project_name}", ""]
|
|
161
|
+
for step, mark, note in step_marks(workflow, state):
|
|
162
|
+
lines.append(f" [{mark}] {step.id:<10} /{step.skill:<12} {note}")
|
|
163
|
+
lines.append("")
|
|
164
|
+
if next_step is None:
|
|
165
|
+
lines.append("Every enabled phase is done.")
|
|
166
|
+
else:
|
|
167
|
+
lines.append(f"Next: /{next_step.skill}: {next_step.title}")
|
|
168
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: project-kit-cli
|
|
3
|
+
Version: 0.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: Topic :: Software Development
|
|
15
|
+
Requires-Dist: pyyaml>=6.0.3
|
|
16
|
+
Requires-Python: >=3.12
|
|
17
|
+
Project-URL: Repository, https://github.com/miguelmatg/project-kit
|
|
18
|
+
Project-URL: Issues, https://github.com/miguelmatg/project-kit/issues
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# project-kit
|
|
22
|
+
|
|
23
|
+
Herramienta para el **antes** de un proyecto: definirlo, planificarlo, decidir su
|
|
24
|
+
arquitectura y montar su flujo de desarrollo y gestión (GitHub primero, GitLab después).
|
|
25
|
+
Al terminar entrega el relevo a [Spec Kit](https://github.com/github/spec-kit), que define e
|
|
26
|
+
implementa cada spec.
|
|
27
|
+
|
|
28
|
+
> Estado: diseño y esqueleto del paquete (v0.1 en desarrollo). Ver [`docs/design.md`](docs/design.md).
|
|
29
|
+
|
|
30
|
+
## Instalación y uso
|
|
31
|
+
|
|
32
|
+
Requisitos: [uv](https://docs.astral.sh/uv/), `git`, y [`gh`](https://cli.github.com/) con
|
|
33
|
+
sesión iniciada y los scopes `repo` y `project` (`gh auth refresh -s project`) para publicar en
|
|
34
|
+
GitHub. [Claude Code](https://claude.com/claude-code) es el agente que guía las fases.
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
# Instalar (ahora, desde un clon; cuando se publique será `uv tool install project-kit-cli`; el comando es `project-kit`)
|
|
38
|
+
uv tool install --editable <ruta-a-este-clon>
|
|
39
|
+
|
|
40
|
+
# En el repositorio del proyecto (puede estar vacío)
|
|
41
|
+
project-kit doctor # comprueba las herramientas
|
|
42
|
+
project-kit init # instala skills y plantillas; pregunta si la IA firma commits y si abrir Claude
|
|
43
|
+
project-kit help # guía visual del flujo y los comandos (también: `project-kit` a secas)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
`init` copia los ficheros y, al terminar, muestra los agentes de IA soportados (hoy solo Claude
|
|
47
|
+
Code) y ofrece abrirlo en esa misma terminal ya con **`/pk-start`** (Intro acepta). Se puede
|
|
48
|
+
evitar con `--agent none`; sin terminal no pregunta ni lanza nada, y dentro de Claude Code no
|
|
49
|
+
abre otro. Si lo prefieres, ábrelo tú y ejecuta `/pk-start`. Pregunta si quieres **solo
|
|
50
|
+
inicializar** o **empezar a definir el proyecto**, y al terminar cada fase ofrece continuar o
|
|
51
|
+
parar. `project-kit status` muestra en qué fase estás y cuál toca.
|
|
52
|
+
|
|
53
|
+
| Fase | Skill | Qué hace | Salida |
|
|
54
|
+
|---|---|---|---|
|
|
55
|
+
| 0 | `/pk-start` | Comprueba el entorno y elige: solo inicializar o empezar | `.project-kit/` |
|
|
56
|
+
| 1 | `/pk-discover` | Entrevista sobre qué es el proyecto | `docs/project/brief.md` |
|
|
57
|
+
| 2 | `/pk-decide` | Guía las decisiones de stack y arquitectura | `docs/adr/*.md` |
|
|
58
|
+
| 3 | `/pk-workflow` | Acuerda el flujo de trabajo del equipo | `docs/development-workflow.md` |
|
|
59
|
+
| 4 | `/pk-plan` | Deriva fases, épicas y backlog | `roadmap.md`, `backlog.json` |
|
|
60
|
+
| 5 | `/pk-provision` | Crea milestones, ramas, Project e issues | GitHub |
|
|
61
|
+
| 6 | `/pk-handoff` | Relevo a Spec Kit (una spec por issue) | `constitution-input.md` |
|
|
62
|
+
|
|
63
|
+
Cada fase termina con una aprobación (`project-kit approve <fase>`) y puede rellenarse a mano
|
|
64
|
+
a partir de las plantillas de `.project-kit/templates/`. Las fases se listan en
|
|
65
|
+
`.project-kit/workflow.yml`, con el mismo estilo que el de Spec Kit; para un proyecto pequeño se
|
|
66
|
+
desactiva una con `enabled: false`.
|
|
67
|
+
|
|
68
|
+
## Documentación
|
|
69
|
+
|
|
70
|
+
- [Diseño](docs/design.md): alcance, fases, proveedor, relevo a Spec Kit, decisiones abiertas.
|
|
71
|
+
- [Publicación](docs/publishing.md): qué hace falta para `uv tool install project-kit-cli` público.
|
|
72
|
+
- [Esquema de `backlog.json`](docs/backlog-schema.md): formato del backlog y reglas de validación.
|
|
73
|
+
- [Relevo a Spec Kit](docs/handoff.md): qué genera `handoff`, cómo instala Spec Kit y qué viene después.
|
|
74
|
+
- [Provisionado en GitHub](docs/provisioning.md): qué crea `provision`, idempotencia y protección de ramas.
|
|
75
|
+
- [Referencia: playbook del flujo](docs/references/template-repo-playbook.md): receta de origen
|
|
76
|
+
(extraída de otro proyecto).
|
|
77
|
+
|
|
78
|
+
## Licencia y contribuciones
|
|
79
|
+
|
|
80
|
+
MIT ([`LICENSE`](LICENSE)). Las issues con ideas y mejoras son bienvenidas; el código lo
|
|
81
|
+
desarrolla solo su autor y las pull requests están desactivadas ([`CONTRIBUTING.md`](CONTRIBUTING.md)).
|
|
82
|
+
|
|
83
|
+
## Ramas
|
|
84
|
+
|
|
85
|
+
`main` (estable), `release` (pruebas) y `develop` (desarrollo). Todo entra por PR.
|
|
86
|
+
|
|
87
|
+
## Desarrollo
|
|
88
|
+
|
|
89
|
+
Requiere [uv](https://docs.astral.sh/uv/).
|
|
90
|
+
|
|
91
|
+
```
|
|
92
|
+
uv sync # entorno y dependencias
|
|
93
|
+
uv run project-kit --version
|
|
94
|
+
uv run pytest # tests
|
|
95
|
+
uv run ruff check . # lint
|
|
96
|
+
uv run ruff format . # formato
|
|
97
|
+
```
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
project_kit/__init__.py,sha256=rfqHzTyFKfQC37a8Drkf3RPB3g_MFIdwPohAC_bEIN8,308
|
|
2
|
+
project_kit/__main__.py,sha256=30-4ifzqERnbwzERDfR8OIvPTAnCa1p4cRLKqJnXdR0,63
|
|
3
|
+
project_kit/agents.py,sha256=LSbperuz7qfNRPeQfu7e3DhjQ2KEJ4f7D5s6s8xvDkw,2575
|
|
4
|
+
project_kit/ai_attribution.py,sha256=SYmYUxNdwEFj0jL2ZIXvLCRfNM0Y6dBzT5N0yZn_BZU,2848
|
|
5
|
+
project_kit/approval.py,sha256=FNC40ZH_CslKSiGnoakPkkQH3YuQg2smny_YDnr6vW4,1490
|
|
6
|
+
project_kit/assets/hooks/commit-msg,sha256=hXK6LP8jC4bxq8AkfktQ6Ff65OEnAs_94fL9r1BZ8aU,573
|
|
7
|
+
project_kit/assets/project/workflow.yml,sha256=XVxUIuLAyVqs170lbseAXX6YiMdCao_udJmXx1F_tLM,1892
|
|
8
|
+
project_kit/assets/skills/pk-decide/SKILL.md,sha256=bXE7_fdKxs4pHC5Nsf_HelwGXKh6hlyG4-6HZwZt5TU,3572
|
|
9
|
+
project_kit/assets/skills/pk-discover/SKILL.md,sha256=ziwlpu5S_o1UG8wajV4BhNfrJi1scN_H1EFSkQvR_2o,3454
|
|
10
|
+
project_kit/assets/skills/pk-handoff/SKILL.md,sha256=uoxd_mST1OGDMRchTzQ7U9eEcF3O3ntsVuRk0k8CnrA,2613
|
|
11
|
+
project_kit/assets/skills/pk-plan/SKILL.md,sha256=9lRB97dEMtOnlxXjBrY-CrgaKUcRASpdvs-5rlP2qws,3174
|
|
12
|
+
project_kit/assets/skills/pk-provision/SKILL.md,sha256=HQgJusTvK41lhhtmk2tLDcvb7szPWzvKbw0CDA72IlU,2457
|
|
13
|
+
project_kit/assets/skills/pk-start/SKILL.md,sha256=5PrjdfdxlWwkjqV4LQS65bhDJiRiF5UPEz3LeaNKgd8,2852
|
|
14
|
+
project_kit/assets/skills/pk-workflow/SKILL.md,sha256=PO8IYLCcvzwlfGBobCKCbj7P7-wvMau0eF8QbkEyIzE,2654
|
|
15
|
+
project_kit/assets/templates/adr.md,sha256=akh47VtHesUwu04xqWuF0lMxtFqX_la2FUNiJdOdF5A,760
|
|
16
|
+
project_kit/assets/templates/backlog.example.json,sha256=J5ccoKNq5oiTkn6BwELOUDybsbvtUnhVrTy-N4Ti8IE,1745
|
|
17
|
+
project_kit/assets/templates/brief.md,sha256=acTirEJx_PzuYiXjscG3dh_Ca27KlL3UFxHj6X6I5_o,1502
|
|
18
|
+
project_kit/assets/templates/constitution-input.md,sha256=UzrkT4YvnA-tIfOZycwIp085ferK-pw47GZQOI8ooPk,3968
|
|
19
|
+
project_kit/assets/templates/development-workflow.md,sha256=7Y7mknEvGQ2RZMTBdkd2-m24veP3yTBArXCzttwYIhA,1397
|
|
20
|
+
project_kit/assets/templates/github/ISSUE_TEMPLATE/adr.yml,sha256=QZkRGgOR84jwH1oEtqf01J97Im97Hft9GOVUNG0Tzwk,459
|
|
21
|
+
project_kit/assets/templates/github/ISSUE_TEMPLATE/bug.yml,sha256=V4wW0mPYh7Nck9zVe1cg7I8l0iKK8hJTk_aTc6tRoyI,530
|
|
22
|
+
project_kit/assets/templates/github/ISSUE_TEMPLATE/config.yml,sha256=HxA8ap3QfNE6mm8XrOa4E_R3R-uct-AEiMsgc8qvkbs,28
|
|
23
|
+
project_kit/assets/templates/github/ISSUE_TEMPLATE/epic.yml,sha256=CV_bMgSimkG0CyD4IAwf7VTJpC_vyJrX3yhnp7jW0cw,309
|
|
24
|
+
project_kit/assets/templates/github/ISSUE_TEMPLATE/feature.yml,sha256=m-IT6NG7AESFuBr9B51mFmERS20Kd_jZG0Jl19JuH_s,475
|
|
25
|
+
project_kit/assets/templates/github/pull_request_template.md,sha256=VFVxbuFFVkYxfXi6R8j_v1fRhEbKEYUBjPmE20q3QAc,394
|
|
26
|
+
project_kit/assets/templates/interview.md,sha256=iQ74Ml5NkwAoFYsjiaFSuua16kxeqQN_s091-gXDm4E,3612
|
|
27
|
+
project_kit/backlog.py,sha256=sLY1TWynro9q9Nb3G1i2_KIwwJm4-T6PkpGRcw8vGFE,11247
|
|
28
|
+
project_kit/cli.py,sha256=NbB3WOO1IjDjuR-nzDkTVgMOnNJputi8SPWqFwzFkQI,9933
|
|
29
|
+
project_kit/config.py,sha256=PP222aKodd8Di1ShDxygJDRQ-JWH9BcN2hOzQHkTwxQ,4817
|
|
30
|
+
project_kit/doctor.py,sha256=qZ5GB_NJ_2BHvLwZmzr94XRh-tAC2W6b6vVv1qlyiUY,5790
|
|
31
|
+
project_kit/fsutil.py,sha256=MoocEm8qAvGw4HfAkQzWmwxEp6fId55dkQmi62j9n7c,951
|
|
32
|
+
project_kit/gh.py,sha256=u-PjjpCrgbHitr0tU-s2f7ntRAMk2YKldNDz-vhennw,5491
|
|
33
|
+
project_kit/handoff.py,sha256=289o1NUpRtCssvFU9KqnUmHCazwlnQ6zO0-xyjL4xPg,12490
|
|
34
|
+
project_kit/help.py,sha256=Y8e126puyP69St0t_7jz1gLYTC8_FLREti5ZgAnSpzw,5793
|
|
35
|
+
project_kit/installer.py,sha256=gYu-naIWu7X-PSBXl38m4RB4HMMmy_GsZLCnQU7wAao,3767
|
|
36
|
+
project_kit/labels.py,sha256=I5l_g7QRJWlzrZdoQWDuBM4hsn7HOeVn0fX1P8GfI_Y,764
|
|
37
|
+
project_kit/project_choice.py,sha256=D7yBF3VLQfQLc2KUQg4ubXZyeS06ygjgCelkOWu3_EM,4663
|
|
38
|
+
project_kit/providers/__init__.py,sha256=iV97GHRPiMZJq0w6j0lRV4FcYToM9m_5ccMioagdAT0,85
|
|
39
|
+
project_kit/providers/github.py,sha256=0ZBDqxWkWF1JTMnQ9qV6ivvEpMDmxwhW56CGP-NS7ck,12793
|
|
40
|
+
project_kit/provision.py,sha256=qecVdcgAGIJVNw7QuOfVXXzIqmaQmzMj19-aOpy6nWI,9651
|
|
41
|
+
project_kit/provision_issues.py,sha256=rr2wq5n-rFPNTo2okppW1L-bzRV8dbhQjyxEPX8OGEQ,14449
|
|
42
|
+
project_kit/state.py,sha256=S9d_2iPQuSi0hi2gdOwHT8zOSOlyNejhOjafJwbyFHU,4003
|
|
43
|
+
project_kit/templating.py,sha256=BV3e51DktlUxOtIcahp2iq8X0f63MfxZktFjh6Pwnpw,826
|
|
44
|
+
project_kit/workflow.py,sha256=C6PD-nTNIrzznn2aJ0L1qlGTnqu9ZC5dz0jFfKGNL00,6204
|
|
45
|
+
project_kit_cli-0.1.0.dist-info/licenses/LICENSE,sha256=dFazNRGw47ZuUFnKCZRvdgbjpEH2aO--gWslO1Ck3Us,1070
|
|
46
|
+
project_kit_cli-0.1.0.dist-info/WHEEL,sha256=9IUYslYuG_asT4WlWvfk_K76nveuLvSTKDoLeL0cV-Q,80
|
|
47
|
+
project_kit_cli-0.1.0.dist-info/entry_points.txt,sha256=HtNObc7MmuDfwtN4c9mktht-Sri29HVzEg3DSjtj9cE,54
|
|
48
|
+
project_kit_cli-0.1.0.dist-info/METADATA,sha256=uLIQ20pbse9sKYHzpcH5Vt33M0UFsFeHerk4d1YiiuQ,4708
|
|
49
|
+
project_kit_cli-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Miguel Tomás
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|