@cassiomc1/forgeloop 1.8.1 → 1.10.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 (93) hide show
  1. package/.cursor/rules/project-loop.mdc +6 -3
  2. package/.github/copilot-instructions.md +5 -0
  3. package/AGENTS.md +6 -0
  4. package/AGENT_COMPATIBILITY.md +15 -0
  5. package/CLAUDE.md +6 -0
  6. package/DELEGATION_PROTOCOL.md +6 -0
  7. package/DOCS_INDEX.md +9 -2
  8. package/ENG/accessibility-eng.md +12 -2
  9. package/ENG/design-code-eng.md +22 -1
  10. package/LOOP_ENGINEERING.md +41 -0
  11. package/LOOP_SYSTEM_DESIGN.md +33 -0
  12. package/ORCHESTRATOR_INTEGRATION.md +9 -0
  13. package/PROTOCOL_INTEGRATION.md +57 -0
  14. package/QUALITY_SCORECARD.md +2 -0
  15. package/README.md +51 -0
  16. package/TERMINOLOGY.md +12 -0
  17. package/THREAT_MODEL.md +48 -0
  18. package/completions/_forgeloop +5 -1
  19. package/completions/forgeloop.bash +9 -1
  20. package/completions/forgeloop.fish +27 -1
  21. package/docs/ADVISORY_CONTEXT.md +174 -0
  22. package/docs/AGENT_PROTOCOL_SUMMARY.md +33 -2
  23. package/docs/ARTIFACT_REFERENCE.md +142 -0
  24. package/docs/CLI_REFERENCE.md +124 -1
  25. package/docs/CROSS_HARNESS_CONTINUITY.md +85 -0
  26. package/docs/DOCUMENTATION_GUIDE.md +7 -0
  27. package/docs/GETTING_STARTED.md +22 -0
  28. package/docs/KNOWLEDGE_SOURCES.md +171 -0
  29. package/docs/MCP.md +17 -1
  30. package/docs/RECIPES.md +111 -0
  31. package/docs/RELEASE_CHECKLIST.md +14 -0
  32. package/docs/STRUCTURAL_QUALITY.md +350 -0
  33. package/docs/TROUBLESHOOTING.md +161 -2
  34. package/docs/UNIVERSAL_INTEGRATION.md +60 -0
  35. package/package.json +4 -1
  36. package/schemas/config.schema.json +46 -0
  37. package/schemas/handoff-envelope.schema.json +1 -0
  38. package/schemas/preflight.schema.json +2 -1
  39. package/schemas/structural-quality.schema.json +175 -0
  40. package/scripts/check-changelog-freshness.mjs +27 -3
  41. package/scripts/generate-agent-protocol-summary.mjs +18 -0
  42. package/src/cli.js +24 -0
  43. package/src/commands/handoff-accept.js +36 -0
  44. package/src/commands/handoff-list.js +28 -2
  45. package/src/commands/handoff-show.js +27 -2
  46. package/src/commands/quality-baseline.js +28 -0
  47. package/src/commands/quality-status.js +34 -0
  48. package/src/commands/quality-verify.js +30 -0
  49. package/src/commands/reconcile-continuity.js +4 -0
  50. package/src/core/advisory-context/constants.js +74 -0
  51. package/src/core/advisory-context/provider.js +287 -0
  52. package/src/core/advisory-context/service.js +140 -0
  53. package/src/core/artifact-registry.js +12 -0
  54. package/src/core/audit.js +38 -0
  55. package/src/core/bundles.js +134 -1
  56. package/src/core/cli-command-definitions.js +62 -0
  57. package/src/core/command-executors.js +28 -0
  58. package/src/core/command-input.js +23 -1
  59. package/src/core/completion-artifacts.js +2 -0
  60. package/src/core/completion.js +42 -0
  61. package/src/core/config.js +3 -0
  62. package/src/core/continuity-lint.js +89 -0
  63. package/src/core/continuity-reconciliation.js +16 -0
  64. package/src/core/continuity.js +10 -11
  65. package/src/core/error-codes.js +186 -0
  66. package/src/core/events.js +32 -0
  67. package/src/core/execution-profile-context.js +15 -1
  68. package/src/core/filesystem.js +34 -3
  69. package/src/core/handoff-acceptance.js +277 -0
  70. package/src/core/handoff.js +41 -8
  71. package/src/core/inspect.js +64 -0
  72. package/src/core/integration-invocation-policy.js +34 -2
  73. package/src/core/integration-resources.js +38 -1
  74. package/src/core/next-action-model.js +11 -1
  75. package/src/core/next-action-phases.js +84 -5
  76. package/src/core/phase.js +9 -1
  77. package/src/core/portable-context.js +103 -0
  78. package/src/core/preflight.js +33 -0
  79. package/src/core/protocol-info.js +33 -2
  80. package/src/core/runtime-context.js +58 -0
  81. package/src/core/schema-validation.js +1 -0
  82. package/src/core/structural-quality/artifacts.js +329 -0
  83. package/src/core/structural-quality/constants.js +67 -0
  84. package/src/core/structural-quality/policy.js +227 -0
  85. package/src/core/structural-quality/provider.js +287 -0
  86. package/src/core/structural-quality/sentrux-mcp.js +477 -0
  87. package/src/core/structural-quality/service.js +1138 -0
  88. package/src/core/structural-quality/source-fingerprint.js +112 -0
  89. package/src/core/structural-quality/status.js +3 -0
  90. package/src/core/task-paths.js +24 -0
  91. package/src/core/templates.js +1 -0
  92. package/src/integration.d.ts +141 -0
  93. package/src/integration.js +36 -0
