@golden-frijoles/kit 0.7.0 → 0.9.0
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/dist/jev-eval.fixtures.json +7871 -0
- package/dist/jev-eval.mjs +369 -0
- package/dist/lib/config-registry.mjs +11 -1
- package/dist/lib/config.mjs +1 -1
- package/dist/lib/jev.mjs +10 -5
- package/dist/lib/notification-text.mjs +41 -0
- package/dist/lib/slack-text.mjs +21 -0
- package/dist/notify-setup.mjs +317 -0
- package/dist/routine-bootstrap.mjs +194 -0
- package/dist/routines/README.md +473 -0
- package/dist/routines/ops-nightly.prompt.md +125 -0
- package/dist/routines/pmo-report.prompt.md +52 -0
- package/dist/routines/pr-review.prompt.md +98 -0
- package/dist/routines/prod-smoke.prompt.md +140 -0
- package/dist/routines/roadmap-hygiene.prompt.md +97 -0
- package/dist/routines/smoke-triage.prompt.md +196 -0
- package/dist/routines/weekly-recap.prompt.md +95 -0
- package/dist/slack-notify.mjs +154 -0
- package/package.json +1 -1
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
weekly-recap.prompt.md — Routine weekly-recap (the weekly Telegram exec recap/retro), the Claude-
|
|
3
|
+
Routines prompt.
|
|
4
|
+
|
|
5
|
+
This is the prompt for a WEEKLY Claude Code *Routine* (cloud session, research preview) on the
|
|
6
|
+
monorepo-root repo (<root-repo>), running as the product owner — the longer-horizon complement to
|
|
7
|
+
the nightly `ops-nightly` standup. Added by Sprint 3 of the ops-routines-reporting epic. Mirrors
|
|
8
|
+
ops-nightly's shape (a load-bearing Telegram post is the routine's actual output, not an optional
|
|
9
|
+
failure-ping) but on a weekly cadence, and mirrors Routine C's precedent (a dedicated weekly routine,
|
|
10
|
+
not a day-of-week-gated step folded into a nightly one).
|
|
11
|
+
|
|
12
|
+
Reuse, don't rebuild (the `weekly-recap` skill comes from the `golden-frijoles` plugin, golden-frijoles
|
|
13
|
+
marketplace — invoke by name, not a repo-local
|
|
14
|
+
`skills/<name>/SKILL.md` path, which no longer exists in this repo):
|
|
15
|
+
- `weekly-recap` skill → scripts/weekly-recap.mjs (the gathering, message-building, and the
|
|
16
|
+
actual Telegram send + weekly-recaps.log commit — this routine just invokes it and reports back)
|
|
17
|
+
- gh CLI (every configured repo), git log -p on epic READMEs (status: SSOT), the same configured repo list
|
|
18
|
+
scripts/standup.mjs already uses.
|
|
19
|
+
|
|
20
|
+
Stand-up + guardrails: scripts/routines/README.md. Decision: made in the origin project — see this directory's README preamble. (skill conventions) and 09-platform-infra/ops-routines-reporting/
|
|
21
|
+
sprint-3.md (this story's scope).
|
|
22
|
+
|
|
23
|
+
The HTML comment above is not part of the prompt; a routine runs everything below the first `---`.
|
|
24
|
+
-->
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
You are a weekly **weekly-recap** Claude Code Routine on the root repo (`<root-repo>`),
|
|
29
|
+
running as the product owner. Your job is one step, then stop: post the week's executive recap to Telegram.
|
|
30
|
+
Everything you do is **advisory/observability only** — read-only aggregation plus one Telegram post and
|
|
31
|
+
one log-commit; you never merge, approve, block, or touch any repo's code.
|
|
32
|
+
|
|
33
|
+
## The one step — `weekly-recap`, written by YOU in three phases
|
|
34
|
+
|
|
35
|
+
The recap is no longer a tally: **you write it, as the CPO persona, and a mechanical guard checks
|
|
36
|
+
your draft before it posts.** Same rail as the nightly standup, one altitude higher.
|
|
37
|
+
|
|
38
|
+
**You must write the prose yourself, in this session. Do NOT call `devin`, `agy`, `codex`, or
|
|
39
|
+
`prose-draft.mjs`** — none of them is authenticated here, and reaching for one produces no report at
|
|
40
|
+
all rather than an obvious failure. This is the single most important line in this file.
|
|
41
|
+
|
|
42
|
+
Config/secrets first: the committed `reporting.config.json` (repos and signals) plus the chat id — from the
|
|
43
|
+
`TELEGRAM_CHAT_ID` env var in this unattended session (a gitignored `reporting.config.local.json` exists
|
|
44
|
+
only on a local machine, never here). If `TELEGRAM_CHAT_ID` is unset (and nothing is configured), that's a
|
|
45
|
+
hard stop — use the failure ping below; never `AskUserQuestion`, no interactive human is present.
|
|
46
|
+
`TELEGRAM_BOT_TOKEN` must be set.
|
|
47
|
+
|
|
48
|
+
**Phase 1 — get the brief.**
|
|
49
|
+
```
|
|
50
|
+
node scripts/weekly-recap.mjs --brief
|
|
51
|
+
```
|
|
52
|
+
It prints your persona, the accumulated lessons, and the week's evidence — **shipped and closed epics
|
|
53
|
+
first, with excerpts from their retrospectives**, then merged-PR volume as corroboration. It writes
|
|
54
|
+
nothing and sends nothing. **If it returns the "Nothing to report" brief**, do NOT write prose — run
|
|
55
|
+
`node scripts/weekly-recap.mjs --post --quiet` instead. It posts the one-line quiet-week message
|
|
56
|
+
**and advances the window log**; stopping without it leaves `windowEnd` unmoved and the next run
|
|
57
|
+
double-counts. Do not manufacture content.
|
|
58
|
+
|
|
59
|
+
**Phase 2 — write the prose.** Answer one question: *what is now possible that was not possible on
|
|
60
|
+
Monday, and what is still owed?* Group by **theme**, never by commit or by repository — a list of
|
|
61
|
+
merges is not a report, and the word budget is not permission to enumerate. Name the decision that
|
|
62
|
+
shaped the week if there was one. State what is owed; a weekly that quietly omits a known gap is the
|
|
63
|
+
one unforgivable error here. Save your draft to a temp file.
|
|
64
|
+
|
|
65
|
+
**Phase 3 — guard, then post.**
|
|
66
|
+
```
|
|
67
|
+
node scripts/weekly-recap.mjs --post --prose-file <your-file>
|
|
68
|
+
```
|
|
69
|
+
- **Exit 0** → posted.
|
|
70
|
+
- **Non-zero exit** → it prints a numbered revision note. Rewrite the draft in full against every
|
|
71
|
+
point, then re-run once with `--force-post`. A flagged-but-posted report beats a missing one, and
|
|
72
|
+
the label tells a human it did not converge.
|
|
73
|
+
- One revision, then post. Do not loop further.
|
|
74
|
+
|
|
75
|
+
Report the result afterwards (merged-PR / shipped-epic counts, whether the guard passed clean).
|
|
76
|
+
|
|
77
|
+
## Nothing else
|
|
78
|
+
No PR, no comment, no code change of your own — the Telegram post (plus the `scripts/weekly-recaps.log`
|
|
79
|
+
commit `weekly-recap.mjs` makes itself) is the routine's entire output. **Advisory only** — not a gate,
|
|
80
|
+
not a merge, not a status check. If the week was genuinely quiet (nothing merged, nothing shipped),
|
|
81
|
+
that's still a fully successful run — the script itself collapses to a one-line "quiet week" message; do
|
|
82
|
+
not manufacture extra content to pad it out.
|
|
83
|
+
|
|
84
|
+
## If the run can't complete (optional failure ping)
|
|
85
|
+
A healthy run reaches the product owner via the recap's Telegram post — no extra notice needed. But a run that
|
|
86
|
+
**fails to complete** (missing `TELEGRAM_BOT_TOKEN`, missing/unconfigured chat id, `gh` unauthenticated,
|
|
87
|
+
the script erroring out, the log push failing) would otherwise be silent. So, **only on a blocking
|
|
88
|
+
failure**, if **both** `TELEGRAM_BOT_TOKEN` and `TELEGRAM_CHAT_ID` are set in the environment,
|
|
89
|
+
best-effort POST a one-line alert:
|
|
90
|
+
`curl -s "https://api.telegram.org/bot$TELEGRAM_BOT_TOKEN/sendMessage" -d chat_id="$TELEGRAM_CHAT_ID" --data-urlencode text="⚠️ Routine weekly-recap failed: <one-line reason>"`
|
|
91
|
+
If either var is unset (or `api.telegram.org` isn't allow-listed), skip it silently — never block on it,
|
|
92
|
+
and **never** ping after a run that completed successfully, even a fully quiet one.
|
|
93
|
+
|
|
94
|
+
Note: `TELEGRAM_CHAT_ID` here is both the failure-ping target and what `weekly-recap.mjs` posts the recap
|
|
95
|
+
to — one variable, both call sites.
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// slack-notify.mjs — the CI/CD Slack ping, structured the same way as scripts/telegram-notify.mjs.
|
|
3
|
+
//
|
|
4
|
+
// Used by .github/workflows/notify-telegram.yml's push-notification and deploy-notification jobs,
|
|
5
|
+
// alongside (not instead of) the existing Telegram calls:
|
|
6
|
+
// node scripts/slack-notify.mjs --icon 📦 --sha "$GITHUB_SHA" --subject "$COMMIT_MSG" \
|
|
7
|
+
// --meta "by @$AUTHOR" --url "$DIFF_URL" --link-label "view diff"
|
|
8
|
+
//
|
|
9
|
+
// ── Why Incoming Webhook and not a bot token + chat.postMessage ────────────────────────────────
|
|
10
|
+
// Same reasoning this repo already applied to Telegram (no VERCEL_API_TOKEN when a push-based
|
|
11
|
+
// signal existed instead — see notify-telegram.yml's header): one fewer credential to mint, store
|
|
12
|
+
// and rotate. An Incoming Webhook URL is itself the channel-scoped credential — there is no
|
|
13
|
+
// separate bot token, channel ID, or OAuth scope to manage for this feature. If a future need
|
|
14
|
+
// requires posting to a channel chosen at runtime (rather than the one fixed at webhook-creation
|
|
15
|
+
// time), that is the point to move to `chat.postMessage` with a bot token — not before.
|
|
16
|
+
//
|
|
17
|
+
// ── Response handling is NOT the same shape as Telegram's — this is the one real gotcha ───────
|
|
18
|
+
// Telegram's sendMessage returns JSON ({ok: bool, ...}) regardless of outcome. Slack's Incoming
|
|
19
|
+
// Webhook endpoint does NOT: on success it returns the literal string "ok" (200, `text/plain`); on
|
|
20
|
+
// failure it returns a short plain-text error token (e.g. "invalid_payload", "channel_not_found",
|
|
21
|
+
// "no_service") with a non-200 status. Parsing the body as JSON here would throw on every single
|
|
22
|
+
// call, success or failure. So `send()` reads the body as text and treats HTTP status as the
|
|
23
|
+
// primary success signal — see Slack's own webhook error-handling docs if this ever needs
|
|
24
|
+
// re-verifying against a live change.
|
|
25
|
+
//
|
|
26
|
+
// ── Escaping is mrkdwn, not HTML, but the substitution table is identical ──────────────────────
|
|
27
|
+
// See scripts/lib/slack-text.mjs's header for why escapeMrkdwn and Telegram's escapeHtml do the
|
|
28
|
+
// same three replacements. Links use Slack's `<url|label>` syntax rather than `<a href>`; inline
|
|
29
|
+
// code uses single backticks rather than `<code>`.
|
|
30
|
+
//
|
|
31
|
+
// House style carried over unchanged: secrets only from GitHub Secrets, a 10s request timeout, and
|
|
32
|
+
// a repo with the secret unset gets a clean skip (not a failure) — see main() below.
|
|
33
|
+
|
|
34
|
+
import { escapeMrkdwn, escapeToFit, SLACK_LIMIT } from './lib/slack-text.mjs';
|
|
35
|
+
import { die, need } from './lib/cross-agent-cli.mjs';
|
|
36
|
+
import { resolve } from 'node:path';
|
|
37
|
+
import { fileURLToPath } from 'node:url';
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Assemble the Incoming Webhook payload.
|
|
41
|
+
*
|
|
42
|
+
* The subject is the only field given a length budget, same rule as Telegram's buildNotification:
|
|
43
|
+
* the sha, status, icon and link are structural, and truncating them to make room for a runaway
|
|
44
|
+
* commit message would discard exactly the parts a reader needs.
|
|
45
|
+
*
|
|
46
|
+
* Pure, and exported, so a test exercises the real assembler rather than a paraphrase of it.
|
|
47
|
+
*/
|
|
48
|
+
export function buildNotification({ repo, sha, icon, subject, meta, status, url, linkLabel }) {
|
|
49
|
+
const shortSha = String(sha ?? '').slice(0, 7);
|
|
50
|
+
|
|
51
|
+
// Chrome first, measured rather than estimated — same reasoning as Telegram's version: a
|
|
52
|
+
// hardcoded guess here is how a "safe" cap stops being safe once a line is added below.
|
|
53
|
+
const head = `${escapeMrkdwn(repo)} · ${icon} · \`${escapeMrkdwn(shortSha)}\``;
|
|
54
|
+
const tail = `<${escapeMrkdwn(url)}|${escapeMrkdwn(linkLabel)}>`;
|
|
55
|
+
const metaLine = meta ? escapeMrkdwn(meta) : null;
|
|
56
|
+
// `status` is our own literal ("✅ READY"), never external input — not escaped, same as Telegram's
|
|
57
|
+
// version, so the emoji and casing survive.
|
|
58
|
+
const statusLine = status || null;
|
|
59
|
+
|
|
60
|
+
const chrome = [head, metaLine, statusLine, tail].filter(Boolean).join('\n').length + 1;
|
|
61
|
+
|
|
62
|
+
// First line only: a commit body belongs behind the link, not in a notification.
|
|
63
|
+
const firstLine = String(subject ?? '').split('\n')[0];
|
|
64
|
+
const safeSubject = escapeToFit(firstLine, Math.max(1, SLACK_LIMIT - chrome - 16));
|
|
65
|
+
|
|
66
|
+
const lines = [head, safeSubject];
|
|
67
|
+
if (metaLine) lines.push(metaLine);
|
|
68
|
+
if (statusLine) lines.push(statusLine);
|
|
69
|
+
lines.push(tail);
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
text: lines.join('\n'),
|
|
73
|
+
// Belt-and-suspenders alongside Telegram's disable_web_page_preview: without this a commit or
|
|
74
|
+
// deployment link can unfurl into a big preview card that buries the next message.
|
|
75
|
+
unfurl_links: false,
|
|
76
|
+
unfurl_media: false,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* POST it. Returns { ok, status, body } — never throws.
|
|
82
|
+
*
|
|
83
|
+
* Slack's webhook response is PLAIN TEXT ("ok" on success, a short error token on failure), not
|
|
84
|
+
* JSON like Telegram's — see this file's header. An accepted webhook is the conjunction Slack
|
|
85
|
+
* documents: a 2xx response and the literal plain-text body `ok`.
|
|
86
|
+
*/
|
|
87
|
+
export async function send(webhookUrl, payload, fetchImpl = fetch) {
|
|
88
|
+
try {
|
|
89
|
+
const res = await fetchImpl(webhookUrl, {
|
|
90
|
+
method: 'POST',
|
|
91
|
+
headers: { 'Content-Type': 'application/json' },
|
|
92
|
+
body: JSON.stringify(payload),
|
|
93
|
+
signal: AbortSignal.timeout(10_000),
|
|
94
|
+
});
|
|
95
|
+
const body = await res.text().catch(() => '');
|
|
96
|
+
return { ok: res.ok && body.trim() === 'ok', status: res.status, body };
|
|
97
|
+
} catch (err) {
|
|
98
|
+
return { ok: false, status: 0, body: `request failed: ${err?.message ?? err}` };
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function main() {
|
|
103
|
+
const args = process.argv.slice(2);
|
|
104
|
+
const opt = {};
|
|
105
|
+
for (let i = 0; i < args.length; i++) {
|
|
106
|
+
const k = args[i];
|
|
107
|
+
if (!k.startsWith('--')) die(`unexpected argument ${k}`);
|
|
108
|
+
opt[k.slice(2)] = need(args[++i], k);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const webhookUrl = process.env.SLACK_WEBHOOK_URL?.trim();
|
|
112
|
+
if (!webhookUrl) {
|
|
113
|
+
// Same clean-skip stance as telegram-notify.mjs: a repo without the secret configured is a
|
|
114
|
+
// configuration state, not a failure.
|
|
115
|
+
process.stderr.write(
|
|
116
|
+
'SLACK_WEBHOOK_URL not set — skipping cleanly.\n' +
|
|
117
|
+
' Set it as a GitHub Secret on this repo to turn Slack notifications on.\n'
|
|
118
|
+
);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const payload = buildNotification({
|
|
123
|
+
repo: opt.repo ?? 'golden-beans',
|
|
124
|
+
sha: opt.sha ?? '',
|
|
125
|
+
icon: opt.icon ?? '📦',
|
|
126
|
+
subject: opt.subject ?? '',
|
|
127
|
+
meta: opt.meta ?? '',
|
|
128
|
+
status: opt.status ?? '',
|
|
129
|
+
url: opt.url ?? '',
|
|
130
|
+
linkLabel: opt['link-label'] ?? 'view',
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
if (payload.text.length > SLACK_LIMIT) {
|
|
134
|
+
// Should be unreachable — escapeToFit bounds the only unbounded field. Checked anyway, same
|
|
135
|
+
// stance as telegram-notify.mjs.
|
|
136
|
+
die(`assembled text is ${payload.text.length} chars, over the ${SLACK_LIMIT}-char budget`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const result = await send(webhookUrl, payload);
|
|
140
|
+
if (result.ok) {
|
|
141
|
+
process.stderr.write(`✓ ping delivered (${payload.text.length} chars)\n`);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Loud, same policy as Telegram: an observer workflow with a rejected message and no visible
|
|
146
|
+
// failure is a rail with no monitoring at all.
|
|
147
|
+
process.stderr.write(
|
|
148
|
+
`::error title=Slack ping NOT delivered::status=${result.status} body=${result.body}\n`
|
|
149
|
+
);
|
|
150
|
+
process.exitCode = 1;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const isMain = process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
154
|
+
if (isMain) main();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@golden-frijoles/kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "The scripts the Golden Frijoles skills run, packaged so they work in any repo: planning, build-order, reporting and verification rails. Zero dependencies.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|