@cassiomc1/forgeloop 0.1.10 → 0.1.12

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 (69) hide show
  1. package/.cursor/rules/project-loop.mdc +16 -18
  2. package/.github/copilot-instructions.md +16 -19
  3. package/AGENTS.md +18 -24
  4. package/AGENT_COMPATIBILITY.md +4 -211
  5. package/CLAUDE.md +16 -18
  6. package/LOOP_ENGINEERING.md +73 -0
  7. package/LOOP_SYSTEM_DESIGN.md +9 -5
  8. package/ORCHESTRATOR_INTEGRATION.md +4 -0
  9. package/PROTOCOL_INTEGRATION.md +171 -0
  10. package/QUALITY_SCORECARD.md +1 -1
  11. package/README.md +123 -103
  12. package/TERMINOLOGY.md +2 -0
  13. package/THREAT_MODEL.md +4 -0
  14. package/package.json +5 -4
  15. package/schemas/current-contract.schema.json +48 -2
  16. package/schemas/evidence.schema.json +1 -0
  17. package/schemas/execution-receipt.schema.json +2 -0
  18. package/schemas/work-state.schema.json +44 -1
  19. package/src/cli.js +48 -4
  20. package/src/commands/record-terminal-result.js +18 -0
  21. package/src/commands/validate-protocol.js +14 -2
  22. package/src/core/checks.js +20 -0
  23. package/src/core/completion-artifacts.js +266 -11
  24. package/src/core/completion-recovery.js +15 -0
  25. package/src/core/completion-relationships.js +23 -9
  26. package/src/core/completion.js +130 -2
  27. package/src/core/contract.js +82 -4
  28. package/src/core/coverage.js +17 -12
  29. package/src/core/discovery-surfaces.js +22 -0
  30. package/src/core/events.js +183 -2
  31. package/src/core/evidence-readiness.js +333 -0
  32. package/src/core/evidence.js +22 -4
  33. package/src/core/execution-prerequisites.js +6 -1
  34. package/src/core/inspect.js +23 -12
  35. package/src/core/native-adapters.js +30 -2
  36. package/src/core/next-action.js +103 -27
  37. package/src/core/phase.js +68 -5
  38. package/src/core/protocol.js +28 -1
  39. package/src/core/receipt.js +5 -0
  40. package/src/core/schema-validation.js +28 -6
  41. package/src/core/templates.js +1 -0
  42. package/src/core/work-state.js +27 -0
  43. package/conformance/README.md +0 -148
  44. package/conformance/backend-auth/EXPECTED_ROUTE.json +0 -7
  45. package/conformance/backend-auth/REQUEST.md +0 -4
  46. package/conformance/backend-auth/REQUIRED_EVIDENCE.json +0 -3
  47. package/conformance/backend-auth/REQUIRED_GATES.json +0 -3
  48. package/conformance/blind-premium-website/EXPECTED_ROUTE.json +0 -7
  49. package/conformance/blind-premium-website/REQUEST.md +0 -6
  50. package/conformance/blind-premium-website/REQUIRED_EVIDENCE.json +0 -14
  51. package/conformance/blind-premium-website/REQUIRED_GATES.json +0 -3
  52. package/conformance/complete-website/EXPECTED_ROUTE.json +0 -7
  53. package/conformance/complete-website/REQUEST.md +0 -6
  54. package/conformance/complete-website/REQUIRED_EVIDENCE.json +0 -3
  55. package/conformance/complete-website/REQUIRED_GATES.json +0 -3
  56. package/conformance/docs-only/EXPECTED_ROUTE.json +0 -7
  57. package/conformance/docs-only/REQUEST.md +0 -4
  58. package/conformance/docs-only/REQUIRED_EVIDENCE.json +0 -3
  59. package/conformance/docs-only/REQUIRED_GATES.json +0 -3
  60. package/conformance/runs/2026-08-11-codex-first-live.md +0 -98
  61. package/conformance/runs/2026-08-11-codex-second-live.md +0 -87
  62. package/conformance/runs/2026-08-13-codex-fifth-live.md +0 -386
  63. package/conformance/runs/2026-08-13-codex-fourth-live.md +0 -309
  64. package/conformance/runs/2026-08-13-codex-sixth-live.md +0 -412
  65. package/conformance/simple-bug/EXPECTED_ROUTE.json +0 -7
  66. package/conformance/simple-bug/REQUEST.md +0 -4
  67. package/conformance/simple-bug/REQUIRED_EVIDENCE.json +0 -3
  68. package/conformance/simple-bug/REQUIRED_GATES.json +0 -3
  69. package/src/core/agent-support.js +0 -89
