# @stravica-ai/engineering-pa-cli

Identity: `@stravica-ai/engineering-pa-cli` — operator-facing CLIs.

## Status

**Skeleton — v0.1.0.** Workspace-internal, `"private": true`, not individually published.

Six bin entries are declared and ship as placeholders that print "not yet implemented" and exit 0. Phase 1 Task 4 ports the real implementations from the predecessor `Stravica/dave` `core/tools/` tree.

| Bin | Source | Lands in |
|---|---|---|
| `pa` | new (PA-instance control plane) | Task 4 |
| `desk` | `core/tools/desk/` | Task 4 |
| `secrets` | `core/tools/secrets/` | Task 4 |
| `scheduler` | `core/tools/scheduler/` (or equivalent) | Task 4 |
| `sync` | `core/tools/sync/` | Task 4 |
| `memory-sync` | `core/tools/memory-sync/` | Task 4 |

`pa-init` is intentionally NOT in this package — it lives under `packages/scaffold/bin/pa-init` per the brief's "scaffold/ = pa-init + onboarding flows" mapping.

## Layout

```
packages/cli/
  index.mjs            # shared placeholder entrypoint with runPlaceholder()
  package.json         # name, version, bin entries (6), scripts
  README.md            # this file
  bin/
    pa
    desk
    secrets
    scheduler
    sync
    memory-sync
  test/
    index.test.mjs
```

Each bin shim is a one-line Node script that calls `runPlaceholder(toolName, argv)` from `index.mjs`. When Task 4 lands real CLIs, the shims become standard tooling-spec shims pointing at `src/<tool>/cli.mjs` modules and the chokepoint pattern (`src/<tool>/runner.mjs`) per the tooling standard §7.

## Commands

In v0.1.0, every bin behaves identically:

```bash
<tool> --help    # prints usage, exits 0
<tool>           # prints "not yet implemented", exits 0
<tool> anything  # prints "unknown verb" to stderr, exits 1
```

### `pa project` — project state writes

Write-side complement to `desk projects`. Source of truth is
`projects/<id>/project.json`; the CLI rewrites manifests in place,
preserving the operator-repo 2-space indent and trailing newline.

```bash
pa project create <id> --name "..." --category <c>           # scaffold projects/<id>/{project.json, README.md}
                       [--priority 0|1|2|3] [--owner ...] [--assignee ...] [--summary "..."]

pa project status <id> <draft|planned|in-progress|blocked|done|archived>
                                                              # set status; stamps `shipped` on done

pa project doc add  <id> --label "..." --path "..." [--group "..."]
                                                              # append a doc to a group (default: "Documents")
pa project doc rm   <id> --path "..."                         # remove the entry whose path matches

pa project list [--status ...] [--category ...] [--all] [--json]
```

`docs` is a **grouped object** keyed by group name → array of `{label, path}`:

```json
{
  "docs": {
    "Documents": [ { "label": "README", "path": "projects/foo/README.md" } ],
    "Plans":     [ { "label": "Brief",  "path": "projects/foo/brief.md"  } ]
  }
}
```

Legacy flat-array manifests are accepted on read and migrated to the
grouped shape transparently on the first `pa project doc add` write.
The workspace project-detail page renders both shapes.

### `pa work` — work-item state writes

Write-side complement to `desk work`. Items live at
`work/w-YYYY-MM-DD-NNN.json`; the CLI auto-numbers `NNN` against today's
existing items.

```bash
pa work create [--project <id>] --title "..." --category <c>
               [--priority 0|1|2|3] [--summary "..."] [--owner ...] [--assignee ...]

pa work status <id> <draft|planned|in-progress|blocked|done|archived>
                                                              # set status; stamps `completedAt` on done

pa work list [--project <id>] [--status ...] [--category ...]
             [--mine] [--theirs] [--all] [--json]
```

`--mine` resolves the PA name from `operator/config/preferences.json`
(`paName` / `agentName`), defaulting to `"pa"`.

### `pa knowledge` — query the operator's knowledge graph

Reads `<repo-root>/operator/knowledge/.graph.json` and exposes five query
verbs for the operator's knowledge system, plus `rebuild` for the graph itself. Feeds the third leg of the
PA operating model — knowledge retrieval discipline — so agents can
extract the right snippet at the right size instead of reading whole
files.

```bash
pa knowledge related <slug> [--in|--out]     # 1-hop neighbourhood
pa knowledge search  <term> [--full]         # find docs matching a term
pa knowledge domain  <name>                  # all docs in a domain + internal edges
pa knowledge orphans [--exclude-stubs]       # docs with no inbound edges
pa knowledge path    <a> <b>                 # shortest path between two docs
pa knowledge rebuild [--format json]         # delete and rebuild the graph; prints nodes, edges, ms
```

The graph is per-device and untracked. The first query builds it when it
is absent; nothing schedules a rebuild, so run `pa knowledge rebuild`
after the knowledge tree changes.

