spec-kitty-cli 0.12.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- spec_kitty_cli-0.12.1.dist-info/METADATA +1767 -0
- spec_kitty_cli-0.12.1.dist-info/RECORD +242 -0
- spec_kitty_cli-0.12.1.dist-info/WHEEL +4 -0
- spec_kitty_cli-0.12.1.dist-info/entry_points.txt +2 -0
- spec_kitty_cli-0.12.1.dist-info/licenses/LICENSE +21 -0
- specify_cli/__init__.py +171 -0
- specify_cli/acceptance.py +627 -0
- specify_cli/agent_utils/README.md +157 -0
- specify_cli/agent_utils/__init__.py +9 -0
- specify_cli/agent_utils/status.py +356 -0
- specify_cli/cli/__init__.py +6 -0
- specify_cli/cli/commands/__init__.py +46 -0
- specify_cli/cli/commands/accept.py +189 -0
- specify_cli/cli/commands/agent/__init__.py +22 -0
- specify_cli/cli/commands/agent/config.py +382 -0
- specify_cli/cli/commands/agent/context.py +191 -0
- specify_cli/cli/commands/agent/feature.py +1057 -0
- specify_cli/cli/commands/agent/release.py +11 -0
- specify_cli/cli/commands/agent/tasks.py +1253 -0
- specify_cli/cli/commands/agent/workflow.py +801 -0
- specify_cli/cli/commands/context.py +246 -0
- specify_cli/cli/commands/dashboard.py +85 -0
- specify_cli/cli/commands/implement.py +973 -0
- specify_cli/cli/commands/init.py +827 -0
- specify_cli/cli/commands/init_help.py +62 -0
- specify_cli/cli/commands/merge.py +755 -0
- specify_cli/cli/commands/mission.py +240 -0
- specify_cli/cli/commands/ops.py +265 -0
- specify_cli/cli/commands/orchestrate.py +640 -0
- specify_cli/cli/commands/repair.py +175 -0
- specify_cli/cli/commands/research.py +165 -0
- specify_cli/cli/commands/sync.py +364 -0
- specify_cli/cli/commands/upgrade.py +249 -0
- specify_cli/cli/commands/validate_encoding.py +186 -0
- specify_cli/cli/commands/validate_tasks.py +186 -0
- specify_cli/cli/commands/verify.py +310 -0
- specify_cli/cli/helpers.py +123 -0
- specify_cli/cli/step_tracker.py +91 -0
- specify_cli/cli/ui.py +192 -0
- specify_cli/core/__init__.py +53 -0
- specify_cli/core/agent_context.py +311 -0
- specify_cli/core/config.py +96 -0
- specify_cli/core/context_validation.py +362 -0
- specify_cli/core/dependency_graph.py +351 -0
- specify_cli/core/git_ops.py +129 -0
- specify_cli/core/multi_parent_merge.py +323 -0
- specify_cli/core/paths.py +260 -0
- specify_cli/core/project_resolver.py +110 -0
- specify_cli/core/stale_detection.py +263 -0
- specify_cli/core/tool_checker.py +79 -0
- specify_cli/core/utils.py +43 -0
- specify_cli/core/vcs/__init__.py +114 -0
- specify_cli/core/vcs/detection.py +341 -0
- specify_cli/core/vcs/exceptions.py +85 -0
- specify_cli/core/vcs/git.py +1304 -0
- specify_cli/core/vcs/jujutsu.py +1208 -0
- specify_cli/core/vcs/protocol.py +285 -0
- specify_cli/core/vcs/types.py +249 -0
- specify_cli/core/version_checker.py +261 -0
- specify_cli/core/worktree.py +506 -0
- specify_cli/dashboard/__init__.py +28 -0
- specify_cli/dashboard/diagnostics.py +204 -0
- specify_cli/dashboard/handlers/__init__.py +17 -0
- specify_cli/dashboard/handlers/api.py +143 -0
- specify_cli/dashboard/handlers/base.py +65 -0
- specify_cli/dashboard/handlers/features.py +390 -0
- specify_cli/dashboard/handlers/router.py +81 -0
- specify_cli/dashboard/handlers/static.py +50 -0
- specify_cli/dashboard/lifecycle.py +541 -0
- specify_cli/dashboard/scanner.py +437 -0
- specify_cli/dashboard/server.py +123 -0
- specify_cli/dashboard/static/dashboard/dashboard.css +722 -0
- specify_cli/dashboard/static/dashboard/dashboard.js +1424 -0
- specify_cli/dashboard/static/spec-kitty.png +0 -0
- specify_cli/dashboard/templates/__init__.py +36 -0
- specify_cli/dashboard/templates/index.html +258 -0
- specify_cli/doc_generators.py +621 -0
- specify_cli/doc_state.py +408 -0
- specify_cli/frontmatter.py +384 -0
- specify_cli/gap_analysis.py +915 -0
- specify_cli/gitignore_manager.py +300 -0
- specify_cli/guards.py +145 -0
- specify_cli/legacy_detector.py +83 -0
- specify_cli/manifest.py +286 -0
- specify_cli/merge/__init__.py +63 -0
- specify_cli/merge/executor.py +653 -0
- specify_cli/merge/forecast.py +215 -0
- specify_cli/merge/ordering.py +126 -0
- specify_cli/merge/preflight.py +230 -0
- specify_cli/merge/state.py +185 -0
- specify_cli/merge/status_resolver.py +354 -0
- specify_cli/mission.py +654 -0
- specify_cli/missions/documentation/command-templates/implement.md +309 -0
- specify_cli/missions/documentation/command-templates/plan.md +275 -0
- specify_cli/missions/documentation/command-templates/review.md +344 -0
- specify_cli/missions/documentation/command-templates/specify.md +206 -0
- specify_cli/missions/documentation/command-templates/tasks.md +189 -0
- specify_cli/missions/documentation/mission.yaml +113 -0
- specify_cli/missions/documentation/templates/divio/explanation-template.md +192 -0
- specify_cli/missions/documentation/templates/divio/howto-template.md +168 -0
- specify_cli/missions/documentation/templates/divio/reference-template.md +179 -0
- specify_cli/missions/documentation/templates/divio/tutorial-template.md +146 -0
- specify_cli/missions/documentation/templates/generators/jsdoc.json.template +18 -0
- specify_cli/missions/documentation/templates/generators/sphinx-conf.py.template +36 -0
- specify_cli/missions/documentation/templates/plan-template.md +269 -0
- specify_cli/missions/documentation/templates/release-template.md +222 -0
- specify_cli/missions/documentation/templates/spec-template.md +172 -0
- specify_cli/missions/documentation/templates/task-prompt-template.md +140 -0
- specify_cli/missions/documentation/templates/tasks-template.md +159 -0
- specify_cli/missions/research/command-templates/merge.md +388 -0
- specify_cli/missions/research/command-templates/plan.md +125 -0
- specify_cli/missions/research/command-templates/review.md +144 -0
- specify_cli/missions/research/command-templates/tasks.md +225 -0
- specify_cli/missions/research/mission.yaml +115 -0
- specify_cli/missions/research/templates/data-model-template.md +33 -0
- specify_cli/missions/research/templates/plan-template.md +161 -0
- specify_cli/missions/research/templates/research/evidence-log.csv +18 -0
- specify_cli/missions/research/templates/research/source-register.csv +18 -0
- specify_cli/missions/research/templates/research-template.md +35 -0
- specify_cli/missions/research/templates/spec-template.md +64 -0
- specify_cli/missions/research/templates/task-prompt-template.md +148 -0
- specify_cli/missions/research/templates/tasks-template.md +114 -0
- specify_cli/missions/software-dev/command-templates/accept.md +75 -0
- specify_cli/missions/software-dev/command-templates/analyze.md +183 -0
- specify_cli/missions/software-dev/command-templates/checklist.md +286 -0
- specify_cli/missions/software-dev/command-templates/clarify.md +157 -0
- specify_cli/missions/software-dev/command-templates/constitution.md +432 -0
- specify_cli/missions/software-dev/command-templates/dashboard.md +101 -0
- specify_cli/missions/software-dev/command-templates/implement.md +41 -0
- specify_cli/missions/software-dev/command-templates/merge.md +383 -0
- specify_cli/missions/software-dev/command-templates/plan.md +171 -0
- specify_cli/missions/software-dev/command-templates/review.md +32 -0
- specify_cli/missions/software-dev/command-templates/specify.md +321 -0
- specify_cli/missions/software-dev/command-templates/tasks.md +566 -0
- specify_cli/missions/software-dev/mission.yaml +100 -0
- specify_cli/missions/software-dev/templates/plan-template.md +132 -0
- specify_cli/missions/software-dev/templates/spec-template.md +116 -0
- specify_cli/missions/software-dev/templates/task-prompt-template.md +140 -0
- specify_cli/missions/software-dev/templates/tasks-template.md +159 -0
- specify_cli/orchestrator/__init__.py +75 -0
- specify_cli/orchestrator/agent_config.py +224 -0
- specify_cli/orchestrator/agents/__init__.py +170 -0
- specify_cli/orchestrator/agents/augment.py +112 -0
- specify_cli/orchestrator/agents/base.py +243 -0
- specify_cli/orchestrator/agents/claude.py +112 -0
- specify_cli/orchestrator/agents/codex.py +106 -0
- specify_cli/orchestrator/agents/copilot.py +137 -0
- specify_cli/orchestrator/agents/cursor.py +139 -0
- specify_cli/orchestrator/agents/gemini.py +115 -0
- specify_cli/orchestrator/agents/kilocode.py +94 -0
- specify_cli/orchestrator/agents/opencode.py +132 -0
- specify_cli/orchestrator/agents/qwen.py +96 -0
- specify_cli/orchestrator/config.py +455 -0
- specify_cli/orchestrator/executor.py +642 -0
- specify_cli/orchestrator/integration.py +1230 -0
- specify_cli/orchestrator/monitor.py +898 -0
- specify_cli/orchestrator/scheduler.py +832 -0
- specify_cli/orchestrator/state.py +508 -0
- specify_cli/orchestrator/testing/__init__.py +122 -0
- specify_cli/orchestrator/testing/availability.py +346 -0
- specify_cli/orchestrator/testing/fixtures.py +684 -0
- specify_cli/orchestrator/testing/paths.py +218 -0
- specify_cli/plan_validation.py +107 -0
- specify_cli/scripts/debug-dashboard-scan.py +61 -0
- specify_cli/scripts/tasks/acceptance_support.py +695 -0
- specify_cli/scripts/tasks/task_helpers.py +506 -0
- specify_cli/scripts/tasks/tasks_cli.py +848 -0
- specify_cli/scripts/validate_encoding.py +180 -0
- specify_cli/task_metadata_validation.py +274 -0
- specify_cli/tasks_support.py +447 -0
- specify_cli/template/__init__.py +47 -0
- specify_cli/template/asset_generator.py +206 -0
- specify_cli/template/github_client.py +334 -0
- specify_cli/template/manager.py +193 -0
- specify_cli/template/renderer.py +99 -0
- specify_cli/templates/AGENTS.md +190 -0
- specify_cli/templates/POWERSHELL_SYNTAX.md +229 -0
- specify_cli/templates/agent-file-template.md +35 -0
- specify_cli/templates/checklist-template.md +42 -0
- specify_cli/templates/claudeignore-template +58 -0
- specify_cli/templates/command-templates/accept.md +141 -0
- specify_cli/templates/command-templates/analyze.md +253 -0
- specify_cli/templates/command-templates/checklist.md +352 -0
- specify_cli/templates/command-templates/clarify.md +224 -0
- specify_cli/templates/command-templates/constitution.md +432 -0
- specify_cli/templates/command-templates/dashboard.md +175 -0
- specify_cli/templates/command-templates/implement.md +190 -0
- specify_cli/templates/command-templates/merge.md +374 -0
- specify_cli/templates/command-templates/plan.md +171 -0
- specify_cli/templates/command-templates/research.md +88 -0
- specify_cli/templates/command-templates/review.md +510 -0
- specify_cli/templates/command-templates/specify.md +321 -0
- specify_cli/templates/command-templates/status.md +92 -0
- specify_cli/templates/command-templates/tasks.md +199 -0
- specify_cli/templates/git-hooks/pre-commit +22 -0
- specify_cli/templates/git-hooks/pre-commit-agent-check +37 -0
- specify_cli/templates/git-hooks/pre-commit-encoding-check +142 -0
- specify_cli/templates/plan-template.md +108 -0
- specify_cli/templates/spec-template.md +118 -0
- specify_cli/templates/task-prompt-template.md +165 -0
- specify_cli/templates/tasks-template.md +161 -0
- specify_cli/templates/vscode-settings.json +13 -0
- specify_cli/text_sanitization.py +225 -0
- specify_cli/upgrade/__init__.py +18 -0
- specify_cli/upgrade/detector.py +239 -0
- specify_cli/upgrade/metadata.py +182 -0
- specify_cli/upgrade/migrations/__init__.py +65 -0
- specify_cli/upgrade/migrations/base.py +80 -0
- specify_cli/upgrade/migrations/m_0_10_0_python_only.py +359 -0
- specify_cli/upgrade/migrations/m_0_10_12_constitution_cleanup.py +99 -0
- specify_cli/upgrade/migrations/m_0_10_14_update_implement_slash_command.py +176 -0
- specify_cli/upgrade/migrations/m_0_10_1_populate_slash_commands.py +174 -0
- specify_cli/upgrade/migrations/m_0_10_2_update_slash_commands.py +172 -0
- specify_cli/upgrade/migrations/m_0_10_6_workflow_simplification.py +174 -0
- specify_cli/upgrade/migrations/m_0_10_8_fix_memory_structure.py +252 -0
- specify_cli/upgrade/migrations/m_0_10_9_repair_templates.py +168 -0
- specify_cli/upgrade/migrations/m_0_11_0_workspace_per_wp.py +182 -0
- specify_cli/upgrade/migrations/m_0_11_1_improved_workflow_templates.py +173 -0
- specify_cli/upgrade/migrations/m_0_11_1_update_implement_slash_command.py +160 -0
- specify_cli/upgrade/migrations/m_0_11_2_improved_workflow_templates.py +173 -0
- specify_cli/upgrade/migrations/m_0_11_3_workflow_agent_flag.py +114 -0
- specify_cli/upgrade/migrations/m_0_12_0_documentation_mission.py +155 -0
- specify_cli/upgrade/migrations/m_0_12_1_remove_kitty_specs_from_gitignore.py +183 -0
- specify_cli/upgrade/migrations/m_0_2_0_specify_to_kittify.py +80 -0
- specify_cli/upgrade/migrations/m_0_4_8_gitignore_agents.py +118 -0
- specify_cli/upgrade/migrations/m_0_5_0_encoding_hooks.py +141 -0
- specify_cli/upgrade/migrations/m_0_6_5_commands_rename.py +169 -0
- specify_cli/upgrade/migrations/m_0_6_7_ensure_missions.py +228 -0
- specify_cli/upgrade/migrations/m_0_7_2_worktree_commands_dedup.py +89 -0
- specify_cli/upgrade/migrations/m_0_7_3_update_scripts.py +114 -0
- specify_cli/upgrade/migrations/m_0_8_0_remove_active_mission.py +82 -0
- specify_cli/upgrade/migrations/m_0_8_0_worktree_agents_symlink.py +148 -0
- specify_cli/upgrade/migrations/m_0_9_0_frontmatter_only_lanes.py +346 -0
- specify_cli/upgrade/migrations/m_0_9_1_complete_lane_migration.py +656 -0
- specify_cli/upgrade/migrations/m_0_9_2_research_mission_templates.py +221 -0
- specify_cli/upgrade/registry.py +121 -0
- specify_cli/upgrade/runner.py +284 -0
- specify_cli/validators/__init__.py +14 -0
- specify_cli/validators/paths.py +154 -0
- specify_cli/validators/research.py +428 -0
- specify_cli/verify_enhanced.py +270 -0
- specify_cli/workspace_context.py +224 -0
|
@@ -0,0 +1,627 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Acceptance workflow utilities for Spec Kitty features."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import re
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from datetime import datetime, timezone
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Dict, Iterable, List, Mapping, MutableMapping, Optional, Sequence, Tuple
|
|
13
|
+
|
|
14
|
+
from .tasks_support import (
|
|
15
|
+
LANES,
|
|
16
|
+
TaskCliError,
|
|
17
|
+
WorkPackage,
|
|
18
|
+
activity_entries,
|
|
19
|
+
extract_scalar,
|
|
20
|
+
find_repo_root,
|
|
21
|
+
get_lane_from_frontmatter,
|
|
22
|
+
git_status_lines,
|
|
23
|
+
is_legacy_format,
|
|
24
|
+
run_git,
|
|
25
|
+
split_frontmatter,
|
|
26
|
+
)
|
|
27
|
+
from specify_cli.mission import MissionError, get_mission_for_feature
|
|
28
|
+
from specify_cli.validators.paths import PathValidationError, validate_mission_paths
|
|
29
|
+
|
|
30
|
+
AcceptanceMode = str # Expected values: "pr", "local", "checklist"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class AcceptanceError(TaskCliError):
|
|
34
|
+
"""Raised when acceptance cannot complete due to outstanding issues."""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class WorkPackageState:
|
|
39
|
+
work_package_id: str
|
|
40
|
+
lane: str
|
|
41
|
+
title: str
|
|
42
|
+
path: str
|
|
43
|
+
has_lane_entry: bool
|
|
44
|
+
latest_lane: Optional[str]
|
|
45
|
+
metadata: Dict[str, Optional[str]] = field(default_factory=dict)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass
|
|
49
|
+
class AcceptanceSummary:
|
|
50
|
+
feature: str
|
|
51
|
+
repo_root: Path
|
|
52
|
+
feature_dir: Path
|
|
53
|
+
tasks_dir: Path
|
|
54
|
+
branch: Optional[str]
|
|
55
|
+
worktree_root: Path
|
|
56
|
+
primary_repo_root: Path
|
|
57
|
+
lanes: Dict[str, List[str]]
|
|
58
|
+
work_packages: List[WorkPackageState]
|
|
59
|
+
metadata_issues: List[str]
|
|
60
|
+
activity_issues: List[str]
|
|
61
|
+
unchecked_tasks: List[str]
|
|
62
|
+
needs_clarification: List[str]
|
|
63
|
+
missing_artifacts: List[str]
|
|
64
|
+
optional_missing: List[str]
|
|
65
|
+
git_dirty: List[str]
|
|
66
|
+
path_violations: List[str]
|
|
67
|
+
warnings: List[str]
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def all_done(self) -> bool:
|
|
71
|
+
return not (
|
|
72
|
+
self.lanes.get("planned")
|
|
73
|
+
or self.lanes.get("doing")
|
|
74
|
+
or self.lanes.get("for_review")
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def ok(self) -> bool:
|
|
79
|
+
return (
|
|
80
|
+
self.all_done
|
|
81
|
+
and not self.metadata_issues
|
|
82
|
+
and not self.activity_issues
|
|
83
|
+
and not self.unchecked_tasks
|
|
84
|
+
and not self.needs_clarification
|
|
85
|
+
and not self.missing_artifacts
|
|
86
|
+
and not self.git_dirty
|
|
87
|
+
and not self.path_violations
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
def outstanding(self) -> Dict[str, List[str]]:
|
|
91
|
+
buckets = {
|
|
92
|
+
"not_done": [
|
|
93
|
+
*self.lanes.get("planned", []),
|
|
94
|
+
*self.lanes.get("doing", []),
|
|
95
|
+
*self.lanes.get("for_review", []),
|
|
96
|
+
],
|
|
97
|
+
"metadata": self.metadata_issues,
|
|
98
|
+
"activity": self.activity_issues,
|
|
99
|
+
"unchecked_tasks": self.unchecked_tasks,
|
|
100
|
+
"needs_clarification": self.needs_clarification,
|
|
101
|
+
"missing_artifacts": self.missing_artifacts,
|
|
102
|
+
"git_dirty": self.git_dirty,
|
|
103
|
+
"path_violations": self.path_violations,
|
|
104
|
+
}
|
|
105
|
+
return {key: value for key, value in buckets.items() if value}
|
|
106
|
+
|
|
107
|
+
def to_dict(self) -> Dict[str, object]:
|
|
108
|
+
return {
|
|
109
|
+
"feature": self.feature,
|
|
110
|
+
"branch": self.branch,
|
|
111
|
+
"repo_root": str(self.repo_root),
|
|
112
|
+
"feature_dir": str(self.feature_dir),
|
|
113
|
+
"tasks_dir": str(self.tasks_dir),
|
|
114
|
+
"worktree_root": str(self.worktree_root),
|
|
115
|
+
"primary_repo_root": str(self.primary_repo_root),
|
|
116
|
+
"lanes": self.lanes,
|
|
117
|
+
"work_packages": [
|
|
118
|
+
{
|
|
119
|
+
"id": wp.work_package_id,
|
|
120
|
+
"lane": wp.lane,
|
|
121
|
+
"title": wp.title,
|
|
122
|
+
"path": wp.path,
|
|
123
|
+
"latest_lane": wp.latest_lane,
|
|
124
|
+
"has_lane_entry": wp.has_lane_entry,
|
|
125
|
+
"metadata": wp.metadata,
|
|
126
|
+
}
|
|
127
|
+
for wp in self.work_packages
|
|
128
|
+
],
|
|
129
|
+
"metadata_issues": self.metadata_issues,
|
|
130
|
+
"activity_issues": self.activity_issues,
|
|
131
|
+
"unchecked_tasks": self.unchecked_tasks,
|
|
132
|
+
"needs_clarification": self.needs_clarification,
|
|
133
|
+
"missing_artifacts": self.missing_artifacts,
|
|
134
|
+
"optional_missing": self.optional_missing,
|
|
135
|
+
"git_dirty": self.git_dirty,
|
|
136
|
+
"path_violations": self.path_violations,
|
|
137
|
+
"warnings": self.warnings,
|
|
138
|
+
"all_done": self.all_done,
|
|
139
|
+
"ok": self.ok,
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
@dataclass
|
|
144
|
+
class AcceptanceResult:
|
|
145
|
+
summary: AcceptanceSummary
|
|
146
|
+
mode: AcceptanceMode
|
|
147
|
+
accepted_at: str
|
|
148
|
+
accepted_by: str
|
|
149
|
+
parent_commit: Optional[str]
|
|
150
|
+
accept_commit: Optional[str]
|
|
151
|
+
commit_created: bool
|
|
152
|
+
instructions: List[str]
|
|
153
|
+
cleanup_instructions: List[str]
|
|
154
|
+
notes: List[str] = field(default_factory=list)
|
|
155
|
+
|
|
156
|
+
def to_dict(self) -> Dict[str, object]:
|
|
157
|
+
return {
|
|
158
|
+
"accepted_at": self.accepted_at,
|
|
159
|
+
"accepted_by": self.accepted_by,
|
|
160
|
+
"mode": self.mode,
|
|
161
|
+
"parent_commit": self.parent_commit,
|
|
162
|
+
"accept_commit": self.accept_commit,
|
|
163
|
+
"commit_created": self.commit_created,
|
|
164
|
+
"instructions": self.instructions,
|
|
165
|
+
"cleanup_instructions": self.cleanup_instructions,
|
|
166
|
+
"notes": self.notes,
|
|
167
|
+
"summary": self.summary.to_dict(),
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _iter_work_packages(repo_root: Path, feature: str) -> Iterable[WorkPackage]:
|
|
172
|
+
"""Iterate over work packages, supporting both legacy and new formats.
|
|
173
|
+
|
|
174
|
+
Legacy format: WP files in tasks/{lane}/ subdirectories
|
|
175
|
+
New format: WP files in flat tasks/ directory with lane in frontmatter
|
|
176
|
+
"""
|
|
177
|
+
feature_path = repo_root / "kitty-specs" / feature
|
|
178
|
+
tasks_dir = feature_path / "tasks"
|
|
179
|
+
if not tasks_dir.exists():
|
|
180
|
+
raise AcceptanceError(f"Feature '{feature}' has no tasks directory at {tasks_dir}.")
|
|
181
|
+
|
|
182
|
+
use_legacy = is_legacy_format(feature_path)
|
|
183
|
+
|
|
184
|
+
if use_legacy:
|
|
185
|
+
# Legacy format: iterate over lane subdirectories
|
|
186
|
+
for lane_dir in sorted(tasks_dir.iterdir()):
|
|
187
|
+
if not lane_dir.is_dir():
|
|
188
|
+
continue
|
|
189
|
+
lane = lane_dir.name
|
|
190
|
+
if lane not in LANES:
|
|
191
|
+
continue
|
|
192
|
+
for path in sorted(lane_dir.rglob("*.md")):
|
|
193
|
+
text = path.read_text(encoding="utf-8-sig")
|
|
194
|
+
front, body, padding = split_frontmatter(text)
|
|
195
|
+
relative = path.relative_to(lane_dir)
|
|
196
|
+
yield WorkPackage(
|
|
197
|
+
feature=feature,
|
|
198
|
+
path=path,
|
|
199
|
+
current_lane=lane,
|
|
200
|
+
relative_subpath=relative,
|
|
201
|
+
frontmatter=front,
|
|
202
|
+
body=body,
|
|
203
|
+
padding=padding,
|
|
204
|
+
)
|
|
205
|
+
else:
|
|
206
|
+
# New format: flat tasks/ directory, lane from frontmatter
|
|
207
|
+
for path in sorted(tasks_dir.glob("*.md")):
|
|
208
|
+
if path.name.lower() == "readme.md":
|
|
209
|
+
continue
|
|
210
|
+
text = path.read_text(encoding="utf-8-sig")
|
|
211
|
+
front, body, padding = split_frontmatter(text)
|
|
212
|
+
# Get lane from frontmatter
|
|
213
|
+
lane = get_lane_from_frontmatter(path, warn_on_missing=False)
|
|
214
|
+
relative = path.relative_to(tasks_dir)
|
|
215
|
+
yield WorkPackage(
|
|
216
|
+
feature=feature,
|
|
217
|
+
path=path,
|
|
218
|
+
current_lane=lane,
|
|
219
|
+
relative_subpath=relative,
|
|
220
|
+
frontmatter=front,
|
|
221
|
+
body=body,
|
|
222
|
+
padding=padding,
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def detect_feature_slug(
|
|
227
|
+
repo_root: Path,
|
|
228
|
+
*,
|
|
229
|
+
env: Optional[Mapping[str, str]] = None,
|
|
230
|
+
cwd: Optional[Path] = None,
|
|
231
|
+
) -> str:
|
|
232
|
+
env = env or os.environ
|
|
233
|
+
if "SPECIFY_FEATURE" in env and env["SPECIFY_FEATURE"].strip():
|
|
234
|
+
return env["SPECIFY_FEATURE"].strip()
|
|
235
|
+
|
|
236
|
+
try:
|
|
237
|
+
branch = (
|
|
238
|
+
run_git(["rev-parse", "--abbrev-ref", "HEAD"], cwd=repo_root, check=True)
|
|
239
|
+
.stdout.strip()
|
|
240
|
+
)
|
|
241
|
+
if branch and branch != "HEAD" and re.match(r"^\d{3}-", branch):
|
|
242
|
+
return branch
|
|
243
|
+
except TaskCliError:
|
|
244
|
+
pass
|
|
245
|
+
|
|
246
|
+
cwd = (cwd or Path.cwd()).resolve()
|
|
247
|
+
for parent in [cwd, *cwd.parents]:
|
|
248
|
+
if parent.name.startswith(".worktrees"):
|
|
249
|
+
parts = list(parent.parts)
|
|
250
|
+
try:
|
|
251
|
+
idx = parts.index(".worktrees")
|
|
252
|
+
candidate = parts[idx + 1]
|
|
253
|
+
if re.match(r"^\d{3}-", candidate):
|
|
254
|
+
return candidate
|
|
255
|
+
except (ValueError, IndexError):
|
|
256
|
+
continue
|
|
257
|
+
if parent.name.startswith("0") and re.match(r"^\d{3}-", parent.name):
|
|
258
|
+
return parent.name
|
|
259
|
+
|
|
260
|
+
raise AcceptanceError(
|
|
261
|
+
"Unable to determine feature slug automatically. Provide --feature explicitly."
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _read_file(path: Path) -> str:
|
|
266
|
+
return path.read_text(encoding="utf-8-sig") if path.exists() else ""
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _find_unchecked_tasks(tasks_file: Path) -> List[str]:
|
|
270
|
+
if not tasks_file.exists():
|
|
271
|
+
return ["tasks.md missing"]
|
|
272
|
+
|
|
273
|
+
unchecked: List[str] = []
|
|
274
|
+
for line in tasks_file.read_text(encoding="utf-8-sig").splitlines():
|
|
275
|
+
if re.match(r"^\s*-\s*\[ \]", line):
|
|
276
|
+
unchecked.append(line.strip())
|
|
277
|
+
return unchecked
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _check_needs_clarification(files: Sequence[Path]) -> List[str]:
|
|
281
|
+
results: List[str] = []
|
|
282
|
+
for file_path in files:
|
|
283
|
+
if file_path.exists():
|
|
284
|
+
text = file_path.read_text(encoding="utf-8-sig")
|
|
285
|
+
if "[NEEDS CLARIFICATION" in text:
|
|
286
|
+
results.append(str(file_path))
|
|
287
|
+
return results
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _missing_artifacts(feature_dir: Path) -> Tuple[List[str], List[str]]:
|
|
291
|
+
required = [feature_dir / "spec.md", feature_dir / "plan.md", feature_dir / "tasks.md"]
|
|
292
|
+
optional = [
|
|
293
|
+
feature_dir / "quickstart.md",
|
|
294
|
+
feature_dir / "data-model.md",
|
|
295
|
+
feature_dir / "research.md",
|
|
296
|
+
feature_dir / "contracts",
|
|
297
|
+
]
|
|
298
|
+
missing_required = [str(p.relative_to(feature_dir)) for p in required if not p.exists()]
|
|
299
|
+
missing_optional = [str(p.relative_to(feature_dir)) for p in optional if not p.exists()]
|
|
300
|
+
return missing_required, missing_optional
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def collect_feature_summary(
|
|
304
|
+
repo_root: Path,
|
|
305
|
+
feature: str,
|
|
306
|
+
*,
|
|
307
|
+
strict_metadata: bool = True,
|
|
308
|
+
) -> AcceptanceSummary:
|
|
309
|
+
feature_dir = repo_root / "kitty-specs" / feature
|
|
310
|
+
tasks_dir = feature_dir / "tasks"
|
|
311
|
+
if not feature_dir.exists():
|
|
312
|
+
raise AcceptanceError(f"Feature directory not found: {feature_dir}")
|
|
313
|
+
|
|
314
|
+
branch: Optional[str] = None
|
|
315
|
+
try:
|
|
316
|
+
branch_value = (
|
|
317
|
+
run_git(["rev-parse", "--abbrev-ref", "HEAD"], cwd=repo_root, check=True)
|
|
318
|
+
.stdout.strip()
|
|
319
|
+
)
|
|
320
|
+
if branch_value and branch_value != "HEAD":
|
|
321
|
+
branch = branch_value
|
|
322
|
+
except TaskCliError:
|
|
323
|
+
branch = None
|
|
324
|
+
|
|
325
|
+
try:
|
|
326
|
+
worktree_root = Path(
|
|
327
|
+
run_git(["rev-parse", "--show-toplevel"], cwd=repo_root, check=True)
|
|
328
|
+
.stdout.strip()
|
|
329
|
+
).resolve()
|
|
330
|
+
except TaskCliError:
|
|
331
|
+
worktree_root = repo_root
|
|
332
|
+
|
|
333
|
+
try:
|
|
334
|
+
git_common_dir = Path(
|
|
335
|
+
run_git(["rev-parse", "--git-common-dir"], cwd=repo_root, check=True)
|
|
336
|
+
.stdout.strip()
|
|
337
|
+
).resolve()
|
|
338
|
+
primary_repo_root = git_common_dir.parent
|
|
339
|
+
except TaskCliError:
|
|
340
|
+
primary_repo_root = repo_root
|
|
341
|
+
|
|
342
|
+
lanes: Dict[str, List[str]] = {lane: [] for lane in LANES}
|
|
343
|
+
work_packages: List[WorkPackageState] = []
|
|
344
|
+
metadata_issues: List[str] = []
|
|
345
|
+
activity_issues: List[str] = []
|
|
346
|
+
|
|
347
|
+
use_legacy = is_legacy_format(feature_dir)
|
|
348
|
+
|
|
349
|
+
for wp in _iter_work_packages(repo_root, feature):
|
|
350
|
+
wp_id = wp.work_package_id or wp.path.stem
|
|
351
|
+
title = (wp.title or "").strip('"')
|
|
352
|
+
lanes[wp.current_lane].append(wp_id)
|
|
353
|
+
|
|
354
|
+
entries = activity_entries(wp.body)
|
|
355
|
+
lanes_logged = {entry["lane"] for entry in entries}
|
|
356
|
+
latest_lane = entries[-1]["lane"] if entries else None
|
|
357
|
+
has_lane_entry = wp.current_lane in lanes_logged
|
|
358
|
+
|
|
359
|
+
metadata: Dict[str, Optional[str]] = {
|
|
360
|
+
"lane": wp.lane,
|
|
361
|
+
"agent": wp.agent,
|
|
362
|
+
"assignee": wp.assignee,
|
|
363
|
+
"shell_pid": wp.shell_pid,
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if strict_metadata:
|
|
367
|
+
lane_value = (wp.lane or "").strip()
|
|
368
|
+
if not lane_value:
|
|
369
|
+
metadata_issues.append(f"{wp_id}: missing lane in frontmatter")
|
|
370
|
+
elif use_legacy and lane_value != wp.current_lane:
|
|
371
|
+
# Only check directory/frontmatter mismatch in legacy format
|
|
372
|
+
metadata_issues.append(
|
|
373
|
+
f"{wp_id}: frontmatter lane '{lane_value}' does not match directory '{wp.current_lane}'"
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
if not wp.agent:
|
|
377
|
+
metadata_issues.append(f"{wp_id}: missing agent in frontmatter")
|
|
378
|
+
if wp.current_lane in {"doing", "for_review", "done"} and not wp.assignee:
|
|
379
|
+
metadata_issues.append(f"{wp_id}: missing assignee in frontmatter")
|
|
380
|
+
if not wp.shell_pid:
|
|
381
|
+
metadata_issues.append(f"{wp_id}: missing shell_pid in frontmatter")
|
|
382
|
+
|
|
383
|
+
if not entries:
|
|
384
|
+
activity_issues.append(f"{wp_id}: Activity Log missing entries")
|
|
385
|
+
else:
|
|
386
|
+
if wp.current_lane not in lanes_logged:
|
|
387
|
+
activity_issues.append(
|
|
388
|
+
f"{wp_id}: Activity Log missing entry for lane={wp.current_lane}"
|
|
389
|
+
)
|
|
390
|
+
if wp.current_lane == "done" and entries[-1]["lane"] != "done":
|
|
391
|
+
activity_issues.append(f"{wp_id}: latest Activity Log entry not lane=done")
|
|
392
|
+
|
|
393
|
+
work_packages.append(
|
|
394
|
+
WorkPackageState(
|
|
395
|
+
work_package_id=wp_id,
|
|
396
|
+
lane=wp.current_lane,
|
|
397
|
+
title=title,
|
|
398
|
+
path=str(wp.path.relative_to(repo_root)),
|
|
399
|
+
has_lane_entry=has_lane_entry,
|
|
400
|
+
latest_lane=latest_lane,
|
|
401
|
+
metadata=metadata,
|
|
402
|
+
)
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
unchecked_tasks = _find_unchecked_tasks(feature_dir / "tasks.md")
|
|
406
|
+
needs_clarification = _check_needs_clarification(
|
|
407
|
+
[
|
|
408
|
+
feature_dir / "spec.md",
|
|
409
|
+
feature_dir / "plan.md",
|
|
410
|
+
feature_dir / "quickstart.md",
|
|
411
|
+
feature_dir / "tasks.md",
|
|
412
|
+
feature_dir / "research.md",
|
|
413
|
+
feature_dir / "data-model.md",
|
|
414
|
+
]
|
|
415
|
+
)
|
|
416
|
+
missing_required, missing_optional = _missing_artifacts(feature_dir)
|
|
417
|
+
|
|
418
|
+
try:
|
|
419
|
+
git_dirty = git_status_lines(repo_root)
|
|
420
|
+
except TaskCliError:
|
|
421
|
+
git_dirty = []
|
|
422
|
+
|
|
423
|
+
path_violations: List[str] = []
|
|
424
|
+
try:
|
|
425
|
+
mission = get_mission_for_feature(feature_dir)
|
|
426
|
+
except MissionError:
|
|
427
|
+
mission = None
|
|
428
|
+
|
|
429
|
+
if mission and mission.config.paths:
|
|
430
|
+
try:
|
|
431
|
+
validate_mission_paths(mission, repo_root, strict=True)
|
|
432
|
+
except PathValidationError as exc:
|
|
433
|
+
message = exc.result.format_errors() or str(exc)
|
|
434
|
+
path_violations.append(message)
|
|
435
|
+
|
|
436
|
+
warnings: List[str] = []
|
|
437
|
+
if missing_optional:
|
|
438
|
+
warnings.append(
|
|
439
|
+
"Optional artifacts missing: " + ", ".join(missing_optional)
|
|
440
|
+
)
|
|
441
|
+
if path_violations:
|
|
442
|
+
warnings.append("Path conventions not satisfied.")
|
|
443
|
+
|
|
444
|
+
return AcceptanceSummary(
|
|
445
|
+
feature=feature,
|
|
446
|
+
repo_root=repo_root,
|
|
447
|
+
feature_dir=feature_dir,
|
|
448
|
+
tasks_dir=tasks_dir,
|
|
449
|
+
branch=branch,
|
|
450
|
+
worktree_root=worktree_root,
|
|
451
|
+
primary_repo_root=primary_repo_root,
|
|
452
|
+
lanes=lanes,
|
|
453
|
+
work_packages=work_packages,
|
|
454
|
+
metadata_issues=metadata_issues,
|
|
455
|
+
activity_issues=activity_issues,
|
|
456
|
+
unchecked_tasks=unchecked_tasks if unchecked_tasks != ["tasks.md missing"] else [],
|
|
457
|
+
needs_clarification=needs_clarification,
|
|
458
|
+
missing_artifacts=missing_required,
|
|
459
|
+
optional_missing=missing_optional,
|
|
460
|
+
git_dirty=git_dirty,
|
|
461
|
+
path_violations=path_violations,
|
|
462
|
+
warnings=warnings,
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
def choose_mode(preference: Optional[str], repo_root: Path) -> AcceptanceMode:
|
|
467
|
+
if preference in {"pr", "local", "checklist"}:
|
|
468
|
+
return preference
|
|
469
|
+
try:
|
|
470
|
+
remotes = (
|
|
471
|
+
run_git(["remote"], cwd=repo_root, check=False).stdout.strip().splitlines()
|
|
472
|
+
)
|
|
473
|
+
if remotes:
|
|
474
|
+
return "pr"
|
|
475
|
+
except TaskCliError:
|
|
476
|
+
pass
|
|
477
|
+
return "local"
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
def perform_acceptance(
|
|
481
|
+
summary: AcceptanceSummary,
|
|
482
|
+
*,
|
|
483
|
+
mode: AcceptanceMode,
|
|
484
|
+
actor: Optional[str],
|
|
485
|
+
tests: Optional[Sequence[str]] = None,
|
|
486
|
+
auto_commit: bool = True,
|
|
487
|
+
) -> AcceptanceResult:
|
|
488
|
+
if mode != "checklist" and not summary.ok:
|
|
489
|
+
raise AcceptanceError(
|
|
490
|
+
"Acceptance checks failed; run verify to see outstanding issues."
|
|
491
|
+
)
|
|
492
|
+
|
|
493
|
+
actor_name = (actor or os.getenv("USER") or os.getenv("USERNAME") or "system").strip()
|
|
494
|
+
timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
495
|
+
|
|
496
|
+
parent_commit: Optional[str] = None
|
|
497
|
+
accept_commit: Optional[str] = None
|
|
498
|
+
|
|
499
|
+
if auto_commit and mode != "checklist":
|
|
500
|
+
try:
|
|
501
|
+
parent_commit = (
|
|
502
|
+
run_git(["rev-parse", "HEAD"], cwd=summary.repo_root, check=False)
|
|
503
|
+
.stdout.strip()
|
|
504
|
+
or None
|
|
505
|
+
)
|
|
506
|
+
except TaskCliError:
|
|
507
|
+
parent_commit = None
|
|
508
|
+
|
|
509
|
+
meta_path = summary.feature_dir / "meta.json"
|
|
510
|
+
if meta_path.exists():
|
|
511
|
+
meta = json.loads(meta_path.read_text(encoding="utf-8-sig"))
|
|
512
|
+
else:
|
|
513
|
+
meta = {}
|
|
514
|
+
|
|
515
|
+
acceptance_record: Dict[str, object] = {
|
|
516
|
+
"accepted_at": timestamp,
|
|
517
|
+
"accepted_by": actor_name,
|
|
518
|
+
"mode": mode,
|
|
519
|
+
"branch": summary.branch,
|
|
520
|
+
"accepted_from_commit": parent_commit,
|
|
521
|
+
}
|
|
522
|
+
if tests:
|
|
523
|
+
acceptance_record["validation_commands"] = list(tests)
|
|
524
|
+
|
|
525
|
+
meta["accepted_at"] = timestamp
|
|
526
|
+
meta["accepted_by"] = actor_name
|
|
527
|
+
meta["acceptance_mode"] = mode
|
|
528
|
+
meta["accepted_from_commit"] = parent_commit
|
|
529
|
+
meta["accept_commit"] = None
|
|
530
|
+
|
|
531
|
+
history: List[Dict[str, object]] = meta.setdefault("acceptance_history", [])
|
|
532
|
+
history.append(acceptance_record)
|
|
533
|
+
# limit history to last 20 entries
|
|
534
|
+
if len(history) > 20:
|
|
535
|
+
meta["acceptance_history"] = history[-20:]
|
|
536
|
+
|
|
537
|
+
meta_path.write_text(json.dumps(meta, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
|
538
|
+
run_git(
|
|
539
|
+
["add", str(meta_path.relative_to(summary.repo_root))],
|
|
540
|
+
cwd=summary.repo_root,
|
|
541
|
+
check=True,
|
|
542
|
+
)
|
|
543
|
+
|
|
544
|
+
status = run_git(["diff", "--cached", "--name-only"], cwd=summary.repo_root, check=True)
|
|
545
|
+
staged_files = [line.strip() for line in status.stdout.splitlines() if line.strip()]
|
|
546
|
+
commit_created = False
|
|
547
|
+
if staged_files:
|
|
548
|
+
commit_msg = f"Accept {summary.feature}"
|
|
549
|
+
run_git(["commit", "-m", commit_msg], cwd=summary.repo_root, check=True)
|
|
550
|
+
commit_created = True
|
|
551
|
+
try:
|
|
552
|
+
accept_commit = (
|
|
553
|
+
run_git(["rev-parse", "HEAD"], cwd=summary.repo_root, check=True)
|
|
554
|
+
.stdout.strip()
|
|
555
|
+
)
|
|
556
|
+
except TaskCliError:
|
|
557
|
+
accept_commit = None
|
|
558
|
+
else:
|
|
559
|
+
commit_created = False
|
|
560
|
+
else:
|
|
561
|
+
commit_created = False
|
|
562
|
+
|
|
563
|
+
instructions: List[str] = []
|
|
564
|
+
cleanup_instructions: List[str] = []
|
|
565
|
+
|
|
566
|
+
branch = summary.branch or summary.feature
|
|
567
|
+
if mode == "pr":
|
|
568
|
+
instructions.extend(
|
|
569
|
+
[
|
|
570
|
+
f"Review the acceptance commit on branch `{branch}`.",
|
|
571
|
+
f"Push your branch: `git push origin {branch}`",
|
|
572
|
+
"Open a pull request referencing spec/plan/tasks artifacts.",
|
|
573
|
+
"Include acceptance summary and test evidence in the PR description.",
|
|
574
|
+
]
|
|
575
|
+
)
|
|
576
|
+
elif mode == "local":
|
|
577
|
+
instructions.extend(
|
|
578
|
+
[
|
|
579
|
+
"Switch to your integration branch (e.g., `git checkout main`).",
|
|
580
|
+
"Synchronize it (e.g., `git pull --ff-only`).",
|
|
581
|
+
f"Merge the feature: `git merge {branch}`",
|
|
582
|
+
]
|
|
583
|
+
)
|
|
584
|
+
else: # checklist
|
|
585
|
+
instructions.append(
|
|
586
|
+
"All checks passed. Proceed with your manual acceptance workflow."
|
|
587
|
+
)
|
|
588
|
+
|
|
589
|
+
if summary.worktree_root != summary.primary_repo_root:
|
|
590
|
+
cleanup_instructions.append(
|
|
591
|
+
f"After merging, remove the worktree: `git worktree remove {summary.worktree_root}`"
|
|
592
|
+
)
|
|
593
|
+
cleanup_instructions.append(f"Delete the feature branch when done: `git branch -d {branch}`")
|
|
594
|
+
|
|
595
|
+
notes: List[str] = []
|
|
596
|
+
if accept_commit:
|
|
597
|
+
notes.append(f"Acceptance commit: {accept_commit}")
|
|
598
|
+
if parent_commit:
|
|
599
|
+
notes.append(f"Accepted from parent commit: {parent_commit}")
|
|
600
|
+
if tests:
|
|
601
|
+
notes.append("Validation commands:")
|
|
602
|
+
notes.extend(f" - {cmd}" for cmd in tests)
|
|
603
|
+
|
|
604
|
+
return AcceptanceResult(
|
|
605
|
+
summary=summary,
|
|
606
|
+
mode=mode,
|
|
607
|
+
accepted_at=timestamp,
|
|
608
|
+
accepted_by=actor_name,
|
|
609
|
+
parent_commit=parent_commit,
|
|
610
|
+
accept_commit=accept_commit,
|
|
611
|
+
commit_created=commit_created,
|
|
612
|
+
instructions=instructions,
|
|
613
|
+
cleanup_instructions=cleanup_instructions,
|
|
614
|
+
notes=notes,
|
|
615
|
+
)
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
__all__ = [
|
|
619
|
+
"AcceptanceError",
|
|
620
|
+
"AcceptanceResult",
|
|
621
|
+
"AcceptanceSummary",
|
|
622
|
+
"AcceptanceMode",
|
|
623
|
+
"collect_feature_summary",
|
|
624
|
+
"detect_feature_slug",
|
|
625
|
+
"choose_mode",
|
|
626
|
+
"perform_acceptance",
|
|
627
|
+
]
|