@@ -0,0 +1,333 @@
1
+ import { sha256 } from "./manifest.js";
2
+
3
+ export const REQUIREMENT_TYPES = Object.freeze([
4
+ "PRODUCT",
5
+ "VERIFICATION",
6
+ "LIFECYCLE",
7
+ "PUBLICATION",
8
+ "PRODUCTION_READINESS",
9
+ ]);
10
+
11
+ export const TERMINAL_OWNED_TYPES = Object.freeze([
12
+ "LIFECYCLE",
13
+ "PUBLICATION",
14
+ "PRODUCTION_READINESS",
15
+ ]);
16
+
17
+ const LIFECYCLE_TERMS = /\b(?:lifecycle reaches|forgeloop reaches|complete returns|validator-backed complete|completion validated|review is approved)\b/i;
18
+ const PUBLICATION_TERMS = /\b(?:publication succeeds|release (?:is )?published|package (?:is )?published|published to|published package)\b/i;
19
+ const PRODUCTION_TERMS = /\b(?:deployment succeeds|production deployment|production readiness|production validation|production smoke|deployed to)\b/i;
20
+ const NON_TERMINAL_TERMS = /\b(?:tests?|lint|build|typecheck|types?|coverage|keyboard|zoom|contrast|motion|checks?|unit|e2e|integration|suite|regression|browser|responsive|html|css|js|component|api)\b/i;
21
+ const CONJUNCTION_TERMS = /\b(?:and|with|then|plus|also|after)\b/i;
22
+
23
+ export function isMixedTerminalRequirement(text) {
24
+ if (typeof text !== "string") return false;
25
+ const hasTerminal = LIFECYCLE_TERMS.test(text) || PUBLICATION_TERMS.test(text) || PRODUCTION_TERMS.test(text);
26
+ if (!hasTerminal) return false;
27
+ return NON_TERMINAL_TERMS.test(text) || CONJUNCTION_TERMS.test(text);
28
+ }
29
+
30
+ function stableId(text) {
31
+ return `REQ_${sha256(Buffer.from(text.trim().replace(/\s+/g, " ").toLowerCase())).slice(0, 16).toUpperCase()}`;
32
+ }
33
+
34
+ export function isPublicationStatusSatisfied(actual, required) {
35
+ if (required === "committed") {
36
+ return ["committed", "pushed", "published"].includes(actual);
37
+ }
38
+ if (required === "pushed") {
39
+ return ["pushed", "published"].includes(actual);
40
+ }
41
+ if (required === "published") {
42
+ return actual === "published";
43
+ }
44
+ if (required === "deployed") {
45
+ return actual === "deployed";
46
+ }
47
+ return actual === required;
48
+ }
49
+
50
+ export function classifyRequirement(input) {
51
+ if (typeof input === "string") {
52
+ const text = input.trim();
53
+ const isMixed = isMixedTerminalRequirement(text);
54
+ const type = isMixed
55
+ ? "VERIFICATION"
56
+ : LIFECYCLE_TERMS.test(text)
57
+ ? "LIFECYCLE"
58
+ : PUBLICATION_TERMS.test(text)
59
+ ? "PUBLICATION"
60
+ : PRODUCTION_TERMS.test(text)
61
+ ? "PRODUCTION_READINESS"
62
+ : "VERIFICATION";
63
+ let requiredPublicationStatus = undefined;
64
+ if (type === "PUBLICATION") {
65
+ if (/\b(?:publish|published|release published|published package)\b/i.test(text)) {
66
+ requiredPublicationStatus = "published";
67
+ } else if (/\b(?:deploy|deployed)\b/i.test(text)) {
68
+ requiredPublicationStatus = "deployed";
69
+ } else if (/\b(?:push|pushed)\b/i.test(text)) {
70
+ requiredPublicationStatus = "pushed";
71
+ } else if (/\b(?:commit|committed)\b/i.test(text)) {
72
+ requiredPublicationStatus = "committed";
73
+ } else {
74
+ requiredPublicationStatus = "published";
75
+ }
76
+ }
77
+ return {
78
+ id: stableId(text),
79
+ text,
80
+ type,
81
+ operator: "SINGLE",
82
+ lifecycleOwned: type === "LIFECYCLE",
83
+ terminalOwned: TERMINAL_OWNED_TYPES.includes(type),
84
+ mixedTerminal: isMixed,
85
+ requiredEvidenceKind: "OBSERVED",
86
+ requiredPublicationStatus,
87
+ requirements: [],
88
+ };
89
+ }
90
+ if (!input || typeof input !== "object" || Array.isArray(input)) {
91
+ throw new TypeError("Requirement must be a string or object");
92
+ }
93
+ const text = input.text ?? input.id;
94
+ const classified = classifyRequirement(text);
95
+ const type = input.type ?? classified.type;
96
+ const isMixed = input.mixedTerminal ?? (typeof text === "string" && isMixedTerminalRequirement(text));
97
+ return {
98
+ ...classified,
99
+ ...structuredClone(input),
100
+ id: input.id ?? classified.id,
101
+ text,
102
+ type,
103
+ operator: input.operator ?? (input.requirements?.length ? "ALL" : "SINGLE"),
104
+ lifecycleOwned: input.lifecycleOwned ?? type === "LIFECYCLE",
105
+ terminalOwned: input.terminalOwned ?? TERMINAL_OWNED_TYPES.includes(type),
106
+ mixedTerminal: isMixed,
107
+ requiredEvidenceKind: input.requiredEvidenceKind ?? "OBSERVED",
108
+ requiredPublicationStatus: input.requiredPublicationStatus ?? classified.requiredPublicationStatus,
109
+ requirements: (input.requirements ?? []).map(classifyRequirement),
110
+ };
111
+ }
112
+
113
+ export function normalizeRequirements(requirements = []) {
114
+ const byId = new Map();
115
+ for (const raw of requirements) {
116
+ const requirement = classifyRequirement(raw);
117
+ if (!byId.has(requirement.id)) byId.set(requirement.id, requirement);
118
+ }
119
+ return [...byId.values()].sort((left, right) => left.id.localeCompare(right.id));
120
+ }
121
+
122
+ export function ordinaryRequirements(requirements = []) {
123
+ return normalizeRequirements(requirements).filter((requirement) => !requirement.terminalOwned);
124
+ }
125
+
126
+ export function ordinaryLeafRequirements(requirements = []) {
127
+ const result = [];
128
+ function visit(requirement) {
129
+ if (requirement.terminalOwned) return;
130
+ if (requirement.operator === "ALL" && Array.isArray(requirement.requirements) && requirement.requirements.length > 0) {
131
+ for (const child of requirement.requirements) {
132
+ visit(child);
133
+ }
134
+ return;
135
+ }
136
+ result.push(requirement);
137
+ }
138
+ for (const req of normalizeRequirements(requirements)) {
139
+ visit(req);
140
+ }
141
+ return result;
142
+ }
143
+
144
+ export function terminalRequirements(requirements = []) {
145
+ return normalizeRequirements(requirements).filter((requirement) => requirement.terminalOwned);
146
+ }
147
+
148
+ export function lifecycleRequirements(requirements = []) {
149
+ return normalizeRequirements(requirements).filter((requirement) => requirement.type === "LIFECYCLE");
150
+ }
151
+
152
+ export function publicationRequirements(requirements = []) {
153
+ return normalizeRequirements(requirements).filter((requirement) => requirement.type === "PUBLICATION");
154
+ }
155
+
156
+ export function productionReadinessRequirements(requirements = []) {
157
+ return normalizeRequirements(requirements).filter((requirement) => requirement.type === "PRODUCTION_READINESS");
158
+ }
159
+
160
+ export function evaluateTerminalRequirements({ requirements = [], receipt = null } = {}) {
161
+ const normalized = normalizeRequirements(requirements);
162
+ const terminal = terminalRequirements(normalized);
163
+ const result = {
164
+ covered: [],
165
+ pending: [],
166
+ invalid: [],
167
+ errors: [],
168
+ };
169
+ const pubStatus = receipt ? (receipt.publicationStatus ?? "not-published") : "not-published";
170
+ const prodStatus = receipt ? (receipt.productionReadiness ?? "not-verified") : "not-verified";
171
+ const receiptEvidence = Array.isArray(receipt?.evidence) ? receipt.evidence : [];
172
+
173
+ for (const req of terminal) {
174
+ if (req.type === "LIFECYCLE") {
175
+ result.covered.push(req);
176
+ } else if (req.type === "PUBLICATION") {
177
+ const requiredLevel = req.requiredPublicationStatus ?? "published";
178
+ const matchingEvidence = receiptEvidence
179
+ .filter((item) => item.kind === "OBSERVED")
180
+ .filter((item) => item.details?.requirementId === req.id)
181
+ .filter((item) => item.details?.terminalType === "PUBLICATION");
182
+ const latestEvidence = latestAuthoritativeCheck(matchingEvidence);
183
+ const evidenceStatus = latestEvidence?.details?.terminalStatus;
184
+ const evidenceSatisfied = isPublicationStatusSatisfied(evidenceStatus, requiredLevel);
185
+ const globalSatisfied = isPublicationStatusSatisfied(pubStatus, requiredLevel);
186
+
187
+ if (globalSatisfied && evidenceSatisfied) {
188
+ result.covered.push(req);
189
+ } else {
190
+ result.pending.push(req);
191
+ result.errors.push({
192
+ code: "E_PUBLICATION_REQUIREMENT_PENDING",
193
+ message: `The contract explicitly requires publication status '${requiredLevel}', but publication evidence/status is insufficient for requirement: ${req.text}`,
194
+ requirementId: req.id,
195
+ });
196
+ }
197
+ } else if (req.type === "PRODUCTION_READINESS") {
198
+ const matchingEvidence = receiptEvidence
199
+ .filter((item) => item.kind === "OBSERVED")
200
+ .filter((item) => item.details?.requirementId === req.id)
201
+ .filter((item) => item.details?.terminalType === "PRODUCTION_READINESS");
202
+ const latestEvidence = latestAuthoritativeCheck(matchingEvidence);
203
+ const evidenceStatus = latestEvidence?.details?.terminalStatus;
204
+ const evidenceSatisfied = evidenceStatus === "ready" || evidenceStatus === "verified";
205
+ const globalSatisfied = prodStatus === "ready" || prodStatus === "verified";
206
+
207
+ if (globalSatisfied && evidenceSatisfied) {
208
+ result.covered.push(req);
209
+ } else {
210
+ result.pending.push(req);
211
+ result.errors.push({
212
+ code: "E_PRODUCTION_REQUIREMENT_PENDING",
213
+ message: `The contract explicitly requires production readiness, but production readiness evidence/status is insufficient for requirement: ${req.text}`,
214
+ requirementId: req.id,
215
+ });
216
+ }
217
+ }
218
+ }
219
+ return result;
220
+ }
221
+
222
+ export function matchesRequirement(check, requirement) {
223
+ return check?.requirement === requirement.id || check?.requirement === requirement.text;
224
+ }
225
+
226
+ export function latestAuthoritativeCheck(candidates) {
227
+ if (!candidates || candidates.length === 0) return null;
228
+ return [...candidates].sort((a, b) => {
229
+ const cycleA = a.details?.verificationCycle ?? a.verificationCycle ?? 1;
230
+ const cycleB = b.details?.verificationCycle ?? b.verificationCycle ?? 1;
231
+ if (cycleA !== cycleB) return cycleA - cycleB;
232
+ const timeA = typeof a.timestamp === "string" ? a.timestamp : "";
233
+ const timeB = typeof b.timestamp === "string" ? b.timestamp : "";
234
+ if (timeA !== timeB) return timeA.localeCompare(timeB);
235
+ return 0;
236
+ }).at(-1);
237
+ }
238
+
239
+ export function authoritativeChecksForRequirements({ requirements = [], checks = [] } = {}) {
240
+ return normalizeRequirements(requirements).map((requirement) => {
241
+ const candidates = checks.filter((check) => matchesRequirement(check, requirement));
242
+ return {
243
+ requirement,
244
+ check: latestAuthoritativeCheck(candidates),
245
+ };
246
+ });
247
+ }
248
+
249
+ function componentStatus(check, requirement, allChecks = []) {
250
+ if (requirement.operator !== "ALL" || !requirement.requirements?.length) return null;
251
+ const components = check?.details?.components;
252
+ const statuses = requirement.requirements.map((child) => {
253
+ if (Array.isArray(components)) {
254
+ const matchingComp = components.filter((item) => (
255
+ item?.requirementId === child.id || item?.requirement === child.text
256
+ )).at(-1);
257
+ if (matchingComp) return matchingComp;
258
+ }
259
+ const childCandidates = allChecks.filter((candidate) => matchesRequirement(candidate, child));
260
+ return latestAuthoritativeCheck(childCandidates);
261
+ });
262
+ if (statuses.some((item) => !item)) return "MISSING";
263
+ if (statuses.some((item) => item.status === "failed")) return "INVALID";
264
+ if (statuses.some((item) => item.status !== "passed" || item.evidenceKind !== "OBSERVED")) return "PARTIAL";
265
+ return "COVERED";
266
+ }
267
+
268
+ export function evaluateRequiredEvidence({ requirements = [], checks = [] } = {}) {
269
+ const normalized = normalizeRequirements(requirements);
270
+ const result = {
271
+ ready: true,
272
+ required: normalized,
273
+ covered: [],
274
+ missing: [],
275
+ partial: [],
276
+ invalid: [],
277
+ lifecyclePending: [],
278
+ reasonCodes: [],
279
+ };
280
+ for (const requirement of normalized) {
281
+ if (requirement.mixedTerminal) {
282
+ result.invalid.push({
283
+ ...requirement,
284
+ reasonCode: "E_CIRCULAR_COMPLETION_REQUIREMENT",
285
+ });
286
+ continue;
287
+ }
288
+ if (requirement.terminalOwned || requirement.lifecycleOwned) {
289
+ result.lifecyclePending.push(requirement);
290
+ continue;
291
+ }
292
+ const candidates = checks.filter((check) => matchesRequirement(check, requirement));
293
+ const check = latestAuthoritativeCheck(candidates);
294
+ const compound = componentStatus(check, requirement, checks);
295
+ if (compound === "INVALID" || check?.status === "failed") {
296
+ result.invalid.push(requirement);
297
+ } else if (compound === "PARTIAL") {
298
+ result.partial.push(requirement);
299
+ } else if (compound === "MISSING") {
300
+ result.missing.push(requirement);
301
+ } else if (
302
+ (check?.status === "passed" || compound === "COVERED")
303
+ && (requirement.requiredEvidenceKind !== "OBSERVED" || check?.evidenceKind === "OBSERVED" || compound === "COVERED")
304
+ && (compound === null || compound === "COVERED")
305
+ ) {
306
+ result.covered.push(requirement);
307
+ } else if (check?.status === "passed" && check.evidenceKind !== "OBSERVED") {
308
+ result.invalid.push({ ...requirement, reasonCode: "E_EVIDENCE_KIND_INVALID" });
309
+ } else if (check) {
310
+ result.partial.push(requirement);
311
+ } else {
312
+ result.missing.push(requirement);
313
+ }
314
+ }
315
+ result.ready = result.missing.length === 0 && result.partial.length === 0 && result.invalid.length === 0;
316
+ if (result.invalid.length) {
317
+ const specificCodes = result.invalid
318
+ .map((item) => item.reasonCode)
319
+ .filter(Boolean);
320
+ if (specificCodes.length > 0) {
321
+ result.reasonCodes.push(...new Set(specificCodes));
322
+ } else {
323
+ result.reasonCodes.push("E_EVIDENCE_INVALID");
324
+ }
325
+ }
326
+ if (result.missing.length) result.reasonCodes.push("E_EVIDENCE_REQUIRED");
327
+ if (result.partial.length) result.reasonCodes.push("E_EVIDENCE_PARTIAL");
328
+ const leaves = ordinaryLeafRequirements(normalized);
329
+ const authoritativeLeaves = authoritativeChecksForRequirements({ requirements: leaves, checks });
330
+ result.authoritativeFailures = authoritativeLeaves.filter(({ check }) => check?.status === "failed");
331
+ result.authoritativeBlocked = authoritativeLeaves.filter(({ check }) => check?.status === "blocked");
332
+ return result;
333
+ }
@@ -19,10 +19,22 @@ function assertNonEmptyString(value, label) {
19
19
  }