**Scan roots.** The home `operator/knowledge/` and `core/knowledge/`, then
each clone in `LIBRARY_KNOWLEDGE_EXTRA_ROOTS` (colon-separated clone
roots). A clone is matched to its registry entry by its `origin` remote
(normalised against `gitRemote`), else by its directory basename, using
the company clone's `registry/repos.json` when that clone is among the
roots, else the clone's own. For every mount whose `to` lies under
`operator/knowledge/`, `<clone>/<from>` is read as workspace ids under
`<to>`: hq and company `knowledge/` at `operator/knowledge/`, wsd
`knowledge/` at `operator/knowledge/wsd/`, operator `voice/` at
`operator/knowledge/writing/`. The legacy `<clone>/operator/knowledge/`
form is still read during the path migration. On a colliding id the home
copy wins, then the mapped copy (higher registry `precedence` first), then
the legacy copy. Within one clone that holds both forms of an id, the copy
whose last commit in that clone is newer wins (`git log -1 --format=%ct`,
read only on such a collision), until the migration removes the legacy
copies. On a tie, or when either time is unavailable (the clone is not a
git repo, the file is untracked, git fails), the legacy copy wins. Commit
times, not file mtimes, because a clone's mtimes are checkout times. A
clone with no registry entry is read in the legacy form only.

Global flag: `--format <table|json|mermaid>` (default: `table`). The
`json` shape is stable-by-convention — `{ "verb": "<name>", ... }` —
so downstream roles (e.g. `dispatch-briefer`) can pipe query output
into their own tooling.

Slug resolution accepts exact ids, path-suffix filenames
(`voice-guide.md` matches `operator/knowledge/writing/voice-guide.md`),
or title-substring matches. Ambiguous matches exit `2` with the
candidate list.

`search` defaults to a title-only scan for speed. `--full` walks the
doc body under `<repo-root>/operator/knowledge/`, but returns the same
ranking (title exact > title substring > body match, tie-break on
edge count). Zero-result queries exit `0` — empty is a valid answer.

**Exit codes**: `0` ok, `1` usage, `2` unknown/ambiguous slug or
unknown domain, `3` IO (graph missing / unreadable, or `rebuild` could
not write the artefacts).

### `pa staging-alias` — canonical staging.<site> alias for CF Workers sites

Wires a `staging.<hostname>` subdomain to a chosen branch's preview
deployment on the operator's CF-hosted Workers sites. Built around a
small router-worker that proxies to the chosen workers.dev preview URL,
because CF Custom Domains can only target prod deployments and CF
Preview URLs can only run on `*.workers.dev` (per CF docs 2026-05-21).

```bash
pa staging-alias set --site <hostname> --branch <branch> [--version <id>]
pa staging-alias unset --site <hostname> [--keep-worker]
pa staging-alias status --site <hostname>
pa staging-alias list
```

Registered sites are data-driven (`src/staging-alias/sites.mjs`): one
entry per site, naming the source worker, the router worker, the vault
key suffix, and the expected CF account-name. Adding a fourth site is
one entry + a vault-key pair.

Every write runs the 6-step CF multi-account drill: load the
site-specific vault keys, `GET /accounts/{id}`, assert
`result.name == expectedAccountName`, ABORT on mismatch. The drill
output is echoed on stdout so the operator sees which account a write
landed on. Full design + trade-offs: [`docs/staging-alias.md`](./docs/staging-alias.md).

### `pa lint linkedin` — voice-rule linter

Lints operator-authored text against a JSON voice-rule corpus and reports
violations in **alter-format** (`swap → X`, `cut`, `rename`) with rationale and
source pointer. Deterministic rules (em-dash, banned vocab, reused reaction
phrases, repeated openers) move out of the writer's prompt and into a validator.

```bash
pa lint linkedin <input-file> [--rules <path>] [--format <text|json>] \
                              [--severity <error|warning|info>] [--surface <name>]
```

Default corpus resolves to `operator/knowledge/writing/linkedin-rules.json`
under the repo root. Three detector types (`literal-phrase`, `regex`, `count`)
and three alter kinds (`swap`, `cut`, `rename`). Full schema + examples:
[`docs/lint-linkedin.md`](./docs/lint-linkedin.md).

This verb uses **lint-semantic exit codes** (ESLint-style) that intentionally
deviate from §6 below — `0` clean, `1` errors found, `2` warnings only, `3`
usage/IO/config error. Rationale and full table in the doc.

## Exit codes (tooling standard §6)

| Code | Meaning |
|---|---|
| 0 | OK (placeholder ran; no work done) |
| 1 | Usage error (bad flag, unknown verb) |
| 2 | Logical / operational failure (unused in skeleton) |
| 3 | Transport / IO failure (unused in skeleton) |

## Tests

```bash
pnpm --filter @stravica-ai/engineering-pa-cli test
```

## Standards

Follows the [tooling standard](../../CONTRIBUTING.md#standards): JS only, zero npm deps, `node:test`, `node:util.parseArgs` with `strict: true`, camelCase JSON.
