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
@@ -1,15 +1,13 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import {
4
- allowedTaskStates,
5
- allowedTaskBudgets,
6
4
  visualMapFile,
7
5
  legacyVisualRoadmapFile,
8
6
  lessonCandidatesFile,
9
7
  longRunningTaskContractFile,
10
- taskContractMarker,
11
8
  toPosix,
12
9
  readFileSafe,
10
+ readJsonSafe,
13
11
  walkFiles,
14
12
  titleFromMarkdown,
15
13
  } from "./core-shared.mjs";
@@ -19,6 +17,23 @@ import {
19
17
  splitList,
20
18
  splitDependencies,
21
19
  } from "./markdown-utils.mjs";
20
+ import {
21
+ normalizePhaseActor,
22
+ normalizePhaseKind,
23
+ phaseCompletionAverage,
24
+ } from "./phase-kind.mjs";
25
+ import {
26
+ legacyAuditIssues,
27
+ parseTaskAuditMetadata,
28
+ scaffoldProvenanceSummaryFromTaskAudit,
29
+ taskAuditMaterialIssues,
30
+ } from "./task-audit-metadata.mjs";
31
+ import {
32
+ parseTaskBudget,
33
+ parseTaskContractInfo,
34
+ parseTaskMetadata,
35
+ parseTaskStateInfo,
36
+ } from "./task-metadata.mjs";
22
37
  import {
23
38
  isLessonCandidateDecisionComplete,
24
39
  parseLessonCandidateStatus,
@@ -40,6 +55,13 @@ import {
40
55
  taskReviewStatus,
41
56
  taskScannerVersion,
42
57
  } from "./task-review-model.mjs";
58
+ export {
59
+ parseTaskBudget,
60
+ parseTaskContractInfo,
61
+ parseTaskMetadata,
62
+ parseTaskState,
63
+ parseTaskStateInfo,
64
+ } from "./task-metadata.mjs";
43
65
  export {
44
66
  collectReviewRisks,
45
67
  deriveLifecycleState,
@@ -53,6 +75,9 @@ export {
53
75
  taskReviewStatus,
54
76
  taskScannerVersion,
55
77
  } from "./task-review-model.mjs";
78
+ export {
79
+ parseTaskAuditMetadata,
80
+ } from "./task-audit-metadata.mjs";
56
81
  export {
57
82
  allowedLessonCandidateRowStatuses,
58
83
  allowedLessonCandidateTaskStatuses,
@@ -61,139 +86,33 @@ export {
61
86
  reviewCompleteLessonCandidateStatuses,
62
87
  } from "./task-lesson-candidates.mjs";
63
88
 
64
- export function parseTaskState(progressContent) {
65
- return parseTaskStateInfo(progressContent).state;
66
- }
67
-
68
- export function parseTaskBudget(taskPlanContent) {
69
- const match =
70
- String(taskPlanContent || "").match(/^Selected budget\s*[::]\s*([^\n]+)/im) ||
71
- String(taskPlanContent || "").match(/^选择预算\s*[::]\s*([^\n]+)/im);
72
- if (!match) return "standard";
73
- const raw = match[1].replace(/`/g, "").trim().toLowerCase();
74
- const normalized = raw.replaceAll("_", "-").replace(/\s+/g, "-");
75
- if (allowedTaskBudgets.has(normalized)) return normalized;
76
- if (["long-running", "longrunning", "module-parallel"].includes(normalized)) return "complex";
77
- return "standard";
78
- }
79
-
80
- function parseMetadataLine(content, labels) {
81
- const escaped = labels.map((label) => label.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|");
82
- const match = String(content || "").match(new RegExp(`^(?:${escaped})\\s*[::]\\s*([^\\n]+)`, "im"));
83
- return match ? match[1].replace(/`/g, "").trim() : "";
84
- }
85
-
86
- function normalizeMetadataValue(value, fallback = "") {
87
- const normalized = String(value || "")
88
- .replace(/`/g, "")
89
- .trim()
90
- .toLowerCase()
91
- .replaceAll("_", "-")
92
- .replace(/\s+/g, "-");
93
- return normalized || fallback;
94
- }
95
-
96
- export function parseTaskMetadata(taskPlanContent) {
97
- const content = String(taskPlanContent || "");
98
- const kind = normalizeMetadataValue(parseMetadataLine(content, ["Task Kind", "任务类型"]), "general");
99
- const preset = normalizeMetadataValue(parseMetadataLine(content, ["Task Preset", "Preset", "任务预设"]), "none");
100
- const presetVersion = parseMetadataLine(content, ["Preset Version", "预设版本"]);
101
- const migrationTargetLevel = normalizeMetadataValue(
102
- parseMetadataLine(content, ["Migration Target Level", "Target Level", "迁移目标等级", "目标等级"]),
103
- "",
104
- );
105
- const migrationAchievedLevel = normalizeMetadataValue(
106
- parseMetadataLine(content, ["Migration Achieved Level", "Achieved Level", "迁移实际完成等级", "实际完成等级"]),
107
- "",
108
- );
109
- const evidenceBundle = parseMetadataLine(content, ["Evidence Bundle", "证据包"]);
110
- return {
111
- kind,
112
- preset,
113
- presetVersion,
114
- migrationTargetLevel,
115
- migrationAchievedLevel,
116
- evidenceBundle,
117
- };
118
- }
119
-
120
- export function parseTaskContractInfo(taskPlanContent) {
121
- const content = String(taskPlanContent || "");
122
- const explicit =
123
- content.match(/^Task Contract\s*[::]\s*`?([^`\n]+)`?\s*$/im) ||
124
- content.match(/^任务合同\s*[::]\s*`?([^`\n]+)`?\s*$/im);
125
- const version = explicit ? explicit[1].trim() : "";
126
- return {
127
- version,
128
- generated: version === "harness-task/v1" || content.includes(taskContractMarker),
129
- };
130
- }
131
-
132
- export function parseTaskStateInfo(progressContent) {
133
- const match = progressContent.match(/^##\s*(?:Current Status|Status|状态)\s*[::]?\s*(?:\n\s*)?([^\n]+)/im);
134
- if (!match) return inferLegacyTaskState(progressContent);
135
- const raw = match[1].replace(/`/g, "").trim();
136
- if (!raw || raw.includes("|") || /^[-*]\s+/.test(raw)) return inferLegacyTaskState(progressContent);
137
- const aliases = new Map([
138
- ["进行中", "in_progress"],
139
- ["已完成", "done"],
140
- ["未开始", "not_started"],
141
- ["计划中", "planned"],
142
- ["审查中", "review"],
143
- ["已阻塞", "blocked"],
144
- ["pending", "planned"],
145
- ]);
146
- const normalized = aliases.get(raw) || raw.toLowerCase().replaceAll("-", "_").replaceAll(" ", "_");
147
- return allowedTaskStates.has(normalized)
148
- ? { state: normalized, source: "explicit", raw }
149
- : { state: "unknown", source: "invalid", raw };
150
- }
151
-
152
- function inferLegacyTaskState(progressContent) {
153
- const { header, rows } = tableAfterHeading(progressContent, /^(Status|状态)$/i);
154
- const statusIndex = firstColumn(header, ["Status", "状态"]);
155
- if (statusIndex < 0 || rows.length === 0) return { state: "unknown", source: "missing", raw: "" };
156
- const states = rows.map((row) => normalizeLegacyState(row[statusIndex])).filter(Boolean);
157
- if (states.includes("blocked")) return { state: "blocked", source: "legacy-table", raw: "blocked" };
158
- if (states.includes("in_progress")) return { state: "in_progress", source: "legacy-table", raw: "in_progress" };
159
- if (states.includes("review")) return { state: "review", source: "legacy-table", raw: "review" };
160
- if (states.length > 0 && states.every((state) => state === "done")) return { state: "done", source: "legacy-table", raw: "done" };
161
- if (states.some((state) => ["planned", "not_started"].includes(state))) return { state: "planned", source: "legacy-table", raw: "planned" };
162
- return { state: "unknown", source: "missing", raw: "" };
163
- }
164
-
165
- function normalizeLegacyState(value) {
166
- const raw = String(value || "").replace(/`/g, "").trim().toLowerCase();
167
- if (!raw || /^(none|n\/a|na|-|—|–|无)$/.test(raw)) return "";
168
- if (/block|阻塞|blocked/.test(raw)) return "blocked";
169
- if (/in[-_\s]?progress|doing|active|进行中|当前|working/.test(raw)) return "in_progress";
170
- if (/review|审查|审核|验证中/.test(raw)) return "review";
171
- if (/done|complete|completed|merged|closed|完成|已完成/.test(raw)) return "done";
172
- if (/pending|planned|todo|not[-_\s]?started|未开始|计划/.test(raw)) return "planned";
173
- return "";
174
- }
175
-
176
89
  export function parsePhases(taskPlanContent) {
177
90
  const { header, rows } = tableAfterHeading(taskPlanContent, /^Phase ID$/i);
178
91
  if (rows.length === 0) return [];
179
92
  const indexes = {
180
93
  id: firstColumn(header, ["Phase ID", "阶段 ID"]),
94
+ kind: firstColumn(header, ["Kind", "阶段类型", "类型"]),
181
95
  dependsOn: firstColumn(header, ["Depends On", "依赖"]),
182
96
  state: firstColumn(header, ["State", "状态"]),
183
97
  completion: firstColumn(header, ["Completion", "完成度"]),
184
98
  output: firstColumn(header, ["Output", "产出"]),
185
99
  requiredEvidence: firstColumn(header, ["Required Evidence", "必要证据"]),
100
+ exitCommand: firstColumn(header, ["Exit Command", "出口命令", "退出命令"]),
101
+ actor: firstColumn(header, ["Actor", "执行者", "角色"]),
186
102
  evidenceStatus: firstColumn(header, ["Evidence Status", "证据状态"]),
187
103
  blockingRisk: firstColumn(header, ["Blocking Risk", "阻塞风险"]),
188
104
  owner: firstColumn(header, ["Owner / Handoff", "负责人 / 交接"]),
189
105
  };
190
106
  return rows.map((row) => ({
191
107
  id: row[indexes.id] || "",
108
+ kind: normalizePhaseKind(row[indexes.kind]),
192
109
  dependsOn: splitDependencies(row[indexes.dependsOn] || ""),
193
110
  state: row[indexes.state] || "planned",
194
111
  completion: Number.parseInt(String(row[indexes.completion] || "0").replace("%", ""), 10) || 0,
195
112
  output: row[indexes.output] || "",
196
113
  requiredEvidence: splitList(row[indexes.requiredEvidence] || ""),
114
+ exitCommand: row[indexes.exitCommand] || "",
115
+ actor: normalizePhaseActor(row[indexes.actor]),
197
116
  evidenceStatus: row[indexes.evidenceStatus] || "missing",
198
117
  blockingRisk: row[indexes.blockingRisk] || "",
199
118
  owner: row[indexes.owner] || "",
@@ -310,12 +229,15 @@ export function taskCutoverCounters(tasks) {
310
229
  };
311
230
  }
312
231
 
313
- export function collectTasks(target) {
314
- return listTaskPlanPaths(target).map((taskPlanPath) => {
232
+ export function collectTasks(target, { requireGeneratedScaffoldProvenance = false, taskPlanPaths, closeoutContent } = {}) {
233
+ const paths = taskPlanPaths || listTaskPlanPaths(target);
234
+ const closeout = closeoutContent ?? readFileSafe(path.join(target.docsRoot, "10-WALKTHROUGH/Closeout-SSoT.md"));
235
+ return paths.map((taskPlanPath) => {
315
236
  const taskDir = path.dirname(taskPlanPath);
316
237
  const taskPlan = readFileSafe(taskPlanPath);
317
238
  const brief = readTaskContractFile(taskDir, "brief.md", "");
318
239
  const executionStrategyPath = path.join(taskDir, "execution_strategy.md");
240
+ const indexPath = path.join(taskDir, "INDEX.md");
319
241
  const progressPath = path.join(taskDir, "progress.md");
320
242
  const reviewPath = path.join(taskDir, "review.md");
321
243
  const findingsPath = path.join(taskDir, "findings.md");
@@ -324,19 +246,14 @@ export function collectTasks(target) {
324
246
  const visualMap = readVisualMapContractFile(taskDir, taskPlan);
325
247
  const progress = readFileSafe(progressPath);
326
248
  const review = readFileSafe(reviewPath);
249
+ const indexContent = readFileSafe(indexPath);
327
250
  const parsedLessonCandidates = parseLessonCandidateStatus(readFileSafe(lessonCandidatesPath));
328
251
  const lessonDetailIssues = validateLessonCandidateDetailArtifacts(target, taskDir, parsedLessonCandidates);
329
252
  const lessonCandidates = lessonDetailIssues.length
330
253
  ? { ...parsedLessonCandidates, issues: [...parsedLessonCandidates.issues, ...lessonDetailIssues] }
331
254
  : parsedLessonCandidates;
332
255
  const phases = parsePhases(visualMap.content);
333
- const completion =
334
- phases.length > 0
335
- ? Math.round(
336
- phases.filter((phase) => phase.state !== "skipped").reduce((sum, phase) => sum + phase.completion, 0) /
337
- Math.max(1, phases.filter((phase) => phase.state !== "skipped").length),
338
- )
339
- : 0;
256
+ const completion = phaseCompletionAverage(phases);
340
257
  const relative = toPosix(path.relative(target.projectRoot, taskDir));
341
258
  const id = taskIdForDirectory(target, taskDir);
342
259
  const identity = parseTaskIdentity(taskPlan, id);
@@ -346,6 +263,10 @@ export function collectTasks(target) {
346
263
  const budget = parseTaskBudget(taskPlan);
347
264
  const metadata = parseTaskMetadata(taskPlan);
348
265
  const taskContract = parseTaskContractInfo(taskPlan);
266
+ const taskAudit = parseTaskAuditMetadata(indexContent, {
267
+ required: requireGeneratedScaffoldProvenance && taskContract.generated,
268
+ });
269
+ const scaffoldProvenance = { summary: scaffoldProvenanceSummaryFromTaskAudit(taskAudit) };
349
270
  const explicitModule = id.startsWith("MODULES/") ? id.split("/")[1] : null;
350
271
  const legacyCandidate = brief.source !== "standalone" || visualMap.status === "legacy-only" || !fs.existsSync(executionStrategyPath);
351
272
  const classification = inferTaskClassification({ id, title, relative, explicitModule, legacyCandidate });
@@ -355,14 +276,19 @@ export function collectTasks(target) {
355
276
  const reviewSubmission = parseAgentReviewSubmission(review, { taskKey: identity.taskKey });
356
277
  const reviewConfirmation = parseReviewConfirmation(review, {
357
278
  taskKey: identity.taskKey,
279
+ taskAudit,
358
280
  projectRoot: target.projectRoot,
359
281
  taskDir,
282
+ indexPath,
360
283
  reviewPath,
361
284
  progressPath,
362
285
  });
363
286
  const reviewStatus = taskReviewStatus({ reviewContent: review, risks, confirmation: reviewConfirmation, submission: reviewSubmission });
364
- const closeoutInfo = taskCloseoutInfo(target, taskPlanPath);
365
- const lifecycleState = deriveLifecycleState({ state: stateInfo.state, reviewStatus, closeoutStatus: closeoutInfo.status });
287
+ const closeoutInfo = taskCloseoutInfo(target, taskPlanPath, closeout);
288
+ const effectiveCloseoutStatus = budget === "simple" && stateInfo.state === "done" && completion === 100
289
+ ? "closed"
290
+ : closeoutInfo.status;
291
+ const lifecycleState = deriveLifecycleState({ state: stateInfo.state, reviewStatus, closeoutStatus: effectiveCloseoutStatus, budget });
366
292
  const materialReadiness = assessMaterialsReadiness({
367
293
  budget,
368
294
  taskDir,
@@ -376,19 +302,24 @@ export function collectTasks(target) {
376
302
  reviewSurfaceRequired: requiresReviewMaterials({
377
303
  state: stateInfo.state,
378
304
  lifecycleState,
379
- closeoutStatus: closeoutInfo.status,
305
+ closeoutStatus: effectiveCloseoutStatus,
380
306
  }),
381
307
  });
382
- const stateConflicts = collectStateConflicts({ state: stateInfo.state, reviewStatus, closeoutStatus: closeoutInfo.status, lifecycleState });
308
+ const materialIssues = [
309
+ ...materialReadiness.issues,
310
+ ...taskAuditMaterialIssues(target, taskDir, taskAudit),
311
+ ...legacyAuditIssues(target, taskDir, { briefContent: brief.content, reviewContent: review }),
312
+ ];
313
+ const stateConflicts = collectStateConflicts({ state: stateInfo.state, reviewStatus, closeoutStatus: effectiveCloseoutStatus, lifecycleState, budget });
383
314
  const reviewQueueState = deriveReviewQueueState({
384
315
  state: stateInfo.state,
385
316
  lifecycleState,
386
317
  reviewStatus,
387
- closeoutStatus: closeoutInfo.status,
318
+ closeoutStatus: effectiveCloseoutStatus,
388
319
  budget,
389
320
  walkthroughPath: closeoutInfo.walkthroughPath,
390
321
  lessonCandidateDecisionComplete: isLessonCandidateDecisionComplete(lessonCandidates),
391
- materialsReady: materialReadiness.ready,
322
+ materialsReady: materialIssues.length === 0,
392
323
  deletionState: tombstone.deletionState,
393
324
  });
394
325
  const queueModel = deriveTaskQueues({
@@ -400,11 +331,11 @@ export function collectTasks(target) {
400
331
  reviewSubmission,
401
332
  reviewConfirmation,
402
333
  reviewQueueState,
403
- materialIssues: materialReadiness.issues,
334
+ materialIssues,
404
335
  risks,
405
336
  stateConflicts,
406
337
  lessonCandidates,
407
- closeoutStatus: closeoutInfo.status,
338
+ closeoutStatus: effectiveCloseoutStatus,
408
339
  tombstone,
409
340
  taskDir,
410
341
  target,
@@ -450,18 +381,20 @@ export function collectTasks(target) {
450
381
  migrationAchievedLevel: metadata.migrationAchievedLevel,
451
382
  evidenceBundle: formatEvidenceBundle(metadata.evidenceBundle),
452
383
  migrationSnapshot: collectMigrationSnapshot(target, metadata),
384
+ scaffoldProvenance: scaffoldProvenance.summary,
385
+ taskAudit: taskAudit.summary,
453
386
  lifecycleState,
454
387
  reviewStatus,
455
388
  reviewSubmitted: Boolean(reviewSubmission?.submitted),
456
389
  reviewSubmission,
457
390
  reviewQueueState,
458
391
  reviewConfirmation,
459
- materialsReady: materialReadiness.ready,
460
- materialIssues: materialReadiness.issues,
392
+ materialsReady: materialIssues.length === 0,
393
+ materialIssues,
461
394
  taskQueues: queueModel.taskQueues,
462
395
  queueReasons: queueModel.queueReasons,
463
396
  repairPrompt: queueModel.repairPrompt,
464
- closeoutStatus: closeoutInfo.status,
397
+ closeoutStatus: effectiveCloseoutStatus,
465
398
  walkthroughPath: closeoutInfo.walkthroughPath ? `TARGET:${closeoutInfo.walkthroughPath}` : "",
466
399
  lessonCandidatePath: fs.existsSync(lessonCandidatesPath)
467
400
  ? `TARGET:${toPosix(path.relative(target.projectRoot, lessonCandidatesPath))}`
@@ -505,12 +438,7 @@ function collectMigrationSnapshot(target, metadata) {
505
438
  const evidenceBundle = String(metadata.evidenceBundle || "").replace(/^TARGET:/, "").replace(/^\/+/, "");
506
439
  const bundlePath = evidenceBundle ? path.join(target.projectRoot, evidenceBundle) : "";
507
440
  const sessionPath = bundlePath ? path.join(bundlePath, "session.json") : "";
508
- let session = null;
509
- try {
510
- session = sessionPath && fs.existsSync(sessionPath) ? JSON.parse(fs.readFileSync(sessionPath, "utf8")) : null;
511
- } catch {
512
- session = null;
513
- }
441
+ const session = sessionPath && fs.existsSync(sessionPath) ? readJsonSafe(sessionPath, null) : null;
514
442
  const summary = session?.plan?.summary || {};
515
443
  return {
516
444
  targetLevel: metadata.migrationTargetLevel || "",
@@ -536,8 +464,7 @@ function formatEvidenceBundle(value) {
536
464
  return normalized ? `TARGET:${normalized}` : "";
537
465
  }
538
466
 
539
- function taskCloseoutInfo(target, taskPlanPath) {
540
- const closeout = readFileSafe(path.join(target.docsRoot, "10-WALKTHROUGH/Closeout-SSoT.md"));
467
+ function taskCloseoutInfo(target, taskPlanPath, closeout) {
541
468
  if (!closeout.trim()) return { status: "missing", walkthroughPath: "" };
542
469
  const docsRelative = `docs/${toPosix(path.relative(target.docsRoot, taskPlanPath))}`;
543
470
  const projectRelative = toPosix(path.relative(target.projectRoot, taskPlanPath));
@@ -30,3 +30,14 @@ State the user-visible or project-visible result this task must deliver.
30
30
  ## First Next Step
31
31
 
32
32
  Replace this line with the first concrete action before implementation starts.
33
+
34
+ ## Scaffold Provenance
35
+
36
+ | Field | Value |
37
+ | --- | --- |
38
+ | Created By | {{SCAFFOLD_CREATED_BY}} |
39
+ | Command Shape | {{SCAFFOLD_COMMAND}} |
40
+ | Created At | {{SCAFFOLD_CREATED_AT}} |
41
+ | Budget | {{SCAFFOLD_BUDGET}} |
42
+ | Template Source | {{SCAFFOLD_TEMPLATE_SOURCE}} |
43
+ | Exception Reason | {{SCAFFOLD_EXCEPTION_REASON}} |
@@ -16,10 +16,11 @@ Keep it as a charter and routing index. Detailed rules belong in
16
16
 
17
17
  1. Preserve architecture boundaries documented in `docs/11-REFERENCE/engineering-standard.md`.
18
18
  2. Do not commit secrets, credentials, private endpoints, or user data.
19
- 3. Start non-trivial work with a task directory under `docs/09-PLANNING/TASKS/`.
19
+ 3. Start non-trivial work with a Harness CLI-created task directory under `docs/09-PLANNING/TASKS/`.
20
20
  4. Record evidence before claiming completion.
21
21
  5. Protect unrelated working-tree changes; never revert files outside the assigned scope.
22
22
  6. Commit verified, meaningful work slices proactively. Do not leave completed work only as dirty files unless the user explicitly pauses commits, checks fail, dirty ownership is unclear, or a security boundary prevents a clean commit; in those cases record the no-commit reason, owner, and next step in progress, handoff, or closeout notes. Never mix unrelated dirty changes into a task commit.
23
+ 7. Before the final response, inspect the current task `visual_map.md` gate phase. If the current `Exit Command` has `Actor: agent`, run it or record the blocker. Never run a `human` gate such as `review-confirm` unless the user has explicitly performed or delegated that human confirmation.
23
24
 
24
25
  ## Reading Matrix
25
26
 
@@ -33,7 +34,7 @@ Read the smallest context that can answer the task.
33
34
  | API, event, webhook, SDK, or third-party contract | `docs/06-INTEGRATIONS/README.md` and the related contract file |
34
35
  | Tests, smoke, or regression | `docs/11-REFERENCE/testing-standard.md`, `docs/05-TEST-QA/Regression-SSoT.md` |
35
36
  | Execution, commit, PR, or release work | `docs/11-REFERENCE/execution-workflow-standard.md`, `docs/11-REFERENCE/repo-governance-standard.md`, `docs/11-REFERENCE/pull-request-standard.md`, `docs/11-REFERENCE/ci-cd-standard.md` |
36
- | Creating or advancing a task | Current task directory under `docs/09-PLANNING/TASKS/`; use the Harness CLI if this project has it configured |
37
+ | Creating or advancing a task | Current task directory under `docs/09-PLANNING/TASKS/`; use `harness new-task` / lifecycle commands when this project has the Harness CLI configured |
37
38
  | Brief, Execution Strategy, or Visual Map | Current task `brief.md`, `execution_strategy.md`, and `visual_map.md` |
38
39
  | Long-running task | `docs/11-REFERENCE/long-running-task-standard.md` |
39
40
  | Reviewer, subagent, or adversarial review | `docs/11-REFERENCE/review-routing-standard.md`, `docs/11-REFERENCE/adversarial-review-standard.md` |
@@ -48,7 +49,7 @@ Read the smallest context that can answer the task.
48
49
  ## Standard Execution Flow
49
50
 
50
51
  1. Confirm the request, scope, and affected files.
51
- 2. For non-trivial work, create or update the task plan before editing code.
52
+ 2. For non-trivial work, create or update the task plan before editing code. Use `harness new-task`; if the CLI is unavailable, keep Task Audit Metadata in the task `INDEX.md` and set `Created By` to `manual-exception` with a concrete reason.
52
53
  3. Assess delegation from the user's goal; users do not need to know or ask for subagents.
53
54
  4. Load only the reference documents required by the Reading Matrix.
54
55
  5. Preserve existing project facts; merge new context instead of overwriting history.
@@ -56,8 +57,9 @@ Read the smallest context that can answer the task.
56
57
  7. Implement only within the approved scope.
57
58
  8. Run the relevant checks and capture evidence in the task records.
58
59
  9. Route required review and close blocking findings.
59
- 10. Write or update the walkthrough and closeout records.
60
- 11. Update SSoT / ledger files that the task actually touched.
60
+ 10. Follow the current `visual_map.md` lifecycle gate and its `Exit Command` / `Actor`.
61
+ 11. Write or update the walkthrough and closeout records.
62
+ 12. Update SSoT / ledger files that the task actually touched.
61
63
 
62
64
  ## Coordination Rules
63
65
 
@@ -12,6 +12,18 @@ const state = {
12
12
  taskGroupPage: 1,
13
13
  warningFilter: "all",
14
14
  warningPage: 1,
15
+ presetQuery: "",
16
+ presetSourceFilter: "all",
17
+ selectedPresetKey: "",
18
+ selectedPresetId: "",
19
+ presetActionResult: null,
20
+ presetInstallSource: "",
21
+ presetInstallScope: "project",
22
+ presetInstallForce: false,
23
+ presetSeedScope: "project",
24
+ presetSeedForce: false,
25
+ presetUninstallScope: "project",
26
+ presetUninstallConfirm: "",
15
27
  renderMode: "rendered",
16
28
  theme: localStorage.getItem("harness.theme") || "system",
17
29
  taskLayout: localStorage.getItem("harness.taskLayout") || "list",
@@ -29,6 +29,7 @@ function shell() {
29
29
  ${routeLink("#/tasks", t("taskIndex"), "tasks")}
30
30
  ${routeLink("#/review", t("reviewQueue"), "review")}
31
31
  ${routeLink("#/modules", t("moduleView"), "modules")}
32
+ ${routeLink("#/presets", t("presetCatalog"), "presets")}
32
33
  <button data-language-toggle>${locale === "zh" ? "EN" : "中文"}</button>
33
34
  <button data-theme-toggle>${themeLabel()}</button>
34
35
  </div>
@@ -55,6 +56,7 @@ function renderRoute() {
55
56
  if (route.name === "reviewTask") return reviewWorkspace(route);
56
57
  if (route.name === "review") return reviewQueue();
57
58
  if (route.name === "modules") return modulesView(route.id);
59
+ if (route.name === "presets") return presetsView();
58
60
  if (route.name === "tasks") return taskIndex();
59
61
  return overview();
60
62
  }
@@ -66,6 +68,7 @@ function currentRoute() {
66
68
  if (parts[0] === "review" && parts[1]) return { name: "reviewTask", id: parts[1] };
67
69
  if (parts[0] === "review") return { name: "review" };
68
70
  if (parts[0] === "modules") return { name: "modules", id: parts[1] || "" };
71
+ if (parts[0] === "presets") return { name: "presets" };
69
72
  if (parts[0] === "tasks") return { name: "tasks" };
70
73
  return { name: "overview" };
71
74
  }
@@ -16,6 +16,9 @@ function overview() {
16
16
 
17
17
  function statusStrip() {
18
18
  const status = bundle.status?.checkState?.status || "unknown";
19
+ const validationMode = bundle.status?.checkState?.validationMode || "validated";
20
+ const dataOnly = validationMode === "data-only";
21
+ const displayState = dataOnly ? "snapshot" : status;
19
22
  const failures = bundle.status?.checkState?.failures || 0;
20
23
  const warnings = bundle.status?.checkState?.warnings || 0;
21
24
  const tasks = bundle.status?.tasks || [];
@@ -23,9 +26,9 @@ function statusStrip() {
23
26
  const visual = summary.visualMapCoverage || {};
24
27
  const withBrief = tasks.filter((task) => task.briefSource === "standalone").length;
25
28
  return `<section class="status-card-group">
26
- <div class="status-primary ${status}">
27
- <span>${t("readiness")}</span>
28
- <strong>${label(status)}</strong>
29
+ <div class="status-primary ${displayState}">
30
+ <span>${dataOnly ? t("snapshotStatus") : t("readiness")}</span>
31
+ <strong>${dataOnly ? t("snapshot") : label(status)}</strong>
29
32
  <p>${nextActionText()}</p>
30
33
  </div>
31
34
  <div class="metrics-grid">
@@ -46,6 +49,7 @@ function metric(labelText, value) {
46
49
  }
47
50
 
48
51
  function nextActionText() {
52
+ if ((bundle.status?.checkState?.validationMode || "validated") === "data-only") return t("snapshotNotValidated");
49
53
  const failures = bundle.status?.checkState?.failures || 0;
50
54
  if (failures > 0) return t("resolveBlockers");
51
55
  const missingBriefs = (bundle.status?.tasks || []).filter((task) => task.briefSource !== "standalone").length;
@@ -72,17 +72,57 @@ function taskQueueReasonSummary(task) {
72
72
  }
73
73
 
74
74
  function phaseTimeline(task) {
75
+ const knownKinds = new Set(["init", "execution", "gate"]);
76
+ const groups = [
77
+ ["init", "Init"],
78
+ ["execution", "Execution"],
79
+ ["gate", "Gate"],
80
+ ["other", "Other / Invalid"],
81
+ ];
82
+ const phases = task.phases || [];
83
+ const grouped = groups
84
+ .map(([kind, label]) => {
85
+ const items = kind === "other"
86
+ ? phases.filter((phase) => !knownKinds.has(phase.kind || "execution"))
87
+ : phases.filter((phase) => (phase.kind || "execution") === kind);
88
+ if (!items.length) return "";
89
+ return `<div class="phase-kind-group ${escapeAttr(kind)}">
90
+ <h3>${escapeHtml(label)}</h3>
91
+ ${items.map(phaseStep).join("")}
92
+ </div>`;
93
+ })
94
+ .join("");
75
95
  return `<section class="phase-timeline">
76
96
  <h2>${t("phaseTimeline")}</h2>
77
- ${(task.phases || []).map((phase) => `<div class="phase-step ${phase.state}">
78
- <strong>${escapeHtml(phase.id)}</strong>
79
- <span>${phase.completion}%</span>
80
- <p>${escapeHtml(phase.output || phase.blockingRisk || phase.state)}</p>
81
- ${progressBar(phase.completion)}
82
- </div>`).join("") || emptyState(t("noPhaseData"))}
97
+ ${grouped || emptyState(t("noPhaseData"))}
83
98
  </section>`;
84
99
  }
85
100
 
101
+ function phaseStep(phase) {
102
+ const kind = phase.kind || "execution";
103
+ const actor = phase.actor || "agent";
104
+ const knownKind = ["init", "execution", "gate"].includes(kind);
105
+ const kindLabel = knownKind ? escapeHtml(kind) : `<span class="tag warn">${escapeHtml(kind)}</span>`;
106
+ const phaseKindClass = knownKind ? kind : "other";
107
+ return `<div class="phase-step ${escapeAttr(phase.state)} ${escapeAttr(phaseKindClass)}">
108
+ <div class="phase-step-head">
109
+ <strong>${escapeHtml(phase.id)}</strong>
110
+ <span>${kindLabel} · ${phase.completion}%</span>
111
+ </div>
112
+ <p>${escapeHtml(phase.output || phase.blockingRisk || phase.state)}</p>
113
+ ${progressBar(phase.completion)}
114
+ <div class="phase-meta">
115
+ ${phaseMetaTag(actor)}
116
+ ${tag(phase.evidenceStatus || "missing")}
117
+ </div>
118
+ ${phase.exitCommand ? `<code class="phase-exit-command">${escapeHtml(phase.exitCommand)}</code>` : ""}
119
+ </div>`;
120
+ }
121
+
122
+ function phaseMetaTag(value) {
123
+ return `<span class="tag">${escapeHtml(String(value || "unknown").replaceAll("_", " "))}</span>`;
124
+ }
125
+
86
126
  function taskDocSection(task, fileName, title, required) {
87
127
  const doc = taskDocument(task, fileName);
88
128
  if (!doc && !required) return "";