@cassiomc1/forgeloop 1.10.1 → 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 (92) 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 +13 -1
  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 +21 -18
  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/ADVISORY_CONTEXT.md +24 -0
  22. package/docs/AGENT_PROTOCOL_SUMMARY.md +8 -1
  23. package/docs/CLI_REFERENCE.md +114 -2
  24. package/docs/DOCUMENTATION_GUIDE.md +10 -5
  25. package/docs/GETTING_STARTED.md +17 -0
  26. package/docs/MCP.md +16 -1
  27. package/docs/PACKAGE_CONTENTS.md +12 -1
  28. package/docs/PERSISTENT_SEARCH_TRANSPORT.md +289 -0
  29. package/docs/RECIPES.md +30 -0
  30. package/docs/RELEASE_CHECKLIST.md +36 -2
  31. package/docs/REPOSITORY_INDEX.md +553 -0
  32. package/docs/RIPWIRE_ADAPTER.md +189 -0
  33. package/docs/TROUBLESHOOTING.md +147 -0
  34. package/docs/UNIVERSAL_INTEGRATION.md +30 -0
  35. package/docs/diagrams/README.md +19 -0
  36. package/package.json +12 -2
  37. package/scripts/update-tgrep-manifest.mjs +86 -0
  38. package/scripts/verify-tgrep-manifest.mjs +17 -0
  39. package/src/adapters/ripwire/normalize.js +352 -0
  40. package/src/adapters/ripwire/process.js +248 -0
  41. package/src/adapters/ripwire/provider.js +245 -0
  42. package/src/cli.js +35 -0
  43. package/src/commands/doctor.js +76 -1
  44. package/src/commands/index-rebuild.js +1 -0
  45. package/src/commands/index-setup.js +1 -0
  46. package/src/commands/index-start.js +1 -0
  47. package/src/commands/index-status.js +1 -0
  48. package/src/commands/index-stop.js +1 -0
  49. package/src/commands/init.js +39 -1
  50. package/src/commands/repository-index.js +111 -0
  51. package/src/commands/search.js +1 -0
  52. package/src/commands/update.js +32 -4
  53. package/src/core/advisory-context/service.js +36 -14
  54. package/src/core/cli-command-definitions.js +97 -3
  55. package/src/core/command-executors.js +35 -2
  56. package/src/core/command-input.js +23 -0
  57. package/src/core/error-codes.js +195 -0
  58. package/src/core/filesystem.js +10 -1
  59. package/src/core/handoff-acceptance.js +10 -1
  60. package/src/core/integration-invocation-policy.js +27 -0
  61. package/src/core/integration-resources.js +16 -1
  62. package/src/core/protocol-info.js +23 -0
  63. package/src/core/work-state.js +15 -6
  64. package/src/integration.d.ts +101 -0
  65. package/src/integration.js +22 -0
  66. package/src/persistent-transport/client.js +293 -0
  67. package/src/persistent-transport/constants.js +24 -0
  68. package/src/persistent-transport/errors.js +38 -0
  69. package/src/persistent-transport/framing.js +61 -0
  70. package/src/persistent-transport/lifecycle.js +116 -0
  71. package/src/persistent-transport/ownership.js +184 -0
  72. package/src/persistent-transport/paths.js +31 -0
  73. package/src/persistent-transport/protocol.js +95 -0
  74. package/src/persistent-transport/server.js +256 -0
  75. package/src/persistent-transport/state.js +49 -0
  76. package/src/repository-index/args.js +59 -0
  77. package/src/repository-index/binary-manager.js +413 -0
  78. package/src/repository-index/constants.js +45 -0
  79. package/src/repository-index/errors.js +38 -0
  80. package/src/repository-index/lifecycle.js +17 -0
  81. package/src/repository-index/lock.js +113 -0
  82. package/src/repository-index/manifest.js +132 -0
  83. package/src/repository-index/metrics.js +30 -0
  84. package/src/repository-index/normalize-json.js +187 -0
  85. package/src/repository-index/paths.js +39 -0
  86. package/src/repository-index/platform.js +20 -0
  87. package/src/repository-index/process.js +140 -0
  88. package/src/repository-index/readiness.js +62 -0
  89. package/src/repository-index/search.js +262 -0
  90. package/src/repository-index/server.js +432 -0
  91. package/src/repository-index/status.js +397 -0
  92. package/src/repository-index/tgrep-manifest.json +38 -0
