diff --git a/.gitmodules b/.gitmodules index b37014f..7682bb6 100644 --- a/.gitmodules +++ b/.gitmodules @@ -10,9 +10,6 @@ [submodule "tool-base"] path = tool-base url = https://github.com/SpineEventEngine/tool-base.git -[submodule "delivery-server"] - path = delivery-server - url = https://github.com/SpineEventEngine/delivery-server.git [submodule "config"] path = config url = https://github.com/SpineEventEngine/config.git @@ -49,9 +46,15 @@ [submodule "money"] path = money url = https://github.com/SpineEventEngine/money.git +[submodule "gcloud-jvm"] + path = gcloud-jvm + url = https://github.com/SpineEventEngine/gcloud-jvm.git [submodule ".agents/shared"] path = .agents/shared url = https://github.com/SpineEventEngine/agents.git branch = master update = merge ignore = all +[submodule "agents"] + path = agents + url = https://github.com/SpineEventEngine/agents.git diff --git a/README.md b/README.md index a4d2bd9..162016e 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,119 @@ # summit -The repository for automation of cross-repository works + +The meta-repository for automating cross-repository work across the [Spine SDK][spine-org]. + +`summit` is a Git superproject: it assembles the Spine SDK repositories as submodules +in a single working tree, so agents and the shared `config`/CI tooling can act across +the whole SDK at once. It ships no code and has no build of its own — its content is the +pinned commits of the submodules below, plus the shared agent tooling under `.agents/`. + +## Repository layout + +The submodules are pinned to fixed commits, giving a reproducible snapshot of the +SDK. They are grouped below by function, not by strict dependency order. + +### Shared infrastructure + +| Submodule | Role | +|-----------|------| +| [`config`][config] | Dependencies and build configurations shared among subprojects | + +The shared agent tooling — skills, scripts, and guidelines — lives in the +[`agents`][agents] repository, mounted at `.agents/shared`. Unlike the submodules +listed here, that mount floats to the tip of `master` rather than a pinned commit. +The same repository is also wired in as a pinned top-level submodule at `agents/`, +so cross-repository automation can run against the tooling repo itself, just as it +does against the SDK repositories above. + +### Foundation & utilities + +| Submodule | Role | +|-----------|------| +| [`base-libraries`][base-libraries] | The framework's base types and utilities | +| [`reflect`][reflect] | Utilities for working with reflection in Java and Kotlin projects | +| [`logging`][logging] | Fluent logging API for Kotlin projects | +| [`testlib`][testlib] | Testing utilities for Spine SDK development and users | + +### Domain & value types + +| Submodule | Role | +|-----------|------| +| [`base-types`][base-types] | Popular value object types and associated code | +| [`change`][change] | Data types and utilities for changes and mismatches in data values | +| [`time`][time] | Protobuf-based date/time types and utilities | +| [`money`][money] | Currency and money data types and operations | + +### Compiler & code generation + +| Submodule | Role | +|-----------|------| +| [`compiler`][compiler] | The Spine Compiler — extendable Protobuf compilation | +| [`tool-base`][tool-base] | Common code for development tools | +| [`ProtoTap`][ProtoTap] | Utilities for tapping `protoc` output | +| [`validation`][validation] | Library and Compiler plugins for generating custom validation code | +| [`core-jvm-compiler`][core-jvm-compiler] | Plugins of the CoreJvm library for the Spine Compiler | + +### Framework core + +| Submodule | Role | +|-----------|------| +| [`core-jvm`][core-jvm] | The JVM-based implementation of the Spine framework core | + +### Storage & runtime + +| Submodule | Role | +|-----------|------| +| [`jdbc-storage`][jdbc-storage] | Support for storage in JDBC-compliant databases | +| [`gcloud-jvm`][gcloud-jvm] | Support for Spine-based Kotlin and Java apps on Google Cloud | + +## Getting started + +Clone with all submodules in one step: + +```bash +git clone --recursive https://github.com/SpineEventEngine/summit.git +``` + +If you cloned without `--recursive`, or you are working in a fresh `git worktree` or +a shallow checkout, the submodules start uninitialised and the `.agents` symlinks +dangle. Bootstrap them with: + +```bash +./init-submodules # materialise the config-managed submodules at their pinned commits +./config/pull # float the shared submodules and copy in the shared files +``` + +`./init-submodules` runs automatically at the start of a Claude Code session. See +[`AGENTS.md`](AGENTS.md) for the full bootstrap chain and the rationale behind it. + +## Working across repositories + +`summit` is where cross-repository work happens: coordinated agent sessions and the +shared `config` tooling propagate changes — dependency bumps, CI workflows, agent +guidelines — across the SDK from one place. Agent orientation, safety rules, and the +available skills are described in [`AGENTS.md`](AGENTS.md). + +## License + +`summit`, like the rest of the Spine SDK, is distributed under the +[Apache License 2.0](LICENSE). + +[spine-org]: https://github.com/SpineEventEngine +[agents]: https://github.com/SpineEventEngine/agents +[config]: https://github.com/SpineEventEngine/config +[base-libraries]: https://github.com/SpineEventEngine/base-libraries +[reflect]: https://github.com/SpineEventEngine/reflect +[logging]: https://github.com/SpineEventEngine/logging +[testlib]: https://github.com/SpineEventEngine/testlib +[base-types]: https://github.com/SpineEventEngine/base-types +[change]: https://github.com/SpineEventEngine/change +[time]: https://github.com/SpineEventEngine/time +[money]: https://github.com/SpineEventEngine/money +[compiler]: https://github.com/SpineEventEngine/compiler +[tool-base]: https://github.com/SpineEventEngine/tool-base +[ProtoTap]: https://github.com/SpineEventEngine/ProtoTap +[validation]: https://github.com/SpineEventEngine/validation +[core-jvm-compiler]: https://github.com/SpineEventEngine/core-jvm-compiler +[core-jvm]: https://github.com/SpineEventEngine/core-jvm +[jdbc-storage]: https://github.com/SpineEventEngine/jdbc-storage +[gcloud-jvm]: https://github.com/SpineEventEngine/gcloud-jvm diff --git a/agents b/agents new file mode 160000 index 0000000..2369ab8 --- /dev/null +++ b/agents @@ -0,0 +1 @@ +Subproject commit 2369ab816bb5d0feee562bfd6395b0b2852a3a9d diff --git a/base-libraries b/base-libraries index 76b2d00..7a05857 160000 --- a/base-libraries +++ b/base-libraries @@ -1 +1 @@ -Subproject commit 76b2d003f8e5711a50741bbd0b016302f399e82d +Subproject commit 7a05857b2adc68dc2f9b28a03ae3073e7c3e9df3 diff --git a/base-types b/base-types index ee191de..0f2898b 160000 --- a/base-types +++ b/base-types @@ -1 +1 @@ -Subproject commit ee191de26a712f0ab318ee8e794929f8e7a89dbe +Subproject commit 0f2898be841a4f0f92497dd597ab04e5216218ba diff --git a/delivery-server b/delivery-server deleted file mode 160000 index 27aef68..0000000 --- a/delivery-server +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 27aef68d77c9b40c19f2f0dd253e351363f76377 diff --git a/docs/project.md b/docs/project.md index 0fdaffc..9a9ad97 100644 --- a/docs/project.md +++ b/docs/project.md @@ -1,18 +1,44 @@ - - -# Project: +# Project: summit ## Overview -*One paragraph: what this repo is, what problem it solves, and its role in the -Spine SDK organisation.* +`summit` is a Git superproject (a meta-repository) that assembles the Spine SDK +repositories as submodules in a single working tree. It exists to automate +cross-repository work across the SDK: coordinated agent sessions and the shared +`config`/CI machinery act across many repositories at once. `summit` ships no code and +has no build of its own — its content is the pinned commits of the submodules it +aggregates, plus the shared agent tooling under `.agents/`. ## Architecture -*Role in the org: library / tool / Gradle plugin / application. -Key patterns, public API boundaries, and constraints specific to this repo.* +Role in the organisation: a **coordination superproject** — not a library, tool, +Gradle plugin, or application. + +- **Submodules.** The SDK repositories listed in `.gitmodules` are pinned to fixed + commits, giving a reproducible snapshot of the whole SDK. The shared + `.agents/shared` submodule is the exception: it declares a tracked `branch` and + floats to the tip of `master`, so shared skills, scripts, and guidelines stay + current with no file churn in consumer pull requests. +- **Bootstrapping.** A fresh `git worktree` or shallow checkout starts with the + submodules uninitialised, so the `.agents` symlinks dangle. Run `./init-submodules` + to materialise the config-managed submodules at their pinned commits, then + `./config/pull` to float the shared submodules and copy the shared files in. Claude + Code runs `./init-submodules` automatically via a `SessionStart` hook. +- **Not a JVM build.** `summit` has no Gradle build, coding style, or tests of its + own, so the shared JVM requirements in `.agents/guidelines/jvm-project.md` do not + govern this repository. The aggregated repositories are JVM (Kotlin/Java) projects + and are each subject to that guideline in their own right. + + + +## Cross-repository workflows + +Repeatable, parameterised procedures for acting across the SDK repositories live under +[`docs/rollout/`](rollout/). Each pairs a repo-owned script (the deterministic +mechanics) with an agent-driven playbook (the judgement steps): - +- [`rollout/proofread.md`](rollout/proofread.md) — run the `proofread` skill across a + repo end-to-end (bump → build → sweep → pre-PR → PR), driven by the + [`proofread-repo`](../proofread-repo) script, with an optional + [`proofread-fanout.workflow.js`](rollout/proofread-fanout.workflow.js) for the sweep. diff --git a/docs/rollout/proofread-fanout.workflow.js b/docs/rollout/proofread-fanout.workflow.js new file mode 100644 index 0000000..c28fb1d --- /dev/null +++ b/docs/rollout/proofread-fanout.workflow.js @@ -0,0 +1,302 @@ +// proofread-fanout.workflow.js — step 2 (the proofread sweep) as a deterministic +// fan-out: one proofreader agent per file chunk. +// +// STATUS: the Proofread stage was validated once (base-types — 33 files, 3 chunks, +// 14 fixes), then hardened for coverage honesty (a failed chunk is reported in +// `failedFiles`, never silently dropped), loud input validation, and de-duplicated +// file lists. The Verify stage (below) is NEW and not yet exercised on a real corrupt +// diff. Neither stage has run on a large repo, an already-clean (no-op) repo, or a +// forced-failure run — see docs/rollout/proofread.md for the remaining validation. +// +// OPT-IN BY DESIGN (not a gap to close): the Workflow tool requires explicit opt-in, +// so this runs only when the calling session invokes it — it is never auto-triggered +// from the proofread-repo bash script. +// +// Invoke from the calling session: +// Workflow({ scriptPath: "docs/rollout/proofread-fanout.workflow.js", +// args: { repo: "base-types", +// root: "/abs/base-types", +// catalog: "/abs/.agents/guidelines/english-style.md", +// files: [ ...ABSOLUTE prose file paths... ], +// chunkSize: 40 } }) +// +// The CALLER pre-scopes `files` (ABSOLUTE paths) and passes ABSOLUTE `catalog` and +// `root`, because the Workflow runtime has NO filesystem access — it can neither list +// files nor resolve repo-relative paths. Scope = `git ls-files` of +// *.kt/*.kts/*.java/*.proto/*.md minus build/, buildSrc/, .idea/, .claude/, .junie/, +// .github/, .agents/, the config-distributed Markdown, and the generated +// docs/dependencies/ reports. Proofread agents edit DISJOINT files — no worktree +// isolation needed. +// +// Two stages: (1) Proofread — one agent per file chunk edits prose in place; +// (2) Verify — read-only-git agents inspect the ACTUAL working-tree diff of every +// changed file and flag any edit that escaped prose, ESPECIALLY trailing whitespace +// stripped inside a string literal (the sanitize-source-code.sh hook's known corruption +// of test fixtures). Verify only reads — it never edits or commits. The caller still +// owns the final audit and the commit, but works from `verification.suspectFiles` +// instead of eyeballing the whole diff (docs/rollout/proofread.md step 2). + +export const meta = { + name: 'proofread-fanout', + description: 'Fan the proofread sweep across a repo\'s prose files, then verify the diff', + phases: [ + { title: 'Proofread', detail: 'one proofreader per file chunk' }, + { title: 'Verify', detail: 'read-only-git audit of every changed file' }, + ], +} + +// Structured report each proofreader returns (validated at the tool-call layer). +const REPORT = { + type: 'object', + additionalProperties: false, + properties: { + filesChanged: { type: 'integer' }, + changes: { + type: 'array', + items: { + type: 'object', additionalProperties: false, + required: ['file', 'topic', 'before', 'after'], + properties: { + file: { type: 'string' }, line: { type: 'integer' }, + topic: { type: 'string' }, before: { type: 'string' }, after: { type: 'string' }, + }, + }, + }, + skipped: { + type: 'array', + items: { + type: 'object', additionalProperties: false, + required: ['file', 'topic', 'reason'], + properties: { file: { type: 'string' }, topic: { type: 'string' }, reason: { type: 'string' } }, + }, + }, + }, + required: ['filesChanged', 'changes', 'skipped'], +} + +// Ground truth from the Verify stage's discover step: what actually changed on disk, +// independent of what the proofreaders self-reported. +const DISCOVER = { + type: 'object', + additionalProperties: false, + required: ['gitOk', 'changedFiles'], + properties: { + gitOk: { type: 'boolean' }, + changedFiles: { type: 'array', items: { type: 'string' } }, + note: { type: 'string' }, + }, +} + +// Per-file verdict from the Verify stage's inspect step. +const VERDICT = { + type: 'object', + additionalProperties: false, + required: ['files'], + properties: { + files: { + type: 'array', + items: { + type: 'object', + additionalProperties: false, + required: ['file', 'status'], + properties: { + file: { type: 'string' }, + status: { type: 'string', enum: ['clean', 'suspect', 'unverified'] }, + issues: { + type: 'array', + items: { + type: 'object', + additionalProperties: false, + required: ['kind', 'detail'], + properties: { + kind: { type: 'string', enum: ['whitespace-in-string-literal', 'non-prose-edit', 'other'] }, + line: { type: 'integer' }, + detail: { type: 'string' }, + }, + }, + }, + }, + }, + }, + }, +} + +// The Workflow harness may hand `args` in as a JSON string rather than an object; +// accept either so `args.files` can't silently read as undefined (0 files, 0 agents). +let A +try { + A = typeof args === 'string' ? JSON.parse(args) : (args || {}) +} catch (e) { + throw new Error(`proofread-fanout: \`args\` was a string but not valid JSON — ${e.message}`) +} +const repo = A.repo +const catalog = A.catalog +const root = A.root + +// Fail loudly on caller mistakes instead of no-op'ing into an empty, "successful" run +// (the first base-types attempt silently proofread 0 files this way). +if (!repo) throw new Error('proofread-fanout: `repo` is required (the repo being swept).') +if (!catalog) { + throw new Error('proofread-fanout: `catalog` is required — the ABSOLUTE path to english-style.md.') +} +if (!root) { + throw new Error('proofread-fanout: `root` is required — the ABSOLUTE repo root for read-only git verification.') +} + +// De-duplicate: agents edit DISJOINT files only if the list has no repeats. Without +// this, a duplicated path could land in two concurrent agents and race on one file. +const rawFiles = Array.isArray(A.files) ? A.files : [] +const files = [...new Set(rawFiles)] +if (files.length === 0) { + throw new Error('proofread-fanout: `files` is empty — the caller must pre-scope the prose file list (see the header).') +} +const deduped = rawFiles.length - files.length + +const CHUNK = A.chunkSize || 40 +const VCHUNK = A.verifyChunkSize || 20 +const chunks = [] +for (let i = 0; i < files.length; i += CHUNK) chunks.push(files.slice(i, i + CHUNK)) + +if (deduped > 0) log(`deduped ${deduped} repeated path(s) from the input`) +log(`proofread ${files.length} files in ${repo} across ${chunks.length} chunk(s)`) +phase('Proofread') + +// One proofreader per chunk, all concurrent (runtime caps at ~cores-2 at a time). +// parallel() is the right call here: a single stage whose results we aggregate. +const results = await parallel(chunks.map((chunk, i) => () => + agent( + [ + `Proofread ONLY the files listed below (ABSOLUTE paths); they belong to ${repo}.`, + `First read the catalog at ${catalog} IN FULL — it is the sole authority on what`, + `counts as an error and when to leave text alone. Bias: a missed error is cheaper`, + `than a wrong fix — skip anything not clearly correct and record it in "skipped".`, + ``, + `Edit PROSE ONLY: comments in .kt/.kts/.java/.proto (KDoc/Javadoc/line/block) and body`, + `text in .md. NEVER touch identifiers, keywords, string literals, annotations, doc-link`, + `targets like [Type], copyright headers, or machine-read directives. Keep edits minimal.`, + `Do NOT run git, builds, or commits — only read and Edit the listed files.`, + ``, + `Files:`, + ...chunk, + ].join('\n'), + { label: `proofread:${i}`, phase: 'Proofread', schema: REPORT } + ) +)) + +// A died/refused agent resolves to null. Do NOT silently drop it: its whole chunk +// went un-swept, so the caller must learn coverage was incomplete before committing. +const ok = [] +const failedChunks = [] +results.forEach((r, i) => { + if (r) ok.push(r) + else failedChunks.push(i) +}) +const failedFiles = failedChunks.flatMap(i => chunks[i]) +if (failedFiles.length > 0) { + log(`WARNING: ${failedChunks.length}/${chunks.length} chunk(s) failed — ${failedFiles.length} file(s) were NOT proofread. Sweep the files in \`failedFiles\` before committing.`) +} + +const changes = ok.flatMap(r => r.changes || []) +const skipped = ok.flatMap(r => r.skipped || []) +const claimedChanged = [...new Set(changes.map(c => c.file))] + +// ---- Stage 2: Verify — close the whitespace-in-string-literal failure mode. ---- +// The sanitize-source-code.sh PostToolUse hook strips trailing whitespace file-wide +// after every Edit, silently corrupting whitespace-sensitive test string literals +// (this broke a StringsSpec trim test). The proofreaders' self-reports cannot catch a +// side effect they never saw, so an INDEPENDENT pass inspects the actual working-tree +// diff of every changed file. +phase('Verify') + +// Discover ground truth independently of what the proofreaders claimed — a verifier +// must not trust the party whose edits it checks. +const discovery = await agent( + [ + `You verify a proofread sweep of ${repo}. Using ONLY read-only git, list the files that`, + `currently differ in the working tree.`, + ``, + `Run exactly: git -C "${root}" diff --name-only`, + `Return the repo-relative paths it prints as "changedFiles" and set "gitOk" true. If git`, + `cannot run (no shell/git, or an error), set "gitOk" false, "changedFiles" [], and put the`, + `error text in "note". Do NOT edit, stage, commit, or build anything.`, + ].join('\n'), + { label: 'verify:discover', phase: 'Verify', schema: DISCOVER } +) + +let verification +if (!discovery || !discovery.gitOk) { + const note = discovery ? (discovery.note || 'git unavailable') : 'discover agent did not return' + log(`WARNING: verification could not enumerate changes (${note}) — audit the diff manually before committing.`) + verification = { ran: false, verified: false, actualChanged: [], suspectFiles: [], unverifiedFiles: [], changedButNotClaimed: [], claimedButNotChanged: [], note } +} else { + const actualChanged = [...new Set(discovery.changedFiles || [])] + if (actualChanged.length === 0) { + verification = { ran: true, verified: true, actualChanged: [], suspectFiles: [], unverifiedFiles: [], changedButNotClaimed: [], claimedButNotChanged: claimedChanged, note: 'no working-tree changes to verify' } + } else { + const vChunks = [] + for (let i = 0; i < actualChanged.length; i += VCHUNK) vChunks.push(actualChanged.slice(i, i + VCHUNK)) + log(`verify ${actualChanged.length} changed file(s) across ${vChunks.length} chunk(s)`) + + const vResults = await parallel(vChunks.map((chunk, i) => () => + agent( + [ + `You audit a proofread sweep of ${repo} for edits that escaped prose. For EACH file`, + `below, inspect its working-tree diff with read-only git and classify it.`, + ``, + `For each file run: git -C "${root}" diff -- ""`, + `A file is "clean" only if EVERY added/removed line is inside a comment (KDoc/Javadoc/`, + `line/block) or Markdown body prose. Mark it "suspect" if the diff touches anything else,`, + `and record one issue per offending hunk:`, + ` - kind "whitespace-in-string-literal": a changed line inside a string literal —`, + ` a raw/multiline string or test fixture — INCLUDING a pure trailing-whitespace`, + ` removal. This is the sanitize-source-code.sh hook's known corruption; always flag it.`, + ` - kind "non-prose-edit": a change to an identifier, keyword, annotation, a doc-link`, + ` target like [Type], or any other executable token.`, + `Quote the offending hunk in "detail" and give its "line". If git cannot run for a file,`, + `mark it "unverified" with the reason. Do NOT edit, stage, commit, or build — read only.`, + ``, + `Files (repo-relative to ${root}):`, + ...chunk, + ].join('\n'), + { label: `verify:${i}`, phase: 'Verify', schema: VERDICT } + ) + )) + + const verdicts = vResults.filter(Boolean).flatMap(r => r.files || []) + const deadVerify = [] + vResults.forEach((r, i) => { if (!r) deadVerify.push(...vChunks[i]) }) + + const suspectFiles = verdicts.filter(v => v.status === 'suspect') + const unverifiedFiles = [ + ...verdicts.filter(v => v.status === 'unverified') + .map(v => ({ file: v.file, reason: (v.issues && v.issues[0] && v.issues[0].detail) || 'unverified' })), + ...deadVerify.map(f => ({ file: f, reason: 'verifier agent failed' })), + ] + + // Best-effort reconcile of ground truth against the proofreaders' self-reports; + // paths may be absolute or repo-relative, so match on a shared suffix. + const matches = (a, b) => a === b || a.endsWith('/' + b) || b.endsWith('/' + a) + const changedButNotClaimed = actualChanged.filter(a => !claimedChanged.some(c => matches(a, c))) + const claimedButNotChanged = claimedChanged.filter(c => !actualChanged.some(a => matches(a, c))) + + const verified = suspectFiles.length === 0 && unverifiedFiles.length === 0 + if (!verified) { + log(`WARNING: verification flagged ${suspectFiles.length} suspect + ${unverifiedFiles.length} unverified file(s) — review \`verification.suspectFiles\` before committing.`) + } + verification = { ran: true, verified, actualChanged, suspectFiles, unverifiedFiles, changedButNotClaimed, claimedButNotChanged } + } +} + +return { + repo, + complete: failedChunks.length === 0, // false ⇒ proofread coverage gap; see failedFiles + filesScanned: files.length, + chunksTotal: chunks.length, + chunksSucceeded: ok.length, + chunksFailed: failedChunks.length, + failedFiles, // files whose proofread chunk failed — must be swept + filesChanged: ok.reduce((n, r) => n + (r.filesChanged || 0), 0), + changes, + skipped, + verification, // { ran, verified, suspectFiles, ... } — see Stage 2 +} diff --git a/docs/rollout/proofread-pr-body.md b/docs/rollout/proofread-pr-body.md new file mode 100644 index 0000000..655e029 --- /dev/null +++ b/docs/rollout/proofread-pr-body.md @@ -0,0 +1,27 @@ +## Summary + +Repo-wide English proofread — grammar, spelling, punctuation, articles, restrictive +`which`/`that`, verb forms in API summaries, and verb complementation (`allow`/`enable` ++ gerund) — across KDoc, Javadoc, Protobuf doc comments, and Markdown. + +**Prose only:** no executable code, identifiers, string literals, or doc-link +targets were changed. + +## Commits + +- **Update shared agent tooling** — floated `.agents/shared` to current via `./config/pull`. +- **Bump version** — snapshot increment required by the versioning policy. +- **Update dependency reports** — `docs/dependencies/` refreshed for the new version; + the report diff is version-only (no dependency changes). +- **Proofread comments and documentation** — the prose fixes. + +## Verification + +- `./gradlew clean build dokkaGenerate` passes — compile, tests, and Dokka/KDoc link + checks are all green. +- Reviewed with the repository's own reviewers (`review-docs`, + `spine-code-review`, `kotlin-engineer`). + + diff --git a/docs/rollout/proofread.md b/docs/rollout/proofread.md new file mode 100644 index 0000000..cc25aa6 --- /dev/null +++ b/docs/rollout/proofread.md @@ -0,0 +1,91 @@ +# Repo-by-repo proofread rollout + +A parameterised procedure for running the `proofread` skill across the Spine SDK +repositories from `summit`, one repo at a time, ending in a pull request. + +The deterministic mechanics live in the repo-owned [`proofread-repo`](../../proofread-repo) +script; the two judgement steps — the proofread sweep and the pre-PR review — are +agent-driven and described below. Run the four steps in order for a given `` +(e.g. `base-libraries`). + +> **Why a script *and* a playbook?** The interventions the first run needed +> (`JAVA_HOME`, `./config/pull` ordering, flaky dependency reports) were +> deterministic mechanics, not judgement — so they live in the script, where they +> can't be re-interpreted. Only the proofread and the review genuinely need an +> agent. + +## 0. Prerequisites (once per machine) + +- `JAVA_HOME`: `proofread-repo` exports it for its own builds, but the **agent-run** + builds (step 1's edge-case bump and step 3's `pre-pr`) run in separate shells that do + **not** inherit it — so a durable `export JAVA_HOME="$(/usr/libexec/java_home -v 21)"` + in your shell profile is the real fix. Without it the Gradle Doctor plugin + hard-fails every build. +- `gh auth status` shows a token with `repo` + `workflow` scope. + +## 1. Prep (mechanical) — `./proofread-repo prep ` + +Cuts `proofread-sweep` off the latest `master`, floats the repo's `.agents/shared` +to current via `./config/pull` (committed on its own, so the repo has the current +`english-style` catalog and `proofread` skill), bumps the snapshot version, then +regenerates `docs/dependencies/` with a clean build and commits it verbatim — so the +reports reflect any dependency-pin updates `config/pull` brought in as well as the +bump. No agent judgement. + +The bump handles the common snapshot case (`+1`). For a **release-line version** or a +`version.gradle.kts` still using `by extra(...)` (which needs migration), `prep` stops +and asks you to run the `bump-version` skill first, then re-run `./proofread-repo prep +`. Every step is idempotent, so the re-run picks up where it left off. + +## 2. Proofread sweep (agent) + +**Scope.** `git ls-files` of project-owned prose — `*.kt`, `*.kts`, `*.java`, +`*.proto`, `*.md`. **Exclude** (not project-owned, or not prose): `build/`, +`.gradle/`, `buildSrc/`, `.idea/`, `.claude/`, `.junie/`, `.github/`, `.agents/`, +`AGENTS.md`, `CLAUDE.md`, `CODE_OF_CONDUCT.md`, `CONTRIBUTING.md`, and the +**generated** `docs/dependencies/` reports. + +**Fan out.** Split the file list into ~40-file chunks and run one proofreader per +chunk. Each reads `.agents/guidelines/english-style.md` (present after step 1) and +edits **prose only** — comments in source, body in Markdown — never identifiers, +string literals, doc-link targets (`[Type]`), or machine-read directives. Bias: +*a missed error is cheaper than a wrong fix*; skip ambiguous cases. See +[`proofread-fanout.workflow.js`](proofread-fanout.workflow.js) for this step as a saved +Workflow — the fan-out as one deterministic, opt-in call that reports any failed chunks +(`failedFiles`) instead of dropping them. Validated on `base-types`; invoked on demand, +never auto-run from `proofread-repo`. + +**Audit before committing — do NOT rely on `git diff --ignore-all-space`.** A +`PostToolUse` formatter hook (`sanitize-source-code.sh`) strips trailing whitespace +file-wide after each edit. That is house style everywhere *except* inside +whitespace-sensitive **test string literals**, where it silently corrupts fixtures +(this broke a `StringsSpec` trim test on the first run, caught only by the build + +reviewers). If you ran the fan-out Workflow, start from its `verification.suspectFiles` — +its Verify stage inspects every changed file's diff for `whitespace-in-string-literal` +and non-prose edits; otherwise inspect `git diff` **with** whitespace yourself. Either +way, restore any stripped trailing whitespace inside string literals with `sed` via Bash +— the hook fires on `Edit`/`Write`/`MultiEdit` only, so a Bash edit is not re-stripped. +Then commit. + +## 3. Pre-PR (agent) — run the `pre-pr` skill + +Version gate (already satisfied), `./gradlew clean build dokkaGenerate`, and the +reviewers (`review-docs`, `spine-code-review`, `kotlin-engineer`). Apply their +Must-fixes; apply the clear Should-fixes that are genuine proofread misses. On PASS +it writes the `.git/pre-pr.ok` sentinel that gates PR creation. + +## 4. Ship (mechanical) — `./proofread-repo ship ` + +Verifies the sentinel matches HEAD, discards any post-build report noise, pushes +`proofread-sweep`, and opens the PR from [`proofread-pr-body.md`](proofread-pr-body.md). + +## Cost & caveats + +- A full run is **expensive** — a dozen proofreader subagents, **two clean builds** + (`prep` regenerates the reports and verifies the bump; `pre-pr` re-verifies after + the sweep), and three reviewers per repo. Budget accordingly. Because the sweep + touches `.proto`, `pre-pr` runs a *clean* build per policy; an incremental build + would suffice for doc-only proto edits and is a worthwhile refinement. +- If `./config/pull` updates dependency pins, that simply flows through: the + regenerated reports reflect reality and the proofread lands on top of them. +- `proofread-repo` uses macOS `sed -i ''`; adjust for Linux runners. diff --git a/gcloud-jvm b/gcloud-jvm new file mode 160000 index 0000000..f4ade19 --- /dev/null +++ b/gcloud-jvm @@ -0,0 +1 @@ +Subproject commit f4ade19d8bf7666447f068607426475cda485afe diff --git a/proofread-repo b/proofread-repo new file mode 100755 index 0000000..88fe14d --- /dev/null +++ b/proofread-repo @@ -0,0 +1,162 @@ +#!/usr/bin/env bash +# +# proofread-repo — deterministic mechanics for the summit repo-by-repo proofread +# workflow. *** DRAFT — not wired into any hook. *** +# +# Owns only the parts that must NOT be re-interpreted each run (the environmental +# fixes that bit us on the first run: JAVA_HOME, config/pull ordering, and +# generating authoritative dependency reports via a clean build). The two +# judgement steps — the proofread sweep and the pre-PR review — are agent-driven +# and run BETWEEN `prep` and `ship`. See docs/rollout/proofread.md. +# +# Re-runnable: every step is idempotent, so a run interrupted midway — e.g. when +# `prep` bails to the bump-version skill for an edge case — resumes cleanly on a +# second `./proofread-repo prep `. +# +# Usage: +# ./proofread-repo prep # config/pull, branch, bump, regenerate reports +# ./proofread-repo ship # verify pre-pr sentinel, push, open the PR +# +# Note: uses macOS `sed -i ''`; adjust for Linux runners. +# +set -euo pipefail + +MODE="${1:?usage: proofread-repo }" +REPO="${2:?usage: proofread-repo }" +SUMMIT="$(cd "$(dirname "$0")" && pwd)" +DIR="$SUMMIT/$REPO" +BASE="master" +BRANCH="proofread-sweep" + +# A submodule initialised the normal way (`git submodule update --init` / clone +# --recursive) has `.git` as a gitfile — a file pointing into the superproject's +# .git/modules — NOT a directory, so `[ -d .git ]` would reject every valid submodule. +# Probe with git instead: it accepts the gitfile, directory, and worktree forms and +# still rejects an empty, uninitialised directory. +git -C "$DIR" rev-parse --is-inside-work-tree >/dev/null 2>&1 \ + || { echo "!! no initialised submodule at $DIR (try: git submodule update --init $REPO)"; exit 1; } + +# --- Fix #1: JAVA_HOME. Without it the Gradle Doctor plugin hard-fails every +# build. Agent-run skill builds (bump-version, pre-pr) need it too, so a durable +# `export JAVA_HOME=...` in your shell profile is the real cross-cutting fix; this +# line only covers the script's own builds. +export JAVA_HOME="${JAVA_HOME:-$(/usr/libexec/java_home -v 21 2>/dev/null || true)}" +[ -n "${JAVA_HOME:-}" ] || { echo "!! JAVA_HOME unset and no JDK 21 found"; exit 1; } + +cd "$DIR" + +version_of() { sed -nE 's/.*versionToPublish"?,?[[:space:]]*"?([0-9A-Za-z.-]+)".*/\1/p' | head -1; } + +# True when the branch already carries a change to relative to base. +branch_touched() { ! git diff --quiet "origin/$BASE...HEAD" -- "$1" 2>/dev/null; } + +ensure_branch() { + [ "$(git rev-parse --abbrev-ref HEAD)" = "$BRANCH" ] && return 0 # already here (re-run) + git fetch --quiet origin "$BASE" + git checkout -q "$BASE" + git pull --ff-only --quiet origin "$BASE" + git switch -q -c "$BRANCH" 2>/dev/null || git switch -q "$BRANCH" +} + +pull_shared() { + # Fix #2: float shared tooling FIRST (brings the current english-style catalog + # + proofread skill) plus any config-distributed updates; commit as its own + # change. Skip if this branch already carries that commit (re-run safety). + git log --format=%s "origin/$BASE..HEAD" | grep -q '^Update shared agent tooling' \ + && { echo "-- shared tooling already pulled on this branch — skipping"; return 0; } + # Guard: the tree must be clean before ./config/pull so the `git add -A` below + # stages ONLY config/pull's output — never pre-existing local edits or stray + # untracked files. `ensure_branch` normally leaves it clean; bail loudly if not. + [ -z "$(git status --porcelain)" ] \ + || { echo "!! working tree not clean before ./config/pull at $DIR — commit or stash first"; exit 1; } + ./config/pull + if [ -n "$(git status --porcelain)" ]; then + git add -A # safe now: only config/pull's output is dirty + git commit -q -m "Update shared agent tooling and config (./config/pull)" + fi +} + +bump_version() { + local f="version.gradle.kts" + [ -f "$f" ] || { echo "-- no $f — bump N/A"; return 0; } + branch_touched "$f" && { echo "-- already bumped on this branch — skipping"; return 0; } + + # Defer edge cases to the `bump-version` skill (agent): a deprecated + # `by extra(...)` delegate needing migration, or a non-snapshot/release version + # whose increment is not a simple +1. The skill also builds and commits the + # reports, so a re-run of `prep` afterwards no-ops through the rest. + if grep -qE 'versionToPublish.*by[[:space:]]+extra\(' "$f"; then + echo "!! $f uses 'by extra(...)' — run the bump-version skill, then: ./proofread-repo prep $REPO"; exit 3 + fi + local cur; cur="$(version_of < "$f")" + if ! printf '%s' "$cur" | grep -qE 'SNAPSHOT\.[0-9]+$'; then + echo "!! '$cur' is not a snapshot line — run the bump-version skill, then: ./proofread-repo prep $REPO"; exit 3 + fi + + # Common case: snapshot +1, preserving zero-padding. + local pre suf w next + pre="${cur%.*}"; suf="${cur##*.}"; w="${#suf}" + next="$pre.$(printf "%0${w}d" "$((10#$suf + 1))")" + # Escape the current version for use as a sed BRE — its dots are metacharacters — + # so the substitution matches the literal version, not a `2X0X0-...` variant. + local cur_re="${cur//./\\.}" + sed -i '' "s/${cur_re}/${next}/" "$f" + git commit -q -m "Bump version -> \`${next}\`" -- "$f" + echo "-- bumped $cur -> $next" +} + +generate_reports() { + [ -d docs/dependencies ] || return 0 + branch_touched docs/dependencies && { echo "-- reports already updated on this branch — skipping build"; return 0; } + # Fix #3: regenerate for real (reflects config/pull dep-pin updates AND the bump); + # a *clean* build is authoritative — a warm build can emit stale cached values. + # This build also verifies the bump compiles. + ./gradlew clean build dokkaGenerate --console=plain + if [ -n "$(git status --porcelain docs/dependencies/)" ]; then + git add docs/dependencies/ + git commit -q -m "Update dependency reports" + fi +} + +prep() { + ensure_branch + pull_shared + bump_version + generate_reports + cat < commit the prose fixes + 3. Run the pre-pr skill -> reviewers + writes .git/pre-pr.ok +Then: ./proofread-repo ship $REPO +EOF +} + +ship() { + local gitdir head s_head s_status + gitdir="$(git rev-parse --absolute-git-dir)" + head="$(git rev-parse HEAD)" + s_head="$(sed -n 's/^head=//p' "$gitdir/pre-pr.ok" 2>/dev/null || true)" + s_status="$(sed -n 's/^status=//p' "$gitdir/pre-pr.ok" 2>/dev/null || true)" + [ "$s_status" = "PASS" ] && [ "$s_head" = "$head" ] \ + || { echo "!! no PASS pre-pr sentinel for HEAD $head — run the pre-pr skill first"; exit 1; } + + git checkout -- docs/dependencies/ 2>/dev/null || true # drop any post-build report noise + + # Re-run safety: if a PR already exists, just push any new commits to it. + if gh pr view "$BRANCH" --json url >/dev/null 2>&1; then + git push -q origin "$BRANCH" + echo "-- PR already open: $(gh pr view "$BRANCH" --json url --jq .url)" + return 0 + fi + git push -u origin "$BRANCH" + gh pr create --base "$BASE" --head "$BRANCH" \ + --title "Proofread comments and documentation" \ + --body-file "$SUMMIT/docs/rollout/proofread-pr-body.md" +} + +case "$MODE" in + prep) prep ;; + ship) ship ;; + *) echo "usage: proofread-repo "; exit 2 ;; +esac