workflow-toolkit 1.0.1

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 (142) hide show
  1. package/.agents/skills/ponytail/SKILL.md +111 -0
  2. package/.agents/skills/ponytail-audit/SKILL.md +37 -0
  3. package/.agents/skills/ponytail-debt/SKILL.md +41 -0
  4. package/.agents/skills/ponytail-gain/SKILL.md +47 -0
  5. package/.agents/skills/ponytail-help/SKILL.md +70 -0
  6. package/.agents/skills/ponytail-review/SKILL.md +52 -0
  7. package/.agents/skills/prompt-review/SKILL.md +60 -0
  8. package/.agents/skills/wtk/SKILL.md +64 -0
  9. package/.agents/skills/wtk/references/artifacts.md +82 -0
  10. package/.agents/skills/wtk/references/evidence.md +68 -0
  11. package/.agents/skills/wtk/references/git.md +75 -0
  12. package/.agents/skills/wtk/references/test-contract.md +76 -0
  13. package/.agents/skills/wtk/references/validation.md +127 -0
  14. package/.agents/skills/wtk-config/SKILL.md +102 -0
  15. package/.agents/skills/wtk-config/assets/agents/claude/deep-reviewer.md +40 -0
  16. package/.agents/skills/wtk-config/assets/agents/claude/designer.md +57 -0
  17. package/.agents/skills/wtk-config/assets/agents/claude/explorer.md +41 -0
  18. package/.agents/skills/wtk-config/assets/agents/claude/implementer.md +57 -0
  19. package/.agents/skills/wtk-config/assets/agents/claude/planner.md +68 -0
  20. package/.agents/skills/wtk-config/assets/agents/claude/verifier.md +80 -0
  21. package/.agents/skills/wtk-config/assets/agents/codex/deep-reviewer.toml +41 -0
  22. package/.agents/skills/wtk-config/assets/agents/codex/designer.toml +55 -0
  23. package/.agents/skills/wtk-config/assets/agents/codex/explorer.toml +35 -0
  24. package/.agents/skills/wtk-config/assets/agents/codex/implementer.toml +52 -0
  25. package/.agents/skills/wtk-config/assets/agents/codex/planner.toml +66 -0
  26. package/.agents/skills/wtk-config/assets/agents/codex/verifier.toml +78 -0
  27. package/.agents/skills/wtk-config/assets/agents/cursor/deep-reviewer.md +38 -0
  28. package/.agents/skills/wtk-config/assets/agents/cursor/designer.md +55 -0
  29. package/.agents/skills/wtk-config/assets/agents/cursor/explorer.md +39 -0
  30. package/.agents/skills/wtk-config/assets/agents/cursor/implementer.md +55 -0
  31. package/.agents/skills/wtk-config/assets/agents/cursor/planner.md +66 -0
  32. package/.agents/skills/wtk-config/assets/agents/cursor/verifier.md +79 -0
  33. package/.agents/skills/wtk-config/scripts/ad-index.py +123 -0
  34. package/.agents/skills/wtk-config/scripts/repository_intelligence.py +671 -0
  35. package/.agents/skills/wtk-config/scripts/workflow_config.py +972 -0
  36. package/.agents/skills/wtk-deep-review/SKILL.md +164 -0
  37. package/.agents/skills/wtk-deep-review/assets/PROMPT.md +56 -0
  38. package/.agents/skills/wtk-deep-review/assets/REVIEW_UI.html +1340 -0
  39. package/.agents/skills/wtk-deep-review/assets/findings.schema.json +113 -0
  40. package/.agents/skills/wtk-deep-review/references/context-pack.md +88 -0
  41. package/.agents/skills/wtk-deep-review/references/orchestration.md +164 -0
  42. package/.agents/skills/wtk-deep-review/references/output-contracts.md +149 -0
  43. package/.agents/skills/wtk-deep-review/references/publish-github.md +83 -0
  44. package/.agents/skills/wtk-deep-review/references/state-and-learnings.md +64 -0
  45. package/.agents/skills/wtk-deep-review/references/subagent-runtimes.md +33 -0
  46. package/.agents/skills/wtk-deep-review/references/taxonomy.md +70 -0
  47. package/.agents/skills/wtk-deep-review/scripts/_common.py +371 -0
  48. package/.agents/skills/wtk-deep-review/scripts/build_jobs.py +561 -0
  49. package/.agents/skills/wtk-deep-review/scripts/build_knowledge.py +349 -0
  50. package/.agents/skills/wtk-deep-review/scripts/build_manifest.py +479 -0
  51. package/.agents/skills/wtk-deep-review/scripts/graft_context.py +160 -0
  52. package/.agents/skills/wtk-deep-review/scripts/graphify_context.py +75 -0
  53. package/.agents/skills/wtk-deep-review/scripts/merge_findings.py +313 -0
  54. package/.agents/skills/wtk-deep-review/scripts/render_html.py +240 -0
  55. package/.agents/skills/wtk-deep-review/scripts/render_review.py +282 -0
  56. package/.agents/skills/wtk-deep-review/scripts/run_jobs.py +436 -0
  57. package/.agents/skills/wtk-deep-review/scripts/token_metrics.py +369 -0
  58. package/.agents/skills/wtk-discover/SKILL.md +270 -0
  59. package/.agents/skills/wtk-discover/references/document-format.md +149 -0
  60. package/.agents/skills/wtk-implement/SKILL.md +95 -0
  61. package/.agents/skills/wtk-implement/references/checklist-format.md +116 -0
  62. package/.agents/skills/wtk-implement/references/screens.md +129 -0
  63. package/.agents/skills/wtk-implement/references/test-policy.md +152 -0
  64. package/.agents/skills/wtk-implement/references/verify.md +297 -0
  65. package/.agents/skills/wtk-knowledge-check/SKILL.md +16 -0
  66. package/.agents/skills/wtk-knowledge-check/scripts/check.ts +502 -0
  67. package/.agents/skills/wtk-knowledge-check/scripts/cli.ts +32 -0
  68. package/.agents/skills/wtk-knowledge-check/scripts/frontmatter.ts +44 -0
  69. package/.agents/skills/wtk-lean/.skill-meta.json +6 -0
  70. package/.agents/skills/wtk-lean/NOTICE.md +18 -0
  71. package/.agents/skills/wtk-lean/SKILL.md +137 -0
  72. package/.agents/skills/wtk-lean/references/build.md +168 -0
  73. package/.agents/skills/wtk-lean/references/checks.md +253 -0
  74. package/.agents/skills/wtk-lean/references/memory.md +156 -0
  75. package/.agents/skills/wtk-lean/references/plan.md +514 -0
  76. package/.agents/skills/wtk-lean/references/verify.md +336 -0
  77. package/.agents/skills/wtk-lean/scripts/check_commit.py +121 -0
  78. package/.agents/skills/wtk-lean/scripts/fixtures/checks.md +98 -0
  79. package/.agents/skills/wtk-lean/scripts/fixtures/plan.md +119 -0
  80. package/.agents/skills/wtk-lean/scripts/fixtures/verification.md +41 -0
  81. package/.agents/skills/wtk-lean/scripts/lessons.py +412 -0
  82. package/.agents/skills/wtk-lean/scripts/selftest.py +401 -0
  83. package/.agents/skills/wtk-lean/scripts/validate_checks.py +417 -0
  84. package/.agents/skills/wtk-lean/scripts/validate_plan.py +580 -0
  85. package/.agents/skills/wtk-lean/scripts/validate_verification.py +357 -0
  86. package/.agents/skills/wtk-plan/SKILL.md +102 -0
  87. package/.agents/skills/wtk-plan/references/document-format.md +222 -0
  88. package/.agents/skills/wtk-qa/SKILL.md +14 -0
  89. package/.agents/skills/wtk-qa-execute/SKILL.md +115 -0
  90. package/.agents/skills/wtk-qa-execute/references/fix-loop.md +23 -0
  91. package/.agents/skills/wtk-qa-execute/references/session-protocol.md +25 -0
  92. package/.agents/skills/wtk-qa-plan/SKILL.md +111 -0
  93. package/.agents/skills/wtk-qa-plan/references/profile.md +35 -0
  94. package/.agents/skills/wtk-ship/SKILL.md +49 -0
  95. package/.agents/skills/wtk-ship/remediation.py +170 -0
  96. package/.agents/skills/wtk-ship/scripts/close_feature.py +83 -0
  97. package/.agents/skills/wtk-ship/scripts/review_convergence.py +373 -0
  98. package/.wtk.toml.example +93 -0
  99. package/AGENTS.md +129 -0
  100. package/NOTICE.md +12 -0
  101. package/README.md +440 -0
  102. package/bin/wtk.js +25 -0
  103. package/docs/toolkit/README.md +56 -0
  104. package/docs/toolkit/decisions.md +61 -0
  105. package/docs/toolkit/guidelines/CONTEXT-BUDGET.md +64 -0
  106. package/docs/toolkit/guidelines/DX.md +67 -0
  107. package/docs/toolkit/guidelines/FRONTEND.md +73 -0
  108. package/docs/toolkit/guidelines/KNOWLEDGE-WIKI.md +128 -0
  109. package/docs/toolkit/guidelines/MODELING.md +134 -0
  110. package/docs/toolkit/guidelines/QA-EXECUTION.md +46 -0
  111. package/docs/toolkit/guidelines/QA-SCENARIOS.md +165 -0
  112. package/docs/toolkit/guidelines/REVIEW-ROUNDS.md +160 -0
  113. package/docs/toolkit/guidelines/SECURITY.md +160 -0
  114. package/docs/toolkit/guidelines/UI-UX.md +117 -0
  115. package/docs/toolkit/guidelines/WORKFLOW-MEMORY.md +73 -0
  116. package/docs/toolkit/guidelines.md +50 -0
  117. package/docs/toolkit/loop.md +77 -0
  118. package/docs/toolkit/purpose.md +51 -0
  119. package/docs/toolkit/repository-intelligence.md +53 -0
  120. package/docs/toolkit/reviews.md +77 -0
  121. package/knowledge/AGENTS.md +316 -0
  122. package/knowledge/raw/README.md +17 -0
  123. package/package.json +74 -0
  124. package/scripts/install_security_skills.py +816 -0
  125. package/scripts/installer/engine.js +254 -0
  126. package/scripts/installer/knowledge.js +28 -0
  127. package/scripts/installer/packets.js +77 -0
  128. package/scripts/installer/terminal.js +98 -0
  129. package/scripts/installer/transaction.js +75 -0
  130. package/skills-lock.json +92 -0
  131. package/templates/adoption/agents/core.md +23 -0
  132. package/templates/adoption/agents/quality.md +3 -0
  133. package/templates/adoption/knowledge/wiki/architecture/index.md +3 -0
  134. package/templates/adoption/knowledge/wiki/decisions/index.md +3 -0
  135. package/templates/adoption/knowledge/wiki/design/index.md +3 -0
  136. package/templates/adoption/knowledge/wiki/domain/index.md +3 -0
  137. package/templates/adoption/knowledge/wiki/index.md +3 -0
  138. package/templates/adoption/knowledge/wiki/log.md +3 -0
  139. package/templates/adoption/knowledge/wiki/open-questions/index.md +3 -0
  140. package/templates/adoption/knowledge/wiki/product/index.md +3 -0
  141. package/templates/adoption/knowledge/wiki/research/index.md +3 -0
  142. package/templates/adoption/product/AGENT-CONTEXT.md +25 -0
