agent-project-sdlc 0.1.24 → 0.1.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +35 -10
  2. package/assets/agents/AGENTS_CORE.md +14 -9
  3. package/assets/docs/README.md +64 -11
  4. package/assets/make/sdlc-harness.mk +5 -1
  5. package/assets/policies/allowed_paths.yaml +9 -0
  6. package/assets/policies/gates.yaml +6 -0
  7. package/assets/policies/phase_contracts.yaml +49 -0
  8. package/assets/skills/pjsdlc_architect_design/SKILL.md +14 -8
  9. package/assets/skills/pjsdlc_dev_sprint/SKILL.md +8 -3
  10. package/assets/skills/pjsdlc_implementation_doc/SKILL.md +9 -4
  11. package/assets/skills/pjsdlc_manager/SKILL.md +17 -16
  12. package/assets/skills/pjsdlc_reviewer/SKILL.md +6 -1
  13. package/assets/skills/pjsdlc_rfc_recalibrate/SKILL.md +8 -5
  14. package/assets/skills/pjsdlc_tester/SKILL.md +12 -4
  15. package/assets/skills/pjsdlc_uiux_design/SKILL.md +76 -0
  16. package/assets/templates/PLAN_TEMPLATE.yaml +4 -0
  17. package/assets/templates/REVIEW_TEMPLATE.md +14 -4
  18. package/assets/templates/RFC_TEMPLATE.md +13 -5
  19. package/assets/templates/TECH_DESIGN_TEMPLATE.md +18 -10
  20. package/assets/templates/TEST_CASES_TEMPLATE.md +5 -3
  21. package/assets/templates/TEST_STRATEGY_TEMPLATE.md +4 -0
  22. package/assets/templates/UI_UX_DESIGN_TEMPLATE.md +67 -0
  23. package/assets/tools/harness_utils.py +92 -18
  24. package/assets/tools/transition.py +2 -1
  25. package/assets/tools/validate_allowed_paths.py +2 -2
  26. package/assets/tools/validate_design.py +56 -3
  27. package/assets/tools/validate_dev_state.py +1 -1
  28. package/assets/tools/validate_harness.py +17 -14
  29. package/assets/tools/validate_plan_draft.py +1 -1
  30. package/assets/tools/validate_prompt_language.py +17 -17
  31. package/assets/tools/validate_rfc.py +31 -0
  32. package/assets/tools/validate_test_plan.py +118 -1
  33. package/assets/tools/validate_uiux_design.py +101 -0
  34. package/dist/commands/index.js +5 -1
  35. package/dist/commands/inspect-workflow.d.ts +1 -0
  36. package/dist/commands/inspect-workflow.js +71 -0
  37. package/dist/lib/harness-root.js +5 -5
  38. package/dist/lib/init.js +7 -3
  39. package/dist/lib/validators.js +341 -27
  40. package/dist/lib/workflow-inspector.d.ts +35 -0
  41. package/dist/lib/workflow-inspector.js +340 -0
  42. package/package.json +2 -1
@@ -3,6 +3,8 @@
3
3
  ## 1. Review 范围
4
4
 
5
5
  - PRD:
6
+ - UI/UX:
7
+ - DESIGN.md:
6
8
  - 技术方案(Technical design):
7
9
  - 实现文档(Implementation docs):
8
10
  - Diff/commit:
@@ -21,11 +23,19 @@
21
23
 
22
24
  -
23
25
 
24
- ## 5. Test Gaps(测试缺口)
26
+ ## 5. UX / Design Conformance(体验与设计一致性)
27
+
28
+ - Screen contracts:
29
+ - Interaction states:
30
+ - Responsive / accessibility:
31
+ - DESIGN.md token usage:
32
+ - Blocking gaps before TESTING:
33
+
34
+ ## 6. Test Gaps(测试缺口)
25
35
 
26
36
  -
27
37
 
28
- ## 6. Runnable Entry/Exit Readiness(可运行入口/出口)
38
+ ## 7. Runnable Entry/Exit Readiness(可运行入口/出口)
29
39
 
30
40
  - Entry points:
31
41
  - Exit / side effects:
@@ -37,7 +47,7 @@
37
47
  - Testing Handoff Contract:
38
48
  - Blocking gaps before TESTING:
39
49
 
40
- ## 7. Application Readiness Checklist(应用就绪检查)
50
+ ## 8. Application Readiness Checklist(应用就绪检查)
41
51
 
42
52
  - Runnable Entry: `PASS` / `BLOCKED`