@@ -0,0 +1,287 @@
1
+ import path from "node:path";
2
+
3
+ import {
4
+ E_STRUCTURAL_QUALITY_PROVIDER_INVALID,
5
+ E_STRUCTURAL_QUALITY_PROVIDER_VERSION_UNSUPPORTED,
6
+ E_STRUCTURAL_QUALITY_PROVIDER_UNAVAILABLE,
7
+ } from "../error-codes.js";
8
+ import {
9
+ STRUCTURAL_QUALITY_MAX_DIAGNOSTIC_STRING,
10
+ STRUCTURAL_QUALITY_MAX_DIAGNOSTICS,
11
+ STRUCTURAL_QUALITY_PROVIDER_ID_PATTERN,
12
+ STRUCTURAL_QUALITY_ROOT_CAUSES,
13
+ structuralQualityError,
14
+ STRUCTURAL_QUALITY_SENTRUX_VERIFIED_VERSIONS,
15
+ } from "./constants.js";
16
+
17
+ const SECRET_KEY = /(token|secret|password|passwd|api[-_]?key|authorization|cookie|private[-_]?key|credential)/iu;
18
+ const DETECTION_TRANSPORT = "mcp-stdio";
19
+
20
+ export function structuralQualityProviderCompatibility({ id, version, measurementModel, compatibilityKey } = {}) {
21
+ if (id !== "sentrux") return { supported: true, measurementModel, compatibilityKey };
22
+ if (!STRUCTURAL_QUALITY_SENTRUX_VERIFIED_VERSIONS.includes(version)) {
23
+ return {
24
+ supported: false,
25
+ reasonCode: E_STRUCTURAL_QUALITY_PROVIDER_VERSION_UNSUPPORTED,
26
+ };
27
+ }
28
+ return {
29
+ supported: true,
30
+ measurementModel: "structural-root-causes-v1",
31
+ compatibilityKey: "sentrux-structural-root-causes-v1",
32
+ };
33
+ }
34
+
35
+ function isRecord(value) {
36
+ return value !== null && typeof value === "object" && !Array.isArray(value);
37
+ }
38
+
39
+ function providerError(code, message, artifacts = []) {
40
+ return structuralQualityError(code, message, artifacts);
41
+ }
42
+
43
+ function requiredString(value, label, { allowEmpty = false } = {}) {
44
+ if (typeof value !== "string" || (!allowEmpty && value.trim() === "")) {
45
+ throw providerError(E_STRUCTURAL_QUALITY_PROVIDER_INVALID, `${label} must be a non-empty string`);
46
+ }
47
+ return value;
48
+ }
49
+
50
+ function score(value, label) {
51
+ if (!Number.isInteger(value) || value < 0 || value > 10_000) {
52
+ throw providerError(E_STRUCTURAL_QUALITY_PROVIDER_INVALID, `${label} must be an integer between 0 and 10000`);
53
+ }
54
+ return value;
55
+ }
56
+
57
+ function rawScore(value, label) {
58
+ if (typeof value !== "number" || !Number.isFinite(value)) {
59
+ throw providerError(E_STRUCTURAL_QUALITY_PROVIDER_INVALID, `${label} must be a finite number`);
60
+ }
61
+ return value;
62
+ }
63
+
64
+ function nonNegativeIntegerOrNull(value, label) {
65
+ if (value === undefined || value === null) return null;
66
+ if (!Number.isInteger(value) || value < 0) {
67
+ throw providerError(E_STRUCTURAL_QUALITY_PROVIDER_INVALID, `${label} must be a non-negative integer or null`);
68
+ }
69
+ return value;
70
+ }
71
+
72
+ function relativePortablePath(value, projectPath, label) {
73
+ if (typeof value !== "string") return value;
74
+ const trimmed = value.trim();
75
+ if (!trimmed) return trimmed;
76
+ if (!path.isAbsolute(trimmed)) return trimmed.replaceAll("\\", "/");
77
+ if (!projectPath) {
78
+ throw providerError(E_STRUCTURAL_QUALITY_PROVIDER_INVALID, `${label} must not contain an absolute path`);
79
+ }
80
+ const relative = path.relative(path.resolve(projectPath), path.resolve(trimmed));
81
+ if (!relative || (!relative.startsWith("..") && !path.isAbsolute(relative))) {
82
+ return relative.replaceAll("\\", "/") || ".";
83
+ }
84
+ throw providerError(E_STRUCTURAL_QUALITY_PROVIDER_INVALID, `${label} contains a path outside the project target`);
85
+ }
86
+
87
+ function boundedDiagnosticValue(value, projectPath, label, depth = 0) {
88
+ if (depth > 8) throw providerError(E_STRUCTURAL_QUALITY_PROVIDER_INVALID, `${label} is too deeply nested`);
89
+ if (typeof value === "string") {
90
+ if (value.length > STRUCTURAL_QUALITY_MAX_DIAGNOSTIC_STRING) {
91
+ throw providerError(E_STRUCTURAL_QUALITY_PROVIDER_INVALID, `${label} exceeds the diagnostic string limit`);
92
+ }
93
+ return relativePortablePath(value, projectPath, label);
94
+ }
95
+ if (typeof value === "number" || typeof value === "boolean" || value === null) return value;
96
+ if (Array.isArray(value)) {
97
+ if (value.length > STRUCTURAL_QUALITY_MAX_DIAGNOSTICS) {
98
+ throw providerError(E_STRUCTURAL_QUALITY_PROVIDER_INVALID, `${label} exceeds the diagnostic count limit`);
99
+ }
100
+ return value.map((item, index) => boundedDiagnosticValue(item, projectPath, `${label}[${index}]`, depth + 1));
101
+ }
102
+ if (!isRecord(value)) {
103
+ throw providerError(E_STRUCTURAL_QUALITY_PROVIDER_INVALID, `${label} contains an unsupported value`);
104
+ }
105
+ const output = {};
106
+ for (const [key, item] of Object.entries(value)) {
107
+ if (SECRET_KEY.test(key)) {
108
+ throw providerError(E_STRUCTURAL_QUALITY_PROVIDER_INVALID, `${label} contains a secret-like field`);
109
+ }
110
+ output[key] = boundedDiagnosticValue(item, projectPath, `${label}.${key}`, depth + 1);
111
+ }
112
+ return output;
113
+ }
114
+
115
+ function rootCauseInput(raw, cause) {
116
+ const source = raw?.rootCauses?.[cause]
117
+ ?? raw?.root_causes?.[cause]
118
+ ?? raw?.rootCauseScores?.[cause]
119
+ ?? raw?.root_cause_scores?.[cause];
120
+ if (typeof source === "number") return { score: source, raw: source };
121
+ if (!isRecord(source)) {
122
+ throw providerError(E_STRUCTURAL_QUALITY_PROVIDER_INVALID, `scan.rootCauses.${cause} is required`);
123
+ }
124
+ return {
125
+ score: source.score ?? source.normalizedScore ?? source.normalized_score,
126
+ raw: source.raw ?? source.value ?? source.metric,
127
+ };
128
+ }
129
+
130
+ function canonicalBottleneck(rootCauses) {
131
+ return STRUCTURAL_QUALITY_ROOT_CAUSES.reduce((best, cause) => (
132
+ best === null || rootCauses[cause].score < rootCauses[best].score ? cause : best
133
+ ), null);
134
+ }
135
+
136
+ function normalizeStatistics(raw) {
137
+ const statistics = raw?.statistics ?? {};
138
+ const source = raw?.scan ?? raw;
139
+ return {
140
+ files: nonNegativeIntegerOrNull(statistics.files ?? source.files ?? source.fileCount ?? source.file_count, "snapshot.statistics.files"),
141
+ lines: nonNegativeIntegerOrNull(statistics.lines ?? source.lines ?? source.lineCount ?? source.line_count, "snapshot.statistics.lines"),
142
+ importEdges: nonNegativeIntegerOrNull(statistics.importEdges ?? statistics.import_edges ?? source.importEdges ?? source.import_edges ?? source.importEdgeCount, "snapshot.statistics.importEdges"),
143
+ crossModuleEdges: nonNegativeIntegerOrNull(statistics.crossModuleEdges ?? statistics.cross_module_edges ?? source.crossModuleEdges ?? source.cross_module_edges, "snapshot.statistics.crossModuleEdges"),
144
+ };
145
+ }
146
+
147
+ export function normalizeStructuralQualityDetection(raw = {}, defaults = {}) {
148
+ const source = isRecord(raw) ? raw : {};
149
+ const providerId = source.providerId ?? source.provider_id ?? defaults.providerId ?? defaults.id;
150
+ requiredString(providerId, "detection.providerId");
151
+ if (!STRUCTURAL_QUALITY_PROVIDER_ID_PATTERN.test(providerId)) {
152
+ throw providerError(E_STRUCTURAL_QUALITY_PROVIDER_INVALID, "detection.providerId must be a lower-case provider ID");
153
+ }
154
+ const providerVersion = source.providerVersion ?? source.provider_version ?? source.version ?? defaults.providerVersion ?? defaults.version ?? null;
155
+ if (providerVersion !== null && typeof providerVersion !== "string") {
156
+ throw providerError(E_STRUCTURAL_QUALITY_PROVIDER_INVALID, "detection.providerVersion must be a string or null");
157
+ }
158
+ const transport = source.transport ?? defaults.transport ?? DETECTION_TRANSPORT;
159
+ requiredString(transport, "detection.transport");
160
+ const measurementModel = source.measurementModel ?? source.measurement_model ?? defaults.measurementModel ?? defaults.measurement_model ?? "structural-root-causes-v1";
161
+ requiredString(measurementModel, "detection.measurementModel");
162
+ const compatibilityKey = source.compatibilityKey ?? source.compatibility_key ?? defaults.compatibilityKey ?? defaults.compatibility_key ?? null;
163
+ if (compatibilityKey !== null && typeof compatibilityKey !== "string") {
164
+ throw providerError(E_STRUCTURAL_QUALITY_PROVIDER_INVALID, "detection.compatibilityKey must be a string or null");
165
+ }
166
+ const reasonCode = source.reasonCode ?? source.reason_code ?? null;
167
+ if (reasonCode !== null && typeof reasonCode !== "string") {
168
+ throw providerError(E_STRUCTURAL_QUALITY_PROVIDER_INVALID, "detection.reasonCode must be a string or null");
169
+ }
170
+ return {
171
+ available: source.available === true,
172
+ providerId,
173
+ providerVersion,
174
+ transport,
175
+ measurementModel,
176
+ compatibilityKey,
177
+ reasonCode,
178
+ };
179
+ }
180
+
181
+ export function normalizeStructuralQualitySnapshot(raw, { projectPath = null } = {}) {
182
+ const source = raw?.snapshot && isRecord(raw.snapshot) ? raw.snapshot : raw;
183
+ if (!isRecord(source)) throw providerError(E_STRUCTURAL_QUALITY_PROVIDER_INVALID, "provider scan result must be an object");
184
+ const qualitySignal = source.qualitySignal ?? source.quality_signal ?? source.signal;
185
+ const rootCauses = Object.fromEntries(STRUCTURAL_QUALITY_ROOT_CAUSES.map((cause) => {
186
+ const input = rootCauseInput(source, cause);
187
+ return [cause, {
188
+ score: score(input.score, `snapshot.rootCauses.${cause}.score`),
189
+ raw: rawScore(input.raw, `snapshot.rootCauses.${cause}.raw`),
190
+ }];
191
+ }));
192
+ const snapshot = {
193
+ qualitySignal: score(qualitySignal, "snapshot.qualitySignal"),
194
+ bottleneck: canonicalBottleneck(rootCauses),
195
+ rootCauses,
196
+ statistics: normalizeStatistics(source),
197
+ diagnostics: source.diagnostics === undefined || source.diagnostics === null
198
+ ? null
199
+ : boundedDiagnosticValue(source.diagnostics, projectPath, "snapshot.diagnostics"),
200
+ };
201
+ if (source.bottleneck !== undefined && source.bottleneck !== snapshot.bottleneck) {
202
+ throw providerError(E_STRUCTURAL_QUALITY_PROVIDER_INVALID, `snapshot.bottleneck must be the canonical lowest-score root cause (${snapshot.bottleneck})`);
203
+ }
204
+ return snapshot;
205
+ }
206
+
207
+ export function assertStructuralQualityProvider(provider) {
208
+ if (!isRecord(provider)) {
209
+ throw providerError(E_STRUCTURAL_QUALITY_PROVIDER_INVALID, "Structural-quality provider must be an object");
210
+ }
211
+ if (typeof provider.id !== "string" || !STRUCTURAL_QUALITY_PROVIDER_ID_PATTERN.test(provider.id)) {
212
+ throw providerError(E_STRUCTURAL_QUALITY_PROVIDER_INVALID, "Structural-quality provider id must be a lower-case provider ID");
213
+ }
214
+ if (typeof provider.observe !== "function" && (typeof provider.detect !== "function" || typeof provider.scan !== "function")) {
215
+ throw providerError(E_STRUCTURAL_QUALITY_PROVIDER_INVALID, "Structural-quality provider must expose observe(input) or detect(input) and scan(input)");
216
+ }
217
+ return provider;
218
+ }
219
+
220
+ function freezeProviderInput(input = {}) {
221
+ const source = isRecord(input) ? input : {};
222
+ const projectPath = requiredString(source.projectPath ?? source.target, "provider input.projectPath");
223
+ const taskId = requiredString(source.taskId, "provider input.taskId");
224
+ const timeoutMs = source.timeoutMs ?? 120_000;
225
+ const maxOutputBytes = source.maxOutputBytes ?? 2 * 1024 * 1024;
226
+ if (!Number.isInteger(timeoutMs) || timeoutMs < 0) throw providerError(E_STRUCTURAL_QUALITY_PROVIDER_INVALID, "provider input.timeoutMs must be a non-negative integer");
227
+ if (!Number.isInteger(maxOutputBytes) || maxOutputBytes < 1) throw providerError(E_STRUCTURAL_QUALITY_PROVIDER_INVALID, "provider input.maxOutputBytes must be a positive integer");
228
+ return Object.freeze({ projectPath, taskId, timeoutMs, maxOutputBytes });
229
+ }
230
+
231
+ export function createStructuralQualityProviderRegistry({ providers = {}, builtIns = {} } = {}) {
232
+ if (!isRecord(providers) || !isRecord(builtIns)) {
233
+ throw providerError(E_STRUCTURAL_QUALITY_PROVIDER_INVALID, "Structural-quality provider registry entries must be objects");
234
+ }
235
+ const custom = new Map();
236
+ for (const [id, provider] of Object.entries(providers)) {
237
+ if (!STRUCTURAL_QUALITY_PROVIDER_ID_PATTERN.test(id) || id === "sentrux") {
238
+ throw providerError(E_STRUCTURAL_QUALITY_PROVIDER_INVALID, `Invalid or reserved provider ID: ${id}`);
239
+ }
240
+ custom.set(id, provider);
241
+ }
242
+ const builtInEntries = new Map(Object.entries(builtIns));
243
+ return Object.freeze({
244
+ async resolve(name, input) {
245
+ const providerOrFactory = custom.get(name) ?? builtInEntries.get(name);
246
+ if (!providerOrFactory) {
247
+ throw providerError(E_STRUCTURAL_QUALITY_PROVIDER_UNAVAILABLE, `Structural-quality provider is unavailable: ${name}`);
248
+ }
249
+ const providerInput = freezeProviderInput(input);
250
+ const provider = typeof providerOrFactory === "function"
251
+ ? await providerOrFactory(providerInput)
252
+ : providerOrFactory;
253
+ const asserted = assertStructuralQualityProvider(provider);
254
+ if (asserted.id !== name) {
255
+ throw providerError(E_STRUCTURAL_QUALITY_PROVIDER_INVALID, `Structural-quality provider identity ${asserted.id} does not match registry key ${name}`);
256
+ }
257
+ return asserted;
258
+ },
259
+ });
260
+ }
261
+
262
+ export async function resolveStructuralQualityProvider({ providerName = "sentrux", target, taskId, timeoutMs, maxOutputBytes, runtimeContext } = {}) {
263
+ if (typeof providerName !== "string" || !STRUCTURAL_QUALITY_PROVIDER_ID_PATTERN.test(providerName)) {
264
+ throw providerError(E_STRUCTURAL_QUALITY_PROVIDER_INVALID, `Invalid structural-quality provider ID: ${providerName}`);
265
+ }
266
+ const configured = runtimeContext?.structuralQualityProviders;
267
+ const custom = configured instanceof Map ? Object.fromEntries(configured.entries()) : configured ?? {};
268
+ if (providerName !== "sentrux" && !Object.prototype.hasOwnProperty.call(custom, providerName)) {
269
+ throw providerError(E_STRUCTURAL_QUALITY_PROVIDER_UNAVAILABLE, `Structural-quality provider is unavailable: ${providerName}`);
270
+ }
271
+ const builtIns = {
272
+ sentrux: async (input) => {
273
+ const { createSentruxStructuralQualityProvider } = await import("./sentrux-mcp.js");
274
+ return createSentruxStructuralQualityProvider(input);
275
+ },
276
+ };
277
+ return createStructuralQualityProviderRegistry({ providers: custom, builtIns }).resolve(providerName, {
278
+ projectPath: target,
279
+ taskId,
280
+ timeoutMs,
281
+ maxOutputBytes,
282
+ });
283
+ }
284
+
285
+ export function providerInputFor({ projectPath, taskId, timeoutMs, maxOutputBytes } = {}) {
286
+ return freezeProviderInput({ projectPath, taskId, timeoutMs, maxOutputBytes });
287
+ }