@pzy560117/codex-harness 0.1.7 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +1 -1
  2. package/package-source/AGENTS.md +18 -1
  3. package/package-source/docs/codex-harness-engineering/templates/bootstrap-codex-harness.ps1 +57 -48
  4. package/package-source/docs/codex-harness-engineering/templates/config/rules/agents.md +3 -3
  5. package/package-source/docs/codex-harness-engineering/templates/docs/project-agents-template.md +8 -1
  6. package/package-source/docs/codex-harness-engineering/templates/docs/task-session-strategy.md +7 -0
  7. package/package-source/docs/codex-harness-engineering/templates/hooks/hook-stop-verify.ps1 +76 -10
  8. package/package-source/docs/codex-harness-engineering/templates/package-assets/docs/codex-harness-engineering/examples/ticket-filter-demo/task.json +2 -2
  9. package/package-source/docs/codex-harness-engineering/templates/package-assets/docs/codex-harness-engineering/project-agents-template.md +51 -1
  10. package/package-source/docs/codex-harness-engineering/templates/package-assets/docs/codex-harness-engineering/templates/prompts/controller-loop.md +88 -0
  11. package/package-source/docs/codex-harness-engineering/templates/package-assets/docs/codex-harness-engineering/templates/prompts/failure-triage.md +71 -0
  12. package/package-source/docs/codex-harness-engineering/templates/package-assets/docs/codex-harness-engineering/templates/prompts/harness-audit.md +54 -0
  13. package/package-source/docs/codex-harness-engineering/templates/package-assets/docs/codex-harness-engineering/templates/prompts/implement-one-task.md +18 -15
  14. package/package-source/docs/codex-harness-engineering/templates/package-assets/docs/codex-harness-engineering/templates/prompts/repair-one-finding.md +1 -2
  15. package/package-source/docs/codex-harness-engineering/templates/package-assets/docs/codex-harness-engineering/templates/prompts/review-one-task.md +45 -0
  16. package/package-source/docs/codex-harness-engineering/templates/package-assets/docs/codex-harness-engineering/templates/prompts/review-stage1-spec.md +111 -0
  17. package/package-source/docs/codex-harness-engineering/templates/package-assets/docs/codex-harness-engineering/templates/prompts/review-stage2-quality.md +82 -0
  18. package/package-source/docs/codex-harness-engineering/templates/package-assets/docs/codex-harness-engineering/templates/prompts/visual-evaluator.md +80 -0
  19. package/package-source/docs/codex-harness-engineering/templates/package-assets/docs/codex-harness-engineering/templates/prompts/worker-role/backend-worker.md +41 -0
  20. package/package-source/docs/codex-harness-engineering/templates/package-assets/docs/codex-harness-engineering/templates/prompts/worker-role/docs-worker.md +28 -0
  21. package/package-source/docs/codex-harness-engineering/templates/package-assets/docs/codex-harness-engineering/templates/prompts/worker-role/frontend-worker.md +46 -0
  22. package/package-source/docs/codex-harness-engineering/templates/package-assets/docs/codex-harness-engineering/templates/prompts/worker-role/harness-writer.md +40 -0
  23. package/package-source/docs/codex-harness-engineering/templates/package-assets/docs/codex-harness-engineering/templates/prompts/worker-role/test-runner.md +27 -0
  24. package/package-source/docs/codex-harness-engineering/templates/package-assets/rules/agents.md +6 -3
  25. package/package-source/docs/codex-harness-engineering/templates/package-assets/skills/auto-commit/SKILL.md +1 -1
  26. package/package-source/docs/codex-harness-engineering/templates/package-assets/skills/speckit-plan/SKILL.md +1 -1
  27. package/package-source/docs/codex-harness-engineering/templates/prompts/controller-loop.md +4 -4
  28. package/package-source/docs/codex-harness-engineering/templates/prompts/failure-triage.md +1 -1
  29. package/package-source/docs/codex-harness-engineering/templates/prompts/harness-audit.md +1 -1
  30. package/package-source/docs/codex-harness-engineering/templates/prompts/implement-one-task.md +18 -15
  31. package/package-source/docs/codex-harness-engineering/templates/prompts/repair-one-finding.md +1 -2
  32. package/package-source/docs/codex-harness-engineering/templates/prompts/review-one-task.md +1 -1
  33. package/package-source/docs/codex-harness-engineering/templates/prompts/review-stage1-spec.md +1 -1
  34. package/package-source/docs/codex-harness-engineering/templates/prompts/review-stage2-quality.md +1 -1
  35. package/package-source/docs/codex-harness-engineering/templates/prompts/visual-evaluator.md +1 -1
  36. package/package-source/docs/codex-harness-engineering/templates/prompts/worker-role/backend-worker.md +2 -2
  37. package/package-source/docs/codex-harness-engineering/templates/prompts/worker-role/docs-worker.md +2 -2
  38. package/package-source/docs/codex-harness-engineering/templates/prompts/worker-role/frontend-worker.md +2 -2
  39. package/package-source/docs/codex-harness-engineering/templates/prompts/worker-role/harness-writer.md +4 -4
  40. package/package-source/docs/codex-harness-engineering/templates/prompts/worker-role/test-runner.md +1 -1
  41. package/package-source/docs/codex-harness-engineering/templates/runtime/AGENTS.md +45 -46
  42. package/package-source/docs/codex-harness-engineering/templates/runtime/doctor.ps1 +151 -42
  43. package/package-source/docs/codex-harness-engineering/templates/runtime/project-task-template.json +81 -66
  44. package/package-source/docs/codex-harness-engineering/templates/runtime/smoke-task.json +1 -1
  45. package/package-source/docs/codex-harness-engineering/templates/runtime/task.json +1 -1
  46. package/package-source/docs/codex-harness-engineering/templates/runtime/verify.ps1 +29 -12
  47. package/package-source/docs/codex-harness-engineering/templates/tools/harness/task-structure-lint.ps1 +399 -0
  48. package/package-source/install-manifest.json +1 -1
  49. package/package-source/tools/install/bootstrap-codex-harness.ps1 +23 -10
  50. package/package-source/tools/install/install-agent.ps1 +59 -18
  51. package/package.json +1 -1
