FN-6275: allow verified no-op completions

Allow executors to finish already-satisfied tasks without fabricating commits while preserving existing completion guards.

- Add leading sentinel parsing for premise-stale, no-op, duplicate, and redundant completion summaries.
- Permit zero-commit fn_task_done only for recognized sentinels or existing no-commit contracts, with audit log/activity details.
- Document the verified no-op completion contract and add regression coverage for accepted and refused paths.

Files changed:
 .changeset/fn-6275-no-op-completion.md             |   5 ++
 docs/architecture.md                               |   1 +
 .../src/__tests__/no-op-completion-marker.test.ts  |  55 ++++++++++++
 packages/core/src/agent-prompts.ts                 |   4 +
 packages/core/src/index.ts                         |   5 ++
 packages/core/src/no-op-completion-marker.ts       |  48 ++++++++++
 .../__tests__/executor-task-done-invariant.test.ts | 100 +++++++++++++++++++++
 .../engine/src/__tests__/executor-test-helpers.ts  |   1 +
 packages/engine/src/executor.ts                    |  64 ++++++++++++-
 9 files changed, 279 insertions(+), 4 deletions(-)

Fusion-Task-Id: FN-6275

Fusion-Task-Lineage: 0a2ec21b-415f-411c-bfcd-4f1c19a6a136
This commit is contained in:
gsxdsm
2026-06-13 02:54:57 -07:00
parent 34ada00f80
commit 4e6df035c1
9 changed files with 279 additions and 4 deletions

View File

@@ -0,0 +1,5 @@
---
"@runfusion/fusion": minor
---
Add a verified no-op/duplicate task completion path so executors can close already-satisfied tasks without fabricating commits by using an audited `fn_task_done` sentinel summary.

View File

