solo-cto-agent 1.2.0 → 1.3.1
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/CHANGELOG.md +74 -1
- package/action.yml +101 -0
- package/bin/central-setup.js +352 -0
- package/bin/cli.js +356 -17
- package/bin/cowork-engine.js +187 -1382
- package/bin/diff-guard.js +159 -0
- package/bin/engine/core.js +326 -0
- package/bin/engine/review.js +406 -0
- package/bin/engine/routine.js +406 -0
- package/bin/engine/session.js +323 -0
- package/bin/external-signals.js +3 -3
- package/bin/notify.js +15 -1
- package/bin/plugin-manager.js +195 -0
- package/bin/rework.js +91 -13
- package/bin/safe-log.js +97 -0
- package/bin/self-evolve/error-collector.js +2 -2
- package/bin/self-evolve/external-trends.js +5 -5
- package/bin/self-evolve/feedback-collector.js +1 -1
- package/bin/self-evolve/quality-analyzer.js +3 -3
- package/bin/self-evolve/self-evolve-orchestrator.js +1 -1
- package/bin/self-evolve/skill-improver.js +3 -3
- package/bin/self-evolve/skill-scout.js +7 -7
- package/bin/self-evolve/weekly-report.js +3 -3
- package/bin/telegram-bot.js +561 -0
- package/bin/telegram-wizard.js +0 -6
- package/bin/template-audit.js +104 -0
- package/bin/watch.js +2 -2
- package/docs/cursor.md +310 -0
- package/docs/demo.svg +1 -1
- package/docs/migration-v1.3.md +145 -0
- package/docs/windsurf.md +475 -0
- package/failure-catalog.json +71 -1
- package/index.d.ts +843 -0
- package/package.json +4 -2
- package/templates/central/.github/workflows/telegram-bot-runner.yml +56 -0
- package/templates/central/.github/workflows/telegram-digest.yml +114 -0
- package/templates/orchestrator/CLAUDE.md +75 -0
- package/templates/orchestrator/api/telegram-webhook.js +16 -16
- package/templates/orchestrator/ops/agents/claude-reviewer.js +1 -1
- package/templates/orchestrator/ops/agents/claude-worker.js +5 -5
- package/templates/orchestrator/ops/agents/codex-worker.js +3 -3
- package/templates/orchestrator/ops/agents/cross-reviewer.js +2 -1
- package/templates/orchestrator/ops/agents/status-checker.js +3 -3
- package/templates/orchestrator/ops/config/design-guidelines.json +4 -4
- package/templates/orchestrator/ops/orchestrator/decision-message.js +6 -3
- package/templates/orchestrator/ops/scripts/auto-diagnose.js +7 -7
- package/templates/orchestrator/ops/scripts/daily-briefing.js +5 -5
- package/templates/orchestrator/ops/scripts/decision-queue.js +3 -3
- package/templates/orchestrator/ops/scripts/meta-report.js +3 -3
- package/templates/product-repo/.claude/hooks/post-compact.sh +117 -0
- package/templates/product-repo/.claude/settings.json +22 -0
- package/templates/product-repo/CLAUDE.md +184 -0
- package/templates/workflows/solo-cto-review.yml +1 -1
- package/bin/consensus-review.js +0 -962
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,42 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v1.3.0 (2026-04-17)
|
|
4
|
+
|
|
5
|
+
**Theme**: Tier 3 deep integration features — plugin registry search, setup automation, type system enhancements.
|
|
6
|
+
|
|
7
|
+
### Highlights
|
|
8
|
+
* `solo-cto-agent plugin search <query>` — search npm registry for plugins with "solo-cto-agent-plugin" keyword
|
|
9
|
+
* setup.sh enhancements: `--include-benchmarks` flag to deploy dashboard.html, auto-detection of Cursor/Windsurf editors
|
|
10
|
+
* Auto-copy of editor-specific docs (docs/cursor.md, docs/windsurf.md) based on detected environment
|
|
11
|
+
* Enhanced TypeScript definitions: BenchmarkMetrics, BenchmarkDiffResult, PluginSearchResult, HistoryEntry
|
|
12
|
+
* Full test coverage for plugin search, CLI subcommands, and new features
|
|
13
|
+
|
|
14
|
+
### New: Plugin Search
|
|
15
|
+
* `solo-cto-agent plugin search <query>` fetches from npm registry with fallback to helpful error messages
|
|
16
|
+
* `--json` flag for programmatic consumption
|
|
17
|
+
* Graceful handling of network failures with user-friendly messages
|
|
18
|
+
* Integration with plugin-manager.js for unified plugin experience
|
|
19
|
+
|
|
20
|
+
### New: Setup.sh Enhancements
|
|
21
|
+
* `--include-benchmarks` flag copies benchmarks/dashboard.html to orchestrator directory
|
|
22
|
+
* Auto-detection of Cursor editor (checks ~/.cursor/ directory)
|
|
23
|
+
* Auto-detection of Windsurf editor (checks ~/.windsurf/ directory)
|
|
24
|
+
* Automatic copy of editor-specific documentation to CLAUDE_DIR/docs/
|
|
25
|
+
* Enhanced summary output showing detected editor and installed features
|
|
26
|
+
|
|
27
|
+
### Enhanced Types
|
|
28
|
+
* `BenchmarkMetrics`: Matches metrics-latest.json shape (name, description, value, unit, timestamp)
|
|
29
|
+
* `BenchmarkDiffResult`: For --diff output (baseline, current, delta, percentChange)
|
|
30
|
+
* `PluginSearchResult`: npm registry search results (name, version, description, author, links)
|
|
31
|
+
* `HistoryEntry`: For benchmarks/history/*.json (timestamp, metrics[], changes[])
|
|
32
|
+
|
|
33
|
+
### Tests
|
|
34
|
+
* `tests/plugin-search.test.mjs` — 10 new tests for registry search with HTTP mocking
|
|
35
|
+
* Updated CLI tests for `plugin search` and `plugin list` subcommands
|
|
36
|
+
* All existing tests pass with version bump
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
3
40
|
## v1.2.0 (2026-04-17)
|
|
4
41
|
|
|
5
42
|
**Theme**: Public release polish + cowork-main Phase 2/3 + dual-agent metrics.
|
|
@@ -82,7 +119,43 @@ non-interactive verify in CI, and tear it all down with one command.
|
|
|
82
119
|
|
|
83
120
|
---
|
|
84
121
|
|
|
85
|
-
## Unreleased
|
|
122
|
+
## Unreleased
|
|
123
|
+
|
|
124
|
+
* chore: vscode extension packaging verified (icon, license, gitignore)
|
|
125
|
+
|
|
126
|
+
* fix: routine.js readTier import from personalization (not core)
|
|
127
|
+
|
|
128
|
+
* fix: resolve 2 hanging tests + add vitest timeout config
|
|
129
|
+
|
|
130
|
+
* feat: P3 — GitHub Actions marketplace, VS Code extension, npm release prep
|
|
131
|
+
|
|
132
|
+
* feat: P2 — type sync, template validation CI, migration guide
|
|
133
|
+
|
|
134
|
+
* feat: P1 — cowork-engine split, plugin install, template-audit --apply
|
|
135
|
+
|
|
136
|
+
* security: add API key masking + diff secret detection (P0)
|
|
137
|
+
|
|
138
|
+
* feat: add `setup --central` for centralized workflow architecture
|
|
139
|
+
|
|
140
|
+
* feat(tier3): plugin registry search, setup.sh enhancement, v1.3.0 (#100)
|
|
141
|
+
|
|
142
|
+
* feat: Tier 2 — metrics history, benchmark diff/trend, enhanced validation (#99)
|
|
143
|
+
|
|
144
|
+
* feat: Tier 1 — benchmark CLI, docs, expanded error catalog (#98)
|
|
145
|
+
|
|
146
|
+
* feat: /prs, /dashboard commands + natural language (T3) (#104)
|
|
147
|
+
|
|
148
|
+
* fix: align HTTP timeout with Telegram long-poll timeout (#103)
|
|
149
|
+
|
|
150
|
+
* fix: delete webhook before getUpdates polling (#102)
|
|
151
|
+
|
|
152
|
+
* feat: telegram-bot callback handler + remove experimental gate (#101)
|
|
153
|
+
|
|
154
|
+
* fix: create local ref for base branch in solo-cto-review template
|
|
155
|
+
|
|
156
|
+
* chore: anonymize personal info and internal project names
|
|
157
|
+
|
|
158
|
+
* release: v1.2.0 — public release polish — Toolkit upgrade: per-tool entry points + examples/
|
|
86
159
|
|
|
87
160
|
**Theme**: repositioning from "skill pack" to "toolkit" by splitting the
|
|
88
161
|
docs surface along tool boundaries and filling `examples/` with real
|
package/action.yml
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
name: "Solo CTO Agent — AI Code Review"
|
|
2
|
+
description: "CTO-level AI code review with dual-agent cross-check (Claude + OpenAI), secret detection, and circuit breakers."
|
|
3
|
+
author: "seunghunbae-3svs"
|
|
4
|
+
|
|
5
|
+
branding:
|
|
6
|
+
icon: "shield"
|
|
7
|
+
color: "purple"
|
|
8
|
+
|
|
9
|
+
inputs:
|
|
10
|
+
anthropic_api_key:
|
|
11
|
+
description: "Anthropic API key for Claude reviews"
|
|
12
|
+
required: true
|
|
13
|
+
openai_api_key:
|
|
14
|
+
description: "OpenAI API key for dual-agent cross-review (optional — enables Tier 2+)"
|
|
15
|
+
required: false
|
|
16
|
+
tier:
|
|
17
|
+
description: "Agent tier: maker | builder | cto"
|
|
18
|
+
required: false
|
|
19
|
+
default: "builder"
|
|
20
|
+
mode:
|
|
21
|
+
description: "Review mode: review | dual-review | deep-review"
|
|
22
|
+
required: false
|
|
23
|
+
default: "review"
|
|
24
|
+
target_branch:
|
|
25
|
+
description: "Base branch for diff comparison"
|
|
26
|
+
required: false
|
|
27
|
+
default: "main"
|
|
28
|
+
redact:
|
|
29
|
+
description: "Redact secrets from diff before sending to AI (true/false)"
|
|
30
|
+
required: false
|
|
31
|
+
default: "true"
|
|
32
|
+
fail_on:
|
|
33
|
+
description: "Fail the check if verdict is REQUEST_CHANGES or BLOCKER found (true/false)"
|
|
34
|
+
required: false
|
|
35
|
+
default: "false"
|
|
36
|
+
extra_args:
|
|
37
|
+
description: "Additional CLI arguments passed to solo-cto-agent"
|
|
38
|
+
required: false
|
|
39
|
+
default: ""
|
|
40
|
+
|
|
41
|
+
outputs:
|
|
42
|
+
verdict:
|
|
43
|
+
description: "Review verdict: APPROVE | REQUEST_CHANGES | COMMENT | UNKNOWN"
|
|
44
|
+
issues_count:
|
|
45
|
+
description: "Number of issues found"
|
|
46
|
+
summary:
|
|
47
|
+
description: "One-line review summary"
|
|
48
|
+
|
|
49
|
+
runs:
|
|
50
|
+
using: "composite"
|
|
51
|
+
steps:
|
|
52
|
+
- name: Setup Node.js
|
|
53
|
+
uses: actions/setup-node@v4
|
|
54
|
+
with:
|
|
55
|
+
node-version: "20"
|
|
56
|
+
|
|
57
|
+
- name: Install solo-cto-agent
|
|
58
|
+
shell: bash
|
|
59
|
+
run: npm install -g solo-cto-agent@latest
|
|
60
|
+
|
|
61
|
+
- name: Run review
|
|
62
|
+
id: review
|
|
63
|
+
shell: bash
|
|
64
|
+
env:
|
|
65
|
+
ANTHROPIC_API_KEY: ${{ inputs.anthropic_api_key }}
|
|
66
|
+
OPENAI_API_KEY: ${{ inputs.openai_api_key }}
|
|
67
|
+
SOLO_CTO_TIER: ${{ inputs.tier }}
|
|
68
|
+
run: |
|
|
69
|
+
REVIEW_CMD="${{ inputs.mode }}"
|
|
70
|
+
ARGS="--branch --target ${{ inputs.target_branch }} --json"
|
|
71
|
+
|
|
72
|
+
if [ "${{ inputs.redact }}" = "true" ]; then
|
|
73
|
+
ARGS="$ARGS --redact"
|
|
74
|
+
fi
|
|
75
|
+
|
|
76
|
+
if [ -n "${{ inputs.extra_args }}" ]; then
|
|
77
|
+
ARGS="$ARGS ${{ inputs.extra_args }}"
|
|
78
|
+
fi
|
|
79
|
+
|
|
80
|
+
# Run review and capture output
|
|
81
|
+
OUTPUT=$(solo-cto-agent $REVIEW_CMD $ARGS 2>&1) || true
|
|
82
|
+
echo "$OUTPUT"
|
|
83
|
+
|
|
84
|
+
# Parse JSON output for verdict and issues count
|
|
85
|
+
VERDICT=$(echo "$OUTPUT" | grep -o '"verdict":"[^"]*"' | head -1 | cut -d'"' -f4)
|
|
86
|
+
ISSUES=$(echo "$OUTPUT" | grep -o '"issues":\[[^]]*\]' | head -1 | grep -o '"severity"' | wc -l)
|
|
87
|
+
SUMMARY=$(echo "$OUTPUT" | grep -o '"summary":"[^"]*"' | head -1 | cut -d'"' -f4)
|
|
88
|
+
|
|
89
|
+
echo "verdict=${VERDICT:-UNKNOWN}" >> $GITHUB_OUTPUT
|
|
90
|
+
echo "issues_count=${ISSUES:-0}" >> $GITHUB_OUTPUT
|
|
91
|
+
echo "summary=${SUMMARY:-No summary}" >> $GITHUB_OUTPUT
|
|
92
|
+
|
|
93
|
+
- name: Check fail condition
|
|
94
|
+
if: inputs.fail_on == 'true'
|
|
95
|
+
shell: bash
|
|
96
|
+
run: |
|
|
97
|
+
VERDICT="${{ steps.review.outputs.verdict }}"
|
|
98
|
+
if [ "$VERDICT" = "REQUEST_CHANGES" ]; then
|
|
99
|
+
echo "::error::Review verdict is REQUEST_CHANGES — failing check."
|
|
100
|
+
exit 1
|
|
101
|
+
fi
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/* eslint-disable no-console */
|
|
3
|
+
/**
|
|
4
|
+
* central-setup.js
|
|
5
|
+
*
|
|
6
|
+
* Sets up centralized workflow architecture:
|
|
7
|
+
* - Orchestrator repo: cross-repo workflows (telegram-bot-runner, telegram-digest)
|
|
8
|
+
* - Product repos: repo-local workflows only (solo-cto-review, telegram-notify, solo-cto-pipeline)
|
|
9
|
+
* - Disables cross-repo workflows if found in product repos (prevents notification spam)
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* solo-cto-agent setup --central --org <owner> --orchestrator <repo> --repos <repo1,repo2,...>
|
|
13
|
+
* GITHUB_TOKEN=... solo-cto-agent setup --central
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const https = require("https");
|
|
17
|
+
const fs = require("fs");
|
|
18
|
+
const path = require("path");
|
|
19
|
+
|
|
20
|
+
// ─── Constants ────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
const CROSS_REPO_WORKFLOWS = ["telegram-bot-runner.yml", "telegram-digest.yml"];
|
|
23
|
+
const REPO_LOCAL_WORKFLOWS = [
|
|
24
|
+
"solo-cto-review.yml",
|
|
25
|
+
"solo-cto-pipeline.yml",
|
|
26
|
+
"telegram-notify.yml",
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
const TEMPLATES_DIR = path.join(__dirname, "..", "templates");
|
|
30
|
+
|
|
31
|
+
// ─── GitHub API Helper ─────────────────────────────
|
|
32
|
+
|
|
33
|
+
function githubRequest(method, reqPath, token, body = null) {
|
|
34
|
+
return new Promise((resolve, reject) => {
|
|
35
|
+
const options = {
|
|
36
|
+
hostname: "api.github.com",
|
|
37
|
+
port: 443,
|
|
38
|
+
path: reqPath,
|
|
39
|
+
method,
|
|
40
|
+
headers: {
|
|
41
|
+
"User-Agent": "solo-cto-agent/central-setup",
|
|
42
|
+
Authorization: `token ${token}`,
|
|
43
|
+
Accept: "application/vnd.github.v3+json",
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
if (body) {
|
|
48
|
+
const bodyStr = JSON.stringify(body);
|
|
49
|
+
options.headers["Content-Type"] = "application/json";
|
|
50
|
+
options.headers["Content-Length"] = Buffer.byteLength(bodyStr);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const req = https.request(options, (res) => {
|
|
54
|
+
let data = "";
|
|
55
|
+
res.on("data", (chunk) => (data += chunk));
|
|
56
|
+
res.on("end", () => {
|
|
57
|
+
try {
|
|
58
|
+
resolve({ status: res.statusCode, data: JSON.parse(data) });
|
|
59
|
+
} catch {
|
|
60
|
+
resolve({ status: res.statusCode, data });
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
req.on("error", reject);
|
|
66
|
+
if (body) req.write(JSON.stringify(body));
|
|
67
|
+
req.end();
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ─── Core Logic ────────────────────────────────────
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Get workflow IDs for a repo, filtered by name list
|
|
75
|
+
*/
|
|
76
|
+
async function getWorkflowsByName(token, owner, repo, nameFilter) {
|
|
77
|
+
const resp = await githubRequest(
|
|
78
|
+
"GET",
|
|
79
|
+
`/repos/${owner}/${repo}/actions/workflows`,
|
|
80
|
+
token
|
|
81
|
+
);
|
|
82
|
+
if (resp.status !== 200) return [];
|
|
83
|
+
|
|
84
|
+
return (resp.data.workflows || []).filter((w) => {
|
|
85
|
+
const filename = w.path.split("/").pop();
|
|
86
|
+
return nameFilter.includes(filename);
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Disable a workflow by ID
|
|
92
|
+
*/
|
|
93
|
+
async function disableWorkflow(token, owner, repo, workflowId) {
|
|
94
|
+
const resp = await githubRequest(
|
|
95
|
+
"PUT",
|
|
96
|
+
`/repos/${owner}/${repo}/actions/workflows/${workflowId}/disable`,
|
|
97
|
+
token
|
|
98
|
+
);
|
|
99
|
+
// 204 = success, 403 = already disabled
|
|
100
|
+
return resp.status === 204 || resp.status === 403;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Enable a workflow by ID
|
|
105
|
+
*/
|
|
106
|
+
async function enableWorkflow(token, owner, repo, workflowId) {
|
|
107
|
+
const resp = await githubRequest(
|
|
108
|
+
"PUT",
|
|
109
|
+
`/repos/${owner}/${repo}/actions/workflows/${workflowId}/enable`,
|
|
110
|
+
token
|
|
111
|
+
);
|
|
112
|
+
return resp.status === 204 || resp.status === 403;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Upload a workflow file to a repo via GitHub Contents API
|
|
117
|
+
*/
|
|
118
|
+
async function uploadWorkflow(token, owner, repo, filename, content, message) {
|
|
119
|
+
const apiPath = `/repos/${owner}/${repo}/contents/.github/workflows/${filename}`;
|
|
120
|
+
|
|
121
|
+
// Check if file exists (need sha for update)
|
|
122
|
+
let sha = null;
|
|
123
|
+
const check = await githubRequest("GET", apiPath, token);
|
|
124
|
+
if (check.status === 200 && check.data.sha) {
|
|
125
|
+
sha = check.data.sha;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const body = {
|
|
129
|
+
message,
|
|
130
|
+
content: Buffer.from(content).toString("base64"),
|
|
131
|
+
};
|
|
132
|
+
if (sha) body.sha = sha;
|
|
133
|
+
|
|
134
|
+
const resp = await githubRequest("PUT", apiPath, token, body);
|
|
135
|
+
return resp.status === 200 || resp.status === 201;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Read a template file, replacing placeholders
|
|
140
|
+
*/
|
|
141
|
+
function readTemplate(templatePath, replacements = {}) {
|
|
142
|
+
let content = fs.readFileSync(templatePath, "utf8");
|
|
143
|
+
for (const [key, value] of Object.entries(replacements)) {
|
|
144
|
+
content = content.replace(new RegExp(escapeRegExp(key), "g"), value);
|
|
145
|
+
}
|
|
146
|
+
return content;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function escapeRegExp(s) {
|
|
150
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ─── Main ──────────────────────────────────────────
|
|
154
|
+
|
|
155
|
+
async function centralSetup({
|
|
156
|
+
org,
|
|
157
|
+
orchestrator = "dual-agent-review-orchestrator",
|
|
158
|
+
repos = [],
|
|
159
|
+
token,
|
|
160
|
+
dryRun = false,
|
|
161
|
+
}) {
|
|
162
|
+
if (!token) {
|
|
163
|
+
console.error("❌ GITHUB_TOKEN required.");
|
|
164
|
+
process.exit(1);
|
|
165
|
+
}
|
|
166
|
+
if (!org) {
|
|
167
|
+
console.error("❌ --org required (your GitHub username or org).");
|
|
168
|
+
process.exit(1);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
console.log("");
|
|
172
|
+
console.log("╔══════════════════════════════════════════════════╗");
|
|
173
|
+
console.log("║ solo-cto-agent — Central Control Setup ║");
|
|
174
|
+
console.log("╠══════════════════════════════════════════════════╣");
|
|
175
|
+
console.log(`║ Org: ${org.padEnd(34)}║`);
|
|
176
|
+
console.log(`║ Orchestrator: ${orchestrator.padEnd(34)}║`);
|
|
177
|
+
console.log(`║ Products: ${(repos.join(", ") || "(auto-detect)").padEnd(34).slice(0, 34)}║`);
|
|
178
|
+
console.log("╚══════════════════════════════════════════════════╝");
|
|
179
|
+
console.log("");
|
|
180
|
+
|
|
181
|
+
const results = { orchestrator: [], products: [], disabled: [] };
|
|
182
|
+
|
|
183
|
+
// ──────────────────────────────────────────────
|
|
184
|
+
// Step 1: Orchestrator — ensure cross-repo workflows exist + active
|
|
185
|
+
// ──────────────────────────────────────────────
|
|
186
|
+
console.log("━━━ Step 1: Orchestrator repo ━━━");
|
|
187
|
+
|
|
188
|
+
for (const wfFile of CROSS_REPO_WORKFLOWS) {
|
|
189
|
+
const templatePath = path.join(
|
|
190
|
+
TEMPLATES_DIR,
|
|
191
|
+
"central",
|
|
192
|
+
".github",
|
|
193
|
+
"workflows",
|
|
194
|
+
wfFile
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
if (!fs.existsSync(templatePath)) {
|
|
198
|
+
console.log(`⚠️ Template not found: ${wfFile} (skip)`);
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const replacements = {
|
|
203
|
+
"{{GITHUB_OWNER}}": org,
|
|
204
|
+
"{{ORCHESTRATOR_REPO}}": orchestrator,
|
|
205
|
+
"{{MANAGED_REPOS}}": repos.map((r) => `${org}/${r}`).join(" "),
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const content = readTemplate(templatePath, replacements);
|
|
209
|
+
|
|
210
|
+
if (dryRun) {
|
|
211
|
+
console.log(` [DRY] Would upload ${wfFile} to ${org}/${orchestrator}`);
|
|
212
|
+
} else {
|
|
213
|
+
const ok = await uploadWorkflow(
|
|
214
|
+
token,
|
|
215
|
+
org,
|
|
216
|
+
orchestrator,
|
|
217
|
+
wfFile,
|
|
218
|
+
content,
|
|
219
|
+
`chore: centralize ${wfFile} (solo-cto-agent setup --central)`
|
|
220
|
+
);
|
|
221
|
+
console.log(
|
|
222
|
+
ok
|
|
223
|
+
? ` ✅ ${wfFile} → ${org}/${orchestrator}`
|
|
224
|
+
: ` ❌ Failed: ${wfFile}`
|
|
225
|
+
);
|
|
226
|
+
results.orchestrator.push({ file: wfFile, ok });
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Ensure they're enabled
|
|
231
|
+
const orchWorkflows = await getWorkflowsByName(
|
|
232
|
+
token,
|
|
233
|
+
org,
|
|
234
|
+
orchestrator,
|
|
235
|
+
CROSS_REPO_WORKFLOWS
|
|
236
|
+
);
|
|
237
|
+
for (const wf of orchWorkflows) {
|
|
238
|
+
if (wf.state !== "active") {
|
|
239
|
+
if (!dryRun) await enableWorkflow(token, org, orchestrator, wf.id);
|
|
240
|
+
console.log(` 🔄 Enabled: ${wf.name}`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// ──────────────────────────────────────────────
|
|
245
|
+
// Step 2: Product repos — disable cross-repo workflows
|
|
246
|
+
// ──────────────────────────────────────────────
|
|
247
|
+
console.log("\n━━━ Step 2: Product repos — disable duplicates ━━━");
|
|
248
|
+
|
|
249
|
+
// Auto-detect repos if none specified
|
|
250
|
+
let productRepos = repos;
|
|
251
|
+
if (productRepos.length === 0) {
|
|
252
|
+
console.log(" Auto-detecting repos with cross-repo workflows...");
|
|
253
|
+
const allRepos = await githubRequest(
|
|
254
|
+
"GET",
|
|
255
|
+
`/users/${org}/repos?per_page=100&sort=updated`,
|
|
256
|
+
token
|
|
257
|
+
);
|
|
258
|
+
if (allRepos.status === 200) {
|
|
259
|
+
for (const r of allRepos.data) {
|
|
260
|
+
if (r.name === orchestrator || r.fork || r.archived) continue;
|
|
261
|
+
const crossWfs = await getWorkflowsByName(
|
|
262
|
+
token,
|
|
263
|
+
org,
|
|
264
|
+
r.name,
|
|
265
|
+
CROSS_REPO_WORKFLOWS
|
|
266
|
+
);
|
|
267
|
+
if (crossWfs.some((w) => w.state === "active")) {
|
|
268
|
+
productRepos.push(r.name);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
if (productRepos.length > 0) {
|
|
273
|
+
console.log(
|
|
274
|
+
` Found ${productRepos.length}: ${productRepos.join(", ")}`
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
for (const repo of productRepos) {
|
|
280
|
+
const crossWfs = await getWorkflowsByName(
|
|
281
|
+
token,
|
|
282
|
+
org,
|
|
283
|
+
repo,
|
|
284
|
+
CROSS_REPO_WORKFLOWS
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
for (const wf of crossWfs) {
|
|
288
|
+
if (wf.state === "active") {
|
|
289
|
+
if (dryRun) {
|
|
290
|
+
console.log(` [DRY] Would disable ${wf.name} in ${repo}`);
|
|
291
|
+
} else {
|
|
292
|
+
const ok = await disableWorkflow(token, org, repo, wf.id);
|
|
293
|
+
console.log(
|
|
294
|
+
ok
|
|
295
|
+
? ` ✅ Disabled ${wf.name} in ${repo}`
|
|
296
|
+
: ` ❌ Failed to disable ${wf.name} in ${repo}`
|
|
297
|
+
);
|
|
298
|
+
results.disabled.push({ repo, workflow: wf.name, ok });
|
|
299
|
+
}
|
|
300
|
+
} else {
|
|
301
|
+
console.log(` ⏭️ ${wf.name} in ${repo} — already disabled`);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// ──────────────────────────────────────────────
|
|
307
|
+
// Step 3: Summary
|
|
308
|
+
// ──────────────────────────────────────────────
|
|
309
|
+
console.log("\n━━━ Summary ━━━");
|
|
310
|
+
console.log("");
|
|
311
|
+
console.log(" Architecture:");
|
|
312
|
+
console.log(` Central (${orchestrator}):`);
|
|
313
|
+
console.log(" ├─ telegram-bot-runner.yml (every 15min)");
|
|
314
|
+
console.log(" └─ telegram-digest.yml (every 30min)");
|
|
315
|
+
console.log(" Product repos:");
|
|
316
|
+
console.log(" ├─ solo-cto-review.yml (PR review)");
|
|
317
|
+
console.log(" ├─ solo-cto-pipeline.yml (orchestration)");
|
|
318
|
+
console.log(" └─ telegram-notify.yml (per-repo events)");
|
|
319
|
+
console.log("");
|
|
320
|
+
console.log(" Cross-repo workflows are centralized. No more duplicate notifications.");
|
|
321
|
+
console.log("");
|
|
322
|
+
|
|
323
|
+
// Secrets reminder
|
|
324
|
+
console.log(" ⚠️ Required secrets in orchestrator repo:");
|
|
325
|
+
console.log(" - TELEGRAM_BOT_TOKEN");
|
|
326
|
+
console.log(" - TELEGRAM_CHAT_ID");
|
|
327
|
+
console.log(" - ORCHESTRATOR_PAT (repo + workflow scopes)");
|
|
328
|
+
console.log("");
|
|
329
|
+
|
|
330
|
+
return results;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// ─── CLI Entry ─────────────────────────────────────
|
|
334
|
+
|
|
335
|
+
if (require.main === module) {
|
|
336
|
+
const args = process.argv.slice(2);
|
|
337
|
+
const getArg = (name) => {
|
|
338
|
+
const idx = args.indexOf(name);
|
|
339
|
+
return idx >= 0 && args[idx + 1] ? args[idx + 1] : null;
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
centralSetup({
|
|
343
|
+
org: getArg("--org") || process.env.GITHUB_ORG || "",
|
|
344
|
+
orchestrator:
|
|
345
|
+
getArg("--orchestrator") || "dual-agent-review-orchestrator",
|
|
346
|
+
repos: getArg("--repos") ? getArg("--repos").split(",") : [],
|
|
347
|
+
token: process.env.GITHUB_TOKEN || "",
|
|
348
|
+
dryRun: args.includes("--dry-run"),
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
module.exports = { centralSetup };
|