# pib-db Access Protocol

How to interact with the process infrastructure database (pib-db).

## Preference Order

1. **MCP tools (preferred):** If `pib_*` MCP tools are available (check
   by attempting to use them), use them directly. They return structured
   JSON — no parsing needed.

2. **CLI fallback:** If MCP tools are not available, use the CLI:
   ```bash
   node scripts/pib-db.mjs <command> [args]
   ```

Skills should reference this document instead of embedding their own
fallback logic. The access method is determined once at the start of the
skill execution:

```
Check: are pib_* MCP tools available?
  YES → use pib_list_projects, pib_create_action, etc.
  NO  → use node scripts/pib-db.mjs list-projects, etc.
```

### MCP invocation specifics (read before first use, not after a failure)

Two details that otherwise cost a failed call + a `ToolSearch` round-trip
every session:

- **Full tool names are prefixed `mcp__pib-db__`** (e.g.
  `mcp__pib-db__pib_query`). When loading deferred tools via `ToolSearch`,
  query `select:mcp__pib-db__pib_query,mcp__pib-db__pib_list_actions` —
  the bare `pib_*` names will not match.
- **`pib_query`'s parameter is `sql`, not `query`.** Calling it with a
  `query` argument fails (and can fail quietly). The CLI form is
  `query "SQL"`; the MCP form is `{ "sql": "SELECT ..." }`.

## Available Operations

| MCP Tool                   | CLI Equivalent                                  | Description                    |
| -------------------------- | ----------------------------------------------- | ------------------------------ |
| pib_create_project         | create-project "name" [--kind area\|project] [--notes X] [--parent prj:X] | Create a container. Default kind: area (never completes). kind=project requires the outcome in notes ("done when …"); `parentFid`/`--parent` names the AREA a project serves |
| pib_update_project         | update-project fid [--kind X] [--status X] [--parent prj:X \| --parent ""] ... | Update/reclassify a container, or re-file a project under a different area (`parentFid`; `null`/`""` clears it). Gated: done refused for areas; a project cannot close over open children; only a project may carry a parent and only an area may be one; an area holding projects can be neither promoted nor parked |
| pib_complete_project       | complete-project fid [--map-file f]             | Complete a PROJECT through the gate. fid-only returns the walk sheet (mutates nothing); refuses areas; never touches the parent area |
| pib_list_projects          | list-projects                                   | List active containers (each row carries kind, `parent_fid`/`parent_name`, `child_projects`, and both `open_actions` and `open_actions_rolled`) |
| pib_create_action          | create-action "text" --notes X                  | Create an action (work item)   |
| pib_list_actions           | list-actions [--status X] [--project X] [--scope direct\|rolled-up] | List actions. `scope` decides how a container filter reads: `direct` (default) is only what is filed straight to it, `rolled-up` also includes the actions of projects filed under an area |
| pib_update_action          | update-action fid --status X                    | Update action fields           |
| pib_complete_action        | complete-action fid                             | Mark action done               |
| pib_add_dependency         | add-dependency blocker-fid blocked-fid          | State that one action must finish before another can start |
| pib_remove_dependency      | remove-dependency blocker-fid blocked-fid       | Remove a dependency edge       |
| pib_list_dependencies      | list-dependencies fid                           | What it waits on, and what waits on it |
| pib_next_actions           | next-actions [--project X] [--limit N] [--scope direct\|rolled-up] | **What can I start right now** (derived); same `scope` reading as list-actions |
| pib_list_blocked           | list-blocked [--project X] [--scope direct\|rolled-up] | What is waiting, and on what; same `scope` reading as list-actions |
| —                          | propose-dependencies                            | Propose edges from prose (read-only) |
| pib_defer_with_trigger     | defer-with-trigger fid --trigger "<text>"       | Defer with a return condition  |
| pib_list_triggered         | list-triggered [--include-done]                 | List items waiting on triggers |
| pib_mark_trigger_checked   | mark-trigger-checked fid --result <value>       | Record a trigger evaluation    |
| pib_add_engagement_event   | add-event --engagement prj:X --kind K --author A --allowed-authors "..." | Append an engagement event |
| pib_list_engagement_events | list-events [--engagement prj:X] [--unaddressed-only]                    | List engagement events     |
| pib_mark_event_addressed   | mark-event-addressed id                         | Mark an engagement event addressed |
| pib_ingest_findings        | ingest-findings run-dir                         | Ingest audit findings          |
| pib_triage                 | triage finding-id status [notes]                | Triage a finding               |
| pib_triage_history         | triage-history                                  | Get suppression list           |
| pib_query                  | query "SQL"                                     | Run arbitrary SQL              |