@@ -1791,6 +1791,7 @@ This section preserves the detailed lifecycle/self-healing contracts that were f
- **Scheduler fanout tiebreaker (FN-4969)**: within the same priority class, scheduler dispatch prefers runnable `todo` tasks with the highest active dependency-dependent fanout; `urgent` always outranks lower priorities regardless of fanout, and `overlapBlockedBy`/file-scope overlap blockers are excluded from unblock weight.
- **Scheduler overlap priority/age guard (FN-5325)**: with `groupOverlappingFiles=true`, scheduler now defers a lower-priority (or younger same-priority) candidate when an overlapping queued todo task exists, preserving priority→age→task-id order for overlap serialization without preempting in-progress work. If the inversion is against an already-running lower-priority blocker, scheduler still defers the candidate; the per-pairing audit event was removed in FN-6174 due to zero consumers and table bloat.
- **Empty-commit refusal + early empty-own-diff finalize (FN-5345/FN-5377)**: Fusion task worktrees install a `prepare-commit-msg` hook that refuses `git commit --allow-empty` and other zero-staged-diff commits, preventing verification-only tasks from manufacturing empty handoff commits that defeat the merger's no-op classifier. The hook allows legitimate empty-tree paths (amend, merge, squash, cherry-pick, revert, rebase). Amend detection tokenizes the parent process command line (`ps -o args=` with `/proc/$PPID/cmdline` fallback for Alpine/busybox) and stops at the first message-supplying flag (`-m`/`-F`/`--message`/`--file`) so a commit message containing the substring `--amend` cannot bypass the guard. In `aiMergeTask`, an early empty-own-diff fast-path runs BEFORE any reuse-handoff acquisition: when integration mode is `reuse-task-worktree`, the branch exists, `git rev-list --count <mergeTarget>..<branch>` is > 0, and `git diff --quiet <mergeBase>..<branch>` exits 0, the task auto-finalizes as no-op with `mergeDetails.noOpMerge: true` and emits `task:auto-recover-finalize-already-on-main` with `reason: "empty-own-diff-early-fast-path"`. The fast-path best-effort removes the stranded worktree (FN-4811 same-task/foreign-owner guard) and deletes the `fusion/<id>` branch so empty-own-diff residuals do not accumulate. This unsticks tasks where a stale empty handoff commit combined with drifted worktree↔branch mapping would otherwise wedge the handoff gate with `registered-branch-mismatch`. The explicit `cwd-integration-branch` mode is unchanged (`cwd-main` remains a deprecated alias normalized to it). `classifyOwnedLandedEvidence` also detects empty-own-diff (aheadCount > 0, zero net diff) and returns `proven-no-op` so downstream self-healing and post-handoff finalize paths benefit too. Additionally, merger's reuse-fallback path now consults `git worktree list --porcelain` before creating a new worktree: extant usable registrations of `fusion/<id>` are reused directly (rather than blindly `git worktree add -f` producing a duplicate registration), and stale registrations are pruned first. The direct-reuse shortcut is guarded by FN-4811 (refuses paths owned by a different task in `activeSessionRegistry`) and FN-4954 (skipped when `recycleWorktrees=true` with a pool attached, so `WorktreePool.acquire` lease bookkeeping stays consistent). Two audit subtypes — `merge:reuse-fallback-pruned-stale-registration` and `merge:reuse-fallback-reused-existing-registration` — replace the prior overloading of `merge:reuse-fallback-new-worktree` for these cases.
- **Verified no-op/duplicate executor completion (FN-6275)**: explicit `fn_task_done` may complete with zero branch commits only when the summary starts with a recognized sentinel (`PREMISE STALE:`, `NO-OP:`, `NOOP:`, `DUPLICATE: FN-NNNN ...`, or `REDUNDANT:`) or the task already carries a no-commit contract. The sentinel only relaxes the `no_commits` invariant; `wrong_toplevel`, `wrong_branch`, pending-step/review refusals, and scope-leak guards still run. Accepted sentinel completions persist `noCommitsExpected: true`, write task-log audit details with marker kind/reason/raw summary/run/agent IDs, and add a task timeline activity so the no-code terminal path remains explainable. Ordinary zero-commit implementation completions without a leading sentinel are still refused.
- **In-review branch-binding self-heal (FN-5083)**: `reconcile-in-review-branch-rebind` runs after `reconcile-task-worktree-metadata` and before `reclaim-stale-active-branches`. It restores `task.branch` (and clears `task.worktree` for fresh acquisition) for `in-review` tasks when exactly one case-insensitive `fusion/<id>` candidate branch has unique commits versus the integration base. Ambiguous candidates emit `task:auto-rebind-skipped` (`reason: "ambiguous-candidates"`) and are never auto-resolved. Branch construction across executor/worktree-pool/worktree-acquisition/merger/self-healing canonicalizes to lowercase via `canonicalFusionBranchName`; `fn_task_done` wrong-branch checks now auto-canonicalize case-only mismatches and emit `branch:auto-canonicalize-case`.
- **In-review is terminal-until-merged under `autoMerge: false` (FN-5147)**: when a project sets `settings.autoMerge: false`, `in-review` is the intended resting state until a human merges the PR. No lifecycle-mutating self-healing sweep (`reclaimSelfOwnedBranchConflicts`, `recoverGhostReviewTasks`, `recoverStaleIncompleteReviewTasks`, `recoverInterruptedMergingTasks`, `recoverStuckMergeDeadlocks`, `recoverMissingWorktreeReviewFailures`, `recoverPartialProgressNoTaskDoneFailures`, `recoverCompletionHandoffLimbo`, `recoverPostDoneNonContinuableWedge`, `recoverMergeableReviewTasks`, `recoverMergedReviewTasks`, `recoverAlreadyMergedReviewTasks`, `recoverOrphanOnlyScopeViolations`, `recoverForeignOnlyContaminatedInReviewTasks`, `recoverReviewTasksWithFailedPreMergeSteps`, `finalizeNoOpReviewTasks`, `surfaceInReviewStalls`, `surfaceInReviewStalled`) may move the task out of `in-review`, mark it `paused`/`failed`, or re-enqueue it for execution. Scoped FN-5819 exception: shared-group members (`branchContext.assignmentMode === "shared"`) are still allowed through the member→`branch_groups.branchName` integration step while `autoMerge` is off; this is a soft pre-integration only and does not permit shared-branch → default-branch promotion. RECONCILE-ONLY sweeps (branch rebind, blocker fan-out, stale-status clears, contamination metadata cleanup, attribution restore, PR refresh, misclassified-failure error clearing) continue to run.
- **Auto-merge integration-root default (FN-5279)**: direct auto-merge now defaults `mergeIntegrationWorktree` to `reuse-task-worktree`; merger must pass the reuse handoff gates or emit `merge:reuse-handoff-refused` and leave the task in `in-review` without silently falling back to `cwd-integration-branch` (`cwd-main` remains a deprecated alias normalized to that mode).

