@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
@@ -3,6 +3,7 @@ import { ARTIFACT_PATHS, readJsonArtifact, writeJsonArtifact } from "./artifacts
3
3
  import { E_ATTESTATION_CONFIGURATION_INVALID } from "./error-codes.js";
4
4
  import { normalizeVerificationConfiguration } from "./verification-scope-capability.js";
5
5
  import { EXECUTION_PROFILE_REQUESTS } from "./execution-profile.js";
6
+ import { normalizeStructuralQualityConfig } from "./structural-quality/policy.js";
6
7
 
7
8
  export const CONFIG_SCHEMA_VERSION = 1;
8
9
  export const COMPLIANCE_MODES = Object.freeze(["advisory", "standard", "strict"]);
@@ -80,6 +81,7 @@ export function createConfig(input = {}) {
80
81
  }
81
82
  executionProfile = input.executionProfile;
82
83
  }
84
+ const structuralQuality = normalizeStructuralQualityConfig(input.structuralQuality);
83
85
  return {
84
86
  schemaVersion: CONFIG_SCHEMA_VERSION,
85
87
  protocolVersion: PROTOCOL_VERSION,
@@ -90,6 +92,7 @@ export function createConfig(input = {}) {
90
92
  ...(verification ? { verification } : {}),
91
93
  ...(attestation ? { attestation } : {}),
92
94
  ...(executionProfile ? { executionProfile } : {}),
95
+ ...(structuralQuality !== undefined ? { structuralQuality } : {}),
93
96
  };
94
97
  }
95
98
 
@@ -0,0 +1,89 @@
1
+ import { assertSafePath, ensureWithin, fileExists } from "./filesystem.js";
2
+
3
+ function finding(code, severity, field, itemId = null) {
4
+ return { code, severity, field, itemId };
5
+ }
6
+
7
+ function hasOperationalHints(continuity) {
8
+ return Boolean(
9
+ continuity.currentFocus
10
+ || continuity.remainingWork?.length
11
+ || continuity.knownIssues?.length
12
+ || continuity.changedAreas?.length
13
+ || continuity.inspectFirst?.length
14
+ || (typeof continuity.resumeNote === "string" && continuity.resumeNote.trim() !== ""),
15
+ );
16
+ }
17
+
18
+ export async function lintContinuity({ target, continuity, state } = {}) {
19
+ const findings = [];
20
+ if (!continuity || typeof continuity !== "object" || Array.isArray(continuity)) {
21
+ return { status: "PASS", findings };
22
+ }
23
+
24
+ const completedSteps = new Set(
25
+ Array.isArray(state?.completedSteps) ? state.completedSteps : [],
26
+ );
27
+
28
+ for (const [index, item] of (continuity.remainingWork ?? []).entries()) {
29
+ if (!item?.id) continue;
30
+ if (completedSteps.has(item.id)) {
31
+ findings.push(finding(
32
+ "CONTINUITY_REMAINING_ALREADY_COMPLETED",
33
+ "WARN",
34
+ `remainingWork[${index}]`,
35
+ item.id,
36
+ ));
37
+ }
38
+ }
39
+
40
+ if (continuity.currentFocus?.id && completedSteps.has(continuity.currentFocus.id)) {
41
+ findings.push(finding(
42
+ "CONTINUITY_FOCUS_ALREADY_COMPLETED",
43
+ "WARN",
44
+ "currentFocus",
45
+ continuity.currentFocus.id,
46
+ ));
47
+ }
48
+
49
+ const knownIssueIds = new Set(
50
+ (continuity.knownIssues ?? []).map((item) => item?.id).filter(Boolean),
51
+ );
52
+ for (const [index, item] of (continuity.remainingWork ?? []).entries()) {
53
+ if (item?.id && knownIssueIds.has(item.id)) {
54
+ findings.push(finding(
55
+ "CONTINUITY_ITEM_ROLE_CONFLICT",
56
+ "WARN",
57
+ `remainingWork[${index}]`,
58
+ item.id,
59
+ ));
60
+ }
61
+ }
62
+
63
+ if (typeof target === "string" && target.trim() !== "") {
64
+ for (const [index, inspectPath] of (continuity.inspectFirst ?? []).entries()) {
65
+ try {
66
+ await assertSafePath(target, inspectPath);
67
+ if (!(await fileExists(ensureWithin(target, inspectPath)))) {
68
+ findings.push(finding(
69
+ "CONTINUITY_INSPECT_PATH_MISSING",
70
+ "WARN",
71
+ `inspectFirst[${index}]`,
72
+ ));
73
+ }
74
+ } catch {
75
+ // Invalid or unsafe paths are rejected by continuity schema validation;
76
+ // lint never resolves an unchecked path or turns it into authority.
77
+ }
78
+ }
79
+ }
80
+
81
+ if (!hasOperationalHints(continuity)) {
82
+ findings.push(finding("CONTINUITY_EMPTY_HINT_SET", "INFO", "continuity"));
83
+ }
84
+
85
+ return {
86
+ status: findings.some((item) => item.severity === "WARN") ? "WARN" : "PASS",
87
+ findings,
88
+ };
89
+ }
@@ -1,6 +1,7 @@
1
1
  import { canonicalFingerprint } from "./artifacts.js";
