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.
- package/.agentic-pdlc/hooks/pdlc-stage-gate.sh +37 -10
- package/.claude/settings.json +18 -0
- package/.coderabbit.yaml +41 -0
- package/.github/workflows/add-to-board.yml +55 -7
- package/.github/workflows/agent-trigger.yml +57 -25
- package/.github/workflows/board-reconciliation.yml +176 -0
- package/.github/workflows/pdlc-health-check.yml +81 -81
- package/.github/workflows/project-automation.yml +252 -259
- package/CLAUDE.md +1 -1
- package/README.md +33 -32
- package/adapters/claude-code/skill.md +12 -8
- package/bin/cli.js +607 -213
- package/docs/superpowers/plans/2026-06-04-spec-format-issue-template.md +160 -0
- package/docs/superpowers/plans/2026-06-04-two-tier-installer.md +1056 -0
- package/docs/superpowers/plans/2026-06-05-archive-card-on-issue-close.md +105 -0
- package/docs/superpowers/plans/2026-06-05-project-id-actions-variable.md +336 -0
- package/docs/superpowers/specs/2026-06-04-spec-format-issue-template-design.md +46 -0
- package/docs/superpowers/specs/2026-06-05-project-id-actions-variable-design.md +114 -0
- package/package.json +2 -2
- package/scripts/derive-column.js +20 -0
- package/templates/.github/workflows/add-to-board.yml +2 -2
- package/templates/.github/workflows/agent-trigger.yml +2 -2
- package/templates/.github/workflows/pdlc-health-check.yml +2 -2
- package/templates/.github/workflows/project-automation.yml +47 -8
- package/templates/full/CLAUDE.md +30 -0
- package/templates/lite/AGENTS.md +121 -0
- package/templates/lite/CLAUDE.md +44 -0
- package/tests/cli.test.js +118 -0
- package/.github/workflows/agentic-metrics.yml +0 -545
- package/.github/workflows/qa-agent.yml +0 -139
- package/.github/workflows/qa-gate.yml +0 -51
- /package/templates/{AGENTS.md → full/AGENTS.md} +0 -0
- /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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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:
|
|
33
|
-
|
|
34
|
-
|
|
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
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
57
|
-
|
|
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
|
-
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
62
232
|
|
|
63
|
-
|
|
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:
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
|
102
|
-
|
|
103
|
-
}
|
|
258
|
+
for (const nodeId of move.nodeIds) {
|
|
259
|
+
await moveItem(nodeId, move.targetStatusId);
|
|
260
|
+
}
|
|
104
261
|
|
|
105
|
-
const
|
|
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 (
|
|
108
|
-
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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 != '
|
|
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',
|
|
342
|
-
'
|
|
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.
|
|
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
|
|