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.
Files changed (37) hide show
  1. {spec_agent_cli-0.1.0 → spec_agent_cli-0.2.0}/.agents/skills/spec-drift-sync/SKILL.md +16 -9
  2. {spec_agent_cli-0.1.0 → spec_agent_cli-0.2.0}/.agents/skills/spec-drift-sync/scripts/check.py +129 -6
  3. spec_agent_cli-0.2.0/.agents/skills/spec-request-flow/SKILL.md +71 -0
  4. {spec_agent_cli-0.1.0 → spec_agent_cli-0.2.0}/.agents/skills/spec-request-flow/scripts/create_feature.py +1 -1
  5. {spec_agent_cli-0.1.0 → spec_agent_cli-0.2.0}/AGENTS.md +14 -0
  6. {spec_agent_cli-0.1.0 → spec_agent_cli-0.2.0}/PKG-INFO +18 -2
  7. {spec_agent_cli-0.1.0 → spec_agent_cli-0.2.0}/README.md +17 -1
  8. {spec_agent_cli-0.1.0 → spec_agent_cli-0.2.0}/SPEC.md +23 -1
  9. {spec_agent_cli-0.1.0 → spec_agent_cli-0.2.0}/pyproject.toml +1 -1
  10. {spec_agent_cli-0.1.0 → spec_agent_cli-0.2.0}/src/spec_agent/cli.py +19 -0
  11. spec_agent_cli-0.2.0/src/spec_agent/commands/traceability.py +35 -0
  12. {spec_agent_cli-0.1.0 → spec_agent_cli-0.2.0}/src/spec_agent/installer.py +3 -2
  13. {spec_agent_cli-0.1.0 → spec_agent_cli-0.2.0}/tests/test_agent_assets.py +12 -0
  14. spec_agent_cli-0.2.0/tests/test_cli.py +50 -0
  15. {spec_agent_cli-0.1.0 → spec_agent_cli-0.2.0}/tests/test_cli_installer.py +7 -0
  16. {spec_agent_cli-0.1.0 → spec_agent_cli-0.2.0}/tests/test_skill_workflows.py +14 -3
  17. {spec_agent_cli-0.1.0 → spec_agent_cli-0.2.0}/tests/test_spec_drift.py +103 -5
  18. spec_agent_cli-0.1.0/.agents/skills/spec-request-flow/SKILL.md +0 -78
  19. spec_agent_cli-0.1.0/tests/test_cli.py +0 -23
  20. {spec_agent_cli-0.1.0 → spec_agent_cli-0.2.0}/.agents/skills/spec-evolution/SKILL.md +0 -0
  21. {spec_agent_cli-0.1.0 → spec_agent_cli-0.2.0}/.agents/skills/spec-evolution/scripts/record.py +0 -0
  22. {spec_agent_cli-0.1.0 → spec_agent_cli-0.2.0}/.agents/skills/spec-evolution/scripts/timeline.py +0 -0
  23. {spec_agent_cli-0.1.0 → spec_agent_cli-0.2.0}/.agents/skills/spec-request-flow/assets/SPEC.template.md +0 -0
  24. {spec_agent_cli-0.1.0 → spec_agent_cli-0.2.0}/.agents/skills/spec-request-flow/assets/feature-acceptance.template.md +0 -0
  25. {spec_agent_cli-0.1.0 → spec_agent_cli-0.2.0}/.agents/skills/spec-request-flow/assets/feature-spec.template.md +0 -0
  26. {spec_agent_cli-0.1.0 → spec_agent_cli-0.2.0}/.agents/skills/spec-request-flow/references/spec-format.md +0 -0
  27. {spec_agent_cli-0.1.0 → spec_agent_cli-0.2.0}/.gitignore +0 -0
  28. {spec_agent_cli-0.1.0 → spec_agent_cli-0.2.0}/LICENSE +0 -0
  29. {spec_agent_cli-0.1.0 → spec_agent_cli-0.2.0}/src/spec_agent/__init__.py +0 -0
  30. {spec_agent_cli-0.1.0 → spec_agent_cli-0.2.0}/src/spec_agent/__main__.py +0 -0
  31. {spec_agent_cli-0.1.0 → spec_agent_cli-0.2.0}/src/spec_agent/commands/__init__.py +0 -0
  32. {spec_agent_cli-0.1.0 → spec_agent_cli-0.2.0}/src/spec_agent/commands/init.py +0 -0
  33. {spec_agent_cli-0.1.0 → spec_agent_cli-0.2.0}/src/spec_agent/package_assets.py +0 -0
  34. {spec_agent_cli-0.1.0 → spec_agent_cli-0.2.0}/tests/test_end_to_end.py +0 -0
  35. {spec_agent_cli-0.1.0 → spec_agent_cli-0.2.0}/tests/test_event_log.py +0 -0
  36. {spec_agent_cli-0.1.0 → spec_agent_cli-0.2.0}/tests/test_feature_scaffold.py +0 -0
  37. {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 `scripts/check.py check --repo REPOSITORY`. Use `--spec-root SPEC_SOURCE` only
23
- for an intentionally scoped observation.
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
- 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.
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`, `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.
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
 
@@ -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(scan_code_backlinks(repo))
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
- if entry.planned and entry.behavior_id in by_id:
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
- not entry.planned
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
- {"path": backlink.code_file, "line": backlink.line}
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": 1,
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.
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env python3
2
2
  """Create a non-overwriting two-file feature specification packet."""
3
3
 
4
- # spec: FSP-001, FSP-002, FSP-003
4
+ # spec: FSP-001, FSP-002, FSP-003, TRACE-001
5
5
 
6
6
  from __future__ import annotations
7
7
 
@@ -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.1.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.0.0
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.
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "spec-agent-cli"
7
- version = "0.1.0"
7
+ version = "0.2.0"
8
8
  description = "A strict Spec Agent workflow for spec-driven development"
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -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
- "schema_version": 1,
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"], 1)
42
+ self.assertEqual(data["schema_version"], 2)
41
43
  self.assertEqual(data["source"], "derived-from-code-backlinks")
42
- self.assertEqual(
43
- data["behaviors"]["FEATURE-X-001"],
44
- [{"path": "service.py", "line": 1}],
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