create-agentic-pdlc 2.4.0 → 3.1.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.
Files changed (33) hide show
  1. package/.agentic-pdlc/hooks/pdlc-stage-gate.sh +37 -10
  2. package/.claude/settings.json +18 -0
  3. package/.coderabbit.yaml +41 -0
  4. package/.github/workflows/add-to-board.yml +55 -7
  5. package/.github/workflows/agent-trigger.yml +57 -25
  6. package/.github/workflows/board-reconciliation.yml +176 -0
  7. package/.github/workflows/pdlc-health-check.yml +81 -81
  8. package/.github/workflows/project-automation.yml +252 -259
  9. package/CLAUDE.md +1 -1
  10. package/README.md +33 -32
  11. package/adapters/claude-code/skill.md +12 -8
  12. package/bin/cli.js +607 -213
  13. package/docs/superpowers/plans/2026-06-04-spec-format-issue-template.md +160 -0
  14. package/docs/superpowers/plans/2026-06-04-two-tier-installer.md +1056 -0
  15. package/docs/superpowers/plans/2026-06-05-archive-card-on-issue-close.md +105 -0
  16. package/docs/superpowers/plans/2026-06-05-project-id-actions-variable.md +336 -0
  17. package/docs/superpowers/specs/2026-06-04-spec-format-issue-template-design.md +46 -0
  18. package/docs/superpowers/specs/2026-06-05-project-id-actions-variable-design.md +114 -0
  19. package/package.json +2 -2
  20. package/scripts/derive-column.js +20 -0
  21. package/templates/.github/workflows/add-to-board.yml +2 -2
  22. package/templates/.github/workflows/agent-trigger.yml +2 -2
  23. package/templates/.github/workflows/pdlc-health-check.yml +2 -2
  24. package/templates/.github/workflows/project-automation.yml +47 -8
  25. package/templates/full/CLAUDE.md +30 -0
  26. package/templates/lite/AGENTS.md +121 -0
  27. package/templates/lite/CLAUDE.md +44 -0
  28. package/tests/cli.test.js +118 -0
  29. package/.github/workflows/agentic-metrics.yml +0 -545
  30. package/.github/workflows/qa-agent.yml +0 -139
  31. package/.github/workflows/qa-gate.yml +0 -51
  32. /package/templates/{AGENTS.md → full/AGENTS.md} +0 -0
  33. /package/templates/{docs → full/docs}/pdlc.md +0 -0
@@ -8,59 +8,230 @@ on:
8
8
  issues:
9
9
  types: [labeled, closed]
10
10
 
11
- env:
12
- PROJECT_ID: "PVT_kwHODpFFL84BXg7h"
13
- STATUS_FIELD_ID: "PVTSSF_lAHODpFFL84BXg7hzhStRHI"
14
- STATUS_IDEA: "bb6e5a20"
15
- STATUS_BRAINSTORMING: "8eb07c5b"
16
- STATUS_DETAILING: "9f6ce70e"
17
- STATUS_APPROVAL: "31bf4610"
18
- STATUS_DEVELOPMENT: "2c9e78e6"
19
- STATUS_TESTING: "96b59ade"
20
- STATUS_CODE_REVIEW_PR: "86ca9720"
21
- STATUS_PRODUCTION: "1581e5bd"
22
-
23
11
  jobs:
24
- # Issue Labeled → Move Upstream
25
- move-card-on-label:
26
- name: Upstream Label Move Card
27
- if: github.event_name == 'issues' && github.event.action == 'labeled'
12
+ resolve-ids:
13
+ name: Resolve Board Column IDs
14
+ if: github.event_name != 'issues' || github.event.action != 'closed'
28
15
  runs-on: ubuntu-latest
29
16
  env:
30
17
  PROJECT_TOKEN: ${{ secrets.PROJECT_TOKEN }}
