@pzy560117/codex-harness 0.1.7 → 0.1.8

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.
package/README.md CHANGED
@@ -106,7 +106,7 @@ harness init --plan
106
106
  ### 钉住远程版本初始化
107
107
 
108
108
  ```powershell
109
- npx @pzy560117/codex-harness init --version 0.1.7
109
+ npx @pzy560117/codex-harness init --version 0.1.8
110
110
  ```
111
111
 
112
112
  适合 release smoke 或回滚到某个已发布版本。
@@ -7,6 +7,13 @@
7
7
  - 当前仓库根目录下的活跃文档或脚本,只在它本身就是 canonical,或为了验证模板落地效果、保持当前仓库可运行时再同步修改。
8
8
  - 如果一次改动同时涉及根目录活跃文件和模板文件,优先说明哪一份是 canonical,避免只修当前仓库、不修模板源。
9
9
 
10
+ ## 任务结构硬门禁
11
+
12
+ - 未经用户明确同意,不得把标准 `task.json` 模板 phase 压缩成更少任务;正式任务队列至少保留 `ANALYSIS-001`、`TESTCASE-001`、`PLAN-001`。
13
+ - P0/P1 项目禁止跳过 `TESTCASE-001`;缺少 `docs/testing/NATURAL_LANGUAGE_TEST_CASES.md`、`ACCEPTANCE_CRITERIA.md`、`TRACEABILITY_MATRIX.md`、`TEST_DATA_MATRIX.md`、`REGRESSION_PLAN.md`、`verify-matrix.md` 任一项,都不得生成最终 `task.json`。
14
+ - `feature_impl` 没有 `qa_contract` 不得进入正式任务队列;`qa_contract` 没有完整 `tdd_contract`、`development_validation`、`acceptance_validation` 不得开始实现。
15
+ - 单个 `feature_impl` 默认不得覆盖超过 3 条主需求;订单、支付、库存、RBAC 必须拆独立任务;前端和后端不得长期混在同一个 story,除非这是明确的 `release` 任务。
16
+
10
17
  ## 目录级规则
11
18
 
12
19
  - 当某个目录满足以下任一条件时,应优先考虑新增或更新该目录下的 `AGENTS.md`,而不是继续扩充根入口:
@@ -57,7 +57,7 @@ function Resolve-TemplateRoot {
57
57
  }
58
58
 
59
59
  $agentsTemplate = Join-Path $resolved "runtime\AGENTS.md"
60
- $driverTemplate = Join-Path $resolved "runtime\codex-loop.ps1"
60
+ $driverTemplate = Join-Path $resolved "runtime\codex-loop.ps1"
61
61
  if ((Test-Path -LiteralPath $agentsTemplate) -and (Test-Path -LiteralPath $driverTemplate)) {
62
62
  return $resolved
63
63
  }
@@ -225,16 +225,28 @@ function Ensure-TaskFile {
225
225
  return Copy-ManagedFile -SourceRoot $SourceRoot -SourceRelativePath "runtime\task.json" -DestinationRoot $DestinationRoot -DestinationRelativePath "task.json" -Overwrite:$true
226
226
  }
227
227
 
