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.
Files changed (100) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/LICENSE +661 -21
  3. package/LICENSE-EXCEPTION.md +37 -0
  4. package/README.md +33 -1
  5. package/README.zh-CN.md +23 -1
  6. package/SKILL.md +9 -8
  7. package/docs-release/architecture/overview.md +1 -1
  8. package/docs-release/architecture/overview.zh-CN.md +1 -1
  9. package/docs-release/architecture/system-explainer/01-system-overview.md +217 -0
  10. package/docs-release/architecture/system-explainer/02-module-dependency.md +257 -0
  11. package/docs-release/architecture/system-explainer/03-task-lifecycle.md +304 -0
  12. package/docs-release/architecture/system-explainer/04-check-and-governance.md +239 -0
  13. package/docs-release/architecture/system-explainer/05-data-flow.md +276 -0
  14. package/docs-release/architecture/system-explainer/06-preset-and-migration.md +303 -0
  15. package/docs-release/architecture/system-explainer/README.md +67 -0
  16. package/docs-release/architecture/system-explainer/en-US/01-system-overview.md +226 -0
  17. package/docs-release/architecture/system-explainer/en-US/02-module-dependency.md +263 -0
  18. package/docs-release/architecture/system-explainer/en-US/03-task-lifecycle.md +319 -0
  19. package/docs-release/architecture/system-explainer/en-US/04-check-and-governance.md +250 -0
  20. package/docs-release/architecture/system-explainer/en-US/05-data-flow.md +290 -0
  21. package/docs-release/architecture/system-explainer/en-US/06-preset-and-migration.md +323 -0
  22. package/docs-release/architecture/system-explainer/en-US/README.md +70 -0
  23. package/docs-release/guides/agent-installation.en-US.md +8 -7
  24. package/docs-release/guides/agent-installation.md +9 -7
  25. package/docs-release/guides/preset-development.md +26 -2
  26. package/docs-release/guides/task-state-machine.en-US.md +30 -13
  27. package/docs-release/guides/task-state-machine.md +30 -13
  28. package/examples/minimal-project/docs/09-PLANNING/TASKS/demo-task/INDEX.md +60 -0
  29. package/package.json +3 -2
  30. package/references/harness-ledger.md +1 -1
  31. package/scripts/commands/migration-command.mjs +30 -0
  32. package/scripts/commands/task-command.mjs +26 -25
  33. package/scripts/harness.mjs +7 -3
  34. package/scripts/lib/capability-registry.mjs +17 -21
  35. package/scripts/lib/check-module-parallel.mjs +9 -16
  36. package/scripts/lib/check-profiles.mjs +35 -81
  37. package/scripts/lib/check-task-contracts.mjs +13 -5
  38. package/scripts/lib/core-shared.mjs +55 -2
  39. package/scripts/lib/dashboard-data.mjs +126 -18
  40. package/scripts/lib/dashboard-workbench.mjs +80 -1
  41. package/scripts/lib/dashboard-writer.mjs +6 -2
  42. package/scripts/lib/git-status-summary.mjs +1 -1
  43. package/scripts/lib/governance-sync.mjs +180 -83
  44. package/scripts/lib/harness-core.mjs +1 -0
  45. package/scripts/lib/markdown-utils.mjs +33 -0
  46. package/scripts/lib/migration-planner.mjs +4 -6
  47. package/scripts/lib/phase-kind.mjs +50 -0
  48. package/scripts/lib/preset-engine.mjs +5 -8
  49. package/scripts/lib/preset-registry.mjs +188 -39
  50. package/scripts/lib/review-confirm-git-gate.mjs +1 -1
  51. package/scripts/lib/status-builder.mjs +88 -0
  52. package/scripts/lib/status-dashboard-renderer.mjs +7 -4
  53. package/scripts/lib/task-audit-metadata.mjs +385 -0
  54. package/scripts/lib/task-audit-migration.mjs +350 -0
  55. package/scripts/lib/task-completion-consistency.mjs +11 -1
  56. package/scripts/lib/task-lifecycle/create-task-helpers.mjs +67 -0
  57. package/scripts/lib/task-lifecycle/phase-sync.mjs +88 -0
  58. package/scripts/lib/task-lifecycle/review-confirm.mjs +40 -29
  59. package/scripts/lib/task-lifecycle/review-gates.mjs +13 -10
  60. package/scripts/lib/task-lifecycle/review-submission.mjs +63 -0
  61. package/scripts/lib/task-lifecycle/scaffold-provenance.mjs +49 -0
  62. package/scripts/lib/task-lifecycle/template-files.mjs +53 -0
  63. package/scripts/lib/task-lifecycle.mjs +114 -147
  64. package/scripts/lib/task-metadata.mjs +118 -0
  65. package/scripts/lib/task-review-model.mjs +54 -68
  66. package/scripts/lib/task-scanner.mjs +70 -143
  67. package/skills/preset-creator/references/complex-task-skeleton/brief.md +11 -0
  68. package/templates/AGENTS.md.template +7 -5
  69. package/templates/dashboard/assets/app-src/00-state.js +12 -0
  70. package/templates/dashboard/assets/app-src/10-router.js +3 -0
  71. package/templates/dashboard/assets/app-src/20-overview.js +7 -3
  72. package/templates/dashboard/assets/app-src/35-task-detail.js +46 -6
  73. package/templates/dashboard/assets/app-src/55-presets.js +375 -0
  74. package/templates/dashboard/assets/app-src/60-shared.js +3 -1
  75. package/templates/dashboard/assets/app-src/90-bindings.js +131 -0
  76. package/templates/dashboard/assets/app.css +583 -0
  77. package/templates/dashboard/assets/app.css.manifest.json +1 -0
  78. package/templates/dashboard/assets/app.js +578 -10
  79. package/templates/dashboard/assets/app.manifest.json +1 -0
  80. package/templates/dashboard/assets/css-src/00-foundation.css +4 -0
  81. package/templates/dashboard/assets/css-src/40-detail-modules-migration.css +62 -0
  82. package/templates/dashboard/assets/css-src/45-presets.css +516 -0
  83. package/templates/dashboard/assets/i18n.js +140 -2
  84. package/templates/planning/INDEX.md +87 -0
  85. package/templates/planning/brief.md +1 -1
  86. package/templates/planning/module_session_prompt.md +1 -0
  87. package/templates/planning/review.md +0 -18
  88. package/templates/planning/task_plan.md +4 -43
  89. package/templates/planning/visual_map.md +13 -9
  90. package/templates/planning/visual_map.simple.md +52 -0
  91. package/templates/reference/execution-workflow-standard.md +29 -2
  92. package/templates-zh-CN/AGENTS.md.template +7 -5
  93. package/templates-zh-CN/planning/INDEX.md +87 -0
  94. package/templates-zh-CN/planning/brief.md +1 -1
  95. package/templates-zh-CN/planning/module_session_prompt.md +1 -0
  96. package/templates-zh-CN/planning/review.md +0 -18
  97. package/templates-zh-CN/planning/task_plan.md +3 -63
  98. package/templates-zh-CN/planning/visual_map.md +14 -7
  99. package/templates-zh-CN/planning/visual_map.simple.md +48 -0
  100. 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.filter((phase) => phase.state !== "skipped");
41
- const hasRecordedPhaseProgress = actionablePhases.some(
42
- (phase) =>
43
- phase.completion > 0 ||
44
- ["in_progress", "review", "blocked", "done"].includes(phase.state) ||
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
- getColumn,
47
- firstColumn,
48
- updateMarkdownTableRow,
49
- } from "./markdown-utils.mjs";
47
+ renderAgentReviewSubmission,
48
+ replaceAgentReviewSubmission,
49
+ } from "./task-lifecycle/review-submission.mjs";
50
50
  import {
51
- validateLifecycleTransition,
52
- validateReviewEntryGate,
53
- } from "./task-lifecycle/review-gates.mjs";
54
- import { confirmTaskReview as confirmTaskReviewWithContext } from "./task-lifecycle/review-confirm.mjs";
55
- import { appendProgressLog, markdownCell } from "./task-lifecycle/text-utils.mjs";
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
- export function createTask(targetInput, taskId, { title = "", locale = "en-US", dryRun = false, moduleKey = "", budget = "standard", longRunning = false, preset = "", fromSession = "", presetArgs = [] } = {}) {
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);