loopforge 3.9.2 → 4.0.7
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.
- package/README.md +236 -112
- package/dist/audit.d.ts +73 -15
- package/dist/audit.d.ts.map +1 -1
- package/dist/audit.js +131 -61
- package/dist/audit.js.map +1 -1
- package/dist/canonical-state.d.ts +152 -40
- package/dist/canonical-state.d.ts.map +1 -1
- package/dist/canonical-state.js +382 -206
- package/dist/canonical-state.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +41 -12
- package/dist/cli.js.map +1 -1
- package/dist/cognitive-governance.d.ts +0 -2
- package/dist/cognitive-governance.d.ts.map +1 -1
- package/dist/cognitive-governance.js +0 -4
- package/dist/cognitive-governance.js.map +1 -1
- package/dist/committed-round.d.ts +105 -141
- package/dist/committed-round.d.ts.map +1 -1
- package/dist/committed-round.js +249 -372
- package/dist/committed-round.js.map +1 -1
- package/dist/constraint-lifecycle.d.ts +73 -0
- package/dist/constraint-lifecycle.d.ts.map +1 -0
- package/dist/constraint-lifecycle.js +174 -0
- package/dist/constraint-lifecycle.js.map +1 -0
- package/dist/context-boundary.d.ts +83 -0
- package/dist/context-boundary.d.ts.map +1 -0
- package/dist/context-boundary.js +101 -0
- package/dist/context-boundary.js.map +1 -0
- package/dist/contract-items.d.ts +12 -8
- package/dist/contract-items.d.ts.map +1 -1
- package/dist/contract-items.js +31 -26
- package/dist/contract-items.js.map +1 -1
- package/dist/contract-requirement.d.ts +47 -0
- package/dist/contract-requirement.d.ts.map +1 -0
- package/dist/contract-requirement.js +51 -0
- package/dist/contract-requirement.js.map +1 -0
- package/dist/criterion-status.d.ts +35 -0
- package/dist/criterion-status.d.ts.map +1 -0
- package/dist/criterion-status.js +89 -0
- package/dist/criterion-status.js.map +1 -0
- package/dist/enforcement-gate.d.ts +32 -67
- package/dist/enforcement-gate.d.ts.map +1 -1
- package/dist/enforcement-gate.js +203 -439
- package/dist/enforcement-gate.js.map +1 -1
- package/dist/engine.d.ts +9 -103
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +28 -887
- package/dist/engine.js.map +1 -1
- package/dist/errors.d.ts +41 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +69 -0
- package/dist/errors.js.map +1 -0
- package/dist/evidence-claims.d.ts +33 -31
- package/dist/evidence-claims.d.ts.map +1 -1
- package/dist/evidence-claims.js +58 -72
- package/dist/evidence-claims.js.map +1 -1
- package/dist/evidence-posture-view.d.ts +19 -0
- package/dist/evidence-posture-view.d.ts.map +1 -0
- package/dist/evidence-posture-view.js +116 -0
- package/dist/evidence-posture-view.js.map +1 -0
- package/dist/evidence-posture.d.ts +73 -0
- package/dist/evidence-posture.d.ts.map +1 -0
- package/dist/evidence-posture.js +281 -0
- package/dist/evidence-posture.js.map +1 -0
- package/dist/evidence-provider.d.ts +55 -37
- package/dist/evidence-provider.d.ts.map +1 -1
- package/dist/evidence-provider.js +182 -332
- package/dist/evidence-provider.js.map +1 -1
- package/dist/explain.d.ts +167 -27
- package/dist/explain.d.ts.map +1 -1
- package/dist/explain.js +373 -58
- package/dist/explain.js.map +1 -1
- package/dist/field-accounting.d.ts.map +1 -1
- package/dist/field-accounting.js +3 -22
- package/dist/field-accounting.js.map +1 -1
- package/dist/generate-schema.js +13 -8
- package/dist/generate-schema.js.map +1 -1
- package/dist/git-boundary.d.ts +128 -0
- package/dist/git-boundary.d.ts.map +1 -0
- package/dist/git-boundary.js +625 -0
- package/dist/git-boundary.js.map +1 -0
- package/dist/git-facts.d.ts +22 -0
- package/dist/git-facts.d.ts.map +1 -0
- package/dist/git-facts.js +29 -0
- package/dist/git-facts.js.map +1 -0
- package/dist/git-motion.d.ts +37 -0
- package/dist/git-motion.d.ts.map +1 -0
- package/dist/git-motion.js +59 -0
- package/dist/git-motion.js.map +1 -0
- package/dist/history.d.ts +44 -0
- package/dist/history.d.ts.map +1 -0
- package/dist/history.js +77 -0
- package/dist/history.js.map +1 -0
- package/dist/index.d.ts +19 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -4
- package/dist/index.js.map +1 -1
- package/dist/init.d.ts +16 -0
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +80 -14
- package/dist/init.js.map +1 -1
- package/dist/loop-compiler.d.ts +51 -19
- package/dist/loop-compiler.d.ts.map +1 -1
- package/dist/loop-compiler.js +423 -507
- package/dist/loop-compiler.js.map +1 -1
- package/dist/loop-store.d.ts +6 -5
- package/dist/loop-store.d.ts.map +1 -1
- package/dist/loop-store.js +153 -35
- package/dist/loop-store.js.map +1 -1
- package/dist/mcp/round-lifecycle.d.ts +285 -81
- package/dist/mcp/round-lifecycle.d.ts.map +1 -1
- package/dist/mcp/round-lifecycle.js +1629 -574
- package/dist/mcp/round-lifecycle.js.map +1 -1
- package/dist/mcp/server.d.ts +5 -0
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +61 -19
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/session.d.ts +81 -39
- package/dist/mcp/session.d.ts.map +1 -1
- package/dist/mcp/session.js +963 -271
- package/dist/mcp/session.js.map +1 -1
- package/dist/mcp/submission-boundary.d.ts +23 -31
- package/dist/mcp/submission-boundary.d.ts.map +1 -1
- package/dist/mcp/submission-boundary.js +242 -82
- package/dist/mcp/submission-boundary.js.map +1 -1
- package/dist/mcp/submission-handle.d.ts +66 -0
- package/dist/mcp/submission-handle.d.ts.map +1 -0
- package/dist/mcp/submission-handle.js +173 -0
- package/dist/mcp/submission-handle.js.map +1 -0
- package/dist/mcp/tools.d.ts +17 -7
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +763 -244
- package/dist/mcp/tools.js.map +1 -1
- package/dist/open-question.d.ts +7 -0
- package/dist/open-question.d.ts.map +1 -0
- package/dist/open-question.js +14 -0
- package/dist/open-question.js.map +1 -0
- package/dist/policy-metrics.d.ts +31 -21
- package/dist/policy-metrics.d.ts.map +1 -1
- package/dist/policy-metrics.js +117 -64
- package/dist/policy-metrics.js.map +1 -1
- package/dist/policy.d.ts +15 -40
- package/dist/policy.d.ts.map +1 -1
- package/dist/policy.js +13 -13
- package/dist/policy.js.map +1 -1
- package/dist/progress.d.ts +45 -0
- package/dist/progress.d.ts.map +1 -0
- package/dist/progress.js +136 -0
- package/dist/progress.js.map +1 -0
- package/dist/prompt-assembler.d.ts +28 -9
- package/dist/prompt-assembler.d.ts.map +1 -1
- package/dist/prompt-assembler.js +592 -603
- package/dist/prompt-assembler.js.map +1 -1
- package/dist/prompt-coverage.d.ts +39 -0
- package/dist/prompt-coverage.d.ts.map +1 -0
- package/dist/prompt-coverage.js +80 -0
- package/dist/prompt-coverage.js.map +1 -0
- package/dist/prompt-freshness.d.ts +25 -0
- package/dist/prompt-freshness.d.ts.map +1 -0
- package/dist/prompt-freshness.js +88 -0
- package/dist/prompt-freshness.js.map +1 -0
- package/dist/prompt-policy.d.ts +14 -4
- package/dist/prompt-policy.d.ts.map +1 -1
- package/dist/prompt-policy.js +29 -10
- package/dist/prompt-policy.js.map +1 -1
- package/dist/protocol.d.ts +875 -419
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +73 -99
- package/dist/protocol.js.map +1 -1
- package/dist/recovery-guidance.d.ts +13 -0
- package/dist/recovery-guidance.d.ts.map +1 -0
- package/dist/recovery-guidance.js +15 -0
- package/dist/recovery-guidance.js.map +1 -0
- package/dist/recovery.d.ts +68 -0
- package/dist/recovery.d.ts.map +1 -0
- package/dist/recovery.js +234 -0
- package/dist/recovery.js.map +1 -0
- package/dist/replay.d.ts +42 -8
- package/dist/replay.d.ts.map +1 -1
- package/dist/replay.js +311 -84
- package/dist/replay.js.map +1 -1
- package/dist/result-facts.d.ts +45 -0
- package/dist/result-facts.d.ts.map +1 -0
- package/dist/result-facts.js +86 -0
- package/dist/result-facts.js.map +1 -0
- package/dist/round-contract.d.ts +38 -8
- package/dist/round-contract.d.ts.map +1 -1
- package/dist/round-contract.js +71 -31
- package/dist/round-contract.js.map +1 -1
- package/dist/round-coordinator.d.ts +45 -91
- package/dist/round-coordinator.d.ts.map +1 -1
- package/dist/round-coordinator.js +254 -271
- package/dist/round-coordinator.js.map +1 -1
- package/dist/round-driver.d.ts +56 -16
- package/dist/round-driver.d.ts.map +1 -1
- package/dist/round-driver.js +225 -39
- package/dist/round-driver.js.map +1 -1
- package/dist/round-facts.d.ts +33 -52
- package/dist/round-facts.d.ts.map +1 -1
- package/dist/round-facts.js +54 -157
- package/dist/round-facts.js.map +1 -1
- package/dist/round-guidance.d.ts +56 -0
- package/dist/round-guidance.d.ts.map +1 -0
- package/dist/round-guidance.js +161 -0
- package/dist/round-guidance.js.map +1 -0
- package/dist/round-outcome.d.ts +21 -0
- package/dist/round-outcome.d.ts.map +1 -0
- package/dist/round-outcome.js +31 -0
- package/dist/round-outcome.js.map +1 -0
- package/dist/round-transaction.d.ts +168 -21
- package/dist/round-transaction.d.ts.map +1 -1
- package/dist/round-transaction.js +856 -39
- package/dist/round-transaction.js.map +1 -1
- package/dist/self-eval.d.ts +40 -60
- package/dist/self-eval.d.ts.map +1 -1
- package/dist/self-eval.js +455 -384
- package/dist/self-eval.js.map +1 -1
- package/dist/storage.d.ts +16 -0
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +54 -4
- package/dist/storage.js.map +1 -1
- package/dist/subgoal-state.d.ts +2 -3
- package/dist/subgoal-state.d.ts.map +1 -1
- package/dist/subgoal-state.js +2 -3
- package/dist/subgoal-state.js.map +1 -1
- package/dist/submission-guidance.d.ts +43 -21
- package/dist/submission-guidance.d.ts.map +1 -1
- package/dist/submission-guidance.js +368 -262
- package/dist/submission-guidance.js.map +1 -1
- package/dist/submission-receipt.d.ts +16 -8
- package/dist/submission-receipt.d.ts.map +1 -1
- package/dist/submission-receipt.js +146 -51
- package/dist/submission-receipt.js.map +1 -1
- package/dist/token-utils.d.ts +47 -7
- package/dist/token-utils.d.ts.map +1 -1
- package/dist/token-utils.js +66 -15
- package/dist/token-utils.js.map +1 -1
- package/dist/transaction-identity.d.ts +9 -0
- package/dist/transaction-identity.d.ts.map +1 -0
- package/dist/transaction-identity.js +69 -0
- package/dist/transaction-identity.js.map +1 -0
- package/dist/verification-actions.d.ts +13 -0
- package/dist/verification-actions.d.ts.map +1 -0
- package/dist/verification-actions.js +44 -0
- package/dist/verification-actions.js.map +1 -0
- package/dist/verification-gate.d.ts +26 -46
- package/dist/verification-gate.d.ts.map +1 -1
- package/dist/verification-gate.js +218 -427
- package/dist/verification-gate.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/working-memory.d.ts +187 -0
- package/dist/working-memory.d.ts.map +1 -0
- package/dist/working-memory.js +767 -0
- package/dist/working-memory.js.map +1 -0
- package/dist/workspace-checkpoint.d.ts +88 -0
- package/dist/workspace-checkpoint.d.ts.map +1 -0
- package/dist/workspace-checkpoint.js +326 -0
- package/dist/workspace-checkpoint.js.map +1 -0
- package/dist/workspace-restore.d.ts +68 -0
- package/dist/workspace-restore.d.ts.map +1 -0
- package/dist/workspace-restore.js +604 -0
- package/dist/workspace-restore.js.map +1 -0
- package/dist/workspace.d.ts +0 -9
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +0 -17
- package/dist/workspace.js.map +1 -1
- package/loop_policy.json +5 -19
- package/package.json +2 -2
- package/skills/forge/SKILL.md +212 -0
- package/skills/forge/references/evidence-and-contracts.md +204 -0
- package/skills/forge/references/recovery-and-views.md +188 -0
- package/dist/cognitive-facts.d.ts +0 -30
- package/dist/cognitive-facts.d.ts.map +0 -1
- package/dist/cognitive-facts.js +0 -113
- package/dist/cognitive-facts.js.map +0 -1
- package/dist/loop-extras-parser.d.ts +0 -81
- package/dist/loop-extras-parser.d.ts.map +0 -1
- package/dist/loop-extras-parser.js +0 -213
- package/dist/loop-extras-parser.js.map +0 -1
- package/dist/loop-projection.d.ts +0 -6
- package/dist/loop-projection.d.ts.map +0 -1
- package/dist/loop-projection.js +0 -28
- package/dist/loop-projection.js.map +0 -1
- package/skills/perception/SKILL.md +0 -370
package/dist/loop-compiler.js
CHANGED
|
@@ -5,191 +5,186 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { createHash } from "node:crypto";
|
|
7
7
|
import { deriveConfiguredCapability, getPolicy } from "./policy.js";
|
|
8
|
-
import { AgentStatus, makeLoopCompileResponse, makeLoopObjective, makeConstraintMeta,
|
|
8
|
+
import { AgentStatus, makeLoopCompileResponse, makeLoopObjective, makeConstraintMeta, } from "./protocol.js";
|
|
9
9
|
import { createCanonicalLoopState, renderCanonicalStateMarkdown, } from "./canonical-state.js";
|
|
10
|
-
import {
|
|
10
|
+
import { pendingBacktrackRecord, roundResultFromCommittedRound, } from "./committed-round.js";
|
|
11
|
+
import { buildCommittedHistory, historyWindow, operatorAcceptanceKey, } from "./history.js";
|
|
12
|
+
import { LegacyHistoryError, OperatorAcceptanceConflictError } from "./errors.js";
|
|
13
|
+
import { deriveProgressWindow, } from "./progress.js";
|
|
14
|
+
import { deriveCriterionStatuses as deriveCriterionStatusesFromViews } from "./criterion-status.js";
|
|
15
|
+
import { derivePromptFreshness, UNKNOWN_PROMPT_FRESHNESS, } from "./prompt-freshness.js";
|
|
11
16
|
import { NO_IN_FLIGHT_ROUND, deriveRoundFacts } from "./round-facts.js";
|
|
12
17
|
import { recoveryBriefLines } from "./enforcement-gate.js";
|
|
13
18
|
import { deriveSubGoals, duplicateEmergedDeclarations } from "./subgoal-state.js";
|
|
14
|
-
import {
|
|
19
|
+
import { deriveConstraintProvenance, resolveConstraintRetractions, } from "./constraint-lifecycle.js";
|
|
15
20
|
import { assemblePromptArtifact } from "./prompt-assembler.js";
|
|
21
|
+
import { derivePromptContextBoundary } from "./context-boundary.js";
|
|
22
|
+
import { deriveWorkingMemory } from "./working-memory.js";
|
|
16
23
|
import { renderSelfEvalBlock, } from "./submission-guidance.js";
|
|
17
24
|
import { decidePromptLevel, } from "./prompt-policy.js";
|
|
18
|
-
import { criterionRefMatches,
|
|
25
|
+
import { criterionRefMatches, deriveConstraintId, deriveCriterionId, normalizeText, unique, uniqueNormalized, } from "./token-utils.js";
|
|
19
26
|
function contextEntries(context) {
|
|
20
27
|
if (!context || !Array.isArray(context.results))
|
|
21
28
|
return [];
|
|
22
29
|
return context.results.filter((value) => value !== null && typeof value === "object" && !Array.isArray(value));
|
|
23
30
|
}
|
|
24
|
-
function loopEntries(loopId, context) {
|
|
25
|
-
return contextEntries(context)
|
|
26
|
-
.filter((entry) => entry.loop_id === loopId || entryLineage(entry).loop_id === loopId)
|
|
27
|
-
.sort((a, b) => entryRound(a) - entryRound(b));
|
|
28
|
-
}
|
|
29
|
-
/** v3.3.1: The canonical entry for a round — its compile-time lineage entry
|
|
30
|
-
* (task_type "loop_lineage"; engine hydration merges the committed
|
|
31
|
-
* decision into it, so one round = one merged lineage entry in the
|
|
32
|
-
* compiler's view). Event/journal entries (delegation journals, gate
|
|
33
|
-
* records) share lineage.round with the round but carry none of its
|
|
34
|
-
* compile-time fields; round-level consumers that matched on the round
|
|
35
|
-
* number alone could hit them first (they sort after the lineage entry of
|
|
36
|
-
* the same round), producing an empty goal_id / task / constraints_active
|
|
37
|
-
* and forcing spurious L2 recovery. A direct :feedback entry is accepted
|
|
38
|
-
* when the caller supplies a raw committed view instead of hydration. */
|
|
39
|
-
function roundCanonicalEntry(entries, round) {
|
|
40
|
-
const candidates = entries.filter((entry) => entryRound(entry) === round);
|
|
41
|
-
return (candidates.find((entry) => entry.task_type === "loop_lineage") ??
|
|
42
|
-
candidates.find((entry) => String(entry.task_id ?? "").endsWith(":feedback")) ??
|
|
43
|
-
// Untyped compiler inputs are treated as round views, not events. Known event
|
|
44
|
-
// types (delegation journals, gate records) are excluded explicitly so
|
|
45
|
-
// they can never shadow a round again.
|
|
46
|
-
candidates.find((entry) => {
|
|
47
|
-
const type = String(entry.task_type ?? "");
|
|
48
|
-
return type === "" || (!type.startsWith("delegation") && !type.endsWith("journal"));
|
|
49
|
-
}) ??
|
|
50
|
-
null);
|
|
51
|
-
}
|
|
52
31
|
export function computeGoalTextHash(text) {
|
|
53
32
|
const normalized = text.trim().replace(/\s+/g, " ").toLowerCase();
|
|
54
33
|
return createHash("sha256").update(normalized).digest("hex").slice(0, 12);
|
|
55
34
|
}
|
|
56
|
-
|
|
57
|
-
|
|
35
|
+
/** v4.0.6: goal identity derives from the WORKING objective (the latest
|
|
36
|
+
* committed objective_refinement, else Operator Acceptance). There is no
|
|
37
|
+
* caller-supplied goal_id override. */
|
|
38
|
+
export function deriveGoalId(loopId, task) {
|
|
39
|
+
const slug = task.trim().toLowerCase()
|
|
58
40
|
.replace(/[^a-z0-9\u4e00-\u9fff]+/g, "-")
|
|
59
41
|
.replace(/^-|-$/g, "")
|
|
60
|
-
.slice(0, 48)
|
|
42
|
+
.slice(0, 48);
|
|
43
|
+
// Keep the readable prefix while making the identity depend on the full
|
|
44
|
+
// normalized objective; shared prefixes must not collapse distinct goals.
|
|
45
|
+
return `${loopId}:${slug || "goal"}:${computeGoalTextHash(task)}`;
|
|
61
46
|
}
|
|
62
47
|
export function getPreviousRound(loopId, round, context) {
|
|
63
|
-
const
|
|
64
|
-
|
|
48
|
+
const history = buildCommittedHistory(contextEntries(context));
|
|
49
|
+
return previousRoundFromWindow(historyWindow(history, Number.POSITIVE_INFINITY), round);
|
|
50
|
+
}
|
|
51
|
+
function previousRoundFromWindow(window, round) {
|
|
52
|
+
const view = window.find((candidate) => candidate.round === round);
|
|
53
|
+
if (!view)
|
|
65
54
|
return null;
|
|
66
|
-
const data = entryLineage(entry);
|
|
67
55
|
return {
|
|
68
56
|
round,
|
|
69
|
-
goal_id:
|
|
70
|
-
goal_text_hash: typeof data.goal_text_hash === "string" ? data.goal_text_hash : "",
|
|
71
|
-
success: typeof entry.success === "boolean"
|
|
72
|
-
? entry.success
|
|
73
|
-
: typeof data.success === "boolean" ? data.success : false,
|
|
74
|
-
task: typeof data.task === "string"
|
|
75
|
-
? data.task
|
|
76
|
-
: typeof entry.task === "string" ? entry.task : "",
|
|
77
|
-
constraints_active: Array.isArray(data.constraints_active)
|
|
78
|
-
? data.constraints_active.filter((value) => typeof value === "string")
|
|
79
|
-
: [],
|
|
80
|
-
output_summary: typeof entry.output_summary === "string"
|
|
81
|
-
? entry.output_summary
|
|
82
|
-
: typeof data.output_summary === "string" ? data.output_summary : "",
|
|
57
|
+
goal_id: view.compiled?.goalId ?? "",
|
|
83
58
|
};
|
|
84
59
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
60
|
+
/** v4.0.6: THE working objective.
|
|
61
|
+
*
|
|
62
|
+
* - The immutable operator surface is the committed `OperatorAcceptance`.
|
|
63
|
+
* Acceptance is byte-identical across copies (enforced by the history
|
|
64
|
+
* loader), so it does not need a round or branch window.
|
|
65
|
+
* - The objective TEXT is the latest committed `objective_refinement` on the
|
|
66
|
+
* CURRENT BRANCH, else the acceptance task. A refinement from a branch
|
|
67
|
+
* that was rolled back never applies.
|
|
68
|
+
* - Success criteria and hard constraints ALWAYS come from the acceptance; a
|
|
69
|
+
* refinement can never rewrite them.
|
|
70
|
+
*
|
|
71
|
+
* Read-only callers that only need criteria/hard constraints read those
|
|
72
|
+
* fields; they must not treat `objective` as the operator acceptance text. */
|
|
73
|
+
export function latestObjective(loopId, context) {
|
|
74
|
+
const history = buildCommittedHistory(contextEntries(context));
|
|
75
|
+
const acceptanceView = history.rounds.find((view) => view.operatorAcceptance) ??
|
|
76
|
+
history.views.find((view) => view.operatorAcceptance);
|
|
77
|
+
if (!acceptanceView?.operatorAcceptance)
|
|
78
|
+
return null;
|
|
79
|
+
const acceptance = acceptanceView.operatorAcceptance;
|
|
80
|
+
let objective = acceptance.task;
|
|
81
|
+
for (const view of history.rounds) {
|
|
82
|
+
const refinement = view.objectiveRefinement;
|
|
83
|
+
if (typeof refinement === "string" && refinement.trim().length > 0) {
|
|
84
|
+
objective = refinement;
|
|
85
|
+
}
|
|
107
86
|
}
|
|
108
|
-
return
|
|
87
|
+
return makeLoopObjective({
|
|
88
|
+
objective,
|
|
89
|
+
success_criteria: [...acceptance.success_criteria],
|
|
90
|
+
hard_constraints: [...acceptance.hard_constraints],
|
|
91
|
+
created_at_round: 1,
|
|
92
|
+
loop_id: loopId,
|
|
93
|
+
});
|
|
109
94
|
}
|
|
110
|
-
function evolveObjective(request, context) {
|
|
111
|
-
|
|
95
|
+
function evolveObjective(request, context, operatorAcceptance) {
|
|
96
|
+
// v3.9.7: committed acceptance is authoritative once it exists. A transient
|
|
97
|
+
// request may bootstrap the objective for round 1 or a library caller with
|
|
98
|
+
// no committed objective; it may not rewrite operator acceptance after a
|
|
99
|
+
// committed objective exists.
|
|
100
|
+
const committed = latestObjective(request.loop_id, context);
|
|
101
|
+
const allowRequestObjective = request.loop_objective !== null &&
|
|
102
|
+
(request.round === 1 || committed === null);
|
|
103
|
+
// v4.0.6: the accepted objective's TEXT is the working objective
|
|
104
|
+
// (committed refinement or acceptance task); criteria/hard constraints come
|
|
105
|
+
// from the immutable acceptance. The refinement never rewrites acceptance.
|
|
106
|
+
const accepted = operatorAcceptance
|
|
107
|
+
? makeLoopObjective({
|
|
108
|
+
objective: committed?.objective || operatorAcceptance.task,
|
|
109
|
+
success_criteria: [...operatorAcceptance.success_criteria],
|
|
110
|
+
hard_constraints: [...operatorAcceptance.hard_constraints],
|
|
111
|
+
created_at_round: 1,
|
|
112
|
+
loop_id: request.loop_id,
|
|
113
|
+
})
|
|
114
|
+
: null;
|
|
115
|
+
const current = accepted ??
|
|
116
|
+
(allowRequestObjective ? request.loop_objective : null) ??
|
|
117
|
+
committed ??
|
|
112
118
|
makeLoopObjective({
|
|
113
119
|
objective: request.task,
|
|
114
120
|
success_criteria: ["Task goal achieved with verifiable evidence"],
|
|
115
|
-
hard_constraints: unique(request.constraints_from_plan),
|
|
121
|
+
hard_constraints: unique(request.round === 1 || committed === null ? request.constraints_from_plan : []),
|
|
116
122
|
created_at_round: 1,
|
|
117
123
|
loop_id: request.loop_id,
|
|
118
124
|
});
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
let objective = current.objective || request.task;
|
|
122
|
-
let version = current.version ?? 1;
|
|
123
|
-
if (last?.objective_refinement?.trim()) {
|
|
124
|
-
const refinement = last.objective_refinement.trim();
|
|
125
|
-
// M5: fold each refinement exactly once per persisted objective state.
|
|
126
|
-
// A retry recompiles the same round against the same committed
|
|
127
|
-
// last_round_result; without this guard every retry attempt re-appends
|
|
128
|
-
// the refinement and inflates the version — the objective text and the
|
|
129
|
-
// state file grew "Refinement: X" once per attempt.
|
|
130
|
-
if (!history.includes(refinement)) {
|
|
131
|
-
history.push(refinement);
|
|
132
|
-
objective = `${objective}\nRefinement: ${refinement}`;
|
|
133
|
-
version++;
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
const revisions = new Map((last?.revised_success_criteria ?? []).map((item) => [item.old, item.new]));
|
|
125
|
+
// The operator objective is the committed acceptance; no Agent report
|
|
126
|
+
// rewrites it.
|
|
137
127
|
return makeLoopObjective({
|
|
138
128
|
...current,
|
|
139
|
-
objective,
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
success_criteria: unique(current.success_criteria.map((item) => revisions.get(item) ?? item)),
|
|
143
|
-
hard_constraints: unique(current.hard_constraints),
|
|
129
|
+
objective: current.objective || request.task,
|
|
130
|
+
success_criteria: uniqueNormalized(current.success_criteria),
|
|
131
|
+
hard_constraints: uniqueNormalized(current.hard_constraints),
|
|
144
132
|
loop_id: request.loop_id,
|
|
145
133
|
});
|
|
146
134
|
}
|
|
147
|
-
function evolveConstraints(request, objective, previous,
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
const active = unique([
|
|
178
|
-
...(previous?.constraints_active ?? []),
|
|
179
|
-
...request.constraints_from_plan,
|
|
135
|
+
function evolveConstraints(request, objective, previous, windowRounds, roundOnePlan, preservedDiscoveries, preservedRound) {
|
|
136
|
+
// The active set at the START of the round that produced the retraction:
|
|
137
|
+
// the previous committed compile's stamped `constraints_active`.
|
|
138
|
+
const carried = uniqueNormalized(previous.view?.activeConstraints ?? []);
|
|
139
|
+
const activeTargets = carried.map((text) => ({
|
|
140
|
+
text,
|
|
141
|
+
provenance: deriveConstraintProvenance({
|
|
142
|
+
text,
|
|
143
|
+
objective,
|
|
144
|
+
windowRounds,
|
|
145
|
+
roundOnePlan,
|
|
146
|
+
preservedDiscoveries,
|
|
147
|
+
preservedRound,
|
|
148
|
+
}),
|
|
149
|
+
}));
|
|
150
|
+
const previouslyRetired = unique(windowRounds.flatMap((round) => round.retractedConstraints ?? []));
|
|
151
|
+
const retraction = resolveConstraintRetractions({
|
|
152
|
+
targets: previous.result?.retracted_constraints ?? [],
|
|
153
|
+
active: activeTargets,
|
|
154
|
+
// The compiler does not see this submission's new discoveries; the
|
|
155
|
+
// boundary resolves the same target set and reports same-round cases.
|
|
156
|
+
newDiscoveries: [],
|
|
157
|
+
previouslyRetired,
|
|
158
|
+
});
|
|
159
|
+
const effectiveKeys = new Set(retraction.effective.map(normalizeText));
|
|
160
|
+
// Only round 1 accepts a plan input. After round 1 the active set is carried
|
|
161
|
+
// from committed compile facts; a late request value is not a fact.
|
|
162
|
+
const base = uniqueNormalized([
|
|
163
|
+
...carried,
|
|
164
|
+
...roundOnePlan,
|
|
180
165
|
...objective.hard_constraints,
|
|
181
166
|
...objective.success_criteria,
|
|
182
|
-
...
|
|
183
|
-
]).filter((
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
167
|
+
...preservedDiscoveries,
|
|
168
|
+
]).filter((text) => !effectiveKeys.has(normalizeText(text)));
|
|
169
|
+
// Only the previous round's NEW discoveries are added after retraction:
|
|
170
|
+
// a new declaration in the retraction round survives the retraction.
|
|
171
|
+
const allDiscovered = unique(previous.result?.discovered_constraints ?? []);
|
|
172
|
+
const discovered = allDiscovered
|
|
173
|
+
.slice(0, getPolicy().evolution.max_discovered_constraints_per_round);
|
|
174
|
+
const discoveryDropped = allDiscovered.length - discovered.length;
|
|
175
|
+
const active = uniqueNormalized([...base, ...discovered]);
|
|
176
|
+
const provenance = new Map();
|
|
177
|
+
for (const text of active) {
|
|
178
|
+
provenance.set(text, deriveConstraintProvenance({
|
|
179
|
+
text,
|
|
180
|
+
objective,
|
|
181
|
+
windowRounds,
|
|
182
|
+
roundOnePlan,
|
|
183
|
+
preservedDiscoveries,
|
|
184
|
+
preservedRound,
|
|
185
|
+
}));
|
|
186
|
+
}
|
|
187
|
+
return { active, retired: retraction.effective, provenance, discoveryDropped };
|
|
193
188
|
}
|
|
194
189
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
195
190
|
// Hierarchical Summary (v2.1)
|
|
@@ -244,18 +239,27 @@ export function deriveRecurringFlags(committedRounds) {
|
|
|
244
239
|
* — the dashboard then shows no machine row. Display-only companion to the
|
|
245
240
|
* R4/R5 exculpatory signal. */
|
|
246
241
|
function deriveMachineStatus(rounds, currentRound) {
|
|
247
|
-
|
|
248
|
-
|
|
242
|
+
// v3.9.9: the dashboard shows the SAME fact the stall rule consumes.
|
|
243
|
+
const window = deriveProgressWindow(rounds, currentRound, getPolicy().engine.stall_lookback_rounds);
|
|
244
|
+
if (window.incomplete || window.state === "not_applicable")
|
|
249
245
|
return undefined;
|
|
250
|
-
const
|
|
251
|
-
return {
|
|
246
|
+
const changedRounds = window.roundStates.filter((entry) => entry.state === "changed").length;
|
|
247
|
+
return {
|
|
248
|
+
windowRounds: window.lookback,
|
|
249
|
+
machineProgress: window.state,
|
|
250
|
+
changedRounds,
|
|
251
|
+
providers: window.providers,
|
|
252
|
+
};
|
|
252
253
|
}
|
|
253
254
|
/** v3.2: Derive per-criterion status — the "goal → criteria → evidence"
|
|
254
|
-
* vertical view. Each objective criterion gets
|
|
255
|
-
* (from per-round
|
|
256
|
-
* the round it was first reported met
|
|
257
|
-
*
|
|
258
|
-
*
|
|
255
|
+
* vertical view. Each objective criterion gets claimed/remaining/unknown
|
|
256
|
+
* (from the `cr-` entries of per-round execution_report.claims, ID-first or
|
|
257
|
+
* normalized-exact matching) and the round it was first reported met. Zero
|
|
258
|
+
* persistence — re-derived from committed rounds every compile.
|
|
259
|
+
*
|
|
260
|
+
* v3.9.6: the derivation lives in `criterion-status.ts` so the submission
|
|
261
|
+
* boundary and guidance can read the same rows without importing this module
|
|
262
|
+
* (which would close the `loop-compiler → submission-guidance` cycle). */
|
|
259
263
|
export function deriveCriterionStatuses(loopId, context, objective, currentRound, lastRoundResult,
|
|
260
264
|
/** v3.8.1: the criterion machine facts from the shared bundle. A criterion
|
|
261
265
|
* is `verified` / `contradicted` / `insufficient` only through an item that
|
|
@@ -263,54 +267,12 @@ export function deriveCriterionStatuses(loopId, context, objective, currentRound
|
|
|
263
267
|
* is derived over the WHOLE committed history, so it survives its contract
|
|
264
268
|
* closing (see `deriveCriterionFacts`). */
|
|
265
269
|
criterionFacts = []) {
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
const lastMet = claimedMetCriteria(lastEv);
|
|
273
|
-
const lastRemaining = claimedRemainingCriteria(lastEv);
|
|
274
|
-
const lastEntry = entries[entries.length - 1] ?? null;
|
|
275
|
-
const lastEntryRemaining = lastEntry ? entryCriteriaRemaining(lastEntry) : [];
|
|
276
|
-
// v3.8.1: criterion → machine status + explicit sub-goal links, from the
|
|
277
|
-
// shared `RoundFacts` bundle (whole committed history). It used to be
|
|
278
|
-
// re-derived here from the ACTIVE contract, which made both maps empty the
|
|
279
|
-
// moment the contract closed — the criterion's machine fact, and the
|
|
280
|
-
// sub-goal links carried on the same item, disappeared while the sub-goal
|
|
281
|
-
// side of the very same fact survived. One derivation, one window.
|
|
282
|
-
const factsByCriterion = new Map(criterionFacts.map((fact) => [fact.criterion_id, fact]));
|
|
283
|
-
return objective.success_criteria.map((text) => {
|
|
284
|
-
let metAtRound = null;
|
|
285
|
-
for (const entry of entries) {
|
|
286
|
-
const rnd = entryRound(entry);
|
|
287
|
-
if (entryCriteriaMet(entry).some((met) => criteriaMatch(met, text))) {
|
|
288
|
-
metAtRound = rnd;
|
|
289
|
-
break; // first report wins — chronological scan
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
if (metAtRound === null && lastMet.some((met) => criteriaMatch(met, text))) {
|
|
293
|
-
metAtRound = currentRound - 1;
|
|
294
|
-
}
|
|
295
|
-
// v3.8: a claim reaches `claimed` at most. Only a machine fact about a
|
|
296
|
-
// contract item that references this criterion can go further.
|
|
297
|
-
const claimedStatus = metAtRound !== null
|
|
298
|
-
? "claimed"
|
|
299
|
-
: lastRemaining.some((remaining) => criteriaMatch(remaining, text)) ||
|
|
300
|
-
lastEntryRemaining.some((remaining) => criteriaMatch(remaining, text))
|
|
301
|
-
? "remaining"
|
|
302
|
-
: "unknown";
|
|
303
|
-
const fact = factsByCriterion.get(deriveCriterionId(text));
|
|
304
|
-
const status = fact?.status ?? claimedStatus;
|
|
305
|
-
return {
|
|
306
|
-
id: deriveCriterionId(text),
|
|
307
|
-
text,
|
|
308
|
-
status,
|
|
309
|
-
...(metAtRound !== null ? { met_at_round: metAtRound } : {}),
|
|
310
|
-
// v3.8.1: explicit refs only — empty when no contract item links this
|
|
311
|
-
// criterion to a sub-goal.
|
|
312
|
-
related_subgoal_ids: fact?.related_subgoal_ids ?? [],
|
|
313
|
-
};
|
|
270
|
+
const history = buildCommittedHistory(contextEntries(context));
|
|
271
|
+
return deriveCriterionStatusesFromViews({
|
|
272
|
+
rounds: historyWindow(history, currentRound),
|
|
273
|
+
currentRound,
|
|
274
|
+
objective,
|
|
275
|
+
criterionFacts,
|
|
314
276
|
});
|
|
315
277
|
}
|
|
316
278
|
/** v3.8: Rank item statuses so the strongest machine fact wins when several
|
|
@@ -323,186 +285,16 @@ export function criteriaMatch(a, b) {
|
|
|
323
285
|
// v3.9.2: ONE identity rule, shared with the prompt and submission boundary.
|
|
324
286
|
return criterionRefMatches(a, b);
|
|
325
287
|
}
|
|
326
|
-
/** Detect newly met criteria by comparing the current entry's
|
|
327
|
-
* met criterion_claims against the previous entry's.
|
|
328
|
-
* v2.11/v3.8.1: id-first (cr-XXXXXXXX), then normalized-exact text —
|
|
329
|
-
* no similarity.
|
|
330
|
-
* Returns empty array when there is no previous entry — the first
|
|
331
|
-
* entry's criteria are the baseline, not a "new" event. */
|
|
332
|
-
function detectNewCriteria(current, previous) {
|
|
333
|
-
const currMet = entryCriteriaMet(current);
|
|
334
|
-
if (currMet.length === 0)
|
|
335
|
-
return [];
|
|
336
|
-
if (!previous) {
|
|
337
|
-
// First round with criteria is the baseline — not a milestone trigger.
|
|
338
|
-
return [];
|
|
339
|
-
}
|
|
340
|
-
const prevMet = entryCriteriaMet(previous);
|
|
341
|
-
if (prevMet.length === 0)
|
|
342
|
-
return currMet;
|
|
343
|
-
return currMet.filter((curr) => prevMet.every((prev) => !criteriaMatch(curr, prev)));
|
|
344
|
-
}
|
|
345
|
-
/** Build a single MilestoneSummary from a range of completed round entries. */
|
|
346
|
-
function buildMilestoneFromEntries(phaseEntries, startRound, endRound, label, kind) {
|
|
347
|
-
const last = phaseEntries.at(-1);
|
|
348
|
-
const lastView = last ? decodeRound(last) : null;
|
|
349
|
-
const outcomeRaw = lastView
|
|
350
|
-
? (lastView.outputSummary ?? "")
|
|
351
|
-
: last && typeof last.output_summary === "string" ? last.output_summary : "";
|
|
352
|
-
const outcomeMax = getPolicy().checkpoint.outcome_max_chars;
|
|
353
|
-
const outcome = outcomeRaw.length > outcomeMax
|
|
354
|
-
? outcomeRaw.slice(0, outcomeMax) + "…"
|
|
355
|
-
: outcomeRaw;
|
|
356
|
-
const carried = last ? entryActiveConstraints(last) : [];
|
|
357
|
-
const resolved = unique(phaseEntries.flatMap((e) => entryRetractedConstraints(e)));
|
|
358
|
-
return makeMilestoneSummary({
|
|
359
|
-
label,
|
|
360
|
-
round_range: { start: startRound, end: endRound },
|
|
361
|
-
outcome,
|
|
362
|
-
carried_constraints: carried,
|
|
363
|
-
resolved_constraints: resolved,
|
|
364
|
-
kind,
|
|
365
|
-
generated_at_round: endRound,
|
|
366
|
-
});
|
|
367
|
-
}
|
|
368
|
-
/** v3.8.1: the milestone history bound. Applied identically at every prompt
|
|
369
|
-
* level and in the state file, so the milestone set is a function of
|
|
370
|
-
* committed facts rather than of which level happened to compile. (The
|
|
371
|
-
* v3.0.1 sampler chose which milestones to keep from an UNBOUNDED list and
|
|
372
|
-
* did nothing at L2 — so the state file’s phase history depended on the
|
|
373
|
-
* prompt level and had no bound of its own.) */
|
|
374
|
-
const MILESTONE_HISTORY_CAP = 50;
|
|
375
|
-
export function buildRollingSummary(loopId, currentRound, context, sinceRound = 0) {
|
|
376
|
-
const policy = getPolicy().summary;
|
|
377
|
-
const allEntries = loopEntries(loopId, context);
|
|
378
|
-
// ── Phase 1: Recent Window (preserves existing behavior) ──
|
|
379
|
-
const windowEntries = allEntries
|
|
380
|
-
.filter((entry) => {
|
|
381
|
-
const round = entryRound(entry);
|
|
382
|
-
return round >= sinceRound && round < currentRound;
|
|
383
|
-
})
|
|
384
|
-
.slice(-policy.window);
|
|
385
|
-
const outcomes = [];
|
|
386
|
-
for (const entry of windowEntries) {
|
|
387
|
-
const view = decodeRound(entry);
|
|
388
|
-
const data = entryLineage(entry);
|
|
389
|
-
const round = entryRound(entry);
|
|
390
|
-
const success = view ? view.success : entry.success ?? data.success;
|
|
391
|
-
const summary = view
|
|
392
|
-
? view.outputSummary ?? ""
|
|
393
|
-
: typeof entry.output_summary === "string"
|
|
394
|
-
? entry.output_summary
|
|
395
|
-
: typeof data.output_summary === "string" ? data.output_summary : "";
|
|
396
|
-
// v2.12: Declared outcome wins the label; the default wording stays
|
|
397
|
-
// "accepted" for zero regression on existing loop output.
|
|
398
|
-
const declared = view ? view.outcome : entry.outcome;
|
|
399
|
-
const outcomeLabel = declared === "success" || declared === "partial" ||
|
|
400
|
-
declared === "failed" || declared === "blocked"
|
|
401
|
-
? declared
|
|
402
|
-
: success === false ? "failed" : "accepted";
|
|
403
|
-
if (summary)
|
|
404
|
-
outcomes.push(`[R${round}] ${outcomeLabel}: ${summary}`);
|
|
405
|
-
}
|
|
406
|
-
// ── Phase 2: Milestone Accumulation ──
|
|
407
|
-
// Scan all completed entries in round order. For each entry, check three
|
|
408
|
-
// trigger signals in priority order: agent_declared > criteria_milestone > auto.
|
|
409
|
-
const completedEntries = allEntries.filter((entry) => entryRound(entry) >= 1 && entryRound(entry) < currentRound);
|
|
410
|
-
const milestones = [];
|
|
411
|
-
let lastMilestoneRound = 0;
|
|
412
|
-
for (let i = 0; i < completedEntries.length; i++) {
|
|
413
|
-
const entry = completedEntries[i];
|
|
414
|
-
const rnd = entryRound(entry);
|
|
415
|
-
const lin = entryLineage(entry);
|
|
416
|
-
// Signal 1: Agent-declared checkpoint
|
|
417
|
-
const isAgentCheckpoint = lin.compression_checkpoint === true;
|
|
418
|
-
// Signal 2: New criteria met (compare with previous vault entry)
|
|
419
|
-
const prev = i > 0 ? completedEntries[i - 1] : null;
|
|
420
|
-
const newCriteria = detectNewCriteria(entry, prev);
|
|
421
|
-
if (isAgentCheckpoint || newCriteria.length > 0) {
|
|
422
|
-
const startRound = lastMilestoneRound + 1;
|
|
423
|
-
const phaseEntries = completedEntries.filter((e) => {
|
|
424
|
-
const er = entryRound(e);
|
|
425
|
-
return er >= startRound && er <= rnd;
|
|
426
|
-
});
|
|
427
|
-
const kind = isAgentCheckpoint
|
|
428
|
-
? "agent_declared"
|
|
429
|
-
: "criteria_milestone";
|
|
430
|
-
const label = isAgentCheckpoint
|
|
431
|
-
? (typeof lin.checkpoint_label === "string" && lin.checkpoint_label.trim()
|
|
432
|
-
? lin.checkpoint_label.trim()
|
|
433
|
-
: `Round ${rnd}`)
|
|
434
|
-
: `Completed: ${newCriteria.join(", ")}`;
|
|
435
|
-
milestones.push(buildMilestoneFromEntries(phaseEntries, startRound, rnd, label, kind));
|
|
436
|
-
lastMilestoneRound = rnd;
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
// Signal 3: Safety net — auto milestone when gap exceeds milestone_interval.
|
|
440
|
-
// v3.3.1: the policy contract (milestone_interval: "only fires when no
|
|
441
|
-
// agent_declared or criteria_milestone has been created for this many
|
|
442
|
-
// rounds") includes loops that NEVER created a milestone — but the guard
|
|
443
|
-
// `lastMilestoneRound > 0` made the gap start from the (nonexistent) first
|
|
444
|
-
// milestone, so a checkpoint-less loop could run forever with an empty
|
|
445
|
-
// Phase History and no memory skeleton. With lastMilestoneRound == 0 the
|
|
446
|
-
// gap is measured from round 1 and the safety net fires from the very
|
|
447
|
-
// first interval.
|
|
448
|
-
const gapSinceLast = (currentRound - 1) - lastMilestoneRound;
|
|
449
|
-
if (gapSinceLast >= policy.milestone_interval &&
|
|
450
|
-
completedEntries.length > 0) {
|
|
451
|
-
const startRound = lastMilestoneRound + 1;
|
|
452
|
-
const endRound = currentRound - 1;
|
|
453
|
-
const phaseEntries = completedEntries.filter((e) => {
|
|
454
|
-
const er = entryRound(e);
|
|
455
|
-
return er >= startRound && er <= endRound;
|
|
456
|
-
});
|
|
457
|
-
if (phaseEntries.length > 0) {
|
|
458
|
-
milestones.push(buildMilestoneFromEntries(phaseEntries, startRound, endRound, `Rounds ${startRound}–${endRound}`, "auto"));
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
// v3.8.1: one bound, applied identically at every prompt level. The v3.0.1
|
|
462
|
-
// sampler kept head anchors + a newest tail + an even middle sample, and
|
|
463
|
-
// deliberately did nothing at L2 — so the milestone set written to the state
|
|
464
|
-
// file depended on which level happened to compile, not on committed facts.
|
|
465
|
-
// It also did not bound growth at all (it only chose WHICH milestones to
|
|
466
|
-
// keep from an unbounded list), which is why the cap has to live here.
|
|
467
|
-
const cappedMilestones = milestones.slice(-MILESTONE_HISTORY_CAP);
|
|
468
|
-
if (windowEntries.length === 0 && cappedMilestones.length === 0)
|
|
469
|
-
return null;
|
|
470
|
-
return makeRollingSummary({
|
|
471
|
-
key_outcomes: unique(outcomes),
|
|
472
|
-
rounds_sampled: windowEntries.length,
|
|
473
|
-
generated_at_round: currentRound,
|
|
474
|
-
failed_patterns: [],
|
|
475
|
-
milestones: cappedMilestones,
|
|
476
|
-
});
|
|
477
|
-
}
|
|
478
288
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
479
289
|
// Sub-Goal Structured Tracking (v2.2)
|
|
480
290
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
}
|
|
486
|
-
// v3.8.1: `deriveCriterionId` / `isCriterionId` moved to token-utils.ts, where
|
|
487
|
-
// the other stable-id helpers live — the criterion fact derivation in
|
|
488
|
-
// round-facts.ts needs them, and defining them here would make
|
|
489
|
-
// round-facts ⇄ loop-compiler an import cycle. Re-exported so this module's
|
|
490
|
-
// public surface ("./compiler") is unchanged.
|
|
491
|
-
export { deriveCriterionId, isCriterionId } from "./token-utils.js";
|
|
291
|
+
// v3.8.1/v3.9.7: the stable-id helpers live in token-utils.ts, where the
|
|
292
|
+
// criterion/constraint fact derivations can use them without an import
|
|
293
|
+
// cycle. Re-exported so this module's public surface is unchanged.
|
|
294
|
+
export { deriveConstraintId, deriveCriterionId, isCriterionId, } from "./token-utils.js";
|
|
492
295
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
493
296
|
// Time-Aware Constraint Management (v2.3)
|
|
494
297
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
495
|
-
/** Determine the source of a constraint by matching against known sources. */
|
|
496
|
-
function constraintSource(text, objective, constraintsFromPlan) {
|
|
497
|
-
const norm = text.toLowerCase().trim();
|
|
498
|
-
if (objective.hard_constraints.some((c) => c.toLowerCase().trim() === norm))
|
|
499
|
-
return "hard";
|
|
500
|
-
if (constraintsFromPlan.some((c) => c.toLowerCase().trim() === norm))
|
|
501
|
-
return "plan";
|
|
502
|
-
if (objective.success_criteria.some((c) => c.toLowerCase().trim() === norm))
|
|
503
|
-
return "criteria";
|
|
504
|
-
return "discovered";
|
|
505
|
-
}
|
|
506
298
|
/** True when a reported violation text matches a constraint text.
|
|
507
299
|
* v2.11: ID-first — an exact `c-XXXXXXXX` match against the derived id.
|
|
508
300
|
* v3.8.1: the Jaccard fallback is gone; the remaining arm requires the
|
|
@@ -516,22 +308,13 @@ function matchesConstraintText(text, violation) {
|
|
|
516
308
|
}
|
|
517
309
|
return normalizeText(text) === normalizeText(violation);
|
|
518
310
|
}
|
|
519
|
-
function findLastViolatedRound(text,
|
|
311
|
+
function findLastViolatedRound(text, rounds) {
|
|
520
312
|
let latest = 0;
|
|
521
|
-
for (const
|
|
522
|
-
const
|
|
523
|
-
if (rnd < 1)
|
|
524
|
-
continue;
|
|
525
|
-
const view = decodeRound(entry);
|
|
526
|
-
const violations = view
|
|
527
|
-
? view.constraintViolations ?? []
|
|
528
|
-
: Array.isArray(entry.constraint_violations)
|
|
529
|
-
? entry.constraint_violations.filter((v) => typeof v === "string")
|
|
530
|
-
: [];
|
|
531
|
-
for (const v of violations) {
|
|
313
|
+
for (const view of rounds) {
|
|
314
|
+
for (const v of view.constraintViolations ?? []) {
|
|
532
315
|
if (matchesConstraintText(text, v)) {
|
|
533
|
-
if (
|
|
534
|
-
latest =
|
|
316
|
+
if (view.round > latest)
|
|
317
|
+
latest = view.round;
|
|
535
318
|
break;
|
|
536
319
|
}
|
|
537
320
|
}
|
|
@@ -549,18 +332,10 @@ function findLastViolatedRound(text, vaultEntries) {
|
|
|
549
332
|
* and last_violated_at_round for the L1 collapse "(violated this round)"
|
|
550
333
|
* annotation (diffConstraints compares it to the current round).
|
|
551
334
|
*/
|
|
552
|
-
function manageConstraintLifecycle(active,
|
|
553
|
-
const allEntries = loopEntries(objective.loop_id || "", vaultContext);
|
|
554
|
-
const completedEntries = allEntries.filter((e) => entryRound(e) >= 1 && entryRound(e) < currentRound);
|
|
335
|
+
function manageConstraintLifecycle(active, provenance, windowRounds) {
|
|
555
336
|
const metadata = [];
|
|
556
337
|
for (const text of active) {
|
|
557
|
-
const source =
|
|
558
|
-
// v3.2.1: a violation reported in THIS round (last_round_result) counts
|
|
559
|
-
// as a violation at currentRound — the previous rounds' scan cannot see
|
|
560
|
-
// it (this round is not committed yet). This makes the L1 collapse
|
|
561
|
-
// render "(violated this round)" (diffConstraints compares
|
|
562
|
-
// last_violated_at_round === round).
|
|
563
|
-
const violatedThisRound = lastRoundViolations.some((v) => matchesConstraintText(text, v));
|
|
338
|
+
const source = provenance.get(text) ?? { source: "unrecorded" };
|
|
564
339
|
// v3.3.1: one text, one identity — a success-criterion text that also
|
|
565
340
|
// lives in the merged active set belongs to the cr-XXXXXXXX namespace
|
|
566
341
|
// (its own Success Criteria section and the verification gate's
|
|
@@ -569,50 +344,73 @@ function manageConstraintLifecycle(active, objective, constraintsFromPlan, vault
|
|
|
569
344
|
// and an agent echoing the active-list ID into its criterion claims
|
|
570
345
|
// could never match.
|
|
571
346
|
metadata.push(makeConstraintMeta({
|
|
572
|
-
id: source === "criteria" ? deriveCriterionId(text) : deriveConstraintId(text),
|
|
347
|
+
id: source.source === "criteria" ? deriveCriterionId(text) : deriveConstraintId(text),
|
|
573
348
|
text,
|
|
574
|
-
last_violated_at_round:
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
source,
|
|
349
|
+
last_violated_at_round: findLastViolatedRound(text, windowRounds),
|
|
350
|
+
source: source.source,
|
|
351
|
+
...(source.source_round !== undefined ? { source_round: source.source_round } : {}),
|
|
578
352
|
}));
|
|
579
353
|
}
|
|
580
354
|
return { active, metadata };
|
|
581
355
|
}
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
356
|
+
/** Resolve the ONE previous-round fact bundle for a compile.
|
|
357
|
+
*
|
|
358
|
+
* It is built from the committed window only. A retry's verification findings
|
|
359
|
+
* are authoritative even when empty: the retry must never inherit the
|
|
360
|
+
* previous committed round's flags. */
|
|
361
|
+
function resolvePreviousRoundFacts(request, entries, windowRounds) {
|
|
362
|
+
const view = windowRounds.find((round) => round.round === request.round - 1) ?? null;
|
|
363
|
+
const recoveryBoundary = pendingBacktrackRecord(entries, request.round) !== null;
|
|
364
|
+
const inFlight = (request.attempt ?? 1) > 1 || recoveryBoundary;
|
|
365
|
+
return {
|
|
366
|
+
view,
|
|
367
|
+
result: view ? roundResultFromCommittedRound(view) : null,
|
|
368
|
+
verificationFlags: inFlight
|
|
369
|
+
? (request.verification_flags ?? [])
|
|
370
|
+
: (view?.verificationFlags ?? request.verification_flags ?? []),
|
|
371
|
+
freshness: view ? derivePromptFreshness(view) : UNKNOWN_PROMPT_FRESHNESS,
|
|
372
|
+
recoveryBoundary,
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
function levelDecision(request, prior, previous, goalId) {
|
|
599
376
|
return decidePromptLevel({
|
|
600
377
|
round: request.round,
|
|
601
378
|
attempt: request.attempt,
|
|
602
379
|
forceLevel: request.force_level,
|
|
603
380
|
hasPlanSource: Boolean(request.plan_source),
|
|
604
|
-
recoveryBoundary,
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
.
|
|
381
|
+
recoveryBoundary: previous.recoveryBoundary,
|
|
382
|
+
subtaskBoundary: previous.result?.subtask_boundary === true,
|
|
383
|
+
// An empty prior goal_id is unobservable, not a goal change: never
|
|
384
|
+
// fabricate L2. `goalId` is derived from the working objective, so an
|
|
385
|
+
// objective_refinement legitimately changes it.
|
|
386
|
+
goalChanged: prior !== null && prior.goal_id !== "" &&
|
|
387
|
+
prior.goal_id !== goalId,
|
|
388
|
+
previousStateMissing: request.round > 1 && prior === null,
|
|
389
|
+
// v4.0.3: prompt density consumes one explicit previous-round bundle.
|
|
390
|
+
// Agent outcome remains semantic input; posture/finding facts remain
|
|
391
|
+
// machine input. The policy owns the combination so callers cannot drift.
|
|
392
|
+
previousRound: previous.view
|
|
393
|
+
? {
|
|
394
|
+
agentOutcome: previous.view.agentOutcome,
|
|
395
|
+
evidencePosture: previous.view.evidencePosture?.status ?? null,
|
|
396
|
+
observableNewInformation: previous.freshness.observableNewInformation,
|
|
397
|
+
hasVerificationError: previous.verificationFlags.some((flag) => flag.severity === "error"),
|
|
398
|
+
}
|
|
399
|
+
: null,
|
|
611
400
|
consecutiveRejections: request.consecutive_rejections,
|
|
401
|
+
// v4.0.5: a cold handoff rehydrates at L2; the escalation threshold above
|
|
402
|
+
// still wins because that retry is already clean context.
|
|
403
|
+
invocationRecovery: request.invocation_recovery === true,
|
|
612
404
|
});
|
|
613
405
|
}
|
|
614
|
-
export function decideLevel(request, context) {
|
|
615
|
-
|
|
406
|
+
export function decideLevel(request, context, workingObjective) {
|
|
407
|
+
const entries = contextEntries(context);
|
|
408
|
+
const history = buildCommittedHistory(entries);
|
|
409
|
+
const window = historyWindow(history, request.round);
|
|
410
|
+
const prior = previousRoundFromWindow(window, request.round - 1);
|
|
411
|
+
const objective = workingObjective ??
|
|
412
|
+
latestObjective(request.loop_id, context)?.objective ?? request.task;
|
|
413
|
+
return levelDecision(request, prior, resolvePreviousRoundFacts(request, entries, window), deriveGoalId(request.loop_id, objective)).level;
|
|
616
414
|
}
|
|
617
415
|
/** The prompt's evaluation block. All content (examples, contract/sub-goal
|
|
618
416
|
* guidance, flag-specific hints) comes from the one FIELD_GUIDES table in
|
|
@@ -620,80 +418,141 @@ export function decideLevel(request, context) {
|
|
|
620
418
|
export function buildSelfEvalBlock(ctx) {
|
|
621
419
|
return renderSelfEvalBlock(ctx);
|
|
622
420
|
}
|
|
623
|
-
// v2.5: checkpoint() removed — agent_declared milestones from
|
|
624
|
-
// compression_checkpoint supersede the separate CheckpointSummary.
|
|
625
|
-
// See buildRollingSummary() signal 1.
|
|
626
421
|
/** v3.7.1: Derived Recovery Brief for the state-file Recent tier. Present
|
|
627
422
|
* ONLY while a committed backtrack decision is the current round's record
|
|
628
423
|
* (the recovery window); the redo commit replaces that record, so the
|
|
629
424
|
* brief exits the projection by construction. Sources are the committed
|
|
630
425
|
* rollback decision's own fields — no new persistence, no rejected
|
|
631
426
|
* payloads. */
|
|
632
|
-
function committedBacktrackBrief(loopId, round, context) {
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
//
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
// reads the durable :feedback envelope, so both carriers decode to one shape
|
|
640
|
-
// — and this brief and the rollback prompt no longer render the same
|
|
641
|
-
// rollback with two hand-written line builders.
|
|
642
|
-
const record = decodeBacktrackRecord(entryLineage(entry));
|
|
427
|
+
function committedBacktrackBrief(loopId, round, context, restoreVerified) {
|
|
428
|
+
// The SAME pending-backtrack lookup the recovery boundary uses: a deep
|
|
429
|
+
// restore commits at the failed round while its redo targets
|
|
430
|
+
// `backtrackTarget + 1`, so an exact-round match loses both the boundary
|
|
431
|
+
// and this brief.
|
|
432
|
+
const view = pendingBacktrackRecord(contextEntries(context), round);
|
|
433
|
+
const record = view?.recovery;
|
|
643
434
|
if (!record)
|
|
644
435
|
return undefined;
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
436
|
+
const lines = recoveryBriefLines({
|
|
437
|
+
redoRound: record.target.redo_round,
|
|
438
|
+
checkpointId: record.target.checkpoint_id,
|
|
439
|
+
restoreVerified,
|
|
440
|
+
triggerRule: record.origin === "machine"
|
|
441
|
+
? (record.trigger_rule ?? "machine_recovery")
|
|
442
|
+
: "agent_recovery",
|
|
443
|
+
origin: record.origin,
|
|
444
|
+
reason: record.reason,
|
|
445
|
+
...(record.origin === "machine" && record.reason.trim()
|
|
446
|
+
? { failureReason: record.reason }
|
|
447
|
+
: {}),
|
|
448
|
+
failedRounds: record.failed_rounds,
|
|
649
449
|
approaches: record.approaches,
|
|
650
|
-
wrongAssumptions: record.
|
|
450
|
+
wrongAssumptions: record.wrong_assumptions,
|
|
451
|
+
preservedDiscoveries: record.skipped_discoveries,
|
|
452
|
+
// The redo round's stable identity is a fact the old prepended prose
|
|
453
|
+
// carried; keep it inside the artifact now.
|
|
454
|
+
recoveryRoundId: `loop:${loopId}:round:${record.target.redo_round}`,
|
|
651
455
|
});
|
|
456
|
+
const observedFiles = [...(record.observed_skipped_files ??
|
|
457
|
+
record.skipped_files ?? [])].sort();
|
|
458
|
+
const reportedFiles = [...(record.reported_skipped_files ?? [])].sort();
|
|
459
|
+
if (observedFiles.length > 0) {
|
|
460
|
+
const shown = observedFiles.slice(0, 15);
|
|
461
|
+
lines.push("- **Observed changes since the checkpoint (machine)**:");
|
|
462
|
+
for (const file of shown)
|
|
463
|
+
lines.push(` - \`${file}\``);
|
|
464
|
+
if (observedFiles.length > shown.length) {
|
|
465
|
+
lines.push(` - … and ${observedFiles.length - shown.length} more files`);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
if (reportedFiles.length > 0) {
|
|
469
|
+
const shown = reportedFiles.slice(0, 15);
|
|
470
|
+
lines.push("- **Agent-reported changed files (report)**:");
|
|
471
|
+
for (const file of shown)
|
|
472
|
+
lines.push(` - \`${file}\``);
|
|
473
|
+
if (reportedFiles.length > shown.length) {
|
|
474
|
+
lines.push(` - … and ${reportedFiles.length - shown.length} more files`);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
return lines;
|
|
652
478
|
}
|
|
653
|
-
export function compileLoop(request, context) {
|
|
654
|
-
const
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
const
|
|
659
|
-
const
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
479
|
+
export function compileLoop(request, context, options = {}, committedHistory) {
|
|
480
|
+
const operatorAcceptance = options.operatorAcceptance ?? null;
|
|
481
|
+
// ONE history window and ONE previous-round fact bundle for the whole
|
|
482
|
+
// compile. Every consumer below reads these values; none re-derives the
|
|
483
|
+
// window with a private rule.
|
|
484
|
+
const entries = contextEntries(context);
|
|
485
|
+
const history = committedHistory ?? buildCommittedHistory(entries);
|
|
486
|
+
if (history.legacyRounds.length > 0) {
|
|
487
|
+
throw new LegacyHistoryError(request.loop_id, history.legacyRounds.map((round) => round.round));
|
|
488
|
+
}
|
|
489
|
+
// The operator surface is immutable. A caller-supplied copy that disagrees
|
|
490
|
+
// with a committed copy is an integrity conflict — never a silent override.
|
|
491
|
+
const committedAcceptance = history.views.find((view) => view.operatorAcceptance !== null)?.operatorAcceptance ?? null;
|
|
492
|
+
if (operatorAcceptance && committedAcceptance &&
|
|
493
|
+
operatorAcceptanceKey(operatorAcceptance) !==
|
|
494
|
+
operatorAcceptanceKey(committedAcceptance)) {
|
|
495
|
+
throw new OperatorAcceptanceConflictError(request.loop_id);
|
|
496
|
+
}
|
|
497
|
+
const windowRounds = historyWindow(history, request.round);
|
|
498
|
+
const prior = previousRoundFromWindow(windowRounds, request.round - 1);
|
|
499
|
+
const previous = resolvePreviousRoundFacts(request, entries, windowRounds);
|
|
500
|
+
// v4.0.6: the working objective is computed BEFORE the level decision —
|
|
501
|
+
// a committed objective_refinement changes goal identity, and that change
|
|
502
|
+
// must be able to select L2.
|
|
503
|
+
const objective = evolveObjective(request, context, operatorAcceptance);
|
|
504
|
+
const goalId = deriveGoalId(request.loop_id, objective.objective);
|
|
505
|
+
const decision = levelDecision(request, prior, previous, goalId);
|
|
506
|
+
const pendingDirective = pendingBacktrackRecord(entries, request.round);
|
|
507
|
+
const preservedDiscoveries = pendingDirective?.recovery &&
|
|
508
|
+
getPolicy().engine.backtrack_preserve_discoveries
|
|
509
|
+
? pendingDirective.recovery.skipped_discoveries
|
|
510
|
+
: [];
|
|
511
|
+
const preservedRound = pendingDirective?.round;
|
|
512
|
+
// Plan constraints are a round-1 operator/plan input, or a library
|
|
513
|
+
// bootstrap when no committed round exists. Once a committed view exists,
|
|
514
|
+
// the committed active set carries them and a late request is not a fact.
|
|
515
|
+
const planInput = request.round === 1 || previous.view === null
|
|
516
|
+
? request.constraints_from_plan
|
|
517
|
+
: [];
|
|
518
|
+
const constraints = evolveConstraints(request, objective, previous, windowRounds, planInput, preservedDiscoveries, preservedRound);
|
|
519
|
+
const constraintLifecycle = manageConstraintLifecycle(constraints.active, constraints.provenance, windowRounds);
|
|
663
520
|
// v3.8: sub-goal lifecycle lives in subgoal-state.ts and consumes the
|
|
664
|
-
// shared committed-round view — one history interpretation.
|
|
521
|
+
// shared committed-round view — one history interpretation. The previous
|
|
522
|
+
// round's report is idempotently replayed; a committed round is already in
|
|
523
|
+
// the window, while the library fallback has no view.
|
|
665
524
|
const subGoals = deriveSubGoals({
|
|
666
525
|
loopId: request.loop_id,
|
|
667
526
|
currentRound: request.round,
|
|
668
527
|
rounds: windowRounds,
|
|
669
|
-
currentReport: request.last_round_result
|
|
670
|
-
? {
|
|
671
|
-
round: request.last_round_result.round || (request.round - 1),
|
|
672
|
-
emergedSubtasks: request.last_round_result.emerged_subtasks ?? [],
|
|
673
|
-
subgoalUpdates: request.last_round_result.subgoal_updates ?? [],
|
|
674
|
-
}
|
|
675
|
-
: null,
|
|
676
528
|
});
|
|
677
|
-
// v3.4/v3.8/v3.8.1: the ACTIVE Round Contract
|
|
678
|
-
//
|
|
679
|
-
//
|
|
680
|
-
//
|
|
681
|
-
//
|
|
682
|
-
//
|
|
683
|
-
//
|
|
684
|
-
//
|
|
529
|
+
// v3.4/v3.8/v3.8.1: the ACTIVE Round Contract and its per-item statuses for
|
|
530
|
+
// this compile — ONE `deriveRoundFacts` call, the same bundle the projection
|
|
531
|
+
// consumes. Derived from committed rounds, so rejection retries, resume,
|
|
532
|
+
// unpause and backtrack compiles keep showing the same contract, and a
|
|
533
|
+
// verified or blocked contract stops being shown. A verified item stays
|
|
534
|
+
// proven after its contract closes because the bundle reads the whole
|
|
535
|
+
// history, not the active contract. The compile path has no in-flight
|
|
536
|
+
// observations of its own.
|
|
685
537
|
const roundFacts = deriveRoundFacts({
|
|
686
538
|
rounds: windowRounds,
|
|
687
539
|
currentRound: request.round,
|
|
688
540
|
inFlight: NO_IN_FLIGHT_ROUND,
|
|
689
|
-
subGoals,
|
|
690
541
|
commands: getPolicy().evidence.commands ?? [],
|
|
691
542
|
});
|
|
692
|
-
// `roundFacts.verifiedSubGoals` is not read here — it reaches the state file
|
|
693
|
-
// and the projection through the bundle itself.
|
|
694
543
|
const { activeContract } = roundFacts;
|
|
544
|
+
// v3.9.4: the criterion reference space is the ACTIVE criterion set, not
|
|
545
|
+
// every text the objective ever carried. A retracted criterion must not be
|
|
546
|
+
// claimable or contract-referenceable while the prompt does not render it.
|
|
547
|
+
const activeConstraintKeys = new Set(constraintLifecycle.active.map((text) => normalizeText(text)));
|
|
548
|
+
const activeCriteria = uniqueNormalized(objective.success_criteria)
|
|
549
|
+
.filter((text) => activeConstraintKeys.has(normalizeText(text)));
|
|
550
|
+
const criterionObjective = {
|
|
551
|
+
...objective,
|
|
552
|
+
success_criteria: activeCriteria,
|
|
553
|
+
};
|
|
695
554
|
// v3.2: Goal → criteria → evidence vertical view (derived, zero persistence).
|
|
696
|
-
const criterionStatuses = deriveCriterionStatuses(request.loop_id, context,
|
|
555
|
+
const criterionStatuses = deriveCriterionStatuses(request.loop_id, context, criterionObjective, request.round, previous.result, roundFacts.criterionFacts);
|
|
697
556
|
// v3.8.1: the ONE recurring-fact derivation, over the shared window. The
|
|
698
557
|
// prompt renders its recent tail; the state file renders the recurring set.
|
|
699
558
|
const recurringFlags = deriveRecurringFlags(windowRounds);
|
|
@@ -703,13 +562,27 @@ export function compileLoop(request, context) {
|
|
|
703
562
|
// gone with the rest of the fuzzy matching, and so are the task-alignment
|
|
704
563
|
// and loop-health warnings: both were Jaccard scores over task/objective
|
|
705
564
|
// text, never machine facts.
|
|
706
|
-
|
|
565
|
+
// v3.9.5: only committed declarations are history. A library fallback is
|
|
566
|
+
// in-flight and must not manufacture a prompt warning.
|
|
567
|
+
const repeatedDeclarations = previous.view
|
|
568
|
+
? duplicateEmergedDeclarations(previous.view.emergedSubtasks ?? [])
|
|
569
|
+
: [];
|
|
707
570
|
const warnings = unique([
|
|
708
571
|
repeatedDeclarations.length > 0
|
|
709
572
|
? `duplicate_declaration: ${repeatedDeclarations.length} sub-goal ` +
|
|
710
573
|
`declaration(s) repeated an earlier entry in the same round and were ` +
|
|
711
574
|
`not created: ${repeatedDeclarations.join("; ")}`
|
|
712
575
|
: "",
|
|
576
|
+
request.round > 1 && previous.view !== null && request.loop_objective !== null
|
|
577
|
+
? "uncommitted_objective_ignored: a committed objective exists; the request objective is ignored"
|
|
578
|
+
: "",
|
|
579
|
+
request.round > 1 && previous.view !== null && request.constraints_from_plan.length > 0
|
|
580
|
+
? "uncommitted_plan_ignored: a committed active set exists; request plan constraints are ignored"
|
|
581
|
+
: "",
|
|
582
|
+
constraints.discoveryDropped > 0
|
|
583
|
+
? `discovery_cap: ${constraints.discoveryDropped} discovered constraint(s) exceeded ` +
|
|
584
|
+
`max_discovered_constraints_per_round and were not expanded`
|
|
585
|
+
: "",
|
|
713
586
|
]);
|
|
714
587
|
// v3.8.1: the agent trust score and trend are deleted. The score was
|
|
715
588
|
// `1 - errors*0.15 - warns*0.03` — arbitrary weights with no machine
|
|
@@ -718,28 +591,34 @@ export function compileLoop(request, context) {
|
|
|
718
591
|
// `constraint_violations`, i.e. a DIFFERENT source from the score it was
|
|
719
592
|
// displayed next to. "Trend" was therefore not the score's history at all.
|
|
720
593
|
// Neither fed a decision.
|
|
721
|
-
const completedEntries = loopEntries(request.loop_id, context)
|
|
722
|
-
.filter((e) => entryRound(e) >= 1 && entryRound(e) < request.round);
|
|
723
|
-
const committedRounds = derivationRounds(completedEntries, request.round);
|
|
724
594
|
// v3.3/v3.8.1: machine git-motion (display-only, derived). The per-round
|
|
725
595
|
// "round stats" table is deleted: its only renderer was the L2 prompt
|
|
726
596
|
// section removed above, so it was computed, put into the canonical state
|
|
727
|
-
// (and therefore into stateHash) and read by nothing.
|
|
728
|
-
|
|
597
|
+
// (and therefore into stateHash) and read by nothing. `windowRounds` is the
|
|
598
|
+
// ONE window computed at the top of this function.
|
|
599
|
+
const machineStatus = deriveMachineStatus(windowRounds, request.round);
|
|
600
|
+
// v4.0.2: ONE derivation of the bounded Working Memory and the context
|
|
601
|
+
// boundary for the whole compile. State, prompt, artifact and every
|
|
602
|
+
// prompt-bearing response project these values; none re-derives them.
|
|
603
|
+
const workingMemory = deriveWorkingMemory(history, request.round);
|
|
604
|
+
const contextBoundary = derivePromptContextBoundary({
|
|
605
|
+
round: request.round,
|
|
606
|
+
attempt: request.attempt,
|
|
607
|
+
consecutiveRejections: request.consecutive_rejections,
|
|
608
|
+
recoveryBoundary: previous.recoveryBoundary,
|
|
609
|
+
hasPreviousCommittedRound: previous.view !== null,
|
|
610
|
+
invocationRecovery: request.invocation_recovery === true,
|
|
611
|
+
});
|
|
729
612
|
const response = makeLoopCompileResponse({
|
|
730
613
|
status: AgentStatus.OK,
|
|
731
614
|
recompile_level: decision.level,
|
|
732
|
-
diff_from_previous: decision.reasons.join(","),
|
|
733
|
-
lineage: [`${request.loop_id}:r${request.round}`],
|
|
734
615
|
constraints_active: constraintLifecycle.active,
|
|
735
616
|
constraints_retired: constraints.retired,
|
|
736
617
|
constraint_metadata: constraintLifecycle.metadata,
|
|
737
618
|
loop_id: request.loop_id,
|
|
738
619
|
round: request.round,
|
|
739
|
-
goal_id:
|
|
740
|
-
goal_text_hash: computeGoalTextHash(request.task),
|
|
620
|
+
goal_id: goalId,
|
|
741
621
|
loop_objective: objective,
|
|
742
|
-
rolling_summary: rolling,
|
|
743
622
|
sub_goals: subGoals,
|
|
744
623
|
criterion_statuses: criterionStatuses,
|
|
745
624
|
recurring_flags: recurringFlags,
|
|
@@ -754,17 +633,31 @@ export function compileLoop(request, context) {
|
|
|
754
633
|
const statePath = policy.state_file.enabled
|
|
755
634
|
? `${policy.state_file.directory}/${request.loop_id}-state.md`
|
|
756
635
|
: "";
|
|
636
|
+
// The recovery brief is a current-round fact, computed once and shared by
|
|
637
|
+
// the state file, the prompt and the evaluation block. It never becomes
|
|
638
|
+
// prompt control state.
|
|
639
|
+
const recoveryBrief = committedBacktrackBrief(request.loop_id, request.round, context, request.restore_verified === true);
|
|
757
640
|
const state = createCanonicalLoopState(request, response, statePath, {
|
|
758
641
|
machineStatus,
|
|
759
642
|
// v3.4/v3.8.1: the contract-fact bundle — the Current Task's source of
|
|
760
643
|
// truth, its verification view and the machine-verified sub-goal facts as
|
|
761
644
|
// ONE derivation. The static capability rides on the state itself.
|
|
762
645
|
roundFacts,
|
|
646
|
+
// 3.9.3: the same previous-round bundle the level decision used. This is
|
|
647
|
+
// what keeps corrections/blockers/remaining/progress identical on live,
|
|
648
|
+
// resume, and unpause compiles.
|
|
649
|
+
// v3.9.5: the committed flag is what keeps a caller fallback out of the
|
|
650
|
+
// prompt/state narrative.
|
|
651
|
+
previousRound: { ...previous, committed: previous.view !== null },
|
|
652
|
+
recoveryBrief,
|
|
653
|
+
workingMemory,
|
|
654
|
+
context: {
|
|
655
|
+
round: request.round,
|
|
656
|
+
attempt: request.attempt,
|
|
657
|
+
mode: contextBoundary.mode,
|
|
658
|
+
reason: contextBoundary.reason,
|
|
659
|
+
},
|
|
763
660
|
});
|
|
764
|
-
// The recovery brief is a current-round fact, computed once and shared by
|
|
765
|
-
// the state file and the evaluation block. It never becomes prompt control
|
|
766
|
-
// state.
|
|
767
|
-
const recoveryBrief = committedBacktrackBrief(request.loop_id, request.round, context);
|
|
768
661
|
const configuredCapability = deriveConfiguredCapability(policy);
|
|
769
662
|
const contractCommands = configuredCapability.commands
|
|
770
663
|
.filter((command) => command.enabled)
|
|
@@ -776,6 +669,12 @@ export function compileLoop(request, context) {
|
|
|
776
669
|
// round's record; the redo commit removes it by construction.
|
|
777
670
|
attempt: request.attempt,
|
|
778
671
|
recoveryBrief,
|
|
672
|
+
context: {
|
|
673
|
+
round: request.round,
|
|
674
|
+
attempt: request.attempt,
|
|
675
|
+
mode: contextBoundary.mode,
|
|
676
|
+
reason: contextBoundary.reason,
|
|
677
|
+
},
|
|
779
678
|
});
|
|
780
679
|
// v3.8.1: the L2 budget is a FIXED ceiling. The v2.4 adaptive budget
|
|
781
680
|
// scaled it by round count, milestone count and sub-goal count, which made
|
|
@@ -783,43 +682,60 @@ export function compileLoop(request, context) {
|
|
|
783
682
|
// rather than on the facts of this round — the same loop could render a
|
|
784
683
|
// section at round 5 and silently drop it at round 50.
|
|
785
684
|
//
|
|
786
|
-
//
|
|
787
|
-
//
|
|
788
|
-
//
|
|
789
|
-
// L7: pointer mode REQUIRES the file — with state_file disabled it would
|
|
790
|
-
// silently drop the full state from L2 prompts and point at nothing.
|
|
791
|
-
const l2Pointer = policy.state_file.enabled &&
|
|
792
|
-
policy.prompt.l2_pointer_enabled && decision.level === "l2";
|
|
685
|
+
// v3.9.5: L2 never embeds the monolithic state markdown. The prompt is a
|
|
686
|
+
// current-governance projection; full history stays in the optional state
|
|
687
|
+
// file / replay / audit.
|
|
793
688
|
const artifact = assemblePromptArtifact({
|
|
794
689
|
state,
|
|
795
690
|
level: decision.level,
|
|
796
|
-
reasons: decision.reasons,
|
|
797
691
|
budgets: {
|
|
798
692
|
l0: policy.prompt.l0_max_chars,
|
|
799
693
|
l1: policy.prompt.l1_max_chars,
|
|
800
694
|
l2: policy.prompt.l2_max_chars,
|
|
801
695
|
},
|
|
802
696
|
attempt: request.attempt,
|
|
697
|
+
warnings,
|
|
803
698
|
selfEvaluationBlock: buildSelfEvalBlock({
|
|
804
699
|
round: request.round,
|
|
700
|
+
maxRounds: request.max_rounds ?? policy.engine.max_rounds,
|
|
805
701
|
level: decision.level,
|
|
806
702
|
activeContract,
|
|
807
703
|
itemStatuses: roundFacts.itemStatuses,
|
|
808
704
|
commands: contractCommands,
|
|
809
705
|
contractVerificationAvailable: configuredCapability.contractVerificationAvailable,
|
|
810
706
|
subGoals,
|
|
811
|
-
verificationFlags:
|
|
707
|
+
verificationFlags: previous.verificationFlags,
|
|
812
708
|
recovery: recoveryBrief !== undefined,
|
|
813
709
|
gateEnabled: policy.gate.enabled === true,
|
|
814
710
|
criteria: criterionStatuses,
|
|
815
711
|
}),
|
|
816
|
-
|
|
817
|
-
//
|
|
818
|
-
|
|
712
|
+
// v3.9.4: the SAME committed-backtrack facts the state file renders. The
|
|
713
|
+
// recovery section is part of the artifact, so the delivered prompt is
|
|
714
|
+
// exactly `artifact.renderedPrompt` (no post-compile prepend).
|
|
715
|
+
recoveryBrief,
|
|
716
|
+
// v4.0.2: the ONE context boundary and the SAME bounded Working Memory
|
|
717
|
+
// the state carries. A retry renders no Working Memory section.
|
|
718
|
+
contextMode: contextBoundary.mode,
|
|
719
|
+
contextReason: contextBoundary.reason,
|
|
720
|
+
workingMemory,
|
|
819
721
|
});
|
|
820
722
|
response.prompt = artifact.renderedPrompt;
|
|
821
723
|
response.prompt_artifact = artifact;
|
|
822
724
|
response.state_file_content = policy.state_file.enabled ? markdown : undefined;
|
|
725
|
+
// v4.0.6: the fallback acceptance uses the immutable facts, never the
|
|
726
|
+
// working objective text: `objective.objective` may be an Agent refinement
|
|
727
|
+
// and must not be written back as Operator Acceptance.
|
|
728
|
+
response.operatorAcceptance = operatorAcceptance ?? committedAcceptance ?? {
|
|
729
|
+
task: request.task,
|
|
730
|
+
success_criteria: [...objective.success_criteria],
|
|
731
|
+
hard_constraints: [...objective.hard_constraints],
|
|
732
|
+
};
|
|
733
|
+
response.compiled = {
|
|
734
|
+
task: objective.objective,
|
|
735
|
+
goalId: response.goal_id,
|
|
736
|
+
constraintsActive: [...constraintLifecycle.active],
|
|
737
|
+
constraintsRetired: [...constraints.retired],
|
|
738
|
+
};
|
|
823
739
|
return response;
|
|
824
740
|
}
|
|
825
741
|
//# sourceMappingURL=loop-compiler.js.map
|