@guilz-dev/sdlc-gh 0.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 (176) hide show
  1. package/.github/CODEOWNERS +5 -0
  2. package/.github/ISSUE_TEMPLATE/bug_report.yml +68 -0
  3. package/.github/ISSUE_TEMPLATE/config.yml +1 -0
  4. package/.github/ISSUE_TEMPLATE/feature_request.yml +39 -0
  5. package/.github/ISSUE_TEMPLATE/support.yml +56 -0
  6. package/.github/ISSUE_TEMPLATE/task.yml +89 -0
  7. package/.github/agents/implementer.agent.md +17 -0
  8. package/.github/agents/reviewer.agent.md +18 -0
  9. package/.github/agents/triager.agent.md +13 -0
  10. package/.github/aw/actions-lock.json +9 -0
  11. package/.github/copilot-instructions.md +35 -0
  12. package/.github/hooks/hooks.json +12 -0
  13. package/.github/instructions/core.instructions.md +11 -0
  14. package/.github/instructions/profiles/go.instructions.md +10 -0
  15. package/.github/instructions/profiles/php.instructions.md +11 -0
  16. package/.github/instructions/profiles/python.instructions.md +11 -0
  17. package/.github/instructions/profiles/ruby.instructions.md +11 -0
  18. package/.github/instructions/profiles/typescript.instructions.md +11 -0
  19. package/.github/labels.yml +55 -0
  20. package/.github/pull_request_template.md +33 -0
  21. package/.github/ruleset.example.json +33 -0
  22. package/.github/ruleset.harness-eval.example.json +29 -0
  23. package/.github/skills/quality-loop/SKILL.md +23 -0
  24. package/.github/workflows/agent-retry-orchestrator.yml +161 -0
  25. package/.github/workflows/copilot-setup-steps.yml +64 -0
  26. package/.github/workflows/eval-ci.yml +169 -0
  27. package/.github/workflows/eval-drift.yml +75 -0
  28. package/.github/workflows/gh-aw-dogfood-ci.yml +73 -0
  29. package/.github/workflows/harness-ci.yml +244 -0
  30. package/.github/workflows/harness-sync.yml +28 -0
  31. package/.github/workflows/l1-readiness-check.yml +45 -0
  32. package/.github/workflows/labels-sync.yml +24 -0
  33. package/.github/workflows/nightly-harness-review.lock.yml +1643 -0
  34. package/.github/workflows/nightly-harness-review.md +87 -0
  35. package/.github/workflows/nightly-harness-review.yml +63 -0
  36. package/.github/workflows/npm-publish.yml +49 -0
  37. package/.github/workflows/pr-context-comment.yml +138 -0
  38. package/.github/workflows/product-ci-go.yml +33 -0
  39. package/.github/workflows/product-ci-php.yml +39 -0
  40. package/.github/workflows/product-ci-python.yml +34 -0
  41. package/.github/workflows/product-ci-ruby.yml +35 -0
  42. package/.github/workflows/product-ci-ts.yml +37 -0
  43. package/.github/workflows/task-issue-label-sync.yml +50 -0
  44. package/.github/workflows/weekly-redteam.lock.yml +1571 -0
  45. package/.github/workflows/weekly-redteam.md +76 -0
  46. package/.github/zizmor.yml +11 -0
  47. package/AGENTS.md +54 -0
  48. package/LICENSE +21 -0
  49. package/README.md +366 -0
  50. package/config/stacks.json +55 -0
  51. package/docs/adoption.md +126 -0
  52. package/docs/arch.md +535 -0
  53. package/docs/auth-boundaries.md +16 -0
  54. package/docs/coding-agent-l1.md +152 -0
  55. package/docs/exceptions/README.md +25 -0
  56. package/docs/exceptions/TEMPLATE.md +8 -0
  57. package/docs/failure-taxonomy.md +23 -0
  58. package/docs/gh-aw-dogfood.md +109 -0
  59. package/docs/kpi-baseline.md +9 -0
  60. package/docs/nightly-harness-review.md +94 -0
  61. package/docs/operations.md +108 -0
  62. package/docs/publishing.md +79 -0
  63. package/docs/revert-playbook.md +44 -0
  64. package/docs/shared-config.md +30 -0
  65. package/docs/telemetry-artifacts.md +78 -0
  66. package/docs/telemetry-schema.md +60 -0
  67. package/evals/.score-baseline.json +6 -0
  68. package/evals/e2e-bench/README.md +28 -0
  69. package/evals/e2e-bench/manifest.json +16 -0
  70. package/evals/e2e-bench/tasks/e2e-001.yml +10 -0
  71. package/evals/e2e-bench/tasks/e2e-002.yml +11 -0
  72. package/evals/e2e-bench/tasks/e2e-003.yml +10 -0
  73. package/evals/e2e-bench/tasks/e2e-004.yml +14 -0
  74. package/evals/e2e-bench/tasks/e2e-005.yml +11 -0
  75. package/evals/e2e-bench/tasks/e2e-006.yml +10 -0
  76. package/evals/e2e-bench/tasks/e2e-007.yml +10 -0
  77. package/evals/e2e-bench/tasks/e2e-008.yml +10 -0
  78. package/evals/e2e-bench/tasks/e2e-009.yml +10 -0
  79. package/evals/trajectories/rubric.md +12 -0
  80. package/evals/trajectories/test_harness_conventions.py +271 -0
  81. package/infra/README.md +49 -0
  82. package/infra/langfuse/docker-compose.yml +25 -0
  83. package/infra/otel/collector-config.yml +24 -0
  84. package/infra/samples/gh-aw-dogfood-report.json +44 -0
  85. package/infra/samples/harness-review-routing-plan.json +19 -0
  86. package/infra/samples/harness-review-summary.json +61 -0
  87. package/infra/samples/telemetry-artifact.json +29 -0
  88. package/infra/samples/telemetry-payload.json +19 -0
  89. package/package.json +85 -0
  90. package/prompts/triager-classify.prompt.yml +10 -0
  91. package/sample/go/add.go +5 -0
  92. package/sample/go/add_test.go +9 -0
  93. package/sample/go/go.mod +3 -0
  94. package/sample/php/composer.json +26 -0
  95. package/sample/php/composer.lock +1881 -0
  96. package/sample/php/phpunit.xml +8 -0
  97. package/sample/php/src/Add.php +13 -0
  98. package/sample/php/tests/AddTest.php +16 -0
  99. package/sample/python/requirements-dev.txt +2 -0
  100. package/sample/python/src/__init__.py +0 -0
  101. package/sample/python/src/greet.py +3 -0
  102. package/sample/python/tests/conftest.py +4 -0
  103. package/sample/python/tests/test_greet.py +5 -0
  104. package/sample/ruby/.rubocop.yml +10 -0
  105. package/sample/ruby/Gemfile +6 -0
  106. package/sample/ruby/Gemfile.lock +58 -0
  107. package/sample/ruby/lib/add.rb +9 -0
  108. package/sample/ruby/spec/add_spec.rb +11 -0
  109. package/sample/ts/biome.json +6 -0
  110. package/sample/ts/package-lock.json +1763 -0
  111. package/sample/ts/package.json +15 -0
  112. package/sample/ts/src/add.ts +3 -0
  113. package/sample/ts/tests/add.test.ts +8 -0
  114. package/sample/ts/tsconfig.json +12 -0
  115. package/scripts/aggregate-harness-review.mjs +48 -0
  116. package/scripts/bootstrap-harness.sh +411 -0
  117. package/scripts/check-diff-size.mjs +46 -0
  118. package/scripts/check-e2e-manifest.mjs +35 -0
  119. package/scripts/check-eval-score-drift.mjs +31 -0
  120. package/scripts/check-gh-aw-dogfood-scope.mjs +51 -0
  121. package/scripts/check-issue-spec.mjs +215 -0
  122. package/scripts/check-l1-readiness.mjs +82 -0
  123. package/scripts/check-open-pr-limit.mjs +34 -0
  124. package/scripts/doctor.mjs +177 -0
  125. package/scripts/emit-gh-aw-dogfood-report.mjs +112 -0
  126. package/scripts/emit-telemetry-artifact.mjs +99 -0
  127. package/scripts/fetch-telemetry-artifacts.mjs +176 -0
  128. package/scripts/harness-drift-report.mjs +99 -0
  129. package/scripts/lib/bootstrap-copy.mjs +123 -0
  130. package/scripts/lib/ccsd-contract.mjs +212 -0
  131. package/scripts/lib/diff-size.mjs +103 -0
  132. package/scripts/lib/doctor-local.mjs +179 -0
  133. package/scripts/lib/e2e-manifest.mjs +76 -0
  134. package/scripts/lib/gh-aw-dogfood.mjs +293 -0
  135. package/scripts/lib/github-config.mjs +94 -0
  136. package/scripts/lib/harness-ci-fragments.mjs +98 -0
  137. package/scripts/lib/harness-review-routing.mjs +244 -0
  138. package/scripts/lib/harness-review.mjs +388 -0
  139. package/scripts/lib/issue-form-label-sync.mjs +56 -0
  140. package/scripts/lib/l1-readiness.mjs +258 -0
  141. package/scripts/lib/merge-harness-package.mjs +36 -0
  142. package/scripts/lib/npm-package.mjs +129 -0
  143. package/scripts/lib/setup-wizard.mjs +224 -0
  144. package/scripts/lib/stacks.mjs +138 -0
  145. package/scripts/lib/telemetry-artifact.mjs +253 -0
  146. package/scripts/lib/template-root.mjs +39 -0
  147. package/scripts/merge-harness-package.mjs +14 -0
  148. package/scripts/route-harness-review.mjs +168 -0
  149. package/scripts/run-e2e-bench.mjs +216 -0
  150. package/scripts/sdlc-gh-cli.mjs +91 -0
  151. package/scripts/select-eval-jobs.mjs +41 -0
  152. package/scripts/setup-github.mjs +242 -0
  153. package/scripts/setup-github.sh +4 -0
  154. package/scripts/setup-wizard.mjs +426 -0
  155. package/scripts/test-bootstrap-guidance-scenarios.mjs +94 -0
  156. package/scripts/test-diff-size-scenarios.mjs +88 -0
  157. package/scripts/test-doctor-scenarios.mjs +70 -0
  158. package/scripts/test-e2e-manifest-scenarios.mjs +65 -0
  159. package/scripts/test-gh-aw-dogfood-scenarios.mjs +74 -0
  160. package/scripts/test-harness-review-routing-scenarios.mjs +130 -0
  161. package/scripts/test-harness-review-scenarios.mjs +92 -0
  162. package/scripts/test-hooks-scenarios.mjs +44 -0
  163. package/scripts/test-issue-form-label-sync-scenarios.mjs +48 -0
  164. package/scripts/test-issue-spec-scenarios.mjs +258 -0
  165. package/scripts/test-l1-readiness-scenarios.mjs +204 -0
  166. package/scripts/test-merge-harness-package-scenarios.mjs +53 -0
  167. package/scripts/test-npm-package-scenarios.mjs +31 -0
  168. package/scripts/test-sdlc-gh-cli-scenarios.mjs +54 -0
  169. package/scripts/test-setup-github-scenarios.mjs +103 -0
  170. package/scripts/test-setup-wizard-scenarios.mjs +114 -0
  171. package/scripts/test-telemetry-artifact-scenarios.mjs +69 -0
  172. package/scripts/trim-harness-ci.mjs +18 -0
  173. package/scripts/validate-gh-aw-compile.mjs +64 -0
  174. package/scripts/validate-harness.mjs +199 -0
  175. package/scripts/validate-telemetry.mjs +21 -0
  176. package/scripts/verify-bootstrap-stacks.sh +192 -0
