@cassiomc1/forgeloop 1.8.1 → 1.10.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/.cursor/rules/project-loop.mdc +6 -3
- package/.github/copilot-instructions.md +5 -0
- package/AGENTS.md +6 -0
- package/AGENT_COMPATIBILITY.md +15 -0
- package/CLAUDE.md +6 -0
- package/DELEGATION_PROTOCOL.md +6 -0
- package/DOCS_INDEX.md +9 -2
- package/ENG/accessibility-eng.md +12 -2
- package/ENG/design-code-eng.md +22 -1
- package/LOOP_ENGINEERING.md +41 -0
- package/LOOP_SYSTEM_DESIGN.md +33 -0
- package/ORCHESTRATOR_INTEGRATION.md +9 -0
- package/PROTOCOL_INTEGRATION.md +57 -0
- package/QUALITY_SCORECARD.md +2 -0
- package/README.md +51 -0
- package/TERMINOLOGY.md +12 -0
- package/THREAT_MODEL.md +48 -0
- package/completions/_forgeloop +5 -1
- package/completions/forgeloop.bash +9 -1
- package/completions/forgeloop.fish +27 -1
- package/docs/ADVISORY_CONTEXT.md +174 -0
- package/docs/AGENT_PROTOCOL_SUMMARY.md +33 -2
- package/docs/ARTIFACT_REFERENCE.md +142 -0
- package/docs/CLI_REFERENCE.md +124 -1
- package/docs/CROSS_HARNESS_CONTINUITY.md +85 -0
- package/docs/DOCUMENTATION_GUIDE.md +7 -0
- package/docs/GETTING_STARTED.md +22 -0
- package/docs/KNOWLEDGE_SOURCES.md +171 -0
- package/docs/MCP.md +17 -1
- package/docs/RECIPES.md +111 -0
- package/docs/RELEASE_CHECKLIST.md +14 -0
- package/docs/STRUCTURAL_QUALITY.md +350 -0
- package/docs/TROUBLESHOOTING.md +161 -2
- package/docs/UNIVERSAL_INTEGRATION.md +60 -0
- package/package.json +4 -1
- package/schemas/config.schema.json +46 -0
- package/schemas/handoff-envelope.schema.json +1 -0
- package/schemas/preflight.schema.json +2 -1
- package/schemas/structural-quality.schema.json +175 -0
- package/scripts/check-changelog-freshness.mjs +27 -3
- package/scripts/generate-agent-protocol-summary.mjs +18 -0
- package/src/cli.js +24 -0
- package/src/commands/handoff-accept.js +36 -0
- package/src/commands/handoff-list.js +28 -2
- package/src/commands/handoff-show.js +27 -2
- package/src/commands/quality-baseline.js +28 -0
- package/src/commands/quality-status.js +34 -0
- package/src/commands/quality-verify.js +30 -0
- package/src/commands/reconcile-continuity.js +4 -0
- package/src/core/advisory-context/constants.js +74 -0
- package/src/core/advisory-context/provider.js +287 -0
- package/src/core/advisory-context/service.js +140 -0
- package/src/core/artifact-registry.js +12 -0
- package/src/core/audit.js +38 -0
- package/src/core/bundles.js +134 -1
- package/src/core/cli-command-definitions.js +62 -0
- package/src/core/command-executors.js +28 -0
- package/src/core/command-input.js +23 -1
- package/src/core/completion-artifacts.js +2 -0
- package/src/core/completion.js +42 -0
- package/src/core/config.js +3 -0
- package/src/core/continuity-lint.js +89 -0
- package/src/core/continuity-reconciliation.js +16 -0
- package/src/core/continuity.js +10 -11
- package/src/core/error-codes.js +186 -0
- package/src/core/events.js +32 -0
- package/src/core/execution-profile-context.js +15 -1
- package/src/core/filesystem.js +34 -3
- package/src/core/handoff-acceptance.js +277 -0
- package/src/core/handoff.js +41 -8
- package/src/core/inspect.js +64 -0
- package/src/core/integration-invocation-policy.js +34 -2
- package/src/core/integration-resources.js +38 -1
- package/src/core/next-action-model.js +11 -1
- package/src/core/next-action-phases.js +84 -5
- package/src/core/phase.js +9 -1
- package/src/core/portable-context.js +103 -0
- package/src/core/preflight.js +33 -0
- package/src/core/protocol-info.js +33 -2
- package/src/core/runtime-context.js +58 -0
- package/src/core/schema-validation.js +1 -0
- package/src/core/structural-quality/artifacts.js +329 -0
- package/src/core/structural-quality/constants.js +67 -0
- package/src/core/structural-quality/policy.js +227 -0
- package/src/core/structural-quality/provider.js +287 -0
- package/src/core/structural-quality/sentrux-mcp.js +477 -0
- package/src/core/structural-quality/service.js +1138 -0
- package/src/core/structural-quality/source-fingerprint.js +112 -0
- package/src/core/structural-quality/status.js +3 -0
- package/src/core/task-paths.js +24 -0
- package/src/core/templates.js +1 -0
- package/src/integration.d.ts +141 -0
- package/src/integration.js +36 -0
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import { canonicalFingerprint } from "./artifacts.js";
|
|
2
|
+
import { readContract } from "./contract.js";
|
|
3
|
+
import { readPersistedRoute } from "./route-artifact.js";
|
|
4
|
+
import { readWorkState } from "./work-state.js";
|
|
5
|
+
import { assertStateIdentity } from "./completion-relationships.js";
|
|
6
|
+
import { compareRepositoryFingerprint, readCanonicalHandoff } from "./handoff.js";
|
|
7
|
+
import { appendProtocolEvent, validateEventLedger } from "./events.js";
|
|
8
|
+
import { currentChangedPaths, currentRepositoryFingerprint } from "./repository.js";
|
|
9
|
+
import { withTaskTransaction } from "./transaction.js";
|
|
10
|
+
import { getPackageRoot } from "./templates.js";
|
|
11
|
+
import {
|
|
12
|
+
normalizePortableText,
|
|
13
|
+
assertPortableContextSafe,
|
|
14
|
+
} from "./portable-context.js";
|
|
15
|
+
import {
|
|
16
|
+
E_HANDOFF_ACCEPTANCE_UNBOUND,
|
|
17
|
+
E_HANDOFF_STALE,
|
|
18
|
+
E_HANDOFF_ALREADY_ACCEPTED,
|
|
19
|
+
E_HANDOFF_ACCEPTANCE_INCONSISTENT,
|
|
20
|
+
} from "./error-codes.js";
|
|
21
|
+
|
|
22
|
+
function acceptanceError(code, message) {
|
|
23
|
+
const error = new Error(message);
|
|
24
|
+
error.name = "HandoffAcceptanceError";
|
|
25
|
+
error.code = code;
|
|
26
|
+
return error;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function uniqueSorted(values) {
|
|
30
|
+
return [...new Set(values.filter(Boolean))].sort();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function resolveHandoffAcceptance(input = {}, legacyHandoff) {
|
|
34
|
+
const projection = Array.isArray(input)
|
|
35
|
+
? { events: input, handoff: legacyHandoff }
|
|
36
|
+
: input;
|
|
37
|
+
const {
|
|
38
|
+
events = [],
|
|
39
|
+
handoff,
|
|
40
|
+
ledgerValid = true,
|
|
41
|
+
ledgerErrors = [],
|
|
42
|
+
} = projection ?? {};
|
|
43
|
+
|
|
44
|
+
if (ledgerValid !== true) {
|
|
45
|
+
return {
|
|
46
|
+
status: "INCONSISTENT",
|
|
47
|
+
reasonCodes: uniqueSorted(ledgerErrors.map((error) => typeof error === "string" ? error : error?.code)),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!handoff?.state?.workStateFingerprint) {
|
|
52
|
+
return { status: "UNBOUND" };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const acceptedEvents = (events ?? []).filter(
|
|
56
|
+
(e) => e.event === "HANDOFF_ACCEPTED" && e.details?.handoffId === handoff.handoffId,
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
if (acceptedEvents.length === 0) {
|
|
60
|
+
return { status: "OPEN" };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (acceptedEvents.length > 1) {
|
|
64
|
+
return { status: "INCONSISTENT" };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const accepted = acceptedEvents[0];
|
|
68
|
+
if (accepted.details?.handoffDigest !== handoff.artifactDigest) {
|
|
69
|
+
return { status: "INCONSISTENT" };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
status: "ACCEPTED",
|
|
74
|
+
consumerId: accepted.details?.consumerId,
|
|
75
|
+
...(accepted.details?.harness ? { harness: accepted.details.harness } : {}),
|
|
76
|
+
acceptedAt: accepted.at,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function readHandoffAcceptanceLedger(target, packageRoot = getPackageRoot(), { taskId } = {}) {
|
|
81
|
+
try {
|
|
82
|
+
return await validateEventLedger(target, packageRoot, { taskId });
|
|
83
|
+
} catch (error) {
|
|
84
|
+
return {
|
|
85
|
+
valid: false,
|
|
86
|
+
events: [],
|
|
87
|
+
errors: [{
|
|
88
|
+
code: error.code ?? "E_EVENT_INVALID",
|
|
89
|
+
message: error.message,
|
|
90
|
+
}],
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export async function acceptCanonicalHandoff(target, {
|
|
96
|
+
taskId,
|
|
97
|
+
handoffId,
|
|
98
|
+
consumerId,
|
|
99
|
+
harness,
|
|
100
|
+
packageRoot = getPackageRoot(),
|
|
101
|
+
} = {}) {
|
|
102
|
+
if (!target || typeof target !== "string") {
|
|
103
|
+
throw acceptanceError("E_TARGET_REQUIRED", "target path is required");
|
|
104
|
+
}
|
|
105
|
+
if (!taskId || typeof taskId !== "string") {
|
|
106
|
+
throw acceptanceError("E_TASK_REQUIRED", "taskId is required");
|
|
107
|
+
}
|
|
108
|
+
if (!handoffId || typeof handoffId !== "string") {
|
|
109
|
+
throw acceptanceError("E_HANDOFF_INVALID", "handoffId is required");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const normalizedConsumerId = normalizePortableText(consumerId, {
|
|
113
|
+
label: "consumerId",
|
|
114
|
+
maxLength: 128,
|
|
115
|
+
});
|
|
116
|
+
assertPortableContextSafe(normalizedConsumerId, { label: "consumerId" });
|
|
117
|
+
|
|
118
|
+
const normalizedHarness = harness !== undefined
|
|
119
|
+
? normalizePortableText(harness, {
|
|
120
|
+
label: "harness",
|
|
121
|
+
maxLength: 64,
|
|
122
|
+
optional: true,
|
|
123
|
+
})
|
|
124
|
+
: null;
|
|
125
|
+
if (normalizedHarness) {
|
|
126
|
+
assertPortableContextSafe(normalizedHarness, { label: "harness" });
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return withTaskTransaction(
|
|
130
|
+
{
|
|
131
|
+
target,
|
|
132
|
+
taskId,
|
|
133
|
+
packageRoot,
|
|
134
|
+
operation: "handoff-accept",
|
|
135
|
+
recordCommitEvent: true,
|
|
136
|
+
},
|
|
137
|
+
async () => {
|
|
138
|
+
const { value: handoff } = await readCanonicalHandoff(target, {
|
|
139
|
+
taskId,
|
|
140
|
+
handoffId,
|
|
141
|
+
packageRoot,
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
if (!handoff.state?.workStateFingerprint) {
|
|
145
|
+
throw acceptanceError(
|
|
146
|
+
E_HANDOFF_ACCEPTANCE_UNBOUND,
|
|
147
|
+
"Handoff snapshot is unbound (lacks state.workStateFingerprint); create a fresh handoff",
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const currentState = await readWorkState(target, { packageRoot, taskId });
|
|
152
|
+
if (!currentState) {
|
|
153
|
+
throw acceptanceError(
|
|
154
|
+
E_HANDOFF_STALE,
|
|
155
|
+
"Canonical work state is unavailable for handoff acceptance",
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const currentWorkStateFingerprint = canonicalFingerprint(currentState);
|
|
160
|
+
if (currentWorkStateFingerprint !== handoff.state.workStateFingerprint) {
|
|
161
|
+
throw acceptanceError(
|
|
162
|
+
E_HANDOFF_STALE,
|
|
163
|
+
"Current work state has drifted from handoff workStateFingerprint",
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const contract = await readContract(target, packageRoot, { taskId });
|
|
168
|
+
const route = await readPersistedRoute(target, packageRoot, { taskId });
|
|
169
|
+
try {
|
|
170
|
+
assertStateIdentity({ contract, route, state: currentState });
|
|
171
|
+
} catch (error) {
|
|
172
|
+
throw acceptanceError(
|
|
173
|
+
E_HANDOFF_STALE,
|
|
174
|
+
`Current canonical task artifacts are not coherent with one another: ${error.message}`,
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
if (contract.fingerprint !== handoff.state.contractFingerprint) {
|
|
178
|
+
throw acceptanceError(
|
|
179
|
+
E_HANDOFF_STALE,
|
|
180
|
+
"Current contract fingerprint has drifted from handoff contractFingerprint",
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
if ((route.fingerprint ?? null) !== (handoff.state.routeFingerprint ?? null)) {
|
|
184
|
+
throw acceptanceError(
|
|
185
|
+
E_HANDOFF_STALE,
|
|
186
|
+
"Current route fingerprint has drifted from handoff routeFingerprint",
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const repositoryFingerprint = await currentRepositoryFingerprint(target);
|
|
191
|
+
if (compareRepositoryFingerprint(handoff.state.repositoryFingerprint, repositoryFingerprint) === "MISMATCH") {
|
|
192
|
+
throw acceptanceError(
|
|
193
|
+
E_HANDOFF_STALE,
|
|
194
|
+
"Current repository branch or HEAD has drifted from handoff snapshot",
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const changedPaths = await currentChangedPaths(target);
|
|
199
|
+
const currentList = [...new Set((changedPaths ?? []).map((p) => p.replaceAll("\\", "/")))].sort();
|
|
200
|
+
const handoffList = [...new Set((handoff.state.changedPaths ?? []).map((p) => p.replaceAll("\\", "/")))].sort();
|
|
201
|
+
if (JSON.stringify(currentList) !== JSON.stringify(handoffList)) {
|
|
202
|
+
throw acceptanceError(
|
|
203
|
+
E_HANDOFF_STALE,
|
|
204
|
+
"Current changed paths have drifted from handoff snapshot",
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const ledgerResult = await validateEventLedger(target, packageRoot, { taskId });
|
|
209
|
+
if (!ledgerResult.valid) {
|
|
210
|
+
throw acceptanceError(
|
|
211
|
+
E_HANDOFF_ACCEPTANCE_INCONSISTENT,
|
|
212
|
+
"Task event ledger is invalid",
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const events = ledgerResult.events;
|
|
217
|
+
const createdEvent = events.find(
|
|
218
|
+
(e) => e.event === "HANDOFF_CREATED" && e.details?.handoffId === handoffId,
|
|
219
|
+
);
|
|
220
|
+
if (!createdEvent || createdEvent.details?.digest !== handoff.artifactDigest) {
|
|
221
|
+
throw acceptanceError(
|
|
222
|
+
E_HANDOFF_ACCEPTANCE_INCONSISTENT,
|
|
223
|
+
"No matching HANDOFF_CREATED event with matching digest found in ledger",
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const existingAccepted = events.filter(
|
|
228
|
+
(e) => e.event === "HANDOFF_ACCEPTED" && e.details?.handoffId === handoffId,
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
if (existingAccepted.length > 0) {
|
|
232
|
+
const prev = existingAccepted[0];
|
|
233
|
+
if (prev.details?.consumerId === normalizedConsumerId) {
|
|
234
|
+
return {
|
|
235
|
+
accepted: true,
|
|
236
|
+
idempotent: true,
|
|
237
|
+
handoffId,
|
|
238
|
+
consumerId: normalizedConsumerId,
|
|
239
|
+
harness: prev.details?.harness ?? null,
|
|
240
|
+
acceptedAt: prev.at,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
throw acceptanceError(
|
|
244
|
+
E_HANDOFF_ALREADY_ACCEPTED,
|
|
245
|
+
`Handoff ${handoffId} has already been accepted by consumer "${prev.details?.consumerId}"`,
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const eventDetails = {
|
|
250
|
+
handoffId,
|
|
251
|
+
handoffDigest: handoff.artifactDigest,
|
|
252
|
+
consumerId: normalizedConsumerId,
|
|
253
|
+
...(normalizedHarness ? { harness: normalizedHarness } : {}),
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
const appended = await appendProtocolEvent(
|
|
257
|
+
target,
|
|
258
|
+
{
|
|
259
|
+
taskId,
|
|
260
|
+
event: "HANDOFF_ACCEPTED",
|
|
261
|
+
details: eventDetails,
|
|
262
|
+
},
|
|
263
|
+
packageRoot,
|
|
264
|
+
{ taskId },
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
return {
|
|
268
|
+
accepted: true,
|
|
269
|
+
idempotent: false,
|
|
270
|
+
handoffId,
|
|
271
|
+
consumerId: normalizedConsumerId,
|
|
272
|
+
harness: normalizedHarness ?? null,
|
|
273
|
+
acceptedAt: appended.at,
|
|
274
|
+
};
|
|
275
|
+
},
|
|
276
|
+
);
|
|
277
|
+
}
|
package/src/core/handoff.js
CHANGED
|
@@ -2,12 +2,13 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import { readdir } from "node:fs/promises";
|
|
3
3
|
|
|
4
4
|
import { canonicalFingerprint, readJsonArtifact, writeJsonArtifact } from "./artifacts.js";
|
|
5
|
-
import {
|
|
5
|
+
import { assertPortableContextSafe, normalizePortableText } from "./portable-context.js";
|
|
6
6
|
import { appendProtocolEvent } from "./events.js";
|
|
7
|
-
import { currentChangedPaths } from "./repository.js";
|
|
7
|
+
import { currentChangedPaths, currentRepositoryFingerprint } from "./repository.js";
|
|
8
8
|
import { readContract } from "./contract.js";
|
|
9
9
|
import { readPersistedRoute } from "./route-artifact.js";
|
|
10
10
|
import { readWorkState } from "./work-state.js";
|
|
11
|
+
import { assertStateIdentity } from "./completion-relationships.js";
|
|
11
12
|
import { resolveTaskClaimState } from "./task-claim-state.js";
|
|
12
13
|
import { readContinuity } from "./continuity.js";
|
|
13
14
|
import { getPackageRoot } from "./templates.js";
|
|
@@ -26,12 +27,13 @@ function handoffError(code, message, artifacts = []) {
|
|
|
26
27
|
return error;
|
|
27
28
|
}
|
|
28
29
|
|
|
29
|
-
function optionalText(value, label) {
|
|
30
|
+
function optionalText(value, label, maxLength = 2000) {
|
|
30
31
|
if (value === undefined || value === null) return null;
|
|
31
|
-
|
|
32
|
+
try {
|
|
33
|
+
return normalizePortableText(value, { label, maxLength, optional: true });
|
|
34
|
+
} catch (error) {
|
|
32
35
|
throw handoffError("E_HANDOFF_INVALID", `${label} must be a non-empty string when provided`);
|
|
33
36
|
}
|
|
34
|
-
return value;
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
function pathList(value) {
|
|
@@ -43,8 +45,22 @@ function handoffWithoutDigest(value) {
|
|
|
43
45
|
return body;
|
|
44
46
|
}
|
|
45
47
|
|
|
48
|
+
export function compareRepositoryFingerprint(expected, current) {
|
|
49
|
+
const expectedUnavailable = !expected || (expected.branch === null && expected.head === null);
|
|
50
|
+
const currentUnavailable = !current || (current.branch === null && current.head === null);
|
|
51
|
+
if (expectedUnavailable && currentUnavailable) return "NOT_VERIFIED";
|
|
52
|
+
if (expectedUnavailable !== currentUnavailable) return "MISMATCH";
|
|
53
|
+
return expected.branch === current.branch && expected.head === current.head
|
|
54
|
+
? "MATCH"
|
|
55
|
+
: "MISMATCH";
|
|
56
|
+
}
|
|
57
|
+
|
|
46
58
|
export function validateHandoffDigest(handoff) {
|
|
47
|
-
|
|
59
|
+
try {
|
|
60
|
+
assertPortableContextSafe(handoff);
|
|
61
|
+
} catch (error) {
|
|
62
|
+
throw handoffError("E_HANDOFF_INVALID", error.message);
|
|
63
|
+
}
|
|
48
64
|
if (typeof handoff?.artifactDigest !== "string"
|
|
49
65
|
|| handoff.artifactDigest !== canonicalFingerprint(handoffWithoutDigest(handoff))) {
|
|
50
66
|
throw handoffError("E_HANDOFF_TAMPERED", "Handoff artifact digest does not match its canonical content");
|
|
@@ -73,17 +89,33 @@ export async function buildCanonicalHandoff(target, {
|
|
|
73
89
|
createdAt = new Date().toISOString(),
|
|
74
90
|
} = {}) {
|
|
75
91
|
if (!taskId) throw handoffError("E_HANDOFF_STATE_UNAVAILABLE", "taskId is required to build a handoff");
|
|
76
|
-
const [state, contract, route, claims, changedPaths, continuity] = await Promise.all([
|
|
92
|
+
const [state, contract, route, claims, changedPaths, continuity, repositoryFingerprint] = await Promise.all([
|
|
77
93
|
readWorkState(target, { packageRoot, taskId }),
|
|
78
94
|
readContract(target, packageRoot, { taskId }),
|
|
79
95
|
readPersistedRoute(target, packageRoot, { taskId }),
|
|
80
96
|
resolveTaskClaimState(target, { packageRoot, taskId }),
|
|
81
97
|
currentChangedPaths(target),
|
|
82
98
|
currentContinuity(target, packageRoot, taskId),
|
|
99
|
+
currentRepositoryFingerprint(target),
|
|
83
100
|
]);
|
|
84
101
|
if (!state || !contract || !route || changedPaths === null || !claims.valid) {
|
|
85
102
|
throw handoffError("E_HANDOFF_STATE_UNAVAILABLE", "Canonical task state, route, claims, and changed paths are required for a handoff");
|
|
86
103
|
}
|
|
104
|
+
try {
|
|
105
|
+
assertStateIdentity({ contract, route, state });
|
|
106
|
+
} catch (error) {
|
|
107
|
+
throw handoffError(
|
|
108
|
+
"E_HANDOFF_STATE_UNAVAILABLE",
|
|
109
|
+
`Canonical task artifacts are not coherent for handoff creation: ${error.message}`,
|
|
110
|
+
error.artifacts ?? [],
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
if (compareRepositoryFingerprint(state.repositoryFingerprint, repositoryFingerprint) === "MISMATCH") {
|
|
114
|
+
throw handoffError(
|
|
115
|
+
"E_HANDOFF_STATE_UNAVAILABLE",
|
|
116
|
+
"Canonical repository fingerprint has drifted from current repository state",
|
|
117
|
+
);
|
|
118
|
+
}
|
|
87
119
|
const checkItems = [...(state.checks ?? [])];
|
|
88
120
|
const executionRefs = pathList(checkItems.map((check) => check.executionRef).filter(Boolean));
|
|
89
121
|
const checkIds = pathList(checkItems.map((check) => check.id).filter(Boolean));
|
|
@@ -101,9 +133,10 @@ export async function buildCanonicalHandoff(target, {
|
|
|
101
133
|
phase: WORK_PHASES.includes(state.phase) ? state.phase : "UNKNOWN",
|
|
102
134
|
revision: state.revision ?? 0,
|
|
103
135
|
verificationCycle: state.verificationCycle ?? 1,
|
|
136
|
+
workStateFingerprint: canonicalFingerprint(state),
|
|
104
137
|
contractFingerprint: contract.fingerprint,
|
|
105
138
|
routeFingerprint: route.fingerprint ?? null,
|
|
106
|
-
repositoryFingerprint
|
|
139
|
+
repositoryFingerprint,
|
|
107
140
|
writeClaims: pathList(claims.effectiveWriteClaims ?? []),
|
|
108
141
|
changedPaths: pathList(changedPaths),
|
|
109
142
|
},
|
package/src/core/inspect.js
CHANGED
|
@@ -15,6 +15,8 @@ import { findTaskById } from "./task-discovery.js";
|
|
|
15
15
|
import { buildTaskTrace } from "./trace.js";
|
|
16
16
|
import { evaluateProgress } from "./progress.js";
|
|
17
17
|
import { readEvents } from "./events.js";
|
|
18
|
+
import { taskStructuralQualityDirectory } from "./task-paths.js";
|
|
19
|
+
import { projectStructuralQualityStatus } from "./structural-quality/status.js";
|
|
18
20
|
|
|
19
21
|
function reasonCodesFor({ state, trace, progress }) {
|
|
20
22
|
const codes = [];
|
|
@@ -28,6 +30,40 @@ function reasonCodesFor({ state, trace, progress }) {
|
|
|
28
30
|
return codes;
|
|
29
31
|
}
|
|
30
32
|
|
|
33
|
+
function structuralQualityIssues(quality, taskId) {
|
|
34
|
+
if (!quality || quality.mode !== "gate") return [];
|
|
35
|
+
const qualityPath = quality.current?.artifactRef ?? taskStructuralQualityDirectory(taskId);
|
|
36
|
+
if (quality.baseline?.status !== "OBSERVED") {
|
|
37
|
+
return [{
|
|
38
|
+
code: "E_STRUCTURAL_QUALITY_BASELINE_MISSING",
|
|
39
|
+
message: "Structural-quality gate evidence has no valid immutable baseline.",
|
|
40
|
+
path: taskStructuralQualityDirectory(taskId),
|
|
41
|
+
}];
|
|
42
|
+
}
|
|
43
|
+
if (!quality.current?.artifactRef) {
|
|
44
|
+
return [{
|
|
45
|
+
code: "E_STRUCTURAL_QUALITY_EVIDENCE_STALE",
|
|
46
|
+
message: "Structural-quality gate has no evaluation for the current verification cycle.",
|
|
47
|
+
path: qualityPath,
|
|
48
|
+
}];
|
|
49
|
+
}
|
|
50
|
+
if (quality.current.status === "FAIL") {
|
|
51
|
+
return [{
|
|
52
|
+
code: "E_STRUCTURAL_QUALITY_REGRESSION",
|
|
53
|
+
message: "Structural-quality verification detected a regression.",
|
|
54
|
+
path: qualityPath,
|
|
55
|
+
}];
|
|
56
|
+
}
|
|
57
|
+
if (quality.current.status === "BLOCKED") {
|
|
58
|
+
return [{
|
|
59
|
+
code: "E_STRUCTURAL_QUALITY_EVALUATION_INCOMPARABLE",
|
|
60
|
+
message: "Structural-quality verification is blocked or incomparable.",
|
|
61
|
+
path: qualityPath,
|
|
62
|
+
}];
|
|
63
|
+
}
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
|
|
31
67
|
async function buildTaskInspection({ target, packageRoot, taskId, state, classifiedStatus = null }) {
|
|
32
68
|
const trace = await buildTaskTrace({ target, packageRoot, taskId });
|
|
33
69
|
const events = await readEvents(target, packageRoot, { taskId });
|
|
@@ -39,6 +75,22 @@ async function buildTaskInspection({ target, packageRoot, taskId, state, classif
|
|
|
39
75
|
)].sort();
|
|
40
76
|
|
|
41
77
|
const issues = [];
|
|
78
|
+
let structuralQuality;
|
|
79
|
+
try {
|
|
80
|
+
structuralQuality = await projectStructuralQualityStatus({ target, packageRoot, taskId });
|
|
81
|
+
} catch (error) {
|
|
82
|
+
structuralQuality = {
|
|
83
|
+
mode: "off",
|
|
84
|
+
provider: null,
|
|
85
|
+
baseline: { status: "INVALID", qualitySignal: null, artifactRef: null, fingerprint: null },
|
|
86
|
+
current: { status: "NOT_OBSERVED", verificationCycle: null, attempt: null, qualitySignal: null, delta: null, bottleneck: null, artifactRef: null },
|
|
87
|
+
comparable: null,
|
|
88
|
+
completionRequired: false,
|
|
89
|
+
reasonCodes: [error.code ?? "E_STRUCTURAL_QUALITY_EVIDENCE_STALE"],
|
|
90
|
+
next: null,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
issues.push(...structuralQualityIssues(structuralQuality, taskId));
|
|
42
94
|
if (!trace.snapshot.consistent) {
|
|
43
95
|
issues.push({ code: "E_TRACE_SNAPSHOT_INCONSISTENT", message: "Task artifacts changed while being read; rerun inspect for a consistent view." });
|
|
44
96
|
}
|
|
@@ -98,6 +150,7 @@ async function buildTaskInspection({ target, packageRoot, taskId, state, classif
|
|
|
98
150
|
valid: trace.integrity.valid,
|
|
99
151
|
errors: trace.integrity.errors,
|
|
100
152
|
},
|
|
153
|
+
structuralQuality,
|
|
101
154
|
audit: {},
|
|
102
155
|
completion: {},
|
|
103
156
|
issues,
|
|
@@ -213,6 +266,17 @@ export async function inspectTarget({ target, packageRoot, contractFile = null,
|
|
|
213
266
|
const taskInspection = taskId
|
|
214
267
|
? await buildTaskInspection({ target, packageRoot, taskId, state: rawState, classifiedStatus: classifiedState.status })
|
|
215
268
|
: null;
|
|
269
|
+
for (const issue of taskInspection?.issues ?? []) {
|
|
270
|
+
if (!issue.code.startsWith("E_STRUCTURAL_QUALITY")) continue;
|
|
271
|
+
findings.push({
|
|
272
|
+
code: issue.code,
|
|
273
|
+
severity: "error",
|
|
274
|
+
path: issue.path,
|
|
275
|
+
message: issue.message,
|
|
276
|
+
remediation: `Run forgeloop next --task ${taskId} --json and follow the structural-quality guidance.`,
|
|
277
|
+
evidence: createEvidence({ kind: "BLOCKED", source: issue.path, result: issue.code }),
|
|
278
|
+
});
|
|
279
|
+
}
|
|
216
280
|
return {
|
|
217
281
|
target: { path: target },
|
|
218
282
|
authority: trustedAuthorityConfiguration({ target, authorityContext, runtimeContext }),
|
|
@@ -32,6 +32,7 @@ const READ_ONLY_COMMANDS = Object.freeze(new Set([
|
|
|
32
32
|
"efficiency",
|
|
33
33
|
"workspace-status", "handoff-list", "handoff-show", "responsibility-status",
|
|
34
34
|
"attestation-verify", "attestation-status", "attestation-verify-range",
|
|
35
|
+
"quality-status",
|
|
35
36
|
]));
|
|
36
37
|
|
|
37
38
|
const LOOP_MUTATION_COMMANDS = Object.freeze(new Set([
|
|
@@ -41,7 +42,7 @@ const LOOP_MUTATION_COMMANDS = Object.freeze(new Set([
|
|
|
41
42
|
"record-terminal-result", "complete",
|
|
42
43
|
"record-intervention", "record-hypothesis-disposition",
|
|
43
44
|
"usage-record",
|
|
44
|
-
"workspace-bind", "handoff-create", "responsibility-set", "verify-scope", "attestation-create",
|
|
45
|
+
"workspace-bind", "handoff-create", "handoff-accept", "responsibility-set", "verify-scope", "attestation-create",
|
|
45
46
|
]));
|
|
46
47
|
|
|
47
48
|
const STATIC_RISK_CLASSES = Object.freeze({
|
|
@@ -49,6 +50,8 @@ const STATIC_RISK_CLASSES = Object.freeze({
|
|
|
49
50
|
...Object.fromEntries([...LOOP_MUTATION_COMMANDS].map((name) => [name, INTEGRATION_RISK_CLASSES.LOOP_MUTATION])),
|
|
50
51
|
"task-resume": INTEGRATION_RISK_CLASSES.CLAIM_REACQUISITION,
|
|
51
52
|
"run-check": INTEGRATION_RISK_CLASSES.EXTERNAL_EXECUTION,
|
|
53
|
+
"quality-baseline": INTEGRATION_RISK_CLASSES.EXTERNAL_EXECUTION,
|
|
54
|
+
"quality-verify": INTEGRATION_RISK_CLASSES.EXTERNAL_EXECUTION,
|
|
52
55
|
"run-action": INTEGRATION_RISK_CLASSES.EXTERNAL_EXECUTION,
|
|
53
56
|
"reconcile-closure": INTEGRATION_RISK_CLASSES.EXTERNAL_EXECUTION,
|
|
54
57
|
"action-propose": INTEGRATION_RISK_CLASSES.LOOP_MUTATION,
|
|
@@ -186,10 +189,27 @@ export function getForgeLoopCapabilities({ packageVersion = null } = {}) {
|
|
|
186
189
|
explicitRebinding: false,
|
|
187
190
|
},
|
|
188
191
|
canonicalHandoffs: {
|
|
189
|
-
version:
|
|
192
|
+
version: 2,
|
|
190
193
|
supported: true,
|
|
191
194
|
immutable: true,
|
|
192
195
|
lifecycleAuthority: false,
|
|
196
|
+
evidenceAuthority: false,
|
|
197
|
+
exactlyOnceAcceptance: true,
|
|
198
|
+
acceptanceLedgerBacked: true,
|
|
199
|
+
acceptanceCommand: "handoff-accept",
|
|
200
|
+
acceptanceStatuses: ["OPEN", "ACCEPTED", "UNBOUND", "INCONSISTENT"],
|
|
201
|
+
},
|
|
202
|
+
advisoryContextProviders: {
|
|
203
|
+
version: 1,
|
|
204
|
+
supported: true,
|
|
205
|
+
providerNeutral: true,
|
|
206
|
+
integrationApiOnly: true,
|
|
207
|
+
lazy: true,
|
|
208
|
+
optIn: true,
|
|
209
|
+
persistedByForgeLoop: false,
|
|
210
|
+
lifecycleAuthority: false,
|
|
211
|
+
evidenceAuthority: false,
|
|
212
|
+
executable: false,
|
|
193
213
|
},
|
|
194
214
|
responsibilityConstraints: {
|
|
195
215
|
version: 1,
|
|
@@ -203,6 +223,17 @@ export function getForgeLoopCapabilities({ packageVersion = null } = {}) {
|
|
|
203
223
|
modes: ["AUTO", "CHANGED", "CLAIMED", "FULL"],
|
|
204
224
|
impactedMode: false,
|
|
205
225
|
},
|
|
226
|
+
structuralQuality: {
|
|
227
|
+
version: 1,
|
|
228
|
+
supported: true,
|
|
229
|
+
schemaVersion: 1,
|
|
230
|
+
providerNeutral: true,
|
|
231
|
+
modes: ["off", "observe", "gate"],
|
|
232
|
+
builtInProviders: ["sentrux"],
|
|
233
|
+
commands: ["quality-baseline", "quality-verify", "quality-status"],
|
|
234
|
+
baselineImmutableAfterExecution: true,
|
|
235
|
+
maxOutputBytes: 2 * 1024 * 1024,
|
|
236
|
+
},
|
|
206
237
|
codeAttestation: {
|
|
207
238
|
version: 1,
|
|
208
239
|
supported: true,
|
|
@@ -232,6 +263,7 @@ export function getForgeLoopCapabilities({ packageVersion = null } = {}) {
|
|
|
232
263
|
{ name: "task/responsibility", scope: "TASK" },
|
|
233
264
|
{ name: "task/verification-scope", scope: "TASK" },
|
|
234
265
|
{ name: "task/attestation", scope: "TASK" },
|
|
266
|
+
{ name: "task/structural-quality", scope: "TASK" },
|
|
235
267
|
{ name: "task/actions", scope: "TASK" },
|
|
236
268
|
{ name: "task/action", scope: "TASK" },
|
|
237
269
|
{ name: "task/approvals", scope: "TASK" },
|
|
@@ -14,10 +14,12 @@ import { readJsonArtifact } from "./artifacts.js";
|
|
|
14
14
|
import { taskDirectory } from "./task-paths.js";
|
|
15
15
|
import { resolveWorkspaceBindingStatus } from "./workspace-binding.js";
|
|
16
16
|
import { listCanonicalHandoffs } from "./handoff.js";
|
|
17
|
+
import { readHandoffAcceptanceLedger, resolveHandoffAcceptance } from "./handoff-acceptance.js";
|
|
17
18
|
import { resolveResponsibilityStatus } from "./responsibility.js";
|
|
18
19
|
import { readVerificationScope } from "./verification-scope.js";
|
|
19
20
|
import { resolveAttestationStatus } from "./attestation.js";
|
|
20
21
|
import { buildExecutionProfileContext } from "./execution-profile-context.js";
|
|
22
|
+
import { projectStructuralQualityStatus } from "./structural-quality/service.js";
|
|
21
23
|
|
|
22
24
|
/**
|
|
23
25
|
* Canonical integration resource allowlist.
|
|
@@ -77,6 +79,7 @@ export const INTEGRATION_RESOURCE_DEFINITIONS = Object.freeze({
|
|
|
77
79
|
scope: "TASK",
|
|
78
80
|
description: "Local code-attestation status and trust level for one task.",
|
|
79
81
|
}),
|
|
82
|
+
"task/structural-quality": Object.freeze({ scope: "TASK", description: "Read-only structural-quality baseline, evaluation, comparison, and next-action projection." }),
|
|
80
83
|
"task/actions": Object.freeze({ scope: "TASK", description: "Canonical durable action summaries for one task." }),
|
|
81
84
|
"task/action": Object.freeze({ scope: "TASK", description: "One canonical durable action artifact." }),
|
|
82
85
|
"task/approvals": Object.freeze({ scope: "TASK", description: "Durable approval artifacts for one task." }),
|
|
@@ -157,6 +160,14 @@ export async function readForgeLoopIntegrationResource(uri, {
|
|
|
157
160
|
}
|
|
158
161
|
break;
|
|
159
162
|
}
|
|
163
|
+
case "task/structural-quality": {
|
|
164
|
+
if (typeof taskId !== "string" || !taskId) {
|
|
165
|
+
const error = new Error(`Resource ${uri} requires a taskId`);
|
|
166
|
+
error.code = "E_TASK_REQUIRED";
|
|
167
|
+
throw error;
|
|
168
|
+
}
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
160
171
|
case "task/actions":
|
|
161
172
|
case "task/action":
|
|
162
173
|
case "task/approvals":
|
|
@@ -174,12 +185,38 @@ export async function readForgeLoopIntegrationResource(uri, {
|
|
|
174
185
|
const projection = await resolveTaskClaimState(projectPath, { taskId, packageRoot });
|
|
175
186
|
return { uri, taskId, data: ownershipProjection(projection) };
|
|
176
187
|
}
|
|
188
|
+
if (uri === "task/structural-quality") {
|
|
189
|
+
return {
|
|
190
|
+
uri,
|
|
191
|
+
taskId,
|
|
192
|
+
data: await projectStructuralQualityStatus({ projectRoot: projectPath, target: projectPath, packageRoot, taskId }),
|
|
193
|
+
};
|
|
194
|
+
}
|
|
177
195
|
if (uri === "task/workspace-binding") {
|
|
178
196
|
return { uri, taskId, data: await resolveWorkspaceBindingStatus(projectPath, { packageRoot, taskId }) };
|
|
179
197
|
}
|
|
180
198
|
if (uri === "task/handoffs") {
|
|
181
199
|
const handoffs = await listCanonicalHandoffs(projectPath, { packageRoot, taskId });
|
|
182
|
-
|
|
200
|
+
const ledger = await readHandoffAcceptanceLedger(projectPath, packageRoot, { taskId });
|
|
201
|
+
const projectedHandoffs = handoffs.map((handoff) => {
|
|
202
|
+
const resolved = resolveHandoffAcceptance({
|
|
203
|
+
events: ledger.events,
|
|
204
|
+
handoff,
|
|
205
|
+
ledgerValid: ledger.valid,
|
|
206
|
+
ledgerErrors: ledger.errors,
|
|
207
|
+
});
|
|
208
|
+
return {
|
|
209
|
+
...handoff,
|
|
210
|
+
acceptance: {
|
|
211
|
+
status: resolved.status,
|
|
212
|
+
consumerId: resolved.consumerId ?? null,
|
|
213
|
+
harness: resolved.harness ?? null,
|
|
214
|
+
acceptedAt: resolved.acceptedAt ?? null,
|
|
215
|
+
...(resolved.reasonCodes ? { reasonCodes: [...resolved.reasonCodes] } : {}),
|
|
216
|
+
},
|
|
217
|
+
};
|
|
218
|
+
});
|
|
219
|
+
return { uri, taskId, data: { taskId, count: projectedHandoffs.length, handoffs: projectedHandoffs } };
|
|
183
220
|
}
|
|
184
221
|
if (uri === "task/responsibility") {
|
|
185
222
|
return { uri, taskId, data: await resolveResponsibilityStatus(projectPath, { packageRoot, taskId }) };
|
|
@@ -12,6 +12,10 @@ export const NEXT_ACTIONS = Object.freeze({
|
|
|
12
12
|
ENTER_VERIFYING: "ENTER_VERIFYING",
|
|
13
13
|
CONTINUE_IMPLEMENTATION: "CONTINUE_IMPLEMENTATION",
|
|
14
14
|
RECORD_VERIFICATION: "RECORD_VERIFICATION",
|
|
15
|
+
CAPTURE_STRUCTURAL_QUALITY_BASELINE: "CAPTURE_STRUCTURAL_QUALITY_BASELINE",
|
|
16
|
+
VERIFY_STRUCTURAL_QUALITY: "VERIFY_STRUCTURAL_QUALITY",
|
|
17
|
+
DIAGNOSE_STRUCTURAL_QUALITY_REGRESSION: "DIAGNOSE_STRUCTURAL_QUALITY_REGRESSION",
|
|
18
|
+
RESOLVE_STRUCTURAL_QUALITY_BLOCKER: "RESOLVE_STRUCTURAL_QUALITY_BLOCKER",
|
|
15
19
|
DIAGNOSE: "DIAGNOSE",
|
|
16
20
|
RECORD_DIAGNOSIS: "RECORD_DIAGNOSIS",
|
|
17
21
|
CORRECT: "CORRECT",
|
|
@@ -118,6 +122,7 @@ export function result({
|
|
|
118
122
|
approvalRequired = undefined,
|
|
119
123
|
capabilityDecision = undefined,
|
|
120
124
|
reconciliationAuthorityRequired = undefined,
|
|
125
|
+
optionalActions = undefined,
|
|
121
126
|
}) {
|
|
122
127
|
const normalizedReasons = reasons
|
|
123
128
|
.map((reason) => {
|
|
@@ -158,6 +163,7 @@ export function result({
|
|
|
158
163
|
...(reconciliationAuthorityRequired
|
|
159
164
|
? { reconciliationAuthorityRequired: structuredClone(reconciliationAuthorityRequired) }
|
|
160
165
|
: {}),
|
|
166
|
+
...(optionalActions?.length ? { optionalActions: structuredClone(optionalActions) } : {}),
|
|
161
167
|
};
|
|
162
168
|
}
|
|
163
169
|
|
|
@@ -167,6 +173,9 @@ export function commandFor(action) {
|
|
|
167
173
|
[NEXT_ACTIONS.RUN_PREFLIGHT]: "forgeloop preflight --json",
|
|
168
174
|
[NEXT_ACTIONS.START_EXECUTION]: "forgeloop advance --to EXECUTING",
|
|
169
175
|
[NEXT_ACTIONS.ENTER_VERIFYING]: "forgeloop advance --to VERIFYING",
|
|
176
|
+
[NEXT_ACTIONS.CAPTURE_STRUCTURAL_QUALITY_BASELINE]: "forgeloop quality-baseline --task <id> --json",
|
|
177
|
+
[NEXT_ACTIONS.VERIFY_STRUCTURAL_QUALITY]: "forgeloop quality-verify --task <id> --json",
|
|
178
|
+
[NEXT_ACTIONS.DIAGNOSE_STRUCTURAL_QUALITY_REGRESSION]: "forgeloop advance --to DIAGNOSING",
|
|
170
179
|
[NEXT_ACTIONS.DIAGNOSE]: "forgeloop advance --to DIAGNOSING",
|
|
171
180
|
[NEXT_ACTIONS.CORRECT]: "forgeloop advance --to CORRECTING",
|
|
172
181
|
[NEXT_ACTIONS.ENTER_REVIEWING]: "forgeloop advance --to REVIEWING",
|
|
@@ -237,7 +246,7 @@ export function recordTerminalResultCommandSpec(requirement) {
|
|
|
237
246
|
};
|
|
238
247
|
}
|
|
239
248
|
|
|
240
|
-
export function decision(input, action, reason, requiredArtifacts = [], missingArtifacts = []) {
|
|
249
|
+
export function decision(input, action, reason, requiredArtifacts = [], missingArtifacts = [], optionalActions = undefined) {
|
|
241
250
|
const command = commandFor(action);
|
|
242
251
|
return result({
|
|
243
252
|
...input,
|
|
@@ -246,6 +255,7 @@ export function decision(input, action, reason, requiredArtifacts = [], missingA
|
|
|
246
255
|
...(command ? { commands: [command] } : {}),
|
|
247
256
|
requiredArtifacts,
|
|
248
257
|
missingArtifacts,
|
|
258
|
+
optionalActions,
|
|
249
259
|
});
|
|
250
260
|
}
|
|
251
261
|
|