@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
@@ -29,6 +29,10 @@ This guide provides symptom-first recovery procedures for common ForgeLoop proto
29
29
  - [Responsibility contract rejects a pass](#symptom-responsibility-contract-rejects-a-pass)
30
30
  - [Verification scope is stale or unresolved](#symptom-verification-scope-is-stale-or-unresolved)
31
31
  - [Attestation or revision coverage is invalid](#symptom-attestation-or-revision-coverage-is-invalid)
32
+ - [Structural-quality evidence is missing or blocked](#symptom-structural-quality-evidence-is-missing-or-blocked)
33
+ - [Structural-quality verification reports a regression](#symptom-structural-quality-verification-reports-a-regression)
34
+ - [Structural-quality provider is unavailable or invalid](#symptom-structural-quality-provider-is-unavailable-or-invalid)
35
+ - [Structural-quality evidence is stale or incomparable](#symptom-structural-quality-evidence-is-stale-or-incomparable)
32
36
  - [Another harness cannot resume the task](#symptom-another-harness-cannot-resume)
33
37
  - [Task claim conflict or recovered task](#symptom-task-creation-blocked-by-a-write-claim-conflict-e_task_scope_conflict)
34
38
  - [Stable Error & Reason Code Reference](#stable-error-and-reason-codes)
@@ -757,16 +761,50 @@ explicit operation only when the task's scope permits it.
757
761
 
758
762
  ---
759
763
 
764
+ ### Symptom: Advisory Context Recall Is Unavailable or Rejected
765
+
766
+ #### Error Codes: `E_ADVISORY_CONTEXT_PROVIDER_INVALID`, `E_ADVISORY_CONTEXT_PROVIDER_UNAVAILABLE`, `E_ADVISORY_CONTEXT_QUERY_INVALID`, `E_ADVISORY_CONTEXT_REQUEST_INVALID`, `E_ADVISORY_CONTEXT_RESULT_INVALID`, `E_ADVISORY_CONTEXT_TIMEOUT`, `E_ADVISORY_CONTEXT_OUTPUT_LIMIT`, `E_PORTABLE_CONTEXT_INVALID`
767
+
768
+ #### What it means
769
+
770
+ The host's optional advisory provider is missing, malformed, unavailable, too
771
+ slow, or returned content outside the bounded portable-context contract. These
772
+ failures never become lifecycle, evidence, completion, or next-action failures.
773
+
774
+ #### Safe recovery
775
+
776
+ Inspect provider registration and normalize the request before retrying through
777
+ the Integration API. Reduce query, item, total-output, and timeout values to the
778
+ documented budgets, remove secrets/control characters, and ensure the provider
779
+ returns only allowlisted item fields. If advisory context is unavailable,
780
+ continue from canonical task state; do not fabricate a provider or execute its
781
+ text.
782
+
783
+ ### Symptom: Continuity Lint Reports a Contradictory Hint
784
+
785
+ Continuity lint is diagnostic only. Its findings do not change reconciliation,
786
+ state, evidence, authority, or completion:
787
+
788
+ | Finding | Meaning | Safe response |
789
+ | --- | --- | --- |
790
+ | `CONTINUITY_REMAINING_ALREADY_COMPLETED` | A remaining-work item is already recorded as completed. | Refresh the operational note; do not change canonical completion evidence. |
791
+ | `CONTINUITY_FOCUS_ALREADY_COMPLETED` | The current focus ID is already completed. | Choose a current inspection focus or clear the hint. |
792
+ | `CONTINUITY_ITEM_ROLE_CONFLICT` | An item appears in both remaining work and known issues. | Remove the contradictory hint through `record-continuity`. |
793
+ | `CONTINUITY_INSPECT_PATH_MISSING` | An `inspectFirst` path is not present in the current target. | Reconcile the checkout and update the path hint. |
794
+ | `CONTINUITY_EMPTY_HINT_SET` | No operational hint was supplied. | Treat the result as informational and follow canonical `next`. |
795
+
760
796
  ### Symptom: Handoff Is Invalid or Tampered
761
797
 
762
- #### Error Codes: `E_HANDOFF_INVALID`, `E_HANDOFF_STATE_UNAVAILABLE`, `E_HANDOFF_TAMPERED`, `E_HANDOFF_NOT_FOUND`
798
+ #### Error Codes: `E_HANDOFF_INVALID`, `E_HANDOFF_STATE_UNAVAILABLE`, `E_HANDOFF_TAMPERED`, `E_HANDOFF_NOT_FOUND`, `E_HANDOFF_STALE`, `E_HANDOFF_ALREADY_ACCEPTED`, `E_HANDOFF_ACCEPTANCE_INCONSISTENT`
763
799
 
764
800
  #### What it means
765
801
 
766
802
  The immutable handoff envelope is malformed, its state is unavailable, its
767
803
  digest no longer matches, or the requested snapshot does not exist. A handoff
768
804
  note is not delegation, authority, independent review evidence, or completion
769
- evidence.
805
+ evidence. Acceptance is exactly-once operational receipt only. The current
806
+ repository branch and HEAD are checked directly, so clean committed checkout
807
+ drift can make a handoff stale even when changed paths are empty.
770
808
 
771
809
  #### Safe recovery
772
810
 
@@ -777,6 +815,12 @@ forgeloop continuity --task <id> --json
777
815
  forgeloop reconcile-continuity --task <id> --json
778
816
  ```
779
817
 
818
+ If `handoff-list` or `handoff-show` reports `INCONSISTENT`, inspect the
819
+ `reasonCodes` field. The commands validate the event ledger before projecting
820
+ acceptance and never turn an unreadable or invalid ledger into an empty ledger.
821
+ Repair the named ledger through the canonical protocol workflow; do not edit
822
+ `events.ndjson` by hand.
823
+
780
824
  Use the last valid handoff or continuity only to focus inspection, then trust
781
825
  the canonical task state and checkout. Never repair a handoff by editing or
782
826
  deleting its JSON file.
@@ -890,6 +934,88 @@ forgeloop next --task <id> --json
890
934
 
891
935
  ---
892
936
 
937
+ ### Symptom: Structural-quality evidence is missing or blocked
938
+
939
+ #### Error Codes: `E_STRUCTURAL_QUALITY_BASELINE_MISSING`, `E_STRUCTURAL_QUALITY_BASELINE_PHASE_INVALID`, `E_STRUCTURAL_QUALITY_PROVIDER_UNAVAILABLE`
940
+
941
+ This applies when structural quality is configured in `gate` mode but a valid
942
+ baseline or current provider observation is unavailable. `preflight` can be
943
+ ready before the baseline exists, but `PLANNED` cannot enter `EXECUTING` until
944
+ the baseline is captured. A missing provider is a visible limitation in
945
+ `observe` mode and a blocker in `gate` mode.
946
+
947
+ Inspect the persisted projection without launching a provider:
948
+
949
+ ```bash
950
+ forgeloop quality-status --task <id> --json
951
+ forgeloop next --task <id> --json
952
+ ```
953
+
954
+ Capture the baseline only in `PLANNED` after a valid preflight:
955
+
956
+ ```bash
957
+ forgeloop quality-baseline --task <id> --json
958
+ ```
959
+
960
+ Do not replace a baseline after execution begins. Install or upgrade Sentrux
961
+ only through a separately authorized, user-managed process.
962
+
963
+ ### Symptom: Structural-quality verification reports a regression
964
+
965
+ #### Error Code: `E_STRUCTURAL_QUALITY_REGRESSION`
966
+
967
+ The current observation violated the configured aggregate, dimension, cycle, or
968
+ minimum policy. An improving aggregate can still fail when a dimension budget
969
+ or cycle rule fails. Inspect the evidence and follow the normal diagnosis and
970
+ correction lifecycle. Use the evaluation artifact reference returned by
971
+ `quality-verify` as evidence when recording the diagnosis.
972
+
973
+ ```bash
974
+ forgeloop quality-status --task <id> --json
975
+ forgeloop next --task <id> --json
976
+ ```
977
+
978
+ The evaluation's bottleneck, root-cause deltas, failed conditions, and artifact
979
+ reference are evidence. ForgeLoop does not auto-create a diagnosis and does not
980
+ accept a repeated hypothesis without new information.
981
+
982
+ ### Symptom: Structural-quality provider is unavailable or invalid
983
+
984
+ #### Error Codes: `E_STRUCTURAL_QUALITY_PROVIDER_INVALID`, `E_STRUCTURAL_QUALITY_PROVIDER_VERSION_UNSUPPORTED`, `E_STRUCTURAL_QUALITY_PROVIDER_PROTOCOL_INVALID`, `E_STRUCTURAL_QUALITY_SCAN_FAILED`, `E_STRUCTURAL_QUALITY_TIMEOUT`, `E_STRUCTURAL_QUALITY_OUTPUT_LIMIT`
985
+
986
+ The provider boundary failed closed because detection, version, MCP protocol,
987
+ scan, timeout, or output limits were not satisfied. Raw MCP streams are never
988
+ persisted and partial provider output is never a passing observation.
989
+
990
+ Use the configured mode's semantics: `observe` records `NOT_OBSERVED`, while
991
+ `gate` records `BLOCKED` and keeps completion unavailable. Check the provider's
992
+ own installation and logs, then rerun the same ForgeLoop command after the
993
+ cause is understood. The project configuration cannot select an executable,
994
+ shell, arbitrary arguments, or a score override.
995
+
996
+ ### Symptom: Structural-quality evidence is stale or incomparable
997
+
998
+ #### Error Codes: `E_STRUCTURAL_QUALITY_BASELINE_BINDING_MISMATCH`, `E_STRUCTURAL_QUALITY_EVALUATION_INCOMPARABLE`, `E_STRUCTURAL_QUALITY_EVIDENCE_STALE`, `E_STRUCTURAL_QUALITY_SOURCE_FINGERPRINT_UNAVAILABLE`
999
+
1000
+ The baseline and current evaluation no longer describe the same task inputs.
1001
+ Provider ID or version, policy, route, contract, scan scope, or
1002
+ `.sentrux/rules.toml` drift makes the comparison incomparable. A prior-cycle
1003
+ pass cannot satisfy a current-cycle gate. If source material cannot be read
1004
+ or contains an unsafe symlink, ForgeLoop returns
1005
+ `E_STRUCTURAL_QUALITY_SOURCE_FINGERPRINT_UNAVAILABLE` and does not persist a
1006
+ passing observation.
1007
+
1008
+ Inspect the bound artifacts and current projection:
1009
+
1010
+ ```bash
1011
+ forgeloop quality-status --task <id> --json
1012
+ forgeloop audit --task <id> --json
1013
+ ```
1014
+
1015
+ Resolve the legitimate drift through the normal task lifecycle. Do not edit a
1016
+ baseline or evaluation by hand, and do not use a bundle as a reason to bypass
1017
+ current-cycle validation.
1018
+
893
1019
  ## Stable Error and Reason Codes
894
1020
 
895
1021
  <!-- BEGIN FORGELOOP GENERATED: public-error-codes -->
@@ -916,6 +1042,13 @@ forgeloop next --task <id> --json
916
1042
  | `E_ACTION_STATE_MISMATCH` | Requested durable action transition is not part of the canonical state machine. | Inspect current action state with forgeloop action-show and use a legal transition; never edit action artifacts by hand. |
917
1043
  | `E_ACTION_VERIFICATION_INVALID` | Verification evidence does not resolve to a canonical passed ForgeLoop artifact bound to this task and action. | Supply a canonical execution or check reference produced by run-check for this task; arbitrary strings fail closed. |
918
1044
  | `E_ACTION_VERIFICATION_REQUIRED` | The action cannot reach VERIFIED through this surface; canonical independent postcondition evidence is required. | Run an independent verification check, then record it with forgeloop action-verify; exit code 0 alone is not verification. |
1045
+ | `E_ADVISORY_CONTEXT_OUTPUT_LIMIT` | Advisory context output exceeded the configured character or item limit. | Reduce query scope, limit items, or truncate oversized summaries at the provider. |
1046
+ | `E_ADVISORY_CONTEXT_PROVIDER_INVALID` | Advisory context provider configuration or interface implementation is invalid. | Use a provider implementing id, recall(input) with bounded query parameters; advisory context is optional. |
1047
+ | `E_ADVISORY_CONTEXT_PROVIDER_UNAVAILABLE` | Requested advisory context provider is not registered in runtime context. | Register the provider in runtime context before recall, or proceed without advisory context; provider failure never blocks canonical lifecycle. |
1048
+ | `E_ADVISORY_CONTEXT_QUERY_INVALID` | Advisory context query failed portable-context validation or exceeded budget. | Provide a bounded query free of control characters and secret-like values. |
1049
+ | `E_ADVISORY_CONTEXT_REQUEST_INVALID` | Advisory context recall budgets are not finite integer values within the supported request contract. | Provide finite integer limit, maxItemChars, maxTotalChars, and timeoutMs values; oversized valid values are clamped to documented maxima. |
1050
+ | `E_ADVISORY_CONTEXT_RESULT_INVALID` | Advisory context provider returned an invalid result structure. | Ensure provider returns items with string summary and optional title, sourceRef, observedAt, confidence. |
1051
+ | `E_ADVISORY_CONTEXT_TIMEOUT` | Advisory context recall exceeded its execution timeout. | Use a responsive provider or increase timeout within limits; advisory context is optional. |
919
1052
  | `E_APPROVAL_ALREADY_RESOLVED` | Approval is one-time resolvable and has already been approved or rejected. | Request a new approval if another decision is required. |
920
1053
  | `E_APPROVAL_INVALID` | Approval artifact is malformed or does not bind the required fingerprint tuple. | Request a new approval with forgeloop approval-request; never hand-edit approval artifacts. |
921
1054
  | `E_APPROVAL_STALE` | Approval no longer matches the current action fingerprint, contract fingerprint, task revision, or capability. | Request and resolve a fresh approval against the current action revision. |
@@ -1004,8 +1137,12 @@ forgeloop next --task <id> --json
1004
1137
  | `E_GATE_REQUIRED` | A ForgeLoop protocol validation or lifecycle condition was not satisfied. | Inspect the structured command result, correct the named artifact or prerequisite, then run forgeloop next --json. |
1005
1138
  | `E_GATE_STALE` | Referenced gate artifact changed after approval. | Update artifact SHA-256 in gate file. |
1006
1139
  | `E_GATE_UNVERIFIED` | A ForgeLoop protocol validation or lifecycle condition was not satisfied. | Inspect the structured command result, correct the named artifact or prerequisite, then run forgeloop next --json. |
1140
+ | `E_HANDOFF_ACCEPTANCE_INCONSISTENT` | Handoff acceptance disagrees with task event ledger history. | Verify ledger integrity and require a preceding valid HANDOFF_CREATED event. |
1141
+ | `E_HANDOFF_ACCEPTANCE_UNBOUND` | Handoff snapshot lacks required workStateFingerprint binding. | Create a fresh handoff from the current ForgeLoop version before accepting it. |
1142
+ | `E_HANDOFF_ALREADY_ACCEPTED` | Handoff was already accepted by a different consumer. | Create a new handoff for the new consumer; do not manually edit acceptance events. |
1007
1143
  | `E_HANDOFF_INVALID` | A ForgeLoop boundary, artifact, provider, or attestation validation condition was not satisfied. | Inspect the structured command result, correct the named boundary or artifact, then retry the canonical command. |
1008
1144
  | `E_HANDOFF_NOT_FOUND` | A ForgeLoop boundary, artifact, provider, or attestation validation condition was not satisfied. | Inspect the structured command result, correct the named boundary or artifact, then retry the canonical command. |
1145
+ | `E_HANDOFF_STALE` | Handoff snapshot has drifted from the current canonical task state or repository. | Create a new fresh handoff from the current task state instead of accepting a stale snapshot. |
1009
1146
  | `E_HANDOFF_STATE_UNAVAILABLE` | A ForgeLoop boundary, artifact, provider, or attestation validation condition was not satisfied. | Inspect the structured command result, correct the named boundary or artifact, then retry the canonical command. |
1010
1147
  | `E_HANDOFF_TAMPERED` | A ForgeLoop boundary, artifact, provider, or attestation validation condition was not satisfied. | Inspect the structured command result, correct the named boundary or artifact, then retry the canonical command. |
1011
1148
  | `E_HYPOTHESIS_DISPOSITION_EVIDENCE_INVALID` | Hypothesis disposition evidence references do not resolve to checks of the active cycle. | Reference at least one check ID recorded in the active verification cycle. |
@@ -1038,6 +1175,7 @@ forgeloop next --task <id> --json
1038
1175
  | `E_POLICY_PROOF_STALE` | Mutation verification proof is stale due to checker or fixture modifications. | Re-run forgeloop rule-verify to refresh mutation proof. |
1039
1176
  | `E_POLICY_SNAPSHOT_WRITE_FAILED` | Failed to persist task policy snapshot during preflight. | Ensure the target task directory is writable and repair filesystem permissions. |
1040
1177
  | `E_POLICY_WEAKENING` | Policy rules were weakened during task execution without explicit authority. | Restore the original policy configuration. |
1178
+ | `E_PORTABLE_CONTEXT_INVALID` | Text or object failed portable-context safety, character, or secret limits. | Ensure text is bounded, contains no control characters, and contains no secret-like values. |
1041
1179
  | `E_PREFLIGHT_EVENT_MISSING` | A ForgeLoop protocol validation or lifecycle condition was not satisfied. | Inspect the structured command result, correct the named artifact or prerequisite, then run forgeloop next --json. |
1042
1180
  | `E_PREFLIGHT_GATES_STALE` | A ForgeLoop protocol validation or lifecycle condition was not satisfied. | Inspect the structured command result, correct the named artifact or prerequisite, then run forgeloop next --json. |
1043
1181
  | `E_PREFLIGHT_GATE_EVENT_MISSING` | A ForgeLoop protocol validation or lifecycle condition was not satisfied. | Inspect the structured command result, correct the named artifact or prerequisite, then run forgeloop next --json. |
@@ -1090,6 +1228,27 @@ forgeloop next --task <id> --json
1090
1228
  | `E_STATE_REVALIDATION_REQUIRED` | The work-state checkpoint must be revalidated before the lifecycle can continue. | Run forgeloop reconcile-closure for externally satisfied EXECUTING tasks, or inspect the freshness reasons for other drift. |
1091
1229
  | `E_STATE_TASK_MISMATCH` | A ForgeLoop protocol validation or lifecycle condition was not satisfied. | Inspect the structured command result, correct the named artifact or prerequisite, then run forgeloop next --json. |
1092
1230
  | `E_STRATEGY_OSCILLATION` | Correction history oscillates between previously exhausted strategies without new information. | Gather a genuinely new observation or test a materially different falsifiable hypothesis. |
1231
+ | `E_STRUCTURAL_QUALITY_BASELINE_BINDING_MISMATCH` | Structural-quality evidence did not satisfy its provider, artifact, comparison, or lifecycle boundary. | Reconcile contract, route, policy, scope, provider, or rules drift before using the baseline. |
1232
+ | `E_STRUCTURAL_QUALITY_BASELINE_EXISTS` | Structural-quality evidence did not satisfy its provider, artifact, comparison, or lifecycle boundary. | Use the existing immutable baseline or request --replace while the task is still before EXECUTING. |
1233
+ | `E_STRUCTURAL_QUALITY_BASELINE_MISSING` | Structural-quality evidence did not satisfy its provider, artifact, comparison, or lifecycle boundary. | Run forgeloop quality-baseline --task <id> after PLANNED and before EXECUTING. |
1234
+ | `E_STRUCTURAL_QUALITY_BASELINE_PHASE_INVALID` | Structural-quality evidence did not satisfy its provider, artifact, comparison, or lifecycle boundary. | Baseline replacement is forbidden after EXECUTING; repair the current task against its original baseline. |
1235
+ | `E_STRUCTURAL_QUALITY_CONFIGURATION_INVALID` | Structural-quality evidence did not satisfy its provider, artifact, comparison, or lifecycle boundary. | Correct structuralQuality mode, provider ID, budgets, floors, or optimization limits in .forgeloop/config.json. |
1236
+ | `E_STRUCTURAL_QUALITY_EVALUATION_INCOMPARABLE` | Structural-quality evidence did not satisfy its provider, artifact, comparison, or lifecycle boundary. | Restore the baseline provider/version/rules/policy/scope bindings and rerun quality-verify. |
1237
+ | `E_STRUCTURAL_QUALITY_EVIDENCE_STALE` | Structural-quality evidence did not satisfy its provider, artifact, comparison, or lifecycle boundary. | Rerun quality-verify in the active verification cycle and refresh completion through the canonical receipt pipeline. |
1238
+ | `E_STRUCTURAL_QUALITY_MEASUREMENT_MODEL_MISMATCH` | Structural-quality evidence did not satisfy its provider, artifact, comparison, or lifecycle boundary. | Use a compatible measurement model and provider across baseline and evaluation observations. |
1239
+ | `E_STRUCTURAL_QUALITY_OBSERVATION_EPOCH_STALE` | Structural-quality evidence did not satisfy its provider, artifact, comparison, or lifecycle boundary. | Rerun quality-verify under the active task epoch without concurrent state mutations. |
1240
+ | `E_STRUCTURAL_QUALITY_OUTPUT_LIMIT` | Structural-quality evidence did not satisfy its provider, artifact, comparison, or lifecycle boundary. | Reduce provider output or diagnostics; the 2 MiB combined process-output limit is fail-closed. |
1241
+ | `E_STRUCTURAL_QUALITY_PROJECTION_INCOMPLETE` | Structural-quality evidence did not satisfy its provider, artifact, comparison, or lifecycle boundary. | Rerun quality-verify to reconcile and project the canonical check from the existing evaluation artifact. |
1242
+ | `E_STRUCTURAL_QUALITY_PROVIDER_INVALID` | Structural-quality evidence did not satisfy its provider, artifact, comparison, or lifecycle boundary. | Use a provider implementing id, detect(input), and scan(input) with the documented normalized boundary. |
1243
+ | `E_STRUCTURAL_QUALITY_PROVIDER_PROTOCOL_INVALID` | Structural-quality evidence did not satisfy its provider, artifact, comparison, or lifecycle boundary. | Repair the provider MCP handshake or response shape; malformed external data cannot become evidence. |
1244
+ | `E_STRUCTURAL_QUALITY_PROVIDER_TOOL_CONTRACT_INVALID` | Structural-quality evidence did not satisfy its provider, artifact, comparison, or lifecycle boundary. | Ensure the provider exposes the required scan and health tool argument schemas. |
1245
+ | `E_STRUCTURAL_QUALITY_PROVIDER_UNAVAILABLE` | Structural-quality evidence did not satisfy its provider, artifact, comparison, or lifecycle boundary. | Install or expose the requested provider outside ForgeLoop, or use observe mode and record the limitation; ForgeLoop never auto-installs it. |
1246
+ | `E_STRUCTURAL_QUALITY_PROVIDER_VERSION_UNSUPPORTED` | Structural-quality evidence did not satisfy its provider, artifact, comparison, or lifecycle boundary. | Use a verified Sentrux version (0.5.5, 0.5.6, or 0.5.7), or select a compatible provider through trusted runtime context. |
1247
+ | `E_STRUCTURAL_QUALITY_REGRESSION` | Structural-quality evidence did not satisfy its provider, artifact, comparison, or lifecycle boundary. | Use the bottleneck and root-cause deltas to record an evidence-backed diagnosis, correct within scope, and verify a new cycle. |
1248
+ | `E_STRUCTURAL_QUALITY_SCAN_FAILED` | Structural-quality evidence did not satisfy its provider, artifact, comparison, or lifecycle boundary. | Inspect the provider failure and rerun quality-baseline or quality-verify after the external analyzer is healthy. |
1249
+ | `E_STRUCTURAL_QUALITY_SOURCE_DRIFT` | Structural-quality evidence did not satisfy its provider, artifact, comparison, or lifecycle boundary. | Ensure the worktree is not mutated during provider observation and rerun quality-baseline or quality-verify. |
1250
+ | `E_STRUCTURAL_QUALITY_SOURCE_FINGERPRINT_UNAVAILABLE` | Structural-quality evidence did not satisfy its provider, artifact, comparison, or lifecycle boundary. | Repair unreadable or unsafe source material before structural-quality evidence can be trusted. |
1251
+ | `E_STRUCTURAL_QUALITY_TIMEOUT` | Structural-quality evidence did not satisfy its provider, artifact, comparison, or lifecycle boundary. | Use a responsive provider or a bounded timeout within the supported limit; never promote a timed-out scan. |
1093
1252
  | `E_TASK_ALREADY_EXISTS` | A ForgeLoop protocol validation or lifecycle condition was not satisfied. | Inspect the structured command result, correct the named artifact or prerequisite, then run forgeloop next --json. |
1094
1253
  | `E_TASK_ALREADY_RECOVERED` | The task already has active durable recovered state. | Inspect the existing recovery metadata; use task-resume to reacquire claims or leave the task recovered. |
1095
1254
  | `E_TASK_AMBIGUOUS` | Multiple tasks exist in the project but no task selector was provided. | Select a task explicitly using --task <id> or FORGELOOP_TASK=<id>. |
@@ -93,6 +93,66 @@ commands in [`EXECUTION_PROFILE_BENCHMARKS.md`](./EXECUTION_PROFILE_BENCHMARKS.m
93
93
  The benchmark source policy accepts provider or host observations only and
94
94
  keeps unavailable or non-comparable measurements out of efficiency claims.
95
95
 
96
+ ### Advisory Context & Handoff Acceptance
97
+
98
+ Capability negotiation is feature-first: `canonicalHandoffs` is v2 and
99
+ `advisoryContextProviders` is v1. These capability-family versions are
100
+ independent of Protocol v1, schema v1, and Integration API v1. The programmatic
101
+ integration API exposes lazy advisory context querying and exactly-once handoff
102
+ acceptance:
103
+
104
+ ```javascript
105
+ import {
106
+ createForgeLoopContext,
107
+ recallAdvisoryContext,
108
+ acceptCanonicalHandoff,
109
+ resolveHandoffAcceptance,
110
+ } from "@cassiomc1/forgeloop/integration";
111
+
112
+ // 1. Register host-provided advisory context
113
+ const runtimeContext = createForgeLoopContext({
114
+ advisoryContextProviders: {
115
+ "host-memory": {
116
+ id: "host-memory",
117
+ recall: async ({ query }) => ({ items: [...] }),
118
+ },
119
+ },
120
+ });
121
+
122
+ // 2. Explicitly query advisory context (strictly non-evidence, non-executable)
123
+ const advisoryResult = await recallAdvisoryContext({
124
+ target: ".",
125
+ taskId: "task-1",
126
+ providerName: "host-memory",
127
+ query: "authentication tokens",
128
+ runtimeContext,
129
+ });
130
+
131
+ // 3. Exactly-once handoff acceptance
132
+ const acceptance = await acceptCanonicalHandoff(".", {
133
+ taskId: "task-1",
134
+ handoffId: "handoff-001",
135
+ consumerId: "agent-session-42",
136
+ harness: "cursor",
137
+ });
138
+ ```
139
+
140
+ See [`ADVISORY_CONTEXT.md`](./ADVISORY_CONTEXT.md) for full trust boundary specifications, portable text sanitization rules, and budget enforcement.
141
+
142
+ Advisory recall budgets are normalized before provider dispatch: valid oversized
143
+ integer requests are clamped, while invalid finite/integer/minimum types fail
144
+ with `E_ADVISORY_CONTEXT_REQUEST_INVALID` before provider lookup. The registry
145
+ key and resolved provider `id` must match. Handoff acceptance independently
146
+ checks canonical state and the current repository branch/HEAD, and remains an
147
+ exactly-once operational receipt with no evidence, claim transfer, or authority.
148
+
149
+ Advisory context is not a canonical resource unless a future explicitly
150
+ versioned resource introduces one. `protocol-info` advertises the optional
151
+ provider capability, but the stock CLI never recalls it automatically and
152
+ there is no stock `context-recall` command. A consumer that only understands
153
+ `canonicalHandoffs` v1 may disable only the handoff-specific UI while retaining
154
+ the rest of Protocol v1 functionality.
155
+
96
156
  ## Consumers
97
157
 
98
158
  | Surface | Entry |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cassiomc1/forgeloop",
3
- "version": "1.8.1",
3
+ "version": "1.10.0",
4
4
  "description": "Portable, verifiable engineering protocol for AI coding environments and developer workflows",
5
5
  "repository": {
6
6
  "type": "git",
@@ -51,10 +51,12 @@
51
51
  "docs/assets/diagrams",
52
52
  "docs/GETTING_STARTED.md",
53
53
  "docs/CROSS_HARNESS_CONTINUITY.md",
54
+ "docs/ADVISORY_CONTEXT.md",
54
55
  "docs/CLI_REFERENCE.md",
55
56
  "docs/ARTIFACT_REFERENCE.md",
56
57
  "docs/TROUBLESHOOTING.md",
57
58
  "docs/RECIPES.md",
59
+ "docs/STRUCTURAL_QUALITY.md",
58
60
  "docs/DOCUMENTATION_GUIDE.md",
59
61
  "docs/RELEASE_CHECKLIST.md",
60
62
  "scripts/CI_VALIDATORS.md",
@@ -68,6 +70,7 @@
68
70
  "docs/PLATFORM_ADAPTERS.md",
69
71
  "docs/AGENT_PROTOCOL_SUMMARY.md",
70
72
  "docs/EXECUTION_PROFILE_BENCHMARKS.md",
73
+ "docs/KNOWLEDGE_SOURCES.md",
71
74
  "scripts/generate-shell-completions.mjs",
72
75
  "scripts/generate-agent-protocol-summary.mjs",
73
76
  "scripts/run-execution-profile-benchmarks.mjs",
@@ -12,6 +12,28 @@
12
12
  "policy": { "type": "string", "minLength": 1 },
13
13
  "requiredGates": { "type": "array", "items": { "type": "string", "minLength": 1 } },
14
14
  "requiredEvidence": { "type": "array", "items": { "type": "string", "minLength": 1 } },
15
+ "structuralQuality": {
16
+ "type": "object",
17
+ "additionalProperties": false,
18
+ "properties": {
19
+ "mode": { "enum": ["off", "observe", "gate"] },
20
+ "provider": { "type": "string", "pattern": "^[a-z][a-z0-9-]{0,63}$" },
21
+ "maxRegressionPoints": { "type": "integer", "minimum": 0, "maximum": 10000 },
22
+ "dimensionBudgets": { "$ref": "#/$defs/dimensionMap" },
23
+ "forbidNewCycles": { "type": "boolean" },
24
+ "minQualitySignal": { "oneOf": [{ "type": "integer", "minimum": 0, "maximum": 10000 }, { "type": "null" }] },
25
+ "minimums": { "$ref": "#/$defs/minimumMap" },
26
+ "optimization": {
27
+ "type": "object",
28
+ "additionalProperties": false,
29
+ "properties": {
30
+ "mode": { "enum": ["off", "bounded"] },
31
+ "maxExtraEvaluations": { "type": "integer", "minimum": 0, "maximum": 2 },
32
+ "minGainPoints": { "type": "integer", "minimum": 1, "maximum": 10000 }
33
+ }
34
+ }
35
+ }
36
+ },
15
37
  "verification": {
16
38
  "type": "object",
17
39
  "required": ["checkers"],
@@ -69,5 +91,29 @@
69
91
  }
70
92
  }
71
93
  },
94
+ "$defs": {
95
+ "dimensionMap": {
96
+ "type": "object",
97
+ "properties": {
98
+ "modularity": { "oneOf": [{ "type": "integer", "minimum": 0, "maximum": 10000 }, { "type": "null" }] },
99
+ "acyclicity": { "oneOf": [{ "type": "integer", "minimum": 0, "maximum": 10000 }, { "type": "null" }] },
100
+ "depth": { "oneOf": [{ "type": "integer", "minimum": 0, "maximum": 10000 }, { "type": "null" }] },
101
+ "equality": { "oneOf": [{ "type": "integer", "minimum": 0, "maximum": 10000 }, { "type": "null" }] },
102
+ "redundancy": { "oneOf": [{ "type": "integer", "minimum": 0, "maximum": 10000 }, { "type": "null" }] }
103
+ },
104
+ "additionalProperties": false
105
+ },
106
+ "minimumMap": {
107
+ "type": "object",
108
+ "properties": {
109
+ "modularity": { "type": "integer", "minimum": 0, "maximum": 10000 },
110
+ "acyclicity": { "type": "integer", "minimum": 0, "maximum": 10000 },
111
+ "depth": { "type": "integer", "minimum": 0, "maximum": 10000 },
112
+ "equality": { "type": "integer", "minimum": 0, "maximum": 10000 },
113
+ "redundancy": { "type": "integer", "minimum": 0, "maximum": 10000 }
114
+ },
115
+ "additionalProperties": false
116
+ }
117
+ },
72
118
  "additionalProperties": false
73
119
  }
@@ -27,6 +27,7 @@
27
27
  "phase": { "type": "string", "minLength": 1 },
28
28
  "revision": { "type": "integer", "minimum": 0 },
29
29
  "verificationCycle": { "type": "integer", "minimum": 1 },
30
+ "workStateFingerprint": { "type": "string", "pattern": "^[a-f0-9]{64}$" },
30
31
  "contractFingerprint": { "type": "string", "pattern": "^[a-f0-9]{64}$" },
31
32
  "routeFingerprint": { "oneOf": [{ "type": "string", "pattern": "^[a-f0-9]{64}$" }, { "type": "null" }] },
32
33
  "repositoryFingerprint": { "type": "object" },
@@ -17,7 +17,8 @@
17
17
  "errors": { "type": "array", "items": { "type": "object" } },
18
18
  "fingerprints": { "type": "object" },
19
19
  "sources": { "type": "object" },
20
- "policy": { "type": "object" }
20
+ "policy": { "type": "object" },
21
+ "structuralQuality": { "type": "object" }
21
22
  },
22
23
  "additionalProperties": false
23
24
  }
@@ -0,0 +1,175 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "forgeloop://schemas/structural-quality.schema.json",
4
+ "title": "ForgeLoop structural quality evidence",
5
+ "type": "object",
6
+ "required": ["schemaVersion", "protocolVersion", "role", "taskId", "capturedAt", "verificationCycle", "attempt", "bindings", "provider", "scope", "status", "reasonCodes"],
7
+ "properties": {
8
+ "schemaVersion": { "const": 1 },
9
+ "protocolVersion": { "const": 1 },
10
+ "role": { "enum": ["BASELINE", "EVALUATION"] },
11
+ "taskId": { "type": "string", "minLength": 1 },
12
+ "capturedAt": { "type": "string", "minLength": 1 },
13
+ "verificationCycle": { "oneOf": [{ "type": "integer", "minimum": 1 }, { "type": "null" }] },
14
+ "attempt": { "type": "integer", "minimum": 1 },
15
+ "status": { "enum": ["PASS", "FAIL", "BLOCKED", "NOT_OBSERVED"] },
16
+ "reasonCodes": { "type": "array", "items": { "type": "string", "minLength": 1 } },
17
+ "errorCode": { "oneOf": [{ "type": "string", "minLength": 1 }, { "type": "null" }] },
18
+ "baselineSignal": { "oneOf": [{ "type": "integer", "minimum": 0, "maximum": 10000 }, { "type": "null" }] },
19
+ "currentSignal": { "oneOf": [{ "type": "integer", "minimum": 0, "maximum": 10000 }, { "type": "null" }] },
20
+ "bindings": {
21
+ "type": "object",
22
+ "required": ["contractFingerprint", "routeFingerprint", "policyFingerprint", "scopeFingerprint"],
23
+ "properties": {
24
+ "contractFingerprint": { "type": "string", "pattern": "^[a-f0-9]{64}$" },
25
+ "routeFingerprint": { "type": "string", "pattern": "^[a-f0-9]{64}$" },
26
+ "policyFingerprint": { "type": "string", "pattern": "^[a-f0-9]{64}$" },
27
+ "scopeFingerprint": { "type": "string", "pattern": "^[a-f0-9]{64}$" },
28
+ "baselineFingerprint": { "oneOf": [{ "type": "string", "pattern": "^[a-f0-9]{64}$" }, { "type": "null" }] },
29
+ "sourceMaterialFingerprint": { "oneOf": [{ "type": "string", "pattern": "^[a-f0-9]{64}$" }, { "type": "null" }] },
30
+ "stateRevision": { "type": "integer", "minimum": 0 }
31
+ },
32
+ "additionalProperties": false
33
+ },
34
+ "sourceObservation": {
35
+ "type": "object",
36
+ "required": ["beforeFingerprint", "afterFingerprint", "stable"],
37
+ "properties": {
38
+ "beforeFingerprint": { "type": "string", "pattern": "^[a-f0-9]{64}$" },
39
+ "afterFingerprint": { "type": "string", "pattern": "^[a-f0-9]{64}$" },
40
+ "stable": { "type": "boolean" }
41
+ },
42
+ "additionalProperties": false
43
+ },
44
+ "provider": {
45
+ "type": "object",
46
+ "required": ["id", "version", "transport", "executionMode"],
47
+ "properties": {
48
+ "id": { "type": "string", "minLength": 1 },
49
+ "version": { "oneOf": [{ "type": "string", "minLength": 1 }, { "type": "null" }] },
50
+ "transport": { "type": "string", "minLength": 1 },
51
+ "executionMode": { "type": "string", "minLength": 1 },
52
+ "measurementModel": { "type": "string", "minLength": 1 },
53
+ "compatibilityKey": { "oneOf": [{ "type": "string", "minLength": 1 }, { "type": "null" }] }
54
+ },
55
+ "additionalProperties": false
56
+ },
57
+ "detection": {
58
+ "type": "object",
59
+ "required": ["available", "providerId", "transport", "reasonCode"],
60
+ "properties": {
61
+ "available": { "type": "boolean" },
62
+ "providerId": { "type": "string", "minLength": 1 },
63
+ "providerVersion": { "oneOf": [{ "type": "string", "minLength": 1 }, { "type": "null" }] },
64
+ "transport": { "type": "string", "minLength": 1 },
65
+ "measurementModel": { "type": "string", "minLength": 1 },
66
+ "compatibilityKey": { "oneOf": [{ "type": "string", "minLength": 1 }, { "type": "null" }] },
67
+ "reasonCode": { "oneOf": [{ "type": "string", "minLength": 1 }, { "type": "null" }] }
68
+ },
69
+ "additionalProperties": false
70
+ },
71
+ "scope": {
72
+ "type": "object",
73
+ "required": ["kind", "projectRoot"],
74
+ "properties": {
75
+ "kind": { "const": "PROJECT" },
76
+ "projectRoot": { "const": "." },
77
+ "providerConfigFingerprint": { "oneOf": [{ "type": "string", "pattern": "^[a-f0-9]{64}$" }, { "type": "null" }] },
78
+ "architectureRulesFingerprint": { "type": "string", "pattern": "^[a-f0-9]{64}$" }
79
+ },
80
+ "additionalProperties": false
81
+ },
82
+ "snapshot": { "$ref": "#/$defs/snapshot" },
83
+ "comparison": { "$ref": "#/$defs/comparison" }
84
+ },
85
+ "additionalProperties": false,
86
+ "$defs": {
87
+ "rootCause": {
88
+ "type": "object",
89
+ "required": ["score", "raw"],
90
+ "properties": {
91
+ "score": { "type": "integer", "minimum": 0, "maximum": 10000 },
92
+ "raw": { "type": "number" }
93
+ },
94
+ "additionalProperties": false
95
+ },
96
+ "snapshot": {
97
+ "type": "object",
98
+ "required": ["qualitySignal", "bottleneck", "rootCauses", "statistics", "diagnostics"],
99
+ "properties": {
100
+ "qualitySignal": { "type": "integer", "minimum": 0, "maximum": 10000 },
101
+ "bottleneck": { "enum": ["modularity", "acyclicity", "depth", "equality", "redundancy"] },
102
+ "rootCauses": {
103
+ "type": "object",
104
+ "required": ["modularity", "acyclicity", "depth", "equality", "redundancy"],
105
+ "properties": {
106
+ "modularity": { "$ref": "#/$defs/rootCause" },
107
+ "acyclicity": { "$ref": "#/$defs/rootCause" },
108
+ "depth": { "$ref": "#/$defs/rootCause" },
109
+ "equality": { "$ref": "#/$defs/rootCause" },
110
+ "redundancy": { "$ref": "#/$defs/rootCause" }
111
+ },
112
+ "additionalProperties": false
113
+ },
114
+ "statistics": {
115
+ "type": "object",
116
+ "required": ["files", "lines", "importEdges", "crossModuleEdges"],
117
+ "properties": {
118
+ "files": { "oneOf": [{ "type": "integer", "minimum": 0 }, { "type": "null" }] },
119
+ "lines": { "oneOf": [{ "type": "integer", "minimum": 0 }, { "type": "null" }] },
120
+ "importEdges": { "oneOf": [{ "type": "integer", "minimum": 0 }, { "type": "null" }] },
121
+ "crossModuleEdges": { "oneOf": [{ "type": "integer", "minimum": 0 }, { "type": "null" }] }
122
+ },
123
+ "additionalProperties": false
124
+ },
125
+ "diagnostics": { "oneOf": [{ "type": "object" }, { "type": "null" }] }
126
+ },
127
+ "additionalProperties": false
128
+ },
129
+ "comparison": {
130
+ "type": "object",
131
+ "required": ["comparable", "qualityDelta", "rootCauseDeltas", "status", "reasonCodes"],
132
+ "properties": {
133
+ "comparable": { "type": "boolean" },
134
+ "qualityDelta": { "oneOf": [{ "type": "integer" }, { "type": "null" }] },
135
+ "rootCauseDeltas": { "type": "object" },
136
+ "failedConditions": { "type": "array", "items": { "type": "string", "minLength": 1 } },
137
+ "status": { "enum": ["PASS", "FAIL", "BLOCKED", "NOT_OBSERVED"] },
138
+ "reasonCodes": { "type": "array", "items": { "type": "string", "minLength": 1 } }
139
+ },
140
+ "additionalProperties": false
141
+ }
142
+ },
143
+ "allOf": [
144
+ {
145
+ "if": { "properties": { "role": { "const": "BASELINE" } } },
146
+ "then": {
147
+ "required": ["sourceObservation"],
148
+ "properties": {
149
+ "bindings": {
150
+ "required": ["sourceMaterialFingerprint"],
151
+ "properties": {
152
+ "sourceMaterialFingerprint": { "type": "string", "pattern": "^[a-f0-9]{64}$" }
153
+ }
154
+ },
155
+ "sourceObservation": { "properties": { "stable": { "const": true } } }
156
+ }
157
+ }
158
+ },
159
+ {
160
+ "if": { "properties": { "status": { "enum": ["PASS", "FAIL"] } } },
161
+ "then": {
162
+ "required": ["sourceObservation"],
163
+ "properties": {
164
+ "bindings": {
165
+ "required": ["sourceMaterialFingerprint"],
166
+ "properties": {
167
+ "sourceMaterialFingerprint": { "type": "string", "pattern": "^[a-f0-9]{64}$" }
168
+ }
169
+ },
170
+ "sourceObservation": { "properties": { "stable": { "const": true } } }
171
+ }
172
+ }
173
+ }
174
+ ]
175
+ }
@@ -26,6 +26,26 @@ export function unreleasedSection(changelog) {
26
26
  return nextHeading ? rest.slice(0, nextHeading.index) : rest;
27
27
  }
28
28
 
29
+ export function releaseSection(changelog) {
30
+ const heading = /^##[ \t]+(v?\d+\.\d+\.\d+)(?:[ \t]+-[^\r\n]*)?$/imu.exec(changelog);
31
+ if (!heading) return null;
32
+ const rest = changelog.slice(heading.index + heading[0].length);
33
+ const nextHeading = /^##\s/imu.exec(rest);
34
+ return {
35
+ version: heading[1].replace(/^v/u, ""),
36
+ section: nextHeading ? rest.slice(0, nextHeading.index) : rest,
37
+ };
38
+ }
39
+
40
+ function compareVersions(left, right) {
41
+ const leftParts = left.split(".").map(Number);
42
+ const rightParts = right.split(".").map(Number);
43
+ for (let index = 0; index < leftParts.length; index += 1) {
44
+ if (leftParts[index] !== rightParts[index]) return leftParts[index] - rightParts[index];
45
+ }
46
+ return 0;
47
+ }
48
+
29
49
  export function stripHtmlComments(value) {
30
50
  const chunks = [];
31
51
  let cursor = 0;
@@ -55,11 +75,15 @@ export function hasMeaningfulUnreleasedContent(section) {
55
75
 
56
76
  export function checkChangelogFreshness({ changelog, tags = [], commitsSinceLatestTag = 0, allowEmpty = false } = {}) {
57
77
  const latestTag = latestReleaseTag(tags);
58
- const section = unreleasedSection(changelog);
78
+ const unreleased = unreleasedSection(changelog);
79
+ const pendingRelease = releaseSection(changelog);
80
+ const pendingReleaseIsNewer = pendingRelease &&
81
+ (!latestTag || compareVersions(pendingRelease.version, latestTag.replace(/^v/u, "")) > 0);
82
+ const section = unreleased || (pendingReleaseIsNewer ? pendingRelease.section : "");
59
83
  const hasChanges = commitsSinceLatestTag > 0;
60
84
  const populated = hasMeaningfulUnreleasedContent(section);
61
85
  const ok = allowEmpty || !hasChanges || populated;
62
- return { ok, latestTag, hasChanges, populated, section };
86
+ return { ok, latestTag, hasChanges, populated, section, pendingRelease };
63
87
  }
64
88
 
65
89
  async function run() {
@@ -71,7 +95,7 @@ async function run() {
71
95
  : Number(git(["rev-list", "--count", "HEAD"]));
72
96
  const result = checkChangelogFreshness({ changelog, tags, commitsSinceLatestTag, allowEmpty: process.argv.includes("--allow-empty") });
73
97
  if (!result.ok) {
74
- console.error(`CHANGELOG.md has changes since ${result.latestTag ?? "the initial commit"} but the Unreleased section is empty.`);
98
+ console.error(`CHANGELOG.md has changes since ${result.latestTag ?? "the initial commit"} but no populated Unreleased or pending release section exists.`);
75
99
  process.exitCode = 1;
76
100
  return;
77
101
  }