@delorenj/pjangler 1.4.6 → 1.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
@@ -1,373 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# Bind this agent to the repo's ONE ticket board.
|
|
3
|
-
#
|
|
4
|
-
# Source of truth is the repo-root .project.json `ticket_provider` block
|
|
5
|
-
# (written by the CommonProject base template). The model is:
|
|
6
|
-
# ONE board per repo — the PM owns it, and its heartbeat reconciliation pass
|
|
7
|
-
# watches it. So we never mint a per-agent, role-suffixed board ("Foo PM" /
|
|
8
|
-
# "Foo Sentinel"). Instead:
|
|
9
|
-
# 1. If .project.json already names a board -> BIND to it (no creation).
|
|
10
|
-
# 2. Otherwise (hermes run on a repo with no CommonProject board yet)
|
|
11
|
-
# -> create ONE repo-named board and write it back into .project.json so
|
|
12
|
-
# it becomes the SOT for every agent in this repo.
|
|
13
|
-
# Either way we register this agent under .project.json `agents` and mirror the
|
|
14
|
-
# binding into role.yaml for back-compat (80-registry.sh / 99-summary.sh).
|
|
15
|
-
|
|
16
|
-
# The caller's negative board grant is authoritative. Check it before sourcing
|
|
17
|
-
# _lib.sh so a skipped step cannot load fleet credentials, create logs/markers,
|
|
18
|
-
# inspect bindings, or reach a provider adapter.
|
|
19
|
-
if [[ "${SKIP_PLANE:-0}" == "1" ]]; then
|
|
20
|
-
printf '%s\n' '[42] ticket provider — SKIPPED (SKIP_PLANE=1)' >&2
|
|
21
|
-
exit 0
|
|
22
|
-
fi
|
|
23
|
-
|
|
24
|
-
# shellcheck source=_lib.sh
|
|
25
|
-
source "$(dirname "$0")/_lib.sh"
|
|
26
|
-
load_role_env
|
|
27
|
-
|
|
28
|
-
# Locate the repo-root .project.json (the SOT).
|
|
29
|
-
REPO_ROOT="$(project_repo_path 2>/dev/null || true)"
|
|
30
|
-
[ -n "$REPO_ROOT" ] || REPO_ROOT="$(cd "$ROLE_DIR/../../.." 2>/dev/null && pwd)"
|
|
31
|
-
PROJECT_JSON="$REPO_ROOT/.project.json"
|
|
32
|
-
ROLE_DIR_REL="${ROLE_DIR#"$REPO_ROOT"/}"
|
|
33
|
-
|
|
34
|
-
# Serialize the complete read / provider check-or-create / atomic write
|
|
35
|
-
# transaction per project. The lock lives outside the checkout so a normal
|
|
36
|
-
# provision cannot leave repository dirt behind.
|
|
37
|
-
command -v flock >/dev/null 2>&1 \
|
|
38
|
-
|| die "flock is required for safe ticket-provider binding"
|
|
39
|
-
PROJECT_LOCK_KEY="$(printf '%s' "$PROJECT_JSON" | sha256sum)"
|
|
40
|
-
PROJECT_LOCK_KEY="${PROJECT_LOCK_KEY%% *}"
|
|
41
|
-
PROJECT_LOCK_DIR="${XDG_RUNTIME_DIR:-${TMPDIR:-/tmp}}/pjangler-ticket-provider"
|
|
42
|
-
mkdir -p "$PROJECT_LOCK_DIR"
|
|
43
|
-
PROJECT_LOCK_FILE="$PROJECT_LOCK_DIR/$PROJECT_LOCK_KEY.lock"
|
|
44
|
-
[[ ! -L "$PROJECT_LOCK_FILE" ]] \
|
|
45
|
-
|| die "refusing ticket-provider lock symlink: $PROJECT_LOCK_FILE"
|
|
46
|
-
exec {PROJECT_LOCK_FD}>"$PROJECT_LOCK_FILE"
|
|
47
|
-
chmod 600 "$PROJECT_LOCK_FILE"
|
|
48
|
-
flock -w "${PROJECT_LOCK_TIMEOUT_SECONDS:-30}" "$PROJECT_LOCK_FD" \
|
|
49
|
-
|| die "timed out waiting for ticket-provider lock: $PROJECT_JSON"
|
|
50
|
-
project_lock_release() {
|
|
51
|
-
flock -u "$PROJECT_LOCK_FD" 2>/dev/null || true
|
|
52
|
-
exec {PROJECT_LOCK_FD}>&-
|
|
53
|
-
}
|
|
54
|
-
trap project_lock_release EXIT
|
|
55
|
-
|
|
56
|
-
[[ ! -L "$PROJECT_JSON" ]] || die "refusing .project.json symlink: $PROJECT_JSON"
|
|
57
|
-
if [[ -e "$PROJECT_JSON" ]]; then
|
|
58
|
-
python3 - "$PROJECT_JSON" <<'PY' \
|
|
59
|
-
|| die "malformed .project.json; refusing ticket-provider mutation or board creation"
|
|
60
|
-
import json
|
|
61
|
-
import pathlib
|
|
62
|
-
import sys
|
|
63
|
-
|
|
64
|
-
path = pathlib.Path(sys.argv[1])
|
|
65
|
-
try:
|
|
66
|
-
value = json.loads(path.read_text(encoding="utf-8"))
|
|
67
|
-
except (OSError, UnicodeError, json.JSONDecodeError):
|
|
68
|
-
raise SystemExit(1)
|
|
69
|
-
raise SystemExit(0 if isinstance(value, dict) else 1)
|
|
70
|
-
PY
|
|
71
|
-
fi
|
|
72
|
-
|
|
73
|
-
# shellcheck source=lib/ticket-provider.sh
|
|
74
|
-
source "$(dirname "$0")/lib/ticket-provider.sh"
|
|
75
|
-
|
|
76
|
-
already_done 42-ticket-provider \
|
|
77
|
-
&& log "[42] ticket-provider marker found — revalidating canonical board binding"
|
|
78
|
-
|
|
79
|
-
# pj <dotted.key> — read a string value from .project.json (empty if absent).
|
|
80
|
-
pj() {
|
|
81
|
-
[ -f "$PROJECT_JSON" ] || { printf ''; return 0; }
|
|
82
|
-
python3 - "$PROJECT_JSON" "$1" <<'PY'
|
|
83
|
-
import sys, json, pathlib
|
|
84
|
-
d = json.loads(pathlib.Path(sys.argv[1]).read_text(encoding="utf-8"))
|
|
85
|
-
if not isinstance(d, dict):
|
|
86
|
-
raise SystemExit(".project.json root must be an object")
|
|
87
|
-
cur = d
|
|
88
|
-
for k in sys.argv[2].split("."):
|
|
89
|
-
if isinstance(cur, dict) and k in cur:
|
|
90
|
-
cur = cur[k]
|
|
91
|
-
else:
|
|
92
|
-
print(""); raise SystemExit(0)
|
|
93
|
-
print(cur if isinstance(cur, str) else "")
|
|
94
|
-
PY
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
# pj_write — merge board binding (optional) + this agent into .project.json.
|
|
98
|
-
# args: set_provider(0|1) provider board_id workspace identifier team
|
|
99
|
-
# identifier_source
|
|
100
|
-
#
|
|
101
|
-
# identifier_source is REQUIRED whenever identifier is non-empty and must be
|
|
102
|
-
# `provider` (read back out of the provider's own response) or `proposed` (a
|
|
103
|
-
# local prefix the provider never confirmed, e.g. the string Trello echoes back
|
|
104
|
-
# because Trello mints no key). There is deliberately no third option and no
|
|
105
|
-
# default: an unstamped identifier is refused outright, so no locally-computed
|
|
106
|
-
# string can ever land in .project.json wearing the authority of a live board.
|
|
107
|
-
#
|
|
108
|
-
# `provider` is further refused outright for a provider that assigns no
|
|
109
|
-
# identifiers at all. Trello cannot have named a key it has no concept of, so
|
|
110
|
-
# the claim is not merely unproven, it is impossible — and a downstream reader
|
|
111
|
-
# must always be able to tell "Plane assigned PJAN" from "we picked INT and
|
|
112
|
-
# Trello confirmed a board exists".
|
|
113
|
-
#
|
|
114
|
-
# Those are two different facts, so they get two different fields. Whether the
|
|
115
|
-
# BOARD is real is stamped in board_confirmed_at, and that is what a link rests
|
|
116
|
-
# on; whether the KEY came from the provider is identifier_source. Every
|
|
117
|
-
# provider can answer the first. Only some can answer the second.
|
|
118
|
-
pj_write() {
|
|
119
|
-
REPO="$REPO" REPO_ROOT="$REPO_ROOT" AGENT_ID="$AGENT_ID" ROLE="$ROLE" \
|
|
120
|
-
ROLE_DIR_REL="$ROLE_DIR_REL" PROJECT_DESC="${PROJECT_DESC:-}" \
|
|
121
|
-
python3 - "$PROJECT_JSON" "$@" <<'PY'
|
|
122
|
-
import datetime
|
|
123
|
-
import errno
|
|
124
|
-
import sys, os, json, pathlib, stat, tempfile
|
|
125
|
-
(path, set_provider, provider, board_id, workspace, identifier, team,
|
|
126
|
-
identifier_source) = sys.argv[1:9]
|
|
127
|
-
# Providers that mint their own keys and hand them back on read.
|
|
128
|
-
IDENTIFIER_ASSIGNING = ("plane", "linear")
|
|
129
|
-
if identifier and identifier_source not in ("provider", "proposed"):
|
|
130
|
-
raise SystemExit(
|
|
131
|
-
"refusing to persist ticket_provider.identifier %r with source %r: "
|
|
132
|
-
"every identifier must be stamped provider|proposed" % (identifier, identifier_source)
|
|
133
|
-
)
|
|
134
|
-
if identifier_source == "provider" and provider not in IDENTIFIER_ASSIGNING:
|
|
135
|
-
raise SystemExit(
|
|
136
|
-
"refusing to persist ticket_provider.identifier %r as provider-sourced: "
|
|
137
|
-
"%s assigns no identifiers, so it cannot have named one" % (identifier, provider)
|
|
138
|
-
)
|
|
139
|
-
now = (
|
|
140
|
-
datetime.datetime.now(datetime.timezone.utc)
|
|
141
|
-
.isoformat(timespec="milliseconds")
|
|
142
|
-
.replace("+00:00", "Z")
|
|
143
|
-
)
|
|
144
|
-
p = pathlib.Path(path)
|
|
145
|
-
if p.is_symlink():
|
|
146
|
-
raise SystemExit(f"refusing .project.json symlink: {p}")
|
|
147
|
-
if p.exists():
|
|
148
|
-
try:
|
|
149
|
-
d = json.loads(p.read_text(encoding="utf-8"))
|
|
150
|
-
except (OSError, UnicodeError, json.JSONDecodeError) as exc:
|
|
151
|
-
raise SystemExit(f"malformed .project.json: {type(exc).__name__}")
|
|
152
|
-
if not isinstance(d, dict):
|
|
153
|
-
raise SystemExit(".project.json root must be an object")
|
|
154
|
-
else:
|
|
155
|
-
d = {}
|
|
156
|
-
repo = os.environ.get("REPO", "")
|
|
157
|
-
d.setdefault("project_name", repo)
|
|
158
|
-
d.setdefault("project_slug", repo)
|
|
159
|
-
if not d.get("repo_path"):
|
|
160
|
-
d["repo_path"] = os.environ.get("REPO_ROOT", "")
|
|
161
|
-
if set_provider == "1":
|
|
162
|
-
tp = d.setdefault("ticket_provider", {})
|
|
163
|
-
tp["type"] = provider
|
|
164
|
-
if workspace: tp["workspace"] = workspace
|
|
165
|
-
if identifier:
|
|
166
|
-
tp["identifier"] = identifier
|
|
167
|
-
tp["identifier_source"] = identifier_source
|
|
168
|
-
if identifier_source == "provider":
|
|
169
|
-
tp["identifier_fetched_at"] = now
|
|
170
|
-
else:
|
|
171
|
-
tp.pop("identifier_fetched_at", None)
|
|
172
|
-
if board_id:
|
|
173
|
-
tp["board_id"] = board_id
|
|
174
|
-
# Every caller that reaches here with a board id got it out of the
|
|
175
|
-
# provider itself (create_board or resolve), so this write IS the
|
|
176
|
-
# confirmation and may stamp its own instant.
|
|
177
|
-
tp["board_confirmed_at"] = now
|
|
178
|
-
if team: tp["team"] = team
|
|
179
|
-
tp.pop("board_url", None)
|
|
180
|
-
tp["state"] = "linked" if board_id else "deferred"
|
|
181
|
-
ag = d.setdefault("agents", {})
|
|
182
|
-
entry = ag.get(os.environ["AGENT_ID"], {})
|
|
183
|
-
if not isinstance(entry, dict):
|
|
184
|
-
entry = {}
|
|
185
|
-
entry.update({
|
|
186
|
-
"role": os.environ["ROLE"],
|
|
187
|
-
"role_dir": os.environ["ROLE_DIR_REL"],
|
|
188
|
-
"provisioning_state": "linked" if board_id else "deferred",
|
|
189
|
-
})
|
|
190
|
-
ag[os.environ["AGENT_ID"]] = entry
|
|
191
|
-
rendered = json.dumps(d, indent=2) + "\n"
|
|
192
|
-
p.parent.mkdir(parents=True, exist_ok=True)
|
|
193
|
-
mode = stat.S_IMODE(p.stat().st_mode) if p.exists() else 0o644
|
|
194
|
-
fd, temporary = tempfile.mkstemp(prefix=f".{p.name}.ticket-provider-", dir=p.parent)
|
|
195
|
-
try:
|
|
196
|
-
os.fchmod(fd, mode)
|
|
197
|
-
with os.fdopen(fd, "w", encoding="utf-8") as handle:
|
|
198
|
-
handle.write(rendered)
|
|
199
|
-
handle.flush()
|
|
200
|
-
os.fsync(handle.fileno())
|
|
201
|
-
os.replace(temporary, p)
|
|
202
|
-
unsupported = {errno.EINVAL, getattr(errno, "ENOTSUP", errno.EINVAL), errno.ENOSYS}
|
|
203
|
-
directory_fd = os.open(p.parent, os.O_RDONLY | getattr(os, "O_DIRECTORY", 0))
|
|
204
|
-
try:
|
|
205
|
-
try:
|
|
206
|
-
os.fsync(directory_fd)
|
|
207
|
-
except OSError as exc:
|
|
208
|
-
if exc.errno not in unsupported:
|
|
209
|
-
raise
|
|
210
|
-
finally:
|
|
211
|
-
os.close(directory_fd)
|
|
212
|
-
except BaseException:
|
|
213
|
-
try:
|
|
214
|
-
os.unlink(temporary)
|
|
215
|
-
except FileNotFoundError:
|
|
216
|
-
pass
|
|
217
|
-
raise
|
|
218
|
-
PY
|
|
219
|
-
log " .project.json updated (agent=$AGENT_ID)"
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
# Mirror the binding into role.yaml so legacy consumers keep working.
|
|
223
|
-
mirror_to_role_yaml() {
|
|
224
|
-
# mirror_to_role_yaml <provider> <board_id> <workspace> <identifier> <team>
|
|
225
|
-
local provider="$1" bid="$2" ws="$3" ident="$4" team="$5"
|
|
226
|
-
yaml_set ticket_provider.name "$provider" 2>/dev/null || true
|
|
227
|
-
[ -n "$bid" ] && yaml_set ticket_provider.board_id "$bid" 2>/dev/null || true
|
|
228
|
-
case "$provider" in
|
|
229
|
-
plane)
|
|
230
|
-
[ -n "$bid" ] && echo "$bid" > "$ROLE_DIR/.scripts/.plane-project-id"
|
|
231
|
-
[ -n "$ws" ] && yaml_set ticket_provider.workspace "$ws" 2>/dev/null || true
|
|
232
|
-
[ -n "$bid" ] && yaml_set ticket_provider.project "$bid" 2>/dev/null || true
|
|
233
|
-
[ -n "$ws" ] && yaml_set plane.workspace "$ws" 2>/dev/null || true
|
|
234
|
-
[ -n "$ident" ] && yaml_set plane.identifier "$ident" 2>/dev/null || true
|
|
235
|
-
;;
|
|
236
|
-
trello)
|
|
237
|
-
[ -n "$bid" ] && yaml_set ticket_provider.board "$bid" 2>/dev/null || true
|
|
238
|
-
;;
|
|
239
|
-
linear)
|
|
240
|
-
[ -n "$team" ] && yaml_set ticket_provider.team "$team" 2>/dev/null || true
|
|
241
|
-
;;
|
|
242
|
-
esac
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
# ── Provider resolution ──────────────────────────────────────────────────
|
|
246
|
-
# An existing repo board (in .project.json) wins — every agent binds to it.
|
|
247
|
-
SOT_TYPE="$(pj ticket_provider.type)"
|
|
248
|
-
SOT_BOARD_ID="$(pj ticket_provider.board_id)"
|
|
249
|
-
SOT_WS="$(pj ticket_provider.workspace)"
|
|
250
|
-
SOT_IDENT="$(pj ticket_provider.identifier)"
|
|
251
|
-
SOT_IDENT_SOURCE="$(pj ticket_provider.identifier_source)"
|
|
252
|
-
SOT_TEAM="$(pj ticket_provider.team)"
|
|
253
|
-
|
|
254
|
-
# role.yaml provider comes from copier --data (the operator's pjangler choice).
|
|
255
|
-
ROLE_PROVIDER="$(yaml_get ticket_provider.name)"
|
|
256
|
-
|
|
257
|
-
if [ -n "$SOT_BOARD_ID" ]; then
|
|
258
|
-
# ── BIND to the repo's existing board ──────────────────────────────────
|
|
259
|
-
PROVIDER="${SOT_TYPE:-${ROLE_PROVIDER:-plane}}"
|
|
260
|
-
if [ -n "$ROLE_PROVIDER" ] && [ "$ROLE_PROVIDER" != "$PROVIDER" ]; then
|
|
261
|
-
warn "[42] requested provider '$ROLE_PROVIDER' but repo board is '$PROVIDER' (.project.json wins); binding to existing board"
|
|
262
|
-
fi
|
|
263
|
-
log "[42] binding $AGENT_ID to existing repo board (provider=$PROVIDER, id=$SOT_BOARD_ID)"
|
|
264
|
-
LIVE_IDENT="$SOT_IDENT"
|
|
265
|
-
# An identifier already sitting in .project.json is only as trustworthy as
|
|
266
|
-
# the source recorded beside it. A record written before sources existed
|
|
267
|
-
# carries none, and an unsourced identifier is a proposal — not a fact.
|
|
268
|
-
IDENT_SOURCE=""
|
|
269
|
-
[ -z "$LIVE_IDENT" ] || IDENT_SOURCE="${SOT_IDENT_SOURCE:-proposed}"
|
|
270
|
-
# …and a `provider` stamp beside a provider that assigns no identifiers is
|
|
271
|
-
# not evidence either. It is a stale claim from a writer that conflated
|
|
272
|
-
# "the board is confirmed" with "the provider named this key". Correct it
|
|
273
|
-
# on the way through rather than carrying the lie into another manifest.
|
|
274
|
-
case "$PROVIDER" in
|
|
275
|
-
plane|linear) ;;
|
|
276
|
-
*) if [ "$IDENT_SOURCE" = provider ]; then IDENT_SOURCE=proposed; fi ;;
|
|
277
|
-
esac
|
|
278
|
-
if [ "$PROVIDER" = plane ]; then
|
|
279
|
-
OUT="$(tp resolve)" || die "existing Plane board could not be validated"
|
|
280
|
-
LIVE_IDENT="$(printf '%s' "$OUT" | python3 -c 'import sys,json
|
|
281
|
-
try: print(str(json.load(sys.stdin).get("identifier") or ""))
|
|
282
|
-
except Exception: print("")')"
|
|
283
|
-
[ -n "$LIVE_IDENT" ] || die "existing Plane board has no authoritative live identifier"
|
|
284
|
-
IDENT_SOURCE=provider
|
|
285
|
-
fi
|
|
286
|
-
mirror_to_role_yaml "$PROVIDER" "$SOT_BOARD_ID" "$SOT_WS" "$LIVE_IDENT" "$SOT_TEAM"
|
|
287
|
-
pj_write 1 "$PROVIDER" "$SOT_BOARD_ID" "$SOT_WS" "$LIVE_IDENT" "$SOT_TEAM" "$IDENT_SOURCE"
|
|
288
|
-
mark_done 42-ticket-provider
|
|
289
|
-
exit 0
|
|
290
|
-
fi
|
|
291
|
-
|
|
292
|
-
# ── No repo board yet: create ONE repo-named board (no role suffix) ───────
|
|
293
|
-
PROVIDER="${ROLE_PROVIDER:-${SOT_TYPE:-plane}}"
|
|
294
|
-
log "[42] no board in .project.json — bootstrapping a repo board (provider: $PROVIDER)"
|
|
295
|
-
|
|
296
|
-
# Identifier PROPOSAL for a brand-new board only. It is sent to the provider in
|
|
297
|
-
# the create request and is NEVER persisted as though it were confirmed: every
|
|
298
|
-
# write below uses LIVE_IDENT, taken from the provider's own response, carrying
|
|
299
|
-
# the source that response earns it (`provider` where the provider mints the
|
|
300
|
-
# identifier, `proposed` where — as with Trello — it only echoes ours back).
|
|
301
|
-
PROPOSED_RAW=$(printf '%s' "$REPO" | tr -cd '[:alnum:]' | tr '[:lower:]' '[:upper:]')
|
|
302
|
-
while [ ${#PROPOSED_RAW} -lt 2 ]; do PROPOSED_RAW="${PROPOSED_RAW}X"; done
|
|
303
|
-
PROPOSED_IDENT="${SOT_IDENT:-${PROPOSED_RAW:0:4}}"
|
|
304
|
-
# Board NAME = repo name, separators->space, title-cased. NOT display_name —
|
|
305
|
-
# display_name carries the role suffix and must never become the board name.
|
|
306
|
-
NAME="$(printf '%s' "$REPO" | tr '_-' ' ' | python3 -c 'import sys; print(" ".join(w[:1].upper()+w[1:] for w in sys.stdin.read().split()))')"
|
|
307
|
-
DESC="Ticket board for $REPO"
|
|
308
|
-
|
|
309
|
-
case "$PROVIDER" in
|
|
310
|
-
linear)
|
|
311
|
-
# Linear DOES mint an authoritative identifier — the team key — so nothing
|
|
312
|
-
# here ever persists the local proposal. Without a live team read there is
|
|
313
|
-
# no identifier to record, and the record simply carries none.
|
|
314
|
-
if [[ -z "${LINEAR_API_KEY:-}" ]]; then
|
|
315
|
-
warn "[42] LINEAR_API_KEY not set; set role.yaml/.project.json ticket_provider.team and re-run ./.scripts/42-ticket-provider.sh"
|
|
316
|
-
pj_write 1 linear "" "" "" "$SOT_TEAM" ""
|
|
317
|
-
mark_done 42-ticket-provider; exit 0
|
|
318
|
-
fi
|
|
319
|
-
OUT="$(tp resolve 2>/dev/null || true)"
|
|
320
|
-
BID="$(printf '%s' "$OUT" | python3 -c 'import sys,json
|
|
321
|
-
try: print(json.load(sys.stdin).get("board_id",""))
|
|
322
|
-
except Exception: print("")')"
|
|
323
|
-
LIVE_IDENT="$(printf '%s' "$OUT" | python3 -c 'import sys,json
|
|
324
|
-
try: print(str(json.load(sys.stdin).get("identifier") or ""))
|
|
325
|
-
except Exception: print("")')"
|
|
326
|
-
if [ -n "$BID" ]; then
|
|
327
|
-
[ -n "$LIVE_IDENT" ] || die "linear resolved a team with no authoritative key"
|
|
328
|
-
mirror_to_role_yaml linear "$BID" "" "$LIVE_IDENT" "$SOT_TEAM"
|
|
329
|
-
pj_write 1 linear "$BID" "" "$LIVE_IDENT" "$SOT_TEAM" provider
|
|
330
|
-
else
|
|
331
|
-
warn "[42] linear resolve returned no board; set ticket_provider.team and re-run"
|
|
332
|
-
pj_write 1 linear "" "" "" "$SOT_TEAM" ""
|
|
333
|
-
fi
|
|
334
|
-
;;
|
|
335
|
-
|
|
336
|
-
plane|trello)
|
|
337
|
-
KEYVAR=PLANE_API_KEY; [ "$PROVIDER" = trello ] && KEYVAR=TRELLO_KEY
|
|
338
|
-
if [[ -z "${!KEYVAR:-}" ]]; then
|
|
339
|
-
warn "[42] $KEYVAR not set; skipping board creation. Set creds and re-run ./.scripts/42-ticket-provider.sh"
|
|
340
|
-
# Deferred: no board was created, so there is no confirmed identifier.
|
|
341
|
-
# Persist nothing rather than freezing the proposal into .project.json.
|
|
342
|
-
pj_write 1 "$PROVIDER" "" "${SOT_WS:-$PLANE_WORKSPACE}" "" "" ""
|
|
343
|
-
mark_done 42-ticket-provider; exit 0
|
|
344
|
-
fi
|
|
345
|
-
OUT="$(tp create_board "$NAME" "$PROPOSED_IDENT" "$DESC")" || die "create_board failed for $PROVIDER"
|
|
346
|
-
BID="$(printf '%s' "$OUT" | python3 -c 'import sys,json; print(json.load(sys.stdin).get("board_id",""))')"
|
|
347
|
-
LIVE_IDENT="$(printf '%s' "$OUT" | python3 -c 'import sys,json; print(json.load(sys.stdin).get("identifier","") or "")')"
|
|
348
|
-
WS="${SOT_WS:-$PLANE_WORKSPACE}"
|
|
349
|
-
[ "$PROVIDER" = trello ] && WS=""
|
|
350
|
-
case "$PROVIDER" in
|
|
351
|
-
plane)
|
|
352
|
-
# Plane mints the identifier and hands it back on create; anything else
|
|
353
|
-
# is a guess and must not be written.
|
|
354
|
-
[ -n "$LIVE_IDENT" ] \
|
|
355
|
-
|| die "created/bound Plane board has no authoritative live identifier"
|
|
356
|
-
IDENT_SOURCE=provider
|
|
357
|
-
;;
|
|
358
|
-
trello)
|
|
359
|
-
# Trello mints no key. create_board echoes back the prefix it was
|
|
360
|
-
# handed, which confirms what the board is BOUND under — not a value
|
|
361
|
-
# Trello chose. Record it stamped as the proposal it is, so nothing
|
|
362
|
-
# downstream can read it as provider truth.
|
|
363
|
-
IDENT_SOURCE=proposed
|
|
364
|
-
;;
|
|
365
|
-
esac
|
|
366
|
-
mirror_to_role_yaml "$PROVIDER" "$BID" "$WS" "$LIVE_IDENT" ""
|
|
367
|
-
pj_write 1 "$PROVIDER" "$BID" "$WS" "$LIVE_IDENT" "" "$IDENT_SOURCE"
|
|
368
|
-
;;
|
|
369
|
-
|
|
370
|
-
*) die "unknown ticket provider: $PROVIDER (expected linear|plane|trello)" ;;
|
|
371
|
-
esac
|
|
372
|
-
|
|
373
|
-
mark_done 42-ticket-provider
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# Compatibility checkpoint for the fleet-shared Bloodbank gateway contract.
|
|
3
|
-
# shellcheck source=_lib.sh
|
|
4
|
-
source "$(dirname "$0")/_lib.sh"
|
|
5
|
-
load_role_env
|
|
6
|
-
|
|
7
|
-
already_done 60-bloodbank && { log "[60] bloodbank fleet-routing checkpoint already complete — skipping"; exit 0; }
|
|
8
|
-
|
|
9
|
-
# SKIP_BLOODBANK used to suppress per-profile consumer installation. Keep the
|
|
10
|
-
# flag harmless for existing unattended provisioning commands; there is no
|
|
11
|
-
# profile-local process, dependency, broker probe, or file to skip anymore.
|
|
12
|
-
if [[ "${SKIP_BLOODBANK:-0}" == "1" ]]; then
|
|
13
|
-
log "[60] bloodbank — SKIP_BLOODBANK accepted as a compatibility no-op"
|
|
14
|
-
else
|
|
15
|
-
log "[60] bloodbank — fleet-shared gateway routes target_agent_id=$AGENT_ID"
|
|
16
|
-
fi
|
|
17
|
-
|
|
18
|
-
mark_done 60-bloodbank
|