Skip to content

Commit 09a0f8d

Browse files
pugstroclaude
andcommitted
feat(forge): form-based intake, document workflow, and local release-QA gate
- src/slack/forms/: guided intake forms (draft + metrics) with delivery and prompt scaffolding, wired through new forms listener - document workflow: router/pipeline/provenance improvements across src/forge/ and src/slack/workflows/, grounding lint and preflight-repair refinements - scripts/release-qa.mjs: single local gate chaining typst pin, template sync, vendored packages, lint, script syntax, format, build, and tests - templates: kpi_report + project_status visual polish, goldens regenerated - tests: new coverage for forms, document workflow, provenance, action status; existing suites updated (73 files / 546 tests green) - docs: CLAUDE.md project notes + AGENTS.md symlink for agent tooling Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 6e59be6 commit 09a0f8d

74 files changed

Lines changed: 2904 additions & 593 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎AGENTS.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
CLAUDE.md

‎CLAUDE.md‎

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# typstmcp — codebase notes for Claude
2+
3+
## What this is
4+
5+
Two products in one repo (see [README.md](./README.md) for the full picture):
6+
7+
- **DocForge** — an MCP server (stdio) for agent-driven PDF production via Typst.
8+
Engine spec: [DOCFORGE.md](./DOCFORGE.md). Entry point `src/index.ts` → `dist/index.js`.
9+
- **Forge** — a Slack agent (Bolt) that drives DocForge **over MCP as a stdio child
10+
process**, not in-process (`src/forge/mcp-client.ts`; `FORGE_MCP=off` forces the
11+
in-process fallback). Product spec: [FORGE.md](./FORGE.md). Entry `dist/slack/app.js`.
12+
13+
Other docs: [AUDIT.md](./AUDIT.md) (security/correctness audit log),
14+
[HARDENING.md](./HARDENING.md), [DELIVERY.md](./DELIVERY.md),
15+
`.cursor/skills/docforge/SKILL.md` (agent-facing tool usage guide).
16+
17+
## Stack
18+
19+
- TypeScript, `strict`, **NodeNext ESM** — relative imports need the `.js` suffix.
20+
- Node ≥ 20. Runtime deps: `@modelcontextprotocol/sdk`, `@slack/bolt`, `ajv`, `zod`.
21+
- **Typst 0.14.2 pinned** (`TYPST_VERSION_PIN` in `src/config.ts`); the `typst` binary
22+
must be on PATH. `@preview` packages are vendored in `vendor/typst-packages/` so
23+
compiles need no network.
24+
- vitest, eslint (flat config + typescript-eslint), prettier (enforced in CI-style
25+
via `format:check`).
26+
27+
## Commands (verified 2026-07-13)
28+
29+
```bash
30+
npm run build # rm -rf dist && tsc — clean
31+
npm test # vitest run — 73 files / 546 tests, ~10 s wall
32+
npm run lint # eslint — clean
33+
npm run format:check # prettier — clean (use `npm run format` to fix)
34+
npm run check:typst # asserts local typst matches the 0.14.2 pin
35+
npm run check:packages # asserts vendored @preview packages present
36+
```
37+
38+
Heavier gates (documented in README, **not** run during bootstrap):
39+
`npm run release:qa` (full local release gate), `npm run test:visual` (golden-PNG
40+
regression over 24 templates), `npm run forge:smoke`. Slack dev loop: `npm run slack`
41+
(needs `.env` from `.env.sample` with bot + app tokens).
42+
43+
## Layout
44+
45+
- `src/` — engine (`compile/`, `templates/`, `validation/`, `qa/`, `repair/`, `lint/`,
46+
`sandbox/`, `security/`, `versioning/`), Slack agent (`slack/`), the Slack→engine
47+
bridge (`forge/`), transcript→template-data mappers (`workflow-mappers/`).
48+
- `templates/<id>/` — 21 built-in Typst templates, each with `main.typ`,
49+
`components.typ`, `theme.typ`, a JSON schema/README/sample, and a
50+
`golden-page1.png` for visual regression. 3 more live in `marketplace/`.
51+
- `tests/` — flat `*.test.ts` files, vitest `describe`/`it`, shared helpers in
52+
`tests/helpers.ts` (e.g. `templateDir()`). Tests import from `../src/**/*.js`
53+
(compiled-style specifiers, resolved by vitest).
54+
- `scripts/*.mjs` — release/QA/dev tooling; syntax-checked by `npm run check:scripts`.
55+
56+
## Conventions & quirks
57+
58+
- Prettier is the formatter of record — run `npm run format` before committing;
59+
`format:check` covers src, tests, scripts, key markdown, and Slack manifests.
60+
- Golden PNGs (`templates/*/golden-page1.png`) are committed binaries; template
61+
visual changes require regenerating them (`npm run compile:golden`).
62+
- The package publishes `dist/` plus templates/vendor/docs (see `files` in
63+
package.json); `bin` is `docforge-mcp`.
64+
- Docker image (Dockerfile) bundles Node 22 + pinned Typst for offline compiles.

