repopact 2.0.2__tar.gz → 2.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 (45) hide show
  1. {repopact-2.0.2 → repopact-2.2.0}/PKG-INFO +17 -8
  2. {repopact-2.0.2 → repopact-2.2.0}/README.md +16 -7
  3. repopact-2.2.0/VERSION +1 -0
  4. {repopact-2.0.2 → repopact-2.2.0}/schemas/work-item.schema.json +1 -2
  5. {repopact-2.0.2 → repopact-2.2.0}/scripts/adopt_repo.py +17 -10
  6. {repopact-2.0.2 → repopact-2.2.0}/scripts/doctor.py +17 -8
  7. {repopact-2.0.2 → repopact-2.2.0}/scripts/generate_dashboard.py +30 -16
  8. {repopact-2.0.2 → repopact-2.2.0}/scripts/init_repo.py +4 -1
  9. {repopact-2.0.2 → repopact-2.2.0}/scripts/new.py +13 -4
  10. {repopact-2.0.2 → repopact-2.2.0}/scripts/plan_import.py +14 -4
  11. {repopact-2.0.2 → repopact-2.2.0}/scripts/repo_model.py +1 -2
  12. {repopact-2.0.2 → repopact-2.2.0}/scripts/repopact.egg-info/PKG-INFO +17 -8
  13. {repopact-2.0.2 → repopact-2.2.0}/scripts/repopact_cli.py +19 -16
  14. {repopact-2.0.2 → repopact-2.2.0}/scripts/run_conformance.py +6 -0
  15. {repopact-2.0.2 → repopact-2.2.0}/scripts/takeover.py +4 -1
  16. {repopact-2.0.2 → repopact-2.2.0}/scripts/validate_repo.py +50 -15
  17. {repopact-2.0.2 → repopact-2.2.0}/tests/test_conformance.py +2 -0
  18. {repopact-2.0.2 → repopact-2.2.0}/tests/test_validate_repo.py +97 -38
  19. repopact-2.0.2/VERSION +0 -1
  20. {repopact-2.0.2 → repopact-2.2.0}/LICENSE +0 -0
  21. {repopact-2.0.2 → repopact-2.2.0}/pyproject.toml +0 -0
  22. {repopact-2.0.2 → repopact-2.2.0}/schemas/audit-finding.schema.json +0 -0
  23. {repopact-2.0.2 → repopact-2.2.0}/schemas/conformance-manifest.schema.json +0 -0
  24. {repopact-2.0.2 → repopact-2.2.0}/schemas/evidence-run.schema.json +0 -0
  25. {repopact-2.0.2 → repopact-2.2.0}/schemas/frozen-surface.schema.json +0 -0
  26. {repopact-2.0.2 → repopact-2.2.0}/schemas/invariants.schema.json +0 -0
  27. {repopact-2.0.2 → repopact-2.2.0}/schemas/record-frontmatter.schema.json +0 -0
  28. {repopact-2.0.2 → repopact-2.2.0}/scripts/check_frozen_surface.py +0 -0
  29. {repopact-2.0.2 → repopact-2.2.0}/scripts/frontmatter.py +0 -0
  30. {repopact-2.0.2 → repopact-2.2.0}/scripts/generate_spec.py +0 -0
  31. {repopact-2.0.2 → repopact-2.2.0}/scripts/repopact.egg-info/SOURCES.txt +0 -0
  32. {repopact-2.0.2 → repopact-2.2.0}/scripts/repopact.egg-info/dependency_links.txt +0 -0
  33. {repopact-2.0.2 → repopact-2.2.0}/scripts/repopact.egg-info/entry_points.txt +0 -0
  34. {repopact-2.0.2 → repopact-2.2.0}/scripts/repopact.egg-info/requires.txt +0 -0
  35. {repopact-2.0.2 → repopact-2.2.0}/scripts/repopact.egg-info/top_level.txt +0 -0
  36. {repopact-2.0.2 → repopact-2.2.0}/scripts/track_import.py +0 -0
  37. {repopact-2.0.2 → repopact-2.2.0}/setup.cfg +0 -0
  38. {repopact-2.0.2 → repopact-2.2.0}/templates/README.md +0 -0
  39. {repopact-2.0.2 → repopact-2.2.0}/templates/decision.md +0 -0
  40. {repopact-2.0.2 → repopact-2.2.0}/templates/evidence-run.json +0 -0
  41. {repopact-2.0.2 → repopact-2.2.0}/templates/policy.md +0 -0
  42. {repopact-2.0.2 → repopact-2.2.0}/templates/work-item.README.md +0 -0
  43. {repopact-2.0.2 → repopact-2.2.0}/templates/work-item.json +0 -0
  44. {repopact-2.0.2 → repopact-2.2.0}/tests/test_plan_import_links.py +0 -0
  45. {repopact-2.0.2 → repopact-2.2.0}/tests/test_takeover_refs.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: repopact
3
- Version: 2.0.2
3
+ Version: 2.2.0
4
4
  Summary: A repository-native standard for durable agent work: binding invariants, evidence-gated work items, and drift audits.
5
5
  License: Apache-2.0
6
6
  Project-URL: Homepage, https://github.com/ForgeWireLabs/repopact
@@ -22,8 +22,8 @@ cannot be silently weakened.
22
22
  > The repository is the pact: authority, intent, evidence, and history that survive every
23
23
  > session.
24
24
 
25
- `pip install repopact` · Apache-2.0 · current release **2.0.2**
26
- ([changelog](decisions/0022-release-2.0.2-installed-seed-lookup.md)).
25
+ `pip install repopact` · Apache-2.0 · current release **2.2.0**
26
+ ([changelog](decisions/0025-release-2.2.0-dashboard-integrity.md)).
27
27
 
28
28
  ## How it relates to `AGENTS.md`
29
29
 
@@ -71,7 +71,8 @@ intent -> scoped authority -> work item -> implementation -> evidence -> audit -
71
71
  pip install repopact # the CLI + reference validator, from PyPI
72
72
  repopact init --target ../your-repo # seed a valid RepoPact in a new repo
73
73
  cd ../your-repo
74
- repopact new work-item "Title of the work" # stamps records (incl. the preflight marker)
74
+ repopact new work-item "Title of the work" # stamps active work (incl. the preflight marker)
75
+ repopact new work-item "Candidate idea" --status proposed
75
76
  repopact validate
76
77
  repopact dashboard
