@rubytech/create-maxy-code 0.1.223 → 0.1.225
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/dist/__tests__/installer-settings-permissions.test.js +112 -0
- package/dist/index.js +13 -0
- package/dist/permissions-seed.js +76 -0
- package/package.json +1 -1
- package/payload/platform/plugins/admin/mcp/dist/index.js +1 -1
- package/payload/platform/plugins/admin/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/admin/skills/file-presentation/SKILL.md +9 -7
- package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +18 -4
- package/payload/platform/plugins/admin/skills/professional-document/SKILL.md +1 -1
- package/payload/platform/plugins/docs/references/admin-ui.md +11 -1
- package/payload/platform/plugins/docs/references/deployment.md +1 -1
- package/payload/platform/plugins/docs/references/platform.md +4 -0
- package/payload/platform/plugins/docs/references/troubleshooting.md +1 -1
- package/payload/platform/plugins/graph-viewer/mcp/dist/index.js +1 -1
- package/payload/platform/plugins/graph-viewer/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/graph-viewer/mcp/dist/tools/graph-render.d.ts.map +1 -1
- package/payload/platform/plugins/graph-viewer/mcp/dist/tools/graph-render.js +7 -5
- package/payload/platform/plugins/graph-viewer/mcp/dist/tools/graph-render.js.map +1 -1
- package/payload/platform/plugins/graph-viewer/skills/render-graph/SKILL.md +6 -6
- package/payload/platform/plugins/scheduling/PLUGIN.md +2 -2
- package/payload/platform/plugins/scheduling/mcp/dist/index.js +2 -2
- package/payload/platform/plugins/scheduling/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-export-ics.d.ts.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-export-ics.js +3 -2
- package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-export-ics.js.map +1 -1
- package/payload/platform/scripts/check-plugin-references.mjs +256 -0
- package/payload/platform/scripts/logs-read-jsonl.test.sh +307 -0
- package/payload/platform/scripts/logs-read.sh +435 -32
- package/payload/platform/scripts/setup-account.sh +20 -8
- package/payload/platform/services/claude-session-manager/dist/system-prompt.d.ts +0 -1
- package/payload/platform/services/claude-session-manager/dist/system-prompt.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/system-prompt.js +11 -10
- package/payload/platform/services/claude-session-manager/dist/system-prompt.js.map +1 -1
- package/payload/platform/templates/specialists/agents/content-producer.md +1 -1
- package/payload/platform/templates/specialists/agents/personal-assistant.md +1 -1
- package/payload/premium-plugins/teaching/PLUGIN.md +7 -7
- package/payload/premium-plugins/venture-studio/skills/brand-pack/SKILL.md +1 -1
- package/payload/server/public/assets/data-BzRzOKWv.js +1 -0
- package/payload/server/public/data.html +1 -1
- package/payload/server/server.js +13 -6
- package/payload/server/public/assets/data-CHdaTjgn.js +0 -1
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Task 581 — bundle-time existence check for `references/<file>.md` paths
|
|
3
|
+
// cited in PLUGIN.md, SKILL.md, and specialist agent template bodies.
|
|
4
|
+
//
|
|
5
|
+
// Why: the `plugin-read` tool resolves `pluginName + file` against
|
|
6
|
+
// `<platform>/plugins/<pluginName>/<file>`. When a doc body cites
|
|
7
|
+
// `references/foo.md` (or `plugins/<other>/references/foo.md`) and that file
|
|
8
|
+
// does not exist, the agent burns a tool call only to receive
|
|
9
|
+
// "Plugin file not found". This module fails the bundle before such a payload
|
|
10
|
+
// is assembled.
|
|
11
|
+
//
|
|
12
|
+
// Two citation shapes are extracted:
|
|
13
|
+
// 1. Bare `references/<file>.md` — resolves under the citing file's owning
|
|
14
|
+
// plugin directory (PLUGIN.md and skills/<slug>/SKILL.md only; specialist
|
|
15
|
+
// templates have no plugin owner, so the bare shape there is itself a
|
|
16
|
+
// violation).
|
|
17
|
+
// 2. `plugins/<name>/references/<file>.md` (optionally prefixed by
|
|
18
|
+
// `platform/`) — resolves at `<platformRoot>/plugins/<name>/references/
|
|
19
|
+
// <file>.md`.
|
|
20
|
+
|
|
21
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
22
|
+
import { resolve, relative, join, dirname } from "node:path";
|
|
23
|
+
import { pathToFileURL } from "node:url";
|
|
24
|
+
|
|
25
|
+
// Citation shapes captured (all must end in `references/<file>.md`):
|
|
26
|
+
// references/<file>.md
|
|
27
|
+
// plugins/<name>/references/<file>.md
|
|
28
|
+
// plugins/<name>/skills/<slug>/references/<file>.md
|
|
29
|
+
// skills/<slug>/references/<file>.md
|
|
30
|
+
// platform/<any of the above starting with plugins/>
|
|
31
|
+
//
|
|
32
|
+
// The lookbehind excludes [a-z0-9_/-] so the capture cannot start mid-path
|
|
33
|
+
// (`foo/references/bar.md` does not match). `/` is in the excluded set —
|
|
34
|
+
// that prevents a mid-path `references/...` from being captured without its
|
|
35
|
+
// leading `skills/<slug>/` or `plugins/<name>/` prefix. Every legitimate
|
|
36
|
+
// citation root we want to recognise is named explicitly in the alternation.
|
|
37
|
+
const REF_PATTERN =
|
|
38
|
+
/(?:^|[^a-z0-9_/-])((?:platform\/)?(?:plugins\/[a-z0-9][a-z0-9-]*\/)?(?:skills\/[a-z0-9][a-z0-9-]*\/)?references\/[a-z0-9][a-z0-9-]*\.md)/g;
|
|
39
|
+
|
|
40
|
+
// Walk `dir` recursively, collecting every file whose basename matches `predicate`.
|
|
41
|
+
function walk(dir, predicate, out = []) {
|
|
42
|
+
if (!existsSync(dir)) return out;
|
|
43
|
+
let entries;
|
|
44
|
+
try {
|
|
45
|
+
entries = readdirSync(dir);
|
|
46
|
+
} catch {
|
|
47
|
+
return out;
|
|
48
|
+
}
|
|
49
|
+
for (const entry of entries) {
|
|
50
|
+
const full = join(dir, entry);
|
|
51
|
+
let st;
|
|
52
|
+
try {
|
|
53
|
+
st = statSync(full);
|
|
54
|
+
} catch {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (st.isDirectory()) {
|
|
58
|
+
walk(full, predicate, out);
|
|
59
|
+
} else if (predicate(entry, full)) {
|
|
60
|
+
out.push(full);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Plugin root for a citing file, or null if there is none (specialist
|
|
67
|
+
// templates). For SKILL.md, walks dirname up past `skills/<slug>/` to the
|
|
68
|
+
// ancestor that contains the `skills` segment as a direct child.
|
|
69
|
+
function owningPluginRoot(filePath) {
|
|
70
|
+
if (filePath.endsWith("/PLUGIN.md")) {
|
|
71
|
+
return dirname(filePath);
|
|
72
|
+
}
|
|
73
|
+
if (filePath.endsWith("/SKILL.md")) {
|
|
74
|
+
let cursor = dirname(filePath);
|
|
75
|
+
while (cursor && cursor !== "/" && cursor !== ".") {
|
|
76
|
+
const parts = cursor.split("/");
|
|
77
|
+
if (parts[parts.length - 1] === "skills") {
|
|
78
|
+
return dirname(cursor);
|
|
79
|
+
}
|
|
80
|
+
cursor = dirname(cursor);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Directories to resolve a bare `references/<file>.md` citation against.
|
|
87
|
+
// Returns an array of candidate directories; the citation passes if the file
|
|
88
|
+
// exists under ANY of them. Both candidate paths for SKILL.md are reachable
|
|
89
|
+
// via `plugin-read` — the agent may construct `file=references/foo.md`
|
|
90
|
+
// (plugin-root form) or `file=skills/<slug>/references/foo.md` (skill-local
|
|
91
|
+
// form), depending on how the plugin organises its references.
|
|
92
|
+
//
|
|
93
|
+
// PLUGIN.md — plugin-root only. → [<plugin-root>]
|
|
94
|
+
// SKILL.md — skill-local OR plugin-root.
|
|
95
|
+
// → [<skill-dir>, <plugin-root>]
|
|
96
|
+
// specialist template — no owning context. → []
|
|
97
|
+
function bareReferenceContextDirs(filePath) {
|
|
98
|
+
if (filePath.endsWith("/PLUGIN.md")) {
|
|
99
|
+
return [dirname(filePath)];
|
|
100
|
+
}
|
|
101
|
+
if (filePath.endsWith("/SKILL.md")) {
|
|
102
|
+
const skillDir = dirname(filePath);
|
|
103
|
+
const pluginRoot = owningPluginRoot(filePath);
|
|
104
|
+
return pluginRoot ? [skillDir, pluginRoot] : [skillDir];
|
|
105
|
+
}
|
|
106
|
+
return [];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Return every candidate absolute path a citation could resolve to. The
|
|
110
|
+
// citation passes the existence check if ANY candidate exists. Returns an
|
|
111
|
+
// empty array if the citation has no resolvable shape (bare reference cited
|
|
112
|
+
// from a non-plugin file).
|
|
113
|
+
function candidateTargets({ raw, sourcePath, platformRoot }) {
|
|
114
|
+
// Strip an optional leading `platform/` so `platform/plugins/<name>/...`
|
|
115
|
+
// and `plugins/<name>/...` resolve identically.
|
|
116
|
+
const normalised = raw.replace(/^platform\//, "");
|
|
117
|
+
|
|
118
|
+
// Cross-plugin (and cross-plugin-sub-skill) references resolve at the
|
|
119
|
+
// platform root regardless of the citing file's owning plugin.
|
|
120
|
+
if (normalised.startsWith("plugins/")) {
|
|
121
|
+
return [resolve(platformRoot, normalised)];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// `skills/<slug>/references/<file>.md` resolves under the citing file's
|
|
125
|
+
// owning plugin directory. Used by PLUGIN.md bodies that index references
|
|
126
|
+
// owned by sub-skills (e.g. premium-plugins/teaching/PLUGIN.md).
|
|
127
|
+
if (normalised.startsWith("skills/")) {
|
|
128
|
+
const pluginRoot = owningPluginRoot(sourcePath);
|
|
129
|
+
return pluginRoot ? [resolve(pluginRoot, normalised)] : [];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Bare `references/<file>.md`.
|
|
133
|
+
const ctxDirs = bareReferenceContextDirs(sourcePath);
|
|
134
|
+
return ctxDirs.map(d => resolve(d, normalised));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function scanFile(filePath, { platformRoot, repoRoot }) {
|
|
138
|
+
let body;
|
|
139
|
+
try {
|
|
140
|
+
body = readFileSync(filePath, "utf-8");
|
|
141
|
+
} catch {
|
|
142
|
+
return { violations: [], citations: 0 };
|
|
143
|
+
}
|
|
144
|
+
const violations = [];
|
|
145
|
+
let citations = 0;
|
|
146
|
+
const lines = body.split("\n");
|
|
147
|
+
for (let i = 0; i < lines.length; i++) {
|
|
148
|
+
const line = lines[i];
|
|
149
|
+
REF_PATTERN.lastIndex = 0;
|
|
150
|
+
let m;
|
|
151
|
+
while ((m = REF_PATTERN.exec(line)) !== null) {
|
|
152
|
+
const raw = m[1];
|
|
153
|
+
// 1-based column, pointing at the start of the captured citation.
|
|
154
|
+
const column = (m.index + m[0].indexOf(raw)) + 1;
|
|
155
|
+
citations++;
|
|
156
|
+
const candidates = candidateTargets({ raw, sourcePath: filePath, platformRoot });
|
|
157
|
+
if (candidates.length === 0) {
|
|
158
|
+
violations.push({
|
|
159
|
+
source: relative(repoRoot, filePath),
|
|
160
|
+
line: i + 1,
|
|
161
|
+
column,
|
|
162
|
+
target: "<no plugin owner — bare references/ cited from non-plugin file>",
|
|
163
|
+
raw,
|
|
164
|
+
});
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
if (!candidates.some(p => existsSync(p))) {
|
|
168
|
+
// Report the most informative candidate — for SKILL.md citations the
|
|
169
|
+
// first candidate is the skill-local path; if both exist as candidates,
|
|
170
|
+
// listing both makes the fix unambiguous.
|
|
171
|
+
const targetMsg = candidates.length === 1
|
|
172
|
+
? candidates[0]
|
|
173
|
+
: `none of [${candidates.join(", ")}]`;
|
|
174
|
+
violations.push({
|
|
175
|
+
source: relative(repoRoot, filePath),
|
|
176
|
+
line: i + 1,
|
|
177
|
+
column,
|
|
178
|
+
target: targetMsg,
|
|
179
|
+
raw,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return { violations, citations };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function runPluginReferenceCheck({ platformRoot, premiumRoot }) {
|
|
188
|
+
const repoRoot = resolve(platformRoot, "..");
|
|
189
|
+
const sources = [];
|
|
190
|
+
|
|
191
|
+
// platform: every PLUGIN.md and every SKILL.md under plugins/
|
|
192
|
+
const platformPlugins = resolve(platformRoot, "plugins");
|
|
193
|
+
walk(
|
|
194
|
+
platformPlugins,
|
|
195
|
+
(name) => name === "PLUGIN.md" || name === "SKILL.md",
|
|
196
|
+
sources,
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
// platform: every specialist template
|
|
200
|
+
const specialistDir = resolve(platformRoot, "templates", "specialists", "agents");
|
|
201
|
+
walk(specialistDir, (name) => name.endsWith(".md"), sources);
|
|
202
|
+
|
|
203
|
+
// premium: every PLUGIN.md and SKILL.md under any bundle (standalone or sub)
|
|
204
|
+
if (premiumRoot && existsSync(premiumRoot)) {
|
|
205
|
+
walk(
|
|
206
|
+
premiumRoot,
|
|
207
|
+
(name) => name === "PLUGIN.md" || name === "SKILL.md",
|
|
208
|
+
sources,
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const violations = [];
|
|
213
|
+
let citations = 0;
|
|
214
|
+
for (const src of sources) {
|
|
215
|
+
const r = scanFile(src, { platformRoot, repoRoot });
|
|
216
|
+
violations.push(...r.violations);
|
|
217
|
+
citations += r.citations;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return {
|
|
221
|
+
ok: violations.length === 0,
|
|
222
|
+
violations,
|
|
223
|
+
scanned: sources.length,
|
|
224
|
+
citations,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// CLI entry — `node check-plugin-references.mjs [platformRoot] [premiumRoot]`.
|
|
229
|
+
// Use `pathToFileURL` so the self-detect survives paths with spaces or non-
|
|
230
|
+
// ASCII characters; the string-concat form fails because `process.argv[1]` is
|
|
231
|
+
// raw while `import.meta.url` is percent-encoded. When this module is loaded
|
|
232
|
+
// via `import` (e.g. from `bundle.js`), `process.argv[1]` may still be set
|
|
233
|
+
// to the importing script, in which case the URLs differ and the CLI block
|
|
234
|
+
// is skipped.
|
|
235
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
236
|
+
const defaultPlatform = resolve(import.meta.dirname, "..");
|
|
237
|
+
const defaultPremium = resolve(import.meta.dirname, "..", "..", "premium-plugins");
|
|
238
|
+
const platformRoot = process.argv[2] ? resolve(process.argv[2]) : defaultPlatform;
|
|
239
|
+
const premiumRoot = process.argv[3] ? resolve(process.argv[3]) : defaultPremium;
|
|
240
|
+
const r = runPluginReferenceCheck({ platformRoot, premiumRoot });
|
|
241
|
+
if (!r.ok) {
|
|
242
|
+
for (const v of r.violations) {
|
|
243
|
+
console.error(`[bundle-validator] reference not found`);
|
|
244
|
+
console.error(` source: ${v.source}:${v.line}:${v.column}`);
|
|
245
|
+
console.error(` cites: ${v.raw}`);
|
|
246
|
+
console.error(` target: ${v.target}`);
|
|
247
|
+
}
|
|
248
|
+
console.error(
|
|
249
|
+
`FATAL: ${r.violations.length} broken reference citation(s) across ${r.scanned} scanned file(s) (${r.citations} citations).`,
|
|
250
|
+
);
|
|
251
|
+
process.exit(1);
|
|
252
|
+
}
|
|
253
|
+
console.log(
|
|
254
|
+
`[bundle-validator] plugin references ok scanned=${r.scanned} citations=${r.citations}`,
|
|
255
|
+
);
|
|
256
|
+
}
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# bash harness for logs-read.sh rc-spawn JSONL retrieval (Task 585).
|
|
3
|
+
#
|
|
4
|
+
# Covers the three in-scope deliverables:
|
|
5
|
+
# 1. `logs-read.sh <key>` (bare, no type) resolves the configDir Claude tree
|
|
6
|
+
# and emits a merged session+subagent timeline. Key forms tested:
|
|
7
|
+
# session_<bridge>, bare <bridge>, full <uuid>, <uuid> prefix.
|
|
8
|
+
# 2. A subagent `is_error:true` tool_result is flagged inline in the timeline
|
|
9
|
+
# (agentType + failing tool + error text), and absent when the session is
|
|
10
|
+
# clean.
|
|
11
|
+
# 3. `logs-read.sh --scan-subagent-errors` lists every subagents/agent-*.jsonl
|
|
12
|
+
# carrying an is_error result, and only those.
|
|
13
|
+
#
|
|
14
|
+
# The fake configDir is pointed at via CLAUDE_CONFIG_DIR (the same override the
|
|
15
|
+
# session manager honours), so no real ~/.<brand> tree is touched. Each case
|
|
16
|
+
# builds an rc-spawn-shaped install (data/accounts/<id>/ with NO logs/ subdir)
|
|
17
|
+
# to prove the JSONL path does not depend on the dead per-account logs tree.
|
|
18
|
+
set -euo pipefail
|
|
19
|
+
|
|
20
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
21
|
+
LOGS_READ="$SCRIPT_DIR/logs-read.sh"
|
|
22
|
+
if [[ ! -x "$LOGS_READ" ]]; then
|
|
23
|
+
echo "FATAL: $LOGS_READ not executable" >&2
|
|
24
|
+
exit 1
|
|
25
|
+
fi
|
|
26
|
+
|
|
27
|
+
UUID_A="6031941e-0000-4000-8000-000000000001"
|
|
28
|
+
UUID_C="c0c0c0c0-0000-4000-8000-0000000000c0"
|
|
29
|
+
UUID_CLEAN="c1ea0000-0000-4000-8000-00000000c1ea"
|
|
30
|
+
BRIDGE="TESTBRIDGE01"
|
|
31
|
+
|
|
32
|
+
PASS=0
|
|
33
|
+
FAIL=0
|
|
34
|
+
|
|
35
|
+
# Build an rc-spawn-shaped install root + a fake CLAUDE tree. Echos two paths
|
|
36
|
+
# on stdout: "<logs_read_script> <claude_config_dir>".
|
|
37
|
+
setup_tree() {
|
|
38
|
+
local root="$1"
|
|
39
|
+
mkdir -p "$root/platform/scripts"
|
|
40
|
+
mkdir -p "$root/data/accounts/acct-585" # account dir exists, but NO logs/ — rc-spawn shape
|
|
41
|
+
ln -sf "$LOGS_READ" "$root/platform/scripts/logs-read.sh"
|
|
42
|
+
mkdir -p "$HOME/.$(basename "$root")/logs" # server.log home for best-effort missing-on-resolve
|
|
43
|
+
local cfg="$root/cfg/.claude"
|
|
44
|
+
mkdir -p "$cfg/sessions" "$cfg/projects/-fake-cwd"
|
|
45
|
+
echo "$root/platform/scripts/logs-read.sh $cfg"
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
cleanup_tree() {
|
|
49
|
+
local root="$1"
|
|
50
|
+
rm -rf "$root"
|
|
51
|
+
rm -rf "$HOME/.$(basename "$root")"
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
# Write a parent JSONL with a couple of turns.
|
|
55
|
+
write_parent() {
|
|
56
|
+
local cfg="$1" uuid="$2"
|
|
57
|
+
local f="$cfg/projects/-fake-cwd/${uuid}.jsonl"
|
|
58
|
+
{
|
|
59
|
+
printf '%s\n' '{"type":"user","timestamp":"2026-05-31T19:40:00.000Z","uuid":"p1","message":{"role":"user","content":"make me a pdf"}}'
|
|
60
|
+
printf '%s\n' '{"type":"assistant","timestamp":"2026-05-31T19:40:02.000Z","uuid":"p2","message":{"role":"assistant","model":"claude-opus-4-8","content":[{"type":"text","text":"dispatching content-producer"}]}}'
|
|
61
|
+
} > "$f"
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
# Write a FAILING subagent transcript (is_error tool_result) + its meta.
|
|
65
|
+
write_subagent_failed() {
|
|
66
|
+
local cfg="$1" parent_uuid="$2" agent="$3" atype="$4"
|
|
67
|
+
local d="$cfg/projects/-fake-cwd/${parent_uuid}/subagents"
|
|
68
|
+
mkdir -p "$d"
|
|
69
|
+
{
|
|
70
|
+
printf '%s\n' '{"type":"user","timestamp":"2026-05-31T19:40:10.000Z","uuid":"s1","message":{"role":"user","content":"produce the pdf and deliver it"}}'
|
|
71
|
+
printf '%s\n' '{"type":"assistant","timestamp":"2026-05-31T19:40:20.000Z","uuid":"s2","message":{"role":"assistant","content":[{"type":"tool_use","id":"toolu_DEAD","name":"Bash","input":{"command":"SendUserFile /tmp/out.pdf"}}]}}'
|
|
72
|
+
printf '%s\n' '{"type":"user","timestamp":"2026-05-31T19:40:21.000Z","uuid":"s3","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_DEAD","is_error":true,"content":"/bin/bash: line 1: SendUserFile: command not found"}]},"toolUseResult":"Error: Exit code 127"}'
|
|
73
|
+
} > "$d/${agent}.jsonl"
|
|
74
|
+
printf '{"agentType":"%s","description":"render pdf"}\n' "$atype" > "$d/${agent}.meta.json"
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
# Write a parent JSONL that contains its OWN failing tool_result (NOT a
|
|
78
|
+
# subagent error — a parent Bash that exited non-zero). Used to prove the
|
|
79
|
+
# parent's own errors are not mislabelled as subagent errors.
|
|
80
|
+
write_parent_with_error() {
|
|
81
|
+
local cfg="$1" uuid="$2"
|
|
82
|
+
local f="$cfg/projects/-fake-cwd/${uuid}.jsonl"
|
|
83
|
+
{
|
|
84
|
+
printf '%s\n' '{"type":"user","timestamp":"2026-05-31T19:39:00.000Z","uuid":"e1","message":{"role":"user","content":"check the repo"}}'
|
|
85
|
+
printf '%s\n' '{"type":"assistant","timestamp":"2026-05-31T19:39:02.000Z","uuid":"e2","message":{"role":"assistant","content":[{"type":"tool_use","id":"toolu_PARENT","name":"Bash","input":{"command":"grep nonesuch ."}}]}}'
|
|
86
|
+
printf '%s\n' '{"type":"user","timestamp":"2026-05-31T19:39:03.000Z","uuid":"e3","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_PARENT","is_error":true,"content":"Exit code 1 PARENT_OWN_GREP_MISS"}]},"toolUseResult":"Error: Exit code 1"}'
|
|
87
|
+
} > "$f"
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
# Write a CLEAN subagent transcript (no errors) + its meta.
|
|
91
|
+
write_subagent_clean() {
|
|
92
|
+
local cfg="$1" parent_uuid="$2" agent="$3" atype="$4"
|
|
93
|
+
local d="$cfg/projects/-fake-cwd/${parent_uuid}/subagents"
|
|
94
|
+
mkdir -p "$d"
|
|
95
|
+
{
|
|
96
|
+
printf '%s\n' '{"type":"user","timestamp":"2026-05-31T19:41:10.000Z","uuid":"q1","message":{"role":"user","content":"review the diff"}}'
|
|
97
|
+
printf '%s\n' '{"type":"assistant","timestamp":"2026-05-31T19:41:12.000Z","uuid":"q2","message":{"role":"assistant","content":[{"type":"tool_use","id":"toolu_OK","name":"Read","input":{"file_path":"/x"}}]}}'
|
|
98
|
+
printf '%s\n' '{"type":"user","timestamp":"2026-05-31T19:41:13.000Z","uuid":"q3","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_OK","content":"ok"}]}}'
|
|
99
|
+
} > "$d/${agent}.jsonl"
|
|
100
|
+
printf '{"agentType":"%s","description":"review"}\n' "$atype" > "$d/${agent}.meta.json"
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
write_pidfile() {
|
|
104
|
+
local cfg="$1" pid="$2" uuid="$3" bridge="$4"
|
|
105
|
+
printf '{"pid":%s,"sessionId":"%s","bridgeSessionId":"session_%s","cwd":"/fake/cwd","startedAt":1780000000000}\n' \
|
|
106
|
+
"$pid" "$uuid" "$bridge" > "$cfg/sessions/${pid}.json"
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
# meta sidecar carrying bridgeIds (persistent bridge->uuid map, survives PID-file deletion).
|
|
110
|
+
write_meta_bridgeids() {
|
|
111
|
+
local cfg="$1" uuid="$2" bridge="$3"
|
|
112
|
+
printf '{"senderId":"s1","role":"admin","channel":"browser","url":null,"startedAt":"2026-05-31T19:40:00.000Z","permissionMode":null,"effectivePermissionMode":null,"model":null,"hidden":false,"specialist":null,"scopeUnitToken":null,"bridgeIds":["%s"]}\n' \
|
|
113
|
+
"$bridge" > "$cfg/projects/-fake-cwd/${uuid}.meta.json"
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
# Parent transcript whose BODY embeds a bridge key string (no pidfile, no
|
|
117
|
+
# bridgeIds) — exercises the content-grep last-resort resolver tier.
|
|
118
|
+
write_parent_with_key_in_body() {
|
|
119
|
+
local cfg="$1" uuid="$2" keystr="$3"
|
|
120
|
+
printf '%s\n' "{\"type\":\"user\",\"timestamp\":\"2026-05-31T19:38:00.000Z\",\"uuid\":\"b1\",\"message\":{\"role\":\"user\",\"content\":\"resumed from ${keystr} earlier\"}}" \
|
|
121
|
+
> "$cfg/projects/-fake-cwd/${uuid}.jsonl"
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
run_case() {
|
|
125
|
+
local name="$1"; shift
|
|
126
|
+
echo ""
|
|
127
|
+
echo "=== CASE: $name ==="
|
|
128
|
+
if "$@"; then
|
|
129
|
+
PASS=$((PASS + 1)); echo "PASS: $name"
|
|
130
|
+
else
|
|
131
|
+
FAIL=$((FAIL + 1)); echo "FAIL: $name"
|
|
132
|
+
fi
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
# Capture stdout, stderr, rc of a logs-read.sh invocation under a fake config dir.
|
|
136
|
+
# Sets globals: OUT ERR RC.
|
|
137
|
+
invoke() {
|
|
138
|
+
local script="$1" cfg="$2"; shift 2
|
|
139
|
+
local errf; errf=$(mktemp)
|
|
140
|
+
RC=0
|
|
141
|
+
OUT=$(CLAUDE_CONFIG_DIR="$cfg" "$script" "$@" 2>"$errf") || RC=$?
|
|
142
|
+
ERR=$(cat "$errf"); rm -f "$errf"
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
# --- Case A: bare key via PID-file bridge → timeline + flagged subagent error ---
|
|
146
|
+
case_bridge_timeline_flagged() {
|
|
147
|
+
local root="/tmp/maxy-585-A-$$"; local out
|
|
148
|
+
out=$(setup_tree "$root"); local script="${out% *}"; local cfg="${out#* }"
|
|
149
|
+
write_parent "$cfg" "$UUID_A"
|
|
150
|
+
write_subagent_failed "$cfg" "$UUID_A" "agent-aDEAD0000" "content-producer"
|
|
151
|
+
write_pidfile "$cfg" 4242 "$UUID_A" "$BRIDGE"
|
|
152
|
+
|
|
153
|
+
invoke "$script" "$cfg" "session_${BRIDGE}"
|
|
154
|
+
cleanup_tree "$root"
|
|
155
|
+
|
|
156
|
+
[[ $RC -eq 0 ]] || { echo " expected exit 0, got $RC; err=$ERR"; return 1; }
|
|
157
|
+
[[ "$OUT" == *"content-producer"* ]] || { echo " missing agentType content-producer"; return 1; }
|
|
158
|
+
[[ "$OUT" == *"SendUserFile"* ]] || { echo " missing failing command SendUserFile"; return 1; }
|
|
159
|
+
[[ "$OUT" == *"command not found"* ]] || { echo " missing error text"; return 1; }
|
|
160
|
+
[[ "$OUT" == *"SUBAGENT ERROR"* ]] || { echo " missing SUBAGENT ERROR flag"; return 1; }
|
|
161
|
+
[[ "$OUT" == *"6031941e"* ]] || { echo " missing resolved session uuid"; return 1; }
|
|
162
|
+
return 0
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
# --- Case B: clean session → timeline, NO error flag ---
|
|
166
|
+
case_clean_no_flag() {
|
|
167
|
+
local root="/tmp/maxy-585-B-$$"; local out
|
|
168
|
+
out=$(setup_tree "$root"); local script="${out% *}"; local cfg="${out#* }"
|
|
169
|
+
write_parent "$cfg" "$UUID_A"
|
|
170
|
+
write_subagent_clean "$cfg" "$UUID_A" "agent-aCLEAN000" "code-reviewer"
|
|
171
|
+
write_pidfile "$cfg" 4243 "$UUID_A" "$BRIDGE"
|
|
172
|
+
|
|
173
|
+
invoke "$script" "$cfg" "session_${BRIDGE}"
|
|
174
|
+
cleanup_tree "$root"
|
|
175
|
+
|
|
176
|
+
[[ $RC -eq 0 ]] || { echo " expected exit 0, got $RC; err=$ERR"; return 1; }
|
|
177
|
+
[[ "$OUT" == *"code-reviewer"* ]] || { echo " missing clean subagent in timeline"; return 1; }
|
|
178
|
+
[[ "$OUT" != *"SUBAGENT ERROR"* ]] || { echo " unexpected SUBAGENT ERROR flag on clean session"; return 1; }
|
|
179
|
+
return 0
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
# --- Case C: resolve via bridgeIds sidecar when PID file is absent ---
|
|
183
|
+
case_resolve_via_bridgeids() {
|
|
184
|
+
local root="/tmp/maxy-585-C-$$"; local out
|
|
185
|
+
out=$(setup_tree "$root"); local script="${out% *}"; local cfg="${out#* }"
|
|
186
|
+
write_parent "$cfg" "$UUID_C"
|
|
187
|
+
write_subagent_failed "$cfg" "$UUID_C" "agent-aBEEF0000" "content-producer"
|
|
188
|
+
write_meta_bridgeids "$cfg" "$UUID_C" "ENDEDBRIDGE9" # no pidfile — session ended
|
|
189
|
+
|
|
190
|
+
invoke "$script" "$cfg" "session_ENDEDBRIDGE9"
|
|
191
|
+
cleanup_tree "$root"
|
|
192
|
+
|
|
193
|
+
[[ $RC -eq 0 ]] || { echo " expected exit 0, got $RC; err=$ERR"; return 1; }
|
|
194
|
+
[[ "$OUT" == *"c0c0c0c0"* ]] || { echo " did not resolve UUID_C via bridgeIds"; return 1; }
|
|
195
|
+
return 0
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
# --- Case D: resolve via UUID directly (full) ---
|
|
199
|
+
case_resolve_uuid_direct() {
|
|
200
|
+
local root="/tmp/maxy-585-D-$$"; local out
|
|
201
|
+
out=$(setup_tree "$root"); local script="${out% *}"; local cfg="${out#* }"
|
|
202
|
+
write_parent "$cfg" "$UUID_A"
|
|
203
|
+
write_subagent_clean "$cfg" "$UUID_A" "agent-aOK000000" "code-reviewer"
|
|
204
|
+
|
|
205
|
+
invoke "$script" "$cfg" "$UUID_A"
|
|
206
|
+
cleanup_tree "$root"
|
|
207
|
+
|
|
208
|
+
[[ $RC -eq 0 ]] || { echo " expected exit 0, got $RC; err=$ERR"; return 1; }
|
|
209
|
+
[[ "$OUT" == *"6031941e"* ]] || { echo " did not return timeline for UUID_A"; return 1; }
|
|
210
|
+
return 0
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
# --- Case E: --scan-subagent-errors lists failed subagent only ---
|
|
214
|
+
case_scan_lists_failed_only() {
|
|
215
|
+
local root="/tmp/maxy-585-E-$$"; local out
|
|
216
|
+
out=$(setup_tree "$root"); local script="${out% *}"; local cfg="${out#* }"
|
|
217
|
+
write_parent "$cfg" "$UUID_A"
|
|
218
|
+
write_subagent_failed "$cfg" "$UUID_A" "agent-aDEAD0000" "content-producer"
|
|
219
|
+
write_parent "$cfg" "$UUID_CLEAN"
|
|
220
|
+
write_subagent_clean "$cfg" "$UUID_CLEAN" "agent-aSAFE0000" "code-reviewer"
|
|
221
|
+
|
|
222
|
+
invoke "$script" "$cfg" "--scan-subagent-errors"
|
|
223
|
+
cleanup_tree "$root"
|
|
224
|
+
|
|
225
|
+
[[ $RC -eq 0 ]] || { echo " expected exit 0, got $RC; err=$ERR"; return 1; }
|
|
226
|
+
[[ "$OUT" == *"agent-aDEAD0000"* ]] || { echo " failed subagent not listed"; return 1; }
|
|
227
|
+
[[ "$OUT" == *"content-producer"* ]] || { echo " failed subagent agentType missing"; return 1; }
|
|
228
|
+
[[ "$OUT" == *"command not found"* ]] || { echo " error text missing in scan"; return 1; }
|
|
229
|
+
[[ "$OUT" != *"agent-aSAFE0000"* ]] || { echo " clean subagent wrongly listed"; return 1; }
|
|
230
|
+
return 0
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
# --- Case G: parent's OWN tool error is not mislabelled as a subagent error ---
|
|
234
|
+
case_parent_error_not_subagent() {
|
|
235
|
+
local root="/tmp/maxy-585-G-$$"; local out
|
|
236
|
+
out=$(setup_tree "$root"); local script="${out% *}"; local cfg="${out#* }"
|
|
237
|
+
write_parent_with_error "$cfg" "$UUID_A"
|
|
238
|
+
write_subagent_clean "$cfg" "$UUID_A" "agent-aCLEAN111" "code-reviewer"
|
|
239
|
+
write_pidfile "$cfg" 4244 "$UUID_A" "$BRIDGE"
|
|
240
|
+
|
|
241
|
+
invoke "$script" "$cfg" "session_${BRIDGE}"
|
|
242
|
+
cleanup_tree "$root"
|
|
243
|
+
|
|
244
|
+
[[ $RC -eq 0 ]] || { echo " expected exit 0, got $RC; err=$ERR"; return 1; }
|
|
245
|
+
[[ "$OUT" == *"PARENT_OWN_GREP_MISS"* ]] || { echo " parent tool error not shown in timeline"; return 1; }
|
|
246
|
+
[[ "$OUT" != *"SUBAGENT ERROR"* ]] || { echo " parent error wrongly tagged SUBAGENT ERROR"; return 1; }
|
|
247
|
+
[[ "$OUT" == *"subagent-errors: 0"* ]] || { echo " header miscounts parent error as subagent error"; return 1; }
|
|
248
|
+
return 0
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
# --- Case I: content-grep resolves a unique body match (last-resort tier) ---
|
|
252
|
+
case_content_grep_single() {
|
|
253
|
+
local root="/tmp/maxy-585-I-$$"; local out
|
|
254
|
+
out=$(setup_tree "$root"); local script="${out% *}"; local cfg="${out#* }"
|
|
255
|
+
write_parent_with_key_in_body "$cfg" "$UUID_A" "session_SOLOKEYxyz1" # no pidfile, no bridgeIds
|
|
256
|
+
|
|
257
|
+
invoke "$script" "$cfg" "session_SOLOKEYxyz1"
|
|
258
|
+
cleanup_tree "$root"
|
|
259
|
+
|
|
260
|
+
[[ $RC -eq 0 ]] || { echo " expected exit 0 (single content match), got $RC; err=$ERR"; return 1; }
|
|
261
|
+
[[ "$OUT" == *"6031941e"* ]] || { echo " content-grep did not resolve the unique match"; return 1; }
|
|
262
|
+
return 0
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
# --- Case H: content-grep refuses when the key matches multiple transcripts ---
|
|
266
|
+
case_content_grep_ambiguous_refuses() {
|
|
267
|
+
local root="/tmp/maxy-585-H-$$"; local out
|
|
268
|
+
out=$(setup_tree "$root"); local script="${out% *}"; local cfg="${out#* }"
|
|
269
|
+
write_parent_with_key_in_body "$cfg" "$UUID_A" "session_AMBIGKEYxyz"
|
|
270
|
+
write_parent_with_key_in_body "$cfg" "$UUID_C" "session_AMBIGKEYxyz" # same key in two bodies
|
|
271
|
+
|
|
272
|
+
invoke "$script" "$cfg" "session_AMBIGKEYxyz"
|
|
273
|
+
cleanup_tree "$root"
|
|
274
|
+
|
|
275
|
+
# Must NOT silently pick one of the two — refuse (exit 1), like uuid-prefix ambiguity.
|
|
276
|
+
[[ $RC -eq 1 ]] || { echo " expected exit 1 (ambiguous content match), got $RC; out=$OUT"; return 1; }
|
|
277
|
+
return 0
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
# --- Case F: unresolvable key → exit 1 (clean miss, not a crash) ---
|
|
281
|
+
case_unresolvable_miss() {
|
|
282
|
+
local root="/tmp/maxy-585-F-$$"; local out
|
|
283
|
+
out=$(setup_tree "$root"); local script="${out% *}"; local cfg="${out#* }"
|
|
284
|
+
write_parent "$cfg" "$UUID_A"
|
|
285
|
+
|
|
286
|
+
invoke "$script" "$cfg" "session_NOSUCHBRIDGE"
|
|
287
|
+
cleanup_tree "$root"
|
|
288
|
+
|
|
289
|
+
[[ $RC -eq 1 ]] || { echo " expected exit 1 on unresolvable key, got $RC"; return 1; }
|
|
290
|
+
return 0
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
run_case "bare key via PID bridge → timeline + flagged subagent error" case_bridge_timeline_flagged
|
|
294
|
+
run_case "clean session → timeline, no error flag" case_clean_no_flag
|
|
295
|
+
run_case "resolve via bridgeIds sidecar (PID file absent)" case_resolve_via_bridgeids
|
|
296
|
+
run_case "resolve via full UUID directly" case_resolve_uuid_direct
|
|
297
|
+
run_case "--scan-subagent-errors lists failed subagent only" case_scan_lists_failed_only
|
|
298
|
+
run_case "parent's own tool error not mislabelled SUBAGENT ERROR" case_parent_error_not_subagent
|
|
299
|
+
run_case "content-grep resolves a unique body match" case_content_grep_single
|
|
300
|
+
run_case "content-grep refuses an ambiguous (multi) body match" case_content_grep_ambiguous_refuses
|
|
301
|
+
run_case "unresolvable key → exit 1" case_unresolvable_miss
|
|
302
|
+
|
|
303
|
+
echo ""
|
|
304
|
+
echo "================================================"
|
|
305
|
+
echo " Passed: $PASS / Failed: $FAIL"
|
|
306
|
+
echo "================================================"
|
|
307
|
+
[[ $FAIL -eq 0 ]]
|