@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.
- package/.cursor/rules/project-loop.mdc +2 -2
- package/.forgeloop/forgeloop.gitignore +1 -0
- package/.github/copilot-instructions.md +2 -2
- package/AGENTS.md +1 -0
- package/AGENT_COMPATIBILITY.md +7 -0
- package/CLAUDE.md +1 -0
- package/DOCS_INDEX.md +13 -1
- package/LOOP_SYSTEM_DESIGN.md +12 -0
- package/ORCHESTRATOR_INTEGRATION.md +9 -0
- package/PROTOCOL_INTEGRATION.md +17 -0
- package/README.md +21 -18
- package/THIRD_PARTY_NOTICES.md +11 -0
- package/THREAT_MODEL.md +21 -0
- package/benchmarks/repository-index/README.md +73 -0
- package/benchmarks/repository-index/queries.json +12 -0
- package/benchmarks/repository-index/run-hot-path.mjs +142 -0
- package/benchmarks/repository-index/run-persistent-transport.mjs +169 -0
- package/completions/_forgeloop +7 -1
- package/completions/forgeloop.bash +13 -1
- package/completions/forgeloop.fish +40 -1
- package/docs/ADVISORY_CONTEXT.md +24 -0
- package/docs/AGENT_PROTOCOL_SUMMARY.md +8 -1
- package/docs/CLI_REFERENCE.md +114 -2
- package/docs/DOCUMENTATION_GUIDE.md +10 -5
- package/docs/GETTING_STARTED.md +17 -0
- package/docs/MCP.md +16 -1
- package/docs/PACKAGE_CONTENTS.md +12 -1
- package/docs/PERSISTENT_SEARCH_TRANSPORT.md +289 -0
- package/docs/RECIPES.md +30 -0
- package/docs/RELEASE_CHECKLIST.md +36 -2
- package/docs/REPOSITORY_INDEX.md +553 -0
- package/docs/RIPWIRE_ADAPTER.md +189 -0
- package/docs/TROUBLESHOOTING.md +147 -0
- package/docs/UNIVERSAL_INTEGRATION.md +30 -0
- package/docs/diagrams/README.md +19 -0
- package/package.json +12 -2
- package/scripts/update-tgrep-manifest.mjs +86 -0
- package/scripts/verify-tgrep-manifest.mjs +17 -0
- package/src/adapters/ripwire/normalize.js +352 -0
- package/src/adapters/ripwire/process.js +248 -0
- package/src/adapters/ripwire/provider.js +245 -0
- package/src/cli.js +35 -0
- package/src/commands/doctor.js +76 -1
- package/src/commands/index-rebuild.js +1 -0
- package/src/commands/index-setup.js +1 -0
- package/src/commands/index-start.js +1 -0
- package/src/commands/index-status.js +1 -0
- package/src/commands/index-stop.js +1 -0
- package/src/commands/init.js +39 -1
- package/src/commands/repository-index.js +111 -0
- package/src/commands/search.js +1 -0
- package/src/commands/update.js +32 -4
- package/src/core/advisory-context/service.js +36 -14
- package/src/core/cli-command-definitions.js +97 -3
- package/src/core/command-executors.js +35 -2
- package/src/core/command-input.js +23 -0
- package/src/core/error-codes.js +195 -0
- package/src/core/filesystem.js +10 -1
- package/src/core/handoff-acceptance.js +10 -1
- package/src/core/integration-invocation-policy.js +27 -0
- package/src/core/integration-resources.js +16 -1
- package/src/core/protocol-info.js +23 -0
- package/src/core/work-state.js +15 -6
- package/src/integration.d.ts +101 -0
- package/src/integration.js +22 -0
- package/src/persistent-transport/client.js +293 -0
- package/src/persistent-transport/constants.js +24 -0
- package/src/persistent-transport/errors.js +38 -0
- package/src/persistent-transport/framing.js +61 -0
- package/src/persistent-transport/lifecycle.js +116 -0
- package/src/persistent-transport/ownership.js +184 -0
- package/src/persistent-transport/paths.js +31 -0
- package/src/persistent-transport/protocol.js +95 -0
- package/src/persistent-transport/server.js +256 -0
- package/src/persistent-transport/state.js +49 -0
- package/src/repository-index/args.js +59 -0
- package/src/repository-index/binary-manager.js +413 -0
- package/src/repository-index/constants.js +45 -0
- package/src/repository-index/errors.js +38 -0
- package/src/repository-index/lifecycle.js +17 -0
- package/src/repository-index/lock.js +113 -0
- package/src/repository-index/manifest.js +132 -0
- package/src/repository-index/metrics.js +30 -0
- package/src/repository-index/normalize-json.js +187 -0
- package/src/repository-index/paths.js +39 -0
- package/src/repository-index/platform.js +20 -0
- package/src/repository-index/process.js +140 -0
- package/src/repository-index/readiness.js +62 -0
- package/src/repository-index/search.js +262 -0
- package/src/repository-index/server.js +432 -0
- package/src/repository-index/status.js +397 -0
- 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);
|
package/src/commands/doctor.js
CHANGED
|
@@ -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
|
-
|
|
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";
|
package/src/commands/init.js
CHANGED
|
@@ -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";
|
package/src/commands/update.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
}
|