coding-agent-harness 1.0.4 → 1.0.6
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 +7 -0
- package/CONTRIBUTING.md +2 -2
- package/LICENSE +661 -21
- package/LICENSE-EXCEPTION.md +37 -0
- package/README.md +96 -4
- package/README.zh-CN.md +75 -4
- package/SKILL.md +52 -51
- package/dist/build-dist.mjs +189 -0
- package/dist/check-dist-observation.mjs +428 -0
- package/dist/check-harness.mjs +489 -0
- package/dist/check-import-graph.mjs +511 -0
- package/dist/check-runtime-emit.mjs +304 -0
- package/dist/check-type-boundaries.mjs +139 -0
- package/dist/commands/dashboard-command.mjs +80 -0
- package/dist/commands/migration-command.mjs +152 -0
- package/dist/commands/preset-command.mjs +91 -0
- package/dist/commands/task-command.mjs +324 -0
- package/dist/harness.mjs +304 -0
- package/dist/lib/capability-registry.mjs +643 -0
- package/dist/lib/check-module-parallel.mjs +227 -0
- package/dist/lib/check-profiles.mjs +414 -0
- package/dist/lib/check-task-contracts.mjs +54 -0
- package/dist/lib/core-shared.mjs +254 -0
- package/dist/lib/dashboard-data.mjs +608 -0
- package/dist/lib/dashboard-workbench.mjs +334 -0
- package/dist/lib/dashboard-writer.mjs +200 -0
- package/dist/lib/git-status-summary.mjs +45 -0
- package/dist/lib/governance-index-generator.mjs +236 -0
- package/dist/lib/governance-sync.mjs +617 -0
- package/dist/lib/governance-table-boundary.mjs +161 -0
- package/{scripts → dist}/lib/harness-core.mjs +3 -0
- package/dist/lib/harness-paths.mjs +338 -0
- package/dist/lib/lesson-maintenance.mjs +139 -0
- package/dist/lib/markdown-utils.mjs +193 -0
- package/dist/lib/migration-planner.mjs +439 -0
- package/dist/lib/migration-support.mjs +317 -0
- package/dist/lib/phase-kind.mjs +46 -0
- package/dist/lib/preset-audit-contracts.mjs +40 -0
- package/dist/lib/preset-engine.mjs +516 -0
- package/dist/lib/preset-registry.mjs +831 -0
- package/dist/lib/preset-resource-contracts.mjs +83 -0
- package/dist/lib/review-confirm-git-gate.mjs +244 -0
- package/dist/lib/status-builder.mjs +87 -0
- package/{scripts → dist}/lib/status-dashboard-renderer.mjs +48 -47
- package/dist/lib/structure-migration.mjs +404 -0
- package/dist/lib/subagent-authorization-audit.mjs +198 -0
- package/dist/lib/task-audit-metadata.mjs +376 -0
- package/dist/lib/task-audit-migration.mjs +355 -0
- package/dist/lib/task-completion-consistency.mjs +29 -0
- package/dist/lib/task-index.mjs +133 -0
- package/dist/lib/task-lesson-candidates.mjs +239 -0
- package/dist/lib/task-lesson-sedimentation.mjs +300 -0
- package/dist/lib/task-lifecycle/create-task-helpers.mjs +84 -0
- package/dist/lib/task-lifecycle/phase-sync.mjs +82 -0
- package/dist/lib/task-lifecycle/review-confirm.mjs +93 -0
- package/dist/lib/task-lifecycle/review-gates.mjs +62 -0
- package/dist/lib/task-lifecycle/review-submission.mjs +52 -0
- package/dist/lib/task-lifecycle/scaffold-provenance.mjs +54 -0
- package/dist/lib/task-lifecycle/template-files.mjs +52 -0
- package/dist/lib/task-lifecycle/text-utils.mjs +26 -0
- package/dist/lib/task-lifecycle.mjs +611 -0
- package/dist/lib/task-metadata.mjs +116 -0
- package/dist/lib/task-review-model.mjs +474 -0
- package/dist/lib/task-scanner.mjs +439 -0
- package/dist/lib/task-tombstone-commands.mjs +125 -0
- package/dist/postinstall.mjs +14 -0
- package/dist/run-built-tests.mjs +84 -0
- package/docs-release/README.md +1 -0
- package/docs-release/architecture/overview.md +13 -13
- package/docs-release/architecture/overview.zh-CN.md +13 -13
- package/docs-release/architecture/system-explainer/01-system-overview.md +218 -0
- package/docs-release/architecture/system-explainer/02-module-dependency.md +257 -0
- package/docs-release/architecture/system-explainer/03-task-lifecycle.md +304 -0
- package/docs-release/architecture/system-explainer/04-check-and-governance.md +241 -0
- package/docs-release/architecture/system-explainer/05-data-flow.md +276 -0
- package/docs-release/architecture/system-explainer/06-preset-and-migration.md +300 -0
- package/docs-release/architecture/system-explainer/README.md +67 -0
- package/docs-release/architecture/system-explainer/en-US/01-system-overview.md +227 -0
- package/docs-release/architecture/system-explainer/en-US/02-module-dependency.md +263 -0
- package/docs-release/architecture/system-explainer/en-US/03-task-lifecycle.md +319 -0
- package/docs-release/architecture/system-explainer/en-US/04-check-and-governance.md +252 -0
- package/docs-release/architecture/system-explainer/en-US/05-data-flow.md +290 -0
- package/docs-release/architecture/system-explainer/en-US/06-preset-and-migration.md +320 -0
- package/docs-release/architecture/system-explainer/en-US/README.md +70 -0
- package/docs-release/guides/agent-installation.en-US.md +22 -15
- package/docs-release/guides/agent-installation.md +23 -15
- package/docs-release/guides/contributing.md +3 -3
- package/docs-release/guides/contributing.zh-CN.md +3 -3
- package/docs-release/guides/document-audience-and-surfaces.en-US.md +10 -10
- package/docs-release/guides/document-audience-and-surfaces.md +10 -10
- package/docs-release/guides/legacy-migration-agent-prompt.md +25 -2
- package/docs-release/guides/legacy-migration-agent-prompt.zh-CN.md +25 -2
- package/docs-release/guides/migration-playbook.en-US.md +63 -1
- package/docs-release/guides/migration-playbook.md +59 -1
- package/docs-release/guides/parent-control-repository-pattern.en-US.md +25 -25
- package/docs-release/guides/parent-control-repository-pattern.md +25 -25
- package/docs-release/guides/preset-development.md +28 -4
- package/docs-release/guides/repository-operating-models.en-US.md +21 -21
- package/docs-release/guides/repository-operating-models.md +21 -21
- package/docs-release/guides/task-state-machine.en-US.md +35 -18
- package/docs-release/guides/task-state-machine.md +35 -18
- package/docs-release/guides/typescript-runtime-migration-closeout.md +96 -0
- package/examples/minimal-project/AGENTS.md +2 -2
- package/examples/minimal-project/coding-agent-harness/harness.yaml +14 -0
- package/examples/minimal-project/coding-agent-harness/planning/tasks/demo-task/INDEX.md +60 -0
- package/examples/minimal-project/coding-agent-harness/planning/tasks/demo-task/progress.md +11 -0
- package/examples/minimal-project/{docs/09-PLANNING/TASKS → coding-agent-harness/planning/tasks}/demo-task/review.md +1 -1
- package/package.json +22 -13
- 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/standard-task/preset.yaml +2 -2
- package/references/adversarial-review-standard.md +2 -2
- package/references/agents-md-pattern.md +14 -14
- package/references/cadence-ledger.md +1 -1
- package/references/ci-cd-standard.md +1 -1
- package/references/delivery-operating-model-standard.md +4 -4
- package/references/docs-directory-standard.md +65 -159
- package/references/external-source-intake-standard.md +10 -10
- package/references/harness-ledger.md +6 -6
- package/references/legacy-12-phase-bootstrap.md +2 -2
- package/references/lessons-governance.md +15 -15
- package/references/long-running-task-standard.md +6 -6
- package/references/module-parallel-standard.md +34 -34
- package/references/planning-loop.md +6 -6
- package/references/project-onboarding-audit.md +4 -4
- package/references/regression-system.md +2 -2
- package/references/repo-governance-standard.md +4 -4
- package/references/review-routing-standard.md +1 -1
- package/references/ssot-governance.md +19 -19
- package/references/taskr-gap-analysis.md +5 -5
- package/references/walkthrough-closeout.md +14 -14
- package/references/worktree-parallel.md +3 -3
- package/skills/preset-creator/references/complex-task-skeleton/brief.md +11 -0
- package/skills/preset-creator/references/complex-task-skeleton/task_plan.md +1 -1
- package/skills/preset-creator/references/preset-package-skeleton.md +5 -5
- package/templates/AGENTS.md.template +31 -29
- package/templates/architecture/README.md +4 -4
- 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 +12 -0
- package/templates/dashboard/assets/app-src/10-router.js +3 -0
- package/templates/dashboard/assets/app-src/20-overview.js +13 -3
- package/templates/dashboard/assets/app-src/35-task-detail.js +46 -6
- package/templates/dashboard/assets/app-src/40-modules.js +1 -1
- package/templates/dashboard/assets/app-src/55-presets.js +375 -0
- package/templates/dashboard/assets/app-src/60-shared.js +3 -1
- package/templates/dashboard/assets/app-src/90-bindings.js +131 -0
- package/templates/dashboard/assets/app.css +583 -0
- package/templates/dashboard/assets/app.css.manifest.json +1 -0
- package/templates/dashboard/assets/app.js +585 -11
- package/templates/dashboard/assets/app.manifest.json +1 -0
- package/templates/dashboard/assets/css-src/00-foundation.css +4 -0
- package/templates/dashboard/assets/css-src/40-detail-modules-migration.css +62 -0
- package/templates/dashboard/assets/css-src/45-presets.css +516 -0
- package/templates/dashboard/assets/i18n.js +144 -4
- package/templates/development/README.md +10 -10
- package/templates/development/cross-repo-debugging.md +3 -3
- package/templates/development/external-context/service-template.md +2 -2
- package/templates/development/external-source-packs/README.md +4 -4
- package/templates/integrations/README.md +4 -4
- package/templates/integrations/api-contract.md +2 -2
- package/templates/integrations/event-contract.md +2 -2
- package/templates/integrations/third-party/vendor-template.md +2 -2
- package/templates/integrations/webhook-contract.md +2 -2
- package/templates/ledger/Harness-Ledger.md +1 -1
- package/templates/planning/INDEX.md +88 -0
- package/templates/planning/brief.md +1 -1
- package/templates/planning/module_session_prompt.md +2 -1
- package/templates/planning/review.md +0 -18
- package/templates/planning/task_plan.md +5 -44
- package/templates/planning/visual_map.md +13 -9
- package/templates/planning/visual_map.simple.md +52 -0
- package/templates/planning/walkthrough.md +47 -0
- package/templates/reference/docs-library-standard.md +8 -8
- package/templates/reference/execution-workflow-standard.md +29 -2
- package/templates/reference/external-source-intake-standard.md +15 -15
- package/templates/reference/repo-governance-standard.md +1 -1
- package/templates/ssot/Module-Registry.md +1 -1
- package/templates/walkthrough/walkthrough-template.md +2 -2
- package/templates-zh-CN/AGENTS.md.template +31 -29
- package/templates-zh-CN/CLAUDE.md.template +1 -1
- package/templates-zh-CN/architecture/README.md +4 -4
- 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 +10 -10
- package/templates-zh-CN/development/cross-repo-debugging.md +3 -3
- package/templates-zh-CN/development/external-context/service-template.md +2 -2
- package/templates-zh-CN/development/external-source-packs/README.md +4 -4
- package/templates-zh-CN/integrations/README.md +4 -4
- package/templates-zh-CN/integrations/api-contract.md +2 -2
- package/templates-zh-CN/integrations/event-contract.md +2 -2
- package/templates-zh-CN/integrations/third-party/vendor-template.md +2 -2
- package/templates-zh-CN/integrations/webhook-contract.md +2 -2
- 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/planning/INDEX.md +87 -0
- package/templates-zh-CN/planning/brief.md +1 -1
- package/templates-zh-CN/planning/module_session_prompt.md +12 -11
- package/templates-zh-CN/planning/review.md +0 -18
- package/templates-zh-CN/planning/task_plan.md +3 -63
- package/templates-zh-CN/planning/visual_map.md +14 -7
- package/templates-zh-CN/planning/visual_map.simple.md +48 -0
- package/templates-zh-CN/planning/walkthrough.md +47 -0
- package/templates-zh-CN/reference/adversarial-review-standard.md +2 -2
- package/templates-zh-CN/reference/delivery-operating-model-standard.md +3 -3
- package/templates-zh-CN/reference/docs-library-standard.md +28 -28
- package/templates-zh-CN/reference/execution-workflow-standard.md +32 -7
- package/templates-zh-CN/reference/external-source-intake-standard.md +16 -16
- package/templates-zh-CN/reference/harness-ledger-standard.md +6 -6
- 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 +1 -1
- package/templates-zh-CN/reference/walkthrough-standard.md +7 -7
- 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 +3 -3
- package/templates-zh-CN/ssot/Module-Registry.md +3 -3
- package/templates-zh-CN/ssot/Regression-SSoT.md +2 -2
- package/templates-zh-CN/walkthrough/walkthrough-template.md +5 -5
- package/tsconfig.dist.json +16 -0
- package/tsconfig.json +25 -0
- package/tsconfig.runtime.json +24 -0
- package/examples/minimal-project/.harness-capabilities.json +0 -8
- package/examples/minimal-project/docs/09-PLANNING/TASKS/demo-task/progress.md +0 -11
- package/scripts/check-harness.mjs +0 -508
- package/scripts/commands/dashboard-command.mjs +0 -67
- package/scripts/commands/migration-command.mjs +0 -96
- package/scripts/commands/preset-command.mjs +0 -73
- package/scripts/commands/task-command.mjs +0 -327
- package/scripts/harness.mjs +0 -287
- package/scripts/lib/capability-registry.mjs +0 -591
- package/scripts/lib/check-module-parallel.mjs +0 -237
- package/scripts/lib/check-profiles.mjs +0 -418
- package/scripts/lib/check-task-contracts.mjs +0 -47
- package/scripts/lib/core-shared.mjs +0 -196
- package/scripts/lib/dashboard-data.mjs +0 -412
- package/scripts/lib/dashboard-workbench.mjs +0 -257
- package/scripts/lib/dashboard-writer.mjs +0 -198
- package/scripts/lib/git-status-summary.mjs +0 -46
- package/scripts/lib/governance-index-generator.mjs +0 -174
- package/scripts/lib/governance-sync.mjs +0 -514
- package/scripts/lib/governance-table-boundary.mjs +0 -175
- package/scripts/lib/lesson-maintenance.mjs +0 -152
- package/scripts/lib/markdown-utils.mjs +0 -158
- package/scripts/lib/migration-planner.mjs +0 -478
- package/scripts/lib/migration-support.mjs +0 -312
- package/scripts/lib/preset-audit-contracts.mjs +0 -37
- package/scripts/lib/preset-engine.mjs +0 -497
- package/scripts/lib/preset-registry.mjs +0 -627
- package/scripts/lib/preset-resource-contracts.mjs +0 -83
- package/scripts/lib/review-confirm-git-gate.mjs +0 -248
- package/scripts/lib/subagent-authorization-audit.mjs +0 -196
- package/scripts/lib/task-completion-consistency.mjs +0 -16
- package/scripts/lib/task-index.mjs +0 -93
- package/scripts/lib/task-lesson-candidates.mjs +0 -242
- package/scripts/lib/task-lesson-sedimentation.mjs +0 -326
- package/scripts/lib/task-lifecycle/review-confirm.mjs +0 -101
- package/scripts/lib/task-lifecycle/review-gates.mjs +0 -70
- package/scripts/lib/task-lifecycle/text-utils.mjs +0 -24
- package/scripts/lib/task-lifecycle.mjs +0 -649
- package/scripts/lib/task-review-model.mjs +0 -469
- package/scripts/lib/task-scanner.mjs +0 -576
- package/scripts/lib/task-tombstone-commands.mjs +0 -140
- package/scripts/postinstall.mjs +0 -14
- package/templates/walkthrough/Closeout-SSoT.md +0 -43
- package/templates-zh-CN/walkthrough/Closeout-SSoT.md +0 -42
- /package/examples/minimal-project/{docs → coding-agent-harness/governance/generated}/Harness-Ledger.md +0 -0
- /package/examples/minimal-project/{docs/09-PLANNING/TASKS → coding-agent-harness/planning/tasks}/demo-task/brief.md +0 -0
- /package/examples/minimal-project/{docs/09-PLANNING/TASKS → coding-agent-harness/planning/tasks}/demo-task/execution_strategy.md +0 -0
- /package/examples/minimal-project/{docs/09-PLANNING/TASKS → coding-agent-harness/planning/tasks}/demo-task/findings.md +0 -0
- /package/examples/minimal-project/{docs/09-PLANNING/TASKS → coding-agent-harness/planning/tasks}/demo-task/lesson_candidates.md +0 -0
- /package/examples/minimal-project/{docs/09-PLANNING/TASKS → coding-agent-harness/planning/tasks}/demo-task/task_plan.md +0 -0
- /package/examples/minimal-project/{docs/09-PLANNING/TASKS → coding-agent-harness/planning/tasks}/demo-task/visual_map.md +0 -0
|
@@ -1,242 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import {
|
|
4
|
-
tableAfterHeading,
|
|
5
|
-
firstColumn,
|
|
6
|
-
} from "./markdown-utils.mjs";
|
|
7
|
-
import { toPosix } from "./core-shared.mjs";
|
|
8
|
-
|
|
9
|
-
export const allowedLessonCandidateTaskStatuses = new Set([
|
|
10
|
-
"missing",
|
|
11
|
-
"pending-review",
|
|
12
|
-
"no-candidate-accepted",
|
|
13
|
-
"needs-promotion",
|
|
14
|
-
"promoted",
|
|
15
|
-
"rejected",
|
|
16
|
-
]);
|
|
17
|
-
|
|
18
|
-
export const allowedLessonCandidateRowStatuses = new Set([
|
|
19
|
-
"ready-for-review",
|
|
20
|
-
"needs-promotion",
|
|
21
|
-
"promoted",
|
|
22
|
-
"rejected",
|
|
23
|
-
]);
|
|
24
|
-
|
|
25
|
-
export const reviewCompleteLessonCandidateStatuses = new Set([
|
|
26
|
-
"no-candidate-accepted",
|
|
27
|
-
"needs-promotion",
|
|
28
|
-
"promoted",
|
|
29
|
-
"rejected",
|
|
30
|
-
]);
|
|
31
|
-
|
|
32
|
-
export function parseLessonCandidateStatus(content) {
|
|
33
|
-
const text = String(content || "");
|
|
34
|
-
if (!text.trim()) {
|
|
35
|
-
return emptyLessonCandidateStatus("missing", ["missing-candidate-file"]);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const fields = lessonCandidateFields(text);
|
|
39
|
-
const declaredStatus = normalizeLessonCandidateStatus(fields.get("task-level status") || "pending-review");
|
|
40
|
-
const reviewDecision = normalizeCandidateField(fields.get("review decision") || "pending-human-review");
|
|
41
|
-
const promotionState = normalizeCandidateField(fields.get("promotion state") || "not-promoted");
|
|
42
|
-
const closeoutToken = String(fields.get("closeout token") || "pending").trim();
|
|
43
|
-
const candidateTable = lessonCandidateRows(text);
|
|
44
|
-
const rows = candidateTable.rows;
|
|
45
|
-
const issues = [];
|
|
46
|
-
|
|
47
|
-
if (!allowedLessonCandidateTaskStatuses.has(declaredStatus)) {
|
|
48
|
-
issues.push(`invalid-task-status:${declaredStatus}`);
|
|
49
|
-
}
|
|
50
|
-
for (const row of rows) {
|
|
51
|
-
if (!allowedLessonCandidateRowStatuses.has(row.status)) issues.push(`invalid-row-status:${row.id || "missing-id"}:${row.status}`);
|
|
52
|
-
}
|
|
53
|
-
const promotionRows = rows.filter((row) => row.status === "needs-promotion");
|
|
54
|
-
if (promotionRows.length > 0) {
|
|
55
|
-
for (const column of candidateTable.missingColumns) issues.push(`missing-column:${column}`);
|
|
56
|
-
for (const row of promotionRows) {
|
|
57
|
-
for (const [field, label] of [
|
|
58
|
-
["scope", "Scope"],
|
|
59
|
-
["detailArtifact", "Detail Artifact"],
|
|
60
|
-
["boundaryReason", "Boundary Reason"],
|
|
61
|
-
["whyItMightMatter", "Why It Might Matter"],
|
|
62
|
-
["promotionTarget", "Promotion Target"],
|
|
63
|
-
["conflictCheck", "Conflict Check"],
|
|
64
|
-
["requiredStandardUpdate", "Required Standard Update"],
|
|
65
|
-
["followUpTask", "Follow-up Task"],
|
|
66
|
-
]) {
|
|
67
|
-
if (!String(row[field] || "").trim()) issues.push(`missing-row-field:${row.id || "missing-id"}:${label}`);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const aggregateStatus = aggregateLessonCandidateStatus(rows, declaredStatus);
|
|
73
|
-
if (declaredStatus !== aggregateStatus && declaredStatus !== "missing") {
|
|
74
|
-
issues.push(`status-aggregate-mismatch:${declaredStatus}->${aggregateStatus}`);
|
|
75
|
-
}
|
|
76
|
-
if (aggregateStatus === "no-candidate-accepted" && !noCandidateReason(text)) {
|
|
77
|
-
issues.push("missing-no-candidate-reason");
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
return {
|
|
81
|
-
status: aggregateStatus,
|
|
82
|
-
declaredStatus,
|
|
83
|
-
schemaVersion: fields.get("schema version") || "",
|
|
84
|
-
reviewDecision,
|
|
85
|
-
promotionState,
|
|
86
|
-
closeoutToken,
|
|
87
|
-
rows,
|
|
88
|
-
openCount: rows.filter((row) => ["ready-for-review", "needs-promotion"].includes(row.status)).length,
|
|
89
|
-
issues,
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export function isLessonCandidateDecisionComplete(candidateStatus) {
|
|
94
|
-
if (!candidateStatus || candidateStatus.issues?.length) return false;
|
|
95
|
-
return reviewCompleteLessonCandidateStatuses.has(candidateStatus.status);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
export function validateLessonCandidateDetailArtifacts(target, taskDir, candidateStatus) {
|
|
99
|
-
const issues = [];
|
|
100
|
-
const rows = Array.isArray(candidateStatus?.rows) ? candidateStatus.rows : [];
|
|
101
|
-
for (const row of rows.filter((candidate) => candidate.status === "needs-promotion")) {
|
|
102
|
-
const rawPath = String(row.detailArtifact || "").trim();
|
|
103
|
-
if (!rawPath) continue;
|
|
104
|
-
const resolved = resolveTaskLocalLessonArtifact(target, taskDir, rawPath);
|
|
105
|
-
if (!resolved || !isInsideDirectory(taskDir, resolved)) {
|
|
106
|
-
issues.push(`invalid-detail-artifact-path:${row.id || "missing-id"}:${rawPath}`);
|
|
107
|
-
continue;
|
|
108
|
-
}
|
|
109
|
-
if (!fs.existsSync(resolved) || !fs.statSync(resolved).isFile()) {
|
|
110
|
-
issues.push(`missing-detail-artifact:${row.id || "missing-id"}:${rawPath}`);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
return issues;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function emptyLessonCandidateStatus(status, issues = []) {
|
|
117
|
-
return {
|
|
118
|
-
status,
|
|
119
|
-
declaredStatus: status,
|
|
120
|
-
schemaVersion: "",
|
|
121
|
-
reviewDecision: "",
|
|
122
|
-
promotionState: "",
|
|
123
|
-
closeoutToken: "",
|
|
124
|
-
rows: [],
|
|
125
|
-
openCount: 0,
|
|
126
|
-
issues,
|
|
127
|
-
};
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function lessonCandidateFields(content) {
|
|
131
|
-
const { header, rows } = tableAfterHeading(content, /^Field$/i);
|
|
132
|
-
const fieldIndex = firstColumn(header, ["Field", "字段"]);
|
|
133
|
-
const valueIndex = firstColumn(header, ["Value", "值"]);
|
|
134
|
-
const fields = new Map();
|
|
135
|
-
if (fieldIndex < 0 || valueIndex < 0) return fields;
|
|
136
|
-
for (const row of rows) {
|
|
137
|
-
const key = String(row[fieldIndex] || "").trim().toLowerCase();
|
|
138
|
-
if (key) fields.set(key, String(row[valueIndex] || "").trim());
|
|
139
|
-
}
|
|
140
|
-
return fields;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function resolveTaskLocalLessonArtifact(target, taskDir, artifactPath) {
|
|
144
|
-
const raw = String(artifactPath || "").trim();
|
|
145
|
-
if (!raw) return "";
|
|
146
|
-
if (/^(?:https?:|file:|[A-Za-z]:\\|\/)/.test(raw)) return "";
|
|
147
|
-
const relative = raw.startsWith("TARGET:")
|
|
148
|
-
? raw.replace(/^TARGET:/, "").replace(/^\/+/, "")
|
|
149
|
-
: toPosix(path.join(toPosix(path.relative(target.projectRoot, taskDir)), raw));
|
|
150
|
-
return path.resolve(target.projectRoot, relative);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
function isInsideDirectory(parent, child) {
|
|
154
|
-
const relative = path.relative(parent, child);
|
|
155
|
-
return Boolean(relative) && !relative.startsWith("..") && !path.isAbsolute(relative);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
function lessonCandidateRows(content) {
|
|
159
|
-
const { header, rows } = tableAfterHeading(content, /^ID$/i);
|
|
160
|
-
const idIndex = firstColumn(header, ["ID", "候选 ID"]);
|
|
161
|
-
const statusIndex = firstColumn(header, ["Row Status", "行状态", "Status", "状态"]);
|
|
162
|
-
const titleIndex = firstColumn(header, ["Title", "标题"]);
|
|
163
|
-
const decisionIndex = firstColumn(header, ["Review Decision", "审查决定"]);
|
|
164
|
-
const targetIndex = firstColumn(header, ["Promotion Target", "沉淀目标"]);
|
|
165
|
-
const scopeIndex = firstColumn(header, ["Scope", "范围"]);
|
|
166
|
-
const boundaryIndex = firstColumn(header, ["Boundary Reason", "边界原因"]);
|
|
167
|
-
const moduleKeyIndex = firstColumn(header, ["Module Key", "模块 Key", "模块"]);
|
|
168
|
-
const detailArtifactIndex = firstColumn(header, ["Detail Artifact", "Lesson Detail", "详情产物", "详情文件"]);
|
|
169
|
-
const whyIndex = firstColumn(header, ["Why It Might Matter", "价值说明", "为什么重要"]);
|
|
170
|
-
const conflictIndex = firstColumn(header, ["Conflict Check", "冲突检查"]);
|
|
171
|
-
const requiredUpdateIndex = firstColumn(header, ["Required Standard Update", "必需标准更新"]);
|
|
172
|
-
const followUpIndex = firstColumn(header, ["Follow-up Task", "Followup Task", "后续任务"]);
|
|
173
|
-
if (idIndex < 0 || statusIndex < 0) return { rows: [], missingColumns: [] };
|
|
174
|
-
const requiredColumnSpecs = [
|
|
175
|
-
["Scope", scopeIndex],
|
|
176
|
-
["Detail Artifact", detailArtifactIndex],
|
|
177
|
-
["Boundary Reason", boundaryIndex],
|
|
178
|
-
["Why It Might Matter", whyIndex],
|
|
179
|
-
["Promotion Target", targetIndex],
|
|
180
|
-
["Conflict Check", conflictIndex],
|
|
181
|
-
["Required Standard Update", requiredUpdateIndex],
|
|
182
|
-
["Follow-up Task", followUpIndex],
|
|
183
|
-
];
|
|
184
|
-
const missingColumns = requiredColumnSpecs.filter(([, index]) => index < 0).map(([label]) => label);
|
|
185
|
-
return {
|
|
186
|
-
missingColumns,
|
|
187
|
-
rows: rows
|
|
188
|
-
.filter((row) => /^LC-[A-Za-z0-9-]+$/i.test(row[idIndex] || ""))
|
|
189
|
-
.map((row) => ({
|
|
190
|
-
id: row[idIndex] || "",
|
|
191
|
-
status: normalizeLessonCandidateStatus(row[statusIndex] || ""),
|
|
192
|
-
title: row[titleIndex] || "",
|
|
193
|
-
scope: scopeIndex >= 0 ? row[scopeIndex] || "" : "",
|
|
194
|
-
moduleKey: moduleKeyIndex >= 0 ? row[moduleKeyIndex] || "" : "",
|
|
195
|
-
detailArtifact: detailArtifactIndex >= 0 ? row[detailArtifactIndex] || "" : "",
|
|
196
|
-
boundaryReason: boundaryIndex >= 0 ? row[boundaryIndex] || "" : "",
|
|
197
|
-
whyItMightMatter: whyIndex >= 0 ? row[whyIndex] || "" : "",
|
|
198
|
-
reviewDecision: row[decisionIndex] || "",
|
|
199
|
-
promotionTarget: row[targetIndex] || "",
|
|
200
|
-
conflictCheck: conflictIndex >= 0 ? row[conflictIndex] || "" : "",
|
|
201
|
-
requiredStandardUpdate: requiredUpdateIndex >= 0 ? row[requiredUpdateIndex] || "" : "",
|
|
202
|
-
followUpTask: followUpIndex >= 0 ? row[followUpIndex] || "" : "",
|
|
203
|
-
originalRow: row.map((cell) => String(cell || "").trim()).join(" | "),
|
|
204
|
-
})),
|
|
205
|
-
};
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
function normalizeLessonCandidateStatus(value) {
|
|
209
|
-
return String(value || "")
|
|
210
|
-
.replace(/`/g, "")
|
|
211
|
-
.trim()
|
|
212
|
-
.toLowerCase()
|
|
213
|
-
.replaceAll("_", "-")
|
|
214
|
-
.replace(/\s+/g, "-");
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
function normalizeCandidateField(value) {
|
|
218
|
-
return String(value || "").replace(/`/g, "").trim().toLowerCase().replaceAll("_", "-").replace(/\s+/g, "-");
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
function aggregateLessonCandidateStatus(rows, declaredStatus) {
|
|
222
|
-
if (rows.length === 0) return declaredStatus === "no-candidate-accepted" ? "no-candidate-accepted" : declaredStatus;
|
|
223
|
-
const statuses = rows.map((row) => row.status);
|
|
224
|
-
if (statuses.includes("ready-for-review")) return "pending-review";
|
|
225
|
-
if (statuses.includes("needs-promotion")) return "needs-promotion";
|
|
226
|
-
if (statuses.every((status) => status === "promoted")) return "promoted";
|
|
227
|
-
if (statuses.every((status) => status === "rejected")) return "rejected";
|
|
228
|
-
if (statuses.every((status) => ["promoted", "rejected"].includes(status))) return "promoted";
|
|
229
|
-
return declaredStatus;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
function noCandidateReason(content) {
|
|
233
|
-
const lines = String(content || "").split(/\r?\n/);
|
|
234
|
-
const start = lines.findIndex((line) => /^##\s*No-Candidate Reason\s*$/i.test(line.trim()));
|
|
235
|
-
if (start < 0) return "";
|
|
236
|
-
const body = [];
|
|
237
|
-
for (const line of lines.slice(start + 1)) {
|
|
238
|
-
if (/^##\s+/.test(line)) break;
|
|
239
|
-
body.push(line);
|
|
240
|
-
}
|
|
241
|
-
return body.join("\n").replace(/`/g, "").trim();
|
|
242
|
-
}
|
|
@@ -1,326 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import {
|
|
4
|
-
lessonCandidatesFile,
|
|
5
|
-
normalizeTarget,
|
|
6
|
-
readFileSafe,
|
|
7
|
-
toPosix,
|
|
8
|
-
normalizeTaskId,
|
|
9
|
-
localDate,
|
|
10
|
-
} from "./core-shared.mjs";
|
|
11
|
-
import { parseLessonCandidateStatus } from "./task-lesson-candidates.mjs";
|
|
12
|
-
import { createTask, resolveTaskDirectory } from "./task-lifecycle.mjs";
|
|
13
|
-
import { readPresetPackage, buildPresetAudit, renderPresetTemplate } from "./preset-registry.mjs";
|
|
14
|
-
import { firstColumn, updateMarkdownTableRow } from "./markdown-utils.mjs";
|
|
15
|
-
import { taskIdForDirectory } from "./task-scanner.mjs";
|
|
16
|
-
import {
|
|
17
|
-
beginGovernanceSync,
|
|
18
|
-
commitGovernanceSync,
|
|
19
|
-
governanceRelativePaths,
|
|
20
|
-
releaseGovernanceSync,
|
|
21
|
-
} from "./governance-sync.mjs";
|
|
22
|
-
|
|
23
|
-
const presetId = "lesson-sedimentation";
|
|
24
|
-
|
|
25
|
-
export class LessonSedimentationError extends Error {
|
|
26
|
-
constructor(message, { code = "lesson-sedimentation-failed", status = 400, details = {}, recovery = [] } = {}) {
|
|
27
|
-
super(message);
|
|
28
|
-
this.name = "LessonSedimentationError";
|
|
29
|
-
this.code = code;
|
|
30
|
-
this.status = status;
|
|
31
|
-
this.details = details;
|
|
32
|
-
this.recovery = recovery;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export function createLessonSedimentationTask(targetInput, taskRef, candidateId, { dryRun = false, title = "" } = {}) {
|
|
37
|
-
const target = normalizeTarget(targetInput);
|
|
38
|
-
const sourceTaskDir = resolveTaskDirectory(target, taskRef);
|
|
39
|
-
const sourceTaskId = taskIdForDirectory(target, sourceTaskDir);
|
|
40
|
-
const sourceShortId = path.basename(sourceTaskDir);
|
|
41
|
-
const candidatePath = path.join(sourceTaskDir, lessonCandidatesFile);
|
|
42
|
-
const content = readFileSafe(candidatePath);
|
|
43
|
-
const candidateStatus = parseLessonCandidateStatus(content);
|
|
44
|
-
const candidate = candidateStatus.rows.find((row) => row.id === candidateId);
|
|
45
|
-
if (!candidate) {
|
|
46
|
-
throw new LessonSedimentationError(`Lesson candidate not found: ${candidateId}`, {
|
|
47
|
-
code: "lesson-candidate-not-found",
|
|
48
|
-
status: 404,
|
|
49
|
-
details: { candidateId, sourceTask: sourceTaskId },
|
|
50
|
-
recovery: [
|
|
51
|
-
"Open the source task lesson_candidates.md and confirm the candidate ID.",
|
|
52
|
-
"Refresh the Dashboard snapshot if the candidate was just added.",
|
|
53
|
-
],
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
if (!["needs-promotion", "ready-for-review"].includes(candidate.status)) {
|
|
57
|
-
throw new LessonSedimentationError(`Lesson candidate must be ready-for-review or needs-promotion; current status is ${candidate.status}`, {
|
|
58
|
-
code: "lesson-candidate-not-actionable",
|
|
59
|
-
status: 409,
|
|
60
|
-
details: { candidateId, status: candidate.status, sourceTask: sourceTaskId },
|
|
61
|
-
recovery: [
|
|
62
|
-
"Set the candidate status to ready-for-review or needs-promotion after human review.",
|
|
63
|
-
"Use Copy lesson prompt if you only need the prompt without creating a task.",
|
|
64
|
-
],
|
|
65
|
-
});
|
|
66
|
-
}
|
|
67
|
-
if (candidate.followUpTask && !/^pending$/i.test(candidate.followUpTask)) {
|
|
68
|
-
throw new LessonSedimentationError(`Lesson candidate already has follow-up task: ${candidate.followUpTask}`, {
|
|
69
|
-
code: "lesson-follow-up-exists",
|
|
70
|
-
status: 409,
|
|
71
|
-
details: { candidateId, followUpTask: candidate.followUpTask, sourceTask: sourceTaskId },
|
|
72
|
-
recovery: [
|
|
73
|
-
"Open the existing follow-up task instead of creating a duplicate.",
|
|
74
|
-
"If the existing task is wrong, edit the Follow-up Task cell back to pending after review.",
|
|
75
|
-
],
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const preset = readPresetPackage(presetId);
|
|
80
|
-
const slug = normalizeTaskId(`lesson-${sourceShortId.replace(/^\d{4}-\d{2}-\d{2}-/, "")}-${candidate.id}`);
|
|
81
|
-
const taskTitle = title || `Lesson sedimentation for ${candidate.id}`;
|
|
82
|
-
let taskResult;
|
|
83
|
-
try {
|
|
84
|
-
taskResult = createTask(target.projectRoot, slug, {
|
|
85
|
-
title: taskTitle,
|
|
86
|
-
locale: "en-US",
|
|
87
|
-
budget: "standard",
|
|
88
|
-
longRunning: true,
|
|
89
|
-
dryRun,
|
|
90
|
-
});
|
|
91
|
-
} catch (error) {
|
|
92
|
-
if (/Task already exists:/i.test(error.message)) {
|
|
93
|
-
const existingTask = `TASKS/${localDate()}-${slug}`;
|
|
94
|
-
throw new LessonSedimentationError(error.message, {
|
|
95
|
-
code: "lesson-follow-up-directory-exists",
|
|
96
|
-
status: 409,
|
|
97
|
-
details: { candidateId, existingTask, sourceTask: sourceTaskId },
|
|
98
|
-
recovery: [
|
|
99
|
-
"Open the existing task directory and confirm whether it is the intended follow-up.",
|
|
100
|
-
"If it is valid, update the source candidate Follow-up Task cell to that task id.",
|
|
101
|
-
],
|
|
102
|
-
});
|
|
103
|
-
}
|
|
104
|
-
throw error;
|
|
105
|
-
}
|
|
106
|
-
const followUpTaskId = taskResult.task.id;
|
|
107
|
-
const followUpDir = path.join(target.projectRoot, taskResult.task.path.replace(/^TARGET:/, ""));
|
|
108
|
-
const audit = buildPresetAudit(preset, {
|
|
109
|
-
taskId: followUpTaskId,
|
|
110
|
-
targetRoot: target.projectRoot,
|
|
111
|
-
entrypoint: "newTask",
|
|
112
|
-
writeScopes: ["docs/09-PLANNING/TASKS/**"],
|
|
113
|
-
});
|
|
114
|
-
const prompt = renderLessonSedimentationPrompt(preset, {
|
|
115
|
-
target,
|
|
116
|
-
sourceTaskDir,
|
|
117
|
-
sourceTaskId,
|
|
118
|
-
sourceShortId,
|
|
119
|
-
candidate,
|
|
120
|
-
followUpTaskId,
|
|
121
|
-
});
|
|
122
|
-
const contextPacket = renderContextPacket({
|
|
123
|
-
target,
|
|
124
|
-
sourceTaskDir,
|
|
125
|
-
sourceTaskId,
|
|
126
|
-
candidate,
|
|
127
|
-
followUpTaskId,
|
|
128
|
-
audit,
|
|
129
|
-
});
|
|
130
|
-
const changes = [...taskResult.changes];
|
|
131
|
-
|
|
132
|
-
if (!dryRun) {
|
|
133
|
-
const governanceContext = beginGovernanceSync(target, { operation: `lesson-sediment ${sourceTaskId} ${candidate.id}` });
|
|
134
|
-
try {
|
|
135
|
-
appendToFollowUpTask({ followUpDir, sourceTaskId, candidate, prompt, contextPacket, audit });
|
|
136
|
-
updateSourceFollowUpTask(candidatePath, candidate.id, followUpTaskId);
|
|
137
|
-
changes.push(
|
|
138
|
-
{
|
|
139
|
-
destination: toPosix(path.relative(target.projectRoot, path.join(followUpDir, "task_plan.md"))),
|
|
140
|
-
source: "lesson-sedimentation",
|
|
141
|
-
action: "append-preset-context",
|
|
142
|
-
},
|
|
143
|
-
{
|
|
144
|
-
destination: toPosix(path.relative(target.projectRoot, path.join(followUpDir, "progress.md"))),
|
|
145
|
-
source: "lesson-sedimentation",
|
|
146
|
-
action: "append-preset-progress",
|
|
147
|
-
},
|
|
148
|
-
{
|
|
149
|
-
destination: toPosix(path.relative(target.projectRoot, path.join(followUpDir, "artifacts/lesson-sedimentation-prompt.md"))),
|
|
150
|
-
source: "lesson-sedimentation",
|
|
151
|
-
action: "create-prompt-artifact",
|
|
152
|
-
},
|
|
153
|
-
{
|
|
154
|
-
destination: toPosix(path.relative(target.projectRoot, path.join(followUpDir, "artifacts/preset-audit.json"))),
|
|
155
|
-
source: "lesson-sedimentation",
|
|
156
|
-
action: "create-preset-audit",
|
|
157
|
-
},
|
|
158
|
-
{
|
|
159
|
-
destination: toPosix(path.relative(target.projectRoot, candidatePath)),
|
|
160
|
-
source: lessonCandidatesFile,
|
|
161
|
-
action: "update-follow-up-task",
|
|
162
|
-
},
|
|
163
|
-
);
|
|
164
|
-
taskResult.governance = {
|
|
165
|
-
...(taskResult.governance || {}),
|
|
166
|
-
lessonSedimentationCommit: commitGovernanceSync(governanceContext, governanceRelativePaths(changes), {
|
|
167
|
-
message: `chore(harness): record lesson sedimentation ${candidate.id}`,
|
|
168
|
-
}),
|
|
169
|
-
};
|
|
170
|
-
} finally {
|
|
171
|
-
releaseGovernanceSync(governanceContext);
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
return {
|
|
176
|
-
dryRun,
|
|
177
|
-
event: "lesson-sedimentation-task",
|
|
178
|
-
preset: presetId,
|
|
179
|
-
sourceTask: sourceTaskId,
|
|
180
|
-
candidate,
|
|
181
|
-
followUpTask: {
|
|
182
|
-
id: followUpTaskId,
|
|
183
|
-
path: taskResult.task.path,
|
|
184
|
-
title: taskTitle,
|
|
185
|
-
},
|
|
186
|
-
prompt,
|
|
187
|
-
changes,
|
|
188
|
-
governance: taskResult.governance || null,
|
|
189
|
-
};
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
function renderLessonSedimentationPrompt(preset, values) {
|
|
193
|
-
const detailArtifact = resolveDetailArtifact(values.target, values.sourceTaskDir, values.candidate);
|
|
194
|
-
const prompt = renderPresetTemplate(preset, preset.entrypoints.newTask?.templates?.prompt, {
|
|
195
|
-
sourceTaskId: values.sourceTaskId,
|
|
196
|
-
sourceShortId: values.sourceShortId,
|
|
197
|
-
candidateId: values.candidate.id,
|
|
198
|
-
candidateTitle: values.candidate.title,
|
|
199
|
-
candidateScope: values.candidate.scope,
|
|
200
|
-
candidateModuleKey: values.candidate.moduleKey || "n/a",
|
|
201
|
-
detailArtifact: detailArtifact.prefixedPath || "not provided",
|
|
202
|
-
boundaryReason: values.candidate.boundaryReason,
|
|
203
|
-
whyItMightMatter: values.candidate.whyItMightMatter,
|
|
204
|
-
promotionTarget: values.candidate.promotionTarget,
|
|
205
|
-
conflictCheck: values.candidate.conflictCheck,
|
|
206
|
-
requiredStandardUpdate: values.candidate.requiredStandardUpdate,
|
|
207
|
-
followUpTaskId: values.followUpTaskId,
|
|
208
|
-
});
|
|
209
|
-
return prompt.trim();
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
function renderContextPacket({ target, sourceTaskDir, sourceTaskId, candidate, followUpTaskId, audit }) {
|
|
213
|
-
const sourceLessonPath = `TARGET:${toPosix(path.relative(target.projectRoot, path.join(sourceTaskDir, lessonCandidatesFile)))}`;
|
|
214
|
-
const detailArtifact = resolveDetailArtifact(target, sourceTaskDir, candidate);
|
|
215
|
-
const sourceReview = summarizeMarkdown(readFileSafe(path.join(sourceTaskDir, "review.md")));
|
|
216
|
-
const sourceFindings = summarizeMarkdown(readFileSafe(path.join(sourceTaskDir, "findings.md")));
|
|
217
|
-
const sourceProgress = summarizeMarkdown(readFileSafe(path.join(sourceTaskDir, "progress.md")));
|
|
218
|
-
const sourceLessonDetail = summarizeMarkdown(detailArtifact.path ? readFileSafe(detailArtifact.path) : "");
|
|
219
|
-
return [
|
|
220
|
-
"## Lesson Sedimentation Context Packet",
|
|
221
|
-
"",
|
|
222
|
-
"| Field | Value |",
|
|
223
|
-
"| --- | --- |",
|
|
224
|
-
`| Preset | ${presetId} |`,
|
|
225
|
-
`| Follow-up Task | ${followUpTaskId} |`,
|
|
226
|
-
`| Source Task | ${sourceTaskId} |`,
|
|
227
|
-
`| Source Lesson Candidates | ${sourceLessonPath} |`,
|
|
228
|
-
`| Source Lesson Detail | ${detailArtifact.prefixedPath || "not provided"} |`,
|
|
229
|
-
`| Candidate ID | ${candidate.id} |`,
|
|
230
|
-
`| Candidate Title | ${markdownCell(candidate.title)} |`,
|
|
231
|
-
`| Original Candidate Row | ${markdownCell(candidate.originalRow || "")} |`,
|
|
232
|
-
`| Scope | ${markdownCell(candidate.scope || "unspecified")} |`,
|
|
233
|
-
`| Module Key | ${markdownCell(candidate.moduleKey || "n/a")} |`,
|
|
234
|
-
`| Detail Summary | ${markdownCell(sourceLessonDetail || "not recorded")} |`,
|
|
235
|
-
`| Boundary Reason | ${markdownCell(candidate.boundaryReason || "unspecified")} |`,
|
|
236
|
-
`| Why It Might Matter | ${markdownCell(candidate.whyItMightMatter || "unspecified")} |`,
|
|
237
|
-
`| Promotion Target | ${markdownCell(candidate.promotionTarget || "unspecified")} |`,
|
|
238
|
-
`| Conflict Check | ${markdownCell(candidate.conflictCheck || "pending")} |`,
|
|
239
|
-
`| Required Standard Update | ${markdownCell(candidate.requiredStandardUpdate || "pending")} |`,
|
|
240
|
-
`| Review Summary | ${markdownCell(sourceReview)} |`,
|
|
241
|
-
`| Findings Summary | ${markdownCell(sourceFindings)} |`,
|
|
242
|
-
`| Evidence Summary | ${markdownCell(sourceProgress)} |`,
|
|
243
|
-
`| Preset Manifest | ${audit.manifestPath} |`,
|
|
244
|
-
"",
|
|
245
|
-
].join("\n");
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
function resolveDetailArtifact(target, sourceTaskDir, candidate) {
|
|
249
|
-
const raw = String(candidate.detailArtifact || "").trim();
|
|
250
|
-
if (!raw || /^(?:n\/a|none|pending)$/i.test(raw)) return { path: "", prefixedPath: "" };
|
|
251
|
-
const absolute = raw.startsWith("TARGET:")
|
|
252
|
-
? path.resolve(target.projectRoot, raw.replace(/^TARGET:/, "").replace(/^\/+/, ""))
|
|
253
|
-
: path.resolve(sourceTaskDir, raw.replace(/^\.?\//, ""));
|
|
254
|
-
if (!absolute.startsWith(path.resolve(sourceTaskDir) + path.sep)) return { path: "", prefixedPath: "" };
|
|
255
|
-
return {
|
|
256
|
-
path: absolute,
|
|
257
|
-
prefixedPath: `TARGET:${toPosix(path.relative(target.projectRoot, absolute))}`,
|
|
258
|
-
};
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
function appendToFollowUpTask({ followUpDir, sourceTaskId, candidate, prompt, contextPacket, audit }) {
|
|
262
|
-
const taskPlanPath = path.join(followUpDir, "task_plan.md");
|
|
263
|
-
const progressPath = path.join(followUpDir, "progress.md");
|
|
264
|
-
const artifactsDir = path.join(followUpDir, "artifacts");
|
|
265
|
-
fs.mkdirSync(artifactsDir, { recursive: true });
|
|
266
|
-
fs.writeFileSync(path.join(artifactsDir, "lesson-sedimentation-prompt.md"), `${prompt}\n`);
|
|
267
|
-
fs.writeFileSync(path.join(artifactsDir, "preset-audit.json"), `${JSON.stringify(audit, null, 2)}\n`);
|
|
268
|
-
|
|
269
|
-
const taskPlanAppend = [
|
|
270
|
-
"",
|
|
271
|
-
"## Lesson Sedimentation Preset",
|
|
272
|
-
"",
|
|
273
|
-
"Task Preset: lesson-sedimentation",
|
|
274
|
-
"Preset Version: 1",
|
|
275
|
-
"Task Kind: lesson-sedimentation",
|
|
276
|
-
`Source Task: ${sourceTaskId}`,
|
|
277
|
-
`Source Candidate: ${candidate.id}`,
|
|
278
|
-
`Promotion Target: ${candidate.promotionTarget || "pending"}`,
|
|
279
|
-
"",
|
|
280
|
-
contextPacket.trimEnd(),
|
|
281
|
-
"",
|
|
282
|
-
"## Execution Prompt",
|
|
283
|
-
"",
|
|
284
|
-
"The copyable prompt for a new agent session is stored in `artifacts/lesson-sedimentation-prompt.md`.",
|
|
285
|
-
"",
|
|
286
|
-
].join("\n");
|
|
287
|
-
fs.appendFileSync(taskPlanPath, taskPlanAppend);
|
|
288
|
-
fs.appendFileSync(
|
|
289
|
-
progressPath,
|
|
290
|
-
[
|
|
291
|
-
"",
|
|
292
|
-
"### Lesson sedimentation task created",
|
|
293
|
-
"",
|
|
294
|
-
`- Source task: ${sourceTaskId}`,
|
|
295
|
-
`- Source candidate: ${candidate.id}`,
|
|
296
|
-
"- Next: paste the execution prompt into a fresh agent session and require diff-first review before applying changes.",
|
|
297
|
-
"",
|
|
298
|
-
].join("\n"),
|
|
299
|
-
);
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
function updateSourceFollowUpTask(candidatePath, candidateId, followUpTaskId) {
|
|
303
|
-
const content = readFileSafe(candidatePath);
|
|
304
|
-
const update = updateMarkdownTableRow(content, /^ID$/i, (header, row) => {
|
|
305
|
-
const idIndex = firstColumn(header, ["ID", "候选 ID"]);
|
|
306
|
-
const followUpIndex = firstColumn(header, ["Follow-up Task", "Followup Task", "后续任务"]);
|
|
307
|
-
if (idIndex < 0 || followUpIndex < 0 || row[idIndex] !== candidateId) return null;
|
|
308
|
-
const next = [...row];
|
|
309
|
-
next[followUpIndex] = followUpTaskId;
|
|
310
|
-
return next;
|
|
311
|
-
});
|
|
312
|
-
if (!update.matched) throw new Error(`Could not update Follow-up Task column for ${candidateId}`);
|
|
313
|
-
fs.writeFileSync(candidatePath, update.content.endsWith("\n") ? update.content : `${update.content}\n`);
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
function markdownCell(value) {
|
|
317
|
-
return String(value || "").replace(/\r?\n/g, " ").replaceAll("|", "\\|").trim();
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
function summarizeMarkdown(content) {
|
|
321
|
-
const lines = String(content || "")
|
|
322
|
-
.split(/\r?\n/)
|
|
323
|
-
.map((line) => line.replace(/^#+\s*/, "").trim())
|
|
324
|
-
.filter((line) => line && !/^\|?\s*-{3,}/.test(line));
|
|
325
|
-
return lines.slice(0, 4).join(" / ") || "not recorded";
|
|
326
|
-
}
|
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import {
|
|
4
|
-
lessonCandidatesFile,
|
|
5
|
-
nowTimestamp,
|
|
6
|
-
readFileSafe,
|
|
7
|
-
} from "../core-shared.mjs";
|
|
8
|
-
import {
|
|
9
|
-
collectReviewRisks,
|
|
10
|
-
isBlockingReviewRisk,
|
|
11
|
-
isLessonCandidateDecisionComplete,
|
|
12
|
-
parseLessonCandidateStatus,
|
|
13
|
-
parseTaskBudget,
|
|
14
|
-
taskIdForDirectory,
|
|
15
|
-
} from "../task-scanner.mjs";
|
|
16
|
-
import { commitReviewConfirmationGate, prepareReviewConfirmGitGate } from "../review-confirm-git-gate.mjs";
|
|
17
|
-
import { validateHumanReviewConfirmation } from "./review-gates.mjs";
|
|
18
|
-
import { appendProgressLog, markdownCell } from "./text-utils.mjs";
|
|
19
|
-
|
|
20
|
-
export function confirmTaskReview({ target, taskDir, findTaskByDirectory }, { reviewer = "Human Reviewer", message = "", confirmText = "", evidence = "" } = {}) {
|
|
21
|
-
assertTaskDirectoryInsidePlanning(target, taskDir);
|
|
22
|
-
const canonicalTaskId = taskIdForDirectory(target, taskDir);
|
|
23
|
-
const shortId = path.basename(taskDir);
|
|
24
|
-
if (confirmText && ![shortId, canonicalTaskId].includes(confirmText)) {
|
|
25
|
-
throw new Error(`Review confirmation text must match task id: ${shortId}`);
|
|
26
|
-
}
|
|
27
|
-
if (!confirmText) throw new Error(`Missing review confirmation text: ${shortId}`);
|
|
28
|
-
|
|
29
|
-
const reviewPath = path.join(taskDir, "review.md");
|
|
30
|
-
const progressPath = path.join(taskDir, "progress.md");
|
|
31
|
-
const reviewContent = readFileSafe(reviewPath);
|
|
32
|
-
const budget = parseTaskBudget(readFileSafe(path.join(taskDir, "task_plan.md")));
|
|
33
|
-
const candidateStatus = parseLessonCandidateStatus(readFileSafe(path.join(taskDir, lessonCandidatesFile)));
|
|
34
|
-
const blockingRisks = collectReviewRisks(reviewContent).filter(isBlockingReviewRisk);
|
|
35
|
-
if (blockingRisks.length > 0) {
|
|
36
|
-
const ids = blockingRisks.map((risk) => risk.id || risk.severity).join(", ");
|
|
37
|
-
throw new Error(`Open blocking review findings must be closed before confirmation: ${ids}`);
|
|
38
|
-
}
|
|
39
|
-
validateHumanReviewConfirmation({
|
|
40
|
-
task: findTaskByDirectory(target, taskDir),
|
|
41
|
-
budget,
|
|
42
|
-
});
|
|
43
|
-
if (budget !== "simple" && !isLessonCandidateDecisionComplete(candidateStatus)) {
|
|
44
|
-
throw new Error(`Human review confirmation requires lesson candidate decision complete; current status is ${candidateStatus.status}.`);
|
|
45
|
-
}
|
|
46
|
-
const gitGate = prepareReviewConfirmGitGate(target.projectRoot, [reviewPath, progressPath]);
|
|
47
|
-
|
|
48
|
-
const timestamp = nowTimestamp();
|
|
49
|
-
const confirmationId = `HRC-${timestamp.replace(/[^0-9]/g, "").slice(0, 14)}`;
|
|
50
|
-
const safeReviewer = markdownCell(reviewer || "Human Reviewer");
|
|
51
|
-
const safeReviewerEmail = markdownCell(process.env.GIT_AUTHOR_EMAIL || process.env.GIT_COMMITTER_EMAIL || "reviewer@example.invalid");
|
|
52
|
-
const safeMessage = markdownCell(message || "Human review confirmed");
|
|
53
|
-
const safeEvidence = markdownCell(evidence || `TARGET:docs/09-PLANNING/${canonicalTaskId}/review.md`);
|
|
54
|
-
const renderConfirmationBlock = ({ commitSha = "pending", auditStatus = "commit-pending" } = {}) =>
|
|
55
|
-
`## Human Review Confirmation\n\n| Field | Value |\n| --- | --- |\n| Confirmation ID | ${confirmationId} |\n| Confirmed At | ${timestamp} |\n| Reviewer | ${safeReviewer} |\n| Reviewer Email | ${safeReviewerEmail} |\n| Task Key | ${canonicalTaskId} |\n| Confirm Text | ${markdownCell(confirmText)} |\n| Evidence Checked | ${safeEvidence} |\n| Commit SHA | ${markdownCell(commitSha)} |\n| Audit Status | ${markdownCell(auditStatus)} |\n| Message | ${safeMessage} |\n`;
|
|
56
|
-
const confirmationBlock = renderConfirmationBlock();
|
|
57
|
-
const nextReview = replaceReviewConfirmation(reviewContent, confirmationBlock);
|
|
58
|
-
fs.writeFileSync(reviewPath, nextReview.endsWith("\n") ? nextReview : `${nextReview}\n`);
|
|
59
|
-
let progressContent = readFileSafe(progressPath);
|
|
60
|
-
progressContent = appendProgressLog(progressContent, {
|
|
61
|
-
event: "review-confirm",
|
|
62
|
-
message: message || `Human review confirmed by ${reviewer}`,
|
|
63
|
-
evidence: evidence || `TARGET:docs/09-PLANNING/${canonicalTaskId}/review.md`,
|
|
64
|
-
actor: reviewer || "Human Reviewer",
|
|
65
|
-
});
|
|
66
|
-
fs.writeFileSync(progressPath, progressContent.endsWith("\n") ? progressContent : `${progressContent}\n`);
|
|
67
|
-
const audit = commitReviewConfirmationGate(gitGate, {
|
|
68
|
-
taskId: canonicalTaskId,
|
|
69
|
-
reviewPath,
|
|
70
|
-
message: message || `Human review confirmed by ${reviewer}`,
|
|
71
|
-
writeFinalAudit(commitSha) {
|
|
72
|
-
const currentReview = readFileSafe(reviewPath);
|
|
73
|
-
const finalReview = replaceReviewConfirmation(currentReview, renderConfirmationBlock({ commitSha, auditStatus: "committed" }));
|
|
74
|
-
fs.writeFileSync(reviewPath, finalReview.endsWith("\n") ? finalReview : `${finalReview}\n`);
|
|
75
|
-
},
|
|
76
|
-
});
|
|
77
|
-
return {
|
|
78
|
-
event: "review-confirm",
|
|
79
|
-
task: findTaskByDirectory(target, taskDir) || { id: canonicalTaskId, reviewStatus: "confirmed" },
|
|
80
|
-
audit,
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function assertTaskDirectoryInsidePlanning(target, taskDir) {
|
|
85
|
-
const realTaskDir = fs.realpathSync(taskDir);
|
|
86
|
-
const allowedRoots = [
|
|
87
|
-
path.join(target.docsRoot, "09-PLANNING/TASKS"),
|
|
88
|
-
path.join(target.docsRoot, "09-PLANNING/MODULES"),
|
|
89
|
-
].filter(fs.existsSync).map((root) => fs.realpathSync(root));
|
|
90
|
-
if (!allowedRoots.some((root) => realTaskDir === root || realTaskDir.startsWith(`${root}${path.sep}`))) {
|
|
91
|
-
throw new Error(`Task directory outside planning root: ${taskIdForDirectory(target, taskDir)}`);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function replaceReviewConfirmation(content, block) {
|
|
96
|
-
const trimmed = String(content || "").trimEnd();
|
|
97
|
-
if (/^##\s*(?:Human Review Confirmation|人工审查确认)\s*$/im.test(trimmed)) {
|
|
98
|
-
return trimmed.replace(/^##\s*(?:Human Review Confirmation|人工审查确认)\s*$[\s\S]*?(?=^##\s+|(?![\s\S]))/im, `${block.trimEnd()}\n\n`);
|
|
99
|
-
}
|
|
100
|
-
return `${trimmed}\n\n${block.trimEnd()}\n`;
|
|
101
|
-
}
|