@cassiomc1/forgeloop 1.3.0 → 1.6.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/.github/copilot-instructions.md +1 -0
- package/AGENTS.md +1 -0
- package/CLAUDE.md +1 -0
- package/DOCS_INDEX.md +20 -8
- package/EXECUTION_STATE.md +60 -0
- package/LOOP_ENGINEERING.md +135 -5
- package/LOOP_SYSTEM_DESIGN.md +54 -1
- package/PROTOCOL_INTEGRATION.md +87 -0
- package/QUALITY_SCORECARD.md +2 -0
- package/README.md +69 -9
- package/TERMINOLOGY.md +15 -0
- package/THIRD_PARTY_NOTICES.md +30 -0
- package/THREAT_MODEL.md +59 -1
- package/docs/ARTIFACT_REFERENCE.md +183 -0
- package/docs/CLI_REFERENCE.md +391 -6
- package/docs/CROSS_HARNESS_CONTINUITY.md +23 -0
- package/docs/DIAGNOSTIC_MODEL.md +181 -0
- package/docs/DOCUMENTATION_GUIDE.md +36 -13
- package/docs/EXECUTION_TRACE.md +76 -0
- package/docs/GETTING_STARTED.md +1 -0
- package/docs/MCP.md +159 -0
- package/docs/RECIPES.md +149 -0
- package/docs/RELEASE_CHECKLIST_1_4.md +38 -0
- package/docs/RELEASE_CHECKLIST_1_5_MCP.md +78 -0
- package/docs/TROUBLESHOOTING.md +217 -3
- package/docs/UNIVERSAL_INTEGRATION.md +48 -0
- package/docs/assets/diagrams/forgeloop-engineering-flow.html +13797 -0
- package/docs/assets/diagrams/forgeloop-engineering-flow.receipt.json +37 -0
- package/docs/assets/diagrams/forgeloop-engineering-flow.svg +5002 -0
- package/docs/diagrams/README.md +55 -0
- package/docs/diagrams/forgeloop-engineering-flow.workflow.json +122 -0
- package/docs/diagrams/manifest.json +42 -0
- package/docs/diagrams/reviews/forgeloop-engineering-flow.review.json +20 -0
- package/package.json +21 -8
- package/schemas/action.schema.json +100 -0
- package/schemas/approval.schema.json +51 -0
- package/schemas/capability-policy.schema.json +41 -0
- package/schemas/diagnostic-case.schema.json +85 -0
- package/schemas/execution-receipt.schema.json +16 -0
- package/schemas/hypothesis-disposition.schema.json +16 -0
- package/schemas/intervention.schema.json +27 -0
- package/schemas/policy-lock.schema.json +1 -0
- package/schemas/policy-snapshot.schema.json +2 -0
- package/schemas/task-recovery.schema.json +61 -0
- package/schemas/trajectory-evaluation.schema.json +64 -0
- package/schemas/trajectory-scenario.schema.json +42 -0
- package/src/cli.js +267 -347
- package/src/commands/action-authorize.js +41 -0
- package/src/commands/action-propose.js +10 -0
- package/src/commands/action-reconcile.js +10 -0
- package/src/commands/action-record.js +47 -0
- package/src/commands/action-show.js +10 -0
- package/src/commands/action-verify.js +10 -0
- package/src/commands/advance.js +7 -2
- package/src/commands/approval-request.js +64 -0
- package/src/commands/approval-resolve.js +10 -0
- package/src/commands/audit.js +5 -0
- package/src/commands/baseline.js +3 -3
- package/src/commands/eval.js +6 -0
- package/src/commands/history.js +18 -0
- package/src/commands/init.js +2 -2
- package/src/commands/inspect.js +55 -0
- package/src/commands/metrics.js +7 -0
- package/src/commands/next.js +8 -2
- package/src/commands/policy-discover.js +2 -2
- package/src/commands/progress.js +6 -2
- package/src/commands/record-diagnosis.js +37 -1
- package/src/commands/record-hypothesis-disposition.js +45 -0
- package/src/commands/record-intervention.js +35 -0
- package/src/commands/reflect.js +38 -0
- package/src/commands/report.js +9 -1
- package/src/commands/run-action.js +18 -0
- package/src/commands/status.js +17 -0
- package/src/commands/task-create.js +39 -1
- package/src/commands/task-list.js +14 -1
- package/src/commands/task-lock-status.js +2 -2
- package/src/commands/task-recover.js +202 -0
- package/src/commands/task-repair-legacy-recovery.js +417 -0
- package/src/commands/task-resume.js +172 -0
- package/src/commands/task-scope.js +23 -4
- package/src/commands/task-show.js +18 -4
- package/src/commands/trace.js +34 -0
- package/src/commands/validate-protocol.js +40 -15
- package/src/core/action-authorization.js +106 -0
- package/src/core/action-constants.js +86 -0
- package/src/core/action-execution.js +105 -0
- package/src/core/action-ledger-projection.js +302 -0
- package/src/core/action-model.js +581 -0
- package/src/core/action-readiness.js +141 -0
- package/src/core/action-reconciliation-policy.js +49 -0
- package/src/core/action-reconciliation.js +66 -0
- package/src/core/action-verification.js +111 -0
- package/src/core/actions.js +462 -0
- package/src/core/approvals.js +405 -0
- package/src/core/artifact-registry.js +60 -0
- package/src/core/audit.js +45 -4
- package/src/core/bundles.js +30 -0
- package/src/core/capability-policy.js +226 -0
- package/src/core/cli-command-definitions.js +260 -5
- package/src/core/command-executors.js +543 -0
- package/src/core/command-input.js +107 -0
- package/src/core/command-runtime.js +117 -0
- package/src/core/completion-artifacts.js +39 -15
- package/src/core/completion-ownership.js +88 -0
- package/src/core/completion-recovery-rebind.js +194 -0
- package/src/core/completion.js +70 -0
- package/src/core/continuity-reconciliation.js +24 -5
- package/src/core/diagnostic-model.js +396 -0
- package/src/core/diagnostic-projection.js +51 -0
- package/src/core/diagnostic-record.js +360 -0
- package/src/core/error-codes.js +461 -1
- package/src/core/events.js +171 -2
- package/src/core/execution-prerequisites.js +4 -1
- package/src/core/execution.js +26 -188
- package/src/core/failure-signature.js +70 -0
- package/src/core/failure-surface.js +57 -0
- package/src/core/filesystem.js +55 -6
- package/src/core/history.js +110 -0
- package/src/core/hypothesis-projection.js +85 -0
- package/src/core/information-gain-projection.js +283 -0
- package/src/core/information-gain.js +138 -0
- package/src/core/inspect.js +132 -7
- package/src/core/integration-invocation-policy.js +217 -0
- package/src/core/integration-limits.js +20 -0
- package/src/core/integration-resources.js +178 -0
- package/src/core/next-action-model.js +94 -0
- package/src/core/next-action.js +490 -3
- package/src/core/phase.js +42 -22
- package/src/core/policy-engine.js +113 -6
- package/src/core/preflight-consistency.js +31 -5
- package/src/core/preflight.js +19 -2
- package/src/core/prepared-execution.js +227 -0
- package/src/core/progress.js +41 -4
- package/src/core/project-root.js +21 -0
- package/src/core/protocol-info.js +61 -0
- package/src/core/protocol.js +14 -0
- package/src/core/receipt.js +1 -0
- package/src/core/reconcile-closure.js +35 -10
- package/src/core/recovery-history.js +116 -0
- package/src/core/reflection.js +305 -0
- package/src/core/resumability.js +57 -3
- package/src/core/schema-validation.js +9 -0
- package/src/core/strategy-analysis.js +97 -0
- package/src/core/task-claim-state.js +272 -0
- package/src/core/task-command.js +5 -1
- package/src/core/task-conflict-inspection.js +321 -0
- package/src/core/task-context.js +32 -29
- package/src/core/task-discovery.js +14 -1
- package/src/core/task-lock.js +216 -22
- package/src/core/task-paths.js +31 -2
- package/src/core/task-recovery-migration.js +192 -0
- package/src/core/task-recovery.js +205 -0
- package/src/core/task-scope.js +33 -1
- package/src/core/task-snapshot.js +53 -0
- package/src/core/templates.js +9 -0
- package/src/core/trace.js +548 -0
- package/src/core/trajectory-evaluation.js +71 -0
- package/src/core/trajectory-metrics.js +80 -0
- package/src/core/transaction.js +36 -2
- package/src/core/work-state.js +10 -5
- package/src/integration.js +47 -0
- package/docs/assets/forgeloop-flow.svg +0 -1
- package/docs/forgeloop-flow.mmd +0 -51
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
export const GAIN_DIMENSIONS = Object.freeze([
|
|
2
|
+
"newObservation",
|
|
3
|
+
"newContributor",
|
|
4
|
+
"newHypothesis",
|
|
5
|
+
"hypothesisDispositionChanged",
|
|
6
|
+
"failureSignatureChanged",
|
|
7
|
+
"failureSurfaceChanged",
|
|
8
|
+
"interventionChanged",
|
|
9
|
+
"strategyChanged",
|
|
10
|
+
"newEvidence",
|
|
11
|
+
"hypothesisEliminated",
|
|
12
|
+
]);
|
|
13
|
+
|
|
14
|
+
function statementSet(values) {
|
|
15
|
+
return new Set((values ?? []).map((value) => `${value}`.trim().toLowerCase()).filter(Boolean));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function normalizeDiagnosticSnapshot(input) {
|
|
19
|
+
// Idempotent: already-normalized snapshots pass through unchanged so
|
|
20
|
+
// repeated normalization never fabricates empty semantic sets.
|
|
21
|
+
if (input
|
|
22
|
+
&& input.observationStatements instanceof Set
|
|
23
|
+
&& input.contributorStatements instanceof Set
|
|
24
|
+
&& input.hypothesisStatements instanceof Set
|
|
25
|
+
&& input.evidenceRefs instanceof Set) {
|
|
26
|
+
return input;
|
|
27
|
+
}
|
|
28
|
+
if (!input) {
|
|
29
|
+
return {
|
|
30
|
+
observationStatements: new Set(),
|
|
31
|
+
contributorStatements: new Set(),
|
|
32
|
+
hypothesisStatements: new Set(),
|
|
33
|
+
evidenceRefs: new Set(),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
const legacyHypothesis = input.legacy
|
|
37
|
+
? [input.hypothesis ?? ""]
|
|
38
|
+
: (input.hypotheses ?? []).map((hypothesis) => hypothesis.statement ?? "");
|
|
39
|
+
const evidenceRefs = input.legacy
|
|
40
|
+
? (input.evidenceRefs ?? [])
|
|
41
|
+
: [
|
|
42
|
+
...(input.hypotheses ?? []).flatMap((hypothesis) => hypothesis.evidenceRefs ?? []),
|
|
43
|
+
...(input.observations ?? []).map((observation) => observation.evidenceRef).filter(Boolean),
|
|
44
|
+
];
|
|
45
|
+
return {
|
|
46
|
+
observationStatements: statementSet(input.legacy ? [] : (input.observations ?? []).map((observation) => observation.statement)),
|
|
47
|
+
contributorStatements: statementSet(input.legacy ? [] : (input.contributors ?? []).map((contributor) => contributor.statement)),
|
|
48
|
+
hypothesisStatements: statementSet(legacyHypothesis),
|
|
49
|
+
evidenceRefs: statementSet(evidenceRefs),
|
|
50
|
+
hypothesisIds: new Set(input.legacy ? [] : (input.hypotheses ?? []).map((hypothesis) => hypothesis.id)),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function hasNewValue(previousSet, currentSet) {
|
|
55
|
+
for (const value of currentSet) {
|
|
56
|
+
if (!previousSet.has(value)) return true;
|
|
57
|
+
}
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function compareDiagnosticCycles(previousInput, currentInput, context = {}) {
|
|
62
|
+
const previous = previousInput instanceof Set || Array.isArray(previousInput)
|
|
63
|
+
? normalizeDiagnosticSnapshot(null)
|
|
64
|
+
: normalizeDiagnosticSnapshot(previousInput);
|
|
65
|
+
const current = normalizeDiagnosticSnapshot(currentInput);
|
|
66
|
+
|
|
67
|
+
const dimensions = {
|
|
68
|
+
newObservation: hasNewValue(previous.observationStatements, current.observationStatements),
|
|
69
|
+
newContributor: hasNewValue(previous.contributorStatements, current.contributorStatements),
|
|
70
|
+
newHypothesis: hasNewValue(previous.hypothesisStatements, current.hypothesisStatements),
|
|
71
|
+
hypothesisDispositionChanged: Boolean(context.hypothesisDispositionChanged),
|
|
72
|
+
failureSignatureChanged: Boolean(context.failureSignatureChanged),
|
|
73
|
+
failureSurfaceChanged: Boolean(context.failureSurfaceChanged),
|
|
74
|
+
interventionChanged: Boolean(context.interventionChanged),
|
|
75
|
+
strategyChanged: Boolean(context.strategyChanged),
|
|
76
|
+
newEvidence: hasNewValue(previous.evidenceRefs, current.evidenceRefs),
|
|
77
|
+
// Elimination is decided semantically by the cycle analysis projection:
|
|
78
|
+
// an id disappearing while its statement survives is ID-only churn, not gain.
|
|
79
|
+
hypothesisEliminated: Boolean(context.hypothesisEliminated),
|
|
80
|
+
};
|
|
81
|
+
return dimensions;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const CLASSIFICATION = Object.freeze({
|
|
85
|
+
FIRST_DIAGNOSIS: "FIRST_DIAGNOSIS",
|
|
86
|
+
NEW_HYPOTHESIS: "NEW_HYPOTHESIS",
|
|
87
|
+
NEW_EVIDENCE: "NEW_EVIDENCE",
|
|
88
|
+
NEW_HYPOTHESIS_AND_EVIDENCE: "NEW_HYPOTHESIS_AND_EVIDENCE",
|
|
89
|
+
NONE: "NONE",
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
export function classifyGain(dimensions, { first = false } = {}) {
|
|
93
|
+
if (first) return CLASSIFICATION.FIRST_DIAGNOSIS;
|
|
94
|
+
const hypothesis = dimensions.newHypothesis;
|
|
95
|
+
const evidence = dimensions.newEvidence;
|
|
96
|
+
if (hypothesis && evidence) return CLASSIFICATION.NEW_HYPOTHESIS_AND_EVIDENCE;
|
|
97
|
+
if (hypothesis) return CLASSIFICATION.NEW_HYPOTHESIS;
|
|
98
|
+
if (evidence) return CLASSIFICATION.NEW_EVIDENCE;
|
|
99
|
+
return CLASSIFICATION.NONE;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function isEffectiveGain(dimensions, classification) {
|
|
103
|
+
if (classification !== CLASSIFICATION.NONE) return true;
|
|
104
|
+
|
|
105
|
+
// Every meaningful semantic dimension counts as effective gain.
|
|
106
|
+
// newHypothesis and newEvidence remain covered by the compatibility
|
|
107
|
+
// classification above; the remaining dimensions are checked explicitly.
|
|
108
|
+
return Boolean(
|
|
109
|
+
dimensions.newObservation
|
|
110
|
+
|| dimensions.newContributor
|
|
111
|
+
|| dimensions.hypothesisDispositionChanged
|
|
112
|
+
|| dimensions.failureSignatureChanged
|
|
113
|
+
|| dimensions.failureSurfaceChanged
|
|
114
|
+
|| dimensions.interventionChanged
|
|
115
|
+
|| dimensions.strategyChanged
|
|
116
|
+
|| dimensions.hypothesisEliminated
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function computeInformationGain(entries) {
|
|
121
|
+
const results = [];
|
|
122
|
+
let previous = null;
|
|
123
|
+
let seenFirst = false;
|
|
124
|
+
for (const entry of entries) {
|
|
125
|
+
const dimensions = compareDiagnosticCycles(previous?.snapshot ?? null, entry.snapshot, entry.context ?? {});
|
|
126
|
+
const classification = classifyGain(dimensions, { first: !seenFirst });
|
|
127
|
+
seenFirst = true;
|
|
128
|
+
results.push({
|
|
129
|
+
verificationCycle: entry.verificationCycle,
|
|
130
|
+
sequence: entry.sequence ?? null,
|
|
131
|
+
effectiveGain: isEffectiveGain(dimensions, classification),
|
|
132
|
+
classification,
|
|
133
|
+
dimensions,
|
|
134
|
+
});
|
|
135
|
+
previous = entry;
|
|
136
|
+
}
|
|
137
|
+
return results;
|
|
138
|
+
}
|
package/src/core/inspect.js
CHANGED
|
@@ -11,6 +11,100 @@ import { FORGELOOP_KIT_DIR } from "./target-layout.js";
|
|
|
11
11
|
import { trustedAuthorityConfiguration } from "./trusted-authority.js";
|
|
12
12
|
import { reconcileContinuity } from "./continuity-reconciliation.js";
|
|
13
13
|
import { continuityFinding, continuityIsHealthy } from "./continuity-observability.js";
|
|
14
|
+
import { findTaskById } from "./task-discovery.js";
|
|
15
|
+
import { buildTaskTrace } from "./trace.js";
|
|
16
|
+
import { evaluateProgress } from "./progress.js";
|
|
17
|
+
import { readEvents } from "./events.js";
|
|
18
|
+
|
|
19
|
+
function reasonCodesFor({ state, trace, progress }) {
|
|
20
|
+
const codes = [];
|
|
21
|
+
codes.push(trace.integrity.valid ? "LEDGER_VALID" : "LEDGER_INCONSISTENT");
|
|
22
|
+
if (state?.status === "VALID" || state?.status === "FRESH") codes.push("REPOSITORY_FRESH");
|
|
23
|
+
if (["STALE", "REVALIDATION_REQUIRED"].includes(state?.status)) codes.push("REPOSITORY_STALE");
|
|
24
|
+
if (trace.task.phase === "COMPLETE") codes.push("COMPLETION_VALID");
|
|
25
|
+
else codes.push("COMPLETION_INCOMPLETE");
|
|
26
|
+
if (progress.status === "ADVANCING") codes.push("DIAGNOSTIC_PROGRESS_ADVANCING");
|
|
27
|
+
if (progress.status === "STALLED") codes.push("DIAGNOSTIC_PROGRESS_STALLED");
|
|
28
|
+
return codes;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function buildTaskInspection({ target, packageRoot, taskId, state, classifiedStatus = null }) {
|
|
32
|
+
const trace = await buildTaskTrace({ target, packageRoot, taskId });
|
|
33
|
+
const events = await readEvents(target, packageRoot, { taskId });
|
|
34
|
+
const progress = evaluateProgress({ state, events });
|
|
35
|
+
const failedRequirements = [...new Set(
|
|
36
|
+
trace.checks
|
|
37
|
+
.filter((check) => check.currentResult === "failed" || check.currentResult === "blocked")
|
|
38
|
+
.map((check) => check.requirement ?? check.id),
|
|
39
|
+
)].sort();
|
|
40
|
+
|
|
41
|
+
const issues = [];
|
|
42
|
+
if (!trace.snapshot.consistent) {
|
|
43
|
+
issues.push({ code: "E_TRACE_SNAPSHOT_INCONSISTENT", message: "Task artifacts changed while being read; rerun inspect for a consistent view." });
|
|
44
|
+
}
|
|
45
|
+
for (const error of trace.integrity.errors) {
|
|
46
|
+
issues.push({ code: error.code ?? "E_EVENT_INVALID", message: error.message });
|
|
47
|
+
}
|
|
48
|
+
if (failedRequirements.length > 0 && trace.diagnostics.cases.length === 0 && trace.diagnostics.legacyDiagnoses.length === 0) {
|
|
49
|
+
issues.push({ code: "E_DIAGNOSIS_REQUIRED", message: "Failed requirements have no recorded diagnosis." });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const explanation = {
|
|
53
|
+
result: failedRequirements.length > 0 ? "INCOMPLETE_VERIFICATION" : (trace.task.phase === "COMPLETE" ? "COMPLETE" : "INCOMPLETE"),
|
|
54
|
+
reasons: reasonCodesFor({ state: { status: classifiedStatus }, trace, progress }),
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
task: taskId,
|
|
59
|
+
snapshot: {
|
|
60
|
+
consistent: trace.snapshot.consistent,
|
|
61
|
+
stateRevision: trace.snapshot.stateRevision,
|
|
62
|
+
ledgerTailSequence: trace.snapshot.ledgerTailSequence,
|
|
63
|
+
},
|
|
64
|
+
lifecycle: {
|
|
65
|
+
phase: trace.task.phase,
|
|
66
|
+
verificationCycle: trace.task.verificationCycle,
|
|
67
|
+
transitions: trace.transitions,
|
|
68
|
+
},
|
|
69
|
+
history: {
|
|
70
|
+
eventCount: trace.events.length,
|
|
71
|
+
quality: trace.historyQuality,
|
|
72
|
+
},
|
|
73
|
+
verification: {
|
|
74
|
+
checks: trace.checks.map((check) => ({
|
|
75
|
+
id: check.id,
|
|
76
|
+
requirement: check.requirement,
|
|
77
|
+
currentResult: check.currentResult,
|
|
78
|
+
attemptCount: check.attemptCount,
|
|
79
|
+
failedAttempts: check.failedAttempts,
|
|
80
|
+
})),
|
|
81
|
+
failedRequirements,
|
|
82
|
+
},
|
|
83
|
+
diagnostics: {
|
|
84
|
+
legacyDiagnosisCount: trace.diagnostics.legacyDiagnoses.length,
|
|
85
|
+
diagnosticCaseCount: trace.diagnostics.cases.length,
|
|
86
|
+
interventionCount: trace.diagnostics.interventions.length,
|
|
87
|
+
dispositionCount: trace.diagnostics.dispositions.length,
|
|
88
|
+
latestCase: trace.diagnostics.cases.at(-1) ?? null,
|
|
89
|
+
},
|
|
90
|
+
progress,
|
|
91
|
+
failureSurfaces: trace.failureSurfaces,
|
|
92
|
+
failureSignatures: trace.failureSignatures.map((entry) => ({
|
|
93
|
+
signature: entry.signature,
|
|
94
|
+
requirements: entry.requirements,
|
|
95
|
+
cycles: entry.cycles,
|
|
96
|
+
})),
|
|
97
|
+
integrity: {
|
|
98
|
+
valid: trace.integrity.valid,
|
|
99
|
+
errors: trace.integrity.errors,
|
|
100
|
+
},
|
|
101
|
+
audit: {},
|
|
102
|
+
completion: {},
|
|
103
|
+
issues,
|
|
104
|
+
explanation,
|
|
105
|
+
next: { command: `forgeloop next --task ${taskId} --json` },
|
|
106
|
+
};
|
|
107
|
+
}
|
|
14
108
|
|
|
15
109
|
function profileMetadata(bytes) {
|
|
16
110
|
const text = bytes.toString("utf8");
|
|
@@ -39,7 +133,9 @@ export async function inspectTarget({ target, packageRoot, contractFile = null,
|
|
|
39
133
|
const effectiveStateRel = stateFile ?? (taskId ? taskArtifactPath(taskId, "state") : WORK_STATE_PATH);
|
|
40
134
|
const statePath = ensureWithin(target, effectiveStateRel);
|
|
41
135
|
const statePresent = await fileExists(statePath);
|
|
42
|
-
const
|
|
136
|
+
const classifiedState = await readAndClassifyWorkState({ target, packageRoot, contractFile, taskId, stateFile: effectiveStateRel });
|
|
137
|
+
const rawState = classifiedState?.state ?? null;
|
|
138
|
+
const taskInfo = taskId ? await findTaskById(target, taskId, packageRoot) : null;
|
|
43
139
|
const continuity = await reconcileContinuity({ target, packageRoot, taskId });
|
|
44
140
|
const schemaRoot = manifest?.layoutVersion >= 2
|
|
45
141
|
? ensureWithin(target, FORGELOOP_KIT_DIR)
|
|
@@ -78,12 +174,27 @@ export async function inspectTarget({ target, packageRoot, contractFile = null,
|
|
|
78
174
|
const continuityIssue = continuityFinding(continuity);
|
|
79
175
|
if (continuityIssue) findings.push(continuityIssue);
|
|
80
176
|
|
|
81
|
-
if (
|
|
177
|
+
if (taskInfo?.ownershipValid === false) {
|
|
178
|
+
findings.push({
|
|
179
|
+
code: "task-claim-ownership-inconsistent",
|
|
180
|
+
severity: "error",
|
|
181
|
+
path: taskArtifactPath(taskId, "recovery"),
|
|
182
|
+
message: "Task claim ownership cannot be validated from recovery state and ledger history.",
|
|
183
|
+
remediation: `Run forgeloop validate-protocol --task ${taskId} --json and repair the reported protocol-owned artifact.`,
|
|
184
|
+
evidence: createEvidence({
|
|
185
|
+
kind: "BLOCKED",
|
|
186
|
+
source: taskArtifactPath(taskId, "events"),
|
|
187
|
+
result: "E_TASK_CLAIM_OWNERSHIP_INCONSISTENT",
|
|
188
|
+
}),
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (classifiedState.status === "INVALID") {
|
|
82
193
|
findings.push({
|
|
83
194
|
code: "state-invalid",
|
|
84
195
|
severity: "error",
|
|
85
|
-
path:
|
|
86
|
-
message:
|
|
196
|
+
path: effectiveStateRel,
|
|
197
|
+
message: classifiedState.error ?? "Work state is invalid.",
|
|
87
198
|
remediation: "Repair or clear the checkpoint after reviewing the parse error.",
|
|
88
199
|
evidence: createEvidence({ kind: "BLOCKED", source: WORK_STATE_PATH, result: "invalid" }),
|
|
89
200
|
});
|
|
@@ -96,9 +207,12 @@ export async function inspectTarget({ target, packageRoot, contractFile = null,
|
|
|
96
207
|
})];
|
|
97
208
|
const evidence = [
|
|
98
209
|
...(doctor.evidence ?? []),
|
|
99
|
-
...(
|
|
210
|
+
...(classifiedState.evidence ?? []),
|
|
100
211
|
...protocolEvidence,
|
|
101
212
|
];
|
|
213
|
+
const taskInspection = taskId
|
|
214
|
+
? await buildTaskInspection({ target, packageRoot, taskId, state: rawState, classifiedStatus: classifiedState.status })
|
|
215
|
+
: null;
|
|
102
216
|
return {
|
|
103
217
|
target: { path: target },
|
|
104
218
|
authority: trustedAuthorityConfiguration({ target, authorityContext, runtimeContext }),
|
|
@@ -131,7 +245,16 @@ export async function inspectTarget({ target, packageRoot, contractFile = null,
|
|
|
131
245
|
schemas: schemaHealth.schemas,
|
|
132
246
|
evidence: protocolEvidence,
|
|
133
247
|
},
|
|
134
|
-
state: { ...
|
|
248
|
+
state: { ...classifiedState, path: effectiveStateRel, present: statePresent },
|
|
249
|
+
recovery: taskInfo?.recovery ?? null,
|
|
250
|
+
claims: taskInfo ? {
|
|
251
|
+
state: taskInfo.claimState,
|
|
252
|
+
historical: taskInfo.historicalWriteClaims,
|
|
253
|
+
effective: taskInfo.effectiveWriteClaims,
|
|
254
|
+
mutationAllowed: taskInfo.mutationAllowed,
|
|
255
|
+
ownershipValid: taskInfo.ownershipValid,
|
|
256
|
+
ownershipErrors: taskInfo.ownershipErrors ?? taskInfo.errors ?? [],
|
|
257
|
+
} : null,
|
|
135
258
|
continuity,
|
|
136
259
|
compatibility: {
|
|
137
260
|
deprecated: true,
|
|
@@ -139,10 +262,12 @@ export async function inspectTarget({ target, packageRoot, contractFile = null,
|
|
|
139
262
|
},
|
|
140
263
|
findings,
|
|
141
264
|
evidence,
|
|
265
|
+
...(taskInspection ? { taskInspection } : {}),
|
|
142
266
|
ok: doctor.ok
|
|
143
267
|
&& !manifestError
|
|
144
268
|
&& schemaHealth.status === "valid"
|
|
145
|
-
&&
|
|
269
|
+
&& taskInfo?.ownershipValid !== false
|
|
270
|
+
&& !["INVALID", "REVALIDATION_REQUIRED"].includes(classifiedState.status)
|
|
146
271
|
&& continuityIsHealthy(continuity),
|
|
147
272
|
};
|
|
148
273
|
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { CLI_COMMAND_DEFINITIONS } from "./cli-command-definitions.js";
|
|
2
|
+
import { COMMAND_EXECUTORS } from "./command-executors.js";
|
|
3
|
+
import { PROTOCOL_VERSION } from "./protocol.js";
|
|
4
|
+
import { FORGELOOP_INTEGRATION_RUNTIME_VERSION } from "./command-runtime.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Integration risk classes. They classify the *invocation*, not only the
|
|
8
|
+
* command name: input-dependent commands (doctor --fix, task-unlock --force,
|
|
9
|
+
* policy-discover --write, baseline mutations) are refined by the sparse
|
|
10
|
+
* override table below.
|
|
11
|
+
*/
|
|
12
|
+
export const INTEGRATION_RISK_CLASSES = Object.freeze({
|
|
13
|
+
READ_ONLY: "READ_ONLY",
|
|
14
|
+
LOOP_MUTATION: "LOOP_MUTATION",
|
|
15
|
+
CLAIM_REACQUISITION: "CLAIM_REACQUISITION",
|
|
16
|
+
EXTERNAL_EXECUTION: "EXTERNAL_EXECUTION",
|
|
17
|
+
AUTHORITY_MUTATION: "AUTHORITY_MUTATION",
|
|
18
|
+
EXTERNAL_STATE_ATTESTATION: "EXTERNAL_STATE_ATTESTATION",
|
|
19
|
+
MAINTENANCE: "MAINTENANCE",
|
|
20
|
+
CLAIM_RELEASE_RECOVERY: "CLAIM_RELEASE_RECOVERY",
|
|
21
|
+
LEGACY_MIGRATION: "LEGACY_MIGRATION",
|
|
22
|
+
FORCE_DESTRUCTIVE: "FORCE_DESTRUCTIVE",
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const READ_ONLY_COMMANDS = Object.freeze(new Set([
|
|
26
|
+
"protocol-info", "status", "next", "continuity", "reconcile-continuity",
|
|
27
|
+
"task-list", "task-show", "task-lock-status", "progress", "audit", "report",
|
|
28
|
+
"inspect", "validate-state", "validate-protocol", "validate-receipt",
|
|
29
|
+
"policy-status", "policy-diff", "rule-verify", "policy",
|
|
30
|
+
"history", "trace", "reflect",
|
|
31
|
+
]));
|
|
32
|
+
|
|
33
|
+
const LOOP_MUTATION_COMMANDS = Object.freeze(new Set([
|
|
34
|
+
"route", "preflight", "advance", "task-create", "task-scope",
|
|
35
|
+
"record-continuity", "clear-continuity", "prepare-completion",
|
|
36
|
+
"record-check", "record-diagnosis", "record-decision-criterion",
|
|
37
|
+
"record-terminal-result", "complete",
|
|
38
|
+
"record-intervention", "record-hypothesis-disposition",
|
|
39
|
+
]));
|
|
40
|
+
|
|
41
|
+
const STATIC_RISK_CLASSES = Object.freeze({
|
|
42
|
+
...Object.fromEntries([...READ_ONLY_COMMANDS].map((name) => [name, INTEGRATION_RISK_CLASSES.READ_ONLY])),
|
|
43
|
+
...Object.fromEntries([...LOOP_MUTATION_COMMANDS].map((name) => [name, INTEGRATION_RISK_CLASSES.LOOP_MUTATION])),
|
|
44
|
+
"task-resume": INTEGRATION_RISK_CLASSES.CLAIM_REACQUISITION,
|
|
45
|
+
"run-check": INTEGRATION_RISK_CLASSES.EXTERNAL_EXECUTION,
|
|
46
|
+
"run-action": INTEGRATION_RISK_CLASSES.EXTERNAL_EXECUTION,
|
|
47
|
+
"reconcile-closure": INTEGRATION_RISK_CLASSES.EXTERNAL_EXECUTION,
|
|
48
|
+
"action-propose": INTEGRATION_RISK_CLASSES.LOOP_MUTATION,
|
|
49
|
+
"action-authorize": INTEGRATION_RISK_CLASSES.LOOP_MUTATION,
|
|
50
|
+
"action-record": INTEGRATION_RISK_CLASSES.LOOP_MUTATION,
|
|
51
|
+
"action-verify": INTEGRATION_RISK_CLASSES.LOOP_MUTATION,
|
|
52
|
+
"action-reconcile": INTEGRATION_RISK_CLASSES.LOOP_MUTATION,
|
|
53
|
+
"approval-request": INTEGRATION_RISK_CLASSES.LOOP_MUTATION,
|
|
54
|
+
"approval-resolve": INTEGRATION_RISK_CLASSES.AUTHORITY_MUTATION,
|
|
55
|
+
eval: INTEGRATION_RISK_CLASSES.LOOP_MUTATION,
|
|
56
|
+
"action-show": INTEGRATION_RISK_CLASSES.READ_ONLY,
|
|
57
|
+
metrics: INTEGRATION_RISK_CLASSES.READ_ONLY,
|
|
58
|
+
init: INTEGRATION_RISK_CLASSES.MAINTENANCE,
|
|
59
|
+
update: INTEGRATION_RISK_CLASSES.MAINTENANCE,
|
|
60
|
+
activate: INTEGRATION_RISK_CLASSES.MAINTENANCE,
|
|
61
|
+
"task-migrate": INTEGRATION_RISK_CLASSES.MAINTENANCE,
|
|
62
|
+
"migrate-protocol": INTEGRATION_RISK_CLASSES.MAINTENANCE,
|
|
63
|
+
"clear-state": INTEGRATION_RISK_CLASSES.MAINTENANCE,
|
|
64
|
+
doctor: INTEGRATION_RISK_CLASSES.MAINTENANCE,
|
|
65
|
+
"policy-discover": INTEGRATION_RISK_CLASSES.MAINTENANCE,
|
|
66
|
+
baseline: INTEGRATION_RISK_CLASSES.MAINTENANCE,
|
|
67
|
+
"task-unlock": INTEGRATION_RISK_CLASSES.MAINTENANCE,
|
|
68
|
+
// bundle writes a bundle artifact set under the task namespace; it is not
|
|
69
|
+
// read-only despite producing no protocol-state mutations.
|
|
70
|
+
bundle: INTEGRATION_RISK_CLASSES.MAINTENANCE,
|
|
71
|
+
"profile-interview": INTEGRATION_RISK_CLASSES.MAINTENANCE,
|
|
72
|
+
"task-recover": INTEGRATION_RISK_CLASSES.CLAIM_RELEASE_RECOVERY,
|
|
73
|
+
"task-repair-legacy-recovery": INTEGRATION_RISK_CLASSES.LEGACY_MIGRATION,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// Sparse input-dependent refinements over the static table.
|
|
77
|
+
function refineRiskClass(command, input) {
|
|
78
|
+
if (command === "task-unlock" && input?.force === true) {
|
|
79
|
+
return INTEGRATION_RISK_CLASSES.FORCE_DESTRUCTIVE;
|
|
80
|
+
}
|
|
81
|
+
// Settling external commit state is independently gated: recording an
|
|
82
|
+
// UNKNOWN observation stays LOOP_MUTATION, while COMMITTED / NOT_COMMITTED
|
|
83
|
+
// settlements attest external state and require their own capability.
|
|
84
|
+
if (
|
|
85
|
+
command === "action-reconcile"
|
|
86
|
+
&& ["COMMITTED", "NOT_COMMITTED"].includes(input?.reconciliationOutcome)
|
|
87
|
+
) {
|
|
88
|
+
return INTEGRATION_RISK_CLASSES.EXTERNAL_STATE_ATTESTATION;
|
|
89
|
+
}
|
|
90
|
+
// Fail closed: every canonical command must be explicitly classified.
|
|
91
|
+
return baseRiskClass(command);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function baseRiskClass(command) {
|
|
95
|
+
const riskClass = STATIC_RISK_CLASSES[command];
|
|
96
|
+
if (!riskClass) {
|
|
97
|
+
throw new Error(`Command ${command} has no integration risk classification`);
|
|
98
|
+
}
|
|
99
|
+
return riskClass;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function getForgeLoopCapabilities({ packageVersion = null } = {}) {
|
|
103
|
+
const commands = Object.keys(CLI_COMMAND_DEFINITIONS).sort().map((name) => {
|
|
104
|
+
const def = CLI_COMMAND_DEFINITIONS[name];
|
|
105
|
+
return {
|
|
106
|
+
name,
|
|
107
|
+
category: def.category,
|
|
108
|
+
mutation: def.mutation,
|
|
109
|
+
baseRiskClass: baseRiskClass(name),
|
|
110
|
+
mayExecuteExternalProcess: def.mayExecuteExternalProcess === true,
|
|
111
|
+
description: def.description,
|
|
112
|
+
};
|
|
113
|
+
});
|
|
114
|
+
return {
|
|
115
|
+
packageVersion,
|
|
116
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
117
|
+
integrationApiVersion: FORGELOOP_INTEGRATION_RUNTIME_VERSION,
|
|
118
|
+
executorParity: Object.keys(COMMAND_EXECUTORS).length === Object.keys(CLI_COMMAND_DEFINITIONS).length,
|
|
119
|
+
features: {
|
|
120
|
+
taskClaimRecovery: {
|
|
121
|
+
version: 1,
|
|
122
|
+
durableRecoveryState: true,
|
|
123
|
+
explicitResume: true,
|
|
124
|
+
validatedClaimProjection: true,
|
|
125
|
+
},
|
|
126
|
+
durableActions: {
|
|
127
|
+
version: 1,
|
|
128
|
+
readOnlyResources: true,
|
|
129
|
+
externalExecutionOverMcp: false,
|
|
130
|
+
},
|
|
131
|
+
trajectoryEvaluation: {
|
|
132
|
+
version: 1,
|
|
133
|
+
readOnlyMetrics: true,
|
|
134
|
+
projectLocalReference: true,
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
commands,
|
|
138
|
+
resources: [
|
|
139
|
+
{ name: "protocol/info", scope: "PROJECT" },
|
|
140
|
+
{ name: "project/tasks", scope: "PROJECT" },
|
|
141
|
+
{ name: "task/status", scope: "TASK" },
|
|
142
|
+
{ name: "task/ownership", scope: "TASK" },
|
|
143
|
+
{ name: "task/contract", scope: "TASK" },
|
|
144
|
+
{ name: "task/continuity", scope: "TASK" },
|
|
145
|
+
{ name: "task/actions", scope: "TASK" },
|
|
146
|
+
{ name: "task/action", scope: "TASK" },
|
|
147
|
+
{ name: "task/approvals", scope: "TASK" },
|
|
148
|
+
{ name: "task/metrics", scope: "TASK" },
|
|
149
|
+
{ name: "task/evaluations", scope: "TASK" },
|
|
150
|
+
{ name: "project/capability-policy", scope: "PROJECT" },
|
|
151
|
+
],
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Classify a concrete command invocation (command + structured input).
|
|
157
|
+
* Tool-provided input can never elevate a launch-level capability; this
|
|
158
|
+
* classifier only describes what the invocation would do.
|
|
159
|
+
*/
|
|
160
|
+
export function classifyForgeLoopInvocation(command, input = {}) {
|
|
161
|
+
const definition = CLI_COMMAND_DEFINITIONS[command];
|
|
162
|
+
if (!definition) {
|
|
163
|
+
throw new Error(`Unknown ForgeLoop command: ${command}`);
|
|
164
|
+
}
|
|
165
|
+
const riskClass = refineRiskClass(command, input);
|
|
166
|
+
const readOnly = riskClass === INTEGRATION_RISK_CLASSES.READ_ONLY;
|
|
167
|
+
const requiredCapability = (() => {
|
|
168
|
+
switch (riskClass) {
|
|
169
|
+
case INTEGRATION_RISK_CLASSES.EXTERNAL_EXECUTION:
|
|
170
|
+
return "allowExternalExecution";
|
|
171
|
+
case INTEGRATION_RISK_CLASSES.AUTHORITY_MUTATION:
|
|
172
|
+
return "allowApprovalResolution";
|
|
173
|
+
case INTEGRATION_RISK_CLASSES.EXTERNAL_STATE_ATTESTATION:
|
|
174
|
+
return "allowActionReconciliationSettlement";
|
|
175
|
+
case INTEGRATION_RISK_CLASSES.MAINTENANCE:
|
|
176
|
+
return "allowMaintenance";
|
|
177
|
+
case INTEGRATION_RISK_CLASSES.CLAIM_RELEASE_RECOVERY:
|
|
178
|
+
return "allowRecovery";
|
|
179
|
+
case INTEGRATION_RISK_CLASSES.LEGACY_MIGRATION:
|
|
180
|
+
return "allowLegacyRepair";
|
|
181
|
+
case INTEGRATION_RISK_CLASSES.FORCE_DESTRUCTIVE:
|
|
182
|
+
return "allowForceRecovery";
|
|
183
|
+
default:
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
})();
|
|
187
|
+
return Object.freeze({
|
|
188
|
+
command,
|
|
189
|
+
riskClass,
|
|
190
|
+
readOnly,
|
|
191
|
+
mutatesProtocol: !readOnly && [
|
|
192
|
+
INTEGRATION_RISK_CLASSES.LOOP_MUTATION,
|
|
193
|
+
INTEGRATION_RISK_CLASSES.CLAIM_REACQUISITION,
|
|
194
|
+
INTEGRATION_RISK_CLASSES.EXTERNAL_EXECUTION,
|
|
195
|
+
INTEGRATION_RISK_CLASSES.AUTHORITY_MUTATION,
|
|
196
|
+
INTEGRATION_RISK_CLASSES.EXTERNAL_STATE_ATTESTATION,
|
|
197
|
+
INTEGRATION_RISK_CLASSES.MAINTENANCE,
|
|
198
|
+
INTEGRATION_RISK_CLASSES.CLAIM_RELEASE_RECOVERY,
|
|
199
|
+
INTEGRATION_RISK_CLASSES.LEGACY_MIGRATION,
|
|
200
|
+
INTEGRATION_RISK_CLASSES.FORCE_DESTRUCTIVE,
|
|
201
|
+
].includes(riskClass),
|
|
202
|
+
removesArtifacts: definition.removes.length > 0,
|
|
203
|
+
executesExternalProcess: definition.mayExecuteExternalProcess === true,
|
|
204
|
+
affectsClaimAuthority: [
|
|
205
|
+
"task-resume", "task-recover", "task-repair-legacy-recovery",
|
|
206
|
+
"task-create", "task-scope", "complete",
|
|
207
|
+
].includes(command),
|
|
208
|
+
destructive: [
|
|
209
|
+
INTEGRATION_RISK_CLASSES.AUTHORITY_MUTATION,
|
|
210
|
+
INTEGRATION_RISK_CLASSES.FORCE_DESTRUCTIVE,
|
|
211
|
+
INTEGRATION_RISK_CLASSES.CLAIM_RELEASE_RECOVERY,
|
|
212
|
+
INTEGRATION_RISK_CLASSES.LEGACY_MIGRATION,
|
|
213
|
+
INTEGRATION_RISK_CLASSES.MAINTENANCE,
|
|
214
|
+
].includes(riskClass),
|
|
215
|
+
requiredCapability,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bounded input limits for structured integrations. These bound the
|
|
3
|
+
* transport-facing surface; canonical ForgeLoop JSON safety limits remain
|
|
4
|
+
* authoritative for persisted artifacts. Values are conservative and
|
|
5
|
+
* intentionally small for an agent-facing API.
|
|
6
|
+
*/
|
|
7
|
+
export const INTEGRATION_LIMITS = Object.freeze({
|
|
8
|
+
/** Maximum length of any single string input (task IDs, names, text). */
|
|
9
|
+
maxStringLength: 4096,
|
|
10
|
+
/** Maximum number of entries in a repeatable string option. */
|
|
11
|
+
maxRepeatedValues: 32,
|
|
12
|
+
/** Maximum number of exact argv items passed to external execution. */
|
|
13
|
+
maxArgvItems: 64,
|
|
14
|
+
/** Maximum length of a single argv item. */
|
|
15
|
+
maxArgvItemLength: 2048,
|
|
16
|
+
/** Maximum serialized size of a JSON-object input field. */
|
|
17
|
+
maxStructuredInputBytes: 256 * 1024,
|
|
18
|
+
/** Maximum serialized size of any tool/resource response payload. */
|
|
19
|
+
maxOutputBytes: 4 * 1024 * 1024,
|
|
20
|
+
});
|