@@ -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.9",
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" },
@@ -2022,7 +2022,7 @@ function Copy-ManifestEntries {
2022
2022
  }
2023
2023
  }
2024
2024
 
2025
- function Overlay-DirectoryIfPresent {
2025
+ function Overlay-DirectoryIfPresent {
2026
2026
  param(
2027
2027
  [string]$SourceDirectory,
2028
2028
  [string]$DestinationDirectory
@@ -2056,8 +2056,8 @@ function Overlay-DirectoryIfPresent {
2056
2056
  }
2057
2057
  }
2058
2058
 
2059
- function Ensure-ReadmeForSmoke {
2060
- param([string]$Root)
2059
+ function Ensure-ReadmeForSmoke {
2060
+ param([string]$Root)
2061
2061
 
2062
2062
  $readmePath = Join-Path $Root "README.md"
2063
2063
  if (Test-Path -LiteralPath $readmePath) {
@@ -2070,11 +2070,47 @@ function Ensure-ReadmeForSmoke {
2070
2070
  "This README was generated by install-agent.ps1 to support the initial smoke task.",
2071
2071
  "Replace it with project-specific content after harness setup is verified."
2072
2072
  ) | Set-Content -LiteralPath $readmePath -Encoding UTF8
2073
-
2074
- return $readmePath
2075
- }
2076
-
2077
- function Copy-Package {
2073
+
2074
+ return $readmePath
2075
+ }
2076
+
2077
+ function Assert-InstalledProjectScopeSurface {
2078
+ param(
2079
+ [object]$ManifestInfo,
2080
+ [string]$ProjectRoot
2081
+ )
2082
+
2083
+ $projectEntries = @(
2084
+ $ManifestInfo.Entries |
2085
+ Where-Object { [string]$_.Scope -eq "project" }
2086
+ )
2087
+
2088
+ $missingPaths = @()
2089
+ foreach ($entry in $projectEntries) {
2090
+ $targetPath = [string]$entry.TargetPath
2091
+ if ([string]::IsNullOrWhiteSpace($targetPath)) {
2092
+ continue
2093
+ }
2094
+
2095
+ if ($entry.SourceKind -eq "directory") {
2096
+ if (-not (Test-Path -LiteralPath $targetPath -PathType Container)) {
2097
+ $missingPaths += $entry.Destination
2098
+ }
2099
+ continue
2100
+ }
2101
+
2102
+ if (-not (Test-Path -LiteralPath $targetPath -PathType Leaf)) {
2103
+ $missingPaths += $entry.Destination
2104
+ }
2105
+ }
2106
+
2107
+ if ($missingPaths.Count -gt 0) {
2108
+ $details = ($missingPaths | Sort-Object -Unique | ForEach-Object { "- $_" }) -join "`n"
2109
+ throw "project scope 安装后缺少以下真相源或运行文件:`n$details"
2110
+ }
2111
+ }
2112
+
2113
+ function Copy-Package {
2078
2114
  param(
2079
2115
  [string]$SourceRoot,
2080
2116
  [string]$DestinationRoot,
@@ -2399,18 +2435,23 @@ try {
2399
2435
  Write-Step "已写入 package lock: $lockPath"
2400
2436
  }
2401
2437
 
2402
- if ($Mode -eq "project" -or $Mode -eq "vendor") {
2403
- $configPackageRoot = if ($Mode -eq "vendor") { $targetAgentsRoot } elseif ($null -ne $scopeUserPackage) { [string]$scopeUserPackage.PackageRoot } else { $effectiveSourceRoot }
2404
- $configPath = Write-ProjectHarnessConfig `
2405
- -ManifestInfo $installManifestInfo `
2438
+ if ($Mode -eq "project" -or $Mode -eq "vendor") {
2439
+ $configPackageRoot = if ($Mode -eq "vendor") { $targetAgentsRoot } elseif ($null -ne $scopeUserPackage) { [string]$scopeUserPackage.PackageRoot } else { $effectiveSourceRoot }
2440
+ $configPath = Write-ProjectHarnessConfig `
2441
+ -ManifestInfo $installManifestInfo `
2406
2442
  -ProjectRoot $resolvedProjectRoot `
2407
2443
  -InstallScope $Mode `
2408
2444
  -VendorInstalled:($Mode -eq "vendor") `
2409
- -PackageRoot $configPackageRoot
2410
- Write-Step "已写入 harness config: $configPath"
2411
- }
2412
-
2413
- $generatedReadme = $null
2445
+ -PackageRoot $configPackageRoot
2446
+ Write-Step "已写入 harness config: $configPath"
2447
+ }
2448
+
2449
+ if ($Mode -eq "project") {
2450
+ Assert-InstalledProjectScopeSurface -ManifestInfo $installManifestInfo -ProjectRoot $resolvedProjectRoot
2451
+ Write-Step "project scope 真相源与运行文件已按 manifest 落地"
2452
+ }
2453
+
2454
+ $generatedReadme = $null
2414
2455
  if ($InitSmoke) {
2415
2456
  $generatedReadme = Ensure-ReadmeForSmoke -Root $resolvedProjectRoot
2416
2457
  if ($null -ne $generatedReadme) {
@@ -2506,7 +2547,7 @@ try {
2506
2547
  Write-Output ("- powershell -NoProfile -ExecutionPolicy Bypass -File `"{0}`"" -f (Join-Path $resolvedProjectRoot 'tools\harness\codex-loop.ps1'))
2507
2548
  }
2508
2549
  }
2509
- finally {
2550
+ finally {
2510
2551
  if (($null -ne $stagedSourceRoot) -and (Test-Path -LiteralPath $stagedSourceRoot)) {
2511
2552
  Remove-Item -LiteralPath $stagedSourceRoot -Recurse -Force -ErrorAction SilentlyContinue
2512
2553
  }
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.9",
4
4
  "description": "Codex Harness installer and project runtime CLI",
5
5
  "type": "module",
6
6
  "bin": {