Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
126 commits
Select commit Hold shift + click to select a range
39cd1f5
docs(space): workflow hooks v2 design spec
lsm Aug 12, 2026
7e8dd62
feat(shared): add workflow hooks v2 type contracts
lsm Aug 12, 2026
9b9aff0
refactor(shared): keep business logic out of hook context; link not url
lsm Aug 12, 2026
f2c0162
feat(extensions): add hooks package; port post_approval_only
lsm Aug 12, 2026
11329ed
refactor(shared): drop connectors from HookContext
lsm Aug 12, 2026
8436c07
feat(extensions): github helper + port pr_merged
lsm Aug 12, 2026
c43cbee
feat(extensions): port pr_ready, review_posted, codex_review_approved
lsm Aug 12, 2026
a8aa915
feat(space): add v2 hook-binding storage on SpaceWorkflow
lsm Aug 13, 2026
a482f2a
Merge branch 'feat/workflow-hooks-v2' into space/workflow-hooks-v2-fi…
lsm Aug 13, 2026
eb22921
feat(space): workflow hooks v2 engine cutover (4b–4e)
lsm Aug 13, 2026
314e11a
Merge origin/dev: renumber hook migrations to 192/193/194 (dev #943 t…
lsm Aug 13, 2026
44a7ebc
fix(space): address review findings on the v2 hook cutover
lsm Aug 13, 2026
61c423a
fix(extensions/hooks): harden GraphQL + codex approval (review round 2)
lsm Aug 13, 2026
b9a49c0
fix(space): merge-gate P1, GraphQL injection, retry ceiling, perf (re…
lsm Aug 13, 2026
86be205
fix(space): export v4 + rate-limit GraphQL retryable + custom-hook va…
lsm Aug 13, 2026
f6e6cbf
test(space): update export-import handler version assertion to v4
lsm Aug 13, 2026
34a0003
fix(extensions/hooks): pr_url tolerance, gh pr view host check, drain…
lsm Aug 13, 2026
071d2b0
fix(space): follow-up timer leak, per-binding artifact queries, clean…
lsm Aug 13, 2026
f47b6bd
fix(space): PR-identity immutability, exact codex login, v3-hooks rej…
lsm Aug 13, 2026
6deaf70
fix(space): honor post-approval merge reports + engine human override…
lsm Aug 13, 2026
d279cbe
test(space): make engine orchestration suite runner-agnostic
lsm Aug 13, 2026
b0d858a
fix(space): bind exempted merge reports to reviewed PR + caller clean…
lsm Aug 13, 2026
de27412
fix(space): approval-consume fail-closed, GraphQL pagination, typed c…
lsm Aug 13, 2026
11905cb
fix(space): codex reaction dominance, createdAt identity ordering, ex…
lsm Aug 13, 2026
834e72e
test(space): pagination-loop integration coverage + GHE Cloud host tr…
lsm Aug 13, 2026
05b76ee
ci(extensions): run the extensions/hooks test suite in CI (round 10 P1)
lsm Aug 13, 2026
d6a7d41
ci(extensions): run extensions/hooks tests from the package directory
lsm Aug 13, 2026
9164a76
fix(space): cooldown ceiling, legacy-pin guard, reserved order, route…
lsm Aug 13, 2026
3ffd5d0
test(space): de-race the reserved-stamp ordering regression
lsm Aug 13, 2026
05afc35
fix(space): grace-timer leak, migration-194 ordering guard, gh auth t…
lsm Aug 13, 2026
baf9fda
fix(space): process-group reaping, migration deferral marking, follow…
lsm Aug 13, 2026
4c33e4a
fix(space): global reserved ordering, narrowed retry patch, schema pa…
lsm Aug 13, 2026
7e2e728
fix(space): script-env allow-list, fail-closed artifact reads, backof…
lsm Aug 13, 2026
815964a
test(extensions): use base64-shaped pagination cursors in fixtures
lsm Aug 13, 2026
f96255d
fix(space): isolated script HOME, blocking artifact-write failures, o…
lsm Aug 13, 2026
d6fba61
fix(space): drop inherited HYPERNEO_* env forwarding — credential key…
lsm Aug 13, 2026
dc9b450
fix(space): per-node ctx identity, evidence back-pagination, byte bud…
lsm Aug 13, 2026
854f6a1
fix(space): stamp-bound review evidence, reaction back-pagination (ro…
lsm Aug 13, 2026
503c76e
fix(space): initial reaction page read, unified backoff deadline (rou…
lsm Aug 13, 2026
df69568
fix(space): fail closed on an unresolvable bound hook (round 21)
lsm Aug 13, 2026
ecd4cf6
fix(space): non-routed targetNode rejected, durable-queue fail-closed…
lsm Aug 13, 2026
e598aa1
fix(space): block non-message retries whose bookkeeping cannot persis…
lsm Aug 13, 2026
6ee3b4f
fix(space): non-stalling queue-persist failure, boolean pageInfo (rou…
lsm Aug 13, 2026
d39932b
fix(space): migration-194 pinned-run deferral, slot-widening, head re…
lsm Aug 13, 2026
9bb9b0b
test(extensions): account for the head-recheck query in the endCursor…
lsm Aug 13, 2026
3d05075
fix(space): mixed-multicast gating, override eligibility, elapsed cei…
lsm Aug 13, 2026
4b8c715
fix(space): run-hook-then-override, generic-address gating, codex[bot…
lsm Aug 13, 2026
2bf5cec
test(extensions): unresolved thread without a URL counts with placeho…
lsm Aug 13, 2026
2e31073
fix(space): committed multicast gating, ineligible approvals, retry-c…
lsm Aug 13, 2026
e944cdb
fix(space): thrown-hook ineligibility, stop-path clears, name-paired …
lsm Aug 13, 2026
deff16c
fix(space): consume approvals on continue, corrupt-artifact fail-clos…
lsm Aug 13, 2026
c874e10
fix(space): cancelled-branch cycle reset, profile fail-closed, null-t…
lsm Aug 13, 2026
25b05c3
fix(space): distinct queued actions, exec-failure ineligibility, guar…
lsm Aug 13, 2026
512fcc6
chore(space): merge dev (delivery_mode m192), renumber hook migration…
lsm Aug 13, 2026
176201e
test(space): marker-key assertion follows the 197 renumber
lsm Aug 13, 2026
b80fc43
fix(space): per-action durable queue, per-hook-id placements, deadlin…
lsm Aug 13, 2026
a20afe6
test(space): admissibility fixtures use resolvable distinct hook ids
lsm Aug 13, 2026
61d0767
fix(space): queue tombstones, per-key delivery, gh infra ineligibilit…
lsm Aug 13, 2026
ae184fc
chore(space): merge dev (channel_cycle_events m193); clear ceiling ma…
lsm Aug 13, 2026
c8dbcdc
fix(space): scoped queue lookup, cooldown approvals, CAS approvals (r…
lsm Aug 13, 2026
27b4fc9
fix(space): per-entry key search, pr-view validation, slot routabilit…
lsm Aug 14, 2026
1f42149
test(space): direct coverage for the round-34-36 fail-closed boundari…
lsm Aug 14, 2026
ead84b8
fix(space): block delivery when pre-delivery hook state persist fails…
lsm Aug 14, 2026
533d81c
docs(space): restrict custom script hooks to stateless flow decisions…
lsm Aug 14, 2026
5a8ed8e
fix(space): router parity, epoch boundaries, queue compaction, replay…
lsm Aug 14, 2026
b1a67ba
fix(space): retryHook per-key clears, m197 post-approval deferral, ep…
lsm Aug 14, 2026
8625047
fix(space): keyed cancelled clears, retry-RPC durability, router pari…
lsm Aug 14, 2026
92a880f
fix(space): scoped queue writes, node/envelope validation (round 46)
lsm Aug 14, 2026
d101ffe
fix(space): array-wildcard parity, mergeable guard, codex node fail-c…
lsm Aug 14, 2026
84ec9fc
fix(space): all-or-nothing multicast auth, comment pagination, export…
lsm Aug 14, 2026
5092190
fix(space): unauthorized worker entry refuses the whole generic-path …
lsm Aug 14, 2026
24182b0
fix(space): sequential generic-path gates, head-commit fail-closed, a…
lsm Aug 14, 2026
c1f9303
fix(space): slot decode parity, timer preservation, comments connecti…
lsm Aug 14, 2026
baaab92
fix(space): skip outcome-irrelevant scans after decisive evidence (ro…
lsm Aug 14, 2026
000fc1d
fix(space): translated-multicast model, scan flag/boundary validation…
lsm Aug 14, 2026
f1e9e29
fix(space): multicast dedup, bare-slot auth, ceiling marker, per-page…
lsm Aug 14, 2026
720ecb9
fix(space): array slot-fallback, atomic identity stamp, outcome-irrel…
lsm Aug 14, 2026
c6d1e24
fix(space): deferred approval consumption, claim normalization, sessi…
lsm Aug 14, 2026
3e534e7
fix(space): foreign-worker misses, retry approval invalidation, verif…
lsm Aug 14, 2026
4525f12
fix(space): own-PR early-exit condition, reserved legacy id, script t…
lsm Aug 14, 2026
63cb55f
fix(space): authorize @role targets before running node gates (round 58)
lsm Aug 14, 2026
b0bddaf
fix(space): array role authorization, backward codex review paginatio…
lsm Aug 14, 2026
6689b4f
fix(space): role-resolver cross-product, pre-armed approval guard, re…
lsm Aug 14, 2026
a27af69
fix(space): role-authorized routability, mid-node worker aborts (roun…
lsm Aug 14, 2026
56a833d
fix(space): per-node role scoping, atomic approval consume, window-bo…
lsm Aug 14, 2026
b808cb0
style(space): drop useless spread fallback
lsm Aug 14, 2026
d94c1e8
fix(space): boundary recheck+reactions, delivered-occurrence gates, s…
lsm Aug 14, 2026
9dc5f42
test(space): @role coverage, pinned early exit, reserved-id/armed-app…
lsm Aug 14, 2026
acc359d
fix(space): boundary-path parity, pinned suppression tests, survivor-…
lsm Aug 14, 2026
9ceab71
fix(space): boundary reaction early-stop + version-guarded hook-state…
lsm Aug 14, 2026
e225958
fix(space): per-node plain-slot suppression + positive-evidence early…
lsm Aug 14, 2026
4d3322a
fix(space): token-bound approval consume, sequential scalar fan-out, …
lsm Aug 14, 2026
6036e86
fix(space): restamp-convergence guard on the legacy hooks drop (round…
lsm Aug 14, 2026
2e06503
fix(space): action-bound approvals + retry-flow guard on retryHook (r…
lsm Aug 14, 2026
5a4ac31
fix(space): delivery-time consume ordering, migration backup precheck…
lsm Aug 14, 2026
c1f392c
fix(space): binding element validation, named-slot authorization, dur…
lsm Aug 14, 2026
247c49e
fix(space): atomic batch consume, canonical action identity, thread e…
lsm Aug 14, 2026
6cd59e5
fix(space): identity-replacement CAS + rate-limit reset backoff (roun…
lsm Aug 14, 2026
ca808e1
fix(space): execution-order bare-slot parity + boundary seed prelimin…
lsm Aug 14, 2026
f6152ad
fix(space): strict oid head-binding, deep binding validation, GraphQL…
lsm Aug 14, 2026
f3691de
fix(space): refuse corrupt-hook exports, jitter floor, ceiling-approv…
lsm Aug 14, 2026
c2f46f9
fix(space): require usable authorized callers in decoded bindings (ro…
lsm Aug 15, 2026
8a2325c
fix(space): usable-caller validation, routing-store fail-close, ident…
lsm Aug 15, 2026
ec86375
fix(space): require an own-source caller in decoded bindings (round 7…
lsm Aug 15, 2026
966c88f
fix(space): repo-backed identity re-read + routed-target validation (…
lsm Aug 15, 2026
9f751e0
fix(space): node-reference validation, deep custom-hook fields, execu…
lsm Aug 15, 2026
7a99b53
fix(space): reject decoded callers naming undeclared slots (round 78 …
lsm Aug 15, 2026
2c14e75
fix(space): execution-status parity for role-holder activity (round 7…
lsm Aug 15, 2026
2a0470f
fix(space): marker anti-laundering, legacy remediation docs, db_query…
lsm Aug 15, 2026
6d708f0
fix(space): legacy-migration completeness, transient routing-stop rec…
lsm Aug 15, 2026
1144138
fix(space): sibling-aware retry-bookkeeping resets (round 82 review P2)
lsm Aug 15, 2026
c209d20
fix(space): validator-id coverage, replacement-list validation, malfo…
lsm Aug 15, 2026
6d07a33
fix(space): marker-save refusal, slot-scoped role activity, retry-pat…
lsm Aug 15, 2026
eaaa3cf
fix(space): reject empty decoded agent-slot allowlists (round 83 codex)
lsm Aug 15, 2026
5defb1e
fix(space): rollback restores elapsed stamp, replacement sessions re-…
lsm Aug 15, 2026
4ae71a7
fix(space): clearLegacyHooks RPC strip, export partial-coverage refus…
lsm Aug 15, 2026
0c99cff
fix(space): per-placement legacy-hook coverage (round 85 codex)
lsm Aug 15, 2026
1ea7c8f
fix(space): required-flag decoding + actor-role activity normalizatio…
lsm Aug 15, 2026
e709cde
fix(space): strict legacy-column decode + full binding validation in …
lsm Aug 15, 2026
25f4097
fix(space): in-transaction token validation, route-matching coverage,…
lsm Aug 15, 2026
8c5a9a6
fix(space): divergent-only identity recheck in review hooks (round 88…
lsm Aug 15, 2026
28aaf62
fix(space): source-node coverage, stamp-fail override lockout, diverg…
lsm Aug 15, 2026
25805bd
fix(space): duplicate-id decoding, session-independent approval ident…
lsm Aug 15, 2026
5414eb8
fix(space): json-type + colliding-id decode rejection, marker-loaded …
lsm Aug 15, 2026
b19a140
fix(space): duplicate-id migration deferral, run-scoped post-approval…
lsm Aug 15, 2026
8a30b7e
fix(space): bash-only interpreter decode, custom-definition validatio…
lsm Aug 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,15 @@ jobs:
run: bun run check:db-schema-parity
continue-on-error: true

# The extensions/hooks suite (GraphQL extractors, pagination loops,
# trusted-host gating) runs under `bun test` — without this step nothing
# in CI executes it (tsc only type-checks the package).
- name: Run extensions/hooks unit tests
id: extensions-hooks-tests
run: bun test tests
working-directory: packages/extensions/hooks
continue-on-error: true

- name: Verify exact version pins
id: exact-pins
run: |
Expand All @@ -127,7 +136,7 @@ jobs:
continue-on-error: true

- name: Check for failures
if: steps.lint.outcome == 'failure' || steps.format.outcome == 'failure' || steps.knip.outcome == 'failure' || steps.typecheck.outcome == 'failure' || steps.db-schema-parity.outcome == 'failure' || steps.exact-pins.outcome == 'failure'
if: steps.lint.outcome == 'failure' || steps.format.outcome == 'failure' || steps.knip.outcome == 'failure' || steps.typecheck.outcome == 'failure' || steps.db-schema-parity.outcome == 'failure' || steps.exact-pins.outcome == 'failure' || steps.extensions-hooks-tests.outcome == 'failure'
run: |
echo "One or more checks failed:"
echo " Lint: ${{ steps.lint.outcome }}"
Expand All @@ -136,6 +145,7 @@ jobs:
echo " Typecheck: ${{ steps.typecheck.outcome }}"
echo " DB schema parity: ${{ steps.db-schema-parity.outcome }}"
echo " Exact pins: ${{ steps.exact-pins.outcome }}"
echo " Extensions/hooks tests: ${{ steps.extensions-hooks-tests.outcome }}"
exit 1

# ============================================
Expand Down
17 changes: 17 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

239 changes: 239 additions & 0 deletions docs/features/workflow-hooks-v2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
# Workflow Hooks v2

Status: design spec. Drives the `feat/workflow-hooks-v2` PR series.

## Principle

Hook bodies are **business logic** ("a PR must be mergeable", "a review must be
posted"). Business logic does not live in the daemon. The daemon keeps only
infrastructure: the chain engine, routing, persistence, sandboxing, and the
capability context it injects into each hook. The rules move to a dedicated
package.

## 1. Two layers

A hook and its placement in a workflow are separate things.

**Layer 1 — the hook (definition, reusable):**

```ts
interface Hook {
id: string; // e.g. 'pr_ready'
requiredData: DataField[]; // the input contract (data)
run: (action: HookAction, ctx: HookContext) => Promise<HookReturn>;
}

interface DataField {
key: string;
type: 'string' | 'number' | 'boolean' | 'link';
required: boolean;
description?: string;
}
```

A hook knows nothing about nodes or methods. The function knows what data it
needs, so `requiredData` lives here.

**Layer 2 — the binding (how a workflow uses a hook):**

```ts
interface HookBinding {
hookId: string; // references a Layer-1 hook, or a local custom hook id
sourceNode: string;
targetNode: string;
method: HookMethod; // send_message | submit_for_approval | mark_complete | ...
order: number;
enabled: boolean;
authorizedCallers?: AuthorizedCaller[];
}
```

A workflow is nodes + channels + `hookBindings[]`. Built-in hooks are referenced
by `hookId`; custom hooks are defined inline (§3) and referenced the same way.

## 2. Package layout

Built-in hooks live in **`packages/extensions/hooks`**
(`@hyperneo/extensions-hooks`), depending only on `@hyperneo/shared`. It exports
the built-in `Hook` definitions. `run` receives a `HookContext` (defined in
shared, implemented by the daemon) for every capability — the package never
imports daemon internals. `packages/extensions/` is the home for hook extensions
and any future extension kinds.

## 3. Custom / script hooks

User-authored hooks cannot be TypeScript (it is not safe to run arbitrary TS),
so they are bash scripts defined per-workflow in a local `hooks[]`:

```ts
interface CustomHook {
id: string;
requiredData: DataField[];
run: { kind: 'script'; interpreter: 'bash'; source: string; timeoutMs?: number };
}
```

The daemon executor branches on the hook kind: a built-in → call the registry's
TS function; a custom → spawn the sandboxed bash. `requiredData` is authored
alongside either.

**Trust boundary.** Custom scripts run as the daemon's own OS user. The
restricted environment, isolated `HOME`, process-group reaping, and bounded
buffers are hygiene — NOT an OS sandbox: a script can resolve the account's
real home via the passwd database and read daemon-owned files. Only run
custom hooks from workflows you author; importing a bundle imports its
scripts with the daemon's filesystem privileges. An OS-level sandbox for
imported scripts is a tracked follow-up.

**Custom script hooks are restricted to stateless flow decisions.** The
script's only bridge to the run is a read-only snapshot environment
(`HYPERNEO_PARAMS_JSON`, `HYPERNEO_CURRENT_ARTIFACTS_JSON`, run/node/task
identity — see `buildScriptEnv`), and its stdout is consumed as flow metadata
only (`flow` / `reason` / `payload` / `retryAfterMs`); every other field,
including `result`, is logged and ignored. There is deliberately **no script
bridge to the `HookContext` side-effecting methods** (`readState`,
`recordState`, `queueFollowUp`, `writeArtifact`) — bash cannot call the
injected JS functions, and snapshots of mutable state would invite
lost-update bugs. A hook that needs state, follow-ups, or artifacts must be a
built-in (or a new built-in added to `@hyperneo/extensions-hooks`). A bounded
file/stdout protocol for script side effects is a tracked follow-up; until it
lands, the custom-hook contract is exactly "decide the flow".

## 4. Return contract — hooks own their side effects

```ts
type Flow = 'continue' | 'stop' | 'retry';

interface HookReturn {
flow: Flow;
reason?: string; // shown to the agent on stop / retry
payload?: Record<string, unknown>; // optional rewrite of the action params before delivery
retryAfterMs?: number;
result?: unknown; // optional: record of what the hook did (audit/log/UI)
}
```

There is **no `effects` field** and **no `validator` / `side_effect`
classification**. The hook performs its side effects itself, inside `run`, by
calling `HookContext` methods (§5) — record state, queue a follow-up, write an
artifact. The engine's only obligations for the return are:

- honor `flow` — `continue` proceeds to delivery, `stop` blocks delivery and
ends the chain, `retry` re-runs after engine-managed backoff;
- apply `payload` to the action if present;
- log / ignore `result` — never act on it.

Bindings run in `order`. A `stop` ends the chain.

## 5. HookContext — capabilities the daemon injects

```ts
interface HookContext {
runId: string;
workspacePath: string;
taskId: string;
taskStatus?: string;
runStartedAt?: number;

readState(key: string): unknown;
recordState(key: string, value: unknown): void;
queueFollowUp(targetNode: string, message: string): void;
writeArtifact(artifact: ArtifactInput): void;
readArtifacts(): Artifact[];
}
```

All side-effecting methods are implemented by the daemon. The hook decides
*what* to do; the daemon *executes* it through this interface. This is what
keeps the extensions package free of daemon internals.

## 6. requiredData → prompt generation

`buildRoleSection` derives a route's data contract generically: for a given
`sourceNode → targetNode / method`, union the `requiredData` of every bound hook
(built-in looked up by id, custom inline) and emit the `send_message` data
instruction from it. This removes the hardcoded `{'pr_ready','review_posted'}`
special-case and the stale-prompt drift that the gate removal surfaced.

## 7. Built-in hooks (move into `extensions/hooks`)

`pr_ready`, `review_posted`, `post_approval_only`, `pr_merged`,
`codex_review_approved` — each becomes `{ id, requiredData, run }`, with `run`
lifted from the current validator files. Seed `requiredData`:

- `pr_ready` → `[{ key:'pr_link', type:'link', required:true }]`
- `post_approval_only` → `[{ key:'pr_link', ... }, { key:'reason', ... }]`
- `review_posted`, `pr_merged`, `codex_review_approved` → their respective inputs.

## 8. What dies (no migration)

Destroy, do not migrate:

- `WorkflowHook.validator`, `classification`, and the six-variant
`WorkflowHookResult`;
- `buildHookValidatedHandoffLines` and its hardcoded validator set;
- the welded `sourceNode` / `targetNode` / `method` on the hook object;
- the old built-in-validator registry / presets shape and the validator files.

Re-seed built-in workflows from the new binding shape. Persisted hook state is
re-seeded, not migrated.

## 9. PR sequence

1. **docs** — this spec.
2. **shared types** — `Hook`, `HookBinding`, `CustomHook`, `HookReturn`,
`HookContext`, `DataField`; remove the old hook types.
3. **`extensions/hooks` package** — the five built-in hooks (`run` +
`requiredData`) lifted out of the daemon.
4. **daemon engine** — `executeAction` driven off `flow` + `payload`; binding
storage; `HookContext` implementation + injection; the script sandbox stays.
5. **built-in workflows + re-seed** — rewritten as bindings.
6. **web editor** — "define hook" vs "place binding"; `requiredData`-driven
contract display.
7. **tests** — retire the old hook-shape suites; add chain/flow and
prompt-generation coverage.

## 10. Operational remediation after the cutover

Two post-upgrade states need operator action. Both fail CLOSED (every
hookable action on an affected workflow is blocked; nothing runs ungated).

### Corrupt hook columns (`__corrupt_hook_bindings__`)

The repository loads a workflow whose persisted `hook_bindings`/`custom_hooks`
column cannot be decoded (bad JSON, wrong shape, or any element the
create/update validator would reject — method enum, node references, callers,
custom-hook fields) with a synthetic marker binding whose reserved id resolves
to no hook. Every hookable action stops with that diagnosable id, and
`exportWorkflow` refuses the export with a repair message.

**Remediation:** re-author the workflow's hook bindings in the visual editor
(or via `spaceWorkflow.update`), or clear them deliberately. Editor saves and
the startup restamp strip the marker before persisting — the synthetic state
can never launder itself into real configuration. Unrelated edits are NOT
wedged (the marker is filtered from validation) and leave the corrupt column
untouched, so the fail-closed protection persists until the column is healed.

### Legacy pre-v2 hooks (custom workflows)

A CUSTOM workflow whose immutable definition still carries the legacy `hooks`
array blocks every hookable action on its runs after the cutover (the
`__legacy_hooks__` guard) — there is no automatic translation, by design.
Built-in workflows migrate automatically: the startup restamp installs v2
`hook_bindings` once their runs finish (migration 197 defers the legacy-column
drop until then).

**Remediation:** archive the affected task, then either re-create the
workflow's hooks as v2 hook bindings (visual editor or `spaceWorkflow.update`,
clearing the legacy hooks) or delete and re-create the workflow. Until then
runs of that workflow stay blocked and the workflow cannot be exported.

**Known limitation (step-6 deferral):** there is no visual editor for v2 hook
bindings yet — authoring today goes through the workflow RPCs/import. The
web editor rework is tracked as the follow-up step 6 of this plan.

## 11. Deferred / out of scope

Persisted-state migration (explicitly skipped), hook versioning across spaces,
per-binding `requiredData` overrides, an editor marketplace for sharing custom
hooks.
6 changes: 6 additions & 0 deletions docs/features/workflow-hooks.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
> **Deprecated (hooks v2).** This document describes the removed v1 hook
> model (`hooks[]`, connectors, `effects`/`side_effect` classification). It was
> hard-cut in the v2 cutover — see [workflow-hooks-v2.md](./workflow-hooks-v2.md)
> for the current two-layer model (`Hook` + `HookBinding`, `HookContext`,
> flow-only returns).

# Workflow hooks

Workflow hooks replace legacy workflow gate polling for MCP action checks. They run inside the daemon before a node-agent MCP action such as `send_message`, `save_artifact`, or `approve_task`. Hook results are persisted before the underlying action handler runs, so `side_effect` classification means "non-blocking pre-action side effect", not post-success handling.
Expand Down
1 change: 1 addition & 0 deletions knip.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"**/packages/e2e/tests/**",
"**/packages/shared/tests/**",
"**/packages/messaging/tests/**",
"**/packages/extensions/hooks/tests/**",
"**/packages/ui/tests/**",
"**/packages/ui/demo/**",
"packages/shared/src/neo-prompt/**",
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
"type": "module",
"packageManager": "bun@1.3.13",
"workspaces": [
"packages/*"
"packages/*",
"packages/extensions/*"
],
"scripts": {
"test": "echo \"Error: This is a monorepo. Do not run 'bun test', 'bun run test', or 'npm test' from the root.\" && echo \"\" && echo \"For daemon unit shards: ./scripts/test-daemon.sh\" && echo \"For targeted daemon tests: cd packages/daemon && bun\" \"test ./tests/unit/some-test.test.ts\" && echo \"For web tests: cd packages/web && bunx vitest run\" && echo \"For e2e tests: make run-e2e TEST=tests/features/foo.e2e.ts\" && echo \"See CLAUDE.md for more details.\" && exit 1",
Expand Down
1 change: 1 addition & 0 deletions packages/daemon/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"@github/copilot-sdk": "0.3.0",
"@huggingface/transformers": "4.2.0",
"@modelcontextprotocol/sdk": "1.29.0",
"@hyperneo/extensions-hooks": "workspace:*",
"@hyperneo/shared": "workspace:*",
"croner": "10.0.1",
"simple-git": "3.36.0",
Expand Down
5 changes: 0 additions & 5 deletions packages/daemon/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,6 @@ import {
import { createReactiveDatabase } from './storage/reactive-database';
import { LiveQueryEngine } from './storage/live-query';
import { SpaceAgentRepository } from './storage/repositories/space-agent-repository';
import { WorkflowHookRuntimeService } from './lib/space/workflow-hook-runtime-service';
import { WorkflowHookStateRepository } from './storage/repositories/workflow-hook-state-repository';
import { SpaceLongHorizonAgentRepository } from './storage/repositories/space-long-horizon-agent-repository';
import { SpaceAgentManager } from './lib/space/managers/space-agent-manager';
Expand Down Expand Up @@ -222,8 +221,6 @@ export interface DaemonAppContext {
spaceWorktreeManager: SpaceWorktreeManager;
/** Persistent workflow hook-local state repository */
workflowHookStateRepository: WorkflowHookStateRepository;
/** Runtime helper for hook caller and result validation */
workflowHookRuntimeService: WorkflowHookRuntimeService;
/** Persistent job queue repository */
jobQueue: JobQueueRepository;
/** Persistent job queue processor */
Expand Down Expand Up @@ -331,7 +328,6 @@ export async function createDaemonApp(options: CreateDaemonAppOptions): Promise<
// Initialize job queue
const jobQueue = new JobQueueRepository(db.getDatabase());
const workflowHookStateRepository = new WorkflowHookStateRepository(db.getDatabase());
const workflowHookRuntimeService = new WorkflowHookRuntimeService();
const maxConcurrent = Number(process.env.HYPERNEO_JOB_QUEUE_MAX_CONCURRENT) || 5;
const jobProcessor = new JobQueueProcessor(jobQueue, {
pollIntervalMs: 1000,
Expand Down Expand Up @@ -1456,7 +1452,6 @@ export async function createDaemonApp(options: CreateDaemonAppOptions): Promise<
taskAgentManager,
spaceWorktreeManager,
workflowHookStateRepository,
workflowHookRuntimeService,
jobQueue,
jobProcessor,
appMcpManager,
Expand Down
Loading