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
@@ -1,39 +1,66 @@
1
1
  #!/bin/bash
2
- # PDLC Stage Gate — blocks gh pr create without stage:approval on linked issue.
2
+ # PDLC Stage Gate — blocks gh pr create and file edits without spec:approved on linked issue.
3
3
  # Bypass: branch prefix hotfix/ skips all checks.
4
4
 
5
5
  INPUT=$(cat)
6
6
  COMMAND=$(echo "$INPUT" | node -e "const d=JSON.parse(require('fs').readFileSync(0)); console.log(d.command || '')" 2>/dev/null || echo "")
7
+ FILE_PATH=$(echo "$INPUT" | node -e "const d=JSON.parse(require('fs').readFileSync(0)); console.log(d.file_path || '')" 2>/dev/null || echo "")
7
8
 
8
- if ! echo "$COMMAND" | grep -q "gh pr create"; then
9
+ IS_PR_CREATE=false
10
+ IS_FILE_EDIT=false
11
+
12
+ if echo "$COMMAND" | grep -q "gh pr create"; then
13
+ IS_PR_CREATE=true
14
+ elif [ -n "$FILE_PATH" ]; then
15
+ IS_FILE_EDIT=true
16
+ fi
17
+
18
+ if [ "$IS_PR_CREATE" != "true" ] && [ "$IS_FILE_EDIT" != "true" ]; then
9
19
  exit 0
10
20
  fi
11
21
 
12
22
  BRANCH=$(git branch --show-current 2>/dev/null || echo "")
13
23
 
14
24
  if echo "$BRANCH" | grep -qE "^hotfix/"; then
15
- echo "✅ PDLC: Hotfix branch — stage gate bypassed."
16
25
  exit 0
17
26
  fi
18
27
 
19
28
  ISSUE_NUM=$(echo "$BRANCH" | grep -oE '[0-9]+' | head -1)
20
29
 
21
30
  if [ -z "$ISSUE_NUM" ]; then
31
+ if [ "$IS_FILE_EDIT" = "true" ]; then
32
+ exit 0
33
+ fi
22
34
  echo "❌ PDLC Stage Gate: Cannot determine issue from branch '$BRANCH'."
23
35
  echo " Use: feat/<issue-number>-<description> or hotfix/<issue-number>-<description>"
24
36
  exit 1
25
37
  fi
26
38
 
27
- LABELS=$(gh issue view "$ISSUE_NUM" --json labels --jq '[.labels[].name] | join(" ")' 2>/dev/null || echo "")
39
+ LABELS=$(gh issue view "$ISSUE_NUM" --json labels --jq '[.labels[].name] | join(" ")' 2>/dev/null)
40
+ if [ $? -ne 0 ] || [ -z "$LABELS" ]; then
41
+ echo "❌ PDLC Stage Gate: Could not fetch labels for issue #$ISSUE_NUM."
42
+ echo " Missing condition: spec:approved"
43
+ echo " Next step: verify gh auth and issue number, then have PM add spec:approved."
44
+ exit 1
45
+ fi
28
46
 
29
- if echo "$LABELS" | grep -qw "stage:approval"; then
30
- echo "✅ PDLC: Issue #$ISSUE_NUM approved — gate passed."
47
+ if echo "$LABELS" | grep -qw "spec:approved"; then
31
48
  exit 0
32
49
  fi
33
50
 
34
51
  STAGE=$(echo "$LABELS" | tr ' ' '\n' | grep "^stage:" | head -1 || echo "none")
35
- echo "❌ PDLC Stage Gate: Issue #$ISSUE_NUM is not approved."
36
- echo " Current stage: $STAGE"
37
- echo " Required: stage:approval label on the issue."
38
- echo " Emergency bypass: rename branch to hotfix/<issue-number>-<description>."
52
+
53
+ if [ "$IS_PR_CREATE" = "true" ]; then
54
+ echo " PDLC Stage Gate: PR creation blocked issue #$ISSUE_NUM missing spec:approved."
55
+ echo " Current stage: $STAGE"
56
+ echo " Missing condition: spec:approved (set by PM after reviewing spec in issue body)"
57
+ echo " Next step: complete spec → stage:approval → wait for PM to add spec:approved."
58
+ echo " Emergency bypass: rename branch to hotfix/<issue-number>-<description>."
59
+ else
60
+ echo "❌ PDLC Stage Gate: File edit blocked — issue #$ISSUE_NUM missing spec:approved."
61
+ echo " Current stage: $STAGE"
62
+ echo " Missing condition: spec:approved (set by PM after reviewing spec in issue body)"
63
+ echo " Next step: complete spec → stage:approval → wait for PM to add spec:approved."
64
+ echo " Emergency bypass: rename branch to hotfix/<issue-number>-<description>."
65
+ fi
39
66
  exit 1