@@ -0,0 +1,170 @@
1
+ #!/usr/bin/env python3
2
+ """Pure remediation-stall normalization and transition helper.
3
+
4
+ The executable JSON protocol reads one object from stdin and writes the next
5
+ state to stdout. No repository, process, or remote authority is involved.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import re
12
+ import sys
13
+ from collections.abc import Iterable, Mapping
14
+ from typing import Any
15
+
16
+ DEFAULT_STALL_ATTEMPTS = 3
17
+ _LOCATION = re.compile(
18
+ r"(?P<path>(?:(?:[A-Za-z]:)?[\\/]|(?<!\S))[^()\r\n]*?"
19
+ r"[^/\\\s:]+\.[^/\\\s:()]+)(?::\d+(?::\d+)?|\(\d+,\d+\):?)"
20
+ )
21
+ _PATH = re.compile(
22
+ r"(?P<path>(?:(?:[A-Za-z]:)?[\\/]|(?<!\S))[^()\r\n]*?"
23
+ r"[^/\\\s:]+\.[^/\\\s:()]+)(?=\s*(?:>|$))"
24
+ )
25
+ _TIMING = re.compile(r"\s*\(?\d+(?:\.\d+)?\s*(?:ms|s)\)?", re.IGNORECASE)
26
+ _WHITESPACE = re.compile(r"\s+")
27
+
28
+
29
+ def normalize_stall_attempts(value: Any) -> int:
30
+ """Return the strict bound: absent is 3, zero is unbounded."""
31
+ if value is None:
32
+ return DEFAULT_STALL_ATTEMPTS
33
+ if isinstance(value, bool) or not isinstance(value, int) or value < 0:
34
+ raise ValueError("stall_attempts must be a non-negative integer")
35
+ return value
36
+
37
+
38
+ def _normalize_test_name(value: Any) -> str:
39
+ if not isinstance(value, str):
40
+ raise ValueError("failing test names must be strings")
41
+ text = _TIMING.sub("", value.strip())
42
+
43
+ def strip_location(match: re.Match[str]) -> str:
44
+ path = match.group("path").replace("\\", "/").rsplit("/", 1)[-1]
45
+ return f"{path} >" if match.group(0).endswith(":") else path
46
+
47
+ text = _LOCATION.sub(strip_location, text)
48
+ text = _PATH.sub(strip_location, text)
49
+ return _WHITESPACE.sub(" ", text).strip()
50
+
51
+
52
+ def normalize_failing_tests(names: Iterable[Any]) -> list[str]:
53
+ """Trim, de-time, de-path, deduplicate, and sort test identifiers."""
54
+ if isinstance(names, (str, bytes)):
55
+ raise ValueError("failing tests must be an array")
56
+ values = {_normalize_test_name(name) for name in names}
57
+ values.discard("")
58
+ return sorted(values, key=lambda value: (value.casefold(), value))
59
+
60
+
61
+ def _normalize_fixes(fixes: Iterable[Any]) -> list[str]:
62
+ if isinstance(fixes, (str, bytes)):
63
+ raise ValueError("fixes_tried must be an array")
64
+ result: list[str] = []
65
+ seen: set[str] = set()
66
+ for fix in fixes:
67
+ if not isinstance(fix, str):
68
+ raise ValueError("fixes_tried must contain strings")
69
+ value = _WHITESPACE.sub(" ", fix.strip())
70
+ if value and value not in seen:
71
+ seen.add(value)
72
+ result.append(value)
73
+ return result
74
+
75
+
76
+ def _previous_tests(previous: Mapping[str, Any]) -> tuple[bool, list[str]]:
77
+ attempt_count = int(previous.get("attempt_count", 0))
78
+ if attempt_count == 0:
79
+ return False, []
80
+ if "minimum_failing_tests" not in previous:
81
+ raise ValueError("previous state is missing minimum_failing_tests")
82
+ return True, normalize_failing_tests(previous["minimum_failing_tests"])
83
+
84
+
85
+ def transition_remediation(
86
+ previous: Mapping[str, Any] | None,
87
+ failing_tests: Iterable[Any],
88
+ *,
89
+ stall_attempts: Any = None,
90
+ gate_available: bool = True,
91
+ fixes_tried: Iterable[Any] = (),
92
+ ) -> dict[str, Any]:
93
+ """Advance one post-cap remediation attempt without side effects."""
94
+ previous_state: Mapping[str, Any] = previous or {}
95
+ threshold = normalize_stall_attempts(stall_attempts)
96
+ current = normalize_failing_tests(failing_tests)
97
+ fixes = _normalize_fixes(previous_state.get("fixes_tried", []))
98
+ for fix in _normalize_fixes(fixes_tried):
99
+ if fix not in fixes:
100
+ fixes.append(fix)
101
+ attempt_count = int(previous_state.get("attempt_count", 0)) + 1
102
+ prior_stalls = int(previous_state.get("consecutive_stalls", 0))
103
+ had_baseline, prior_tests = _previous_tests(previous_state)
104
+ minimum_tests = prior_tests
105
+ minimum_count = len(minimum_tests)
106
+
107
+ state: dict[str, Any] = {
108
+ "stall_attempts": threshold,
109
+ "failing_tests": current,
110
+ "failing_signature": " | ".join(current),
111
+ "minimum_failing_tests": minimum_tests,
112
+ "attempt_count": attempt_count,
113
+ "consecutive_stalls": prior_stalls,
114
+ "minimum_failing_count": minimum_count,
115
+ "fixes_tried": fixes,
116
+ "halted": False,
117
+ }
118
+
119
+ if not gate_available:
120
+ state.update({"status": "halted", "reason": "scoped_gate_unavailable", "halted": True})
121
+ state["report"] = {
122
+ "reason": state["reason"],
123
+ "failing_signature": state["failing_signature"],
124
+ "attempt_count": state["attempt_count"],
125
+ "fixes_tried": state["fixes_tried"],
126
+ }
127
+ return state
128
+
129
+ if not had_baseline or set(current) < set(minimum_tests):
130
+ state.update({
131
+ "status": "progress",
132
+ "reason": "smaller_failing_set" if had_baseline else "initial_failure_set",
133
+ "consecutive_stalls": 0,
134
+ "minimum_failing_tests": current,
135
+ "minimum_failing_count": len(current),
136
+ })
137
+ return state
138
+
139
+ stalls = prior_stalls + 1
140
+ state.update({"status": "stalled", "reason": "failing_set_not_smaller", "consecutive_stalls": stalls})
141
+ if threshold and stalls >= threshold:
142
+ state.update({"status": "halted", "reason": "stall_threshold_reached", "halted": True})
143
+ state["report"] = {
144
+ "reason": state["reason"],
145
+ "failing_signature": state["failing_signature"],
146
+ "attempt_count": state["attempt_count"],
147
+ "fixes_tried": state["fixes_tried"],
148
+ }
149
+ return state
150
+
151
+
152
+ def main() -> int:
153
+ try:
154
+ payload = json.load(sys.stdin)
155
+ result = transition_remediation(
156
+ payload.get("previous"),
157
+ payload.get("failing_tests", []),
158
+ stall_attempts=payload.get("stall_attempts"),
159
+ gate_available=payload.get("gate_available", True),
160
+ fixes_tried=payload.get("fixes_tried", []),
161
+ )
162
+ except (TypeError, ValueError, KeyError, json.JSONDecodeError) as error:
163
+ print(f"error: {error}", file=sys.stderr)
164
+ return 2
165
+ print(json.dumps(result, sort_keys=True))
166
+ return 0
167
+
168
+
169
+ if __name__ == "__main__":
170
+ raise SystemExit(main())
@@ -0,0 +1,83 @@
1
+ #!/usr/bin/env python3
2
+ """Close a verified Workflow Toolkit feature after explicit fact promotion.
3
+
4
+ Promotion is semantic work performed by the agent and human, so this helper only
5
+ checks the receipt and removes the transient feature directory. It never writes
6
+ knowledge, decisions, product promises, or architecture documentation.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import argparse
12
+ import json
13
+ import re
14
+ import shutil
15
+ import subprocess
16
+ import sys
17
+ from pathlib import Path
18
+
19
+ SLUG = re.compile(r"^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$")
20
+
21
+
22
+ def feature_dir(root: Path, feature: str) -> Path:
23
+ if not SLUG.fullmatch(feature):
24
+ raise ValueError("feature must be a lowercase slug")
25
+ root = root.resolve()
26
+ specs = root / ".specs"
27
+ features = specs / "features"
28
+ target = features / feature
29
+ for parent in (specs, features):
30
+ if parent.is_symlink():
31
+ raise ValueError(f"cleanup path must not be a symlink: {parent.relative_to(root)}")
32
+ if not parent.is_dir():
33
+ raise ValueError(f"feature directory is missing: {features.relative_to(root)}")
34
+ if target.is_symlink():
35
+ raise ValueError(f"cleanup path must not be a symlink: {target.relative_to(root)}")
36
+ if target.parent != features or target == features or target.resolve().parent != features:
37
+ raise ValueError("feature path must remain under .specs/features")
38
+ return target
39
+
40
+
41
+ def close_feature(root: Path, feature: str, *, promoted: bool = False) -> Path:
42
+ """Delete one verified feature directory after a recorded promotion."""
43
+ if not promoted:
44
+ raise ValueError("promotion receipt is required before cleanup")
45
+ root = root.resolve()
46
+ target = feature_dir(root, feature)
47
+ verification = target / "verification.md"
48
+ if not verification.is_file():
49
+ raise ValueError("verification.md is required before cleanup")
50
+ checks = target / "checks.md"
51
+ if not checks.is_file():
52
+ raise ValueError("checks.md is required before cleanup")
53
+ validator = root / ".agents" / "skills" / "wtk-lean" / "scripts" / "validate_verification.py"
54
+ result = subprocess.run(
55
+ [sys.executable, str(validator), str(target.resolve()), "--root", str(root)],
56
+ cwd=root,
57
+ text=True,
58
+ capture_output=True,
59
+ check=False,
60
+ )
61
+ if result.returncode != 0:
62
+ raise ValueError(result.stderr.strip() or result.stdout.strip() or "verification gate failed")
63
+ shutil.rmtree(target)
64
+ return target
65
+
66
+
67
+ def main(argv: list[str] | None = None) -> int:
68
+ parser = argparse.ArgumentParser(description=__doc__)
69
+ parser.add_argument("feature")
70
+ parser.add_argument("--root", type=Path, default=Path.cwd())
71
+ parser.add_argument("--promoted", action="store_true", help="confirm durable facts were promoted")
72
+ args = parser.parse_args(argv)
73
+ try:
74
+ removed = close_feature(args.root, args.feature, promoted=args.promoted)
75
+ except (OSError, ValueError) as exc:
76
+ print(f"close_feature: {exc}", file=sys.stderr)
77
+ return 1
78
+ print(json.dumps({"feature": args.feature, "removed": str(removed.relative_to(args.root.resolve()))}))
79
+ return 0
80
+
81
+
82
+ if __name__ == "__main__":
83
+ raise SystemExit(main())
@@ -0,0 +1,373 @@
1
+ #!/usr/bin/env python3
2
+ """Persist immutable blocker fingerprints and append-only audit generations."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import hashlib
8
+ import json
9
+ import os
10
+ import re
11
+ import sys
12
+ import tempfile
13
+ from copy import deepcopy
14
+ from pathlib import Path
15
+ from typing import Any
16
+
17
+ _SKILLS_ROOT = Path(__file__).resolve().parents[2]
18
+ sys.path.insert(0, str(_SKILLS_ROOT / "wtk-ship"))
19
+ import remediation
20
+
21
+ sys.path.insert(0, str(_SKILLS_ROOT / "wtk-config" / "scripts"))
22
+ import workflow_config
23
+
24
+ MAX_FAILURES = 3
25
+ GENERATION_STATUSES = {"open", "halted", "closed"}
26
+ REMEDIATION_FIELDS = (
27
+ "failing_tests", "failing_signature", "minimum_failing_tests", "minimum_failing_count",
28
+ "consecutive_stalls", "attempt_count", "fixes_tried",
29
+ )
30
+
31
+
32
+ def _normalize(value: str) -> str:
33
+ return re.sub(r"\s+", " ", value.strip().lower())
34
+
35
+
36
+ def fingerprint(requirement: str, root_cause: str, failure_path: str) -> str:
37
+ material = "\0".join(_normalize(value) for value in (requirement, root_cause, failure_path))
38
+ return hashlib.sha256(material.encode("utf-8")).hexdigest()
39
+
40
+
41
+ def state_path(root: Path, feature: str) -> Path:
42
+ if not re.fullmatch(r"[a-z0-9]+(?:-[a-z0-9]+)*", feature):
43
+ raise ValueError("invalid feature")
44
+ resolved_root = Path(root).resolve()
45
+ feature_dir = (resolved_root / ".specs" / "features" / feature).resolve()
46
+ try:
47
+ feature_dir.relative_to(resolved_root)
48
+ except ValueError as exc:
49
+ raise ValueError("feature path escapes root") from exc
50
+ return feature_dir / "review-fingerprints.json"
51
+
52
+
53
+ def _halt_event(generation: int, failures: int) -> dict[str, Any]:
54
+ return {"generation": generation, "failed_remediations": failures, "status": "halted"}
55
+
56
+
57
+ def _generation(number: int, failures: int, status: str, **extra: Any) -> dict[str, Any]:
58
+ value: dict[str, Any] = {
59
+ "generation": number,
60
+ "failed_remediations": failures,
61
+ "status": status,
62
+ "failing_tests": [],
63
+ "failing_signature": "",
64
+ "minimum_failing_tests": [],
65
+ "minimum_failing_count": 0,
66
+ "consecutive_stalls": 0,
67
+ "attempt_count": 0,
68
+ "fixes_tried": [],
69
+ }
70
+ value.update(extra)
71
+ if status == "halted":
72
+ value.setdefault("halt_event", _halt_event(number, failures))
73
+ return value
74
+
75
+
76
+ def _legacy_generation(entry: dict[str, Any]) -> dict[str, Any]:
77
+ failures = entry.get("failed_remediations")
78
+ status = entry.get("status")
79
+ if type(failures) is not int or failures < 0 or status not in GENERATION_STATUSES:
80
+ raise ValueError("invalid convergence entry")
81
+ if status == "halted" and failures < MAX_FAILURES:
82
+ raise ValueError("halted convergence entry has too few failures")
83
+ if status == "open" and failures >= MAX_FAILURES:
84
+ raise ValueError("open convergence entry has too many failures")
85
+ if status == "closed" and failures >= MAX_FAILURES:
86
+ raise ValueError("closed convergence entry has too many failures")
87
+ return _generation(1, failures, status)
88
+
89
+
90
+ def _valid_authorization(reference: str) -> bool:
91
+ return bool(
92
+ reference and reference == reference.strip() and reference.startswith(".specs/")
93
+ and "#" in reference and ".." not in reference and "\\" not in reference
94
+ and not Path(reference).is_absolute()
95
+ )
96
+
97
+
98
+ def _validate_generation(generation: Any, expected_number: int) -> dict[str, Any]:
99
+ if not isinstance(generation, dict) or generation.get("generation") != expected_number:
100
+ raise ValueError("non-contiguous audit generations")
101
+ failures = generation.get("failed_remediations")
102
+ status = generation.get("status")
103
+ if type(failures) is not int or failures < 0 or status not in GENERATION_STATUSES:
104
+ raise ValueError("invalid audit generation")
105
+ for field in REMEDIATION_FIELDS:
106
+ if field not in generation:
107
+ if field == "failing_tests":
108
+ generation[field] = []
109
+ elif field == "minimum_failing_tests":
110
+ generation[field] = list(generation.get("failing_tests", []))
111
+ elif field == "failing_signature":
112
+ generation[field] = ""
113
+ elif field == "fixes_tried":
114
+ generation[field] = []
115
+ else:
116
+ generation[field] = 0
117
+ if not isinstance(generation["failing_tests"], list) or any(not isinstance(value, str) for value in generation["failing_tests"]):
118
+ raise ValueError("invalid remediation failing tests")
119
+ if not isinstance(generation["minimum_failing_tests"], list) or any(not isinstance(value, str) for value in generation["minimum_failing_tests"]):
120
+ raise ValueError("invalid remediation minimum failing tests")
121
+ if not isinstance(generation["failing_signature"], str):
122
+ raise ValueError("invalid remediation failing signature")
123
+ for field in ("minimum_failing_count", "consecutive_stalls", "attempt_count"):
124
+ if type(generation[field]) is not int or generation[field] < 0:
125
+ raise ValueError("invalid remediation counters")
126
+ if not isinstance(generation["fixes_tried"], list) or any(not isinstance(value, str) for value in generation["fixes_tried"]):
127
+ raise ValueError("invalid remediation fixes")
128
+ halt_reason = generation.get("halt_reason")
129
+ if halt_reason is not None and halt_reason not in {"stall_threshold_reached", "scoped_gate_unavailable"}:
130
+ raise ValueError("invalid remediation halt reason")
131
+ if status == "halted":
132
+ if halt_reason == "stall_threshold_reached" and generation["consecutive_stalls"] < 1:
133
+ raise ValueError("halted audit generation has no recorded stall")
134
+ if failures < MAX_FAILURES and halt_reason not in {"stall_threshold_reached", "scoped_gate_unavailable"}:
135
+ raise ValueError("halted audit generation has no valid halt reason")
136
+ event = generation.get("halt_event")
137
+ if not isinstance(event, dict) or event != _halt_event(expected_number, failures):
138
+ raise ValueError("invalid halt event")
139
+ elif "halt_event" in generation or halt_reason is not None:
140
+ raise ValueError("halt event on non-halted generation")
141
+ if "authorization_ref" in generation and (
142
+ not isinstance(generation["authorization_ref"], str)
143
+ or not _valid_authorization(generation["authorization_ref"])
144
+ ):
145
+ raise ValueError("invalid authorization reference")
146
+ return generation
147
+
148
+
149
+ def _normalize_entry(key: str, value: Any) -> dict[str, Any]:
150
+ if not isinstance(value, dict) or value.get("fingerprint") != key:
151
+ raise ValueError("invalid convergence entry")
152
+ required = ("requirement", "root_cause", "failure_path")
153
+ if any(not isinstance(value.get(field), str) or not value[field].strip() for field in required):
154
+ raise ValueError("invalid convergence entry")
155
+ normalized = dict(value)
156
+ for field in required:
157
+ normalized[field] = _normalize(normalized[field])
158
+ generations = normalized.get("generations")
159
+ if generations is None:
160
+ generations = [_legacy_generation(normalized)]
161
+ elif not isinstance(generations, list) or not generations:
162
+ raise ValueError("invalid audit generations")
163
+ generations = [_validate_generation(item, index) for index, item in enumerate(generations, 1)]
164
+ current_generation = normalized.get("current_generation", len(generations))
165
+ if current_generation != len(generations):
166
+ raise ValueError("invalid current generation")
167
+ cumulative = sum(item["failed_remediations"] for item in generations)
168
+ if type(normalized.get("failed_remediations")) is not int or normalized.get("failed_remediations") != cumulative:
169
+ raise ValueError("inconsistent cumulative failures")
170
+ if normalized.get("status") != generations[-1]["status"]:
171
+ raise ValueError("inconsistent convergence status")
172
+ if any(item["status"] == "closed" for item in generations[:-1]):
173
+ raise ValueError("closed audit generation cannot be resumed")
174
+ normalized.update({"current_generation": current_generation, "generations": generations})
175
+ return normalized
176
+
177
+
178
+ def _load(path: Path, feature: str) -> dict[str, Any]:
179
+ if not path.exists():
180
+ return {"version": 2, "feature": feature, "fingerprints": {}}
181
+ try:
182
+ payload = json.loads(path.read_text(encoding="utf-8"))
183
+ except (OSError, json.JSONDecodeError) as exc:
184
+ raise ValueError("invalid convergence state") from exc
185
+ if payload.get("version") not in {1, 2} or payload.get("feature") != feature or not isinstance(payload.get("fingerprints"), dict):
186
+ raise ValueError("invalid convergence state")
187
+ return {
188
+ "version": 2,
189
+ "feature": feature,
190
+ "fingerprints": {key: _normalize_entry(key, value) for key, value in payload["fingerprints"].items()},
191
+ }
192
+
193
+
194
+ def _save(path: Path, payload: dict[str, Any]) -> None:
195
+ path.parent.mkdir(parents=True, exist_ok=True)
196
+ fd, temporary = tempfile.mkstemp(prefix=f".{path.name}.", dir=path.parent)
197
+ try:
198
+ with os.fdopen(fd, "w", encoding="utf-8") as handle:
199
+ json.dump(payload, handle, sort_keys=True, separators=(",", ":"))
200
+ handle.write("\n")
201
+ handle.flush()
202
+ os.fsync(handle.fileno())
203
+ os.replace(temporary, path)
204
+ finally:
205
+ try:
206
+ os.unlink(temporary)
207
+ except FileNotFoundError:
208
+ pass
209
+
210
+
211
+ def _entry_key(state: dict[str, Any], requirement: str, root_cause: str, failure_path: str, previous_fingerprint: str | None) -> str:
212
+ key = previous_fingerprint or fingerprint(requirement, root_cause, failure_path)
213
+ current = state["fingerprints"].get(key)
214
+ if previous_fingerprint is not None and current is None:
215
+ raise ValueError("unknown previous fingerprint")
216
+ if current is not None and _normalize(str(current.get("requirement", ""))) != _normalize(requirement):
217
+ raise ValueError("previous fingerprint requirement mismatch")
218
+ if previous_fingerprint is None and any(
219
+ item.get("status") == "halted" and item.get("requirement") == _normalize(requirement)
220
+ for item in state["fingerprints"].values()
221
+ ):
222
+ raise ValueError("halted fingerprint requires authorized resume")
223
+ return key
224
+
225
+
226
+ def record_result(
227
+ root: Path, feature: str, requirement: str, root_cause: str, failure_path: str, *,
228
+ verifier_failed: bool, gate_passed: bool, previous_fingerprint: str | None = None,
229
+ independent: bool = False, evidence_ref: str | None = None,
230
+ failing_tests: list[Any] | tuple[Any, ...] = (),
231
+ fixes_tried: list[Any] | tuple[Any, ...] = (),
232
+ gate_available: bool = True,
233
+ ) -> dict[str, Any]:
234
+ path = state_path(root, feature)
235
+ state = _load(path, feature)
236
+ key = _entry_key(state, requirement, root_cause, failure_path, previous_fingerprint)
237
+ current = state["fingerprints"].get(key)
238
+ if current is None:
239
+ current = {
240
+ "fingerprint": key, "requirement": _normalize(requirement),
241
+ "root_cause": _normalize(root_cause), "failure_path": _normalize(failure_path),
242
+ "failed_remediations": 0, "status": "open", "current_generation": 1,
243
+ "generations": [_generation(1, 0, "open")],
244
+ }
245
+ else:
246
+ current = deepcopy(current)
247
+ if previous_fingerprint is not None and current["status"] == "halted":
248
+ if _normalize(root_cause) != current["root_cause"] or _normalize(failure_path) != current["failure_path"]:
249
+ raise ValueError("halted fingerprint identity cannot be reworded")
250
+ elif previous_fingerprint is not None:
251
+ current.update({"requirement": _normalize(requirement), "root_cause": _normalize(root_cause), "failure_path": _normalize(failure_path)})
252
+ generation = current["generations"][-1]
253
+ prior_status = current["status"]
254
+ remediation_attempt = verifier_failed or bool(failing_tests) or bool(fixes_tried) or not gate_available
255
+ if remediation_attempt and prior_status == "halted":
256
+ raise ValueError("halted fingerprint requires authorized resume")
257
+ transition: dict[str, Any] | None = None
258
+ if remediation_attempt:
259
+ transition = remediation.transition_remediation(
260
+ generation,
261
+ failing_tests,
262
+ stall_attempts=workflow_config.stall_attempts(root),
263
+ gate_available=gate_available,
264
+ fixes_tried=fixes_tried,
265
+ )
266
+ for field in REMEDIATION_FIELDS:
267
+ generation[field] = transition[field]
268
+ if transition["halted"]:
269
+ generation["status"] = "halted"
270
+ generation["halt_reason"] = transition["reason"]
271
+ elif verifier_failed:
272
+ generation["status"] = "open"
273
+ generation.pop("halt_event", None)
274
+ generation.pop("halt_reason", None)
275
+ if verifier_failed:
276
+ generation["failed_remediations"] += 1
277
+ if transition is None or not transition["halted"]:
278
+ generation["status"] = "open"
279
+ generation.pop("halt_event", None)
280
+ generation.pop("halt_reason", None)
281
+ if transition is not None and transition["halted"]:
282
+ generation["halt_event"] = _halt_event(generation["generation"], generation["failed_remediations"])
283
+ elif gate_passed and previous_fingerprint is not None and current["status"] == "open":
284
+ qualifies = independent and bool(evidence_ref) and _valid_authorization(evidence_ref)
285
+ if current["current_generation"] == 1 or qualifies:
286
+ if current["current_generation"] > 1:
287
+ generation["evidence_ref"] = evidence_ref
288
+ generation["status"] = "closed"
289
+ generation.pop("halt_event", None)
290
+ current["failed_remediations"] = sum(item["failed_remediations"] for item in current["generations"])
291
+ current["status"] = current["generations"][-1]["status"]
292
+ state["fingerprints"][key] = current
293
+ _save(path, state)
294
+ return deepcopy(current)
295
+
296
+
297
+ def record_failure(
298
+ root: Path, feature: str, requirement: str, root_cause: str, failure_path: str, *,
299
+ gate_passed: bool = False, previous_fingerprint: str | None = None,
300
+ failing_tests: list[Any] | tuple[Any, ...] = (),
301
+ fixes_tried: list[Any] | tuple[Any, ...] = (),
302
+ gate_available: bool = True,
303
+ ) -> dict[str, Any]:
304
+ return record_result(
305
+ root, feature, requirement, root_cause, failure_path,
306
+ verifier_failed=True, gate_passed=gate_passed,
307
+ previous_fingerprint=previous_fingerprint, failing_tests=failing_tests,
308
+ fixes_tried=fixes_tried, gate_available=gate_available,
309
+ )
310
+
311
+
312
+ def resume(root: Path, feature: str, resume_fingerprint: str, authorization_ref: str) -> dict[str, Any]:
313
+ """Append an authorized generation without mutating the halted history."""
314
+ path = state_path(root, feature)
315
+ state = _load(path, feature)
316
+ if not re.fullmatch(r"[0-9a-f]{64}", resume_fingerprint):
317
+ raise ValueError("invalid resume fingerprint")
318
+ if not _valid_authorization(authorization_ref):
319
+ raise ValueError("invalid authorization reference")
320
+ current = state["fingerprints"].get(resume_fingerprint)
321
+ if current is None:
322
+ raise ValueError("unknown resume fingerprint")
323
+ if current["status"] != "halted" or current["generations"][-1]["status"] != "halted":
324
+ raise ValueError("resume requires halted fingerprint")
325
+ updated = deepcopy(current)
326
+ generation = _generation(current["current_generation"] + 1, 0, "open", authorization_ref=authorization_ref)
327
+ updated["generations"].append(generation)
328
+ updated["current_generation"] = generation["generation"]
329
+ updated["status"] = "open"
330
+ updated["failed_remediations"] = sum(item["failed_remediations"] for item in updated["generations"])
331
+ state["fingerprints"][resume_fingerprint] = updated
332
+ _save(path, state)
333
+ return deepcopy(updated)
334
+
335
+
336
+ def main(argv: list[str] | None = None) -> int:
337
+ parser = argparse.ArgumentParser(description=__doc__)
338
+ parser.add_argument("--root", type=Path, default=Path.cwd())
339
+ parser.add_argument("--feature", required=True)
340
+ parser.add_argument("--requirement")
341
+ parser.add_argument("--root-cause")
342
+ parser.add_argument("--failure-path")
343
+ parser.add_argument("--previous-fingerprint")
344
+ parser.add_argument("--resume-fingerprint")
345
+ parser.add_argument("--authorization-ref")
346
+ parser.add_argument("--verifier-failed", action="store_true")
347
+ parser.add_argument("--gate-passed", action="store_true")
348
+ parser.add_argument("--independent", action="store_true")
349
+ parser.add_argument("--evidence-ref")
350
+ parser.add_argument("--failing-test", action="append", default=[])
351
+ parser.add_argument("--fix-tried", action="append", default=[])
352
+ parser.add_argument("--gate-unavailable", action="store_true")
353
+ args = parser.parse_args(argv)
354
+ if args.resume_fingerprint is not None:
355
+ if args.authorization_ref is None:
356
+ raise SystemExit("--authorization-ref is required with --resume-fingerprint")
357
+ result = resume(args.root, args.feature, args.resume_fingerprint, args.authorization_ref)
358
+ else:
359
+ if any(value is None for value in (args.requirement, args.root_cause, args.failure_path)):
360
+ raise SystemExit("--requirement, --root-cause, and --failure-path are required")
361
+ result = record_result(
362
+ args.root, args.feature, args.requirement, args.root_cause, args.failure_path,
363
+ verifier_failed=args.verifier_failed, gate_passed=args.gate_passed,
364
+ previous_fingerprint=args.previous_fingerprint, independent=args.independent,
365
+ evidence_ref=args.evidence_ref, failing_tests=args.failing_test,
366
+ fixes_tried=args.fix_tried, gate_available=not args.gate_unavailable,
367
+ )
368
+ print(json.dumps(result, sort_keys=True))
369
+ return 0
370
+
371
+
372
+ if __name__ == "__main__":
373
+ raise SystemExit(main())