@webpresso/agent-kit 3.3.4 → 3.3.5
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/esm/audit/agents.js +126 -26
- package/dist/esm/blueprint/trust/dossier.js +41 -1
- package/dist/esm/build/vendor-oxlint-plugins.d.ts +4 -1
- package/dist/esm/build/vendor-oxlint-plugins.js +9 -14
- package/dist/esm/cli/commands/init/index.js +3 -4
- package/dist/esm/cli/commands/review.d.ts +1 -0
- package/dist/esm/cli/commands/review.js +77 -1
- package/dist/esm/cli/commands/sync.d.ts +8 -6
- package/dist/esm/cli/commands/sync.js +11 -9
- package/dist/esm/config/oxlint/oxlintrc.d.ts +1 -1
- package/dist/esm/config/oxlint/oxlintrc.js +3 -12
- package/dist/esm/content/dispatch.js +4 -0
- package/dist/esm/content/loader.d.ts +1 -1
- package/dist/esm/content/loader.js +10 -6
- package/dist/esm/content/skill-policy.d.ts +11 -0
- package/dist/esm/content/skill-policy.js +33 -0
- package/dist/esm/hooks/pretool-guard/dev-routing.js +67 -4
- package/dist/esm/hooks/pretool-guard/validators/worktree-discipline.js +62 -3
- package/dist/esm/review/authority.js +56 -1
- package/dist/esm/review/delivery-verifier.d.ts +4 -0
- package/dist/esm/review/delivery-verifier.js +9 -1
- package/dist/esm/symlinker/consumers.d.ts +3 -1
- package/dist/esm/symlinker/consumers.js +7 -1
- package/dist/esm/symlinker/unified-sync.d.ts +3 -3
- package/dist/esm/symlinker/unified-sync.js +88 -11
- package/dist/esm/test/shard-durations.json +0 -1
- package/package.json +12 -12
- package/dist/esm/cli/commands/init/scaffolders/subagents/index.d.ts +0 -6
- package/dist/esm/cli/commands/init/scaffolders/subagents/index.js +0 -77
|
@@ -21,10 +21,12 @@ const DEFAULT_KINDS = ["rule", "skill"];
|
|
|
21
21
|
const CONSUMER_DIR_BY_KIND = {
|
|
22
22
|
rule: "agent-rules",
|
|
23
23
|
skill: "agent-skills",
|
|
24
|
+
agent: "agent-agents",
|
|
24
25
|
};
|
|
25
26
|
const CATALOG_DIR_BY_KIND = {
|
|
26
27
|
rule: "rules",
|
|
27
28
|
skill: "skills",
|
|
29
|
+
agent: "agents",
|
|
28
30
|
};
|
|
29
31
|
export function loadContent(options) {
|
|
30
32
|
const { catalogDir, consumerRoot, kinds = DEFAULT_KINDS } = options;
|
|
@@ -83,9 +85,11 @@ function readKind(args) {
|
|
|
83
85
|
const stat = statSync(root);
|
|
84
86
|
if (!stat.isDirectory())
|
|
85
87
|
return [];
|
|
86
|
-
|
|
88
|
+
// agent kind shares the rule shape: flat `<slug>.md` files, README.md
|
|
89
|
+
// excluded. Only `skill` is directory-shaped (`<slug>/SKILL.md`).
|
|
90
|
+
return kind === "skill" ? readSkills(root, source) : readFlatMarkdown(kind, root, source);
|
|
87
91
|
}
|
|
88
|
-
function
|
|
92
|
+
function readFlatMarkdown(kind, root, source) {
|
|
89
93
|
const out = [];
|
|
90
94
|
for (const entry of readdirSync(root, { withFileTypes: true })) {
|
|
91
95
|
if (!entry.isFile())
|
|
@@ -93,9 +97,9 @@ function readRules(root, source) {
|
|
|
93
97
|
if (!entry.name.endsWith(".md"))
|
|
94
98
|
continue;
|
|
95
99
|
// Skip directory-documentation files. README.md exists in both the
|
|
96
|
-
// canonical catalog and the consumer-owned agent-rules/
|
|
97
|
-
// surface; treating it as
|
|
98
|
-
// collision and would also try to project it into per-IDE surfaces.
|
|
100
|
+
// canonical catalog and the consumer-owned agent-rules/ (or agent-agents/)
|
|
101
|
+
// scaffolder surface; treating it as content would produce a spurious
|
|
102
|
+
// slug collision and would also try to project it into per-IDE surfaces.
|
|
99
103
|
if (entry.name === "README.md")
|
|
100
104
|
continue;
|
|
101
105
|
const filePath = realpathSync(join(root, entry.name));
|
|
@@ -103,7 +107,7 @@ function readRules(root, source) {
|
|
|
103
107
|
const parsed = matter(readFileSync(filePath, "utf8"));
|
|
104
108
|
const raw = { ...parsed.data };
|
|
105
109
|
out.push({
|
|
106
|
-
kind
|
|
110
|
+
kind,
|
|
107
111
|
slug,
|
|
108
112
|
source,
|
|
109
113
|
filePath,
|
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
export declare const DEFAULT_PACKAGED_SKILL_SLUGS: readonly ["verify", "fix", "investigate", "ai-deslop", "plan-refine", "ralplan", "best-practice-research", "testing-philosophy", "tph", "claude", "codex", "grok", "opencode-go", "plan-eng-review", "plan-ceo-review", "plan-design-review", "plan-devex-review", "browse", "devex-review", "design-review", "hooks-doctor", "tech-debt", "lore-protocol", "deep-interview", "deep-research", "autoresearch", "ultragoal", "autopilot", "team"];
|
|
2
2
|
export type DefaultPackagedSkillSlug = (typeof DEFAULT_PACKAGED_SKILL_SLUGS)[number];
|
|
3
3
|
export declare const DEFAULT_PACKAGED_SKILL_SLUG_SET: ReadonlySet<string>;
|
|
4
|
+
/**
|
|
5
|
+
* Catalog skills that are deliberately NOT in DEFAULT_PACKAGED_SKILL_SLUGS,
|
|
6
|
+
* with a one-line reason each. Every slug under `catalog/agent/skills/` must
|
|
7
|
+
* appear in either that list or this one (enforced by
|
|
8
|
+
* `checkCatalogSkillClassification` in `src/audit/agents.ts`) — an
|
|
9
|
+
* unclassified slug means a new catalog skill shipped without a deliberate
|
|
10
|
+
* packaging decision.
|
|
11
|
+
*/
|
|
12
|
+
export declare const UNPACKAGED_SKILL_SLUGS: Readonly<Record<string, string>>;
|
|
13
|
+
export declare const UNPACKAGED_SKILL_SLUG_SET: ReadonlySet<string>;
|
|
14
|
+
export declare function isUnpackagedSkillSlug(slug: string): boolean;
|
|
4
15
|
export declare const MERGED_DELETED_SKILL_SLUGS: readonly ["autoplan", "health", "systematic-debugging", "test-driven-development"];
|
|
5
16
|
export declare const MERGED_DELETED_SKILL_SLUG_SET: ReadonlySet<string>;
|
|
6
17
|
/**
|
|
@@ -30,6 +30,39 @@ export const DEFAULT_PACKAGED_SKILL_SLUGS = [
|
|
|
30
30
|
"team",
|
|
31
31
|
];
|
|
32
32
|
export const DEFAULT_PACKAGED_SKILL_SLUG_SET = new Set(DEFAULT_PACKAGED_SKILL_SLUGS);
|
|
33
|
+
/**
|
|
34
|
+
* Catalog skills that are deliberately NOT in DEFAULT_PACKAGED_SKILL_SLUGS,
|
|
35
|
+
* with a one-line reason each. Every slug under `catalog/agent/skills/` must
|
|
36
|
+
* appear in either that list or this one (enforced by
|
|
37
|
+
* `checkCatalogSkillClassification` in `src/audit/agents.ts`) — an
|
|
38
|
+
* unclassified slug means a new catalog skill shipped without a deliberate
|
|
39
|
+
* packaging decision.
|
|
40
|
+
*/
|
|
41
|
+
export const UNPACKAGED_SKILL_SLUGS = {
|
|
42
|
+
qa: "source-catalog QA skill, not package-visible in the default plugin skill set today (see workflow-skills-routing.md)",
|
|
43
|
+
"qa-only": "source-catalog QA skill, not package-visible in the default plugin skill set today (see workflow-skills-routing.md)",
|
|
44
|
+
review: "listed as a default workflow skill in workflow-skills-routing.md but not yet added to DEFAULT_PACKAGED_SKILL_SLUGS",
|
|
45
|
+
pll: "ultragoal's parallel-execute companion (ready-queue/worktrees driver); not independently default-packaged",
|
|
46
|
+
"frontend-design": "opt-in design-guidance skill, not part of the default packaged workflow set",
|
|
47
|
+
"monorepo-navigation": "tier-3 opt-in preset (wp setup --with monorepo-navigation), not default-packaged",
|
|
48
|
+
"tanstack-query": "tier-3 opt-in preset (wp setup --with tanstack-query), not default-packaged",
|
|
49
|
+
"react-doctor": "opt-in framework best-practice skill, not default-packaged",
|
|
50
|
+
"better-auth-best-practices": "opt-in best-practice reference skill, not default-packaged",
|
|
51
|
+
"logging-best-practices": "opt-in best-practice reference skill, not default-packaged",
|
|
52
|
+
"vercel-react-best-practices": "opt-in best-practice reference skill, not default-packaged",
|
|
53
|
+
"web-design-guidelines": "opt-in best-practice reference skill, not default-packaged",
|
|
54
|
+
deepseek: "individual OpenCode-Go model reviewer skill; only the aggregate opencode-go skill is default-packaged",
|
|
55
|
+
glm: "individual OpenCode-Go model reviewer skill; only the aggregate opencode-go skill is default-packaged",
|
|
56
|
+
hy3: "individual OpenCode-Go model reviewer skill; only the aggregate opencode-go skill is default-packaged",
|
|
57
|
+
kimi: "individual OpenCode-Go model reviewer skill; only the aggregate opencode-go skill is default-packaged",
|
|
58
|
+
mimo: "individual OpenCode-Go model reviewer skill; only the aggregate opencode-go skill is default-packaged",
|
|
59
|
+
minimax: "individual OpenCode-Go model reviewer skill; only the aggregate opencode-go skill is default-packaged",
|
|
60
|
+
qwen: "individual OpenCode-Go model reviewer skill; only the aggregate opencode-go skill is default-packaged",
|
|
61
|
+
};
|
|
62
|
+
export const UNPACKAGED_SKILL_SLUG_SET = new Set(Object.keys(UNPACKAGED_SKILL_SLUGS));
|
|
63
|
+
export function isUnpackagedSkillSlug(slug) {
|
|
64
|
+
return UNPACKAGED_SKILL_SLUG_SET.has(slug);
|
|
65
|
+
}
|
|
33
66
|
export const MERGED_DELETED_SKILL_SLUGS = [
|
|
34
67
|
"autoplan",
|
|
35
68
|
"health",
|
|
@@ -296,11 +296,11 @@ const SAFE_PASSTHROUGH_PREFIXES = [
|
|
|
296
296
|
const SANDBOX_PREFIXES = [
|
|
297
297
|
{
|
|
298
298
|
prefix: "grep",
|
|
299
|
-
guidance: "
|
|
299
|
+
guidance: "Recursive/directory-wide grep is unbounded; narrow to explicit file target(s) (`grep -n pattern file`) to pass through directly. Otherwise use wp_session_batch_execute for bounded search so output is capped, indexed, and recallable instead of flooding the transcript — note wp_session_batch_execute runs argv directly (no shell) and cannot express `|`/`>`. If MCP is unavailable, use a bounded `rg -n ... | sed -n '1,120p'` excerpt instead of recursive raw grep output",
|
|
300
300
|
},
|
|
301
301
|
{
|
|
302
302
|
prefix: "find",
|
|
303
|
-
guidance: "Use wp_session_batch_execute for bounded shell gathering/search commands so output is capped, indexed, and recallable instead of flooding the transcript
|
|
303
|
+
guidance: "Use wp_session_batch_execute for bounded shell gathering/search commands so output is capped, indexed, and recallable instead of flooding the transcript — note wp_session_batch_execute runs argv directly (no shell) and cannot express `|`/`>`. If MCP is unavailable, use `rg --files ... | sed -n '1,120p'` or another explicit bounded listing",
|
|
304
304
|
},
|
|
305
305
|
{
|
|
306
306
|
prefix: "cat",
|
|
@@ -324,7 +324,7 @@ const SANDBOX_PREFIXES = [
|
|
|
324
324
|
},
|
|
325
325
|
{
|
|
326
326
|
prefix: "git log",
|
|
327
|
-
guidance: "
|
|
327
|
+
guidance: "Add an explicit count bound (`-N`, `-n N`, or `--max-count=N`) to pass through directly — e.g. `git log --oneline -50`. Otherwise use wp_session_batch_execute for bounded git log shell gathering so output is capped, indexed, and recallable. If MCP is unavailable, use an explicit bound directly, e.g. `git log --oneline -n 50`",
|
|
328
328
|
},
|
|
329
329
|
{
|
|
330
330
|
prefix: "git diff",
|
|
@@ -332,7 +332,7 @@ const SANDBOX_PREFIXES = [
|
|
|
332
332
|
},
|
|
333
333
|
{
|
|
334
334
|
prefix: "git show",
|
|
335
|
-
guidance: "
|
|
335
|
+
guidance: "Add `--stat`/`--name-only`/`--name-status`/`--shortstat` to pass through directly — e.g. `git show --stat HEAD` — or use wp_session_batch_execute or wp_session_execute for bounded gathering of the full patch",
|
|
336
336
|
},
|
|
337
337
|
{
|
|
338
338
|
prefix: "vp run build",
|
|
@@ -782,6 +782,63 @@ const BOUNDED_GIT_DIFF = new RegExp(String.raw `^git\s+${SIMPLE_GIT_GLOBAL_OPTIO
|
|
|
782
782
|
function isBoundedGitDiffRead(command) {
|
|
783
783
|
return BOUNDED_GIT_DIFF.test(command);
|
|
784
784
|
}
|
|
785
|
+
// `git log`/`git show` reads are bounded (small, predictable output) exactly when
|
|
786
|
+
// they carry an explicit count/summary bound the same way BOUNDED_GIT_DIFF already
|
|
787
|
+
// recognizes `--stat`/`--name-only`/etc. as bounded. Unbounded forms (bare `git log`,
|
|
788
|
+
// full-patch `git show <ref>`) still fall through to the SANDBOX_PREFIXES rules below.
|
|
789
|
+
const SIMPLE_GIT_LOG_PREFIX = new RegExp(String.raw `^git\s+${SIMPLE_GIT_GLOBAL_OPTION}log\b`, "u");
|
|
790
|
+
const GIT_LOG_COUNT_BOUND = /(?:^|\s)(?:-\d+|-n[=\s]?\d+|--max-count(?:=|\s+)\d+)(?:\s|$)/u;
|
|
791
|
+
function isBoundedGitLogRead(command) {
|
|
792
|
+
return SIMPLE_GIT_LOG_PREFIX.test(command) && GIT_LOG_COUNT_BOUND.test(command);
|
|
793
|
+
}
|
|
794
|
+
const BOUNDED_GIT_SHOW = new RegExp(String.raw `^git\s+${SIMPLE_GIT_GLOBAL_OPTION}show\b(?=[\s\S]*(?:--stat\b|--shortstat\b|--name-only\b|--name-status\b))(?![\s\S]*(?:--patch\b|-p\b))`, "u");
|
|
795
|
+
function isBoundedGitShowRead(command) {
|
|
796
|
+
return BOUNDED_GIT_SHOW.test(command);
|
|
797
|
+
}
|
|
798
|
+
// A grep read is bounded when it targets explicit file argument(s) (not a whole
|
|
799
|
+
// directory tree via -r/-R/--recursive/--include/--exclude*) and supplies at least
|
|
800
|
+
// one non-flag file target beyond the pattern itself. Recursive/directory-wide grep
|
|
801
|
+
// stays sandboxed since its output size is unpredictable.
|
|
802
|
+
const GREP_RECURSIVE_LONG_FLAGS = new Set([
|
|
803
|
+
"--recursive",
|
|
804
|
+
"--include",
|
|
805
|
+
"--exclude",
|
|
806
|
+
"--exclude-dir",
|
|
807
|
+
"--exclude-from",
|
|
808
|
+
]);
|
|
809
|
+
function isBoundedGrepRead(command) {
|
|
810
|
+
const tokens = tokenizeCommand(command);
|
|
811
|
+
if (tokens[0] !== "grep")
|
|
812
|
+
return false;
|
|
813
|
+
const positionals = [];
|
|
814
|
+
let optionsEnded = false;
|
|
815
|
+
for (let index = 1; index < tokens.length; index += 1) {
|
|
816
|
+
const token = tokens[index];
|
|
817
|
+
if (!token)
|
|
818
|
+
continue;
|
|
819
|
+
if (!optionsEnded && token === "--") {
|
|
820
|
+
optionsEnded = true;
|
|
821
|
+
continue;
|
|
822
|
+
}
|
|
823
|
+
if (!optionsEnded && token !== "-" && token.startsWith("-")) {
|
|
824
|
+
const longFlag = token.split("=")[0] ?? token;
|
|
825
|
+
if (GREP_RECURSIVE_LONG_FLAGS.has(longFlag))
|
|
826
|
+
return false;
|
|
827
|
+
// Strip a trailing bundled numeric argument (e.g. the `2` in `-rA2` for
|
|
828
|
+
// `-r -A 2`) before checking for a bundled `r`/`R` — otherwise a
|
|
829
|
+
// recursive short flag bundled with a value-taking flag like `-A`/`-B`/
|
|
830
|
+
// `-C`/`-m` slips past undetected.
|
|
831
|
+
const shortCluster = token.slice(1).replace(/\d+$/u, "");
|
|
832
|
+
if (/^[A-Za-z]+$/u.test(shortCluster) && /[rR]/u.test(shortCluster))
|
|
833
|
+
return false;
|
|
834
|
+
continue;
|
|
835
|
+
}
|
|
836
|
+
positionals.push(token);
|
|
837
|
+
}
|
|
838
|
+
// First positional is the search pattern; at least one more explicit file
|
|
839
|
+
// target is required to count as a bounded (non-stdin, non-directory) read.
|
|
840
|
+
return positionals.length >= 2;
|
|
841
|
+
}
|
|
785
842
|
function escapedRegexToken(value) {
|
|
786
843
|
return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
787
844
|
}
|
|
@@ -982,6 +1039,12 @@ export function routeCommand(command, _sessionId) {
|
|
|
982
1039
|
}
|
|
983
1040
|
if (isBoundedGitDiffRead(trimmed))
|
|
984
1041
|
return { action: { action: "passthrough" } };
|
|
1042
|
+
if (isBoundedGitLogRead(trimmed))
|
|
1043
|
+
return { action: { action: "passthrough" } };
|
|
1044
|
+
if (isBoundedGitShowRead(trimmed))
|
|
1045
|
+
return { action: { action: "passthrough" } };
|
|
1046
|
+
if (isBoundedGrepRead(trimmed))
|
|
1047
|
+
return { action: { action: "passthrough" } };
|
|
985
1048
|
// Explicit passthroughs (audits, safe git/nav commands)
|
|
986
1049
|
for (const prefix of PASSTHROUGH_PREFIXES) {
|
|
987
1050
|
if (matchesPrefix(trimmed, prefix))
|
|
@@ -850,7 +850,9 @@ const GIT_GLOBAL_RUN = `(?:-C\\s+${QUOTED_ARG}\\s+|-c\\s+${QUOTED_ARG}\\s+|--git
|
|
|
850
850
|
// branch flags are allowed.
|
|
851
851
|
const FORBIDDEN_SUBCOMMAND = "(commit\\b|switch\\b(?!\\s+(?:-h|--help)\\b)|checkout\\s+(?!--(?:\\s|$))(?:(?:\\S+\\s+)*)\\S|branch\\s+(?:(?!-|--)\\S|--track\\b|--no-track\\b|--create-reflog\\b|--force\\b|-f\\b|-c\\b|-C\\b|-m\\b|-M\\b))";
|
|
852
852
|
const FORBIDDEN_OP = new RegExp(`\\bgit\\s+(${GIT_GLOBAL_RUN})${FORBIDDEN_SUBCOMMAND}`, "gu");
|
|
853
|
-
|
|
853
|
+
// Verb-only: deliberately does NOT slurp trailing arguments itself (see
|
|
854
|
+
// mainOwnershipGitOps below for why a naive `[^;&|(){}]*` capture is unsafe).
|
|
855
|
+
const MAIN_OWNERSHIP_VERB = new RegExp(`\\bgit\\s+(${GIT_GLOBAL_RUN})(switch\\b|checkout\\b|branch\\b|worktree\\s+add\\b)`, "gu");
|
|
854
856
|
function labelFor(subcommand) {
|
|
855
857
|
if (subcommand.startsWith("commit"))
|
|
856
858
|
return "git commit";
|
|
@@ -958,12 +960,69 @@ function mainOwnershipLabel(words) {
|
|
|
958
960
|
return "git worktree add main";
|
|
959
961
|
return null;
|
|
960
962
|
}
|
|
963
|
+
/**
|
|
964
|
+
* Quote-aware segment bounds for `command`, split on the same unquoted
|
|
965
|
+
* `;&|(){}` control characters as {@link splitShellSegments} (kept in sync
|
|
966
|
+
* intentionally), but returning OFFSETS instead of trimmed substrings so a
|
|
967
|
+
* caller can slice the original command text for a match that falls inside a
|
|
968
|
+
* given segment.
|
|
969
|
+
*/
|
|
970
|
+
function segmentBounds(command) {
|
|
971
|
+
const bounds = [];
|
|
972
|
+
let start = 0;
|
|
973
|
+
let quote = null;
|
|
974
|
+
let escaped = false;
|
|
975
|
+
for (let i = 0; i < command.length; i += 1) {
|
|
976
|
+
const ch = command[i];
|
|
977
|
+
if (ch === undefined)
|
|
978
|
+
continue;
|
|
979
|
+
if (escaped) {
|
|
980
|
+
escaped = false;
|
|
981
|
+
continue;
|
|
982
|
+
}
|
|
983
|
+
if (quote !== "'" && ch === "\\") {
|
|
984
|
+
escaped = true;
|
|
985
|
+
continue;
|
|
986
|
+
}
|
|
987
|
+
if ((ch === '"' || ch === "'") && (quote === null || quote === ch)) {
|
|
988
|
+
quote = quote === ch ? null : ch;
|
|
989
|
+
continue;
|
|
990
|
+
}
|
|
991
|
+
if (!quote && /[;&|(){}]/u.test(ch)) {
|
|
992
|
+
bounds.push({ start, end: i });
|
|
993
|
+
start = i + 1;
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
bounds.push({ start, end: command.length });
|
|
997
|
+
return bounds;
|
|
998
|
+
}
|
|
999
|
+
/**
|
|
1000
|
+
* Main-ownership candidate ops (`git switch|checkout|branch|worktree add`).
|
|
1001
|
+
*
|
|
1002
|
+
* MAIN_OWNERSHIP_VERB intentionally does NOT slurp trailing arguments itself
|
|
1003
|
+
* (unlike the pre-fix implementation) — a naive `[^;&|(){}]*` character class
|
|
1004
|
+
* is not quote-aware, so a read-only `git branch --format='%(refname:short)'`
|
|
1005
|
+
* has its captured subcommand text truncated mid-quote at the `(` inside the
|
|
1006
|
+
* quoted format string, producing an unterminated quote that shellWords()
|
|
1007
|
+
* reports as "ambiguous" and gets fail-closed blocked — purely because of a
|
|
1008
|
+
* paren inside its OWN quoted argument, not because it mutates anything.
|
|
1009
|
+
* Bounding the subcommand text to the enclosing quote-aware shell segment
|
|
1010
|
+
* (segmentBounds, sharing splitShellSegments' control-char set) fixes that
|
|
1011
|
+
* without weakening real-mutation detection: genuinely ambiguous syntax
|
|
1012
|
+
* (nested shells, unresolved substitutions, unterminated quotes) still
|
|
1013
|
+
* produces "ambiguous" and fails closed exactly as before.
|
|
1014
|
+
*/
|
|
961
1015
|
function mainOwnershipGitOps(command) {
|
|
962
1016
|
if (!/\bgit\b/u.test(command))
|
|
963
1017
|
return [];
|
|
964
1018
|
const ops = [];
|
|
965
|
-
|
|
966
|
-
|
|
1019
|
+
const segments = segmentBounds(command);
|
|
1020
|
+
for (const m of command.matchAll(MAIN_OWNERSHIP_VERB)) {
|
|
1021
|
+
const verb = m[2] ?? "";
|
|
1022
|
+
const verbStart = (m.index ?? 0) + m[0].length - verb.length;
|
|
1023
|
+
const segment = segments.find((s) => verbStart >= s.start && verbStart < s.end) ??
|
|
1024
|
+
{ start: verbStart, end: command.length };
|
|
1025
|
+
const subcommand = command.slice(verbStart, segment.end);
|
|
967
1026
|
const words = shellWords(subcommand);
|
|
968
1027
|
if (words === "ambiguous") {
|
|
969
1028
|
ops.push({ label: "git main", globals: m[1] ?? "", index: m.index ?? 0 });
|
|
@@ -9,6 +9,7 @@ import { createHash } from "node:crypto";
|
|
|
9
9
|
import { existsSync, lstatSync, readFileSync } from "node:fs";
|
|
10
10
|
import path from "node:path";
|
|
11
11
|
import { evaluateReviewEvents, parseReviewEventLog } from "./events.js";
|
|
12
|
+
import { isPolicyApprovalVerdict } from "./verdict.js";
|
|
12
13
|
import { createDeliverySubjectAtRef, createLegacyPlanSubjectAtRef, createPlanSubjectAtRef, createPreTagsStripPlanSubjectAtRef, resolveReviewCommit, } from "./subject.js";
|
|
13
14
|
const GIT_TIMEOUT_MS = 5_000;
|
|
14
15
|
const GIT_MAX_BUFFER_BYTES = 128 * 1024 * 1024;
|
|
@@ -114,6 +115,54 @@ function markCompromisedArtifactEvents(cwd, entries, blueprintDirectory, events)
|
|
|
114
115
|
}
|
|
115
116
|
return { compromisedIds, issues };
|
|
116
117
|
}
|
|
118
|
+
/** Bounded `git branch -a --contains <commit>` probe, tolerant of a pruned/unknown object. */
|
|
119
|
+
function branchesContainingCommit(cwd, commit) {
|
|
120
|
+
try {
|
|
121
|
+
const output = gitBuffer(cwd, ["branch", "-a", "--contains", commit]).toString("utf8");
|
|
122
|
+
return output
|
|
123
|
+
.split("\n")
|
|
124
|
+
.map((line) => line.trim())
|
|
125
|
+
.filter((line) => line.length > 0);
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
// Diagnostic-only: names historical ledger approvals whose `reviewedCommit` is
|
|
132
|
+
// no longer reachable from any branch (typically because a rebase rewrote the
|
|
133
|
+
// history that commit lived on). Such an approval no longer matches the
|
|
134
|
+
// current subjectDigest, so it already contributes zero approvals under the
|
|
135
|
+
// existing policy in ./events.ts -- this only makes the reason visible instead
|
|
136
|
+
// of the operator seeing a bare "approvals does not satisfy the promotion
|
|
137
|
+
// reviewer mix" and mistaking a dead commit ref for a genuinely missing
|
|
138
|
+
// review. Never mutates `approvals`/`matching`; policy is unchanged.
|
|
139
|
+
function markOrphanedApprovalEvents(cwd, events, current) {
|
|
140
|
+
const issues = [];
|
|
141
|
+
const seen = new Set();
|
|
142
|
+
for (const event of events) {
|
|
143
|
+
if (event.purpose !== current.purpose ||
|
|
144
|
+
event.blueprintSlug !== current.blueprintSlug ||
|
|
145
|
+
event.subjectScheme !== current.subjectScheme ||
|
|
146
|
+
event.subjectDigest === current.subjectDigest ||
|
|
147
|
+
event.status !== "complete" ||
|
|
148
|
+
!isPolicyApprovalVerdict(event.verdict)) {
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
const key = `${event.reviewer}:${event.reviewedCommit}`;
|
|
152
|
+
if (seen.has(key))
|
|
153
|
+
continue;
|
|
154
|
+
seen.add(key);
|
|
155
|
+
const branches = branchesContainingCommit(cwd, event.reviewedCommit);
|
|
156
|
+
if (branches === null || branches.length === 0) {
|
|
157
|
+
issues.push(`Historical approval from ${event.reviewer} (event ${event.id}) reviewed commit ` +
|
|
158
|
+
`${event.reviewedCommit}, which is unreachable from any branch (git branch -a ` +
|
|
159
|
+
`--contains ${event.reviewedCommit} found nothing) -- likely orphaned by a rebase. ` +
|
|
160
|
+
"This approval does not count toward the current subject; rebase this worktree onto " +
|
|
161
|
+
"the latest base and re-run the gate.");
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return issues;
|
|
165
|
+
}
|
|
117
166
|
export function readReviewAuthorityAtRef(cwd, ref, stableSlug, purpose) {
|
|
118
167
|
const commit = resolveReviewCommit(cwd, ref);
|
|
119
168
|
const entries = parseRefTree(cwd, commit);
|
|
@@ -131,6 +180,12 @@ export function readReviewAuthorityAtRef(cwd, ref, stableSlug, purpose) {
|
|
|
131
180
|
const subject = purpose === "plan"
|
|
132
181
|
? createPlanSubjectAtRef(cwd, commit, stableSlug, overview.path)
|
|
133
182
|
: createDeliverySubjectAtRef(cwd, commit, stableSlug, overview.path);
|
|
183
|
+
const orphanIssues = markOrphanedApprovalEvents(cwd, events, {
|
|
184
|
+
purpose,
|
|
185
|
+
blueprintSlug: stableSlug,
|
|
186
|
+
subjectScheme: subject.scheme,
|
|
187
|
+
subjectDigest: subject.digest,
|
|
188
|
+
});
|
|
134
189
|
let evaluation = evaluateReviewEvents(events, {
|
|
135
190
|
purpose,
|
|
136
191
|
blueprintSlug: stableSlug,
|
|
@@ -166,7 +221,7 @@ export function readReviewAuthorityAtRef(cwd, ref, stableSlug, purpose) {
|
|
|
166
221
|
subject,
|
|
167
222
|
events,
|
|
168
223
|
...evaluation,
|
|
169
|
-
issues,
|
|
224
|
+
issues: [...issues, ...orphanIssues],
|
|
170
225
|
};
|
|
171
226
|
}
|
|
172
227
|
export function readWorkingReviewEvents(blueprintPath, stableSlug) {
|
|
@@ -21,6 +21,10 @@ export interface ReviewGateVerification {
|
|
|
21
21
|
readonly logPath?: string;
|
|
22
22
|
readonly sandbox?: ReviewSandboxMode;
|
|
23
23
|
readonly sandboxReason?: string;
|
|
24
|
+
readonly violations?: readonly {
|
|
25
|
+
readonly section: string;
|
|
26
|
+
readonly message: string;
|
|
27
|
+
}[];
|
|
24
28
|
}
|
|
25
29
|
/** Runs one gate command in the review checkout and returns its raw result. */
|
|
26
30
|
export type RunSandboxedGate = (args: {
|
|
@@ -18,7 +18,15 @@ export async function verifyDeliveryPromotionGates(input) {
|
|
|
18
18
|
parsed.violations[0]?.message === "missing Trust Dossier section") {
|
|
19
19
|
return undefined;
|
|
20
20
|
}
|
|
21
|
-
return {
|
|
21
|
+
return {
|
|
22
|
+
status: "failed",
|
|
23
|
+
commands: [],
|
|
24
|
+
failureCode: "malformed-trust-dossier",
|
|
25
|
+
violations: parsed.violations.map((violation) => ({
|
|
26
|
+
section: violation.section,
|
|
27
|
+
message: violation.message,
|
|
28
|
+
})),
|
|
29
|
+
};
|
|
22
30
|
}
|
|
23
31
|
const gates = parsed.dossier?.gates ?? [];
|
|
24
32
|
if (gates.length === 0)
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* the canonical `.agent/{rules,skills}` SSOT always project.
|
|
18
18
|
*
|
|
19
19
|
* The `UNIFIED_CONSUMERS` registry below describes per-IDE projection of the
|
|
20
|
-
* unified rule/skill content kinds (catalog ∪ consumer). Strategies:
|
|
20
|
+
* unified rule/skill/agent content kinds (catalog ∪ consumer). Strategies:
|
|
21
21
|
* - 'symlink': create a relative symlink to the source (file or dir)
|
|
22
22
|
* - 'copy': copy file or recursively copy dir tree
|
|
23
23
|
* - 'transform': run a transform function over the body and write the
|
|
@@ -116,6 +116,8 @@ export declare function unifiedRuleFilename(consumer: UnifiedConsumerConfig, slu
|
|
|
116
116
|
* `selectUnifiedConsumers(hosts)`:
|
|
117
117
|
* - `.agent/{rules,skills}/` (working dir, SSOT): symlink, always
|
|
118
118
|
* - `.claude/rules/`: symlink, always (rules are not plugin-delivered)
|
|
119
|
+
* - `.claude/agents/`: symlink, always (canonical subagents; Claude has no
|
|
120
|
+
* native subagent delivery channel, so this projection is not plugin-owned)
|
|
119
121
|
*
|
|
120
122
|
* Project-local legacy skill roots are intentionally omitted. Codex has no
|
|
121
123
|
* `.codex/agents/` consumer.
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* the canonical `.agent/{rules,skills}` SSOT always project.
|
|
18
18
|
*
|
|
19
19
|
* The `UNIFIED_CONSUMERS` registry below describes per-IDE projection of the
|
|
20
|
-
* unified rule/skill content kinds (catalog ∪ consumer). Strategies:
|
|
20
|
+
* unified rule/skill/agent content kinds (catalog ∪ consumer). Strategies:
|
|
21
21
|
* - 'symlink': create a relative symlink to the source (file or dir)
|
|
22
22
|
* - 'copy': copy file or recursively copy dir tree
|
|
23
23
|
* - 'transform': run a transform function over the body and write the
|
|
@@ -59,6 +59,8 @@ export function unifiedRuleFilename(consumer, slug) {
|
|
|
59
59
|
* `selectUnifiedConsumers(hosts)`:
|
|
60
60
|
* - `.agent/{rules,skills}/` (working dir, SSOT): symlink, always
|
|
61
61
|
* - `.claude/rules/`: symlink, always (rules are not plugin-delivered)
|
|
62
|
+
* - `.claude/agents/`: symlink, always (canonical subagents; Claude has no
|
|
63
|
+
* native subagent delivery channel, so this projection is not plugin-owned)
|
|
62
64
|
*
|
|
63
65
|
* Project-local legacy skill roots are intentionally omitted. Codex has no
|
|
64
66
|
* `.codex/agents/` consumer.
|
|
@@ -69,6 +71,10 @@ export const DEFAULT_UNIFIED_CONSUMERS = [
|
|
|
69
71
|
{ id: "agent-skills", dir: ".agent/skills", acceptsKind: "skill", strategy: "symlink" },
|
|
70
72
|
// Claude: rules are scaffolded to .claude/rules; skills come from native channels.
|
|
71
73
|
{ id: "claude-rules", dir: ".claude/rules", acceptsKind: "rule", strategy: "symlink" },
|
|
74
|
+
// Claude: canonical subagents are scaffolded to .claude/agents. Host-agnostic
|
|
75
|
+
// (unconditional) because subagents were previously scaffolded unconditionally
|
|
76
|
+
// by the now-retired standalone scaffoldSubagents scaffolder.
|
|
77
|
+
{ id: "claude-agents", dir: ".claude/agents", acceptsKind: "agent", strategy: "symlink" },
|
|
72
78
|
];
|
|
73
79
|
/**
|
|
74
80
|
* Projection surfaces retired by a newer agent-kit release.
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Unified rule + skill sync.
|
|
2
|
+
* Unified rule + skill + agent sync.
|
|
3
3
|
*
|
|
4
|
-
* Source of truth: catalog (`<pkg>/dist/catalog/agent/{rules,skills}/`)
|
|
5
|
-
* consumer (`<repo>/agent-{rules,skills}/`). The loader returns a
|
|
4
|
+
* Source of truth: catalog (`<pkg>/dist/catalog/agent/{rules,skills,agents}/`)
|
|
5
|
+
* UNION consumer (`<repo>/agent-{rules,skills,agents}/`). The loader returns a
|
|
6
6
|
* source-tagged record list; this module projects that list into per-IDE
|
|
7
7
|
* surfaces according to `DEFAULT_UNIFIED_CONSUMERS`.
|
|
8
8
|
*
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Unified rule + skill sync.
|
|
2
|
+
* Unified rule + skill + agent sync.
|
|
3
3
|
*
|
|
4
|
-
* Source of truth: catalog (`<pkg>/dist/catalog/agent/{rules,skills}/`)
|
|
5
|
-
* consumer (`<repo>/agent-{rules,skills}/`). The loader returns a
|
|
4
|
+
* Source of truth: catalog (`<pkg>/dist/catalog/agent/{rules,skills,agents}/`)
|
|
5
|
+
* UNION consumer (`<repo>/agent-{rules,skills,agents}/`). The loader returns a
|
|
6
6
|
* source-tagged record list; this module projects that list into per-IDE
|
|
7
7
|
* surfaces according to `DEFAULT_UNIFIED_CONSUMERS`.
|
|
8
8
|
*
|
|
@@ -86,7 +86,9 @@ function planTargets(records, consumers, consumerRoot) {
|
|
|
86
86
|
const consumerDirAbs = join(consumerRoot, consumer.dir);
|
|
87
87
|
let entryName;
|
|
88
88
|
let targetPath;
|
|
89
|
-
if (record.kind === "rule") {
|
|
89
|
+
if (record.kind === "rule" || record.kind === "agent") {
|
|
90
|
+
// agent records are file-shaped like rules (a single <slug>.md),
|
|
91
|
+
// not dir-shaped like skills.
|
|
90
92
|
entryName = unifiedRuleFilename(consumer, record.slug);
|
|
91
93
|
targetPath = join(consumerDirAbs, entryName);
|
|
92
94
|
}
|
|
@@ -248,13 +250,40 @@ function applyTarget(plan, check) {
|
|
|
248
250
|
}
|
|
249
251
|
}
|
|
250
252
|
}
|
|
253
|
+
/**
|
|
254
|
+
* True when `fullPath` is a symlink whose resolved target's directory is one
|
|
255
|
+
* of `managedSourceDirs`. Used to distinguish an agent-kit-managed symlink
|
|
256
|
+
* (safe to prune when stale/renamed) from a real file or a foreign symlink
|
|
257
|
+
* (never agent-kit-managed, must survive `wp sync`). The resolved target
|
|
258
|
+
* itself need not currently exist — a dangling symlink to a since-removed
|
|
259
|
+
* catalog agent still resolves into the managed source dir and is prunable.
|
|
260
|
+
*/
|
|
261
|
+
function isManagedAgentSymlink(fullPath, managedSourceDirs) {
|
|
262
|
+
let stat;
|
|
263
|
+
try {
|
|
264
|
+
stat = lstatSync(fullPath);
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
267
|
+
return false;
|
|
268
|
+
}
|
|
269
|
+
if (!stat.isSymbolicLink())
|
|
270
|
+
return false;
|
|
271
|
+
try {
|
|
272
|
+
const linkTarget = readlinkSync(fullPath);
|
|
273
|
+
const resolved = resolve(dirname(fullPath), linkTarget);
|
|
274
|
+
return managedSourceDirs.has(dirname(resolved));
|
|
275
|
+
}
|
|
276
|
+
catch {
|
|
277
|
+
return false;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
251
280
|
/**
|
|
252
281
|
* Remove entries under each consumer dir that no longer correspond to any
|
|
253
282
|
* planned record. Only removes entries whose shape matches the consumer's
|
|
254
283
|
* managed pattern (rule files: `<slug><ext>`; skill dirs: subdirectories).
|
|
255
284
|
* Real files outside the managed pattern (README.md, .gitkeep) are left alone.
|
|
256
285
|
*/
|
|
257
|
-
function pruneStale(plans, consumers, consumerRoot, check, preserveSkillSlugs) {
|
|
286
|
+
function pruneStale(plans, consumers, consumerRoot, check, preserveSkillSlugs, configuredAgentSourceDirs) {
|
|
258
287
|
const expectedByConsumerDir = new Map();
|
|
259
288
|
for (const plan of plans) {
|
|
260
289
|
const key = plan.consumer.dir;
|
|
@@ -265,6 +294,25 @@ function pruneStale(plans, consumers, consumerRoot, check, preserveSkillSlugs) {
|
|
|
265
294
|
}
|
|
266
295
|
set.add(plan.entryName);
|
|
267
296
|
}
|
|
297
|
+
// Directories that agent-kind sources resolve from (the catalog `agents/`
|
|
298
|
+
// dir, and a consumer `agent-agents/` override dir if present). Unlike
|
|
299
|
+
// `.claude/rules` and `.claude/skills` — surfaces with no legitimate
|
|
300
|
+
// hand-authoring workflow — `.claude/agents` is Claude Code's own native
|
|
301
|
+
// subagent authoring location. A consumer-authored `.claude/agents/foo.md`
|
|
302
|
+
// that was never agent-kit-managed must survive `wp sync`; only symlinks
|
|
303
|
+
// that resolve into a known managed source dir are eligible for pruning.
|
|
304
|
+
//
|
|
305
|
+
// The set unions plan-derived dirs with the CONFIGURED source dirs so it is
|
|
306
|
+
// never empty: with an empty or missing catalog agents dir there are no
|
|
307
|
+
// plans, and a plans-only set would leave nothing provably managed — the
|
|
308
|
+
// sweep must then delete nothing, not everything. Dangling managed symlinks
|
|
309
|
+
// (pointing into a configured dir whose file is gone) stay prunable.
|
|
310
|
+
const agentSourceDirs = new Set([
|
|
311
|
+
...configuredAgentSourceDirs,
|
|
312
|
+
...plans
|
|
313
|
+
.filter((plan) => plan.record.kind === "agent")
|
|
314
|
+
.map((plan) => dirname(plan.record.filePath)),
|
|
315
|
+
]);
|
|
268
316
|
let removed = 0;
|
|
269
317
|
const mismatches = [];
|
|
270
318
|
// Each unique dir gets a single sweep — multiple consumers can share a dir
|
|
@@ -292,15 +340,28 @@ function pruneStale(plans, consumers, consumerRoot, check, preserveSkillSlugs) {
|
|
|
292
340
|
preserveSkillSlugs.has(name) &&
|
|
293
341
|
entry.isDirectory())
|
|
294
342
|
continue;
|
|
295
|
-
// Only prune entries that match a managed shape: rule extension
|
|
296
|
-
// directory (skill).
|
|
297
|
-
const matchesRulePattern = consumersForDir.some((c) => c.acceptsKind === "rule"
|
|
343
|
+
// Only prune entries that match a managed shape: rule/agent extension
|
|
344
|
+
// OR a directory (skill). Agent records are file-shaped like rules.
|
|
345
|
+
const matchesRulePattern = consumersForDir.some((c) => (c.acceptsKind === "rule" || c.acceptsKind === "agent") &&
|
|
346
|
+
name.endsWith(c.ruleExtension ?? ".md"));
|
|
298
347
|
const matchesSkillPattern = consumersForDir.some((c) => c.acceptsKind === "skill") && entry.isDirectory();
|
|
299
348
|
// Also handle stale symlinks of either shape.
|
|
300
349
|
const isLink = entry.isSymbolicLink();
|
|
301
350
|
if (!matchesRulePattern && !matchesSkillPattern && !isLink)
|
|
302
351
|
continue;
|
|
303
352
|
const fullPath = join(dirAbs, name);
|
|
353
|
+
// Conservative prune for agent-only dirs: skip real files and foreign
|
|
354
|
+
// symlinks (anything not resolving into a known agent-kit managed
|
|
355
|
+
// source dir). This guard is unconditional — the source-dir set is
|
|
356
|
+
// configuration-derived and never empty, so a broken install (empty or
|
|
357
|
+
// missing catalog agents dir) cannot degrade the sweep into deleting
|
|
358
|
+
// hand-authored files. Rules/skills dirs have no legitimate
|
|
359
|
+
// hand-authoring workflow, so their existing
|
|
360
|
+
// sweep-everything-unexpected behavior is unchanged.
|
|
361
|
+
const isAgentOnlyDir = consumersForDir.every((c) => c.acceptsKind === "agent");
|
|
362
|
+
if (isAgentOnlyDir && !isManagedAgentSymlink(fullPath, agentSourceDirs)) {
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
304
365
|
if (check) {
|
|
305
366
|
mismatches.push({
|
|
306
367
|
consumerId: consumersForDir[0]?.id ?? dir,
|
|
@@ -348,7 +409,7 @@ function pruneEmptyRetiredConsumerDirs(consumers, consumerRoot, check) {
|
|
|
348
409
|
* Main entrypoint. See module docstring.
|
|
349
410
|
*/
|
|
350
411
|
export function runUnifiedSync(options) {
|
|
351
|
-
const kinds = options.kinds ?? ["rule", "skill"];
|
|
412
|
+
const kinds = options.kinds ?? ["rule", "skill", "agent"];
|
|
352
413
|
const kindSet = new Set(kinds);
|
|
353
414
|
const consumers = (options.consumers ?? selectUnifiedConsumers(options.hosts)).filter((consumer) => kindSet.has(consumer.acceptsKind));
|
|
354
415
|
const activeDirs = new Set(consumers.map((consumer) => consumer.dir));
|
|
@@ -394,10 +455,26 @@ export function runUnifiedSync(options) {
|
|
|
394
455
|
}
|
|
395
456
|
}
|
|
396
457
|
}
|
|
397
|
-
|
|
458
|
+
// Configuration-derived agent source dirs (catalog `agents/` plus the
|
|
459
|
+
// consumer `agent-agents/` override). Added in both raw-resolved and
|
|
460
|
+
// realpath forms so set membership survives the /var → /private/var realm
|
|
461
|
+
// split on macOS. Present even when the dirs are empty or absent — that is
|
|
462
|
+
// the point: prune eligibility must not collapse to "everything" when a
|
|
463
|
+
// broken install yields zero planned agent records.
|
|
464
|
+
const configuredAgentSourceDirs = new Set();
|
|
465
|
+
for (const candidate of [
|
|
466
|
+
join(options.catalogDir, "agents"),
|
|
467
|
+
join(consumerRoot, "agent-agents"),
|
|
468
|
+
]) {
|
|
469
|
+
configuredAgentSourceDirs.add(resolve(candidate));
|
|
470
|
+
if (existsSync(candidate)) {
|
|
471
|
+
configuredAgentSourceDirs.add(realpathSync(candidate));
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
const prune = pruneStale(plans, consumers, consumerRoot, options.check === true, options.preserveSkillSlugs, configuredAgentSourceDirs);
|
|
398
475
|
fixCount += prune.removed;
|
|
399
476
|
mismatches.push(...prune.mismatches);
|
|
400
|
-
const retiredPrune = pruneStale([], retiredConsumers, consumerRoot, options.check === true, undefined);
|
|
477
|
+
const retiredPrune = pruneStale([], retiredConsumers, consumerRoot, options.check === true, undefined, configuredAgentSourceDirs);
|
|
401
478
|
fixCount += retiredPrune.removed;
|
|
402
479
|
mismatches.push(...retiredPrune.mismatches);
|
|
403
480
|
const retiredDirectories = pruneEmptyRetiredConsumerDirs(retiredConsumers, consumerRoot, options.check === true);
|
|
@@ -296,7 +296,6 @@
|
|
|
296
296
|
"src/hooks/dispatch/index.test.ts": 11.771874999999994,
|
|
297
297
|
"src/hooks/shared/hook-bootstrap.test.ts": 58.217375000000004,
|
|
298
298
|
"src/cli/commands/init/scaffolders/agent-hooks/emitters/grok.test.ts": 67.57654199999999,
|
|
299
|
-
"src/cli/commands/init/scaffolders/subagents/index.test.ts": 49.99270800000005,
|
|
300
299
|
"src/blueprint/core/validation/criteria.test.ts": 9.635125000000016,
|
|
301
300
|
"src/cli/commands/session.test.ts": 662.8378339999999,
|
|
302
301
|
"src/cli/commands/dash/run-registry.test.ts": 793.002083,
|