@@ -9,6 +9,24 @@
9
9
  "command": "bash .agentic-pdlc/hooks/pdlc-stage-gate.sh"
10
10
  }
11
11
  ]
12
+ },
13
+ {
14
+ "matcher": "Edit",
15
+ "hooks": [
16
+ {
17
+ "type": "command",
18
+ "command": "bash .agentic-pdlc/hooks/pdlc-stage-gate.sh"
19
+ }
20
+ ]
21
+ },
22
+ {
23
+ "matcher": "Write",
24
+ "hooks": [
25
+ {
26
+ "type": "command",
27
+ "command": "bash .agentic-pdlc/hooks/pdlc-stage-gate.sh"
28
+ }
29
+ ]
12
30
  }
13
31
  ]
14
32
  }
@@ -0,0 +1,41 @@
1
+ language: "en-US"
2
+ reviews:
3
+ profile: "assertive"
4
+ request_changes_workflow: false
5
+ high_level_summary: true
6
+ poem: false
7
+ auto_review:
8
+ enabled: true
9
+ drafts: false
10
+ base_branches:
11
+ - main
12
+ path_instructions:
13
+ - path: "bin/cli.js"
14
+ instructions: >
15
+ All user-facing console.log strings must use the t() translation helper.
16
+ New strings must be added to the i18n object and referenced via i18n.key_name.
17
+ Flag any hardcoded English-only string in console output as a violation of
18
+ the project's i18n pattern.
19
+ - path: ".github/workflows/**"
20
+ instructions: >
21
+ Pay close attention to GitHub Actions syntax correctness, trigger
22
+ semantics (push/pull_request/workflow_run events and their filters),
23
+ expression syntax (${{ }}), and shell quoting in run steps.
24
+ Flag any logic that could cause silent failures or unintended trigger
25
+ behavior.
26
+ review_status: true
27
+ auto_apply_labels: true
28
+ labeling_instructions:
29
+ - label: "architecture-violation"
30
+ instructions: >
31
+ Apply when the PR introduces runtime behavior that violates documented
32
+ architectural invariants: code that bypasses the PDLC stage gate at runtime,
33
+ application code that programmatically sets or removes labels reserved for
34
+ automation (stage:*, spec:approved, qa:*) outside of their designated
35
+ automation workflows, business logic added directly to templates instead of
36
+ the CLI, or behavior that directly contradicts invariants in AGENTS.md or
37
+ CLAUDE.md. Do NOT apply for configuration changes to tools (e.g., updating
38
+ .coderabbit.yaml, GitHub Actions workflows, or CI config files) — those are
39
+ infrastructure, not invariant violations.
40
+ chat:
41
+ auto_reply: true
@@ -4,21 +4,69 @@ on:
4
4
  issues:
5
5
  types: [opened]
6
6
 
7
- env:
8
- PROJECT_ID: "PVT_kwHODpFFL84BXg7h"
9
- STATUS_FIELD_ID: "PVTSSF_lAHODpFFL84BXg7hzhStRHI"
10
- STATUS_IDEA: "bb6e5a20"
11
-
12
7
  jobs:
