spec-agent-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.
- spec_agent/__init__.py +11 -0
- spec_agent/__main__.py +9 -0
- spec_agent/cli.py +70 -0
- spec_agent/commands/__init__.py +1 -0
- spec_agent/commands/init.py +29 -0
- spec_agent/installer.py +201 -0
- spec_agent/package_assets.py +56 -0
- spec_agent/resources/project-rules.md +18 -0
- spec_agent/resources/skills/spec-drift-sync/SKILL.md +60 -0
- spec_agent/resources/skills/spec-drift-sync/scripts/check.py +404 -0
- spec_agent/resources/skills/spec-evolution/SKILL.md +57 -0
- spec_agent/resources/skills/spec-evolution/scripts/record.py +126 -0
- spec_agent/resources/skills/spec-evolution/scripts/timeline.py +138 -0
- spec_agent/resources/skills/spec-request-flow/SKILL.md +78 -0
- spec_agent/resources/skills/spec-request-flow/assets/SPEC.template.md +41 -0
- spec_agent/resources/skills/spec-request-flow/assets/feature-acceptance.template.md +46 -0
- spec_agent/resources/skills/spec-request-flow/assets/feature-spec.template.md +71 -0
- spec_agent/resources/skills/spec-request-flow/references/spec-format.md +60 -0
- spec_agent/resources/skills/spec-request-flow/scripts/create_feature.py +63 -0
- spec_agent_cli-0.1.0.dist-info/METADATA +86 -0
- spec_agent_cli-0.1.0.dist-info/RECORD +24 -0
- spec_agent_cli-0.1.0.dist-info/WHEEL +4 -0
- spec_agent_cli-0.1.0.dist-info/entry_points.txt +2 -0
- spec_agent_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
spec_agent/__init__.py
ADDED
spec_agent/__main__.py
ADDED
spec_agent/cli.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Command-line interface for installing Spec Agent into repositories."""
|
|
2
|
+
|
|
3
|
+
# spec: CLI-001, CLI-007, CLI-008
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import sys
|
|
8
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
import typer
|
|
12
|
+
|
|
13
|
+
from spec_agent.commands.init import cmd_init
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
app = typer.Typer(
|
|
17
|
+
no_args_is_help=True,
|
|
18
|
+
add_completion=True,
|
|
19
|
+
help="Install the strict Spec Agent workflow into a repository.",
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _version_callback(value: bool) -> None:
|
|
24
|
+
if not value:
|
|
25
|
+
return
|
|
26
|
+
try:
|
|
27
|
+
current = version("spec-agent-cli")
|
|
28
|
+
except PackageNotFoundError:
|
|
29
|
+
current = "dev"
|
|
30
|
+
print(f"spec-agent {current}")
|
|
31
|
+
raise typer.Exit()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@app.callback()
|
|
35
|
+
def root(
|
|
36
|
+
show_version: bool = typer.Option(
|
|
37
|
+
False,
|
|
38
|
+
"--version",
|
|
39
|
+
help="Show the installed version and exit.",
|
|
40
|
+
callback=_version_callback,
|
|
41
|
+
is_eager=True,
|
|
42
|
+
),
|
|
43
|
+
) -> None:
|
|
44
|
+
"""Install and maintain Spec Agent project assets."""
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@app.command("init")
|
|
48
|
+
def init_command(
|
|
49
|
+
repo: Path = typer.Option(Path("."), "--repo", help="Repository to initialize."),
|
|
50
|
+
check: bool = typer.Option(False, "--check", help="Report status; write nothing."),
|
|
51
|
+
force: bool = typer.Option(
|
|
52
|
+
False,
|
|
53
|
+
"--force",
|
|
54
|
+
help="Update locally changed managed skill files.",
|
|
55
|
+
),
|
|
56
|
+
) -> None:
|
|
57
|
+
"""Install .agents/skills and scaffold missing specification files."""
|
|
58
|
+
raise typer.Exit(cmd_init(repo, check=check, force=force))
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def main(argv: list[str] | None = None) -> int:
|
|
62
|
+
try:
|
|
63
|
+
app(args=argv, prog_name="spec-agent")
|
|
64
|
+
except SystemExit as error:
|
|
65
|
+
return error.code if isinstance(error.code, int) else 1
|
|
66
|
+
return 0
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
if __name__ == "__main__":
|
|
70
|
+
sys.exit(main())
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Spec Agent command implementations."""
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Implementation of ``spec-agent init``."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from spec_agent import installer
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _print_rows(rows: list[tuple[str, str]]) -> None:
|
|
11
|
+
width = max((len(path) for path, _ in rows), default=0)
|
|
12
|
+
for path, status in rows:
|
|
13
|
+
print(f" {path.ljust(width)} {status}")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def cmd_init(repo_root: Path, *, check: bool = False, force: bool = False) -> int:
|
|
17
|
+
root = repo_root.resolve()
|
|
18
|
+
if check:
|
|
19
|
+
rows = installer.check_status(root)
|
|
20
|
+
_print_rows(rows)
|
|
21
|
+
valid = {"up to date", "present"}
|
|
22
|
+
return 0 if all(status in valid for _, status in rows) else 1
|
|
23
|
+
|
|
24
|
+
rows = installer.install(root, force=force)
|
|
25
|
+
print(f"Spec Agent initialized in {root}")
|
|
26
|
+
_print_rows(rows)
|
|
27
|
+
print("")
|
|
28
|
+
print("Next: ask your agent to use spec-request-flow for a feature request.")
|
|
29
|
+
return 0 if all(status != installer.DAMAGED for _, status in rows) else 1
|
spec_agent/installer.py
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""Safely install packaged Spec Agent assets into a repository."""
|
|
2
|
+
|
|
3
|
+
# spec: CLI-002, CLI-003, CLI-004, CLI-005, CLI-006, CLI-007, CLI-008, CLI-009, CLI-010, CLI-011, SA-017, SA-018, SA-019
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from spec_agent import package_assets
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
MARKER_START = "<!-- spec-agent:rules:start -->"
|
|
14
|
+
MARKER_END = "<!-- spec-agent:rules:end -->"
|
|
15
|
+
DAMAGED = "damaged markers — fix or remove the spec-agent:rules markers, then re-run"
|
|
16
|
+
|
|
17
|
+
EMPTY_TIMELINE = """<!-- Generated from spec/evolution/events.jsonl by .agents/skills/spec-evolution/scripts/timeline.py; do not edit. -->
|
|
18
|
+
# Specification Evolution Timeline
|
|
19
|
+
|
|
20
|
+
## Mermaid
|
|
21
|
+
|
|
22
|
+
```mermaid
|
|
23
|
+
timeline
|
|
24
|
+
title Specification Evolution
|
|
25
|
+
```
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
EMPTY_TRACEABILITY = json.dumps(
|
|
29
|
+
{
|
|
30
|
+
"behaviors": {},
|
|
31
|
+
"schema_version": 1,
|
|
32
|
+
"source": "derived-from-code-backlinks",
|
|
33
|
+
},
|
|
34
|
+
indent=2,
|
|
35
|
+
sort_keys=True,
|
|
36
|
+
) + "\n"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def load_skill_files() -> dict[str, bytes]:
|
|
40
|
+
"""Return every file from the three canonical skill directories."""
|
|
41
|
+
return package_assets.skill_files()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def render_rules_block(body: str) -> str:
|
|
45
|
+
return f"{MARKER_START}\n{body.strip()}\n{MARKER_END}"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _find_block(text: str) -> tuple[int, int] | None:
|
|
49
|
+
starts = text.count(MARKER_START)
|
|
50
|
+
ends = text.count(MARKER_END)
|
|
51
|
+
if starts == 0 and ends == 0:
|
|
52
|
+
return None
|
|
53
|
+
if starts != 1 or ends != 1:
|
|
54
|
+
raise ValueError("unbalanced spec-agent rules markers")
|
|
55
|
+
start = text.find(MARKER_START)
|
|
56
|
+
end = text.find(MARKER_END)
|
|
57
|
+
if end < start:
|
|
58
|
+
raise ValueError("spec-agent rules end marker precedes start marker")
|
|
59
|
+
return start, end + len(MARKER_END)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _managed_file_status(path: Path, expected: bytes) -> str:
|
|
63
|
+
if not path.is_file():
|
|
64
|
+
return "missing"
|
|
65
|
+
return "up to date" if path.read_bytes() == expected else "outdated"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _rules_status(path: Path, block: str) -> str:
|
|
69
|
+
if not path.is_file():
|
|
70
|
+
return "missing"
|
|
71
|
+
text = path.read_text(encoding="utf-8")
|
|
72
|
+
try:
|
|
73
|
+
span = _find_block(text)
|
|
74
|
+
except ValueError:
|
|
75
|
+
return DAMAGED
|
|
76
|
+
if span is None:
|
|
77
|
+
return "missing"
|
|
78
|
+
return "up to date" if text[span[0] : span[1]] == block else "outdated"
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _apply_managed_file(path: Path, expected: bytes, force: bool) -> str:
|
|
82
|
+
if not path.is_file():
|
|
83
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
84
|
+
path.write_bytes(expected)
|
|
85
|
+
return "installed"
|
|
86
|
+
if path.read_bytes() == expected:
|
|
87
|
+
return "up to date"
|
|
88
|
+
if force:
|
|
89
|
+
path.write_bytes(expected)
|
|
90
|
+
return "updated"
|
|
91
|
+
return "outdated (--force to update)"
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _apply_rules(path: Path, block: str) -> str:
|
|
95
|
+
if not path.is_file():
|
|
96
|
+
path.write_text(block + "\n", encoding="utf-8")
|
|
97
|
+
return "created"
|
|
98
|
+
text = path.read_text(encoding="utf-8")
|
|
99
|
+
try:
|
|
100
|
+
span = _find_block(text)
|
|
101
|
+
except ValueError:
|
|
102
|
+
return DAMAGED
|
|
103
|
+
if span is None:
|
|
104
|
+
updated = text.rstrip("\n") + ("\n\n" if text.strip() else "") + block + "\n"
|
|
105
|
+
else:
|
|
106
|
+
updated = text[: span[0]] + block + text[span[1] :]
|
|
107
|
+
if updated == text:
|
|
108
|
+
return "up to date"
|
|
109
|
+
path.write_text(updated, encoding="utf-8")
|
|
110
|
+
return "rules block added" if span is None else "updated"
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _create_file(path: Path, content: str) -> str:
|
|
114
|
+
if path.exists():
|
|
115
|
+
return "present"
|
|
116
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
117
|
+
path.write_text(content, encoding="utf-8")
|
|
118
|
+
return "created"
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _create_directory(path: Path) -> str:
|
|
122
|
+
if path.is_dir():
|
|
123
|
+
return "present"
|
|
124
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
125
|
+
return "created"
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _scaffold_items(repo_root: Path) -> list[tuple[str, str]]:
|
|
129
|
+
return [
|
|
130
|
+
("SPEC.md", "present" if (repo_root / "SPEC.md").is_file() else "missing"),
|
|
131
|
+
(
|
|
132
|
+
"spec/features/",
|
|
133
|
+
"present" if (repo_root / "spec/features").is_dir() else "missing",
|
|
134
|
+
),
|
|
135
|
+
(
|
|
136
|
+
"spec/evolution/events.jsonl",
|
|
137
|
+
"present"
|
|
138
|
+
if (repo_root / "spec/evolution/events.jsonl").is_file()
|
|
139
|
+
else "missing",
|
|
140
|
+
),
|
|
141
|
+
(
|
|
142
|
+
"spec/evolution/timeline.md",
|
|
143
|
+
"present"
|
|
144
|
+
if (repo_root / "spec/evolution/timeline.md").is_file()
|
|
145
|
+
else "missing",
|
|
146
|
+
),
|
|
147
|
+
(
|
|
148
|
+
"spec/traceability.json",
|
|
149
|
+
"present" if (repo_root / "spec/traceability.json").is_file() else "missing",
|
|
150
|
+
),
|
|
151
|
+
]
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def check_status(repo_root: Path) -> list[tuple[str, str]]:
|
|
155
|
+
"""Report installation state without writing anything."""
|
|
156
|
+
root = repo_root.resolve()
|
|
157
|
+
rows = [
|
|
158
|
+
(
|
|
159
|
+
f".agents/skills/{relative}",
|
|
160
|
+
_managed_file_status(root / ".agents/skills" / relative, content),
|
|
161
|
+
)
|
|
162
|
+
for relative, content in sorted(load_skill_files().items())
|
|
163
|
+
]
|
|
164
|
+
block = render_rules_block(package_assets.rules_text())
|
|
165
|
+
rows.append(("AGENTS.md", _rules_status(root / "AGENTS.md", block)))
|
|
166
|
+
rows.extend(_scaffold_items(root))
|
|
167
|
+
return rows
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def install(repo_root: Path, *, force: bool = False) -> list[tuple[str, str]]:
|
|
171
|
+
"""Install agent assets and scaffold missing project-owned spec files."""
|
|
172
|
+
root = repo_root.resolve()
|
|
173
|
+
root.mkdir(parents=True, exist_ok=True)
|
|
174
|
+
rows = [
|
|
175
|
+
(
|
|
176
|
+
f".agents/skills/{relative}",
|
|
177
|
+
_apply_managed_file(root / ".agents/skills" / relative, content, force),
|
|
178
|
+
)
|
|
179
|
+
for relative, content in sorted(load_skill_files().items())
|
|
180
|
+
]
|
|
181
|
+
block = render_rules_block(package_assets.rules_text())
|
|
182
|
+
rows.append(("AGENTS.md", _apply_rules(root / "AGENTS.md", block)))
|
|
183
|
+
rows.extend(
|
|
184
|
+
[
|
|
185
|
+
("SPEC.md", _create_file(root / "SPEC.md", package_assets.project_spec_template())),
|
|
186
|
+
("spec/features/", _create_directory(root / "spec/features")),
|
|
187
|
+
(
|
|
188
|
+
"spec/evolution/events.jsonl",
|
|
189
|
+
_create_file(root / "spec/evolution/events.jsonl", ""),
|
|
190
|
+
),
|
|
191
|
+
(
|
|
192
|
+
"spec/evolution/timeline.md",
|
|
193
|
+
_create_file(root / "spec/evolution/timeline.md", EMPTY_TIMELINE),
|
|
194
|
+
),
|
|
195
|
+
(
|
|
196
|
+
"spec/traceability.json",
|
|
197
|
+
_create_file(root / "spec/traceability.json", EMPTY_TRACEABILITY),
|
|
198
|
+
),
|
|
199
|
+
]
|
|
200
|
+
)
|
|
201
|
+
return rows
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Read canonical Spec Agent assets in source checkouts and built wheels."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from importlib.resources import files
|
|
6
|
+
from pathlib import Path, PurePosixPath
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _development_root() -> Path:
|
|
11
|
+
return Path(__file__).resolve().parents[2]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _embedded_root() -> Any:
|
|
15
|
+
return files("spec_agent").joinpath("resources")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def skill_root() -> Any:
|
|
19
|
+
embedded = _embedded_root().joinpath("skills")
|
|
20
|
+
if embedded.is_dir():
|
|
21
|
+
return embedded
|
|
22
|
+
return _development_root() / ".agents/skills"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def rules_text() -> str:
|
|
26
|
+
embedded = _embedded_root().joinpath("project-rules.md")
|
|
27
|
+
if embedded.is_file():
|
|
28
|
+
return embedded.read_text(encoding="utf-8")
|
|
29
|
+
return (_development_root() / "AGENTS.md").read_text(encoding="utf-8")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def project_spec_template() -> str:
|
|
33
|
+
return (
|
|
34
|
+
skill_root()
|
|
35
|
+
.joinpath("spec-request-flow")
|
|
36
|
+
.joinpath("assets")
|
|
37
|
+
.joinpath("SPEC.template.md")
|
|
38
|
+
.read_text(encoding="utf-8")
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def skill_files() -> dict[str, bytes]:
|
|
43
|
+
result: dict[str, bytes] = {}
|
|
44
|
+
|
|
45
|
+
def visit(directory: Any, prefix: PurePosixPath) -> None:
|
|
46
|
+
for entry in sorted(directory.iterdir(), key=lambda item: item.name):
|
|
47
|
+
if entry.name == "__pycache__" or entry.name.endswith((".pyc", ".pyo")):
|
|
48
|
+
continue
|
|
49
|
+
relative = prefix / entry.name
|
|
50
|
+
if entry.is_dir():
|
|
51
|
+
visit(entry, relative)
|
|
52
|
+
elif entry.is_file():
|
|
53
|
+
result[str(relative)] = entry.read_bytes()
|
|
54
|
+
|
|
55
|
+
visit(skill_root(), PurePosixPath())
|
|
56
|
+
return result
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Strict Spec Agent
|
|
2
|
+
|
|
3
|
+
Read root `SPEC.md`, then load one discoverable skill from `.agents/skills/`.
|
|
4
|
+
Feature packets contain product-only `spec.md` and `acceptance.md` files under
|
|
5
|
+
`spec/features/<feature-slug>/`.
|
|
6
|
+
|
|
7
|
+
| Situation | Load skill |
|
|
8
|
+
|---|---|
|
|
9
|
+
| Feature, fix, refactor, or behavior request | `spec-request-flow` |
|
|
10
|
+
| Possible spec/code/backlink disagreement | `spec-drift-sync` |
|
|
11
|
+
| Approved product change, rationale, or timeline | `spec-evolution` |
|
|
12
|
+
|
|
13
|
+
`spec-request-flow` ends after explicit approval and the Code Agent handoff. It
|
|
14
|
+
never writes implementation plans, code, tests, or engineering commands.
|
|
15
|
+
|
|
16
|
+
Code backlinks use `spec: BEHAVIOR-ID`. `spec-drift-sync` observes them and may
|
|
17
|
+
regenerate only non-normative `spec/traceability.json`; it never repairs code or
|
|
18
|
+
specifications. Product decision history lives in `spec/evolution/events.jsonl`.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: spec-drift-sync
|
|
3
|
+
description: Use when accepted specifications may disagree with implementations after refactors, merges, hotfixes, or code-only changes, or when backlinks, traceability, tests, or verification evidence may be missing or stale.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Spec Drift Sync
|
|
7
|
+
|
|
8
|
+
Act as a read-only observer of specification/code disagreement. Product specs
|
|
9
|
+
remain normative; code, tests, Git history, and passing verification are evidence.
|
|
10
|
+
|
|
11
|
+
<HARD-GATE>
|
|
12
|
+
Do not edit specifications, code, tests, code backlinks, or verification state. Do not
|
|
13
|
+
run an implementation or repair workflow. The only permitted write is regenerating the
|
|
14
|
+
non-normative derived traceability index from backlinks that already exist in code.
|
|
15
|
+
</HARD-GATE>
|
|
16
|
+
|
|
17
|
+
## Detection
|
|
18
|
+
|
|
19
|
+
1. Read root `SPEC.md`, both files in the affected feature packet, current code and
|
|
20
|
+
public behavior, existing `spec: BEHAVIOR-ID` backlinks, relevant Git changes, and
|
|
21
|
+
related product-decision history.
|
|
22
|
+
2. Run `scripts/check.py check --repo REPOSITORY`. Use `--spec-root SPEC_SOURCE` only
|
|
23
|
+
for an intentionally scoped observation.
|
|
24
|
+
3. Inspect meaning manually. A clean structural report proves identity consistency,
|
|
25
|
+
not semantic compliance.
|
|
26
|
+
|
|
27
|
+
## Traceability refresh
|
|
28
|
+
|
|
29
|
+
Run `scripts/check.py sync --repo REPOSITORY --output spec/traceability.json` to replace
|
|
30
|
+
only the derived traceability index. It maps behavior IDs to existing code backlink
|
|
31
|
+
locations. It must not be copied into `spec.md` or `acceptance.md`, treated as product
|
|
32
|
+
authority, or edited to conceal missing links.
|
|
33
|
+
|
|
34
|
+
## Evidence report
|
|
35
|
+
|
|
36
|
+
For every finding report:
|
|
37
|
+
|
|
38
|
+
- behavior ID and drift category;
|
|
39
|
+
- normative product evidence with specification path and rule;
|
|
40
|
+
- implementation evidence with code, interface, test, or Git location;
|
|
41
|
+
- structural identity problem or semantic conflict;
|
|
42
|
+
- observable impact and the authority decision required.
|
|
43
|
+
|
|
44
|
+
Structural findings include `phantom`, `dead-ref`, `silent-implementation`, `unlinked`,
|
|
45
|
+
and `stale-verification`. Semantic conflicts include code contradicting accepted
|
|
46
|
+
behavior, obsolete or ambiguous product rules, and behavior without product authority.
|
|
47
|
+
|
|
48
|
+
## Authority handoff
|
|
49
|
+
|
|
50
|
+
Ask one product decision at a time and present viable outcomes with impact. Never
|
|
51
|
+
silently promote observed code to intended behavior.
|
|
52
|
+
|
|
53
|
+
- If intended product behavior changes, hand the decision to `spec-request-flow` for
|
|
54
|
+
clarification, both spec files, and approval.
|
|
55
|
+
- If the approved product behavior remains correct, hand the evidence to a separate
|
|
56
|
+
Code Agent to plan engineering changes.
|
|
57
|
+
- Use `spec-evolution` only to preserve an approved product authority decision.
|
|
58
|
+
|
|
59
|
+
Stop after the evidence report and handoff. Do not apply the selected repair, add
|
|
60
|
+
backlinks, execute tests, weaken the checker, or mark verification current.
|