@delorenj/pjangler 1.4.6 → 1.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/README.md +41 -596
  2. package/dist/assets/project-notebook-skill/SHA256SUMS +5 -5
  3. package/dist/assets/project-notebook-skill/export-manifest.json +5 -5
  4. package/dist/assets/project-notebook-skill/hooks/session-end.sh +20 -1
  5. package/dist/assets/project-notebook-skill/hooks/session-start.sh +20 -1
  6. package/dist/assets/project-notebook-skill/references/configuration.md +2 -0
  7. package/dist/assets/project-notebook-skill/scripts/project-hooks.py +33 -3
  8. package/dist/assets/project-notebook-skill/tests/test_project_hooks.py +37 -0
  9. package/dist/index.js +9385 -18379
  10. package/dist/mcp-server.js +9432 -17852
  11. package/dist/notebook/journal-service-main.js +5682 -0
  12. package/dist/project-registry-service.js +748 -0
  13. package/dist/prompt.js +2 -1
  14. package/package.json +15 -10
  15. package/scripts/install-project-registry.mjs +34 -0
  16. package/scripts/migrate-project-registry.mjs +117 -0
  17. package/src/project/registryClient.ts +62 -0
  18. package/templates/commonproject/copier.yml +16 -19
  19. package/templates/commonproject/template/.agents/hooks/README.md +10 -4
  20. package/templates/commonproject/template/.agents/hooks/hermes/hindsight-hook.sh +12 -2
  21. package/templates/commonproject/template/.agents/hooks/hooks.master.json +10 -10
  22. package/templates/commonproject/template/.agents/hooks/lib/hook-guard.sh +13 -0
  23. package/templates/commonproject/template/.agents/hooks/sync.py +66 -17
  24. package/templates/commonproject/template/.agents/skills.json +0 -1
  25. package/templates/commonproject/template/.github/workflows/code-review.yml.jinja +7 -1
  26. package/templates/commonproject/template/.project.json.jinja +1 -1
  27. package/templates/commonproject/template/mise.toml.jinja +11 -21
  28. package/contracts/fleet-contract.yaml +0 -584
  29. package/templates/commonproject/template/.mise/scripts/provision-packs.py +0 -587
  30. package/templates/commonproject/template/.mise/scripts/sync-skills.py +0 -2183
  31. package/templates/hermes-agent/copier.yml +0 -186
  32. package/templates/hermes-agent/template/.gitignore.jinja +0 -11
  33. package/templates/hermes-agent/template/.runtime-scaffold/.gitattributes +0 -10
  34. package/templates/hermes-agent/template/.runtime-scaffold/.gitignore.jinja +0 -44
  35. package/templates/hermes-agent/template/.runtime-scaffold/README.md +0 -56
  36. package/templates/hermes-agent/template/.runtime-scaffold/decisions/.gitkeep +0 -2
  37. package/templates/hermes-agent/template/.runtime-scaffold/logs/.gitkeep +0 -1
  38. package/templates/hermes-agent/template/.runtime-scaffold/memories/MEMORY.md +0 -50
  39. package/templates/hermes-agent/template/.runtime-scaffold/memories/USER.md +0 -51
  40. package/templates/hermes-agent/template/.scripts/00-banner.sh +0 -21
  41. package/templates/hermes-agent/template/.scripts/01-config.sh +0 -60
  42. package/templates/hermes-agent/template/.scripts/05-fleet-env.sh +0 -42
  43. package/templates/hermes-agent/template/.scripts/10-hermes-profile.sh +0 -222
  44. package/templates/hermes-agent/template/.scripts/20-runtime-repo.sh +0 -199
  45. package/templates/hermes-agent/template/.scripts/30-telegram.sh +0 -363
  46. package/templates/hermes-agent/template/.scripts/31-slack.sh +0 -457
  47. package/templates/hermes-agent/template/.scripts/40-plane.sh +0 -60
  48. package/templates/hermes-agent/template/.scripts/42-ticket-provider.sh +0 -373
  49. package/templates/hermes-agent/template/.scripts/60-bloodbank.sh +0 -18
  50. package/templates/hermes-agent/template/.scripts/70-systemd.sh +0 -354
  51. package/templates/hermes-agent/template/.scripts/80-registry.sh +0 -185
  52. package/templates/hermes-agent/template/.scripts/99-summary.sh +0 -87
  53. package/templates/hermes-agent/template/.scripts/_lib.sh +0 -1138
  54. package/templates/hermes-agent/template/.scripts/channel-transaction.py +0 -2340
  55. package/templates/hermes-agent/template/.scripts/checkpoint.sh +0 -82
  56. package/templates/hermes-agent/template/.scripts/config.example.toml +0 -70
  57. package/templates/hermes-agent/template/.scripts/credential-launch.sh +0 -85
  58. package/templates/hermes-agent/template/.scripts/heartbeat.sh +0 -283
  59. package/templates/hermes-agent/template/.scripts/lib/fleet-env.sh +0 -202
  60. package/templates/hermes-agent/template/.scripts/lib/parse-fleet-env.py +0 -761
  61. package/templates/hermes-agent/template/.scripts/lib/profile-config-lock.py +0 -182
  62. package/templates/hermes-agent/template/.scripts/lib/profile-config-seed.py +0 -108
  63. package/templates/hermes-agent/template/.scripts/lib/ticket-provider.sh +0 -199
  64. package/templates/hermes-agent/template/.scripts/lib/voice-config.py +0 -546
  65. package/templates/hermes-agent/template/.scripts/lifecycle.sh +0 -126
  66. package/templates/hermes-agent/template/.scripts/momo-wip-lock.py +0 -137
  67. package/templates/hermes-agent/template/.scripts/providers/linear.sh +0 -329
  68. package/templates/hermes-agent/template/.scripts/providers/plane.sh +0 -671
  69. package/templates/hermes-agent/template/.scripts/providers/trello.sh +0 -241
  70. package/templates/hermes-agent/template/.scripts/reporter-watchdog.py +0 -242
  71. package/templates/hermes-agent/template/.scripts/secret-scan.py +0 -134
  72. package/templates/hermes-agent/template/.scripts/sentinel/bin/emit-event.py +0 -106
  73. package/templates/hermes-agent/template/.scripts/sentinel/bin/issue-autonomous-review.sh +0 -339
  74. package/templates/hermes-agent/template/.scripts/sentinel/bin/issue-close-gate.sh +0 -179
  75. package/templates/hermes-agent/template/.scripts/sentinel/docs/autonomous-delegated-review.md +0 -162
  76. package/templates/hermes-agent/template/.scripts/sentinel/docs/bloodbank-events.md +0 -42
  77. package/templates/hermes-agent/template/.scripts/sentinel/docs/continuous-ticket-orchestration.md +0 -77
  78. package/templates/hermes-agent/template/.scripts/sentinel.prompt.md.jinja +0 -116
  79. package/templates/hermes-agent/template/.scripts/store-onepassword-secret.py +0 -260
  80. package/templates/hermes-agent/template/SOUL.md.jinja +0 -197
  81. package/templates/hermes-agent/template/hermes.jinja +0 -109
  82. package/templates/hermes-agent/template/momo.jinja +0 -177
  83. package/templates/hermes-agent/template/role.yaml.jinja +0 -112
