Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
547 changes: 455 additions & 92 deletions docs/specs/2026-07-25-monorepo-workspace-support.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion scripts/agent-customize/core/items.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -719,7 +719,7 @@ function allowedSourceScopes(scopeKind = "user", tab = "plugins") {
return tab === "mcps" ? new Set(["team", "dashboard"]) : new Set(["team"]);
}
if (scopeKind === "workspace" || scopeKind === "project") {
return new Set(["project"]);
return new Set(["project", "inherited"]);
}
return new Set(["user", "plugin"]);
}
Expand Down
106 changes: 102 additions & 4 deletions scripts/agent-lint/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { parseFrontmatter } from "../agent-customize/core/items.mjs";
import { enrichFindingWithRecommendation } from "../findings-recommend.mjs";
import { isDirectory, pathExists } from "../session-analysis/fs.mjs";
import { normalizeWorkspace } from "../session-analysis/paths.mjs";
import { ownerRouteForPath, routeContains } from "../workspace-topology/index.mjs";
import { reviewHostInstructions } from "./host-instructions.mjs";
import { reviewHookAssets } from "./hook-review.mjs";

Expand Down Expand Up @@ -480,9 +481,87 @@ async function collectNestedEntrypoints(workspace, maxEntrypointDepth, provider)
}));
}

function topologyScopeOwnerRoute(route) {
if (route === ".claude/CLAUDE.md"
|| route === ".github/copilot-instructions.md"
|| route.startsWith(".github/instructions/")) return ".";
for (const marker of ["/.claude/rules/", "/.cursor/rules/", "/.qoder/rules/"]) {
const normalized = `/${route}`;
const index = normalized.indexOf(marker);
if (index !== -1) {
return normalized.slice(1, index) || ".";
}
}
const owner = path.posix.dirname(route);
return owner === "." ? "." : owner;
}

function topologyScopeSourceKind(route) {
const base = path.posix.basename(route);
if (route === ".github/copilot-instructions.md") return "copilot-instructions";
if (route.startsWith(".github/instructions/") && route.endsWith(".instructions.md")) {
return "copilot-instructions";
}
if (route.includes("/.claude/rules/") || route.startsWith(".claude/rules/")) return "claude-rule";
if (route.includes("/.cursor/rules/") || route.startsWith(".cursor/rules/")) return "cursor-rule";
if (route.includes("/.qoder/rules/") || route.startsWith(".qoder/rules/")) return "qoder-rule";
if (base === "AGENTS.md") return route === "AGENTS.md" ? "agents-md" : "nested-agent-guide";
if (base === "CLAUDE.md") return route === "CLAUDE.md" ? "claude-md" : "nested-agent-guide";
if (base === "CLAUDE.local.md") return "claude-local";
if (base === "QWEN.md") return "qwen-md-context";
return "nested-agent-guide";
}

function topologyScopeApplies(topology, scope) {
if (topology.target.route === ".") return true;
const ownerRoute = topologyScopeOwnerRoute(scope.route);
return routeContains(ownerRoute, topology.target.route)
|| routeContains(topology.target.route, ownerRoute);
}

async function topologyEntrypoints(topology, provider) {
const workspace = normalizeWorkspace(topology.gitRoot ?? topology.requestedWorkspace);
const selected = (topology.instructionScopes?.items ?? [])
.filter((scope) => !provider || scope.provider === provider)
.filter((scope) => topologyScopeApplies(topology, scope));
const grouped = new Map();

for (const scope of selected) {
const current = grouped.get(scope.route);
const providers = [...new Set([...(current?.providers ?? []), scope.provider])].sort();
grouped.set(scope.route, {
route: scope.route,
providers,
activation: current && (current.activation !== "effective" || scope.activation !== "effective")
? "candidate"
: scope.activation,
});
}

const entrypoints = [];
for (const scope of [...grouped.values()].sort((left, right) => left.route.localeCompare(right.route))) {
const filePath = path.join(workspace, ...scope.route.split("/"));
if (!await pathExists(filePath)) continue;
const sourceKind = topologyScopeSourceKind(scope.route);
entrypoints.push({
path: filePath,
relativePath: scope.route,
sourceKind,
nested: sourceKind === "nested-agent-guide",
activation: scope.activation,
packageRoute: ownerRouteForPath(topology, scope.route),
...(provider ? { provider } : { providers: scope.providers }),
});
}
return entrypoints;
}

