@next-friday/create-app 9.3.0 → 9.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/package.json +1 -1
- package/templates/base/SETUP.md +1 -2
- package/templates/base/plugins/sdlc/claude-code/skills/sdlc-mode/SKILL.md +1 -1
- package/templates/base/plugins/sdlc/claude-code/skills/sdlc-mode/playbooks/build.md +4 -4
- package/templates/base/plugins/sdlc/claude-code/skills/sdlc-mode/playbooks/design.md +3 -3
- package/templates/base/plugins/sdlc/claude-code/skills/sdlc-mode/playbooks/fix.md +2 -2
- package/templates/base/plugins/sdlc/claude-code/skills/sdlc-mode/playbooks/plan.md +3 -3
- package/templates/base/plugins/sdlc/cursor/skills/sdlc-mode/SKILL.md +1 -1
- package/templates/base/plugins/sdlc/cursor/skills/sdlc-mode/playbooks/build.md +4 -4
- package/templates/base/plugins/sdlc/cursor/skills/sdlc-mode/playbooks/design.md +3 -3
- package/templates/base/plugins/sdlc/cursor/skills/sdlc-mode/playbooks/fix.md +2 -2
- package/templates/base/plugins/sdlc/cursor/skills/sdlc-mode/playbooks/plan.md +3 -3
- package/templates/base/scripts/check-plan.ts +397 -70
- package/templates/base/.cursor/BUGBOT.md +0 -23
package/README.md
CHANGED
|
@@ -32,7 +32,7 @@ Every stack is built from [`templates/base`](templates/base), which holds the sh
|
|
|
32
32
|
- **Git hooks:** Husky runs gitleaks, `pnpm rules:check`, and lint-staged before each commit, commitlint on each message, and `pnpm check` before each push. After the first commit, commits to `main` are blocked, and local merges into `main` always are; pushes to `main` are blocked once it exists on the remote.
|
|
33
33
|
- **CI:** GitHub Actions run the static checks, tests with patch coverage, the build, end-to-end tests for Next.js and Payload, a container build and vulnerability scan, Semgrep, OSV-Scanner, CodeQL, dependency review, OpenSSF Scorecard, gitleaks, TruffleHog, zizmor, and pull request title validation. Dependabot keeps dependencies current.
|
|
34
34
|
- **Deployment:** a `Dockerfile` that runs as the `node` user, and a Railway workflow that promotes one commit through `dev`, `uat`, and `prod`.
|
|
35
|
-
- **Agent rules:** `CLAUDE.md` imports `AGENTS.md`. Shared rules live in `.agents/rules`, with hooks that enforce them in Claude Code and Cursor
|
|
35
|
+
- **Agent rules:** `CLAUDE.md` imports `AGENTS.md`. Shared rules live in `.agents/rules`, with hooks that enforce them in Claude Code and Cursor.
|
|
36
36
|
|
|
37
37
|
## Requirements
|
|
38
38
|
|
package/package.json
CHANGED
package/templates/base/SETUP.md
CHANGED
|
@@ -81,10 +81,9 @@ Replace `security@example.com` in `SECURITY.md` with the contact the security po
|
|
|
81
81
|
|
|
82
82
|
**Who:** a Cursor team admin.
|
|
83
83
|
|
|
84
|
-
- **Bugbot:** enable the repository at [Bugbot](https://cursor.com/automations/from-cursor/bugbot). Bugbot reviews pull requests by the files `.cursor/BUGBOT.md` links, including `.agents/review.md`.
|
|
85
84
|
- **Sandbox:** set the team's sandbox policy ([Cursor sandbox](https://cursor.com/docs/reference/sandbox)), which repository and user settings cannot weaken.
|
|
86
85
|
|
|
87
|
-
**Check:** the
|
|
86
|
+
**Check:** the team's settings in Cursor show the sandbox policy.
|
|
88
87
|
|
|
89
88
|
## 6. Team decisions
|
|
90
89
|
|
|
@@ -14,7 +14,7 @@ Do these steps in order.
|
|
|
14
14
|
| :---------------------------------------------------------------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------- |
|
|
15
15
|
| A question, or a request to explain something without changing anything | Follow [`playbooks/question.md`](playbooks/question.md). |
|
|
16
16
|
| A spec for an intent, or a path to an `intent.md` | Follow [`playbooks/design.md`](playbooks/design.md). |
|
|
17
|
-
| A build for a spec, a path to a `spec.md`, or a plan the engineer
|
|
17
|
+
| A build for a spec, a path to a `spec.md`, or a plan the engineer merged | Follow [`playbooks/build.md`](playbooks/build.md). |
|
|
18
18
|
| A review or check of a pull request or a branch, a failing check on a pull request, or a pull request to drive to green | Follow [`playbooks/review.md`](playbooks/review.md). |
|
|
19
19
|
| A deploy, a promotion to an environment, or a release to production | Follow [`playbooks/deploy.md`](playbooks/deploy.md). |
|
|
20
20
|
| A finding sent by a job, a monitor, or a trigger, or one the message says no person can answer questions about | Follow [`playbooks/maintain.md`](playbooks/maintain.md). |
|
|
@@ -5,11 +5,11 @@ This playbook turns a merged spec into a plan the engineer accepts, and then int
|
|
|
5
5
|
1. Find the change: the `intent/<change>/` folder whose `spec.md` or `plan.md` the request names. Its `spec.md` must be on the default branch beside its `intent.md`. In each case below, say only that, in two or three lines, and stop: write no file.
|
|
6
6
|
- The request names no change, or the folder has no `spec.md` on any branch: list each `intent/*/` on the default branch that has a `spec.md` and no `plan.md`, or say that there is none.
|
|
7
7
|
- The spec exists only on another branch: say that the spec is not merged yet, and name that branch.
|
|
8
|
-
- `intent/<change>/plan.md` is
|
|
8
|
+
- `intent/<change>/plan.md` is on the default branch and `spec.md` says `Status: done.`: say that the change is built, and name both files.
|
|
9
9
|
|
|
10
10
|
When the request is a bug fix with no spec, none of these cases applies: follow `playbooks/fix.md` instead.
|
|
11
11
|
|
|
12
|
-
2. When `intent/<change>/plan.md`
|
|
12
|
+
2. When `intent/<change>/plan.md` is on the default branch, the engineer accepted it by merging its pull request: go to step 6. When it exists only on a branch that is not merged, check out that branch, show that plan, and go to step 5.
|
|
13
13
|
3. Read `intent.md`, `spec.md`, the four policies, found as `playbooks/design.md` step 2 says, and the code the change touches. Start from the accepted spec, in the agent tool's plan mode when it has one, and give the plan the change's `intent.md` and `spec.md`. Then interview the engineer until they have said four things: the files the change touches, the order of the work, the tests that prove it, and each option they rule out.
|
|
14
14
|
- **When all four are already said, in the request or in the files, go to step 4 in the same turn.**
|
|
15
15
|
- **When one is missing, ask and stop.** Write no plan in that turn. Ask in rounds, as step 3 of `playbooks/plan.md` does: numbered questions, each with a recommended answer or "No recommendation". Wait for the answers, and build the next round on them.
|
|
@@ -21,8 +21,8 @@ This playbook turns a merged spec into a plan the engineer accepts, and then int
|
|
|
21
21
|
|
|
22
22
|
Show the whole plan and name the file only as `intent/<change>/plan.md`. In plan mode you cannot write the file. Show the plan as the plan.
|
|
23
23
|
|
|
24
|
-
5. **Gate: the engineer accepts the plan.**
|
|
25
|
-
6.
|
|
24
|
+
5. **Gate: the engineer accepts the plan.** Commit `intent/<change>/plan.md` on a new branch, with the `Accepted by:` line as the template has it. In that commit, set the spec's status line to `Status: accepted.`, because its pull request is merged. Push the branch and open a pull request that changes no file outside `intent/`, as the opening paragraph of `playbooks/open-pr.md` says for branches and pull requests. Revise the plan as the engineer asks; each revision is a commit on that branch. The engineer merges that pull request to accept the plan, or closes it to reject it. You never merge it, and you change no file outside `intent/` before it is merged. In plan mode, the engineer's approval lets you write the file and open this pull request; it is not the acceptance. When the spec classes the change as higher risk, say that a tech lead or an architect approves that pull request before it is merged. Report the pull request's URL, keep it green as `playbooks/review.md` says, and wait.
|
|
25
|
+
6. When the plan's pull request is merged, create a new branch as `playbooks/open-pr.md` says. In its first commit, set the plan's `Accepted by:` line, under its title, to the account that merged that pull request, read from the host. When that pull request has an approving review from a second person, add `Also accepted by: @<handle>.` on the next line. When you cannot read the host, write the handle the engineer gives. Never write a handle the host or the engineer did not give.
|
|
26
26
|
7. Implement the plan in its order. After each step, run the check that proves it and fix what fails. For a bug fix, follow `playbooks/fix.md` steps 7 and 8: commit the failing test on a `fix/` branch, and the gate then blocks test edits. When the work departs from the plan, change `plan.md` in the same commit. When the change meets the last requirement of `spec.md`, set `Status: done.` in `intent.md` and `spec.md`.
|
|
27
27
|
8. Run every check the project's agent instructions name and keep the output. Every check must pass: fix the code, and never skip, delete, or weaken a test. Then run the `verifier` subagent and fix what it reports. Start it on the model that `.agents/sdlc-models.md` names after `verify:`, at the effort on that line when the session can set one; when you cannot start a subagent, do what `../../../agents/verifier.md`, relative to this playbook, says yourself. For UI work, when the session has a browser tool, also close the loop with a visual check: start the app, give the browser tool the page and the mock the spec or the request names, screenshot, compare, and adjust, and repeat until it matches; two or three rounds is normal. When a check cannot run in this session, say which one and why, and never report it as passed.
|
|
28
28
|
9. Go on in the same turn, without asking: follow `playbooks/open-pr.md`, and then `playbooks/review.md`. Pushing the branch and opening the pull request are part of this task.
|
|
@@ -8,14 +8,14 @@ This playbook turns a merged intent into the requirements and design spec.
|
|
|
8
8
|
- `spec.md` already exists: say so and name the file.
|
|
9
9
|
2. Read the four policies, every time, before you write: for each of `brand`, `security`, `compliance`, and `ux`, read the project's `.agents/skills/<area>-policy/SKILL.md` when it exists, and the plugin's default at `../../<area>-policy/SKILL.md`, relative to this playbook, otherwise. Then read the code and the docs the change touches.
|
|
10
10
|
3. Write `intent/<change>/spec.md` from the template below. Use exactly the template's headings.
|
|
11
|
-
- **Request:** under the status line, quote the words of the request that started this spec.
|
|
11
|
+
- **Request:** under the status line, quote the words of the request that started this spec. End the line with `Skills at <hash>.`, the short hash of the last commit that changed the plugin's files in this repository: `git log -1 --format=%h -- plugins/sdlc`. When the plugin is not in this repository, write `Skills at unknown.`
|
|
12
12
|
- **Requirements:** numbered. Each one states a result a test or a reader can check.
|
|
13
13
|
- **Design:** how the change fits the existing code: the files that change, and what each one does afterwards.
|
|
14
14
|
- **Policies:** one line for each of the four policies. State which rules of that policy the change must meet, each as a requirement number, or write "Does not apply" and why.
|
|
15
|
-
- **Areas of concern:** a policy rule the change cannot meet, two policies that contradict, or a decision the files do not settle. Give each one a number and the policy it comes from. Write "None" only when there are none.
|
|
15
|
+
- **Areas of concern:** a policy rule the change cannot meet, two policies that contradict, or a decision the files do not settle. Give each one a number and the policy it comes from. Under each, on a line of its own, write `Decision: <the decision you propose>.` Write "None" only when there are none.
|
|
16
16
|
- **Open questions:** each open question from `intent.md`, answered with its evidence or carried forward. Write "None" only when the intent has none.
|
|
17
17
|
4. Commit `spec.md` on a new branch. In the same commit, set the intent's status line to `Status: accepted.`, because its pull request is merged. Push the branch, and open a pull request ready for review, as the opening paragraph of `playbooks/open-pr.md` says for branches and pull requests. When you cannot open a pull request from this session, say so and give the pushed branch's name.
|
|
18
|
-
5. **Gate: the product owner reviews the spec.** The product owner
|
|
18
|
+
5. **Gate: the product owner reviews the spec.** The product owner corrects the `Decision:` line under each area of concern, with its policy owner, then merges the pull request to accept the spec and those decisions as written, or closes it to reject it. Change a `Decision:` line when they tell you its text. For a change the spec classes as higher risk, the product owner consults a tech lead before accepting. You do none of these. The spec stays `Status: draft.`: never write `Status: accepted.` in this pull request, because the plan's commit sets it. Report the pull request's URL, keep it green as `playbooks/review.md` says, and wait.
|
|
19
19
|
|
|
20
20
|
**Next:** `playbooks/build.md`; an engineer with an agent; when the product owner has merged the spec's pull request and the engineer sends `/sdlc:sdlc-mode build` with the spec's path.
|
|
21
21
|
|
|
@@ -13,8 +13,8 @@ This playbook takes a defect in behaviour that already exists to a pull request.
|
|
|
13
13
|
|
|
14
14
|
Show the whole plan and name the file only as `intent/<short-description>/plan.md`. In plan mode you cannot write the file. Show the plan as the plan.
|
|
15
15
|
|
|
16
|
-
5. **Gate: the engineer accepts the plan.**
|
|
17
|
-
6.
|
|
16
|
+
5. **Gate: the engineer accepts the plan.** Commit `intent/<short-description>/plan.md` on a new branch, with the `Accepted by:` line as the template has it, push it, and open a pull request that changes no file outside `intent/`, as the opening paragraph of `playbooks/open-pr.md` says for branches and pull requests. Revise the plan as the engineer asks; each revision is a commit on that branch. The engineer merges that pull request to accept the plan, or closes it to reject it. You never merge it, and you change no file outside `intent/` before it is merged. In plan mode, the engineer's approval lets you write the file and open this pull request; it is not the acceptance. Report the pull request's URL, keep it green as `playbooks/review.md` says, and wait.
|
|
17
|
+
6. When the plan's pull request is merged, create the branch `fix/<short-description>` as `playbooks/open-pr.md` says. In its first commit, set the plan's `Accepted by:` line, under its title, to the account that merged that pull request, read from the host. When you cannot read the host, write the handle the engineer gives. Never write a handle the host or the engineer did not give.
|
|
18
18
|
7. Reproduce the defect as a test. Run it and check that it fails for the reason the cause gives. Commit the failing test before any fix.
|
|
19
19
|
8. Make the test pass without editing it.
|
|
20
20
|
9. Run every check the project's agent instructions name. Keep the output. When one fails, fix the code, and never skip, delete, or weaken a test. When a check cannot run in this session, say which one and why, and never report it as passed.
|
|
@@ -3,14 +3,14 @@
|
|
|
3
3
|
This playbook writes the intent. The build plan, `intent/<change>/plan.md`, comes later and is not written here.
|
|
4
4
|
|
|
5
5
|
1. Let the originator describe the problem in their own words: what they cannot do today, who is affected, what better looks like, and what is out of scope.
|
|
6
|
-
2. Read the code, the docs, and the existing `intent/` folders. Find the rest yourself: the
|
|
6
|
+
2. Read the code, the docs, and the existing `intent/` folders. Find the rest yourself. The author is the originator's handle on the host: read it from the host when the session shows whose account it runs as, and ask for it otherwise. Never take it from `git config user.name`, which names the machine's user.
|
|
7
7
|
3. Interview until the originator has said four things: the problem, the outcome, the constraints, and what is out of scope. The outcome is what the user would see.
|
|
8
8
|
- **When all four are already said, go to step 4 in the same turn.**
|
|
9
9
|
- **When one is missing, ask and stop.** Write no draft in that turn. Ask in rounds. Each round holds only questions that do not depend on another question in the same round. Number the questions. Each asks one thing and carries a concrete answer you recommend, worded so "yes" accepts it. A recommended answer is a fact or a choice, such as "Recommended: only the checkout page", never an instruction to the person. When you have no answer to recommend, write "No recommendation" and ask plainly. Wait for the answers, and build the next round on them.
|
|
10
10
|
4. Write `intent/<change>/intent.md` from the template below, where `<change>` is a short kebab-case name. Use exactly the template's five headings. What is out of scope goes under **Proposed outcome**. Anything else that is unknown, unverified, or not yet decided goes under **Open questions**. Write "None" only when nothing is, and never list what the originator already answered. Show the whole draft, and name the file only as `intent/<change>/intent.md`. In plan mode you cannot write the file. Show the draft as the plan, and write the file once it is approved.
|
|
11
11
|
5. **Gate: the originator corrects the draft.** Ask whether it matches what they meant, and revise until they say yes. Commit nothing, push nothing, and open no pull request before that yes. In plan mode, the originator approving the plan is that yes.
|
|
12
12
|
6. Commit `intent.md` on a new branch, push it, and open a pull request ready for review, as the opening paragraph of `playbooks/open-pr.md` says for branches and pull requests.
|
|
13
|
-
7. **Gate: the product owner accepts or rejects the intent.** The product owner merges the pull request to accept the intent, or closes it to reject it. You do neither. The file stays `Status: draft.`: never write `Status: accepted.` in this pull request, because the spec's pull request sets it. Report the pull request's URL, keep it green as `playbooks/review.md` says, and wait.
|
|
13
|
+
7. **Gate: the product owner accepts or rejects the intent.** The product owner merges the pull request to accept the intent, or closes it to reject it. You do neither. When the originator is not the person who merges, the originator is the pull request's author or approves it; say so when neither holds. The file stays `Status: draft.`: never write `Status: accepted.` in this pull request, because the spec's pull request sets it. Report the pull request's URL, keep it green as `playbooks/review.md` says, and wait.
|
|
14
14
|
|
|
15
15
|
**Next:** `playbooks/design.md`; an agent; when the product owner has merged the intent's pull request and a person or a job asks for the spec.
|
|
16
16
|
|
|
@@ -19,7 +19,7 @@ This playbook writes the intent. The build plan, `intent/<change>/plan.md`, come
|
|
|
19
19
|
```markdown
|
|
20
20
|
# Intent: <title>
|
|
21
21
|
|
|
22
|
-
Author:
|
|
22
|
+
Author: @<handle> (<team>). Status: draft.
|
|
23
23
|
|
|
24
24
|
## Problem
|
|
25
25
|
|
|
@@ -14,7 +14,7 @@ Do these steps in order.
|
|
|
14
14
|
| :---------------------------------------------------------------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------- |
|
|
15
15
|
| A question, or a request to explain something without changing anything | Follow [`playbooks/question.md`](playbooks/question.md). |
|
|
16
16
|
| A spec for an intent, or a path to an `intent.md` | Follow [`playbooks/design.md`](playbooks/design.md). |
|
|
17
|
-
| A build for a spec, a path to a `spec.md`, or a plan the engineer
|
|
17
|
+
| A build for a spec, a path to a `spec.md`, or a plan the engineer merged | Follow [`playbooks/build.md`](playbooks/build.md). |
|
|
18
18
|
| A review or check of a pull request or a branch, a failing check on a pull request, or a pull request to drive to green | Follow [`playbooks/review.md`](playbooks/review.md). |
|
|
19
19
|
| A deploy, a promotion to an environment, or a release to production | Follow [`playbooks/deploy.md`](playbooks/deploy.md). |
|
|
20
20
|
| A finding sent by a job, a monitor, or a trigger, or one the message says no person can answer questions about | Follow [`playbooks/maintain.md`](playbooks/maintain.md). |
|
|
@@ -5,11 +5,11 @@ This playbook turns a merged spec into a plan the engineer accepts, and then int
|
|
|
5
5
|
1. Find the change: the `intent/<change>/` folder whose `spec.md` or `plan.md` the request names. Its `spec.md` must be on the default branch beside its `intent.md`. In each case below, say only that, in two or three lines, and stop: write no file.
|
|
6
6
|
- The request names no change, or the folder has no `spec.md` on any branch: list each `intent/*/` on the default branch that has a `spec.md` and no `plan.md`, or say that there is none.
|
|
7
7
|
- The spec exists only on another branch: say that the spec is not merged yet, and name that branch.
|
|
8
|
-
- `intent/<change>/plan.md` is
|
|
8
|
+
- `intent/<change>/plan.md` is on the default branch and `spec.md` says `Status: done.`: say that the change is built, and name both files.
|
|
9
9
|
|
|
10
10
|
When the request is a bug fix with no spec, none of these cases applies: follow `playbooks/fix.md` instead.
|
|
11
11
|
|
|
12
|
-
2. When `intent/<change>/plan.md`
|
|
12
|
+
2. When `intent/<change>/plan.md` is on the default branch, the engineer accepted it by merging its pull request: go to step 6. When it exists only on a branch that is not merged, check out that branch, show that plan, and go to step 5.
|
|
13
13
|
3. Read `intent.md`, `spec.md`, the four policies, found as `playbooks/design.md` step 2 says, and the code the change touches. Start from the accepted spec, in the agent tool's plan mode when it has one, and give the plan the change's `intent.md` and `spec.md`. Then interview the engineer until they have said four things: the files the change touches, the order of the work, the tests that prove it, and each option they rule out.
|
|
14
14
|
- **When all four are already said, in the request or in the files, go to step 4 in the same turn.**
|
|
15
15
|
- **When one is missing, ask and stop.** Write no plan in that turn. Ask in rounds, as step 3 of `playbooks/plan.md` does: numbered questions, each with a recommended answer or "No recommendation". Wait for the answers, and build the next round on them.
|
|
@@ -21,8 +21,8 @@ This playbook turns a merged spec into a plan the engineer accepts, and then int
|
|
|
21
21
|
|
|
22
22
|
Show the whole plan and name the file only as `intent/<change>/plan.md`. In plan mode you cannot write the file. Show the plan as the plan.
|
|
23
23
|
|
|
24
|
-
5. **Gate: the engineer accepts the plan.**
|
|
25
|
-
6.
|
|
24
|
+
5. **Gate: the engineer accepts the plan.** Commit `intent/<change>/plan.md` on a new branch, with the `Accepted by:` line as the template has it. In that commit, set the spec's status line to `Status: accepted.`, because its pull request is merged. Push the branch and open a pull request that changes no file outside `intent/`, as the opening paragraph of `playbooks/open-pr.md` says for branches and pull requests. Revise the plan as the engineer asks; each revision is a commit on that branch. The engineer merges that pull request to accept the plan, or closes it to reject it. You never merge it, and you change no file outside `intent/` before it is merged. In plan mode, the engineer's approval lets you write the file and open this pull request; it is not the acceptance. When the spec classes the change as higher risk, say that a tech lead or an architect approves that pull request before it is merged. Report the pull request's URL, keep it green as `playbooks/review.md` says, and wait.
|
|
25
|
+
6. When the plan's pull request is merged, create a new branch as `playbooks/open-pr.md` says. In its first commit, set the plan's `Accepted by:` line, under its title, to the account that merged that pull request, read from the host. When that pull request has an approving review from a second person, add `Also accepted by: @<handle>.` on the next line. When you cannot read the host, write the handle the engineer gives. Never write a handle the host or the engineer did not give.
|
|
26
26
|
7. Implement the plan in its order. After each step, run the check that proves it and fix what fails. For a bug fix, follow `playbooks/fix.md` steps 7 and 8: commit the failing test on a `fix/` branch, and the gate then blocks test edits. When the work departs from the plan, change `plan.md` in the same commit. When the change meets the last requirement of `spec.md`, set `Status: done.` in `intent.md` and `spec.md`.
|
|
27
27
|
8. Run every check the project's agent instructions name and keep the output. Every check must pass: fix the code, and never skip, delete, or weaken a test. Then run the `verifier` subagent and fix what it reports. Start it on the model that `.agents/sdlc-models.md` names after `verify:`, at the effort on that line when the session can set one; when you cannot start a subagent, do what `../../../agents/verifier.md`, relative to this playbook, says yourself. For UI work, when the session has a browser tool, also close the loop with a visual check: start the app, give the browser tool the page and the mock the spec or the request names, screenshot, compare, and adjust, and repeat until it matches; two or three rounds is normal. When a check cannot run in this session, say which one and why, and never report it as passed.
|
|
28
28
|
9. Go on in the same turn, without asking: follow `playbooks/open-pr.md`, and then `playbooks/review.md`. Pushing the branch and opening the pull request are part of this task.
|
|
@@ -8,14 +8,14 @@ This playbook turns a merged intent into the requirements and design spec.
|
|
|
8
8
|
- `spec.md` already exists: say so and name the file.
|
|
9
9
|
2. Read the four policies, every time, before you write: for each of `brand`, `security`, `compliance`, and `ux`, read the project's `.agents/skills/<area>-policy/SKILL.md` when it exists, and the plugin's default at `../../<area>-policy/SKILL.md`, relative to this playbook, otherwise. Then read the code and the docs the change touches.
|
|
10
10
|
3. Write `intent/<change>/spec.md` from the template below. Use exactly the template's headings.
|
|
11
|
-
- **Request:** under the status line, quote the words of the request that started this spec.
|
|
11
|
+
- **Request:** under the status line, quote the words of the request that started this spec. End the line with `Skills at <hash>.`, the short hash of the last commit that changed the plugin's files in this repository: `git log -1 --format=%h -- plugins/sdlc`. When the plugin is not in this repository, write `Skills at unknown.`
|
|
12
12
|
- **Requirements:** numbered. Each one states a result a test or a reader can check.
|
|
13
13
|
- **Design:** how the change fits the existing code: the files that change, and what each one does afterwards.
|
|
14
14
|
- **Policies:** one line for each of the four policies. State which rules of that policy the change must meet, each as a requirement number, or write "Does not apply" and why.
|
|
15
|
-
- **Areas of concern:** a policy rule the change cannot meet, two policies that contradict, or a decision the files do not settle. Give each one a number and the policy it comes from. Write "None" only when there are none.
|
|
15
|
+
- **Areas of concern:** a policy rule the change cannot meet, two policies that contradict, or a decision the files do not settle. Give each one a number and the policy it comes from. Under each, on a line of its own, write `Decision: <the decision you propose>.` Write "None" only when there are none.
|
|
16
16
|
- **Open questions:** each open question from `intent.md`, answered with its evidence or carried forward. Write "None" only when the intent has none.
|
|
17
17
|
4. Commit `spec.md` on a new branch. In the same commit, set the intent's status line to `Status: accepted.`, because its pull request is merged. Push the branch, and open a pull request ready for review, as the opening paragraph of `playbooks/open-pr.md` says for branches and pull requests. When you cannot open a pull request from this session, say so and give the pushed branch's name.
|
|
18
|
-
5. **Gate: the product owner reviews the spec.** The product owner
|
|
18
|
+
5. **Gate: the product owner reviews the spec.** The product owner corrects the `Decision:` line under each area of concern, with its policy owner, then merges the pull request to accept the spec and those decisions as written, or closes it to reject it. Change a `Decision:` line when they tell you its text. For a change the spec classes as higher risk, the product owner consults a tech lead before accepting. You do none of these. The spec stays `Status: draft.`: never write `Status: accepted.` in this pull request, because the plan's commit sets it. Report the pull request's URL, keep it green as `playbooks/review.md` says, and wait.
|
|
19
19
|
|
|
20
20
|
**Next:** `playbooks/build.md`; an engineer with an agent; when the product owner has merged the spec's pull request and the engineer sends `/sdlc:sdlc-mode build` with the spec's path.
|
|
21
21
|
|
|
@@ -13,8 +13,8 @@ This playbook takes a defect in behaviour that already exists to a pull request.
|
|
|
13
13
|
|
|
14
14
|
Show the whole plan and name the file only as `intent/<short-description>/plan.md`. In plan mode you cannot write the file. Show the plan as the plan.
|
|
15
15
|
|
|
16
|
-
5. **Gate: the engineer accepts the plan.**
|
|
17
|
-
6.
|
|
16
|
+
5. **Gate: the engineer accepts the plan.** Commit `intent/<short-description>/plan.md` on a new branch, with the `Accepted by:` line as the template has it, push it, and open a pull request that changes no file outside `intent/`, as the opening paragraph of `playbooks/open-pr.md` says for branches and pull requests. Revise the plan as the engineer asks; each revision is a commit on that branch. The engineer merges that pull request to accept the plan, or closes it to reject it. You never merge it, and you change no file outside `intent/` before it is merged. In plan mode, the engineer's approval lets you write the file and open this pull request; it is not the acceptance. Report the pull request's URL, keep it green as `playbooks/review.md` says, and wait.
|
|
17
|
+
6. When the plan's pull request is merged, create the branch `fix/<short-description>` as `playbooks/open-pr.md` says. In its first commit, set the plan's `Accepted by:` line, under its title, to the account that merged that pull request, read from the host. When you cannot read the host, write the handle the engineer gives. Never write a handle the host or the engineer did not give.
|
|
18
18
|
7. Reproduce the defect as a test. Run it and check that it fails for the reason the cause gives. Commit the failing test before any fix.
|
|
19
19
|
8. Make the test pass without editing it.
|
|
20
20
|
9. Run every check the project's agent instructions name. Keep the output. When one fails, fix the code, and never skip, delete, or weaken a test. When a check cannot run in this session, say which one and why, and never report it as passed.
|
|
@@ -3,14 +3,14 @@
|
|
|
3
3
|
This playbook writes the intent. The build plan, `intent/<change>/plan.md`, comes later and is not written here.
|
|
4
4
|
|
|
5
5
|
1. Let the originator describe the problem in their own words: what they cannot do today, who is affected, what better looks like, and what is out of scope.
|
|
6
|
-
2. Read the code, the docs, and the existing `intent/` folders. Find the rest yourself: the
|
|
6
|
+
2. Read the code, the docs, and the existing `intent/` folders. Find the rest yourself. The author is the originator's handle on the host: read it from the host when the session shows whose account it runs as, and ask for it otherwise. Never take it from `git config user.name`, which names the machine's user.
|
|
7
7
|
3. Interview until the originator has said four things: the problem, the outcome, the constraints, and what is out of scope. The outcome is what the user would see.
|
|
8
8
|
- **When all four are already said, go to step 4 in the same turn.**
|
|
9
9
|
- **When one is missing, ask and stop.** Write no draft in that turn. Ask in rounds. Each round holds only questions that do not depend on another question in the same round. Number the questions. Each asks one thing and carries a concrete answer you recommend, worded so "yes" accepts it. A recommended answer is a fact or a choice, such as "Recommended: only the checkout page", never an instruction to the person. When you have no answer to recommend, write "No recommendation" and ask plainly. Wait for the answers, and build the next round on them.
|
|
10
10
|
4. Write `intent/<change>/intent.md` from the template below, where `<change>` is a short kebab-case name. Use exactly the template's five headings. What is out of scope goes under **Proposed outcome**. Anything else that is unknown, unverified, or not yet decided goes under **Open questions**. Write "None" only when nothing is, and never list what the originator already answered. Show the whole draft, and name the file only as `intent/<change>/intent.md`. In plan mode you cannot write the file. Show the draft as the plan, and write the file once it is approved.
|
|
11
11
|
5. **Gate: the originator corrects the draft.** Ask whether it matches what they meant, and revise until they say yes. Commit nothing, push nothing, and open no pull request before that yes. In plan mode, the originator approving the plan is that yes.
|
|
12
12
|
6. Commit `intent.md` on a new branch, push it, and open a pull request ready for review, as the opening paragraph of `playbooks/open-pr.md` says for branches and pull requests.
|
|
13
|
-
7. **Gate: the product owner accepts or rejects the intent.** The product owner merges the pull request to accept the intent, or closes it to reject it. You do neither. The file stays `Status: draft.`: never write `Status: accepted.` in this pull request, because the spec's pull request sets it. Report the pull request's URL, keep it green as `playbooks/review.md` says, and wait.
|
|
13
|
+
7. **Gate: the product owner accepts or rejects the intent.** The product owner merges the pull request to accept the intent, or closes it to reject it. You do neither. When the originator is not the person who merges, the originator is the pull request's author or approves it; say so when neither holds. The file stays `Status: draft.`: never write `Status: accepted.` in this pull request, because the spec's pull request sets it. Report the pull request's URL, keep it green as `playbooks/review.md` says, and wait.
|
|
14
14
|
|
|
15
15
|
**Next:** `playbooks/design.md`; an agent; when the product owner has merged the intent's pull request and a person or a job asks for the spec.
|
|
16
16
|
|
|
@@ -19,7 +19,7 @@ This playbook writes the intent. The build plan, `intent/<change>/plan.md`, come
|
|
|
19
19
|
```markdown
|
|
20
20
|
# Intent: <title>
|
|
21
21
|
|
|
22
|
-
Author:
|
|
22
|
+
Author: @<handle> (<team>). Status: draft.
|
|
23
23
|
|
|
24
24
|
## Problem
|
|
25
25
|
|
|
@@ -3,30 +3,15 @@ import {existsSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSyn
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import {tmpdir} from "node:os";
|
|
5
5
|
|
|
6
|
-
type BranchChanges = {
|
|
7
|
-
changedPaths: string[];
|
|
8
|
-
headCommit: string;
|
|
9
|
-
mergeBase: string;
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
type SelfTestCase = {
|
|
13
|
-
expectedMessagePart?: string;
|
|
14
|
-
name: string;
|
|
15
|
-
problems: string[];
|
|
16
|
-
};
|
|
17
|
-
|
|
18
6
|
const defaultBranch = "origin/main";
|
|
19
7
|
const intentDirectory = "intent";
|
|
20
|
-
const planFileName = "plan.md";
|
|
21
|
-
const specFileName = "spec.md";
|
|
22
|
-
const intentFileName = "intent.md";
|
|
23
|
-
const proofHeading = "## Proof";
|
|
24
|
-
const requirementsHeading = "## Requirements";
|
|
25
|
-
const acceptancePlaceholder = "@<engineer>";
|
|
26
8
|
const statusLineLimit = 5;
|
|
27
9
|
const acceptedByPrefix = "Accepted by: ";
|
|
28
|
-
|
|
29
|
-
|
|
10
|
+
|
|
11
|
+
type PlanMerge = {
|
|
12
|
+
approvers: string[];
|
|
13
|
+
mergedBy: string;
|
|
14
|
+
};
|
|
30
15
|
|
|
31
16
|
/**
|
|
32
17
|
* Run git in the current directory.
|
|
@@ -40,52 +25,6 @@ function runGit(gitArguments: string[]): string {
|
|
|
40
25
|
}).trim();
|
|
41
26
|
}
|
|
42
27
|
|
|
43
|
-
/**
|
|
44
|
-
* Find what this branch changed since it left the default branch.
|
|
45
|
-
* @returns The merge base, HEAD, and the changed paths, or undefined when the default branch is not available.
|
|
46
|
-
*/
|
|
47
|
-
function findBranchChanges(): BranchChanges | undefined {
|
|
48
|
-
try {
|
|
49
|
-
const mergeBase = runGit(["merge-base", "HEAD", defaultBranch]);
|
|
50
|
-
|
|
51
|
-
return {
|
|
52
|
-
mergeBase,
|
|
53
|
-
headCommit: runGit(["rev-parse", "HEAD"]),
|
|
54
|
-
changedPaths: runGit(["diff", "--name-only", `${mergeBase}...HEAD`])
|
|
55
|
-
.split("\n")
|
|
56
|
-
.filter(changedPath => changedPath !== ""),
|
|
57
|
-
};
|
|
58
|
-
} catch {
|
|
59
|
-
return undefined;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Tell whether a plan must name its accepting engineer: the branch changes a file outside `intent/`.
|
|
65
|
-
* @param branchChanges What the branch changed, or undefined when no merge base was found.
|
|
66
|
-
* @returns False when there is no merge base or HEAD is the default branch tip.
|
|
67
|
-
*/
|
|
68
|
-
function requiresAcceptance(branchChanges: BranchChanges | undefined): boolean {
|
|
69
|
-
return (
|
|
70
|
-
branchChanges !== undefined &&
|
|
71
|
-
branchChanges.mergeBase !== branchChanges.headCommit &&
|
|
72
|
-
branchChanges.changedPaths.some(changedPath => !changedPath.startsWith(`${intentDirectory}/`))
|
|
73
|
-
);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Say why the acceptance rule cannot run.
|
|
78
|
-
* @param branchChanges What the branch changed, or undefined when no merge base was found.
|
|
79
|
-
* @returns A problem when the default branch is not available, otherwise undefined.
|
|
80
|
-
*/
|
|
81
|
-
function describeMissingDefaultBranch(
|
|
82
|
-
branchChanges: BranchChanges | undefined,
|
|
83
|
-
): string | undefined {
|
|
84
|
-
return branchChanges === undefined
|
|
85
|
-
? `${defaultBranch} cannot be found, so the "${acceptedByPrefix}" rule cannot be applied; run "git fetch origin main" to apply it`
|
|
86
|
-
: undefined;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
28
|
/**
|
|
90
29
|
* Blank every line of a fenced code block, keeping line numbers.
|
|
91
30
|
* @param lines Document lines.
|
|
@@ -148,6 +87,217 @@ function readSection(maskedLines: string[], heading: string): string[] | undefin
|
|
|
148
87
|
return bounds === undefined ? undefined : maskedLines.slice(...bounds);
|
|
149
88
|
}
|
|
150
89
|
|
|
90
|
+
const concernsHeading = "## Areas of concern";
|
|
91
|
+
const decisionPrefix = "Decision: ";
|
|
92
|
+
const decisionPattern = /^ *Decision: \S/mu;
|
|
93
|
+
const alsoAcceptedByPrefix = "Also accepted by: ";
|
|
94
|
+
const hostTokenVariable = "GH_TOKEN";
|
|
95
|
+
const hostRepositoryPath = "repos/{owner}/{repo}";
|
|
96
|
+
const authorPrefix = "Author: ";
|
|
97
|
+
const authorPattern = /^Author: @[\dA-Za-z][\dA-Za-z-]*[ .]/;
|
|
98
|
+
const alsoAcceptedByPattern = /^Also accepted by: @([\dA-Za-z][\dA-Za-z-]*)\.$/;
|
|
99
|
+
|
|
100
|
+
type BranchChanges = {
|
|
101
|
+
changedPaths: string[];
|
|
102
|
+
headCommit: string;
|
|
103
|
+
mergeBase: string;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
type SelfTestCase = {
|
|
107
|
+
expectedMessagePart?: string;
|
|
108
|
+
name: string;
|
|
109
|
+
problems: string[];
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Find a plan that this branch adds together with a change outside `intent/`.
|
|
114
|
+
* @param isMerged Whether the default branch already holds the plan.
|
|
115
|
+
* @param isAcceptanceRequired Whether the branch changes a file outside `intent/`.
|
|
116
|
+
* @returns At most one message.
|
|
117
|
+
*/
|
|
118
|
+
function findUnmergedPlanProblems(isMerged: boolean, isAcceptanceRequired: boolean): string[] {
|
|
119
|
+
return isMerged || !isAcceptanceRequired
|
|
120
|
+
? []
|
|
121
|
+
: [
|
|
122
|
+
`the plan is not on ${defaultBranch} and this branch changes a file outside ${intentDirectory}/; an engineer accepts a plan by merging the pull request that holds only it, so open that pull request and build on a new branch after it is merged`,
|
|
123
|
+
];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Compare the plan's accepting handles with the host's record of its pull request.
|
|
128
|
+
* @param maskedLines Plan lines with fenced code blocks emptied.
|
|
129
|
+
* @param planMerge Who merged the pull request that last changed the plan, and who approved it.
|
|
130
|
+
* @returns One message per handle the host does not confirm.
|
|
131
|
+
*/
|
|
132
|
+
function findHostAcceptanceProblems(maskedLines: string[], planMerge: PlanMerge): string[] {
|
|
133
|
+
const acceptedLine = maskedLines.slice(1).find(line => line.trim() !== "") ?? "";
|
|
134
|
+
const accepter = acceptedLine.trimEnd().slice(acceptedByPrefix.length + 1, -1);
|
|
135
|
+
const secondAccepter = readSecondAccepter(maskedLines)?.toLowerCase();
|
|
136
|
+
const merger = planMerge.mergedBy.toLowerCase();
|
|
137
|
+
const approvers = planMerge.approvers.map(approver => approver.toLowerCase());
|
|
138
|
+
|
|
139
|
+
const mergerProblems =
|
|
140
|
+
accepter.toLowerCase() === merger
|
|
141
|
+
? []
|
|
142
|
+
: [
|
|
143
|
+
`the "${acceptedByPrefix}" line names @${accepter}, and the host shows that @${planMerge.mergedBy} merged the plan's pull request; write the account that merged it`,
|
|
144
|
+
];
|
|
145
|
+
|
|
146
|
+
const isSecondConfirmed =
|
|
147
|
+
secondAccepter === undefined ||
|
|
148
|
+
(secondAccepter !== merger && approvers.includes(secondAccepter));
|
|
149
|
+
|
|
150
|
+
return isSecondConfirmed
|
|
151
|
+
? mergerProblems
|
|
152
|
+
: [
|
|
153
|
+
...mergerProblems,
|
|
154
|
+
`the "${alsoAcceptedByPrefix}" line names a second person, and the host shows no approving review from that account on the plan's pull request; write "${alsoAcceptedByPrefix}@<handle>." with the account that approved it`,
|
|
155
|
+
];
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Find each numbered area of concern in a spec that carries no decision.
|
|
160
|
+
* @param specText Spec content.
|
|
161
|
+
* @returns At most one message, which names the numbers.
|
|
162
|
+
*/
|
|
163
|
+
function findDecisionProblems(specText: string): string[] {
|
|
164
|
+
const undecidedNumbers = (
|
|
165
|
+
readSection(maskFencedBlocks(splitLines(specText)), concernsHeading) ?? []
|
|
166
|
+
)
|
|
167
|
+
.join("\n")
|
|
168
|
+
.split(/^(?=\d+\. )/mu)
|
|
169
|
+
.filter(concern => /^\d+\. /u.test(concern) && !decisionPattern.test(concern))
|
|
170
|
+
.map(concern => /^\d+/u.exec(concern)?.[0]);
|
|
171
|
+
|
|
172
|
+
return undecidedNumbers.length === 0
|
|
173
|
+
? []
|
|
174
|
+
: [
|
|
175
|
+
`area of concern ${undecidedNumbers.join(", ")} has no "${decisionPrefix}<the decision>." line under it; write the proposed decision there, and the product owner corrects it before merging the spec`,
|
|
176
|
+
];
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Find an intent whose author is not a handle on the host.
|
|
181
|
+
* @param intentText Intent content.
|
|
182
|
+
* @returns At most one message.
|
|
183
|
+
*/
|
|
184
|
+
function findAuthorProblems(intentText: string): string[] {
|
|
185
|
+
const authorLine = splitLines(intentText).find(line => line.startsWith(authorPrefix));
|
|
186
|
+
|
|
187
|
+
return authorLine !== undefined && authorPattern.test(authorLine)
|
|
188
|
+
? []
|
|
189
|
+
: [
|
|
190
|
+
`the "${authorPrefix}" line holds no handle; write "${authorPrefix}@<handle> (<team>)." with the originator's account on the host`,
|
|
191
|
+
];
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Ask the host who merged the pull request that last changed a plan on the default branch, and who approved it.
|
|
196
|
+
* @param mergeBase The commit this branch left the default branch at.
|
|
197
|
+
* @param planPath Path of the plan.
|
|
198
|
+
* @returns The merging account and the approving accounts, or undefined when the host cannot be read.
|
|
199
|
+
*/
|
|
200
|
+
function readPlanMerge(mergeBase: string, planPath: string): PlanMerge | undefined {
|
|
201
|
+
if (process.env[hostTokenVariable] === undefined || process.env[hostTokenVariable] === "") {
|
|
202
|
+
return undefined;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
try {
|
|
206
|
+
const commit = runGit(["log", "-1", "--format=%H", mergeBase, "--", planPath]);
|
|
207
|
+
const pullRequest = runHost(`${hostRepositoryPath}/commits/${commit}/pulls`, ".[0].number");
|
|
208
|
+
const pullRequestPath = `${hostRepositoryPath}/pulls/${pullRequest}`;
|
|
209
|
+
const mergedBy = runHost(pullRequestPath, ".merged_by.login");
|
|
210
|
+
const approvedFilter = '.[] | select(.state == "APPROVED") | .user.login';
|
|
211
|
+
const approvers = runHost(`${pullRequestPath}/reviews`, approvedFilter);
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
mergedBy,
|
|
215
|
+
approvers: approvers.split("\n").filter(approver => approver !== ""),
|
|
216
|
+
};
|
|
217
|
+
} catch {
|
|
218
|
+
return undefined;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Call the host's API through its command-line tool.
|
|
224
|
+
* @param apiPath Path after `gh api`.
|
|
225
|
+
* @param filter The jq filter applied to the answer.
|
|
226
|
+
* @returns Trimmed standard output.
|
|
227
|
+
*/
|
|
228
|
+
function runHost(apiPath: string, filter: string): string {
|
|
229
|
+
return execFileSync("gh", ["api", apiPath, "--jq", filter], {
|
|
230
|
+
encoding: "utf8",
|
|
231
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
232
|
+
}).trim();
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Read the handle on the plan's `Also accepted by:` line.
|
|
237
|
+
* @param maskedLines Plan lines with fenced code blocks emptied.
|
|
238
|
+
* @returns The handle, an empty string when the line is malformed, or undefined when there is no such line.
|
|
239
|
+
*/
|
|
240
|
+
function readSecondAccepter(maskedLines: string[]): string | undefined {
|
|
241
|
+
const line = maskedLines.find(planLine => planLine.startsWith(alsoAcceptedByPrefix));
|
|
242
|
+
|
|
243
|
+
return line === undefined ? undefined : (alsoAcceptedByPattern.exec(line.trimEnd())?.[1] ?? "");
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const planFileName = "plan.md";
|
|
247
|
+
const specFileName = "spec.md";
|
|
248
|
+
const intentFileName = "intent.md";
|
|
249
|
+
const proofHeading = "## Proof";
|
|
250
|
+
const requirementsHeading = "## Requirements";
|
|
251
|
+
const acceptancePlaceholder = "@<engineer>";
|
|
252
|
+
const acceptedByPattern = /^Accepted by: @[\dA-Za-z][\dA-Za-z-]*\.$/;
|
|
253
|
+
const requiredHeadings = ["## Files that change", "## Order of work", "## Risks", proofHeading];
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Find what this branch changed since it left the default branch.
|
|
257
|
+
* @returns The merge base, HEAD, and the changed paths, or undefined when the default branch is not available.
|
|
258
|
+
*/
|
|
259
|
+
function findBranchChanges(): BranchChanges | undefined {
|
|
260
|
+
try {
|
|
261
|
+
const mergeBase = runGit(["merge-base", "HEAD", defaultBranch]);
|
|
262
|
+
|
|
263
|
+
return {
|
|
264
|
+
mergeBase,
|
|
265
|
+
headCommit: runGit(["rev-parse", "HEAD"]),
|
|
266
|
+
changedPaths: runGit(["diff", "--name-only", `${mergeBase}...HEAD`])
|
|
267
|
+
.split("\n")
|
|
268
|
+
.filter(changedPath => changedPath !== ""),
|
|
269
|
+
};
|
|
270
|
+
} catch {
|
|
271
|
+
return undefined;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Tell whether a plan must name its accepting engineer: the branch changes a file outside `intent/`.
|
|
277
|
+
* @param branchChanges What the branch changed, or undefined when no merge base was found.
|
|
278
|
+
* @returns False when there is no merge base or HEAD is the default branch tip.
|
|
279
|
+
*/
|
|
280
|
+
function requiresAcceptance(branchChanges: BranchChanges | undefined): boolean {
|
|
281
|
+
return (
|
|
282
|
+
branchChanges !== undefined &&
|
|
283
|
+
branchChanges.mergeBase !== branchChanges.headCommit &&
|
|
284
|
+
branchChanges.changedPaths.some(changedPath => !changedPath.startsWith(`${intentDirectory}/`))
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Say why the acceptance rule cannot run.
|
|
290
|
+
* @param branchChanges What the branch changed, or undefined when no merge base was found.
|
|
291
|
+
* @returns A problem when the default branch is not available, otherwise undefined.
|
|
292
|
+
*/
|
|
293
|
+
function describeMissingDefaultBranch(
|
|
294
|
+
branchChanges: BranchChanges | undefined,
|
|
295
|
+
): string | undefined {
|
|
296
|
+
return branchChanges === undefined
|
|
297
|
+
? `${defaultBranch} cannot be found, so the "${acceptedByPrefix}" rule cannot be applied; run "git fetch origin main" to apply it`
|
|
298
|
+
: undefined;
|
|
299
|
+
}
|
|
300
|
+
|
|
151
301
|
/**
|
|
152
302
|
* Quote each heading and join them for a message.
|
|
153
303
|
* @param headings Heading texts.
|
|
@@ -520,6 +670,26 @@ function createChangeFolder(fileNames: string[]): string {
|
|
|
520
670
|
const fixPlanFolder = createChangeFolder([planFileName]);
|
|
521
671
|
const intentOnlyFolder = createChangeFolder([intentFileName, planFileName]);
|
|
522
672
|
const specOnlyFolder = createChangeFolder([specFileName, planFileName]);
|
|
673
|
+
const onePlanLines = splitLines(buildPlan({}));
|
|
674
|
+
|
|
675
|
+
const twoPlanLines = splitLines(
|
|
676
|
+
buildPlan({
|
|
677
|
+
accepted: `${engineerLine}\n${alsoAcceptedByPrefix}@lead.`,
|
|
678
|
+
}),
|
|
679
|
+
);
|
|
680
|
+
|
|
681
|
+
/**
|
|
682
|
+
* Build the host's record of a plan's pull request, for a self-test.
|
|
683
|
+
* @param mergedByLogin The account that merged it.
|
|
684
|
+
* @param approvers The accounts that approved it.
|
|
685
|
+
* @returns The record.
|
|
686
|
+
*/
|
|
687
|
+
function hostRecord(mergedByLogin: string, approvers: string[]): PlanMerge {
|
|
688
|
+
return {
|
|
689
|
+
approvers,
|
|
690
|
+
mergedBy: mergedByLogin,
|
|
691
|
+
};
|
|
692
|
+
}
|
|
523
693
|
|
|
524
694
|
const selfTestCases: SelfTestCase[] = [
|
|
525
695
|
{
|
|
@@ -843,6 +1013,80 @@ const selfTestCases: SelfTestCase[] = [
|
|
|
843
1013
|
false,
|
|
844
1014
|
),
|
|
845
1015
|
},
|
|
1016
|
+
{
|
|
1017
|
+
name: "a plan added together with code",
|
|
1018
|
+
expectedMessagePart: `is not on ${defaultBranch}`,
|
|
1019
|
+
problems: findUnmergedPlanProblems(false, true),
|
|
1020
|
+
},
|
|
1021
|
+
{
|
|
1022
|
+
name: "a plan added alone",
|
|
1023
|
+
problems: findUnmergedPlanProblems(false, false),
|
|
1024
|
+
},
|
|
1025
|
+
{
|
|
1026
|
+
name: "a plan that is on the default branch, built with code",
|
|
1027
|
+
problems: findUnmergedPlanProblems(true, true),
|
|
1028
|
+
},
|
|
1029
|
+
{
|
|
1030
|
+
name: "the accepting handle is the account that merged, in another letter case",
|
|
1031
|
+
problems: findHostAcceptanceProblems(onePlanLines, hostRecord("Engineer", [])),
|
|
1032
|
+
},
|
|
1033
|
+
{
|
|
1034
|
+
name: "the accepting handle is not the account that merged",
|
|
1035
|
+
expectedMessagePart: "@someone-else merged",
|
|
1036
|
+
problems: findHostAcceptanceProblems(onePlanLines, hostRecord("someone-else", [])),
|
|
1037
|
+
},
|
|
1038
|
+
{
|
|
1039
|
+
name: "the second accepter approved the plan's pull request",
|
|
1040
|
+
problems: findHostAcceptanceProblems(twoPlanLines, hostRecord("engineer", ["lead"])),
|
|
1041
|
+
},
|
|
1042
|
+
{
|
|
1043
|
+
name: "the second accepter gave no approving review",
|
|
1044
|
+
expectedMessagePart: "no approving review",
|
|
1045
|
+
problems: findHostAcceptanceProblems(twoPlanLines, hostRecord("engineer", [])),
|
|
1046
|
+
},
|
|
1047
|
+
{
|
|
1048
|
+
name: "the second accepter is the account that merged",
|
|
1049
|
+
expectedMessagePart: "no approving review",
|
|
1050
|
+
problems: findHostAcceptanceProblems(
|
|
1051
|
+
splitLines(
|
|
1052
|
+
buildPlan({
|
|
1053
|
+
accepted: `${engineerLine}\n${alsoAcceptedByPrefix}@engineer.`,
|
|
1054
|
+
}),
|
|
1055
|
+
),
|
|
1056
|
+
hostRecord("engineer", ["engineer"]),
|
|
1057
|
+
),
|
|
1058
|
+
},
|
|
1059
|
+
{
|
|
1060
|
+
name: "every area of concern has a decision",
|
|
1061
|
+
problems: findDecisionProblems(
|
|
1062
|
+
`${concernsHeading}\n\n1. A.\n ${decisionPrefix}yes.\n2. B.\n ${decisionPrefix}no.\n\n## Open questions\n`,
|
|
1063
|
+
),
|
|
1064
|
+
},
|
|
1065
|
+
{
|
|
1066
|
+
name: "a spec with no area of concern",
|
|
1067
|
+
problems: findDecisionProblems(`${concernsHeading}\n\nNone\n\n## Open questions\n`),
|
|
1068
|
+
},
|
|
1069
|
+
{
|
|
1070
|
+
name: "an area of concern has no decision",
|
|
1071
|
+
expectedMessagePart: "area of concern 2 has no",
|
|
1072
|
+
problems: findDecisionProblems(
|
|
1073
|
+
`${concernsHeading}\n\n1. A.\n ${decisionPrefix}yes.\n2. B.\n\n## Open questions\n`,
|
|
1074
|
+
),
|
|
1075
|
+
},
|
|
1076
|
+
{
|
|
1077
|
+
name: "a decision line with no text is not a decision",
|
|
1078
|
+
expectedMessagePart: "area of concern 1 has no",
|
|
1079
|
+
problems: findDecisionProblems(`${concernsHeading}\n\n1. A.\n ${decisionPrefix}\n`),
|
|
1080
|
+
},
|
|
1081
|
+
{
|
|
1082
|
+
name: "an intent whose author is a handle",
|
|
1083
|
+
problems: findAuthorProblems("# Intent: A\n\nAuthor: @originator (Team). Status: draft.\n"),
|
|
1084
|
+
},
|
|
1085
|
+
{
|
|
1086
|
+
name: "an intent whose author is a name",
|
|
1087
|
+
expectedMessagePart: "holds no handle",
|
|
1088
|
+
problems: findAuthorProblems("# Intent: A\n\nAuthor: A Person (Team). Status: draft.\n"),
|
|
1089
|
+
},
|
|
846
1090
|
];
|
|
847
1091
|
|
|
848
1092
|
/**
|
|
@@ -945,17 +1189,27 @@ function findPlanLines(planPaths: string[]): string[] {
|
|
|
945
1189
|
`${planPath}: the file does not exist; pass the path of an existing ${planFileName}`,
|
|
946
1190
|
);
|
|
947
1191
|
|
|
948
|
-
const
|
|
1192
|
+
const changedPlanPaths = planPaths
|
|
949
1193
|
.filter(planPath => existsSync(planPath))
|
|
950
1194
|
.filter(
|
|
951
1195
|
planPath =>
|
|
952
1196
|
branchChanges === undefined ||
|
|
953
1197
|
readDefaultBranchFile(branchChanges.mergeBase, planPath) !==
|
|
954
1198
|
readFileSync(planPath, "utf8").trim(),
|
|
955
|
-
)
|
|
956
|
-
|
|
1199
|
+
);
|
|
1200
|
+
|
|
1201
|
+
const checkedLines = changedPlanPaths.flatMap(planPath =>
|
|
1202
|
+
findPlanFileLines(planPath, isAcceptanceRequired),
|
|
1203
|
+
);
|
|
1204
|
+
|
|
1205
|
+
const acceptanceLines =
|
|
1206
|
+
branchChanges === undefined
|
|
1207
|
+
? []
|
|
1208
|
+
: changedPlanPaths.flatMap(planPath =>
|
|
1209
|
+
findPlanAcceptanceLines(planPath, branchChanges.mergeBase, isAcceptanceRequired),
|
|
1210
|
+
);
|
|
957
1211
|
|
|
958
|
-
return [...missingBranchLines, ...missingLines, ...checkedLines];
|
|
1212
|
+
return [...missingBranchLines, ...missingLines, ...checkedLines, ...acceptanceLines];
|
|
959
1213
|
}
|
|
960
1214
|
|
|
961
1215
|
/**
|
|
@@ -972,6 +1226,78 @@ function readDefaultBranchFile(mergeBase: string, filePath: string): string | un
|
|
|
972
1226
|
}
|
|
973
1227
|
}
|
|
974
1228
|
|
|
1229
|
+
/**
|
|
1230
|
+
* Check that a plan this branch builds was accepted by a merge, and by whom.
|
|
1231
|
+
* @param planPath Path of the plan.
|
|
1232
|
+
* @param mergeBase The commit this branch left the default branch at.
|
|
1233
|
+
* @param isAcceptanceRequired Whether the branch changes a file outside `intent/`.
|
|
1234
|
+
* @returns One line per problem.
|
|
1235
|
+
*/
|
|
1236
|
+
function findPlanAcceptanceLines(
|
|
1237
|
+
planPath: string,
|
|
1238
|
+
mergeBase: string,
|
|
1239
|
+
isAcceptanceRequired: boolean,
|
|
1240
|
+
): string[] {
|
|
1241
|
+
const isOnDefaultBranch = readDefaultBranchFile(mergeBase, planPath) !== undefined;
|
|
1242
|
+
const unmergedProblems = findUnmergedPlanProblems(isOnDefaultBranch, isAcceptanceRequired);
|
|
1243
|
+
|
|
1244
|
+
if (!isAcceptanceRequired || unmergedProblems.length > 0) {
|
|
1245
|
+
return unmergedProblems.map(message => `${planPath}: ${message}`);
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
const planMerge = readPlanMerge(mergeBase, planPath);
|
|
1249
|
+
|
|
1250
|
+
if (planMerge === undefined) {
|
|
1251
|
+
process.stdout.write(
|
|
1252
|
+
`${planPath}: the "${acceptedByPrefix}" handle was not checked against the host; set ${hostTokenVariable} and install gh to check who merged the plan's pull request\n`,
|
|
1253
|
+
);
|
|
1254
|
+
|
|
1255
|
+
return [];
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
const planLines = splitLines(readFileSync(planPath, "utf8"));
|
|
1259
|
+
|
|
1260
|
+
return findHostAcceptanceProblems(maskFencedBlocks(planLines), planMerge).map(
|
|
1261
|
+
message => `${planPath}: ${message}`,
|
|
1262
|
+
);
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
/**
|
|
1266
|
+
* Report each spec this branch adds or changes that leaves an area of concern undecided, and each intent it adds whose author is not a handle.
|
|
1267
|
+
* @param changeFolders The `intent/<change>` folders.
|
|
1268
|
+
* @returns One line per problem.
|
|
1269
|
+
*/
|
|
1270
|
+
function findRecordLines(changeFolders: string[]): string[] {
|
|
1271
|
+
const branchChanges = changeFolders.length > 0 ? findBranchChanges() : undefined;
|
|
1272
|
+
|
|
1273
|
+
if (branchChanges === undefined) {
|
|
1274
|
+
return [];
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
return changeFolders.flatMap(folder => {
|
|
1278
|
+
const specPath = path.join(folder, specFileName);
|
|
1279
|
+
const intentPath = path.join(folder, intentFileName);
|
|
1280
|
+
const specText = readSpec(specPath);
|
|
1281
|
+
|
|
1282
|
+
const specProblems =
|
|
1283
|
+
specText === undefined ||
|
|
1284
|
+
readDefaultBranchFile(branchChanges.mergeBase, specPath) === specText.trim()
|
|
1285
|
+
? []
|
|
1286
|
+
: findDecisionProblems(specText);
|
|
1287
|
+
|
|
1288
|
+
const authorProblems =
|
|
1289
|
+
existsSync(intentPath) &&
|
|
1290
|
+
readDefaultBranchFile(branchChanges.mergeBase, intentPath) === undefined
|
|
1291
|
+
? findAuthorProblems(readFileSync(intentPath, "utf8"))
|
|
1292
|
+
: [];
|
|
1293
|
+
|
|
1294
|
+
return [
|
|
1295
|
+
...specProblems.map(message => `${specPath}: ${message}`),
|
|
1296
|
+
...authorProblems.map(message => `${intentPath}: ${message}`),
|
|
1297
|
+
];
|
|
1298
|
+
});
|
|
1299
|
+
}
|
|
1300
|
+
|
|
975
1301
|
/**
|
|
976
1302
|
* Report each intent and spec that this branch marks accepted before its pull request merged.
|
|
977
1303
|
* @param changeFolders The `intent/<change>` folders.
|
|
@@ -1034,6 +1360,7 @@ if (selfTestFailures.length > 0) {
|
|
|
1034
1360
|
...findPlanLines(planPaths),
|
|
1035
1361
|
...findMissingPlanLines(changeFolders),
|
|
1036
1362
|
...findAcceptedStatusLines(changeFolders),
|
|
1363
|
+
...findRecordLines(changeFolders),
|
|
1037
1364
|
];
|
|
1038
1365
|
|
|
1039
1366
|
if (problemLines.length > 0) {
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
# Review rules
|
|
2
|
-
|
|
3
|
-
Review pull requests against the project's agent guide and rules:
|
|
4
|
-
|
|
5
|
-
- [Review instructions](../.agents/review.md)
|
|
6
|
-
- [Agent guide](../AGENTS.md)
|
|
7
|
-
- [Agent-Owned Work](../.agents/rules/agent-owned-work.md)
|
|
8
|
-
- [Owned Checks](../.agents/rules/automation-owned-checks.md)
|
|
9
|
-
- [Required Comments](../.agents/rules/code-comments.md)
|
|
10
|
-
- [Codebase Hygiene](../.agents/rules/codebase-hygiene.md)
|
|
11
|
-
- [Constitution Changes](../.agents/rules/constitution-changes.md)
|
|
12
|
-
- [Current State](../.agents/rules/current-state-authority.md)
|
|
13
|
-
- [Demand-Driven Configuration](../.agents/rules/demand-driven-configuration.md)
|
|
14
|
-
- [Dependencies](../.agents/rules/dependency-security.md)
|
|
15
|
-
- [Descriptive Naming](../.agents/rules/descriptive-naming.md)
|
|
16
|
-
- [Test Admission](../.agents/rules/deterministic-testing.md)
|
|
17
|
-
- [Diagnostics](../.agents/rules/diagnostics-and-error-handling.md)
|
|
18
|
-
- [Evidence](../.agents/rules/evidence-before-claims.md)
|
|
19
|
-
- [Findings](../.agents/rules/no-suppression.md)
|
|
20
|
-
- [Platform and Library First](../.agents/rules/platform-and-library-first.md)
|
|
21
|
-
- [Sibling Style](../.agents/rules/single-author-style.md)
|
|
22
|
-
- [Logging](../.agents/rules/structured-logging.md)
|
|
23
|
-
- [TypeScript-First](../.agents/rules/typescript-first.md)
|