Skip to content
Open
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
17 changes: 9 additions & 8 deletions src/archetypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -395,7 +395,7 @@ function flowPullRequestSection(manifest: BootstrapManifest): string {
- [ ] Every blocker has a next actor and next action
- [ ] No active blocking requested changes remain
- [ ] Non-author approval is present when required
- [ ] Auto-merge is appropriate when gates pass
- [ ] PR author enabled auto-merge where GitHub allows it, or recorded why it is unavailable/unsafe
`;
}

Expand All @@ -407,7 +407,7 @@ function pullRequestTemplate(manifest: BootstrapManifest): string {

## Governing Issue

Closes #
Refs #<issue-number> <!-- use Closes/Fixes/Resolves only when this PR fully completes the issue; otherwise use Refs/Part of, owner/repo#123, a full GitHub issue URL, or explain why no issue is linked -->

## Validation

Expand All @@ -419,14 +419,14 @@ function pullRequestTemplate(manifest: BootstrapManifest): string {

- [ ] Changes are scoped to the linked issue
- [ ] Contributor or PR guidance changes are reflected in \`CONTRIBUTING.md\`, \`.github/PULL_REQUEST_TEMPLATE.md\`, and \`docs/bootstrap/onboarding.md\` when applicable
- [ ] Auto-merge is enabled, or GitHub plan-limit evidence is recorded and the fallback merge-readiness policy applies
- [ ] PR author enabled auto-merge where GitHub allows it, or GitHub plan-limit evidence/unavailable reason is recorded and the fallback merge-readiness policy applies
- [ ] No real secrets, runtime auth, or machine-local env files are committed

${flowPullRequestSection(manifest)}
${indentBlock(flowPullRequestSection(manifest), 4)}

## Merge Automation

- [ ] Auto-merge is enabled, or the reason it is unavailable or unsafe is noted below
- [ ] PR author enabled auto-merge with \`gh pr merge --auto --squash\`, or the reason it is unavailable/unsafe is noted below

## Notes

Expand Down Expand Up @@ -1347,7 +1347,7 @@ ${indentBlock(setupSteps(manifest), 6)}
failed=1
fi

if ! grep -Eiq '(^|[[:space:]-])((close[sd]?|fix(e[sd])?|resolve[sd]?)[[:space:]]+#[0-9]+|no issue is linked|no linked issue|without a linked issue|no governing issue)' <<<"$PR_BODY"; then
if ! grep -Eiq '(^|[[:space:]-])(((close[sd]?|fix(e[sd])?|resolve[sd]?|refs?|part[[:space:]]+of)[[:space:]]+)?(#|[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+#|https://github\\.com/[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+/issues/)[0-9]+|no issue is linked|no linked issue|without a linked issue|no governing issue)' <<<"$PR_BODY"; then
echo "PR body must close/link an issue or explicitly explain why no issue is linked."
failed=1
fi
Expand All @@ -1357,8 +1357,9 @@ ${indentBlock(setupSteps(manifest), 6)}
failed=1
fi

if ! grep -Eiq '(auto-merge is enabled|auto-merge enabled|auto merge is enabled|auto merge enabled|auto-merge.*(unavailable|unsafe|blocked|not supported)|auto merge.*(unavailable|unsafe|blocked|not supported))' <<<"$PR_BODY"; then
echo "PR body must state that auto-merge is enabled or explain why it is unavailable or unsafe."
auto_merge_evidence="$(grep -Eiv '^[[:space:]]*-[[:space:]]+\\[[[:space:]]\\][[:space:]]' <<<"$PR_BODY" || true)"
if ! grep -Eiq 'auto-merge (is )?(enabled|armed)|enabled auto-merge|gh pr merge --auto|auto_merge|auto merge enabled|auto-merge (is )?(unavailable|unsafe|not available|not safe)|plan-limit|fallback merge-readiness' <<<"$auto_merge_evidence"; then
echo "PR body must state that the PR author enabled auto-merge, or explain why auto-merge is unavailable/unsafe."
failed=1
fi

Expand Down
37 changes: 24 additions & 13 deletions src/manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,25 +53,36 @@ export const DEFAULT_FLOW_LABELS: IssueLabelConfig[] = [
{ name: "lane:daedalus", color: "80b1d3", description: "Implementation and substantive code repair work." },
{ name: "lane:hephaestus", color: "fdb462", description: "CI, build, lockfile, mergeability, and artifact work." },
{ name: "lane:hermes", color: "bebada", description: "macOS/platform-native or special execution work." },
{ name: "lane:pheidon", color: "b3de69", description: "Orchestration, gate, governance, and controller action." },
{ name: "lane:pheidon", color: "b3de69", description: "Orchestration, gate, governance, and explicit controller action." },
{ name: "state:intake", color: "d9d9d9", description: "Captured but not yet planned." },
{ name: "state:ready-for-planning", color: "ccebc5", description: "Ready for planning refinement." },
{ name: "state:ready-for-implementation", color: "bc80bd", description: "Ready for assigned implementation." },
{ name: "state:needs-review", color: "ffffb3", description: "Needs review before advancing." },
{ name: "state:needs-repair", color: "fb8072", description: "Needs repair before advancing." },
{ name: "state:ready-for-planning", color: "ccebc5", description: "Ready for Apollo/Pheidon planning refinement." },
{ name: "state:ready-for-implementation", color: "bc80bd", description: "Issue has enough contract to assign implementation." },
{ name: "state:implementing", color: "80b1d3", description: "Worker lane is actively implementing." },
{ name: "state:needs-review", color: "ffffb3", description: "PR needs review." },
{ name: "state:needs-repair", color: "fb8072", description: "PR or issue needs repair before it can advance." },
{ name: "state:repairing", color: "fdb462", description: "Repair is actively assigned." },
{ name: "state:ready-for-approval", color: "b3de69", description: "Pheidon/gate approval is next." },
{ name: "state:waiting-checks", color: "ffffb3", description: "Waiting on checks or merge queue." },
{ name: "state:auto-merge-armed", color: "b3de69", description: "Auto-merge is enabled." },
{ name: "state:ready-for-approval", color: "b3de69", description: "Pheidon/gate approval is the next action." },
{ name: "state:waiting-checks", color: "ffffb3", description: "Approved or ready but waiting on checks/merge queue." },
{ name: "state:auto-merge-armed", color: "b3de69", description: "Auto-merge is enabled and GitHub gates own completion." },
{ name: "state:blocked-human", color: "e41a1c", description: "Human decision required." },
{ name: "state:blocked-infra", color: "984ea3", description: "Infrastructure/tooling/auth blocker." },
{ name: "state:blocked-scope", color: "ff7f00", description: "Scope or acceptance criteria blocker." },
{ name: "state:blocked-infra", color: "984ea3", description: "Blocked by tool, auth, runner, or infrastructure failure." },
{ name: "state:blocked-scope", color: "ff7f00", description: "Blocked by unclear scope or acceptance criteria." },
{ name: "state:paused", color: "999999", description: "Intentionally paused." },
{ name: "autonomy:observe", color: "d9d9d9", description: "Class 0; observe only." },
{ name: "autonomy:safe", color: "b3de69", description: "Class 1; safe autonomous work allowed." },
{ name: "autonomy:review-gated", color: "ffffb3", description: "Class 2; review-gated autonomous work." },
{ name: "autonomy:human-required", color: "fb8072", description: "Class 3; human decision required." },
{ name: "autonomy:forbidden-unattended", color: "000000", description: "Class 4; forbidden unattended." }
{ name: "autonomy:review-gated", color: "ffffb3", description: "Class 2; autonomous work allowed, review/gate required." },
{ name: "autonomy:human-required", color: "fb8072", description: "Class 3; human decision required before action/merge." },
{ name: "autonomy:forbidden-unattended", color: "000000", description: "Class 4; must not run unattended." },
{ name: "kind:feature", color: "80b1d3", description: "Feature/product behavior work." },
{ name: "kind:bug", color: "fb8072", description: "Bug fix." },
{ name: "kind:test", color: "ffffb3", description: "Test/validation work." },
{ name: "kind:ci", color: "fdb462", description: "CI/build/tooling work." },
{ name: "kind:docs", color: "ccebc5", description: "Documentation work." },
{ name: "kind:governance", color: "bc80bd", description: "Policy, flow, bootstrap, or governance work." },
{ name: "priority:p0", color: "e41a1c", description: "Critical/urgent." },
{ name: "priority:p1", color: "ff7f00", description: "High priority." },
{ name: "priority:p2", color: "ffff33", description: "Normal priority." },
{ name: "priority:p3", color: "999999", description: "Low priority/backlog." }
];

const environmentSchema = z.object({
Expand Down
53 changes: 51 additions & 2 deletions tests/render.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,16 @@ import { describe, expect, it } from "vitest";
import { renderManagedFiles } from "../src/archetypes.js";
import { normalizeManifest } from "../src/manifest.js";

const autoMergeEvidencePattern =
/auto-merge (is )?(enabled|armed)|enabled auto-merge|gh pr merge --auto|auto_merge|auto merge enabled|auto-merge (is )?(unavailable|unsafe|not available|not safe)|plan-limit|fallback merge-readiness/i;

function autoMergeEvidenceLines(body: string): string {
return body
.split(/\r?\n/)
.filter((line) => !/^\s*-\s+\[\s\]\s/.test(line))
.join("\n");
}

describe("renderManagedFiles", () => {
const archetypes = ["nextjs-web", "node-ts-service", "python-service", "generic-empty"] as const;

Expand Down Expand Up @@ -35,6 +45,11 @@ describe("renderManagedFiles", () => {
expect(prWorkflow?.contents).toContain("['self-hosted', 'synology'");
expect(prWorkflow?.contents).toContain("validate-pr-description:");
expect(prWorkflow?.contents).toContain("PR body must close/link an issue");
expect(prWorkflow?.contents).toContain("refs?|part[[:space:]]+of");
expect(prWorkflow?.contents).toContain("[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+#");
expect(prWorkflow?.contents).toContain("https://github\\.com/[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+/issues/");
expect(prWorkflow?.contents).toContain("auto_merge_evidence=");
expect(prWorkflow?.contents).toContain('<<<"$auto_merge_evidence"');

const prTemplate = files.find((file) => file.path === ".github/PULL_REQUEST_TEMPLATE.md");
const dependabot = files.find((file) => file.path === ".github/dependabot.yml");
Expand All @@ -57,7 +72,7 @@ describe("renderManagedFiles", () => {
const contributing = files.find((file) => file.path === "CONTRIBUTING.md");
expect(contributing?.contents).toContain("Use `.github/PULL_REQUEST_TEMPLATE.md`");

expect(prTemplate?.contents).toContain("Closes #");
expect(prTemplate?.contents).toContain("Refs #<issue-number>");
});
}

Expand Down Expand Up @@ -102,6 +117,34 @@ describe("renderManagedFiles", () => {
expect(prWorkflow?.contents).not.toContain("name: CI Gate");
});

it("does not accept untouched auto-merge checklist text as merge automation evidence", () => {
const manifest = normalizeManifest({
project: {
name: "merge-gated-repo",
owner: "acme"
},
archetype: {
kind: "generic-empty"
}
});

const files = renderManagedFiles(manifest);
const prTemplate = files.find((file) => file.path === ".github/PULL_REQUEST_TEMPLATE.md");
const templateBody = prTemplate?.contents ?? "";

expect(autoMergeEvidencePattern.test(templateBody)).toBe(true);
expect(autoMergeEvidencePattern.test(autoMergeEvidenceLines(templateBody))).toBe(false);

const bodyWithAuthorStatement = `${templateBody}\nAuto-merge is unavailable because review is pending.`;
expect(autoMergeEvidencePattern.test(autoMergeEvidenceLines(bodyWithAuthorStatement))).toBe(true);

const bodyWithCheckedChecklist = templateBody.replace(
"- [ ] PR author enabled auto-merge with",
"- [x] PR author enabled auto-merge with"
);
expect(autoMergeEvidencePattern.test(autoMergeEvidenceLines(bodyWithCheckedChecklist))).toBe(true);
});

it("uses the display name in docs while keeping the repository slug visible", () => {
const manifest = normalizeManifest({
project: {
Expand Down Expand Up @@ -272,10 +315,16 @@ describe("renderManagedFiles", () => {
const blocker = files.find((file) => file.path === ".github/ISSUE_TEMPLATE/flow_blocker.yml");

expect(manifest.github.issueLabels.some((label) => label.name === "state:needs-repair")).toBe(true);
expect(manifest.github.issueLabels.some((label) => label.name === "state:implementing")).toBe(true);
expect(manifest.github.issueLabels.some((label) => label.name === "lane:daedalus")).toBe(true);
expect(manifest.github.issueLabels.some((label) => label.name === "kind:governance")).toBe(true);
expect(manifest.github.issueLabels.some((label) => label.name === "priority:p2")).toBe(true);
expect(prTemplate?.contents).toContain("\n## Flow Contract\n");
expect(prTemplate?.contents).toContain("\n- [ ] Auto-merge is appropriate when gates pass");
expect(prTemplate?.contents).toContain("\n- [ ] PR author enabled auto-merge where GitHub allows it");
expect(prTemplate?.contents).toMatch(/^## Summary/m);
expect(prTemplate?.contents).toMatch(/^## Merge Automation/m);
expect(prTemplate?.contents).not.toContain(" ## Flow Contract");
expect(prTemplate?.contents).not.toContain(" ## Summary");
expect(implementation?.contents).toContain("Autonomy class");
expect(implementation?.contents).toContain("Recommended lane");
expect(blocker?.contents).toContain("Required unblock action");
Expand Down
Loading