create-agentic-pdlc 3.0.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.
@@ -8,188 +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';
54
- }
55
-
56
- if (!targetStatusId) {
57
- console.log('No stage PDLC label found. Skipping.');
35
+ const projectId = '${{ vars.PROJECT_ID }}';
36
+ if (!projectId || projectId === '{{PROJECT_ID}}') {
37
+ core.warning('vars.PROJECT_ID not set — board features disabled');
58
38
  return;
59
39
  }
60
-
61
- const { issue: { number, node_id } } = context.payload;
62
-
63
- const moveItem = async (nodeId, statusId) => {
64
- const { addProjectV2ItemById: { item } } = await github.graphql(`
65
- mutation($p: ID!, $c: ID!) {
66
- addProjectV2ItemById(input: {projectId: $p, contentId: $c}) { item { id } }
67
- }`, { p: process.env.PROJECT_ID, c: nodeId });
68
-
69
- await github.graphql(`
70
- mutation($p: ID!, $i: ID!, $f: ID!, $v: ProjectV2FieldValue!) {
71
- updateProjectV2ItemFieldValue(input: {projectId: $p, itemId: $i, fieldId: $f, value: $v}) {
72
- projectV2Item { id }
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
+ }
73
53
  }
74
- }`, {
75
- p: process.env.PROJECT_ID, i: item.id, f: process.env.STATUS_FIELD_ID,
76
- v: { singleSelectOptionId: statusId }
77
- });
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'),
78
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(', ')}`);
71
+ return;
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');
79
82
 
80
- await moveItem(node_id, targetStatusId);
81
- console.log(`Issue #${number} moved to ${stageName}`);
82
-
83
- # OPTIONAL: Uncomment to enable architecture-violation → Idea
84
- # move-violation-to-board:
85
- # name: architecture-violation → 💡 Idea
86
- # if: github.event_name == 'issues' && github.event.action == 'labeled' && github.event.label.name == 'architecture-violation'
87
- # runs-on: ubuntu-latest
88
- # steps:
89
- # - name: Move issue to Idea
90
- # if: ${{ env.PROJECT_TOKEN != '' && env.PROJECT_ID != '{{PROJECT_ID}}' }}
91
- # uses: actions/github-script@v8
92
- # with:
93
- # github-token: ${{ env.PROJECT_TOKEN }}
94
- # script: |
95
- # const { issue: { number, node_id } } = context.payload;
96
- # const { addProjectV2ItemById: { item } } = await github.graphql(`
97
- # mutation($p: ID!, $c: ID!) {
98
- # addProjectV2ItemById(input: {projectId: $p, contentId: $c}) { item { id } }
99
- # }`, { p: process.env.PROJECT_ID, c: node_id });
100
- # await github.graphql(`
101
- # mutation($p: ID!, $i: ID!, $f: ID!, $v: ProjectV2FieldValue!) {
102
- # updateProjectV2ItemFieldValue(input: {projectId: $p, itemId: $i, fieldId: $f, value: $v}) {
103
- # projectV2Item { id }
104
- # }
105
- # }`, {
106
- # p: process.env.PROJECT_ID, i: item.id, f: process.env.STATUS_FIELD_ID,
107
- # v: { singleSelectOptionId: process.env.STATUS_IDEA }
108
- # });
109
- # console.log(`Issue #${number} moved to Idea`);
110
-
111
- # PR Opened → Move linked issue to Code Review / PR
112
- move-card-on-pr-open:
113
- name: Open PR → Code Review / PR
114
- if: github.event_name == 'pull_request' && (github.event.action == 'opened' || github.event.action == 'reopened')
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 != ''
115
88
  runs-on: ubuntu-latest
116
89
  env:
117
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 }}
118
99
  steps:
119
- - name: Move linked issue to Code Review / PR
120
- if: ${{ env.PROJECT_TOKEN != '' && env.PROJECT_ID != '{{PROJECT_ID}}' }}
121
- uses: actions/github-script@v8
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
122
104
  with:
123
105
  github-token: ${{ env.PROJECT_TOKEN }}
124
106
  script: |
125
- const prNumber = context.payload.pull_request.number;
107
+ const { classifyItem } = require('./scripts/derive-column.js');
126
108
  const { owner, repo } = context.repo;
109
+ const eventName = context.eventName;
110
+ const action = context.payload.action;
127
111
 
