@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,117 @@
|
|
|
1
|
+
import { resolveTarget } from "./filesystem.js";
|
|
2
|
+
import { getPackageRoot } from "./templates.js";
|
|
3
|
+
import { CLI_COMMAND_DEFINITIONS } from "./cli-command-definitions.js";
|
|
4
|
+
import { COMMAND_EXECUTORS } from "./command-executors.js";
|
|
5
|
+
import { defaultCommandInputValues, validateForgeLoopCommandInput } from "./command-input.js";
|
|
6
|
+
import { PROTOCOL_VERSION } from "./protocol.js";
|
|
7
|
+
|
|
8
|
+
export const FORGELOOP_INTEGRATION_RUNTIME_VERSION = 1;
|
|
9
|
+
|
|
10
|
+
async function readPackageVersion(packageRoot) {
|
|
11
|
+
const { readFile } = await import("node:fs/promises");
|
|
12
|
+
const path = await import("node:path");
|
|
13
|
+
const packageJson = JSON.parse(await readFile(path.join(packageRoot, "package.json"), "utf8"));
|
|
14
|
+
return packageJson.version;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Transport-neutral ForgeLoop command execution.
|
|
19
|
+
*
|
|
20
|
+
* Resolves the project target, validates the command and its structured
|
|
21
|
+
* input, invokes the canonical executor, and returns a deterministic
|
|
22
|
+
* envelope. Performs zero terminal output, never spawns a shell to run
|
|
23
|
+
* ForgeLoop itself, never bypasses task ownership/recovery guards, and never
|
|
24
|
+
* retries mutations.
|
|
25
|
+
*
|
|
26
|
+
* A non-zero exitCode is a protocol/domain outcome (e.g. preflight BLOCKED),
|
|
27
|
+
* not an invocation failure: `ok` stays true. `ok:false` means the command
|
|
28
|
+
* could not be executed (unknown command, invalid input, canonical error).
|
|
29
|
+
*/
|
|
30
|
+
export async function executeForgeLoopCommand({
|
|
31
|
+
command,
|
|
32
|
+
projectPath = ".",
|
|
33
|
+
input = {},
|
|
34
|
+
authorityContext,
|
|
35
|
+
runtimeContext,
|
|
36
|
+
} = {}) {
|
|
37
|
+
const metadata = Object.freeze({
|
|
38
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
39
|
+
integrationApiVersion: FORGELOOP_INTEGRATION_RUNTIME_VERSION,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
if (typeof command !== "string" || !CLI_COMMAND_DEFINITIONS[command]) {
|
|
43
|
+
return {
|
|
44
|
+
ok: false,
|
|
45
|
+
command: command ?? null,
|
|
46
|
+
exitCode: 1,
|
|
47
|
+
result: null,
|
|
48
|
+
error: { code: "E_COMMAND_UNSUPPORTED", message: `Unsupported ForgeLoop command: ${command ?? "(missing)"}` },
|
|
49
|
+
metadata,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
if (CLI_COMMAND_DEFINITIONS[command].bootstrapOnly) {
|
|
53
|
+
return {
|
|
54
|
+
ok: false,
|
|
55
|
+
command,
|
|
56
|
+
exitCode: 1,
|
|
57
|
+
result: null,
|
|
58
|
+
error: { code: "E_COMMAND_UNSUPPORTED", message: `Command ${command} is not available through the programmatic runtime` },
|
|
59
|
+
metadata,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const executor = COMMAND_EXECUTORS[command];
|
|
64
|
+
if (typeof executor !== "function") {
|
|
65
|
+
return {
|
|
66
|
+
ok: false,
|
|
67
|
+
command,
|
|
68
|
+
exitCode: 1,
|
|
69
|
+
result: null,
|
|
70
|
+
error: { code: "E_COMMAND_UNSUPPORTED", message: `Command ${command} has no canonical executor` },
|
|
71
|
+
metadata,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
const options = {
|
|
77
|
+
...defaultCommandInputValues(),
|
|
78
|
+
...(input ?? {}),
|
|
79
|
+
};
|
|
80
|
+
validateForgeLoopCommandInput({ command, input: options });
|
|
81
|
+
|
|
82
|
+
const target = await resolveTarget(process.cwd(), projectPath);
|
|
83
|
+
const packageRoot = getPackageRoot();
|
|
84
|
+
const packageVersion = await readPackageVersion(packageRoot);
|
|
85
|
+
|
|
86
|
+
// Trusted host authority and runtime context travel out-of-band, never
|
|
87
|
+
// inside actor-controlled command input (INV-AUTH-03).
|
|
88
|
+
const { result, exitCode } = await executor({
|
|
89
|
+
target,
|
|
90
|
+
packageRoot,
|
|
91
|
+
packageVersion,
|
|
92
|
+
options,
|
|
93
|
+
authorityContext,
|
|
94
|
+
runtimeContext,
|
|
95
|
+
});
|
|
96
|
+
return {
|
|
97
|
+
ok: true,
|
|
98
|
+
command,
|
|
99
|
+
exitCode,
|
|
100
|
+
result,
|
|
101
|
+
error: null,
|
|
102
|
+
metadata: Object.freeze({ ...metadata, packageVersion }),
|
|
103
|
+
};
|
|
104
|
+
} catch (error) {
|
|
105
|
+
return {
|
|
106
|
+
ok: false,
|
|
107
|
+
command,
|
|
108
|
+
exitCode: 1,
|
|
109
|
+
result: null,
|
|
110
|
+
error: {
|
|
111
|
+
code: error.code ?? "E_COMMAND_EXECUTION_FAILED",
|
|
112
|
+
message: error.message,
|
|
113
|
+
},
|
|
114
|
+
metadata,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -22,9 +22,22 @@ import { normalizeRequirements } from "./evidence-readiness.js";
|
|
|
22
22
|
import { classifyCommandResolution, validateVerificationAuthority } from "./verification-capability.js";
|
|
23
23
|
import { readExecutionArtifact, validateExecutionBinding } from "./execution.js";
|
|
24
24
|
import { taskArtifactPath, taskExecutionPath } from "./task-paths.js";
|
|
25
|
-
import { readTaskDescriptor } from "./task-descriptor.js";
|
|
26
25
|
import { assertClaimsCoverChangedPaths } from "./task-scope.js";
|
|
27
26
|
import { discoverTasks } from "./task-discovery.js";
|
|
27
|
+
import { listActions } from "./actions.js";
|
|
28
|
+
|
|
29
|
+
async function actionReceiptSummary(target, packageRoot, taskId) {
|
|
30
|
+
const actions = await listActions(target, { packageRoot, taskId });
|
|
31
|
+
return {
|
|
32
|
+
count: actions.length,
|
|
33
|
+
required: actions.filter((action) => action.requiredForCompletion).length,
|
|
34
|
+
verified: actions.filter((action) => action.state === "VERIFIED").length,
|
|
35
|
+
failed: actions.filter((action) => action.state === "FAILED").length,
|
|
36
|
+
ambiguous: actions.filter((action) => action.state === "COMMIT_UNKNOWN").length,
|
|
37
|
+
pending: actions.filter((action) => !["VERIFIED", "FAILED", "CANCELLED"].includes(action.state)).length,
|
|
38
|
+
actionRefs: actions.map((action) => action.actionId),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
28
41
|
|
|
29
42
|
/**
|
|
30
43
|
* Canonical terminal-result types shared by runtime validation, tests, and
|
|
@@ -250,12 +263,22 @@ export async function prepareCompletion({
|
|
|
250
263
|
if (existing && existingValue.stateFingerprint === undefined) {
|
|
251
264
|
throw artifactError("E_RECEIPT_STATE_MISMATCH", "Execution receipt requires the current work-state fingerprint", [receiptRel]);
|
|
252
265
|
}
|
|
266
|
+
// A checkpoint recreated after clear-state/loss starts with empty checks; a
|
|
267
|
+
// receipt still bound to the previous checkpoint fingerprint belongs to a
|
|
268
|
+
// superseded epoch and must not be adopted into the fresh one. The prior
|
|
269
|
+
// epoch remains auditable through the executions/ artifacts and the
|
|
270
|
+
// append-only ledger.
|
|
271
|
+
const receiptFromSupersededEpoch = Boolean(existing)
|
|
272
|
+
&& existingValue.stateFingerprint !== undefined
|
|
273
|
+
&& existingValue.stateFingerprint !== canonicalFingerprint(state)
|
|
274
|
+
&& (state.checks ?? []).length === 0;
|
|
275
|
+
const adoptedValue = receiptFromSupersededEpoch ? {} : existingValue;
|
|
253
276
|
assertStateIdentity({ contract, route, state });
|
|
254
277
|
|
|
255
278
|
let writeClaims = [];
|
|
256
279
|
try {
|
|
257
|
-
const
|
|
258
|
-
writeClaims =
|
|
280
|
+
const tasks = taskId ? await discoverTasks(target, packageRoot) : [];
|
|
281
|
+
writeClaims = tasks.find((task) => task.taskId === taskId && task.healthy !== false)?.writeClaims ?? [];
|
|
259
282
|
} catch {
|
|
260
283
|
// descriptor may not exist
|
|
261
284
|
}
|
|
@@ -287,35 +310,36 @@ export async function prepareCompletion({
|
|
|
287
310
|
|
|
288
311
|
const changedPaths = observedPaths !== null
|
|
289
312
|
? [...observedPaths]
|
|
290
|
-
: existing
|
|
313
|
+
: !receiptFromSupersededEpoch && existing
|
|
291
314
|
? [...(existingValue.changedPaths ?? [])]
|
|
292
315
|
: [];
|
|
293
|
-
const checks = existing ? [...existingValue.checks] : [...state.checks];
|
|
294
|
-
const evidence = existing ? [...(existingValue.evidence ?? [])] : [...state.verificationEvidence];
|
|
316
|
+
const checks = !receiptFromSupersededEpoch && existing ? [...existingValue.checks] : [...state.checks];
|
|
317
|
+
const evidence = !receiptFromSupersededEpoch && existing ? [...(existingValue.evidence ?? [])] : [...state.verificationEvidence];
|
|
295
318
|
const receipt = await createReceipt({
|
|
296
|
-
...
|
|
319
|
+
...adoptedValue,
|
|
297
320
|
taskId: contract.value.taskId,
|
|
298
321
|
contractFingerprint: contract.fingerprint,
|
|
299
322
|
routeFingerprint: route.fingerprint,
|
|
300
323
|
stateFingerprint: canonicalFingerprint(state),
|
|
301
324
|
verificationCycle: state.verificationCycle ?? 1,
|
|
302
|
-
status:
|
|
303
|
-
taskStatus:
|
|
304
|
-
verificationStatus:
|
|
305
|
-
publicationStatus:
|
|
306
|
-
productionReadiness:
|
|
325
|
+
status: adoptedValue.status ?? "in-progress",
|
|
326
|
+
taskStatus: adoptedValue.taskStatus ?? "in-progress",
|
|
327
|
+
verificationStatus: adoptedValue.verificationStatus ?? "not-verified",
|
|
328
|
+
publicationStatus: adoptedValue.publicationStatus ?? "local-only",
|
|
329
|
+
productionReadiness: adoptedValue.productionReadiness ?? "not-verified",
|
|
307
330
|
selectedGuides: [...route.value.guides],
|
|
308
331
|
changedPaths,
|
|
309
332
|
checks,
|
|
333
|
+
actions: await actionReceiptSummary(target, packageRoot, contract.value.taskId),
|
|
310
334
|
evidence,
|
|
311
335
|
evidenceCoverage: coverageForRequirements(requiredEvidence, checks, {
|
|
312
336
|
target,
|
|
313
337
|
taskId: contract.value.taskId,
|
|
314
338
|
options: { authorityContext, runtimeContext },
|
|
315
339
|
}),
|
|
316
|
-
review:
|
|
317
|
-
limitations: [...(
|
|
318
|
-
publication:
|
|
340
|
+
review: adoptedValue.review ?? { status: "not-run", independent: false },
|
|
341
|
+
limitations: [...(adoptedValue.limitations ?? [])],
|
|
342
|
+
publication: adoptedValue.publication ?? {
|
|
319
343
|
committed: false,
|
|
320
344
|
pushed: false,
|
|
321
345
|
pullRequest: null,
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { LIFECYCLE_MILESTONES, validateStateLedgerCoherence } from "./events.js";
|
|
2
|
+
|
|
3
|
+
export const CANONICAL_COMPLETION_EVENT = "COMPLETION_VALIDATED";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Canonical completion ownership proof: the minimal, validator-backed evidence
|
|
7
|
+
* that the lifecycle itself officially reached COMPLETE. This is intentionally
|
|
8
|
+
* NOT a re-run of full publication/receipt/evidence semantics — it only proves
|
|
9
|
+
* that claim ownership may be released because canonical completion exists.
|
|
10
|
+
*
|
|
11
|
+
* Returns `{ valid: true, completionEvent }` or `{ valid: false, errors }`.
|
|
12
|
+
*/
|
|
13
|
+
export function validateCompletionOwnershipProof({ taskId, state, ledger }) {
|
|
14
|
+
const errors = [];
|
|
15
|
+
if (!taskId || typeof taskId !== "string") {
|
|
16
|
+
return { valid: false, errors: [{ code: "E_COMPLETION_OWNERSHIP_UNPROVEN", message: "Completion ownership proof requires a taskId" }] };
|
|
17
|
+
}
|
|
18
|
+
if (!state || state.phase !== "COMPLETE") {
|
|
19
|
+
errors.push({ code: "E_COMPLETION_OWNERSHIP_UNPROVEN", message: "Work-state phase is not COMPLETE" });
|
|
20
|
+
}
|
|
21
|
+
if (!ledger || ledger.valid !== true) {
|
|
22
|
+
errors.push({
|
|
23
|
+
code: "E_COMPLETION_OWNERSHIP_UNPROVEN",
|
|
24
|
+
message: `Task event ledger is invalid; completion cannot be proven${ledger?.errors?.length
|
|
25
|
+
? `: ${ledger.errors.map((error) => error.message).join("; ")}`
|
|
26
|
+
: ""}`,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let completionEvent = null;
|
|
31
|
+
if (ledger && Array.isArray(ledger.events)) {
|
|
32
|
+
if (ledger.events.some((event) => event.taskId !== taskId)) {
|
|
33
|
+
errors.push({
|
|
34
|
+
code: "E_COMPLETION_OWNERSHIP_UNPROVEN",
|
|
35
|
+
message: "Ledger contains an event belonging to a different task",
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
const candidates = ledger.events
|
|
39
|
+
.filter((event) => event.event === CANONICAL_COMPLETION_EVENT && event.taskId === taskId);
|
|
40
|
+
if (candidates.length === 0) {
|
|
41
|
+
errors.push({
|
|
42
|
+
code: "E_COMPLETION_OWNERSHIP_UNPROVEN",
|
|
43
|
+
message: `No canonical ${CANONICAL_COMPLETION_EVENT} event exists for this task`,
|
|
44
|
+
});
|
|
45
|
+
} else if (candidates.length > 1) {
|
|
46
|
+
errors.push({
|
|
47
|
+
code: "E_COMPLETION_OWNERSHIP_UNPROVEN",
|
|
48
|
+
message: `Multiple ${CANONICAL_COMPLETION_EVENT} events exist; completion is ambiguous`,
|
|
49
|
+
});
|
|
50
|
+
} else {
|
|
51
|
+
completionEvent = candidates[0];
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (state && ledger && Array.isArray(ledger.events)) {
|
|
56
|
+
const coherenceErrors = validateStateLedgerCoherence(state, ledger.events);
|
|
57
|
+
if (coherenceErrors.length > 0) {
|
|
58
|
+
for (const error of coherenceErrors) {
|
|
59
|
+
errors.push({
|
|
60
|
+
code: "E_COMPLETION_OWNERSHIP_UNPROVEN",
|
|
61
|
+
message: `State/ledger coherence invalid: ${error.message}`,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// No contradictory lifecycle activity may follow the canonical completion:
|
|
68
|
+
// any milestone at or after VERIFICATION_RECORDED occurring after the
|
|
69
|
+
// completion event means the lifecycle moved past terminal state.
|
|
70
|
+
if (completionEvent && ledger && Array.isArray(ledger.events)) {
|
|
71
|
+
const completionIndex = ledger.events.indexOf(completionEvent);
|
|
72
|
+
const contradiction = ledger.events.slice(completionIndex + 1).find((event) => {
|
|
73
|
+
const index = LIFECYCLE_MILESTONES.indexOf(event.event);
|
|
74
|
+
return index >= LIFECYCLE_MILESTONES.indexOf("VERIFICATION_RECORDED");
|
|
75
|
+
});
|
|
76
|
+
if (contradiction) {
|
|
77
|
+
errors.push({
|
|
78
|
+
code: "E_COMPLETION_OWNERSHIP_UNPROVEN",
|
|
79
|
+
message: `Lifecycle event ${contradiction.event} follows canonical completion; terminal state contradicted`,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (errors.length > 0) {
|
|
85
|
+
return { valid: false, completionEvent: null, errors };
|
|
86
|
+
}
|
|
87
|
+
return { valid: true, completionEvent };
|
|
88
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { ARTIFACT_PATHS, canonicalFingerprint, readJsonArtifact, writeJsonArtifact } from "./artifacts.js";
|
|
2
|
+
import { appendProtocolEvent, validateCompletionRecoveryAuthorization, validateEventLedger } from "./events.js";
|
|
3
|
+
import { createReceipt } from "./receipt.js";
|
|
4
|
+
import { taskArtifactPath } from "./task-paths.js";
|
|
5
|
+
import { mutateWorkState, readWorkState } from "./work-state.js";
|
|
6
|
+
|
|
7
|
+
function resolveArtifactPath(key, taskId, override) {
|
|
8
|
+
if (override) return override;
|
|
9
|
+
return taskId ? taskArtifactPath(taskId, key) : ARTIFACT_PATHS[key];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const FINGERPRINT_MISMATCH_CODES = new Set([
|
|
13
|
+
"E_COMPLETION_REJECTION_STATE_FINGERPRINT_MISMATCH",
|
|
14
|
+
"E_COMPLETION_REJECTION_RECEIPT_FINGERPRINT_MISMATCH",
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
export function isFingerprintOnlyRecoveryMismatch(errors = []) {
|
|
18
|
+
return Array.isArray(errors)
|
|
19
|
+
&& errors.length > 0
|
|
20
|
+
&& errors.every((error) => FINGERPRINT_MISMATCH_CODES.has(error?.code));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function sortedValues(values) {
|
|
24
|
+
return [...new Set(values ?? [])].sort();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function sameSortedValues(left, right) {
|
|
28
|
+
return JSON.stringify(sortedValues(left)) === JSON.stringify(sortedValues(right));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function findMatchingRejectionEvent(events, attempt, cycle) {
|
|
32
|
+
let latestReviewIndex = -1;
|
|
33
|
+
for (let index = 0; index < events.length; index += 1) {
|
|
34
|
+
const event = events[index];
|
|
35
|
+
if (event.event === "REVIEW_STARTED" && (event.details?.verificationCycle ?? 1) === cycle) {
|
|
36
|
+
latestReviewIndex = index;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
for (let index = events.length - 1; index >= 0; index -= 1) {
|
|
40
|
+
const event = events[index];
|
|
41
|
+
if (event.event !== "COMPLETION_REJECTED") continue;
|
|
42
|
+
if ((event.details?.verificationCycle ?? 1) !== cycle) continue;
|
|
43
|
+
if (index < latestReviewIndex) continue;
|
|
44
|
+
return { event, index };
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Rebind a persisted REJECTED completion attempt to the current work-state
|
|
51
|
+
* checkpoint when the only authorization failures are fingerprint mismatches.
|
|
52
|
+
*
|
|
53
|
+
* Repository drift or a recovery/resume cycle can mutate work-state after a
|
|
54
|
+
* completion rejection was persisted, so the ledger snapshot no longer matches
|
|
55
|
+
* the live checkpoint. Without rebinding, every sanctioned closure path refuses
|
|
56
|
+
* (reconcile-closure and REVIEWING -> VERIFYING require authorized completion
|
|
57
|
+
* recovery; complete cannot persist a fresh evidence-only rejection while the
|
|
58
|
+
* checkpoint is stale), which deadlocks the task.
|
|
59
|
+
*
|
|
60
|
+
* The rebind is append-only and logically conservative:
|
|
61
|
+
* - the rejection reasonCodes, missingRequirementIds, and verification cycle
|
|
62
|
+
* must be logically identical between work-state and the latest matching
|
|
63
|
+
* ledger rejection; any logical difference is refused,
|
|
64
|
+
* - the original COMPLETION_REJECTED event is never modified; a rebound
|
|
65
|
+
* rejection carrying the current fingerprints is appended,
|
|
66
|
+
* - the execution receipt is re-bound to the current checkpoint when present.
|
|
67
|
+
*/
|
|
68
|
+
export async function rebindCompletionRejectionSnapshot({
|
|
69
|
+
target,
|
|
70
|
+
packageRoot,
|
|
71
|
+
taskId = null,
|
|
72
|
+
statePath = null,
|
|
73
|
+
receiptPath = null,
|
|
74
|
+
eventsPath = null,
|
|
75
|
+
authorityContext,
|
|
76
|
+
runtimeContext,
|
|
77
|
+
} = {}) {
|
|
78
|
+
const statePathResolved = resolveArtifactPath("state", taskId, statePath);
|
|
79
|
+
const receiptPathResolved = resolveArtifactPath("receipt", taskId, receiptPath);
|
|
80
|
+
const state = await readWorkState(target, { packageRoot, taskId, statePath: statePathResolved });
|
|
81
|
+
if (!state || state.phase !== "REVIEWING") {
|
|
82
|
+
return { rebound: false };
|
|
83
|
+
}
|
|
84
|
+
const attempt = state.lastCompletionAttempt;
|
|
85
|
+
if (!attempt || attempt.status !== "REJECTED") {
|
|
86
|
+
return { rebound: false };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const ledger = await validateEventLedger(target, packageRoot, { taskId, eventsPath });
|
|
90
|
+
if (!ledger.valid) {
|
|
91
|
+
return { rebound: false };
|
|
92
|
+
}
|
|
93
|
+
const cycle = attempt.verificationCycle ?? state.verificationCycle ?? 1;
|
|
94
|
+
const matching = findMatchingRejectionEvent(ledger.events, attempt, cycle);
|
|
95
|
+
if (!matching) {
|
|
96
|
+
return { rebound: false };
|
|
97
|
+
}
|
|
98
|
+
const details = matching.event.details ?? {};
|
|
99
|
+
const logicalMatch = (details.verificationCycle ?? 1) === cycle
|
|
100
|
+
&& sameSortedValues(details.reasonCodes, attempt.reasonCodes)
|
|
101
|
+
&& sameSortedValues(details.missingRequirementIds, attempt.missingRequirementIds);
|
|
102
|
+
if (!logicalMatch) {
|
|
103
|
+
return { rebound: false };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const next = await mutateWorkState(target, {
|
|
107
|
+
expectedRevision: state.revision ?? 0,
|
|
108
|
+
packageRoot,
|
|
109
|
+
taskId,
|
|
110
|
+
statePath: statePathResolved,
|
|
111
|
+
}, () => ({
|
|
112
|
+
...state,
|
|
113
|
+
revision: (state.revision ?? 0) + 1,
|
|
114
|
+
lastUpdated: new Date().toISOString(),
|
|
115
|
+
}));
|
|
116
|
+
|
|
117
|
+
let reboundReceiptFingerprint;
|
|
118
|
+
try {
|
|
119
|
+
const receipt = await readJsonArtifact(target, receiptPathResolved, "execution-receipt", packageRoot);
|
|
120
|
+
const reboundReceipt = await createReceipt({
|
|
121
|
+
...receipt.value,
|
|
122
|
+
stateFingerprint: canonicalFingerprint(next),
|
|
123
|
+
verificationCycle: next.verificationCycle ?? receipt.value.verificationCycle ?? 1,
|
|
124
|
+
}, packageRoot, { target, taskId, authorityContext, runtimeContext });
|
|
125
|
+
await writeJsonArtifact(target, receiptPathResolved, reboundReceipt, "execution-receipt", packageRoot);
|
|
126
|
+
reboundReceiptFingerprint = canonicalFingerprint(reboundReceipt);
|
|
127
|
+
} catch (error) {
|
|
128
|
+
if (error.code !== "ARTIFACT_MISSING") throw error;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
await appendProtocolEvent(target, {
|
|
132
|
+
taskId: state.taskId,
|
|
133
|
+
event: "COMPLETION_REJECTED",
|
|
134
|
+
details: {
|
|
135
|
+
verificationCycle: cycle,
|
|
136
|
+
reasonCodes: sortedValues(details.reasonCodes),
|
|
137
|
+
missingRequirementIds: sortedValues(details.missingRequirementIds),
|
|
138
|
+
stateFingerprint: canonicalFingerprint(next),
|
|
139
|
+
...(reboundReceiptFingerprint ? { receiptFingerprint: reboundReceiptFingerprint } : {}),
|
|
140
|
+
...(details.stateFingerprint ? { reboundFromStateFingerprint: details.stateFingerprint } : {}),
|
|
141
|
+
},
|
|
142
|
+
}, packageRoot, { taskId, eventsPath });
|
|
143
|
+
|
|
144
|
+
return { rebound: true, state: next };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export async function authorizeCompletionRecoveryOrRebind({
|
|
148
|
+
target,
|
|
149
|
+
packageRoot,
|
|
150
|
+
taskId = null,
|
|
151
|
+
statePath = null,
|
|
152
|
+
receiptPath = null,
|
|
153
|
+
eventsPath = null,
|
|
154
|
+
authorityContext,
|
|
155
|
+
runtimeContext,
|
|
156
|
+
} = {}) {
|
|
157
|
+
const statePathResolved = resolveArtifactPath("state", taskId, statePath);
|
|
158
|
+
const receiptPathResolved = resolveArtifactPath("receipt", taskId, receiptPath);
|
|
159
|
+
const resolveArtifacts = async () => {
|
|
160
|
+
const state = await readWorkState(target, { packageRoot, taskId, statePath: statePathResolved });
|
|
161
|
+
let receipt = null;
|
|
162
|
+
try {
|
|
163
|
+
receipt = (await readJsonArtifact(target, receiptPathResolved, "execution-receipt", packageRoot))?.value ?? null;
|
|
164
|
+
} catch {
|
|
165
|
+
receipt = null;
|
|
166
|
+
}
|
|
167
|
+
const events = (await validateEventLedger(target, packageRoot, { taskId, eventsPath })).events;
|
|
168
|
+
return { state, receipt, events };
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const initial = await resolveArtifacts();
|
|
172
|
+
const recoveryAuth = validateCompletionRecoveryAuthorization(initial);
|
|
173
|
+
if (recoveryAuth.authorized || !isFingerprintOnlyRecoveryMismatch(recoveryAuth.errors)) {
|
|
174
|
+
return { ...initial, recoveryAuth, rebound: false };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const reboundResult = await rebindCompletionRejectionSnapshot({
|
|
178
|
+
target,
|
|
179
|
+
packageRoot,
|
|
180
|
+
taskId,
|
|
181
|
+
statePath,
|
|
182
|
+
receiptPath,
|
|
183
|
+
eventsPath,
|
|
184
|
+
authorityContext,
|
|
185
|
+
runtimeContext,
|
|
186
|
+
});
|
|
187
|
+
if (!reboundResult.rebound) {
|
|
188
|
+
return { ...initial, recoveryAuth, rebound: false };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const rebounded = await resolveArtifacts();
|
|
192
|
+
const reboundAuth = validateCompletionRecoveryAuthorization(rebounded);
|
|
193
|
+
return { ...rebounded, recoveryAuth: reboundAuth, rebound: reboundAuth.authorized };
|
|
194
|
+
}
|
package/src/core/completion.js
CHANGED
|
@@ -13,6 +13,25 @@ import { isRecoverableCompletionEvidenceCode } from "./completion-recovery.js";
|
|
|
13
13
|
import { evaluateTerminalRequirements } from "./evidence-readiness.js";
|
|
14
14
|
import { PROJECT_ARTIFACT_PATHS, taskArtifactPath } from "./task-paths.js";
|
|
15
15
|
import { detectPolicyCapability, evaluateTargetPolicy } from "./policy-engine.js";
|
|
16
|
+
import { listActions } from "./actions.js";
|
|
17
|
+
|
|
18
|
+
async function actionReceiptSummary(target, packageRoot, taskId) {
|
|
19
|
+
const actions = await listActions(target, { packageRoot, taskId });
|
|
20
|
+
const { evaluateRequiredActionReadiness } = await import("./action-readiness.js");
|
|
21
|
+
const readiness = await evaluateRequiredActionReadiness({ target, packageRoot, taskId });
|
|
22
|
+
return {
|
|
23
|
+
count: actions.length,
|
|
24
|
+
required: readiness.total,
|
|
25
|
+
verified: actions.filter((action) => action.state === "VERIFIED").length,
|
|
26
|
+
// Raw state counts are observability only; trusted completion is below.
|
|
27
|
+
trustedSatisfied: readiness.satisfied,
|
|
28
|
+
unresolvedRequired: readiness.unresolved,
|
|
29
|
+
failed: actions.filter((action) => action.state === "FAILED").length,
|
|
30
|
+
ambiguous: actions.filter((action) => action.state === "COMMIT_UNKNOWN").length,
|
|
31
|
+
pending: actions.filter((action) => !["VERIFIED", "FAILED", "CANCELLED"].includes(action.state)).length,
|
|
32
|
+
actionRefs: actions.map((action) => action.actionId),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
16
35
|
|
|
17
36
|
/**
|
|
18
37
|
* Canonical completion return statuses shared by the runtime, tests, and
|
|
@@ -424,6 +443,44 @@ export async function evaluateCompletion({
|
|
|
424
443
|
}
|
|
425
444
|
}
|
|
426
445
|
|
|
446
|
+
const actionTaskId = contract?.value?.taskId ?? taskId;
|
|
447
|
+
const durableActions = actionTaskId
|
|
448
|
+
? await listActions(target, { packageRoot, taskId: actionTaskId })
|
|
449
|
+
: [];
|
|
450
|
+
const contractRequirements = new Set([
|
|
451
|
+
...(contract?.value?.verification ?? []), ...(contract?.value?.successCriteria ?? []),
|
|
452
|
+
]);
|
|
453
|
+
// Completion truth consumes the canonical action-readiness projection, not
|
|
454
|
+
// raw state labels: a forged VERIFIED label can never satisfy a required
|
|
455
|
+
// action (INV-VERIFY-02).
|
|
456
|
+
const { evaluateRequiredActionReadiness } = await import("./action-readiness.js");
|
|
457
|
+
const requiredActionReadiness = actionTaskId
|
|
458
|
+
? await evaluateRequiredActionReadiness({ target, packageRoot, taskId: actionTaskId })
|
|
459
|
+
: { total: 0, satisfied: 0, unresolved: 0, ambiguous: 0, failed: 0, untrusted: 0, actions: [] };
|
|
460
|
+
for (const readiness of requiredActionReadiness.actions) {
|
|
461
|
+
if (readiness.status === "SATISFIED") continue;
|
|
462
|
+
const action = durableActions.find((candidate) => candidate.actionId === readiness.actionId);
|
|
463
|
+
if (
|
|
464
|
+
readiness.status === "FAILED"
|
|
465
|
+
&& action?.state === "CANCELLED"
|
|
466
|
+
&& action.requirement
|
|
467
|
+
&& !contractRequirements.has(action.requirement)
|
|
468
|
+
) {
|
|
469
|
+
continue;
|
|
470
|
+
}
|
|
471
|
+
const code = readiness.status === "AMBIGUOUS"
|
|
472
|
+
? "E_ACTION_RECONCILIATION_REQUIRED"
|
|
473
|
+
: readiness.status === "UNTRUSTED"
|
|
474
|
+
? "E_ACTION_VERIFICATION_REQUIRED"
|
|
475
|
+
: "E_ACTION_STATE_MISMATCH";
|
|
476
|
+
errors.push(issue(
|
|
477
|
+
code,
|
|
478
|
+
`Required action ${readiness.actionId} is not trusted-satisfied (${readiness.status}): ${readiness.reasons[0] ?? ""}`,
|
|
479
|
+
[taskArtifactPath(action?.taskId ?? actionTaskId, "actions")],
|
|
480
|
+
{ actionId: readiness.actionId, actionState: action?.state, readiness: readiness.status },
|
|
481
|
+
));
|
|
482
|
+
}
|
|
483
|
+
|
|
427
484
|
const sortedErrors = sortIssues(errors);
|
|
428
485
|
const valid = sortedErrors.length === 0;
|
|
429
486
|
return {
|
|
@@ -440,6 +497,17 @@ export async function evaluateCompletion({
|
|
|
440
497
|
status: ledger.valid ? "valid" : "invalid",
|
|
441
498
|
events: ledger.events.length,
|
|
442
499
|
},
|
|
500
|
+
actions: {
|
|
501
|
+
count: durableActions.length,
|
|
502
|
+
required: requiredActionReadiness.total,
|
|
503
|
+
verified: durableActions.filter((action) => action.state === "VERIFIED").length,
|
|
504
|
+
trustedSatisfied: requiredActionReadiness.satisfied,
|
|
505
|
+
unresolvedRequired: requiredActionReadiness.unresolved,
|
|
506
|
+
failed: durableActions.filter((action) => action.state === "FAILED").length,
|
|
507
|
+
ambiguous: durableActions.filter((action) => action.state === "COMMIT_UNKNOWN").length,
|
|
508
|
+
pending: durableActions.filter((action) => !["VERIFIED", "FAILED", "CANCELLED"].includes(action.state)).length,
|
|
509
|
+
actionRefs: durableActions.map((action) => action.actionId),
|
|
510
|
+
},
|
|
443
511
|
};
|
|
444
512
|
}
|
|
445
513
|
|
|
@@ -528,6 +596,7 @@ export async function runComplete({
|
|
|
528
596
|
if (receipt) {
|
|
529
597
|
nextReceipt = await createReceipt({
|
|
530
598
|
...receipt.value,
|
|
599
|
+
actions: await actionReceiptSummary(target, packageRoot, state.taskId),
|
|
531
600
|
stateFingerprint: canonicalFingerprint(next),
|
|
532
601
|
verificationCycle: next.verificationCycle ?? receipt.value.verificationCycle ?? 1,
|
|
533
602
|
}, packageRoot, { target, taskId: state.taskId, authorityContext, runtimeContext });
|
|
@@ -574,6 +643,7 @@ export async function runComplete({
|
|
|
574
643
|
next.revision = (state.revision ?? 0) + 1;
|
|
575
644
|
const nextReceipt = await createReceipt({
|
|
576
645
|
...receipt.value,
|
|
646
|
+
actions: await actionReceiptSummary(target, packageRoot, state.taskId),
|
|
577
647
|
stateFingerprint: canonicalFingerprint(next),
|
|
578
648
|
verificationCycle: next.verificationCycle ?? receipt.value.verificationCycle ?? 1,
|
|
579
649
|
}, packageRoot, { target, taskId: state.taskId, authorityContext, runtimeContext });
|
|
@@ -169,20 +169,25 @@ export function classifyContinuity({
|
|
|
169
169
|
});
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
-
export async function reconcileContinuity({ target, packageRoot } = {}) {
|
|
172
|
+
export async function reconcileContinuity({ target, packageRoot, taskId = null } = {}) {
|
|
173
173
|
const [{ readWorkState }, { readContract }, repository] = await Promise.all([
|
|
174
174
|
import("./work-state.js"),
|
|
175
175
|
import("./contract.js"),
|
|
176
176
|
import("./repository.js"),
|
|
177
177
|
]);
|
|
178
178
|
|
|
179
|
-
const state = await readWorkState(target, packageRoot);
|
|
179
|
+
const state = await readWorkState(target, { packageRoot, taskId });
|
|
180
180
|
let continuityArtifact;
|
|
181
181
|
try {
|
|
182
|
-
continuityArtifact = await readContinuity(target, packageRoot);
|
|
182
|
+
continuityArtifact = await readContinuity(target, { packageRoot, taskId });
|
|
183
183
|
} catch (error) {
|
|
184
184
|
if (error.code === "ARTIFACT_MISSING") {
|
|
185
|
-
return {
|
|
185
|
+
return {
|
|
186
|
+
...classifyContinuity({ continuity: null, state }),
|
|
187
|
+
path: ".forgeloop/continuity.json",
|
|
188
|
+
present: false,
|
|
189
|
+
diagnosticContext: await deriveDiagnosticContextSafe({ target, packageRoot, state }),
|
|
190
|
+
};
|
|
186
191
|
}
|
|
187
192
|
return {
|
|
188
193
|
...baseResult("INVALID", {
|
|
@@ -197,7 +202,7 @@ export async function reconcileContinuity({ target, packageRoot } = {}) {
|
|
|
197
202
|
|
|
198
203
|
let contractFingerprint = null;
|
|
199
204
|
try {
|
|
200
|
-
const contract = await readContract(target, packageRoot);
|
|
205
|
+
const contract = await readContract(target, packageRoot, { taskId });
|
|
201
206
|
contractFingerprint = contract.fingerprint;
|
|
202
207
|
} catch {
|
|
203
208
|
contractFingerprint = null;
|
|
@@ -220,5 +225,19 @@ export async function reconcileContinuity({ target, packageRoot } = {}) {
|
|
|
220
225
|
present: true,
|
|
221
226
|
fingerprint: continuityArtifact.fingerprint,
|
|
222
227
|
continuity: continuityArtifact.value,
|
|
228
|
+
diagnosticContext: await deriveDiagnosticContextSafe({ target, packageRoot, state }),
|
|
223
229
|
};
|
|
224
230
|
}
|
|
231
|
+
|
|
232
|
+
async function deriveDiagnosticContextSafe({ target, packageRoot, state }) {
|
|
233
|
+
try {
|
|
234
|
+
const [{ readEvents }, { deriveDiagnosticContext }] = await Promise.all([
|
|
235
|
+
import("./events.js"),
|
|
236
|
+
import("./reflection.js"),
|
|
237
|
+
]);
|
|
238
|
+
const events = await readEvents(target, packageRoot, { taskId: state?.taskId ?? null });
|
|
239
|
+
return { present: true, ...deriveDiagnosticContext(events, state) };
|
|
240
|
+
} catch {
|
|
241
|
+
return { present: false };
|
|
242
|
+
}
|
|
243
|
+
}
|