@cassiomc1/forgeloop 0.1.3 → 0.1.5
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/.cursor/rules/project-loop.mdc +14 -0
- package/.github/copilot-instructions.md +14 -0
- package/AGENTS.md +14 -0
- package/AGENT_COMPATIBILITY.md +42 -0
- package/CLAUDE.md +14 -0
- package/LOOP_ENGINEERING.md +254 -15
- package/QUALITY_SCORECARD.md +20 -7
- package/conformance/README.md +28 -0
- package/conformance/runs/2026-08-13-codex-fifth-live.md +386 -0
- package/conformance/runs/2026-08-13-codex-fourth-live.md +309 -0
- package/package.json +1 -1
- package/src/core/decision-classification.js +55 -0
- package/src/core/workflow-compatibility.js +151 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export const BLOCKING_REASON_CODES = Object.freeze([
|
|
2
|
+
"REAL_BUSINESS_FACT_REQUIRED",
|
|
3
|
+
"SENSITIVE_VALUE_REQUIRED",
|
|
4
|
+
"EXTERNAL_AUTHORITY_REQUIRED",
|
|
5
|
+
"IRREVERSIBLE_DECISION_REQUIRED",
|
|
6
|
+
"REGULATED_CLAIM_REQUIRED",
|
|
7
|
+
"DESTRUCTIVE_ACTION_REQUIRED",
|
|
8
|
+
]);
|
|
9
|
+
|
|
10
|
+
const BLOCKING_RULES = Object.freeze([
|
|
11
|
+
["realBusinessFact", "REAL_BUSINESS_FACT_REQUIRED"],
|
|
12
|
+
["sensitive", "SENSITIVE_VALUE_REQUIRED"],
|
|
13
|
+
["authoritative", "EXTERNAL_AUTHORITY_REQUIRED"],
|
|
14
|
+
["external", "EXTERNAL_AUTHORITY_REQUIRED"],
|
|
15
|
+
["irreversible", "IRREVERSIBLE_DECISION_REQUIRED"],
|
|
16
|
+
["regulatedClaim", "REGULATED_CLAIM_REQUIRED"],
|
|
17
|
+
["destructive", "DESTRUCTIVE_ACTION_REQUIRED"],
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
function blockingDecision(reasonCode) {
|
|
21
|
+
return {
|
|
22
|
+
classification: "BLOCKING",
|
|
23
|
+
reasonCode,
|
|
24
|
+
blockingReason: reasonCode,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function classifyDecision(flags = {}) {
|
|
29
|
+
if (!flags || typeof flags !== "object" || Array.isArray(flags)) {
|
|
30
|
+
return { classification: "NON_BLOCKING", reasonCode: "SAFE_REVERSIBLE_LOCAL_DEFAULT", blockingReason: null };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
for (const [flag, reasonCode] of BLOCKING_RULES) {
|
|
34
|
+
if (flags[flag] === true) return blockingDecision(reasonCode);
|
|
35
|
+
}
|
|
36
|
+
if (flags.reversible === false || flags.local === false) {
|
|
37
|
+
return blockingDecision(
|
|
38
|
+
flags.reversible === false
|
|
39
|
+
? "IRREVERSIBLE_DECISION_REQUIRED"
|
|
40
|
+
: "REAL_BUSINESS_FACT_REQUIRED",
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
classification: "NON_BLOCKING",
|
|
46
|
+
reasonCode: "SAFE_REVERSIBLE_LOCAL_DEFAULT",
|
|
47
|
+
blockingReason: null,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function canAskUser(decision) {
|
|
52
|
+
return decision?.classification === "BLOCKING"
|
|
53
|
+
&& BLOCKING_REASON_CODES.includes(decision.reasonCode)
|
|
54
|
+
&& decision.blockingReason === decision.reasonCode;
|
|
55
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { canAskUser } from "./decision-classification.js";
|
|
2
|
+
|
|
3
|
+
export const QUESTION_SOURCES = Object.freeze([
|
|
4
|
+
"USER_REQUIREMENT",
|
|
5
|
+
"FORGELOOP_BLOCKING_DECISION",
|
|
6
|
+
"EXTERNAL_WORKFLOW_POLICY",
|
|
7
|
+
"MODEL_PREFERENCE",
|
|
8
|
+
]);
|
|
9
|
+
|
|
10
|
+
export const EXTERNAL_WORKFLOW_REASON_CODES = Object.freeze({
|
|
11
|
+
APPROVAL_CONFLICT: "E_EXTERNAL_WORKFLOW_APPROVAL_CONFLICT",
|
|
12
|
+
BLOCKS_NON_BLOCKING: "E_EXTERNAL_WORKFLOW_BLOCKS_NON_BLOCKING",
|
|
13
|
+
REQUIRES_USER_GATE: "E_EXTERNAL_WORKFLOW_REQUIRES_USER_GATE",
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const DECISION_CLASSIFICATIONS = new Set(["NON_BLOCKING", "BLOCKING"]);
|
|
17
|
+
|
|
18
|
+
function normalizeDecision(value) {
|
|
19
|
+
if (typeof value === "string") {
|
|
20
|
+
if (!DECISION_CLASSIFICATIONS.has(value)) {
|
|
21
|
+
throw new TypeError(`forgeLoopDecision classification is invalid: ${value}`);
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
classification: value,
|
|
25
|
+
reasonCode: value === "NON_BLOCKING" ? "SAFE_REVERSIBLE_LOCAL_DEFAULT" : null,
|
|
26
|
+
blockingReason: null,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
30
|
+
throw new TypeError("forgeLoopDecision must be a decision object or classification");
|
|
31
|
+
}
|
|
32
|
+
if (!DECISION_CLASSIFICATIONS.has(value.classification)) {
|
|
33
|
+
throw new TypeError(`forgeLoopDecision classification is invalid: ${value.classification}`);
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
classification: value.classification,
|
|
37
|
+
reasonCode: value.reasonCode ?? null,
|
|
38
|
+
blockingReason: value.blockingReason ?? null,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function assertBoolean(value, label) {
|
|
43
|
+
if (typeof value !== "boolean") throw new TypeError(`${label} must be a boolean`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function workflowName(value) {
|
|
47
|
+
if (value === undefined) return "external-workflow";
|
|
48
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
49
|
+
throw new TypeError("workflowName must be a non-empty string");
|
|
50
|
+
}
|
|
51
|
+
return value;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function compatibleResult({ decision, name, autonomousMode, requiresUserApproval }) {
|
|
55
|
+
return {
|
|
56
|
+
status: "COMPATIBLE",
|
|
57
|
+
compatible: true,
|
|
58
|
+
autonomousMode,
|
|
59
|
+
workflowName: name,
|
|
60
|
+
forgeLoopClassification: decision.classification,
|
|
61
|
+
requiresUserApproval,
|
|
62
|
+
questionSource: null,
|
|
63
|
+
canAskUser: false,
|
|
64
|
+
requiresUserDecision: false,
|
|
65
|
+
recordAs: null,
|
|
66
|
+
addsUnresolvedDecision: false,
|
|
67
|
+
reasonCodes: [],
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Evaluates an external workflow policy after ForgeLoop classifies the decision.
|
|
73
|
+
* The helper records a policy conflict instead of turning a reversible local
|
|
74
|
+
* default into a fake user blocker or mutating the decision classifier.
|
|
75
|
+
*/
|
|
76
|
+
export function classifyWorkflowCompatibility({
|
|
77
|
+
forgeLoopDecision,
|
|
78
|
+
workflowName: requestedWorkflowName,
|
|
79
|
+
requiresUserApproval = false,
|
|
80
|
+
autonomousMode,
|
|
81
|
+
} = {}) {
|
|
82
|
+
assertBoolean(autonomousMode, "autonomousMode");
|
|
83
|
+
assertBoolean(requiresUserApproval, "requiresUserApproval");
|
|
84
|
+
|
|
85
|
+
const decision = normalizeDecision(forgeLoopDecision);
|
|
86
|
+
const name = workflowName(requestedWorkflowName);
|
|
87
|
+
|
|
88
|
+
if (!requiresUserApproval) {
|
|
89
|
+
return compatibleResult({
|
|
90
|
+
decision,
|
|
91
|
+
name,
|
|
92
|
+
autonomousMode,
|
|
93
|
+
requiresUserApproval,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (decision.classification === "NON_BLOCKING") {
|
|
98
|
+
if (!autonomousMode) {
|
|
99
|
+
return {
|
|
100
|
+
status: "INTERACTIVE_APPROVAL",
|
|
101
|
+
compatible: true,
|
|
102
|
+
autonomousMode,
|
|
103
|
+
workflowName: name,
|
|
104
|
+
forgeLoopClassification: decision.classification,
|
|
105
|
+
requiresUserApproval,
|
|
106
|
+
questionSource: "EXTERNAL_WORKFLOW_POLICY",
|
|
107
|
+
canAskUser: true,
|
|
108
|
+
requiresUserDecision: true,
|
|
109
|
+
recordAs: null,
|
|
110
|
+
addsUnresolvedDecision: true,
|
|
111
|
+
reasonCodes: [EXTERNAL_WORKFLOW_REASON_CODES.REQUIRES_USER_GATE],
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
status: "WORKFLOW_CONFLICT",
|
|
117
|
+
compatible: false,
|
|
118
|
+
autonomousMode,
|
|
119
|
+
workflowName: name,
|
|
120
|
+
forgeLoopClassification: decision.classification,
|
|
121
|
+
requiresUserApproval,
|
|
122
|
+
questionSource: "EXTERNAL_WORKFLOW_POLICY",
|
|
123
|
+
canAskUser: false,
|
|
124
|
+
requiresUserDecision: false,
|
|
125
|
+
recordAs: "WORKFLOW_CONFLICT",
|
|
126
|
+
addsUnresolvedDecision: false,
|
|
127
|
+
reasonCodes: [
|
|
128
|
+
EXTERNAL_WORKFLOW_REASON_CODES.APPROVAL_CONFLICT,
|
|
129
|
+
EXTERNAL_WORKFLOW_REASON_CODES.BLOCKS_NON_BLOCKING,
|
|
130
|
+
EXTERNAL_WORKFLOW_REASON_CODES.REQUIRES_USER_GATE,
|
|
131
|
+
],
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const legitimateBlockingQuestion = canAskUser(decision);
|
|
136
|
+
return {
|
|
137
|
+
...compatibleResult({
|
|
138
|
+
decision,
|
|
139
|
+
name,
|
|
140
|
+
autonomousMode,
|
|
141
|
+
requiresUserApproval,
|
|
142
|
+
}),
|
|
143
|
+
questionSource: legitimateBlockingQuestion ? "FORGELOOP_BLOCKING_DECISION" : null,
|
|
144
|
+
canAskUser: legitimateBlockingQuestion,
|
|
145
|
+
requiresUserDecision: legitimateBlockingQuestion,
|
|
146
|
+
addsUnresolvedDecision: legitimateBlockingQuestion,
|
|
147
|
+
reasonCodes: legitimateBlockingQuestion
|
|
148
|
+
? [EXTERNAL_WORKFLOW_REASON_CODES.REQUIRES_USER_GATE]
|
|
149
|
+
: [],
|
|
150
|
+
};
|
|
151
|
+
}
|