From e721e08468490b5cf77a53cb5c58ed89182c4515 Mon Sep 17 00:00:00 2001 From: Pheidon Date: Fri, 8 May 2026 16:32:05 +0000 Subject: [PATCH 1/6] Align flow governance projection with baseline labels --- src/archetypes.ts | 4 ++-- src/manifest.ts | 35 +++++++++++++++++++++++------------ tests/render.test.ts | 8 +++++++- 3 files changed, 32 insertions(+), 15 deletions(-) diff --git a/src/archetypes.ts b/src/archetypes.ts index bab81d0..df7bbcd 100644 --- a/src/archetypes.ts +++ b/src/archetypes.ts @@ -407,7 +407,7 @@ function pullRequestTemplate(manifest: BootstrapManifest): string { ## Governing Issue - Closes # + Refs # ## Validation @@ -422,7 +422,7 @@ function pullRequestTemplate(manifest: BootstrapManifest): string { - [ ] Auto-merge is enabled, or GitHub plan-limit evidence 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 diff --git a/src/manifest.ts b/src/manifest.ts index a6926f4..63a5e22 100644 --- a/src/manifest.ts +++ b/src/manifest.ts @@ -55,23 +55,34 @@ export const DEFAULT_FLOW_LABELS: IssueLabelConfig[] = [ { 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: "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({ diff --git a/tests/render.test.ts b/tests/render.test.ts index f3a74d3..2742ebe 100644 --- a/tests/render.test.ts +++ b/tests/render.test.ts @@ -57,7 +57,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 #"); }); } @@ -272,10 +272,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).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"); From 4c48f7c05ae7c066311723b5f2f9da271506f3f6 Mon Sep 17 00:00:00 2001 From: Pheidon Date: Fri, 8 May 2026 18:48:56 +0000 Subject: [PATCH 2/6] Cement author-owned auto-merge policy --- src/archetypes.ts | 10 +++++----- src/manifest.ts | 2 +- tests/render.test.ts | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/archetypes.ts b/src/archetypes.ts index df7bbcd..f9a6f3f 100644 --- a/src/archetypes.ts +++ b/src/archetypes.ts @@ -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 `; } @@ -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 ${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 @@ -1357,8 +1357,8 @@ ${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." + 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' <<<"$PR_BODY"; then + echo "PR body must state that the PR author enabled auto-merge, or explain why auto-merge is unavailable/unsafe." failed=1 fi diff --git a/src/manifest.ts b/src/manifest.ts index 63a5e22..97d138a 100644 --- a/src/manifest.ts +++ b/src/manifest.ts @@ -53,7 +53,7 @@ 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 Apollo/Pheidon planning refinement." }, { name: "state:ready-for-implementation", color: "bc80bd", description: "Issue has enough contract to assign implementation." }, diff --git a/tests/render.test.ts b/tests/render.test.ts index 2742ebe..c5a6f74 100644 --- a/tests/render.test.ts +++ b/tests/render.test.ts @@ -277,7 +277,7 @@ describe("renderManagedFiles", () => { 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"); From bf6e4847d639683ceb2322e1f49d92a43615a8b5 Mon Sep 17 00:00:00 2001 From: Daedalus Date: Fri, 8 May 2026 19:37:25 +0000 Subject: [PATCH 3/6] Accept qualified issue links in PR validation --- src/archetypes.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/archetypes.ts b/src/archetypes.ts index f9a6f3f..cca9f53 100644 --- a/src/archetypes.ts +++ b/src/archetypes.ts @@ -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]?)[[: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 From e3611e15a31ace7796af577d00f8ab85d5a08119 Mon Sep 17 00:00:00 2001 From: John McChesney TenEyck Jr Date: Fri, 8 May 2026 14:52:11 -0500 Subject: [PATCH 4/6] Align PR issue-link validation --- src/archetypes.ts | 2 +- tests/render.test.ts | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/archetypes.ts b/src/archetypes.ts index cca9f53..d08ecb4 100644 --- a/src/archetypes.ts +++ b/src/archetypes.ts @@ -1347,7 +1347,7 @@ ${indentBlock(setupSteps(manifest), 6)} failed=1 fi - if ! grep -Eiq '(^|[[:space:]-])((close[sd]?|fix(e[sd])?|resolve[sd]?)[[: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 + if ! grep -Eiq '(^|[[:space:]-])(((close[sd]?|fix(e[sd])?|resolve[sd]?|refs?)[[:space:]]+|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]+|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 diff --git a/tests/render.test.ts b/tests/render.test.ts index c5a6f74..0ae69f3 100644 --- a/tests/render.test.ts +++ b/tests/render.test.ts @@ -35,6 +35,9 @@ 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("resolve[sd]?|refs?"); + expect(prWorkflow?.contents).toContain("part[[:space:]]+of"); + expect(prWorkflow?.contents).toContain("https://github.com/[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+/issues/[0-9]+"); const prTemplate = files.find((file) => file.path === ".github/PULL_REQUEST_TEMPLATE.md"); const dependabot = files.find((file) => file.path === ".github/dependabot.yml"); From d5c9bcff57b357944238752c44fce271f182cc33 Mon Sep 17 00:00:00 2001 From: Daedalus Date: Fri, 8 May 2026 19:57:18 +0000 Subject: [PATCH 5/6] fix: cover non-closing issue references --- src/archetypes.ts | 2 +- tests/render.test.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/archetypes.ts b/src/archetypes.ts index d08ecb4..7d1c4a5 100644 --- a/src/archetypes.ts +++ b/src/archetypes.ts @@ -1347,7 +1347,7 @@ ${indentBlock(setupSteps(manifest), 6)} failed=1 fi - if ! grep -Eiq '(^|[[:space:]-])(((close[sd]?|fix(e[sd])?|resolve[sd]?|refs?)[[:space:]]+|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]+|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 + 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 diff --git a/tests/render.test.ts b/tests/render.test.ts index 0ae69f3..418a2d4 100644 --- a/tests/render.test.ts +++ b/tests/render.test.ts @@ -35,9 +35,9 @@ 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("resolve[sd]?|refs?"); - expect(prWorkflow?.contents).toContain("part[[:space:]]+of"); - expect(prWorkflow?.contents).toContain("https://github.com/[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+/issues/[0-9]+"); + 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/"); const prTemplate = files.find((file) => file.path === ".github/PULL_REQUEST_TEMPLATE.md"); const dependabot = files.find((file) => file.path === ".github/dependabot.yml"); From 630684474b2460f11222dce7fb03a433e0aec145 Mon Sep 17 00:00:00 2001 From: John McChesney TenEyck Jr Date: Fri, 8 May 2026 15:29:28 -0500 Subject: [PATCH 6/6] Require explicit auto-merge evidence --- src/archetypes.ts | 3 ++- tests/render.test.ts | 40 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/src/archetypes.ts b/src/archetypes.ts index 7d1c4a5..21efbba 100644 --- a/src/archetypes.ts +++ b/src/archetypes.ts @@ -1357,7 +1357,8 @@ ${indentBlock(setupSteps(manifest), 6)} failed=1 fi - 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' <<<"$PR_BODY"; then + 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 diff --git a/tests/render.test.ts b/tests/render.test.ts index 418a2d4..1811426 100644 --- a/tests/render.test.ts +++ b/tests/render.test.ts @@ -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; @@ -38,6 +48,8 @@ describe("renderManagedFiles", () => { 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"); @@ -105,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: {