import { d as CleoError, E as ExitCode, n as isAllowedWorkGraphParentType, T as TERMINAL_TASK_STATUSES, c as getProjectRoot, o as resolveOrCwd, b as getLogger } from './paths-2M_UxD_H.js';
import { g as getTaskAccessor } from './data-accessor-CVbz8X_7.js';
import { w as wrapWithAgentSession } from './sessions-j73dkbCI.js';
import './branch-lock-qP1EkZ7p.js';
import { a as getNexusNativeDb } from './nexus-sqlite2-DgTuv9BQ.js';
import { l as loadConfig, g as getRawConfigValue } from './config-B0nEjgSh.js';
import { t as typedAll } from './typed-query-l1D_QElt.js';
import { r as requireActiveSession, f as createAcceptanceEnforcement, v as validateTitle, h as validateStatus, n as normalizePriority, i as validateTaskType, j as validateSize, k as validateLabels, l as validatePipelineTransition, m as validateEpicStageAdvancement, o as findEpicAncestor, p as validateChildStageCeiling, q as resolveHierarchyPolicy, u as planAcUpdate, w as applyAcPlan, x as rebuildChildProjectionAc, y as trackBackgroundOp, z as isValidPipelineStage, A as isTerminalPipelineStage } from './tasks-CnwUAUXf.js';
import { f as findSagasGroupingTask, b as buildSagaAutoCloseEvidence } from './storage-B9UA3BHR.js';
import './session-store-OwQWMSf_.js';
import { mkdirSync, appendFileSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { mkdir, appendFile } from 'node:fs/promises';

//#region ../core/dist/tasks/ac-coverage-gate.js
/**
* AC-coverage gate — the load-bearing IVTR-closure piece.
*
* Runs inside `cleo complete <tid>` BEFORE the status flip and refuses to
* mark a task done when any acceptance criterion has zero
* `evidence_ac_bindings` rows pointing at it. Closes the rubber-stamp
* loophole that let workers complete tasks whose ACs were never tied to
* evidence.
*
* # Binding semantics
*
* An AC is "covered" when at least one of the following kinds of binding
* row exists for its `ac_id`:
*   - `direct`    — Worker's own evidence atom emitted by the owning task
*   - `satisfies` — Cross-task atom via ADR-079-r2 `satisfies:<task>#<ac>`
*   - `coverage`  — Validator-computed transitive coverage marker
*
* All three counts contribute equally. Per ADR-079-r4 §3.
*
* # Override paths
*
* The gate is hard: programmatic evidence is the canonical satisfaction
* path. Two audited override paths exist for legitimate exceptions:
*   1. `--waive-ac "<csv>" --waive-reason "<text>"` — per-AC waiver
*      logged to `.cleo/audit/ac-waiver.jsonl`. Waivers are scoped to a
*      single `cleo complete` call; the unsatisfied AC list is
*      recomputed AFTER applying the waiver set so partially-waived
*      tasks fail when any AC is left unaddressed.
*   2. `CLEO_OWNER_OVERRIDE=1` (+ `CLEO_OWNER_OVERRIDE_REASON=<text>`)
*      — full bypass logged to `.cleo/audit/force-bypass.jsonl` per
*      existing convention (ADR-051 §6.2).
*
* # Tasks with zero ACs
*
* The gate is a no-op for tasks that declare zero acceptance criteria
* — nothing to satisfy means nothing to enforce. Operators that want
* "every task must have ACs" should turn on the existing
* `enforcement.acceptance.mode='block'` config knob, which fires
* earlier in the pipeline.
*
* # Transactional safety
*
* The coverage check is run as a pre-condition inside the caller's
* transaction context, BEFORE any state mutation. The DB read uses the
* same accessor handle as the subsequent writes so SQLite snapshot
* isolation pins the AC + binding rows for the duration of the
* completion call.
*
* @task T10509
* @saga T10377 (SG-IVTR-AC-BINDING)
* @epic T10381 (E-AC-MIGRATION)
* @adr ADR-079-r4
*/
/**
* Compute coverage for a task. Returns `{ok:true}` when every AC has
* at least one binding OR the task declared zero ACs; otherwise
* `{ok:false}` with the offending AC rows.
*
* @param taskId - The task being completed.
* @param accessor - DataAccessor used for the AC + binding reads.
* @returns Coverage outcome.
*/
async function computeAcCoverage(taskId, accessor) {
	const acRows = await accessor.getAcRows(taskId);
	if (acRows.length === 0) return { ok: true };
	const acIds = acRows.map((r) => r.id);
	const bindings = await accessor.getAcBindings(acIds);
	const covered = /* @__PURE__ */ new Set();
	for (const b of bindings) covered.add(b.acId);
	const unsatisfied = acRows.filter((r) => !covered.has(r.id)).map((r) => ({
		acId: r.id,
		alias: `AC${r.ordinal}`,
		text: r.text
	}));
	if (unsatisfied.length === 0) return { ok: true };
	return {
		ok: false,
		unsatisfied
	};
}
/**
* Resolve the comma-separated `--waive-ac` token list against the
* task's AC rows. Tokens may be:
*   - A canonical UUIDv4 (`task_acceptance_criteria.id`)
*   - A display alias (`AC<ordinal>`)
*
* Tokens that resolve to neither are surfaced on `unresolved` so the
* caller can warn — they are NOT silently dropped, but they also do
* not fail the gate by themselves. The caller decides.
*
* @param waiveCsv - Comma-separated token list (may be empty / undefined).
* @param acRows - The task's AC rows (from `accessor.getAcRows`).
* @returns Resolved waiver descriptor.
*/
function resolveWaivers(waiveCsv, acRows) {
	if (waiveCsv === void 0 || waiveCsv.trim().length === 0) return {
		acIds: [],
		aliases: [],
		texts: [],
		unresolved: []
	};
	const tokens = waiveCsv.split(",").map((t) => t.trim()).filter((t) => t.length > 0);
	const byUuid = /* @__PURE__ */ new Map();
	const byAlias = /* @__PURE__ */ new Map();
	for (const row of acRows) {
		byUuid.set(row.id.toLowerCase(), row);
		byAlias.set(`AC${row.ordinal}`.toLowerCase(), row);
	}
	const acIds = [];
	const aliases = [];
	const texts = [];
	const unresolved = [];
	const seen = /* @__PURE__ */ new Set();
	for (const token of tokens) {
		const key = token.toLowerCase();
		const row = byUuid.get(key) ?? byAlias.get(key);
		if (row === void 0) {
			unresolved.push(token);
			continue;
		}
		if (seen.has(row.id)) continue;
		seen.add(row.id);
		acIds.push(row.id);
		aliases.push(`AC${row.ordinal}`);
		texts.push(row.text);
	}
	return {
		acIds,
		aliases,
		texts,
		unresolved
	};
}
/**
* Subtract a waiver set from an unsatisfied list. Returns the residual
* unsatisfied ACs. When the residual is empty, the gate passes; when
* non-empty the caller surfaces `E_AC_COVERAGE_INCOMPLETE` with the
* residual.
*
* @param unsatisfied - From {@link computeAcCoverage}.
* @param waivedAcIds - Set of canonical AC UUIDs the operator waived.
* @returns Residual unsatisfied list (ACs not covered AND not waived).
*/
function applyWaivers(unsatisfied, waivedAcIds) {
	if (waivedAcIds.size === 0) return [...unsatisfied];
	return unsatisfied.filter((u) => !waivedAcIds.has(u.acId));
}
/**
* Append a single waiver entry to `.cleo/audit/ac-waiver.jsonl`. Creates
* the audit directory on first use. Errors propagate — failure to write
* the audit row MUST block the completion since the audit trail is the
* forensic guarantee that backs the override.
*
* @param entry - Pre-built audit entry.
* @param projectRoot - Absolute path to the project root.
*/
async function appendAcWaiverAudit(entry, projectRoot) {
	const auditDir = join(projectRoot, ".cleo", "audit");
	await mkdir(auditDir, { recursive: true });
	await appendFile(join(auditDir, "ac-waiver.jsonl"), `${JSON.stringify(entry)}\n`, { encoding: "utf8" });
}
/**
* Append an AC-coverage force-bypass entry to `.cleo/audit/force-bypass.jsonl`.
* Re-uses the canonical force-bypass log per ADR-051 §6.2; the `kind`
* discriminator on the entry distinguishes AC-coverage bypasses from
* existing evidence-gate bypasses.
*
* Errors propagate — like {@link appendAcWaiverAudit} the audit trail
* is load-bearing.
*
* @param entry - Pre-built audit entry.
* @param projectRoot - Absolute path to the project root.
*/
async function appendAcCoverageForceBypass(entry, projectRoot) {
	const auditDir = join(projectRoot, ".cleo", "audit");
	await mkdir(auditDir, { recursive: true });
	await appendFile(join(auditDir, "force-bypass.jsonl"), `${JSON.stringify(entry)}\n`, { encoding: "utf8" });
}
/**
* Inspect the runtime env for the canonical `CLEO_OWNER_OVERRIDE=1`
* + `CLEO_OWNER_OVERRIDE_REASON=<text>` pair. Returns the reason when
* BOTH are present; returns `null` when override is off or the reason
* is missing/blank.
*
* Centralised here so the gate, the audit writer, and the dispatch
* layer agree on the exact semantics. Mirrors the convention used by
* `gate-audit.ts`.
*/
function readOwnerOverride(env = process.env) {
	const flag = env["CLEO_OWNER_OVERRIDE"];
	if (flag !== "1" && flag !== "true") return null;
	const reason = env["CLEO_OWNER_OVERRIDE_REASON"];
	if (typeof reason !== "string") return null;
	const trimmed = reason.trim();
	if (trimmed.length === 0) return null;
	return trimmed;
}
//#endregion
//#region ../core/dist/tasks/coordination-parent.js
/**
* Coordination parent detection and rollup evidence synthesis.
*
* A "coordination parent" is a task that:
*   1. Has no files of its own (`files` is absent, null, or empty).
*   2. Has at least one registered child.
*   3. Has `noAutoComplete` unset or `false`.
*
* These tasks act purely as scope containers — their scope was delivered
* entirely by their children (e.g. T1916/T1918/T1919 in T1910). Before T9040,
* they required manual evidence atoms and stayed `pending` forever unless the
* orchestrator performed an expensive workaround.
*
* This module provides:
*   - {@link isCoordinationParent} — predicate used by the completion path.
*   - {@link buildRollupEvidence} — synthesizes verification evidence by
*     aggregating children's gate state.
*
* @task T9040
*/
/**
* Determine whether a task is a "coordination parent".
*
* A coordination parent has NO own implementation files but DOES own children.
* This pattern arises when a task was added as a scope container for a wave of
* subtasks, with no code changes on the parent itself.
*
* Rules:
*   - `task.files` is absent, `null`, or an empty array.
*   - `childrenCount > 0` (caller must pass the actual children length).
*   - `task.noAutoComplete` is NOT `true` (respects the existing opt-out flag).
*
* Both epic and non-epic tasks can be coordination parents. Epics already have
* their own rollup path in `completeTask`; this helper is primarily used for
* `type='task'` nodes that act as group containers within a larger epic.
*
* @param task - The task to test.
* @param childrenCount - Number of registered children for the task.
* @returns `true` when the task is a coordination parent.
*
* @task T9040
*/
function isCoordinationParent(task, childrenCount) {
	if (task.noAutoComplete === true) return false;
	if (childrenCount === 0) return false;
	return !(Array.isArray(task.files) && task.files.length > 0);
}
/**
* Standard verification gates synthesized during parent rollup.
*
* Order mirrors the canonical gate sequence from the verification module.
*/
var ROLLUP_GATES = [
	"implemented",
	"testsPassed",
	"qaPassed",
	"cleanupDone",
	"securityPassed",
	"documented"
];
/**
* Synthesize a {@link TaskVerification} for a coordination parent by
* aggregating the verification state of all non-cancelled children.
*
* Gate synthesis rules:
*   - `implemented` — always `true` (children carried the scope).
*   - `testsPassed`, `qaPassed`, `cleanupDone`, `securityPassed`, `documented`
*     — `true` when ALL non-cancelled children have the gate set to `true`, OR
*     when no child has any verification metadata (best-effort rollup for
*     projects without strict enforcement).
*   - `passed` — always `true` for a coordination parent whose children are all
*     done/cancelled (the caller has already confirmed terminal state).
*
* The `evidence` block on each gate carries a `note` atom explaining that this
* is a synthesized rollup, and a second `note` atom referencing the child IDs
* that delivered the scope.
*
* @param parentId - ID of the coordination parent (used in evidence notes).
* @param children - All registered children of the parent (including cancelled).
* @returns A fully synthesized {@link TaskVerification} ready for upsert.
*
* @task T9040
*/
function buildRollupEvidence(parentId, children) {
	const nonCancelled = children.filter((c) => c.status !== "cancelled");
	const childIds = nonCancelled.map((c) => c.id).join(",");
	const now = (/* @__PURE__ */ new Date()).toISOString();
	/**
	* Return true when the given gate passes for ALL non-cancelled children.
	* Children without any verification record are treated as passing so that
	* coordination parents in non-enforcement projects can still roll up.
	*/
	function childrenPassGate(gate) {
		if (nonCancelled.length === 0) return true;
		return nonCancelled.every((c) => {
			if (!c.verification) return true;
			return c.verification.gates?.[gate] === true;
		});
	}
	/**
	* Build a single gate's evidence record with two note atoms:
	*   1. A note describing the rollup provenance.
	*   2. A note listing the child task IDs that delivered this gate.
	*/
	function makeGateEvidence(gate, passed) {
		return {
			atoms: [{
				kind: "note",
				note: `coordination-rollup:${parentId}:${gate}=${String(passed)}`
			}, {
				kind: "note",
				note: `linked_tasks:${childIds}`
			}],
			capturedAt: now,
			capturedBy: "system:coordination-rollup"
		};
	}
	const gates = { implemented: true };
	const evidence = {};
	evidence.implemented = makeGateEvidence("implemented", true);
	for (const gate of ROLLUP_GATES) {
		if (gate === "implemented") continue;
		const passed = childrenPassGate(gate);
		gates[gate] = passed;
		evidence[gate] = makeGateEvidence(gate, passed);
	}
	return {
		round: 1,
		passed: true,
		gates: {
			implemented: true,
			testsPassed: gates.testsPassed ?? false,
			qaPassed: gates.qaPassed ?? false,
			cleanupDone: gates.cleanupDone ?? false,
			securityPassed: gates.securityPassed ?? false,
			documented: gates.documented ?? false
		},
		evidence,
		lastAgent: null,
		lastUpdated: now,
		failureLog: [],
		initializedAt: now
	};
}
//#endregion
//#region ../core/dist/tasks/nexus-impact-gate.js
/**
* IVTR Breaking-Change Gate — blocks completion when task touches CRITICAL-risk symbols.
*
* Implements EP3-T8 from T1042 living-brain decomposition. The gate reads the task's
* `files` field, queries nexus for symbols in those files, calls `reasonImpactOfChange()`
* for each symbol, and fails if any symbol returns `mergedRiskScore === 'CRITICAL'`.
*
* Gate is OPT-IN via `CLEO_NEXUS_IMPACT_GATE=1` env var to prevent surprise breakage.
*
* @task T1073
* @epic T1042
*/
/**
* Validate the nexusImpact gate for a task.
*
* If `CLEO_NEXUS_IMPACT_GATE` env var is not set to '1', returns passed=true
* with a note that the gate is disabled (prevents surprise breakage).
*
* When enabled, reads the task's `files` array, queries nexus for all symbols
* in those files, and calls `reasonImpactOfChange()` for each. If any symbol
* returns `mergedRiskScore === 'CRITICAL'`, the gate fails with a list of
* critical symbols.
*
* @param task - The task to validate
* @param projectRoot - Absolute path to project root
* @returns Gate result with pass/fail status and evidence
*/
async function validateNexusImpactGate(task, projectRoot) {
	if (!(process.env.CLEO_NEXUS_IMPACT_GATE === "1")) return {
		passed: true,
		narrative: "CLEO_NEXUS_IMPACT_GATE is disabled (gate skipped, set env var to 1 to enable)"
	};
	const filesToCheck = task.files ?? [];
	if (filesToCheck.length === 0) return {
		passed: true,
		narrative: "No files touched in task.files (no impact analysis needed)"
	};
	let symbolsInFiles = [];
	try {
		const nexusNative = getNexusNativeDb();
		if (nexusNative) {
			const placeholders = filesToCheck.map(() => "?").join(", ");
			symbolsInFiles = typedAll(nexusNative.prepare(`SELECT id, name, file_path
           FROM nexus_nodes
           WHERE file_path IN (${placeholders}) AND kind NOT IN ('folder', 'community', 'process')
           ORDER BY file_path, name`), ...filesToCheck);
		}
	} catch (err) {
		console.warn("[nexus-impact-gate] nexus symbol lookup failed:", err instanceof Error ? err.message : String(err));
		return {
			passed: true,
			narrative: "nexus symbol lookup failed; gate disabled (inspect error logs for details)"
		};
	}
	if (symbolsInFiles.length === 0) return {
		passed: true,
		narrative: `No symbols found in touched files (${filesToCheck.length} files checked, 0 symbols)`
	};
	const criticalSymbols = [];
	for (const symbol of symbolsInFiles) {
		let impact;
		try {
			const { reasonImpactOfChange } = await import('./living-brain2-BAssycIO.js');
			impact = await reasonImpactOfChange(symbol.id, projectRoot);
		} catch (err) {
			console.warn(`[nexus-impact-gate] reasonImpactOfChange failed for ${symbol.id}:`, err instanceof Error ? err.message : String(err));
			continue;
		}
		if (impact.mergedRiskScore === "CRITICAL") criticalSymbols.push({
			symbolId: symbol.id,
			symbolName: symbol.name,
			filePath: symbol.file_path,
			mergedRiskScore: impact.mergedRiskScore,
			narrative: impact.narrative
		});
	}
	if (criticalSymbols.length === 0) return {
		passed: true,
		narrative: `All ${symbolsInFiles.length} symbols have acceptable risk levels (no CRITICAL)`
	};
	const symbolList = criticalSymbols.map((s) => `${s.symbolName}(${s.symbolId})`).join(", ");
	return {
		passed: false,
		exitCode: ExitCode.NEXUS_IMPACT_CRITICAL,
		error: `Task ${task.id} touches ${criticalSymbols.length} CRITICAL-risk symbol${criticalSymbols.length !== 1 ? "s" : ""}: ${symbolList}. Use --acknowledge-risk "<reason>" to bypass.`,
		criticalSymbols,
		narrative: `CRITICAL impact detected on ${criticalSymbols.length} symbol${criticalSymbols.length !== 1 ? "s" : ""}. ${symbolList}`
	};
}
//#endregion
//#region ../core/dist/tasks/complete.js
/**
* Task completion logic.
* @task T4461
* @epic T4454
*/
/**
* IVTR execution stages — tasks in these stages auto-advance to 'release'
* when marked complete (cleo complete TXXX).  T719.
*
* Historical note: prior to T871 this set drove the only pipeline-stage
* side-effect of `cleo complete`. T871 adds a second (always-fires) step
* that sets `pipelineStage='contribution'` so status and pipeline_stage
* stay in sync — the Studio pipeline DONE column depends on this.
*/
var EXECUTION_STAGES_FOR_RELEASE = new Set([
	"implementation",
	"validation",
	"testing"
]);
var DEFAULT_VERIFICATION_REQUIRED_GATES = [
	"implemented",
	"testsPassed",
	"qaPassed",
	"securityPassed",
	"documented"
];
var VERIFICATION_GATES = new Set([
	"implemented",
	"testsPassed",
	"qaPassed",
	"cleanupDone",
	"securityPassed",
	"documented"
]);
function isVerificationGate(value) {
	return VERIFICATION_GATES.has(value);
}
async function loadCompletionEnforcement(cwd) {
	const isTest = !!process.env.VITEST;
	const config = await loadConfig(cwd);
	const acceptance = config.enforcement?.acceptance;
	const verificationCfg = config.verification;
	const acceptanceMode = acceptance?.mode ?? (isTest ? "off" : "block");
	const acceptanceRequiredForPriorities = acceptance?.requiredForPriorities ?? (isTest ? [] : [
		"critical",
		"high",
		"medium",
		"low"
	]);
	const rawVerificationEnabled = await getRawConfigValue("verification.enabled", cwd);
	return {
		acceptanceMode,
		acceptanceRequiredForPriorities,
		verificationEnabled: rawVerificationEnabled !== void 0 ? rawVerificationEnabled : !isTest,
		verificationRequiredGates: (verificationCfg?.requiredGates ?? []).filter(isVerificationGate).length > 0 ? (verificationCfg?.requiredGates ?? []).filter(isVerificationGate) : DEFAULT_VERIFICATION_REQUIRED_GATES,
		verificationMaxRounds: verificationCfg?.maxRounds ?? 5,
		lifecycleMode: config.lifecycle?.mode ?? (isTest ? "off" : "strict")
	};
}
/**
* Check whether an epic has sufficient evidence to complete.
*
* An epic satisfies the evidence requirement if ANY of the following is true:
* 1. It has at least one direct evidence atom on any verification gate.
* 2. All of its children (excluding cancelled) have `status='done'` AND
*    `verification.passed=true`.
*
* Returns `true` when the epic may proceed; `false` when it must be rejected.
*
* @param task - The epic task to check.
* @param acc - Data accessor used to load children.
* @returns `true` when evidence is sufficient; `false` otherwise.
* @task T1404
*/
async function verifyEpicHasEvidence(task, acc) {
	if (task.verification?.evidence) {
		const gates = task.verification.evidence;
		if (Object.values(gates).some((gateEvidence) => gateEvidence != null && gateEvidence.atoms.length > 0)) return true;
	}
	const nonCancelled = (await acc.getChildren(task.id)).filter((c) => c.status !== "cancelled");
	if (nonCancelled.length > 0 && nonCancelled.every((c) => c.status === "done" && c.verification?.passed === true)) return true;
	return false;
}
/**
* Resolve the absolute project root to write audit logs to. The gate
* call site has `cwd` as an optional parameter; we re-use the same
* canonical fallback the rest of `completeTask` uses ({@link getProjectRoot}).
*
* @internal
* @task T10509
*/
function projectRootForGate(cwd) {
	return cwd ?? getProjectRoot();
}
async function enforceAcCoverageGate(options, projectRoot, accessor) {
	const waiveCsv = options.waiveAc?.trim();
	if (waiveCsv !== void 0 && waiveCsv.length > 0) {
		if ((options.waiveReason?.trim() ?? "").length === 0) throw new CleoError(ExitCode.AC_COVERAGE_INCOMPLETE, "--waive-ac requires --waive-reason \"<text>\" — the reason is the audit anchor.", {
			fix: "Re-run with --waive-reason \"<justification>\" (the reason is captured in .cleo/audit/ac-waiver.jsonl).",
			details: {
				field: "waiveReason",
				codeName: "E_AC_COVERAGE_INCOMPLETE",
				missingFlag: "waiveReason"
			}
		});
	}
	const coverage = await computeAcCoverage(options.taskId, accessor);
	if (coverage.ok) return;
	const ownerReason = readOwnerOverride();
	if (ownerReason !== null) {
		await appendAcCoverageForceBypass({
			kind: "ac-coverage",
			timestamp: (/* @__PURE__ */ new Date()).toISOString(),
			taskId: options.taskId,
			reason: ownerReason,
			actor: process.env["CLEO_AGENT_ID"] ?? "cleo",
			unsatisfied: coverage.unsatisfied
		}, projectRoot);
		return;
	}
	let residual = coverage.unsatisfied;
	let unresolvedTokens = [];
	if (waiveCsv !== void 0 && waiveCsv.length > 0) {
		const resolved = resolveWaivers(waiveCsv, await accessor.getAcRows(options.taskId));
		unresolvedTokens = resolved.unresolved;
		residual = applyWaivers(coverage.unsatisfied, new Set(resolved.acIds));
		await appendAcWaiverAudit({
			timestamp: (/* @__PURE__ */ new Date()).toISOString(),
			taskId: options.taskId,
			waivedAcs: resolved.acIds,
			waivedAliases: resolved.aliases,
			reason: (options.waiveReason ?? "").trim(),
			actor: process.env["CLEO_AGENT_ID"] ?? "cleo",
			unresolvedTokens
		}, projectRoot);
		if (residual.length === 0 && unresolvedTokens.length === 0) return;
	}
	const offenderList = residual.map((u) => `${u.alias} (${u.acId})`).join(", ");
	const unresolvedHint = unresolvedTokens.length > 0 ? ` Unresolved waive tokens: ${unresolvedTokens.join(", ")}.` : "";
	throw new CleoError(ExitCode.AC_COVERAGE_INCOMPLETE, `Task ${options.taskId} cannot complete — ${residual.length} acceptance criterion/criteria have no evidence bindings: ${offenderList}.${unresolvedHint}`, {
		fix: "Either (1) record evidence via `cleo verify <taskId> --gate … --evidence …` so a binding row is written, (2) pass `--waive-ac \"<csv>\" --waive-reason \"<text>\"` to record an audited waiver, or (3) set CLEO_OWNER_OVERRIDE=1 + CLEO_OWNER_OVERRIDE_REASON=<text> for a full bypass.",
		details: {
			field: "acceptance",
			codeName: "E_AC_COVERAGE_INCOMPLETE",
			unsatisfied: residual,
			unresolvedTokens
		}
	});
}
/**
* Execute the task-completion critical section under the tasks DB write lock.
*
* The concrete SQLite accessor backs `transaction()` with `BEGIN IMMEDIATE` at
* the outermost boundary, so the AC completion gate and status/autoclose writes
* run in one atomic write transaction instead of validating on a stale read
* snapshot and flipping `status='done'` later.
*
* @task T10595
*/
async function withTaskWriteTransaction(accessor, fn) {
	return accessor.transaction(fn);
}
/**
* Complete a task by ID.
* Handles dependency checking and optional auto-completion of epics.
* @task T4461
*/
async function completeTask(options, cwd, accessor) {
	const acc = accessor ?? await getTaskAccessor(cwd);
	const task = await acc.loadSingleTask(options.taskId);
	if (!task) throw new CleoError(ExitCode.NOT_FOUND, `Task not found: ${options.taskId}`, { fix: `Use 'cleo find "${options.taskId}"' to search` });
	await requireActiveSession("tasks.complete", cwd);
	const enforcement = await loadCompletionEnforcement(cwd);
	if (task.status === "done") throw new CleoError(ExitCode.TASK_COMPLETED, `Task ${options.taskId} is already completed`, {
		fix: `To reopen, run cleo update ${options.taskId} --status active`,
		details: {
			field: "status",
			expected: "not done",
			actual: "done"
		}
	});
	if (task.depends?.length) {
		const unresolvedDeps = (await acc.loadTasks(task.depends)).filter((d) => !TERMINAL_TASK_STATUSES.has(d.status)).map((d) => ({
			id: d.id,
			status: d.status
		}));
		if (unresolvedDeps.length > 0) {
			const waiverReason = options.waiveDependsReason?.trim();
			if (!waiverReason) {
				const offenders = unresolvedDeps.map((d) => `${d.id} (${d.status})`).join(", ");
				throw new CleoError(ExitCode.DEPENDENCY_ERROR, `Task ${options.taskId} has unresolved dependencies: ${offenders}`, {
					fix: `Complete the dependencies first (${unresolvedDeps.map((d) => `cleo complete ${d.id}`).join(", ")}), OR — if the edge is stale/over-specified and this task's own work is done — drop it with 'cleo update ${options.taskId} --remove-depends <id>', OR record an audited one-shot override: 'cleo complete ${options.taskId} --waive-depends "<reason>"' (audited to .cleo/audit/depends-waiver.jsonl).`,
					details: {
						field: "depends",
						unresolvedDeps
					}
				});
			}
			try {
				const { appendDependsWaiverAudit } = await import('./depends-waiver-audit-zJ6KDrCS.js');
				await appendDependsWaiverAudit({
					taskId: options.taskId,
					unresolvedDeps,
					waiverReason,
					timestamp: (/* @__PURE__ */ new Date()).toISOString(),
					agent: process.env["CLEO_AGENT_ID"] ?? "cleo"
				}, cwd);
			} catch (err) {
				console.warn("[complete] failed to write depends-waiver audit:", err instanceof Error ? err.message : String(err));
			}
		}
	}
	const completionValidation = (await createAcceptanceEnforcement(cwd)).validateCompletion(task);
	if (!completionValidation.valid) throw new CleoError(completionValidation.exitCode ?? ExitCode.VALIDATION_ERROR, completionValidation.error, { fix: completionValidation.fix });
	if (enforcement.verificationEnabled && task.type !== "epic") {
		if (!task.verification) throw new CleoError(ExitCode.VERIFICATION_INIT_FAILED, `Task ${options.taskId} is missing verification metadata`, { fix: `Initialize verification for ${options.taskId} before completion` });
		if (task.verification.round > enforcement.verificationMaxRounds) throw new CleoError(ExitCode.MAX_ROUNDS_EXCEEDED, `Task ${options.taskId} exceeded verification max rounds (${enforcement.verificationMaxRounds})`, { fix: `Review failure log and resolve blockers before retrying completion` });
		const missingRequiredGates = enforcement.verificationRequiredGates.filter((gate) => task.verification?.gates?.[gate] !== true);
		if (missingRequiredGates.length > 0 || task.verification.passed !== true) throw new CleoError(enforcement.lifecycleMode === "strict" ? ExitCode.LIFECYCLE_GATE_FAILED : ExitCode.GATE_DEPENDENCY, `Task ${options.taskId} failed verification gates: ${missingRequiredGates.join(", ") || "verification.passed=false"}`, { fix: `Set required verification gates before completion: ${enforcement.verificationRequiredGates.join(", ")}` });
	}
	if (enforcement.verificationEnabled && enforcement.lifecycleMode === "strict" && task.type === "epic") {
		if (!await verifyEpicHasEvidence(task, acc)) throw new CleoError(ExitCode.LIFECYCLE_GATE_FAILED, `Epic ${options.taskId} cannot complete without direct evidence atoms or verified children`, { fix: `Either add direct evidence via 'cleo verify ${options.taskId} --gate implemented --evidence "..."' or ensure all children have status=done with verification.passed=true.` });
	}
	const projectRoot = cwd ?? getProjectRoot();
	const gateResult = await validateNexusImpactGate(task, projectRoot);
	if (!gateResult.passed && !options.acknowledgeRisk) throw new CleoError(gateResult.exitCode ?? ExitCode.NEXUS_IMPACT_CRITICAL, gateResult.error ?? "CRITICAL impact risk detected", {
		fix: "Either fix the CRITICAL symbols or pass --acknowledge-risk \"<reason>\" to bypass the gate",
		details: {
			field: "criticalSymbols",
			criticalSymbols: gateResult.criticalSymbols?.map((s) => ({
				symbol: s.symbolName ?? s.symbolId,
				risk: s.mergedRiskScore,
				narrative: s.narrative
			}))
		}
	});
	if (options.acknowledgeRisk && gateResult.criticalSymbols && gateResult.criticalSymbols.length > 0) try {
		const { appendNexusRiskAck } = await import('./nexus-risk-audit-orjcwn2y.js');
		await appendNexusRiskAck({
			taskId: options.taskId,
			symbols: gateResult.criticalSymbols.map((s) => ({
				symbolId: s.symbolId,
				symbolName: s.symbolName ?? void 0,
				risk: s.mergedRiskScore
			})),
			reason: options.acknowledgeRisk,
			timestamp: (/* @__PURE__ */ new Date()).toISOString(),
			agent: process.env.CLEO_AGENT_ID ?? "cleo"
		});
	} catch (err) {
		console.warn("[complete] failed to audit nexus risk acknowledgment:", err instanceof Error ? err.message : String(err));
	}
	const children = await acc.getChildren(options.taskId);
	const pendingChildren = children.filter((c) => c.status !== "done" && c.status !== "cancelled");
	if (pendingChildren.length > 0 && task.type === "epic") {
		if (!options.overrideReason) throw new CleoError(ExitCode.EPIC_HAS_PENDING_CHILDREN, `Epic ${options.taskId} has ${pendingChildren.length} pending/active children: ${pendingChildren.map((c) => c.id).join(", ")}`, { fix: "Complete all children first, or pass --override-reason \"<reason>\" to bypass (audited to .cleo/audit/premature-close.jsonl)." });
		try {
			const { appendPrematureCloseAudit } = await import('./premature-close-audit-BR0_Yt4I.js');
			await appendPrematureCloseAudit({
				epicId: options.taskId,
				pendingChildIds: pendingChildren.map((c) => c.id),
				overrideReason: options.overrideReason,
				timestamp: (/* @__PURE__ */ new Date()).toISOString(),
				agent: process.env["CLEO_AGENT_ID"] ?? "cleo"
			}, cwd);
		} catch (err) {
			console.warn("[complete] failed to write premature-close audit:", err instanceof Error ? err.message : String(err));
		}
	}
	const cancelledChildren = children.filter((c) => c.status === "cancelled");
	if (cancelledChildren.length > 0) {
		const waiverReason = options.cancelledChildWaiverReason?.trim();
		if (!waiverReason) throw new CleoError(ExitCode.CANCELLED_CHILD_NO_WAIVER, `Task ${options.taskId} has ${cancelledChildren.length} cancelled child${cancelledChildren.length === 1 ? "" : "ren"} that must be waived or replaced before completion: ${cancelledChildren.map((c) => c.id).join(", ")}`, {
			fix: `Cancelled children represent work that was NOT done — they cannot silently satisfy parent completion. Either re-open and finish the work, replace it with a done child, or record an audited waiver: 'cleo complete ${options.taskId} --waive-cancelled-children "<reason>"' (cite the replacement task id in the reason when applicable; audited to .cleo/audit/cancelled-child-waiver.jsonl).`,
			details: {
				field: "cancelledChildIds",
				cancelledChildIds: cancelledChildren.map((c) => c.id)
			}
		});
		try {
			const { appendCancelledChildWaiverAudit } = await import('./cancelled-child-waiver-audit-DHilCj01.js');
			await appendCancelledChildWaiverAudit({
				parentId: options.taskId,
				cancelledChildIds: cancelledChildren.map((c) => c.id),
				waiverReason,
				timestamp: (/* @__PURE__ */ new Date()).toISOString(),
				agent: process.env["CLEO_AGENT_ID"] ?? "cleo"
			}, cwd);
		} catch (err) {
			console.warn("[complete] failed to write cancelled-child-waiver audit:", err instanceof Error ? err.message : String(err));
		}
	}
	const now = (/* @__PURE__ */ new Date()).toISOString();
	const before = { ...task };
	const autoCompleted = [];
	const autoCompletedTasks = [];
	const { receipt } = await wrapWithAgentSession({
		sessionId: typeof process.env.CLEO_SESSION_ID === "string" && process.env.CLEO_SESSION_ID.length > 0 ? process.env.CLEO_SESSION_ID : void 0,
		agentId: process.env.CLEO_AGENT_ID ?? "cleo",
		projectRoot: resolveOrCwd(cwd),
		label: `complete:${options.taskId}`
	}, async () => withTaskWriteTransaction(acc, async (tx) => {
		if (task.verification?.passed === true) {
			const acRows = await acc.getAcRows(options.taskId);
			if (acRows.length > 0) {
				const acIds = acRows.map((ac) => ac.id);
				const bindings = await acc.getAcBindings(acIds);
				const boundAcIds = new Set(bindings.map((b) => b.acId));
				const unbound = acRows.filter((ac) => !boundAcIds.has(ac.id));
				if (unbound.length > 0) await tx.insertAcBindings(unbound.map((ac) => ({
					id: `auto-coverage-${ac.id.slice(0, 8)}`,
					evidenceAtomId: "auto-coverage-verification-passed",
					acId: ac.id,
					bindingType: "coverage"
				})));
			}
		}
		await enforceAcCoverageGate(options, projectRootForGate(cwd), tx);
		const completionStage = task.pipelineStage;
		if (completionStage && isValidPipelineStage(completionStage) && EXECUTION_STAGES_FOR_RELEASE.has(completionStage)) task.pipelineStage = "release";
		if (!isTerminalPipelineStage(task.pipelineStage)) task.pipelineStage = "contribution";
		task.status = "done";
		task.completedAt = now;
		task.updatedAt = now;
		if (options.notes) {
			const timestampedNote = `${(/* @__PURE__ */ new Date()).toISOString().replace("T", " ").replace(/\.\d+Z$/, " UTC")}: ${options.notes}`;
			if (!task.notes) task.notes = [];
			task.notes.push(timestampedNote);
		}
		if (options.changeset) {
			if (!task.notes) task.notes = [];
			task.notes.push(`Changeset: ${options.changeset}`);
		}
		if (task.parentId) {
			const parent = await acc.loadSingleTask(task.parentId);
			if (parent && parent.type === "epic" && !parent.noAutoComplete) {
				const siblings = await acc.getChildren(parent.id);
				const allDone = siblings.length > 0 && siblings.every((c) => c.id === task.id || c.status === "done" || c.status === "cancelled");
				const hasCancelledSibling = siblings.some((c) => c.id !== task.id && c.status === "cancelled");
				if (allDone && hasCancelledSibling) getLogger("engine:complete").info({
					epicId: parent.id,
					completedChild: task.id
				}, "[complete] suppressing epic auto-close: un-waived cancelled children require `cleo complete --waive-cancelled-children`");
				if (allDone && !hasCancelledSibling) {
					if (enforcement.verificationEnabled && enforcement.lifecycleMode === "strict" ? await verifyEpicHasEvidence(parent, {
						...acc,
						getChildren: async (parentId) => {
							if (parentId !== parent.id) return acc.getChildren(parentId);
							return siblings.map((c) => {
								if (c.id !== task.id) return c;
								return {
									...c,
									status: "done",
									verification: c.verification ?? task.verification ?? void 0
								};
							});
						}
					}) : true) {
						parent.status = "done";
						parent.completedAt = now;
						parent.updatedAt = now;
						if (!isTerminalPipelineStage(parent.pipelineStage)) parent.pipelineStage = "contribution";
						autoCompleted.push(parent.id);
						autoCompletedTasks.push(parent);
					}
				}
			}
		}
		if (task.parentId) {
			const coordinationParent = await acc.loadSingleTask(task.parentId);
			if (coordinationParent && coordinationParent.type !== "epic" && coordinationParent.type !== "saga" && !autoCompleted.includes(coordinationParent.id)) {
				const cpChildren = await acc.getChildren(coordinationParent.id);
				if (cpChildren.length > 0 && isCoordinationParent(coordinationParent, cpChildren.length)) {
					const allCpDone = cpChildren.every((c) => c.id === task.id || c.status === "done" || c.status === "cancelled");
					const cpHasCancelledChild = cpChildren.some((c) => c.id !== task.id && c.status === "cancelled");
					if (allCpDone && cpHasCancelledChild) getLogger("engine:complete").info({
						parentId: coordinationParent.id,
						completedChild: task.id
					}, "[complete] suppressing coordination-parent auto-close: un-waived cancelled children require `cleo complete --waive-cancelled-children`");
					if (allCpDone && !cpHasCancelledChild) {
						const childrenForRollup = cpChildren.map((c) => c.id === task.id ? {
							...c,
							status: "done"
						} : c);
						coordinationParent.verification = buildRollupEvidence(coordinationParent.id, childrenForRollup);
						coordinationParent.status = "done";
						coordinationParent.completedAt = now;
						coordinationParent.updatedAt = now;
						if (!isTerminalPipelineStage(coordinationParent.pipelineStage)) coordinationParent.pipelineStage = "contribution";
						autoCompleted.push(coordinationParent.id);
						autoCompletedTasks.push(coordinationParent);
					}
				}
			}
		}
		const sagasGroupingThisTask = await findSagasGroupingTask(acc, task.id);
		for (const saga of sagasGroupingThisTask) {
			if (saga.status === "done" || saga.status === "cancelled") continue;
			if (autoCompleted.includes(saga.id)) continue;
			const memberTasks = (await acc.queryTasks({ parentId: saga.id })).tasks;
			if (memberTasks.map((member) => member.id).length === 0) continue;
			if (!memberTasks.every((m) => {
				if (m.id === task.id) return true;
				return m.status === "done" || m.status === "cancelled";
			})) continue;
			const terminalMemberIds = memberTasks.filter((m) => m.id === task.id || m.status === "done" || m.status === "cancelled").map((m) => m.id);
			saga.verification = buildSagaAutoCloseEvidence(saga.id, terminalMemberIds, now);
			saga.status = "done";
			saga.completedAt = now;
			saga.updatedAt = now;
			if (!isTerminalPipelineStage(saga.pipelineStage)) saga.pipelineStage = "contribution";
			autoCompleted.push(saga.id);
			autoCompletedTasks.push(saga);
		}
		await tx.upsertSingleTask(task);
		for (const parentTask of autoCompletedTasks) await tx.upsertSingleTask(parentTask);
		await tx.appendLog({
			id: `log-${Math.floor(Date.now() / 1e3)}-${(await import('node:crypto')).randomBytes(3).toString("hex")}`,
			timestamp: (/* @__PURE__ */ new Date()).toISOString(),
			action: "task_completed",
			taskId: options.taskId,
			actor: "system",
			details: {
				title: task.title,
				previousStatus: before.status
			},
			before: null,
			after: {
				title: task.title,
				previousStatus: before.status
			}
		});
		return {};
	}));
	const dependents = await acc.getDependents(options.taskId);
	const unblockedTasks = [];
	for (const dep of dependents) {
		if (dep.status === "done" || dep.status === "cancelled" || dep.status === "archived") continue;
		if (dep.depends?.length) {
			if ((await acc.loadTasks(dep.depends)).filter((d) => d.id !== options.taskId && d.status !== "done" && d.status !== "cancelled" && d.status !== "archived").length === 0) unblockedTasks.push({
				id: dep.id,
				title: dep.title
			});
		} else unblockedTasks.push({
			id: dep.id,
			title: dep.title
		});
	}
	try {
		const { completeAgentWorktreeIntegration } = await import('./branch-lock2-CF6OCMBW.js');
		const integration = completeAgentWorktreeIntegration(options.taskId, projectRoot, { taskTitle: task.title });
		if (!integration.merged && integration.error) getLogger("tasks:complete").warn({
			taskId: options.taskId,
			mergeError: integration.error,
			worktreeRemoved: integration.worktreeRemoved
		}, "[T9175] worktree integration failed — branch + worktree preserved for manual recovery");
	} catch (err) {
		getLogger("tasks:complete").debug({ err: err instanceof Error ? err.message : String(err) }, "[T9175] worktree integration helper unavailable — skipping");
	}
	trackBackgroundOp(import('./graph-auto-populate-C4tZUMjb.js').then(({ upsertGraphNode, addGraphEdge }) => (async () => {
		const projectRoot = resolveOrCwd(cwd);
		await upsertGraphNode(projectRoot, `task:${task.id}`, "task", `${task.id}: ${task.title}`.substring(0, 200), 1, task.title, {
			status: "done",
			priority: task.priority
		});
		if (task.parentId) {
			await upsertGraphNode(projectRoot, `epic:${task.parentId}`, "epic", task.parentId, 1, "");
			await addGraphEdge(projectRoot, `task:${task.id}`, `epic:${task.parentId}`, "part_of", 1, "auto:task-complete");
		}
	})()).catch(() => {}));
	try {
		const { hooks } = await import('./registry-CpiJvLWz.js');
		await hooks.dispatch("PostToolUse", resolveOrCwd(cwd), {
			timestamp: (/* @__PURE__ */ new Date()).toISOString(),
			taskId: options.taskId,
			taskTitle: task.title,
			previousStatus: before.status,
			newStatus: "done",
			unblockedCount: unblockedTasks.length
		}).catch(() => {});
	} catch {}
	const receiptSummary = receipt !== null ? {
		receiptId: receipt.sessionId,
		...receipt.signature ? { signature: receipt.signature } : {}
	} : void 0;
	return {
		task,
		...autoCompleted.length > 0 && { autoCompleted },
		...unblockedTasks.length > 0 && { unblockedTasks },
		...receiptSummary ? { receipt: receiptSummary } : {}
	};
}
//#endregion
//#region ../core/dist/tasks/ac-immutability.js
/**
* Acceptance-criteria immutability guard.
*
* Locks `task.acceptance` once a task enters the implementation pipeline
* stage (or any later stage). Prevents the "T-THIN-WRAPPER feature-complete"
* anti-pattern where acceptance criteria are silently reframed after work
* has shipped to match what was actually built.
*
* The lock can be overridden by supplying an explicit operator `reason`,
* which is appended to an append-only audit log at
* `.cleo/audit/ac-changes.jsonl` together with the before/after AC, the
* stage at which the override occurred, the timestamp and the agent
* identifier.
*
* Locked stages: implementation, validation, testing, release, contribution.
*
* @epic T1586 Foundation Lockdown (Wave A)
* @task T1590
*/
/**
* Pipeline stages at which acceptance criteria are considered locked.
*
* Once a task crosses into `implementation`, AC are treated as a binding
* contract — any subsequent change must be operator-approved with a
* written reason that is captured in the audit log.
*
* @task T1590
*/
var AC_LOCKED_STAGES = new Set([
	"implementation",
	"validation",
	"testing",
	"release",
	"contribution"
]);
/** Relative path within project root for the AC-changes audit log. */
var AC_CHANGES_AUDIT_FILE = ".cleo/audit/ac-changes.jsonl";
/**
* Check whether a task's pipeline stage locks acceptance-criteria changes.
*
* @param stage - Current pipeline stage of the task (may be null/undefined).
* @returns True if the stage is in {@link AC_LOCKED_STAGES}.
*
* @example
* ```ts
* isAcceptanceLocked('research');       // => false
* isAcceptanceLocked('implementation'); // => true
* ```
*
* @task T1590
*/
function isAcceptanceLocked(stage) {
	return !!stage && AC_LOCKED_STAGES.has(stage);
}
/**
* Determine whether two acceptance-criteria arrays are structurally equal.
*
* Used to suppress the lock when the caller passes an unchanged AC payload
* (a no-op should never trip the immutability guard).
*
* @param a - First criteria array (or null/undefined).
* @param b - Second criteria array (or null/undefined).
* @returns True if both arrays have identical length and elements.
*
* @task T1590
*/
function acceptanceEquals(a, b) {
	const aArr = a ?? [];
	const bArr = b ?? [];
	if (aArr.length !== bArr.length) return false;
	for (let i = 0; i < aArr.length; i++) {
		const left = aArr[i];
		const right = bArr[i];
		if (typeof left === "string" && typeof right === "string") {
			if (left !== right) return false;
			continue;
		}
		if (JSON.stringify(left) !== JSON.stringify(right)) return false;
	}
	return true;
}
/**
* Enforce the AC-immutability guard for a task update.
*
* Behaviour:
*  - When `newAcceptance` is `undefined`, the call is a no-op.
*  - When the task is not in a locked stage, the call is a no-op.
*  - When the new AC is structurally identical to the existing AC, the call
*    is a no-op (idempotent updates are always allowed).
*  - When the task is in a locked stage and `reason` is missing or empty,
*    a {@link CleoError} with {@link ExitCode.AC_LOCKED} is thrown.
*  - When the task is in a locked stage and `reason` is provided, an audit
*    entry is appended to `.cleo/audit/ac-changes.jsonl`.
*
* @param options - {@link EnforceAcceptanceImmutabilityOptions}.
* @throws CleoError(AC_LOCKED) when the AC are locked and no reason is given.
*
* @example
* ```ts
* enforceAcceptanceImmutability({
*   task,
*   newAcceptance: ['AC1', 'AC2', 'AC3'],
*   reason: 'operator approved scope expansion',
*   projectRoot: '/project',
* });
* ```
*
* @task T1590
*/
function enforceAcceptanceImmutability(options) {
	const { task, newAcceptance, reason, projectRoot } = options;
	if (newAcceptance === void 0) return;
	if (!isAcceptanceLocked(task.pipelineStage)) return;
	if (acceptanceEquals(task.acceptance, newAcceptance)) return;
	const trimmedReason = typeof reason === "string" ? reason.trim() : "";
	if (!trimmedReason) throw new CleoError(ExitCode.AC_LOCKED, `Acceptance criteria locked at stage ${task.pipelineStage}. Reframing AC after implementation is anti-pattern. Use --reason '<X>' to override with audit log entry.`, {
		fix: `cleo update ${task.id} --acceptance "..." --reason "<why this change is acceptable>"`,
		details: {
			field: "acceptance",
			stage: task.pipelineStage ?? null,
			taskId: task.id
		}
	});
	appendAcceptanceChangeAudit({
		projectRoot: resolveOrCwd(projectRoot),
		taskId: task.id,
		stage: task.pipelineStage ?? "",
		reason: trimmedReason,
		oldAcceptance: task.acceptance ?? [],
		newAcceptance
	});
}
/**
* Append a single {@link AcceptanceChangeAuditEntry} to
* `.cleo/audit/ac-changes.jsonl`.
*
* Errors are deliberately swallowed (best-effort write) so audit failures
* never block a legitimate operator-approved update. The path mirrors
* `force-bypass.jsonl` and `contract-violations.jsonl` (ADR-039).
*
* @task T1590
*/
function appendAcceptanceChangeAudit(input) {
	try {
		const filePath = join(input.projectRoot, AC_CHANGES_AUDIT_FILE);
		mkdirSync(dirname(filePath), { recursive: true });
		const entry = {
			timestamp: (/* @__PURE__ */ new Date()).toISOString(),
			taskId: input.taskId,
			stage: input.stage,
			reason: input.reason,
			oldAcceptance: JSON.parse(JSON.stringify(input.oldAcceptance)),
			newAcceptance: JSON.parse(JSON.stringify(input.newAcceptance)),
			agent: process.env["CLEO_AGENT_ID"] ?? "cleo"
		};
		appendFileSync(filePath, `${JSON.stringify(entry)}\n`, { encoding: "utf-8" });
	} catch {}
}
//#endregion
//#region ../core/dist/tasks/child-disposition.js
/**
* Shared child-disposition guard for terminal-status writers (T11811 AC2).
*
* Three writers can drive a task into a terminal state: `coreTaskCancel`
* (`cleo cancel`), `updateTask` with `status='cancelled'`
* (`cleo update --status cancelled`), and `archiveTasks` (`cleo archive`).
* Historically only `coreTaskCancel` consulted the child set before
* terminalising a parent — the other two flipped the parent and SILENTLY
* STRANDED its still-active children under a terminal (or removed) parent
* (the T9031/T9044 strand class).
*
* The orphan-prevention guard collapses that to ONE decision: before any
* terminal-status writer terminalises a parent, it MUST consult this module.
* The lowest-blast-radius default — matching {@link coreTaskCancel}'s own
* `children: 'block'` default — is to REFUSE the write when the parent still
* has active (non-terminal) children, surfacing {@link ExitCode.HAS_CHILDREN}
* and pointing the operator at `cleo cancel <id> --children …`, where the full
* `block | cascade | reparent` disposition matrix lives.
*
* This is a read-only guard: it queries the child set and throws on violation;
* it performs no writes. The actual child disposition (cascade/reparent) is
* owned by `cleo cancel` via {@link coreTaskCancel} so there is exactly one
* place that mutates children on cancellation.
*
* @task T11811 — orphan-prevention guard (AC2: one disposition path)
* @saga T10400
*/
/**
* Refuse to terminalise a parent that still has active children.
*
* Counts the parent's non-terminal children via
* {@link DataAccessor.countActiveChildren} (the canonical "active" =
* not-in-`TERMINAL_TASK_STATUSES` count). When that count is non-zero the
* function throws {@link CleoError} with {@link ExitCode.HAS_CHILDREN}, a
* `fix:` that routes the operator to `cleo cancel <id> --children …`, and a
* structured `details` payload carrying the live child count. When the parent
* has zero active children it returns normally and the caller proceeds with
* the terminal write — no strand is possible.
*
* Routing every terminal-status writer through this single guard means the
* child-disposition decision lives in exactly one place (AC2): the only way to
* dispose of active children on cancellation is the explicit
* `cleo cancel <id> --children cascade|reparent` surface.
*
* @param accessor - Open data accessor for the active task store.
* @param taskId - The parent task ID being terminalised.
* @param verb - Which terminal write is being attempted (`cancel`/`archive`).
* @throws {CleoError} {@link ExitCode.HAS_CHILDREN} when the parent has ≥ 1
*   active (non-terminal) child.
*
* @example
* ```typescript
* // Inside the update --status cancelled path:
* await assertNoActiveChildrenForTerminal(accessor, taskId, 'cancel');
* // …only reached when the parent has no active children to strand.
* ```
*/
async function assertNoActiveChildrenForTerminal(accessor, taskId, verb) {
	const activeChildCount = await accessor.countActiveChildren(taskId);
	if (activeChildCount === 0) return;
	const verbNoun = "Cancelling";
	throw new CleoError(ExitCode.HAS_CHILDREN, `${verbNoun} ${taskId} would strand ${activeChildCount} active child task(s). Dispose of the children first via \`cleo cancel ${taskId} --children cascade\` (cancel the whole subtree) or \`cleo cancel ${taskId} --children reparent --to <epicId>\` (move them under another parent).`, {
		fix: `cleo cancel ${taskId} --children cascade   # cancel the subtree, or\ncleo cancel ${taskId} --children reparent --to <epicId>   # move children elsewhere`,
		details: {
			field: "status",
			taskId,
			activeChildCount,
			verb
		}
	});
}
//#endregion
//#region ../core/dist/tasks/update.js
/**
* Task update logic.
* @task T4461
* @epic T4454
*/
var NON_STATUS_DONE_FIELDS = [
	"title",
	"priority",
	"type",
	"size",
	"phase",
	"description",
	"labels",
	"addLabels",
	"removeLabels",
	"depends",
	"addDepends",
	"removeDepends",
	"notes",
	"acceptance",
	"files",
	"addFiles",
	"removeFiles",
	"blockedBy",
	"clearBlockedBy",
	"parentId",
	"noAutoComplete",
	"pipelineStage",
	"kind",
	"scope",
	"severity",
	"relates",
	"addRelates",
	"removeRelates"
];
function typeForParent(parentType, currentType) {
	if (currentType === "saga") return "saga";
	if (currentType === "epic") return "epic";
	if (parentType === "task") return "subtask";
	if (parentType === "epic") return "task";
	return currentType === "subtask" ? "task" : currentType ?? "task";
}
function hasNonStatusDoneFields(options) {
	return NON_STATUS_DONE_FIELDS.some((field) => options[field] !== void 0);
}
/**
* Update a task's fields.
* @task T4461
*/
async function updateTask(options, cwd, accessor) {
	const acc = accessor ?? await getTaskAccessor(cwd);
	const task = await acc.loadSingleTask(options.taskId);
	if (!task) throw new CleoError(ExitCode.NOT_FOUND, `Task not found: ${options.taskId}`, { fix: `Use 'cleo find "${options.taskId}"' to search` });
	await requireActiveSession("tasks.update", cwd);
	const changes = [];
	const now = (/* @__PURE__ */ new Date()).toISOString();
	const originalParentId = task.parentId ?? null;
	let lifecycleEvent = null;
	let lifecycleBeforeStatus = null;
	let lifecycleAfterStatus = null;
	if (options.status === "done" && task.status !== "done" && !hasNonStatusDoneFields(options)) return {
		task: (await completeTask({ taskId: options.taskId }, cwd, accessor)).task,
		changes: ["status"]
	};
	if (options.status === "done" && task.status !== "done") throw new CleoError(ExitCode.VALIDATION_ERROR, "status=done must use complete flow; do not combine with other update fields", { fix: `Run 'cleo complete ${options.taskId}' first, then apply additional updates with 'cleo update ${options.taskId} ...'` });
	const updateValidation = (await createAcceptanceEnforcement(cwd)).validateUpdate(task, { acceptance: options.acceptance });
	if (!updateValidation.valid) throw new CleoError(updateValidation.exitCode ?? ExitCode.VALIDATION_ERROR, updateValidation.error, { fix: updateValidation.fix });
	enforceAcceptanceImmutability({
		task,
		newAcceptance: options.acceptance,
		reason: options.reason,
		projectRoot: cwd
	});
	if (options.title !== void 0) {
		validateTitle(options.title);
		task.title = options.title;
		changes.push("title");
	}
	if (options.status !== void 0) {
		validateStatus(options.status);
		const { validateStatusTransition } = await import('./validation-rules-CE27rtoF.js');
		const transitionViolations = validateStatusTransition(task.status, options.status);
		if (transitionViolations.length > 0) throw new CleoError(ExitCode.VALIDATION_ERROR, transitionViolations[0].message, {
			fix: `Valid transitions from '${task.status}': see cleo update --help`,
			details: {
				field: transitionViolations[0].field ?? "status",
				expected: `valid transition from ${task.status}`,
				actual: options.status
			}
		});
		if (options.status === "cancelled" && task.status !== "cancelled") await assertNoActiveChildrenForTerminal(acc, options.taskId, "cancel");
		const oldStatus = task.status;
		task.status = options.status;
		changes.push("status");
		lifecycleBeforeStatus = oldStatus;
		lifecycleAfterStatus = options.status;
		if (options.status === "done" && oldStatus !== "done") {
			task.completedAt = now;
			lifecycleEvent = "task_completed";
		}
		if ((options.status === "pending" || options.status === "active") && oldStatus === "done") lifecycleEvent = "task_reopened";
		if (options.status === "pending" && oldStatus === "cancelled") lifecycleEvent = "task_uncancelled";
		if (options.status === "cancelled" && oldStatus !== "cancelled") {
			task.cancelledAt = now;
			lifecycleEvent = "task_cancelled";
			if (task.pipelineStage !== "cancelled") {
				task.pipelineStage = "cancelled";
				if (!changes.includes("pipelineStage")) changes.push("pipelineStage");
			}
		}
	}
	if (options.priority !== void 0) {
		task.priority = normalizePriority(options.priority);
		changes.push("priority");
	}
	if (options.type !== void 0) {
		validateTaskType(options.type);
		task.type = options.type;
		changes.push("type");
	}
	if (options.size !== void 0) {
		validateSize(options.size);
		task.size = options.size;
		changes.push("size");
	}
	if (options.phase !== void 0) {
		task.phase = options.phase;
		changes.push("phase");
	}
	if (options.description !== void 0) {
		task.description = options.description;
		changes.push("description");
	}
	if (options.labels !== void 0) {
		if (options.labels.length) validateLabels(options.labels);
		task.labels = options.labels;
		changes.push("labels");
	}
	if (options.addLabels?.length) {
		validateLabels(options.addLabels);
		const existing = new Set(task.labels ?? []);
		for (const l of options.addLabels) existing.add(l.trim());
		task.labels = [...existing];
		changes.push("labels");
	}
	if (options.removeLabels?.length) {
		const toRemove = new Set(options.removeLabels.map((l) => l.trim()));
		task.labels = (task.labels ?? []).filter((l) => !toRemove.has(l));
		changes.push("labels");
	}
	if (options.depends !== void 0) {
		task.depends = options.depends;
		changes.push("depends");
	}
	if (options.addDepends?.length) {
		const existing = new Set(task.depends ?? []);
		for (const d of options.addDepends) existing.add(d.trim());
		task.depends = [...existing];
		changes.push("depends");
	}
	if (options.removeDepends?.length) {
		const toRemove = new Set(options.removeDepends.map((d) => d.trim()));
		task.depends = (task.depends ?? []).filter((d) => !toRemove.has(d));
		changes.push("depends");
	}
	if (options.notes !== void 0) {
		const timestampedNote = `${(/* @__PURE__ */ new Date()).toISOString().replace("T", " ").replace(/\.\d+Z$/, " UTC")}: ${options.notes}`;
		if (!task.notes) task.notes = [];
		task.notes.push(timestampedNote);
		changes.push("notes");
	}
	if (options.acceptance !== void 0) {
		task.acceptance = options.acceptance;
		changes.push("acceptance");
	}
	if (options.files !== void 0) {
		task.files = options.files;
		changes.push("files");
	}
	if (options.addFiles?.length) {
		const existing = new Set(task.files ?? []);
		for (const f of options.addFiles) existing.add(f.trim());
		task.files = [...existing];
		changes.push("files");
	}
	if (options.removeFiles?.length) {
		const toRemove = new Set(options.removeFiles.map((f) => f.trim()));
		task.files = (task.files ?? []).filter((f) => !toRemove.has(f));
		changes.push("files");
	}
	if (options.blockedBy !== void 0) {
		task.blockedBy = options.blockedBy === "" ? void 0 : options.blockedBy;
		changes.push("blockedBy");
	}
	if (options.clearBlockedBy === true) {
		task.blockedBy = void 0;
		if (!changes.includes("blockedBy")) changes.push("blockedBy");
	}
	if (options.noAutoComplete !== void 0) {
		task.noAutoComplete = options.noAutoComplete;
		changes.push("noAutoComplete");
	}
	if (options.kind !== void 0) {
		task.kind = options.kind;
		changes.push("kind");
	}
	if (options.scope !== void 0) {
		task.scope = options.scope;
		changes.push("scope");
	}
	if (options.severity !== void 0) {
		task.severity = options.severity;
		changes.push("severity");
	}
	if (options.relates !== void 0) {
		task.relates = options.relates.map((r) => ({
			taskId: r.taskId,
			type: r.type,
			...r.reason ? { reason: r.reason } : {}
		}));
		changes.push("relates");
	}
	if (options.addRelates?.length) {
		const existing = new Map((task.relates ?? []).map((r) => [r.taskId, r]));
		for (const r of options.addRelates) existing.set(r.taskId, {
			taskId: r.taskId,
			type: r.type,
			...r.reason ? { reason: r.reason } : {}
		});
		task.relates = [...existing.values()];
		changes.push("relates");
	}
	if (options.removeRelates?.length) {
		const toRemove = new Set(options.removeRelates.map((id) => id.trim()));
		task.relates = (task.relates ?? []).filter((r) => !toRemove.has(r.taskId));
		changes.push("relates");
	}
	if (options.pipelineStage !== void 0) {
		validatePipelineTransition(task.pipelineStage, options.pipelineStage);
		if (task.type === "epic" && task.pipelineStage) await validateEpicStageAdvancement({
			epicId: task.id,
			currentStage: task.pipelineStage,
			newStage: options.pipelineStage
		}, acc, cwd);
		if (task.type !== "epic") {
			const epicAncestor = task.parentId ? await findEpicAncestor(task.parentId, acc) : null;
			const directParent = task.parentId ? await acc.loadSingleTask(task.parentId) : null;
			const epicToCheck = directParent?.type === "epic" ? directParent : epicAncestor;
			if (epicToCheck) await validateChildStageCeiling({
				childStage: options.pipelineStage,
				epicId: epicToCheck.id
			}, acc, cwd);
		}
		task.pipelineStage = options.pipelineStage;
		changes.push("pipelineStage");
	}
	if (options.parentId !== void 0) {
		const newParentId = options.parentId || null;
		if (newParentId !== (task.parentId ?? null)) {
			const originalType = task.type;
			if (!newParentId) {
				task.parentId = null;
				if (task.type === "subtask") task.type = "task";
				changes.push("parentId");
				if (task.type !== originalType) changes.push("type");
			} else {
				const newParent = await acc.loadSingleTask(newParentId);
				if (!newParent) throw new CleoError(ExitCode.PARENT_NOT_FOUND, `Parent task ${newParentId} not found`, {
					fix: `Use 'cleo find "${newParentId}"' to search or remove --parent flag`,
					details: {
						field: "parentId",
						actual: newParentId
					}
				});
				const newParentType = newParent.type ?? "task";
				const updatedType = typeForParent(newParentType, task.type);
				if (!isAllowedWorkGraphParentType(updatedType, newParentType)) throw new CleoError(ExitCode.INVALID_PARENT_TYPE, `Invalid parent type for ${updatedType}: parent ${newParentId} has type '${newParentType}'.`, {
					fix: "Use Saga→Epic, Epic→Task, and Task→Subtask containment.",
					details: {
						field: "parentId",
						expected: "saga->epic | epic->task | task->subtask",
						actual: {
							parentType: newParentType,
							childType: updatedType
						}
					}
				});
				if ((await acc.getSubtree(options.taskId)).some((t) => t.id === newParentId)) throw new CleoError(ExitCode.CIRCULAR_REFERENCE, `Moving '${options.taskId}' under '${newParentId}' would create a circular reference`, {
					fix: `Choose a parent that is not a descendant of ${options.taskId}`,
					details: {
						field: "parentId",
						actual: newParentId
					}
				});
				const parentDepth = (await acc.getAncestorChain(newParentId)).length;
				const policy = resolveHierarchyPolicy(await loadConfig(cwd));
				if (parentDepth + 1 >= policy.maxDepth) throw new CleoError(ExitCode.DEPTH_EXCEEDED, `Maximum nesting depth ${policy.maxDepth} would be exceeded`, {
					fix: "Choose a parent at a shallower level in the hierarchy",
					details: {
						field: "parentId",
						expected: `depth < ${policy.maxDepth}`,
						actual: parentDepth + 1
					}
				});
				task.parentId = newParentId;
				task.type = updatedType;
				changes.push("parentId");
				if (task.type !== originalType) changes.push("type");
			}
		}
	}
	if (changes.length === 0) throw new CleoError(ExitCode.NO_CHANGE, "No changes specified", {
		fix: `Provide at least one field to update (e.g. cleo update ${options.taskId} --status active)`,
		details: { field: "options" }
	});
	task.updatedAt = now;
	const finalRelates = task.relates ?? [];
	const isRelatesChange = changes.includes("relates");
	const isAcceptanceChange = changes.includes("acceptance");
	const isReparent = changes.includes("parentId");
	await acc.transaction(async (tx) => {
		await tx.upsertSingleTask(task);
		if (isRelatesChange) if (options.relates !== void 0) {
			await tx.clearRelations(options.taskId);
			for (const r of finalRelates) await tx.addRelation(options.taskId, r.taskId, r.type, r.reason);
		} else {
			if (options.addRelates?.length) for (const r of options.addRelates) await tx.addRelation(options.taskId, r.taskId, r.type, r.reason);
			if (options.removeRelates?.length) for (const id of options.removeRelates) await tx.removeRelation(options.taskId, id.trim());
		}
		if (isAcceptanceChange && options.acceptance !== void 0) {
			const existing = await tx.getAcRows(options.taskId);
			const plan = planAcUpdate(options.taskId, existing, options.acceptance);
			await applyAcPlan(tx, options.taskId, plan);
		}
		if (isReparent) {
			const parentIds = [originalParentId, task.parentId ?? null].filter((parentId) => parentId !== null);
			const uniqueParentIds = [...new Set(parentIds)];
			const projectionAudits = [];
			for (const parentId of uniqueParentIds) {
				const audit = await rebuildChildProjectionAc(tx, parentId, (await tx.getChildren(parentId)).map((child) => ({
					id: child.id,
					title: child.title
				})), now);
				projectionAudits.push(audit);
			}
			await tx.appendLog({
				id: `log-${Math.floor(Date.now() / 1e3)}-${(await import('node:crypto')).randomBytes(3).toString("hex")}`,
				timestamp: (/* @__PURE__ */ new Date()).toISOString(),
				action: "ac_projection_rebuilt",
				taskId: options.taskId,
				actor: "system",
				details: {
					reason: "reparent",
					oldParentId: originalParentId,
					newParentId: task.parentId ?? null,
					audits: projectionAudits
				},
				before: null,
				after: { parentIds: uniqueParentIds }
			});
		}
		if (lifecycleEvent) await tx.appendLog({
			id: `log-${Math.floor(Date.now() / 1e3)}-${(await import('node:crypto')).randomBytes(3).toString("hex")}`,
			timestamp: (/* @__PURE__ */ new Date()).toISOString(),
			action: lifecycleEvent,
			taskId: options.taskId,
			actor: "system",
			details: {
				title: task.title,
				changes
			},
			before: { status: lifecycleBeforeStatus },
			after: { status: lifecycleAfterStatus }
		});
		await tx.appendLog({
			id: `log-${Math.floor(Date.now() / 1e3)}-${(await import('node:crypto')).randomBytes(3).toString("hex")}`,
			timestamp: (/* @__PURE__ */ new Date()).toISOString(),
			action: "task_updated",
			taskId: options.taskId,
			actor: "system",
			details: {
				changes,
				title: task.title
			},
			before: null,
			after: {
				changes,
				title: task.title
			}
		});
	});
	return {
		task,
		changes
	};
}

export { updateTask as u };
//# sourceMappingURL=update-Dj_q4-Re.js.map