‎DELIVERY.md‎

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ Monorepo: Slack agent + DocForge engine.
111111
| Reviewers invited | ☐ |
112112
| Pinned `#general` instructions | ☐ |
113113
| `forge:smoke` passes in CI/local | ☐ |
114+
| `npm run release:qa` passed within 48h | ☐ |
114115
| Cold QA script (§8) passed | ☐ |
115116
| HTTP deploy up through Aug 6 | ☐ |
116117

@@ -125,6 +126,30 @@ Monorepo: Slack agent + DocForge engine.
125126
| Impact | Universal workflows (sales, SRE, leadership) |
126127
| Idea | Workflow completion vs summarize bots |
127128

129+
### Current verified evidence (2026-06-26)
130+
131+
- `npm run release:qa` passed locally: Typst pin, template sync, vendored packages,
132+
lint, script syntax, format, build, 545 tests, Forge smoke, visual regression,
133+
Slack manifest validation, package inventory, and tracked-file secret scan.
134+
- Live sandbox workspace: `Workspace Sand` (`T0BA8FFE63T`), bot `forge`.
135+
- Live status PDF generated from `#campaign-nto` and uploaded to Slack:
136+
`Status-campaign-nto-hero-polished.pdf` (`F0BDJAYP2JG`). Local QA copy:
137+
`/tmp/forge-live-slack-qa/Status-campaign-nto-hero-polished.pdf`.
138+
- The status PDF render is 2 pages with 35 source snippets, zero lint errors,
139+
zero preflight warnings, clean emoji shortcode handling, and a visible Forge
140+
grounding proof/evidence ledger.
141+
- Reviewer seed/poll flow was executed against existing sandbox channels:
142+
`#service-swarm`, `#acct-edge`, `#campaign-nto`, and `#general`.
143+
- Seeded-context document runs were executed in the same channels:
144+
`Status-campaign-nto-seeded-run.pdf` (`F0BDJN7SXGC`, 7 seeded status
145+
messages), `Incident-service-swarm-seeded-run.pdf` (`F0BE9BFESRE`, 10 seeded
146+
incident messages), and `Northstar-Proposal-seeded-run.pdf` (`F0BDGUA7X1P`,
147+
10 seeded sales discovery messages + reviewer pricing).
148+
149+
Still open: full human Slack cold QA for all five workflows, public walkthrough
150+
video, reviewer invites, and hosted HTTP verification (`/health`, Slack event URL,
151+
interactivity URL, and uptime through the reviewer window).
152+
128153
---
129154

130155
## 5. Product walkthrough video script (~3:00)
@@ -235,6 +260,27 @@ Forge workflow commands
235260
4. `@forge status for #team-eng`
236261
5. `@forge draft Weekly update: pipeline improved, onboarding risk remains, recommendation is a setup wizard.`
237262