128
- const { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number: prNumber });
129
- const body = pr.body ?? '';
130
-
131
- const linkedIssues = [...body.matchAll(/(?:Closes?|Fixes?|Resolves?)\s+#(\d+)/gi)]
132
- .map(m => parseInt(m[1]));
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
+ };
133
119
 
134
- const moveItem = async (nodeId) => {
135
- const { addProjectV2ItemById: { item } } = await github.graphql(`
136
- mutation($p: ID!, $c: ID!) {
137
- addProjectV2ItemById(input: {projectId: $p, contentId: $c}) { item { id } }
138
- }`, { p: process.env.PROJECT_ID, c: nodeId });
120
+ // f(event) { targetStatusId, linkedNodeIds, stageLabelsToClear, prLabelSwap }
121
+ async function deriveMove() {
122
+ const col = (key) => process.env[key];
139
123
 
140
- await github.graphql(`
141
- mutation($p: ID!, $i: ID!, $f: ID!, $v: ProjectV2FieldValue!) {
142
- updateProjectV2ItemFieldValue(input: {projectId: $p, itemId: $i, fieldId: $f, value: $v}) {
143
- projectV2Item { id }
144
- }
145
- }`, {
146
- p: process.env.PROJECT_ID, i: item.id, f: process.env.STATUS_FIELD_ID,
147
- v: { singleSelectOptionId: process.env.STATUS_CODE_REVIEW_PR }
148
- });
149
- };
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
+ }
150
138
 
151
- const stageLabelsToRemove = ['stage:development', 'stage:brainstorming', 'stage:detailing', 'stage:testing', 'jules'];
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'];
152
146
 
153
- if (linkedIssues.length > 0) {
154
- for (const n of linkedIssues) {
155
- const { data: issue } = await github.rest.issues.get({ owner, repo, issue_number: n });
156
- await moveItem(issue.node_id);
157
- for (const label of stageLabelsToRemove) {
158
- await github.rest.issues.removeLabel({ owner, repo, issue_number: n, name: label }).catch(() => {});
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
+ };
159
167
  }
160
- console.log(`Issue #${n} → Code Review / PR`);
161
168
  }
162
- } else {
163
- await moveItem(pr.node_id);
164
- console.log(`PR #${prNumber} → Code Review / PR (no linked issue)`);
165
- }
166
169
 
167
- await github.rest.issues.addLabels({ owner, repo, issue_number: prNumber, labels: ['pr:in-review'] }).catch(() => {});
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]));
168
176
 
169
- # Review Approved Move linked issue to Code Review / PR + swap PR labels
170
- move-card-on-review-approved:
171
- name: Approved PR Code Review / PR
172
- if: github.event_name == 'pull_request_review' && github.event.review.state == 'approved'
173
- runs-on: ubuntu-latest
174
- env:
175
- PROJECT_TOKEN: ${{ secrets.PROJECT_TOKEN }}
176
- steps:
177
- - name: Move linked issue to Code Review and swap PR labels
178
- if: ${{ env.PROJECT_TOKEN != '' && env.PROJECT_ID != '{{PROJECT_ID}}' }}
179
- uses: actions/github-script@v8
180
- with:
181
- github-token: ${{ env.PROJECT_TOKEN }}
182
- script: |
183
- const prNumber = context.payload.pull_request.number;
184
- const { owner, repo } = context.repo;
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
+ }
185
199
 
186
- const { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number: prNumber });
187
- const body = pr.body ?? '';
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]));
188
206
 
189
- const linkedIssues = [...body.matchAll(/(?:Closes?|Fixes?|Resolves?)\s+#(\d+)/gi)]
190
- .map(m => parseInt(m[1]));
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
+ }
229
+
230
+ return null;
231
+ }
191
232
 
192
- const moveItem = async (nodeId) => {
233
+ // Move a single item to target column
234
+ async function moveItem(nodeId, targetStatusId) {
193
235
  const { addProjectV2ItemById: { item } } = await github.graphql(`
194
236
  mutation($p: ID!, $c: ID!) {
195
237
  addProjectV2ItemById(input: {projectId: $p, contentId: $c}) { item { id } }
@@ -202,81 +244,86 @@ jobs:
202
244
  }
203
245
  }`, {
204
246
  p: process.env.PROJECT_ID, i: item.id, f: process.env.STATUS_FIELD_ID,
205
- v: { singleSelectOptionId: process.env.STATUS_CODE_REVIEW_PR }
247
+ v: { singleSelectOptionId: targetStatusId }
206
248
  });
207
- };
249
+ }
208
250
 
