@tea-agent/loop-agent 0.44.0-next.8 → 0.44.0-next.9
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/CHANGELOG.md +97 -0
- package/dist/application/evaluation/budget.js +21 -0
- package/dist/application/evaluation/corpus-hash.js +10 -15
- package/dist/application/evaluation/corpus.js +2 -1
- package/dist/application/evaluation/frontend-browser-acceptance.js +77 -0
- package/dist/application/evaluation/frontend-corpus-browser-acceptance.js +175 -0
- package/dist/application/evaluation/frontend-gateway-evidence.js +226 -0
- package/dist/application/evaluation/frontend-pair-registration.js +143 -0
- package/dist/application/evaluation/frontend-paired-summary.js +150 -0
- package/dist/application/evaluation/frontend-run-observation.js +144 -0
- package/dist/application/evaluation/frontend-shared-evidence.js +105 -0
- package/dist/application/evaluation/types.js +45 -12
- package/dist/application/task-lifecycle/observe.js +26 -1
- package/dist/build-stamp.json +3 -3
- package/dist/cli/command-definitions.js +11 -0
- package/dist/cli/program.js +4 -0
- package/dist/commands/task-advance.js +3 -5
- package/dist/commands/task-source-prepare.js +13 -1
- package/dist/executors/dag-pi/tools/design-terminal-tools.js +17 -7
- package/dist/executors/dag-pi/tools/review-terminal-tools.js +17 -7
- package/dist/executors/dag-pi-executor.js +2048 -125
- package/dist/executors/pi-executor.js +6 -1
- package/dist/executors/pi-sdk-executor.js +76 -11
- package/dist/executors/shell-executor.js +88 -12
- package/dist/infrastructure/console/operation-store.js +2 -0
- package/dist/infrastructure/evaluation/corpus-store.js +37 -12
- package/dist/shared/operator/capabilities.js +10 -5
- package/dist/task/config-types.js +22 -9
- package/dist/task/contract/project.js +13 -0
- package/dist/task/contract/schema.js +2 -8
- package/dist/task/source-prepare/parse-intent.js +124 -5
- package/dist/task/source-prepare/prepare.js +6 -1
- package/dist/task/source-prepare/semantic-intake.js +17 -2
- package/dist/task/source-prepare/source-execution.js +65 -0
- package/dist/task/source-prepare/source-fidelity-pi.js +21 -8
- package/dist/task/source-prepare/source-provider-budget.js +290 -0
- package/dist/worker/console/operator-actions.js +0 -1
- package/dist/worker/console/operator-user-error.js +1 -1
- package/dist/worker/console/prd-intake-bridge.js +5 -15
- package/dist/workflows/dag/budget-enforcement.js +31 -2
- package/dist/workflows/dag/frontend-closeout.js +3 -1
- package/dist/workflows/dag/frontend-contract-facts.js +11 -8
- package/dist/workflows/dag/frontend-design-policy.js +6 -6
- package/dist/workflows/dag/frontend-durable-tools.js +90 -70
- package/dist/workflows/dag/frontend-implementation-contract.js +146 -59
- package/dist/workflows/dag/frontend-plan-canary.js +66 -16
- package/dist/workflows/dag/frontend-plan-decision-contract.js +13 -21
- package/dist/workflows/dag/frontend-plan-progress.js +92 -0
- package/dist/workflows/dag/frontend-recovery-plan.js +11 -10
- package/dist/workflows/dag/frontend-recovery-run.js +51 -46
- package/dist/workflows/dag/frontend-repair-assertions.js +36 -0
- package/dist/workflows/dag/frontend-review-context.js +29 -1
- package/dist/workflows/dag/frontend-review-scopes.js +92 -2
- package/dist/workflows/dag/frontend-risk.js +8 -3
- package/dist/workflows/dag/frontend-root-observation.js +261 -0
- package/dist/workflows/dag/frontend-session-budget.js +52 -39
- package/dist/workflows/dag/frontend-session-context.js +10 -0
- package/dist/workflows/dag/frontend-test-execution-evidence.js +206 -48
- package/dist/workflows/dag/frontend-typed-event-store.js +11 -6
- package/dist/workflows/dag/frontend-verification-trace.js +6 -0
- package/dist/workflows/dag/frontend-writer-admission.js +2 -1
- package/dist/workflows/dag/init-hybrid.js +108 -37
- package/dist/workflows/dag/node-execution.js +11 -11
- package/dist/workflows/dag/rerun-feedback.js +35 -4
- package/dist/workflows/dag/rerun-task.js +62 -67
- package/dist/workflows/dag/runner.js +58 -12
- package/dist/workflows/dag/types.js +15 -8
- package/docs/architecture/runtime-boundaries.md +1 -1
- package/docs/templates/backend-test-dag.json +4 -4
- package/docs/templates/frontend-implementation-contract.schema.json +31 -11
- package/package.json +1 -1
- package/skills/frontend-design-review/SKILL.md +8 -11
- package/skills/frontend-design-review/references/review-checklist.md +3 -4
- package/skills/frontend-review/SKILL.md +5 -1
- package/skills/loop-agent/references/command-reference.md +8 -0
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { readFrontendCorpusBrowserAcceptance } from "./frontend-corpus-browser-acceptance.js";
|
|
2
|
+
import { readFrontendGatewayGraph } from "./frontend-gateway-evidence.js";
|
|
3
|
+
import { readFrontendSharedEvidence } from "./frontend-shared-evidence.js";
|
|
4
|
+
import { readFrontendBrowserAcceptance } from "./frontend-browser-acceptance.js";
|
|
5
|
+
import { computeCampaignPlanHash } from "./campaign-hash.js";
|
|
6
|
+
import { readCampaignPlan } from "../../infrastructure/evaluation/campaign-store.js";
|
|
7
|
+
import { realpath } from "node:fs/promises";
|
|
8
|
+
import { createHash } from "node:crypto";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { readFileBoundarySafe } from "../../shared/path-safety.js";
|
|
11
|
+
import { locateDagRun, readDagRunSpec, readDagRunState } from "../../workflows/dag/lifecycle.js";
|
|
12
|
+
import { readFrontendRootObservation } from "../../workflows/dag/frontend-root-observation.js";
|
|
13
|
+
import { verifyControllerIdentityForResume } from "../../workflows/dag/controller-identity.js";
|
|
14
|
+
import { assertFrontendPairExecution } from "./frontend-pair-registration.js";
|
|
15
|
+
import { summarizeFrontendTaskPairs } from "./frontend-paired-summary.js";
|
|
16
|
+
/** Collect run/cost facts. FINISHED is deliberately not converted to a passed
|
|
17
|
+
* product: independent acceptance and shared-identity admission are separate. */
|
|
18
|
+
export async function readFrontendTaskRunObservation(input) {
|
|
19
|
+
if (input.acceptance && input.corpusBrowser)
|
|
20
|
+
throw Error("conflicting browser acceptance evidence");
|
|
21
|
+
const root = await readFrontendRootObservation(input.repoRoot, input.runId);
|
|
22
|
+
if (!root.runIds.length)
|
|
23
|
+
throw Error("frontend observation root missing");
|
|
24
|
+
const snapshots = [];
|
|
25
|
+
let binding;
|
|
26
|
+
let sharedRegistration;
|
|
27
|
+
let registeredAcceptance;
|
|
28
|
+
let firstStatus;
|
|
29
|
+
let lastState;
|
|
30
|
+
for (const runId of root.runIds) {
|
|
31
|
+
const located = await locateDagRun(input.repoRoot, runId);
|
|
32
|
+
if (!located)
|
|
33
|
+
throw Error("frontend observation run missing");
|
|
34
|
+
const read = (name) => readFileBoundarySafe({ boundaryRoot: located.runDir,
|
|
35
|
+
targetPath: path.join(located.runDir, name), label: "frontend run observation" });
|
|
36
|
+
const snapshot = { runDir: located.runDir, state: await read("state.json"), spec: await read("run.json"), controller: await read("controller-identity.json") };
|
|
37
|
+
const state = await readDagRunState(located.runDir), spec = await readDagRunSpec(located.runDir);
|
|
38
|
+
if (await realpath(state.cwd) !== await realpath(input.repoRoot))
|
|
39
|
+
throw Error("frontend observation workspace mismatch");
|
|
40
|
+
firstStatus ??= state.status;
|
|
41
|
+
if (state.runId !== runId || !state.evaluation?.frontendPair || !spec.evaluation?.frontendPair ||
|
|
42
|
+
JSON.stringify(state.evaluation) !== JSON.stringify(spec.evaluation))
|
|
43
|
+
throw Error("frontend observation frozen evaluation mismatch");
|
|
44
|
+
if (binding && JSON.stringify(binding) !== JSON.stringify(state.evaluation))
|
|
45
|
+
throw Error("frontend observation recovery evaluation drift");
|
|
46
|
+
binding = state.evaluation;
|
|
47
|
+
const artifact = JSON.parse(snapshot.controller.toString("utf8"));
|
|
48
|
+
if (state.controllerIdentityRef?.path !== "controller-identity.json" || artifact.status !== "pinned" || !artifact.identity)
|
|
49
|
+
throw Error("frontend observation controller not pinned");
|
|
50
|
+
const pinned = await verifyControllerIdentityForResume({ runDir: located.runDir, state, currentIdentity: artifact.identity });
|
|
51
|
+
const registration = await assertFrontendPairExecution({ repoRoot: input.repoRoot, binding,
|
|
52
|
+
controllerFingerprint: pinned.identity.packageFingerprint.value, startedAt: state.startedAt });
|
|
53
|
+
sharedRegistration = registration;
|
|
54
|
+
registeredAcceptance = registration.shared.acceptance;
|
|
55
|
+
snapshots.push(snapshot);
|
|
56
|
+
lastState = state;
|
|
57
|
+
}
|
|
58
|
+
let acceptance;
|
|
59
|
+
if (input.acceptance) {
|
|
60
|
+
if (input.acceptance.contract.sha256 !== registeredAcceptance)
|
|
61
|
+
throw Error("browser acceptance contract not registered for run");
|
|
62
|
+
acceptance = await readFrontendBrowserAcceptance({ repoRoot: input.repoRoot, runDir: snapshots.at(-1).runDir, ...input.acceptance });
|
|
63
|
+
}
|
|
64
|
+
let corpusBrowserEvidence;
|
|
65
|
+
if (input.corpusBrowser) {
|
|
66
|
+
if (input.corpusBrowser.contract.sha256 !== registeredAcceptance)
|
|
67
|
+
throw Error("corpus browser contract not registered for run");
|
|
68
|
+
corpusBrowserEvidence = await readFrontendCorpusBrowserAcceptance({ repoRoot: input.repoRoot, runDir: snapshots.at(-1).runDir, ...input.corpusBrowser });
|
|
69
|
+
}
|
|
70
|
+
// A failed required browser behavior disproves delivery. Passing browser
|
|
71
|
+
// checks alone does not prove the remaining product quality gates.
|
|
72
|
+
const productVerdict = corpusBrowserEvidence?.browserPassed === false || acceptance?.passed === false ? false : null;
|
|
73
|
+
const sharedEvidence = sharedRegistration?.sharedArtifacts ? await readFrontendSharedEvidence({
|
|
74
|
+
repoRoot: input.repoRoot, runDirs: snapshots.map(snapshot => snapshot.runDir),
|
|
75
|
+
artifacts: sharedRegistration.sharedArtifacts, expected: sharedRegistration.shared,
|
|
76
|
+
}) : undefined;
|
|
77
|
+
let gatewayObservation;
|
|
78
|
+
if (input.gatewaySnapshots) {
|
|
79
|
+
if (!sharedRegistration?.gatewayTopology)
|
|
80
|
+
throw Error("frontend run gateway topology not registered");
|
|
81
|
+
const graph = await readFrontendGatewayGraph({ repoRoot: input.repoRoot,
|
|
82
|
+
topology: sharedRegistration.gatewayTopology, expectedPlanHash: sharedRegistration.planHash,
|
|
83
|
+
evidence: input.gatewaySnapshots });
|
|
84
|
+
const providerAttempts = graph.providerAttempts.filter(row => row.originCellId === binding.frontendPair.cellId);
|
|
85
|
+
if (providerAttempts.some(row => row.originTaskRef !== binding.taskRef))
|
|
86
|
+
throw Error("frontend gateway task binding mismatch");
|
|
87
|
+
gatewayObservation = {
|
|
88
|
+
evidenceKind: "registered-cell-gateway-observations",
|
|
89
|
+
cellId: binding.frontendPair.cellId, taskRef: binding.taskRef,
|
|
90
|
+
topologySha256: graph.topologySha256, snapshots: input.gatewaySnapshots,
|
|
91
|
+
observedProviderAttempts: providerAttempts.length,
|
|
92
|
+
reportedProviderTokens: graph.diagnostics.length || providerAttempts.some(row => row.totalTokens === null)
|
|
93
|
+
? null : providerAttempts.reduce((sum, row) => sum + row.totalTokens, 0),
|
|
94
|
+
providerAttempts,
|
|
95
|
+
// Graph-wide unresolved hops remain conservative even for another cell.
|
|
96
|
+
diagnostics: [...graph.diagnostics, "gateway-run-coverage-not-proven"],
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
for (const snapshot of snapshots)
|
|
100
|
+
for (const [file, expected] of [["state.json", snapshot.state], ["run.json", snapshot.spec], ["controller-identity.json", snapshot.controller]]) {
|
|
101
|
+
const actual = await readFileBoundarySafe({ boundaryRoot: snapshot.runDir, targetPath: path.join(snapshot.runDir, file), label: "frontend observation recheck" });
|
|
102
|
+
if (!actual.equals(expected))
|
|
103
|
+
throw Error("frontend observation changed during collection");
|
|
104
|
+
}
|
|
105
|
+
const recheckedRoot = await readFrontendRootObservation(input.repoRoot, input.runId);
|
|
106
|
+
if (JSON.stringify(recheckedRoot) !== JSON.stringify(root))
|
|
107
|
+
throw Error("frontend root observations changed during collection");
|
|
108
|
+
const knownFailure = root.complete && ["failed", "partial_failed"].includes(lastState.status);
|
|
109
|
+
const consumed = root.cumulativeBudget?.consumed;
|
|
110
|
+
// Reservations precede dispatch and can survive a failed dispatch. The root
|
|
111
|
+
// timer starts at DAG allocation, after task/source preparation. Keep these
|
|
112
|
+
// useful facts, but do not relabel them as task-wide request/time metrics.
|
|
113
|
+
const observedBudget = {
|
|
114
|
+
reservedRequests: root.complete ? consumed?.providerRequests ?? null : null,
|
|
115
|
+
cumulativeTokens: root.complete && consumed?.missingTokenNodeIds.length === 0 ? consumed.tokens : null,
|
|
116
|
+
dagWallClockMs: root.dagWallClockMs,
|
|
117
|
+
};
|
|
118
|
+
const metricDiagnostics = ["task-provider-request-count-not-observed", "task-wallclock-not-observed"];
|
|
119
|
+
if (!root.sourceUsage?.complete)
|
|
120
|
+
metricDiagnostics.push("task-source-usage-not-observed");
|
|
121
|
+
const observation = {
|
|
122
|
+
cellId: binding.frontendPair.cellId, bundleHash: binding.bundleHash, runId: root.runIds[0],
|
|
123
|
+
firstPassed: root.complete && root.runIds.length === 1 ? (knownFailure ? false : productVerdict) : root.complete && ["failed", "partial_failed"].includes(firstStatus) ? false : null,
|
|
124
|
+
finalPassed: root.complete ? (knownFailure ? false : productVerdict) : null,
|
|
125
|
+
requests: null,
|
|
126
|
+
tokens: root.sourceUsage?.complete ? observedBudget.cumulativeTokens : null,
|
|
127
|
+
wallClockMs: null,
|
|
128
|
+
};
|
|
129
|
+
return { campaignId: binding.campaignId, observation, outcome: root.complete ? lastState.status : null,
|
|
130
|
+
diagnostics: [...root.diagnostics, ...metricDiagnostics, ...(gatewayObservation?.diagnostics ?? ["gateway-snapshots-not-supplied"]), ...(corpusBrowserEvidence?.diagnostics ?? acceptance?.diagnostics ?? ["independent-acceptance-pending"]), ...(sharedEvidence?.diagnostics ?? ["shared-identity-admission-pending"])],
|
|
131
|
+
acceptance, corpusBrowserEvidence, sharedEvidence, observedBudget, gatewayObservation,
|
|
132
|
+
runs: snapshots.map((snapshot, index) => ({ runId: root.runIds[index], stateSha256: createHash("sha256").update(snapshot.state).digest("hex"), runSha256: createHash("sha256").update(snapshot.spec).digest("hex") })) };
|
|
133
|
+
}
|
|
134
|
+
export async function collectFrontendTaskPairSummary(input) {
|
|
135
|
+
const stored = await readCampaignPlan(input.repoRoot, input.plan.campaignId);
|
|
136
|
+
if (!stored || input.plan.planHash !== stored.planHash || computeCampaignPlanHash(input.plan.campaignId, input.plan.cells) !== stored.planHash)
|
|
137
|
+
throw Error("frontend summary registered plan mismatch");
|
|
138
|
+
const collected = [];
|
|
139
|
+
for (const runId of input.runIds)
|
|
140
|
+
collected.push(await readFrontendTaskRunObservation({ repoRoot: input.repoRoot, runId, corpusBrowser: input.corpusBrowserByRun?.[runId], gatewaySnapshots: input.gatewaySnapshotsByRun?.[runId], acceptance: input.acceptanceByRun?.[runId] }));
|
|
141
|
+
if (collected.some(row => row.campaignId !== input.plan.campaignId))
|
|
142
|
+
throw Error("frontend summary campaign mismatch");
|
|
143
|
+
return { summary: summarizeFrontendTaskPairs({ ...input, observations: collected.map(row => row.observation) }), collected };
|
|
144
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { readFileBoundarySafe } from "../../shared/path-safety.js";
|
|
5
|
+
import { sha256OfCanonicalJson } from "../../task/contract/hash.js";
|
|
6
|
+
import { readDagRunSpec } from "../../workflows/dag/lifecycle.js";
|
|
7
|
+
import { readFrontendSessionBudgetReceipts } from "../../workflows/dag/frontend-session-budget.js";
|
|
8
|
+
import { verifyFrozenSourceUsage } from "../../task/source-prepare/source-provider-budget.js";
|
|
9
|
+
import { frontendSharedArtifactsSchema } from "./frontend-pair-registration.js";
|
|
10
|
+
export async function readFrontendSharedEvidence(input) {
|
|
11
|
+
if (!input.runDirs.length)
|
|
12
|
+
throw Error("shared evidence requires a root run");
|
|
13
|
+
const artifacts = frontendSharedArtifactsSchema.parse(input.artifacts);
|
|
14
|
+
const snapshots = [];
|
|
15
|
+
const read = async (root, name, expected) => {
|
|
16
|
+
const target = path.resolve(root, name);
|
|
17
|
+
const bytes = await readFileBoundarySafe({ boundaryRoot: root, targetPath: target, label: "frontend shared evidence" });
|
|
18
|
+
if (expected && createHash("sha256").update(bytes).digest("hex") !== expected)
|
|
19
|
+
throw Error(`shared artifact hash mismatch: ${name}`);
|
|
20
|
+
snapshots.push({ root, target, bytes });
|
|
21
|
+
return JSON.parse(bytes.toString("utf8"));
|
|
22
|
+
};
|
|
23
|
+
const values = {};
|
|
24
|
+
for (const key of ["source", "budget", "model", "initialTree"]) {
|
|
25
|
+
if (artifacts[key].sha256 !== input.expected[key])
|
|
26
|
+
throw Error(`shared artifact not registered: ${key}`);
|
|
27
|
+
values[key] = await read(input.repoRoot, artifacts[key].path, artifacts[key].sha256);
|
|
28
|
+
}
|
|
29
|
+
const model = z.object({ model: z.string().min(1), parameters: z.record(z.unknown()).optional() }).strict().parse(values.model);
|
|
30
|
+
const diagnostics = [];
|
|
31
|
+
if (model.parameters && Object.keys(model.parameters).length)
|
|
32
|
+
diagnostics.push("model-sampling-parameters-not-observed");
|
|
33
|
+
const expectedTree = z.object({ gitHeadSha: z.string().regex(/^[a-f0-9]{40,64}$/), changedPaths: z.array(z.unknown()), stableGovernanceInputs: z.array(z.unknown()) }).strict().parse(values.initialTree);
|
|
34
|
+
const checkpoint = await read(input.runDirs[0], ".runtime/workspace-checkpoint-start.json");
|
|
35
|
+
const actualTree = { gitHeadSha: checkpoint.gitHeadSha, changedPaths: checkpoint.changedPaths, stableGovernanceInputs: checkpoint.stableGovernanceInputs ?? [] };
|
|
36
|
+
if (sha256OfCanonicalJson(actualTree) !== sha256OfCanonicalJson(expectedTree))
|
|
37
|
+
throw Error("shared initial tree mismatch");
|
|
38
|
+
let attempts = 0;
|
|
39
|
+
const inspected = new Map();
|
|
40
|
+
const inspect = (receipt) => {
|
|
41
|
+
if (receipt.identity.executionKind !== "model")
|
|
42
|
+
return;
|
|
43
|
+
const key = JSON.stringify([receipt.identity.runId ?? receipt.identity.taskId, receipt.identity.sessionId]);
|
|
44
|
+
const contentHash = sha256OfCanonicalJson(receipt);
|
|
45
|
+
if (inspected.has(key)) {
|
|
46
|
+
if (inspected.get(key) !== contentHash)
|
|
47
|
+
throw Error("conflicting shared model receipt");
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
inspected.set(key, contentHash);
|
|
51
|
+
if (receipt.completion !== "complete" || !receipt.attempts.length || !receipt.planned?.capabilities.model)
|
|
52
|
+
diagnostics.push("shared model observation incomplete");
|
|
53
|
+
if (receipt.planned?.capabilities.model && receipt.planned.capabilities.model !== model.model)
|
|
54
|
+
throw Error("shared planned model mismatch");
|
|
55
|
+
for (const attempt of receipt.attempts) {
|
|
56
|
+
if (attempt.model !== model.model)
|
|
57
|
+
throw Error("shared attempt model mismatch");
|
|
58
|
+
if (attempt.completion !== "complete")
|
|
59
|
+
diagnostics.push("shared model observation incomplete");
|
|
60
|
+
attempts++;
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
const sourceReceipts = new Set();
|
|
64
|
+
for (const runDir of input.runDirs) {
|
|
65
|
+
await read(runDir, "run.json");
|
|
66
|
+
const spec = await readDagRunSpec(runDir);
|
|
67
|
+
if (sha256OfCanonicalJson(spec.sourceBinding ?? null) !== sha256OfCanonicalJson(values.source))
|
|
68
|
+
throw Error("shared source mismatch");
|
|
69
|
+
if (sha256OfCanonicalJson(spec.budget ?? null) !== sha256OfCanonicalJson(values.budget))
|
|
70
|
+
throw Error("shared budget mismatch");
|
|
71
|
+
const state = await read(runDir, "state.json");
|
|
72
|
+
for (const task of spec.tasks) {
|
|
73
|
+
const receipts = await readFrontendSessionBudgetReceipts(runDir, task.id);
|
|
74
|
+
const node = state.nodes?.[task.id];
|
|
75
|
+
if (task.executor === "pi" && node && !["PENDING", "SKIPPED"].includes(node.status) && node.origin?.kind !== "imported" && !receipts.length)
|
|
76
|
+
diagnostics.push(`missing model receipts: ${task.id}`);
|
|
77
|
+
for (const receipt of receipts)
|
|
78
|
+
inspect(receipt);
|
|
79
|
+
}
|
|
80
|
+
if (spec.sourceProviderBudget) {
|
|
81
|
+
const verified = await verifyFrozenSourceUsage(input.repoRoot, spec.sourceProviderBudget);
|
|
82
|
+
diagnostics.push(...verified.diagnostics);
|
|
83
|
+
for (const source of spec.sourceProviderBudget.evidence?.receipts ?? []) {
|
|
84
|
+
if (sourceReceipts.has(source.sha256))
|
|
85
|
+
continue;
|
|
86
|
+
sourceReceipts.add(source.sha256);
|
|
87
|
+
inspect(JSON.parse(source.content));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (!attempts)
|
|
92
|
+
diagnostics.push("shared model has no observed attempts");
|
|
93
|
+
// Sampling settings are not yet present in session receipts; matching model
|
|
94
|
+
// names alone cannot prove every parameter used by the SDK/provider.
|
|
95
|
+
if (!diagnostics.includes("model-sampling-parameters-not-observed"))
|
|
96
|
+
diagnostics.push("model-sampling-parameters-not-observed");
|
|
97
|
+
diagnostics.push("provider-resolved-model-not-observed");
|
|
98
|
+
for (const snapshot of snapshots) {
|
|
99
|
+
const latest = await readFileBoundarySafe({ boundaryRoot: snapshot.root, targetPath: snapshot.target, label: "shared evidence recheck" });
|
|
100
|
+
if (!latest.equals(snapshot.bytes))
|
|
101
|
+
throw Error("shared evidence changed during collection");
|
|
102
|
+
}
|
|
103
|
+
return { diagnostics: [...new Set(diagnostics)], model: model.model, observedAttempts: attempts,
|
|
104
|
+
verified: { source: true, budget: true, initialTree: true, modelReference: attempts > 0 && diagnostics.every(item => ["model-sampling-parameters-not-observed", "provider-resolved-model-not-observed"].includes(item)), samplingParameters: false } };
|
|
105
|
+
}
|
|
@@ -265,18 +265,51 @@ const corpusManifestObjectSchema = z
|
|
|
265
265
|
.optional(),
|
|
266
266
|
})
|
|
267
267
|
.strict();
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
})
|
|
278
|
-
|
|
279
|
-
|
|
268
|
+
// The six-case exploratory frontend protocol is distinct from the heterogeneous
|
|
269
|
+
// dogfood corpus. All six are public execution tasks, not held-out claims.
|
|
270
|
+
const frontendCorpusCategories = [
|
|
271
|
+
"local-interaction", "form-validation", "component-reuse",
|
|
272
|
+
"table-filter-pagination", "multi-api-mock-boundary", "shared-context-domains",
|
|
273
|
+
];
|
|
274
|
+
const frontendCorpusFileSchema = z.object({
|
|
275
|
+
path: z.string().min(1).refine(value => !value.includes("\\") && !value.includes(":") &&
|
|
276
|
+
!value.startsWith("/") && value.split("/").every(part => part !== "" && part !== "." && part !== ".."), "corpus artifact must be a normalized relative file path"),
|
|
277
|
+
sha256: z.string().regex(/^[a-f0-9]{64}$/),
|
|
278
|
+
}).strict();
|
|
279
|
+
const frontendCorpusFilesSchema = z.array(frontendCorpusFileSchema).min(1).superRefine((files, ctx) => {
|
|
280
|
+
if (new Set(files.map(file => file.path)).size !== files.length)
|
|
281
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "duplicate corpus artifact path" });
|
|
282
|
+
});
|
|
283
|
+
const frontendCorpusTaskSchema = corpusTaskSchema.extend({
|
|
284
|
+
class: z.literal("frontend"), split: z.literal("public"),
|
|
285
|
+
category: z.enum(frontendCorpusCategories),
|
|
286
|
+
requirements: frontendCorpusFilesSchema,
|
|
287
|
+
initialFiles: frontendCorpusFilesSchema,
|
|
288
|
+
oracleFiles: frontendCorpusFilesSchema,
|
|
289
|
+
}).superRefine((task, ctx) => {
|
|
290
|
+
if (task.seeds.length !== 3)
|
|
291
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "frontend corpus requires three planned repetitions" });
|
|
292
|
+
});
|
|
293
|
+
const frontendCorpusObjectSchema = z.object({
|
|
294
|
+
schemaVersion: z.literal(2), purpose: z.literal("frontend-task-pairs"),
|
|
295
|
+
corpusId: corpusIdSchema, createdAt: z.string().datetime(), description: z.string().optional(),
|
|
296
|
+
tasks: z.array(frontendCorpusTaskSchema).length(6),
|
|
297
|
+
corpusHash: z.string().regex(/^(sha256:)?[a-f0-9]{64}$/i).optional(),
|
|
298
|
+
}).strict();
|
|
299
|
+
function refineFrontendCorpus(manifest, ctx) {
|
|
300
|
+
if (new Set(manifest.tasks.map(task => task.category)).size !== frontendCorpusCategories.length)
|
|
301
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "frontend corpus requires all six categories exactly once" });
|
|
302
|
+
if (new Set(manifest.tasks.map(task => task.taskRef)).size !== manifest.tasks.length)
|
|
303
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "duplicate frontend corpus taskRef" });
|
|
304
|
+
}
|
|
305
|
+
export const corpusManifestInputSchema = z.union([
|
|
306
|
+
corpusManifestObjectSchema.superRefine(refineCorpusTasks),
|
|
307
|
+
frontendCorpusObjectSchema.superRefine(refineFrontendCorpus),
|
|
308
|
+
]);
|
|
309
|
+
export const corpusManifestSchema = z.union([
|
|
310
|
+
corpusManifestObjectSchema.extend({ corpusHash: z.string().regex(/^sha256:[a-f0-9]{64}$/) }).superRefine(refineCorpusTasks),
|
|
311
|
+
frontendCorpusObjectSchema.extend({ corpusHash: z.string().regex(/^sha256:[a-f0-9]{64}$/) }).superRefine(refineFrontendCorpus),
|
|
312
|
+
]);
|
|
280
313
|
// --- Private Verifier + Campaign (M4) ---
|
|
281
314
|
export const privateVerifierIdSchema = z
|
|
282
315
|
.string()
|
|
@@ -198,6 +198,10 @@ export async function listExplicitlyAssociatedRuns(repoRoot, taskId) {
|
|
|
198
198
|
const recorded = new Map((record?.runAssociations ?? []).map((entry) => [entry.runId, entry.source]));
|
|
199
199
|
const entries = await listAllDagRunEntries(repoRoot);
|
|
200
200
|
const related = [];
|
|
201
|
+
// Recency key per run. `runId` is NOT chronological (a later recovery run can
|
|
202
|
+
// sort before an earlier one lexicographically, e.g. `dag-…` vs `2026…`), so
|
|
203
|
+
// ordering by runId alone reports a superseded run as the latest.
|
|
204
|
+
const startedAtByRunId = new Map();
|
|
201
205
|
for (const entry of entries) {
|
|
202
206
|
let source = recorded.get(entry.runId) ?? null;
|
|
203
207
|
let stateStatus;
|
|
@@ -207,6 +211,9 @@ export async function listExplicitlyAssociatedRuns(repoRoot, taskId) {
|
|
|
207
211
|
try {
|
|
208
212
|
const state = await readDagRunState(entry.runDir);
|
|
209
213
|
stateStatus = state.status;
|
|
214
|
+
if (typeof state.startedAt === "string") {
|
|
215
|
+
startedAtByRunId.set(entry.runId, state.startedAt);
|
|
216
|
+
}
|
|
210
217
|
const liveness = assessDagRunLiveness({ state });
|
|
211
218
|
const activeNode = Object.values(state.nodes ?? {}).find((node) => node.status === "RUNNING");
|
|
212
219
|
lastMeaningfulProgressAt =
|
|
@@ -266,7 +273,25 @@ export async function listExplicitlyAssociatedRuns(repoRoot, taskId) {
|
|
|
266
273
|
contractBinding,
|
|
267
274
|
});
|
|
268
275
|
}
|
|
269
|
-
|
|
276
|
+
// Chronological order by run start time; runs without a readable start time
|
|
277
|
+
// sort first so a run with real timing is never displaced by an unknown.
|
|
278
|
+
// runId breaks ties deterministically.
|
|
279
|
+
return related.sort((a, b) => {
|
|
280
|
+
const aStarted = startedAtByRunId.get(a.runId);
|
|
281
|
+
const bStarted = startedAtByRunId.get(b.runId);
|
|
282
|
+
if (aStarted !== bStarted) {
|
|
283
|
+
if (aStarted === undefined)
|
|
284
|
+
return -1;
|
|
285
|
+
if (bStarted === undefined)
|
|
286
|
+
return 1;
|
|
287
|
+
const aTime = Date.parse(aStarted);
|
|
288
|
+
const bTime = Date.parse(bStarted);
|
|
289
|
+
if (Number.isFinite(aTime) && Number.isFinite(bTime) && aTime !== bTime) {
|
|
290
|
+
return aTime - bTime;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return a.runId.localeCompare(b.runId);
|
|
294
|
+
});
|
|
270
295
|
}
|
|
271
296
|
function deriveLifecycleState(input) {
|
|
272
297
|
if (!input.exists)
|
package/dist/build-stamp.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"version": "0.44.0-next.
|
|
4
|
-
"gitSha": "
|
|
5
|
-
"builtAt": "2026-09-
|
|
3
|
+
"version": "0.44.0-next.9",
|
|
4
|
+
"gitSha": "e4db199463a7b550af890295b630d91660e4239f",
|
|
5
|
+
"builtAt": "2026-09-17T01:09:20.426Z"
|
|
6
6
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { runDoctor } from "../commands/doctor.js";
|
|
2
|
+
import { runPromoteRun } from "../commands/promote-run.js";
|
|
2
3
|
import { runDocsArchive } from "../commands/docs-archive.js";
|
|
3
4
|
import { runDocsAudit } from "../commands/docs-audit.js";
|
|
4
5
|
import { runEval } from "../commands/eval.js";
|
|
@@ -197,6 +198,16 @@ export const COMMAND_DEFINITIONS = [
|
|
|
197
198
|
await runDoctor(adapter, repoRoot, rest.filter((arg) => Boolean(arg)));
|
|
198
199
|
},
|
|
199
200
|
},
|
|
201
|
+
{
|
|
202
|
+
name: "promote-run",
|
|
203
|
+
adapter: "required",
|
|
204
|
+
tier: "operator",
|
|
205
|
+
intent: "Promote deterministic evidence from a completed DAG or one-shot run into task artifacts (修改记录.md, 验证结果.md).",
|
|
206
|
+
usage: "promote-run <task-id> --run-id <run-id>",
|
|
207
|
+
handler: async ({ adapter, repoRoot, rest }) => {
|
|
208
|
+
await runPromoteRun(repoRoot, rest.filter((arg) => Boolean(arg)));
|
|
209
|
+
},
|
|
210
|
+
},
|
|
200
211
|
{
|
|
201
212
|
name: "docs",
|
|
202
213
|
adapter: "required",
|
package/dist/cli/program.js
CHANGED
|
@@ -29,6 +29,7 @@ import { runDocsArchive } from "../commands/docs-archive.js";
|
|
|
29
29
|
import { runDocsAudit } from "../commands/docs-audit.js";
|
|
30
30
|
import { runEval } from "../commands/eval.js";
|
|
31
31
|
import { runDoctor } from "../commands/doctor.js";
|
|
32
|
+
import { runPromoteRun } from "../commands/promote-run.js";
|
|
32
33
|
import { runExamples } from "../commands/examples.js";
|
|
33
34
|
import { runGoal } from "../commands/goal.js";
|
|
34
35
|
import { runHandoffCheck } from "../commands/handoff-check.js";
|
|
@@ -149,6 +150,9 @@ async function runCommanderAction(ctx, command, subcommand, rest) {
|
|
|
149
150
|
case "doctor":
|
|
150
151
|
await runDoctor(ctx.adapter, ctx.repoRoot, compactArgs([subcommand, ...rest]));
|
|
151
152
|
return;
|
|
153
|
+
case "promote-run":
|
|
154
|
+
await runPromoteRun(ctx.repoRoot, compactArgs([subcommand, ...rest]));
|
|
155
|
+
return;
|
|
152
156
|
case "docs":
|
|
153
157
|
await runDocsCommand(ctx.repoRoot, subcommand, rest);
|
|
154
158
|
return;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { capabilityBoundarySchema } from "../task/config-types.js";
|
|
1
2
|
import { advanceTaskLifecycle, } from "../application/task-lifecycle/index.js";
|
|
2
3
|
import { backendTestConfigSchema, } from "../task/config-types.js";
|
|
3
4
|
import { buildOperatorResult, operatorFailed, processExitCodeForOutcome, writeOperatorJson, } from "../shared/operator/index.js";
|
|
@@ -145,13 +146,10 @@ export function parseTaskAdvanceArgs(args) {
|
|
|
145
146
|
if (token === "--capability-boundary") {
|
|
146
147
|
try {
|
|
147
148
|
const parsed = JSON.parse(next());
|
|
148
|
-
|
|
149
|
-
throw new Error("not an object");
|
|
150
|
-
}
|
|
151
|
-
options.capabilityBoundary = parsed;
|
|
149
|
+
options.capabilityBoundary = capabilityBoundarySchema.parse(parsed);
|
|
152
150
|
}
|
|
153
151
|
catch (error) {
|
|
154
|
-
throw new Error(`--capability-boundary must be a JSON object {allowed?, forbidden?}: ${error.message}`);
|
|
152
|
+
throw new Error(`--capability-boundary must be a JSON object {allowed?, forbidden?, requiredClauses?}: ${error.message}`);
|
|
155
153
|
}
|
|
156
154
|
continue;
|
|
157
155
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { stdin as stdinStream } from "node:process";
|
|
4
|
-
import { parseAcceptanceFlag, parseVerifyFlag, parseVerifyTimeoutFlag, prepareTaskSource, PREPARE_MAX_FILE_BYTES, } from "../task/source-prepare/index.js";
|
|
4
|
+
import { parseAcceptanceFlag, parseVerifyFlag, parseVerifyLaneFlag, parseVerifyTimeoutFlag, prepareTaskSource, PREPARE_MAX_FILE_BYTES, } from "../task/source-prepare/index.js";
|
|
5
5
|
import { parseTaskContractDraft } from "../task/contract/schema.js";
|
|
6
6
|
import { buildOperatorResult, operatorFailed, processExitCodeForOutcome, writeOperatorJson, } from "../shared/operator/index.js";
|
|
7
7
|
const COMMAND = "task source prepare";
|
|
@@ -28,6 +28,7 @@ const USAGE = `usage:
|
|
|
28
28
|
[--open-question <text>]...
|
|
29
29
|
[--verify <label:command>]...
|
|
30
30
|
[--verify-timeout <label:timeoutMs>]...
|
|
31
|
+
[--verify-lane <label:static|behavior>]...
|
|
31
32
|
[--request-id <id>]
|
|
32
33
|
[--dry-run | --apply]
|
|
33
34
|
[--force-overwrite-source]
|
|
@@ -157,6 +158,10 @@ export function parseTaskSourcePrepareArgs(args) {
|
|
|
157
158
|
options.verifyTimeout = pushList(options.verifyTimeout, next());
|
|
158
159
|
continue;
|
|
159
160
|
}
|
|
161
|
+
if (token === "--verify-lane") {
|
|
162
|
+
options.verifyLane = pushList(options.verifyLane, next());
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
160
165
|
if (token === "--request-id") {
|
|
161
166
|
options.requestId = next();
|
|
162
167
|
continue;
|
|
@@ -287,13 +292,20 @@ function buildFlags(options) {
|
|
|
287
292
|
const parsed = parseVerifyTimeoutFlag(raw);
|
|
288
293
|
timeouts.set(parsed.label, parsed.timeoutMs);
|
|
289
294
|
}
|
|
295
|
+
const lanes = new Map();
|
|
296
|
+
for (const raw of options.verifyLane ?? []) {
|
|
297
|
+
const parsed = parseVerifyLaneFlag(raw);
|
|
298
|
+
lanes.set(parsed.label, parsed.mode);
|
|
299
|
+
}
|
|
290
300
|
const commands = (options.verify ?? []).map((raw) => {
|
|
291
301
|
const parsed = parseVerifyFlag(raw);
|
|
292
302
|
const timeoutMs = timeouts.get(parsed.label);
|
|
303
|
+
const mode = lanes.get(parsed.label);
|
|
293
304
|
return {
|
|
294
305
|
label: parsed.label,
|
|
295
306
|
command: parsed.command,
|
|
296
307
|
...(timeoutMs !== undefined ? { timeoutMs } : {}),
|
|
308
|
+
...(mode !== undefined ? { mode } : {}),
|
|
297
309
|
};
|
|
298
310
|
});
|
|
299
311
|
// If only timeouts provided without verify, treat as empty explicit list? No — only set when --verify appears.
|
|
@@ -16,7 +16,7 @@ export async function createFrontendDesignTerminalTools(input) {
|
|
|
16
16
|
import("typebox"),
|
|
17
17
|
import("@earendil-works/pi-coding-agent"),
|
|
18
18
|
]);
|
|
19
|
-
const { approveDesignFactSchema, readCommittedEvents, requestDesignChangesFactSchema, writeTypedEventStoreJsonl, } = await import("../../../workflows/dag/frontend-typed-event-store.js");
|
|
19
|
+
const { approveDesignFactSchema, readCommittedEvents, requestDesignChangesFactSchema, reviewFindingSchema, writeTypedEventStoreJsonl, } = await import("../../../workflows/dag/frontend-typed-event-store.js");
|
|
20
20
|
const { adoptTypedEventFact, stageTypedEventFact } = await import("../../../workflows/dag/frontend-typed-event-transaction.js");
|
|
21
21
|
let store = input.store;
|
|
22
22
|
const attemptId = input.attemptId;
|
|
@@ -26,6 +26,7 @@ export async function createFrontendDesignTerminalTools(input) {
|
|
|
26
26
|
line: Type.Optional(Type.Integer({ minimum: 1 })),
|
|
27
27
|
issue: Type.String({ minLength: 1 }),
|
|
28
28
|
requiredChange: Type.Optional(Type.String({ minLength: 1 })),
|
|
29
|
+
repairAssertions: Type.Optional(Type.Unknown()),
|
|
29
30
|
}, { additionalProperties: false });
|
|
30
31
|
const scopeProtocol = createFrontendReviewScopeProtocol({ phase: "design", inventory: input.inventory, getStore: () => store, attemptId });
|
|
31
32
|
const savedFindings = () => [...new Map(readCommittedEvents(store, attemptId).filter(r => r.fact.kind === "design-finding").map(r => [r.fact.id, r.fact.finding])).values()];
|
|
@@ -35,12 +36,21 @@ export async function createFrontendDesignTerminalTools(input) {
|
|
|
35
36
|
description: "Save one finding with a stable id. Submit findings incrementally, then finalize without repeating the findings array. Saved blocking findings cannot be omitted from approval; correct a finding explicitly with replace:true.",
|
|
36
37
|
parameters: Type.Object({ id: Type.String({ minLength: 1 }), finding: findingSchema }, { additionalProperties: false }),
|
|
37
38
|
async execute(_callId, params) {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
39
|
+
try {
|
|
40
|
+
const previous = readCommittedEvents(store, attemptId).filter(r => r.fact.kind === "design-finding" && r.fact.id === params.id).at(-1);
|
|
41
|
+
const scopeIds = scopeProtocol.findingScopeIds(previous?.fact.scopeIds);
|
|
42
|
+
const fact = { kind: "design-finding", id: params.id, finding: reviewFindingSchema.parse(params.finding), ...(scopeIds ? { scopeIds } : {}) };
|
|
43
|
+
const requestId = `${attemptId}:finding:${randomUUID()}`;
|
|
44
|
+
const staged = stageTypedEventFact({ store, requestId, attemptId, fact });
|
|
45
|
+
const adopted = await adoptTypedEventFact({ store, requestId, attemptId, fact, eventId: staged.eventId, expectedRevision: store.revision });
|
|
46
|
+
const details = { ok: true, eventId: adopted.eventId, revision: adopted.revision };
|
|
47
|
+
return { content: [{ type: "text", text: JSON.stringify(details) }], details };
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
const code = error.code ?? "FRONTEND_INPUT_SCOPE_VIOLATION";
|
|
51
|
+
const details = { ok: false, code, error: error instanceof Error ? error.message : String(error) };
|
|
52
|
+
return { content: [{ type: "text", text: JSON.stringify(details) }], details };
|
|
53
|
+
}
|
|
44
54
|
},
|
|
45
55
|
});
|
|
46
56
|
const approveParameters = Type.Object({
|
|
@@ -16,7 +16,7 @@ export async function createFrontendReviewTerminalTools(input) {
|
|
|
16
16
|
import("typebox"),
|
|
17
17
|
import("@earendil-works/pi-coding-agent"),
|
|
18
18
|
]);
|
|
19
|
-
const { approveReviewFactSchema, readCommittedEvents, requestReviewChangesFactSchema, writeTypedEventStoreJsonl, } = await import("../../../workflows/dag/frontend-typed-event-store.js");
|
|
19
|
+
const { approveReviewFactSchema, readCommittedEvents, requestReviewChangesFactSchema, reviewFindingSchema, writeTypedEventStoreJsonl, } = await import("../../../workflows/dag/frontend-typed-event-store.js");
|
|
20
20
|
const { adoptTypedEventFact, stageTypedEventFact } = await import("../../../workflows/dag/frontend-typed-event-transaction.js");
|
|
21
21
|
let store = input.store;
|
|
22
22
|
const attemptId = input.attemptId;
|
|
@@ -26,6 +26,7 @@ export async function createFrontendReviewTerminalTools(input) {
|
|
|
26
26
|
line: Type.Optional(Type.Integer({ minimum: 1 })),
|
|
27
27
|
issue: Type.String({ minLength: 1 }),
|
|
28
28
|
requiredChange: Type.Optional(Type.String({ minLength: 1 })),
|
|
29
|
+
repairAssertions: Type.Optional(Type.Unknown()),
|
|
29
30
|
}, { additionalProperties: false });
|
|
30
31
|
const scopeProtocol = createFrontendReviewScopeProtocol({ phase: "review", inventory: input.inventory, getStore: () => store, attemptId });
|
|
31
32
|
const savedFindings = () => [...new Map(readCommittedEvents(store, attemptId).filter(r => r.fact.kind === "review-finding").map(r => [r.fact.id, r.fact.finding])).values()];
|
|
@@ -35,12 +36,21 @@ export async function createFrontendReviewTerminalTools(input) {
|
|
|
35
36
|
description: "Save one finding with a stable id. Submit findings incrementally, then finalize without repeating the findings array. Saved blocking findings cannot be omitted from approval; correct a finding explicitly with replace:true.",
|
|
36
37
|
parameters: Type.Object({ id: Type.String({ minLength: 1 }), finding: findingSchema }, { additionalProperties: false }),
|
|
37
38
|
async execute(_callId, params) {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
39
|
+
try {
|
|
40
|
+
const previous = readCommittedEvents(store, attemptId).filter(r => r.fact.kind === "review-finding" && r.fact.id === params.id).at(-1);
|
|
41
|
+
const scopeIds = scopeProtocol.findingScopeIds(previous?.fact.scopeIds);
|
|
42
|
+
const fact = { kind: "review-finding", id: params.id, finding: reviewFindingSchema.parse(params.finding), ...(scopeIds ? { scopeIds } : {}) };
|
|
43
|
+
const requestId = `${attemptId}:finding:${randomUUID()}`;
|
|
44
|
+
const staged = stageTypedEventFact({ store, requestId, attemptId, fact });
|
|
45
|
+
const adopted = await adoptTypedEventFact({ store, requestId, attemptId, fact, eventId: staged.eventId, expectedRevision: store.revision });
|
|
46
|
+
const details = { ok: true, eventId: adopted.eventId, revision: adopted.revision };
|
|
47
|
+
return { content: [{ type: "text", text: JSON.stringify(details) }], details };
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
const code = error.code ?? "FRONTEND_INPUT_SCOPE_VIOLATION";
|
|
51
|
+
const details = { ok: false, code, error: error instanceof Error ? error.message : String(error) };
|
|
52
|
+
return { content: [{ type: "text", text: JSON.stringify(details) }], details };
|
|
53
|
+
}
|
|
44
54
|
},
|
|
45
55
|
});
|
|
46
56
|
const approveParameters = Type.Object({
|