2
2
  import { assertContinuitySemantics, readContinuity } from "./continuity.js";
3
3
  import { WORK_TRANSITIONS } from "./protocol.js";
4
+ import { lintContinuity } from "./continuity-lint.js";
4
5
 
5
6
  const RECONCILIATION_CODE = "E_CONTINUITY_RECONCILIATION_REQUIRED";
6
7
 
@@ -188,6 +189,7 @@ export async function reconcileContinuity({ target, packageRoot, taskId = null }
188
189
  path: ".forgeloop/continuity.json",
189
190
  present: false,
190
191
  latestHandoff,
192
+ lint: { status: "PASS", findings: [] },
191
193
  diagnosticContext: await deriveDiagnosticContextSafe({ target, packageRoot, state }),
192
194
  };
193
195
  }
@@ -200,6 +202,15 @@ export async function reconcileContinuity({ target, packageRoot, taskId = null }
200
202
  present: true,
201
203
  error: error.message,
202
204
  latestHandoff,
205
+ lint: {
206
+ status: "WARN",
207
+ findings: [{
208
+ code: error.code ?? "CONTINUITY_INVALID",
209
+ severity: "WARN",
210
+ field: "continuity",
211
+ itemId: null,
212
+ }],
213
+ },
203
214
  };
204
215
  }
205
216
 
@@ -229,6 +240,11 @@ export async function reconcileContinuity({ target, packageRoot, taskId = null }
229
240
  fingerprint: continuityArtifact.fingerprint,
230
241
  continuity: continuityArtifact.value,
231
242
  latestHandoff,
243
+ lint: await lintContinuity({
244
+ target,
245
+ continuity: continuityArtifact.value,
246
+ state,
247
+ }),
232
248
  diagnosticContext: await deriveDiagnosticContextSafe({ target, packageRoot, state }),
233
249
  };
234
250
  }
@@ -9,7 +9,7 @@ import {
9
9
  } from "./artifacts.js";
10
10
  import { assertSafePath, ensureWithin, fileExists } from "./filesystem.js";
11
11
  import { PROTOCOL_VERSION, WORK_PHASES } from "./protocol.js";
12
- import { assertSecretFree } from "./receipt.js";
12
+ import { normalizePortableText, assertPortableContextSafe } from "./portable-context.js";
13
13
  import { getPackageRoot } from "./templates.js";
14
14
  import { taskArtifactPath } from "./task-paths.js";
15
15
 
@@ -37,16 +37,11 @@ function continuityError(code, message, artifacts = [CONTINUITY_PATH]) {
37
37
  }
38
38
 
39
39
  function nonEmptyString(value, label, maxLength) {
40
- if (typeof value !== "string" || value.trim() === "") {
41
- throw continuityError("E_CONTINUITY_INVALID", `${label} must be a non-empty string`);
40
+ try {
41
+ return normalizePortableText(value, { label, maxLength });
42
+ } catch (error) {
43
+ throw continuityError("E_CONTINUITY_INVALID", error.message);
42
44
  }
43
- if (value.length > maxLength) {
44
- throw continuityError("E_CONTINUITY_INVALID", `${label} exceeds the ${maxLength}-character limit`);
45
- }
46
- if (/\p{Cc}/u.test(value)) {
47
- throw continuityError("E_CONTINUITY_INVALID", `${label} contains control characters`);
48
- }
49
- return value;
50
45
  }
51
46
 
52
47
  function fingerprint(value, label) {
@@ -171,7 +166,11 @@ export function assertContinuitySemantics(input) {
171
166
  ? { resumeNote: nonEmptyString(input.resumeNote, "resumeNote", LIMITS.resumeNote) }
172
167
  : {}),
173
168
  };
174
- assertSecretFree(normalized);
169
+ try {
170
+ assertPortableContextSafe(normalized);
171
+ } catch (error) {
172
+ throw continuityError("E_CONTINUITY_INVALID", error.message);
173
+ }
175
174
  return normalized;
176
175
  }
177
176
 
@@ -83,6 +83,70 @@ export const E_EXECUTION_PROFILE_SAFETY_FLOOR_INVALID = "E_EXECUTION_PROFILE_SAF
83
83
  export const E_USAGE_INVALID = "E_USAGE_INVALID";
84
84
  export const E_USAGE_SOURCE_INVALID = "E_USAGE_SOURCE_INVALID";
85
85
  export const E_EFFICIENCY_BASELINE_INVALID = "E_EFFICIENCY_BASELINE_INVALID";