209
- if (linkedIssues.length > 0) {
210
- for (const n of linkedIssues) {
211
- const { data: issue } = await github.rest.issues.get({ owner, repo, issue_number: n });
212
- await moveItem(issue.node_id);
213
- if (issue.labels?.some(l => l.name === 'stage:testing')) {
214
- await github.rest.issues.removeLabel({ owner, repo, issue_number: n, name: 'stage:testing' }).catch(() => {});
215
- }
216
- console.log(`Issue #${n} → Code Review / PR`);
217
- }
218
- } else {
219
- await moveItem(pr.node_id);
220
- console.log(`PR #${prNumber} → Code Review / PR (no linked issue)`);
251
+ // Execute
252
+ const move = await deriveMove();
253
+ if (!move) {
254
+ console.log('No board move needed for this event. Skipping.');
255
+ return;
221
256
  }
222
257
 
223
- try { await github.rest.issues.removeLabel({ owner, repo, issue_number: prNumber, name: 'pr:in-review' }); } catch {}
224
- await github.rest.issues.addLabels({ owner, repo, issue_number: prNumber, labels: ['pr:approved'] }).catch(() => {});
258
+ for (const nodeId of move.nodeIds) {
259
+ await moveItem(nodeId, move.targetStatusId);
260
+ }
225
261
 
226
- # PR Merged Production
227
- move-card-on-pr-merge:
228
- name: Merged PR Production
229
- if: github.event_name == 'pull_request' && github.event.action == 'closed' && github.event.pull_request.merged == true
230
- runs-on: ubuntu-latest
231
- env:
232
- PROJECT_TOKEN: ${{ secrets.PROJECT_TOKEN }}
233
- steps:
234
- - name: Move issue to Production
235
- if: ${{ env.PROJECT_TOKEN != '' && env.PROJECT_ID != '{{PROJECT_ID}}' }}
236
- uses: actions/github-script@v8
237
- with:
238
- github-token: ${{ env.PROJECT_TOKEN }}
239
- script: |
240
- const prNumber = context.payload.pull_request.number;
241
- const { owner, repo } = context.repo;
242
- const { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number: prNumber });
243
- const body = pr.body ?? '';
244
- const linkedIssues = [...body.matchAll(/(?:Closes?|Fixes?|Resolves?)\s+#(\d+)/gi)].map(m => parseInt(m[1]));
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
+ }
245
268
 
246
- const moveItem = async (nodeId) => {
247
- const { addProjectV2ItemById: { item } } = await github.graphql(`
248
- mutation($p: ID!, $c: ID!) {
249
- addProjectV2ItemById(input: {projectId: $p, contentId: $c}) { item { id } }
250
- }`, { p: process.env.PROJECT_ID, c: nodeId });
251
- await github.graphql(`
252
- mutation($p: ID!, $i: ID!, $f: ID!, $v: ProjectV2FieldValue!) {
253
- updateProjectV2ItemFieldValue(input: {projectId: $p, itemId: $i, fieldId: $f, value: $v}) {
254
- projectV2Item { id }
255
- }
256
- }`, { p: process.env.PROJECT_ID, i: item.id, f: process.env.STATUS_FIELD_ID,
257
- v: { singleSelectOptionId: process.env.STATUS_PRODUCTION } });
258
- };
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)
271
+ }
259
272
 
260
- if (linkedIssues.length > 0) {
261
- for (const n of linkedIssues) {
262
- const { data: issue } = await github.rest.issues.get({ owner, repo, issue_number: n });
263
- await moveItem(issue.node_id);
264
- if (issue.labels?.some(l => l.name === 'stage:approval')) {
265
- await github.rest.issues.removeLabel({ owner, repo, issue_number: n, name: 'stage:approval' }).catch(() => {});
266
- }
267
- console.log(`Issue #${n} Production`);
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] });
268
281
  }
269
- } else {
270
- await moveItem(pr.node_id);
271
282
  }
272
283
 
