spec-agent-cli 0.1.0__tar.gz → 0.2.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.
- {spec_agent_cli-0.1.0 → spec_agent_cli-0.2.0}/.agents/skills/spec-drift-sync/SKILL.md +16 -9
- {spec_agent_cli-0.1.0 → spec_agent_cli-0.2.0}/.agents/skills/spec-drift-sync/scripts/check.py +129 -6
- spec_agent_cli-0.2.0/.agents/skills/spec-request-flow/SKILL.md +71 -0
- {spec_agent_cli-0.1.0 → spec_agent_cli-0.2.0}/.agents/skills/spec-request-flow/scripts/create_feature.py +1 -1
- {spec_agent_cli-0.1.0 → spec_agent_cli-0.2.0}/AGENTS.md +14 -0
- {spec_agent_cli-0.1.0 → spec_agent_cli-0.2.0}/PKG-INFO +18 -2
- {spec_agent_cli-0.1.0 → spec_agent_cli-0.2.0}/README.md +17 -1
- {spec_agent_cli-0.1.0 → spec_agent_cli-0.2.0}/SPEC.md +23 -1
- {spec_agent_cli-0.1.0 → spec_agent_cli-0.2.0}/pyproject.toml +1 -1
- {spec_agent_cli-0.1.0 → spec_agent_cli-0.2.0}/src/spec_agent/cli.py +19 -0
- spec_agent_cli-0.2.0/src/spec_agent/commands/traceability.py +35 -0
- {spec_agent_cli-0.1.0 → spec_agent_cli-0.2.0}/src/spec_agent/installer.py +3 -2
- {spec_agent_cli-0.1.0 → spec_agent_cli-0.2.0}/tests/test_agent_assets.py +12 -0
- spec_agent_cli-0.2.0/tests/test_cli.py +50 -0
- {spec_agent_cli-0.1.0 → spec_agent_cli-0.2.0}/tests/test_cli_installer.py +7 -0
- {spec_agent_cli-0.1.0 → spec_agent_cli-0.2.0}/tests/test_skill_workflows.py +14 -3
- {spec_agent_cli-0.1.0 → spec_agent_cli-0.2.0}/tests/test_spec_drift.py +103 -5
- spec_agent_cli-0.1.0/.agents/skills/spec-request-flow/SKILL.md +0 -78
- spec_agent_cli-0.1.0/tests/test_cli.py +0 -23
- {spec_agent_cli-0.1.0 → spec_agent_cli-0.2.0}/.agents/skills/spec-evolution/SKILL.md +0 -0
- {spec_agent_cli-0.1.0 → spec_agent_cli-0.2.0}/.agents/skills/spec-evolution/scripts/record.py +0 -0
- {spec_agent_cli-0.1.0 → spec_agent_cli-0.2.0}/.agents/skills/spec-evolution/scripts/timeline.py +0 -0
- {spec_agent_cli-0.1.0 → spec_agent_cli-0.2.0}/.agents/skills/spec-request-flow/assets/SPEC.template.md +0 -0
- {spec_agent_cli-0.1.0 → spec_agent_cli-0.2.0}/.agents/skills/spec-request-flow/assets/feature-acceptance.template.md +0 -0
- {spec_agent_cli-0.1.0 → spec_agent_cli-0.2.0}/.agents/skills/spec-request-flow/assets/feature-spec.template.md +0 -0
- {spec_agent_cli-0.1.0 → spec_agent_cli-0.2.0}/.agents/skills/spec-request-flow/references/spec-format.md +0 -0
- {spec_agent_cli-0.1.0 → spec_agent_cli-0.2.0}/.gitignore +0 -0
- {spec_agent_cli-0.1.0 → spec_agent_cli-0.2.0}/LICENSE +0 -0
- {spec_agent_cli-0.1.0 → spec_agent_cli-0.2.0}/src/spec_agent/__init__.py +0 -0
- {spec_agent_cli-0.1.0 → spec_agent_cli-0.2.0}/src/spec_agent/__main__.py +0 -0
- {spec_agent_cli-0.1.0 → spec_agent_cli-0.2.0}/src/spec_agent/commands/__init__.py +0 -0
- {spec_agent_cli-0.1.0 → spec_agent_cli-0.2.0}/src/spec_agent/commands/init.py +0 -0
- {spec_agent_cli-0.1.0 → spec_agent_cli-0.2.0}/src/spec_agent/package_assets.py +0 -0
- {spec_agent_cli-0.1.0 → spec_agent_cli-0.2.0}/tests/test_end_to_end.py +0 -0
- {spec_agent_cli-0.1.0 → spec_agent_cli-0.2.0}/tests/test_event_log.py +0 -0
- {spec_agent_cli-0.1.0 → spec_agent_cli-0.2.0}/tests/test_feature_scaffold.py +0 -0
- {spec_agent_cli-0.1.0 → spec_agent_cli-0.2.0}/tests/test_timeline.py +0 -0
|
@@ -19,17 +19,23 @@ non-normative derived traceability index from backlinks that already exist in co
|
|
|
19
19
|
1. Read root `SPEC.md`, both files in the affected feature packet, current code and
|
|
20
20
|
public behavior, existing `spec: BEHAVIOR-ID` backlinks, relevant Git changes, and
|
|
21
21
|
related product-decision history.
|
|
22
|
-
2. Run `
|
|
23
|
-
|
|
22
|
+
2. Run `spec-agent validate --repo REPOSITORY`; if the CLI is unavailable, run
|
|
23
|
+
`scripts/check.py check --repo REPOSITORY`. Use `--spec-root SPEC_SOURCE` only for
|
|
24
|
+
an intentionally scoped observation.
|
|
24
25
|
3. Inspect meaning manually. A clean structural report proves identity consistency,
|
|
25
26
|
not semantic compliance.
|
|
26
27
|
|
|
27
28
|
## Traceability refresh
|
|
28
29
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
After an approved implementation or explicit reconciliation, run `spec-agent
|
|
31
|
+
traceability-sync --repo REPOSITORY`; if the CLI is unavailable, run `scripts/check.py
|
|
32
|
+
sync --repo REPOSITORY --output spec/traceability.json`. It records behavior IDs,
|
|
33
|
+
backlink locations, and linked-file fingerprints in the derived traceability baseline.
|
|
34
|
+
|
|
35
|
+
Do not refresh traceability to silence unexplained drift. Validate first, understand
|
|
36
|
+
existing findings, and refresh only when product intent or implementation authority is
|
|
37
|
+
explicit. Never copy traceability into `spec.md` or `acceptance.md` or treat it as
|
|
38
|
+
product authority.
|
|
33
39
|
|
|
34
40
|
## Evidence report
|
|
35
41
|
|
|
@@ -41,9 +47,10 @@ For every finding report:
|
|
|
41
47
|
- structural identity problem or semantic conflict;
|
|
42
48
|
- observable impact and the authority decision required.
|
|
43
49
|
|
|
44
|
-
Structural findings include `phantom`, `
|
|
45
|
-
and `stale-verification`.
|
|
46
|
-
|
|
50
|
+
Structural findings include `phantom`, `unlinked`, `unbaselined`, `stale-trace`,
|
|
51
|
+
`code-changed`, legacy `dead-ref`, `silent-implementation`, and `stale-verification`.
|
|
52
|
+
Semantic conflicts include code contradicting accepted behavior, obsolete or ambiguous
|
|
53
|
+
product rules, and behavior without product authority.
|
|
47
54
|
|
|
48
55
|
## Authority handoff
|
|
49
56
|
|
{spec_agent_cli-0.1.0 → spec_agent_cli-0.2.0}/.agents/skills/spec-drift-sync/scripts/check.py
RENAMED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
"""Dependency-free structural specification/code drift checker."""
|
|
3
3
|
|
|
4
|
-
# spec: SA-013, SA-014, SA-015
|
|
4
|
+
# spec: SA-013, SA-014, SA-015, SA-021, SA-022, SA-023, TRACE-004, TRACE-005, TRACE-006, TRACE-007, TRACE-008, TRACE-009, TRACE-011, TRACE-012
|
|
5
5
|
|
|
6
6
|
from __future__ import annotations
|
|
7
7
|
|
|
8
8
|
import argparse
|
|
9
|
+
import hashlib
|
|
9
10
|
import json
|
|
10
11
|
import re
|
|
11
12
|
import subprocess
|
|
@@ -42,6 +43,7 @@ class SpecEntry:
|
|
|
42
43
|
statement: str
|
|
43
44
|
spec_file: str
|
|
44
45
|
line: int
|
|
46
|
+
status: str | None
|
|
45
47
|
planned: bool
|
|
46
48
|
code_status: str | None
|
|
47
49
|
code_refs: tuple[str, ...]
|
|
@@ -87,7 +89,12 @@ def analyze_repo(
|
|
|
87
89
|
entries = tuple(
|
|
88
90
|
entry for source in specs for entry in parse_spec_entries(source, repo)
|
|
89
91
|
)
|
|
90
|
-
backlinks = tuple(
|
|
92
|
+
backlinks = tuple(
|
|
93
|
+
backlink
|
|
94
|
+
for backlink in scan_code_backlinks(repo)
|
|
95
|
+
if _is_spec_agent_source(repo)
|
|
96
|
+
or not backlink.code_file.startswith(".agents/skills/")
|
|
97
|
+
)
|
|
91
98
|
known = {entry.behavior_id: entry for entry in entries}
|
|
92
99
|
by_id: dict[str, list[CodeBacklink]] = {}
|
|
93
100
|
for backlink in backlinks:
|
|
@@ -119,7 +126,8 @@ def analyze_repo(
|
|
|
119
126
|
ref=ref,
|
|
120
127
|
)
|
|
121
128
|
)
|
|
122
|
-
|
|
129
|
+
requires_link = entry.status in {None, "approved"}
|
|
130
|
+
if requires_link and entry.planned and entry.behavior_id in by_id:
|
|
123
131
|
link = by_id[entry.behavior_id][0]
|
|
124
132
|
issues.append(
|
|
125
133
|
DriftIssue(
|
|
@@ -132,7 +140,8 @@ def analyze_repo(
|
|
|
132
140
|
)
|
|
133
141
|
)
|
|
134
142
|
if (
|
|
135
|
-
|
|
143
|
+
requires_link
|
|
144
|
+
and not entry.planned
|
|
136
145
|
and entry.code_status != "not_applicable"
|
|
137
146
|
and not entry.code_refs
|
|
138
147
|
and entry.behavior_id not in by_id
|
|
@@ -164,6 +173,10 @@ def analyze_repo(
|
|
|
164
173
|
)
|
|
165
174
|
)
|
|
166
175
|
|
|
176
|
+
traceability = _read_traceability(repo / "spec/traceability.json")
|
|
177
|
+
if traceability is not None:
|
|
178
|
+
issues.extend(_baseline_issues(repo, known, backlinks, traceability))
|
|
179
|
+
|
|
167
180
|
return DriftReport(
|
|
168
181
|
str(repo),
|
|
169
182
|
" | ".join(str(source) for source in specs),
|
|
@@ -181,6 +194,7 @@ def parse_spec_entries(spec_root: Path, repo_root: Path) -> Iterable[SpecEntry]:
|
|
|
181
194
|
text = _read(path)
|
|
182
195
|
verified = _frontmatter(text, "verified_commit")
|
|
183
196
|
code_status = _frontmatter(text, "code_status")
|
|
197
|
+
status = _frontmatter(text, "status")
|
|
184
198
|
lines = text.splitlines()
|
|
185
199
|
index = 0
|
|
186
200
|
while index < len(lines):
|
|
@@ -203,6 +217,7 @@ def parse_spec_entries(spec_root: Path, repo_root: Path) -> Iterable[SpecEntry]:
|
|
|
203
217
|
statement,
|
|
204
218
|
_relative(path, repo_root),
|
|
205
219
|
index + 1,
|
|
220
|
+
status,
|
|
206
221
|
"[planned]" in statement.lower(),
|
|
207
222
|
code_status,
|
|
208
223
|
tuple(refs),
|
|
@@ -227,17 +242,24 @@ def scan_code_backlinks(repo_root: Path) -> Iterable[CodeBacklink]:
|
|
|
227
242
|
def write_traceability(report: DriftReport, output: Path | str) -> dict[str, object]:
|
|
228
243
|
"""Write a deterministic, non-normative index derived from code backlinks."""
|
|
229
244
|
target = Path(output)
|
|
245
|
+
repo = Path(report.repo_root)
|
|
230
246
|
behaviors: dict[str, list[dict[str, object]]] = {}
|
|
231
247
|
for backlink in sorted(
|
|
232
248
|
report.backlinks,
|
|
233
249
|
key=lambda item: (item.behavior_id, item.code_file, item.line),
|
|
234
250
|
):
|
|
251
|
+
code_path = repo / backlink.code_file
|
|
235
252
|
behaviors.setdefault(backlink.behavior_id, []).append(
|
|
236
|
-
{
|
|
253
|
+
{
|
|
254
|
+
"path": backlink.code_file,
|
|
255
|
+
"line": backlink.line,
|
|
256
|
+
"file_sha256": _file_sha256(code_path),
|
|
257
|
+
}
|
|
237
258
|
)
|
|
238
259
|
document: dict[str, object] = {
|
|
239
|
-
"schema_version":
|
|
260
|
+
"schema_version": 2,
|
|
240
261
|
"source": "derived-from-code-backlinks",
|
|
262
|
+
"baseline_commit": _git(repo, ["rev-parse", "HEAD"]) or None,
|
|
241
263
|
"behaviors": behaviors,
|
|
242
264
|
}
|
|
243
265
|
target.parent.mkdir(parents=True, exist_ok=True)
|
|
@@ -248,6 +270,102 @@ def write_traceability(report: DriftReport, output: Path | str) -> dict[str, obj
|
|
|
248
270
|
return document
|
|
249
271
|
|
|
250
272
|
|
|
273
|
+
def _read_traceability(path: Path) -> dict[str, object] | None:
|
|
274
|
+
if not path.is_file():
|
|
275
|
+
return None
|
|
276
|
+
try:
|
|
277
|
+
value = json.loads(path.read_text(encoding="utf-8"))
|
|
278
|
+
except (json.JSONDecodeError, OSError):
|
|
279
|
+
return {"behaviors": {}, "invalid": True}
|
|
280
|
+
if not isinstance(value, dict) or not isinstance(value.get("behaviors"), dict):
|
|
281
|
+
return {"behaviors": {}, "invalid": True}
|
|
282
|
+
return value
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _baseline_issues(
|
|
286
|
+
repo: Path,
|
|
287
|
+
known: dict[str, SpecEntry],
|
|
288
|
+
backlinks: tuple[CodeBacklink, ...],
|
|
289
|
+
traceability: dict[str, object],
|
|
290
|
+
) -> list[DriftIssue]:
|
|
291
|
+
if traceability.get("invalid"):
|
|
292
|
+
return [
|
|
293
|
+
DriftIssue(
|
|
294
|
+
"invalid-traceability",
|
|
295
|
+
"TRACEABILITY",
|
|
296
|
+
"derived traceability file is invalid",
|
|
297
|
+
code_file="spec/traceability.json",
|
|
298
|
+
)
|
|
299
|
+
]
|
|
300
|
+
|
|
301
|
+
raw_behaviors = traceability.get("behaviors", {})
|
|
302
|
+
baseline: dict[tuple[str, str], dict[str, object]] = {}
|
|
303
|
+
if isinstance(raw_behaviors, dict):
|
|
304
|
+
for behavior_id, raw_links in raw_behaviors.items():
|
|
305
|
+
if not isinstance(behavior_id, str) or not isinstance(raw_links, list):
|
|
306
|
+
continue
|
|
307
|
+
for raw_link in raw_links:
|
|
308
|
+
if not isinstance(raw_link, dict):
|
|
309
|
+
continue
|
|
310
|
+
path = raw_link.get("path")
|
|
311
|
+
if isinstance(path, str) and path:
|
|
312
|
+
baseline[(behavior_id, path)] = raw_link
|
|
313
|
+
|
|
314
|
+
current = {(link.behavior_id, link.code_file): link for link in backlinks}
|
|
315
|
+
findings: list[DriftIssue] = []
|
|
316
|
+
for key, backlink in sorted(current.items()):
|
|
317
|
+
behavior_id, code_file = key
|
|
318
|
+
if not _entry_exists(behavior_id, known):
|
|
319
|
+
continue
|
|
320
|
+
previous = baseline.get(key)
|
|
321
|
+
if previous is None:
|
|
322
|
+
findings.append(
|
|
323
|
+
DriftIssue(
|
|
324
|
+
"unbaselined",
|
|
325
|
+
behavior_id,
|
|
326
|
+
"current backlink is not in the traceability baseline",
|
|
327
|
+
code_file=code_file,
|
|
328
|
+
line=backlink.line,
|
|
329
|
+
)
|
|
330
|
+
)
|
|
331
|
+
continue
|
|
332
|
+
expected_hash = previous.get("file_sha256")
|
|
333
|
+
if isinstance(expected_hash, str) and expected_hash:
|
|
334
|
+
current_hash = _file_sha256(repo / code_file)
|
|
335
|
+
if current_hash != expected_hash:
|
|
336
|
+
findings.append(
|
|
337
|
+
DriftIssue(
|
|
338
|
+
"code-changed",
|
|
339
|
+
behavior_id,
|
|
340
|
+
"linked code file changed since the traceability baseline",
|
|
341
|
+
code_file=code_file,
|
|
342
|
+
line=backlink.line,
|
|
343
|
+
)
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
for key, previous in sorted(baseline.items()):
|
|
347
|
+
if key in current:
|
|
348
|
+
continue
|
|
349
|
+
behavior_id, code_file = key
|
|
350
|
+
findings.append(
|
|
351
|
+
DriftIssue(
|
|
352
|
+
"stale-trace",
|
|
353
|
+
behavior_id,
|
|
354
|
+
"baseline backlink moved or disappeared",
|
|
355
|
+
code_file=code_file,
|
|
356
|
+
line=previous.get("line") if isinstance(previous.get("line"), int) else None,
|
|
357
|
+
)
|
|
358
|
+
)
|
|
359
|
+
return findings
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def _file_sha256(path: Path) -> str:
|
|
363
|
+
try:
|
|
364
|
+
return hashlib.sha256(path.read_bytes()).hexdigest()
|
|
365
|
+
except OSError:
|
|
366
|
+
return hashlib.sha256(b"").hexdigest()
|
|
367
|
+
|
|
368
|
+
|
|
251
369
|
def render_text(report: DriftReport) -> str:
|
|
252
370
|
if report.clean:
|
|
253
371
|
return "=== SPEC-AGENT DRIFT CHECK ===\n\nCLEAN\n - no structural drift detected"
|
|
@@ -377,6 +495,11 @@ def _is_git_repo(repo: Path) -> bool:
|
|
|
377
495
|
return _git(repo, ["rev-parse", "--is-inside-work-tree"]) == "true"
|
|
378
496
|
|
|
379
497
|
|
|
498
|
+
def _is_spec_agent_source(repo: Path) -> bool:
|
|
499
|
+
"""True only in this package's source tree, not an initialized consumer repo."""
|
|
500
|
+
return (repo / "src/spec_agent/installer.py").is_file()
|
|
501
|
+
|
|
502
|
+
|
|
380
503
|
def _git(repo: Path, args: list[str]) -> str:
|
|
381
504
|
try:
|
|
382
505
|
return subprocess.check_output(
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: spec-request-flow
|
|
3
|
+
description: Use when a user asks to add, change, fix, refactor, design, or implement repository behavior under spec-driven development, including vague requests and follow-ups that modify an existing feature even when specifications are not mentioned.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Spec Request Flow
|
|
7
|
+
|
|
8
|
+
Produce an approved product specification, then stop with a Code Agent handoff.
|
|
9
|
+
|
|
10
|
+
<HARD-GATE>
|
|
11
|
+
Do not scaffold or write specifications before product-decision approval.
|
|
12
|
+
Do not create an implementation plan or choose files or technologies. Do not write
|
|
13
|
+
code or tests, dispatch implementation, or perform engineering verification.
|
|
14
|
+
</HARD-GATE>
|
|
15
|
+
|
|
16
|
+
## Explore context
|
|
17
|
+
|
|
18
|
+
Read `AGENTS.md`, root `SPEC.md`, the existing packet, relevant behavior, and related
|
|
19
|
+
evolution events. Separate evidence, outcomes, conflicts, assumptions, and unknowns.
|
|
20
|
+
Decompose independent subsystems and process one valuable feature at a time.
|
|
21
|
+
|
|
22
|
+
## Clarify product behavior
|
|
23
|
+
|
|
24
|
+
Ask one blocking question at a time. Present 2-3 approaches for each material product
|
|
25
|
+
decision, with trade-offs and a recommendation about observable behavior. Cover actors,
|
|
26
|
+
rules, conceptual data, flows, failures, recovery, states, edges, permissions, privacy,
|
|
27
|
+
defaults, constraints, non-goals, and measurable outcomes. Label every unresolved item
|
|
28
|
+
`blocking` or `non-blocking`; never convert an assumption into a requirement.
|
|
29
|
+
|
|
30
|
+
## Requirements-completeness gate
|
|
31
|
+
|
|
32
|
+
Verify every category above. Continue while a blocking item remains; defer only with
|
|
33
|
+
user approval of a stated default.
|
|
34
|
+
|
|
35
|
+
## Product-decision approval gate
|
|
36
|
+
|
|
37
|
+
Present the product behavior for approval or revision. Do not write either file while
|
|
38
|
+
approval is absent or ambiguous.
|
|
39
|
+
|
|
40
|
+
## Write and self-review the specification
|
|
41
|
+
|
|
42
|
+
1. Reuse `spec/features/<feature-slug>/` when present; otherwise run
|
|
43
|
+
`scripts/create_feature.py <feature-slug> --title "Feature Title"`.
|
|
44
|
+
2. Read [references/spec-format.md](references/spec-format.md). Write both `spec.md`
|
|
45
|
+
and `acceptance.md`; add both paths to root `SPEC.md`.
|
|
46
|
+
3. Keep both files strictly product-facing. Do not include code paths, symbols,
|
|
47
|
+
frameworks, storage choices, ordered engineering steps, test files, commands,
|
|
48
|
+
deployment instructions, or code backlinks.
|
|
49
|
+
4. Run the complete self-review on both files and fix every finding.
|
|
50
|
+
|
|
51
|
+
## Written-spec approval gate
|
|
52
|
+
|
|
53
|
+
Show both paths and summarize their delta. Ask for explicit written-spec approval. If
|
|
54
|
+
revised, update both files, rerun self-review, and ask again.
|
|
55
|
+
|
|
56
|
+
## Completion and Code Agent handoff
|
|
57
|
+
|
|
58
|
+
After approval, mark both files `approved`, use `spec-evolution` to record the decision,
|
|
59
|
+
emit the contract below, and stop. `spec-drift-sync` is used after implementation.
|
|
60
|
+
|
|
61
|
+
Output exactly the following, replacing `[SPEC_PATH]` with the actual `spec.md` path:
|
|
62
|
+
|
|
63
|
+
Spec complete. To implement this, give your Code Agent the following prompt: 'Read the specification at [SPEC_PATH] and its companion acceptance.md, then generate an implementation plan. During implementation, add spec: BEHAVIOR-ID backlinks to relevant production code and tests for every implemented rule. After the approved implementation, run spec-agent traceability-sync --repo ., followed by spec-agent validate --repo . Do not claim completion unless validation reports CLEAN.'
|
|
64
|
+
|
|
65
|
+
This is a handoff contract, not implementation by the Spec Agent. Do not append other
|
|
66
|
+
planning advice, implementation steps, or an offer to write code.
|
|
67
|
+
|
|
68
|
+
## Later changes
|
|
69
|
+
|
|
70
|
+
A later prompt updates the same two-file packet and preserves stable behavior IDs.
|
|
71
|
+
Return to clarification and both approval gates for every normative product change.
|
|
@@ -16,3 +16,17 @@ never writes implementation plans, code, tests, or engineering commands.
|
|
|
16
16
|
Code backlinks use `spec: BEHAVIOR-ID`. `spec-drift-sync` observes them and may
|
|
17
17
|
regenerate only non-normative `spec/traceability.json`; it never repairs code or
|
|
18
18
|
specifications. Product decision history lives in `spec/evolution/events.jsonl`.
|
|
19
|
+
|
|
20
|
+
## Code Agent contract
|
|
21
|
+
|
|
22
|
+
When implementing an approved handoff, the Code Agent MUST:
|
|
23
|
+
|
|
24
|
+
1. Read the feature `spec.md` and `acceptance.md`.
|
|
25
|
+
2. Add `spec: BEHAVIOR-ID` backlinks to relevant production code and tests for every
|
|
26
|
+
implemented rule.
|
|
27
|
+
3. Run `spec-agent traceability-sync --repo .` after the approved implementation.
|
|
28
|
+
4. Run `spec-agent validate --repo .` and resolve or report every finding.
|
|
29
|
+
|
|
30
|
+
The Code Agent MUST NOT claim completion unless validation reports `CLEAN`. It MUST NOT
|
|
31
|
+
edit product specifications merely to match code; changed product intent returns to
|
|
32
|
+
`spec-request-flow` for clarification and approval.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: spec-agent-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: A strict Spec Agent workflow for spec-driven development
|
|
5
5
|
Author: Spec Agent contributors
|
|
6
6
|
License: MIT
|
|
@@ -79,7 +79,23 @@ Use spec-request-flow to define feature X. Do not implement it.
|
|
|
79
79
|
The Spec Agent repeatedly clarifies blocking product questions, compares meaningful
|
|
80
80
|
product approaches, writes `spec.md` and `acceptance.md`, asks for approval, records
|
|
81
81
|
the approved product evolution, and then gives you the prompt for a separate Code
|
|
82
|
-
Agent.
|
|
82
|
+
Agent. That handoff requires `spec: BEHAVIOR-ID` backlinks in relevant production code
|
|
83
|
+
and tests.
|
|
84
|
+
|
|
85
|
+
## Validate implementation traceability
|
|
86
|
+
|
|
87
|
+
After an approved implementation, the Code Agent establishes the derived baseline and
|
|
88
|
+
then validates it:
|
|
89
|
+
|
|
90
|
+
```sh
|
|
91
|
+
spec-agent traceability-sync --repo .
|
|
92
|
+
spec-agent validate --repo .
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
`validate` is read-only and returns failure for missing, unknown, unbaselined, moved,
|
|
96
|
+
or changed backlink evidence. `traceability-sync` is used only after approved
|
|
97
|
+
implementation or explicit reconciliation; it must not be used to hide unexplained
|
|
98
|
+
drift.
|
|
83
99
|
|
|
84
100
|
## License
|
|
85
101
|
|
|
@@ -56,7 +56,23 @@ Use spec-request-flow to define feature X. Do not implement it.
|
|
|
56
56
|
The Spec Agent repeatedly clarifies blocking product questions, compares meaningful
|
|
57
57
|
product approaches, writes `spec.md` and `acceptance.md`, asks for approval, records
|
|
58
58
|
the approved product evolution, and then gives you the prompt for a separate Code
|
|
59
|
-
Agent.
|
|
59
|
+
Agent. That handoff requires `spec: BEHAVIOR-ID` backlinks in relevant production code
|
|
60
|
+
and tests.
|
|
61
|
+
|
|
62
|
+
## Validate implementation traceability
|
|
63
|
+
|
|
64
|
+
After an approved implementation, the Code Agent establishes the derived baseline and
|
|
65
|
+
then validates it:
|
|
66
|
+
|
|
67
|
+
```sh
|
|
68
|
+
spec-agent traceability-sync --repo .
|
|
69
|
+
spec-agent validate --repo .
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
`validate` is read-only and returns failure for missing, unknown, unbaselined, moved,
|
|
73
|
+
or changed backlink evidence. `traceability-sync` is used only after approved
|
|
74
|
+
implementation or explicit reconciliation; it must not be used to hide unexplained
|
|
75
|
+
drift.
|
|
60
76
|
|
|
61
77
|
## License
|
|
62
78
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
id: SPEC-AGENT
|
|
3
3
|
title: Strict Spec Agent
|
|
4
4
|
status: approved
|
|
5
|
-
version: 4.
|
|
5
|
+
version: 4.1.0
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Strict Spec Agent
|
|
@@ -28,6 +28,7 @@ hands the approved specification to a separate Code Agent.
|
|
|
28
28
|
- [Evolution event log rules](spec/features/evolution-event-log/spec.md) and [acceptance](spec/features/evolution-event-log/acceptance.md)
|
|
29
29
|
- [Strict skill behavior](spec/features/production-skill-hardening/spec.md) and [acceptance](spec/features/production-skill-hardening/acceptance.md)
|
|
30
30
|
- [CLI distribution](spec/features/cli-distribution/spec.md) and [acceptance](spec/features/cli-distribution/acceptance.md)
|
|
31
|
+
- [Code/spec traceability](spec/features/code-spec-traceability/spec.md) and [acceptance](spec/features/code-spec-traceability/acceptance.md)
|
|
31
32
|
|
|
32
33
|
## Product structure
|
|
33
34
|
|
|
@@ -125,6 +126,23 @@ merge a managed rules block safely, and scaffold only missing project-owned arti
|
|
|
125
126
|
SA-019: Repeated initialization MUST be safe and idempotent, and forced updates MUST
|
|
126
127
|
remain limited to CLI-managed skill assets.
|
|
127
128
|
|
|
129
|
+
## Code Agent traceability contract
|
|
130
|
+
|
|
131
|
+
SA-020: The Code Agent handoff MUST require both feature specifications, stable
|
|
132
|
+
behavior-ID backlinks in relevant production code and tests, traceability refresh, and
|
|
133
|
+
a clean drift validation before implementation completion.
|
|
134
|
+
|
|
135
|
+
SA-021: Derived traceability MUST preserve a baseline of backlink locations and linked
|
|
136
|
+
file fingerprints so later code changes can be reported without placing code paths in
|
|
137
|
+
normative specifications.
|
|
138
|
+
|
|
139
|
+
SA-022: Drift validation MUST be read-only and MUST distinguish missing, unknown,
|
|
140
|
+
unbaselined, moved, and changed-code evidence while leaving semantic authority to a
|
|
141
|
+
human product decision.
|
|
142
|
+
|
|
143
|
+
SA-023: Traceability refresh MUST establish a new baseline only after an approved
|
|
144
|
+
implementation or reconciliation; it MUST NOT silently make changed code authoritative.
|
|
145
|
+
|
|
128
146
|
## Workflow
|
|
129
147
|
|
|
130
148
|
```text
|
|
@@ -163,3 +181,7 @@ refresh derived traceability, and route a product decision back through request
|
|
|
163
181
|
- AC-SA-006: Approved product evolution remains append-only and UI-readable.
|
|
164
182
|
- AC-SA-007: A built wheel installs in a clean environment and initializes a repository
|
|
165
183
|
that passes `spec-agent init --check`.
|
|
184
|
+
- AC-SA-008: A Code Agent handoff cannot complete without behavior backlinks, refreshed
|
|
185
|
+
traceability, and clean read-only validation.
|
|
186
|
+
- AC-SA-009: Modifying a baselined linked code file produces a drift finding until an
|
|
187
|
+
authorized reconciliation establishes a new baseline.
|
|
@@ -11,6 +11,7 @@ from pathlib import Path
|
|
|
11
11
|
import typer
|
|
12
12
|
|
|
13
13
|
from spec_agent.commands.init import cmd_init
|
|
14
|
+
from spec_agent.commands.traceability import cmd_traceability_sync, cmd_validate
|
|
14
15
|
|
|
15
16
|
|
|
16
17
|
app = typer.Typer(
|
|
@@ -58,6 +59,24 @@ def init_command(
|
|
|
58
59
|
raise typer.Exit(cmd_init(repo, check=check, force=force))
|
|
59
60
|
|
|
60
61
|
|
|
62
|
+
@app.command("validate")
|
|
63
|
+
def validate_command(
|
|
64
|
+
repo: Path = typer.Option(Path("."), "--repo", help="Repository to validate."),
|
|
65
|
+
) -> None:
|
|
66
|
+
"""Read-only validation of specifications, backlinks, and traceability."""
|
|
67
|
+
raise typer.Exit(cmd_validate(repo))
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@app.command("traceability-sync")
|
|
71
|
+
def traceability_sync_command(
|
|
72
|
+
repo: Path = typer.Option(
|
|
73
|
+
Path("."), "--repo", help="Repository whose approved baseline is refreshed."
|
|
74
|
+
),
|
|
75
|
+
) -> None:
|
|
76
|
+
"""Refresh derived traceability after approved implementation or reconciliation."""
|
|
77
|
+
raise typer.Exit(cmd_traceability_sync(repo))
|
|
78
|
+
|
|
79
|
+
|
|
61
80
|
def main(argv: list[str] | None = None) -> int:
|
|
62
81
|
try:
|
|
63
82
|
app(args=argv, prog_name="spec-agent")
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Run the installed read-only drift checker and traceability refresh."""
|
|
2
|
+
|
|
3
|
+
# spec: SA-020, SA-022, TRACE-003, TRACE-008, TRACE-009, TRACE-012
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _checker(repo_root: Path) -> Path:
|
|
13
|
+
return repo_root / ".agents/skills/spec-drift-sync/scripts/check.py"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _run(repo_root: Path, command: str) -> int:
|
|
17
|
+
root = repo_root.resolve()
|
|
18
|
+
checker = _checker(root)
|
|
19
|
+
if not checker.is_file():
|
|
20
|
+
print("Spec Agent drift checker is missing. Run 'spec-agent init' first.")
|
|
21
|
+
return 2
|
|
22
|
+
arguments = [sys.executable, str(checker), command, "--repo", str(root)]
|
|
23
|
+
if command == "sync":
|
|
24
|
+
arguments.extend(["--output", "spec/traceability.json"])
|
|
25
|
+
return subprocess.run(arguments, check=False).returncode
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def cmd_validate(repo_root: Path) -> int:
|
|
29
|
+
"""Validate current specs, backlinks, and traceability without writes."""
|
|
30
|
+
return _run(repo_root, "check")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def cmd_traceability_sync(repo_root: Path) -> int:
|
|
34
|
+
"""Accept current approved backlink evidence as the derived baseline."""
|
|
35
|
+
return _run(repo_root, "sync")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Safely install packaged Spec Agent assets into a repository."""
|
|
2
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
|
|
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, TRACE-005
|
|
4
4
|
|
|
5
5
|
from __future__ import annotations
|
|
6
6
|
|
|
@@ -28,7 +28,8 @@ timeline
|
|
|
28
28
|
EMPTY_TRACEABILITY = json.dumps(
|
|
29
29
|
{
|
|
30
30
|
"behaviors": {},
|
|
31
|
-
"
|
|
31
|
+
"baseline_commit": None,
|
|
32
|
+
"schema_version": 2,
|
|
32
33
|
"source": "derived-from-code-backlinks",
|
|
33
34
|
},
|
|
34
35
|
indent=2,
|
|
@@ -96,6 +96,18 @@ class AgentArchitectureTests(unittest.TestCase):
|
|
|
96
96
|
self.assertIn(name, text)
|
|
97
97
|
self.assertLessEqual(len(text.split()), 220)
|
|
98
98
|
|
|
99
|
+
def test_agents_file_contains_mandatory_code_agent_traceability_contract(self) -> None:
|
|
100
|
+
text = (ROOT / "AGENTS.md").read_text(encoding="utf-8")
|
|
101
|
+
for term in (
|
|
102
|
+
"Code Agent contract",
|
|
103
|
+
"spec: BEHAVIOR-ID",
|
|
104
|
+
"production code and tests",
|
|
105
|
+
"spec-agent traceability-sync",
|
|
106
|
+
"spec-agent validate",
|
|
107
|
+
"MUST NOT claim completion",
|
|
108
|
+
):
|
|
109
|
+
self.assertIn(term, text)
|
|
110
|
+
|
|
99
111
|
def test_spec_md_indexes_feature_spec_packets(self) -> None:
|
|
100
112
|
self.assertTrue((ROOT / "SPEC.md").is_file())
|
|
101
113
|
root_spec = (ROOT / "SPEC.md").read_text(encoding="utf-8")
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
# spec: CLI-001, CLI-007, TRACE-003, TRACE-008, TRACE-012
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
ROOT = Path(__file__).resolve().parents[1]
|
|
10
|
+
sys.path.insert(0, str(ROOT / "src"))
|
|
11
|
+
|
|
12
|
+
from spec_agent.cli import main
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_cli_version(capsys) -> None:
|
|
16
|
+
assert main(["--version"]) == 0
|
|
17
|
+
assert capsys.readouterr().out.startswith("spec-agent ")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_cli_init_and_check(tmp_path: Path) -> None:
|
|
21
|
+
assert main(["init", "--repo", str(tmp_path)]) == 0
|
|
22
|
+
assert main(["init", "--repo", str(tmp_path), "--check"]) == 0
|
|
23
|
+
assert (tmp_path / ".agents/skills/spec-request-flow/SKILL.md").is_file()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_cli_traceability_sync_and_validate_detect_later_code_change(tmp_path: Path) -> None:
|
|
27
|
+
assert main(["init", "--repo", str(tmp_path)]) == 0
|
|
28
|
+
(tmp_path / "SPEC.md").write_text(
|
|
29
|
+
"---\nstatus: approved\n---\n\nFEATURE-001: Feature works.\n",
|
|
30
|
+
encoding="utf-8",
|
|
31
|
+
)
|
|
32
|
+
code = tmp_path / "service.py"
|
|
33
|
+
code.write_text("# spec: FEATURE-001\nvalue = 1\n", encoding="utf-8")
|
|
34
|
+
|
|
35
|
+
assert main(["traceability-sync", "--repo", str(tmp_path)]) == 0
|
|
36
|
+
before_validation = {
|
|
37
|
+
str(path.relative_to(tmp_path)): path.read_bytes()
|
|
38
|
+
for path in tmp_path.rglob("*")
|
|
39
|
+
if path.is_file()
|
|
40
|
+
}
|
|
41
|
+
assert main(["validate", "--repo", str(tmp_path)]) == 0
|
|
42
|
+
after_validation = {
|
|
43
|
+
str(path.relative_to(tmp_path)): path.read_bytes()
|
|
44
|
+
for path in tmp_path.rglob("*")
|
|
45
|
+
if path.is_file()
|
|
46
|
+
}
|
|
47
|
+
assert after_validation == before_validation
|
|
48
|
+
|
|
49
|
+
code.write_text("# spec: FEATURE-001\nvalue = 2\n", encoding="utf-8")
|
|
50
|
+
assert main(["validate", "--repo", str(tmp_path)]) == 1
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import json
|
|
4
|
+
|
|
3
5
|
import sys
|
|
4
6
|
from pathlib import Path
|
|
5
7
|
|
|
@@ -35,6 +37,11 @@ def test_fresh_init_installs_complete_open_agent_package(tmp_path: Path) -> None
|
|
|
35
37
|
assert (tmp_path / "spec/evolution/events.jsonl").read_text() == ""
|
|
36
38
|
assert (tmp_path / "spec/evolution/timeline.md").is_file()
|
|
37
39
|
assert (tmp_path / "spec/traceability.json").is_file()
|
|
40
|
+
traceability = json.loads(
|
|
41
|
+
(tmp_path / "spec/traceability.json").read_text(encoding="utf-8")
|
|
42
|
+
)
|
|
43
|
+
assert traceability["schema_version"] == 2
|
|
44
|
+
assert traceability["baseline_commit"] is None
|
|
38
45
|
|
|
39
46
|
|
|
40
47
|
def test_init_is_idempotent_and_preserves_project_content(tmp_path: Path) -> None:
|
|
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
import unittest
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
|
|
6
|
-
# spec: SA-005, SA-006, SA-007, SA-008, SA-009, SA-010, SA-011, SA-012, SA-016, ELOG-006, PSH-002, PSH-003, PSH-004, PSH-005, PSH-006, PSH-007, PSH-008, PSH-009, PSH-010, PSH-011
|
|
6
|
+
# spec: SA-005, SA-006, SA-007, SA-008, SA-009, SA-010, SA-011, SA-012, SA-016, SA-020, ELOG-006, PSH-002, PSH-003, PSH-004, PSH-005, PSH-006, PSH-007, PSH-008, PSH-009, PSH-010, PSH-011, TRACE-002, TRACE-003, TRACE-009, TRACE-010, TRACE-011
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
ROOT = Path(__file__).resolve().parents[1]
|
|
@@ -70,10 +70,18 @@ class SkillWorkflowTests(unittest.TestCase):
|
|
|
70
70
|
self.assertNotIn("TDD", text)
|
|
71
71
|
self.assertNotIn("debugging", text)
|
|
72
72
|
self.assertIn(
|
|
73
|
-
"Spec complete. To implement this, give your Code Agent the following prompt:
|
|
74
|
-
"'Read the specification at [SPEC_PATH] and generate an implementation plan.'",
|
|
73
|
+
"Spec complete. To implement this, give your Code Agent the following prompt:",
|
|
75
74
|
text,
|
|
76
75
|
)
|
|
76
|
+
for term in (
|
|
77
|
+
"acceptance.md",
|
|
78
|
+
"spec: BEHAVIOR-ID",
|
|
79
|
+
"production code and tests",
|
|
80
|
+
"spec-agent traceability-sync",
|
|
81
|
+
"spec-agent validate",
|
|
82
|
+
"reports CLEAN",
|
|
83
|
+
):
|
|
84
|
+
self.assertIn(term, text)
|
|
77
85
|
self.assertIn("spec-drift-sync", text)
|
|
78
86
|
self.assertIn("spec-evolution", text)
|
|
79
87
|
|
|
@@ -144,6 +152,9 @@ class SkillWorkflowTests(unittest.TestCase):
|
|
|
144
152
|
self.assertIn("Do not edit specifications, code, tests", text)
|
|
145
153
|
self.assertNotIn("## Repair and validation", text)
|
|
146
154
|
self.assertNotIn("Apply only the approved delta", text)
|
|
155
|
+
self.assertIn("linked-file fingerprints", text)
|
|
156
|
+
self.assertIn("Do not refresh", text)
|
|
157
|
+
self.assertIn("spec-agent validate", text)
|
|
147
158
|
|
|
148
159
|
def test_evolution_flow_defines_product_boundaries_and_privacy(self) -> None:
|
|
149
160
|
text = skill("spec-evolution")
|
|
@@ -7,6 +7,8 @@ import tempfile
|
|
|
7
7
|
import unittest
|
|
8
8
|
from pathlib import Path
|
|
9
9
|
|
|
10
|
+
# spec: SA-021, SA-022, SA-023, TRACE-004, TRACE-005, TRACE-006, TRACE-007, TRACE-008, TRACE-009, TRACE-011
|
|
11
|
+
|
|
10
12
|
|
|
11
13
|
PACKAGE_ROOT = Path(__file__).resolve().parents[1]
|
|
12
14
|
SCRIPTS_DIR = (
|
|
@@ -37,12 +39,13 @@ class SpecDriftTests(unittest.TestCase):
|
|
|
37
39
|
spec_drift.write_traceability(spec_drift.analyze_repo(root), output)
|
|
38
40
|
|
|
39
41
|
data = json.loads(output.read_text(encoding="utf-8"))
|
|
40
|
-
self.assertEqual(data["schema_version"],
|
|
42
|
+
self.assertEqual(data["schema_version"], 2)
|
|
41
43
|
self.assertEqual(data["source"], "derived-from-code-backlinks")
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
)
|
|
44
|
+
link = data["behaviors"]["FEATURE-X-001"][0]
|
|
45
|
+
self.assertEqual(link["path"], "service.py")
|
|
46
|
+
self.assertEqual(link["line"], 1)
|
|
47
|
+
self.assertRegex(link["file_sha256"], r"^[0-9a-f]{64}$")
|
|
48
|
+
self.assertIn("baseline_commit", data)
|
|
46
49
|
self.assertEqual(
|
|
47
50
|
spec_file.read_text(encoding="utf-8"),
|
|
48
51
|
"FEATURE-X-001: Users can complete Feature X.\n",
|
|
@@ -227,6 +230,101 @@ PROCESS-001: Specs describe current intended behavior.
|
|
|
227
230
|
f"Expected no unlinked drift for not_applicable spec, got {report.issues!r}",
|
|
228
231
|
)
|
|
229
232
|
|
|
233
|
+
def test_draft_spec_does_not_require_backlinks(self) -> None:
|
|
234
|
+
import check as spec_drift
|
|
235
|
+
|
|
236
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
237
|
+
root = Path(tmp)
|
|
238
|
+
spec = root / "SPEC.md"
|
|
239
|
+
spec.write_text(
|
|
240
|
+
"""---
|
|
241
|
+
status: draft
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
DRAFT-001: Product behavior is still being decided.
|
|
245
|
+
""",
|
|
246
|
+
encoding="utf-8",
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
report = spec_drift.analyze_repo(root)
|
|
250
|
+
|
|
251
|
+
self.assertFalse(
|
|
252
|
+
any(issue.kind == "unlinked" for issue in report.issues),
|
|
253
|
+
report.issues,
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
def test_reports_unbaselined_backlink_until_traceability_is_synced(self) -> None:
|
|
257
|
+
import check as spec_drift
|
|
258
|
+
|
|
259
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
260
|
+
root = Path(tmp)
|
|
261
|
+
(root / "spec").mkdir()
|
|
262
|
+
(root / "SPEC.md").write_text(
|
|
263
|
+
"---\nstatus: approved\n---\n\nFEATURE-001: Feature works.\n",
|
|
264
|
+
encoding="utf-8",
|
|
265
|
+
)
|
|
266
|
+
(root / "service.py").write_text(
|
|
267
|
+
"# spec: FEATURE-001\nvalue = 1\n", encoding="utf-8"
|
|
268
|
+
)
|
|
269
|
+
traceability = root / "spec/traceability.json"
|
|
270
|
+
traceability.write_text(
|
|
271
|
+
'{"schema_version": 2, "source": "derived-from-code-backlinks", "baseline_commit": null, "behaviors": {}}\n',
|
|
272
|
+
encoding="utf-8",
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
report = spec_drift.analyze_repo(root)
|
|
276
|
+
self.assertIssue(report, "unbaselined", "FEATURE-001")
|
|
277
|
+
|
|
278
|
+
spec_drift.write_traceability(report, traceability)
|
|
279
|
+
clean = spec_drift.analyze_repo(root)
|
|
280
|
+
|
|
281
|
+
self.assertTrue(clean.clean, clean.issues)
|
|
282
|
+
|
|
283
|
+
def test_reports_linked_code_changed_since_traceability_baseline(self) -> None:
|
|
284
|
+
import check as spec_drift
|
|
285
|
+
|
|
286
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
287
|
+
root = Path(tmp)
|
|
288
|
+
(root / "spec").mkdir()
|
|
289
|
+
(root / "SPEC.md").write_text(
|
|
290
|
+
"---\nstatus: approved\n---\n\nFEATURE-001: Feature works.\n",
|
|
291
|
+
encoding="utf-8",
|
|
292
|
+
)
|
|
293
|
+
code = root / "service.py"
|
|
294
|
+
code.write_text("# spec: FEATURE-001\nvalue = 1\n", encoding="utf-8")
|
|
295
|
+
traceability = root / "spec/traceability.json"
|
|
296
|
+
initial = spec_drift.analyze_repo(root)
|
|
297
|
+
spec_drift.write_traceability(initial, traceability)
|
|
298
|
+
|
|
299
|
+
code.write_text("# spec: FEATURE-001\nvalue = 2\n", encoding="utf-8")
|
|
300
|
+
changed = spec_drift.analyze_repo(root)
|
|
301
|
+
|
|
302
|
+
self.assertIssue(changed, "code-changed", "FEATURE-001")
|
|
303
|
+
|
|
304
|
+
def test_reports_baseline_link_that_moved_or_disappeared(self) -> None:
|
|
305
|
+
import check as spec_drift
|
|
306
|
+
|
|
307
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
308
|
+
root = Path(tmp)
|
|
309
|
+
(root / "spec").mkdir()
|
|
310
|
+
(root / "SPEC.md").write_text(
|
|
311
|
+
"---\nstatus: approved\n---\n\nFEATURE-001: Feature works.\n",
|
|
312
|
+
encoding="utf-8",
|
|
313
|
+
)
|
|
314
|
+
old = root / "old_service.py"
|
|
315
|
+
old.write_text("# spec: FEATURE-001\nvalue = 1\n", encoding="utf-8")
|
|
316
|
+
traceability = root / "spec/traceability.json"
|
|
317
|
+
spec_drift.write_traceability(spec_drift.analyze_repo(root), traceability)
|
|
318
|
+
|
|
319
|
+
old.unlink()
|
|
320
|
+
(root / "new_service.py").write_text(
|
|
321
|
+
"# spec: FEATURE-001\nvalue = 1\n", encoding="utf-8"
|
|
322
|
+
)
|
|
323
|
+
moved = spec_drift.analyze_repo(root)
|
|
324
|
+
|
|
325
|
+
self.assertIssue(moved, "stale-trace", "FEATURE-001")
|
|
326
|
+
self.assertIssue(moved, "unbaselined", "FEATURE-001")
|
|
327
|
+
|
|
230
328
|
def test_default_spec_root_is_standalone_spec_directory(self) -> None:
|
|
231
329
|
import check as spec_drift
|
|
232
330
|
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: spec-request-flow
|
|
3
|
-
description: Use when a user asks to add, change, fix, refactor, design, or implement repository behavior under spec-driven development, including vague requests and follow-ups that modify an existing feature even when specifications are not mentioned.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Spec Request Flow
|
|
7
|
-
|
|
8
|
-
Produce an approved, implementation-independent product specification, then stop with
|
|
9
|
-
a discoverable Code Agent handoff.
|
|
10
|
-
|
|
11
|
-
<HARD-GATE>
|
|
12
|
-
Do not scaffold or write specification files before the user approves the presented
|
|
13
|
-
product decisions. Do not create an implementation plan, choose repository files,
|
|
14
|
-
write code or tests, dispatch implementation, or perform engineering verification at
|
|
15
|
-
any point in this skill.
|
|
16
|
-
</HARD-GATE>
|
|
17
|
-
|
|
18
|
-
## Explore context
|
|
19
|
-
|
|
20
|
-
1. Read applicable `AGENTS.md`, root `SPEC.md`, the existing feature packet, relevant
|
|
21
|
-
product behavior, and related `spec/evolution/events.jsonl` entries.
|
|
22
|
-
2. Separate evidence, outcomes, conflicts, assumptions, and unknowns.
|
|
23
|
-
3. If the request contains independent subsystems, propose decomposition and process
|
|
24
|
-
one independently valuable feature at a time.
|
|
25
|
-
|
|
26
|
-
## Clarify product behavior
|
|
27
|
-
|
|
28
|
-
1. Ask one blocking question at a time. Do not ask what repository evidence answers.
|
|
29
|
-
2. For each material product decision, present 2-3 approaches, trade-offs, and a
|
|
30
|
-
recommendation. Keep choices about observable behavior, not technical solutions.
|
|
31
|
-
3. Cover actors, rules, conceptual data, primary and alternate flows, failures,
|
|
32
|
-
recovery, states, edges, permissions, privacy, defaults, constraints, non-goals,
|
|
33
|
-
and measurable outcomes.
|
|
34
|
-
4. Label every unresolved item `blocking` or `non-blocking`. Never turn an assumption
|
|
35
|
-
into an accepted requirement.
|
|
36
|
-
|
|
37
|
-
## Requirements-completeness gate
|
|
38
|
-
|
|
39
|
-
Verify every category above before presenting the product decision. Continue while any
|
|
40
|
-
blocking item remains. Defer one only when the user approves a stated default.
|
|
41
|
-
|
|
42
|
-
## Product-decision approval gate
|
|
43
|
-
|
|
44
|
-
Present the proposed product behavior and ask the user to approve or revise it. Do not
|
|
45
|
-
scaffold or write either specification file while approval is absent or ambiguous.
|
|
46
|
-
|
|
47
|
-
## Write and self-review the specification
|
|
48
|
-
|
|
49
|
-
1. Reuse `spec/features/<feature-slug>/` when present; otherwise run
|
|
50
|
-
`scripts/create_feature.py <feature-slug> --title "Feature Title"`.
|
|
51
|
-
2. Read [references/spec-format.md](references/spec-format.md). Write both `spec.md`
|
|
52
|
-
and `acceptance.md`; add both paths to root `SPEC.md`.
|
|
53
|
-
3. Keep both files strictly product-facing. Do not include code paths, symbols,
|
|
54
|
-
frameworks, storage choices, ordered engineering steps, test files, commands,
|
|
55
|
-
deployment instructions, or code backlinks.
|
|
56
|
-
4. Run the complete self-review on both files and fix every finding.
|
|
57
|
-
|
|
58
|
-
## Written-spec approval gate
|
|
59
|
-
|
|
60
|
-
Show both paths and summarize their delta. Ask for explicit written-spec approval. If
|
|
61
|
-
revised, update both files, rerun self-review, and ask again.
|
|
62
|
-
|
|
63
|
-
## Completion and Code Agent handoff
|
|
64
|
-
|
|
65
|
-
After approval, mark both files `approved`, use `spec-evolution` to record the approved
|
|
66
|
-
product decision, and stop. `spec-drift-sync` is used later as a read-only observer;
|
|
67
|
-
it is not implementation work for this skill.
|
|
68
|
-
|
|
69
|
-
Output exactly the following, replacing `[SPEC_PATH]` with the actual `spec.md` path:
|
|
70
|
-
|
|
71
|
-
Spec complete. To implement this, give your Code Agent the following prompt: 'Read the specification at [SPEC_PATH] and generate an implementation plan.'
|
|
72
|
-
|
|
73
|
-
Do not append planning advice, implementation steps, or an offer to write code.
|
|
74
|
-
|
|
75
|
-
## Later changes
|
|
76
|
-
|
|
77
|
-
A later prompt updates the same two-file packet and preserves stable behavior IDs.
|
|
78
|
-
Return to clarification and both approval gates for every normative product change.
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import sys
|
|
4
|
-
from pathlib import Path
|
|
5
|
-
|
|
6
|
-
# spec: CLI-001, CLI-007
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
ROOT = Path(__file__).resolve().parents[1]
|
|
10
|
-
sys.path.insert(0, str(ROOT / "src"))
|
|
11
|
-
|
|
12
|
-
from spec_agent.cli import main
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def test_cli_version(capsys) -> None:
|
|
16
|
-
assert main(["--version"]) == 0
|
|
17
|
-
assert capsys.readouterr().out.startswith("spec-agent ")
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
def test_cli_init_and_check(tmp_path: Path) -> None:
|
|
21
|
-
assert main(["init", "--repo", str(tmp_path)]) == 0
|
|
22
|
-
assert main(["init", "--repo", str(tmp_path), "--check"]) == 0
|
|
23
|
-
assert (tmp_path / ".agents/skills/spec-request-flow/SKILL.md").is_file()
|
|
File without changes
|
{spec_agent_cli-0.1.0 → spec_agent_cli-0.2.0}/.agents/skills/spec-evolution/scripts/record.py
RENAMED
|
File without changes
|
{spec_agent_cli-0.1.0 → spec_agent_cli-0.2.0}/.agents/skills/spec-evolution/scripts/timeline.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|