specstamp 0.11.0__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.
- codex_sdlc/__init__.py +5 -0
- codex_sdlc/adapters/__init__.py +1 -0
- codex_sdlc/cli.py +418 -0
- codex_sdlc/commands/__init__.py +1 -0
- codex_sdlc/commands/accept_cmd.py +122 -0
- codex_sdlc/commands/add_cmd.py +135 -0
- codex_sdlc/commands/agent_sync_cmd.py +76 -0
- codex_sdlc/commands/backup_cmd.py +270 -0
- codex_sdlc/commands/capture_cmd.py +380 -0
- codex_sdlc/commands/change_cmd.py +538 -0
- codex_sdlc/commands/clean_cmd.py +171 -0
- codex_sdlc/commands/context_cmd.py +231 -0
- codex_sdlc/commands/design_cmd.py +262 -0
- codex_sdlc/commands/docs_cmd.py +469 -0
- codex_sdlc/commands/doctor_cmd.py +916 -0
- codex_sdlc/commands/draft_cmd.py +590 -0
- codex_sdlc/commands/export_cmd.py +266 -0
- codex_sdlc/commands/facts_cmd.py +132 -0
- codex_sdlc/commands/finish_cmd.py +83 -0
- codex_sdlc/commands/grill_cmd.py +216 -0
- codex_sdlc/commands/handoff_cmd.py +24 -0
- codex_sdlc/commands/hooks_cmd.py +23 -0
- codex_sdlc/commands/init_cmd.py +60 -0
- codex_sdlc/commands/lessons_cmd.py +371 -0
- codex_sdlc/commands/material_cmd.py +993 -0
- codex_sdlc/commands/next_cmd.py +23 -0
- codex_sdlc/commands/placeholder_cmd.py +20 -0
- codex_sdlc/commands/plan_cmd.py +1964 -0
- codex_sdlc/commands/regression_cmd.py +545 -0
- codex_sdlc/commands/review_cmd.py +115 -0
- codex_sdlc/commands/start_cmd.py +33 -0
- codex_sdlc/commands/status_cmd.py +23 -0
- codex_sdlc/commands/task_cmd.py +3619 -0
- codex_sdlc/core/__init__.py +1 -0
- codex_sdlc/core/agent_sync.py +1824 -0
- codex_sdlc/core/artifact_index.py +828 -0
- codex_sdlc/core/atomic_import.py +984 -0
- codex_sdlc/core/backup.py +2328 -0
- codex_sdlc/core/change_contract.py +1335 -0
- codex_sdlc/core/change_migration.py +676 -0
- codex_sdlc/core/change_workspace.py +2341 -0
- codex_sdlc/core/code_evidence.py +1019 -0
- codex_sdlc/core/codegraph_context.py +616 -0
- codex_sdlc/core/codex_assets.py +185 -0
- codex_sdlc/core/command_registry.py +115 -0
- codex_sdlc/core/dependency_graph.py +564 -0
- codex_sdlc/core/design_artifact_contract.py +1043 -0
- codex_sdlc/core/design_plan_contract.py +449 -0
- codex_sdlc/core/design_summary_contract.py +855 -0
- codex_sdlc/core/draft_artifacts.py +711 -0
- codex_sdlc/core/draft_contract.py +224 -0
- codex_sdlc/core/draft_lifecycle.py +960 -0
- codex_sdlc/core/draft_ownership.py +106 -0
- codex_sdlc/core/draft_sections.py +140 -0
- codex_sdlc/core/environment.py +52 -0
- codex_sdlc/core/errors.py +7 -0
- codex_sdlc/core/external_version.py +180 -0
- codex_sdlc/core/fact_artifacts.py +650 -0
- codex_sdlc/core/fact_gate.py +544 -0
- codex_sdlc/core/fact_review_trust.py +727 -0
- codex_sdlc/core/fact_schema.py +344 -0
- codex_sdlc/core/formal_manifest_contract.py +324 -0
- codex_sdlc/core/git_tools.py +130 -0
- codex_sdlc/core/id_allocator.py +194 -0
- codex_sdlc/core/lessons.py +371 -0
- codex_sdlc/core/markdown_contract.py +209 -0
- codex_sdlc/core/project.py +299 -0
- codex_sdlc/core/reference_index.py +1063 -0
- codex_sdlc/core/reference_locator.py +487 -0
- codex_sdlc/core/render.py +14 -0
- codex_sdlc/core/requirement_contract.py +603 -0
- codex_sdlc/core/review_contract.py +337 -0
- codex_sdlc/core/start_contract.py +805 -0
- codex_sdlc/core/start_staging.py +1196 -0
- codex_sdlc/core/start_transaction.py +1078 -0
- codex_sdlc/core/state.py +9349 -0
- codex_sdlc/core/structured_contract.py +251 -0
- codex_sdlc/core/task_contract.py +2108 -0
- codex_sdlc/core/task_coverage_contract.py +918 -0
- codex_sdlc/core/task_evidence.py +394 -0
- codex_sdlc/core/task_ids.py +21 -0
- codex_sdlc/core/task_outputs.py +918 -0
- codex_sdlc/core/task_pack.py +28 -0
- codex_sdlc/core/task_pack_contract.py +3 -0
- codex_sdlc/core/task_quality.py +116 -0
- codex_sdlc/core/task_read_manifest.py +188 -0
- codex_sdlc/core/task_run.py +2255 -0
- codex_sdlc/legacy/__init__.py +4 -0
- codex_sdlc/legacy/task_pack_reader.py +352 -0
- codex_sdlc/schemas/__init__.py +64 -0
- codex_sdlc/schemas/artifact-index.v1.json +120 -0
- codex_sdlc/schemas/atomic-import-registry.v1.json +83 -0
- codex_sdlc/schemas/atomic-import-result.v1.json +73 -0
- codex_sdlc/schemas/atomic-import-transaction.v1.json +142 -0
- codex_sdlc/schemas/atomic-import.v1.json +139 -0
- codex_sdlc/schemas/change-material-manifest.v1.json +474 -0
- codex_sdlc/schemas/change-package.v1.json +1014 -0
- codex_sdlc/schemas/change-workspace.v1.json +92 -0
- codex_sdlc/schemas/code-evidence.v1.json +93 -0
- codex_sdlc/schemas/design-artifact.v1.json +910 -0
- codex_sdlc/schemas/design-facts.v1.json +565 -0
- codex_sdlc/schemas/design-node-index.v1.json +60 -0
- codex_sdlc/schemas/design-plan.v1.json +280 -0
- codex_sdlc/schemas/design-reference-index.v1.json +181 -0
- codex_sdlc/schemas/design-reference.v1.json +240 -0
- codex_sdlc/schemas/design-summary.v1.json +221 -0
- codex_sdlc/schemas/external-version-evidence.v1.json +95 -0
- codex_sdlc/schemas/formal-document-first.v3.json +136 -0
- codex_sdlc/schemas/material-manifest.v1.json +524 -0
- codex_sdlc/schemas/model-review.v1.json +141 -0
- codex_sdlc/schemas/projected-design.v2.json +1 -0
- codex_sdlc/schemas/projected-reference-index.v2.json +1 -0
- codex_sdlc/schemas/projected-requirement.v2.json +1 -0
- codex_sdlc/schemas/projected-task-plan.v2.json +1 -0
- codex_sdlc/schemas/projected-test-matrix.v2.json +1 -0
- codex_sdlc/schemas/reference-index.v1.json +54 -0
- codex_sdlc/schemas/reference-locator.v1.json +615 -0
- codex_sdlc/schemas/requirement-confirmation.v1.json +98 -0
- codex_sdlc/schemas/requirement-coverage.v1.json +304 -0
- codex_sdlc/schemas/requirement-facts.v1.json +699 -0
- codex_sdlc/schemas/requirement-split.v1.json +380 -0
- codex_sdlc/schemas/review-request.v1.json +83 -0
- codex_sdlc/schemas/review-result.v1.json +139 -0
- codex_sdlc/schemas/review-trust-registry.v1.json +162 -0
- codex_sdlc/schemas/secret-reference.v1.json +52 -0
- codex_sdlc/schemas/source-index.v1.json +262 -0
- codex_sdlc/schemas/task-coverage.v1.json +168 -0
- codex_sdlc/schemas/task-feedback.v1.json +54 -0
- codex_sdlc/schemas/task-plan.v2.json +189 -0
- codex_sdlc/schemas/task-read-manifest.v1.json +68 -0
- codex_sdlc/schemas/task-run.v1.json +98 -0
- codex_sdlc/schemas/task.v2.json +283 -0
- codex_sdlc/services/__init__.py +1 -0
- codex_sdlc/services/capture_service.py +158 -0
- codex_sdlc/services/change_service.py +1984 -0
- codex_sdlc/services/design_service.py +1650 -0
- codex_sdlc/services/discuss_service.py +1114 -0
- codex_sdlc/services/draft_service.py +1329 -0
- codex_sdlc/services/review_service.py +3387 -0
- codex_sdlc/services/start_service.py +851 -0
- codex_sdlc/templates/__init__.py +1 -0
- specstamp-0.11.0.data/data/share/specstamp/shared-skills/agent-capability-sync/SKILL.md +44 -0
- specstamp-0.11.0.data/data/share/specstamp/skills/sdlc-accept/SKILL.md +19 -0
- specstamp-0.11.0.data/data/share/specstamp/skills/sdlc-add/SKILL.md +43 -0
- specstamp-0.11.0.data/data/share/specstamp/skills/sdlc-agent-sync/SKILL.md +29 -0
- specstamp-0.11.0.data/data/share/specstamp/skills/sdlc-audit/SKILL.md +22 -0
- specstamp-0.11.0.data/data/share/specstamp/skills/sdlc-backup/SKILL.md +20 -0
- specstamp-0.11.0.data/data/share/specstamp/skills/sdlc-backup-clean/SKILL.md +18 -0
- specstamp-0.11.0.data/data/share/specstamp/skills/sdlc-backup-list/SKILL.md +21 -0
- specstamp-0.11.0.data/data/share/specstamp/skills/sdlc-capture/SKILL.md +21 -0
- specstamp-0.11.0.data/data/share/specstamp/skills/sdlc-capture-change/SKILL.md +19 -0
- specstamp-0.11.0.data/data/share/specstamp/skills/sdlc-capture-link/SKILL.md +21 -0
- specstamp-0.11.0.data/data/share/specstamp/skills/sdlc-capture-requirement/SKILL.md +16 -0
- specstamp-0.11.0.data/data/share/specstamp/skills/sdlc-change/SKILL.md +78 -0
- specstamp-0.11.0.data/data/share/specstamp/skills/sdlc-change-accept/SKILL.md +22 -0
- specstamp-0.11.0.data/data/share/specstamp/skills/sdlc-change-capture/SKILL.md +19 -0
- specstamp-0.11.0.data/data/share/specstamp/skills/sdlc-change-plan/SKILL.md +23 -0
- specstamp-0.11.0.data/data/share/specstamp/skills/sdlc-clean/SKILL.md +17 -0
- specstamp-0.11.0.data/data/share/specstamp/skills/sdlc-clean-confirm/SKILL.md +17 -0
- specstamp-0.11.0.data/data/share/specstamp/skills/sdlc-context/SKILL.md +24 -0
- specstamp-0.11.0.data/data/share/specstamp/skills/sdlc-design/SKILL.md +111 -0
- specstamp-0.11.0.data/data/share/specstamp/skills/sdlc-design-accept/SKILL.md +52 -0
- specstamp-0.11.0.data/data/share/specstamp/skills/sdlc-discuss/SKILL.md +93 -0
- specstamp-0.11.0.data/data/share/specstamp/skills/sdlc-discuss-link/SKILL.md +19 -0
- specstamp-0.11.0.data/data/share/specstamp/skills/sdlc-docs/SKILL.md +27 -0
- specstamp-0.11.0.data/data/share/specstamp/skills/sdlc-doctor/SKILL.md +19 -0
- specstamp-0.11.0.data/data/share/specstamp/skills/sdlc-doctor-deep/SKILL.md +23 -0
- specstamp-0.11.0.data/data/share/specstamp/skills/sdlc-doctor-install/SKILL.md +15 -0
- specstamp-0.11.0.data/data/share/specstamp/skills/sdlc-doctor-repair/SKILL.md +21 -0
- specstamp-0.11.0.data/data/share/specstamp/skills/sdlc-export/SKILL.md +17 -0
- specstamp-0.11.0.data/data/share/specstamp/skills/sdlc-export-requirement/SKILL.md +15 -0
- specstamp-0.11.0.data/data/share/specstamp/skills/sdlc-finish/SKILL.md +17 -0
- specstamp-0.11.0.data/data/share/specstamp/skills/sdlc-fix/SKILL.md +26 -0
- specstamp-0.11.0.data/data/share/specstamp/skills/sdlc-goal/SKILL.md +57 -0
- specstamp-0.11.0.data/data/share/specstamp/skills/sdlc-grill/SKILL.md +52 -0
- specstamp-0.11.0.data/data/share/specstamp/skills/sdlc-handoff/SKILL.md +19 -0
- specstamp-0.11.0.data/data/share/specstamp/skills/sdlc-hooks-upgrade/SKILL.md +21 -0
- specstamp-0.11.0.data/data/share/specstamp/skills/sdlc-init/SKILL.md +19 -0
- specstamp-0.11.0.data/data/share/specstamp/skills/sdlc-init-basic/SKILL.md +17 -0
- specstamp-0.11.0.data/data/share/specstamp/skills/sdlc-init-basic-plain/SKILL.md +17 -0
- specstamp-0.11.0.data/data/share/specstamp/skills/sdlc-init-plain/SKILL.md +19 -0
- specstamp-0.11.0.data/data/share/specstamp/skills/sdlc-lessons/SKILL.md +27 -0
- specstamp-0.11.0.data/data/share/specstamp/skills/sdlc-light-start/SKILL.md +16 -0
- specstamp-0.11.0.data/data/share/specstamp/skills/sdlc-material/SKILL.md +60 -0
- specstamp-0.11.0.data/data/share/specstamp/skills/sdlc-next/SKILL.md +43 -0
- specstamp-0.11.0.data/data/share/specstamp/skills/sdlc-plan/SKILL.md +20 -0
- specstamp-0.11.0.data/data/share/specstamp/skills/sdlc-plan-add-task/SKILL.md +18 -0
- specstamp-0.11.0.data/data/share/specstamp/skills/sdlc-plan-amend-task/SKILL.md +19 -0
- specstamp-0.11.0.data/data/share/specstamp/skills/sdlc-plan-close/SKILL.md +17 -0
- specstamp-0.11.0.data/data/share/specstamp/skills/sdlc-plan-depends/SKILL.md +17 -0
- specstamp-0.11.0.data/data/share/specstamp/skills/sdlc-plan-priority/SKILL.md +17 -0
- specstamp-0.11.0.data/data/share/specstamp/skills/sdlc-plan-reorder/SKILL.md +17 -0
- specstamp-0.11.0.data/data/share/specstamp/skills/sdlc-regression/SKILL.md +18 -0
- specstamp-0.11.0.data/data/share/specstamp/skills/sdlc-restore/SKILL.md +26 -0
- specstamp-0.11.0.data/data/share/specstamp/skills/sdlc-start/SKILL.md +68 -0
- specstamp-0.11.0.data/data/share/specstamp/skills/sdlc-status/SKILL.md +44 -0
- specstamp-0.11.0.data/data/share/specstamp/skills/sdlc-task/SKILL.md +63 -0
- specstamp-0.11.0.data/data/share/specstamp/skills/sdlc-task-done/SKILL.md +19 -0
- specstamp-0.11.0.data/data/share/specstamp/skills/sdlc-task-pause/SKILL.md +18 -0
- specstamp-0.11.0.data/data/share/specstamp/skills/sdlc-task-restore/SKILL.md +22 -0
- specstamp-0.11.0.data/data/share/specstamp/skills/sdlc-tasks/SKILL.md +67 -0
- specstamp-0.11.0.data/data/share/specstamp/skills/sdlc-version/SKILL.md +14 -0
- specstamp-0.11.0.dist-info/METADATA +182 -0
- specstamp-0.11.0.dist-info/RECORD +209 -0
- specstamp-0.11.0.dist-info/WHEEL +5 -0
- specstamp-0.11.0.dist-info/entry_points.txt +3 -0
- specstamp-0.11.0.dist-info/licenses/LICENSE +201 -0
- specstamp-0.11.0.dist-info/licenses/NOTICE +11 -0
- specstamp-0.11.0.dist-info/top_level.txt +1 -0
codex_sdlc/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""适配层预留目录。"""
|
codex_sdlc/cli.py
ADDED
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import os
|
|
5
|
+
import re
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from codex_sdlc.core.backup import create_backup, record_auto_backup_failure, require_matching_sdlc_identity
|
|
10
|
+
from codex_sdlc.core.environment import version_text
|
|
11
|
+
from codex_sdlc.core.errors import SdlcError
|
|
12
|
+
from codex_sdlc.core.project import build_paths, resolve_project_root
|
|
13
|
+
from codex_sdlc.core.state import derive_state, render_next_text
|
|
14
|
+
from codex_sdlc.commands import (
|
|
15
|
+
accept_cmd,
|
|
16
|
+
add_cmd,
|
|
17
|
+
agent_sync_cmd,
|
|
18
|
+
backup_cmd,
|
|
19
|
+
capture_cmd,
|
|
20
|
+
change_cmd,
|
|
21
|
+
clean_cmd,
|
|
22
|
+
context_cmd,
|
|
23
|
+
draft_cmd,
|
|
24
|
+
design_cmd,
|
|
25
|
+
doctor_cmd,
|
|
26
|
+
docs_cmd,
|
|
27
|
+
export_cmd,
|
|
28
|
+
finish_cmd,
|
|
29
|
+
facts_cmd,
|
|
30
|
+
grill_cmd,
|
|
31
|
+
handoff_cmd,
|
|
32
|
+
hooks_cmd,
|
|
33
|
+
init_cmd,
|
|
34
|
+
lessons_cmd,
|
|
35
|
+
material_cmd,
|
|
36
|
+
next_cmd,
|
|
37
|
+
plan_cmd,
|
|
38
|
+
regression_cmd,
|
|
39
|
+
start_cmd,
|
|
40
|
+
status_cmd,
|
|
41
|
+
task_cmd,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def translate_argparse_error(message: str) -> str:
|
|
46
|
+
required_match = re.match(r"the following arguments are required: (.+)", message)
|
|
47
|
+
if required_match:
|
|
48
|
+
return f"缺少必填参数:{required_match.group(1)}"
|
|
49
|
+
|
|
50
|
+
invalid_choice_match = re.match(r"argument (.+): invalid choice: '(.+)' \((.+)\)", message)
|
|
51
|
+
if invalid_choice_match:
|
|
52
|
+
target, invalid_value, choices = invalid_choice_match.groups()
|
|
53
|
+
return f"{target} 的值 `{invalid_value}` 不支持,{choices.replace('choose from', '可选值:')}"
|
|
54
|
+
|
|
55
|
+
unrecognized_match = re.match(r"unrecognized arguments: (.+)", message)
|
|
56
|
+
if unrecognized_match:
|
|
57
|
+
return f"无法识别这些参数:{unrecognized_match.group(1)}"
|
|
58
|
+
|
|
59
|
+
expected_match = re.match(r"argument (.+): expected one argument", message)
|
|
60
|
+
if expected_match:
|
|
61
|
+
return f"{expected_match.group(1)} 后面还缺少参数值"
|
|
62
|
+
|
|
63
|
+
return message
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class ChineseArgumentParser(argparse.ArgumentParser):
|
|
67
|
+
def error(self, message: str) -> None:
|
|
68
|
+
translated = translate_argparse_error(message)
|
|
69
|
+
raise SdlcError(f"{self.format_usage().strip()}\n参数错误:{translated}", exit_code=2)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def render_uninitialized_next_recommendation() -> str:
|
|
73
|
+
return "\n".join(
|
|
74
|
+
[
|
|
75
|
+
"下一步推荐",
|
|
76
|
+
"- 主推荐:$sdlc-init",
|
|
77
|
+
"- 原因:当前目录还没有可用的 SDLC 状态,先初始化项目。",
|
|
78
|
+
"",
|
|
79
|
+
"备选指令",
|
|
80
|
+
"- $sdlc-doctor-install",
|
|
81
|
+
"- $sdlc-status",
|
|
82
|
+
]
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def render_state_read_failed_recommendation(error_message: str) -> str:
|
|
87
|
+
return "\n".join(
|
|
88
|
+
[
|
|
89
|
+
"下一步推荐",
|
|
90
|
+
"- 主推荐:$sdlc-doctor-repair",
|
|
91
|
+
f"- 原因:当前状态读取失败:{error_message}",
|
|
92
|
+
"",
|
|
93
|
+
"备选指令",
|
|
94
|
+
"- $sdlc-doctor-deep",
|
|
95
|
+
"- $sdlc-status",
|
|
96
|
+
]
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def render_cli_next_recommendation() -> str:
|
|
101
|
+
try:
|
|
102
|
+
root = resolve_project_root(Path.cwd(), allow_plain_directory=True)
|
|
103
|
+
paths = build_paths(root)
|
|
104
|
+
if not paths.events_file.exists():
|
|
105
|
+
return render_uninitialized_next_recommendation()
|
|
106
|
+
state = derive_state(paths)
|
|
107
|
+
return render_next_text(paths, state)
|
|
108
|
+
except SdlcError as exc:
|
|
109
|
+
return render_state_read_failed_recommendation(exc.message)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def should_append_next_recommendation(args: argparse.Namespace, exit_code: int) -> bool:
|
|
113
|
+
if exit_code != 0:
|
|
114
|
+
return False
|
|
115
|
+
command = getattr(args, "command", None)
|
|
116
|
+
# 这些命令自己已经给出下一步,或可能处在恢复/体检场景;不要再按当前状态追加通用推荐。
|
|
117
|
+
skip_commands = {
|
|
118
|
+
"help",
|
|
119
|
+
"next",
|
|
120
|
+
"status",
|
|
121
|
+
"init",
|
|
122
|
+
"init-plain",
|
|
123
|
+
"init-basic",
|
|
124
|
+
"init-basic-plain",
|
|
125
|
+
"start",
|
|
126
|
+
"light-start",
|
|
127
|
+
"discuss",
|
|
128
|
+
"discuss-link",
|
|
129
|
+
"capture",
|
|
130
|
+
"capture-link",
|
|
131
|
+
"capture-requirement",
|
|
132
|
+
"capture-change",
|
|
133
|
+
"add",
|
|
134
|
+
"material",
|
|
135
|
+
"design",
|
|
136
|
+
"design-accept",
|
|
137
|
+
"draft",
|
|
138
|
+
"plan",
|
|
139
|
+
"plan-add-task",
|
|
140
|
+
"plan-amend-task",
|
|
141
|
+
"plan-reorder",
|
|
142
|
+
"plan-depends",
|
|
143
|
+
"plan-close",
|
|
144
|
+
"plan-priority",
|
|
145
|
+
"change",
|
|
146
|
+
"change-accept",
|
|
147
|
+
"change-capture",
|
|
148
|
+
"change-plan",
|
|
149
|
+
"task",
|
|
150
|
+
"task-done",
|
|
151
|
+
"task-restore",
|
|
152
|
+
"task-pause",
|
|
153
|
+
"fix",
|
|
154
|
+
"audit",
|
|
155
|
+
"accept",
|
|
156
|
+
"regression",
|
|
157
|
+
"backup",
|
|
158
|
+
"backup-list",
|
|
159
|
+
"backup-clean",
|
|
160
|
+
"restore",
|
|
161
|
+
"context",
|
|
162
|
+
"lessons",
|
|
163
|
+
"doctor",
|
|
164
|
+
"doctor-install",
|
|
165
|
+
"doctor-repair",
|
|
166
|
+
"doctor-deep",
|
|
167
|
+
"docs",
|
|
168
|
+
"finish",
|
|
169
|
+
"handoff",
|
|
170
|
+
"grill",
|
|
171
|
+
"agent-sync",
|
|
172
|
+
"hooks-upgrade",
|
|
173
|
+
"tasks",
|
|
174
|
+
"version",
|
|
175
|
+
}
|
|
176
|
+
return bool(command and command not in skip_commands)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def print_next_recommendation_footer() -> None:
|
|
180
|
+
print()
|
|
181
|
+
print(render_cli_next_recommendation(), end="")
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
AUTO_BACKUP_COMMANDS = {
|
|
185
|
+
"init",
|
|
186
|
+
"init-plain",
|
|
187
|
+
"init-basic",
|
|
188
|
+
"init-basic-plain",
|
|
189
|
+
"start",
|
|
190
|
+
"light-start",
|
|
191
|
+
"discuss",
|
|
192
|
+
"discuss-link",
|
|
193
|
+
"capture",
|
|
194
|
+
"capture-link",
|
|
195
|
+
"capture-requirement",
|
|
196
|
+
"capture-change",
|
|
197
|
+
"add",
|
|
198
|
+
"grill",
|
|
199
|
+
"material",
|
|
200
|
+
"design",
|
|
201
|
+
"design-accept",
|
|
202
|
+
"draft",
|
|
203
|
+
"tasks",
|
|
204
|
+
"plan",
|
|
205
|
+
"plan-add-task",
|
|
206
|
+
"plan-amend-task",
|
|
207
|
+
"plan-reorder",
|
|
208
|
+
"plan-depends",
|
|
209
|
+
"plan-close",
|
|
210
|
+
"plan-priority",
|
|
211
|
+
"change",
|
|
212
|
+
"change-accept",
|
|
213
|
+
"change-capture",
|
|
214
|
+
"change-plan",
|
|
215
|
+
"task",
|
|
216
|
+
"task-done",
|
|
217
|
+
"task-restore",
|
|
218
|
+
"task-pause",
|
|
219
|
+
"fix",
|
|
220
|
+
"audit",
|
|
221
|
+
"accept",
|
|
222
|
+
"regression",
|
|
223
|
+
"docs",
|
|
224
|
+
"finish",
|
|
225
|
+
"handoff",
|
|
226
|
+
"export",
|
|
227
|
+
"export-requirement",
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
PINNED_AUTO_BACKUP_COMMANDS = {
|
|
231
|
+
"design-accept",
|
|
232
|
+
"task-done",
|
|
233
|
+
"regression",
|
|
234
|
+
"docs",
|
|
235
|
+
"accept",
|
|
236
|
+
"finish",
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
# 大多数写命令成功后的快照已经能恢复上一步状态;只有修复类命令需要额外留下写入前状态。
|
|
240
|
+
PRE_CHANGE_AUTO_BACKUP_COMMANDS = {"doctor-repair"}
|
|
241
|
+
|
|
242
|
+
IDENTITY_CHECK_COMMANDS = {
|
|
243
|
+
*AUTO_BACKUP_COMMANDS,
|
|
244
|
+
"backup",
|
|
245
|
+
"next",
|
|
246
|
+
"status",
|
|
247
|
+
"doctor-repair",
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def should_auto_backup(args: argparse.Namespace, *, phase: str) -> bool:
|
|
252
|
+
if os.environ.get("CODEX_SDLC_DISABLE_AUTO_BACKUP") == "1":
|
|
253
|
+
return False
|
|
254
|
+
if os.environ.get("PYTEST_CURRENT_TEST") and not os.environ.get("CODEX_SDLC_BACKUP_HOME"):
|
|
255
|
+
return False
|
|
256
|
+
command = getattr(args, "command", "")
|
|
257
|
+
if phase not in {"before", "after"}:
|
|
258
|
+
return False
|
|
259
|
+
is_repair_command = command == "doctor-repair" or (command == "doctor" and getattr(args, "repair", False))
|
|
260
|
+
if phase == "before":
|
|
261
|
+
return command in PRE_CHANGE_AUTO_BACKUP_COMMANDS or is_repair_command
|
|
262
|
+
if command == "lessons":
|
|
263
|
+
return getattr(args, "lesson_command", "") in {"add", "apply", "promote", "demote", "retire"}
|
|
264
|
+
if command in AUTO_BACKUP_COMMANDS:
|
|
265
|
+
return True
|
|
266
|
+
return is_repair_command
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def run_auto_backup(args: argparse.Namespace, *, phase: str) -> None:
|
|
270
|
+
if not should_auto_backup(args, phase=phase):
|
|
271
|
+
return
|
|
272
|
+
try:
|
|
273
|
+
root = resolve_project_root(Path.cwd(), allow_plain_directory=True)
|
|
274
|
+
paths = build_paths(root)
|
|
275
|
+
if not paths.events_file.exists():
|
|
276
|
+
return
|
|
277
|
+
command = getattr(args, "command", "command")
|
|
278
|
+
result = create_backup(
|
|
279
|
+
paths,
|
|
280
|
+
label=f"auto-{command}-{phase}",
|
|
281
|
+
pinned=command in PINNED_AUTO_BACKUP_COMMANDS and phase == "after",
|
|
282
|
+
automatic=True,
|
|
283
|
+
command=command,
|
|
284
|
+
phase=phase,
|
|
285
|
+
)
|
|
286
|
+
git_result = result.get("git", {})
|
|
287
|
+
if isinstance(git_result, dict) and git_result.get("status") == "failed":
|
|
288
|
+
record_auto_backup_failure(
|
|
289
|
+
command=command,
|
|
290
|
+
phase=phase,
|
|
291
|
+
message=str(git_result.get("message") or "Git 提交失败"),
|
|
292
|
+
)
|
|
293
|
+
except Exception as exc:
|
|
294
|
+
# 自动备份不能打断用户当前命令;手动备份失败才需要显式报错。
|
|
295
|
+
record_auto_backup_failure(command=getattr(args, "command", "command"), phase=phase, message=str(exc))
|
|
296
|
+
return
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def run_identity_guard(args: argparse.Namespace) -> None:
|
|
300
|
+
command = getattr(args, "command", "")
|
|
301
|
+
if command == "context":
|
|
302
|
+
if getattr(args, "context_command", "") != "save":
|
|
303
|
+
return
|
|
304
|
+
root = resolve_project_root(Path.cwd(), allow_plain_directory=True)
|
|
305
|
+
paths = build_paths(root)
|
|
306
|
+
require_matching_sdlc_identity(paths)
|
|
307
|
+
return
|
|
308
|
+
if command == "lessons":
|
|
309
|
+
if getattr(args, "lesson_command", "") not in {"add", "apply", "promote", "demote", "retire"}:
|
|
310
|
+
return
|
|
311
|
+
root = resolve_project_root(Path.cwd(), allow_plain_directory=True)
|
|
312
|
+
paths = build_paths(root)
|
|
313
|
+
require_matching_sdlc_identity(paths)
|
|
314
|
+
return
|
|
315
|
+
if command not in IDENTITY_CHECK_COMMANDS:
|
|
316
|
+
return
|
|
317
|
+
root = resolve_project_root(Path.cwd(), allow_plain_directory=True)
|
|
318
|
+
paths = build_paths(root)
|
|
319
|
+
require_matching_sdlc_identity(paths)
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def run_start_transaction_recovery() -> None:
|
|
323
|
+
"""所有普通命令先收口建档事务,不能让身份检查或备份看到半成品。"""
|
|
324
|
+
|
|
325
|
+
from codex_sdlc.core.start_transaction import (
|
|
326
|
+
recover_incomplete_start_transactions,
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
root = resolve_project_root(Path.cwd(), allow_plain_directory=True)
|
|
330
|
+
paths = build_paths(root)
|
|
331
|
+
if not paths.sdlc_dir.is_dir():
|
|
332
|
+
return
|
|
333
|
+
recover_incomplete_start_transactions(paths)
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
337
|
+
parser = ChineseArgumentParser(
|
|
338
|
+
prog="specstamp",
|
|
339
|
+
description="SpecStamp 本机软件开发生命周期辅助工具",
|
|
340
|
+
)
|
|
341
|
+
parser.add_argument("--version", action="store_true", help="显示当前版本")
|
|
342
|
+
subparsers = parser.add_subparsers(dest="command", parser_class=ChineseArgumentParser)
|
|
343
|
+
|
|
344
|
+
help_parser = subparsers.add_parser("help", help="查看帮助")
|
|
345
|
+
help_parser.add_argument("topic", nargs="?", help="可选命令名")
|
|
346
|
+
|
|
347
|
+
version_parser = subparsers.add_parser("version", help="显示当前版本")
|
|
348
|
+
version_parser.set_defaults(func=lambda _args: print(version_text()) or 0)
|
|
349
|
+
|
|
350
|
+
init_cmd.register(subparsers)
|
|
351
|
+
agent_sync_cmd.register(subparsers)
|
|
352
|
+
hooks_cmd.register(subparsers)
|
|
353
|
+
clean_cmd.register(subparsers)
|
|
354
|
+
accept_cmd.register(subparsers)
|
|
355
|
+
backup_cmd.register(subparsers)
|
|
356
|
+
context_cmd.register(subparsers)
|
|
357
|
+
next_cmd.register(subparsers)
|
|
358
|
+
capture_cmd.register(subparsers)
|
|
359
|
+
add_cmd.register(subparsers)
|
|
360
|
+
grill_cmd.register(subparsers)
|
|
361
|
+
lessons_cmd.register(subparsers)
|
|
362
|
+
material_cmd.register(subparsers)
|
|
363
|
+
design_cmd.register(subparsers)
|
|
364
|
+
draft_cmd.register(subparsers)
|
|
365
|
+
facts_cmd.register(subparsers)
|
|
366
|
+
start_cmd.register(subparsers)
|
|
367
|
+
plan_cmd.register(subparsers)
|
|
368
|
+
regression_cmd.register(subparsers)
|
|
369
|
+
docs_cmd.register(subparsers)
|
|
370
|
+
task_cmd.register(subparsers)
|
|
371
|
+
change_cmd.register(subparsers)
|
|
372
|
+
finish_cmd.register(subparsers)
|
|
373
|
+
handoff_cmd.register(subparsers)
|
|
374
|
+
status_cmd.register(subparsers)
|
|
375
|
+
doctor_cmd.register(subparsers)
|
|
376
|
+
export_cmd.register(subparsers)
|
|
377
|
+
|
|
378
|
+
return parser
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def main(argv: list[str] | None = None) -> int:
|
|
382
|
+
parser = build_parser()
|
|
383
|
+
try:
|
|
384
|
+
args = parser.parse_args(argv)
|
|
385
|
+
|
|
386
|
+
if args.version:
|
|
387
|
+
print(version_text())
|
|
388
|
+
return 0
|
|
389
|
+
|
|
390
|
+
if getattr(args, "command", None):
|
|
391
|
+
run_start_transaction_recovery()
|
|
392
|
+
|
|
393
|
+
if args.command == "help":
|
|
394
|
+
topic = getattr(args, "topic", None)
|
|
395
|
+
if not topic:
|
|
396
|
+
parser.print_help()
|
|
397
|
+
return 0
|
|
398
|
+
nested_args = parser.parse_args([topic, "--help"])
|
|
399
|
+
return 0 if nested_args else 0
|
|
400
|
+
|
|
401
|
+
if not getattr(args, "command", None):
|
|
402
|
+
parser.print_help()
|
|
403
|
+
return 0
|
|
404
|
+
|
|
405
|
+
run_identity_guard(args)
|
|
406
|
+
run_auto_backup(args, phase="before")
|
|
407
|
+
exit_code = int(args.func(args))
|
|
408
|
+
if exit_code == 0:
|
|
409
|
+
run_auto_backup(args, phase="after")
|
|
410
|
+
if should_append_next_recommendation(args, exit_code):
|
|
411
|
+
print_next_recommendation_footer()
|
|
412
|
+
return exit_code
|
|
413
|
+
except SdlcError as exc:
|
|
414
|
+
print(f"错误:{exc.message}", file=sys.stderr)
|
|
415
|
+
return exc.exit_code
|
|
416
|
+
except KeyboardInterrupt:
|
|
417
|
+
print("已取消。", file=sys.stderr)
|
|
418
|
+
return 130
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""命令入口模块。"""
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from codex_sdlc.core.errors import SdlcError
|
|
7
|
+
from codex_sdlc.core.project import build_paths, project_lock, resolve_project_root
|
|
8
|
+
from codex_sdlc.core.state import (
|
|
9
|
+
append_event,
|
|
10
|
+
collect_docs_actions,
|
|
11
|
+
derive_state,
|
|
12
|
+
latest_unresolved_manual_pending_event,
|
|
13
|
+
now_iso,
|
|
14
|
+
refresh_materialized_state,
|
|
15
|
+
resolve_requirement,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
|
|
20
|
+
parser = subparsers.add_parser("accept", help="确认需求真正结束")
|
|
21
|
+
parser.add_argument("requirement_id", nargs="?", help="可选需求编号")
|
|
22
|
+
parser.set_defaults(func=run)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def candidate_line(requirement: dict[str, object]) -> str:
|
|
26
|
+
return f"{requirement['requirement_id']}:{requirement['title']}"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def recent_requirement(state: dict[str, object], candidates: list[dict[str, object]]) -> dict[str, object] | None:
|
|
30
|
+
candidate_map = {requirement["requirement_id"]: requirement for requirement in candidates}
|
|
31
|
+
for event in reversed(state["events"]): # type: ignore[index]
|
|
32
|
+
requirement_id = event.get("requirement_id")
|
|
33
|
+
if requirement_id in candidate_map and event.get("source") in {"sdlc-task", "sdlc-plan", "sdlc-doctor"}:
|
|
34
|
+
return candidate_map[requirement_id]
|
|
35
|
+
return None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def select_requirement_to_accept(state: dict[str, object], requirement_id: str | None) -> dict[str, object]:
|
|
39
|
+
if requirement_id:
|
|
40
|
+
return resolve_requirement(state, requirement_id)
|
|
41
|
+
|
|
42
|
+
done_candidates = [
|
|
43
|
+
requirement
|
|
44
|
+
for requirement in state["requirements"].values() # type: ignore[index]
|
|
45
|
+
if requirement["status"] == "done"
|
|
46
|
+
]
|
|
47
|
+
recent_done = recent_requirement(state, done_candidates)
|
|
48
|
+
if recent_done:
|
|
49
|
+
return recent_done
|
|
50
|
+
if len(done_candidates) == 1:
|
|
51
|
+
return done_candidates[0]
|
|
52
|
+
if len(done_candidates) > 1:
|
|
53
|
+
lines = ["多个需求都已完成,请指定要接受哪一个:"]
|
|
54
|
+
lines.extend(f"- {candidate_line(requirement)}" for requirement in done_candidates)
|
|
55
|
+
raise SdlcError("\n".join(lines), exit_code=1)
|
|
56
|
+
|
|
57
|
+
active = list(state["active_requirements"]) # type: ignore[index]
|
|
58
|
+
if len(active) == 1:
|
|
59
|
+
return active[0]
|
|
60
|
+
if active:
|
|
61
|
+
lines = ["当前没有已完成需求,且有多个活跃需求,请指定需求编号:"]
|
|
62
|
+
lines.extend(f"- {candidate_line(requirement)}" for requirement in active)
|
|
63
|
+
raise SdlcError("\n".join(lines), exit_code=1)
|
|
64
|
+
raise SdlcError("当前没有可接受的需求。", exit_code=1)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def run(args: argparse.Namespace) -> int:
|
|
68
|
+
root = resolve_project_root(Path.cwd())
|
|
69
|
+
paths = build_paths(root)
|
|
70
|
+
if not paths.events_file.exists():
|
|
71
|
+
raise SdlcError("当前项目还没初始化,请先使用 `$sdlc-init`。")
|
|
72
|
+
|
|
73
|
+
with project_lock(paths):
|
|
74
|
+
state = derive_state(paths)
|
|
75
|
+
requirement = select_requirement_to_accept(state, args.requirement_id)
|
|
76
|
+
unfinished = [
|
|
77
|
+
task
|
|
78
|
+
for task in requirement["tasks"] # type: ignore[index]
|
|
79
|
+
if task["status"] not in {"done", "closed"}
|
|
80
|
+
]
|
|
81
|
+
if unfinished:
|
|
82
|
+
lines = [f"{requirement['requirement_id']} 还有未完成任务,不能接受需求:"]
|
|
83
|
+
lines.extend(f"- {task['task_id']} [{task['status']}] {task['title']}" for task in unfinished)
|
|
84
|
+
raise SdlcError("\n".join(lines), exit_code=1)
|
|
85
|
+
pending_manual = latest_unresolved_manual_pending_event(state, requirement)
|
|
86
|
+
if pending_manual is not None:
|
|
87
|
+
payload = pending_manual.get("payload", {}) if isinstance(pending_manual.get("payload", {}), dict) else {}
|
|
88
|
+
manual_items = payload.get("manual_items", []) if isinstance(payload, dict) else []
|
|
89
|
+
lines = [f"{requirement['requirement_id']} 还有人工或模拟器验收项没确认,不能接受需求:"]
|
|
90
|
+
for item in manual_items[:8]:
|
|
91
|
+
if not isinstance(item, dict):
|
|
92
|
+
continue
|
|
93
|
+
case_ids = "、".join(str(case_id) for case_id in item.get("case_ids", []) if str(case_id).strip())
|
|
94
|
+
title = str(item.get("task_title", "")).strip()
|
|
95
|
+
task_id = str(item.get("task_id", "")).strip()
|
|
96
|
+
lines.append(f"- {task_id} {title}" + (f";待确认测试:{case_ids}" if case_ids else ""))
|
|
97
|
+
lines.append(
|
|
98
|
+
"请先完成上面的人工/视觉验收,再写入验证记录,"
|
|
99
|
+
"例如 `$sdlc-task-done REQ-001 T-001 --verify \"人工验收通过:...\"`。"
|
|
100
|
+
)
|
|
101
|
+
raise SdlcError("\n".join(lines), exit_code=1)
|
|
102
|
+
|
|
103
|
+
append_event(
|
|
104
|
+
paths,
|
|
105
|
+
event_type="requirement_accepted",
|
|
106
|
+
source="sdlc-accept",
|
|
107
|
+
summary=f"接受需求 {requirement['requirement_id']}",
|
|
108
|
+
requirement_id=requirement["requirement_id"],
|
|
109
|
+
payload={"accepted_at": now_iso()},
|
|
110
|
+
)
|
|
111
|
+
refreshed_state = refresh_materialized_state(paths)
|
|
112
|
+
docs_actions = [
|
|
113
|
+
action
|
|
114
|
+
for action in collect_docs_actions(refreshed_state, paths.root)
|
|
115
|
+
if action["command"].startswith(f"$sdlc-docs {requirement['requirement_id']}")
|
|
116
|
+
]
|
|
117
|
+
next_command = docs_actions[0]["command"] if docs_actions else "$sdlc-finish"
|
|
118
|
+
|
|
119
|
+
print(f"已接受需求:{requirement['requirement_id']}")
|
|
120
|
+
print("状态:accepted")
|
|
121
|
+
print(f"下一步建议:{next_command}")
|
|
122
|
+
return 0
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from codex_sdlc.commands.plan_cmd import (
|
|
7
|
+
added_task_change_redirects,
|
|
8
|
+
append_extra_tasks,
|
|
9
|
+
build_initial_tasks,
|
|
10
|
+
ensure_mutable_task_plan_contract,
|
|
11
|
+
print_current_next_suggestion,
|
|
12
|
+
refresh_task_coverage_tests,
|
|
13
|
+
refresh_planning_state,
|
|
14
|
+
restore_existing_coverage_points,
|
|
15
|
+
)
|
|
16
|
+
from codex_sdlc.core.errors import SdlcError
|
|
17
|
+
from codex_sdlc.core.project import build_paths, project_lock, resolve_project_root
|
|
18
|
+
from codex_sdlc.core.state import (
|
|
19
|
+
append_event,
|
|
20
|
+
bind_tasks_to_current_contract,
|
|
21
|
+
derive_state,
|
|
22
|
+
resolve_requirement,
|
|
23
|
+
)
|
|
24
|
+
from codex_sdlc.core.task_quality import analyze_task_quality
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
|
|
28
|
+
parser = subparsers.add_parser("add", help="按模型给出的显式类型追加内容")
|
|
29
|
+
parser.add_argument("requirement_id", help="需求编号")
|
|
30
|
+
parser.add_argument("description", help="要追加的内容")
|
|
31
|
+
parser.add_argument("--kind", choices=["bug", "task", "change"], required=True, help="模型已确认的内容类型")
|
|
32
|
+
parser.add_argument("--source-task", default="", help="bug 修复对应的原任务编号;--kind bug 时必填")
|
|
33
|
+
parser.add_argument("--coverage", action="append", default=[], help="任务显式覆盖的 FR 编号,可重复传入")
|
|
34
|
+
parser.add_argument("--acceptance", action="append", default=[], help="验收或回归要求,可重复传入")
|
|
35
|
+
parser.add_argument("--task", action="append", default=[], help="变更的结构化任务,可重复传入;格式为“标题||摘要||FR-001,FR-002”")
|
|
36
|
+
parser.set_defaults(func=run)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def run(args: argparse.Namespace) -> int:
|
|
40
|
+
if not str(args.description).strip():
|
|
41
|
+
raise SdlcError("追加内容不能为空。", exit_code=1)
|
|
42
|
+
|
|
43
|
+
kind = str(args.kind)
|
|
44
|
+
if kind == "bug":
|
|
45
|
+
raise SdlcError(
|
|
46
|
+
"add 不再创建修复任务;当前任务反馈请使用 `$sdlc-task-restore`,历史已完成任务问题请使用 `$sdlc-fix`。",
|
|
47
|
+
exit_code=1,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
if kind == "task":
|
|
51
|
+
return run_plain_task_autoflow(args)
|
|
52
|
+
|
|
53
|
+
raise SdlcError(
|
|
54
|
+
"add 不再登记自然语言变更;请先使用 `codex-sdlc change-create REQ-xxx --request-key <稳定请求键>` 创建结构化 CHG 工作区。",
|
|
55
|
+
exit_code=1,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def append_plain_task(
|
|
60
|
+
paths,
|
|
61
|
+
state: dict[str, object],
|
|
62
|
+
requirement: dict[str, object],
|
|
63
|
+
description: str,
|
|
64
|
+
*,
|
|
65
|
+
allow_broad_scope_tasks: bool = False,
|
|
66
|
+
coverage_points: list[str] | None = None,
|
|
67
|
+
) -> tuple[str, dict[str, object] | None, str | None, str]:
|
|
68
|
+
ensure_mutable_task_plan_contract(paths, requirement)
|
|
69
|
+
tasks = build_initial_tasks(requirement)
|
|
70
|
+
saved_coverage = {
|
|
71
|
+
str(task.get("task_id", "")): [str(item) for item in (task.get("coverage_points") or []) if str(item).strip()]
|
|
72
|
+
for task in tasks
|
|
73
|
+
}
|
|
74
|
+
tasks, created_tasks = append_extra_tasks(tasks, [description])
|
|
75
|
+
if created_tasks:
|
|
76
|
+
created_tasks[0]["coverage_points"] = list(dict.fromkeys(str(item).strip() for item in (coverage_points or []) if str(item).strip()))
|
|
77
|
+
bind_tasks_to_current_contract(requirement, tasks, force=True)
|
|
78
|
+
restore_existing_coverage_points(tasks, saved_coverage)
|
|
79
|
+
redirects = added_task_change_redirects(requirement, created_tasks, allow_broad_scope_tasks=allow_broad_scope_tasks)
|
|
80
|
+
if redirects:
|
|
81
|
+
raise SdlcError(
|
|
82
|
+
"--kind task 需要显式的任务覆盖关系:\n"
|
|
83
|
+
+ "\n".join(f"- {task['title']}:{reason}" for task, reason in redirects)
|
|
84
|
+
+ "\n请通过 --coverage 传入准确的 FR 编号,CLI 不会把它改猜为需求变更。",
|
|
85
|
+
exit_code=1,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
refresh_task_coverage_tests(requirement, tasks)
|
|
89
|
+
quality_report = analyze_task_quality(tasks)
|
|
90
|
+
append_event(
|
|
91
|
+
paths,
|
|
92
|
+
event_type="plan_updated",
|
|
93
|
+
source="sdlc-add",
|
|
94
|
+
summary=f"追加普通任务 {created_tasks[0]['task_id']}",
|
|
95
|
+
requirement_id=requirement["requirement_id"],
|
|
96
|
+
task_id=created_tasks[0]["task_id"],
|
|
97
|
+
payload={
|
|
98
|
+
"tasks": tasks,
|
|
99
|
+
"priority": requirement.get("priority", "normal"),
|
|
100
|
+
"blocked_reason": "",
|
|
101
|
+
"resolved_change_ids": [],
|
|
102
|
+
"task_quality": quality_report,
|
|
103
|
+
},
|
|
104
|
+
)
|
|
105
|
+
refresh_planning_state(paths, str(requirement["requirement_id"]))
|
|
106
|
+
return "task", created_tasks[0], None, ""
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def run_plain_task_autoflow(args: argparse.Namespace) -> int:
|
|
110
|
+
root = resolve_project_root(Path.cwd())
|
|
111
|
+
paths = build_paths(root)
|
|
112
|
+
if not paths.events_file.exists():
|
|
113
|
+
raise SdlcError("当前项目还没初始化,请先使用 `$sdlc-init`。")
|
|
114
|
+
|
|
115
|
+
description = str(args.description).strip()
|
|
116
|
+
with project_lock(paths):
|
|
117
|
+
state = derive_state(paths)
|
|
118
|
+
requirement = resolve_requirement(state, args.requirement_id)
|
|
119
|
+
_result_kind, created_task, _change_id, _redirect_reason = append_plain_task(
|
|
120
|
+
paths,
|
|
121
|
+
state,
|
|
122
|
+
requirement,
|
|
123
|
+
description,
|
|
124
|
+
allow_broad_scope_tasks=False,
|
|
125
|
+
coverage_points=list(getattr(args, "coverage", []) or []),
|
|
126
|
+
)
|
|
127
|
+
refreshed_state = derive_state(paths)
|
|
128
|
+
refreshed_requirement = resolve_requirement(refreshed_state, args.requirement_id)
|
|
129
|
+
task_id = str(created_task["task_id"]) if created_task else ""
|
|
130
|
+
|
|
131
|
+
print("显式类型:已有需求下的普通任务")
|
|
132
|
+
print(f"已自动追加任务:{refreshed_requirement['requirement_id']} / {task_id}")
|
|
133
|
+
print("任务已经加入结构化任务计划,覆盖关系已同步。")
|
|
134
|
+
print_current_next_suggestion(paths)
|
|
135
|
+
return 0
|