43
53
  - Observable Exit: `PASS` / `BLOCKED`
@@ -45,7 +55,7 @@
45
55
  - Config Contract: `PASS` / `BLOCKED`
46
56
  - Testing Handoff Readiness: `PASS` / `BLOCKED`
47
57
 
48
- ## 8. Gate Result(阶段结论)
58
+ ## 9. Gate Result(阶段结论)
49
59
 
50
60
  - Decision: `PASS` / `BLOCKED`
51
61
  - Required before testing:
@@ -23,22 +23,30 @@
23
23
  |---|---|---|
24
24
  | | | low/medium/high |
25
25
 
26
- ## 5. Acceptance Criteria
26
+ ## 5. UI/UX Impact(体验影响)
27
+
28
+ - Reviewed experience docs:
29
+ - DESIGN.md impact:
30
+ - Superseded screen contracts: none
31
+ - Retained UX facts:
32
+ - Reason:
33
+
34
+ ## 6. Acceptance Criteria
27
35
 
28
36
  - [ ]
29
37
 
30
- ## 6. Regression Requirements(回归要求)
38
+ ## 7. Regression Requirements(回归要求)
31
39
 
32
40
  - [ ]
33
41
 
34
- ## 7. Test Fact Source Impact(测试事实源影响)
42
+ ## 8. Test Fact Source Impact(测试事实源影响)
35
43
 
36
44
  - Reviewed test docs:
37
45
  - Superseded test docs: none
38
46
  - Retained test docs:
39
47
  - Reason:
40
48
 
41
- ## 8. Development Self-Test Impact(开发自测影响)
49
+ ## 9. Development Self-Test Impact(开发自测影响)
42
50
 
43
51
  - Entry/exit impact:
44
52
  - Runtime / target environment impact:
@@ -49,6 +57,6 @@
49
57
  - Module key test path impact:
50
58
  - Review / Testing handoff impact:
51
59
 
52
- ## 9. Status
60
+ ## 10. Status
53
61
 
54
62
  - Status: DRAFT
@@ -11,7 +11,15 @@
11
11
  - 相关 APIs(Related APIs):
12
12
  - 相关数据(Related data):
13
13
 
14
- ## 3. 方案架构
14
+ ## 3. Experience Input Review(体验输入审查)
15
+
16
+ - UI/UX slices:
17
+ - DESIGN.md:
18
+ - Screen contracts consumed:
19
+ - Handoff matrix consumed:
20
+ - Not applicable reason:
21
+
22
+ ## 4. 方案架构
15
23
 
16
24
  - 领域边界(Domain boundary):
17
25
  - 主要组件(Main components):
@@ -25,27 +33,27 @@ Input
25
33
  -> Output
