@cassiomc1/forgeloop 1.8.1 → 1.9.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 (64) 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/CLAUDE.md +6 -0
  5. package/DOCS_INDEX.md +5 -0
  6. package/ENG/accessibility-eng.md +12 -2
  7. package/ENG/design-code-eng.md +22 -1
  8. package/LOOP_ENGINEERING.md +28 -0
  9. package/PROTOCOL_INTEGRATION.md +26 -0
  10. package/QUALITY_SCORECARD.md +2 -0
  11. package/README.md +11 -0
  12. package/THREAT_MODEL.md +24 -0
  13. package/completions/_forgeloop +4 -1
  14. package/completions/forgeloop.bash +7 -1
  15. package/completions/forgeloop.fish +19 -1
  16. package/docs/AGENT_PROTOCOL_SUMMARY.md +6 -1
  17. package/docs/ARTIFACT_REFERENCE.md +128 -0
  18. package/docs/CLI_REFERENCE.md +84 -1
  19. package/docs/KNOWLEDGE_SOURCES.md +161 -0
  20. package/docs/MCP.md +1 -1
  21. package/docs/RECIPES.md +31 -0
  22. package/docs/STRUCTURAL_QUALITY.md +350 -0
  23. package/docs/TROUBLESHOOTING.md +107 -0
  24. package/package.json +3 -1
  25. package/schemas/config.schema.json +46 -0
  26. package/schemas/preflight.schema.json +2 -1
  27. package/schemas/structural-quality.schema.json +175 -0
  28. package/src/cli.js +18 -0
  29. package/src/commands/quality-baseline.js +28 -0
  30. package/src/commands/quality-status.js +34 -0
  31. package/src/commands/quality-verify.js +30 -0
  32. package/src/core/artifact-registry.js +12 -0
  33. package/src/core/audit.js +38 -0
  34. package/src/core/bundles.js +134 -1
  35. package/src/core/cli-command-definitions.js +45 -0
  36. package/src/core/command-executors.js +16 -0
  37. package/src/core/command-input.js +12 -0
  38. package/src/core/completion-artifacts.js +2 -0
  39. package/src/core/completion.js +42 -0
  40. package/src/core/config.js +3 -0
  41. package/src/core/error-codes.js +73 -0
  42. package/src/core/filesystem.js +18 -3
  43. package/src/core/inspect.js +64 -0
  44. package/src/core/integration-invocation-policy.js +15 -0
  45. package/src/core/integration-resources.js +17 -0
  46. package/src/core/next-action-model.js +11 -1
  47. package/src/core/next-action-phases.js +84 -5
  48. package/src/core/phase.js +9 -1
  49. package/src/core/preflight.js +33 -0
  50. package/src/core/protocol-info.js +15 -0
  51. package/src/core/runtime-context.js +27 -0
  52. package/src/core/schema-validation.js +1 -0
  53. package/src/core/structural-quality/artifacts.js +329 -0
  54. package/src/core/structural-quality/constants.js +67 -0
  55. package/src/core/structural-quality/policy.js +227 -0
  56. package/src/core/structural-quality/provider.js +287 -0
  57. package/src/core/structural-quality/sentrux-mcp.js +477 -0
  58. package/src/core/structural-quality/service.js +1138 -0
  59. package/src/core/structural-quality/source-fingerprint.js +112 -0
  60. package/src/core/structural-quality/status.js +3 -0
  61. package/src/core/task-paths.js +24 -0
  62. package/src/core/templates.js +1 -0
  63. package/src/integration.d.ts +25 -0
  64. package/src/integration.js +14 -0