package/README.md CHANGED
@@ -1,9 +1,14 @@
1
1
  # pjangler
2
2
 
3
+ Project definitions now live in repository `.project.json` files with one case-insensitive `project_id`. Use `pj info`, `pj list`, and `pj doctor`; the singleton PostgreSQL registry indexes those manifests. See [project registry](docs/project-registry.md) for installation, migration, and compatibility details.
4
+
3
5
  Project subsystem bootstrapper CLI + MCP server.
4
6
 
5
7
  ## Install
6
8
 
9
+ Requires Node 24 or newer. Project skills use `@delorenj/skillex` 0.1.2; see
10
+ [the integration contract](docs/skillex-integration.md).
11
+
7
12
  ```bash
8
13
  npm install
9
14
  npm run build
@@ -18,6 +23,17 @@ node dist/index.js --help
18
23
  pjangler --help
19
24
  ```
20
25
 
26
+ ## Repository ignore contract
27
+
28
+ CommonProject preserves an existing `.gitignore` and adds only portable
29
+ project rules for secrets and local `.agents` projections. It never copies the
30
+ operator's `core.excludesFile`. `.agents/` is the sole canonical agent-config
31
+ tree; `.claude/`, `.codex/`, and the other supported client roots are generated
32
+ local projections covered by machine-wide ignore policy. `bmad.cli-roots`
33
+ removes only exact obsolete lines previously generated by pjangler and never
34
+ changes Git's index. Use the `gitignore-maintenance` skill to audit effective
35
+ rule provenance and reconcile already-tracked ignored paths.
36
+
21
37
  ## Project notebook
22
38
 
23
39
  `pjangler notebook` manages one companion Open Notebook for a registered
@@ -38,606 +54,38 @@ True `SessionStart` and `SessionEnd` hooks are projected separately from the
38
54
  project-scoped hook masters. Their per-repository policy stays fail-open and
39
55
  disabled until explicitly enabled in `.project.json`.
40
56
 
41
- ## Fleet contract
42
-
43
- `pjangler fleet` inspects the 33GOD fleet authority and managed-state contract
44
- at `contracts/fleet-contract.yaml`. The contract is a declaration, never an
45
- observation: it records who owns which field, which projections flow in which
46
- direction, the lifecycle class every managed thing lands in, the canonical
47
- systemd service model, the activation gate, and the modes that are retired
48
- drift rather than alternate healthy states.
49
-
50
- `validate` is strictly read-only — it opens no registry, profile, service, or
51
- process, and writes nothing anywhere.
52
-
53
- ```bash
54
- pjangler fleet contract validate # human report
55
- pjangler fleet contract validate --json # fleet JSON v1 envelope
56
- pjangler fleet contract validate --contract ./candidate.yaml --json
57
- ```
58
-
59
- Exit codes are categorized: `0` valid, `2` malformed contract, `3` contract not
60
- found, `4` a contract that states something forbidden (dual field ownership, an
61
- incomplete lifecycle entry, a retired mode declared healthy), `5` a schema
62
- version this build cannot read, `6` internal.
63
-
64
- Two things about the tracked contract that are easy to trip over:
65
-
66
- - **It must be its own canonical serialization.** `contracts/fleet-contract.yaml`
67
- is re-serialized through `yaml` and compared byte-for-byte, so a hand edit
68
- with different indentation or a trailing blank line fails with exit `2` even
69
- though the contract is perfectly valid. Re-save it through the round trip
70
- (`node -e 'const Y=require("yaml"),f="contracts/fleet-contract.yaml",fs=require("fs");fs.writeFileSync(f,String(Y.parseDocument(fs.readFileSync(f,"utf8"))))'`)
71
- and the diff disappears. A file passed with `--contract` owes nobody canonical
72
- formatting: there the round trip is reported as `byte_stable`, not enforced.
73
- - **`x-`-prefixed keys are yours.** At any depth they round-trip verbatim, are
74
- reported separately under `data.extensions`, and are never read as policy — so
75
- provenance, ticket references and local annotations have somewhere to live.
76
- They are still scanned for credentials and host paths, because a secret in an
77
- extension is still a secret in a tracked file.
78
-
79
- ## Fleet inventory
80
-
81
- `pjangler fleet inventory` reads the two canonical registries — the Hermes agent
82
- registry and the PJangler project registry — plus each repository's
83
- `.project.json`, and answers the question neither store answers alone: what is
84
- the whole fleet, and where does it disagree with itself?
85
-
86
- ```bash
87
- pjangler fleet inventory # human report
88
- pjangler fleet inventory --json # fleet JSON v1 envelope
89
- pjangler fleet inventory --agent pjangler-pm --json # one row, full-fleet totals
90
- pjangler fleet inventory --agent-registry ./copy.yaml # inspect a copy
91
- pjangler fleet inventory --project-registry ./copy.yaml # inspect a copy
92
- pjangler fleet inventory --contract ./candidate.yaml # read a candidate contract
93
- pjangler fleet inventory --deadline-ms 30000 # bound the whole run
94
- ```
57
+ ## Parity audit
95
58
 