77
78
  ```
@@ -110,7 +111,7 @@ idempotent.
110
111
 
111
112
  ## 2.0: mandatory preflight + provenance-typed records
112
113
 
113
- Decision [`0021`](decisions/0022-release-2.0.2-installed-seed-lookup.md) (supersedes 0018):
114
+ Decision [`0021`](decisions/0021-preflight-mandatory-and-provenance.md) (supersedes 0018):
114
115
 
115
116
  - **Mandatory preflight (default on).** No work begins until a work item exists and
116
117
  propagates through the pact; `repopact new` stamps the marker. Existing repos grandfather
@@ -123,9 +124,17 @@ Decision [`0021`](decisions/0022-release-2.0.2-installed-seed-lookup.md) (supers
123
124
 
124
125
  ## Status is a filesystem transition
125
126
 
126
- Work moves between `work/active`, `work/blocked`, `work/deferred`, and `work/completed`; its
127
- `work-item.json` status must match its directory. Moving a work item never deletes its
128
- reasoning, decisions, or evidence links.
127
+ Work moves between lifecycle directories; its `work-item.json` status must match its
128
+ directory. Moving a work item never deletes its reasoning, decisions, or evidence links.
129
+
130
+ - `proposed`: captured candidate work that is not yet accepted or authorized for
131
+ implementation.
132
+ - `active`: accepted work authorized for design or implementation.
133
+ - `blocked`: accepted/current work that cannot proceed until a named condition changes.
134
+ - `deferred`: accepted work intentionally postponed with rationale.
135
+ - `completed`: delivered work whose acceptance criteria are evidence-closed.
136
+
137
+ Active and completed work cannot depend on proposed work as if it were accepted.
129
138
 
130
139
  ## Derive over declare
131
140
 
@@ -9,8 +9,8 @@ cannot be silently weakened.
9
9
  > The repository is the pact: authority, intent, evidence, and history that survive every
10
10
  > session.
11
11
 
12
- `pip install repopact` · Apache-2.0 · current release **2.0.2**
13
- ([changelog](decisions/0022-release-2.0.2-installed-seed-lookup.md)).
12
+ `pip install repopact` · Apache-2.0 · current release **2.2.0**
13
+ ([changelog](decisions/0025-release-2.2.0-dashboard-integrity.md)).
14
14
 
15
15
  ## How it relates to `AGENTS.md`
16
16
 
@@ -58,7 +58,8 @@ intent -> scoped authority -> work item -> implementation -> evidence -> audit -
58
58
  pip install repopact # the CLI + reference validator, from PyPI
59
59
  repopact init --target ../your-repo # seed a valid RepoPact in a new repo
60
60
  cd ../your-repo
61
- repopact new work-item "Title of the work" # stamps records (incl. the preflight marker)
61
+ repopact new work-item "Title of the work" # stamps active work (incl. the preflight marker)
62
+ repopact new work-item "Candidate idea" --status proposed
62
63
  repopact validate
63
64
  repopact dashboard
64
65
  ```
@@ -97,7 +98,7 @@ idempotent.
97
98
 
98
99
  ## 2.0: mandatory preflight + provenance-typed records
99
100
 
100
- Decision [`0021`](decisions/0022-release-2.0.2-installed-seed-lookup.md) (supersedes 0018):
101
+ Decision [`0021`](decisions/0021-preflight-mandatory-and-provenance.md) (supersedes 0018):
101
102
 
102
103
  - **Mandatory preflight (default on).** No work begins until a work item exists and
103
104
  propagates through the pact; `repopact new` stamps the marker. Existing repos grandfather
@@ -110,9 +111,17 @@ Decision [`0021`](decisions/0022-release-2.0.2-installed-seed-lookup.md) (supers
110
111
 
111
112
  ## Status is a filesystem transition
112
113
 
113
- Work moves between `work/active`, `work/blocked`, `work/deferred`, and `work/completed`; its
114
- `work-item.json` status must match its directory. Moving a work item never deletes its
115
- reasoning, decisions, or evidence links.
114
+ Work moves between lifecycle directories; its `work-item.json` status must match its
115
+ directory. Moving a work item never deletes its reasoning, decisions, or evidence links.
116
+
117
+ - `proposed`: captured candidate work that is not yet accepted or authorized for
118
+ implementation.
119
+ - `active`: accepted work authorized for design or implementation.
120
+ - `blocked`: accepted/current work that cannot proceed until a named condition changes.
121
+ - `deferred`: accepted work intentionally postponed with rationale.
122
+ - `completed`: delivered work whose acceptance criteria are evidence-closed.
123
+
124
+ Active and completed work cannot depend on proposed work as if it were accepted.
116
125
 
117
126
  ## Derive over declare
118
127
 
repopact-2.2.0/VERSION ADDED
@@ -0,0 +1 @@
1
+ 2.2.0
@@ -6,7 +6,7 @@
6
6
  "properties": {
7
7
  "id": {"type": "string", "pattern": "^[0-9]{3,}$"},
8
8
  "title": {"type": "string", "minLength": 1},
9
- "status": {"enum": ["active", "blocked", "deferred", "completed"]},
9
+ "status": {"enum": ["proposed", "active", "blocked", "deferred", "completed"]},
10
10
  "owner_scope": {"type": "string", "minLength": 1},
11
11
  "affected_scopes": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
12
12
  "depends_on": {"type": "array", "items": {"type": "string", "pattern": "^[0-9]{3,}$"}, "uniqueItems": true},
@@ -39,4 +39,3 @@
39
39
  "updated": {"type": "string", "format": "date"}
40
40
  }
41
41
  }
42
-
@@ -21,9 +21,10 @@ import re
21
21
  import subprocess
22
22
  import sys
23
23
  from datetime import date, datetime, timedelta
24
- from pathlib import Path
25
-
26
- import init_repo # reuse _seed_dir, _json/_write semantics, LIFECYCLE
24
+ from pathlib import Path
25
+
26
+ import generate_dashboard
27
+ import init_repo # reuse _seed_dir, _json/_write semantics, LIFECYCLE
27
28
 
28
29
 
29
30
  # --- non-destructive primitives --------------------------------------------
@@ -330,16 +331,22 @@ def adopt(target: Path, today: date | None = None, dry_run: bool = False) -> Rep
330
331
  "from the repository scan rather than proven by a run. Attach real verification evidence and\n"
331
332
  "run `repopact doctor` to ratchet it to concrete, then complete it. " + summary + "\n", target)
332
333
 
