@windyroad/architect 0.21.4-preview.1108 → 0.21.5-preview.1128
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/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/lib/install-utils.mjs +138 -8
- package/package.json +1 -1
- package/skills/capture-adr/SKILL.md +1 -1
- package/skills/create-adr/SKILL.md +1 -1
- package/skills/review-decisions/SKILL.md +1 -1
- package/skills/review-design/SKILL.md +1 -1
package/lib/install-utils.mjs
CHANGED
|
@@ -4,11 +4,17 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { execSync } from "node:child_process";
|
|
7
|
+
import { createHash } from "node:crypto";
|
|
8
|
+
import { cpSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
9
|
+
import { homedir } from "node:os";
|
|
10
|
+
import { dirname, join, resolve } from "node:path";
|
|
11
|
+
import { fileURLToPath } from "node:url";
|
|
7
12
|
|
|
8
13
|
const MARKETPLACE_REPO = "windyroad/agent-plugins";
|
|
9
14
|
const MARKETPLACE_NAME = "windyroad";
|
|
10
15
|
const CODEX_MARKETPLACE_PATH = ".";
|
|
11
16
|
const CODEX_MARKETPLACE_NAME = "windyroad-local";
|
|
17
|
+
const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
12
18
|
|
|
13
19
|
let _dryRun = false;
|
|
14
20
|
|
|
@@ -82,6 +88,132 @@ export function addCodexMarketplace() {
|
|
|
82
88
|
);
|
|
83
89
|
}
|
|
84
90
|
|
|
91
|
+
function codexMarketplace(pluginName) {
|
|
92
|
+
return `windyroad-${pluginName.replace(/^wr-/, "")}-local`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function codexMarketplaceRoot(pluginName) {
|
|
96
|
+
const version = JSON.parse(readFileSync(join(PACKAGE_ROOT, "package.json"), "utf8")).version;
|
|
97
|
+
return join(process.env.CODEX_HOME || join(homedir(), ".codex"), ".tmp", "marketplaces", `${pluginName}-${version}`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function codexAgentDir(scope) {
|
|
101
|
+
return scope === "user"
|
|
102
|
+
? join(process.env.CODEX_HOME || join(homedir(), ".codex"), "agents")
|
|
103
|
+
: join(process.cwd(), ".codex", "agents");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function codexTerms(text) {
|
|
107
|
+
return text
|
|
108
|
+
.replaceAll("AskUserQuestion", "request_user_input")
|
|
109
|
+
.replaceAll("Agent tool", "native Codex subagent tool")
|
|
110
|
+
.replaceAll("Skill tool", "installed skill invocation")
|
|
111
|
+
.replaceAll("Claude Code", "Codex")
|
|
112
|
+
.replace(/\bClaude\b/g, "Codex")
|
|
113
|
+
.replaceAll(".claude", ".codex");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function splitFrontmatter(markdown) {
|
|
117
|
+
const end = markdown.indexOf("\n---\n", 4);
|
|
118
|
+
return markdown.startsWith("---\n") && end !== -1
|
|
119
|
+
? { frontmatter: markdown.slice(4, end), body: markdown.slice(end + 5) }
|
|
120
|
+
: { frontmatter: "", body: markdown };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function frontmatterDescription(frontmatter) {
|
|
124
|
+
const lines = frontmatter.split(/\r?\n/);
|
|
125
|
+
const start = lines.findIndex((line) => line.startsWith("description:"));
|
|
126
|
+
if (start === -1) return "Windy Road reviewer.";
|
|
127
|
+
const value = [lines[start].slice("description:".length).trim()];
|
|
128
|
+
for (let index = start + 1; index < lines.length && /^\s+/.test(lines[index]); index += 1) {
|
|
129
|
+
value.push(lines[index].trim());
|
|
130
|
+
}
|
|
131
|
+
return value.filter(Boolean).join(" ");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function renderCodexAgent(pluginName, agent) {
|
|
135
|
+
const source = join(PACKAGE_ROOT, agent.source);
|
|
136
|
+
const { frontmatter, body } = splitFrontmatter(readFileSync(source, "utf8"));
|
|
137
|
+
const codexInstructions = agent.name === "wr-voice-tone:external-comms"
|
|
138
|
+
? `\n\n## Codex completion marker compatibility\n\nOn PASS, compute the lowercase SHA-256 marker key using the normalization specified above and append \`EXTERNAL_COMMS_VOICE_TONE_KEY: <64 lowercase hex characters>\`. Codex may hide the spawn prompt from PostToolUse hooks, so this emitted key is required. On FAIL, do not emit a key.`
|
|
139
|
+
: "";
|
|
140
|
+
const payload = [
|
|
141
|
+
`# Do not edit by hand; update ${agent.source} and reinstall.`,
|
|
142
|
+
`name = ${JSON.stringify(agent.name)}`,
|
|
143
|
+
`description = ${JSON.stringify(frontmatterDescription(frontmatter))}`,
|
|
144
|
+
'sandbox_mode = "read-only"',
|
|
145
|
+
'developer_instructions = """',
|
|
146
|
+
codexTerms(`${body.trimEnd()}${codexInstructions}`).replace(/\\/g, "\\\\").replace(/"""/g, '\\"\\"\\"').trimEnd(),
|
|
147
|
+
'"""',
|
|
148
|
+
"",
|
|
149
|
+
].join("\n");
|
|
150
|
+
const owner = `# Generated by @windyroad/${pluginName.replace(/^wr-/, "")} from ${agent.source}.`;
|
|
151
|
+
const hash = createHash("sha256").update(payload).digest("hex");
|
|
152
|
+
return `${owner}\n# Generated content SHA-256: ${hash}\n${payload}`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function isOwnedCodexAgent(content, pluginName, agent) {
|
|
156
|
+
const owner = `# Generated by @windyroad/${pluginName.replace(/^wr-/, "")} from ${agent.source}.`;
|
|
157
|
+
const lines = content.split("\n");
|
|
158
|
+
const hash = lines[1]?.match(/^# Generated content SHA-256: ([0-9a-f]{64})$/)?.[1];
|
|
159
|
+
return content.startsWith(`${owner}\n`) && Boolean(hash)
|
|
160
|
+
&& createHash("sha256").update(lines.slice(2).join("\n")).digest("hex") === hash;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function installCodexAgents(pluginName, agents, scope) {
|
|
164
|
+
if (agents.length === 0 || _dryRun) return;
|
|
165
|
+
const targetDir = codexAgentDir(scope);
|
|
166
|
+
mkdirSync(targetDir, { recursive: true });
|
|
167
|
+
for (const agent of agents) {
|
|
168
|
+
const target = join(targetDir, agent.filename);
|
|
169
|
+
const expected = renderCodexAgent(pluginName, agent);
|
|
170
|
+
if (existsSync(target)) {
|
|
171
|
+
const current = readFileSync(target, "utf8");
|
|
172
|
+
if (current === expected) continue;
|
|
173
|
+
if (!isOwnedCodexAgent(current, pluginName, agent)) {
|
|
174
|
+
console.log(`Preserved user-managed Codex agent at ${target}.`);
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
writeFileSync(target, expected, "utf8");
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function uninstallCodexAgents(pluginName, agents, scope) {
|
|
183
|
+
if (_dryRun) return;
|
|
184
|
+
for (const targetDir of new Set([codexAgentDir(scope), codexAgentDir("user")])) {
|
|
185
|
+
for (const agent of agents) {
|
|
186
|
+
const target = join(targetDir, agent.filename);
|
|
187
|
+
if (existsSync(target) && isOwnedCodexAgent(readFileSync(target, "utf8"), pluginName, agent)) rmSync(target);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function installPackedCodexPlugin(pluginName, { agents = [], scope = "project" } = {}) {
|
|
193
|
+
const marketplace = codexMarketplace(pluginName);
|
|
194
|
+
const root = codexMarketplaceRoot(pluginName);
|
|
195
|
+
if (!_dryRun) {
|
|
196
|
+
rmSync(root, { recursive: true, force: true });
|
|
197
|
+
mkdirSync(dirname(root), { recursive: true });
|
|
198
|
+
cpSync(PACKAGE_ROOT, root, { recursive: true });
|
|
199
|
+
const hooks = join(root, "hooks-codex", "hooks.json");
|
|
200
|
+
if (existsSync(hooks)) cpSync(hooks, join(root, "hooks", "hooks.json"));
|
|
201
|
+
}
|
|
202
|
+
if (!run(`codex plugin marketplace add ${JSON.stringify(root)}`, `Codex marketplace: ${marketplace}`)) return false;
|
|
203
|
+
if (!run(`codex plugin add ${pluginName}@${marketplace}`, pluginName)) return false;
|
|
204
|
+
installCodexAgents(pluginName, agents, scope);
|
|
205
|
+
return true;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function uninstallPackedCodexPlugin(pluginName, { agents = [], scope = "project" } = {}) {
|
|
209
|
+
const marketplace = codexMarketplace(pluginName);
|
|
210
|
+
const removed = run(`codex plugin remove ${pluginName}@${marketplace}`, `Removing ${pluginName}`);
|
|
211
|
+
run(`codex plugin marketplace remove ${marketplace}`, `Removing ${marketplace}`);
|
|
212
|
+
uninstallCodexAgents(pluginName, agents, scope);
|
|
213
|
+
if (!_dryRun) rmSync(codexMarketplaceRoot(pluginName), { recursive: true, force: true });
|
|
214
|
+
return removed;
|
|
215
|
+
}
|
|
216
|
+
|
|
85
217
|
export function installPlugin(pluginName, { scope = "project" } = {}) {
|
|
86
218
|
return run(
|
|
87
219
|
`claude plugin install ${pluginName}@${MARKETPLACE_NAME} --scope ${scope}`,
|
|
@@ -121,7 +253,7 @@ export function uninstallCodexPlugin(pluginName) {
|
|
|
121
253
|
/**
|
|
122
254
|
* Install a single package: marketplace add + plugin install.
|
|
123
255
|
*/
|
|
124
|
-
export function installPackage(pluginName, { deps = [], scope = "project", runtime = "claude" } = {}) {
|
|
256
|
+
export function installPackage(pluginName, { agents = [], deps = [], scope = "project", runtime = "claude" } = {}) {
|
|
125
257
|
console.log(`\nInstalling @windyroad/${pluginName.replace("wr-", "")} (${scope} scope)...\n`);
|
|
126
258
|
|
|
127
259
|
if (runtime === "claude" || runtime === "both") {
|
|
@@ -130,8 +262,7 @@ export function installPackage(pluginName, { deps = [], scope = "project", runti
|
|
|
130
262
|
}
|
|
131
263
|
|
|
132
264
|
if (runtime === "codex" || runtime === "both") {
|
|
133
|
-
|
|
134
|
-
installCodexPlugin(pluginName);
|
|
265
|
+
if (!installPackedCodexPlugin(pluginName, { agents, scope })) process.exitCode = 1;
|
|
135
266
|
}
|
|
136
267
|
|
|
137
268
|
if (deps.length > 0) {
|
|
@@ -149,7 +280,7 @@ export function installPackage(pluginName, { deps = [], scope = "project", runti
|
|
|
149
280
|
/**
|
|
150
281
|
* Update a single package.
|
|
151
282
|
*/
|
|
152
|
-
export function updatePackage(pluginName, { scope = "project", runtime = "claude" } = {}) {
|
|
283
|
+
export function updatePackage(pluginName, { agents = [], scope = "project", runtime = "claude" } = {}) {
|
|
153
284
|
console.log(`\nUpdating @windyroad/${pluginName.replace("wr-", "")}...\n`);
|
|
154
285
|
|
|
155
286
|
if (runtime === "claude" || runtime === "both") {
|
|
@@ -161,8 +292,7 @@ export function updatePackage(pluginName, { scope = "project", runtime = "claude
|
|
|
161
292
|
}
|
|
162
293
|
|
|
163
294
|
if (runtime === "codex" || runtime === "both") {
|
|
164
|
-
|
|
165
|
-
installCodexPlugin(pluginName);
|
|
295
|
+
if (!installPackedCodexPlugin(pluginName, { agents, scope })) process.exitCode = 1;
|
|
166
296
|
}
|
|
167
297
|
|
|
168
298
|
console.log(`\nDone! Restart ${runtime === "codex" ? "Codex" : runtime === "both" ? "Claude Code and Codex" : "Claude Code"} to apply updates.\n`);
|
|
@@ -171,7 +301,7 @@ export function updatePackage(pluginName, { scope = "project", runtime = "claude
|
|
|
171
301
|
/**
|
|
172
302
|
* Uninstall a single package.
|
|
173
303
|
*/
|
|
174
|
-
export function uninstallPackage(pluginName, { runtime = "claude" } = {}) {
|
|
304
|
+
export function uninstallPackage(pluginName, { agents = [], scope = "project", runtime = "claude" } = {}) {
|
|
175
305
|
console.log(`\nUninstalling @windyroad/${pluginName.replace("wr-", "")}...\n`);
|
|
176
306
|
|
|
177
307
|
if (runtime === "claude" || runtime === "both") {
|
|
@@ -179,7 +309,7 @@ export function uninstallPackage(pluginName, { runtime = "claude" } = {}) {
|
|
|
179
309
|
}
|
|
180
310
|
|
|
181
311
|
if (runtime === "codex" || runtime === "both") {
|
|
182
|
-
|
|
312
|
+
if (!uninstallPackedCodexPlugin(pluginName, { agents, scope })) process.exitCode = 1;
|
|
183
313
|
}
|
|
184
314
|
|
|
185
315
|
console.log(`\nDone. Restart ${runtime === "codex" ? "Codex" : runtime === "both" ? "Claude Code and Codex" : "Claude Code"} to apply changes.\n`);
|
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
name:
|
|
2
|
+
name: capture-adr
|
|
3
3
|
description: Lightweight ADR-capture skill for aside-invocation during foreground work — derives full MADR substance (Considered Options / Decision Drivers / Consequences / Confirmation / Reassessment) silently from the in-session decision context, single commit, no inline architect-review handoff, no request_user_input. Lightweight means zero-interaction + single-commit, not skimpy content (the full-substance capture implementation design). Use this when the user (or agent mid-iter) wants to record a decision quickly without the ~10-15 turn ceremony of /wr-architect:create-adr. For interactive full-intake new ADR creation where the user authors the options + drivers + consequences + confirmation, use /wr-architect:create-adr.
|
|
4
4
|
allowed-tools: Read, Write, Edit, Bash, Grep, Glob
|
|
5
5
|
---
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
name:
|
|
2
|
+
name: create-adr
|
|
3
3
|
description: Create a new Architecture Decision Record (MADR 4.0) in docs/decisions/. Examines existing decisions, asks about the problem and options, and writes a properly formatted ADR.
|
|
4
4
|
allowed-tools: Read, Write, Edit, Bash, Glob, Grep, request_user_input
|
|
5
5
|
---
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
name:
|
|
2
|
+
name: review-decisions
|
|
3
3
|
description: Drain the set of recorded decisions (ADRs) that lack human oversight. Surfaces each unconfirmed ADR's chosen option and alternatives via request_user_input so a human confirms, amends, or rejects the auto-made call, then writes the human-oversight marker. Use when the session-start nudge reports decisions lack oversight, or any time you want to review recorded decisions.
|
|
4
4
|
allowed-tools: Read, Glob, Grep, Bash, Edit, request_user_input
|
|
5
5
|
---
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
name:
|
|
2
|
+
name: review-design
|
|
3
3
|
description: On-demand architecture compliance review. Checks staged changes and recent commits against existing ADRs in docs/decisions/. Use before editing architecture-bearing files or before a release.
|
|
4
4
|
allowed-tools: Read, Glob, Grep, Bash, request_user_input, Skill
|
|
5
5
|
---
|