18
+ outputs:
19
+ project_id: ${{ steps.resolve.outputs.project_id }}
20
+ status_field_id: ${{ steps.resolve.outputs.status_field_id }}
21
+ status_brainstorming: ${{ steps.resolve.outputs.status_brainstorming }}
22
+ status_detailing: ${{ steps.resolve.outputs.status_detailing }}
23
+ status_approval: ${{ steps.resolve.outputs.status_approval }}
24
+ status_development: ${{ steps.resolve.outputs.status_development }}
25
+ status_code_review_pr: ${{ steps.resolve.outputs.status_code_review_pr }}
26
+ status_production: ${{ steps.resolve.outputs.status_production }}
31
27
  steps:
32
- - name: Detect Label and Move Issue
33
- if: ${{ env.PROJECT_TOKEN != '' && env.PROJECT_ID != '{{PROJECT_ID}}' }}
34
- uses: actions/github-script@v8
28
+ - name: Resolve column IDs by name
29
+ id: resolve
30
+ if: ${{ env.PROJECT_TOKEN != '' && vars.PROJECT_ID != '' }}
31
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
35
32
  with:
36
33
  github-token: ${{ env.PROJECT_TOKEN }}
37
34
  script: |
38
- const labelName = context.payload.label.name;
39
- let targetStatusId = null;
40
- let stageName = null;
41
-
42
- if (labelName === 'stage:brainstorming') {
43
- targetStatusId = process.env.STATUS_BRAINSTORMING;
44
- stageName = 'Brainstorming';
45
- } else if (labelName === 'stage:detailing') {
46
- targetStatusId = process.env.STATUS_DETAILING;
47
- stageName = 'Detailing';
48
- } else if (labelName === 'stage:approval') {
49
- targetStatusId = process.env.STATUS_APPROVAL;
50
- stageName = 'Approval';
51
- } else if (labelName === 'stage:development') {
52
- targetStatusId = process.env.STATUS_DEVELOPMENT;
53
- stageName = 'Development';
35
+ const projectId = '${{ vars.PROJECT_ID }}';
36
+ if (!projectId || projectId === '{{PROJECT_ID}}') {
37
+ core.warning('vars.PROJECT_ID not set — board features disabled');
38
+ return;
54
39
  }
55
-
56
- if (!targetStatusId) {
57
- console.log('No stage PDLC label found. Skipping.');
40
+ const { node } = await github.graphql(`
41
+ query($id: ID!) {
42
+ node(id: $id) {
43
+ ... on ProjectV2 {
44
+ fields(first: 20) {
45
+ nodes {
46
+ ... on ProjectV2SingleSelectField {
47
+ id
48
+ name
49
+ options { id name }
50
+ }
51
+ }
52
+ }
53
+ }
54
+ }
55
+ }
56
+ `, { id: projectId });
57
+ const statusField = node.fields.nodes.find(f => f.name === 'Status');
58
+ if (!statusField) { core.setFailed('Status field not found on board'); return; }
59
+ const opt = (name) => statusField.options.find(o => o.name.includes(name))?.id ?? '';
60
+ const ids = {
61
+ brainstorming: opt('Brainstorming'),
62
+ detailing: opt('Detailing'),
63
+ approval: opt('Approval'),
64
+ development: opt('Development'),
65
+ code_review_pr: opt('Code Review'),
66
+ production: opt('Production'),
67
+ };
68
+ const missing = Object.entries(ids).filter(([, v]) => !v).map(([k]) => k);
69
+ if (missing.length > 0) {
70
+ core.setFailed(`Required Status columns not found on board: ${missing.join(', ')}`);
58
71
  return;
59
72
  }
73
+ core.setOutput('project_id', projectId);
74
+ core.setOutput('status_field_id', statusField.id);
75
+ core.setOutput('status_brainstorming', ids.brainstorming);
76
+ core.setOutput('status_detailing', ids.detailing);
77
+ core.setOutput('status_approval', ids.approval);
78
+ core.setOutput('status_development', ids.development);
79
+ core.setOutput('status_code_review_pr', ids.code_review_pr);
80
+ core.setOutput('status_production', ids.production);
81
+ console.log('✅ Board IDs resolved by name');
82
+
83
+ # Single dispatcher — f(event) → target column → move card
84
+ move-card:
85
+ name: Board Dispatcher
86
+ needs: [resolve-ids]
87
+ if: needs.resolve-ids.outputs.project_id != ''
88
+ runs-on: ubuntu-latest
89
+ env:
90
+ PROJECT_TOKEN: ${{ secrets.PROJECT_TOKEN }}
91
+ PROJECT_ID: ${{ needs.resolve-ids.outputs.project_id }}
92
+ STATUS_FIELD_ID: ${{ needs.resolve-ids.outputs.status_field_id }}
93
+ STATUS_BRAINSTORMING: ${{ needs.resolve-ids.outputs.status_brainstorming }}
94
+ STATUS_DETAILING: ${{ needs.resolve-ids.outputs.status_detailing }}
95
+ STATUS_APPROVAL: ${{ needs.resolve-ids.outputs.status_approval }}
96
+ STATUS_DEVELOPMENT: ${{ needs.resolve-ids.outputs.status_development }}
97
+ STATUS_CODE_REVIEW_PR: ${{ needs.resolve-ids.outputs.status_code_review_pr }}
98
+ STATUS_PRODUCTION: ${{ needs.resolve-ids.outputs.status_production }}
99
+ steps:
100
+ - uses: actions/checkout@v5.0.1
101
+ - name: Dispatch board move
102
+ if: ${{ env.PROJECT_TOKEN != '' && env.PROJECT_ID != '' }}
103
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
104
+ with:
105
+ github-token: ${{ env.PROJECT_TOKEN }}
106
+ script: |
107
+ const { classifyItem } = require('./scripts/derive-column.js');
108
+ const { owner, repo } = context.repo;
109
+ const eventName = context.eventName;
110
+ const action = context.payload.action;
111
+
112
+ const COLUMN_KEY_TO_ENV = {
113
+ 'code_review_pr': 'STATUS_CODE_REVIEW_PR',
114
+ 'development': 'STATUS_DEVELOPMENT',
115
+ 'approval': 'STATUS_APPROVAL',
116
+ 'detailing': 'STATUS_DETAILING',
117
+ 'brainstorming': 'STATUS_BRAINSTORMING',
118
+ };
119
+
120
+ // f(event) → { targetStatusId, linkedNodeIds, stageLabelsToClear, prLabelSwap }
121
+ async function deriveMove() {
122
+ const col = (key) => process.env[key];
123
+
124
+ // Issue labeled → stage column (uses shared classify logic)
125
+ if (eventName === 'issues' && action === 'labeled') {
126
+ const labelNames = (context.payload.issue.labels ?? []).map(l => l.name);
127
+ const columnKey = classifyItem(labelNames);
128
+ const targetStatusId = columnKey ? col(COLUMN_KEY_TO_ENV[columnKey]) : null;
129
+ if (!targetStatusId) return null;
130
+ return {
131
+ targetStatusId,
132
+ nodeIds: [context.payload.issue.node_id],
133
+ issueNumbers: [],
134
+ stageLabelsToClear: [],
135
+ prLabelSwap: null,
136
+ };
137
+ }
138
+
139
+ // PR opened/reopened → Code Review
140
+ if (eventName === 'pull_request' && (action === 'opened' || action === 'reopened')) {
141
+ const prNumber = context.payload.pull_request.number;
142
+ const { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number: prNumber });
143
+ const linkedIssues = [...(pr.body ?? '').matchAll(/(?:Closes?|Fixes?|Resolves?)\s+#(\d+)/gi)]
144
+ .map(m => parseInt(m[1]));
145
+ const stageLabels = ['stage:development', 'stage:brainstorming', 'stage:detailing', 'jules'];
146
+
147
+ if (linkedIssues.length > 0) {
148
+ const nodes = await Promise.all(linkedIssues.map(async n => {
149
+ const { data: issue } = await github.rest.issues.get({ owner, repo, issue_number: n });
150
+ return { nodeId: issue.node_id, issueNumber: n };
151
+ }));
152
+ return {
153
+ targetStatusId: col('STATUS_CODE_REVIEW_PR'),
154
+ nodeIds: nodes.map(n => n.nodeId),
155
+ issueNumbers: nodes.map(n => n.issueNumber),
156
+ stageLabelsToClear: stageLabels,
157
+ prLabelSwap: { add: 'pr:in-review', prNumber },
158
+ };
159
+ } else {
160
+ return {
161
+ targetStatusId: col('STATUS_CODE_REVIEW_PR'),
162
+ nodeIds: [context.payload.pull_request.node_id],
163
+ issueNumbers: [],
164
+ stageLabelsToClear: [],
165
+ prLabelSwap: { add: 'pr:in-review', prNumber },
166
+ };
167
+ }
168
+ }
169
+
170
+ // PR review approved → Code Review + swap PR labels
171
+ if (eventName === 'pull_request_review' && context.payload.review.state === 'approved') {
172
+ const prNumber = context.payload.pull_request.number;
173
+ const { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number: prNumber });
174
+ const linkedIssues = [...(pr.body ?? '').matchAll(/(?:Closes?|Fixes?|Resolves?)\s+#(\d+)/gi)]
175
+ .map(m => parseInt(m[1]));
176
+
177
+ if (linkedIssues.length > 0) {
178
+ const nodes = await Promise.all(linkedIssues.map(async n => {
179
+ const { data: issue } = await github.rest.issues.get({ owner, repo, issue_number: n });
180
+ return { nodeId: issue.node_id, issueNumber: n };
181
+ }));
182
+ return {
183
+ targetStatusId: col('STATUS_CODE_REVIEW_PR'),
184
+ nodeIds: nodes.map(n => n.nodeId),
185
+ issueNumbers: nodes.map(n => n.issueNumber),
186
+ stageLabelsToClear: [],
187
+ prLabelSwap: { remove: 'pr:in-review', add: 'pr:approved', prNumber },
188
+ };
189
+ } else {
190
+ return {
191
+ targetStatusId: col('STATUS_CODE_REVIEW_PR'),
192
+ nodeIds: [context.payload.pull_request.node_id],
193
+ issueNumbers: [],
194
+ stageLabelsToClear: [],
195
+ prLabelSwap: { remove: 'pr:in-review', add: 'pr:approved', prNumber },
196
+ };
197
+ }
198
+ }
199
+
200
+ // PR merged → Production
201
+ if (eventName === 'pull_request' && action === 'closed' && context.payload.pull_request.merged) {
202
+ const prNumber = context.payload.pull_request.number;
203
+ const { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number: prNumber });
204
+ const linkedIssues = [...(pr.body ?? '').matchAll(/(?:Closes?|Fixes?|Resolves?)\s+#(\d+)/gi)]
205
+ .map(m => parseInt(m[1]));
206
+
207
+ if (linkedIssues.length > 0) {
208
+ const nodes = await Promise.all(linkedIssues.map(async n => {
209
+ const { data: issue } = await github.rest.issues.get({ owner, repo, issue_number: n });
210
+ return { nodeId: issue.node_id, issueNumber: n };
211
+ }));
212
+ return {
213
+ targetStatusId: col('STATUS_PRODUCTION'),
214
+ nodeIds: nodes.map(n => n.nodeId),
215
+ issueNumbers: nodes.map(n => n.issueNumber),
216
+ stageLabelsToClear: ['stage:approval'],
217
+ prLabelSwap: null,
218
+ };
219
+ } else {
220
+ return {
221
+ targetStatusId: col('STATUS_PRODUCTION'),
222
+ nodeIds: [context.payload.pull_request.node_id],
223
+ issueNumbers: [],
224
+ stageLabelsToClear: [],
225
+ prLabelSwap: null,
226
+ };
227
+ }
228
+ }
60
229
 
61
- const { issue: { number, node_id } } = context.payload;
230
+ return null;
231
+ }
62
232
 
63
- const moveItem = async (nodeId, statusId) => {
233
+ // Move a single item to target column
234
+ async function moveItem(nodeId, targetStatusId) {
64
235
  const { addProjectV2ItemById: { item } } = await github.graphql(`
65
236
  mutation($p: ID!, $c: ID!) {
66
237
  addProjectV2ItemById(input: {projectId: $p, contentId: $c}) { item { id } }
@@ -73,58 +244,60 @@ jobs:
73
244
  }
74
245
  }`, {
75
246
  p: process.env.PROJECT_ID, i: item.id, f: process.env.STATUS_FIELD_ID,
76
- v: { singleSelectOptionId: statusId }
247
+ v: { singleSelectOptionId: targetStatusId }
77
248
  });
78
- };
79
-
80
- await moveItem(node_id, targetStatusId);
81
- console.log(`Issue #${number} moved to ${stageName}`);
249
+ }
82
250
 
83
- # human-approved on issue → qa:approved on linked open PRs
84
- handle-human-approved:
85
- name: human-approved → qa:approved on linked PRs
86
- if: github.event_name == 'issues' && github.event.action == 'labeled' && github.event.label.name == 'human-approved'
87
- runs-on: ubuntu-latest
88
- permissions:
89
- issues: write
90
- pull-requests: write
91
- steps:
92
- - name: Add qa:approved to linked open PRs
93
- uses: actions/github-script@v8
94
- with:
95
- github-token: ${{ secrets.GITHUB_TOKEN }}
96
- script: |
97
- const { owner, repo } = context.repo;
98
- const issueNumber = context.payload.issue.number;
99
- const pattern = new RegExp(`(?:Closes?|Fixes?|Resolves?)\\s+#${issueNumber}\\b`, 'i');
251
+ // Execute
252
+ const move = await deriveMove();
253
+ if (!move) {
254
+ console.log('No board move needed for this event. Skipping.');
255
+ return;
256
+ }
100
257
 
101
- const prs = await github.paginate(github.rest.pulls.list, {
102
- owner, repo, state: 'open', per_page: 100
103
- });
258
+ for (const nodeId of move.nodeIds) {
259
+ await moveItem(nodeId, move.targetStatusId);
260
+ }
104
261
 
105
- const linked = prs.filter(pr => pattern.test(pr.body ?? ''));
262
+ for (const issueNumber of move.issueNumbers) {
263
+ for (const label of move.stageLabelsToClear) {
264
+ await github.rest.issues.removeLabel({ owner, repo, issue_number: issueNumber, name: label })
265
+ .catch(e => { if (e.status !== 404) { core.error(`Failed to remove label ${label}: ${e.message}`); throw e; } });
266
+ }
267
+ }
106
268
 
107
- if (linked.length === 0) {
108
- console.log(`No open PRs linking issue #${issueNumber}. Exiting.`);
109
- return;
269
+ if (move.stageLabelsToClear.length > 0 && move.issueNumbers.length === 0) {
270
+ // no linked issue numbers tracked for stage-label events (issue is identified by nodeId only)
110
271
  }
111
272
 
112
- for (const pr of linked) {
113
- await github.rest.issues.addLabels({
114
- owner, repo, issue_number: pr.number, labels: ['qa:approved']
115
- }).catch(() => {});
116
- console.log(`PR #${pr.number} qa:approved`);
273
+ if (move.prLabelSwap) {
274
+ const { prNumber, remove, add } = move.prLabelSwap;
275
+ if (remove) {
276
+ await github.rest.issues.removeLabel({ owner, repo, issue_number: prNumber, name: remove })
277
+ .catch(e => { if (e.status !== 404) { core.error(`Failed to remove PR label ${remove}: ${e.message}`); throw e; } });
278
+ }
279
+ if (add) {
280
+ await github.rest.issues.addLabels({ owner, repo, issue_number: prNumber, labels: [add] });
281
+ }
117
282
  }
118
283
 
284
+ console.log(`✅ Board move complete → ${move.targetStatusId} (${move.nodeIds.length} item(s))`);
285
+
119
286
  # OPTIONAL: Uncomment to enable architecture-violation → Idea
120
287
  # move-violation-to-board:
121
288
  # name: architecture-violation → 💡 Idea
289
+ # needs: [resolve-ids]
122
290
  # if: github.event_name == 'issues' && github.event.action == 'labeled' && github.event.label.name == 'architecture-violation'
123
291
  # runs-on: ubuntu-latest
292
+ # env:
293
+ # PROJECT_TOKEN: ${{ secrets.PROJECT_TOKEN }}
294
+ # PROJECT_ID: ${{ needs.resolve-ids.outputs.project_id }}
295
+ # STATUS_FIELD_ID: ${{ needs.resolve-ids.outputs.status_field_id }}
296
+ # STATUS_IDEA: ${{ needs.resolve-ids.outputs.status_idea }}
124
297
  # steps:
125
298
  # - name: Move issue to Idea
126
- # if: ${{ env.PROJECT_TOKEN != '' && env.PROJECT_ID != '{{PROJECT_ID}}' }}
127
- # uses: actions/github-script@v8
299
+ # if: ${{ env.PROJECT_TOKEN != '' && env.PROJECT_ID != '' }}
300
+ # uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
128
301
  # with:
129
302
  # github-token: ${{ env.PROJECT_TOKEN }}
130
303
  # script: |
@@ -144,193 +317,13 @@ jobs:
144
317
  # });
145
318
  # console.log(`Issue #${number} moved to Idea`);
146
319
 
147
- # PR Opened → Move linked issue to Testing (Variant B — QA Agent enabled)
148
- move-card-on-pr-open:
149
- name: Open PR → Testing
150
- if: github.event_name == 'pull_request' && (github.event.action == 'opened' || github.event.action == 'reopened')
151
- runs-on: ubuntu-latest
152
- env:
153
- PROJECT_TOKEN: ${{ secrets.PROJECT_TOKEN }}
154
- steps:
155
- - name: Move linked issue to Testing
156
- if: ${{ env.PROJECT_TOKEN != '' && env.PROJECT_ID != '{{PROJECT_ID}}' }}
157
- uses: actions/github-script@v8
158
- with:
159
- github-token: ${{ env.PROJECT_TOKEN }}
160
- script: |
161
- const prNumber = context.payload.pull_request.number;
162
- const { owner, repo } = context.repo;
163
-
164
- const { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number: prNumber });
165
- const body = pr.body ?? '';
166
-
167
- // Extract issues linked via "Closes #N", "Fixes #N", "Resolves #N"
168
- const linkedIssues = [...body.matchAll(/(?:Closes?|Fixes?|Resolves?)\s+#(\d+)/gi)]
169
- .map(m => parseInt(m[1]));
170
-
171
- const moveItem = async (nodeId) => {
172
- const { addProjectV2ItemById: { item } } = await github.graphql(`
173
- mutation($p: ID!, $c: ID!) {
174
- addProjectV2ItemById(input: {projectId: $p, contentId: $c}) { item { id } }
175
- }`, { p: process.env.PROJECT_ID, c: nodeId });
176
-
177
- await github.graphql(`
178
- mutation($p: ID!, $i: ID!, $f: ID!, $v: ProjectV2FieldValue!) {
179
- updateProjectV2ItemFieldValue(input: {projectId: $p, itemId: $i, fieldId: $f, value: $v}) {
180
- projectV2Item { id }
181
- }
182
- }`, {
183
- p: process.env.PROJECT_ID, i: item.id, f: process.env.STATUS_FIELD_ID,
184
- v: { singleSelectOptionId: process.env.STATUS_TESTING }
185
- });
186
- };
187
-
188
- const stageLabelsToRemove = ['stage:development', 'stage:brainstorming', 'stage:detailing', 'jules'];
189
-
190
- if (linkedIssues.length > 0) {
191
- for (const n of linkedIssues) {
192
- const { data: issue } = await github.rest.issues.get({ owner, repo, issue_number: n });
193
- await moveItem(issue.node_id);
194
- for (const label of stageLabelsToRemove) {
195
- await github.rest.issues.removeLabel({ owner, repo, issue_number: n, name: label }).catch(() => {});
196
- }
197
- await github.rest.issues.addLabels({ owner, repo, issue_number: n, labels: ['stage:testing'] }).catch(() => {});
198
- console.log(`Issue #${n} → Testing (labels updated)`);
199
- }
200
- } else {
201
- await moveItem(pr.node_id);
202
- console.log(`PR #${prNumber} → Testing (no linked issue)`);
203
- }
204
-
205
- await github.rest.issues.addLabels({ owner, repo, issue_number: prNumber, labels: ['pr:in-review'] }).catch(() => {});
206
-
207
- # QA Approved → Move linked issue to Code Review / PR
208
- move-card-on-qa-pass:
209
- name: qa:approved → Code Review / PR
210
- if: github.event_name == 'pull_request' && github.event.action == 'labeled' && github.event.label.name == 'qa:approved'
211
- runs-on: ubuntu-latest
212
- env:
213
- PROJECT_TOKEN: ${{ secrets.PROJECT_TOKEN }}
214
- steps:
215
- - name: Move linked issue to Code Review / PR
216
- if: ${{ env.PROJECT_TOKEN != '' && env.PROJECT_ID != '{{PROJECT_ID}}' }}
217
- uses: actions/github-script@v8
218
- with:
219
- github-token: ${{ env.PROJECT_TOKEN }}
220
- script: |
221
- const prNumber = context.payload.pull_request.number;
222
- const { owner, repo } = context.repo;
223
-
224
- const { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number: prNumber });
225
- const body = pr.body ?? '';
226
-
227
- const linkedIssues = [...body.matchAll(/(?:Closes?|Fixes?|Resolves?)\s+#(\d+)/gi)]
228
- .map(m => parseInt(m[1]));
229
-
230
- const moveItem = async (nodeId) => {
231
- const { addProjectV2ItemById: { item } } = await github.graphql(`
232
- mutation($p: ID!, $c: ID!) {
233
- addProjectV2ItemById(input: {projectId: $p, contentId: $c}) { item { id } }
234
- }`, { p: process.env.PROJECT_ID, c: nodeId });
235
-
236
- await github.graphql(`
237
- mutation($p: ID!, $i: ID!, $f: ID!, $v: ProjectV2FieldValue!) {
238
- updateProjectV2ItemFieldValue(input: {projectId: $p, itemId: $i, fieldId: $f, value: $v}) {
239
- projectV2Item { id }
240
- }
241
- }`, {
242
- p: process.env.PROJECT_ID, i: item.id, f: process.env.STATUS_FIELD_ID,
243
- v: { singleSelectOptionId: process.env.STATUS_CODE_REVIEW_PR }
244
- });
245
- };
246
-
247
- if (linkedIssues.length > 0) {
248
- for (const n of linkedIssues) {
249
- const { data: issue } = await github.rest.issues.get({ owner, repo, issue_number: n });
250
- await moveItem(issue.node_id);
251
- if (issue.labels?.some(l => l.name === 'stage:testing')) {
252
- await github.rest.issues.removeLabel({ owner, repo, issue_number: n, name: 'stage:testing' }).catch(() => {});
253
- }
254
- console.log(`Issue #${n} → Code Review / PR`);
255
- }
256
- } else {
257
- await moveItem(pr.node_id);
258
- console.log(`PR #${prNumber} → Code Review / PR (no linked issue)`);
259
- }
260
-
261
- # Review Approved → Add Label
262
- move-card-on-review-approved:
263
- name: Approved PR → Add Label
264
- if: github.event_name == 'pull_request_review' && github.event.review.state == 'approved'
265
- runs-on: ubuntu-latest
266
- env:
267
- PROJECT_TOKEN: ${{ secrets.PROJECT_TOKEN }}
268
- steps:
269
- - name: Swap PR labels
270
- if: ${{ env.PROJECT_TOKEN != '' && env.PROJECT_ID != '{{PROJECT_ID}}' }}
271
- uses: actions/github-script@v8
272
- with:
273
- github-token: ${{ env.PROJECT_TOKEN }}
274
- script: |
275
- const prNumber = context.payload.pull_request.number;
276
- const { owner, repo } = context.repo;
277
- try { await github.rest.issues.removeLabel({ owner, repo, issue_number: prNumber, name: 'pr:in-review' }); } catch {}
278
- await github.rest.issues.addLabels({ owner, repo, issue_number: prNumber, labels: ['pr:approved'] }).catch(() => {});
279
-
280
- # PR Merged → Production
281
- move-card-on-pr-merge:
282
- name: Merged PR → Production
283
- if: github.event_name == 'pull_request' && github.event.action == 'closed' && github.event.pull_request.merged == true
284
- runs-on: ubuntu-latest
285
- env:
286
- PROJECT_TOKEN: ${{ secrets.PROJECT_TOKEN }}
287
- steps:
288
- - name: Move issue to Production
289
- if: ${{ env.PROJECT_TOKEN != '' && env.PROJECT_ID != '{{PROJECT_ID}}' }}
290
- uses: actions/github-script@v8
291
- with:
292
- github-token: ${{ env.PROJECT_TOKEN }}
293
- script: |
294
- const prNumber = context.payload.pull_request.number;
295
- const { owner, repo } = context.repo;
296
- const { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number: prNumber });
297
- const body = pr.body ?? '';
298
- const linkedIssues = [...body.matchAll(/(?:Closes?|Fixes?|Resolves?)\s+#(\d+)/gi)].map(m => parseInt(m[1]));
299
-
300
- const moveItem = async (nodeId) => {
301
- const { addProjectV2ItemById: { item } } = await github.graphql(`
302
- mutation($p: ID!, $c: ID!) {
303
- addProjectV2ItemById(input: {projectId: $p, contentId: $c}) { item { id } }
304
- }`, { p: process.env.PROJECT_ID, c: nodeId });
305
- await github.graphql(`
306
- mutation($p: ID!, $i: ID!, $f: ID!, $v: ProjectV2FieldValue!) {
307
- updateProjectV2ItemFieldValue(input: {projectId: $p, itemId: $i, fieldId: $f, value: $v}) {
308
- projectV2Item { id }
309
- }
310
- }`, { p: process.env.PROJECT_ID, i: item.id, f: process.env.STATUS_FIELD_ID,
311
- v: { singleSelectOptionId: process.env.STATUS_PRODUCTION } });
312
- };
313
-
314
- if (linkedIssues.length > 0) {
315
- for (const n of linkedIssues) {
316
- const { data: issue } = await github.rest.issues.get({ owner, repo, issue_number: n });
317
- await moveItem(issue.node_id);
318
- if (issue.labels?.some(l => l.name === 'stage:approval')) {
319
- await github.rest.issues.removeLabel({ owner, repo, issue_number: n, name: 'stage:approval' }).catch(() => {});
320
- }
321
- console.log(`Issue #${n} → Production`);
322
- }
323
- } else {
324
- await moveItem(pr.node_id);
325
- }
326
-
327
320
  cleanup-labels-on-close:
328
321
  name: Issue closed → strip stage/agent labels
329
322
  if: github.event_name == 'issues' && github.event.action == 'closed'
330
323
  runs-on: ubuntu-latest
331
324
  steps:
332
325
  - name: Remove transient labels
333
- uses: actions/github-script@v8
326
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
334
327
  with:
335
328
  github-token: ${{ secrets.GITHUB_TOKEN }}
336
329
  script: |
@@ -338,8 +331,8 @@ jobs:
338
331
  const issue_number = context.payload.issue.number;
339
332
  const toRemove = [
340
333
  'stage:brainstorming', 'stage:detailing',
341
- 'stage:approval', 'stage:development', 'stage:testing',
342
- 'qa:needs-work', 'pr:in-review', 'jules'
334
+ 'stage:approval', 'stage:development',
335
+ 'pr:in-review', 'jules'
343
336
  ];
344
337
  for (const label of toRemove) {
345
338
  await github.rest.issues.removeLabel({ owner, repo, issue_number, name: label }).catch(() => {});
package/CLAUDE.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # agentic-pdlc
2
2
 
3
- **What it is:** npm CLI (`npx create-agentic-pdlc`) that installs a PDLC workflow for AI agent projects. Stack: Markdown + GitHub Actions YAML only — never complex Node/Python/Bash scripts.
3
+ **What it is:** npm CLI (`npx create-agentic-pdlc`) that installs a PDLC workflow for AI agent projects.
4
4
 
5
5
  **Workflow:** `stage:brainstorming` → `stage:detailing` → `spec:approved` → `stage:development` → PR → merge. Labels move the board automatically.
6
6