333
- rep.write(target / "decisions" / "0001-adopt-repopact.md",
334
+ rep.write(target / "decisions" / "0001-adopt-repopact.md",
334
335
  "---\nid: 0001\ntitle: Adopt RepoPact\nstatus: accepted\n"
335
336
  f"date: {today.isoformat()}\nsupersedes: []\n---\n\n# 0001: Adopt RepoPact\n\n## Context\n\n"
336
337
  "The project already had ad-hoc governance (CODEOWNERS, CI gates, AGENTS.md). RepoPact\n"
337
- "makes those bindings explicit and machine-checkable.\n\n## Decision\n\n"
338
- "Adopt RepoPact; existing workflows become binding gates (INV-2) and ownership becomes\n"
339
- "scopes/roles. Existing files were preserved; RepoPact records were added around them.\n", target)
340
-
341
- rep.gitignored = gitignored_records(target, rep.created)
342
- return rep
338
+ "makes those bindings explicit and machine-checkable.\n\n## Decision\n\n"
339
+ "Adopt RepoPact; existing workflows become binding gates (INV-2) and ownership becomes\n"
340
+ "scopes/roles. Existing files were preserved; RepoPact records were added around them.\n", target)
341
+
342
+ # The dashboard is derived, but adoption remains non-destructive: create the
343
+ # canonical projection only when the reserved path is absent. An existing file
344
+ # is preserved and validation will identify it if it is not canonical.
345
+ dashboard_text = "" if dry_run else generate_dashboard.generate(target, today=today)
346
+ rep.write(target / "audits" / "reports" / "dashboard.md", dashboard_text, target)
347
+
348
+ rep.gitignored = gitignored_records(target, rep.created)
349
+ return rep
343
350
 
344
351
 
345
352
  def _print_report(rep: Report) -> None:
@@ -20,10 +20,11 @@ import shutil
20
20
  import sys
21
21
  from dataclasses import dataclass
22
22
  from datetime import date, timedelta
23
- from pathlib import Path
24
-
25
- import adopt_repo
26
- import validate_repo
23
+ from pathlib import Path
24
+
25
+ import adopt_repo
26
+ import generate_dashboard
27
+ import validate_repo
27
28
 
28
29
 
29
30
  @dataclass
@@ -296,10 +297,18 @@ def fix(root: Path, today: date | None = None) -> list[str]:
296
297
  # 6. ratchet provisional records whose evidence is now concrete (decision 0021)
297
298
  actions.extend(_ratchet_provisional(root))
298
299
 
299
- # 7. migrate preflight on upgrade: grandfather pre-2.0 work items (decision 0021)
300
- actions.extend(_migrate_preflight(root, today))
301
-
302
- return actions
300
+ # 7. migrate preflight on upgrade: grandfather pre-2.0 work items (decision 0021)
301
+ actions.extend(_migrate_preflight(root, today))
302
+
303
+ # 8. derived output is safe to repair because it is fully reproducible.
304
+ dashboard = root / "audits" / "reports" / "dashboard.md"
305
+ expected = generate_dashboard.generate(root, today=today)
306
+ actual = dashboard.read_text(encoding="utf-8") if dashboard.is_file() else None
307
+ if actual != expected:
308
+ generate_dashboard.write_dashboard(root, today=today)
309
+ actions.append("regenerated audits/reports/dashboard.md")
310
+
311
+ return actions
303
312
 
304
313
 
305
314
  def main() -> int:
@@ -46,8 +46,15 @@ def _overdue_scopes(root: Path, today: date) -> list[tuple[str, str]]:
46
46
  return overdue
47
47
 
48
48
 
49
- def generate(root: Path) -> str:
50
- today = date.today()
49
+ def generate(root: Path, today: date | None = None) -> str:
50
+ """Render the canonical dashboard projection for ``root``.
51
+
52
+ ``today`` is injectable so audit-cadence transitions can be tested. The
53
+ rendering deliberately does not include the wall-clock date: a generated
54
+ artifact should change only when a displayed source-derived value changes,
55
+ not merely because the command ran on another day.
56
+ """
57
+ today = today or date.today()
51
58
  items = discover_work_items(root)
52
59
  counts = Counter(item.status for item in items)
53
60
  contracts = iter_contracts(root)
@@ -60,12 +67,12 @@ def generate(root: Path) -> str:
60
67
  finding_count = _count_json_records(root / "audits" / "findings")
61
68
  overdue = _overdue_scopes(root, today)
62
69
 