export async function discoverAgentEntrypoints(options = {}) {
const workspace = normalizeWorkspace(options.workspace);
const topology = options.topology;
const provider = options.provider ? String(options.provider).toLowerCase() : undefined;
if (topology) return topologyEntrypoints(topology, provider);

const workspace = normalizeWorkspace(options.workspace);
const maxEntrypointDepth = Number(options.maxEntrypointDepth ?? options["max-entrypoint-depth"] ?? 4);
const entrypoints = [];

Expand Down Expand Up @@ -537,7 +616,9 @@ function summarizeEntrypoints(entrypoints) {
}

export async function collectAgentInstructionGraph(options = {}) {
const workspace = normalizeWorkspace(options.workspace);
const workspace = options.topology
? normalizeWorkspace(options.topology.gitRoot ?? options.topology.requestedWorkspace)
: normalizeWorkspace(options.workspace);
const maxReferenceDepth = Number(options.maxReferenceDepth ?? options["max-reference-depth"] ?? 0);
const entrypoints = await discoverAgentEntrypoints({ ...options, workspace });
const queue = entrypoints.map((entrypoint) => ({ ...entrypoint, filePath: entrypoint.path, depth: 0 }));
Expand All @@ -556,6 +637,10 @@ export async function collectAgentInstructionGraph(options = {}) {
const parsed = await parseFile(current.filePath, workspace, {
sourceKind: current.sourceKind,
entrypoint: current.depth === 0,
...(current.activation ? { activation: current.activation } : {}),
...(current.packageRoute ? { packageRoute: current.packageRoute } : {}),
...(current.provider ? { provider: current.provider } : {}),
...(current.providers ? { providers: current.providers } : {}),
});
const references = [];
for (const link of parsed.links) {
Expand Down Expand Up @@ -734,6 +819,8 @@ function finding(id, severity, evidence, remediation, options = {}) {
"assetName",
"scope",
"sourceLabel",
"packageRoute",
"ownerRoute",
]) {
if (options[key] !== undefined) {
result[key] = options[key];
Expand Down Expand Up @@ -1576,18 +1663,29 @@ async function singleWorkspacePayload(options = {}) {
: options.profile === PROFILE_AGENT_ASSETS_REVIEW
? await applyAgentAssetsReviewProfile(graph, options)
: { findings: [], manifestEvidence: [], assetInventory: undefined };
const findings = profileResult.findings.map((item) => {
if (!options.topology || typeof item?.file !== "string") return item;
const route = normalizeSlash(item.file);
if (!route || path.isAbsolute(route) || route === ".." || route.startsWith("../")) return item;
const packageRoute = ownerRouteForPath(options.topology, route);
return {
...item,
packageRoute,
ownerRoute: packageRoute,
};
});
return {
kind: "agent-lint",
profile: options.profile,
summary: {
...summarizeGraph(graph),
...summarizeFindings(profileResult.findings),
...summarizeFindings(findings),
},
graph,
manifestEvidence: profileResult.manifestEvidence,
hostInstructionReview: profileResult.hostInstructionReview,
assetInventory: profileResult.assetInventory,
findings: profileResult.findings,
findings,
};
}

Expand Down
1 change: 1 addition & 0 deletions scripts/better-harness-cli/cli.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ const GROUP_EXAMPLES = {
"harness": [
{ audience: "workflow", text: "better-harness harness analyze --workspace . --language en --format json" },
{ audience: "workflow", text: "better-harness harness checkup --phase scan --provider qoder --workspace . --json" },
{ audience: "advanced", text: "better-harness harness workspace-topology --workspace . --json" },
{ audience: "maintainer", text: "better-harness harness source --workspace . --source <scratch>/report.source.json --language en" },
{ audience: "advanced", text: "better-harness harness render --findings <input>/findings.json --mode qoder-canvas --out .qoder/better-harness --target . --validate --json" },
{ audience: "advanced", text: "better-harness harness preview-canvas <run>/report.canvas.tsx --open" },
Expand Down
7 changes: 7 additions & 0 deletions scripts/better-harness-cli/registry.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,13 @@ const COMMANDS = [
summary: "Collect the three specialist lanes and lead evidence in one frozen-context bundle.",
description: "Return versioned Session Evidence, Project Harness, and Agent Customize envelopes with explicit lane status and unchanged diagnostic commands.",
},
{
name: "workspace-topology",
audience: "advanced",
script: "workspace-topology/cli.mjs",
summary: "Resolve the Git-aware workspace target and member topology.",
description: "Report the canonical repository target, workspace members, instruction scopes, bounded inventory coverage, and path-scoped analysis contract without mutating the workspace.",
},
{
name: "analyze",
audience: "workflow",
Expand Down
110 changes: 107 additions & 3 deletions scripts/coding-agent-practices/asset-baseline.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ const OWNER_KIND_RANK = Object.freeze({
agents: 7,
workflows: 8,
});
const OWNER_SCOPE_RANK = Object.freeze({ workspace: 0, project: 0, user: 1, plugin: 2 });
const OWNER_SCOPE_RANK = Object.freeze({ workspace: 0, project: 0, inherited: 1, user: 2, plugin: 3 });

function text(value, limit = 320) {
return String(value ?? "").replace(/[\u0000-\u001f\u007f]/gu, " ").replace(/\s+/gu, " ").trim().slice(0, limit);
Expand Down Expand Up @@ -67,6 +67,7 @@ function compactFindings(findings = []) {
items: ordered.slice(0, MAX_BASELINE_FINDINGS).map(compactFinding),
total: ordered.length,
omitted: Math.max(0, ordered.length - MAX_BASELINE_FINDINGS),
truncated: ordered.length > MAX_BASELINE_FINDINGS,
};
}

Expand All @@ -91,7 +92,9 @@ function ownerRoutes(inventory, workspace) {
name,
version: text(item?.version, 32),
owner: text(item?.pluginName ?? item?.ownerName ?? item?.sourceLabel, 96),
route: workspaceRoute(item?.path ?? item?.filePath, workspace),
route: text(item?.originRoute, 180)
|| workspaceRoute(item?.path ?? item?.filePath, workspace),
effectiveTarget: text(item?.effectiveTarget, 180),
}).filter(([, value]) => value !== undefined && value !== ""));
const key = [route.kind, route.scope, route.name, route.version, route.owner, route.route].join(":");
if (!routes.has(key)) routes.set(key, route);
Expand Down Expand Up @@ -125,6 +128,7 @@ function ownerRoutes(inventory, workspace) {
items: selected,
total: ordered.length,
omitted: Math.max(0, ordered.length - MAX_BASELINE_OWNER_ROUTES),
truncated: ordered.length > MAX_BASELINE_OWNER_ROUTES,
};
}

Expand Down Expand Up @@ -197,6 +201,81 @@ function available(data) {
return { status: "available", data };
}

function inheritedWorkspaceRoots(topology, workspace) {
if (!topology?.gitRoot
|| !new Set(["workspace-member", "repo-subtree"]).has(topology.target?.kind)) {
return [];
}
const relative = path.relative(topology.gitRoot, workspace);
if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) return [];
const parts = relative.split(path.sep).filter(Boolean);
const roots = [topology.gitRoot];
for (let index = 1; index < parts.length; index += 1) {
roots.push(path.join(topology.gitRoot, ...parts.slice(0, index)));
}
return roots;
}

function rawItemPath(item) {
return item?.evidence?.path ?? item?.filePath ?? item?.rootPath;
}

function inheritedItem(item, topology) {
const filePath = rawItemPath(item);
const relative = filePath ? path.relative(topology.gitRoot, path.resolve(filePath)) : "";
const originRoute = relative
&& !relative.startsWith("..")
&& !path.isAbsolute(relative)
? relative.split(path.sep).join("/")
: undefined;
return {
...item,
scope: "inherited",
originScope: "inherited",
originRoute,
effectiveTarget: topology.target.route,
};
}

function mergeInheritedInventories(projectInventory, inheritedInventories, topology) {
const manage = Object.fromEntries(
Object.entries(projectInventory.manage ?? {}).map(([collection, items]) => [collection, [...items]]),
);
for (const inventory of inheritedInventories) {
for (const [collection, items] of Object.entries(inventory.manage ?? {})) {
const target = manage[collection] ?? [];
for (const item of items) {
if (item?.scope !== "project") continue;
const inherited = inheritedItem(item, topology);
const key = [
inherited.id,
inherited.kind,
inherited.name ?? inherited.displayName ?? inherited.label,
rawItemPath(inherited),
].join(":");
if (!target.some((candidate) => [
candidate.id,
candidate.kind,
candidate.name ?? candidate.displayName ?? candidate.label,
rawItemPath(candidate),
].join(":") === key)) {
target.push(inherited);
}
}
manage[collection] = target;
}
}
return {
...projectInventory,
manage,
diagnostics: {
...(projectInventory.diagnostics ?? {}),
inheritedWorkspaceCount: inheritedInventories.length,
inheritedTargetRoute: topology.target.route,
},
};
}

export async function collectAssetBaseline(options = {}, dependencies = {}) {
const provider = options.provider ?? options.platform ?? "qoder";
if (!PROVIDERS.has(provider)) {
Expand Down Expand Up @@ -224,6 +303,20 @@ export async function collectAssetBaseline(options = {}, dependencies = {}) {
let rawInventory;
try {
rawInventory = await collectRawInventory(common);
const inheritedRoots = inheritedWorkspaceRoots(options.topology, workspace);
if (inheritedRoots.length > 0) {
const inheritedInventories = [];
for (const inheritedWorkspace of inheritedRoots) {
inheritedInventories.push(await collectRawInventory({
...common,
workspace: inheritedWorkspace,
includeUserHome: false,
includeGlobalHooks: false,
includeMemories: false,
}));
}
rawInventory = mergeInheritedInventories(rawInventory, inheritedInventories, options.topology);
}
} catch (error) {
const failed = unavailable(error, "inventory");
return {
Expand Down Expand Up @@ -262,17 +355,28 @@ export async function collectAssetBaseline(options = {}, dependencies = {}) {
}
const envelopes = { lint: lintEnvelope, inventory: inventoryEnvelope, integrity: integrityEnvelope };
const availableCount = Object.values(envelopes).filter((envelope) => envelope.status === "available").length;
const truncatedStages = [
lintEnvelope.data?.findings?.truncated ? "lint-findings" : null,
inventoryEnvelope.data?.ownerRoutes?.truncated ? "inventory-owner-routes" : null,
integrityEnvelope.data?.findings?.truncated ? "integrity-findings" : null,
].filter(Boolean);
return {
kind: ASSET_BASELINE_KIND,
schemaVersion: ASSET_BASELINE_SCHEMA_VERSION,
status: availableCount === 3 ? "complete" : availableCount === 0 ? "failed" : "partial",
status: availableCount === 3 && truncatedStages.length === 0
? "complete"
: availableCount === 0
? "failed"
: "partial",
scope: { provider, workspace, includeUserHome, includeMemories },
envelopes,
diagnostics: {
sharedInventorySnapshot: true,
compact: true,
findingLimitPerEnvelope: MAX_BASELINE_FINDINGS,
ownerRouteLimit: MAX_BASELINE_OWNER_ROUTES,
inheritedWorkspaceCount: rawInventory?.diagnostics?.inheritedWorkspaceCount ?? 0,
truncatedStages,
},
};
}
Expand Down
Loading