263+
### Seed/poll utilities
264+
265+
- `npm run seed:workspace` posts the reviewer channel fixtures above and prints the board CSV path.
266+
- `npm run poll:slack -- --channel #team-eng` verifies bot-visible channel history before recording or reviewer handoff.
267+
- `tests/reviewer-workspace-scripts.test.mjs` keeps the seed messages, command copy, and board CSV fixture aligned with this playbook.
268+
269+
2026-06-26 sandbox run used existing channels:
270+
271+
```bash
272+
SEED_INCIDENT_CHANNEL=service-swarm \
273+
SEED_SALES_CHANNEL=acct-edge \
274+
SEED_STATUS_CHANNEL=campaign-nto \
275+
SEED_GENERAL_CHANNEL=general \
276+
SEED_DELAY_MS=100 \
277+
npm run seed:workspace
278+
```
279+
280+
Readback evidence was exported with `npm run poll:slack -- --channel ... --json`
281+
to `/tmp/forge-poll-service-swarm.json`, `/tmp/forge-poll-acct-edge.json`, and
282+
`/tmp/forge-poll-campaign-nto.json`.
283+
238284
---
239285

240286
## 8. Manual QA script
@@ -282,6 +328,7 @@ Run as someone other than the author. **All must pass.**
282328
### Headless (before Slack)
283329

284330
- [ ] `npm run forge:smoke` → 5 non-empty PDF files on disk
331+
- [ ] `npm run release:qa` → offline release gates, Slack manifest check, package inventory, and secret scan pass
285332

286333
---
287334

@@ -373,7 +420,7 @@ Reviewers test **Jul 14 – Aug 6**. Laptop `slack run` is insufficient.
373420
Release the review package only when **all** true:
374421

375422
- [ ] `forge:smoke` green
376-
- [ ] QA script passed within 48h
423+
- [ ] `npm run release:qa` passed within 48h
377424
- [ ] Video plays without login
378425
- [ ] Reviewers have workspace access
379426
- [ ] Deploy uptime 24h+

‎FORGE.md‎

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -389,15 +389,26 @@ Scripts (in `package.json`):
389389
```json
390390
{
391391
"slack": "npm run build && node --env-file=.env dist/slack/app.js",
392-
"forge:smoke": "npm run build && node scripts/forge-smoke.mjs"
392+
"forge:smoke": "npm run build && node scripts/forge-smoke.mjs",
393+
"test:visual": "node scripts/visual-regression.mjs",
394+
"compile:golden": "node scripts/compile-golden.mjs",
395+
"seed:workspace": "node --env-file=.env scripts/seed-workspace.mjs",
396+
"poll:slack": "node --env-file=.env scripts/poll-slack.mjs",
397+
"release:qa": "node scripts/release-qa.mjs"
393398
}
394399
```
395400

396401
---
397402

398403
## 13. Definition of done
399404

400-
- [ ] `forge:smoke` produces 5 PDFs via helpers/inference + pipeline (no Slack)
405+
- [x] `forge:smoke` produces 5 PDFs via helpers/inference + pipeline (no Slack)
406+
- [x] `npm run release:qa` passes locally, including visual regression, Slack
407+
manifest validation, package inventory, and secret scan
408+
- [x] Reviewer workspace seed/poll utilities are script-backed, test-covered,
409+
and executed against the sandbox Slack workspace
410+
- [x] Project status hero workflow has live Slack PDF evidence from
411+
`#campaign-nto` with clean emoji handling and evidence ledger
401412
- [ ] All 5 workflows work in workspace via `@forge` commands
402413
- [ ] RTS used in ≥1 workflow; history fallback in all
403414
- [ ] Confirm-before-export on all workflows

