@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,140 @@
1
+ import path from "node:path";
2
+
3
+ import {
4
+ normalizePortableText,
5
+ assertPortableContextSafe,
6
+ } from "../portable-context.js";
7
+ import {
8
+ ADVISORY_CONTEXT_LIMITS,
9
+ normalizeAdvisoryRecallOptions,
10
+ } from "./constants.js";
11
+ import {
12
+ normalizeAdvisoryContextResult,
13
+ resolveAdvisoryContextProvider,
14
+ } from "./provider.js";
15
+ import {
16
+ E_ADVISORY_CONTEXT_PROVIDER_INVALID,
17
+ E_ADVISORY_CONTEXT_PROVIDER_UNAVAILABLE,
18
+ E_ADVISORY_CONTEXT_QUERY_INVALID,
19
+ E_ADVISORY_CONTEXT_TIMEOUT,
20
+ E_PORTABLE_CONTEXT_INVALID,
21
+ } from "../error-codes.js";
22
+
23
+ function serviceError(code, message, cause) {
24
+ const error = new Error(message, cause !== undefined ? { cause } : undefined);
25
+ error.name = "AdvisoryContextServiceError";
26
+ error.code = code;
27
+ return error;
28
+ }
29
+
30
+ export async function recallAdvisoryContext({
31
+ target,
32
+ taskId,
33
+ providerName,
34
+ query,
35
+ limit,
36
+ maxItemChars,
37
+ maxTotalChars,
38
+ timeoutMs,
39
+ runtimeContext,
40
+ } = {}) {
41
+ if (!target || typeof target !== "string") {
42
+ throw serviceError(
43
+ E_ADVISORY_CONTEXT_PROVIDER_INVALID,
44
+ "Target project path is required for advisory recall",
45
+ );
46
+ }
47
+
48
+ if (!taskId || typeof taskId !== "string") {
49
+ throw serviceError(
50
+ E_ADVISORY_CONTEXT_PROVIDER_INVALID,
51
+ "taskId is required for advisory recall",
52
+ );
53
+ }
54
+
55
+ if (!providerName || typeof providerName !== "string") {
56
+ throw serviceError(
57
+ E_ADVISORY_CONTEXT_PROVIDER_INVALID,
58
+ "providerName is required for advisory recall",
59
+ );
60
+ }
61
+
62
+ let normalizedQuery;
63
+ try {
64
+ normalizedQuery = normalizePortableText(query, {
65
+ label: "advisory query",
66
+ maxLength: ADVISORY_CONTEXT_LIMITS.maxQueryChars,
67
+ });
68
+ assertPortableContextSafe(normalizedQuery, { label: "advisory query" });
69
+ } catch (err) {
70
+ if (err.code === E_PORTABLE_CONTEXT_INVALID) {
71
+ throw err;
72
+ }
73
+ throw serviceError(
74
+ E_ADVISORY_CONTEXT_QUERY_INVALID,
75
+ `Advisory context query is invalid: ${err.message}`,
76
+ err,
77
+ );
78
+ }
79
+
80
+ const effectiveOptions = normalizeAdvisoryRecallOptions({
81
+ limit,
82
+ maxItemChars,
83
+ maxTotalChars,
84
+ timeoutMs,
85
+ });
86
+
87
+ let provider;
88
+ try {
89
+ provider = await resolveAdvisoryContextProvider({
90
+ providers: runtimeContext?.advisoryContextProviders,
91
+ providerName,
92
+ });
93
+ } catch (err) {
94
+ throw serviceError(
95
+ E_ADVISORY_CONTEXT_PROVIDER_INVALID,
96
+ `Advisory context provider "${providerName}" failed validation: ${err.message}`,
97
+ err,
98
+ );
99
+ }
100
+
101
+ if (!provider) {
102
+ throw serviceError(
103
+ E_ADVISORY_CONTEXT_PROVIDER_UNAVAILABLE,
104
+ `Advisory context provider "${providerName}" is not registered in runtime context`,
105
+ );
106
+ }
107
+
108
+ let timer;
109
+ const timeoutPromise = new Promise((_, reject) => {
110
+ timer = setTimeout(() => {
111
+ reject(
112
+ serviceError(
113
+ E_ADVISORY_CONTEXT_TIMEOUT,
114
+ `Advisory recall from provider "${providerName}" timed out after ${effectiveOptions.timeoutMs}ms`,
115
+ ),
116
+ );
117
+ }, effectiveOptions.timeoutMs);
118
+ });
119
+
120
+ let rawResult;
121
+ try {
122
+ const recallPromise = Promise.resolve(
123
+ provider.recall({
124
+ projectPath: path.resolve(target),
125
+ taskId,
126
+ query: normalizedQuery,
127
+ ...effectiveOptions,
128
+ }),
129
+ );
130
+ rawResult = await Promise.race([recallPromise, timeoutPromise]);
131
+ } finally {
132
+ clearTimeout(timer);
133
+ }
134
+
135
+ return normalizeAdvisoryContextResult(rawResult, {
136
+ provider,
137
+ taskId,
138
+ ...effectiveOptions,
139
+ });
140
+ }
@@ -283,6 +283,18 @@ export const ARTIFACT_REGISTRY = Object.freeze({
283
283
  isPersisted: true,
284
284
  description: "Immutable trajectory evaluation results compiled from the canonical trace against a local reference scenario.",
285
285
  }),
