Skip to content
Hive

Task changes

done

hive · create-and-ship-a-first-260719-7fa2

← Back to task

Available

Diff available. Open the public pull request for the complete change.

Committed changes

diff --git a/.github/workflows/live-agent-skills.yml b/.github/workflows/live-agent-skills.yml
index c0ab06b1b9..b67698f703 100644
--- a/.github/workflows/live-agent-skills.yml
+++ b/.github/workflows/live-agent-skills.yml
@@ -194,10 +194,72 @@ jobs:
           if-no-files-found: error
           retention-days: 7
 
+  live-workflow-creator:
+    name: OpenClaw live workflow-creator skill
+    runs-on: ubuntu-24.04
+    needs: [validate, build]
+    timeout-minutes: 25
+    environment: live-agent-skills-openclaw
+    permissions:
+      actions: read
+      contents: read
+    steps:
+      - uses: actions/download-artifact@v8
+        with:
+          name: live-agent-candidate-artifacts-${{ github.run_attempt }}
+          path: candidate-artifacts
+      - name: Extract the exact candidate source
+        env:
+          CANDIDATE_SHA: ${{ needs.validate.outputs.candidate_sha }}
+        run: |
+          set -euo pipefail
+          mkdir -p candidate-source
+          tar -xzf "candidate-artifacts/hive-source-${CANDIDATE_SHA}.tar.gz" -C candidate-source
+      - uses: ruby/setup-ruby@v1
+        with:
+          ruby-version: "3.4"
+      - uses: actions/setup-node@v6
+        with:
+          node-version: "22"
+      - name: Install OpenClaw in the ephemeral runner
+        run: npm install --global openclaw
+      - name: Install the exact candidate gem and test dependencies
+        run: |
+          set -euo pipefail
+          gem_file="$(find candidate-artifacts -maxdepth 1 -type f -name 'hive-cli-*.gem' -print -quit)"
+          [[ -n "$gem_file" && -s "$gem_file" ]]
+          bash candidate-source/packaging/live_agent_skills/install_candidate_gem.sh \
+            "$gem_file" "$RUNNER_TEMP/proven-gems"
+          "$RUNNER_TEMP/proven-gems/bin/hive" --version
+          cd candidate-source
+          bundle install --jobs 4 --retry 2
+      - name: Run authenticated workflow-creator proof
+        env:
+          OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
+          HIVE_LIVE_MODEL: ${{ vars.HIVE_LIVE_MODEL }}
+          HIVE_LIVE_AGENT_SKILLS: "1"
+          HIVE_RELEASE_GATE: "1"
+          HIVE_CANDIDATE_SHA: ${{ needs.validate.outputs.candidate_sha }}
+          HIVE_PROOF_ARTIFACTS: ${{ github.workspace }}/candidate-artifacts
+          HIVE_CREATOR_EVIDENCE_PATH: ${{ runner.temp }}/workflow-creator-evidence/openclaw-workflow-creator.json
+        run: |
+          set -euo pipefail
+          mkdir -p "$RUNNER_TEMP/workflow-creator-evidence"
+          cd candidate-source
+          bundle exec ruby -Itest test/smoke/live_hive_workflow_creator_smoke_test.rb
+      - name: Upload redacted workflow-creator evidence
+        if: ${{ always() }}
+        uses: actions/upload-artifact@v7
+        with:
+          name: workflow-creator-evidence-openclaw-${{ github.run_attempt }}
+          path: ${{ runner.temp }}/workflow-creator-evidence/openclaw-workflow-creator.json
+          if-no-files-found: error
+          retention-days: 7
+
   attest:
     name: Attest exact-SHA live agent proof
     runs-on: ubuntu-24.04
-    needs: [validate, build, live-agent]
+    needs: [validate, build, live-agent, live-workflow-creator]
     permissions:
       actions: read
       checks: write
@@ -214,6 +276,11 @@ jobs:
           pattern: live-agent-evidence-*-${{ github.run_attempt }}
           path: evidence
           merge-multiple: true
+      - uses: actions/download-artifact@v8
+        with:
+          pattern: workflow-creator-evidence-*-${{ github.run_attempt }}
+          path: creator-evidence
+          merge-multiple: true
       - name: Extract candidate verifier
         env:
           CANDIDATE_SHA: ${{ needs.validate.outputs.candidate_sha }}
@@ -224,7 +291,7 @@ jobs:
       - uses: ruby/setup-ruby@v1
         with:
           ruby-version: "3.4"
-      - name: Validate four proofs and assemble private evidence
+      - name: Validate operating and workflow-creator proofs
         id: attest
         env:
           CANDIDATE_SHA: ${{ needs.validate.outputs.candidate_sha }}
@@ -234,7 +301,7 @@ jobs:
           result="$(ruby candidate-source/packaging/live_agent_skills/attest.rb \
             "$CANDIDATE_SHA" "$WORKFLOW_REVISION" "$GITHUB_REPOSITORY" \
             "$GITHUB_RUN_ID" "$GITHUB_RUN_ATTEMPT" \
-            candidate-artifacts evidence proof)"
+            candidate-artifacts evidence creator-evidence proof)"
           attestation_sha256="$(jq -er '.attestation_sha256' <<<"$result")"
           echo "attestation_sha256=$attestation_sha256" >> "$GITHUB_OUTPUT"
       - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
@@ -262,7 +329,8 @@ jobs:
               run_id: context.runId,
               run_attempt: runAttempt,
               attestation_sha256: process.env.ATTESTATION_SHA256,
-              platforms: ["openclaw", "claude", "codex", "pi"]
+              platforms: ["openclaw", "claude", "codex", "pi"],
+              workflow_creator: "openclaw"
             };
             await github.rest.checks.create({
               owner: context.repo.owner,
@@ -275,7 +343,7 @@ jobs:
               external_id: externalId,
               details_url: detailsUrl,
               output: {
-                title: "All four Hive operating-skill surfaces passed",
+                title: "Hive operating and workflow-creator proofs passed",
                 summary: `\`\`\`json\n${JSON.stringify(summary, null, 2)}\n\`\`\``
               }
             });
diff --git a/README.md b/README.md
index e98fec430a..ef5671283d 100644
--- a/README.md
+++ b/README.md
@@ -135,7 +135,12 @@ readiness state alongside the daemon outcome. Project enrollment is a separate
 babysitter defaults can be reviewed before confirmation. After setup, pass Hive
 CLI commands through `/hive ...`, for example
 `/hive status --operational --json`, `/hive watch <project>:<task>`,
-`/hive new . "build this feature"`, and `/hive review <task-slug>`.
+`/hive new . "build this feature"`, and `/hive review <task-slug>`. The same
+canonical skill can create a new project-local workflow from natural language:
+`/hive create a three-stage editorial workflow that researches, drafts, and
+requires approval before publishing`. It scaffolds through Hive, validates the
+result, reports every inferred default, and creates no task unless the request
+explicitly asks for one.
 
 For local checkout testing, run `openclaw skills install ./openclaw/skills/hive
 --as hive`. See [openclaw/README.md](openclaw/README.md) for the publish
@@ -321,19 +326,28 @@ full 24-hour periods or the exact lowercase value `never`; omission remains
 compatible and means `3`. This changes visibility only—`hive archive` remains
 the complete record, and no task folders are deleted or moved by retention.
 
-Three commands cover authoring: `hive workflow new ID` (scaffold a blank starter in an existing project), `hive workflow new ID --template research` (seed from the multi-stage research sample), and `hive init --new-workflow ID` (bootstrap a project and bind the workflow as its default in one go). Custom workflows are discovered from `.hive-state/workflows/*.yml` and run through the same surfaces as the built-ins — `hive new --workflow`, `status`, `run`, `approve`, and the daemon.
+Ask the canonical `/hive` skill to create a workflow in natural language when
+you do not want to author YAML. It uses the same three CLI primitives:
+`hive workflow new ID` (scaffold a blank starter in an existing project),
+`hive workflow new ID --template research` (seed from the multi-stage research
+sample), and `hive init --new-workflow ID` (bootstrap a project and bind the
+workflow as its default in one go). Validate edits with
+`hive workflow validate ID --json`. Custom workflows are discovered from
+`.hive-state/workflows/*.yml` and run through the same surfaces as the
+built-ins — `hive new --workflow`, `status`, `run`, `approve`, `decide`, and
+the daemon.
 
 **Full walkthrough** — mental model, descriptor anatomy, writing stage instructions, advanced options (terminal approval gates, `skill:` stages, per-stage permissions), and gotchas: **[hivecli.sh/docs/custom-workflows](https://hivecli.sh/docs/custom-workflows/)** (also in-repo at [docs/workflows.md](docs/workflows.md)).
 
 ## Power-User / Scripting CLI
 
-The TUI is the recommended human interface and an agent-driven CLI is the recommended automation surface, but the workflow commands are also available directly on `bin/hive` (or the `hv` shim when Apache Hive shadows the name) for scripting, debugging, and recovery. Stage-driving verbs support `--json` and return typed envelopes; `hive new` remains plain text and prints the captured task path plus the next-step hint.
+The TUI is the recommended human interface and an agent-driven CLI is the recommended automation surface, but the workflow commands are also available directly on `bin/hive` (or the `hv` shim when Apache Hive shadows the name) for scripting, debugging, and recovery. Stage-driving verbs support `--json` and return typed envelopes. `hive new` keeps its existing text output by default and adds an opt-in JSON contract for idempotent automation.
 
 | Group | Verbs | What it's for |
 |---|---|---|
 | Native setup & web | `hive setup`, `hive web`, `hive web status/install/start/stop` | Provision the default loopback Hive web service, run the foreground server, or observe/repair the managed unit. `--no-service` opts out of web-service mutation. See [docs/cli.md#day-to-day-workflow](docs/cli.md#day-to-day-workflow). |
-| Workflow | `hive new`, `hive brainstorm`, `hive plan`, `hive develop`, `hive open-pr`, `hive review`, `hive artifacts`, `hive finalize`, `hive archive`, `hive run`, `hive approve` | Drive a single stage of a single task by hand. `--from <stage>` lets you re-run a stage in place. See [docs/cli.md#day-to-day-workflow](docs/cli.md#day-to-day-workflow). |
-| Workflow authoring | `hive workflow new` | Scaffold a project-local workflow descriptor under `.hive-state/workflows/` (`--template research` seeds from a sample). See [Custom Workflows](#custom-workflows) and [docs/workflows.md](docs/workflows.md). |
+| Workflow | `hive new`, `hive brainstorm`, `hive plan`, `hive develop`, `hive open-pr`, `hive review`, `hive artifacts`, `hive finalize`, `hive archive`, `hive run`, `hive approve`, `hive decide` | Drive a single stage of a single task by hand. `--from <stage>` makes advances retry-safe; human decisions also require the visit-specific `--decision-id`. See [docs/cli.md#day-to-day-workflow](docs/cli.md#day-to-day-workflow). |
+| Workflow authoring | `hive workflow new`, `hive workflow validate` | Scaffold and read-only validate a project-local workflow descriptor under `.hive-state/workflows/` (`--template research` seeds from a sample). See [Custom Workflows](#custom-workflows) and [docs/workflows.md](docs/workflows.md). |
 | Review findings | `hive findings`, `hive accept-finding`, `hive reject-finding` | Inspect GFM-checkbox findings from the latest review pass and tick which ones should feed the next fix pass. See [docs/cli.md#findings-triage](docs/cli.md#findings-triage). |
 | Patrol | `hive patrol` | Run one opt-in repository patrol cycle: map feature slices, review them, validate fixes, and open PRs for passed fixes only. See [docs/cli.md#patrol](docs/cli.md#patrol). |
 | Daemon | `hive daemon install/enable/start/status/tail/stop/disable` | Manage the global daemon service plus per-project enrollment. The service polls `hive status --json` and dispatches workflow verbs for enrolled projects. Read [wiki/operating.md](wiki/operating.md) before going live. See [docs/cli.md#daemon](docs/cli.md#daemon). |
diff --git a/bin/hive b/bin/hive
index 863596e536..2c4bf7a217 100755
--- a/bin/hive
+++ b/bin/hive
@@ -5,13 +5,35 @@ $LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
 require "hive"
 require "hive/llm_wiki_bootstrap"
 
+# The workflow creator owns two strict observation routes. They must stay
+# byte-for-byte no-write even when an upgraded binary would otherwise repair
+# scheduler units during process startup.
+init_index = ARGV.index("init")
+workflow_index = ARGV.index("workflow")
+strict_no_write_route =
+  if init_index && (!workflow_index || init_index < workflow_index)
+    ARGV.drop(init_index + 1).any? do |arg|
+      arg == "--preview" || arg.match?(/\A--preview=true\z/i)
+    end
+  elsif workflow_index
+    workflow_subcommands = %w[new validate commit install list update remove publish].freeze
+    workflow_subcommand = ARGV.drop(workflow_index + 1).find do |arg|
+      workflow_subcommands.include?(arg)
+    end
+    workflow_subcommand == "validate"
+  else
+    false
+  end
+
 # Repair legacy path-keyed LLM-wiki timers as soon as an upgraded Hive binary
 # is invoked, including `hive --version`. The reconciliation is idempotent and
 # touches only Hive-managed units.
-begin
-  Hive::LlmWikiBootstrap::Scheduler.reconcile_existing!
-rescue StandardError => e
-  warn "hive: warning: could not reconcile LLM-wiki scheduler units: #{e.message}"
+unless strict_no_write_route
+  begin
+    Hive::LlmWikiBootstrap::Scheduler.reconcile_existing!
+  rescue StandardError => e
+    warn "hive: warning: could not reconcile LLM-wiki scheduler units: #{e.message}"
+  end
 end
 
 if ENV["HIVE_ATTEMPT_INTERNAL"] == "1"
@@ -49,7 +71,7 @@ end
 
 JSON_BOOLEAN_OPTIONS = Hive::CliJsonOptions::BOOLEAN_OPTIONS
 STAGE_ACTION_USAGE_COMMANDS = %w[brainstorm plan develop open-pr review artifacts finalize archive].freeze
-NEW_VALUE_OPTIONS = %w[--base --depends-on --workflow].freeze
+NEW_VALUE_OPTIONS = %w[--base --depends-on --workflow --idempotency-key].freeze
 JSON_USAGE_ERROR_CONTRACTS = {
   "run" => { schema: "hive-run", error_kind: "invalid_task_path" },
   "rebase-status" => { schema: "hive-rebase-status", schema_version: 1, error_kind: "invalid_task_path" },
@@ -350,6 +372,8 @@ def json_usage_error_contract(argv)
     subcommand = json_usage_subcommand(argv, command_index)
     schema = subcommand == "approve" ? "hive-pairing-approve" : "hive-pairing-list"
     return { schema: schema, error_kind: "invalid_arguments" }
+  when "decide"
+    return { schema: "hive-decide", error_kind: "invalid_task_path" }
   when "refactor-patrol"
     if (jobs = refactor_patrol_jobs_argv(option_argv))
       return {
@@ -365,9 +389,18 @@ def json_usage_error_contract(argv)
     when "remove" then "hive-workflow-remove"
     when "update" then "hive-workflow-update"
     when "publish" then "hive-workflow-publish"
+    when "validate" then "hive-workflow-validate"
     else "hive-workflow-new"
     end
-    return { schema: schema, error_kind: "usage" } if Hive::Schemas::SCHEMA_VERSIONS.key?(schema)
+    if Hive::Schemas::SCHEMA_VERSIONS.key?(schema)
+      extras = if subcommand == "validate"
+        id = json_usage_positionals(argv, command_index).fetch(1, "")
+        { "valid" => false, "id" => id }
+      else
+        {}
+      end
+      return { schema: schema, error_kind: "usage", extras: extras }
+    end
   end
 
   JSON_USAGE_ERROR_CONTRACTS[command]
@@ -480,6 +513,9 @@ def json_usage_error_payload(contract, error, argv: [])
       error_kind: error_kind,
       extras: extras
     )
+    if schema == "hive-workflow-validate"
+      payload["diagnostics"] = [ { "message" => error.message } ]
+    end
     payload.delete("error_class") if contract[:omit_error_class]
     return payload
   end
diff --git a/docs/RELEASING.md b/docs/RELEASING.md
index f002409d0e..9655b6763d 100644
--- a/docs/RELEASING.md
+++ b/docs/RELEASING.md
@@ -272,6 +272,20 @@ bullets (prefix fixes with "Fixed"); no `[Unreleased]` accumulator and no dates
 notable minor/major releases; keep routine patch releases terse. A release with
 nothing user-facing gets a one-line "no user-facing changes" note.
 
+### Workflow-creator public wording
+
+The canonical skill may describe current-main behavior because its generated
+projections and candidate artifacts are pinned to the same source commit. Do
+not describe current-main workflow-creator commands as stable on hivecli.sh,
+ClawHub, or release announcements until a separately authorized release
+containing those commands is published. Stable-user wording should name the
+containing release or tell users to run the supported `hive update` path after
+that release exists.
+
+The downstream wording handoff is tracked as `hive-site #23116`. It does not block this repository.
+Implementation, tests, and release readiness proceed independently, and this
+repository must not mutate or deploy hive-site as part of that coordination.
+
 ---
 
 ## One-time setup
diff --git a/docs/cli.md b/docs/cli.md
index f5d7846a99..ea007789a0 100644
--- a/docs/cli.md
+++ b/docs/cli.md
@@ -11,7 +11,7 @@ This page documents the command surface exposed by `bin/hive` in this checkout.
 | `hive web status [--json]` | Observe installed, enabled, running, and readiness state without mutation. | `hive web status --json` |
 | `hive web install [--force] [--json]` | Install or explicitly repair the managed per-user web service. | `hive web install --force` |
 | `hive status` | See active tasks grouped by next action. | `hive status` |
-| `hive new PROJECT TEXT` | Capture an idea in `1-inbox/`. | `hive new xbookmark "save bookmarks"` |
+| `hive new PROJECT [--workflow ID] [--idempotency-key KEY] [--json] TEXT` | Capture an idea in the workflow entry stage; optional idempotency makes agent retries return the existing task. | `hive new xbookmark --workflow editorial --idempotency-key creator:42 --json "draft a launch post"` |
 | `hive brainstorm TARGET` | Promote inbox to brainstorm or re-run brainstorm. | `hive brainstorm <slug>` |
 | `hive plan TARGET` | Promote completed brainstorm to plan or re-run plan. | `hive plan <slug> --from 2-brainstorm` |
 | `hive develop TARGET` | Promote completed plan to execute or re-run execute. | `hive develop <slug> --from 3-plan` |
@@ -21,6 +21,7 @@ This page documents the command surface exposed by `bin/hive` in this checkout.
 | `hive artifacts TARGET` | Collect the reviewed task's release artifacts. | `hive artifacts <slug> --from 6-review` |
 | `hive finalize TARGET` | Refresh PR body and mark the draft PR ready. | `hive finalize <slug> --from 7-artifacts` |
 | `hive archive TARGET` | Move finalized work to `9-done/`. | `hive archive <slug> --from 8-finalize` |
+| `hive decide TARGET OUTCOME --from STAGE --decision-id ID [--note TEXT] [--json]` | Apply one descriptor-declared outcome to the observed visit of a durable human stage. | `hive decide <slug> approve --from approval --decision-id abcdef0123456789 --json` |
 
 The workflow verbs promote then run when the task is at the previous stage. If the task is already at the target stage, they only run that stage.
 
@@ -103,6 +104,9 @@ returns every flat patch and supersession receipt.
 | `hive refactor-patrol PROJECT --list\|--show JOB_ID [--limit N]` | Inspect durable architecture-patrol jobs without mutation; list pages use `--cursor`, while show accepts explicit `--full` history. |
 | `hive migrate [PROJECT_PATH]` | Rewrite legacy project config, rename in-flight task folders from older stage layouts, and backfill legacy task metadata. |
 | `hive workflow new ID` | Scaffold a blank project workflow descriptor under `<hive_state_path>/workflows/`. |
+| `hive workflow validate ID [--json]` | Read-only load and validate a built-in or project workflow, including instructions and normalized transitions/outcomes. |
+| `hive workflow commit ID` | Validate and commit a populated owner-authored descriptor and instruction directory under Hive's state commit lock. |
+| `hive init --new-workflow ID --minimal --preview --json` | Resolve and disclose creator-safe fresh-project initialization without writing files, registration, hooks, services, or timers. |
 | `hive workflow install honeycomb/NAME[@REF]` | Verify and atomically select a reviewed package. |
 | `hive workflow list` | Show built-in, authored, selected, and retained workflows with integrity/provenance; JSON v2 includes the active managed configuration and redacted input availability. |
 | `hive workflow update NAME [--dry-run]` | Report a semantic/security diff and atomically advance after consent. |
@@ -168,7 +172,7 @@ never prompts; without `--yes`, planned mutation returns 64.
 
 ## JSON Output
 
-Workflow verbs (`brainstorm`, `plan`, `develop`, `open-pr`, `review`, `artifacts`, `finalize`, `archive`, `run`, `approve`), findings triage (`findings`, `accept-finding`, `reject-finding`), patrol (`patrol`), architecture-patrol job inspection (`refactor-patrol --list` / `--show`), diagnostics/setup (`status`, `setup`, `doctor`, `setup-agents`, `web status`, `web install`, `rebase-status`, `markers clear`, `metrics rollback-rate`), registry cleanup (`forget`, `prune`), workflow lifecycle (`workflow new/install/list/update/remove/publish`), `init`, and daemon control support `--json` where documented and emit typed envelopes. Native setup uses `hive-setup.v1`; web observation and installation use `hive-web-status.v1` and `hive-web-install.v1`. These contracts keep `mode`, effective `url`, compatibility `warnings`, and service installed/enabled/running/readiness distinct. `hive doctor --json` emits `hive-doctor.v2`; `hive setup-agents --json --yes` emits `hive-setup-agents.v1`, never prompts, and requires `--yes` whenever mutation is planned. `hive init --json` emits a single `hive-init.v2` success payload with resolved answers and project metadata; architecture-patrol list/show emits `hive-refactor-patrol-jobs.v1`. Workflow verbs emit a `hive-stage-action` envelope; Honeycomb lifecycle schemas are `hive-workflow-{install,list,update,remove,publish}.v1`. Non-TTY/JSON mutations require `--yes`, and an escalating workflow update separately requires `--allow-escalation`. Schema files live under [schemas/](../schemas/), and [wiki/cli.md](../wiki/cli.md) lists the contract details. `hive tui` rejects `--json`; legacy or one-shot utilities (`version`, `tree`, `new`, `migrate`) are still text-only.
+Workflow verbs (`brainstorm`, `plan`, `develop`, `open-pr`, `review`, `artifacts`, `finalize`, `archive`, `run`, `approve`, `decide`), findings triage (`findings`, `accept-finding`, `reject-finding`), patrol (`patrol`), architecture-patrol job inspection (`refactor-patrol --list` / `--show`), diagnostics/setup (`status`, `setup`, `doctor`, `setup-agents`, `web status`, `web install`, `rebase-status`, `markers clear`, `metrics rollback-rate`), registry cleanup (`forget`, `prune`), workflow lifecycle (`workflow new/validate/install/list/update/remove/publish`), `new`, `init`, and daemon control support `--json` where documented and emit typed envelopes. Native setup uses `hive-setup.v1`; web observation and installation use `hive-web-status.v1` and `hive-web-install.v1`. These contracts keep `mode`, effective `url`, compatibility `warnings`, and service installed/enabled/running/readiness distinct. `hive doctor --json` emits `hive-doctor.v2`; `hive setup-agents --json --yes` emits `hive-setup-agents.v1`, never prompts, and requires `--yes` whenever mutation is planned. Ordinary `hive init --json` emits `hive-init.v2`; minimal preview emits `hive-init-preview.v1` without mutation. Idempotent task capture emits `hive-new.v1`, human decisions emit `hive-decide.v1`, and read-only workflow validation emits `hive-workflow-validate.v1`. Architecture-patrol list/show emits `hive-refactor-patrol-jobs.v1`. Other workflow verbs emit a `hive-stage-action` envelope; Honeycomb lifecycle schemas are `hive-workflow-{install,list,update,remove,publish}.v1`. Non-TTY/JSON mutations require `--yes` where documented, and an escalating workflow update separately requires `--allow-escalation`. Schema files live under [schemas/](../schemas/), and [wiki/cli.md](../wiki/cli.md) lists the contract details. `hive tui` rejects `--json`; `version`, `tree`, and `migrate` remain text-only.
 
 ## Exit Codes
 
diff --git a/docs/workflows.md b/docs/workflows.md
index 6a3feb3f0d..32f60d42b8 100644
--- a/docs/workflows.md
+++ b/docs/workflows.md
@@ -37,6 +37,99 @@ the stage writes a cooldown-aware `limits_reached` marker so the daemon retries
 after reset; malformed/missing/non-limit failures remain manual `WAITING`
 states.
 
+## Create A Workflow From Natural Language
+
+The canonical Hive agent skill accepts ordinary-language workflow creation
+requests. In OpenClaw and Claude invoke it as `/hive`; Codex uses `$hive` and
+Pi uses `/skill:hive`. For example:
+
+```text
+/hive create a three-stage editorial workflow that researches, drafts, and requires approval before publishing
+```
+
+The creator checks the installed Hive version, project settings, templates,
+reserved IDs, and existing workflow IDs before it writes anything. In an
+initialized project it scaffolds only new paths with `hive workflow new`, edits
+those generated files, validates them, and commits the populated descriptor and
+instruction directory to `hive/state` before reporting success:
+
+```bash
+hive workflow validate editorial --json
+hive workflow commit editorial
+```
+
+In a fresh project it first renders the no-write
+`hive init --new-workflow editorial --minimal --preview --json` plan and waits
+for one explicit confirmation before running the matching command without
+`--preview`. The minimal profile creates the core Hive project, state worktree,
+registration, and required wiki/context integration while leaving optional
+patrol, auto-fix, daemon dispatch/autostart, babysitter, and timer setup off. It
+never selects a starter template or adds `--force`.
+
+The accepted editorial result has exactly three stages:
+
+```yaml
+id: editorial
+stages:
+  - name: research
+    kind: agent
+    state_file: research.md
+    instruction: ./editorial/research.md
+    permissions: yolo
+  - name: draft
+    kind: agent
+    state_file: draft.md
+    instruction: ./editorial/draft.md
+    permissions: yolo
+  - name: approval
+    kind: human
+    state_file: approval.md
+    input: draft.md
+    outcomes:
+      approve:
+        complete: true
+        artifact: draft.md
+      reject:
+        to: draft
+```
+
+The stage and model choices inherit project defaults. The creator reports that
+inheritance, sequential transitions, local `yolo` permissions, artifact names,
+the neutral scaffold/template choice, every created file, and the validation
+result. No task is created by workflow creation alone. Its completion summary
+includes the exact opt-in command:
+
+```bash
+hive new '<project>' --workflow 'editorial' '<your request>'
+```
+
+If the original request explicitly asks to create or run a task, the creator
+uses `hive new --idempotency-key KEY --json`, so a retry finds the same task
+even after it moves stages. It queries `hive status --operational --json` after
+creation or an explicitly requested first run. It derives that key
+deterministically from the real project path, normalized workflow ID, and
+canonicalized request text. Every dynamic argument uses POSIX shell escaping,
+so quotes, substitutions, variables, glob characters, and newlines remain
+literal request bytes.
+
+When the task reaches approval, the only transitions are:
+
+```bash
+hive decide <task> approve --from approval --decision-id <decision-id>
+hive decide <task> reject --from approval --decision-id <decision-id> --note "revise the conclusion"
+```
+
+The waiting `hive run --json` response supplies the visit-specific decision
+ID. Approve requires a non-empty, regular task-local `draft.md` (symlinks are
+refused), records it as publish-ready, and completes the task. Reject records the decision, returns the same task to
+`draft`, and resets that stage to `WAITING`. Neither outcome publishes, deploys,
+sends, or otherwise acts outside Hive; an external destination and separate
+authorization are always required.
+
+The creator is create-only. Reserved or colliding IDs stop before mutation and
+return an available alternative. Editing or repairing an existing workflow is
+outside this contract.
+
 ## Create A Blank Workflow
 
 ```bash
@@ -55,7 +148,12 @@ This writes a minimal `inbox -> work -> done` workflow:
 The generated descriptor is valid immediately, and the placeholder
 `work.md` is the prompt for the `work` stage. Edit that file to define what the
 agent should do. New scaffolds explicitly set
-`archive_visibility_retention_days: 3`.
+`archive_visibility_retention_days: 3`. Then validate the production-loaded
+descriptor:
+
+```bash
+hive workflow validate my-flow --json
+```
 
 `README.md` and `honeycomb.yml` are publish-preflight inputs. The scaffolded
 summary and author are placeholders; edit them before running `workflow
@@ -208,6 +306,9 @@ Rules:
 - `kind: terminal` creates an inert stage; it does not spawn an agent.
 - `kind: agent` spawns the generic stage runner.
 - `kind: council` runs a document review council over an input artifact.
+- `kind: human` persists `WAITING` and exposes only descriptor-declared
+  outcomes through
+  `hive decide TARGET OUTCOME --from STAGE --decision-id DECISION_ID`.
 - Every agent stage must declare exactly one of `skill:` or `instruction:`.
 - `agent:`, `model:`, and `effort:` are optional on `kind: agent` and
   `kind: council`. Descriptor `agent` overrides the project stage block;
@@ -226,6 +327,12 @@ Rules:
     enforce it; `timeout_sec` remains enforced for every agent spawn.
   - Council command reviewers and command revisers also use `timeout_sec`; Hive
     terminates their process group when the limit expires.
+- A human stage declares `input:` plus one or more named `outcomes:`. Every
+  outcome has exactly one action: `complete: true` with a required non-empty
+  `artifact:` basename, or `to: <stage>`. Human stages cannot declare agent,
+  model, permission, runner, reviewer, or executable command settings. A
+  completing outcome writes a durable decision and `completed_at`, then enters
+  the same archive visibility/retention path as other completed workflows.
 - `skill:`, `instruction:`, `agent:`, `model:`, `effort:`, `budget_usd:`,
   `timeout_sec:`, `permissions:`, `input:`, `reviewers:`, `council:`, and
   `deliverable:` are rejected on `kind: terminal` stages.
@@ -237,10 +344,11 @@ Rules:
   report names, inert/council use, and nonterminal use are rejected. The pair
   creates a controller-owned exact-origin worktree and controller-owned draft
   PR delivery without embedding an agent, model, or effort choice.
-- The last stage may be `kind: terminal`, `kind: agent`, or `kind: council`.
+- The last stage may be `kind: terminal`, `kind: agent`, `kind: council`, or `kind: human`.
   A terminal agent/council stage is archived only when it has a terminal
   `COMPLETE` marker and a non-empty deliverable. `deliverable:` defaults to the
-  stage's `state_file`.
+  stage's `state_file`. A terminal human stage is archived only after a named
+  completing outcome verifies its declared non-empty artifact.
 
 Council stages declare reviewers and a council policy:
 
diff --git a/lib/hive.rb b/lib/hive.rb
index f56e55c269..0c01151ba7 100644
--- a/lib/hive.rb
+++ b/lib/hive.rb
@@ -20,6 +20,8 @@ module Schemas
       "hive-watch-event" => 1,
       "hive-act" => 2,
       "hive-init" => 2,
+      "hive-init-preview" => 1,
+      "hive-new" => 1,
       "hive-setup-agents" => 1,
       "hive-setup" => 1,
       "hive-web-status" => 1,
@@ -31,6 +33,7 @@ module Schemas
       "hive-status-diagnose" => 2,
       "hive-run" => 2,
       "hive-approve" => 2,
+      "hive-decide" => 1,
       "hive-findings" => 1,
       "hive-stage-action" => 2,
       "hive-metrics-rollback-rate" => 1,
@@ -58,6 +61,9 @@ module Schemas
       # every other agent-callable command's error envelope; the success arm
       # builds its hash directly.
       "hive-workflow-new" => 1,
+      # Read-only production-resolution validation for owner-authored,
+      # managed, and built-in workflows (`hive workflow validate ID --json`).
+      "hive-workflow-validate" => 1,
       "hive-workflow-install" => 2,
       # v2 gives selected managed rows their active immutable configuration
       # digest plus redacted per-slot mapping and optional-input discovery.
diff --git a/lib/hive/cli.rb b/lib/hive/cli.rb
index c78964e808..c2d2302bb5 100644
--- a/lib/hive/cli.rb
+++ b/lib/hive/cli.rb
@@ -128,6 +128,19 @@ def version
       already-initialized project scaffolds the workflow and rebinds the
       default in one hive-state commit.
 
+      Workflow creators can inspect the neutral fresh-project plan without
+      writes, then execute that exact automation-disabled profile after an
+      explicit confirmation:
+
+        hive init --new-workflow editorial --minimal --preview --json
+        hive init --new-workflow editorial --minimal --json
+
+      Minimal init is fresh-target only, rejects --force, and disables patrol,
+      architecture patrol, ad-hoc auto-fix, daemon dispatch/autostart,
+      babysitting, optional reviewers, and service/timer installation. It
+      retains the core hive/state worktree, global project registration, and
+      required llm-wiki/context hooks.
+
       Headless callers can explicitly select architecture discovery before
       any state is written with --refactor-patrol or --no-refactor-patrol;
       omitting both keeps the fresh-project default enabled. Set other
@@ -151,6 +164,10 @@ def version
     option :workflow, type: :string, desc: workflow_option_desc
     option :new_workflow, type: :string,
                           desc: "scaffold custom workflow ID, bind it as this project's default, and print paths to edit"
+    option :minimal, type: :boolean, default: false,
+                     desc: "with --new-workflow on a fresh project: use the neutral automation-disabled profile"
+    option :preview, type: :boolean, default: false,
+                     desc: "with --minimal: report the resolved initialization plan without writing"
     option :refactor_patrol, type: :boolean, default: nil,
                              desc: "enable or disable post-merge architecture discovery before init writes state"
     def init(project_path = Dir.pwd)
@@ -161,7 +178,9 @@ def init(project_path = Dir.pwd)
         json: options[:json],
         workflow: options[:workflow],
         new_workflow: options[:new_workflow],
-        refactor_patrol: options[:refactor_patrol]
+        refactor_patrol: options[:refactor_patrol],
+        minimal: options[:minimal],
+        preview: options[:preview]
       ).call
     end
 
@@ -454,6 +473,8 @@ def wiki(subcommand, project_path = Dir.pwd)
         new ID    Scaffold a per-project workflow descriptor under
                   <hive_state_path>/workflows/ID.yml plus its stage
                   instruction(s) under <hive_state_path>/workflows/ID/.
+        validate ID  Read and validate the normalized workflow graph without writes.
+        commit ID    Commit a validated owner-authored graph under Hive's state lock.
         install honeycomb/NAME[@VERSION]  Verify and install a reviewed package.
         list                              Inspect built-in, authored, and managed workflows.
         update NAME                       Diff and advance a managed package.
@@ -524,6 +545,11 @@ def bench(subcommand, slug = nil)
       uses the project's default workflow; pass --workflow to pin a registered
       workflow for this task. (Options may also follow the text.)
 
+      --idempotency-key makes retries state-wide and machine-safe. Hive stores
+      the opaque key with a fingerprint of the input and resolved workflow,
+      returns the original task after it moves stages, and rejects reuse for
+      different input. Pair it with --json for the hive-new.v1 result.
+
       --depends-on stacks this task on a prerequisite: the daemon holds
       auto-advance until the prerequisite reaches the project's dependency
       gate stage (8-finalize by default, configurable via
@@ -544,6 +570,8 @@ def bench(subcommand, slug = nil)
         hive new myproj --depends-on add-export-endpoint-260618-ab12 "wire up export API"
 
         hive new myproj --depends-on api:add-export-endpoint-260618-ab12 "wire up export UI"
+
+        hive new myproj --workflow content --idempotency-key creator:launch:v1 --json "write the launch post"
     DESC
     option :depends_on, type: :string,
                         desc: "depend on a same-project id/slug or explicit project:slug; hold daemon " \
@@ -552,6 +580,8 @@ def bench(subcommand, slug = nil)
     option :base, type: :string,
                   desc: "base branch for a worktree/draft-PR workflow"
     option :workflow, type: :string, desc: workflow_option_desc
+    option :idempotency_key, type: :string,
+                             desc: "return the original task on a retry with identical input; reject conflicting reuse"
     def new_task(project, *text_parts)
       require "hive/commands/new"
       text = text_parts.join(" ")
@@ -562,7 +592,9 @@ def new_task(project, *text_parts)
         text,
         base: options[:base],
         depends_on: options[:depends_on],
-        workflow: options[:workflow]
+        workflow: options[:workflow],
+        idempotency_key: options[:idempotency_key],
+        json: options[:json]
       ).call
     end
     map "new" => :new_task
@@ -1106,6 +1138,34 @@ def approve(target)
       ).call
     end
 
+    desc "decide TARGET OUTCOME", "Apply a named outcome to a durable human workflow stage"
+    long_desc <<~DESC
+      TARGET is a task slug or folder currently waiting at a descriptor-declared
+      human stage. OUTCOME must be one of that stage's closed named outcomes.
+
+      --from and --decision-id are required and identify the expected human-stage
+      visit. Repeating the same decision is a no-op; a conflicting or stale
+      decision fails with WRONG_STAGE instead of changing the moved task.
+    DESC
+    option :from, type: :string, required: true,
+                  desc: "expected human stage, full or short form"
+    option :decision_id, type: :string,
+                         desc: "required decision identity reported by run/status for this visit"
+    option :note, type: :string, desc: "optional audit note recorded with the decision"
+    option :project, type: :string, desc: "scope slug lookup to one registered project"
+    def decide(target, outcome)
+      require "hive/commands/decide"
+      Hive::Commands::Decide.new(
+        target,
+        outcome,
+        from: options[:from],
+        decision_id: options[:decision_id],
+        note: options[:note],
+        project: options[:project],
+        json: options[:json]
+      ).call
+    end
+
     desc "findings TARGET", "List findings in the latest reviews/ce-review-NN.md (or --pass N)"
     long_desc <<~DESC
       TARGET is either a 4-execute task folder path or a bare slug. Findings
diff --git a/lib/hive/commands/approve.rb b/lib/hive/commands/approve.rb
index 6ef7216128..e9d32afd9e 100644
--- a/lib/hive/commands/approve.rb
+++ b/lib/hive/commands/approve.rb
@@ -2,6 +2,7 @@
 require "json"
 require "open3"
 require "pathname"
+require "securerandom"
 require "hive/config"
 require "hive/task"
 require "hive/task_resolver"
@@ -14,6 +15,7 @@
 require "hive/commit_or_rollback"
 require "hive/dependency_snapshot"
 require "hive/attempts/context"
+require "hive/atomic_file"
 require "hive/conditions/transition_guard"
 require "hive/workflow_package/mutation_lock"
 require "hive/task_meta"
@@ -62,7 +64,7 @@ def self.error_kind_for(error)
       end
 
       def initialize(target, to: nil, from: nil, project: nil, force: false, json: false, quiet: false,
-                     observation_guard: nil, clock: DEFAULT_CLOCK)
+                     observation_guard: nil, commit_lock: true, clock: DEFAULT_CLOCK)
         @target = target
         @to = to
         @from = from
@@ -74,6 +76,7 @@ def initialize(target, to: nil, from: nil, project: nil, force: false, json: fal
         # State changes and typed exceptions still propagate normally.
         @quiet = quiet
         @observation_guard = observation_guard
+        @commit_lock = commit_lock
         @clock = clock
       end
 
@@ -266,35 +269,108 @@ def stage_for_dest!(task, dest_stage)
       def perform_move_and_commit(task, dest_stage, enforce_admission: false)
         new_folder = nil
         commit_action = nil
+        human_state_snapshot = nil
         completion_snapshot = nil
         completion_time = nil
-        Hive::Lock.with_commit_lock(task.hive_state_path) do
-          Hive::Lock.with_task_lock(task.folder, slug: task.slug, op: "approve") do
-            # Preserve the command's typed collision contract before building
-            # a multi-project snapshot: a pre-existing destination necessarily
-            # duplicates the slug, but it is first and foremost an unsafe move
-            # destination and requires no dependency inference to reject.
-            raise_destination_collision(destination_folder(task, dest_stage)) if
-              destination_exists?(destination_folder(task, dest_stage))
-            Hive::DependencySnapshot.enforce_admission!(task) if enforce_admission
-            Hive::Attempts::Context.current&.validate_generation!(task)
-            @observation_guard&.call(task)
-            if completion_on_terminal_entry?(task, dest_stage)
-              completion_snapshot = Hive::TaskMeta.snapshot(task.folder)
-              completion_time = legacy_completion_time(task)
+        begin
+          with_optional_commit_lock(task.hive_state_path) do
+            Hive::Lock.with_task_lock(task.folder, slug: task.slug, op: "approve") do
+              # Preserve the command's typed collision contract before building
+              # a multi-project snapshot: a pre-existing destination necessarily
+              # duplicates the slug, but it is first and foremost an unsafe move
+              # destination and requires no dependency inference to reject.
+              raise_destination_collision(destination_folder(task, dest_stage)) if
+                destination_exists?(destination_folder(task, dest_stage))
+              Hive::DependencySnapshot.enforce_admission!(task) if enforce_admission
+              Hive::Attempts::Context.current&.validate_generation!(task)
+              @observation_guard&.call(task)
+              human_state_snapshot = initialize_human_destination!(task, dest_stage)
+              if completion_on_terminal_entry?(task, dest_stage)
+                completion_snapshot = Hive::TaskMeta.snapshot(task.folder)
+                completion_time = legacy_completion_time(task)
+              end
+              new_folder = move_task!(task, dest_stage)
             end
-            new_folder = move_task!(task, dest_stage)
+            cleanup_orphan_task_lock(new_folder)
+            commit_action = "approve #{task.stage_index}-#{task.stage_name} -> #{dest_stage}"
+            record_commit_or_rollback!(
+              task, dest_stage, new_folder, commit_action,
+              completion_snapshot: completion_snapshot, completion_time: completion_time
+            )
           end
-          cleanup_orphan_task_lock(new_folder)
-          commit_action = "approve #{task.stage_index}-#{task.stage_name} -> #{dest_stage}"
-          record_commit_or_rollback!(
-            task, dest_stage, new_folder, commit_action,
-            completion_snapshot: completion_snapshot, completion_time: completion_time
-          )
+        rescue StandardError, Interrupt
+          restore_human_destination!(task, new_folder, human_state_snapshot)
+          raise
         end
         [ new_folder, commit_action ]
       end
 
+      def with_optional_commit_lock(hive_state_path, &block)
+        return block.call unless @commit_lock
+
+        Hive::Lock.with_commit_lock(hive_state_path, &block)
+      end
+
+      def initialize_human_destination!(task, dest_stage)
+        stage = stage_for_dest!(task, dest_stage)
+        return nil unless stage.kind == :human
+
+        path = File.join(task.folder, stage.state_file)
+        snapshot = nil
+        Hive::Markers.with_markers_lock(path) do
+          snapshot = read_human_state_snapshot!(path, stage.state_file)
+          marker = Hive::Markers.build_marker(
+            "WAITING", "decision_id" => SecureRandom.hex(8)
+          )
+          body, count = Hive::Markers.replace_last_marker(snapshot.fetch(:body).to_s, marker)
+          if count.zero?
+            separator = body.empty? || body.end_with?("\n") ? "" : "\n"
+            body = "#{body}#{separator}#{marker}\n"
+          end
+          Hive::AtomicFile.write(path, body)
+        end
+        snapshot
+      end
+
+      def read_human_state_snapshot!(path, state_file)
+        stat = File.lstat(path)
+        unless stat.file? && !stat.symlink?
+          raise Hive::InvalidTaskPath,
+                "human stage state file #{state_file.inspect} must be a regular file, not a symlink"
+        end
+
+        flags = File::RDONLY
+        flags |= File::NOFOLLOW if File.const_defined?(:NOFOLLOW)
+        body = File.open(path, flags) do |file|
+          opened = file.stat
+          unless opened.file? && opened.dev == stat.dev && opened.ino == stat.ino
+            raise Hive::InvalidTaskPath,
+                  "human stage state file #{state_file.inspect} changed while entering the stage"
+          end
+          file.read
+        end
+        { state_file: state_file, existed: true, body: body }
+      rescue Errno::ENOENT
+        { state_file: state_file, existed: false, body: nil }
+      rescue Errno::ELOOP, Errno::EMLINK
+        raise Hive::InvalidTaskPath,
+              "human stage state file #{state_file.inspect} must be a regular file, not a symlink"
+      end
+
+      def restore_human_destination!(task, new_folder, snapshot)
+        return unless snapshot
+
+        folder = File.directory?(task.folder) ? task.folder : new_folder
+        return unless folder && File.directory?(folder)
+
+        path = File.join(folder, snapshot.fetch(:state_file))
+        if snapshot.fetch(:existed)
+          Hive::AtomicFile.write(path, snapshot.fetch(:body))
+        else
+          File.delete(path) if File.exist?(path) || File.symlink?(path)
+        end
+      end
+
       def move_task!(task, dest_stage)
         workflows_dir = File.join(task.hive_state_path, "workflows")
         Hive::WorkflowPackage::MutationLock.with_lock(workflows_dir, shared: true) do
diff --git a/lib/hive/commands/decide.rb b/lib/hive/commands/decide.rb
new file mode 100644
index 0000000000..316ce737b3
--- /dev/null
+++ b/lib/hive/commands/decide.rb
@@ -0,0 +1,474 @@
+require "json"
+require "open3"
+require "securerandom"
+require "time"
+require "hive/commands/approve"
+require "hive/git_ops"
+require "hive/lock"
+require "hive/markers"
+require "hive/task"
+require "hive/task_action"
+require "hive/task_meta"
+require "hive/task_resolver"
+
+module Hive
+  module Commands
+    # Apply one descriptor-declared outcome to a durable human workflow stage.
+    # The decision identity is minted when the task enters the stage and carried
+    # by its WAITING marker, so retries can distinguish the current visit from a
+    # prior reject/return cycle without a second workflow language.
+    class Decide
+      include Hive::Schemas::EnvelopeEmitter
+
+      RECORD_RE = /<!-- HIVE_DECISION_V1 (?<payload>\{.*\}) -->/.freeze
+      DEFAULT_CLOCK = -> { Time.now.utc }
+      SUCCESS_KEYS = %w[
+        schema schema_version ok applied noop slug workflow from_stage outcome
+        decision_id note decided_at completed artifact artifact_path to
+        current_stage task_folder next_action
+      ].freeze
+
+      def self.latest_record(path)
+        latest_record_from_content(read_state_file(path))
+      end
+
+      def self.latest_record_from_content(content)
+        content.to_s.each_line.filter_map do |line|
+          match = RECORD_RE.match(line)
+          JSON.parse(match[:payload]) if match
+        rescue JSON::ParserError
+          nil
+        end.last
+      end
+
+      def self.read_state_file(path, binary: false)
+        stat = File.lstat(path)
+        unless stat.file? && !stat.symlink?
+          raise Hive::InvalidTaskPath,
+                "human stage state file #{File.basename(path).inspect} must be a regular file, not a symlink"
+        end
+
+        flags = File::RDONLY
+        flags |= File::NOFOLLOW if File.const_defined?(:NOFOLLOW)
+        body = File.open(path, flags) do |file|
+          opened = file.stat
+          unless opened.file? && opened.dev == stat.dev && opened.ino == stat.ino
+            raise Hive::InvalidTaskPath,
+                  "human stage state file #{File.basename(path).inspect} changed while opening it"
+          end
+          file.read
+        end
+        return body if binary
+
+        body.force_encoding(Encoding::UTF_8)
+        unless body.valid_encoding?
+          raise Hive::InvalidTaskPath,
+                "human stage state file #{File.basename(path).inspect} must contain valid UTF-8"
+        end
+        body
+      rescue Errno::ENOENT
+        nil
+      rescue Errno::ELOOP, Errno::EMLINK
+        raise Hive::InvalidTaskPath,
+              "human stage state file #{File.basename(path).inspect} must be a regular file, not a symlink"
+      end
+
+      def initialize(target, outcome, from:, decision_id: nil, note: nil, project: nil, json: false,
+                     clock: DEFAULT_CLOCK)
+        @target = target
+        @outcome_name = outcome.to_s.strip
+        @from = from.to_s.strip
+        @decision_id = decision_id.to_s.strip
+        @note = note
+        @project_filter = project
+        @json = json
+        @clock = clock
+      end
+
+      def call
+        call_with_envelope { do_call }
+      end
+
+      def envelope_schema = "hive-decide"
+
+      def envelope_error_kind(error)
+        case error
+        when Hive::WrongStage then "wrong_stage"
+        when Hive::AmbiguousSlug then "ambiguous_slug"
+        when Hive::InvalidTaskPath then "invalid_task_path"
+        when Hive::ConcurrentRunError then "concurrent_run"
+        when Hive::ConfigError then "config"
+        when Hive::GitError then "git"
+        when Hive::InternalError then "internal"
+        else "error"
+        end
+      end
+
+      def envelope_serialization_failure_policy = :raise
+
+      private
+
+      def do_call
+        validate_arguments!
+        task = resolve_task
+        stage = resolve_human_stage!(task)
+        outcome = resolve_outcome!(stage)
+        state_body = self.class.read_state_file(File.join(task.folder, stage.state_file))
+        record = self.class.latest_record_from_content(state_body)
+        expected_dir = stage.dir
+        actual_dir = "#{task.stage_index}-#{task.stage_name}"
+
+        if actual_dir != expected_dir
+          return emit_replay_or_stale!(task, stage, outcome, record)
+        end
+
+        marker = Hive::Markers.current_from_content(state_body)
+        if marker.name == :waiting
+          current_id = marker.attrs["decision_id"].to_s
+          unless current_id.match?(/\A[0-9a-f]{16}\z/) && current_id == @decision_id
+            return emit_same_or_conflicting!(task, stage, outcome, record) if
+              current_record?(record, @decision_id)
+
+            raise Hive::WrongStage.new(
+              "decision observation is stale; task or decision identity changed",
+              current_stage: actual_dir, target_stage: expected_dir
+            )
+          end
+        elsif current_record?(record, @decision_id)
+          return emit_same_or_conflicting!(task, stage, outcome, record)
+        else
+          raise Hive::WrongStage.new(
+            "task #{task.slug} is not awaiting a decision at #{stage.dir}",
+            current_stage: actual_dir, target_stage: expected_dir
+          )
+        end
+
+        outcome.complete ? complete!(task, stage, outcome, @decision_id) : return_to_stage!(task, stage, outcome, @decision_id)
+      end
+
+      def validate_arguments!
+        raise Hive::InvalidTaskPath, "--from must name the expected human stage" if @from.empty?
+        unless @decision_id.match?(/\A[0-9a-f]{16}\z/)
+          raise Hive::InvalidTaskPath,
+                "--decision-id must be the 16-character identity reported for this human-stage visit"
+        end
+        raise Hive::InvalidTaskPath, "outcome must be a non-empty name" if @outcome_name.empty?
+        return if @note.nil? || @note.is_a?(String)
+
+        raise Hive::InvalidTaskPath, "--note must be text"
+      end
+
+      def resolve_task
+        Hive::TaskResolver.new(@target, project_filter: @project_filter).resolve
+      end
+
+      def resolve_human_stage!(task)
+        dir = task.workflow.resolve_stage_ref(@from) ||
+              raise(Hive::InvalidTaskPath,
+                    "unknown --from stage #{@from.inspect}; valid: #{task.workflow.stage_dirs.join(', ')}")
+        stage = task.workflow.stage_for_dir(dir)
+        return stage if stage.kind == :human
+
+        raise Hive::InvalidTaskPath, "stage #{dir} is not a human stage"
+      end
+
+      def resolve_outcome!(stage)
+        stage.outcomes[@outcome_name] ||
+          raise(Hive::InvalidTaskPath,
+                "unknown outcome #{@outcome_name.inspect} for #{stage.dir}; " \
+                "allowed: #{stage.outcomes.keys.join(', ')}")
+      end
+
+      def current_record?(record, decision_id)
+        record && !decision_id.empty? && record["decision_id"] == decision_id
+      end
+
+      def emit_replay_or_stale!(task, stage, outcome, record)
+        if record && record["from"] == stage.name && record["decision_id"] == @decision_id
+          return emit_same_or_conflicting!(task, stage, outcome, record)
+        end
+
+        raise Hive::WrongStage.new(
+          "task is at #{task.stage_index}-#{task.stage_name} but --from expected #{stage.dir}",
+          current_stage: "#{task.stage_index}-#{task.stage_name}", target_stage: stage.dir
+        )
+      end
+
+      def emit_same_or_conflicting!(task, stage, outcome, record)
+        unless record["outcome"] == outcome.name
+          raise Hive::WrongStage.new(
+            "decision #{record['decision_id']} already recorded #{record['outcome'].inspect}; " \
+            "cannot apply conflicting outcome #{outcome.name.inspect}",
+            current_stage: "#{task.stage_index}-#{task.stage_name}", target_stage: stage.dir
+          )
+        end
+
+        emit_success(task, stage, outcome, record, applied: false)
+      end
+
+      def complete!(task, stage, outcome, decision_id)
+        record = nil
+        snapshot = nil
+        meta_snapshot = nil
+        ops = Hive::GitOps.new(task.project_root)
+
+        Hive::Lock.with_commit_lock(task.hive_state_path) do
+          begin
+            Hive::Lock.with_task_lock(task.folder, slug: task.slug, op: "decide") do
+              locked = Hive::Task.new(task.folder)
+              locked_record = self.class.latest_record(File.join(locked.folder, stage.state_file))
+              return emit_same_or_conflicting!(locked, stage, outcome, locked_record) if
+                current_record?(locked_record, decision_id)
+
+              validate_current_decision!(locked, stage, decision_id)
+              artifact_path = File.join(locked.folder, outcome.artifact)
+              validate_publishable_artifact!(artifact_path, locked.folder, stage, outcome)
+              record = decision_record(
+                locked, stage, outcome, decision_id,
+                artifact_path: artifact_path, artifact_status: "publish_ready"
+              )
+              snapshot = snapshot_files(locked.folder, [ stage.state_file ])
+              meta_snapshot = Hive::TaskMeta.snapshot(locked.folder)
+              record["decided_at"] = Hive::TaskMeta.write_completed_at_once(
+                locked.folder, Time.iso8601(record.fetch("decided_at"))
+              )
+              write_decision_record(File.join(locked.folder, stage.state_file), record)
+            end
+            ops.hive_commit(stage_name: stage.dir, slug: task.slug,
+                            action: "decide #{stage.name} #{outcome.name}")
+          rescue StandardError, Interrupt
+            if snapshot
+              restore_files(task.folder, snapshot)
+              Hive::TaskMeta.restore(task.folder, meta_snapshot) if meta_snapshot
+              restage_restored_files(
+                task, snapshot, extra_names: meta_snapshot ? [ Hive::TaskMeta::FILENAME ] : []
+              )
+            end
+            raise
+          end
+        end
+
+        emit_success(task, stage, outcome, record, applied: true)
+      end
+
+      def validate_publishable_artifact!(artifact_path, task_folder, stage, outcome)
+        expanded = File.expand_path(artifact_path)
+        task_root = "#{File.expand_path(task_folder)}/"
+        raise Hive::WrongStage, "artifact leaves the task folder" unless expanded.start_with?(task_root)
+
+        stat = File.lstat(expanded)
+        raise Hive::WrongStage, "artifact must be a regular task-local file" unless stat.file? && !stat.symlink?
+
+        flags = File::RDONLY
+        flags |= File::NOFOLLOW if File.const_defined?(:NOFOLLOW)
+        body = File.open(expanded, flags) do |file|
+          opened = file.stat
+          unless opened.file? && opened.dev == stat.dev && opened.ino == stat.ino
+            raise Hive::WrongStage, "artifact identity changed while opening it"
+          end
+
+          file.read
+        end
+        publishable = body && !Hive::Markers.without_markers(body).strip.empty?
+        return if publishable
+
+        raise Hive::WrongStage.new(
+          "outcome #{outcome.name.inspect} requires non-empty artifact #{outcome.artifact.inspect}",
+          current_stage: stage.dir, target_stage: stage.dir
+        )
+      rescue Hive::WrongStage, SystemCallError, IOError
+        raise Hive::WrongStage.new(
+          "outcome #{outcome.name.inspect} requires non-empty artifact #{outcome.artifact.inspect}",
+          current_stage: stage.dir, target_stage: stage.dir
+        )
+      end
+
+      def return_to_stage!(task, stage, outcome, decision_id)
+        target = task.workflow.stage_named(outcome.to)
+        return repeat_stage!(task, stage, outcome, decision_id) if target.dir == stage.dir
+
+        record = decision_record(task, stage, outcome, decision_id)
+        snapshot = nil
+        mutation = lambda do |locked|
+          validate_current_decision!(locked, stage, decision_id)
+          snapshot = snapshot_files(locked.folder, [ stage.state_file, target.state_file ])
+          write_decision_record(File.join(locked.folder, stage.state_file), record)
+          Hive::Markers.set(File.join(locked.folder, target.state_file), :waiting)
+        end
+
+        begin
+          Hive::Lock.with_commit_lock(task.hive_state_path) do
+            Hive::Commands::Approve.new(
+              task.slug, to: target.dir, from: stage.dir, project: task.project_name,
+              force: true, quiet: true, observation_guard: mutation, commit_lock: false
+            ).call
+          end
+        rescue Hive::WrongStage => error
+          folder = File.directory?(task.folder) ? task.folder : File.join(
+            task.hive_state_path, "stages", target.dir, task.slug
+          )
+          restore_files(folder, snapshot) if snapshot
+          replay = resolve_task
+          replay_record = self.class.latest_record(File.join(replay.folder, stage.state_file))
+          return emit_same_or_conflicting!(replay, stage, outcome, replay_record) if
+            current_record?(replay_record, decision_id)
+
+          raise error
+        rescue StandardError, Interrupt
+          folder = File.directory?(task.folder) ? task.folder : File.join(
+            task.hive_state_path, "stages", target.dir, task.slug
+          )
+          restore_files(folder, snapshot) if snapshot
+          raise
+        end
+
+        moved = Hive::Task.new(File.join(task.hive_state_path, "stages", target.dir, task.slug))
+        emit_success(moved, stage, outcome, record, applied: true)
+      end
+
+      def repeat_stage!(task, stage, outcome, decision_id)
+        record = decision_record(task, stage, outcome, decision_id)
+        snapshot = nil
+        ops = Hive::GitOps.new(task.project_root)
+
+        Hive::Lock.with_commit_lock(task.hive_state_path) do
+          begin
+            Hive::Lock.with_task_lock(task.folder, slug: task.slug, op: "decide") do
+              locked = Hive::Task.new(task.folder)
+              locked_record = self.class.latest_record(File.join(locked.folder, stage.state_file))
+              return emit_same_or_conflicting!(locked, stage, outcome, locked_record) if
+                current_record?(locked_record, decision_id)
+
+              validate_current_decision!(locked, stage, decision_id)
+              snapshot = snapshot_files(locked.folder, [ stage.state_file ])
+              state_path = File.join(locked.folder, stage.state_file)
+              write_decision_record(state_path, record)
+              Hive::Markers.set(
+                state_path, :waiting, "decision_id" => SecureRandom.hex(8)
+              )
+            end
+            ops.hive_commit(
+              stage_name: stage.dir, slug: task.slug,
+              action: "decide #{stage.name} #{outcome.name}"
+            )
+          rescue StandardError, Interrupt
+            if snapshot
+              restore_files(task.folder, snapshot)
+              restage_restored_files(task, snapshot)
+            end
+            raise
+          end
+        end
+
+        emit_success(Hive::Task.new(task.folder), stage, outcome, record, applied: true)
+      end
+
+      def validate_current_decision!(task, stage, decision_id)
+        actual = "#{task.stage_index}-#{task.stage_name}"
+        body = self.class.read_state_file(File.join(task.folder, stage.state_file))
+        marker = Hive::Markers.current_from_content(body)
+        return if actual == stage.dir && marker.name == :waiting && marker.attrs["decision_id"] == decision_id
+
+        raise Hive::WrongStage.new(
+          "decision observation is stale; task or decision identity changed",
+          current_stage: actual, target_stage: stage.dir
+        )
+      end
+
+      def decision_record(task, stage, outcome, decision_id, artifact_path: nil, artifact_status: nil)
+        {
+          "schema_version" => 1,
+          "decision_id" => decision_id,
+          "from" => stage.name,
+          "outcome" => outcome.name,
+          "note" => @note,
+          "artifact" => outcome.artifact,
+          "artifact_path" => artifact_path,
+          "artifact_status" => artifact_status,
+          "to" => outcome.to,
+          "decided_at" => @clock.call.utc.iso8601
+        }
+      end
+
+      def write_decision_record(path, record)
+        Hive::Markers.with_markers_lock(path) do
+          body = self.class.read_state_file(path) || ""
+          complete = Hive::Markers.build_marker(
+            "COMPLETE", "decision_id" => record.fetch("decision_id"), "outcome" => record.fetch("outcome")
+          )
+          updated, count = Hive::Markers.replace_last_marker(body, complete)
+          updated = "#{updated}#{updated.end_with?("\n") || updated.empty? ? "" : "\n"}#{complete}\n" if count.zero?
+          updated = "#{updated}#{updated.end_with?("\n") ? "" : "\n"}" \
+                    "<!-- HIVE_DECISION_V1 #{JSON.generate(record)} -->\n"
+          Hive::Markers.write_atomic(path, updated)
+        end
+      end
+
+      def snapshot_files(folder, names)
+        names.uniq.to_h do |name|
+          path = File.join(folder, name)
+          body = self.class.read_state_file(path, binary: true)
+          [ name, { existed: !body.nil?, body: body } ]
+        end
+      end
+
+      def restore_files(folder, snapshots)
+        snapshots.each do |name, snapshot|
+          path = File.join(folder, name)
+          if snapshot.fetch(:existed)
+            Hive::Markers.write_atomic(path, snapshot.fetch(:body))
+          else
+            File.delete(path) if File.exist?(path) || File.symlink?(path)
+          end
+        end
+      end
+
+      def restage_restored_files(task, snapshots, extra_names: [])
+        ops = Hive::GitOps.new(task.project_root)
+        (snapshots.keys + extra_names).uniq.each do |name|
+          rel = File.join("stages", "#{task.stage_index}-#{task.stage_name}", task.slug, name)
+          ops.run_git!("-C", task.hive_state_path, "add", "-A", "--", rel)
+        end
+      rescue Hive::GitError
+        nil
+      end
+
+      def emit_success(task, stage, outcome, record, applied:)
+        current_stage = "#{task.stage_index}-#{task.stage_name}"
+        next_action = if outcome.complete
+          { "kind" => Hive::Schemas::NextActionKind::NO_OP, "reason" => "workflow_complete" }
+        else
+          action = Hive::TaskAction.for(task, Hive::Markers.current(task.state_file))
+          { "kind" => action.key, "command" => action.command }
+        end
+        payload = {
+          "schema" => "hive-decide",
+          "schema_version" => Hive::Schemas::SCHEMA_VERSIONS.fetch("hive-decide"),
+          "ok" => true,
+          "applied" => applied,
+          "noop" => !applied,
+          "slug" => task.slug,
+          "workflow" => task.workflow.id.to_s,
+          "from_stage" => stage.dir,
+          "outcome" => outcome.name,
+          "decision_id" => record.fetch("decision_id"),
+          "note" => record["note"],
+          "decided_at" => record.fetch("decided_at"),
+          "completed" => outcome.complete,
+          "artifact" => record["artifact"],
+          "artifact_path" => record["artifact_path"],
+          "to" => record["to"],
+          "current_stage" => current_stage,
+          "task_folder" => task.folder,
+          "next_action" => next_action
+        }
+        puts JSON.generate(SUCCESS_KEYS.to_h { |key| [ key, payload.fetch(key) ] }) if @json
+        unless @json
+          puts "hive: decided #{task.slug} #{stage.name}=#{outcome.name}"
+          puts "  current_stage: #{current_stage}"
+        end
+        payload
+      end
+    end
+  end
+end
diff --git a/lib/hive/commands/init.rb b/lib/hive/commands/init.rb
index e1242f3281..82748ee38d 100644
--- a/lib/hive/commands/init.rb
+++ b/lib/hive/commands/init.rb
@@ -23,6 +23,19 @@
 module Hive
   module Commands
     class Init
+      include Hive::Schemas::EnvelopeEmitter
+
+      class PreconditionError < Hive::Error
+        attr_reader :error_kind
+
+        def initialize(message, error_kind:)
+          super(message)
+          @error_kind = error_kind
+        end
+      end
+
+      ProjectRegistrationCollision = Hive::Config::ProjectRegistrationCollision
+
       INIT_MAIN_CHECKOUT_PATHS = %w[
         .gitignore
         .llm-wiki/config.json
@@ -82,6 +95,7 @@ def initialize(descriptor:, source:)
 
       def initialize(project_path, force: false, json: false, prompts: nil,
                      workflow: nil, new_workflow: nil, refactor_patrol: nil,
+                     minimal: false, preview: false,
                      workflow_input: $stdin, workflow_output: $stderr,
                      provisioning_input: $stdin, provisioning_output: $stdout,
                      provisioning_error: $stderr, preflight_inspector: nil,
@@ -91,6 +105,11 @@ def initialize(project_path, force: false, json: false, prompts: nil,
         @json = json
         @workflow_name = workflow
         @new_workflow = new_workflow
+        @minimal = minimal
+        @preview = preview
+        unless [ true, false ].include?(@minimal) && [ true, false ].include?(@preview)
+          raise ArgumentError, "minimal and preview must be boolean"
+        end
         unless refactor_patrol.nil? || [ true, false ].include?(refactor_patrol)
           raise ArgumentError, "refactor_patrol must be true, false, or nil"
         end
@@ -123,6 +142,50 @@ def initialize(project_path, force: false, json: false, prompts: nil,
       end
 
       def call
+        return do_call unless minimal_json?
+
+        call_with_envelope { do_call }
+      end
+
+      def envelope_schema
+        @preview ? "hive-init-preview" : "hive-init"
+      end
+
+      def envelope_error_kind(error)
+        case error
+        when PreconditionError then error.error_kind
+        when ProjectRegistrationCollision then "project_name_collision"
+        when Hive::AlreadyInitialized then "already_initialized"
+        when Hive::Commands::Workflow::UsageError then "usage"
+        when Hive::ConcurrentRunError then "concurrent_run"
+        when Hive::ConfigError then "config"
+        when Hive::GitError then "git"
+        when Hive::InternalError then "internal"
+        else "error"
+        end
+      end
+
+      def envelope_extras_for(error)
+        extras = {
+          "minimal" => true,
+          "preview" => @preview,
+          "project" => File.basename(@project_path),
+          "path" => @project_path,
+          "workflow" => @new_workflow.to_s
+        }
+        extras["value"] = error.value if error.respond_to?(:value) && !error.value.nil?
+        if error.respond_to?(:suggested_id) && !error.suggested_id.nil?
+          extras["suggested_id"] = error.suggested_id
+        end
+        if error.is_a?(ProjectRegistrationCollision)
+          extras["existing_path"] = error.existing_path
+        end
+        extras
+      end
+
+      def envelope_serialization_failure_policy = :raise
+
+      def do_call
         validate_git_repo!
         # Argument-level mutual exclusivity is checked before validate_clean_tree!
         # so a dirty repo passing both flags gets the clearer usage error instead
@@ -130,9 +193,14 @@ def call
         if @workflow_name && @new_workflow
           raise Hive::Commands::Workflow::UsageError, "--workflow and --new-workflow are mutually exclusive"
         end
+        validate_minimal_arguments!
         validate_clean_tree! unless @force
 
         ops = Hive::GitOps.new(@project_path)
+        if @minimal
+          init_minimal(ops)
+          return
+        end
         reject_existing_refactor_patrol_selector!(ops)
         if @new_workflow
           init_with_new_workflow(ops)
@@ -194,6 +262,157 @@ def call
         raise Hive::InternalError, "init failed: #{e.class}: #{e.message}"
       end
 
+      def validate_minimal_arguments!
+        if @preview && !@minimal
+          raise Hive::Commands::Workflow::UsageError, "--preview requires --minimal"
+        end
+        return unless @minimal
+
+        raise Hive::Commands::Workflow::UsageError, "--minimal requires --new-workflow ID" unless @new_workflow
+        raise Hive::Commands::Workflow::UsageError, "--minimal never accepts --force" if @force
+        if @refactor_patrol
+          raise Hive::Commands::Workflow::UsageError,
+                "--minimal fixes refactor patrol off; do not pass --refactor-patrol"
+        end
+      end
+
+      def init_minimal(ops)
+        ensure_minimal_target_fresh!(ops)
+        ensure_minimal_registration_available!
+        id = Hive::Commands::Workflow.normalize_and_validate_id!(
+          @new_workflow, project_root: @project_path
+        )
+        if Hive::Commands::Workflow.scaffold_collision?(id, project_root: @project_path)
+          suggested = Hive::Commands::Workflow.available_id(id, project_root: @project_path)
+          raise Hive::Commands::Workflow::UsageError.new(
+            "workflow scaffold already exists for #{id.inspect}; try #{suggested.inspect}",
+            value: id, suggested_id: suggested
+          )
+        end
+
+        if @preview
+          emit_minimal_preview(ops, id)
+        else
+          scaffold_and_bind_fresh_minimal(ops, id)
+        end
+      end
+
+      def ensure_minimal_target_fresh!(ops)
+        if ops.hive_state_branch_exists? ||
+           File.exist?(ops.hive_state_path) ||
+           File.symlink?(ops.hive_state_path)
+          raise Hive::AlreadyInitialized,
+                "minimal initialization requires a fresh target with no hive/state branch or .hive-state path"
+        end
+      end
+
+      def ensure_minimal_registration_available!
+        name = File.basename(@project_path)
+        existing = Hive::Config.registered_projects.find { |project| project["name"] == name }
+        return unless existing
+        return if File.expand_path(existing.fetch("path")) == @project_path
+
+        raise ProjectRegistrationCollision.new(
+          "minimal initialization refuses to replace registered project #{name.inspect} " \
+          "at #{existing.fetch('path')}; rename the target directory or forget the existing project first",
+          name: name, existing_path: existing.fetch("path")
+        )
+      end
+
+      def scaffold_and_bind_fresh_minimal(ops, id)
+        answers = minimal_answers
+        project_config_content = render_project_config(ops, answers: answers, default_workflow: id)
+        scaffold = nil
+        entry = initialize_project_state(ops, content: project_config_content) do
+          scaffold = scaffold_descriptor_commit!(ops, id)
+        end
+        extra = scaffold_payload(scaffold.fetch(:paths)).merge("minimal" => true)
+        if @json
+          emit_json_summary(entry: entry, ops: ops, answers: answers, workflow: id, extra: extra)
+        else
+          print_summary(
+            entry: entry, ops: ops, answers: answers, workflow: id,
+            scaffold_paths: scaffold.fetch(:paths)
+          )
+        end
+        # Minimal initialization intentionally omits service installation,
+        # daemon-autostart persistence, optional agent provisioning, patrol,
+        # babysitting, and timers. Core project registration and wiki/context
+        # hooks were installed transactionally by initialize_project_state.
+      end
+
+      def emit_minimal_preview(ops, id)
+        payload = minimal_preview_payload(ops, id)
+        if @json
+          puts JSON.generate(payload)
+        else
+          puts "hive: minimal init preview for #{@project_path}"
+          puts "  workflow: #{id}"
+          puts "  creates: #{payload.fetch('project_files').length} project/context paths plus hive/state"
+          puts "  automation: disabled"
+        end
+        payload
+      rescue Errno::EPIPE
+        nil
+      end
+
+      def minimal_preview_payload(ops, id)
+        answers = minimal_answers
+        hive_state_path = ops.hive_state_path
+        workflows = File.join(hive_state_path, "workflows")
+        {
+          "schema" => "hive-init-preview",
+          "schema_version" => Hive::Schemas::SCHEMA_VERSIONS.fetch("hive-init-preview"),
+          "ok" => true,
+          "preview" => true,
+          "minimal" => true,
+          "project" => File.basename(@project_path),
+          "path" => @project_path,
+          "default_branch" => ops.default_branch,
+          "workflow" => id,
+          "profile" => "minimal",
+          "template" => "blank",
+          "project_files" => INIT_MAIN_CHECKOUT_PATHS.map { |path| File.join(@project_path, path) },
+          "state_worktree" => {
+            "branch" => Hive::GitOps::HIVE_BRANCH,
+            "path" => hive_state_path,
+            "config_path" => File.join(hive_state_path, "config.yml"),
+            "workflow_descriptor" => File.join(workflows, "#{id}.yml"),
+            "workflow_instruction" => File.join(workflows, id, "work.md")
+          },
+          "context_integration" => {
+            "llm_wiki" => true,
+            "agent_instructions" => true,
+            "post_commit_hook" => true,
+            "scheduler" => false
+          },
+          "global_registration" => {
+            "enabled" => true,
+            "config_path" => Hive::Config.global_config_path,
+            "name" => File.basename(@project_path),
+            "status" => "available"
+          },
+          "services" => {
+            "daemon_install" => false,
+            "daemon_autostart" => false,
+            "babysitter_timer" => false,
+            "llm_wiki_timer" => false
+          },
+          "background_automation" => {
+            "daemon_dispatch" => answers.fetch("daemon_enabled"),
+            "patrol" => answers.fetch("patrol_mode") != "off",
+            "refactor_patrol" => answers.fetch("refactor_patrol_enabled"),
+            "adhoc_auto_fix" => answers.fetch("adhoc_auto_fix"),
+            "babysitter" => answers.fetch("babysitter_enabled")
+          },
+          "task_creation" => false
+        }
+      end
+
+      def minimal_answers
+        Hive::Commands::Init::Prompts.minimal_defaults
+      end
+
       def init_with_new_workflow(ops)
         id = Hive::Commands::Workflow.normalize_and_validate_id!(@new_workflow)
         if ops.hive_state_branch_exists?
@@ -247,7 +466,7 @@ def scaffold_descriptor_commit!(ops, id)
             )
           end
           scaffold
-        rescue StandardError
+        rescue StandardError, Interrupt
           # Re-derive paths from `scaffold` rather than the local `paths` (bound
           # just above): if scaffold_files! raised before its assignment, the
           # local is unset — the `if scaffold` guard makes this the only safe read.
@@ -270,7 +489,7 @@ def scaffold_and_bind_existing(ops, id)
             write_default_workflow!(cfg_path, id)
             begin
               Hive::Commands::Workflow.commit_workflow_scaffold(ops, slug: id, pathspecs: pathspecs)
-            rescue StandardError
+            rescue StandardError, Interrupt
               # hive_commit stages config.yml + descriptor + instruction dir via
               # a per-pathspec `git add -A -- <path>` before it runs; a failure
               # AFTER staging leaves those entries in the long-lived .hive-state
@@ -287,7 +506,7 @@ def scaffold_and_bind_existing(ops, id)
               raise
             end
           end
-        rescue StandardError
+        rescue StandardError, Interrupt
           # Filesystem-only scaffold teardown stays outside the lock: it removes
           # working-tree files, not index entries, so it cannot be swept into a
           # concurrent commit.
@@ -395,7 +614,7 @@ def update_existing_default_workflow!(ops, workflow_id)
               action: "set to #{next_value}",
               pathspecs: [ "config.yml" ]
             )
-          rescue StandardError
+          rescue StandardError, Interrupt
             restore_config_snapshot(cfg_path, before)
             raise
           end
@@ -564,6 +783,7 @@ def fieldless_in_flight_tasks(ops)
 
       def initialize_project_state(ops, content:, workflow: nil, &after_bootstrap)
         rollback_on_failure = false
+        registration_written = false
         side_effect_snapshot = capture_init_side_effect_snapshot
         begin
           rollback_on_failure = true
@@ -574,12 +794,20 @@ def initialize_project_state(ops, content:, workflow: nil, &after_bootstrap)
           ops.add_hive_state_to_master_gitignore!
           Hive::LlmWikiBootstrap.install!(@project_path, post_commit_hook: false, scheduler: false)
           ops.commit_llm_wiki_bootstrap!
-          Hive::LlmWikiBootstrap.install_runtime_hooks!(@project_path)
+          Hive::LlmWikiBootstrap.install_runtime_hooks!(@project_path, scheduler: !@minimal)
 
-          entry = Hive::Config.register_project(name: File.basename(@project_path), path: @project_path)
+          entry = Hive::Config.register_project(
+            name: File.basename(@project_path),
+            path: @project_path,
+            replace_existing: !@minimal
+          )
+          registration_written = true
           after_bootstrap&.call
           entry
-        rescue StandardError
+        rescue StandardError, Interrupt
+          unless registration_written
+            side_effect_snapshot.fetch(:paths).delete(Hive::Config.global_config_path)
+          end
           rollback_partial_init(ops, side_effect_snapshot: side_effect_snapshot) if rollback_on_failure
           raise
         end
@@ -1067,6 +1295,11 @@ def collect_prompt_answers
       def validate_git_repo!
         out, _err, status = Open3.capture3("git", "-C", @project_path, "rev-parse", "--git-common-dir")
         unless status.success?
+          if minimal_json?
+            raise PreconditionError.new(
+              "not a git repository: #{@project_path}", error_kind: "not_git_repository"
+            )
+          end
           warn "hive: not a git repository: #{@project_path}"
           exit 1
         end
@@ -1075,6 +1308,12 @@ def validate_git_repo!
         expected = File.join(@project_path, ".git")
         return if File.expand_path(common) == File.expand_path(expected)
 
+        if minimal_json?
+          raise PreconditionError.new(
+            "target appears to be inside a worktree (common dir #{common}); init must run on the main checkout",
+            error_kind: "main_checkout_required"
+          )
+        end
         warn "hive: target appears to be inside a worktree (common dir #{common}); init must run on the main checkout"
         exit 1
       end
@@ -1088,10 +1327,20 @@ def validate_clean_tree!
         modified = out.lines.reject { |l| l.start_with?("??") }
         return if modified.empty?
 
+        if minimal_json?
+          raise PreconditionError.new(
+            "uncommitted modifications to tracked files; commit before minimal init",
+            error_kind: "dirty_worktree"
+          )
+        end
         warn "hive: uncommitted modifications to tracked files; commit or pass --force"
         exit 1
       end
 
+      def minimal_json?
+        @minimal && @json
+      end
+
       def write_per_project_config(ops, content:)
         cfg_path = File.join(ops.hive_state_path, "config.yml")
         return if File.exist?(cfg_path)
diff --git a/lib/hive/commands/init/prompts.rb b/lib/hive/commands/init/prompts.rb
index 4b17ca284b..22f134c292 100644
--- a/lib/hive/commands/init/prompts.rb
+++ b/lib/hive/commands/init/prompts.rb
@@ -147,6 +147,37 @@ def initialize(input: $stdin, output: $stderr, summary_io: $stdout,
           end
         end
 
+        # Neutral non-interactive profile used exclusively by
+        # `hive init --new-workflow ID --minimal`. It keeps the core project,
+        # context hooks, and inherited agent choices while disabling every
+        # optional background or automatic workflow.
+        def self.minimal_defaults
+          {
+            "planning_agent" => DEFAULT_PLANNING_AGENT,
+            "claude_mode" => DEFAULT_CLAUDE_MODE,
+            "claude_permission_mode" => DEFAULT_CLAUDE_PERMISSION_MODE,
+            "claude_model" => DEFAULT_CLAUDE_MODEL,
+            "claude_effort" => DEFAULT_CLAUDE_EFFORT,
+            "development_agent" => DEFAULT_DEVELOPMENT_AGENT,
+            "enabled_reviewers" => [],
+            "patrol_reviewers" => [],
+            "patrol_mode" => "off",
+            "triage_bias" => DEFAULT_TRIAGE_BIAS,
+            "adhoc_auto_fix" => false,
+            "refactor_patrol_enabled" => false,
+            "budgets" => limit_defaults("budget_usd"),
+            "timeouts" => limit_defaults("timeout_sec"),
+            "daemon_enabled" => false,
+            "babysitter_enabled" => false,
+            "daemon_autostart" => false
+          }
+        end
+
+        def self.limit_defaults(section)
+          LIMIT_KEYS.to_h { |key| [ key, Hive::Config::DEFAULTS.fetch(section).fetch(key) ] }
+        end
+        private_class_method :limit_defaults
+
         # Run the prompt flow (or short-circuit to defaults).
         # Returns the answers hash with shape:
         #   {
diff --git a/lib/hive/commands/new.rb b/lib/hive/commands/new.rb
index 08988594b0..6c44e9c2b2 100644
--- a/lib/hive/commands/new.rb
+++ b/lib/hive/commands/new.rb
@@ -2,12 +2,18 @@
 require "fileutils"
 require "time"
 require "erb"
+require "digest"
+require "json"
+require "tmpdir"
 require "hive/config"
 require "hive/git_ops"
 require "hive/lock"
 require "hive/paths"
 require "hive/task_counter"
 require "hive/task_meta"
+require "hive/task"
+require "hive/task_action"
+require "hive/markers"
 require "hive/workflows"
 require "hive/workflow_selection"
 require "hive/workflow_package/managed_store"
@@ -19,6 +25,9 @@
 module Hive
   module Commands
     class New
+      include Hive::Schemas::EnvelopeEmitter
+
+      SCHEMA = "hive-new".freeze
       RESERVED_SLUGS = %w[
         head fetch_head orig_head merge_head
         master main origin hive hive-state hive_state state
@@ -70,9 +79,13 @@ class UnregisteredProjectWorkflow < TypedValueError; end
       # config.yml (mirroring UnregisteredProjectWorkflow) instead of crashing
       # with a raw Psych backtrace that escapes call's rescue list.
       class ProjectConfigUnreadable < TypedValueError; end
+      class IdempotencyConflict < TypedValueError
+        def exit_code = Hive::ExitCodes::USAGE
+      end
+      PreparedAttachment = Data.define(:snapshot_path, :destination, :name, :sha256)
 
       def initialize(project_name, text, slug_override: nil, body_override: nil, attachments: [], base: nil,
-                     depends_on: nil, workflow: nil)
+                     depends_on: nil, workflow: nil, idempotency_key: nil, json: false)
         @project_name = project_name
         @text = text.to_s
         @slug_override = slug_override
@@ -81,6 +94,11 @@ def initialize(project_name, text, slug_override: nil, body_override: nil, attac
         @base = base
         @depends_on = depends_on
         @workflow_name = workflow
+        @idempotency_key_raw = idempotency_key
+        # Machine-readable creation was added for idempotent automation.
+        # Preserve the legacy plain-text contract for a bare `hive new --json`
+        # whose caller did not opt into that side-effect boundary.
+        @json = json && !idempotency_key.nil?
       end
 
       class << self
@@ -103,13 +121,9 @@ def name_generator_spawn_configured?
       # raising so they can rescue typed errors without losing the alt
       # screen.
       def call
-        call!
-      rescue ProjectNotFound, InvalidSlugError, InvalidDependencyError, InvalidBaseError,
-             InvalidDraftPrCombination, InvalidAttachmentError,
-             SlugCollisionError, UnregisteredProjectWorkflow, ProjectConfigUnreadable,
-             Hive::Workflows::UnknownWorkflow, Hive::UnsupportedProjectConfigError,
-             SystemCallError, IOError => e
-        warn "hive: #{e.message}"
+        call_with_envelope { call! }
+      rescue Hive::Error, SystemCallError, IOError => e
+        warn "hive: #{e.message}" unless @json
         # Honor each typed error's contract exit code (e.g. UnknownWorkflow →
         # USAGE 64 so an agent can tell "typo'd --workflow, fix the flag" from a
         # transient GENERIC failure); fall back to 1 for the untyped
@@ -117,6 +131,28 @@ def call
         exit(e.respond_to?(:exit_code) ? e.exit_code : 1)
       end
 
+      def envelope_schema = SCHEMA
+
+      def envelope_error_kind(error)
+        case error
+        when IdempotencyConflict, InvalidBaseError, InvalidDraftPrCombination,
+             Hive::Workflows::UnknownWorkflow then "usage"
+        when Hive::ConfigError, ProjectConfigUnreadable, UnregisteredProjectWorkflow then "config"
+        when Hive::ConcurrentRunError then "concurrent_run"
+        when Hive::InternalError then "internal"
+        else "error"
+        end
+      end
+
+      def envelope_extras_for(error)
+        extras = {}
+        extras["value"] = error.value if error.respond_to?(:value) && !error.value.nil?
+        extras["idempotency_key"] = @idempotency_key if @idempotency_key
+        extras
+      end
+
+      def envelope_serialization_failure_policy = :raise
+
       def call!
         project = Hive::Config.find_project(@project_name)
         unless project
@@ -126,8 +162,8 @@ def call!
           )
         end
 
-        slug = @slug_override || derive_slug(@text)
-        validate_slug!(slug)
+        @idempotency_key = validate_idempotency_key!
+        prepare_idempotent_attachments! if @idempotency_key
         depends_on = normalize_optional(@depends_on)
         depends_on = validate_dependency!(depends_on) if depends_on
         workflow_info = resolve_workflow(project)
@@ -147,51 +183,211 @@ def call!
         end
         base_branch = effective_base_branch(project, workflow)
         entry_stage = workflow.stages.first
-
         hive_state = project["hive_state_path"]
+        fingerprint = input_fingerprint(
+          workflow_info, depends_on: depends_on, base_branch: base_branch
+        ) if @idempotency_key
+        slug = @slug_override || derive_slug(@text)
+        validate_slug!(slug)
         task_dir = File.join(hive_state, "stages", entry_stage.dir, slug)
-        if File.exist?(task_dir)
-          raise SlugCollisionError.new(
-            "slug collision at #{task_dir} (rare; retry the command)",
-            value: slug
-          )
+        ops = Hive::GitOps.new(project["path"])
+
+        if @idempotency_key
+          result = with_stable_workflow_selection(workflow_info, hive_state) do |stable_selection|
+            Hive::Lock.with_commit_lock(hive_state) do
+              validate_stable_authored_workflow!(workflow_info, hive_state)
+              existing = find_idempotent_task!(hive_state, @idempotency_key, fingerprint)
+              validate_stable_authored_workflow!(workflow_info, hive_state)
+              next { folder: existing.fetch(:folder), created: false } if existing
+
+              create_task_candidate!(
+                task_dir, slug: slug, entry_stage: entry_stage, workflow: workflow,
+                depends_on: depends_on, base_branch: base_branch,
+                workflow_info: workflow_info, hive_state: hive_state,
+                idempotency_key: @idempotency_key, input_fingerprint: fingerprint,
+                stable_selection: stable_selection
+              )
+              begin
+                ops.hive_commit(stage_name: entry_stage.dir, slug: slug, action: "captured")
+              rescue StandardError, Interrupt
+                cleanup_failed_task_commit!(ops, hive_state, task_dir)
+                raise
+              end
+              { folder: task_dir, created: true }
+            end
+          end
+          unless result.fetch(:created)
+            return emit_task_result(result.fetch(:folder), workflow, created: false)
+          end
+
+          spawn_name_generator(task_dir)
+          return emit_task_result(task_dir, workflow, created: true)
+        end
+
+        create_task_candidate!(
+          task_dir, slug: slug, entry_stage: entry_stage, workflow: workflow,
+          depends_on: depends_on, base_branch: base_branch,
+          workflow_info: workflow_info, hive_state: hive_state,
+          idempotency_key: nil, input_fingerprint: nil
+        )
+        Hive::Lock.with_commit_lock(hive_state) do
+          begin
+            ops.hive_commit(stage_name: entry_stage.dir, slug: slug, action: "captured")
+          rescue StandardError, Interrupt
+            cleanup_failed_task_commit!(ops, hive_state, task_dir)
+            raise
+          end
         end
-        FileUtils.mkdir_p(task_dir)
+        spawn_name_generator(task_dir)
+        emit_task_result(task_dir, workflow, created: true)
+      ensure
+        cleanup_prepared_attachments!
+      end
 
+      def create_task_candidate!(task_dir, slug:, entry_stage:, workflow:, depends_on:, base_branch:,
+                                 workflow_info:, hive_state:, idempotency_key:, input_fingerprint:,
+                                 stable_selection: :unlocked)
+        created = false
+        FileUtils.mkdir_p(File.dirname(task_dir))
+        Dir.mkdir(task_dir)
+        created = true
         idea_path = File.join(task_dir, entry_stage.state_file)
-        id = nil
-        begin
-          File.write(idea_path, render_initial_state(slug, @text, body_override: @body_override, workflow: workflow))
-          copy_attachments!(task_dir)
-          id = Hive::TaskCounter.next_or_nil
-          write_task_meta(task_dir, id: id, slug: slug, depends_on: depends_on,
-                          base_branch: base_branch, workflow: workflow,
-                          workflow_info: workflow_info, hive_state: hive_state)
-        rescue StandardError
-          # An idea.md or attachment write failure leaves an orphan
-          # uncommitted task on disk that the snapshot would surface as a
-          # broken entry under the workflow's entry stage dir (`entry_stage.dir`
-          # — `1-inbox` for coding, but different for other workflows). Roll the
-          # directory back so the capture is atomic — either the task is
-          # committed or it never existed.
+        File.write(
+          idea_path,
+          render_initial_state(slug, @text, body_override: @body_override, workflow: workflow)
+        )
+        if entry_stage.kind == :human
+          Hive::Markers.set(idea_path, :waiting, "decision_id" => SecureRandom.hex(8))
+        end
+        copy_attachments!(task_dir)
+        validate_stable_authored_workflow!(workflow_info, hive_state)
+        id = Hive::TaskCounter.next_or_nil
+        write_task_meta(
+          task_dir, id: id, slug: slug, depends_on: depends_on,
+          base_branch: base_branch, workflow: workflow,
+          workflow_info: workflow_info, hive_state: hive_state,
+          idempotency_key: idempotency_key, input_fingerprint: input_fingerprint,
+          stable_selection: stable_selection
+        )
+      rescue Errno::EEXIST
+        if created
           FileUtils.rm_rf(task_dir)
           raise
         end
 
-        ops = Hive::GitOps.new(project["path"])
-        Hive::Lock.with_commit_lock(hive_state) do
-          ops.hive_commit(stage_name: entry_stage.dir, slug: slug, action: "captured")
+        raise SlugCollisionError.new(
+          "slug collision at #{task_dir} (rare; retry the command)",
+          value: slug
+        )
+      rescue StandardError, Interrupt
+        # Once this invocation owns the directory, any idea, attachment, or
+        # metadata failure must remove its uncommitted candidate. An EEXIST
+        # contender never owns the directory and therefore never removes it.
+        FileUtils.rm_rf(task_dir) if created
+        raise
+      end
+
+      def validate_idempotency_key!
+        return nil if @idempotency_key_raw.nil?
+
+        key = @idempotency_key_raw.to_s.strip
+        unless !key.empty? && key.valid_encoding? && key.bytesize <= 512
+          raise IdempotencyConflict.new(
+            "idempotency key must be non-empty UTF-8 text no longer than 512 bytes",
+            value: @idempotency_key_raw.to_s
+          )
         end
-        spawn_name_generator(task_dir)
+        key
+      end
+
+      def input_fingerprint(workflow_info, depends_on:, base_branch:)
+        managed = workflow_info.fetch(:managed)
+        attachments = @prepared_attachments.map do |attachment|
+          {
+            "destination" => attachment.destination,
+            "sha256" => attachment.sha256
+          }
+        end
+        input = {
+          "text" => @text,
+          "body" => @body_override,
+          "attachments" => attachments,
+          "depends_on" => depends_on,
+          "base_branch" => base_branch,
+          "workflow" => workflow_info.fetch(:descriptor).id.to_s,
+          "workflow_commit" => managed&.fetch("source_commit"),
+          "workflow_configuration_digest" => managed&.fetch("configuration_digest"),
+          "workflow_content_digest" => workflow_info[:authored_digest]
+        }
+        ::Digest::SHA256.hexdigest(JSON.generate(input))
+      end
 
-        puts "hive: captured #{idea_path}"
-        target_hint = id ? id.to_s : slug
-        next_stage = workflow.next_stage_after(entry_stage.name)
-        if next_stage
-          puts "next: mv #{task_dir} #{File.join(hive_state, 'stages', "#{next_stage.dir}/")} && hive run #{target_hint}"
+      def find_idempotent_task!(hive_state, key, fingerprint)
+        matches = Dir.glob(File.join(hive_state, "stages", "*", "*", Hive::TaskMeta::FILENAME)).filter_map do |path|
+          folder = File.dirname(path)
+          read = Hive::TaskMeta.read_for_admission(folder)
+          unless read.status == :ok
+            raise IdempotencyConflict.new(
+              "cannot prove idempotency while task metadata is unreadable at #{path}: " \
+              "#{read.error || read.status}",
+              value: key
+            )
+          end
+          meta = read.data
+          { folder: folder, meta: meta } if meta[:idempotency_key] == key
+        end
+        return nil if matches.empty?
+        if matches.length > 1
+          raise IdempotencyConflict.new(
+            "idempotency key #{key.inspect} is already attached to multiple tasks; repair metadata before retrying",
+            value: key
+          )
+        end
+
+        match = matches.first
+        return match if match.dig(:meta, :input_fingerprint) == fingerprint
+
+        raise IdempotencyConflict.new(
+          "idempotency key #{key.inspect} was already used for different input or workflow",
+          value: key
+        )
+      end
+
+      def emit_task_result(task_folder, workflow, created:)
+        task = Hive::Task.new(task_folder)
+        action = Hive::TaskAction.for(task, Hive::Markers.current(task.state_file))
+        payload = {
+          "schema" => SCHEMA,
+          "schema_version" => Hive::Schemas::SCHEMA_VERSIONS.fetch(SCHEMA),
+          "ok" => true,
+          "created" => created,
+          "slug" => task.slug,
+          "workflow" => workflow.id.to_s,
+          "current_stage" => "#{task.stage_index}-#{task.stage_name}",
+          "task_folder" => task.folder,
+          "next_action" => {
+            "kind" => action.key,
+            "command" => action.command,
+            "outcomes" => action.allowed_outcomes
+          }
+        }
+        if @json
+          puts JSON.generate(payload)
+        elsif created
+          puts "hive: captured #{task.state_file}"
+          target_hint = task.id ? task.id.to_s : task.slug
+          next_stage = workflow.next_stage_after(task.stage_name)
+          if next_stage
+            destination = File.join(task.hive_state_path, "stages", "#{next_stage.dir}/")
+            puts "next: mv #{task.folder} #{destination} && hive run #{target_hint}"
+          else
+            puts "next: hive run #{target_hint}"
+          end
         else
-          puts "next: hive run #{target_hint}"
+          puts "hive: idempotent task already exists at #{task.folder}"
+          puts "next: #{action.command}" if action.command
         end
+        payload
       end
 
       def resolve_workflow(project)
@@ -218,31 +414,88 @@ def workflow_resolution(descriptor, project, pin:)
         managed = store.selected(descriptor.id.to_s) { cfg = managed_project_config(project) }
         {
           descriptor: descriptor, pin: pin,
-          managed: managed, managed_cfg: cfg
+          managed: managed, managed_cfg: cfg,
+          authored_digest: authored_workflow_digest(project.fetch("hive_state_path"), descriptor)
         }
       end
 
-      def write_task_meta(task_dir, id:, slug:, depends_on:, base_branch:, workflow:, workflow_info:, hive_state:)
+      def write_task_meta(task_dir, id:, slug:, depends_on:, base_branch:, workflow:, workflow_info:, hive_state:,
+                          idempotency_key: nil, input_fingerprint: nil, stable_selection: :unlocked)
         managed = workflow_info.fetch(:managed)
         managed_cfg = workflow_info.fetch(:managed_cfg, {})
         store = Hive::WorkflowPackage::ManagedStore.new(hive_state)
-        store.with_stable_selection(workflow.id.to_s, cfg: managed_cfg) do |current|
-          if managed
-            unless current && current.fetch("source_commit") == managed.fetch("source_commit") &&
-                   current.fetch("manifest_digest") == managed.fetch("manifest_digest") &&
-                   current.fetch("configuration_digest") == managed.fetch("configuration_digest")
-              raise Hive::ConcurrentRunError.new("managed workflow selection changed while creating the task")
-            end
-          end
+        writer = lambda do |current|
+          validate_stable_selection!(managed, current)
           Hive::TaskMeta.write(
             task_dir, id: id, slug: slug, display_name: nil, depends_on: depends_on,
             base_branch: base_branch,
             workflow: workflow_info.fetch(:pin) ? workflow.id.to_s : nil,
             workflow_commit: managed&.fetch("source_commit"),
             workflow_manifest_digest: managed&.fetch("manifest_digest"),
-            workflow_configuration_digest: managed&.fetch("configuration_digest")
+            workflow_configuration_digest: managed&.fetch("configuration_digest"),
+            idempotency_key: idempotency_key,
+            input_fingerprint: input_fingerprint
           )
         end
+        return writer.call(stable_selection) unless stable_selection == :unlocked
+
+        store.with_stable_selection(workflow.id.to_s, cfg: managed_cfg, &writer)
+      end
+
+      def with_stable_workflow_selection(workflow_info, hive_state)
+        workflow = workflow_info.fetch(:descriptor)
+        store = Hive::WorkflowPackage::ManagedStore.new(hive_state)
+        store.with_stable_selection(
+          workflow.id.to_s, cfg: workflow_info.fetch(:managed_cfg, {})
+        ) do |current|
+          validate_stable_selection!(workflow_info.fetch(:managed), current)
+          yield current
+        end
+      end
+
+      def validate_stable_selection!(managed, current)
+        return unless managed
+        return if current && current.fetch("source_commit") == managed.fetch("source_commit") &&
+          current.fetch("manifest_digest") == managed.fetch("manifest_digest") &&
+          current.fetch("configuration_digest") == managed.fetch("configuration_digest")
+
+        raise Hive::ConcurrentRunError.new("managed workflow selection changed while creating the task")
+      end
+
+      def validate_stable_authored_workflow!(workflow_info, hive_state)
+        expected = workflow_info[:authored_digest]
+        return unless expected
+
+        actual = authored_workflow_digest(hive_state, workflow_info.fetch(:descriptor))
+        return if actual == expected
+
+        raise Hive::ConcurrentRunError,
+              "owner-authored workflow changed while creating the task; retry against the current descriptor"
+      end
+
+      def authored_workflow_digest(hive_state, descriptor)
+        workflows = File.join(hive_state, "workflows")
+        descriptor_path = File.join(workflows, "#{descriptor.id}.yml")
+        return nil unless File.file?(descriptor_path)
+
+        instruction_root = File.join(workflows, descriptor.id.to_s)
+        paths = [ descriptor_path ]
+        if File.directory?(instruction_root)
+          paths.concat(
+            Dir.glob(File.join(instruction_root, "**", "*"), File::FNM_DOTMATCH)
+               .select { |path| File.file?(path) }
+          )
+        end
+        entries = paths.sort.map do |path|
+          [ path.delete_prefix("#{workflows}/"), ::Digest::SHA256.file(path).hexdigest ]
+        end
+        ::Digest::SHA256.hexdigest(JSON.generate(entries))
+      end
+
+      def cleanup_failed_task_commit!(ops, hive_state, task_dir)
+        FileUtils.rm_rf(task_dir)
+        relative = task_dir.delete_prefix("#{File.expand_path(hive_state)}/")
+        ops.run_git!("-C", hive_state, "reset", "-q", "HEAD", "--", relative)
       end
 
       def managed_project_config(project)
@@ -408,27 +661,74 @@ def copy_attachments!(task_dir)
 
         assets_dir = File.join(task_dir, "assets")
         FileUtils.mkdir_p(assets_dir)
-        @attachments.each do |src, dest_name|
-          name = Hive::Tui::Text.sanitize(dest_name.to_s)
-          # `name != File.basename(name)` rejects directory separators
-          # and the like, but `File.basename(".") == "."` and
-          # `File.basename("..") == ".."` — both would otherwise slip
-          # through and either overwrite `assets/` itself or escape it
-          # via FileUtils.cp's path-join. Reject them explicitly.
-          if name.empty? || name == "." || name == ".." || name != File.basename(name)
-            raise InvalidAttachmentError.new("invalid attachment filename '#{name}'", value: name)
+        if @prepared_attachments
+          @prepared_attachments.each do |attachment|
+            destination = File.join(assets_dir, attachment.name)
+            FileUtils.cp(attachment.snapshot_path, destination)
+            next if ::Digest::SHA256.file(destination).hexdigest == attachment.sha256
+
+            raise InvalidAttachmentError.new(
+              "attachment snapshot changed while creating the task",
+              value: attachment.destination
+            )
           end
+        else
+          @attachments.each do |src, dest_name|
+            name = attachment_name!(dest_name)
+            # `attachments:` is a programmatic contract used by the TUI and
+            # tests; callers may pass any absolute source path they captured.
+            # Keep the destination guard strict, but let FileUtils surface
+            # source readability/existence failures directly.
+            src_path = File.expand_path(src.to_s)
+            FileUtils.cp(src_path, File.join(assets_dir, name))
+          end
+        end
+      end
 
-          # `attachments:` is a programmatic contract used by the TUI and
-          # tests; callers may pass any absolute source path they captured.
-          # Keep the destination guard strict, but let FileUtils surface
-          # source readability/existence failures directly.
-          src_path = File.expand_path(src.to_s)
+      def prepare_idempotent_attachments!
+        if @attachments.empty?
+          @prepared_attachments = []
+          return
+        end
 
-          FileUtils.cp(src_path, File.join(assets_dir, name))
+        @attachment_snapshot_dir = Dir.mktmpdir("hive-new-attachments")
+        @prepared_attachments = @attachments.each_with_index.map do |(source, destination), index|
+          name = attachment_name!(destination)
+          snapshot_path = File.join(@attachment_snapshot_dir, index.to_s)
+          FileUtils.cp(File.expand_path(source.to_s), snapshot_path)
+          PreparedAttachment.new(
+            snapshot_path: snapshot_path,
+            destination: destination.to_s,
+            name: name,
+            sha256: ::Digest::SHA256.file(snapshot_path).hexdigest
+          )
         end
       end
 
+      def cleanup_prepared_attachments!
+        return unless @attachment_snapshot_dir
+
+        FileUtils.remove_entry_secure(@attachment_snapshot_dir)
+      rescue Errno::ENOENT
+        nil
+      ensure
+        @attachment_snapshot_dir = nil
+        @prepared_attachments = nil
+      end
+
+      def attachment_name!(destination)
+        name = Hive::Tui::Text.sanitize(destination.to_s)
+        # `name != File.basename(name)` rejects directory separators and the
+        # like, but `File.basename(".") == "."` and `File.basename("..") ==
+        # ".."` — both would otherwise slip through and overwrite or escape
+        # `assets/`.
+        if name.empty? || name == "." || name == ".." || name != File.basename(name)
+          raise InvalidAttachmentError.new("invalid attachment filename '#{name}'", value: name)
+        end
+
+        name
+      end
+
       def spawn_name_generator(task_dir)
         return nil if defined?(Minitest) && !self.class.name_generator_spawn_configured?
 
diff --git a/lib/hive/commands/run.rb b/lib/hive/commands/run.rb
index 53d7c294c6..fc905cd634 100644
--- a/lib/hive/commands/run.rb
+++ b/lib/hive/commands/run.rb
@@ -39,7 +39,7 @@ class Run
       # only when the runner result carries the corresponding data — currently
       # `cleanup_instructions:` from the terminal `done` stage. See
       # schemas/hive-run.v1.json $defs.SuccessPayload.properties.
-      OPTIONAL_PAYLOAD_KEYS = %w[cleanup_instructions].freeze
+      OPTIONAL_PAYLOAD_KEYS = %w[cleanup_instructions allowed_outcomes].freeze
 
       # Exact pre-run task-folder snapshot used only for terminal runners. Lock
       # artifacts are process-owned and intentionally remain live across a
@@ -134,6 +134,12 @@ def do_call
           @observation_guard&.call(task)
           cfg = Hive::Config.load(task.project_root)
           marker = Hive::Markers.current(task.state_file)
+          if human_stage?(task)
+            @rebase_result = human_stage_rebase_result
+            status = marker.name == :complete ? :complete : :needs_input
+            report(task, { commit: nil, status: status })
+            return
+          end
           if marker.name == :manual_steering
             @rebase_result = manual_steering_rebase_result
             report(task, { commit: nil, status: :manual_steering })
@@ -249,6 +255,12 @@ def manual_steering_rebase_result
         Hive::Rebase::Result.skipped(:manual_steering)
       end
 
+      def human_stage_rebase_result
+        require "hive/rebase"
+
+        Hive::Rebase::Result.skipped(:human_stage)
+      end
+
       def pick_runner(task)
         Hive::Stages::Resolver.resolve(task, descriptor: task.workflow)
       end
@@ -373,6 +385,7 @@ def report_json(task, result, marker)
         if result.is_a?(Hash) && result[:cleanup_instructions]
           payload["cleanup_instructions"] = result[:cleanup_instructions]
         end
+        payload["allowed_outcomes"] = allowed_outcomes(task) if human_decision_required?(task, marker)
         # The JSON payload is written to stdout *before* the raise. bin/hive
         # rescues Hive::Error and calls `exit(e.exit_code)`; Ruby's normal
         # interpreter shutdown flushes stdout via IO finalizers, so the
@@ -410,6 +423,21 @@ def rebase_envelope_block
 
       def json_next_action(task, marker)
         kind = Hive::Schemas::NextActionKind
+        if human_stage?(task)
+          if marker.name == :complete
+            return { "kind" => kind::NO_OP, "reason" => "human_stage_complete" }
+          end
+
+          decision_id = marker.attrs["decision_id"].to_s
+          return {
+            "kind" => kind::NO_OP,
+            "reason" => "human_decision_required",
+            "allowed_outcomes" => allowed_outcomes(task),
+            "decide_with" => "hive decide #{task.slug} <outcome> --from #{task.stage_name} " \
+                             "--decision-id #{decision_id}"
+          }
+        end
+
         case marker.name
         when :waiting
           { "kind" => kind::EDIT, "target" => task.state_file, "rerun_with" => friendly_command(task, marker) }
@@ -596,6 +624,18 @@ def report_text(task, result, marker)
         end
         puts "hive: marker=#{marker.name}"
         puts "  state_file: #{task.state_file}"
+        if human_stage?(task)
+          if marker.name == :complete
+            puts "  complete: human workflow outcome recorded"
+            return
+          end
+
+          decision_id = marker.attrs["decision_id"].to_s
+          puts "  outcomes: #{allowed_outcomes(task).map { |outcome| outcome.fetch('name') }.join(', ')}"
+          puts "  next: hive decide #{task.slug} <outcome> --from #{task.stage_name} " \
+               "--decision-id #{decision_id}"
+          return
+        end
         case marker.name
         when :waiting
           puts "  next: edit the file, then `#{friendly_command(task, marker)}` again"
@@ -661,6 +701,18 @@ def project_name_for(task)
         project ? project["name"] : task.project_name
       end
 
+      def human_stage?(task)
+        task.workflow.stage_named(task.stage_name)&.kind == :human
+      end
+
+      def human_decision_required?(task, marker)
+        human_stage?(task) && marker.name != :complete
+      end
+
+      def allowed_outcomes(task)
+        Hive::TaskAction.for(task, Hive::Markers.current(task.state_file)).allowed_outcomes
+      end
+
       # Map a Hive::Error subclass to a RunErrorKind value. Ordering matters:
       # `case/when` uses `===` (is_a?), so subclasses MUST precede their
       # ancestors. Notably:
diff --git a/lib/hive/commands/status.rb b/lib/hive/commands/status.rb
index 684628d3e8..6cd0b003d7 100644
--- a/lib/hive/commands/status.rb
+++ b/lib/hive/commands/status.rb
@@ -611,6 +611,7 @@ def task_payload(row, now: Time.now.utc)
           "action" => row[:action_key],
           "action_label" => row[:action_label],
           "suggested_command" => row[:suggested_command],
+          "outcomes" => row[:outcomes] || [],
           "next_action" => row[:next_action],
           "diagnostic" => row[:diagnostic]
         }
@@ -1477,6 +1478,7 @@ def decorate(task, marker, lock_holder: nil, live_task_lock: false)
         "Answer questions",
         "Review plan draft",
         "Needs your input",
+        "Awaiting human decision",
         "Needs review decision",
         "Confirm finalize",
         "Ready to plan",
@@ -1592,6 +1594,7 @@ def annotate_actions(rows, project, project_count, config: nil, with_diagnostic:
             action_key: action.key,
             action_label: action.label,
             suggested_command: action.command,
+            outcomes: action.allowed_outcomes,
             next_action: action.next_action,
             diagnostic: with_diagnostic ? action.diagnostic : nil,
             condition_gate: action.condition_gate&.to_h,
diff --git a/lib/hive/commands/workflow.rb b/lib/hive/commands/workflow.rb
index a74e5cce8a..1193f96fef 100644
--- a/lib/hive/commands/workflow.rb
+++ b/lib/hive/commands/workflow.rb
@@ -3,6 +3,7 @@
 require "json"
 require "pathname"
 require "shellwords"
+require "stringio"
 require "hive"
 require "hive/config"
 require "hive/git_ops"
@@ -20,20 +21,21 @@ class Workflow
       HONEYCOMB_TEMPLATES = %w[architecture writing].freeze
       WORKFLOW_ID_RE = Hive::Workflows::DescriptorParser::SAFE_SLUG
       SCHEMA = "hive-workflow-new".freeze
-      SUBCOMMANDS = %w[new install list update remove publish].freeze
+      SUBCOMMANDS = %w[new validate commit install list update remove publish].freeze
 
       class UsageError < Hive::Error
-        attr_reader :value, :expected
+        attr_reader :value, :expected, :suggested_id
 
         # Closed set of usage-error shapes (which structured field each carries):
         #   missing subcommand        -> expected only
         #   unknown subcommand        -> value (the rejected verb) + expected
         #   bad/missing/reserved id / -> value (the rejected/colliding token) only
         #   scaffold collision
-        def initialize(message, value: nil, expected: nil)
+        def initialize(message, value: nil, expected: nil, suggested_id: nil)
           super(message)
           @value = value
           @expected = expected
+          @suggested_id = suggested_id
         end
 
         def exit_code
@@ -45,22 +47,22 @@ def self.new!(id, project_root: Dir.pwd, json: false, stdout: $stdout, template:
         new("new", id, project_root: project_root, json: json, stdout: stdout, template: template).call!
       end
 
-      def self.normalize_and_validate_id!(raw)
+      def self.normalize_and_validate_id!(raw, project_root: nil)
         id = normalize_id(raw)
-        validate_id!(id)
+        validate_id!(id, project_root: project_root)
         id
       end
 
       def self.scaffold_files!(id_raw, project_root:, template: DEFAULT_TEMPLATE)
-        id = normalize_and_validate_id!(id_raw)
+        id = normalize_and_validate_id!(id_raw, project_root: project_root)
         template_dir = template_dir!(template)
         paths = scaffold_paths(id, project_root: project_root, template_dir: template_dir)
-        refuse_overwrite!(paths)
+        refuse_overwrite!(paths, id: id, project_root: project_root)
         begin
           write_scaffold!(id, paths, template_dir)
           validate_descriptor!(paths.fetch(:descriptor))
           Hive::Workflows::Project.reset!
-        rescue StandardError
+        rescue StandardError, Interrupt
           rollback_scaffold(paths)
           raise
         end
@@ -86,6 +88,21 @@ def self.scaffold_collision?(id, project_root:)
         scaffold_collisions(paths).any?
       end
 
+      # Return the first create-safe id in a deterministic numeric sequence.
+      # This is intentionally read-only and shared by the CLI and canonical
+      # skill so collision recovery never depends on an agent guessing a name.
+      def self.available_id(raw, project_root:)
+        base = raw.to_s.strip
+        base = "workflow" unless WORKFLOW_ID_RE.match?(base)
+        suffix = 1
+        loop do
+          candidate = suffix == 1 ? base : "#{base}-#{suffix}"
+          return candidate unless unavailable_id?(candidate, project_root: project_root)
+
+          suffix += 1
+        end
+      end
+
       # Sample templates ship as directories under `templates/workflows/` (the
       # bare `blank` stub plus richer multi-stage samples). A template dir holds
       # `descriptor.yml.erb` (rendered with the new id) and one `.md` instruction
@@ -131,19 +148,9 @@ def self.commit_workflow_scaffold(ops, slug:, pathspecs:)
       end
 
       # Filesystem-only rollback: unwinds the working-tree files write_scaffold!
-      # created, NOT any git side-effects of a partially-run commit. hive_commit
-      # stages by explicit pathspec, so a same-command retry re-stages exactly
-      # these paths and refuse_overwrite! re-checks them — removing the files is
-      # all a retry needs.
-      #
-      # NOTE: the shared-worktree callers commit into the long-lived
-      # `.hive-state` worktree, where a failed commit ALSO leaves these pathspecs
-      # STAGED. `hive init --new-workflow` on an existing project resets the index
-      # for them (Init#reset_hive_state_index); `hive workflow new`'s own
-      # commit_scaffold! (below) shares the SAME pre-existing exposure but
-      # currently relies on this filesystem-only rollback alone — out of scope to
-      # fix here, but `workflow new` is NOT index-safe just because init now is.
-      # Either way this method intentionally leaves git untouched.
+      # created. Callers that attempted a commit own exact-path index rollback
+      # while still holding the commit lock; this helper intentionally leaves
+      # git untouched.
       # remove_scaffold_path tolerates
       # an already-absent target (like rm_f/rm_rf's force) and continues to the
       # next path on a fault, but CAPTURES the errno so warn_failed_scaffold_cleanup
@@ -152,11 +159,19 @@ def self.commit_workflow_scaffold(ops, slug:, pathspecs:)
       # refuse_overwrite! "already exists" on the next attempt.
       def self.rollback_scaffold(paths)
         reasons = {}
-        remove_scaffold_path(paths.fetch(:descriptor), reasons)
-        remove_scaffold_path(paths.fetch(:instruction_dir), reasons)
-        warn_failed_scaffold_cleanup(paths, reasons)
+        rollback_targets(paths).reverse_each do |path|
+          remove_scaffold_path(path, reasons)
+        end
+        warn_failed_scaffold_cleanup(paths, reasons, targets: rollback_targets(paths))
       end
 
+      def self.rollback_targets(paths)
+        paths.fetch(:owned_paths) do
+          [ paths.fetch(:instruction_dir), paths.fetch(:descriptor) ]
+        end
+      end
+      private_class_method :rollback_targets
+
       # Remove one scaffold path, recording the underlying errno when a genuine
       # permission/busy fault keeps it on disk. The existence guard makes an
       # already-absent target a no-op (matching rm_f/rm_rf's force: true) without
@@ -178,16 +193,34 @@ def self.normalize_id(value)
       end
       private_class_method :normalize_id
 
-      def self.validate_id!(id)
+      def self.validate_id!(id, project_root: nil)
         unless WORKFLOW_ID_RE.match?(id)
           raise UsageError.new("invalid workflow id #{id.inspect} (must match #{WORKFLOW_ID_RE.source})", value: id)
         end
         return unless Hive::Workflows::Registry::WORKFLOWS.key?(id.to_sym)
 
-        raise UsageError.new("workflow id #{id.inspect} is reserved by a built-in workflow", value: id)
+        suggested = available_id(id, project_root: project_root) if project_root
+        suffix = suggested ? "; try #{suggested.inspect}" : ""
+        raise UsageError.new(
+          "workflow id #{id.inspect} is reserved by a built-in workflow#{suffix}",
+          value: id, suggested_id: suggested
+        )
       end
       private_class_method :validate_id!
 
+      def self.unavailable_id?(id, project_root:)
+        return true if Hive::Workflows::Registry::WORKFLOWS.key?(id.to_sym)
+
+        dir = workflow_dir(project_root)
+        path_present?(File.join(dir, "#{id}.yml")) || path_present?(File.join(dir, id))
+      end
+      private_class_method :unavailable_id?
+
+      def self.path_present?(path)
+        File.exist?(path) || File.symlink?(path)
+      end
+      private_class_method :path_present?
+
       def self.scaffold_paths(id, project_root:, template_dir:)
         workflows_dir = workflow_dir(project_root)
         instruction_dir = File.join(workflows_dir, id)
@@ -219,11 +252,15 @@ def self.workflow_dir(project_root)
       end
       private_class_method :workflow_dir
 
-      def self.refuse_overwrite!(paths)
+      def self.refuse_overwrite!(paths, id:, project_root:)
         collisions = scaffold_collisions(paths)
         return if collisions.empty?
 
-        raise UsageError.new("workflow scaffold already exists at #{collisions.join(', ')}", value: collisions.first)
+        suggested = available_id(id, project_root: project_root)
+        raise UsageError.new(
+          "workflow scaffold already exists at #{collisions.join(', ')}; try #{suggested.inspect}",
+          value: collisions.first, suggested_id: suggested
+        )
       end
       private_class_method :refuse_overwrite!
 
@@ -233,28 +270,55 @@ def self.refuse_overwrite!(paths)
       # dir, and every per-stage instruction file the template would write.
       def self.scaffold_collisions(paths)
         ([ paths.fetch(:descriptor), paths.fetch(:instruction_dir) ] + paths.fetch(:instructions)).select do |path|
-          File.exist?(path)
+          path_present?(path)
         end
       end
       private_class_method :scaffold_collisions
 
       def self.write_scaffold!(id, paths, template_dir)
-        FileUtils.mkdir_p(paths.fetch(:instruction_dir))
-        File.write(paths.fetch(:descriptor), render_descriptor(id, template_dir))
+        paths[:owned_paths] = []
+        ensure_scaffold_root!(File.dirname(paths.fetch(:instruction_dir)))
+        Dir.mkdir(paths.fetch(:instruction_dir))
+        paths.fetch(:owned_paths) << paths.fetch(:instruction_dir)
+        write_exclusive(paths.fetch(:descriptor), render_descriptor(id, template_dir))
+        paths.fetch(:owned_paths) << paths.fetch(:descriptor)
         template_instruction_files(template_dir).each do |file|
-          File.write(File.join(paths.fetch(:instruction_dir), file), File.read(File.join(template_dir, file)))
+          write_exclusive(
+            File.join(paths.fetch(:instruction_dir), file),
+            File.read(File.join(template_dir, file))
+          )
         end
         write_optional_template_asset(id, template_dir, "README.md.erb", File.join(paths.fetch(:instruction_dir), "README.md"))
         write_optional_template_asset(id, template_dir, "honeycomb.yml.erb", File.join(paths.fetch(:instruction_dir), "honeycomb.yml"))
       end
       private_class_method :write_scaffold!
 
+      def self.ensure_scaffold_root!(path)
+        begin
+          Dir.mkdir(path)
+        rescue Errno::EEXIST
+          nil
+        end
+        stat = File.lstat(path)
+        return if stat.directory? && !stat.symlink?
+
+        raise Hive::ConfigError, "workflow scaffold root #{path} must be a regular directory, not a symlink"
+      end
+      private_class_method :ensure_scaffold_root!
+
+      def self.write_exclusive(path, content)
+        flags = File::WRONLY | File::CREAT | File::EXCL
+        flags |= File::NOFOLLOW if File.const_defined?(:NOFOLLOW)
+        File.open(path, flags, 0o644) { |file| file.write(content) }
+      end
+      private_class_method :write_exclusive
+
       def self.write_optional_template_asset(id, template_dir, template_name, destination)
         source = File.join(template_dir, template_name)
         return unless File.file?(source)
 
         rendered = ERB.new(File.read(source), trim_mode: "-").result_with_hash(id: id)
-        File.write(destination, rendered)
+        write_exclusive(destination, rendered)
       end
       private_class_method :write_optional_template_asset
 
@@ -275,8 +339,8 @@ def self.validate_descriptor!(path)
       # the errno remove_scaffold_path captured per leftover so the operator
       # learns WHY cleanup failed (e.g. EACCES on a read-only parent) instead of
       # a bare path list.
-      def self.warn_failed_scaffold_cleanup(paths, reasons = {})
-        leftovers = [ paths.fetch(:descriptor), paths.fetch(:instruction_dir) ].select { |path| File.exist?(path) }
+      def self.warn_failed_scaffold_cleanup(paths, reasons = {}, targets: rollback_targets(paths))
+        leftovers = targets.select { |path| path_present?(path) }
         return if leftovers.empty?
 
         described = leftovers.map do |path|
@@ -309,13 +373,12 @@ def initialize(subcommand, id = nil, project_root: Dir.pwd, json: false, stdout:
 
       def call
         call!
-      rescue UsageError, Hive::ConfigError, Hive::GitError, Hive::ConcurrentRunError,
-             SystemCallError, IOError => e
+      rescue Hive::Error, SystemCallError, IOError => e
         # ConcurrentRunError (commit-lock contention, TEMPFAIL/75) is now caught
         # too: previously it escaped to bin/hive as plain stderr, hiding the
         # retryable-vs-terminal distinction from --json agent callers.
         # SystemCallError/IOError are caught for the same reason: a disk-write
-        # fault from write_scaffold!'s mkdir_p/File.write would otherwise escape
+        # fault from write_scaffold!'s exclusive directory/file creation would otherwise escape
         # past `call` to bin/hive (which only handles Errno::EPIPE/Thor::Error/
         # Hive::Error), yielding a raw backtrace instead of the schema'd --json
         # envelope. error_kind_for's else→ERROR arm classifies them.
@@ -346,6 +409,7 @@ def call!
           )
         end
 
+        return commit_authored_workflow! if @subcommand == "commit"
         return lifecycle_command.call unless @subcommand == "new"
 
         scaffold = self.class.scaffold_files!(@id, project_root: @project_root, template: @template)
@@ -357,7 +421,7 @@ def call!
           # descriptor + instruction files on disk that make the next retry fail
           # in refuse_overwrite!, turning a retryable command into a stuck one.
           commit_scaffold!(id, paths)
-        rescue StandardError
+        rescue StandardError, Interrupt
           self.class.rollback_scaffold(paths)
           raise
         end
@@ -379,8 +443,55 @@ def call!
 
       private
 
+      def commit_authored_workflow!
+        raise UsageError.new("workflow commit does not support --json", value: @id) if @json
+
+        id = self.class.normalize_and_validate_id!(@id, project_root: @project_root)
+        require "hive/commands/workflow/validate"
+        validation = Hive::Commands::Workflow::Validate.new(
+          id, project_root: @project_root, stdout: StringIO.new
+        ).call!
+        unless validation.fetch("origin") == "authored"
+          raise UsageError.new(
+            "workflow commit requires an owner-authored workflow, got #{validation.fetch('origin')}",
+            value: id
+          )
+        end
+
+        descriptor = validation.fetch("descriptor_path")
+        instruction_dir = File.join(File.dirname(descriptor), id)
+        pathspecs = [ relative_to_workflows_root(descriptor) ]
+        pathspecs << relative_to_workflows_root(instruction_dir) if File.directory?(instruction_dir)
+        ops = Hive::GitOps.new(@project_root)
+        result = Hive::Lock.with_commit_lock(hive_state_path) do
+          begin
+            ops.hive_commit(
+              stage_name: "workflows", slug: id, action: "defined",
+              pathspecs: pathspecs
+            )
+          rescue StandardError, Interrupt
+            ops.run_git!("-C", hive_state_path, "reset", "-q", "HEAD", "--", *pathspecs)
+            raise
+          end
+        end
+        @stdout.puts(
+          result == :committed ?
+            "hive: committed populated workflow #{id}" :
+            "hive: populated workflow #{id} is already committed"
+        )
+        {
+          "ok" => true, "id" => id, "committed" => result == :committed,
+          "descriptor_path" => descriptor
+        }
+      end
+
       def lifecycle_command
         case @subcommand
+        when "validate"
+          require "hive/commands/workflow/validate"
+          Hive::Commands::Workflow::Validate.new(
+            @id, project_root: @project_root, json: @json, stdout: @stdout
+          )
         when "install"
           require "hive/commands/workflow/install"
           Hive::Commands::Workflow::Install.new(
@@ -416,11 +527,19 @@ def lifecycle_command
 
       def commit_scaffold!(id, paths)
         ops = Hive::GitOps.new(@project_root)
+        pathspecs = [
+          relative_to_workflows_root(paths.fetch(:descriptor)),
+          relative_to_workflows_root(paths.fetch(:instruction_dir))
+        ]
         Hive::Lock.with_commit_lock(hive_state_path) do
-          self.class.commit_workflow_scaffold(ops, slug: id, pathspecs: [
-            relative_to_workflows_root(paths.fetch(:descriptor)),
-            relative_to_workflows_root(paths.fetch(:instruction_dir))
-          ])
+          begin
+            self.class.commit_workflow_scaffold(
+              ops, slug: id, pathspecs: pathspecs
+            )
+          rescue StandardError, Interrupt
+            ops.run_git!("-C", hive_state_path, "reset", "-q", "HEAD", "--", *pathspecs)
+            raise
+          end
         end
       end
 
@@ -449,7 +568,7 @@ def success_payload(id, paths)
       # (agents branch on those uniformly).
       def error_payload(error)
         Hive::Schemas::ErrorEnvelope.build(
-          schema: SCHEMA,
+          schema: response_schema,
           error: error,
           error_kind: error_kind_for(error),
           extras: error_extras(error)
@@ -463,14 +582,27 @@ def error_extras(error)
         extras = {}
         extras["value"] = error.value if error.respond_to?(:value) && !error.value.nil?
         extras["expected"] = error.expected if error.respond_to?(:expected) && !error.expected.nil?
+        if error.respond_to?(:suggested_id) && !error.suggested_id.nil?
+          extras["suggested_id"] = error.suggested_id
+        end
+        if @subcommand == "validate"
+          extras["valid"] = false
+          extras["id"] = @id.to_s
+          extras["diagnostics"] = [ { "message" => error.message } ]
+        end
 
         extras
       end
 
+      def response_schema
+        @subcommand == "validate" ? "hive-workflow-validate" : SCHEMA
+      end
+
       def error_kind_for(error)
         kinds = Hive::Schemas::WorkflowNewErrorKind
         case error
         when UsageError                then kinds::USAGE
+        when Hive::Workflows::UnknownWorkflow then kinds::USAGE
         when Hive::ConcurrentRunError  then kinds::CONCURRENT_RUN
         when Hive::GitError            then kinds::GIT
         when Hive::ConfigError         then kinds::CONFIG
diff --git a/lib/hive/commands/workflow/validate.rb b/lib/hive/commands/workflow/validate.rb
new file mode 100644
index 0000000000..de9c4e30b6
--- /dev/null
+++ b/lib/hive/commands/workflow/validate.rb
@@ -0,0 +1,194 @@
+require "json"
+require "hive/config"
+require "hive/workflows/descriptor_parser"
+require "hive/workflows/registry"
+require "hive/workflow_package/managed_store"
+
+module Hive
+  module Commands
+    class Workflow
+      # Strictly read-only validation through direct authored, built-in, and
+      # managed-store lookups. It deliberately bypasses production selection
+      # reconciliation because even lock creation or journal repair would
+      # violate this command's no-write contract.
+      class Validate
+        SCHEMA = "hive-workflow-validate".freeze
+
+        def initialize(id, project_root:, json: false, stdout: $stdout)
+          @id = id.to_s.strip
+          @project_root = File.expand_path(project_root)
+          @json = json
+          @stdout = stdout
+        end
+
+        def call!
+          validate_id!
+          workflow = resolve_read_only!
+          validate_instruction_paths!(workflow)
+          payload = payload_for(workflow)
+          emit(payload)
+          payload
+        end
+
+        def call = call!
+
+        private
+
+        def validate_id!
+          raise UsageError.new("missing workflow id", value: @id) if @id.empty?
+          return if WORKFLOW_ID_RE.match?(@id)
+
+          raise UsageError.new(
+            "invalid workflow id #{@id.inspect} (must match #{WORKFLOW_ID_RE.source})", value: @id
+          )
+        end
+
+        def validate_instruction_paths!(workflow)
+          workflow.each do |stage|
+            next unless stage.instruction
+            next if File.file?(stage.instruction) && File.readable?(stage.instruction)
+
+            raise Hive::ConfigError,
+                  "workflow descriptor #{descriptor_path || workflow.id.inspect} " \
+                  "stage #{stage.name.inspect} references missing instruction #{stage.instruction.inspect}"
+          end
+        end
+
+        def payload_for(workflow)
+          {
+            "schema" => SCHEMA,
+            "schema_version" => Hive::Schemas::SCHEMA_VERSIONS.fetch(SCHEMA),
+            "ok" => true,
+            "valid" => true,
+            "id" => workflow.id.to_s,
+            "origin" => origin,
+            "descriptor_path" => descriptor_path,
+            "instruction_paths" => workflow.filter_map(&:instruction).uniq,
+            "stages" => workflow.map { |stage| stage_payload(stage) },
+            "automatic_edges" => automatic_edges(workflow),
+            "human_outcomes" => human_outcomes(workflow),
+            "diagnostics" => []
+          }
+        end
+
+        def stage_payload(stage)
+          {
+            "name" => stage.name,
+            "dir" => stage.dir,
+            "kind" => stage.kind&.to_s,
+            "state_file" => stage.state_file,
+            "instruction_path" => stage.instruction,
+            "input" => stage.input
+          }
+        end
+
+        def automatic_edges(workflow)
+          workflow.filter_map do |stage|
+            target = workflow.next_stage_after(stage.name)
+            next if stage.kind == :human || target.nil?
+
+            { "from" => stage.name, "to" => target.name }
+          end
+        end
+
+        def human_outcomes(workflow)
+          workflow.flat_map do |stage|
+            next [] unless stage.kind == :human
+
+            stage.outcomes.values.map do |outcome|
+              {
+                "stage" => stage.name,
+                "name" => outcome.name,
+                "complete" => outcome.complete,
+                "artifact" => outcome.artifact,
+                "to" => outcome.to
+              }
+            end
+          end
+        end
+
+        def workflows_dir
+          @workflows_dir ||= begin
+            source_path, data = Hive::Config.read_project_config(@project_root)
+            data = Hive::Config.normalize_legacy_project_config(
+              data, source_path, emit_warning: false
+            )
+            hive_state_path = data["hive_state_path"]
+            unless hive_state_path.is_a?(String)
+              hive_state_path = Hive::Config::DEFAULTS.fetch("hive_state_path")
+            end
+            File.join(File.expand_path(hive_state_path, @project_root), "workflows")
+          end
+        end
+
+        def resolve_read_only!
+          if descriptor_path
+            workflow = Hive::Workflows::DescriptorParser.parse_file(descriptor_path)
+            if Hive::Workflows::Registry::WORKFLOWS.key?(workflow.id)
+              raise Hive::ConfigError,
+                    "workflow descriptor #{descriptor_path} collides with registered workflow #{workflow.id.inspect}"
+            end
+            @origin = "authored"
+            return workflow
+          end
+
+          id = @id.to_sym
+          if Hive::Workflows::Registry::WORKFLOWS.key?(id)
+            @origin = "built_in"
+            return Hive::Workflows::Registry.fetch(id)
+          end
+
+          store = Hive::WorkflowPackage::ManagedStore.new(File.dirname(workflows_dir))
+          selection = store.selected_read_only(@id)
+          if selection
+            @origin = "managed"
+            return store.workflow(
+              @id, selection.fetch("source_commit"), selection.fetch("manifest_digest"),
+              configuration_digest: selection.fetch("configuration_digest"),
+              verify_profiles: false
+            )
+          end
+
+          raise Hive::Workflows::UnknownWorkflow.new(
+            "unknown workflow #{@id.inspect}; valid workflows: #{read_only_workflow_ids.join(', ')}",
+            value: @id, valid: read_only_workflow_ids
+          )
+        end
+
+        def read_only_workflow_ids
+          authored = Dir.glob(File.join(workflows_dir, "*.yml")).map do |path|
+            File.basename(path, ".yml")
+          end
+          managed = Dir.glob(
+            File.join(workflows_dir, "*", Hive::WorkflowPackage::ManagedStore::LOCK_FILE)
+          ).map { |path| File.basename(File.dirname(path)) }
+          (Hive::Workflows::Registry::WORKFLOWS.keys.map(&:to_s) + authored + managed).uniq.sort
+        end
+
+        def descriptor_path
+          @descriptor_path ||= begin
+            path = File.join(workflows_dir, "#{@id}.yml")
+            File.file?(path) ? path : nil
+          end
+        end
+
+        def origin
+          return @origin if @origin
+          return "authored" if descriptor_path
+          return "built_in" if Hive::Workflows::Registry::WORKFLOWS.key?(@id.to_sym)
+
+          "managed"
+        end
+
+        def emit(payload)
+          if @json
+            @stdout.puts JSON.generate(payload)
+          else
+            @stdout.puts "hive: workflow #{@id} is valid (#{origin})"
+            @stdout.puts "  stages: #{payload.fetch('stages').map { |stage| stage.fetch('name') }.join(' -> ')}"
+          end
+        end
+      end
+    end
+  end
+end
diff --git a/lib/hive/config.rb b/lib/hive/config.rb
index 1ec95dc843..ec8de88e53 100644
--- a/lib/hive/config.rb
+++ b/lib/hive/config.rb
@@ -1663,7 +1663,19 @@ def global_bot_defaults
       defaults
     end
 
-    def register_project(name:, path:, repository_identity: :detect)
+    class ProjectRegistrationCollision < ConfigError
+      attr_reader :name, :existing_path
+
+      def initialize(message, name:, existing_path:)
+        super(message)
+        @name = name
+        @existing_path = existing_path
+      end
+
+      def exit_code = Hive::ExitCodes::USAGE
+    end
+
+    def register_project(name:, path:, repository_identity: :detect, replace_existing: true)
       entry = nil
       update_global_config! do |data|
         data["registered_projects"] = Array(data["registered_projects"])
@@ -1676,6 +1688,13 @@ def register_project(name:, path:, repository_identity: :detect)
         entry["real_path"] = real_path if real_path
         existing = data["registered_projects"].find { |p| p.is_a?(Hash) && p["name"] == name }
         if existing
+          existing_path = File.expand_path(existing.fetch("path"))
+          if !replace_existing && existing_path != abs_path
+            raise ProjectRegistrationCollision.new(
+              "project #{name.inspect} is already registered at #{existing.fetch('path')}",
+              name: name, existing_path: existing.fetch("path")
+            )
+          end
           existing.replace(entry)
         else
           data["registered_projects"] << entry
diff --git a/lib/hive/llm_wiki_bootstrap.rb b/lib/hive/llm_wiki_bootstrap.rb
index 849003948f..5ef8bd995e 100644
--- a/lib/hive/llm_wiki_bootstrap.rb
+++ b/lib/hive/llm_wiki_bootstrap.rb
@@ -17,11 +17,35 @@ module LlmWikiBootstrap
     POST_COMMIT_BEGIN = "# BEGIN LLM WIKI POST-COMMIT".freeze
     POST_COMMIT_END = "# END LLM WIKI POST-COMMIT".freeze
     SESSION_HOOK_MARKER = "LLM WIKI SESSION START".freeze
+    PROJECT_MANAGED_PATHS = %w[
+      .llm-wiki
+      .llm-wiki/config.json
+      .llm-wiki/refresh-wiki.sh
+      .llm-wiki/post-commit-refresh.sh
+      .llm-wiki/compile-log.sh
+      .claude
+      .claude/settings.json
+      AGENTS.md
+      CLAUDE.md
+      wiki
+      wiki/log.d
+      wiki/log.d/.gitkeep
+      wiki/index.md
+      wiki/log.md
+      wiki/gaps.md
+      wiki/architecture.md
+      wiki/decisions.md
+      wiki/dependencies.md
+      raw
+      raw/notes
+      raw/notes/.gitkeep
+    ].freeze
 
     module_function
 
     def install!(project_root, post_commit_hook: true, scheduler: true)
       project_root = File.expand_path(project_root)
+      validate_project_managed_paths!(project_root)
       FileUtils.mkdir_p(File.join(project_root, ".llm-wiki"))
 
       ensure_config(project_root)
@@ -33,13 +57,36 @@ def install!(project_root, post_commit_hook: true, scheduler: true)
       Scheduler.install(project_root) if scheduler
     end
 
-    def install_runtime_hooks!(project_root)
+    def install_runtime_hooks!(project_root, scheduler: true)
       project_root = File.expand_path(project_root)
+      validate_project_managed_paths!(project_root)
       ensure_config(project_root)
       ensure_refresh_scripts(project_root)
       ensure_shared_runtime(project_root)
       ensure_post_commit_hook(project_root)
-      Scheduler.install(project_root)
+      Scheduler.install(project_root) if scheduler
+    end
+
+    def validate_project_managed_paths!(project_root)
+      PROJECT_MANAGED_PATHS.each do |relative|
+        current = project_root
+        relative.split(File::SEPARATOR).each_with_index do |component, index|
+          current = File.join(current, component)
+          break unless File.exist?(current) || File.symlink?(current)
+
+          stat = File.lstat(current)
+          if stat.symlink?
+            raise Hive::ConfigError,
+                  "llm-wiki managed path #{current} must not be a symlink"
+          end
+          next if index == relative.split(File::SEPARATOR).length - 1
+          next if stat.directory?
+
+          raise Hive::ConfigError,
+                "llm-wiki managed parent #{current} must be a directory"
+        end
+      end
+      true
     end
 
     def ensure_config(project_root)
@@ -231,7 +278,14 @@ def managed_content(existing, begin_marker, end_marker, block)
 
     def write_file(path, content)
       FileUtils.mkdir_p(File.dirname(path))
-      File.binwrite(path, content)
+      flags = File::WRONLY | File::CREAT | File::TRUNC
+      flags |= File::NOFOLLOW if File.const_defined?(:NOFOLLOW)
+      File.open(path, flags, 0o644) do |file|
+        file.binmode
+        file.write(content)
+      end
+    rescue Errno::ELOOP, Errno::EMLINK
+      raise Hive::ConfigError, "llm-wiki managed file #{path} must not be a symlink"
     end
   end
 end
diff --git a/lib/hive/markers.rb b/lib/hive/markers.rb
index 26651012c0..569b930960 100644
--- a/lib/hive/markers.rb
+++ b/lib/hive/markers.rb
@@ -49,9 +49,12 @@ def none?
     def current(state_file_path)
       return State.new(name: :none, attrs: {}, raw: nil) unless File.exist?(state_file_path)
 
-      content = File.read(state_file_path, encoding: "UTF-8")
+      current_from_content(File.read(state_file_path, encoding: "UTF-8"))
+    end
+
+    def current_from_content(content)
       last = nil
-      content.scan(MARKER_RE) do
+      content.to_s.scan(MARKER_RE) do
         match = Regexp.last_match
         last = match
       end
diff --git a/lib/hive/operational_status.rb b/lib/hive/operational_status.rb
index 10db88d41c..9a50981e5f 100644
--- a/lib/hive/operational_status.rb
+++ b/lib/hive/operational_status.rb
@@ -345,7 +345,11 @@ def project_row(project, row, scheduler)
           "folder" => row.fetch("folder")
         },
         "workflow" => row["workflow"],
-        "position" => { "stage" => row.fetch("stage"), "marker" => row.fetch("marker") },
+        "position" => {
+          "stage" => row.fetch("stage"),
+          "marker" => row.fetch("marker"),
+          "allowed_outcomes" => Array(row["outcomes"])
+        },
         "liveness" => {
           "status" => liveness_status(row),
           "pid" => row["task_lock_pid"] || row["claude_pid"],
diff --git a/lib/hive/task_action.rb b/lib/hive/task_action.rb
index 3123de109a..ee2040f253 100644
--- a/lib/hive/task_action.rb
+++ b/lib/hive/task_action.rb
@@ -160,6 +160,11 @@ class TaskAction
         label: "Needs your input",
         command: "run"
       },
+      human_needs_input: {
+        key: Hive::Schemas::TaskActionKind::NEEDS_INPUT,
+        label: "Awaiting human decision",
+        command: nil
+      },
       recover_draft_pr: {
         key: Hive::Schemas::TaskActionKind::RECOVER_DRAFT_PR,
         label: "Retry draft PR handoff manually",
@@ -280,10 +285,25 @@ def payload
         "key" => key,
         "label" => label,
         "command" => command,
-        "next_action" => next_action
+        "next_action" => next_action,
+        "outcomes" => allowed_outcomes
       }
     end
 
+    def allowed_outcomes
+      stage = workflow_stage
+      return [] unless stage&.kind == :human
+
+      stage.outcomes.values.map do |outcome|
+        {
+          "name" => outcome.name,
+          "complete" => outcome.complete,
+          "artifact" => outcome.artifact,
+          "to" => outcome.to
+        }
+      end
+    end
+
     def migration_selection
       @migration_selection ||= Hive::Conditions::Migration.selection(
         config: @config, stage: "#{task.stage_index}-#{task.stage_name}",
@@ -363,6 +383,8 @@ def kind_action
         review_action
       when :finalize
         finalize_action
+      when :human
+        human_action
       when :agent, :council, :inert
         # `|| generic_action(stage)` is the LIVE path for NON-coding
         # `:agent`/`:inert` workflows: `coding_table_action` returns nil for any
@@ -377,6 +399,10 @@ def kind_action
       end
     end
 
+    def human_action
+      marker.name == :complete ? ACTIONS.fetch(:done) : ACTIONS.fetch(:human_needs_input)
+    end
+
     def coding_table_action(stage)
       return nil unless Hive::Workflows.coding_id?(task_workflow.id)
 
diff --git a/lib/hive/task_meta.rb b/lib/hive/task_meta.rb
index e17f251ebe..a0c4a01605 100644
--- a/lib/hive/task_meta.rb
+++ b/lib/hive/task_meta.rb
@@ -11,7 +11,8 @@ module TaskMeta
     FILENAME = "meta.yml".freeze
     WRITABLE_FIELDS = %i[
       id slug display_name depends_on workflow workflow_commit
-      workflow_manifest_digest workflow_configuration_digest base_branch completed_at
+      workflow_manifest_digest workflow_configuration_digest base_branch
+      idempotency_key input_fingerprint completed_at
     ].freeze
 
     class InvalidMetadata < StandardError; end
@@ -53,6 +54,12 @@ def read(task_folder)
           raw["workflow_configuration_digest"] || raw[:workflow_configuration_digest]
         )
       end
+      if raw.key?("idempotency_key") || raw.key?(:idempotency_key)
+        data[:idempotency_key] = normalize_string(raw["idempotency_key"] || raw[:idempotency_key])
+      end
+      if raw.key?("input_fingerprint") || raw.key?(:input_fingerprint)
+        data[:input_fingerprint] = normalize_string(raw["input_fingerprint"] || raw[:input_fingerprint])
+      end
       if key?(raw, "completed_at")
         data[:completed_at] = normalize_completed_at(
           fetch(raw, "completed_at"), label: path(task_folder), warn_on_invalid: true
@@ -72,13 +79,14 @@ def read(task_folder)
       # are observable instead of failing the gate open in silence.
       warn "hive: task_meta: failed to read #{path(task_folder)} " \
            "(#{e.class}: #{e.message}); treating meta as empty " \
-           "(depends_on, workflow, base_branch dropped; managed provenance dropped; completed_at dropped)"
+           "(depends_on, workflow, base_branch dropped; idempotency dropped; " \
+           "managed provenance dropped; completed_at dropped)"
       empty
     end
 
     def read_for_admission(task_folder)
       source = File.read(path(task_folder))
-      %w[depends_on base_branch].each do |field|
+      %w[depends_on base_branch idempotency_key input_fingerprint].each do |field|
         if Hive::Dependencies.duplicate_top_level_key?(source, field)
           return AdmissionRead.new(
             status: :invalid,
@@ -134,7 +142,7 @@ def read_for_admission(task_folder)
 
     def write(task_folder, id:, slug:, display_name:, depends_on: nil, workflow: nil, base_branch: nil,
               workflow_commit: nil, workflow_manifest_digest: nil, workflow_configuration_digest: nil,
-              completed_at: nil)
+              idempotency_key: nil, input_fingerprint: nil, completed_at: nil)
       FileUtils.mkdir_p(task_folder)
       normalized_depends_on = normalize_string(depends_on)
       normalized_workflow = normalize_string(workflow)
@@ -142,6 +150,8 @@ def write(task_folder, id:, slug:, display_name:, depends_on: nil, workflow: nil
       normalized_commit = normalize_string(workflow_commit)
       normalized_digest = normalize_string(workflow_manifest_digest)
       normalized_configuration_digest = normalize_string(workflow_configuration_digest)
+      normalized_idempotency_key = normalize_string(idempotency_key)
+      normalized_input_fingerprint = normalize_string(input_fingerprint)
       normalized_completed_at = normalize_completed_at(completed_at, label: "completed_at", strict: true)
       if normalized_commit.nil? != normalized_digest.nil?
         raise ArgumentError, "workflow_commit and workflow_manifest_digest must be written together"
@@ -149,6 +159,9 @@ def write(task_folder, id:, slug:, display_name:, depends_on: nil, workflow: nil
       if normalized_configuration_digest && normalized_commit.nil?
         raise ArgumentError, "workflow_configuration_digest requires immutable workflow provenance"
       end
+      if normalized_idempotency_key.nil? != normalized_input_fingerprint.nil?
+        raise ArgumentError, "idempotency_key and input_fingerprint must be written together"
+      end
       data = {
         "id" => normalize_id(id),
         "slug" => normalize_string(slug),
@@ -160,6 +173,8 @@ def write(task_folder, id:, slug:, display_name:, depends_on: nil, workflow: nil
       data["workflow_commit"] = normalized_commit if normalized_commit
       data["workflow_manifest_digest"] = normalized_digest if normalized_digest
       data["workflow_configuration_digest"] = normalized_configuration_digest if normalized_configuration_digest
+      data["idempotency_key"] = normalized_idempotency_key if normalized_idempotency_key
+      data["input_fingerprint"] = normalized_input_fingerprint if normalized_input_fingerprint
       data["completed_at"] = normalized_completed_at if normalized_completed_at
       tmp = File.join(task_folder, ".#{FILENAME}.tmp.#{Process.pid}.#{SecureRandom.hex(4)}")
       File.write(tmp, data.to_yaml)
@@ -174,6 +189,10 @@ def write(task_folder, id:, slug:, display_name:, depends_on: nil, workflow: nil
         result[:workflow_manifest_digest] = normalized_digest
         result[:workflow_configuration_digest] = normalized_configuration_digest if normalized_configuration_digest
       end
+      if normalized_idempotency_key
+        result[:idempotency_key] = normalized_idempotency_key
+        result[:input_fingerprint] = normalized_input_fingerprint
+      end
       result[:completed_at] = normalized_completed_at if normalized_completed_at
       result
     ensure
@@ -260,7 +279,9 @@ def normalized_data(raw)
         base_branch: normalize_string(fetch(raw, "base_branch")),
         workflow_commit: normalize_string(fetch(raw, "workflow_commit")),
         workflow_manifest_digest: normalize_string(fetch(raw, "workflow_manifest_digest")),
-        workflow_configuration_digest: normalize_string(fetch(raw, "workflow_configuration_digest"))
+        workflow_configuration_digest: normalize_string(fetch(raw, "workflow_configuration_digest")),
+        idempotency_key: normalize_string(fetch(raw, "idempotency_key")),
+        input_fingerprint: normalize_string(fetch(raw, "input_fingerprint"))
       }
       if key?(raw, "completed_at")
         data[:completed_at] = normalize_completed_at(fetch(raw, "completed_at"), label: "completed_at")
diff --git a/lib/hive/tui/snapshot.rb b/lib/hive/tui/snapshot.rb
index 9cfab2fdee..459b27b683 100644
--- a/lib/hive/tui/snapshot.rb
+++ b/lib/hive/tui/snapshot.rb
@@ -91,6 +91,7 @@ def initialize(legacy_stage_dirs: [].freeze, legacy_migrate_command: nil,
         :action_label,
         :suggested_command,
         :next_action,
+        :outcomes,
         :diagnostic,
         :live_task_lock,
         :task_lock_pid,
@@ -112,6 +113,8 @@ def initialize(legacy_stage_dirs: [].freeze, legacy_migrate_command: nil,
         # unanswered_questions defaults to 0 so payloads / test factories
         # that predate issue #270 keep working; production payloads always
         # emit the integer explicitly.
+        # outcomes defaults to [] for pre-human-stage payloads and factories;
+        # current status payloads always emit the descriptor-derived array.
         def initialize(id: nil, display_name: nil, workflow: nil, worktree_path: nil,
                        pr_url: nil, attempt_id: nil, task_generation: nil,
                        condition_task_generation: nil, commit_generation: nil,
@@ -122,7 +125,7 @@ def initialize(id: nil, display_name: nil, workflow: nil, worktree_path: nil,
                        observation_mtime: nil, folder_mtime: nil, live_task_lock: false,
                        task_lock_pid: nil, task_lock_process_start_time: nil, task_lock_id: nil,
                        implementation_identity: nil,
-                       unanswered_questions: 0, depends_on: nil,
+                       unanswered_questions: 0, outcomes: [], depends_on: nil,
                        blocked_by: nil, dependency_stage: nil,
                        blocked: false, admission_error: nil, held: nil, **rest)
           super(id: id, display_name: display_name,
@@ -157,7 +160,8 @@ def initialize(id: nil, display_name: nil, workflow: nil, worktree_path: nil,
                 task_lock_process_start_time: task_lock_process_start_time,
                 task_lock_id: task_lock_id,
                 implementation_identity: implementation_identity,
-                unanswered_questions: unanswered_questions, **rest)
+                unanswered_questions: unanswered_questions,
+                outcomes: outcomes, **rest)
         end
       end
 
@@ -268,6 +272,7 @@ def self.build_row(payload, project_name)
           action_label: payload["action_label"],
           suggested_command: payload["suggested_command"],
           next_action: payload["next_action"],
+          outcomes: Array(payload["outcomes"]).freeze,
           diagnostic: payload["diagnostic"],
           live_task_lock: payload["live_task_lock"] == true,
           task_lock_pid: payload["task_lock_pid"],
diff --git a/lib/hive/workflow.rb b/lib/hive/workflow.rb
index 60ef8e4900..208672b320 100644
--- a/lib/hive/workflow.rb
+++ b/lib/hive/workflow.rb
@@ -16,7 +16,7 @@ class Workflow
     # :execute/:review_council/:finalize drive coding status/action
     # classification (the coding runners are selected by name, not kind — see
     # Stages::Resolver), and nil is the unspecified default.
-    KNOWN_KINDS = [ nil, :agent, :council, :inert, :execute, :review_council, :finalize ].freeze
+    KNOWN_KINDS = [ nil, :agent, :council, :human, :inert, :execute, :review_council, :finalize ].freeze
 
     # Single source of truth for the council triage artifact default. Referenced
     # by the Council default below, the descriptor parser, and both council
@@ -107,7 +107,8 @@ def executable_slots
       :name, :index, :state_file, :advance_verb, :kind, :skill, :instruction,
       :permissions, :status_mode, :budget_usd, :timeout_sec, :capability,
       :agent, :model, :effort, :input, :reviewers, :council, :deliverable,
-      :workspace, :handoff, :condition_policy, :mapping_role, :mapping_contract
+      :workspace, :handoff, :condition_policy, :mapping_role, :mapping_contract,
+      :outcomes
     ) do
       def initialize(name:, index:, state_file:, advance_verb: nil, kind: nil,
                      skill: nil, instruction: nil, permissions: nil,
@@ -115,13 +116,21 @@ def initialize(name:, index:, state_file:, advance_verb: nil, kind: nil,
                      capability: nil, agent: nil, model: nil, effort: nil,
                      input: nil, reviewers: nil, council: nil, deliverable: nil,
                      workspace: nil, handoff: nil,
-                     condition_policy: nil, mapping_role: nil, mapping_contract: nil)
+                     condition_policy: nil, mapping_role: nil, mapping_contract: nil,
+                     outcomes: nil)
+        outcomes = outcomes&.dup&.freeze unless outcomes&.frozen?
         super
       end
 
       def dir = "#{index}-#{name}"
     end
 
+    Outcome = Data.define(:name, :complete, :artifact, :to) do
+      def initialize(name:, complete: false, artifact: nil, to: nil) = super
+
+      def terminal? = complete
+    end
+
     Council = Data.define(:quorum, :max_rounds, :exit_rule, :on_max_rounds, :triage_output, :revise) do
       def initialize(quorum:, max_rounds: 1, exit_rule: :human,
                      on_max_rounds: :wait, triage_output: DEFAULT_TRIAGE_OUTPUT, revise: nil)
@@ -187,8 +196,58 @@ def validate_structure!
         stages: structural_stages,
         allowed_kinds: KNOWN_KINDS
       )
+      each { |stage| validate_human_stage!(stage) }
     rescue Hive::WorkLedger::InvalidDescriptor => e
       raise ArgumentError, "workflow #{id.inspect} #{e.message}"
     end
+
+    def validate_human_stage!(stage)
+      if stage.kind != :human
+        return unless stage.respond_to?(:outcomes) && stage.outcomes
+
+        raise ArgumentError,
+              "workflow #{id.inspect} non-human stage #{stage.name.inspect} must not declare outcomes"
+      end
+
+      outcomes = stage.outcomes
+      unless outcomes.is_a?(Hash) && !outcomes.empty?
+        raise ArgumentError,
+              "workflow #{id.inspect} human stage #{stage.name.inspect} must declare at least one outcome"
+      end
+
+      outcomes.each do |name, outcome|
+        unless outcome.is_a?(Outcome) && name == outcome.name
+          raise ArgumentError,
+                "workflow #{id.inspect} stage #{stage.name.inspect} outcome #{name.inspect} is invalid"
+        end
+
+        actions = [ outcome.complete, !outcome.to.nil? ].count(true)
+        unless actions == 1
+          raise ArgumentError,
+                "workflow #{id.inspect} stage #{stage.name.inspect} outcome #{name.inspect} " \
+                "must declare exactly one of complete or to"
+        end
+
+        if outcome.complete
+          unless outcome.artifact
+            raise ArgumentError,
+                  "workflow #{id.inspect} stage #{stage.name.inspect} outcome #{name.inspect} " \
+                  "must declare an artifact when complete"
+          end
+          next
+        end
+
+        if outcome.artifact
+          raise ArgumentError,
+                "workflow #{id.inspect} stage #{stage.name.inspect} outcome #{name.inspect} " \
+                "artifact is only valid for a completing outcome"
+        end
+        next if stage_named(outcome.to)
+
+        raise ArgumentError,
+              "workflow #{id.inspect} stage #{stage.name.inspect} outcome #{name.inspect} " \
+              "targets unknown stage #{outcome.to.inspect}"
+      end
+    end
   end
 end
diff --git a/lib/hive/workflow_package/managed_store.rb b/lib/hive/workflow_package/managed_store.rb
index 816202cfcd..e09ea8c637 100644
--- a/lib/hive/workflow_package/managed_store.rb
+++ b/lib/hive/workflow_package/managed_store.rb
@@ -126,6 +126,15 @@ def selected(name, cfg: {}, &legacy_cfg_loader)
         result
       end
 
+      # Validation and other strict no-write probes cannot use #selected:
+      # with_stable_read creates the mutation lock and reconciles an interrupted
+      # transaction before taking its shared read. This variant validates the
+      # selected pointer and configuration without creating, restoring, or
+      # clearing any managed-state path.
+      def selected_read_only(name, cfg: {}, &legacy_cfg_loader)
+        selected_unlocked(name, cfg: cfg, &legacy_cfg_loader)
+      end
+
       def selections(cfg: {})
         result = []
         with_stable_read do
diff --git a/lib/hive/workflows/descriptor_parser.rb b/lib/hive/workflows/descriptor_parser.rb
index 74db4bf3ba..f1c7835b9e 100644
--- a/lib/hive/workflows/descriptor_parser.rb
+++ b/lib/hive/workflows/descriptor_parser.rb
@@ -36,7 +36,9 @@ class DescriptorParser
         permissions
         mapping_role
         mapping_contract
+        outcomes
       ].freeze
+      OUTCOME_KEYS = %w[complete artifact to].freeze
       REVIEWER_KEYS = %w[
         name
         agent
@@ -112,10 +114,10 @@ def validate_terminal_last_stage!(stages)
         return if stages.empty?
 
         last = stages.last
-        return if [ :inert, :agent, :council ].include?(last.kind)
+        return if [ :inert, :agent, :council, :human ].include?(last.kind)
 
         raise descriptor_error(
-          "last stage #{last.name.inspect} must be terminal, agent, or council"
+          "last stage #{last.name.inspect} must be terminal, agent, council, or human"
         )
       end
 
@@ -271,6 +273,7 @@ def parse_stage(data, id:, index:)
         condition_policy = parse_condition_policy(stage["conditions"], label: label)
         reviewers = parse_reviewers(stage["reviewers"], id: id, label: label) if kind == :council
         council = parse_council(stage["council"], id: id, label: label, reviewer_count: reviewers&.length) if kind == :council
+        outcomes = parse_outcomes(stage["outcomes"], stage_name: name, label: label) if kind == :human
         validate_agent_instruction!(skill: skill, instruction: instruction, label: label) if kind == :agent
 
         Hive::Workflow::Stage.new(
@@ -295,7 +298,8 @@ def parse_stage(data, id:, index:)
           condition_policy: condition_policy,
           permissions: permissions,
           mapping_role: parse_mapping_role(stage["mapping_role"], label: label),
-          mapping_contract: optional_string(stage["mapping_contract"], label: "#{label} mapping_contract")
+          mapping_contract: optional_string(stage["mapping_contract"], label: "#{label} mapping_contract"),
+          outcomes: outcomes
         )
       end
 
@@ -320,11 +324,15 @@ def parse_stage_name(value, label:)
       # the rest of the runner assumes and removes the nested-dir fragility in the
       # agent runner's output path.
       def parse_state_file(value, label:)
-        file = required_string(value, label: "#{label} state_file")
+        parse_task_file(value, label: "#{label} state_file")
+      end
+
+      def parse_task_file(value, label:)
+        file = required_string(value, label: label)
         return file if file == File.basename(file) && ![ ".", ".." ].include?(file)
 
         raise descriptor_error(
-          "#{label} state_file #{file.inspect} must be a bare filename inside the task folder " \
+          "#{label} #{file.inspect} must be a bare filename inside the task folder " \
           "(no '/' path separator, not '.' or '..'); built-in stages use basenames like 'idea.md'"
         )
       end
@@ -334,10 +342,50 @@ def parse_kind(value, label:)
         case raw
         when "agent" then :agent
         when "council" then :council
+        when "human" then :human
         when "terminal" then :inert
         else
-          raise descriptor_error("#{label} kind #{raw.inspect} must be agent, council, or terminal")
+          raise descriptor_error("#{label} kind #{raw.inspect} must be agent, council, human, or terminal")
+        end
+      end
+
+      def parse_outcomes(data, stage_name:, label:)
+        unless data.is_a?(Hash) && !data.empty?
+          raise descriptor_error("#{label} human stage must declare at least one outcome")
         end
+
+        outcomes = stringify_hash(data, label: "#{label} #{stage_name.inspect} outcomes")
+        outcomes.each_with_object({}) do |(name, raw), parsed|
+          unless SAFE_SLUG.match?(name)
+            raise descriptor_error(
+              "#{label} #{stage_name.inspect} outcome name #{name.inspect} must match #{SAFE_SLUG.source}"
+            )
+          end
+
+          outcome_label = "#{label} #{stage_name.inspect} outcome #{name.inspect}"
+          value = stringify_hash(raw, label: outcome_label)
+          reject_unknown_keys!(value, OUTCOME_KEYS, label: outcome_label)
+          action_keys = %w[complete to].select { |key| value.key?(key) }
+          unless action_keys.one?
+            raise descriptor_error("#{outcome_label} must declare exactly one of complete or to")
+          end
+
+          if value.key?("complete")
+            unless value["complete"] == true
+              raise descriptor_error("#{outcome_label} complete must be true")
+            end
+            artifact = parse_task_file(value["artifact"], label: "#{outcome_label} artifact")
+            parsed[name] = Hive::Workflow::Outcome.new(
+              name: name, complete: true, artifact: artifact
+            )
+          else
+            if value.key?("artifact")
+              raise descriptor_error("#{outcome_label} artifact is only valid on a completing outcome")
+            end
+            target = parse_stage_name(value["to"], label: "#{outcome_label} target")
+            parsed[name] = Hive::Workflow::Outcome.new(name: name, to: target)
+          end
+        end.freeze
       end
 
       def parse_instruction(value, label:)
@@ -579,6 +627,16 @@ def parse_advance_verb(stage, name:, index:, label:)
       def reject_agent_only_fields!(stage, kind:, label:)
         return if [ :agent, :council ].include?(kind)
 
+        if kind == :human
+          present = %w[skill instruction agent model effort budget_usd timeout_sec reviewers council deliverable workspace handoff conditions permissions mapping_role mapping_contract]
+                    .select { |key| stage.key?(key) }
+          return if present.empty?
+
+          raise descriptor_error(
+            "#{label} #{present.inspect} #{present.one? ? 'is' : 'are'} not valid on a human stage"
+          )
+        end
+
         present = %w[skill instruction agent model effort budget_usd timeout_sec input reviewers council deliverable workspace handoff permissions mapping_role mapping_contract]
                   .select { |key| stage.key?(key) }
         return if present.empty?
@@ -605,6 +663,10 @@ def reject_agent_only_fields!(stage, kind:, label:)
       AGENT_ONLY_FIELDS = %w[skill instruction workspace handoff].freeze
 
       def reject_wrong_kind_fields!(stage, kind:, label:)
+        if kind != :human && stage.key?("outcomes")
+          raise descriptor_error("#{label} outcomes is only valid on a human stage")
+        end
+
         case kind
         when :agent
           reject_fields_for_kind!(stage, COUNCIL_ONLY_FIELDS, kind: :agent, other: "council", label: label)
diff --git a/lib/hive/workflows/loader.rb b/lib/hive/workflows/loader.rb
index c5152b9919..52288a3227 100644
--- a/lib/hive/workflows/loader.rb
+++ b/lib/hive/workflows/loader.rb
@@ -51,6 +51,13 @@ def load_dir(workflows_dir)
       def load_managed(workflows_dir)
         hive_state = File.dirname(workflows_dir)
         store = Hive::WorkflowPackage::ManagedStore.new(hive_state)
+        # Avoid manufacturing the managed-workflow lock file on the overwhelmingly
+        # common authored/built-in read path. No selection exists when there is
+        # no lock manifest to load, so taking the mutation lock cannot improve
+        # consistency and would make read-only commands observable as writes.
+        locks = Dir.glob(File.join(workflows_dir, "*", Hive::WorkflowPackage::ManagedStore::LOCK_FILE))
+        return {} if store.is_a?(Hive::WorkflowPackage::ManagedStore) && locks.empty?
+
         store.selections.each_with_object({}) do |lock, workflows|
           name = lock.fetch("name")
           workflow = store.workflow(
diff --git a/openclaw/README.md b/openclaw/README.md
index 8444a090d6..7434ffc189 100644
--- a/openclaw/README.md
+++ b/openclaw/README.md
@@ -32,6 +32,7 @@ through the progressive setup reference:
 /hive setup
 /hive status
 /hive watch <project>:<slug>
+/hive create a three-stage editorial workflow that researches, drafts, and requires approval before publishing
 ```
 
 Guided setup explains the selected platform channel and asks before changing
@@ -46,6 +47,13 @@ launching OpenClaw and points missing or stale installations to
 `openclaw skills install/update @ivankuznetsov/hive-cli`;
 `hive setup-agents` never writes OpenClaw state.
 
+Natural-language workflow creation is a focused capability inside this same
+`/hive` skill, not another ClawHub listing. It inspects the installed Hive
+version and project workflow IDs before mutation, uses Hive's scaffold and
+validation commands, and creates no task by default. A fresh directory first
+receives the machine-readable minimal-init preview and changes only after one
+explicit confirmation.
+
 On supported Linux/macOS, the approved guided setup reports the loopback URL
 and distinct installed, enabled, running, and ready state for the default
 managed Hive web service alongside daemon setup. Use
diff --git a/openclaw/skills/hive/.hive-skill.json b/openclaw/skills/hive/.hive-skill.json
index 731f525158..6a24ea51a3 100644
--- a/openclaw/skills/hive/.hive-skill.json
+++ b/openclaw/skills/hive/.hive-skill.json
@@ -5,15 +5,23 @@
   "platform": "openclaw",
   "invocation": "/hive",
   "skill_version": "0.1.3",
-  "canonical_digest": "c8aacce6c5e2af2a84c51c65d0b756613a14c686c839321f8a79b9a38a172a7a",
+  "canonical_digest": "f49fae3aaaa1f9c1d0c7f19ebc86f88e3d08ad6069a66133fb43b44ce82e8799",
   "hive_version": "0.6.9",
   "files": {
-    "SKILL.md": "291d24e4ee417d4ad1bdf5d24ca6e14db2181c0571c7e09413da72b801414a01",
+    "SKILL.md": "1fafce4871d38a4372bc2532c3f9d3df5f93681bfceccb1eafd2c67cae11f840",
     "references/recovery.md": "feb3a27a582673d87a46742f2935ee6e1067e3123ef865478263375c1bfd125c",
     "references/release-candidate-qa.md": "595dbc9a9780d56ddb41ffad8554d3121c7bf24567b8b02c539b034b76297dbf",
-    "references/safety.md": "4b420b964a5d729ccafe92bda01738517449f9779e1fe90d184463f461efacd3",
-    "references/setup-and-platforms.md": "f7e3079e8a0b3de79ebce6257c2ab576d22649a97c988043f3053471ee350d8b",
+    "references/safety.md": "1b519b4f186e9b015604c96bf9d1e5d1269ca0d97476a153f595da483453002a",
+    "references/setup-and-platforms.md": "06713fc76c29e7b1507b9b8788e1369e3be7cef48c5ac4d6fefc2dd88bac69ec",
     "references/status-and-watch.md": "9cff9c6eef5344646b682c7f005d56334410f20c6a9a5632b29c2bd7f62b9d49",
-    "references/workflow-actions.md": "e94624d41b1093b2edf1c6e2105b9fd5a17549ef1be46e6260e2cf5178c5ce07"
+    "references/workflow-actions.md": "e94624d41b1093b2edf1c6e2105b9fd5a17549ef1be46e6260e2cf5178c5ce07",
+    "references/workflow-checkpoints.md": "42d2339d9cb068bb2f72c671710cf08770dc1a9a2b65ad96172dfa1376b985a3",
+    "references/workflow-common-mistakes.md": "4e08b7afc90e83b3af340b9e4e59acff49ec6d0434e126e42ee7249f86a00839",
+    "references/workflow-creator-example.md": "6c12986e656d733f154d055d59928e248c423934ffe789bf2d315d5bdb7741e5",
+    "references/workflow-creator.md": "a501ef08124e1f251c80c84443ac569e2d248055d883b4f3e8680fa8cbd0ae74",
+    "references/workflow-permissions.md": "301d72cae02115fec26f450cdc478e27d7c322e5c3dcb0e047bed34804dc6714",
+    "references/workflow-schema.md": "5ecdb7dac296d055aa904d248318e57e941ad7ab7da58f9785c66b7c2a7fbcd2",
+    "references/workflow-stage-design.md": "578b9daf8b2ad2d6fcf984a3f7caad2898a80fa04a5cf97f2bb7f648c960e7fa",
+    "references/workflow-testing.md": "8b44cf16044022bb56b3df91a069ead4c95b12913348c53f06147f4aefd81833"
   }
 }
diff --git a/openclaw/skills/hive/SKILL.md b/openclaw/skills/hive/SKILL.md
index fb4966411a..a9d4c7dd98 100644
--- a/openclaw/skills/hive/SKILL.md
+++ b/openclaw/skills/hive/SKILL.md
@@ -1,6 +1,6 @@
 ---
 name: hive
-description: "Operate Hive task pipelines and pre-release QA through its native CLI. Use when an agent needs to answer current Hive or Hive-bench status, identify the exact active task and blocker owner, follow work through bounded transitions, inspect candidate QA evidence, advance a fresh routine recommendation, diagnose recovery state, initialize or configure Hive, or explain what requires operator action across registered projects."
+description: "Operate Hive task pipelines and pre-release QA through its native CLI. Use when an agent needs to answer current Hive or Hive-bench status, identify the exact active task and blocker owner, follow work through bounded transitions, create a new project-local workflow from an ordinary-language request, inspect candidate QA evidence, advance a fresh routine recommendation, diagnose recovery state, initialize or configure Hive, or explain what requires operator action across registered projects."
 version: "0.1.3"
 user-invocable: true
 metadata:
@@ -17,7 +17,7 @@ metadata:
 platform: openclaw
 invocation: /hive
 skill-version: 0.1.3
-canonical-digest: c8aacce6c5e2af2a84c51c65d0b756613a14c686c839321f8a79b9a38a172a7a
+canonical-digest: f49fae3aaaa1f9c1d0c7f19ebc86f88e3d08ad6069a66133fb43b44ce82e8799
 hive-version: 0.6.9
 -->
 
@@ -56,6 +56,9 @@ Ask before destructive or administrative changes, marker clearing, force/bypass
 
 ## Load the relevant reference
 
+- Read [workflow-creator.md](references/workflow-creator.md) when ordinary language asks to create a new project-local workflow. This focused route is the `hive-workflow-creator` capability inside the single canonical `/hive` skill; it is not a second skill or package.
+- Read [workflow-creator-example.md](references/workflow-creator-example.md) for the accepted research → draft → approval editorial example.
+- Read [workflow-schema.md](references/workflow-schema.md), [workflow-stage-design.md](references/workflow-stage-design.md), [workflow-checkpoints.md](references/workflow-checkpoints.md), [workflow-permissions.md](references/workflow-permissions.md), [workflow-testing.md](references/workflow-testing.md), and [workflow-common-mistakes.md](references/workflow-common-mistakes.md) only as needed while authoring or diagnosing a newly scaffolded descriptor.
 - Read [status-and-watch.md](references/status-and-watch.md) for status interpretation, reporting, compatibility, and native watch semantics.
 - Read [workflow-actions.md](references/workflow-actions.md) for task creation, direct workflow verbs, daemon ownership, closed actions, and completion checks.
 - Read [recovery.md](references/recovery.md) for diagnosis, coordinator ownership, provider holds, stale workers, migration, and guarded recovery.
diff --git a/openclaw/skills/hive/references/safety.md b/openclaw/skills/hive/references/safety.md
index 8b89180bf7..46e0e552a8 100644
--- a/openclaw/skills/hive/references/safety.md
+++ b/openclaw/skills/hive/references/safety.md
@@ -10,10 +10,16 @@ Explain scope, affected state, and recovery before running:
 - Foreground or unbounded streams such as daemon or bot tails when a bounded native status/watch answer is sufficient.
 - Any external message, PR mutation beyond the user’s request, publication, deployment, tag, package release, or version change.
 - Workflow install/update/remove/publish, `setup-agents`, manual patrol starts,
-  and outbound `digest` or `bench submit` operations.
+  and outbound `bench submit` operations.
 
 Routine read-only inspection, `hive watch`, `hive doctor`, and a fresh `hive act` descriptor with `confirmation_required: false` do not require another prompt.
 
+Creating a new workflow authorizes only the newly scaffolded descriptor and
+instruction paths. It never authorizes edits to an existing workflow or task
+creation. Fresh minimal initialization requires its own disclosed preview and
+one explicit confirmation. External publication remains separately gated even
+when a human outcome marks an artifact publish-ready.
+
 No-write Honeycomb workflow previews (`workflow install|update|remove` with
 `--dry-run --json`) are read-only. Do not generalize that exemption: patrol
 and refactor-patrol dry-runs still launch agents and remain consent-gated.
diff --git a/openclaw/skills/hive/references/setup-and-platforms.md b/openclaw/skills/hive/references/setup-and-platforms.md
index 2b1cbf4fdb..1ab6381266 100644
--- a/openclaw/skills/hive/references/setup-and-platforms.md
+++ b/openclaw/skills/hive/references/setup-and-platforms.md
@@ -106,6 +106,14 @@ the user reviews and approves enrollment, ask them to run `hive init .` in
 their own real terminal so they can change those defaults. Do not run it
 headlessly on their behalf or invent bypass flags.
 
+The workflow-creator route has one narrower fresh-project exception. It must
+first show the no-write `hive init --new-workflow ID --minimal --preview
+--json` payload and receive one explicit confirmation. The matching minimal
+execution disables patrol, architecture patrol, ad-hoc auto-fix, daemon
+dispatch/autostart, babysitting, optional reviewers, and service/timer setup;
+it retains core hive/state, registration, and required context hooks. Do not
+substitute ordinary non-TTY init for this preview/confirmation contract.
+
 OpenClaw installs the public ClawHub projection separately:
 
 ```bash
diff --git a/openclaw/skills/hive/references/workflow-checkpoints.md b/openclaw/skills/hive/references/workflow-checkpoints.md
new file mode 100644
index 0000000000..426ffab186
--- /dev/null
+++ b/openclaw/skills/hive/references/workflow-checkpoints.md
@@ -0,0 +1,20 @@
+# Workflow checkpoints
+
+Checkpoints are durable human stages, not comments in an agent instruction.
+Add one only when the request names a decision or the next action would be
+irreversible, externally visible, privileged, or otherwise high consequence.
+
+Declare a small closed set of named outcomes. Each outcome either completes
+with a verified artifact or returns to an existing stage. Hive records the
+decision with its stage visit identity, note, artifact, and timestamp.
+
+Operate a waiting checkpoint with:
+
+```bash
+hive decide TARGET OUTCOME --from STAGE --decision-id DECISION_ID --note "REASON" --json
+```
+
+Read `DECISION_ID` from the waiting stage's `hive run --json` response. The
+stage and visit observations make retries safe. Repeating the same decision is
+a no-op; a stale or conflicting decision is rejected. Do not bypass a
+checkpoint with marker edits, a forced move, or an inferred external action.
diff --git a/openclaw/skills/hive/references/workflow-common-mistakes.md b/openclaw/skills/hive/references/workflow-common-mistakes.md
new file mode 100644
index 0000000000..17187d0959
--- /dev/null
+++ b/openclaw/skills/hive/references/workflow-common-mistakes.md
@@ -0,0 +1,22 @@
+# Workflow creator common mistakes
+
+- Editing an existing descriptor after discovering a collision. Stop and
+  propose Hive’s deterministic available ID instead.
+- Scaffolding before checking the installed version, project, and ID inventory.
+- Treating neutral blank scaffolding as permission to select a research,
+  writing, or coding template by keyword alone.
+- Adding stage indices to YAML. Array order is authoritative.
+- Adding `input` to an ordinary agent stage when its instruction can name the
+  prior artifact; `input` belongs to supported consumers such as human/council
+  stages.
+- Emitting agent/model on every stage instead of inheriting project choices.
+- Adding checkpoints for harmless local work, or omitting them before a named
+  material decision.
+- Turning “before publishing” into an inferred publish stage. Approval may mark
+  an artifact publish-ready but never publishes it.
+- Claiming success after YAML parsing alone. Production workflow resolution and
+  `hive workflow validate` are required.
+- Creating or running a task when the original request asked only for the
+  workflow, or retrying a moved task without its stable idempotency key.
+- Using force, overwriting generated paths not returned by the scaffold, or
+  repairing invalid output in unrelated existing files.
diff --git a/openclaw/skills/hive/references/workflow-creator-example.md b/openclaw/skills/hive/references/workflow-creator-example.md
new file mode 100644
index 0000000000..d55188cb06
--- /dev/null
+++ b/openclaw/skills/hive/references/workflow-creator-example.md
@@ -0,0 +1,48 @@
+# Workflow creator example: editorial approval
+
+Request: “Create a three-stage editorial workflow that researches, drafts,
+and requires approval before publishing.”
+
+The accepted interpretation is exactly `research -> draft -> approval`. There
+is no inferred publish stage and no external publication action. Research and
+draft inherit the project agent/model and use ordinary local `yolo`
+permissions. Approval is a durable human stage.
+
+```yaml
+id: editorial
+stages:
+  - name: research
+    kind: agent
+    state_file: research.md
+    instruction: editorial/research.md
+    permissions: yolo
+  - name: draft
+    kind: agent
+    state_file: draft.md
+    instruction: editorial/draft.md
+    permissions: yolo
+  - name: approval
+    kind: human
+    state_file: approval.md
+    input: draft.md
+    outcomes:
+      approve:
+        complete: true
+        artifact: draft.md
+      reject:
+        to: draft
+```
+
+`approve` requires a non-empty `draft.md`, records it as publish-ready, and
+completes from approval. “Publish-ready” is an artifact status, not authority
+to publish. `reject` records the decision, returns the same task to `draft`,
+and resets draft to `WAITING` so stale completion cannot re-advance it.
+
+Required validation facts:
+
+- stages: research, draft, approval;
+- automatic edges: research to draft, draft to approval;
+- human outcomes: approval.approve completes with draft.md;
+  approval.reject returns to draft;
+- no task for a creation-only request; and
+- no publish stage, destination, command, or external side effect.
diff --git a/openclaw/skills/hive/references/workflow-creator.md b/openclaw/skills/hive/references/workflow-creator.md
new file mode 100644
index 0000000000..68e81824b2
--- /dev/null
+++ b/openclaw/skills/hive/references/workflow-creator.md
@@ -0,0 +1,130 @@
+# Natural-language workflow creator
+
+This is the `hive-workflow-creator` contract inside the canonical `/hive`
+skill. It creates one new owner-authored project workflow. It never edits,
+repairs, overwrites, or partially merges an existing workflow.
+
+## Gate before mutation
+
+The minimum Hive version: 0.6.9. First run `hive version` and parse
+the bare semantic version. If it is older, stop before inspecting or writing
+project workflow paths and say exactly: “Hive 0.6.9 or newer is
+required; run `hive update`, then retry.” Do not invent a compatibility adapter.
+
+After the version passes, run `hive workflow list --json` in the target project
+and inspect `.hive-state/config.yml` when present. Record the project’s current
+agent/model choices, built-ins, managed workflows, and owner-authored IDs.
+Infer a safe lowercase-hyphen ID. A reserved or existing ID is a hard stop:
+leave every file byte-identical and report Hive’s `suggested_id` (or the first
+available numeric suffix obtained from the same ID inventory). Never silently
+switch IDs and continue.
+
+Determine whether the project is initialized before scaffolding. For a fresh
+target, obtain the authoritative preview with:
+
+```bash
+hive init --new-workflow ID --minimal --preview --json
+```
+
+Summarize its project files, hive/state worktree, global registration,
+wiki/context hooks, and explicitly disabled services and automation. Wait for
+one explicit confirmation. If confirmation is declined, absent, or ambiguous,
+stop and leave the target unchanged. After approval, run the same command
+without `--preview`. Never add a force option, select a starter template, or
+initialize an existing target through the minimal route.
+
+For an initialized project, scaffold only after all gates pass:
+
+```bash
+hive workflow new ID --json
+```
+
+Use a built-in sample only when its semantics genuinely match the request;
+otherwise keep the neutral blank scaffold. Modify only `descriptor_path` and
+the newly returned instruction directory. Treat any collision returned by the
+CLI as authoritative and stop with its deterministic alternative.
+
+## Infer, populate, and validate
+
+Translate the original request into the smallest complete graph:
+
+- Preserve the requested stage order and use sequential automatic edges by default.
+- Infer one durable artifact/state file per stage and reusable instruction text for agent stages.
+- Omit stage `agent` and `model` when project inheritance is sufficient.
+- Use `permissions: yolo` for ordinary local agent work unless the request or a high-consequence boundary requires less authority.
+- Add a human stage only for a materially necessary decision or irreversible/high-consequence boundary. Never infer branching, specialist models, or external destinations.
+- Never publish externally, deploy, message, tag, or release from an inferred stage. A separate destination and explicit authorization are mandatory.
+
+Write only the newly scaffolded paths, then run:
+
+```bash
+hive workflow validate ID --json
+```
+
+Success requires `valid: true`, the intended ordered stages and kinds, exact
+automatic edges, exact human outcomes, and every instruction path present.
+On any diagnostic, stop and report it; do not claim success or create a task.
+
+After successful validation, commit the populated graph through Hive so the
+write shares the project commit lock with daemon and task writers:
+
+```bash
+hive workflow commit "$ID"
+```
+
+The commit is part of workflow creation. If it fails, stop and report it; do
+not claim success or create a task from an uncommitted graph.
+
+## Task side-effect boundary
+
+No task by default. A creation-only request ends after validation and the
+populated-graph commit, then prints an exact shell-quoted next command. Render
+every dynamic argv element with POSIX `Shellwords.escape` semantics. In
+particular, never put the request in double quotes: surround it with single
+quotes and replace each embedded `'` with the exact shell sequence `'"'"'`, so
+`$()`, backticks, variables, glob characters, and newlines
+remain literal bytes:
+
+```bash
+hive new 'PROJECT' --workflow 'ID' 'ORIGINAL REQUEST'
+```
+
+Only when the original request explicitly says to create or run a task, derive
+one stable opaque key and keep it unchanged across retries. The
+`workflow-creator:v1` derivation is canonical:
+
+1. Project is `File.realpath(PROJECT_PATH)`.
+2. Workflow is Hive's normalized ID from the scaffold result.
+3. Request is valid UTF-8 normalized to Unicode NFC, with CRLF/CR converted to
+   LF, leading/trailing whitespace stripped, and every remaining whitespace run
+   collapsed to one ASCII space.
+4. Hash the UTF-8 bytes of `JSON.generate([project, workflow, request])` with
+   SHA-256 and use `workflow-creator:v1:<64 lowercase hex>` as `KEY`.
+
+Do not add timestamps, random bytes, agent/session IDs, slugs, or retry counts.
+Create at most once with the same POSIX shell-quoting rule:
+
+```bash
+hive new 'PROJECT' --workflow 'ID' --idempotency-key 'KEY' --json 'ORIGINAL REQUEST'
+```
+
+If the original request also explicitly said run, invoke the reported first
+stage only when `created: true`; `created: false` means a retry and must not
+dispatch that stage again. Then query `hive status --operational --json` and
+report the slug, current stage, daemon disposition, and expected next
+transition from live state.
+
+## Required completion summary
+
+Always report:
+
+- created files (descriptor plus every instruction/reference path);
+- neutral scaffold or the genuinely reused template;
+- applied defaults, including ID, stage order, artifacts, inheritance,
+  permissions, automatic transitions, and every checkpoint/outcome;
+- validation result and normalized graph;
+- task side effect (`none`, `created: true`, or `created: false`);
+- the exact next command when no task was created; and
+- any question or explicit choice that remains material.
+
+Do not describe a workflow as created until Hive has loaded and validated it.
diff --git a/openclaw/skills/hive/references/workflow-permissions.md b/openclaw/skills/hive/references/workflow-permissions.md
new file mode 100644
index 0000000000..d7c8fa2130
--- /dev/null
+++ b/openclaw/skills/hive/references/workflow-permissions.md
@@ -0,0 +1,16 @@
+# Workflow permissions
+
+For ordinary local agent stages created from natural language, use
+`permissions: yolo`; this preserves Hive’s normal local execution semantics.
+Omit stage agent/model fields to inherit project choices.
+
+Do not silently grant external authority. Publishing, deployment, messaging,
+credential use, production mutation, release operations, and destination
+selection require an explicit destination and authorization outside inferred
+workflow creation. Represent an approval request as a human stage; do not hide
+it in an agent prompt.
+
+Use a narrower supported permission scope only when the request clearly needs
+it and the selected agent can enforce it. If enforcement capability or the
+required directories/tools are unclear, ask one material question rather than
+inventing a scope that may be ineffective.
diff --git a/openclaw/skills/hive/references/workflow-schema.md b/openclaw/skills/hive/references/workflow-schema.md
new file mode 100644
index 0000000000..9bb87cb66b
--- /dev/null
+++ b/openclaw/skills/hive/references/workflow-schema.md
@@ -0,0 +1,34 @@
+# Owner-authored workflow schema
+
+Descriptors live at `<hive_state_path>/workflows/ID.yml`; referenced agent
+instructions live below `<hive_state_path>/workflows/ID/`. Use the current
+format only. Stage indices are array order and are not authored as keys.
+
+Every stage declares a safe `name`, `kind`, and relative `state_file`.
+Agent stages declare exactly one of `instruction` or `skill`. Omit agent/model
+to inherit project choices. Ordinary transitions are sequential array edges.
+
+A human stage is closed and non-executable:
+
+```yaml
+- name: approval
+  kind: human
+  state_file: approval.md
+  input: draft.md
+  outcomes:
+    approve:
+      complete: true
+      artifact: draft.md
+    reject:
+      to: draft
+```
+
+Each outcome declares exactly one of `complete: true` or `to: STAGE`.
+Completing outcomes require an artifact. Returning outcomes cannot declare an
+artifact. Outcome and target names must be safe and targets must exist in the
+same descriptor. Human stages cannot declare agent, model, permissions,
+instruction, skill, budgets, timeouts, reviewers, councils, workspaces,
+handoffs, conditions, or executable commands.
+
+Treat `hive workflow validate ID --json` as authoritative for normalized
+stages, automatic edges, human outcomes, and instruction paths.
diff --git a/openclaw/skills/hive/references/workflow-stage-design.md b/openclaw/skills/hive/references/workflow-stage-design.md
new file mode 100644
index 0000000000..8334bd09c6
--- /dev/null
+++ b/openclaw/skills/hive/references/workflow-stage-design.md
@@ -0,0 +1,19 @@
+# Workflow stage design
+
+Use the fewest stages that preserve distinct ownership, durable artifacts, or
+material decisions. A verb phrase in the request is not automatically a stage.
+
+- Split work when it produces a reusable artifact consumed later, needs a
+  different actor/permission boundary, or must wait durably for a person.
+- Keep ordinary transformations sequential. Do not infer speculative branches.
+- Give each agent stage one focused instruction: inputs, required output,
+  completion criteria, and explicit non-goals.
+- Prefer project agent/model inheritance. Specialize only when the request
+  materially requires a capability the inherited choice cannot supply.
+- Use safe lowercase-hyphen stage names and distinct state files.
+- A terminal human approval uses `complete: true` with a required non-empty
+  artifact basename; it does not need a synthetic “done” or “publish” stage.
+
+Before writing, restate the inferred ordered graph, artifacts, inheritance,
+permissions, and decision points. Ask only when two plausible interpretations
+would materially change behavior or consequences.
diff --git a/openclaw/skills/hive/references/workflow-testing.md b/openclaw/skills/hive/references/workflow-testing.md
new file mode 100644
index 0000000000..fbfa6bef2f
--- /dev/null
+++ b/openclaw/skills/hive/references/workflow-testing.md
@@ -0,0 +1,22 @@
+# Workflow creator testing
+
+Validate the descriptor with Hive before reporting success:
+
+```bash
+hive workflow validate ID --json
+```
+
+Check `valid`, origin, descriptor path, ordered stages/kinds, instruction
+paths, automatic edges, and human outcomes. Validation must not change the
+project tree, hive/state commit, tasks, registry, services, or timers.
+
+For a human checkpoint, exercise both outcomes in a disposable task: completing
+requires a non-empty artifact; returning records the decision, moves to the
+declared stage, and leaves it waiting. Verify no agent dispatch occurs while a
+human stage waits.
+
+For an explicitly requested task, repeat the same idempotency key after the
+task moves stages. The response must return one slug with `created: false`.
+Reuse with different input/workflow must fail and leave one task. Finish with a
+fresh operational status snapshot; hermetic validation is primary and live
+agent proof is an additional, credential-gated attestation.
diff --git a/packaging/live_agent_skills/attest.rb b/packaging/live_agent_skills/attest.rb
index fcfe386857..c9cd28b82b 100644
--- a/packaging/live_agent_skills/attest.rb
+++ b/packaging/live_agent_skills/attest.rb
@@ -3,12 +3,13 @@
 
 require_relative "proof"
 
-abort "usage: attest.rb CANDIDATE_SHA WORKFLOW_REVISION REPOSITORY RUN_ID RUN_ATTEMPT ARTIFACT_DIR EVIDENCE_DIR OUTPUT_DIR" unless ARGV.length == 8
+abort "usage: attest.rb CANDIDATE_SHA WORKFLOW_REVISION REPOSITORY RUN_ID RUN_ATTEMPT ARTIFACT_DIR EVIDENCE_DIR CREATOR_EVIDENCE_DIR OUTPUT_DIR" unless ARGV.length == 9
 
 begin
   result = HiveLiveAgentProof::Attestor.new(
     candidate_sha: ARGV[0], workflow_revision: ARGV[1], repository: ARGV[2], run_id: ARGV[3],
-    run_attempt: ARGV[4], artifact_dir: ARGV[5], evidence_dir: ARGV[6], output_dir: ARGV[7]
+    run_attempt: ARGV[4], artifact_dir: ARGV[5], evidence_dir: ARGV[6],
+    creator_evidence_dir: ARGV[7], output_dir: ARGV[8]
   ).call
   puts JSON.generate("attestation_sha256" => result.fetch("sha256"), "path" => result.fetch("path"))
 rescue HiveLiveAgentProof::Error => e
diff --git a/packaging/live_agent_skills/proof.rb b/packaging/live_agent_skills/proof.rb
index 07cc495364..d63450eda7 100644
--- a/packaging/live_agent_skills/proof.rb
+++ b/packaging/live_agent_skills/proof.rb
@@ -29,6 +29,45 @@ module HiveLiveAgentProof
     "--timeout", "20", "--max-events", "5", "--interval", "1", "--json-lines"
   ].freeze
   OBSERVATION_COMMANDS = [ STATUS_ARGV, WATCH_ARGV ].freeze
+  WORKFLOW_CREATOR_REQUEST =
+    "Create a three-stage editorial workflow that researches, drafts, and requires approval before publishing.".freeze
+  WORKFLOW_CREATOR_PROMPT = <<~PROMPT.freeze
+    /hive
+    #{WORKFLOW_CREATOR_REQUEST}
+    Use the installed Hive workflow-creator capability in this initialized project.
+    This is creation-only: validate the result, report the defaults, and do not create or run a task.
+  PROMPT
+  WORKFLOW_CREATOR_TASK_REQUEST = "Research and draft the launch announcement for approval.".freeze
+  WORKFLOW_CREATOR_TASK_KEY = "workflow-creator-proof:editorial:live-proof".freeze
+  WORKFLOW_CREATOR_TASK_SLUG = "editorial-live-proof".freeze
+  WORKFLOW_CREATOR_TASK_PROMPT = <<~PROMPT.freeze
+    /hive
+    Use the validated editorial workflow to create and run one task for:
+    "#{WORKFLOW_CREATOR_TASK_REQUEST}"
+    Use idempotency key #{WORKFLOW_CREATOR_TASK_KEY}. In this exact order: create the task,
+    run its first stage once, repeat the same creation command once to prove the retry is a no-op,
+    then query operational status. Do not publish or perform any other external action.
+  PROMPT
+  WORKFLOW_CREATOR_TASK_NEW_ARGV = [
+    "new", "workflow-creator-proof", "--workflow", "editorial",
+    "--idempotency-key", WORKFLOW_CREATOR_TASK_KEY, "--json", WORKFLOW_CREATOR_TASK_REQUEST
+  ].freeze
+  WORKFLOW_CREATOR_COMMANDS = [
+    [ "version" ],
+    [ "workflow", "list", "--json" ],
+    [ "workflow", "new", "editorial", "--json" ],
+    [ "workflow", "validate", "editorial", "--json" ],
+    [ "workflow", "commit", "editorial" ],
+    WORKFLOW_CREATOR_TASK_NEW_ARGV,
+    [ "run", WORKFLOW_CREATOR_TASK_SLUG ],
+    WORKFLOW_CREATOR_TASK_NEW_ARGV,
+    [ "status", "--operational", "--json" ]
+  ].freeze
+  WORKFLOW_CREATOR_FILES = [
+    ".hive-state/workflows/editorial.yml",
+    ".hive-state/workflows/editorial/draft.md",
+    ".hive-state/workflows/editorial/research.md"
+  ].freeze
   SAFE_SHA = /\A[0-9a-f]{40}\z/.freeze
   SAFE_REPOSITORY = /\A[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+\z/.freeze
   SECRET_PATTERNS = [
@@ -312,7 +351,7 @@ def read_skill_archive(path)
 
   class Attestor
     def initialize(candidate_sha:, workflow_revision:, repository:, run_id:, run_attempt:,
-                   artifact_dir:, evidence_dir:, output_dir:)
+                   artifact_dir:, evidence_dir:, creator_evidence_dir:, output_dir:)
       @candidate_sha = HiveLiveAgentProof.validate_sha!(candidate_sha, "candidate_sha")
       @workflow_revision = HiveLiveAgentProof.validate_sha!(workflow_revision, "workflow_revision")
       @repository = HiveLiveAgentProof.validate_repository!(repository)
@@ -320,6 +359,7 @@ def initialize(candidate_sha:, workflow_revision:, repository:, run_id:, run_att
       @run_attempt = Integer(run_attempt, 10)
       @artifact_dir = File.expand_path(artifact_dir)
       @evidence_dir = File.expand_path(evidence_dir)
+      @creator_evidence_dir = File.expand_path(creator_evidence_dir)
       @output_dir = File.expand_path(output_dir)
       raise Error, "run_id and run_attempt must be positive" unless @run_id.positive? && @run_attempt.positive?
     rescue ArgumentError
@@ -329,6 +369,7 @@ def initialize(candidate_sha:, workflow_revision:, repository:, run_id:, run_att
     def call
       manifest = validate_artifacts!
       evidence = validate_evidence!(manifest)
+      creator_evidence = validate_creator_evidence!(manifest)
       raise Error, "proof output already exists: #{@output_dir}" if File.exist?(@output_dir)
       FileUtils.mkdir_p(File.join(@output_dir, "artifacts"), mode: 0o700)
       FileUtils.mkdir_p(File.join(@output_dir, "evidence"), mode: 0o700)
@@ -341,6 +382,10 @@ def call
       evidence.each do |platform, row|
         HiveLiveAgentProof.write_json(File.join(@output_dir, "evidence", "#{platform}.json"), row)
       end
+      HiveLiveAgentProof.write_json(
+        File.join(@output_dir, "evidence", "openclaw-workflow-creator.json"),
+        creator_evidence
+      )
 
       attestation = {
         "schema" => "hive-live-agent-skills-attestation",
@@ -356,10 +401,11 @@ def call
         },
         "artifacts" => manifest,
         "platforms" => PLATFORMS.to_h { |platform| [ platform, evidence.fetch(platform) ] },
+        "workflow_creator" => creator_evidence,
         "secret_scan" => {
           "status" => "passed",
           "scanner" => "hive-live-agent-proof/v1",
-          "files_scanned" => PLATFORMS.length + 1
+          "files_scanned" => PLATFORMS.length + 2
         }
       }
       attestation_path = File.join(@output_dir, "attestation.json")
@@ -429,6 +475,82 @@ def validate_evidence_row!(platform, row, manifest)
               "#{platform} must use exactly one operational status and one bounded native watch"
       end
     end
+
+    def validate_creator_evidence!(manifest)
+      actual_names = Dir.glob(File.join(@creator_evidence_dir, "*.json")).map { |path| File.basename(path) }
+      unless actual_names == [ "openclaw-workflow-creator.json" ]
+        raise Error, "workflow-creator evidence must contain exactly openclaw-workflow-creator.json"
+      end
+
+      path = File.join(@creator_evidence_dir, actual_names.fetch(0))
+      row = HiveLiveAgentProof.read_json(path)
+      unless row.is_a?(Hash) && row["schema"] == "hive-live-workflow-creator-evidence" &&
+             row["schema_version"] == SCHEMA_VERSION && row["platform"] == "openclaw" &&
+             row["candidate_sha"] == @candidate_sha && row["result"] == "passed"
+        raise Error, "workflow-creator evidence identity or result is invalid"
+      end
+      unless row.dig("skill", "canonical_digest") == manifest["canonical_digest"] &&
+             row.dig("skill", "skill_version") == manifest["skill_version"]
+        raise Error, "workflow-creator evidence canonical provenance mismatch"
+      end
+      unless HiveLiveAgentProof.valid_native_activation?("openclaw", row["native_activation"])
+        raise Error, "workflow-creator native activation evidence is invalid"
+      end
+      expected_prompt = Digest::SHA256.hexdigest(WORKFLOW_CREATOR_PROMPT)
+      expected_task_prompt = Digest::SHA256.hexdigest(WORKFLOW_CREATOR_TASK_PROMPT)
+      unless row["prompt_sha256"] == expected_prompt &&
+             row["task_prompt_sha256"] == expected_task_prompt &&
+             row["hive_commands"] == WORKFLOW_CREATOR_COMMANDS
+        raise Error, "workflow-creator prompt or command sequence is invalid"
+      end
+      validate_creator_result!(row)
+      unless row.dig("secret_scan", "status") == "passed" && row.dig("cleanup", "status") == "passed"
+        raise Error, "workflow-creator evidence lacks secret-scan or cleanup proof"
+      end
+      findings = HiveLiveAgentProof.secret_findings(File.read(path))
+      raise Error, "workflow-creator evidence secret scan failed: #{findings.join(', ')}" unless findings.empty?
+
+      row
+    end
+
+    def validate_creator_result!(row)
+      created = row["created_files"]
+      unless created.is_a?(Array) && created.map { |record| record["path"] }.sort == WORKFLOW_CREATOR_FILES
+        raise Error, "workflow-creator created files are incomplete"
+      end
+      unless created.all? { |record| record.keys.sort == %w[path sha256 size] &&
+        record["sha256"].to_s.match?(/\A[0-9a-f]{64}\z/) && record["size"].to_i.positive? }
+        raise Error, "workflow-creator created-file records are invalid"
+      end
+
+      validation = row["validation"]
+      stages = validation.is_a?(Hash) ? validation["stages"] : nil
+      outcomes = validation.is_a?(Hash) ? validation["human_outcomes"] : nil
+      unless validation&.fetch("valid", false) == true &&
+             stages == %w[research draft approval] &&
+             validation["automatic_edges"] == [ %w[research draft], %w[draft approval] ] &&
+             outcomes == [
+               { "stage" => "approval", "name" => "approve", "complete" => true,
+                 "artifact" => "draft.md", "to" => nil },
+               { "stage" => "approval", "name" => "reject", "complete" => false,
+                 "artifact" => nil, "to" => "draft" }
+             ]
+        raise Error, "workflow-creator normalized graph is invalid"
+      end
+      task = row["task"]
+      unless row["creation_only_task_count"] == 0 && row["task_count"] == 1 &&
+             task == {
+               "slug" => WORKFLOW_CREATOR_TASK_SLUG,
+               "workflow" => "editorial",
+               "first_created" => true,
+               "retry_created" => false,
+               "run_count" => 1,
+               "current_stage" => "1-research"
+             } &&
+             row["external_actions"] == []
+        raise Error, "workflow-creator proof contains an unauthorized side effect"
+      end
+    end
   end
 
   class Verifier
@@ -462,6 +584,7 @@ def call
         HiveLiveAgentProof.verify_file_record!(artifact_root, name, record)
       end
       validate_platforms!(manifest)
+      validate_workflow_creator!(manifest)
       findings = HiveLiveAgentProof.secret_findings(File.read(path))
       raise Error, "proof secret scan failed: #{findings.join(', ')}" unless findings.empty?
 
@@ -509,5 +632,26 @@ def validate_platforms!(manifest)
         end
       end
     end
+
+    def validate_workflow_creator!(manifest)
+      row = @attestation["workflow_creator"]
+      unless row.is_a?(Hash) && row["schema"] == "hive-live-workflow-creator-evidence" &&
+             row["platform"] == "openclaw" && row["candidate_sha"] == @candidate_sha &&
+             row["result"] == "passed" && row.dig("skill", "canonical_digest") == manifest["canonical_digest"] &&
+             row.dig("secret_scan", "status") == "passed" && row.dig("cleanup", "status") == "passed"
+        raise Error, "workflow-creator attested evidence is incomplete"
+      end
+      unless HiveLiveAgentProof.valid_native_activation?("openclaw", row["native_activation"])
+        raise Error, "workflow-creator attested native activation evidence is invalid"
+      end
+      unless row["prompt_sha256"] == Digest::SHA256.hexdigest(WORKFLOW_CREATOR_PROMPT) &&
+             row["task_prompt_sha256"] == Digest::SHA256.hexdigest(WORKFLOW_CREATOR_TASK_PROMPT) &&
+             row["hive_commands"] == WORKFLOW_CREATOR_COMMANDS &&
+             row["creation_only_task_count"] == 0 && row["task_count"] == 1 &&
+             row["task"].is_a?(Hash) && row["task"]["retry_created"] == false &&
+             row["task"]["run_count"] == 1 && row["external_actions"] == []
+        raise Error, "workflow-creator attested contract is invalid"
+      end
+    end
   end
 end
diff --git a/schemas/hive-decide.v1.json b/schemas/hive-decide.v1.json
new file mode 100644
index 0000000000..5bb59e229c
--- /dev/null
+++ b/schemas/hive-decide.v1.json
@@ -0,0 +1,74 @@
+{
+  "$schema": "https://json-schema.org/draft/2020-12/schema",
+  "$id": "urn:hive:schema:decide:v1",
+  "title": "hive-decide v1",
+  "oneOf": [
+    { "$ref": "#/$defs/SuccessPayload" },
+    { "$ref": "#/$defs/ErrorPayload" }
+  ],
+  "$defs": {
+    "SuccessPayload": {
+      "type": "object",
+      "additionalProperties": false,
+      "required": [
+        "schema", "schema_version", "ok", "applied", "noop", "slug", "workflow",
+        "from_stage", "outcome", "decision_id", "note", "decided_at", "completed",
+        "artifact", "artifact_path", "to", "current_stage", "task_folder", "next_action"
+      ],
+      "properties": {
+        "schema": { "const": "hive-decide" },
+        "schema_version": { "const": 1 },
+        "ok": { "const": true },
+        "applied": { "type": "boolean" },
+        "noop": { "type": "boolean" },
+        "slug": { "type": "string" },
+        "workflow": { "type": "string" },
+        "from_stage": { "type": "string", "pattern": "^[0-9]+-[a-z0-9][a-z0-9-]*$" },
+        "outcome": { "type": "string", "pattern": "^[a-z0-9][a-z0-9-]*$" },
+        "decision_id": { "type": "string", "pattern": "^[0-9a-f]{16}$" },
+        "note": { "type": ["string", "null"] },
+        "decided_at": { "type": "string", "format": "date-time" },
+        "completed": { "type": "boolean" },
+        "artifact": { "type": ["string", "null"] },
+        "artifact_path": { "type": ["string", "null"] },
+        "to": { "type": ["string", "null"] },
+        "current_stage": { "type": "string", "pattern": "^[0-9]+-[a-z0-9][a-z0-9-]*$" },
+        "task_folder": { "type": "string" },
+        "next_action": {
+          "type": "object",
+          "additionalProperties": true,
+          "required": ["kind"],
+          "properties": {
+            "kind": { "enum": ["run", "needs_input", "no_op"] },
+            "command": { "type": ["string", "null"] },
+            "reason": { "type": "string" }
+          }
+        }
+      }
+    },
+    "ErrorPayload": {
+      "type": "object",
+      "additionalProperties": false,
+      "required": ["schema", "schema_version", "ok", "error_class", "error_kind", "exit_code", "message"],
+      "properties": {
+        "schema": { "const": "hive-decide" },
+        "schema_version": { "const": 1 },
+        "ok": { "const": false },
+        "error_class": { "type": "string" },
+        "error_kind": {
+          "enum": [
+            "wrong_stage", "ambiguous_slug", "invalid_task_path", "concurrent_run",
+            "config", "git", "internal", "error"
+          ]
+        },
+        "exit_code": { "type": "integer", "minimum": 0, "maximum": 255 },
+        "message": { "type": "string" },
+        "candidates": { "type": "array", "items": { "type": "object", "additionalProperties": true } },
+        "current_stage": { "type": "string" },
+        "target_stage": { "type": "string" },
+        "holder": { "type": "object", "additionalProperties": true },
+        "lock_path": { "type": "string" }
+      }
+    }
+  }
+}
diff --git a/schemas/hive-init-preview.v1.json b/schemas/hive-init-preview.v1.json
new file mode 100644
index 0000000000..3f8b901027
--- /dev/null
+++ b/schemas/hive-init-preview.v1.json
@@ -0,0 +1,132 @@
+{
+  "$schema": "https://json-schema.org/draft/2020-12/schema",
+  "$id": "https://github.com/ivankuznetsov/hive/blob/main/schemas/hive-init-preview.v1.json",
+  "title": "hive init minimal preview (v1)",
+  "description": "Resolved, no-write plan emitted by hive init --new-workflow ID --minimal --preview --json.",
+  "oneOf": [
+    { "$ref": "#/$defs/SuccessPayload" },
+    { "$ref": "#/$defs/ErrorPayload" }
+  ],
+  "$defs": {
+    "SuccessPayload": {
+  "type": "object",
+  "additionalProperties": false,
+  "required": [
+    "schema", "schema_version", "ok", "preview", "minimal", "project", "path",
+    "default_branch", "workflow", "profile", "template", "project_files",
+    "state_worktree", "context_integration", "global_registration", "services",
+    "background_automation", "task_creation"
+  ],
+  "properties": {
+    "schema": { "const": "hive-init-preview" },
+    "schema_version": { "const": 1 },
+    "ok": { "const": true },
+    "preview": { "const": true },
+    "minimal": { "const": true },
+    "project": { "type": "string" },
+    "path": { "type": "string" },
+    "default_branch": { "type": "string" },
+    "workflow": { "type": "string" },
+    "profile": { "const": "minimal" },
+    "template": { "const": "blank" },
+    "project_files": {
+      "type": "array",
+      "minItems": 1,
+      "items": { "type": "string" },
+      "uniqueItems": true
+    },
+    "state_worktree": {
+      "type": "object",
+      "additionalProperties": false,
+      "required": ["branch", "path", "config_path", "workflow_descriptor", "workflow_instruction"],
+      "properties": {
+        "branch": { "const": "hive/state" },
+        "path": { "type": "string" },
+        "config_path": { "type": "string" },
+        "workflow_descriptor": { "type": "string" },
+        "workflow_instruction": { "type": "string" }
+      }
+    },
+    "context_integration": {
+      "type": "object",
+      "additionalProperties": false,
+      "required": ["llm_wiki", "agent_instructions", "post_commit_hook", "scheduler"],
+      "properties": {
+        "llm_wiki": { "const": true },
+        "agent_instructions": { "const": true },
+        "post_commit_hook": { "const": true },
+        "scheduler": { "const": false }
+      }
+    },
+    "global_registration": {
+      "type": "object",
+      "additionalProperties": false,
+      "required": ["enabled", "config_path", "name", "status"],
+      "properties": {
+        "enabled": { "const": true },
+        "config_path": { "type": "string" },
+        "name": { "type": "string" },
+        "status": { "const": "available" }
+      }
+    },
+    "services": {
+      "type": "object",
+      "additionalProperties": false,
+      "required": ["daemon_install", "daemon_autostart", "babysitter_timer", "llm_wiki_timer"],
+      "properties": {
+        "daemon_install": { "const": false },
+        "daemon_autostart": { "const": false },
+        "babysitter_timer": { "const": false },
+        "llm_wiki_timer": { "const": false }
+      }
+    },
+    "background_automation": {
+      "type": "object",
+      "additionalProperties": false,
+      "required": ["daemon_dispatch", "patrol", "refactor_patrol", "adhoc_auto_fix", "babysitter"],
+      "properties": {
+        "daemon_dispatch": { "const": false },
+        "patrol": { "const": false },
+        "refactor_patrol": { "const": false },
+        "adhoc_auto_fix": { "const": false },
+        "babysitter": { "const": false }
+      }
+    },
+    "task_creation": { "const": false }
+  }
+    },
+    "ErrorPayload": {
+      "type": "object",
+      "additionalProperties": false,
+      "required": [
+        "schema", "schema_version", "ok", "error_class", "error_kind", "exit_code",
+        "message", "minimal", "preview", "project", "path", "workflow"
+      ],
+      "properties": {
+        "schema": { "const": "hive-init-preview" },
+        "schema_version": { "const": 1 },
+        "ok": { "const": false },
+        "error_class": { "type": "string" },
+        "error_kind": {
+          "enum": [
+            "usage", "already_initialized", "project_name_collision",
+            "not_git_repository", "main_checkout_required", "dirty_worktree",
+            "concurrent_run", "config", "git", "internal", "error"
+          ]
+        },
+        "exit_code": { "type": "integer", "minimum": 0, "maximum": 255 },
+        "message": { "type": "string" },
+        "minimal": { "const": true },
+        "preview": { "const": true },
+        "project": { "type": "string" },
+        "path": { "type": "string" },
+        "workflow": { "type": "string" },
+        "value": { "type": "string" },
+        "suggested_id": { "type": "string" },
+        "existing_path": { "type": "string" },
+        "holder": { "type": "object", "additionalProperties": true },
+        "lock_path": { "type": "string" }
+      }
+    }
+  }
+}
diff --git a/schemas/hive-init.v2.json b/schemas/hive-init.v2.json
index fba570e580..683d8a7a4f 100644
--- a/schemas/hive-init.v2.json
+++ b/schemas/hive-init.v2.json
@@ -5,9 +5,43 @@
   "description": "Stable success contract emitted by `hive init --json`. V2 adds the explicit fresh-project architecture patrol discovery choice while leaving auto-fix and issue-filing policy in project config.",
   "oneOf": [
     { "$ref": "#/$defs/SuccessPayload" },
-    { "$ref": "#/$defs/AlreadyInitializedPayload" }
+    { "$ref": "#/$defs/AlreadyInitializedPayload" },
+    { "$ref": "#/$defs/ErrorPayload" }
   ],
   "$defs": {
+    "ErrorPayload": {
+      "type": "object",
+      "additionalProperties": false,
+      "required": [
+        "schema", "schema_version", "ok", "error_class", "error_kind", "exit_code",
+        "message", "minimal", "preview", "project", "path", "workflow"
+      ],
+      "properties": {
+        "schema": { "const": "hive-init" },
+        "schema_version": { "const": 2 },
+        "ok": { "const": false },
+        "error_class": { "type": "string" },
+        "error_kind": {
+          "enum": [
+            "usage", "already_initialized", "project_name_collision",
+            "not_git_repository", "main_checkout_required", "dirty_worktree",
+            "concurrent_run", "config", "git", "internal", "error"
+          ]
+        },
+        "exit_code": { "type": "integer", "minimum": 0, "maximum": 255 },
+        "message": { "type": "string" },
+        "minimal": { "const": true },
+        "preview": { "const": false },
+        "project": { "type": "string" },
+        "path": { "type": "string" },
+        "workflow": { "type": "string" },
+        "value": { "type": "string" },
+        "suggested_id": { "type": "string" },
+        "existing_path": { "type": "string" },
+        "holder": { "type": "object", "additionalProperties": true },
+        "lock_path": { "type": "string" }
+      }
+    },
     "AlreadyInitializedPayload": {
       "type": "object",
       "additionalProperties": false,
@@ -31,6 +65,7 @@
         "hive_state_path": { "type": "string" },
         "descriptor_path": { "type": "string" },
         "instruction_path": { "type": "string" },
+        "minimal": { "const": true },
         "workflow": { "type": "string" }
       }
     },
@@ -168,6 +203,7 @@
         "hive_state_path": { "type": "string" },
         "descriptor_path": { "type": "string" },
         "instruction_path": { "type": "string" },
+        "minimal": { "const": true },
         "workflow": { "type": "string" },
         "hints": {
           "type": "array",
diff --git a/schemas/hive-new.v1.json b/schemas/hive-new.v1.json
new file mode 100644
index 0000000000..20cfb6815b
--- /dev/null
+++ b/schemas/hive-new.v1.json
@@ -0,0 +1,73 @@
+{
+  "$schema": "https://json-schema.org/draft/2020-12/schema",
+  "$id": "https://github.com/ivankuznetsov/hive/blob/main/schemas/hive-new.v1.json",
+  "title": "hive new output (v1)",
+  "description": "Machine-readable task creation with optional state-wide idempotency.",
+  "oneOf": [
+    { "$ref": "#/$defs/SuccessPayload" },
+    { "$ref": "#/$defs/ErrorPayload" }
+  ],
+  "$defs": {
+    "HumanOutcome": {
+      "type": "object",
+      "additionalProperties": false,
+      "required": ["name", "complete", "artifact", "to"],
+      "properties": {
+        "name": { "type": "string" },
+        "complete": { "type": "boolean" },
+        "artifact": { "type": ["string", "null"] },
+        "to": { "type": ["string", "null"] }
+      }
+    },
+    "NextAction": {
+      "type": "object",
+      "additionalProperties": false,
+      "required": ["kind", "command", "outcomes"],
+      "properties": {
+        "kind": { "type": "string" },
+        "command": { "type": ["string", "null"] },
+        "outcomes": { "type": "array", "items": { "$ref": "#/$defs/HumanOutcome" } }
+      }
+    },
+    "SuccessPayload": {
+      "type": "object",
+      "additionalProperties": false,
+      "required": [
+        "schema", "schema_version", "ok", "created", "slug", "workflow",
+        "current_stage", "task_folder", "next_action"
+      ],
+      "properties": {
+        "schema": { "const": "hive-new" },
+        "schema_version": { "const": 1 },
+        "ok": { "const": true },
+        "created": { "type": "boolean" },
+        "slug": { "type": "string" },
+        "workflow": { "type": "string" },
+        "current_stage": { "type": "string" },
+        "task_folder": { "type": "string" },
+        "next_action": { "$ref": "#/$defs/NextAction" }
+      }
+    },
+    "ErrorPayload": {
+      "type": "object",
+      "additionalProperties": false,
+      "required": [
+        "schema", "schema_version", "ok", "error_class", "error_kind",
+        "exit_code", "message"
+      ],
+      "properties": {
+        "schema": { "const": "hive-new" },
+        "schema_version": { "const": 1 },
+        "ok": { "const": false },
+        "error_class": { "type": "string" },
+        "error_kind": { "enum": ["usage", "config", "concurrent_run", "internal", "error"] },
+        "exit_code": { "type": "integer" },
+        "message": { "type": "string" },
+        "value": { "type": ["string", "integer"] },
+        "idempotency_key": { "type": "string" },
+        "holder": { "type": "object", "additionalProperties": true },
+        "lock_path": { "type": "string" }
+      }
+    }
+  }
+}
diff --git a/schemas/hive-operational-status.v3.json b/schemas/hive-operational-status.v3.json
index f732bb18d1..0058c24caa 100644
--- a/schemas/hive-operational-status.v3.json
+++ b/schemas/hive-operational-status.v3.json
@@ -226,10 +226,14 @@
         "position": {
           "type": "object",
           "additionalProperties": false,
-          "required": ["stage", "marker"],
+          "required": ["stage", "marker", "allowed_outcomes"],
           "properties": {
             "stage": { "type": "string" },
-            "marker": { "type": "string" }
+            "marker": { "type": "string" },
+            "allowed_outcomes": {
+              "type": "array",
+              "items": { "$ref": "#/$defs/HumanOutcome" }
+            }
           }
         },
         "liveness": {
@@ -328,6 +332,17 @@
         }
       }
     },
+    "HumanOutcome": {
+      "type": "object",
+      "additionalProperties": false,
+      "required": ["name", "complete", "artifact", "to"],
+      "properties": {
+        "name": { "type": "string", "pattern": "^[a-z0-9][a-z0-9-]*$" },
+        "complete": { "type": "boolean" },
+        "artifact": { "type": ["string", "null"] },
+        "to": { "type": ["string", "null"], "pattern": "^[a-z0-9][a-z0-9-]*$" }
+      }
+    },
     "Reason": {
       "type": "object",
       "additionalProperties": false,
diff --git a/schemas/hive-run.v2.json b/schemas/hive-run.v2.json
index 0457934604..c3749d7248 100644
--- a/schemas/hive-run.v2.json
+++ b/schemas/hive-run.v2.json
@@ -115,6 +115,11 @@
             "type": "string"
           }
         },
+        "allowed_outcomes": {
+          "description": "Descriptor-declared outcomes when the current stage requires a human decision.",
+          "type": "array",
+          "items": { "$ref": "#/$defs/HumanOutcome" }
+        },
         "rebase": {
           "description": "Outcome of the auto-rebase pre-step (Hive::Rebase). Always present. `attempted` is true only when the rebase machinery ran past the disabled/no-worktree guards. `succeeded` is true on clean rebase (or no-op when commits_behind==0). `reason` is null on success and a string (snake_case) naming the skip-or-failure cause otherwise. `post_rebase_warnings` carries non-fatal warnings produced after a successful rebase (e.g., `worktree.yml` execute_base_head write failure); always an array, empty when none.",
           "type": "object",
@@ -163,6 +168,7 @@
                 "disabled",
                 "cli_override",
                 "manual_steering",
+                "human_stage",
                 "no_worktree",
                 "pre_existing_rebase",
                 "dirty_worktree",
@@ -189,6 +195,17 @@
         }
       }
     },
+    "HumanOutcome": {
+      "type": "object",
+      "additionalProperties": false,
+      "required": ["name", "complete", "artifact", "to"],
+      "properties": {
+        "name": { "type": "string", "pattern": "^[a-z0-9][a-z0-9-]*$" },
+        "complete": { "type": "boolean" },
+        "artifact": { "type": ["string", "null"] },
+        "to": { "type": ["string", "null"], "pattern": "^[a-z0-9][a-z0-9-]*$" }
+      }
+    },
     "ErrorPayload": {
       "type": "object",
       "additionalProperties": false,
diff --git a/schemas/hive-status.v7.json b/schemas/hive-status.v7.json
index 2cfaa20255..07a4c36c69 100644
--- a/schemas/hive-status.v7.json
+++ b/schemas/hive-status.v7.json
@@ -120,7 +120,7 @@
          "commit_generation", "current_attempt", "conditions", "condition_history",
          "evidence", "closure", "condition_overrides", "condition_gate", "condition_migration", "condition_provenance",
          "shadow_audit", "condition_warning", "unanswered_questions", "action",
-         "action_label", "suggested_command",
+         "action_label", "suggested_command", "outcomes",
         "next_action", "diagnostic"
       ],
       "properties": {
@@ -267,6 +267,10 @@
         },
         "action_label": { "type": "string" },
         "suggested_command": { "type": ["string", "null"] },
+        "outcomes": {
+          "type": "array",
+          "items": { "$ref": "#/$defs/HumanOutcome" }
+        },
         "next_action": {
           "type": ["object", "null"],
           "additionalProperties": true,
@@ -285,6 +289,17 @@
         }
       }
     },
+    "HumanOutcome": {
+      "type": "object",
+      "additionalProperties": false,
+      "required": ["name", "complete", "artifact", "to"],
+      "properties": {
+        "name": { "type": "string", "pattern": "^[a-z0-9][a-z0-9-]*$" },
+        "complete": { "type": "boolean" },
+        "artifact": { "type": ["string", "null"] },
+        "to": { "type": ["string", "null"], "pattern": "^[a-z0-9][a-z0-9-]*$" }
+      }
+    },
     "ImplementationIdentity": {
       "description": "Generation-scoped implementation ownership projected from the task journal. Status reads are pure: preview entries describe the policy that would be used if the stage launched now, while resolved entries came from launch events.",
       "type": "object",
diff --git a/schemas/hive-workflow-new.v1.json b/schemas/hive-workflow-new.v1.json
index b9c17eb0cd..65f25c0056 100644
--- a/schemas/hive-workflow-new.v1.json
+++ b/schemas/hive-workflow-new.v1.json
@@ -114,6 +114,10 @@
           },
           "description": "Present only for missing/unknown subcommand usage errors: the valid subcommand verbs (e.g. [\"new\"]), so an agent discovers them without regexing `message`."
         },
+        "suggested_id": {
+          "type": "string",
+          "description": "The deterministic first available workflow id after a reserved-id or scaffold collision."
+        },
         "holder": {
           "type": "object",
           "description": "Present only for concurrent_run: metadata about the lock holder.",
diff --git a/schemas/hive-workflow-validate.v1.json b/schemas/hive-workflow-validate.v1.json
new file mode 100644
index 0000000000..cc806dd67e
--- /dev/null
+++ b/schemas/hive-workflow-validate.v1.json
@@ -0,0 +1,106 @@
+{
+  "$schema": "https://json-schema.org/draft/2020-12/schema",
+  "$id": "https://github.com/ivankuznetsov/hive/blob/main/schemas/hive-workflow-validate.v1.json",
+  "title": "hive workflow validate output (v1)",
+  "description": "Strictly read-only validation through direct authored, built-in, and managed workflow lookups.",
+  "oneOf": [
+    { "$ref": "#/$defs/SuccessPayload" },
+    { "$ref": "#/$defs/ErrorPayload" }
+  ],
+  "$defs": {
+    "Diagnostic": {
+      "type": "object",
+      "additionalProperties": false,
+      "required": ["message"],
+      "properties": {
+        "message": { "type": "string" }
+      }
+    },
+    "Stage": {
+      "type": "object",
+      "additionalProperties": false,
+      "required": ["name", "dir", "kind", "state_file", "instruction_path", "input"],
+      "properties": {
+        "name": { "type": "string" },
+        "dir": { "type": "string" },
+        "kind": { "type": ["string", "null"] },
+        "state_file": { "type": ["string", "null"] },
+        "instruction_path": { "type": ["string", "null"] },
+        "input": { "type": ["string", "null"] }
+      }
+    },
+    "AutomaticEdge": {
+      "type": "object",
+      "additionalProperties": false,
+      "required": ["from", "to"],
+      "properties": {
+        "from": { "type": "string" },
+        "to": { "type": "string" }
+      }
+    },
+    "HumanOutcome": {
+      "type": "object",
+      "additionalProperties": false,
+      "required": ["stage", "name", "complete", "artifact", "to"],
+      "properties": {
+        "stage": { "type": "string" },
+        "name": { "type": "string" },
+        "complete": { "type": "boolean" },
+        "artifact": { "type": ["string", "null"] },
+        "to": { "type": ["string", "null"] }
+      }
+    },
+    "SuccessPayload": {
+      "type": "object",
+      "additionalProperties": false,
+      "required": [
+        "schema", "schema_version", "ok", "valid", "id", "origin",
+        "descriptor_path", "instruction_paths", "stages", "automatic_edges",
+        "human_outcomes", "diagnostics"
+      ],
+      "properties": {
+        "schema": { "const": "hive-workflow-validate" },
+        "schema_version": { "const": 1 },
+        "ok": { "const": true },
+        "valid": { "const": true },
+        "id": { "type": "string" },
+        "origin": { "enum": ["built_in", "authored", "managed"] },
+        "descriptor_path": { "type": ["string", "null"] },
+        "instruction_paths": {
+          "type": "array",
+          "items": { "type": "string" },
+          "uniqueItems": true
+        },
+        "stages": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/Stage" } },
+        "automatic_edges": { "type": "array", "items": { "$ref": "#/$defs/AutomaticEdge" } },
+        "human_outcomes": { "type": "array", "items": { "$ref": "#/$defs/HumanOutcome" } },
+        "diagnostics": { "type": "array", "maxItems": 0 }
+      }
+    },
+    "ErrorPayload": {
+      "type": "object",
+      "additionalProperties": false,
+      "required": [
+        "schema", "schema_version", "ok", "valid", "id", "error_class",
+        "error_kind", "exit_code", "message", "diagnostics"
+      ],
+      "properties": {
+        "schema": { "const": "hive-workflow-validate" },
+        "schema_version": { "const": 1 },
+        "ok": { "const": false },
+        "valid": { "const": false },
+        "id": { "type": "string" },
+        "error_class": { "type": "string" },
+        "error_kind": { "enum": ["usage", "config", "git", "concurrent_run", "error"] },
+        "exit_code": { "type": "integer" },
+        "message": { "type": "string" },
+        "diagnostics": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/Diagnostic" } },
+        "value": { "type": "string" },
+        "expected": { "type": "array", "items": { "type": "string" } },
+        "suggested_id": { "type": "string" },
+        "holder": { "type": "object", "additionalProperties": true },
+        "lock_path": { "type": "string" }
+      }
+    }
+  }
+}
diff --git a/skills/hive/SKILL.md b/skills/hive/SKILL.md
index 0fd8fd8b4a..71967f1d96 100644
--- a/skills/hive/SKILL.md
+++ b/skills/hive/SKILL.md
@@ -1,6 +1,6 @@
 ---
 name: hive
-description: Operate Hive task pipelines and pre-release QA through its native CLI. Use when an agent needs to answer current Hive or Hive-bench status, identify the exact active task and blocker owner, follow work through bounded transitions, inspect candidate QA evidence, advance a fresh routine recommendation, diagnose recovery state, initialize or configure Hive, or explain what requires operator action across registered projects.
+description: Operate Hive task pipelines and pre-release QA through its native CLI. Use when an agent needs to answer current Hive or Hive-bench status, identify the exact active task and blocker owner, follow work through bounded transitions, create a new project-local workflow from an ordinary-language request, inspect candidate QA evidence, advance a fresh routine recommendation, diagnose recovery state, initialize or configure Hive, or explain what requires operator action across registered projects.
 ---
 
 # Hive Operations
@@ -36,6 +36,9 @@ Ask before destructive or administrative changes, marker clearing, force/bypass
 
 ## Load the relevant reference
 
+- Read [workflow-creator.md](references/workflow-creator.md) when ordinary language asks to create a new project-local workflow. This focused route is the `hive-workflow-creator` capability inside the single canonical `/hive` skill; it is not a second skill or package.
+- Read [workflow-creator-example.md](references/workflow-creator-example.md) for the accepted research → draft → approval editorial example.
+- Read [workflow-schema.md](references/workflow-schema.md), [workflow-stage-design.md](references/workflow-stage-design.md), [workflow-checkpoints.md](references/workflow-checkpoints.md), [workflow-permissions.md](references/workflow-permissions.md), [workflow-testing.md](references/workflow-testing.md), and [workflow-common-mistakes.md](references/workflow-common-mistakes.md) only as needed while authoring or diagnosing a newly scaffolded descriptor.
 - Read [status-and-watch.md](references/status-and-watch.md) for status interpretation, reporting, compatibility, and native watch semantics.
 - Read [workflow-actions.md](references/workflow-actions.md) for task creation, direct workflow verbs, daemon ownership, closed actions, and completion checks.
 - Read [recovery.md](references/recovery.md) for diagnosis, coordinator ownership, provider holds, stale workers, migration, and guarded recovery.
diff --git a/skills/hive/references/safety.md b/skills/hive/references/safety.md
index 8b89180bf7..46e0e552a8 100644
--- a/skills/hive/references/safety.md
+++ b/skills/hive/references/safety.md
@@ -10,10 +10,16 @@ Explain scope, affected state, and recovery before running:
 - Foreground or unbounded streams such as daemon or bot tails when a bounded native status/watch answer is sufficient.
 - Any external message, PR mutation beyond the user’s request, publication, deployment, tag, package release, or version change.
 - Workflow install/update/remove/publish, `setup-agents`, manual patrol starts,
-  and outbound `digest` or `bench submit` operations.
+  and outbound `bench submit` operations.
 
 Routine read-only inspection, `hive watch`, `hive doctor`, and a fresh `hive act` descriptor with `confirmation_required: false` do not require another prompt.
 
+Creating a new workflow authorizes only the newly scaffolded descriptor and
+instruction paths. It never authorizes edits to an existing workflow or task
+creation. Fresh minimal initialization requires its own disclosed preview and
+one explicit confirmation. External publication remains separately gated even
+when a human outcome marks an artifact publish-ready.
+
 No-write Honeycomb workflow previews (`workflow install|update|remove` with
 `--dry-run --json`) are read-only. Do not generalize that exemption: patrol
 and refactor-patrol dry-runs still launch agents and remain consent-gated.
diff --git a/skills/hive/references/setup-and-platforms.md b/skills/hive/references/setup-and-platforms.md
index 406ed6c0b8..7f52d66a0d 100644
--- a/skills/hive/references/setup-and-platforms.md
+++ b/skills/hive/references/setup-and-platforms.md
@@ -106,6 +106,14 @@ the user reviews and approves enrollment, ask them to run `hive init .` in
 their own real terminal so they can change those defaults. Do not run it
 headlessly on their behalf or invent bypass flags.
 
+The workflow-creator route has one narrower fresh-project exception. It must
+first show the no-write `hive init --new-workflow ID --minimal --preview
+--json` payload and receive one explicit confirmation. The matching minimal
+execution disables patrol, architecture patrol, ad-hoc auto-fix, daemon
+dispatch/autostart, babysitting, optional reviewers, and service/timer setup;
+it retains core hive/state, registration, and required context hooks. Do not
+substitute ordinary non-TTY init for this preview/confirmation contract.
+
 OpenClaw installs the public ClawHub projection separately:
 
 ```bash
diff --git a/skills/hive/references/workflow-checkpoints.md b/skills/hive/references/workflow-checkpoints.md
new file mode 100644
index 0000000000..426ffab186
--- /dev/null
+++ b/skills/hive/references/workflow-checkpoints.md
@@ -0,0 +1,20 @@
+# Workflow checkpoints
+
+Checkpoints are durable human stages, not comments in an agent instruction.
+Add one only when the request names a decision or the next action would be
+irreversible, externally visible, privileged, or otherwise high consequence.
+
+Declare a small closed set of named outcomes. Each outcome either completes
+with a verified artifact or returns to an existing stage. Hive records the
+decision with its stage visit identity, note, artifact, and timestamp.
+
+Operate a waiting checkpoint with:
+
+```bash
+hive decide TARGET OUTCOME --from STAGE --decision-id DECISION_ID --note "REASON" --json
+```
+
+Read `DECISION_ID` from the waiting stage's `hive run --json` response. The
+stage and visit observations make retries safe. Repeating the same decision is
+a no-op; a stale or conflicting decision is rejected. Do not bypass a
+checkpoint with marker edits, a forced move, or an inferred external action.
diff --git a/skills/hive/references/workflow-common-mistakes.md b/skills/hive/references/workflow-common-mistakes.md
new file mode 100644
index 0000000000..17187d0959
--- /dev/null
+++ b/skills/hive/references/workflow-common-mistakes.md
@@ -0,0 +1,22 @@
+# Workflow creator common mistakes
+
+- Editing an existing descriptor after discovering a collision. Stop and
+  propose Hive’s deterministic available ID instead.
+- Scaffolding before checking the installed version, project, and ID inventory.
+- Treating neutral blank scaffolding as permission to select a research,
+  writing, or coding template by keyword alone.
+- Adding stage indices to YAML. Array order is authoritative.
+- Adding `input` to an ordinary agent stage when its instruction can name the
+  prior artifact; `input` belongs to supported consumers such as human/council
+  stages.
+- Emitting agent/model on every stage instead of inheriting project choices.
+- Adding checkpoints for harmless local work, or omitting them before a named
+  material decision.
+- Turning “before publishing” into an inferred publish stage. Approval may mark
+  an artifact publish-ready but never publishes it.
+- Claiming success after YAML parsing alone. Production workflow resolution and
+  `hive workflow validate` are required.
+- Creating or running a task when the original request asked only for the
+  workflow, or retrying a moved task without its stable idempotency key.
+- Using force, overwriting generated paths not returned by the scaffold, or
+  repairing invalid output in unrelated existing files.
diff --git a/skills/hive/references/workflow-creator-example.md b/skills/hive/references/workflow-creator-example.md
new file mode 100644
index 0000000000..d55188cb06
--- /dev/null
+++ b/skills/hive/references/workflow-creator-example.md
@@ -0,0 +1,48 @@
+# Workflow creator example: editorial approval
+
+Request: “Create a three-stage editorial workflow that researches, drafts,
+and requires approval before publishing.”
+
+The accepted interpretation is exactly `research -> draft -> approval`. There
+is no inferred publish stage and no external publication action. Research and
+draft inherit the project agent/model and use ordinary local `yolo`
+permissions. Approval is a durable human stage.
+
+```yaml
+id: editorial
+stages:
+  - name: research
+    kind: agent
+    state_file: research.md
+    instruction: editorial/research.md
+    permissions: yolo
+  - name: draft
+    kind: agent
+    state_file: draft.md
+    instruction: editorial/draft.md
+    permissions: yolo
+  - name: approval
+    kind: human
+    state_file: approval.md
+    input: draft.md
+    outcomes:
+      approve:
+        complete: true
+        artifact: draft.md
+      reject:
+        to: draft
+```
+
+`approve` requires a non-empty `draft.md`, records it as publish-ready, and
+completes from approval. “Publish-ready” is an artifact status, not authority
+to publish. `reject` records the decision, returns the same task to `draft`,
+and resets draft to `WAITING` so stale completion cannot re-advance it.
+
+Required validation facts:
+
+- stages: research, draft, approval;
+- automatic edges: research to draft, draft to approval;
+- human outcomes: approval.approve completes with draft.md;
+  approval.reject returns to draft;
+- no task for a creation-only request; and
+- no publish stage, destination, command, or external side effect.
diff --git a/skills/hive/references/workflow-creator.md b/skills/hive/references/workflow-creator.md
new file mode 100644
index 0000000000..0c3b3bc647
--- /dev/null
+++ b/skills/hive/references/workflow-creator.md
@@ -0,0 +1,130 @@
+# Natural-language workflow creator
+
+This is the `hive-workflow-creator` contract inside the canonical `/hive`
+skill. It creates one new owner-authored project workflow. It never edits,
+repairs, overwrites, or partially merges an existing workflow.
+
+## Gate before mutation
+
+The minimum Hive version: {{HIVE_VERSION}}. First run `hive version` and parse
+the bare semantic version. If it is older, stop before inspecting or writing
+project workflow paths and say exactly: “Hive {{HIVE_VERSION}} or newer is
+required; run `hive update`, then retry.” Do not invent a compatibility adapter.
+
+After the version passes, run `hive workflow list --json` in the target project
+and inspect `.hive-state/config.yml` when present. Record the project’s current
+agent/model choices, built-ins, managed workflows, and owner-authored IDs.
+Infer a safe lowercase-hyphen ID. A reserved or existing ID is a hard stop:
+leave every file byte-identical and report Hive’s `suggested_id` (or the first
+available numeric suffix obtained from the same ID inventory). Never silently
+switch IDs and continue.
+
+Determine whether the project is initialized before scaffolding. For a fresh
+target, obtain the authoritative preview with:
+
+```bash
+hive init --new-workflow ID --minimal --preview --json
+```
+
+Summarize its project files, hive/state worktree, global registration,
+wiki/context hooks, and explicitly disabled services and automation. Wait for
+one explicit confirmation. If confirmation is declined, absent, or ambiguous,
+stop and leave the target unchanged. After approval, run the same command
+without `--preview`. Never add a force option, select a starter template, or
+initialize an existing target through the minimal route.
+
+For an initialized project, scaffold only after all gates pass:
+
+```bash
+hive workflow new ID --json
+```
+
+Use a built-in sample only when its semantics genuinely match the request;
+otherwise keep the neutral blank scaffold. Modify only `descriptor_path` and
+the newly returned instruction directory. Treat any collision returned by the
+CLI as authoritative and stop with its deterministic alternative.
+
+## Infer, populate, and validate
+
+Translate the original request into the smallest complete graph:
+
+- Preserve the requested stage order and use sequential automatic edges by default.
+- Infer one durable artifact/state file per stage and reusable instruction text for agent stages.
+- Omit stage `agent` and `model` when project inheritance is sufficient.
+- Use `permissions: yolo` for ordinary local agent work unless the request or a high-consequence boundary requires less authority.
+- Add a human stage only for a materially necessary decision or irreversible/high-consequence boundary. Never infer branching, specialist models, or external destinations.
+- Never publish externally, deploy, message, tag, or release from an inferred stage. A separate destination and explicit authorization are mandatory.
+
+Write only the newly scaffolded paths, then run:
+
+```bash
+hive workflow validate ID --json
+```
+
+Success requires `valid: true`, the intended ordered stages and kinds, exact
+automatic edges, exact human outcomes, and every instruction path present.
+On any diagnostic, stop and report it; do not claim success or create a task.
+
+After successful validation, commit the populated graph through Hive so the
+write shares the project commit lock with daemon and task writers:
+
+```bash
+hive workflow commit "$ID"
+```
+
+The commit is part of workflow creation. If it fails, stop and report it; do
+not claim success or create a task from an uncommitted graph.
+
+## Task side-effect boundary
+
+No task by default. A creation-only request ends after validation and the
+populated-graph commit, then prints an exact shell-quoted next command. Render
+every dynamic argv element with POSIX `Shellwords.escape` semantics. In
+particular, never put the request in double quotes: surround it with single
+quotes and replace each embedded `'` with the exact shell sequence `'"'"'`, so
+`$()`, backticks, variables, glob characters, and newlines
+remain literal bytes:
+
+```bash
+hive new 'PROJECT' --workflow 'ID' 'ORIGINAL REQUEST'
+```
+
+Only when the original request explicitly says to create or run a task, derive
+one stable opaque key and keep it unchanged across retries. The
+`workflow-creator:v1` derivation is canonical:
+
+1. Project is `File.realpath(PROJECT_PATH)`.
+2. Workflow is Hive's normalized ID from the scaffold result.
+3. Request is valid UTF-8 normalized to Unicode NFC, with CRLF/CR converted to
+   LF, leading/trailing whitespace stripped, and every remaining whitespace run
+   collapsed to one ASCII space.
+4. Hash the UTF-8 bytes of `JSON.generate([project, workflow, request])` with
+   SHA-256 and use `workflow-creator:v1:<64 lowercase hex>` as `KEY`.
+
+Do not add timestamps, random bytes, agent/session IDs, slugs, or retry counts.
+Create at most once with the same POSIX shell-quoting rule:
+
+```bash
+hive new 'PROJECT' --workflow 'ID' --idempotency-key 'KEY' --json 'ORIGINAL REQUEST'
+```
+
+If the original request also explicitly said run, invoke the reported first
+stage only when `created: true`; `created: false` means a retry and must not
+dispatch that stage again. Then query `hive status --operational --json` and
+report the slug, current stage, daemon disposition, and expected next
+transition from live state.
+
+## Required completion summary
+
+Always report:
+
+- created files (descriptor plus every instruction/reference path);
+- neutral scaffold or the genuinely reused template;
+- applied defaults, including ID, stage order, artifacts, inheritance,
+  permissions, automatic transitions, and every checkpoint/outcome;
+- validation result and normalized graph;
+- task side effect (`none`, `created: true`, or `created: false`);
+- the exact next command when no task was created; and
+- any question or explicit choice that remains material.
+
+Do not describe a workflow as created until Hive has loaded and validated it.
diff --git a/skills/hive/references/workflow-permissions.md b/skills/hive/references/workflow-permissions.md
new file mode 100644
index 0000000000..d7c8fa2130
--- /dev/null
+++ b/skills/hive/references/workflow-permissions.md
@@ -0,0 +1,16 @@
+# Workflow permissions
+
+For ordinary local agent stages created from natural language, use
+`permissions: yolo`; this preserves Hive’s normal local execution semantics.
+Omit stage agent/model fields to inherit project choices.
+
+Do not silently grant external authority. Publishing, deployment, messaging,
+credential use, production mutation, release operations, and destination
+selection require an explicit destination and authorization outside inferred
+workflow creation. Represent an approval request as a human stage; do not hide
+it in an agent prompt.
+
+Use a narrower supported permission scope only when the request clearly needs
+it and the selected agent can enforce it. If enforcement capability or the
+required directories/tools are unclear, ask one material question rather than
+inventing a scope that may be ineffective.
diff --git a/skills/hive/references/workflow-schema.md b/skills/hive/references/workflow-schema.md
new file mode 100644
index 0000000000..9bb87cb66b
--- /dev/null
+++ b/skills/hive/references/workflow-schema.md
@@ -0,0 +1,34 @@
+# Owner-authored workflow schema
+
+Descriptors live at `<hive_state_path>/workflows/ID.yml`; referenced agent
+instructions live below `<hive_state_path>/workflows/ID/`. Use the current
+format only. Stage indices are array order and are not authored as keys.
+
+Every stage declares a safe `name`, `kind`, and relative `state_file`.
+Agent stages declare exactly one of `instruction` or `skill`. Omit agent/model
+to inherit project choices. Ordinary transitions are sequential array edges.
+
+A human stage is closed and non-executable:
+
+```yaml
+- name: approval
+  kind: human
+  state_file: approval.md
+  input: draft.md
+  outcomes:
+    approve:
+      complete: true
+      artifact: draft.md
+    reject:
+      to: draft
+```
+
+Each outcome declares exactly one of `complete: true` or `to: STAGE`.
+Completing outcomes require an artifact. Returning outcomes cannot declare an
+artifact. Outcome and target names must be safe and targets must exist in the
+same descriptor. Human stages cannot declare agent, model, permissions,
+instruction, skill, budgets, timeouts, reviewers, councils, workspaces,
+handoffs, conditions, or executable commands.
+
+Treat `hive workflow validate ID --json` as authoritative for normalized
+stages, automatic edges, human outcomes, and instruction paths.
diff --git a/skills/hive/references/workflow-stage-design.md b/skills/hive/references/workflow-stage-design.md
new file mode 100644
index 0000000000..8334bd09c6
--- /dev/null
+++ b/skills/hive/references/workflow-stage-design.md
@@ -0,0 +1,19 @@
+# Workflow stage design
+
+Use the fewest stages that preserve distinct ownership, durable artifacts, or
+material decisions. A verb phrase in the request is not automatically a stage.
+
+- Split work when it produces a reusable artifact consumed later, needs a
+  different actor/permission boundary, or must wait durably for a person.
+- Keep ordinary transformations sequential. Do not infer speculative branches.
+- Give each agent stage one focused instruction: inputs, required output,
+  completion criteria, and explicit non-goals.
+- Prefer project agent/model inheritance. Specialize only when the request
+  materially requires a capability the inherited choice cannot supply.
+- Use safe lowercase-hyphen stage names and distinct state files.
+- A terminal human approval uses `complete: true` with a required non-empty
+  artifact basename; it does not need a synthetic “done” or “publish” stage.
+
+Before writing, restate the inferred ordered graph, artifacts, inheritance,
+permissions, and decision points. Ask only when two plausible interpretations
+would materially change behavior or consequences.
diff --git a/skills/hive/references/workflow-testing.md b/skills/hive/references/workflow-testing.md
new file mode 100644
index 0000000000..fbfa6bef2f
--- /dev/null
+++ b/skills/hive/references/workflow-testing.md
@@ -0,0 +1,22 @@
+# Workflow creator testing
+
+Validate the descriptor with Hive before reporting success:
+
+```bash
+hive workflow validate ID --json
+```
+
+Check `valid`, origin, descriptor path, ordered stages/kinds, instruction
+paths, automatic edges, and human outcomes. Validation must not change the
+project tree, hive/state commit, tasks, registry, services, or timers.
+
+For a human checkpoint, exercise both outcomes in a disposable task: completing
+requires a non-empty artifact; returning records the decision, moves to the
+declared stage, and leaves it waiting. Verify no agent dispatch occurs while a
+human stage waits.
+
+For an explicitly requested task, repeat the same idempotency key after the
+task moves stages. The response must return one slug with `created: false`.
+Reuse with different input/workflow must fail and leave one task. Finish with a
+fresh operational status snapshot; hermetic validation is primary and live
+agent proof is an additional, credential-gated attestation.
diff --git a/skills/hive/skill.json b/skills/hive/skill.json
index 0c1ef5669f..71bdcd15db 100644
--- a/skills/hive/skill.json
+++ b/skills/hive/skill.json
@@ -5,6 +5,14 @@
   "name": "hive",
   "version": "0.1.3",
   "references": [
+    "references/workflow-creator.md",
+    "references/workflow-creator-example.md",
+    "references/workflow-schema.md",
+    "references/workflow-stage-design.md",
+    "references/workflow-checkpoints.md",
+    "references/workflow-permissions.md",
+    "references/workflow-testing.md",
+    "references/workflow-common-mistakes.md",
     "references/status-and-watch.md",
     "references/workflow-actions.md",
     "references/recovery.md",
diff --git a/templates/project_config.yml.erb b/templates/project_config.yml.erb
index 82dbf8fe6a..b18493632a 100644
--- a/templates/project_config.yml.erb
+++ b/templates/project_config.yml.erb
@@ -235,6 +235,9 @@ review:
   # here; disabled reviewers are omitted from the array entirely. The
   # deep-merge replaces `review.reviewers` wholesale, so editing this
   # list later replaces it (no per-element merge).
+<% if enabled_reviewers.empty? -%>
+  reviewers: []
+<% else -%>
   reviewers:
 <% if enabled_reviewers.include?("claude-ce-code-review") -%>
     - name: claude-ce-code-review
@@ -276,6 +279,7 @@ review:
       budget_usd: 50
       timeout_sec: 7200
 <% end -%>
+<% end -%>
 
 # Hive daemon (ADR-024): auto-advances tasks through the pipeline up to
 # human-input gates. When `enabled: true`, `hive daemon start` polls
@@ -367,6 +371,9 @@ patrol:
   review:
     max_context_files: 4
     max_owned_files: 4
+<% if patrol_reviewers.empty? -%>
+    reviewers: []
+<% else -%>
     reviewers:
 <% if patrol_reviewers.include?("codex-native-review") -%>
       # Native `codex review` — one cheap, tuned, read-only pass whose
@@ -398,6 +405,7 @@ patrol:
         budget_usd: 50
         timeout_sec: 5400
 <% end -%>
+<% end -%>
 
 # Architecture patrol is independent from ordinary patrol. Fresh init
 # recommends discovery with confined auto-fix/PR attempts and GitHub issues as
diff --git a/test/integration/cli_usage_error_json_test.rb b/test/integration/cli_usage_error_json_test.rb
index 892975af1d..b69f8c6e45 100644
--- a/test/integration/cli_usage_error_json_test.rb
+++ b/test/integration/cli_usage_error_json_test.rb
@@ -55,6 +55,7 @@ def test_json_usage_errors_emit_envelopes_before_command_dispatch
     cases = [
       [ %w[run --json], "hive-run", {} ],
       [ %w[approve --json], "hive-approve", {} ],
+      [ %w[decide task approve --json], "hive-decide", {} ],
       [ %w[markers clear --json], "hive-markers-clear", {} ],
       [ %w[drop --json], "hive-drop", {} ],
       [ %w[findings --json], "hive-findings", {} ],
@@ -211,8 +212,8 @@ def test_bare_workflow_json_usage_error_uses_workflow_new_envelope
       assert_equal "UsageError", payload["error_class"]
       assert_equal "usage", payload["error_kind"]
       assert_equal Hive::ExitCodes::USAGE, payload["exit_code"]
-      assert_equal "missing SUBCOMMAND (expected: new, install, list, update, remove, publish)", payload["message"]
-      assert_equal %w[new install list update remove publish], payload["expected"]
+      assert_equal "missing SUBCOMMAND (expected: new, validate, commit, install, list, update, remove, publish)", payload["message"]
+      assert_equal %w[new validate commit install list update remove publish], payload["expected"]
     end
   end
 
@@ -223,7 +224,7 @@ def test_bare_workflow_human_usage_error_names_expected_subcommand
       refute status.success?
       assert_equal Hive::ExitCodes::USAGE, status.exitstatus
       assert_empty out
-      assert_equal "hive workflow: missing SUBCOMMAND (expected: new, install, list, update, remove, publish)\n", err
+      assert_equal "hive workflow: missing SUBCOMMAND (expected: new, validate, commit, install, list, update, remove, publish)\n", err
     end
   end
 
@@ -258,6 +259,17 @@ def test_too_many_positionals_workflow_json_usage_error_uses_workflow_new_envelo
     end
   end
 
+  def test_workflow_validate_arity_error_uses_validate_envelope
+    with_tmp_global_config do |home|
+      assert_pre_dispatch_error(
+        home,
+        %w[workflow validate editorial extra --json],
+        schema: "hive-workflow-validate",
+        error_kind: "usage"
+      )
+    end
+  end
+
   # Human-mode (non-`--json`) sibling of the case above. bin/hive branches its
   # stderr prefix on whether a JSON envelope was emitted (`hive: ...` only when
   # JSON rode out); without --json the raw Thor arity prose must surface on
diff --git a/test/integration/cli_version_test.rb b/test/integration/cli_version_test.rb
index 15b8aed4ff..2cc872dc64 100644
--- a/test/integration/cli_version_test.rb
+++ b/test/integration/cli_version_test.rb
@@ -14,6 +14,40 @@ def test_bin_hive_version_outputs_version
     assert_equal "#{Hive::VERSION}\n", out
   end
 
+  def test_strict_no_write_routes_skip_scheduler_reconciliation
+    with_tmp_global_config do |home|
+      with_tmp_git_repo do |project_root|
+        capture_io { Hive::Commands::Init.new(project_root, agent_skill_preflight: false).call }
+        assert_startup_reconcile(project_root, home, %w[workflow validate coding --json], expected: false)
+      end
+
+      with_tmp_git_repo do |project_root|
+        assert_startup_reconcile(
+          project_root, home,
+          [ "init", "--new-workflow", "editorial", "--minimal", "--preview", "--json" ],
+          expected: false
+        )
+      end
+
+      with_tmp_git_repo do |project_root|
+        assert_startup_reconcile(
+          project_root, home,
+          [ "init", "--new-workflow", "workflow", "--minimal", "--preview", "--json" ],
+          expected: false
+        )
+      end
+
+      with_tmp_git_repo do |project_root|
+        capture_io { Hive::Commands::Init.new(project_root, agent_skill_preflight: false).call }
+        assert_startup_reconcile(
+          project_root, home, %w[workflow --json validate coding], expected: false
+        )
+      end
+
+      assert_startup_reconcile(Dir.pwd, home, [ "--version" ], expected: true)
+    end
+  end
+
   def test_bin_hive_help_after_option_value_shows_command_usage
     out, err, status = Open3.capture3(
       RbConfig.ruby, "-Ilib", "bin/hive", "approve", "--from", "2-brainstorm", "--help"
@@ -189,6 +223,32 @@ def run_status_json!(*args, home:)
     JSON.parse(out)
   end
 
+  def assert_startup_reconcile(chdir, home, args, expected:)
+    with_tmp_dir do |dir|
+      probe = File.join(dir, "reconciled")
+      preload = File.join(dir, "scheduler_probe.rb")
+      File.write(preload, <<~RUBY)
+        require "hive/llm_wiki_bootstrap"
+        Hive::LlmWikiBootstrap::Scheduler.define_singleton_method(:reconcile_existing!) do |**|
+          File.write(ENV.fetch("HIVE_RECONCILE_PROBE"), "called")
+        end
+      RUBY
+      env = {
+        "HIVE_HOME" => home,
+        "HOME" => home,
+        "PATH" => ENV.fetch("PATH", ""),
+        "HIVE_RECONCILE_PROBE" => probe,
+        "RUBYOPT" => [ ENV["RUBYOPT"], "-r#{preload}" ].compact.join(" ")
+      }
+      bin = File.expand_path("../../bin/hive", __dir__)
+      lib = File.expand_path("../../lib", __dir__)
+      out, err, status = Open3.capture3(env, RbConfig.ruby, "-I#{lib}", bin, *args, chdir: chdir)
+
+      assert status.success?, "#{args.join(' ')} failed: #{out}\n#{err}"
+      assert_equal expected, File.exist?(probe)
+    end
+  end
+
   def assert_new_idea_includes(dir, text)
     idea_paths = Dir[File.join(dir, ".hive-state", "stages", "1-inbox", "*", "idea.md")]
     assert_equal 1, idea_paths.size, "expected one captured idea, got #{idea_paths.inspect}"
diff --git a/test/integration/decide_test.rb b/test/integration/decide_test.rb
new file mode 100644
index 0000000000..c3347816c0
--- /dev/null
+++ b/test/integration/decide_test.rb
@@ -0,0 +1,900 @@
+require "test_helper"
+require "json"
+require "json_schemer"
+require "hive/commands/approve"
+require "hive/commands/decide"
+require "hive/commands/init"
+require "hive/commands/new"
+require "hive/commands/run"
+require "hive/commands/status"
+require "hive/daemon/policy"
+require "hive/cli"
+
+class DecideTest < Minitest::Test
+  include HiveTestHelper
+
+  SLUG_PATTERN = "editorial-probe-*".freeze
+
+  def test_latest_record_ignores_malformed_audit_lines
+    with_tmp_dir do |dir|
+      path = File.join(dir, "approval.md")
+      File.write(path, <<~TEXT)
+        <!-- HIVE_DECISION_V1 {not-json} -->
+        <!-- HIVE_DECISION_V1 {"decision_id":"valid","outcome":"approve"} -->
+      TEXT
+
+      assert_equal "valid", Hive::Commands::Decide.latest_record(path).fetch("decision_id")
+      assert_nil Hive::Commands::Decide.latest_record(File.join(dir, "missing.md"))
+    end
+  end
+
+  def test_state_reader_rejects_file_swaps_invalid_utf8_and_nofollow_errors
+    with_tmp_dir do |dir|
+      path = File.join(dir, "approval.md")
+      File.binwrite(path, "\xFF".b)
+      error = assert_raises(Hive::InvalidTaskPath) do
+        Hive::Commands::Decide.read_state_file(path)
+      end
+      assert_includes error.message, "valid UTF-8"
+
+      File.write(path, "owner approval\n")
+      observed = File.lstat(path)
+      swapped_stat = Struct.new(:dev, :ino) do
+        def file? = true
+      end.new(observed.dev, observed.ino + 1)
+      swapped_file = Object.new
+      swapped_file.define_singleton_method(:stat) { swapped_stat }
+      swapped_file.define_singleton_method(:read) { "replacement\n" }
+
+      with_replaced_singleton_method(
+        File, :open, ->(_path, *_args, &block) { block.call(swapped_file) }
+      ) do
+        error = assert_raises(Hive::InvalidTaskPath) do
+          Hive::Commands::Decide.read_state_file(path)
+        end
+        assert_includes error.message, "changed while opening it"
+      end
+
+      with_replaced_singleton_method(
+        File, :open, ->(*_args) { raise Errno::ELOOP, path }
+      ) do
+        error = assert_raises(Hive::InvalidTaskPath) do
+          Hive::Commands::Decide.read_state_file(path)
+        end
+        assert_includes error.message, "must be a regular file, not a symlink"
+      end
+    end
+  end
+
+  def test_restore_files_removes_a_path_created_after_snapshot
+    with_tmp_dir do |dir|
+      path = File.join(dir, "created.md")
+      File.write(path, "created")
+
+      Hive::Commands::Decide.new("task", "approve", from: "approval").send(
+        :restore_files, dir,
+        { "created.md" => { existed: false, body: nil } }
+      )
+
+      refute File.exist?(path)
+    end
+  end
+
+  def test_error_kinds_and_argument_guards_are_typed
+    command = Hive::Commands::Decide.new("task", "approve", from: "approval")
+    cases = [
+      [ Hive::WrongStage.new("wrong"), "wrong_stage" ],
+      [
+        Hive::AmbiguousSlug.new("ambiguous", slug: "task", candidates: []),
+        "ambiguous_slug"
+      ],
+      [ Hive::InvalidTaskPath.new("bad"), "invalid_task_path" ],
+      [ Hive::ConcurrentRunError.new("busy"), "concurrent_run" ],
+      [ Hive::ConfigError.new("config"), "config" ],
+      [ Hive::GitError.new("git"), "git" ],
+      [ Hive::InternalError.new("internal"), "internal" ],
+      [ RuntimeError.new("other"), "error" ]
+    ]
+    cases.each do |error, kind|
+      assert_equal kind, command.envelope_error_kind(error)
+    end
+
+    assert_raises(Hive::InvalidTaskPath) do
+      Hive::Commands::Decide.new(
+        "task", "approve", from: "", decision_id: "a" * 16, note: "ok"
+      ).send(:validate_arguments!)
+    end
+    assert_raises(Hive::InvalidTaskPath) do
+      Hive::Commands::Decide.new(
+        "task", "", from: "approval", decision_id: "a" * 16
+      ).send(:validate_arguments!)
+    end
+    assert_raises(Hive::InvalidTaskPath) do
+      Hive::Commands::Decide.new(
+        "task", "approve", from: "approval", decision_id: "a" * 16, note: 123
+      )
+                            .send(:validate_arguments!)
+    end
+    assert_raises(Hive::InvalidTaskPath) do
+      Hive::Commands::Decide.new("task", "approve", from: "approval")
+                            .send(:validate_arguments!)
+    end
+  end
+
+  def test_entering_human_stage_waits_without_dispatch_and_surfaces_outcomes
+    with_editorial_task do |_dir, approval, slug|
+      task = Hive::Task.new(approval)
+      marker = Hive::Markers.current(task.state_file)
+      action = Hive::TaskAction.for(task, marker)
+
+      assert_equal :waiting, marker.name
+      assert_match(/\A[0-9a-f]{16}\z/, marker.attrs.fetch("decision_id"))
+      assert_equal "needs_input", action.key
+      assert_nil action.command
+      assert_equal %w[approve reject], action.allowed_outcomes.map { |entry| entry.fetch("name") }
+      assert_equal :skip, Hive::Daemon::Policy.decide(
+        action: action.key, stage: "3-approval", workflow: "editorial",
+        command: action.command, state_file_mtime: File.mtime(task.state_file),
+        last_dispatched_state_file_mtime: nil, now: Time.now
+      )
+
+      out, = capture_io { Hive::Commands::Run.new(slug, json: true).call }
+      payload = JSON.parse(out)
+      assert_equal true, payload.fetch("ok")
+      assert_equal "waiting", payload.fetch("marker")
+      assert_equal "human_decision_required", payload.dig("next_action", "reason")
+      assert_includes payload.dig("next_action", "decide_with"),
+                      "--decision-id #{marker.attrs.fetch('decision_id')}"
+      assert_equal %w[approve reject], payload.fetch("allowed_outcomes").map { |entry| entry.fetch("name") }
+      assert_schema_valid("hive-run", payload)
+    end
+  end
+
+  def test_approve_records_publish_ready_artifact_and_completes_idempotently
+    with_editorial_task do |dir, approval, slug|
+      decided_at = Time.now.utc
+      out, = capture_io do
+        Hive::Commands::Decide.new(
+          slug, "approve", from: "approval", decision_id: decision_id_for(approval),
+          note: "Ready for the editor", json: true, clock: -> { decided_at }
+        ).call
+      end
+      payload = JSON.parse(out)
+
+      assert_equal true, payload.fetch("ok")
+      assert_equal true, payload.fetch("applied")
+      assert_equal true, payload.fetch("completed")
+      assert_equal "draft.md", payload.fetch("artifact")
+      assert_equal "3-approval", payload.fetch("current_stage")
+      assert_equal decided_at.iso8601, payload.fetch("decided_at")
+      assert_schema_valid("hive-decide", payload)
+
+      task = Hive::Task.new(approval)
+      assert_equal :complete, Hive::Markers.current(task.state_file).name
+      assert_equal "archived", Hive::TaskAction.for(
+        task, Hive::Markers.current(task.state_file)
+      ).key
+      assert_equal decided_at.iso8601, Hive::TaskMeta.read(approval).fetch(:completed_at)
+      status_out, = capture_io { Hive::Commands::Status.new(json: true).call }
+      status_row = JSON.parse(status_out).fetch("projects").flat_map { |project|
+        project.fetch("tasks")
+      }.find { |row| row.fetch("slug") == slug }
+      assert_equal "archived", status_row.fetch("action")
+      record = Hive::Commands::Decide.latest_record(task.state_file)
+      assert_equal "approve", record.fetch("outcome")
+      assert_equal "Ready for the editor", record.fetch("note")
+      assert_equal "draft.md", record.fetch("artifact")
+      assert_equal "publish_ready", record.fetch("artifact_status")
+      assert_equal "approval", record.fetch("from")
+      assert_equal decided_at.iso8601, record.fetch("decided_at")
+      assert File.file?(File.join(approval, "draft.md"))
+      refute Dir.exist?(File.join(dir, ".hive-state", "stages", "4-publish"))
+
+      retry_out, = capture_io do
+        Hive::Commands::Decide.new(
+          slug, "approve", from: "approval", decision_id: record.fetch("decision_id"),
+          note: "Ready for the editor", json: true
+        ).call
+      end
+      retry_payload = JSON.parse(retry_out)
+      assert_equal false, retry_payload.fetch("applied")
+      assert_equal true, retry_payload.fetch("noop")
+      assert_equal record.fetch("decision_id"), retry_payload.fetch("decision_id")
+
+      assert_raises(Hive::WrongStage) do
+        Hive::Commands::Decide.new(
+          slug, "reject", from: "approval", decision_id: record.fetch("decision_id")
+        ).call
+      end
+    end
+  end
+
+  def test_approve_requires_non_empty_artifact_without_mutation
+    with_editorial_task do |_dir, approval, slug|
+      draft = File.join(approval, "draft.md")
+      File.write(draft, "")
+      before = File.binread(File.join(approval, "approval.md"))
+
+      error = assert_raises(Hive::WrongStage) do
+        Hive::Commands::Decide.new(
+          slug, "approve", from: "approval", decision_id: decision_id_for(approval)
+        ).call
+      end
+
+      assert_includes error.message, "non-empty artifact"
+      assert_equal before, File.binread(File.join(approval, "approval.md"))
+      assert_equal :waiting, Hive::Markers.current(File.join(approval, "approval.md")).name
+      assert File.directory?(approval)
+    end
+  end
+
+  def test_approve_refuses_an_artifact_symlink_outside_the_task
+    with_editorial_task do |_dir, approval, slug|
+      with_tmp_dir do |outside|
+        external = File.join(outside, "external-draft.md")
+        File.write(external, "# External bytes\n")
+        draft = File.join(approval, "draft.md")
+        File.delete(draft)
+        File.symlink(external, draft)
+
+        error = assert_raises(Hive::WrongStage) do
+          Hive::Commands::Decide.new(
+            slug, "approve", from: "approval", decision_id: decision_id_for(approval)
+          ).call
+        end
+
+        assert_includes error.message, "non-empty artifact"
+        assert_equal "# External bytes\n", File.read(external)
+        assert File.symlink?(draft)
+        assert_equal :waiting, Hive::Markers.current(File.join(approval, "approval.md")).name
+        assert_nil Hive::Commands::Decide.latest_record(File.join(approval, "approval.md"))
+      end
+    end
+  end
+
+  def test_approve_refuses_an_artifact_swapped_while_opening
+    with_editorial_task do |_dir, approval, slug|
+      draft = File.join(approval, "draft.md")
+      task = Hive::Task.new(approval)
+      stage = task.workflow.stage_named("approval")
+      outcome = stage.outcomes.fetch("approve")
+      observed = File.lstat(draft)
+      swapped_stat = Struct.new(:dev, :ino) do
+        def file? = true
+      end.new(observed.dev, observed.ino + 1)
+      swapped_file = Object.new
+      swapped_file.define_singleton_method(:stat) { swapped_stat }
+      swapped_file.define_singleton_method(:read) { "# raced bytes\n" }
+      command = Hive::Commands::Decide.new(
+        slug, "approve", from: "approval", decision_id: decision_id_for(approval)
+      )
+
+      with_replaced_singleton_method(
+        File, :open, ->(_path, *_args, &block) { block.call(swapped_file) }
+      ) do
+        error = assert_raises(Hive::WrongStage) do
+          command.send(
+            :validate_publishable_artifact!, draft, approval, stage, outcome
+          )
+        end
+        assert_includes error.message, "requires non-empty artifact"
+      end
+    end
+  end
+
+  def test_decide_refuses_a_symlinked_human_state_file
+    with_editorial_task do |dir, approval, slug|
+      with_tmp_dir do |outside|
+        state = File.join(approval, "approval.md")
+        external = File.join(outside, "external-approval.md")
+        observed_id = decision_id_for(approval)
+        external_body = "private review\n<!-- WAITING decision_id=#{observed_id} -->\n"
+        File.write(external, external_body)
+        File.delete(state)
+        File.symlink(external, state)
+        head_before = run!("git", "-C", File.join(dir, ".hive-state"), "rev-parse", "HEAD")
+
+        error = assert_raises(Hive::InvalidTaskPath) do
+          Hive::Commands::Decide.new(
+            slug, "approve", from: "approval", decision_id: observed_id
+          ).send(:do_call)
+        end
+
+        assert_includes error.message, "must be a regular file, not a symlink"
+        assert_equal external_body, File.read(external)
+        assert File.symlink?(state)
+        assert_equal head_before,
+                     run!("git", "-C", File.join(dir, ".hive-state"), "rev-parse", "HEAD")
+      end
+    end
+  end
+
+  def test_concurrent_identical_completion_returns_one_apply_and_one_noop
+    with_editorial_task do |_dir, approval, slug|
+      decision_id = decision_id_for(approval)
+      ready = Queue.new
+      release = Queue.new
+      commands = 2.times.map do
+        command = Hive::Commands::Decide.new(
+          slug, "approve", from: "approval", decision_id: decision_id
+        )
+        original = command.method(:complete!)
+        command.define_singleton_method(:complete!) do |*args|
+          ready << true
+          release.pop
+          original.call(*args)
+        end
+        command
+      end
+      threads = commands.map do |command|
+        Thread.new do
+          begin
+            command.send(:do_call)
+          rescue StandardError => e
+            e
+          end
+        end
+      end
+      2.times { ready.pop }
+      2.times { release << true }
+      results = threads.map(&:value)
+
+      assert results.none?(Exception), results.inspect
+      assert_equal 1, results.count { |payload| payload.fetch("applied") }
+      assert_equal 1, results.count { |payload| payload.fetch("noop") }
+      assert_equal 1, File.read(File.join(approval, "approval.md")).scan(
+        Hive::Commands::Decide::RECORD_RE
+      ).size
+    end
+  end
+
+  def test_self_target_outcome_records_and_mints_a_fresh_decision
+    with_registered_workflow(revision_workflow) do
+      with_tmp_global_config do
+        with_tmp_git_repo do |dir|
+          capture_io { Hive::Commands::Init.new(dir, agent_skill_preflight: false).call }
+          project = File.basename(dir)
+          capture_io do
+            Hive::Commands::New.new(
+              project, "review this", workflow: "revision", slug_override: "revision-task"
+            ).call!
+          end
+          folder = File.join(dir, ".hive-state", "stages", "1-approval", "revision-task")
+          old_id = decision_id_for(folder)
+          out, = capture_io do
+            Hive::Commands::Decide.new(
+              "revision-task", "revise", from: "approval",
+              decision_id: old_id, json: true
+            ).call
+          end
+          payload = JSON.parse(out)
+          marker = Hive::Markers.current(File.join(folder, "approval.md"))
+
+          assert_equal true, payload.fetch("applied")
+          assert_equal false, payload.fetch("completed")
+          assert_equal "1-approval", payload.fetch("current_stage")
+          assert_equal "needs_input", payload.dig("next_action", "kind")
+          assert_equal :waiting, marker.name
+          refute_equal old_id, marker.attrs.fetch("decision_id")
+          assert_equal old_id, Hive::Commands::Decide.latest_record(
+            File.join(folder, "approval.md")
+          ).fetch("decision_id")
+          assert_schema_valid("hive-decide", payload)
+
+          retry_payload = Hive::Commands::Decide.new(
+            "revision-task", "revise", from: "approval",
+            decision_id: old_id
+          ).send(:do_call)
+          assert_equal false, retry_payload.fetch("applied")
+          assert_equal true, retry_payload.fetch("noop")
+        end
+      end
+    end
+  end
+
+  def test_self_target_outcome_restores_state_when_commit_is_interrupted
+    with_registered_workflow(revision_workflow) do
+      with_tmp_global_config do
+        with_tmp_git_repo do |dir|
+          capture_io { Hive::Commands::Init.new(dir, agent_skill_preflight: false).call }
+          project = File.basename(dir)
+          capture_io do
+            Hive::Commands::New.new(
+              project, "review this", workflow: "revision", slug_override: "revision-task"
+            ).call!
+          end
+          folder = File.join(dir, ".hive-state", "stages", "1-approval", "revision-task")
+          state = File.join(folder, "approval.md")
+          before = File.binread(state)
+          fake_ops = Object.new
+          fake_ops.define_singleton_method(:hive_commit) { |**| raise Interrupt, "stop" }
+          fake_ops.define_singleton_method(:run_git!) { |*| nil }
+
+          with_replaced_singleton_method(Hive::GitOps, :new, ->(_root) { fake_ops }) do
+            assert_raises(Interrupt) do
+              Hive::Commands::Decide.new(
+                "revision-task", "revise", from: "approval",
+                decision_id: decision_id_for(folder)
+              ).send(:do_call)
+            end
+          end
+
+          assert_equal before, File.binread(state)
+          assert_nil Hive::Commands::Decide.latest_record(state)
+          assert_equal :waiting, Hive::Markers.current(state).name
+        end
+      end
+    end
+  end
+
+  def test_invalid_waiting_identity_and_non_human_from_are_rejected
+    with_editorial_task do |_dir, approval, slug|
+      Hive::Markers.set(File.join(approval, "approval.md"), :waiting)
+      error = assert_raises(Hive::WrongStage) do
+        Hive::Commands::Decide.new(
+          slug, "approve", from: "approval", decision_id: "a" * 16
+        ).send(:do_call)
+      end
+      assert_includes error.message, "decision observation is stale"
+
+      error = assert_raises(Hive::InvalidTaskPath) do
+        Hive::Commands::Decide.new(
+          slug, "approve", from: "draft", decision_id: "a" * 16
+        ).send(:do_call)
+      end
+      assert_includes error.message, "not a human stage"
+    end
+  end
+
+  def test_decision_requires_the_human_stage_to_still_be_waiting
+    with_editorial_task do |_dir, approval, slug|
+      Hive::Markers.set(File.join(approval, "approval.md"), :complete)
+
+      error = assert_raises(Hive::WrongStage) do
+        Hive::Commands::Decide.new(
+          slug, "approve", from: "approval", decision_id: "a" * 16
+        ).send(:do_call)
+      end
+
+      assert_includes error.message, "is not awaiting a decision"
+    end
+  end
+
+  def test_decision_from_a_moved_task_without_a_record_is_stale
+    with_editorial_task do |dir, approval, slug|
+      draft = File.join(dir, ".hive-state", "stages", "2-draft", slug)
+      FileUtils.mkdir_p(File.dirname(draft))
+      File.rename(approval, draft)
+
+      error = assert_raises(Hive::WrongStage) do
+        Hive::Commands::Decide.new(
+          slug, "approve", from: "approval", decision_id: decision_id_for(draft)
+        ).send(:do_call)
+      end
+      assert_includes error.message, "--from expected 3-approval"
+    end
+  end
+
+  def test_approve_rolls_back_decision_file_when_commit_fails
+    with_editorial_task do |_dir, approval, slug|
+      state = File.join(approval, "approval.md")
+      before = File.binread(state)
+      fake_ops = Object.new
+      fake_ops.define_singleton_method(:hive_commit) { |**| raise Hive::GitError, "commit failed" }
+      fake_ops.define_singleton_method(:run_git!) { |*| raise Hive::GitError, "restage failed" }
+
+      with_replaced_singleton_method(Hive::GitOps, :new, ->(_root) { fake_ops }) do
+        error = assert_raises(Hive::GitError) do
+          Hive::Commands::Decide.new(
+            slug, "approve", from: "approval", decision_id: decision_id_for(approval)
+          ).send(:do_call)
+        end
+        assert_includes error.message, "commit failed"
+      end
+
+      assert_equal before, File.binread(state)
+      assert_nil Hive::Commands::Decide.latest_record(state)
+      assert_equal :waiting, Hive::Markers.current(state).name
+      refute Hive::TaskMeta.read(approval).key?(:completed_at)
+    end
+  end
+
+  def test_approve_rolls_back_decision_and_completion_time_when_commit_is_interrupted
+    with_editorial_task do |_dir, approval, slug|
+      state = File.join(approval, "approval.md")
+      state_before = File.binread(state)
+      meta_before = File.binread(File.join(approval, Hive::TaskMeta::FILENAME))
+      fake_ops = Object.new
+      fake_ops.define_singleton_method(:hive_commit) { |**| raise Interrupt, "stop" }
+      fake_ops.define_singleton_method(:run_git!) { |*| nil }
+
+      with_replaced_singleton_method(Hive::GitOps, :new, ->(_root) { fake_ops }) do
+        assert_raises(Interrupt) do
+          Hive::Commands::Decide.new(
+            slug, "approve", from: "approval", decision_id: decision_id_for(approval)
+          ).send(:do_call)
+        end
+      end
+
+      assert_equal state_before, File.binread(state)
+      assert_equal meta_before, File.binread(File.join(approval, Hive::TaskMeta::FILENAME))
+      assert_nil Hive::Commands::Decide.latest_record(state)
+      assert_equal :waiting, Hive::Markers.current(state).name
+      refute Hive::TaskMeta.read(approval).key?(:completed_at)
+    end
+  end
+
+  def test_reject_rolls_back_both_state_files_when_move_fails
+    with_editorial_task do |dir, approval, slug|
+      approval_before = File.binread(File.join(approval, "approval.md"))
+      draft_before = File.binread(File.join(approval, "draft.md"))
+      destination = File.join(dir, ".hive-state", "stages", "2-draft", slug)
+
+      replacement = lambda do |_target, **kwargs|
+        fake = Object.new
+        fake.define_singleton_method(:call) do
+          kwargs.fetch(:observation_guard).call(Hive::Task.new(approval))
+          FileUtils.mkdir_p(File.dirname(destination))
+          File.rename(approval, destination)
+          raise Hive::GitError, "move failed"
+        end
+        fake
+      end
+      with_replaced_singleton_method(Hive::Commands::Approve, :new, replacement) do
+        error = assert_raises(Hive::GitError) do
+          Hive::Commands::Decide.new(
+            slug, "reject", from: "approval", decision_id: decision_id_for(approval)
+          ).send(:do_call)
+        end
+        assert_includes error.message, "move failed"
+      end
+
+      assert_equal approval_before, File.binread(File.join(destination, "approval.md"))
+      assert_equal draft_before, File.binread(File.join(destination, "draft.md"))
+      assert_nil Hive::Commands::Decide.latest_record(File.join(destination, "approval.md"))
+    end
+  end
+
+  def test_reject_rolls_back_both_state_files_when_move_is_interrupted
+    with_editorial_task do |dir, approval, slug|
+      approval_before = File.binread(File.join(approval, "approval.md"))
+      draft_before = File.binread(File.join(approval, "draft.md"))
+      destination = File.join(dir, ".hive-state", "stages", "2-draft", slug)
+
+      replacement = lambda do |_target, **kwargs|
+        fake = Object.new
+        fake.define_singleton_method(:call) do
+          kwargs.fetch(:observation_guard).call(Hive::Task.new(approval))
+          FileUtils.mkdir_p(File.dirname(destination))
+          File.rename(approval, destination)
+          raise Interrupt, "stop"
+        end
+        fake
+      end
+      with_replaced_singleton_method(Hive::Commands::Approve, :new, replacement) do
+        assert_raises(Interrupt) do
+          Hive::Commands::Decide.new(
+            slug, "reject", from: "approval", decision_id: decision_id_for(approval)
+          ).send(:do_call)
+        end
+      end
+
+      assert_equal approval_before, File.binread(File.join(destination, "approval.md"))
+      assert_equal draft_before, File.binread(File.join(destination, "draft.md"))
+      assert_nil Hive::Commands::Decide.latest_record(File.join(destination, "approval.md"))
+    end
+  end
+
+  def test_reject_wrong_stage_after_local_mutation_restores_and_reraises
+    with_editorial_task do |_dir, approval, slug|
+      state = File.join(approval, "approval.md")
+      draft = File.join(approval, "draft.md")
+      state_before = File.binread(state)
+      draft_before = File.binread(draft)
+
+      replacement = lambda do |_target, **kwargs|
+        fake = Object.new
+        fake.define_singleton_method(:call) do
+          kwargs.fetch(:observation_guard).call(Hive::Task.new(approval))
+          raise Hive::WrongStage, "another transition won"
+        end
+        fake
+      end
+
+      with_replaced_singleton_method(Hive::Commands::Approve, :new, replacement) do
+        error = assert_raises(Hive::WrongStage) do
+          Hive::Commands::Decide.new(
+            slug, "reject", from: "approval", decision_id: decision_id_for(approval)
+          ).send(:do_call)
+        end
+        assert_includes error.message, "another transition won"
+      end
+
+      assert_equal state_before, File.binread(state)
+      assert_equal draft_before, File.binread(draft)
+      assert_nil Hive::Commands::Decide.latest_record(state)
+    end
+  end
+
+  def test_reject_wrong_stage_after_identical_concurrent_move_returns_noop
+    with_editorial_task do |dir, approval, slug|
+      command = Hive::Commands::Decide.new(
+        slug, "reject", from: "approval", decision_id: decision_id_for(approval)
+      )
+      task = Hive::Task.new(approval)
+      stage = task.workflow.stage_named("approval")
+      outcome = stage.outcomes.fetch("reject")
+      record = command.send(
+        :decision_record, task, stage, outcome, decision_id_for(approval)
+      )
+      destination = File.join(dir, ".hive-state", "stages", "2-draft", slug)
+
+      replacement = lambda do |_target, **_kwargs|
+        fake = Object.new
+        fake.define_singleton_method(:call) do
+          FileUtils.mkdir_p(File.dirname(destination))
+          File.rename(approval, destination)
+          command.send(
+            :write_decision_record, File.join(destination, "approval.md"), record
+          )
+          Hive::Markers.set(File.join(destination, "draft.md"), :waiting)
+          raise Hive::WrongStage, "another transition won"
+        end
+        fake
+      end
+
+      payload = with_replaced_singleton_method(
+        Hive::Commands::Approve, :new, replacement
+      ) { command.send(:do_call) }
+
+      assert_equal true, payload.fetch("noop")
+      assert_equal false, payload.fetch("applied")
+      assert_equal "2-draft", payload.fetch("current_stage")
+      assert_equal "reject", Hive::Commands::Decide.latest_record(
+        File.join(destination, "approval.md")
+      ).fetch("outcome")
+    end
+  end
+
+  def test_stale_decision_identity_is_rejected_under_lock
+    with_editorial_task do |_dir, approval, _slug|
+      task = Hive::Task.new(approval)
+      stage = task.workflow.stage_named("approval")
+
+      error = assert_raises(Hive::WrongStage) do
+        Hive::Commands::Decide.new("unused", "approve", from: "approval")
+                              .send(:validate_current_decision!, task, stage, "rotated")
+      end
+      assert_includes error.message, "decision observation is stale"
+    end
+  end
+
+  def test_plain_success_reports_decision_and_current_stage
+    with_editorial_task do |_dir, approval, slug|
+      out, = capture_io do
+        Hive::Commands::Decide.new(
+          slug, "approve", from: "approval", decision_id: decision_id_for(approval)
+        ).call
+      end
+
+      assert_includes out, "hive: decided #{slug} approval=approve"
+      assert_includes out, "current_stage: 3-approval"
+    end
+  end
+
+  def test_reject_records_decision_returns_to_draft_and_retries_as_noop
+    with_editorial_task do |dir, approval, slug|
+      observed_id = decision_id_for(approval)
+      out, = capture_io do
+        Hive::CLI.start(
+          [
+            "decide", slug, "reject", "--from", "3-approval",
+            "--decision-id", observed_id, "--note", "Strengthen the lead", "--json"
+          ]
+        )
+      end
+      payload = JSON.parse(out)
+      draft = File.join(dir, ".hive-state", "stages", "2-draft", slug)
+
+      assert_equal true, payload.fetch("applied")
+      assert_equal false, payload.fetch("completed")
+      assert_equal "2-draft", payload.fetch("current_stage")
+      assert File.directory?(draft)
+      refute File.exist?(approval)
+      assert_equal :waiting, Hive::Markers.current(File.join(draft, "draft.md")).name
+      record = Hive::Commands::Decide.latest_record(File.join(draft, "approval.md"))
+      assert_equal "reject", record.fetch("outcome")
+      assert_equal "Strengthen the lead", record.fetch("note")
+      assert_equal "draft", record.fetch("to")
+
+      retry_out, = capture_io do
+        Hive::Commands::Decide.new(
+          slug, "reject", from: "approval", decision_id: observed_id,
+          note: "Strengthen the lead", json: true
+        ).call
+      end
+      retry_payload = JSON.parse(retry_out)
+      assert_equal true, retry_payload.fetch("noop")
+      assert_equal false, retry_payload.fetch("applied")
+      assert_equal "2-draft", retry_payload.fetch("current_stage")
+
+      assert_raises(Hive::WrongStage) do
+        Hive::Commands::Decide.new(
+          slug, "approve", from: "approval", decision_id: observed_id
+        ).call
+      end
+    end
+  end
+
+  def test_marker_only_artifact_is_not_publish_ready
+    with_editorial_task do |_dir, approval, slug|
+      draft = File.join(approval, "draft.md")
+      File.write(draft, "<!-- COMPLETE -->\n<!-- WAITING decision_id=aaaaaaaaaaaaaaaa -->\n")
+
+      error = assert_raises(Hive::WrongStage) do
+        Hive::Commands::Decide.new(
+          slug, "approve", from: "approval", decision_id: decision_id_for(approval)
+        ).call
+      end
+
+      assert_includes error.message, "non-empty artifact"
+      assert_equal :waiting, Hive::Markers.current(File.join(approval, "approval.md")).name
+    end
+  end
+
+  def test_artifact_is_rechecked_under_the_decision_lock
+    with_editorial_task do |_dir, approval, slug|
+      draft = File.join(approval, "draft.md")
+      command = Hive::Commands::Decide.new(
+        slug, "approve", from: "approval", decision_id: decision_id_for(approval)
+      )
+      original = command.method(:validate_current_decision!)
+      command.define_singleton_method(:validate_current_decision!) do |task, stage, decision_id|
+        original.call(task, stage, decision_id)
+        File.write(draft, "")
+      end
+
+      assert_raises(Hive::WrongStage) { command.call }
+      assert_equal :waiting, Hive::Markers.current(File.join(approval, "approval.md")).name
+      assert_nil Hive::Commands::Decide.latest_record(File.join(approval, "approval.md"))
+    end
+  end
+
+  def test_decision_from_an_earlier_visit_is_rejected_after_reentry
+    with_editorial_task do |_dir, approval, slug|
+      old_id = decision_id_for(approval)
+      Hive::Markers.set(
+        File.join(approval, "approval.md"), :waiting, "decision_id" => "b" * 16
+      )
+
+      error = assert_raises(Hive::WrongStage) do
+        Hive::Commands::Decide.new(
+          slug, "approve", from: "approval", decision_id: old_id
+        ).call
+      end
+
+      assert_includes error.message, "decision observation is stale"
+      assert_equal "b" * 16, decision_id_for(approval)
+    end
+  end
+
+  def test_completed_human_stage_run_reports_completion_without_outcomes
+    with_editorial_task do |_dir, approval, slug|
+      capture_io do
+        Hive::Commands::Decide.new(
+          slug, "approve", from: "approval", decision_id: decision_id_for(approval)
+        ).call
+      end
+
+      out, = capture_io { Hive::Commands::Run.new(slug, json: true).call }
+      payload = JSON.parse(out)
+      assert_equal "complete", payload.fetch("marker")
+      assert_equal "human_stage_complete", payload.dig("next_action", "reason")
+      refute payload.key?("allowed_outcomes")
+      refute payload.dig("next_action").key?("allowed_outcomes")
+      refute payload.dig("next_action").key?("decide_with")
+    end
+  end
+
+  def test_completed_human_stage_plain_run_reports_recorded_outcome
+    with_editorial_task do |_dir, approval, slug|
+      capture_io do
+        Hive::Commands::Decide.new(
+          slug, "approve", from: "approval", decision_id: decision_id_for(approval)
+        ).call
+      end
+
+      out, = capture_io { Hive::Commands::Run.new(slug).call }
+
+      assert_includes out, "complete: human workflow outcome recorded"
+      refute_includes out, "outcomes:"
+    end
+  end
+
+  def test_status_and_operational_status_expose_human_outcomes
+    with_editorial_task do |_dir, _approval, slug|
+      status_out, = capture_io { Hive::Commands::Status.new(json: true).call }
+      row = JSON.parse(status_out).fetch("projects").flat_map { |project| project.fetch("tasks") }
+                     .find { |task| task.fetch("slug") == slug }
+      assert_equal "needs_input", row.fetch("action")
+      assert_nil row.fetch("suggested_command")
+      assert_equal %w[approve reject], row.fetch("outcomes").map { |entry| entry.fetch("name") }
+      assert_schema_valid("hive-status", JSON.parse(status_out))
+
+      operational_out, = capture_io { Hive::Commands::Status.new(json: true, operational: true).call }
+      operational = JSON.parse(operational_out)
+      projected = operational.fetch("tasks").find { |task| task.dig("identity", "slug") == slug }
+      assert_equal "waiting_on_you", projected.fetch("state")
+      assert_equal %w[approve reject], projected.dig("position", "allowed_outcomes").map { |entry| entry.fetch("name") }
+      assert_nil projected.fetch("action")
+      assert_schema_valid("hive-operational-status", operational)
+    end
+  end
+
+  private
+
+  def decision_id_for(folder)
+    Hive::Markers.current(File.join(folder, "approval.md")).attrs.fetch("decision_id")
+  end
+
+  def editorial_workflow
+    Hive::Workflow.new(
+      id: :editorial,
+      stages: [
+        Hive::Workflow::Stage.new(name: "research", index: 1, state_file: "research.md", kind: :agent, skill: "/research"),
+        Hive::Workflow::Stage.new(name: "draft", index: 2, state_file: "draft.md", kind: :agent, skill: "/draft"),
+        Hive::Workflow::Stage.new(
+          name: "approval", index: 3, state_file: "approval.md", kind: :human, input: "draft.md",
+          outcomes: {
+            "approve" => Hive::Workflow::Outcome.new(name: "approve", complete: true, artifact: "draft.md"),
+            "reject" => Hive::Workflow::Outcome.new(name: "reject", to: "draft")
+          }.freeze
+        )
+      ]
+    )
+  end
+
+  def revision_workflow
+    Hive::Workflow.new(
+      id: :revision,
+      stages: [
+        Hive::Workflow::Stage.new(
+          name: "approval", index: 1, state_file: "approval.md", kind: :human,
+          outcomes: {
+            "revise" => Hive::Workflow::Outcome.new(name: "revise", to: "approval")
+          }.freeze
+        )
+      ]
+    )
+  end
+
+  def with_editorial_task
+    descriptor = editorial_workflow
+    with_registered_workflow(descriptor) do
+      with_tmp_global_config do
+        with_tmp_git_repo do |dir|
+          capture_io { Hive::Commands::Init.new(dir, agent_skill_preflight: false).call }
+          project = File.basename(dir)
+          capture_io { Hive::Commands::New.new(project, "editorial probe", workflow: "editorial").call }
+          research = Dir[File.join(dir, ".hive-state", "stages", "1-research", SLUG_PATTERN)].fetch(0)
+          slug = File.basename(research)
+          Hive::Markers.set(File.join(research, "research.md"), :complete)
+          capture_io { Hive::Commands::Approve.new(slug, from: "research").call }
+          draft = File.join(dir, ".hive-state", "stages", "2-draft", slug)
+          File.write(File.join(draft, "draft.md"), "# Draft\n\nPublishable copy.\n")
+          Hive::Markers.set(File.join(draft, "draft.md"), :complete)
+          capture_io { Hive::Commands::Approve.new(slug, from: "draft").call }
+          approval = File.join(dir, ".hive-state", "stages", "3-approval", slug)
+
+          yield dir, approval, slug
+        end
+      end
+    end
+  end
+
+  def assert_schema_valid(name, payload)
+    schemer = JSONSchemer.schema(JSON.parse(File.read(Hive::Schemas.schema_path(name))))
+    errors = schemer.validate(payload).to_a
+    assert_empty errors, "#{name} payload errors: #{errors.map { |error| error['error'] }.inspect}"
+  end
+end
diff --git a/test/integration/init_minimal_test.rb b/test/integration/init_minimal_test.rb
new file mode 100644
index 0000000000..0c1a717e8c
--- /dev/null
+++ b/test/integration/init_minimal_test.rb
@@ -0,0 +1,455 @@
+require "test_helper"
+require "json"
+require "json_schemer"
+require "hive/commands/init"
+require "hive/commands/workflow"
+
+class InitMinimalTest < Minitest::Test
+  include HiveTestHelper
+
+  def setup
+    super
+    Hive::Workflows::Project.reset!
+  end
+
+  def teardown
+    Hive::Workflows::Project.reset!
+    super
+  end
+
+  def test_preview_is_machine_readable_and_leaves_every_target_unchanged
+    with_tmp_global_config do
+      with_tmp_git_repo do |project_root|
+        before = repository_snapshot(project_root)
+        global_path = Hive::Config.global_config_path
+        global_before = File.exist?(global_path) ? File.binread(global_path) : nil
+
+        out, err = capture_io do
+          Hive::Commands::Init.new(
+            project_root, new_workflow: "editorial", minimal: true, preview: true,
+            json: true, agent_skill_preflight: false
+          ).call
+        end
+        payload = JSON.parse(out)
+
+        assert_empty err
+        assert_equal before, repository_snapshot(project_root)
+        refute File.exist?(File.join(project_root, ".hive-state"))
+        assert_equal global_before, File.exist?(global_path) ? File.binread(global_path) : nil
+        assert_equal true, payload.fetch("preview")
+        assert_equal true, payload.fetch("minimal")
+        assert_equal "editorial", payload.fetch("workflow")
+        assert_equal false, payload.dig("services", "daemon_install")
+        assert_equal false, payload.dig("background_automation", "patrol")
+        assert_equal true, payload.dig("context_integration", "llm_wiki")
+        assert_equal File.basename(project_root), payload.dig("global_registration", "name")
+        assert_equal "available", payload.dig("global_registration", "status")
+        schemer = JSONSchemer.schema(JSON.parse(File.read(Hive::Schemas.schema_path("hive-init-preview"))))
+        assert_empty schemer.validate(payload).to_a
+      end
+    end
+  end
+
+  def test_execute_uses_same_minimal_profile_and_scaffolds_loadable_workflow
+    with_tmp_global_config do |global_root|
+      with_tmp_git_repo do |project_root|
+        out, err = capture_io do
+          Hive::Commands::Init.new(
+            project_root, new_workflow: "editorial", minimal: true,
+            json: true, agent_skill_preflight: false
+          ).call
+        end
+        payload = JSON.parse(out)
+
+        assert_empty err
+        assert_equal true, payload.fetch("minimal")
+        init_schemer = JSONSchemer.schema(JSON.parse(File.read(Hive::Schemas.schema_path("hive-init"))))
+        assert_empty init_schemer.validate(payload).to_a
+        config = Hive::Config.load(project_root)
+        assert_equal "editorial", config.fetch("default_workflow")
+        assert_equal "off", config.dig("patrol", "mode")
+        assert_equal false, config.dig("refactor_patrol", "enabled")
+        assert_equal false, config.dig("review", "adhoc", "fix")
+        assert_equal false, config.dig("daemon", "enabled")
+        assert_equal false, config.dig("babysitter", "enabled")
+        global_config = YAML.safe_load(File.read(File.join(global_root, "config.yml")))
+        refute global_config.key?("daemon"), "minimal init must not install or persist daemon autostart"
+        refute File.exist?(File.join(global_root, ".config", "systemd", "user"))
+
+        validation = Hive::Commands::Workflow.new(
+          "validate", "editorial", project_root: project_root, json: false, stdout: StringIO.new
+        ).call!
+        assert_equal true, validation.fetch("valid")
+      end
+    end
+  end
+
+  def test_execute_does_not_install_the_llm_wiki_scheduler
+    with_tmp_global_config do
+      with_tmp_git_repo do |project_root|
+        installs = []
+        with_replaced_singleton_method(
+          Hive::LlmWikiBootstrap::Scheduler, :install, ->(root) { installs << root }
+        ) do
+          capture_io do
+            Hive::Commands::Init.new(
+              project_root, new_workflow: "editorial", minimal: true,
+              agent_skill_preflight: false
+            ).call
+          end
+        end
+
+        assert_empty installs
+      end
+    end
+  end
+
+  def test_interrupted_scaffold_commit_rolls_back_minimal_initialization
+    with_tmp_global_config do
+      with_tmp_git_repo do |project_root|
+        before = repository_snapshot(project_root)
+        replacement = ->(*_args, **_kwargs) { raise Interrupt, "stop" }
+
+        with_replaced_singleton_method(
+          Hive::Commands::Workflow, :commit_workflow_scaffold, replacement
+        ) do
+          assert_raises(Interrupt) do
+            Hive::Commands::Init.new(
+              project_root, new_workflow: "editorial", minimal: true,
+              agent_skill_preflight: false
+            ).call
+          end
+        end
+
+        assert_equal before, repository_snapshot(project_root)
+        refute File.exist?(File.join(project_root, ".hive-state"))
+        refute Hive::Config.registered_projects.any? { |project|
+          File.expand_path(project.fetch("path")) == project_root
+        }
+      end
+    end
+  end
+
+  def test_execute_refuses_managed_project_symlinks_without_writing_through_them
+    with_tmp_global_config do
+      with_tmp_git_repo do |project_root|
+        outside = File.join(File.dirname(project_root), "outside-agents.md")
+        File.write(outside, "outside choice\n")
+        managed = File.join(project_root, "AGENTS.md")
+        File.symlink(outside, managed)
+        run!("git", "-C", project_root, "add", "AGENTS.md")
+        run!("git", "-C", project_root, "commit", "-m", "track project choice", "--quiet")
+
+        error = assert_raises(Hive::ConfigError) do
+          capture_io do
+            Hive::Commands::Init.new(
+              project_root, new_workflow: "editorial", minimal: true,
+              agent_skill_preflight: false
+            ).call
+          end
+        end
+
+        assert_includes error.message, "must not be a symlink"
+        assert_equal "outside choice\n", File.read(outside)
+        assert File.symlink?(managed)
+        refute File.exist?(File.join(project_root, ".hive-state"))
+      end
+    end
+  end
+
+  def test_minimal_refuses_a_dangling_hive_state_symlink
+    with_tmp_global_config do
+      with_tmp_git_repo do |project_root|
+        hive_state = File.join(project_root, ".hive-state")
+        outside = File.join(File.dirname(project_root), "missing-hive-state")
+        File.symlink(outside, hive_state)
+
+        error = assert_raises(Hive::AlreadyInitialized) do
+          Hive::Commands::Init.new(
+            project_root, new_workflow: "editorial", minimal: true, preview: true,
+            agent_skill_preflight: false
+          ).call
+        end
+
+        assert_includes error.message, "fresh target"
+        assert File.symlink?(hive_state)
+        refute File.exist?(outside)
+      end
+    end
+  end
+
+  def test_minimal_rejects_force_missing_workflow_and_initialized_target
+    with_tmp_global_config do
+      with_tmp_git_repo do |project_root|
+        assert_raises(Hive::Commands::Workflow::UsageError) do
+          Hive::Commands::Init.new(project_root, minimal: true, force: true, new_workflow: "editorial").call
+        end
+        assert_raises(Hive::Commands::Workflow::UsageError) do
+          Hive::Commands::Init.new(project_root, minimal: true).call
+        end
+
+        capture_io do
+          Hive::Commands::Init.new(
+            project_root, minimal: true, new_workflow: "editorial", agent_skill_preflight: false
+          ).call
+        end
+        out, = capture_io do
+          assert_raises(Hive::AlreadyInitialized) do
+            Hive::Commands::Init.new(
+              project_root, minimal: true, new_workflow: "another",
+              json: true, agent_skill_preflight: false
+            ).call
+          end
+        end
+        payload = JSON.parse(out)
+        assert_equal "already_initialized", payload.fetch("error_kind")
+        assert_schema_valid("hive-init", payload)
+      end
+    end
+  end
+
+  def test_minimal_rejects_invalid_flag_combinations_and_collisions
+    assert_raises(ArgumentError) do
+      Hive::Commands::Init.new(".", minimal: :yes)
+    end
+    assert_raises(Hive::Commands::Workflow::UsageError) do
+      Hive::Commands::Init.new(".", preview: true).send(:validate_minimal_arguments!)
+    end
+    assert_raises(Hive::Commands::Workflow::UsageError) do
+      Hive::Commands::Init.new(
+        ".", minimal: true, new_workflow: "editorial", refactor_patrol: true
+      ).send(:validate_minimal_arguments!)
+    end
+
+    with_tmp_global_config do
+      with_tmp_git_repo do |project_root|
+        collision = ->(_id, project_root:) { project_root == File.expand_path(project_root) }
+        suggestion = ->(_id, project_root:) { "editorial-2" }
+        with_replaced_singleton_method(Hive::Commands::Workflow, :scaffold_collision?, collision) do
+          with_replaced_singleton_method(Hive::Commands::Workflow, :available_id, suggestion) do
+            error = assert_raises(Hive::Commands::Workflow::UsageError) do
+              Hive::Commands::Init.new(
+                project_root, minimal: true, new_workflow: "editorial",
+                agent_skill_preflight: false
+              ).call
+            end
+            assert_equal "editorial-2", error.suggested_id
+          end
+        end
+      end
+    end
+  end
+
+  def test_minimal_refuses_to_replace_an_unrelated_same_name_registration
+    with_tmp_global_config do
+      with_tmp_dir do |root|
+        first = File.join(root, "first", "shared")
+        second = File.join(root, "second", "shared")
+        initialize_git_repo(first)
+        initialize_git_repo(second)
+        Hive::Config.register_project(name: "shared", path: first)
+
+        out, = capture_io do
+          assert_raises(Hive::Commands::Init::ProjectRegistrationCollision) do
+            Hive::Commands::Init.new(
+              second, minimal: true, preview: true, new_workflow: "editorial",
+              json: true, agent_skill_preflight: false
+            ).call
+          end
+        end
+        payload = JSON.parse(out)
+
+        assert_equal false, payload.fetch("ok")
+        assert_equal "project_name_collision", payload.fetch("error_kind")
+        assert_equal first, payload.fetch("existing_path")
+        assert_equal first, Hive::Config.find_project("shared").fetch("path")
+        schemer = JSONSchemer.schema(
+          JSON.parse(File.read(Hive::Schemas.schema_path("hive-init-preview")))
+        )
+        assert_empty schemer.validate(payload).to_a
+      end
+    end
+  end
+
+  def test_minimal_rechecks_registration_inside_the_locked_write
+    with_tmp_global_config do
+      with_tmp_dir do |root|
+        existing = File.join(root, "existing", "shared")
+        target = File.join(root, "target", "shared")
+        initialize_git_repo(existing)
+        initialize_git_repo(target)
+
+        command = Hive::Commands::Init.new(
+          target, minimal: true, new_workflow: "editorial",
+          json: true, agent_skill_preflight: false
+        )
+        original = command.method(:ensure_minimal_registration_available!)
+        command.define_singleton_method(:ensure_minimal_registration_available!) do
+          original.call
+          Hive::Config.register_project(name: "shared", path: existing)
+        end
+
+        out, = capture_io do
+          assert_raises(Hive::Commands::Init::ProjectRegistrationCollision) { command.call }
+        end
+        payload = JSON.parse(out)
+
+        assert_equal "project_name_collision", payload.fetch("error_kind")
+        assert_equal existing, payload.fetch("existing_path")
+        assert_equal existing, Hive::Config.find_project("shared").fetch("path")
+        refute File.exist?(File.join(target, ".hive-state"))
+        assert_schema_valid("hive-init", payload)
+      end
+    end
+  end
+
+  def test_minimal_json_failures_use_typed_command_envelopes
+    with_tmp_global_config do
+      with_tmp_git_repo do |project_root|
+        File.write(File.join(project_root, "README.md"), "dirty\n")
+        out, = capture_io do
+          assert_raises(Hive::Commands::Init::PreconditionError) do
+            Hive::Commands::Init.new(
+              project_root, minimal: true, new_workflow: "editorial",
+              json: true, agent_skill_preflight: false
+            ).call
+          end
+        end
+        payload = JSON.parse(out)
+        assert_equal "hive-init", payload.fetch("schema")
+        assert_equal "dirty_worktree", payload.fetch("error_kind")
+        assert_equal false, payload.fetch("preview")
+        assert_schema_valid("hive-init", payload)
+      end
+
+      with_tmp_git_repo do |project_root|
+        out, = capture_io do
+          assert_raises(Hive::Commands::Workflow::UsageError) do
+            Hive::Commands::Init.new(
+              project_root, minimal: true, preview: true, new_workflow: "Bad_ID",
+              json: true, agent_skill_preflight: false
+            ).call
+          end
+        end
+        payload = JSON.parse(out)
+        assert_equal "hive-init-preview", payload.fetch("schema")
+        assert_equal "usage", payload.fetch("error_kind")
+        assert_equal "Bad_ID", payload.fetch("value")
+        assert_schema_valid("hive-init-preview", payload)
+      end
+    end
+  end
+
+  def test_minimal_error_envelope_classifies_shared_failures_and_suggestions
+    command = Hive::Commands::Init.new(
+      ".", minimal: true, new_workflow: "editorial", json: true
+    )
+    {
+      Hive::ConcurrentRunError.new("busy") => "concurrent_run",
+      Hive::ConfigError.new("config") => "config",
+      Hive::GitError.new("git") => "git",
+      Hive::InternalError.new("internal") => "internal",
+      RuntimeError.new("other") => "error"
+    }.each do |error, kind|
+      assert_equal kind, command.send(:envelope_error_kind, error)
+    end
+
+    usage = Hive::Commands::Workflow::UsageError.new(
+      "workflow exists", value: "editorial", suggested_id: "editorial-2"
+    )
+    extras = command.send(:envelope_extras_for, usage)
+    assert_equal "editorial", extras.fetch("value")
+    assert_equal "editorial-2", extras.fetch("suggested_id")
+  end
+
+  def test_minimal_git_preconditions_remain_typed_for_automation
+    with_tmp_dir do |project_root|
+      command = Hive::Commands::Init.new(
+        project_root, minimal: true, new_workflow: "editorial", json: true
+      )
+      failure = Object.new
+      failure.define_singleton_method(:success?) { false }
+
+      with_replaced_singleton_method(
+        Open3, :capture3, ->(*_args) { [ "", "not a repository", failure ] }
+      ) do
+        error = assert_raises(Hive::Commands::Init::PreconditionError) do
+          command.send(:validate_git_repo!)
+        end
+        assert_equal "not_git_repository", error.error_kind
+      end
+
+      success = Object.new
+      success.define_singleton_method(:success?) { true }
+      with_replaced_singleton_method(
+        Open3, :capture3, ->(*_args) { [ "../shared.git\n", "", success ] }
+      ) do
+        error = assert_raises(Hive::Commands::Init::PreconditionError) do
+          command.send(:validate_git_repo!)
+        end
+        assert_equal "main_checkout_required", error.error_kind
+      end
+    end
+  end
+
+  def test_plain_preview_reports_plan_and_broken_pipe_is_harmless
+    with_tmp_global_config do
+      with_tmp_git_repo do |project_root|
+        out, = capture_io do
+          Hive::Commands::Init.new(
+            project_root, new_workflow: "editorial", minimal: true, preview: true,
+            agent_skill_preflight: false
+          ).call
+        end
+        assert_includes out, "minimal init preview"
+        assert_includes out, "workflow: editorial"
+        assert_includes out, "automation: disabled"
+
+        command = Hive::Commands::Init.new(
+          project_root, new_workflow: "editorial", minimal: true, preview: true,
+          agent_skill_preflight: false
+        )
+        command.define_singleton_method(:minimal_preview_payload) do |_ops, _id|
+          { "project_files" => [], "workflow" => "editorial" }
+        end
+        broken = Object.new
+        broken.define_singleton_method(:write) { |_body| raise Errno::EPIPE }
+        original = $stdout
+        begin
+          $stdout = broken
+          assert_nil command.send(
+            :emit_minimal_preview, Hive::GitOps.new(project_root), "editorial"
+          )
+        ensure
+          $stdout = original
+        end
+      end
+    end
+  end
+
+  private
+
+  def initialize_git_repo(path)
+    FileUtils.mkdir_p(path)
+    run!("git", "-C", path, "init", "-b", "master", "--quiet")
+    run!("git", "-C", path, "config", "user.email", "test@example.com")
+    run!("git", "-C", path, "config", "user.name", "Test")
+    File.write(File.join(path, "README.md"), "test\n")
+    run!("git", "-C", path, "add", ".")
+    run!("git", "-C", path, "commit", "-m", "initial", "--quiet")
+  end
+
+  def assert_schema_valid(name, payload)
+    schemer = JSONSchemer.schema(JSON.parse(File.read(Hive::Schemas.schema_path(name))))
+    errors = schemer.validate(payload).to_a
+    assert_empty errors, "#{name} payload errors: #{errors.map { |error| error['error'] }.inspect}"
+  end
+
+  def repository_snapshot(project_root)
+    {
+      head: run!("git", "-C", project_root, "rev-parse", "HEAD").strip,
+      status: run!("git", "-C", project_root, "status", "--porcelain"),
+      branches: run!("git", "-C", project_root, "branch", "--format=%(refname:short)")
+    }
+  end
+end
diff --git a/test/integration/new_idempotency_test.rb b/test/integration/new_idempotency_test.rb
new file mode 100644
index 0000000000..784aa8586a
--- /dev/null
+++ b/test/integration/new_idempotency_test.rb
@@ -0,0 +1,550 @@
+require "test_helper"
+require "json"
+require "json_schemer"
+require "hive/commands/approve"
+require "hive/commands/init"
+require "hive/commands/new"
+require "hive/commands/workflow"
+require "hive/task_meta"
+
+class NewIdempotencyTest < Minitest::Test
+  include HiveTestHelper
+
+  def test_error_kind_maps_config_concurrency_and_internal_failures
+    command = Hive::Commands::New.new("project", "task", idempotency_key: "key", json: true)
+    cases = [
+      [ Hive::ConfigError.new("config"), "config" ],
+      [ Hive::Commands::New::ProjectConfigUnreadable.new("config"), "config" ],
+      [ Hive::Commands::New::UnregisteredProjectWorkflow.new("config"), "config" ],
+      [ Hive::ConcurrentRunError.new("busy"), "concurrent_run" ],
+      [ Hive::InternalError.new("internal"), "internal" ],
+      [ RuntimeError.new("other"), "error" ]
+    ]
+
+    cases.each do |error, kind|
+      assert_equal kind, command.envelope_error_kind(error)
+    end
+  end
+
+  def test_retry_returns_original_task_after_it_moves
+    with_initialized_project do |project_root, project|
+      first = create_json(
+        project, "draft launch post", key: "workflow-creator:editorial:v1", slug: "editorial-task"
+      )
+      assert_equal true, first.fetch("created")
+      folder = File.join(project_root, ".hive-state", "stages", "1-inbox", "editorial-task")
+      Hive::Commands::Approve.new(
+        folder, to: "2-brainstorm", from: "1-inbox", force: true, quiet: true
+      ).call
+
+      retry_payload = create_json(
+        project, "draft launch post", key: "workflow-creator:editorial:v1", slug: "editorial-task"
+      )
+
+      assert_equal false, retry_payload.fetch("created")
+      assert_equal "editorial-task", retry_payload.fetch("slug")
+      assert_equal "2-brainstorm", retry_payload.fetch("current_stage")
+      assert_equal 1, idempotent_tasks(project_root).size
+      schemer = JSONSchemer.schema(JSON.parse(File.read(Hive::Schemas.schema_path("hive-new"))))
+      assert_empty schemer.validate(retry_payload).to_a
+    end
+  end
+
+  def test_reusing_key_for_different_input_is_rejected_without_a_second_task
+    with_initialized_project do |project_root, project|
+      create_json(project, "first request", key: "creator:stable", slug: "first-task")
+
+      error = assert_raises(Hive::Commands::New::IdempotencyConflict) do
+        Hive::Commands::New.new(
+          project, "different request", slug_override: "second-task",
+          idempotency_key: "creator:stable", json: true
+        ).call!
+      end
+
+      assert_includes error.message, "different input or workflow"
+      assert_equal 1, idempotent_tasks(project_root).size
+
+      out, err, status = with_captured_exit do
+        Hive::Commands::New.new(
+          project, "different request", slug_override: "second-task",
+          idempotency_key: "creator:stable", json: true
+        ).call
+      end
+      payload = JSON.parse(out)
+      assert_equal Hive::ExitCodes::USAGE, status
+      assert_empty err
+      assert_equal "usage", payload.fetch("error_kind")
+      schemer = JSONSchemer.schema(JSON.parse(File.read(Hive::Schemas.schema_path("hive-new"))))
+      assert_empty schemer.validate(payload).to_a
+      assert_equal 1, idempotent_tasks(project_root).size
+    end
+  end
+
+  def test_invalid_idempotency_keys_are_rejected
+    with_initialized_project do |_project_root, project|
+      [ "", " ", "x" * 513 ].each do |key|
+        error = assert_raises(Hive::Commands::New::IdempotencyConflict) do
+          Hive::Commands::New.new(
+            project, "task", slug_override: "invalid-key-task",
+            idempotency_key: key, json: true
+          ).call!
+        end
+        assert_includes error.message, "idempotency key must be"
+      end
+    end
+  end
+
+  def test_attachment_content_participates_in_the_fingerprint
+    with_initialized_project do |project_root, project|
+      with_tmp_dir do |dir|
+        source = File.join(dir, "source.png")
+        File.binwrite(source, "png-bytes")
+
+        payload = create_json_with(
+          project, "task with image", key: "creator:attachment", slug: "attachment-task",
+          attachments: [ [ source, "source.png" ] ]
+        )
+        metadata = Hive::TaskMeta.read(payload.fetch("task_folder"))
+
+        assert_equal true, payload.fetch("created")
+        assert_match(/\A[0-9a-f]{64}\z/, metadata.fetch(:input_fingerprint))
+        assert_equal "png-bytes",
+                     File.binread(File.join(project_root, ".hive-state", "stages", "1-inbox",
+                                            "attachment-task", "assets", "source.png"))
+      end
+    end
+  end
+
+  def test_idempotent_attachment_fingerprint_and_task_use_one_byte_snapshot
+    with_initialized_project do |project_root, project|
+      with_tmp_dir do |dir|
+        source = File.join(dir, "source.png")
+        File.binwrite(source, "original-bytes")
+        command = Hive::Commands::New.new(
+          project, "task with raced image", slug_override: "raced-attachment-task",
+          idempotency_key: "creator:raced-attachment", json: true,
+          attachments: [ [ source, "source.png" ] ]
+        )
+        original_lookup = command.method(:find_idempotent_task!)
+        command.define_singleton_method(:find_idempotent_task!) do |*args|
+          File.binwrite(source, "changed-after-fingerprint")
+          original_lookup.call(*args)
+        end
+
+        out, = capture_io { command.call! }
+        payload = JSON.parse(out)
+        captured = File.join(payload.fetch("task_folder"), "assets", "source.png")
+
+        assert_equal "original-bytes", File.binread(captured)
+
+        File.binwrite(source, "original-bytes")
+        retry_payload = create_json_with(
+          project, "task with raced image", key: "creator:raced-attachment",
+          slug: "raced-attachment-task", attachments: [ [ source, "source.png" ] ]
+        )
+        assert_equal false, retry_payload.fetch("created")
+        assert_equal 1, idempotent_tasks(project_root).size
+      end
+    end
+  end
+
+  def test_idempotent_attachment_copy_rejects_a_corrupted_snapshot_destination
+    with_initialized_project do |project_root, project|
+      with_tmp_dir do |dir|
+        source = File.join(dir, "source.png")
+        File.binwrite(source, "trusted-bytes")
+        original_cp = FileUtils.method(:cp)
+        corrupting_cp = lambda do |from, to, *args, **kwargs|
+          original_cp.call(from, to, *args, **kwargs)
+          File.binwrite(to, "corrupted-after-copy") if to.include?("/assets/")
+        end
+
+        with_replaced_singleton_method(FileUtils, :cp, corrupting_cp) do
+          error = assert_raises(Hive::Commands::New::InvalidAttachmentError) do
+            Hive::Commands::New.new(
+              project, "task with corrupted image",
+              slug_override: "corrupted-attachment-task",
+              idempotency_key: "creator:corrupted-attachment", json: true,
+              attachments: [ [ source, "source.png" ] ]
+            ).call!
+          end
+          assert_includes error.message, "attachment snapshot changed"
+          assert_equal "source.png", error.value
+        end
+
+        refute Dir.exist?(
+          File.join(
+            project_root, ".hive-state", "stages", "1-inbox",
+            "corrupted-attachment-task"
+          )
+        )
+      end
+    end
+  end
+
+  def test_idempotent_attachment_cleanup_tolerates_an_already_missing_snapshot
+    with_tmp_dir do |dir|
+      source = File.join(dir, "source.png")
+      File.binwrite(source, "trusted-bytes")
+      command = Hive::Commands::New.new(
+        "project", "task", idempotency_key: "creator:cleanup",
+        attachments: [ [ source, "source.png" ] ]
+      )
+      command.send(:prepare_idempotent_attachments!)
+      snapshot_dir = command.instance_variable_get(:@attachment_snapshot_dir)
+      FileUtils.remove_entry_secure(snapshot_dir)
+
+      assert_nil command.send(:cleanup_prepared_attachments!)
+      assert_nil command.instance_variable_get(:@attachment_snapshot_dir)
+      assert_nil command.instance_variable_get(:@prepared_attachments)
+    end
+  end
+
+  def test_commit_failure_removes_the_uncommitted_task
+    with_initialized_project do |project_root, project|
+      fake_ops = Object.new
+      runner = method(:run!)
+      hive_state = File.join(project_root, ".hive-state")
+      fake_ops.define_singleton_method(:hive_commit) do |**|
+        runner.call("git", "-C", hive_state, "add", "--",
+                    "stages/1-inbox/failed-task")
+        raise Hive::GitError, "commit failed"
+      end
+      fake_ops.define_singleton_method(:run_git!) do |*args|
+        runner.call("git", *args)
+      end
+
+      with_replaced_singleton_method(Hive::GitOps, :new, ->(_root) { fake_ops }) do
+        error = assert_raises(Hive::GitError) do
+          Hive::Commands::New.new(
+            project, "will fail", slug_override: "failed-task",
+            idempotency_key: "creator:commit-failure", json: true
+          ).call!
+        end
+        assert_includes error.message, "commit failed"
+      end
+
+      refute Dir.exist?(
+        File.join(project_root, ".hive-state", "stages", "1-inbox", "failed-task")
+      )
+      assert_empty run!("git", "-C", File.join(project_root, ".hive-state"),
+                        "diff", "--cached", "--name-only")
+    end
+  end
+
+  def test_interrupted_commit_removes_the_uncommitted_task
+    with_initialized_project do |project_root, project|
+      fake_ops = Object.new
+      runner = method(:run!)
+      hive_state = File.join(project_root, ".hive-state")
+      fake_ops.define_singleton_method(:hive_commit) do |**|
+        runner.call("git", "-C", hive_state, "add", "--",
+                    "stages/1-inbox/interrupted-task")
+        raise Interrupt, "stop"
+      end
+      fake_ops.define_singleton_method(:run_git!) { |*args| runner.call("git", *args) }
+
+      with_replaced_singleton_method(Hive::GitOps, :new, ->(_root) { fake_ops }) do
+        assert_raises(Interrupt) do
+          Hive::Commands::New.new(
+            project, "will stop", slug_override: "interrupted-task",
+            idempotency_key: "creator:interrupted", json: true
+          ).call!
+        end
+      end
+
+      refute Dir.exist?(
+        File.join(hive_state, "stages", "1-inbox", "interrupted-task")
+      )
+      assert_empty run!("git", "-C", hive_state, "diff", "--cached", "--name-only")
+    end
+  end
+
+  def test_non_idempotent_commit_failure_also_clears_staged_state
+    with_initialized_project do |project_root, project|
+      fake_ops = Object.new
+      runner = method(:run!)
+      hive_state = File.join(project_root, ".hive-state")
+      fake_ops.define_singleton_method(:hive_commit) do |**|
+        runner.call("git", "-C", hive_state, "add", "--",
+                    "stages/1-inbox/failed-legacy-task")
+        raise Hive::GitError, "commit failed"
+      end
+      fake_ops.define_singleton_method(:run_git!) { |*args| runner.call("git", *args) }
+
+      with_replaced_singleton_method(Hive::GitOps, :new, ->(_root) { fake_ops }) do
+        assert_raises(Hive::GitError) do
+          Hive::Commands::New.new(
+            project, "will fail", slug_override: "failed-legacy-task"
+          ).call!
+        end
+      end
+
+      refute Dir.exist?(
+        File.join(project_root, ".hive-state", "stages", "1-inbox", "failed-legacy-task")
+      )
+      assert_empty run!("git", "-C", File.join(project_root, ".hive-state"),
+                        "diff", "--cached", "--name-only")
+    end
+  end
+
+  def test_owner_authored_workflow_content_participates_in_fingerprint
+    with_initialized_project do |project_root, project|
+      create_authored_workflow(project_root, "editorial")
+      create_json_with(
+        project, "same request", key: "creator:authored-content", slug: "first-authored",
+        workflow: "editorial"
+      )
+      File.write(
+        File.join(project_root, ".hive-state", "workflows", "editorial", "work.md"),
+        "Changed owner instructions.\n"
+      )
+      Hive::Workflows::Project.reset!
+
+      error = assert_raises(Hive::Commands::New::IdempotencyConflict) do
+        Hive::Commands::New.new(
+          project, "same request", slug_override: "second-authored",
+          workflow: "editorial", idempotency_key: "creator:authored-content", json: true
+        ).call!
+      end
+
+      assert_includes error.message, "different input or workflow"
+      assert_equal 1, idempotent_tasks(project_root).size
+    end
+  end
+
+  def test_owner_authored_workflow_change_during_creation_aborts_candidate
+    with_initialized_project do |project_root, project|
+      create_authored_workflow(project_root, "editorial")
+      instruction = File.join(
+        project_root, ".hive-state", "workflows", "editorial", "work.md"
+      )
+      command = Hive::Commands::New.new(
+        project, "same request", slug_override: "raced-authored",
+        workflow: "editorial", idempotency_key: "creator:authored-race", json: true
+      )
+      original_lookup = command.method(:find_idempotent_task!)
+      command.define_singleton_method(:find_idempotent_task!) do |*args|
+        File.write(instruction, "Changed during task creation.\n")
+        original_lookup.call(*args)
+      end
+
+      error = assert_raises(Hive::ConcurrentRunError) { command.call! }
+
+      assert_includes error.message, "owner-authored workflow changed"
+      refute Dir.exist?(
+        File.join(project_root, ".hive-state", "stages", "1-inbox", "raced-authored")
+      )
+      assert_empty idempotent_tasks(project_root)
+    end
+  end
+
+  def test_unreadable_task_metadata_fails_idempotency_closed
+    with_initialized_project do |project_root, project|
+      capture_io do
+        Hive::Commands::New.new(
+          project, "legacy task", slug_override: "legacy-task"
+        ).call!
+      end
+      meta = File.join(
+        project_root, ".hive-state", "stages", "1-inbox", "legacy-task", "meta.yml"
+      )
+      File.write(meta, "id: [\n")
+
+      error = assert_raises(Hive::Commands::New::IdempotencyConflict) do
+        Hive::Commands::New.new(
+          project, "new task", slug_override: "must-not-exist",
+          idempotency_key: "creator:fail-closed", json: true
+        ).call!
+      end
+
+      assert_includes error.message, "cannot prove idempotency"
+      refute Dir.exist?(
+        File.join(project_root, ".hive-state", "stages", "1-inbox", "must-not-exist")
+      )
+    end
+  end
+
+  def test_post_ownership_eexist_cleans_the_candidate
+    with_initialized_project do |project_root, project|
+      replacement = lambda do |*|
+        raise Errno::EEXIST, "metadata collision"
+      end
+
+      with_replaced_singleton_method(Hive::TaskMeta, :write, replacement) do
+        assert_raises(Errno::EEXIST) do
+          Hive::Commands::New.new(
+            project, "candidate", slug_override: "owned-candidate",
+            idempotency_key: "creator:owned-candidate", json: true
+          ).call!
+        end
+      end
+
+      refute Dir.exist?(
+        File.join(project_root, ".hive-state", "stages", "1-inbox", "owned-candidate")
+      )
+    end
+  end
+
+  def test_idempotent_candidate_is_created_with_workflow_then_commit_locks_held
+    with_initialized_project do |project_root, project|
+      command = Hive::Commands::New.new(
+        project, "same task", slug_override: "locked-task",
+        idempotency_key: "creator:race", json: true
+      )
+      original = command.method(:create_task_candidate!)
+      observe_lock = method(:other_process_lock_state)
+      command.define_singleton_method(:create_task_candidate!) do |*args, **kwargs|
+        commit_lock = File.join(project_root, ".hive-state", ".commit-lock")
+        workflow_lock = File.join(
+          project_root, ".hive-state", "workflows", ".mutation.lock"
+        )
+        raise "candidate creation ran outside workflow mutation lock" unless
+          observe_lock.call(workflow_lock) == "blocked"
+        raise "candidate creation ran outside commit lock" unless
+          observe_lock.call(commit_lock) == "blocked"
+
+        original.call(*args, **kwargs)
+      end
+
+      out, = capture_io { command.call! }
+      payload = JSON.parse(out)
+
+      assert_equal true, payload.fetch("created")
+      assert Dir.exist?(
+        File.join(project_root, ".hive-state", "stages", "1-inbox", "locked-task")
+      )
+    end
+  end
+
+  def test_same_slug_with_a_different_key_never_reuses_or_removes_the_first_task
+    with_initialized_project do |project_root, project|
+      first = create_json(
+        project, "first task", key: "creator:first", slug: "shared-slug"
+      )
+
+      error = assert_raises(Hive::Commands::New::SlugCollisionError) do
+        Hive::Commands::New.new(
+          project, "second task", slug_override: "shared-slug",
+          idempotency_key: "creator:second", json: true
+        ).call!
+      end
+
+      assert_includes error.message, "slug collision"
+      assert_includes File.read(File.join(first.fetch("task_folder"), "idea.md")), "first task"
+      metadata = Hive::TaskMeta.read(first.fetch("task_folder"))
+      assert_equal "creator:first", metadata.fetch(:idempotency_key)
+      assert_equal 1, idempotent_tasks(project_root).size
+    end
+  end
+
+  def test_multiple_tasks_with_the_same_key_fail_closed
+    with_initialized_project do |project_root, project|
+      %w[first second].each do |slug|
+        capture_io do
+          Hive::Commands::New.new(
+            project, "#{slug} task", slug_override: "#{slug}-task"
+          ).call!
+        end
+        folder = File.join(project_root, ".hive-state", "stages", "1-inbox", "#{slug}-task")
+        Hive::TaskMeta.rewrite(
+          folder, idempotency_key: "creator:duplicate", input_fingerprint: "fingerprint"
+        )
+      end
+
+      error = assert_raises(Hive::Commands::New::IdempotencyConflict) do
+        Hive::Commands::New.new(
+          project, "third task", slug_override: "third-task",
+          idempotency_key: "creator:duplicate", json: true
+        ).call!
+      end
+      assert_includes error.message, "multiple tasks"
+      assert_equal 2, idempotent_tasks(project_root).size
+    end
+  end
+
+  def test_plain_retry_reports_existing_task_and_next_command
+    with_initialized_project do |_project_root, project|
+      create_json(project, "plain retry", key: "creator:plain", slug: "plain-task")
+
+      out, = capture_io do
+        Hive::Commands::New.new(
+          project, "plain retry", slug_override: "ignored-task",
+          idempotency_key: "creator:plain"
+        ).call!
+      end
+
+      assert_includes out, "idempotent task already exists"
+      assert_includes out, "next: hive brainstorm"
+    end
+  end
+
+  def test_legacy_creation_does_not_write_idempotency_metadata
+    with_initialized_project do |project_root, project|
+      capture_io { Hive::Commands::New.new(project, "ordinary task", slug_override: "ordinary-task").call! }
+      folder = File.join(project_root, ".hive-state", "stages", "1-inbox", "ordinary-task")
+      metadata = File.read(File.join(folder, "meta.yml"))
+
+      refute_includes metadata, "idempotency_key"
+      refute_includes metadata, "input_fingerprint"
+    end
+  end
+
+  private
+
+  def with_initialized_project
+    with_tmp_global_config do
+      with_tmp_git_repo do |project_root|
+        capture_io do
+          Hive::Commands::Init.new(project_root, agent_skill_preflight: false).call
+        end
+        yield project_root, File.basename(project_root)
+      end
+    end
+  end
+
+  def create_json(project, text, key:, slug:)
+    create_json_with(project, text, key: key, slug: slug)
+  end
+
+  def create_json_with(project, text, key:, slug:, attachments: [], workflow: nil)
+    out, err = capture_io do
+      Hive::Commands::New.new(
+        project, text, slug_override: slug, idempotency_key: key, json: true,
+        attachments: attachments, workflow: workflow
+      ).call!
+    end
+    assert_empty err
+    JSON.parse(out)
+  end
+
+  def create_authored_workflow(project_root, id)
+    Hive::Commands::Workflow.new!(
+      id, project_root: project_root, stdout: StringIO.new
+    )
+  end
+
+  def idempotent_tasks(project_root)
+    Dir.glob(File.join(project_root, ".hive-state", "stages", "*", "*", "meta.yml")).select do |path|
+      Hive::TaskMeta.read(File.dirname(path))[:idempotency_key]
+    end
+  end
+
+  def other_process_lock_state(path)
+    reader, writer = IO.pipe
+    pid = fork do
+      reader.close
+      acquired = File.open(path, File::RDWR | File::CREAT, 0o644) do |lock|
+        lock.flock(File::LOCK_EX | File::LOCK_NB)
+      end
+      writer.write(acquired ? "acquired" : "blocked")
+      writer.close
+      exit! 0
+    end
+    writer.close
+    state = reader.read
+    Process.wait(pid)
+    reader.close
+    state
+  end
+end
diff --git a/test/integration/new_test.rb b/test/integration/new_test.rb
index 040e62eba2..bce1091ac8 100644
--- a/test/integration/new_test.rb
+++ b/test/integration/new_test.rb
@@ -91,6 +91,37 @@ def test_new_workflow_override_with_agent_entry_seeds_markerless_state
     end
   end
 
+  def test_new_workflow_with_human_entry_seeds_identified_waiting_marker
+    descriptor = Hive::Workflow.new(
+      id: :human_entry,
+      stages: [
+        Hive::Workflow::Stage.new(
+          name: "approval", index: 1, state_file: "approval.md", kind: :human,
+          outcomes: {
+            "approve" => Hive::Workflow::Outcome.new(
+              name: "approve", complete: true, artifact: "approval.md"
+            )
+          }.freeze
+        )
+      ]
+    )
+    with_registered_workflow(descriptor) do
+      with_tmp_global_config do
+        with_tmp_git_repo do |dir|
+          setup_project { Hive::Commands::Init.new(dir, agent_skill_preflight: false).call }
+          project = File.basename(dir)
+
+          capture_io { Hive::Commands::New.new(project, "human entry", workflow: "human_entry").call }
+
+          folder = Dir[File.join(dir, ".hive-state", "stages", "1-approval", "human-entry-*")].fetch(0)
+          marker = Hive::Markers.current(File.join(folder, "approval.md"))
+          assert_equal :waiting, marker.name
+          assert_match(/\A[0-9a-f]{16}\z/, marker.attrs.fetch("decision_id"))
+        end
+      end
+    end
+  end
+
   def test_new_single_stage_workflow_prints_run_hint_without_move
     with_registered_workflow(single_stage_workflow) do
       with_tmp_global_config do
diff --git a/test/integration/new_wrapper_argv_test.rb b/test/integration/new_wrapper_argv_test.rb
index 25384fa482..dbfeadf34d 100644
--- a/test/integration/new_wrapper_argv_test.rb
+++ b/test/integration/new_wrapper_argv_test.rb
@@ -190,6 +190,28 @@ def test_json_boolean_after_project_is_lifted_but_new_stays_plain_text
     end
   end
 
+  def test_idempotency_key_after_project_enables_machine_readable_retry
+    with_workflow_project do |home, project_root, project|
+      argv = [
+        "new", project, "--workflow", WORKFLOW_ID,
+        "--idempotency-key", "creator:wrapper:v1", "--json", "idempotent wrapper task"
+      ]
+      first_out, first_err, first_status = run_hive(home, *argv, chdir: project_root)
+      retry_out, retry_err, retry_status = run_hive(home, *argv, chdir: project_root)
+
+      assert first_status.success?, first_err
+      assert retry_status.success?, retry_err
+      first = JSON.parse(first_out)
+      retry_payload = JSON.parse(retry_out)
+      assert_equal true, first.fetch("created")
+      assert_equal false, retry_payload.fetch("created")
+      assert_equal first.fetch("slug"), retry_payload.fetch("slug")
+      folder = only_task_folder(project_root)
+      assert_equal "creator:wrapper:v1", Hive::TaskMeta.read(folder).fetch(:idempotency_key)
+      refute_includes File.read(File.join(folder, "idea.md")), "--idempotency-key"
+    end
+  end
+
   def test_missing_text_after_lifted_workflow_fails_without_creating_task
     with_workflow_project do |home, project_root, project|
       _out, err, status = run_hive(home, "new", project, "--workflow", WORKFLOW_ID, chdir: project_root)
diff --git a/test/integration/tui_new_idea_attachments_smoke_test.rb b/test/integration/tui_new_idea_attachments_smoke_test.rb
index ee42b9e695..8e24b5f083 100644
--- a/test/integration/tui_new_idea_attachments_smoke_test.rb
+++ b/test/integration/tui_new_idea_attachments_smoke_test.rb
@@ -116,7 +116,7 @@ def test_paste_three_images_submit_and_brainstorm_sees_asset_refs
 
             writer.write("\r")
             writer.flush
-            grid_buffer = read_until(reader, deadline_seconds: 10.0) { |buf| buf.include?("bug-here") }
+            grid_buffer = read_until(reader, deadline_seconds: 30.0) { |buf| buf.include?("bug-here") }
             assert_includes grid_buffer, "bug-here",
               "post-submit grid must show the bug-here slug row"
 
@@ -238,7 +238,7 @@ def test_ctrl_v_stages_clipboard_image_and_submit_persists_asset
 
           writer.write("\r")
           writer.flush
-          grid_buffer = read_until(reader, deadline_seconds: 10.0) { |buf| buf.include?("ctrl-v-image") }
+          grid_buffer = read_until(reader, deadline_seconds: 30.0) { |buf| buf.include?("ctrl-v-image") }
           assert_includes grid_buffer, "ctrl-v-image",
             "post-submit grid must show the ctrl-v-image slug row"
 
diff --git a/test/integration/workflow_command_test.rb b/test/integration/workflow_command_test.rb
new file mode 100644
index 0000000000..506d69017a
--- /dev/null
+++ b/test/integration/workflow_command_test.rb
@@ -0,0 +1,350 @@
+require "test_helper"
+require "json"
+require "json_schemer"
+require "hive/commands/init"
+require "hive/commands/workflow"
+require "hive/commands/workflow/validate"
+
+class WorkflowCommandTest < Minitest::Test
+  include HiveTestHelper
+
+  def setup
+    super
+    Hive::Workflows::Project.reset!
+  end
+
+  def teardown
+    Hive::Workflows::Project.reset!
+    super
+  end
+
+  def test_validate_reports_exact_editorial_graph_without_writing
+    with_initialized_project do |project_root|
+      descriptor = write_editorial_workflow(project_root)
+      before = project_snapshot(project_root)
+      before_head = state_head(project_root)
+      stdout = StringIO.new
+
+      payload = Hive::Commands::Workflow.new(
+        "validate", "editorial", project_root: project_root, json: true, stdout: stdout
+      ).call!
+
+      assert_equal before, project_snapshot(project_root)
+      assert_equal before_head, state_head(project_root)
+      assert_equal true, payload.fetch("valid")
+      assert_equal "authored", payload.fetch("origin")
+      assert_equal descriptor, payload.fetch("descriptor_path")
+      assert_equal %w[research draft approval], payload.fetch("stages").map { |row| row.fetch("name") }
+      assert_equal %w[research draft], payload.fetch("automatic_edges").map { |row| row.fetch("from") }
+      assert_equal [
+        [ "approval", "approve", true, nil ],
+        [ "approval", "reject", false, "draft" ]
+      ], payload.fetch("human_outcomes").map { |row|
+        [ row.fetch("stage"), row.fetch("name"), row.fetch("complete"), row["to"] ]
+      }
+
+      schemer = JSONSchemer.schema(JSON.parse(File.read(Hive::Schemas.schema_path("hive-workflow-validate"))))
+      assert_empty schemer.validate(payload).to_a
+    end
+  end
+
+  def test_validate_rejects_missing_instruction_without_mutation
+    with_initialized_project do |project_root|
+      descriptor = write_editorial_workflow(project_root)
+      File.delete(File.join(File.dirname(descriptor), "editorial", "draft.md"))
+      before = project_snapshot(project_root)
+      before_head = state_head(project_root)
+
+      out, err, status = with_captured_exit do
+        Hive::Commands::Workflow.new(
+          "validate", "editorial", project_root: project_root, json: true
+        ).call
+      end
+
+      assert_equal Hive::ExitCodes::CONFIG, status
+      assert_empty err
+      payload = JSON.parse(out)
+      assert_equal false, payload.fetch("valid")
+      assert_includes payload.fetch("message"), "draft.md"
+      assert_equal before, project_snapshot(project_root)
+      assert_equal before_head, state_head(project_root)
+      schemer = JSONSchemer.schema(JSON.parse(File.read(Hive::Schemas.schema_path("hive-workflow-validate"))))
+      assert_empty schemer.validate(payload).to_a
+    end
+  end
+
+  def test_validate_loads_built_in_through_project_resolution
+    with_initialized_project do |project_root|
+      payload = Hive::Commands::Workflow.new(
+        "validate", "coding", project_root: project_root, json: false, stdout: StringIO.new
+      ).call!
+
+      assert_equal "built_in", payload.fetch("origin")
+      assert_nil payload.fetch("descriptor_path")
+      assert_equal Hive::Workflows::Coding::DESCRIPTOR.stage_names,
+                   payload.fetch("stages").map { |row| row.fetch("name") }
+    end
+  end
+
+  def test_validate_defaults_missing_hive_state_configuration_without_writing
+    with_tmp_dir do |project_root|
+      validator = Hive::Commands::Workflow::Validate.new(
+        "coding", project_root: project_root, stdout: StringIO.new
+      )
+      config_path = File.join(project_root, "config.yml")
+      read = ->(_root) { [ config_path, {} ] }
+      normalize = ->(data, _source, **_options) { data }
+
+      workflows = with_replaced_singleton_method(
+        Hive::Config, :read_project_config, read
+      ) do
+        with_replaced_singleton_method(
+          Hive::Config, :normalize_legacy_project_config, normalize
+        ) { validator.send(:workflows_dir) }
+      end
+
+      assert_equal File.join(project_root, ".hive-state", "workflows"), workflows
+      refute File.exist?(File.join(project_root, ".hive-state"))
+    end
+  end
+
+  def test_validate_rejects_an_authored_descriptor_claiming_a_built_in_id
+    with_initialized_project do |project_root|
+      editorial = write_editorial_workflow(project_root)
+      shadow = File.join(File.dirname(editorial), "coding.yml")
+      File.write(
+        shadow,
+        File.read(editorial).sub(/^id: editorial$/, "id: coding")
+      )
+
+      error = assert_raises(Hive::ConfigError) do
+        Hive::Commands::Workflow::Validate.new(
+          "coding", project_root: project_root, stdout: StringIO.new
+        ).call!
+      end
+
+      assert_includes error.message, "collides with registered workflow"
+      assert_includes error.message, "coding"
+    end
+  end
+
+  def test_validate_unknown_workflow_lists_authored_managed_and_built_in_ids
+    with_initialized_project do |project_root|
+      descriptor = write_editorial_workflow(project_root)
+      workflows = File.dirname(descriptor)
+      managed = File.join(workflows, "managed-flow")
+      FileUtils.mkdir_p(managed)
+      File.write(
+        File.join(managed, Hive::WorkflowPackage::ManagedStore::LOCK_FILE),
+        "{}\n"
+      )
+
+      error = assert_raises(Hive::Workflows::UnknownWorkflow) do
+        Hive::Commands::Workflow::Validate.new(
+          "missing-flow", project_root: project_root, stdout: StringIO.new
+        ).call!
+      end
+
+      assert_includes error.message, "editorial"
+      assert_includes error.message, "managed-flow"
+      assert_includes error.message, "coding"
+    end
+  end
+
+  def test_validate_never_calls_the_production_project_config_loader
+    with_initialized_project do |project_root|
+      write_editorial_workflow(project_root)
+      forbidden = lambda do |_root|
+        raise "strict read-only validation called Config.load"
+      end
+
+      with_replaced_singleton_method(Hive::Config, :load, forbidden) do
+        payload = Hive::Commands::Workflow::Validate.new(
+          "editorial", project_root: project_root, stdout: StringIO.new
+        ).call!
+        assert_equal true, payload.fetch("valid")
+        assert_equal "authored", payload.fetch("origin")
+      end
+    end
+  end
+
+  def test_validate_rejects_invalid_id_and_unreadable_instruction
+    with_initialized_project do |project_root|
+      error = assert_raises(Hive::Commands::Workflow::UsageError) do
+        Hive::Commands::Workflow::Validate.new(
+          "Not Valid", project_root: project_root
+        ).call!
+      end
+      assert_includes error.message, "invalid workflow id"
+
+      instruction = File.join(project_root, "missing.md")
+      stage = Struct.new(:instruction, :name).new(instruction, "draft")
+      workflow = [ stage ]
+      workflow.define_singleton_method(:id) { :editorial }
+      validator = Hive::Commands::Workflow::Validate.new(
+        "editorial", project_root: project_root
+      )
+      error = assert_raises(Hive::ConfigError) do
+        validator.send(:validate_instruction_paths!, workflow)
+      end
+      assert_includes error.message, "missing instruction"
+    end
+  end
+
+  def test_validate_reports_managed_origin_and_plain_summary
+    with_initialized_project do |project_root|
+      stdout = StringIO.new
+      validator = Hive::Commands::Workflow::Validate.new(
+        "managed-flow", project_root: project_root, stdout: stdout
+      )
+      validator.instance_variable_set(:@descriptor_path, nil)
+      assert_equal "managed", validator.send(:origin)
+
+      payload = {
+        "stages" => [ { "name" => "work" } ]
+      }
+      validator.send(:emit, payload)
+      assert_includes stdout.string, "managed-flow is valid (managed)"
+      assert_includes stdout.string, "stages: work"
+    end
+  end
+
+  def test_validate_resolves_managed_workflow_through_no_write_selection
+    with_initialized_project do |project_root|
+      workflow = Hive::Workflows::Registry.default
+      fake_store = Object.new
+      test = self
+      fake_store.define_singleton_method(:selected_read_only) do |name|
+        test.assert_equal "managed-flow", name
+        {
+          "source_commit" => "a" * 40,
+          "manifest_digest" => "b" * 64,
+          "configuration_digest" => "c" * 64
+        }
+      end
+      fake_store.define_singleton_method(:workflow) do |name, _commit, _manifest, **options|
+        test.assert_equal "managed-flow", name
+        test.assert_equal false, options.fetch(:verify_profiles)
+        workflow
+      end
+
+      with_replaced_singleton_method(
+        Hive::WorkflowPackage::ManagedStore, :new, ->(_state) { fake_store }
+      ) do
+        payload = Hive::Commands::Workflow::Validate.new(
+          "managed-flow", project_root: project_root, stdout: StringIO.new
+        ).call!
+        assert_equal "managed", payload.fetch("origin")
+      end
+    end
+  end
+
+  def test_workflow_maps_unknown_workflow_to_usage
+    command = Hive::Commands::Workflow.new(
+      "validate", "missing", project_root: ".", stdout: StringIO.new
+    )
+    error = Hive::Workflows::UnknownWorkflow.new("missing")
+
+    assert_equal Hive::Schemas::WorkflowNewErrorKind::USAGE,
+                 command.send(:error_kind_for, error)
+  end
+
+  def test_commit_refuses_a_managed_workflow
+    with_tmp_dir do |project_root|
+      validation = Object.new
+      validation.define_singleton_method(:call!) do
+        {
+          "origin" => "managed",
+          "descriptor_path" => File.join(project_root, "managed-flow.yml")
+        }
+      end
+
+      with_replaced_singleton_method(
+        Hive::Commands::Workflow::Validate, :new, ->(*_args, **_kwargs) { validation }
+      ) do
+        error = assert_raises(Hive::Commands::Workflow::UsageError) do
+          Hive::Commands::Workflow.new(
+            "commit", "managed-flow", project_root: project_root,
+            stdout: StringIO.new
+          ).call!
+        end
+        assert_includes error.message, "requires an owner-authored workflow"
+        assert_includes error.message, "managed"
+      end
+    end
+  end
+
+  def test_new_collision_proposes_deterministic_available_id
+    with_initialized_project do |project_root|
+      Hive::Commands::Workflow.new!("editorial", project_root: project_root, stdout: StringIO.new)
+      Hive::Commands::Workflow.new!("editorial-2", project_root: project_root, stdout: StringIO.new)
+      before = project_snapshot(project_root)
+
+      error = assert_raises(Hive::Commands::Workflow::UsageError) do
+        Hive::Commands::Workflow.new!("editorial", project_root: project_root, stdout: StringIO.new)
+      end
+
+      assert_equal "editorial-3", error.suggested_id
+      assert_includes error.message, "try \"editorial-3\""
+      assert_equal before, project_snapshot(project_root)
+    end
+  end
+
+  private
+
+  def with_initialized_project
+    with_tmp_global_config do
+      with_tmp_git_repo do |project_root|
+        capture_io { Hive::Commands::Init.new(project_root, agent_skill_preflight: false).call }
+        yield project_root
+      end
+    end
+  end
+
+  def write_editorial_workflow(project_root)
+    workflows = File.join(project_root, ".hive-state", "workflows")
+    instructions = File.join(workflows, "editorial")
+    FileUtils.mkdir_p(instructions)
+    File.write(File.join(instructions, "research.md"), "Research the request.\n")
+    File.write(File.join(instructions, "draft.md"), "Draft from research.md.\n")
+    descriptor = File.join(workflows, "editorial.yml")
+    File.write(descriptor, <<~YAML)
+      id: editorial
+      stages:
+        - name: research
+          kind: agent
+          state_file: research.md
+          instruction: editorial/research.md
+          permissions: yolo
+        - name: draft
+          kind: agent
+          state_file: draft.md
+          instruction: editorial/draft.md
+          permissions: yolo
+        - name: approval
+          kind: human
+          state_file: approval.md
+          input: draft.md
+          outcomes:
+            approve:
+              complete: true
+              artifact: draft.md
+            reject:
+              to: draft
+    YAML
+    descriptor
+  end
+
+  def project_snapshot(project_root)
+    root = File.join(project_root, ".hive-state", "workflows")
+    Dir.glob(File.join(root, "**", "*"), File::FNM_DOTMATCH).sort.filter_map do |path|
+      next if File.directory?(path)
+
+      [ path.delete_prefix("#{root}/"), File.binread(path) ]
+    end
+  end
+
+  def state_head(project_root)
+    run!("git", "-C", File.join(project_root, ".hive-state"), "rev-parse", "HEAD").strip
+  end
+end
diff --git a/test/integration/workflow_creator_e2e_test.rb b/test/integration/workflow_creator_e2e_test.rb
new file mode 100644
index 0000000000..13092b7ebf
--- /dev/null
+++ b/test/integration/workflow_creator_e2e_test.rb
@@ -0,0 +1,412 @@
+require "test_helper"
+require "json"
+require "json_schemer"
+require "rubygems/version"
+require "hive/agent_skills/canonical_skill"
+require "hive/commands/approve"
+require "hive/commands/decide"
+require "hive/commands/init"
+require "hive/commands/new"
+require "hive/commands/status"
+require "hive/commands/workflow"
+require "hive/task_meta"
+
+class WorkflowCreatorE2ETest < Minitest::Test
+  include HiveTestHelper
+
+  EDITORIAL_PROMPT =
+    "Create a three-stage editorial workflow that researches, drafts, and requires approval before publishing".freeze
+
+  def setup
+    super
+    Hive::Workflows::Project.reset!
+  end
+
+  def teardown
+    Hive::Workflows::Project.reset!
+    super
+  end
+
+  def test_ae1_creation_only_uses_scaffold_and_validation_without_a_task
+    with_initialized_project do |project_root, _project|
+      commands = []
+      payload = create_editorial(project_root, commands: commands)
+
+      assert_equal [
+        [ "version" ],
+        [ "workflow", "list", "--json" ],
+        [ "workflow", "new", "editorial" ],
+        [ "workflow", "validate", "editorial", "--json" ],
+        [ "workflow", "commit", "editorial" ]
+      ], commands
+      assert_equal %w[research draft approval], payload.fetch("stages").map { |stage| stage.fetch("name") }
+      assert_equal [
+        [ "research", "draft" ],
+        [ "draft", "approval" ]
+      ], payload.fetch("automatic_edges").map { |edge| edge.values_at("from", "to") }
+      assert_equal [
+        [ "approval", "approve", true, "draft.md", nil ],
+        [ "approval", "reject", false, nil, "draft" ]
+      ], payload.fetch("human_outcomes").map { |outcome|
+        outcome.values_at("stage", "name", "complete", "artifact", "to")
+      }
+      assert_empty task_folders(project_root)
+      assert_schema_valid("hive-workflow-validate", payload)
+      committed = run!(
+        "git", "-C", File.join(project_root, ".hive-state"),
+        "show", "--format=", "--name-only", "HEAD"
+      )
+      assert_includes committed, "workflows/editorial.yml"
+      assert_includes committed, "workflows/editorial/research.md"
+      assert_includes committed, "workflows/editorial/draft.md"
+
+      creator = Hive::AgentSkills::CanonicalSkill.new.rendered_canonical_files
+                                                   .fetch("references/workflow-creator.md")
+      assert_includes creator, "No task by default"
+      assert_includes creator, "hive new 'PROJECT' --workflow 'ID'"
+      assert_includes creator, "Never publish externally"
+      assert_operator creator.index("hive workflow new"), :<, creator.index("hive workflow validate")
+      assert_operator creator.index("hive workflow validate"), :<,
+                      creator.index('hive workflow commit "$ID"')
+      assert_operator creator.index('hive workflow commit "$ID"'), :<,
+                      creator.index("hive new 'PROJECT' --workflow 'ID'")
+    end
+  end
+
+  def test_creator_refuses_an_old_hive_before_inspection_or_mutation
+    with_initialized_project do |project_root, _project|
+      before = repository_snapshot(project_root)
+      commands = []
+
+      result = create_editorial(
+        project_root, commands: commands, installed_version: "0.0.1"
+      )
+
+      assert_equal :version_refused, result
+      assert_equal [ [ "version" ] ], commands
+      assert_equal before, repository_snapshot(project_root)
+      refute File.exist?(workflow_descriptor(project_root))
+    end
+  end
+
+  def test_creator_refuses_unconfirmed_fresh_init_without_mutation
+    with_tmp_global_config do
+      with_tmp_git_repo do |project_root|
+        before = repository_snapshot(project_root)
+        commands = []
+
+        result = preview_fresh_creator(
+          project_root, confirmed: false, commands: commands
+        )
+
+        assert_equal :confirmation_required, result
+        assert_equal [
+          [ "version" ],
+          [ "init", "--new-workflow", "editorial", "--minimal", "--preview", "--json" ]
+        ], commands
+        assert_equal before, repository_snapshot(project_root)
+        refute File.exist?(workflow_descriptor(project_root))
+      end
+    end
+  end
+
+  def test_ae1_and_ae2_approval_and_rejection_are_durable_and_closed
+    with_initialized_project do |project_root, project|
+      create_editorial(project_root)
+
+      approved = create_task(project, "approved-editorial", key: "creator:approved")
+      approval = advance_to_approval(project_root, approved.fetch("slug"))
+      approve_payload = decide(approved.fetch("slug"), "approve", note: "Publish-ready")
+      record = Hive::Commands::Decide.latest_record(File.join(approval, "approval.md"))
+
+      assert_equal true, approve_payload.fetch("completed")
+      assert_equal "draft.md", approve_payload.fetch("artifact")
+      assert_equal "approve", record.fetch("outcome")
+      assert_equal "publish_ready", record.fetch("artifact_status")
+      assert_equal "draft.md", record.fetch("artifact")
+      refute Dir.exist?(File.join(project_root, ".hive-state", "stages", "4-publish"))
+
+      rejected = create_task(project, "rejected-editorial", key: "creator:rejected")
+      rejected_approval = advance_to_approval(project_root, rejected.fetch("slug"))
+      reject_payload = decide(rejected.fetch("slug"), "reject", note: "Strengthen the evidence")
+      draft = task_folder(project_root, "2-draft", rejected.fetch("slug"))
+      reject_record = Hive::Commands::Decide.latest_record(File.join(draft, "approval.md"))
+
+      assert_equal false, reject_payload.fetch("completed")
+      assert_equal "2-draft", reject_payload.fetch("current_stage")
+      assert_equal :waiting, Hive::Markers.current(File.join(draft, "draft.md")).name
+      assert_equal "reject", reject_record.fetch("outcome")
+      assert_equal "draft", reject_record.fetch("to")
+      refute Dir.exist?(rejected_approval)
+    end
+  end
+
+  def test_ae3_collision_is_byte_identical_and_proposes_available_id
+    with_initialized_project do |project_root, _project|
+      create_editorial(project_root)
+      before = workflow_snapshot(project_root)
+
+      error = assert_raises(Hive::Commands::Workflow::UsageError) do
+        Hive::Commands::Workflow.new!(
+          "editorial", project_root: project_root, stdout: StringIO.new
+        )
+      end
+
+      assert_equal "editorial-2", error.suggested_id
+      assert_includes error.message, 'try "editorial-2"'
+      assert_equal before, workflow_snapshot(project_root)
+    end
+  end
+
+  def test_ae4_fresh_minimal_preview_is_no_write_until_confirmed
+    with_tmp_global_config do
+      with_tmp_git_repo do |project_root|
+        before = repository_snapshot(project_root)
+        preview_out, preview_err = capture_io do
+          Hive::Commands::Init.new(
+            project_root, new_workflow: "editorial", minimal: true, preview: true,
+            json: true, agent_skill_preflight: false
+          ).call
+        end
+        preview = JSON.parse(preview_out)
+
+        assert_empty preview_err
+        assert_equal before, repository_snapshot(project_root)
+        assert_equal false, preview.fetch("task_creation")
+        assert_equal false, preview.dig("services", "daemon_install")
+        assert_equal false, preview.dig("background_automation", "daemon_dispatch")
+        assert_schema_valid("hive-init-preview", preview)
+
+        execute_out, execute_err = capture_io do
+          Hive::Commands::Init.new(
+            project_root, new_workflow: "editorial", minimal: true,
+            json: true, agent_skill_preflight: false
+          ).call
+        end
+        execute = JSON.parse(execute_out)
+        assert_empty execute_err
+        assert_equal true, execute.fetch("minimal")
+
+        author_editorial(project_root)
+        validation = validate_editorial(project_root)
+        commit_editorial(project_root)
+        assert_equal %w[research draft approval], validation.fetch("stages").map { |stage| stage.fetch("name") }
+        assert_empty task_folders(project_root)
+      end
+    end
+  end
+
+  def test_ae5_explicit_task_creation_is_idempotent_after_movement
+    with_initialized_project do |project_root, project|
+      create_editorial(project_root)
+      key = "workflow-creator:editorial:stable"
+      first = create_task(project, "idempotent-editorial", key: key)
+      research = task_folder(project_root, "1-research", first.fetch("slug"))
+      Hive::Markers.set(File.join(research, "research.md"), :complete)
+      capture_io { Hive::Commands::Approve.new(first.fetch("slug"), from: "research").call }
+
+      retry_payload = create_task(project, "idempotent-editorial", key: key)
+      status_out, = capture_io { Hive::Commands::Status.new(json: true, operational: true).call }
+      operational = JSON.parse(status_out)
+      live = operational.fetch("tasks").find do |task|
+        task.dig("identity", "slug") == first.fetch("slug")
+      end
+
+      assert_equal true, first.fetch("created")
+      assert_equal false, retry_payload.fetch("created")
+      assert_equal first.fetch("slug"), retry_payload.fetch("slug")
+      assert_equal "2-draft", retry_payload.fetch("current_stage")
+      assert_equal 1, idempotent_task_folders(project_root, key).size
+      assert_equal "2-draft", live.dig("position", "stage")
+      assert_schema_valid("hive-new", retry_payload)
+      assert_schema_valid("hive-operational-status", operational)
+    end
+  end
+
+  def test_invalid_generated_yaml_stops_before_task_creation
+    with_initialized_project do |project_root, _project|
+      Hive::Commands::Workflow.new!(
+        "editorial", project_root: project_root, stdout: StringIO.new
+      )
+      descriptor = workflow_descriptor(project_root)
+      File.write(descriptor, "id: editorial\nstages: [\n")
+      before = workflow_snapshot(project_root)
+
+      assert_raises(Hive::ConfigError) { validate_editorial(project_root) }
+      assert_equal before, workflow_snapshot(project_root)
+      assert_empty task_folders(project_root)
+    end
+  end
+
+  private
+
+  def with_initialized_project
+    with_tmp_global_config do
+      with_tmp_git_repo do |project_root|
+        capture_io { Hive::Commands::Init.new(project_root, agent_skill_preflight: false).call }
+        yield project_root, File.basename(project_root)
+      end
+    end
+  end
+
+  def create_editorial(project_root, commands: nil, installed_version: Hive::VERSION)
+    commands&.push([ "version" ])
+    return :version_refused if Gem::Version.new(installed_version) < Gem::Version.new(Hive::VERSION)
+
+    commands&.push([ "workflow", "list", "--json" ])
+    Hive::Commands::Workflow.new(
+      "list", project_root: project_root, json: true, stdout: StringIO.new
+    ).call!
+    commands&.push([ "workflow", "new", "editorial" ])
+    Hive::Commands::Workflow.new!(
+      "editorial", project_root: project_root, stdout: StringIO.new
+    )
+    author_editorial(project_root)
+    commands&.push([ "workflow", "validate", "editorial", "--json" ])
+    validation = validate_editorial(project_root)
+    commands&.push([ "workflow", "commit", "editorial" ])
+    commit_editorial(project_root)
+    validation
+  end
+
+  def preview_fresh_creator(project_root, confirmed:, commands:)
+    commands.push([ "version" ])
+    commands.push(
+      [ "init", "--new-workflow", "editorial", "--minimal", "--preview", "--json" ]
+    )
+    capture_io do
+      Hive::Commands::Init.new(
+        project_root, new_workflow: "editorial", minimal: true, preview: true,
+        json: true, agent_skill_preflight: false
+      ).call
+    end
+    return :confirmation_required unless confirmed
+
+    raise "confirmed fresh creator execution is covered by AE4"
+  end
+
+  def author_editorial(project_root)
+    descriptor = workflow_descriptor(project_root)
+    instruction_dir = File.join(File.dirname(descriptor), "editorial")
+    FileUtils.rm_f(File.join(instruction_dir, "work.md"))
+    File.write(File.join(instruction_dir, "research.md"), "Research the request and write research.md.\n")
+    File.write(File.join(instruction_dir, "draft.md"), "Use research.md to write a publish-ready draft.md.\n")
+    File.write(descriptor, <<~YAML)
+      id: editorial
+      stages:
+        - name: research
+          kind: agent
+          state_file: research.md
+          instruction: editorial/research.md
+          permissions: yolo
+        - name: draft
+          kind: agent
+          state_file: draft.md
+          instruction: editorial/draft.md
+          permissions: yolo
+        - name: approval
+          kind: human
+          state_file: approval.md
+          input: draft.md
+          outcomes:
+            approve:
+              complete: true
+              artifact: draft.md
+            reject:
+              to: draft
+    YAML
+    Hive::Workflows::Project.reset!
+  end
+
+  def validate_editorial(project_root)
+    Hive::Commands::Workflow.new(
+      "validate", "editorial", project_root: project_root,
+      json: true, stdout: StringIO.new
+    ).call!
+  end
+
+  def commit_editorial(project_root)
+    Hive::Commands::Workflow.new(
+      "commit", "editorial", project_root: project_root, stdout: StringIO.new
+    ).call!
+  end
+
+  def create_task(project, slug, key:)
+    out, err = capture_io do
+      Hive::Commands::New.new(
+        project, EDITORIAL_PROMPT, slug_override: slug, workflow: "editorial",
+        idempotency_key: key, json: true
+      ).call!
+    end
+    assert_empty err
+    JSON.parse(out)
+  end
+
+  def advance_to_approval(project_root, slug)
+    research = task_folder(project_root, "1-research", slug)
+    Hive::Markers.set(File.join(research, "research.md"), :complete)
+    capture_io { Hive::Commands::Approve.new(slug, from: "research").call }
+    draft = task_folder(project_root, "2-draft", slug)
+    File.write(File.join(draft, "draft.md"), "# Draft\n\nPublishable copy.\n")
+    Hive::Markers.set(File.join(draft, "draft.md"), :complete)
+    capture_io { Hive::Commands::Approve.new(slug, from: "draft").call }
+    task_folder(project_root, "3-approval", slug)
+  end
+
+  def decide(slug, outcome, note:)
+    task = Hive::TaskResolver.new(slug).resolve
+    decision_id = Hive::Markers.current(task.state_file).attrs.fetch("decision_id")
+    out, err = capture_io do
+      Hive::Commands::Decide.new(
+        slug, outcome, from: "approval", decision_id: decision_id, note: note, json: true
+      ).call
+    end
+    assert_empty err
+    payload = JSON.parse(out)
+    assert_schema_valid("hive-decide", payload)
+    payload
+  end
+
+  def workflow_descriptor(project_root)
+    File.join(project_root, ".hive-state", "workflows", "editorial.yml")
+  end
+
+  def task_folder(project_root, stage, slug)
+    File.join(project_root, ".hive-state", "stages", stage, slug)
+  end
+
+  def task_folders(project_root)
+    Dir.glob(File.join(project_root, ".hive-state", "stages", "*", "*")).select do |folder|
+      File.file?(File.join(folder, "meta.yml"))
+    end
+  end
+
+  def idempotent_task_folders(project_root, key)
+    task_folders(project_root).select do |folder|
+      Hive::TaskMeta.read(folder)[:idempotency_key] == key
+    end
+  end
+
+  def workflow_snapshot(project_root)
+    root = File.join(project_root, ".hive-state", "workflows")
+    Dir.glob(File.join(root, "**", "*"), File::FNM_DOTMATCH).sort.filter_map do |path|
+      next if File.directory?(path)
+
+      [ path.delete_prefix("#{root}/"), File.binread(path) ]
+    end
+  end
+
+  def repository_snapshot(project_root)
+    {
+      "head" => run!("git", "-C", project_root, "rev-parse", "HEAD").strip,
+      "status" => run!("git", "-C", project_root, "status", "--porcelain"),
+      "branches" => run!("git", "-C", project_root, "branch", "--format=%(refname:short)")
+    }
+  end
+
+  def assert_schema_valid(name, payload)
+    schema = JSONSchemer.schema(JSON.parse(File.read(Hive::Schemas.schema_path(name))))
+    errors = schema.validate(payload).to_a
+    assert_empty errors, "#{name}: #{errors.inspect}"
+  end
+end
diff --git a/test/smoke/live_hive_workflow_creator_smoke_test.rb b/test/smoke/live_hive_workflow_creator_smoke_test.rb
new file mode 100644
index 0000000000..440a5093d4
--- /dev/null
+++ b/test/smoke/live_hive_workflow_creator_smoke_test.rb
@@ -0,0 +1,602 @@
+require "test_helper"
+require "digest"
+require "json"
+require "open3"
+require "pathname"
+require "rbconfig"
+require "timeout"
+require "yaml"
+require_relative "../../packaging/live_agent_skills/proof"
+
+# Authenticated, opt-in proof that OpenClaw discovers the exact candidate
+# /hive skill and follows its workflow-creator route. The success oracle is the
+# controlled Hive command stream plus the authored files and validation graph;
+# agent prose is never retained or trusted.
+class LiveHiveWorkflowCreatorSmokeTest < Minitest::Test
+  include HiveTestHelper
+
+  TIMEOUT_SEC = 240
+  PASSTHROUGH_ENV = %w[
+    PATH LANG LC_ALL TERM TMPDIR SSL_CERT_FILE SSL_CERT_DIR NODE_EXTRA_CA_CERTS
+    HTTP_PROXY HTTPS_PROXY NO_PROXY http_proxy https_proxy no_proxy
+  ].freeze
+
+  def test_openclaw_creates_and_validates_editorial_workflow_without_a_task
+    availability!(ENV["HIVE_LIVE_AGENT_SKILLS"] == "1",
+                  "set HIVE_LIVE_AGENT_SKILLS=1 to run authenticated workflow-creator proof")
+    candidate_sha = ENV["HIVE_CANDIDATE_SHA"].to_s.downcase
+    artifacts = ENV["HIVE_PROOF_ARTIFACTS"].to_s
+    evidence_path = ENV["HIVE_CREATOR_EVIDENCE_PATH"].to_s
+    credential = ENV["OPENAI_API_KEY"].to_s
+    binary = find_executable("openclaw")
+    availability!(HiveLiveAgentProof::SAFE_SHA.match?(candidate_sha),
+                  "HIVE_CANDIDATE_SHA must be a full commit SHA")
+    availability!(File.directory?(artifacts), "HIVE_PROOF_ARTIFACTS is missing")
+    availability!(!evidence_path.empty?, "HIVE_CREATOR_EVIDENCE_PATH is missing")
+    availability!(!credential.empty?, "OPENAI_API_KEY is unavailable")
+    availability!(!binary.nil?, "openclaw binary is not on PATH")
+
+    root = Dir.mktmpdir("hive-live-workflow-creator")
+    FileUtils.chmod(0o700, root)
+    evidence = base_evidence(candidate_sha)
+    failure = nil
+    begin
+      manifest = validate_candidate_artifacts!(artifacts, candidate_sha)
+      skill_root = extract_openclaw_skill!(root, artifacts, manifest)
+      workspace, environment, destination = prepare_openclaw_home(
+        root, skill_root, credential
+      )
+      audit_path = File.join(root, "hive-argv.jsonl")
+      validation_path = File.join(root, "workflow-validation.json")
+      task_proof_path = File.join(root, "task-proof.json")
+      install_controlled_hive(workspace, root, audit_path, validation_path, task_proof_path)
+      environment["PATH"] = [ File.join(root, "bin"), environment.fetch("PATH") ].join(File::PATH_SEPARATOR)
+
+      discovery = discover_openclaw_skill(binary, environment, workspace, destination, credential)
+      stdout, stderr, status = run_bounded(
+        environment,
+        [
+          binary, "agent", "--local", "--agent", "main",
+          "--message", HiveLiveAgentProof::WORKFLOW_CREATOR_PROMPT,
+          "--timeout", "180", "--json"
+        ],
+        chdir: workspace
+      )
+      assert status.success?,
+             "OpenClaw workflow creator failed: #{redact(stderr, credential).lines.first(12).join}\n" \
+             "#{redact(stdout, credential).lines.first(12).join}"
+
+      assert_equal HiveLiveAgentProof::WORKFLOW_CREATOR_COMMANDS.first(5),
+                   read_audited_commands(audit_path)
+      validation = JSON.parse(File.read(validation_path))
+      assert_equal true, validation.fetch("valid")
+      created_files = created_file_records(workspace)
+      assert_equal HiveLiveAgentProof::WORKFLOW_CREATOR_FILES,
+                   created_files.map { |record| record.fetch("path") }.sort
+      assert_equal 0, task_count(workspace)
+      task_stdout, task_stderr, task_status = run_bounded(
+        environment,
+        [
+          binary, "agent", "--local", "--agent", "main",
+          "--message", HiveLiveAgentProof::WORKFLOW_CREATOR_TASK_PROMPT,
+          "--timeout", "180", "--json"
+        ],
+        chdir: workspace
+      )
+      assert task_status.success?,
+             "OpenClaw workflow task retry failed: #{redact(task_stderr, credential).lines.first(12).join}\n" \
+             "#{redact(task_stdout, credential).lines.first(12).join}"
+
+      commands = read_audited_commands(audit_path)
+      assert_equal HiveLiveAgentProof::WORKFLOW_CREATOR_COMMANDS, commands
+      task_proof = JSON.parse(File.read(task_proof_path))
+      assert_equal 1, task_count(workspace)
+      findings = HiveLiveAgentProof.secret_findings("#{stdout}\n#{stderr}", exact_secrets: [ credential ])
+      findings.concat(
+        HiveLiveAgentProof.secret_findings(
+          "#{task_stdout}\n#{task_stderr}", exact_secrets: [ credential ]
+        )
+      )
+      assert_empty findings, "OpenClaw retained output contains credential material"
+
+      projection = JSON.parse(File.read(File.join(skill_root, ".hive-skill.json")))
+      evidence.merge!(
+        "result" => "passed",
+        "prompt_sha256" => Digest::SHA256.hexdigest(HiveLiveAgentProof::WORKFLOW_CREATOR_PROMPT),
+        "task_prompt_sha256" =>
+          Digest::SHA256.hexdigest(HiveLiveAgentProof::WORKFLOW_CREATOR_TASK_PROMPT),
+        "agent" => {
+          "binary" => "openclaw",
+          "native_output_sha256" => Digest::SHA256.hexdigest("#{stdout}\n#{task_stdout}"),
+          "native_stderr_sha256" => Digest::SHA256.hexdigest("#{stderr}\n#{task_stderr}"),
+          "discovery_sha256" => Digest::SHA256.hexdigest(JSON.generate(discovery))
+        },
+        "skill" => {
+          "skill_version" => projection.fetch("skill_version"),
+          "canonical_digest" => projection.fetch("canonical_digest"),
+          "projection_manifest_sha256" =>
+            Digest::SHA256.file(File.join(skill_root, ".hive-skill.json")).hexdigest
+        },
+        "native_activation" => {
+          "kind" => HiveLiveAgentProof::NATIVE_ACTIVATION_KINDS.fetch("openclaw"),
+          "invocation" => HiveLiveAgentProof::INVOCATIONS.fetch("openclaw")
+        },
+        "hive_commands" => commands,
+        "created_files" => created_files,
+        "validation" => normalized_validation(validation),
+        "creation_only_task_count" => 0,
+        "task_count" => 1,
+        "task" => task_proof,
+        "external_actions" => [],
+        "secret_scan" => { "status" => "passed", "scanner" => "hive-live-agent-proof/v1" }
+      )
+    rescue Minitest::Assertion, StandardError => e
+      failure = e
+      evidence["result"] = "failed"
+      evidence["failure"] = redact(e.message.to_s, credential).byteslice(0, 1_000)
+      evidence["secret_scan"] ||= { "status" => "passed", "scanner" => "hive-live-agent-proof/v1" }
+    ensure
+      FileUtils.rm_rf(root)
+      evidence["cleanup"] = { "status" => File.exist?(root) ? "failed" : "passed" }
+      write_evidence(evidence_path, evidence)
+    end
+    raise failure if failure
+  end
+
+  def test_controlled_hive_enforces_the_editorial_graph_and_command_order
+    with_tmp_dir do |root|
+      workspace = File.join(root, "workspace")
+      FileUtils.mkdir_p(workspace)
+      audit_path = File.join(root, "audit.jsonl")
+      validation_path = File.join(root, "validation.json")
+      task_proof_path = File.join(root, "task-proof.json")
+      binary = install_controlled_hive(
+        workspace, root, audit_path, validation_path, task_proof_path
+      )
+
+      run_controlled!(binary, workspace, "version")
+      run_controlled!(binary, workspace, "workflow", "list", "--json")
+      scaffold = JSON.parse(run_controlled!(binary, workspace, "workflow", "new", "editorial", "--json"))
+      author_editorial(scaffold)
+      validation = JSON.parse(
+        run_controlled!(binary, workspace, "workflow", "validate", "editorial", "--json")
+      )
+      run_controlled!(binary, workspace, "workflow", "commit", "editorial")
+      first = JSON.parse(
+        run_controlled!(binary, workspace, *HiveLiveAgentProof::WORKFLOW_CREATOR_TASK_NEW_ARGV)
+      )
+      run_controlled!(binary, workspace, "run", HiveLiveAgentProof::WORKFLOW_CREATOR_TASK_SLUG)
+      retry_payload = JSON.parse(
+        run_controlled!(binary, workspace, *HiveLiveAgentProof::WORKFLOW_CREATOR_TASK_NEW_ARGV)
+      )
+      run_controlled!(binary, workspace, "status", "--operational", "--json")
+      task = JSON.parse(File.read(task_proof_path))
+
+      assert_equal HiveLiveAgentProof::WORKFLOW_CREATOR_COMMANDS, read_audited_commands(audit_path)
+      assert_equal %w[research draft approval], validation.fetch("stages").map { |stage| stage.fetch("name") }
+      assert_equal true, validation.fetch("valid")
+      assert_equal true, first.fetch("created")
+      assert_equal false, retry_payload.fetch("created")
+      assert_equal 1, task.fetch("run_count")
+      assert_equal 1, task_count(workspace)
+    end
+  end
+
+  private
+
+  def availability!(condition, message)
+    return if condition
+    raise Minitest::Assertion, message if ENV["HIVE_RELEASE_GATE"] == "1"
+
+    skip message
+  end
+
+  def base_evidence(candidate_sha)
+    {
+      "schema" => "hive-live-workflow-creator-evidence",
+      "schema_version" => 1,
+      "platform" => "openclaw",
+      "candidate_sha" => candidate_sha,
+      "result" => "failed"
+    }
+  end
+
+  def validate_candidate_artifacts!(root, candidate_sha)
+    manifest = HiveLiveAgentProof.read_json(File.join(root, "artifact-manifest.json"))
+    assert_equal "hive-live-agent-candidate-artifacts", manifest.fetch("schema")
+    assert_equal candidate_sha, manifest.fetch("candidate_sha")
+    manifest.fetch("files").each do |name, record|
+      HiveLiveAgentProof.verify_file_record!(root, name, record)
+    end
+    manifest
+  end
+
+  def extract_openclaw_skill!(root, artifacts, manifest)
+    archive_name = "hive-agent-skills-#{manifest.fetch('candidate_sha')}.tar.gz"
+    archive = HiveLiveAgentProof.relative_file!(artifacts, archive_name)
+    listing, list_error, list_status = Open3.capture3("tar", "-tzf", archive)
+    assert list_status.success?, "cannot list skill archive: #{list_error}"
+    entries = listing.lines.map(&:strip).reject(&:empty?)
+    assert entries.all? { |entry| safe_archive_entry?(entry) },
+           "skill archive contains an unsafe path"
+
+    extract = File.join(root, "projections")
+    FileUtils.mkdir_p(extract, mode: 0o700)
+    _out, extract_error, extract_status = Open3.capture3("tar", "-xzf", archive, "-C", extract)
+    assert extract_status.success?, "cannot extract skill archive: #{extract_error}"
+    skill_root = File.join(extract, "openclaw", "hive")
+    projection = JSON.parse(File.read(File.join(skill_root, ".hive-skill.json")))
+    assert_equal "openclaw", projection.fetch("platform")
+    assert_equal manifest.fetch("skill_version"), projection.fetch("skill_version")
+    assert_equal manifest.fetch("canonical_digest"), projection.fetch("canonical_digest")
+    projection.fetch("files").each do |relative, digest|
+      path = HiveLiveAgentProof.relative_file!(skill_root, relative)
+      assert_equal digest, Digest::SHA256.file(path).hexdigest
+    end
+    skill_root
+  end
+
+  def safe_archive_entry?(entry)
+    normalized = entry.delete_prefix("./")
+    !normalized.empty? && !normalized.start_with?("/") &&
+      !Pathname.new(normalized).each_filename.include?("..")
+  end
+
+  def prepare_openclaw_home(root, skill_root, credential)
+    home = File.join(root, "home")
+    workspace = File.join(root, "workspace")
+    state = File.join(home, ".openclaw")
+    destination = File.join(workspace, "skills", "hive")
+    FileUtils.mkdir_p([ state, workspace, File.dirname(destination) ], mode: 0o700)
+    FileUtils.cp_r(skill_root, destination, preserve: false)
+    model = ENV["HIVE_LIVE_MODEL"].to_s
+    availability!(!model.empty?, "HIVE_LIVE_MODEL is required for OpenClaw proof")
+    config = {
+      "agents" => {
+        "defaults" => { "workspace" => workspace, "model" => { "primary" => model } }
+      },
+      "tools" => { "profile" => "coding" }
+    }
+    config_path = File.join(state, "openclaw.json")
+    secure_write(config_path, JSON.pretty_generate(config) + "\n")
+    environment = PASSTHROUGH_ENV.to_h { |name| [ name, ENV[name] ] }.compact
+    environment.merge!(
+      "HOME" => home,
+      "OPENAI_API_KEY" => credential,
+      "OPENCLAW_STATE_DIR" => state,
+      "OPENCLAW_CONFIG_PATH" => config_path,
+      "HIVE_LIVE_PROOF" => "1"
+    )
+    [ workspace, environment, destination ]
+  end
+
+  def discover_openclaw_skill(binary, environment, workspace, destination, credential)
+    stdout, stderr, status = run_bounded(
+      environment, [ binary, "skills", "info", "hive", "--json" ],
+      chdir: workspace, timeout: 60
+    )
+    assert status.success?, "OpenClaw skill discovery failed: #{redact(stderr, credential)}"
+    info = JSON.parse(stdout)
+    assert_equal "hive", info.fetch("name")
+    assert_equal true, info.fetch("eligible")
+    assert_equal true, info.fetch("userInvocable")
+    assert_equal File.realpath(File.join(destination, "SKILL.md")),
+                 File.realpath(info.fetch("filePath"))
+    {
+      "kind" => "openclaw-skills-info",
+      "name" => info.fetch("name"),
+      "file_path_sha256" => Digest::SHA256.hexdigest(info.fetch("filePath"))
+    }
+  end
+
+  def install_controlled_hive(workspace, root, audit_path, validation_path, task_proof_path)
+    state = File.join(workspace, ".hive-state")
+    workflows = File.join(state, "workflows")
+    FileUtils.mkdir_p(workflows, mode: 0o700)
+    secure_write(
+      File.join(state, "config.yml"),
+      YAML.dump(
+        "project_name" => "workflow-creator-proof",
+        "hive_state_path" => state,
+        "agents" => { "default" => "codex" },
+        "models" => { "default" => "project-default" }
+      )
+    )
+    bin_dir = File.join(root, "bin")
+    FileUtils.mkdir_p(bin_dir, mode: 0o700)
+    script = <<~RUBY
+      #!#{RbConfig.ruby}
+      require "json"
+      require "fileutils"
+      require "yaml"
+
+      allowed = #{HiveLiveAgentProof::WORKFLOW_CREATOR_COMMANDS.inspect}
+      audit = #{audit_path.dump}
+      ordinal = File.file?(audit) ? File.foreach(audit).count : 0
+      unless ARGV == allowed[ordinal]
+        warn "workflow-creator proof expected \#{allowed[ordinal].inspect}, got \#{ARGV.inspect}"
+        exit 64
+      end
+      File.open(audit, File::WRONLY | File::CREAT | File::APPEND, 0o600) do |file|
+        file.puts(JSON.generate("argv" => ARGV, "ordinal" => ordinal + 1))
+      end
+
+      workspace = #{workspace.dump}
+      workflows = File.join(workspace, ".hive-state", "workflows")
+      descriptor = File.join(workflows, "editorial.yml")
+      instruction_dir = File.join(workflows, "editorial")
+      task_dir = File.join(
+        workspace, ".hive-state", "stages", "1-research",
+        #{HiveLiveAgentProof::WORKFLOW_CREATOR_TASK_SLUG.dump}
+      )
+      case ARGV
+      when ["version"]
+        puts #{Hive::VERSION.dump}
+      when ["workflow", "list", "--json"]
+        puts JSON.generate(
+          "schema" => "hive-workflow-list", "schema_version" => 2,
+          "ok" => true, "workflows" => []
+        )
+      when ["workflow", "new", "editorial", "--json"]
+        abort "workflow already exists" if File.exist?(descriptor) || File.exist?(instruction_dir)
+        FileUtils.mkdir_p(instruction_dir)
+        File.write(File.join(instruction_dir, "work.md"), "Describe this stage.\\n")
+        File.write(descriptor, <<~YAML)
+          id: editorial
+          stages:
+            - name: work
+              kind: agent
+              state_file: work.md
+              instruction: editorial/work.md
+              permissions: yolo
+        YAML
+        puts JSON.generate(
+          "schema" => "hive-workflow-new", "schema_version" => 1, "ok" => true,
+          "id" => "editorial", "descriptor_path" => descriptor,
+          "instruction_path" => File.join(instruction_dir, "work.md"),
+          "next" => "hive new workflow-creator-proof --workflow editorial \\"<your idea>\\""
+        )
+      when ["workflow", "validate", "editorial", "--json"]
+        expected = {
+          "id" => "editorial",
+          "stages" => [
+            {
+              "name" => "research", "kind" => "agent", "state_file" => "research.md",
+              "instruction" => "editorial/research.md", "permissions" => "yolo"
+            },
+            {
+              "name" => "draft", "kind" => "agent", "state_file" => "draft.md",
+              "instruction" => "editorial/draft.md", "permissions" => "yolo"
+            },
+            {
+              "name" => "approval", "kind" => "human", "state_file" => "approval.md",
+              "input" => "draft.md",
+              "outcomes" => {
+                "approve" => { "complete" => true, "artifact" => "draft.md" },
+                "reject" => { "to" => "draft" }
+              }
+            }
+          ]
+        }
+        actual = YAML.safe_load(File.read(descriptor), aliases: false)
+        abort "editorial descriptor differs from the accepted graph" unless actual == expected
+        %w[research draft].each do |name|
+          path = File.join(instruction_dir, "\#{name}.md")
+          abort "missing or empty instruction \#{path}" unless File.file?(path) && !File.read(path).strip.empty?
+        end
+        expected_files = %w[draft.md research.md]
+        actual_files = Dir.children(instruction_dir).sort
+        abort "unexpected editorial instruction files: \#{actual_files.inspect}" unless actual_files == expected_files
+        payload = {
+          "schema" => "hive-workflow-validate", "schema_version" => 1,
+          "valid" => true, "id" => "editorial", "origin" => "authored",
+          "descriptor_path" => descriptor,
+          "instruction_paths" => expected_files.map { |name| File.join(instruction_dir, name) },
+          "stages" => [
+            { "name" => "research", "kind" => "agent" },
+            { "name" => "draft", "kind" => "agent" },
+            { "name" => "approval", "kind" => "human" }
+          ],
+          "automatic_edges" => [
+            { "from" => "research", "to" => "draft" },
+            { "from" => "draft", "to" => "approval" }
+          ],
+          "human_outcomes" => [
+            { "stage" => "approval", "name" => "approve", "complete" => true,
+              "artifact" => "draft.md", "to" => nil },
+            { "stage" => "approval", "name" => "reject", "complete" => false,
+              "artifact" => nil, "to" => "draft" }
+          ]
+        }
+        File.write(#{validation_path.dump}, JSON.pretty_generate(payload) + "\\n")
+        puts JSON.generate(payload)
+      when ["workflow", "commit", "editorial"]
+        abort "workflow was not validated before commit" unless File.file?(#{validation_path.dump})
+        puts "hive: committed populated workflow editorial"
+      when #{HiveLiveAgentProof::WORKFLOW_CREATOR_TASK_NEW_ARGV.inspect}
+        created = !File.directory?(task_dir)
+        if created
+          FileUtils.mkdir_p(task_dir)
+          File.write(File.join(task_dir, "research.md"), "<!-- WAITING -->\\n")
+          File.write(
+            File.join(task_dir, "meta.yml"),
+            YAML.dump(
+              "slug" => #{HiveLiveAgentProof::WORKFLOW_CREATOR_TASK_SLUG.dump},
+              "workflow" => "editorial",
+              "idempotency_key" => #{HiveLiveAgentProof::WORKFLOW_CREATOR_TASK_KEY.dump}
+            )
+          )
+          proof = {
+            "slug" => #{HiveLiveAgentProof::WORKFLOW_CREATOR_TASK_SLUG.dump},
+            "workflow" => "editorial", "first_created" => true,
+            "retry_created" => nil, "run_count" => 0, "current_stage" => "1-research"
+          }
+        else
+          proof = JSON.parse(File.read(#{task_proof_path.dump}))
+          proof["retry_created"] = false
+        end
+        File.write(#{task_proof_path.dump}, JSON.pretty_generate(proof) + "\\n")
+        puts JSON.generate(
+          "schema" => "hive-new", "schema_version" => 1, "ok" => true,
+          "created" => created, "slug" => proof.fetch("slug"), "workflow" => "editorial",
+          "current_stage" => "1-research", "task_folder" => task_dir,
+          "next_action" => {
+            "kind" => "run", "command" => "hive run \#{proof.fetch('slug')}"
+          }
+        )
+      when ["run", #{HiveLiveAgentProof::WORKFLOW_CREATOR_TASK_SLUG.dump}]
+        abort "task is missing" unless File.directory?(task_dir)
+        proof = JSON.parse(File.read(#{task_proof_path.dump}))
+        abort "first stage was dispatched more than once" unless proof.fetch("run_count") == 0
+        proof["run_count"] = 1
+        File.write(#{task_proof_path.dump}, JSON.pretty_generate(proof) + "\\n")
+        puts JSON.generate(
+          "schema" => "hive-run", "schema_version" => 1, "ok" => true,
+          "slug" => proof.fetch("slug"), "current_stage" => "1-research"
+        )
+      when ["status", "--operational", "--json"]
+        proof = JSON.parse(File.read(#{task_proof_path.dump}))
+        abort "idempotent retry did not occur" unless proof["retry_created"] == false
+        File.write(#{task_proof_path.dump}, JSON.pretty_generate(proof) + "\\n")
+        puts JSON.generate(
+          "schema" => "hive-operational-status", "schema_version" => 1,
+          "tasks" => [
+            {
+              "identity" => { "slug" => proof.fetch("slug") },
+              "position" => { "stage" => "1-research" },
+              "runtime" => { "daemon_disposition" => "manual" },
+              "transition" => { "next" => "draft" }
+            }
+          ]
+        )
+      end
+    RUBY
+    path = File.join(bin_dir, "hive")
+    secure_write(path, script)
+    FileUtils.chmod(0o700, path)
+    path
+  end
+
+  def author_editorial(scaffold)
+    descriptor = scaffold.fetch("descriptor_path")
+    instruction_dir = File.dirname(scaffold.fetch("instruction_path"))
+    FileUtils.rm_f(File.join(instruction_dir, "work.md"))
+    secure_write(File.join(instruction_dir, "research.md"), "Research and write research.md.\n")
+    secure_write(File.join(instruction_dir, "draft.md"), "Draft from research.md into draft.md.\n")
+    secure_write(descriptor, <<~YAML)
+      id: editorial
+      stages:
+        - name: research
+          kind: agent
+          state_file: research.md
+          instruction: editorial/research.md
+          permissions: yolo
+        - name: draft
+          kind: agent
+          state_file: draft.md
+          instruction: editorial/draft.md
+          permissions: yolo
+        - name: approval
+          kind: human
+          state_file: approval.md
+          input: draft.md
+          outcomes:
+            approve:
+              complete: true
+              artifact: draft.md
+            reject:
+              to: draft
+    YAML
+  end
+
+  def run_controlled!(binary, workspace, *argv)
+    stdout, stderr, status = Open3.capture3(binary, *argv, chdir: workspace)
+    assert status.success?, stderr
+    stdout
+  end
+
+  def read_audited_commands(path)
+    return [] unless File.file?(path)
+
+    File.readlines(path, chomp: true).map { |line| JSON.parse(line).fetch("argv") }
+  end
+
+  def normalized_validation(payload)
+    {
+      "valid" => payload.fetch("valid"),
+      "stages" => payload.fetch("stages").map { |stage| stage.fetch("name") },
+      "automatic_edges" =>
+        payload.fetch("automatic_edges").map { |edge| edge.values_at("from", "to") },
+      "human_outcomes" => payload.fetch("human_outcomes")
+    }
+  end
+
+  def created_file_records(workspace)
+    HiveLiveAgentProof::WORKFLOW_CREATOR_FILES.map do |relative|
+      path = File.join(workspace, relative)
+      assert File.file?(path), "missing created file #{relative}"
+      {
+        "path" => relative,
+        "sha256" => Digest::SHA256.file(path).hexdigest,
+        "size" => File.size(path)
+      }
+    end
+  end
+
+  def task_count(workspace)
+    Dir.glob(File.join(workspace, ".hive-state", "stages", "*", "*"))
+       .count { |path| File.directory?(path) }
+  end
+
+  def run_bounded(environment, argv, chdir:, timeout: TIMEOUT_SEC)
+    stdout = +""
+    stderr = +""
+    status = nil
+    Open3.popen3(environment, *argv, chdir: chdir, pgroup: true, unsetenv_others: true) do |stdin, out, err, wait|
+      stdin.close
+      out_reader = Thread.new { out.read }
+      err_reader = Thread.new { err.read }
+      begin
+        Timeout.timeout(timeout) { status = wait.value }
+      rescue Timeout::Error
+        Process.kill("TERM", -wait.pid)
+        status = wait.value
+        raise Minitest::Assertion, "#{File.basename(argv.first)} exceeded #{timeout}s proof timeout"
+      ensure
+        stdout = out_reader.value
+        stderr = err_reader.value
+      end
+    end
+    [ stdout, stderr, status ]
+  end
+
+  def redact(text, *secrets)
+    redacted = text.to_s.dup
+    secrets.flatten.compact.reject(&:empty?).each { |secret| redacted.gsub!(secret, "[REDACTED]") }
+    HiveLiveAgentProof::SECRET_PATTERNS.each { |pattern| redacted.gsub!(pattern, "[REDACTED]") }
+    redacted
+  end
+
+  def secure_write(path, body)
+    FileUtils.mkdir_p(File.dirname(path), mode: 0o700)
+    File.open(path, File::WRONLY | File::CREAT | File::TRUNC, 0o600) { |file| file.write(body) }
+  end
+
+  def write_evidence(path, evidence)
+    return if path.to_s.empty?
+
+    findings = HiveLiveAgentProof.secret_findings(JSON.generate(evidence))
+    evidence["secret_scan"] = {
+      "status" => "failed", "scanner" => "hive-live-agent-proof/v1"
+    } unless findings.empty?
+    HiveLiveAgentProof.write_json(path, evidence)
+  end
+
+  def find_executable(name)
+    ENV.fetch("PATH", "").split(File::PATH_SEPARATOR).each do |directory|
+      path = File.join(directory, name)
+      return File.expand_path(path) if File.file?(path) && File.executable?(path)
+    end
+    nil
+  end
+end
diff --git a/test/unit/agent_skills/canonical_skill_test.rb b/test/unit/agent_skills/canonical_skill_test.rb
index 55d53c2480..bde42a9e06 100644
--- a/test/unit/agent_skills/canonical_skill_test.rb
+++ b/test/unit/agent_skills/canonical_skill_test.rb
@@ -17,13 +17,38 @@ def test_canonical_metadata_frontmatter_and_references_are_strict
     assert_match(/\A[0-9a-f]{64}\z/, skill.canonical_digest)
     assert_equal %w[description name], skill.frontmatter.keys.sort
     assert_operator skill.body.lines.size, :<, 120
-    assert_equal 6, skill.reference_paths.size
+    assert_equal 14, skill.reference_paths.size
     assert skill.reference_paths.all? { |path| path.start_with?("references/") }
     refute_includes skill.rendered_canonical_files.values.join("\n"), "{{HIVE_VERSION}}"
     assert_includes skill.rendered_canonical_files.fetch("references/setup-and-platforms.md"),
                     "/v#{Hive::VERSION}/install.sh"
   end
 
+  def test_workflow_creator_contract_gates_mutation_and_reports_every_side_effect
+    files = Hive::AgentSkills::CanonicalSkill.new.rendered_canonical_files
+    route = files.fetch("SKILL.md")
+    creator = files.fetch("references/workflow-creator.md")
+
+    assert_includes route, "hive-workflow-creator"
+    assert_includes creator, "minimum Hive version: #{Hive::VERSION}"
+    assert_operator creator.index("hive version"), :<, creator.index("hive workflow list --json")
+    assert_operator creator.index("hive workflow list --json"), :<, creator.index("hive workflow new")
+    assert_operator creator.index("hive workflow validate"), :<, creator.index("hive workflow commit")
+    assert_operator creator.index("hive workflow commit"), :<, creator.index("hive new")
+    assert_includes creator, "No task by default"
+    assert_includes creator, "created files"
+    assert_includes creator, "applied defaults"
+    assert_includes creator, "validation result"
+    assert_includes creator, "hive update"
+    assert_includes creator, "idempotency-key"
+    assert_includes creator, "workflow-creator:v1:<64 lowercase hex>"
+    assert_includes creator, "JSON.generate([project, workflow, request])"
+    assert_includes creator, "POSIX `Shellwords.escape` semantics"
+    assert_includes creator, %q(replace each embedded `'` with the exact shell sequence `'"'"'`)
+    assert_includes creator, "Never publish externally"
+    refute_match(/hive init[^\n]*--force/, creator)
+  end
+
   def test_four_platform_projections_share_payload_but_keep_thin_wrappers
     skill = Hive::AgentSkills::CanonicalSkill.new
     projections = %w[openclaw claude codex pi].to_h do |platform|
diff --git a/test/unit/cli_test.rb b/test/unit/cli_test.rb
index 730ad6a0cd..effba71054 100644
--- a/test/unit/cli_test.rb
+++ b/test/unit/cli_test.rb
@@ -96,7 +96,7 @@ def test_init_forget_prune_update_uninstall_and_migrate_pass_options
       assert_equal [ "/tmp/project" ], calls.first.fetch(:args)
       assert_equal({
                      force: true, json: true, workflow: "content_fixture",
-                     new_workflow: nil, refactor_patrol: nil
+                     new_workflow: nil, refactor_patrol: nil, minimal: false, preview: false
                    },
                    calls.first.fetch(:kwargs))
       assert_equal :call, calls.last
@@ -107,12 +107,22 @@ def test_init_forget_prune_update_uninstall_and_migrate_pass_options
       assert_equal [ "/tmp/project" ], calls.first.fetch(:args)
       assert_equal({
                      force: false, json: false, workflow: nil,
-                     new_workflow: "writing", refactor_patrol: nil
+                     new_workflow: "writing", refactor_patrol: nil, minimal: false, preview: false
                    },
                    calls.first.fetch(:kwargs))
       assert_equal :call, calls.last
     end
 
+    with_command_new_stub(Hive::Commands::Init) do |calls|
+      Hive::CLI.start([
+        "init", "/tmp/project", "--new-workflow", "writing", "--minimal", "--preview", "--json"
+      ])
+      assert_equal true, calls.first.fetch(:kwargs).fetch(:minimal)
+      assert_equal true, calls.first.fetch(:kwargs).fetch(:preview)
+      assert_equal true, calls.first.fetch(:kwargs).fetch(:json)
+      assert_equal :call, calls.last
+    end
+
     with_command_new_stub(Hive::Commands::Init) do |calls|
       Hive::CLI.start([ "init", "/tmp/project", "--no-refactor-patrol" ])
       assert_equal false, calls.first.fetch(:kwargs).fetch(:refactor_patrol)
@@ -250,13 +260,25 @@ def test_new_task_dispatches_joined_text_and_rejects_blank_text
     with_command_new_stub(Hive::Commands::New) do |calls|
       Hive::CLI.start([ "new", "proj", "build", "thing" ])
       assert_equal [ "proj", "build thing" ], calls.first.fetch(:args)
-      assert_equal({ base: nil, depends_on: nil, workflow: nil }, calls.first.fetch(:kwargs))
+      assert_equal(
+        { base: nil, depends_on: nil, workflow: nil, idempotency_key: nil, json: false },
+        calls.first.fetch(:kwargs)
+      )
     end
 
     with_command_new_stub(Hive::Commands::New) do |calls|
-      Hive::CLI.start([ "new", "proj", "--depends-on", "base-task", "--workflow", "content_fixture", "build", "thing" ])
+      Hive::CLI.start([
+        "new", "proj", "--depends-on", "base-task", "--workflow", "content_fixture",
+        "--idempotency-key", "creator:v1", "--json", "build", "thing"
+      ])
       assert_equal [ "proj", "build thing" ], calls.first.fetch(:args)
-      assert_equal({ base: nil, depends_on: "base-task", workflow: "content_fixture" }, calls.first.fetch(:kwargs))
+      assert_equal(
+        {
+          base: nil, depends_on: "base-task", workflow: "content_fixture",
+          idempotency_key: "creator:v1", json: true
+        },
+        calls.first.fetch(:kwargs)
+      )
     end
 
     _out, err, status = with_captured_exit { Hive::CLI.start([ "new", "proj" ]) }
diff --git a/test/unit/commands/approve_test.rb b/test/unit/commands/approve_test.rb
index 7197097469..023d638703 100644
--- a/test/unit/commands/approve_test.rb
+++ b/test/unit/commands/approve_test.rb
@@ -39,6 +39,122 @@ def test_observation_guard_runs_under_commit_and_task_locks_before_move
     end
   end
 
+  def test_restore_human_destination_handles_existing_new_and_missing_folders
+    with_tmp_dir do |root|
+      source = File.join(root, "source")
+      destination = File.join(root, "destination")
+      FileUtils.mkdir_p(source)
+      FileUtils.mkdir_p(destination)
+      current = task(folder: source)
+      state = File.join(source, "approval.md")
+      File.write(state, "changed")
+
+      command.send(
+        :restore_human_destination!, current, destination,
+        { state_file: "approval.md", existed: true, body: "original" }
+      )
+      assert_equal "original", File.read(state)
+
+      FileUtils.rm_rf(source)
+      destination_state = File.join(destination, "approval.md")
+      File.write(destination_state, "created")
+      command.send(
+        :restore_human_destination!, current, destination,
+        { state_file: "approval.md", existed: false, body: nil }
+      )
+      refute File.exist?(destination_state)
+
+      FileUtils.rm_rf(destination)
+      assert_nil command.send(
+        :restore_human_destination!, current, destination,
+        { state_file: "approval.md", existed: false, body: nil }
+      )
+      assert_nil command.send(:restore_human_destination!, current, destination, nil)
+    end
+  end
+
+  def test_initialize_human_destination_refuses_symlinked_state_file
+    with_tmp_dir do |root|
+      source = File.join(root, "source")
+      outside = File.join(root, "outside.md")
+      FileUtils.mkdir_p(source)
+      File.write(outside, "external bytes\n")
+      File.symlink(outside, File.join(source, "approval.md"))
+      current = task(folder: source, workflow: human_workflow)
+
+      error = assert_raises(Hive::InvalidTaskPath) do
+        command.send(:initialize_human_destination!, current, "2-approval")
+      end
+
+      assert_includes error.message, "must be a regular file, not a symlink"
+      assert_equal "external bytes\n", File.read(outside)
+      assert File.symlink?(File.join(source, "approval.md"))
+    end
+  end
+
+  def test_human_state_snapshot_rejects_file_swaps_and_nofollow_errors
+    with_tmp_dir do |root|
+      path = File.join(root, "approval.md")
+      File.write(path, "owner approval\n")
+      observed = File.lstat(path)
+      swapped_stat = Struct.new(:dev, :ino) do
+        def file? = true
+      end.new(observed.dev, observed.ino + 1)
+      swapped_file = Object.new
+      swapped_file.define_singleton_method(:stat) { swapped_stat }
+      swapped_file.define_singleton_method(:read) { "replacement\n" }
+
+      with_replaced_singleton_method(
+        File, :open, ->(_path, *_args, &block) { block.call(swapped_file) }
+      ) do
+        error = assert_raises(Hive::InvalidTaskPath) do
+          command.send(:read_human_state_snapshot!, path, "approval.md")
+        end
+        assert_includes error.message, "changed while entering the stage"
+      end
+
+      with_replaced_singleton_method(
+        File, :open, ->(*_args) { raise Errno::ELOOP, path }
+      ) do
+        error = assert_raises(Hive::InvalidTaskPath) do
+          command.send(:read_human_state_snapshot!, path, "approval.md")
+        end
+        assert_includes error.message, "must be a regular file, not a symlink"
+      end
+    end
+  end
+
+  def test_interrupted_human_stage_commit_restores_move_and_destination_state
+    with_tmp_dir do |root|
+      state = File.join(root, ".hive-state")
+      source = File.join(state, "stages", "1-draft", "slug-260522-abcd")
+      destination = File.join(state, "stages", "2-approval", "slug-260522-abcd")
+      FileUtils.mkdir_p(source)
+      File.write(File.join(source, "draft.md"), "# Draft\n")
+      original = "# Approval instructions\n"
+      File.write(File.join(source, "approval.md"), original)
+      current = task(
+        folder: source, hive_state_path: state, project_root: root,
+        stage_index: 1, stage_name: "draft", workflow: human_workflow
+      )
+      cmd = command
+      cmd.define_singleton_method(:record_hive_commit) { |*| raise Interrupt, "stop" }
+      fake_ops = Object.new
+      fake_ops.define_singleton_method(:run_git!) { |*| nil }
+
+      with_replaced_singleton_method(Hive::GitOps, :new, ->(*) { fake_ops }) do
+        assert_raises(Interrupt) do
+          cmd.send(:perform_move_and_commit, current, "2-approval")
+        end
+      end
+
+      assert File.directory?(source)
+      refute File.exist?(destination)
+      assert_equal original, File.binread(File.join(source, "approval.md"))
+      assert_equal :none, Hive::Markers.current(File.join(source, "approval.md")).name
+    end
+  end
+
   def test_inert_terminal_entry_writes_completed_at_with_the_move
     with_tmp_dir do |root|
       state = File.join(root, ".hive-state")
@@ -204,6 +320,25 @@ def terminal_workflow
     )
   end
 
+  def human_workflow
+    Hive::Workflow.new(
+      id: :editorial,
+      stages: [
+        Hive::Workflow::Stage.new(
+          name: "draft", index: 1, state_file: "draft.md", kind: :agent
+        ),
+        Hive::Workflow::Stage.new(
+          name: "approval", index: 2, state_file: "approval.md", kind: :human,
+          outcomes: {
+            "approve" => Hive::Workflow::Outcome.new(
+              name: "approve", complete: true, artifact: "draft.md"
+            )
+          }.freeze
+        )
+      ]
+    )
+  end
+
   def failing_status
     status = Object.new
     status.define_singleton_method(:success?) { false }
diff --git a/test/unit/commands/init/prompts_test.rb b/test/unit/commands/init/prompts_test.rb
index a8f85ad046..f7b879a312 100644
--- a/test/unit/commands/init/prompts_test.rb
+++ b/test/unit/commands/init/prompts_test.rb
@@ -122,6 +122,23 @@ def test_non_tty_returns_recommended_defaults
     assert_equal all_defaults, answers
   end
 
+  def test_minimal_defaults_disable_optional_automation
+    answers = Hive::Commands::Init::Prompts.minimal_defaults
+
+    assert_equal [], answers.fetch("enabled_reviewers")
+    assert_equal [], answers.fetch("patrol_reviewers")
+    assert_equal "off", answers.fetch("patrol_mode")
+    assert_equal false, answers.fetch("adhoc_auto_fix")
+    assert_equal false, answers.fetch("refactor_patrol_enabled")
+    assert_equal false, answers.fetch("daemon_enabled")
+    assert_equal false, answers.fetch("babysitter_enabled")
+    assert_equal false, answers.fetch("daemon_autostart")
+    assert_equal Hive::Commands::Init::Prompts::LIMIT_KEYS.sort,
+                 answers.fetch("budgets").keys.sort
+    assert_equal Hive::Commands::Init::Prompts::LIMIT_KEYS.sort,
+                 answers.fetch("timeouts").keys.sort
+  end
+
   def test_non_tty_recommends_refactor_patrol_without_arming_legacy_config
     prompts, _output = make_prompts("", tty: false)
     answers = prompts.collect
diff --git a/test/unit/commands/run_test.rb b/test/unit/commands/run_test.rb
index 2c0b9b31a7..4b6fb8eb1f 100644
--- a/test/unit/commands/run_test.rb
+++ b/test/unit/commands/run_test.rb
@@ -657,6 +657,33 @@ def test_report_text_covers_manual_and_stale_guidance
     assert_includes out, "manual steering active"
   end
 
+  def test_report_text_human_stage_lists_decision_outcomes
+    outcomes = {
+      "approve" => Hive::Workflow::Outcome.new(name: "approve", complete: true, artifact: "draft.md"),
+      "reject" => Hive::Workflow::Outcome.new(name: "reject", to: "draft")
+    }.freeze
+    workflow = Hive::Workflow.new(
+      id: :editorial,
+      stages: [
+        Hive::Workflow::Stage.new(
+          name: "draft", index: 1, state_file: "draft.md", kind: :agent
+        ),
+        Hive::Workflow::Stage.new(
+          name: "approval", index: 2, state_file: "approval.md", kind: :human,
+          outcomes: outcomes
+        )
+      ]
+    )
+    t = task(stage_name: "approval", stage_index: 2, workflow: workflow)
+
+    out, = capture_io do
+      command.send(:report_text, t, {}, marker(:waiting))
+    end
+
+    assert_includes out, "outcomes: approve, reject"
+    assert_includes out, "hive decide some-slug <outcome> --from approval --decision-id"
+  end
+
   def test_report_text_review_error_prints_phase_and_reason_before_raising
     run = command
     t = task
diff --git a/test/unit/commands/workflow_new_test.rb b/test/unit/commands/workflow_new_test.rb
index e8effe4b9f..76bbac4213 100644
--- a/test/unit/commands/workflow_new_test.rb
+++ b/test/unit/commands/workflow_new_test.rb
@@ -60,6 +60,73 @@ def test_scaffolds_blank_workflow_descriptor_and_instruction
     end
   end
 
+  def test_commit_validates_and_commits_populated_authored_workflow
+    with_initialized_project do |project_root|
+      Hive::Commands::Workflow.new!(
+        "my-flow", project_root: project_root, stdout: StringIO.new
+      )
+      instruction = File.join(
+        project_root, ".hive-state", "workflows", "my-flow", "work.md"
+      )
+      File.write(instruction, "Write the owner-defined deliverable.\n")
+      stdout = StringIO.new
+
+      payload = Hive::Commands::Workflow.new(
+        "commit", "my-flow", project_root: project_root, stdout: stdout
+      ).call!
+
+      assert_equal true, payload.fetch("committed")
+      assert_includes stdout.string, "committed populated workflow my-flow"
+      state = File.join(project_root, ".hive-state")
+      assert_equal "hive: workflows/my-flow defined",
+                   run!("git", "-C", state, "log", "-1", "--format=%s").strip
+      assert_equal "Write the owner-defined deliverable.\n",
+                   run!("git", "-C", state, "show", "HEAD:workflows/my-flow/work.md")
+      assert_empty run!(
+        "git", "-C", state, "status", "--porcelain", "--",
+        "workflows/my-flow.yml", "workflows/my-flow"
+      )
+
+      retry_stdout = StringIO.new
+      retry_payload = Hive::Commands::Workflow.new(
+        "commit", "my-flow", project_root: project_root, stdout: retry_stdout
+      ).call!
+      assert_equal false, retry_payload.fetch("committed")
+      assert_includes retry_stdout.string, "populated workflow my-flow is already committed"
+    end
+  end
+
+  def test_failed_populated_workflow_commit_clears_its_staged_entries
+    with_initialized_project do |project_root|
+      Hive::Commands::Workflow.new!(
+        "my-flow", project_root: project_root, stdout: StringIO.new
+      )
+      state = File.join(project_root, ".hive-state")
+      instruction = File.join(state, "workflows", "my-flow", "work.md")
+      File.write(instruction, "Changed owner instructions.\n")
+      runner = method(:run!)
+      fake_ops = Object.new
+      fake_ops.define_singleton_method(:hive_commit) do |pathspecs:, **|
+        runner.call("git", "-C", state, "add", "--", *pathspecs)
+        raise Hive::GitError, "commit failed after staging"
+      end
+      fake_ops.define_singleton_method(:run_git!) { |*args| runner.call("git", *args) }
+
+      with_replaced_singleton_method(Hive::GitOps, :new, ->(_root) { fake_ops }) do
+        assert_raises(Hive::GitError) do
+          Hive::Commands::Workflow.new(
+            "commit", "my-flow", project_root: project_root, stdout: StringIO.new
+          ).call!
+        end
+      end
+
+      assert_equal "Changed owner instructions.\n", File.read(instruction)
+      assert_empty run!("git", "-C", state, "diff", "--cached", "--name-only")
+      assert_includes run!("git", "-C", state, "status", "--porcelain"),
+                      "workflows/my-flow/work.md"
+    end
+  end
+
   def test_scaffolds_keyword_like_id_with_quoted_descriptor_so_it_parses
     # `no`/`yes`/`off` are valid SAFE_SLUG ids that YAML.safe_load coerces to
     # booleans when the descriptor's `id:` is emitted unquoted, failing
@@ -181,6 +248,48 @@ def test_refuses_to_overwrite_existing_scaffold
     end
   end
 
+  def test_refuses_dangling_descriptor_and_instruction_symlink_collisions
+    with_initialized_project do |project_root|
+      workflows = File.join(project_root, ".hive-state", "workflows")
+      outside = File.join(project_root, "outside-target")
+      FileUtils.mkdir_p(workflows)
+
+      {
+        "linked-descriptor" => File.join(workflows, "linked-descriptor.yml"),
+        "linked-instructions" => File.join(workflows, "linked-instructions")
+      }.each do |id, collision|
+        File.symlink(outside, collision)
+        error = assert_raises(Hive::Commands::Workflow::UsageError) do
+          Hive::Commands::Workflow.new!(id, project_root: project_root, stdout: StringIO.new)
+        end
+
+        assert_includes error.message, "already exists"
+        assert File.symlink?(collision)
+        refute File.exist?(outside)
+      end
+    end
+  end
+
+  def test_refuses_a_symlinked_workflow_scaffold_root
+    with_initialized_project do |project_root|
+      workflows = File.join(project_root, ".hive-state", "workflows")
+      outside = File.join(project_root, "outside-workflows")
+      FileUtils.rm_rf(workflows)
+      FileUtils.mkdir_p(outside)
+      File.symlink(outside, workflows)
+
+      error = assert_raises(Hive::ConfigError) do
+        Hive::Commands::Workflow.new!(
+          "my-flow", project_root: project_root, stdout: StringIO.new
+        )
+      end
+
+      assert_includes error.message, "scaffold root"
+      assert File.symlink?(workflows)
+      assert_empty Dir.children(outside)
+    end
+  end
+
   def test_json_error_payload
     with_initialized_project do |project_root|
       out, err, status = with_captured_exit do
@@ -240,7 +349,7 @@ def test_json_envelopes_validate_against_published_schema
         Hive::Commands::Workflow.new(nil, nil, project_root: project_root, json: true).call
       end
       missing_payload = JSON.parse(missing_out)
-      assert_equal %w[new install list update remove publish], missing_payload.fetch("expected")
+      assert_equal %w[new validate commit install list update remove publish], missing_payload.fetch("expected")
       missing_errors = schemer.validate(missing_payload).map { |e| e["error"] }
       assert_empty missing_errors,
                    "hive-workflow-new usage ErrorPayload (with `expected`) must validate " \
@@ -251,7 +360,7 @@ def test_json_envelopes_validate_against_published_schema
       end
       unknown_payload = JSON.parse(unknown_out)
       assert_equal "bogus", unknown_payload.fetch("value")
-      assert_equal %w[new install list update remove publish], unknown_payload.fetch("expected")
+      assert_equal %w[new validate commit install list update remove publish], unknown_payload.fetch("expected")
       unknown_errors = schemer.validate(unknown_payload).map { |e| e["error"] }
       assert_empty unknown_errors,
                    "hive-workflow-new usage ErrorPayload (with `value` AND `expected`) must " \
@@ -302,6 +411,55 @@ def test_json_git_error_payload_classifies_git
     end
   end
 
+  def test_failed_scaffold_commit_clears_its_staged_index_entries
+    with_initialized_project do |project_root|
+      hive_state = File.join(project_root, ".hive-state")
+      runner = method(:run!)
+      fake_ops = Object.new
+      fake_ops.define_singleton_method(:hive_commit) do |pathspecs:, **|
+        runner.call("git", "-C", hive_state, "add", "--", *pathspecs)
+        raise Hive::GitError, "commit failed after staging"
+      end
+      fake_ops.define_singleton_method(:run_git!) { |*args| runner.call("git", *args) }
+
+      with_replaced_singleton_method(Hive::GitOps, :new, ->(_root) { fake_ops }) do
+        assert_raises(Hive::GitError) do
+          Hive::Commands::Workflow.new!(
+            "failed-flow", project_root: project_root, stdout: StringIO.new
+          )
+        end
+      end
+
+      refute File.exist?(File.join(hive_state, "workflows", "failed-flow.yml"))
+      assert_empty run!("git", "-C", hive_state, "diff", "--cached", "--name-only")
+    end
+  end
+
+  def test_interrupted_scaffold_commit_clears_staged_entries_and_owned_files
+    with_initialized_project do |project_root|
+      hive_state = File.join(project_root, ".hive-state")
+      runner = method(:run!)
+      fake_ops = Object.new
+      fake_ops.define_singleton_method(:hive_commit) do |pathspecs:, **|
+        runner.call("git", "-C", hive_state, "add", "--", *pathspecs)
+        raise Interrupt, "stop"
+      end
+      fake_ops.define_singleton_method(:run_git!) { |*args| runner.call("git", *args) }
+
+      with_replaced_singleton_method(Hive::GitOps, :new, ->(_root) { fake_ops }) do
+        assert_raises(Interrupt) do
+          Hive::Commands::Workflow.new!(
+            "interrupted-flow", project_root: project_root, stdout: StringIO.new
+          )
+        end
+      end
+
+      refute File.exist?(File.join(hive_state, "workflows", "interrupted-flow.yml"))
+      refute File.exist?(File.join(hive_state, "workflows", "interrupted-flow"))
+      assert_empty run!("git", "-C", hive_state, "diff", "--cached", "--name-only")
+    end
+  end
+
   def test_json_concurrent_run_error_payload_classifies_concurrent
     with_initialized_project do |project_root|
       out, err, status = with_replaced_singleton_method(
@@ -322,14 +480,14 @@ def test_json_concurrent_run_error_payload_classifies_concurrent
   def test_json_disk_write_error_rides_the_envelope_as_error_kind
     with_initialized_project do |project_root|
       out, err, status = with_replaced_singleton_method(
-        FileUtils, :mkdir_p, ->(*_args) { raise Errno::EACCES, "denied" }
+        Dir, :mkdir, ->(*_args) { raise Errno::EACCES, "denied" }
       ) do
         with_captured_exit do
           Hive::Commands::Workflow.new("new", "disk-flow", project_root: project_root, json: true).call
         end
       end
 
-      # The disk fault from write_scaffold!'s mkdir_p must ride the JSON envelope
+      # The disk fault from write_scaffold!'s exclusive mkdir must ride the JSON envelope
       # (error_kind=error, no raw backtrace on stderr), not escape to bin/hive.
       assert_empty err
       payload = JSON.parse(out)
@@ -434,6 +592,32 @@ def test_scaffold_files_rolls_back_when_generated_descriptor_fails_validation
     end
   end
 
+  def test_scaffold_race_preserves_a_foreign_descriptor
+    with_initialized_project do |project_root|
+      workflows = File.join(project_root, ".hive-state", "workflows")
+      instruction_dir = File.join(workflows, "raced-flow")
+      descriptor = File.join(workflows, "raced-flow.yml")
+      original_mkdir = Dir.method(:mkdir)
+
+      replacement = lambda do |path, *args|
+        result = original_mkdir.call(path, *args)
+        File.write(descriptor, "foreign descriptor\n") if path == instruction_dir
+        result
+      end
+      with_replaced_singleton_method(Dir, :mkdir, replacement) do
+        assert_raises(Errno::EEXIST) do
+          Hive::Commands::Workflow.scaffold_files!("raced-flow", project_root: project_root)
+        end
+      end
+
+      assert_equal "foreign descriptor\n", File.read(descriptor)
+      refute File.exist?(instruction_dir),
+             "rollback should remove only the instruction directory this invocation created"
+    ensure
+      FileUtils.rm_f(descriptor) if descriptor
+    end
+  end
+
   def test_rolls_back_files_when_generated_descriptor_fails_validation
     with_initialized_project do |project_root|
       error = Hive::ConfigError.new("boom")
@@ -459,7 +643,7 @@ def test_call_reports_unknown_subcommand_as_usage
 
       assert_equal Hive::ExitCodes::USAGE, status
       assert_empty out
-      assert_equal "hive workflow: unknown workflow subcommand \"save\" (expected: new, install, list, update, remove, publish)\n", err
+      assert_equal "hive workflow: unknown workflow subcommand \"save\" (expected: new, validate, commit, install, list, update, remove, publish)\n", err
     end
   end
 
@@ -471,7 +655,7 @@ def test_call_reports_missing_subcommand_as_usage
 
       assert_equal Hive::ExitCodes::USAGE, status
       assert_empty out
-      assert_equal "hive workflow: missing SUBCOMMAND (expected: new, install, list, update, remove, publish)\n", err
+      assert_equal "hive workflow: missing SUBCOMMAND (expected: new, validate, commit, install, list, update, remove, publish)\n", err
     end
   end
 
@@ -488,8 +672,8 @@ def test_json_reports_missing_subcommand_with_expected_values
       assert_equal "UsageError", payload.fetch("error_class")
       assert_equal "usage", payload.fetch("error_kind")
       assert_equal Hive::ExitCodes::USAGE, payload.fetch("exit_code")
-      assert_equal "missing SUBCOMMAND (expected: new, install, list, update, remove, publish)", payload.fetch("message")
-      assert_equal %w[new install list update remove publish], payload.fetch("expected")
+      assert_equal "missing SUBCOMMAND (expected: new, validate, commit, install, list, update, remove, publish)", payload.fetch("message")
+      assert_equal %w[new validate commit install list update remove publish], payload.fetch("expected")
       refute payload.key?("value")
     end
   end
@@ -507,9 +691,9 @@ def test_json_reports_unknown_subcommand_with_value_and_expected_values
       assert_equal "UsageError", payload.fetch("error_class")
       assert_equal "usage", payload.fetch("error_kind")
       assert_equal Hive::ExitCodes::USAGE, payload.fetch("exit_code")
-      assert_equal "unknown workflow subcommand \"bogus\" (expected: new, install, list, update, remove, publish)", payload.fetch("message")
+      assert_equal "unknown workflow subcommand \"bogus\" (expected: new, validate, commit, install, list, update, remove, publish)", payload.fetch("message")
       assert_equal "bogus", payload.fetch("value")
-      assert_equal %w[new install list update remove publish], payload.fetch("expected")
+      assert_equal %w[new validate commit install list update remove publish], payload.fetch("expected")
     end
   end
 
@@ -570,6 +754,23 @@ def test_rollback_scaffold_swallows_epipe_from_the_cleanup_warning
     end
   end
 
+  def test_rollback_scaffold_supports_legacy_path_sets_without_ownership_metadata
+    with_tmp_dir do |root|
+      instruction_dir = File.join(root, "legacy-flow")
+      descriptor = File.join(root, "legacy-flow.yml")
+      FileUtils.mkdir_p(instruction_dir)
+      File.write(File.join(instruction_dir, "work.md"), "work\n")
+      File.write(descriptor, "id: legacy-flow\n")
+
+      Hive::Commands::Workflow.rollback_scaffold(
+        instruction_dir: instruction_dir, descriptor: descriptor
+      )
+
+      refute File.exist?(instruction_dir)
+      refute File.exist?(descriptor)
+    end
+  end
+
   private
 
     def with_initialized_project
diff --git a/test/unit/gemspec_test.rb b/test/unit/gemspec_test.rb
index 1729ed117c..272dc85370 100644
--- a/test/unit/gemspec_test.rb
+++ b/test/unit/gemspec_test.rb
@@ -43,6 +43,24 @@ def test_gem_package_includes_every_canonical_hive_skill_asset
     expected.each { |path| assert_includes spec.files, path }
   end
 
+  def test_gem_package_includes_workflow_creator_references
+    spec = Gem::Specification.load(GEMSPEC_PATH)
+    references = %w[
+      workflow-creator.md
+      workflow-creator-example.md
+      workflow-schema.md
+      workflow-stage-design.md
+      workflow-checkpoints.md
+      workflow-permissions.md
+      workflow-testing.md
+      workflow-common-mistakes.md
+    ]
+
+    references.each do |name|
+      assert_includes spec.files, "skills/hive/references/#{name}"
+    end
+  end
+
   def test_gem_package_includes_metadata_for_managed_web_path_dependency
     spec = Gem::Specification.load(GEMSPEC_PATH)
 
diff --git a/test/unit/llm_wiki_bootstrap_test.rb b/test/unit/llm_wiki_bootstrap_test.rb
index 8d40931e18..e278dbf347 100644
--- a/test/unit/llm_wiki_bootstrap_test.rb
+++ b/test/unit/llm_wiki_bootstrap_test.rb
@@ -35,4 +35,35 @@ def test_primary_worktree_root_reports_git_resolution_failure
     assert_match(/cannot resolve primary Git worktree/, error.message)
     assert_match(/not a Git repository/, error.message)
   end
+
+  def test_managed_path_validation_rejects_a_regular_file_parent
+    with_tmp_dir do |root|
+      parent = File.join(root, ".llm-wiki")
+      File.write(parent, "not a directory\n")
+
+      error = assert_raises(Hive::ConfigError) do
+        Hive::LlmWikiBootstrap.validate_project_managed_paths!(root)
+      end
+
+      assert_includes error.message, "managed parent"
+      assert_includes error.message, "must be a directory"
+      assert_equal "not a directory\n", File.read(parent)
+    end
+  end
+
+  def test_managed_file_write_translates_nofollow_errors
+    with_tmp_dir do |root|
+      path = File.join(root, ".llm-wiki", "config.json")
+
+      with_replaced_singleton_method(
+        File, :open, ->(*_args) { raise Errno::ELOOP, path }
+      ) do
+        error = assert_raises(Hive::ConfigError) do
+          Hive::LlmWikiBootstrap.write_file(path, "{}\n")
+        end
+        assert_includes error.message, "managed file"
+        assert_includes error.message, "must not be a symlink"
+      end
+    end
+  end
 end
diff --git a/test/unit/openclaw_skills_test.rb b/test/unit/openclaw_skills_test.rb
index 07fa04304d..4646794dcc 100644
--- a/test/unit/openclaw_skills_test.rb
+++ b/test/unit/openclaw_skills_test.rb
@@ -57,6 +57,30 @@ def test_projection_uses_native_status_watch_and_progressive_references
     refute_match(/pgrep\s+-af|kill\s+-0|while\s+:/, text)
   end
 
+  def test_projection_routes_natural_language_workflow_creation_inside_hive
+    root = ROOT.join("hive")
+    creator_references = %w[
+      workflow-creator.md
+      workflow-creator-example.md
+      workflow-schema.md
+      workflow-stage-design.md
+      workflow-checkpoints.md
+      workflow-permissions.md
+      workflow-testing.md
+      workflow-common-mistakes.md
+    ]
+
+    creator_references.each do |name|
+      assert root.join("references", name).file?, name
+    end
+
+    text = projection_text
+    assert_includes text, "hive-workflow-creator"
+    assert_includes text, "hive workflow validate ID --json"
+    assert_includes text, "No task by default"
+    assert_includes text, "Never publish externally"
+  end
+
   def test_projection_keeps_recovery_and_release_authority_guarded
     text = projection_text
     normalized_text = text.gsub(/\s+/, " ")
diff --git a/test/unit/operational_status_test.rb b/test/unit/operational_status_test.rb
index 13c200af9b..c1d30ea069 100644
--- a/test/unit/operational_status_test.rb
+++ b/test/unit/operational_status_test.rb
@@ -786,6 +786,33 @@ def test_human_and_nonrecoverable_elevated_states_have_no_action_but_error_has_r
     assert_equal "unavailable", projected.dig("error", "recovery", "status")
   end
 
+  def test_human_outcomes_are_preserved_in_operational_position
+    human = task(action: "needs_input", slug: "approval", stage: "3-approval")
+    human["workflow"] = "editorial"
+    human["outcomes"] = [
+      { "name" => "approve", "complete" => true, "artifact" => "draft.md", "to" => nil },
+      { "name" => "reject", "complete" => false, "artifact" => nil, "to" => "draft" }
+    ]
+
+    projected = project(status_payload(human)).fetch("tasks").first
+
+    assert_equal human.fetch("outcomes"), projected.dig("position", "allowed_outcomes")
+    assert_equal "waiting_on_you", projected.fetch("state")
+    assert_nil projected.fetch("action")
+  end
+
+  def test_completed_human_stage_is_archived
+    human = task(
+      action: "archived", slug: "approved", stage: "3-approval", marker: "complete"
+    )
+    result = project(status_payload(human))
+
+    assert_equal 0, result.dig("summary", "active")
+    assert_equal 1, result.dig("summary", "archived")
+    assert_equal 1, result.dig("archive", "count")
+    assert_empty result.fetch("tasks")
+  end
+
   def test_payload_validates_against_published_schema
     result = project(status_payload(task(action: "ready_to_plan", slug: "valid")))
     schema = JSONSchemer.schema(JSON.parse(File.read(Hive::Schemas.schema_path("hive-operational-status"))))
diff --git a/test/unit/packaging/live_agent_proof_test.rb b/test/unit/packaging/live_agent_proof_test.rb
index cc08eb137b..2eb726f6f1 100644
--- a/test/unit/packaging/live_agent_proof_test.rb
+++ b/test/unit/packaging/live_agent_proof_test.rb
@@ -90,8 +90,9 @@ def test_attestor_and_verifier_accept_only_exact_proven_artifacts
     with_tmp_dir do |dir|
       artifacts = prepare_artifacts(dir)
       evidence = prepare_evidence(dir, artifacts)
+      creator_evidence = prepare_creator_evidence(dir, artifacts)
       proof = File.join(dir, "proof")
-      result = attest(artifacts, evidence, proof)
+      result = attest(artifacts, evidence, creator_evidence, proof)
 
       verified = HiveLiveAgentProof::Verifier.new(
         proof_dir: proof, candidate_sha: SHA, workflow_revision: WORKFLOW_SHA,
@@ -107,9 +108,10 @@ def test_attestor_rejects_missing_skipped_or_unsafe_platform_evidence
     with_tmp_dir do |dir|
       artifacts = prepare_artifacts(dir)
       evidence = prepare_evidence(dir, artifacts)
+      creator_evidence = prepare_creator_evidence(dir, artifacts)
       FileUtils.rm_f(File.join(evidence, "pi.json"))
       error = assert_raises(HiveLiveAgentProof::Error) do
-        attest(artifacts, evidence, File.join(dir, "proof-missing"))
+        attest(artifacts, evidence, creator_evidence, File.join(dir, "proof-missing"))
       end
       assert_includes error.message, "exactly"
 
@@ -119,7 +121,7 @@ def test_attestor_rejects_missing_skipped_or_unsafe_platform_evidence
       row["result"] = "skipped"
       HiveLiveAgentProof.write_json(row_path, row)
       error = assert_raises(HiveLiveAgentProof::Error) do
-        attest(artifacts, evidence, File.join(dir, "proof-skipped"))
+        attest(artifacts, evidence, creator_evidence, File.join(dir, "proof-skipped"))
       end
       assert_includes error.message, "invalid"
 
@@ -127,7 +129,7 @@ def test_attestor_rejects_missing_skipped_or_unsafe_platform_evidence
       row["hive_commands"] << [ "act", "resume", "proof:live-agent-skill" ]
       HiveLiveAgentProof.write_json(row_path, row)
       error = assert_raises(HiveLiveAgentProof::Error) do
-        attest(artifacts, evidence, File.join(dir, "proof-unsafe"))
+        attest(artifacts, evidence, creator_evidence, File.join(dir, "proof-unsafe"))
       end
       assert_includes error.message, "exactly one operational status"
 
@@ -139,18 +141,46 @@ def test_attestor_rejects_missing_skipped_or_unsafe_platform_evidence
       }
       HiveLiveAgentProof.write_json(row_path, row)
       error = assert_raises(HiveLiveAgentProof::Error) do
-        attest(artifacts, evidence, File.join(dir, "proof-generic-activation"))
+        attest(artifacts, evidence, creator_evidence, File.join(dir, "proof-generic-activation"))
       end
       assert_includes error.message, "native activation"
     end
   end
 
+  def test_attestor_rejects_workflow_creator_mutation_or_graph_drift
+    with_tmp_dir do |dir|
+      artifacts = prepare_artifacts(dir)
+      evidence = prepare_evidence(dir, artifacts)
+      creator_evidence = prepare_creator_evidence(dir, artifacts)
+      row_path = File.join(creator_evidence, "openclaw-workflow-creator.json")
+      row = JSON.parse(File.read(row_path))
+      row["task_count"] = 2
+      HiveLiveAgentProof.write_json(row_path, row)
+
+      error = assert_raises(HiveLiveAgentProof::Error) do
+        attest(artifacts, evidence, creator_evidence, File.join(dir, "proof-task"))
+      end
+      assert_includes error.message, "unauthorized side effect"
+
+      creator_evidence = prepare_creator_evidence(dir, artifacts)
+      row_path = File.join(creator_evidence, "openclaw-workflow-creator.json")
+      row = JSON.parse(File.read(row_path))
+      row["validation"]["stages"] << "publish"
+      HiveLiveAgentProof.write_json(row_path, row)
+      error = assert_raises(HiveLiveAgentProof::Error) do
+        attest(artifacts, evidence, creator_evidence, File.join(dir, "proof-publish"))
+      end
+      assert_includes error.message, "normalized graph"
+    end
+  end
+
   def test_verifier_fails_closed_on_check_digest_identity_and_artifact_substitution
     with_tmp_dir do |dir|
       artifacts = prepare_artifacts(dir)
       evidence = prepare_evidence(dir, artifacts)
+      creator_evidence = prepare_creator_evidence(dir, artifacts)
       proof = File.join(dir, "proof")
-      result = attest(artifacts, evidence, proof)
+      result = attest(artifacts, evidence, creator_evidence, proof)
 
       assert_raises(HiveLiveAgentProof::Error) do
         verifier(proof, "0" * 64).call
@@ -243,11 +273,66 @@ def prepare_evidence(dir, artifacts)
     evidence
   end
 
-  def attest(artifacts, evidence, proof)
+  def prepare_creator_evidence(dir, artifacts)
+    evidence = File.join(dir, "creator-evidence")
+    FileUtils.rm_rf(evidence)
+    FileUtils.mkdir_p(evidence)
+    manifest = JSON.parse(File.read(File.join(artifacts, "artifact-manifest.json")))
+    row = {
+      "schema" => "hive-live-workflow-creator-evidence",
+      "schema_version" => 1,
+      "platform" => "openclaw",
+      "candidate_sha" => SHA,
+      "result" => "passed",
+      "prompt_sha256" => Digest::SHA256.hexdigest(HiveLiveAgentProof::WORKFLOW_CREATOR_PROMPT),
+      "task_prompt_sha256" =>
+        Digest::SHA256.hexdigest(HiveLiveAgentProof::WORKFLOW_CREATOR_TASK_PROMPT),
+      "skill" => {
+        "skill_version" => manifest.fetch("skill_version"),
+        "canonical_digest" => manifest.fetch("canonical_digest")
+      },
+      "native_activation" => {
+        "kind" => HiveLiveAgentProof::NATIVE_ACTIVATION_KINDS.fetch("openclaw"),
+        "invocation" => HiveLiveAgentProof::INVOCATIONS.fetch("openclaw")
+      },
+      "hive_commands" => HiveLiveAgentProof::WORKFLOW_CREATOR_COMMANDS,
+      "created_files" => HiveLiveAgentProof::WORKFLOW_CREATOR_FILES.map do |path|
+        { "path" => path, "sha256" => "c" * 64, "size" => 10 }
+      end,
+      "validation" => {
+        "valid" => true,
+        "stages" => %w[research draft approval],
+        "automatic_edges" => [ %w[research draft], %w[draft approval] ],
+        "human_outcomes" => [
+          { "stage" => "approval", "name" => "approve", "complete" => true,
+            "artifact" => "draft.md", "to" => nil },
+          { "stage" => "approval", "name" => "reject", "complete" => false,
+            "artifact" => nil, "to" => "draft" }
+        ]
+      },
+      "creation_only_task_count" => 0,
+      "task_count" => 1,
+      "task" => {
+        "slug" => HiveLiveAgentProof::WORKFLOW_CREATOR_TASK_SLUG,
+        "workflow" => "editorial",
+        "first_created" => true,
+        "retry_created" => false,
+        "run_count" => 1,
+        "current_stage" => "1-research"
+      },
+      "external_actions" => [],
+      "secret_scan" => { "status" => "passed" },
+      "cleanup" => { "status" => "passed" }
+    }
+    HiveLiveAgentProof.write_json(File.join(evidence, "openclaw-workflow-creator.json"), row)
+    evidence
+  end
+
+  def attest(artifacts, evidence, creator_evidence, proof)
     HiveLiveAgentProof::Attestor.new(
       candidate_sha: SHA, workflow_revision: WORKFLOW_SHA, repository: REPOSITORY,
       run_id: "42", run_attempt: "1", artifact_dir: artifacts,
-      evidence_dir: evidence, output_dir: proof
+      evidence_dir: evidence, creator_evidence_dir: creator_evidence, output_dir: proof
     ).call
   end
 
diff --git a/test/unit/release_contract_test.rb b/test/unit/release_contract_test.rb
index 84e5697587..7da26d7c7d 100644
--- a/test/unit/release_contract_test.rb
+++ b/test/unit/release_contract_test.rb
@@ -45,7 +45,12 @@ def test_public_credibility_copy_matches_current_capabilities
 
     assert_includes workflows, "built-in `coding`, `content`, and `bench` workflows"
     assert_includes workflows, "hive workflow install honeycomb/architecture --yes"
-    assert_includes workflows, "The last stage may be `kind: terminal`, `kind: agent`, or `kind: council`."
+    assert_includes workflows, "/hive create a three-stage editorial workflow"
+    assert_includes workflows, "hive workflow validate editorial --json"
+    assert_includes workflows, "hive decide <task> approve --from approval --decision-id <decision-id>"
+    assert_includes workflows, "The last stage may be `kind: terminal`, `kind: agent`, `kind: council`, or `kind: human`."
+    assert_includes workflows, "No task is created by workflow creation alone."
+    assert_includes readme, "natural language"
 
     %w[OpenClaw Grok hive-bench Honeycomb].each { |capability| assert_includes readme, capability }
     assert_includes readme, "### Hive web (default native experience)"
@@ -72,6 +77,7 @@ def test_agent_skill_release_metadata_is_derived_from_authoritative_inputs
     projection = JSON.parse(File.read(File.join(ROOT, "openclaw/skills/hive/.hive-skill.json")))
     setup = File.read(File.join(ROOT, "openclaw/skills/hive/references/setup-and-platforms.md"))
     publish_docs = read("openclaw/README.md")
+    release_docs = read("docs/RELEASING.md")
 
     assert_equal canonical.version, openclaw.fetch("version")
     assert_equal canonical.version, projection.fetch("skill_version")
@@ -80,6 +86,9 @@ def test_agent_skill_release_metadata_is_derived_from_authoritative_inputs
     refute_match(%r{/v(?!#{Regexp.escape(Hive::VERSION)}/)[0-9]+\.[0-9]+\.[0-9]+/install\.sh}, setup)
     assert_includes publish_docs, "skills/hive/skill.json"
     refute_match(/--version\s+\d+\.\d+\.\d+/, publish_docs)
+    assert_includes release_docs, "hive-site #23116"
+    assert_includes release_docs, "does not block this repository"
+    assert_match(/Do\s+not describe current-main workflow-creator commands as stable/, release_docs)
   end
 
   def test_live_agent_proof_is_protected_exact_sha_and_four_surface
@@ -100,7 +109,8 @@ def test_live_agent_proof_is_protected_exact_sha_and_four_surface
     assert_equal %w[claude codex openclaw pi], matrix.map { |row| row.fetch("platform") }.sort
     assert_equal "live-agent-skills-${{ matrix.platform }}", jobs.fetch("live-agent").fetch("environment")
     assert_equal false, jobs.fetch("live-agent").dig("strategy", "fail-fast")
-    assert_equal [ "validate", "build", "live-agent" ], jobs.fetch("attest").fetch("needs")
+    assert_equal [ "validate", "build", "live-agent", "live-workflow-creator" ],
+                 jobs.fetch("attest").fetch("needs")
     assert_includes body, "retention-days: 7"
     assert_includes body, 'name: "live-agent-skills"'
     assert_includes body, "checks: write"
@@ -134,6 +144,18 @@ def test_live_agent_proof_is_protected_exact_sha_and_four_surface
     assert_includes candidate_install_body, "install_candidate_gem.sh"
     assert_includes candidate_install_body, '"$RUNNER_TEMP/proven-gems/bin/hive" --version'
     refute_includes candidate_install_body, 'gem install "$gem_file"'
+
+    creator = jobs.fetch("live-workflow-creator")
+    assert_equal [ "validate", "build" ], creator.fetch("needs")
+    assert_equal "live-agent-skills-openclaw", creator.fetch("environment")
+    creator_step = creator.fetch("steps").find do |step|
+      step["name"] == "Run authenticated workflow-creator proof"
+    end
+    refute_nil creator_step
+    assert_equal "${{ secrets.OPENAI_API_KEY }}", creator_step.dig("env", "OPENAI_API_KEY")
+    assert_includes creator_step.fetch("run"), "live_hive_workflow_creator_smoke_test.rb"
+    assert_includes body, "workflow-creator-evidence-openclaw"
+    assert_includes body, "creator-evidence"
   end
 
   def test_tag_release_selects_and_reverifies_exact_pre_tag_candidate
diff --git a/test/unit/schema_files_test.rb b/test/unit/schema_files_test.rb
index be8274cdcb..81876118d6 100644
--- a/test/unit/schema_files_test.rb
+++ b/test/unit/schema_files_test.rb
@@ -8,6 +8,7 @@
 require "hive/commands/daemon"
 require "hive/commands/drop"
 require "hive/commands/doctor"
+require "hive/commands/decide"
 require "hive/commands/forget"
 require "hive/commands/init"
 require "hive/commands/patrol"
@@ -15,6 +16,8 @@
 require "hive/commands/run"
 require "hive/commands/stage_action"
 require "hive/commands/status"
+require "hive/agent_skills/inspector"
+require "hive/agent_skills/provisioner"
 require "hive/patrol/candidate_selector"
 require "hive/patrol/reviewer"
 require "hive/commands/setup_agents"
@@ -30,6 +33,15 @@
 #   3. Pin the same required-key set the producer code emits, so a producer
 #      change without a schema update fails at test time.
 class SchemaFilesTest < Minitest::Test
+  def test_hive_decide_schema_matches_success_payload_contract
+    path = Hive::Schemas.schema_path("hive-decide")
+    assert File.exist?(path)
+    doc = JSON.parse(File.read(path))
+    assert_equal Hive::Commands::Decide::SUCCESS_KEYS.sort,
+                 doc.dig("$defs", "SuccessPayload", "required").sort
+    assert_equal 1, doc.dig("$defs", "SuccessPayload", "properties", "schema_version", "const")
+  end
+
   def test_native_web_schema_files_accept_versioned_success_and_error_shapes
     service = {
       "platform" => "linux", "unit_path" => "/tmp/hive-web.service",
diff --git a/test/unit/task_action_generic_test.rb b/test/unit/task_action_generic_test.rb
index 0592d38e1d..3451cee0fe 100644
--- a/test/unit/task_action_generic_test.rb
+++ b/test/unit/task_action_generic_test.rb
@@ -100,6 +100,24 @@ def test_council_marker_to_action_matrix
     assert_equal "hive approve #{SLUG} --from 2-review", complete_middle.command
   end
 
+  def test_human_stage_waits_for_a_named_decision_and_never_dispatches
+    action = action_for("approval", :waiting, { "decision_id" => "a" * 16 }, descriptor: human_workflow)
+
+    assert_equal "needs_input", action.key
+    assert_equal "Awaiting human decision", action.label
+    assert_nil action.command
+    assert_equal [
+      { "name" => "approve", "complete" => true, "artifact" => "draft.md", "to" => nil },
+      { "name" => "reject", "complete" => false, "artifact" => nil, "to" => "draft" }
+    ], action.allowed_outcomes
+    assert_equal :skip, policy_decision(action)
+
+    completed = action_for("approval", :complete, descriptor: human_workflow)
+    assert_equal "archived", completed.key
+    assert_equal "Archived", completed.label
+    assert_nil completed.command
+  end
+
   def test_terminal_agent_complete_requires_non_empty_deliverable
     missing = action_for_terminal_active(:agent, write_deliverable: false)
     assert_equal "error", missing.fetch(:key)
@@ -322,6 +340,22 @@ def council_workflow
     )
   end
 
+  def human_workflow
+    Hive::Workflow.new(
+      id: :human_status,
+      stages: [
+        Hive::Workflow::Stage.new(name: "draft", index: 1, state_file: "draft.md", kind: :agent, skill: "/draft"),
+        Hive::Workflow::Stage.new(
+          name: "approval", index: 2, state_file: "approval.md", kind: :human,
+          outcomes: {
+            "approve" => Hive::Workflow::Outcome.new(name: "approve", complete: true, artifact: "draft.md"),
+            "reject" => Hive::Workflow::Outcome.new(name: "reject", to: "draft")
+          }.freeze
+        )
+      ]
+    )
+  end
+
   def terminal_active_workflow(kind)
     terminal = if kind == :council
       Hive::Workflow::Stage.new(
diff --git a/test/unit/task_action_test.rb b/test/unit/task_action_test.rb
index 88d4b7fced..9a6611dd0e 100644
--- a/test/unit/task_action_test.rb
+++ b/test/unit/task_action_test.rb
@@ -730,7 +730,7 @@ def test_command_shellescapes_slug_with_special_characters
   def test_payload_has_expected_keys
     task = fake_task(stage_name: "brainstorm", stage_index: 2)
     payload = Hive::TaskAction.for(task, marker(:complete)).payload
-    assert_equal %w[command key label next_action].sort, payload.keys.sort
+    assert_equal %w[command key label next_action outcomes].sort, payload.keys.sort
     assert_equal "ready_to_plan", payload["key"]
   end
 
diff --git a/test/unit/task_meta_test.rb b/test/unit/task_meta_test.rb
index 35cf27c63d..d0961ffdb5 100644
--- a/test/unit/task_meta_test.rb
+++ b/test/unit/task_meta_test.rb
@@ -361,7 +361,7 @@ def test_malformed_yaml_warns_that_depends_on_and_workflow_were_dropped
       result = nil
       _out, err = capture_io { result = Hive::TaskMeta.read(dir) }
       assert_equal Hive::TaskMeta.empty, result
-      assert_match(/depends_on, workflow, base_branch dropped; managed provenance dropped/, err,
+      assert_match(/depends_on, workflow, base_branch dropped; idempotency dropped; managed provenance dropped/, err,
                    "a YAML parse failure must warn that depends_on (and the " \
                    "workflow selector) were dropped")
     end
@@ -403,6 +403,33 @@ def test_update_display_name_preserves_id_slug_dependency_and_workflow
     end
   end
 
+  def test_idempotency_metadata_round_trips_and_survives_rewrites
+    with_tmp_dir do |dir|
+      fingerprint = "a" * 64
+      Hive::TaskMeta.write(
+        dir, id: 7, slug: "keep-slug", display_name: nil,
+        idempotency_key: "creator:v1", input_fingerprint: fingerprint
+      )
+
+      Hive::TaskMeta.update_display_name(dir, "Readable Name")
+      meta = Hive::TaskMeta.read(dir)
+
+      assert_equal "creator:v1", meta.fetch(:idempotency_key)
+      assert_equal fingerprint, meta.fetch(:input_fingerprint)
+      assert_includes File.read(File.join(dir, "meta.yml")), "idempotency_key: creator:v1"
+    end
+  end
+
+  def test_idempotency_metadata_requires_key_and_fingerprint_together
+    with_tmp_dir do |dir|
+      assert_raises(ArgumentError) do
+        Hive::TaskMeta.write(
+          dir, id: 1, slug: "task", display_name: nil, idempotency_key: "creator:v1"
+        )
+      end
+    end
+  end
+
   def test_update_id_preserves_slug_display_name_dependency_and_workflow
     with_tmp_dir do |dir|
       Hive::TaskMeta.write(
diff --git a/test/unit/workflow_package/managed_store_test.rb b/test/unit/workflow_package/managed_store_test.rb
index 286e5916f9..24da947496 100644
--- a/test/unit/workflow_package/managed_store_test.rb
+++ b/test/unit/workflow_package/managed_store_test.rb
@@ -319,6 +319,36 @@ def test_malformed_locks_are_visible_directly_and_reported_by_selection_enumerat
     end
   end
 
+  def test_read_only_selection_does_not_create_a_lock_or_reconcile_a_journal
+    with_tmp_dir do |dir|
+      store = Hive::WorkflowPackage::ManagedStore.new(File.join(dir, ".hive-state"))
+      package = File.join(dir, "package")
+      resolution = write_package(package, "a" * 40)
+      store.place_generation(package, resolution)
+      store.activate(resolution)
+
+      mutation_lock = File.join(store.workflows_dir, ".mutation.lock")
+      FileUtils.rm_f(mutation_lock)
+      lock_bytes = File.binread(store.lock_path("demo"))
+      journal = Hive::WorkflowPackage::TransactionJournal.new(store.workflows_dir)
+      journal.write(
+        "schema_version" => 1,
+        "phase" => "prepared",
+        "lock_path" => store.lock_path("demo"),
+        "old_lock" => nil,
+        "new_lock" => lock_bytes
+      )
+      journal_bytes = File.binread(journal.path)
+
+      selection = store.selected_read_only("demo")
+
+      assert_equal "demo", selection.fetch("name")
+      refute File.exist?(mutation_lock)
+      assert_equal lock_bytes, File.binread(store.lock_path("demo"))
+      assert_equal journal_bytes, File.binread(journal.path)
+    end
+  end
+
   def test_selection_enumeration_reports_missing_configuration_and_keeps_healthy_sibling
     assert_invalid_configuration_isolated("unavailable") do |_store, _digest, _path|
       nil
diff --git a/test/unit/workflow_test.rb b/test/unit/workflow_test.rb
index 1f94a9cbf3..983241d677 100644
--- a/test/unit/workflow_test.rb
+++ b/test/unit/workflow_test.rb
@@ -58,10 +58,12 @@ def test_stage_defaults_optional_descriptor_fields
     assert_nil stage.reviewers
     assert_nil stage.council
     assert_nil stage.deliverable
+    assert_nil stage.outcomes
   end
 
   def test_known_kinds_include_coding_runtime_primitives
     assert_includes Hive::Workflow::KNOWN_KINDS, :council
+    assert_includes Hive::Workflow::KNOWN_KINDS, :human
     assert_includes Hive::Workflow::KNOWN_KINDS, :execute
     assert_includes Hive::Workflow::KNOWN_KINDS, :review_council
     assert_includes Hive::Workflow::KNOWN_KINDS, :finalize
@@ -71,6 +73,118 @@ def test_known_kinds_include_coding_runtime_primitives
     refute_includes Hive::Workflow::KNOWN_KINDS, :marker
   end
 
+  def test_human_stage_exposes_immutable_named_outcomes
+    approve = Hive::Workflow::Outcome.new(
+      name: "approve", complete: true, artifact: "draft.md"
+    )
+    reject = Hive::Workflow::Outcome.new(name: "reject", to: "draft")
+    outcomes = { "approve" => approve, "reject" => reject }.freeze
+    workflow = Hive::Workflow.new(
+      id: :editorial,
+      stages: [
+        Hive::Workflow::Stage.new(
+          name: "draft", index: 1, state_file: "draft.md", kind: :agent
+        ),
+        Hive::Workflow::Stage.new(
+          name: "approval", index: 2, state_file: "approval.md", kind: :human,
+          input: "draft.md", outcomes: outcomes
+        )
+      ]
+    )
+
+    approval = workflow.stage_named("approval")
+    assert_same outcomes, approval.outcomes
+    assert approval.outcomes.frozen?
+    assert approval.outcomes.fetch("approve").frozen?
+    assert approval.outcomes.fetch("approve").terminal?
+    refute approval.outcomes.fetch("reject").terminal?
+    assert_equal "draft", approval.outcomes.fetch("reject").to
+  end
+
+  def test_workflow_rejects_human_outcome_target_outside_descriptor
+    error = assert_raises(ArgumentError) do
+      Hive::Workflow.new(
+        id: :editorial,
+        stages: [
+          Hive::Workflow::Stage.new(
+            name: "approval", index: 1, state_file: "approval.md", kind: :human,
+            outcomes: {
+              "reject" => Hive::Workflow::Outcome.new(name: "reject", to: "draft")
+            }.freeze
+          )
+        ]
+      )
+    end
+
+    assert_includes error.message, 'stage "approval" outcome "reject" targets unknown stage "draft"'
+  end
+
+  def test_workflow_defensively_rejects_invalid_runtime_human_outcome_shapes
+    valid = Hive::Workflow::Stage.new(
+      name: "draft", index: 1, state_file: "draft.md", kind: :agent
+    )
+    cases = [
+      [
+        Hive::Workflow::Stage.new(
+          name: "approval", index: 2, state_file: "approval.md", kind: :human, outcomes: nil
+        ),
+        "must declare at least one outcome"
+      ],
+      [
+        Hive::Workflow::Stage.new(
+          name: "approval", index: 2, state_file: "approval.md", kind: :human,
+          outcomes: { "approve" => Object.new }.freeze
+        ),
+        'outcome "approve" is invalid'
+      ],
+      [
+        Hive::Workflow::Stage.new(
+          name: "approval", index: 2, state_file: "approval.md", kind: :human,
+          outcomes: {
+            "approve" => Hive::Workflow::Outcome.new(name: "approve", complete: true, to: "draft")
+          }.freeze
+        ),
+        "must declare exactly one of complete or to"
+      ],
+      [
+        Hive::Workflow::Stage.new(
+          name: "approval", index: 2, state_file: "approval.md", kind: :human,
+          outcomes: {
+            "approve" => Hive::Workflow::Outcome.new(name: "approve", complete: true)
+          }.freeze
+        ),
+        "must declare an artifact when complete"
+      ],
+      [
+        Hive::Workflow::Stage.new(
+          name: "approval", index: 2, state_file: "approval.md", kind: :human,
+          outcomes: {
+            "reject" => Hive::Workflow::Outcome.new(name: "reject", to: "draft", artifact: "draft.md")
+          }.freeze
+        ),
+        "artifact is only valid for a completing outcome"
+      ]
+    ]
+
+    cases.each do |stage, message|
+      error = assert_raises(ArgumentError) do
+        Hive::Workflow.new(id: :editorial, stages: [ valid, stage ])
+      end
+      assert_includes error.message, message
+    end
+
+    non_human = Hive::Workflow::Stage.new(
+      name: "draft", index: 1, state_file: "draft.md", kind: :agent,
+      outcomes: {
+        "approve" => Hive::Workflow::Outcome.new(name: "approve", complete: true, artifact: "draft.md")
+      }.freeze
+    )
+    error = assert_raises(ArgumentError) do
+      Hive::Workflow.new(id: :editorial, stages: [ non_human ])
+    end
+    assert_includes error.message, "non-human stage"
+  end
+
   def test_stage_carries_user_descriptor_instruction_and_permissions
     stage = Hive::Workflow::Stage.new(
       name: "work",
diff --git a/test/unit/workflows/descriptor_parser_test.rb b/test/unit/workflows/descriptor_parser_test.rb
index 2afe59af89..a876feb507 100644
--- a/test/unit/workflows/descriptor_parser_test.rb
+++ b/test/unit/workflows/descriptor_parser_test.rb
@@ -180,6 +180,124 @@ def test_skill_backed_agent_stage_is_valid
     assert_equal "/ship", workflow.stage_named("work").skill
   end
 
+  def test_editorial_human_stage_parses_closed_outcomes
+    workflow = Hive::Workflows::DescriptorParser.parse_hash(
+      {
+        "id" => "editorial",
+        "stages" => [
+          { "name" => "research", "kind" => "agent", "state_file" => "research.md", "skill" => "/research" },
+          { "name" => "draft", "kind" => "agent", "state_file" => "draft.md", "skill" => "/draft" },
+          {
+            "name" => "approval",
+            "kind" => "human",
+            "state_file" => "approval.md",
+            "input" => "draft.md",
+            "outcomes" => {
+              "approve" => { "complete" => true, "artifact" => "draft.md" },
+              "reject" => { "to" => "draft" }
+            }
+          }
+        ]
+      },
+      path: "/tmp/editorial.yml"
+    )
+
+    assert_equal %w[research draft approval], workflow.stage_names
+    approval = workflow.stage_named("approval")
+    assert_equal :human, approval.kind
+    assert_equal "draft.md", approval.input
+    assert_equal %w[approve reject], approval.outcomes.keys
+    assert_equal true, approval.outcomes.fetch("approve").complete
+    assert_equal "draft.md", approval.outcomes.fetch("approve").artifact
+    assert_equal "draft", approval.outcomes.fetch("reject").to
+    assert approval.outcomes.frozen?
+  end
+
+  def test_human_outcomes_reject_invalid_shapes_and_targets
+    invalid = [
+      [ { "approve" => { "complete" => true, "to" => "draft", "artifact" => "draft.md" } }, "exactly one" ],
+      [ { "approve" => { "artifact" => "draft.md" } }, "exactly one" ],
+      [ { "approve" => { "complete" => false, "artifact" => "draft.md" } }, "complete must be true" ],
+      [ { "approve" => { "complete" => true } }, "artifact must be a non-empty string" ],
+      [ { "reject" => { "to" => "draft", "artifact" => "draft.md" } }, "artifact is only valid" ],
+      [ { "bad name" => { "to" => "draft" } }, "outcome name" ],
+      [ { "reject" => { "to" => "missing" } }, "targets unknown stage" ],
+      [ { "reject" => { "to" => "draft", "command" => "publish" } }, "unknown key" ]
+    ]
+
+    invalid.each do |outcomes, message|
+      error = assert_config_error(
+        {
+          "id" => "bad",
+          "stages" => [
+            { "name" => "draft", "kind" => "agent", "state_file" => "draft.md", "skill" => "/draft" },
+            {
+              "name" => "approval", "kind" => "human", "state_file" => "approval.md",
+              "input" => "draft.md", "outcomes" => outcomes
+            }
+          ]
+        },
+        path: "/tmp/bad.yml"
+      )
+
+      assert_includes error.message, message, "expected #{outcomes.inspect} to report #{message.inspect}"
+      assert_includes error.message, "approval"
+    end
+  end
+
+  def test_human_stage_rejects_agent_settings_and_non_human_outcomes
+    %w[agent model permissions instruction skill effort budget_usd timeout_sec reviewers council deliverable workspace handoff conditions mapping_role mapping_contract].each do |field|
+      value =
+        case field
+        when "budget_usd", "timeout_sec" then 1
+        when "reviewers" then []
+        when "council", "conditions" then {}
+        else "value"
+        end
+      error = assert_config_error(
+        {
+          "id" => "bad",
+          "stages" => [
+            {
+              "name" => "approval", "kind" => "human", "state_file" => "approval.md",
+              "outcomes" => { "approve" => { "complete" => true, "artifact" => "draft.md" } },
+              field => value
+            }
+          ]
+        },
+        path: "/tmp/bad.yml"
+      )
+      assert_includes error.message, field
+      assert_includes error.message, "human stage"
+    end
+
+    error = assert_config_error(
+      {
+        "id" => "bad",
+        "stages" => [
+          {
+            "name" => "draft", "kind" => "agent", "state_file" => "draft.md", "skill" => "/draft",
+            "outcomes" => { "approve" => { "complete" => true, "artifact" => "draft.md" } }
+          }
+        ]
+      },
+      path: "/tmp/bad.yml"
+    )
+    assert_includes error.message, "outcomes is only valid on a human stage"
+  end
+
+  def test_human_stage_requires_at_least_one_outcome
+    [ nil, {} ].each do |outcomes|
+      stage = { "name" => "approval", "kind" => "human", "state_file" => "approval.md" }
+      stage["outcomes"] = outcomes unless outcomes.nil?
+      error = assert_config_error(
+        { "id" => "bad", "stages" => [ stage ] },
+        path: "/tmp/bad.yml"
+      )
+      assert_includes error.message, "human stage must declare at least one outcome"
+    end
+  end
+
   def test_stage_conditions_select_registered_semantics_only
     workflow = Hive::Workflows::DescriptorParser.parse_hash(
       {
@@ -817,7 +935,7 @@ def test_parser_terminal_last_stage_validation_reports_internal_kind_drift
       parser.send(:validate_terminal_last_stage!, [ stage ])
     end
 
-    assert_includes error.message, 'last stage "execute" must be terminal, agent, or council'
+    assert_includes error.message, 'last stage "execute" must be terminal, agent, council, or human'
   end
 
   def test_council_stage_requires_reviewers
@@ -988,7 +1106,7 @@ def test_unknown_kind_is_rejected
       path: "/tmp/bad.yml"
     )
 
-    assert_includes error.message, 'stage 1 kind "marker" must be agent, council, or terminal'
+    assert_includes error.message, 'stage 1 kind "marker" must be agent, council, human, or terminal'
 
     # The coding runtime kinds (execute/review_council/finalize) are Ruby-only:
     # only Workflows::Coding declares them via Stage.new, and parse_kind must keep
@@ -1004,7 +1122,7 @@ def test_unknown_kind_is_rejected
         path: "/tmp/bad.yml"
       )
 
-      assert_includes runtime_error.message, %(stage 1 kind "#{coding_runtime_kind}" must be agent, council, or terminal),
+      assert_includes runtime_error.message, %(stage 1 kind "#{coding_runtime_kind}" must be agent, council, human, or terminal),
                       "YAML descriptors must not be able to declare Ruby-only coding runtime kind " \
                       "#{coding_runtime_kind.inspect}"
     end
diff --git a/wiki/cli.md b/wiki/cli.md
index 5daf76dc24..866abdc886 100644
--- a/wiki/cli.md
+++ b/wiki/cli.md
@@ -90,6 +90,7 @@ contracts retain their established unversioned or schema-less shapes.
 | `hive init [PROJECT_PATH] [--json] [--refactor-patrol\|--no-refactor-patrol]` | Bootstrap `.hive-state` plus managed llm-wiki context; fresh projects enable architecture discovery by default, and the boolean flag resolves it before any state write; `--json` emits `hive-init.v2` | `Hive::Commands::Init` | [[commands/init]] |
 | `hive new PROJECT TEXT... [--workflow ID] [--depends-on TASK] [--base BRANCH] [--json]` | Create a task in `1-inbox/` of a registered project, writing the request plus `meta.yml`; `--base` records the exact non-stacked origin base for managed draft-PR workflows, and stdout uses the numeric task id when allocation succeeds | `Hive::Commands::New` | [[commands/new]] |
 | `hive workflow new ID [--json]` | Scaffold a blank per-project workflow descriptor plus placeholder instruction under `<hive_state_path>/workflows/` | `Hive::Commands::Workflow` | [[commands/workflow]] |
+| `hive workflow commit ID` | Validate and commit a populated owner-authored workflow under the shared Hive state commit lock | `Hive::Commands::Workflow` | [[commands/workflow]] |
 | `hive generate-name TARGET [--project NAME] [--stage STAGE]` | Generate and persist a short display title for a task; target resolves by path, slug, or numeric id | `Hive::Commands::GenerateName` | [[commands/generate-name]] |
 | `hive status [--operational [--json] \| --full \| --diagnose SLUG ...]` | Default/`--operational` is the concise closed-state human view; `--operational --json` emits `hive-operational-status.v3`; bare `--json` emits complete `hive-status.v7`; `--full` retains the detailed table. Diagnosis keeps its existing bounded read/write surface. | `Hive::Commands::Status` | [[commands/status]] |
 | `hive watch TARGET... [--project NAME] [--until settled\|completion] [--json-lines]` | Resolve at most 100 tasks once and emit only semantic initial/transition/warning/final events until a terminal condition, timeout, or event cap. Read-only; global `--json` is rejected. | `Hive::Commands::Watch` | [[commands/watch]] |
@@ -117,6 +118,7 @@ contracts retain their established unversioned or schema-less shapes.
 | `hive run TARGET [--no-rebase]` | Lower-level dispatcher for a slug or task folder. `--no-rebase` skips the auto-rebase pre-step for one invocation (one-off override of `cfg.rebase.enabled`). | `Hive::Commands::Run` → stage runner | [[commands/run]] |
 | `hive rebase-status TARGET` | Read-only inspector: reports whether the next `hive run` would attempt an auto-rebase, how many commits behind `origin/<default>` the worktree is, and which guard (if any) would short-circuit. Never mutates; never calls `git fetch`. | `Hive::Commands::RebaseStatus` | [[commands/rebase-status]] |
 | `hive approve TARGET [--to STAGE] [--from STAGE]` | Move a task between stages + record a hive/state commit (agent-callable equivalent of shell `mv`; `--from` asserts current stage for retry idempotency) | `Hive::Commands::Approve` | [[commands/approve]] |
+| `hive decide TARGET OUTCOME --from STAGE --decision-id ID [--note TEXT] [--json]` | Apply one descriptor-declared human outcome to the observed stage visit; completing outcomes atomically verify a non-marker artifact. | `Hive::Commands::Decide` | [[commands/workflow]] |
 | `hive drop TARGET [--project NAME] [--from STAGE]` | Hard-delete an active task: kill its recorded agent, close draft PR best-effort, remove task folder(s), logs, worktree, branch, and locks, then commit an audit record. Refuses `9-done`. | `Hive::Commands::Drop` | [[commands/drop]] |
 | `hive findings TARGET [--pass N] [--stage STAGE]` | List GFM-checkbox findings in `reviews/ce-review-NN.md` (latest by default) | `Hive::Commands::Findings` | [[commands/findings]] |
 | `hive accept-finding TARGET [ID...] [--severity S] [--all] [--stage STAGE]` | Tick `[x]` on review findings; selectors are unioned | `Hive::Commands::FindingToggle` (accept) | [[commands/findings]] |
diff --git a/wiki/commands/init.md b/wiki/commands/init.md
index 7ec0e535a0..0c20a371f4 100644
--- a/wiki/commands/init.md
+++ b/wiki/commands/init.md
@@ -4,7 +4,7 @@ type: command
 source: lib/hive/commands/init.rb
 created: 2026-04-25
 updated: 2026-07-26
-tags: [command, bootstrap, git, prompts, llm-wiki, provisioning]
+tags: [command, bootstrap, git, prompts, preview, minimal, llm-wiki, provisioning]
 ---
 
 **TLDR**: `hive init [PATH]` bootstraps a project for Hive and `--json` emits the resolved bootstrap contract. Alongside workflow, agents, review, patrol cadence, and daemon settings, fresh terminal and web setup recommend post-merge architecture patrol with an explicit default-yes answer. Headless callers can choose the same value before any write with `--refactor-patrol` or `--no-refactor-patrol`; enabling it also enables confined auto-fix/PR attempts and deduplicated GitHub issues as the fallback review surface. After the project transaction succeeds, interactive init diagnoses enabled manifest-managed agent skills and can delegate an accepted offer to the same setup engine as `hive setup-agents`; decline, non-TTY, and JSON flows only report remediation.
@@ -14,6 +14,8 @@ tags: [command, bootstrap, git, prompts, llm-wiki, provisioning]
 ```
 hive init [PROJECT_PATH] [--force] [--json] [--workflow NAME] [--refactor-patrol|--no-refactor-patrol]
 hive init --new-workflow ID [PROJECT_PATH]
+hive init --new-workflow ID --minimal --preview --json [PROJECT_PATH]
+hive init --new-workflow ID --minimal --json [PROJECT_PATH]
 ```
 
 `PROJECT_PATH` defaults to `Dir.pwd`. `--force` skips the clean-tree check. `--json` emits the current `hive-init.v2` success document on stdout with project metadata and the resolved prompt answers; v1 remains a compatibility schema. `--workflow NAME` selects the project default workflow and is validated against `Hive::Workflows::Registry`; unknown names fail before disk writes and list valid names.
@@ -175,6 +177,32 @@ This branch is what the orphan worktree is initially based on, and what feature
 - `test/unit/commands/init_test.rb` covers small init collaborators, including the `ProjectConfigBinding` complete-answer path, top-level and nested missing-key fail-fast contracts, rollback helper behavior, daemon-registration warning paths, and JSON summary EPIPE handling.
 - `test/unit/commands/init/prompts_test.rb` covers prompt defaults and parsing, including the explicit default-yes architecture-patrol answer and the non-TTY summary. `test/integration/init_test.rb` and the hivebox setup tests pin terminal/web parity, explicit pre-write architecture-patrol selection, rejection of fresh-only selectors on every existing-project re-init path, config rendering, default-on fresh auto-fix/issue gates, explicit disablement, and inert legacy missing-block behavior. `test/unit/schema_files_test.rb` pins the current `schemas/hive-init.v2.json` projection, including `refactor_patrol_enabled`; v1 remains a compatibility schema.
 
+## Minimal workflow-creator profile
+
+`--minimal` is a non-interactive, fresh-target-only profile for an explicitly
+named `--new-workflow ID`. `--preview --json` emits
+`hive-init-preview.v1` after the same preflight and ID/collision resolution as
+execution, but writes nothing. The payload names planned project files, the
+Hive state worktree, context/wiki integration, global registration, services,
+background automation, and task creation.
+
+The minimal profile creates the core project/state registration and required
+context hooks while disabling patrol/refactor patrol, ad-hoc auto-fix, daemon
+dispatch/autostart, and babysitter/timer installation, including the llm-wiki
+scheduler timer. Bootstrap refuses pre-existing symlinks anywhere in its
+managed project paths and uses no-follow final writes, preserving outside
+targets and existing project choices. It never selects a
+starter template and rejects `--force`, missing `--new-workflow`, an existing
+Hive project/state branch, a non-fresh target, or a basename already registered
+to a different project path. The preview discloses the available registration
+name, and execution rechecks that name inside the locked registry write so a
+post-preview contender cannot be replaced. Minimal `--json` precondition,
+collision, already-initialized, and usage failures use the typed
+`hive-init-preview.v1` or `hive-init.v2` error envelope instead of prose. The
+natural-language creator presents this preview and waits for one explicit
+confirmation before invoking the same command without `--preview`; unanswered
+or declined confirmation therefore leaves the directory unchanged.
+
 ## Backlinks
 
 - [[cli]] · [[commands/run]] · [[commands/doctor]] · [[commands/setup-agents]]
diff --git a/wiki/commands/new.md b/wiki/commands/new.md
index 7c575c13d1..55ddcdd158 100644
--- a/wiki/commands/new.md
+++ b/wiki/commands/new.md
@@ -3,8 +3,8 @@ title: hive new
 type: command
 source: bin/hive, lib/hive/commands/new.rb, templates/idea.md.erb
 created: 2026-04-25
-updated: 2026-07-21
-tags: [command, capture, slug, task-id, commit-lock, workflow, dependencies, base-branch]
+updated: 2026-07-24
+tags: [command, capture, slug, task-id, idempotency, json, commit-lock, workflow, dependencies, base-branch]
 ---
 
 **TLDR**: `hive new PROJECT TEXT...` captures an idea: derives a slug, resolves the effective workflow (`--workflow` override, project `default_workflow`, then `coding`), scaffolds the descriptor's entry-stage folder plus state file and `meta.yml`, commits it on the `hive/state` branch, and best-effort starts display-name generation after the commit.
@@ -123,6 +123,8 @@ slug: add-inbox-filter-260603-abcd
 display_name:
 depends_on: api:base-task-260716-abcd
 base_branch: main
+idempotency_key: workflow-creator:editorial:stable
+input_fingerprint: 3f...
 ```
 
 `id` comes from the process-global counter at `Hive::Paths.task_counter_path` (`<state_home>/task-counter.yml`), protected by `<state_home>/.task-counter.lock`. `display_name` starts nil; `Hive::Task#display_label` falls back to the slug until name generation succeeds. `depends_on` is omitted when not supplied and remains the authoritative scheduling declaration when present. `workflow:` is omitted for plain coding captures, but set for explicit overrides and non-coding project defaults. `base_branch:` is omitted outside draft-PR workflows and is authoritative when present.
@@ -133,6 +135,41 @@ base_branch: main
 - `test/integration/new_wrapper_argv_test.rb` drives the real `bin/hive` subprocess and pins wrapper-only argv behavior: canonical `PROJECT --workflow ID "text"`, before-project and trailing options, `--workflow=ID`, combined dependency/workflow flags, literal `--workflow` substrings, lifted-but-plain `--json`, missing text after lifted options, and unrecognized options as task text.
 - `test/integration/tui_new_idea_attachments_test.rb` covers the TUI-internal rich submit path.
 
+## Idempotent machine-readable creation
+
+Automation may add `--idempotency-key KEY --json`. Hive stores the opaque key
+with a fingerprint of the normalized input, resolved workflow, dependency, and
+base branch. Idempotent attachments are copied once into a private temporary
+snapshot; the fingerprint and task asset both use those bytes, and the final
+copy is digest-verified. A pathname replacement or source edit between lookup
+and task creation therefore cannot make metadata describe different captured
+content. For owner-authored workflows the fingerprint also covers the
+descriptor and complete instruction tree, so changed owner content conflicts
+instead of reusing an older task. Hive rechecks that authored digest inside the
+commit boundary before lookup reuse and again before metadata publication; a
+mid-creation edit aborts and removes the candidate instead of binding a stale
+fingerprint to a different graph. Metadata admission fails closed if any
+candidate task metadata is malformed or unreadable. The managed-workflow
+mutation lock is acquired before the state commit lock, matching lifecycle
+writers. One commit lock now covers the state-wide key lookup, exclusive
+task-directory creation, metadata write, and commit. Same-slug contenders
+therefore cannot share a candidate directory, report two creations, overwrite
+one another, or remove a directory they did not create. A matching retry
+returns the original slug with `created: false` even after the task has moved;
+reuse for different input/workflow fails without a second task, and duplicate
+metadata is a fail-closed repair error.
+
+If a capture commit fails after staging, both idempotent and legacy paths
+remove the owned candidate and reset its exact state-index pathspec. Failures
+raised after directory ownership, including `EEXIST`, likewise remove only the
+owned candidate before propagating the original error.
+
+The `hive-new.v1` success payload contains `created`, `slug`, `workflow`,
+`current_stage`, `task_folder`, and a structured `next_action`. Callers without
+an idempotency key retain the existing capture behavior. The workflow creator
+uses this surface only when task creation was explicit in the original request;
+creation-only workflow authoring never invokes `hive new`.
+
 ## Backlinks
 
 - [[cli]] · [[commands/run]] · [[stages/inbox]]
diff --git a/wiki/commands/workflow.md b/wiki/commands/workflow.md
index 7f2e6028f7..6334cea6d0 100644
--- a/wiki/commands/workflow.md
+++ b/wiki/commands/workflow.md
@@ -4,7 +4,7 @@ type: command
 source: lib/hive/cli.rb, lib/hive/commands/workflow.rb, templates/workflows/
 created: 2026-06-21
 updated: 2026-07-24
-tags: [command, workflow, authoring, honeycomb, registry, archive, retention]
+tags: [command, workflow, authoring, validation, human-stage, honeycomb, registry, archive, retention]
 ---
 
 **TLDR**: `hive workflow` manages two ownership domains: `new` scaffolds trusted project-authored descriptors, while `install`, `list`, `update`, and `remove` manage immutable reviewed Honeycomb generations; `publish` validates an authored descriptor and opens a registry PR whose status is only `pending_review`.
@@ -15,6 +15,8 @@ tags: [command, workflow, authoring, honeycomb, registry, archive, retention]
 hive workflow new my-flow
 hive workflow new my-flow --template research
 hive workflow new my-flow --json
+hive workflow validate my-flow --json
+hive workflow commit my-flow
 hive workflow install honeycomb/repo-brief --yes
 hive workflow install honeycomb/repo-brief --yes --allow-escalation \
   --mapping stages.research=codex,model=gpt-5.6-sol,effort=high \
@@ -258,6 +260,67 @@ pointing at the directory of instructions to define (the single-stage blank
 still names its one `work.md`). An unknown `--template` is a USAGE error
 listing the available names; with `--json` they ride the `expected` array.
 
+## Read-only validation and human outcomes
+
+`hive workflow validate ID --json` resolves authored descriptors, built-ins,
+and the currently selected managed generation through direct read-only
+lookups. It does not acquire the managed mutation lock, reconcile a selected
+pointer, or replay/clear a transaction journal. It validates descriptor YAML,
+referenced instructions, stage inputs/state files, automatic edges, and
+descriptor-declared human outcomes without creating a task or writing project
+or Hive state. The `hive-workflow-validate.v1` result reports the descriptor
+origin/path, ordered stages, instruction paths, automatic edges, human
+outcomes, and `valid`. Invalid input uses the same command-specific envelope
+with diagnostics, including malformed `--json` invocations.
+
+Project-authored descriptors may declare a durable `kind: human` stage:
+
+```yaml
+- name: approval
+  kind: human
+  state_file: approval.md
+  input: draft.md
+  outcomes:
+    approve:
+      complete: true
+      artifact: draft.md
+    reject:
+      to: draft
+```
+
+Every outcome has exactly one action: `complete: true` with a required
+non-empty artifact basename, or `to: STAGE`. Human stages reject
+agent/model/permissions/instruction/runner settings and are never dispatched
+by the daemon. Apply a decision with
+`hive decide TASK OUTCOME --from STAGE --decision-id DECISION_ID [--note TEXT] [--json]`.
+The waiting `hive run --json` response supplies that visit-specific ID. The
+expected stage and decision identity make matching retries no-ops and reject
+stale or conflicting decisions. Matching concurrent retries are rechecked
+under the decision lock and return one apply plus idempotent no-ops. Completing
+outcomes accept only a no-follow regular file inside the task folder. A
+self-targeting outcome records the decision, leaves the task in place, and
+mints a fresh waiting identity instead of reporting a false move. Human state
+files are also read no-follow with inode verification. A completing outcome
+writes its decision record and `completed_at` from one clock, classifies as
+archived, and participates in the descriptor's archive visibility retention;
+commit failure or interruption rolls both files back.
+
+Create-only natural-language requests are handled by the canonical `/hive`
+skill's `hive-workflow-creator` route. It gates on the installed version,
+inventories IDs, scaffolds only through this command (or the minimal init path),
+edits only returned new paths, validates here, commits the populated descriptor
+and instruction directory with `hive workflow commit ID`, reports all defaults, and creates no
+task unless the original request explicitly asks for one. The populated-graph
+commit is required because `workflow new` commits only the initial scaffold.
+Both `workflow validate` and minimal-init preview bypass startup scheduler
+reconciliation, preserving their strict no-write contract. Scaffold collision
+checks treat dangling descriptor or instruction symlinks as occupied paths.
+Creation claims the instruction directory and descriptor with exclusive
+filesystem operations; rollback removes only paths claimed by that invocation,
+so a concurrent or raced scaffold is never overwritten or deleted.
+If a scaffold commit fails after staging, Hive resets those exact index
+pathspecs under the commit lock before removing the generated files.
+
 ## Backlinks
 
 - [[cli]]
diff --git a/wiki/gaps.md b/wiki/gaps.md
index b45b4fcad0..4359abb4e5 100644
--- a/wiki/gaps.md
+++ b/wiki/gaps.md
@@ -737,6 +737,21 @@ read-through alias has not been selected. Remove it only after warned projects
 have had a migration window and release notes make the strict boundary
 explicit; no version or publication decision is implied by the source change.
 
+## Workflow-creator protected OpenClaw proof awaits a credentialed run (2026-07-24)
+
+The natural-language workflow creator is covered hermetically through AE1–AE5,
+including real descriptor loading, human approve/reject decisions, minimal-init
+preview, collision refusal, and state-wide task idempotency. The protected
+OpenClaw workflow and fail-closed attestor are source/unit tested, but this
+implementation session had no release-gate OpenAI credential and therefore did
+not produce a trusted live attestation. Keep this gap open until the dedicated
+`live-workflow-creator` job succeeds for an exact protected-main candidate and
+the resulting Check Run binds the prompt, candidate skill digest, ordered Hive
+commands, exact editorial graph, zero task count, secret scan, and cleanup.
+The same proof must also attest one explicitly authorized task, one first-stage
+run, a no-op retry with the same idempotency key, and matching operational
+status.
+
 ## WorkLedger is an internal boundary, not a published format (2026-07-26)
 
 `Hive::WorkLedger` now has clean-load, structural descriptor, durable append,
diff --git a/wiki/log.d/20260722-natural-language-workflow-creator.md b/wiki/log.d/20260722-natural-language-workflow-creator.md
new file mode 100644
index 0000000000..49211847bb
--- /dev/null
+++ b/wiki/log.d/20260722-natural-language-workflow-creator.md
@@ -0,0 +1,24 @@
+## 2026-07-22 — Natural-language workflow creator
+
+- Added the `hive-workflow-creator` route inside the one canonical `/hive`
+  AgentSkill. It inventories the installed version/project/workflow IDs,
+  scaffolds only through Hive, validates the normalized graph, reports every
+  inferred default, and creates no task unless the original request asks.
+- Added durable descriptor-declared human outcomes and
+  `hive decide TARGET OUTCOME --from STAGE [--note TEXT] [--json]`. Human
+  stages remain `WAITING`, are never daemon-dispatched, record idempotent
+  decisions, and can complete with an artifact or return to a named stage.
+- Added read-only `hive workflow validate ID --json`, no-write
+  `hive init --new-workflow ID --minimal --preview --json`, and idempotent
+  machine-readable `hive new ... --idempotency-key KEY --json`.
+- The editorial acceptance path is exactly
+  `research -> draft -> approval`: approve records non-empty `draft.md` as
+  publish-ready and completes; reject records the decision and resets draft to
+  `WAITING`. No publish stage or external action is inferred.
+- Added hermetic AE1–AE5 acceptance coverage plus a separate protected OpenClaw
+  proof whose attestation binds the exact candidate skill, prompt, commands,
+  created files, validation graph, creation-only no-task result, explicit
+  create/run no-op retry, operational status, secret scan, and cleanup.
+- Updated manual/natural-language workflow docs and the canonical OpenClaw
+  projection. Hive-site `#23116` remains a non-blocking wording handoff; this
+  change does not select a release version, publish, tag, or deploy.
diff --git a/wiki/log.d/20260724-workflow-creator-review-fix-pass-2.md b/wiki/log.d/20260724-workflow-creator-review-fix-pass-2.md
new file mode 100644
index 0000000000..56e82a5a0e
--- /dev/null
+++ b/wiki/log.d/20260724-workflow-creator-review-fix-pass-2.md
@@ -0,0 +1,17 @@
+## 2026-07-24 — Workflow creator review fix pass 2
+
+- Preserved strict no-write workflow validation and minimal-init preview at
+  executable startup, kept minimal init free of scheduler timers, rejected
+  managed-path and scaffold symlink redirection, and added a locked
+  `hive workflow commit ID` path for populated owner-authored graphs.
+- Made human decisions reject non-local or symlinked completion artifacts,
+  record self-target outcomes with a fresh decision identity, and return
+  idempotent no-ops for matching concurrent retries.
+- Made task creation fingerprint owner-authored workflow bytes, fail closed on
+  corrupt metadata, use the canonical workflow-mutation/state-commit lock
+  order, and clean both owned candidates and staged index entries on failure.
+  Applied the same exact-path index rollback to failed workflow scaffold and
+  populated-graph commits.
+- Specified exact POSIX request quoting and deterministic retry-key derivation,
+  then expanded hermetic creator acceptance to cover version/confirmation
+  refusals and durable populated-graph commit evidence.
diff --git a/wiki/log.d/20260724-workflow-creator-review-hardening.md b/wiki/log.d/20260724-workflow-creator-review-hardening.md
new file mode 100644
index 0000000000..576cd7e6cb
--- /dev/null
+++ b/wiki/log.d/20260724-workflow-creator-review-hardening.md
@@ -0,0 +1,17 @@
+## 2026-07-24 — Workflow creator review hardening
+
+- Bound every human decision to the current `decision_id`, moved completion
+  artifact verification under the task/commit locks, rejected marker-only
+  artifacts, and separated completed-human status from archived tasks.
+- Made human-stage entry reject symlinked state files and copy a stable,
+  no-follow snapshot into the destination.
+- Made `workflow validate` strictly read-only for authored, built-in, and
+  managed workflows, including prepared managed journals, and gave malformed
+  JSON invocations their command-specific schema.
+- Made minimal-init collisions and failures machine-readable without replacing
+  an unrelated same-basename registration.
+- Serialized idempotent lookup, exclusive candidate creation, and commit so
+  same-slug contenders cannot share or overwrite a task directory.
+- Required the natural-language creator to commit its populated workflow graph
+  after validation, and synchronized the canonical/OpenClaw guidance, public
+  docs, wiki, schemas, and focused regressions.
diff --git a/wiki/log.d/20260727T192524Z-human-workflow-archive-integration.md b/wiki/log.d/20260727T192524Z-human-workflow-archive-integration.md
new file mode 100644
index 0000000000..23f67b689e
--- /dev/null
+++ b/wiki/log.d/20260727T192524Z-human-workflow-archive-integration.md
@@ -0,0 +1,22 @@
+## 2026-07-27 — Human workflow archive integration
+
+- Routed completed human stages through the shared `archived` action and
+  descriptor retention path instead of maintaining a human-only completion
+  bucket.
+- Stamped `meta.yml completed_at` from the durable decision timestamp and made
+  state plus metadata rollback together on commit failure or interruption.
+- Refused symlinked human decision-state reads with no-follow inode checks.
+- Made strict no-write startup routing select the actual top-level command, so
+  a workflow id named `workflow` cannot trigger scheduler reconciliation during
+  minimal-init preview and option placement cannot hide workflow validation.
+- Bound idempotent attachment fingerprints and task assets to one private byte
+  snapshot, with a final digest check before metadata is committed.
+- Treated a dangling `.hive-state` symlink as an occupied minimal-init target
+  instead of allowing initialization to reach an unsafe path.
+- Made workflow scaffolds claim their instruction directory and descriptor
+  exclusively, and limited rollback to paths the invocation actually created,
+  so a concurrent creator cannot be overwritten or deleted.
+- Extended task creation, workflow scaffolding/commit, and minimal-init rollback
+  boundaries to interrupts as well as ordinary exceptions.
+- Rechecked owner-authored workflow content inside idempotent creation before
+  reuse and metadata publication, aborting raced descriptor/instruction edits.
diff --git a/wiki/modules/task_action.md b/wiki/modules/task_action.md
index 6f8f2db9a3..6800e15e67 100644
--- a/wiki/modules/task_action.md
+++ b/wiki/modules/task_action.md
@@ -4,7 +4,7 @@ type: module
 source: lib/hive/task_action.rb
 created: 2026-04-26
 updated: 2026-07-25
-tags: [module, status, action, classifier, diagnostic]
+tags: [module, status, action, classifier, human-stage, diagnostic]
 ---
 
 **TLDR**: Classifier that turns a `(Hive::Task, Hive::Markers::State)` pair into a user-facing action with a stable key (per `Hive::Schemas::TaskActionKind`), a human label for `hive status` output, a copy-paste-executable shell command, an optional structured `next_action` for row-specific recovery, and (since 2026-05-16) a bounded `diagnostic` payload for red recovery rows. Used by `hive status` (action grouping + `tasks[].action`/`diagnostic` JSON fields), `hive run` (`next_action.command` / `rerun_with`), `hive approve` (`next_action.command` after a successful advance), `hive accept-finding` / `hive reject-finding` (`next_action.command` after a toggle), and `hive status --diagnose` (`#diagnostic` is the local fallback path when no agent-written artifact is fresh).
@@ -140,6 +140,19 @@ For non-coding workflows, command emission bypasses the coding
 emit `hive run <slug>` (plus `--project` when the status snapshot spans multiple
 projects, and `--stage` for run rows only when a stage collision was reported).
 
+Human descriptor stages use the dedicated `human_needs_input` classification:
+their non-complete marker state is `NEEDS_INPUT` with label
+`Awaiting human decision`, no daemon-dispatch command, and a structured
+`outcomes` list. The operator transition is deliberately separate from generic
+approval:
+`hive decide TASK OUTCOME --from STAGE --decision-id DECISION_ID`. A completed human stage
+uses the shared `archived` action even though its folder remains in the terminal
+human-stage directory. The completing decision writes `completed_at` from the
+same clock as its durable decision record, so descriptor retention applies
+without a separate human-only completion state. An identical decision retry is
+a no-op while stale or conflicting decisions fail against the expected stage
+and visit-specific decision ID.
+
 `--project <name>` is appended whenever `project_count > 1` so multi-project status output emits unambiguous commands.
 
 The slug is `Shellwords.shelljoin`-escaped so a slug containing shell metacharacters can't break the suggested command.
@@ -160,6 +173,7 @@ adapter before a baseline; downstream consumers do not invent condition state.
 | `lib/hive/commands/status.rb` | `annotate_actions` calls `TaskAction.for` per row and routes by `action_key` for grouping. JSON `tasks[].action`/`action_label`/`suggested_command`/`next_action` come from this. |
 | `lib/hive/commands/run.rb` | `friendly_command` and `approve_action` delegate; `next_action.command` and `rerun_with` use the workflow form. |
 | `lib/hive/commands/approve.rb` | `json_next_action` builds the post-advance command via `TaskAction.for(post_move_task)` so the user lands on a runnable form for the new stage. |
+| `lib/hive/commands/decide.rb` | Applies a descriptor-declared human outcome and reports the post-decision action/state. |
 | `lib/hive/commands/stage_action.rb` | `success_payload` includes a `next_action` block built from TaskAction. |
 
 ## Why a class, not a hash lookup?
diff --git a/wiki/modules/workflows.md b/wiki/modules/workflows.md
index 39ba50a84c..0e0eb35f62 100644
--- a/wiki/modules/workflows.md
+++ b/wiki/modules/workflows.md
@@ -4,7 +4,7 @@ type: module
 source: lib/hive/workflows.rb, lib/hive/workflow.rb, lib/hive/workflows/registry.rb, lib/hive/workflows/coding.rb, lib/hive/workflows/content.rb, lib/hive/workflows/bench.rb, lib/hive/workflows/descriptor_parser.rb, lib/hive/workflows/loader.rb, lib/hive/workflows/project.rb, lib/hive/workflow_package/
 created: 2026-04-26
 updated: 2026-07-26
-tags: [module, workflow, verbs, selection, honeycomb, registry, archive, retention]
+tags: [module, workflow, verbs, selection, human-stage, outcomes, honeycomb, registry, archive, retention]
 ---
 
 **TLDR**: The coding, content, bench, and project-authored workflows are described as ordered `Hive::Workflow` value objects whose stages carry directory names, state files, incoming advance verbs, runner metadata, optional instruction files, optional permission specs, per-stage agent/model/effort overrides, council reviewer configs, terminal deliverables, and an archive visibility retention policy. `Hive::Workflows::Registry.default` still returns the coding descriptor, and the legacy public constants (`Hive::Stages::DIRS`, `Hive::Task::STAGE_NAMES` / `STATE_FILES`, `Hive::Workflows::VERBS`) are derived from it at load time. `Hive::Task` resolves a per-task descriptor from `meta.yml workflow:` or project `default_workflow`, `Hive::WorkflowSelection` centralizes CLI validation and valid-name listing, `Hive::Workflows::Registry.all` exposes the live descriptor set for built-in, runtime/test, and active-project registrations, and `Hive::Stages::Resolver` consumes `kind: :agent` / `kind: :council` as fallbacks for non-coding stage names while coding's bespoke runners remain name-authoritative only for `:coding`. Coding's descriptor now uses runtime primitive kinds (`:execute`, `:review_council`, `:finalize`) for the worktree-coupled stages; the old `:marker` descriptor kind is retired.
@@ -57,7 +57,9 @@ Per-project descriptors live under `<hive_state_path>/workflows/*.yml`, defaulti
 - council stages require a `reviewers:` list; each reviewer declares exactly one of `skill`, `instruction`, `prompt`, or `command`, plus optional agent/model/effort/permissions/output basename. The `council:` block carries `quorum`, `max_rounds`, `exit_rule`, `on_max_rounds` (`wait` by default or `complete` for a bounded downstream delivery), `triage_output`, and optional `revise`.
 - `instruction:` paths are resolved relative to the descriptor directory and stored on the stage as absolute paths.
 - `permissions:` values are validated through `Hive::PermissionScope` at load time and later passed to the generic agent runner as the explicit permission spec.
-- the last stage may be inert, agent, or council. Active terminal stages require both `COMPLETE` and a non-empty deliverable before `TaskAction` classifies them as archived.
+- the last stage may be inert, agent, council, or human. Active terminal stages
+  require `COMPLETE` and their declared non-empty deliverable/artifact before
+  `TaskAction` classifies them as archived.
 - `workspace: worktree` plus `handoff: draft_pr` is one closed terminal-agent
   contract. Both fields must appear together and both `state_file` and
   `deliverable` must equal task-root `fix-report.md`. Parser and managed-package
@@ -111,7 +113,7 @@ The upgrade path was live-smoked on the existing hive-bench state checkout on
 `coding` tasks all remained resolvable, with all nine pre-migration status rows
 still visible and the unrelated dirty-state fingerprint unchanged.
 
-`hive workflow new ID` (see [[commands/workflow]]) scaffolds the minimal `inbox -> work -> done` descriptor plus `work.md` instruction and commits those files to `hive/state`. The only richer shipped scaffold is `--template research`; Architecture and Writing are installed as full reviewed Honeycomb packages so their agent-slot configuration remains operator-owned.
+`hive workflow new ID` (see [[commands/workflow]]) scaffolds the minimal `inbox -> work -> done` descriptor plus `work.md` instruction and commits those initial files to `hive/state`. After editing, the natural-language creator validates and invokes `hive workflow commit ID`, which commits the populated descriptor/instruction directory under the shared state commit lock before it reports success or creates a task. The only richer shipped scaffold is `--template research`; Architecture and Writing are installed as full reviewed Honeycomb packages so their agent-slot configuration remains operator-owned.
 
 ## Managed Honeycomb overlay
 
@@ -323,6 +325,32 @@ Hermetic coverage lives in `test/unit/workflows/content_test.rb`,
 `test/integration/content_workflow_stage_test.rb`, and
 `test/integration/content_workflow_e2e_test.rb`.
 
+## Durable human stages
+
+`Hive::Workflow::Stage` accepts `kind: :human` plus immutable named
+`outcomes`. The owner-authored parser exposes only closed directional actions:
+an outcome must either complete the workflow or target another stage, and a
+completing outcome may name an artifact. Unsafe outcome names, unknown keys,
+missing/duplicate actions, unknown targets, and agent-only settings fail while
+the descriptor is loaded.
+
+A human stage has no runner. Entering it leaves the state file `WAITING`, and
+status exposes `NEEDS_INPUT` with the descriptor's allowed outcomes.
+`Hive::Commands::Decide` writes a durable decision record with outcome, note,
+artifact/target, and timestamp. A completing decision verifies its declared
+artifact, stamps `meta.yml completed_at` from the same clock, and exposes the
+task through the shared archive/retention path. Decision-state reads refuse
+symlinks and verify the opened inode. State, metadata, and moves roll back
+together when a commit fails or is interrupted. A returning decision moves the
+same task under the task/state locks and resets the target state file to
+`WAITING`, preventing a stale completion marker from immediately advancing it
+again.
+
+The accepted editorial graph is exactly
+`research -> draft -> approval`: `approve` completes with non-empty
+`draft.md` recorded as publish-ready, while `reject` returns to `draft`.
+There is no publish stage or executable outcome action.
+
 ## Backlinks
 
 - [[commands/run]] — workflow verb dispatch via `Hive::Commands::StageAction`
diff --git a/wiki/testing.md b/wiki/testing.md
index b7e4a14ce0..d76d54466d 100644
--- a/wiki/testing.md
+++ b/wiki/testing.md
@@ -18,6 +18,33 @@ credentials; the live-agent workflow is an optional diagnostic that directly
 exercises native OpenClaw, Claude, Codex, and Pi discovery/use when credentials
 are available and can never replace a deterministic blocking row.
 
+The natural-language workflow creator has a hermetic primary acceptance gate
+in `test/integration/workflow_creator_e2e_test.rb`. It exercises AE1–AE5 through
+the real CLI collaborators: exact editorial approve/reject semantics,
+byte-identical collision refusal, no-write minimal-init preview followed by
+confirmed execution, explicit old-version and unconfirmed-preview refusals,
+durable populated-graph commit evidence, no-task default behavior, and
+state-wide idempotent task retry after movement. Focused parser, decision, validation, init, task-meta,
+skill projection, schema, package, and release-contract tests own the smaller
+contracts, including visit-bound decision IDs, marker-only/concurrently changed
+artifact rejection, no-follow human-state entry, read-only managed validation,
+minimal-init registration collisions, symlink refusal, timer disablement, and
+JSON failures, serialized idempotent creation with index rollback and authored
+content fingerprints, concurrent/self-target decision behavior, and the
+distinct completed-human action.
+
+`test/smoke/live_hive_workflow_creator_smoke_test.rb` is a separate,
+OpenClaw-only protected proof. The exact candidate projection receives the
+editorial prompt in a disposable initialized project. Its controlled Hive
+surface permits only version, workflow inventory, scaffold, validation, and
+the populated-graph commit before task creation.
+Attestation verifies the prompt digest, native `/hive` discovery, ordered argv,
+created-file digests, exact normalized graph, zero tasks after creation-only,
+then one created-and-run slug plus a no-op retry with the same idempotency key,
+operational status, no external actions, secret scanning, and cleanup. Missing
+provider credentials make this live gate explicitly unavailable; they never
+turn a skipped test into release evidence.
+
 ## Local feedback loop
 
 During implementation, run the smallest relevant test files directly:

Next: Open the public pull request for the complete change.

HiveDev, when it’s ready.

HiveDev is the hosted product being built around open-source Hive. It isn’t available yet; join the waitlist to hear when it is. We’ll use your email for HiveDev availability updates.

Read our privacy notice.