@cassiomc1/forgeloop 1.6.4 → 1.7.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 (73) hide show
  1. package/AGENT_COMPATIBILITY.md +11 -0
  2. package/DOCS_INDEX.md +1 -0
  3. package/GUIDE_ROUTER.md +26 -1
  4. package/LOOP_ENGINEERING.md +48 -0
  5. package/ORCHESTRATOR_INTEGRATION.md +38 -0
  6. package/PROTOCOL_INTEGRATION.md +62 -0
  7. package/README.md +25 -0
  8. package/benchmarks/execution-profiles/README.md +44 -0
  9. package/benchmarks/execution-profiles/api-feature.json +18 -0
  10. package/benchmarks/execution-profiles/authentication-change.json +18 -0
  11. package/benchmarks/execution-profiles/documentation-correction.json +18 -0
  12. package/benchmarks/execution-profiles/infrastructure-release.json +18 -0
  13. package/benchmarks/execution-profiles/novatask-saas-landing-page.json +36 -0
  14. package/benchmarks/execution-profiles/small-bug-fix.json +18 -0
  15. package/benchmarks/execution-profiles/static-landing-page.json +18 -0
  16. package/completions/_forgeloop +6 -4
  17. package/completions/forgeloop.bash +8 -4
  18. package/completions/forgeloop.fish +24 -1
  19. package/docs/AGENT_PROTOCOL_SUMMARY.md +45 -1
  20. package/docs/ARTIFACT_REFERENCE.md +53 -7
  21. package/docs/CLI_REFERENCE.md +83 -1
  22. package/docs/EXECUTION_PROFILE_BENCHMARKS.md +208 -0
  23. package/docs/GETTING_STARTED.md +28 -0
  24. package/docs/MCP.md +6 -0
  25. package/docs/RELEASE_CHECKLIST.md +4 -0
  26. package/docs/TROUBLESHOOTING.md +6 -0
  27. package/docs/UNIVERSAL_INTEGRATION.md +58 -0
  28. package/package.json +14 -2
  29. package/schemas/config.schema.json +1 -0
  30. package/schemas/execution-profile-benchmark-aggregate.schema.json +43 -0
  31. package/schemas/execution-profile-benchmark-run.schema.json +106 -0
  32. package/schemas/execution-profile-benchmark-scenario.schema.json +66 -0
  33. package/schemas/routing-result.schema.json +12 -0
  34. package/schemas/usage.schema.json +30 -0
  35. package/scripts/check-efficiency-regression.mjs +99 -0
  36. package/scripts/generate-agent-protocol-summary.mjs +35 -0
  37. package/scripts/lib/execution-profile-benchmark-io.mjs +67 -0
  38. package/scripts/run-execution-profile-benchmarks.mjs +265 -0
  39. package/scripts/summarize-execution-profile-benchmarks.mjs +84 -0
  40. package/scripts/validate-execution-profile-benchmarks.mjs +120 -0
  41. package/src/cli.js +16 -4
  42. package/src/commands/efficiency.js +12 -0
  43. package/src/commands/eval.js +8 -2
  44. package/src/commands/metrics.js +2 -2
  45. package/src/commands/next.js +26 -2
  46. package/src/commands/route.js +20 -2
  47. package/src/commands/task-show.js +31 -2
  48. package/src/commands/usage-record.js +61 -0
  49. package/src/core/artifact-registry.js +12 -0
  50. package/src/core/cli-command-definitions.js +27 -0
  51. package/src/core/command-executors.js +29 -5
  52. package/src/core/command-input.js +34 -0
  53. package/src/core/config.js +9 -0
  54. package/src/core/efficiency.js +197 -0
  55. package/src/core/error-codes.js +18 -0
  56. package/src/core/execution-profile-benchmarks.js +674 -0
  57. package/src/core/execution-profile-context.js +177 -0
  58. package/src/core/execution-profile.js +248 -0
  59. package/src/core/integration-invocation-policy.js +43 -0
  60. package/src/core/integration-resources.js +22 -1
  61. package/src/core/protocol-info.js +42 -0
  62. package/src/core/resumability.js +27 -1
  63. package/src/core/router.js +23 -1
  64. package/src/core/runtime-context.js +11 -0
  65. package/src/core/schema-validation.js +4 -0
  66. package/src/core/task-paths.js +2 -0
  67. package/src/core/templates.js +4 -0
  68. package/src/core/trace.js +1 -0
  69. package/src/core/trajectory-evaluation.js +2 -2
  70. package/src/core/trajectory-metrics.js +18 -2
  71. package/src/core/usage.js +137 -0
  72. package/src/integration.d.ts +84 -0
  73. package/src/integration.js +14 -0
