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
@@ -0,0 +1,118 @@
1
+ import {
2
+ allowedTaskStates,
3
+ allowedTaskBudgets,
4
+ taskContractMarker,
5
+ } from "./core-shared.mjs";
6
+ import { tableAfterHeading, firstColumn } from "./markdown-utils.mjs";
7
+
8
+ export function parseTaskState(progressContent) {
9
+ return parseTaskStateInfo(progressContent).state;
10
+ }
11
+
12
+ export function parseTaskBudget(taskPlanContent) {
13
+ const match =
14
+ String(taskPlanContent || "").match(/^Selected budget\s*[::]\s*([^\n]+)/im) ||
15
+ String(taskPlanContent || "").match(/^选择预算\s*[::]\s*([^\n]+)/im);
16
+ if (!match) return "standard";
17
+ const raw = match[1].replace(/`/g, "").trim().toLowerCase();
18
+ const normalized = raw.replaceAll("_", "-").replace(/\s+/g, "-");
19
+ if (allowedTaskBudgets.has(normalized)) return normalized;
20
+ if (["long-running", "longrunning", "module-parallel"].includes(normalized)) return "complex";
21
+ return "standard";
22
+ }
23
+
24
+ function parseMetadataLine(content, labels) {
25
+ const escaped = labels.map((label) => label.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|");
26
+ const match = String(content || "").match(new RegExp(`^(?:${escaped})\\s*[::]\\s*([^\\n]+)`, "im"));
27
+ return match ? match[1].replace(/`/g, "").trim() : "";
28
+ }
29
+
30
+ function normalizeMetadataValue(value, fallback = "") {
31
+ const normalized = String(value || "")
32
+ .replace(/`/g, "")
33
+ .trim()
34
+ .toLowerCase()
35
+ .replaceAll("_", "-")
36
+ .replace(/\s+/g, "-");
37
+ return normalized || fallback;
38
+ }
39
+
40
+ export function parseTaskMetadata(taskPlanContent) {
41
+ const content = String(taskPlanContent || "");
42
+ const kind = normalizeMetadataValue(parseMetadataLine(content, ["Task Kind", "任务类型"]), "general");
43
+ const preset = normalizeMetadataValue(parseMetadataLine(content, ["Task Preset", "Preset", "任务预设"]), "none");
44
+ const presetVersion = parseMetadataLine(content, ["Preset Version", "预设版本"]);
45
+ const migrationTargetLevel = normalizeMetadataValue(
46
+ parseMetadataLine(content, ["Migration Target Level", "Target Level", "迁移目标等级", "目标等级"]),
47
+ "",
48
+ );
49
+ const migrationAchievedLevel = normalizeMetadataValue(
50
+ parseMetadataLine(content, ["Migration Achieved Level", "Achieved Level", "迁移实际完成等级", "实际完成等级"]),
51
+ "",
52
+ );
53
+ const evidenceBundle = parseMetadataLine(content, ["Evidence Bundle", "证据包"]);
54
+ return {
55
+ kind,
56
+ preset,
57
+ presetVersion,
58
+ migrationTargetLevel,
59
+ migrationAchievedLevel,
60
+ evidenceBundle,
61
+ };
62
+ }
63
+
64
+ export function parseTaskContractInfo(taskPlanContent) {
65
+ const content = String(taskPlanContent || "");
66
+ const explicit =
67
+ content.match(/^Task Contract\s*[::]\s*`?([^`\n]+)`?\s*$/im) ||
68
+ content.match(/^任务合同\s*[::]\s*`?([^`\n]+)`?\s*$/im);
69
+ const version = explicit ? explicit[1].trim() : "";
70
+ return {
71
+ version,
72
+ generated: version === "harness-task/v1" || content.includes(taskContractMarker),
73
+ };
74
+ }
75
+
76
+ export function parseTaskStateInfo(progressContent) {
77
+ const match = progressContent.match(/^##\s*(?:Current Status|Status|状态)\s*[::]?\s*(?:\n\s*)?([^\n]+)/im);
78
+ if (!match) return inferLegacyTaskState(progressContent);
79
+ const raw = match[1].replace(/`/g, "").trim();
80
+ if (!raw || raw.includes("|") || /^[-*]\s+/.test(raw)) return inferLegacyTaskState(progressContent);
81
+ const aliases = new Map([
82
+ ["进行中", "in_progress"],
83
+ ["已完成", "done"],
84
+ ["未开始", "not_started"],
85
+ ["计划中", "planned"],
86
+ ["审查中", "review"],
87
+ ["已阻塞", "blocked"],
88
+ ["pending", "planned"],
89
+ ]);
90
+ const normalized = aliases.get(raw) || raw.toLowerCase().replaceAll("-", "_").replaceAll(" ", "_");
91
+ return allowedTaskStates.has(normalized)
92
+ ? { state: normalized, source: "explicit", raw }
93
+ : { state: "unknown", source: "invalid", raw };
94
+ }
95
+
96
+ function inferLegacyTaskState(progressContent) {
97
+ const { header, rows } = tableAfterHeading(progressContent, /^(Status|状态)$/i);
98
+ const statusIndex = firstColumn(header, ["Status", "状态"]);
99
+ if (statusIndex < 0 || rows.length === 0) return { state: "unknown", source: "missing", raw: "" };
100
+ const states = rows.map((row) => normalizeLegacyState(row[statusIndex])).filter(Boolean);
101
+ if (states.includes("blocked")) return { state: "blocked", source: "legacy-table", raw: "blocked" };
102
+ if (states.includes("in_progress")) return { state: "in_progress", source: "legacy-table", raw: "in_progress" };
103
+ if (states.includes("review")) return { state: "review", source: "legacy-table", raw: "review" };
104
+ if (states.length > 0 && states.every((state) => state === "done")) return { state: "done", source: "legacy-table", raw: "done" };
105
+ if (states.some((state) => ["planned", "not_started"].includes(state))) return { state: "planned", source: "legacy-table", raw: "planned" };
106
+ return { state: "unknown", source: "missing", raw: "" };
107
+ }
108
+
109
+ function normalizeLegacyState(value) {
110
+ const raw = String(value || "").replace(/`/g, "").trim().toLowerCase();
111
+ if (!raw || /^(none|n\/a|na|-|—|–|无)$/.test(raw)) return "";
112
+ if (/block|阻塞|blocked/.test(raw)) return "blocked";
113
+ if (/in[-_\s]?progress|doing|active|进行中|当前|working/.test(raw)) return "in_progress";
114
+ if (/review|审查|审核|验证中/.test(raw)) return "review";
115
+ if (/done|complete|completed|merged|closed|完成|已完成/.test(raw)) return "done";
116
+ if (/pending|planned|todo|not[-_\s]?started|未开始|计划/.test(raw)) return "planned";
117
+ return "";
118
+ }
@@ -12,10 +12,15 @@ import {
12
12
  splitMarkdownRow,
13
13
  tableAfterHeading,
14
14
  } from "./markdown-utils.mjs";
15
+ import {
16
+ implementationPhases,
17
+ phaseHasRecordedProgress,
18
+ } from "./phase-kind.mjs";
15
19
  import { validateReviewConfirmationGitAudit } from "./review-confirm-git-gate.mjs";
16
20
  import { isLessonCandidateDecisionComplete } from "./task-lesson-candidates.mjs";
21
+ import { reviewConfirmationFromTaskAudit } from "./task-audit-metadata.mjs";
17
22
 
18
- export const taskScannerVersion = "task-scanner/2026-05-23-lifecycle-queues";
23
+ export const taskScannerVersion = "task-scanner/2026-05-25-phase-kind";
19
24
  export const reviewFindingColumns = {
20
25
  severity: ["Severity", "严重级别", "优先级"],
21
26
  finding: ["Finding", "发现"],
@@ -158,15 +163,12 @@ export function assessMaterialsReadiness({ budget, taskDir, brief, visualMap, re
158
163
  if (!isLessonCandidateDecisionComplete(lessonCandidates)) {
159
164
  addIssue("missing-lesson-decision", `Lesson candidate decision is not complete: ${lessonCandidates.status}.`, `TARGET:${lessonCandidatesFile}`);
160
165
  }
161
- const actionablePhases = (phases || []).filter((phase) => phase.state !== "skipped");
162
- const hasPhaseEvidence = actionablePhases.some(
163
- (phase) =>
164
- phase.completion > 0 ||
165
- ["in_progress", "review", "blocked", "done"].includes(String(phase.state || "").toLowerCase()) ||
166
- ["partial", "present", "waived"].includes(String(phase.evidenceStatus || "").toLowerCase()),
167
- );
168
- if (actionablePhases.length > 0 && !hasPhaseEvidence) {
169
- addIssue("phase-incomplete", "Visual Map has no phase progress or evidence yet.", `TARGET:${visualMapFile}`);
166
+ const actionablePhases = implementationPhases(phases || []);
167
+ const hasPhaseEvidence = actionablePhases.some(phaseHasRecordedProgress);
168
+ if ((phases || []).length > 0 && actionablePhases.length === 0) {
169
+ addIssue("missing-execution-phase", "Visual Map has no non-skipped execution phase.", `TARGET:${visualMapFile}`);
170
+ } else if (actionablePhases.length > 0 && !hasPhaseEvidence) {
171
+ addIssue("phase-incomplete", "Visual Map has no execution phase progress or evidence yet.", `TARGET:${visualMapFile}`);
170
172
  }
171
173
  }
172
174
  return { ready: issues.length === 0, issues };
@@ -181,7 +183,7 @@ export function requiresReviewMaterials({ state = "unknown", lifecycleState = "u
181
183
  );
182
184
  }
183
185
 
184
- export function deriveTaskQueues({ id, title, reviewStatus, reviewSubmission, reviewConfirmation, reviewQueueState, materialIssues, risks, stateConflicts, lessonCandidates, closeoutStatus, tombstone, taskDir, target }) {
186
+ export function deriveTaskQueues({ id, title, state, budget, reviewStatus, reviewSubmission, reviewConfirmation, reviewQueueState, materialIssues, risks, stateConflicts, lessonCandidates, closeoutStatus, tombstone, taskDir, target }) {
185
187
  const queueReasons = [];
186
188
  const pushReason = (reason) => {
187
189
  queueReasons.push({
@@ -227,17 +229,26 @@ export function deriveTaskQueues({ id, title, reviewStatus, reviewSubmission, re
227
229
  message: "Agent Review Submission was generated by a stale scanner version.",
228
230
  });
229
231
  }
232
+ if (budget !== "simple" && reviewSubmission?.submitted && reviewQueueState === "needs-material" && !queueReasons.some((reason) => reason.queue === "missing-materials")) {
233
+ pushReason({
234
+ code: "review-closeout-materials-incomplete",
235
+ queue: "missing-materials",
236
+ sourcePath: "TARGET:docs/10-WALKTHROUGH/Closeout-SSoT.md",
237
+ message: "Agent review was submitted, but closeout materials are not ready for human confirmation.",
238
+ });
239
+ }
230
240
  const hasLessonWork = lessonCandidates?.status === "needs-promotion" || lessonCandidates?.promotionState === "queued" || lessonCandidates?.openCount > 0;
231
241
  const taskQueues = [];
232
242
  if (tombstone.deletionState !== "active") {
233
243
  taskQueues.push("soft-deleted-superseded");
234
244
  } else {
235
- if ((materialIssues || []).length > 0) taskQueues.push("missing-materials");
245
+ if ((materialIssues || []).length > 0 || queueReasons.some((reason) => reason.queue === "missing-materials")) taskQueues.push("missing-materials");
236
246
  if (queueReasons.some((reason) => reason.queue === "blocked")) taskQueues.push("blocked");
237
247
  if (reviewSubmission?.submitted && reviewQueueState === "ready-to-confirm" && !reviewConfirmation?.confirmed && !hasLessonWork && !taskQueues.includes("blocked") && !taskQueues.includes("missing-materials")) {
238
248
  taskQueues.push("review");
239
249
  }
240
250
  if (hasLessonWork) taskQueues.push("lessons");
251
+ if (budget === "simple" && state === "done" && closeoutStatus === "closed") taskQueues.push("finalized");
241
252
  if (reviewStatus === "confirmed") taskQueues.push(closeoutStatus === "closed" ? "finalized" : "confirmed");
242
253
  }
243
254
  if (taskQueues.length === 0) taskQueues.push("active");
@@ -248,60 +259,34 @@ export function deriveTaskQueues({ id, title, reviewStatus, reviewSubmission, re
248
259
  };
249
260
  }
250
261
 
251
- export function parseReviewConfirmation(reviewContent, { taskKey = "", projectRoot = "", taskDir = "", reviewPath = "", progressPath = "" } = {}) {
252
- const match = String(reviewContent || "").match(/^##\s*(?:Human Review Confirmation|人工审查确认)\s*$([\s\S]*?)(?=^##\s+|(?![\s\S]))/im);
253
- if (!match) return null;
254
- const fields = fieldsFromMarkdownBlock(match[1] || "");
255
- const required = ["Confirmation ID", "Confirmed At", "Reviewer", "Reviewer Email", "Task Key", "Confirm Text", "Evidence Checked", "Commit SHA", "Audit Status"];
256
- const missing = required.filter((field) => !isConcreteField(fields.get(field.toLowerCase())));
257
- const confirmedTaskKey = fields.get("task key") || "";
258
- const confirmText = fields.get("confirm text") || "";
259
- const commitSha = fields.get("commit sha") || "";
260
- const auditStatus = fields.get("audit status") || "";
261
- const taskKeyMismatch = Boolean(taskKey && isConcreteField(confirmedTaskKey) && !taskKeysMatch(confirmedTaskKey, taskKey));
262
- const confirmTextMismatch = Boolean(taskKey && isConcreteField(confirmText) && !taskKeysMatch(confirmText, taskKey));
263
- const commitShaInvalid = Boolean(isConcreteField(commitSha) && !/^[0-9a-f]{7,40}$/i.test(commitSha));
264
- const auditStatusInvalid = Boolean(isConcreteField(auditStatus) && auditStatus.trim().toLowerCase() !== "committed");
265
- let gitAudit = null;
266
- if (missing.length === 0 && !taskKeyMismatch && !confirmTextMismatch && !commitShaInvalid && !auditStatusInvalid) {
267
- gitAudit = validateReviewConfirmationGitAudit({
268
- projectRoot,
269
- taskId: taskKey,
270
- reviewPath: reviewPath || (taskDir ? path.join(taskDir, "review.md") : ""),
271
- progressPath: progressPath || (taskDir ? path.join(taskDir, "progress.md") : ""),
272
- commitSha,
273
- });
274
- }
275
- const gitAuditInvalid = Boolean(gitAudit && !gitAudit.valid);
276
- const invalidFields = [
277
- ...(taskKeyMismatch ? ["Task Key match"] : []),
278
- ...(confirmTextMismatch ? ["Confirm Text match"] : []),
279
- ...(commitShaInvalid ? ["Commit SHA valid"] : []),
280
- ...(auditStatusInvalid ? ["Audit Status committed"] : []),
281
- ...(gitAuditInvalid ? ["Commit SHA git audit"] : []),
282
- ];
283
- if (fields.size > 0) {
284
- return {
285
- confirmed: missing.length === 0 && invalidFields.length === 0,
286
- missingFields: [...missing, ...invalidFields],
287
- confirmationId: fields.get("confirmation id") || "",
288
- confirmedAt: fields.get("confirmed at") || "",
289
- reviewer: fields.get("reviewer") || "",
290
- reviewerEmail: fields.get("reviewer email") || "",
291
- taskKey: confirmedTaskKey,
292
- taskKeyMismatch,
293
- confirmText,
294
- confirmTextMismatch,
295
- evidenceChecked: fields.get("evidence checked") || "",
296
- commitSha,
297
- commitShaInvalid,
298
- auditStatus,
299
- auditStatusInvalid,
300
- gitAudit,
301
- gitAuditInvalid,
302
- };
262
+ export function parseReviewConfirmation(reviewContent, { taskKey = "", taskAudit = null, projectRoot = "", taskDir = "", indexPath = "", reviewPath = "", progressPath = "" } = {}) {
263
+ if (taskAudit) {
264
+ const confirmation = reviewConfirmationFromTaskAudit(taskAudit, { taskKey });
265
+ if (
266
+ confirmation?.confirmed &&
267
+ confirmation.auditSource !== "migrated-legacy-review" &&
268
+ projectRoot &&
269
+ (indexPath || taskDir) &&
270
+ confirmation.commitSha
271
+ ) {
272
+ const gitAudit = validateReviewConfirmationGitAudit({
273
+ projectRoot,
274
+ taskId: taskKey,
275
+ reviewPath: indexPath || path.join(taskDir, "INDEX.md"),
276
+ progressPath: "",
277
+ commitSha: confirmation.commitSha,
278
+ });
279
+ return {
280
+ ...confirmation,
281
+ confirmed: confirmation.confirmed && gitAudit.valid,
282
+ missingFields: gitAudit.valid ? confirmation.missingFields : [...confirmation.missingFields, "Review Commit SHA git audit"],
283
+ gitAudit,
284
+ gitAuditInvalid: !gitAudit.valid,
285
+ };
286
+ }
287
+ return confirmation;
303
288
  }
304
- return { confirmed: false, missingFields: required };
289
+ return null;
305
290
  }
306
291
 
307
292
  export function taskReviewStatus({ reviewContent = "", risks = [], confirmation = null, submission = null } = {}) {
@@ -327,8 +312,9 @@ export function isBlockingReviewRisk(risk) {
327
312
  return /^P[0-2]$/i.test(risk?.severity || "") && (risk.open || risk.blocksRelease);
328
313
  }
329
314
 
330
- export function deriveLifecycleState({ state = "unknown", reviewStatus = "missing", closeoutStatus = "missing" } = {}) {
315
+ export function deriveLifecycleState({ state = "unknown", reviewStatus = "missing", closeoutStatus = "missing", budget = "standard" } = {}) {
331
316
  if (reviewStatus === "blocked-open-findings") return "review-blocked";
317
+ if (budget === "simple" && closeoutStatus === "closed") return "closed";
332
318
  if (closeoutStatus === "closed" && reviewStatus !== "confirmed") return "closed-review-pending";
333
319
  if (closeoutStatus === "closed") return "closed";
334
320
  if (state === "blocked") return "blocked";
@@ -354,12 +340,12 @@ export function deriveReviewQueueState({ state = "unknown", lifecycleState = "un
354
340
  return "ready-to-confirm";
355
341
  }
356
342
 
357
- export function collectStateConflicts({ state, reviewStatus, closeoutStatus, lifecycleState }) {
343
+ export function collectStateConflicts({ state, reviewStatus, closeoutStatus, lifecycleState, budget = "standard" }) {
358
344
  const conflicts = [];
359
345
  if (state === "done" && closeoutStatus !== "closed") {
360
346
  conflicts.push({ code: "done-without-closeout", severity: "warn", message: "Task state is done, but closeout is still missing or pending." });
361
347
  }
362
- if (closeoutStatus === "closed" && reviewStatus !== "confirmed") {
348
+ if (closeoutStatus === "closed" && reviewStatus !== "confirmed" && budget !== "simple") {
363
349
  conflicts.push({ code: "closed-without-human-review", severity: "warn", message: "Task is closed, but human review confirmation is still missing." });
364
350
  }
365
351
  if (reviewStatus === "blocked-open-findings") {