@delorenj/pjangler 1.4.5 → 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 -520
  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 +5747 -13483
  10. package/dist/mcp-server.js +6853 -14059
  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 -513
  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
@@ -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.
@@ -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)