8
+ resolve-ids:
9
+ name: Resolve Board Column IDs
10
+ runs-on: ubuntu-latest
11
+ env:
12
+ PROJECT_TOKEN: ${{ secrets.PROJECT_TOKEN }}
13
+ outputs:
14
+ project_id: ${{ steps.resolve.outputs.project_id }}
15
+ status_field_id: ${{ steps.resolve.outputs.status_field_id }}
16
+ status_idea: ${{ steps.resolve.outputs.status_idea }}
17
+ steps:
18
+ - name: Resolve column IDs by name
19
+ id: resolve
20
+ if: ${{ env.PROJECT_TOKEN != '' && vars.PROJECT_ID != '' }}
21
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
22
+ with:
23
+ github-token: ${{ env.PROJECT_TOKEN }}
24
+ script: |
25
+ const projectId = '${{ vars.PROJECT_ID }}';
26
+ if (!projectId || projectId === '{{PROJECT_ID}}') {
27
+ core.warning('vars.PROJECT_ID not set — board features disabled');
28
+ return;
29
+ }
30
+ const { node } = await github.graphql(`
31
+ query($id: ID!) {
32
+ node(id: $id) {
33
+ ... on ProjectV2 {
34
+ fields(first: 20) {
35
+ nodes {
36
+ ... on ProjectV2SingleSelectField {
37
+ id
38
+ name
39
+ options { id name }
40
+ }
41
+ }
42
+ }
43
+ }
44
+ }
45
+ }
46
+ `, { id: projectId });
47
+ const statusField = node.fields.nodes.find(f => f.name === 'Status');
48
+ if (!statusField) { core.setFailed('Status field not found on board'); return; }
49
+ const opt = (name) => statusField.options.find(o => o.name.includes(name))?.id ?? '';
50
+ const ideaId = opt('Idea');
51
+ if (!ideaId) { core.setFailed('Required Status column "Idea" not found on board'); return; }
52
+ core.setOutput('project_id', projectId);
53
+ core.setOutput('status_field_id', statusField.id);
54
+ core.setOutput('status_idea', ideaId);
55
+ console.log('✅ Board IDs resolved by name');
56
+
13
57
  add-to-board:
14
58
  name: Auto-add new issue to board
59
+ needs: [resolve-ids]
15
60
  runs-on: ubuntu-latest
16
61
  env:
17
62
  PROJECT_TOKEN: ${{ secrets.PROJECT_TOKEN }}
63
+ PROJECT_ID: ${{ needs.resolve-ids.outputs.project_id }}
64
+ STATUS_FIELD_ID: ${{ needs.resolve-ids.outputs.status_field_id }}
65
+ STATUS_IDEA: ${{ needs.resolve-ids.outputs.status_idea }}
18
66
  steps:
19
67
  - name: Add issue to project board
20
- if: ${{ env.PROJECT_TOKEN != '' && env.PROJECT_ID != '{{PROJECT_ID}}' }}
21
- uses: actions/github-script@v8
68
+ if: ${{ env.PROJECT_TOKEN != '' && env.PROJECT_ID != '' }}
69
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
22
70
  with:
23
71
  github-token: ${{ env.PROJECT_TOKEN }}
24
72
  script: |
@@ -7,9 +7,56 @@ on:
7
7
  types: [labeled]
8
8
 
9
9
  jobs:
10
+ resolve-ids:
11
+ name: Resolve Board Column IDs
12
+ runs-on: ubuntu-latest
13
+ env:
14
+ PROJECT_TOKEN: ${{ secrets.PROJECT_TOKEN }}
15
+ outputs:
16
+ project_id: ${{ steps.resolve.outputs.project_id }}
17
+ status_field_id: ${{ steps.resolve.outputs.status_field_id }}
18
+ status_development: ${{ steps.resolve.outputs.status_development }}
19
+ steps:
20
+ - name: Resolve column IDs by name
21
+ id: resolve
22
+ if: ${{ env.PROJECT_TOKEN != '' && vars.PROJECT_ID != '' }}
23
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
24
+ with:
25
+ github-token: ${{ env.PROJECT_TOKEN }}
26
+ script: |
27
+ const projectId = '${{ vars.PROJECT_ID }}';
28
+ if (!projectId || projectId === '{{PROJECT_ID}}') {
29
+ core.warning('vars.PROJECT_ID not set — board features disabled');
30
+ return;
31
+ }
32
+ const { node } = await github.graphql(`
33
+ query($id: ID!) {
34
+ node(id: $id) {
35
+ ... on ProjectV2 {
36
+ fields(first: 20) {
37
+ nodes {
38
+ ... on ProjectV2SingleSelectField {
39
+ id
40
+ name
41
+ options { id name }
42
+ }
43
+ }
44
+ }
45
+ }
46
+ }
47
+ }
48
+ `, { id: projectId });
49
+ const statusField = node.fields.nodes.find(f => f.name === 'Status');
50
+ if (!statusField) { core.setFailed('Status field not found on board'); return; }
51
+ const opt = (name) => statusField.options.find(o => o.name.includes(name))?.id ?? '';
52
+ core.setOutput('project_id', projectId);
53
+ core.setOutput('status_field_id', statusField.id);
54
+ core.setOutput('status_development', opt('Development'));
55
+ console.log('✅ Board IDs resolved by name');
56
+
10
57
  trigger-implementation-agent:
