@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 +1 -1
- package/package-source/AGENTS.md +7 -0
- package/package-source/docs/codex-harness-engineering/templates/bootstrap-codex-harness.ps1 +57 -48
- package/package-source/docs/codex-harness-engineering/templates/docs/task-session-strategy.md +4 -0
- package/package-source/docs/codex-harness-engineering/templates/hooks/hook-stop-verify.ps1 +76 -10
- package/package-source/docs/codex-harness-engineering/templates/package-assets/docs/codex-harness-engineering/examples/ticket-filter-demo/task.json +2 -2
- package/package-source/docs/codex-harness-engineering/templates/runtime/AGENTS.md +4 -0
- package/package-source/docs/codex-harness-engineering/templates/runtime/doctor.ps1 +89 -24
- package/package-source/docs/codex-harness-engineering/templates/runtime/project-task-template.json +81 -66
- package/package-source/docs/codex-harness-engineering/templates/runtime/smoke-task.json +1 -1
- package/package-source/docs/codex-harness-engineering/templates/runtime/task.json +1 -1
- package/package-source/docs/codex-harness-engineering/templates/runtime/verify.ps1 +29 -12
- package/package-source/docs/codex-harness-engineering/templates/tools/harness/task-structure-lint.ps1 +399 -0
- package/package-source/install-manifest.json +1 -1
- package/package-source/tools/install/bootstrap-codex-harness.ps1 +23 -10
- package/package.json +1 -1
package/README.md
CHANGED
package/package-source/AGENTS.md
CHANGED
|
@@ -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
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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
|
-
|
|
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 = "
|
|
565
|
-
@{ Source = "runtime\codex-loop.ps1"; Destination = "tools
|
|
566
|
-
@{ Source = "runtime\doctor.ps1"; Destination = "tools
|
|
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 = "
|
|
590
|
-
@{ Source = "
|
|
591
|
-
@{ Source = "tools\harness\docs-lint.ps1"; Destination = "tools
|
|
592
|
-
@{ Source = "tools\harness\data-lint.ps1"; Destination = "tools
|
|
593
|
-
@{ Source = "tools\harness\integration-lint.ps1"; Destination = "tools
|
|
594
|
-
@{ Source = "tools\harness\mobile-lint.ps1"; Destination = "tools
|
|
595
|
-
@{ Source = "tools\harness\acceptance-lint.ps1"; Destination = "tools
|
|
596
|
-
@{ Source = "tools\harness\
|
|
597
|
-
@{ Source = "
|
|
598
|
-
@{ Source = "
|
|
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" },
|
package/package-source/docs/codex-harness-engineering/templates/docs/task-session-strategy.md
CHANGED
|
@@ -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
|
-
$
|
|
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\\
|
|
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\\
|
|
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
|
-
$
|
|
1149
|
-
|
|
1150
|
-
|
|
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 `
|
package/package-source/docs/codex-harness-engineering/templates/runtime/project-task-template.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"runtime": {
|
|
3
|
-
"driver": "powershell -NoProfile -ExecutionPolicy Bypass -File .\\tools\\harness\\
|
|
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
|
-
"
|
|
99
|
-
"
|
|
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/
|
|
499
|
-
"docs/testing/
|
|
500
|
-
"docs/testing/
|
|
501
|
-
"docs/testing/
|
|
502
|
-
"docs/testing/
|
|
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
|
-
"
|
|
527
|
-
"
|
|
528
|
-
|
|
529
|
-
|
|
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/
|
|
664
|
-
"docs/testing/
|
|
665
|
-
"docs/testing/
|
|
666
|
-
"docs/testing/
|
|
667
|
-
"docs/testing/
|
|
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
|
-
"
|
|
692
|
-
"
|
|
693
|
-
|
|
694
|
-
|
|
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/
|
|
829
|
-
"docs/testing/
|
|
830
|
-
"docs/testing/
|
|
831
|
-
"docs/testing/
|
|
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
|
-
"
|
|
856
|
-
"
|
|
857
|
-
|
|
858
|
-
|
|
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\\
|
|
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\\
|
|
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
|
-
|
|
281
|
-
Test-
|
|
282
|
-
Test-
|
|
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
|
|
@@ -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
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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" },
|