Skip to content

feat(workflow): expose task.start/task.status as MCP tools - #64

Closed
yohnark wants to merge 2 commits into
mainfrom
claude/task-lifecycle-impl-sgyneu
Closed

yohnark wants to merge 2 commits into
mainfrom
claude/task-lifecycle-impl-sgyneu

Conversation

@yohnark

@yohnark yohnark commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Summary

Exposes policy explain, task start, and task status as MCP tools and CLI commands, wiring the existing task-lifecycle domain layer (src/workflow/domain/task.ts, policy resolution, WorkflowSqliteStateStore) to a new src/workflow/commands/mcp-tools.ts module and to src/cli.ts, gated behind opt-in gateway.workflowTasks.

Linked issue

Closes #34.

An earlier revision of this PR shipped mottainai_task_start/mottainai_task_status directly in src/local-tools.ts without policy explain, without CLI commands, and without deprecating mottainai_worktree_new — flagged in review as not fully matching #34's acceptance criteria. This revision fills those gaps; see Scope and Implementation below for the one deliberate, documented scope decision that remains (how policy explain handles non-RuleMode fields).

Scope

Included

  • New src/workflow/commands/mcp-tools.ts: mottainai_workflow_policy_explain, mottainai_workflow_task_start, mottainai_workflow_task_status, following the Tool[] + dispatch-function pattern from src/local-tools.ts. Wired into src/proxy.ts's tool listing, dispatch, and risk lookup alongside the existing adaptive/broker/codeSearch tool families — its own family, since it has its own gating (config.workflowTasks) and its own state dependency (WorkflowStateStore).
  • mottainai_workflow_policy_explain: resolves the effective policy for the workspace and, for every genuine RuleMode field (protectedBranchRule.*; worktree.required/issueRequired/multipleActiveTasksPerIssue/multipleWorktreesPerTask/staleBaseBranch; cleanup.*), returns the full resolve.ts ResolvedRule — value, mode, authority (preset vs. repository), and weakening permission. When .mottainai/workflow.json declares a preset, that preset's values become the "preset" authority and the file's own values become "repository", so a declared preset's enforce rule can't be silently weakened by editing the file (no humanApproval channel exists in the file schema yet, so any such weakening attempt is rejected and the preset's stronger mode wins — this is resolveRule()'s existing, tested behavior, now actually exercised).
  • mottainai_workflow_task_start/_status: same behavior as before — task_start never passes skipWorktree, so it always creates a dedicated worktree/branch off the current branch; task_status resolves the active task for the calling worktree without a task id, with no side effects. (Renamed from the earlier mottainai_task_start/mottainai_task_status, which were removed from src/local-tools.ts.)
  • src/cli.ts: mottainai policy explain [--workspace path], mottainai task start <slug> [--issue ref] [--workspace path], mottainai task status [--workspace path]. These share the same default state DB as the MCP tools (same resolveStateDbPath()), so a task started via CLI is visible via MCP task_status and vice versa.
  • mottainai_worktree_new (src/local-tools.ts) is annotated as deprecated in its description and with a doc comment, pointing at mottainai_workflow_task_start. Not removed, not behaviorally changed — same input schema, same annotations, same implementation.
  • resolveEffectiveWorkflowPolicy() (src/workflow/policy/load.ts) centralizes "load .mottainai/workflow.json, fall back to the built-in standard preset if missing, fail closed on corruption" for task_start/task_status/CLI.
  • Domain-layer addition carried over from the prior revision: startTask rejects starting a second task from inside a worktree that already hosts an active task (active-task-in-workspace), which is also this PR's policy-driven rejection test path.

Excluded / scope decisions

  • policy explain's ResolvedRule output covers only fields that are actually RuleMode-shaped in src/workflow/policy/schema.ts. protectedBranches (string[]), controlPlaneRole, stagingMode, and worktree.bootstrapMode have no associated mode field in the schema at all (confirmed by reading resolve.test.ts and schema.ts together — the ResolvedPolicy mapped type in resolve.ts is typed generically enough to wrap these too, but there is no existing builder or schema support for what "mode" would even mean for e.g. stagingMode). Rather than fabricate a mode for these, they're returned as plain descriptive values, documented in src/workflow/policy/explain.ts's module comment. Extending the schema itself to carry authority/weakening for these fields is left to Child Issue 9a-1, as the original Issue anticipated ("Child Issue 9a-1 later extends these same three tools to their final full spec").
  • task_start/task_status still consult the plain effective policy document (resolveEffectiveWorkflowPolicy), not the authority-resolved view policy explain shows — task-start enforcement does not yet route through resolveRule(). This isn't a regression introduced here (startTask never consulted resolve.ts, going back to Issue feat: add Issue-bound task and worktree lifecycle #33); policy explain is simply the first real caller of that resolution engine. Noted in docs/workflow-policy.md.