228
- $taskDocument = $taskContent | ConvertFrom-Json
229
- if ($null -eq $taskDocument.runtime) {
230
- $taskDocument | Add-Member -NotePropertyName "runtime" -NotePropertyValue ([PSCustomObject]@{}) -Force
231
- }
232
-
233
- if ([string]::IsNullOrWhiteSpace($taskDocument.runtime.driver)) {
234
- $taskDocument.runtime.driver = "powershell -NoProfile -ExecutionPolicy Bypass -File .\tools\\harness/codex-loop.ps1"
235
- $taskDocument | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $taskPath -Encoding UTF8
236
- return [PSCustomObject]@{
237
- Path = $taskPath
228
+ $taskDocument = $taskContent | ConvertFrom-Json
229
+ if ($null -eq $taskDocument.runtime) {
230
+ $taskDocument | Add-Member -NotePropertyName "runtime" -NotePropertyValue ([PSCustomObject]@{}) -Force
231
+ }
232
+
233
+ $canonicalDriver = "powershell -NoProfile -ExecutionPolicy Bypass -File .\tools\harness\codex-loop.ps1"
234
+ $driverPattern = 'tools[\\/]+harness[\\/]+tools[\\/]+harness[\\/]+codex-loop\.ps1'
235
+
236
+ if ([string]::IsNullOrWhiteSpace($taskDocument.runtime.driver)) {
237
+ $taskDocument.runtime.driver = $canonicalDriver
238
+ $taskDocument | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $taskPath -Encoding UTF8
239
+ return [PSCustomObject]@{
240
+ Path = $taskPath
241
+ Action = "updated"
242
+ }
243
+ }
244
+
245
+ if ([string]$taskDocument.runtime.driver -match $driverPattern) {
246
+ $taskDocument.runtime.driver = $canonicalDriver
247
+ $taskDocument | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $taskPath -Encoding UTF8
248
+ return [PSCustomObject]@{
249
+ Path = $taskPath
238
250
  Action = "updated"
239
251
  }
240
252
  }
@@ -293,11 +305,25 @@ function Resolve-LockManifestSourcePath {
293
305
  return Join-Path $PackageRoot ($normalizedSource.Replace('/', '\'))
294
306
  }
295
307
 
296
- function Get-FileSha256 {
297
- param([string]$Path)
298
-
299
- return (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash.ToLowerInvariant()
300
- }
308
+ function Get-FileSha256 {
309
+ param([string]$Path)
310
+
311
+ $hashAlgorithm = [System.Security.Cryptography.SHA256]::Create()
312
+ try {
313
+ $stream = [System.IO.File]::OpenRead($Path)
314
+ try {
315
+ $hashBytes = $hashAlgorithm.ComputeHash($stream)
316
+ }
317
+ finally {
318
+ $stream.Dispose()
319
+ }
320
+ }
321
+ finally {
322
+ $hashAlgorithm.Dispose()
323
+ }
324
+
325
+ return ([System.BitConverter]::ToString($hashBytes)).Replace('-', '').ToLowerInvariant()
326
+ }
301
327
 
302
328
  function Add-LockManagedFile {
303
329
  param(
@@ -561,13 +587,14 @@ $results = @()
561
587
 
562
588
  $rootFiles = @(
563
589
  @{ Source = "runtime\AGENTS.md"; Destination = "AGENTS.md" },
564
- @{ Source = "tools/install/bootstrap-codex-harness.ps1"; Destination = "tools\\install/bootstrap-codex-harness.ps1" },
565
- @{ Source = "runtime\codex-loop.ps1"; Destination = "tools\\harness/codex-loop.ps1" },
566
- @{ Source = "runtime\doctor.ps1"; Destination = "tools\\harness/doctor.ps1" },
590
+ @{ Source = "bootstrap-codex-harness.ps1"; Destination = "tools\install\bootstrap-codex-harness.ps1" },
591
+ @{ Source = "runtime\codex-loop.ps1"; Destination = "tools\harness\codex-loop.ps1" },
592
+ @{ Source = "runtime\doctor.ps1"; Destination = "tools\harness\doctor.ps1" },
567
593
  @{ Source = "config\codex-config.toml"; Destination = ".codex\config.toml" },
568
594
  @{ Source = "config\codex-readme.md"; Destination = ".codex\README.md" },
569
- @{ Source = "hooks\hooks.json"; Destination = ".codex\hooks.json" },
595
+ @{ Source = "hooks\hooks.json"; Destination = ".codex\hooks.json" },
570
596
  @{ Source = "hooks\hook-stop-verify.ps1"; Destination = "tools\harness\hook-stop-verify.ps1" },
597
+ @{ Source = "scripts\harness\harness-governance-check.ps1"; Destination = "scripts\harness\harness-governance-check.ps1" },
571
598
  @{ Source = "scripts\ai-workflow\check-ai-sync-drift.ps1"; Destination = "tools\install\ai-workflow\check-ai-sync-drift.ps1" },
572
599
  @{ Source = "runtime\task-run-profile.json"; Destination = ".codex\task-run-profile.json" },
573
600
  @{ Source = "prompts\implement-one-task.md"; Destination = ".codex\prompts\implement-one-task.md" },
@@ -586,33 +613,16 @@ $rootFiles = @(
586
613
  @{ Source = "prompts\worker-role\harness-writer.md"; Destination = ".codex\prompts\worker-role\harness-writer.md" },
587
614
  @{ Source = "runtime\smoke-task.json"; Destination = "tools\harness\templates\smoke-task.json" },
588
615
  @{ Source = "runtime\project-task-template.json"; Destination = "tools\harness\templates\project-task-template.json" },
589
- @{ Source = "runtime\verify.ps1"; Destination = "tools\\harness/verify.ps1" },
590
- @{ Source = "tools\harness\spec-lint.ps1"; Destination = "tools\\harness/spec-lint.ps1" },
591
- @{ Source = "tools\harness\docs-lint.ps1"; Destination = "tools\\harness/docs-lint.ps1" },
592
- @{ Source = "tools\harness\data-lint.ps1"; Destination = "tools\\harness/data-lint.ps1" },
593
- @{ Source = "tools\harness\integration-lint.ps1"; Destination = "tools\\harness/integration-lint.ps1" },
594
- @{ Source = "tools\harness\mobile-lint.ps1"; Destination = "tools\\harness/mobile-lint.ps1" },
595
- @{ Source = "tools\harness\acceptance-lint.ps1"; Destination = "tools\\harness/acceptance-lint.ps1" },
596
- @{ Source = "tools\harness\context-lint.ps1"; Destination = "tools\\harness/context-lint.ps1" },
597
- @{ Source = "tools\harness\architecture-lint.ps1"; Destination = "tools\\harness/architecture-lint.ps1" },
598
- @{ Source = "tools\harness\directory-lint.ps1"; Destination = "tools\\harness/directory-lint.ps1" },
599
- @{ Source = "tools\harness\component-lint.ps1"; Destination = "tools\\harness/component-lint.ps1" },
600
- @{ Source = "tools\harness\business-lint.ps1"; Destination = "tools\\harness/business-lint.ps1" },
601
- @{ Source = "tools\harness\contract-lint.ps1"; Destination = "tools\\harness/contract-lint.ps1" },
602
- @{ Source = "tools\harness\state-lint.ps1"; Destination = "tools\\harness/state-lint.ps1" },
603
- @{ Source = "tools\harness\ui-lint.ps1"; Destination = "tools\\harness/ui-lint.ps1" },
604
- @{ Source = "tools\harness\backend-lint.ps1"; Destination = "tools\\harness/backend-lint.ps1" },
605
- @{ Source = "tools\harness\security-lint.ps1"; Destination = "tools\\harness/security-lint.ps1" },
606
- @{ Source = "tools\harness\testing-lint.ps1"; Destination = "tools\\harness/testing-lint.ps1" },
607
- @{ Source = "tools\harness\impact-lint.ps1"; Destination = "tools\\harness/impact-lint.ps1" },
608
- @{ Source = "tools\harness\performance-lint.ps1"; Destination = "tools\\harness/performance-lint.ps1" },
609
- @{ Source = "tools\harness\config-lint.ps1"; Destination = "tools\\harness/config-lint.ps1" },
610
- @{ Source = "tools\harness\observability-lint.ps1"; Destination = "tools\\harness/observability-lint.ps1" },
611
- @{ Source = "tools\harness\refactor-lint.ps1"; Destination = "tools\\harness/refactor-lint.ps1" },
612
- @{ Source = "tools\harness\session-lint.ps1"; Destination = "tools\\harness/session-lint.ps1" },
613
- @{ Source = "tools\harness\style-lint.ps1"; Destination = "tools\\harness/style-lint.ps1" },
614
- @{ Source = "config\tools/install/env-check.ps1"; Destination = "tools\\install/env-check.ps1" },
615
- @{ Source = "trace\docs/harness/trace.schema.json"; Destination = "docs\\harness/trace.schema.json" }
616
+ @{ Source = "tools\harness\task-structure-lint.ps1"; Destination = "tools\harness\task-structure-lint.ps1" },
617
+ @{ Source = "runtime\verify.ps1"; Destination = "tools\harness\verify.ps1" },
618
+ @{ Source = "tools\harness\docs-lint.ps1"; Destination = "tools\harness\docs-lint.ps1" },
619
+ @{ Source = "tools\harness\data-lint.ps1"; Destination = "tools\harness\data-lint.ps1" },
620
+ @{ Source = "tools\harness\integration-lint.ps1"; Destination = "tools\harness\integration-lint.ps1" },
621
+ @{ Source = "tools\harness\mobile-lint.ps1"; Destination = "tools\harness\mobile-lint.ps1" },
622
+ @{ Source = "tools\harness\acceptance-lint.ps1"; Destination = "tools\harness\acceptance-lint.ps1" },
623
+ @{ Source = "tools\harness\session-lint.ps1"; Destination = "tools\harness\session-lint.ps1" },
624
+ @{ Source = "config\env-check.ps1"; Destination = "tools\install\env-check.ps1" },
625
+ @{ Source = "trace\trace.schema.json"; Destination = "docs\harness\trace.schema.json" }
616
626
  )
617
627
 
618
628
  foreach ($file in $rootFiles) {
@@ -735,7 +745,6 @@ if ($Profile -eq "legacy" -and $IncludeDocs) {
735
745
  @{ Source = "docs\prompt-knowledge-integration.md"; Destination = "docs/harness/prompt-knowledge-integration.md" },
736
746
  @{ Source = "docs\mcp-knowledge-governance.md"; Destination = "docs/harness/mcp-knowledge-governance.md" },
737
747
  @{ Source = "docs\code-semantics-and-navigation.md"; Destination = "docs/harness/code-semantics-and-navigation.md" },
738
- @{ Source = "docs\code-style-and-naming.md"; Destination = "docs/harness/code-style-and-naming.md" },
739
748
  @{ Source = "docs\rule-governance.md"; Destination = "docs/harness/rule-governance.md" },
740
749
  @{ Source = "docs\regression-rules.md"; Destination = "docs/harness/regression-rules.md" },
741
750
  @{ Source = "docs\team-knowledge-sync.md"; Destination = "docs/harness/team-knowledge-sync.md" },
@@ -26,12 +26,16 @@
26
26
  - 标准交接入口是 `tools/harness/templates/project-task-template.json`,首个任务固定为 `INIT-001`,用于锁定 spec 输入、任务依赖、验证矩阵、`execution.mode` 和 owned paths。
27
27
  - 如果 `task.json` 为空文件,先初始化为合法 JSON;如果仍是 smoke 模板或示例任务,先替换为当前项目真实任务。
28
28
  - 交互开发模式可以决定新增或更新哪些待办任务,但实际写入必须委派给 `harness-writer` 等匹配 writer。
29
+ - 未经用户明确同意,不得把标准模板 phase 压缩成更少任务;正式任务队列至少保留 `ANALYSIS-001`、`TESTCASE-001`、`PLAN-001`。
29
30
  - 任务进入 driver 前必须冻结交付语义:`scope`、`non_goals`、`entrypoints`、`inputs_outputs`、`failure_policy`、`rollback_strategy`、`state_surface`、`writeback_targets`。不适用字段必须写 `not_applicable`,不能留给实现会话猜。
30
31
  - 进入实现前,`docs/ai/CURRENT_TASK.md` 必须能回答本轮已完成内容、剩余问题、修改文件、测试结果、风险点和下一步,不允许只靠聊天记忆续跑。
32
+ - `ANALYSIS-001`、`TESTCASE-001`、`PLAN-001` 的 testing truth source 产物必须在生成最终 `task.json` 前补齐:`docs/testing/ACCEPTANCE_CRITERIA.md`、`docs/testing/NATURAL_LANGUAGE_TEST_CASES.md`、`docs/testing/TRACEABILITY_MATRIX.md`、`docs/testing/TEST_DATA_MATRIX.md`、`docs/testing/REGRESSION_PLAN.md`、`docs/testing/verify-matrix.md`。缺任一项都不得进入正式任务队列。
31
33
  - 创建或重建任务队列时必须一次性补齐每个任务的完整交接字段:`requirement_ids`、`owned_paths`、`context_files`、`produces_artifacts`、`test_command`、`acceptance`、自然语言测试用例、测试数据、开发验证、最终验收验证、证据路径和 `qa_contract`。不得创建“先跑起来、后面再补测试/证据/整链路”的实现任务。
32
34
  - PRD / 需求文档完成后、进入实现前必须有自然语言测试用例阶段;每条 P0/P1 需求必须按 `docs/testing/NATURAL_LANGUAGE_TEST_CASES.md` 的需求类型覆盖矩阵满足最小用例数,并回溯到 Requirement ID、PRD 来源、Oracle、测试数据、证据路径和 TDD RED 预期失败。
33
35
  - 每个 `feature_impl` 任务必须声明两段验证:`development_validation` 用于编码过程中的 affected tests、单元 / 组件、局部 API、契约、类型检查或 lint;`acceptance_validation` 用于代码写完后从用户故事入口重新跑完整链路。
34
36
  - 每个 `feature_impl.qa_contract.tdd_contract.red.source_case_ids` 必须能回溯到 `docs/testing/NATURAL_LANGUAGE_TEST_CASES.md`;缺少自然语言用例来源时不得进入实现阶段。
37
+ - 每个 `feature_impl.qa_contract.required_layers` 必须至少包含 `unit_or_component`、`contract_or_api`、`story_full_chain`、`affected_regression`;`tdd_contract` 必须同时包含 `red`、`green`、`refactor_guard`,且 `source_case_ids` 不得为空。
38
+ - 单个 `feature_impl` 默认不得覆盖超过 3 条主需求;订单、支付、库存、RBAC 必须拆成独立任务;前端和后端不得长期混在同一个 story,除非这是明确的 `release` 任务。只有用户明确批准时,才允许在任务中增加 `decomposition_exemption` 记录例外原因。
35
39
  - 不适合作为 TDD RED 的自然语言用例必须通过 `story_full_chain.source_case_ids`、`acceptance_validation.source_case_ids`、回归计划或 `verify-matrix` 进入验收链路;不得只保留在 PRD 或测试设计文档中。
36
40
  - 每个 `feature_impl` 的验收链路必须包含入口、动作、Oracle、副作用/无副作用证据、测试数据、失败态证据和 release 影响。单元测试、小范围 smoke、静态检查或 `git diff --check` 只能作为开发验证的一层,不能替代最终验收链路。
37
41
  - 后端、CLI、worker、数据同步和外部集成类 `feature_impl` 必须按 `static`、`unit`、`chain`、`failure`、`writeback` 五层声明验证;没有 UI 可见结果时,链路验证和失败验证不能省略。
@@ -608,6 +608,57 @@ function Invoke-HarnessGovernanceCheck {
608
608
  }
609
609
  }
610
610
 
611
+ function Invoke-TaskStructureValidation {
612
+ param([string]$Root)
613
+
614
+ $scriptPath = Join-Path $Root "tools\harness\task-structure-lint.ps1"
615
+ if (-not (Test-Path -LiteralPath $scriptPath -PathType Leaf)) {
616
+ return [pscustomobject]@{
617
+ status = "fail"
618
+ findings = @([pscustomobject]@{
619
+ severity = "error"
620
+ code = "missing_task_structure_lint"
621
+ message = "缺少 task structure lint: tools\\harness\\task-structure-lint.ps1"
622
+ task_id = ""
623
+ field = "tools/harness/task-structure-lint.ps1"
624
+ })
625
+ }
626
+ }
627
+
628
+ try {
629
+ return (& powershell -NoProfile -ExecutionPolicy Bypass -File $scriptPath -ProjectRoot $Root -JsonOutput | ConvertFrom-Json)
630
+ }
631
+ catch {
632
+ return [pscustomobject]@{
633
+ status = "fail"
634
+ findings = @([pscustomobject]@{
635
+ severity = "error"
636
+ code = "task_structure_lint_failed"
637
+ message = "task structure lint 执行失败: $($_.Exception.Message)"
638
+ task_id = ""
639
+ field = "task.json"
640
+ })
641
+ }
642
+ }
643
+ }
644
+
645
+ function ConvertTo-TaskStructureFindingText {
646
+ param([object]$ValidationResult)
647
+
648
+ $lines = @()
649
+ foreach ($finding in @($ValidationResult.findings)) {
650
+ $taskLabel = if ([string]::IsNullOrWhiteSpace([string]$finding.task_id)) { "" } else { " task=$($finding.task_id)" }
651
+ $fieldLabel = if ([string]::IsNullOrWhiteSpace([string]$finding.field)) { "" } else { " field=$($finding.field)" }
652
+ $lines += "- [$($finding.severity)] $($finding.code)$taskLabel${fieldLabel}: $($finding.message)"
653
+ }
654
+
655
+ if ($lines.Count -eq 0) {
656
+ return "- (none)"
657
+ }
658
+
659
+ return ($lines -join "`n")
660
+ }
661
+
611
662
  function Block-WithState {
612
663
  param(
613
664
  [string]$Root,
@@ -759,16 +810,31 @@ Initialize task.json with a valid harness task queue, then rerun the driver or v
759
810
  "@
760
811
  }
761
812
 
762
- if ($tasks.Count -eq 0) {
763
- Block-WithState -Root $resolvedProjectRoot -EvidenceKey $evidenceKey -ReasonCode "task_queue_empty" -Reason @"
764
- Harness stop gate: task.json contains no tasks.
765
-
766
- Next action:
767
- Initialize a real harness task queue before ending the session.
768
- "@
769
- }
770
-
771
- $pendingCount = Get-PendingTaskCount -Tasks $tasks
813
+ if ($tasks.Count -eq 0) {
814
+ Block-WithState -Root $resolvedProjectRoot -EvidenceKey $evidenceKey -ReasonCode "task_queue_empty" -Reason @"
815
+ Harness stop gate: task.json contains no tasks.
816
+
817
+ Next action:
818
+ Initialize a real harness task queue before ending the session.
819
+ "@
820
+ }
821
+
822
+ $taskStructureResult = Invoke-TaskStructureValidation -Root $resolvedProjectRoot
823
+ if ([string]$taskStructureResult.status -eq "fail") {
824
+ $findingText = ConvertTo-TaskStructureFindingText -ValidationResult $taskStructureResult
825
+ Block-WithState -Root $resolvedProjectRoot -EvidenceKey $evidenceKey -ReasonCode "task_structure_invalid" -Reason @"
826
+ Harness stop gate: task.json 未通过结构门禁。
827
+
828
+ Findings:
829
+ $findingText
830
+
831
+ Next action:
832
+ 补齐缺失 phase、testing truth source、qa_contract、tdd_contract 或拆分过大的 feature_impl 后,再运行:
833
+ powershell -NoProfile -ExecutionPolicy Bypass -File .\tools\harness\verify.ps1
834
+ "@
835
+ }
836
+
837
+ $pendingCount = Get-PendingTaskCount -Tasks $tasks
772
838
  if ($pendingCount -le 0) {
773
839
  Block-OnTraceEvidenceIfNeeded -Root $resolvedProjectRoot -EvidenceKey $evidenceKey -TraceInfo $latestTraceInfo
774
840
  Exit-Allow
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "runtime": {
3
- "driver": "powershell -NoProfile -ExecutionPolicy Bypass -File .\\tools\\harness\\tools/harness/codex-loop.ps1",
3
+ "driver": "powershell -NoProfile -ExecutionPolicy Bypass -File .\\tools\\harness\\codex-loop.ps1",
4
4
  "run_profile": ".codex\\task-run-profile.json"
5
5
  },
6
6
  "tasks": [
@@ -43,7 +43,7 @@
43
43
  "如有需要补齐 demo 资产",
44
44
  "运行 tools/harness/verify.ps1 验证 demo 闭环"
45
45
  ],
46
- "test_command": "powershell -NoProfile -ExecutionPolicy Bypass -File .\\tools\\harness\\tools/harness/verify.ps1",
46
+ "test_command": "powershell -NoProfile -ExecutionPolicy Bypass -File .\\tools\\harness\\verify.ps1",
47
47
  "acceptance": [
48
48
  "demo 目录同时具备 product、design、frontend architecture、dev-plan、contract、component、story、generated client",
49
49
  "tools/harness/verify.ps1 通过",
@@ -30,6 +30,10 @@
30
30
  - 不要删除或改写已有任务描述,除非用户明确要求替换模板示例或重建任务队列。
31
31
  - 修改后必须运行与改动直接对应的验证;文档改动至少运行 `git diff --check`。
32
32
  - 测试范围从需求收敛阶段开始定义;P0/P1 需求进入实现前必须有可追溯验收、测试数据和证据路径。
33
+ - 未经用户明确同意,不得把标准模板 phase 压缩成更少任务;正式任务队列至少保留 `ANALYSIS-001`、`TESTCASE-001`、`PLAN-001` 三段测试左移与实施计划 phase。
34
+ - P0/P1 任务禁止跳过 `TESTCASE-001`;没有自然语言测试用例、测试数据矩阵、回归计划和验证矩阵,不得生成最终 `task.json`。
35
+ - `feature_impl` 没有 `qa_contract` 不得进入正式 `task.json`;`qa_contract` 没有完整 `tdd_contract`、开发验证和最终验收验证,不得开始实现。
36
+ - 单个 `feature_impl` 默认不得覆盖超过 3 条主需求;订单、支付、库存、RBAC 必须拆成独立任务;前端和后端不得长期混在同一个 story,除非这是明确的 `release` 任务。
33
37
  - 外部系统、开源栈、第三方平台或真实环境集成需求,完成声明必须包含真实依赖接入、成功态证据和失败态证据。
34
38
  - 进入实现前必须有当前项目声明的架构约束 truth source,默认路径为 `docs/architecture/constraints.md`,也可由 `task.json.runtime.handoff.truth_sources.architecture` 指向其他路径。
35
39
  - `feature_impl` 任务必须携带可执行的 `architecture_constraints`、`forbidden_implementations` 和对应验证命令;禁止用测试替身、local-only adapter 或领域原型冒充任务声明的交付路径。
@@ -57,14 +57,50 @@ function Add-DoctorError {
57
57
  $Errors.Value += $Message
58
58
  }
59
59
 
60
- function Add-DoctorWarning {
61
- param(
62
- [ref]$Warnings,
63
- [string]$Message
64
- )
65
-
66
- $Warnings.Value += $Message
67
- }
60
+ function Add-DoctorWarning {
61
+ param(
62
+ [ref]$Warnings,
63
+ [string]$Message
64
+ )
65
+
66
+ $Warnings.Value += $Message
67
+ }
68
+
69
+ function Invoke-TaskStructureValidation {
70
+ param(
71
+ [string]$Root,
72
+ [ref]$Errors,
73
+ [ref]$Warnings
74
+ )
75
+
76
+ $scriptPath = Join-Path $Root "tools\harness\task-structure-lint.ps1"
77
+ if (-not (Test-Path -LiteralPath $scriptPath -PathType Leaf)) {
78
+ Add-DoctorError -Errors $Errors -Message "缺少 task structure lint: tools\\harness\\task-structure-lint.ps1"
79
+ return
80
+ }
81
+
82
+ try {
83
+ $result = & powershell -NoProfile -ExecutionPolicy Bypass -File $scriptPath -ProjectRoot $Root -JsonOutput | ConvertFrom-Json
84
+ }
85
+ catch {
86
+ Add-DoctorError -Errors $Errors -Message "task structure lint 执行失败: $($_.Exception.Message)"
87
+ return
88
+ }
89
+
90
+ foreach ($finding in @($result.findings)) {
91
+ $message = [string]$finding.message
92
+ if (-not [string]::IsNullOrWhiteSpace([string]$finding.task_id)) {
93
+ $message = "task $($finding.task_id): $message"
94
+ }
95
+
96
+ if ($finding.severity -eq "warning") {
97
+ Add-DoctorWarning -Warnings $Warnings -Message $message
98
+ }
99
+ else {
100
+ Add-DoctorError -Errors $Errors -Message $message
101
+ }
102
+ }
103
+ }
68
104
 
69
105
  function Get-DoctorFileSha256 {
70
106
  param([string]$Path)
@@ -1017,15 +1053,16 @@ $isThinProjectInstall = $harnessConfigInfo.Exists -and $harnessConfigInfo.Valid
1017
1053
  $isVendorProjectInstall = $harnessConfigInfo.Exists -and $harnessConfigInfo.Valid -and $harnessConfigInfo.InstallScope -eq "vendor"
1018
1054
  $isNotInstalledProject = $baseProjectShape -eq "not-installed"
1019
1055
 
1020
- $requiredRootFiles = @(
1021
- "AGENTS.md",
1056
+ $requiredRootFiles = @(
1057
+ "AGENTS.md",
1022
1058
  "tools\harness\codex-loop.ps1",
1023
1059
  "tools\harness\doctor.ps1",
1060
+ "tools\harness\task-structure-lint.ps1",
1024
1061
  "tools\harness\verify.ps1",
1025
1062
  "tools\install\env-check.ps1",
1026
1063
  "task.json",
1027
1064
  "docs\harness\trace.schema.json"
1028
- )
1065
+ )
1029
1066
 
1030
1067
  $requiredPromptFiles = @(
1031
1068
  ".codex\\prompts\\implement-one-task.md",
@@ -1143,11 +1180,37 @@ elseif (-not $isVendorProjectInstall) {
1143
1180
  Add-DoctorError -Errors ([ref]$errors) -Message "task.json 仍包含模板占位符,请先替换为当前项目真实任务。"
1144
1181
  }
1145
1182
 
1146
- $taskDocument = $taskContent | ConvertFrom-Json
1147
- $runtime = $taskDocument.runtime
1148
- $runProfileRelativePath = $DefaultRunProfile
1149
- if ($null -ne $runtime -and -not [string]::IsNullOrWhiteSpace($runtime.run_profile)) {
1150
- $runProfileRelativePath = $runtime.run_profile
1183
+ $taskDocument = $taskContent | ConvertFrom-Json
1184
+ $runtime = $taskDocument.runtime
1185
+ if ($null -eq $runtime -or [string]::IsNullOrWhiteSpace([string]$runtime.driver)) {
1186
+ Add-DoctorError -Errors ([ref]$errors) -Message "task.json 缺少 runtime.driver"
1187
+ }
1188
+ else {
1189
+ $driverCommand = [string]$runtime.driver
1190
+ if ($driverCommand -match 'tools[\\/]+harness[\\/]+tools[\\/]+harness[\\/]+codex-loop\.ps1') {
1191
+ Add-DoctorError -Errors ([ref]$errors) -Message "task.json 的 runtime.driver 包含重复路径段 tools/harness/tools/harness,请改为 .\\tools\\harness\\codex-loop.ps1"
1192
+ }
1193
+
1194
+ $driverFileMatch = [regex]::Match($driverCommand, '(?i)-File\s+("?)([^"]+)\1')
1195
+ if (-not $driverFileMatch.Success) {
1196
+ Add-DoctorError -Errors ([ref]$errors) -Message "task.json 的 runtime.driver 不是可解析的 PowerShell -File 命令: $driverCommand"
1197
+ }
1198
+ else {
1199
+ $driverRelativePath = $driverFileMatch.Groups[2].Value.Replace('/', '\')
1200
+ if ($driverRelativePath.StartsWith('.\')) {
1201
+ $driverRelativePath = $driverRelativePath.Substring(2)
1202
+ }
1203
+
1204
+ $driverPath = Join-Path $resolvedProjectRoot $driverRelativePath
1205
+ if (-not (Test-Path -LiteralPath $driverPath)) {
1206
+ Add-DoctorError -Errors ([ref]$errors) -Message "task.json 的 runtime.driver 指向不存在的脚本: $driverRelativePath"
1207
+ }
1208
+ }
1209
+ }
1210
+
1211
+ $runProfileRelativePath = $DefaultRunProfile
1212
+ if ($null -ne $runtime -and -not [string]::IsNullOrWhiteSpace($runtime.run_profile)) {
1213
+ $runProfileRelativePath = $runtime.run_profile
1151
1214
  }
1152
1215
 
1153
1216
  $runProfilePath = Join-Path $resolvedProjectRoot $runProfileRelativePath
@@ -1204,7 +1267,7 @@ elseif (-not $isVendorProjectInstall) {
1204
1267
  }
1205
1268
 
1206
1269
  $taskIds = @{}
1207
- foreach ($task in @($taskDocument.tasks)) {
1270
+ foreach ($task in @($taskDocument.tasks)) {
1208
1271
  if ($null -eq $task.id -or [string]::IsNullOrWhiteSpace([string]$task.id)) {
1209
1272
  Add-DoctorError -Errors ([ref]$errors) -Message "存在缺少 id 的任务。"
1210
1273
  continue
@@ -1231,13 +1294,15 @@ elseif (-not $isVendorProjectInstall) {
1231
1294
 
1232
1295
  $requiredTruthSourcesProperty = $task.PSObject.Properties["required_truth_sources"]
1233
1296
  $requiredTruthSources = if ($null -ne $requiredTruthSourcesProperty) { $requiredTruthSourcesProperty.Value } else { @() }
1234
- foreach ($truthSource in @($requiredTruthSources)) {
1235
- if ($allowedTruthSources -notcontains [string]$truthSource) {
1236
- Add-DoctorError -Errors ([ref]$errors) -Message "任务 $($task.id) 使用了未知 required_truth_source: $truthSource"
1237
- }
1238
- }
1239
- }
1240
- }
1297
+ foreach ($truthSource in @($requiredTruthSources)) {
1298
+ if ($allowedTruthSources -notcontains [string]$truthSource) {
1299
+ Add-DoctorError -Errors ([ref]$errors) -Message "任务 $($task.id) 使用了未知 required_truth_source: $truthSource"
1300
+ }
1301
+ }
1302
+ }
1303
+
1304
+ Invoke-TaskStructureValidation -Root $resolvedProjectRoot -Errors ([ref]$errors) -Warnings ([ref]$warnings)
1305
+ }
1241
1306
 
1242
1307
  Write-DoctorReadiness `
1243
1308
  -Root $resolvedProjectRoot `
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "runtime": {
3
- "driver": "powershell -NoProfile -ExecutionPolicy Bypass -File .\\tools\\harness\\tools/harness/codex-loop.ps1",
3
+ "driver": "powershell -NoProfile -ExecutionPolicy Bypass -File .\\tools\\harness\\codex-loop.ps1",
4
4
  "trace_dir": "traces",
5
5
  "agent": "codex",
6
6
  "run_profile": ".codex\\task-run-profile.json",
@@ -92,12 +92,13 @@
92
92
  "execution": {
93
93
  "mode": "single"
94
94
  },
95
- "steps": [
96
- "把模板中的 <feature-slug>、用户故事标题、requirement_ids、owned_paths、context_files 和 test_command 全部替换成当前项目真实值",
97
- "确认 task 依赖、优先级、execution.mode 和 non_blocking_dirty_paths 已与当前项目约束一致",
98
- "确认架构约束 truth source 已明确交付形态、公开入口、数据边界、异步边界、测试替身策略、禁止实现和 Definition of Done;如项目不用默认路径,更新 runtime.handoff.truth_sources.architecture",
99
- "确认当前 task.json 已经是正式任务队列,不再保留领域示例、smoke 占位或过期路径"
100
- ],
95
+ "steps": [
96
+ "把模板中的 <feature-slug>、用户故事标题、requirement_ids、owned_paths、context_files 和 test_command 全部替换成当前项目真实值",
97
+ "确认 task 依赖、优先级、execution.mode 和 non_blocking_dirty_paths 已与当前项目约束一致",
98
+ "不得删除 ANALYSIS-001、TESTCASE-001、PLAN-001;未经用户明确批准并记录例外原因,不得压缩标准模板 phase",
99
+ "确认架构约束 truth source 已明确交付形态、公开入口、数据边界、异步边界、测试替身策略、禁止实现和 Definition of Done;如项目不用默认路径,更新 runtime.handoff.truth_sources.architecture",
100
+ "确认当前 task.json 已经是正式任务队列,不再保留领域示例、smoke 占位或过期路径"
101
+ ],
101
102
  "test_command": "git diff --check",
102
103
  "acceptance": [
103
104
  "任务队列只保留当前项目真实阶段与用户故事",
@@ -354,7 +355,9 @@
354
355
  "把后续实现按用户故事顺序映射到 dev-plan、affected tests、验证命令和证据路径",
355
356
  "把自然语言测试用例中的 TDD 候选转换为每个 feature_impl.qa_contract.tdd_contract,并保留 source_case_ids",
356
357
  "把非 TDD 自然语言用例映射到 feature_impl.qa_contract.story_full_chain.source_case_ids、acceptance_validation.source_case_ids、回归计划或 verify-matrix",
358
+ "不要把模板 phase 合并成更少任务;如果用户明确批准压缩,必须在对应任务补充 decomposition_exemption 记录批准范围和原因",
357
359
  "确认每个实现任务都已有 requirement_ids、owned_paths、context_files 和最小可行验收闭环",
360
+ "确认每个 feature_impl 默认不超过 3 条主 requirement_ids;订单、支付、库存、RBAC 拆成独立任务;前端和后端不要长期混在同一个 story",
358
361
  "对涉及用户可见行为、路由、表单、权限、状态流转或关键业务闭环的任务,明确是否要求 docs/testing/e2e-plan.md,并把相关 E2E 命令写进 test_command 或 release 验证链路",
359
362
  "确认每个 feature_impl 任务都携带 architecture_constraints、forbidden_implementations、tdd_contract 和项目真实可执行的 test_command",
360
363
  "明确哪些故事必须串行,哪些依赖可在后续手工改成并行"
@@ -364,6 +367,8 @@
364
367
  "dev-plan、verify-matrix 和 story 级执行顺序保持一致",
365
368
  "每个 feature_impl 的 tdd_contract.red.source_case_ids 均能回溯到 NATURAL_LANGUAGE_TEST_CASES.md",
366
369
  "每个 feature_impl 的非 TDD 自然语言用例均能回溯到 story_full_chain、acceptance_validation、回归计划或 verify-matrix",
370
+ "模板 phase 没有被压缩;如存在压缩或跨层混合,已附带用户批准的 decomposition_exemption",
371
+ "每个 feature_impl 默认不超过 3 条主 requirement_ids,且订单、支付、库存、RBAC 已拆成独立任务",
367
372
  "高风险用户可见任务已经明确是否需要 e2e-plan 与对应 E2E 证据",
368
373
  "实现任务不会把测试替身、local-only adapter 或领域原型伪装成声明的交付路径",
369
374
  "后续实现与 release 任务的验证边界已经明确且可执行"
@@ -487,19 +492,21 @@
487
492
  ],
488
493
  "exemptions": []
489
494
  },
490
- "context_files": [
491
- "docs/product/prd-lite.md",
492
- "docs/product/requirement-interface-matrix.md",
493
- "docs/architecture/constraints.md",
494
- "docs/context/feature-pack.md",
495
- "docs/design/design-brief.md",
496
- "docs/design/component-map.md",
497
- "docs/design/screen-states.md",
498
- "docs/testing/ACCEPTANCE_EXAMPLES.md",
499
- "docs/testing/NATURAL_LANGUAGE_TEST_CASES.md",
500
- "docs/testing/TRACEABILITY_MATRIX.md",
501
- "docs/testing/TEST_STRATEGY.md",
502
- "docs/testing/TEST_DATA_MATRIX.md",
495
+ "context_files": [
496
+ "docs/product/prd-lite.md",
497
+ "docs/product/requirement-interface-matrix.md",
498
+ "docs/architecture/constraints.md",
499
+ "docs/context/feature-pack.md",
500
+ "docs/design/design-brief.md",
501
+ "docs/design/component-map.md",
502
+ "docs/design/screen-states.md",
503
+ "docs/testing/ACCEPTANCE_CRITERIA.md",
504
+ "docs/testing/ACCEPTANCE_EXAMPLES.md",
505
+ "docs/testing/NATURAL_LANGUAGE_TEST_CASES.md",
506
+ "docs/testing/REGRESSION_PLAN.md",
507
+ "docs/testing/TRACEABILITY_MATRIX.md",
508
+ "docs/testing/TEST_STRATEGY.md",
509
+ "docs/testing/TEST_DATA_MATRIX.md",
503
510
  "docs/testing/EVIDENCE_PROTOCOL.md",
504
511
  "docs/testing/test-matrix.md",
505
512
  "docs/testing/verify-matrix.md",
@@ -520,13 +527,14 @@
520
527
  "execution": {
521
528
  "mode": "single"
522
529
  },
523
- "steps": [
524
- "把 US1 标题、requirement_ids、owned_paths、产物路径和独立验证方式替换为真实项目内容",
525
- "把 qa_contract 中的 tdd_contract.source_case_ids、story_full_chain.source_case_ids、acceptance_validation.source_case_ids、入口、数据、动作、Oracle、证据、失败态和 release impact 全部替换为真实内容",
526
- "仅实现首个可单独交付的用户故事,并同步补齐其 affected tests 与必要文档",
527
- "运行与 US1 对应的真实验证命令,必须包含开发验证和代码完成后的故事级验收链路,并记录可回溯证据"
528
- ],
529
- "test_command": "powershell -NoProfile -ExecutionPolicy Bypass -File .\\tools\\harness\\tools/harness/verify.ps1 -Commands @('<replace-with-project-fitness-check>', '<replace-with-affected-tests>', 'git diff --check')",
530
+ "steps": [
531
+ "把 US1 标题、requirement_ids、owned_paths、产物路径和独立验证方式替换为真实项目内容",
532
+ "把 qa_contract 中的 tdd_contract.source_case_ids、story_full_chain.source_case_ids、acceptance_validation.source_case_ids、入口、数据、动作、Oracle、证据、失败态和 release impact 全部替换为真实内容",
533
+ "单个 feature_impl 默认不超过 3 条主 requirement_ids;订单、支付、库存、RBAC 不要和其他主故事捆绑;前端和后端不要长期混在同一个 story",
534
+ "仅实现首个可单独交付的用户故事,并同步补齐其 affected tests 与必要文档",
535
+ "运行与 US1 对应的真实验证命令,必须包含开发验证和代码完成后的故事级验收链路,并记录可回溯证据"
536
+ ],
537
+ "test_command": "powershell -NoProfile -ExecutionPolicy Bypass -File .\\tools\\harness\\verify.ps1 -Commands @('<replace-with-project-fitness-check>', '<replace-with-affected-tests>', 'git diff --check')",
530
538
  "acceptance": [
531
539
  "US1 能作为 MVP 独立演示或独立验收",
532
540
  "US1 满足架构约束 truth source,不以测试替身冒充声明交付路径",
@@ -652,19 +660,21 @@
652
660
  ],
653
661
  "exemptions": []
654
662
  },
655
- "context_files": [
656
- "docs/product/prd-lite.md",
657
- "docs/product/requirement-interface-matrix.md",
658
- "docs/architecture/constraints.md",
659
- "docs/context/feature-pack.md",
660
- "docs/design/design-brief.md",
661
- "docs/design/component-map.md",
662
- "docs/design/screen-states.md",
663
- "docs/testing/ACCEPTANCE_EXAMPLES.md",
664
- "docs/testing/NATURAL_LANGUAGE_TEST_CASES.md",
665
- "docs/testing/TRACEABILITY_MATRIX.md",
666
- "docs/testing/TEST_STRATEGY.md",
667
- "docs/testing/TEST_DATA_MATRIX.md",
663
+ "context_files": [
664
+ "docs/product/prd-lite.md",
665
+ "docs/product/requirement-interface-matrix.md",
666
+ "docs/architecture/constraints.md",
667
+ "docs/context/feature-pack.md",
668
+ "docs/design/design-brief.md",
669
+ "docs/design/component-map.md",
670
+ "docs/design/screen-states.md",
671
+ "docs/testing/ACCEPTANCE_CRITERIA.md",
672
+ "docs/testing/ACCEPTANCE_EXAMPLES.md",
673
+ "docs/testing/NATURAL_LANGUAGE_TEST_CASES.md",
674
+ "docs/testing/REGRESSION_PLAN.md",
675
+ "docs/testing/TRACEABILITY_MATRIX.md",
676
+ "docs/testing/TEST_STRATEGY.md",
677
+ "docs/testing/TEST_DATA_MATRIX.md",
668
678
  "docs/testing/EVIDENCE_PROTOCOL.md",
669
679
  "docs/testing/test-matrix.md",
670
680
  "docs/testing/verify-matrix.md",
@@ -685,13 +695,14 @@
685
695
  "execution": {
686
696
  "mode": "single"
687
697
  },
688
- "steps": [
689
- "将 US2 占位内容替换为真实范围、路径、需求编号和验证命令",
690
- "把 qa_contract 中的 tdd_contract.source_case_ids、story_full_chain.source_case_ids、acceptance_validation.source_case_ids、入口、数据、动作、Oracle、证据、失败态和 release impact 全部替换为真实内容",
691
- "确保 US2 可作为独立增量交付,不把未确认范围混入同一任务",
692
- "补齐 US2 的 affected tests、开发验证、代码完成后的故事级验收链路、回归说明和证据路径"
693
- ],
694
- "test_command": "powershell -NoProfile -ExecutionPolicy Bypass -File .\\tools\\harness\\tools/harness/verify.ps1 -Commands @('<replace-with-project-fitness-check>', '<replace-with-affected-tests>', 'git diff --check')",
698
+ "steps": [
699
+ "将 US2 占位内容替换为真实范围、路径、需求编号和验证命令",
700
+ "把 qa_contract 中的 tdd_contract.source_case_ids、story_full_chain.source_case_ids、acceptance_validation.source_case_ids、入口、数据、动作、Oracle、证据、失败态和 release impact 全部替换为真实内容",
701
+ "单个 feature_impl 默认不超过 3 条主 requirement_ids;订单、支付、库存、RBAC 不要和其他主故事捆绑;前端和后端不要长期混在同一个 story",
702
+ "确保 US2 可作为独立增量交付,不把未确认范围混入同一任务",
703
+ "补齐 US2 的 affected tests、开发验证、代码完成后的故事级验收链路、回归说明和证据路径"
704
+ ],
705
+ "test_command": "powershell -NoProfile -ExecutionPolicy Bypass -File .\\tools\\harness\\verify.ps1 -Commands @('<replace-with-project-fitness-check>', '<replace-with-affected-tests>', 'git diff --check')",
695
706
  "acceptance": [
696
707
  "US2 范围清晰且不与其他故事混杂",
697
708
  "US2 满足架构约束 truth source,不以测试替身冒充声明交付路径",
@@ -817,18 +828,21 @@
817
828
  ],
818
829
  "exemptions": []
819
830
  },
820
- "context_files": [
821
- "docs/product/prd-lite.md",
822
- "docs/product/requirement-interface-matrix.md",
823
- "docs/architecture/constraints.md",
824
- "docs/context/feature-pack.md",
825
- "docs/design/design-brief.md",
826
- "docs/design/component-map.md",
827
- "docs/design/screen-states.md",
828
- "docs/testing/ACCEPTANCE_EXAMPLES.md",
829
- "docs/testing/TRACEABILITY_MATRIX.md",
830
- "docs/testing/TEST_STRATEGY.md",
831
- "docs/testing/TEST_DATA_MATRIX.md",
831
+ "context_files": [
832
+ "docs/product/prd-lite.md",
833
+ "docs/product/requirement-interface-matrix.md",
834
+ "docs/architecture/constraints.md",
835
+ "docs/context/feature-pack.md",
836
+ "docs/design/design-brief.md",
837
+ "docs/design/component-map.md",
838
+ "docs/design/screen-states.md",
839
+ "docs/testing/ACCEPTANCE_CRITERIA.md",
840
+ "docs/testing/ACCEPTANCE_EXAMPLES.md",
841
+ "docs/testing/NATURAL_LANGUAGE_TEST_CASES.md",
842
+ "docs/testing/REGRESSION_PLAN.md",
843
+ "docs/testing/TRACEABILITY_MATRIX.md",
844
+ "docs/testing/TEST_STRATEGY.md",
845
+ "docs/testing/TEST_DATA_MATRIX.md",
832
846
  "docs/testing/EVIDENCE_PROTOCOL.md",
833
847
  "docs/testing/test-matrix.md",
834
848
  "docs/testing/verify-matrix.md",
@@ -849,13 +863,14 @@
849
863
  "execution": {
850
864
  "mode": "single"
851
865
  },
852
- "steps": [
853
- "将 US3 占位内容替换为真实故事标题、路径、需求编号和验证方式",
854
- "把 qa_contract 中的 tdd_contract.source_case_ids、story_full_chain.source_case_ids、acceptance_validation.source_case_ids、入口、数据、动作、Oracle、证据、失败态和 release impact 全部替换为真实内容",
855
- "只承接剩余优先级最高且可单独验收的增量,不把 release 收口动作混入本故事",
856
- "完成 US3 的实现、测试映射、开发验证、代码完成后的故事级验收链路和证据沉淀"
857
- ],
858
- "test_command": "powershell -NoProfile -ExecutionPolicy Bypass -File .\\tools\\harness\\tools/harness/verify.ps1 -Commands @('<replace-with-project-fitness-check>', '<replace-with-affected-tests>', 'git diff --check')",
866
+ "steps": [
867
+ "将 US3 占位内容替换为真实故事标题、路径、需求编号和验证方式",
868
+ "把 qa_contract 中的 tdd_contract.source_case_ids、story_full_chain.source_case_ids、acceptance_validation.source_case_ids、入口、数据、动作、Oracle、证据、失败态和 release impact 全部替换为真实内容",
869
+ "单个 feature_impl 默认不超过 3 条主 requirement_ids;订单、支付、库存、RBAC 不要和其他主故事捆绑;前端和后端不要长期混在同一个 story",
870
+ "只承接剩余优先级最高且可单独验收的增量,不把 release 收口动作混入本故事",
871
+ "完成 US3 的实现、测试映射、开发验证、代码完成后的故事级验收链路和证据沉淀"
872
+ ],
873
+ "test_command": "powershell -NoProfile -ExecutionPolicy Bypass -File .\\tools\\harness\\verify.ps1 -Commands @('<replace-with-project-fitness-check>', '<replace-with-affected-tests>', 'git diff --check')",
859
874
  "acceptance": [
860
875
  "US3 的增量边界和独立验收方式已明确",
861
876
  "US3 满足架构约束 truth source,不以测试替身冒充声明交付路径",
@@ -37,7 +37,7 @@
37
37
  }
38
38
  ],
39
39
  "runtime": {
40
- "driver": "powershell -NoProfile -ExecutionPolicy Bypass -File .\\tools\\harness\\tools/harness/codex-loop.ps1",
40
+ "driver": "powershell -NoProfile -ExecutionPolicy Bypass -File .\\tools\\harness\\codex-loop.ps1",
41
41
  "run_profile": ".codex\\task-run-profile.json",
42
42
  "session": {
43
43
  "mode": "fresh-process",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "runtime": {
3
- "driver": "powershell -NoProfile -ExecutionPolicy Bypass -File .\\tools\\harness\\tools/harness/codex-loop.ps1",
3
+ "driver": "powershell -NoProfile -ExecutionPolicy Bypass -File .\\tools\\harness\\codex-loop.ps1",
4
4
  "trace_dir": "traces",
5
5
  "agent": "codex",
6
6
  "run_profile": ".codex\\task-run-profile.json",
@@ -11,7 +11,7 @@ if ([string]::IsNullOrWhiteSpace($ProjectRoot)) {
11
11
  $ProjectRoot = [System.IO.Path]::GetFullPath((Join-Path $PSScriptRoot "..\.."))
12
12
  }
13
13
 
14
- function Invoke-VerificationCommand {
14
+ function Invoke-VerificationCommand {
15
15
  param(
16
16
  [string]$Command,
17
17
  [string]$Root
@@ -27,8 +27,22 @@ function Invoke-VerificationCommand {
27
27
  }
28
28
  finally {
29
29
  Pop-Location
30
- }
31
- }
30
+ }
31
+ }
32
+
33
+ function Invoke-TaskStructureValidation {
34
+ param([string]$Root)
35
+
36
+ $scriptPath = Join-Path $Root "tools\harness\task-structure-lint.ps1"
37
+ if (-not (Test-Path -LiteralPath $scriptPath)) {
38
+ throw "缺少 task structure lint: tools\\harness\\task-structure-lint.ps1"
39
+ }
40
+
41
+ & powershell -NoProfile -ExecutionPolicy Bypass -File $scriptPath -ProjectRoot $Root | Out-Host
42
+ if ($LASTEXITCODE -ne 0) {
43
+ throw "task structure 校验失败。"
44
+ }
45
+ }
32
46
 
33
47
  function Get-CodexHookCommand {
34
48
  param(
@@ -166,15 +180,17 @@ function Test-CodexHookCommandCompatibility {
166
180
  function Test-PowerShellSyntax {
167
181
  param([string]$Root)
168
182
 
169
- $paths = @(
183
+ $paths = @(
170
184
  "tools\install\bootstrap-codex-harness.ps1",
171
185
  "tools\harness\codex-loop.ps1",
172
186
  "tools\install\env-check.ps1",
173
187
  "tools\install\install-agent.ps1",
174
188
  "tools\install\install-agent-here.ps1",
189
+ "tools\harness\task-structure-lint.ps1",
175
190
  "tools\harness\verify.ps1",
176
191
  "docs\codex-harness-engineering\templates\bootstrap-codex-harness.ps1",
177
- "docs\codex-harness-engineering\templates\hooks\hook-stop-verify.ps1"
192
+ "docs\codex-harness-engineering\templates\hooks\hook-stop-verify.ps1",
193
+ "docs\codex-harness-engineering\templates\tools\harness\task-structure-lint.ps1"
178
194
  )
179
195
 
180
196
  foreach ($relativePath in $paths) {
@@ -273,13 +289,14 @@ function Invoke-AgentPackageFreshnessCheck {
273
289
 
274
290
  $resolvedProjectRoot = (Resolve-Path -LiteralPath $ProjectRoot).Path
275
291
 
276
- foreach ($command in $Commands) {
277
- Invoke-VerificationCommand -Command $command -Root $resolvedProjectRoot
278
- }
279
-
280
- Test-CodexHookCommandCompatibility -Root $resolvedProjectRoot -AdditionalPaths $HookConfigPaths
281
- Test-PowerShellSyntax -Root $resolvedProjectRoot
282
- Test-NoStaleFeedbackMcpReferences -Root $resolvedProjectRoot
292
+ foreach ($command in $Commands) {
293
+ Invoke-VerificationCommand -Command $command -Root $resolvedProjectRoot
294
+ }
295
+
296
+ Invoke-TaskStructureValidation -Root $resolvedProjectRoot
297
+ Test-CodexHookCommandCompatibility -Root $resolvedProjectRoot -AdditionalPaths $HookConfigPaths
298
+ Test-PowerShellSyntax -Root $resolvedProjectRoot
299
+ Test-NoStaleFeedbackMcpReferences -Root $resolvedProjectRoot
283
300
  Invoke-AgentPackageFreshnessCheck -Root $resolvedProjectRoot
284
301
 
285
302
  Write-Output "Verification passed."
@@ -0,0 +1,399 @@
1
+ param(
2
+ [string]$ProjectRoot = "",
3
+ [string]$TaskFile = "task.json",
4
+ [switch]$JsonOutput
5
+ )
6
+
7
+ Set-StrictMode -Version Latest
8
+ $ErrorActionPreference = "Stop"
9
+
10
+ if ([string]::IsNullOrWhiteSpace($ProjectRoot)) {
11
+ $candidateRoots = @(
12
+ [System.IO.Path]::GetFullPath((Join-Path $PSScriptRoot "..\..")),
13
+ [System.IO.Path]::GetFullPath((Join-Path $PSScriptRoot "..\..\..\..\.."))
14
+ )
15
+
16
+ foreach ($candidateRoot in $candidateRoots | Select-Object -Unique) {
17
+ if (Test-Path -LiteralPath (Join-Path $candidateRoot "task.json")) {
18
+ $ProjectRoot = $candidateRoot
19
+ break
20
+ }
21
+ }
22
+
23
+ if ([string]::IsNullOrWhiteSpace($ProjectRoot)) {
24
+ $ProjectRoot = $candidateRoots[0]
25
+ }
26
+ }
27
+
28
+ $script:Findings = New-Object System.Collections.Generic.List[object]
29
+
30
+ function Get-ObjectPropertyValue {
31
+ param(
32
+ [object]$InputObject,
33
+ [string]$Name,
34
+ [object]$Default = $null
35
+ )
36
+
37
+ if ($null -eq $InputObject) {
38
+ return $Default
39
+ }
40
+
41
+ $property = $InputObject.PSObject.Properties[$Name]
42
+ if ($null -eq $property) {
43
+ return $Default
44
+ }
45
+
46
+ return $property.Value
47
+ }
48
+
49
+ function ConvertTo-StringArray {
50
+ param([object]$Value)
51
+
52
+ if ($null -eq $Value) {
53
+ return @()
54
+ }
55
+
56
+ if ($Value -is [string]) {
57
+ if ([string]::IsNullOrWhiteSpace($Value)) {
58
+ return @()
59
+ }
60
+
61
+ return @([string]$Value)
62
+ }
63
+
64
+ $items = @()
65
+ foreach ($item in $Value) {
66
+ if ($null -ne $item -and -not [string]::IsNullOrWhiteSpace([string]$item)) {
67
+ $items += [string]$item
68
+ }
69
+ }
70
+
71
+ return $items
72
+ }
73
+
74
+ function Add-Finding {
75
+ param(
76
+ [string]$Severity,
77
+ [string]$Code,
78
+ [string]$Message,
79
+ [string]$TaskId = "",
80
+ [string]$Field = ""
81
+ )
82
+
83
+ $script:Findings.Add([pscustomobject]@{
84
+ severity = $Severity
85
+ code = $Code
86
+ message = $Message
87
+ task_id = $TaskId
88
+ field = $Field
89
+ })
90
+ }
91
+
92
+ function Read-JsonFileOrNull {
93
+ param([string]$Path)
94
+
95
+ try {
96
+ return (Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json)
97
+ }
98
+ catch {
99
+ return $null
100
+ }
101
+ }
102
+
103
+ function Get-TaskList {
104
+ param([object]$TaskDocument)
105
+
106
+ $tasks = Get-ObjectPropertyValue -InputObject $TaskDocument -Name "tasks" -Default $null
107
+ if ($null -eq $tasks -or ($tasks -is [string]) -or (-not ($tasks -is [System.Collections.IEnumerable]))) {
108
+ return @()
109
+ }
110
+
111
+ return @($tasks)
112
+ }
113
+
114
+ function Test-FileExists {
115
+ param(
116
+ [string]$Root,
117
+ [string]$RelativePath
118
+ )
119
+
120
+ return (Test-Path -LiteralPath (Join-Path $Root $RelativePath))
121
+ }
122
+
123
+ function Test-IsFormalTaskQueue {
124
+ param([object[]]$Tasks)
125
+
126
+ foreach ($task in @($Tasks)) {
127
+ $taskKind = [string](Get-ObjectPropertyValue -InputObject $task -Name "task_kind" -Default "")
128
+ if (@("feature_research", "feature_spec", "feature_design", "feature_plan", "feature_impl", "release", "archive") -contains $taskKind) {
129
+ return $true
130
+ }
131
+ }
132
+
133
+ return $false
134
+ }
135
+
136
+ function Test-ApprovedDecompositionExemption {
137
+ param(
138
+ [object]$Task,
139
+ [string]$Scope
140
+ )
141
+
142
+ $exemption = Get-ObjectPropertyValue -InputObject $Task -Name "decomposition_exemption" -Default $null
143
+ if ($null -eq $exemption) {
144
+ return $false
145
+ }
146
+
147
+ $approved = [bool](Get-ObjectPropertyValue -InputObject $exemption -Name "user_approved" -Default $false)
148
+ if (-not $approved) {
149
+ return $false
150
+ }
151
+
152
+ $scopes = @(ConvertTo-StringArray -Value (Get-ObjectPropertyValue -InputObject $exemption -Name "scopes" -Default @()))
153
+ if ($scopes.Count -eq 0) {
154
+ return $true
155
+ }
156
+
157
+ return $scopes -contains $Scope
158
+ }
159
+
160
+ function Get-OwnedSurfaceKinds {
161
+ param([string[]]$OwnedPaths)
162
+
163
+ $hasBackend = $false
164
+ $hasFrontend = $false
165
+
166
+ foreach ($rawPath in @($OwnedPaths)) {
167
+ $path = $rawPath.Replace('/', '\').ToLowerInvariant()
168
+ if ($path -match '(^|\\)(apps\\api|backend|server|services?)($|\\)') {
169
+ $hasBackend = $true
170
+ }
171
+
172
+ if ($path -match '(^|\\)(apps\\admin|apps\\web|apps\\mobile|frontend|web|mobile|ui)($|\\)') {
173
+ $hasFrontend = $true
174
+ }
175
+ }
176
+
177
+ return [pscustomobject]@{
178
+ backend = $hasBackend
179
+ frontend = $hasFrontend
180
+ }
181
+ }
182
+
183
+ function Get-DomainMatches {
184
+ param([object]$Task)
185
+
186
+ $description = [string](Get-ObjectPropertyValue -InputObject $Task -Name "description" -Default "")
187
+ $steps = ConvertTo-StringArray -Value (Get-ObjectPropertyValue -InputObject $Task -Name "steps" -Default @())
188
+ $requirementIds = ConvertTo-StringArray -Value (Get-ObjectPropertyValue -InputObject $Task -Name "requirement_ids" -Default @())
189
+ $haystack = (($description + " " + ($steps -join " ") + " " + ($requirementIds -join " ")).ToLowerInvariant())
190
+
191
+ $domainMatchesList = @()
192
+ $patterns = @(
193
+ @{ name = "order"; pattern = '(订单|order)' },
194
+ @{ name = "payment"; pattern = '(支付|payment)' },
195
+ @{ name = "inventory"; pattern = '(库存|inventory)' },
196
+ @{ name = "rbac"; pattern = '(rbac|角色权限|权限角色|role[ -]?based access)' }
197
+ )
198
+
199
+ foreach ($entry in $patterns) {
200
+ if ($haystack -match $entry["pattern"]) {
201
+ $domainMatchesList += [string]$entry["name"]
202
+ }
203
+ }
204
+
205
+ return @($domainMatchesList | Select-Object -Unique)
206
+ }
207
+
208
+ function Test-RequiredSourceCaseIds {
209
+ param(
210
+ [object]$ParentObject,
211
+ [string]$PropertyName,
212
+ [string]$TaskId,
213
+ [string]$FieldBase
214
+ )
215
+
216
+ $target = Get-ObjectPropertyValue -InputObject $ParentObject -Name $PropertyName -Default $null
217
+ if ($null -eq $target) {
218
+ Add-Finding -Severity "error" -Code "missing_field" -Message "任务缺少 $FieldBase.$PropertyName。" -TaskId $TaskId -Field "$FieldBase.$PropertyName"
219
+ return
220
+ }
221
+
222
+ $sourceCaseIds = @(ConvertTo-StringArray -Value (Get-ObjectPropertyValue -InputObject $target -Name "source_case_ids" -Default @()))
223
+ if ($sourceCaseIds.Count -eq 0) {
224
+ Add-Finding -Severity "error" -Code "empty_source_case_ids" -Message "任务缺少 $FieldBase.$PropertyName.source_case_ids。" -TaskId $TaskId -Field "$FieldBase.$PropertyName.source_case_ids"
225
+ }
226
+ }
227
+
228
+ function Test-FormalTaskQueueStructure {
229
+ param(
230
+ [string]$Root,
231
+ [object[]]$Tasks
232
+ )
233
+
234
+ $taskIdSet = @{}
235
+ foreach ($task in @($Tasks)) {
236
+ $taskId = [string](Get-ObjectPropertyValue -InputObject $task -Name "id" -Default "")
237
+ if (-not [string]::IsNullOrWhiteSpace($taskId)) {
238
+ $taskIdSet[$taskId] = $true
239
+ }
240
+ }
241
+
242
+ foreach ($requiredTaskId in @("ANALYSIS-001", "TESTCASE-001", "PLAN-001")) {
243
+ if (-not $taskIdSet.ContainsKey($requiredTaskId)) {
244
+ Add-Finding -Severity "error" -Code "missing_phase_task" -Message "正式任务队列缺少必需 phase:$requiredTaskId。" -Field "tasks"
245
+ }
246
+ }
247
+
248
+ $featureImplTasks = @($Tasks | Where-Object { [string](Get-ObjectPropertyValue -InputObject $_ -Name "task_kind" -Default "") -eq "feature_impl" })
249
+ $releaseTasks = @($Tasks | Where-Object { [string](Get-ObjectPropertyValue -InputObject $_ -Name "task_kind" -Default "") -eq "release" })
250
+
251
+ if (($featureImplTasks.Count + $releaseTasks.Count) -gt 0) {
252
+ $requiredTestingArtifacts = @(
253
+ "docs\testing\ACCEPTANCE_CRITERIA.md",
254
+ "docs\testing\NATURAL_LANGUAGE_TEST_CASES.md",
255
+ "docs\testing\TRACEABILITY_MATRIX.md",
256
+ "docs\testing\TEST_DATA_MATRIX.md",
257
+ "docs\testing\REGRESSION_PLAN.md",
258
+ "docs\testing\verify-matrix.md"
259
+ )
260
+
261
+ foreach ($relativePath in $requiredTestingArtifacts) {
262
+ if (-not (Test-FileExists -Root $Root -RelativePath $relativePath)) {
263
+ Add-Finding -Severity "error" -Code "missing_testing_artifact" -Message "正式任务队列缺少测试真相源:$relativePath。" -Field $relativePath
264
+ }
265
+ }
266
+ }
267
+
268
+ foreach ($task in $featureImplTasks) {
269
+ $taskId = [string](Get-ObjectPropertyValue -InputObject $task -Name "id" -Default "")
270
+ $qaContract = Get-ObjectPropertyValue -InputObject $task -Name "qa_contract" -Default $null
271
+ if ($null -eq $qaContract) {
272
+ Add-Finding -Severity "error" -Code "missing_qa_contract" -Message "feature_impl 缺少 qa_contract。" -TaskId $taskId -Field "qa_contract"
273
+ }
274
+ else {
275
+ $requiredLayers = @(ConvertTo-StringArray -Value (Get-ObjectPropertyValue -InputObject $qaContract -Name "required_layers" -Default @()))
276
+ foreach ($layer in @("unit_or_component", "contract_or_api", "story_full_chain", "affected_regression")) {
277
+ if ($requiredLayers -notcontains $layer) {
278
+ Add-Finding -Severity "error" -Code "missing_required_layer" -Message "feature_impl 缺少 qa_contract.required_layers[$layer]。" -TaskId $taskId -Field "qa_contract.required_layers"
279
+ }
280
+ }
281
+
282
+ $developmentValidation = Get-ObjectPropertyValue -InputObject $qaContract -Name "development_validation" -Default $null
283
+ if ($null -eq $developmentValidation) {
284
+ Add-Finding -Severity "error" -Code "missing_development_validation" -Message "feature_impl 缺少 qa_contract.development_validation。" -TaskId $taskId -Field "qa_contract.development_validation"
285
+ }
286
+
287
+ $acceptanceValidation = Get-ObjectPropertyValue -InputObject $qaContract -Name "acceptance_validation" -Default $null
288
+ if ($null -eq $acceptanceValidation) {
289
+ Add-Finding -Severity "error" -Code "missing_acceptance_validation" -Message "feature_impl 缺少 qa_contract.acceptance_validation。" -TaskId $taskId -Field "qa_contract.acceptance_validation"
290
+ }
291
+
292
+ $tddContract = Get-ObjectPropertyValue -InputObject $qaContract -Name "tdd_contract" -Default $null
293
+ if ($null -eq $tddContract) {
294
+ Add-Finding -Severity "error" -Code "missing_tdd_contract" -Message "feature_impl 缺少 qa_contract.tdd_contract。" -TaskId $taskId -Field "qa_contract.tdd_contract"
295
+ }
296
+ else {
297
+ $red = Get-ObjectPropertyValue -InputObject $tddContract -Name "red" -Default $null
298
+ if ($null -eq $red) {
299
+ Add-Finding -Severity "error" -Code "missing_tdd_red" -Message "feature_impl 缺少 qa_contract.tdd_contract.red。" -TaskId $taskId -Field "qa_contract.tdd_contract.red"
300
+ }
301
+ else {
302
+ $sourceCaseIds = @(ConvertTo-StringArray -Value (Get-ObjectPropertyValue -InputObject $red -Name "source_case_ids" -Default @()))
303
+ if ($sourceCaseIds.Count -eq 0) {
304
+ Add-Finding -Severity "error" -Code "empty_source_case_ids" -Message "feature_impl 缺少 qa_contract.tdd_contract.red.source_case_ids。" -TaskId $taskId -Field "qa_contract.tdd_contract.red.source_case_ids"
305
+ }
306
+ }
307
+
308
+ foreach ($requiredField in @("green", "refactor_guard")) {
309
+ if ($null -eq (Get-ObjectPropertyValue -InputObject $tddContract -Name $requiredField -Default $null)) {
310
+ Add-Finding -Severity "error" -Code "missing_tdd_field" -Message "feature_impl 缺少 qa_contract.tdd_contract.$requiredField。" -TaskId $taskId -Field "qa_contract.tdd_contract.$requiredField"
311
+ }
312
+ }
313
+ }
314
+
315
+ Test-RequiredSourceCaseIds -ParentObject $qaContract -PropertyName "story_full_chain" -TaskId $taskId -FieldBase "qa_contract"
316
+ if ($null -ne $acceptanceValidation) {
317
+ $acceptanceSourceCaseIds = @(ConvertTo-StringArray -Value (Get-ObjectPropertyValue -InputObject $acceptanceValidation -Name "source_case_ids" -Default @()))
318
+ if ($acceptanceSourceCaseIds.Count -eq 0) {
319
+ Add-Finding -Severity "error" -Code "empty_source_case_ids" -Message "feature_impl 缺少 qa_contract.acceptance_validation.source_case_ids。" -TaskId $taskId -Field "qa_contract.acceptance_validation.source_case_ids"
320
+ }
321
+ }
322
+ }
323
+
324
+ $requirementIds = @(ConvertTo-StringArray -Value (Get-ObjectPropertyValue -InputObject $task -Name "requirement_ids" -Default @()))
325
+ if ($requirementIds.Count -gt 3 -and -not (Test-ApprovedDecompositionExemption -Task $task -Scope "requirement_span")) {
326
+ Add-Finding -Severity "error" -Code "oversized_requirement_span" -Message "单个 feature_impl 默认不得覆盖超过 3 条主 requirement_ids。" -TaskId $taskId -Field "requirement_ids"
327
+ }
328
+
329
+ $ownedPaths = @(ConvertTo-StringArray -Value (Get-ObjectPropertyValue -InputObject $task -Name "owned_paths" -Default @()))
330
+ $surfaceKinds = Get-OwnedSurfaceKinds -OwnedPaths $ownedPaths
331
+ if ($surfaceKinds.backend -and $surfaceKinds.frontend -and -not (Test-ApprovedDecompositionExemption -Task $task -Scope "mixed_surface")) {
332
+ Add-Finding -Severity "error" -Code "mixed_frontend_backend_story" -Message "feature_impl 同时覆盖前端与后端路径;请拆分为独立 story 或改为明确的 release 收口。" -TaskId $taskId -Field "owned_paths"
333
+ }
334
+
335
+ $domains = @(Get-DomainMatches -Task $task)
336
+ if ($domains.Count -gt 1 -and -not (Test-ApprovedDecompositionExemption -Task $task -Scope "domain_bundle")) {
337
+ Add-Finding -Severity "error" -Code "combined_high_risk_domains" -Message ("feature_impl 同时覆盖高风险主域:{0}。订单、支付、库存、RBAC 必须拆独立任务。" -f ($domains -join ", ")) -TaskId $taskId -Field "description"
338
+ }
339
+ }
340
+ }
341
+
342
+ $resolvedProjectRoot = (Resolve-Path -LiteralPath $ProjectRoot).Path
343
+ $taskPath = Join-Path $resolvedProjectRoot $TaskFile
344
+
345
+ if (-not (Test-Path -LiteralPath $taskPath)) {
346
+ Add-Finding -Severity "error" -Code "missing_task_file" -Message "缺少 task.json:$TaskFile。" -Field $TaskFile
347
+ }
348
+ else {
349
+ $taskDocument = Read-JsonFileOrNull -Path $taskPath
350
+ if ($null -eq $taskDocument) {
351
+ Add-Finding -Severity "error" -Code "invalid_task_json" -Message "task.json 不是合法 JSON。" -Field $TaskFile
352
+ }
353
+ else {
354
+ $tasks = Get-TaskList -TaskDocument $taskDocument
355
+ if ($tasks.Count -eq 0) {
356
+ Add-Finding -Severity "error" -Code "empty_task_queue" -Message "task.json 不包含可校验任务。" -Field "tasks"
357
+ }
358
+ elseif (Test-IsFormalTaskQueue -Tasks $tasks) {
359
+ Test-FormalTaskQueueStructure -Root $resolvedProjectRoot -Tasks $tasks
360
+ }
361
+ }
362
+ }
363
+
364
+ $errors = @($script:Findings | Where-Object { $_.severity -eq "error" })
365
+ $warnings = @($script:Findings | Where-Object { $_.severity -eq "warning" })
366
+ $status = if ($errors.Count -gt 0) { "fail" } elseif ($warnings.Count -gt 0) { "warn" } else { "pass" }
367
+ $result = @{
368
+ status = $status
369
+ error_count = $errors.Count
370
+ warning_count = $warnings.Count
371
+ findings = @($script:Findings.ToArray())
372
+ }
373
+
374
+ if ($JsonOutput) {
375
+ $result | ConvertTo-Json -Depth 8
376
+ }
377
+ else {
378
+ if ($status -eq "fail") {
379
+ Write-Output "Task structure validation failed."
380
+ }
381
+ elseif ($status -eq "warn") {
382
+ Write-Output "Task structure validation passed with warnings."
383
+ }
384
+ else {
385
+ Write-Output "Task structure validation passed."
386
+ }
387
+
388
+ foreach ($finding in $script:Findings.ToArray()) {
389
+ $taskLabel = if ([string]::IsNullOrWhiteSpace([string]$finding.task_id)) { "" } else { " task=$($finding.task_id)" }
390
+ $fieldLabel = if ([string]::IsNullOrWhiteSpace([string]$finding.field)) { "" } else { " field=$($finding.field)" }
391
+ Write-Output "- [$($finding.severity)] $($finding.code)$taskLabel$fieldLabel :: $($finding.message)"
392
+ }
393
+ }
394
+
395
+ if ($status -eq "fail") {
396
+ exit 1
397
+ }
398
+
399
+ exit 0
@@ -2,7 +2,7 @@
2
2
  "schemaVersion": "0.1",
3
3
  "status": "draft",
4
4
  "package": "codex-harness",
5
- "version": "0.1.7",
5
+ "version": "0.1.8",
6
6
  "installModes": [
7
7
  {
8
8
  "name": "user",
@@ -225,16 +225,28 @@ function Ensure-TaskFile {
225
225
  return Copy-ManagedFile -SourceRoot $SourceRoot -SourceRelativePath "runtime\task.json" -DestinationRoot $DestinationRoot -DestinationRelativePath "task.json" -Overwrite:$true
226
226
  }
227
227
 
228
- $taskDocument = $taskContent | ConvertFrom-Json
229
- if ($null -eq $taskDocument.runtime) {
230
- $taskDocument | Add-Member -NotePropertyName "runtime" -NotePropertyValue ([PSCustomObject]@{}) -Force
231
- }
232
-
233
- if ([string]::IsNullOrWhiteSpace($taskDocument.runtime.driver)) {
234
- $taskDocument.runtime.driver = "powershell -NoProfile -ExecutionPolicy Bypass -File .\tools\harness\codex-loop.ps1"
235
- $taskDocument | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $taskPath -Encoding UTF8
236
- return [PSCustomObject]@{
237
- Path = $taskPath
228
+ $taskDocument = $taskContent | ConvertFrom-Json
229
+ if ($null -eq $taskDocument.runtime) {
230
+ $taskDocument | Add-Member -NotePropertyName "runtime" -NotePropertyValue ([PSCustomObject]@{}) -Force
231
+ }
232
+
233
+ $canonicalDriver = "powershell -NoProfile -ExecutionPolicy Bypass -File .\tools\harness\codex-loop.ps1"
234
+ $driverPattern = 'tools[\\/]+harness[\\/]+tools[\\/]+harness[\\/]+codex-loop\.ps1'
235
+
236
+ if ([string]::IsNullOrWhiteSpace($taskDocument.runtime.driver)) {
237
+ $taskDocument.runtime.driver = $canonicalDriver
238
+ $taskDocument | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $taskPath -Encoding UTF8
239
+ return [PSCustomObject]@{
240
+ Path = $taskPath
241
+ Action = "updated"
242
+ }
243
+ }
244
+
245
+ if ([string]$taskDocument.runtime.driver -match $driverPattern) {
246
+ $taskDocument.runtime.driver = $canonicalDriver
247
+ $taskDocument | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $taskPath -Encoding UTF8
248
+ return [PSCustomObject]@{
249
+ Path = $taskPath
238
250
  Action = "updated"
239
251
  }
240
252
  }
@@ -601,6 +613,7 @@ $rootFiles = @(
601
613
  @{ Source = "prompts\worker-role\harness-writer.md"; Destination = ".codex\prompts\worker-role\harness-writer.md" },
602
614
  @{ Source = "runtime\smoke-task.json"; Destination = "tools\harness\templates\smoke-task.json" },
603
615
  @{ Source = "runtime\project-task-template.json"; Destination = "tools\harness\templates\project-task-template.json" },
616
+ @{ Source = "tools\harness\task-structure-lint.ps1"; Destination = "tools\harness\task-structure-lint.ps1" },
604
617
  @{ Source = "runtime\verify.ps1"; Destination = "tools\harness\verify.ps1" },
605
618
  @{ Source = "tools\harness\docs-lint.ps1"; Destination = "tools\harness\docs-lint.ps1" },
606
619
  @{ Source = "tools\harness\data-lint.ps1"; Destination = "tools\harness\data-lint.ps1" },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pzy560117/codex-harness",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "description": "Codex Harness installer and project runtime CLI",
5
5
  "type": "module",
6
6
  "bin": {