96
- `--contract` and `--deadline-ms` are shared with `pjangler fleet provenance`, and
97
- mean the same thing on both: the same options, the same defaults, the same
98
- envelope. `SIGINT` and `SIGTERM` cancel either command (exit `8`).
99
-
100
- It is strictly read-only. It opens no service, no process, and no network, and
101
- it creates no directory, project, role, profile, or registry row. Every declared
102
- path is classified with `lstat`, so a link is *seen* as a link: a symlinked
103
- profile directory is reported as a symlink with its target as evidence, and the
104
- target is never substituted for the declared value or used to derive one. (One
105
- read does traverse the filesystem's own links: the `.project.json` under an
106
- agent's `project_path` is opened by path. It is confirming evidence only — it can
107
- never become a field's `source` or its value — but a symlinked `project_path`
108
- does redirect which manifest is read. Nothing else is opened: a `project_path`
109
- the classifier calls `relative`, `absent`, `outside-root`, or `not-a-directory`
110
- is reported as `manifest-not-consulted` and no file is read for it, so the
111
- evidence an agent is judged against never depends on the directory you ran the
112
- command from.)
113
-
114
- Every emitted value carries `{value, source, state}`, where `source` is the
115
- authority owner `contracts/fleet-contract.yaml` declares for that field path and
116
- `state` is one of `resolved`, `unresolved`, `conflicted`, or `unobserved`. An
117
- unknown is an explicit `null` at `unresolved`, never a guess from a convenient
118
- basename. `.project.json` is read as confirming evidence only: it is never the
119
- `source` of a field and never a tiebreaker when the two registries disagree.
120
-
121
- **An unhealthy fleet is data, not a failure.** A fleet with identity conflicts
122
- exits `0` with `ok: true` and `data.health.healthy: false` — the envelope nulls
123
- `data` on `ok: false`, so reporting drift as a failure would blank the inventory
124
- on exactly the runs that matter. Only a *command* failure is nonzero:
125
-
126
- | exit | meaning |
127
- | --- | --- |
128
- | `0` | the command ran — read `data.health.healthy` for the verdict |
129
- | `2` | a malformed flag value, or a registry that could not be parsed |
130
- | `3` | a registry that is not there, or an `--agent` id that is not registered |
131
- | `4` | the fleet contract declares a conflicting authority, an invalid class, or a live retired mode |
132
- | `5` | the fleet contract declares a schema version this build does not support |
133
- | `6` | internal |
134
- | `7` | the run did not finish inside `--deadline-ms` |
135
- | `8` | the run was cancelled (`SIGINT`/`SIGTERM`, or an aborted MCP request) |
136
-
137
- Exit `4` and `5` come from the contract, not from a registry: the inventory
138
- validates `contracts/fleet-contract.yaml` before it reads anything, and refuses
139
- to attribute provenance against a contract it cannot trust. Run
140
- `pjangler fleet contract validate` for the diagnostic.
141
-
142
- Two more things worth knowing:
143
-
144
- - **`--agent` scopes the rows, never the totals or the verdict.** `data.rows`
145
- carries the one agent and `data.scope` says the result is scoped, but
146
- `data.totals`, `data.health` and `data.conflicts` still describe the whole
147
- registered fleet. A scoped run therefore reports `healthy: false` for a fleet
148
- that is unhealthy elsewhere — deliberately, because a slice that could report
149
- "healthy" while the fleet is broken is the one thing an aggregate must never
150
- do.
151
- - **`--agent-registry` / `--project-registry` say which bytes to read, not which
152
- file is canonical.** `data.stores[].configured_path` keeps naming the
153
- configured store and `inspected_path` names the override.
154
-
155
- An identity conflict is grouped under a stable id —
156
- `conflict:{field-path}:{12 hex}` — identical for every participant, on every
157
- machine, run after run. A group can be declared permitted by adding an entry to
158
- `classifications.intentionally_unmanaged.entries` in the contract whose `source`
159
- equals the group's field path and whose `participants` match the group's set
160
- exactly; a superset never absorbs a claimant nobody ruled on.
161
-
162
- ## Fleet provenance
163
-
164
- `pjangler fleet provenance` answers the question the inventory does not: *which
165
- build is each agent actually running?* It pairs every **recorded, pinned, or
166
- declared** value with its **live** counterpart, each side naming its own source.
59
+ `pjangler audit` runs the deterministic 33god parity rules against a repository
60
+ and exits nonzero when any of them fails. `pjangler migrate` applies the
61
+ idempotent fix the owning rule declares.
167
62
 
168
63
  ```bash
169
- pjangler fleet provenance # human report
170
- pjangler fleet provenance --json # fleet JSON v1 envelope
171
- pjangler fleet provenance --agent pjangler-pm --json # one agent, full-fleet totals
172
- pjangler fleet provenance --agent-registry ./copy.yaml # inspect a copy
173
- pjangler fleet provenance --project-registry ./copy.yaml # inspect a copy
174
- pjangler fleet provenance --contract ./candidate.yaml # read a candidate contract
175
- pjangler fleet provenance --deadline-ms 30000 # bound the whole run
64
+ pjangler audit # every rule, human report
65
+ pjangler audit --json # machine-parseable
66
+ pjangler audit --rules mise.config-root,sot.project-json
67
+ pjangler migrate sot.project-json --dry-run
68
+ pjangler migrate --all
176
69
  ```
177
70
 
178
- `--agent`, `--project-registry`, `--agent-registry`, `--contract` and
179
- `--deadline-ms` are also accepted by `pjangler fleet inventory`, and both
180
- commands are exposed as MCP tools with the same options and the same envelope.
71
+ `--rules` reports only the rules you name. An unknown id is an **error, never an
72
+ empty pass** — a caller probing for one rule learns that the rule is gone
73
+ instead of reading a clean report over nothing.
181
74
 