11
58
  name: Advance issue to stage:development
12
- # Runs only when spec:approved is added
59
+ needs: [resolve-ids]
13
60
  if: github.event.label.name == 'spec:approved'
14
61
  runs-on: ubuntu-latest
15
62
  permissions:
@@ -17,40 +64,28 @@ jobs:
17
64
  contents: read
18
65
  env:
19
66
  PROJECT_TOKEN: ${{ secrets.PROJECT_TOKEN }}
20
- PROJECT_ID: "PVT_kwHODpFFL84BXg7h"
21
- STATUS_FIELD_ID: "PVTSSF_lAHODpFFL84BXg7hzhStRHI"
22
- STATUS_DEVELOPMENT: "2c9e78e6"
67
+ PROJECT_ID: ${{ needs.resolve-ids.outputs.project_id }}
68
+ STATUS_FIELD_ID: ${{ needs.resolve-ids.outputs.status_field_id }}
69
+ STATUS_DEVELOPMENT: ${{ needs.resolve-ids.outputs.status_development }}
23
70
  steps:
24
71
  - name: Swap labels — stage:approval → stage:development
25
- uses: actions/github-script@v8
72
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
26
73
  with:
27
74
  github-token: ${{ secrets.GITHUB_TOKEN }}
28
75
  script: |
29
76
  const { owner, repo } = context.repo;
30
77
  const issue_number = context.payload.issue.number;
31
-
32
78
  try {
33
- await github.rest.issues.removeLabel({
34
- owner,
35
- repo,
36
- issue_number,
37
- name: 'stage:approval'
38
- });
79
+ await github.rest.issues.removeLabel({ owner, repo, issue_number, name: 'stage:approval' });
39
80
  } catch (error) {
40
81
  console.log('Label stage:approval not found or could not be removed');
41
82
  }
42
-
43
- await github.rest.issues.addLabels({
44
- owner,
45
- repo,
46
- issue_number,
47
- labels: ['stage:development']
48
- });
83
+ await github.rest.issues.addLabels({ owner, repo, issue_number, labels: ['stage:development'] });
49
84
 
50
85
  - name: Move board card to Development
51
- if: ${{ env.PROJECT_TOKEN != '' }}
86
+ if: ${{ env.PROJECT_TOKEN != '' && env.PROJECT_ID != '' }}
52
87
  continue-on-error: true
53
- uses: actions/github-script@v8
88
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
54
89
  with:
55
90
  github-token: ${{ env.PROJECT_TOKEN }}
56
91
  script: |
@@ -71,7 +106,6 @@ jobs:
71
106
 
72
107
  trigger-agent-on-violation:
73
108
  name: Flag architecture violation
74
- # Runs when architecture-violation is added (Sentinel flow)
75
109
  if: github.event.label.name == 'architecture-violation'
76
110
  runs-on: ubuntu-latest
77
111
  permissions:
@@ -79,12 +113,11 @@ jobs:
79
113
  contents: read
80
114
  steps:
81
115
  - name: Comment on issue
82
- uses: actions/github-script@v8
116
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
83
117
  with:
84
118
  github-token: ${{ secrets.GITHUB_TOKEN }}
85
119
  script: |
86
120
  const issueNumber = context.payload.issue.number;
87
-
88
121
  const body = [
89
122
  `⚠️ **Architecture violation detected.** Please fix the issue described above.`,
90
123
  '',
@@ -96,7 +129,6 @@ jobs:
96
129
  '- Fix only what the violation points out — do not refactor unrelated code',
97
130
  `- Include \`Closes #${issueNumber}\` in the PR body`,
98
131
  ].join('\n');
