@lousy-agents/mcp 5.22.28 → 5.23.1
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/mcp-server.js +2406 -825
- package/package.json +1 -1
package/dist/mcp-server.js
CHANGED
|
@@ -6559,7 +6559,7 @@ function escapeJsonPtr(str) {
|
|
|
6559
6559
|
|
|
6560
6560
|
|
|
6561
6561
|
},
|
|
6562
|
-
|
|
6562
|
+
8066(__unused_rspack_module, __unused_rspack___webpack_exports__, __webpack_require__) {
|
|
6563
6563
|
// NAMESPACE OBJECT: ../../node_modules/micromark/lib/constructs.js
|
|
6564
6564
|
var constructs_namespaceObject = {};
|
|
6565
6565
|
__webpack_require__.r(constructs_namespaceObject);
|
|
@@ -28463,8 +28463,32 @@ const EMPTY_COMPLETION_RESULT = {
|
|
|
28463
28463
|
}
|
|
28464
28464
|
};
|
|
28465
28465
|
//# sourceMappingURL=mcp.js.map
|
|
28466
|
-
// EXTERNAL MODULE: external "node:fs
|
|
28467
|
-
var
|
|
28466
|
+
// EXTERNAL MODULE: external "node:fs"
|
|
28467
|
+
var external_node_fs_ = __webpack_require__(3024);
|
|
28468
|
+
;// CONCATENATED MODULE: ./src/package-version.ts
|
|
28469
|
+
/**
|
|
28470
|
+
* Reads the version of the installed @lousy-agents/mcp package.
|
|
28471
|
+
*
|
|
28472
|
+
* The release process writes the published version into package.json after
|
|
28473
|
+
* the build, so the version is read at runtime instead of being bundled.
|
|
28474
|
+
* Both src/ (tests) and dist/ (published bundle) sit one level below the
|
|
28475
|
+
* package root.
|
|
28476
|
+
*/
|
|
28477
|
+
|
|
28478
|
+
const PackageManifestSchema = schemas_object({
|
|
28479
|
+
version: schemas_string().min(1)
|
|
28480
|
+
});
|
|
28481
|
+
const UNKNOWN_VERSION = "0.0.0-unknown";
|
|
28482
|
+
const OWN_MANIFEST_URL = new URL("../package.json", import.meta.url);
|
|
28483
|
+
function readPackageVersion(manifestUrl = OWN_MANIFEST_URL) {
|
|
28484
|
+
try {
|
|
28485
|
+
const manifest = JSON.parse((0,external_node_fs_.readFileSync)(manifestUrl, "utf-8"));
|
|
28486
|
+
return PackageManifestSchema.parse(manifest).version;
|
|
28487
|
+
} catch {
|
|
28488
|
+
return UNKNOWN_VERSION;
|
|
28489
|
+
}
|
|
28490
|
+
}
|
|
28491
|
+
|
|
28468
28492
|
// EXTERNAL MODULE: external "node:path"
|
|
28469
28493
|
var external_node_path_ = __webpack_require__(6760);
|
|
28470
28494
|
;// CONCATENATED MODULE: ../core/src/gateways/action-version-gateway.ts
|
|
@@ -28510,6 +28534,8 @@ var external_node_path_ = __webpack_require__(6760);
|
|
|
28510
28534
|
* List of known action names for convenience
|
|
28511
28535
|
*/ const KNOWN_ACTIONS = Object.keys(DEFAULT_ACTION_VERSIONS);
|
|
28512
28536
|
|
|
28537
|
+
// EXTERNAL MODULE: external "node:fs/promises"
|
|
28538
|
+
var promises_ = __webpack_require__(1455);
|
|
28513
28539
|
;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/errors.js
|
|
28514
28540
|
const OPERATIONAL_CODE_VALUES = [
|
|
28515
28541
|
"helper-failed",
|
|
@@ -28545,8 +28571,6 @@ function isNoReplaceUnsupported(error) {
|
|
|
28545
28571
|
error.details?.capability === "rename-noreplace";
|
|
28546
28572
|
}
|
|
28547
28573
|
|
|
28548
|
-
// EXTERNAL MODULE: external "node:fs"
|
|
28549
|
-
var external_node_fs_ = __webpack_require__(3024);
|
|
28550
28574
|
;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/exact-identity.js
|
|
28551
28575
|
|
|
28552
28576
|
|
|
@@ -50180,7 +50204,9 @@ async function lstatIfExists(path) {
|
|
|
50180
50204
|
return false;
|
|
50181
50205
|
}
|
|
50182
50206
|
}
|
|
50183
|
-
|
|
50207
|
+
/**
|
|
50208
|
+
* Returns true when candidatePath is rootPath or a descendant of it.
|
|
50209
|
+
*/ function isPathWithinRoot(rootPath, candidatePath) {
|
|
50184
50210
|
if (candidatePath === rootPath) {
|
|
50185
50211
|
return true;
|
|
50186
50212
|
}
|
|
@@ -53222,9 +53248,16 @@ async function readVersionFileContent(targetDir, relativePath) {
|
|
|
53222
53248
|
* File system implementation of the environment gateway.
|
|
53223
53249
|
*/ class FileSystemEnvironmentGateway {
|
|
53224
53250
|
cwd;
|
|
53225
|
-
config
|
|
53226
|
-
|
|
53251
|
+
config;
|
|
53252
|
+
/**
|
|
53253
|
+
* @param cwd Directory the configuration is loaded from (defaults to
|
|
53254
|
+
* process.cwd()), unless `config` is given
|
|
53255
|
+
* @param config A configuration loaded by the caller. The gateway uses it
|
|
53256
|
+
* as it is and does not read the configuration file itself, so a caller
|
|
53257
|
+
* that also needs the configuration reads the file once for both.
|
|
53258
|
+
*/ constructor(cwd, config){
|
|
53227
53259
|
this.cwd = cwd;
|
|
53260
|
+
this.config = config ?? null;
|
|
53228
53261
|
}
|
|
53229
53262
|
async getConfig() {
|
|
53230
53263
|
if (!this.config) {
|
|
@@ -53359,6 +53392,13 @@ async function readVersionFileContent(targetDir, relativePath) {
|
|
|
53359
53392
|
*/ function createEnvironmentGateway(cwd) {
|
|
53360
53393
|
return new FileSystemEnvironmentGateway(cwd);
|
|
53361
53394
|
}
|
|
53395
|
+
/**
|
|
53396
|
+
* Creates an environment gateway that detects with a configuration the caller
|
|
53397
|
+
* has already loaded, so the configuration file is read once and the gateway
|
|
53398
|
+
* and the caller cannot see two different versions of it.
|
|
53399
|
+
*/ function createEnvironmentGatewayWithConfig(config) {
|
|
53400
|
+
return new FileSystemEnvironmentGateway(undefined, config);
|
|
53401
|
+
}
|
|
53362
53402
|
|
|
53363
53403
|
;// CONCATENATED MODULE: ../core/src/gateways/github-ruleset-gateway.ts
|
|
53364
53404
|
/**
|
|
@@ -53777,37 +53817,1055 @@ function createInitHooksConfigGateway() {
|
|
|
53777
53817
|
return new InitHooksConfigGateway();
|
|
53778
53818
|
}
|
|
53779
53819
|
|
|
53820
|
+
;// CONCATENATED MODULE: ../core/src/entities/file-limits.ts
|
|
53821
|
+
/**
|
|
53822
|
+
* The most bytes of an instruction file that lint and doctor read. Import
|
|
53823
|
+
* expansion, the instruction readers and doctor's file reads all use this one
|
|
53824
|
+
* number, and the advisories that tell a user a file was too large describe it
|
|
53825
|
+
* with {@link describeByteLimit}, so the cap and its text cannot drift.
|
|
53826
|
+
*/ const MAX_INSTRUCTION_FILE_BYTES = 1_048_576;
|
|
53827
|
+
const BYTES_PER_MIB = 1_048_576;
|
|
53828
|
+
/**
|
|
53829
|
+
* A byte limit in words a reader can check a file against: a whole number of
|
|
53830
|
+
* mebibytes ("1 MiB"), or the exact number of bytes, never a rounded figure.
|
|
53831
|
+
*/ function describeByteLimit(bytes) {
|
|
53832
|
+
return bytes % BYTES_PER_MIB === 0 ? `${bytes / BYTES_PER_MIB} MiB` : `${bytes} bytes`;
|
|
53833
|
+
}
|
|
53834
|
+
|
|
53835
|
+
;// CONCATENATED MODULE: ../core/src/entities/lint-rules.ts
|
|
53836
|
+
/**
|
|
53837
|
+
* Lint rule registry entity.
|
|
53838
|
+
* Defines all known lint rule IDs with their default severities, organized by target.
|
|
53839
|
+
*/ /** Valid severity values for rule configuration */ /** Reported when an instruction file could not be read or parsed. */ const INSTRUCTION_PARSE_ERROR_RULE_ID = "instruction/parse-error";
|
|
53840
|
+
/** Reported when instruction discovery stopped at one of its limits. */ const DISCOVERY_LIMIT_REACHED_RULE_ID = "instruction/discovery-limit-reached";
|
|
53841
|
+
/** Reported when instruction discovery skipped symbolic links that lead outside the repository. */ const DISCOVERY_SYMLINK_SKIPPED_RULE_ID = "instruction/discovery-symlink-skipped";
|
|
53842
|
+
/** Default severity levels for all known lint rules */ const DEFAULT_LINT_RULES = (/* unused pure expression or super */ null && ({
|
|
53843
|
+
agents: {
|
|
53844
|
+
"agent/missing-frontmatter": "error",
|
|
53845
|
+
"agent/invalid-frontmatter": "error",
|
|
53846
|
+
"agent/missing-name": "error",
|
|
53847
|
+
"agent/invalid-name-format": "error",
|
|
53848
|
+
"agent/name-mismatch": "error",
|
|
53849
|
+
"agent/missing-description": "error",
|
|
53850
|
+
"agent/invalid-description": "error",
|
|
53851
|
+
"agent/invalid-field": "warn"
|
|
53852
|
+
},
|
|
53853
|
+
subagents: {
|
|
53854
|
+
"subagent/missing-frontmatter": "error",
|
|
53855
|
+
"subagent/invalid-frontmatter": "error",
|
|
53856
|
+
"subagent/missing-name": "error",
|
|
53857
|
+
// Claude Code's documented subagent contract requires only `name`
|
|
53858
|
+
// and `description`; it neither specifies a name format beyond
|
|
53859
|
+
// "unique identifier" nor ties `name` to the filename the way
|
|
53860
|
+
// VS Code derives a Copilot custom agent's name from
|
|
53861
|
+
// `<name>.agent.md`. Defaulting these to "warn" avoids failing
|
|
53862
|
+
// lint for subagents that are valid per Claude Code's own docs.
|
|
53863
|
+
"subagent/invalid-name-format": "warn",
|
|
53864
|
+
"subagent/name-mismatch": "warn",
|
|
53865
|
+
"subagent/missing-description": "error",
|
|
53866
|
+
"subagent/invalid-description": "error",
|
|
53867
|
+
"subagent/invalid-field": "warn"
|
|
53868
|
+
},
|
|
53869
|
+
mcpServers: {
|
|
53870
|
+
"mcpserver/invalid-json": "error",
|
|
53871
|
+
"mcpserver/invalid-config": "error"
|
|
53872
|
+
},
|
|
53873
|
+
hooks: {
|
|
53874
|
+
"hook/invalid-json": "error",
|
|
53875
|
+
"hook/invalid-config": "error",
|
|
53876
|
+
"hook/missing-command": "error",
|
|
53877
|
+
"hook/unknown-event": "error",
|
|
53878
|
+
"hook/missing-matcher": "warn",
|
|
53879
|
+
"hook/missing-timeout": "warn"
|
|
53880
|
+
},
|
|
53881
|
+
instructions: {
|
|
53882
|
+
[INSTRUCTION_PARSE_ERROR_RULE_ID]: "warn",
|
|
53883
|
+
"instruction/command-not-in-code-block": "warn",
|
|
53884
|
+
"instruction/command-outside-section": "warn",
|
|
53885
|
+
"instruction/missing-error-handling": "warn",
|
|
53886
|
+
"instruction/missing-structural-heading": "warn",
|
|
53887
|
+
"instruction/import-unresolved": "warn",
|
|
53888
|
+
"instruction/import-escape": "warn",
|
|
53889
|
+
"instruction/import-symlink": "warn",
|
|
53890
|
+
"instruction/import-cycle": "warn",
|
|
53891
|
+
"instruction/import-depth-exceeded": "warn",
|
|
53892
|
+
"instruction/import-size-exceeded": "warn",
|
|
53893
|
+
[DISCOVERY_LIMIT_REACHED_RULE_ID]: "warn",
|
|
53894
|
+
[DISCOVERY_SYMLINK_SKIPPED_RULE_ID]: "warn"
|
|
53895
|
+
},
|
|
53896
|
+
skills: {
|
|
53897
|
+
"skill/invalid-frontmatter": "error",
|
|
53898
|
+
"skill/missing-frontmatter": "error",
|
|
53899
|
+
"skill/missing-name": "error",
|
|
53900
|
+
"skill/invalid-name-format": "error",
|
|
53901
|
+
"skill/name-mismatch": "error",
|
|
53902
|
+
"skill/missing-description": "error",
|
|
53903
|
+
"skill/invalid-description": "error",
|
|
53904
|
+
"skill/missing-allowed-tools": "warn",
|
|
53905
|
+
"skill/missing-argument-hint": "warn"
|
|
53906
|
+
}
|
|
53907
|
+
}));
|
|
53908
|
+
const RUN_LEVEL_RULE_IDS = new Set([
|
|
53909
|
+
DISCOVERY_LIMIT_REACHED_RULE_ID,
|
|
53910
|
+
DISCOVERY_SYMLINK_SKIPPED_RULE_ID
|
|
53911
|
+
]);
|
|
53912
|
+
/**
|
|
53913
|
+
* Whether a rule's diagnostics describe the whole run rather than a place in
|
|
53914
|
+
* one file, so a reporter that filters by changed file or line (reviewdog's
|
|
53915
|
+
* diff filter) would drop them. Reporters surface these some other way. A
|
|
53916
|
+
* predicate rather than the set itself, so a caller cannot change which rules
|
|
53917
|
+
* count.
|
|
53918
|
+
*/ function isRunLevelRuleId(ruleId) {
|
|
53919
|
+
return ruleId !== undefined && RUN_LEVEL_RULE_IDS.has(ruleId);
|
|
53920
|
+
}
|
|
53921
|
+
|
|
53922
|
+
;// CONCATENATED MODULE: ../core/src/entities/discovery-limit-message.ts
|
|
53923
|
+
/**
|
|
53924
|
+
* The one wording for a directory walk that stopped at a limit, shared by lint
|
|
53925
|
+
* (instruction/discovery-limit-reached) and doctor (discovery-limit-reached)
|
|
53926
|
+
* so the two cannot describe the same limits differently. Each caller says
|
|
53927
|
+
* what it was looking for and what the user should take from the gap.
|
|
53928
|
+
*/ /** The limits a walk reached; a limit it did not reach is left out. */ /** One sentence for each limit that was reached: directories, depth, entries. */ function describeDiscoveryLimits(reached, { subject, missing, consequence }) {
|
|
53929
|
+
const sentences = [];
|
|
53930
|
+
if (reached.maxDirectories !== undefined) {
|
|
53931
|
+
sentences.push(`${subject} stopped after listing ${reached.maxDirectories} directories, so ${missing} in the remaining directories ${consequence}.`);
|
|
53932
|
+
}
|
|
53933
|
+
if (reached.maxDepth !== undefined) {
|
|
53934
|
+
sentences.push(`${subject} did not go more than ${reached.maxDepth} directories deep, so ${missing} in deeper directories ${consequence}.`);
|
|
53935
|
+
}
|
|
53936
|
+
if (reached.maxEntries !== undefined) {
|
|
53937
|
+
sentences.push(`${subject} stopped after examining ${reached.maxEntries} directory entries, so ${missing} in the remaining entries ${consequence}.`);
|
|
53938
|
+
}
|
|
53939
|
+
return sentences;
|
|
53940
|
+
}
|
|
53941
|
+
/** How many paths a message names before it counts the rest. */ const NAMED_PATHS = 5;
|
|
53942
|
+
/**
|
|
53943
|
+
* The first few paths joined with commas, and a count of the rest, so a
|
|
53944
|
+
* message about a fan-out stays readable: "a, b, c, d, e, and 3 more".
|
|
53945
|
+
*/ function listPaths(paths) {
|
|
53946
|
+
const listed = paths.slice(0, NAMED_PATHS).join(", ");
|
|
53947
|
+
const unlisted = paths.length - NAMED_PATHS;
|
|
53948
|
+
return unlisted > 0 ? `${listed}, and ${unlisted} more` : listed;
|
|
53949
|
+
}
|
|
53950
|
+
/**
|
|
53951
|
+
* The message for instruction file discovery skipping symbolic links that
|
|
53952
|
+
* point outside the repository, so their targets were not analyzed.
|
|
53953
|
+
*/ function describeSkippedSymlinks(paths) {
|
|
53954
|
+
return `Instruction file discovery skipped ${paths.length} symbolic link(s) that point outside the repository, so their targets were not analyzed: ${listPaths(paths)}. Replace a link with a file inside the repository to have it analyzed.`;
|
|
53955
|
+
}
|
|
53956
|
+
|
|
53957
|
+
;// CONCATENATED MODULE: ../core/src/entities/agentic-location-catalog.ts
|
|
53958
|
+
/**
|
|
53959
|
+
* Canonical catalog of agentic construct locations shared by lint and doctor.
|
|
53960
|
+
* Pure data only — no filesystem I/O.
|
|
53961
|
+
*/ const agentic_location_catalog_AGENTIC_LOCATION_CATALOG = [
|
|
53962
|
+
// Skill lint roots
|
|
53963
|
+
{
|
|
53964
|
+
id: "skill-github",
|
|
53965
|
+
path: ".github/skills",
|
|
53966
|
+
matchKind: "directory-prefix",
|
|
53967
|
+
primaryConstruct: "skill",
|
|
53968
|
+
lintTarget: "skills",
|
|
53969
|
+
harnessHints: [
|
|
53970
|
+
"copilot"
|
|
53971
|
+
]
|
|
53972
|
+
},
|
|
53973
|
+
{
|
|
53974
|
+
id: "skill-claude",
|
|
53975
|
+
path: ".claude/skills",
|
|
53976
|
+
matchKind: "directory-prefix",
|
|
53977
|
+
primaryConstruct: "skill",
|
|
53978
|
+
lintTarget: "skills",
|
|
53979
|
+
harnessHints: [
|
|
53980
|
+
"claude"
|
|
53981
|
+
]
|
|
53982
|
+
},
|
|
53983
|
+
{
|
|
53984
|
+
id: "skill-agents",
|
|
53985
|
+
path: ".agents/skills",
|
|
53986
|
+
matchKind: "directory-prefix",
|
|
53987
|
+
primaryConstruct: "skill",
|
|
53988
|
+
lintTarget: "skills",
|
|
53989
|
+
harnessHints: [
|
|
53990
|
+
"shared"
|
|
53991
|
+
]
|
|
53992
|
+
},
|
|
53993
|
+
// Additional skill roots (SKILL.md convention, no alternate layout documented)
|
|
53994
|
+
{
|
|
53995
|
+
id: "skill-pi",
|
|
53996
|
+
path: ".pi/skills",
|
|
53997
|
+
matchKind: "directory-prefix",
|
|
53998
|
+
primaryConstruct: "skill",
|
|
53999
|
+
lintTarget: "skills",
|
|
54000
|
+
harnessHints: [
|
|
54001
|
+
"pi"
|
|
54002
|
+
]
|
|
54003
|
+
},
|
|
54004
|
+
{
|
|
54005
|
+
id: "skill-pi-prompts",
|
|
54006
|
+
path: ".pi/prompts",
|
|
54007
|
+
matchKind: "directory-prefix",
|
|
54008
|
+
primaryConstruct: "skill",
|
|
54009
|
+
lintTarget: "skills",
|
|
54010
|
+
harnessHints: [
|
|
54011
|
+
"pi"
|
|
54012
|
+
]
|
|
54013
|
+
},
|
|
54014
|
+
// Agent lint root (dual-role: agent + instruction)
|
|
54015
|
+
{
|
|
54016
|
+
id: "agent-github",
|
|
54017
|
+
path: ".github/agents",
|
|
54018
|
+
matchKind: "directory-prefix",
|
|
54019
|
+
primaryConstruct: "agent",
|
|
54020
|
+
secondaryConstructs: [
|
|
54021
|
+
"instruction"
|
|
54022
|
+
],
|
|
54023
|
+
lintTarget: "agents",
|
|
54024
|
+
harnessHints: [
|
|
54025
|
+
"copilot"
|
|
54026
|
+
],
|
|
54027
|
+
instructionFormat: "copilot-agent"
|
|
54028
|
+
},
|
|
54029
|
+
// Hook lint exact configs
|
|
54030
|
+
{
|
|
54031
|
+
id: "hook-copilot-agent-shell",
|
|
54032
|
+
path: ".github/hooks/agent-shell/hooks.json",
|
|
54033
|
+
matchKind: "exact",
|
|
54034
|
+
primaryConstruct: "hook",
|
|
54035
|
+
lintTarget: "hooks",
|
|
54036
|
+
harnessHints: [
|
|
54037
|
+
"copilot"
|
|
54038
|
+
],
|
|
54039
|
+
hookPlatform: "copilot"
|
|
54040
|
+
},
|
|
54041
|
+
{
|
|
54042
|
+
id: "hook-claude-settings",
|
|
54043
|
+
path: ".claude/settings.json",
|
|
54044
|
+
matchKind: "exact",
|
|
54045
|
+
primaryConstruct: "hook",
|
|
54046
|
+
lintTarget: "hooks",
|
|
54047
|
+
harnessHints: [
|
|
54048
|
+
"claude"
|
|
54049
|
+
],
|
|
54050
|
+
hookPlatform: "claude"
|
|
54051
|
+
},
|
|
54052
|
+
{
|
|
54053
|
+
id: "hook-claude-settings-local",
|
|
54054
|
+
path: ".claude/settings.local.json",
|
|
54055
|
+
matchKind: "exact",
|
|
54056
|
+
primaryConstruct: "hook",
|
|
54057
|
+
lintTarget: "hooks",
|
|
54058
|
+
harnessHints: [
|
|
54059
|
+
"claude"
|
|
54060
|
+
],
|
|
54061
|
+
hookPlatform: "claude"
|
|
54062
|
+
},
|
|
54063
|
+
// Doctor-only hook directory prefixes
|
|
54064
|
+
{
|
|
54065
|
+
id: "hook-github-dir",
|
|
54066
|
+
path: ".github/hooks",
|
|
54067
|
+
matchKind: "directory-prefix",
|
|
54068
|
+
primaryConstruct: "hook",
|
|
54069
|
+
lintTarget: "none",
|
|
54070
|
+
harnessHints: [
|
|
54071
|
+
"copilot"
|
|
54072
|
+
]
|
|
54073
|
+
},
|
|
54074
|
+
{
|
|
54075
|
+
id: "hook-claude-dir",
|
|
54076
|
+
path: ".claude/hooks",
|
|
54077
|
+
matchKind: "directory-prefix",
|
|
54078
|
+
primaryConstruct: "hook",
|
|
54079
|
+
lintTarget: "none",
|
|
54080
|
+
harnessHints: [
|
|
54081
|
+
"claude"
|
|
54082
|
+
]
|
|
54083
|
+
},
|
|
54084
|
+
// Instruction lint locations
|
|
54085
|
+
{
|
|
54086
|
+
id: "instruction-copilot-root",
|
|
54087
|
+
path: ".github/copilot-instructions.md",
|
|
54088
|
+
matchKind: "exact",
|
|
54089
|
+
primaryConstruct: "instruction",
|
|
54090
|
+
lintTarget: "instructions",
|
|
54091
|
+
harnessHints: [
|
|
54092
|
+
"copilot"
|
|
54093
|
+
],
|
|
54094
|
+
instructionFormat: "copilot-instructions"
|
|
54095
|
+
},
|
|
54096
|
+
{
|
|
54097
|
+
id: "instruction-copilot-scoped",
|
|
54098
|
+
path: ".github/instructions",
|
|
54099
|
+
matchKind: "directory-prefix",
|
|
54100
|
+
primaryConstruct: "instruction",
|
|
54101
|
+
lintTarget: "instructions",
|
|
54102
|
+
harnessHints: [
|
|
54103
|
+
"copilot"
|
|
54104
|
+
],
|
|
54105
|
+
instructionFormat: "copilot-scoped",
|
|
54106
|
+
includeSubdirectories: true,
|
|
54107
|
+
fileSuffix: ".instructions.md"
|
|
54108
|
+
},
|
|
54109
|
+
{
|
|
54110
|
+
id: "instruction-agents-md",
|
|
54111
|
+
path: "AGENTS.md",
|
|
54112
|
+
matchKind: "exact",
|
|
54113
|
+
primaryConstruct: "instruction",
|
|
54114
|
+
lintTarget: "instructions",
|
|
54115
|
+
harnessHints: [
|
|
54116
|
+
"shared"
|
|
54117
|
+
],
|
|
54118
|
+
instructionFormat: "agents-md"
|
|
54119
|
+
},
|
|
54120
|
+
{
|
|
54121
|
+
id: "instruction-agents-override-md",
|
|
54122
|
+
path: "AGENTS.override.md",
|
|
54123
|
+
matchKind: "exact",
|
|
54124
|
+
primaryConstruct: "instruction",
|
|
54125
|
+
lintTarget: "instructions",
|
|
54126
|
+
harnessHints: [
|
|
54127
|
+
"codex"
|
|
54128
|
+
],
|
|
54129
|
+
instructionFormat: "agents-md"
|
|
54130
|
+
},
|
|
54131
|
+
{
|
|
54132
|
+
id: "instruction-claude-md",
|
|
54133
|
+
path: "CLAUDE.md",
|
|
54134
|
+
matchKind: "exact",
|
|
54135
|
+
primaryConstruct: "instruction",
|
|
54136
|
+
lintTarget: "instructions",
|
|
54137
|
+
harnessHints: [
|
|
54138
|
+
"claude"
|
|
54139
|
+
],
|
|
54140
|
+
instructionFormat: "claude-md"
|
|
54141
|
+
},
|
|
54142
|
+
{
|
|
54143
|
+
id: "instruction-claude-dir-md",
|
|
54144
|
+
path: ".claude/CLAUDE.md",
|
|
54145
|
+
matchKind: "exact",
|
|
54146
|
+
primaryConstruct: "instruction",
|
|
54147
|
+
lintTarget: "instructions",
|
|
54148
|
+
harnessHints: [
|
|
54149
|
+
"claude"
|
|
54150
|
+
],
|
|
54151
|
+
instructionFormat: "claude-md"
|
|
54152
|
+
},
|
|
54153
|
+
// Subagent lint root
|
|
54154
|
+
{
|
|
54155
|
+
id: "subagent-claude",
|
|
54156
|
+
path: ".claude/agents",
|
|
54157
|
+
matchKind: "directory-prefix",
|
|
54158
|
+
primaryConstruct: "subagent",
|
|
54159
|
+
lintTarget: "subagents",
|
|
54160
|
+
harnessHints: [
|
|
54161
|
+
"claude"
|
|
54162
|
+
]
|
|
54163
|
+
},
|
|
54164
|
+
{
|
|
54165
|
+
id: "agent-claude-commands",
|
|
54166
|
+
path: ".claude/commands",
|
|
54167
|
+
matchKind: "directory-prefix",
|
|
54168
|
+
primaryConstruct: "agent",
|
|
54169
|
+
lintTarget: "none",
|
|
54170
|
+
harnessHints: [
|
|
54171
|
+
"claude"
|
|
54172
|
+
]
|
|
54173
|
+
},
|
|
54174
|
+
{
|
|
54175
|
+
id: "plugin-codex",
|
|
54176
|
+
path: ".codex-plugin",
|
|
54177
|
+
matchKind: "directory-prefix",
|
|
54178
|
+
primaryConstruct: "plugin",
|
|
54179
|
+
lintTarget: "none",
|
|
54180
|
+
harnessHints: [
|
|
54181
|
+
"codex"
|
|
54182
|
+
]
|
|
54183
|
+
}
|
|
54184
|
+
];
|
|
54185
|
+
|
|
54186
|
+
;// CONCATENATED MODULE: ../core/src/lib/agentic-location-matchers.ts
|
|
54187
|
+
/**
|
|
54188
|
+
* Pure path→construct matching helpers over the agentic location catalog.
|
|
54189
|
+
* No filesystem I/O.
|
|
54190
|
+
*/
|
|
54191
|
+
/**
|
|
54192
|
+
* Normalizes a repo-relative path for catalog matching.
|
|
54193
|
+
* Collapses `.` / `..` segments. Returns "" when the path is absolute or
|
|
54194
|
+
* escapes the repository root — callers treat "" as non-matching.
|
|
54195
|
+
*/ function isAbsoluteRepoPath(normalized) {
|
|
54196
|
+
// POSIX absolute, UNC-style leading slash after backslash conversion,
|
|
54197
|
+
// and Windows drive-letter absolute (C:/..., d:\...).
|
|
54198
|
+
return normalized.startsWith("/") || /^[A-Za-z]:(\/|$)/.test(normalized);
|
|
54199
|
+
}
|
|
54200
|
+
function normalizeRepoRelativePath(path) {
|
|
54201
|
+
const normalized = path.replaceAll("\\", "/");
|
|
54202
|
+
if (isAbsoluteRepoPath(normalized)) {
|
|
54203
|
+
return "";
|
|
54204
|
+
}
|
|
54205
|
+
const resolved = [];
|
|
54206
|
+
for (const segment of normalized.split("/")){
|
|
54207
|
+
if (segment === "" || segment === ".") {
|
|
54208
|
+
continue;
|
|
54209
|
+
}
|
|
54210
|
+
if (segment === ".." && resolved.length === 0) {
|
|
54211
|
+
return "";
|
|
54212
|
+
}
|
|
54213
|
+
if (segment === "..") {
|
|
54214
|
+
resolved.pop();
|
|
54215
|
+
continue;
|
|
54216
|
+
}
|
|
54217
|
+
resolved.push(segment);
|
|
54218
|
+
}
|
|
54219
|
+
return resolved.join("/");
|
|
54220
|
+
}
|
|
54221
|
+
function matchesLocationEntry(path, entry) {
|
|
54222
|
+
const normalized = normalizeRepoRelativePath(path);
|
|
54223
|
+
if (entry.matchKind === "exact") {
|
|
54224
|
+
return normalized === entry.path;
|
|
54225
|
+
}
|
|
54226
|
+
return normalized === entry.path || normalized.startsWith(`${entry.path}/`) && (entry.fileSuffix === undefined || normalized.endsWith(entry.fileSuffix));
|
|
54227
|
+
}
|
|
54228
|
+
function entriesMatchingPath(path, catalog = AGENTIC_LOCATION_CATALOG) {
|
|
54229
|
+
return catalog.filter((entry)=>matchesLocationEntry(path, entry));
|
|
54230
|
+
}
|
|
54231
|
+
/**
|
|
54232
|
+
* True when the path sits under a catalog location that only accepts files
|
|
54233
|
+
* with a given suffix, the path lacks that suffix, and no other entry claims
|
|
54234
|
+
* it. The harness that owns that location does not load such a file (Copilot
|
|
54235
|
+
* loads only `*.instructions.md` from `.github/instructions`), so a consumer
|
|
54236
|
+
* that finds the file by directory prefix alone should leave it out.
|
|
54237
|
+
*/ function isExcludedBySuffix(path, catalog = AGENTIC_LOCATION_CATALOG) {
|
|
54238
|
+
const normalized = normalizeRepoRelativePath(path);
|
|
54239
|
+
return entriesMatchingPath(path, catalog).length === 0 && catalog.some((entry)=>entry.matchKind === "directory-prefix" && entry.fileSuffix !== undefined && normalized.startsWith(`${entry.path}/`));
|
|
54240
|
+
}
|
|
54241
|
+
function specificityScore(entry) {
|
|
54242
|
+
// Exact matches outrank any prefix; longer prefixes outrank shorter ones.
|
|
54243
|
+
const kindBonus = entry.matchKind === "exact" ? 1_000_000 : 0;
|
|
54244
|
+
return kindBonus + entry.path.length;
|
|
54245
|
+
}
|
|
54246
|
+
function bestMatchingEntry(path, catalog = AGENTIC_LOCATION_CATALOG) {
|
|
54247
|
+
const matches = entriesMatchingPath(path, catalog);
|
|
54248
|
+
if (matches.length === 0) {
|
|
54249
|
+
return null;
|
|
54250
|
+
}
|
|
54251
|
+
let best = matches[0];
|
|
54252
|
+
let bestScore = specificityScore(best);
|
|
54253
|
+
for(let i = 1; i < matches.length; i++){
|
|
54254
|
+
const candidate = matches[i];
|
|
54255
|
+
const score = specificityScore(candidate);
|
|
54256
|
+
if (score > bestScore) {
|
|
54257
|
+
best = candidate;
|
|
54258
|
+
bestScore = score;
|
|
54259
|
+
}
|
|
54260
|
+
}
|
|
54261
|
+
return best;
|
|
54262
|
+
}
|
|
54263
|
+
function primaryConstructTypeForPath(path, catalog = AGENTIC_LOCATION_CATALOG) {
|
|
54264
|
+
return bestMatchingEntry(path, catalog)?.primaryConstruct ?? null;
|
|
54265
|
+
}
|
|
54266
|
+
function constructTypesForPath(path, catalog = AGENTIC_LOCATION_CATALOG) {
|
|
54267
|
+
const best = bestMatchingEntry(path, catalog);
|
|
54268
|
+
if (!best) {
|
|
54269
|
+
return [];
|
|
54270
|
+
}
|
|
54271
|
+
const secondary = best.secondaryConstructs ?? [];
|
|
54272
|
+
if (secondary.length === 0) {
|
|
54273
|
+
return [
|
|
54274
|
+
best.primaryConstruct
|
|
54275
|
+
];
|
|
54276
|
+
}
|
|
54277
|
+
return [
|
|
54278
|
+
best.primaryConstruct,
|
|
54279
|
+
...secondary
|
|
54280
|
+
];
|
|
54281
|
+
}
|
|
54282
|
+
function lintTargetEntries(target, catalog = agentic_location_catalog_AGENTIC_LOCATION_CATALOG) {
|
|
54283
|
+
return catalog.filter((entry)=>entry.lintTarget === target);
|
|
54284
|
+
}
|
|
54285
|
+
function skillDirectoryRoots(catalog = agentic_location_catalog_AGENTIC_LOCATION_CATALOG) {
|
|
54286
|
+
return lintTargetEntries("skills", catalog).filter((entry)=>entry.matchKind === "directory-prefix").map((entry)=>entry.path);
|
|
54287
|
+
}
|
|
54288
|
+
function agentDirectoryRoots(catalog = AGENTIC_LOCATION_CATALOG) {
|
|
54289
|
+
return lintTargetEntries("agents", catalog).filter((entry)=>entry.matchKind === "directory-prefix").map((entry)=>entry.path);
|
|
54290
|
+
}
|
|
54291
|
+
function subagentDirectoryRoots(catalog = AGENTIC_LOCATION_CATALOG) {
|
|
54292
|
+
return lintTargetEntries("subagents", catalog).filter((entry)=>entry.matchKind === "directory-prefix").map((entry)=>entry.path);
|
|
54293
|
+
}
|
|
54294
|
+
function isHookConfigEntry(entry) {
|
|
54295
|
+
return entry.matchKind === "exact" && entry.hookPlatform != null;
|
|
54296
|
+
}
|
|
54297
|
+
function hookConfigPaths(catalog = AGENTIC_LOCATION_CATALOG) {
|
|
54298
|
+
return lintTargetEntries("hooks", catalog).filter(isHookConfigEntry).map((entry)=>({
|
|
54299
|
+
relativePath: entry.path,
|
|
54300
|
+
platform: entry.hookPlatform
|
|
54301
|
+
}));
|
|
54302
|
+
}
|
|
54303
|
+
function instructionDiscoveryEntries(catalog = agentic_location_catalog_AGENTIC_LOCATION_CATALOG) {
|
|
54304
|
+
return catalog.filter((entry)=>entry.instructionFormat != null);
|
|
54305
|
+
}
|
|
54306
|
+
|
|
54307
|
+
;// CONCATENATED MODULE: ../core/src/gateways/real-path-within-root.ts
|
|
54308
|
+
/**
|
|
54309
|
+
* Shared symlink policy for discovering agentic files (instruction files,
|
|
54310
|
+
* doctor scan targets): follow a symlink only when its real target stays
|
|
54311
|
+
* inside the repository.
|
|
54312
|
+
*/
|
|
54313
|
+
|
|
54314
|
+
const nodeInspectionFs = {
|
|
54315
|
+
realpath: promises_.realpath,
|
|
54316
|
+
stat: promises_.stat
|
|
54317
|
+
};
|
|
54318
|
+
/**
|
|
54319
|
+
* The error codes that mean no file is there. A path the system cannot
|
|
54320
|
+
* represent cannot name a file, so it is as missing as one that was never
|
|
54321
|
+
* created: a name too long for the file system to hold, one it rejects as
|
|
54322
|
+
* invalid, or one holding a NUL byte, which Node rejects before it asks the
|
|
54323
|
+
* system at all.
|
|
54324
|
+
*/ const MISSING_ERROR_CODES = [
|
|
54325
|
+
"ENOENT",
|
|
54326
|
+
"ENOTDIR",
|
|
54327
|
+
"ENAMETOOLONG",
|
|
54328
|
+
"EINVAL",
|
|
54329
|
+
"ERR_INVALID_ARG_VALUE"
|
|
54330
|
+
];
|
|
54331
|
+
function isMissingError(error) {
|
|
54332
|
+
return error instanceof Error && "code" in error && typeof error.code === "string" && MISSING_ERROR_CODES.includes(error.code);
|
|
54333
|
+
}
|
|
54334
|
+
const UNREADABLE_ERROR_CODES = [
|
|
54335
|
+
"EMFILE",
|
|
54336
|
+
"ENFILE",
|
|
54337
|
+
"EACCES",
|
|
54338
|
+
"EPERM",
|
|
54339
|
+
"EIO"
|
|
54340
|
+
];
|
|
54341
|
+
function isUnreadableError(error) {
|
|
54342
|
+
return error instanceof Error && "code" in error && typeof error.code === "string" && UNREADABLE_ERROR_CODES.includes(error.code);
|
|
54343
|
+
}
|
|
54344
|
+
function statusForInspectionError(error) {
|
|
54345
|
+
if (isMissingError(error)) {
|
|
54346
|
+
return "missing";
|
|
54347
|
+
}
|
|
54348
|
+
return isUnreadableError(error) ? "unreadable" : "unsupported";
|
|
54349
|
+
}
|
|
54350
|
+
async function kindOutsideRoot(real, fs) {
|
|
54351
|
+
try {
|
|
54352
|
+
const outside = await fs.stat(real);
|
|
54353
|
+
if (outside.isDirectory()) {
|
|
54354
|
+
return "directory";
|
|
54355
|
+
}
|
|
54356
|
+
return outside.isFile() ? "file" : "other";
|
|
54357
|
+
} catch {
|
|
54358
|
+
return "other";
|
|
54359
|
+
}
|
|
54360
|
+
}
|
|
54361
|
+
/**
|
|
54362
|
+
* Follows a path (and any symlink along it) to its real location and reports
|
|
54363
|
+
* why it cannot be used: missing, outside the real root, neither a regular
|
|
54364
|
+
* file nor a directory, or unreadable (the system refused to look at it). Stats the already-resolved real path (rather than
|
|
54365
|
+
* re-resolving the symlink) so the within-root check, the type check and the
|
|
54366
|
+
* recorded identity observe the same target.
|
|
54367
|
+
*
|
|
54368
|
+
* @param rootReal Real (symlink-free) path of the root directory
|
|
54369
|
+
* @param absolutePath Path to resolve, which may contain symlinks
|
|
54370
|
+
* @param fs The file system calls to make; the real ones by default
|
|
54371
|
+
*/ async function inspectPathWithinRoot(rootReal, absolutePath, fs = nodeInspectionFs) {
|
|
54372
|
+
try {
|
|
54373
|
+
const real = await fs.realpath(absolutePath);
|
|
54374
|
+
if (!isPathWithinRoot(rootReal, real)) {
|
|
54375
|
+
return {
|
|
54376
|
+
status: "outside-root",
|
|
54377
|
+
kind: await kindOutsideRoot(real, fs)
|
|
54378
|
+
};
|
|
54379
|
+
}
|
|
54380
|
+
const targetStat = await fs.stat(real);
|
|
54381
|
+
const identity = {
|
|
54382
|
+
dev: targetStat.dev,
|
|
54383
|
+
ino: targetStat.ino
|
|
54384
|
+
};
|
|
54385
|
+
if (targetStat.isDirectory()) {
|
|
54386
|
+
return {
|
|
54387
|
+
status: "ok",
|
|
54388
|
+
kind: "directory",
|
|
54389
|
+
real,
|
|
54390
|
+
identity
|
|
54391
|
+
};
|
|
54392
|
+
}
|
|
54393
|
+
if (targetStat.isFile()) {
|
|
54394
|
+
return {
|
|
54395
|
+
status: "ok",
|
|
54396
|
+
kind: "file",
|
|
54397
|
+
real,
|
|
54398
|
+
identity
|
|
54399
|
+
};
|
|
54400
|
+
}
|
|
54401
|
+
return {
|
|
54402
|
+
status: "unsupported"
|
|
54403
|
+
};
|
|
54404
|
+
} catch (error) {
|
|
54405
|
+
return {
|
|
54406
|
+
status: statusForInspectionError(error)
|
|
54407
|
+
};
|
|
54408
|
+
}
|
|
54409
|
+
}
|
|
54410
|
+
/**
|
|
54411
|
+
* Shared symlink policy for discovering agentic files: a path (or any
|
|
54412
|
+
* symlink along it) is followed only when its real target stays inside the
|
|
54413
|
+
* real root and is a regular file or directory. Callers must still read the
|
|
54414
|
+
* content through the real path with the no-follow readers, which reject a
|
|
54415
|
+
* symlink swapped in after this check.
|
|
54416
|
+
*
|
|
54417
|
+
* @param rootReal Real (symlink-free) path of the root directory
|
|
54418
|
+
* @param absolutePath Path to resolve, which may contain symlinks
|
|
54419
|
+
* @returns The real path and its kind, or null when the target is missing,
|
|
54420
|
+
* outside the root, or not a regular file or directory
|
|
54421
|
+
*/ async function resolveRealPathWithinRoot(rootReal, absolutePath) {
|
|
54422
|
+
const inspection = await inspectPathWithinRoot(rootReal, absolutePath);
|
|
54423
|
+
return inspection.status === "ok" ? {
|
|
54424
|
+
kind: inspection.kind,
|
|
54425
|
+
real: inspection.real
|
|
54426
|
+
} : null;
|
|
54427
|
+
}
|
|
54428
|
+
|
|
54429
|
+
;// CONCATENATED MODULE: ../core/src/entities/code-unit-order.ts
|
|
54430
|
+
/**
|
|
54431
|
+
* Orders strings by UTF-16 code unit.
|
|
54432
|
+
*
|
|
54433
|
+
* Unlike `localeCompare`, the result does not depend on the machine's locale
|
|
54434
|
+
* or ICU data, so anything sorted with it (directory entries, inventory
|
|
54435
|
+
* paths) comes out in the same order on every machine and in CI.
|
|
54436
|
+
*/ function compareCodeUnits(a, b) {
|
|
54437
|
+
if (a < b) {
|
|
54438
|
+
return -1;
|
|
54439
|
+
}
|
|
54440
|
+
return a > b ? 1 : 0;
|
|
54441
|
+
}
|
|
54442
|
+
|
|
54443
|
+
;// CONCATENATED MODULE: ../core/src/gateways/repository-walk.ts
|
|
54444
|
+
/**
|
|
54445
|
+
* The shared, bounded walk over a repository tree, used by instruction
|
|
54446
|
+
* discovery (lint, MCP) and by doctor so the two cannot disagree about how a
|
|
54447
|
+
* tree is traversed.
|
|
54448
|
+
*
|
|
54449
|
+
* The walk follows symlinks only while their real target stays inside the
|
|
54450
|
+
* repository (the shared policy in real-path-within-root), terminates on
|
|
54451
|
+
* symlink cycles, lists entries in name order so the result does not depend
|
|
54452
|
+
* on the file system, and is bounded in depth and in the number of
|
|
54453
|
+
* directories it lists.
|
|
54454
|
+
*/
|
|
54455
|
+
|
|
54456
|
+
|
|
54457
|
+
|
|
54458
|
+
|
|
54459
|
+
/**
|
|
54460
|
+
* The deepest level whose directories are listed (the start directory is 0).
|
|
54461
|
+
* A directory at this depth is listed, and its own subdirectories are not
|
|
54462
|
+
* entered.
|
|
54463
|
+
*/ const DEFAULT_MAX_WALK_DEPTH = 10;
|
|
54464
|
+
/** Directories listed before the walk stops descending. */ const DEFAULT_MAX_WALK_DIRECTORIES = 10_000;
|
|
54465
|
+
/**
|
|
54466
|
+
* Directory entries examined before the walk stops. The directory cap counts
|
|
54467
|
+
* listings, not the entries in them, and a fan-out of directory aliases lists
|
|
54468
|
+
* a wide directory many times over, so this bounds the work itself.
|
|
54469
|
+
*/ const DEFAULT_MAX_WALK_ENTRIES = 200_000;
|
|
54470
|
+
const DEFAULT_SKIPPED_DIRECTORY_NAMES = [
|
|
54471
|
+
"node_modules",
|
|
54472
|
+
".git"
|
|
54473
|
+
];
|
|
54474
|
+
const listWithinRoot = async (rootReal, relativePath)=>{
|
|
54475
|
+
try {
|
|
54476
|
+
return await file_system_utils_listDirectoryWithinRoot(rootReal, relativePath);
|
|
54477
|
+
} catch {
|
|
54478
|
+
return [];
|
|
54479
|
+
}
|
|
54480
|
+
};
|
|
54481
|
+
function resolveSettings(options) {
|
|
54482
|
+
return {
|
|
54483
|
+
startRelativePath: options.startRelativePath,
|
|
54484
|
+
includeFile: options.includeFile,
|
|
54485
|
+
includeSubdirectories: options.includeSubdirectories ?? true,
|
|
54486
|
+
revisit: options.revisit ?? "once-per-real-directory",
|
|
54487
|
+
maxDepth: options.maxDepth ?? DEFAULT_MAX_WALK_DEPTH,
|
|
54488
|
+
maxDirectories: options.maxDirectories ?? DEFAULT_MAX_WALK_DIRECTORIES,
|
|
54489
|
+
maxEntries: options.maxEntries ?? DEFAULT_MAX_WALK_ENTRIES,
|
|
54490
|
+
skipDirectoryNames: new Set(options.skipDirectoryNames ?? DEFAULT_SKIPPED_DIRECTORY_NAMES),
|
|
54491
|
+
listDirectory: options.listDirectory ?? listWithinRoot
|
|
54492
|
+
};
|
|
54493
|
+
}
|
|
54494
|
+
/**
|
|
54495
|
+
* Whether a directory entry name could escape its directory: a name that is
|
|
54496
|
+
* the parent or current directory, or that holds a path separator. A name
|
|
54497
|
+
* with two dots inside it, such as `review..strict.md`, is an ordinary name.
|
|
54498
|
+
*/ function isUnsafeEntryName(name) {
|
|
54499
|
+
return name === "." || name === ".." || name.includes("/") || name.includes("\\");
|
|
54500
|
+
}
|
|
54501
|
+
/** Joins a repository-relative directory and an entry without a leading "/". */ function joinRelative(directory, name) {
|
|
54502
|
+
return directory === "" ? name : `${directory}/${name}`;
|
|
54503
|
+
}
|
|
54504
|
+
function segmentsOf(relativePath) {
|
|
54505
|
+
return relativePath.split("/").filter((segment)=>segment !== "");
|
|
54506
|
+
}
|
|
54507
|
+
/**
|
|
54508
|
+
* Owns the bookkeeping of one walk. State lives on the instance, not in
|
|
54509
|
+
* parameters, so a walk cannot leak into another.
|
|
54510
|
+
*/ class RepositoryWalk {
|
|
54511
|
+
targetDir;
|
|
54512
|
+
rootReal;
|
|
54513
|
+
settings;
|
|
54514
|
+
files = [];
|
|
54515
|
+
visitedRealDirectories = new Set();
|
|
54516
|
+
listedDirectories = 0;
|
|
54517
|
+
limitReached = false;
|
|
54518
|
+
depthLimitReached = false;
|
|
54519
|
+
entriesExamined = 0;
|
|
54520
|
+
entryLimitReached = false;
|
|
54521
|
+
skippedOutsideRoot = [];
|
|
54522
|
+
constructor(targetDir, rootReal, settings){
|
|
54523
|
+
this.targetDir = targetDir;
|
|
54524
|
+
this.rootReal = rootReal;
|
|
54525
|
+
this.settings = settings;
|
|
54526
|
+
}
|
|
54527
|
+
async run() {
|
|
54528
|
+
await this.walkDirectory(this.settings.startRelativePath, 0, []);
|
|
54529
|
+
return {
|
|
54530
|
+
files: this.files,
|
|
54531
|
+
directoryLimitReached: this.limitReached,
|
|
54532
|
+
depthLimitReached: this.depthLimitReached,
|
|
54533
|
+
entryLimitReached: this.entryLimitReached,
|
|
54534
|
+
skippedOutsideRoot: this.skippedOutsideRoot,
|
|
54535
|
+
limits: {
|
|
54536
|
+
maxDirectories: this.settings.maxDirectories,
|
|
54537
|
+
maxDepth: this.settings.maxDepth,
|
|
54538
|
+
maxEntries: this.settings.maxEntries
|
|
54539
|
+
}
|
|
54540
|
+
};
|
|
54541
|
+
}
|
|
54542
|
+
isRevisit(real, ancestors) {
|
|
54543
|
+
return this.settings.revisit === "refuse-ancestors" ? ancestors.includes(real) : this.visitedRealDirectories.has(real);
|
|
54544
|
+
}
|
|
54545
|
+
async walkDirectory(directoryRelativePath, depth, ancestors) {
|
|
54546
|
+
if (this.limitReached) {
|
|
54547
|
+
return;
|
|
54548
|
+
}
|
|
54549
|
+
const directory = await inspectPathWithinRoot(this.rootReal, (0,external_node_path_.join)(this.targetDir, ...segmentsOf(directoryRelativePath)));
|
|
54550
|
+
if (directory.status === "outside-root" && directory.kind === "directory") {
|
|
54551
|
+
this.skippedOutsideRoot.push(directoryRelativePath);
|
|
54552
|
+
return;
|
|
54553
|
+
}
|
|
54554
|
+
if (directory.status !== "ok" || directory.kind !== "directory") {
|
|
54555
|
+
return;
|
|
54556
|
+
}
|
|
54557
|
+
if (this.isRevisit(directory.real, ancestors)) {
|
|
54558
|
+
return;
|
|
54559
|
+
}
|
|
54560
|
+
if (this.listedDirectories >= this.settings.maxDirectories) {
|
|
54561
|
+
this.limitReached = true;
|
|
54562
|
+
return;
|
|
54563
|
+
}
|
|
54564
|
+
this.listedDirectories += 1;
|
|
54565
|
+
this.visitedRealDirectories.add(directory.real);
|
|
54566
|
+
const entries = await this.settings.listDirectory(this.rootReal, (0,external_node_path_.relative)(this.rootReal, directory.real));
|
|
54567
|
+
const sorted = entries.filter((entry)=>!isUnsafeEntryName(entry.name)).sort((a, b)=>compareCodeUnits(a.name, b.name));
|
|
54568
|
+
for (const entry of sorted){
|
|
54569
|
+
if (this.entriesExamined >= this.settings.maxEntries) {
|
|
54570
|
+
this.entryLimitReached = true;
|
|
54571
|
+
return;
|
|
54572
|
+
}
|
|
54573
|
+
this.entriesExamined += 1;
|
|
54574
|
+
await this.walkEntry(directory.real, joinRelative(directoryRelativePath, entry.name), entry.name, depth, [
|
|
54575
|
+
...ancestors,
|
|
54576
|
+
directory.real
|
|
54577
|
+
]);
|
|
54578
|
+
}
|
|
54579
|
+
}
|
|
54580
|
+
async walkEntry(directoryReal, entryRelativePath, name, depth, ancestors) {
|
|
54581
|
+
const entry = await inspectPathWithinRoot(this.rootReal, (0,external_node_path_.join)(directoryReal, name));
|
|
54582
|
+
if (entry.status === "outside-root") {
|
|
54583
|
+
if (this.wouldHaveCollected(entry.kind, name, depth)) {
|
|
54584
|
+
this.skippedOutsideRoot.push(entryRelativePath);
|
|
54585
|
+
}
|
|
54586
|
+
return;
|
|
54587
|
+
}
|
|
54588
|
+
if (entry.status !== "ok") {
|
|
54589
|
+
return;
|
|
54590
|
+
}
|
|
54591
|
+
if (entry.kind === "file") {
|
|
54592
|
+
this.collectFile(entryRelativePath, name, entry.real, entry.identity);
|
|
54593
|
+
return;
|
|
54594
|
+
}
|
|
54595
|
+
if (this.shouldDescend(name, depth)) {
|
|
54596
|
+
await this.walkDirectory(entryRelativePath, depth + 1, ancestors);
|
|
54597
|
+
} else if (this.isTooDeep(name, depth) && !this.isRevisit(entry.real, ancestors)) {
|
|
54598
|
+
this.depthLimitReached = true;
|
|
54599
|
+
}
|
|
54600
|
+
}
|
|
54601
|
+
wouldHaveCollected(kind, name, depth) {
|
|
54602
|
+
if (kind === "file") {
|
|
54603
|
+
return this.settings.includeFile(name);
|
|
54604
|
+
}
|
|
54605
|
+
return kind === "directory" && this.shouldDescend(name, depth);
|
|
54606
|
+
}
|
|
54607
|
+
collectFile(relativePath, name, real, identity) {
|
|
54608
|
+
if (this.settings.includeFile(name)) {
|
|
54609
|
+
this.files.push({
|
|
54610
|
+
relativePath,
|
|
54611
|
+
real,
|
|
54612
|
+
identity
|
|
54613
|
+
});
|
|
54614
|
+
}
|
|
54615
|
+
}
|
|
54616
|
+
shouldDescend(name, depth) {
|
|
54617
|
+
return this.settings.includeSubdirectories && depth < this.settings.maxDepth && !this.settings.skipDirectoryNames.has(name);
|
|
54618
|
+
}
|
|
54619
|
+
isTooDeep(name, depth) {
|
|
54620
|
+
return this.settings.includeSubdirectories && depth >= this.settings.maxDepth && !this.settings.skipDirectoryNames.has(name);
|
|
54621
|
+
}
|
|
54622
|
+
}
|
|
54623
|
+
/**
|
|
54624
|
+
* Walks a repository from options.startRelativePath and returns the files
|
|
54625
|
+
* accepted by options.includeFile, in name order, with the logical path a
|
|
54626
|
+
* harness loads each at and the real path to read it from.
|
|
54627
|
+
*
|
|
54628
|
+
* The cap counts every directory the walk lists (a logical visit), so a
|
|
54629
|
+
* directory reached through several aliases is counted each time it is
|
|
54630
|
+
* listed. When the cap is hit the walk stops listing, keeps what it found and
|
|
54631
|
+
* reports directoryLimitReached; it never throws for it.
|
|
54632
|
+
*/ async function walkRepository(targetDir, options) {
|
|
54633
|
+
const rootReal = await (0,promises_.realpath)((0,external_node_path_.resolve)(targetDir)).catch(()=>null);
|
|
54634
|
+
const settings = resolveSettings(options);
|
|
54635
|
+
if (rootReal === null) {
|
|
54636
|
+
return {
|
|
54637
|
+
files: [],
|
|
54638
|
+
directoryLimitReached: false,
|
|
54639
|
+
depthLimitReached: false,
|
|
54640
|
+
entryLimitReached: false,
|
|
54641
|
+
skippedOutsideRoot: [],
|
|
54642
|
+
limits: {
|
|
54643
|
+
maxDirectories: settings.maxDirectories,
|
|
54644
|
+
maxDepth: settings.maxDepth,
|
|
54645
|
+
maxEntries: settings.maxEntries
|
|
54646
|
+
}
|
|
54647
|
+
};
|
|
54648
|
+
}
|
|
54649
|
+
return new RepositoryWalk(targetDir, rootReal, settings).run();
|
|
54650
|
+
}
|
|
54651
|
+
|
|
54652
|
+
;// CONCATENATED MODULE: ../core/src/gateways/instruction-file-discovery-gateway.ts
|
|
54653
|
+
/**
|
|
54654
|
+
* Gateway for discovering instruction files across multiple formats.
|
|
54655
|
+
* Supports GitHub Copilot, Claude Code, and community agent instruction formats.
|
|
54656
|
+
*/
|
|
54657
|
+
|
|
54658
|
+
|
|
54659
|
+
|
|
54660
|
+
|
|
54661
|
+
|
|
54662
|
+
|
|
54663
|
+
/**
|
|
54664
|
+
* File system implementation of the instruction file discovery gateway.
|
|
54665
|
+
*
|
|
54666
|
+
* Symlinks follow the shared core policy (resolveRealPathWithinRoot): a
|
|
54667
|
+
* symlinked instruction file or directory is discovered only when its real
|
|
54668
|
+
* target stays inside the repository. The discovered filePath keeps the
|
|
54669
|
+
* location the harness loads; contentPath points at the real file.
|
|
54670
|
+
*/ class FileSystemInstructionFileDiscoveryGateway {
|
|
54671
|
+
maxDirectories;
|
|
54672
|
+
maxEntries;
|
|
54673
|
+
/**
|
|
54674
|
+
* @param options.maxDirectories Directories one entry walk lists before it
|
|
54675
|
+
* stops; defaults to the shared walk's production limit
|
|
54676
|
+
* @param options.maxEntries Directory entries one entry walk examines
|
|
54677
|
+
* before it stops; defaults to the shared walk's production limit
|
|
54678
|
+
*/ constructor(options = {}){
|
|
54679
|
+
this.maxDirectories = options.maxDirectories ?? (/* inlined export .DEFAULT_MAX_WALK_DIRECTORIES */10000);
|
|
54680
|
+
this.maxEntries = options.maxEntries;
|
|
54681
|
+
}
|
|
54682
|
+
async discoverInstructionFiles(targetDir, onDiagnostic) {
|
|
54683
|
+
const rootReal = await (0,promises_.realpath)((0,external_node_path_.resolve)(targetDir)).catch(()=>null);
|
|
54684
|
+
if (rootReal === null) {
|
|
54685
|
+
return [];
|
|
54686
|
+
}
|
|
54687
|
+
const root = {
|
|
54688
|
+
targetDir,
|
|
54689
|
+
rootReal
|
|
54690
|
+
};
|
|
54691
|
+
const findings = new DiscoveryFindings();
|
|
54692
|
+
for (const entry of instructionDiscoveryEntries()){
|
|
54693
|
+
const format = entry.instructionFormat;
|
|
54694
|
+
if (format == null) {
|
|
54695
|
+
continue;
|
|
54696
|
+
}
|
|
54697
|
+
if (entry.matchKind === "exact") {
|
|
54698
|
+
findings.addExact(await discoverExactFile(root, entry.path, format));
|
|
54699
|
+
continue;
|
|
54700
|
+
}
|
|
54701
|
+
findings.addWalk(await walkRepository(targetDir, this.walkOptions(entry)), (file)=>toDiscoveredFile(root, file.relativePath, {
|
|
54702
|
+
kind: "file",
|
|
54703
|
+
real: file.real
|
|
54704
|
+
}, format));
|
|
54705
|
+
}
|
|
54706
|
+
findings.report(onDiagnostic);
|
|
54707
|
+
return findings.files;
|
|
54708
|
+
}
|
|
54709
|
+
walkOptions(entry) {
|
|
54710
|
+
return {
|
|
54711
|
+
startRelativePath: entry.path,
|
|
54712
|
+
includeFile: (name)=>name.endsWith(entry.fileSuffix ?? ".md"),
|
|
54713
|
+
includeSubdirectories: entry.includeSubdirectories === true,
|
|
54714
|
+
revisit: "once-per-real-directory",
|
|
54715
|
+
maxDirectories: this.maxDirectories,
|
|
54716
|
+
...this.maxEntries === undefined ? {} : {
|
|
54717
|
+
maxEntries: this.maxEntries
|
|
54718
|
+
}
|
|
54719
|
+
};
|
|
54720
|
+
}
|
|
54721
|
+
}
|
|
54722
|
+
/**
|
|
54723
|
+
* What discovery found across the instruction locations it looked at, and
|
|
54724
|
+
* what it left out, so it can say so once at the end.
|
|
54725
|
+
*/ class DiscoveryFindings {
|
|
54726
|
+
files = [];
|
|
54727
|
+
skippedOutsideRoot = [];
|
|
54728
|
+
directoryLimitReached = false;
|
|
54729
|
+
depthLimitReached = false;
|
|
54730
|
+
entryLimitReached = false;
|
|
54731
|
+
limits;
|
|
54732
|
+
addExact(exact) {
|
|
54733
|
+
this.files.push(...exact.files);
|
|
54734
|
+
this.skippedOutsideRoot.push(...exact.skippedOutsideRoot);
|
|
54735
|
+
}
|
|
54736
|
+
addWalk(walked, toDiscovered) {
|
|
54737
|
+
this.limits = walked.limits;
|
|
54738
|
+
this.directoryLimitReached ||= walked.directoryLimitReached;
|
|
54739
|
+
this.depthLimitReached ||= walked.depthLimitReached;
|
|
54740
|
+
this.entryLimitReached ||= walked.entryLimitReached;
|
|
54741
|
+
this.skippedOutsideRoot.push(...walked.skippedOutsideRoot);
|
|
54742
|
+
this.files.push(...walked.files.map(toDiscovered));
|
|
54743
|
+
}
|
|
54744
|
+
report(onDiagnostic) {
|
|
54745
|
+
const reasons = this.describeLimits();
|
|
54746
|
+
if (reasons.length > 0) {
|
|
54747
|
+
onDiagnostic?.({
|
|
54748
|
+
ruleId: DISCOVERY_LIMIT_REACHED_RULE_ID,
|
|
54749
|
+
message: reasons.join(" ")
|
|
54750
|
+
});
|
|
54751
|
+
}
|
|
54752
|
+
if (this.skippedOutsideRoot.length > 0) {
|
|
54753
|
+
onDiagnostic?.({
|
|
54754
|
+
ruleId: DISCOVERY_SYMLINK_SKIPPED_RULE_ID,
|
|
54755
|
+
message: describeSkippedSymlinks(this.skippedOutsideRoot)
|
|
54756
|
+
});
|
|
54757
|
+
}
|
|
54758
|
+
}
|
|
54759
|
+
describeLimits() {
|
|
54760
|
+
if (this.limits === undefined) {
|
|
54761
|
+
return [];
|
|
54762
|
+
}
|
|
54763
|
+
return describeDiscoveryLimits({
|
|
54764
|
+
...this.directoryLimitReached ? {
|
|
54765
|
+
maxDirectories: this.limits.maxDirectories
|
|
54766
|
+
} : {},
|
|
54767
|
+
...this.depthLimitReached ? {
|
|
54768
|
+
maxDepth: this.limits.maxDepth
|
|
54769
|
+
} : {},
|
|
54770
|
+
...this.entryLimitReached ? {
|
|
54771
|
+
maxEntries: this.limits.maxEntries
|
|
54772
|
+
} : {}
|
|
54773
|
+
}, DISCOVERY_WORDING);
|
|
54774
|
+
}
|
|
54775
|
+
}
|
|
54776
|
+
const DISCOVERY_WORDING = {
|
|
54777
|
+
subject: "Instruction file discovery",
|
|
54778
|
+
missing: "instruction files",
|
|
54779
|
+
consequence: "were not analyzed"
|
|
54780
|
+
};
|
|
54781
|
+
/**
|
|
54782
|
+
* Discovers one exact-path instruction file, or nothing when it is missing
|
|
54783
|
+
* or resolves outside the root, in which case a link out of the repository is
|
|
54784
|
+
* reported as skipped.
|
|
54785
|
+
*/ async function discoverExactFile(root, relativePosixPath, format) {
|
|
54786
|
+
const inspected = await inspectPathWithinRoot(root.rootReal, (0,external_node_path_.join)(root.targetDir, ...relativePosixPath.split("/")));
|
|
54787
|
+
if (inspected.status === "ok" && inspected.kind === "file") {
|
|
54788
|
+
return {
|
|
54789
|
+
files: [
|
|
54790
|
+
toDiscoveredFile(root, relativePosixPath, inspected, format)
|
|
54791
|
+
],
|
|
54792
|
+
skippedOutsideRoot: []
|
|
54793
|
+
};
|
|
54794
|
+
}
|
|
54795
|
+
const leadsOutside = inspected.status === "outside-root" && inspected.kind === "file";
|
|
54796
|
+
return {
|
|
54797
|
+
files: [],
|
|
54798
|
+
skippedOutsideRoot: leadsOutside ? [
|
|
54799
|
+
relativePosixPath
|
|
54800
|
+
] : []
|
|
54801
|
+
};
|
|
54802
|
+
}
|
|
54803
|
+
/**
|
|
54804
|
+
* Builds the discovered file, adding contentPath when the logical path
|
|
54805
|
+
* resolves through a symlink to another file.
|
|
54806
|
+
*/ function toDiscoveredFile(root, relativePosixPath, resolved, format) {
|
|
54807
|
+
const segments = relativePosixPath.split("/");
|
|
54808
|
+
const filePath = (0,external_node_path_.join)(root.targetDir, ...segments);
|
|
54809
|
+
const lexicalReal = (0,external_node_path_.join)(root.rootReal, ...segments);
|
|
54810
|
+
if (resolved.real === lexicalReal) {
|
|
54811
|
+
return {
|
|
54812
|
+
filePath,
|
|
54813
|
+
format
|
|
54814
|
+
};
|
|
54815
|
+
}
|
|
54816
|
+
return {
|
|
54817
|
+
filePath,
|
|
54818
|
+
format,
|
|
54819
|
+
contentPath: (0,external_node_path_.join)(root.targetDir, (0,external_node_path_.relative)(root.rootReal, resolved.real))
|
|
54820
|
+
};
|
|
54821
|
+
}
|
|
54822
|
+
/**
|
|
54823
|
+
* Creates and returns the default instruction file discovery gateway.
|
|
54824
|
+
*/ function createInstructionFileDiscoveryGateway(options) {
|
|
54825
|
+
return new FileSystemInstructionFileDiscoveryGateway(options);
|
|
54826
|
+
}
|
|
54827
|
+
|
|
53780
54828
|
;// CONCATENATED MODULE: ../core/src/gateways/instruction-analysis-gateway.ts
|
|
53781
54829
|
/**
|
|
53782
54830
|
* Gateway for analyzing repository instructions for feedback loop coverage
|
|
53783
54831
|
*/
|
|
53784
54832
|
|
|
53785
|
-
|
|
54833
|
+
|
|
54834
|
+
|
|
54835
|
+
|
|
53786
54836
|
/**
|
|
53787
|
-
* File system implementation of instruction analysis gateway
|
|
54837
|
+
* File system implementation of instruction analysis gateway.
|
|
54838
|
+
* Scans the same instruction files that instruction quality analysis
|
|
54839
|
+
* discovers (the shared agentic location catalog).
|
|
53788
54840
|
*/ class FileSystemInstructionAnalysisGateway {
|
|
54841
|
+
discoveryGateway;
|
|
54842
|
+
constructor(discoveryGateway = createInstructionFileDiscoveryGateway()){
|
|
54843
|
+
this.discoveryGateway = discoveryGateway;
|
|
54844
|
+
}
|
|
53789
54845
|
async analyzeCoverage(targetDir, scripts, tools) {
|
|
53790
|
-
const instructionFiles = await this.findInstructionFiles(targetDir);
|
|
54846
|
+
const { files: instructionFiles, diagnostics: discoveryDiagnostics } = await this.findInstructionFiles(targetDir);
|
|
54847
|
+
const targetNames = [
|
|
54848
|
+
...scripts,
|
|
54849
|
+
...tools
|
|
54850
|
+
].map((item)=>item.name);
|
|
53791
54851
|
const references = [];
|
|
53792
|
-
const
|
|
54852
|
+
const unreadable = [];
|
|
53793
54853
|
for (const file of instructionFiles){
|
|
53794
|
-
const content = await
|
|
53795
|
-
|
|
53796
|
-
|
|
53797
|
-
|
|
53798
|
-
|
|
53799
|
-
|
|
53800
|
-
|
|
53801
|
-
}
|
|
53802
|
-
}
|
|
53803
|
-
for (const tool of tools){
|
|
53804
|
-
const toolRefs = this.findReferencesInContent(tool.name, content, lines, (0,external_node_path_.join)(targetDir, file), targetDir);
|
|
53805
|
-
if (toolRefs.length > 0) {
|
|
53806
|
-
references.push(...toolRefs);
|
|
53807
|
-
documentedTargets.add(tool.name);
|
|
53808
|
-
}
|
|
54854
|
+
const content = await this.readInstructionFile(targetDir, file);
|
|
54855
|
+
if (content.ok) {
|
|
54856
|
+
references.push(...this.findReferencesInFile(targetDir, file, content.text, targetNames));
|
|
54857
|
+
} else {
|
|
54858
|
+
unreadable.push({
|
|
54859
|
+
ruleId: INSTRUCTION_PARSE_ERROR_RULE_ID,
|
|
54860
|
+
message: `Could not read ${file.reportedPath} (${content.reason}), so it was not checked.`
|
|
54861
|
+
});
|
|
53809
54862
|
}
|
|
53810
54863
|
}
|
|
54864
|
+
const diagnostics = [
|
|
54865
|
+
...discoveryDiagnostics,
|
|
54866
|
+
...unreadable
|
|
54867
|
+
];
|
|
54868
|
+
const documentedTargets = new Set(references.map((reference)=>reference.target));
|
|
53811
54869
|
const mandatoryScripts = scripts.filter((s)=>s.isMandatory);
|
|
53812
54870
|
const mandatoryTools = tools.filter((t)=>t.isMandatory);
|
|
53813
54871
|
const allMandatory = [
|
|
@@ -53823,6 +54881,7 @@ const MAX_INSTRUCTION_FILE_BYTES = 1_048_576;
|
|
|
53823
54881
|
missingInInstructions,
|
|
53824
54882
|
documentedInInstructions,
|
|
53825
54883
|
references,
|
|
54884
|
+
diagnostics,
|
|
53826
54885
|
summary: {
|
|
53827
54886
|
totalMandatory,
|
|
53828
54887
|
totalDocumented,
|
|
@@ -53830,31 +54889,42 @@ const MAX_INSTRUCTION_FILE_BYTES = 1_048_576;
|
|
|
53830
54889
|
}
|
|
53831
54890
|
};
|
|
53832
54891
|
}
|
|
53833
|
-
async
|
|
54892
|
+
async findInstructionFiles(targetDir) {
|
|
54893
|
+
const diagnostics = [];
|
|
54894
|
+
const discovered = await this.discoveryGateway.discoverInstructionFiles(targetDir, (diagnostic)=>diagnostics.push(diagnostic));
|
|
54895
|
+
return {
|
|
54896
|
+
files: discovered.map((file)=>({
|
|
54897
|
+
reportedPath: (0,external_node_path_.relative)(targetDir, file.filePath),
|
|
54898
|
+
contentPath: (0,external_node_path_.relative)(targetDir, file.contentPath ?? file.filePath)
|
|
54899
|
+
})),
|
|
54900
|
+
diagnostics
|
|
54901
|
+
};
|
|
54902
|
+
}
|
|
54903
|
+
/**
|
|
54904
|
+
* Reads one instruction file, or says why it could not be read (a hard
|
|
54905
|
+
* link, a file over the size limit, a file that vanished), so one file
|
|
54906
|
+
* that cannot be read leaves out only itself and not the whole analysis.
|
|
54907
|
+
*/ async readInstructionFile(targetDir, file) {
|
|
53834
54908
|
try {
|
|
53835
|
-
|
|
53836
|
-
|
|
53837
|
-
|
|
54909
|
+
const text = await file_system_utils_readTextWithinRoot(targetDir, file.contentPath, MAX_INSTRUCTION_FILE_BYTES);
|
|
54910
|
+
return {
|
|
54911
|
+
ok: true,
|
|
54912
|
+
text
|
|
54913
|
+
};
|
|
54914
|
+
} catch (error) {
|
|
54915
|
+
return {
|
|
54916
|
+
ok: false,
|
|
54917
|
+
reason: error instanceof Error ? error.message : "unreadable"
|
|
54918
|
+
};
|
|
53838
54919
|
}
|
|
53839
54920
|
}
|
|
53840
|
-
|
|
53841
|
-
|
|
53842
|
-
|
|
53843
|
-
|
|
53844
|
-
|
|
53845
|
-
|
|
53846
|
-
|
|
53847
|
-
if (await this.safePathExists(targetDir, instructionsDir)) {
|
|
53848
|
-
try {
|
|
53849
|
-
const instructionFiles = await file_system_utils_listDirectoryWithinRoot(targetDir, instructionsDir);
|
|
53850
|
-
for (const file of instructionFiles){
|
|
53851
|
-
if (file.isFile() && file.name.endsWith(".md")) {
|
|
53852
|
-
files.push((0,external_node_path_.join)(instructionsDir, file.name));
|
|
53853
|
-
}
|
|
53854
|
-
}
|
|
53855
|
-
} catch {}
|
|
53856
|
-
}
|
|
53857
|
-
return files;
|
|
54921
|
+
/**
|
|
54922
|
+
* Finds the references to every target in one instruction file, grouped by
|
|
54923
|
+
* target in the order the targets are given.
|
|
54924
|
+
*/ findReferencesInFile(targetDir, file, content, targetNames) {
|
|
54925
|
+
const lines = content.split("\n");
|
|
54926
|
+
const reportedPath = (0,external_node_path_.join)(targetDir, file.reportedPath);
|
|
54927
|
+
return targetNames.flatMap((target)=>this.findReferencesInContent(target, content, lines, reportedPath, targetDir));
|
|
53858
54928
|
}
|
|
53859
54929
|
findReferencesInContent(target, content, lines, file, targetDir) {
|
|
53860
54930
|
const references = [];
|
|
@@ -55718,8 +56788,6 @@ const MAX_NPMRC_BYTES = (/* unused pure expression or super */ null && (64 * 102
|
|
|
55718
56788
|
* Core domain entities for SDLC feedback loop discovery and validation.
|
|
55719
56789
|
* These types represent scripts, tools, and their mapping to SDLC phases.
|
|
55720
56790
|
*/ /**
|
|
55721
|
-
* SDLC feedback loop phases that scripts and tools support
|
|
55722
|
-
*/ /**
|
|
55723
56791
|
* Maps common script names to SDLC phases
|
|
55724
56792
|
*/ const SCRIPT_PHASE_MAPPING = {
|
|
55725
56793
|
test: "test",
|
|
@@ -55948,322 +57016,6 @@ const PackageJsonSchema = schemas_object({
|
|
|
55948
57016
|
return new FileSystemSkillFileGateway();
|
|
55949
57017
|
}
|
|
55950
57018
|
|
|
55951
|
-
;// CONCATENATED MODULE: ../core/src/entities/agentic-location-catalog.ts
|
|
55952
|
-
/**
|
|
55953
|
-
* Canonical catalog of agentic construct locations shared by lint and doctor.
|
|
55954
|
-
* Pure data only — no filesystem I/O.
|
|
55955
|
-
*/ const agentic_location_catalog_AGENTIC_LOCATION_CATALOG = [
|
|
55956
|
-
// Skill lint roots
|
|
55957
|
-
{
|
|
55958
|
-
id: "skill-github",
|
|
55959
|
-
path: ".github/skills",
|
|
55960
|
-
matchKind: "directory-prefix",
|
|
55961
|
-
primaryConstruct: "skill",
|
|
55962
|
-
lintTarget: "skills",
|
|
55963
|
-
harnessHints: [
|
|
55964
|
-
"copilot"
|
|
55965
|
-
]
|
|
55966
|
-
},
|
|
55967
|
-
{
|
|
55968
|
-
id: "skill-claude",
|
|
55969
|
-
path: ".claude/skills",
|
|
55970
|
-
matchKind: "directory-prefix",
|
|
55971
|
-
primaryConstruct: "skill",
|
|
55972
|
-
lintTarget: "skills",
|
|
55973
|
-
harnessHints: [
|
|
55974
|
-
"claude"
|
|
55975
|
-
]
|
|
55976
|
-
},
|
|
55977
|
-
{
|
|
55978
|
-
id: "skill-agents",
|
|
55979
|
-
path: ".agents/skills",
|
|
55980
|
-
matchKind: "directory-prefix",
|
|
55981
|
-
primaryConstruct: "skill",
|
|
55982
|
-
lintTarget: "skills",
|
|
55983
|
-
harnessHints: [
|
|
55984
|
-
"shared"
|
|
55985
|
-
]
|
|
55986
|
-
},
|
|
55987
|
-
// Additional skill roots (SKILL.md convention, no alternate layout documented)
|
|
55988
|
-
{
|
|
55989
|
-
id: "skill-pi",
|
|
55990
|
-
path: ".pi/skills",
|
|
55991
|
-
matchKind: "directory-prefix",
|
|
55992
|
-
primaryConstruct: "skill",
|
|
55993
|
-
lintTarget: "skills",
|
|
55994
|
-
harnessHints: [
|
|
55995
|
-
"pi"
|
|
55996
|
-
]
|
|
55997
|
-
},
|
|
55998
|
-
{
|
|
55999
|
-
id: "skill-pi-prompts",
|
|
56000
|
-
path: ".pi/prompts",
|
|
56001
|
-
matchKind: "directory-prefix",
|
|
56002
|
-
primaryConstruct: "skill",
|
|
56003
|
-
lintTarget: "skills",
|
|
56004
|
-
harnessHints: [
|
|
56005
|
-
"pi"
|
|
56006
|
-
]
|
|
56007
|
-
},
|
|
56008
|
-
// Agent lint root (dual-role: agent + instruction)
|
|
56009
|
-
{
|
|
56010
|
-
id: "agent-github",
|
|
56011
|
-
path: ".github/agents",
|
|
56012
|
-
matchKind: "directory-prefix",
|
|
56013
|
-
primaryConstruct: "agent",
|
|
56014
|
-
secondaryConstructs: [
|
|
56015
|
-
"instruction"
|
|
56016
|
-
],
|
|
56017
|
-
lintTarget: "agents",
|
|
56018
|
-
harnessHints: [
|
|
56019
|
-
"copilot"
|
|
56020
|
-
],
|
|
56021
|
-
instructionFormat: "copilot-agent"
|
|
56022
|
-
},
|
|
56023
|
-
// Hook lint exact configs
|
|
56024
|
-
{
|
|
56025
|
-
id: "hook-copilot-agent-shell",
|
|
56026
|
-
path: ".github/hooks/agent-shell/hooks.json",
|
|
56027
|
-
matchKind: "exact",
|
|
56028
|
-
primaryConstruct: "hook",
|
|
56029
|
-
lintTarget: "hooks",
|
|
56030
|
-
harnessHints: [
|
|
56031
|
-
"copilot"
|
|
56032
|
-
],
|
|
56033
|
-
hookPlatform: "copilot"
|
|
56034
|
-
},
|
|
56035
|
-
{
|
|
56036
|
-
id: "hook-claude-settings",
|
|
56037
|
-
path: ".claude/settings.json",
|
|
56038
|
-
matchKind: "exact",
|
|
56039
|
-
primaryConstruct: "hook",
|
|
56040
|
-
lintTarget: "hooks",
|
|
56041
|
-
harnessHints: [
|
|
56042
|
-
"claude"
|
|
56043
|
-
],
|
|
56044
|
-
hookPlatform: "claude"
|
|
56045
|
-
},
|
|
56046
|
-
{
|
|
56047
|
-
id: "hook-claude-settings-local",
|
|
56048
|
-
path: ".claude/settings.local.json",
|
|
56049
|
-
matchKind: "exact",
|
|
56050
|
-
primaryConstruct: "hook",
|
|
56051
|
-
lintTarget: "hooks",
|
|
56052
|
-
harnessHints: [
|
|
56053
|
-
"claude"
|
|
56054
|
-
],
|
|
56055
|
-
hookPlatform: "claude"
|
|
56056
|
-
},
|
|
56057
|
-
// Doctor-only hook directory prefixes
|
|
56058
|
-
{
|
|
56059
|
-
id: "hook-github-dir",
|
|
56060
|
-
path: ".github/hooks",
|
|
56061
|
-
matchKind: "directory-prefix",
|
|
56062
|
-
primaryConstruct: "hook",
|
|
56063
|
-
lintTarget: "none",
|
|
56064
|
-
harnessHints: [
|
|
56065
|
-
"copilot"
|
|
56066
|
-
]
|
|
56067
|
-
},
|
|
56068
|
-
{
|
|
56069
|
-
id: "hook-claude-dir",
|
|
56070
|
-
path: ".claude/hooks",
|
|
56071
|
-
matchKind: "directory-prefix",
|
|
56072
|
-
primaryConstruct: "hook",
|
|
56073
|
-
lintTarget: "none",
|
|
56074
|
-
harnessHints: [
|
|
56075
|
-
"claude"
|
|
56076
|
-
]
|
|
56077
|
-
},
|
|
56078
|
-
// Instruction lint locations
|
|
56079
|
-
{
|
|
56080
|
-
id: "instruction-copilot-root",
|
|
56081
|
-
path: ".github/copilot-instructions.md",
|
|
56082
|
-
matchKind: "exact",
|
|
56083
|
-
primaryConstruct: "instruction",
|
|
56084
|
-
lintTarget: "instructions",
|
|
56085
|
-
harnessHints: [
|
|
56086
|
-
"copilot"
|
|
56087
|
-
],
|
|
56088
|
-
instructionFormat: "copilot-instructions"
|
|
56089
|
-
},
|
|
56090
|
-
{
|
|
56091
|
-
id: "instruction-copilot-scoped",
|
|
56092
|
-
path: ".github/instructions",
|
|
56093
|
-
matchKind: "directory-prefix",
|
|
56094
|
-
primaryConstruct: "instruction",
|
|
56095
|
-
lintTarget: "instructions",
|
|
56096
|
-
harnessHints: [
|
|
56097
|
-
"copilot"
|
|
56098
|
-
],
|
|
56099
|
-
instructionFormat: "copilot-scoped"
|
|
56100
|
-
},
|
|
56101
|
-
{
|
|
56102
|
-
id: "instruction-agents-md",
|
|
56103
|
-
path: "AGENTS.md",
|
|
56104
|
-
matchKind: "exact",
|
|
56105
|
-
primaryConstruct: "instruction",
|
|
56106
|
-
lintTarget: "instructions",
|
|
56107
|
-
harnessHints: [
|
|
56108
|
-
"shared"
|
|
56109
|
-
],
|
|
56110
|
-
instructionFormat: "agents-md"
|
|
56111
|
-
},
|
|
56112
|
-
{
|
|
56113
|
-
id: "instruction-claude-md",
|
|
56114
|
-
path: "CLAUDE.md",
|
|
56115
|
-
matchKind: "exact",
|
|
56116
|
-
primaryConstruct: "instruction",
|
|
56117
|
-
lintTarget: "instructions",
|
|
56118
|
-
harnessHints: [
|
|
56119
|
-
"claude"
|
|
56120
|
-
],
|
|
56121
|
-
instructionFormat: "claude-md"
|
|
56122
|
-
},
|
|
56123
|
-
// Subagent lint root
|
|
56124
|
-
{
|
|
56125
|
-
id: "subagent-claude",
|
|
56126
|
-
path: ".claude/agents",
|
|
56127
|
-
matchKind: "directory-prefix",
|
|
56128
|
-
primaryConstruct: "subagent",
|
|
56129
|
-
lintTarget: "subagents",
|
|
56130
|
-
harnessHints: [
|
|
56131
|
-
"claude"
|
|
56132
|
-
]
|
|
56133
|
-
},
|
|
56134
|
-
{
|
|
56135
|
-
id: "agent-claude-commands",
|
|
56136
|
-
path: ".claude/commands",
|
|
56137
|
-
matchKind: "directory-prefix",
|
|
56138
|
-
primaryConstruct: "agent",
|
|
56139
|
-
lintTarget: "none",
|
|
56140
|
-
harnessHints: [
|
|
56141
|
-
"claude"
|
|
56142
|
-
]
|
|
56143
|
-
},
|
|
56144
|
-
{
|
|
56145
|
-
id: "plugin-codex",
|
|
56146
|
-
path: ".codex-plugin",
|
|
56147
|
-
matchKind: "directory-prefix",
|
|
56148
|
-
primaryConstruct: "plugin",
|
|
56149
|
-
lintTarget: "none",
|
|
56150
|
-
harnessHints: [
|
|
56151
|
-
"codex"
|
|
56152
|
-
]
|
|
56153
|
-
}
|
|
56154
|
-
];
|
|
56155
|
-
|
|
56156
|
-
;// CONCATENATED MODULE: ../core/src/lib/agentic-location-matchers.ts
|
|
56157
|
-
/**
|
|
56158
|
-
* Pure path→construct matching helpers over the agentic location catalog.
|
|
56159
|
-
* No filesystem I/O.
|
|
56160
|
-
*/
|
|
56161
|
-
/**
|
|
56162
|
-
* Normalizes a repo-relative path for catalog matching.
|
|
56163
|
-
* Collapses `.` / `..` segments. Returns "" when the path is absolute or
|
|
56164
|
-
* escapes the repository root — callers treat "" as non-matching.
|
|
56165
|
-
*/ function isAbsoluteRepoPath(normalized) {
|
|
56166
|
-
// POSIX absolute, UNC-style leading slash after backslash conversion,
|
|
56167
|
-
// and Windows drive-letter absolute (C:/..., d:\...).
|
|
56168
|
-
return normalized.startsWith("/") || /^[A-Za-z]:(\/|$)/.test(normalized);
|
|
56169
|
-
}
|
|
56170
|
-
function normalizeRepoRelativePath(path) {
|
|
56171
|
-
const normalized = path.replaceAll("\\", "/");
|
|
56172
|
-
if (isAbsoluteRepoPath(normalized)) {
|
|
56173
|
-
return "";
|
|
56174
|
-
}
|
|
56175
|
-
const resolved = [];
|
|
56176
|
-
for (const segment of normalized.split("/")){
|
|
56177
|
-
if (segment === "" || segment === ".") {
|
|
56178
|
-
continue;
|
|
56179
|
-
}
|
|
56180
|
-
if (segment === ".." && resolved.length === 0) {
|
|
56181
|
-
return "";
|
|
56182
|
-
}
|
|
56183
|
-
if (segment === "..") {
|
|
56184
|
-
resolved.pop();
|
|
56185
|
-
continue;
|
|
56186
|
-
}
|
|
56187
|
-
resolved.push(segment);
|
|
56188
|
-
}
|
|
56189
|
-
return resolved.join("/");
|
|
56190
|
-
}
|
|
56191
|
-
function matchesLocationEntry(path, entry) {
|
|
56192
|
-
const normalized = normalizeRepoRelativePath(path);
|
|
56193
|
-
if (entry.matchKind === "exact") {
|
|
56194
|
-
return normalized === entry.path;
|
|
56195
|
-
}
|
|
56196
|
-
return normalized === entry.path || normalized.startsWith(`${entry.path}/`);
|
|
56197
|
-
}
|
|
56198
|
-
function entriesMatchingPath(path, catalog = AGENTIC_LOCATION_CATALOG) {
|
|
56199
|
-
return catalog.filter((entry)=>matchesLocationEntry(path, entry));
|
|
56200
|
-
}
|
|
56201
|
-
function specificityScore(entry) {
|
|
56202
|
-
// Exact matches outrank any prefix; longer prefixes outrank shorter ones.
|
|
56203
|
-
const kindBonus = entry.matchKind === "exact" ? 1_000_000 : 0;
|
|
56204
|
-
return kindBonus + entry.path.length;
|
|
56205
|
-
}
|
|
56206
|
-
function bestMatchingEntry(path, catalog = AGENTIC_LOCATION_CATALOG) {
|
|
56207
|
-
const matches = entriesMatchingPath(path, catalog);
|
|
56208
|
-
if (matches.length === 0) {
|
|
56209
|
-
return null;
|
|
56210
|
-
}
|
|
56211
|
-
let best = matches[0];
|
|
56212
|
-
let bestScore = specificityScore(best);
|
|
56213
|
-
for(let i = 1; i < matches.length; i++){
|
|
56214
|
-
const candidate = matches[i];
|
|
56215
|
-
const score = specificityScore(candidate);
|
|
56216
|
-
if (score > bestScore) {
|
|
56217
|
-
best = candidate;
|
|
56218
|
-
bestScore = score;
|
|
56219
|
-
}
|
|
56220
|
-
}
|
|
56221
|
-
return best;
|
|
56222
|
-
}
|
|
56223
|
-
function primaryConstructTypeForPath(path, catalog = AGENTIC_LOCATION_CATALOG) {
|
|
56224
|
-
return bestMatchingEntry(path, catalog)?.primaryConstruct ?? null;
|
|
56225
|
-
}
|
|
56226
|
-
function constructTypesForPath(path, catalog = AGENTIC_LOCATION_CATALOG) {
|
|
56227
|
-
const best = bestMatchingEntry(path, catalog);
|
|
56228
|
-
if (!best) {
|
|
56229
|
-
return [];
|
|
56230
|
-
}
|
|
56231
|
-
const secondary = best.secondaryConstructs ?? [];
|
|
56232
|
-
if (secondary.length === 0) {
|
|
56233
|
-
return [
|
|
56234
|
-
best.primaryConstruct
|
|
56235
|
-
];
|
|
56236
|
-
}
|
|
56237
|
-
return [
|
|
56238
|
-
best.primaryConstruct,
|
|
56239
|
-
...secondary
|
|
56240
|
-
];
|
|
56241
|
-
}
|
|
56242
|
-
function lintTargetEntries(target, catalog = agentic_location_catalog_AGENTIC_LOCATION_CATALOG) {
|
|
56243
|
-
return catalog.filter((entry)=>entry.lintTarget === target);
|
|
56244
|
-
}
|
|
56245
|
-
function skillDirectoryRoots(catalog = agentic_location_catalog_AGENTIC_LOCATION_CATALOG) {
|
|
56246
|
-
return lintTargetEntries("skills", catalog).filter((entry)=>entry.matchKind === "directory-prefix").map((entry)=>entry.path);
|
|
56247
|
-
}
|
|
56248
|
-
function agentDirectoryRoots(catalog = AGENTIC_LOCATION_CATALOG) {
|
|
56249
|
-
return lintTargetEntries("agents", catalog).filter((entry)=>entry.matchKind === "directory-prefix").map((entry)=>entry.path);
|
|
56250
|
-
}
|
|
56251
|
-
function subagentDirectoryRoots(catalog = AGENTIC_LOCATION_CATALOG) {
|
|
56252
|
-
return lintTargetEntries("subagents", catalog).filter((entry)=>entry.matchKind === "directory-prefix").map((entry)=>entry.path);
|
|
56253
|
-
}
|
|
56254
|
-
function isHookConfigEntry(entry) {
|
|
56255
|
-
return entry.matchKind === "exact" && entry.hookPlatform != null;
|
|
56256
|
-
}
|
|
56257
|
-
function hookConfigPaths(catalog = AGENTIC_LOCATION_CATALOG) {
|
|
56258
|
-
return lintTargetEntries("hooks", catalog).filter(isHookConfigEntry).map((entry)=>({
|
|
56259
|
-
relativePath: entry.path,
|
|
56260
|
-
platform: entry.hookPlatform
|
|
56261
|
-
}));
|
|
56262
|
-
}
|
|
56263
|
-
function instructionDiscoveryEntries(catalog = agentic_location_catalog_AGENTIC_LOCATION_CATALOG) {
|
|
56264
|
-
return catalog.filter((entry)=>entry.instructionFormat != null);
|
|
56265
|
-
}
|
|
56266
|
-
|
|
56267
57019
|
;// CONCATENATED MODULE: ../core/src/gateways/skill-lint-gateway.ts
|
|
56268
57020
|
/**
|
|
56269
57021
|
* Gateway for skill lint file system operations.
|
|
@@ -56532,87 +57284,78 @@ const MAX_WORKFLOW_FILE_BYTES = 1_048_576;
|
|
|
56532
57284
|
return patterns.includes(actionName);
|
|
56533
57285
|
}
|
|
56534
57286
|
/**
|
|
56535
|
-
*
|
|
57287
|
+
* Narrows to a truthy, object-typed value (arrays included).
|
|
57288
|
+
*/ function isObjectLike(value) {
|
|
57289
|
+
return Boolean(value) && typeof value === "object";
|
|
57290
|
+
}
|
|
57291
|
+
/**
|
|
57292
|
+
* Returns every step object in every job of a workflow, in job and step order.
|
|
57293
|
+
* Anything that is not an object (a missing "jobs" map, a job without a
|
|
57294
|
+
* "steps" array, a non-object step) contributes no steps.
|
|
56536
57295
|
* @param workflow The parsed workflow object
|
|
56537
|
-
|
|
56538
|
-
|
|
56539
|
-
|
|
56540
|
-
return;
|
|
56541
|
-
}
|
|
56542
|
-
const jobs = workflow.jobs;
|
|
56543
|
-
if (!jobs || typeof jobs !== "object") {
|
|
56544
|
-
return;
|
|
57296
|
+
*/ function workflowStepObjects(workflow) {
|
|
57297
|
+
if (!isObjectLike(workflow) || !isObjectLike(workflow.jobs)) {
|
|
57298
|
+
return [];
|
|
56545
57299
|
}
|
|
56546
|
-
|
|
56547
|
-
|
|
56548
|
-
|
|
56549
|
-
|
|
56550
|
-
|
|
56551
|
-
|
|
56552
|
-
|
|
56553
|
-
|
|
56554
|
-
|
|
56555
|
-
|
|
56556
|
-
|
|
56557
|
-
|
|
56558
|
-
|
|
56559
|
-
|
|
56560
|
-
|
|
56561
|
-
const withConfig = stepObj.with;
|
|
56562
|
-
callback({
|
|
56563
|
-
uses,
|
|
56564
|
-
with: withConfig && typeof withConfig === "object" ? withConfig : undefined
|
|
56565
|
-
});
|
|
57300
|
+
return Object.values(workflow.jobs).filter(isObjectLike).flatMap((job)=>Array.isArray(job.steps) ? job.steps.filter(isObjectLike) : []);
|
|
57301
|
+
}
|
|
57302
|
+
/**
|
|
57303
|
+
* Reads a step's "with" configuration, ignoring anything that is not an object
|
|
57304
|
+
*/ function toWithConfig(value) {
|
|
57305
|
+
return isObjectLike(value) ? value : undefined;
|
|
57306
|
+
}
|
|
57307
|
+
/**
|
|
57308
|
+
* Returns all steps in a workflow that have a "uses" field
|
|
57309
|
+
* @param workflow The parsed workflow object
|
|
57310
|
+
*/ function workflowUsesSteps(workflow) {
|
|
57311
|
+
return workflowStepObjects(workflow).flatMap((step)=>typeof step.uses === "string" ? [
|
|
57312
|
+
{
|
|
57313
|
+
uses: step.uses,
|
|
57314
|
+
with: toWithConfig(step.with)
|
|
56566
57315
|
}
|
|
56567
|
-
|
|
56568
|
-
|
|
57316
|
+
] : []);
|
|
57317
|
+
}
|
|
57318
|
+
/**
|
|
57319
|
+
* Builds the key under which a run step is recorded in the set of existing
|
|
57320
|
+
* workflow steps. Run steps have no "uses" field, so they are matched by
|
|
57321
|
+
* their command.
|
|
57322
|
+
* @example "npm ci" -> "run-command:npm ci"
|
|
57323
|
+
*/ function runCommandKey(command) {
|
|
57324
|
+
return `run-command:${command.trim()}`;
|
|
56569
57325
|
}
|
|
56570
57326
|
/**
|
|
56571
57327
|
* Extracts all steps from a workflow (including steps without "uses")
|
|
56572
57328
|
* @param workflow The parsed workflow object
|
|
56573
57329
|
* @returns Array of step details
|
|
56574
57330
|
*/ function extractAllWorkflowSteps(workflow) {
|
|
56575
|
-
|
|
56576
|
-
|
|
56577
|
-
|
|
56578
|
-
|
|
56579
|
-
|
|
56580
|
-
|
|
56581
|
-
|
|
57331
|
+
return workflowStepObjects(workflow).map((step)=>({
|
|
57332
|
+
name: typeof step.name === "string" ? step.name : undefined,
|
|
57333
|
+
uses: typeof step.uses === "string" ? step.uses : undefined,
|
|
57334
|
+
with: toWithConfig(step.with)
|
|
57335
|
+
}));
|
|
57336
|
+
}
|
|
57337
|
+
/**
|
|
57338
|
+
* The key a step contributes to the set of existing workflow steps: its action
|
|
57339
|
+
* name when it uses an action, otherwise its run command key.
|
|
57340
|
+
*/ function existingActionKeys(step) {
|
|
57341
|
+
if (typeof step.uses === "string") {
|
|
57342
|
+
return [
|
|
57343
|
+
parseActionName(step.uses)
|
|
57344
|
+
];
|
|
56582
57345
|
}
|
|
56583
|
-
|
|
56584
|
-
|
|
56585
|
-
|
|
56586
|
-
|
|
56587
|
-
const jobSteps = job.steps;
|
|
56588
|
-
if (!Array.isArray(jobSteps)) {
|
|
56589
|
-
continue;
|
|
56590
|
-
}
|
|
56591
|
-
for (const step of jobSteps){
|
|
56592
|
-
if (!step || typeof step !== "object") {
|
|
56593
|
-
continue;
|
|
56594
|
-
}
|
|
56595
|
-
const stepObj = step;
|
|
56596
|
-
const withConfig = stepObj.with;
|
|
56597
|
-
steps.push({
|
|
56598
|
-
name: typeof stepObj.name === "string" ? stepObj.name : undefined,
|
|
56599
|
-
uses: typeof stepObj.uses === "string" ? stepObj.uses : undefined,
|
|
56600
|
-
with: withConfig && typeof withConfig === "object" ? withConfig : undefined
|
|
56601
|
-
});
|
|
56602
|
-
}
|
|
57346
|
+
if (typeof step.run === "string") {
|
|
57347
|
+
return [
|
|
57348
|
+
runCommandKey(step.run)
|
|
57349
|
+
];
|
|
56603
57350
|
}
|
|
56604
|
-
return
|
|
57351
|
+
return [];
|
|
56605
57352
|
}
|
|
56606
57353
|
/**
|
|
56607
57354
|
* Extracts existing setup actions from a parsed workflow
|
|
56608
57355
|
* @param workflow The parsed workflow object
|
|
56609
|
-
* @returns Set of action names already present
|
|
57356
|
+
* @returns Set of action names already present, plus a runCommandKey for each run step
|
|
56610
57357
|
*/ function getExistingActionsFromWorkflow(workflow) {
|
|
56611
|
-
|
|
56612
|
-
forEachWorkflowStep(workflow, (step)=>{
|
|
56613
|
-
actions.add(parseActionName(step.uses));
|
|
56614
|
-
});
|
|
56615
|
-
return actions;
|
|
57358
|
+
return new Set(workflowStepObjects(workflow).flatMap(existingActionKeys));
|
|
56616
57359
|
}
|
|
56617
57360
|
/**
|
|
56618
57361
|
* Identifies candidates that are missing from an existing workflow
|
|
@@ -56620,24 +57363,14 @@ const MAX_WORKFLOW_FILE_BYTES = 1_048_576;
|
|
|
56620
57363
|
* @param existingActions Actions already present in the workflow
|
|
56621
57364
|
* @returns Candidates that need to be added
|
|
56622
57365
|
*/ function findMissingCandidates(candidates, existingActions) {
|
|
56623
|
-
return candidates.filter((candidate)
|
|
57366
|
+
return candidates.filter((candidate)=>candidate.run ? !existingActions.has(runCommandKey(candidate.run)) : !existingActions.has(candidate.action));
|
|
56624
57367
|
}
|
|
56625
57368
|
/**
|
|
56626
57369
|
* Merges candidates from multiple sources, with earlier sources taking precedence
|
|
56627
57370
|
* @param candidateSources Arrays of candidates, in order of precedence
|
|
56628
57371
|
* @returns Merged and deduplicated candidates
|
|
56629
57372
|
*/ function mergeCandidates(...candidateSources) {
|
|
56630
|
-
|
|
56631
|
-
const seen = new Set();
|
|
56632
|
-
for (const candidates of candidateSources){
|
|
56633
|
-
for (const candidate of candidates){
|
|
56634
|
-
if (!seen.has(candidate.action)) {
|
|
56635
|
-
seen.add(candidate.action);
|
|
56636
|
-
result.push(candidate);
|
|
56637
|
-
}
|
|
56638
|
-
}
|
|
56639
|
-
}
|
|
56640
|
-
return result;
|
|
57373
|
+
return deduplicateCandidates(candidateSources.flat());
|
|
56641
57374
|
}
|
|
56642
57375
|
/**
|
|
56643
57376
|
* Extracts version from a "uses" string
|
|
@@ -56653,19 +57386,16 @@ const MAX_WORKFLOW_FILE_BYTES = 1_048_576;
|
|
|
56653
57386
|
* @param workflow The parsed workflow object
|
|
56654
57387
|
* @returns Array of action references with name and version
|
|
56655
57388
|
*/ function extractActionsFromWorkflow(workflow) {
|
|
56656
|
-
|
|
56657
|
-
forEachWorkflowStep(workflow, (step)=>{
|
|
56658
|
-
const name = parseActionName(step.uses);
|
|
57389
|
+
return workflowUsesSteps(workflow).flatMap((step)=>{
|
|
56659
57390
|
const version = parseActionVersion(step.uses);
|
|
56660
57391
|
// Skip actions without versions - these are incomplete references
|
|
56661
|
-
|
|
56662
|
-
|
|
56663
|
-
name,
|
|
57392
|
+
return version ? [
|
|
57393
|
+
{
|
|
57394
|
+
name: parseActionName(step.uses),
|
|
56664
57395
|
version
|
|
56665
|
-
}
|
|
56666
|
-
|
|
57396
|
+
}
|
|
57397
|
+
] : [];
|
|
56667
57398
|
});
|
|
56668
|
-
return actions;
|
|
56669
57399
|
}
|
|
56670
57400
|
/**
|
|
56671
57401
|
* Extracts setup step candidates from a parsed workflow based on action patterns
|
|
@@ -56673,19 +57403,17 @@ const MAX_WORKFLOW_FILE_BYTES = 1_048_576;
|
|
|
56673
57403
|
* @param patterns List of action patterns to detect
|
|
56674
57404
|
* @returns Array of setup step candidates
|
|
56675
57405
|
*/ function extractSetupStepsFromWorkflow(workflow, patterns) {
|
|
56676
|
-
|
|
56677
|
-
forEachWorkflowStep(workflow, (step)=>{
|
|
57406
|
+
return workflowUsesSteps(workflow).flatMap((step)=>{
|
|
56678
57407
|
const action = parseActionName(step.uses);
|
|
56679
|
-
|
|
56680
|
-
|
|
57408
|
+
return isSetupAction(action, patterns) ? [
|
|
57409
|
+
{
|
|
56681
57410
|
action,
|
|
56682
57411
|
version: parseActionVersion(step.uses),
|
|
56683
57412
|
config: step.with,
|
|
56684
57413
|
source: "workflow"
|
|
56685
|
-
}
|
|
56686
|
-
|
|
57414
|
+
}
|
|
57415
|
+
] : [];
|
|
56687
57416
|
});
|
|
56688
|
-
return candidates;
|
|
56689
57417
|
}
|
|
56690
57418
|
/**
|
|
56691
57419
|
* Deduplicates candidates by action name, keeping the first occurrence
|
|
@@ -56719,9 +57447,16 @@ const COPILOT_SETUP_WORKFLOW_FILENAMES = [
|
|
|
56719
57447
|
const file_system_workflow_gateway_MAX_WORKFLOW_FILE_BYTES = 1024 * 1024;
|
|
56720
57448
|
class FileSystemWorkflowGateway {
|
|
56721
57449
|
cwd;
|
|
56722
|
-
config
|
|
56723
|
-
|
|
57450
|
+
config;
|
|
57451
|
+
/**
|
|
57452
|
+
* @param cwd Directory the configuration is loaded from (defaults to
|
|
57453
|
+
* process.cwd()), unless `config` is given
|
|
57454
|
+
* @param config A configuration loaded by the caller. The gateway uses it
|
|
57455
|
+
* as it is and does not read the configuration file itself, so a caller
|
|
57456
|
+
* that also needs the configuration reads the file once for both.
|
|
57457
|
+
*/ constructor(cwd, config){
|
|
56724
57458
|
this.cwd = cwd;
|
|
57459
|
+
this.config = config ?? null;
|
|
56725
57460
|
}
|
|
56726
57461
|
async getConfig() {
|
|
56727
57462
|
if (!this.config) {
|
|
@@ -56792,6 +57527,13 @@ class FileSystemWorkflowGateway {
|
|
|
56792
57527
|
function createWorkflowGateway(cwd) {
|
|
56793
57528
|
return new FileSystemWorkflowGateway(cwd);
|
|
56794
57529
|
}
|
|
57530
|
+
/**
|
|
57531
|
+
* Creates a workflow gateway that scans workflows with a configuration the
|
|
57532
|
+
* caller has already loaded, so the configuration file is read once and the
|
|
57533
|
+
* gateway and the caller cannot see two different versions of it.
|
|
57534
|
+
*/ function createWorkflowGatewayWithConfig(config) {
|
|
57535
|
+
return new FileSystemWorkflowGateway(undefined, config);
|
|
57536
|
+
}
|
|
56795
57537
|
|
|
56796
57538
|
;// CONCATENATED MODULE: ../core/src/gateways/workflow-gateway.ts
|
|
56797
57539
|
/**
|
|
@@ -56820,7 +57562,8 @@ function createWorkflowGateway(cwd) {
|
|
|
56820
57562
|
;// CONCATENATED MODULE: ./src/tools/types.ts
|
|
56821
57563
|
/**
|
|
56822
57564
|
* Shared types and utilities for MCP tool handlers.
|
|
56823
|
-
*/
|
|
57565
|
+
*/
|
|
57566
|
+
/**
|
|
56824
57567
|
* Creates an error response for MCP tools.
|
|
56825
57568
|
*/ function types_errorResponse(error) {
|
|
56826
57569
|
return {
|
|
@@ -56832,9 +57575,23 @@ function createWorkflowGateway(cwd) {
|
|
|
56832
57575
|
error
|
|
56833
57576
|
})
|
|
56834
57577
|
}
|
|
56835
|
-
]
|
|
57578
|
+
],
|
|
57579
|
+
isError: true
|
|
56836
57580
|
};
|
|
56837
57581
|
}
|
|
57582
|
+
/**
|
|
57583
|
+
* Returns an error response when the target path is missing or is not a
|
|
57584
|
+
* directory, or null when it is a directory the tool can operate on.
|
|
57585
|
+
*/ async function validateTargetDirectory(dir) {
|
|
57586
|
+
const dirStat = await (0,promises_.stat)(dir).catch(()=>null);
|
|
57587
|
+
if (dirStat === null) {
|
|
57588
|
+
return types_errorResponse(`Target directory does not exist: ${dir}`);
|
|
57589
|
+
}
|
|
57590
|
+
if (!dirStat.isDirectory()) {
|
|
57591
|
+
return types_errorResponse(`Target path is not a directory: ${dir}`);
|
|
57592
|
+
}
|
|
57593
|
+
return null;
|
|
57594
|
+
}
|
|
56838
57595
|
/**
|
|
56839
57596
|
* Creates a success response for MCP tools.
|
|
56840
57597
|
*/ function successResponse(data) {
|
|
@@ -56859,7 +57616,6 @@ function createWorkflowGateway(cwd) {
|
|
|
56859
57616
|
|
|
56860
57617
|
|
|
56861
57618
|
|
|
56862
|
-
|
|
56863
57619
|
/**
|
|
56864
57620
|
* Parses a workflow file and extracts action references.
|
|
56865
57621
|
*/ async function parseWorkflowFile(rootDir, relativePath) {
|
|
@@ -56891,9 +57647,9 @@ function createWorkflowGateway(cwd) {
|
|
|
56891
57647
|
*/ const analyzeActionVersionsHandler = async (args)=>{
|
|
56892
57648
|
try {
|
|
56893
57649
|
const dir = args.targetDir || process.cwd();
|
|
56894
|
-
const
|
|
56895
|
-
if (
|
|
56896
|
-
return
|
|
57650
|
+
const targetDirError = await validateTargetDirectory(dir);
|
|
57651
|
+
if (targetDirError !== null) {
|
|
57652
|
+
return targetDirError;
|
|
56897
57653
|
}
|
|
56898
57654
|
const workflowsRelativeDir = (0,external_node_path_.join)(".github", "workflows");
|
|
56899
57655
|
if (!await file_system_utils_pathExistsWithinRoot(dir, workflowsRelativeDir)) {
|
|
@@ -56939,101 +57695,699 @@ function createWorkflowGateway(cwd) {
|
|
|
56939
57695
|
;// CONCATENATED MODULE: ../core/src/entities/source-position.ts
|
|
56940
57696
|
/**
|
|
56941
57697
|
* Pure helpers for mapping string offsets to 1-based line/column positions.
|
|
56942
|
-
*/ /** 1-based line and column within a text document */
|
|
56943
|
-
|
|
57698
|
+
*/ /** 1-based line and column within a text document */ const LINE_FEED = 0x0a;
|
|
57699
|
+
/**
|
|
57700
|
+
* Indexes the line starts of a content once, and answers each offset with a
|
|
57701
|
+
* binary search, so mapping many offsets of one content costs the length of
|
|
57702
|
+
* the content plus a logarithm per offset, not a rescan from the start.
|
|
56944
57703
|
* Offsets are clamped to [0, content.length]. Newlines (`\n`) advance the line.
|
|
57704
|
+
*
|
|
57705
|
+
* @param countSteps Told one step per character when the index is built, and
|
|
57706
|
+
* one per comparison when an offset is answered, so a test can show that the
|
|
57707
|
+
* work stays linear and logarithmic and not quadratic
|
|
57708
|
+
*/ function createSourcePositionIndex(content, countSteps) {
|
|
57709
|
+
const lineStarts = [
|
|
57710
|
+
0
|
|
57711
|
+
];
|
|
57712
|
+
for(let i = 0; i < content.length; i += 1){
|
|
57713
|
+
if (content.charCodeAt(i) === LINE_FEED) {
|
|
57714
|
+
lineStarts.push(i + 1);
|
|
57715
|
+
}
|
|
57716
|
+
}
|
|
57717
|
+
countSteps?.(content.length);
|
|
57718
|
+
return (offset)=>{
|
|
57719
|
+
const safeOffset = Math.max(0, Math.min(offset, content.length));
|
|
57720
|
+
let low = 0;
|
|
57721
|
+
let high = lineStarts.length - 1;
|
|
57722
|
+
countSteps?.(1);
|
|
57723
|
+
while(low < high){
|
|
57724
|
+
countSteps?.(1);
|
|
57725
|
+
const middle = low + high + 1 >> 1;
|
|
57726
|
+
if ((lineStarts[middle] ?? 0) <= safeOffset) {
|
|
57727
|
+
low = middle;
|
|
57728
|
+
} else {
|
|
57729
|
+
high = middle - 1;
|
|
57730
|
+
}
|
|
57731
|
+
}
|
|
57732
|
+
return {
|
|
57733
|
+
line: low + 1,
|
|
57734
|
+
column: safeOffset - (lineStarts[low] ?? 0) + 1
|
|
57735
|
+
};
|
|
57736
|
+
};
|
|
57737
|
+
}
|
|
57738
|
+
/**
|
|
57739
|
+
* Maps a 0-based string offset into a 1-based line and column. To map many
|
|
57740
|
+
* offsets of the same content, build one index with createSourcePositionIndex.
|
|
56945
57741
|
*/ function offsetToSourcePosition(content, offset) {
|
|
56946
|
-
|
|
56947
|
-
|
|
56948
|
-
|
|
56949
|
-
|
|
56950
|
-
|
|
56951
|
-
|
|
56952
|
-
|
|
56953
|
-
|
|
56954
|
-
|
|
57742
|
+
return createSourcePositionIndex(content)(offset);
|
|
57743
|
+
}
|
|
57744
|
+
|
|
57745
|
+
;// CONCATENATED MODULE: ../core/src/entities/external-import-message.ts
|
|
57746
|
+
/**
|
|
57747
|
+
* The one wording for a Claude `@import` that resolves outside the working
|
|
57748
|
+
* directory, shared by lint (instruction/import-escape) and doctor
|
|
57749
|
+
* (external-claude-import) so the two cannot drift apart.
|
|
57750
|
+
*/ /**
|
|
57751
|
+
* - `outside-working-directory`: an absolute path outside the repository, or a
|
|
57752
|
+
* relative path that leaves it. Observed: in headless runs it was not loaded
|
|
57753
|
+
* (Claude Code 2.1.316, absolute path outside the working directory).
|
|
57754
|
+
* - `home`: a `~/` path. Claude Code's documentation says it is external, but
|
|
57755
|
+
* that case was not run in testing, so the message must not claim it was.
|
|
57756
|
+
*/ const MOVE_ADVICE = "Move the file into the repo, or import it from ~/.claude/CLAUDE.md instead.";
|
|
57757
|
+
const OBSERVATION_BY_KIND = {
|
|
57758
|
+
"outside-working-directory": "In headless, `-p` or CI runs it was not loaded in testing (Claude Code 2.1.316).",
|
|
57759
|
+
home: "This `~/` case is docs-based and was not observed in testing."
|
|
57760
|
+
};
|
|
57761
|
+
function externalImportMessage(path, kind) {
|
|
57762
|
+
return `External import: \`${path}\` resolves outside the working directory. Claude Code loads it only after a one-time approval in an interactive session (docs: memory, 'Import additional files'). ${OBSERVATION_BY_KIND[kind]} ${MOVE_ADVICE}`;
|
|
57763
|
+
}
|
|
57764
|
+
|
|
57765
|
+
;// CONCATENATED MODULE: ../core/src/gateways/read-regular-file-no-follow.ts
|
|
57766
|
+
/**
|
|
57767
|
+
* Bounded, verified reads of a single regular file.
|
|
57768
|
+
*
|
|
57769
|
+
* Unlike `readFileNoFollow`, which throws, `readRegularFileNoFollow` reports
|
|
57770
|
+
* every refusal as a value, bounds the bytes it reads, and verifies the opened
|
|
57771
|
+
* file is the file it was asked to read.
|
|
57772
|
+
*/
|
|
57773
|
+
|
|
57774
|
+
function read_regular_file_no_follow_isEloopError(error) {
|
|
57775
|
+
return error instanceof Error && "code" in error && error.code === "ELOOP";
|
|
57776
|
+
}
|
|
57777
|
+
const nodeFsPort = {
|
|
57778
|
+
open: (path, flags)=>(0,promises_.open)(path, flags),
|
|
57779
|
+
lstat: (path)=>(0,promises_.lstat)(path)
|
|
57780
|
+
};
|
|
57781
|
+
const read_regular_file_no_follow_READ_CHUNK_BYTES = 64 * 1024;
|
|
57782
|
+
/**
|
|
57783
|
+
* Opening a named pipe for reading waits for a writer. With O_NONBLOCK the
|
|
57784
|
+
* open returns at once, and the check on the open descriptor then refuses
|
|
57785
|
+
* anything that is not a regular file. It has no effect on a regular file.
|
|
57786
|
+
*/ const NON_BLOCKING = external_node_fs_.constants.O_NONBLOCK ?? 0;
|
|
57787
|
+
function isSameFile(a, b) {
|
|
57788
|
+
return a.dev === b.dev && a.ino === b.ino;
|
|
57789
|
+
}
|
|
57790
|
+
function platformHasKernelNoFollow() {
|
|
57791
|
+
return typeof external_node_fs_.constants.O_NOFOLLOW === "number" && external_node_fs_.constants.O_NOFOLLOW !== 0;
|
|
57792
|
+
}
|
|
57793
|
+
function hasKernelNoFollow(fs) {
|
|
57794
|
+
return fs.kernelNoFollow ?? platformHasKernelNoFollow();
|
|
57795
|
+
}
|
|
57796
|
+
/**
|
|
57797
|
+
* Without kernel O_NOFOLLOW support, a symlink is rejected up front with lstat.
|
|
57798
|
+
*/ async function isSymlinkWithoutKernelSupport(fs, filePath) {
|
|
57799
|
+
if (hasKernelNoFollow(fs)) {
|
|
57800
|
+
return false;
|
|
57801
|
+
}
|
|
57802
|
+
return (await fs.lstat(filePath)).isSymbolicLink();
|
|
57803
|
+
}
|
|
57804
|
+
async function openForBoundedRead(fs, filePath) {
|
|
57805
|
+
try {
|
|
57806
|
+
if (await isSymlinkWithoutKernelSupport(fs, filePath)) {
|
|
57807
|
+
return {
|
|
57808
|
+
failure: "symlink"
|
|
57809
|
+
};
|
|
56955
57810
|
}
|
|
57811
|
+
const flags = (hasKernelNoFollow(fs) ? external_node_fs_.constants.O_RDONLY | external_node_fs_.constants.O_NOFOLLOW : external_node_fs_.constants.O_RDONLY) | NON_BLOCKING;
|
|
57812
|
+
return {
|
|
57813
|
+
handle: await fs.open(filePath, flags)
|
|
57814
|
+
};
|
|
57815
|
+
} catch (error) {
|
|
57816
|
+
return {
|
|
57817
|
+
failure: read_regular_file_no_follow_isEloopError(error) ? "symlink" : "unreadable"
|
|
57818
|
+
};
|
|
57819
|
+
}
|
|
57820
|
+
}
|
|
57821
|
+
/**
|
|
57822
|
+
* Reads at most maxBytes + 1 bytes, so a file that grows after it was
|
|
57823
|
+
* measured can never make the read unbounded. Returns null when the file
|
|
57824
|
+
* holds more than maxBytes.
|
|
57825
|
+
*/ async function readAtMost(handle, maxBytes) {
|
|
57826
|
+
const chunks = [];
|
|
57827
|
+
let total = 0;
|
|
57828
|
+
while(true){
|
|
57829
|
+
const want = Math.min(read_regular_file_no_follow_READ_CHUNK_BYTES, maxBytes + 1 - total);
|
|
57830
|
+
const chunk = Buffer.alloc(want);
|
|
57831
|
+
const { bytesRead } = await handle.read(chunk, 0, want, null);
|
|
57832
|
+
if (bytesRead === 0) {
|
|
57833
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
57834
|
+
}
|
|
57835
|
+
chunks.push(chunk.subarray(0, bytesRead));
|
|
57836
|
+
total += bytesRead;
|
|
57837
|
+
if (total > maxBytes) {
|
|
57838
|
+
return null;
|
|
57839
|
+
}
|
|
57840
|
+
}
|
|
57841
|
+
}
|
|
57842
|
+
/**
|
|
57843
|
+
* Verifies the opened descriptor is a plain regular file that is still the
|
|
57844
|
+
* file at the path (and the file that was checked earlier, when an identity
|
|
57845
|
+
* was recorded). Returns the failure, or null when the file is acceptable.
|
|
57846
|
+
*/ async function checkOpenedFile(fs, handle, filePath, maxBytes, expectedIdentity) {
|
|
57847
|
+
const opened = await handle.stat();
|
|
57848
|
+
if (!opened.isFile()) {
|
|
57849
|
+
return "not-regular";
|
|
57850
|
+
}
|
|
57851
|
+
// Defense in depth: a hard link may alias a file outside the root. This
|
|
57852
|
+
// only fires locally (for example pnpm stores or `cp -al` trees).
|
|
57853
|
+
if (opened.nlink > 1) {
|
|
57854
|
+
return "hard-linked";
|
|
57855
|
+
}
|
|
57856
|
+
if (expectedIdentity !== undefined && !isSameFile(opened, expectedIdentity)) {
|
|
57857
|
+
return "identity-mismatch";
|
|
57858
|
+
}
|
|
57859
|
+
const atPath = await fs.lstat(filePath);
|
|
57860
|
+
if (!atPath.isFile() || !isSameFile(opened, atPath)) {
|
|
57861
|
+
return "identity-mismatch";
|
|
57862
|
+
}
|
|
57863
|
+
return opened.size > maxBytes ? "too-large" : null;
|
|
57864
|
+
}
|
|
57865
|
+
/**
|
|
57866
|
+
* Applies the checks of readRegularFileNoFollow (no symlink at the final
|
|
57867
|
+
* component, a plain regular file, a single hard link, the file now at the
|
|
57868
|
+
* path, within the size cap) to a file without reading its content. Returns
|
|
57869
|
+
* the refusal, or null when the file would be read.
|
|
57870
|
+
*/ async function checkRegularFileNoFollow(filePath, maxBytes, options = {}) {
|
|
57871
|
+
const fs = options.fs ?? nodeFsPort;
|
|
57872
|
+
const opened = await openForBoundedRead(fs, filePath);
|
|
57873
|
+
if ("failure" in opened) {
|
|
57874
|
+
return opened.failure;
|
|
57875
|
+
}
|
|
57876
|
+
const { handle } = opened;
|
|
57877
|
+
try {
|
|
57878
|
+
return await checkOpenedFile(fs, handle, filePath, maxBytes, options.expectedIdentity);
|
|
57879
|
+
} catch {
|
|
57880
|
+
return "unreadable";
|
|
57881
|
+
} finally{
|
|
57882
|
+
await handle.close().catch(()=>undefined);
|
|
57883
|
+
}
|
|
57884
|
+
}
|
|
57885
|
+
/**
|
|
57886
|
+
* Reads a regular file without following a symlink at the final path
|
|
57887
|
+
* component, never throwing for an unacceptable file.
|
|
57888
|
+
*
|
|
57889
|
+
* Opens with O_NOFOLLOW (an ELOOP is a "symlink" outcome), checks on the open
|
|
57890
|
+
* descriptor that it is a plain regular file within the size cap and the same
|
|
57891
|
+
* file that is currently at the path, then reads at most maxBytes + 1 bytes.
|
|
57892
|
+
* Pass the real (symlink-free) path of a file inside the root: O_NOFOLLOW
|
|
57893
|
+
* protects only the final component.
|
|
57894
|
+
*/ async function readRegularFileNoFollow(filePath, maxBytes, options = {}) {
|
|
57895
|
+
const fs = options.fs ?? nodeFsPort;
|
|
57896
|
+
const opened = await openForBoundedRead(fs, filePath);
|
|
57897
|
+
if ("failure" in opened) {
|
|
57898
|
+
return {
|
|
57899
|
+
ok: false,
|
|
57900
|
+
reason: opened.failure
|
|
57901
|
+
};
|
|
57902
|
+
}
|
|
57903
|
+
const { handle } = opened;
|
|
57904
|
+
try {
|
|
57905
|
+
const failure = await checkOpenedFile(fs, handle, filePath, maxBytes, options.expectedIdentity);
|
|
57906
|
+
if (failure !== null) {
|
|
57907
|
+
return {
|
|
57908
|
+
ok: false,
|
|
57909
|
+
reason: failure
|
|
57910
|
+
};
|
|
57911
|
+
}
|
|
57912
|
+
const content = await readAtMost(handle, maxBytes);
|
|
57913
|
+
return content === null ? {
|
|
57914
|
+
ok: false,
|
|
57915
|
+
reason: "too-large"
|
|
57916
|
+
} : {
|
|
57917
|
+
ok: true,
|
|
57918
|
+
content
|
|
57919
|
+
};
|
|
57920
|
+
} catch {
|
|
57921
|
+
return {
|
|
57922
|
+
ok: false,
|
|
57923
|
+
reason: "unreadable"
|
|
57924
|
+
};
|
|
57925
|
+
} finally{
|
|
57926
|
+
await handle.close().catch(()=>undefined);
|
|
56956
57927
|
}
|
|
56957
|
-
return {
|
|
56958
|
-
line,
|
|
56959
|
-
column
|
|
56960
|
-
};
|
|
56961
57928
|
}
|
|
56962
57929
|
|
|
56963
|
-
;// CONCATENATED MODULE: ../core/src/lib/
|
|
57930
|
+
;// CONCATENATED MODULE: ../core/src/lib/import-file-location.ts
|
|
56964
57931
|
/**
|
|
56965
|
-
*
|
|
57932
|
+
* Maps what the file system reported about an import target to the file to
|
|
57933
|
+
* read, or to the import failure it means. Pure: no I/O.
|
|
56966
57934
|
*/
|
|
57935
|
+
function toPosix(pathValue) {
|
|
57936
|
+
return pathValue.split(external_node_path_.sep).join("/");
|
|
57937
|
+
}
|
|
57938
|
+
/**
|
|
57939
|
+
* The repository-relative path of an absolute target that lies strictly
|
|
57940
|
+
* inside one of the roots, or the "absolute" failure when it does not.
|
|
57941
|
+
*/ function relativeToAnyRoot(absoluteTarget, repoRoots) {
|
|
57942
|
+
for (const root of repoRoots){
|
|
57943
|
+
const inside = (0,external_node_path_.relative)(root, absoluteTarget);
|
|
57944
|
+
const isInside = inside !== "" && inside !== ".." && !inside.startsWith(`..${external_node_path_.sep}`);
|
|
57945
|
+
if (isInside && !(0,external_node_path_.isAbsolute)(inside)) {
|
|
57946
|
+
return {
|
|
57947
|
+
ok: true,
|
|
57948
|
+
relativePath: toPosix(inside)
|
|
57949
|
+
};
|
|
57950
|
+
}
|
|
57951
|
+
}
|
|
57952
|
+
return {
|
|
57953
|
+
ok: false,
|
|
57954
|
+
status: "absolute"
|
|
57955
|
+
};
|
|
57956
|
+
}
|
|
57957
|
+
/**
|
|
57958
|
+
* Maps a path inspection to a usable file, or to the import failure it means.
|
|
57959
|
+
* Only a path that is not there is "unresolved". A path that only resolves
|
|
57960
|
+
* outside the repository is a symlink violation, one the system would not let
|
|
57961
|
+
* the process look at is "unreadable", and one that is there but cannot be
|
|
57962
|
+
* used as a file (a directory, a named pipe, a symbolic link that loops) is
|
|
57963
|
+
* "not-regular".
|
|
57964
|
+
*/ function toLocatedFile(rootReal, inspection) {
|
|
57965
|
+
switch(inspection.status){
|
|
57966
|
+
case "outside-root":
|
|
57967
|
+
return {
|
|
57968
|
+
ok: false,
|
|
57969
|
+
status: "symlink"
|
|
57970
|
+
};
|
|
57971
|
+
case "unreadable":
|
|
57972
|
+
return {
|
|
57973
|
+
ok: false,
|
|
57974
|
+
status: "unreadable"
|
|
57975
|
+
};
|
|
57976
|
+
case "unsupported":
|
|
57977
|
+
return {
|
|
57978
|
+
ok: false,
|
|
57979
|
+
status: "not-regular"
|
|
57980
|
+
};
|
|
57981
|
+
case "missing":
|
|
57982
|
+
return {
|
|
57983
|
+
ok: false,
|
|
57984
|
+
status: "unresolved"
|
|
57985
|
+
};
|
|
57986
|
+
case "ok":
|
|
57987
|
+
return locateInspectedFile(rootReal, inspection);
|
|
57988
|
+
}
|
|
57989
|
+
}
|
|
57990
|
+
function locateInspectedFile(rootReal, inspection) {
|
|
57991
|
+
if (inspection.kind !== "file") {
|
|
57992
|
+
return {
|
|
57993
|
+
ok: false,
|
|
57994
|
+
status: "not-regular"
|
|
57995
|
+
};
|
|
57996
|
+
}
|
|
57997
|
+
return {
|
|
57998
|
+
ok: true,
|
|
57999
|
+
real: toPosix((0,external_node_path_.relative)(rootReal, inspection.real)),
|
|
58000
|
+
realAbsolute: inspection.real,
|
|
58001
|
+
identity: inspection.identity
|
|
58002
|
+
};
|
|
58003
|
+
}
|
|
58004
|
+
function statusForReadFailure(reason) {
|
|
58005
|
+
switch(reason){
|
|
58006
|
+
case "symlink":
|
|
58007
|
+
return "symlink";
|
|
58008
|
+
case "too-large":
|
|
58009
|
+
return "size-exceeded";
|
|
58010
|
+
case "not-regular":
|
|
58011
|
+
return "not-regular";
|
|
58012
|
+
case "hard-linked":
|
|
58013
|
+
return "hard-linked";
|
|
58014
|
+
case "unreadable":
|
|
58015
|
+
case "identity-mismatch":
|
|
58016
|
+
return "unreadable";
|
|
58017
|
+
}
|
|
58018
|
+
}
|
|
56967
58019
|
|
|
56968
|
-
|
|
56969
|
-
|
|
56970
|
-
|
|
56971
|
-
|
|
56972
|
-
|
|
56973
|
-
|
|
56974
|
-
|
|
58020
|
+
;// CONCATENATED MODULE: ../core/src/lib/import-tokens.ts
|
|
58021
|
+
/**
|
|
58022
|
+
* The single tokenizer for Claude `@` imports, shared by import expansion and
|
|
58023
|
+
* the doctor edge parser, so lint and doctor agree on what an import is.
|
|
58024
|
+
*
|
|
58025
|
+
* Rules (documented for Claude Code, and for the first four observed on 2.1.316):
|
|
58026
|
+
* - A token is `@` at the start of a line followed by a path: a relative or
|
|
58027
|
+
* bare file name (`@./a.md`, `@AGENTS.md`), an absolute path, or `~/`.
|
|
58028
|
+
* - A backslash-escaped space stays in the path; the path ends at the first
|
|
58029
|
+
* unescaped space.
|
|
58030
|
+
* - A quoted path (`@"..."`, `@'...'`) is not an import.
|
|
58031
|
+
* - Inside a code span or a fenced block nothing is an import.
|
|
58032
|
+
* - A token whose last segment has no extension and that does not begin with a
|
|
58033
|
+
* path prefix (`@types/node`, `@no-such-thing`) is not clearly a path
|
|
58034
|
+
* (`looksLikePath` is false): if it resolves to a file it is an import, and
|
|
58035
|
+
* if it does not, nothing should be reported.
|
|
58036
|
+
*/ /**
|
|
58037
|
+
* Receives the number of steps a scan has just taken: lines looked at,
|
|
58038
|
+
* characters walked, lookups made. It exists so a test can pin the cost of a
|
|
58039
|
+
* scan exactly, without timing it.
|
|
58040
|
+
*/ const ignoreSteps = ()=>undefined;
|
|
58041
|
+
/**
|
|
58042
|
+
* The lines that can close fences of one character and length, in file order.
|
|
58043
|
+
* The cursor moves forward only: a lookup never goes back to a line before the
|
|
58044
|
+
* one an earlier lookup stopped at, so all lookups together cost one pass.
|
|
58045
|
+
*/ class ClosingLines {
|
|
58046
|
+
starts = [];
|
|
58047
|
+
ends = [];
|
|
58048
|
+
cursor = 0;
|
|
58049
|
+
add(start, end) {
|
|
58050
|
+
this.starts.push(start);
|
|
58051
|
+
this.ends.push(end);
|
|
58052
|
+
}
|
|
58053
|
+
firstEndFrom(from, countSteps) {
|
|
58054
|
+
const startedAt = this.cursor;
|
|
58055
|
+
while(this.cursor < this.starts.length && (this.starts[this.cursor] ?? 0) < from){
|
|
58056
|
+
this.cursor += 1;
|
|
58057
|
+
}
|
|
58058
|
+
countSteps(this.cursor - startedAt + 1);
|
|
58059
|
+
return this.ends[this.cursor];
|
|
58060
|
+
}
|
|
58061
|
+
}
|
|
58062
|
+
/** The closing lines, indexed by fence character and length. */ class ClosingIndex {
|
|
58063
|
+
byKey = new Map();
|
|
58064
|
+
add(key, start, end) {
|
|
58065
|
+
const existing = this.byKey.get(key);
|
|
58066
|
+
if (existing) {
|
|
58067
|
+
existing.add(start, end);
|
|
58068
|
+
return;
|
|
58069
|
+
}
|
|
58070
|
+
const created = new ClosingLines();
|
|
58071
|
+
created.add(start, end);
|
|
58072
|
+
this.byKey.set(key, created);
|
|
58073
|
+
}
|
|
58074
|
+
get(key) {
|
|
58075
|
+
return this.byKey.get(key);
|
|
58076
|
+
}
|
|
58077
|
+
}
|
|
58078
|
+
/**
|
|
58079
|
+
* Line-start `@` and a path of non-space characters, where `\ ` (a backslash
|
|
58080
|
+
* and a space) belongs to the path. The first character is not a space or `@`.
|
|
58081
|
+
*/ const HARD_IMPORT_RE = /^@(?![\s@])((?:\\ |\S)+)/gm;
|
|
56975
58082
|
const INLINE_CODE_RE = /`+[^`\r\n]*`+/g;
|
|
56976
|
-
|
|
58083
|
+
const import_tokens_LINE_FEED = 0x0a;
|
|
58084
|
+
const CARRIAGE_RETURN = 0x0d;
|
|
58085
|
+
const LINE_SEPARATOR = 0x2028;
|
|
58086
|
+
const PARAGRAPH_SEPARATOR = 0x2029;
|
|
58087
|
+
const SPACE = 0x20;
|
|
58088
|
+
const TAB = 0x09;
|
|
58089
|
+
const BACKTICK = 0x60;
|
|
58090
|
+
const TILDE = 0x7e;
|
|
58091
|
+
const MIN_FENCE_LENGTH = 3;
|
|
58092
|
+
const QUOTE_OPENERS = new Set([
|
|
58093
|
+
'"',
|
|
58094
|
+
"'",
|
|
58095
|
+
"`"
|
|
58096
|
+
]);
|
|
58097
|
+
const PATH_PREFIXES = [
|
|
58098
|
+
"./",
|
|
58099
|
+
"../",
|
|
58100
|
+
"/",
|
|
58101
|
+
"~/"
|
|
58102
|
+
];
|
|
58103
|
+
const EXTENSION_RE = /\.[A-Za-z0-9]+$/;
|
|
58104
|
+
function isLineTerminator(code) {
|
|
58105
|
+
return code === import_tokens_LINE_FEED || code === CARRIAGE_RETURN || code === LINE_SEPARATOR || code === PARAGRAPH_SEPARATOR;
|
|
58106
|
+
}
|
|
58107
|
+
function isLineStart(content, index) {
|
|
58108
|
+
return index === 0 || isLineTerminator(content.charCodeAt(index - 1));
|
|
58109
|
+
}
|
|
58110
|
+
function fenceRunLength(content, start) {
|
|
58111
|
+
const code = content.charCodeAt(start);
|
|
58112
|
+
if (code !== BACKTICK && code !== TILDE) {
|
|
58113
|
+
return 0;
|
|
58114
|
+
}
|
|
58115
|
+
let end = start + 1;
|
|
58116
|
+
while(content.charCodeAt(end) === code){
|
|
58117
|
+
end += 1;
|
|
58118
|
+
}
|
|
58119
|
+
return end - start;
|
|
58120
|
+
}
|
|
58121
|
+
function skipSpacesAndTabs(content, from) {
|
|
58122
|
+
let index = from;
|
|
58123
|
+
while(content.charCodeAt(index) === SPACE || content.charCodeAt(index) === TAB){
|
|
58124
|
+
index += 1;
|
|
58125
|
+
}
|
|
58126
|
+
return index;
|
|
58127
|
+
}
|
|
58128
|
+
function closingKey(code, length) {
|
|
58129
|
+
return length * 2 + (code === TILDE ? 1 : 0);
|
|
58130
|
+
}
|
|
58131
|
+
/**
|
|
58132
|
+
* One pass over the lines that begin with three or more fence characters.
|
|
58133
|
+
* A line that is only the fence run and trailing spaces or tabs can close a
|
|
58134
|
+
* fence of exactly that length, so those lines are indexed by character and
|
|
58135
|
+
* length, in file order.
|
|
58136
|
+
*/ function collectFenceLines(content, countSteps) {
|
|
58137
|
+
const fenceLines = {
|
|
58138
|
+
starts: [],
|
|
58139
|
+
lengths: []
|
|
58140
|
+
};
|
|
58141
|
+
const closers = new ClosingIndex();
|
|
58142
|
+
for(let index = 0; index < content.length; index += 1){
|
|
58143
|
+
if (!isLineStart(content, index)) {
|
|
58144
|
+
continue;
|
|
58145
|
+
}
|
|
58146
|
+
const length = fenceRunLength(content, index);
|
|
58147
|
+
countSteps(length + 1);
|
|
58148
|
+
if (length < MIN_FENCE_LENGTH) {
|
|
58149
|
+
continue;
|
|
58150
|
+
}
|
|
58151
|
+
fenceLines.starts.push(index);
|
|
58152
|
+
fenceLines.lengths.push(length);
|
|
58153
|
+
const trailingEnd = skipSpacesAndTabs(content, index + length);
|
|
58154
|
+
countSteps(trailingEnd - index - length);
|
|
58155
|
+
if (trailingEnd >= content.length || isLineTerminator(content.charCodeAt(trailingEnd))) {
|
|
58156
|
+
closers.add(closingKey(content.charCodeAt(index), length), index, trailingEnd);
|
|
58157
|
+
}
|
|
58158
|
+
}
|
|
56977
58159
|
return {
|
|
56978
|
-
|
|
56979
|
-
|
|
56980
|
-
maxEdges: overrides?.maxEdges ?? DEFAULT_MAX_EDGES,
|
|
56981
|
-
maxEmittedBytes: overrides?.maxEmittedBytes ?? DEFAULT_MAX_EMITTED_BYTES,
|
|
56982
|
-
maxFileBytes: overrides?.maxFileBytes ?? DEFAULT_MAX_FILE_BYTES
|
|
58160
|
+
fenceLines,
|
|
58161
|
+
closers
|
|
56983
58162
|
};
|
|
56984
58163
|
}
|
|
56985
|
-
|
|
56986
|
-
|
|
58164
|
+
/**
|
|
58165
|
+
* Finds the first carriage return or line feed at or after `from`. Calls
|
|
58166
|
+
* arrive in increasing order, so the last answer is reused while it still
|
|
58167
|
+
* applies and the whole scan reads each character once.
|
|
58168
|
+
*/ function createLineBreakFinder(content, countSteps) {
|
|
58169
|
+
let answeredFrom = -1;
|
|
58170
|
+
let answer = -1;
|
|
58171
|
+
return (from)=>{
|
|
58172
|
+
if (answeredFrom <= from && answer >= from) {
|
|
58173
|
+
countSteps(1);
|
|
58174
|
+
return answer;
|
|
58175
|
+
}
|
|
58176
|
+
let index = from;
|
|
58177
|
+
while(index < content.length && content.charCodeAt(index) !== import_tokens_LINE_FEED && content.charCodeAt(index) !== CARRIAGE_RETURN){
|
|
58178
|
+
index += 1;
|
|
58179
|
+
}
|
|
58180
|
+
countSteps(index - from + 1);
|
|
58181
|
+
answeredFrom = from;
|
|
58182
|
+
answer = index;
|
|
58183
|
+
return index;
|
|
58184
|
+
};
|
|
58185
|
+
}
|
|
58186
|
+
/**
|
|
58187
|
+
* Where the body of a fence begins: the opening line must end in a line feed
|
|
58188
|
+
* or a carriage return and a line feed.
|
|
58189
|
+
*/ function fenceBodyStart(content, infoStart, findLineBreak) {
|
|
58190
|
+
const lineBreak = findLineBreak(infoStart);
|
|
58191
|
+
const code = content.charCodeAt(lineBreak);
|
|
58192
|
+
if (code === import_tokens_LINE_FEED) {
|
|
58193
|
+
return lineBreak + 1;
|
|
58194
|
+
}
|
|
58195
|
+
if (code === CARRIAGE_RETURN && content.charCodeAt(lineBreak + 1) === import_tokens_LINE_FEED) {
|
|
58196
|
+
return lineBreak + 2;
|
|
58197
|
+
}
|
|
58198
|
+
return undefined;
|
|
56987
58199
|
}
|
|
56988
|
-
|
|
56989
|
-
|
|
58200
|
+
/**
|
|
58201
|
+
* A fence is closed by the first line after it that is exactly as many fence
|
|
58202
|
+
* characters as the opener. When no such line follows, shorter lengths are
|
|
58203
|
+
* tried in turn, down to three.
|
|
58204
|
+
*/ function findClosingEnd(closers, code, fenceLength, bodyStart, countSteps) {
|
|
58205
|
+
for(let length = fenceLength; length >= MIN_FENCE_LENGTH; length -= 1){
|
|
58206
|
+
countSteps(1);
|
|
58207
|
+
const closing = closers.get(closingKey(code, length));
|
|
58208
|
+
const end = closing?.firstEndFrom(bodyStart, countSteps);
|
|
58209
|
+
if (end !== undefined) {
|
|
58210
|
+
return end;
|
|
58211
|
+
}
|
|
58212
|
+
}
|
|
58213
|
+
return undefined;
|
|
56990
58214
|
}
|
|
56991
|
-
|
|
56992
|
-
|
|
56993
|
-
|
|
56994
|
-
|
|
58215
|
+
/**
|
|
58216
|
+
* Finds the fenced blocks in time linear in the content. A fence that is
|
|
58217
|
+
* never closed hides nothing, so a file of unclosed fences costs one lookup
|
|
58218
|
+
* per fence line.
|
|
58219
|
+
*/ function findFencedRegions(content, countSteps) {
|
|
58220
|
+
const { fenceLines, closers } = collectFenceLines(content, countSteps);
|
|
58221
|
+
const findLineBreak = createLineBreakFinder(content, countSteps);
|
|
58222
|
+
const regions = {
|
|
58223
|
+
starts: [],
|
|
58224
|
+
ends: []
|
|
58225
|
+
};
|
|
58226
|
+
let scanFrom = 0;
|
|
58227
|
+
for(let line = 0; line < fenceLines.starts.length; line += 1){
|
|
58228
|
+
const start = fenceLines.starts[line] ?? 0;
|
|
58229
|
+
const length = fenceLines.lengths[line] ?? 0;
|
|
58230
|
+
countSteps(1);
|
|
58231
|
+
if (start < scanFrom) {
|
|
56995
58232
|
continue;
|
|
56996
58233
|
}
|
|
56997
|
-
|
|
58234
|
+
const bodyStart = fenceBodyStart(content, start + length, findLineBreak);
|
|
58235
|
+
if (bodyStart === undefined) {
|
|
56998
58236
|
continue;
|
|
56999
58237
|
}
|
|
57000
|
-
|
|
57001
|
-
|
|
57002
|
-
|
|
57003
|
-
|
|
58238
|
+
const end = findClosingEnd(closers, content.charCodeAt(start), length, bodyStart, countSteps);
|
|
58239
|
+
if (end !== undefined) {
|
|
58240
|
+
regions.starts.push(start);
|
|
58241
|
+
regions.ends.push(end);
|
|
58242
|
+
scanFrom = end;
|
|
58243
|
+
}
|
|
57004
58244
|
}
|
|
57005
58245
|
return regions;
|
|
57006
58246
|
}
|
|
57007
|
-
|
|
57008
|
-
|
|
57009
|
-
|
|
57010
|
-
|
|
57011
|
-
|
|
57012
|
-
|
|
57013
|
-
];
|
|
58247
|
+
/**
|
|
58248
|
+
* Answers whether an index falls inside any region, in logarithmic time per
|
|
58249
|
+
* question: the regions are ordered by start, each carries the furthest end
|
|
58250
|
+
* reached so far, and a binary search finds the last region that starts at or
|
|
58251
|
+
* before the index.
|
|
58252
|
+
*/ function createRegionLookup(regions, countSteps) {
|
|
58253
|
+
const furthestEnds = [];
|
|
58254
|
+
let furthest = 0;
|
|
58255
|
+
for (const end of regions.ends){
|
|
58256
|
+
furthest = Math.max(furthest, end);
|
|
58257
|
+
furthestEnds.push(furthest);
|
|
58258
|
+
}
|
|
58259
|
+
countSteps(furthestEnds.length);
|
|
58260
|
+
return (index)=>{
|
|
58261
|
+
let low = 0;
|
|
58262
|
+
let high = regions.starts.length - 1;
|
|
58263
|
+
let lastStartingAtOrBefore = -1;
|
|
58264
|
+
while(low <= high){
|
|
58265
|
+
countSteps(1);
|
|
58266
|
+
const middle = low + high >> 1;
|
|
58267
|
+
if ((regions.starts[middle] ?? 0) <= index) {
|
|
58268
|
+
lastStartingAtOrBefore = middle;
|
|
58269
|
+
low = middle + 1;
|
|
58270
|
+
} else {
|
|
58271
|
+
high = middle - 1;
|
|
58272
|
+
}
|
|
58273
|
+
}
|
|
58274
|
+
return lastStartingAtOrBefore >= 0 && (furthestEnds[lastStartingAtOrBefore] ?? 0) > index;
|
|
58275
|
+
};
|
|
57014
58276
|
}
|
|
57015
|
-
function
|
|
57016
|
-
const
|
|
57017
|
-
|
|
57018
|
-
|
|
57019
|
-
|
|
57020
|
-
|
|
58277
|
+
function findInlineRegions(content, isInsideFence, countSteps) {
|
|
58278
|
+
const regions = {
|
|
58279
|
+
starts: [],
|
|
58280
|
+
ends: []
|
|
58281
|
+
};
|
|
58282
|
+
for (const match of content.matchAll(INLINE_CODE_RE)){
|
|
58283
|
+
countSteps(1);
|
|
58284
|
+
const start = match.index ?? 0;
|
|
58285
|
+
if (!isInsideFence(start)) {
|
|
58286
|
+
regions.starts.push(start);
|
|
58287
|
+
regions.ends.push(start + match[0].length);
|
|
57021
58288
|
}
|
|
57022
|
-
|
|
57023
|
-
|
|
57024
|
-
|
|
58289
|
+
}
|
|
58290
|
+
return regions;
|
|
58291
|
+
}
|
|
58292
|
+
/** Merges two lists that are each ordered by start into one. */ function mergeRegionLists(first, second, countSteps) {
|
|
58293
|
+
countSteps(first.starts.length + second.starts.length);
|
|
58294
|
+
const merged = {
|
|
58295
|
+
starts: [],
|
|
58296
|
+
ends: []
|
|
58297
|
+
};
|
|
58298
|
+
let a = 0;
|
|
58299
|
+
let b = 0;
|
|
58300
|
+
while(a < first.starts.length || b < second.starts.length){
|
|
58301
|
+
const takeFirst = b >= second.starts.length || a < first.starts.length && (first.starts[a] ?? 0) <= (second.starts[b] ?? 0);
|
|
58302
|
+
const source = takeFirst ? first : second;
|
|
58303
|
+
const position = takeFirst ? a : b;
|
|
58304
|
+
merged.starts.push(source.starts[position] ?? 0);
|
|
58305
|
+
merged.ends.push(source.ends[position] ?? 0);
|
|
58306
|
+
if (takeFirst) {
|
|
58307
|
+
a += 1;
|
|
58308
|
+
} else {
|
|
58309
|
+
b += 1;
|
|
57025
58310
|
}
|
|
57026
|
-
|
|
58311
|
+
}
|
|
58312
|
+
return merged;
|
|
58313
|
+
}
|
|
58314
|
+
function findCodeRegions(content, countSteps) {
|
|
58315
|
+
const fenced = findFencedRegions(content, countSteps);
|
|
58316
|
+
const inline = findInlineRegions(content, createRegionLookup(fenced, countSteps), countSteps);
|
|
58317
|
+
return mergeRegionLists(fenced, inline, countSteps);
|
|
58318
|
+
}
|
|
58319
|
+
function looksLikePath(rawTarget) {
|
|
58320
|
+
const lastSegment = rawTarget.slice(rawTarget.lastIndexOf("/") + 1);
|
|
58321
|
+
return PATH_PREFIXES.some((prefix)=>rawTarget.startsWith(prefix)) || EXTENSION_RE.test(lastSegment);
|
|
58322
|
+
}
|
|
58323
|
+
function isQuoted(rawTarget) {
|
|
58324
|
+
return QUOTE_OPENERS.has(rawTarget.charAt(0));
|
|
58325
|
+
}
|
|
58326
|
+
/**
|
|
58327
|
+
* Finds the Claude `@path` import tokens in markdown content, ignoring
|
|
58328
|
+
* fenced and inline code, quoted paths, and anything not at the start of a
|
|
58329
|
+
* line.
|
|
58330
|
+
*/ function findImportTokens(content, countSteps = ignoreSteps) {
|
|
58331
|
+
const isInsideCode = createRegionLookup(findCodeRegions(content, countSteps), countSteps);
|
|
58332
|
+
const tokens = [];
|
|
58333
|
+
for (const match of content.matchAll(HARD_IMPORT_RE)){
|
|
58334
|
+
countSteps(1);
|
|
58335
|
+
const start = match.index ?? 0;
|
|
58336
|
+
const target = match[1] ?? "";
|
|
58337
|
+
if (isInsideCode(start) || isQuoted(target)) {
|
|
57027
58338
|
continue;
|
|
57028
58339
|
}
|
|
58340
|
+
const rawTarget = target.replaceAll("\\ ", " ");
|
|
57029
58341
|
tokens.push({
|
|
57030
|
-
start
|
|
57031
|
-
end:
|
|
57032
|
-
rawTarget
|
|
58342
|
+
start,
|
|
58343
|
+
end: start + match[0].length,
|
|
58344
|
+
rawTarget,
|
|
58345
|
+
looksLikePath: looksLikePath(rawTarget)
|
|
57033
58346
|
});
|
|
57034
58347
|
}
|
|
57035
58348
|
return tokens;
|
|
57036
58349
|
}
|
|
58350
|
+
|
|
58351
|
+
;// CONCATENATED MODULE: ../core/src/lib/instruction-import-expand.ts
|
|
58352
|
+
/**
|
|
58353
|
+
* Claude `@` import expander that builds an ordered EffectiveDocument.
|
|
58354
|
+
*
|
|
58355
|
+
* It reads the file system through an injectable port (real paths, bounded
|
|
58356
|
+
* reads of regular files, no symlink followed at the final component), so it
|
|
58357
|
+
* is deterministic for a given tree but not pure.
|
|
58358
|
+
*/
|
|
58359
|
+
|
|
58360
|
+
|
|
58361
|
+
|
|
58362
|
+
|
|
58363
|
+
|
|
58364
|
+
|
|
58365
|
+
|
|
58366
|
+
|
|
58367
|
+
|
|
58368
|
+
const DEFAULT_MAX_IMPORT_DEPTH = 4;
|
|
58369
|
+
const DEFAULT_MAX_UNIQUE_FILES = 64;
|
|
58370
|
+
const DEFAULT_MAX_EDGES = 256;
|
|
58371
|
+
const DEFAULT_MAX_EMITTED_BYTES = 512_000;
|
|
58372
|
+
const DEFAULT_MAX_FILE_BYTES = MAX_INSTRUCTION_FILE_BYTES;
|
|
58373
|
+
const nodeImportFileSystem = {
|
|
58374
|
+
inspect: inspectPathWithinRoot,
|
|
58375
|
+
read: (realPath, maxBytes, expectedIdentity)=>readRegularFileNoFollow(realPath, maxBytes, {
|
|
58376
|
+
expectedIdentity
|
|
58377
|
+
})
|
|
58378
|
+
};
|
|
58379
|
+
function resolveLimits(overrides) {
|
|
58380
|
+
return {
|
|
58381
|
+
maxDepth: overrides?.maxDepth ?? DEFAULT_MAX_IMPORT_DEPTH,
|
|
58382
|
+
maxUniqueFiles: overrides?.maxUniqueFiles ?? DEFAULT_MAX_UNIQUE_FILES,
|
|
58383
|
+
maxEdges: overrides?.maxEdges ?? DEFAULT_MAX_EDGES,
|
|
58384
|
+
maxEmittedBytes: overrides?.maxEmittedBytes ?? DEFAULT_MAX_EMITTED_BYTES,
|
|
58385
|
+
maxFileBytes: overrides?.maxFileBytes ?? DEFAULT_MAX_FILE_BYTES
|
|
58386
|
+
};
|
|
58387
|
+
}
|
|
58388
|
+
function toPosixRelative(pathValue) {
|
|
58389
|
+
return pathValue.split(external_node_path_.sep).join("/");
|
|
58390
|
+
}
|
|
57037
58391
|
function rebaseSegments(segments, offset) {
|
|
57038
58392
|
return segments.map((segment)=>({
|
|
57039
58393
|
...segment,
|
|
@@ -57065,6 +58419,8 @@ function createLiteralSegment(contentOffset, sourcePath, sourceStart, text, impo
|
|
|
57065
58419
|
const RULE_ID_BY_STATUS = {
|
|
57066
58420
|
unresolved: "instruction/import-unresolved",
|
|
57067
58421
|
"not-regular": "instruction/import-unresolved",
|
|
58422
|
+
"hard-linked": "instruction/import-unresolved",
|
|
58423
|
+
unreadable: "instruction/import-unresolved",
|
|
57068
58424
|
escape: "instruction/import-escape",
|
|
57069
58425
|
absolute: "instruction/import-escape",
|
|
57070
58426
|
home: "instruction/import-escape",
|
|
@@ -57076,14 +58432,16 @@ const RULE_ID_BY_STATUS = {
|
|
|
57076
58432
|
};
|
|
57077
58433
|
const FAILURE_MESSAGE_BY_STATUS = {
|
|
57078
58434
|
unresolved: (rawTarget)=>`Import target could not be resolved: ${rawTarget}`,
|
|
57079
|
-
escape: (rawTarget)
|
|
57080
|
-
absolute: (rawTarget)
|
|
57081
|
-
home: (rawTarget)
|
|
58435
|
+
escape: (rawTarget)=>externalImportMessage(rawTarget, "outside-working-directory"),
|
|
58436
|
+
absolute: (rawTarget)=>externalImportMessage(rawTarget, "outside-working-directory"),
|
|
58437
|
+
home: (rawTarget)=>externalImportMessage(rawTarget, "home"),
|
|
57082
58438
|
symlink: (rawTarget)=>`Import target path contains a symbolic link: ${rawTarget}`,
|
|
57083
58439
|
cycle: (rawTarget)=>`Import cycle detected while resolving: ${rawTarget}`,
|
|
57084
58440
|
"depth-exceeded": (rawTarget)=>`Import depth limit exceeded while resolving: ${rawTarget}`,
|
|
57085
58441
|
"size-exceeded": (rawTarget)=>`Import expansion size or graph limit exceeded while resolving: ${rawTarget}`,
|
|
57086
58442
|
"not-regular": (rawTarget)=>`Import target is not a regular file: ${rawTarget}`,
|
|
58443
|
+
"hard-linked": (rawTarget)=>`Import target has more than one hard link and is not read: ${rawTarget}`,
|
|
58444
|
+
unreadable: (rawTarget)=>`Import target could not be read, for example because access was denied or too many files were open: ${rawTarget}`,
|
|
57087
58445
|
resolved: ()=>""
|
|
57088
58446
|
};
|
|
57089
58447
|
function ruleIdForStatus(status) {
|
|
@@ -57108,7 +58466,17 @@ function classifyPathError(error) {
|
|
|
57108
58466
|
}
|
|
57109
58467
|
return "unresolved";
|
|
57110
58468
|
}
|
|
57111
|
-
|
|
58469
|
+
/**
|
|
58470
|
+
* Classifies an import target relative to its importer without touching the
|
|
58471
|
+
* file system: home-directory targets, absolute targets outside every given
|
|
58472
|
+
* root, and repository-escaping targets are rejected as external; anything
|
|
58473
|
+
* else yields a repository-relative POSIX path.
|
|
58474
|
+
*
|
|
58475
|
+
* An absolute target inside one of `repoRoots` is valid, because Claude Code
|
|
58476
|
+
* loads absolute paths inside the working directory (observed on Claude Code
|
|
58477
|
+
* 2.1.316). Pass every spelling of the root (its path and its real path). With
|
|
58478
|
+
* no roots, every absolute target is rejected.
|
|
58479
|
+
*/ function normalizeRelativeWithinRoot(importerRelativePath, rawTarget, repoRoots = []) {
|
|
57112
58480
|
if (rawTarget === "~" || rawTarget.startsWith("~/") || rawTarget.startsWith("~\\")) {
|
|
57113
58481
|
return {
|
|
57114
58482
|
ok: false,
|
|
@@ -57116,10 +58484,7 @@ function normalizeRelativeWithinRoot(importerRelativePath, rawTarget) {
|
|
|
57116
58484
|
};
|
|
57117
58485
|
}
|
|
57118
58486
|
if ((0,external_node_path_.isAbsolute)(rawTarget)) {
|
|
57119
|
-
return
|
|
57120
|
-
ok: false,
|
|
57121
|
-
status: "absolute"
|
|
57122
|
-
};
|
|
58487
|
+
return relativeToAnyRoot(rawTarget, repoRoots);
|
|
57123
58488
|
}
|
|
57124
58489
|
const importerDir = (0,external_node_path_.dirname)(importerRelativePath);
|
|
57125
58490
|
const joined = (0,external_node_path_.normalize)((0,external_node_path_.join)(importerDir, rawTarget));
|
|
@@ -57149,17 +58514,77 @@ function isUnsafeRootPath(rootRelativePath) {
|
|
|
57149
58514
|
* Mutations stay on the session instance rather than shared parameter bags.
|
|
57150
58515
|
*/ class ExpansionSession {
|
|
57151
58516
|
limits;
|
|
57152
|
-
|
|
58517
|
+
rootReal;
|
|
58518
|
+
fileSystem;
|
|
58519
|
+
/** File content keyed by real repository-relative path */ contentByReal = new Map();
|
|
58520
|
+
/** Real path of every logical path already resolved */ realByLogical = new Map();
|
|
57153
58521
|
uniqueFiles = new Set();
|
|
57154
58522
|
edges = [];
|
|
57155
58523
|
diagnostics = [];
|
|
58524
|
+
importersAtTheLimit = new Set();
|
|
58525
|
+
tokensAttempted = 0;
|
|
57156
58526
|
emittedBytes = 0;
|
|
57157
|
-
constructor(limits){
|
|
58527
|
+
constructor(limits, rootReal, fileSystem){
|
|
57158
58528
|
this.limits = limits;
|
|
58529
|
+
this.rootReal = rootReal;
|
|
58530
|
+
this.fileSystem = fileSystem;
|
|
58531
|
+
}
|
|
58532
|
+
/** Every spelling of the repository root an absolute import may use. */ rootSpellings(repoRoot) {
|
|
58533
|
+
return [
|
|
58534
|
+
repoRoot,
|
|
58535
|
+
this.rootReal
|
|
58536
|
+
];
|
|
57159
58537
|
}
|
|
57160
58538
|
get edgeCount() {
|
|
57161
58539
|
return this.edges.length;
|
|
57162
58540
|
}
|
|
58541
|
+
/**
|
|
58542
|
+
* Counts a token against the edge limit. Every token counts, including
|
|
58543
|
+
* one that is not clearly a path and does not resolve, so the work a file
|
|
58544
|
+
* can cost is bounded by the limit and not by its size.
|
|
58545
|
+
*/ hasEdgeBudgetLeft() {
|
|
58546
|
+
if (this.tokensAttempted >= this.limits.maxEdges) {
|
|
58547
|
+
return false;
|
|
58548
|
+
}
|
|
58549
|
+
this.tokensAttempted += 1;
|
|
58550
|
+
return true;
|
|
58551
|
+
}
|
|
58552
|
+
/**
|
|
58553
|
+
* Reports that a size limit stopped an importer, once per importer: the
|
|
58554
|
+
* first token it could not expand carries the diagnostic, and the tokens
|
|
58555
|
+
* after it are left alone without one each.
|
|
58556
|
+
*/ recordLimitAtToken(importer, token, message) {
|
|
58557
|
+
if (this.importersAtTheLimit.has(importer)) {
|
|
58558
|
+
return;
|
|
58559
|
+
}
|
|
58560
|
+
this.importersAtTheLimit.add(importer);
|
|
58561
|
+
this.recordFailure(importer, token, "size-exceeded", undefined, message);
|
|
58562
|
+
}
|
|
58563
|
+
/**
|
|
58564
|
+
* Reports that the importer has more `@` tokens than the limit, the cause
|
|
58565
|
+
* of a stop that is neither a file size nor a graph size.
|
|
58566
|
+
*/ recordTokenLimitAtToken(importer, token) {
|
|
58567
|
+
this.recordLimitAtToken(importer, token, `Too many @ tokens (the limit is ${this.limits.maxEdges}), so this import and the ones after it in ${importer} were not expanded: ${token.rawTarget}`);
|
|
58568
|
+
}
|
|
58569
|
+
/**
|
|
58570
|
+
* Reports that the emit budget cut the text of an importer short, from the
|
|
58571
|
+
* offset where the text stopped to its end. An importer already reported
|
|
58572
|
+
* for hitting a size limit is not reported again. The budget is spent once
|
|
58573
|
+
* it has cut text, so no importer is expanded again after this.
|
|
58574
|
+
*/ recordClippedContent(importer, stoppedAt, contentLength) {
|
|
58575
|
+
if (this.importersAtTheLimit.has(importer)) {
|
|
58576
|
+
return;
|
|
58577
|
+
}
|
|
58578
|
+
this.diagnostics.push({
|
|
58579
|
+
ruleId: RULE_ID_BY_STATUS["size-exceeded"],
|
|
58580
|
+
message: `Effective instruction content stopped at ${this.limits.maxEmittedBytes} characters, so the rest of this file was not analyzed.`,
|
|
58581
|
+
filePath: importer,
|
|
58582
|
+
range: {
|
|
58583
|
+
start: stoppedAt,
|
|
58584
|
+
end: contentLength
|
|
58585
|
+
}
|
|
58586
|
+
});
|
|
58587
|
+
}
|
|
57163
58588
|
get remainingEmitBudget() {
|
|
57164
58589
|
return this.limits.maxEmittedBytes - this.emittedBytes;
|
|
57165
58590
|
}
|
|
@@ -57176,7 +58601,7 @@ function isUnsafeRootPath(rootRelativePath) {
|
|
|
57176
58601
|
]
|
|
57177
58602
|
};
|
|
57178
58603
|
}
|
|
57179
|
-
recordEdge(edge) {
|
|
58604
|
+
recordEdge(edge, message) {
|
|
57180
58605
|
this.edges.push(edge);
|
|
57181
58606
|
// Failures always carry ruleId; resolved edges do not.
|
|
57182
58607
|
if (!edge.ruleId) {
|
|
@@ -57184,12 +58609,12 @@ function isUnsafeRootPath(rootRelativePath) {
|
|
|
57184
58609
|
}
|
|
57185
58610
|
this.diagnostics.push({
|
|
57186
58611
|
ruleId: edge.ruleId,
|
|
57187
|
-
message: messageForFailure(edge.status, edge.rawTarget),
|
|
58612
|
+
message: message ?? messageForFailure(edge.status, edge.rawTarget),
|
|
57188
58613
|
filePath: edge.importer,
|
|
57189
58614
|
range: edge.tokenRange
|
|
57190
58615
|
});
|
|
57191
58616
|
}
|
|
57192
|
-
recordFailure(importer, token, status, target) {
|
|
58617
|
+
recordFailure(importer, token, status, target, message) {
|
|
57193
58618
|
this.recordEdge({
|
|
57194
58619
|
importer,
|
|
57195
58620
|
tokenRange: {
|
|
@@ -57200,7 +58625,7 @@ function isUnsafeRootPath(rootRelativePath) {
|
|
|
57200
58625
|
target,
|
|
57201
58626
|
status,
|
|
57202
58627
|
ruleId: ruleIdForStatus(status)
|
|
57203
|
-
});
|
|
58628
|
+
}, message);
|
|
57204
58629
|
}
|
|
57205
58630
|
recordResolved(importer, token, target) {
|
|
57206
58631
|
this.recordEdge({
|
|
@@ -57244,65 +58669,65 @@ function isUnsafeRootPath(rootRelativePath) {
|
|
|
57244
58669
|
complete: false
|
|
57245
58670
|
};
|
|
57246
58671
|
}
|
|
57247
|
-
|
|
57248
|
-
|
|
57249
|
-
|
|
58672
|
+
/**
|
|
58673
|
+
* Resolves a logical repository-relative path to a real file inside the
|
|
58674
|
+
* repository (following symlinks that stay inside it) and reads it. A
|
|
58675
|
+
* file is identified by its real path everywhere: content, the unique-file
|
|
58676
|
+
* budget, cycle detection and the base for the imports it contains.
|
|
58677
|
+
*/ async readFile(repoRoot, relativePath) {
|
|
58678
|
+
const known = this.realByLogical.get(relativePath);
|
|
58679
|
+
const knownContent = known === undefined ? undefined : this.contentByReal.get(known);
|
|
58680
|
+
if (known !== undefined && knownContent !== undefined) {
|
|
57250
58681
|
return {
|
|
57251
58682
|
ok: true,
|
|
57252
|
-
content:
|
|
58683
|
+
content: knownContent,
|
|
58684
|
+
real: known
|
|
57253
58685
|
};
|
|
57254
58686
|
}
|
|
57255
|
-
|
|
57256
|
-
|
|
57257
|
-
|
|
57258
|
-
return {
|
|
57259
|
-
ok: false,
|
|
57260
|
-
status: classifyPathError(error)
|
|
57261
|
-
};
|
|
58687
|
+
const located = await this.locate(repoRoot, relativePath);
|
|
58688
|
+
if (!located.ok) {
|
|
58689
|
+
return located;
|
|
57262
58690
|
}
|
|
57263
|
-
|
|
57264
|
-
const stats = await file_system_utils_statWithinRoot(repoRoot, relativePath);
|
|
57265
|
-
if (stats.isSymbolicLink) {
|
|
57266
|
-
return {
|
|
57267
|
-
ok: false,
|
|
57268
|
-
status: "symlink"
|
|
57269
|
-
};
|
|
57270
|
-
}
|
|
57271
|
-
if (!stats.isFile) {
|
|
57272
|
-
return {
|
|
57273
|
-
ok: false,
|
|
57274
|
-
status: "not-regular"
|
|
57275
|
-
};
|
|
57276
|
-
}
|
|
57277
|
-
} catch (error) {
|
|
57278
|
-
return {
|
|
57279
|
-
ok: false,
|
|
57280
|
-
status: classifyPathError(error)
|
|
57281
|
-
};
|
|
57282
|
-
}
|
|
57283
|
-
const isNewUnique = !this.uniqueFiles.has(relativePath);
|
|
58691
|
+
const isNewUnique = !this.uniqueFiles.has(located.real);
|
|
57284
58692
|
if (isNewUnique && this.uniqueFiles.size >= this.limits.maxUniqueFiles) {
|
|
57285
58693
|
return {
|
|
57286
58694
|
ok: false,
|
|
57287
58695
|
status: "size-exceeded"
|
|
57288
58696
|
};
|
|
57289
58697
|
}
|
|
58698
|
+
const cached = this.contentByReal.get(located.real);
|
|
58699
|
+
const content = cached ?? await this.readLocated(located);
|
|
58700
|
+
if (typeof content !== "string") {
|
|
58701
|
+
return content;
|
|
58702
|
+
}
|
|
58703
|
+
this.contentByReal.set(located.real, content);
|
|
58704
|
+
this.uniqueFiles.add(located.real);
|
|
58705
|
+
this.realByLogical.set(relativePath, located.real);
|
|
58706
|
+
return {
|
|
58707
|
+
ok: true,
|
|
58708
|
+
content,
|
|
58709
|
+
real: located.real
|
|
58710
|
+
};
|
|
58711
|
+
}
|
|
58712
|
+
async locate(repoRoot, relativePath) {
|
|
58713
|
+
let absolutePath;
|
|
57290
58714
|
try {
|
|
57291
|
-
|
|
57292
|
-
this.contentCache.set(relativePath, content);
|
|
57293
|
-
if (isNewUnique) {
|
|
57294
|
-
this.uniqueFiles.add(relativePath);
|
|
57295
|
-
}
|
|
57296
|
-
return {
|
|
57297
|
-
ok: true,
|
|
57298
|
-
content
|
|
57299
|
-
};
|
|
58715
|
+
absolutePath = await resolvePathWithinRoot(repoRoot, relativePath);
|
|
57300
58716
|
} catch (error) {
|
|
57301
58717
|
return {
|
|
57302
58718
|
ok: false,
|
|
57303
58719
|
status: classifyPathError(error)
|
|
57304
58720
|
};
|
|
57305
58721
|
}
|
|
58722
|
+
const inspection = await this.fileSystem.inspect(this.rootReal, absolutePath);
|
|
58723
|
+
return toLocatedFile(this.rootReal, inspection);
|
|
58724
|
+
}
|
|
58725
|
+
async readLocated(located) {
|
|
58726
|
+
const outcome = await this.fileSystem.read(located.realAbsolute, this.limits.maxFileBytes, located.identity);
|
|
58727
|
+
return outcome.ok ? outcome.content : {
|
|
58728
|
+
ok: false,
|
|
58729
|
+
status: statusForReadFailure(outcome.reason)
|
|
58730
|
+
};
|
|
57306
58731
|
}
|
|
57307
58732
|
}
|
|
57308
58733
|
class ContentBuilder {
|
|
@@ -57340,15 +58765,30 @@ class ContentBuilder {
|
|
|
57340
58765
|
}
|
|
57341
58766
|
function emitSourceSlice(session, builder, sourcePath, content, from, to, importChain) {
|
|
57342
58767
|
if (to <= from) {
|
|
57343
|
-
return
|
|
58768
|
+
return {
|
|
58769
|
+
complete: true,
|
|
58770
|
+
stoppedAt: from
|
|
58771
|
+
};
|
|
57344
58772
|
}
|
|
57345
58773
|
const { emitted, complete } = session.takeEmitBudget(content.slice(from, to));
|
|
57346
58774
|
builder.appendLiteral(sourcePath, from, emitted, importChain);
|
|
57347
|
-
return
|
|
58775
|
+
return {
|
|
58776
|
+
complete,
|
|
58777
|
+
stoppedAt: from + emitted.length
|
|
58778
|
+
};
|
|
58779
|
+
}
|
|
58780
|
+
const NON_BLANK = /\S/;
|
|
58781
|
+
/**
|
|
58782
|
+
* Reports text the emit budget cut off, unless nothing but whitespace was
|
|
58783
|
+
* lost, which no reader of the file would miss.
|
|
58784
|
+
*/ function recordIfClipped(session, sourcePath, content, outcome) {
|
|
58785
|
+
if (!outcome.complete && NON_BLANK.test(content.slice(outcome.stoppedAt))) {
|
|
58786
|
+
session.recordClippedContent(sourcePath, outcome.stoppedAt, content.length);
|
|
58787
|
+
}
|
|
57348
58788
|
}
|
|
57349
58789
|
async function expandToken(session, repoRoot, importerPath, token, hop, stack, importChain, outputOffset) {
|
|
57350
|
-
if (session.
|
|
57351
|
-
session.
|
|
58790
|
+
if (!session.hasEdgeBudgetLeft()) {
|
|
58791
|
+
session.recordTokenLimitAtToken(importerPath, token);
|
|
57352
58792
|
return {
|
|
57353
58793
|
kind: "unexpanded"
|
|
57354
58794
|
};
|
|
@@ -57360,7 +58800,7 @@ async function expandToken(session, repoRoot, importerPath, token, hop, stack, i
|
|
|
57360
58800
|
kind: "unexpanded"
|
|
57361
58801
|
};
|
|
57362
58802
|
}
|
|
57363
|
-
const normalized = normalizeRelativeWithinRoot(importerPath, token.rawTarget);
|
|
58803
|
+
const normalized = normalizeRelativeWithinRoot(importerPath, token.rawTarget, session.rootSpellings(repoRoot));
|
|
57364
58804
|
if (!normalized.ok) {
|
|
57365
58805
|
session.recordFailure(importerPath, token, normalized.status);
|
|
57366
58806
|
return {
|
|
@@ -57368,33 +58808,45 @@ async function expandToken(session, repoRoot, importerPath, token, hop, stack, i
|
|
|
57368
58808
|
};
|
|
57369
58809
|
}
|
|
57370
58810
|
const targetPath = normalized.relativePath;
|
|
57371
|
-
|
|
57372
|
-
|
|
58811
|
+
const readResult = await session.readFile(repoRoot, targetPath);
|
|
58812
|
+
if (!readResult.ok) {
|
|
58813
|
+
// `@types/node` or `@no-such-thing` at the start of a line is prose
|
|
58814
|
+
// as often as an import: when it does not resolve to a file and does
|
|
58815
|
+
// not look like a path, leave it alone instead of warning.
|
|
58816
|
+
if (readResult.status === "unresolved" && !token.looksLikePath) {
|
|
58817
|
+
return {
|
|
58818
|
+
kind: "unexpanded"
|
|
58819
|
+
};
|
|
58820
|
+
}
|
|
58821
|
+
session.recordFailure(importerPath, token, readResult.status, targetPath);
|
|
57373
58822
|
return {
|
|
57374
58823
|
kind: "unexpanded"
|
|
57375
58824
|
};
|
|
57376
58825
|
}
|
|
57377
|
-
|
|
57378
|
-
|
|
57379
|
-
|
|
58826
|
+
// From here the target is identified by its real path, so two symlinks to
|
|
58827
|
+
// one file are one node of the import graph and a symlink that leads back
|
|
58828
|
+
// to an importer is a cycle.
|
|
58829
|
+
const targetReal = readResult.real;
|
|
58830
|
+
if (stack.includes(targetReal)) {
|
|
58831
|
+
session.recordFailure(importerPath, token, "cycle", targetReal);
|
|
57380
58832
|
return {
|
|
57381
58833
|
kind: "unexpanded"
|
|
57382
58834
|
};
|
|
57383
58835
|
}
|
|
57384
58836
|
if (session.isEmitBudgetExhausted()) {
|
|
57385
|
-
session.recordFailure(importerPath, token, "size-exceeded",
|
|
58837
|
+
session.recordFailure(importerPath, token, "size-exceeded", targetReal);
|
|
57386
58838
|
return {
|
|
57387
58839
|
kind: "unexpanded"
|
|
57388
58840
|
};
|
|
57389
58841
|
}
|
|
57390
|
-
const child = await expandContent(session, repoRoot,
|
|
58842
|
+
const child = await expandContent(session, repoRoot, targetReal, readResult.content, nextHop, [
|
|
57391
58843
|
...stack,
|
|
57392
|
-
|
|
58844
|
+
targetReal
|
|
57393
58845
|
], [
|
|
57394
58846
|
...importChain,
|
|
57395
|
-
|
|
58847
|
+
targetReal
|
|
57396
58848
|
]);
|
|
57397
|
-
session.recordResolved(importerPath, token,
|
|
58849
|
+
session.recordResolved(importerPath, token, targetReal);
|
|
57398
58850
|
return {
|
|
57399
58851
|
kind: "expanded",
|
|
57400
58852
|
content: child.content,
|
|
@@ -57406,9 +58858,9 @@ async function expandContent(session, repoRoot, sourcePath, content, hop, stack,
|
|
|
57406
58858
|
const builder = new ContentBuilder();
|
|
57407
58859
|
let cursor = 0;
|
|
57408
58860
|
for (const token of tokens){
|
|
57409
|
-
const
|
|
57410
|
-
if (!
|
|
57411
|
-
session.
|
|
58861
|
+
const literal = emitSourceSlice(session, builder, sourcePath, content, cursor, token.start, importChain);
|
|
58862
|
+
if (!literal.complete) {
|
|
58863
|
+
session.recordLimitAtToken(sourcePath, token);
|
|
57412
58864
|
emitSourceSlice(session, builder, sourcePath, content, token.start, content.length, importChain);
|
|
57413
58865
|
return builder.toResult();
|
|
57414
58866
|
}
|
|
@@ -57416,14 +58868,15 @@ async function expandContent(session, repoRoot, sourcePath, content, hop, stack,
|
|
|
57416
58868
|
if (expansion.kind === "expanded") {
|
|
57417
58869
|
builder.appendExpanded(expansion.content, expansion.segments);
|
|
57418
58870
|
} else {
|
|
57419
|
-
const
|
|
57420
|
-
if (!
|
|
58871
|
+
const emittedToken = emitSourceSlice(session, builder, sourcePath, content, token.start, token.end, importChain);
|
|
58872
|
+
if (!emittedToken.complete) {
|
|
58873
|
+
recordIfClipped(session, sourcePath, content, emittedToken);
|
|
57421
58874
|
return builder.toResult();
|
|
57422
58875
|
}
|
|
57423
58876
|
}
|
|
57424
58877
|
cursor = token.end;
|
|
57425
58878
|
}
|
|
57426
|
-
emitSourceSlice(session, builder, sourcePath, content, cursor, content.length, importChain);
|
|
58879
|
+
recordIfClipped(session, sourcePath, content, emitSourceSlice(session, builder, sourcePath, content, cursor, content.length, importChain));
|
|
57427
58880
|
return builder.toResult();
|
|
57428
58881
|
}
|
|
57429
58882
|
/**
|
|
@@ -57434,15 +58887,22 @@ async function expandContent(session, repoRoot, sourcePath, content, hop, stack,
|
|
|
57434
58887
|
if (isUnsafeRootPath(rootRelativePath)) {
|
|
57435
58888
|
throw new Error(`Root path is outside repository root: ${input.rootRelativePath}`);
|
|
57436
58889
|
}
|
|
57437
|
-
const
|
|
58890
|
+
const rootReal = await (0,promises_.realpath)(input.repoRoot).catch(()=>null);
|
|
58891
|
+
if (rootReal === null) {
|
|
58892
|
+
throw new Error(`Unable to read root instruction file ${rootRelativePath}: unresolved`);
|
|
58893
|
+
}
|
|
58894
|
+
const session = new ExpansionSession(limits, rootReal, {
|
|
58895
|
+
...nodeImportFileSystem,
|
|
58896
|
+
...input.fileSystem
|
|
58897
|
+
});
|
|
57438
58898
|
const rootRead = await session.readFile(input.repoRoot, rootRelativePath);
|
|
57439
58899
|
if (!rootRead.ok) {
|
|
57440
58900
|
throw new Error(`Unable to read root instruction file ${rootRelativePath}: ${rootRead.status}`);
|
|
57441
58901
|
}
|
|
57442
|
-
const expanded = await expandContent(session, input.repoRoot,
|
|
57443
|
-
|
|
58902
|
+
const expanded = await expandContent(session, input.repoRoot, rootRead.real, rootRead.content, 0, [
|
|
58903
|
+
rootRead.real
|
|
57444
58904
|
], [
|
|
57445
|
-
|
|
58905
|
+
rootRead.real
|
|
57446
58906
|
]);
|
|
57447
58907
|
const snapshot = session.snapshot();
|
|
57448
58908
|
return {
|
|
@@ -57471,39 +58931,45 @@ function toRepoRelativePosix(repoRoot, absoluteFilePath) {
|
|
|
57471
58931
|
function toAbsolutePath(repoRoot, relativePosixPath) {
|
|
57472
58932
|
return (0,external_node_path_.join)(repoRoot, ...relativePosixPath.split("/"));
|
|
57473
58933
|
}
|
|
57474
|
-
async function
|
|
57475
|
-
const
|
|
57476
|
-
|
|
57477
|
-
|
|
57478
|
-
|
|
57479
|
-
|
|
57480
|
-
|
|
57481
|
-
|
|
57482
|
-
|
|
57483
|
-
|
|
57484
|
-
|
|
57485
|
-
|
|
57486
|
-
|
|
57487
|
-
|
|
57488
|
-
|
|
57489
|
-
|
|
57490
|
-
|
|
57491
|
-
|
|
57492
|
-
|
|
57493
|
-
|
|
57494
|
-
|
|
57495
|
-
const end = offsetToSourcePosition(content, diagnostic.range.end);
|
|
57496
|
-
mapped.push({
|
|
58934
|
+
async function readImporterSource(readSource, createPositionIndex, absolutePath) {
|
|
58935
|
+
const outcome = await readSource(absolutePath, DEFAULT_MAX_FILE_BYTES);
|
|
58936
|
+
return [
|
|
58937
|
+
absolutePath,
|
|
58938
|
+
outcome.ok ? createPositionIndex(outcome.content) : null
|
|
58939
|
+
];
|
|
58940
|
+
}
|
|
58941
|
+
async function mapExpansionDiagnostics(repoRoot, diagnostics, readSource, createPositionIndex) {
|
|
58942
|
+
const located = diagnostics.map((diagnostic)=>({
|
|
58943
|
+
diagnostic,
|
|
58944
|
+
absolutePath: toAbsolutePath(repoRoot, diagnostic.filePath)
|
|
58945
|
+
}));
|
|
58946
|
+
const importerPaths = new Set(located.map((entry)=>entry.absolutePath));
|
|
58947
|
+
const contentByPath = new Map(await Promise.all([
|
|
58948
|
+
...importerPaths
|
|
58949
|
+
].map((path)=>readImporterSource(readSource, createPositionIndex, path))));
|
|
58950
|
+
return located.map(({ diagnostic, absolutePath })=>toExpansionDiagnostic(diagnostic, absolutePath, contentByPath.get(absolutePath) ?? null));
|
|
58951
|
+
}
|
|
58952
|
+
function toExpansionDiagnostic(diagnostic, absolutePath, positionOf) {
|
|
58953
|
+
if (diagnostic.range === undefined || positionOf === null) {
|
|
58954
|
+
return {
|
|
57497
58955
|
ruleId: diagnostic.ruleId,
|
|
57498
58956
|
message: diagnostic.message,
|
|
57499
58957
|
filePath: absolutePath,
|
|
57500
|
-
line:
|
|
57501
|
-
column:
|
|
57502
|
-
|
|
57503
|
-
endColumn: end.column
|
|
57504
|
-
});
|
|
58958
|
+
line: 1,
|
|
58959
|
+
column: 1
|
|
58960
|
+
};
|
|
57505
58961
|
}
|
|
57506
|
-
|
|
58962
|
+
const start = positionOf(diagnostic.range.start);
|
|
58963
|
+
const end = positionOf(diagnostic.range.end);
|
|
58964
|
+
return {
|
|
58965
|
+
ruleId: diagnostic.ruleId,
|
|
58966
|
+
message: diagnostic.message,
|
|
58967
|
+
filePath: absolutePath,
|
|
58968
|
+
line: start.line,
|
|
58969
|
+
column: start.column,
|
|
58970
|
+
endLine: end.line,
|
|
58971
|
+
endColumn: end.column
|
|
58972
|
+
};
|
|
57507
58973
|
}
|
|
57508
58974
|
function collectResolvedImports(repoRoot, edges) {
|
|
57509
58975
|
const resolved = [];
|
|
@@ -57523,7 +58989,9 @@ function collectResolvedImports(repoRoot, edges) {
|
|
|
57523
58989
|
}
|
|
57524
58990
|
/**
|
|
57525
58991
|
* Creates the default Claude instruction import expander used by composition roots.
|
|
57526
|
-
*/ function createClaudeInstructionImportExpander() {
|
|
58992
|
+
*/ function createClaudeInstructionImportExpander(options = {}) {
|
|
58993
|
+
const readSource = options.readSource ?? readRegularFileNoFollow;
|
|
58994
|
+
const createPositionIndex = options.createPositionIndex ?? ((content)=>createSourcePositionIndex(content));
|
|
57527
58995
|
return {
|
|
57528
58996
|
async expandClaudeEntrypoint (input) {
|
|
57529
58997
|
const rootRelativePath = toRepoRelativePosix(input.repoRoot, input.absoluteFilePath);
|
|
@@ -57531,7 +58999,7 @@ function collectResolvedImports(repoRoot, edges) {
|
|
|
57531
58999
|
repoRoot: input.repoRoot,
|
|
57532
59000
|
rootRelativePath
|
|
57533
59001
|
});
|
|
57534
|
-
const expansionDiagnostics = await mapExpansionDiagnostics(input.repoRoot, document.expansionDiagnostics);
|
|
59002
|
+
const expansionDiagnostics = await mapExpansionDiagnostics(input.repoRoot, document.expansionDiagnostics, readSource, createPositionIndex);
|
|
57535
59003
|
return {
|
|
57536
59004
|
content: document.content,
|
|
57537
59005
|
effectiveRoot: input.absoluteFilePath,
|
|
@@ -57542,86 +59010,6 @@ function collectResolvedImports(repoRoot, edges) {
|
|
|
57542
59010
|
};
|
|
57543
59011
|
}
|
|
57544
59012
|
|
|
57545
|
-
;// CONCATENATED MODULE: ../core/src/gateways/instruction-file-discovery-gateway.ts
|
|
57546
|
-
/**
|
|
57547
|
-
* Gateway for discovering instruction files across multiple formats.
|
|
57548
|
-
* Supports GitHub Copilot, Claude Code, and community agent instruction formats.
|
|
57549
|
-
*/
|
|
57550
|
-
|
|
57551
|
-
|
|
57552
|
-
/**
|
|
57553
|
-
* File system implementation of the instruction file discovery gateway.
|
|
57554
|
-
*/ class FileSystemInstructionFileDiscoveryGateway {
|
|
57555
|
-
async discoverInstructionFiles(targetDir) {
|
|
57556
|
-
const files = [];
|
|
57557
|
-
for (const entry of instructionDiscoveryEntries()){
|
|
57558
|
-
const format = entry.instructionFormat;
|
|
57559
|
-
if (format == null) {
|
|
57560
|
-
continue;
|
|
57561
|
-
}
|
|
57562
|
-
const relativePath = (0,external_node_path_.join)(...entry.path.split("/"));
|
|
57563
|
-
if (entry.matchKind === "exact") {
|
|
57564
|
-
if (await this.safePathExists(targetDir, relativePath)) {
|
|
57565
|
-
files.push({
|
|
57566
|
-
filePath: (0,external_node_path_.join)(targetDir, relativePath),
|
|
57567
|
-
format
|
|
57568
|
-
});
|
|
57569
|
-
}
|
|
57570
|
-
continue;
|
|
57571
|
-
}
|
|
57572
|
-
await this.discoverMdFilesInDir(targetDir, relativePath, format, files);
|
|
57573
|
-
}
|
|
57574
|
-
return files;
|
|
57575
|
-
}
|
|
57576
|
-
async safePathExists(targetDir, relativePath) {
|
|
57577
|
-
try {
|
|
57578
|
-
return await file_system_utils_pathExistsWithinRoot(targetDir, relativePath);
|
|
57579
|
-
} catch {
|
|
57580
|
-
return false;
|
|
57581
|
-
}
|
|
57582
|
-
}
|
|
57583
|
-
async discoverMdFilesInDir(targetDir, dirPath, format, files) {
|
|
57584
|
-
if (!await this.safePathExists(targetDir, dirPath)) {
|
|
57585
|
-
return;
|
|
57586
|
-
}
|
|
57587
|
-
try {
|
|
57588
|
-
const entries = await file_system_utils_listDirectoryWithinRoot(targetDir, dirPath);
|
|
57589
|
-
const resolvedDirPath = (0,external_node_path_.resolve)(targetDir, dirPath);
|
|
57590
|
-
for (const entry of entries){
|
|
57591
|
-
if (entry.isSymbolicLink()) {
|
|
57592
|
-
continue;
|
|
57593
|
-
}
|
|
57594
|
-
if (!entry.isFile()) {
|
|
57595
|
-
continue;
|
|
57596
|
-
}
|
|
57597
|
-
const name = entry.name;
|
|
57598
|
-
if (name.includes("..") || name.includes("/") || name.includes("\\")) {
|
|
57599
|
-
continue;
|
|
57600
|
-
}
|
|
57601
|
-
if (name.endsWith(".md")) {
|
|
57602
|
-
const filePath = (0,external_node_path_.join)(targetDir, dirPath, name);
|
|
57603
|
-
const resolvedFilePath = (0,external_node_path_.resolve)(filePath);
|
|
57604
|
-
const rel = (0,external_node_path_.relative)(resolvedDirPath, resolvedFilePath);
|
|
57605
|
-
if (rel.startsWith("..") || rel.startsWith(external_node_path_.sep)) {
|
|
57606
|
-
continue;
|
|
57607
|
-
}
|
|
57608
|
-
files.push({
|
|
57609
|
-
filePath,
|
|
57610
|
-
format
|
|
57611
|
-
});
|
|
57612
|
-
}
|
|
57613
|
-
}
|
|
57614
|
-
} catch {
|
|
57615
|
-
// Skip directory if we can't read it
|
|
57616
|
-
}
|
|
57617
|
-
}
|
|
57618
|
-
}
|
|
57619
|
-
/**
|
|
57620
|
-
* Creates and returns the default instruction file discovery gateway.
|
|
57621
|
-
*/ function createInstructionFileDiscoveryGateway() {
|
|
57622
|
-
return new FileSystemInstructionFileDiscoveryGateway();
|
|
57623
|
-
}
|
|
57624
|
-
|
|
57625
59013
|
;// CONCATENATED MODULE: ../../node_modules/mdast-util-to-string/lib/index.js
|
|
57626
59014
|
/**
|
|
57627
59015
|
* @typedef {import('mdast').Nodes} Nodes
|
|
@@ -78148,6 +79536,19 @@ const HEADING_PATTERN_DESCRIPTIONS = new Map(Object.entries(HEADING_PATTERN_DESC
|
|
|
78148
79536
|
/** Type guard narrowing an `unknown` AST child to {@link AstNodeLike}. */ function isAstNodeLike(node) {
|
|
78149
79537
|
return typeof node === "object" && node !== null && "type" in node && typeof node.type === "string";
|
|
78150
79538
|
}
|
|
79539
|
+
/**
|
|
79540
|
+
* Turns something discovery reported about itself into a lint diagnostic. It
|
|
79541
|
+
* concerns the whole run, not one file, so it points at the target directory.
|
|
79542
|
+
*/ function toDiscoveryLintDiagnostic(targetDir, report) {
|
|
79543
|
+
return {
|
|
79544
|
+
filePath: targetDir,
|
|
79545
|
+
line: 1,
|
|
79546
|
+
severity: "warning",
|
|
79547
|
+
message: report.message,
|
|
79548
|
+
ruleId: report.ruleId,
|
|
79549
|
+
target: "instruction"
|
|
79550
|
+
};
|
|
79551
|
+
}
|
|
78151
79552
|
/** Maximum number of raw heading pattern entries accepted before deduplication. */ const MAX_RAW_HEADING_PATTERNS = 1000;
|
|
78152
79553
|
/** Maximum number of heading patterns accepted in a single execute() call. */ const MAX_HEADING_PATTERNS = 50;
|
|
78153
79554
|
/** Maximum character length for a single heading pattern. */ const analyze_instruction_quality_MAX_PATTERN_LENGTH = 200;
|
|
@@ -78277,7 +79678,9 @@ const HEADING_PATTERN_DESCRIPTIONS = new Map(Object.entries(HEADING_PATTERN_DESC
|
|
|
78277
79678
|
}
|
|
78278
79679
|
}
|
|
78279
79680
|
const proximityWindow = parsed.proximityWindow ?? 3;
|
|
78280
|
-
const
|
|
79681
|
+
const discoveryReports = [];
|
|
79682
|
+
const discoveredFiles = await this.discoveryGateway.discoverInstructionFiles(parsed.targetDir, (report)=>discoveryReports.push(report));
|
|
79683
|
+
const discoveryDiagnostics = discoveryReports.map((report)=>toDiscoveryLintDiagnostic(parsed.targetDir, report));
|
|
78281
79684
|
const mandatoryCommands = await this.commandsGateway.getMandatoryCommands(parsed.targetDir);
|
|
78282
79685
|
if (discoveredFiles.length === 0) {
|
|
78283
79686
|
return {
|
|
@@ -78287,12 +79690,12 @@ const HEADING_PATTERN_DESCRIPTIONS = new Map(Object.entries(HEADING_PATTERN_DESC
|
|
|
78287
79690
|
overallQualityScore: 0,
|
|
78288
79691
|
suggestions: [
|
|
78289
79692
|
{
|
|
78290
|
-
message: "No agent instruction files found. Supported formats: .github/copilot-instructions.md, .github/instructions
|
|
79693
|
+
message: "No agent instruction files found. Supported formats: .github/copilot-instructions.md, .github/instructions/**/*.instructions.md, .github/agents/*.md, AGENTS.md, AGENTS.override.md, CLAUDE.md, .claude/CLAUDE.md"
|
|
78291
79694
|
}
|
|
78292
79695
|
],
|
|
78293
79696
|
parsingErrors: []
|
|
78294
79697
|
},
|
|
78295
|
-
diagnostics:
|
|
79698
|
+
diagnostics: discoveryDiagnostics
|
|
78296
79699
|
};
|
|
78297
79700
|
}
|
|
78298
79701
|
// Analyze each file (Claude entrypoints use effective import-expanded content)
|
|
@@ -78320,7 +79723,9 @@ const HEADING_PATTERN_DESCRIPTIONS = new Map(Object.entries(HEADING_PATTERN_DESC
|
|
|
78320
79723
|
parsingErrors.sort((a, b)=>a.filePath < b.filePath ? -1 : a.filePath > b.filePath ? 1 : 0);
|
|
78321
79724
|
// Score each mandatory command across all files
|
|
78322
79725
|
const commandScores = [];
|
|
78323
|
-
const diagnostics = [
|
|
79726
|
+
const diagnostics = [
|
|
79727
|
+
...discoveryDiagnostics
|
|
79728
|
+
];
|
|
78324
79729
|
// Emit diagnostics for parsing errors
|
|
78325
79730
|
for (const pe of parsingErrors){
|
|
78326
79731
|
diagnostics.push({
|
|
@@ -78333,7 +79738,7 @@ const HEADING_PATTERN_DESCRIPTIONS = new Map(Object.entries(HEADING_PATTERN_DESC
|
|
|
78333
79738
|
});
|
|
78334
79739
|
}
|
|
78335
79740
|
// Import-expansion failures (stable ruleIds, importer-token provenance)
|
|
78336
|
-
diagnostics.push(...importDiagnostics);
|
|
79741
|
+
diagnostics.push(...AnalyzeInstructionQualityUseCase.withoutRepeats(importDiagnostics));
|
|
78337
79742
|
// Check each successfully parsed file for missing structural headings
|
|
78338
79743
|
const sortedFilePaths = Array.from(fileStructures.keys()).sort((a, b)=>a < b ? -1 : a > b ? 1 : 0);
|
|
78339
79744
|
for (const filePath of sortedFilePaths){
|
|
@@ -78401,7 +79806,7 @@ const HEADING_PATTERN_DESCRIPTIONS = new Map(Object.entries(HEADING_PATTERN_DESC
|
|
|
78401
79806
|
if (file.format === "claude-md" && this.claudeImportExpander !== undefined) {
|
|
78402
79807
|
const effective = await this.claudeImportExpander.expandClaudeEntrypoint({
|
|
78403
79808
|
repoRoot: targetDir,
|
|
78404
|
-
absoluteFilePath: file.filePath
|
|
79809
|
+
absoluteFilePath: file.contentPath ?? file.filePath
|
|
78405
79810
|
});
|
|
78406
79811
|
const importDiagnostics = effective.expansionDiagnostics.map((diagnostic)=>AnalyzeInstructionQualityUseCase.toImportLintDiagnostic(diagnostic));
|
|
78407
79812
|
importDiagnostics.sort(AnalyzeInstructionQualityUseCase.compareImportDiagnostics);
|
|
@@ -78415,7 +79820,7 @@ const HEADING_PATTERN_DESCRIPTIONS = new Map(Object.entries(HEADING_PATTERN_DESC
|
|
|
78415
79820
|
};
|
|
78416
79821
|
}
|
|
78417
79822
|
return {
|
|
78418
|
-
structure: await this.astGateway.parseFile(file.filePath),
|
|
79823
|
+
structure: await this.astGateway.parseFile(file.contentPath ?? file.filePath),
|
|
78419
79824
|
importDiagnostics: []
|
|
78420
79825
|
};
|
|
78421
79826
|
}
|
|
@@ -78432,6 +79837,27 @@ const HEADING_PATTERN_DESCRIPTIONS = new Map(Object.entries(HEADING_PATTERN_DESC
|
|
|
78432
79837
|
target: "instruction"
|
|
78433
79838
|
};
|
|
78434
79839
|
}
|
|
79840
|
+
/**
|
|
79841
|
+
* An imported file that several import paths or several entrypoints reach
|
|
79842
|
+
* is expanded once per path, so the same failure at the same place in it
|
|
79843
|
+
* is found more than once. Keeps the first of each.
|
|
79844
|
+
*/ static withoutRepeats(diagnostics) {
|
|
79845
|
+
const seen = new Set();
|
|
79846
|
+
return diagnostics.filter((diagnostic)=>{
|
|
79847
|
+
const identity = JSON.stringify([
|
|
79848
|
+
diagnostic.ruleId,
|
|
79849
|
+
diagnostic.filePath,
|
|
79850
|
+
diagnostic.line,
|
|
79851
|
+
diagnostic.column,
|
|
79852
|
+
diagnostic.message
|
|
79853
|
+
]);
|
|
79854
|
+
if (seen.has(identity)) {
|
|
79855
|
+
return false;
|
|
79856
|
+
}
|
|
79857
|
+
seen.add(identity);
|
|
79858
|
+
return true;
|
|
79859
|
+
});
|
|
79860
|
+
}
|
|
78435
79861
|
static compareImportDiagnostics(a, b) {
|
|
78436
79862
|
if (a.filePath !== b.filePath) {
|
|
78437
79863
|
return a.filePath < b.filePath ? -1 : 1;
|
|
@@ -78729,12 +80155,12 @@ const HEADING_PATTERN_DESCRIPTIONS = new Map(Object.entries(HEADING_PATTERN_DESC
|
|
|
78729
80155
|
|
|
78730
80156
|
|
|
78731
80157
|
|
|
78732
|
-
|
|
80158
|
+
|
|
78733
80159
|
/**
|
|
78734
80160
|
* Remark-based implementation of the Markdown AST gateway.
|
|
78735
80161
|
*/ class RemarkMarkdownAstGateway {
|
|
78736
80162
|
async parseFile(filePath) {
|
|
78737
|
-
const content = await read_file_no_follow_readFileNoFollow(filePath,
|
|
80163
|
+
const content = await read_file_no_follow_readFileNoFollow(filePath, MAX_INSTRUCTION_FILE_BYTES);
|
|
78738
80164
|
return this.parseContent(content);
|
|
78739
80165
|
}
|
|
78740
80166
|
parseContent(content) {
|
|
@@ -78848,14 +80274,14 @@ const HEADING_PATTERN_DESCRIPTIONS = new Map(Object.entries(HEADING_PATTERN_DESC
|
|
|
78848
80274
|
|
|
78849
80275
|
|
|
78850
80276
|
|
|
78851
|
-
|
|
78852
80277
|
/**
|
|
78853
80278
|
* Analyzes the structural quality of feedback loop documentation in instruction files.
|
|
78854
80279
|
* Assesses structural context, execution clarity, and loop completeness.
|
|
78855
80280
|
*/ const analyzeInstructionQualityHandler = async (args)=>{
|
|
78856
80281
|
const dir = args.targetDir || process.cwd();
|
|
78857
|
-
|
|
78858
|
-
|
|
80282
|
+
const targetDirError = await validateTargetDirectory(dir);
|
|
80283
|
+
if (targetDirError) {
|
|
80284
|
+
return targetDirError;
|
|
78859
80285
|
}
|
|
78860
80286
|
try {
|
|
78861
80287
|
const discoveryGateway = createInstructionFileDiscoveryGateway();
|
|
@@ -79238,13 +80664,14 @@ const HEADING_PATTERN_DESCRIPTIONS = new Map(Object.entries(HEADING_PATTERN_DESC
|
|
|
79238
80664
|
*/ const createClaudeCodeWebSetupHandler = async (args)=>{
|
|
79239
80665
|
try {
|
|
79240
80666
|
const dir = args.targetDir || process.cwd();
|
|
79241
|
-
|
|
79242
|
-
|
|
80667
|
+
const targetDirError = await validateTargetDirectory(dir);
|
|
80668
|
+
if (targetDirError) {
|
|
80669
|
+
return targetDirError;
|
|
79243
80670
|
}
|
|
79244
|
-
const
|
|
80671
|
+
const copilotSetupConfig = await loadCopilotSetupConfig(dir);
|
|
80672
|
+
const environmentGateway = createEnvironmentGatewayWithConfig(copilotSetupConfig);
|
|
79245
80673
|
const claudeGateway = createClaudeFileGateway();
|
|
79246
80674
|
const environment = await environmentGateway.detectEnvironment(dir);
|
|
79247
|
-
const copilotSetupConfig = await loadCopilotSetupConfig(dir);
|
|
79248
80675
|
const hooks = await buildSessionStartHooks(environment, copilotSetupConfig);
|
|
79249
80676
|
const existingSettings = await claudeGateway.readSettings(dir);
|
|
79250
80677
|
const mergedSettings = mergeClaudeSettings(existingSettings, hooks);
|
|
@@ -79376,16 +80803,17 @@ Example resolved format: actions/setup-node@1a2b3c4d5e6f # v4.0.0`;
|
|
|
79376
80803
|
}
|
|
79377
80804
|
/**
|
|
79378
80805
|
* Builds an array of ActionToResolve entries from setup step candidates.
|
|
79379
|
-
* Filters out actions that already have resolved versions.
|
|
80806
|
+
* Filters out run steps and actions that already have resolved versions.
|
|
79380
80807
|
* @param candidates The setup step candidates
|
|
79381
80808
|
* @param resolvedVersions Optional array of already-resolved versions
|
|
79382
80809
|
* @returns Array of actions that need version resolution
|
|
79383
80810
|
*/ function buildActionsToResolve(candidates, resolvedVersions) {
|
|
79384
80811
|
const resolvedActions = new Set(resolvedVersions?.map((r)=>r.action) ?? []);
|
|
79385
|
-
// Include checkout action which is always added to workflows
|
|
80812
|
+
// Include checkout action which is always added to workflows.
|
|
80813
|
+
// Run steps (e.g., install commands) have no action version to resolve.
|
|
79386
80814
|
const allActions = [
|
|
79387
80815
|
"actions/checkout",
|
|
79388
|
-
...candidates.map((
|
|
80816
|
+
...candidates.filter((candidate)=>!candidate.run).map((candidate)=>candidate.action)
|
|
79389
80817
|
];
|
|
79390
80818
|
// Deduplicate and filter out already-resolved actions
|
|
79391
80819
|
const uniqueActions = [
|
|
@@ -79552,6 +80980,64 @@ function getInstallStepName(packageManagerType) {
|
|
|
79552
80980
|
return names[packageManagerType] || "Install dependencies";
|
|
79553
80981
|
}
|
|
79554
80982
|
|
|
80983
|
+
;// CONCATENATED MODULE: ../core/src/use-cases/placeholder-pinning.ts
|
|
80984
|
+
/**
|
|
80985
|
+
* Finds steps of an existing workflow that still hold the version
|
|
80986
|
+
* placeholder and have a resolved SHA-pinned version available.
|
|
80987
|
+
*/
|
|
80988
|
+
const PLACEHOLDER_SUFFIX = `@${VERSION_PLACEHOLDER}`;
|
|
80989
|
+
function isRecord(value) {
|
|
80990
|
+
return value !== null && typeof value === "object";
|
|
80991
|
+
}
|
|
80992
|
+
/**
|
|
80993
|
+
* Collects every step object in a parsed workflow's jobs.
|
|
80994
|
+
*/ function collectStepObjects(jobs) {
|
|
80995
|
+
return Object.values(jobs).map((job)=>isRecord(job) ? job.steps : undefined).filter((steps)=>Array.isArray(steps)).flat().filter(isRecord);
|
|
80996
|
+
}
|
|
80997
|
+
/**
|
|
80998
|
+
* Finds the resolved version for a step whose "uses" still holds the
|
|
80999
|
+
* version placeholder.
|
|
81000
|
+
*/ function findResolvedVersionForPlaceholderStep(step, resolvedVersions) {
|
|
81001
|
+
const uses = step.uses;
|
|
81002
|
+
if (typeof uses !== "string" || !uses.endsWith(PLACEHOLDER_SUFFIX)) {
|
|
81003
|
+
return undefined;
|
|
81004
|
+
}
|
|
81005
|
+
const action = uses.slice(0, -PLACEHOLDER_SUFFIX.length);
|
|
81006
|
+
return findResolvedVersion(action, resolvedVersions);
|
|
81007
|
+
}
|
|
81008
|
+
/**
|
|
81009
|
+
* Lists the steps in a workflow's jobs that still hold the version
|
|
81010
|
+
* placeholder and for which a resolved version is available.
|
|
81011
|
+
* @param jobs The parsed "jobs" mapping of a workflow
|
|
81012
|
+
* @param resolvedVersions Resolved versions to apply
|
|
81013
|
+
*/ function findPlaceholderStepsToPin(jobs, resolvedVersions) {
|
|
81014
|
+
if (!resolvedVersions || resolvedVersions.length === 0) {
|
|
81015
|
+
return [];
|
|
81016
|
+
}
|
|
81017
|
+
return collectStepObjects(jobs).flatMap((step)=>{
|
|
81018
|
+
const resolved = findResolvedVersionForPlaceholderStep(step, resolvedVersions);
|
|
81019
|
+
return resolved ? [
|
|
81020
|
+
{
|
|
81021
|
+
step,
|
|
81022
|
+
resolved
|
|
81023
|
+
}
|
|
81024
|
+
] : [];
|
|
81025
|
+
});
|
|
81026
|
+
}
|
|
81027
|
+
/**
|
|
81028
|
+
* Lists the actions in an existing workflow whose steps still hold the version
|
|
81029
|
+
* placeholder and for which a resolved version is available.
|
|
81030
|
+
* @param existingWorkflow The parsed existing workflow
|
|
81031
|
+
* @param resolvedVersions Resolved versions to apply
|
|
81032
|
+
* @returns Action names that would be SHA-pinned by updateWorkflowWithMissingSteps
|
|
81033
|
+
*/ function findPlaceholderActionsToPin(existingWorkflow, resolvedVersions) {
|
|
81034
|
+
const jobs = isRecord(existingWorkflow) ? existingWorkflow.jobs : undefined;
|
|
81035
|
+
if (!isRecord(jobs)) {
|
|
81036
|
+
return [];
|
|
81037
|
+
}
|
|
81038
|
+
return findPlaceholderStepsToPin(jobs, resolvedVersions).map(({ resolved })=>resolved.action);
|
|
81039
|
+
}
|
|
81040
|
+
|
|
79555
81041
|
;// CONCATENATED MODULE: ../../node_modules/@github-actions-workflow-ts/lib/dist/esm/workflow/index.js
|
|
79556
81042
|
/**
|
|
79557
81043
|
* Represents a GitHub Actions workflow.
|
|
@@ -79943,6 +81429,10 @@ function getGlobalWacContext() {
|
|
|
79943
81429
|
|
|
79944
81430
|
|
|
79945
81431
|
|
|
81432
|
+
|
|
81433
|
+
/**
|
|
81434
|
+
* Name of the job that Copilot runs to prepare its environment.
|
|
81435
|
+
*/ const COPILOT_SETUP_JOB_NAME = "copilot-setup-steps";
|
|
79946
81436
|
const workflow_generator_DEFAULT_ACTION_VERSIONS = {
|
|
79947
81437
|
"actions/checkout": "v4"
|
|
79948
81438
|
};
|
|
@@ -80040,6 +81530,22 @@ const workflow_generator_defaultActionVersionPort = createActionVersionPort(work
|
|
|
80040
81530
|
steps.push(newStep.step);
|
|
80041
81531
|
}
|
|
80042
81532
|
}
|
|
81533
|
+
/**
|
|
81534
|
+
* Selects the job that receives missing setup steps: the copilot-setup-steps
|
|
81535
|
+
* job when it exists (Copilot runs only that job), otherwise the first job.
|
|
81536
|
+
*/ function selectSetupJobName(jobs) {
|
|
81537
|
+
return COPILOT_SETUP_JOB_NAME in jobs ? COPILOT_SETUP_JOB_NAME : Object.keys(jobs)[0];
|
|
81538
|
+
}
|
|
81539
|
+
/**
|
|
81540
|
+
* Replaces the version placeholder of existing steps with the resolved
|
|
81541
|
+
* SHA-pinned version, in place.
|
|
81542
|
+
*/ function pinPlaceholderSteps(jobs, resolvedVersions) {
|
|
81543
|
+
for (const { step, resolved } of findPlaceholderStepsToPin(jobs, resolvedVersions)){
|
|
81544
|
+
step.uses = buildUsesValue(resolved.action, resolved.sha, {
|
|
81545
|
+
resolvedVersions
|
|
81546
|
+
});
|
|
81547
|
+
}
|
|
81548
|
+
}
|
|
80043
81549
|
/**
|
|
80044
81550
|
* Gets the checkout version based on resolved versions, placeholders, or gateway.
|
|
80045
81551
|
* Reduces duplication by centralizing the version resolution logic.
|
|
@@ -80076,7 +81582,7 @@ const workflow_generator_defaultActionVersionPort = createActionVersionPort(work
|
|
|
80076
81582
|
new Step(checkoutStepProps),
|
|
80077
81583
|
...buildStepsFromCandidates(candidates, stepOptions)
|
|
80078
81584
|
];
|
|
80079
|
-
const job = new NormalJob(
|
|
81585
|
+
const job = new NormalJob(COPILOT_SETUP_JOB_NAME, {
|
|
80080
81586
|
"runs-on": "ubuntu-latest",
|
|
80081
81587
|
"timeout-minutes": 30,
|
|
80082
81588
|
permissions: options?.includeIdTokenPermission ? {
|
|
@@ -80115,7 +81621,10 @@ const workflow_generator_defaultActionVersionPort = createActionVersionPort(work
|
|
|
80115
81621
|
})}`;
|
|
80116
81622
|
}
|
|
80117
81623
|
/**
|
|
80118
|
-
* Updates an existing workflow by appending missing setup steps
|
|
81624
|
+
* Updates an existing workflow by appending missing setup steps to the
|
|
81625
|
+
* copilot-setup-steps job (or the first job when it is absent), and by
|
|
81626
|
+
* SHA-pinning existing steps that still hold the version placeholder when a
|
|
81627
|
+
* resolved version is provided.
|
|
80119
81628
|
* @param existingWorkflow The parsed existing workflow
|
|
80120
81629
|
* @param missingCandidates Candidates to add to the workflow
|
|
80121
81630
|
* @param versionGateway Optional gateway for looking up action versions (defaults to local)
|
|
@@ -80130,12 +81639,8 @@ const workflow_generator_defaultActionVersionPort = createActionVersionPort(work
|
|
|
80130
81639
|
if (!jobs) {
|
|
80131
81640
|
return generateWorkflowContent(missingCandidates, versionGateway, options);
|
|
80132
81641
|
}
|
|
80133
|
-
const
|
|
80134
|
-
|
|
80135
|
-
return generateWorkflowContent(missingCandidates, versionGateway, options);
|
|
80136
|
-
}
|
|
80137
|
-
const mainJobName = jobNames[0];
|
|
80138
|
-
const mainJob = jobs[mainJobName];
|
|
81642
|
+
const mainJobName = selectSetupJobName(jobs);
|
|
81643
|
+
const mainJob = mainJobName !== undefined ? jobs[mainJobName] : undefined;
|
|
80139
81644
|
if (!mainJob || !Array.isArray(mainJob.steps)) {
|
|
80140
81645
|
return generateWorkflowContent(missingCandidates, versionGateway, options);
|
|
80141
81646
|
}
|
|
@@ -80143,6 +81648,7 @@ const workflow_generator_defaultActionVersionPort = createActionVersionPort(work
|
|
|
80143
81648
|
usePlaceholders: options?.usePlaceholders,
|
|
80144
81649
|
resolvedVersions: options?.resolvedVersions
|
|
80145
81650
|
};
|
|
81651
|
+
pinPlaceholderSteps(jobs, options?.resolvedVersions);
|
|
80146
81652
|
appendMissingStepsToJob(mainJob.steps, missingCandidates, stepOptions);
|
|
80147
81653
|
return `---\n${(0,yaml_dist/* .stringify */.As)(workflow, {
|
|
80148
81654
|
lineWidth: 0
|
|
@@ -80159,6 +81665,7 @@ const workflow_generator_defaultActionVersionPort = createActionVersionPort(work
|
|
|
80159
81665
|
// Re-export workflow generation functionality
|
|
80160
81666
|
|
|
80161
81667
|
|
|
81668
|
+
|
|
80162
81669
|
;// CONCATENATED MODULE: ./src/tools/create-copilot-setup-workflow.ts
|
|
80163
81670
|
/**
|
|
80164
81671
|
* MCP tool handler for creating or updating Copilot Setup Steps workflow.
|
|
@@ -80171,10 +81678,8 @@ const workflow_generator_defaultActionVersionPort = createActionVersionPort(work
|
|
|
80171
81678
|
|
|
80172
81679
|
/**
|
|
80173
81680
|
* Gathers setup step candidates from environment and existing workflows.
|
|
80174
|
-
*/ async function gatherCandidates(dir, workflowsDirExists) {
|
|
80175
|
-
const environmentGateway =
|
|
80176
|
-
const workflowGateway = createWorkflowGateway();
|
|
80177
|
-
const copilotSetupConfig = await loadCopilotSetupConfig();
|
|
81681
|
+
*/ async function gatherCandidates(dir, workflowsDirExists, workflowGateway, copilotSetupConfig) {
|
|
81682
|
+
const environmentGateway = createEnvironmentGatewayWithConfig(copilotSetupConfig);
|
|
80178
81683
|
// Detect environment configuration
|
|
80179
81684
|
const environment = await environmentGateway.detectEnvironment(dir);
|
|
80180
81685
|
// Parse existing workflows for setup actions
|
|
@@ -80186,12 +81691,12 @@ const workflow_generator_defaultActionVersionPort = createActionVersionPort(work
|
|
|
80186
81691
|
}
|
|
80187
81692
|
/**
|
|
80188
81693
|
* Updates an existing workflow with missing steps.
|
|
80189
|
-
*/ async function updateExistingWorkflow(dir, workflowPath, allCandidates, args) {
|
|
80190
|
-
const workflowGateway = createWorkflowGateway();
|
|
81694
|
+
*/ async function updateExistingWorkflow(dir, workflowGateway, workflowPath, allCandidates, args) {
|
|
80191
81695
|
const existingWorkflow = await workflowGateway.readCopilotSetupWorkflow(dir);
|
|
80192
81696
|
const existingActions = getExistingActionsFromWorkflow(existingWorkflow);
|
|
80193
81697
|
const missingCandidates = findMissingCandidates(allCandidates, existingActions);
|
|
80194
|
-
|
|
81698
|
+
const actionsToPin = findPlaceholderActionsToPin(existingWorkflow, args.resolvedVersions);
|
|
81699
|
+
if (missingCandidates.length === 0 && actionsToPin.length === 0) {
|
|
80195
81700
|
// Build actions to resolve for version resolution metadata
|
|
80196
81701
|
const actionsToResolve = buildActionsToResolve(allCandidates, args.resolvedVersions);
|
|
80197
81702
|
return successResponse({
|
|
@@ -80215,7 +81720,8 @@ const workflow_generator_defaultActionVersionPort = createActionVersionPort(work
|
|
|
80215
81720
|
action: "updated",
|
|
80216
81721
|
workflowPath,
|
|
80217
81722
|
stepsAdded: missingCandidates.map((c)=>c.action),
|
|
80218
|
-
|
|
81723
|
+
actionsPinned: actionsToPin,
|
|
81724
|
+
message: `Updated workflow with ${missingCandidates.length} new step(s) and ${actionsToPin.length} SHA-pinned action(s)`,
|
|
80219
81725
|
workflowTemplate: updatedContent,
|
|
80220
81726
|
actionsToResolve,
|
|
80221
81727
|
instructions: actionsToResolve.length > 0 ? VERSION_RESOLUTION_INSTRUCTIONS : undefined
|
|
@@ -80223,8 +81729,7 @@ const workflow_generator_defaultActionVersionPort = createActionVersionPort(work
|
|
|
80223
81729
|
}
|
|
80224
81730
|
/**
|
|
80225
81731
|
* Creates a new workflow with the provided candidates.
|
|
80226
|
-
*/ async function createNewWorkflow(dir, workflowPath, allCandidates, args) {
|
|
80227
|
-
const workflowGateway = createWorkflowGateway();
|
|
81732
|
+
*/ async function createNewWorkflow(dir, workflowGateway, workflowPath, allCandidates, args) {
|
|
80228
81733
|
// Generate workflow with placeholder mode and resolved versions
|
|
80229
81734
|
const content = await generateWorkflowContent(allCandidates, undefined, {
|
|
80230
81735
|
usePlaceholders: true,
|
|
@@ -80244,33 +81749,45 @@ const workflow_generator_defaultActionVersionPort = createActionVersionPort(work
|
|
|
80244
81749
|
});
|
|
80245
81750
|
}
|
|
80246
81751
|
/**
|
|
80247
|
-
*
|
|
80248
|
-
|
|
80249
|
-
|
|
80250
|
-
|
|
80251
|
-
|
|
80252
|
-
|
|
80253
|
-
|
|
80254
|
-
|
|
80255
|
-
|
|
80256
|
-
|
|
80257
|
-
|
|
80258
|
-
|
|
80259
|
-
await (
|
|
80260
|
-
|
|
80261
|
-
|
|
80262
|
-
|
|
80263
|
-
|
|
80264
|
-
|
|
80265
|
-
|
|
80266
|
-
|
|
81752
|
+
* Builds the handler for creating or updating the Copilot Setup Steps
|
|
81753
|
+
* workflow. The configuration is loaded once per call and shared by the
|
|
81754
|
+
* environment detection and the workflow scan, so the two cannot see two
|
|
81755
|
+
* different versions of the file.
|
|
81756
|
+
*/ function buildCreateCopilotSetupWorkflowHandler(ports) {
|
|
81757
|
+
return async (args)=>{
|
|
81758
|
+
try {
|
|
81759
|
+
const dir = args.targetDir || process.cwd();
|
|
81760
|
+
const targetDirError = await validateTargetDirectory(dir);
|
|
81761
|
+
if (targetDirError) {
|
|
81762
|
+
return targetDirError;
|
|
81763
|
+
}
|
|
81764
|
+
const copilotSetupConfig = await ports.loadConfig(dir);
|
|
81765
|
+
const workflowGateway = createWorkflowGatewayWithConfig(copilotSetupConfig);
|
|
81766
|
+
const workflowsDir = await file_system_utils_resolveSafePath(dir, ".github/workflows");
|
|
81767
|
+
const workflowsDirExists = await file_system_utils_fileExists(workflowsDir);
|
|
81768
|
+
const allCandidates = await gatherCandidates(dir, workflowsDirExists, workflowGateway, copilotSetupConfig);
|
|
81769
|
+
if (!workflowsDirExists) {
|
|
81770
|
+
await (0,promises_.mkdir)(workflowsDir, {
|
|
81771
|
+
recursive: true
|
|
81772
|
+
});
|
|
81773
|
+
}
|
|
81774
|
+
const workflowPath = await workflowGateway.getCopilotSetupWorkflowPath(dir);
|
|
81775
|
+
const workflowExists = await workflowGateway.copilotSetupWorkflowExists(dir);
|
|
81776
|
+
if (workflowExists) {
|
|
81777
|
+
return await updateExistingWorkflow(dir, workflowGateway, workflowPath, allCandidates, args);
|
|
81778
|
+
}
|
|
81779
|
+
return await createNewWorkflow(dir, workflowGateway, workflowPath, allCandidates, args);
|
|
81780
|
+
} catch (error) {
|
|
81781
|
+
const message = error instanceof Error ? error.message : "Unknown error occurred";
|
|
81782
|
+
return types_errorResponse(`Failed to create/update workflow: ${message}`);
|
|
80267
81783
|
}
|
|
80268
|
-
|
|
80269
|
-
|
|
80270
|
-
|
|
80271
|
-
|
|
80272
|
-
|
|
80273
|
-
|
|
81784
|
+
};
|
|
81785
|
+
}
|
|
81786
|
+
/**
|
|
81787
|
+
* Creates or updates the Copilot Setup Steps workflow.
|
|
81788
|
+
*/ const createCopilotSetupWorkflowHandler = buildCreateCopilotSetupWorkflowHandler({
|
|
81789
|
+
loadConfig: loadCopilotSetupConfig
|
|
81790
|
+
});
|
|
80274
81791
|
|
|
80275
81792
|
;// CONCATENATED MODULE: ./src/tools/discover-environment.ts
|
|
80276
81793
|
/**
|
|
@@ -80282,10 +81799,11 @@ const workflow_generator_defaultActionVersionPort = createActionVersionPort(work
|
|
|
80282
81799
|
*/ const discoverEnvironmentHandler = async (args)=>{
|
|
80283
81800
|
try {
|
|
80284
81801
|
const dir = args.targetDir || process.cwd();
|
|
80285
|
-
|
|
80286
|
-
|
|
81802
|
+
const targetDirError = await validateTargetDirectory(dir);
|
|
81803
|
+
if (targetDirError) {
|
|
81804
|
+
return targetDirError;
|
|
80287
81805
|
}
|
|
80288
|
-
const environmentGateway = createEnvironmentGateway();
|
|
81806
|
+
const environmentGateway = createEnvironmentGateway(dir);
|
|
80289
81807
|
const environment = await environmentGateway.detectEnvironment(dir);
|
|
80290
81808
|
return successResponse({
|
|
80291
81809
|
hasMise: environment.hasMise,
|
|
@@ -80366,16 +81884,30 @@ const workflow_generator_defaultActionVersionPort = createActionVersionPort(work
|
|
|
80366
81884
|
|
|
80367
81885
|
|
|
80368
81886
|
|
|
81887
|
+
/** Groups items by their phase, each as the entry the response shows. */ function groupByPhase(items, toEntry) {
|
|
81888
|
+
const grouped = {};
|
|
81889
|
+
for (const item of items){
|
|
81890
|
+
grouped[item.phase] = [
|
|
81891
|
+
...grouped[item.phase] ?? [],
|
|
81892
|
+
toEntry(item)
|
|
81893
|
+
];
|
|
81894
|
+
}
|
|
81895
|
+
return grouped;
|
|
81896
|
+
}
|
|
80369
81897
|
/**
|
|
80370
81898
|
* Discovers npm scripts from package.json and CLI tools from GitHub Actions workflows,
|
|
80371
81899
|
* mapping them to SDLC feedback loop phases (test, build, lint, format, etc.).
|
|
80372
81900
|
*/ const discoverFeedbackLoopsHandler = async (args)=>{
|
|
80373
81901
|
const dir = args.targetDir || process.cwd();
|
|
81902
|
+
const targetDirError = await validateTargetDirectory(dir);
|
|
81903
|
+
if (targetDirError) {
|
|
81904
|
+
return targetDirError;
|
|
81905
|
+
}
|
|
80374
81906
|
try {
|
|
80375
81907
|
// Create gateways
|
|
80376
81908
|
const scriptGateway = createScriptDiscoveryGateway();
|
|
80377
81909
|
const toolGateway = createToolDiscoveryGateway();
|
|
80378
|
-
const environmentGateway = createEnvironmentGateway();
|
|
81910
|
+
const environmentGateway = createEnvironmentGateway(dir);
|
|
80379
81911
|
// Adapter for package manager gateway
|
|
80380
81912
|
const packageManagerGateway = {
|
|
80381
81913
|
async detectPackageManagers (targetDir) {
|
|
@@ -80391,29 +81923,17 @@ const workflow_generator_defaultActionVersionPort = createActionVersionPort(work
|
|
|
80391
81923
|
});
|
|
80392
81924
|
const { scripts, tools, packageManager } = result.feedbackLoops;
|
|
80393
81925
|
// Group by phase for better readability
|
|
80394
|
-
const scriptsByPhase = scripts
|
|
80395
|
-
if (!acc[script.phase]) {
|
|
80396
|
-
acc[script.phase] = [];
|
|
80397
|
-
}
|
|
80398
|
-
acc[script.phase].push({
|
|
81926
|
+
const scriptsByPhase = groupByPhase(scripts, (script)=>({
|
|
80399
81927
|
name: script.name,
|
|
80400
81928
|
command: script.command,
|
|
80401
81929
|
mandatory: script.isMandatory
|
|
80402
|
-
});
|
|
80403
|
-
|
|
80404
|
-
}, {});
|
|
80405
|
-
const toolsByPhase = tools.reduce((acc, tool)=>{
|
|
80406
|
-
if (!acc[tool.phase]) {
|
|
80407
|
-
acc[tool.phase] = [];
|
|
80408
|
-
}
|
|
80409
|
-
acc[tool.phase].push({
|
|
81930
|
+
}));
|
|
81931
|
+
const toolsByPhase = groupByPhase(tools, (tool)=>({
|
|
80410
81932
|
name: tool.name,
|
|
80411
81933
|
command: tool.fullCommand,
|
|
80412
81934
|
mandatory: tool.isMandatory,
|
|
80413
81935
|
source: tool.sourceWorkflow
|
|
80414
|
-
});
|
|
80415
|
-
return acc;
|
|
80416
|
-
}, {});
|
|
81936
|
+
}));
|
|
80417
81937
|
const mandatoryScripts = scripts.filter((s)=>s.isMandatory);
|
|
80418
81938
|
const mandatoryTools = tools.filter((t)=>t.isMandatory);
|
|
80419
81939
|
return successResponse({
|
|
@@ -80444,10 +81964,11 @@ const workflow_generator_defaultActionVersionPort = createActionVersionPort(work
|
|
|
80444
81964
|
*/ const discoverWorkflowSetupActionsHandler = async (args)=>{
|
|
80445
81965
|
try {
|
|
80446
81966
|
const dir = args.targetDir || process.cwd();
|
|
80447
|
-
|
|
80448
|
-
|
|
81967
|
+
const targetDirError = await validateTargetDirectory(dir);
|
|
81968
|
+
if (targetDirError) {
|
|
81969
|
+
return targetDirError;
|
|
80449
81970
|
}
|
|
80450
|
-
const workflowGateway = createWorkflowGateway();
|
|
81971
|
+
const workflowGateway = createWorkflowGateway(dir);
|
|
80451
81972
|
const workflowsDir = (0,external_node_path_.join)(dir, ".github", "workflows");
|
|
80452
81973
|
const workflowsDirExists = await file_system_utils_fileExists(workflowsDir);
|
|
80453
81974
|
if (!workflowsDirExists) {
|
|
@@ -80483,8 +82004,9 @@ const workflow_generator_defaultActionVersionPort = createActionVersionPort(work
|
|
|
80483
82004
|
*/ const readCopilotSetupWorkflowHandler = async (args)=>{
|
|
80484
82005
|
try {
|
|
80485
82006
|
const dir = args.targetDir || process.cwd();
|
|
80486
|
-
|
|
80487
|
-
|
|
82007
|
+
const targetDirError = await validateTargetDirectory(dir);
|
|
82008
|
+
if (targetDirError) {
|
|
82009
|
+
return targetDirError;
|
|
80488
82010
|
}
|
|
80489
82011
|
const workflowGateway = createWorkflowGateway();
|
|
80490
82012
|
const exists = await workflowGateway.copilotSetupWorkflowExists(dir);
|
|
@@ -80521,13 +82043,14 @@ const workflow_generator_defaultActionVersionPort = createActionVersionPort(work
|
|
|
80521
82043
|
*/
|
|
80522
82044
|
|
|
80523
82045
|
|
|
80524
|
-
|
|
80525
82046
|
/**
|
|
80526
82047
|
* Resolves action versions for the provided actions or detected candidates.
|
|
80527
82048
|
*/ const resolveActionVersionsHandler = async (args)=>{
|
|
80528
82049
|
try {
|
|
80529
82050
|
if (args.actions && args.actions.length > 0) {
|
|
80530
|
-
const actionsToResolve =
|
|
82051
|
+
const actionsToResolve = [
|
|
82052
|
+
...new Set(args.actions)
|
|
82053
|
+
].filter((action)=>{
|
|
80531
82054
|
if (args.resolvedVersions) {
|
|
80532
82055
|
return !args.resolvedVersions.some((r)=>r.action === action);
|
|
80533
82056
|
}
|
|
@@ -80540,8 +82063,9 @@ const workflow_generator_defaultActionVersionPort = createActionVersionPort(work
|
|
|
80540
82063
|
});
|
|
80541
82064
|
}
|
|
80542
82065
|
const dir = args.targetDir || process.cwd();
|
|
80543
|
-
|
|
80544
|
-
|
|
82066
|
+
const targetDirError = await validateTargetDirectory(dir);
|
|
82067
|
+
if (targetDirError) {
|
|
82068
|
+
return targetDirError;
|
|
80545
82069
|
}
|
|
80546
82070
|
const config = await loadCopilotSetupConfig(dir);
|
|
80547
82071
|
const defaultActions = [
|
|
@@ -80566,10 +82090,37 @@ const workflow_generator_defaultActionVersionPort = createActionVersionPort(work
|
|
|
80566
82090
|
}
|
|
80567
82091
|
};
|
|
80568
82092
|
|
|
82093
|
+
;// CONCATENATED MODULE: ../core/src/entities/coverage-discovery-caveat.ts
|
|
82094
|
+
/**
|
|
82095
|
+
* What a coverage result says when instruction file discovery left files out
|
|
82096
|
+
* (it hit a limit, or skipped a symbolic link that points outside the
|
|
82097
|
+
* repository). A mandatory item reported as undocumented may be documented in
|
|
82098
|
+
* a file that was not checked, and a fully documented result covers only the
|
|
82099
|
+
* files found. The use case that writes the suggestions and the MCP tool that
|
|
82100
|
+
* writes the message use these once, so they cannot say different things.
|
|
82101
|
+
*/ /** Whether discovery reported anything about itself, which means it left files out. */ function discoveryLeftFilesOut(diagnostics) {
|
|
82102
|
+
return diagnostics.length > 0;
|
|
82103
|
+
}
|
|
82104
|
+
const ALL_DOCUMENTED_IN_FILES_FOUND = "✅ All mandatory feedback loops are documented in the instruction files found, but discovery left some instruction files out, so they were not checked";
|
|
82105
|
+
const UNDOCUMENTED_IN_FILES_FOUND = " in the instruction files found; discovery left some instruction files out, so they may be documented in files that were not checked";
|
|
82106
|
+
/**
|
|
82107
|
+
* The statement that every mandatory item is documented: the caller's own
|
|
82108
|
+
* sentence, or the qualified one when discovery left files out.
|
|
82109
|
+
*/ function qualifyAllDocumented(plain, leftFilesOut) {
|
|
82110
|
+
return leftFilesOut ? ALL_DOCUMENTED_IN_FILES_FOUND : plain;
|
|
82111
|
+
}
|
|
82112
|
+
/**
|
|
82113
|
+
* The statement that mandatory items are not documented, with the caveat that
|
|
82114
|
+
* they may be documented in a file discovery left out.
|
|
82115
|
+
*/ function qualifyUndocumented(plain, leftFilesOut) {
|
|
82116
|
+
return leftFilesOut ? `${plain}${UNDOCUMENTED_IN_FILES_FOUND}` : plain;
|
|
82117
|
+
}
|
|
82118
|
+
|
|
80569
82119
|
;// CONCATENATED MODULE: ../core/src/use-cases/validate-instruction-coverage.ts
|
|
80570
82120
|
/**
|
|
80571
82121
|
* Use case for validating instruction coverage of feedback loops
|
|
80572
|
-
*/
|
|
82122
|
+
*/
|
|
82123
|
+
/**
|
|
80573
82124
|
* Use case for validating that repository instructions cover mandatory feedback loops
|
|
80574
82125
|
*/ class ValidateInstructionCoverageUseCase {
|
|
80575
82126
|
discoverFeedbackLoops;
|
|
@@ -80596,11 +82147,14 @@ const workflow_generator_defaultActionVersionPort = createActionVersionPort(work
|
|
|
80596
82147
|
}
|
|
80597
82148
|
generateSuggestions(coverage, packageManager) {
|
|
80598
82149
|
const suggestions = [];
|
|
82150
|
+
const leftFilesOut = discoveryLeftFilesOut(coverage.diagnostics);
|
|
80599
82151
|
if (coverage.missingInInstructions.length === 0) {
|
|
80600
|
-
suggestions.push("✅ All mandatory feedback loops are documented in instructions");
|
|
82152
|
+
suggestions.push(qualifyAllDocumented("✅ All mandatory feedback loops are documented in instructions", leftFilesOut));
|
|
80601
82153
|
return suggestions;
|
|
80602
82154
|
}
|
|
80603
|
-
|
|
82155
|
+
const missingCount = coverage.missingInInstructions.length;
|
|
82156
|
+
const undocumented = qualifyUndocumented(`⚠️ ${missingCount} mandatory feedback loop(s) are not documented`, leftFilesOut);
|
|
82157
|
+
suggestions.push(`${undocumented}:`);
|
|
80604
82158
|
suggestions.push("");
|
|
80605
82159
|
const byPhase = new Map();
|
|
80606
82160
|
for (const item of coverage.missingInInstructions){
|
|
@@ -80635,17 +82189,34 @@ const workflow_generator_defaultActionVersionPort = createActionVersionPort(work
|
|
|
80635
82189
|
|
|
80636
82190
|
|
|
80637
82191
|
|
|
82192
|
+
|
|
80638
82193
|
/**
|
|
80639
|
-
*
|
|
82194
|
+
* Summarizes the coverage. When discovery left instruction files out (it hit a
|
|
82195
|
+
* limit, or skipped a symbolic link that points outside the repository) the
|
|
82196
|
+
* result says so: a mandatory item reported as undocumented may be documented
|
|
82197
|
+
* in a file that was not checked, and a fully documented result covers only the
|
|
82198
|
+
* files found.
|
|
82199
|
+
*/ function coverageMessage({ hasFullCoverage, coveragePercentage, missingCount, leftFilesOut }) {
|
|
82200
|
+
if (hasFullCoverage) {
|
|
82201
|
+
return qualifyAllDocumented("✅ All mandatory feedback loops are documented", leftFilesOut);
|
|
82202
|
+
}
|
|
82203
|
+
return qualifyUndocumented(`⚠️ ${coveragePercentage.toFixed(1)}% coverage - ${missingCount} mandatory items not documented`, leftFilesOut);
|
|
82204
|
+
}
|
|
82205
|
+
/**
|
|
82206
|
+
* Validates that repository instruction files (the catalog shared with analyze_instruction_quality)
|
|
80640
82207
|
* document all mandatory feedback loop scripts and tools.
|
|
80641
82208
|
* Returns coverage percentage and suggestions for missing documentation.
|
|
80642
82209
|
*/ const validateInstructionCoverageHandler = async (args)=>{
|
|
80643
82210
|
const dir = args.targetDir || process.cwd();
|
|
82211
|
+
const targetDirError = await validateTargetDirectory(dir);
|
|
82212
|
+
if (targetDirError) {
|
|
82213
|
+
return targetDirError;
|
|
82214
|
+
}
|
|
80644
82215
|
try {
|
|
80645
82216
|
// Create gateways
|
|
80646
82217
|
const scriptGateway = createScriptDiscoveryGateway();
|
|
80647
82218
|
const toolGateway = createToolDiscoveryGateway();
|
|
80648
|
-
const environmentGateway = createEnvironmentGateway();
|
|
82219
|
+
const environmentGateway = createEnvironmentGateway(dir);
|
|
80649
82220
|
const instructionGateway = createInstructionAnalysisGateway();
|
|
80650
82221
|
// Adapter for package manager gateway
|
|
80651
82222
|
const packageManagerGateway = {
|
|
@@ -80708,7 +82279,16 @@ const workflow_generator_defaultActionVersionPort = createActionVersionPort(work
|
|
|
80708
82279
|
missing,
|
|
80709
82280
|
documented,
|
|
80710
82281
|
suggestions,
|
|
80711
|
-
|
|
82282
|
+
diagnostics: coverage.diagnostics.map((d)=>({
|
|
82283
|
+
ruleId: d.ruleId,
|
|
82284
|
+
message: d.message
|
|
82285
|
+
})),
|
|
82286
|
+
message: coverageMessage({
|
|
82287
|
+
hasFullCoverage,
|
|
82288
|
+
coveragePercentage: coverage.summary.coveragePercentage,
|
|
82289
|
+
missingCount: missing.length,
|
|
82290
|
+
leftFilesOut: discoveryLeftFilesOut(coverage.diagnostics)
|
|
82291
|
+
})
|
|
80712
82292
|
});
|
|
80713
82293
|
} catch (error) {
|
|
80714
82294
|
return types_errorResponse(`Failed to validate instruction coverage: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
@@ -80741,12 +82321,13 @@ const workflow_generator_defaultActionVersionPort = createActionVersionPort(work
|
|
|
80741
82321
|
*/
|
|
80742
82322
|
|
|
80743
82323
|
|
|
82324
|
+
|
|
80744
82325
|
/**
|
|
80745
82326
|
* Resolved version schema for input validation.
|
|
80746
82327
|
*/ const resolvedVersionSchema = schemas_object({
|
|
80747
82328
|
action: schemas_string().describe("Action name (e.g., 'actions/setup-node')"),
|
|
80748
|
-
sha: schemas_string().describe("
|
|
80749
|
-
versionTag: schemas_string().describe("Version tag (e.g., 'v4.0.0')")
|
|
82329
|
+
sha: schemas_string().regex(/^[0-9a-f]{40}$/i, "sha must be a full 40-character commit SHA").describe("Full 40-character commit SHA for the action version"),
|
|
82330
|
+
versionTag: schemas_string().regex(/^[A-Za-z0-9][A-Za-z0-9._+/-]*$/, "versionTag must be a single-line version tag").describe("Version tag (e.g., 'v4.0.0')")
|
|
80750
82331
|
});
|
|
80751
82332
|
/**
|
|
80752
82333
|
* Shared input schema for tools that operate on a target directory.
|
|
@@ -80789,7 +82370,7 @@ const workflow_generator_defaultActionVersionPort = createActionVersionPort(work
|
|
|
80789
82370
|
},
|
|
80790
82371
|
{
|
|
80791
82372
|
name: "validate_instruction_coverage",
|
|
80792
|
-
description: "Validate that repository
|
|
82373
|
+
description: "Validate that repository instruction files (the same files analyze_instruction_quality discovers: .github/copilot-instructions.md, .github/instructions/**/*.instructions.md, .github/agents/*.md, AGENTS.md, AGENTS.override.md, CLAUDE.md, .claude/CLAUDE.md) document all mandatory feedback loop scripts and tools. Returns coverage percentage and suggestions for missing documentation.",
|
|
80793
82374
|
handler: validateInstructionCoverageHandler,
|
|
80794
82375
|
inputSchema: targetDirInputSchema
|
|
80795
82376
|
},
|
|
@@ -80847,7 +82428,7 @@ const workflow_generator_defaultActionVersionPort = createActionVersionPort(work
|
|
|
80847
82428
|
*/ function createMcpServer() {
|
|
80848
82429
|
const server = new McpServer({
|
|
80849
82430
|
name: "lousy-agents",
|
|
80850
|
-
version:
|
|
82431
|
+
version: readPackageVersion()
|
|
80851
82432
|
});
|
|
80852
82433
|
registerTools(server);
|
|
80853
82434
|
return server;
|
|
@@ -91334,4 +92915,4 @@ if (installedChunkData !== 0) { // 0 means "already installed".'
|
|
|
91334
92915
|
// module factories are used so entry inlining is disabled
|
|
91335
92916
|
// startup
|
|
91336
92917
|
// Load entry module and return exports
|
|
91337
|
-
var __webpack_exports__ = __webpack_require__(
|
|
92918
|
+
var __webpack_exports__ = __webpack_require__(8066);
|