182
- **One global rule: `desired` is the recorded side, `observed` is the live side.**
183
- That is what makes the template gitlink structural rather than defensive — the
184
- recorded gitlink is read from `git ls-files --stage` on the *parent*, so no
185
- worktree move can make it report the worktree's SHA. `observed` is the
186
- submodule's own `HEAD`. A reader never has to ask which side is authoritative.
75
+ `pjangler audit --json` and the `pjangler_list_parity_rules` MCP tool report the
76
+ live catalog. The rules are owned by the recipes in `src/recipes/`, not by a
77
+ central table, so that catalog is the only authority on which ones exist.
187
78
 
188
- Every fact lands in exactly one of six statuses, and **absence is never a
189
- match**:
79
+ ## Agents
190
80
 
191
- | status | meaning |
192
- | --- | --- |
193
- | `match` | both sides are present and equal |
194
- | `mismatch` | both sides are present and differ |
195
- | `dirty` | a cleanliness fact whose observed side is not clean — always its own fact, never a modifier on the value beside it |
196
- | `missing` | a side that should carry a value carries none |
197
- | `unsupported` | no comparable value exists without inventing one: nothing records the desired value, or it is spelled as an unexpanded `$VAR` |
198
- | `unobserved` | the probe did not run, or ran and failed — nothing may be claimed |
199
-
200
- Within one fact the precedence is
201
- `unobserved` > `unsupported` > `missing` > `dirty` > `mismatch` > `match`.
202
- `data.totals.by_status` counts all six, and `data.health` reports `healthy`
203
- (drift-free) and `complete` (everything that should have been observed was) as
204
- two separate verdicts — a run that could not reach half the fleet must never
205
- read as a clean bill.
206
-
207
- It is strictly read-only, and provably so. Every git probe passes
208
- `--no-optional-locks`, because a plain `git status` refreshes `.git/index` and a
209
- command that rewrites an index on 28 repositories is not read-only. The observed
210
- `hermes` binary is classified by **path** — against the configured release root
211
- first, then the contract's retired `detect` patterns — and is **never executed**.
212
- Nothing fetches, pulls, clones, or reaches the network. `~/.hermes/fleet.env` is
213
- read through a key allowlist, so the Plane API keys beside the fleet paths never
214
- enter memory at all.
215
-
216
- Two failure modes are deliberately different. A **per-probe** timeout downgrades
217
- one fact to `unobserved`, records the probe, sets `health.complete: false`, and
218
- the run still succeeds. A **whole-run** deadline is a command failure, because a
219
- truncated provenance report is exactly the kind of partial that must never be
220
- mistaken for a complete one.
221
-
222
- **A drifted fleet is data, not a failure.** It exits `0` with `ok: true` and
223
- `data.health.healthy: false`. Only a *command* failure is nonzero:
224
-
225
- | exit | meaning |
226
- | --- | --- |
227
- | `0` | the command ran — read `data.health.healthy` and `data.health.complete` for the verdicts |
228
- | `2` | a malformed flag value |
229
- | `3` | a **registry** that is not there, or an `--agent` id that is not registered — a missing template config or fleet env is a finding, not an exit |
230
- | `4` | the fleet contract declares a conflicting authority, an invalid class, or a live retired mode |
231
- | `5` | the fleet contract declares a schema version this build does not support |
232
- | `6` | internal |
233
- | `7` | the whole-run `--deadline-ms` budget expired; no partial result is reported |
234
- | `8` | the run was cancelled (`SIGINT`/`SIGTERM`, or an aborted MCP request); no probe child survives |
235
-
236
- `data` is deterministic: no timestamp, duration, hostname, or ordering by
237
- completion. Two runs over unchanged state produce byte-identical `data`, which is
238
- what lets the MCP tool result be compared to the CLI `--json` envelope by
239
- equality rather than by resemblance.
240
-
241
- Two provenance questions this host records nothing to answer, and which are
242
- therefore reported as `unsupported` with their observed evidence rather than
243
- guessed: a deployed role scaffold carries no template ref (it renders no
244
- `.copier-answers.yml`), and a generated profile config carries only the
245
- `GENERATED FILE -- DO NOT EDIT` marker — no generation counter, digest, or
246
- sidecar — so a sha256 of its bytes is the only stable evidence.
247
-
248
- ## Fleet status
249
-
250
- `pjangler fleet status` answers the question the inventory and provenance
251
- commands do not: *is the fleet correct?* One read-only traversal of the registry
252
- reports every registered agent across **all nine observation domains**, plus one
253
- aggregate — in a single invocation.
81
+ pjangler does not hire, provision, or inspect agents. That is Flume's surface —
82
+ `flume hire`, `flume roster`, `flume record`, `flume review` — and the org chart
83
+ is Flume's record.
254
84
 
