Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .githooks/commit-msg
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ set -eu

ROOTFORM_REPO_ROOT=$(git rev-parse --show-toplevel)
cd "$ROOTFORM_REPO_ROOT"
bun scripts/check-publication.ts --message-file "$1"
exec bun scripts/check-commit-message.ts --file "$1"
1 change: 1 addition & 0 deletions .githooks/pre-commit
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ set -eu

ROOTFORM_REPO_ROOT=$(git rev-parse --show-toplevel)
cd "$ROOTFORM_REPO_ROOT"
bun scripts/check-publication.ts --staged
exec bun scripts/check-staged.ts
1 change: 1 addition & 0 deletions .githooks/pre-push
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ set -eu

ROOTFORM_REPO_ROOT=$(git rev-parse --show-toplevel)
cd "$ROOTFORM_REPO_ROOT"
bun scripts/check-publication.ts
exec bun scripts/pre-push.ts
28 changes: 4 additions & 24 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -1,27 +1,7 @@
<!-- required: replace every prompt and remove this comment -->
## Change

## Spec and outcome
Describe the outcome and its accepted specification or repository scope. Include compatibility or security effects when relevant.

- Spec or repository-only rationale:
- Accepted outcome:
- Explicit non-goals:
## Validation

## Done when and evidence

- Done when command or protocol:
- Evidence path or exact output:
- Full gate: `bun run verify`

## Risk and privacy

- Security/privacy impact:
- Offline and determinism impact:
- Dependency or toolchain change:
- Rollback path:

## Review checklist

- [ ] Scope matches accepted spec or repository-only rationale.
- [ ] Success, boundary, failure, and regression paths have evidence proportional to risk.
- [ ] No secret, real Terraform material, private AI material, or generated noise enters Git.
- [ ] Primary reviewer inspected final diff and recorded remaining limits.
List commands and results that verify this change.
7 changes: 7 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,13 @@ jobs:
with:
bun-version: 1.4.0

- name: Validate public contribution text
env:
PUBLIC_TITLE: ${{ github.event.pull_request.title }}
PUBLIC_BODY: ${{ github.event.pull_request.body }}
BASE_SHA: ${{ github.event.pull_request.base.sha }}
run: bun scripts/check-publication.ts --metadata-env --range "$BASE_SHA..HEAD"

- name: Install exact foundation dependencies
run: bun install --frozen-lockfile

Expand Down
29 changes: 7 additions & 22 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,28 +63,13 @@ Before changing anything:

Full normative wording lives in `docs/constitution.md`.

## AI collaboration

- Primary agent owns plan, cross-cutting decisions, integration, final review,
and completion claim.
- Prefer doing the work in the primary agent when it already holds the
necessary context.
- Delegate only self-contained tasks with non-overlapping scopes; use subagents
primarily for bounded investigation, isolated implementation, or independent
verification.
- Never ask multiple subagents to independently read the same broad set of
files. Provide distilled context in the task instead.
- Do not delegate simple edits, mechanical refactors, or work touching tightly
coupled files.
- On any 429 or rate-limit failure, do not spawn replacement agents; continue
sequentially in the primary agent.
- Assign one writer per file. Parallel writers must own disjoint paths.
- Every subtask returns evidence: paths and lines, commands run, observed
output, limitations, and changed files.
- Primary agent rereads the final diff and reruns gates. Subagent claims are
not proof.
- Never expose private prompts, transcripts, routing configuration, memory, or
credentials in commits or logs.
## Public contribution boundary

Keep private notes, prompts, research and session reports outside this checkout.
Public files, commits, pull requests and reports describe product behavior,
contributor commands, validation and required technical provenance. Validate
messages before sending them; diagnostics never reproduce sensitive values.
Configure `git config core.hooksPath .githooks` before contributing.

## Toolchain and dependencies

Expand Down
267 changes: 134 additions & 133 deletions dist/shared/index.js

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions docs/adr/006-github-native-actions.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
- Status: Accepted
- Date: 2026-10-02
- Owner: @soulbah
- Owner approval: @soulbah — 2026-10-02 (complete public Action redesign and delegated research; architecture and final validation owned by lead)
- Owner approval: @soulbah — 2026-10-02 (complete public Action redesign and reviewed protocol documentation; architecture and final validation owned by lead)
- Related spec: SPEC-006
- Supersedes: ADR-002 through ADR-005 for active runtime behavior

Expand All @@ -26,7 +26,7 @@ Vault transports remote credentials; golangci-lint combines install/lint/cache;
Checkov exposes many scan/failure/platform settings. Their mutable version
selectors, credentials and infrastructure execution do not fit Rootform.
An inventory of documentation is not a claim that their entire implementations
were audited. The retained design below is a lead decision, not delegated
were audited. The retained design below is a maintainer decision, not delegated
architecture.

