coding-agent-harness 1.0.4 → 1.0.5
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/LICENSE +661 -21
- package/LICENSE-EXCEPTION.md +37 -0
- package/README.md +33 -1
- package/README.zh-CN.md +23 -1
- package/SKILL.md +9 -8
- package/docs-release/architecture/overview.md +1 -1
- package/docs-release/architecture/overview.zh-CN.md +1 -1
- package/docs-release/architecture/system-explainer/01-system-overview.md +217 -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 +239 -0
- package/docs-release/architecture/system-explainer/05-data-flow.md +276 -0
- package/docs-release/architecture/system-explainer/06-preset-and-migration.md +303 -0
- package/docs-release/architecture/system-explainer/README.md +67 -0
- package/docs-release/architecture/system-explainer/en-US/01-system-overview.md +226 -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 +250 -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 +323 -0
- package/docs-release/architecture/system-explainer/en-US/README.md +70 -0
- package/docs-release/guides/agent-installation.en-US.md +8 -7
- package/docs-release/guides/agent-installation.md +9 -7
- package/docs-release/guides/preset-development.md +26 -2
- package/docs-release/guides/task-state-machine.en-US.md +30 -13
- package/docs-release/guides/task-state-machine.md +30 -13
- package/examples/minimal-project/docs/09-PLANNING/TASKS/demo-task/INDEX.md +60 -0
- package/package.json +3 -2
- package/references/harness-ledger.md +1 -1
- package/scripts/commands/migration-command.mjs +30 -0
- package/scripts/commands/task-command.mjs +26 -25
- package/scripts/harness.mjs +7 -3
- package/scripts/lib/capability-registry.mjs +17 -21
- package/scripts/lib/check-module-parallel.mjs +9 -16
- package/scripts/lib/check-profiles.mjs +35 -81
- package/scripts/lib/check-task-contracts.mjs +13 -5
- package/scripts/lib/core-shared.mjs +55 -2
- package/scripts/lib/dashboard-data.mjs +126 -18
- package/scripts/lib/dashboard-workbench.mjs +80 -1
- package/scripts/lib/dashboard-writer.mjs +6 -2
- package/scripts/lib/git-status-summary.mjs +1 -1
- package/scripts/lib/governance-sync.mjs +180 -83
- package/scripts/lib/harness-core.mjs +1 -0
- package/scripts/lib/markdown-utils.mjs +33 -0
- package/scripts/lib/migration-planner.mjs +4 -6
- package/scripts/lib/phase-kind.mjs +50 -0
- package/scripts/lib/preset-engine.mjs +5 -8
- package/scripts/lib/preset-registry.mjs +188 -39
- package/scripts/lib/review-confirm-git-gate.mjs +1 -1
- package/scripts/lib/status-builder.mjs +88 -0
- package/scripts/lib/status-dashboard-renderer.mjs +7 -4
- package/scripts/lib/task-audit-metadata.mjs +385 -0
- package/scripts/lib/task-audit-migration.mjs +350 -0
- package/scripts/lib/task-completion-consistency.mjs +11 -1
- package/scripts/lib/task-lifecycle/create-task-helpers.mjs +67 -0
- package/scripts/lib/task-lifecycle/phase-sync.mjs +88 -0
- package/scripts/lib/task-lifecycle/review-confirm.mjs +40 -29
- package/scripts/lib/task-lifecycle/review-gates.mjs +13 -10
- package/scripts/lib/task-lifecycle/review-submission.mjs +63 -0
- package/scripts/lib/task-lifecycle/scaffold-provenance.mjs +49 -0
- package/scripts/lib/task-lifecycle/template-files.mjs +53 -0
- package/scripts/lib/task-lifecycle.mjs +114 -147
- package/scripts/lib/task-metadata.mjs +118 -0
- package/scripts/lib/task-review-model.mjs +54 -68
- package/scripts/lib/task-scanner.mjs +70 -143
- package/skills/preset-creator/references/complex-task-skeleton/brief.md +11 -0
- package/templates/AGENTS.md.template +7 -5
- 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 +7 -3
- package/templates/dashboard/assets/app-src/35-task-detail.js +46 -6
- 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 +578 -10
- 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 +140 -2
- package/templates/planning/INDEX.md +87 -0
- package/templates/planning/brief.md +1 -1
- package/templates/planning/module_session_prompt.md +1 -0
- package/templates/planning/review.md +0 -18
- package/templates/planning/task_plan.md +4 -43
- package/templates/planning/visual_map.md +13 -9
- package/templates/planning/visual_map.simple.md +52 -0
- package/templates/reference/execution-workflow-standard.md +29 -2
- package/templates-zh-CN/AGENTS.md.template +7 -5
- 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 +1 -0
- 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/reference/execution-workflow-standard.md +31 -6
|
@@ -6,12 +6,17 @@ import {
|
|
|
6
6
|
import {
|
|
7
7
|
collectReviewRisks,
|
|
8
8
|
isBlockingReviewRisk,
|
|
9
|
+
parseTaskAuditMetadata,
|
|
9
10
|
parsePhases,
|
|
10
11
|
parseReviewConfirmation,
|
|
11
12
|
readVisualMapContractFile,
|
|
12
13
|
} from "../task-scanner.mjs";
|
|
14
|
+
import {
|
|
15
|
+
implementationPhases,
|
|
16
|
+
phaseHasRecordedProgress,
|
|
17
|
+
} from "../phase-kind.mjs";
|
|
13
18
|
|
|
14
|
-
export function validateLifecycleTransition({ event, currentState, budget, reviewContent = "", reviewTaskKey = "", projectRoot = "", taskDir = "" }) {
|
|
19
|
+
export function validateLifecycleTransition({ event, currentState, budget, reviewContent = "", indexContent = "", reviewTaskKey = "", projectRoot = "", taskDir = "" }) {
|
|
15
20
|
if (event === "task-review" && currentState !== "in_progress") {
|
|
16
21
|
throw new Error(`task-review requires current state in_progress; current state is ${currentState || "unknown"}`);
|
|
17
22
|
}
|
|
@@ -24,7 +29,7 @@ export function validateLifecycleTransition({ event, currentState, budget, revie
|
|
|
24
29
|
const ids = blockingRisks.map((risk) => risk.id || risk.severity).join(", ");
|
|
25
30
|
throw new Error(`Open blocking review findings must be closed before task-complete: ${ids}`);
|
|
26
31
|
}
|
|
27
|
-
if (!parseReviewConfirmation(reviewContent, { taskKey: reviewTaskKey, projectRoot, taskDir })?.confirmed) {
|
|
32
|
+
if (!parseReviewConfirmation(reviewContent, { taskKey: reviewTaskKey, taskAudit: parseTaskAuditMetadata(indexContent), projectRoot, taskDir, indexPath: path.join(taskDir, "INDEX.md") })?.confirmed) {
|
|
28
33
|
throw new Error("Human review must be confirmed before task-complete. Run review-confirm first.");
|
|
29
34
|
}
|
|
30
35
|
}
|
|
@@ -37,15 +42,13 @@ export function validateReviewEntryGate(taskDir, budget) {
|
|
|
37
42
|
throw new Error(`task-review requires ${lessonCandidatesFile} before entering human review.`);
|
|
38
43
|
}
|
|
39
44
|
const phases = parsePhases(readVisualMapContractFile(taskDir).content);
|
|
40
|
-
const actionablePhases = phases
|
|
41
|
-
|
|
42
|
-
(phase)
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
["partial", "present", "waived"].includes(phase.evidenceStatus),
|
|
46
|
-
);
|
|
45
|
+
const actionablePhases = implementationPhases(phases);
|
|
46
|
+
if (phases.length > 0 && actionablePhases.length === 0) {
|
|
47
|
+
throw new Error("task-review requires at least one non-skipped Visual Map execution phase.");
|
|
48
|
+
}
|
|
49
|
+
const hasRecordedPhaseProgress = actionablePhases.some(phaseHasRecordedProgress);
|
|
47
50
|
if (actionablePhases.length > 0 && !hasRecordedPhaseProgress) {
|
|
48
|
-
throw new Error("task-review requires at least one Visual Map phase progress update. Run task-phase before entering human review.");
|
|
51
|
+
throw new Error("task-review requires at least one Visual Map execution phase progress update. Run task-phase before entering human review.");
|
|
49
52
|
}
|
|
50
53
|
}
|
|
51
54
|
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import {
|
|
5
|
+
lessonCandidatesFile,
|
|
6
|
+
longRunningTaskContractFile,
|
|
7
|
+
nowTimestamp,
|
|
8
|
+
readFileSafe,
|
|
9
|
+
toPosix,
|
|
10
|
+
visualMapFile,
|
|
11
|
+
} from "../core-shared.mjs";
|
|
12
|
+
import {
|
|
13
|
+
collectReviewRisks,
|
|
14
|
+
isBlockingReviewRisk,
|
|
15
|
+
taskScannerVersion,
|
|
16
|
+
} from "../task-review-model.mjs";
|
|
17
|
+
import { markdownCell } from "./text-utils.mjs";
|
|
18
|
+
|
|
19
|
+
export function renderAgentReviewSubmission({ target, taskDir, canonicalTaskId, message, evidence }) {
|
|
20
|
+
const timestamp = nowTimestamp();
|
|
21
|
+
const submissionId = `ARS-${timestamp.replace(/[^0-9]/g, "").slice(0, 14)}`;
|
|
22
|
+
const materialsHash = hashTaskMaterials(taskDir);
|
|
23
|
+
const reviewContent = readFileSafe(path.join(taskDir, "review.md"));
|
|
24
|
+
const openFindings = collectReviewRisks(reviewContent).filter(isBlockingReviewRisk).length;
|
|
25
|
+
const evidenceSummary = evidence || message || "Agent submitted task for human review.";
|
|
26
|
+
return [
|
|
27
|
+
"## Agent Review Submission",
|
|
28
|
+
"",
|
|
29
|
+
"| Field | Value |",
|
|
30
|
+
"| --- | --- |",
|
|
31
|
+
`| Submission ID | ${submissionId} |`,
|
|
32
|
+
`| Submitted At | ${timestamp} |`,
|
|
33
|
+
"| Submitted By | agent |",
|
|
34
|
+
`| Task Key | ${canonicalTaskId} |`,
|
|
35
|
+
`| Materials Checklist Hash | ${materialsHash} |`,
|
|
36
|
+
`| Evidence Summary | ${markdownCell(evidenceSummary)} |`,
|
|
37
|
+
`| Open Findings Count | ${openFindings} |`,
|
|
38
|
+
`| Scanner Version | ${taskScannerVersion} |`,
|
|
39
|
+
`| Target | TARGET:${toPosix(path.relative(target.projectRoot, taskDir))} |`,
|
|
40
|
+
"",
|
|
41
|
+
].join("\n");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function replaceAgentReviewSubmission(content, block) {
|
|
45
|
+
const trimmed = String(content || "").trimEnd();
|
|
46
|
+
if (/^##\s*(?:Agent Review Submission|Agent 审查提交|Agent 提交审查)\s*$/im.test(trimmed)) {
|
|
47
|
+
return `${trimmed.replace(/^##\s*(?:Agent Review Submission|Agent 审查提交|Agent 提交审查)\s*$[\s\S]*?(?=^##\s+|(?![\s\S]))/im, `${block.trimEnd()}\n\n`)}\n`;
|
|
48
|
+
}
|
|
49
|
+
return `${trimmed}\n\n${block.trimEnd()}\n`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function hashTaskMaterials(taskDir) {
|
|
53
|
+
const hash = crypto.createHash("sha256");
|
|
54
|
+
for (const fileName of ["brief.md", "task_plan.md", visualMapFile, lessonCandidatesFile, "progress.md", "review.md", "findings.md", longRunningTaskContractFile]) {
|
|
55
|
+
const filePath = path.join(taskDir, fileName);
|
|
56
|
+
if (!fs.existsSync(filePath)) continue;
|
|
57
|
+
hash.update(fileName);
|
|
58
|
+
hash.update("\0");
|
|
59
|
+
hash.update(readFileSafe(filePath));
|
|
60
|
+
hash.update("\0");
|
|
61
|
+
}
|
|
62
|
+
return hash.digest("hex").slice(0, 16);
|
|
63
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { localizedTemplateSource, todayDate } from "../core-shared.mjs";
|
|
3
|
+
import { markdownCell } from "./text-utils.mjs";
|
|
4
|
+
|
|
5
|
+
function shellArg(value) {
|
|
6
|
+
const text = String(value || "");
|
|
7
|
+
if (/^[A-Za-z0-9._/:=@+-]+$/.test(text)) return text;
|
|
8
|
+
return `'${text.replaceAll("'", "'\\''")}'`;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function commandPathArg(value, fallback) {
|
|
12
|
+
const text = String(value || fallback || ".").trim() || ".";
|
|
13
|
+
if (text === ".") return ".";
|
|
14
|
+
return path.isAbsolute(text) ? fallback : text;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function renderNewTaskCommand({ taskId, title, locale, budget, longRunning, moduleKey, preset, fromSession, targetInput }) {
|
|
18
|
+
const parts = ["harness", "new-task"];
|
|
19
|
+
if (taskId) parts.push(taskId);
|
|
20
|
+
parts.push("--budget", budget, "--locale", locale);
|
|
21
|
+
if (title) parts.push("--title", title);
|
|
22
|
+
if (moduleKey) parts.push("--module", moduleKey);
|
|
23
|
+
if (preset && preset !== "none") parts.push("--preset", preset);
|
|
24
|
+
if (fromSession) parts.push("--from-session", commandPathArg(fromSession, "<session.json>"));
|
|
25
|
+
if (longRunning) parts.push("--long-running");
|
|
26
|
+
parts.push(commandPathArg(targetInput, "<target>"));
|
|
27
|
+
return parts.map(shellArg).join(" ");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function buildScaffoldProvenance({ taskId, normalizedTaskId, title, locale, budget, longRunning, moduleKey, preset, fromSession, targetInput, automaticTaskId = false }) {
|
|
31
|
+
return {
|
|
32
|
+
createdBy: "harness new-task",
|
|
33
|
+
command: markdownCell(renderNewTaskCommand({
|
|
34
|
+
taskId: automaticTaskId ? "" : taskId || normalizedTaskId,
|
|
35
|
+
title,
|
|
36
|
+
locale,
|
|
37
|
+
budget,
|
|
38
|
+
longRunning,
|
|
39
|
+
moduleKey,
|
|
40
|
+
preset,
|
|
41
|
+
fromSession,
|
|
42
|
+
targetInput,
|
|
43
|
+
})),
|
|
44
|
+
createdAt: todayDate(),
|
|
45
|
+
budget,
|
|
46
|
+
templateSource: localizedTemplateSource("templates/planning/brief.md", locale),
|
|
47
|
+
exceptionReason: "n/a",
|
|
48
|
+
};
|
|
49
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { localizedTemplateSource, longRunningTaskContractFile, visualMapFile, lessonCandidatesFile } from "../core-shared.mjs";
|
|
2
|
+
|
|
3
|
+
export function taskTemplateFiles({ locale = "en-US" } = {}) {
|
|
4
|
+
return [
|
|
5
|
+
["INDEX.md", "templates/planning/INDEX.md"],
|
|
6
|
+
["brief.md", "templates/planning/brief.md"],
|
|
7
|
+
["task_plan.md", "templates/planning/task_plan.md"],
|
|
8
|
+
["execution_strategy.md", "templates/planning/execution_strategy.md"],
|
|
9
|
+
[visualMapFile, "templates/planning/visual_map.md"],
|
|
10
|
+
["findings.md", "templates/planning/findings.md"],
|
|
11
|
+
[lessonCandidatesFile, "templates/planning/lesson_candidates.md"],
|
|
12
|
+
["progress.md", "templates/planning/progress.md"],
|
|
13
|
+
["review.md", "templates/planning/review.md"],
|
|
14
|
+
].map(([destination, source]) => [destination, localizedTemplateSource(source, locale)]);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function simpleTaskTemplateFiles({ locale = "en-US" } = {}) {
|
|
18
|
+
return [
|
|
19
|
+
["INDEX.md", "templates/planning/INDEX.md"],
|
|
20
|
+
["brief.md", "templates/planning/brief.md"],
|
|
21
|
+
["task_plan.md", "templates/planning/task_plan.md"],
|
|
22
|
+
[visualMapFile, "templates/planning/visual_map.simple.md"],
|
|
23
|
+
["progress.md", "templates/planning/progress.md"],
|
|
24
|
+
].map(([destination, source]) => [destination, localizedTemplateSource(source, locale)]);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function optionalTaskTemplateFiles({ locale = "en-US" } = {}) {
|
|
28
|
+
return [
|
|
29
|
+
["references/INDEX.md", "templates/planning/optional/references/INDEX.md"],
|
|
30
|
+
["artifacts/INDEX.md", "templates/planning/optional/artifacts/INDEX.md"],
|
|
31
|
+
].map(([destination, source]) => [destination, localizedTemplateSource(source, locale)]);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function moduleTemplateFiles({ locale = "en-US" } = {}) {
|
|
35
|
+
return [
|
|
36
|
+
["brief.md", "templates/planning/module_brief.md"],
|
|
37
|
+
["module_plan.md", "templates/planning/module_plan.md"],
|
|
38
|
+
["execution_strategy.md", "templates/planning/execution_strategy.md"],
|
|
39
|
+
[visualMapFile, "templates/planning/visual_map.md"],
|
|
40
|
+
["session_prompt.md", "templates/planning/module_session_prompt.md"],
|
|
41
|
+
].map(([destination, source]) => [destination, localizedTemplateSource(source, locale)]);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function taskFilesForBudget({ budget, locale }) {
|
|
45
|
+
if (budget === "simple") return simpleTaskTemplateFiles({ locale });
|
|
46
|
+
if (budget === "complex") return [...taskTemplateFiles({ locale }), ...optionalTaskTemplateFiles({ locale })];
|
|
47
|
+
return taskTemplateFiles({ locale });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function appendLongRunningContractFile(files, { locale, longRunning }) {
|
|
51
|
+
if (!longRunning) return files;
|
|
52
|
+
return [...files, [longRunningTaskContractFile, localizedTemplateSource("templates/planning/long-running-task-contract.md", locale)]];
|
|
53
|
+
}
|
|
@@ -5,7 +5,6 @@ import {
|
|
|
5
5
|
visualMapFile,
|
|
6
6
|
legacyVisualRoadmapFile,
|
|
7
7
|
lessonCandidatesFile,
|
|
8
|
-
longRunningTaskContractFile,
|
|
9
8
|
allowedTaskStates,
|
|
10
9
|
allowedTaskBudgets,
|
|
11
10
|
allowedPhaseStates,
|
|
@@ -15,11 +14,9 @@ import {
|
|
|
15
14
|
toPosix,
|
|
16
15
|
readFileSafe,
|
|
17
16
|
readBundledTemplate,
|
|
18
|
-
localizedTemplateSource,
|
|
19
17
|
todayDate,
|
|
20
18
|
localDate,
|
|
21
19
|
datePrefix,
|
|
22
|
-
nowTimestamp,
|
|
23
20
|
normalizeTaskId,
|
|
24
21
|
renderTaskTemplate,
|
|
25
22
|
} from "./core-shared.mjs";
|
|
@@ -35,24 +32,30 @@ import {
|
|
|
35
32
|
} from "./preset-engine.mjs";
|
|
36
33
|
import {
|
|
37
34
|
collectTasks,
|
|
38
|
-
collectReviewRisks,
|
|
39
|
-
isBlockingReviewRisk,
|
|
40
35
|
listTaskPlanPaths,
|
|
41
36
|
parseTaskBudget,
|
|
42
37
|
taskIdForDirectory,
|
|
43
|
-
taskScannerVersion,
|
|
44
38
|
} from "./task-scanner.mjs";
|
|
39
|
+
import { getColumn, firstColumn, updateMarkdownTableRow } from "./markdown-utils.mjs";
|
|
40
|
+
import { validateLifecycleTransition, validateReviewEntryGate } from "./task-lifecycle/review-gates.mjs";
|
|
41
|
+
import { advanceLifecyclePhase, autoRecordNoLessonCandidateDecision } from "./task-lifecycle/phase-sync.mjs";
|
|
42
|
+
import { confirmTaskReview as confirmTaskReviewWithContext } from "./task-lifecycle/review-confirm.mjs";
|
|
43
|
+
import { appendProgressLog } from "./task-lifecycle/text-utils.mjs";
|
|
44
|
+
import { buildScaffoldProvenance } from "./task-lifecycle/scaffold-provenance.mjs";
|
|
45
|
+
import { buildCreationTaskAudit } from "./task-audit-metadata.mjs";
|
|
45
46
|
import {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
} from "./markdown-utils.mjs";
|
|
47
|
+
renderAgentReviewSubmission,
|
|
48
|
+
replaceAgentReviewSubmission,
|
|
49
|
+
} from "./task-lifecycle/review-submission.mjs";
|
|
50
50
|
import {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
import {
|
|
51
|
+
appendLongRunningContractFile,
|
|
52
|
+
moduleTemplateFiles,
|
|
53
|
+
taskFilesForBudget,
|
|
54
|
+
} from "./task-lifecycle/template-files.mjs";
|
|
55
|
+
import {
|
|
56
|
+
planCreateTaskChanges,
|
|
57
|
+
refreshPresetCommandAudit,
|
|
58
|
+
} from "./task-lifecycle/create-task-helpers.mjs";
|
|
56
59
|
import {
|
|
57
60
|
beginGovernanceSync,
|
|
58
61
|
commitGovernanceSync,
|
|
@@ -62,45 +65,6 @@ import {
|
|
|
62
65
|
syncTaskGovernance,
|
|
63
66
|
} from "./governance-sync.mjs";
|
|
64
67
|
|
|
65
|
-
function taskTemplateFiles({ locale = "en-US" } = {}) {
|
|
66
|
-
return [
|
|
67
|
-
["brief.md", "templates/planning/brief.md"],
|
|
68
|
-
["task_plan.md", "templates/planning/task_plan.md"],
|
|
69
|
-
["execution_strategy.md", "templates/planning/execution_strategy.md"],
|
|
70
|
-
[visualMapFile, "templates/planning/visual_map.md"],
|
|
71
|
-
["findings.md", "templates/planning/findings.md"],
|
|
72
|
-
[lessonCandidatesFile, "templates/planning/lesson_candidates.md"],
|
|
73
|
-
["progress.md", "templates/planning/progress.md"],
|
|
74
|
-
["review.md", "templates/planning/review.md"],
|
|
75
|
-
].map(([destination, source]) => [destination, localizedTemplateSource(source, locale)]);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function simpleTaskTemplateFiles({ locale = "en-US" } = {}) {
|
|
79
|
-
return [
|
|
80
|
-
["brief.md", "templates/planning/brief.md"],
|
|
81
|
-
["task_plan.md", "templates/planning/task_plan.md"],
|
|
82
|
-
[visualMapFile, "templates/planning/visual_map.md"],
|
|
83
|
-
["progress.md", "templates/planning/progress.md"],
|
|
84
|
-
].map(([destination, source]) => [destination, localizedTemplateSource(source, locale)]);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function optionalTaskTemplateFiles({ locale = "en-US" } = {}) {
|
|
88
|
-
return [
|
|
89
|
-
["references/INDEX.md", "templates/planning/optional/references/INDEX.md"],
|
|
90
|
-
["artifacts/INDEX.md", "templates/planning/optional/artifacts/INDEX.md"],
|
|
91
|
-
].map(([destination, source]) => [destination, localizedTemplateSource(source, locale)]);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function moduleTemplateFiles({ locale = "en-US" } = {}) {
|
|
95
|
-
return [
|
|
96
|
-
["brief.md", "templates/planning/module_brief.md"],
|
|
97
|
-
["module_plan.md", "templates/planning/module_plan.md"],
|
|
98
|
-
["execution_strategy.md", "templates/planning/execution_strategy.md"],
|
|
99
|
-
[visualMapFile, "templates/planning/visual_map.md"],
|
|
100
|
-
["session_prompt.md", "templates/planning/module_session_prompt.md"],
|
|
101
|
-
].map(([destination, source]) => [destination, localizedTemplateSource(source, locale)]);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
68
|
function taskRoot(target, taskId, { moduleKey = "" } = {}) {
|
|
105
69
|
const normalizedTaskId = normalizeTaskId(taskId);
|
|
106
70
|
if (moduleKey) return path.join(target.docsRoot, "09-PLANNING/MODULES", normalizeTaskId(moduleKey), normalizedTaskId);
|
|
@@ -175,17 +139,6 @@ function normalizeTaskPresetInput(preset, { targetInput = "" } = {}) {
|
|
|
175
139
|
return readPresetPackage(normalized, { targetInput }).id;
|
|
176
140
|
}
|
|
177
141
|
|
|
178
|
-
function taskFilesForBudget({ budget, locale }) {
|
|
179
|
-
if (budget === "simple") return simpleTaskTemplateFiles({ locale });
|
|
180
|
-
if (budget === "complex") return [...taskTemplateFiles({ locale }), ...optionalTaskTemplateFiles({ locale })];
|
|
181
|
-
return taskTemplateFiles({ locale });
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
function appendLongRunningContractFile(files, { locale, longRunning }) {
|
|
185
|
-
if (!longRunning) return files;
|
|
186
|
-
return [...files, [longRunningTaskContractFile, localizedTemplateSource("templates/planning/long-running-task-contract.md", locale)]];
|
|
187
|
-
}
|
|
188
|
-
|
|
189
142
|
function updateProgressState(content, state, locale) {
|
|
190
143
|
const label = stateLabel(state, locale);
|
|
191
144
|
if (/^##\s*状态[::][^\n]*/im.test(content)) {
|
|
@@ -207,7 +160,31 @@ function bareSlug(datedId) {
|
|
|
207
160
|
return datedId;
|
|
208
161
|
}
|
|
209
162
|
|
|
210
|
-
|
|
163
|
+
function automaticTaskSlug(seed) {
|
|
164
|
+
return normalizeTaskId(seed || "task").slice(0, 48).replace(/-+$/g, "") || "task";
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function randomTaskSuffix() {
|
|
168
|
+
return crypto.randomBytes(4).toString("hex");
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function resolveTaskIdentity({ target, taskId, title, presetPackage, moduleKey, automaticTaskId }) {
|
|
172
|
+
if (!automaticTaskId) {
|
|
173
|
+
const rawNormalized = normalizeTaskId(taskId || (presetPackage?.task?.defaultTaskId || ""));
|
|
174
|
+
const normalizedTaskId = ensureDatePrefix(rawNormalized);
|
|
175
|
+
if (!normalizedTaskId) throw new Error("Missing task id");
|
|
176
|
+
return { normalizedTaskId, semanticSlug: bareSlug(normalizedTaskId) };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const semanticSlug = automaticTaskSlug(title || presetPackage?.task?.defaultTaskId || "task");
|
|
180
|
+
for (let attempt = 0; attempt < 8; attempt += 1) {
|
|
181
|
+
const normalizedTaskId = `${localDate()}-${semanticSlug}-${randomTaskSuffix()}`;
|
|
182
|
+
if (!fs.existsSync(taskRoot(target, normalizedTaskId, { moduleKey }))) return { normalizedTaskId, semanticSlug };
|
|
183
|
+
}
|
|
184
|
+
throw new Error(`Unable to allocate automatic task id for: ${semanticSlug}`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function createTask(targetInput, taskId, { title = "", locale = "en-US", dryRun = false, moduleKey = "", budget = "standard", longRunning = false, preset = "", fromSession = "", presetArgs = [], automaticTaskId = false } = {}) {
|
|
211
188
|
const requestedPreset = preset || (moduleKey ? "module" : "");
|
|
212
189
|
const normalizedPreset = normalizeTaskPresetInput(requestedPreset, { targetInput });
|
|
213
190
|
const presetPackage = normalizedPreset === "none" ? null : readPresetPackage(normalizedPreset, { targetInput });
|
|
@@ -220,15 +197,28 @@ export function createTask(targetInput, taskId, { title = "", locale = "en-US",
|
|
|
220
197
|
if (presetPackage && !presetPackage.compatibleBudgets.includes(normalizedBudget)) throw new Error(`${normalizedPreset} preset requires --budget ${presetPackage.compatibleBudgets.join("|")}`);
|
|
221
198
|
if (presetPackage?.task?.projectLevelOnly === true && moduleKey) throw new Error(`${normalizedPreset} preset is project-level and cannot be combined with --module`);
|
|
222
199
|
if (presetPackage?.task?.requiresFromSession === true && !fromSession) throw new Error(`${normalizedPreset} preset requires --from-session`);
|
|
223
|
-
const rawNormalized = normalizeTaskId(taskId || (presetPackage?.task?.defaultTaskId || ""));
|
|
224
|
-
const normalizedTaskId = ensureDatePrefix(rawNormalized);
|
|
225
|
-
if (!normalizedTaskId) throw new Error("Missing task id");
|
|
226
|
-
const semanticSlug = bareSlug(normalizedTaskId);
|
|
227
200
|
const normalizedModuleKey = moduleKey ? normalizeTaskId(moduleKey) : "";
|
|
201
|
+
const identity = resolveTaskIdentity({ target, taskId, title, presetPackage, moduleKey: normalizedModuleKey, automaticTaskId });
|
|
202
|
+
const normalizedTaskId = identity.normalizedTaskId;
|
|
203
|
+
const semanticSlug = identity.semanticSlug;
|
|
228
204
|
const normalizedLocale = normalizeLocale(locale || readCapabilityRegistry(target).locale);
|
|
229
205
|
const taskTitle = title || (normalizedPreset === "legacy-migration" ? "Harness v1 legacy migration" : semanticSlug);
|
|
230
206
|
const directory = taskRoot(target, normalizedTaskId, { moduleKey: normalizedModuleKey });
|
|
231
207
|
if (fs.existsSync(directory)) throw new Error(`Task already exists: ${normalizedTaskId}`);
|
|
208
|
+
const scaffoldProvenance = buildScaffoldProvenance({
|
|
209
|
+
taskId,
|
|
210
|
+
normalizedTaskId,
|
|
211
|
+
title,
|
|
212
|
+
locale: normalizedLocale,
|
|
213
|
+
budget: normalizedBudget,
|
|
214
|
+
longRunning,
|
|
215
|
+
moduleKey: normalizedModuleKey,
|
|
216
|
+
preset: normalizedPreset,
|
|
217
|
+
fromSession,
|
|
218
|
+
targetInput: presetInputs?.targetInput || targetInput,
|
|
219
|
+
automaticTaskId,
|
|
220
|
+
});
|
|
221
|
+
const baseTaskAudit = buildCreationTaskAudit(scaffoldProvenance, { projectRoot: target.projectRoot });
|
|
232
222
|
const evaluatedPresetValues = presetPackage ? evaluateTemplateValues(presetPackage, presetInputs.inputs, { taskId: normalizedTaskId, taskTitle, moduleKey: normalizedModuleKey }) : null;
|
|
233
223
|
const presetContext = presetPackage
|
|
234
224
|
? buildPresetContext({ ...presetPackage, task: { ...(presetPackage.task || {}), kind: presetPackage.task?.kind || "general" } }, {
|
|
@@ -240,8 +230,37 @@ export function createTask(targetInput, taskId, { title = "", locale = "en-US",
|
|
|
240
230
|
evaluatedValues: evaluatedPresetValues,
|
|
241
231
|
})
|
|
242
232
|
: null;
|
|
233
|
+
const task = {
|
|
234
|
+
id: taskIdForDirectory(target, directory),
|
|
235
|
+
shortId: normalizedTaskId,
|
|
236
|
+
title: taskTitle,
|
|
237
|
+
module: normalizedModuleKey || null,
|
|
238
|
+
path: `TARGET:${toPosix(path.relative(target.projectRoot, directory))}`,
|
|
239
|
+
locale: normalizedLocale,
|
|
240
|
+
budget: normalizedBudget,
|
|
241
|
+
kind: presetContext?.kind || "general",
|
|
242
|
+
preset: normalizedPreset,
|
|
243
|
+
presetVersion: presetContext?.presetVersion || "",
|
|
244
|
+
presetAudit: presetContext?.audit || null,
|
|
245
|
+
migrationTargetLevel: presetContext?.migrationTargetLevel || "",
|
|
246
|
+
migrationAchievedLevel: presetContext?.migrationAchievedLevel || "",
|
|
247
|
+
evidenceBundle: presetContext?.evidenceBundle || "",
|
|
248
|
+
longRunning,
|
|
249
|
+
};
|
|
250
|
+
const plannedChanges = planCreateTaskChanges({
|
|
251
|
+
target,
|
|
252
|
+
directory,
|
|
253
|
+
normalizedModuleKey,
|
|
254
|
+
normalizedLocale,
|
|
255
|
+
normalizedBudget,
|
|
256
|
+
longRunning,
|
|
257
|
+
presetContext,
|
|
258
|
+
task,
|
|
259
|
+
});
|
|
260
|
+
const plannedGovernance = syncTaskGovernance(target, task, { event: "new-task", state: "planned", message: "task registered by CLI", dryRun: true });
|
|
261
|
+
const plannedWriteScopes = governanceRelativePaths([...plannedChanges, ...plannedGovernance.changes]);
|
|
243
262
|
const changes = [];
|
|
244
|
-
const governanceContext = beginGovernanceSync(target, { operation: `new-task ${normalizedTaskId}`, dryRun });
|
|
263
|
+
const governanceContext = beginGovernanceSync(target, { operation: `new-task ${normalizedTaskId}`, dryRun, allowDirtyWorktree: true, allowedRelativePaths: plannedWriteScopes });
|
|
245
264
|
try {
|
|
246
265
|
if (normalizedModuleKey) {
|
|
247
266
|
const moduleDirectory = path.dirname(directory);
|
|
@@ -263,6 +282,13 @@ export function createTask(targetInput, taskId, { title = "", locale = "en-US",
|
|
|
263
282
|
title: normalizedModuleKey,
|
|
264
283
|
locale: normalizedLocale,
|
|
265
284
|
budget: normalizedBudget,
|
|
285
|
+
moduleKey: normalizedModuleKey,
|
|
286
|
+
preset: normalizedPreset,
|
|
287
|
+
presetVersion: presetContext?.presetVersion || "",
|
|
288
|
+
evidenceBundle: presetContext?.evidenceBundle || "",
|
|
289
|
+
longRunning,
|
|
290
|
+
scaffoldProvenance,
|
|
291
|
+
taskAudit: buildCreationTaskAudit({ ...scaffoldProvenance, templateSource: source }, { projectRoot: target.projectRoot }),
|
|
266
292
|
}),
|
|
267
293
|
);
|
|
268
294
|
}
|
|
@@ -288,6 +314,18 @@ export function createTask(targetInput, taskId, { title = "", locale = "en-US",
|
|
|
288
314
|
title: taskTitle,
|
|
289
315
|
locale: normalizedLocale,
|
|
290
316
|
budget: normalizedBudget,
|
|
317
|
+
moduleKey: normalizedModuleKey,
|
|
318
|
+
preset: normalizedPreset,
|
|
319
|
+
presetVersion: presetContext?.presetVersion || "",
|
|
320
|
+
evidenceBundle: presetContext?.evidenceBundle || "",
|
|
321
|
+
longRunning,
|
|
322
|
+
scaffoldProvenance: {
|
|
323
|
+
...scaffoldProvenance,
|
|
324
|
+
templateSource: source,
|
|
325
|
+
},
|
|
326
|
+
taskAudit: destination === "INDEX.md"
|
|
327
|
+
? buildCreationTaskAudit({ ...scaffoldProvenance, templateSource: source }, { projectRoot: target.projectRoot })
|
|
328
|
+
: baseTaskAudit,
|
|
291
329
|
}), presetContext),
|
|
292
330
|
);
|
|
293
331
|
}
|
|
@@ -333,23 +371,6 @@ export function createTask(targetInput, taskId, { title = "", locale = "en-US",
|
|
|
333
371
|
fs.writeFileSync(destinationPath, renderPresetResourceIndex(existing, kind, rows));
|
|
334
372
|
}
|
|
335
373
|
}
|
|
336
|
-
const task = {
|
|
337
|
-
id: taskIdForDirectory(target, directory),
|
|
338
|
-
shortId: normalizedTaskId,
|
|
339
|
-
title: taskTitle,
|
|
340
|
-
module: normalizedModuleKey || null,
|
|
341
|
-
path: `TARGET:${toPosix(path.relative(target.projectRoot, directory))}`,
|
|
342
|
-
locale: normalizedLocale,
|
|
343
|
-
budget: normalizedBudget,
|
|
344
|
-
kind: presetContext?.kind || "general",
|
|
345
|
-
preset: normalizedPreset,
|
|
346
|
-
presetVersion: presetContext?.presetVersion || "",
|
|
347
|
-
presetAudit: presetContext?.audit || null,
|
|
348
|
-
migrationTargetLevel: presetContext?.migrationTargetLevel || "",
|
|
349
|
-
migrationAchievedLevel: presetContext?.migrationAchievedLevel || "",
|
|
350
|
-
evidenceBundle: presetContext?.evidenceBundle || "",
|
|
351
|
-
longRunning,
|
|
352
|
-
};
|
|
353
374
|
const governance = syncTaskGovernance(target, task, { event: "new-task", state: "planned", message: "task registered by CLI", dryRun });
|
|
354
375
|
changes.push(...governance.changes);
|
|
355
376
|
const commandWriteScopes = governanceRelativePaths(changes);
|
|
@@ -371,21 +392,6 @@ export function createTask(targetInput, taskId, { title = "", locale = "en-US",
|
|
|
371
392
|
}
|
|
372
393
|
}
|
|
373
394
|
|
|
374
|
-
function refreshPresetCommandAudit(target, presetContext, { commandWriteScopes = [], dryRun = false } = {}) {
|
|
375
|
-
const scopes = [...new Set(commandWriteScopes.filter(Boolean))];
|
|
376
|
-
presetContext.audit = {
|
|
377
|
-
...presetContext.audit,
|
|
378
|
-
presetWriteScopes: presetContext.audit.writeScopes || [],
|
|
379
|
-
commandWriteScopes: scopes,
|
|
380
|
-
};
|
|
381
|
-
for (const evidence of presetContext.evidenceFiles || []) {
|
|
382
|
-
if (evidence.source !== "preset-audit") continue;
|
|
383
|
-
evidence.content = `${JSON.stringify(presetContext.audit, null, 2)}\n`;
|
|
384
|
-
if (dryRun) continue;
|
|
385
|
-
fs.writeFileSync(path.join(target.projectRoot, evidence.relativePath), evidence.content);
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
|
|
389
395
|
export function updateTaskLifecycle(targetInput, taskId, { event = "task-log", state = "", message = "", evidence = "" } = {}) {
|
|
390
396
|
const target = normalizeTarget(targetInput);
|
|
391
397
|
const taskDir = resolveTaskDirectory(target, taskId);
|
|
@@ -401,6 +407,7 @@ export function updateTaskLifecycle(targetInput, taskId, { event = "task-log", s
|
|
|
401
407
|
currentState: currentTask?.state || "unknown",
|
|
402
408
|
budget,
|
|
403
409
|
reviewContent: readFileSafe(path.join(taskDir, "review.md")),
|
|
410
|
+
indexContent: readFileSafe(path.join(taskDir, "INDEX.md")),
|
|
404
411
|
reviewTaskKey: canonicalTaskId,
|
|
405
412
|
projectRoot: target.projectRoot,
|
|
406
413
|
taskDir,
|
|
@@ -413,6 +420,8 @@ export function updateTaskLifecycle(targetInput, taskId, { event = "task-log", s
|
|
|
413
420
|
content = appendProgressLog(content, { event, message, evidence });
|
|
414
421
|
fs.writeFileSync(progressPath, content.endsWith("\n") ? content : `${content}\n`);
|
|
415
422
|
const allowedPaths = [toPosix(path.relative(target.projectRoot, progressPath))];
|
|
423
|
+
const advancedPhasePath = advanceLifecyclePhase(target, taskDir, event);
|
|
424
|
+
if (advancedPhasePath) allowedPaths.push(advancedPhasePath);
|
|
416
425
|
if (event === "task-review") {
|
|
417
426
|
const reviewPath = path.join(taskDir, "review.md");
|
|
418
427
|
const reviewContent = readFileSafe(reviewPath);
|
|
@@ -430,6 +439,8 @@ export function updateTaskLifecycle(targetInput, taskId, { event = "task-log", s
|
|
|
430
439
|
),
|
|
431
440
|
);
|
|
432
441
|
allowedPaths.push(toPosix(path.relative(target.projectRoot, reviewPath)));
|
|
442
|
+
const lessonDecisionPath = autoRecordNoLessonCandidateDecision(target, taskDir);
|
|
443
|
+
if (lessonDecisionPath) allowedPaths.push(lessonDecisionPath);
|
|
433
444
|
}
|
|
434
445
|
const task =
|
|
435
446
|
findTaskByDirectory(target, taskDir) ||
|
|
@@ -460,50 +471,6 @@ export function confirmTaskReview(targetInput, taskId, { reviewer = "Human Revie
|
|
|
460
471
|
const taskDir = resolveTaskDirectory(target, taskId);
|
|
461
472
|
return confirmTaskReviewWithContext({ target, taskDir, findTaskByDirectory }, { reviewer, message, confirmText, evidence });
|
|
462
473
|
}
|
|
463
|
-
function renderAgentReviewSubmission({ target, taskDir, canonicalTaskId, message, evidence }) {
|
|
464
|
-
const timestamp = nowTimestamp();
|
|
465
|
-
const submissionId = `ARS-${timestamp.replace(/[^0-9]/g, "").slice(0, 14)}`;
|
|
466
|
-
const materialsHash = hashTaskMaterials(taskDir);
|
|
467
|
-
const reviewContent = readFileSafe(path.join(taskDir, "review.md"));
|
|
468
|
-
const openFindings = collectReviewRisks(reviewContent).filter(isBlockingReviewRisk).length;
|
|
469
|
-
const evidenceSummary = evidence || message || "Agent submitted task for human review.";
|
|
470
|
-
return [
|
|
471
|
-
"## Agent Review Submission",
|
|
472
|
-
"",
|
|
473
|
-
"| Field | Value |",
|
|
474
|
-
"| --- | --- |",
|
|
475
|
-
`| Submission ID | ${submissionId} |`,
|
|
476
|
-
`| Submitted At | ${timestamp} |`,
|
|
477
|
-
"| Submitted By | agent |",
|
|
478
|
-
`| Task Key | ${canonicalTaskId} |`,
|
|
479
|
-
`| Materials Checklist Hash | ${materialsHash} |`,
|
|
480
|
-
`| Evidence Summary | ${markdownCell(evidenceSummary)} |`,
|
|
481
|
-
`| Open Findings Count | ${openFindings} |`,
|
|
482
|
-
`| Scanner Version | ${taskScannerVersion} |`,
|
|
483
|
-
`| Target | TARGET:${toPosix(path.relative(target.projectRoot, taskDir))} |`,
|
|
484
|
-
"",
|
|
485
|
-
].join("\n");
|
|
486
|
-
}
|
|
487
|
-
function replaceAgentReviewSubmission(content, block) {
|
|
488
|
-
const trimmed = String(content || "").trimEnd();
|
|
489
|
-
if (/^##\s*(?:Agent Review Submission|Agent 审查提交|Agent 提交审查)\s*$/im.test(trimmed)) {
|
|
490
|
-
return `${trimmed.replace(/^##\s*(?:Agent Review Submission|Agent 审查提交|Agent 提交审查)\s*$[\s\S]*?(?=^##\s+|(?![\s\S]))/im, `${block.trimEnd()}\n\n`)}\n`;
|
|
491
|
-
}
|
|
492
|
-
return `${trimmed}\n\n${block.trimEnd()}\n`;
|
|
493
|
-
}
|
|
494
|
-
function hashTaskMaterials(taskDir) {
|
|
495
|
-
const hash = crypto.createHash("sha256");
|
|
496
|
-
for (const fileName of ["brief.md", "task_plan.md", visualMapFile, lessonCandidatesFile, "progress.md", "review.md", "findings.md", longRunningTaskContractFile]) {
|
|
497
|
-
const filePath = path.join(taskDir, fileName);
|
|
498
|
-
if (!fs.existsSync(filePath)) continue;
|
|
499
|
-
hash.update(fileName);
|
|
500
|
-
hash.update("\0");
|
|
501
|
-
hash.update(readFileSafe(filePath));
|
|
502
|
-
hash.update("\0");
|
|
503
|
-
}
|
|
504
|
-
return hash.digest("hex").slice(0, 16);
|
|
505
|
-
}
|
|
506
|
-
|
|
507
474
|
export function updateTaskPhase(targetInput, taskId, phaseId, { state = "", completion = "", evidenceStatus = "" } = {}) {
|
|
508
475
|
const target = normalizeTarget(targetInput);
|
|
509
476
|
const taskDir = resolveTaskDirectory(target, taskId);
|