286
+ structuralQuality: Object.freeze({
287
+ key: "structuralQuality",
288
+ scope: "TASK",
289
+ path: `${TASK_STATE_ROOT}/<task-key>/${TASK_ARTIFACT_FILES.structuralQuality}/baseline.json`,
290
+ schema: "structural-quality",
291
+ owner: "PROTOCOL_COMPILED",
292
+ mutability: "BASELINE_IMMUTABLE_AFTER_EXECUTION",
293
+ trustRole: "STRUCTURAL_QUALITY_EVIDENCE",
294
+ isPublic: true,
295
+ isPersisted: true,
296
+ description: "Typed, provider-neutral structural-quality baseline and evaluation evidence bound to task, route, policy, scope, and provider identity.",
297
+ }),
286
298
  usage: Object.freeze({
287
299
  key: "usage",
288
300
  scope: "TASK",
package/src/core/audit.js CHANGED
@@ -11,6 +11,7 @@ import { validateActionLedgerConsistency } from "./actions.js";
11
11
  import { readCodeManifest } from "./code-manifest.js";
12
12
  import { readAttestationStatement, validateAttestationStatement } from "./attestation.js";
13
13
  import { assertAttestationStatementBindings, verifyCodeManifestContent } from "./attestation-verifier.js";
14
+ import { projectStructuralQualityStatus } from "./structural-quality/status.js";
14
15
 
15
16
  function sortErrors(errors) {
16
17
  return [...errors].sort((left, right) => left.code.localeCompare(right.code)
@@ -244,6 +245,33 @@ export async function evaluateAudit({
244
245
  policyStatus = { status: "NOT_APPLICABLE", provenRules: 0, inertRules: 0, unsupportedRules: 0, baselineViolations: 0, drift: false };
245
246
  }
246
247
 
248
+ let structuralQuality = {
249
+ mode: "off",
250
+ provider: null,
251
+ baseline: { status: "NOT_REQUESTED", qualitySignal: null, artifactRef: null, fingerprint: null },
252
+ current: { status: "NOT_OBSERVED", verificationCycle: null, attempt: null, qualitySignal: null, delta: null, bottleneck: null, artifactRef: null },
253
+ comparable: null,
254
+ completionRequired: false,
255
+ reasonCodes: [],
256
+ next: null,
257
+ };
258
+ if (taskId) {
259
+ try {
260
+ structuralQuality = await projectStructuralQualityStatus({ target, packageRoot, taskId });
261
+ } catch (error) {
262
+ structuralQuality = {
263
+ ...structuralQuality,
264
+ mode: "unknown",
265
+ reasonCodes: [error.code ?? "E_STRUCTURAL_QUALITY_EVIDENCE_STALE"],
266
+ };
267
+ }
268
+ }
269
+ const qualityEvidenceKind = structuralQuality.current.status === "PASS" || structuralQuality.current.status === "FAIL"
270
+ ? "OBSERVED"
271
+ : structuralQuality.current.status === "BLOCKED" || (structuralQuality.mode === "gate" && structuralQuality.baseline.status !== "OBSERVED")
272
+ ? "BLOCKED"
273
+ : structuralQuality.mode === "off" ? "NOT_REQUESTED" : "NOT_VERIFIED";
274
+
247
275
  return {
248
276
  schemaVersion: 1,
249
277
  protocolVersion: PROTOCOL_VERSION,
@@ -256,6 +284,15 @@ export async function evaluateAudit({
256
284
  manifest: Boolean(manifest),
257
285
  },
258
286
  policy: policyStatus,
287
+ structuralQuality: {
288
+ ...structuralQuality,
289
+ policy: structuralQuality.mode === "off" ? null : {
290
+ mode: structuralQuality.mode,
291
+ provider: structuralQuality.provider,
292
+ result: structuralQuality.current.status,
293
+ },
294
+ evidenceKind: qualityEvidenceKind,
295
+ },
259
296
  completion,
260
297
  attestation: {
261
298
  mode: attestation.mode,
@@ -282,6 +319,7 @@ export async function evaluateAudit({
282
319
  route: routeRel,
283
320
  state: stateRel,
284
321
  receipt: receiptRel,
322
+ structuralQuality: taskId ? taskArtifactPath(taskId, "structuralQuality") : null,
285
323
  },
286
324
  };
287
325
  }
@@ -8,7 +8,7 @@ import { validateChecksExecutionProvenance } from "./completion-artifacts.js";
8
8
  import { readExecutionArtifact } from "./execution.js";
9
9
  import { assertContinuitySemantics } from "./continuity.js";
10
10
  import { validateEventLedger } from "./events.js";
11
- import { taskArtifactPath, taskDirectory } from "./task-paths.js";
11
+ import { taskArtifactPath, taskDirectory, taskStructuralQualityDirectory } from "./task-paths.js";
12
12
  import { resolveTaskClaimState } from "./task-claim-state.js";
13
13
  import { E_TASK_CLAIM_OWNERSHIP_INCONSISTENT } from "./error-codes.js";
14
14
  import { listActions } from "./actions.js";
@@ -19,6 +19,13 @@ import { validateWorkspaceBinding } from "./workspace-binding.js";
19
19
  import { validateCodeManifest, validateCodeManifestBindings } from "./code-manifest.js";
20
20
  import { assertAttestationStatementBindings } from "./attestation-verifier.js";
21
21
  import { validateAttestationStatement } from "./attestation.js";
22
+ import {
23
+ listStructuralQualityEvaluations,
24
+ readStructuralQualityBaseline,
25
+ validateStructuralQualityArtifact,
26
+ validateStructuralQualityBindings,
27
+ } from "./structural-quality/artifacts.js";
28
+ import { normalizeStructuralQualityConfig, structuralQualityPolicyFingerprint } from "./structural-quality/policy.js";
22
29
 
23
30
  export const BUNDLE_SCHEMA_VERSION = 1;
24
31
  const BUNDLE_ROOT = ".forgeloop/tasks";
@@ -42,6 +49,96 @@ function bundleBindingError(code, message) {
42
49
  return error;
43
50
  }
44
51
 
52
+ function structuralQualityBundleKind(artifact) {
53
+ if (artifact === "structural-quality/baseline.json") return "baseline";
54
+ if (/^structural-quality\/evaluations\/cycle-\d+-attempt-\d+\.json$/u.test(artifact)) return "evaluation";
55
+ if (artifact.startsWith("structural-quality/")) {
56
+ throw bundleBindingError("E_BUNDLE_PATH_INVALID", `Unknown structural-quality bundle artifact: ${artifact}`);
57
+ }
58
+ return null;
59
+ }
60
+
61
+ function bundleQualityReference(reference, taskId) {
62
+ if (typeof reference !== "string" || reference.trim() === "") {
63
+ throw bundleBindingError("E_STRUCTURAL_QUALITY_EVIDENCE_STALE", "Structural-quality check has no artifact reference");
64
+ }
65
+ const normalized = reference.replaceAll("\\", "/");
66
+ const sourceRoot = taskStructuralQualityDirectory(taskId).replaceAll("\\", "/");
67
+ const suffix = normalized.startsWith(`${sourceRoot}/`)
68
+ ? normalized.slice(sourceRoot.length + 1)
69
+ : normalized.startsWith("structural-quality/")
70
+ ? normalized.slice("structural-quality/".length)
71
+ : null;
72
+ if (!suffix || !(suffix === "baseline.json" || /^evaluations\/cycle-\d+-attempt-\d+\.json$/u.test(suffix))) {
73
+ throw bundleBindingError("E_STRUCTURAL_QUALITY_EVIDENCE_STALE", "Structural-quality artifact reference escapes its task quality directory");
74
+ }
75
+ return `structural-quality/${suffix}`;
76
+ }
77
+
78
+ function structuralQualityCheckProjection(value) {
79
+ if (value.status === "PASS") return { status: "passed", evidenceKind: "OBSERVED" };
80
+ if (value.status === "FAIL") return { status: "failed", evidenceKind: "OBSERVED" };
81
+ if (value.status === "BLOCKED") return { status: "blocked", evidenceKind: "BLOCKED" };
82
+ return { status: "not-run", evidenceKind: "NOT_VERIFIED" };
83
+ }
84
+
85
+ function assertStructuralQualityBundleEvidence({ loaded, manifest, taskId }) {
86
+ const quality = loaded.structuralQuality;
87
+ if (!quality) return;
88
+ const baseline = quality.baseline;
89
+ const evaluations = quality.evaluations ?? [];
90
+ const qualityArtifacts = new Map();
91
+ if (baseline) qualityArtifacts.set("structural-quality/baseline.json", baseline);
92
+ for (const evaluation of evaluations) {
93
+ const name = `structural-quality/evaluations/cycle-${evaluation.verificationCycle}-attempt-${evaluation.attempt}.json`;
94
+ qualityArtifacts.set(name, evaluation);
95
+ }
96
+ for (const value of [baseline, ...evaluations].filter(Boolean)) {
97
+ if (value.taskId !== taskId) throw bundleBindingError("E_BUNDLE_TASK_MISMATCH", "Structural-quality artifact taskId does not match its bundle task");
98
+ const bindingErrors = validateStructuralQualityBindings(value);
99
+ if (bindingErrors.length > 0) throw bundleBindingError(bindingErrors[0].code, bindingErrors[0].message);
100
+ }
101
+ const baselineFingerprint = baseline ? canonicalFingerprint(baseline) : null;
102
+ const bundledConfig = loaded.config?.structuralQuality;
103
+ let policy = null;
104
+ if (bundledConfig) policy = normalizeStructuralQualityConfig(bundledConfig);
105
+ const expectedContractFingerprint = loaded.contract ? canonicalFingerprint(loaded.contract) : null;
106
+ const expectedRouteFingerprint = loaded.route ? canonicalFingerprint(loaded.route) : null;
107
+ for (const value of [baseline, ...evaluations].filter(Boolean)) {
108
+ if (baselineFingerprint && value.role === "EVALUATION" && value.bindings?.baselineFingerprint !== baselineFingerprint) {
109
+ throw bundleBindingError("E_STRUCTURAL_QUALITY_BASELINE_BINDING_MISMATCH", "Structural-quality evaluation does not bind the bundled baseline");
110
+ }
111
+ if (policy && value.bindings?.policyFingerprint !== structuralQualityPolicyFingerprint(policy)) {
112
+ throw bundleBindingError("E_STRUCTURAL_QUALITY_BASELINE_BINDING_MISMATCH", "Structural-quality artifact policy binding does not match bundled configuration");
113
+ }
114
+ if (expectedContractFingerprint && value.bindings?.contractFingerprint !== expectedContractFingerprint) {
115
+ throw bundleBindingError("E_STRUCTURAL_QUALITY_BASELINE_BINDING_MISMATCH", "Structural-quality contract binding does not match the bundled contract");
116
+ }
117
+ if (expectedRouteFingerprint && value.bindings?.routeFingerprint !== expectedRouteFingerprint) {
118
+ throw bundleBindingError("E_STRUCTURAL_QUALITY_BASELINE_BINDING_MISMATCH", "Structural-quality route binding does not match the bundled route");
119
+ }
120
+ }
121
+ const checks = [
122
+ ...(loaded.state?.checks ?? []),
123
+ ...(loaded.receipt?.checks ?? []),
124
+ ].filter((check) => check?.kind === "structural-quality");
125
+ for (const check of checks) {
126
+ const bundleReference = bundleQualityReference(check.details?.artifactRef, taskId);
127
+ const value = qualityArtifacts.get(bundleReference);
128
+ if (!value) throw bundleBindingError("E_STRUCTURAL_QUALITY_EVIDENCE_STALE", `Bundled structural-quality check references missing ${bundleReference}`);
129
+ if (check.details?.artifactFingerprint !== canonicalFingerprint(value)) {
130
+ throw bundleBindingError("E_STRUCTURAL_QUALITY_EVIDENCE_STALE", `Bundled structural-quality check fingerprint does not match ${bundleReference}`);
131
+ }
132
+ const expected = structuralQualityCheckProjection(value);
133
+ if (check.status !== expected.status || check.evidenceKind !== expected.evidenceKind) {
134
+ throw bundleBindingError("E_STRUCTURAL_QUALITY_EVIDENCE_STALE", `Bundled structural-quality check does not match ${bundleReference}`);
135
+ }
136
+ }
137
+ for (const artifact of manifest.artifacts.filter((item) => item.startsWith("structural-quality/"))) {
138
+ if (!qualityArtifacts.has(artifact)) throw bundleBindingError("E_STRUCTURAL_QUALITY_EVIDENCE_STALE", `Bundled structural-quality manifest entry was not loaded: ${artifact}`);
139
+ }
140
+ }
141
+
45
142
  function assertBundledCodeManifestBindings({ loaded, ledger, taskId }) {
46
143
  const manifest = loaded.codeManifest;
47
144
  if (!manifest) return;
@@ -191,6 +288,22 @@ export async function exportTaskBundle(target, taskId, packageRoot) {
191
288
  if (destinationName === "verification-scope.json") exportedVerificationScope = copied;
192
289
  }
193
290
 
291
+ // Structural-quality evidence is provider output made portable by
292
+ // ForgeLoop. Copy typed artifacts rather than raw process output, and keep
293
+ // every immutable evaluation so an audit can inspect the complete attempt
294
+ // history without rescanning the project.
295
+ const qualityBaseline = await readStructuralQualityBaseline(target, taskId, packageRoot);
296
+ const qualityEvaluations = await listStructuralQualityEvaluations(target, taskId, packageRoot);
297
+ if (qualityBaseline) {
298
+ await writeJsonArtifact(target, `${directory}/structural-quality/baseline.json`, qualityBaseline.value, "structural-quality", packageRoot);
299
+ artifacts.push("structural-quality/baseline.json");
300
+ }
301
+ for (const evaluation of qualityEvaluations) {
302
+ const destination = `structural-quality/evaluations/cycle-${evaluation.value.verificationCycle}-attempt-${evaluation.value.attempt}.json`;
303
+ await writeJsonArtifact(target, `${directory}/${destination}`, evaluation.value, "structural-quality", packageRoot);
304
+ artifacts.push(destination);
305
+ }
306
+
194
307
  if (exportedWorkspaceBinding?.value) {
195
308
  exportedWorkspaceBinding.value = await validateWorkspaceBinding(exportedWorkspaceBinding.value, packageRoot);
196
309
  if (exportedWorkspaceBinding.value.taskId !== taskId) {
@@ -335,6 +448,22 @@ export async function readTaskBundle(target, taskId, packageRoot) {
335
448
  };
336
449
  const executions = {};
337
450
  for (const artifact of manifest.value.artifacts) {
451
+ const qualityKind = structuralQualityBundleKind(artifact);
452
+ if (qualityKind) {
453
+ const qualityArtifact = await readJsonArtifact(target, `${directory}/${artifact}`, "structural-quality", packageRoot);
454
+ validateStructuralQualityArtifact(qualityArtifact.value, artifact);
455
+ if (qualityArtifact.value.taskId !== taskId) {
456
+ throw bundleBindingError("E_BUNDLE_TASK_MISMATCH", `Structural-quality ${qualityKind} taskId does not match its bundle task`);
457
+ }
458
+ loaded.structuralQuality ??= { baseline: null, evaluations: [] };
459
+ if (qualityKind === "baseline") {
460
+ if (loaded.structuralQuality.baseline) throw bundleBindingError("E_STRUCTURAL_QUALITY_EVIDENCE_STALE", "A bundle cannot contain more than one structural-quality baseline");
461
+ loaded.structuralQuality.baseline = qualityArtifact.value;
462
+ } else {
463
+ loaded.structuralQuality.evaluations.push(qualityArtifact.value);
464
+ }
465
+ continue;
466
+ }
338
467
  if (artifact.startsWith("executions/") && artifact.endsWith(".json")) {
339
468
  const execution = await readJsonArtifact(target, `${directory}/${artifact}`, "execution", packageRoot);
340
469
  executions[execution.value.executionId] = execution.value;
@@ -415,6 +544,10 @@ export async function readTaskBundle(target, taskId, packageRoot) {
415
544
  }
416
545
  loaded[mapping[0]] = loadedArtifact.value;
417
546
  }
547
+ if (loaded.structuralQuality) {
548
+ loaded.structuralQuality.evaluations.sort((left, right) => left.verificationCycle - right.verificationCycle || left.attempt - right.attempt);
549
+ assertStructuralQualityBundleEvidence({ loaded, manifest: manifest.value, taskId });
550
+ }
418
551
  const bundledLedger = loaded.codeManifest && manifest.value.artifacts.includes("events.ndjson")
419
552
  ? await validateEventLedger(target, packageRoot, { taskId, eventsPath: `${directory}/events.ndjson` })
420
553
  : null;
@@ -162,6 +162,51 @@ export const CLI_COMMAND_DEFINITIONS = Object.freeze({
162
162
  mayExecuteExternalProcess: false,
163
163
  description: "Evaluates pre-implementation contract, routing, and gates; synchronizes work state when READY.",
164
164
  }),
165
+ "quality-baseline": Object.freeze({
166
+ name: "quality-baseline",
167
+ category: "verification",
168
+ mutation: "EXTERNAL_EXECUTION",
169
+ options: Object.freeze({
170
+ ...CLI_COMMON_OPTIONS,
171
+ ...CLI_TASK_OPTION,
172
+ "--replace": Object.freeze({ targetKey: "replace", parseType: "boolean", takesValue: false, description: "replace a different baseline before EXECUTING" }),
173
+ "--timeout-ms": Object.freeze({ targetKey: "timeoutMs", parseType: "non-negative-integer", takesValue: true, valueName: "number", missingValueMessage: "--timeout-ms requires a non-negative integer", description: "bounded analyzer timeout in milliseconds" }),
174
+ "--json": Object.freeze({ targetKey: "json", parseType: "boolean", takesValue: false, description: "emit baseline result as JSON" }),
175
+ }),
176
+ writes: [".forgeloop/task-state/<taskKey>/structural-quality/baseline.json", ".forgeloop/task-state/<taskKey>/events.ndjson"],
177
+ removes: [],
178
+ mayExecuteExternalProcess: true,
179
+ description: "Captures an immutable provider-neutral structural-quality baseline through the trusted analyzer adapter.",
180
+ }),
181
+ "quality-verify": Object.freeze({
182
+ name: "quality-verify",
183
+ category: "verification",
184
+ mutation: "EXTERNAL_EXECUTION",
185
+ options: Object.freeze({
186
+ ...CLI_COMMON_OPTIONS,
187
+ ...CLI_TASK_OPTION,
188
+ "--timeout-ms": Object.freeze({ targetKey: "timeoutMs", parseType: "non-negative-integer", takesValue: true, valueName: "number", missingValueMessage: "--timeout-ms requires a non-negative integer", description: "bounded analyzer timeout in milliseconds" }),
189
+ "--json": Object.freeze({ targetKey: "json", parseType: "boolean", takesValue: false, description: "emit structural-quality verification as JSON" }),
190
+ }),
191
+ writes: [".forgeloop/task-state/<taskKey>/structural-quality/evaluations/cycle-<n>-attempt-<n>.json", ".forgeloop/task-state/<taskKey>/work-state.json", ".forgeloop/task-state/<taskKey>/execution-receipt.json", ".forgeloop/task-state/<taskKey>/events.ndjson"],
192
+ removes: [],
193
+ mayExecuteExternalProcess: true,
194
+ description: "Captures one bounded structural-quality evaluation and projects it into canonical verification evidence.",
195
+ }),
196
+ "quality-status": Object.freeze({
197
+ name: "quality-status",
198
+ category: "verification",
199
+ mutation: "READ_ONLY",
200
+ options: Object.freeze({
201
+ ...CLI_COMMON_OPTIONS,
202
+ ...CLI_TASK_OPTION,
203
+ "--json": Object.freeze({ targetKey: "json", parseType: "boolean", takesValue: false, description: "emit persisted structural-quality status as JSON" }),
204
+ }),
205
+ writes: [],
206
+ removes: [],
207
+ mayExecuteExternalProcess: false,
208
+ description: "Projects persisted structural-quality baseline and evaluation status without invoking a provider.",
209
+ }),
165
210
  advance: Object.freeze({
166
211
  name: "advance",
167
212
  category: "lifecycle",
@@ -1127,6 +1172,23 @@ export const CLI_COMMAND_DEFINITIONS = Object.freeze({
1127
1172
  mayExecuteExternalProcess: false,
1128
1173
  description: "Reads and verifies one immutable handoff snapshot.",
1129
1174
  }),
1175
+ "handoff-accept": Object.freeze({
1176
+ name: "handoff-accept",
1177
+ category: "task",
1178
+ mutation: "MUTATING",
1179
+ options: Object.freeze({
1180
+ ...CLI_COMMON_OPTIONS,
1181
+ ...CLI_TASK_OPTION,
1182
+ "--handoff": Object.freeze({ targetKey: "handoffId", parseType: "string", takesValue: true, valueName: "id", missingValueMessage: "--handoff requires a handoff ID", description: "handoff identifier" }),
1183
+ "--consumer-id": Object.freeze({ targetKey: "consumerId", parseType: "string", takesValue: true, valueName: "id", missingValueMessage: "--consumer-id requires a consumer ID", description: "consumer identifier accepting the handoff" }),
1184
+ "--harness": Object.freeze({ targetKey: "harness", parseType: "string", takesValue: true, valueName: "name", missingValueMessage: "--harness requires a harness name", description: "optional harness accepting the handoff" }),
1185
+ "--json": Object.freeze({ targetKey: "json", parseType: "boolean", takesValue: false, description: "emit acceptance result as JSON" }),
1186
+ }),
1187
+ writes: [".forgeloop/task-state/<taskKey>/events.ndjson"],
1188
+ removes: [],
1189
+ mayExecuteExternalProcess: false,
1190
+ description: "Accepts an immutable handoff exactly once in the task event ledger.",
1191
+ }),
1130
1192
  "responsibility-set": Object.freeze({
1131
1193
  name: "responsibility-set",
1132
1194
  category: "scope",
@@ -11,6 +11,9 @@ import { runUpdate } from "../commands/update.js";
11
11
  import { runActivate } from "../commands/activate.js";
12
12
  import { runAdvance } from "../commands/advance.js";
13
13
  import { runPreflight } from "../commands/preflight.js";
14
+ import { runQualityBaseline } from "../commands/quality-baseline.js";
15
+ import { runQualityVerify } from "../commands/quality-verify.js";
16
+ import { runQualityStatus } from "../commands/quality-status.js";
14
17
  import { runComplete } from "../commands/complete.js";
15
18
  import { runAudit } from "../commands/audit.js";
16
19
  import { runReport } from "../commands/report.js";
@@ -70,6 +73,7 @@ import { runWorkspaceStatus } from "../commands/workspace-status.js";
70
73
  import { runHandoffCreate } from "../commands/handoff-create.js";
71
74
  import { runHandoffList } from "../commands/handoff-list.js";
72
75
  import { runHandoffShow } from "../commands/handoff-show.js";
76
+ import { runHandoffAccept } from "../commands/handoff-accept.js";
73
77
  import { runResponsibilitySet } from "../commands/responsibility-set.js";
74
78
  import { runResponsibilityStatus } from "../commands/responsibility-status.js";
75
79
  import { runVerifyScope } from "../commands/verify-scope.js";
@@ -132,6 +136,19 @@ export const COMMAND_EXECUTORS = {
132
136
  const result = await runPreflight({ target, packageRoot, strict: options.strict, taskId: options.taskId });
133
137
  return { result, exitCode: result.status === "READY" ? 0 : 1 };
134
138
  },
139
+ "quality-baseline": async ({ target, packageRoot, options, runtimeContext }) => {
140
+ const result = await runQualityBaseline({ target, packageRoot, taskId: options.taskId, replace: options.replace, timeoutMs: options.timeoutMs, runtimeContext });
141
+ return { result, exitCode: ["CAPTURED", "EXISTING", "REPLACED", "NOT_REQUESTED"].includes(result.status) ? 0 : 1 };
142
+ },
143
+ "quality-verify": async ({ target, packageRoot, options, authorityContext, runtimeContext }) => {
144
+ const result = await runQualityVerify({ target, packageRoot, taskId: options.taskId, timeoutMs: options.timeoutMs, authorityContext, runtimeContext });
145
+ const status = result.evaluation?.status ?? result.status;
146
+ return { result, exitCode: ["PASS", "NOT_OBSERVED", "CONVERGED", "NOT_REQUESTED"].includes(status) ? 0 : 1 };
147
+ },
148
+ "quality-status": async ({ target, packageRoot, options }) => ({
149
+ result: await runQualityStatus({ target, packageRoot, taskId: options.taskId }),
150
+ exitCode: 0,
151
+ }),
135
152
  advance: async ({ target, packageRoot, options, authorityContext, runtimeContext }) => ({
136
153
  result: await runAdvance({
137
154
  target,
@@ -220,6 +237,17 @@ export const COMMAND_EXECUTORS = {
220
237
  result: await runHandoffShow({ target, packageRoot, taskId: options.taskId, handoffId: options.handoffId }),
221
238
  exitCode: 0,
222
239
  }),
240
+ "handoff-accept": async ({ target, packageRoot, options }) => ({
241
+ result: await runHandoffAccept({
242
+ target,
243
+ packageRoot,
244
+ taskId: options.taskId,
245
+ handoffId: options.handoffId,
246
+ consumerId: options.consumerId,
247
+ harness: options.harness,
248
+ }),
249
+ exitCode: 0,
250
+ }),
223
251
  "responsibility-set": async ({ target, packageRoot, options }) => ({
224
252
  result: await runResponsibilitySet({
225
253
  target,
@@ -60,6 +60,7 @@ export function defaultCommandInputValues() {
60
60
  checkExecutionRef: null,
61
61
  checkProvenance: null,
62
62
  timeoutMs: null,
63
+ replace: false,
63
64
  scopeRef: null,
64
65
  commandArgv: [],
65
66
  checkType: null,
@@ -69,6 +70,8 @@ export function defaultCommandInputValues() {
69
70
  recipientHint: null,
70
71
  handoffNote: null,
71
72
  handoffId: null,
73
+ consumerId: null,
74
+ harness: null,
72
75
  responsibilityLabel: null,
73
76
  responsibilityAllowedPaths: [],
74
77
  responsibilityReadOnlyPaths: [],
@@ -130,19 +133,38 @@ export function validateForgeLoopCommandInput({ command, input, help = false } =
130
133
  if (command === "efficiency" && !help && !options.taskId) {
131
134
  throw inputError("efficiency requires --task");
132
135
  }
136
+ if (["quality-baseline", "quality-verify", "quality-status"].includes(command) && !help && !options.taskId) {
137
+ throw inputError(`${command} requires --task`);
138
+ }
139
+ if (["quality-baseline", "quality-verify"].includes(command) && !help
140
+ && options.timeoutMs !== null && options.timeoutMs !== undefined
141
+ && (!Number.isInteger(options.timeoutMs) || options.timeoutMs < 0 || options.timeoutMs > 300000)) {
142
+ throw inputError(`${command} --timeout-ms must be between 0 and 300000`);
143
+ }
144
+ if (command !== "quality-baseline" && options.replace === true) {
145
+ throw inputError(`--replace is only valid for quality-baseline`);
146
+ }
133
147
  if (command !== "usage-record" && options.usageSource !== undefined && options.usageSource !== "ACTOR_REPORTED") {
134
148
  throw inputError(`usageSource is not valid for ${command}`);
135
149
  }
136
150
  if (options.compact === true && !["next", "task-show"].includes(command)) {
137
151
  throw inputError(`compact output is not valid for ${command}`);
138
152
  }
139
- if (["workspace-bind", "workspace-status", "handoff-create", "handoff-list", "handoff-show", "responsibility-set", "responsibility-status", "verify-scope", "attestation-create", "attestation-status", "attestation-verify"].includes(command)
153
+ if (["workspace-bind", "workspace-status", "handoff-create", "handoff-list", "handoff-show", "handoff-accept", "responsibility-set", "responsibility-status", "verify-scope", "attestation-create", "attestation-status", "attestation-verify"].includes(command)
140
154
  && !options.taskId) {
141
155
  throw inputError(`${command} requires --task`);
142
156
  }
143
157
  if (command === "handoff-show" && !help && !options.handoffId) {
144
158
  throw inputError("handoff-show requires --id");
145
159
  }
160
+ if (command === "handoff-accept" && !help) {
161
+ if (!options.handoffId) {
162
+ throw inputError("handoff-accept requires --handoff");
163
+ }
164
+ if (!options.consumerId) {
165
+ throw inputError("handoff-accept requires --consumer-id");
166
+ }
167
+ }
146
168
  if (command === "responsibility-set" && !help && !options.responsibilityLabel) {
147
169
  throw inputError("responsibility-set requires --label");
148
170
  }
@@ -25,6 +25,7 @@ import { taskArtifactPath, taskExecutionPath } from "./task-paths.js";
25
25
  import { assertClaimsCoverChangedPaths } from "./task-scope.js";
26
26
  import { discoverTasks } from "./task-discovery.js";
27
27
  import { listActions } from "./actions.js";
28
+ import { STRUCTURAL_QUALITY_REQUIREMENT } from "./structural-quality/constants.js";
28
29
 
29
30
  async function actionReceiptSummary(target, packageRoot, taskId) {
30
31
  const actions = await listActions(target, { packageRoot, taskId });
@@ -239,6 +240,7 @@ export async function requiredEvidenceForTarget({
239
240
  ...(contract.value.successCriteria ?? []),
240
241
  ...guideEvidence,
241
242
  ...(config.requiredEvidence ?? []),
243
+ ...(config.structuralQuality?.mode === "gate" ? [STRUCTURAL_QUALITY_REQUIREMENT] : []),
242
244
  ...additionalEvidence,
243
245
  ])].sort();
244
246
  }
@@ -18,6 +18,7 @@ import { readConfig } from "./config.js";
18
18
  import { resolveResponsibilityStatus } from "./responsibility.js";
19
19
  import { createCodeManifest, readCodeManifest, validateCodeManifestBindings, writeCodeManifest } from "./code-manifest.js";
20
20
  import { getActiveTaskTransaction, withTaskTransaction } from "./transaction.js";
21
+ import { validateStructuralQualityCheckProvenance } from "./structural-quality/service.js";
21
22
 
22
23
  async function attestationConfiguration(target, packageRoot, errors) {
23
24
  try {
@@ -157,6 +158,26 @@ function repairNext(error) {
157
158
  return "Do not execute installation-capable verification commands without explicit scoped installation authority; use local equivalents or record NOT_VERIFIED.";
158
159
  case "E_VERIFICATION_TOOL_UNAVAILABLE":
159
160
  return "Use an available local verifier, an existing equivalent, or record NOT_VERIFIED if installation was not authorized.";
161
+ case "E_STRUCTURAL_QUALITY_CONFIGURATION_INVALID":
162
+ return "Repair structuralQuality configuration in .forgeloop/config.json and rerun preflight.";
163
+ case "E_STRUCTURAL_QUALITY_PROVIDER_UNAVAILABLE":
164
+ case "E_STRUCTURAL_QUALITY_PROVIDER_PROTOCOL_INVALID":
165
+ case "E_STRUCTURAL_QUALITY_SCAN_FAILED":
166
+ case "E_STRUCTURAL_QUALITY_TIMEOUT":
167
+ case "E_STRUCTURAL_QUALITY_OUTPUT_LIMIT":
168
+ return "Restore the trusted structural-quality provider and rerun quality-verify; do not promote an unavailable or malformed scan.";
169
+ case "E_STRUCTURAL_QUALITY_BASELINE_MISSING":
170
+ return "Run forgeloop quality-baseline --task <id> while the task is PLANNED, before execution begins.";
171
+ case "E_STRUCTURAL_QUALITY_BASELINE_EXISTS":
172
+ case "E_STRUCTURAL_QUALITY_BASELINE_PHASE_INVALID":
173
+ return "Keep the original baseline and correct the task against it; baseline replacement is only allowed before EXECUTING.";
174
+ case "E_STRUCTURAL_QUALITY_BASELINE_BINDING_MISMATCH":
175
+ case "E_STRUCTURAL_QUALITY_EVALUATION_INCOMPARABLE":
176
+ return "Reconcile provider, version, rules, policy, scope, contract, or route drift, then rerun quality-verify in the active cycle.";
177
+ case "E_STRUCTURAL_QUALITY_EVIDENCE_STALE":
178
+ return "Run quality-verify for the current verification cycle and refresh the receipt through the canonical completion pipeline.";
179
+ case "E_STRUCTURAL_QUALITY_REGRESSION":
180
+ return "Use the bottleneck and root-cause deltas to record an evidence-backed diagnosis, correct within scope, and verify a new cycle.";
160
181
  case "E_NEW_POLICY_VIOLATION":
161
182
  return "Resolve the new policy violation or record baseline if adopted debt before completion.";
162
183
  case "E_POLICY_WEAKENING":
@@ -433,6 +454,27 @@ export async function evaluateCompletion({
433
454
  coverage = receipt?.value?.evidenceCoverage ?? [];
434
455
  }
435
456
 
457
+ if (contract && route && state) {
458
+ const qualityChecks = [...new Map([
459
+ ...(state.checks ?? []),
460
+ ...(receipt?.value?.checks ?? []),
461
+ ].filter((check) => check?.kind === "structural-quality").map((check) => [check.id, check])).values()];
462
+ for (const check of qualityChecks) {
463
+ const provenanceErrors = await validateStructuralQualityCheckProvenance(check, {
464
+ target,
465
+ packageRoot,
466
+ taskId: contract.value.taskId,
467
+ state,
468
+ contract,
469
+ route,
470
+ runtimeContext,
471
+ });
472
+ for (const error of provenanceErrors) {
473
+ errors.push(issue(error.code ?? "E_STRUCTURAL_QUALITY_EVIDENCE_STALE", error.message, error.artifacts ?? [stateRel, receiptRel]));
474
+ }
475
+ }
476
+ }
477
+
436
478
  const ledger = contract && state
437
479
  ? await validateLedger(target, taskId, contract.value.taskId, state, errors, packageRoot, { eventsPath, statePath })
438
480
  : { valid: false, events: [], errors: [] };