@delorenj/pjangler 1.4.4 → 1.4.6
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.
- package/README.md +77 -1
- package/contracts/fleet-contract.yaml +82 -11
- package/dist/index.js +1416 -158
- package/dist/mcp-server.js +1359 -145
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -275,7 +275,7 @@ the same envelope, including `baseline` and `exitCode`.
|
|
|
275
275
|
| --- | --- | --- |
|
|
276
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
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)
|
|
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
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
280
|
| `runtime` | the role-local runtime directory derived from `role_dir` | `hermes.untracked-runtimes` |
|
|
281
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 |
|
|
@@ -304,6 +304,82 @@ Story 1.8 owns the systemd observer, 1.9 the live-process observer, and 1.10
|
|
|
304
304
|
Bloodbank routing readiness. Until then those domains say so, by name, rather
|
|
305
305
|
than disappearing.
|
|
306
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
|
+
|
|
307
383
|
### Seven states, one precedence
|
|
308
384
|
|
|
309
385
|
| state | meaning |
|
|
@@ -12,7 +12,11 @@
|
|
|
12
12
|
# agents.* Hermes agent registry, per-agent rows
|
|
13
13
|
# gateways.* Hermes agent registry, fleet-shared gateway block
|
|
14
14
|
# profiles.* generated Hermes profile tree
|
|
15
|
-
# scaffold.* tracked per-agent role scaffold
|
|
15
|
+
# scaffold.* tracked per-agent role scaffold. Each leaf is mapped to the
|
|
16
|
+
# role-relative path it owns by `scaffold_manifest.groups`, so
|
|
17
|
+
# `scaffold.gitignore` means `.gitignore` and
|
|
18
|
+
# `scaffold.sentinel.prompt.md` means `.scripts/sentinel.prompt.md`
|
|
19
|
+
# -- the leaves name owned asset GROUPS, not store keys.
|
|
16
20
|
# units.* systemd user units
|
|
17
21
|
# processes.* observed Hermes processes (read-only)
|
|
18
22
|
#
|
|
@@ -24,12 +28,12 @@
|
|
|
24
28
|
# rather than be rejected as an unknown key, because rejecting it would hide the
|
|
25
29
|
# very drift this contract exists to name.
|
|
26
30
|
|
|
27
|
-
schema_version:
|
|
28
|
-
contract_version: 1.
|
|
31
|
+
schema_version: 3
|
|
32
|
+
contract_version: 1.2.0
|
|
29
33
|
|
|
30
34
|
compatibility:
|
|
31
35
|
min_schema_version: 1
|
|
32
|
-
max_schema_version:
|
|
36
|
+
max_schema_version: 3
|
|
33
37
|
|
|
34
38
|
x-delonet/provenance:
|
|
35
39
|
assessment: fleet-convergence-live-assessment-2026-08-31
|
|
@@ -393,13 +397,6 @@ health_policy:
|
|
|
393
397
|
can dispatch to it is not. Routing readiness is a separate authority
|
|
394
398
|
from the record and must never be inferred from it.
|
|
395
399
|
owner_story: "1.10"
|
|
396
|
-
- domain: template_scaffold
|
|
397
|
-
capability: scaffold.template_ref
|
|
398
|
-
reason: >-
|
|
399
|
-
A deployed role scaffold records no template ref, so there is no
|
|
400
|
-
recorded value on this host to compare against the pinned gitlink and
|
|
401
|
-
none may be invented.
|
|
402
|
-
owner_story: "1.6"
|
|
403
400
|
- domain: release_provenance
|
|
404
401
|
capability: profile.render_generation
|
|
405
402
|
reason: >-
|
|
@@ -459,6 +456,80 @@ health_policy:
|
|
|
459
456
|
max_age_days: 365
|
|
460
457
|
applies_to: registry
|
|
461
458
|
|
|
459
|
+
# An operator's ruling on ONE agent's drift in ONE domain. The drifted
|
|
460
|
+
# observation is still reported and still `fail`; what changes is that the
|
|
461
|
+
# agent is counted `exception` rather than `unhealthy`, exactly as a permitted
|
|
462
|
+
# identity conflict is. Ships empty on purpose: a ruling is written here when
|
|
463
|
+
# it is made, never inferred.
|
|
464
|
+
agent_exceptions: []
|
|
465
|
+
|
|
466
|
+
# What a deployed role directory SHOULD contain, as POLICY about the tracked
|
|
467
|
+
# template rather than a copy of it.
|
|
468
|
+
#
|
|
469
|
+
# The bytes come from git at the gitlink the parent repository has COMMITTED for
|
|
470
|
+
# `template_submodule` -- never from the submodule worktree, never from a
|
|
471
|
+
# sibling clone, never from a branch tip. This block says how to read that
|
|
472
|
+
# tree: which subdirectory holds the rendered files, how a rendered file is
|
|
473
|
+
# named, which registry fields feed each simple `{{ name }}` placeholder, which
|
|
474
|
+
# `scaffold.*` writable leaf owns each role-relative path, which assets are
|
|
475
|
+
# compared for presence only (and why), what must never appear in the tree,
|
|
476
|
+
# and which role-local directory is ignored runtime state nobody owns.
|
|
477
|
+
scaffold_manifest:
|
|
478
|
+
template_submodule: templates/hermes-agent
|
|
479
|
+
template_subdirectory: template
|
|
480
|
+
render_suffix: .jinja
|
|
481
|
+
# Placeholder name -> the field that feeds it. Only simple substitution is
|
|
482
|
+
# rendered; a template that needs control flow or a filter in a
|
|
483
|
+
# content-compared asset is `incomplete`, never rendered "as best it can".
|
|
484
|
+
render_inputs:
|
|
485
|
+
agent_id: agents.{agent_id}
|
|
486
|
+
role: agents.{agent_id}.role
|
|
487
|
+
target_repo: agents.{agent_id}.repo
|
|
488
|
+
display_name: agents.{agent_id}.display_name
|
|
489
|
+
ticket_provider: projects.{slug}.ticket_provider.type
|
|
490
|
+
# Declared leaf -> the role-relative path it owns. A trailing `/` owns a
|
|
491
|
+
# directory; longest prefix wins, so the rendered sentinel prompt inside the
|
|
492
|
+
# verbatim `.scripts/` tree has its own leaf. Every key is a writable field of
|
|
493
|
+
# `authorities.tracked_role_scaffold`, which is what makes those eight leaves
|
|
494
|
+
# real paths rather than invented ones.
|
|
495
|
+
groups:
|
|
496
|
+
scaffold.role.yaml: role.yaml
|
|
497
|
+
scaffold.SOUL.md: SOUL.md
|
|
498
|
+
scaffold.hermes: hermes
|
|
499
|
+
scaffold.momo: momo
|
|
500
|
+
scaffold.sentinel.prompt.md: .scripts/sentinel.prompt.md
|
|
501
|
+
scaffold.gitignore: .gitignore
|
|
502
|
+
scaffold.scripts: .scripts/
|
|
503
|
+
scaffold.runtime-scaffold: .runtime-scaffold/
|
|
504
|
+
# Compared for type and mode only. Policy, with a reason each, never a
|
|
505
|
+
# shortcut: any OTHER control-flow template is reported incomplete.
|
|
506
|
+
presence_only:
|
|
507
|
+
- path: role.yaml
|
|
508
|
+
reason: >-
|
|
509
|
+
Rendered with strftime and rewritten by deployment state (verified
|
|
510
|
+
channel identities, operator choices); Copier itself skips it when it
|
|
511
|
+
exists.
|
|
512
|
+
- path: SOUL.md
|
|
513
|
+
reason: >-
|
|
514
|
+
The operator's persona for the agent. The fanout writes it only when
|
|
515
|
+
absent, so its content is the operator's, not the template's.
|
|
516
|
+
# Never allowed in the tree at the gitlink. Matching a tracked file in a role
|
|
517
|
+
# directory makes it `unexpected-owned`; matching a template path makes the
|
|
518
|
+
# source `source-contaminated`.
|
|
519
|
+
excluded_patterns:
|
|
520
|
+
- "__pycache__/"
|
|
521
|
+
- "*.pyc"
|
|
522
|
+
- "*.pyo"
|
|
523
|
+
- "*.log"
|
|
524
|
+
- ".done-*"
|
|
525
|
+
- ".last-run"
|
|
526
|
+
- ".provision.log"
|
|
527
|
+
- ".plane-project-id"
|
|
528
|
+
- ".DS_Store"
|
|
529
|
+
# Ignored role-local runtime. Never compared, never named, never proposed
|
|
530
|
+
# for deletion; owned by nobody.
|
|
531
|
+
runtime_dir: runtime
|
|
532
|
+
|
|
462
533
|
activation:
|
|
463
534
|
states:
|
|
464
535
|
- discovered
|