View File

@@ -0,0 +1,55 @@
import { describe, expect, it } from "vitest";
import { parseNoOpCompletionMarker } from "../no-op-completion-marker.js";
describe("parseNoOpCompletionMarker", () => {
it.each([
["PREMISE STALE: already implemented on HEAD", "premise-stale"],
["NO-OP: existing behavior already satisfies the request", "no-op"],
["NOOP: no code changes are needed", "no-op"],
["DUPLICATE: FN-6239 covers the same requested behavior", "duplicate"],
["REDUNDANT: FN-6239 already landed this", "redundant"],
] as const)("recognizes leading prefix %s", (summary, kind) => {
const marker = parseNoOpCompletionMarker(summary);
expect(marker).toMatchObject({ kind });
expect(marker?.reason.length).toBeGreaterThan(0);
});
it("matches prefixes case-insensitively", () => {
expect(parseNoOpCompletionMarker("no-op: verified unchanged")?.kind).toBe("no-op");
expect(parseNoOpCompletionMarker("duplicate: fn-6239 already covers it")).toMatchObject({
kind: "duplicate",
canonicalId: "FN-6239",
});
});
it("requires the marker at the start of the summary", () => {
expect(parseNoOpCompletionMarker("Verified existing behavior; NO-OP: no changes needed")).toBeNull();
expect(parseNoOpCompletionMarker("The task is DUPLICATE: FN-6239")).toBeNull();
});
it("returns null for empty, undefined, and ordinary prose", () => {
expect(parseNoOpCompletionMarker(undefined)).toBeNull();
expect(parseNoOpCompletionMarker("")).toBeNull();
expect(parseNoOpCompletionMarker("Implemented the requested behavior and verified tests.")).toBeNull();
});
it("captures duplicate and redundant canonical task ids", () => {
expect(parseNoOpCompletionMarker("DUPLICATE: FN-6239 existing QuickChatFAB tests cover this")).toMatchObject({
kind: "duplicate",
canonicalId: "FN-6239",
reason: "FN-6239 existing QuickChatFAB tests cover this",
});
expect(parseNoOpCompletionMarker("REDUNDANT: covered by fn-42 after rebase")).toMatchObject({
kind: "redundant",
canonicalId: "FN-42",
});
});
it("does not require a canonical id for duplicate and redundant summaries", () => {
expect(parseNoOpCompletionMarker("DUPLICATE: same request already exists on HEAD")).toEqual({
kind: "duplicate",
reason: "same request already exists on HEAD",
});
});
});

View File