20
20
  }
21
21
 
22
- export function createEvidence(input, source, result, details) {
22
+ export function createEvidence(input, source, result, details, cycle) {
23
23
  const value = typeof input === "string"
24
- ? { kind: input, source, result, ...(details === undefined ? {} : { details }) }
25
- : input;
24
+ ? {
25
+ kind: input,
26
+ source,
27
+ result,
28
+ ...(cycle !== undefined ? { verificationCycle: cycle } : {}),
29
+ details: {
30
+ ...(details === undefined ? {} : details),
31
+ ...(cycle !== undefined ? { verificationCycle: cycle } : {}),
32
+ },
33
+ }
34
+ : structuredClone(input);
35
+ if (value.details && typeof value.details === "object" && value.verificationCycle !== undefined) {
36
+ value.details.verificationCycle = value.verificationCycle;
37
+ }
26
38
  assertEvidence(value);
27
39
  return value;
28
40
  }
@@ -36,6 +48,9 @@ export function assertEvidence(value, label = "evidence") {
36
48
  }
37
49
  assertNonEmptyString(value.source, `${label}.source`);
38
50
  assertNonEmptyString(value.result, `${label}.result`);
51
+ if (value.verificationCycle !== undefined && (!Number.isInteger(value.verificationCycle) || value.verificationCycle < 1)) {
52
+ throw new EvidenceError(`${label}.verificationCycle must be a positive integer`);
53
+ }
39
54
  return value;
40
55
  }
