@delorenj/pjangler 1.4.2 → 1.4.4
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 +528 -0
- package/contracts/fleet-contract.yaml +513 -0
- package/dist/index.js +9333 -1509
- package/dist/mcp-server.js +6634 -1096
- package/dist/prompt.js +2 -1
- package/package.json +10 -4
- package/templates/hermes-agent/copier.yml +16 -3
- package/templates/hermes-agent/template/.runtime-scaffold/memories/MEMORY.md +7 -4
- package/templates/hermes-agent/template/.scripts/10-hermes-profile.sh +88 -94
- package/templates/hermes-agent/template/.scripts/20-runtime-repo.sh +44 -21
- package/templates/hermes-agent/template/.scripts/30-telegram.sh +182 -171
- package/templates/hermes-agent/template/.scripts/31-slack.sh +260 -165
- package/templates/hermes-agent/template/.scripts/40-plane.sh +45 -36
- package/templates/hermes-agent/template/.scripts/42-ticket-provider.sh +210 -41
- package/templates/hermes-agent/template/.scripts/70-systemd.sh +129 -15
- package/templates/hermes-agent/template/.scripts/80-registry.sh +42 -6
- package/templates/hermes-agent/template/.scripts/99-summary.sh +69 -16
- package/templates/hermes-agent/template/.scripts/_lib.sh +773 -0
- package/templates/hermes-agent/template/.scripts/channel-transaction.py +2340 -0
- package/templates/hermes-agent/template/.scripts/config.example.toml +8 -2
- package/templates/hermes-agent/template/.scripts/credential-launch.sh +5 -1
- package/templates/hermes-agent/template/.scripts/heartbeat.sh +2 -3
- package/templates/hermes-agent/template/.scripts/lib/profile-config-lock.py +182 -0
- package/templates/hermes-agent/template/.scripts/lib/profile-config-seed.py +108 -0
- package/templates/hermes-agent/template/.scripts/lib/ticket-provider.sh +93 -4
- package/templates/hermes-agent/template/.scripts/lib/voice-config.py +546 -0
- package/templates/hermes-agent/template/.scripts/providers/linear.sh +138 -25
- package/templates/hermes-agent/template/.scripts/providers/plane.sh +408 -51
- package/templates/hermes-agent/template/.scripts/providers/trello.sh +54 -6
- package/templates/hermes-agent/template/.scripts/sentinel/bin/issue-autonomous-review.sh +257 -43
- package/templates/hermes-agent/template/.scripts/sentinel/bin/issue-close-gate.sh +142 -25
- package/templates/hermes-agent/template/.scripts/sentinel/docs/autonomous-delegated-review.md +13 -19
- package/templates/hermes-agent/template/.scripts/sentinel/docs/bloodbank-events.md +29 -36
- package/templates/hermes-agent/template/.scripts/sentinel/docs/continuous-ticket-orchestration.md +3 -1
- package/templates/hermes-agent/template/.scripts/sentinel.prompt.md.jinja +7 -8
- package/templates/hermes-agent/template/.scripts/store-onepassword-secret.py +260 -0
- package/templates/hermes-agent/template/SOUL.md.jinja +14 -16
- package/templates/hermes-agent/template/hermes.jinja +1 -1
- package/templates/hermes-agent/template/role.yaml.jinja +15 -4
package/templates/hermes-agent/template/.scripts/sentinel/docs/autonomous-delegated-review.md
CHANGED
|
@@ -112,29 +112,23 @@ loop:
|
|
|
112
112
|
.scripts/sentinel/bin/issue-autonomous-review.sh <ISSUE> <REPORT> --close
|
|
113
113
|
```
|
|
114
114
|
|
|
115
|
-
## Decision
|
|
115
|
+
## Decision record
|
|
116
116
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
`reviewer_agent`, `evidence_file`, `report_file`. See `bloodbank-events.md`.
|
|
117
|
+
The verdict is carried by the script's exit code (`0` accepted, `3` held) and
|
|
118
|
+
its stdout/stderr, plus the review report and the ticket comment it posts. The
|
|
119
|
+
durable accountability trail is the review report + evidence file in the repo.
|
|
120
|
+
No event is published; see `bloodbank-events.md` for why the old
|
|
121
|
+
`repo.issue.*` family was retired.
|
|
123
122
|
|
|
124
123
|
## Downstream regression rollback
|
|
125
124
|
|
|
126
125
|
Deferring operator QA means a later dependent can prove a review-accepted
|
|
127
126
|
feature is ACTUALLY BROKEN. When that happens, move the accepted ticket back to
|
|
128
127
|
active (`started` if a worker takes it now, else `unstarted`) as a PREREQUISITE
|
|
129
|
-
of the dependent, via the adapter (`tp transition <id> <state>`)
|
|
130
|
-
the dependent + symptom
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
bloodbank.v1.repo.<repo>.issue.review_rollback.recorded
|
|
134
|
-
```
|
|
135
|
-
|
|
136
|
-
Required data: `issue`, `surfaced_by`, `reason`. The dependent is blocked on the
|
|
137
|
-
prerequisite until it is fixed. This is expected and healthy — it is the trade
|
|
128
|
+
of the dependent, via the adapter (`tp transition <id> <state>`), and comment
|
|
129
|
+
naming the dependent + symptom. Record the rollback in the ticket comment and
|
|
130
|
+
the issue evidence file. The dependent is blocked on the prerequisite until it
|
|
131
|
+
is fixed. This is expected and healthy — it is the trade
|
|
138
132
|
for deferring operator QA, not a failure.
|
|
139
133
|
|
|
140
134
|
## Review report shape
|
|
@@ -163,6 +157,6 @@ Write `<ISSUE>.review.md`; the script validates it:
|
|
|
163
157
|
## Operator override
|
|
164
158
|
|
|
165
159
|
`reconcile.auto_review: false` (role.yaml) or `RECONCILE_AUTO_REVIEW=off`
|
|
166
|
-
disables autonomous acceptance. Autonomous acceptances are
|
|
167
|
-
the
|
|
168
|
-
|
|
160
|
+
disables autonomous acceptance. Autonomous acceptances are traceable via the
|
|
161
|
+
review report, the issue evidence file, and the ticket comment the script
|
|
162
|
+
posts.
|
|
@@ -4,46 +4,39 @@ Status: Sentinel engine protocol (provider-agnostic)
|
|
|
4
4
|
|
|
5
5
|
## Purpose
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
The board remains the human command center and issue evidence files remain the
|
|
8
|
+
close gate. Neither depends on an event trail: `bin/issue-close-gate.sh` and
|
|
9
|
+
`bin/issue-autonomous-review.sh` report their verdicts on stdout/stderr and via
|
|
10
|
+
their exit codes, and publish nothing.
|
|
11
|
+
|
|
12
|
+
The `repo.issue.*` family the sentinel loop used to mint was retired on
|
|
13
|
+
2026-08-28. It was never published to NATS and never consumed by anything, and
|
|
14
|
+
its shape was invalid twice over: it embedded the repo slug inside the type
|
|
15
|
+
(`bloodbank.v1.repo.<repo>.issue.…`) and `issue` is not in the Bloodbank §7
|
|
16
|
+
entity allowlist. There is no correct version of it to migrate to, so it is
|
|
17
|
+
gone rather than renamed.
|
|
11
18
|
|
|
12
19
|
## Emitter
|
|
13
20
|
|
|
21
|
+
`bin/emit-event.py` stays as a dependency-free local emitter for a family a
|
|
22
|
+
future pass genuinely needs:
|
|
23
|
+
|
|
14
24
|
```bash
|
|
15
25
|
.scripts/sentinel/bin/emit-event.py <event_type> --field key=value [...]
|
|
16
26
|
```
|
|
17
27
|
|
|
18
|
-
|
|
19
|
-
(git-ignored dev spool) using the Hermes CloudEvents envelope shape.
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
## Rules
|
|
35
|
-
|
|
36
|
-
- Emit events for consequential transitions; do not invent types casually.
|
|
37
|
-
- Event emission never replaces the board update or issue evidence.
|
|
38
|
-
- If emission fails, continue and report the trail is incomplete.
|
|
39
|
-
- Autonomous acceptance is legitimate only when
|
|
40
|
-
`issue.autonomous_review.decided` is emitted with `decision=accepted` and
|
|
41
|
-
`close_gate=pass` by `bin/issue-autonomous-review.sh`. That script will not
|
|
42
|
-
emit an `accepted` decision while the close gate fails or drift is `significant`.
|
|
43
|
-
|
|
44
|
-
## Canonical BloodBank
|
|
45
|
-
|
|
46
|
-
These project-local repo-lane events are BloodBank-*style*. Promote a type to a
|
|
47
|
-
canonical NATS subject only after adding its JSON Schema to the BloodBank schema
|
|
48
|
-
tree and passing validation. The local emitter does not require NATS so the loop
|
|
49
|
-
stays reliable offline.
|
|
28
|
+
It appends to `_bmad-output/implementation-artifacts/bloodbank-events.jsonl`
|
|
29
|
+
(git-ignored dev spool) using the Hermes CloudEvents envelope shape. Nothing in
|
|
30
|
+
the engine calls it today.
|
|
31
|
+
|
|
32
|
+
## Naming, before you add one
|
|
33
|
+
|
|
34
|
+
Event types are exactly four tokens — `bloodbank.<domain>.<entity>.<action>` —
|
|
35
|
+
with `<domain>` and `<entity>` drawn from the allowlists in
|
|
36
|
+
`~/code/33GOD/bloodbank/docs/event-naming.md` (§6/§7). Repo and agent identity
|
|
37
|
+
go in `data.repo` / `actor.agent_id`, never in a type or subject token. Schema
|
|
38
|
+
revision lives in `dataschema`/`schemaref`, never in the type.
|
|
39
|
+
|
|
40
|
+
Add a family when something will actually consume it — register the schema in
|
|
41
|
+
the BloodBank schema tree first, then emit. A type nobody reads is a type that
|
|
42
|
+
rots into the wrong shape.
|
package/templates/hermes-agent/template/.scripts/sentinel/docs/continuous-ticket-orchestration.md
CHANGED
|
@@ -31,7 +31,9 @@ current and machine-readable: `source`, `agent_id`, `repo`, `ticket_provider`,
|
|
|
31
31
|
|
|
32
32
|
## Source order (each pass)
|
|
33
33
|
|
|
34
|
-
1. Active milestone (`tp active_milestone`) and issues (`tp list_issues`).
|
|
34
|
+
1. Active milestone (`tp active_milestone`) and issues (`tp list_issues`). Plane's
|
|
35
|
+
issue list is project-wide: current-cycle visibility is exactly the rows with
|
|
36
|
+
`in_active_milestone:true`, not every returned issue.
|
|
35
37
|
2. Local evidence under `_bmad-output/implementation-artifacts/issue-evidence/`.
|
|
36
38
|
3. Live worker state: zellij sessions, worktrees, branches, recent git.
|
|
37
39
|
|
|
@@ -33,8 +33,8 @@ on Linear, Plane, or Trello.
|
|
|
33
33
|
## Pass
|
|
34
34
|
|
|
35
35
|
**Trigger.** This pass runs both on the cheap heartbeat timer AND on a live
|
|
36
|
-
**Plane board event** — a `bloodbank.evt.
|
|
37
|
-
by the `plane-webhook-bridge`. If a specific ticket event triggered you, FIRST
|
|
36
|
+
**Plane board event** — a `bloodbank.evt.repo.task.*` event (repo identity lives
|
|
37
|
+
in `data.repo`, never in the subject) delivered by the `plane-webhook-bridge`. If a specific ticket event triggered you, FIRST
|
|
38
38
|
`tp get_issue <that ticket>`, read the change, and react to it (triage / refine /
|
|
39
39
|
comment / transition per the lifecycle) before the general reconcile below.
|
|
40
40
|
|
|
@@ -68,19 +68,18 @@ comment / transition per the lifecycle) before the general reconcile below.
|
|
|
68
68
|
independence satisfied, close gate passes): autonomously TREAT THE TICKET AS
|
|
69
69
|
DONE. Leave it in the review lane as the operator's deferred-QA queue (do NOT
|
|
70
70
|
auto-transition to `completed`; `--close` is an optional operator-QA-sweep
|
|
71
|
-
flag the loop omits). Record the adversarial review report
|
|
72
|
-
|
|
71
|
+
flag the loop omits). Record the adversarial review report and post ONE
|
|
72
|
+
ticket comment stating the autonomous acceptance with a
|
|
73
73
|
pointer to the report — never a "waiting on you" comment, no grace wait. A
|
|
74
74
|
dependent blocked only on this review-accepted feature is now unblocked.
|
|
75
75
|
- **Held** (a real finding or gate failure): the ticket goes back to active
|
|
76
76
|
(`started` if a worker takes it now, else `unstarted`); record the hold
|
|
77
|
-
reasons and
|
|
77
|
+
reasons in the review report and a ticket comment.
|
|
78
78
|
6. **Regression rollback.** If a later dependent proves a review-accepted feature
|
|
79
79
|
is ACTUALLY BROKEN, move that feature back to active (`started` if a worker
|
|
80
80
|
takes it now, else `unstarted`) as a PREREQUISITE of the dependent; comment
|
|
81
|
-
naming the dependent + symptom
|
|
82
|
-
|
|
83
|
-
`{issue, surfaced_by, reason}`. The dependent stays blocked on the prerequisite
|
|
81
|
+
naming the dependent + symptom, and record the rollback (issue, surfaced_by,
|
|
82
|
+
reason) in the issue evidence file. The dependent stays blocked on the prerequisite
|
|
84
83
|
until it is fixed. This is expected and healthy — it is the trade for deferring
|
|
85
84
|
operator QA, not a failure.
|
|
86
85
|
7. **External blockers only** (credentials, third-party access, paid actions,
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Store process-only secrets in 1Password and print op:// references.
|
|
3
|
+
|
|
4
|
+
Secret values are read from stdin and are never placed in argv, a temporary
|
|
5
|
+
file, stdout, or an ``op`` child environment. Item JSON travels only over an
|
|
6
|
+
anonymous pipe to the 1Password CLI. Credential pairs are staged in a new,
|
|
7
|
+
versioned item and both fields are verified before either reference is emitted.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
import re
|
|
15
|
+
import shutil
|
|
16
|
+
import subprocess
|
|
17
|
+
import sys
|
|
18
|
+
import uuid
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def fail(message: str) -> "None":
|
|
22
|
+
raise SystemExit(f"1Password secret storage failed: {message}")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def op_run(args: list[str], *, payload: str | None = None) -> subprocess.CompletedProcess[str]:
|
|
26
|
+
allowed = (
|
|
27
|
+
"PATH",
|
|
28
|
+
"HOME",
|
|
29
|
+
"USERPROFILE",
|
|
30
|
+
"APPDATA",
|
|
31
|
+
"LOCALAPPDATA",
|
|
32
|
+
"SystemRoot",
|
|
33
|
+
"TMPDIR",
|
|
34
|
+
"TMP",
|
|
35
|
+
"TEMP",
|
|
36
|
+
"XDG_CONFIG_HOME",
|
|
37
|
+
"XDG_RUNTIME_DIR",
|
|
38
|
+
"OP_ACCOUNT",
|
|
39
|
+
"OP_CONNECT_HOST",
|
|
40
|
+
"OP_CONNECT_TOKEN",
|
|
41
|
+
"OP_LOAD_DESKTOP_APP_SETTINGS",
|
|
42
|
+
"OP_SERVICE_ACCOUNT_TOKEN",
|
|
43
|
+
)
|
|
44
|
+
env = {name: os.environ[name] for name in allowed if name in os.environ}
|
|
45
|
+
env.update(
|
|
46
|
+
(name, value)
|
|
47
|
+
for name, value in os.environ.items()
|
|
48
|
+
if name.startswith("OP_SESSION_")
|
|
49
|
+
)
|
|
50
|
+
env["NO_COLOR"] = "1"
|
|
51
|
+
return subprocess.run(
|
|
52
|
+
[OP, *args],
|
|
53
|
+
input=payload,
|
|
54
|
+
text=True,
|
|
55
|
+
stdout=subprocess.PIPE,
|
|
56
|
+
stderr=subprocess.PIPE,
|
|
57
|
+
check=False,
|
|
58
|
+
env=env,
|
|
59
|
+
timeout=30,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
OP = shutil.which("op") or ""
|
|
64
|
+
if not OP:
|
|
65
|
+
fail("the op CLI is not installed")
|
|
66
|
+
|
|
67
|
+
if len(sys.argv) == 3 and sys.argv[1] == "--validate-reference":
|
|
68
|
+
reference = sys.argv[2]
|
|
69
|
+
if not reference.startswith("op://") or any(ch in reference for ch in "\r\n\0"):
|
|
70
|
+
fail("invalid 1Password reference")
|
|
71
|
+
resolved = op_run(["read", "--", reference])
|
|
72
|
+
if resolved.returncode != 0 or not resolved.stdout.rstrip("\n"):
|
|
73
|
+
fail("the configured reference did not resolve")
|
|
74
|
+
raise SystemExit(0)
|
|
75
|
+
|
|
76
|
+
if len(sys.argv) == 4 and sys.argv[1] == "--delete-item-id":
|
|
77
|
+
vault, item_id = sys.argv[2:]
|
|
78
|
+
if not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._ -]{0,126}", vault):
|
|
79
|
+
fail("vault name contains unsupported characters")
|
|
80
|
+
if not re.fullmatch(r"[A-Za-z0-9_-]{1,128}", item_id):
|
|
81
|
+
fail("invalid immutable item id")
|
|
82
|
+
deleted = op_run(["item", "delete", item_id, "--vault", vault, "--archive"])
|
|
83
|
+
if deleted.returncode != 0:
|
|
84
|
+
fail("staged item cleanup was rejected")
|
|
85
|
+
raise SystemExit(0)
|
|
86
|
+
|
|
87
|
+
pair_mode = len(sys.argv) == 6 and sys.argv[1] == "--store-pair"
|
|
88
|
+
single_staged_mode = len(sys.argv) == 5 and sys.argv[1] == "--store-staged"
|
|
89
|
+
staged_mode = pair_mode or single_staged_mode
|
|
90
|
+
if pair_mode:
|
|
91
|
+
vault, item, field_one, field_two = sys.argv[2:]
|
|
92
|
+
if field_one == field_two or not all(
|
|
93
|
+
re.fullmatch(r"[a-z][a-z0-9_]{0,63}", field)
|
|
94
|
+
for field in (field_one, field_two)
|
|
95
|
+
):
|
|
96
|
+
fail("pair field names are invalid or not distinct")
|
|
97
|
+
elif single_staged_mode:
|
|
98
|
+
vault, item, field_one = sys.argv[2:]
|
|
99
|
+
if not re.fullmatch(r"[a-z][a-z0-9_]{0,63}", field_one):
|
|
100
|
+
fail("staged field name is invalid")
|
|
101
|
+
else:
|
|
102
|
+
if len(sys.argv) != 3:
|
|
103
|
+
fail(
|
|
104
|
+
"usage: store-onepassword-secret.py <vault> <item>, "
|
|
105
|
+
"--store-staged <vault> <item-prefix> <field>, or "
|
|
106
|
+
"--store-pair <vault> <item-prefix> <field-one> <field-two>"
|
|
107
|
+
)
|
|
108
|
+
vault, item = sys.argv[1:]
|
|
109
|
+
safe = re.compile(r"[A-Za-z0-9][A-Za-z0-9._ -]{0,126}")
|
|
110
|
+
if not safe.fullmatch(vault) or not safe.fullmatch(item):
|
|
111
|
+
fail("vault or item name contains unsupported characters")
|
|
112
|
+
|
|
113
|
+
payload = sys.stdin.read()
|
|
114
|
+
if pair_mode:
|
|
115
|
+
values = payload.split("\n")
|
|
116
|
+
if len(values) != 2 or not all(values):
|
|
117
|
+
fail("credential pair input must contain exactly two non-empty lines")
|
|
118
|
+
secrets = [(field_one, values[0]), (field_two, values[1])]
|
|
119
|
+
elif single_staged_mode:
|
|
120
|
+
if payload.endswith("\n"):
|
|
121
|
+
payload = payload[:-1]
|
|
122
|
+
secrets = [(field_one, payload)]
|
|
123
|
+
else:
|
|
124
|
+
if payload.endswith("\n"):
|
|
125
|
+
payload = payload[:-1]
|
|
126
|
+
secrets = [("password", payload)]
|
|
127
|
+
if any(not value or any(ch in value for ch in "\r\n\0") for _, value in secrets):
|
|
128
|
+
fail("refusing to store an empty or multiline value")
|
|
129
|
+
|
|
130
|
+
if staged_mode:
|
|
131
|
+
if len(item) > 105:
|
|
132
|
+
fail("item prefix is too long for a versioned item")
|
|
133
|
+
item = f"{item}-v-{uuid.uuid4().hex[:16]}"
|
|
134
|
+
document = {
|
|
135
|
+
"title": item,
|
|
136
|
+
"category": "PASSWORD",
|
|
137
|
+
"fields": [
|
|
138
|
+
{
|
|
139
|
+
"id": field,
|
|
140
|
+
"type": "CONCEALED",
|
|
141
|
+
"label": field,
|
|
142
|
+
"value": value,
|
|
143
|
+
}
|
|
144
|
+
for field, value in secrets
|
|
145
|
+
]
|
|
146
|
+
+ [
|
|
147
|
+
{
|
|
148
|
+
"id": "notesPlain",
|
|
149
|
+
"type": "STRING",
|
|
150
|
+
"purpose": "NOTES",
|
|
151
|
+
"label": "notesPlain",
|
|
152
|
+
"value": (
|
|
153
|
+
"Managed by hermes-agent-template as a staged credential; "
|
|
154
|
+
"rotate through the provisioner."
|
|
155
|
+
),
|
|
156
|
+
}
|
|
157
|
+
],
|
|
158
|
+
}
|
|
159
|
+
stored = op_run(
|
|
160
|
+
["item", "create", "--vault", vault, "--format=json", "-"],
|
|
161
|
+
payload=json.dumps(document),
|
|
162
|
+
)
|
|
163
|
+
if stored.returncode != 0:
|
|
164
|
+
fail("op rejected the staged credential update")
|
|
165
|
+
try:
|
|
166
|
+
created = json.loads(stored.stdout)
|
|
167
|
+
item_id = str(created.get("id") or "")
|
|
168
|
+
except (AttributeError, TypeError, json.JSONDecodeError):
|
|
169
|
+
fail("op item create returned invalid JSON")
|
|
170
|
+
if not re.fullmatch(r"[A-Za-z0-9_-]{1,128}", item_id):
|
|
171
|
+
fail("op item create omitted an immutable item id")
|
|
172
|
+
references = [f"op://{vault}/{item_id}/{field}" for field, _ in secrets]
|
|
173
|
+
for reference, (_, expected) in zip(references, secrets, strict=True):
|
|
174
|
+
verified = op_run(["read", "--", reference])
|
|
175
|
+
if verified.returncode != 0 or verified.stdout.rstrip("\n") != expected:
|
|
176
|
+
# The item is not active until the caller commits its refs. Archive
|
|
177
|
+
# this failed stage by immutable id; never guess/delete by title.
|
|
178
|
+
op_run(["item", "delete", item_id, "--vault", vault, "--archive"])
|
|
179
|
+
fail("a staged credential field did not verify; cleanup attempted")
|
|
180
|
+
print("\n".join([item_id, *references]))
|
|
181
|
+
raise SystemExit(0)
|
|
182
|
+
|
|
183
|
+
listed = op_run(["item", "list", "--vault", vault, "--format=json"])
|
|
184
|
+
if listed.returncode != 0:
|
|
185
|
+
fail("vault access/authentication was rejected")
|
|
186
|
+
try:
|
|
187
|
+
rows = json.loads(listed.stdout)
|
|
188
|
+
except (TypeError, json.JSONDecodeError):
|
|
189
|
+
fail("op item list returned invalid JSON")
|
|
190
|
+
|
|
191
|
+
matches = [row for row in rows if isinstance(row, dict) and row.get("title") == item]
|
|
192
|
+
if len(matches) > 1:
|
|
193
|
+
fail("multiple items have the requested title")
|
|
194
|
+
|
|
195
|
+
if matches:
|
|
196
|
+
item_id = str(matches[0].get("id") or "")
|
|
197
|
+
if not item_id:
|
|
198
|
+
fail("the existing item has no id")
|
|
199
|
+
fetched = op_run(["item", "get", item_id, "--vault", vault, "--format=json"])
|
|
200
|
+
if fetched.returncode != 0:
|
|
201
|
+
fail("the existing item could not be read")
|
|
202
|
+
try:
|
|
203
|
+
document = json.loads(fetched.stdout)
|
|
204
|
+
except (TypeError, json.JSONDecodeError):
|
|
205
|
+
fail("op item get returned invalid JSON")
|
|
206
|
+
fields = document.get("fields")
|
|
207
|
+
if not isinstance(fields, list):
|
|
208
|
+
fail("the existing item has no fields list")
|
|
209
|
+
concealed_field = next(
|
|
210
|
+
(field for field in fields if isinstance(field, dict) and field.get("id") == "password"),
|
|
211
|
+
None,
|
|
212
|
+
)
|
|
213
|
+
if concealed_field is None:
|
|
214
|
+
concealed_field = {
|
|
215
|
+
"id": "password",
|
|
216
|
+
"type": "CONCEALED",
|
|
217
|
+
"purpose": "PASSWORD",
|
|
218
|
+
"label": "password",
|
|
219
|
+
}
|
|
220
|
+
fields.append(concealed_field)
|
|
221
|
+
concealed_field["value"] = secrets[0][1]
|
|
222
|
+
stored = op_run(
|
|
223
|
+
["item", "edit", item_id, "--vault", vault],
|
|
224
|
+
payload=json.dumps(document),
|
|
225
|
+
)
|
|
226
|
+
else:
|
|
227
|
+
document = {
|
|
228
|
+
"title": item,
|
|
229
|
+
"category": "PASSWORD",
|
|
230
|
+
"fields": [
|
|
231
|
+
{
|
|
232
|
+
"id": "password",
|
|
233
|
+
"type": "CONCEALED",
|
|
234
|
+
"purpose": "PASSWORD",
|
|
235
|
+
"label": "password",
|
|
236
|
+
"value": secrets[0][1],
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
"id": "notesPlain",
|
|
240
|
+
"type": "STRING",
|
|
241
|
+
"purpose": "NOTES",
|
|
242
|
+
"label": "notesPlain",
|
|
243
|
+
"value": "Managed by hermes-agent-template; rotate through the provisioner.",
|
|
244
|
+
},
|
|
245
|
+
],
|
|
246
|
+
}
|
|
247
|
+
stored = op_run(
|
|
248
|
+
["item", "create", "--vault", vault, "-"],
|
|
249
|
+
payload=json.dumps(document),
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
if stored.returncode != 0:
|
|
253
|
+
fail("op rejected the item update")
|
|
254
|
+
|
|
255
|
+
reference = f"op://{vault}/{item}/password"
|
|
256
|
+
verified = op_run(["read", "--", reference])
|
|
257
|
+
if verified.returncode != 0 or verified.stdout.rstrip("\n") != secrets[0][1]:
|
|
258
|
+
fail("the stored reference did not resolve to the supplied value")
|
|
259
|
+
|
|
260
|
+
print(reference)
|
|
@@ -39,7 +39,7 @@ question — not three vague ones.
|
|
|
39
39
|
|
|
40
40
|
## Default contract (every role)
|
|
41
41
|
|
|
42
|
-
Envelope shape: CloudEvents 1.0, type `bloodbank
|
|
42
|
+
Envelope shape: CloudEvents 1.0, type `bloodbank.<domain>.<entity>.<action>`,
|
|
43
43
|
`actor.agent_id = {{ agent_id }}`, `producer = hermes-agent:{{ agent_id }}`,
|
|
44
44
|
`source = hermes://agent/{{ agent_id }}`. Inbound commands arrive through the
|
|
45
45
|
fleet-shared Hermes gateway and are routed by `data.target_agent_id`.
|
|
@@ -55,9 +55,8 @@ You are the **project-manager ORCHESTRATOR** — the autonomous Hermes carrier o
|
|
|
55
55
|
Momo, and the twin of the human-drivable Momo. You share ONE board and ONE
|
|
56
56
|
Hindsight bank with it; stay attributable and never split-brain the state. You
|
|
57
57
|
triage incoming requests, decompose them into discrete tasks on the Plane
|
|
58
|
-
board, and route work to other agents (e.g. the
|
|
59
|
-
|
|
60
|
-
`data.target_agent_id = {{ target_repo }}-dev`).
|
|
58
|
+
board, and route work to other agents (e.g. the `{{ target_repo }}-dev`
|
|
59
|
+
role).
|
|
61
60
|
|
|
62
61
|
**Prime directives (non-negotiable):**
|
|
63
62
|
- **Never mutate code** — every code change flows through a delegated worker.
|
|
@@ -75,9 +74,9 @@ Default execution workflow for implementation delivery: use
|
|
|
75
74
|
(WIP=1, spec review gate, quality review gate).
|
|
76
75
|
|
|
77
76
|
Decision events you commonly emit:
|
|
78
|
-
- `bloodbank.
|
|
79
|
-
- `bloodbank.
|
|
80
|
-
- `bloodbank.
|
|
77
|
+
- `bloodbank.repo.decision.recorded`
|
|
78
|
+
- `bloodbank.repo.intake.triaged`
|
|
79
|
+
- `bloodbank.repo.task.created`
|
|
81
80
|
|
|
82
81
|
Put `repo = {{ target_repo }}` in event data; never insert repo or agent
|
|
83
82
|
identifiers into Bloodbank type or subject tokens.
|
|
@@ -94,18 +93,17 @@ You are the **developer**. You implement tasks dispatched by the PM agent,
|
|
|
94
93
|
write tests, run the project's `mise run test` or equivalent, open PRs, and
|
|
95
94
|
respond to review comments. You do not merge — that's the reviewer's call.
|
|
96
95
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
- `bloodbank.v1.repo.pr.opened`
|
|
96
|
+
You emit no Bloodbank event family of your own. Report progress on the ticket;
|
|
97
|
+
if a delivery fact is genuinely worth publishing, ask for a family to be minted
|
|
98
|
+
first — do not invent a type on the fly.
|
|
101
99
|
{%- elif role == "review" -%}
|
|
102
100
|
You are the **reviewer**. You read PRs critically, check against the
|
|
103
101
|
project's CONTRIBUTING and AGENTS.md, and either approve or request changes.
|
|
104
102
|
You hold the merge gate for non-trivial changes.
|
|
105
103
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
104
|
+
You emit no Bloodbank event family of your own. The review verdict lives on the
|
|
105
|
+
ticket and in the review report; if a review fact is genuinely worth publishing,
|
|
106
|
+
ask for a family to be minted first — do not invent a type on the fly.
|
|
109
107
|
{%- elif role == "reporter" -%}
|
|
110
108
|
You are the **DeLoNET company reporter**. Your single durable product is the
|
|
111
109
|
operator's evidence-backed daily company rollup. Run the `delonet-daily-report`
|
|
@@ -135,8 +133,8 @@ credential is verified as owned by this reporter. Never reuse another agent's
|
|
|
135
133
|
Telegram or Slack identity.
|
|
136
134
|
{%- else -%}
|
|
137
135
|
You operate as the **{{ role }}** agent for this repo. Define your contract
|
|
138
|
-
in this file (this section)
|
|
139
|
-
|
|
136
|
+
in this file (this section) — this file is the fleet's record of what to
|
|
137
|
+
route to you.
|
|
140
138
|
{%- endif %}
|
|
141
139
|
|
|
142
140
|
## DeloNet conventions you respect
|
|
@@ -42,7 +42,7 @@ if ! REPO_ROOT="$(git -C "$ROLE_DIR" rev-parse --show-toplevel 2>/dev/null)"; th
|
|
|
42
42
|
fi
|
|
43
43
|
|
|
44
44
|
# Never pass an identity-bearing chat credential from shared fleet state into
|
|
45
|
-
# a profile process. Hermes
|
|
45
|
+
# a profile process. Hermes resolves the named profile's 1Password references.
|
|
46
46
|
unset TELEGRAM_BOT_TOKEN SLACK_BOT_TOKEN SLACK_APP_TOKEN
|
|
47
47
|
|
|
48
48
|
CONFIG="${HERMES_TEMPLATE_CONFIG:-${XDG_CONFIG_HOME:-$HOME/.config}/hermes-agent-template/config.toml}"
|
|
@@ -30,8 +30,8 @@ telegram:
|
|
|
30
30
|
bot_username: {{ bot_handle | tojson }}
|
|
31
31
|
bot_id: ""
|
|
32
32
|
|
|
33
|
-
# Slack is opt-in. Credentials live
|
|
34
|
-
# safe
|
|
33
|
+
# Slack is opt-in. Credentials live in 1Password and the profile maps only
|
|
34
|
+
# op:// references; this block records safe state and auth.test identity.
|
|
35
35
|
slack:
|
|
36
36
|
provisioning_status: "deferred"
|
|
37
37
|
team_id: ""
|
|
@@ -46,7 +46,6 @@ slack:
|
|
|
46
46
|
ticket_provider:
|
|
47
47
|
name: {{ ticket_provider | tojson }}
|
|
48
48
|
board_id: ""
|
|
49
|
-
board_url: ""
|
|
50
49
|
{%- if ticket_provider == 'linear' %}
|
|
51
50
|
team: "" # Linear team key, e.g. DEL (set before first sentinel run)
|
|
52
51
|
project: "" # optional Linear project name to scope milestones/issues
|
|
@@ -56,14 +55,19 @@ ticket_provider:
|
|
|
56
55
|
{%- elif ticket_provider == 'trello' %}
|
|
57
56
|
board: "" # Trello board id (set by 42-ticket-provider.sh)
|
|
58
57
|
{%- endif %}
|
|
58
|
+
timezone: "" # IANA project calendar; empty = TICKET_PROVIDER_TIMEZONE, TZ, then host-local
|
|
59
59
|
# Optional normalized-state name overrides for non-standard boards.
|
|
60
|
+
backlog: "" # e.g. "Backlog"
|
|
61
|
+
unstarted: "" # e.g. "Todo"
|
|
62
|
+
started: "" # e.g. "In Progress"
|
|
60
63
|
in_review: "" # e.g. "In Review"
|
|
61
64
|
completed: "" # e.g. "Done"
|
|
62
65
|
cancelled: "" # e.g. "Cancelled"
|
|
63
66
|
|
|
64
67
|
# Board-reconciliation knobs for the heartbeat sentinel pass.
|
|
65
68
|
reconcile:
|
|
66
|
-
enabled:
|
|
69
|
+
enabled: {{ reconcile_enabled | tojson }} # default on for PM operation; set false as the explicit checkpoint-only opt-out
|
|
70
|
+
explicit_opt_out: {% if reconcile_enabled %}false{% else %}true{% endif %} # distinguishes operator choice from the legacy default-off manifest
|
|
67
71
|
grace_hours: 0 # 0 = no human-approval wait (adversarially-reviewed review-lane = done); set >0 to require a wait
|
|
68
72
|
auto_review: true # autonomous adversarial review (act, do not wait) on/off
|
|
69
73
|
|
|
@@ -84,6 +88,13 @@ bloodbank:
|
|
|
84
88
|
target_agent_id: {{ agent_id | tojson }}
|
|
85
89
|
producer: {{ ("hermes-agent:" ~ agent_id) | tojson }}
|
|
86
90
|
|
|
91
|
+
# Reconciled by 70-systemd.sh. These are lifecycle observations, not desired
|
|
92
|
+
# state: gateway stays deferred until a dedicated chat credential is verified;
|
|
93
|
+
# heartbeat is independent and drives PM board reconciliation by default.
|
|
94
|
+
service_state:
|
|
95
|
+
gateway: "pending"
|
|
96
|
+
heartbeat: "pending"
|
|
97
|
+
|
|
87
98
|
# Pure-local role-owned state. HERMES_HOME itself is the named profile directory
|
|
88
99
|
# above; the legacy GitHub identity remains metadata-only for locating old
|
|
89
100
|
# archives, and provisioning never creates or attaches a submodule.
|