@@ -0,0 +1,245 @@
1
+ import path from "node:path";
2
+
3
+ import { assertSafePath } from "../../core/filesystem.js";
4
+ import {
5
+ normalizeAdvisoryRecallOptions,
6
+ } from "../../core/advisory-context/constants.js";
7
+ import {
8
+ E_ADVISORY_CONTEXT_PROVIDER_INVALID,
9
+ E_ADVISORY_CONTEXT_PROVIDER_UNAVAILABLE,
10
+ E_ADVISORY_CONTEXT_RESULT_INVALID,
11
+ E_ADVISORY_CONTEXT_TIMEOUT,
12
+ } from "../../core/error-codes.js";
13
+ import {
14
+ RIPWIRE_PROCESS_LIMITS,
15
+ parseRipwireVersion,
16
+ runRipwireCommand,
17
+ } from "./process.js";
18
+ import {
19
+ extractCandidateRows,
20
+ getRipwireRowPath,
21
+ normalizeReportedPath,
22
+ normalizeRipwireResult,
23
+ } from "./normalize.js";
24
+
25
+ function providerError(code, message) {
26
+ const error = new Error(message);
27
+ error.name = "RipwireAdvisoryProviderError";
28
+ error.code = code;
29
+ return error;
30
+ }
31
+
32
+ function assertAbsoluteExecutablePath(value) {
33
+ if (typeof value !== "string" || value.trim() === "" || !path.isAbsolute(value) || /\p{Cc}/u.test(value)) {
34
+ throw providerError(E_ADVISORY_CONTEXT_PROVIDER_INVALID, "Ripwire executablePath must be an absolute portable path");
35
+ }
36
+ return value;
37
+ }
38
+
39
+ function assertExpectedVersion(value) {
40
+ if (
41
+ typeof value !== "string"
42
+ || value.trim() === ""
43
+ || value.length > 64
44
+ || !/^[A-Za-z0-9][A-Za-z0-9._+-]*$/u.test(value)
45
+ ) {
46
+ throw providerError(E_ADVISORY_CONTEXT_PROVIDER_INVALID, "Ripwire expectedVersion must be a qualified version token under 64 characters");
47
+ }
48
+ return value;
49
+ }
50
+
51
+ function assertProcessLimit(value, maximum, label) {
52
+ if (!Number.isSafeInteger(value) || value < 1 || value > maximum) {
53
+ throw providerError(E_ADVISORY_CONTEXT_PROVIDER_INVALID, `${label} must be an integer between 1 and ${maximum}`);
54
+ }
55
+ return value;
56
+ }
57
+
58
+ function remainingTimeout(deadline) {
59
+ const remaining = deadline - Date.now();
60
+ if (remaining < 1) {
61
+ throw providerError(E_ADVISORY_CONTEXT_TIMEOUT, "Ripwire advisory recall deadline expired");
62
+ }
63
+ return remaining;
64
+ }
65
+
66
+ function reportTransport(callback, payload) {
67
+ if (typeof callback !== "function") return;
68
+ try {
69
+ callback(Object.freeze({ ...payload }));
70
+ } catch {
71
+ // Observability hooks cannot change advisory correctness or failure mapping.
72
+ }
73
+ }
74
+
75
+ async function runAndReport(executablePath, args, {
76
+ cwd,
77
+ timeoutMs,
78
+ spawnImpl,
79
+ env,
80
+ maxStdoutBytes,
81
+ maxStderrBytes,
82
+ onTransport,
83
+ kind,
84
+ } = {}) {
85
+ const result = await runRipwireCommand(executablePath, args, {
86
+ cwd,
87
+ timeoutMs,
88
+ spawnImpl,
89
+ env,
90
+ maxStdoutBytes,
91
+ maxStderrBytes,
92
+ });
93
+ reportTransport(onTransport, {
94
+ kind,
95
+ stdoutBytes: result.stdoutBytes,
96
+ stderrBytes: result.stderrBytes,
97
+ durationMs: result.durationMs,
98
+ });
99
+ return result;
100
+ }
101
+
102
+ function queryArguments(projectPath, query) {
103
+ return [
104
+ projectPath,
105
+ `--for=${query}`,
106
+ "--signatures-only",
107
+ "--json",
108
+ "--no-cache",
109
+ "--exclude=.forgeloop",
110
+ ];
111
+ }
112
+
113
+ /**
114
+ * Create an optional host-injected Ripwire adapter.
115
+ *
116
+ * Construction is inert: it validates the explicit executable/version pair
117
+ * but does not discover binaries, spawn processes, access the network, or
118
+ * write lifecycle state. Version qualification is repeated lazily before a
119
+ * query so a changed executable cannot silently change the provider identity.
120
+ */
121
+ export function createRipwireAdvisoryContextProvider({
122
+ executablePath,
123
+ expectedVersion,
124
+ spawnImpl,
125
+ env,
126
+ maxStdoutBytes = RIPWIRE_PROCESS_LIMITS.maxStdoutBytes,
127
+ maxStderrBytes = RIPWIRE_PROCESS_LIMITS.maxStderrBytes,
128
+ onTransport,
129
+ } = {}) {
130
+ const qualifiedExecutablePath = assertAbsoluteExecutablePath(executablePath);
131
+ const qualifiedExpectedVersion = assertExpectedVersion(expectedVersion);
132
+ const qualifiedStdoutLimit = assertProcessLimit(maxStdoutBytes, RIPWIRE_PROCESS_LIMITS.maxStdoutBytes, "Ripwire stdout limit");
133
+ const qualifiedStderrLimit = assertProcessLimit(maxStderrBytes, RIPWIRE_PROCESS_LIMITS.maxStderrBytes, "Ripwire stderr limit");
134
+
135
+ const provider = {
136
+ id: "ripwire",
137
+ version: qualifiedExpectedVersion,
138
+ async recall({
139
+ projectPath,
140
+ taskId,
141
+ query,
142
+ limit,
143
+ maxItemChars,
144
+ maxTotalChars,
145
+ timeoutMs,
146
+ } = {}) {
147
+ if (typeof projectPath !== "string" || !path.isAbsolute(projectPath) || /\p{Cc}/u.test(projectPath)) {
148
+ throw providerError(E_ADVISORY_CONTEXT_PROVIDER_INVALID, "Ripwire projectPath must be an absolute path");
149
+ }
150
+ if (typeof taskId !== "string" || taskId.trim() === "") {
151
+ throw providerError(E_ADVISORY_CONTEXT_PROVIDER_INVALID, "Ripwire taskId must be a non-empty string");
152
+ }
153
+ if (typeof query !== "string" || query.trim() === "" || /\p{Cc}/u.test(query)) {
154
+ throw providerError(E_ADVISORY_CONTEXT_PROVIDER_INVALID, "Ripwire query must be a non-empty portable string");
155
+ }
156
+ const options = normalizeAdvisoryRecallOptions({ limit, maxItemChars, maxTotalChars, timeoutMs });
157
+ const projectRoot = path.resolve(projectPath);
158
+ try {
159
+ await assertSafePath(projectRoot, ".");
160
+ } catch {
161
+ throw providerError(E_ADVISORY_CONTEXT_PROVIDER_UNAVAILABLE, "Ripwire target directory is unavailable or unsafe");
162
+ }
163
+
164
+ const deadline = Date.now() + options.timeoutMs;
165
+ let versionResult;
166
+ try {
167
+ versionResult = await runAndReport(qualifiedExecutablePath, ["--version"], {
168
+ cwd: projectRoot,
169
+ timeoutMs: remainingTimeout(deadline),
170
+ spawnImpl,
171
+ env,
172
+ maxStdoutBytes: Math.min(qualifiedStdoutLimit, 64 * 1024),
173
+ maxStderrBytes: qualifiedStderrLimit,
174
+ onTransport,
175
+ kind: "version",
176
+ });
177
+ } catch (error) {
178
+ throw error.code ? error : providerError(E_ADVISORY_CONTEXT_PROVIDER_UNAVAILABLE, "Ripwire version probe failed");
179
+ }
180
+
181
+ let actualVersion;
182
+ try {
183
+ actualVersion = parseRipwireVersion(versionResult.stdout);
184
+ } catch {
185
+ throw providerError(E_ADVISORY_CONTEXT_PROVIDER_INVALID, "Ripwire version probe was not qualified");
186
+ }
187
+ if (actualVersion !== qualifiedExpectedVersion) {
188
+ throw providerError(
189
+ E_ADVISORY_CONTEXT_PROVIDER_INVALID,
190
+ "Ripwire executable version does not match the expected qualified version",
191
+ );
192
+ }
193
+
194
+ let queryResult;
195
+ try {
196
+ queryResult = await runAndReport(qualifiedExecutablePath, queryArguments(projectRoot, query), {
197
+ cwd: projectRoot,
198
+ timeoutMs: remainingTimeout(deadline),
199
+ spawnImpl,
200
+ env,
201
+ maxStdoutBytes: qualifiedStdoutLimit,
202
+ maxStderrBytes: qualifiedStderrLimit,
203
+ onTransport,
204
+ kind: "query",
205
+ });
206
+ } catch (error) {
207
+ throw error.code ? error : providerError(E_ADVISORY_CONTEXT_PROVIDER_UNAVAILABLE, "Ripwire query failed");
208
+ }
209
+
210
+ let raw;
211
+ try {
212
+ raw = JSON.parse(queryResult.stdout);
213
+ } catch {
214
+ throw providerError(E_ADVISORY_CONTEXT_RESULT_INVALID, "Ripwire query did not return valid JSON");
215
+ }
216
+
217
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
218
+ throw providerError(E_ADVISORY_CONTEXT_RESULT_INVALID, "Ripwire query JSON root must be an object");
219
+ }
220
+
221
+ const safeSourcePaths = new Set();
222
+ const rows = extractCandidateRows(raw) ?? [];
223
+ for (const row of rows) {
224
+ const relativePath = normalizeReportedPath(getRipwireRowPath(row), projectRoot);
225
+ if (!relativePath) continue;
226
+ try {
227
+ await assertSafePath(projectRoot, relativePath);
228
+ safeSourcePaths.add(relativePath);
229
+ } catch {
230
+ // The normalizer will disclose and drop this candidate.
231
+ }
232
+ }
233
+
234
+ return normalizeRipwireResult(raw, {
235
+ projectPath: projectRoot,
236
+ ...options,
237
+ sourcePathValidator: (relativePath) => safeSourcePaths.has(relativePath),
238
+ });
239
+ },
240
+ };
241
+
242
+ return Object.freeze(provider);
243
+ }
244
+
245
+ export { queryArguments };
package/src/cli.js CHANGED
@@ -85,6 +85,11 @@ import { formatAttestationCreateResult } from "./commands/attestation-create.js"
85
85
  import { formatAttestationVerifyResult } from "./commands/attestation-verify.js";