## Decision
Expand Down
14 changes: 13 additions & 1 deletion scripts/check-commit-range.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#!/usr/bin/env bun

import { assertPublicMessage } from "../src/publication-safety.ts";
import { validateCommitSubject } from "./lib/commit-message.ts";
import { git, nullSeparated, repositoryRoot } from "./lib/git.ts";

Expand All @@ -18,9 +19,20 @@ if (result.exitCode !== 0) {
}

const errors: string[] = [];
const messages = git(["log", "-z", "--format=%B", `${base}..${head}`], root);
if (messages.exitCode !== 0) {
console.error("Cannot inspect contribution commit messages");
process.exit(1);
}
try {
for (const message of nullSeparated(messages.stdout)) assertPublicMessage(message);
} catch {
console.error("Publication refused: unsafe contribution commit message");
process.exit(1);
}
for (const subject of nullSeparated(result.stdout)) {
const validation = validateCommitSubject(subject);
if (!validation.valid) errors.push(`${subject}: ${validation.reason}`);
if (!validation.valid) errors.push(validation.reason);
}
if (errors.length > 0) {
console.error(errors.map((error) => `- ${error}`).join("\n"));
Expand Down
108 changes: 108 additions & 0 deletions scripts/check-publication.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
#!/usr/bin/env bun
import { execFileSync } from "node:child_process";
import { lstatSync, readFileSync, realpathSync } from "node:fs";
import { join, resolve } from "node:path";
import { assertPublicMessage, publicationIssues } from "../src/publication-safety.ts";

export function trackedPublicationIssues(
directory: string,
revision?: string,
): Array<{ path: string; rule: string; line: number }> {
const git = (args: string[]) => {
try {
return execFileSync("git", args, {
cwd: directory,
maxBuffer: 64 * 1024 * 1024,
stdio: ["ignore", "pipe", "pipe"],
});
} catch {
throw new Error("Publication refused: cannot read tracked Git content");
}
};
const names = git(
revision && revision !== ":"
? ["ls-tree", "-r", "--name-only", "-z", revision]
: ["ls-files", "-z"],
)
.toString()
.split("\0")
.filter(Boolean);
return names.flatMap((path) => {
const full = join(directory, path);
let body: Buffer;
if (revision) body = git(["show", `${revision === ":" ? "" : revision}:${path}`]);
else {
try {
const stat = lstatSync(full);
if (stat.isSymbolicLink()) {
const target = realpathSync(full);
if (
path.startsWith(".claude/skills/") &&
target.startsWith(`${resolve(directory, ".agents/skills")}/`)
)
return [];
return [{ path, rule: "symlink", line: 1 }];
}
body = readFileSync(full);
} catch (error) {
if ((error as NodeJS.ErrnoException).code === "ENOENT") return [];
throw error;
}
}
// Binary metadata is included; a tracked path is never skipped because it is ignored.
return publicationIssues(body.toString("utf8")).map((issue) => ({ path, ...issue }));
});
}

export function checkPublication(args: string[]): void {
const option = (name: string) => {
const index = args.indexOf(name);
return index < 0 ? undefined : args[index + 1];
};
const directory = resolve(option("--repo") ?? process.cwd());
const git = (args: string[]) => {
try {
return execFileSync("git", args, {
cwd: directory,
maxBuffer: 64 * 1024 * 1024,
stdio: ["ignore", "pipe", "pipe"],
}).toString();
} catch {
throw new Error("Publication refused: cannot inspect Git history");
}
};
const message = option("--message-file");
if (message) assertPublicMessage(readFileSync(message, "utf8"));
if (args.includes("--metadata-env"))
assertPublicMessage([process.env.PUBLIC_TITLE ?? "", process.env.PUBLIC_BODY ?? ""]);
const issues = message
? []
: trackedPublicationIssues(directory, args.includes("--staged") ? ":" : undefined);
const range = option("--range");
if (range) {
for (const commit of git(["rev-list", range]).trim().split("\n").filter(Boolean)) {
assertPublicMessage(git(["show", "-s", "--format=%B", commit]));
issues.push(...trackedPublicationIssues(directory, commit));
}
}
const issue = issues[0];
if (issue) {
// A malicious filename can itself contain sensitive text.
const location = publicationIssues(issue.path).length ? "tracked file" : issue.path;
throw new Error(`Publication refused: ${issue.rule} (${location}:${issue.line})`);
}
}

if (import.meta.main) {
try {
checkPublication(process.argv.slice(2));
} catch (error) {
const message = error instanceof Error ? error.message : "";
console.error(
/^(?:Publication|Public message) refused: /u.test(message)
? message
: "Publication refused: cannot inspect contribution",
);
process.exit(1);
}
}
59 changes: 22 additions & 37 deletions scripts/lib/pr-metadata.test.ts
Original file line number Diff line number Diff line change
@@ -1,55 +1,40 @@
import { describe, expect, test } from "bun:test";
import { validatePullRequestMetadata } from "./pr-metadata.ts";

const validBody = `## Spec and outcome
const validBody = `## Change

- Spec or repository-only rationale: repository foundation
- Accepted outcome: shared checks run locally and in CI
- Explicit non-goals: no product code
Repository tooling validates contributions before publication.

## Done when and evidence
## Validation

- Done when command or protocol: bun run verify exits 0
- Evidence path or exact output: CI quality job

## Risk and privacy

- Security/privacy impact: private PRD stays ignored
- Offline and determinism impact: no product runtime
- Dependency or toolchain change: exact foundation tooling only
- Rollback path: revert this pull request

## Review checklist

- [x] one
- [x] two
- [x] three
- [x] four
bun run verify passed.
`;

describe("pull request metadata", () => {
test("accepts completed template", () => {
test("accepts concise change and validation", () => {
expect(
validatePullRequestMetadata("chore: establish repository foundation", validBody).errors,
validatePullRequestMetadata("fix: validate public contributions", validBody).errors,
).toEqual([]);
});

test("rejects empty fields and checklist", () => {
const body = validBody.replace("repository foundation", "").replaceAll("[x]", "[ ]");
const errors = validatePullRequestMetadata(
"chore: establish repository foundation",
body,
).errors.join("\n");
expect(errors).toContain("Spec or repository-only rationale");
expect(errors).toContain("review checklist");
});

test("rejects unresolved template marker", () => {
test("rejects absent validation and template placeholders", () => {
expect(
validatePullRequestMetadata(
"chore: establish repository foundation",
`${validBody}\n<!-- required: x -->`,
"fix: validate public contributions",
"## Change\n\nImproved checks.",
).errors,
).toContain("missing pull request section: Validation");
expect(
validatePullRequestMetadata("fix: validate public contributions", `${validBody}\nTODO`)
.errors,
).not.toEqual([]);
});
test("refuses unsafe text without reproducing it", () => {
const value = "/" + "Users/fixture/private";
const errors = validatePullRequestMetadata(
"fix: validate public contributions",
`${validBody}\n${value}`,
).errors;
expect(errors).toEqual(["unsafe public contribution text"]);
expect(errors.join(" ")).not.toContain(value);
});
});
52 changes: 14 additions & 38 deletions scripts/lib/pr-metadata.ts
Original file line number Diff line number Diff line change
@@ -1,50 +1,26 @@
import { assertPublicMessage } from "../../src/publication-safety.ts";
import { validateCommitSubject } from "./commit-message.ts";

export type PullRequestValidation = { errors: string[] };

const REQUIRED_HEADINGS = [
"## Spec and outcome",
"## Done when and evidence",
"## Risk and privacy",
"## Review checklist",
];

const REQUIRED_FIELDS = [
"Spec or repository-only rationale",
"Accepted outcome",
"Explicit non-goals",
"Done when command or protocol",
"Evidence path or exact output",
"Security/privacy impact",
"Offline and determinism impact",
"Dependency or toolchain change",
"Rollback path",
];

export function validatePullRequestMetadata(title: string, body: string): PullRequestValidation {
const errors: string[] = [];
try {
assertPublicMessage([title, body]);
} catch {
return { errors: ["unsafe public contribution text"] };
}
const titleResult = validateCommitSubject(title);
if (!titleResult.valid) errors.push(`title: ${titleResult.reason}`);

for (const heading of REQUIRED_HEADINGS) {
if (!body.includes(heading)) errors.push(`missing pull request section: ${heading}`);
}
const fields = new Map<string, string>();
for (const line of body.split(/\r?\n/u)) {
if (!line.startsWith("- ")) continue;
const separator = line.indexOf(":", 2);
if (separator === -1) continue;
fields.set(line.slice(2, separator).trim(), line.slice(separator + 1).trim());
for (const heading of ["Change", "Validation"]) {
const section = body.match(new RegExp(`^## ${heading}\\s*\\n([\\s\\S]*?)(?=^## |$)`, "mu"));
if (!section?.[1]?.trim()) errors.push(`missing pull request section: ${heading}`);
}
for (const field of REQUIRED_FIELDS) {
if (!fields.get(field)) errors.push(`missing pull request value: ${field}`);
}
if (/\b(?:TODO|TBD|Pending)\b|<!--\s*required:/u.test(body)) {
if (
/\b(?:TODO|TBD|Pending)\b|<!--\s*required:|Describe the outcome and|List commands and results/u.test(
body,
)
)
errors.push("pull request body contains unresolved placeholder");
}
if ((body.match(/^- \[[xX]\] /gmu) ?? []).length < 4) {
errors.push("review checklist must be completed");
}

return { errors };
}
Loading
Loading