Implementation

  • src/workflow/commands/mcp-tools.ts: the three tools, a lazily dynamic-imported default WorkflowStateStore singleton (tests inject their own), TASK_SLUG_PATTERN/ISSUE_REF_PATTERN boundary validation.
  • src/workflow/policy/explain.ts: explainWorkflowPolicy(), builds PolicySource<RuleMode>[] per rule field from the preset/repository documents and calls resolveRule().
  • src/workflow/policy/load.ts: resolveEffectiveWorkflowPolicy().
  • src/proxy.ts: isWorkflowCommand flag threaded through authorize/dispatch/withRequestId/gatewayToolRisk, parallel to the existing isLocal/isAdaptive flags; workflowCommandToolsFor(gatewayConfig) added to the ListTools response.
  • src/cli.ts: resolveWorkflowWorkspace() (defaults to --workspace, then the current Git repo's top level, then cwd) and openWorkflowStateStore() (dynamic import, same reasoning as the MCP side).
  • src/local-tools.ts: reverted the mottainai_task_start/mottainai_task_status addition (moved to the new module); mottainai_worktree_new's deprecation notice is the only remaining change there.

Behavioral changes

  • New opt-in config: gateway.workflowTasks (default false; unchanged for existing configs). When enabled: three new MCP tools, three new CLI commands, none of which change any existing tool/command's behavior.
  • mottainai_worktree_new's tool description now starts with "Deprecated: superseded by mottainai_workflow_task_start." — same schema, same behavior, purely descriptive.

Validation

  • Typecheck — pnpm run typecheck
  • Tests — pnpm test (705 pass): unit coverage for explainWorkflowPolicy (preset-only, repository-only, preset-declared strengthening/weakening, corrupted file), resolveEffectiveWorkflowPolicy, the new mcp-tools.ts dispatch (gating, each tool's happy path, the active-task-in-workspace policy-driven rejection, boundary validation), a proxy.ts end-to-end test (listTools/callTool through the real MCP Client/Server, gated listing), and CLI subprocess tests (policy explain, task start/status round-trip across two invocations, the same-worktree rejection, corrupted-policy fail-closed).
  • Build — pnpm run build
  • Package check — src/cli.ts is a packageCheckPaths match; deferring to CI's Package check job (not independently reproducible locally beyond typecheck/test/build, which all pass).

pnpm run lint, pnpm run architecture:check (73 production files), and pnpm run format:check also pass.

Risks

  • mottainai_worktree_new's deprecation is description-only; nothing currently enforces migration off it, and it is not scheduled for removal by this PR.
  • No-worktree (skipWorktree) tasks remain outside active-task-in-workspace's detection, since TaskRecord doesn't carry a physical location for that mode. Not reachable via either the MCP tools or the CLI commands added here (both always create a dedicated worktree), but noted for future work if that mode is ever exposed.
  • policy explain's authority resolution is display-only for now (see Excluded); a user reading policy explain might reasonably assume it reflects what task_start will enforce, which isn't yet true. Documented in docs/workflow-policy.md to avoid that misreading.

Breaking changes

No.

Migration / compatibility

None required — workflowTasks defaults to false.

Security impact

mottainai_workflow_task_start and the CLI's task start mutate git state (worktree creation) behind opt-in config, same posture as mottainai_worktree_new. mottainai_workflow_policy_explain and task status are read-only. Input (taskSlug, issueRef) is validated against fixed patterns before being interpolated into branch/path names; all git invocations use argv-array subprocess helpers (no shell interpolation).

Review focus

  • Whether scoping policy explain's ResolvedRule output to genuine RuleMode fields (rather than fabricating modes for protectedBranches/controlPlaneRole/stagingMode/bootstrapMode) is an acceptable reading of the acceptance criterion, or whether that should block this PR pending a schema extension.
  • Tool/CLI naming: mottainai_workflow_policy_explain/_task_start/_task_status and policy explain/task start/task status.

Wire the existing Issue #28/#33 task-lifecycle domain layer
(src/workflow/domain/task.ts, policy, sqlite store) to MCP as
mottainai_task_start and mottainai_task_status, gated behind a new
opt-in gateway.workflowTasks config flag (default false, mirroring
mottainai_worktree_new's gating).

- mottainai_task_start never passes skipWorktree, so it always
  reserves a dedicated worktree/branch off the current branch
  regardless of policy — main/default branch can never become the
  work branch itself.
- mottainai_task_status resolves the active task (if any) for the
  calling worktree without needing a task id, with no side effects.
- Added startTask's missing invariant: reject starting a task from a
  worktree that already hosts another active task
  (active-task-in-workspace), needed once callers can start tasks
  from inside worktrees they didn't create themselves.
- Corrupted/invalid .mottainai/workflow.json fails closed at the tool
  boundary instead of silently falling back to a preset; a missing
  file still falls back to the standard preset per existing
  documented behavior.
- WorkflowSqliteStateStore is loaded via dynamic import only when the
  tools actually run, so workspaces that don't opt in never pay the
  node:sqlite experimental-warning cost on every CLI invocation.
@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added optional workflow task support for local tools.
    • Users can start tasks in dedicated worktrees and check task status, repository details, and warnings.
    • Workflow task tools are available only when explicitly enabled.
  • Bug Fixes

    • Prevented multiple active tasks from using the same workspace.
    • Added validation for task and issue identifiers and invalid workflow policies.
    • Added safeguards for nested tasks and unsupported repository states.
  • Configuration

    • Workflow tasks are disabled by default and accept only boolean values.

Walkthrough

Adds the gateway.workflowTasks setting and local tools for workflow task start and status operations. Adds workspace-level task detection, nested-task prevention, workflow policy validation, dedicated worktree creation, and SQLite-backed lifecycle tests.

Changes

Workflow task lifecycle

Layer / File(s) Summary
Workflow task configuration
src/config.ts, src/config.test.ts, src/code-search.test.ts
Adds optional and resolved workflowTasks configuration. The value defaults to false, accepts booleans, and rejects other types.
Workspace task guards and status
src/workflow/domain/task.ts, src/workflow/domain/task.test.ts
Prevents nested tasks in active worktrees. Adds workspace status reporting for repository identity, worktree state, warnings, and active tasks.
Local workflow task tools
src/local-tools.ts, src/local-tools.test.ts
Conditionally exposes mottainai_task_start and mottainai_task_status. Adds policy resolution, task validation, task execution, status dispatch, and lifecycle coverage.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant LocalTools
  participant WorkflowDomain
  participant WorkflowSqliteStateStore
  Client->>LocalTools: mottainai_task_start
  LocalTools->>WorkflowDomain: validate and start task
  WorkflowDomain->>WorkflowSqliteStateStore: read active tasks and persist task
  WorkflowDomain-->>LocalTools: task and worktree data
  LocalTools-->>Client: structured task response
Loading

Possibly related PRs

  • yohn-jp/mottainai#1: Provides the SQLite and state-store foundation used by workflow task lifecycle handling.
  • yohn-jp/mottainai#44: Provides workflow policy schemas, presets, and resolution APIs consumed during task start.
  • yohn-jp/mottainai#57: Provides task and worktree domain services wired into the local tools.

Suggested reviewers: claude

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed The description clearly explains the workflow MCP tools, CLI commands, configuration gating, implementation, validation, risks, and scope decisions.
Title check ✅ Passed The title accurately identifies the primary workflow task MCP tool changes, although the pull request also adds policy and CLI functionality.
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/task-lifecycle-impl-sgyneu

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (5)
src/workflow/domain/task.test.ts (1)

263-263: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use a substring assertion instead of a regex built from a variable.

outer.task.taskId is a store-generated identifier, so the ReDoS warning from static analysis is a false positive. A plain substring check is still clearer and removes the warning.

♻️ Proposed change
-  assert.match(inner.detail, new RegExp(outer.task.taskId));
+  assert.ok(inner.detail.includes(outer.task.taskId));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/workflow/domain/task.test.ts` at line 263, Replace the regex-based
assertion involving the store-generated outer.task.taskId with a plain substring
assertion, while preserving the expected "active-task-in-workspace" reason
validation in the surrounding test.

Source: Linters/SAST tools

src/local-tools.ts (1)

589-589: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

ISSUE_REF_PATTERN allows .., which git rejects later in the flow.

The pattern permits values such as 7..9. issueRef flows into the branch name through buildWorktreeNaming, and git refuses a ref that contains ... The failure surfaces as git-worktree-add-failed after the task and worktree rows are reserved and rolled back, instead of as a direct validation error.

Rejecting .. at the boundary gives a clearer message and avoids the reserve-then-roll-back cycle.

♻️ Proposed change
-const ISSUE_REF_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
+// `..` を除外する — issueRef は branch 名に入るため、git が ref として拒否する値を
+// 予約前に弾く。
+const ISSUE_REF_PATTERN = /^[A-Za-z0-9](?!.*\.\.)[A-Za-z0-9._-]*$/;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/local-tools.ts` at line 589, Update ISSUE_REF_PATTERN validation to
reject any issueRef containing consecutive dots, while preserving the existing
allowed-character rules. Ensure this boundary validation runs before
buildWorktreeNaming and worktree/task reservation so values such as 7..9 produce
the direct validation error.
src/workflow/domain/task.ts (2)

150-153: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

The task === undefined fallback makes the fail-closed branch in getTaskStatusForWorkspace unreachable.

findActiveTaskAtWorktreePath returns undefined when an active worktree row references a task that getTask cannot resolve. Both call sites then treat that as "no active task here":

  • startTask (Line 176) lets a new task start on a physical worktree that still has an active worktree row.
  • getTaskStatusForWorkspace (Line 412) returns active: false, so the explicit error at Line 418 (references task ... which is missing from the store) can never run.

Consider returning the dangling worktree so callers can decide. The store enforces a FK from worktrees to tasks, so this state should not occur; making it visible instead of silent matches the fail-closed intent stated in the function comment.

♻️ Proposed change
-): { task: TaskRecord; worktree: WorktreeRecord } | undefined {
+): { task: TaskRecord | undefined; worktree: WorktreeRecord } | undefined {
   const worktree = store
     .listWorktreesForInstance(instanceId)
     .find((candidate) => candidate.status === "active" && candidate.canonicalPath === canonicalWorktreePath);
   if (worktree === undefined) return undefined;
-  const task = store.getTask(worktree.taskId);
-  if (task === undefined) return undefined;
-  return { task, worktree };
+  return { task: store.getTask(worktree.taskId), worktree };
 }

Callers then handle task === undefined explicitly: startTask rejects with active-task-in-workspace, and getTaskStatusForWorkspace returns its existing ok: false reason.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/workflow/domain/task.ts` around lines 150 - 153, Update
findActiveTaskAtWorktreePath to return the active worktree alongside an
undefined task instead of returning undefined when store.getTask cannot resolve
the task. Then handle task === undefined explicitly in startTask by rejecting
with active-task-in-workspace, while preserving getTaskStatusForWorkspace’s
existing missing-task error response and fail-closed behavior.

175-182: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚖️ Poor tradeoff

The guard reads the store outside the reservation transaction.

findActiveTaskAtWorktreePath runs here, and reserveTask / reserveWorktree run later in separate transactions. Two concurrent startTask calls with the same workspaceRoot and different taskSlug values can both pass this guard. Their candidateCanonicalPath values differ, so the UNIQUE partial index on canonical_path does not reject the second one. Both tasks then become active for the same physical worktree, which is the exact state this guard prevents.

The window is small and the tools are opt-in, so this may be acceptable for now. If you want the guarantee to hold under concurrency, the check must run inside the same BEGIN IMMEDIATE transaction as reserveTask, or the schema needs a uniqueness constraint on the active task per repository instance path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/workflow/domain/task.ts` around lines 175 - 182, Move the active-worktree
conflict check from the pre-reservation flow into the same BEGIN IMMEDIATE
transaction used by reserveTask and reserveWorktree, ensuring the check and
reservations are atomic. Update startTask and the reservation transaction path
around findActiveTaskAtWorktreePath so concurrent calls for the same physical
worktree cannot both proceed; preserve the existing conflict response.
src/config.ts (1)

306-308: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider an optionalBoolean helper to match the neighboring validation style.

Every other field in normalizeGateway uses a small validator helper (optionalString, positiveIntegerConfig, stringArrayRecord). The workflowTasks field uses an inline guard plus an as boolean | undefined cast. A helper removes the cast and keeps the function uniform.

♻️ Proposed refactor
-  if (value.workflowTasks !== undefined && typeof value.workflowTasks !== "boolean") {
-    throw new Error("invalid gateway workflowTasks");
-  }
   return {
     workspaceRoot,
@@
-    workflowTasks: value.workflowTasks as boolean | undefined,
+    workflowTasks: optionalBoolean(value.workflowTasks, "invalid gateway workflowTasks"),
   };

Add the helper next to optionalString:

function optionalBoolean(value: unknown, message: string): boolean | undefined {
  if (value === undefined) return undefined;
  if (typeof value !== "boolean") throw new Error(message);
  return value;
}

Also applies to: 323-323

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/config.ts` around lines 306 - 308, Refactor normalizeGateway to add an
optionalBoolean helper alongside optionalString, validating undefined or boolean
values and throwing the supplied message otherwise. Replace the inline
workflowTasks type guard and boolean cast with this helper, preserving the
existing error message and behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/local-tools.test.ts`:
- Around line 526-529: Assert that the initial `mottainai_task_start` result in
the test succeeds before accessing `started.worktree`; validate its success
status and fail immediately with the returned failure details when it is not
successful. Only then cast or read the worktree and pass its canonical path to
the second call.

In `@src/local-tools.ts`:
- Around line 145-146: In src/local-tools.ts lines 145-146, update the
mottainai_task_start and mottainai_task_status arms in callLocalTool to check
config.workflowTasks and throw before resolving workflowStore or calling
defaultWorkflowStore, while retaining the existing guards in taskStartToolImpl
and taskStatusToolImpl. In src/local-tools.test.ts lines 437-455, pass an
explicit openWorkflowStore() instance to both disabled-feature tests so they
never open the default state database.
- Around line 163-172: Replace the resolved-store cache used by
defaultWorkflowStore with a cached initialization promise so concurrent callers
share one import, WorkflowSqliteStateStore construction, and init sequence.
Return the promise result while preserving the existing store behavior, and
reset the cached promise to undefined when initialization rejects so later calls
can retry.
- Line 103: Update the tool annotation for mottainai_task_start by setting
openWorldHint to true, while preserving the existing values for readOnlyHint,
destructiveHint, and idempotentHint.

In `@src/workflow/domain/task.test.ts`:
- Around line 357-362: Register store1 cleanup with t.after immediately after
store1.init(), matching the existing store2 pattern, so it runs even when
startTask returns early; then remove the later direct store1.close() or avoid
duplicate cleanup. Confirm WorkflowSqliteStateStore.close() is idempotent before
relying on the callback.

---

Nitpick comments:
In `@src/config.ts`:
- Around line 306-308: Refactor normalizeGateway to add an optionalBoolean
helper alongside optionalString, validating undefined or boolean values and
throwing the supplied message otherwise. Replace the inline workflowTasks type
guard and boolean cast with this helper, preserving the existing error message
and behavior.

In `@src/local-tools.ts`:
- Line 589: Update ISSUE_REF_PATTERN validation to reject any issueRef
containing consecutive dots, while preserving the existing allowed-character
rules. Ensure this boundary validation runs before buildWorktreeNaming and
worktree/task reservation so values such as 7..9 produce the direct validation
error.

In `@src/workflow/domain/task.test.ts`:
- Line 263: Replace the regex-based assertion involving the store-generated
outer.task.taskId with a plain substring assertion, while preserving the
expected "active-task-in-workspace" reason validation in the surrounding test.

In `@src/workflow/domain/task.ts`:
- Around line 150-153: Update findActiveTaskAtWorktreePath to return the active
worktree alongside an undefined task instead of returning undefined when
store.getTask cannot resolve the task. Then handle task === undefined explicitly
in startTask by rejecting with active-task-in-workspace, while preserving
getTaskStatusForWorkspace’s existing missing-task error response and fail-closed
behavior.
- Around line 175-182: Move the active-worktree conflict check from the
pre-reservation flow into the same BEGIN IMMEDIATE transaction used by
reserveTask and reserveWorktree, ensuring the check and reservations are atomic.
Update startTask and the reservation transaction path around
findActiveTaskAtWorktreePath so concurrent calls for the same physical worktree
cannot both proceed; preserve the existing conflict response.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 19c4543a-a905-45d8-bb6d-07a98de84072

📥 Commits

Reviewing files that changed from the base of the PR and between 3e78cd4 and 74d95e4.

📒 Files selected for processing (7)
  • src/code-search.test.ts
  • src/config.test.ts
  • src/config.ts
  • src/local-tools.test.ts
  • src/local-tools.ts
  • src/workflow/domain/task.test.ts
  • src/workflow/domain/task.ts

Comment thread src/local-tools.test.ts Outdated
Comment on lines +526 to +529
const started = structured(await callLocalTool(
"mottainai_task_start", { taskSlug: "outer" }, workflowConfig(config), store, undefined, undefined, wfStore,
));
const worktree = started.worktree as { canonicalPath: string };

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Assert that the first task_start succeeded before reading its worktree.

The cast on Line 529 hides a failure. If task_start returns status: "failed", started.worktree is undefined, and Line 532 passes workspaceRoot: undefined into the second call. The test then fails somewhere downstream with an unrelated message instead of at the real cause.

💚 Proposed fix
   const started = structured(await callLocalTool(
     "mottainai_task_start", { taskSlug: "outer" }, workflowConfig(config), store, undefined, undefined, wfStore,
   ));
+  assert.equal(started.status, "success");
   const worktree = started.worktree as { canonicalPath: string };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const started = structured(await callLocalTool(
"mottainai_task_start", { taskSlug: "outer" }, workflowConfig(config), store, undefined, undefined, wfStore,
));
const worktree = started.worktree as { canonicalPath: string };
const started = structured(await callLocalTool(
"mottainai_task_start", { taskSlug: "outer" }, workflowConfig(config), store, undefined, undefined, wfStore,
));
assert.equal(started.status, "success");
const worktree = started.worktree as { canonicalPath: string };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/local-tools.test.ts` around lines 526 - 529, Assert that the initial
`mottainai_task_start` result in the test succeeds before accessing
`started.worktree`; validate its success status and fail immediately with the
returned failure details when it is not successful. Only then cast or read the
worktree and pass its canonical path to the second call.

Comment thread src/local-tools.ts Outdated
inputSchema: { type: "object", properties: {
taskSlug: { type: "string" }, issueRef: { type: "string" },
}, required: ["taskSlug"] }, outputSchema: OUTPUT_SCHEMA,
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Compare annotations across worktree-creating local tools.
rg -n -B4 -A2 'annotations:' src/local-tools.ts

# Inspect the remote-facing paths reached by startTask.
ast-grep run --pattern 'async function checkStaleBaseBranch($$$) { $$$ }' --lang typescript src/workflow/domain/task.ts
ast-grep run --pattern 'async function runBootstrap($$$) { $$$ }' --lang typescript
rg -n 'ls-remote|fetch|origin/' src/workflow/domain/task.ts

Repository: yohn-jp/mottainai

Length of output: 5576


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- task.ts outline ---'
ast-grep outline src/workflow/domain/task.ts
printf '%s\n' '--- startTask and bootstrap call sites ---'
rg -n -B8 -A20 'startTask|runBootstrap|checkStaleBaseBranch|bootstrapMode' src/workflow/domain/task.ts src
printf '%s\n' '--- local-tools task registration and handler ---'
cat -n src/local-tools.ts | sed -n '75,115p'
rg -n -B10 -A35 'mottainai_task_start|taskStartTool|startTask\(' src

Repository: yohn-jp/mottainai

Length of output: 50374


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- relevant policy defaults and tool implementation ---'
rg -n -B8 -A18 'bootstrapMode|standard|taskStartTool|mottainai_task_start|worktreeNewTool' src/workflow/policy src/local-tools.ts
printf '%s\n' '--- exact task-start annotation and handler ---'
sed -n '95,107p' src/local-tools.ts
sed -n '585,635p' src/local-tools.ts
printf '%s\n' '--- exact bootstrap implementation ---'
sed -n '74,153p' src/workflow/git/worktree.ts
printf '%s\n' '--- deterministic bootstrap reachability check ---'
python3 - <<'PY'
from pathlib import Path
text = Path("src/workflow/git/worktree.ts").read_text()
assert 'const BOOTSTRAP_COMMAND = "pnpm install --frozen-lockfile";' in text
assert 'if (mode === "automatic")' in text
assert 'return { mode, command: BOOTSTRAP_COMMAND, shouldExecute: true' in text
assert 'runProgram(program, args, worktreePath' in text
print("automatic bootstrap executes:", "pnpm install --frozen-lockfile")
print("execution environment:", "PATH, HOME, LANG, TMPDIR, NODE_ENV allowlist")
PY

Repository: yohn-jp/mottainai

Length of output: 31874


Set openWorldHint to true for mottainai_task_start.

checkStaleBaseBranch reads only local tracking refs. However, bootstrapMode: "automatic" runs pnpm install --frozen-lockfile, which can contact package registries.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/local-tools.ts` at line 103, Update the tool annotation for
mottainai_task_start by setting openWorldHint to true, while preserving the
existing values for readOnlyHint, destructiveHint, and idempotentHint.

Source: Coding guidelines

Comment thread src/local-tools.ts Outdated
Comment thread src/local-tools.ts Outdated
Comment on lines +163 to +172
let defaultWorkflowStoreInstance: WorkflowStateStore | undefined;
async function defaultWorkflowStore(): Promise<WorkflowStateStore> {
if (defaultWorkflowStoreInstance === undefined) {
const { WorkflowSqliteStateStore } = await import("./workflow/state/sqlite-store.js");
const created = new WorkflowSqliteStateStore();
created.init();
defaultWorkflowStoreInstance = created;
}
return defaultWorkflowStoreInstance;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Cache the promise, not the resolved store, to avoid a double-initialization race.

defaultWorkflowStore is async, and it assigns defaultWorkflowStoreInstance only after two await points. Two concurrent mottainai_task_start / mottainai_task_status calls both observe undefined, both run await import(...), and both call new WorkflowSqliteStateStore() and created.init(). The second assignment overwrites the first. The first store keeps an open SQLite handle plus its WAL and SHM files, and nothing ever closes it.

The MCP server can dispatch tool calls concurrently, so this path is reachable. Cache the in-flight promise so initialization happens exactly once.

🔒️ Proposed fix
-let defaultWorkflowStoreInstance: WorkflowStateStore | undefined;
-async function defaultWorkflowStore(): Promise<WorkflowStateStore> {
-  if (defaultWorkflowStoreInstance === undefined) {
-    const { WorkflowSqliteStateStore } = await import("./workflow/state/sqlite-store.js");
-    const created = new WorkflowSqliteStateStore();
-    created.init();
-    defaultWorkflowStoreInstance = created;
-  }
-  return defaultWorkflowStoreInstance;
-}
+let defaultWorkflowStorePromise: Promise<WorkflowStateStore> | undefined;
+function defaultWorkflowStore(): Promise<WorkflowStateStore> {
+  // 生成済みインスタンスではなく生成中の promise を保持する — await の間に
+  // 同時呼び出しが入ると store が二重に init() され、先に作った SQLite handle が
+  // close されないまま漏れるため。
+  defaultWorkflowStorePromise ??= (async () => {
+    const { WorkflowSqliteStateStore } = await import("./workflow/state/sqlite-store.js");
+    const created = new WorkflowSqliteStateStore();
+    created.init();
+    return created;
+  })();
+  return defaultWorkflowStorePromise;
+}

If init() throws, reset defaultWorkflowStorePromise to undefined in a .catch so a later call can retry.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let defaultWorkflowStoreInstance: WorkflowStateStore | undefined;
async function defaultWorkflowStore(): Promise<WorkflowStateStore> {
if (defaultWorkflowStoreInstance === undefined) {
const { WorkflowSqliteStateStore } = await import("./workflow/state/sqlite-store.js");
const created = new WorkflowSqliteStateStore();
created.init();
defaultWorkflowStoreInstance = created;
}
return defaultWorkflowStoreInstance;
}
let defaultWorkflowStorePromise: Promise<WorkflowStateStore> | undefined;
function defaultWorkflowStore(): Promise<WorkflowStateStore> {
// 生成済みインスタンスではなく生成中の promise を保持する — await の間に
// 同時呼び出しが入ると store が二重に init() され、先に作った SQLite handle が
// close されないまま漏れるため。
defaultWorkflowStorePromise ??= (async () => {
const { WorkflowSqliteStateStore } = await import("./workflow/state/sqlite-store.js");
const created = new WorkflowSqliteStateStore();
created.init();
return created;
})();
return defaultWorkflowStorePromise;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/local-tools.ts` around lines 163 - 172, Replace the resolved-store cache
used by defaultWorkflowStore with a cached initialization promise so concurrent
callers share one import, WorkflowSqliteStateStore construction, and init
sequence. Return the promise result while preserving the existing store
behavior, and reset the cached promise to undefined when initialization rejects
so later calls can retry.

Comment on lines +357 to +362
const store1 = new WorkflowSqliteStateStore({ dbPath });
store1.init();
const started = await startTask({ workspaceRoot: root, store: store1, policy: standardPolicy(), taskSlug: "restart-check" });
assert.equal(started.ok, true);
if (!started.ok) return;
store1.close();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Register the store1 cleanup before the early return.

If startTask fails, Line 361 returns before store1.close() on Line 362. The SQLite handle and its WAL files then stay open for the rest of the test process. Register the close with t.after right after init(), the same way the test already does for store2.

🛡️ Proposed fix
   const store1 = new WorkflowSqliteStateStore({ dbPath });
   store1.init();
+  t.after(() => store1.close());
   const started = await startTask({ workspaceRoot: root, store: store1, policy: standardPolicy(), taskSlug: "restart-check" });
   assert.equal(started.ok, true);
   if (!started.ok) return;
   store1.close();

WorkflowSqliteStateStore.close() must tolerate a second call for this to be safe. Confirm that before applying.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/workflow/domain/task.test.ts` around lines 357 - 362, Register store1
cleanup with t.after immediately after store1.init(), matching the existing
store2 pattern, so it runs even when startTask returns early; then remove the
later direct store1.close() or avoid duplicate cleanup. Confirm
WorkflowSqliteStateStore.close() is idempotent before relying on the callback.

…, new module, deprecation, CLI)

Fills in the gaps flagged on PR #64 against Issue #34's actual acceptance
criteria: the previous mottainai_task_start/task_status lived directly in
src/local-tools.ts and had no policy-explain counterpart or CLI parity.

- New src/workflow/commands/mcp-tools.ts exposes mottainai_workflow_policy_explain,
  mottainai_workflow_task_start, and mottainai_workflow_task_status as their own
  Tool[] + dispatch family (mirroring src/local-tools.ts's pattern), replacing
  the earlier mottainai_task_start/task_status pair removed from local-tools.ts.
  Wired into src/proxy.ts's tool listing/dispatch/risk-lookup alongside the
  existing adaptive/broker/codeSearch tool families, gated by gateway.workflowTasks.
- mottainai_workflow_policy_explain resolves the genuine RuleMode fields
  (protectedBranchRule.*, worktree.{required,issueRequired,
  multipleActiveTasksPerIssue,multipleWorktreesPerTask,staleBaseBranch},
  cleanup.*) through resolve.ts's resolveRule(), showing value/mode/authority/
  weakening per rule (src/workflow/policy/explain.ts). protectedBranches,
  controlPlaneRole, stagingMode, and worktree.bootstrapMode are returned as
  plain descriptive values since the schema has no associated mode for them.
- src/cli.ts gains `policy explain`, `task start`, and `task status` commands,
  sharing the same default state DB as the MCP tools so CLI- and MCP-started
  tasks are visible to each other.
- mottainai_worktree_new is annotated as deprecated in favor of
  mottainai_workflow_task_start, unchanged in behavior.
- resolveEffectiveWorkflowPolicy() (src/workflow/policy/load.ts) centralizes
  the "load .mottainai/workflow.json, fall back to the standard preset,
  fail closed on corruption" logic shared by task start/status and the CLI.

yohnark commented Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

Closing — this PR's branch (claude/task-lifecycle-impl-sgyneu) doesn't match this repository's required type/123-short-description branch format (docs/governance.md), and Governance / validate-pr fails on it (branch name format is invalid).

Replaced by #69, rebuilt from the same diff on a correctly named branch (feat/34-workflow-task-tools) off current main, with all actionable CodeRabbit findings from this PR's review addressed and Governance / validate-pr/CI green.


Generated by Claude Code

@yohnark yohnark closed this Aug 7, 2026
@yohnark
yohnark deleted the claude/task-lifecycle-impl-sgyneu branch August 10, 2026 10:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: expose policy explain and task start/status early for dogfooding

2 participants