@cassiomc1/forgeloop 1.10.2 → 1.11.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 (84) hide show
  1. package/.cursor/rules/project-loop.mdc +2 -2
  2. package/.forgeloop/forgeloop.gitignore +1 -0
  3. package/.github/copilot-instructions.md +2 -2
  4. package/AGENTS.md +1 -0
  5. package/AGENT_COMPATIBILITY.md +7 -0
  6. package/CLAUDE.md +1 -0
  7. package/DOCS_INDEX.md +10 -0
  8. package/LOOP_SYSTEM_DESIGN.md +12 -0
  9. package/ORCHESTRATOR_INTEGRATION.md +9 -0
  10. package/PROTOCOL_INTEGRATION.md +17 -0
  11. package/README.md +12 -10
  12. package/THIRD_PARTY_NOTICES.md +11 -0
  13. package/THREAT_MODEL.md +21 -0
  14. package/benchmarks/repository-index/README.md +73 -0
  15. package/benchmarks/repository-index/queries.json +12 -0
  16. package/benchmarks/repository-index/run-hot-path.mjs +142 -0
  17. package/benchmarks/repository-index/run-persistent-transport.mjs +169 -0
  18. package/completions/_forgeloop +7 -1
  19. package/completions/forgeloop.bash +13 -1
  20. package/completions/forgeloop.fish +40 -1
  21. package/docs/AGENT_PROTOCOL_SUMMARY.md +8 -1
  22. package/docs/CLI_REFERENCE.md +114 -2
  23. package/docs/DOCUMENTATION_GUIDE.md +10 -5
  24. package/docs/GETTING_STARTED.md +17 -0
  25. package/docs/MCP.md +16 -1
  26. package/docs/PACKAGE_CONTENTS.md +7 -1
  27. package/docs/PERSISTENT_SEARCH_TRANSPORT.md +289 -0
  28. package/docs/RECIPES.md +30 -0
  29. package/docs/RELEASE_CHECKLIST.md +34 -0
  30. package/docs/REPOSITORY_INDEX.md +553 -0
  31. package/docs/TROUBLESHOOTING.md +147 -0
  32. package/docs/UNIVERSAL_INTEGRATION.md +30 -0
  33. package/docs/diagrams/README.md +10 -0
  34. package/package.json +11 -2
  35. package/scripts/update-tgrep-manifest.mjs +86 -0
  36. package/scripts/verify-tgrep-manifest.mjs +17 -0
  37. package/src/cli.js +35 -0
  38. package/src/commands/doctor.js +76 -1
  39. package/src/commands/index-rebuild.js +1 -0
  40. package/src/commands/index-setup.js +1 -0
  41. package/src/commands/index-start.js +1 -0
  42. package/src/commands/index-status.js +1 -0
  43. package/src/commands/index-stop.js +1 -0
  44. package/src/commands/init.js +39 -1
  45. package/src/commands/repository-index.js +111 -0
  46. package/src/commands/search.js +1 -0
  47. package/src/commands/update.js +32 -4
  48. package/src/core/cli-command-definitions.js +97 -3
  49. package/src/core/command-executors.js +35 -2
  50. package/src/core/command-input.js +23 -0
  51. package/src/core/error-codes.js +195 -0
  52. package/src/core/filesystem.js +10 -1
  53. package/src/core/integration-invocation-policy.js +27 -0
  54. package/src/core/integration-resources.js +16 -1
  55. package/src/core/protocol-info.js +23 -0
  56. package/src/integration.d.ts +90 -0
  57. package/src/integration.js +21 -0
  58. package/src/persistent-transport/client.js +293 -0
  59. package/src/persistent-transport/constants.js +24 -0
  60. package/src/persistent-transport/errors.js +38 -0
  61. package/src/persistent-transport/framing.js +61 -0
  62. package/src/persistent-transport/lifecycle.js +116 -0
  63. package/src/persistent-transport/ownership.js +184 -0
  64. package/src/persistent-transport/paths.js +31 -0
  65. package/src/persistent-transport/protocol.js +95 -0
  66. package/src/persistent-transport/server.js +256 -0
  67. package/src/persistent-transport/state.js +49 -0
  68. package/src/repository-index/args.js +59 -0
  69. package/src/repository-index/binary-manager.js +413 -0
  70. package/src/repository-index/constants.js +45 -0
  71. package/src/repository-index/errors.js +38 -0
  72. package/src/repository-index/lifecycle.js +17 -0
  73. package/src/repository-index/lock.js +113 -0
  74. package/src/repository-index/manifest.js +132 -0
  75. package/src/repository-index/metrics.js +30 -0
  76. package/src/repository-index/normalize-json.js +187 -0
  77. package/src/repository-index/paths.js +39 -0
  78. package/src/repository-index/platform.js +20 -0
  79. package/src/repository-index/process.js +140 -0
  80. package/src/repository-index/readiness.js +62 -0
  81. package/src/repository-index/search.js +262 -0
  82. package/src/repository-index/server.js +432 -0
  83. package/src/repository-index/status.js +397 -0
  84. package/src/repository-index/tgrep-manifest.json +38 -0
@@ -0,0 +1,111 @@
1
+ import { rebuildRepositoryIndex, setupRepositoryIndex, startRepositoryIndexServer, stopRepositoryIndexServer } from "../repository-index/server.js";
2
+ import { searchRepository } from "../repository-index/search.js";
3
+ import { formatRepositoryIndexStatus, getRepositoryIndexStatus, sanitizeRepositoryIndexStatus } from "../repository-index/status.js";
4
+ import { searchViaPersistentTransport } from "../persistent-transport/client.js";
5
+
6
+ function sanitizeLifecycleResult(result) {
7
+ const { status, indexed, ...rest } = result ?? {};
8
+ const publicIndexed = indexed && typeof indexed === "object"
9
+ ? Object.fromEntries(Object.entries(indexed).filter(([key]) => !["root_path", "rootPath", "indexPath", "repositoryRoot"].includes(key)))
10
+ : indexed;
11
+ return {
12
+ ...rest,
13
+ ...(status ? { status: sanitizeRepositoryIndexStatus(status) } : {}),
14
+ ...(indexed ? { indexed: publicIndexed } : {}),
15
+ };
16
+ }
17
+
18
+ function nativeOptions(options, packageRoot) {
19
+ return {
20
+ packageRoot,
21
+ assetPath: options.assetPath ?? undefined,
22
+ binaryPath: options.binaryPath ?? undefined,
23
+ homeDirectory: options.homeDirectory ?? undefined,
24
+ force: options.force === true,
25
+ config: options.indexConfig ?? undefined,
26
+ };
27
+ }
28
+
29
+ export async function runRepositoryIndexSetup({ target, packageRoot, options = {} } = {}) {
30
+ const native = nativeOptions(options, packageRoot);
31
+ const result = await setupRepositoryIndex(target, native);
32
+ return sanitizeLifecycleResult({ ...result, status: result.status ?? await getRepositoryIndexStatus(target, { ...native, packageRoot, includeLocalPaths: true }) });
33
+ }
34
+
35
+ export async function runRepositoryIndexStart({ target, packageRoot, options = {} } = {}) {
36
+ const native = nativeOptions(options, packageRoot);
37
+ const result = await startRepositoryIndexServer(target, native);
38
+ return sanitizeLifecycleResult({ ...result, status: result.status ?? await getRepositoryIndexStatus(target, { ...native, packageRoot, includeLocalPaths: true }) });
39
+ }
40
+
41
+ export async function runRepositoryIndexStop({ target, packageRoot, options = {} } = {}) {
42
+ const native = nativeOptions(options, packageRoot);
43
+ const result = await stopRepositoryIndexServer(target, native);
44
+ return sanitizeLifecycleResult({ ...result, status: await getRepositoryIndexStatus(target, { ...native, packageRoot, skipNativeStatus: true }) });
45
+ }
46
+
47
+ export async function runRepositoryIndexStatus({ target, packageRoot, options = {} } = {}) {
48
+ return getRepositoryIndexStatus(target, { ...nativeOptions(options, packageRoot), packageRoot });
49
+ }
50
+
51
+ export async function runRepositoryIndexRebuild({ target, packageRoot, options = {} } = {}) {
52
+ const native = nativeOptions(options, packageRoot);
53
+ const result = await rebuildRepositoryIndex(target, native);
54
+ const status = result.status ?? await getRepositoryIndexStatus(target, { ...native, packageRoot });
55
+ const smoke = await searchRepository(target, {
56
+ ...native,
57
+ pattern: "__FORGELOOP_INDEX_REBUILD_SMOKE__",
58
+ fixedStrings: true,
59
+ maxCount: 1,
60
+ });
61
+ return sanitizeLifecycleResult({
62
+ ...result,
63
+ status: sanitizeRepositoryIndexStatus(status),
64
+ smokeSearch: {
65
+ status: "OK",
66
+ matchCount: smoke.matches.length,
67
+ nativeExitCode: smoke.metrics.exitCode,
68
+ },
69
+ });
70
+ }
71
+
72
+ export async function runSearch({ target, packageRoot, options = {}, transport = "integration" } = {}) {
73
+ if (transport === "cli") {
74
+ return searchViaPersistentTransport(target, options, {
75
+ homeDirectory: options.homeDirectory,
76
+ idleTimeoutMs: options.persistentTransportIdleTimeoutMs,
77
+ startupTimeoutMs: options.persistentTransportStartupTimeoutMs,
78
+ requestTimeoutMs: options.persistentTransportRequestTimeoutMs,
79
+ env: options.env,
80
+ });
81
+ }
82
+ return searchRepository(target, {
83
+ ...options,
84
+ packageRoot,
85
+ pattern: options.pattern,
86
+ globs: options.globs ?? [],
87
+ types: options.types ?? [],
88
+ });
89
+ }
90
+
91
+ export function formatRepositoryIndexResult(result) {
92
+ const status = result.status ?? result;
93
+ const lines = [
94
+ `Repository Index: ${status.health ?? "READY"}`,
95
+ `Engine: ${status.engine ?? "tgrep"} ${status.engineVersion ?? "unknown"}`,
96
+ `Index: ${status.indexPath ?? "unknown"}`,
97
+ `Server: ${status.server?.running ? `running (pid ${status.server.pid ?? "unknown"})` : "not running"}`,
98
+ ];
99
+ if (result.rebuilt) lines.push("Rebuilt: yes");
100
+ if (result.setup) lines.push(`Setup: ${result.rebuilt ? "rebuilt" : "reused"}`);
101
+ if (result.stopped !== undefined) lines.push(`Stopped: ${result.stopped ? "yes" : "no"}`);
102
+ return `${lines.join("\n")}\n`;
103
+ }
104
+
105
+ export function formatSearchResult(result) {
106
+ if (result.query.filesWithMatches) return `${(result.files ?? []).join("\n")}${result.files?.length ? "\n" : "no matches\n"}`;
107
+ const lines = (result.matches ?? []).map((match) => `${match.path}:${match.line}${match.column ? `:${match.column}` : ""}:${match.text}`);
108
+ return `${lines.join("\n")}${lines.length ? "\n" : "no matches\n"}`;
109
+ }
110
+
111
+ export { formatRepositoryIndexStatus };
@@ -0,0 +1 @@
1
+ export { runSearch, formatSearchResult } from "./repository-index.js";
@@ -10,6 +10,8 @@ import {
10
10
  } from "../core/manifest.js";
11
11
  import { readTemplateEntries } from "../core/templates.js";
12
12
  import { isNativeAdapterPath, LAYOUT_VERSION, LEGACY_PROFILE_PATH } from "../core/target-layout.js";
13
+ import { isRepositoryCandidate } from "../repository-index/lifecycle.js";
14
+ import { setupRepositoryIndex } from "../repository-index/server.js";
13
15
 
14
16
  const LEGACY_CLEANUP_DIRECTORIES = Object.freeze(["ENG", "schemas"]);
15
17
 
@@ -295,7 +297,32 @@ async function migrateLegacyLayout({ target, dryRun, packageVersion, currentMani
295
297
  return { actions, conflicts, manifest: nextManifest };
296
298
  }
297
299
 
298
- export async function runUpdate({ target, dryRun, packageRoot, packageVersion, hooks = {} }) {
300
+ async function attachRepositoryIndexResult(result, { target, dryRun, packageRoot, repositoryIndex, repositoryIndexOptions = {} }) {
301
+ if (!repositoryIndex) return result;
302
+ if (!(await isRepositoryCandidate(target))) {
303
+ return { ...result, repositoryIndex: { status: "DEFERRED", required: true, reason: "target is not a Git repository" } };
304
+ }
305
+ if (dryRun) {
306
+ return { ...result, repositoryIndex: { status: "WOULD_SETUP", required: true, reason: "dry-run does not provision or execute the native index engine" } };
307
+ }
308
+ if (result.conflicts?.length > 0) {
309
+ return { ...result, repositoryIndex: { status: "BLOCKED", required: true, reason: "update conflicts must be resolved before index maintenance" } };
310
+ }
311
+ const setup = await setupRepositoryIndex(target, { ...repositoryIndexOptions, packageRoot });
312
+ return {
313
+ ...result,
314
+ repositoryIndex: {
315
+ status: "READY",
316
+ required: true,
317
+ health: setup.status?.health ?? "READY",
318
+ engine: setup.status?.engine ?? "tgrep",
319
+ engineVersion: setup.status?.engineVersion ?? null,
320
+ server: setup.status?.server ?? null,
321
+ },
322
+ };
323
+ }
324
+
325
+ export async function runUpdate({ target, dryRun, packageRoot, packageVersion, hooks = {}, repositoryIndex, repositoryIndexOptions }) {
299
326
  const currentManifest = await readManifest(target);
300
327
  if (!currentManifest) {
301
328
  throw new Error("No .forgeloop/manifest.json found; run forgeloop init first.");
@@ -303,7 +330,8 @@ export async function runUpdate({ target, dryRun, packageRoot, packageVersion, h
303
330
 
304
331
  const entries = await readTemplateEntries(packageRoot);
305
332
  if ((currentManifest.layoutVersion ?? 1) < LAYOUT_VERSION) {
306
- return migrateLegacyLayout({ target, dryRun, packageVersion, currentManifest, entries, hooks });
333
+ const result = await migrateLegacyLayout({ target, dryRun, packageVersion, currentManifest, entries, hooks });
334
+ return attachRepositoryIndexResult(result, { target, dryRun, packageRoot, repositoryIndex, repositoryIndexOptions });
307
335
  }
308
336
 
309
337
  const nextManifest = structuredClone(currentManifest);
@@ -405,7 +433,7 @@ export async function runUpdate({ target, dryRun, packageRoot, packageVersion, h
405
433
  }
406
434
 
407
435
  if (conflicts.length > 0) {
408
- return { actions, conflicts, manifest: currentManifest };
436
+ return attachRepositoryIndexResult({ actions, conflicts, manifest: currentManifest }, { target, dryRun, packageRoot, repositoryIndex, repositoryIndexOptions });
409
437
  }
410
438
 
411
439
  for (const relativePath of Object.keys(nextManifest.files)) {
@@ -423,5 +451,5 @@ export async function runUpdate({ target, dryRun, packageRoot, packageVersion, h
423
451
  nextManifest.packageVersion = packageVersion;
424
452
  await writeManifest(target, nextManifest, { dryRun });
425
453
  await cleanupLegacyFiles({ target, dryRun, cleanupFiles, cleanupDirectories, hooks, actions, conflicts });
426
- return { actions, conflicts, manifest: nextManifest };
454
+ return attachRepositoryIndexResult({ actions, conflicts, manifest: nextManifest }, { target, dryRun, packageRoot, repositoryIndex, repositoryIndexOptions });
427
455
  }
@@ -81,7 +81,7 @@ export const CLI_COMMAND_DEFINITIONS = Object.freeze({
81
81
  }),
82
82
  writes: [".forgeloop/*", "AGENTS.md", "CLAUDE.md", ".cursor/rules/project-loop.mdc", ".github/copilot-instructions.md"],
83
83
  removes: [],
84
- mayExecuteExternalProcess: false,
84
+ mayExecuteExternalProcess: true,
85
85
  description: "Initializes a target project directory with ForgeLoop discovery adapters, schemas, and templates.",
86
86
  }),
87
87
  doctor: Object.freeze({
@@ -97,9 +97,103 @@ export const CLI_COMMAND_DEFINITIONS = Object.freeze({
97
97
  }),
98
98
  writes: [".forgeloop/.manifest.json"],
99
99
  removes: [],
100
- mayExecuteExternalProcess: false,
100
+ mayExecuteExternalProcess: true,
101
101
  description: "Diagnoses project health, discovers adapters, and optionally repairs missing template files.",
102
102
  }),
103
+ "index-setup": Object.freeze({
104
+ name: "index-setup",
105
+ category: "project-maintenance",
106
+ mutation: "MUTATING",
107
+ options: Object.freeze({
108
+ ...CLI_COMMON_OPTIONS,
109
+ "--asset": Object.freeze({ targetKey: "assetPath", parseType: "string", takesValue: true, valueName: "path", missingValueMessage: "--asset requires a local archive path", description: "use a preloaded pinned tgrep archive instead of downloading" }),
110
+ "--force": Object.freeze({ targetKey: "force", parseType: "boolean", takesValue: false, description: "rebuild the repository index even when metadata is current" }),
111
+ "--json": Object.freeze({ targetKey: "json", parseType: "boolean", takesValue: false, description: "emit repository-index setup as JSON" }),
112
+ }),
113
+ writes: [".forgeloop/repository-index/*"],
114
+ removes: [],
115
+ mayExecuteExternalProcess: true,
116
+ description: "Provisions the pinned tgrep engine, builds the repository index, and starts its owned watcher.",
117
+ }),
118
+ "index-start": Object.freeze({
119
+ name: "index-start",
120
+ category: "project-maintenance",
121
+ mutation: "MUTATING",
122
+ options: Object.freeze({
123
+ ...CLI_COMMON_OPTIONS,
124
+ "--json": Object.freeze({ targetKey: "json", parseType: "boolean", takesValue: false, description: "emit repository-index start as JSON" }),
125
+ }),
126
+ writes: [".forgeloop/repository-index/engine-state.json"],
127
+ removes: [],
128
+ mayExecuteExternalProcess: true,
129
+ description: "Starts the owned tgrep repository-index watcher after an index has been built.",
130
+ }),
131
+ "index-stop": Object.freeze({
132
+ name: "index-stop",
133
+ category: "project-maintenance",
134
+ mutation: "MUTATING",
135
+ options: Object.freeze({
136
+ ...CLI_COMMON_OPTIONS,
137
+ "--json": Object.freeze({ targetKey: "json", parseType: "boolean", takesValue: false, description: "emit repository-index stop as JSON" }),
138
+ }),
139
+ writes: [],
140
+ removes: [".forgeloop/repository-index/serve.json", ".forgeloop/repository-index/engine-state.json"],
141
+ mayExecuteExternalProcess: true,
142
+ description: "Stops only a tgrep server whose process identity is provably owned by ForgeLoop.",
143
+ }),
144
+ "index-status": Object.freeze({
145
+ name: "index-status",
146
+ category: "diagnostics",
147
+ mutation: "READ_ONLY",
148
+ options: Object.freeze({
149
+ ...CLI_COMMON_OPTIONS,
150
+ "--json": Object.freeze({ targetKey: "json", parseType: "boolean", takesValue: false, description: "emit repository-index health as JSON" }),
151
+ }),
152
+ writes: [],
153
+ removes: [],
154
+ mayExecuteExternalProcess: true,
155
+ description: "Reports provider-neutral repository-index health, metadata, and owned-server status.",
156
+ }),
157
+ "index-rebuild": Object.freeze({
158
+ name: "index-rebuild",
159
+ category: "project-maintenance",
160
+ mutation: "MUTATING",
161
+ options: Object.freeze({
162
+ ...CLI_COMMON_OPTIONS,
163
+ "--asset": Object.freeze({ targetKey: "assetPath", parseType: "string", takesValue: true, valueName: "path", missingValueMessage: "--asset requires a local archive path", description: "use a preloaded pinned tgrep archive instead of downloading" }),
164
+ "--json": Object.freeze({ targetKey: "json", parseType: "boolean", takesValue: false, description: "emit repository-index rebuild as JSON" }),
165
+ }),
166
+ writes: [".forgeloop/repository-index/*"],
167
+ removes: [".forgeloop/repository-index/tgrep/*"],
168
+ mayExecuteExternalProcess: true,
169
+ description: "Atomically rebuilds the repository index and restarts its owned watcher.",
170
+ }),
171
+ search: Object.freeze({
172
+ name: "search",
173
+ category: "diagnostics",
174
+ mutation: "READ_ONLY",
175
+ options: Object.freeze({
176
+ ...CLI_COMMON_OPTIONS,
177
+ "<pattern>": Object.freeze({ targetKey: "pattern", parseType: "string", takesValue: true, isPositional: true, valueName: "pattern", description: "regular expression or literal search pattern" }),
178
+ "--glob": Object.freeze({ targetKey: "globs", parseType: "string", takesValue: true, repeatable: true, valueName: "glob", missingValueMessage: "--glob requires a pattern", description: "include files matching a glob" }),
179
+ "--type": Object.freeze({ targetKey: "types", parseType: "string", takesValue: true, repeatable: true, valueName: "type", missingValueMessage: "--type requires a file type", description: "include files of a tgrep type" }),
180
+ "--fixed-strings": Object.freeze({ targetKey: "fixedStrings", parseType: "boolean", takesValue: false, description: "treat the pattern as a literal string" }),
181
+ "--ignore-case": Object.freeze({ targetKey: "ignoreCase", parseType: "boolean", takesValue: false, description: "search case-insensitively" }),
182
+ "--smart-case": Object.freeze({ targetKey: "smartCase", parseType: "boolean", takesValue: false, description: "use case-insensitive search only for lowercase patterns" }),
183
+ "--word-regexp": Object.freeze({ targetKey: "wordRegexp", parseType: "boolean", takesValue: false, aliases: ["--word"], description: "match whole words only" }),
184
+ "--context": Object.freeze({ targetKey: "context", parseType: "non-negative-integer", takesValue: true, valueName: "lines", missingValueMessage: "--context requires a non-negative integer", description: "lines of context before and after matches" }),
185
+ "--before-context": Object.freeze({ targetKey: "beforeContext", parseType: "non-negative-integer", takesValue: true, valueName: "lines", missingValueMessage: "--before-context requires a non-negative integer", description: "lines of context before matches" }),
186
+ "--after-context": Object.freeze({ targetKey: "afterContext", parseType: "non-negative-integer", takesValue: true, valueName: "lines", missingValueMessage: "--after-context requires a non-negative integer", description: "lines of context after matches" }),
187
+ "--max-count": Object.freeze({ targetKey: "maxCount", parseType: "non-negative-integer", takesValue: true, valueName: "number", missingValueMessage: "--max-count requires a positive integer", description: "maximum matches per file" }),
188
+ "--files-with-matches": Object.freeze({ targetKey: "filesWithMatches", parseType: "boolean", takesValue: false, description: "return matching file paths only" }),
189
+ "--stats": Object.freeze({ targetKey: "stats", parseType: "boolean", takesValue: false, description: "include observed native search statistics" }),
190
+ "--json": Object.freeze({ targetKey: "json", parseType: "boolean", takesValue: false, description: "emit normalized provider-neutral search JSON" }),
191
+ }),
192
+ writes: [],
193
+ removes: [],
194
+ mayExecuteExternalProcess: true,
195
+ description: "Searches the ForgeLoop repository index through the provider-neutral search contract.",
196
+ }),
103
197
  update: Object.freeze({
104
198
  name: "update",
105
199
  category: "project-maintenance",
@@ -110,7 +204,7 @@ export const CLI_COMMAND_DEFINITIONS = Object.freeze({
110
204
  }),
111
205
  writes: [".forgeloop/*", "AGENTS.md", "CLAUDE.md", ".cursor/rules/project-loop.mdc", ".github/copilot-instructions.md"],
112
206
  removes: [],
113
- mayExecuteExternalProcess: false,
207
+ mayExecuteExternalProcess: true,
114
208
  description: "Updates installed templates, discovery adapters, and canonical engineering guides to the latest version.",
115
209
  }),
116
210
  activate: Object.freeze({
@@ -82,6 +82,14 @@ import { runAttestationVerify } from "../commands/attestation-verify.js";
82
82
  import { runAttestationStatus } from "../commands/attestation-status.js";
83
83
  import { runAttestationVerifyRange } from "../commands/attestation-verify-range.js";
84
84
  import { exitCodeForAttestationResult } from "./exit-codes.js";
85
+ import {
86
+ runRepositoryIndexRebuild,
87
+ runRepositoryIndexSetup,
88
+ runRepositoryIndexStart,
89
+ runRepositoryIndexStatus,
90
+ runRepositoryIndexStop,
91
+ runSearch,
92
+ } from "../commands/repository-index.js";
85
93
 
86
94
  /**
87
95
  * Canonical transport-neutral command executors.
@@ -100,7 +108,7 @@ export const COMMAND_EXECUTORS = {
100
108
  exitCode: 0,
101
109
  }),
102
110
  init: async ({ target, packageRoot, packageVersion, options }) => ({
103
- result: await runInit({ target, dryRun: options.dryRun, packageRoot, packageVersion }),
111
+ result: await runInit({ target, dryRun: options.dryRun, packageRoot, packageVersion, repositoryIndex: true }),
104
112
  exitCode: 0,
105
113
  }),
106
114
  doctor: async ({ target, packageRoot, options }) => {
@@ -110,6 +118,7 @@ export const COMMAND_EXECUTORS = {
110
118
  adoptPaths: options.adopt,
111
119
  strict: options.strict,
112
120
  fix: options.fix,
121
+ repositoryIndex: true,
113
122
  });
114
123
  return { result, exitCode: result.ok ? 0 : 1 };
115
124
  },
@@ -663,9 +672,33 @@ export const COMMAND_EXECUTORS = {
663
672
  exitCode: 0,
664
673
  }),
665
674
  update: async ({ target, packageRoot, packageVersion, options }) => {
666
- const result = await runUpdate({ target, dryRun: options.dryRun, packageRoot, packageVersion });
675
+ const result = await runUpdate({ target, dryRun: options.dryRun, packageRoot, packageVersion, repositoryIndex: true });
667
676
  return { result, exitCode: result.conflicts.length === 0 ? 0 : 1 };
668
677
  },
678
+ "index-setup": async ({ target, packageRoot, options }) => ({
679
+ result: await runRepositoryIndexSetup({ target, packageRoot, options }),
680
+ exitCode: 0,
681
+ }),
682
+ "index-start": async ({ target, packageRoot, options }) => ({
683
+ result: await runRepositoryIndexStart({ target, packageRoot, options }),
684
+ exitCode: 0,
685
+ }),
686
+ "index-stop": async ({ target, packageRoot, options }) => ({
687
+ result: await runRepositoryIndexStop({ target, packageRoot, options }),
688
+ exitCode: 0,
689
+ }),
690
+ "index-status": async ({ target, packageRoot, options }) => ({
691
+ result: await runRepositoryIndexStatus({ target, packageRoot, options }),
692
+ exitCode: 0,
693
+ }),
694
+ "index-rebuild": async ({ target, packageRoot, options }) => ({
695
+ result: await runRepositoryIndexRebuild({ target, packageRoot, options }),
696
+ exitCode: 0,
697
+ }),
698
+ search: async ({ target, packageRoot, options, transport = "integration" }) => ({
699
+ result: await runSearch({ target, packageRoot, options, transport }),
700
+ exitCode: 0,
701
+ }),
669
702
  };
670
703
 
671
704
  export const EXECUTOR_EXCEPTIONS = Object.freeze([
@@ -8,6 +8,12 @@ function inputError(message) {
8
8
  return error;
9
9
  }
10
10
 
11
+ function validateSearchCommandInput(command, options, help) {
12
+ if (command === "search" && !help && (typeof options.pattern !== "string" || options.pattern.length === 0)) {
13
+ throw inputError("search requires a pattern");
14
+ }
15
+ }
16
+
11
17
  /**
12
18
  * Transport-neutral option defaults shared by the CLI parser and the
13
19
  * programmatic command runtime so every executor observes the same
@@ -22,6 +28,22 @@ export function defaultCommandInputValues() {
22
28
  strict: false,
23
29
  fix: false,
24
30
  adopt: [],
31
+ pattern: null,
32
+ globs: [],
33
+ types: [],
34
+ fixedStrings: false,
35
+ ignoreCase: false,
36
+ smartCase: false,
37
+ wordRegexp: false,
38
+ context: null,
39
+ beforeContext: null,
40
+ afterContext: null,
41
+ maxCount: null,
42
+ filesWithMatches: false,
43
+ stats: false,
44
+ assetPath: null,
45
+ binaryPath: null,
46
+ force: false,
25
47
  workType: null,
26
48
  surfaces: [],
27
49
  risks: [],
@@ -118,6 +140,7 @@ export function validateForgeLoopCommandInput({ command, input, help = false } =
118
140
  if (command === "task-create" && !options.taskId) {
119
141
  throw inputError("task-create requires --task");
120
142
  }
143
+ validateSearchCommandInput(command, options, help);
121
144
  if (options.executionProfile !== null && options.executionProfile !== undefined) {
122
145
  if (command !== "route") throw inputError(`executionProfile is not valid for ${command}`);
123
146
  if (!EXECUTION_PROFILE_REQUESTS.includes(options.executionProfile)) {
@@ -11,6 +11,36 @@ import {
11
11
  E_VERIFICATION_ISOLATION_UNAVAILABLE,
12
12
  } from "./verification-execution.js";
13
13
 
14
+ export const E_REPOSITORY_INDEX_PLATFORM_UNSUPPORTED = "E_REPOSITORY_INDEX_PLATFORM_UNSUPPORTED";
15
+ export const E_REPOSITORY_INDEX_ENGINE_MISSING = "E_REPOSITORY_INDEX_ENGINE_MISSING";
16
+ export const E_REPOSITORY_INDEX_ENGINE_DOWNLOAD_FAILED = "E_REPOSITORY_INDEX_ENGINE_DOWNLOAD_FAILED";
17
+ export const E_REPOSITORY_INDEX_ENGINE_CHECKSUM_MISMATCH = "E_REPOSITORY_INDEX_ENGINE_CHECKSUM_MISMATCH";
18
+ export const E_REPOSITORY_INDEX_ENGINE_BINARY_CHECKSUM_MISMATCH = "E_REPOSITORY_INDEX_ENGINE_BINARY_CHECKSUM_MISMATCH";
19
+ export const E_REPOSITORY_INDEX_ENGINE_EXTRACTION_FAILED = "E_REPOSITORY_INDEX_ENGINE_EXTRACTION_FAILED";
20
+ export const E_REPOSITORY_INDEX_ENGINE_VERSION_MISMATCH = "E_REPOSITORY_INDEX_ENGINE_VERSION_MISMATCH";
21
+ export const E_REPOSITORY_INDEX_ENGINE_EXECUTION_FAILED = "E_REPOSITORY_INDEX_ENGINE_EXECUTION_FAILED";
22
+ export const E_REPOSITORY_INDEX_OUTPUT_LIMIT = "E_REPOSITORY_INDEX_OUTPUT_LIMIT";
23
+ export const E_REPOSITORY_INDEX_NOT_INITIALIZED = "E_REPOSITORY_INDEX_NOT_INITIALIZED";
24
+ export const E_REPOSITORY_INDEX_INDEXING = "E_REPOSITORY_INDEX_INDEXING";
25
+ export const E_REPOSITORY_INDEX_SERVER_START_FAILED = "E_REPOSITORY_INDEX_SERVER_START_FAILED";
26
+ export const E_REPOSITORY_INDEX_SERVER_STOP_FAILED = "E_REPOSITORY_INDEX_SERVER_STOP_FAILED";
27
+ export const E_REPOSITORY_INDEX_SERVER_UNHEALTHY = "E_REPOSITORY_INDEX_SERVER_UNHEALTHY";
28
+ export const E_REPOSITORY_INDEX_SEARCH_FAILED = "E_REPOSITORY_INDEX_SEARCH_FAILED";
29
+ export const E_REPOSITORY_INDEX_OUTPUT_INVALID = "E_REPOSITORY_INDEX_OUTPUT_INVALID";
30
+ export const E_REPOSITORY_INDEX_REBUILD_FAILED = "E_REPOSITORY_INDEX_REBUILD_FAILED";
31
+ export const E_REPOSITORY_INDEX_REQUEST_INVALID = "E_REPOSITORY_INDEX_REQUEST_INVALID";
32
+ export const E_REPOSITORY_INDEX_LOCK_UNSAFE = "E_REPOSITORY_INDEX_LOCK_UNSAFE";
33
+ export const E_PERSISTENT_TRANSPORT_FRAME_INVALID = "E_PERSISTENT_TRANSPORT_FRAME_INVALID";
34
+ export const E_PERSISTENT_TRANSPORT_FRAME_TOO_LARGE = "E_PERSISTENT_TRANSPORT_FRAME_TOO_LARGE";
35
+ export const E_PERSISTENT_TRANSPORT_HOST_STALE = "E_PERSISTENT_TRANSPORT_HOST_STALE";
36
+ export const E_PERSISTENT_TRANSPORT_INVALID_REQUEST = "E_PERSISTENT_TRANSPORT_INVALID_REQUEST";
37
+ export const E_PERSISTENT_TRANSPORT_INVALID_RESPONSE = "E_PERSISTENT_TRANSPORT_INVALID_RESPONSE";
38
+ export const E_PERSISTENT_TRANSPORT_OWNERSHIP_UNVERIFIED = "E_PERSISTENT_TRANSPORT_OWNERSHIP_UNVERIFIED";
39
+ export const E_PERSISTENT_TRANSPORT_PROTOCOL_MISMATCH = "E_PERSISTENT_TRANSPORT_PROTOCOL_MISMATCH";
40
+ export const E_PERSISTENT_TRANSPORT_START_FAILED = "E_PERSISTENT_TRANSPORT_START_FAILED";
41
+ export const E_PERSISTENT_TRANSPORT_TIMEOUT = "E_PERSISTENT_TRANSPORT_TIMEOUT";
42
+ export const E_PERSISTENT_TRANSPORT_UNAVAILABLE = "E_PERSISTENT_TRANSPORT_UNAVAILABLE";
43
+
14
44
  export const E_TASK_REQUIRED = "E_TASK_REQUIRED";
15
45
  export const E_TASK_NOT_FOUND = "E_TASK_NOT_FOUND";
16
46
  export const E_TASK_ALREADY_EXISTS = "E_TASK_ALREADY_EXISTS";
@@ -373,6 +403,140 @@ const ADVISORY_CONTEXT_AND_HANDOFF_ERROR_METADATA = Object.freeze(Object.fromEnt
373
403
  })],
374
404
  ]));
375
405
 
406
+ const REPOSITORY_INDEX_ERROR_METADATA = Object.freeze(Object.fromEntries([
407
+ [E_REPOSITORY_INDEX_PLATFORM_UNSUPPORTED, [
408
+ "The current operating-system and architecture pair has no pinned tgrep release asset.",
409
+ "Use a supported platform or add a separately reviewed manifest asset; do not substitute a PATH executable.",
410
+ ]],
411
+ [E_REPOSITORY_INDEX_ENGINE_MISSING, [
412
+ "The managed tgrep executable is absent, unreadable, or has not been provisioned.",
413
+ "Run forgeloop index-setup or preload the exact manifest archive with --asset.",
414
+ ]],
415
+ [E_REPOSITORY_INDEX_ENGINE_DOWNLOAD_FAILED, [
416
+ "The pinned tgrep release asset could not be downloaded or read.",
417
+ "Retry with network access or preload the exact manifest archive; never bypass provisioning verification.",
418
+ ]],
419
+ [E_REPOSITORY_INDEX_ENGINE_CHECKSUM_MISMATCH, [
420
+ "The tgrep archive or executable bytes do not match the pinned manifest SHA-256 digest.",
421
+ "Obtain the exact manifest asset and retry; do not install a checksum mismatch.",
422
+ ]],
423
+ [E_REPOSITORY_INDEX_ENGINE_BINARY_CHECKSUM_MISMATCH, [
424
+ "The extracted or managed tgrep executable bytes do not match the pinned binary SHA-256 digest.",
425
+ "Run forgeloop index-setup or index-rebuild to repair the managed binary; do not run a mismatched executable.",
426
+ ]],
427
+ [E_REPOSITORY_INDEX_ENGINE_EXTRACTION_FAILED, [
428
+ "The tgrep archive is malformed, unsafe, or could not be extracted to a temporary directory.",
429
+ "Use the exact supported archive and retry; unsafe paths and links are rejected.",
430
+ ]],
431
+ [E_REPOSITORY_INDEX_ENGINE_VERSION_MISMATCH, [
432
+ "The executable reports a version different from the ForgeLoop-pinned tgrep version.",
433
+ "Provision the manifest-pinned tgrep release and retry; do not use an unpinned binary.",
434
+ ]],
435
+ [E_REPOSITORY_INDEX_ENGINE_EXECUTION_FAILED, [
436
+ "A managed tgrep process could not be launched, completed, or stayed within its execution boundary.",
437
+ "Inspect the structured error and index status, then retry with the verified managed engine.",
438
+ ]],
439
+ [E_REPOSITORY_INDEX_OUTPUT_LIMIT, [
440
+ "Managed tgrep output exceeded ForgeLoop's bounded process-output limit.",
441
+ "Narrow the search or resource policy and retry; oversized output is never promoted to a result.",
442
+ ]],
443
+ [E_REPOSITORY_INDEX_NOT_INITIALIZED, [
444
+ "The repository index has no complete derived index available for the requested operation.",
445
+ "Run forgeloop index-setup or forgeloop index-rebuild for the selected repository.",
446
+ ]],
447
+ [E_REPOSITORY_INDEX_INDEXING, [
448
+ "The repository index is still being built or reconciled and is not ready for the requested operation.",
449
+ "Wait for index-status to report READY, or use index-rebuild if the operation remains stuck.",
450
+ ]],
451
+ [E_REPOSITORY_INDEX_SERVER_START_FAILED, [
452
+ "The ForgeLoop-owned tgrep watcher could not be started or did not become healthy.",
453
+ "Run index-status, inspect the structured reason, and retry index-start or index-rebuild.",
454
+ ]],
455
+ [E_REPOSITORY_INDEX_SERVER_STOP_FAILED, [
456
+ "The ForgeLoop-owned tgrep watcher could not be stopped or its ownership could not be proven.",
457
+ "Use index-status and retry the canonical stop operation; never terminate processes by name or PID alone.",
458
+ ]],
459
+ [E_REPOSITORY_INDEX_SERVER_UNHEALTHY, [
460
+ "Repository Index server, metadata, process identity, or index readiness validation failed.",
461
+ "Run index-status, then use index-rebuild after resolving the reported boundary.",
462
+ ]],
463
+ [E_REPOSITORY_INDEX_SEARCH_FAILED, [
464
+ "The native tgrep search failed with an execution or provider error; no-match is not an error.",
465
+ "Inspect index-status and retry the query or rebuild the derived index; ForgeLoop does not fall back silently.",
466
+ ]],
467
+ [E_REPOSITORY_INDEX_OUTPUT_INVALID, [
468
+ "Native tgrep output did not match the bounded provider-neutral JSON contract.",
469
+ "Treat the result as unusable, inspect the engine, and rebuild or provision the pinned release.",
470
+ ]],
471
+ [E_REPOSITORY_INDEX_REBUILD_FAILED, [
472
+ "The derived repository index could not be rebuilt successfully.",
473
+ "Inspect the structured failure, resource policy, and repository paths, then retry index-rebuild.",
474
+ ]],
475
+ [E_REPOSITORY_INDEX_REQUEST_INVALID, [
476
+ "Repository Index command input is outside the bounded provider-neutral request contract.",
477
+ "Correct the pattern, path filters, context, or lifecycle options and retry the canonical command.",
478
+ ]],
479
+ [E_REPOSITORY_INDEX_LOCK_UNSAFE, [
480
+ "A Repository Index operation lock is malformed, conflicting, or cannot be safely acquired.",
481
+ "Wait for a concurrent operation to finish and retry; use status or rebuild for a persistent lock failure.",
482
+ ]],
483
+ ].map(([code, [meaning, safeResolution]]) => [code, Object.freeze({
484
+ code,
485
+ category: "repository-index",
486
+ classification: "PUBLIC_STABLE",
487
+ meaning,
488
+ safeResolution,
489
+ })])));
490
+
491
+ const PERSISTENT_TRANSPORT_ERROR_METADATA = Object.freeze(Object.fromEntries([
492
+ [E_PERSISTENT_TRANSPORT_FRAME_INVALID, [
493
+ "The local persistent-search byte stream did not contain a complete valid JSON frame.",
494
+ "Retry the bounded local request; inspect the host only if malformed frames recur.",
495
+ ]],
496
+ [E_PERSISTENT_TRANSPORT_FRAME_TOO_LARGE, [
497
+ "A persistent-search request or response exceeded its bounded frame limit.",
498
+ "Narrow the search request or inspect the host resource boundary; oversized frames are rejected.",
499
+ ]],
500
+ [E_PERSISTENT_TRANSPORT_HOST_STALE, [
501
+ "Persistent-search state points to a host process that is no longer running.",
502
+ "Retry the CLI search so ForgeLoop can remove only the verified stale host state and restart it.",
503
+ ]],
504
+ [E_PERSISTENT_TRANSPORT_INVALID_REQUEST, [
505
+ "A local persistent-search request is outside the versioned transport contract.",
506
+ "Use the supported ForgeLoop search command or update the compatible client and host together.",
507
+ ]],
508
+ [E_PERSISTENT_TRANSPORT_INVALID_RESPONSE, [
509
+ "The persistent-search host returned a response outside the versioned transport contract.",
510
+ "Retry once through the bounded recovery path; do not consume an unvalidated response.",
511
+ ]],
512
+ [E_PERSISTENT_TRANSPORT_OWNERSHIP_UNVERIFIED, [
513
+ "ForgeLoop could not prove that the process or endpoint belongs to its user-scoped persistent-search host.",
514
+ "Do not terminate the process; inspect the endpoint and retry after resolving the ownership conflict.",
515
+ ]],
516
+ [E_PERSISTENT_TRANSPORT_PROTOCOL_MISMATCH, [
517
+ "The persistent-search client and host do not agree on the supported protocol version.",
518
+ "ForgeLoop may replace only a verified compatible host; otherwise update the installed package and retry.",
519
+ ]],
520
+ [E_PERSISTENT_TRANSPORT_START_FAILED, [
521
+ "The user-scoped persistent-search host could not start or become ready within its bounded startup window.",
522
+ "Inspect the structured diagnostics and retry; direct integration APIs remain available without this optimization.",
523
+ ]],
524
+ [E_PERSISTENT_TRANSPORT_TIMEOUT, [
525
+ "A persistent-search connection, handshake, or request exceeded its bounded timeout.",
526
+ "Retry once through the ownership-checked recovery path and inspect host/index health if it persists.",
527
+ ]],
528
+ [E_PERSISTENT_TRANSPORT_UNAVAILABLE, [
529
+ "The user-scoped persistent-search endpoint was not reachable.",
530
+ "ForgeLoop starts one verified local host and retries once; persistent failure is reported without an rg fallback.",
531
+ ]],
532
+ ].map(([code, [meaning, safeResolution]]) => [code, Object.freeze({
533
+ code,
534
+ category: "persistent-transport",
535
+ classification: "PUBLIC_STABLE",
536
+ meaning,
537
+ safeResolution,
538
+ })])));
539
+
376
540
  /**
377
541
  * Public, stable ForgeLoop error and reason codes documented for users and harnesses.
378
542
  */
@@ -380,6 +544,8 @@ export const PUBLIC_ERROR_CODES = Object.freeze({
380
544
  ...EXTENSION_PUBLIC_ERROR_CODES,
381
545
  ...STRUCTURAL_QUALITY_ERROR_METADATA,
382
546
  ...ADVISORY_CONTEXT_AND_HANDOFF_ERROR_METADATA,
547
+ ...REPOSITORY_INDEX_ERROR_METADATA,
548
+ ...PERSISTENT_TRANSPORT_ERROR_METADATA,
383
549
  E_PREFLIGHT_NOT_READY: Object.freeze({
384
550
  code: "E_PREFLIGHT_NOT_READY",
385
551
  category: "preflight",
@@ -1259,6 +1425,35 @@ export const ALL_KNOWN_ERROR_CODES = Object.freeze(new Set([
1259
1425
  E_BASELINE_RECORD_DURING_ACTIVE_TASK,
1260
1426
  E_POLICY_INITIALIZATION_FAILED,
1261
1427
  E_INIT_KIT_CONFLICT,
1428
+ E_REPOSITORY_INDEX_PLATFORM_UNSUPPORTED,
1429
+ E_REPOSITORY_INDEX_ENGINE_MISSING,
1430
+ E_REPOSITORY_INDEX_ENGINE_DOWNLOAD_FAILED,
1431
+ E_REPOSITORY_INDEX_ENGINE_CHECKSUM_MISMATCH,
1432
+ E_REPOSITORY_INDEX_ENGINE_BINARY_CHECKSUM_MISMATCH,
1433
+ E_REPOSITORY_INDEX_ENGINE_EXTRACTION_FAILED,
1434
+ E_REPOSITORY_INDEX_ENGINE_VERSION_MISMATCH,
1435
+ E_REPOSITORY_INDEX_ENGINE_EXECUTION_FAILED,
1436
+ E_REPOSITORY_INDEX_OUTPUT_LIMIT,
1437
+ E_REPOSITORY_INDEX_NOT_INITIALIZED,
1438
+ E_REPOSITORY_INDEX_INDEXING,
1439
+ E_REPOSITORY_INDEX_SERVER_START_FAILED,
1440
+ E_REPOSITORY_INDEX_SERVER_STOP_FAILED,
1441
+ E_REPOSITORY_INDEX_SERVER_UNHEALTHY,
1442
+ E_REPOSITORY_INDEX_SEARCH_FAILED,
1443
+ E_REPOSITORY_INDEX_OUTPUT_INVALID,
1444
+ E_REPOSITORY_INDEX_REBUILD_FAILED,
1445
+ E_REPOSITORY_INDEX_REQUEST_INVALID,
1446
+ E_REPOSITORY_INDEX_LOCK_UNSAFE,
1447
+ E_PERSISTENT_TRANSPORT_FRAME_INVALID,
1448
+ E_PERSISTENT_TRANSPORT_FRAME_TOO_LARGE,
1449
+ E_PERSISTENT_TRANSPORT_HOST_STALE,
1450
+ E_PERSISTENT_TRANSPORT_INVALID_REQUEST,
1451
+ E_PERSISTENT_TRANSPORT_INVALID_RESPONSE,
1452
+ E_PERSISTENT_TRANSPORT_OWNERSHIP_UNVERIFIED,
1453
+ E_PERSISTENT_TRANSPORT_PROTOCOL_MISMATCH,
1454
+ E_PERSISTENT_TRANSPORT_START_FAILED,
1455
+ E_PERSISTENT_TRANSPORT_TIMEOUT,
1456
+ E_PERSISTENT_TRANSPORT_UNAVAILABLE,
1262
1457
  E_ACTION_INVALID,
1263
1458
  E_ACTION_NOT_FOUND,
1264
1459
  E_ACTION_STATE_MISMATCH,