@@ -387,6 +387,8 @@ Anti-heuristics (bias to false-negative when ambiguous):
- LEAVE UNSET: Investigate FN-XYZ and fix if needed
- LEAVE UNSET: Investigate and fix routing if needed
If an executor later proves an ordinary implementation task is already satisfied on HEAD, it may close without fabricating a commit by calling \`fn_task_done\` with a leading verified no-op/duplicate sentinel summary: \`PREMISE STALE:\`, \`NO-OP:\`, \`NOOP:\`, \`DUPLICATE: FN-NNNN ...\`, or \`REDUNDANT:\`. This does not weaken ordinary tasks: zero-commit completions without one of these leading sentinels still fail the no-commits invariant.
## Guidelines
- Read relevant source files before writing the spec
- Be specific: reference concrete files, modules, and commands from this repo
@@ -670,6 +672,8 @@ Anti-heuristics (bias to false-negative when ambiguous):
- LEAVE UNSET: Investigate FN-XYZ and fix if needed
- LEAVE UNSET: Investigate and fix routing if needed
If an executor later proves an ordinary implementation task is already satisfied on HEAD, it may close without fabricating a commit by calling \`fn_task_done\` with a leading verified no-op/duplicate sentinel summary: \`PREMISE STALE:\`, \`NO-OP:\`, \`NOOP:\`, \`DUPLICATE: FN-NNNN ...\`, or \`REDUNDANT:\`. This does not weaken ordinary tasks: zero-commit completions without one of these leading sentinels still fail the no-commits invariant.
## Guidelines
- Read the project structure and relevant source files to understand context BEFORE writing
- Check package.json/scripts and explicit project commands to align real lint/test/build/typecheck commands

View File

@@ -479,6 +479,11 @@ export {
parseExplicitDuplicateMarker,
type ExplicitDuplicateMarker,
} from "./explicit-duplicate-marker.js";
export {
parseNoOpCompletionMarker,
type NoOpCompletionMarker,
type NoOpCompletionMarkerKind,
} from "./no-op-completion-marker.js";
export {
__getDeterministicGuardMutexSize,
deterministicGuardLocks,

View File

@@ -0,0 +1,48 @@
export type NoOpCompletionMarkerKind = "premise-stale" | "no-op" | "duplicate" | "redundant";
export interface NoOpCompletionMarker {
kind: NoOpCompletionMarkerKind;
reason: string;
canonicalId?: string;
}
const PREFIXES: Array<{ pattern: RegExp; kind: NoOpCompletionMarkerKind }> = [
{ pattern: /^PREMISE STALE:\s*/i, kind: "premise-stale" },
{ pattern: /^NO-OP:\s*/i, kind: "no-op" },
{ pattern: /^NOOP:\s*/i, kind: "no-op" },
{ pattern: /^DUPLICATE:\s*/i, kind: "duplicate" },
{ pattern: /^REDUNDANT:\s*/i, kind: "redundant" },
];
/**
* Detects explicit executor completion summaries that mean the task was
* verified as already satisfied on HEAD (no source commit is appropriate).
*
* The marker must be a leading, case-insensitive prefix. Mid-summary mentions
* intentionally do not match so ordinary prose cannot accidentally bypass the
* no-commits invariant.
*/
export function parseNoOpCompletionMarker(summary: string | undefined): NoOpCompletionMarker | null {
const trimmed = summary?.trim() ?? "";
if (!trimmed) {
return null;
}
for (const { pattern, kind } of PREFIXES) {
const match = trimmed.match(pattern);
if (!match) continue;
const reason = trimmed.slice(match[0].length).trim();
const idMatch = kind === "duplicate" || kind === "redundant"
? reason.match(/\b(FN-\d+)\b/i)
: null;
return {
kind,
reason,
...(idMatch ? { canonicalId: idMatch[1].toUpperCase() } : {}),
};
}
return null;
}

View File

@@ -156,6 +156,106 @@ describe("FN-4114 fn_task_done invariants", () => {
expect(store.moveTask).toHaveBeenCalledWith("FN-4114", "todo", { preserveProgress: true });
});
it.each([
"NO-OP: existing tests already cover this",
"PREMISE STALE: targeted reproduction already passes unchanged on HEAD",
"DUPLICATE: FN-6239 existing QuickChatFAB tests already cover this",
])("FN-6275 allows verified no-op zero-commit completion with sentinel %s", async (summary) => {
const { store, tool } = await setup({
steps: [
{ name: "Preflight", status: "done" as const },
{ name: "Implement", status: "skipped" as const },
{ name: "Testing & Verification", status: "done" as const },
],
currentStep: 2,
});
mockedExecSync.mockImplementation((cmd: string) => {
if (cmd.includes("rev-parse --show-toplevel")) return Buffer.from("/repo/.worktrees/swift-falcon\n");
if (cmd.includes("rev-parse --abbrev-ref HEAD")) return Buffer.from("fusion/fn-4114\n");
if (cmd.includes("rev-list --count")) return Buffer.from("0\n");
if (cmd.includes("rev-parse HEAD")) return Buffer.from("def456\n");
return Buffer.from("");
});
const result = await tool.execute("id", { summary });
expect(result.content[0].text).toContain("Task marked complete");
expect(result.content[0].text).not.toContain("fn_task_done refused: no_commits");
expect(store.moveTask).not.toHaveBeenCalledWith("FN-4114", "todo", { preserveProgress: true });
expect(store.updateTask).toHaveBeenCalledWith("FN-4114", { noCommitsExpected: true });
expect(store.logEntry).toHaveBeenCalledWith(
"FN-4114",
expect.stringContaining("completion sentinel accepted"),
expect.stringContaining(summary),
undefined,
);
expect(store.recordActivity).toHaveBeenCalledWith(expect.objectContaining({
type: "task:updated",
taskId: "FN-4114",
metadata: expect.objectContaining({ summary }),
}));
});
it("FN-6275 still refuses ordinary zero-commit completion summaries", async () => {
const { store, tool } = await setup({
steps: [{ name: "Implement", status: "done" as const }],
});
mockedExecSync.mockImplementation((cmd: string) => {
if (cmd.includes("rev-parse --show-toplevel")) return Buffer.from("/repo/.worktrees/swift-falcon\n");
if (cmd.includes("rev-parse --abbrev-ref HEAD")) return Buffer.from("fusion/fn-4114\n");
if (cmd.includes("rev-list --count")) return Buffer.from("0\n");
if (cmd.includes("rev-parse HEAD")) return Buffer.from("def456\n");
return Buffer.from("");
});
const result = await tool.execute("id", { summary: "Verified existing behavior with targeted tests." });
expect(result.content[0].text).toContain("fn_task_done refused: no_commits");
expect(store.moveTask).toHaveBeenCalledWith("FN-4114", "todo", { preserveProgress: true });
expect(store.updateTask).not.toHaveBeenCalledWith("FN-4114", { noCommitsExpected: true });
});
it.each([
["wrong_toplevel", "/repo\n", "fusion/fn-4114\n"],
["wrong_branch", "/repo/.worktrees/swift-falcon\n", "main\n"],
] as const)("FN-6275 does not relax %s for sentinel summaries", async (reason, toplevel, branch) => {
const { store, tool } = await setup({ steps: [{ name: "Implement", status: "done" as const }] });
mockedExecSync.mockImplementation((cmd: string) => {
if (cmd.includes("rev-parse --show-toplevel")) return Buffer.from(toplevel);
if (cmd.includes("rev-parse --abbrev-ref HEAD")) return Buffer.from(branch);
if (cmd.includes("rev-list --count")) return Buffer.from("0\n");
if (cmd.includes("rev-parse HEAD")) return Buffer.from("def456\n");
return Buffer.from("");
});
const result = await tool.execute("id", { summary: "NO-OP: already covered" });
expect(result.content[0].text).toContain(`fn_task_done refused: ${reason}`);
expect(store.moveTask).toHaveBeenCalledWith("FN-4114", "todo", { preserveProgress: true });
expect(store.updateTask).not.toHaveBeenCalledWith("FN-4114", { noCommitsExpected: true });
});
it("FN-6275 sentinel summaries do not auto-complete multiple pending unreviewed steps", async () => {
const { store, tool } = await setup({
steps: [
{ name: "Implement", status: "in-progress" as const },
{ name: "Testing", status: "pending" as const },
],
});
mockedExecSync.mockImplementation((cmd: string) => {
if (cmd.includes("rev-parse --show-toplevel")) return Buffer.from("/repo/.worktrees/swift-falcon\n");
if (cmd.includes("rev-parse --abbrev-ref HEAD")) return Buffer.from("fusion/fn-4114\n");
if (cmd.includes("rev-list --count")) return Buffer.from("0\n");
if (cmd.includes("rev-parse HEAD")) return Buffer.from("def456\n");
return Buffer.from("");
});
const result = await tool.execute("id", { summary: "NO-OP: already covered" });
expect(result.content[0].text).toContain("fn_task_done refused (bulk-step-completion-without-review)");
expect(store.moveTask).toHaveBeenCalledWith("FN-4114", "todo", { preserveProgress: true });
});
it("FN-350 allows Review Level 1 coordination completion with zero commits when no source files are scoped", async () => {
const fn350Prompt = `# Task: FN-350 - Route Ready Swift Tasks to Executor Owner