86
86
  import { formatAttestationStatusResult } from "./commands/attestation-status.js";
87
87
  import { formatAttestationVerifyRangeResult } from "./commands/attestation-verify-range.js";
88
+ import {
89
+ formatRepositoryIndexResult,
90
+ formatRepositoryIndexStatus,
91
+ formatSearchResult,
92
+ } from "./commands/repository-index.js";
88
93
  import { defaultCommandInputValues, validateForgeLoopCommandInput } from "./core/command-input.js";
89
94
  import { COMMAND_EXECUTORS } from "./core/command-executors.js";
90
95
  import { resolveTarget } from "./core/filesystem.js";
@@ -368,6 +373,36 @@ export const COMMAND_HANDLERS = Object.freeze({
368
373
  }
369
374
  return result.ok ? 0 : 1;
370
375
  },
376
+ "index-setup": async ({ target, packageRoot, options }) => {
377
+ const { result } = await COMMAND_EXECUTORS["index-setup"]({ target, packageRoot, options });
378
+ renderJsonOr(options, result, formatRepositoryIndexResult);
379
+ return 0;
380
+ },
381
+ "index-start": async ({ target, packageRoot, options }) => {
382
+ const { result } = await COMMAND_EXECUTORS["index-start"]({ target, packageRoot, options });
383
+ renderJsonOr(options, result, formatRepositoryIndexResult);
384
+ return 0;
385
+ },
386
+ "index-stop": async ({ target, packageRoot, options }) => {
387
+ const { result } = await COMMAND_EXECUTORS["index-stop"]({ target, packageRoot, options });
388
+ renderJsonOr(options, result, formatRepositoryIndexResult);
389
+ return 0;
390
+ },
391
+ "index-status": async ({ target, packageRoot, options }) => {
392
+ const { result } = await COMMAND_EXECUTORS["index-status"]({ target, packageRoot, options });
393
+ renderJsonOr(options, result, formatRepositoryIndexStatus);
394
+ return 0;
395
+ },
396
+ "index-rebuild": async ({ target, packageRoot, options }) => {
397
+ const { result } = await COMMAND_EXECUTORS["index-rebuild"]({ target, packageRoot, options });
398
+ renderJsonOr(options, result, formatRepositoryIndexResult);
399
+ return 0;
400
+ },
401
+ search: async ({ target, packageRoot, options }) => {
402
+ const { result, exitCode } = await COMMAND_EXECUTORS.search({ target, packageRoot, options, transport: "cli" });
403
+ renderJsonOr(options, result, formatSearchResult);
404
+ return exitCode;
405
+ },
371
406
  route: async ({ target, packageRoot, options }) => {
372
407
  const { result } = await COMMAND_EXECUTORS.route({ target, packageRoot, options });
373
408
  renderJsonOr(options, result, formatRouteResult);
@@ -5,6 +5,10 @@ import { createEvidence } from "../core/evidence.js";
5
5
  import { LAYOUT_VERSION } from "../core/target-layout.js";
6
6
  import { inspectNativeAdapter, validateNativeAdapterTargets } from "../core/native-adapters.js";
7
7
  import { findIncompleteTransactions, recoverIncompleteTransactions } from "../core/transaction.js";
8
+ import { isRepositoryCandidate } from "../repository-index/lifecycle.js";
9
+ import { getRepositoryIndexStatus, sanitizeRepositoryIndexStatus } from "../repository-index/status.js";
10
+ import { searchRepository } from "../repository-index/search.js";
11
+ import { getPersistentTransportStatus } from "../persistent-transport/client.js";
8
12
 
9
13
  function finding(code, severity, relativePath, message, remediation = null, evidence = null) {
10
14
  const evidenceRecord = evidence && typeof evidence === "object"
@@ -114,7 +118,69 @@ async function adoptAdapters({ target, manifest, adoptPaths, findings }) {
114
118
  return nextManifest;
115
119
  }
116
120
 
117
- export async function runDoctor({ target, packageRoot, adoptPaths = [], strict = false, fix = false }) {
121
+ async function inspectRepositoryIndexForDoctor({ target, packageRoot, repositoryIndex, repositoryIndexOptions, findings }) {
122
+ const persistentTransport = await getPersistentTransportStatus({ homeDirectory: repositoryIndexOptions?.homeDirectory });
123
+ if (!repositoryIndex) return { status: "DISABLED", required: false, persistentTransport };
124
+ if (!(await isRepositoryCandidate(target))) {
125
+ return { status: "DEFERRED", required: true, reason: "target is not a Git repository", persistentTransport };
126
+ }
127
+
128
+ const status = await getRepositoryIndexStatus(target, { packageRoot, ...repositoryIndexOptions, includeLocalPaths: true });
129
+ const publicStatus = sanitizeRepositoryIndexStatus(status);
130
+ const result = {
131
+ status: publicStatus.health,
132
+ required: true,
133
+ engine: publicStatus.engine,
134
+ engineVersion: publicStatus.engineVersion,
135
+ indexPath: ".forgeloop/repository-index/tgrep",
136
+ server: publicStatus.server,
137
+ diagnostics: publicStatus.diagnostics,
138
+ persistentTransport,
139
+ };
140
+ if (status.health === "READY") {
141
+ try {
142
+ const smoke = await searchRepository(target, {
143
+ ...repositoryIndexOptions,
144
+ packageRoot,
145
+ pattern: "__FORGELOOP_DOCTOR_INDEX_SMOKE__",
146
+ fixedStrings: true,
147
+ maxCount: 1,
148
+ });
149
+ result.smokeSearch = {
150
+ status: "OK",
151
+ matchCount: smoke.matches.length,
152
+ nativeExitCode: smoke.metrics.exitCode,
153
+ };
154
+ } catch (error) {
155
+ result.smokeSearch = {
156
+ status: "ERROR",
157
+ code: error.code ?? "E_REPOSITORY_INDEX_SEARCH_FAILED",
158
+ message: error.message,
159
+ };
160
+ findings.push(finding(
161
+ error.code ?? "E_REPOSITORY_INDEX_SEARCH_FAILED",
162
+ "error",
163
+ ".forgeloop/repository-index",
164
+ `Repository Index smoke search failed: ${error.message}`,
165
+ "Run forgeloop index-status --json, then index-rebuild if the managed search service is unhealthy.",
166
+ error.message,
167
+ ));
168
+ }
169
+ }
170
+ if (status.health !== "READY") {
171
+ findings.push(finding(
172
+ publicStatus.diagnostics?.[0]?.code ?? "E_REPOSITORY_INDEX_SERVER_UNHEALTHY",
173
+ "error",
174
+ ".forgeloop/repository-index",
175
+ `Repository Index is ${status.health}; ${publicStatus.diagnostics?.[0]?.message ?? "native index health is not ready"}`,
176
+ "Run forgeloop index-setup or forgeloop index-rebuild with an approved managed tgrep asset.",
177
+ publicStatus,
178
+ ));
179
+ }
180
+ return result;
181
+ }
182
+
183
+ export async function runDoctor({ target, packageRoot, adoptPaths = [], strict = false, fix = false, repositoryIndex, repositoryIndexOptions }) {
118
184
  const findings = [];
119
185
  let incompleteTransactions = await findIncompleteTransactions(target);
120
186
  if (fix && incompleteTransactions.some((transaction) => transaction.status === "COMMITTING")) {
@@ -318,11 +384,20 @@ export async function runDoctor({ target, packageRoot, adoptPaths = [], strict =
318
384
  await writeManifest(target, manifest);
319
385
  }
320
386
 
387
+ const repositoryIndexResult = await inspectRepositoryIndexForDoctor({
388
+ target,
389
+ packageRoot,
390
+ repositoryIndex,
391
+ repositoryIndexOptions,
392
+ findings,
393
+ });
394
+
321
395
  const ok = findings.every((item) => item.severity !== "error")
322
396
  && (!strict || findings.every((item) => item.severity !== "warning"));
323
397
  return {
324
398
  ok,
325
399
  findings,
400
+ repositoryIndex: repositoryIndexResult,
326
401
  evidence: [createEvidence({
327
402
  kind: "OBSERVED",
328
403
  source: "ForgeLoop doctor",
@@ -0,0 +1 @@
1
+ export { runRepositoryIndexRebuild, formatRepositoryIndexResult } from "./repository-index.js";
@@ -0,0 +1 @@
1
+ export { runRepositoryIndexSetup, formatRepositoryIndexResult } from "./repository-index.js";
@@ -0,0 +1 @@
1
+ export { runRepositoryIndexStart, formatRepositoryIndexResult } from "./repository-index.js";
@@ -0,0 +1 @@
1
+ export { runRepositoryIndexStatus, formatRepositoryIndexStatus } from "./repository-index.js";
@@ -0,0 +1 @@
1
+ export { runRepositoryIndexStop, formatRepositoryIndexResult } from "./repository-index.js";
@@ -9,6 +9,8 @@ import { readTemplateEntries } from "../core/templates.js";
9
9
  import { PROJECT_ARTIFACT_PATHS } from "../core/task-paths.js";
10
10
  import { isKitPath } from "../core/target-layout.js";
11
11
  import { E_INIT_KIT_CONFLICT, E_POLICY_INITIALIZATION_FAILED } from "../core/error-codes.js";
12
+ import { isRepositoryCandidate } from "../repository-index/lifecycle.js";
13
+ import { setupRepositoryIndex } from "../repository-index/server.js";
12
14
 
13
15
  // Compatibility re-exports: the canonical semantic definitions live in
14
16
  // src/core/error-codes.js; this keeps existing import paths working while
@@ -107,6 +109,31 @@ function kitConflictError(relativePath) {
107
109
  return error;
108
110
  }
109
111
 
112
+ async function prepareRepositoryIndexForInit({ target, dryRun, packageRoot, repositoryIndex, repositoryIndexOptions, actions }) {
113
+ if (!repositoryIndex) return { status: "DISABLED", required: false };
114
+ if (!(await isRepositoryCandidate(target))) {
115
+ return { status: "DEFERRED", required: true, reason: "target is not a Git repository" };
116
+ }
117
+ if (dryRun) {
118
+ return {
119
+ status: "WOULD_SETUP",
120
+ required: true,
121
+ reason: "dry-run does not provision or execute the native index engine",
122
+ };
123
+ }
124
+
125
+ const setup = await setupRepositoryIndex(target, { ...repositoryIndexOptions, packageRoot });
126
+ actions.push({ action: "repository-index-ready", path: ".forgeloop/repository-index", reason: "managed tgrep index and watcher are ready" });
127
+ return {
128
+ status: "READY",
129
+ required: true,
130
+ health: setup.status?.health ?? "READY",
131
+ engine: setup.status?.engine ?? "tgrep",
132
+ engineVersion: setup.status?.engineVersion ?? null,
133
+ server: setup.status?.server ?? null,
134
+ };
135
+ }
136
+
110
137
  /**
111
138
  * Initializes a target project with the ForgeLoop kit and executable-policy
112
139
  * bootstrap, committing manifest authority LAST.
@@ -152,6 +179,8 @@ export async function runInit({
152
179
  packageRoot,
153
180
  packageVersion,
154
181
  hooks = {},
182
+ repositoryIndex,
183
+ repositoryIndexOptions,
155
184
  } = {}) {
156
185
  const entries = await readTemplateEntries(packageRoot);
157
186
  const existingManifest = await readManifest(target);
@@ -332,6 +361,15 @@ export async function runInit({
332
361
  }
333
362
  }
334
363
 
364
+ const repositoryIndexResult = await prepareRepositoryIndexForInit({
365
+ target,
366
+ dryRun,
367
+ packageRoot,
368
+ repositoryIndex,
369
+ repositoryIndexOptions,
370
+ actions,
371
+ });
372
+
335
373
  // 10. Commit manifest authority LAST: successful initialization is only
336
374
  // real once the manifest exists.
337
375
  if (hooks.beforeManifestWrite) {
@@ -343,5 +381,5 @@ export async function runInit({
343
381
  }
344
382
  await writeManifest(target, manifest, { dryRun });
345
383
 
346
- return { actions, manifest };
384
+ return { actions, manifest, repositoryIndex: repositoryIndexResult };
347
385
  }
@@ -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
  }