63
- lines = [
64
- "# Repository Dashboard",
65
- "",
66
- "> Generated from source records. Do not edit manually.",
67
- f"> Generated: {today.isoformat()}",
68
- f"> RepoPact spec version: {_spec_version(root)}",
70
+ lines = [
71
+ "# Repository Dashboard",
72
+ "",
73
+ "> Canonically generated from source records. Do not edit manually.",
74
+ "> Validation fails when this file differs from `repopact dashboard` output.",
75
+ f"> RepoPact spec version: {_spec_version(root)}",
69
76
  "",
70
77
  "## Health",
71
78
  "",
@@ -101,14 +108,21 @@ def generate(root: Path) -> str:
101
108
  lines.append("No active or blocked work.")
102
109
  else:
103
110
  lines.extend(f"- {item.item_id}: {item.data['title']} ({item.status})" for item in active)
104
- return "\n".join(lines) + "\n"
105
-
106
-
107
- def main() -> None:
108
- root = Path(__file__).resolve().parents[1]
109
- output = root / "audits" / "reports" / "dashboard.md"
110
- output.write_text(generate(root), encoding="utf-8")
111
- print(f"Generated {output.relative_to(root)}")
111
+ return "\n".join(lines) + "\n"
112
+
113
+
114
+ def write_dashboard(root: Path, today: date | None = None) -> Path:
115
+ """Write the one dashboard derived artifact and return its path."""
116
+ output = root / "audits" / "reports" / "dashboard.md"
117
+ output.parent.mkdir(parents=True, exist_ok=True)
118
+ output.write_text(generate(root, today=today), encoding="utf-8")
119
+ return output
120
+
121
+
122
+ def main() -> None:
123
+ root = Path(__file__).resolve().parents[1]
124
+ output = write_dashboard(root)
125
+ print(f"Generated {output.relative_to(root)}")
112
126
 
113
127
 
114
128
  if __name__ == "__main__":
@@ -16,9 +16,11 @@ import sys
16
16
  from datetime import date, timedelta
17
17
  from pathlib import Path
18
18
 
19
+ import generate_dashboard
20
+
19
21
  HERE = Path(__file__).resolve().parent # directory holding the tooling modules
20
22
  CHECKOUT = HERE.parent # repo root when running from a checkout
21
- LIFECYCLE = ("active", "blocked", "deferred", "completed")
23
+ LIFECYCLE = ("proposed", "active", "blocked", "deferred", "completed")
22
24
  MODULES = (
23
25
  "repo_model.py", "validate_repo.py", "generate_dashboard.py", "generate_spec.py",
24
26
  "init_repo.py", "new.py", "check_frozen_surface.py", "frontmatter.py", "repopact_cli.py",
@@ -126,6 +128,7 @@ def bootstrap(target: Path, today: date | None = None) -> Path:
126
128
  _write(target / "README.md",
127
129
  "# Repository\n\nBootstrapped with RepoPact. Run `repopact validate` "
128
130
  "(or `python scripts/validate_repo.py`).\n")
131
+ generate_dashboard.write_dashboard(target, today=today)
129
132
  return target
130
133
 
131
134
 
@@ -16,6 +16,9 @@ import sys
16
16
  from datetime import date, datetime
17
17
  from pathlib import Path
18
18
 
19
+ import generate_dashboard
20
+ from repo_model import STATUSES
21
+
19
22
  ROOT = Path(__file__).resolve().parents[1]
20
23
 
21
24
 
@@ -32,11 +35,13 @@ def _next_numeric(paths: list[Path], width: int, start: int) -> str:
32
35
  return str(max(used + [start - 1]) + 1).zfill(width)
33
36
 
34
37
 
35
- def new_work_item(title: str, today: date, root: Path = ROOT) -> Path:
38
+ def new_work_item(title: str, today: date, root: Path = ROOT, status: str = "active") -> Path:
39
+ if status not in STATUSES:
40
+ raise ValueError(f"unknown work-item status '{status}'")
36
41
  slug = slugify(title)
37
42
  existing = list((root / "work").glob("*/*/work-item.json"))
38
43
  item_id = _next_numeric([p.parent for p in existing], 3, 1)
39
- directory = root / "work" / "active" / f"{item_id}-{slug}"
44
+ directory = root / "work" / status / f"{item_id}-{slug}"
40
45
  directory.mkdir(parents=True)
41
46
  # Mandatory preflight (decision 0021): creating the work item with `new` IS the
42
47
  # preflight act, so stamp the marker and concrete provenance up front. No work
@@ -44,7 +49,7 @@ def new_work_item(title: str, today: date, root: Path = ROOT) -> Path:
44
49
  created_at = datetime.combine(today, datetime.min.time()).isoformat() + "Z"
45
50
  manifest = {
46
51
  "$schema": "../../../schemas/work-item.schema.json",
47
- "id": item_id, "title": title, "status": "active",
52
+ "id": item_id, "title": title, "status": status,
48
53
  "owner_scope": "governance", "affected_scopes": [], "depends_on": [],
49
54
  "provenance": "concrete",
50
55
  "preflight": {
@@ -60,6 +65,7 @@ def new_work_item(title: str, today: date, root: Path = ROOT) -> Path:
60
65
  (directory / "README.md").write_text(
61
66
  (root / "templates" / "work-item.README.md").read_text(encoding="utf-8")
62
67
  .replace("NNN", item_id).replace("Title Of The Work", title), encoding="utf-8")
68
+ generate_dashboard.write_dashboard(root, today=today)
63
69
  return manifest_path
64
70
 
65
71
 
@@ -76,6 +82,7 @@ def new_markdown(kind: str, title: str, today: date, root: Path = ROOT) -> Path:
76
82
  text = text.replace("Decision Title", title).replace("Policy Title", title)
77
83
  path = directory / f"{record_id}-{slug}.md"
78
84
  path.write_text(text, encoding="utf-8")
85
+ generate_dashboard.write_dashboard(root, today=today)
79
86
  return path
80
87
 
81
88
 
@@ -83,10 +90,12 @@ def main() -> int:
83
90
  parser = argparse.ArgumentParser(description="Stamp a new record from a template")
84
91
  parser.add_argument("kind", choices=["work-item", "decision", "policy"])
85
92
  parser.add_argument("title")
93
+ parser.add_argument("--status", choices=STATUSES, default="active",
94
+ help="Lifecycle status for a new work item")
86
95
  args = parser.parse_args()
87
96
  today = date.today()
88
97
  if args.kind == "work-item":
89
- path = new_work_item(args.title, today)
98
+ path = new_work_item(args.title, today, status=args.status)
90
99
  else:
91
100
  path = new_markdown(args.kind, args.title, today)
92
101
  print(f"Created {path.relative_to(ROOT)}")
@@ -11,7 +11,7 @@ Supported sources (extensible):
11
11
 
12
12
  * **Plan directories** — `todos/`, `todo/`, `tasks/`, `plan/`, `planning/`, `backlog/`
13
13
  with one subdirectory or markdown file per item, optionally grouped under
14
- `completed/`, `deferred/`, `blocked/`, `active/` lifecycle folders.
14
+ `proposed/`, `completed/`, `deferred/`, `blocked/`, `active/` lifecycle folders.
15
15
  * **Checklist files** — `TODO.md`, `TODOS.md`, `ROADMAP.md`, `BACKLOG.md`, `PLAN.md`,
16
16
  `TASKS.md` at the root: each `- [ ]` / `- [x]` line becomes a work item.
17
17
 
@@ -30,19 +30,27 @@ from datetime import date
30
30
  from pathlib import Path
31
31
 
32
32
  import adopt_repo # reuse Report and _slug
33
+ import generate_dashboard
33
34
 
34
35
  PLAN_DIR_NAMES = ("todos", "todo", "tasks", "plan", "planning", "backlog")
35
36
  CHECKLIST_FILES = ("TODO.md", "TODOS.md", "ROADMAP.md", "BACKLOG.md", "PLAN.md", "TASKS.md")
36
- LIFECYCLE_DIRS = {"completed", "deferred", "blocked", "active"}
37
+ LIFECYCLE_DIRS = {"proposed", "completed", "deferred", "blocked", "active"}
37
38
 
38
39
  # source lifecycle -> (work status, acceptance-criterion state)
39
40
  _STATUS = {
41
+ "proposed": ("proposed", "pending"),
40
42
  "completed": ("completed", "waived"),
41
43
  "deferred": ("deferred", "pending"),
42
44
  "blocked": ("blocked", "pending"),
43
45
  "active": ("active", "pending"),
44
46
  }
45
- _EMOJI = {"completed": "✅ Complete", "deferred": "⏸️ Deferred", "blocked": "⛔ Blocked", "active": "📋 Active"}
47
+ _EMOJI = {
48
+ "proposed": "Proposed",
49
+ "completed": "✅ Complete",
50
+ "deferred": "⏸️ Deferred",
51
+ "blocked": "⛔ Blocked",
52
+ "active": "📋 Active",
53
+ }
46
54
 
47
55
  _SKIP_DIRS = {".git", ".venv", "node_modules", "__pycache__"}
48
56
  # Index/template/readme files in a plan directory are scaffolding, not plan items.
@@ -55,7 +63,7 @@ def _is_scaffold(name: str) -> bool:
55
63
 
56
64
  @dataclass
57
65
  class PlanItem:
58
- lifecycle: str # completed | deferred | blocked | active
66
+ lifecycle: str # proposed | completed | deferred | blocked | active
59
67
  slug: str # RepoPact slug (no leading number)
60
68
  title: str
61
69
  narrative: str # markdown body to preserve
@@ -352,6 +360,8 @@ def import_plan(root: Path, today: date | None = None, dry_run: bool = False,
352
360
  # A `tracking/` governance system maps to decisions/, findings, and work items.
353
361
  import track_import
354
362
  track_import.import_tracking(root, rep, today)
363
+ if not dry_run:
364
+ generate_dashboard.write_dashboard(root, today=today)
355
365
  return rep
356
366
 
357
367
 
@@ -6,7 +6,7 @@ from pathlib import Path
6
6
  from typing import Any
7
7
 
8
8
 
9
- STATUSES = ("active", "blocked", "deferred", "completed")
9
+ STATUSES = ("proposed", "active", "blocked", "deferred", "completed")
10
10
 
11
11
  # Directories that are not part of the governed tree: tooling caches and, notably,
12
12
  # test fixtures, which are self-contained sub-repositories validated on their own.
@@ -64,4 +64,3 @@ def discover_evidence_ids(root: Path) -> set[str]:
64
64
  for path in sorted((root / "evidence" / "runs").glob("*.json")):
65
65
  result.add(str(load_json(path).get("id", "")))
66
66
  return result
67
-
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: repopact
3
- Version: 2.0.2
3
+ Version: 2.2.0
4
4
  Summary: A repository-native standard for durable agent work: binding invariants, evidence-gated work items, and drift audits.
5
5
  License: Apache-2.0
6
6
  Project-URL: Homepage, https://github.com/ForgeWireLabs/repopact
@@ -22,8 +22,8 @@ cannot be silently weakened.
22
22
  > The repository is the pact: authority, intent, evidence, and history that survive every
23
23
  > session.
24
24
 
25
- `pip install repopact` · Apache-2.0 · current release **2.0.2**
26
- ([changelog](decisions/0022-release-2.0.2-installed-seed-lookup.md)).
25
+ `pip install repopact` · Apache-2.0 · current release **2.2.0**
26
+ ([changelog](decisions/0025-release-2.2.0-dashboard-integrity.md)).
27
27
 
28
28
  ## How it relates to `AGENTS.md`
29
29
 
@@ -71,7 +71,8 @@ intent -> scoped authority -> work item -> implementation -> evidence -> audit -
71
71
  pip install repopact # the CLI + reference validator, from PyPI
72
72
  repopact init --target ../your-repo # seed a valid RepoPact in a new repo
73
73
  cd ../your-repo
74
- repopact new work-item "Title of the work" # stamps records (incl. the preflight marker)
74
+ repopact new work-item "Title of the work" # stamps active work (incl. the preflight marker)
75
+ repopact new work-item "Candidate idea" --status proposed
75
76
  repopact validate
76
77
  repopact dashboard
77
78
  ```
@@ -110,7 +111,7 @@ idempotent.
110
111
 
111
112
  ## 2.0: mandatory preflight + provenance-typed records
112
113
 
113
- Decision [`0021`](decisions/0022-release-2.0.2-installed-seed-lookup.md) (supersedes 0018):
114
+ Decision [`0021`](decisions/0021-preflight-mandatory-and-provenance.md) (supersedes 0018):
114
115
 
115
116
  - **Mandatory preflight (default on).** No work begins until a work item exists and
116
117
  propagates through the pact; `repopact new` stamps the marker. Existing repos grandfather
@@ -123,9 +124,17 @@ Decision [`0021`](decisions/0022-release-2.0.2-installed-seed-lookup.md) (supers
123
124
 
124
125
  ## Status is a filesystem transition
125
126
 
126
- Work moves between `work/active`, `work/blocked`, `work/deferred`, and `work/completed`; its
127
- `work-item.json` status must match its directory. Moving a work item never deletes its
128
- reasoning, decisions, or evidence links.
127
+ Work moves between lifecycle directories; its `work-item.json` status must match its
128
+ directory. Moving a work item never deletes its reasoning, decisions, or evidence links.
129
+
130
+ - `proposed`: captured candidate work that is not yet accepted or authorized for
131
+ implementation.
132
+ - `active`: accepted work authorized for design or implementation.
133
+ - `blocked`: accepted/current work that cannot proceed until a named condition changes.
134
+ - `deferred`: accepted work intentionally postponed with rationale.
135
+ - `completed`: delivered work whose acceptance criteria are evidence-closed.
136
+
137
+ Active and completed work cannot depend on proposed work as if it were accepted.
129
138
 
130
139
  ## Derive over declare
131
140
 
@@ -10,8 +10,10 @@ from __future__ import annotations
10
10
 
11
11
  import argparse
12
12
  import sys
13
- from datetime import date
14
- from pathlib import Path
13
+ from datetime import date
14
+ from pathlib import Path
15
+
16
+ from repo_model import STATUSES
15
17
 
16
18
 
17
19
  def main(argv: list[str] | None = None) -> int:
@@ -50,10 +52,12 @@ def main(argv: list[str] | None = None) -> int:
50
52
  p_spec = sub.add_parser("spec", help="Regenerate SPEC.md derived blocks")
51
53
  p_spec.add_argument("--root", type=Path, default=Path.cwd())
52
54
 
53
- p_new = sub.add_parser("new", help="Stamp a new record from a template")
54
- p_new.add_argument("kind", choices=["work-item", "decision", "policy"])
55
- p_new.add_argument("title")
56
- p_new.add_argument("--root", type=Path, default=Path.cwd())
55
+ p_new = sub.add_parser("new", help="Stamp a new record from a template")
56
+ p_new.add_argument("kind", choices=["work-item", "decision", "policy"])
57
+ p_new.add_argument("title")
58
+ p_new.add_argument("--root", type=Path, default=Path.cwd())
59
+ p_new.add_argument("--status", choices=STATUSES, default="active",
60
+ help="Lifecycle status for a new work item")
57
61
 
58
62
  p_frz = sub.add_parser("check-frozen", help="Report frozen-surface changes in a diff range")
59
63
  p_frz.add_argument("--root", type=Path, default=Path.cwd())
@@ -146,12 +150,11 @@ def main(argv: list[str] | None = None) -> int:
146
150
  else f"\nValidation failed with {len(problems)} error(s).")
147
151
  return 1 if problems else 0
148
152
 
149
- if args.command == "dashboard":
150
- import generate_dashboard
151
- out = root / "audits" / "reports" / "dashboard.md"
152
- out.write_text(generate_dashboard.generate(root), encoding="utf-8")
153
- print(f"Generated {out.relative_to(root)}")
154
- return 0
153
+ if args.command == "dashboard":
154
+ import generate_dashboard
155
+ out = generate_dashboard.write_dashboard(root)
156
+ print(f"Generated {out.relative_to(root)}")
157
+ return 0
155
158
 
156
159
  if args.command == "spec":
157
160
  import generate_spec
@@ -169,10 +172,10 @@ def main(argv: list[str] | None = None) -> int:
169
172
  print("Generated SPEC.md derived blocks")
170
173
  return 0
171
174
 
172
- if args.command == "new":
173
- import new
174
- if args.kind == "work-item":
175
- path = new.new_work_item(args.title, date.today(), root)
175
+ if args.command == "new":
176
+ import new
177
+ if args.kind == "work-item":
178
+ path = new.new_work_item(args.title, date.today(), root, status=args.status)
176
179
  else:
177
180
  path = new.new_markdown(args.kind, args.title, date.today(), root)
178
181
  print(f"Created {path.relative_to(root)}")
@@ -16,6 +16,8 @@ import tempfile
16
16
  from dataclasses import dataclass
17
17
  from pathlib import Path
18
18
 
19
+ import generate_dashboard
20
+
19
21
 
20
22
  ROOT = Path(__file__).resolve().parents[1]
21
23
  MANIFEST = ROOT / "conformance" / "manifest.json"
@@ -54,6 +56,10 @@ def materialize_case(root: Path, fixtures_root: Path, case: dict) -> Path:
54
56
  else:
55
57
  shutil.copytree(fixtures_root / str(case["path"]), repo)
56
58
  shutil.copytree(ROOT / "schemas", repo / "schemas")
59
+ # Overlays intentionally mutate source records. Materialize their canonical
60
+ # derived projection so each case tests its declared rule rather than failing
61
+ # secondarily on dashboard drift.
62
+ generate_dashboard.write_dashboard(repo)
57
63
  return repo
58
64
 
59
65
 
@@ -35,6 +35,8 @@ import sys
35
35
  from datetime import date
36
36
  from pathlib import Path
37
37
 
38
+ import generate_dashboard
39
+
38
40
  import adopt_repo
39
41
  import plan_import
40
42
  import validate_repo
@@ -197,7 +199,7 @@ def registry_scopes_inside(root: Path, d: Path) -> list[str]:
197
199
  # File types we scan for inbound references; everything else (binaries, build output) is skipped.
198
200
  _TEXT_EXTS = {".md", ".markdown", ".txt", ".rst", ".py", ".toml", ".yaml", ".yml",
199
201
  ".json", ".cfg", ".ini", ".sh", ".ps1"}
200
- _LIFECYCLE = {"completed", "deferred", "active", "blocked"}
202
+ _LIFECYCLE = {"completed", "deferred", "active", "blocked", "proposed"}
201
203
  _MD_LINK = re.compile(r"(\]\()([^)\s]+)(\))")
202
204
 
203
205
 
@@ -362,6 +364,7 @@ def takeover(root: Path, delete: bool = False, dry_run: bool = False) -> dict:
362
364
  report["review"].append(hint)
363
365
 
364
366
  if not dry_run and report["retired"]:
367
+ generate_dashboard.write_dashboard(root)
365
368
  report["post_validate_ok"] = not validate_repo.validate(root)
366
369
  return report
367
370
 
@@ -8,10 +8,11 @@ from dataclasses import dataclass
8
8
  from datetime import date, datetime
9
9
  from pathlib import Path
10
10
 
11
- import jsonschema
12
-
13
- from frontmatter import FrontMatterError, parse_file
14
- from repo_model import STATUSES, discover_evidence_ids, discover_work_items, iter_contracts, load_json
11
+ import jsonschema
12
+
13
+ from frontmatter import FrontMatterError, parse_file
14
+ import generate_dashboard
15
+ from repo_model import STATUSES, discover_evidence_ids, discover_work_items, iter_contracts, load_json
15
16
 
16
17
 
17
18
  REQUIRED_WORK_FIELDS = {
@@ -381,11 +382,18 @@ def validate_work(root: Path, owner_scopes: set[str], enforce_disjoint: bool, pr
381
382
  validate_work_preflight(item, manifest, preflight_cfg, problems)
382
383
  validate_provenance(item, ev_prov, manifest, problems)
383
384
 
384
- all_ids = set(seen)
385
- for item in items:
386
- for dependency in item.data.get("depends_on", []):
387
- if dependency not in all_ids:
388
- problems.append(Problem(item.directory / "work-item.json", f"unknown dependency '{dependency}'"))
385
+ all_ids = set(seen)
386
+ status_by_id = {item.item_id: item.status for item in items}
387
+ for item in items:
388
+ for dependency in item.data.get("depends_on", []):
389
+ if dependency not in all_ids:
390
+ problems.append(Problem(item.directory / "work-item.json", f"unknown dependency '{dependency}'"))
391
+ elif item.status in ("active", "completed") and status_by_id.get(dependency) == "proposed":
392
+ problems.append(Problem(
393
+ item.directory / "work-item.json",
394
+ f"{item.status} work item depends on proposed work item '{dependency}'; "
395
+ "proposed work is not accepted implementation authority",
396
+ ))
389
397
 
390
398
  detect_dependency_cycles(items, all_ids, problems)
391
399
  if enforce_disjoint:
@@ -466,7 +474,7 @@ def validate_evidence(root: Path, work_ids: set[str], problems: list[Problem]) -
466
474
  problems.append(Problem(path, f"unknown work_item '{data['work_item']}'"))
467
475
 
468
476
 
469
- def validate_audit_registry(root: Path, problems: list[Problem]) -> None:
477
+ def validate_audit_registry(root: Path, problems: list[Problem]) -> None:
470
478
  path = root / "audits" / "registry.json"
471
479
  try:
472
480
  data = load_json(path)
@@ -478,7 +486,33 @@ def validate_audit_registry(root: Path, problems: list[Problem]) -> None:
478
486
  if not scope_path.exists():
479
487
  problems.append(Problem(path, f"audit scope does not exist: {entry.get('path')}"))
480
488
  validate_dates(entry.get("last_reviewed"), "last_reviewed", path, problems)
481
- validate_dates(entry.get("next_review"), "next_review", path, problems)
489
+ validate_dates(entry.get("next_review"), "next_review", path, problems)
490
+
491
+
492
+ def validate_dashboard(root: Path, problems: list[Problem]) -> None:
493
+ """Reject a missing or stale committed dashboard projection.
494
+
495
+ Source-record validation remains authoritative. If malformed sources prevent
496
+ generation, their existing validators report the primary error and this
497
+ secondary comparison stays silent instead of crashing validation.
498
+ """
499
+ path = root / "audits" / "reports" / "dashboard.md"
500
+ if not path.is_file():
501
+ problems.append(Problem(
502
+ path,
503
+ "missing generated dashboard; run `repopact dashboard --root .`",
504
+ ))
505
+ return
506
+ try:
507
+ expected = generate_dashboard.generate(root)
508
+ actual = path.read_text(encoding="utf-8")
509
+ except (OSError, ValueError, KeyError, TypeError, json.JSONDecodeError):
510
+ return
511
+ if actual != expected:
512
+ problems.append(Problem(
513
+ path,
514
+ "generated dashboard is stale; run `repopact dashboard --root .` and commit the result",
515
+ ))
482
516
 
483
517
 
484
518
  def _validate_records(root: Path, directory: Path, pattern: str, statuses: tuple[str, ...],
@@ -587,10 +621,11 @@ def validate(root: Path) -> list[Problem]:
587
621
  validate_findings(root, owner_scopes, problems)
588
622
  work_ids = validate_work(root, owner_scopes, enforce_disjoint, problems)
589
623
  validate_orphan_work_dirs(root, problems)
590
- validate_evidence(root, work_ids, problems)
591
- validate_audit_registry(root, problems)
592
- validate_decisions(root, problems)
593
- validate_policies(root, problems)
624
+ validate_evidence(root, work_ids, problems)
625
+ validate_audit_registry(root, problems)
626
+ validate_decisions(root, problems)
627
+ validate_policies(root, problems)
628
+ validate_dashboard(root, problems)
594
629
  return sorted(problems, key=lambda problem: (str(problem.path), problem.message))
595
630
 
596
631
 
@@ -21,6 +21,7 @@ ROOT = Path(__file__).resolve().parents[1]
21
21
  sys.path.insert(0, str(ROOT / "scripts"))
22
22
 
23
23
  from validate_repo import validate # noqa: E402
24
+ import generate_dashboard # noqa: E402
24
25
 
25
26
  import run_conformance # noqa: E402
26
27
 
@@ -33,6 +34,7 @@ INVALID = FIXTURES / "invalid"
33
34
  def build_repo(dst: Path, overlay: Path | None = None) -> Path:
34
35
  shutil.copytree(VALID, dst)
35
36
  shutil.copytree(ROOT / "schemas", dst / "schemas")
37
+ generate_dashboard.write_dashboard(dst)
36
38
  if overlay is not None:
37
39
  for src in overlay.rglob("*"):
38
40
  if src.is_dir() or src.name == "meta.json":
@@ -3,9 +3,10 @@ from __future__ import annotations
3
3
  import json
4
4
  import shutil
5
5
  import sys
6
- import tempfile
7
- import unittest
8
- from pathlib import Path
6
+ import tempfile
7
+ import unittest
8
+ from datetime import date
9
+ from pathlib import Path
9
10
 
10
11
 
11
12
  ROOT = Path(__file__).resolve().parents[1]
@@ -15,8 +16,9 @@ import adopt_repo # noqa: E402
15
16
  import plan_import # noqa: E402
16
17
  import takeover # noqa: E402
17
18
  import doctor # noqa: E402
18
- import check_frozen_surface # noqa: E402
19
- import generate_spec # noqa: E402
19
+ import check_frozen_surface # noqa: E402
20
+ import generate_dashboard # noqa: E402
21
+ import generate_spec # noqa: E402
20
22
  import init_repo # noqa: E402
21
23
  import repopact_cli # noqa: E402
22
24
  from validate_repo import validate # noqa: E402
@@ -48,34 +50,63 @@ class RepositoryValidationTests(unittest.TestCase):
48
50
  mutate(data)
49
51
  path.write_text(json.dumps(data), encoding="utf-8")
50
52
 
51
- def add_active_item(self, item_id: str, owner_scope: str = "work", preflight: bool = True) -> None:
52
- directory = self.root / "work" / "active" / f"{item_id}-probe"
53
- directory.mkdir(parents=True)
54
- (directory / "README.md").write_text("# probe\n", encoding="utf-8")
55
- data = {
56
- "id": item_id,
57
- "title": "probe",
58
- "status": "active",
59
- "owner_scope": owner_scope,
60
- "affected_scopes": [],
61
- "depends_on": [],
53
+ def add_work_item(
54
+ self,
55
+ item_id: str,
56
+ status: str = "active",
57
+ owner_scope: str = "work",
58
+ preflight: bool = True,
59
+ ) -> None:
60
+ directory = self.root / "work" / status / f"{item_id}-probe"
61
+ directory.mkdir(parents=True)
62
+ (directory / "README.md").write_text("# probe\n", encoding="utf-8")
63
+ data = {
64
+ "id": item_id,
65
+ "title": "probe",
66
+ "status": status,
67
+ "owner_scope": owner_scope,
68
+ "affected_scopes": [],
69
+ "depends_on": [],
62
70
  "provenance": "concrete",
63
71
  "acceptance_criteria": [{"id": "AC-1", "text": "x", "state": "pending", "evidence": []}],
64
72
  "created": "2026-06-15",
65
73
  "updated": "2026-06-15",
66
74
  }
67
- if preflight:
75
+ if preflight:
68
76
  data["preflight"] = {
69
77
  "created_before_work_started": True,
70
78
  "created_at": "2026-06-15T00:00:00Z",
71
79
  "note": "probe",
72
- }
73
- (directory / "work-item.json").write_text(json.dumps(data), encoding="utf-8")
74
-
80
+ }
81
+ (directory / "work-item.json").write_text(json.dumps(data), encoding="utf-8")
82
+ generate_dashboard.write_dashboard(self.root)
83
+
84
+ def add_active_item(self, item_id: str, owner_scope: str = "work", preflight: bool = True) -> None:
85
+ self.add_work_item(item_id, "active", owner_scope, preflight)
86
+
75
87
  # --- baseline -----------------------------------------------------------
76
88
 
77
- def test_repository_is_valid(self) -> None:
78
- self.assertEqual([], self.problems())
89
+ def test_repository_is_valid(self) -> None:
90
+ self.assertEqual([], self.problems())
91
+
92
+ def test_missing_dashboard_is_rejected(self) -> None:
93
+ (self.root / "audits" / "reports" / "dashboard.md").unlink()
94
+ self.assertTrue(any("missing generated dashboard" in value for value in self.problems()))
95
+
96
+ def test_stale_dashboard_is_rejected_and_doctor_repairs_it(self) -> None:
97
+ dashboard = self.root / "audits" / "reports" / "dashboard.md"
98
+ dashboard.write_text("# obsolete dashboard\n", encoding="utf-8")
99
+ self.assertTrue(any("generated dashboard is stale" in value for value in self.problems()))
100
+
101
+ actions = doctor.fix(self.root, today=date(2026, 7, 18))
102
+ self.assertIn("regenerated audits/reports/dashboard.md", actions)
103
+ self.assertEqual([], self.problems())
104
+
105
+ def test_dashboard_generation_is_stable_between_audit_transitions(self) -> None:
106
+ first = generate_dashboard.generate(self.root, today=date(2026, 7, 18))
107
+ later = generate_dashboard.generate(self.root, today=date(2026, 7, 19))
108
+ self.assertEqual(first, later)
109
+ self.assertNotIn("> Generated:", first)
79
110
 
80
111
  # --- work lifecycle -----------------------------------------------------
81
112
 
@@ -90,10 +121,27 @@ class RepositoryValidationTests(unittest.TestCase):
90
121
  self.write_json(path, lambda d: (d["acceptance_criteria"][0].update({"state": "satisfied", "evidence": []})))
91
122
  self.assertTrue(any("satisfied without evidence" in v for v in self.problems()))
92
123
 
93
- def test_completed_item_cannot_have_pending_criteria(self) -> None:
94
- path = self.manifest()
95
- self.write_json(path, lambda d: (d["acceptance_criteria"][0].update({"state": "pending", "evidence": []})))
96
- self.assertTrue(any("completed item has pending criterion" in v for v in self.problems()))
124
+ def test_completed_item_cannot_have_pending_criteria(self) -> None:
125
+ path = self.manifest()
126
+ self.write_json(path, lambda d: (d["acceptance_criteria"][0].update({"state": "pending", "evidence": []})))
127
+ self.assertTrue(any("completed item has pending criterion" in v for v in self.problems()))
128
+
129
+ def test_valid_proposed_work_item_validates(self) -> None:
130
+ self.add_work_item("900", status="proposed")
131
+ self.assertEqual([], self.problems())
132
+
133
+ def test_invalid_proposed_work_item_fails_structure_validation(self) -> None:
134
+ self.add_work_item("900", status="proposed")
135
+ path = self.root / "work" / "proposed" / "900-probe" / "work-item.json"
136
+ self.write_json(path, lambda d: d.pop("title"))
137
+ self.assertTrue(any("missing fields: title" in v for v in self.problems()))
138
+
139
+ def test_active_work_cannot_depend_on_proposed_work(self) -> None:
140
+ self.add_work_item("900", status="proposed")
141
+ self.add_active_item("901")
142
+ active = self.root / "work" / "active" / "901-probe" / "work-item.json"
143
+ self.write_json(active, lambda d: d.__setitem__("depends_on", ["900"]))
144
+ self.assertTrue(any("depends on proposed work item '900'" in v for v in self.problems()))
97
145
 
98
146
  def test_readme_checkbox_parity_flags_contradiction(self) -> None:
99
147
  path = self.manifest()
@@ -398,10 +446,11 @@ class RepositoryValidationTests(unittest.TestCase):
398
446
 
399
447
  # --- bootstrap (003 B1) -------------------------------------------------
400
448
 
401
- def test_bootstrap_produces_valid_repo(self) -> None:
402
- target = Path(self.temp.name) / "seeded"
403
- init_repo.bootstrap(target)
404
- self.assertEqual([], [p.message for p in validate(target)])
449
+ def test_bootstrap_produces_valid_repo(self) -> None:
450
+ target = Path(self.temp.name) / "seeded"
451
+ init_repo.bootstrap(target)
452
+ self.assertTrue((target / "work" / "proposed").is_dir())
453
+ self.assertEqual([], [p.message for p in validate(target)])
405
454
 
406
455
  # --- SPEC generator determinism (004) ----------------------------------
407
456
 
@@ -418,12 +467,21 @@ class RepositoryValidationTests(unittest.TestCase):
418
467
  def test_cli_validate_returns_zero_on_valid_repo(self) -> None:
419
468
  self.assertEqual(0, repopact_cli.main(["validate", "--root", str(self.root)]))
420
469
 
421
- def test_cli_new_stamps_a_valid_record(self) -> None:
422
- rc = repopact_cli.main(["new", "work-item", "Cli Probe", "--root", str(self.root)])
423
- self.assertEqual(0, rc)
424
- stamped = list((self.root / "work" / "active").glob("*-cli-probe/work-item.json"))
425
- self.assertEqual(1, len(stamped))
426
- self.assertEqual([], [p.message for p in validate(self.root)])
470
+ def test_cli_new_stamps_a_valid_record(self) -> None:
471
+ rc = repopact_cli.main(["new", "work-item", "Cli Probe", "--root", str(self.root)])
472
+ self.assertEqual(0, rc)
473
+ stamped = list((self.root / "work" / "active").glob("*-cli-probe/work-item.json"))
474
+ self.assertEqual(1, len(stamped))
475
+ self.assertEqual([], [p.message for p in validate(self.root)])
476
+
477
+ def test_cli_new_can_stamp_proposed_work_item(self) -> None:
478
+ rc = repopact_cli.main(["new", "work-item", "Cli Proposal", "--status", "proposed", "--root", str(self.root)])
479
+ self.assertEqual(0, rc)
480
+ stamped = list((self.root / "work" / "proposed").glob("*-cli-proposal/work-item.json"))
481
+ self.assertEqual(1, len(stamped))
482
+ data = json.loads(stamped[0].read_text(encoding="utf-8"))
483
+ self.assertEqual("proposed", data["status"])
484
+ self.assertEqual([], [p.message for p in validate(self.root)])
427
485
 
428
486
  # --- proving-ground hardening (007) -------------------------------------
429
487
 
@@ -668,8 +726,9 @@ class RepositoryValidationTests(unittest.TestCase):
668
726
  "contract": "todos/12-search/AGENTS.md", "last_reviewed": "2026-06-15",
669
727
  "next_review": "2026-09-13", "alignment": "current", "notes": "nested",
670
728
  })
671
- (repo / "audits" / "registry.json").write_text(json.dumps(reg, indent=2) + "\n", encoding="utf-8")
672
- report = takeover.takeover(repo, delete=True)
729
+ (repo / "audits" / "registry.json").write_text(json.dumps(reg, indent=2) + "\n", encoding="utf-8")
730
+ generate_dashboard.write_dashboard(repo)
731
+ report = takeover.takeover(repo, delete=True)
673
732
  self.assertEqual([], report["retired"]) # not retired
674
733
  self.assertTrue((repo / "todos").exists())
675
734
  self.assertTrue(any(b["dir"] == "todos" for b in report["blocked"]))
repopact-2.0.2/VERSION DELETED
@@ -1 +0,0 @@
1
- 2.0.2
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes