@windyroad/architect 0.23.3 → 0.23.4-preview.1265
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/hooks/architect-dispatch.sh +5 -0
- package/hooks/architect-mark-reviewed.sh +2 -9
- package/hooks/codex-agent-completion.mjs +194 -37
- package/hooks/hooks.json +3 -0
- package/hooks/lib/architect-verdict.sh +17 -0
- package/package.json +1 -1
|
@@ -57,12 +57,17 @@ if messages:
|
|
|
57
57
|
fi
|
|
58
58
|
;;
|
|
59
59
|
user-prompt)
|
|
60
|
+
printf '%s' "$INPUT" | node "$SCRIPT_DIR/codex-agent-completion.mjs"
|
|
60
61
|
detect_output="$(printf '%s' "$INPUT" | "$SCRIPT_DIR/architect-detect.sh")"
|
|
61
62
|
stale_output="$(printf '%s' "$INPUT" | "$SCRIPT_DIR/staleness-check.sh")"
|
|
62
63
|
[ -z "$detect_output" ] || printf '%s\n' "$detect_output"
|
|
63
64
|
[ -z "$stale_output" ] || printf '%s\n' "$stale_output"
|
|
64
65
|
;;
|
|
66
|
+
subagent-stop)
|
|
67
|
+
printf '%s' "$INPUT" | node "$SCRIPT_DIR/codex-agent-completion.mjs"
|
|
68
|
+
;;
|
|
65
69
|
pre-tool)
|
|
70
|
+
printf '%s' "$INPUT" | node "$SCRIPT_DIR/codex-agent-completion.mjs"
|
|
66
71
|
case "$(tool_name)" in
|
|
67
72
|
Edit|Write)
|
|
68
73
|
run_gate architect-enforce-edit.sh
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
8
8
|
source "$SCRIPT_DIR/lib/gate-helpers.sh"
|
|
9
|
+
source "$SCRIPT_DIR/lib/architect-verdict.sh"
|
|
9
10
|
|
|
10
11
|
# P191 Phase 2: anchor docs/decisions on the project root, not the hook's
|
|
11
12
|
# runtime CWD (see architect-enforce-edit.sh). A false-negative here never
|
|
@@ -36,15 +37,7 @@ case "$SUBAGENT" in
|
|
|
36
37
|
# canonical verdict on the first nonblank line is required so examples
|
|
37
38
|
# and conflicting output fail closed.
|
|
38
39
|
AGENT_OUTPUT=$(_get_tool_output)
|
|
39
|
-
|
|
40
|
-
sed -nE \
|
|
41
|
-
-e 's/^[[:space:]]*>?[[:space:]]*\*\*Architecture Review: (PASS|ISSUES FOUND|NEEDS DIRECTION)\*\*[[:space:]]*$/\1/p' \
|
|
42
|
-
-e 's/^[[:space:]]*>?[[:space:]]*##[[:space:]]+Architecture Review: (PASS|ISSUES FOUND|NEEDS DIRECTION)[[:space:]]*$/\1/p'
|
|
43
|
-
}
|
|
44
|
-
VERDICT=$(printf '%s\n' "$AGENT_OUTPUT" | _architect_verdicts)
|
|
45
|
-
FIRST_VERDICT=$(printf '%s\n' "$AGENT_OUTPUT" | awk 'NF { print; exit }' | _architect_verdicts)
|
|
46
|
-
[ "$FIRST_VERDICT" = "$VERDICT" ] || VERDICT=""
|
|
47
|
-
[ "$VERDICT" = "ISSUES FOUND" ] && VERDICT="FAIL"
|
|
40
|
+
VERDICT=$(printf '%s\n' "$AGENT_OUTPUT" | architect_verdict)
|
|
48
41
|
|
|
49
42
|
# Substance-aware drift hash + atomic verdict-write (ADR-009 amendment
|
|
50
43
|
# 2026-06-06). The marker + hash file are written as an atomic pair via
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, utimesSync, writeFileSync } from "node:fs";
|
|
4
5
|
import { dirname, join } from "node:path";
|
|
5
6
|
import { spawnSync } from "node:child_process";
|
|
6
7
|
import { fileURLToPath } from "node:url";
|
|
@@ -8,54 +9,198 @@ import { CLOSE_TOOLS, SPAWN_TOOLS, WAIT_TOOLS, response } from "./lib/codex-comp
|
|
|
8
9
|
|
|
9
10
|
const hookDir = dirname(fileURLToPath(import.meta.url));
|
|
10
11
|
const role = "wr-architect:agent";
|
|
12
|
+
const ttlText = process.env.REVIEW_TTL ?? "3600";
|
|
13
|
+
const ttl = Number(ttlText) * 1000;
|
|
11
14
|
|
|
12
|
-
function riskDir(sessionId) {
|
|
13
|
-
|
|
15
|
+
function riskDir(sessionId) { return join(process.env.TMPDIR || "/tmp", `claude-risk-${sessionId}`); }
|
|
16
|
+
function transportDir() { return join(process.env.TMPDIR || "/tmp", "codex-review-transport"); }
|
|
17
|
+
function normalizeTarget(target) {
|
|
18
|
+
if (typeof target !== "string") return "";
|
|
19
|
+
return target.startsWith("/root/") ? target.slice(6) : target;
|
|
20
|
+
}
|
|
21
|
+
function id(sessionId, target) { return createHash("sha256").update(`${sessionId}\0${role}\0${target}`).digest("hex"); }
|
|
22
|
+
function statePath(sessionId, target, suffix = "") {
|
|
23
|
+
return join(riskDir(sessionId), `codex-architect-${Buffer.from(target).toString("base64url")}${suffix}`);
|
|
14
24
|
}
|
|
25
|
+
function registrationPath(sessionId, target) { return join(transportDir(), `registration-${id(sessionId, target)}.json`); }
|
|
26
|
+
function receiptPath(sessionId, target, suffix = "") { return join(transportDir(), `receipt-${id(sessionId, target)}.json${suffix}`); }
|
|
15
27
|
|
|
16
|
-
function
|
|
17
|
-
|
|
28
|
+
function checkout(cwd) {
|
|
29
|
+
if (typeof cwd !== "string" || !cwd) return null;
|
|
30
|
+
let root;
|
|
31
|
+
try { root = realpathSync(cwd); } catch { return null; }
|
|
32
|
+
const git = spawnSync("git", ["-C", root, "rev-parse", "--show-toplevel"], { encoding: "utf8" });
|
|
33
|
+
if (git.status !== 0) return null;
|
|
34
|
+
let gitRoot;
|
|
35
|
+
try { gitRoot = realpathSync(git.stdout.trim()); } catch { return null; }
|
|
36
|
+
if (gitRoot !== root) return null;
|
|
37
|
+
const stat = statSync(root);
|
|
38
|
+
return { root, physical: `${stat.dev}:${stat.ino}` };
|
|
18
39
|
}
|
|
19
40
|
|
|
20
|
-
function
|
|
21
|
-
|
|
41
|
+
function policyHash(root) {
|
|
42
|
+
const result = spawnSync("bash", ["-c", 'source "$1"; _substance_hash_path docs/decisions', "architect-policy", join(hookDir, "lib", "gate-helpers.sh")], { cwd: root, encoding: "utf8" });
|
|
43
|
+
const hash = result.stdout?.trim();
|
|
44
|
+
return result.status === 0 && /^[a-f0-9]{64}$/.test(hash || "") ? hash : null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function parsedVerdict(output) {
|
|
48
|
+
const result = spawnSync("bash", ["-c", 'source "$1"; architect_verdict', "architect-verdict", join(hookDir, "lib", "architect-verdict.sh")], {
|
|
49
|
+
input: output,
|
|
50
|
+
encoding: "utf8",
|
|
51
|
+
});
|
|
52
|
+
return result.status === 0 ? result.stdout.trim() : "";
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function diagnostic(reason, input) {
|
|
56
|
+
const path = join(process.env.TMPDIR || "/tmp", "codex-review-completion-diagnostic.json");
|
|
57
|
+
const temporary = `${path}.${process.pid}.tmp`;
|
|
58
|
+
try {
|
|
59
|
+
writeFileSync(temporary, JSON.stringify({ timestamp: new Date().toISOString(), reason, role, event: input?.hook_event_name || input?.tool_name || "unknown" }), { mode: 0o600 });
|
|
60
|
+
renameSync(temporary, path);
|
|
61
|
+
} catch { rmSync(temporary, { force: true }); }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function reapTransport() {
|
|
65
|
+
if (!existsSync(transportDir())) return;
|
|
66
|
+
for (const name of readdirSync(transportDir())) {
|
|
67
|
+
const path = join(transportDir(), name);
|
|
68
|
+
try {
|
|
69
|
+
const statAge = Date.now() - statSync(path).mtimeMs;
|
|
70
|
+
if (/^(?:receipt|risk-receipt)-[a-f0-9]{64}\.json\.(?:claim|done)$/.test(name)) {
|
|
71
|
+
if (!Number.isFinite(statAge) || statAge < 0 || statAge >= ttl) rmSync(path, { force: true });
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (!/^(?:registration|receipt)-[a-f0-9]{64}\.json$/.test(name)) continue;
|
|
75
|
+
const value = JSON.parse(readFileSync(path, "utf8"));
|
|
76
|
+
if (value.role !== role) continue;
|
|
77
|
+
const timestamp = name.startsWith("receipt-") ? value.completedAt : value.createdAt;
|
|
78
|
+
const age = Date.now() - timestamp;
|
|
79
|
+
if (!Number.isFinite(age) || age < 0 || age >= ttl) rmSync(path, { force: true });
|
|
80
|
+
} catch { /* Another completion may own or remove this transport file. */ }
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function clear(sessionId, target) {
|
|
85
|
+
for (const suffix of ["", ".claim", ".done"]) rmSync(statePath(sessionId, target, suffix), { force: true });
|
|
86
|
+
rmSync(registrationPath(sessionId, target), { force: true });
|
|
87
|
+
for (const suffix of ["", ".claim", ".done"]) rmSync(receiptPath(sessionId, target, suffix), { force: true });
|
|
22
88
|
}
|
|
23
89
|
|
|
24
90
|
function remember(input) {
|
|
25
|
-
|
|
26
|
-
|
|
91
|
+
reapTransport();
|
|
92
|
+
const result = response(input);
|
|
93
|
+
const target = normalizeTarget(result.agent_id ?? result.task_name);
|
|
94
|
+
if (!target) return;
|
|
27
95
|
mkdirSync(riskDir(input.session_id), { recursive: true });
|
|
28
|
-
|
|
29
|
-
if (input.tool_input?.agent_type
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
96
|
+
clear(input.session_id, target);
|
|
97
|
+
if (input.tool_input?.agent_type !== role) return;
|
|
98
|
+
const bound = checkout(input.cwd || process.cwd());
|
|
99
|
+
const hash = bound && policyHash(bound.root);
|
|
100
|
+
if (!bound || !hash) return diagnostic(bound ? "policy-hash-failed" : "invalid-spawn-checkout", input);
|
|
101
|
+
const registered = { parentSession: input.session_id, role, target, ...bound, policyHash: hash, createdAt: Date.now() };
|
|
102
|
+
writeFileSync(statePath(input.session_id, target), JSON.stringify(registered), { mode: 0o600 });
|
|
103
|
+
mkdirSync(transportDir(), { recursive: true, mode: 0o700 });
|
|
104
|
+
writeFileSync(registrationPath(input.session_id, target), JSON.stringify(registered), { mode: 0o600 });
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function valid(registered, input, target, age) {
|
|
108
|
+
if (registered.role !== role || registered.target !== target) return diagnostic("registration-mismatch", input), false;
|
|
109
|
+
if (!Number.isFinite(age) || age < 0) return diagnostic("invalid-registration-age", input), false;
|
|
110
|
+
if (age >= ttl) return diagnostic("stale-registration", input), false;
|
|
111
|
+
const current = checkout(input.cwd || process.cwd());
|
|
112
|
+
if (!current || current.root !== registered.root || current.physical !== registered.physical) return diagnostic("checkout-mismatch", input), false;
|
|
113
|
+
const hash = policyHash(current.root);
|
|
114
|
+
if (!hash || hash !== registered.policyHash) return diagnostic(hash ? "policy-changed" : "policy-hash-failed", input), false;
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function writeMarker(input, registered, target, output, completedAt, claim, done) {
|
|
119
|
+
const synthetic = { ...input, session_id: registered.parentSession, cwd: registered.root, tool_name: "Agent", tool_input: { subagent_type: role }, tool_response: { content: [{ type: "text", text: output }] } };
|
|
120
|
+
const result = spawnSync(join(hookDir, "architect-mark-reviewed.sh"), { cwd: registered.root, env: process.env, input: JSON.stringify(synthetic), encoding: "utf8" });
|
|
121
|
+
if (result.status !== 0) {
|
|
122
|
+
rmSync(claim, { force: true });
|
|
123
|
+
diagnostic("marker-writer-failed", input);
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
renameSync(claim, done);
|
|
127
|
+
rmSync(statePath(registered.parentSession, target), { force: true });
|
|
128
|
+
rmSync(registrationPath(registered.parentSession, target), { force: true });
|
|
129
|
+
const assessedAt = new Date(completedAt);
|
|
130
|
+
for (const marker of [`/tmp/architect-reviewed-${registered.parentSession}`, `/tmp/architect-reviewed-${registered.parentSession}.hash`, `/tmp/architect-plan-reviewed-${registered.parentSession}`]) {
|
|
131
|
+
if (existsSync(marker)) utimesSync(marker, assessedAt, assessedAt);
|
|
132
|
+
}
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function complete(input, rawTarget, output) {
|
|
137
|
+
const target = normalizeTarget(rawTarget);
|
|
138
|
+
if (!target || typeof output !== "string" || !output) return;
|
|
139
|
+
const state = statePath(input.session_id, target);
|
|
140
|
+
if (!existsSync(state) || existsSync(`${state}.done`)) return;
|
|
141
|
+
let registered, age;
|
|
142
|
+
try { registered = JSON.parse(readFileSync(state, "utf8")); age = Date.now() - statSync(state).mtimeMs; }
|
|
143
|
+
catch { return diagnostic("malformed-registration", input); }
|
|
144
|
+
if (!valid(registered, input, target, age)) {
|
|
145
|
+
rmSync(state, { force: true });
|
|
146
|
+
rmSync(registrationPath(registered.parentSession || input.session_id, target), { force: true });
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const claim = `${state}.claim`;
|
|
150
|
+
try { writeFileSync(claim, "", { flag: "wx", mode: 0o600 }); }
|
|
151
|
+
catch (error) { if (error?.code === "EEXIST") return; throw error; }
|
|
152
|
+
writeMarker(input, registered, target, output, Date.now(), claim, `${state}.done`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function registrations(input, target) {
|
|
156
|
+
reapTransport();
|
|
157
|
+
const current = checkout(input.cwd || process.cwd());
|
|
158
|
+
if (!current || !existsSync(transportDir())) return [];
|
|
159
|
+
return readdirSync(transportDir()).filter((name) => /^registration-[a-f0-9]{64}\.json$/.test(name)).flatMap((name) => {
|
|
160
|
+
const path = join(transportDir(), name);
|
|
161
|
+
try {
|
|
162
|
+
const registered = JSON.parse(readFileSync(path, "utf8"));
|
|
163
|
+
return registered.role === role && registered.target === target && registered.root === current.root && registered.physical === current.physical ? [{ registered, age: Date.now() - statSync(path).mtimeMs }] : [];
|
|
164
|
+
} catch { return []; }
|
|
50
165
|
});
|
|
51
166
|
}
|
|
52
167
|
|
|
168
|
+
function persist(input) {
|
|
169
|
+
const target = normalizeTarget(input.task_name || input.agent_name || input.agent_id);
|
|
170
|
+
const output = input.last_assistant_message;
|
|
171
|
+
if (!target || typeof output !== "string" || !output) return;
|
|
172
|
+
if (parsedVerdict(output) !== "PASS") return diagnostic("non-pass-output", input);
|
|
173
|
+
const candidates = registrations(input, target);
|
|
174
|
+
if (candidates.length !== 1) return diagnostic(candidates.length ? "ambiguous-parent-registration" : "missing-parent-registration", input);
|
|
175
|
+
const { registered, age } = candidates[0];
|
|
176
|
+
if (!valid(registered, input, target, age)) return;
|
|
177
|
+
const path = receiptPath(registered.parentSession, target);
|
|
178
|
+
if (existsSync(path) || existsSync(`${path}.done`)) return;
|
|
179
|
+
try { writeFileSync(path, JSON.stringify({ ...registered, output, completedAt: Date.now() }), { flag: "wx", mode: 0o600 }); }
|
|
180
|
+
catch (error) { if (error?.code !== "EEXIST") diagnostic("receipt-write-failed", input); }
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function consume(input) {
|
|
184
|
+
reapTransport();
|
|
185
|
+
if (!existsSync(transportDir())) return;
|
|
186
|
+
for (const name of readdirSync(transportDir()).filter((entry) => /^receipt-[a-f0-9]{64}\.json$/.test(entry))) {
|
|
187
|
+
const path = join(transportDir(), name);
|
|
188
|
+
let pending;
|
|
189
|
+
try { pending = JSON.parse(readFileSync(path, "utf8")); } catch { diagnostic("malformed-receipt", input); continue; }
|
|
190
|
+
if (pending.parentSession !== input.session_id || pending.role !== role) continue;
|
|
191
|
+
if (!valid(pending, input, pending.target, Date.now() - pending.completedAt)) continue;
|
|
192
|
+
const claim = `${path}.claim`;
|
|
193
|
+
try { writeFileSync(claim, "", { flag: "wx", mode: 0o600 }); }
|
|
194
|
+
catch (error) { if (error?.code === "EEXIST") continue; throw error; }
|
|
195
|
+
if (writeMarker(input, pending, pending.target, pending.output, pending.completedAt, claim, `${path}.done`)) rmSync(path, { force: true });
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function close(input) { complete(input, input.tool_input?.target, response(input).previous_status?.completed); }
|
|
53
200
|
function wait(input) {
|
|
54
201
|
const statuses = response(input).status;
|
|
55
202
|
if (!statuses || typeof statuses !== "object") return;
|
|
56
|
-
for (const [target, status] of Object.entries(statuses))
|
|
57
|
-
complete({ ...input, tool_input: { target }, tool_response: { previous_status: status } });
|
|
58
|
-
}
|
|
203
|
+
for (const [target, status] of Object.entries(statuses)) complete(input, target, status?.completed);
|
|
59
204
|
}
|
|
60
205
|
|
|
61
206
|
let body = "";
|
|
@@ -64,7 +209,19 @@ for await (const chunk of process.stdin) body += chunk;
|
|
|
64
209
|
let input;
|
|
65
210
|
try { input = JSON.parse(body); } catch { process.exit(0); }
|
|
66
211
|
if (!/^[A-Za-z0-9-]+$/.test(input.session_id || "")) process.exit(0);
|
|
212
|
+
if (!/^[0-9]+$/.test(ttlText) || !Number.isSafeInteger(ttl) || ttl <= 0) process.exit(0);
|
|
67
213
|
|
|
68
|
-
|
|
69
|
-
if (
|
|
70
|
-
if (
|
|
214
|
+
try {
|
|
215
|
+
if (SPAWN_TOOLS.has(input.tool_name)) remember(input);
|
|
216
|
+
if (CLOSE_TOOLS.has(input.tool_name)) close(input);
|
|
217
|
+
if (WAIT_TOOLS.has(input.tool_name)) wait(input);
|
|
218
|
+
if (input.hook_event_name === "UserPromptSubmit" || input.hook_event_name === "PreToolUse" || ["Bash", "Edit", "Write", "ExitPlanMode"].includes(input.tool_name)) consume(input);
|
|
219
|
+
if (input.hook_event_name === "SubagentStop") {
|
|
220
|
+
if (input.agent_type !== role) diagnostic("unrelated-subagent-stop", input);
|
|
221
|
+
else if (existsSync(statePath(input.session_id, normalizeTarget(input.agent_id)))) complete(input, input.agent_id, input.last_assistant_message);
|
|
222
|
+
else persist(input);
|
|
223
|
+
}
|
|
224
|
+
} catch {
|
|
225
|
+
diagnostic("transport-error", input);
|
|
226
|
+
process.exitCode = 0;
|
|
227
|
+
}
|
package/hooks/hooks.json
CHANGED
|
@@ -13,6 +13,9 @@
|
|
|
13
13
|
],
|
|
14
14
|
"PostToolUse": [
|
|
15
15
|
{ "matcher": "Agent|Bash|Edit|Skill|Write|collaboration.spawn_agent|collaboration.wait_agent|collaboration.interrupt_agent|collaborationspawn_agent|collaborationwait_agent|collaborationinterrupt_agent|spawn_agent|wait_agent|interrupt_agent|close_agent|multi_agent_v1__spawn_agent|multi_agent_v1__wait_agent|multi_agent_v1__close_agent", "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/architect-dispatch.sh post-tool" }] }
|
|
16
|
+
],
|
|
17
|
+
"SubagentStop": [
|
|
18
|
+
{ "matcher": "^wr-architect:agent$", "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/architect-dispatch.sh subagent-stop" }] }
|
|
16
19
|
]
|
|
17
20
|
}
|
|
18
21
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
# Print the one authoritative architect verdict. Exactly one canonical verdict
|
|
4
|
+
# must be the first nonblank line; examples and conflicting headings fail closed.
|
|
5
|
+
architect_verdict() {
|
|
6
|
+
local output verdict first
|
|
7
|
+
output="$(cat)"
|
|
8
|
+
verdict="$(printf '%s\n' "$output" | sed -nE \
|
|
9
|
+
-e 's/^[[:space:]]*>?[[:space:]]*\*\*Architecture Review: (PASS|ISSUES FOUND|NEEDS DIRECTION)\*\*[[:space:]]*$/\1/p' \
|
|
10
|
+
-e 's/^[[:space:]]*>?[[:space:]]*##[[:space:]]+Architecture Review: (PASS|ISSUES FOUND|NEEDS DIRECTION)[[:space:]]*$/\1/p')"
|
|
11
|
+
first="$(printf '%s\n' "$output" | awk 'NF { print; exit }' | sed -nE \
|
|
12
|
+
-e 's/^[[:space:]]*>?[[:space:]]*\*\*Architecture Review: (PASS|ISSUES FOUND|NEEDS DIRECTION)\*\*[[:space:]]*$/\1/p' \
|
|
13
|
+
-e 's/^[[:space:]]*>?[[:space:]]*##[[:space:]]+Architecture Review: (PASS|ISSUES FOUND|NEEDS DIRECTION)[[:space:]]*$/\1/p')"
|
|
14
|
+
[ "$first" = "$verdict" ] || verdict=""
|
|
15
|
+
[ "$verdict" != "ISSUES FOUND" ] || verdict="FAIL"
|
|
16
|
+
printf '%s' "$verdict"
|
|
17
|
+
}
|