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,695 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Acceptance workflow utilities without external dependencies."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from datetime import datetime, timezone
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import re
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Dict, Iterable, List, Mapping, MutableMapping, Optional, Sequence, Set, Tuple
|
|
13
|
+
|
|
14
|
+
from task_helpers 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
|
+
|
|
28
|
+
AcceptanceMode = str # Expected values: "pr", "local", "checklist"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class AcceptanceError(TaskCliError):
|
|
32
|
+
"""Raised when acceptance cannot complete due to outstanding issues."""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ArtifactEncodingError(AcceptanceError):
|
|
36
|
+
"""Raised when a project artifact cannot be decoded as UTF-8."""
|
|
37
|
+
|
|
38
|
+
def __init__(self, path: Path, error: UnicodeDecodeError):
|
|
39
|
+
byte = error.object[error.start : error.start + 1]
|
|
40
|
+
byte_display = f"0x{byte[0]:02x}" if byte else "unknown"
|
|
41
|
+
message = (
|
|
42
|
+
f"Invalid UTF-8 encoding in {path}: byte {byte_display} at offset {error.start}. "
|
|
43
|
+
"Run with --normalize-encoding to fix automatically."
|
|
44
|
+
)
|
|
45
|
+
super().__init__(message)
|
|
46
|
+
self.path = path
|
|
47
|
+
self.error = error
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass
|
|
51
|
+
class WorkPackageState:
|
|
52
|
+
work_package_id: str
|
|
53
|
+
lane: str
|
|
54
|
+
title: str
|
|
55
|
+
path: str
|
|
56
|
+
has_lane_entry: bool
|
|
57
|
+
latest_lane: Optional[str]
|
|
58
|
+
metadata: Dict[str, Optional[str]] = field(default_factory=dict)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass
|
|
62
|
+
class AcceptanceSummary:
|
|
63
|
+
feature: str
|
|
64
|
+
repo_root: Path
|
|
65
|
+
feature_dir: Path
|
|
66
|
+
tasks_dir: Path
|
|
67
|
+
branch: Optional[str]
|
|
68
|
+
worktree_root: Path
|
|
69
|
+
primary_repo_root: Path
|
|
70
|
+
lanes: Dict[str, List[str]]
|
|
71
|
+
work_packages: List[WorkPackageState]
|
|
72
|
+
metadata_issues: List[str]
|
|
73
|
+
activity_issues: List[str]
|
|
74
|
+
unchecked_tasks: List[str]
|
|
75
|
+
needs_clarification: List[str]
|
|
76
|
+
missing_artifacts: List[str]
|
|
77
|
+
optional_missing: List[str]
|
|
78
|
+
git_dirty: List[str]
|
|
79
|
+
warnings: List[str]
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def all_done(self) -> bool:
|
|
83
|
+
return not (
|
|
84
|
+
self.lanes.get("planned")
|
|
85
|
+
or self.lanes.get("doing")
|
|
86
|
+
or self.lanes.get("for_review")
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
def ok(self) -> bool:
|
|
91
|
+
return (
|
|
92
|
+
self.all_done
|
|
93
|
+
and not self.metadata_issues
|
|
94
|
+
and not self.activity_issues
|
|
95
|
+
and not self.unchecked_tasks
|
|
96
|
+
and not self.needs_clarification
|
|
97
|
+
and not self.missing_artifacts
|
|
98
|
+
and not self.git_dirty
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
def outstanding(self) -> Dict[str, List[str]]:
|
|
102
|
+
buckets = {
|
|
103
|
+
"not_done": [
|
|
104
|
+
*self.lanes.get("planned", []),
|
|
105
|
+
*self.lanes.get("doing", []),
|
|
106
|
+
*self.lanes.get("for_review", []),
|
|
107
|
+
],
|
|
108
|
+
"metadata": self.metadata_issues,
|
|
109
|
+
"activity": self.activity_issues,
|
|
110
|
+
"unchecked_tasks": self.unchecked_tasks,
|
|
111
|
+
"needs_clarification": self.needs_clarification,
|
|
112
|
+
"missing_artifacts": self.missing_artifacts,
|
|
113
|
+
"git_dirty": self.git_dirty,
|
|
114
|
+
}
|
|
115
|
+
return {key: value for key, value in buckets.items() if value}
|
|
116
|
+
|
|
117
|
+
def to_dict(self) -> Dict[str, object]:
|
|
118
|
+
return {
|
|
119
|
+
"feature": self.feature,
|
|
120
|
+
"branch": self.branch,
|
|
121
|
+
"repo_root": str(self.repo_root),
|
|
122
|
+
"feature_dir": str(self.feature_dir),
|
|
123
|
+
"tasks_dir": str(self.tasks_dir),
|
|
124
|
+
"worktree_root": str(self.worktree_root),
|
|
125
|
+
"primary_repo_root": str(self.primary_repo_root),
|
|
126
|
+
"lanes": self.lanes,
|
|
127
|
+
"work_packages": [
|
|
128
|
+
{
|
|
129
|
+
"id": wp.work_package_id,
|
|
130
|
+
"lane": wp.lane,
|
|
131
|
+
"title": wp.title,
|
|
132
|
+
"path": wp.path,
|
|
133
|
+
"latest_lane": wp.latest_lane,
|
|
134
|
+
"has_lane_entry": wp.has_lane_entry,
|
|
135
|
+
"metadata": wp.metadata,
|
|
136
|
+
}
|
|
137
|
+
for wp in self.work_packages
|
|
138
|
+
],
|
|
139
|
+
"metadata_issues": self.metadata_issues,
|
|
140
|
+
"activity_issues": self.activity_issues,
|
|
141
|
+
"unchecked_tasks": self.unchecked_tasks,
|
|
142
|
+
"needs_clarification": self.needs_clarification,
|
|
143
|
+
"missing_artifacts": self.missing_artifacts,
|
|
144
|
+
"optional_missing": self.optional_missing,
|
|
145
|
+
"git_dirty": self.git_dirty,
|
|
146
|
+
"warnings": self.warnings,
|
|
147
|
+
"all_done": self.all_done,
|
|
148
|
+
"ok": self.ok,
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@dataclass
|
|
153
|
+
class AcceptanceResult:
|
|
154
|
+
summary: AcceptanceSummary
|
|
155
|
+
mode: AcceptanceMode
|
|
156
|
+
accepted_at: str
|
|
157
|
+
accepted_by: str
|
|
158
|
+
parent_commit: Optional[str]
|
|
159
|
+
accept_commit: Optional[str]
|
|
160
|
+
commit_created: bool
|
|
161
|
+
instructions: List[str]
|
|
162
|
+
cleanup_instructions: List[str]
|
|
163
|
+
notes: List[str] = field(default_factory=list)
|
|
164
|
+
|
|
165
|
+
def to_dict(self) -> Dict[str, object]:
|
|
166
|
+
return {
|
|
167
|
+
"accepted_at": self.accepted_at,
|
|
168
|
+
"accepted_by": self.accepted_by,
|
|
169
|
+
"mode": self.mode,
|
|
170
|
+
"parent_commit": self.parent_commit,
|
|
171
|
+
"accept_commit": self.accept_commit,
|
|
172
|
+
"commit_created": self.commit_created,
|
|
173
|
+
"instructions": self.instructions,
|
|
174
|
+
"cleanup_instructions": self.cleanup_instructions,
|
|
175
|
+
"notes": self.notes,
|
|
176
|
+
"summary": self.summary.to_dict(),
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _iter_work_packages(repo_root: Path, feature: str) -> Iterable[WorkPackage]:
|
|
181
|
+
"""Iterate work packages for a feature.
|
|
182
|
+
|
|
183
|
+
Supports both legacy (directory-based) and new (frontmatter-only) lane formats.
|
|
184
|
+
"""
|
|
185
|
+
feature_dir = repo_root / "kitty-specs" / feature
|
|
186
|
+
tasks_dir = feature_dir / "tasks"
|
|
187
|
+
if not tasks_dir.exists():
|
|
188
|
+
raise AcceptanceError(f"Feature '{feature}' has no tasks directory at {tasks_dir}.")
|
|
189
|
+
|
|
190
|
+
if is_legacy_format(feature_dir):
|
|
191
|
+
# Legacy format: lane determined by subdirectory
|
|
192
|
+
for lane_dir in sorted(tasks_dir.iterdir()):
|
|
193
|
+
if not lane_dir.is_dir():
|
|
194
|
+
continue
|
|
195
|
+
lane = lane_dir.name
|
|
196
|
+
if lane not in LANES:
|
|
197
|
+
continue
|
|
198
|
+
for path in sorted(lane_dir.rglob("*.md")):
|
|
199
|
+
text = _read_text_strict(path)
|
|
200
|
+
front, body, padding = split_frontmatter(text)
|
|
201
|
+
relative = path.relative_to(lane_dir)
|
|
202
|
+
yield WorkPackage(
|
|
203
|
+
feature=feature,
|
|
204
|
+
path=path,
|
|
205
|
+
current_lane=lane,
|
|
206
|
+
relative_subpath=relative,
|
|
207
|
+
frontmatter=front,
|
|
208
|
+
body=body,
|
|
209
|
+
padding=padding,
|
|
210
|
+
)
|
|
211
|
+
else:
|
|
212
|
+
# New format: flat directory, lane from frontmatter
|
|
213
|
+
for path in sorted(tasks_dir.glob("*.md")):
|
|
214
|
+
if path.name == "README.md":
|
|
215
|
+
continue
|
|
216
|
+
text = _read_text_strict(path)
|
|
217
|
+
front, body, padding = split_frontmatter(text)
|
|
218
|
+
lane = get_lane_from_frontmatter(path, warn_on_missing=False)
|
|
219
|
+
yield WorkPackage(
|
|
220
|
+
feature=feature,
|
|
221
|
+
path=path,
|
|
222
|
+
current_lane=lane,
|
|
223
|
+
relative_subpath=path.relative_to(tasks_dir),
|
|
224
|
+
frontmatter=front,
|
|
225
|
+
body=body,
|
|
226
|
+
padding=padding,
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def detect_feature_slug(
|
|
231
|
+
repo_root: Path,
|
|
232
|
+
*,
|
|
233
|
+
env: Optional[Mapping[str, str]] = None,
|
|
234
|
+
cwd: Optional[Path] = None,
|
|
235
|
+
) -> str:
|
|
236
|
+
env = env or os.environ
|
|
237
|
+
if "SPECIFY_FEATURE" in env and env["SPECIFY_FEATURE"].strip():
|
|
238
|
+
return env["SPECIFY_FEATURE"].strip()
|
|
239
|
+
|
|
240
|
+
try:
|
|
241
|
+
branch = (
|
|
242
|
+
run_git(["rev-parse", "--abbrev-ref", "HEAD"], cwd=repo_root, check=True)
|
|
243
|
+
.stdout.strip()
|
|
244
|
+
)
|
|
245
|
+
if branch and branch != "HEAD" and re.match(r"^\d{3}-", branch):
|
|
246
|
+
return branch
|
|
247
|
+
except TaskCliError:
|
|
248
|
+
pass
|
|
249
|
+
|
|
250
|
+
cwd = (cwd or Path.cwd()).resolve()
|
|
251
|
+
for parent in [cwd, *cwd.parents]:
|
|
252
|
+
if parent.name.startswith(".worktrees"):
|
|
253
|
+
parts = list(parent.parts)
|
|
254
|
+
try:
|
|
255
|
+
idx = parts.index(".worktrees")
|
|
256
|
+
candidate = parts[idx + 1]
|
|
257
|
+
if re.match(r"^\d{3}-", candidate):
|
|
258
|
+
return candidate
|
|
259
|
+
except (ValueError, IndexError):
|
|
260
|
+
continue
|
|
261
|
+
if parent.name.startswith("0") and re.match(r"^\d{3}-", parent.name):
|
|
262
|
+
return parent.name
|
|
263
|
+
|
|
264
|
+
raise AcceptanceError(
|
|
265
|
+
"Unable to determine feature slug automatically. Provide --feature explicitly."
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _read_file(path: Path) -> str:
|
|
270
|
+
return _read_text_strict(path) if path.exists() else ""
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _read_text_strict(path: Path) -> str:
|
|
274
|
+
try:
|
|
275
|
+
return path.read_text(encoding="utf-8-sig")
|
|
276
|
+
except UnicodeDecodeError as exc:
|
|
277
|
+
raise ArtifactEncodingError(path, exc) from exc
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _find_unchecked_tasks(tasks_file: Path) -> List[str]:
|
|
281
|
+
if not tasks_file.exists():
|
|
282
|
+
return ["tasks.md missing"]
|
|
283
|
+
|
|
284
|
+
unchecked: List[str] = []
|
|
285
|
+
for line in _read_text_strict(tasks_file).splitlines():
|
|
286
|
+
if re.match(r"^\s*-\s*\[ \]", line):
|
|
287
|
+
unchecked.append(line.strip())
|
|
288
|
+
return unchecked
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def _check_needs_clarification(files: Sequence[Path]) -> List[str]:
|
|
292
|
+
results: List[str] = []
|
|
293
|
+
for file_path in files:
|
|
294
|
+
if file_path.exists():
|
|
295
|
+
text = _read_text_strict(file_path)
|
|
296
|
+
if "[NEEDS CLARIFICATION" in text:
|
|
297
|
+
results.append(str(file_path))
|
|
298
|
+
return results
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def _missing_artifacts(feature_dir: Path) -> Tuple[List[str], List[str]]:
|
|
302
|
+
required = [feature_dir / "spec.md", feature_dir / "plan.md", feature_dir / "tasks.md"]
|
|
303
|
+
optional = [
|
|
304
|
+
feature_dir / "quickstart.md",
|
|
305
|
+
feature_dir / "data-model.md",
|
|
306
|
+
feature_dir / "research.md",
|
|
307
|
+
feature_dir / "contracts",
|
|
308
|
+
]
|
|
309
|
+
missing_required = [str(p.relative_to(feature_dir)) for p in required if not p.exists()]
|
|
310
|
+
missing_optional = [str(p.relative_to(feature_dir)) for p in optional if not p.exists()]
|
|
311
|
+
return missing_required, missing_optional
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def normalize_feature_encoding(repo_root: Path, feature: str) -> List[Path]:
|
|
315
|
+
"""Normalize file encoding from Windows-1252 to UTF-8 with ASCII character mapping.
|
|
316
|
+
|
|
317
|
+
Converts Windows-1252 encoded files to UTF-8, replacing Unicode smart quotes
|
|
318
|
+
and special characters with ASCII equivalents for maximum compatibility.
|
|
319
|
+
"""
|
|
320
|
+
# Map Unicode characters to ASCII equivalents
|
|
321
|
+
NORMALIZE_MAP = {
|
|
322
|
+
'\u2018': "'", # Left single quotation mark → apostrophe
|
|
323
|
+
'\u2019': "'", # Right single quotation mark → apostrophe
|
|
324
|
+
'\u201A': "'", # Single low-9 quotation mark → apostrophe
|
|
325
|
+
'\u201C': '"', # Left double quotation mark → straight quote
|
|
326
|
+
'\u201D': '"', # Right double quotation mark → straight quote
|
|
327
|
+
'\u201E': '"', # Double low-9 quotation mark → straight quote
|
|
328
|
+
'\u2014': '--', # Em dash → double hyphen
|
|
329
|
+
'\u2013': '-', # En dash → hyphen
|
|
330
|
+
'\u2026': '...', # Horizontal ellipsis → three dots
|
|
331
|
+
'\u00A0': ' ', # Non-breaking space → regular space
|
|
332
|
+
'\u2022': '*', # Bullet → asterisk
|
|
333
|
+
'\u00B7': '*', # Middle dot → asterisk
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
feature_dir = repo_root / "kitty-specs" / feature
|
|
337
|
+
if not feature_dir.exists():
|
|
338
|
+
return []
|
|
339
|
+
|
|
340
|
+
candidates: List[Path] = []
|
|
341
|
+
primary_files = [
|
|
342
|
+
feature_dir / "spec.md",
|
|
343
|
+
feature_dir / "plan.md",
|
|
344
|
+
feature_dir / "quickstart.md",
|
|
345
|
+
feature_dir / "tasks.md",
|
|
346
|
+
feature_dir / "research.md",
|
|
347
|
+
feature_dir / "data-model.md",
|
|
348
|
+
]
|
|
349
|
+
candidates.extend(p for p in primary_files if p.exists())
|
|
350
|
+
|
|
351
|
+
for subdir in [feature_dir / "tasks", feature_dir / "research", feature_dir / "checklists"]:
|
|
352
|
+
if subdir.exists():
|
|
353
|
+
candidates.extend(path for path in subdir.rglob("*.md"))
|
|
354
|
+
|
|
355
|
+
rewritten: List[Path] = []
|
|
356
|
+
seen: Set[Path] = set()
|
|
357
|
+
for path in candidates:
|
|
358
|
+
if path in seen or not path.exists():
|
|
359
|
+
continue
|
|
360
|
+
seen.add(path)
|
|
361
|
+
data = path.read_bytes()
|
|
362
|
+
try:
|
|
363
|
+
data.decode("utf-8")
|
|
364
|
+
continue
|
|
365
|
+
except UnicodeDecodeError:
|
|
366
|
+
pass
|
|
367
|
+
|
|
368
|
+
text: Optional[str] = None
|
|
369
|
+
for encoding in ("cp1252", "latin-1"):
|
|
370
|
+
try:
|
|
371
|
+
text = data.decode(encoding)
|
|
372
|
+
break
|
|
373
|
+
except UnicodeDecodeError:
|
|
374
|
+
continue
|
|
375
|
+
if text is None:
|
|
376
|
+
text = data.decode("utf-8", errors="replace")
|
|
377
|
+
|
|
378
|
+
# Strip UTF-8 BOM if present in the text
|
|
379
|
+
text = text.lstrip('\ufeff')
|
|
380
|
+
|
|
381
|
+
# Normalize Unicode characters to ASCII equivalents
|
|
382
|
+
for unicode_char, ascii_replacement in NORMALIZE_MAP.items():
|
|
383
|
+
text = text.replace(unicode_char, ascii_replacement)
|
|
384
|
+
|
|
385
|
+
path.write_text(text, encoding="utf-8")
|
|
386
|
+
rewritten.append(path)
|
|
387
|
+
|
|
388
|
+
return rewritten
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def collect_feature_summary(
|
|
392
|
+
repo_root: Path,
|
|
393
|
+
feature: str,
|
|
394
|
+
*,
|
|
395
|
+
strict_metadata: bool = True,
|
|
396
|
+
) -> AcceptanceSummary:
|
|
397
|
+
feature_dir = repo_root / "kitty-specs" / feature
|
|
398
|
+
tasks_dir = feature_dir / "tasks"
|
|
399
|
+
if not feature_dir.exists():
|
|
400
|
+
raise AcceptanceError(f"Feature directory not found: {feature_dir}")
|
|
401
|
+
|
|
402
|
+
branch: Optional[str] = None
|
|
403
|
+
try:
|
|
404
|
+
branch_value = (
|
|
405
|
+
run_git(["rev-parse", "--abbrev-ref", "HEAD"], cwd=repo_root, check=True)
|
|
406
|
+
.stdout.strip()
|
|
407
|
+
)
|
|
408
|
+
if branch_value and branch_value != "HEAD":
|
|
409
|
+
branch = branch_value
|
|
410
|
+
except TaskCliError:
|
|
411
|
+
branch = None
|
|
412
|
+
|
|
413
|
+
try:
|
|
414
|
+
worktree_root = Path(
|
|
415
|
+
run_git(["rev-parse", "--show-toplevel"], cwd=repo_root, check=True)
|
|
416
|
+
.stdout.strip()
|
|
417
|
+
).resolve()
|
|
418
|
+
except TaskCliError:
|
|
419
|
+
worktree_root = repo_root
|
|
420
|
+
|
|
421
|
+
try:
|
|
422
|
+
git_common_dir = Path(
|
|
423
|
+
run_git(["rev-parse", "--git-common-dir"], cwd=repo_root, check=True)
|
|
424
|
+
.stdout.strip()
|
|
425
|
+
).resolve()
|
|
426
|
+
primary_repo_root = git_common_dir.parent
|
|
427
|
+
except TaskCliError:
|
|
428
|
+
primary_repo_root = repo_root
|
|
429
|
+
|
|
430
|
+
lanes: Dict[str, List[str]] = {lane: [] for lane in LANES}
|
|
431
|
+
work_packages: List[WorkPackageState] = []
|
|
432
|
+
metadata_issues: List[str] = []
|
|
433
|
+
activity_issues: List[str] = []
|
|
434
|
+
|
|
435
|
+
for wp in _iter_work_packages(repo_root, feature):
|
|
436
|
+
wp_id = wp.work_package_id or wp.path.stem
|
|
437
|
+
title = (wp.title or "").strip('"')
|
|
438
|
+
lanes[wp.current_lane].append(wp_id)
|
|
439
|
+
|
|
440
|
+
entries = activity_entries(wp.body)
|
|
441
|
+
lanes_logged = {entry["lane"] for entry in entries}
|
|
442
|
+
latest_lane = entries[-1]["lane"] if entries else None
|
|
443
|
+
has_lane_entry = wp.current_lane in lanes_logged
|
|
444
|
+
|
|
445
|
+
metadata: Dict[str, Optional[str]] = {
|
|
446
|
+
"lane": wp.lane,
|
|
447
|
+
"agent": wp.agent,
|
|
448
|
+
"assignee": wp.assignee,
|
|
449
|
+
"shell_pid": wp.shell_pid,
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
if strict_metadata:
|
|
453
|
+
lane_value = (wp.lane or "").strip()
|
|
454
|
+
if not lane_value:
|
|
455
|
+
metadata_issues.append(f"{wp_id}: missing lane in frontmatter")
|
|
456
|
+
elif lane_value != wp.current_lane:
|
|
457
|
+
metadata_issues.append(
|
|
458
|
+
f"{wp_id}: frontmatter lane '{lane_value}' does not match expected '{wp.current_lane}'"
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
if not wp.agent:
|
|
462
|
+
metadata_issues.append(f"{wp_id}: missing agent in frontmatter")
|
|
463
|
+
if wp.current_lane in {"doing", "for_review", "done"} and not wp.assignee:
|
|
464
|
+
metadata_issues.append(f"{wp_id}: missing assignee in frontmatter")
|
|
465
|
+
if not wp.shell_pid:
|
|
466
|
+
metadata_issues.append(f"{wp_id}: missing shell_pid in frontmatter")
|
|
467
|
+
|
|
468
|
+
if not entries:
|
|
469
|
+
activity_issues.append(f"{wp_id}: Activity Log missing entries")
|
|
470
|
+
else:
|
|
471
|
+
if wp.current_lane not in lanes_logged:
|
|
472
|
+
activity_issues.append(
|
|
473
|
+
f"{wp_id}: Activity Log missing entry for lane={wp.current_lane}"
|
|
474
|
+
)
|
|
475
|
+
if wp.current_lane == "done" and entries[-1]["lane"] != "done":
|
|
476
|
+
activity_issues.append(f"{wp_id}: latest Activity Log entry not lane=done")
|
|
477
|
+
|
|
478
|
+
work_packages.append(
|
|
479
|
+
WorkPackageState(
|
|
480
|
+
work_package_id=wp_id,
|
|
481
|
+
lane=wp.current_lane,
|
|
482
|
+
title=title,
|
|
483
|
+
path=str(wp.path.relative_to(repo_root)),
|
|
484
|
+
has_lane_entry=has_lane_entry,
|
|
485
|
+
latest_lane=latest_lane,
|
|
486
|
+
metadata=metadata,
|
|
487
|
+
)
|
|
488
|
+
)
|
|
489
|
+
|
|
490
|
+
unchecked_tasks = _find_unchecked_tasks(feature_dir / "tasks.md")
|
|
491
|
+
needs_clarification = _check_needs_clarification(
|
|
492
|
+
[
|
|
493
|
+
feature_dir / "spec.md",
|
|
494
|
+
feature_dir / "plan.md",
|
|
495
|
+
feature_dir / "quickstart.md",
|
|
496
|
+
feature_dir / "tasks.md",
|
|
497
|
+
feature_dir / "research.md",
|
|
498
|
+
feature_dir / "data-model.md",
|
|
499
|
+
]
|
|
500
|
+
)
|
|
501
|
+
missing_required, missing_optional = _missing_artifacts(feature_dir)
|
|
502
|
+
|
|
503
|
+
try:
|
|
504
|
+
git_dirty = git_status_lines(repo_root)
|
|
505
|
+
except TaskCliError:
|
|
506
|
+
git_dirty = []
|
|
507
|
+
|
|
508
|
+
warnings: List[str] = []
|
|
509
|
+
if missing_optional:
|
|
510
|
+
warnings.append("Optional artifacts missing: " + ", ".join(missing_optional))
|
|
511
|
+
|
|
512
|
+
return AcceptanceSummary(
|
|
513
|
+
feature=feature,
|
|
514
|
+
repo_root=repo_root,
|
|
515
|
+
feature_dir=feature_dir,
|
|
516
|
+
tasks_dir=tasks_dir,
|
|
517
|
+
branch=branch,
|
|
518
|
+
worktree_root=worktree_root,
|
|
519
|
+
primary_repo_root=primary_repo_root,
|
|
520
|
+
lanes=lanes,
|
|
521
|
+
work_packages=work_packages,
|
|
522
|
+
metadata_issues=metadata_issues,
|
|
523
|
+
activity_issues=activity_issues,
|
|
524
|
+
unchecked_tasks=unchecked_tasks if unchecked_tasks != ["tasks.md missing"] else [],
|
|
525
|
+
needs_clarification=needs_clarification,
|
|
526
|
+
missing_artifacts=missing_required,
|
|
527
|
+
optional_missing=missing_optional,
|
|
528
|
+
git_dirty=git_dirty,
|
|
529
|
+
warnings=warnings,
|
|
530
|
+
)
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
def choose_mode(preference: Optional[str], repo_root: Path) -> AcceptanceMode:
|
|
534
|
+
if preference in {"pr", "local", "checklist"}:
|
|
535
|
+
return preference
|
|
536
|
+
try:
|
|
537
|
+
remotes = (
|
|
538
|
+
run_git(["remote"], cwd=repo_root, check=False).stdout.strip().splitlines()
|
|
539
|
+
)
|
|
540
|
+
if remotes:
|
|
541
|
+
return "pr"
|
|
542
|
+
except TaskCliError:
|
|
543
|
+
pass
|
|
544
|
+
return "local"
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
def perform_acceptance(
|
|
548
|
+
summary: AcceptanceSummary,
|
|
549
|
+
*,
|
|
550
|
+
mode: AcceptanceMode,
|
|
551
|
+
actor: Optional[str],
|
|
552
|
+
tests: Optional[Sequence[str]] = None,
|
|
553
|
+
auto_commit: bool = True,
|
|
554
|
+
) -> AcceptanceResult:
|
|
555
|
+
if mode != "checklist" and not summary.ok:
|
|
556
|
+
raise AcceptanceError(
|
|
557
|
+
"Acceptance checks failed; run verify to see outstanding issues."
|
|
558
|
+
)
|
|
559
|
+
|
|
560
|
+
actor_name = (actor or os.getenv("USER") or os.getenv("USERNAME") or "system").strip()
|
|
561
|
+
timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
562
|
+
|
|
563
|
+
parent_commit: Optional[str] = None
|
|
564
|
+
accept_commit: Optional[str] = None
|
|
565
|
+
|
|
566
|
+
if auto_commit and mode != "checklist":
|
|
567
|
+
try:
|
|
568
|
+
parent_commit = (
|
|
569
|
+
run_git(["rev-parse", "HEAD"], cwd=summary.repo_root, check=False)
|
|
570
|
+
.stdout.strip()
|
|
571
|
+
or None
|
|
572
|
+
)
|
|
573
|
+
except TaskCliError:
|
|
574
|
+
parent_commit = None
|
|
575
|
+
|
|
576
|
+
meta_path = summary.feature_dir / "meta.json"
|
|
577
|
+
if meta_path.exists():
|
|
578
|
+
meta = json.loads(_read_text_strict(meta_path))
|
|
579
|
+
else:
|
|
580
|
+
meta = {}
|
|
581
|
+
|
|
582
|
+
acceptance_record: Dict[str, object] = {
|
|
583
|
+
"accepted_at": timestamp,
|
|
584
|
+
"accepted_by": actor_name,
|
|
585
|
+
"mode": mode,
|
|
586
|
+
"branch": summary.branch,
|
|
587
|
+
"accepted_from_commit": parent_commit,
|
|
588
|
+
}
|
|
589
|
+
if tests:
|
|
590
|
+
acceptance_record["validation_commands"] = list(tests)
|
|
591
|
+
|
|
592
|
+
meta["accepted_at"] = timestamp
|
|
593
|
+
meta["accepted_by"] = actor_name
|
|
594
|
+
meta["acceptance_mode"] = mode
|
|
595
|
+
meta["accepted_from_commit"] = parent_commit
|
|
596
|
+
meta["accept_commit"] = None
|
|
597
|
+
|
|
598
|
+
history: List[Dict[str, object]] = meta.setdefault("acceptance_history", [])
|
|
599
|
+
history.append(acceptance_record)
|
|
600
|
+
if len(history) > 20:
|
|
601
|
+
meta["acceptance_history"] = history[-20:]
|
|
602
|
+
|
|
603
|
+
meta_path.write_text(json.dumps(meta, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
|
604
|
+
run_git(
|
|
605
|
+
["add", str(meta_path.relative_to(summary.repo_root))],
|
|
606
|
+
cwd=summary.repo_root,
|
|
607
|
+
check=True,
|
|
608
|
+
)
|
|
609
|
+
|
|
610
|
+
status = run_git(["diff", "--cached", "--name-only"], cwd=summary.repo_root, check=True)
|
|
611
|
+
staged_files = [line.strip() for line in status.stdout.splitlines() if line.strip()]
|
|
612
|
+
commit_created = False
|
|
613
|
+
if staged_files:
|
|
614
|
+
commit_msg = f"Accept {summary.feature}"
|
|
615
|
+
run_git(["commit", "-m", commit_msg], cwd=summary.repo_root, check=True)
|
|
616
|
+
commit_created = True
|
|
617
|
+
try:
|
|
618
|
+
accept_commit = (
|
|
619
|
+
run_git(["rev-parse", "HEAD"], cwd=summary.repo_root, check=True)
|
|
620
|
+
.stdout.strip()
|
|
621
|
+
)
|
|
622
|
+
except TaskCliError:
|
|
623
|
+
accept_commit = None
|
|
624
|
+
else:
|
|
625
|
+
commit_created = False
|
|
626
|
+
else:
|
|
627
|
+
commit_created = False
|
|
628
|
+
|
|
629
|
+
instructions: List[str] = []
|
|
630
|
+
cleanup_instructions: List[str] = []
|
|
631
|
+
|
|
632
|
+
branch = summary.branch or summary.feature
|
|
633
|
+
if mode == "pr":
|
|
634
|
+
instructions.extend(
|
|
635
|
+
[
|
|
636
|
+
f"Review the acceptance commit on branch `{branch}`.",
|
|
637
|
+
f"Push your branch: `git push origin {branch}`",
|
|
638
|
+
"Open a pull request referencing spec/plan/tasks artifacts.",
|
|
639
|
+
"Include acceptance summary and test evidence in the PR description.",
|
|
640
|
+
]
|
|
641
|
+
)
|
|
642
|
+
elif mode == "local":
|
|
643
|
+
instructions.extend(
|
|
644
|
+
[
|
|
645
|
+
"Switch to your integration branch (e.g., `git checkout main`).",
|
|
646
|
+
"Synchronize it (e.g., `git pull --ff-only`).",
|
|
647
|
+
f"Merge the feature: `git merge {branch}`",
|
|
648
|
+
]
|
|
649
|
+
)
|
|
650
|
+
else: # checklist
|
|
651
|
+
instructions.append(
|
|
652
|
+
"All checks passed. Proceed with your manual acceptance workflow."
|
|
653
|
+
)
|
|
654
|
+
|
|
655
|
+
if summary.worktree_root != summary.primary_repo_root:
|
|
656
|
+
cleanup_instructions.append(
|
|
657
|
+
f"After merging, remove the worktree: `git worktree remove {summary.worktree_root}`"
|
|
658
|
+
)
|
|
659
|
+
cleanup_instructions.append(f"Delete the feature branch when done: `git branch -d {branch}`")
|
|
660
|
+
|
|
661
|
+
notes: List[str] = []
|
|
662
|
+
if accept_commit:
|
|
663
|
+
notes.append(f"Acceptance commit: {accept_commit}")
|
|
664
|
+
if parent_commit:
|
|
665
|
+
notes.append(f"Accepted from parent commit: {parent_commit}")
|
|
666
|
+
if tests:
|
|
667
|
+
notes.append("Validation commands:")
|
|
668
|
+
notes.extend(f" - {cmd}" for cmd in tests)
|
|
669
|
+
|
|
670
|
+
return AcceptanceResult(
|
|
671
|
+
summary=summary,
|
|
672
|
+
mode=mode,
|
|
673
|
+
accepted_at=timestamp,
|
|
674
|
+
accepted_by=actor_name,
|
|
675
|
+
parent_commit=parent_commit,
|
|
676
|
+
accept_commit=accept_commit,
|
|
677
|
+
commit_created=commit_created,
|
|
678
|
+
instructions=instructions,
|
|
679
|
+
cleanup_instructions=cleanup_instructions,
|
|
680
|
+
notes=notes,
|
|
681
|
+
)
|
|
682
|
+
|
|
683
|
+
|
|
684
|
+
__all__ = [
|
|
685
|
+
"AcceptanceError",
|
|
686
|
+
"ArtifactEncodingError",
|
|
687
|
+
"AcceptanceResult",
|
|
688
|
+
"AcceptanceSummary",
|
|
689
|
+
"AcceptanceMode",
|
|
690
|
+
"collect_feature_summary",
|
|
691
|
+
"detect_feature_slug",
|
|
692
|
+
"choose_mode",
|
|
693
|
+
"perform_acceptance",
|
|
694
|
+
"normalize_feature_encoding",
|
|
695
|
+
]
|