@lousy-agents/mcp 5.22.27 → 5.23.0

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.
Files changed (2) hide show
  1. package/dist/mcp-server.js +2406 -825
  2. package/package.json +1 -1
@@ -6559,7 +6559,7 @@ function escapeJsonPtr(str) {
6559
6559
 
6560
6560
 
6561
6561
  },
6562
- 3904(__unused_rspack_module, __unused_rspack___webpack_exports__, __webpack_require__) {
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/promises"
28467
- var promises_ = __webpack_require__(1455);
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
- function isPathWithinRoot(rootPath, candidatePath) {
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 = null;
53226
- constructor(cwd){
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
- const MAX_INSTRUCTION_FILE_BYTES = 1_048_576;
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 documentedTargets = new Set();
54852
+ const unreadable = [];
53793
54853
  for (const file of instructionFiles){
53794
- const content = await file_system_utils_readTextWithinRoot(targetDir, file, MAX_INSTRUCTION_FILE_BYTES);
53795
- const lines = content.split("\n");
53796
- for (const script of scripts){
53797
- const scriptRefs = this.findReferencesInContent(script.name, content, lines, (0,external_node_path_.join)(targetDir, file), targetDir);
53798
- if (scriptRefs.length > 0) {
53799
- references.push(...scriptRefs);
53800
- documentedTargets.add(script.name);
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 safePathExists(targetDir, relativePath) {
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
- return await file_system_utils_pathExistsWithinRoot(targetDir, relativePath);
53836
- } catch {
53837
- return false;
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
- async findInstructionFiles(targetDir) {
53841
- const files = [];
53842
- const copilotInstructions = (0,external_node_path_.join)(".github", "copilot-instructions.md");
53843
- if (await this.safePathExists(targetDir, copilotInstructions)) {
53844
- files.push(copilotInstructions);
53845
- }
53846
- const instructionsDir = (0,external_node_path_.join)(".github", "instructions");
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
- * Iterates over all steps in a workflow that have a "uses" field
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
- * @param callback Function to call for each step with a "uses" field
56538
- */ function forEachWorkflowStep(workflow, callback) {
56539
- if (!workflow || typeof workflow !== "object") {
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
- for (const job of Object.values(jobs)){
56547
- if (!job || typeof job !== "object") {
56548
- continue;
56549
- }
56550
- const steps = job.steps;
56551
- if (!Array.isArray(steps)) {
56552
- continue;
56553
- }
56554
- for (const step of steps){
56555
- if (!step || typeof step !== "object") {
56556
- continue;
56557
- }
56558
- const stepObj = step;
56559
- const uses = stepObj.uses;
56560
- if (typeof uses === "string") {
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
- const steps = [];
56576
- if (!workflow || typeof workflow !== "object") {
56577
- return steps;
56578
- }
56579
- const jobs = workflow.jobs;
56580
- if (!jobs || typeof jobs !== "object") {
56581
- return steps;
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
- for (const job of Object.values(jobs)){
56584
- if (!job || typeof job !== "object") {
56585
- continue;
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 steps;
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
- const actions = new Set();
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)=>!existingActions.has(candidate.action));
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
- const result = [];
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
- const actions = [];
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
- if (version) {
56662
- actions.push({
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
- const candidates = [];
56677
- forEachWorkflowStep(workflow, (step)=>{
57406
+ return workflowUsesSteps(workflow).flatMap((step)=>{
56678
57407
  const action = parseActionName(step.uses);
56679
- if (isSetupAction(action, patterns)) {
56680
- candidates.push({
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 = null;
56723
- constructor(cwd){
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 dirStat = await (0,promises_.stat)(dir).catch(()=>null);
56895
- if (dirStat === null || !dirStat.isDirectory()) {
56896
- return types_errorResponse(`Target directory does not exist: ${dir}`);
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
- * Maps a 0-based string offset into a 1-based line and column.
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
- const safeOffset = Math.max(0, Math.min(offset, content.length));
56947
- let line = 1;
56948
- let column = 1;
56949
- for(let i = 0; i < safeOffset; i++){
56950
- if (content[i] === "\n") {
56951
- line += 1;
56952
- column = 1;
56953
- } else {
56954
- column += 1;
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/instruction-import-expand.ts
57930
+ ;// CONCATENATED MODULE: ../core/src/lib/import-file-location.ts
56964
57931
  /**
56965
- * Pure Claude `@` import expander that builds an ordered EffectiveDocument.
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
- const DEFAULT_MAX_IMPORT_DEPTH = 4;
56969
- const DEFAULT_MAX_UNIQUE_FILES = 64;
56970
- const DEFAULT_MAX_EDGES = 256;
56971
- const DEFAULT_MAX_EMITTED_BYTES = 512_000;
56972
- const DEFAULT_MAX_FILE_BYTES = 1_048_576;
56973
- /** Aligns with doctor HARD_IMPORT: line-start `@path` where path includes `/`. */ const HARD_IMPORT_GLOBAL_RE = /^@([^\s@][^\s]*)/gm;
56974
- const FENCED_CODE_RE = /^(`{3,}|~{3,})[^\r\n]*\r?\n[\s\S]*?^\1[ \t]*$/gm;
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
- function resolveLimits(overrides) {
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
- maxDepth: overrides?.maxDepth ?? DEFAULT_MAX_IMPORT_DEPTH,
56979
- maxUniqueFiles: overrides?.maxUniqueFiles ?? DEFAULT_MAX_UNIQUE_FILES,
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
- function toPosixRelative(pathValue) {
56986
- return pathValue.split(external_node_path_.sep).join("/");
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
- function isInsideCodeRegion(index, regions) {
56989
- return regions.some((region)=>index >= region.start && index < region.end);
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
- function collectRegexRanges(content, pattern, skipInside) {
56992
- const regions = [];
56993
- for (const match of content.matchAll(pattern)){
56994
- if (match.index === undefined) {
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
- if (skipInside && isInsideCodeRegion(match.index, skipInside)) {
58234
+ const bodyStart = fenceBodyStart(content, start + length, findLineBreak);
58235
+ if (bodyStart === undefined) {
56998
58236
  continue;
56999
58237
  }
57000
- regions.push({
57001
- start: match.index,
57002
- end: match.index + match[0].length
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
- function findCodeRegions(content) {
57008
- const fenced = collectRegexRanges(content, FENCED_CODE_RE);
57009
- const inline = collectRegexRanges(content, INLINE_CODE_RE, fenced);
57010
- return [
57011
- ...fenced,
57012
- ...inline
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 findImportTokens(content) {
57016
- const codeRegions = findCodeRegions(content);
57017
- const tokens = [];
57018
- for (const match of content.matchAll(HARD_IMPORT_GLOBAL_RE)){
57019
- if (match.index === undefined) {
57020
- continue;
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
- const rawTarget = match[1];
57023
- if (!rawTarget?.includes("/")) {
57024
- continue;
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
- if (isInsideCodeRegion(match.index, codeRegions)) {
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: match.index,
57031
- end: match.index + match[0].length,
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)=>`Import target escapes the repository root: ${rawTarget}`,
57080
- absolute: (rawTarget)=>`Absolute import paths are not allowed: ${rawTarget}`,
57081
- home: (rawTarget)=>`Home-directory import paths are not allowed: ${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
- function normalizeRelativeWithinRoot(importerRelativePath, rawTarget) {
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
- contentCache = new Map();
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
- async readFile(repoRoot, relativePath) {
57248
- const cached = this.contentCache.get(relativePath);
57249
- if (cached !== undefined) {
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: cached
58683
+ content: knownContent,
58684
+ real: known
57253
58685
  };
57254
58686
  }
57255
- try {
57256
- await resolvePathWithinRoot(repoRoot, relativePath);
57257
- } catch (error) {
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
- try {
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
- const content = await file_system_utils_readTextWithinRoot(repoRoot, relativePath, this.limits.maxFileBytes);
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 true;
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 complete;
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.edgeCount >= session.limits.maxEdges) {
57351
- session.recordFailure(importerPath, token, "size-exceeded");
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
- if (stack.includes(targetPath)) {
57372
- session.recordFailure(importerPath, token, "cycle", targetPath);
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
- const readResult = await session.readFile(repoRoot, targetPath);
57378
- if (!readResult.ok) {
57379
- session.recordFailure(importerPath, token, readResult.status, targetPath);
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", targetPath);
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, targetPath, readResult.content, nextHop, [
58842
+ const child = await expandContent(session, repoRoot, targetReal, readResult.content, nextHop, [
57391
58843
  ...stack,
57392
- targetPath
58844
+ targetReal
57393
58845
  ], [
57394
58846
  ...importChain,
57395
- targetPath
58847
+ targetReal
57396
58848
  ]);
57397
- session.recordResolved(importerPath, token, targetPath);
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 literalOk = emitSourceSlice(session, builder, sourcePath, content, cursor, token.start, importChain);
57410
- if (!literalOk) {
57411
- session.recordFailure(sourcePath, token, "size-exceeded");
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 tokenOk = emitSourceSlice(session, builder, sourcePath, content, token.start, token.end, importChain);
57420
- if (!tokenOk) {
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 session = new ExpansionSession(limits);
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, rootRelativePath, rootRead.content, 0, [
57443
- rootRelativePath
58902
+ const expanded = await expandContent(session, input.repoRoot, rootRead.real, rootRead.content, 0, [
58903
+ rootRead.real
57444
58904
  ], [
57445
- rootRelativePath
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 mapExpansionDiagnostics(repoRoot, diagnostics) {
57475
- const contentByRelative = new Map();
57476
- const mapped = [];
57477
- for (const diagnostic of diagnostics){
57478
- const absolutePath = toAbsolutePath(repoRoot, diagnostic.filePath);
57479
- let content = contentByRelative.get(diagnostic.filePath);
57480
- if (content === undefined) {
57481
- content = await (0,promises_.readFile)(absolutePath, "utf8");
57482
- contentByRelative.set(diagnostic.filePath, content);
57483
- }
57484
- if (diagnostic.range === undefined) {
57485
- mapped.push({
57486
- ruleId: diagnostic.ruleId,
57487
- message: diagnostic.message,
57488
- filePath: absolutePath,
57489
- line: 1,
57490
- column: 1
57491
- });
57492
- continue;
57493
- }
57494
- const start = offsetToSourcePosition(content, diagnostic.range.start);
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: start.line,
57501
- column: start.column,
57502
- endLine: end.line,
57503
- endColumn: end.column
57504
- });
58958
+ line: 1,
58959
+ column: 1
58960
+ };
57505
58961
  }
57506
- return mapped;
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 discoveredFiles = await this.discoveryGateway.discoverInstructionFiles(parsed.targetDir);
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/*.md, .github/agents/*.md, AGENTS.md, CLAUDE.md"
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
- /** Maximum instruction file size: 1 MB */ const markdown_ast_gateway_MAX_INSTRUCTION_FILE_BYTES = 1_048_576;
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, markdown_ast_gateway_MAX_INSTRUCTION_FILE_BYTES);
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
- if (!await file_system_utils_fileExists(dir)) {
78858
- return types_errorResponse(`Target directory does not exist: ${dir}`);
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
- if (!await file_system_utils_fileExists(dir)) {
79242
- return types_errorResponse(`Target directory does not exist: ${dir}`);
80667
+ const targetDirError = await validateTargetDirectory(dir);
80668
+ if (targetDirError) {
80669
+ return targetDirError;
79243
80670
  }
79244
- const environmentGateway = createEnvironmentGateway();
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((c)=>c.action)
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("copilot-setup-steps", {
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 jobNames = Object.keys(jobs);
80134
- if (jobNames.length === 0) {
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 = createEnvironmentGateway();
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
- if (missingCandidates.length === 0) {
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
- message: `Updated workflow with ${missingCandidates.length} new step(s)`,
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
- * Creates or updates the Copilot Setup Steps workflow.
80248
- */ const createCopilotSetupWorkflowHandler = async (args)=>{
80249
- try {
80250
- const dir = args.targetDir || process.cwd();
80251
- if (!await file_system_utils_fileExists(dir)) {
80252
- return types_errorResponse(`Target directory does not exist: ${dir}`);
80253
- }
80254
- const workflowGateway = createWorkflowGateway();
80255
- const workflowsDir = await file_system_utils_resolveSafePath(dir, ".github/workflows");
80256
- const workflowsDirExists = await file_system_utils_fileExists(workflowsDir);
80257
- const allCandidates = await gatherCandidates(dir, workflowsDirExists);
80258
- if (!workflowsDirExists) {
80259
- await (0,promises_.mkdir)(workflowsDir, {
80260
- recursive: true
80261
- });
80262
- }
80263
- const workflowPath = await workflowGateway.getCopilotSetupWorkflowPath(dir);
80264
- const workflowExists = await workflowGateway.copilotSetupWorkflowExists(dir);
80265
- if (workflowExists) {
80266
- return await updateExistingWorkflow(dir, workflowPath, allCandidates, args);
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
- return await createNewWorkflow(dir, workflowPath, allCandidates, args);
80269
- } catch (error) {
80270
- const message = error instanceof Error ? error.message : "Unknown error occurred";
80271
- return types_errorResponse(`Failed to create/update workflow: ${message}`);
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
- if (!await file_system_utils_fileExists(dir)) {
80286
- return types_errorResponse(`Target directory does not exist: ${dir}`);
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.reduce((acc, script)=>{
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
- return acc;
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
- if (!await file_system_utils_fileExists(dir)) {
80448
- return types_errorResponse(`Target directory does not exist: ${dir}`);
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
- if (!await file_system_utils_fileExists(dir)) {
80487
- return types_errorResponse(`Target directory does not exist: ${dir}`);
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 = args.actions.filter((action)=>{
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
- if (!await file_system_utils_fileExists(dir)) {
80544
- return types_errorResponse(`Target directory does not exist: ${dir}`);
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
- suggestions.push(`⚠️ ${coverage.missingInInstructions.length} mandatory feedback loop(s) are not documented:`);
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
- * Validates that repository instructions (.github/copilot-instructions.md and .github/instructions/*.md)
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
- message: hasFullCoverage ? "✅ All mandatory feedback loops are documented" : `⚠️ ${coverage.summary.coveragePercentage.toFixed(1)}% coverage - ${missing.length} mandatory items not documented`
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("Commit SHA for the action version"),
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 instructions (.github/copilot-instructions.md and .github/instructions/*.md) document all mandatory feedback loop scripts and tools. Returns coverage percentage and suggestions for missing documentation.",
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: "0.1.0"
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__(3904);
92918
+ var __webpack_exports__ = __webpack_require__(8066);