284
+ console.log(`✅ Board move complete → ${move.targetStatusId} (${move.nodeIds.length} item(s))`);
285
+
286
+ # OPTIONAL: Uncomment to enable architecture-violation → Idea
287
+ # move-violation-to-board:
288
+ # name: architecture-violation → 💡 Idea
289
+ # needs: [resolve-ids]
290
+ # if: github.event_name == 'issues' && github.event.action == 'labeled' && github.event.label.name == 'architecture-violation'
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 }}
297
+ # steps:
298
+ # - name: Move issue to Idea
299
+ # if: ${{ env.PROJECT_TOKEN != '' && env.PROJECT_ID != '' }}
300
+ # uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
301
+ # with:
302
+ # github-token: ${{ env.PROJECT_TOKEN }}
303
+ # script: |
304
+ # const { issue: { number, node_id } } = context.payload;
305
+ # const { addProjectV2ItemById: { item } } = await github.graphql(`
306
+ # mutation($p: ID!, $c: ID!) {
307
+ # addProjectV2ItemById(input: {projectId: $p, contentId: $c}) { item { id } }
308
+ # }`, { p: process.env.PROJECT_ID, c: node_id });
309
+ # await github.graphql(`
310
+ # mutation($p: ID!, $i: ID!, $f: ID!, $v: ProjectV2FieldValue!) {
311
+ # updateProjectV2ItemFieldValue(input: {projectId: $p, itemId: $i, fieldId: $f, value: $v}) {
312
+ # projectV2Item { id }
313
+ # }
314
+ # }`, {
315
+ # p: process.env.PROJECT_ID, i: item.id, f: process.env.STATUS_FIELD_ID,
316
+ # v: { singleSelectOptionId: process.env.STATUS_IDEA }
317
+ # });
318
+ # console.log(`Issue #${number} moved to Idea`);
319
+
273
320
  cleanup-labels-on-close:
274
321
  name: Issue closed → strip stage/agent labels
275
322
  if: github.event_name == 'issues' && github.event.action == 'closed'
276
323
  runs-on: ubuntu-latest
277
324
  steps:
278
325
  - name: Remove transient labels
279
- uses: actions/github-script@v8
326
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
280
327
  with:
281
328
  github-token: ${{ secrets.GITHUB_TOKEN }}
282
329
  script: |
@@ -284,7 +331,7 @@ jobs:
284
331
  const issue_number = context.payload.issue.number;
285
332
  const toRemove = [
286
333
  'stage:brainstorming', 'stage:detailing',
287
- 'stage:approval', 'stage:development', 'stage:testing',
334
+ 'stage:approval', 'stage:development',
288
335
  'pr:in-review', 'jules'
289
336
  ];
290
337
  for (const label of toRemove) {
@@ -58,22 +58,22 @@ If any of these files are missing, you are in **Setup Mode**. Do not proceed wit
58
58
  - **CLAUDE.md:** If `.agentic-pdlc/templates/CLAUDE.md` exists and `CLAUDE.md` does not yet exist at the project root, write it — replacing only `{{PROJECT_NAME}}` with the project name. Skip if CLAUDE.md already exists (never downgrade).
59
59
  - **Lite profile:** If `cli-context.json` has `"profile": "lite"`, skip steps that reference `docs/pdlc.md`, `project-automation.yml`, `agent-trigger.yml`, and `pdlc-health-check.yml` — these are not installed in lite.
60
60
  6. Offer to run the `gh` commands for labels (`spec:approved`, `pr:in-review`, `pr:approved`, `architecture-violation`).
61
- 7. **`PROJECT_PAT` secret (required for board automation):**
61
+ 7. **`PROJECT_TOKEN` secret (required for board automation):**
62
62
 
63
63
  Read `patAutoSet` from `.agentic-pdlc/cli-context.json`:
64
64
 
65
- **If `patAutoSet === true`:** The CLI already configured this secret automatically. Print `✅ PROJECT_PAT is configured.` and continue to Step 8 — do not ask the user anything.
65
+ **If `patAutoSet === true`:** The CLI already configured this secret automatically. Print `✅ PROJECT_TOKEN is configured.` and continue to Step 8 — do not ask the user anything.
66
66
 
67
67
  **If `patAutoSet === false` (org repo):** Show the block below and wait for the user to reply "done" or "secret set" before continuing:
68
68
 
69
- > Your repo is in an organization. For security, `PROJECT_PAT` must be a dedicated PAT (not your personal OAuth token). Without it, all board card movements in CI will silently skip — no error surfaced.
69
+ > Your repo is in an organization. For security, `PROJECT_TOKEN` must be a dedicated PAT (not your personal OAuth token). Without it, all board card movements in CI will silently skip — no error surfaced.
70
70
  >
71
71
  > 1. Open: **github.com/settings/tokens** → *Generate new token (classic)*
72
- > 2. Name: `PROJECT_PAT — <repo-name>`
72
+ > 2. Name: `PROJECT_TOKEN — <repo-name>`
73
73
  > 3. Select scopes: ✅ `repo` + ✅ `project`
74
74
  > 4. Copy the token, then run:
75
75
  > ```
76
- > gh secret set PROJECT_PAT --body "<your-token>" --repo <owner>/<repo>
76
+ > gh secret set PROJECT_TOKEN --body "<your-token>" --repo <owner>/<repo>
77
77
  > ```
78
78
  > 5. Reply **"done"** when finished.
79
79
  8. **IMPORTANT:** Delete the setup prompt file by running exactly: