@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
@@ -1,5 +1,5 @@
1
1
  import { ARTIFACT_PATHS, readJsonArtifact } from "./artifacts.js";
2
- import { taskArtifactPath } from "./task-paths.js";
2
+ import { taskArtifactPath, taskStructuralQualityDirectory } from "./task-paths.js";
3
3
  import { completionIdentityErrors, evaluateCompletion } from "./completion.js";
4
4
  import { readContract } from "./contract.js";
5
5
  import { evaluatePreflight, validatePersistedPreflight } from "./preflight.js";
@@ -20,6 +20,7 @@ import { listActions } from "./actions.js";
20
20
  import { listApprovals } from "./approvals.js";
21
21
  import { loadPolicyIdentity } from "./policy-engine.js";
22
22
  import { evaluateContinuityNextAction } from "./next-action-continuity.js";
23
+ import { projectStructuralQualityStatus } from "./structural-quality/service.js";
23
24
 
24
25
  export const PHASES_REQUIRING_EXECUTION_CHRONOLOGY = new Set([
25
26
  "EXECUTING",
@@ -34,6 +35,24 @@ export function phaseRequiresExecutionChronology(phase) {
34
35
  return PHASES_REQUIRING_EXECUTION_CHRONOLOGY.has(phase);
35
36
  }
36
37
 
38
+ function structuralQualityOptionalActions(quality, taskId) {
39
+ if (quality?.mode !== "observe") return [];
40
+ if (quality.baseline?.status !== "OBSERVED") {
41
+ return [{
42
+ action: NEXT_ACTIONS.CAPTURE_STRUCTURAL_QUALITY_BASELINE,
43
+ command: `forgeloop quality-baseline --task ${taskId} --json`,
44
+ }];
45
+ }
46
+ if (quality.freshness === "STALE" || quality.current?.verificationCycle === null
47
+ || quality.current?.verificationCycle === undefined) {
48
+ return [{
49
+ action: NEXT_ACTIONS.VERIFY_STRUCTURAL_QUALITY,
50
+ command: `forgeloop quality-verify --task ${taskId} --json`,
51
+ }];
52
+ }
53
+ return [];
54
+ }
55
+
37
56
  export async function resolveNextActionPhase({
38
57
  target,
39
58
  packageRoot,
@@ -434,7 +453,7 @@ export async function resolveNextActionPhase({
434
453
  let executionPrerequisites = null;
435
454
  if (phaseNeedsChronology) {
436
455
  try {
437
- executionPrerequisites = await evaluateStartExecutionPrerequisites({ target, state, packageRoot, taskId: explicitTaskId });
456
+ executionPrerequisites = await evaluateStartExecutionPrerequisites({ target, state, packageRoot, taskId: explicitTaskId, runtimeContext });
438
457
  } catch (error) {
439
458
  return result({
440
459
  ...context,
@@ -506,7 +525,17 @@ export async function resolveNextActionPhase({
506
525
  return decision(context, NEXT_ACTIONS.PLAN, artifactError("PHASE_DESIGNING", "Required gates are ready for planning"));
507
526
  }
508
527
  if (state.phase === "PLANNED") {
509
- const prerequisites = await evaluateStartExecutionPrerequisites({ target, state, packageRoot, taskId: explicitTaskId });
528
+ const quality = await projectStructuralQualityStatus({ target, packageRoot, taskId: explicitTaskId ?? state.taskId, runtimeContext });
529
+ if (quality.mode === "gate" && quality.baseline.status !== "OBSERVED") {
530
+ return result({
531
+ ...context,
532
+ nextAction: NEXT_ACTIONS.CAPTURE_STRUCTURAL_QUALITY_BASELINE,
533
+ commands: [commandFor(NEXT_ACTIONS.CAPTURE_STRUCTURAL_QUALITY_BASELINE).replace("<id>", state.taskId)],
534
+ reasons: [artifactError("E_STRUCTURAL_QUALITY_BASELINE_MISSING", "Gate mode requires a structural-quality baseline before execution", [quality.baseline.artifactRef ?? taskArtifactPath(state.taskId, "structuralQuality")])],
535
+ requiredArtifacts: [taskArtifactPath(state.taskId, "structuralQuality")],
536
+ });
537
+ }
538
+ const prerequisites = await evaluateStartExecutionPrerequisites({ target, state, packageRoot, taskId: explicitTaskId, runtimeContext });
510
539
  if (prerequisites.errors.length > 0) {
511
540
  const preflightOnly = prerequisites.errors.every((error) => error.code.startsWith("E_PREFLIGHT_")
512
541
  || (error.code === "E_PHASE_CHRONOLOGY_INVALID"
@@ -530,7 +559,14 @@ export async function resolveNextActionPhase({
530
559
  missingArtifacts: preflightArtifact.missingArtifacts,
531
560
  });
532
561
  }
533
- return decision(context, NEXT_ACTIONS.START_EXECUTION, artifactError("PHASE_PLANNED", "The persisted preflight is READY"));
562
+ return decision(
563
+ context,
564
+ NEXT_ACTIONS.START_EXECUTION,
565
+ artifactError("PHASE_PLANNED", "The persisted preflight is READY"),
566
+ [],
567
+ [],
568
+ structuralQualityOptionalActions(quality, state.taskId),
569
+ );
534
570
  }
535
571
  if (state.phase === "EXECUTING") {
536
572
  const continuityAction = await evaluateContinuityNextAction({ target, packageRoot, context });
@@ -538,6 +574,41 @@ export async function resolveNextActionPhase({
538
574
  return decision(context, NEXT_ACTIONS.ENTER_VERIFYING, artifactError("PHASE_EXECUTING", "Execution is complete enough to enter verification"));
539
575
  }
540
576
  if (state.phase === "VERIFYING") {
577
+ const quality = await projectStructuralQualityStatus({ target, packageRoot, taskId: explicitTaskId ?? state.taskId, runtimeContext });
578
+ if (quality.mode === "gate") {
579
+ if (quality.baseline.status !== "OBSERVED") {
580
+ return result({
581
+ ...context,
582
+ nextAction: NEXT_ACTIONS.RESOLVE_STRUCTURAL_QUALITY_BLOCKER,
583
+ reasons: [artifactError("E_STRUCTURAL_QUALITY_BASELINE_MISSING", "Structural-quality gate cannot verify without its immutable baseline", [taskArtifactPath(state.taskId, "structuralQuality")])],
584
+ requiredArtifacts: [taskArtifactPath(state.taskId, "structuralQuality")],
585
+ });
586
+ }
587
+ if (!quality.current.artifactRef || quality.current.verificationCycle !== (state.verificationCycle ?? 1)) {
588
+ return result({
589
+ ...context,
590
+ nextAction: NEXT_ACTIONS.VERIFY_STRUCTURAL_QUALITY,
591
+ commands: [commandFor(NEXT_ACTIONS.VERIFY_STRUCTURAL_QUALITY).replace("<id>", state.taskId)],
592
+ reasons: [artifactError("E_STRUCTURAL_QUALITY_EVIDENCE_STALE", "The current verification cycle has no structural-quality evaluation", [taskStructuralQualityDirectory(state.taskId)])],
593
+ requiredArtifacts: [taskStructuralQualityDirectory(state.taskId)],
594
+ });
595
+ }
596
+ if (quality.freshness === "STALE") {
597
+ return result({
598
+ ...context,
599
+ nextAction: NEXT_ACTIONS.VERIFY_STRUCTURAL_QUALITY,
600
+ commands: [commandFor(NEXT_ACTIONS.VERIFY_STRUCTURAL_QUALITY).replace("<id>", state.taskId)],
601
+ reasons: [artifactError("E_STRUCTURAL_QUALITY_EVIDENCE_STALE", "Structural-quality evidence is stale; fresh verification is required before review", [quality.current.artifactRef])],
602
+ requiredArtifacts: [taskStructuralQualityDirectory(state.taskId)],
603
+ });
604
+ }
605
+ if (quality.current.status === "FAIL") {
606
+ return decision(context, NEXT_ACTIONS.DIAGNOSE_STRUCTURAL_QUALITY_REGRESSION, artifactError("E_STRUCTURAL_QUALITY_REGRESSION", "Structural-quality verification detected a regression", [quality.current.artifactRef]));
607
+ }
608
+ if (quality.current.status === "BLOCKED") {
609
+ return decision(context, NEXT_ACTIONS.RESOLVE_STRUCTURAL_QUALITY_BLOCKER, artifactError("E_STRUCTURAL_QUALITY_EVALUATION_INCOMPARABLE", "Structural-quality verification is blocked", [quality.current.artifactRef]));
610
+ }
611
+ }
541
612
  const invalidChecks = checkListReasons(state);
542
613
  if (invalidChecks.length > 0) {
543
614
  return result({
@@ -655,7 +726,14 @@ export async function resolveNextActionPhase({
655
726
  });
656
727
  }
657
728
  if (readiness.ready) {
658
- return decision(context, NEXT_ACTIONS.ENTER_REVIEWING, artifactError("EVIDENCE_COVERED", "All required observed verification evidence is covered"));
729
+ return decision(
730
+ context,
731
+ NEXT_ACTIONS.ENTER_REVIEWING,
732
+ artifactError("EVIDENCE_COVERED", "All required observed verification evidence is covered"),
733
+ [],
734
+ [],
735
+ structuralQualityOptionalActions(quality, state.taskId),
736
+ );
659
737
  }
660
738
  const uncovered = [...readiness.invalid, ...readiness.partial, ...readiness.missing];
661
739
  return result({
@@ -674,6 +752,7 @@ export async function resolveNextActionPhase({
674
752
  [stateRel],
675
753
  )),
676
754
  requiredArtifacts: requiredArtifacts,
755
+ optionalActions: structuralQualityOptionalActions(quality, state.taskId),
677
756
  });
678
757
  }
679
758
  if (state.phase === "DIAGNOSING") {
package/src/core/phase.js CHANGED
@@ -26,6 +26,7 @@ import {
26
26
  buildInformationGainProjection,
27
27
  evaluateStructuredDiagnosticStall,
28
28
  } from "./information-gain-projection.js";
29
+ import { assertStructuralQualityExecutionReady } from "./structural-quality/service.js";
29
30
 
30
31
  function phaseError(code, message, artifacts = []) {
31
32
  const error = new Error(message);
@@ -134,9 +135,16 @@ async function assertPhasePrerequisites(target, state, toPhase, packageRoot, aut
134
135
  throw phaseError("E_PHASE_PREREQUISITE_MISSING", `Phase ${toPhase} requires ${routeRel}: ${error.message}`, [routeRel]);
135
136
  }
136
137
  }
138
+ if (toPhase === "EXECUTING" && scopedTaskId) {
139
+ try {
140
+ await assertStructuralQualityExecutionReady({ target, packageRoot, taskId: scopedTaskId, runtimeContext });
141
+ } catch (error) {
142
+ throw phaseError(error.code, error.message, error.artifacts);
143
+ }
144
+ }
137
145
  if (hasExecutionStarted(toPhase)) {
138
146
  try {
139
- await assertExecutionPrerequisites({ target, state, packageRoot, ...options, taskId: scopedTaskId });
147
+ await assertExecutionPrerequisites({ target, state, packageRoot, ...options, taskId: scopedTaskId, runtimeContext });
140
148
  } catch (error) {
141
149
  throw phaseError(error.code, error.message, error.artifacts);
142
150
  }
@@ -0,0 +1,103 @@
1
+ import { assertJsonLimits } from "./json-safety.js";
2
+ import { assertSecretFree } from "./receipt.js";
3
+ import { E_PORTABLE_CONTEXT_INVALID } from "./error-codes.js";
4
+
5
+ export class PortableContextError extends Error {
6
+ constructor(message, { code = E_PORTABLE_CONTEXT_INVALID, cause } = {}) {
7
+ super(message, cause !== undefined ? { cause } : undefined);
8
+ this.name = "PortableContextError";
9
+ this.code = code;
10
+ }
11
+ }
12
+
13
+ const PORTABLE_SECRET_PATTERNS = [
14
+ /-----BEGIN [A-Z ]*PRIVATE KEY-----/i,
15
+ /(?:^|\s)(?:sk|ghp|glpat|xox[baprs])-[-_a-z0-9]{8,}/i,
16
+ /(?:AKIA|ASIA)[A-Z0-9]{12,}/,
17
+ /(?:bearer\s+[-_a-z0-9\.]{4,})/i,
18
+ /(?:authorization:\s*bearer)/i,
19
+ ];
20
+
21
+ function containsSecretPattern(str) {
22
+ return PORTABLE_SECRET_PATTERNS.some((pattern) => pattern.test(str));
23
+ }
24
+
25
+ function checkObjectForSecrets(value, location = "$") {
26
+ if (typeof value === "string") {
27
+ if (containsSecretPattern(value)) {
28
+ throw new Error(`${location}: secret-like value is not allowed`);
29
+ }
30
+ return;
31
+ }
32
+ if (!value || typeof value !== "object") return;
33
+ if (Array.isArray(value)) {
34
+ value.forEach((item, index) => checkObjectForSecrets(item, `${location}[${index}]`));
35
+ } else {
36
+ for (const [key, child] of Object.entries(value)) {
37
+ checkObjectForSecrets(child, `${location}.${key}`);
38
+ }
39
+ }
40
+ }
41
+
42
+ export function normalizePortableText(
43
+ value,
44
+ {
45
+ label = "portable text",
46
+ maxLength,
47
+ optional = false,
48
+ } = {},
49
+ ) {
50
+ if (value === undefined || value === null) {
51
+ if (optional) return null;
52
+ throw new PortableContextError(`${label} is required`);
53
+ }
54
+
55
+ if (typeof value !== "string" || value.trim() === "") {
56
+ throw new PortableContextError(`${label} must be a non-empty string`);
57
+ }
58
+
59
+ if (typeof maxLength === "number" && value.length > maxLength) {
60
+ throw new PortableContextError(`${label} exceeds the ${maxLength}-character limit`);
61
+ }
62
+
63
+ if (/\p{Cc}/u.test(value)) {
64
+ throw new PortableContextError(`${label} contains control characters`);
65
+ }
66
+
67
+ return value;
68
+ }
69
+
70
+ export function assertPortableContextSafe(
71
+ value,
72
+ {
73
+ label = "portable context",
74
+ } = {},
75
+ ) {
76
+ try {
77
+ assertJsonLimits(value, label);
78
+ assertSecretFree(value);
79
+ checkObjectForSecrets(value, label);
80
+ return value;
81
+ } catch (error) {
82
+ if (error.code === E_PORTABLE_CONTEXT_INVALID) {
83
+ throw error;
84
+ }
85
+ throw new PortableContextError(
86
+ `${label} failed safety verification: ${error.message}`,
87
+ { code: E_PORTABLE_CONTEXT_INVALID, cause: error },
88
+ );
89
+ }
90
+ }
91
+
92
+ export function deepFreeze(object) {
93
+ if (object === null || typeof object !== "object" || Object.isFrozen(object)) {
94
+ return object;
95
+ }
96
+ Object.freeze(object);
97
+ for (const value of Object.values(object)) {
98
+ if (value !== null && typeof value === "object") {
99
+ deepFreeze(value);
100
+ }
101
+ }
102
+ return object;
103
+ }
@@ -28,6 +28,7 @@ import { PROFILE_PATH } from "./target-layout.js";
28
28
  import { readPersistedRoute } from "./route-artifact.js";
29
29
  import { validateEventLedger } from "./events.js";
30
30
  import { PROJECT_ARTIFACT_PATHS, taskArtifactPath } from "./task-paths.js";
31
+ import { normalizeStructuralQualityConfig } from "./structural-quality/policy.js";
31
32
 
32
33
  const PREVIEW_DECISION_LIMIT = 10;
33
34
  const PREVIEW_DECISION_MAX_LENGTH = 240;
@@ -42,6 +43,17 @@ export async function evaluatePreflight({ target, packageRoot, strict = false, t
42
43
  const contract = await loadContract(target, packageRoot, errors, { taskId, contractPath });
43
44
  const route = await loadRoute(target, packageRoot, errors, { taskId, routePath });
44
45
  const config = await optionalConfig(target, packageRoot, errors);
46
+ if (config.structuralQuality !== undefined) {
47
+ try {
48
+ normalizeStructuralQualityConfig(config.structuralQuality);
49
+ } catch (error) {
50
+ errors.push(issue(
51
+ error.code ?? "E_STRUCTURAL_QUALITY_CONFIGURATION_INVALID",
52
+ error.message,
53
+ [PROJECT_ARTIFACT_PATHS.config],
54
+ ));
55
+ }
56
+ }
45
57
  const effectiveStrict = strict || config.complianceMode === "strict";
46
58
  if (effectiveStrict && profile.status !== "verified") {
47
59
  errors.push(issue("E_PROFILE_UNVERIFIED", "Strict preflight requires a verified project profile", [PROFILE_PATH]));
@@ -111,6 +123,26 @@ export async function evaluatePreflight({ target, packageRoot, strict = false, t
111
123
 
112
124
  const sortedErrors = sortIssues(errors);
113
125
  const effectiveTaskId = taskId ?? contract?.value?.taskId ?? state?.taskId ?? "unknown";
126
+ let structuralQuality = {
127
+ mode: config.structuralQuality?.mode ?? "off",
128
+ provider: config.structuralQuality?.mode && config.structuralQuality.mode !== "off"
129
+ ? config.structuralQuality.provider ?? "sentrux"
130
+ : null,
131
+ baseline: { status: config.structuralQuality?.mode && config.structuralQuality.mode !== "off" ? "MISSING" : "NOT_REQUESTED" },
132
+ current: { status: "NOT_OBSERVED" },
133
+ comparable: null,
134
+ completionRequired: config.structuralQuality?.mode === "gate",
135
+ reasonCodes: [],
136
+ next: config.structuralQuality?.mode === "gate" ? "CAPTURE_STRUCTURAL_QUALITY_BASELINE" : null,
137
+ };
138
+ if (effectiveTaskId !== "unknown") {
139
+ try {
140
+ const { projectStructuralQualityStatus } = await import("./structural-quality/service.js");
141
+ structuralQuality = await projectStructuralQualityStatus({ target, packageRoot, taskId: effectiveTaskId });
142
+ } catch (error) {
143
+ structuralQuality.reasonCodes = [error.code ?? "E_STRUCTURAL_QUALITY_EVIDENCE_STALE"];
144
+ }
145
+ }
114
146
  return {
115
147
  schemaVersion: 1,
116
148
  protocolVersion: 1,
@@ -140,6 +172,7 @@ export async function evaluatePreflight({ target, packageRoot, strict = false, t
140
172
  },
141
173
  } : {}),
142
174
  ...(sources ? { sources: { status: "valid", fingerprint: null } } : {}),
175
+ structuralQuality,
143
176
  };
144
177
  }
145
178
 
@@ -83,6 +83,21 @@ export function protocolInfo({ packageVersion = null } = {}) {
83
83
  informationGainV2: true,
84
84
  strategyOscillationDetection: true,
85
85
  },
86
+ structuralQuality: {
87
+ version: 1,
88
+ supported: true,
89
+ schemaVersion: 1,
90
+ modes: ["off", "observe", "gate"],
91
+ builtInProviders: ["sentrux"],
92
+ commands: ["quality-baseline", "quality-verify", "quality-status"],
93
+ resource: "task/structural-quality",
94
+ providerNeutral: true,
95
+ defaultProvider: "sentrux",
96
+ transport: "mcp-stdio",
97
+ rootCauses: ["modularity", "acyclicity", "depth", "equality", "redundancy"],
98
+ baselineImmutableAfterExecution: true,
99
+ optimizationMaxExtraEvaluations: 2,
100
+ },
86
101
  durableActions: {
87
102
  version: 1,
88
103
  supported: true,
@@ -145,11 +160,27 @@ export function protocolInfo({ packageVersion = null } = {}) {
145
160
  rebinding: "EXPLICIT_ONLY",
146
161
  },
147
162
  canonicalHandoffs: {
148
- version: 1,
163
+ version: 2,
149
164
  supported: true,
150
165
  immutable: true,
151
- actorControlledIntent: true,
152
166
  lifecycleAuthority: false,
167
+ evidenceAuthority: false,
168
+ exactlyOnceAcceptance: true,
169
+ acceptanceLedgerBacked: true,
170
+ acceptanceCommand: "handoff-accept",
171
+ acceptanceStatuses: ["OPEN", "ACCEPTED", "UNBOUND", "INCONSISTENT"],
172
+ },
173
+ advisoryContextProviders: {
174
+ version: 1,
175
+ supported: true,
176
+ providerNeutral: true,
177
+ integrationApiOnly: true,
178
+ lazy: true,
179
+ optIn: true,
180
+ persistedByForgeLoop: false,
181
+ lifecycleAuthority: false,
182
+ evidenceAuthority: false,
183
+ executable: false,
153
184
  },
154
185
  responsibilityConstraints: {
155
186
  version: 1,
@@ -3,6 +3,9 @@ import {
3
3
  isVerificationExecutionAdapter,
4
4
  normalizeVerificationExecutionPolicy,
5
5
  } from "./verification-execution.js";
6
+ import { STRUCTURAL_QUALITY_PROVIDER_ID_PATTERN } from "./structural-quality/constants.js";
7
+ import { E_ADVISORY_CONTEXT_PROVIDER_INVALID } from "./error-codes.js";
8
+ import { assertAdvisoryContextProviderIdentity } from "./advisory-context/provider.js";
6
9
 
7
10
  export const AUTHORITY_TRUST_MODES = Object.freeze(["NONE", "HOST_ATTESTED"]);
8
11
 
@@ -105,5 +108,60 @@ export function createForgeLoopContext(options = {}) {
105
108
  }
106
109
  context.usageProvider = options.usageProvider;
107
110
  }
111
+ if (options?.structuralQualityProviders !== undefined) {
112
+ const configured = options.structuralQualityProviders instanceof Map
113
+ ? Object.fromEntries(options.structuralQualityProviders.entries())
114
+ : options.structuralQualityProviders;
115
+ if (!configured || typeof configured !== "object" || Array.isArray(configured)) {
116
+ const error = new Error("structuralQualityProviders must be an object or Map");
117
+ error.code = "E_STRUCTURAL_QUALITY_PROVIDER_INVALID";
118
+ throw error;
119
+ }
120
+ const providers = {};
121
+ for (const [id, provider] of Object.entries(configured)) {
122
+ if (!STRUCTURAL_QUALITY_PROVIDER_ID_PATTERN.test(id) || id === "sentrux") {
123
+ const error = new Error(`Invalid or reserved structural-quality provider ID: ${id}`);
124
+ error.code = "E_STRUCTURAL_QUALITY_PROVIDER_INVALID";
125
+ throw error;
126
+ }
127
+ if (typeof provider !== "function"
128
+ && (!provider || typeof provider !== "object" || Array.isArray(provider))) {
129
+ const error = new Error(`Structural-quality provider ${id} must be an object or factory`);
130
+ error.code = "E_STRUCTURAL_QUALITY_PROVIDER_INVALID";
131
+ throw error;
132
+ }
133
+ providers[id] = provider;
134
+ }
135
+ context.structuralQualityProviders = Object.freeze(providers);
136
+ }
137
+ if (options?.advisoryContextProviders !== undefined) {
138
+ const configured = options.advisoryContextProviders instanceof Map
139
+ ? Object.fromEntries(options.advisoryContextProviders.entries())
140
+ : options.advisoryContextProviders;
141
+ if (!configured || typeof configured !== "object" || Array.isArray(configured)) {
142
+ const error = new Error("advisoryContextProviders must be an object or Map");
143
+ error.code = E_ADVISORY_CONTEXT_PROVIDER_INVALID;
144
+ throw error;
145
+ }
146
+ const providers = {};
147
+ for (const [id, provider] of Object.entries(configured)) {
148
+ if (!/^[a-z0-9][a-z0-9_-]*$/.test(id)) {
149
+ const error = new Error(`Invalid advisory-context provider ID: ${id}`);
150
+ error.code = E_ADVISORY_CONTEXT_PROVIDER_INVALID;
151
+ throw error;
152
+ }
153
+ if (typeof provider !== "function"
154
+ && (!provider || typeof provider !== "object" || Array.isArray(provider))) {
155
+ const error = new Error(`Advisory-context provider ${id} must be an object or factory`);
156
+ error.code = E_ADVISORY_CONTEXT_PROVIDER_INVALID;
157
+ throw error;
158
+ }
159
+ if (typeof provider !== "function") {
160
+ assertAdvisoryContextProviderIdentity(provider, id);
161
+ }
162
+ providers[id] = provider;
163
+ }
164
+ context.advisoryContextProviders = Object.freeze(providers);
165
+ }
108
166
  return Object.freeze(context);
109
167
  }
@@ -54,6 +54,7 @@ export const SHIPPED_SCHEMA_NAMES = Object.freeze([
54
54
  "execution-profile-benchmark-scenario",
55
55
  "execution-profile-benchmark-run",
56
56
  "execution-profile-benchmark-aggregate",
57
+ "structural-quality",
57
58
  ]);
58
59
 
59
60
  export class SchemaValidationError extends Error {