@@ -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,
@@ -130,6 +131,17 @@ export function validateForgeLoopCommandInput({ command, input, help = false } =
130
131
  if (command === "efficiency" && !help && !options.taskId) {
131
132
  throw inputError("efficiency requires --task");
132
133
  }
134
+ if (["quality-baseline", "quality-verify", "quality-status"].includes(command) && !help && !options.taskId) {
135
+ throw inputError(`${command} requires --task`);
136
+ }
137
+ if (["quality-baseline", "quality-verify"].includes(command) && !help
138
+ && options.timeoutMs !== null && options.timeoutMs !== undefined
139
+ && (!Number.isInteger(options.timeoutMs) || options.timeoutMs < 0 || options.timeoutMs > 300000)) {
140
+ throw inputError(`${command} --timeout-ms must be between 0 and 300000`);
141
+ }
142
+ if (command !== "quality-baseline" && options.replace === true) {
143
+ throw inputError(`--replace is only valid for quality-baseline`);
144
+ }
133
145
  if (command !== "usage-record" && options.usageSource !== undefined && options.usageSource !== "ACTOR_REPORTED") {
134
146
  throw inputError(`usageSource is not valid for ${command}`);
135
147
  }
@@ -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: [] };
@@ -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
 
@@ -83,6 +83,57 @@ 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
+ const STRUCTURAL_QUALITY_ERROR_METADATA = Object.freeze(Object.fromEntries([
109
+ [E_STRUCTURAL_QUALITY_CONFIGURATION_INVALID, "Correct structuralQuality mode, provider ID, budgets, floors, or optimization limits in .forgeloop/config.json."],
110
+ [E_STRUCTURAL_QUALITY_PROVIDER_INVALID, "Use a provider implementing id, detect(input), and scan(input) with the documented normalized boundary."],
111
+ [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."],
112
+ [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."],
113
+ [E_STRUCTURAL_QUALITY_PROVIDER_PROTOCOL_INVALID, "Repair the provider MCP handshake or response shape; malformed external data cannot become evidence."],
114
+ [E_STRUCTURAL_QUALITY_PROVIDER_TOOL_CONTRACT_INVALID, "Ensure the provider exposes the required scan and health tool argument schemas."],
115
+ [E_STRUCTURAL_QUALITY_SCAN_FAILED, "Inspect the provider failure and rerun quality-baseline or quality-verify after the external analyzer is healthy."],
116
+ [E_STRUCTURAL_QUALITY_TIMEOUT, "Use a responsive provider or a bounded timeout within the supported limit; never promote a timed-out scan."],
117
+ [E_STRUCTURAL_QUALITY_OUTPUT_LIMIT, "Reduce provider output or diagnostics; the 2 MiB combined process-output limit is fail-closed."],
118
+ [E_STRUCTURAL_QUALITY_BASELINE_MISSING, "Run forgeloop quality-baseline --task <id> after PLANNED and before EXECUTING."],
119
+ [E_STRUCTURAL_QUALITY_BASELINE_EXISTS, "Use the existing immutable baseline or request --replace while the task is still before EXECUTING."],
120
+ [E_STRUCTURAL_QUALITY_BASELINE_PHASE_INVALID, "Baseline replacement is forbidden after EXECUTING; repair the current task against its original baseline."],
121
+ [E_STRUCTURAL_QUALITY_BASELINE_BINDING_MISMATCH, "Reconcile contract, route, policy, scope, provider, or rules drift before using the baseline."],
122
+ [E_STRUCTURAL_QUALITY_EVALUATION_INCOMPARABLE, "Restore the baseline provider/version/rules/policy/scope bindings and rerun quality-verify."],
123
+ [E_STRUCTURAL_QUALITY_MEASUREMENT_MODEL_MISMATCH, "Use a compatible measurement model and provider across baseline and evaluation observations."],
124
+ [E_STRUCTURAL_QUALITY_EVIDENCE_STALE, "Rerun quality-verify in the active verification cycle and refresh completion through the canonical receipt pipeline."],
125
+ [E_STRUCTURAL_QUALITY_SOURCE_DRIFT, "Ensure the worktree is not mutated during provider observation and rerun quality-baseline or quality-verify."],
126
+ [E_STRUCTURAL_QUALITY_SOURCE_FINGERPRINT_UNAVAILABLE, "Repair unreadable or unsafe source material before structural-quality evidence can be trusted."],
127
+ [E_STRUCTURAL_QUALITY_OBSERVATION_EPOCH_STALE, "Rerun quality-verify under the active task epoch without concurrent state mutations."],
128
+ [E_STRUCTURAL_QUALITY_PROJECTION_INCOMPLETE, "Rerun quality-verify to reconcile and project the canonical check from the existing evaluation artifact."],
129
+ [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."],
130
+ ].map(([code, safeResolution]) => [code, Object.freeze({
131
+ code,
132
+ category: "structural-quality",
133
+ classification: "PUBLIC_STABLE",
134
+ meaning: "Structural-quality evidence did not satisfy its provider, artifact, comparison, or lifecycle boundary.",
135
+ safeResolution,
136
+ })])));
86
137
 
87
138
  const EXTENSION_PUBLIC_ERROR_CODES = Object.freeze(Object.fromEntries([
88
139
  E_WORKSPACE_IDENTITY_UNAVAILABLE,
@@ -227,6 +278,7 @@ export const E_TRAJECTORY_REFERENCE_REQUIRED = "E_TRAJECTORY_REFERENCE_REQUIRED"
227
278
  */
228
279
  export const PUBLIC_ERROR_CODES = Object.freeze({
229
280
  ...EXTENSION_PUBLIC_ERROR_CODES,
281
+ ...STRUCTURAL_QUALITY_ERROR_METADATA,
230
282
  E_PREFLIGHT_NOT_READY: Object.freeze({
231
283
  code: "E_PREFLIGHT_NOT_READY",
232
284
  category: "preflight",
@@ -1044,6 +1096,27 @@ export const ALL_KNOWN_ERROR_CODES = Object.freeze(new Set([
1044
1096
  E_USAGE_INVALID,
1045
1097
  E_USAGE_SOURCE_INVALID,
1046
1098
  E_EFFICIENCY_BASELINE_INVALID,
1099
+ E_STRUCTURAL_QUALITY_CONFIGURATION_INVALID,
1100
+ E_STRUCTURAL_QUALITY_PROVIDER_INVALID,
1101
+ E_STRUCTURAL_QUALITY_PROVIDER_UNAVAILABLE,
1102
+ E_STRUCTURAL_QUALITY_PROVIDER_VERSION_UNSUPPORTED,
1103
+ E_STRUCTURAL_QUALITY_PROVIDER_PROTOCOL_INVALID,
1104
+ E_STRUCTURAL_QUALITY_PROVIDER_TOOL_CONTRACT_INVALID,
1105
+ E_STRUCTURAL_QUALITY_SCAN_FAILED,
1106
+ E_STRUCTURAL_QUALITY_TIMEOUT,
1107
+ E_STRUCTURAL_QUALITY_OUTPUT_LIMIT,
1108
+ E_STRUCTURAL_QUALITY_BASELINE_MISSING,
1109
+ E_STRUCTURAL_QUALITY_BASELINE_EXISTS,
1110
+ E_STRUCTURAL_QUALITY_BASELINE_PHASE_INVALID,
1111
+ E_STRUCTURAL_QUALITY_BASELINE_BINDING_MISMATCH,
1112
+ E_STRUCTURAL_QUALITY_EVALUATION_INCOMPARABLE,
1113
+ E_STRUCTURAL_QUALITY_MEASUREMENT_MODEL_MISMATCH,
1114
+ E_STRUCTURAL_QUALITY_EVIDENCE_STALE,
1115
+ E_STRUCTURAL_QUALITY_SOURCE_DRIFT,
1116
+ E_STRUCTURAL_QUALITY_SOURCE_FINGERPRINT_UNAVAILABLE,
1117
+ E_STRUCTURAL_QUALITY_OBSERVATION_EPOCH_STALE,
1118
+ E_STRUCTURAL_QUALITY_PROJECTION_INCOMPLETE,
1119
+ E_STRUCTURAL_QUALITY_REGRESSION,
1047
1120
  E_RECONCILE_NOT_STALE,
1048
1121
  E_RECONCILE_PHASE_INVALID,
1049
1122
  E_RECONCILE_UNSUPPORTED_DRIFT,
@@ -73,6 +73,22 @@ export function ensureWithin(root, relativePath) {
73
73
  return path.join(root, normalized);
74
74
  }
75
75
 
76
+ export function isPathWithin(root, candidate, { platform = process.platform } = {}) {
77
+ const pathApi = platform === "win32" ? path.win32 : path;
78
+ const normalizeForComparison = (value) => {
79
+ const normalized = pathApi.normalize(value);
80
+ return platform === "win32" ? normalized.toLowerCase() : normalized;
81
+ };
82
+ const relative = pathApi.relative(
83
+ normalizeForComparison(root),
84
+ normalizeForComparison(candidate),
85
+ );
86
+ return relative === ""
87
+ || (relative !== ".."
88
+ && !relative.startsWith(`..${pathApi.sep}`)
89
+ && !pathApi.isAbsolute(relative));
90
+ }
91
+
76
92
  export async function assertSafePath(root, relativePath) {
77
93
  const destination = ensureWithin(root, relativePath);
78
94
  const absoluteRoot = path.resolve(root);
@@ -105,8 +121,7 @@ export async function assertSafePath(root, relativePath) {
105
121
  }
106
122
  const resolvedRoot = await realpathWithTransientWindowsRetry(absoluteRoot);
107
123
  const resolvedExisting = await realpathWithTransientWindowsRetry(existing);
108
- const relativeResolved = path.relative(resolvedRoot, resolvedExisting);
109
- if (relativeResolved === ".." || relativeResolved.startsWith(`..${path.sep}`) || path.isAbsolute(relativeResolved)) {
124
+ if (!isPathWithin(resolvedRoot, resolvedExisting)) {
110
125
  throw new Error(`Path escapes target directory: ${relativePath}`);
111
126
  }
112
127
  return destination;
@@ -186,4 +201,4 @@ export async function writeFileAtomic(filePath, bytes, { dryRun = false } = {})
186
201
  }
187
202
  throw error;
188
203
  }
189
- }
204
+ }
@@ -15,6 +15,8 @@ import { findTaskById } from "./task-discovery.js";
15
15
  import { buildTaskTrace } from "./trace.js";
16
16
  import { evaluateProgress } from "./progress.js";
17
17
  import { readEvents } from "./events.js";
18
+ import { taskStructuralQualityDirectory } from "./task-paths.js";
19
+ import { projectStructuralQualityStatus } from "./structural-quality/status.js";
18
20
 
19
21
  function reasonCodesFor({ state, trace, progress }) {
20
22
  const codes = [];
@@ -28,6 +30,40 @@ function reasonCodesFor({ state, trace, progress }) {
28
30
  return codes;
29
31
  }
30
32
 
33
+ function structuralQualityIssues(quality, taskId) {
34
+ if (!quality || quality.mode !== "gate") return [];
35
+ const qualityPath = quality.current?.artifactRef ?? taskStructuralQualityDirectory(taskId);
36
+ if (quality.baseline?.status !== "OBSERVED") {
37
+ return [{
38
+ code: "E_STRUCTURAL_QUALITY_BASELINE_MISSING",
39
+ message: "Structural-quality gate evidence has no valid immutable baseline.",
40
+ path: taskStructuralQualityDirectory(taskId),
41
+ }];
42
+ }
43
+ if (!quality.current?.artifactRef) {
44
+ return [{
45
+ code: "E_STRUCTURAL_QUALITY_EVIDENCE_STALE",
46
+ message: "Structural-quality gate has no evaluation for the current verification cycle.",
47
+ path: qualityPath,
48
+ }];
49
+ }
50
+ if (quality.current.status === "FAIL") {
51
+ return [{
52
+ code: "E_STRUCTURAL_QUALITY_REGRESSION",
53
+ message: "Structural-quality verification detected a regression.",
54
+ path: qualityPath,
55
+ }];
56
+ }
57
+ if (quality.current.status === "BLOCKED") {
58
+ return [{
59
+ code: "E_STRUCTURAL_QUALITY_EVALUATION_INCOMPARABLE",
60
+ message: "Structural-quality verification is blocked or incomparable.",
61
+ path: qualityPath,
62
+ }];
63
+ }
64
+ return [];
65
+ }
66
+
31
67
  async function buildTaskInspection({ target, packageRoot, taskId, state, classifiedStatus = null }) {
32
68
  const trace = await buildTaskTrace({ target, packageRoot, taskId });
33
69
  const events = await readEvents(target, packageRoot, { taskId });
@@ -39,6 +75,22 @@ async function buildTaskInspection({ target, packageRoot, taskId, state, classif
39
75
  )].sort();
40
76
 
41
77
  const issues = [];
78
+ let structuralQuality;
79
+ try {
80
+ structuralQuality = await projectStructuralQualityStatus({ target, packageRoot, taskId });
81
+ } catch (error) {
82
+ structuralQuality = {
83
+ mode: "off",
84
+ provider: null,
85
+ baseline: { status: "INVALID", qualitySignal: null, artifactRef: null, fingerprint: null },
86
+ current: { status: "NOT_OBSERVED", verificationCycle: null, attempt: null, qualitySignal: null, delta: null, bottleneck: null, artifactRef: null },
87
+ comparable: null,
88
+ completionRequired: false,
89
+ reasonCodes: [error.code ?? "E_STRUCTURAL_QUALITY_EVIDENCE_STALE"],
90
+ next: null,
91
+ };
92
+ }
93
+ issues.push(...structuralQualityIssues(structuralQuality, taskId));
42
94
  if (!trace.snapshot.consistent) {
43
95
  issues.push({ code: "E_TRACE_SNAPSHOT_INCONSISTENT", message: "Task artifacts changed while being read; rerun inspect for a consistent view." });
44
96
  }
@@ -98,6 +150,7 @@ async function buildTaskInspection({ target, packageRoot, taskId, state, classif
98
150
  valid: trace.integrity.valid,
99
151
  errors: trace.integrity.errors,
100
152
  },
153
+ structuralQuality,
101
154
  audit: {},
102
155
  completion: {},
103
156
  issues,
@@ -213,6 +266,17 @@ export async function inspectTarget({ target, packageRoot, contractFile = null,
213
266
  const taskInspection = taskId
214
267
  ? await buildTaskInspection({ target, packageRoot, taskId, state: rawState, classifiedStatus: classifiedState.status })
215
268
  : null;
269
+ for (const issue of taskInspection?.issues ?? []) {
270
+ if (!issue.code.startsWith("E_STRUCTURAL_QUALITY")) continue;
271
+ findings.push({
272
+ code: issue.code,
273
+ severity: "error",
274
+ path: issue.path,
275
+ message: issue.message,
276
+ remediation: `Run forgeloop next --task ${taskId} --json and follow the structural-quality guidance.`,
277
+ evidence: createEvidence({ kind: "BLOCKED", source: issue.path, result: issue.code }),
278
+ });
279
+ }
216
280
  return {
217
281
  target: { path: target },
218
282
  authority: trustedAuthorityConfiguration({ target, authorityContext, runtimeContext }),
@@ -32,6 +32,7 @@ const READ_ONLY_COMMANDS = Object.freeze(new Set([
32
32
  "efficiency",
33
33
  "workspace-status", "handoff-list", "handoff-show", "responsibility-status",
34
34
  "attestation-verify", "attestation-status", "attestation-verify-range",
35
+ "quality-status",
35
36
  ]));
36
37
 
37
38
  const LOOP_MUTATION_COMMANDS = Object.freeze(new Set([
@@ -49,6 +50,8 @@ const STATIC_RISK_CLASSES = Object.freeze({
49
50
  ...Object.fromEntries([...LOOP_MUTATION_COMMANDS].map((name) => [name, INTEGRATION_RISK_CLASSES.LOOP_MUTATION])),
50
51
  "task-resume": INTEGRATION_RISK_CLASSES.CLAIM_REACQUISITION,
51
52
  "run-check": INTEGRATION_RISK_CLASSES.EXTERNAL_EXECUTION,
53
+ "quality-baseline": INTEGRATION_RISK_CLASSES.EXTERNAL_EXECUTION,
54
+ "quality-verify": INTEGRATION_RISK_CLASSES.EXTERNAL_EXECUTION,
52
55
  "run-action": INTEGRATION_RISK_CLASSES.EXTERNAL_EXECUTION,
53
56
  "reconcile-closure": INTEGRATION_RISK_CLASSES.EXTERNAL_EXECUTION,
54
57
  "action-propose": INTEGRATION_RISK_CLASSES.LOOP_MUTATION,
@@ -203,6 +206,17 @@ export function getForgeLoopCapabilities({ packageVersion = null } = {}) {
203
206
  modes: ["AUTO", "CHANGED", "CLAIMED", "FULL"],
204
207
  impactedMode: false,
205
208
  },
209
+ structuralQuality: {
210
+ version: 1,
211
+ supported: true,
212
+ schemaVersion: 1,
213
+ providerNeutral: true,
214
+ modes: ["off", "observe", "gate"],
215
+ builtInProviders: ["sentrux"],
216
+ commands: ["quality-baseline", "quality-verify", "quality-status"],
217
+ baselineImmutableAfterExecution: true,
218
+ maxOutputBytes: 2 * 1024 * 1024,
219
+ },
206
220
  codeAttestation: {
207
221
  version: 1,
208
222
  supported: true,
@@ -232,6 +246,7 @@ export function getForgeLoopCapabilities({ packageVersion = null } = {}) {
232
246
  { name: "task/responsibility", scope: "TASK" },
233
247
  { name: "task/verification-scope", scope: "TASK" },
234
248
  { name: "task/attestation", scope: "TASK" },
249
+ { name: "task/structural-quality", scope: "TASK" },
235
250
  { name: "task/actions", scope: "TASK" },
236
251
  { name: "task/action", scope: "TASK" },
237
252
  { name: "task/approvals", scope: "TASK" },
@@ -18,6 +18,7 @@ import { resolveResponsibilityStatus } from "./responsibility.js";
18
18
  import { readVerificationScope } from "./verification-scope.js";
19
19
  import { resolveAttestationStatus } from "./attestation.js";
20
20
  import { buildExecutionProfileContext } from "./execution-profile-context.js";
21
+ import { projectStructuralQualityStatus } from "./structural-quality/service.js";
21
22
 
22
23
  /**
23
24
  * Canonical integration resource allowlist.
@@ -77,6 +78,7 @@ export const INTEGRATION_RESOURCE_DEFINITIONS = Object.freeze({
77
78
  scope: "TASK",
78
79
  description: "Local code-attestation status and trust level for one task.",
79
80
  }),
81
+ "task/structural-quality": Object.freeze({ scope: "TASK", description: "Read-only structural-quality baseline, evaluation, comparison, and next-action projection." }),
80
82
  "task/actions": Object.freeze({ scope: "TASK", description: "Canonical durable action summaries for one task." }),
81
83
  "task/action": Object.freeze({ scope: "TASK", description: "One canonical durable action artifact." }),
82
84
  "task/approvals": Object.freeze({ scope: "TASK", description: "Durable approval artifacts for one task." }),
@@ -157,6 +159,14 @@ export async function readForgeLoopIntegrationResource(uri, {
157
159
  }
158
160
  break;
159
161
  }
162
+ case "task/structural-quality": {
163
+ if (typeof taskId !== "string" || !taskId) {
164
+ const error = new Error(`Resource ${uri} requires a taskId`);
165
+ error.code = "E_TASK_REQUIRED";
166
+ throw error;
167
+ }
168
+ break;
169
+ }
160
170
  case "task/actions":
161
171
  case "task/action":
162
172
  case "task/approvals":
@@ -174,6 +184,13 @@ export async function readForgeLoopIntegrationResource(uri, {
174
184
  const projection = await resolveTaskClaimState(projectPath, { taskId, packageRoot });
175
185
  return { uri, taskId, data: ownershipProjection(projection) };
176
186
  }
187
+ if (uri === "task/structural-quality") {
188
+ return {
189
+ uri,
190
+ taskId,
191
+ data: await projectStructuralQualityStatus({ projectRoot: projectPath, target: projectPath, packageRoot, taskId }),
192
+ };
193
+ }
177
194
  if (uri === "task/workspace-binding") {
178
195
  return { uri, taskId, data: await resolveWorkspaceBindingStatus(projectPath, { packageRoot, taskId }) };
179
196
  }
@@ -12,6 +12,10 @@ export const NEXT_ACTIONS = Object.freeze({
12
12
  ENTER_VERIFYING: "ENTER_VERIFYING",
13
13
  CONTINUE_IMPLEMENTATION: "CONTINUE_IMPLEMENTATION",
14
14
  RECORD_VERIFICATION: "RECORD_VERIFICATION",
15
+ CAPTURE_STRUCTURAL_QUALITY_BASELINE: "CAPTURE_STRUCTURAL_QUALITY_BASELINE",
16
+ VERIFY_STRUCTURAL_QUALITY: "VERIFY_STRUCTURAL_QUALITY",
17
+ DIAGNOSE_STRUCTURAL_QUALITY_REGRESSION: "DIAGNOSE_STRUCTURAL_QUALITY_REGRESSION",
18
+ RESOLVE_STRUCTURAL_QUALITY_BLOCKER: "RESOLVE_STRUCTURAL_QUALITY_BLOCKER",
15
19
  DIAGNOSE: "DIAGNOSE",
16
20
  RECORD_DIAGNOSIS: "RECORD_DIAGNOSIS",
17
21
  CORRECT: "CORRECT",
@@ -118,6 +122,7 @@ export function result({
118
122
  approvalRequired = undefined,
119
123
  capabilityDecision = undefined,
120
124
  reconciliationAuthorityRequired = undefined,
125
+ optionalActions = undefined,
121
126
  }) {
122
127
  const normalizedReasons = reasons
123
128
  .map((reason) => {
@@ -158,6 +163,7 @@ export function result({
158
163
  ...(reconciliationAuthorityRequired
159
164
  ? { reconciliationAuthorityRequired: structuredClone(reconciliationAuthorityRequired) }
160
165
  : {}),
166
+ ...(optionalActions?.length ? { optionalActions: structuredClone(optionalActions) } : {}),
161
167
  };
162
168
  }
163
169
 
@@ -167,6 +173,9 @@ export function commandFor(action) {
167
173
  [NEXT_ACTIONS.RUN_PREFLIGHT]: "forgeloop preflight --json",
168
174
  [NEXT_ACTIONS.START_EXECUTION]: "forgeloop advance --to EXECUTING",
169
175
  [NEXT_ACTIONS.ENTER_VERIFYING]: "forgeloop advance --to VERIFYING",
176
+ [NEXT_ACTIONS.CAPTURE_STRUCTURAL_QUALITY_BASELINE]: "forgeloop quality-baseline --task <id> --json",
177
+ [NEXT_ACTIONS.VERIFY_STRUCTURAL_QUALITY]: "forgeloop quality-verify --task <id> --json",
178
+ [NEXT_ACTIONS.DIAGNOSE_STRUCTURAL_QUALITY_REGRESSION]: "forgeloop advance --to DIAGNOSING",
170
179
  [NEXT_ACTIONS.DIAGNOSE]: "forgeloop advance --to DIAGNOSING",
171
180
  [NEXT_ACTIONS.CORRECT]: "forgeloop advance --to CORRECTING",
172
181
  [NEXT_ACTIONS.ENTER_REVIEWING]: "forgeloop advance --to REVIEWING",
@@ -237,7 +246,7 @@ export function recordTerminalResultCommandSpec(requirement) {
237
246
  };
238
247
  }
239
248
 
240
- export function decision(input, action, reason, requiredArtifacts = [], missingArtifacts = []) {
249
+ export function decision(input, action, reason, requiredArtifacts = [], missingArtifacts = [], optionalActions = undefined) {
241
250
  const command = commandFor(action);
242
251
  return result({
243
252
  ...input,
@@ -246,6 +255,7 @@ export function decision(input, action, reason, requiredArtifacts = [], missingA
246
255
  ...(command ? { commands: [command] } : {}),
247
256
  requiredArtifacts,
248
257
  missingArtifacts,
258
+ optionalActions,
249
259
  });
250
260
  }
251
261
 
@@ -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") {