@@ -0,0 +1,177 @@
1
+ import { readContract } from "./contract.js";
2
+ import { assertExecutionProfile, LEGACY_EXECUTION_PROFILE } from "./execution-profile.js";
3
+ import { getNextAction } from "./next-action.js";
4
+ import { readPersistedRoute } from "./route-artifact.js";
5
+ import { readWorkState } from "./work-state.js";
6
+
7
+ export const PROFILE_CONTEXT_POLICIES = Object.freeze({
8
+ light: Object.freeze({
9
+ contextDepth: "targeted",
10
+ output: "compact",
11
+ planDepth: "short",
12
+ guideStrategy: "targeted",
13
+ verificationStrategy: "focused",
14
+ optionalArtifacts: "lazy",
15
+ requiredSections: ["objective", "scope", "implementation", "verification"],
16
+ excludedContext: ["full-history", "full-trace", "all-schemas", "all-guides", "reflection", "unrelated-repository-context"],
17
+ }),
18
+ balanced: Object.freeze({
19
+ contextDepth: "relevant",
20
+ output: "standard",
21
+ planDepth: "standard",
22
+ guideStrategy: "relevant",
23
+ verificationStrategy: "normal",
24
+ optionalArtifacts: "lazy",
25
+ requiredSections: ["objective", "scope", "implementation", "verification", "relevant-history"],
26
+ excludedContext: ["unrelated-repository-context"],
27
+ }),
28
+ full: Object.freeze({
29
+ contextDepth: "expanded",
30
+ output: "expanded",
31
+ planDepth: "deep",
32
+ guideStrategy: "broad-risk",
33
+ verificationStrategy: "expanded-when-justified",
34
+ optionalArtifacts: "when-required-or-useful",
35
+ requiredSections: ["objective", "scope", "implementation", "verification", "risk", "relevant-history"],
36
+ excludedContext: [],
37
+ }),
38
+ });
39
+
40
+ const OPTIONAL_CONTEXT_BY_PROFILE = Object.freeze({
41
+ light: Object.freeze([]),
42
+ balanced: Object.freeze(["task-history", "relevant-artifacts"]),
43
+ full: Object.freeze([
44
+ "task-history",
45
+ "relevant-artifacts",
46
+ "risk-context",
47
+ "dependency-context",
48
+ "review-context",
49
+ "verification-context",
50
+ ]),
51
+ });
52
+
53
+ const PROFILE_INVARIANTS = Object.freeze({
54
+ lifecyclePhasesPreserved: true,
55
+ requiredGatesPreserved: true,
56
+ evidenceRequirementsPreserved: true,
57
+ verificationTruthPreserved: true,
58
+ authorityChecksPreserved: true,
59
+ provenancePreserved: true,
60
+ completionValidationPreserved: true,
61
+ safetyFloorPreserved: true,
62
+ lifecyclePhaseSkippingAllowed: false,
63
+ });
64
+
65
+ function contextError(code, message) {
66
+ const error = new Error(message);
67
+ error.code = code;
68
+ return error;
69
+ }
70
+
71
+ export function legacyExecutionProfile() {
72
+ return {
73
+ requested: LEGACY_EXECUTION_PROFILE,
74
+ floor: LEGACY_EXECUTION_PROFILE,
75
+ resolved: LEGACY_EXECUTION_PROFILE,
76
+ reasons: ["LEGACY_ROUTE_COMPATIBILITY"],
77
+ escalated: false,
78
+ };
79
+ }
80
+
81
+ export function getExecutionProfileContextPolicy(profile) {
82
+ if (!Object.prototype.hasOwnProperty.call(PROFILE_CONTEXT_POLICIES, profile)) {
83
+ throw contextError("E_EXECUTION_PROFILE_INVALID", `Unsupported resolved execution profile: ${profile}`);
84
+ }
85
+ return {
86
+ ...PROFILE_CONTEXT_POLICIES[profile],
87
+ allowedOptionalContext: [...OPTIONAL_CONTEXT_BY_PROFILE[profile]],
88
+ };
89
+ }
90
+
91
+ function verificationRequirements(contract) {
92
+ if (Array.isArray(contract.verification)) {
93
+ return contract.verification.map((requirement) => {
94
+ if (typeof requirement === "string") return { id: requirement, text: requirement, type: "VERIFICATION" };
95
+ return {
96
+ id: requirement.id ?? null,
97
+ text: requirement.text ?? null,
98
+ type: requirement.type ?? "VERIFICATION",
99
+ };
100
+ });
101
+ }
102
+ return (contract.successCriteria ?? []).map((criterion, index) => ({
103
+ id: `success-criterion-${index + 1}`,
104
+ text: criterion,
105
+ type: "SUCCESS_CRITERION",
106
+ }));
107
+ }
108
+
109
+ export function projectExecutionProfileContext({
110
+ taskId,
111
+ contract,
112
+ route,
113
+ state,
114
+ nextAction,
115
+ } = {}) {
116
+ if (typeof taskId !== "string" || !taskId) throw contextError("E_TASK_REQUIRED", "taskId is required");
117
+ if (!contract || typeof contract !== "object" || Array.isArray(contract)) {
118
+ throw contextError("E_EXECUTION_PROFILE_CONTEXT_INVALID", "contract is required for profile-aware context");
119
+ }
120
+ if (!route || typeof route !== "object" || Array.isArray(route)) {
121
+ throw contextError("E_EXECUTION_PROFILE_CONTEXT_INVALID", "route is required for profile-aware context");
122
+ }
123
+ if (!state || typeof state !== "object" || Array.isArray(state)) {
124
+ throw contextError("E_EXECUTION_PROFILE_CONTEXT_INVALID", "work state is required for profile-aware context");
125
+ }
126
+ const profile = route.executionProfile ? assertExecutionProfile(route.executionProfile) : legacyExecutionProfile();
127
+ const resolvedProfile = profile.resolved;
128
+ const policy = getExecutionProfileContextPolicy(resolvedProfile);
129
+ return {
130
+ schemaVersion: 1,
131
+ protocolVersion: 1,
132
+ taskId,
133
+ executionProfile: profile,
134
+ phase: state.phase,
135
+ nextAction: nextAction?.nextAction ?? null,
136
+ objective: contract.objective ?? null,
137
+ deliverables: [...(contract.deliverables ?? [])],
138
+ constraints: [...(contract.constraints ?? [])],
139
+ selectedGuideIds: [...(state.selectedGuides ?? [])],
140
+ verificationRequirements: verificationRequirements(contract),
141
+ contextPolicy: policy,
142
+ optionalContext: {
143
+ available: [...OPTIONAL_CONTEXT_BY_PROFILE[resolvedProfile]],
144
+ loaded: [],
145
+ },
146
+ invariants: { ...PROFILE_INVARIANTS },
147
+ };
148
+ }
149
+
150
+ export async function buildExecutionProfileContext({
151
+ target,
152
+ packageRoot,
153
+ taskId,
154
+ authorityContext,
155
+ runtimeContext,
156
+ } = {}) {
157
+ if (typeof taskId !== "string" || !taskId) throw contextError("E_TASK_REQUIRED", "taskId is required");
158
+ const [contractArtifact, routeArtifact, state] = await Promise.all([
159
+ readContract(target, packageRoot, { taskId }),
160
+ readPersistedRoute(target, packageRoot, { taskId }),
161
+ readWorkState(target, { packageRoot, taskId }),
162
+ ]);
163
+ const nextAction = await getNextAction({
164
+ target,
165
+ packageRoot,
166
+ taskId,
167
+ authorityContext,
168
+ runtimeContext,
169
+ });
170
+ return projectExecutionProfileContext({
171
+ taskId,
172
+ contract: contractArtifact.value,
173
+ route: routeArtifact.value,
174
+ state,
175
+ nextAction,
176
+ });
177
+ }
@@ -0,0 +1,248 @@
1
+ /**
2
+ * Deterministic execution-profile selection.
3
+ *
4
+ * Profiles are an efficiency and guidance dimension only. They do not alter
5
+ * compliance mode, lifecycle transitions, evidence requirements, authority,
6
+ * or completion validation. A safety floor always wins over an explicit
7
+ * lower request.
8
+ */
9
+
10
+ export const EXECUTION_PROFILES = Object.freeze(["light", "balanced", "full"]);
11
+ export const EXECUTION_PROFILE_REQUESTS = Object.freeze(["auto", ...EXECUTION_PROFILES]);
12
+ export const LEGACY_EXECUTION_PROFILE = "balanced";
13
+
14
+ const PROFILE_RANK = Object.freeze({ light: 0, balanced: 1, full: 2 });
15
+ const FULL_WORK_TYPES = new Set(["infrastructure", "security-review", "release", "api-auth"]);
16
+ const BALANCED_WORK_TYPES = new Set([
17
+ "code", "bug", "refactor", "backend", "api", "dependency-update", "performance", "accessibility",
18
+ ]);
19
+ const FULL_SURFACES = new Set(["auth", "critical-path"]);
20
+ const BALANCED_SURFACES = new Set(["data", "database", "ci", "config"]);
21
+ const FULL_RISKS = new Set([
22
+ "secrets", "personal-data", "publication", "critical-path", "destructive", "irreversible",
23
+ "production-deployment", "credentials", "payment", "migration",
24
+ ]);
25
+ const BALANCED_RISKS = new Set(["untrusted-input", "performance", "accessibility", "external-service"]);
26
+
27
+ function profileError(code, message) {
28
+ const error = new Error(message);
29
+ error.code = code;
30
+ return error;
31
+ }
32
+
33
+ function normalizeRequestedProfile(value, label) {
34
+ const normalized = value ?? "auto";
35
+ if (typeof normalized !== "string" || !EXECUTION_PROFILE_REQUESTS.includes(normalized)) {
36
+ throw profileError("E_EXECUTION_PROFILE_INVALID", `${label} must be one of ${EXECUTION_PROFILE_REQUESTS.join(", ")}`);
37
+ }
38
+ return normalized;
39
+ }
40
+
41
+ function uniqueReasons(reasons) {
42
+ return [...new Set(reasons.filter((reason) => typeof reason === "string" && reason))];
43
+ }
44
+
45
+ function normalizedRouteInput(routeInput = {}) {
46
+ return {
47
+ workType: typeof routeInput.workType === "string" ? routeInput.workType : null,
48
+ surfaces: Array.isArray(routeInput.surfaces) ? [...routeInput.surfaces] : [],
49
+ risks: Array.isArray(routeInput.risks) ? [...routeInput.risks] : [],
50
+ platforms: Array.isArray(routeInput.platforms) ? [...routeInput.platforms] : [],
51
+ behaviorChange: routeInput.behaviorChange === true,
52
+ executableChange: routeInput.executableChange === true,
53
+ };
54
+ }
55
+
56
+ function contractCollections(contract) {
57
+ const source = contract && typeof contract === "object" && !Array.isArray(contract) ? contract : {};
58
+ return {
59
+ deliverables: Array.isArray(source.deliverables) ? source.deliverables : [],
60
+ successCriteria: Array.isArray(source.successCriteria) ? source.successCriteria : [],
61
+ risks: Array.isArray(source.risks) ? source.risks : [],
62
+ constraints: Array.isArray(source.constraints) ? source.constraints : [],
63
+ stopConditions: Array.isArray(source.stopConditions) ? source.stopConditions : [],
64
+ };
65
+ }
66
+
67
+ function contractSignals(contract) {
68
+ const collections = contractCollections(contract);
69
+ const riskText = collections.risks.join(" ").toLowerCase();
70
+ const allText = [
71
+ ...collections.deliverables,
72
+ ...collections.successCriteria,
73
+ ...collections.constraints,
74
+ ...collections.stopConditions,
75
+ ].join(" ").toLowerCase();
76
+ const combinedText = `${riskText} ${allText}`;
77
+ const signals = {
78
+ secrets: /\bsecrets?\b|\bcredentials?\b|\bpasswords?\b|\bprivate keys?\b/.test(combinedText),
79
+ personalData: /\bpersonal[- ]data\b|\bpii\b|\bsensitive data\b/.test(combinedText),
80
+ publication: /\bpublication\b|\bpublish(?:ing|ed)?\b|\bdeploy(?:ment|ed)?\b/.test(combinedText),
81
+ destructive: /\bdestructive\b|\birreversible\b|\bdrop database\b|\bdelete production\b/.test(combinedText),
82
+ migration: /\bmigration\b|\bmigrate\b|\bschema change\b|\birreversible persistence\b/.test(combinedText),
83
+ payment: /\bpayments?\b|\bcheckout\b|\bbilling\b/.test(combinedText),
84
+ externalMutation: /\bexternal mutation\b|\bmutate(?:s|d)? external\b|\bpublish(?:ing|ed)?\b/.test(combinedText),
85
+ authoritySensitiveExternalMutation: /\bauthority[- ]sensitive\b|\bhost[- ]authorized external\b|\bexternal mutation requiring authority\b/.test(combinedText),
86
+ broadProductionValidation: /\bbroad production validation\b|\bproduction validation\b|\bvalidate in production\b/.test(combinedText),
87
+ };
88
+ return signals;
89
+ }
90
+
91
+ function scopeSignals({ routeInput, contract, taskDescriptor }) {
92
+ const collections = contractCollections(contract);
93
+ const claims = Array.isArray(taskDescriptor?.writeClaims) ? taskDescriptor.writeClaims : [];
94
+ const deliverableCount = collections.deliverables.length;
95
+ const claimCount = claims.length;
96
+ const successCriteriaCount = collections.successCriteria.length;
97
+ const executableSurfaceCount = new Set([
98
+ ...(routeInput.surfaces ?? []),
99
+ ...(routeInput.platforms ?? []),
100
+ ]).size;
101
+ return {
102
+ multiDeliverable: deliverableCount > 4,
103
+ multiClaim: claimCount > 4,
104
+ broad: successCriteriaCount > 6,
105
+ multiSurfaceExecutableChange: (routeInput.behaviorChange || routeInput.executableChange)
106
+ && executableSurfaceCount > 1,
107
+ };
108
+ }
109
+
110
+ function assertResolvedProfile(value) {
111
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
112
+ throw profileError("E_EXECUTION_PROFILE_INCONSISTENT", "executionProfile must be an object");
113
+ }
114
+ if (!EXECUTION_PROFILE_REQUESTS.includes(value.requested)
115
+ || !EXECUTION_PROFILES.includes(value.floor)
116
+ || !EXECUTION_PROFILES.includes(value.resolved)) {
117
+ throw profileError("E_EXECUTION_PROFILE_INCONSISTENT", "executionProfile contains an unsupported profile value");
118
+ }
119
+ if (!Array.isArray(value.reasons) || value.reasons.length === 0
120
+ || value.reasons.some((reason) => typeof reason !== "string" || !reason)) {
121
+ throw profileError("E_EXECUTION_PROFILE_INCONSISTENT", "executionProfile.reasons must be a non-empty string array");
122
+ }
123
+ if (new Set(value.reasons).size !== value.reasons.length) {
124
+ throw profileError("E_EXECUTION_PROFILE_INCONSISTENT", "executionProfile.reasons must not contain duplicates");
125
+ }
126
+ if (typeof value.escalated !== "boolean") {
127
+ throw profileError("E_EXECUTION_PROFILE_INCONSISTENT", "executionProfile.escalated must be boolean");
128
+ }
129
+ if (PROFILE_RANK[value.resolved] < PROFILE_RANK[value.floor]) {
130
+ throw profileError("E_EXECUTION_PROFILE_SAFETY_FLOOR_INVALID", "executionProfile.resolved cannot be below its safety floor");
131
+ }
132
+ if (value.requested !== "auto" && PROFILE_RANK[value.resolved] < PROFILE_RANK[value.requested]) {
133
+ throw profileError("E_EXECUTION_PROFILE_INCONSISTENT", "executionProfile.resolved cannot be below its explicit request");
134
+ }
135
+ const expectedEscalated = value.requested !== "auto"
136
+ && PROFILE_RANK[value.requested] < PROFILE_RANK[value.floor];
137
+ if (value.escalated !== expectedEscalated) {
138
+ throw profileError("E_EXECUTION_PROFILE_INCONSISTENT", "executionProfile.escalated does not match the requested profile and safety floor");
139
+ }
140
+ return value;
141
+ }
142
+
143
+ export function assertExecutionProfile(value) {
144
+ return assertResolvedProfile(value);
145
+ }
146
+
147
+ export function projectExecutionProfile(route) {
148
+ if (!route || typeof route !== "object") return null;
149
+ return route.executionProfile?.resolved ?? LEGACY_EXECUTION_PROFILE;
150
+ }
151
+
152
+ export function resolveExecutionProfile({
153
+ routeInput = {},
154
+ contract = null,
155
+ taskDescriptor = null,
156
+ configuredProfile = "auto",
157
+ requestedProfile = null,
158
+ } = {}) {
159
+ const normalizedRoute = normalizedRouteInput(routeInput);
160
+ const configured = normalizeRequestedProfile(configuredProfile, "configuredProfile");
161
+ const requested = requestedProfile === null || requestedProfile === undefined
162
+ ? configured
163
+ : normalizeRequestedProfile(requestedProfile, "requestedProfile");
164
+ const reasons = [];
165
+
166
+ if (requestedProfile !== null && requestedProfile !== undefined) reasons.push("PROFILE_EXPLICIT_REQUEST");
167
+ else if (configured !== "auto") reasons.push("PROFILE_PROJECT_CONFIG");
168
+ else reasons.push("PROFILE_DEFAULT_AUTO");
169
+
170
+ let floor = "light";
171
+ const fullReasons = [];
172
+ const balancedReasons = [];
173
+ const addFull = (reason) => fullReasons.push(reason);
174
+ const addBalanced = (reason) => balancedReasons.push(reason);
175
+
176
+ if (FULL_WORK_TYPES.has(normalizedRoute.workType)) {
177
+ addFull(`WORK_${normalizedRoute.workType.toUpperCase().replaceAll("-", "_")}`);
178
+ }
179
+ if (BALANCED_WORK_TYPES.has(normalizedRoute.workType)) {
180
+ addBalanced(`WORK_${normalizedRoute.workType.toUpperCase().replaceAll("-", "_")}`);
181
+ }
182
+ for (const surface of normalizedRoute.surfaces) {
183
+ if (FULL_SURFACES.has(surface)) addFull(`SURFACE_${surface.toUpperCase().replaceAll("-", "_")}`);
184
+ else if (BALANCED_SURFACES.has(surface)) addBalanced(`SURFACE_${surface.toUpperCase().replaceAll("-", "_")}`);
185
+ }
186
+ for (const risk of normalizedRoute.risks) {
187
+ if (FULL_RISKS.has(risk)) addFull(`RISK_${risk.toUpperCase().replaceAll("-", "_")}`);
188
+ else if (BALANCED_RISKS.has(risk)) addBalanced(`RISK_${risk.toUpperCase().replaceAll("-", "_")}`);
189
+ }
190
+ if (normalizedRoute.behaviorChange) addBalanced("WORK_BEHAVIOR_CHANGE");
191
+ if (normalizedRoute.executableChange) addBalanced("WORK_EXECUTABLE_CHANGE");
192
+
193
+ const textSignals = contractSignals(contract);
194
+ if (textSignals.secrets) addFull("RISK_SECRETS");
195
+ if (textSignals.personalData) addFull("RISK_PERSONAL_DATA");
196
+ if (textSignals.publication) addFull("RISK_PUBLICATION");
197
+ if (textSignals.destructive) addFull("RISK_DESTRUCTIVE");
198
+ if (textSignals.migration && normalizedRoute.surfaces.includes("database")) addFull("RISK_MIGRATION");
199
+ if (textSignals.payment) addFull("RISK_PAYMENT");
200
+ if (textSignals.externalMutation) addFull("RISK_EXTERNAL_MUTATION");
201
+ if (textSignals.authoritySensitiveExternalMutation) addFull("RISK_AUTHORITY_SENSITIVE_EXTERNAL_MUTATION");
202
+ if (textSignals.broadProductionValidation) addFull("RISK_PRODUCTION_VALIDATION");
203
+
204
+ const scope = scopeSignals({ routeInput: normalizedRoute, contract, taskDescriptor });
205
+ if (scope.multiDeliverable) addBalanced("SCOPE_MULTI_DELIVERABLE");
206
+ if (scope.multiClaim) addBalanced("SCOPE_MULTI_CLAIM");
207
+ if (scope.broad || scope.multiSurfaceExecutableChange) addBalanced("SCOPE_BROAD");
208
+
209
+ if (fullReasons.length > 0) {
210
+ floor = "full";
211
+ reasons.push(...fullReasons);
212
+ } else if (balancedReasons.length > 0) {
213
+ floor = "balanced";
214
+ reasons.push(...balancedReasons);
215
+ } else {
216
+ const lowRiskDocumentation = normalizedRoute.workType === "documentation"
217
+ || normalizedRoute.surfaces.includes("documentation");
218
+ const lowRiskUi = normalizedRoute.workType === "ui-copy"
219
+ || normalizedRoute.surfaces.some((surface) => ["ui", "forms", "mobile", "desktop"].includes(surface));
220
+ reasons.push(lowRiskDocumentation
221
+ ? "WORK_LOW_RISK_DOCUMENTATION"
222
+ : lowRiskUi
223
+ ? "WORK_LOW_RISK_UI"
224
+ : "NO_ESCALATION_SIGNAL");
225
+ reasons.push("NO_HIGH_RISK_SIGNAL");
226
+ reasons.push(scope.broad || scope.multiDeliverable || scope.multiClaim || scope.multiSurfaceExecutableChange
227
+ ? "SCOPE_BROAD"
228
+ : "NARROW_DELIVERABLE_SCOPE");
229
+ }
230
+
231
+ if (floor !== "full" && balancedReasons.length > 0 && fullReasons.length === 0) {
232
+ // Keep scope/risk reasons in the same deterministic order as the inputs.
233
+ for (const reason of balancedReasons) if (!reasons.includes(reason)) reasons.push(reason);
234
+ }
235
+
236
+ const requestedRank = requested === "auto" ? PROFILE_RANK[floor] : PROFILE_RANK[requested];
237
+ const resolved = EXECUTION_PROFILES.find((profile) => PROFILE_RANK[profile] === Math.max(requestedRank, PROFILE_RANK[floor]));
238
+ const escalated = requested !== "auto" && PROFILE_RANK[requested] < PROFILE_RANK[floor];
239
+ if (escalated) reasons.push("PROFILE_ESCALATED_BY_SAFETY");
240
+
241
+ return assertResolvedProfile({
242
+ requested,
243
+ floor,
244
+ resolved,
245
+ reasons: uniqueReasons(reasons),
246
+ escalated,
247
+ });
248
+ }
@@ -29,6 +29,7 @@ const READ_ONLY_COMMANDS = Object.freeze(new Set([
29
29
  "inspect", "validate-state", "validate-protocol", "validate-receipt",
30
30
  "policy-status", "policy-diff", "rule-verify", "policy",
31
31
  "history", "trace", "reflect",
32
+ "efficiency",
32
33
  "workspace-status", "handoff-list", "handoff-show", "responsibility-status",
33
34
  "attestation-verify", "attestation-status", "attestation-verify-range",
34
35
  ]));