@@ -0,0 +1,216 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Run executable acceptance checks for E2E bench tasks.
4
+ *
5
+ * This is not yet a full agentic break/fix runner. It validates that each task
6
+ * has machine-checkable acceptance criteria and that the current fixture/sample
7
+ * state satisfies them, so the outer loop measures more than file existence.
8
+ */
9
+ import { execSync } from "node:child_process";
10
+ import { existsSync, readFileSync } from "node:fs";
11
+ import { join, resolve } from "node:path";
12
+
13
+ const ROOT = process.cwd();
14
+ const manifestPath = join(ROOT, "evals/e2e-bench/manifest.json");
15
+ const tasksDir = join(ROOT, "evals/e2e-bench/tasks");
16
+ const nodeMajor = Number(process.versions.node.split(".")[0]);
17
+
18
+ if (!existsSync(manifestPath)) {
19
+ console.error("Missing evals/e2e-bench/manifest.json");
20
+ process.exit(1);
21
+ }
22
+
23
+ function parseSimpleYaml(path) {
24
+ const out = {};
25
+ let currentListKey = null;
26
+ for (const rawLine of readFileSync(path, "utf8").split(/\r?\n/)) {
27
+ const line = rawLine.replace(/\t/g, " ");
28
+ if (!line.trim() || line.trim().startsWith("#")) continue;
29
+
30
+ const scalar = line.match(/^([A-Za-z0-9_-]+):\s*(.+)$/);
31
+ if (scalar) {
32
+ out[scalar[1]] = scalar[2].trim();
33
+ currentListKey = null;
34
+ continue;
35
+ }
36
+
37
+ const listStart = line.match(/^([A-Za-z0-9_-]+):\s*$/);
38
+ if (listStart) {
39
+ currentListKey = listStart[1];
40
+ out[currentListKey] = [];
41
+ continue;
42
+ }
43
+
44
+ const listItem = line.match(/^\s*-\s+(.+)$/);
45
+ if (listItem && currentListKey) {
46
+ out[currentListKey].push(listItem[1].trim());
47
+ continue;
48
+ }
49
+
50
+ throw new Error(`Unsupported task syntax in ${path}: ${rawLine}`);
51
+ }
52
+ return out;
53
+ }
54
+
55
+ function supportsRequirement(requirement) {
56
+ if (!requirement) return true;
57
+ const nodeReq = requirement.match(/^node>=(\d+)$/);
58
+ if (nodeReq) return nodeMajor >= Number(nodeReq[1]);
59
+ const cmdReq = requirement.match(/^cmd:(.+)$/);
60
+ if (cmdReq) {
61
+ try {
62
+ execSync(`command -v ${cmdReq[1]}`, {
63
+ stdio: "pipe",
64
+ encoding: "utf8",
65
+ });
66
+ return true;
67
+ } catch {
68
+ return false;
69
+ }
70
+ }
71
+ return true;
72
+ }
73
+
74
+ function parseScopedSpec(spec) {
75
+ const parts = spec.split("::");
76
+ if (parts.length < 2) {
77
+ throw new Error(`Invalid verification spec: ${spec}`);
78
+ }
79
+ const scope = parts.shift();
80
+ const payload = parts.join("::");
81
+ let requirement = null;
82
+ let cwd = ".";
83
+ if (scope.includes("@")) {
84
+ [requirement, cwd] = scope.split("@", 2);
85
+ } else {
86
+ cwd = scope;
87
+ }
88
+ return {
89
+ requirement,
90
+ cwd: cwd === "." ? ROOT : resolve(ROOT, cwd),
91
+ payload,
92
+ };
93
+ }
94
+
95
+ function runCommandSpec(spec, taskId, counters) {
96
+ const { requirement, cwd, payload } = parseScopedSpec(spec);
97
+ if (!supportsRequirement(requirement)) {
98
+ counters.skipped++;
99
+ console.warn(`::warning::${taskId}: skipped "${payload}" (${requirement} not available)`);
100
+ return;
101
+ }
102
+ execSync(payload, {
103
+ cwd,
104
+ stdio: "pipe",
105
+ encoding: "utf8",
106
+ });
107
+ counters.executed++;
108
+ }
109
+
110
+ function runContainsSpec(spec, taskId, counters, negate = false) {
111
+ const [file, ...needleParts] = spec.split("::");
112
+ if (!file || needleParts.length === 0) {
113
+ throw new Error(`Invalid file assertion spec: ${spec}`);
114
+ }
115
+ const needle = needleParts.join("::");
116
+ const path = resolve(ROOT, file);
117
+ if (!existsSync(path)) {
118
+ throw new Error(`${taskId}: missing file for assertion: ${file}`);
119
+ }
120
+ const text = readFileSync(path, "utf8");
121
+ const matched = text.includes(needle);
122
+ if (negate ? matched : !matched) {
123
+ throw new Error(
124
+ `${taskId}: ${negate ? "unexpected" : "missing"} content in ${file}: ${needle}`,
125
+ );
126
+ }
127
+ counters.executed++;
128
+ }
129
+
130
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
131
+ const tasks = manifest.tasks || [];
132
+ let failed = 0;
133
+ let executedChecks = 0;
134
+ let skippedChecks = 0;
135
+ const classCounts = {};
136
+ const stackCounts = {};
137
+
138
+ for (const entry of tasks) {
139
+ const id = entry.id;
140
+ if (!id) {
141
+ console.error("::error::Manifest task missing id");
142
+ failed++;
143
+ continue;
144
+ }
145
+
146
+ try {
147
+ const taskPath = join(tasksDir, `${id}.yml`);
148
+ if (!existsSync(taskPath)) {
149
+ throw new Error(`Missing task file: evals/e2e-bench/tasks/${id}.yml`);
150
+ }
151
+
152
+ const task = parseSimpleYaml(taskPath);
153
+ for (const field of ["id", "class", "description", "acceptance"]) {
154
+ if (!task[field] || (Array.isArray(task[field]) && task[field].length === 0)) {
155
+ throw new Error(`${id}.yml missing ${field}`);
156
+ }
157
+ }
158
+ if (task.id !== entry.id) {
159
+ throw new Error(`${id}.yml id does not match manifest`);
160
+ }
161
+ if (entry.class && task.class !== entry.class) {
162
+ throw new Error(`${id}.yml class does not match manifest`);
163
+ }
164
+ if (entry.stack && task.stack !== entry.stack) {
165
+ throw new Error(`${id}.yml stack does not match manifest`);
166
+ }
167
+
168
+ classCounts[task.class] = (classCounts[task.class] || 0) + 1;
169
+ const stackKey = task.stack || entry.stack || "any";
170
+ stackCounts[stackKey] = (stackCounts[stackKey] || 0) + 1;
171
+
172
+ const counters = { executed: 0, skipped: 0 };
173
+ for (const spec of task.verification_commands || []) {
174
+ runCommandSpec(spec, id, counters);
175
+ }
176
+ for (const spec of task.verification_contains || []) {
177
+ runContainsSpec(spec, id, counters, false);
178
+ }
179
+ for (const spec of task.verification_not_contains || []) {
180
+ runContainsSpec(spec, id, counters, true);
181
+ }
182
+
183
+ if (counters.executed === 0 && counters.skipped === 0) {
184
+ throw new Error(`${id}.yml has no executable verification checks`);
185
+ }
186
+
187
+ executedChecks += counters.executed;
188
+ skippedChecks += counters.skipped;
189
+ console.log(
190
+ `E2E ${id}: ok (${counters.executed} check(s) executed${counters.skipped ? `, ${counters.skipped} skipped` : ""})`,
191
+ );
192
+ } catch (error) {
193
+ failed++;
194
+ console.error(`::error::${error.message}`);
195
+ }
196
+ }
197
+
198
+ if (failed > 0) {
199
+ console.error(`E2E bench failed: ${failed} task(s) invalid or non-compliant`);
200
+ process.exit(1);
201
+ }
202
+
203
+ const classSummary = Object.entries(classCounts)
204
+ .sort(([a], [b]) => a.localeCompare(b))
205
+ .map(([name, count]) => `${name}=${count}`)
206
+ .join(", ");
207
+ const stackSummary = Object.entries(stackCounts)
208
+ .sort(([a], [b]) => a.localeCompare(b))
209
+ .map(([name, count]) => `${name}=${count}`)
210
+ .join(", ");
211
+
212
+ console.log(`E2E by class: ${classSummary || "none"}`);
213
+ console.log(`E2E by stack: ${stackSummary || "none"}`);
214
+ console.log(
215
+ `E2E bench: ${tasks.length} task(s), ${executedChecks} executed, ${skippedChecks} skipped`,
216
+ );
@@ -0,0 +1,91 @@
1
+ #!/usr/bin/env node
2
+ import { spawnSync } from "node:child_process";
3
+ import { dirname, join } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ const PACKAGE_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
7
+ const WIZARD = join(PACKAGE_ROOT, "scripts/setup-wizard.mjs");
8
+
9
+ function fail(message) {
10
+ console.error(message);
11
+ process.exit(1);
12
+ }
13
+
14
+ function printHelp() {
15
+ console.log(`sdlc-gh — GitHub Copilot agent harness installer
16
+
17
+ Usage:
18
+ npx @guilz-dev/sdlc-gh [init] [wizard options]
19
+ npx @guilz-dev/sdlc-gh --help
20
+
21
+ Runs the interactive setup wizard: bootstrap (if needed), GitHub labels/rulesets,
22
+ and doctor --strict.
23
+
24
+ Examples:
25
+ cd /path/to/your-product && npx @guilz-dev/sdlc-gh
26
+ npx @guilz-dev/sdlc-gh init --yes --stack ts --codeowners @acme/platform
27
+ npx @guilz-dev/sdlc-gh init --repo /path/to/repo --mode existing --skip-github
28
+
29
+ Wizard options (forwarded):
30
+ --repo <path> Target repository (default: git root or cwd)
31
+ --stack ts|python|go|... Primary stack
32
+ --codeowners @org/team CODEOWNERS owner
33
+ --github-repo owner/name GitHub repository for setup-github
34
+ --mode new|existing Bootstrap mode when harness is missing
35
+ --skip-github Local files only
36
+ --with-eval-ruleset Also apply harness-pr-eval-required ruleset
37
+ --yes Non-interactive
38
+ --dry-run Print plan only
39
+
40
+ Requires Node.js 22+, bash, git, and \`gh\` (authenticated) for GitHub setup.
41
+ macOS / Linux / WSL recommended (bootstrap uses bash).
42
+ `);
43
+ }
44
+
45
+ function parseCliArgs(argv) {
46
+ if (argv.length === 0) {
47
+ return { command: "init", wizardArgs: [] };
48
+ }
49
+
50
+ const first = argv[0];
51
+ if (first === "--help" || first === "-h") {
52
+ return { command: "help", wizardArgs: [] };
53
+ }
54
+ if (first === "init") {
55
+ return { command: "init", wizardArgs: argv.slice(1) };
56
+ }
57
+ if (first.startsWith("-")) {
58
+ return { command: "init", wizardArgs: argv };
59
+ }
60
+
61
+ fail(`Unknown command: ${first}. Run \`npx @guilz-dev/sdlc-gh --help\`.`);
62
+ }
63
+
64
+ function main() {
65
+ const nodeMajor = Number.parseInt(process.versions.node.split(".")[0], 10);
66
+ if (nodeMajor < 22) {
67
+ fail(`Node.js 22+ required (current: ${process.versions.node}).`);
68
+ }
69
+
70
+ const { command, wizardArgs } = parseCliArgs(process.argv.slice(2));
71
+ if (command === "help") {
72
+ printHelp();
73
+ process.exit(0);
74
+ }
75
+
76
+ const result = spawnSync(
77
+ process.execPath,
78
+ [WIZARD, "--template-root", PACKAGE_ROOT, ...wizardArgs],
79
+ {
80
+ stdio: "inherit",
81
+ env: {
82
+ ...process.env,
83
+ SDLCGH_TEMPLATE_ROOT: PACKAGE_ROOT,
84
+ },
85
+ },
86
+ );
87
+
88
+ process.exit(result.status ?? 1);
89
+ }
90
+
91
+ main();
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env node
2
+ /** Select eval jobs based on changed files (arch §5.2.1) */
3
+ import { execSync } from "node:child_process";
4
+ import { appendFileSync } from "node:fs";
5
+
6
+ const base = process.env.BASE_SHA || "origin/main";
7
+ let files = [];
8
+ try {
9
+ files = execSync(
10
+ `git diff --name-only ${base}...HEAD 2>/dev/null || git diff --name-only HEAD~1`,
11
+ { encoding: "utf8" },
12
+ )
13
+ .trim()
14
+ .split("\n")
15
+ .filter(Boolean);
16
+ } catch {
17
+ console.warn("::warning::Could not determine changed files; defaulting to trajectory-conventions");
18
+ }
19
+
20
+ const jobs = new Set();
21
+
22
+ if (files.some((f) => /^prompts\/.*\.prompt\.yml$/.test(f))) jobs.add("prompt-eval");
23
+ if (files.some((f) => f.startsWith(".github/agents/"))) {
24
+ jobs.add("prompt-eval");
25
+ jobs.add("agent-policy");
26
+ }
27
+ if (files.some((f) => f.startsWith(".github/instructions/") || f === "AGENTS.md")) {
28
+ jobs.add("trajectory-conventions");
29
+ }
30
+ if (files.some((f) => f.startsWith(".github/skills/"))) jobs.add("trajectory-task");
31
+ if (files.some((f) => f.startsWith("evals/"))) jobs.add("meta-eval");
32
+
33
+ // Path-filtered workflow with no matching files (e.g. copilot-instructions only)
34
+ if (jobs.size === 0) jobs.add("trajectory-conventions");
35
+
36
+ const out = process.env.GITHUB_OUTPUT;
37
+ if (out) {
38
+ appendFileSync(out, `jobs=${[...jobs].join(",")}\n`);
39
+ } else {
40
+ console.log([...jobs].join(","));
41
+ }
@@ -0,0 +1,242 @@
1
+ #!/usr/bin/env node
2
+ import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join, resolve } from "node:path";
5
+ import { execFileSync, spawnSync } from "node:child_process";
6
+ import readline from "node:readline/promises";
7
+ import { stdin as input, stdout as output } from "node:process";
8
+ import { buildEvalRulesetPayload, buildRulesetPayload, loadLabels } from "./lib/github-config.mjs";
9
+ import { resolveStackId } from "./lib/doctor-local.mjs";
10
+
11
+ function fail(message) {
12
+ console.error(message);
13
+ process.exit(1);
14
+ }
15
+
16
+ function run(cmd, args, options = {}) {
17
+ const stdin = Object.prototype.hasOwnProperty.call(options, "input") ? "pipe" : "inherit";
18
+ const result = spawnSync(cmd, args, {
19
+ encoding: "utf8",
20
+ stdio: [stdin, "pipe", "pipe"],
21
+ ...options,
22
+ });
23
+ if (result.status !== 0) {
24
+ const stderr = result.stderr?.trim();
25
+ if (stderr) console.error(stderr);
26
+ process.exit(result.status ?? 1);
27
+ }
28
+ return result.stdout.trim();
29
+ }
30
+
31
+ function hasGh() {
32
+ return spawnSync("gh", ["--version"], { stdio: "ignore" }).status === 0;
33
+ }
34
+
35
+ function ensureGhAuth() {
36
+ const auth = spawnSync("gh", ["auth", "status"], { stdio: "ignore" });
37
+ if (auth.status !== 0) {
38
+ fail("gh is not authenticated. Run `gh auth login`, then retry. Manual fallback: import .github/ruleset.example.json and apply labels from .github/labels.yml.");
39
+ }
40
+ }
41
+
42
+ function parseArgs(argv) {
43
+ const args = { githubRepo: "", yes: false, dryRun: false, withEvalRuleset: false };
44
+ for (let i = 0; i < argv.length; i += 1) {
45
+ const value = argv[i];
46
+ if (value === "--github-repo") {
47
+ args.githubRepo = argv[i + 1] ?? "";
48
+ i += 1;
49
+ } else if (value === "--yes") {
50
+ args.yes = true;
51
+ } else if (value === "--dry-run") {
52
+ args.dryRun = true;
53
+ } else if (value === "--with-eval-ruleset") {
54
+ args.withEvalRuleset = true;
55
+ } else {
56
+ fail(`Unknown argument: ${value}`);
57
+ }
58
+ }
59
+ return args;
60
+ }
61
+
62
+ function resolveRepoRoot() {
63
+ try {
64
+ return execFileSync("git", ["rev-parse", "--show-toplevel"], {
65
+ encoding: "utf8",
66
+ stdio: ["ignore", "pipe", "ignore"],
67
+ }).trim();
68
+ } catch {
69
+ return process.cwd();
70
+ }
71
+ }
72
+
73
+ function resolveGithubRepo(repoRoot, explicitRepo) {
74
+ if (explicitRepo) return explicitRepo;
75
+ const result = spawnSync("gh", ["repo", "view", "--json", "nameWithOwner"], {
76
+ cwd: repoRoot,
77
+ encoding: "utf8",
78
+ stdio: ["ignore", "pipe", "pipe"],
79
+ });
80
+ if (result.status !== 0) {
81
+ fail("Unable to determine GitHub repository. Re-run with `--github-repo owner/name`.");
82
+ }
83
+ const parsed = JSON.parse(result.stdout);
84
+ return parsed.nameWithOwner;
85
+ }
86
+
87
+ async function confirm(summary, yes) {
88
+ if (yes) return;
89
+ console.log(summary);
90
+ const rl = readline.createInterface({ input, output });
91
+ const answer = await rl.question("Proceed? [y/N]: ");
92
+ rl.close();
93
+ if (!/^(y|yes)$/i.test(answer)) {
94
+ console.log("Cancelled.");
95
+ process.exit(1);
96
+ }
97
+ }
98
+
99
+ function syncLabels(repoRoot, githubRepo, dryRun) {
100
+ const labels = loadLabels(join(repoRoot, ".github/labels.yml"));
101
+ for (const label of labels) {
102
+ const encodedName = encodeURIComponent(label.name);
103
+ const payload = JSON.stringify({
104
+ new_name: label.name,
105
+ color: label.color,
106
+ description: label.description,
107
+ });
108
+
109
+ if (dryRun) {
110
+ console.log(`[dry-run] label ${label.name}: PATCH/POST repos/${githubRepo}/labels`);
111
+ continue;
112
+ }
113
+
114
+ const probe = spawnSync("gh", ["api", `repos/${githubRepo}/labels/${encodedName}`], {
115
+ encoding: "utf8",
116
+ stdio: ["ignore", "pipe", "pipe"],
117
+ });
118
+
119
+ if (probe.status === 0) {
120
+ run("gh", [
121
+ "api",
122
+ "--method",
123
+ "PATCH",
124
+ `repos/${githubRepo}/labels/${encodedName}`,
125
+ "--input",
126
+ "-",
127
+ ], { input: payload });
128
+ } else {
129
+ run("gh", [
130
+ "api",
131
+ "--method",
132
+ "POST",
133
+ `repos/${githubRepo}/labels`,
134
+ "--input",
135
+ "-",
136
+ ], { input: JSON.stringify({ name: label.name, color: label.color, description: label.description }) });
137
+ }
138
+ }
139
+ }
140
+
141
+ function resolveExistingRulesetId(githubRepo, name) {
142
+ const output = run("gh", ["api", `repos/${githubRepo}/rulesets`]);
143
+ const rulesets = JSON.parse(output);
144
+ const match = rulesets.find((ruleset) => ruleset.name === name);
145
+ return match?.id ?? null;
146
+ }
147
+
148
+ function applyRuleset(repoRoot, githubRepo, dryRun, { templateName, buildPayload }) {
149
+ const templatePath = join(repoRoot, `.github/${templateName}`);
150
+ const payload = buildPayload(templatePath);
151
+ const rulesetId = dryRun ? null : resolveExistingRulesetId(githubRepo, payload.name);
152
+ const tempDir = mkdtempSync(join(tmpdir(), "sdlc-gh-ruleset-"));
153
+ const tempFile = join(tempDir, "ruleset.json");
154
+ writeFileSync(tempFile, JSON.stringify(payload, null, 2));
155
+
156
+ try {
157
+ if (dryRun) {
158
+ console.log(`[dry-run] ruleset "${payload.name}" for ${githubRepo}`);
159
+ console.log(readFileSync(tempFile, "utf8"));
160
+ return;
161
+ }
162
+
163
+ if (rulesetId) {
164
+ run("gh", [
165
+ "api",
166
+ "--method",
167
+ "PUT",
168
+ `repos/${githubRepo}/rulesets/${rulesetId}`,
169
+ "--input",
170
+ tempFile,
171
+ ]);
172
+ } else {
173
+ run("gh", [
174
+ "api",
175
+ "--method",
176
+ "POST",
177
+ `repos/${githubRepo}/rulesets`,
178
+ "--input",
179
+ tempFile,
180
+ ]);
181
+ }
182
+ } finally {
183
+ rmSync(tempDir, { recursive: true, force: true });
184
+ }
185
+ }
186
+
187
+ function applyMainRuleset(repoRoot, githubRepo, stackId, dryRun) {
188
+ applyRuleset(repoRoot, githubRepo, dryRun, {
189
+ templateName: "ruleset.example.json",
190
+ buildPayload: (templatePath) => buildRulesetPayload(templatePath, stackId),
191
+ });
192
+ }
193
+
194
+ function applyEvalRuleset(repoRoot, githubRepo, dryRun) {
195
+ applyRuleset(repoRoot, githubRepo, dryRun, {
196
+ templateName: "ruleset.harness-eval.example.json",
197
+ buildPayload: (templatePath) => buildEvalRulesetPayload(templatePath),
198
+ });
199
+ }
200
+
201
+ const args = parseArgs(process.argv.slice(2));
202
+ const repoRoot = resolveRepoRoot();
203
+ const stackFile = join(repoRoot, ".harness-stack");
204
+ const stackFromFile = existsSync(stackFile) ? readFileSync(stackFile, "utf8").trim() : "";
205
+ const stackId = resolveStackId(repoRoot);
206
+ if (stackFromFile && !stackId) {
207
+ fail(`Invalid ${stackFile} value: ${stackFromFile}. Run setup-wizard/bootstrap to set a supported stack id.`);
208
+ }
209
+ if (!stackId) {
210
+ fail(
211
+ `Unable to resolve stack id. Set ${stackFile} or keep exactly one .github/workflows/product-ci-*.yml in this repository.`,
212
+ );
213
+ }
214
+ if (!stackFromFile) {
215
+ console.log(`Info: ${stackFile} is missing; inferred stack=${stackId} from product-ci workflow.`);
216
+ }
217
+
218
+ if (!hasGh()) {
219
+ fail("gh is required. Install it, run `gh auth login`, then retry. Manual fallback: import .github/ruleset.example.json and apply labels from .github/labels.yml.");
220
+ }
221
+ ensureGhAuth();
222
+
223
+ const githubRepo = resolveGithubRepo(repoRoot, args.githubRepo);
224
+ const actions = [
225
+ "sync labels via API",
226
+ "create/update main-protection ruleset",
227
+ ];
228
+ if (args.withEvalRuleset) {
229
+ actions.push("create/update harness-pr-eval-required ruleset (optional)");
230
+ }
231
+ await confirm(
232
+ `GitHub setup summary\n repo: ${repoRoot}\n github repo: ${githubRepo}\n stack: ${stackId}\n actions: ${actions.join(", ")}`,
233
+ args.yes,
234
+ );
235
+
236
+ syncLabels(repoRoot, githubRepo, args.dryRun);
237
+ applyMainRuleset(repoRoot, githubRepo, stackId, args.dryRun);
238
+ if (args.withEvalRuleset) {
239
+ applyEvalRuleset(repoRoot, githubRepo, args.dryRun);
240
+ }
241
+
242
+ console.log(args.dryRun ? "Dry run complete." : "GitHub setup complete.");
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
4
+ exec node "$SCRIPT_DIR/setup-github.mjs" "$@"