coding-agent-harness 1.0.8 → 1.1.0
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/CHANGELOG.md +10 -0
- package/CONTRIBUTING.md +8 -4
- package/README.md +12 -2
- package/README.zh-CN.md +10 -2
- package/SKILL.md +14 -3
- package/dist/build-dist.mjs +19 -6
- package/dist/check-dist-observation.mjs +57 -29
- package/dist/check-harness.mjs +0 -1
- package/dist/check-import-graph.mjs +44 -27
- package/dist/check-lite-forbidden-surfaces.mjs +121 -0
- package/dist/check-no-ts-nocheck.mjs +7 -7
- package/dist/check-runtime-emit.mjs +10 -3
- package/dist/check-type-boundaries.mjs +51 -9
- package/dist/commands/dashboard-command.mjs +52 -14
- package/dist/commands/migration-command.mjs +18 -8
- package/dist/commands/module-command.mjs +142 -0
- package/dist/commands/preset-command.mjs +51 -12
- package/dist/commands/registry.mjs +483 -0
- package/dist/commands/task-command.mjs +109 -52
- package/dist/harness.mjs +6 -304
- package/dist/lib/capability-registry.mjs +229 -53
- package/dist/lib/check-module-parallel.mjs +1 -6
- package/dist/lib/check-profiles.mjs +39 -46
- package/dist/lib/check-task-contracts.mjs +6 -4
- package/dist/lib/command-registry.mjs +248 -0
- package/dist/lib/core-shared.mjs +78 -3
- package/dist/lib/dashboard-data.mjs +203 -22
- package/dist/lib/dashboard-workbench.mjs +245 -21
- package/dist/lib/dashboard-writer.mjs +4 -1
- package/dist/lib/git-status-summary.mjs +0 -1
- package/dist/lib/governance-index-generator.mjs +7 -5
- package/dist/lib/governance-sync.mjs +46 -121
- package/dist/lib/governance-table-boundary.mjs +1 -14
- package/dist/lib/harness-core.mjs +4 -1
- package/dist/lib/harness-paths.mjs +115 -1
- package/dist/lib/impact-classifier.mjs +420 -0
- package/dist/lib/lesson-maintenance.mjs +1 -2
- package/dist/lib/markdown-utils.mjs +50 -1
- package/dist/lib/migration-planner.mjs +31 -16
- package/dist/lib/migration-support.mjs +5 -4
- package/dist/lib/module-registry.mjs +296 -0
- package/dist/lib/preset-audit-contracts.mjs +24 -1
- package/dist/lib/preset-engine.mjs +67 -29
- package/dist/lib/preset-registry.mjs +361 -71
- package/dist/lib/preset-runner.mjs +292 -26
- package/dist/lib/review-confirm-git-gate.mjs +73 -19
- package/dist/lib/status-builder.mjs +23 -8
- package/dist/lib/structure-migration.mjs +6 -4
- package/dist/lib/subagent-authorization-audit.mjs +8 -2
- package/dist/lib/task-archive-eligibility.mjs +65 -0
- package/dist/lib/task-audit-metadata.mjs +25 -11
- package/dist/lib/task-audit-migration.mjs +21 -14
- package/dist/lib/task-discovery-contract.mjs +32 -0
- package/dist/lib/task-index.mjs +3 -2
- package/dist/lib/task-lesson-candidates.mjs +1 -2
- package/dist/lib/task-lesson-sedimentation.mjs +310 -9
- package/dist/lib/task-lifecycle/create-task-helpers.mjs +6 -3
- package/dist/lib/task-lifecycle/phase-sync.mjs +0 -1
- package/dist/lib/task-lifecycle/preset-interop.mjs +16 -0
- package/dist/lib/task-lifecycle/review-confirm.mjs +34 -2
- package/dist/lib/task-lifecycle/review-gates.mjs +12 -5
- package/dist/lib/task-lifecycle/review-submission.mjs +1 -2
- package/dist/lib/task-lifecycle/scaffold-provenance.mjs +0 -1
- package/dist/lib/task-lifecycle/template-files.mjs +2 -5
- package/dist/lib/task-lifecycle.mjs +116 -160
- package/dist/lib/task-metadata.mjs +10 -5
- package/dist/lib/task-preset-contract-drift.mjs +45 -0
- package/dist/lib/task-repository.mjs +192 -0
- package/dist/lib/task-review-model.mjs +36 -17
- package/dist/lib/task-scanner.mjs +74 -23
- package/dist/lib/task-template-materials.mjs +131 -0
- package/dist/lib/task-tombstone-commands.mjs +186 -29
- package/dist/lib/types/check-profiles.js +1 -0
- package/dist/lib/types/impact.js +1 -0
- package/dist/lib/types/preset.js +1 -0
- package/dist/lib/types/task-lifecycle.js +1 -0
- package/dist/lib/types/task-scanner.js +1 -0
- package/dist/postinstall.mjs +2 -2
- package/dist/run-built-tests.mjs +10 -3
- package/docs-release/README.md +1 -0
- package/docs-release/architecture/document-contract-kernel/README.md +150 -0
- package/docs-release/architecture/document-contract-kernel/products/full-skill-overlay.md +29 -0
- package/docs-release/architecture/document-contract-kernel/products/lite-forbidden-surfaces.txt +26 -0
- package/docs-release/architecture/document-contract-kernel/products/lite-skill-overlay.md +37 -0
- package/docs-release/architecture/overview.md +2 -2
- package/docs-release/architecture/overview.zh-CN.md +2 -2
- package/docs-release/architecture/system-explainer/01-system-overview.md +11 -7
- package/docs-release/architecture/system-explainer/02-module-dependency.md +4 -4
- package/docs-release/architecture/system-explainer/03-task-lifecycle.md +17 -12
- package/docs-release/architecture/system-explainer/05-data-flow.md +6 -6
- package/docs-release/architecture/system-explainer/06-preset-and-migration.md +2 -2
- package/docs-release/architecture/system-explainer/README.md +1 -1
- package/docs-release/architecture/system-explainer/en-US/01-system-overview.md +12 -8
- package/docs-release/architecture/system-explainer/en-US/02-module-dependency.md +5 -5
- package/docs-release/architecture/system-explainer/en-US/03-task-lifecycle.md +19 -11
- package/docs-release/architecture/system-explainer/en-US/05-data-flow.md +5 -5
- package/docs-release/architecture/system-explainer/en-US/06-preset-and-migration.md +2 -2
- package/docs-release/architecture/system-explainer/en-US/README.md +1 -1
- package/docs-release/guides/agent-installation.en-US.md +4 -6
- package/docs-release/guides/agent-installation.md +11 -8
- package/docs-release/guides/contributing.md +10 -3
- package/docs-release/guides/contributing.zh-CN.md +10 -3
- package/docs-release/guides/legacy-migration-agent-prompt.md +1 -1
- package/docs-release/guides/legacy-migration-agent-prompt.zh-CN.md +1 -1
- package/docs-release/guides/migration-playbook.en-US.md +9 -6
- package/docs-release/guides/migration-playbook.md +9 -6
- package/docs-release/guides/preset-development.md +68 -2
- package/docs-release/guides/task-state-machine.en-US.md +8 -8
- package/docs-release/guides/task-state-machine.md +7 -7
- package/docs-release/guides/typescript-runtime-migration-closeout.md +17 -13
- package/package.json +16 -12
- package/postinstall.mjs +37 -0
- package/presets/legacy-migration/preset.yaml +5 -5
- package/presets/legacy-migration/templates/execution_strategy.append.md +1 -1
- package/presets/lesson-sedimentation/preset.yaml +3 -3
- package/presets/module/preset.yaml +2 -2
- package/presets/module/templates/execution_strategy.append.md +1 -1
- package/presets/module/templates/task_plan.append.md +3 -3
- package/presets/release-closeout/checks/check-release-package.mjs +6 -1
- package/presets/release-closeout/preset.yaml +9 -9
- package/presets/release-closeout/scripts/generate-release-package.mjs +387 -25
- package/presets/release-closeout/templates/task_plan.append.md +5 -5
- package/presets/standard-task/preset.yaml +2 -2
- package/references/agents-md-pattern.md +23 -17
- package/references/lessons-governance.md +2 -2
- package/references/module-parallel-standard.md +3 -6
- package/references/ssot-governance.md +2 -2
- package/references/taskr-gap-analysis.md +3 -3
- package/run-dist.mjs +34 -0
- package/skills/preset-creator/SKILL.md +40 -8
- package/skills/preset-creator/references/complex-task-skeleton/brief.md +32 -8
- package/skills/preset-creator/references/preset-package-skeleton.md +15 -5
- package/skills/preset-creator/references/structure-aware-paths.md +112 -0
- package/templates/AGENTS.md.template +28 -26
- package/templates/architecture/README.md +2 -2
- package/templates/architecture/service-catalog.md +2 -2
- package/templates/architecture/services/service-template.md +1 -1
- package/templates/dashboard/assets/app-src/00-state.js +5 -1
- package/templates/dashboard/assets/app-src/10-router.js +7 -0
- package/templates/dashboard/assets/app-src/20-overview.js +8 -8
- package/templates/dashboard/assets/app-src/30-tasks.js +132 -40
- package/templates/dashboard/assets/app-src/32-task-swimlane.js +314 -0
- package/templates/dashboard/assets/app-src/35-task-detail.js +35 -5
- package/templates/dashboard/assets/app-src/40-modules.js +257 -41
- package/templates/dashboard/assets/app-src/45-review.js +127 -1
- package/templates/dashboard/assets/app-src/90-bindings.js +185 -2
- package/templates/dashboard/assets/app.css +928 -53
- package/templates/dashboard/assets/app.css.manifest.json +2 -0
- package/templates/dashboard/assets/app.js +1071 -98
- package/templates/dashboard/assets/app.manifest.json +1 -0
- package/templates/dashboard/assets/css-src/00-foundation.css +12 -6
- package/templates/dashboard/assets/css-src/10-panels-flow.css +2 -2
- package/templates/dashboard/assets/css-src/30-task-index.css +21 -13
- package/templates/dashboard/assets/css-src/31-archive.css +94 -0
- package/templates/dashboard/assets/css-src/32-task-swimlane.css +487 -0
- package/templates/dashboard/assets/css-src/35-review-workspace.css +78 -0
- package/templates/dashboard/assets/css-src/40-detail-modules-migration.css +191 -14
- package/templates/dashboard/assets/css-src/50-responsive-overrides.css +23 -0
- package/templates/dashboard/assets/i18n.js +166 -2
- package/templates/development/README.md +9 -9
- package/templates/development/cross-repo-debugging.md +3 -3
- package/templates/development/external-context/service-template.md +1 -1
- package/templates/development/external-source-packs/README.md +2 -2
- package/templates/integrations/README.md +4 -4
- package/templates/integrations/api-contract.md +1 -1
- package/templates/integrations/event-contract.md +1 -1
- package/templates/integrations/third-party/vendor-template.md +1 -1
- package/templates/integrations/webhook-contract.md +1 -1
- package/templates/ledger/Harness-Ledger.md +1 -1
- package/templates/modules/module_brief.md +50 -0
- package/templates/modules/module_plan.md +49 -0
- package/templates/modules/registry_view.md +9 -0
- package/templates/modules/session_prompt_pack.md +55 -0
- package/templates/planning/brief.md +32 -8
- package/templates/planning/module_brief.md +28 -3
- package/templates/planning/module_plan.md +26 -11
- package/templates/planning/module_session_prompt.md +11 -2
- package/templates/planning/optional/slices/_slice-template/brief.md +28 -0
- package/templates/planning/review.md +1 -1
- package/templates/planning/visual_map.md +1 -1
- package/templates/reference/docs-library-standard.md +7 -7
- package/templates/reference/execution-workflow-standard.md +13 -0
- package/templates/reference/external-source-intake-standard.md +10 -10
- package/templates/reference/repo-governance-standard.md +1 -1
- package/templates/reference/review-routing-standard.md +4 -0
- package/templates/ssot/Module-Registry.md +4 -38
- package/templates/walkthrough/walkthrough-template.md +1 -1
- package/templates-zh-CN/AGENTS.md.template +27 -25
- package/templates-zh-CN/CLAUDE.md.template +1 -1
- package/templates-zh-CN/architecture/README.md +2 -2
- package/templates-zh-CN/architecture/service-catalog.md +2 -2
- package/templates-zh-CN/architecture/services/service-template.md +1 -1
- package/templates-zh-CN/development/README.md +9 -9
- package/templates-zh-CN/development/cross-repo-debugging.md +3 -3
- package/templates-zh-CN/development/external-context/service-template.md +1 -1
- package/templates-zh-CN/development/external-source-packs/README.md +2 -2
- package/templates-zh-CN/integrations/README.md +4 -4
- package/templates-zh-CN/integrations/api-contract.md +1 -1
- package/templates-zh-CN/integrations/event-contract.md +1 -1
- package/templates-zh-CN/integrations/third-party/vendor-template.md +1 -1
- package/templates-zh-CN/integrations/webhook-contract.md +1 -1
- package/templates-zh-CN/ledger/Harness-Ledger.md +1 -1
- package/templates-zh-CN/lessons/lesson-arch-process-change.md +1 -1
- package/templates-zh-CN/lessons/lesson-new-doc.md +3 -3
- package/templates-zh-CN/lessons/lesson-ref-change.md +4 -4
- package/templates-zh-CN/modules/module_brief.md +47 -0
- package/templates-zh-CN/modules/module_plan.md +48 -0
- package/templates-zh-CN/modules/registry_view.md +9 -0
- package/templates-zh-CN/modules/session_prompt_pack.md +50 -0
- package/templates-zh-CN/planning/INDEX.md +1 -0
- package/templates-zh-CN/planning/brief.md +26 -7
- package/templates-zh-CN/planning/module_brief.md +24 -2
- package/templates-zh-CN/planning/module_plan.md +35 -29
- package/templates-zh-CN/planning/module_session_prompt.md +15 -11
- package/templates-zh-CN/planning/optional/slices/_slice-template/brief.md +28 -11
- package/templates-zh-CN/planning/review.md +1 -1
- package/templates-zh-CN/reference/adversarial-review-standard.md +1 -1
- package/templates-zh-CN/reference/delivery-operating-model-standard.md +3 -3
- package/templates-zh-CN/reference/docs-library-standard.md +27 -27
- package/templates-zh-CN/reference/execution-workflow-standard.md +12 -2
- package/templates-zh-CN/reference/external-source-intake-standard.md +10 -10
- package/templates-zh-CN/reference/harness-ledger-standard.md +3 -3
- package/templates-zh-CN/reference/regression-ssot-governance.md +2 -2
- package/templates-zh-CN/reference/repo-governance-standard.md +1 -1
- package/templates-zh-CN/reference/review-routing-standard.md +3 -0
- package/templates-zh-CN/reference/walkthrough-standard.md +2 -2
- package/templates-zh-CN/reference/worktree-standard.md +1 -1
- package/templates-zh-CN/regression/Cadence-Ledger.md +2 -2
- package/templates-zh-CN/ssot/Delivery-SSoT.md +2 -2
- package/templates-zh-CN/ssot/Module-Registry.md +5 -44
- package/templates-zh-CN/ssot/Regression-SSoT.md +2 -2
- package/templates-zh-CN/walkthrough/walkthrough-template.md +4 -4
|
@@ -43,7 +43,7 @@ templateValues:
|
|
|
43
43
|
entrypoints:
|
|
44
44
|
newTask:
|
|
45
45
|
type: template
|
|
46
|
-
writes: [
|
|
46
|
+
writes: [{{paths.tasksRoot}}/**]
|
|
47
47
|
reads: [session.json]
|
|
48
48
|
audit: true
|
|
49
49
|
templates:
|
|
@@ -55,19 +55,19 @@ entrypoints:
|
|
|
55
55
|
plan:
|
|
56
56
|
type: script
|
|
57
57
|
command: scripts/plan-work-queue.mjs
|
|
58
|
-
writes: [
|
|
58
|
+
writes: [{{paths.tasksRoot}}/**]
|
|
59
59
|
reads: [docs/**, .git/**]
|
|
60
60
|
audit: true
|
|
61
61
|
scaffold:
|
|
62
62
|
type: script
|
|
63
63
|
command: scripts/scaffold-task-contracts.mjs
|
|
64
|
-
writes: [
|
|
64
|
+
writes: [{{paths.tasksRoot}}/**]
|
|
65
65
|
reads: [docs/**]
|
|
66
66
|
audit: true
|
|
67
67
|
check:
|
|
68
68
|
type: check
|
|
69
69
|
command: checks/preset-check.mjs
|
|
70
|
-
writes: [
|
|
70
|
+
writes: [{{paths.tasksRoot}}/**]
|
|
71
71
|
reads: [docs/**]
|
|
72
72
|
audit: true
|
|
73
73
|
workbench:
|
|
@@ -130,5 +130,5 @@ audit:
|
|
|
130
130
|
evidenceFiles: [preset-manifest.json, preset-audit.json, write-scope.json]
|
|
131
131
|
writeScopes:
|
|
132
132
|
taskArtifacts:
|
|
133
|
-
path:
|
|
133
|
+
path: {{paths.tasksRoot}}/**
|
|
134
134
|
access: write
|
|
@@ -15,4 +15,4 @@ Declare lanes before dispatching workers.
|
|
|
15
15
|
|
|
16
16
|
| Lane ID | Allowed globs | Forbidden globs | Shared file owner | Worktree / branch | Handoff path | Merge order | Verification command |
|
|
17
17
|
| --- | --- | --- | --- | --- | --- | --- | --- |
|
|
18
|
-
| coordinator |
|
|
18
|
+
| coordinator | {{paths.harnessRoot}}/planning/tasks/** | AGENTS.md, CLAUDE.md, {{paths.harnessRoot}}/governance/generated/Harness-Ledger.md until closeout | coordinator | current | progress.md | 1 | harness check --profile target-project . |
|
|
@@ -9,14 +9,14 @@ task:
|
|
|
9
9
|
entrypoints:
|
|
10
10
|
newTask:
|
|
11
11
|
type: template
|
|
12
|
-
writes: [
|
|
13
|
-
reads: [
|
|
12
|
+
writes: [{{paths.tasksRoot}}/**]
|
|
13
|
+
reads: [{{paths.tasksRoot}}/**/lesson_candidates.md]
|
|
14
14
|
audit: true
|
|
15
15
|
templates:
|
|
16
16
|
prompt: templates/prompt.md
|
|
17
17
|
writeScopes:
|
|
18
18
|
taskDocs:
|
|
19
|
-
path:
|
|
19
|
+
path: {{paths.tasksRoot}}/**
|
|
20
20
|
access: write
|
|
21
21
|
audit:
|
|
22
22
|
manifestRequired: true
|
|
@@ -8,7 +8,7 @@ task:
|
|
|
8
8
|
entrypoints:
|
|
9
9
|
newTask:
|
|
10
10
|
type: template
|
|
11
|
-
writes: [
|
|
11
|
+
writes: [{{paths.planningRoot}}/**]
|
|
12
12
|
audit: true
|
|
13
13
|
templates:
|
|
14
14
|
taskPlanAppend: templates/task_plan.append.md
|
|
@@ -21,5 +21,5 @@ audit:
|
|
|
21
21
|
evidenceFiles: [preset-audit.json]
|
|
22
22
|
writeScopes:
|
|
23
23
|
planningDocs:
|
|
24
|
-
path:
|
|
24
|
+
path: {{paths.planningRoot}}/**
|
|
25
25
|
access: write
|
|
@@ -3,6 +3,6 @@
|
|
|
3
3
|
| Field | Value |
|
|
4
4
|
| --- | --- |
|
|
5
5
|
| Module Key | {{moduleKey}} |
|
|
6
|
-
| Module Plan |
|
|
6
|
+
| Module Plan | {{paths.harnessRoot}}/planning/modules/{{moduleKey}}/module_plan.md |
|
|
7
7
|
|
|
8
8
|
Keep shared module decisions in the module plan or module context files. Keep task-specific evidence in this task directory.
|
|
@@ -12,6 +12,6 @@ Read these module-level entry points before changing shared module behavior. Con
|
|
|
12
12
|
|
|
13
13
|
| Reference | Path | Why / When |
|
|
14
14
|
| --- | --- | --- |
|
|
15
|
-
| Module brief |
|
|
16
|
-
| Module plan |
|
|
17
|
-
| Module visual map |
|
|
15
|
+
| Module brief | {{paths.harnessRoot}}/planning/modules/{{moduleKey}}/brief.md | Start here for the module purpose and current scope. |
|
|
16
|
+
| Module plan | {{paths.harnessRoot}}/planning/modules/{{moduleKey}}/module_plan.md | Use this for module steps, active task links, and handoff state. |
|
|
17
|
+
| Module visual map | {{paths.harnessRoot}}/planning/modules/{{moduleKey}}/visual_map.md | Inspect when the change affects module sequencing or dependencies. |
|
|
@@ -5,7 +5,12 @@ import path from "node:path";
|
|
|
5
5
|
|
|
6
6
|
const context = JSON.parse(fs.readFileSync(process.env.HARNESS_PRESET_CONTEXT, "utf8"));
|
|
7
7
|
const release = String(context.inputs.release || "").trim();
|
|
8
|
-
const
|
|
8
|
+
const governanceRoot = context.paths?.governanceRoot;
|
|
9
|
+
if (!governanceRoot) {
|
|
10
|
+
console.error("release-closeout check requires structure-aware context.paths from the preset runner");
|
|
11
|
+
process.exit(2);
|
|
12
|
+
}
|
|
13
|
+
const releaseRoot = path.join(context.targetRoot, governanceRoot, "releases", release);
|
|
9
14
|
const required = ["INDEX.md", "task-aggregate.json", "task-archive-plan.md", "public-summary.md", "public-redaction-report.json"];
|
|
10
15
|
const missing = required.filter((file) => !fs.existsSync(path.join(releaseRoot, file)));
|
|
11
16
|
if (missing.length) {
|
|
@@ -47,7 +47,7 @@ templateValues:
|
|
|
47
47
|
entrypoints:
|
|
48
48
|
newTask:
|
|
49
49
|
type: template
|
|
50
|
-
writes: [
|
|
50
|
+
writes: [{{paths.tasksRoot}}/**]
|
|
51
51
|
audit: true
|
|
52
52
|
templates:
|
|
53
53
|
taskPlanAppend: templates/task_plan.append.md
|
|
@@ -57,20 +57,20 @@ entrypoints:
|
|
|
57
57
|
plan:
|
|
58
58
|
type: script
|
|
59
59
|
command: scripts/generate-release-package.mjs
|
|
60
|
-
writes: [
|
|
61
|
-
reads: [
|
|
60
|
+
writes: [{{paths.governanceRoot}}/releases/**]
|
|
61
|
+
reads: [{{paths.tasksRoot}}/**, {{paths.modulesRoot}}/**]
|
|
62
62
|
audit: true
|
|
63
63
|
scaffold:
|
|
64
64
|
type: script
|
|
65
65
|
command: scripts/generate-release-package.mjs
|
|
66
|
-
writes: [
|
|
67
|
-
reads: [
|
|
66
|
+
writes: [{{paths.governanceRoot}}/releases/**]
|
|
67
|
+
reads: [{{paths.tasksRoot}}/**, {{paths.modulesRoot}}/**]
|
|
68
68
|
audit: true
|
|
69
69
|
check:
|
|
70
70
|
type: check
|
|
71
71
|
command: checks/check-release-package.mjs
|
|
72
|
-
writes: [
|
|
73
|
-
reads: [
|
|
72
|
+
writes: [{{paths.governanceRoot}}/releases/**]
|
|
73
|
+
reads: [{{paths.governanceRoot}}/releases/**, {{paths.tasksRoot}}/**, {{paths.modulesRoot}}/**]
|
|
74
74
|
audit: true
|
|
75
75
|
evidence:
|
|
76
76
|
bundleDir: artifacts/preset
|
|
@@ -93,8 +93,8 @@ audit:
|
|
|
93
93
|
evidenceFiles: [preset-audit.json, preset-manifest.json, write-scope.json]
|
|
94
94
|
writeScopes:
|
|
95
95
|
taskDocs:
|
|
96
|
-
path:
|
|
96
|
+
path: {{paths.tasksRoot}}/**
|
|
97
97
|
access: write
|
|
98
98
|
releasePackage:
|
|
99
|
-
path:
|
|
99
|
+
path: {{paths.governanceRoot}}/releases/**
|
|
100
100
|
access: write
|
|
@@ -4,32 +4,70 @@ import fs from "node:fs";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
|
|
6
6
|
const context = JSON.parse(fs.readFileSync(process.env.HARNESS_PRESET_CONTEXT, "utf8"));
|
|
7
|
+
const harnessCore = await import(context.runtime?.coreModule || new URL("../../../dist/harness-core.mjs", import.meta.url).href);
|
|
8
|
+
const {
|
|
9
|
+
normalizeTarget,
|
|
10
|
+
collectTasks: collectCoreTasks,
|
|
11
|
+
archiveBlockReason: sharedArchiveBlockReason,
|
|
12
|
+
} = harnessCore;
|
|
13
|
+
|
|
7
14
|
const release = safeRelease(context.inputs.release);
|
|
8
15
|
if (!release) {
|
|
9
16
|
console.error("release-closeout requires inputs.release");
|
|
10
17
|
process.exit(2);
|
|
11
18
|
}
|
|
12
19
|
|
|
13
|
-
const
|
|
20
|
+
const paths = context.paths || {};
|
|
21
|
+
if (!paths.tasksRoot || !paths.governanceRoot) {
|
|
22
|
+
console.error("release-closeout requires structure-aware context.paths from the preset runner");
|
|
23
|
+
process.exit(2);
|
|
24
|
+
}
|
|
14
25
|
const releaseRoot = path.join(context.outputRoot, "release");
|
|
15
26
|
fs.mkdirSync(releaseRoot, { recursive: true });
|
|
16
27
|
|
|
17
|
-
const
|
|
18
|
-
|
|
28
|
+
const target = normalizeTarget(context.targetRoot);
|
|
29
|
+
const allTasks = collectCoreTasks(target)
|
|
30
|
+
.map(releaseTaskFromCore)
|
|
31
|
+
.filter((task) => task.id !== context.task.id && task.shortId !== context.task.id && task.preset !== "release-closeout")
|
|
19
32
|
.sort((a, b) => a.id.localeCompare(b.id));
|
|
20
|
-
|
|
21
|
-
|
|
33
|
+
let selection;
|
|
34
|
+
try {
|
|
35
|
+
selection = selectTasks(allTasks, context.inputs, release);
|
|
36
|
+
} catch (error) {
|
|
37
|
+
console.error(error.message);
|
|
38
|
+
process.exit(2);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const tasks = selection.tasks.sort((a, b) => a.id.localeCompare(b.id));
|
|
42
|
+
if (tasks.length === 0) {
|
|
43
|
+
console.error("release-closeout selector matched no tasks");
|
|
44
|
+
process.exit(2);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const eligibility = tasks.map((task) => ({ task, reason: archiveBlockReason(task) }));
|
|
48
|
+
const eligibleArchive = eligibility.filter((item) => !item.reason).map((item) => item.task);
|
|
49
|
+
const notEligibleArchive = eligibility.filter((item) => item.reason);
|
|
50
|
+
const governanceRoot = paths.governanceRoot;
|
|
51
|
+
const destinationRoot = `${governanceRoot}/releases/${release}`;
|
|
52
|
+
const releasePackageRef = `${destinationRoot}/INDEX.md`;
|
|
22
53
|
|
|
23
54
|
const aggregate = {
|
|
24
55
|
schemaVersion: "release-closeout-aggregate/v1",
|
|
25
56
|
release,
|
|
26
57
|
generatedAt: new Date().toISOString(),
|
|
58
|
+
selector: selection.selector,
|
|
27
59
|
summary: {
|
|
28
60
|
totalTasks: tasks.length,
|
|
29
61
|
doneTasks: tasks.filter((task) => task.state === "done").length,
|
|
30
62
|
blockedTasks: tasks.filter((task) => task.state === "blocked").length,
|
|
31
63
|
archiveEligibleTasks: eligibleArchive.length,
|
|
32
64
|
},
|
|
65
|
+
matched: tasks.map(taskSummary),
|
|
66
|
+
excluded: selection.excluded.map((task) => ({ ...taskSummary(task), reason: "not-selected" })),
|
|
67
|
+
notEligible: [
|
|
68
|
+
...selection.missing.map((id) => ({ id, reason: "task-not-found" })),
|
|
69
|
+
...notEligibleArchive.map(({ task, reason }) => ({ ...taskSummary(task), reason })),
|
|
70
|
+
],
|
|
33
71
|
tasks: tasks.map((task) => ({
|
|
34
72
|
id: task.id,
|
|
35
73
|
title: task.title,
|
|
@@ -37,7 +75,7 @@ const aggregate = {
|
|
|
37
75
|
preset: task.preset || "none",
|
|
38
76
|
deletionState: task.deletionState,
|
|
39
77
|
archiveEligible: eligibleArchive.some((candidate) => candidate.id === task.id),
|
|
40
|
-
archiveBlockedReason: task
|
|
78
|
+
archiveBlockedReason: archiveBlockReason(task),
|
|
41
79
|
})),
|
|
42
80
|
};
|
|
43
81
|
|
|
@@ -63,6 +101,11 @@ Generated by \`presets/release-closeout\` through the generic preset runner.
|
|
|
63
101
|
const archivePlan = `# Release ${release} Task Archive Plan
|
|
64
102
|
|
|
65
103
|
This plan is generated by the release-closeout preset. It does not archive tasks by itself.
|
|
104
|
+
Run the commands from the target project root after reviewing this package.
|
|
105
|
+
|
|
106
|
+
## Eligible Archive Commands
|
|
107
|
+
|
|
108
|
+
${eligibleArchive.length ? eligibleArchive.map((task) => `- ${archiveCommand(task, release, releasePackageRef)}`).join("\n") : "- none"}
|
|
66
109
|
|
|
67
110
|
## Eligible Tasks
|
|
68
111
|
|
|
@@ -70,7 +113,7 @@ ${eligibleArchive.length ? eligibleArchive.map((task) => `- ${task.id} - ${task.
|
|
|
70
113
|
|
|
71
114
|
## Not Eligible
|
|
72
115
|
|
|
73
|
-
${
|
|
116
|
+
${aggregate.notEligible.length ? aggregate.notEligible.map((task) => `- ${task.id} - ${task.reason}`).join("\n") : "- none"}
|
|
74
117
|
`;
|
|
75
118
|
|
|
76
119
|
const publicSummaryRaw = `# Release ${release} Public Summary
|
|
@@ -101,7 +144,6 @@ write("task-aggregate.json", `${JSON.stringify(aggregate, null, 2)}\n`);
|
|
|
101
144
|
write("public-summary.md", publicSummary);
|
|
102
145
|
write("public-redaction-report.json", `${JSON.stringify(redactionReport, null, 2)}\n`);
|
|
103
146
|
|
|
104
|
-
const destinationRoot = `coding-agent-harness/governance/releases/${release}`;
|
|
105
147
|
fs.writeFileSync(context.materializationManifestPath, `${JSON.stringify({
|
|
106
148
|
schemaVersion: "preset-materialization/v1",
|
|
107
149
|
status: "ok",
|
|
@@ -119,27 +161,264 @@ function write(name, content) {
|
|
|
119
161
|
fs.writeFileSync(path.join(releaseRoot, name), content.endsWith("\n") ? content : `${content}\n`);
|
|
120
162
|
}
|
|
121
163
|
|
|
122
|
-
function
|
|
123
|
-
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
const
|
|
164
|
+
function selectTasks(tasks, inputs, release) {
|
|
165
|
+
const hasTaskList = Boolean(inputs.taskList);
|
|
166
|
+
const hasTaskQuery = Boolean(String(inputs.taskQuery || "").trim());
|
|
167
|
+
if (!hasTaskList && !hasTaskQuery) throw new Error("release-closeout requires --task-list or --task-query");
|
|
168
|
+
if (hasTaskList && hasTaskQuery) throw new Error("release-closeout accepts only one selector: --task-list or --task-query");
|
|
169
|
+
|
|
170
|
+
if (hasTaskList) {
|
|
171
|
+
const taskList = inputs.taskList;
|
|
172
|
+
if (!taskList || typeof taskList !== "object" || Array.isArray(taskList)) {
|
|
173
|
+
throw new Error("--task-list must be a JSON object");
|
|
174
|
+
}
|
|
175
|
+
if (taskList.schemaVersion !== "release-closeout-task-list/v1") {
|
|
176
|
+
throw new Error("--task-list schemaVersion must be release-closeout-task-list/v1");
|
|
177
|
+
}
|
|
178
|
+
if (taskList.release && taskList.release !== release) {
|
|
179
|
+
throw new Error(`--task-list release ${taskList.release} does not match --release ${release}`);
|
|
180
|
+
}
|
|
181
|
+
const requestedTaskIds = Array.isArray(taskList.taskIds) ? taskList.taskIds.map(normalizeTaskListId).filter(Boolean) : [];
|
|
182
|
+
if (requestedTaskIds.length === 0) throw new Error("--task-list requires non-empty taskIds");
|
|
183
|
+
const selected = resolveTaskReferences(requestedTaskIds, tasks);
|
|
184
|
+
const selectedIds = new Set(selected.map((task) => task.id));
|
|
130
185
|
return {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
186
|
+
selector: {
|
|
187
|
+
type: "task-list",
|
|
188
|
+
schemaVersion: taskList.schemaVersion,
|
|
189
|
+
release,
|
|
190
|
+
sourcePath: taskList.sourcePath || "",
|
|
191
|
+
taskIds: requestedTaskIds,
|
|
192
|
+
resolvedTaskIds: selected.map((task) => task.id),
|
|
193
|
+
},
|
|
194
|
+
tasks: selected,
|
|
195
|
+
excluded: tasks.filter((task) => !selectedIds.has(task.id)),
|
|
196
|
+
missing: [],
|
|
139
197
|
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const query = String(inputs.taskQuery || "").trim();
|
|
201
|
+
const terms = parseTaskQuery(query);
|
|
202
|
+
const selected = tasks.filter((task) => terms.every((term) => matchesQueryTerm(task, term)));
|
|
203
|
+
const selectedIds = new Set(selected.map((task) => task.id));
|
|
204
|
+
return {
|
|
205
|
+
selector: {
|
|
206
|
+
type: "task-query",
|
|
207
|
+
query,
|
|
208
|
+
terms,
|
|
209
|
+
},
|
|
210
|
+
tasks: selected,
|
|
211
|
+
excluded: tasks.filter((task) => !selectedIds.has(task.id)),
|
|
212
|
+
missing: [],
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function normalizeTaskListId(value) {
|
|
217
|
+
return String(value || "").trim();
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function resolveTaskReferences(references, tasks) {
|
|
221
|
+
return references.map((reference) => {
|
|
222
|
+
const canonical = tasks.filter((task) => task.id === reference);
|
|
223
|
+
const candidates = canonical.length > 0 ? canonical : tasks.filter((task) => taskReferenceMatches(task, reference));
|
|
224
|
+
if (candidates.length === 0) {
|
|
225
|
+
throw new Error(`Missing task reference: ${reference}`);
|
|
226
|
+
}
|
|
227
|
+
if (candidates.length > 1) {
|
|
228
|
+
throw new Error(`Ambiguous task reference: ${reference} matched ${candidates.map((task) => task.id).join(", ")}`);
|
|
229
|
+
}
|
|
230
|
+
return candidates[0];
|
|
140
231
|
});
|
|
141
232
|
}
|
|
142
233
|
|
|
234
|
+
function taskReferenceMatches(task, reference) {
|
|
235
|
+
if (/^(TASKS|MODULES|EXTERNAL)\//.test(reference)) return task.id === reference;
|
|
236
|
+
return task.shortId === reference || task.legacyId === reference || task.id.endsWith(`/${reference}`);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function parseTaskQuery(query) {
|
|
240
|
+
const rawTerms = query.split(/\s+/).map((item) => item.trim()).filter(Boolean);
|
|
241
|
+
if (rawTerms.length === 0) throw new Error("--task-query requires at least one selector term");
|
|
242
|
+
return rawTerms.map((term) => {
|
|
243
|
+
let match = term.match(/^date:(\d{4}-\d{2}-\d{2})\.\.(\d{4}-\d{2}-\d{2})$/);
|
|
244
|
+
if (match) return { type: "date", from: match[1], to: match[2] };
|
|
245
|
+
match = term.match(/^state:([A-Za-z0-9_-]+)$/);
|
|
246
|
+
if (match) return { type: "state", value: normalizeState(match[1]) };
|
|
247
|
+
match = term.match(/^preset:([A-Za-z0-9._-]+)$/);
|
|
248
|
+
if (match) return { type: "preset", value: match[1].toLowerCase() };
|
|
249
|
+
match = term.match(/^module:([A-Za-z0-9._-]+)$/);
|
|
250
|
+
if (match) return { type: "module", value: match[1].toLowerCase() };
|
|
251
|
+
throw new Error(`Unsupported --task-query term: ${term}`);
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function matchesQueryTerm(task, term) {
|
|
256
|
+
if (term.type === "date") {
|
|
257
|
+
const date = task.date || "";
|
|
258
|
+
return date >= term.from && date <= term.to;
|
|
259
|
+
}
|
|
260
|
+
if (term.type === "state") return task.state === term.value;
|
|
261
|
+
if (term.type === "preset") return (task.preset || "none") === term.value;
|
|
262
|
+
if (term.type === "module") return (task.module || "").toLowerCase() === term.value;
|
|
263
|
+
return false;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function taskSummary(task) {
|
|
267
|
+
return {
|
|
268
|
+
id: task.id,
|
|
269
|
+
title: task.title,
|
|
270
|
+
state: task.state,
|
|
271
|
+
preset: task.preset || "none",
|
|
272
|
+
deletionState: task.deletionState,
|
|
273
|
+
module: task.module || "",
|
|
274
|
+
shortId: task.shortId || "",
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function archiveBlockReason(task) {
|
|
279
|
+
return sharedArchiveBlockReason(task, { archivedBy: task.reviewConfirmation?.reviewer || "" });
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function releaseTaskFromCore(task) {
|
|
283
|
+
const shortId = task.shortId || task.id.split("/").at(-1) || "";
|
|
284
|
+
return {
|
|
285
|
+
...task,
|
|
286
|
+
preset: task.taskPreset || "none",
|
|
287
|
+
date: shortId.match(/^(\d{4}-\d{2}-\d{2})/)?.[1] || "",
|
|
288
|
+
queue: (task.taskQueues || []).includes("blocked") ? "blocked" : "active",
|
|
289
|
+
hasOpenBlockingFindings: (task.risks || []).some((risk) => String(risk.open) !== "no" && (String(risk.blocksRelease) === "yes" || ["P0", "P1", "P2"].includes(String(risk.severity)))),
|
|
290
|
+
materialsIncomplete: task.materialsReady === false,
|
|
291
|
+
evidenceSnippet: "",
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function archiveCommand(task, release, releasePackageRef) {
|
|
296
|
+
return [
|
|
297
|
+
"harness",
|
|
298
|
+
"task-archive",
|
|
299
|
+
quoteCli(task.id),
|
|
300
|
+
"--reason",
|
|
301
|
+
quoteCli(`release closeout ${release}`),
|
|
302
|
+
"--archived-by",
|
|
303
|
+
quoteCli(task.reviewConfirmation?.reviewer || ""),
|
|
304
|
+
"--archive-field",
|
|
305
|
+
quoteCli(`retention bucket=release:${release}`),
|
|
306
|
+
"--archive-field",
|
|
307
|
+
quoteCli(`release package=${releasePackageRef}`),
|
|
308
|
+
".",
|
|
309
|
+
].join(" ");
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function quoteCli(value) {
|
|
313
|
+
return `"${String(value || "").replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function taskRootsFromContext(targetRoot, paths) {
|
|
317
|
+
const roots = [];
|
|
318
|
+
const seen = new Set();
|
|
319
|
+
const addRoot = (kind, relativeRoot) => {
|
|
320
|
+
if (!relativeRoot) return;
|
|
321
|
+
const root = path.join(targetRoot, relativeRoot);
|
|
322
|
+
const key = path.resolve(root);
|
|
323
|
+
if (seen.has(key)) return;
|
|
324
|
+
seen.add(key);
|
|
325
|
+
roots.push({ kind, root, relativeRoot: toPosix(relativeRoot) });
|
|
326
|
+
};
|
|
327
|
+
addRoot("TASKS", paths.tasksRoot);
|
|
328
|
+
addRoot("MODULES", paths.modulesRoot);
|
|
329
|
+
addRoot("EXTERNAL", paths.externalRoot);
|
|
330
|
+
if (Array.isArray(paths.taskRoots)) {
|
|
331
|
+
for (const relativeRoot of paths.taskRoots) addRoot(inferTaskRootKind(relativeRoot, paths), relativeRoot);
|
|
332
|
+
}
|
|
333
|
+
return roots;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function inferTaskRootKind(relativeRoot, paths) {
|
|
337
|
+
const normalized = toPosix(relativeRoot);
|
|
338
|
+
if (normalized === toPosix(paths.tasksRoot || "")) return "TASKS";
|
|
339
|
+
if (normalized === toPosix(paths.modulesRoot || "")) return "MODULES";
|
|
340
|
+
if (normalized === toPosix(paths.externalRoot || "")) return "EXTERNAL";
|
|
341
|
+
return "EXTERNAL";
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function collectTasks(roots) {
|
|
345
|
+
const tasks = [];
|
|
346
|
+
for (const rootInfo of roots) {
|
|
347
|
+
if (!fs.existsSync(rootInfo.root)) continue;
|
|
348
|
+
const taskPlans = walk(rootInfo.root).filter((file) => path.basename(file) === "task_plan.md");
|
|
349
|
+
for (const taskPlanPath of taskPlans) {
|
|
350
|
+
const taskDir = path.dirname(taskPlanPath);
|
|
351
|
+
const identity = taskIdentity(rootInfo, taskDir);
|
|
352
|
+
if (!identity) continue;
|
|
353
|
+
const taskPlan = read(taskPlanPath);
|
|
354
|
+
const progress = read(path.join(taskDir, "progress.md"));
|
|
355
|
+
const review = read(path.join(taskDir, "review.md"));
|
|
356
|
+
const index = read(path.join(taskDir, "INDEX.md"));
|
|
357
|
+
const budget = parseBudget(taskPlan);
|
|
358
|
+
const state = parseState(progress);
|
|
359
|
+
const tombstone = parseTombstone(taskPlan);
|
|
360
|
+
tasks.push({
|
|
361
|
+
...identity,
|
|
362
|
+
title: titleFrom(taskPlan, identity.shortId),
|
|
363
|
+
preset: metadataLine(taskPlan, ["Task Preset", "Preset"]).toLowerCase(),
|
|
364
|
+
state,
|
|
365
|
+
deletionState: tombstone.state || "active",
|
|
366
|
+
queue: state === "blocked" ? "blocked" : "active",
|
|
367
|
+
budget,
|
|
368
|
+
hasOpenBlockingFindings: hasOpenBlockingReviewFinding(review),
|
|
369
|
+
materialsIncomplete: budget !== "simple" && hasIncompleteMaterials(taskDir),
|
|
370
|
+
reviewConfirmation: parseReviewConfirmation(index, identity.id),
|
|
371
|
+
evidenceSnippet: progress.split(/\r?\n/).find((line) => /\/Users\/|\/Volumes\/|file:\/\//.test(line)) || "",
|
|
372
|
+
tombstone,
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
return tasks;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function taskIdentity(rootInfo, taskDir) {
|
|
380
|
+
const relative = toPosix(path.relative(rootInfo.root, taskDir));
|
|
381
|
+
if (!relative || relative.startsWith("..")) return null;
|
|
382
|
+
const segments = relative.split("/").filter(Boolean);
|
|
383
|
+
const shortId = segments.at(-1) || path.basename(taskDir);
|
|
384
|
+
const date = shortId.match(/^(\d{4}-\d{2}-\d{2})/)?.[1] || "";
|
|
385
|
+
if (rootInfo.kind === "TASKS") {
|
|
386
|
+
return {
|
|
387
|
+
id: `TASKS/${relative}`,
|
|
388
|
+
legacyId: shortId,
|
|
389
|
+
shortId,
|
|
390
|
+
date,
|
|
391
|
+
module: "",
|
|
392
|
+
rootKind: "TASKS",
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
if (rootInfo.kind === "MODULES") {
|
|
396
|
+
if (segments.length < 3 || segments[1] !== "tasks") return null;
|
|
397
|
+
const moduleKey = segments[0];
|
|
398
|
+
const taskRelative = segments.slice(2).join("/");
|
|
399
|
+
return {
|
|
400
|
+
id: `MODULES/${moduleKey}/${taskRelative}`,
|
|
401
|
+
legacyId: shortId,
|
|
402
|
+
shortId,
|
|
403
|
+
date,
|
|
404
|
+
module: moduleKey,
|
|
405
|
+
rootKind: "MODULES",
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
return {
|
|
409
|
+
id: `EXTERNAL/${toPosix(path.basename(rootInfo.root))}/${relative}`,
|
|
410
|
+
legacyId: shortId,
|
|
411
|
+
shortId,
|
|
412
|
+
date,
|
|
413
|
+
module: "",
|
|
414
|
+
rootKind: "EXTERNAL",
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function toPosix(value) {
|
|
419
|
+
return String(value || "").split(path.sep).join("/");
|
|
420
|
+
}
|
|
421
|
+
|
|
143
422
|
function walk(root) {
|
|
144
423
|
const files = [];
|
|
145
424
|
for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
|
|
@@ -171,10 +450,40 @@ function metadataLine(content, labels) {
|
|
|
171
450
|
|
|
172
451
|
function parseState(progress) {
|
|
173
452
|
const match = String(progress || "").match(/^##\s*(?:Current Status|Status|状态)\s*[::]?\s*(?:\n\s*)?([^\n]+)/im);
|
|
174
|
-
|
|
453
|
+
return normalizeState(match ? match[1] : "");
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function normalizeState(value) {
|
|
457
|
+
const raw = String(value || "").trim().toLowerCase().replaceAll("-", "_").replaceAll(" ", "_");
|
|
175
458
|
return ["not_started", "planned", "in_progress", "review", "blocked", "done"].includes(raw) ? raw : "unknown";
|
|
176
459
|
}
|
|
177
460
|
|
|
461
|
+
function parseBudget(taskPlan) {
|
|
462
|
+
const match = String(taskPlan || "").match(/Selected budget\s*[::]\s*([A-Za-z0-9_-]+)/i);
|
|
463
|
+
return match ? match[1].trim().toLowerCase() : "standard";
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function hasOpenBlockingReviewFinding(review) {
|
|
467
|
+
const lines = String(review || "").split(/\r?\n/);
|
|
468
|
+
for (const line of lines) {
|
|
469
|
+
const row = line.trim();
|
|
470
|
+
if (!row.startsWith("|") || /---/.test(row) || /severity|finding/i.test(row)) continue;
|
|
471
|
+
const cells = row.slice(1, -1).split("|").map((cell) => cell.trim().toLowerCase());
|
|
472
|
+
const hasBlockingSeverity = cells.some((cell) => /^p[0-2]\b/.test(cell));
|
|
473
|
+
const openOrBlocking = cells.some((cell) => /^(yes|true|open|是|开放)$/.test(cell));
|
|
474
|
+
if (hasBlockingSeverity && openOrBlocking) return true;
|
|
475
|
+
}
|
|
476
|
+
return /^Open Blocking Findings\s*[::]\s*(yes|true|open|是)/im.test(review);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function hasIncompleteMaterials(taskDir) {
|
|
480
|
+
for (const fileName of ["task_plan.md", "progress.md", "review.md", "INDEX.md"]) {
|
|
481
|
+
const content = read(path.join(taskDir, fileName));
|
|
482
|
+
if (/Materials\s*(?:Status|Ready)\s*[::]\s*(incomplete|missing|no|false)/im.test(content)) return true;
|
|
483
|
+
}
|
|
484
|
+
return false;
|
|
485
|
+
}
|
|
486
|
+
|
|
178
487
|
function parseTombstone(content) {
|
|
179
488
|
const match = String(content || "").match(/^##\s*(?:Task Tombstone|任务墓碑)\s*$([\s\S]*?)(?=^##\s+|(?![\s\S]))/im);
|
|
180
489
|
if (!match) return { state: "active", fields: {} };
|
|
@@ -188,6 +497,59 @@ function parseTombstone(content) {
|
|
|
188
497
|
return { state: fields.state || "soft-deleted", fields };
|
|
189
498
|
}
|
|
190
499
|
|
|
500
|
+
function parseReviewConfirmation(content, taskId) {
|
|
501
|
+
const fields = markdownFieldsFromBlock(content, /^##\s*(?:Task Audit Metadata|任务审计元数据)\s*$/im);
|
|
502
|
+
if (fields.size === 0) return { confirmed: false, missingFields: ["Task Audit Metadata"] };
|
|
503
|
+
if (normalizeToken(fields.get("human review status")) !== "confirmed") return { confirmed: false, missingFields: [] };
|
|
504
|
+
const required = ["confirmation id", "confirmed at", "reviewer", "reviewer email", "confirm text", "evidence checked", "review commit sha", "audit status"];
|
|
505
|
+
const missing = required.filter((field) => !isConcreteAuditField(fields.get(field)));
|
|
506
|
+
const confirmText = fields.get("confirm text") || "";
|
|
507
|
+
if (isConcreteAuditField(confirmText) && !taskKeysMatch(confirmText, taskId)) missing.push("confirm text match");
|
|
508
|
+
const auditStatus = fields.get("audit status") || "";
|
|
509
|
+
if (isConcreteAuditField(auditStatus) && normalizeToken(auditStatus) !== "committed") missing.push("audit status committed");
|
|
510
|
+
const commitSha = fields.get("review commit sha") || "";
|
|
511
|
+
if (isConcreteAuditField(commitSha) && !/^[0-9a-f]{7,40}$/i.test(commitSha)) missing.push("review commit sha valid");
|
|
512
|
+
return {
|
|
513
|
+
confirmed: missing.length === 0,
|
|
514
|
+
missingFields: missing,
|
|
515
|
+
confirmationId: fields.get("confirmation id") || "",
|
|
516
|
+
confirmedAt: fields.get("confirmed at") || "",
|
|
517
|
+
reviewer: fields.get("reviewer") || "",
|
|
518
|
+
reviewerEmail: fields.get("reviewer email") || "",
|
|
519
|
+
confirmText,
|
|
520
|
+
evidenceChecked: fields.get("evidence checked") || "",
|
|
521
|
+
commitSha,
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function markdownFieldsFromBlock(content, headingPattern) {
|
|
526
|
+
const match = String(content || "").match(new RegExp(`${headingPattern.source}([\\s\\S]*?)(?=^##\\s+|(?![\\s\\S]))`, headingPattern.flags));
|
|
527
|
+
const fields = new Map();
|
|
528
|
+
if (!match) return fields;
|
|
529
|
+
for (const line of match[1].split(/\r?\n/)) {
|
|
530
|
+
const row = line.trim();
|
|
531
|
+
if (!row.startsWith("|") || /---/.test(row) || /Field\s*\|\s*Value/i.test(row)) continue;
|
|
532
|
+
const cells = row.slice(1, -1).split("|").map((cell) => cell.trim());
|
|
533
|
+
if (cells.length >= 2) fields.set(cells[0].toLowerCase(), cells.slice(1).join("|").trim());
|
|
534
|
+
}
|
|
535
|
+
return fields;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function isConcreteAuditField(value) {
|
|
539
|
+
const raw = String(value || "").replace(/`/g, "").trim();
|
|
540
|
+
return Boolean(raw) && !/^(n\/a|na|none|pending(?:[-_ ].*)?|todo|tbd|\[.*\]|-|—|–|不适用|无|待定|\{\})$/i.test(raw) && !/\{\{[^}]+\}\}/.test(raw);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function normalizeToken(value) {
|
|
544
|
+
return String(value || "").trim().toLowerCase().replaceAll("_", "-").replace(/\s+/g, "-");
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function taskKeysMatch(candidate, expected) {
|
|
548
|
+
const left = String(candidate || "").replace(/`/g, "").trim();
|
|
549
|
+
const right = String(expected || "").replace(/`/g, "").trim();
|
|
550
|
+
return left === right || right.endsWith(`/${left}`);
|
|
551
|
+
}
|
|
552
|
+
|
|
191
553
|
function safeRelease(value) {
|
|
192
554
|
const release = String(value || "").trim();
|
|
193
555
|
return /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(release) ? release : "";
|