41
56
 
@@ -50,7 +65,10 @@ export function evidenceMatches(evidence, terms) {
50
65
  .filter((term) => typeof term === "string" && term.length > 0)
51
66
  .map((term) => term.toLowerCase());
52
67
  return (evidence ?? []).some((item) => {
53
- const haystack = `${item.source} ${item.result}`.toLowerCase();
68
+ const detailsText = item.details
69
+ ? `${item.details.requirementText ?? ""} ${String(item.details.terminalType ?? "").replace(/_/g, " ")}`
70
+ : "";
71
+ const haystack = `${item.source} ${item.result} ${detailsText}`.toLowerCase();
54
72
  return normalizedTerms.some((term) => haystack.includes(term));
55
73
  });
56
74
  }
@@ -1,6 +1,6 @@
1
1
  import { ARTIFACT_PATHS, readJsonArtifact } from "./artifacts.js";
2
2
  import { readContract } from "./contract.js";
3
- import { validateEventLedger } from "./events.js";
3
+ import { validateEventLedger, validateStateLedgerCoherence } from "./events.js";
4
4
  import { evaluatePreflight, validatePersistedPreflight } from "./preflight.js";
5
5
  import { readPersistedRoute } from "./route-artifact.js";
6
6
  import { stateIdentityErrors } from "./completion-relationships.js";
