@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.
- package/.github/CODEOWNERS +5 -0
- package/.github/ISSUE_TEMPLATE/bug_report.yml +68 -0
- package/.github/ISSUE_TEMPLATE/config.yml +1 -0
- package/.github/ISSUE_TEMPLATE/feature_request.yml +39 -0
- package/.github/ISSUE_TEMPLATE/support.yml +56 -0
- package/.github/ISSUE_TEMPLATE/task.yml +89 -0
- package/.github/agents/implementer.agent.md +17 -0
- package/.github/agents/reviewer.agent.md +18 -0
- package/.github/agents/triager.agent.md +13 -0
- package/.github/aw/actions-lock.json +9 -0
- package/.github/copilot-instructions.md +35 -0
- package/.github/hooks/hooks.json +12 -0
- package/.github/instructions/core.instructions.md +11 -0
- package/.github/instructions/profiles/go.instructions.md +10 -0
- package/.github/instructions/profiles/php.instructions.md +11 -0
- package/.github/instructions/profiles/python.instructions.md +11 -0
- package/.github/instructions/profiles/ruby.instructions.md +11 -0
- package/.github/instructions/profiles/typescript.instructions.md +11 -0
- package/.github/labels.yml +55 -0
- package/.github/pull_request_template.md +33 -0
- package/.github/ruleset.example.json +33 -0
- package/.github/ruleset.harness-eval.example.json +29 -0
- package/.github/skills/quality-loop/SKILL.md +23 -0
- package/.github/workflows/agent-retry-orchestrator.yml +161 -0
- package/.github/workflows/copilot-setup-steps.yml +64 -0
- package/.github/workflows/eval-ci.yml +169 -0
- package/.github/workflows/eval-drift.yml +75 -0
- package/.github/workflows/gh-aw-dogfood-ci.yml +73 -0
- package/.github/workflows/harness-ci.yml +244 -0
- package/.github/workflows/harness-sync.yml +28 -0
- package/.github/workflows/l1-readiness-check.yml +45 -0
- package/.github/workflows/labels-sync.yml +24 -0
- package/.github/workflows/nightly-harness-review.lock.yml +1643 -0
- package/.github/workflows/nightly-harness-review.md +87 -0
- package/.github/workflows/nightly-harness-review.yml +63 -0
- package/.github/workflows/npm-publish.yml +49 -0
- package/.github/workflows/pr-context-comment.yml +138 -0
- package/.github/workflows/product-ci-go.yml +33 -0
- package/.github/workflows/product-ci-php.yml +39 -0
- package/.github/workflows/product-ci-python.yml +34 -0
- package/.github/workflows/product-ci-ruby.yml +35 -0
- package/.github/workflows/product-ci-ts.yml +37 -0
- package/.github/workflows/task-issue-label-sync.yml +50 -0
- package/.github/workflows/weekly-redteam.lock.yml +1571 -0
- package/.github/workflows/weekly-redteam.md +76 -0
- package/.github/zizmor.yml +11 -0
- package/AGENTS.md +54 -0
- package/LICENSE +21 -0
- package/README.md +366 -0
- package/config/stacks.json +55 -0
- package/docs/adoption.md +126 -0
- package/docs/arch.md +535 -0
- package/docs/auth-boundaries.md +16 -0
- package/docs/coding-agent-l1.md +152 -0
- package/docs/exceptions/README.md +25 -0
- package/docs/exceptions/TEMPLATE.md +8 -0
- package/docs/failure-taxonomy.md +23 -0
- package/docs/gh-aw-dogfood.md +109 -0
- package/docs/kpi-baseline.md +9 -0
- package/docs/nightly-harness-review.md +94 -0
- package/docs/operations.md +108 -0
- package/docs/publishing.md +79 -0
- package/docs/revert-playbook.md +44 -0
- package/docs/shared-config.md +30 -0
- package/docs/telemetry-artifacts.md +78 -0
- package/docs/telemetry-schema.md +60 -0
- package/evals/.score-baseline.json +6 -0
- package/evals/e2e-bench/README.md +28 -0
- package/evals/e2e-bench/manifest.json +16 -0
- package/evals/e2e-bench/tasks/e2e-001.yml +10 -0
- package/evals/e2e-bench/tasks/e2e-002.yml +11 -0
- package/evals/e2e-bench/tasks/e2e-003.yml +10 -0
- package/evals/e2e-bench/tasks/e2e-004.yml +14 -0
- package/evals/e2e-bench/tasks/e2e-005.yml +11 -0
- package/evals/e2e-bench/tasks/e2e-006.yml +10 -0
- package/evals/e2e-bench/tasks/e2e-007.yml +10 -0
- package/evals/e2e-bench/tasks/e2e-008.yml +10 -0
- package/evals/e2e-bench/tasks/e2e-009.yml +10 -0
- package/evals/trajectories/rubric.md +12 -0
- package/evals/trajectories/test_harness_conventions.py +271 -0
- package/infra/README.md +49 -0
- package/infra/langfuse/docker-compose.yml +25 -0
- package/infra/otel/collector-config.yml +24 -0
- package/infra/samples/gh-aw-dogfood-report.json +44 -0
- package/infra/samples/harness-review-routing-plan.json +19 -0
- package/infra/samples/harness-review-summary.json +61 -0
- package/infra/samples/telemetry-artifact.json +29 -0
- package/infra/samples/telemetry-payload.json +19 -0
- package/package.json +85 -0
- package/prompts/triager-classify.prompt.yml +10 -0
- package/sample/go/add.go +5 -0
- package/sample/go/add_test.go +9 -0
- package/sample/go/go.mod +3 -0
- package/sample/php/composer.json +26 -0
- package/sample/php/composer.lock +1881 -0
- package/sample/php/phpunit.xml +8 -0
- package/sample/php/src/Add.php +13 -0
- package/sample/php/tests/AddTest.php +16 -0
- package/sample/python/requirements-dev.txt +2 -0
- package/sample/python/src/__init__.py +0 -0
- package/sample/python/src/greet.py +3 -0
- package/sample/python/tests/conftest.py +4 -0
- package/sample/python/tests/test_greet.py +5 -0
- package/sample/ruby/.rubocop.yml +10 -0
- package/sample/ruby/Gemfile +6 -0
- package/sample/ruby/Gemfile.lock +58 -0
- package/sample/ruby/lib/add.rb +9 -0
- package/sample/ruby/spec/add_spec.rb +11 -0
- package/sample/ts/biome.json +6 -0
- package/sample/ts/package-lock.json +1763 -0
- package/sample/ts/package.json +15 -0
- package/sample/ts/src/add.ts +3 -0
- package/sample/ts/tests/add.test.ts +8 -0
- package/sample/ts/tsconfig.json +12 -0
- package/scripts/aggregate-harness-review.mjs +48 -0
- package/scripts/bootstrap-harness.sh +411 -0
- package/scripts/check-diff-size.mjs +46 -0
- package/scripts/check-e2e-manifest.mjs +35 -0
- package/scripts/check-eval-score-drift.mjs +31 -0
- package/scripts/check-gh-aw-dogfood-scope.mjs +51 -0
- package/scripts/check-issue-spec.mjs +215 -0
- package/scripts/check-l1-readiness.mjs +82 -0
- package/scripts/check-open-pr-limit.mjs +34 -0
- package/scripts/doctor.mjs +177 -0
- package/scripts/emit-gh-aw-dogfood-report.mjs +112 -0
- package/scripts/emit-telemetry-artifact.mjs +99 -0
- package/scripts/fetch-telemetry-artifacts.mjs +176 -0
- package/scripts/harness-drift-report.mjs +99 -0
- package/scripts/lib/bootstrap-copy.mjs +123 -0
- package/scripts/lib/ccsd-contract.mjs +212 -0
- package/scripts/lib/diff-size.mjs +103 -0
- package/scripts/lib/doctor-local.mjs +179 -0
- package/scripts/lib/e2e-manifest.mjs +76 -0
- package/scripts/lib/gh-aw-dogfood.mjs +293 -0
- package/scripts/lib/github-config.mjs +94 -0
- package/scripts/lib/harness-ci-fragments.mjs +98 -0
- package/scripts/lib/harness-review-routing.mjs +244 -0
- package/scripts/lib/harness-review.mjs +388 -0
- package/scripts/lib/issue-form-label-sync.mjs +56 -0
- package/scripts/lib/l1-readiness.mjs +258 -0
- package/scripts/lib/merge-harness-package.mjs +36 -0
- package/scripts/lib/npm-package.mjs +129 -0
- package/scripts/lib/setup-wizard.mjs +224 -0
- package/scripts/lib/stacks.mjs +138 -0
- package/scripts/lib/telemetry-artifact.mjs +253 -0
- package/scripts/lib/template-root.mjs +39 -0
- package/scripts/merge-harness-package.mjs +14 -0
- package/scripts/route-harness-review.mjs +168 -0
- package/scripts/run-e2e-bench.mjs +216 -0
- package/scripts/sdlc-gh-cli.mjs +91 -0
- package/scripts/select-eval-jobs.mjs +41 -0
- package/scripts/setup-github.mjs +242 -0
- package/scripts/setup-github.sh +4 -0
- package/scripts/setup-wizard.mjs +426 -0
- package/scripts/test-bootstrap-guidance-scenarios.mjs +94 -0
- package/scripts/test-diff-size-scenarios.mjs +88 -0
- package/scripts/test-doctor-scenarios.mjs +70 -0
- package/scripts/test-e2e-manifest-scenarios.mjs +65 -0
- package/scripts/test-gh-aw-dogfood-scenarios.mjs +74 -0
- package/scripts/test-harness-review-routing-scenarios.mjs +130 -0
- package/scripts/test-harness-review-scenarios.mjs +92 -0
- package/scripts/test-hooks-scenarios.mjs +44 -0
- package/scripts/test-issue-form-label-sync-scenarios.mjs +48 -0
- package/scripts/test-issue-spec-scenarios.mjs +258 -0
- package/scripts/test-l1-readiness-scenarios.mjs +204 -0
- package/scripts/test-merge-harness-package-scenarios.mjs +53 -0
- package/scripts/test-npm-package-scenarios.mjs +31 -0
- package/scripts/test-sdlc-gh-cli-scenarios.mjs +54 -0
- package/scripts/test-setup-github-scenarios.mjs +103 -0
- package/scripts/test-setup-wizard-scenarios.mjs +114 -0
- package/scripts/test-telemetry-artifact-scenarios.mjs +69 -0
- package/scripts/trim-harness-ci.mjs +18 -0
- package/scripts/validate-gh-aw-compile.mjs +64 -0
- package/scripts/validate-harness.mjs +199 -0
- package/scripts/validate-telemetry.mjs +21 -0
- package/scripts/verify-bootstrap-stacks.sh +192 -0
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Validate linked Issue CC-SD contract for L1 task:docs / task:test-fix PRs.
|
|
4
|
+
* Canonical field names live in scripts/lib/ccsd-contract.mjs.
|
|
5
|
+
*/
|
|
6
|
+
import { execSync } from "node:child_process";
|
|
7
|
+
import {
|
|
8
|
+
CCSD_REQUIRED_FIELDS,
|
|
9
|
+
extractClosingIssueNumbers,
|
|
10
|
+
pickLinkedIssue,
|
|
11
|
+
resolveFetchFailureAction,
|
|
12
|
+
shouldEnforceCcsd,
|
|
13
|
+
validateCcsdFields,
|
|
14
|
+
validateLabelShape,
|
|
15
|
+
} from "./lib/ccsd-contract.mjs";
|
|
16
|
+
|
|
17
|
+
function ghJson(cmd) {
|
|
18
|
+
const out = execSync(cmd, {
|
|
19
|
+
encoding: "utf8",
|
|
20
|
+
env: process.env,
|
|
21
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
22
|
+
});
|
|
23
|
+
return JSON.parse(out);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function warn(msg) {
|
|
27
|
+
console.warn(`::warning::${msg}`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function error(msg) {
|
|
31
|
+
console.error(`::error::${msg}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function parseLabelEnv(name) {
|
|
35
|
+
return (process.env[name] || "")
|
|
36
|
+
.split(",")
|
|
37
|
+
.map((s) => s.trim())
|
|
38
|
+
.filter(Boolean);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function enforcedByPrLabels() {
|
|
42
|
+
return shouldEnforceCcsd(parseLabelEnv("PR_LABELS"));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function proxyLabelsForFetchFailure(issueLabels = []) {
|
|
46
|
+
const prLabels = parseLabelEnv("PR_LABELS");
|
|
47
|
+
return [...new Set([...issueLabels, ...prLabels])];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function resolveIssueFromEnv() {
|
|
51
|
+
const issueBody = process.env.ISSUE_BODY;
|
|
52
|
+
const issueLabels = parseLabelEnv("ISSUE_LABELS");
|
|
53
|
+
|
|
54
|
+
if (issueBody !== undefined) {
|
|
55
|
+
return { body: issueBody, labels: issueLabels };
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function fetchIssues(repo, issueNumbers) {
|
|
61
|
+
const fetched = [];
|
|
62
|
+
const failures = [];
|
|
63
|
+
|
|
64
|
+
for (const issueNumber of issueNumbers) {
|
|
65
|
+
try {
|
|
66
|
+
const issue = ghJson(
|
|
67
|
+
`gh issue view ${issueNumber} --repo ${repo} --json body,labels,number`,
|
|
68
|
+
);
|
|
69
|
+
fetched.push({
|
|
70
|
+
body: issue.body,
|
|
71
|
+
labels: (issue.labels || []).map((label) => label.name),
|
|
72
|
+
issueNumber: issue.number,
|
|
73
|
+
});
|
|
74
|
+
} catch (e) {
|
|
75
|
+
failures.push({ issueNumber, message: e.message });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return { fetched, failures };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function resolveIssueFromGitHub() {
|
|
83
|
+
const repo = process.env.GITHUB_REPOSITORY;
|
|
84
|
+
const prNumber = process.env.PR_NUMBER || process.env.GITHUB_EVENT_PR_NUMBER;
|
|
85
|
+
const prBody = process.env.PR_BODY;
|
|
86
|
+
|
|
87
|
+
if (!repo || !prNumber) return null;
|
|
88
|
+
|
|
89
|
+
let body = prBody ?? "";
|
|
90
|
+
let issueNumbers = [];
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
const pr = ghJson(
|
|
94
|
+
`gh pr view ${prNumber} --repo ${repo} --json body,closingIssuesReferences`,
|
|
95
|
+
);
|
|
96
|
+
body = prBody ?? pr.body ?? "";
|
|
97
|
+
issueNumbers = (pr.closingIssuesReferences || []).map((issue) => issue.number);
|
|
98
|
+
} catch {
|
|
99
|
+
// Fall through to body keyword parsing.
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (issueNumbers.length === 0) {
|
|
103
|
+
issueNumbers = extractClosingIssueNumbers(body);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
issueNumbers = [...new Set(issueNumbers)];
|
|
107
|
+
|
|
108
|
+
if (issueNumbers.length === 0) {
|
|
109
|
+
return { noIssue: true };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const { fetched, failures } = fetchIssues(repo, issueNumbers);
|
|
113
|
+
|
|
114
|
+
if (fetched.length === 0) {
|
|
115
|
+
const nums = failures.map((f) => f.issueNumber).join(", ");
|
|
116
|
+
return {
|
|
117
|
+
fetchFailed: true,
|
|
118
|
+
message: `Could not fetch linked issue(s) #${nums}`,
|
|
119
|
+
proxyLabels: proxyLabelsForFetchFailure(),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const picked = pickLinkedIssue(fetched);
|
|
124
|
+
if (picked.kind === "ambiguous") {
|
|
125
|
+
return {
|
|
126
|
+
ambiguous: true,
|
|
127
|
+
issueNumbers: picked.issueNumbers,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (picked.kind === "issue") {
|
|
132
|
+
return {
|
|
133
|
+
body: picked.body,
|
|
134
|
+
labels: picked.labels,
|
|
135
|
+
issueNumber: picked.issueNumber,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return { noIssue: true };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function main() {
|
|
143
|
+
const issue =
|
|
144
|
+
resolveIssueFromEnv() ?? resolveIssueFromGitHub() ?? { noIssue: true };
|
|
145
|
+
|
|
146
|
+
if (issue.fetchFailed) {
|
|
147
|
+
const action = resolveFetchFailureAction(issue.proxyLabels);
|
|
148
|
+
if (action === "fail") {
|
|
149
|
+
error(
|
|
150
|
+
`${issue.message} — cannot verify CC-SD for L1 docs/test-fix delegation`,
|
|
151
|
+
);
|
|
152
|
+
process.exit(1);
|
|
153
|
+
}
|
|
154
|
+
warn(`${issue.message}; skipping CC-SD enforcement`);
|
|
155
|
+
process.exit(0);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (issue.ambiguous) {
|
|
159
|
+
error(
|
|
160
|
+
`PR links multiple L1 docs/test-fix issues (#${issue.issueNumbers.join(", #")}); keep one enforced issue per PR`,
|
|
161
|
+
);
|
|
162
|
+
process.exit(1);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (issue.noIssue) {
|
|
166
|
+
if (enforcedByPrLabels()) {
|
|
167
|
+
error(
|
|
168
|
+
"PR is labeled for L1 docs/test-fix but is not tied to a resolvable Issue; link exactly one Issue with a complete CC-SD contract",
|
|
169
|
+
);
|
|
170
|
+
process.exit(1);
|
|
171
|
+
}
|
|
172
|
+
warn(
|
|
173
|
+
"PR is not tied to a resolvable Issue; skipping CC-SD enforcement (CI uses Issue labels, not the form dropdown)",
|
|
174
|
+
);
|
|
175
|
+
process.exit(0);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const { body, labels, issueNumber } = issue;
|
|
179
|
+
|
|
180
|
+
const labelShape = validateLabelShape(labels);
|
|
181
|
+
if (!labelShape.ok) {
|
|
182
|
+
error(`Issue #${issueNumber}: ${labelShape.message}`);
|
|
183
|
+
process.exit(1);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (!shouldEnforceCcsd(labels)) {
|
|
187
|
+
const taskLabels = labels.filter((l) => l.startsWith("task:")).join(", ") || "none";
|
|
188
|
+
const autonomy = labels.find((l) => l.startsWith("autonomy:")) || "none";
|
|
189
|
+
console.log(
|
|
190
|
+
`CC-SD check skipped (task=${taskLabels}, autonomy=${autonomy}) — v1 enforces only autonomy:L1 on task:docs / task:test-fix`,
|
|
191
|
+
);
|
|
192
|
+
process.exit(0);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const result = validateCcsdFields(body);
|
|
196
|
+
const prefix = issueNumber ? `Issue #${issueNumber}` : "Issue";
|
|
197
|
+
|
|
198
|
+
if (result.missing.length > 0) {
|
|
199
|
+
error(
|
|
200
|
+
`${prefix} missing required CC-SD fields: ${result.missing.join(", ")}. Required: ${CCSD_REQUIRED_FIELDS.join(", ")}`,
|
|
201
|
+
);
|
|
202
|
+
process.exit(1);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (result.placeholder.length > 0) {
|
|
206
|
+
error(
|
|
207
|
+
`${prefix} has placeholder-only CC-SD fields: ${result.placeholder.join(", ")}. Replace template placeholders with real content.`,
|
|
208
|
+
);
|
|
209
|
+
process.exit(1);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
console.log(`${prefix} CC-SD contract complete for L1 docs/test-fix delegation`);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
main();
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { appendFileSync } from "node:fs";
|
|
3
|
+
import { formatReadinessSummary, runReadinessCheck } from "./lib/l1-readiness.mjs";
|
|
4
|
+
|
|
5
|
+
function parseArgs(argv) {
|
|
6
|
+
const args = {
|
|
7
|
+
githubRepo: "",
|
|
8
|
+
template: false,
|
|
9
|
+
strict: false,
|
|
10
|
+
json: false,
|
|
11
|
+
summary: false,
|
|
12
|
+
};
|
|
13
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
14
|
+
const value = argv[i];
|
|
15
|
+
if (value === "--github-repo") {
|
|
16
|
+
args.githubRepo = argv[i + 1] ?? "";
|
|
17
|
+
i += 1;
|
|
18
|
+
} else if (value === "--template") {
|
|
19
|
+
args.template = true;
|
|
20
|
+
} else if (value === "--strict") {
|
|
21
|
+
args.strict = true;
|
|
22
|
+
} else if (value === "--json") {
|
|
23
|
+
args.json = true;
|
|
24
|
+
} else if (value === "--summary") {
|
|
25
|
+
args.summary = true;
|
|
26
|
+
} else if (value === "--help" || value === "-h") {
|
|
27
|
+
printHelp();
|
|
28
|
+
process.exit(0);
|
|
29
|
+
} else {
|
|
30
|
+
console.error(`Unknown argument: ${value}`);
|
|
31
|
+
printHelp();
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return args;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function printHelp() {
|
|
39
|
+
console.log(`Usage: check-l1-readiness.mjs [options]
|
|
40
|
+
|
|
41
|
+
Checks whether spec-driven L1 delegation can run on this repository.
|
|
42
|
+
|
|
43
|
+
Options:
|
|
44
|
+
--github-repo owner/name Explicit GitHub repository name
|
|
45
|
+
--template Template repository mode
|
|
46
|
+
--strict Exit non-zero on SKIP as well as FAIL (same as doctor)
|
|
47
|
+
--json Print machine-readable JSON summary
|
|
48
|
+
--summary Append markdown summary to GITHUB_STEP_SUMMARY when set
|
|
49
|
+
`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function printResult(entry) {
|
|
53
|
+
console.log(`${entry.status} ${entry.label}: ${entry.detail}`);
|
|
54
|
+
if (entry.fix) console.log(` fix: ${entry.fix}`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function printNextSteps(hasFail) {
|
|
58
|
+
console.log("\nNext");
|
|
59
|
+
if (hasFail) {
|
|
60
|
+
console.log("- Fix FAIL items above, then re-run `node scripts/check-l1-readiness.mjs --strict`.");
|
|
61
|
+
} else {
|
|
62
|
+
console.log("- Create a Task issue from `.github/ISSUE_TEMPLATE/task.yml`.");
|
|
63
|
+
console.log("- Fill CC-SD fields, then add labels: `task:docs` or `task:test-fix` + `autonomy:L1`.");
|
|
64
|
+
console.log("- Assign `triager`, then `implementer` to start autonomous Draft PR flow.");
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const args = parseArgs(process.argv.slice(2));
|
|
69
|
+
const report = runReadinessCheck(args);
|
|
70
|
+
|
|
71
|
+
if (args.json) {
|
|
72
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
73
|
+
} else {
|
|
74
|
+
for (const entry of report.entries) printResult(entry);
|
|
75
|
+
printNextSteps(report.hasFail);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (args.summary && process.env.GITHUB_STEP_SUMMARY) {
|
|
79
|
+
appendFileSync(process.env.GITHUB_STEP_SUMMARY, formatReadinessSummary(report));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
process.exit(report.exitCode);
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Safe-outputs substitute (Phase 0–2): warn when author has too many open PRs.
|
|
4
|
+
* See docs/operations.md — gh-aw safe_outputs replaces this in Phase 3+.
|
|
5
|
+
*/
|
|
6
|
+
import { execSync } from "node:child_process";
|
|
7
|
+
|
|
8
|
+
const MAX_OPEN = Number(process.env.HARNESS_MAX_OPEN_PRS || 3);
|
|
9
|
+
const author = process.env.PR_AUTHOR || process.env.GITHUB_ACTOR;
|
|
10
|
+
|
|
11
|
+
if (!author) {
|
|
12
|
+
console.log("No PR author context; skipping open PR limit check");
|
|
13
|
+
process.exit(0);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let count = 0;
|
|
17
|
+
try {
|
|
18
|
+
const out = execSync(
|
|
19
|
+
`gh pr list --author "${author}" --state open --json number --jq 'length'`,
|
|
20
|
+
{ encoding: "utf8", env: process.env },
|
|
21
|
+
);
|
|
22
|
+
count = Number(out.trim()) || 0;
|
|
23
|
+
} catch {
|
|
24
|
+
console.warn("::warning::Could not query open PRs (gh CLI unavailable); skipping");
|
|
25
|
+
process.exit(0);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
console.log(`Open PRs by ${author}: ${count} (warn above ${MAX_OPEN})`);
|
|
29
|
+
|
|
30
|
+
if (count > MAX_OPEN) {
|
|
31
|
+
console.warn(
|
|
32
|
+
`::warning::Author has ${count} open PRs (limit ${MAX_OPEN}). Close or merge before opening more.`,
|
|
33
|
+
);
|
|
34
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { execFileSync, spawnSync } from "node:child_process";
|
|
5
|
+
import { loadLabels } from "./lib/github-config.mjs";
|
|
6
|
+
import { localChecks, resolveStackId, result } from "./lib/doctor-local.mjs";
|
|
7
|
+
|
|
8
|
+
const argv = process.argv.slice(2);
|
|
9
|
+
if (argv.includes("--help") || argv.includes("-h")) {
|
|
10
|
+
console.log(`Usage: doctor.mjs [--strict] [--template]
|
|
11
|
+
|
|
12
|
+
--strict Fail on SKIP checks (default: fail only on FAIL)
|
|
13
|
+
--template Allow multiple product-ci workflows (template repository mode)
|
|
14
|
+
`);
|
|
15
|
+
process.exit(0);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const strict = argv.includes("--strict");
|
|
19
|
+
const templateMode = argv.includes("--template");
|
|
20
|
+
|
|
21
|
+
function resolveRepoRoot() {
|
|
22
|
+
try {
|
|
23
|
+
return execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
24
|
+
encoding: "utf8",
|
|
25
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
26
|
+
}).trim();
|
|
27
|
+
} catch {
|
|
28
|
+
return process.cwd();
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function printResult(entry) {
|
|
33
|
+
console.log(`${entry.status} ${entry.label}: ${entry.detail}`);
|
|
34
|
+
if (entry.fix) console.log(` fix: ${entry.fix}`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function ghAvailable() {
|
|
38
|
+
return spawnSync("gh", ["--version"], { stdio: "ignore" }).status === 0;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function ghAuthed() {
|
|
42
|
+
return spawnSync("gh", ["auth", "status"], { stdio: "ignore" }).status === 0;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function ghJson(args, cwd) {
|
|
46
|
+
const result = spawnSync("gh", args, {
|
|
47
|
+
cwd,
|
|
48
|
+
encoding: "utf8",
|
|
49
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
50
|
+
});
|
|
51
|
+
if (result.status !== 0) {
|
|
52
|
+
throw new Error(result.stderr.trim() || "gh command failed");
|
|
53
|
+
}
|
|
54
|
+
return JSON.parse(result.stdout);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function ghJsonPages(args, cwd) {
|
|
58
|
+
const result = spawnSync("gh", [...args, "--paginate", "--slurp"], {
|
|
59
|
+
cwd,
|
|
60
|
+
encoding: "utf8",
|
|
61
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
62
|
+
});
|
|
63
|
+
if (result.status !== 0) {
|
|
64
|
+
throw new Error(result.stderr.trim() || "gh command failed");
|
|
65
|
+
}
|
|
66
|
+
return JSON.parse(result.stdout);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function githubChecks(repoRoot, stackId) {
|
|
70
|
+
const entries = [];
|
|
71
|
+
if (!ghAvailable()) {
|
|
72
|
+
entries.push(result("SKIP", "GitHub CLI", "gh is not installed", "Install `gh` to verify labels and rulesets."));
|
|
73
|
+
return entries;
|
|
74
|
+
}
|
|
75
|
+
if (!ghAuthed()) {
|
|
76
|
+
entries.push(result("SKIP", "GitHub auth", "gh is not authenticated", "Run `gh auth login` to verify GitHub state."));
|
|
77
|
+
return entries;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (!stackId) {
|
|
81
|
+
entries.push(
|
|
82
|
+
result(
|
|
83
|
+
"SKIP",
|
|
84
|
+
"required status checks",
|
|
85
|
+
"stack could not be resolved",
|
|
86
|
+
"Ensure exactly one product-ci-*.yml workflow exists or run setup-wizard.",
|
|
87
|
+
),
|
|
88
|
+
);
|
|
89
|
+
return entries;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
let repo;
|
|
93
|
+
try {
|
|
94
|
+
repo = ghJson(["repo", "view", "--json", "nameWithOwner"], repoRoot).nameWithOwner;
|
|
95
|
+
} catch (error) {
|
|
96
|
+
entries.push(result("SKIP", "GitHub repo", error.message, "Run doctor from a cloned GitHub repository."));
|
|
97
|
+
return entries;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
let remoteLabels = [];
|
|
101
|
+
try {
|
|
102
|
+
remoteLabels = ghJsonPages(["api", `repos/${repo}/labels`], repoRoot).flat();
|
|
103
|
+
} catch (error) {
|
|
104
|
+
entries.push(result("SKIP", "GitHub labels", error.message, "Run `./scripts/setup-github.sh` or apply `.github/labels.yml` manually."));
|
|
105
|
+
return entries;
|
|
106
|
+
}
|
|
107
|
+
const expectedLabels = loadLabels(join(repoRoot, ".github/labels.yml")).map((label) => label.name);
|
|
108
|
+
const remoteLabelNames = new Set(remoteLabels.map((label) => label.name));
|
|
109
|
+
const missingLabels = expectedLabels.filter((name) => !remoteLabelNames.has(name));
|
|
110
|
+
if (missingLabels.length === 0) {
|
|
111
|
+
entries.push(result("PASS", "GitHub labels", "all required labels exist"));
|
|
112
|
+
} else {
|
|
113
|
+
entries.push(
|
|
114
|
+
result(
|
|
115
|
+
"FAIL",
|
|
116
|
+
"GitHub labels",
|
|
117
|
+
`missing labels: ${missingLabels.join(", ")}`,
|
|
118
|
+
"Run `./scripts/setup-github.sh` to sync labels.",
|
|
119
|
+
),
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
let rulesets = [];
|
|
124
|
+
try {
|
|
125
|
+
rulesets = ghJson(["api", `repos/${repo}/rulesets`], repoRoot);
|
|
126
|
+
} catch (error) {
|
|
127
|
+
entries.push(result("SKIP", "GitHub rulesets", error.message, "Grant repo admin permission or import `.github/ruleset.example.json` manually."));
|
|
128
|
+
return entries;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const ruleset = rulesets.find((entry) => entry.name === "main-protection");
|
|
132
|
+
if (!ruleset) {
|
|
133
|
+
entries.push(result("FAIL", "main-protection ruleset", "missing", "Run `./scripts/setup-github.sh` to create it."));
|
|
134
|
+
return entries;
|
|
135
|
+
}
|
|
136
|
+
entries.push(result("PASS", "main-protection ruleset", "exists"));
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
const details = ghJson(["api", `repos/${repo}/rulesets/${ruleset.id}`], repoRoot);
|
|
140
|
+
if (details.enforcement === "active") {
|
|
141
|
+
entries.push(result("PASS", "ruleset enforcement", "active"));
|
|
142
|
+
} else {
|
|
143
|
+
entries.push(result("FAIL", "ruleset enforcement", `expected active, got ${details.enforcement}`, "Update the ruleset enforcement to active."));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const requiredRule = (details.rules ?? []).find((rule) => rule.type === "required_status_checks");
|
|
147
|
+
const contexts = new Set((requiredRule?.parameters?.required_status_checks ?? []).map((check) => check.context));
|
|
148
|
+
const expectedContexts = ["harness-static", "diff-size", "issue-spec-check", `product-ci-${stackId}`];
|
|
149
|
+
const missingContexts = expectedContexts.filter((context) => !contexts.has(context));
|
|
150
|
+
if (missingContexts.length === 0) {
|
|
151
|
+
entries.push(result("PASS", "required status checks", `includes ${expectedContexts.join(", ")}`));
|
|
152
|
+
} else {
|
|
153
|
+
entries.push(
|
|
154
|
+
result(
|
|
155
|
+
"FAIL",
|
|
156
|
+
"required status checks",
|
|
157
|
+
`missing contexts: ${missingContexts.join(", ")}`,
|
|
158
|
+
"Run `./scripts/setup-github.sh` to update the ruleset.",
|
|
159
|
+
),
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
} catch (error) {
|
|
163
|
+
entries.push(result("SKIP", "ruleset details", error.message, "Inspect the ruleset in GitHub Settings -> Rules."));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return entries;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const repoRoot = resolveRepoRoot();
|
|
170
|
+
const local = localChecks(repoRoot, { templateMode });
|
|
171
|
+
const stackId = local.stackId || resolveStackId(repoRoot);
|
|
172
|
+
const entries = [...local.entries, ...githubChecks(repoRoot, stackId)];
|
|
173
|
+
for (const entry of entries) printResult(entry);
|
|
174
|
+
|
|
175
|
+
const hasFail = entries.some((entry) => entry.status === "FAIL");
|
|
176
|
+
const hasSkip = entries.some((entry) => entry.status === "SKIP");
|
|
177
|
+
process.exit(hasFail || (strict && hasSkip) ? 1 : 0);
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Emit machine-readable gh-aw dogfood evaluation report.
|
|
4
|
+
*/
|
|
5
|
+
import { execSync } from "node:child_process";
|
|
6
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import {
|
|
9
|
+
buildDogfoodReport,
|
|
10
|
+
evaluateDogfoodScope,
|
|
11
|
+
evaluateSafeOutputsForWorkflows,
|
|
12
|
+
GH_AW_SOURCE_WORKFLOWS,
|
|
13
|
+
parseDogfoodLabels,
|
|
14
|
+
parseGhAwLockMetadata,
|
|
15
|
+
} from "./lib/gh-aw-dogfood.mjs";
|
|
16
|
+
|
|
17
|
+
export const DOGFOOD_REPORT_DIR = "dogfood-report";
|
|
18
|
+
|
|
19
|
+
function hasGhAw() {
|
|
20
|
+
try {
|
|
21
|
+
execSync("gh aw version", { stdio: ["pipe", "pipe", "pipe"] });
|
|
22
|
+
return true;
|
|
23
|
+
} catch {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function changedFiles() {
|
|
29
|
+
const base = process.env.BASE_SHA;
|
|
30
|
+
if (!base) return [];
|
|
31
|
+
try {
|
|
32
|
+
return execSync(`git diff --name-only ${base}...HEAD`, { encoding: "utf8" })
|
|
33
|
+
.split("\n")
|
|
34
|
+
.filter(Boolean);
|
|
35
|
+
} catch {
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function evaluateScope() {
|
|
41
|
+
const files = changedFiles();
|
|
42
|
+
const labels = parseDogfoodLabels(process.env.PR_LABELS);
|
|
43
|
+
const { ok, issues } = evaluateDogfoodScope(files, labels);
|
|
44
|
+
return { ok, issues };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Lock metadata header presence (compile step catches byte-level drift). */
|
|
48
|
+
function evaluateLockMetadata() {
|
|
49
|
+
const issues = [];
|
|
50
|
+
for (const wf of GH_AW_SOURCE_WORKFLOWS) {
|
|
51
|
+
if (!existsSync(wf.lock)) {
|
|
52
|
+
issues.push(`${wf.lock} missing`);
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
const metadata = parseGhAwLockMetadata(readFileSync(wf.lock, "utf8"));
|
|
56
|
+
if (!metadata) issues.push(`${wf.lock} missing gh-aw-metadata header`);
|
|
57
|
+
if (!existsSync(wf.md)) issues.push(`${wf.md} missing for ${wf.id}`);
|
|
58
|
+
}
|
|
59
|
+
return { ok: issues.length === 0, issues };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function evaluateCompile() {
|
|
63
|
+
if (!hasGhAw()) {
|
|
64
|
+
return {
|
|
65
|
+
ok: true,
|
|
66
|
+
skipped: true,
|
|
67
|
+
issues: ["gh aw CLI not available"],
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const issues = [];
|
|
72
|
+
for (const wf of GH_AW_SOURCE_WORKFLOWS) {
|
|
73
|
+
if (!existsSync(wf.md) || !existsSync(wf.lock)) continue;
|
|
74
|
+
const before = readFileSync(wf.lock, "utf8");
|
|
75
|
+
try {
|
|
76
|
+
execSync(`gh aw compile ${wf.id}.md`, { stdio: ["pipe", "pipe", "pipe"] });
|
|
77
|
+
} catch (error) {
|
|
78
|
+
issues.push(`${wf.id}: ${error.stderr?.toString().trim() || error.message}`);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
const after = readFileSync(wf.lock, "utf8");
|
|
82
|
+
if (before !== after) issues.push(`${wf.lock} would change after compile`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return { ok: issues.length === 0, skipped: false, issues };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function main() {
|
|
89
|
+
const safeOutputs = evaluateSafeOutputsForWorkflows((path) => readFileSync(path, "utf8"));
|
|
90
|
+
const report = buildDogfoodReport({
|
|
91
|
+
repo: process.env.GITHUB_REPOSITORY || "unknown/unknown",
|
|
92
|
+
scope: evaluateScope(),
|
|
93
|
+
safeOutputs,
|
|
94
|
+
compile: evaluateCompile(),
|
|
95
|
+
lockDrift: evaluateLockMetadata(),
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const outDir = process.env.DOGFOOD_REPORT_DIR || DOGFOOD_REPORT_DIR;
|
|
99
|
+
mkdirSync(outDir, { recursive: true });
|
|
100
|
+
const jsonPath = join(outDir, "gh-aw-dogfood-report.json");
|
|
101
|
+
writeFileSync(jsonPath, `${JSON.stringify(report, null, 2)}\n`, "utf8");
|
|
102
|
+
|
|
103
|
+
console.log(`Wrote ${jsonPath}`);
|
|
104
|
+
console.log(`::notice::gh_aw_dogfood_pass=${report.pass}`);
|
|
105
|
+
|
|
106
|
+
if (!report.pass) {
|
|
107
|
+
console.error("::error::gh-aw dogfood evaluation failed");
|
|
108
|
+
process.exit(1);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
main();
|