@delorenj/pjangler 1.4.6 → 1.5.3
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 +41 -596
- package/dist/assets/project-notebook-skill/SHA256SUMS +5 -5
- package/dist/assets/project-notebook-skill/export-manifest.json +5 -5
- package/dist/assets/project-notebook-skill/hooks/session-end.sh +20 -1
- package/dist/assets/project-notebook-skill/hooks/session-start.sh +20 -1
- package/dist/assets/project-notebook-skill/references/configuration.md +2 -0
- package/dist/assets/project-notebook-skill/scripts/project-hooks.py +33 -3
- package/dist/assets/project-notebook-skill/tests/test_project_hooks.py +37 -0
- package/dist/index.js +9385 -18379
- package/dist/mcp-server.js +9432 -17852
- package/dist/notebook/journal-service-main.js +5682 -0
- package/dist/project-registry-service.js +748 -0
- package/dist/prompt.js +2 -1
- package/package.json +15 -10
- package/scripts/install-project-registry.mjs +34 -0
- package/scripts/migrate-project-registry.mjs +117 -0
- package/src/project/registryClient.ts +62 -0
- package/templates/commonproject/copier.yml +16 -19
- package/templates/commonproject/template/.agents/hooks/README.md +10 -4
- package/templates/commonproject/template/.agents/hooks/hermes/hindsight-hook.sh +12 -2
- package/templates/commonproject/template/.agents/hooks/hooks.master.json +10 -10
- package/templates/commonproject/template/.agents/hooks/lib/hook-guard.sh +13 -0
- package/templates/commonproject/template/.agents/hooks/sync.py +66 -17
- package/templates/commonproject/template/.agents/skills.json +0 -1
- package/templates/commonproject/template/.github/workflows/code-review.yml.jinja +7 -1
- package/templates/commonproject/template/.project.json.jinja +1 -1
- package/templates/commonproject/template/mise.toml.jinja +11 -21
- package/contracts/fleet-contract.yaml +0 -584
- package/templates/commonproject/template/.mise/scripts/provision-packs.py +0 -587
- package/templates/commonproject/template/.mise/scripts/sync-skills.py +0 -2183
- package/templates/hermes-agent/copier.yml +0 -186
- package/templates/hermes-agent/template/.gitignore.jinja +0 -11
- package/templates/hermes-agent/template/.runtime-scaffold/.gitattributes +0 -10
- package/templates/hermes-agent/template/.runtime-scaffold/.gitignore.jinja +0 -44
- package/templates/hermes-agent/template/.runtime-scaffold/README.md +0 -56
- package/templates/hermes-agent/template/.runtime-scaffold/decisions/.gitkeep +0 -2
- package/templates/hermes-agent/template/.runtime-scaffold/logs/.gitkeep +0 -1
- package/templates/hermes-agent/template/.runtime-scaffold/memories/MEMORY.md +0 -50
- package/templates/hermes-agent/template/.runtime-scaffold/memories/USER.md +0 -51
- package/templates/hermes-agent/template/.scripts/00-banner.sh +0 -21
- package/templates/hermes-agent/template/.scripts/01-config.sh +0 -60
- package/templates/hermes-agent/template/.scripts/05-fleet-env.sh +0 -42
- package/templates/hermes-agent/template/.scripts/10-hermes-profile.sh +0 -222
- package/templates/hermes-agent/template/.scripts/20-runtime-repo.sh +0 -199
- package/templates/hermes-agent/template/.scripts/30-telegram.sh +0 -363
- package/templates/hermes-agent/template/.scripts/31-slack.sh +0 -457
- package/templates/hermes-agent/template/.scripts/40-plane.sh +0 -60
- package/templates/hermes-agent/template/.scripts/42-ticket-provider.sh +0 -373
- package/templates/hermes-agent/template/.scripts/60-bloodbank.sh +0 -18
- package/templates/hermes-agent/template/.scripts/70-systemd.sh +0 -354
- package/templates/hermes-agent/template/.scripts/80-registry.sh +0 -185
- package/templates/hermes-agent/template/.scripts/99-summary.sh +0 -87
- package/templates/hermes-agent/template/.scripts/_lib.sh +0 -1138
- package/templates/hermes-agent/template/.scripts/channel-transaction.py +0 -2340
- package/templates/hermes-agent/template/.scripts/checkpoint.sh +0 -82
- package/templates/hermes-agent/template/.scripts/config.example.toml +0 -70
- package/templates/hermes-agent/template/.scripts/credential-launch.sh +0 -85
- package/templates/hermes-agent/template/.scripts/heartbeat.sh +0 -283
- package/templates/hermes-agent/template/.scripts/lib/fleet-env.sh +0 -202
- package/templates/hermes-agent/template/.scripts/lib/parse-fleet-env.py +0 -761
- package/templates/hermes-agent/template/.scripts/lib/profile-config-lock.py +0 -182
- package/templates/hermes-agent/template/.scripts/lib/profile-config-seed.py +0 -108
- package/templates/hermes-agent/template/.scripts/lib/ticket-provider.sh +0 -199
- package/templates/hermes-agent/template/.scripts/lib/voice-config.py +0 -546
- package/templates/hermes-agent/template/.scripts/lifecycle.sh +0 -126
- package/templates/hermes-agent/template/.scripts/momo-wip-lock.py +0 -137
- package/templates/hermes-agent/template/.scripts/providers/linear.sh +0 -329
- package/templates/hermes-agent/template/.scripts/providers/plane.sh +0 -671
- package/templates/hermes-agent/template/.scripts/providers/trello.sh +0 -241
- package/templates/hermes-agent/template/.scripts/reporter-watchdog.py +0 -242
- package/templates/hermes-agent/template/.scripts/secret-scan.py +0 -134
- package/templates/hermes-agent/template/.scripts/sentinel/bin/emit-event.py +0 -106
- package/templates/hermes-agent/template/.scripts/sentinel/bin/issue-autonomous-review.sh +0 -339
- package/templates/hermes-agent/template/.scripts/sentinel/bin/issue-close-gate.sh +0 -179
- package/templates/hermes-agent/template/.scripts/sentinel/docs/autonomous-delegated-review.md +0 -162
- package/templates/hermes-agent/template/.scripts/sentinel/docs/bloodbank-events.md +0 -42
- package/templates/hermes-agent/template/.scripts/sentinel/docs/continuous-ticket-orchestration.md +0 -77
- package/templates/hermes-agent/template/.scripts/sentinel.prompt.md.jinja +0 -116
- package/templates/hermes-agent/template/.scripts/store-onepassword-secret.py +0 -260
- package/templates/hermes-agent/template/SOUL.md.jinja +0 -197
- package/templates/hermes-agent/template/hermes.jinja +0 -109
- package/templates/hermes-agent/template/momo.jinja +0 -177
- package/templates/hermes-agent/template/role.yaml.jinja +0 -112
package/templates/hermes-agent/template/.scripts/sentinel/docs/autonomous-delegated-review.md
DELETED
|
@@ -1,162 +0,0 @@
|
|
|
1
|
-
# Autonomous adversarial review (act, do not wait)
|
|
2
|
-
|
|
3
|
-
Status: sentinel engine protocol (provider-agnostic)
|
|
4
|
-
|
|
5
|
-
## Purpose
|
|
6
|
-
|
|
7
|
-
When a ticket reaches the review lane, the sentinel runs an
|
|
8
|
-
**independent adversarial review** of the work against the operator's locked
|
|
9
|
-
intent and acts on the verdict autonomously. This is the **normal per-pass
|
|
10
|
-
path** for review-lane tickets — not a deliberately narrow escape hatch, and not
|
|
11
|
-
the only sanctioned path. The reviewer is an **adversarial microscope**: it
|
|
12
|
-
actively tries to break the work, surface unmet acceptance criteria, hidden
|
|
13
|
-
regressions, and drift. On a clean adversarial verdict the loop treats the
|
|
14
|
-
ticket as done and moves on — through the ticket-provider adapter
|
|
15
|
-
(`tp transition <id> <state>`) — and emits a BloodBank decision event carrying
|
|
16
|
-
the full report. This works identically on Linear, Plane, or Trello.
|
|
17
|
-
|
|
18
|
-
The operator's verification is **deferred** to end-of-product QA over the review
|
|
19
|
-
lane, backed by a queryable decision trail. A downstream regression rollback is
|
|
20
|
-
the safety valve. The loop does NOT wait on a human to approve or close
|
|
21
|
-
reviewed work.
|
|
22
|
-
|
|
23
|
-
## Anti-stall rule
|
|
24
|
-
|
|
25
|
-
The continuous ticket sentinel MUST act on the adversarial verdict autonomously.
|
|
26
|
-
It MUST NOT idle, stop, or end a pass solely because reviewed work awaits human
|
|
27
|
-
approval. "Looks good, waiting on the operator" is **never** a resting state and
|
|
28
|
-
never a terminal pass state. For any review-lane ticket there are exactly three
|
|
29
|
-
legitimate outcomes:
|
|
30
|
-
|
|
31
|
-
- the independent adversarial review has run and the ticket is **accepted**
|
|
32
|
-
(treat as done, move on); or
|
|
33
|
-
- the review **held** it on a real finding (back to / stays active); or
|
|
34
|
-
- the ticket has a genuine **out-of-scope blocker** (recorded and waited on).
|
|
35
|
-
|
|
36
|
-
There is no fourth "waiting for the operator's sign-off" state.
|
|
37
|
-
|
|
38
|
-
## Out-of-scope blockers (not cleared by review)
|
|
39
|
-
|
|
40
|
-
The review does not clear an out-of-scope blocker. Record the blocker and wait,
|
|
41
|
-
exactly as before:
|
|
42
|
-
|
|
43
|
-
- Blocked on external credentials, third-party access, or paid actions.
|
|
44
|
-
- Blocked on an undecided product decision.
|
|
45
|
-
- Acceptance criteria not actually satisfied by repository evidence.
|
|
46
|
-
- Depends on another open, unblocked issue.
|
|
47
|
-
|
|
48
|
-
## Preconditions
|
|
49
|
-
|
|
50
|
-
1. **Review-lane ticket** — issue is `in_review`, complete, with no out-of-scope
|
|
51
|
-
blocker outstanding.
|
|
52
|
-
2. **Evidence exists** — complete evidence file under
|
|
53
|
-
`_bmad-output/implementation-artifacts/issue-evidence/<ISSUE>.md`.
|
|
54
|
-
3. **Independent reviewer available** — the reviewer agent id MUST differ from
|
|
55
|
-
the implementer recorded in the evidence (`Worker:` / `Implemented by:`) and
|
|
56
|
-
from the delegating PM. An implementer NEVER clears their own work.
|
|
57
|
-
|
|
58
|
-
There is no mandatory grace window. `reconcile.grace_hours` (role.yaml;
|
|
59
|
-
override env `RECONCILE_GRACE_HOURS`) defaults to `0` and is an
|
|
60
|
-
optional operator knob only — set `>0` to reintroduce a wait. The reviewer does
|
|
61
|
-
not wait for the operator's first right of refusal; it reviews and acts.
|
|
62
|
-
|
|
63
|
-
## Locked intent baseline
|
|
64
|
-
|
|
65
|
-
Drift is measured against fixed intent, assembled from: the issue's acceptance
|
|
66
|
-
criteria; the active milestone (`tp active_milestone`) and the project's horizon
|
|
67
|
-
model; the product north star; and any locked planning/decision artifacts. The
|
|
68
|
-
reviewer does not re-litigate intent.
|
|
69
|
-
|
|
70
|
-
## Drift rubric
|
|
71
|
-
|
|
72
|
-
- **significant (HOLD)** — an AC unmet; user-facing capability added/removed
|
|
73
|
-
beyond the ACs or milestone; contradicts a locked decision/north star; pulls
|
|
74
|
-
Later work into Now or touches another milestone; contradicts locked
|
|
75
|
-
architecture; introduces a new external dependency/credential/paid action.
|
|
76
|
-
- **minor (accept allowed)** — internal refactors, extra tests, naming, cosmetic
|
|
77
|
-
deviations within locked intent, docs.
|
|
78
|
-
- **none** — matches locked intent and ACs.
|
|
79
|
-
|
|
80
|
-
Only `none`/`minor` with no unresolved critical/high findings may be accepted.
|
|
81
|
-
|
|
82
|
-
## Decision
|
|
83
|
-
|
|
84
|
-
Run from the role's bin (couples gate + drift + event + acceptance):
|
|
85
|
-
|
|
86
|
-
```bash
|
|
87
|
-
.scripts/sentinel/bin/issue-autonomous-review.sh <ISSUE> <REPORT>
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
- **accepted** — independent adversarial review cleared it (drift `none|minor`,
|
|
91
|
-
no unresolved critical/high findings, independence satisfied, close gate
|
|
92
|
-
passes); the loop treats the ticket as done and moves on; event emitted with
|
|
93
|
-
`decision=accepted`. The ticket comment states the autonomous acceptance and
|
|
94
|
-
points to the review report — it does NOT request approval.
|
|
95
|
-
- **held** — any adversarial gate fails; the ticket goes back to / stays active;
|
|
96
|
-
event `decision=held` with the reason. When in doubt, hold.
|
|
97
|
-
|
|
98
|
-
The close gate (evidence completeness) remains a HARD, AUTOMATED lock: the
|
|
99
|
-
script will not emit an `accepted` decision while the close gate fails, drift is
|
|
100
|
-
`significant`, an unresolved critical/high finding stands, or independence is
|
|
101
|
-
not satisfied.
|
|
102
|
-
|
|
103
|
-
## Treat review as done
|
|
104
|
-
|
|
105
|
-
An adversarially-**accepted** `in_review` ticket equals `completed` for
|
|
106
|
-
dependents and flow. By default the ticket STAYS in the review lane — it is the
|
|
107
|
-
operator's deferred-QA queue — and is NOT auto-transitioned to `completed`.
|
|
108
|
-
`--close` is an OPTIONAL flag (operator QA sweep) and is omitted by the normal
|
|
109
|
-
loop:
|
|
110
|
-
|
|
111
|
-
```bash
|
|
112
|
-
.scripts/sentinel/bin/issue-autonomous-review.sh <ISSUE> <REPORT> --close
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
## Decision record
|
|
116
|
-
|
|
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.
|
|
122
|
-
|
|
123
|
-
## Downstream regression rollback
|
|
124
|
-
|
|
125
|
-
Deferring operator QA means a later dependent can prove a review-accepted
|
|
126
|
-
feature is ACTUALLY BROKEN. When that happens, move the accepted ticket back to
|
|
127
|
-
active (`started` if a worker takes it now, else `unstarted`) as a PREREQUISITE
|
|
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
|
|
132
|
-
for deferring operator QA, not a failure.
|
|
133
|
-
|
|
134
|
-
## Review report shape
|
|
135
|
-
|
|
136
|
-
Write `<ISSUE>.review.md`; the script validates it:
|
|
137
|
-
|
|
138
|
-
```markdown
|
|
139
|
-
# Autonomous Review Report: <ISSUE>
|
|
140
|
-
## Issue
|
|
141
|
-
- Linear/Plane/Trello issue: <ISSUE>
|
|
142
|
-
- Review lane reason:
|
|
143
|
-
## Reviewer
|
|
144
|
-
- Reviewer agent: <independent-agent-id>
|
|
145
|
-
- Independent of implementer: yes
|
|
146
|
-
## Locked Intent Baseline
|
|
147
|
-
- Acceptance criteria source:
|
|
148
|
-
- Milestone / horizon:
|
|
149
|
-
## Drift Assessment
|
|
150
|
-
- Drift assessment: none # none | minor | significant
|
|
151
|
-
## Adversarial Findings
|
|
152
|
-
- Critical/high findings: none
|
|
153
|
-
## Decision
|
|
154
|
-
- Decision: accept # accept | hold (legacy `close` tolerated)
|
|
155
|
-
```
|
|
156
|
-
|
|
157
|
-
## Operator override
|
|
158
|
-
|
|
159
|
-
`reconcile.auto_review: false` (role.yaml) or `RECONCILE_AUTO_REVIEW=off`
|
|
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.
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
# Sentinel workflow events
|
|
2
|
-
|
|
3
|
-
Status: Sentinel engine protocol (provider-agnostic)
|
|
4
|
-
|
|
5
|
-
## Purpose
|
|
6
|
-
|
|
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.
|
|
18
|
-
|
|
19
|
-
## Emitter
|
|
20
|
-
|
|
21
|
-
`bin/emit-event.py` stays as a dependency-free local emitter for a family a
|
|
22
|
-
future pass genuinely needs:
|
|
23
|
-
|
|
24
|
-
```bash
|
|
25
|
-
.scripts/sentinel/bin/emit-event.py <event_type> --field key=value [...]
|
|
26
|
-
```
|
|
27
|
-
|
|
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
DELETED
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
# Continuous ticket orchestration (sentinel engine)
|
|
2
|
-
|
|
3
|
-
Status: sentinel engine protocol (provider-agnostic)
|
|
4
|
-
|
|
5
|
-
## Invariant
|
|
6
|
-
|
|
7
|
-
If a ready ticket exists, exactly one implementation worker must be actively
|
|
8
|
-
moving it, or the sentinel records why none can. Prefer one live thread over
|
|
9
|
-
a quiet backlog. WIP limit: one active worker ticket.
|
|
10
|
-
|
|
11
|
-
The sentinel owns the watch loop. Workers (codex, opencode, copilot, …) own
|
|
12
|
-
implementation. The sentinel clears review-lane work via the autonomous
|
|
13
|
-
adversarial review (act, do not wait) protocol
|
|
14
|
-
(`autonomous-delegated-review.md`), and still does not write application code or
|
|
15
|
-
approve merges.
|
|
16
|
-
|
|
17
|
-
## Ticket access
|
|
18
|
-
|
|
19
|
-
All board access goes through the adapter (`tp`, from
|
|
20
|
-
`.scripts/lib/ticket-provider.sh`) and reasons in normalized states:
|
|
21
|
-
`backlog | unstarted | started | in_review | completed`. Never call the provider
|
|
22
|
-
directly — the engine is identical across Linear, Plane, and Trello.
|
|
23
|
-
|
|
24
|
-
## Work-state feed
|
|
25
|
-
|
|
26
|
-
Every heartbeat/pass keeps `runtime/continuous-ticket-sentinel-state.json`
|
|
27
|
-
current and machine-readable: `source`, `agent_id`, `repo`, `ticket_provider`,
|
|
28
|
-
`status` (`idle|checking|active|blocked|stalled|error`), `active_issue`,
|
|
29
|
-
`summary`, `reason`, `session`, `worktree`, `updated_at`, `last_activity_at`,
|
|
30
|
-
`log_path`.
|
|
31
|
-
|
|
32
|
-
## Source order (each pass)
|
|
33
|
-
|
|
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.
|
|
37
|
-
2. Local evidence under `_bmad-output/implementation-artifacts/issue-evidence/`.
|
|
38
|
-
3. Live worker state: zellij sessions, worktrees, branches, recent git.
|
|
39
|
-
|
|
40
|
-
When sources disagree, record a truth-check note and keep the issue open.
|
|
41
|
-
|
|
42
|
-
## Ticket selection (when no worker active)
|
|
43
|
-
|
|
44
|
-
1. A blocked/review ticket needing only agent-doable evidence repair.
|
|
45
|
-
2. An unblocked issue in the current milestone.
|
|
46
|
-
3. A small, high-priority backlog issue when the milestone has no ready ticket.
|
|
47
|
-
|
|
48
|
-
Move the chosen issue to `started` (`tp transition <id> started`) and create/
|
|
49
|
-
refresh its evidence file before spawning exactly one worker.
|
|
50
|
-
|
|
51
|
-
## Stop conditions
|
|
52
|
-
|
|
53
|
-
Stop without spawning only when: the board/evidence cannot be inspected; every
|
|
54
|
-
candidate is blocked by external evidence/credentials/product decisions (a
|
|
55
|
-
ticket blocked **only** on human review is NOT a stop condition — run it through
|
|
56
|
-
the independent adversarial review and act on the verdict immediately, no
|
|
57
|
-
waiting; and a dependent blocked **only** on a review-accepted feature is NOT
|
|
58
|
-
blocked); a worker is already active and healthy; or the next action needs
|
|
59
|
-
destructive git ops / production credentials / a paid action.
|
|
60
|
-
|
|
61
|
-
The loop never ends a pass with work parked waiting on the operator.
|
|
62
|
-
|
|
63
|
-
## Review and closure
|
|
64
|
-
|
|
65
|
-
1. Run ticket verification.
|
|
66
|
-
2. Run the close gate: `.scripts/sentinel/bin/issue-close-gate.sh <ISSUE>`.
|
|
67
|
-
3. Run the independent adversarial review — an adversarial microscope, with the
|
|
68
|
-
reviewer agent NOT the implementer (`autonomous-delegated-review.md`). On a
|
|
69
|
-
clean adversarial verdict the loop autonomously treats the ticket as done
|
|
70
|
-
(leaving it in the review lane as the operator's deferred-QA queue) and moves
|
|
71
|
-
on with no grace wait. A real finding holds it back to active.
|
|
72
|
-
4. Gate fail → leave open, record missing evidence.
|
|
73
|
-
5. Downstream regression rollback: if a later dependent proves a
|
|
74
|
-
review-accepted feature is actually broken, move it back to active as a
|
|
75
|
-
prerequisite of the dependent and record the rollback.
|
|
76
|
-
|
|
77
|
-
Board status is not proof. Repository evidence and the close gate are proof.
|
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
# {{ display_name }} — continuous ticket sentinel pass
|
|
2
|
-
|
|
3
|
-
Run one continuous ticket orchestration pass for the **{{ target_repo }}** repo.
|
|
4
|
-
A cheap systemd heartbeat already decided this full pass is needed.
|
|
5
|
-
|
|
6
|
-
Working repo: the git root containing this role at `agents/hermes/{{ role }}/`.
|
|
7
|
-
Ticket provider: **{{ ticket_provider }}** (reached only through the adapter — see below).
|
|
8
|
-
|
|
9
|
-
You are the **{{ target_repo }} PM**, running your continuous board-reconciliation
|
|
10
|
-
pass. Act autonomously, but stay inside the project contracts. Read
|
|
11
|
-
`agents/hermes/{{ role }}/SOUL.md` and the engine docs under
|
|
12
|
-
`.scripts/sentinel/docs/` before acting:
|
|
13
|
-
`continuous-ticket-orchestration.md`, `autonomous-delegated-review.md`,
|
|
14
|
-
`bloodbank-events.md`.
|
|
15
|
-
|
|
16
|
-
## Ticket access — adapter only
|
|
17
|
-
|
|
18
|
-
Never call {{ ticket_provider }} directly. Use the adapter:
|
|
19
|
-
|
|
20
|
-
```bash
|
|
21
|
-
.scripts/lib/ticket-provider.sh # defines tp(); source it, then:
|
|
22
|
-
tp active_milestone # JSON {id,name,state}
|
|
23
|
-
tp list_issues # JSON [{id,key,title,state,state_type,...}]
|
|
24
|
-
tp get_issue <id> # JSON incl. description + comments + attachments
|
|
25
|
-
tp comment <id> "<body>" # post a PM/review note
|
|
26
|
-
tp transition <id> <normalized-state> # backlog|unstarted|started|in_review|completed|cancelled
|
|
27
|
-
tp create_issue "<title>" "<desc>" # file a NEW ticket; --if-absent to dedupe by title
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
Reason in **normalized states**, not provider terms. This pass works identically
|
|
31
|
-
on Linear, Plane, or Trello.
|
|
32
|
-
|
|
33
|
-
## Pass
|
|
34
|
-
|
|
35
|
-
**Trigger.** This pass runs both on the cheap heartbeat timer AND on a live
|
|
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
|
-
`tp get_issue <that ticket>`, read the change, and react to it (triage / refine /
|
|
39
|
-
comment / transition per the lifecycle) before the general reconcile below.
|
|
40
|
-
|
|
41
|
-
1. Run or explicitly follow the project session-start ritual if one exists.
|
|
42
|
-
2. Reconcile: active milestone (`tp active_milestone`), issues (`tp list_issues`),
|
|
43
|
-
local evidence under `_bmad-output/implementation-artifacts/issue-evidence/`,
|
|
44
|
-
and live worker state (zellij sessions, worktrees, git).
|
|
45
|
-
3. If one worker ticket is `started` and healthy, monitor it and record state.
|
|
46
|
-
4. If no worker is active and a ready unblocked issue exists, delegate exactly
|
|
47
|
-
one worker (see the orchestration doc), move the issue to `started`, and
|
|
48
|
-
create/refresh its evidence file. Do not self-accept review here.
|
|
49
|
-
5. **Independent adversarial review — a NORMAL per-pass action.** For any
|
|
50
|
-
`in_review` ticket whose evidence is complete and for which an independent
|
|
51
|
-
reviewer exists (a reviewer agent id NOT equal to the implementer recorded in
|
|
52
|
-
the evidence, `Worker:` / `Implemented by:`), run the **independent
|
|
53
|
-
adversarial review** in `docs/autonomous-delegated-review.md`. Delegate to that
|
|
54
|
-
independent reviewer and have it run, as a rigorous adversarial review:
|
|
55
|
-
|
|
56
|
-
```bash
|
|
57
|
-
.scripts/sentinel/bin/issue-autonomous-review.sh <ISSUE> <REPORT>
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
(NO `--close`.) This is an adversarial microscope: it holds the work against
|
|
61
|
-
the locked-intent baseline (acceptance criteria, active milestone / horizon
|
|
62
|
-
model, product north star, locked product/architecture decisions), scores
|
|
63
|
-
drift `none | minor | significant` (`significant` → HOLD), surfaces any
|
|
64
|
-
unresolved critical/high finding (→ HOLD), enforces reviewer independence, and
|
|
65
|
-
the close gate (evidence completeness) stays a hard automated lock. Then ACT on
|
|
66
|
-
the verdict autonomously — do not wait on the operator:
|
|
67
|
-
- **Accepted** (drift `none|minor`, no unresolved critical/high findings,
|
|
68
|
-
independence satisfied, close gate passes): autonomously TREAT THE TICKET AS
|
|
69
|
-
DONE. Leave it in the review lane as the operator's deferred-QA queue (do NOT
|
|
70
|
-
auto-transition to `completed`; `--close` is an optional operator-QA-sweep
|
|
71
|
-
flag the loop omits). Record the adversarial review report and post ONE
|
|
72
|
-
ticket comment stating the autonomous acceptance with a
|
|
73
|
-
pointer to the report — never a "waiting on you" comment, no grace wait. A
|
|
74
|
-
dependent blocked only on this review-accepted feature is now unblocked.
|
|
75
|
-
- **Held** (a real finding or gate failure): the ticket goes back to active
|
|
76
|
-
(`started` if a worker takes it now, else `unstarted`); record the hold
|
|
77
|
-
reasons in the review report and a ticket comment.
|
|
78
|
-
6. **Regression rollback.** If a later dependent proves a review-accepted feature
|
|
79
|
-
is ACTUALLY BROKEN, move that feature back to active (`started` if a worker
|
|
80
|
-
takes it now, else `unstarted`) as a PREREQUISITE of the dependent; comment
|
|
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
|
|
83
|
-
until it is fixed. This is expected and healthy — it is the trade for deferring
|
|
84
|
-
operator QA, not a failure.
|
|
85
|
-
7. **External blockers only** (credentials, third-party access, paid actions,
|
|
86
|
-
undecided product decisions): record the blocker and wait on it. Do NOT
|
|
87
|
-
auto-review these.
|
|
88
|
-
8. **Anti-stall.** Never end a pass with work parked waiting on the operator's
|
|
89
|
-
approval or sign-off. "Looks good, waiting on the operator" is never a terminal
|
|
90
|
-
pass state. The only legitimate review-lane outcomes are: **accepted** (treat
|
|
91
|
-
as done, move on), **held** (back to active), or a genuine **out-of-scope
|
|
92
|
-
blocker** (recorded and waited on). There is no fourth "waiting for the
|
|
93
|
-
operator's sign-off" state.
|
|
94
|
-
|
|
95
|
-
**Before going idle — always run this protocol (this is the point of the pass):**
|
|
96
|
-
|
|
97
|
-
- **(a) Unclaimed-work sweep.** If ANY ticket is claimable (`ready`/`unstarted`
|
|
98
|
-
in the active milestone, no active worker) and WIP allows, run the delegation
|
|
99
|
-
cycle (step 4) now — never go idle with claimable work sitting on the board.
|
|
100
|
-
- **(b) In-progress status sweep.** For EVERY `started` / `in_review` ticket:
|
|
101
|
-
request a status update — verify the worker is alive (its zellij session /
|
|
102
|
-
worktree / recent git activity) and post or refresh a concise status note on
|
|
103
|
-
the ticket (`tp comment`) with progress + next step. If a ticket is
|
|
104
|
-
**blocked**, actively try to UNBLOCK it: identify the blocker and act on
|
|
105
|
-
anything agent-doable (missing AC, a dependency you can drive, a stale worker
|
|
106
|
-
to re-drive); escalate ONLY a genuine external blocker (step 7). A silent or
|
|
107
|
-
stalled worker is re-driven or flagged — never left silent.
|
|
108
|
-
|
|
109
|
-
9. Update `runtime/continuous-ticket-sentinel-state.json`: `active` /
|
|
110
|
-
`blocked` / `idle` / `stalled` with the required fields (`source`, `agent_id`,
|
|
111
|
-
`repo`, `ticket_provider`, `status`, `summary`, `reason`, `updated_at`,
|
|
112
|
-
`last_activity_at`, `log_path`).
|
|
113
|
-
10. Run or follow the session-end ritual; report board status, issues touched,
|
|
114
|
-
evidence touched, and the active worker issue or blocker.
|
|
115
|
-
|
|
116
|
-
Do not rely on a hard-coded seed ticket. Query the board every pass.
|
|
@@ -1,260 +0,0 @@
|
|
|
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)
|