@@ -195,6 +195,11 @@ export async function evaluateStartExecutionPrerequisites({ target, state, packa
195
195
 
196
196
  const ledger = await validateEventLedger(target, packageRoot);
197
197
  errors.push(...prerequisiteLedgerErrors(ledger, contract.value.taskId, preflight, route));
198
+ errors.push(...validateStateLedgerCoherence(state, ledger.events).map((error) => issue(
199
+ error.code,
200
+ error.message,
201
+ [ARTIFACT_PATHS.state, ARTIFACT_PATHS.events],
202
+ )));
198
203
  errors.push(...persistedPreflightErrors);
199
204
  return { errors, requiredArtifacts, contract, route, preflight, persistedPreflight, ledger };
200
205
  }
@@ -1,5 +1,5 @@
1
1
  import { fileExists, ensureWithin, readBytes } from "./filesystem.js";
2
- import { AGENT_SUPPORT } from "./agent-support.js";
2
+ import { DISCOVERY_SURFACES } from "./discovery-surfaces.js";
3
3
  import { readManifest } from "./manifest.js";
4
4
  import { PROTOCOL_VERSION } from "./protocol.js";
5
5
  import { inspectSchemaHealth } from "./schema-validation.js";
@@ -42,15 +42,14 @@ export async function inspectTarget({ target, packageRoot, contractFile = null }
42
42
  ? `${FORGELOOP_KIT_DIR}/schemas`
43
43
  : "schemas";
44
44
  const doctor = await runDoctor({ target, packageRoot });
45
- const agents = await Promise.all(AGENT_SUPPORT.map(async (record) => ({
46
- id: record.id,
47
- name: record.name,
48
- support: record.support,
49
- instructionFiles: record.instructionFiles,
50
- available: (await Promise.all(
51
- record.instructionFiles.map(async (relativePath) => fileExists(ensureWithin(target, relativePath))),
52
- )).some(Boolean),
45
+ const surfaces = await Promise.all(DISCOVERY_SURFACES.map(async (surface) => ({
46
+ id: surface.id,
47
+ path: surface.path,
48
+ kind: surface.kind,
49
+ available: await fileExists(ensureWithin(target, surface.path)),
53
50
  })));
51
+ const availableSurfaces = surfaces.filter((surface) => surface.available);
52
+ const protocolActivated = availableSurfaces.length > 0;
54
53
 
55
54
  const findings = [...doctor.findings];
56
55
  for (const schema of schemaHealth.schemas) {
@@ -100,9 +99,20 @@ export async function inspectTarget({ target, packageRoot, contractFile = null }
100
99
  error: manifestError,
101
100
  },
102
101
  profile,
102
+ integration: {
103
+ protocolActivated,
104
+ protocolMarker: "FORGELOOP_PROJECT_PROTOCOL=REQUIRED",
105
+ discovery: {
106
+ status: protocolActivated ? "INSTRUCTION_DISCOVERED" : "INSTRUCTION_ABSENT",
107
+ surfaces,
108
+ },
109
+ capability: {
110
+ status: "NOT_VERIFIED",
111
+ },
112
+ },
103
113
  adapters: {
104
- detected: agents.filter((agent) => agent.available).map((agent) => agent.id),
105
- agents,
114
+ detected: availableSurfaces.map((s) => s.path),
115
+ surfaces,
106
116
  },
107
117
  protocol: {
108
118
  version: PROTOCOL_VERSION,
@@ -112,7 +122,8 @@ export async function inspectTarget({ target, packageRoot, contractFile = null }
112
122
  },
113
123
  state: { ...state, path: WORK_STATE_PATH, present: statePresent },
114
124
  compatibility: {
115
- agents: AGENT_SUPPORT.map((record) => record.id),
125
+ deprecated: true,
126
+ agents: [],
116
127
  },
117
128
  findings,
118
129
  evidence,
@@ -15,13 +15,40 @@ export function nativeShimReferences(relativePath) {
15
15
  const prefix = nativeShimPrefix(relativePath);
16
16
  return [
17
17
  `${prefix}/LOOP_ENGINEERING.md`,
18
- `${prefix}/AGENT_COMPATIBILITY.md`,
18
+ `${prefix}/PROTOCOL_INTEGRATION.md`,
19
19
  ];
20
20
  }
21
21
 
22
22
  export function nativeShim(relativePath) {
23
23
  const kitPrefix = nativeShimPrefix(relativePath);
24
- return `# ForgeLoop native adapter\n\nRead and follow the canonical ForgeLoop protocol in ${kitPrefix}/LOOP_ENGINEERING.md and ${kitPrefix}/AGENT_COMPATIBILITY.md.\nThe canonical guides and schemas are under ${kitPrefix}/; keep this adapter concise and preserve any host-specific instructions.\n`;
24
+ return `<!-- FORGELOOP_PROJECT_PROTOCOL=REQUIRED -->
25
+ # ForgeLoop Project Protocol
26
+
27
+ This project is ForgeLoop-enabled.
28
+
29
+ If your execution environment loaded this file, ForgeLoop applies to work
30
+ performed in this project regardless of model, provider, product, IDE,
31
+ agent name, runtime name, or orchestration environment.
32
+
33
+ Do not treat ForgeLoop as vendor-specific, harness-specific, optional,
34
+ advisory, or something to follow only "in spirit".
35
+
36
+ Read and follow:
37
+ - ${kitPrefix}/LOOP_ENGINEERING.md
38
+ - ${kitPrefix}/PROTOCOL_INTEGRATION.md
39
+
40
+ Before changing product or executable files, establish the ForgeLoop contract,
41
+ route, required gates, and READY preflight.
42
+
43
+ Use the project-local ForgeLoop CLI for lifecycle-owned protocol state.
44
+ Never manually synthesize lifecycle chronology or assign ForgeLoop COMPLETE.
45
+
46
+ Before claiming ForgeLoop-verified completion, require
47
+ \`forgeloop complete\` to return \`VALID\`.
48
+
49
+ If a required capability is unavailable, report that ForgeLoop dimension as
50
+ not verified rather than simulating it.
51
+ `;
25
52
  }
26
53
 
27
54
  export function resolveNativeReference(relativePath, reference) {
@@ -45,6 +72,7 @@ export function inspectNativeAdapter(relativePath, bytes) {
45
72
  missingReferences,
46
73
  resolvedReferences,
47
74
  hasForgeLoopMarker: /forgeloop/i.test(text),
75
+ hasProtocolMarker: text.includes("FORGELOOP_PROJECT_PROTOCOL=REQUIRED"),
48
76
  };
49
77
  }
50
78