86
+ export const E_STRUCTURAL_QUALITY_CONFIGURATION_INVALID = "E_STRUCTURAL_QUALITY_CONFIGURATION_INVALID";
87
+ export const E_STRUCTURAL_QUALITY_PROVIDER_INVALID = "E_STRUCTURAL_QUALITY_PROVIDER_INVALID";
88
+ export const E_STRUCTURAL_QUALITY_PROVIDER_UNAVAILABLE = "E_STRUCTURAL_QUALITY_PROVIDER_UNAVAILABLE";
89
+ export const E_STRUCTURAL_QUALITY_PROVIDER_VERSION_UNSUPPORTED = "E_STRUCTURAL_QUALITY_PROVIDER_VERSION_UNSUPPORTED";
90
+ export const E_STRUCTURAL_QUALITY_PROVIDER_PROTOCOL_INVALID = "E_STRUCTURAL_QUALITY_PROVIDER_PROTOCOL_INVALID";
91
+ export const E_STRUCTURAL_QUALITY_PROVIDER_TOOL_CONTRACT_INVALID = "E_STRUCTURAL_QUALITY_PROVIDER_TOOL_CONTRACT_INVALID";
92
+ export const E_STRUCTURAL_QUALITY_SCAN_FAILED = "E_STRUCTURAL_QUALITY_SCAN_FAILED";
93
+ export const E_STRUCTURAL_QUALITY_TIMEOUT = "E_STRUCTURAL_QUALITY_TIMEOUT";
94
+ export const E_STRUCTURAL_QUALITY_OUTPUT_LIMIT = "E_STRUCTURAL_QUALITY_OUTPUT_LIMIT";
95
+ export const E_STRUCTURAL_QUALITY_BASELINE_MISSING = "E_STRUCTURAL_QUALITY_BASELINE_MISSING";
96
+ export const E_STRUCTURAL_QUALITY_BASELINE_EXISTS = "E_STRUCTURAL_QUALITY_BASELINE_EXISTS";
97
+ export const E_STRUCTURAL_QUALITY_BASELINE_PHASE_INVALID = "E_STRUCTURAL_QUALITY_BASELINE_PHASE_INVALID";
98
+ export const E_STRUCTURAL_QUALITY_BASELINE_BINDING_MISMATCH = "E_STRUCTURAL_QUALITY_BASELINE_BINDING_MISMATCH";
99
+ export const E_STRUCTURAL_QUALITY_EVALUATION_INCOMPARABLE = "E_STRUCTURAL_QUALITY_EVALUATION_INCOMPARABLE";
100
+ export const E_STRUCTURAL_QUALITY_MEASUREMENT_MODEL_MISMATCH = "E_STRUCTURAL_QUALITY_MEASUREMENT_MODEL_MISMATCH";
101
+ export const E_STRUCTURAL_QUALITY_EVIDENCE_STALE = "E_STRUCTURAL_QUALITY_EVIDENCE_STALE";
102
+ export const E_STRUCTURAL_QUALITY_SOURCE_DRIFT = "E_STRUCTURAL_QUALITY_SOURCE_DRIFT";
103
+ export const E_STRUCTURAL_QUALITY_SOURCE_FINGERPRINT_UNAVAILABLE = "E_STRUCTURAL_QUALITY_SOURCE_FINGERPRINT_UNAVAILABLE";
104
+ export const E_STRUCTURAL_QUALITY_OBSERVATION_EPOCH_STALE = "E_STRUCTURAL_QUALITY_OBSERVATION_EPOCH_STALE";
105
+ export const E_STRUCTURAL_QUALITY_PROJECTION_INCOMPLETE = "E_STRUCTURAL_QUALITY_PROJECTION_INCOMPLETE";
106
+ export const E_STRUCTURAL_QUALITY_REGRESSION = "E_STRUCTURAL_QUALITY_REGRESSION";
107
+
108
+ export const E_ADVISORY_CONTEXT_PROVIDER_INVALID = "E_ADVISORY_CONTEXT_PROVIDER_INVALID";
109
+ export const E_ADVISORY_CONTEXT_PROVIDER_UNAVAILABLE = "E_ADVISORY_CONTEXT_PROVIDER_UNAVAILABLE";
110
+ export const E_ADVISORY_CONTEXT_QUERY_INVALID = "E_ADVISORY_CONTEXT_QUERY_INVALID";
111
+ export const E_ADVISORY_CONTEXT_REQUEST_INVALID = "E_ADVISORY_CONTEXT_REQUEST_INVALID";
112
+ export const E_ADVISORY_CONTEXT_RESULT_INVALID = "E_ADVISORY_CONTEXT_RESULT_INVALID";
113
+ export const E_ADVISORY_CONTEXT_TIMEOUT = "E_ADVISORY_CONTEXT_TIMEOUT";
114
+ export const E_ADVISORY_CONTEXT_OUTPUT_LIMIT = "E_ADVISORY_CONTEXT_OUTPUT_LIMIT";
115
+ export const E_PORTABLE_CONTEXT_INVALID = "E_PORTABLE_CONTEXT_INVALID";
116
+ export const E_HANDOFF_ACCEPTANCE_UNBOUND = "E_HANDOFF_ACCEPTANCE_UNBOUND";
117
+ export const E_HANDOFF_STALE = "E_HANDOFF_STALE";
118
+ export const E_HANDOFF_ALREADY_ACCEPTED = "E_HANDOFF_ALREADY_ACCEPTED";
119
+ export const E_HANDOFF_ACCEPTANCE_INCONSISTENT = "E_HANDOFF_ACCEPTANCE_INCONSISTENT";
120
+
121
+ const STRUCTURAL_QUALITY_ERROR_METADATA = Object.freeze(Object.fromEntries([
122
+ [E_STRUCTURAL_QUALITY_CONFIGURATION_INVALID, "Correct structuralQuality mode, provider ID, budgets, floors, or optimization limits in .forgeloop/config.json."],
123
+ [E_STRUCTURAL_QUALITY_PROVIDER_INVALID, "Use a provider implementing id, detect(input), and scan(input) with the documented normalized boundary."],
124
+ [E_STRUCTURAL_QUALITY_PROVIDER_UNAVAILABLE, "Install or expose the requested provider outside ForgeLoop, or use observe mode and record the limitation; ForgeLoop never auto-installs it."],
125
+ [E_STRUCTURAL_QUALITY_PROVIDER_VERSION_UNSUPPORTED, "Use a verified Sentrux version (0.5.5, 0.5.6, or 0.5.7), or select a compatible provider through trusted runtime context."],
126
+ [E_STRUCTURAL_QUALITY_PROVIDER_PROTOCOL_INVALID, "Repair the provider MCP handshake or response shape; malformed external data cannot become evidence."],
127
+ [E_STRUCTURAL_QUALITY_PROVIDER_TOOL_CONTRACT_INVALID, "Ensure the provider exposes the required scan and health tool argument schemas."],
128
+ [E_STRUCTURAL_QUALITY_SCAN_FAILED, "Inspect the provider failure and rerun quality-baseline or quality-verify after the external analyzer is healthy."],
129
+ [E_STRUCTURAL_QUALITY_TIMEOUT, "Use a responsive provider or a bounded timeout within the supported limit; never promote a timed-out scan."],
130
+ [E_STRUCTURAL_QUALITY_OUTPUT_LIMIT, "Reduce provider output or diagnostics; the 2 MiB combined process-output limit is fail-closed."],
131
+ [E_STRUCTURAL_QUALITY_BASELINE_MISSING, "Run forgeloop quality-baseline --task <id> after PLANNED and before EXECUTING."],
132
+ [E_STRUCTURAL_QUALITY_BASELINE_EXISTS, "Use the existing immutable baseline or request --replace while the task is still before EXECUTING."],
133
+ [E_STRUCTURAL_QUALITY_BASELINE_PHASE_INVALID, "Baseline replacement is forbidden after EXECUTING; repair the current task against its original baseline."],
134
+ [E_STRUCTURAL_QUALITY_BASELINE_BINDING_MISMATCH, "Reconcile contract, route, policy, scope, provider, or rules drift before using the baseline."],
135
+ [E_STRUCTURAL_QUALITY_EVALUATION_INCOMPARABLE, "Restore the baseline provider/version/rules/policy/scope bindings and rerun quality-verify."],
136
+ [E_STRUCTURAL_QUALITY_MEASUREMENT_MODEL_MISMATCH, "Use a compatible measurement model and provider across baseline and evaluation observations."],
137
+ [E_STRUCTURAL_QUALITY_EVIDENCE_STALE, "Rerun quality-verify in the active verification cycle and refresh completion through the canonical receipt pipeline."],
138
+ [E_STRUCTURAL_QUALITY_SOURCE_DRIFT, "Ensure the worktree is not mutated during provider observation and rerun quality-baseline or quality-verify."],
139
+ [E_STRUCTURAL_QUALITY_SOURCE_FINGERPRINT_UNAVAILABLE, "Repair unreadable or unsafe source material before structural-quality evidence can be trusted."],
140
+ [E_STRUCTURAL_QUALITY_OBSERVATION_EPOCH_STALE, "Rerun quality-verify under the active task epoch without concurrent state mutations."],
141
+ [E_STRUCTURAL_QUALITY_PROJECTION_INCOMPLETE, "Rerun quality-verify to reconcile and project the canonical check from the existing evaluation artifact."],
142
+ [E_STRUCTURAL_QUALITY_REGRESSION, "Use the bottleneck and root-cause deltas to record an evidence-backed diagnosis, correct within scope, and verify a new cycle."],
143
+ ].map(([code, safeResolution]) => [code, Object.freeze({
144
+ code,
145
+ category: "structural-quality",
146
+ classification: "PUBLIC_STABLE",
147
+ meaning: "Structural-quality evidence did not satisfy its provider, artifact, comparison, or lifecycle boundary.",
148
+ safeResolution,
149
+ })])));
86
150
 
87
151
  const EXTENSION_PUBLIC_ERROR_CODES = Object.freeze(Object.fromEntries([
88
152
  E_WORKSPACE_IDENTITY_UNAVAILABLE,
@@ -222,11 +286,100 @@ export const E_APPROVAL_ALREADY_RESOLVED = "E_APPROVAL_ALREADY_RESOLVED";
222
286
  export const E_TRAJECTORY_SCENARIO_INVALID = "E_TRAJECTORY_SCENARIO_INVALID";
223
287
  export const E_TRAJECTORY_REFERENCE_REQUIRED = "E_TRAJECTORY_REFERENCE_REQUIRED";
224
288
 
289
+ const ADVISORY_CONTEXT_AND_HANDOFF_ERROR_METADATA = Object.freeze(Object.fromEntries([
290
+ [E_ADVISORY_CONTEXT_PROVIDER_INVALID, Object.freeze({
291
+ code: E_ADVISORY_CONTEXT_PROVIDER_INVALID,
292
+ category: "advisory-context",
293
+ classification: "PUBLIC_STABLE",
294
+ meaning: "Advisory context provider configuration or interface implementation is invalid.",
295
+ safeResolution: "Use a provider implementing id, recall(input) with bounded query parameters; advisory context is optional.",
296
+ })],
297
+ [E_ADVISORY_CONTEXT_PROVIDER_UNAVAILABLE, Object.freeze({
298
+ code: E_ADVISORY_CONTEXT_PROVIDER_UNAVAILABLE,
299
+ category: "advisory-context",
300
+ classification: "PUBLIC_STABLE",
301
+ meaning: "Requested advisory context provider is not registered in runtime context.",
302
+ safeResolution: "Register the provider in runtime context before recall, or proceed without advisory context; provider failure never blocks canonical lifecycle.",
303
+ })],
304
+ [E_ADVISORY_CONTEXT_QUERY_INVALID, Object.freeze({
305
+ code: E_ADVISORY_CONTEXT_QUERY_INVALID,
306
+ category: "advisory-context",
307
+ classification: "PUBLIC_STABLE",
308
+ meaning: "Advisory context query failed portable-context validation or exceeded budget.",
309
+ safeResolution: "Provide a bounded query free of control characters and secret-like values.",
310
+ })],
311
+ [E_ADVISORY_CONTEXT_REQUEST_INVALID, Object.freeze({
312
+ code: E_ADVISORY_CONTEXT_REQUEST_INVALID,
313
+ category: "advisory-context",
314
+ classification: "PUBLIC_STABLE",
315
+ meaning: "Advisory context recall budgets are not finite integer values within the supported request contract.",
316
+ safeResolution: "Provide finite integer limit, maxItemChars, maxTotalChars, and timeoutMs values; oversized valid values are clamped to documented maxima.",
317
+ })],
318
+ [E_ADVISORY_CONTEXT_RESULT_INVALID, Object.freeze({
319
+ code: E_ADVISORY_CONTEXT_RESULT_INVALID,
320
+ category: "advisory-context",
321
+ classification: "PUBLIC_STABLE",
322
+ meaning: "Advisory context provider returned an invalid result structure.",
323
+ safeResolution: "Ensure provider returns items with string summary and optional title, sourceRef, observedAt, confidence.",
324
+ })],
325
+ [E_ADVISORY_CONTEXT_TIMEOUT, Object.freeze({
326
+ code: E_ADVISORY_CONTEXT_TIMEOUT,
327
+ category: "advisory-context",
328
+ classification: "PUBLIC_STABLE",
329
+ meaning: "Advisory context recall exceeded its execution timeout.",
330
+ safeResolution: "Use a responsive provider or increase timeout within limits; advisory context is optional.",
331
+ })],
332
+ [E_ADVISORY_CONTEXT_OUTPUT_LIMIT, Object.freeze({
333
+ code: E_ADVISORY_CONTEXT_OUTPUT_LIMIT,
334
+ category: "advisory-context",
335
+ classification: "PUBLIC_STABLE",
336
+ meaning: "Advisory context output exceeded the configured character or item limit.",
337
+ safeResolution: "Reduce query scope, limit items, or truncate oversized summaries at the provider.",
338
+ })],
339
+ [E_PORTABLE_CONTEXT_INVALID, Object.freeze({
340
+ code: E_PORTABLE_CONTEXT_INVALID,
341
+ category: "portable-context",
342
+ classification: "PUBLIC_STABLE",
343
+ meaning: "Text or object failed portable-context safety, character, or secret limits.",
344
+ safeResolution: "Ensure text is bounded, contains no control characters, and contains no secret-like values.",
345
+ })],
346
+ [E_HANDOFF_ACCEPTANCE_UNBOUND, Object.freeze({
347
+ code: E_HANDOFF_ACCEPTANCE_UNBOUND,
348
+ category: "handoff",
349
+ classification: "PUBLIC_STABLE",
350
+ meaning: "Handoff snapshot lacks required workStateFingerprint binding.",
351
+ safeResolution: "Create a fresh handoff from the current ForgeLoop version before accepting it.",
352
+ })],
353
+ [E_HANDOFF_STALE, Object.freeze({
354
+ code: E_HANDOFF_STALE,
355
+ category: "handoff",
356
+ classification: "PUBLIC_STABLE",
357
+ meaning: "Handoff snapshot has drifted from the current canonical task state or repository.",
358
+ safeResolution: "Create a new fresh handoff from the current task state instead of accepting a stale snapshot.",
359
+ })],
360
+ [E_HANDOFF_ALREADY_ACCEPTED, Object.freeze({
361
+ code: E_HANDOFF_ALREADY_ACCEPTED,
362
+ category: "handoff",
363
+ classification: "PUBLIC_STABLE",
364
+ meaning: "Handoff was already accepted by a different consumer.",
365
+ safeResolution: "Create a new handoff for the new consumer; do not manually edit acceptance events.",
366
+ })],
367
+ [E_HANDOFF_ACCEPTANCE_INCONSISTENT, Object.freeze({
368
+ code: E_HANDOFF_ACCEPTANCE_INCONSISTENT,
369
+ category: "handoff",
370
+ classification: "PUBLIC_STABLE",
371
+ meaning: "Handoff acceptance disagrees with task event ledger history.",
372
+ safeResolution: "Verify ledger integrity and require a preceding valid HANDOFF_CREATED event.",
373
+ })],
374
+ ]));
375
+
225
376
  /**
226
377
  * Public, stable ForgeLoop error and reason codes documented for users and harnesses.
227
378
  */
228
379
  export const PUBLIC_ERROR_CODES = Object.freeze({
229
380
  ...EXTENSION_PUBLIC_ERROR_CODES,
381
+ ...STRUCTURAL_QUALITY_ERROR_METADATA,
382
+ ...ADVISORY_CONTEXT_AND_HANDOFF_ERROR_METADATA,
230
383
  E_PREFLIGHT_NOT_READY: Object.freeze({
231
384
  code: "E_PREFLIGHT_NOT_READY",
232
385
  category: "preflight",
@@ -1044,6 +1197,27 @@ export const ALL_KNOWN_ERROR_CODES = Object.freeze(new Set([
1044
1197
  E_USAGE_INVALID,
1045
1198
  E_USAGE_SOURCE_INVALID,
1046
1199
  E_EFFICIENCY_BASELINE_INVALID,
1200
+ E_STRUCTURAL_QUALITY_CONFIGURATION_INVALID,
1201
+ E_STRUCTURAL_QUALITY_PROVIDER_INVALID,
1202
+ E_STRUCTURAL_QUALITY_PROVIDER_UNAVAILABLE,
1203
+ E_STRUCTURAL_QUALITY_PROVIDER_VERSION_UNSUPPORTED,
1204
+ E_STRUCTURAL_QUALITY_PROVIDER_PROTOCOL_INVALID,
1205
+ E_STRUCTURAL_QUALITY_PROVIDER_TOOL_CONTRACT_INVALID,
1206
+ E_STRUCTURAL_QUALITY_SCAN_FAILED,
1207
+ E_STRUCTURAL_QUALITY_TIMEOUT,
1208
+ E_STRUCTURAL_QUALITY_OUTPUT_LIMIT,
1209
+ E_STRUCTURAL_QUALITY_BASELINE_MISSING,
1210
+ E_STRUCTURAL_QUALITY_BASELINE_EXISTS,
1211
+ E_STRUCTURAL_QUALITY_BASELINE_PHASE_INVALID,
1212
+ E_STRUCTURAL_QUALITY_BASELINE_BINDING_MISMATCH,
1213
+ E_STRUCTURAL_QUALITY_EVALUATION_INCOMPARABLE,
1214
+ E_STRUCTURAL_QUALITY_MEASUREMENT_MODEL_MISMATCH,
1215
+ E_STRUCTURAL_QUALITY_EVIDENCE_STALE,
1216
+ E_STRUCTURAL_QUALITY_SOURCE_DRIFT,
1217
+ E_STRUCTURAL_QUALITY_SOURCE_FINGERPRINT_UNAVAILABLE,
1218
+ E_STRUCTURAL_QUALITY_OBSERVATION_EPOCH_STALE,
1219
+ E_STRUCTURAL_QUALITY_PROJECTION_INCOMPLETE,
1220
+ E_STRUCTURAL_QUALITY_REGRESSION,
1047
1221
  E_RECONCILE_NOT_STALE,
1048
1222
  E_RECONCILE_PHASE_INVALID,
1049
1223
  E_RECONCILE_UNSUPPORTED_DRIFT,
@@ -1132,6 +1306,18 @@ export const ALL_KNOWN_ERROR_CODES = Object.freeze(new Set([
1132
1306
  E_FAILURE_SIGNATURE_INVALID,
1133
1307
  E_STRATEGY_OSCILLATION,
1134
1308
  "E_TRACE_SNAPSHOT_INCONSISTENT",
1309
+ E_ADVISORY_CONTEXT_PROVIDER_INVALID,
1310
+ E_ADVISORY_CONTEXT_PROVIDER_UNAVAILABLE,
1311
+ E_ADVISORY_CONTEXT_QUERY_INVALID,
1312
+ E_ADVISORY_CONTEXT_REQUEST_INVALID,
1313
+ E_ADVISORY_CONTEXT_RESULT_INVALID,
1314
+ E_ADVISORY_CONTEXT_TIMEOUT,
1315
+ E_ADVISORY_CONTEXT_OUTPUT_LIMIT,
1316
+ E_PORTABLE_CONTEXT_INVALID,
1317
+ E_HANDOFF_ACCEPTANCE_UNBOUND,
1318
+ E_HANDOFF_STALE,
1319
+ E_HANDOFF_ALREADY_ACCEPTED,
1320
+ E_HANDOFF_ACCEPTANCE_INCONSISTENT,
1135
1321
  ]));
1136
1322
 
1137
1323
  /**
@@ -160,6 +160,16 @@ export function validateKnownEventDetails(event) {
160
160
  }
161
161
  assertFingerprint(event.details.digest, "HANDOFF_CREATED details.digest");
162
162
  return;
163
+ case "HANDOFF_ACCEPTED":
164
+ if (!event.details || typeof event.details !== "object" || Array.isArray(event.details)
165
+ || typeof event.details.handoffId !== "string" || !/^handoff-[A-Za-z0-9_-]+$/.test(event.details.handoffId)
166
+ || typeof event.details.handoffDigest !== "string"
167
+ || typeof event.details.consumerId !== "string" || !event.details.consumerId.trim()
168
+ || (event.details.harness !== undefined && (typeof event.details.harness !== "string" || !event.details.harness.trim()))) {
169
+ throw protocolError("E_EVENT_INVALID", "HANDOFF_ACCEPTED requires a valid handoffId, handoffDigest, and consumerId");
170
+ }
171
+ assertFingerprint(event.details.handoffDigest, "HANDOFF_ACCEPTED details.handoffDigest");
172
+ return;
163
173
  case "RESPONSIBILITY_SET":
164
174
  assertStructuredArtifactEvent(event, ["responsibilityFingerprint"], "RESPONSIBILITY_SET");
165
175
  if (typeof event.details.label !== "string" || !event.details.label) {
@@ -513,6 +523,8 @@ export async function validateEventLedger(target, packageRoot, options = {}) {
513
523
  const seen = new Set();
514
524
  let lastMilestone = -1;
515
525
  const milestoneCounts = new Map();
526
+ const createdHandoffs = new Map();
527
+ const acceptedHandoffs = new Set();
516
528
  for (const [index, event] of events.entries()) {
517
529
  if (event.seq !== index + 1) {
518
530
  errors.push({ code: "E_EVENT_INVALID", message: `event sequence must be ${index + 1}` });
@@ -584,6 +596,26 @@ export async function validateEventLedger(target, packageRoot, options = {}) {
584
596
  if (event.event === "COMPLETION_REJECTED" && !seen.has("VERIFICATION_STARTED")) {
585
597
  errors.push({ code: "E_PHASE_CHRONOLOGY_INVALID", message: "completion rejected before verification started" });
586
598
  }
599
+ if (event.event === "HANDOFF_CREATED") {
600
+ if (event.details?.handoffId) {
601
+ createdHandoffs.set(event.details.handoffId, event.details.digest);
602
+ }
603
+ }
604
+ if (event.event === "HANDOFF_ACCEPTED") {
605
+ const hId = event.details?.handoffId;
606
+ if (hId) {
607
+ if (acceptedHandoffs.has(hId)) {
608
+ errors.push({ code: "E_HANDOFF_ALREADY_ACCEPTED", message: `duplicate HANDOFF_ACCEPTED for handoffId ${hId}` });
609
+ } else {
610
+ acceptedHandoffs.add(hId);
611
+ if (!createdHandoffs.has(hId)) {
612
+ errors.push({ code: "E_HANDOFF_ACCEPTANCE_INCONSISTENT", message: `HANDOFF_ACCEPTED has no preceding HANDOFF_CREATED for handoffId ${hId}` });
613
+ } else if (createdHandoffs.get(hId) !== event.details?.handoffDigest) {
614
+ errors.push({ code: "E_HANDOFF_ACCEPTANCE_INCONSISTENT", message: `HANDOFF_ACCEPTED digest mismatch for handoffId ${hId}` });
615
+ }
616
+ }
617
+ }
618
+ }
587
619
  }
588
620
  validateLegacyRecoveryMigrations(events, errors, {
589
621
  allowUnmigratedLegacyRecoveryEvents: options?.allowUnmigratedLegacyRecoveryEvents === true,
@@ -112,6 +112,7 @@ export function projectExecutionProfileContext({
112
112
  route,
113
113
  state,
114
114
  nextAction,
115
+ runtimeContext,
115
116
  } = {}) {
116
117
  if (typeof taskId !== "string" || !taskId) throw contextError("E_TASK_REQUIRED", "taskId is required");
117
118
  if (!contract || typeof contract !== "object" || Array.isArray(contract)) {
@@ -126,6 +127,18 @@ export function projectExecutionProfileContext({
126
127
  const profile = route.executionProfile ? assertExecutionProfile(route.executionProfile) : legacyExecutionProfile();
127
128
  const resolvedProfile = profile.resolved;
128
129
  const policy = getExecutionProfileContextPolicy(resolvedProfile);
130
+ const available = [...OPTIONAL_CONTEXT_BY_PROFILE[resolvedProfile]];
131
+ const advisoryProviders = runtimeContext?.advisoryContextProviders;
132
+ const hasAdvisory = Boolean(
133
+ advisoryProviders && (
134
+ advisoryProviders instanceof Map
135
+ ? advisoryProviders.size > 0
136
+ : Object.keys(advisoryProviders).length > 0
137
+ ),
138
+ );
139
+ if (hasAdvisory && !available.includes("advisory-context")) {
140
+ available.push("advisory-context");
141
+ }
129
142
  return {
130
143
  schemaVersion: 1,
131
144
  protocolVersion: 1,
@@ -140,7 +153,7 @@ export function projectExecutionProfileContext({
140
153
  verificationRequirements: verificationRequirements(contract),
141
154
  contextPolicy: policy,
142
155
  optionalContext: {
143
- available: [...OPTIONAL_CONTEXT_BY_PROFILE[resolvedProfile]],
156
+ available,
144
157
  loaded: [],
145
158
  },
146
159
  invariants: { ...PROFILE_INVARIANTS },
@@ -173,5 +186,6 @@ export async function buildExecutionProfileContext({
173
186
  route: routeArtifact.value,
174
187
  state,
175
188
  nextAction,
189
+ runtimeContext,
176
190
  });
177
191
  }
@@ -73,6 +73,38 @@ export function ensureWithin(root, relativePath) {
73
73
  return path.join(root, normalized);
74
74
  }
75
75
 
76
+ function normalizeWindowsPathForComparison(value) {
77
+ const lowerValue = value.toLowerCase();
78
+ let normalized = value;
79
+ if (lowerValue.startsWith("\\\\?\\unc\\")) {
80
+ normalized = `\\\\${value.slice(8)}`;
81
+ } else if (lowerValue.startsWith("\\\\.\\unc\\")) {
82
+ normalized = `\\\\${value.slice(8)}`;
83
+ } else if (lowerValue.startsWith("\\\\?\\")) {
84
+ normalized = value.slice(4);
85
+ } else if (lowerValue.startsWith("\\\\.\\")) {
86
+ normalized = value.slice(4);
87
+ }
88
+ return path.win32.normalize(normalized).toLowerCase();
89
+ }
90
+
91
+ export function isPathWithin(root, candidate, { platform = process.platform } = {}) {
92
+ const pathApi = platform === "win32" ? path.win32 : path;
93
+ const normalizeForComparison = (value) => {
94
+ return platform === "win32"
95
+ ? normalizeWindowsPathForComparison(value)
96
+ : pathApi.normalize(value);
97
+ };
98
+ const relative = pathApi.relative(
99
+ normalizeForComparison(root),
100
+ normalizeForComparison(candidate),
101
+ );
102
+ return relative === ""
103
+ || (relative !== ".."
104
+ && !relative.startsWith(`..${pathApi.sep}`)
105
+ && !pathApi.isAbsolute(relative));
106
+ }
107
+
76
108
  export async function assertSafePath(root, relativePath) {
77
109
  const destination = ensureWithin(root, relativePath);
78
110
  const absoluteRoot = path.resolve(root);
@@ -105,8 +137,7 @@ export async function assertSafePath(root, relativePath) {
105
137
  }
106
138
  const resolvedRoot = await realpathWithTransientWindowsRetry(absoluteRoot);
107
139
  const resolvedExisting = await realpathWithTransientWindowsRetry(existing);
108
- const relativeResolved = path.relative(resolvedRoot, resolvedExisting);
109
- if (relativeResolved === ".." || relativeResolved.startsWith(`..${path.sep}`) || path.isAbsolute(relativeResolved)) {
140
+ if (!isPathWithin(resolvedRoot, resolvedExisting)) {
110
141
  throw new Error(`Path escapes target directory: ${relativePath}`);
111
142
  }
112
143
  return destination;
@@ -186,4 +217,4 @@ export async function writeFileAtomic(filePath, bytes, { dryRun = false } = {})
186
217
  }
187
218
  throw error;
188
219
  }
189
- }
220
+ }