255
- ```bash
256
- pjangler fleet status # human report
257
- pjangler fleet status --json # fleet JSON v1 envelope
258
- pjangler fleet status --live --json # authorize the recipe-owned audit rules
259
- pjangler fleet status --agent pjangler-pm --json # one agent, full-fleet totals
260
- pjangler fleet status --domain profile --json # one domain, nothing else collected
261
- pjangler fleet status --agent-registry ./copy.yaml # inspect a copy
262
- pjangler fleet status --project-registry ./copy.yaml # inspect a copy
263
- pjangler fleet status --contract ./candidate.yaml # read a candidate contract
264
- pjangler fleet status --deadline-ms 60000 # bound the whole run
265
- pjangler fleet status --baseline ./base.json --json # correlate against a prior run
266
- pjangler fleet status --exit-code # project the verdict onto the exit status
267
- ```
268
-
269
- It is exposed as the `pjangler_fleet_status` MCP tool with the same options and
270
- the same envelope, including `baseline` and `exitCode`.
271
-
272
- ### The nine domains, and what each observes today
273
-
274
- | domain | observed today | what `--live` adds |
275
- | --- | --- | --- |
276
- | `registry` | the agent row itself: well-formedness, identity conflicts, correlation to a project record | `hermes.registry-parity` (**host-scoped** → `data.host`, unfiltered runs only†) |
277
- | `project_binding` | the row's board binding and whether the repository's `.project.json` agrees | the notebook and `sot.project-json` rules |
278
- | `template_scaffold` | the tracked template's gitlink, remote and cleanliness (fleet-wide), and **every managed role directory compared asset by asset against the template at the committed gitlink** — eight group observations per agent, typed per-asset `items`, `data.scaffold` and `agents[].scaffold` summaries (see *Scaffold parity* below) | every tracked-asset parity rule, plus a `scaffold-rule-disagreement` finding where `hermes.pm-scaffold` and the observer disagree |
279
- | `profile` | the generated profile directory, `lstat`ed and never followed; a symlink is a `fail`, because the contract declares `symlink_allowed: false` | `hermes.runtime-singleton`; `hermes.profile-wiring` (**host-scoped**) |
280
- | `runtime` | the role-local runtime directory derived from `role_dir` | `hermes.untracked-runtimes` |
281
- | `systemd` | `unsupported` — no systemd observer exists in this release; the unit names are the contract's expectations, carried as evidence | `systemd.sentinel` (**host-scoped**, unfiltered runs only†), never promoted to an agent |
282
- | `live_process` | `unsupported` — there is no `ps`, `pgrep`, or `/proc` read anywhere in this build | nothing |
283
- | `bloodbank` | the stored routing record and the strict activation flag; liveness is `unsupported` | `hermes.fleet-config` (**host-scoped**, unfiltered runs only†) |
284
- | `release_provenance` | every provenance fact for the agent: executable, checkout, remote, HEAD, cleanliness | nothing |
285
-
286
- † `registry`, `systemd` and `bloodbank` are each observed live by exactly one
287
- rule, and that rule is **host-scoped** — it can add nothing to any agent's
288
- record, only to `data.host`. Filters constrain collection, so
289
- `--domain systemd --live` spawns no audit child at all and `data.host` comes back
290
- empty; the run says so with an `audit-host-rules-not-collected` finding naming
291
- the rule it did not collect. Run without `--domain` to get them.
292
-
293
- A `--domain` run whose selected domain *is* audit-fed does spawn children, and
294
- those children report every rule — including host-scoped rules for domains you
295
- did not select. Those results are not carried in `data.host` (only the selected
296
- domain is emitted), and the run says so too, with one
297
- `audit-host-rules-not-reported` finding per rule. **An empty `data.host` never
298
- means "this machine is clean"** on a filtered run; the findings say which reading
299
- you are not being shown. For the same reason a host finding's `retrieval` is the
300
- unfiltered `--live` invocation whenever its domain is one of the three above —
301
- the narrowed command could not return it.
302
-
303
- Story 1.8 owns the systemd observer, 1.9 the live-process observer, and 1.10
304
- Bloodbank routing readiness. Until then those domains say so, by name, rather
305
- than disappearing.
306
-
307
- ### Scaffold parity
308
-
309
- `template_scaffold` compares every managed role directory against the tracked
310
- template **at the gitlink this repository has committed** for
311
- `templates/hermes-agent` — git objects, never the submodule worktree, never a
312
- sibling clone, never a branch tip. The contract's `scaffold_manifest` block
313
- (schema 3) is the policy that says how to read that tree: which subdirectory
314
- holds the rendered files, how a rendered file is named, which registry fields
315
- feed each simple `{{ name }}` placeholder (`render_inputs`), which `scaffold.*`
316
- writable leaf owns each role-relative path (`groups`), which assets are
317
- compared for presence only and why (`presence_only`: `role.yaml`, `SOUL.md`),
318
- what may never appear in a template tree (`excluded_patterns`), and which
319
- role-local directory is ignored runtime nobody owns (`runtime_dir`).
320
-
321
- Each agent gets **eight observations, one per declared leaf** — `scaffold.role.yaml`,
322
- `scaffold.SOUL.md`, `scaffold.hermes`, `scaffold.momo`, `scaffold.sentinel.prompt.md`,
323
- `scaffold.gitignore`, `scaffold.scripts`, `scaffold.runtime-scaffold` — each
324
- `pass`, `fail` or `error`, with `observed: "<matching>/<owned> assets match"`
325
- and, on anything but a clean pass, a typed `items[]` list (capped at 100 per
326
- observation, the clip recorded in `truncated`). Every item carries a
327
- **role-relative path**, a kind, and a `desired`/`observed` pair that is a 12-hex
328
- git blob-id prefix or a type/mode word — never a file body, never an absolute
329
- path:
330
-
331
- | kind | meaning |
332
- | --- | --- |
333
- | `missing` | the template renders it; the role does not have it |
334
- | `stale-content` | different bytes, and the observed blob exists in the template's lineage — an older release |
335
- | `locally-modified` | different bytes, and no template version ever shipped them — somebody edited it |
336
- | `wrong-mode` | same bytes, executable bit differs |
337
- | `wrong-type` | a directory or symlink where a file was rendered, or the reverse |
338
- | `unsafe-symlink` | a symlink whose target is absolute or leaves the repository |
339
- | `unexpected-owned` | a tracked file inside an owned group the template did not render (a committed `.done-*`, an unrendered `.jinja`) — named, never proposed for deletion |
340
- | `incomplete` | this build could not decide: a render input is missing (`input-missing: display_name`), the template needs control flow (`render-unsupported`), or the bytes were unreadable |
341
-
342
- `stale-content` versus `locally-modified` is **lineage, not commit state**: a
343
- verbatim asset is stale when its observed blob exists in the template's object
344
- database, a rendered asset when it equals the render of one of the last twelve
345
- versions of its Jinja source. An uncommitted edit is the orthogonal `wip: true`
346
- flag on the item, and a drifted path that also carries one appears in
347
- `agents[].scaffold.wip_overlap`. Ignored runtime bytes, git-ignored entries and
348
- tracked files outside every owned group are **counted, never named**
349
- (`ignored_entries`, `wip_preserved`, `foreign_tracked`).
350
-
351
- **Source integrity is a host finding.** `scaffold.source` in `data.host` reads
352
- `pass` when the committed gitlink is stable (`ls-tree HEAD` and the index
353
- agree), its object exists, the submodule worktree is at it with no modified
354
- tracked file, the tree carries no excluded pattern, and every rendered path
355
- resolves to one declared group. Any other reading — `gitlink-missing`,
356
- `gitlink-unstable`, `source-uninitialized`, `source-missing-object`,
357
- `source-mismatched`, `source-dirty`, `source-contaminated`, `source-empty`,
358
- `manifest-uncovered:<path>` — is `error`, every selected agent's eight groups
359
- are `error`, and **desired bytes are never taken from the worktree as a
360
- fallback**. The same code is carried in `data.scaffold.source.integrity`.
361
-
362
- `data.scaffold.agents` counts every selected agent before any cap
363
- (`total_registered`, `selected`, `applicable`, `passing`, `drifted`,
364
- `incomplete`, `exception_authorized`, `unobserved`), and
365
- `data.scaffold.rule_agreement` says, under `--live`, how the observer and the
366
- `hermes.pm-scaffold` rule agreed over the subset both compare — a disagreement
367
- is a `scaffold-rule-disagreement` finding and both readings stand. An operator
368
- ruling on one agent's scaffold drift lives in
369
- `health_policy.agent_exceptions[]` (`domain`, `agent_id`, `reason`, `owner`):
370
- the drifted groups keep their `fail`, carry `justification.kind: "exception"`
371
- with that entry's own path, and the agent is counted `exception` rather than
372
- `unhealthy` — `health.healthy` is unaffected, exactly as for a permitted
373
- identity conflict. A contract with no `scaffold_manifest` still loads; the
374
- domain then reads `unsupported` under capability `scaffold.manifest`.
375
-
376
- `--domain template_scaffold` is the surface. A run scoped to any other domain
377
- spawns zero scaffold probes; `--agent <id>` reads only that agent's role
378
- directory while `data.scaffold.agents` keeps the fleet-wide totals. The role
379
- directory is the registry row's `role_dir`, defaulting to
380
- `<project_path>/agents/hermes/<role>` only when the row is silent
381
- (`agents[].scaffold.role_dir_source`), and must sit inside `project_path`.
382
-
383
- ### Seven states, one precedence
384
-
385
- | state | meaning |
386
- | --- | --- |
387
- | `pass` | observed, and in the state it should be in |
388
- | `warn` | observed, imperfect, and not a gate |
389
- | `skip` | declared not applicable; does **not** reduce completeness |
390
- | `fail` | observed, and wrong |
391
- | `unsupported` | no adapter exists in this release; counted and visible, but it does **not** reduce completeness |
392
- | `unobserved` | applicable, and not read; **does** reduce completeness |
393
- | `error` | collection itself failed; never silently a `pass`, never a dropped agent |
394
-
395
- Within a domain and then across domains the precedence is
396
-
397
- 1. `unsupported` **yields** whenever the domain produced any other state. It is a
398
- statement about this build, not about the fleet, so it is the strongest answer
399
- for a domain with nothing else (`live_process`) and the weakest thing to report
400
- for a domain that also has real findings — without this, a
401
- `template_scaffold` domain carrying one permanent "no template ref is
402
- recorded" reported `unsupported` while 135 tracked assets were failing.
403
- 2. Then, over whatever is left:
404
- `error` > `unobserved` > `unsupported` > `fail` > `warn` > `skip` > `pass`.
405
-
406
- Both halves are the rule; the ordered list alone is not. `rollUp` in
407
- `src/fleet/status.ts` applies them in that order.
408
-
409
- ### What `--live` does and does not authorize
410
-
411
- `--live` authorizes **bounded, read-only host and network observation**, and
412
- nothing else: it runs the recipe-owned audit rules per repository as bounded
413
- child processes, because one of them (`bmad.version`) makes a real `npm view`
414
- call. It never authorizes mutation, process control, service changes, board
415
- changes, or Bloodbank activation, and it does not conjure a systemd,
416
- live-process, or Bloodbank-liveness observer.
417
-
418
- Each repository is audited as a child of this build with a **narrow, allowlisted
419
- environment**, so no credential in your shell or in `~/.hermes/fleet.env` ever
420
- reaches it. Each child is time-boxed: one hung `systemctl` downgrades that
421
- repository's audit-fed domains to `unobserved` and leaves every other agent
422
- fully reported.
423
-
424
- Filters constrain **collection**, not just emission: `--domain registry` spawns
425
- no audit child and no provenance probe, and `--agent <id>` spawns neither for any
426
- other agent. It holds per probe FAMILY too — `--domain template_scaffold` runs
427
- the gitlink and submodule probes and no checkout probe, and
428
- `--domain release_provenance` the reverse — so a filtered run never pays for
429
- facts it would discard.
430
-
431
- ### Four axes, because one word cannot carry four questions
432
-
433
- Every observation carries four *separate* axes, and collapsing any two of them is
434
- how "we did not look" becomes "it is fine".
435
-
436
- | axis | values | what it answers |
437
- | --- | --- | --- |
438
- | `state` | `pass` `warn` `skip` `fail` `unsupported` `unobserved` `error` | what was concluded |
439
- | `applicability` | `required` `optional` `not_applicable` `deferred` `exception` | whether it was required, and if not, on whose authority |
440
- | `evidence` | `direct` `declared` `derived` `absent` | how strongly it is supported |
441
- | `freshness` | `current` `stale` `unknown` `not_applicable` | whether the evidence is still current |
442
-
443
- `evidence: "declared"` is the load-bearing one. A registry field that *asserts*
444
- something with nothing verifying it — a stored routing target, an activation
445
- flag, a recorded unit name — is `declared`, never `direct`. A `declared`
446
- observation may be `pass` on its own record, but it can never set
447
- `lifecycle.capability_readiness: "ready"` and never contributes to `proven`.
448
- `derived` is a reading computed across other rows, such as an identity conflict.
449
-
450
- **Freshness is a bucket, never an age.** `data` is byte-identical across two runs
451
- over unchanged state, and an age in seconds is not. The reference instant is
452
- captured once per run and never serialized; each `health_policy.freshness` entry
453
- declares a `max_age_days`, and only the bucket is emitted.
454
-
455
- ### `health_policy`: the only thing that can authorize a gap
456
-
457
- `contracts/fleet-contract.yaml` carries an optional `health_policy` root block.
458
- It is the **only** place a skip, a warning, a deferred capability, or a managed
459
- exception can be justified — nothing is inferred from a summary, a severity, or
460
- the absence of other findings.
461
-
462
- | key | authorizes |
463
- | --- | --- |
464
- | `required_domains` | which domains must be observed before proof can be claimed |
465
- | `deferred_capabilities[]` | one `unsupported` answer, with a `reason` and the `owner_story` that will implement the observer |
466
- | `allowed_warnings[]` | one rule whose `warn` is upstream cadence rather than fleet drift |
467
- | `allowed_skips[]` | one rule or domain whose `skip` is a declared property of a read-only run |
468
- | `freshness[]` | how long one recorded timestamp counts as current evidence |
469
-
470
- An authorized gap is still **reported**, with its own state; what changes is
471
- whether the aggregate may claim it was proven. Every justified observation names
472
- the entry that authorized it in `justification.policy`, so an operator can open
473
- the contract at that path.
474
-
475
- `health.unjustified` counts the three states the contract can actually
476
- authorize — `warn`, `skip` and `unsupported` — and no others. A `fail` or an
477
- `error` is not something a policy entry may excuse; those are what
478
- `health.healthy` is for, and an `unobserved` is a coverage question `complete`
479
- already answers. A contract with **no** `health_policy` block still loads — it
480
- is a schema-1 contract — and then authorizes nothing: every `warn`, `skip` and
481
- `unsupported` is unjustified, `proven` is false, and one
482
- `health-policy-undeclared` finding names the missing block rather than the run
483
- failing.
484
-
485
- Adding the block was a grammar change, so the tracked contract is
486
- `schema_version: 2` at `contract_version: 1.1.0`. This build reads schema 1 and 2.
487
-
488
- ### Three verdicts, and which one to read
489
-
490
- ```
491
- verdict = !healthy -> "unhealthy" drift is PROVEN
492
- : !complete || stale || unknown || unjustified -> "unproven" nothing is proven either way
493
- : "healthy"
494
- proven = verdict === "healthy" && fleet_complete
495
- ```
496
-
497
- `health.healthy` and `health.complete` keep exactly the meanings they had:
498
- `healthy` is "no `fail`, no `error`" (the fleet is not *wrong*), `complete` is
499
- "nothing unobserved, no collection error, no truncation, no contradiction" (this
500
- run *read* all of it). `health.verdict` is the aggregate built on top of both,
501
- and it is what the report headline and `data.health.exit_category` lead with — so
502
- `healthy` can no longer be claimed over a fleet whose audit-fed half was never
503
- opened, while `healthy` itself still means what story 1.4 pinned it to mean.
504
-
505
- `health.proven` is the only field that means *we read all of it and it was right*.
506
-
507
- `health.freshness_unknown` sits beside `health.stale` and blocks `proven` just
508
- as hard: a policy entry that applies to a field no row populates buckets every
509
- reading `unknown`, and if that gated nothing the entry would validate, change
510
- nothing, and read as though the fleet had been checked.
511
-
512
- Beside them: `health.stale`, `health.freshness_unknown`, `health.unjustified`, `health.contradictions`, and
513
- `health.members` — every **selected** agent in exactly one of `healthy`,
514
- `unhealthy`, `incomplete`, `deferred`, `exception`, `unclassified`. The six counts
515
- sum to `scope.selected_agents`, not to the records the envelope's cap let
516
- through.
517
-
518
- ### Severity, repair class, and one exact next action
519
-
520
- Every non-pass observation and every host finding carries an `owner`, an
521
- `observed`/`desired` pair, a `severity`, a `repair` class, and one `next_action`.
522
- Each is derived from a real field — the audit rule's own `fixable`, its
523
- `rule_scope`, and the contract's `activation.execution_authority` — never from
524
- prose.
525
-
526
- | `repair` | condition | next action |
527
- | --- | --- | --- |
528
- | `automatic` | an audit rule, project-scoped, reporting `fixable` | the exact `pjangler migrate <rule_id> <repo> --dry-run` |
529
- | `approval-gated` | the observation's field **is** `activation.execution_authority` (`strict: true`, `default: deny`) | the activation route, and it names the authority |
530
- | `blocked` | a contract-declared deferred capability | nothing to run in this release; the action names the owning story |
531
- | `other-owner` | a host-scoped rule | the host route — no work in any repository changes it |
532
- | `manual` | everything else that needs a decision | the retrieval that returns the observation alone |
533
- | `none` | a pass, or a declared-not-applicable skip | the retrieval |
534
-
535
- Severity is `state` × `applicability`: an `error` is `critical`; a `fail` on a
536
- required domain is `critical` and elsewhere `high`; an `unobserved` required
537
- domain is `high`; an unjustified `warn` or a `stale` reading is `medium`; a
538
- **justified** `warn`, `unsupported` or `stale` is `low`; a `pass` and a justified
539
- `skip` are `info`. An *unjustified* `unsupported` outranks a justified one —
540
- same observation, same build, and the only difference is whether anyone wrote
541
- down that it was expected.
542
-
543
- **A recommended command is read-only unless it is labelled.**
544
- `next_action_class` is `"read-only"` or `"requires-authorization"`, and a
545
- `requires-authorization` action names the authorization in the string itself.
546
-
547
- `data.findings` is stable-sorted by gating impact, then severity, then scope,
548
- then agent, then domain, then `finding_id` — **before** any cap, on both the
549
- machine and the human path. A gating finding at position 26 of an unsorted list
550
- is silently dropped by the report's cap of 25, which is exactly the failure the
551
- sort exists to prevent.
552
-
553
- ### Lifecycle: four values, never one boolean
554
-
555
- Each agent record carries `lifecycle` with four separate fields.
556
- `desired_state` is what the registry declares as the target for that row — a
557
- statement of intent, never a claim about the agent. `observed_state` is the
558
- furthest state this run actually proved, and it can never read `routing_ready` or
559
- `activated` in this release because no observer for either exists.
560
- `capability_readiness` is never `ready` for the same reason: a `declared`
561
- registry field is not a direct observation of the shared gateway. `activation`
562
- reports the strict flag verbatim, and the contract's default is deny.
563
-
564
- ### `--baseline`: two runs, correlated read-only
565
-
566
- ```bash
567
- pjangler fleet status --json > base.json
568
- pjangler fleet status --baseline base.json --json
569
- ```
570
-
571
- `--baseline` opens a prior status document **for reading and nothing else**, and
572
- no state is ever written to disk to compute a transition. Findings are joined on
573
- `finding_id`, a sha256 prefix that is stable across runs and identical on the CLI
574
- and MCP adapters, and `data.transitions[]` reports every `appeared`, `resolved`,
575
- `state_changed`, `severity_changed` and `evidence_changed`. An **unchanged**
576
- finding emits nothing, so a byte-identical baseline produces an empty array. An
577
- baseline is refused as `INVALID_INPUT` at exit 2, naming the path, before a
578
- single probe or audit child spawns, when it is unreadable, unparseable, not a
579
- `fleet.status` document, **taken under a different `--agent`/`--domain` scope**,
580
- or **written by a run whose own output was clipped**. The scope check is the
581
- load-bearing one: a document taken over the whole fleet and diffed by a
582
- `--agent alpha` run would otherwise report `resolved` for every other agent —
583
- "it got fixed" about observations the run never collected. `--live` is
584
- deliberately *not* part of the scope, because reading more than the baseline did
585
- is a real transition.
586
-
587
- ### The exit taxonomy, and why the projection is opt-in
588
-
589
- `data.health.exit_category` is `ok`, `unhealthy`, or `incomplete`, and **both
590
- adapters carry it** — it is the discriminant an MCP client had no way to read
591
- before, because `isError` is `false` for a fully unhealthy fleet.
592
-
593
- | category | verdict | `--exit-code` exits |
594
- | --- | --- | --- |
595
- | `ok` | `healthy` | 0 |
596
- | `unhealthy` | `unhealthy` | **10** |
597
- | `incomplete` | `unproven` | **11** |
598
-
599
- A contradiction is reported only where one source **proved a failure** and
600
- another reported a **pass** for the same `(agent, domain, field)` — never on a
601
- `warn` against a `pass`, and never on two differing non-pass states. The narrow
602
- rule is deliberate: `DOMAIN_FIELD` gives every rule in a domain one contract
603
- field path, so even this fires on readings that are both true, and widening it
604
- to "any two states that differ" would make `complete` meaningless.
605
-
606
- `unhealthy` and `incomplete` are `ok: true` states — the command succeeded, the
607
- fleet did not — so they are not error codes and never null out `data`. The
608
- default exit stays **0**: `fleet status` is an observation command, gating CI is a
609
- later story's job, and a `mise run fleet:status` that is permanently red on a real
610
- fleet teaches an operator to ignore it. A *command* failure still wins: an
611
- unknown `--agent` is exit 3 whether or not `--exit-code` was given.
612
-
613
- **Host-scoped findings are reported once**, deduped by rule id, in `data.host`.
614
- They never reach a per-agent record and never make an agent or the fleet
615
- unhealthy: no amount of work in a repository can change a condition about this
616
- machine, so failing the repository for it is a category error.
617
-
618
- **An unhealthy fleet is data, not a failure.** By default it exits `0` with
619
- `ok: true` and `data.health.verdict: "unhealthy"`. Only a *command* failure is
620
- nonzero without `--exit-code`:
621
-
622
- | exit | meaning |
623
- | --- | --- |
624
- | `0` | the command ran — read `data.health.verdict` for the answer |
625
- | `2` | a malformed flag value, a `--domain` that is not one of the nine, or a `--baseline` that could not be read or parsed |
626
- | `3` | an `--agent` id that is not registered, or a registry that is not there |
627
- | `4` | the fleet contract declares a conflicting authority, an invalid class, or a live retired mode |
628
- | `5` | the fleet contract declares a schema version this build does not support |
629
- | `6` | internal |
630
- | `7` | the whole-run `--deadline-ms` budget expired; no partial result is reported |
631
- | `8` | the run was cancelled (`SIGINT`/`SIGTERM`, or an aborted MCP request); no audit child survives |
632
- | `10` | **`--exit-code` only** — `data.health.verdict` is `unhealthy` |
633
- | `11` | **`--exit-code` only** — `data.health.verdict` is `unproven` |
634
-
635
- `data` is deterministic: no timestamp, duration, pid, hostname, or ordering by
636
- completion — the audit child's `auditedAt` is dropped at the boundary and every
637
- path is home-redacted. Two runs over unchanged state produce byte-identical
638
- `data`, and every `--json` document is written through an **awaited stdout
639
- drain**, so it survives a file, a pty, a shell pipe, and a `spawn` capture
640
- identically at any size.
85
+ `pjangler describe` still reports a repository's employees, and `pjangler init`
86
+ carries existing `.project.json` `agents` entries forward. Neither writes a new
87
+ one: that block is a one-way projection of the org chart, not a place a project
88
+ declares intent.
641
89
 
642
90
  ## Orienting in a repo
643
91
 
@@ -675,7 +123,7 @@ has to walk the registry. It spans more than the checked-out branch:
675
123
  The newest of those wins, and the winning source is always reported. Remote
676
124
  state reflects your last fetch — nothing here touches the network.
677
125
 
678
- `pjangler project list` orders by that signal, newest work first.
126
+ `pj list` orders by that signal, newest work first.
679
127
 
680
128
  ## Shell prompt
681
129
 
@@ -730,10 +178,7 @@ Exposed tools:
730
178
  - `pjangler_project_init`
731
179
  - `pjangler_project_list`
732
180
  - `pjangler_project_show`
181
+ - `pjangler_info`
733
182
  - `pjangler_describe_project`
734
183
  - `pjangler_describe_recipe`
735
184
  - `pjangler_run_recipe`
736
- - `pjangler_deploy_hermes_agent`
737
- - `pjangler_fleet_inventory`
738
- - `pjangler_fleet_provenance`
739
- - `pjangler_fleet_status`