specjam 0.0.1__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.
- specjam-0.0.1/MANIFEST.in +3 -0
- specjam-0.0.1/PKG-INFO +112 -0
- specjam-0.0.1/README.md +99 -0
- specjam-0.0.1/pyproject.toml +53 -0
- specjam-0.0.1/setup.cfg +4 -0
- specjam-0.0.1/src/specjam/__init__.py +3 -0
- specjam-0.0.1/src/specjam/__main__.py +6 -0
- specjam-0.0.1/src/specjam/archive.py +52 -0
- specjam-0.0.1/src/specjam/classification.py +30 -0
- specjam-0.0.1/src/specjam/cli.py +124 -0
- specjam-0.0.1/src/specjam/graph_engine.py +211 -0
- specjam-0.0.1/src/specjam/installer.py +280 -0
- specjam-0.0.1/src/specjam/model.py +149 -0
- specjam-0.0.1/src/specjam/payload/__init__.py +2 -0
- specjam-0.0.1/src/specjam/payload/bridge/AGENTS.md +79 -0
- specjam-0.0.1/src/specjam/payload/ignore-rules.txt +7 -0
- specjam-0.0.1/src/specjam/payload/workspace/WORKSPACE.md +32 -0
- specjam-0.0.1/src/specjam/payload/workspace/config.json +8 -0
- specjam-0.0.1/src/specjam/payload/workspace/graphs/delivery-graph.json +61 -0
- specjam-0.0.1/src/specjam/payload/workspace/graphs/discovery-graph.json +39 -0
- specjam-0.0.1/src/specjam/payload/workspace/graphs/postmortem-graph.json +47 -0
- specjam-0.0.1/src/specjam/payload/workspace/references/RWSA.md +11 -0
- specjam-0.0.1/src/specjam/payload/workspace/runtime/capability-matrix.md +15 -0
- specjam-0.0.1/src/specjam/payload/workspace/skills/specjam-bounded-review/SKILL.md +33 -0
- specjam-0.0.1/src/specjam/payload/workspace/skills/specjam-bounded-review/rws.json +11 -0
- specjam-0.0.1/src/specjam/payload/workspace/skills/specjam-daily-loop/SKILL.md +32 -0
- specjam-0.0.1/src/specjam/payload/workspace/skills/specjam-daily-loop/rws.json +12 -0
- specjam-0.0.1/src/specjam/payload/workspace/skills/specjam-flow/SKILL.md +37 -0
- specjam-0.0.1/src/specjam/payload/workspace/skills/specjam-flow/rws.json +13 -0
- specjam-0.0.1/src/specjam/payload/workspace/skills/specjam-skill-authoring/SKILL.md +36 -0
- specjam-0.0.1/src/specjam/payload/workspace/skills/specjam-skill-authoring/references/RWSA.md +4 -0
- specjam-0.0.1/src/specjam/payload/workspace/skills/specjam-skill-authoring/rws.json +13 -0
- specjam-0.0.1/src/specjam/payload/workspace/skills/specjam-trace-to-skill/SKILL.md +37 -0
- specjam-0.0.1/src/specjam/payload/workspace/skills/specjam-trace-to-skill/references/RWSA.md +4 -0
- specjam-0.0.1/src/specjam/payload/workspace/skills/specjam-trace-to-skill/rws.json +12 -0
- specjam-0.0.1/src/specjam/reviewers.py +80 -0
- specjam-0.0.1/src/specjam/rws.py +186 -0
- specjam-0.0.1/src/specjam.egg-info/PKG-INFO +112 -0
- specjam-0.0.1/src/specjam.egg-info/SOURCES.txt +46 -0
- specjam-0.0.1/src/specjam.egg-info/dependency_links.txt +1 -0
- specjam-0.0.1/src/specjam.egg-info/entry_points.txt +2 -0
- specjam-0.0.1/src/specjam.egg-info/top_level.txt +1 -0
- specjam-0.0.1/tests/test_archive.py +35 -0
- specjam-0.0.1/tests/test_classification.py +16 -0
- specjam-0.0.1/tests/test_graph_engine.py +104 -0
- specjam-0.0.1/tests/test_installer.py +58 -0
- specjam-0.0.1/tests/test_reviewers.py +46 -0
- specjam-0.0.1/tests/test_rws.py +26 -0
specjam-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: specjam
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: A harness-neutral agentic engineering workspace built around flow graphs and RWSA skills.
|
|
5
|
+
Author: Fernando Costa
|
|
6
|
+
License-Expression: LicenseRef-Proprietary
|
|
7
|
+
Project-URL: Homepage, https://github.com/nandoligeiro/SpecJam
|
|
8
|
+
Project-URL: Repository, https://github.com/nandoligeiro/SpecJam
|
|
9
|
+
Project-URL: Issues, https://github.com/nandoligeiro/SpecJam/issues
|
|
10
|
+
Keywords: agents,engineering-workflow,flow-graphs,rws,cli
|
|
11
|
+
Requires-Python: >=3.11
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# SpecJam
|
|
15
|
+
|
|
16
|
+
SpecJam is an installable, harness-neutral agentic engineering workspace. It turns a natural-language request into a durable, auditable flow instead of allowing an agent to jump directly from ambiguity to implementation.
|
|
17
|
+
|
|
18
|
+
The first release combines three ideas:
|
|
19
|
+
|
|
20
|
+
- **Flow graphs**: declarative stages, artifact gates, conditional routing, bounded reviewers, and a pure routing function.
|
|
21
|
+
- **RWSA skills**: Routing, Workflow, Semantics, and Attachments as an executable contract for reusable agent capabilities.
|
|
22
|
+
- **A local workspace**: a standard-library-only CLI that installs, verifies, inspects, updates, and scaffolds the method in any repository.
|
|
23
|
+
|
|
24
|
+
## Quick start
|
|
25
|
+
|
|
26
|
+
Run SpecJam directly from PyPI with `uvx`, or install it persistently with `uv tool`:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
uvx specjam install
|
|
30
|
+
uv tool install specjam
|
|
31
|
+
specjam install
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
To pin this release explicitly:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
uvx --from 'specjam==0.0.1' specjam --help
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
For a source checkout, `uv run` keeps the package isolated and reproducible:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
uv run specjam install
|
|
44
|
+
uv run specjam verify
|
|
45
|
+
uv run specjam inspect
|
|
46
|
+
uv run specjam classify "Add a new payment capability"
|
|
47
|
+
uv run specjam flow scaffold --flow delivery --slug payment-capability
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
The installer creates `.specjam/` and a minimal root `AGENTS.md` bridge. Existing bridge files are preserved unless `--force` is supplied. Runtime state is ignored; the lockfile and managed definitions remain inspectable.
|
|
51
|
+
|
|
52
|
+
## Architecture
|
|
53
|
+
|
|
54
|
+
```text
|
|
55
|
+
natural language -----> discovery graph
|
|
56
|
+
|
|
|
57
|
+
+-----------------> delivery graph
|
|
58
|
+
|
|
|
59
|
+
+-----------------> postmortem graph
|
|
60
|
+
|
|
61
|
+
graph + state --> pure route --> decision --> append-only trail
|
|
62
|
+
|
|
|
63
|
+
+--> bounded read-only reviewers
|
|
64
|
+
|
|
|
65
|
+
+--> one synthesis writer
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
The route function never writes files, invokes tools, or calls a model. Persistence belongs to the trail adapter. This split makes the highest-risk policy easy to test.
|
|
69
|
+
|
|
70
|
+
## First-class flows
|
|
71
|
+
|
|
72
|
+
SpecJam keeps each flow explicit and data-driven:
|
|
73
|
+
|
|
74
|
+
- **Discovery** turns uncertainty into a problem statement, evidence, options, and a decision.
|
|
75
|
+
- **Delivery** runs SDD: `SPEC → DESIGN? → BUILD → VALIDATE`.
|
|
76
|
+
- **Postmortem** turns an incident record into root cause, actions, and follow-up.
|
|
77
|
+
|
|
78
|
+
The daily engineering loop is a supporting L0–L3 classification mechanism, not a fourth flow graph.
|
|
79
|
+
|
|
80
|
+
The three graphs are `graphs/discovery-graph.json`, `graphs/delivery-graph.json`, and `graphs/postmortem-graph.json`; organizations can add or replace graphs without changing the routing engine.
|
|
81
|
+
|
|
82
|
+
## RWSA contract
|
|
83
|
+
|
|
84
|
+
The skill representation follows the routing-aware decomposition described in [Workflow-to-Skill: Skill Creation via Routing-Workflow-Semantics-Attachments Decomposition](https://arxiv.org/abs/2606.06893):
|
|
85
|
+
|
|
86
|
+
```text
|
|
87
|
+
Skill = Routing + (Workflow + Semantics + Attachments)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
`src/specjam/rws.py` validates this contract without requiring a YAML or AI dependency. The bundled authoring skills render the contract into the portable `SKILL.md` format described by [Agent Skills](https://agentskills.io/specification).
|
|
91
|
+
|
|
92
|
+
## Development
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
uv run --no-sync python -m unittest discover -s tests -v
|
|
96
|
+
uv run specjam graph validate src/specjam/payload/workspace/graphs/delivery-graph.json
|
|
97
|
+
uv build --no-sources
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
The release workflow builds both wheel and source distribution on a `v*` tag and publishes them through PyPI Trusted Publishing. Configure the `pypi` GitHub environment and the matching PyPI trusted publisher before pushing a release tag.
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
uv version 0.0.1
|
|
104
|
+
uv build --no-sources
|
|
105
|
+
uv publish
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
The project intentionally keeps the engine dependency-free. Packaging helpers may use the Python standard library only; third-party model, agent, tracker, and cloud integrations are extension points.
|
|
109
|
+
|
|
110
|
+
## Status
|
|
111
|
+
|
|
112
|
+
This is the initial public foundation: graph engine, RWSA contract, reviewers, classification, installer, archive packaging, core skills, and test coverage. Organization-specific domain packs and tracker adapters stay outside the core as required by the PRD.
|
specjam-0.0.1/README.md
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# SpecJam
|
|
2
|
+
|
|
3
|
+
SpecJam is an installable, harness-neutral agentic engineering workspace. It turns a natural-language request into a durable, auditable flow instead of allowing an agent to jump directly from ambiguity to implementation.
|
|
4
|
+
|
|
5
|
+
The first release combines three ideas:
|
|
6
|
+
|
|
7
|
+
- **Flow graphs**: declarative stages, artifact gates, conditional routing, bounded reviewers, and a pure routing function.
|
|
8
|
+
- **RWSA skills**: Routing, Workflow, Semantics, and Attachments as an executable contract for reusable agent capabilities.
|
|
9
|
+
- **A local workspace**: a standard-library-only CLI that installs, verifies, inspects, updates, and scaffolds the method in any repository.
|
|
10
|
+
|
|
11
|
+
## Quick start
|
|
12
|
+
|
|
13
|
+
Run SpecJam directly from PyPI with `uvx`, or install it persistently with `uv tool`:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
uvx specjam install
|
|
17
|
+
uv tool install specjam
|
|
18
|
+
specjam install
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
To pin this release explicitly:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
uvx --from 'specjam==0.0.1' specjam --help
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
For a source checkout, `uv run` keeps the package isolated and reproducible:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
uv run specjam install
|
|
31
|
+
uv run specjam verify
|
|
32
|
+
uv run specjam inspect
|
|
33
|
+
uv run specjam classify "Add a new payment capability"
|
|
34
|
+
uv run specjam flow scaffold --flow delivery --slug payment-capability
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
The installer creates `.specjam/` and a minimal root `AGENTS.md` bridge. Existing bridge files are preserved unless `--force` is supplied. Runtime state is ignored; the lockfile and managed definitions remain inspectable.
|
|
38
|
+
|
|
39
|
+
## Architecture
|
|
40
|
+
|
|
41
|
+
```text
|
|
42
|
+
natural language -----> discovery graph
|
|
43
|
+
|
|
|
44
|
+
+-----------------> delivery graph
|
|
45
|
+
|
|
|
46
|
+
+-----------------> postmortem graph
|
|
47
|
+
|
|
48
|
+
graph + state --> pure route --> decision --> append-only trail
|
|
49
|
+
|
|
|
50
|
+
+--> bounded read-only reviewers
|
|
51
|
+
|
|
|
52
|
+
+--> one synthesis writer
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
The route function never writes files, invokes tools, or calls a model. Persistence belongs to the trail adapter. This split makes the highest-risk policy easy to test.
|
|
56
|
+
|
|
57
|
+
## First-class flows
|
|
58
|
+
|
|
59
|
+
SpecJam keeps each flow explicit and data-driven:
|
|
60
|
+
|
|
61
|
+
- **Discovery** turns uncertainty into a problem statement, evidence, options, and a decision.
|
|
62
|
+
- **Delivery** runs SDD: `SPEC → DESIGN? → BUILD → VALIDATE`.
|
|
63
|
+
- **Postmortem** turns an incident record into root cause, actions, and follow-up.
|
|
64
|
+
|
|
65
|
+
The daily engineering loop is a supporting L0–L3 classification mechanism, not a fourth flow graph.
|
|
66
|
+
|
|
67
|
+
The three graphs are `graphs/discovery-graph.json`, `graphs/delivery-graph.json`, and `graphs/postmortem-graph.json`; organizations can add or replace graphs without changing the routing engine.
|
|
68
|
+
|
|
69
|
+
## RWSA contract
|
|
70
|
+
|
|
71
|
+
The skill representation follows the routing-aware decomposition described in [Workflow-to-Skill: Skill Creation via Routing-Workflow-Semantics-Attachments Decomposition](https://arxiv.org/abs/2606.06893):
|
|
72
|
+
|
|
73
|
+
```text
|
|
74
|
+
Skill = Routing + (Workflow + Semantics + Attachments)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
`src/specjam/rws.py` validates this contract without requiring a YAML or AI dependency. The bundled authoring skills render the contract into the portable `SKILL.md` format described by [Agent Skills](https://agentskills.io/specification).
|
|
78
|
+
|
|
79
|
+
## Development
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
uv run --no-sync python -m unittest discover -s tests -v
|
|
83
|
+
uv run specjam graph validate src/specjam/payload/workspace/graphs/delivery-graph.json
|
|
84
|
+
uv build --no-sources
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
The release workflow builds both wheel and source distribution on a `v*` tag and publishes them through PyPI Trusted Publishing. Configure the `pypi` GitHub environment and the matching PyPI trusted publisher before pushing a release tag.
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
uv version 0.0.1
|
|
91
|
+
uv build --no-sources
|
|
92
|
+
uv publish
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
The project intentionally keeps the engine dependency-free. Packaging helpers may use the Python standard library only; third-party model, agent, tracker, and cloud integrations are extension points.
|
|
96
|
+
|
|
97
|
+
## Status
|
|
98
|
+
|
|
99
|
+
This is the initial public foundation: graph engine, RWSA contract, reviewers, classification, installer, archive packaging, core skills, and test coverage. Organization-specific domain packs and tracker adapters stay outside the core as required by the PRD.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "specjam"
|
|
7
|
+
version = "0.0.1"
|
|
8
|
+
description = "A harness-neutral agentic engineering workspace built around flow graphs and RWSA skills."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
authors = [{ name = "Fernando Costa" }]
|
|
12
|
+
license = "LicenseRef-Proprietary"
|
|
13
|
+
keywords = ["agents", "engineering-workflow", "flow-graphs", "rws", "cli"]
|
|
14
|
+
dependencies = []
|
|
15
|
+
|
|
16
|
+
[project.urls]
|
|
17
|
+
Homepage = "https://github.com/nandoligeiro/SpecJam"
|
|
18
|
+
Repository = "https://github.com/nandoligeiro/SpecJam"
|
|
19
|
+
Issues = "https://github.com/nandoligeiro/SpecJam/issues"
|
|
20
|
+
|
|
21
|
+
[project.scripts]
|
|
22
|
+
specjam = "specjam.cli:main"
|
|
23
|
+
|
|
24
|
+
[tool.setuptools]
|
|
25
|
+
package-dir = {"" = "src"}
|
|
26
|
+
|
|
27
|
+
[tool.setuptools.packages.find]
|
|
28
|
+
where = ["src"]
|
|
29
|
+
include = ["specjam", "specjam.*"]
|
|
30
|
+
exclude = ["specjam.payload.__pycache__", "specjam.payload.__pycache__.*"]
|
|
31
|
+
|
|
32
|
+
[tool.setuptools.package-data]
|
|
33
|
+
"specjam.payload" = [
|
|
34
|
+
"ignore-rules.txt",
|
|
35
|
+
"bridge/*.md",
|
|
36
|
+
"workspace/*.md",
|
|
37
|
+
"workspace/*.json",
|
|
38
|
+
"workspace/graphs/*.json",
|
|
39
|
+
"workspace/references/*.md",
|
|
40
|
+
"workspace/runtime/*.md",
|
|
41
|
+
"workspace/skills/*/*.md",
|
|
42
|
+
"workspace/skills/*/*.json",
|
|
43
|
+
"workspace/skills/*/references/*.md",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
[tool.setuptools.exclude-package-data]
|
|
47
|
+
"specjam.payload" = ["**/__pycache__/*", "**/*.py[cod]"]
|
|
48
|
+
|
|
49
|
+
[tool.uv]
|
|
50
|
+
package = true
|
|
51
|
+
|
|
52
|
+
[tool.pytest.ini_options]
|
|
53
|
+
testpaths = ["tests"]
|
specjam-0.0.1/setup.cfg
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Safe validation and extraction for self-contained SpecJam archives."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import stat
|
|
6
|
+
import zipfile
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from re import match
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class UnsafeArchiveError(ValueError):
|
|
12
|
+
"""Raised before extraction when an archive entry can escape its root."""
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _validate_name(name: str) -> None:
|
|
16
|
+
normalized = name.replace("\\", "/")
|
|
17
|
+
if normalized.startswith("/") or ".." in Path(normalized).parts:
|
|
18
|
+
raise UnsafeArchiveError(f"unsafe archive path: {name}")
|
|
19
|
+
if Path(normalized).drive or match(r"^[A-Za-z]:", normalized) or normalized.startswith("//"):
|
|
20
|
+
raise UnsafeArchiveError(f"archive path has a drive prefix: {name}")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def validate_archive(path: str | Path) -> None:
|
|
24
|
+
with zipfile.ZipFile(path) as archive:
|
|
25
|
+
for info in archive.infolist():
|
|
26
|
+
_validate_name(info.filename)
|
|
27
|
+
mode = (info.external_attr >> 16) & 0o170000
|
|
28
|
+
if mode == stat.S_IFLNK:
|
|
29
|
+
raise UnsafeArchiveError(f"symlink archive entry: {info.filename}")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def extract_archive(path: str | Path, destination: str | Path) -> None:
|
|
33
|
+
"""Validate every entry before writing any bytes."""
|
|
34
|
+
|
|
35
|
+
validate_archive(path)
|
|
36
|
+
root = Path(destination).resolve()
|
|
37
|
+
root.mkdir(parents=True, exist_ok=True)
|
|
38
|
+
with zipfile.ZipFile(path) as archive:
|
|
39
|
+
for info in archive.infolist():
|
|
40
|
+
relative = Path(info.filename.replace("\\", "/"))
|
|
41
|
+
target = (root / relative).resolve()
|
|
42
|
+
try:
|
|
43
|
+
target.relative_to(root)
|
|
44
|
+
except ValueError as exc:
|
|
45
|
+
raise UnsafeArchiveError(f"archive entry escapes destination: {info.filename}") from exc
|
|
46
|
+
if info.is_dir():
|
|
47
|
+
target.mkdir(parents=True, exist_ok=True)
|
|
48
|
+
continue
|
|
49
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
50
|
+
with archive.open(info) as source, target.open("wb") as sink:
|
|
51
|
+
while chunk := source.read(1024 * 1024):
|
|
52
|
+
sink.write(chunk)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Deterministic proportional effort classification."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True)
|
|
9
|
+
class Classification:
|
|
10
|
+
level: str
|
|
11
|
+
flow: str
|
|
12
|
+
reason: str
|
|
13
|
+
|
|
14
|
+
def to_dict(self) -> dict[str, str]:
|
|
15
|
+
return {"level": self.level, "flow": self.flow, "reason": self.reason}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def classify_request(text: str, *, ambiguous: bool = False, critical: bool = False) -> Classification:
|
|
19
|
+
normalized = text.strip().lower()
|
|
20
|
+
if not normalized:
|
|
21
|
+
return Classification("L3", "discovery", "empty requests are ambiguous and require discovery")
|
|
22
|
+
lookup_words = ("what is", "how do i", "lookup", "look up", "explain", "compare", "quantos", "qual é")
|
|
23
|
+
if not ambiguous and not critical and (normalized.endswith("?") or any(word in normalized for word in lookup_words)):
|
|
24
|
+
return Classification("L0", "daily", "lookup or explanation; no implementation specification required")
|
|
25
|
+
small_words = ("rename", "typo", "readme", "format", "small", "minor", "documentation")
|
|
26
|
+
if not ambiguous and not critical and any(word in normalized for word in small_words):
|
|
27
|
+
return Classification("L1", "daily", "bounded local change with proportional ceremony")
|
|
28
|
+
if ambiguous or critical or any(word in normalized for word in ("security", "payment", "migration", "breaking", "production")):
|
|
29
|
+
return Classification("L3", "discovery", "ambiguous or critical work requires discovery before delivery")
|
|
30
|
+
return Classification("L2", "delivery", "feature-sized work requires specification and implementation gates")
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""Command-line interface for SpecJam."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from .classification import classify_request
|
|
11
|
+
from .graph_engine import load_graph, record_route
|
|
12
|
+
from .installer import inspect_installation, install, scaffold_flow, update, verify
|
|
13
|
+
from .model import RouteState
|
|
14
|
+
from .rws import load_rwsa, validate_rwsa
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _emit(value) -> None:
|
|
18
|
+
json.dump(value, sys.stdout, indent=2, sort_keys=True)
|
|
19
|
+
sys.stdout.write("\n")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _flags(values: list[str] | None) -> dict[str, bool]:
|
|
23
|
+
result: dict[str, bool] = {}
|
|
24
|
+
for value in values or []:
|
|
25
|
+
key, separator, raw = value.partition("=")
|
|
26
|
+
result[key] = raw.lower() not in {"0", "false", "no", "off"} if separator else True
|
|
27
|
+
return result
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
31
|
+
parser = argparse.ArgumentParser(prog="specjam", description="Deterministic agentic engineering workspace")
|
|
32
|
+
commands = parser.add_subparsers(dest="command", required=True)
|
|
33
|
+
|
|
34
|
+
install_parser = commands.add_parser("install", help="install the managed workspace")
|
|
35
|
+
install_parser.add_argument("--target", default=".")
|
|
36
|
+
install_parser.add_argument("--force", action="store_true")
|
|
37
|
+
|
|
38
|
+
verify_parser = commands.add_parser("verify", help="verify managed files and lockfile")
|
|
39
|
+
verify_parser.add_argument("--target", default=".")
|
|
40
|
+
|
|
41
|
+
update_parser = commands.add_parser("update", help="update unchanged managed files")
|
|
42
|
+
update_parser.add_argument("--target", default=".")
|
|
43
|
+
update_parser.add_argument("--remove-stale", action="store_true")
|
|
44
|
+
|
|
45
|
+
inspect_parser = commands.add_parser("inspect", help="inspect installation metadata")
|
|
46
|
+
inspect_parser.add_argument("--target", default=".")
|
|
47
|
+
|
|
48
|
+
classify_parser = commands.add_parser("classify", help="classify work into L0-L3")
|
|
49
|
+
classify_parser.add_argument("text", nargs="+")
|
|
50
|
+
classify_parser.add_argument("--ambiguous", action="store_true")
|
|
51
|
+
classify_parser.add_argument("--critical", action="store_true")
|
|
52
|
+
|
|
53
|
+
graph = commands.add_parser("graph", help="validate a graph")
|
|
54
|
+
graph_commands = graph.add_subparsers(dest="graph_command", required=True)
|
|
55
|
+
graph_validate = graph_commands.add_parser("validate")
|
|
56
|
+
graph_validate.add_argument("path")
|
|
57
|
+
|
|
58
|
+
route = commands.add_parser("route", help="evaluate one graph gate")
|
|
59
|
+
route.add_argument("--graph", required=True)
|
|
60
|
+
route.add_argument("--stage", required=True)
|
|
61
|
+
route.add_argument("--artifact", action="append", default=[])
|
|
62
|
+
route.add_argument("--flag", action="append", default=[])
|
|
63
|
+
route.add_argument("--trail")
|
|
64
|
+
route.add_argument("--run-id", default="local")
|
|
65
|
+
|
|
66
|
+
rws = commands.add_parser("rws", help="validate an RWSA contract")
|
|
67
|
+
rws_commands = rws.add_subparsers(dest="rws_command", required=True)
|
|
68
|
+
rws_validate = rws_commands.add_parser("validate")
|
|
69
|
+
rws_validate.add_argument("path")
|
|
70
|
+
|
|
71
|
+
flow = commands.add_parser("flow", help="scaffold durable flow artifacts")
|
|
72
|
+
flow_commands = flow.add_subparsers(dest="flow_command", required=True)
|
|
73
|
+
scaffold = flow_commands.add_parser("scaffold")
|
|
74
|
+
scaffold.add_argument("--target", default=".")
|
|
75
|
+
scaffold.add_argument("--flow", required=True, choices=("discovery", "delivery", "postmortem"))
|
|
76
|
+
scaffold.add_argument("--slug", required=True)
|
|
77
|
+
return parser
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def main(argv: list[str] | None = None) -> int:
|
|
81
|
+
args = build_parser().parse_args(argv)
|
|
82
|
+
if args.command == "install":
|
|
83
|
+
_emit(install(args.target, force=args.force).to_dict())
|
|
84
|
+
return 0
|
|
85
|
+
if args.command == "verify":
|
|
86
|
+
report = verify(args.target)
|
|
87
|
+
_emit(report.to_dict())
|
|
88
|
+
return 0 if not (report.missing or report.modified or report.stale) else 1
|
|
89
|
+
if args.command == "update":
|
|
90
|
+
_emit(update(args.target, remove_stale=args.remove_stale).to_dict())
|
|
91
|
+
return 0
|
|
92
|
+
if args.command == "inspect":
|
|
93
|
+
_emit(inspect_installation(args.target))
|
|
94
|
+
return 0
|
|
95
|
+
if args.command == "classify":
|
|
96
|
+
_emit(classify_request(" ".join(args.text), ambiguous=args.ambiguous, critical=args.critical).to_dict())
|
|
97
|
+
return 0
|
|
98
|
+
if args.command == "graph" and args.graph_command == "validate":
|
|
99
|
+
graph = load_graph(args.path)
|
|
100
|
+
_emit({"valid": True, "graph": graph.id, "nodes": sorted(graph.nodes)})
|
|
101
|
+
return 0
|
|
102
|
+
if args.command == "route":
|
|
103
|
+
graph = load_graph(args.graph)
|
|
104
|
+
state = RouteState(args.stage, frozenset(args.artifact), _flags(args.flag))
|
|
105
|
+
if args.trail:
|
|
106
|
+
decision = record_route(__import__("specjam.graph_engine", fromlist=["TrailStore"]).TrailStore(args.trail), args.run_id, graph, state)
|
|
107
|
+
else:
|
|
108
|
+
from .graph_engine import route
|
|
109
|
+
decision = route(graph, state)
|
|
110
|
+
_emit(decision.to_dict())
|
|
111
|
+
return 0 if decision.may_advance or not decision.blocked else 2
|
|
112
|
+
if args.command == "rws" and args.rws_command == "validate":
|
|
113
|
+
profile = load_rwsa(args.path)
|
|
114
|
+
_emit({"valid": True, "skill": profile.routing.name, "workflow_steps": len(profile.workflow)})
|
|
115
|
+
return 0
|
|
116
|
+
if args.command == "flow" and args.flow_command == "scaffold":
|
|
117
|
+
path = scaffold_flow(args.target, args.flow, args.slug)
|
|
118
|
+
_emit({"flow": args.flow, "path": str(path)})
|
|
119
|
+
return 0
|
|
120
|
+
return 2
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
if __name__ == "__main__":
|
|
124
|
+
raise SystemExit(main())
|