@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.
- package/AGENT_COMPATIBILITY.md +11 -0
- package/DOCS_INDEX.md +1 -0
- package/GUIDE_ROUTER.md +26 -1
- package/LOOP_ENGINEERING.md +48 -0
- package/ORCHESTRATOR_INTEGRATION.md +38 -0
- package/PROTOCOL_INTEGRATION.md +62 -0
- package/README.md +25 -0
- package/benchmarks/execution-profiles/README.md +44 -0
- package/benchmarks/execution-profiles/api-feature.json +18 -0
- package/benchmarks/execution-profiles/authentication-change.json +18 -0
- package/benchmarks/execution-profiles/documentation-correction.json +18 -0
- package/benchmarks/execution-profiles/infrastructure-release.json +18 -0
- package/benchmarks/execution-profiles/novatask-saas-landing-page.json +36 -0
- package/benchmarks/execution-profiles/small-bug-fix.json +18 -0
- package/benchmarks/execution-profiles/static-landing-page.json +18 -0
- package/completions/_forgeloop +6 -4
- package/completions/forgeloop.bash +8 -4
- package/completions/forgeloop.fish +24 -1
- package/docs/AGENT_PROTOCOL_SUMMARY.md +45 -1
- package/docs/ARTIFACT_REFERENCE.md +53 -7
- package/docs/CLI_REFERENCE.md +83 -1
- package/docs/EXECUTION_PROFILE_BENCHMARKS.md +208 -0
- package/docs/GETTING_STARTED.md +28 -0
- package/docs/MCP.md +6 -0
- package/docs/RELEASE_CHECKLIST.md +4 -0
- package/docs/TROUBLESHOOTING.md +6 -0
- package/docs/UNIVERSAL_INTEGRATION.md +58 -0
- package/package.json +14 -2
- package/schemas/config.schema.json +1 -0
- package/schemas/execution-profile-benchmark-aggregate.schema.json +43 -0
- package/schemas/execution-profile-benchmark-run.schema.json +106 -0
- package/schemas/execution-profile-benchmark-scenario.schema.json +66 -0
- package/schemas/routing-result.schema.json +12 -0
- package/schemas/usage.schema.json +30 -0
- package/scripts/check-efficiency-regression.mjs +99 -0
- package/scripts/generate-agent-protocol-summary.mjs +35 -0
- package/scripts/lib/execution-profile-benchmark-io.mjs +67 -0
- package/scripts/run-execution-profile-benchmarks.mjs +265 -0
- package/scripts/summarize-execution-profile-benchmarks.mjs +84 -0
- package/scripts/validate-execution-profile-benchmarks.mjs +120 -0
- package/src/cli.js +16 -4
- package/src/commands/efficiency.js +12 -0
- package/src/commands/eval.js +8 -2
- package/src/commands/metrics.js +2 -2
- package/src/commands/next.js +26 -2
- package/src/commands/route.js +20 -2
- package/src/commands/task-show.js +31 -2
- package/src/commands/usage-record.js +61 -0
- package/src/core/artifact-registry.js +12 -0
- package/src/core/cli-command-definitions.js +27 -0
- package/src/core/command-executors.js +29 -5
- package/src/core/command-input.js +34 -0
- package/src/core/config.js +9 -0
- package/src/core/efficiency.js +197 -0
- package/src/core/error-codes.js +18 -0
- package/src/core/execution-profile-benchmarks.js +674 -0
- package/src/core/execution-profile-context.js +177 -0
- package/src/core/execution-profile.js +248 -0
- package/src/core/integration-invocation-policy.js +43 -0
- package/src/core/integration-resources.js +22 -1
- package/src/core/protocol-info.js +42 -0
- package/src/core/resumability.js +27 -1
- package/src/core/router.js +23 -1
- package/src/core/runtime-context.js +11 -0
- package/src/core/schema-validation.js +4 -0
- package/src/core/task-paths.js +2 -0
- package/src/core/templates.js +4 -0
- package/src/core/trace.js +1 -0
- package/src/core/trajectory-evaluation.js +2 -2
- package/src/core/trajectory-metrics.js +18 -2
- package/src/core/usage.js +137 -0
- package/src/integration.d.ts +84 -0
- 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 {
|
|
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,
|
package/src/core/resumability.js
CHANGED
|
@@ -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: [],
|