@windyroad/architect 1.1.0-preview.1304 → 1.1.1-preview.1385

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.
@@ -123,5 +123,5 @@
123
123
  }
124
124
  },
125
125
  "name": "wr-architect",
126
- "version": "1.1.0"
126
+ "version": "1.1.1"
127
127
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wr-architect",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
4
4
  "description": "Architecture decision enforcement for AI coding agents",
5
5
  "author": {
6
6
  "name": "Windy Road Technology",
@@ -5,10 +5,13 @@ import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameS
5
5
  import { dirname, join } from "node:path";
6
6
  import { spawnSync } from "node:child_process";
7
7
  import { fileURLToPath } from "node:url";
8
- import { CLOSE_TOOLS, SPAWN_TOOLS, WAIT_TOOLS, completionTarget, response } from "./lib/codex-completion-input.mjs";
8
+ import { CLOSE_TOOLS, SPAWN_TOOLS, WAIT_TOOLS, completionTarget, response, reviewerDispatch, reviewerInstructionsMatch } from "./lib/codex-completion-input.mjs";
9
9
 
10
10
  const hookDir = dirname(fileURLToPath(import.meta.url));
11
11
  const role = "wr-architect:agent";
12
+ function reviewerInstructions(candidate) {
13
+ return candidate === role ? readFileSync(join(hookDir, "..", "agents", "agent.md"), "utf8") : null;
14
+ }
12
15
  const ttlText = process.env.REVIEW_TTL ?? "3600";
13
16
  const ttl = Number(ttlText) * 1000;
14
17
 
@@ -112,24 +115,26 @@ function rememberNative(input) {
112
115
  }
113
116
 
114
117
  function remember(input) {
115
- if (existsSync(nativePath(input.session_id))) return;
118
+ const dispatch = reviewerDispatch(input, reviewerInstructions);
119
+ if (!dispatch) return;
120
+ if (!dispatch.reviewerTransport && existsSync(nativePath(input.session_id))) return;
116
121
  reapTransport();
117
122
  const result = response(input);
118
123
  const target = normalizeTarget(result.agent_id ?? result.task_name);
119
124
  if (!target) return;
120
125
  mkdirSync(riskDir(input.session_id), { recursive: true });
121
126
  clear(input.session_id, target);
122
- if (input.tool_input?.agent_type !== role) return;
123
127
  const bound = checkout(input.cwd || process.cwd());
124
128
  const hash = bound && policyHash(bound.root);
125
129
  if (!bound || !hash) return diagnostic(bound ? "policy-hash-failed" : "invalid-spawn-checkout", input);
126
- const registered = { parentSession: input.session_id, role, target, ...bound, policyHash: hash, createdAt: Date.now() };
130
+ const registered = { parentSession: input.session_id, role, target, reviewerTransport: dispatch.reviewerTransport, reviewerInstructionsHash: dispatch.reviewerInstructionsHash, ...bound, policyHash: hash, createdAt: Date.now() };
127
131
  writeFileSync(statePath(input.session_id, target), JSON.stringify(registered), { mode: 0o600 });
128
132
  mkdirSync(transportDir(), { recursive: true, mode: 0o700 });
129
133
  writeFileSync(registrationPath(input.session_id, target), JSON.stringify(registered), { mode: 0o600 });
130
134
  }
131
135
 
132
136
  function valid(registered, input, target, age) {
137
+ if (!reviewerInstructionsMatch(registered, reviewerInstructions)) return diagnostic("reviewer-instructions-changed", input), false;
133
138
  if (registered.role !== role || registered.target !== target) return diagnostic("registration-mismatch", input), false;
134
139
  if (!Number.isFinite(age) || age < 0) return diagnostic("invalid-registration-age", input), false;
135
140
  if (age >= ttl) return diagnostic("stale-registration", input), false;
@@ -159,7 +164,6 @@ function writeMarker(input, registered, target, output, completedAt, claim, done
159
164
  }
160
165
 
161
166
  function complete(input, rawTarget, output) {
162
- if (existsSync(nativePath(input.session_id))) return;
163
167
  const target = completionTarget(rawTarget, riskDir(input.session_id), "codex-architect-");
164
168
  if (!target || typeof output !== "string" || !output) return;
165
169
  const state = statePath(input.session_id, target);
@@ -167,6 +171,7 @@ function complete(input, rawTarget, output) {
167
171
  let registered, age;
168
172
  try { registered = JSON.parse(readFileSync(state, "utf8")); age = Date.now() - statSync(state).mtimeMs; }
169
173
  catch { return diagnostic("malformed-registration", input); }
174
+ if (existsSync(nativePath(input.session_id)) && registered.reviewerTransport !== "general") return;
170
175
  if (!valid(registered, input, target, age)) {
171
176
  rmSync(state, { force: true });
172
177
  rmSync(registrationPath(registered.parentSession || input.session_id, target), { force: true });
@@ -220,7 +225,7 @@ function consume(input) {
220
225
  let pending;
221
226
  try { pending = JSON.parse(readFileSync(path, "utf8")); } catch { diagnostic("malformed-receipt", input); continue; }
222
227
  if (pending.parentSession !== input.session_id || pending.role !== role) continue;
223
- if (existsSync(nativePath(input.session_id)) && pending.source !== "subagent-start") continue;
228
+ if (existsSync(nativePath(input.session_id)) && pending.source !== "subagent-start" && pending.reviewerTransport !== "general") continue;
224
229
  if (!valid(pending, input, pending.target, Date.now() - pending.completedAt)) continue;
225
230
  const claim = `${path}.claim`;
226
231
  try { writeFileSync(claim, "", { flag: "wx", mode: 0o600 }); }
@@ -1,4 +1,43 @@
1
- import { readdirSync } from "node:fs";
1
+ import { readdirSync, readFileSync } from "node:fs";
2
+ import { createHash } from "node:crypto";
3
+ import { resolve } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ export function reviewerFallbackPrompt(role, instructions, request) {
7
+ return `WR_REVIEWER_FALLBACK: ${role}\n<reviewer-instructions>\n${instructions}</reviewer-instructions>\n\n${request}`;
8
+ }
9
+
10
+ // A general reviewer is identified by the installed contract actually supplied
11
+ // at dispatch, never by its task name or its returned verdict.
12
+ export function reviewerDispatch(input, loadInstructions) {
13
+ const options = input.tool_input || {};
14
+ const prompt = options.message ?? options.prompt ?? "";
15
+ const named = options.agent_type;
16
+ try {
17
+ if (named && named !== "default") {
18
+ return loadInstructions(named) ? { role: named, prompt } : null;
19
+ }
20
+ if (options.fork_turns !== "none" || typeof prompt !== "string") return null;
21
+ const role = prompt.match(/^WR_REVIEWER_FALLBACK: ([a-z0-9-]+:[a-z0-9-]+)\n/)?.[1];
22
+ if (!role) return null;
23
+ const instructions = loadInstructions(role);
24
+ if (!instructions) return null;
25
+ const prefix = `WR_REVIEWER_FALLBACK: ${role}\n<reviewer-instructions>\n${instructions}</reviewer-instructions>\n\n`;
26
+ if (!prompt.startsWith(prefix) || !prompt.slice(prefix.length).trim()) return null;
27
+ return { role, prompt: prompt.slice(prefix.length), reviewerTransport: "general",
28
+ reviewerInstructionsHash: createHash("sha256").update(instructions).digest("hex") };
29
+ } catch { return null; }
30
+ }
31
+
32
+ export function reviewerInstructionsMatch(registered, loadInstructions) {
33
+ if (registered.reviewerTransport === undefined) return true;
34
+ if (registered.reviewerTransport !== "general") return false;
35
+ try {
36
+ const instructions = loadInstructions(registered.role);
37
+ return Boolean(instructions && registered.reviewerInstructionsHash ===
38
+ createHash("sha256").update(instructions).digest("hex"));
39
+ } catch { return false; }
40
+ }
2
41
 
3
42
  // Resolve only this session's state files, including completed claims so an
4
43
  // ambiguous short name cannot consume a different reviewer after delivery.
@@ -63,3 +102,14 @@ export function parsedResponse(value) {
63
102
  export function response(input) {
64
103
  return parsedResponse(input?.tool_response);
65
104
  }
105
+
106
+ // This only prepares a prompt. Hooks independently verify their own installed
107
+ // instruction source before registering or accepting a general review.
108
+ if (process.argv[2] === "--fallback-prompt" && process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
109
+ if (process.argv[2] !== "--fallback-prompt" || process.argv.length !== 5) {
110
+ console.error("Usage: codex-completion-input.mjs --fallback-prompt <role> <installed-agent.md> < request.txt");
111
+ process.exitCode = 1;
112
+ } else {
113
+ process.stdout.write(reviewerFallbackPrompt(process.argv[3], readFileSync(process.argv[4], "utf8"), readFileSync(0, "utf8")));
114
+ }
115
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@windyroad/architect",
3
- "version": "1.1.0-preview.1304",
3
+ "version": "1.1.1-preview.1385",
4
4
  "description": "Architecture decision enforcement for AI coding agents",
5
5
  "bin": {
6
6
  "windyroad-architect": "./bin/install.mjs"
@@ -12,7 +12,14 @@ allowed-tools: Read, Write, Edit, Bash, Grep, Glob
12
12
  > that token with the absolute path before running a bundled script; never
13
13
  > search the adopter repository or rely on a `wr-architect-*` command being
14
14
  > on `PATH`. Spawn the installed `wr-architect:agent` custom agent with the
15
- > native Codex subagent tool, wait for it, and close the same agent.
15
+ > native Codex subagent tool when available. Otherwise start an independent
16
+ > general/default subagent with `fork_turns: "none"`. Prepare its message with
17
+ > `node "<architect-plugin-root>/hooks/lib/codex-completion-input.mjs" --fallback-prompt wr-architect:agent "<architect-plugin-root>/agents/agent.md" < request.txt`.
18
+ > Pass that output verbatim, preserve read-only scope, wait, and call
19
+ > `interrupt_agent` once on the completed child. For cognitive review, supply
20
+ > the complete installed `agents/cog-a11y.md`, rubric and ADR to an independent
21
+ > general reviewer if its named reviewer is unavailable. A FAIL requires
22
+ > remediation, not fallback; never write markers or claim unverified completion.
16
23
 
17
24
 
18
25
  # Capture ADR Skill
@@ -12,7 +12,14 @@ allowed-tools: Read, Write, Edit, Bash, Glob, Grep, request_user_input, Agent
12
12
  > that token with the absolute path before running a bundled script; never
13
13
  > search the adopter repository or rely on a `wr-architect-*` command being
14
14
  > on `PATH`. Spawn the installed `wr-architect:agent` custom agent with the
15
- > native Codex subagent tool, wait for it, and close the same agent.
15
+ > native Codex subagent tool when available. Otherwise start an independent
16
+ > general/default subagent with `fork_turns: "none"`. Prepare its message with
17
+ > `node "<architect-plugin-root>/hooks/lib/codex-completion-input.mjs" --fallback-prompt wr-architect:agent "<architect-plugin-root>/agents/agent.md" < request.txt`.
18
+ > Pass that output verbatim, preserve read-only scope, wait, and call
19
+ > `interrupt_agent` once on the completed child. For cognitive review, supply
20
+ > the complete installed `agents/cog-a11y.md`, rubric and ADR to an independent
21
+ > general reviewer if its named reviewer is unavailable. A FAIL requires
22
+ > remediation, not fallback; never write markers or claim unverified completion.
16
23
 
17
24
 
18
25
  # Architecture Decision Record Generator
@@ -12,7 +12,14 @@ allowed-tools: Read, Glob, Grep, Bash, Edit, request_user_input, Agent
12
12
  > that token with the absolute path before running a bundled script; never
13
13
  > search the adopter repository or rely on a `wr-architect-*` command being
14
14
  > on `PATH`. Spawn the installed `wr-architect:agent` custom agent with the
15
- > native Codex subagent tool, wait for it, and close the same agent.
15
+ > native Codex subagent tool when available. Otherwise start an independent
16
+ > general/default subagent with `fork_turns: "none"`. Prepare its message with
17
+ > `node "<architect-plugin-root>/hooks/lib/codex-completion-input.mjs" --fallback-prompt wr-architect:agent "<architect-plugin-root>/agents/agent.md" < request.txt`.
18
+ > Pass that output verbatim, preserve read-only scope, wait, and call
19
+ > `interrupt_agent` once on the completed child. For cognitive review, supply
20
+ > the complete installed `agents/cog-a11y.md`, rubric and ADR to an independent
21
+ > general reviewer if its named reviewer is unavailable. A FAIL requires
22
+ > remediation, not fallback; never write markers or claim unverified completion.
16
23
 
17
24
 
18
25
  # Review Decisions — human-oversight drain
@@ -12,7 +12,14 @@ allowed-tools: Read, Glob, Grep, Bash, request_user_input, Skill
12
12
  > that token with the absolute path before running a bundled script; never
13
13
  > search the adopter repository or rely on a `wr-architect-*` command being
14
14
  > on `PATH`. Spawn the installed `wr-architect:agent` custom agent with the
15
- > native Codex subagent tool, wait for it, and close the same agent.
15
+ > native Codex subagent tool when available. Otherwise start an independent
16
+ > general/default subagent with `fork_turns: "none"`. Prepare its message with
17
+ > `node "<architect-plugin-root>/hooks/lib/codex-completion-input.mjs" --fallback-prompt wr-architect:agent "<architect-plugin-root>/agents/agent.md" < request.txt`.
18
+ > Pass that output verbatim, preserve read-only scope, wait, and call
19
+ > `interrupt_agent` once on the completed child. For cognitive review, supply
20
+ > the complete installed `agents/cog-a11y.md`, rubric and ADR to an independent
21
+ > general reviewer if its named reviewer is unavailable. A FAIL requires
22
+ > remediation, not fallback; never write markers or claim unverified completion.
16
23
 
17
24
 
18
25
  # Architecture Compliance Review Skill
@@ -110,7 +117,7 @@ agent: wr-architect:agent
110
117
  prompt: <constructed review prompt from step 4>
111
118
  ```
112
119
 
113
- The Codex installer generates `.codex/agents/wr-architect-agent.toml` from the installed plugin's `agents/agent.md` and registers the exact identity `wr-architect:agent`. SessionStart repairs the user-scoped registration when needed. Adopter projects do not need to ship their own agent configuration. Restart Codex if that exact identity is not visible; do not substitute the built-in `default` agent because the review marker consumes the architect identity. The repo-local `.codex/agents/wr-architect.toml`, kept in sync from `<architect-plugin-root>/agents/agent.md` by `scripts/sync-codex-agents.mjs`, remains the short-name `wr-architect` alias for source-repo dogfooding.
120
+ The Codex installer generates `.codex/agents/wr-architect-agent.toml` from the installed plugin's `agents/agent.md` and registers the exact identity `wr-architect:agent`. SessionStart repairs the user-scoped registration when needed. Adopter projects do not need to ship their own agent configuration. Prefer that named reviewer. If it is unavailable, or the native tool cannot select named reviewers, use an independent general/default subagent with `fork_turns: "none"`. Prepare its message with `node "<architect-plugin-root>/hooks/lib/codex-completion-input.mjs" --fallback-prompt wr-architect:agent "<architect-plugin-root>/agents/agent.md" < request.txt`, where `request.txt` contains the original review request and `<architect-plugin-root>` is the installed plugin root. Pass the output verbatim. Preserve read-only scope, wait for completion, then call `interrupt_agent` once on that child. The normal hook verifies the installed instructions and completed review before recording a result. A substantive FAIL still requires remediation; never write a marker manually. The repo-local `.codex/agents/wr-architect.toml`, kept in sync from `<architect-plugin-root>/agents/agent.md` by `scripts/sync-codex-agents.mjs`, remains the short-name `wr-architect` alias for source-repo dogfooding.
114
121
 
115
122
  Wait for the agent to complete.
116
123