26
34
  ```
27
35
 
28
- ## 4. 接口契约(Interface Contract)
36
+ ## 5. 接口契约(Interface Contract)
29
37
 
30
38
  | 接口(Interface) | 方法/事件(Method/Event) | 请求(Request) | 响应(Response) | 错误(Errors) |
31
39
  |---|---|---|---|---|
32
40
  | | | | | |
33
41
 
34
- ## 5. 数据模型(Data Model)
42
+ ## 6. 数据模型(Data Model)
35
43
 
36
44
  | 实体(Entity) | 字段(Fields) | 负责人(Owner) | 备注(Notes) |
37
45
  |---|---|---|---|
38
46
  | | | | |
39
47
 
40
- ## 6. 任务拆分(Task Breakdown)
48
+ ## 7. 任务拆分(Task Breakdown)
41
49
 
42
- | Task ID | 标题(Title) | Allowed Paths | Required Gates | Implementation Doc |
43
- |---|---|---|---|---|
44
- | TASK-001 | | `src/**`, `tests/**` | `make lint`, `make test-current-domain` | `.docs/04_implementation/...` |
50
+ | Task ID | 标题(Title) | UI/UX refs | Design system | Allowed Paths | Required Gates | Implementation Doc |
51
+ |---|---|---|---|---|---|---|
52
+ | TASK-001 | | `.docs/02_experience/...` | `DESIGN.md` | `src/**`, `tests/**` | `make lint`, `make test-current-domain` | `.docs/04_implementation/...` |
45
53
 
46
54
  `Implementation Doc` 应指向模块、子系统或核心数据流级文档;多个 task 可以更新同一份文档,task id 和 commit 记录在该文档的 provenance / Change Log 中。
47
55
 
48
- ## 7. Development Self-Test Contract(开发自测合同,待执行)
56
+ ## 8. Development Self-Test Contract(开发自测合同,待执行)
49
57
 
50
58
  > service / agent / runtime / worker / frontend app / provider-live / API / CLI 等可运行边界的开发 task 必填,并同步写入 `plan.draft.yaml.tasks[].self_test_contract`。
51
59
 
@@ -62,12 +70,12 @@ Input
62
70
 
63
71
  - Not applicable reason:
64
72
 
65
- ## 8. 风险与缓解
73
+ ## 9. 风险与缓解
66
74
 
67
75
  | 风险(Risk) | 等级(Level) | 缓解措施(Mitigation) |
68
76
  |---|---|---|
69
77
  | | P1 | |
70
78
 
71
- ## 9. 需要关注的方案偏移
79
+ ## 10. 需要关注的方案偏移
72
80
 
73
81
  -
@@ -9,17 +9,19 @@
9
9
 
10
10
  ## 2. Cases(用例)
11
11
 
12
- | ID | Requirement | Preconditions | Steps | Expected Result |
13
- |---|---|---|---|---|
14
- | TC-001 | | | | |
12
+ | Case ID | Requirement / Risk Ref | Type | Priority | Runnable Entry | Preconditions | Steps | Expected Exit | Evidence Pointer |
13
+ |---|---|---|---|---|---|---|---|---|
14
+ | TC-001 | | unit/integration/e2e/regression/smoke/manual | P0/P1/P2 | | | | | |
15
15
 
16
16
  ## 3. Traceability(追溯)
17
17
 
18
18
  - PRD acceptance criteria:
19
19
  - Review findings:
20
20
  - Risk paths:
21
+ - TEST_REPORT references:
21
22
 
22
23
  ## 4. Notes(备注)
23
24
 
24
25
  - Fixture/live boundary:
25
26
  - Data setup:
27
+ - Case body only records test design. Execution result, regression evidence, bugfix route and final decision belong in `TEST_REPORT.md`.
@@ -5,6 +5,8 @@
5
5
  - In scope:
6
6
  - Out of scope:
7
7
  - Target release/module:
8
+ - UI/UX handoff refs:
9
+ - DESIGN.md:
8
10
 
9
11
  ## 2. Environment(环境)
10
12
 
@@ -12,6 +14,7 @@
12
14
  - Expected exits / side effects:
13
15
  - Config contract:
14
16
  - Fixture/live boundary:
17
+ - Screen states / responsive / a11y environment:
15
18
 
16
19
  ## 3. Priority(优先级)
17
20
 
@@ -25,3 +28,4 @@
25
28
  - Manual checks:
26
29
  - Regression gates:
27
30
  - Blocker handling:
31
+ - UI/UX contract coverage:
@@ -0,0 +1,67 @@
1
+ # [Feature/Capability] UI/UX Design
2
+
3
+ ## 1. PRD refs and Requirement IDs
4
+
5
+ - PRD:
6
+ - Requirement IDs:
7
+
8
+ ## 2. Applicability
9
+
10
+ - Applicability: `visual_ui | cli_or_api_experience | not_applicable`
11
+ - N/A reason:
12
+
13
+ ## 3. User journeys
14
+
15
+ | Journey ID | User / Role | Goal | Entry | Success exit | Failure / recovery |
16
+ |---|---|---|---|---|---|
17
+ | UXJ-001 | | | | | |
18
+
19
+ ## 4. Information architecture / routes / screens
20
+
21
+ | Surface | Route / Entry | Screen ID | Purpose | Primary user action |
22
+ |---|---|---|---|---|
23
+ | web / mobile / CLI / API / operator | | SCR-001 | | |
24
+
25
+ ## 5. Screen contracts
26
+
27
+ | Screen ID | Requirement refs | Entry / route | Loading | Empty | Error | Success | Permission | Primary actions | Validation rules |
28
+ |---|---|---|---|---|---|---|---|---|---|
29
+ | SCR-001 | | | | | | | | | |
30
+
31
+ ### Responsive and accessibility acceptance
32
+
33
+ - Breakpoints / form factors:
34
+ - Keyboard / focus behavior:
35
+ - Touch target behavior:
36
+ - Contrast / readable states:
37
+ - Assistive text / labels:
38
+
39
+ ## 6. Component and interaction contracts
40
+
41
+ | Component / Pattern | States | Interaction behavior | Feedback | Notes |
42
+ |---|---|---|---|---|
43
+ | navigation / form / table / list / modal / toast | default / hover / focus / disabled / error / success | | | |
44
+
45
+ ## 7. Design system reference
46
+
47
+ - DESIGN.md: `DESIGN.md | not_applicable`
48
+ - Token / component notes:
49
+ - Known deviations:
50
+
51
+ ## 8. Handoff matrix
52
+
53
+ | Requirement | Screen / state | Component / interaction | Acceptance / test seed |
54
+ |---|---|---|---|
55
+ | | | | |
56
+
57
+ ## 9. Open Questions / Out of Scope
58
+
59
+ ### Open Questions
60
+
61
+ | Question | Owner | Status |
62
+ |---|---|---|
63
+ | | | open |
64
+
65
+ ### Out of Scope
66
+
67
+ - TBD
@@ -12,6 +12,8 @@ from typing import Any
12
12
 
13
13
 
14
14
  ROOT = Path(__file__).resolve().parents[1]
15
+ DEFAULT_HARNESS_ROOT = ".agent"
16
+ HARNESS_JSON_CONFIG_PATH = "sdlc-harness.config.json"
15
17
 
16
18
  TASK_STATUSES = {
17
19
  "pending",
@@ -28,6 +30,7 @@ PARALLEL_TRIGGERS = {"user_requested", "workflow_default"}
28
30
  PARALLEL_RUNTIME_PROVIDERS = {"codex_native_subagents", "user_orchestrated", "codex_exec_worktree"}
29
31
  PARALLEL_ALLOWED_PHASES = {
30
32
  "REQUIREMENT_GATHERING",
33
+ "UI_UX_DESIGNING",
31
34
  "ARCHITECTING",
32
35
  "SPRINTING",
33
36
  "REVIEWING",
@@ -35,7 +38,7 @@ PARALLEL_ALLOWED_PHASES = {
35
38
  "RELEASING",
36
39
  "RFC_RECALIBRATION",
37
40
  }
38
- PARALLEL_READ_ONLY_PHASES = {"REQUIREMENT_GATHERING", "ARCHITECTING", "REVIEWING", "RELEASING", "RFC_RECALIBRATION"}
41
+ PARALLEL_READ_ONLY_PHASES = {"REQUIREMENT_GATHERING", "UI_UX_DESIGNING", "ARCHITECTING", "REVIEWING", "RELEASING", "RFC_RECALIBRATION"}
39
42
  PARALLEL_PROTECTED_WRITE_PATTERNS = {
40
43
  ".codex/state/**",
41
44
  "<harnessRoot>/state/**",
@@ -48,6 +51,7 @@ PARALLEL_PROTECTED_WRITE_PATTERNS = {
48
51
  TASK_ID_PATTERN = re.compile(r"^[A-Z]+-(\d+)$")
49
52
  TASK_PHASES = {
50
53
  "REQUIREMENT_GATHERING",
54
+ "UI_UX_DESIGNING",
51
55
  "ARCHITECTING",
52
56
  "SPRINTING",
53
57
  "REVIEWING",
@@ -68,7 +72,67 @@ def repo_path(relative: str | Path) -> Path:
68
72
  return ROOT / relative
69
73
 
70
74
 
75
+ def normalize_harness_folder_name(value: str) -> str:
76
+ normalized = value.strip().replace("\\", "/").rstrip("/")
77
+ if not normalized or normalized in {".", ".."}:
78
+ raise HarnessError("harnessFolderName must be a non-empty relative directory")
79
+ if Path(normalized).is_absolute() or ".." in normalized.split("/"):
80
+ raise HarnessError("harnessFolderName must not be absolute or contain '..'")
81
+ return normalized
82
+
83
+
84
+ def folder_name_from_object(value: Any) -> str | None:
85
+ if not isinstance(value, dict):
86
+ return None
87
+ folder_name = value.get("harnessFolderName") or value.get("harnessFloderName")
88
+ if isinstance(folder_name, str) and folder_name.strip():
89
+ return folder_name
90
+ return None
91
+
92
+
93
+ def read_json_config(relative: str) -> Any:
94
+ path = repo_path(relative)
95
+ if not path.exists():
96
+ return None
97
+ return json.loads(path.read_text(encoding="utf-8"))
98
+
99
+
100
+ def harness_root() -> str:
101
+ package_json = read_json_config("package.json")
102
+ package_config = package_json.get("sdlcHarness") if isinstance(package_json, dict) else None
103
+ package_value = folder_name_from_object(package_config)
104
+ if package_value:
105
+ return normalize_harness_folder_name(package_value)
106
+
107
+ explicit_config = read_json_config(HARNESS_JSON_CONFIG_PATH)
108
+ explicit_value = folder_name_from_object(explicit_config)
109
+ if explicit_value:
110
+ return normalize_harness_folder_name(explicit_value)
111
+
112
+ return DEFAULT_HARNESS_ROOT
113
+
114
+
115
+ def harness_path(*segments: str) -> str:
116
+ return (Path(harness_root()).joinpath(*segments)).as_posix()
117
+
118
+
119
+ def resolve_harness_relative(relative: str | Path) -> str:
120
+ value = str(relative).replace("\\", "/")
121
+ root = harness_root()
122
+ if value == "<harnessRoot>":
123
+ return root
124
+ if value.startswith("<harnessRoot>/"):
125
+ return f"{root}/{value.removeprefix('<harnessRoot>/')}"
126
+ if root != ".codex":
127
+ if value == ".codex":
128
+ return root
129
+ if value.startswith(".codex/"):
130
+ return f"{root}/{value.removeprefix('.codex/')}"
131
+ return value
132
+
133
+
71
134
  def read_text(relative: str | Path) -> str:
135
+ relative = resolve_harness_relative(relative)
72
136
  path = repo_path(relative)
73
137
  if not path.exists():
74
138
  raise HarnessError(f"Missing required file: {relative}")
@@ -76,6 +140,7 @@ def read_text(relative: str | Path) -> str:
76
140
 
77
141
 
78
142
  def load_yaml(relative: str | Path) -> Any:
143
+ relative = resolve_harness_relative(relative)
79
144
  path = repo_path(relative)
80
145
  if not path.exists():
81
146
  raise HarnessError(f"Missing required YAML file: {relative}")
@@ -112,6 +177,7 @@ def parse_yaml_text(text: str) -> Any:
112
177
 
113
178
 
114
179
  def dump_yaml(data: Any, relative: str | Path) -> None:
180
+ relative = resolve_harness_relative(relative)
115
181
  path = repo_path(relative)
116
182
  path.write_text(to_simple_yaml(data).rstrip() + "\n", encoding="utf-8")
117
183
 
@@ -328,7 +394,8 @@ def require(condition: Any, message: str) -> None:
328
394
 
329
395
  def require_paths(paths: list[str]) -> None:
330
396
  for relative in paths:
331
- require(repo_path(relative).exists(), f"Missing required path: {relative}")
397
+ resolved = resolve_harness_relative(relative)
398
+ require(repo_path(resolved).exists(), f"Missing required path: {resolved}")
332
399
 
333
400
 
334
401
  def combined_text(paths: list[Path]) -> str:
@@ -842,13 +909,13 @@ def is_testing_runtime_boundary_change(path: str) -> bool:
842
909
 
843
910
 
844
911
  def load_lifecycle() -> dict[str, Any]:
845
- data = load_yaml(".codex/state/lifecycle.yaml")
912
+ data = load_yaml(harness_path("state", "lifecycle.yaml"))
846
913
  require(isinstance(data, dict), "lifecycle.yaml must be a mapping")
847
914
  return data
848
915
 
849
916
 
850
917
  def load_phase_contract_data() -> dict[str, Any]:
851
- data = load_yaml(".codex/pjsdlc_managed/policies/phase_contracts.yaml")
918
+ data = load_yaml(harness_path("pjsdlc_managed", "policies", "phase_contracts.yaml"))
852
919
  require(isinstance(data, dict) and isinstance(data.get("phases"), dict), "phase_contracts.yaml must contain phases")
853
920
  return data
854
921
 
@@ -1019,11 +1086,12 @@ def phase_transition_contract_errors(contract_data: dict[str, Any], require_tran
1019
1086
  return errors
1020
1087
 
1021
1088
 
1022
- def load_plan(path: str = ".codex/state/plan.yaml") -> dict[str, Any]:
1023
- data = load_yaml(path)
1024
- require(isinstance(data, dict), f"{path} must be a mapping")
1089
+ def load_plan(path: str | None = None) -> dict[str, Any]:
1090
+ plan_path = path or harness_path("state", "plan.yaml")
1091
+ data = load_yaml(plan_path)
1092
+ require(isinstance(data, dict), f"{resolve_harness_relative(plan_path)} must be a mapping")
1025
1093
  tasks = data.get("tasks", [])
1026
- require(isinstance(tasks, list), f"{path} must contain a tasks list")
1094
+ require(isinstance(tasks, list), f"{resolve_harness_relative(plan_path)} must contain a tasks list")
1027
1095
  return data
1028
1096
 
1029
1097
 
@@ -1172,7 +1240,7 @@ def validate_parallel_execution_contract(data: dict[str, Any], current_phase: st
1172
1240
  require("linked_task_id" not in contract, "parallel_execution must not define linked_task_id; use plan.yaml current_task_id")
1173
1241
  require(
1174
1242
  current_phase in PARALLEL_ALLOWED_PHASES,
1175
- "parallel_execution is only supported during REQUIREMENT_GATHERING, ARCHITECTING, SPRINTING, REVIEWING, TESTING, RELEASING, or RFC_RECALIBRATION",
1243
+ "parallel_execution is only supported during REQUIREMENT_GATHERING, UI_UX_DESIGNING, ARCHITECTING, SPRINTING, REVIEWING, TESTING, RELEASING, or RFC_RECALIBRATION",
1176
1244
  )
1177
1245
  require(contract.get("coordinator") == "main_agent", 'parallel_execution.coordinator must be "main_agent"')
1178
1246
 
@@ -1245,28 +1313,34 @@ def validate_parallel_worker_path_lock(data: dict[str, Any], worker: dict[str, A
1245
1313
  require(not glob_patterns_overlap(owned, forbidden), f"{prefix}.owned_paths must not overlap forbidden paths: {owned} vs {forbidden}")
1246
1314
 
1247
1315
 
1248
- def glob_prefix(pattern: str) -> str:
1249
- normalized = pattern.replace("\\", "/").replace("<harnessRoot>", ".codex")
1316
+ def normalize_harness_pattern(pattern: str, root: str | None = None) -> str:
1317
+ actual_root = root or harness_root()
1318
+ return pattern.replace("\\", "/").replace("<harnessRoot>", actual_root)
1319
+
1320
+
1321
+ def glob_prefix(pattern: str, root: str | None = None) -> str:
1322
+ normalized = normalize_harness_pattern(pattern, root)
1250
1323
  wildcard_positions = [pos for pos in (normalized.find("*"), normalized.find("["), normalized.find("?")) if pos >= 0]
1251
1324
  if wildcard_positions:
1252
1325
  normalized = normalized[: min(wildcard_positions)]
1253
1326
  return normalized.rstrip("/")
1254
1327
 
1255
1328
 
1256
- def glob_patterns_overlap(left: str, right: str) -> bool:
1257
- left_clean = left.replace("\\", "/").replace("<harnessRoot>", ".codex")
1258
- right_clean = right.replace("\\", "/").replace("<harnessRoot>", ".codex")
1329
+ def glob_patterns_overlap(left: str, right: str, root: str | None = None) -> bool:
1330
+ left_clean = normalize_harness_pattern(left, root)
1331
+ right_clean = normalize_harness_pattern(right, root)
1259
1332
  if fnmatch.fnmatch(left_clean, right_clean) or fnmatch.fnmatch(right_clean, left_clean):
1260
1333
  return True
1261
- left_prefix = glob_prefix(left_clean)
1262
- right_prefix = glob_prefix(right_clean)
1334
+ left_prefix = glob_prefix(left_clean, root)
1335
+ right_prefix = glob_prefix(right_clean, root)
1263
1336
  if not left_prefix or not right_prefix:
1264
1337
  return left_prefix == right_prefix
1265
1338
  return left_prefix.startswith(right_prefix + "/") or right_prefix.startswith(left_prefix + "/") or left_prefix == right_prefix
1266
1339
 
1267
1340
 
1268
- def expand_harness_root(patterns: list[str], root: str = ".codex") -> list[str]:
1269
- return [str(pattern).replace("<harnessRoot>", root) for pattern in patterns]
1341
+ def expand_harness_root(patterns: list[str], root: str | None = None) -> list[str]:
1342
+ actual_root = root or harness_root()
1343
+ return [str(pattern).replace("<harnessRoot>", actual_root) for pattern in patterns]
1270
1344
 
1271
1345
 
1272
1346
  def task_by_id(plan_data: dict[str, Any], task_id: str) -> dict[str, Any] | None:
@@ -2,6 +2,7 @@
2
2
  from harness_utils import (
3
3
  dump_yaml,
4
4
  find_phase_transition,
5
+ harness_path,
5
6
  load_lifecycle,
6
7
  load_phase_contract_data,
7
8
  make_arg_parser,
@@ -51,7 +52,7 @@ def main() -> None:
51
52
  str(lifecycle.get("suspended_phase") or ""),
52
53
  )
53
54
 
54
- dump_yaml(lifecycle, ".codex/state/lifecycle.yaml")
55
+ dump_yaml(lifecycle, harness_path("state", "lifecycle.yaml"))
55
56
  print(f"Transitioned {current} -> {target}")
56
57
  if args.reason:
57
58
  print(f"Note: {args.reason}")
@@ -18,7 +18,7 @@ def main() -> None:
18
18
  tasks = [task for task in data.get("tasks", []) if isinstance(task, dict)]
19
19
  open_tasks = [task for task in tasks if task.get("status") in OPEN_TASK_STATUSES]
20
20
 
21
- policies = load_yaml(".codex/pjsdlc_managed/policies/allowed_paths.yaml")
21
+ policies = load_yaml("<harnessRoot>/pjsdlc_managed/policies/allowed_paths.yaml")
22
22
  lifecycle = load_lifecycle()
23
23
  current_phase = lifecycle.get("current_phase") or "SPRINTING"
24
24
  phase_policy = ((policies.get("phases") or {}).get(current_phase) or {})
@@ -29,7 +29,7 @@ def main() -> None:
29
29
  task = task_by_id(data, current_task_id) if current_task_id else None
30
30
  require(task, "current_task_id must point to the task being validated")
31
31
  require(task.get("status") in OPEN_TASK_STATUSES, "current_task_id must point to an open task for path validation")
32
- allowed = list(task.get("allowed_paths") or []) + list(always_allow)
32
+ allowed = expand_harness_root(list(task.get("allowed_paths") or [])) + list(always_allow)
33
33
  else:
34
34
  print("Allowed paths skipped: no open task")
35
35
  return
@@ -33,6 +33,24 @@ CROSS_CUTTING_CATEGORIES = [
33
33
  "architecture_terms": ["compliance", "permission", "authorization", "audit", "合规", "权限", "审计", "授权", "客户确认", "回执归档"],
34
34
  },
35
35
  ]
36
+ UI_DRAFT_TASK_TERMS = [
37
+ "frontend",
38
+ "front-end",
39
+ "browser",
40
+ "page",
41
+ "screen",
42
+ "route",
43
+ "component",
44
+ "visual_ui",
45
+ "design.md",
46
+ ".docs/02_experience/",
47
+ "页面",
48
+ "前端",
49
+ "屏幕",
50
+ "交互",
51
+ "组件",
52
+ "视觉",
53
+ ]
36
54
 
37
55
 
38
56
  def main() -> None:
@@ -43,6 +61,7 @@ def main() -> None:
43
61
  architecture_docs = markdown_deliverables(".docs/02_architecture")
44
62
  tech_plan_docs = markdown_deliverables(".docs/03_tech_plan")
45
63
  product_docs = markdown_deliverables(".docs/01_product")
64
+ experience_docs = markdown_deliverables(".docs/02_experience")
46
65
  require(architecture_docs, "No architecture deliverables found in .docs/02_architecture/")
47
66
  require(tech_plan_docs, "No technical plan deliverables found in .docs/03_tech_plan/")
48
67
 
@@ -50,19 +69,20 @@ def main() -> None:
50
69
  require(contains_any(text, ["prd", "requirement", "需求"]), "Design must cite product requirements")
51
70
  require(contains_any(text, ["api", "interface", "接口", "contract", "契约"]), "Design must describe interfaces or contracts")
52
71
  require(contains_any(text, ["task", "任务", "breakdown"]), "Design must include task breakdown")
53
- draft_tasks = validate_draft_task_tech_plan_refs(tech_plan_docs)
72
+ draft_tasks = validate_draft_task_tech_plan_refs(tech_plan_docs, experience_docs)
54
73
  validate_cross_cutting_architecture(product_docs, tech_plan_docs, architecture_docs, draft_tasks)
55
74
  print(f"Design artifacts OK: {len(architecture_docs)} architecture, {len(tech_plan_docs)} tech plan")
56
75
 
57
76
 
58
- def validate_draft_task_tech_plan_refs(tech_plan_docs: list) -> list[dict]:
59
- draft = load_plan(".codex/state/plan.draft.yaml")
77
+ def validate_draft_task_tech_plan_refs(tech_plan_docs: list, experience_docs: list) -> list[dict]:
78
+ draft = load_plan("<harnessRoot>/state/plan.draft.yaml")
60
79
  require("current_phase" not in draft, "plan.draft.yaml must not define current_phase; lifecycle.yaml is the single source for current_phase")
61
80
  require("current_task_id" not in draft, "plan.draft.yaml must not define current_task_id because drafts are not active task state")
62
81
  tasks = draft.get("tasks", [])
63
82
  require(tasks, "plan.draft.yaml must contain at least one task before leaving ARCHITECTING")
64
83
 
65
84
  available_tech_plans = {repo_relative(path) for path in tech_plan_docs}
85
+ available_experience_docs = {repo_relative(path) for path in experience_docs}
66
86
  development_tasks: list[dict] = []
67
87
  primary_refs: list[str] = []
68
88
  for index, task in enumerate(tasks):
@@ -80,6 +100,7 @@ def validate_draft_task_tech_plan_refs(tech_plan_docs: list) -> list[dict]:
80
100
  for ref in normalized_refs:
81
101
  require(ref.startswith(".docs/03_tech_plan/"), f"Draft task {task.get('id')} docs.tech_plan must point into .docs/03_tech_plan/: {ref}")
82
102
  require(ref in available_tech_plans, f"Draft task {task.get('id')} references missing or generated tech plan slice: {ref}")
103
+ validate_uiux_design_refs_for_draft_task(task, available_experience_docs)
83
104
  validate_self_test_contract_tech_plan_binding(task, normalized_refs)
84
105
  primary_refs.append(normalized_refs[0])
85
106
 
@@ -92,6 +113,38 @@ def validate_draft_task_tech_plan_refs(tech_plan_docs: list) -> list[dict]:
92
113
  return tasks
93
114
 
94
115
 
116
+ def validate_uiux_design_refs_for_draft_task(task: dict, available_experience_docs: set[str]) -> None:
117
+ docs = task.get("docs")
118
+ if not isinstance(docs, dict):
119
+ return
120
+ task_id = task.get("id")
121
+ uiux_refs = [normalize_doc_ref(ref) for ref in as_list(docs.get("uiux"))]
122
+ design_refs = [normalize_doc_ref(ref) for ref in as_list(docs.get("design_system"))]
123
+ ui_task = is_ui_draft_task(task)
124
+
125
+ if ui_task:
126
+ require(uiux_refs, f"UI/frontend draft task {task_id} must reference a UI/UX slice in docs.uiux")
127
+ require(design_refs, f"UI/frontend draft task {task_id} must reference DESIGN.md in docs.design_system")
128
+
129
+ for ref in uiux_refs:
130
+ require(ref.startswith(".docs/02_experience/"), f"Draft task {task_id} docs.uiux must point into .docs/02_experience/: {ref}")
131
+ require(ref in available_experience_docs, f"Draft task {task_id} references missing or generated UI/UX slice: {ref}")
132
+
133
+ for ref in design_refs:
134
+ require(ref == "DESIGN.md", f"Draft task {task_id} docs.design_system must point to DESIGN.md: {ref}")
135
+ require(repo_path("DESIGN.md").exists(), f"Draft task {task_id} references missing design system: DESIGN.md")
136
+
137
+
138
+ def is_ui_draft_task(task: dict) -> bool:
139
+ docs = task.get("docs")
140
+ docs_text = ""
141
+ if isinstance(docs, dict):
142
+ docs_text = "\n".join(ref for value in docs.values() for ref in as_list(value))
143
+ runtime_text = "\n".join(str(task.get(key) or "") for key in ["target_runtime_environment", "self_test_contract"])
144
+ text = "\n".join(str(task.get(key) or "") for key in ["id", "title", "summary", "phase"]) + "\n" + docs_text + "\n" + runtime_text
145
+ return contains_any(text, UI_DRAFT_TASK_TERMS)
146
+
147
+
95
148
  def validate_cross_cutting_architecture(product_docs: list, tech_plan_docs: list, architecture_docs: list, draft_tasks: list[dict]) -> None:
96
149
  source_text = "\n".join(
97
150
  [
@@ -3,7 +3,7 @@ from harness_utils import load_plan, require, run_main
3
3
 
4
4
 
5
5
  def main() -> None:
6
- draft = load_plan(".codex/state/plan.draft.yaml")
6
+ draft = load_plan("<harnessRoot>/state/plan.draft.yaml")
7
7
  require("current_phase" not in draft, "plan.draft.yaml must not define current_phase; lifecycle.yaml is the single source for current_phase")
8
8
  require("current_task_id" not in draft, "plan.draft.yaml must not define current_task_id because drafts are not active task state")
9
9
  tasks = [task for task in draft.get("tasks", []) if isinstance(task, dict)]