View File

@@ -345,6 +345,7 @@ export function createMockStore() {
updatedAt: new Date().toISOString(),
}),
updateTask: vi.fn().mockResolvedValue({}),
recordActivity: vi.fn().mockResolvedValue({}),
moveTask: vi.fn().mockResolvedValue({}),
handoffToReview: vi.fn().mockImplementation(async (id: string) => store.moveTask(id, "in-review")),
mergeTask: vi.fn().mockResolvedValue({}),

View File

@@ -9,7 +9,7 @@ import { delimiter, isAbsolute, join, relative, resolve as resolvePath } from "n
import { existsSync, realpathSync } from "node:fs";
import { readFile, rm, writeFile } from "node:fs/promises";
import type { TaskStore, Task, TaskDetail, TaskTokenUsage, StepStatus, Settings, WorkflowStep, MissionStore, Slice, AgentState, AgentCapability, RunMutationContext, AgentHeartbeatConfig, Agent, AgentMemoryInclusionMode, ProjectSettings, MergeResult, WorkflowIrNode } from "@fusion/core";
import { RetryStormError, TaskDeletedError, serializeRetryStormError, isExperimentalFeatureEnabled, isWorkflowColumnsEnabled, resolveWorkflowIrForTask, resolveColumnAgentBinding, resolveEffectiveAgent, instanceNodeId, getWorkflowExtensionRegistry, getBuiltinWorkflow } from "@fusion/core";
import { RetryStormError, TaskDeletedError, serializeRetryStormError, isExperimentalFeatureEnabled, isWorkflowColumnsEnabled, resolveWorkflowIrForTask, resolveColumnAgentBinding, resolveEffectiveAgent, instanceNodeId, getWorkflowExtensionRegistry, getBuiltinWorkflow, parseNoOpCompletionMarker } from "@fusion/core";
import { mergeEffectiveSettings } from "./effective-settings.js";
import type { TaskStep, WorkflowIr, WorkflowFieldDefinition, WorkflowColumnAgent, EffectiveAgentInput, WorkflowWorkEngineDispatchResult } from "@fusion/core";
import {
@@ -1109,7 +1109,7 @@ PROMPT.md is captured at task-creation time; HEAD may have moved on since then.
3. Mark every remaining step skipped with a one-line reason: \`fn_task_update(step=N, status="skipped")\`.
4. Call \`fn_task_done\` with a summary that begins \`PREMISE STALE:\` followed by the concrete reason (e.g. \`PREMISE STALE: targeted reproduction passes unchanged on HEAD; PROMPT claimed MOBILE_MEDIA_QUERY had been expanded but useViewportMode.ts:9 still exports the legacy value\`).
This path exists specifically to prevent the executor from looping when PROMPT.md is out of sync with HEAD. Use it only after running the actual reproduction — do not invoke it to dodge real work.
This path exists specifically to prevent the executor from looping when PROMPT.md is out of sync with HEAD. Use it only after running the actual reproduction — do not invoke it to dodge real work. If a task is verified as a no-op, duplicate, or redundant for the same reason (the requested behavior is already present on HEAD), \`fn_task_done\` may also use a leading sentinel summary of \`NO-OP:\`, \`NOOP:\`, \`DUPLICATE: FN-NNNN ...\`, or \`REDUNDANT:\`. These sentinels are audit-logged and allow a verified zero-commit completion; ordinary zero-commit implementation completions without a recognized leading sentinel are still refused.
**Logging important actions:** \`fn_task_log(message="what happened")\`
@@ -9509,6 +9509,7 @@ export class TaskExecutor {
task: Task,
worktreePathOverride?: string,
allowReanchor = true,
options?: { noOpCompletion?: boolean; noOpCompletionReason?: string },
): Promise<{ ok: true } | { ok: false; reason: "wrong_toplevel" | "wrong_branch" | "no_commits"; observed: string; expected: string }> {
const settings = await this.store.getSettings();
const branchName = resolveTaskWorkingBranch(task);
@@ -9572,7 +9573,7 @@ export class TaskExecutor {
executorLog.log(`${task.id}: re-anchored nested task.worktree ${worktreePath} -> ${reanchor.root}`);
await this.store.logEntry(task.id, `Re-anchored nested task.worktree from ${worktreePath} to ${reanchor.root}`, undefined, this.getRunContextFor(task.id));
await this.emitWorktreeReanchoredAudit(task.id, worktreePath, reanchor.root, "verify-worktree-invariants");
return this.verifyWorktreeInvariants(task, reanchor.root, false);
return this.verifyWorktreeInvariants(task, reanchor.root, false, options);
}
}
return {
@@ -9649,6 +9650,9 @@ export class TaskExecutor {
);
const noCommitEligibilityReason =
getNoCommitEligibilityReason(task) ??
(options?.noOpCompletion
? options.noOpCompletionReason ?? "verified no-op/duplicate completion sentinel"
: null) ??
(promptDerivedEligibility.eligible
? promptDerivedEligibility.reason ?? "prompt-derived no-commit eligibility"
: null);
@@ -9878,7 +9882,13 @@ export class TaskExecutor {
};
}
const invariantCheck = await this.verifyWorktreeInvariants(task, worktreePath);
const noOpMarker = parseNoOpCompletionMarker(params.summary);
const invariantCheck = await this.verifyWorktreeInvariants(task, worktreePath, true, {
noOpCompletion: Boolean(noOpMarker),
noOpCompletionReason: noOpMarker
? `verified ${noOpMarker.kind} completion sentinel${noOpMarker.canonicalId ? ` (${noOpMarker.canonicalId})` : ""}`
: undefined,
});
if (!invariantCheck.ok) {
const refusalMessage = `fn_task_done refused: ${invariantCheck.reason} — observed=${invariantCheck.observed}, expected=${invariantCheck.expected}`;
await store.logEntry(taskId, refusalMessage, undefined, this.getRunContextFor(task.id));
@@ -10015,6 +10025,52 @@ export class TaskExecutor {
};
}
if (noOpMarker) {
const runContext = this.getRunContextFor(taskId);
await store.updateTask(taskId, { noCommitsExpected: true });
await store.logEntry(
taskId,
`Verified ${noOpMarker.kind} completion sentinel accepted; no commits expected for terminal handoff`,
JSON.stringify({
kind: noOpMarker.kind,
reason: noOpMarker.reason,
canonicalId: noOpMarker.canonicalId,
summary: params.summary,
runId: runContext?.runId,
agentId: runContext?.agentId,
}),
runContext,
);
const recordActivity = (store as typeof store & {
recordActivity?: (entry: {
type: "task:updated";
taskId: string;
taskTitle?: string;
details: string;
metadata?: Record<string, unknown>;
}) => Promise<unknown>;
}).recordActivity;
if (recordActivity) {
await recordActivity.call(store, {
type: "task:updated",
taskId,
taskTitle: task.title,
details: `Task marked as verified ${noOpMarker.kind}; no commits expected`,
metadata: {
taskId,
kind: noOpMarker.kind,
reason: noOpMarker.reason,
canonicalId: noOpMarker.canonicalId,
summary: params.summary,
runId: runContext?.runId,
agentId: runContext?.agentId,
},
}).catch((error: unknown) => {
executorLog.warn(`${taskId}: failed to record no-op completion activity: ${error instanceof Error ? error.message : String(error)}`);
});
}
}
onDone();
// Mark all pending/in-progress steps as done