## Containers nest exactly one level

An **area** holds projects, a **project** holds actions, and a project
may NOT contain another project — the write boundary refuses it. A
project names the area it serves with `parentFid` (CLI `--parent
prj:X`, or `--parent ""` to clear); an area is always a root and may
never carry one. Naming the parent is what makes the area's figures
account for the work, so any read that means "everything under this
area" passes `scope: 'rolled-up'` — the default `'direct'` counts only
what is filed straight to the container. When reporting an AREA, use
the rolled-up figure: an area whose work was carved into child projects
has zero direct actions and is not empty (plan §17, `act:e567d724`).

## Dependency vs trigger — which one to reach for

Two different kinds of "waiting", and they compose on the same action:

- **Waiting on another ACTION** → `pib_add_dependency`. Never write this as
  prose in a title, in notes, or in a trigger condition. An edge is read by
  `pib_next_actions`; prose is read by nobody. A write-time hook refuses a
  literal `act:` fid inside a trigger condition for exactly this reason.
- **Waiting on a DATE, an EVENT, or an OUTCOME** → `pib_defer_with_trigger`.
  Unchanged, and not going away.

A single action often needs both — *"one week after that merge lands"* is an
edge plus a time offset. State the edge, then keep only the residual condition
in the trigger.

**How they combine in the derived answer:** `pib_next_actions` returns an
action only when every blocker is finished AND its trigger condition has been
evaluated and found to have fired. A trigger's state is read from its
`trigger_checks` history, not from the mere presence of the field — so an item
whose condition fired is startable even though the text is still on the row.

## Deferred triggers

When deferring an item that waits on a specific identifiable
condition — a stack decision finalizing, a referenced file appearing,
a date arriving — use `pib_defer_with_trigger` instead of
plain `status='deferred'`. (If the thing it waits on is another *action*,
that is a dependency, not a trigger — see above.) The session loop re-evaluates each trigger every
session and surfaces items whose conditions have become true. See
[pib-db-triggers.md](pib-db-triggers.md) for the full convention:
result vocabulary, cascade semantics for containers, migration
guarantees, and known limitations.

## Engagement events

The `engagement_events` table (schema v5) is an append-only log for
consulting engagements managed by the **engagement** module. Each row
records one event — client feedback, an approval, a status push, a
delegation, a note, or a packet-sent marker — scoped to an engagement
(a pib-db project) and optionally to a specific action.

All writes route through `pib_add_engagement_event` (or the CLI
`add-event`); there is no raw-INSERT path. The caller must pass
`allowedAuthors` — the engagement's recipient ids plus `"consultant"`,
derived from `engagement.yaml`. The library itself never reads config;
the author allowlist is enforced **fail-closed** (an empty or omitted
allowlist rejects every author). `client_feedback` and `approval`
events must carry a meaningful verdict (`approve`/`object`/`comment`);
`none` and null are rejected for those kinds.

See [../engagement/engagement-schema.md](../engagement/engagement-schema.md)
for the full contract: event kinds, dedup semantics, action tag
conventions, the client-facing copy block, packet schema and lifecycle,
the consultant-side `_refmap`, the `item_feedback` envelope, and the
timelog/billing format.

## Surface Area Validation

`pib_create_action` (and the CLI `create-action`) require that notes
contain a `## Surface Area` section with at least one `- files:` or
`- dirs:` line. This ensures every action clearly defines what it
touches.

Example notes format:
```
Implement the new feature.

## Surface Area
- files: src/components/Widget.js
- files: src/utils/helpers.js
- dirs: tests/components/
```