@@ -39,6 +40,7 @@ const LOOP_MUTATION_COMMANDS = Object.freeze(new Set([
39
40
  "record-check", "record-diagnosis", "record-decision-criterion",
40
41
  "record-terminal-result", "complete",
41
42
  "record-intervention", "record-hypothesis-disposition",
43
+ "usage-record",
42
44
  "workspace-bind", "handoff-create", "responsibility-set", "verify-scope", "attestation-create",
43
45
  ]));
44
46
 
@@ -59,6 +61,8 @@ const STATIC_RISK_CLASSES = Object.freeze({
59
61
  eval: INTEGRATION_RISK_CLASSES.LOOP_MUTATION,
60
62
  "action-show": INTEGRATION_RISK_CLASSES.READ_ONLY,
61
63
  metrics: INTEGRATION_RISK_CLASSES.READ_ONLY,
64
+ efficiency: INTEGRATION_RISK_CLASSES.READ_ONLY,
65
+ "usage-record": INTEGRATION_RISK_CLASSES.LOOP_MUTATION,
62
66
  init: INTEGRATION_RISK_CLASSES.MAINTENANCE,
63
67
  update: INTEGRATION_RISK_CLASSES.MAINTENANCE,
64
68
  activate: INTEGRATION_RISK_CLASSES.MAINTENANCE,
@@ -137,6 +141,44 @@ export function getForgeLoopCapabilities({ packageVersion = null } = {}) {
137
141
  readOnlyMetrics: true,
138
142
  projectLocalReference: true,
139
143
  },
144
+ adaptiveExecutionProfiles: {
145
+ version: 1,
146
+ supported: true,
147
+ deterministic: true,
148
+ lifecycleFastPath: false,
149
+ },
150
+ executionProfileContext: {
151
+ version: 1,
152
+ supported: true,
153
+ resource: "task/context",
154
+ resolvedProfileAuthoritative: true,
155
+ compatibilityFallback: "balanced",
156
+ lifecycleFastPath: false,
157
+ },
158
+ compactLifecycleOutput: {
159
+ version: 1,
160
+ supported: true,
161
+ commands: ["next", "task-show"],
162
+ preservesDefaultOutput: true,
163
+ },
164
+ usageTelemetry: {
165
+ version: 1,
166
+ supported: true,
167
+ estimation: false,
168
+ evidence: false,
169
+ },
170
+ efficiencyMetrics: {
171
+ version: 1,
172
+ supported: true,
173
+ comparativeOnly: true,
174
+ },
175
+ contextUsageObservability: {
176
+ version: 1,
177
+ supported: true,
178
+ sources: ["HOST_REPORTED", "UNKNOWN"],
179
+ estimation: false,
180
+ inflationStatus: "OBSERVATIONAL",
181
+ },
140
182
  workspaceBinding: {
141
183
  version: 1,
142
184
  supported: true,
@@ -194,6 +236,7 @@ export function getForgeLoopCapabilities({ packageVersion = null } = {}) {
194
236
  { name: "task/action", scope: "TASK" },
195
237
  { name: "task/approvals", scope: "TASK" },
196
238
  { name: "task/metrics", scope: "TASK" },
239
+ { name: "task/context", scope: "TASK" },
197
240
  { name: "task/evaluations", scope: "TASK" },
198
241
  { name: "project/capability-policy", scope: "PROJECT" },
199
242
  ],
@@ -17,6 +17,7 @@ import { listCanonicalHandoffs } from "./handoff.js";
17
17
  import { resolveResponsibilityStatus } from "./responsibility.js";
18
18
  import { readVerificationScope } from "./verification-scope.js";
19
19
  import { resolveAttestationStatus } from "./attestation.js";
20
+ import { buildExecutionProfileContext } from "./execution-profile-context.js";
20
21
 
21
22
  /**
22
23
  * Canonical integration resource allowlist.
@@ -80,6 +81,7 @@ export const INTEGRATION_RESOURCE_DEFINITIONS = Object.freeze({
80
81
  "task/action": Object.freeze({ scope: "TASK", description: "One canonical durable action artifact." }),
81
82
  "task/approvals": Object.freeze({ scope: "TASK", description: "Durable approval artifacts for one task." }),
82
83
  "task/metrics": Object.freeze({ scope: "TASK", description: "Read-only trajectory metrics for one task." }),
84
+ "task/context": Object.freeze({ scope: "TASK", description: "Read-only profile-aware task context with bounded presentation policy." }),
83
85
  "task/evaluations": Object.freeze({ scope: "TASK", description: "Persisted trajectory evaluations for one task." }),
84
86
  "project/capability-policy": Object.freeze({ scope: "PROJECT", description: "Project capability policy, never host authority." }),
85
87
  });
@@ -104,6 +106,7 @@ export async function readForgeLoopIntegrationResource(uri, {
104
106
  packageVersion = null,
105
107
  taskId = null,
106
108
  actionId = null,
109
+ runtimeContext = null,
107
110
  } = {}) {
108
111
  const resource = INTEGRATION_RESOURCE_DEFINITIONS[uri];
109
112
  if (!resource) {
@@ -158,6 +161,7 @@ export async function readForgeLoopIntegrationResource(uri, {
158
161
  case "task/action":
159
162
  case "task/approvals":
160
163
  case "task/metrics":
164
+ case "task/context":
161
165
  case "task/evaluations": {
162
166
  if (typeof taskId !== "string" || !taskId) {
163
167
  const error = new Error(`Resource ${uri} requires a taskId`); error.code = "E_TASK_REQUIRED"; throw error;
@@ -199,7 +203,24 @@ export async function readForgeLoopIntegrationResource(uri, {
199
203
  return { uri, taskId, data: { approvals: await listApprovals(projectPath, { packageRoot, taskId }) } };
200
204
  }
201
205
  if (uri === "task/metrics") {
202
- return { uri, taskId, data: await buildTrajectoryMetrics({ target: projectPath, packageRoot, taskId }) };
206
+ return {
207
+ uri,
208
+ taskId,
209
+ data: await buildTrajectoryMetrics({ target: projectPath, packageRoot, taskId, runtimeContext }),
210
+ };
211
+ }
212
+ if (uri === "task/context") {
213
+ return {
214
+ uri,
215
+ taskId,
216
+ data: await buildExecutionProfileContext({
217
+ target: projectPath,
218
+ packageRoot,
219
+ taskId,
220
+ authorityContext: runtimeContext?.authorityContext,
221
+ runtimeContext,
222
+ }),
223
+ };
203
224
  }
204
225
  if (uri === "task/evaluations") {
205
226
  const dir = path.join(projectPath, taskDirectory(taskId), "evaluations");
@@ -93,6 +93,48 @@ export function protocolInfo({ packageVersion = null } = {}) {
93
93
  capabilityPolicy: { version: 1, supported: true, decisions: ["ALLOW", "DENY", "REQUIRE_AUTHORITY", "REQUIRE_APPROVAL"] },
94
94
  durableApprovals: { version: 1, supported: true, fingerprintBound: true, hostAttestationMinting: false },
95
95
  trajectoryMetrics: { version: 1, supported: true, usageUnknownWhenUnreported: true, overallScore: false },
96
+ adaptiveExecutionProfiles: {
97
+ version: 1,
98
+ supported: true,
99
+ requests: ["auto", "light", "balanced", "full"],
100
+ resolvedProfiles: ["light", "balanced", "full"],
101
+ deterministic: true,
102
+ lifecycleFastPath: false,
103
+ },
104
+ executionProfileContext: {
105
+ version: 1,
106
+ supported: true,
107
+ resource: "task/context",
108
+ resolvedProfileAuthoritative: true,
109
+ compatibilityFallback: "balanced",
110
+ lifecycleFastPath: false,
111
+ },
112
+ compactLifecycleOutput: {
113
+ version: 1,
114
+ supported: true,
115
+ commands: ["next", "task-show"],
116
+ preservesDefaultOutput: true,
117
+ },
118
+ usageTelemetry: {
119
+ version: 1,
120
+ supported: true,
121
+ sources: ["PROVIDER_REPORTED", "HOST_REPORTED", "ACTOR_REPORTED", "UNKNOWN"],
122
+ estimation: false,
123
+ evidence: false,
124
+ },
125
+ efficiencyMetrics: {
126
+ version: 1,
127
+ supported: true,
128
+ comparativeOnly: true,
129
+ baseline: "OPTIONAL_PROJECT_LOCAL_JSON",
130
+ },
131
+ contextUsageObservability: {
132
+ version: 1,
133
+ supported: true,
134
+ sources: ["HOST_REPORTED", "UNKNOWN"],
135
+ estimation: false,
136
+ inflationStatus: "OBSERVATIONAL",
137
+ },
96
138
  trajectoryEvaluation: { version: 1, supported: true, requiresReferenceScenario: true, source: "PROJECT_LOCAL_REFERENCE" },
97
139
  workspaceBinding: {
98
140
  version: 1,
@@ -26,7 +26,7 @@ async function deriveResumePhaseFromLedger(target, packageRoot, taskId) {
26
26
  return null;
27
27
  }
28
28
  if (!ledger?.valid) return null;
29
- const scoped = (ledger.events ?? []).filter((event) => event.taskId === taskId);
29
+ const scoped = (ledger.events ?? []).filter((event) => !taskId || event.taskId === taskId);
30
30
  if (scoped.some((event) => event.event === "COMPLETION_VALIDATED")) return null;
31
31
  const positions = RESUME_PHASE_BY_MILESTONE;
32
32
  let derived = null;
@@ -42,6 +42,20 @@ async function deriveResumePhaseFromLedger(target, packageRoot, taskId) {
42
42
  return derived;
43
43
  }
44
44
 
45
+ function deriveVerificationCycleFromLedger(events) {
46
+ const cycleEvents = new Set([
47
+ "VERIFICATION_STARTED",
48
+ "VERIFICATION_RECORDED",
49
+ "DIAGNOSIS_RECORDED",
50
+ "DIAGNOSTIC_CASE_RECORDED",
51
+ ]);
52
+ const cycles = events
53
+ .filter((event) => cycleEvents.has(event.event))
54
+ .map((event) => event.details?.verificationCycle)
55
+ .filter((cycle) => Number.isInteger(cycle) && cycle >= 1);
56
+ return cycles.at(-1);
57
+ }
58
+
45
59
  function resumeSteps(phase) {
46
60
  if (phase === "EXECUTING" || phase === "VERIFYING") {
47
61
  return {
@@ -61,6 +75,17 @@ export async function ensureResumableState({ target, packageRoot, contract, rout
61
75
  if (existing) return existing;
62
76
 
63
77
  const resumedPhase = await deriveResumePhaseFromLedger(target, packageRoot, taskId) ?? "ROUTED";
78
+ let verificationCycle;
79
+ try {
80
+ const ledger = await validateEventLedger(target, packageRoot, { taskId });
81
+ if (ledger.valid) {
82
+ verificationCycle = deriveVerificationCycleFromLedger(
83
+ (ledger.events ?? []).filter((event) => !taskId || event.taskId === taskId),
84
+ );
85
+ }
86
+ } catch {
87
+ verificationCycle = undefined;
88
+ }
64
89
  const steps = resumeSteps(resumedPhase);
65
90
  const state = createWorkState({
66
91
  taskId: contract.value.taskId,
@@ -71,6 +96,7 @@ export async function ensureResumableState({ target, packageRoot, contract, rout
71
96
  selectedGuides: route.value.guides,
72
97
  completedSteps: steps.completedSteps,
73
98
  pendingSteps: steps.pendingSteps,
99
+ ...(verificationCycle !== undefined ? { verificationCycle } : {}),
74
100
  checks: [],
75
101
  failures: [],
76
102
  blockers: [],