‎README.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ npm run check:typst # verify Typst pin (0.14.2)
2222
npm run check:packages # verify vendored offline @preview packages
2323
npm run lint # eslint
2424
npm test # unit/integration tests
25+
npm run release:qa # full local release gate before reviewer handoff
2526
npm run test:visual # golden PNG regression (24 templates)
2627
```
2728

@@ -151,3 +152,8 @@ template sync check → vendored-package check → lint + script syntax check
151152
check → build → unit/integration tests → Forge smoke → visual regression → package
152153
contents dry-run → Docker build. Golden drift fails CI; update with
153154
`npm run compile:golden`.
155+
156+
For a local reviewer handoff gate, run `npm run release:qa`. It executes the
157+
offline CI-critical checks, verifies Slack manifest scopes/events, confirms the
158+
npm package carries Slack setup/deploy assets, and scans tracked files for common
159+
secret token patterns.

‎package.json‎

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,19 @@
99
"files": [
1010
"dist/",
1111
"templates/",
12+
"slack/",
1213
"marketplace/",
1314
"brand_kits/",
1415
"packages/",
1516
"vendor/",
16-
"scripts/check-typst-version.mjs",
17-
"scripts/check-vendored-packages.mjs",
17+
"scripts/",
1818
"README.md",
19+
".env.sample",
20+
"DELIVERY.md",
1921
"FORGE.md",
2022
"DOCFORGE.md",
21-
"HARDENING.md"
23+
"HARDENING.md",
24+
"Dockerfile"
2225
],
2326
"scripts": {
2427
"build": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\" && tsc",
@@ -31,10 +34,11 @@
3134
"check:typst": "node scripts/check-typst-version.mjs",
3235
"check:packages": "node scripts/check-vendored-packages.mjs",
3336
"check:scripts": "for f in scripts/*.mjs; do node --check \"$f\" || exit 1; done",
37+
"release:qa": "node scripts/release-qa.mjs",
3438
"pack:check": "npm_config_cache=${TMPDIR:-/tmp}/docforge-npm-cache npm pack --dry-run --json",
3539
"lint": "eslint src tests scripts eslint.config.js vitest.config.ts",
36-
"format": "prettier --write README.md FORGE.md DELIVERY.md \"src/**/*.ts\" \"tests/**/*.ts\" \"scripts/**/*.mjs\" \"slack/**/*.json\" \".github/**/*.yml\"",
37-
"format:check": "prettier --check README.md FORGE.md DELIVERY.md \"src/**/*.ts\" \"tests/**/*.ts\" \"scripts/**/*.mjs\" \"slack/**/*.json\" \".github/**/*.yml\"",
40+
"format": "prettier --write README.md FORGE.md DELIVERY.md \"src/**/*.ts\" \"tests/**/*.{ts,mjs}\" \"scripts/**/*.mjs\" \"slack/**/*.json\" \".github/**/*.yml\"",
41+
"format:check": "prettier --check README.md FORGE.md DELIVERY.md \"src/**/*.ts\" \"tests/**/*.{ts,mjs}\" \"scripts/**/*.mjs\" \"slack/**/*.json\" \".github/**/*.yml\"",
3842
"slack": "npm run build && node --env-file=.env dist/slack/app.js",
3943
"seed:workspace": "node --env-file=.env scripts/seed-workspace.mjs",
4044
"poll:slack": "node --env-file=.env scripts/poll-slack.mjs"

‎scripts/forge-smoke.mjs‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,16 @@ for (const workflow of workflows) {
103103
workflow.templateId,
104104
workflow.data(),
105105
{
106-
workflow: workflow.name === "kpi_report" ? "board" : workflow.name === "sales_proposal" ? "proposal" : workflow.name === "project_status" ? "status" : workflow.name === "incident_report" ? "incident" : "draft",
106+
workflow:
107+
workflow.name === "kpi_report"
108+
? "board"
109+
: workflow.name === "sales_proposal"
110+
? "proposal"
111+
: workflow.name === "project_status"
112+
? "status"
113+
: workflow.name === "incident_report"
114+
? "incident"
115+
: "draft",
107116
gather: { source_labels: ["forge-smoke"], source_count: 1, gather_method: "headless" },
108117
},
109118
);

‎scripts/poll-slack.mjs‎

Lines changed: 30 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,22 +10,16 @@
1010
* for history (or channel membership).
1111
*/
1212
import { writeFile } from "node:fs/promises";
13+
import { pathToFileURL } from "node:url";
1314
import { WebClient } from "@slack/web-api";
1415

15-
const token = process.env.SLACK_BOT_TOKEN?.trim();
16-
if (!token) {
17-
console.error("SLACK_BOT_TOKEN missing — add to .env");
18-
process.exit(1);
19-
}
20-
21-
const client = new WebClient(token);
2216
const args = process.argv.slice(2);
2317
const jsonOut = args.includes("--json");
2418
const channelFilter = args.includes("--channel") ? args[args.indexOf("--channel") + 1] : undefined;
2519
const limit = Number(process.env.POLL_MESSAGE_LIMIT ?? 30);
2620
const outFile = process.env.POLL_OUTPUT;
2721

28-
async function listChannels() {
22+
export async function listChannels(client) {
2923
const channels = [];
3024
let cursor;
3125
do {
@@ -52,7 +46,7 @@ async function listChannels() {
5246
return channels;
5347
}
5448

55-
async function fetchHistory(channelId) {
49+
export async function fetchHistory(client, channelId) {
5650
try {
5751
const res = await client.conversations.history({
5852
channel: channelId,
@@ -77,14 +71,31 @@ async function fetchHistory(channelId) {
7771
}
7872
}
7973

80-
function formatTranscript(messages) {
74+
export function normalizeChannelFilter(value) {
75+
return value?.replace(/^#/, "");
76+
}
77+
78+
export function applyChannelFilter(channels, filter) {
79+
const normalized = normalizeChannelFilter(filter);
80+
if (!normalized) return channels;
81+
return channels.filter((c) => c.name === normalized);
82+
}
83+
84+
export function formatTranscript(messages) {
8185
return messages
8286
.filter((m) => m.text?.trim())
8387
.map((m) => m.text.trim())
8488
.join("\n");
8589
}
8690

8791
async function main() {
92+
const token = process.env.SLACK_BOT_TOKEN?.trim();
93+
if (!token) {
94+
console.error("SLACK_BOT_TOKEN missing — add to .env");
95+
process.exit(1);
96+
}
97+
98+
const client = new WebClient(token);
8899
const auth = await client.auth.test();
89100
const workspace = {
90101
team: auth.team,
@@ -93,9 +104,9 @@ async function main() {
93104
polled_at: new Date().toISOString(),
94105
};
95106

96-
let channels = await listChannels();
107+
let channels = await listChannels(client);
97108
if (channelFilter) {
98-
channels = channels.filter((c) => c.name === channelFilter.replace(/^#/, ""));
109+
channels = applyChannelFilter(channels, channelFilter);
99110
if (channels.length === 0) {
100111
console.error(`Channel not found: ${channelFilter}`);
101112
process.exit(1);
@@ -105,7 +116,7 @@ async function main() {
105116
const report = { workspace, channels: [] };
106117

107118
for (const ch of channels) {
108-
const history = await fetchHistory(ch.id, ch.name);
119+
const history = await fetchHistory(client, ch.id);
109120
const entry = {
110121
...ch,
111122
accessible: history.ok,
@@ -148,7 +159,9 @@ async function main() {
148159
}
149160
}
150161

151-
main().catch((err) => {
152-
console.error("poll failed:", err?.data?.error ?? err.message ?? err);
153-
process.exit(1);
154-
});
162+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
163+
main().catch((err) => {
164+
console.error("poll failed:", err?.data?.error ?? err.message ?? err);
165+
process.exit(1);
166+
});
167+
}

0 commit comments

Comments
 (0)