99
-
100
132
  await github.rest.issues.createComment({
101
133
  owner: context.repo.owner,
102
134
  repo: context.repo.repo,
@@ -0,0 +1,176 @@
1
+ name: Board Reconciliation (Drift Heal)
2
+
3
+ on:
4
+ schedule:
5
+ - cron: '0 */6 * * *'
6
+ workflow_dispatch:
7
+
8
+ jobs:
9
+ resolve-ids:
10
+ name: Resolve Board Column IDs
11
+ runs-on: ubuntu-latest
12
+ env:
13
+ PROJECT_TOKEN: ${{ secrets.PROJECT_TOKEN }}
14
+ outputs:
15
+ project_id: ${{ steps.resolve.outputs.project_id }}
16
+ status_field_id: ${{ steps.resolve.outputs.status_field_id }}
17
+ status_brainstorming: ${{ steps.resolve.outputs.status_brainstorming }}
18
+ status_detailing: ${{ steps.resolve.outputs.status_detailing }}
19
+ status_approval: ${{ steps.resolve.outputs.status_approval }}
20
+ status_development: ${{ steps.resolve.outputs.status_development }}
21
+ status_code_review_pr: ${{ steps.resolve.outputs.status_code_review_pr }}
22
+ steps:
23
+ - name: Resolve column IDs by name
24
+ id: resolve
25
+ if: ${{ env.PROJECT_TOKEN != '' && vars.PROJECT_ID != '' }}
26
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
27
+ with:
28
+ github-token: ${{ env.PROJECT_TOKEN }}
29
+ script: |
30
+ const projectId = '${{ vars.PROJECT_ID }}';
31
+ if (!projectId || projectId === '{{PROJECT_ID}}') {
32
+ core.warning('vars.PROJECT_ID not set — board features disabled');
33
+ return;
34
+ }
35
+ const { node } = await github.graphql(`
36
+ query($id: ID!) {
37
+ node(id: $id) {
38
+ ... on ProjectV2 {
39
+ fields(first: 20) {
40
+ nodes {
41
+ ... on ProjectV2SingleSelectField {
42
+ id
43
+ name
44
+ options { id name }
45
+ }
46
+ }
47
+ }
48
+ }
49
+ }
50
+ }
51
+ `, { id: projectId });
52
+ const statusField = node.fields.nodes.find(f => f.name === 'Status');
53
+ if (!statusField) { core.setFailed('Status field not found on board'); return; }
54
+ const opt = (name) => statusField.options.find(o => o.name.includes(name))?.id ?? '';
55
+ const ids = {
56
+ brainstorming: opt('Brainstorming'),
57
+ detailing: opt('Detailing'),
58
+ approval: opt('Approval'),
59
+ development: opt('Development'),
60
+ code_review_pr: opt('Code Review'),
61
+ };
62
+ const missing = Object.entries(ids).filter(([, v]) => !v).map(([k]) => k);
63
+ if (missing.length > 0) {
64
+ core.setFailed(`Required Status columns not found on board: ${missing.join(', ')}`);
65
+ return;
66
+ }
67
+ core.setOutput('project_id', projectId);
68
+ core.setOutput('status_field_id', statusField.id);
69
+ core.setOutput('status_brainstorming', ids.brainstorming);
70
+ core.setOutput('status_detailing', ids.detailing);
71
+ core.setOutput('status_approval', ids.approval);
72
+ core.setOutput('status_development', ids.development);
73
+ core.setOutput('status_code_review_pr', ids.code_review_pr);
74
+ console.log('✅ Board IDs resolved by name');
75
+
76
+ reconcile-board:
77
+ name: Reconcile Board (idempotent drift heal)
78
+ needs: [resolve-ids]
79
+ if: needs.resolve-ids.outputs.project_id != ''
80
+ runs-on: ubuntu-latest
81
+ env:
82
+ PROJECT_TOKEN: ${{ secrets.PROJECT_TOKEN }}
83
+ PROJECT_ID: ${{ needs.resolve-ids.outputs.project_id }}
84
+ STATUS_FIELD_ID: ${{ needs.resolve-ids.outputs.status_field_id }}
85
+ STATUS_BRAINSTORMING: ${{ needs.resolve-ids.outputs.status_brainstorming }}
86
+ STATUS_DETAILING: ${{ needs.resolve-ids.outputs.status_detailing }}
87
+ STATUS_APPROVAL: ${{ needs.resolve-ids.outputs.status_approval }}
88
+ STATUS_DEVELOPMENT: ${{ needs.resolve-ids.outputs.status_development }}
89
+ STATUS_CODE_REVIEW_PR: ${{ needs.resolve-ids.outputs.status_code_review_pr }}
90
+ steps:
91
+ - uses: actions/checkout@v5.0.1
92
+ - name: Reconcile all open board items
93
+ if: ${{ env.PROJECT_TOKEN != '' && env.PROJECT_ID != '' }}
94
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
95
+ with:
96
+ github-token: ${{ env.PROJECT_TOKEN }}
97
+ script: |
98
+ const { classifyItem } = require('./scripts/derive-column.js');
99
+
100
+ const COLUMN_KEY_TO_ID = {
101
+ 'code_review_pr': process.env.STATUS_CODE_REVIEW_PR,
102
+ 'development': process.env.STATUS_DEVELOPMENT,
103
+ 'approval': process.env.STATUS_APPROVAL,
104
+ 'detailing': process.env.STATUS_DETAILING,
105
+ 'brainstorming': process.env.STATUS_BRAINSTORMING,
106
+ };
107
+
108
+ // Paginate all board items
109
+ let allItems = [];
110
+ let cursor = null;
111
+ do {
112
+ const { node } = await github.graphql(`
113
+ query($id: ID!, $cursor: String) {
114
+ node(id: $id) {
115
+ ... on ProjectV2 {
116
+ items(first: 50, after: $cursor) {
117
+ pageInfo { hasNextPage endCursor }
118
+ nodes {
119
+ id
120
+ fieldValueByName(name: "Status") {
121
+ ... on ProjectV2ItemFieldSingleSelectValue { optionId }
122
+ }
123
+ content {
124
+ ... on Issue {
125
+ number
126
+ state
127
+ labels(first: 20) { nodes { name } }
128
+ }
129
+ }
130
+ }
131
+ }
132
+ }
133
+ }
134
+ }
135
+ `, { id: process.env.PROJECT_ID, cursor });
136
+ const page = node.items;
137
+ allItems = allItems.concat(page.nodes);
138
+ cursor = page.pageInfo.hasNextPage ? page.pageInfo.endCursor : null;
139
+ } while (cursor);
140
+
141
+ let checked = 0, corrected = 0;
142
+
143
+ for (const item of allItems) {
144
+ // Skip PRs and closed issues
145
+ if (!item.content || item.content.state !== 'OPEN') continue;
146
+
147
+ const labelNames = item.content.labels.nodes.map(l => l.name);
148
+ const columnKey = classifyItem(labelNames);
149
+ if (!columnKey) continue; // no actionable label — skip (e.g. Idea column items)
150
+
151
+ const targetId = COLUMN_KEY_TO_ID[columnKey];
152
+ if (!targetId) continue; // column not resolved (shouldn't happen)
153
+
154
+ const currentId = item.fieldValueByName?.optionId ?? null;
155
+ checked++;
156
+
157
+ if (currentId === targetId) continue; // already correct — no mutation
158
+
159
+ await github.graphql(`
160
+ mutation($p: ID!, $i: ID!, $f: ID!, $v: ProjectV2FieldValue!) {
161
+ updateProjectV2ItemFieldValue(input: {projectId: $p, itemId: $i, fieldId: $f, value: $v}) {
162
+ projectV2Item { id }
163
+ }
164
+ }
165
+ `, {
166
+ p: process.env.PROJECT_ID,
167
+ i: item.id,
168
+ f: process.env.STATUS_FIELD_ID,
169
+ v: { singleSelectOptionId: targetId },
170
+ });
171
+
172
+ corrected++;
173
+ console.log(`Corrected #${item.content.number}: ${currentId ?? 'none'} → ${columnKey}`);
174
+ }
175
+
176
+ console.log(`✅ Reconciliation complete: ${checked} items checked, ${corrected} corrected`);