@tea-agent/loop-agent 0.44.0-next.10 → 0.44.0-next.11
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/dist/build-stamp.json +3 -3
- package/dist/executors/dag-pi/sessions/index.js +6 -0
- package/dist/executors/dag-pi/sessions/plan-batches.js +280 -0
- package/dist/executors/dag-pi/sessions/plan-prompts.js +367 -0
- package/dist/executors/dag-pi/sessions/scout-parallel.js +197 -0
- package/dist/executors/dag-pi/sessions/segmented-plan.js +1184 -0
- package/dist/executors/dag-pi/sessions/writer-evidence.js +60 -0
- package/dist/executors/dag-pi-executor.js +12 -2075
- package/dist/workflows/dag/checkpoint.js +686 -0
- package/dist/workflows/dag/hybrid/sources.js +1812 -0
- package/dist/workflows/dag/hybrid/templates/backend-test.js +1807 -0
- package/dist/workflows/dag/hybrid/templates/frontend-test.js +702 -0
- package/dist/workflows/dag/hybrid/templates/frontend.js +1529 -0
- package/dist/workflows/dag/hybrid/templates/index.js +8 -0
- package/dist/workflows/dag/hybrid/templates/kg-bootstrap.js +449 -0
- package/dist/workflows/dag/hybrid/templates/knowledge-sync.js +495 -0
- package/dist/workflows/dag/hybrid/templates/shared.js +101 -0
- package/dist/workflows/dag/hybrid/templates/standard.js +265 -0
- package/dist/workflows/dag/hybrid/types.js +32 -0
- package/dist/workflows/dag/init-hybrid.js +42 -7162
- package/dist/workflows/dag/runner.js +11 -1178
- package/dist/workflows/dag/terminal-status.js +502 -0
- package/docs/architecture/dag-execution.md +1 -1
- package/package.json +1 -1
|
@@ -1,20 +1,17 @@
|
|
|
1
|
-
import { classifyFrontendPlanRecovery } from "../workflows/dag/frontend-plan-recovery-policy.js";
|
|
2
1
|
import { collectFrontendExecutionGroups } from "../workflows/dag/frontend-execution-groups.js";
|
|
3
|
-
import { FRONTEND_SCOPE_TARGET_BYTES,
|
|
2
|
+
import { FRONTEND_SCOPE_TARGET_BYTES, parseFrontendInputBlock } from "../workflows/dag/frontend-input-projection.js";
|
|
4
3
|
import { createDurableFrontendTools } from "../workflows/dag/frontend-durable-tools.js";
|
|
5
4
|
import { sha256OfCanonicalJson } from "../task/contract/hash.js";
|
|
6
|
-
import { z } from "zod";
|
|
7
5
|
import { loadFrontendReviewScopes } from "../workflows/dag/frontend-review-scopes.js";
|
|
8
6
|
import { observeFrontendFinalization, observeFrontendSession } from "../workflows/dag/frontend-session-budget.js";
|
|
9
7
|
import path from "node:path";
|
|
10
8
|
import { createHash } from "node:crypto";
|
|
11
|
-
import { readFile
|
|
9
|
+
import { readFile } from "node:fs/promises";
|
|
12
10
|
import { writeDagNodeJsonArtifact, writeTextArtifactFile, } from "../infrastructure/harness/artifact-store.js";
|
|
13
11
|
import { executePiStep, resolvePiBackend, } from "./pi-executor.js";
|
|
14
12
|
import { resolveDagPiExtensions, } from "./pi-extension-resolver.js";
|
|
15
13
|
import { buildPiWriterToolPolicyContext, createPiReaderCustomTools, createPiWriterCustomTools, } from "./pi-writer-tool-policy.js";
|
|
16
14
|
import { resolveDagPiModelConfig } from "./dag-pi/model-config.js";
|
|
17
|
-
import { isRecordObject } from "./dag-pi/guards.js";
|
|
18
15
|
import { createFrontendReviewTerminalTools } from "./dag-pi/tools/review-terminal-tools.js";
|
|
19
16
|
import { createFrontendDesignTerminalTools } from "./dag-pi/tools/design-terminal-tools.js";
|
|
20
17
|
import { createFrontendScoutEvidenceTools } from "./dag-pi/tools/scout-evidence-tools.js";
|
|
@@ -26,7 +23,7 @@ import { buildPlanSchemas } from "./dag-pi/plan/schema.js";
|
|
|
26
23
|
import { buildPlanRecordTools } from "./dag-pi/plan/record-tools.js";
|
|
27
24
|
import { buildPlanFinalizeTools } from "./dag-pi/plan/finalize-tools.js";
|
|
28
25
|
import { createPlanFactAdopter } from "./dag-pi/plan/fact-adoption.js";
|
|
29
|
-
import {
|
|
26
|
+
import { planFactStringList } from "./dag-pi/plan/facts.js";
|
|
30
27
|
import { createPiReadBudgetCustomTools, } from "./pi-read-budget-policy.js";
|
|
31
28
|
import { cleanupPlaywrightCliDefaultSession, createPlaywrightCliTool, PI_COMMAND_CAPABILITY_REGISTRY, resolveCaseIdFromWriteSet, resolveEvidenceDirFromWriteSet, } from "./pi-playwright-cli-tool.js";
|
|
32
29
|
import { dagCommandPolicyAllows, resolveDagCommandPolicy, } from "../workflows/dag/types.js";
|
|
@@ -44,16 +41,17 @@ import { assessBackendTestPlanProtocol } from "../workflows/dag/backend-test-pla
|
|
|
44
41
|
import { frontendTestLayoutFromSpec } from "../workflows/dag/frontend-test-layout.js";
|
|
45
42
|
import { redactSecrets, truncateUtf8Preview } from "../shared/preview.js";
|
|
46
43
|
import { writeEffectiveContextReceipt } from "../workflows/dag/context-receipt.js";
|
|
44
|
+
import { estimateFrontendPlanRequirementRecordCalls, isWriterThinkingExhausted, readWriterThinkingExhaustionEvidence, WRITER_BUDGET_EXHAUSTED_CATEGORY, WRITER_TOKEN_BUDGET, FRONTEND_PLAN_COVERAGE_MAX_CONCURRENCY, mapWithConcurrency, buildFrontendPlanWorkload, loadOrCreateFrontendPlanCoverageLayout, collectFrontendVerificationCommandFiles, aggregateParallelPiResults, combineSequentialPiResults, runFrontendScoutParallelSessions, runFrontendContractSegmentedSessions, runFrontendPlanSegmentedSessions, runFrontendReviewSegmentedSessions, runFrontendScoutSegmentedSessions, } from "./dag-pi/sessions/index.js";
|
|
45
|
+
// 兼容既有测试导入路径:纯规划/投影符号经 entry 转发(逐步迁移后移除)。
|
|
46
|
+
export { estimateFrontendPlanRequirementRecordCalls, batchFrontendPlanRequirements, mapWithConcurrency, buildFrontendPlanWorkload, collectFrontendPlanRequirementIds, loadOrCreateFrontendPlanCoverageLayout, collectFrontendVerificationCommandFiles, compactFrontendPlanLedgerContext, compactFrontendPlanPromptForRequirementSlice, aggregateParallelPiResults, combineSequentialPiResults, runFrontendScoutParallelSessions, runFrontendContractSegmentedSessions, runFrontendPlanSegmentedSessions, runFrontendReviewSegmentedSessions, runFrontendScoutSegmentedSessions } from "./dag-pi/sessions/index.js";
|
|
47
47
|
/**
|
|
48
48
|
* Legacy diagnostic label retained for artifact compatibility. New executions
|
|
49
49
|
* classify this signal as output-limit so the node can retry incrementally.
|
|
50
50
|
*/
|
|
51
|
-
export const WRITER_THINKING_EXHAUSTED_CATEGORY = "writer-thinking-exhausted";
|
|
52
51
|
/**
|
|
53
52
|
* Legacy diagnostic label retained for artifact compatibility. New executions
|
|
54
53
|
* classify this signal as output-limit and preserve committed typed facts.
|
|
55
54
|
*/
|
|
56
|
-
export const PLANNER_THINKING_EXHAUSTED_CATEGORY = "planner-thinking-exhausted";
|
|
57
55
|
/**
|
|
58
56
|
* Parallel coverage shards namespace their verification target ids with
|
|
59
57
|
* `VT-SHARD-<shard>-` so the reducer can detect cross-shard conflicts. Once
|
|
@@ -260,34 +258,13 @@ export function normalizeParallelCoverageShardRecords(records, shardNumber) {
|
|
|
260
258
|
: record;
|
|
261
259
|
});
|
|
262
260
|
}
|
|
263
|
-
export function isPlannerThinkingExhausted(result, committedAnyFacts) {
|
|
264
|
-
if (result.ok)
|
|
265
|
-
return false;
|
|
266
|
-
const evidence = readWriterThinkingExhaustionEvidence(result);
|
|
267
|
-
if (evidence.stopReason !== "length")
|
|
268
|
-
return false;
|
|
269
|
-
if (evidence.thinkingObserved !== true)
|
|
270
|
-
return false;
|
|
271
|
-
if (committedAnyFacts)
|
|
272
|
-
return false;
|
|
273
|
-
if ((result.assistantText ?? "").trim())
|
|
274
|
-
return false;
|
|
275
|
-
// Gateways sometimes relabel a length-stopped stream as `network` or
|
|
276
|
-
// `nonzero-exit`; provider evidence outweighs the transport label.
|
|
277
|
-
if (result.failureCategory &&
|
|
278
|
-
!["empty-output", "network", "nonzero-exit", "unknown"].includes(result.failureCategory))
|
|
279
|
-
return false;
|
|
280
|
-
return true;
|
|
281
|
-
}
|
|
282
261
|
/**
|
|
283
262
|
* The writer session burned an excessive token budget (a read-edit-test loop
|
|
284
263
|
* that never converged) and still failed. Distinct from empty-output so the
|
|
285
264
|
* report shows the real cause and recovery recommends a fresh compacted run.
|
|
286
265
|
* Not auto-retried by default; operators may rerun after a model switch.
|
|
287
266
|
*/
|
|
288
|
-
export const WRITER_BUDGET_EXHAUSTED_CATEGORY = "writer-budget-exhausted";
|
|
289
267
|
/** Token ceiling for a single writer node before it is judged budget-exhausted. */
|
|
290
|
-
export const WRITER_TOKEN_BUDGET = 2_000_000;
|
|
291
268
|
export function allowsMissingChangedWriterOutcomeRecovery(task) {
|
|
292
269
|
return (isBackendTestCompletenessRetryCandidate(task) ||
|
|
293
270
|
isBackendTestPytestCollectionRepairOutcomeRecoveryCandidate(task) ||
|
|
@@ -302,51 +279,6 @@ export function allowsMissingChangedWriterOutcomeRecovery(task) {
|
|
|
302
279
|
(task.writeSet?.length ?? 0) > 0 &&
|
|
303
280
|
task.writerOutcomePolicy?.type === "implementation-outcome-v1"));
|
|
304
281
|
}
|
|
305
|
-
function readWriterThinkingExhaustionEvidence(result) {
|
|
306
|
-
const wider = result;
|
|
307
|
-
return {
|
|
308
|
-
...(typeof wider.stopReason === "string"
|
|
309
|
-
? { stopReason: wider.stopReason }
|
|
310
|
-
: {}),
|
|
311
|
-
...(typeof wider.thinkingObserved === "boolean"
|
|
312
|
-
? { thinkingObserved: wider.thinkingObserved }
|
|
313
|
-
: {}),
|
|
314
|
-
...(typeof wider.writeToolCallCount === "number"
|
|
315
|
-
? { writeToolCallCount: wider.writeToolCallCount }
|
|
316
|
-
: {}),
|
|
317
|
-
};
|
|
318
|
-
}
|
|
319
|
-
/**
|
|
320
|
-
* Pure classification predicate for writer-thinking-exhausted (AC-002).
|
|
321
|
-
* Requires ALL of: empty-output base category, stopReason exactly "length",
|
|
322
|
-
* thinking observed, zero write/edit tool calls, and a confirmed empty
|
|
323
|
-
* run-attributed diff. Recoverable partial writes stay incomplete-write-set
|
|
324
|
-
* because the caller only consults this predicate when the completeness gate
|
|
325
|
-
* did not upgrade the category.
|
|
326
|
-
*/
|
|
327
|
-
export function isWriterThinkingExhausted(result, mapped, changeManifestChangedFiles) {
|
|
328
|
-
if (mapped.ok)
|
|
329
|
-
return false;
|
|
330
|
-
const evidence = readWriterThinkingExhaustionEvidence(result);
|
|
331
|
-
if (evidence.stopReason !== "length")
|
|
332
|
-
return false;
|
|
333
|
-
if (evidence.thinkingObserved !== true)
|
|
334
|
-
return false;
|
|
335
|
-
if ((evidence.writeToolCallCount ?? 0) !== 0)
|
|
336
|
-
return false;
|
|
337
|
-
// Gateways sometimes classify a length-stopped stream as `network` or
|
|
338
|
-
// `nonzero-exit` because the terminal event is carried in stderr. The
|
|
339
|
-
// provider evidence is stronger than that transport label when no write
|
|
340
|
-
// tool was called and the run produced no diff.
|
|
341
|
-
if (mapped.failureCategory &&
|
|
342
|
-
!["empty-output", "network", "nonzero-exit", "unknown"].includes(mapped.failureCategory))
|
|
343
|
-
return false;
|
|
344
|
-
if (changeManifestChangedFiles === undefined)
|
|
345
|
-
return false;
|
|
346
|
-
if (changeManifestChangedFiles.length !== 0)
|
|
347
|
-
return false;
|
|
348
|
-
return true;
|
|
349
|
-
}
|
|
350
282
|
export const DAG_PI_READONLY_TOOLS = ["read", "grep", "find", "ls"];
|
|
351
283
|
/** Bounded writer tools include bash; edit/write remain policy-wrapped via SDK customTools. */
|
|
352
284
|
export const DAG_PI_WRITE_TOOLS = [
|
|
@@ -1741,171 +1673,12 @@ async function validateFrontendDesignTerminal(input) {
|
|
|
1741
1673
|
}
|
|
1742
1674
|
/** Tool subsets for the frontend plan phases. Local UX decisions and global
|
|
1743
1675
|
* policies intentionally have different sessions and different tool sets. */
|
|
1744
|
-
const FRONTEND_PLAN_SEGMENTS = [
|
|
1745
|
-
{
|
|
1746
|
-
id: "coverage",
|
|
1747
|
-
toolNames: new Set([
|
|
1748
|
-
"record_plan_requirement",
|
|
1749
|
-
"record_plan_group_coverage",
|
|
1750
|
-
"record_plan_verification_target",
|
|
1751
|
-
"record_plan_evidence_gap",
|
|
1752
|
-
"adopt_staged_fact",
|
|
1753
|
-
]),
|
|
1754
|
-
instruction: [
|
|
1755
|
-
"PLAN PHASE — requirement coverage only.",
|
|
1756
|
-
"Your ONLY job: for every frozen requirement, emit record_plan_requirement (requirement → implementation files) and record_plan_verification_target facts (verification target bound to requirement ids and files). Group related requirements under one non-static behavior target when one observable test behavior proves them together; do not mechanically create one target per requirement. Target ids identify contract entries, not test-title markers. Reuse affected existing test files and their names; do not add tests or rename titles just to carry generated ids. Never submit prose as a symbol. Do NOT record components, UI states, mock, dependency, or routes — a follow-up session owns those.",
|
|
1757
|
-
"A coverage session is complete only when EVERY requirement assigned to this session (the full inventory, or the exact COVERAGE BATCH / shard list when present) has committed coverage facts: a record_plan_requirement entry plus verification targets, or a committed evidence gap. Keep committing in batches of up to 4 record_* calls per assistant message until then; do not write a concluding summary while any assigned requirement is still uncommitted — an early stop strands the remainder into a MISSING-FACT repair session and doubles the sessions needed.",
|
|
1758
|
-
"If a requirement genuinely cannot have a verification target, record a non-empty record_plan_evidence_gap. Do not call finalize_plan; it is not available in this phase.",
|
|
1759
|
-
].join(" "),
|
|
1760
|
-
},
|
|
1761
|
-
{
|
|
1762
|
-
id: "ux-registry",
|
|
1763
|
-
toolNames: new Set(["record_state_registry", "adopt_staged_fact"]),
|
|
1764
|
-
instruction: [
|
|
1765
|
-
"PLAN PHASE — global UX vocabulary.",
|
|
1766
|
-
"Bootstrap the global UX vocabulary from the execution-group index and authoritative declared states. This is navigation, not permission to decide unseen behavior. Detailed complete scopes may extend the registry with replace:true while preserving live names. UI states use declaredUiStates ids when present. Interaction names are stable kebab-case behavior domains; merge requirements that describe the same behavior instead of renaming it per AC slice. Empty arrays explicitly declare that no UX vocabulary applies. Do not record component choices or state-flow details in this phase.",
|
|
1767
|
-
"Do not call finalize_plan; it is not available in this phase.",
|
|
1768
|
-
].join(" "),
|
|
1769
|
-
},
|
|
1770
|
-
{
|
|
1771
|
-
id: "ux-local",
|
|
1772
|
-
toolNames: new Set([
|
|
1773
|
-
"record_state_registry",
|
|
1774
|
-
"record_component_choice",
|
|
1775
|
-
"record_state_flow",
|
|
1776
|
-
"record_plan_verification_target",
|
|
1777
|
-
"adopt_staged_fact",
|
|
1778
|
-
]),
|
|
1779
|
-
instruction: [
|
|
1780
|
-
"PLAN PHASE — global UX decisions.",
|
|
1781
|
-
"Requirements and verification targets are already committed in the ledger; do not re-record unchanged facts. Review the current complete execution-group scope and the committed global UX registry together, then record each component choice, UI state and interaction exactly once. Bind each applicable state to its verificationTargetIds; the runtime derives the reverse VT.uiStates relation. If a VT requires correction, record_plan_verification_target with replace:true is available after declaring its states; preserve its requirement coverage. Multiple requirements describing one behavior share one registry name and state-flow entry; never repeat or rename it per AC. Cross-cutting data flow belongs to the global Mock/data phase. Do not record routes, Mock/API policy, dependencies, or design deviations here.",
|
|
1782
|
-
"Do not call finalize_plan; it is not available in this phase.",
|
|
1783
|
-
].join(" "),
|
|
1784
|
-
},
|
|
1785
|
-
{
|
|
1786
|
-
id: "global-route",
|
|
1787
|
-
toolNames: new Set(["record_route_selection", "adopt_staged_fact"]),
|
|
1788
|
-
instruction: [
|
|
1789
|
-
"PLAN PHASE — global route decision.",
|
|
1790
|
-
"Use the Scout target surface and record only the selected route(s). Do not record requirement-local UX, Mock/data, dependency, or deviation facts. Do not call finalize_plan.",
|
|
1791
|
-
].join(" "),
|
|
1792
|
-
},
|
|
1793
|
-
{
|
|
1794
|
-
id: "global-mock-data",
|
|
1795
|
-
toolNames: new Set(["record_data_flow", "record_mock_api", "record_mock_endpoint", "adopt_staged_fact"]),
|
|
1796
|
-
instruction: [
|
|
1797
|
-
"PLAN PHASE — global Mock/API and data policy.",
|
|
1798
|
-
"Record the cross-cutting interaction-to-endpoint data flow and Mock/API strategy only. Keep this decision set separate from route, component, state, dependency, and deviation facts. Do not call finalize_plan.",
|
|
1799
|
-
].join(" "),
|
|
1800
|
-
},
|
|
1801
|
-
{
|
|
1802
|
-
id: "global-dependency-deviation",
|
|
1803
|
-
toolNames: new Set(["record_dependency", "record_design_deviation", "adopt_staged_fact"]),
|
|
1804
|
-
instruction: [
|
|
1805
|
-
"PLAN PHASE — global dependency and design-deviation policy.",
|
|
1806
|
-
"Record only dependency policy and design-evidence conflicts. Do not record route, Mock/data, or requirement-local UX facts. Do not call finalize_plan.",
|
|
1807
|
-
].join(" "),
|
|
1808
|
-
},
|
|
1809
|
-
];
|
|
1810
1676
|
/** Estimate calls conservatively: requirement + one VT, with a second VT
|
|
1811
1677
|
* reserved for behaviour-required requirements. Explicit declarations win. */
|
|
1812
|
-
export function estimateFrontendPlanRequirementRecordCalls(fact) {
|
|
1813
|
-
const record = fact && typeof fact === "object" && !Array.isArray(fact)
|
|
1814
|
-
? fact
|
|
1815
|
-
: {};
|
|
1816
|
-
const declaredTargetCount = Array.isArray(record.verificationTargetIds)
|
|
1817
|
-
? record.verificationTargetIds.filter((value) => typeof value === "string" && value.trim()).length
|
|
1818
|
-
: Array.isArray(record.verificationTargets)
|
|
1819
|
-
? record.verificationTargets.length
|
|
1820
|
-
: 0;
|
|
1821
|
-
const evidence = record.evidence && typeof record.evidence === "object"
|
|
1822
|
-
? record.evidence
|
|
1823
|
-
: undefined;
|
|
1824
|
-
const targetCount = Math.max(declaredTargetCount, evidence?.behavior === "required" ? 2 : 1);
|
|
1825
|
-
return 1 + targetCount;
|
|
1826
|
-
}
|
|
1827
1678
|
/** Pack requirements without splitting one requirement across sessions. */
|
|
1828
|
-
export function batchFrontendPlanRequirements(input) {
|
|
1829
|
-
const batches = [];
|
|
1830
|
-
let current = [];
|
|
1831
|
-
let currentCost = 0;
|
|
1832
|
-
for (const id of input.requirementIds) {
|
|
1833
|
-
const cost = Math.max(1, input.requirementCosts?.get(id) ?? 2);
|
|
1834
|
-
if (current.length > 0 &&
|
|
1835
|
-
(currentCost + cost > input.maxEstimatedRecordCalls ||
|
|
1836
|
-
(input.maxRequirements !== undefined &&
|
|
1837
|
-
current.length >= input.maxRequirements))) {
|
|
1838
|
-
batches.push(current);
|
|
1839
|
-
current = [];
|
|
1840
|
-
currentCost = 0;
|
|
1841
|
-
}
|
|
1842
|
-
current.push(id);
|
|
1843
|
-
currentCost += cost;
|
|
1844
|
-
}
|
|
1845
|
-
if (current.length > 0)
|
|
1846
|
-
batches.push(current);
|
|
1847
|
-
return batches;
|
|
1848
|
-
}
|
|
1849
|
-
const FRONTEND_PLAN_COVERAGE_MAX_RECORD_CALLS = 10;
|
|
1850
|
-
const FRONTEND_PLAN_COVERAGE_MAX_CONCURRENCY = 4;
|
|
1851
1679
|
// A single registry owns global names; large UX work uses bounded serial
|
|
1852
1680
|
// scopes against that registry. Keep a safety bound for adaptive retries
|
|
1853
1681
|
// without letting the old 32-session ceiling skip finalize.
|
|
1854
|
-
const FRONTEND_PLAN_BATCH_MAX_SESSIONS = 128;
|
|
1855
|
-
const FRONTEND_PLAN_SMALL_MAX_REQUIREMENTS = 8;
|
|
1856
|
-
const FRONTEND_PLAN_SMALL_MAX_ESTIMATED_CALLS = 24;
|
|
1857
|
-
async function mapWithConcurrency(items, limit, worker, shouldReduceConcurrency) {
|
|
1858
|
-
const results = new Array(items.length);
|
|
1859
|
-
let nextIndex = 0;
|
|
1860
|
-
let concurrency = Math.max(1, limit);
|
|
1861
|
-
const running = new Map();
|
|
1862
|
-
try {
|
|
1863
|
-
while (nextIndex < items.length || running.size > 0) {
|
|
1864
|
-
while (nextIndex < items.length && running.size < concurrency) {
|
|
1865
|
-
const index = nextIndex++;
|
|
1866
|
-
running.set(index, worker(items[index], index).then(result => ({ index, result })));
|
|
1867
|
-
}
|
|
1868
|
-
const { index, result } = await Promise.race(running.values());
|
|
1869
|
-
running.delete(index);
|
|
1870
|
-
results[index] = result;
|
|
1871
|
-
// Drain existing work; only pending shards use the reduced cap.
|
|
1872
|
-
// Failed shards remain failed and receive no extra retry allowance.
|
|
1873
|
-
if (shouldReduceConcurrency(result))
|
|
1874
|
-
concurrency = Math.max(1, Math.floor(concurrency / 2));
|
|
1875
|
-
}
|
|
1876
|
-
}
|
|
1877
|
-
catch (error) {
|
|
1878
|
-
await Promise.allSettled(running.values());
|
|
1879
|
-
throw error;
|
|
1880
|
-
}
|
|
1881
|
-
return results;
|
|
1882
|
-
}
|
|
1883
|
-
function compactPromptString(value, _maxChars) {
|
|
1884
|
-
return typeof value === "string" && value.trim().length ? value.trim() : undefined;
|
|
1885
|
-
}
|
|
1886
|
-
function compactPromptStringArray(value, _maxEntries = 12, maxChars = 180) {
|
|
1887
|
-
if (!Array.isArray(value))
|
|
1888
|
-
return [];
|
|
1889
|
-
return value
|
|
1890
|
-
.map((item) => compactPromptString(item, maxChars))
|
|
1891
|
-
.filter((item) => item !== undefined);
|
|
1892
|
-
}
|
|
1893
|
-
function countFrontendPlanTargetSurfaces(basePrompt) {
|
|
1894
|
-
const match = /<frontend_plan_input>[\s\S]*?<\/frontend_plan_input>/.exec(basePrompt);
|
|
1895
|
-
if (!match)
|
|
1896
|
-
return undefined;
|
|
1897
|
-
for (const line of match[0].split(/\r?\n/)) {
|
|
1898
|
-
try {
|
|
1899
|
-
const payload = JSON.parse(line);
|
|
1900
|
-
if (Array.isArray(payload.targetSurface))
|
|
1901
|
-
return payload.targetSurface.length;
|
|
1902
|
-
}
|
|
1903
|
-
catch {
|
|
1904
|
-
// surrounding lines are prose
|
|
1905
|
-
}
|
|
1906
|
-
}
|
|
1907
|
-
return undefined;
|
|
1908
|
-
}
|
|
1909
1682
|
/**
|
|
1910
1683
|
* Deterministically extract repository file paths that frozen verification
|
|
1911
1684
|
* commands operate on (`--config <file>`, `node --check <file>`). A frozen
|
|
@@ -1916,19 +1689,6 @@ function countFrontendPlanTargetSurfaces(basePrompt) {
|
|
|
1916
1689
|
* compact session record the missing verification target inside the same
|
|
1917
1690
|
* attempt instead.
|
|
1918
1691
|
*/
|
|
1919
|
-
export function collectFrontendVerificationCommandFiles(basePrompt) {
|
|
1920
|
-
const files = new Set();
|
|
1921
|
-
const configRe = /--config\s+([\w@./-]+\.(?:js|mjs|cjs|ts|json))/g;
|
|
1922
|
-
const checkRe = /node\s+--check\s+([\w@./-]+\.(?:js|mjs|cjs))/g;
|
|
1923
|
-
for (const re of [configRe, checkRe]) {
|
|
1924
|
-
for (const match of basePrompt.matchAll(re)) {
|
|
1925
|
-
const file = match[1];
|
|
1926
|
-
if (file && file.includes("/"))
|
|
1927
|
-
files.add(file);
|
|
1928
|
-
}
|
|
1929
|
-
}
|
|
1930
|
-
return [...files].sort();
|
|
1931
|
-
}
|
|
1932
1692
|
/**
|
|
1933
1693
|
* Remove the repeated full planner input from a coverage batch. The normal
|
|
1934
1694
|
* plan prompt already contains a bounded JSON handoff, but repeating all
|
|
@@ -1938,647 +1698,8 @@ export function collectFrontendVerificationCommandFiles(basePrompt) {
|
|
|
1938
1698
|
* preserve the old prompt as a safe compatibility fallback rather than
|
|
1939
1699
|
* silently giving the model an incomplete requirement.
|
|
1940
1700
|
*/
|
|
1941
|
-
export function compactFrontendPlanPromptForRequirementSlice(basePrompt, requirementIds, options = {}) {
|
|
1942
|
-
const slice = [...new Set(requirementIds.filter((id) => id.trim().length > 0))];
|
|
1943
|
-
if (slice.length === 0)
|
|
1944
|
-
return basePrompt;
|
|
1945
|
-
const planInputPattern = /<frontend_plan_input>[\s\S]*?<\/frontend_plan_input>/;
|
|
1946
|
-
const planInputMatch = planInputPattern.exec(basePrompt);
|
|
1947
|
-
if (!planInputMatch)
|
|
1948
|
-
return basePrompt;
|
|
1949
|
-
const blockLines = planInputMatch[0].split(/\r?\n/);
|
|
1950
|
-
let payload;
|
|
1951
|
-
for (const line of blockLines) {
|
|
1952
|
-
try {
|
|
1953
|
-
const parsed = JSON.parse(line);
|
|
1954
|
-
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
1955
|
-
payload = parsed;
|
|
1956
|
-
break;
|
|
1957
|
-
}
|
|
1958
|
-
}
|
|
1959
|
-
catch {
|
|
1960
|
-
// The surrounding block contains prose; only its JSON line is data.
|
|
1961
|
-
}
|
|
1962
|
-
}
|
|
1963
|
-
if (!payload || !Array.isArray(payload.requirements))
|
|
1964
|
-
throw Error("FRONTEND_INPUT_INVALID: plan inventory is not parseable");
|
|
1965
|
-
const requirementsById = new Map();
|
|
1966
|
-
for (const value of payload.requirements) {
|
|
1967
|
-
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
1968
|
-
continue;
|
|
1969
|
-
const requirement = value;
|
|
1970
|
-
if (typeof requirement.id === "string") {
|
|
1971
|
-
requirementsById.set(requirement.id, requirement);
|
|
1972
|
-
}
|
|
1973
|
-
}
|
|
1974
|
-
const requirements = slice.map((id) => requirementsById.get(id));
|
|
1975
|
-
if (requirements.some((requirement) => requirement === undefined)) {
|
|
1976
|
-
throw Error(`FRONTEND_INPUT_SCOPE_MISSING: ${slice.filter(id => !requirementsById.has(id)).join(", ")}`);
|
|
1977
|
-
}
|
|
1978
|
-
const compactRequirements = requirements.map((requirement) => ({
|
|
1979
|
-
id: compactPromptString(requirement.id, 80),
|
|
1980
|
-
...(options.includeRequirementText !== false && compactPromptString(requirement.text, 240)
|
|
1981
|
-
? { text: compactPromptString(requirement.text, 240) }
|
|
1982
|
-
: {}),
|
|
1983
|
-
sourceFragmentIds: compactPromptStringArray(requirement.sourceFragmentIds, 20, 80),
|
|
1984
|
-
}));
|
|
1985
|
-
const sliceSet = new Set(slice);
|
|
1986
|
-
const compactVerificationTargets = options.includeVerificationTargets === false
|
|
1987
|
-
? []
|
|
1988
|
-
: Array.isArray(payload.verificationTargets)
|
|
1989
|
-
? payload.verificationTargets.flatMap((value) => {
|
|
1990
|
-
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
1991
|
-
return [];
|
|
1992
|
-
const target = value;
|
|
1993
|
-
if (options.verificationTargetIds && !options.verificationTargetIds.includes(String(target.id)))
|
|
1994
|
-
return [];
|
|
1995
|
-
const targetRequirementIds = planFactStringList(target.requirementIds);
|
|
1996
|
-
const relatedRequirementIds = targetRequirementIds.filter((id) => sliceSet.has(id));
|
|
1997
|
-
if (relatedRequirementIds.length === 0)
|
|
1998
|
-
return [];
|
|
1999
|
-
return [
|
|
2000
|
-
{
|
|
2001
|
-
...(typeof target.id === "string"
|
|
2002
|
-
? { id: target.id }
|
|
2003
|
-
: {}),
|
|
2004
|
-
...(typeof target.commandId === "string"
|
|
2005
|
-
? { commandId: target.commandId }
|
|
2006
|
-
: {}),
|
|
2007
|
-
...(compactPromptString(target.commandLabel, 180)
|
|
2008
|
-
? { commandLabel: compactPromptString(target.commandLabel, 180) }
|
|
2009
|
-
: {}),
|
|
2010
|
-
...(typeof target.file === "string"
|
|
2011
|
-
? { file: target.file }
|
|
2012
|
-
: {}),
|
|
2013
|
-
requirementIds: relatedRequirementIds,
|
|
2014
|
-
uiStates: planFactStringList(target.uiStates),
|
|
2015
|
-
},
|
|
2016
|
-
];
|
|
2017
|
-
})
|
|
2018
|
-
: [];
|
|
2019
|
-
const compactTargetSurface = Array.isArray(payload.targetSurface)
|
|
2020
|
-
? payload.targetSurface.flatMap((value) => {
|
|
2021
|
-
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
2022
|
-
return [];
|
|
2023
|
-
const surface = value;
|
|
2024
|
-
return [
|
|
2025
|
-
{
|
|
2026
|
-
...(compactPromptString(surface.completeness, 32)
|
|
2027
|
-
? { completeness: compactPromptString(surface.completeness, 32) }
|
|
2028
|
-
: {}),
|
|
2029
|
-
...(compactPromptString(surface.entrypoint, 180)
|
|
2030
|
-
? { entrypoint: compactPromptString(surface.entrypoint, 180) }
|
|
2031
|
-
: {}),
|
|
2032
|
-
...(compactPromptString(surface.routeOrMount, 180)
|
|
2033
|
-
? { routeOrMount: compactPromptString(surface.routeOrMount, 180) }
|
|
2034
|
-
: {}),
|
|
2035
|
-
implementationPaths: compactPromptStringArray(surface.implementationPaths),
|
|
2036
|
-
proposedPaths: compactPromptStringArray(surface.proposedPaths),
|
|
2037
|
-
testPaths: compactPromptStringArray(surface.testPaths),
|
|
2038
|
-
...(compactPromptString(surface.dataSource, 180)
|
|
2039
|
-
? { dataSource: compactPromptString(surface.dataSource, 180) }
|
|
2040
|
-
: {}),
|
|
2041
|
-
allowedPathConflicts: compactPromptStringArray(surface.allowedPathConflicts),
|
|
2042
|
-
unresolvedPaths: compactPromptStringArray(surface.unresolvedPaths),
|
|
2043
|
-
},
|
|
2044
|
-
];
|
|
2045
|
-
})
|
|
2046
|
-
: [];
|
|
2047
|
-
const compactDesignEvidence = options.includeDesignEvidence === true &&
|
|
2048
|
-
Array.isArray(payload.designEvidence)
|
|
2049
|
-
? payload.designEvidence.flatMap((value) => {
|
|
2050
|
-
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
2051
|
-
return [];
|
|
2052
|
-
const evidence = value;
|
|
2053
|
-
return [{
|
|
2054
|
-
...(compactPromptString(evidence.source, 180)
|
|
2055
|
-
? { source: compactPromptString(evidence.source, 180) }
|
|
2056
|
-
: {}),
|
|
2057
|
-
paths: compactPromptStringArray(evidence.paths, 12, 180),
|
|
2058
|
-
conflicts: compactPromptStringArray(evidence.conflicts, 24, 180),
|
|
2059
|
-
}];
|
|
2060
|
-
})
|
|
2061
|
-
: [];
|
|
2062
|
-
const compactDeclaredUiStates = Array.isArray(payload.declaredUiStates)
|
|
2063
|
-
? payload.declaredUiStates.flatMap((value) => {
|
|
2064
|
-
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
2065
|
-
return [];
|
|
2066
|
-
const state = value;
|
|
2067
|
-
const id = typeof state.id === "string" ? state.id : undefined;
|
|
2068
|
-
if (!id)
|
|
2069
|
-
return [];
|
|
2070
|
-
return [{
|
|
2071
|
-
id,
|
|
2072
|
-
...(compactPromptString(state.trigger, 180)
|
|
2073
|
-
? { trigger: compactPromptString(state.trigger, 180) }
|
|
2074
|
-
: {}),
|
|
2075
|
-
...(compactPromptString(state.observableOutcome, 240)
|
|
2076
|
-
? { observableOutcome: compactPromptString(state.observableOutcome, 240) }
|
|
2077
|
-
: {}),
|
|
2078
|
-
}];
|
|
2079
|
-
})
|
|
2080
|
-
: [];
|
|
2081
|
-
const committedUx = payload.committedUx &&
|
|
2082
|
-
typeof payload.committedUx === "object" &&
|
|
2083
|
-
!Array.isArray(payload.committedUx)
|
|
2084
|
-
? payload.committedUx
|
|
2085
|
-
: undefined;
|
|
2086
|
-
const compactPayload = {
|
|
2087
|
-
inputManifest: { ...projectFrontendInputScope({ ...payload, requirements: [...requirementsById.values()] }, slice).inputManifest, semantics: options.includeRequirementText === false ? "navigation-only" : "full" },
|
|
2088
|
-
constraints: payload.constraints,
|
|
2089
|
-
executionGroups: Array.isArray(payload.executionGroups) ? payload.executionGroups.filter(g => isRecordObject(g) && Array.isArray(g.requirementIds) && g.requirementIds.some(id => slice.includes(String(id)))) : [],
|
|
2090
|
-
requirements: compactRequirements,
|
|
2091
|
-
requiredDeliverables: Array.isArray(payload.requiredDeliverables) ? payload.requiredDeliverables : [],
|
|
2092
|
-
...(compactTargetSurface.length > 0
|
|
2093
|
-
? { targetSurface: compactTargetSurface }
|
|
2094
|
-
: {}),
|
|
2095
|
-
...(compactVerificationTargets.length > 0
|
|
2096
|
-
? { verificationTargets: compactVerificationTargets }
|
|
2097
|
-
: {}),
|
|
2098
|
-
...(compactDesignEvidence.length > 0
|
|
2099
|
-
? { designEvidence: compactDesignEvidence }
|
|
2100
|
-
: {}),
|
|
2101
|
-
...(compactDeclaredUiStates.length > 0
|
|
2102
|
-
? { declaredUiStates: compactDeclaredUiStates }
|
|
2103
|
-
: {}),
|
|
2104
|
-
...(committedUx
|
|
2105
|
-
? {
|
|
2106
|
-
committedUx: {
|
|
2107
|
-
uiStateNames: compactPromptStringArray(committedUx.uiStateNames, 40, 100),
|
|
2108
|
-
interactionNames: compactPromptStringArray(committedUx.interactionNames, 60, 120),
|
|
2109
|
-
},
|
|
2110
|
-
}
|
|
2111
|
-
: {}),
|
|
2112
|
-
};
|
|
2113
|
-
const compactBlock = [
|
|
2114
|
-
"<frontend_plan_input>",
|
|
2115
|
-
`Committed Contract/Scout facts for ONLY the current requirement slice (${slice.join(", ")}); do not infer or record requirements outside this slice.`,
|
|
2116
|
-
JSON.stringify(compactPayload),
|
|
2117
|
-
"Do not read upstream artifacts, task sources, or repository files. If this slice cannot support a decision, record a genuine evidence gap.",
|
|
2118
|
-
"</frontend_plan_input>",
|
|
2119
|
-
].join("\n");
|
|
2120
|
-
let compactPrompt = basePrompt.replace(planInputPattern, compactBlock);
|
|
2121
|
-
compactPrompt = compactPrompt.replace(/<upstream_context>[\s\S]*?<\/upstream_context>/, "<upstream_context>\n(full upstream prose omitted; use only the slice-specific typed facts above)\n</upstream_context>");
|
|
2122
|
-
compactPrompt = compactPrompt.replace(/Cover each frozen requirement ID exactly once:[^\n]*/, `Cover ONLY the current requirement slice: ${slice.join(", ")}. Record each slice requirement exactly once.`);
|
|
2123
|
-
const checklistPattern = /<plan_review_checklist>([\s\S]*?)<\/plan_review_checklist>/;
|
|
2124
|
-
const checklistMatch = checklistPattern.exec(compactPrompt);
|
|
2125
|
-
if (checklistMatch) {
|
|
2126
|
-
if (options.includeChecklist === false) {
|
|
2127
|
-
compactPrompt = compactPrompt.replace(checklistPattern, "");
|
|
2128
|
-
return compactPrompt;
|
|
2129
|
-
}
|
|
2130
|
-
const checklist = checklistMatch[1]
|
|
2131
|
-
.split(/\r?\n/)
|
|
2132
|
-
.filter((line) => {
|
|
2133
|
-
const requirementLine = /^Requirements requiring behavioural coverage:\s*(.*)$/.exec(line.trim());
|
|
2134
|
-
if (requirementLine) {
|
|
2135
|
-
return false;
|
|
2136
|
-
}
|
|
2137
|
-
const citationId = /^([^\s]+)\s+\+/.exec(line.trim())?.[1];
|
|
2138
|
-
return citationId === undefined || sliceSet.has(citationId);
|
|
2139
|
-
})
|
|
2140
|
-
.concat([`Requirements requiring behavioural coverage: ${slice.join(", ") || "(none)"}`])
|
|
2141
|
-
.join("\n");
|
|
2142
|
-
compactPrompt = compactPrompt.replace(checklistPattern, `<plan_review_checklist>${checklist}</plan_review_checklist>`);
|
|
2143
|
-
}
|
|
2144
|
-
return compactPrompt;
|
|
2145
|
-
}
|
|
2146
|
-
function compactFrontendPlanLedgerContext(input) {
|
|
2147
|
-
const slice = new Set(input.requirementIds);
|
|
2148
|
-
const allowed = new Set(input.kinds);
|
|
2149
|
-
const scopedKinds = new Set(input.scopedKinds ?? []);
|
|
2150
|
-
const compactFacts = [];
|
|
2151
|
-
for (const value of input.committedFacts) {
|
|
2152
|
-
const fact = committedFactFromPlanRecord(value);
|
|
2153
|
-
if (!fact || fact.origin !== "plan" || typeof fact.kind !== "string" || !allowed.has(fact.kind))
|
|
2154
|
-
continue;
|
|
2155
|
-
if (scopedKinds.has(fact.kind) && !planFactScopeIntersects(fact, slice)) {
|
|
2156
|
-
continue;
|
|
2157
|
-
}
|
|
2158
|
-
const entry = fact.entry && typeof fact.entry === "object" && !Array.isArray(fact.entry)
|
|
2159
|
-
? fact.entry
|
|
2160
|
-
: undefined;
|
|
2161
|
-
if (fact.kind === "plan-requirement") {
|
|
2162
|
-
if (!entry || typeof entry.id !== "string" || !slice.has(entry.id))
|
|
2163
|
-
continue;
|
|
2164
|
-
compactFacts.push({
|
|
2165
|
-
kind: fact.kind,
|
|
2166
|
-
entry: {
|
|
2167
|
-
id: entry.id,
|
|
2168
|
-
implementationTargets: planFactStringList(entry.implementationTargets).slice(0, 12),
|
|
2169
|
-
verificationTargetIds: planFactStringList(entry.verificationTargetIds).slice(0, 12),
|
|
2170
|
-
...(compactPromptString(entry.expectedOutcome, 240)
|
|
2171
|
-
? { expectedOutcome: compactPromptString(entry.expectedOutcome, 240) }
|
|
2172
|
-
: {}),
|
|
2173
|
-
...(entry.evidenceGap && typeof entry.evidenceGap === "object"
|
|
2174
|
-
? { evidenceGap: entry.evidenceGap }
|
|
2175
|
-
: {}),
|
|
2176
|
-
},
|
|
2177
|
-
});
|
|
2178
|
-
continue;
|
|
2179
|
-
}
|
|
2180
|
-
if (fact.kind === "plan-verification-target") {
|
|
2181
|
-
if (!entry)
|
|
2182
|
-
continue;
|
|
2183
|
-
// Scope against the complete canonical binding before compacting.
|
|
2184
|
-
// A shared target may bind a late requirement beyond the preview cap.
|
|
2185
|
-
const requirementIds = planFactStringList(entry.requirementIds).filter((id) => slice.has(id));
|
|
2186
|
-
if (requirementIds.length === 0)
|
|
2187
|
-
continue;
|
|
2188
|
-
compactFacts.push({
|
|
2189
|
-
kind: fact.kind,
|
|
2190
|
-
entry: {
|
|
2191
|
-
id: entry.id,
|
|
2192
|
-
commandId: entry.commandId,
|
|
2193
|
-
file: entry.file,
|
|
2194
|
-
requirementIds,
|
|
2195
|
-
uiStates: planFactStringList(entry.uiStates).slice(0, 12),
|
|
2196
|
-
},
|
|
2197
|
-
});
|
|
2198
|
-
continue;
|
|
2199
|
-
}
|
|
2200
|
-
if (fact.kind === "component-choice") {
|
|
2201
|
-
const choices = Array.isArray(fact.uiComponentChoices)
|
|
2202
|
-
? fact.uiComponentChoices.flatMap((choice) => {
|
|
2203
|
-
if (!choice || typeof choice !== "object" || Array.isArray(choice))
|
|
2204
|
-
return [];
|
|
2205
|
-
const item = choice;
|
|
2206
|
-
return [{
|
|
2207
|
-
purpose: compactPromptString(item.purpose, 120),
|
|
2208
|
-
component: compactPromptString(item.component, 120),
|
|
2209
|
-
decision: compactPromptString(item.decision, 40),
|
|
2210
|
-
covers: compactPromptStringArray(item.covers, 40, 120),
|
|
2211
|
-
evidencePath: compactPromptString(item.evidencePath, 180),
|
|
2212
|
-
}];
|
|
2213
|
-
})
|
|
2214
|
-
: [];
|
|
2215
|
-
if (choices.length > 0)
|
|
2216
|
-
compactFacts.push({ kind: fact.kind, uiComponentChoices: choices });
|
|
2217
|
-
continue;
|
|
2218
|
-
}
|
|
2219
|
-
if (fact.kind === "state-registry") {
|
|
2220
|
-
compactFacts.push({
|
|
2221
|
-
kind: fact.kind,
|
|
2222
|
-
uiStateNames: planFactStringList(fact.uiStateNames).slice(0, 40),
|
|
2223
|
-
interactionNames: planFactStringList(fact.interactionNames).slice(0, 60),
|
|
2224
|
-
});
|
|
2225
|
-
continue;
|
|
2226
|
-
}
|
|
2227
|
-
if (fact.kind === "state-flow") {
|
|
2228
|
-
compactFacts.push({
|
|
2229
|
-
kind: fact.kind,
|
|
2230
|
-
uiStates: Array.isArray(fact.uiStates) ? fact.uiStates : [],
|
|
2231
|
-
interactions: Array.isArray(fact.interactions) ? fact.interactions : [],
|
|
2232
|
-
removeUiStateNames: compactPromptStringArray(fact.removeUiStateNames, 24, 100),
|
|
2233
|
-
removeInteractionNames: compactPromptStringArray(fact.removeInteractionNames, 24, 100),
|
|
2234
|
-
});
|
|
2235
|
-
continue;
|
|
2236
|
-
}
|
|
2237
|
-
if (fact.kind === "data-flow") {
|
|
2238
|
-
compactFacts.push({
|
|
2239
|
-
kind: fact.kind,
|
|
2240
|
-
interactions: compactPromptStringArray(fact.interactions, 32, 120),
|
|
2241
|
-
endpoints: compactPromptStringArray(fact.endpoints, 32, 180),
|
|
2242
|
-
});
|
|
2243
|
-
continue;
|
|
2244
|
-
}
|
|
2245
|
-
if (fact.kind === "mock-api") {
|
|
2246
|
-
const mockApi = fact.mockApi && typeof fact.mockApi === "object" && !Array.isArray(fact.mockApi)
|
|
2247
|
-
? fact.mockApi
|
|
2248
|
-
: {};
|
|
2249
|
-
compactFacts.push({
|
|
2250
|
-
kind: fact.kind,
|
|
2251
|
-
mockApi: {
|
|
2252
|
-
strategy: compactPromptString(mockApi.strategy, 40),
|
|
2253
|
-
activation: compactPromptString(mockApi.activation, 180),
|
|
2254
|
-
endpoints: Array.isArray(mockApi.endpoints) ? mockApi.endpoints : [],
|
|
2255
|
-
},
|
|
2256
|
-
});
|
|
2257
|
-
continue;
|
|
2258
|
-
}
|
|
2259
|
-
if (fact.kind === "design-deviation") {
|
|
2260
|
-
compactFacts.push({ kind: fact.kind, conflicts: compactPromptStringArray(fact.conflicts, 32, 180) });
|
|
2261
|
-
continue;
|
|
2262
|
-
}
|
|
2263
|
-
if (fact.kind === "dependency") {
|
|
2264
|
-
compactFacts.push({ kind: fact.kind, policy: compactPromptString(fact.policy, 300) });
|
|
2265
|
-
continue;
|
|
2266
|
-
}
|
|
2267
|
-
if (fact.kind === "target-surface") {
|
|
2268
|
-
compactFacts.push({ kind: fact.kind, routes: compactPromptStringArray(fact.routes, 24, 180), implementationPaths: compactPromptStringArray(fact.implementationPaths, 24, 180), proposedPaths: compactPromptStringArray(fact.proposedPaths, 24, 180), testPaths: compactPromptStringArray(fact.testPaths, 24, 180) });
|
|
2269
|
-
}
|
|
2270
|
-
}
|
|
2271
|
-
if (compactFacts.length === 0)
|
|
2272
|
-
return "";
|
|
2273
|
-
const priorityFacts = compactFacts.filter((fact) => fact.kind === "plan-requirement" || fact.kind === "plan-verification-target");
|
|
2274
|
-
const otherFacts = compactFacts.filter((fact) => fact.kind !== "plan-requirement" && fact.kind !== "plan-verification-target");
|
|
2275
|
-
let previewBytes = 0;
|
|
2276
|
-
const boundedFacts = [
|
|
2277
|
-
...priorityFacts.slice(0, 64),
|
|
2278
|
-
...otherFacts.slice(-32),
|
|
2279
|
-
].slice(0, 96).filter(fact => {
|
|
2280
|
-
const bytes = Buffer.byteLength(JSON.stringify(fact));
|
|
2281
|
-
if (previewBytes + bytes > 24_000)
|
|
2282
|
-
return false;
|
|
2283
|
-
previewBytes += bytes;
|
|
2284
|
-
return true;
|
|
2285
|
-
});
|
|
2286
|
-
return [
|
|
2287
|
-
"<frontend_plan_ledger>",
|
|
2288
|
-
"Preview of committed plan facts; not a replacement payload. Fields and lists may be omitted or shortened. Use read_plan_facts with kind, entryId/eventId and optional field to retrieve complete values in bounded pages before corrections; preserve all other bindings. Absence from this preview is not absence from the ledger.",
|
|
2289
|
-
JSON.stringify({ complete: false, matchingFacts: compactFacts.length, omittedFacts: compactFacts.length - boundedFacts.length }),
|
|
2290
|
-
JSON.stringify(boundedFacts),
|
|
2291
|
-
"</frontend_plan_ledger>",
|
|
2292
|
-
].join("\n");
|
|
2293
|
-
}
|
|
2294
|
-
export async function runFrontendReviewSegmentedSessions(input) {
|
|
2295
|
-
const protocol = input.tools.scopeProtocol;
|
|
2296
|
-
const terminalKinds = input.phase === "review" ? ["approve_review", "request_review_changes"] : ["approve_design", "request_design_changes"];
|
|
2297
|
-
const terminal = () => protocol.committedFacts().some(r => terminalKinds.includes(String(r.fact.kind)));
|
|
2298
|
-
const targetBytes = input.sessionOptions.frontendExecutionPolicy?.scopeTargetBytes ?? FRONTEND_SCOPE_TARGET_BYTES;
|
|
2299
|
-
const queue = packFrontendInputUnits(input.inventory.scopes.filter(s => !protocol.completedScopeIds().has(s.id)), { targetBytes, maxUnits: input.sessionOptions.frontendExecutionPolicy?.maxScopeUnits }).map(scopes => ({ scopes, repairs: 0 }));
|
|
2300
|
-
if (!queue.length)
|
|
2301
|
-
queue.push({ scopes: [], repairs: 0 });
|
|
2302
|
-
let calls = 0;
|
|
2303
|
-
let last = { ok: true, assistantText: "", command: [], durationMs: 0, exitCode: 0, failureCategory: "success", modelDisplay: "unknown", parsedEvents: 0, stderr: "", stdout: "", timedOut: false, attemptedModels: [], fallbackUsed: false, tokensUsed: 0 };
|
|
2304
|
-
for (let index = 0; index < queue.length; index++) {
|
|
2305
|
-
await input.tools.flush();
|
|
2306
|
-
await input.inventory.validate();
|
|
2307
|
-
if (terminal()) {
|
|
2308
|
-
protocol.assertComplete();
|
|
2309
|
-
return last;
|
|
2310
|
-
}
|
|
2311
|
-
const item = queue[index];
|
|
2312
|
-
const scopes = item.scopes.filter(s => !protocol.completedScopeIds().has(s.id));
|
|
2313
|
-
protocol.setActiveScope(scopes.map(s => s.id));
|
|
2314
|
-
const finalScope = input.inventory.scopes.every(s => protocol.completedScopeIds().has(s.id) || scopes.some(current => current.id === s.id));
|
|
2315
|
-
const customTools = finalScope ? input.customTools : input.customTools.filter(t => !terminalKinds.includes(String(t.name)));
|
|
2316
|
-
const prompt = `${input.basePrompt}\n<frontend_review_authority>\nThe Contract acceptance criteria, constraints, required deliverables, UI-state declarations, and verification expectations in the supplied input are authoritative. Review the actual diff and evidence against those facts; do not replace them with a Plan-derived interpretation.\n</frontend_review_authority>\n<frontend_review_scope>\n${JSON.stringify({ semantics: "full", inventoryDigest: input.inventory.digest, scopes, previouslyCompleted: [...protocol.completedScopeIds()], savedFindings: protocol.committedFacts().filter(r => String(r.fact.kind).endsWith("-finding")).map(r => ({ id: r.fact.id, finding: r.fact.finding })) })}\n</frontend_review_scope>\nReview the complete supplied scopes, saving each finding immediately. Call complete_review_scope for each exact id only after all its independent permissions, thresholds, errors and evidence have been checked. ${finalScope ? "After every scope is complete, make one independent overall approve/request_changes decision; persisted findings cannot be omitted." : "More scopes remain. Do not finalize or reread already completed scopes unless resolving a cross-scope issue."}`;
|
|
2317
|
-
if (Buffer.byteLength(prompt) > targetBytes && scopes.length > 1) {
|
|
2318
|
-
const at = Math.ceil(scopes.length / 2);
|
|
2319
|
-
queue.splice(index, 1, { scopes: scopes.slice(0, at), repairs: 0 }, { scopes: scopes.slice(at), repairs: 0 });
|
|
2320
|
-
index--;
|
|
2321
|
-
continue;
|
|
2322
|
-
}
|
|
2323
|
-
const completionInstruction = `${item.repairs ? "REPAIR: the prior session did not commit all required checkpoints/verdict. Do not repeat the review narrative. " : ""}Complete these exact runtime scope IDs using complete_review_scope: ${JSON.stringify(scopes.map(scope => scope.id))}. These are scope IDs, not node IDs. ${finalScope ? `After those checkpoints succeed, call exactly one terminal tool: ${terminalKinds.join(" or ")}. A prose conclusion is not a committed verdict.` : "More scopes remain; do not submit an overall verdict yet."}`;
|
|
2324
|
-
const userMessage = `${input.sessionOptions.userMessage}\n\n${completionInstruction}`;
|
|
2325
|
-
if (++calls > 128)
|
|
2326
|
-
return { ...last, ok: false, failureCategory: "frontend-input-exhausted", stderr: "FRONTEND_REVIEW_RECOVERY_EXHAUSTED: session quota reached" };
|
|
2327
|
-
last = await observeFrontendSession({ ...input.observation, phase: `${input.phase}/scope`, scopeIds: scopes.map(s => s.id), prompt, userMessage, customTools,
|
|
2328
|
-
artifactPath: input.observation?.artifactPath ? `${input.observation.artifactPath}-${calls}.json` : undefined,
|
|
2329
|
-
committedCount: () => protocol.committedFacts().length, durableCommittedCount: () => protocol.committedFacts().length,
|
|
2330
|
-
}, observer => input.piStepFn({ ...input.sessionOptions, prompt, userMessage, onAttemptObservation: observer, writerToolPolicy: { requireSdk: true, customTools } }));
|
|
2331
|
-
await input.tools.flush();
|
|
2332
|
-
await input.inventory.validate();
|
|
2333
|
-
if (last.timedOut || /interrupt|termination-unconfirmed|budget_breach/.test(last.failureCategory))
|
|
2334
|
-
return { ...last, ok: false };
|
|
2335
|
-
const capacity = readWriterThinkingExhaustionEvidence(last).stopReason === "length" || ["context-overflow", "context-budget-exhausted"].includes(last.failureCategory);
|
|
2336
|
-
const missing = scopes.filter(s => !protocol.completedScopeIds().has(s.id));
|
|
2337
|
-
if (capacity && (missing.length || !terminal() && finalScope)) {
|
|
2338
|
-
if (missing.length === 1 && scopes.length === 1 || !missing.length && !scopes.length)
|
|
2339
|
-
return { ...last, ok: false, failureCategory: "frontend-input-exhausted", stderr: "FRONTEND_INPUT_UNIT_TOO_LARGE: complete review unit or terminal exhausted" };
|
|
2340
|
-
const at = Math.ceil(missing.length / 2);
|
|
2341
|
-
const smaller = missing.length ? [missing.slice(0, at), missing.slice(at)].filter(s => s.length) : [[]];
|
|
2342
|
-
queue.splice(index, 1, ...smaller.map(scopes => ({ scopes, repairs: 0 })));
|
|
2343
|
-
index--;
|
|
2344
|
-
continue;
|
|
2345
|
-
}
|
|
2346
|
-
if (!last.ok && !capacity) {
|
|
2347
|
-
// Terminal-only completion: this protocol finishes through durable
|
|
2348
|
-
// tools (complete_review_scope -> approve_review/request_review_changes),
|
|
2349
|
-
// so the model can end its turn with no closing prose and the step
|
|
2350
|
-
// classifies as empty-output even though the review is complete
|
|
2351
|
-
// (smoke r27/r31/r32 committed the terminal and still reported
|
|
2352
|
-
// empty-output; the node then replayed the committed fact in an
|
|
2353
|
-
// extra attempt). Accept the segment here instead, so a completed
|
|
2354
|
-
// review neither spends a replay attempt nor depends on the retry
|
|
2355
|
-
// ladder, and a misclassified category (smoke r28 read a finding id
|
|
2356
|
-
// containing UNAUTHORIZED as an auth error, which is not retryable)
|
|
2357
|
-
// can no longer turn a committed review into a node failure.
|
|
2358
|
-
if (last.failureCategory === "empty-output" && terminal() && !missing.length) {
|
|
2359
|
-
try {
|
|
2360
|
-
await input.tools.flush();
|
|
2361
|
-
protocol.assertComplete();
|
|
2362
|
-
return { ...last, ok: true, failureCategory: "success" };
|
|
2363
|
-
}
|
|
2364
|
-
catch {
|
|
2365
|
-
// Unfinished scope checkpoints still fail this segment.
|
|
2366
|
-
}
|
|
2367
|
-
}
|
|
2368
|
-
return last;
|
|
2369
|
-
}
|
|
2370
|
-
if (missing.length || finalScope && !terminal()) {
|
|
2371
|
-
if (item.repairs >= 1)
|
|
2372
|
-
return { ...last, ok: false, failureCategory: "frontend-input-exhausted", stderr: "FRONTEND_REVIEW_SCOPE_INCOMPLETE: required checkpoint or verdict missing" };
|
|
2373
|
-
queue.splice(index, 1, { scopes: missing, repairs: item.repairs + 1 });
|
|
2374
|
-
index--;
|
|
2375
|
-
}
|
|
2376
|
-
}
|
|
2377
|
-
protocol.assertComplete();
|
|
2378
|
-
return terminal() ? { ...last, ok: true, failureCategory: "success" } : { ...last, ok: false, failureCategory: "frontend-input-exhausted", stderr: "FRONTEND_REVIEW_SCOPE_INCOMPLETE: missing independent verdict" };
|
|
2379
|
-
}
|
|
2380
|
-
export async function runFrontendScoutSegmentedSessions(input) {
|
|
2381
|
-
const inventory = parseFrontendInputBlock(input.basePrompt, "scout");
|
|
2382
|
-
if (!inventory)
|
|
2383
|
-
throw Error("FRONTEND_INPUT_MISSING: Scout compiled inventory unavailable");
|
|
2384
|
-
const units = collectFrontendExecutionGroups(inventory.payload.requirements).map(group => ({ ...group, id: `${group.kind === "unclassified" ? "requirement" : "group"}:${group.id}`, requirements: inventory.payload.requirements.filter(r => group.requirementIds.includes(r.id)) }));
|
|
2385
|
-
const targetBytes = input.sessionOptions.frontendExecutionPolicy?.scopeTargetBytes ?? FRONTEND_SCOPE_TARGET_BYTES;
|
|
2386
|
-
const queue = packFrontendInputUnits(units, { targetBytes, maxUnits: input.sessionOptions.frontendExecutionPolicy?.maxScopeUnits }).map(batch => ({ groups: batch, repairs: 0 }));
|
|
2387
|
-
let last = { ok: true, assistantText: "", command: [], durationMs: 0, exitCode: 0, failureCategory: "success", modelDisplay: input.sessionOptions.modelConfig?.model ?? "unknown", parsedEvents: 0, stderr: "", stdout: "", timedOut: false, attemptedModels: [], fallbackUsed: false, tokensUsed: 0 };
|
|
2388
|
-
let calls = 0;
|
|
2389
|
-
for (let index = 0; index < queue.length; index++) {
|
|
2390
|
-
await input.tools.flush();
|
|
2391
|
-
const item = queue[index];
|
|
2392
|
-
const completed = input.tools.completedRequirementIds();
|
|
2393
|
-
const groups = item.groups.filter(g => g.requirementIds.some(id => !completed.has(id)));
|
|
2394
|
-
if (!groups.length)
|
|
2395
|
-
continue;
|
|
2396
|
-
const ids = groups.flatMap(g => g.requirementIds);
|
|
2397
|
-
const scopeId = input.tools.setActiveScope(ids);
|
|
2398
|
-
const prompt = input.basePrompt.replace(inventory.block, `<frontend_scout_input>\n${JSON.stringify(projectFrontendInputScope(inventory.payload, ids))}\n</frontend_scout_input>`) +
|
|
2399
|
-
`\nSCOUT SCOPE ${scopeId}: discover the related surfaces for exactly these complete obligations: ${ids.join(", ")}. Reuse proven paths from completed scope navigation; do not reread their content unless relevant new evidence is needed. Submit record_target_surface with scopeId="${scopeId}" after all discovery/design evidence for this scope. completeness=complete closes only this scope; runtime merges all scopes.\n` +
|
|
2400
|
-
JSON.stringify({ completedScopePaths: input.tools.completedScopeFacts().map(f => ({ id: f.id, requirementIds: f.requirementIds, paths: f.surface?.implementationPaths })) });
|
|
2401
|
-
const envelopeBytes = Buffer.byteLength(prompt + input.sessionOptions.userMessage + JSON.stringify(input.customTools.map(t => { const tool = t; return { name: tool.name, description: tool.description, parameters: tool.parameters }; })));
|
|
2402
|
-
if (envelopeBytes > targetBytes && groups.length > 1) {
|
|
2403
|
-
const at = Math.ceil(groups.length / 2);
|
|
2404
|
-
queue.splice(index, 1, { groups: groups.slice(0, at), repairs: 0 }, { groups: groups.slice(at), repairs: 0 });
|
|
2405
|
-
index--;
|
|
2406
|
-
continue;
|
|
2407
|
-
}
|
|
2408
|
-
if (++calls > 128)
|
|
2409
|
-
return { ...last, ok: false, failureCategory: "frontend-input-exhausted", stderr: "SCOUT_RECOVERY_EXHAUSTED: shared session quota reached" };
|
|
2410
|
-
last = await observeFrontendSession({ ...input.observation, phase: "scout/scope", scopeIds: ids, prompt, userMessage: input.sessionOptions.userMessage, customTools: input.customTools,
|
|
2411
|
-
artifactPath: input.observation?.artifactPath ? `${input.observation.artifactPath}-${calls}.json` : undefined,
|
|
2412
|
-
committedCount: () => input.tools.committedFacts().length, durableCommittedCount: () => input.tools.committedFacts().length,
|
|
2413
|
-
}, observer => input.piStepFn({ ...input.sessionOptions, prompt, onAttemptObservation: observer, writerToolPolicy: { requireSdk: true, customTools: input.customTools } }));
|
|
2414
|
-
await input.tools.flush();
|
|
2415
|
-
const missing = groups.filter(g => g.requirementIds.some(id => !input.tools.completedRequirementIds().has(id)));
|
|
2416
|
-
if (last.timedOut || /interrupt|termination-unconfirmed|budget_breach/.test(last.failureCategory))
|
|
2417
|
-
return { ...last, ok: false };
|
|
2418
|
-
const capacity = readWriterThinkingExhaustionEvidence(last).stopReason === "length" || ["context-overflow", "context-budget-exhausted"].includes(last.failureCategory);
|
|
2419
|
-
if (capacity && missing.length) {
|
|
2420
|
-
if (missing.length === 1 && groups.length === 1)
|
|
2421
|
-
return { ...last, ok: false, failureCategory: "frontend-input-exhausted", stderr: `${last.stderr}\nFRONTEND_INPUT_UNIT_TOO_LARGE: Scout ${missing[0].id}; refine the complete source unit; unchanged retries disabled` };
|
|
2422
|
-
const at = Math.ceil(missing.length / 2);
|
|
2423
|
-
queue.splice(index, 1, ...[missing.slice(0, at), missing.slice(at)].filter(batch => batch.length).map(batch => ({ groups: batch, repairs: 0 })));
|
|
2424
|
-
index--;
|
|
2425
|
-
continue;
|
|
2426
|
-
}
|
|
2427
|
-
if (!last.ok && !capacity)
|
|
2428
|
-
return last;
|
|
2429
|
-
if (missing.length) {
|
|
2430
|
-
if (item.repairs >= 1)
|
|
2431
|
-
return { ...last, ok: false, failureCategory: "frontend-input-exhausted", stderr: "SCOUT_SCOPE_INCOMPLETE: unresolved discovery remains after local correction" };
|
|
2432
|
-
queue.splice(index, 1, { groups: missing, repairs: item.repairs + 1 });
|
|
2433
|
-
index--;
|
|
2434
|
-
continue;
|
|
2435
|
-
}
|
|
2436
|
-
}
|
|
2437
|
-
await input.tools.flush();
|
|
2438
|
-
const { readCompleteScoutTargetSurface } = await import("../workflows/dag/frontend-committed-facts.js");
|
|
2439
|
-
const closure = readCompleteScoutTargetSurface(input.tools.committedFacts());
|
|
2440
|
-
return closure.ok ? { ...last, ok: true, failureCategory: "success" } : { ...last, ok: false, failureCategory: "frontend-input-exhausted", stderr: closure.reason };
|
|
2441
|
-
}
|
|
2442
|
-
export async function runFrontendContractSegmentedSessions(input) {
|
|
2443
|
-
// Requirement identity, text, and source fragments are frozen in the
|
|
2444
|
-
// runtime ledger. Seed those facts once; model sessions spend their budget
|
|
2445
|
-
// on execution grouping, evidence expectations, and genuine decisions.
|
|
2446
|
-
await input.tools.seedCanonicalRequirements();
|
|
2447
|
-
// Build from the frozen runtime inventory if the caller has not rendered it yet.
|
|
2448
|
-
const basePrompt = parseFrontendInputBlock(input.basePrompt, "contract") ? input.basePrompt : input.basePrompt +
|
|
2449
|
-
`\n<frontend_contract_input>\nFrozen complete source obligations.\n${JSON.stringify({ requirements: input.tools.inputRequirements() })}\n</frontend_contract_input>`;
|
|
2450
|
-
const inventory = parseFrontendInputBlock(basePrompt, "contract");
|
|
2451
|
-
const pending = inventory.payload.requirements.filter(r => !input.tools.completedScopeRequirementIds().has(r.id));
|
|
2452
|
-
const batches = packFrontendInputUnits(pending, { targetBytes: input.sessionOptions.frontendExecutionPolicy?.scopeTargetBytes, maxUnits: input.sessionOptions.frontendExecutionPolicy?.maxScopeUnits });
|
|
2453
|
-
if (!batches.length)
|
|
2454
|
-
batches.push([]);
|
|
2455
|
-
let last;
|
|
2456
|
-
let invocation = 0;
|
|
2457
|
-
const maxSessions = batches.length * 3 + 2;
|
|
2458
|
-
const terminal = () => input.tools.committedFacts().some(r => r.fact.kind === "contract-finalized");
|
|
2459
|
-
try {
|
|
2460
|
-
for (let index = 0; index < batches.length; index += 1) {
|
|
2461
|
-
let scopeIds = batches[index].map(r => r.id);
|
|
2462
|
-
const finalScope = index === batches.length - 1;
|
|
2463
|
-
for (let repair = 0; repair < 2; repair += 1) {
|
|
2464
|
-
// Confirmed IDs remain visible until the model commits a scope checkpoint.
|
|
2465
|
-
const completed = input.tools.completedScopeRequirementIds();
|
|
2466
|
-
scopeIds = scopeIds.filter(id => !completed.has(id));
|
|
2467
|
-
input.tools.setActiveRequirementScope(scopeIds);
|
|
2468
|
-
const groupIndex = collectFrontendExecutionGroups(input.tools.committedFacts().filter(r => r.fact.kind === "requirement").map(r => ({ id: String(r.fact.id), execution: r.fact.execution })));
|
|
2469
|
-
const shared = input.tools.committedFacts().filter(r => !["requirement", "contract-finalized", "contract-scope-completed"].includes(String(r.fact.kind))).map(r => r.fact);
|
|
2470
|
-
const prompt = projectFrontendContractPrompt(basePrompt, scopeIds) +
|
|
2471
|
-
`\nCONTRACT SCOPE: analyze only ${scopeIds.join(", ") || "(all scopes complete; verify global facts and terminal)"}. Requirement identity/text/source fragments are already committed by runtime; do not call record_requirement. Use record_requirement_execution for model-owned execution grouping, then submit decisions and evidence records. Call complete_contract_scope after ALL decisions for this scope. ` +
|
|
2472
|
-
(finalScope ? "After complete scope coverage and source-bound deliverables, call finalize_contract. Correct rejected calls and retry." : "Do not finalize; subsequent complete scopes remain.") +
|
|
2473
|
-
`\n<committed_contract_facts>\n${JSON.stringify({ facts: shared, executionGroups: groupIndex })}\n</committed_contract_facts>`;
|
|
2474
|
-
const customTools = input.tools.customTools.filter(t => finalScope || t.name !== "finalize_contract");
|
|
2475
|
-
invocation += 1;
|
|
2476
|
-
if (invocation > maxSessions)
|
|
2477
|
-
return { ...last, ok: false, failureCategory: "invalid-output", stderr: "CONTRACT_RECOVERY_EXHAUSTED: session quota exceeded" };
|
|
2478
|
-
const committedBefore = input.tools.committedFacts().length;
|
|
2479
|
-
last = await observeFrontendSession({
|
|
2480
|
-
...input.observation, phase: "contract/scope", scopeIds, prompt, userMessage: input.sessionOptions.userMessage, customTools,
|
|
2481
|
-
artifactPath: input.observation?.artifactPath ? `${input.observation.artifactPath}-${invocation}.json` : undefined,
|
|
2482
|
-
committedCount: () => input.tools.committedFacts().length, durableCommittedCount: () => input.tools.committedFacts().length,
|
|
2483
|
-
}, observer => input.piStepFn({ ...input.sessionOptions, prompt, onAttemptObservation: observer, writerToolPolicy: { requireSdk: true, customTools } }));
|
|
2484
|
-
await input.tools.flush();
|
|
2485
|
-
if (last.timedOut || /interrupt|termination-unconfirmed|budget_breach/.test(last.failureCategory))
|
|
2486
|
-
return { ...last, ok: false };
|
|
2487
|
-
const capacityExhausted = readWriterThinkingExhaustionEvidence(last).stopReason === "length" || ["context-overflow", "context-budget-exhausted"].includes(last.failureCategory);
|
|
2488
|
-
if (capacityExhausted && !last.timedOut) {
|
|
2489
|
-
const missing = scopeIds.filter(id => !input.tools.completedScopeRequirementIds().has(id));
|
|
2490
|
-
if (!missing.length) {
|
|
2491
|
-
if (finalScope && !terminal()) {
|
|
2492
|
-
if (!scopeIds.length)
|
|
2493
|
-
return { ...last, ok: false, failureCategory: "invalid-output", stderr: `${last.stderr}\nFRONTEND_INPUT_UNIT_TOO_LARGE: contract terminal exhausted; unchanged retry disabled` };
|
|
2494
|
-
batches.push([]);
|
|
2495
|
-
}
|
|
2496
|
-
break;
|
|
2497
|
-
}
|
|
2498
|
-
if (missing.length === 1 && scopeIds.length > 1) {
|
|
2499
|
-
batches.splice(index, 1, inventory.payload.requirements.filter(r => missing.includes(r.id)));
|
|
2500
|
-
index -= 1;
|
|
2501
|
-
break;
|
|
2502
|
-
}
|
|
2503
|
-
if (missing.length <= 1)
|
|
2504
|
-
return { ...last, ok: false, stderr: `${last.stderr}\nFRONTEND_INPUT_UNIT_TOO_LARGE: ${missing[0] ?? "contract terminal"}; atom could not complete; refine the source without dropping conditions` };
|
|
2505
|
-
const smaller = packFrontendInputUnits(inventory.payload.requirements.filter(r => missing.includes(r.id)), { maxUnits: Math.ceil(missing.length / 2) });
|
|
2506
|
-
batches.splice(index, 1, ...smaller);
|
|
2507
|
-
index -= 1;
|
|
2508
|
-
break;
|
|
2509
|
-
}
|
|
2510
|
-
// A session that ends with blank assistant text but committed new
|
|
2511
|
-
// facts is not a node failure: small-output models legitimately
|
|
2512
|
-
// stop after their tool calls. Continue so the scope/repair checks
|
|
2513
|
-
// below decide, instead of burning a full node retry.
|
|
2514
|
-
const committedFactsOnlySuccess = !(last.assistantText ?? "").trim() &&
|
|
2515
|
-
!last.stderr.trim() &&
|
|
2516
|
-
input.tools.committedFacts().length > committedBefore;
|
|
2517
|
-
if (!last.ok && !committedFactsOnlySuccess)
|
|
2518
|
-
return last;
|
|
2519
|
-
if (committedFactsOnlySuccess)
|
|
2520
|
-
last = { ...last, ok: true, failureCategory: "success" };
|
|
2521
|
-
const missing = scopeIds.filter(id => !input.tools.completedScopeRequirementIds().has(id));
|
|
2522
|
-
if (!missing.length && (!finalScope || terminal()))
|
|
2523
|
-
break;
|
|
2524
|
-
if (repair === 1)
|
|
2525
|
-
return { ...last, ok: false, failureCategory: "invalid-output", stderr: `CONTRACT_SCOPE_INCOMPLETE: ${missing.join(", ") || "missing finalize_contract"}` };
|
|
2526
|
-
}
|
|
2527
|
-
}
|
|
2528
|
-
return last;
|
|
2529
|
-
}
|
|
2530
|
-
finally {
|
|
2531
|
-
input.tools.setActiveRequirementScope(null);
|
|
2532
|
-
}
|
|
2533
|
-
}
|
|
2534
1701
|
/** One workload view for both the outer parallel map and inner session queue.
|
|
2535
1702
|
* A declared execution group is an indivisible unit during initial packing. */
|
|
2536
|
-
function buildFrontendPlanWorkload(input) {
|
|
2537
|
-
const allRequirementIds = input.requirementIds;
|
|
2538
|
-
const compiledInput = parseFrontendInputBlock(input.basePrompt, "plan")?.payload;
|
|
2539
|
-
const fullUnits = new Map(compiledInput?.requirements.map(r => [r.id, r]) ?? []);
|
|
2540
|
-
const declaredGroups = Array.isArray(compiledInput?.executionGroups)
|
|
2541
|
-
? compiledInput.executionGroups.filter(isRecordObject) : [];
|
|
2542
|
-
const workGroups = declaredGroups.map(g => ({
|
|
2543
|
-
id: String(g.id), kind: String(g.kind),
|
|
2544
|
-
requirementIds: Array.isArray(g.requirementIds)
|
|
2545
|
-
? g.requirementIds.filter((id) => typeof id === "string" && allRequirementIds.includes(id)) : [],
|
|
2546
|
-
})).filter(g => g.requirementIds.length);
|
|
2547
|
-
const groupedIds = new Set(workGroups.flatMap(g => g.requirementIds));
|
|
2548
|
-
for (const id of allRequirementIds) {
|
|
2549
|
-
if (!groupedIds.has(id))
|
|
2550
|
-
workGroups.push({ id, kind: "unclassified", requirementIds: [id] });
|
|
2551
|
-
}
|
|
2552
|
-
const workCost = (ids) => 1 + ids.reduce((total, id) => total + Math.max(1, (input.requirementCosts?.get(id) ?? 2) - 1), 0);
|
|
2553
|
-
const policy = input.sessionOptions.frontendExecutionPolicy;
|
|
2554
|
-
const targetBytes = policy?.scopeTargetBytes ?? FRONTEND_SCOPE_TARGET_BYTES;
|
|
2555
|
-
const buildWorkBatches = (ids) => {
|
|
2556
|
-
const work = workGroups.flatMap((g, index) => {
|
|
2557
|
-
const members = g.requirementIds.filter(id => ids.includes(id));
|
|
2558
|
-
return members.length ? [{ id: `${index}:${g.id}`, requirementIds: members,
|
|
2559
|
-
requirements: members.map(id => fullUnits.get(id) ?? { id }), estimatedCalls: workCost(members) }] : [];
|
|
2560
|
-
});
|
|
2561
|
-
// Pack complete work units, not the repeated request scaffold. Deducting
|
|
2562
|
-
// fixed context/tools can leave a one-byte budget and force one AC per
|
|
2563
|
-
// session without reducing that overhead. The SDK checks the actual
|
|
2564
|
-
// request against model capacity; capacity recovery splits unfinished work.
|
|
2565
|
-
return packFrontendInputUnits(work, {
|
|
2566
|
-
targetBytes,
|
|
2567
|
-
maxUnits: policy?.maxScopeUnits ?? 4,
|
|
2568
|
-
maxCost: FRONTEND_PLAN_COVERAGE_MAX_RECORD_CALLS, cost: g => g.estimatedCalls,
|
|
2569
|
-
}).map(batch => batch.flatMap(g => g.requirementIds));
|
|
2570
|
-
};
|
|
2571
|
-
// This renderer also carries the Plan's evolving UX checkpoint on resume.
|
|
2572
|
-
// Only the Contract/Scout-derived input participates in ownership binding.
|
|
2573
|
-
const { committedUx: _committedUx, ...frozenInput } = compiledInput ?? {};
|
|
2574
|
-
return {
|
|
2575
|
-
frozenInput, workGroups, buildWorkBatches,
|
|
2576
|
-
compactEligible: input.requirementCosts !== undefined &&
|
|
2577
|
-
workGroups.length <= FRONTEND_PLAN_SMALL_MAX_REQUIREMENTS &&
|
|
2578
|
-
workGroups.reduce((total, group) => total + workCost(group.requirementIds), 0) <= FRONTEND_PLAN_SMALL_MAX_ESTIMATED_CALLS &&
|
|
2579
|
-
countFrontendPlanTargetSurfaces(input.basePrompt) === 1,
|
|
2580
|
-
};
|
|
2581
|
-
}
|
|
2582
1703
|
/**
|
|
2583
1704
|
* Requirement ids the plan coverage layout must own, in first-commit order.
|
|
2584
1705
|
*
|
|
@@ -2591,1005 +1712,8 @@ function buildFrontendPlanWorkload(input) {
|
|
|
2591
1712
|
* FRONTEND_PLAN_LAYOUT_INVALID with failureCategory tool-policy - killing the run
|
|
2592
1713
|
* at the plan node over a completely ordinary ledger.
|
|
2593
1714
|
*/
|
|
2594
|
-
export function collectFrontendPlanRequirementIds(contractFacts) {
|
|
2595
|
-
return [
|
|
2596
|
-
...new Set(contractFacts
|
|
2597
|
-
.filter((record) => record.fact?.kind ===
|
|
2598
|
-
"requirement")
|
|
2599
|
-
.map((record) => record.fact?.id)
|
|
2600
|
-
.filter((id) => typeof id === "string")),
|
|
2601
|
-
];
|
|
2602
|
-
}
|
|
2603
|
-
const frontendPlanCoverageLayoutSchema = z.object({
|
|
2604
|
-
schemaVersion: z.literal(1),
|
|
2605
|
-
bindingSha256: z.string(),
|
|
2606
|
-
coverageBatches: z.array(z.array(z.string().min(1)).min(1)).min(1),
|
|
2607
|
-
layoutSha256: z.string(),
|
|
2608
|
-
}).strict();
|
|
2609
1715
|
/** Freeze ledger ownership before any provider call. Session packing remains
|
|
2610
1716
|
* adaptive inside each owner, but retry instructions cannot rename its scope. */
|
|
2611
|
-
async function loadOrCreateFrontendPlanCoverageLayout(input) {
|
|
2612
|
-
const fileName = "coverage-layout.json";
|
|
2613
|
-
const file = path.join(input.runDir, input.nodeId, fileName);
|
|
2614
|
-
const bindingSha256 = sha256OfCanonicalJson(input.binding);
|
|
2615
|
-
let raw;
|
|
2616
|
-
try {
|
|
2617
|
-
raw = await readFile(file, "utf8");
|
|
2618
|
-
}
|
|
2619
|
-
catch (error) {
|
|
2620
|
-
if (error.code !== "ENOENT")
|
|
2621
|
-
throw error;
|
|
2622
|
-
}
|
|
2623
|
-
let layout;
|
|
2624
|
-
if (raw !== undefined) {
|
|
2625
|
-
layout = frontendPlanCoverageLayoutSchema.parse(JSON.parse(raw));
|
|
2626
|
-
}
|
|
2627
|
-
else {
|
|
2628
|
-
// Losing the ownership receipt must never repartition acknowledged facts.
|
|
2629
|
-
for (const relative of ["plan-typed-facts.jsonl", "parallel"]) {
|
|
2630
|
-
const existing = await stat(path.join(input.runDir, input.nodeId, relative)).catch(error => {
|
|
2631
|
-
if (error.code !== "ENOENT")
|
|
2632
|
-
throw error;
|
|
2633
|
-
return undefined;
|
|
2634
|
-
});
|
|
2635
|
-
if (existing && (existing.isDirectory() || existing.size > 0)) {
|
|
2636
|
-
throw Error("FRONTEND_PLAN_LAYOUT_MISSING: preserve the existing ledger and restart its owning phase");
|
|
2637
|
-
}
|
|
2638
|
-
}
|
|
2639
|
-
const descriptor = {
|
|
2640
|
-
schemaVersion: 1, bindingSha256,
|
|
2641
|
-
coverageBatches: input.workload.compactEligible ? [input.requirementIds] : input.workload.buildWorkBatches(input.requirementIds),
|
|
2642
|
-
};
|
|
2643
|
-
layout = { ...descriptor, layoutSha256: sha256OfCanonicalJson(descriptor) };
|
|
2644
|
-
}
|
|
2645
|
-
const { layoutSha256, ...descriptor } = layout;
|
|
2646
|
-
if (layout.bindingSha256 !== bindingSha256 || layoutSha256 !== sha256OfCanonicalJson(descriptor)) {
|
|
2647
|
-
throw Error("FRONTEND_PLAN_LAYOUT_MISMATCH: frozen coverage ownership or input binding changed");
|
|
2648
|
-
}
|
|
2649
|
-
const members = layout.coverageBatches.flat();
|
|
2650
|
-
const owners = new Map(layout.coverageBatches.flatMap((batch, index) => batch.map(id => [id, index])));
|
|
2651
|
-
if (members.length !== owners.size || members.length !== input.requirementIds.length ||
|
|
2652
|
-
input.requirementIds.some(id => !owners.has(id)) ||
|
|
2653
|
-
input.workload.workGroups.some(group => new Set(group.requirementIds.map(id => owners.get(id))).size !== 1)) {
|
|
2654
|
-
throw Error("FRONTEND_PLAN_LAYOUT_INVALID: coverage must partition complete execution groups exactly once");
|
|
2655
|
-
}
|
|
2656
|
-
if (raw === undefined)
|
|
2657
|
-
await writeDagNodeJsonArtifact(input.runDir, input.nodeId, fileName, layout);
|
|
2658
|
-
return layout.coverageBatches;
|
|
2659
|
-
}
|
|
2660
|
-
export async function runFrontendPlanSegmentedSessions(input) {
|
|
2661
|
-
const queue = [];
|
|
2662
|
-
const coverageSegment = FRONTEND_PLAN_SEGMENTS.find((segment) => segment.id === "coverage");
|
|
2663
|
-
const uxRegistrySegment = FRONTEND_PLAN_SEGMENTS.find((segment) => segment.id === "ux-registry");
|
|
2664
|
-
const uxSegment = FRONTEND_PLAN_SEGMENTS.find((segment) => segment.id === "ux-local");
|
|
2665
|
-
const globalMockDataSegment = FRONTEND_PLAN_SEGMENTS.find((segment) => segment.id === "global-mock-data");
|
|
2666
|
-
const allRequirementIds = input.requirementIds ?? [];
|
|
2667
|
-
const buildPhasePrompt = (segment, missing = [], scopeIds = allRequirementIds) => {
|
|
2668
|
-
const compact = allRequirementIds.length > 0
|
|
2669
|
-
? compactFrontendPlanPromptForRequirementSlice(input.basePrompt, scopeIds, {
|
|
2670
|
-
includeRequirementText: segment.id === "global-mock-data" || segment.id === "finalize",
|
|
2671
|
-
includeVerificationTargets: false,
|
|
2672
|
-
includeDesignEvidence: segment.id === "global-dependency-deviation" || segment.id === "finalize",
|
|
2673
|
-
includeChecklist: false,
|
|
2674
|
-
})
|
|
2675
|
-
: input.basePrompt;
|
|
2676
|
-
const kinds = segment.id === "global-route"
|
|
2677
|
-
? ["target-surface"]
|
|
2678
|
-
: segment.id === "global-mock-data"
|
|
2679
|
-
? ["plan-requirement", "plan-verification-target", "state-flow", "data-flow", "mock-api"]
|
|
2680
|
-
: segment.id === "global-dependency-deviation"
|
|
2681
|
-
? ["dependency", "design-deviation"]
|
|
2682
|
-
: ["plan-requirement", "plan-verification-target", "state-registry", "component-choice", "state-flow", "data-flow", "mock-api", "design-deviation", "dependency", "target-surface"];
|
|
2683
|
-
const ledger = input.committedFacts
|
|
2684
|
-
? compactFrontendPlanLedgerContext({
|
|
2685
|
-
committedFacts: input.committedFacts(),
|
|
2686
|
-
requirementIds: scopeIds,
|
|
2687
|
-
kinds,
|
|
2688
|
-
...(segment.id === "global-mock-data" ? { scopedKinds: ["state-flow"] } : {}),
|
|
2689
|
-
})
|
|
2690
|
-
: "";
|
|
2691
|
-
return [
|
|
2692
|
-
compact,
|
|
2693
|
-
segment.instruction,
|
|
2694
|
-
ledger,
|
|
2695
|
-
...(missing.length > 0
|
|
2696
|
-
? [
|
|
2697
|
-
"MISSING-FACT QUEUE: repair ONLY these items, then re-check the phase:",
|
|
2698
|
-
...missing.map((item) => `- ${item.kind}${item.id ? ` ${item.id}` : ""} for ${item.requirementIds.join(", ")}: ${item.reason}`),
|
|
2699
|
-
]
|
|
2700
|
-
: []),
|
|
2701
|
-
].filter(Boolean).join("\n\n");
|
|
2702
|
-
};
|
|
2703
|
-
const buildCoveragePrompt = (slice, missing = []) => [
|
|
2704
|
-
compactFrontendPlanPromptForRequirementSlice(input.basePrompt, slice),
|
|
2705
|
-
coverageSegment.instruction,
|
|
2706
|
-
`COVERAGE BATCH: process ONLY these requirements in this session: ${slice.join(", ")}. Other requirements are handled by separate sessions; do not record them. Finish the whole list before concluding: commit every listed requirement's coverage facts (verification targets or an evidence gap), batching up to 4 record_* calls per message; an early stop re-queues the remainder as a MISSING-FACT repair session.`,
|
|
2707
|
-
...(missing.length > 0
|
|
2708
|
-
? [
|
|
2709
|
-
"MISSING-FACT QUEUE: the previous session did not establish complete coverage. Repair ONLY these items, then re-check the slice:",
|
|
2710
|
-
...missing.map((item) => `- ${item.kind}${item.id ? ` ${item.id}` : ""} for ${item.requirementIds.join(", ")}: ${item.reason}`),
|
|
2711
|
-
]
|
|
2712
|
-
: []),
|
|
2713
|
-
].join("\n\n");
|
|
2714
|
-
const buildUxPrompt = (slice, missing = []) => {
|
|
2715
|
-
const ledger = input.committedFacts
|
|
2716
|
-
? compactFrontendPlanLedgerContext({
|
|
2717
|
-
committedFacts: input.committedFacts(),
|
|
2718
|
-
requirementIds: slice,
|
|
2719
|
-
kinds: [
|
|
2720
|
-
"plan-requirement",
|
|
2721
|
-
"plan-verification-target",
|
|
2722
|
-
"state-registry",
|
|
2723
|
-
"component-choice",
|
|
2724
|
-
"state-flow",
|
|
2725
|
-
],
|
|
2726
|
-
// Include shared state bindings from other scopes; page full values before correcting.
|
|
2727
|
-
})
|
|
2728
|
-
: "";
|
|
2729
|
-
return [
|
|
2730
|
-
compactFrontendPlanPromptForRequirementSlice(input.basePrompt, slice),
|
|
2731
|
-
uxSegment.instruction,
|
|
2732
|
-
`UX SCOPE: process these complete behavior groups together: ${slice.join(", ")}. Reuse shared registry/component ownership; do not rename a behavior by requirement id. Constraint/exclusion groups do not create UI; preserve genuine verification gaps.`,
|
|
2733
|
-
ledger,
|
|
2734
|
-
...(missing.length > 0
|
|
2735
|
-
? [
|
|
2736
|
-
"MISSING-FACT QUEUE: repair ONLY these items, then re-check this UX slice:",
|
|
2737
|
-
...missing.map((item) => `- ${item.kind}${item.id ? ` ${item.id}` : ""} for ${item.requirementIds.join(", ")}: ${item.reason}`),
|
|
2738
|
-
]
|
|
2739
|
-
: []),
|
|
2740
|
-
].filter(Boolean).join("\n\n");
|
|
2741
|
-
};
|
|
2742
|
-
const buildUxRegistryPrompt = (missing = []) => {
|
|
2743
|
-
const ledger = input.committedFacts
|
|
2744
|
-
? compactFrontendPlanLedgerContext({
|
|
2745
|
-
committedFacts: input.committedFacts(),
|
|
2746
|
-
requirementIds: allRequirementIds,
|
|
2747
|
-
kinds: [
|
|
2748
|
-
"plan-requirement",
|
|
2749
|
-
"plan-verification-target",
|
|
2750
|
-
"state-registry",
|
|
2751
|
-
],
|
|
2752
|
-
})
|
|
2753
|
-
: "";
|
|
2754
|
-
const compact = allRequirementIds.length > 0
|
|
2755
|
-
? compactFrontendPlanPromptForRequirementSlice(input.basePrompt, allRequirementIds, {
|
|
2756
|
-
includeRequirementText: false,
|
|
2757
|
-
includeVerificationTargets: false,
|
|
2758
|
-
includeDesignEvidence: false,
|
|
2759
|
-
includeChecklist: false,
|
|
2760
|
-
})
|
|
2761
|
-
: input.basePrompt;
|
|
2762
|
-
return [
|
|
2763
|
-
compact,
|
|
2764
|
-
uxRegistrySegment.instruction,
|
|
2765
|
-
ledger,
|
|
2766
|
-
...(missing.length > 0
|
|
2767
|
-
? [
|
|
2768
|
-
"MISSING-FACT QUEUE: commit the global UX registry, then re-check this phase:",
|
|
2769
|
-
...missing.map((item) => `- ${item.reason}`),
|
|
2770
|
-
]
|
|
2771
|
-
: []),
|
|
2772
|
-
].filter(Boolean).join("\n\n");
|
|
2773
|
-
};
|
|
2774
|
-
const buildCompactLocalPrompt = (missing = []) => {
|
|
2775
|
-
const ledger = input.committedFacts
|
|
2776
|
-
? compactFrontendPlanLedgerContext({
|
|
2777
|
-
committedFacts: input.committedFacts(),
|
|
2778
|
-
requirementIds: allRequirementIds,
|
|
2779
|
-
kinds: [
|
|
2780
|
-
"plan-requirement",
|
|
2781
|
-
"plan-verification-target",
|
|
2782
|
-
"state-registry",
|
|
2783
|
-
"component-choice",
|
|
2784
|
-
"state-flow",
|
|
2785
|
-
],
|
|
2786
|
-
})
|
|
2787
|
-
: "";
|
|
2788
|
-
return [
|
|
2789
|
-
compactFrontendPlanPromptForRequirementSlice(input.basePrompt, allRequirementIds),
|
|
2790
|
-
"PLAN PHASE — compact local planning for a small frontend request.",
|
|
2791
|
-
"Review all listed requirements together and record_state_registry first with one global UX vocabulary. Then record every plan-requirement and verification-target fact, followed by component/state-flow facts and the minimal route, Mock/data, dependency, and design-deviation policy facts needed by the observable behavior. Do not read the repository or task source; use only the committed input above. Do not call finalize_plan in this session.",
|
|
2792
|
-
"TOOL-FIRST: your first assistant actions must be record_* tool calls, at most 2-3 facts per message. Do not draft the whole analysis before recording; if a fact is uncertain, record it with an evidence gap instead of reasoning longer.",
|
|
2793
|
-
ledger,
|
|
2794
|
-
...(missing.length > 0
|
|
2795
|
-
? [
|
|
2796
|
-
"MISSING-FACT QUEUE: repair ONLY these items, then re-check the compact local phase:",
|
|
2797
|
-
...missing.map((item) => `- ${item.kind}${item.id ? ` ${item.id}` : ""} for ${item.requirementIds.join(", ")}: ${item.reason}`),
|
|
2798
|
-
]
|
|
2799
|
-
: []),
|
|
2800
|
-
].filter(Boolean).join("\n\n");
|
|
2801
|
-
};
|
|
2802
|
-
const mapPlannerExhaustion = (r, committedAnyFacts) => isPlannerThinkingExhausted(r, committedAnyFacts)
|
|
2803
|
-
? {
|
|
2804
|
-
...r,
|
|
2805
|
-
failureCategory: OUTPUT_LIMIT_RETRY_CATEGORY,
|
|
2806
|
-
stderr: `${r.stderr}\n${OUTPUT_LIMIT_RETRY_CATEGORY}: stopReason=length ended the turn before the next typed fact; preserve committed facts and retry only the unfinished phase`.trim(),
|
|
2807
|
-
}
|
|
2808
|
-
: r;
|
|
2809
|
-
// An empty list means "ledger unreadable / unknown" and falls back to one
|
|
2810
|
-
// unscoped coverage session; a non-empty list enables deterministic sharding.
|
|
2811
|
-
const requirementIdsProvided = input.requirementIds !== undefined && input.requirementIds.length > 0;
|
|
2812
|
-
const pending = (input.requirementIds ?? []).filter((id) => !input.committedRequirementIds?.().has(id));
|
|
2813
|
-
const initialMissing = requirementIdsProvided && input.committedFacts
|
|
2814
|
-
? collectFrontendPlanMissingFacts({
|
|
2815
|
-
requirementIds: input.requirementIds,
|
|
2816
|
-
committedFacts: input.committedFacts(),
|
|
2817
|
-
})
|
|
2818
|
-
: [];
|
|
2819
|
-
const incompleteRequirementIds = new Set(initialMissing.flatMap((item) => item.requirementIds));
|
|
2820
|
-
const coverageWorkIds = (input.requirementIds ?? []).filter((id) => pending.includes(id) || incompleteRequirementIds.has(id));
|
|
2821
|
-
if (input.parallelCoverageOnly === true &&
|
|
2822
|
-
requirementIdsProvided &&
|
|
2823
|
-
coverageWorkIds.length === 0) {
|
|
2824
|
-
// A retry may reopen a shard whose committed facts are already complete.
|
|
2825
|
-
// Treat that shard as an idempotent no-op; returning the normal empty
|
|
2826
|
-
// session failure would make a partially failed map impossible to resume.
|
|
2827
|
-
return {
|
|
2828
|
-
ok: true,
|
|
2829
|
-
assistantText: "",
|
|
2830
|
-
command: [],
|
|
2831
|
-
durationMs: 0,
|
|
2832
|
-
exitCode: 0,
|
|
2833
|
-
failureCategory: "success",
|
|
2834
|
-
modelDisplay: "reused-coverage-facts",
|
|
2835
|
-
parsedEvents: 0,
|
|
2836
|
-
stderr: "",
|
|
2837
|
-
stdout: "",
|
|
2838
|
-
timedOut: false,
|
|
2839
|
-
attemptedModels: [],
|
|
2840
|
-
fallbackUsed: false,
|
|
2841
|
-
tokensUsed: 0,
|
|
2842
|
-
};
|
|
2843
|
-
}
|
|
2844
|
-
const compiledInput = parseFrontendInputBlock(input.basePrompt, "plan")?.payload;
|
|
2845
|
-
const fullUnits = new Map(compiledInput?.requirements.map(r => [r.id, r]) ?? []);
|
|
2846
|
-
const workCost = (ids) => 1 + ids.reduce((total, id) => total + Math.max(1, (input.requirementCosts?.get(id) ?? 2) - 1), 0);
|
|
2847
|
-
const { workGroups, buildWorkBatches, compactEligible } = buildFrontendPlanWorkload({
|
|
2848
|
-
basePrompt: input.basePrompt, requirementIds: allRequirementIds,
|
|
2849
|
-
requirementCosts: input.requirementCosts, sessionOptions: input.sessionOptions,
|
|
2850
|
-
});
|
|
2851
|
-
// Data decisions are grouped by observable data domain. Requirements that
|
|
2852
|
-
// mention the same endpoint/resource or execution group share one session;
|
|
2853
|
-
// unrelated domains remain isolated. This prevents the old requirement-by-
|
|
2854
|
-
// requirement repetition while preserving the packer's size bound.
|
|
2855
|
-
const buildDataBatches = (ids) => {
|
|
2856
|
-
const selected = new Set(ids);
|
|
2857
|
-
const domains = new Map();
|
|
2858
|
-
for (const [index, group] of workGroups.entries()) {
|
|
2859
|
-
const members = group.requirementIds.filter((id) => selected.has(id));
|
|
2860
|
-
if (!members.length)
|
|
2861
|
-
continue;
|
|
2862
|
-
const texts = members.map((id) => String(fullUnits.get(id)?.text ?? ""));
|
|
2863
|
-
const endpoint = texts
|
|
2864
|
-
.map((text) => text.match(/\b(?:GET|POST|PUT|PATCH|DELETE)\s+(\/[^\s,;.)]+)/i)?.[1])
|
|
2865
|
-
.find(Boolean);
|
|
2866
|
-
const resource = endpoint
|
|
2867
|
-
? endpoint.split("/").filter(Boolean).slice(0, 2).join("/")
|
|
2868
|
-
: undefined;
|
|
2869
|
-
const domain = resource ? `endpoint:${resource}` : `group:${group.id}`;
|
|
2870
|
-
const current = domains.get(domain);
|
|
2871
|
-
if (current) {
|
|
2872
|
-
current.requirementIds.push(...members);
|
|
2873
|
-
current.requirements.push(...members.map((id) => fullUnits.get(id) ?? { id }));
|
|
2874
|
-
current.estimatedCalls = workCost(current.requirementIds);
|
|
2875
|
-
}
|
|
2876
|
-
else {
|
|
2877
|
-
domains.set(domain, {
|
|
2878
|
-
id: `${index}:${domain}`,
|
|
2879
|
-
requirementIds: [...members],
|
|
2880
|
-
requirements: members.map((id) => fullUnits.get(id) ?? { id }),
|
|
2881
|
-
estimatedCalls: workCost(members),
|
|
2882
|
-
});
|
|
2883
|
-
}
|
|
2884
|
-
}
|
|
2885
|
-
const work = [...domains.values()];
|
|
2886
|
-
// Work-unit packing and full provider envelope capacity have separate budgets.
|
|
2887
|
-
return packFrontendInputUnits(work, {
|
|
2888
|
-
targetBytes: input.sessionOptions.frontendExecutionPolicy?.scopeTargetBytes ?? FRONTEND_SCOPE_TARGET_BYTES,
|
|
2889
|
-
maxUnits: input.sessionOptions.frontendExecutionPolicy?.maxScopeUnits ?? 4,
|
|
2890
|
-
maxCost: FRONTEND_PLAN_COVERAGE_MAX_RECORD_CALLS,
|
|
2891
|
-
cost: (group) => group.estimatedCalls,
|
|
2892
|
-
}).map((batch) => batch.flatMap((group) => group.requirementIds));
|
|
2893
|
-
};
|
|
2894
|
-
// Dependency/deviation is optional policy. For a pure local UI request with
|
|
2895
|
-
// no dependency, package, design-conflict, or OpenSpec signal, opening a
|
|
2896
|
-
// dedicated model session only produces an empty policy fact. Keep the
|
|
2897
|
-
// session when the prompt or ledger contains any such signal so this is a
|
|
2898
|
-
// conservative skip, not a blanket removal of the gate.
|
|
2899
|
-
const dependencyDeviationNeeded = /(?:dependenc|package\.json|npm\s+(?:install|i)|yarn\s+add|pnpm\s+add|openspec|design\s+conflict|规范冲突|依赖)/i.test(input.basePrompt) ||
|
|
2900
|
-
Boolean(input.committedFacts?.().some((record) => {
|
|
2901
|
-
const fact = committedFactFromPlanRecord(record);
|
|
2902
|
-
return fact?.kind === "dependency" || fact?.kind === "design-deviation";
|
|
2903
|
-
}));
|
|
2904
|
-
const globalPolicyToolNames = new Set([
|
|
2905
|
-
"record_route_selection",
|
|
2906
|
-
"record_dependency",
|
|
2907
|
-
"record_design_deviation",
|
|
2908
|
-
"adopt_staged_fact",
|
|
2909
|
-
]);
|
|
2910
|
-
const globalPolicyPrompt = [
|
|
2911
|
-
"PLAN PHASE — global implementation policy.",
|
|
2912
|
-
"Use the Scout target surface to record the selected route(s), then record dependency policy and design-evidence conflicts only when they are relevant. Do not record requirement-local UX or Mock/data facts. Do not call finalize_plan.",
|
|
2913
|
-
dependencyDeviationNeeded
|
|
2914
|
-
? "Dependency/design policy signals are present; inspect them and commit the minimal policy facts needed."
|
|
2915
|
-
: "No dependency/design-conflict signal was found; do not invent a policy fact.",
|
|
2916
|
-
].join(" ");
|
|
2917
|
-
let globalPolicyQueued = false;
|
|
2918
|
-
// Small, single-surface requests do not benefit from six isolated Pi
|
|
2919
|
-
// sessions. Keep the typed ledger as the authority, but let one local
|
|
2920
|
-
// session establish requirement/UX facts and one final session establish
|
|
2921
|
-
// cross-cutting policy + finalize. The old sharded ladder remains available
|
|
2922
|
-
// for larger plans and for the unscoped compatibility path.
|
|
2923
|
-
const useCompactSmallPlan = requirementIdsProvided &&
|
|
2924
|
-
input.compactSmallPlan === true &&
|
|
2925
|
-
compactEligible;
|
|
2926
|
-
if (useCompactSmallPlan) {
|
|
2927
|
-
const compactLocalTools = new Set([
|
|
2928
|
-
"record_plan_requirement",
|
|
2929
|
-
"record_plan_group_coverage",
|
|
2930
|
-
"record_plan_verification_target",
|
|
2931
|
-
"record_plan_evidence_gap",
|
|
2932
|
-
"record_state_registry",
|
|
2933
|
-
"record_component_choice",
|
|
2934
|
-
"record_state_flow",
|
|
2935
|
-
"record_route_selection",
|
|
2936
|
-
"record_data_flow",
|
|
2937
|
-
"record_mock_api",
|
|
2938
|
-
"record_mock_endpoint",
|
|
2939
|
-
"record_dependency",
|
|
2940
|
-
"record_design_deviation",
|
|
2941
|
-
"adopt_staged_fact",
|
|
2942
|
-
]);
|
|
2943
|
-
queue.push({
|
|
2944
|
-
id: "compact-local",
|
|
2945
|
-
toolNames: compactLocalTools,
|
|
2946
|
-
requirementSlice: [...allRequirementIds],
|
|
2947
|
-
prompt: buildCompactLocalPrompt(),
|
|
2948
|
-
});
|
|
2949
|
-
}
|
|
2950
|
-
else if (!requirementIdsProvided) {
|
|
2951
|
-
queue.push({
|
|
2952
|
-
id: "coverage",
|
|
2953
|
-
toolNames: coverageSegment.toolNames,
|
|
2954
|
-
prompt: `${input.basePrompt}\n\n${coverageSegment.instruction}`,
|
|
2955
|
-
});
|
|
2956
|
-
}
|
|
2957
|
-
else if (coverageWorkIds.length > 0) {
|
|
2958
|
-
const completeBatches = buildWorkBatches(coverageWorkIds);
|
|
2959
|
-
completeBatches.forEach((slice, batchIndex) => queue.push({
|
|
2960
|
-
id: `coverage-batch-${batchIndex + 1}`,
|
|
2961
|
-
toolNames: coverageSegment.toolNames,
|
|
2962
|
-
coverageSlice: slice,
|
|
2963
|
-
coverageOnly: true,
|
|
2964
|
-
prompt: buildCoveragePrompt(slice),
|
|
2965
|
-
}));
|
|
2966
|
-
}
|
|
2967
|
-
if (useCompactSmallPlan) {
|
|
2968
|
-
// Compact mode already queued both sessions above.
|
|
2969
|
-
}
|
|
2970
|
-
else if (!input.parallelCoverageOnly) {
|
|
2971
|
-
if (requirementIdsProvided) {
|
|
2972
|
-
queue.push({
|
|
2973
|
-
id: uxRegistrySegment.id,
|
|
2974
|
-
toolNames: uxRegistrySegment.toolNames,
|
|
2975
|
-
prompt: buildUxRegistryPrompt(),
|
|
2976
|
-
});
|
|
2977
|
-
}
|
|
2978
|
-
const uxWorkIds = workGroups.filter(g => !["constraint", "exclusion"].includes(g.kind) || g.requirementIds.some(id => input.behaviorRequiredRequirementIds?.includes(id))).flatMap(g => g.requirementIds);
|
|
2979
|
-
const uxBatches = requirementIdsProvided ? buildWorkBatches(uxWorkIds) : [[]];
|
|
2980
|
-
uxBatches.forEach((slice, batchIndex) => queue.push({
|
|
2981
|
-
id: `ux-local-${batchIndex + 1}`,
|
|
2982
|
-
toolNames: requirementIdsProvided ? uxSegment.toolNames : new Set([...(uxSegment.toolNames ?? []), "record_state_registry"]),
|
|
2983
|
-
...(slice.length ? { requirementSlice: slice } : {}),
|
|
2984
|
-
prompt: slice.length ? buildUxPrompt(slice) : buildPhasePrompt(uxSegment),
|
|
2985
|
-
}));
|
|
2986
|
-
for (const segment of FRONTEND_PLAN_SEGMENTS) {
|
|
2987
|
-
if (["coverage", "ux-registry", "ux-local"].includes(segment.id))
|
|
2988
|
-
continue;
|
|
2989
|
-
if (segment.id === "global-dependency-deviation" && !dependencyDeviationNeeded)
|
|
2990
|
-
continue;
|
|
2991
|
-
if (segment.id === "global-route") {
|
|
2992
|
-
if (globalPolicyQueued)
|
|
2993
|
-
continue;
|
|
2994
|
-
globalPolicyQueued = true;
|
|
2995
|
-
queue.push({
|
|
2996
|
-
id: "global-policy",
|
|
2997
|
-
toolNames: globalPolicyToolNames,
|
|
2998
|
-
prompt: `${buildPhasePrompt(segment)}\n\n${globalPolicyPrompt}`,
|
|
2999
|
-
});
|
|
3000
|
-
continue;
|
|
3001
|
-
}
|
|
3002
|
-
if (segment.id === "global-dependency-deviation")
|
|
3003
|
-
continue;
|
|
3004
|
-
if (segment.id === "global-mock-data" && requirementIdsProvided) {
|
|
3005
|
-
buildDataBatches(allRequirementIds).forEach((slice, i) => queue.push({ id: `global-mock-data-${i + 1}`, toolNames: segment.toolNames, requirementSlice: slice, prompt: buildPhasePrompt(segment, [], slice) }));
|
|
3006
|
-
}
|
|
3007
|
-
else {
|
|
3008
|
-
queue.push({
|
|
3009
|
-
id: segment.id,
|
|
3010
|
-
toolNames: segment.toolNames,
|
|
3011
|
-
prompt: buildPhasePrompt(segment),
|
|
3012
|
-
});
|
|
3013
|
-
}
|
|
3014
|
-
}
|
|
3015
|
-
}
|
|
3016
|
-
let accumulated;
|
|
3017
|
-
let index = 0;
|
|
3018
|
-
let invocationCount = 0;
|
|
3019
|
-
let lastDurableCount = input.committedFactCount();
|
|
3020
|
-
while (index < queue.length) {
|
|
3021
|
-
const session = queue[index];
|
|
3022
|
-
if (!session)
|
|
3023
|
-
break;
|
|
3024
|
-
if (input.attempt > 1 && input.committedFacts && session.id === "ux-registry" &&
|
|
3025
|
-
collectFrontendPlanPhaseMissingFacts({ phase: "ux-registry", requirementIds: allRequirementIds, committedFacts: input.committedFacts() }).length === 0) {
|
|
3026
|
-
index += 1;
|
|
3027
|
-
continue;
|
|
3028
|
-
}
|
|
3029
|
-
// Reuse complete UX scopes on node retry. Require explicit per-requirement
|
|
3030
|
-
// ownership; an unrelated/global fact must not prove a slice complete.
|
|
3031
|
-
if (input.attempt > 1 && input.committedFacts && session.id.startsWith("ux-local-") && session.requirementSlice?.length) {
|
|
3032
|
-
const facts = input.committedFacts();
|
|
3033
|
-
const ownedIds = new Set(facts.flatMap(value => {
|
|
3034
|
-
const fact = committedFactFromPlanRecord(value);
|
|
3035
|
-
return fact ? planFactStringList(fact.scopeRequirementIds) : [];
|
|
3036
|
-
}));
|
|
3037
|
-
const complete = session.requirementSlice.every(id => ownedIds.has(id)) &&
|
|
3038
|
-
collectFrontendPlanPhaseMissingFacts({ phase: "ux-local", requirementIds: session.requirementSlice,
|
|
3039
|
-
committedFacts: facts, behaviorRequiredRequirementIds: input.behaviorRequiredRequirementIds }).length === 0;
|
|
3040
|
-
if (complete) {
|
|
3041
|
-
index += 1;
|
|
3042
|
-
continue;
|
|
3043
|
-
}
|
|
3044
|
-
}
|
|
3045
|
-
const remaining = (session.coverageOnly ? session.coverageSlice ?? [] : []).filter((id) => !input.committedRequirementIds?.().has(id));
|
|
3046
|
-
const preexistingMissing = session.coverageOnly && session.coverageSlice && input.committedFacts
|
|
3047
|
-
? collectFrontendPlanMissingFacts({
|
|
3048
|
-
requirementIds: session.coverageSlice,
|
|
3049
|
-
committedFacts: input.committedFacts(),
|
|
3050
|
-
})
|
|
3051
|
-
: [];
|
|
3052
|
-
// Resume/earlier-batch commits may already cover this slice.
|
|
3053
|
-
if (session.coverageOnly &&
|
|
3054
|
-
session.coverageSlice &&
|
|
3055
|
-
remaining.length === 0 &&
|
|
3056
|
-
preexistingMissing.length === 0) {
|
|
3057
|
-
index += 1;
|
|
3058
|
-
continue;
|
|
3059
|
-
}
|
|
3060
|
-
// Resume only unfinished UX scopes. The registry and local decisions are
|
|
3061
|
-
// durable facts; replaying a completed scope wastes a model session and
|
|
3062
|
-
// can make a previously valid shared decision look like a duplicate.
|
|
3063
|
-
if (session.id === "ux-registry" &&
|
|
3064
|
-
input.committedFacts &&
|
|
3065
|
-
!collectFrontendPlanPhaseMissingFacts({
|
|
3066
|
-
phase: "ux-registry",
|
|
3067
|
-
requirementIds: allRequirementIds,
|
|
3068
|
-
committedFacts: input.committedFacts(),
|
|
3069
|
-
}).length) {
|
|
3070
|
-
index += 1;
|
|
3071
|
-
continue;
|
|
3072
|
-
}
|
|
3073
|
-
if (session.id.startsWith("ux-local-") &&
|
|
3074
|
-
session.requirementSlice &&
|
|
3075
|
-
(input.behaviorRequiredRequirementIds?.length ?? 0) > 0 &&
|
|
3076
|
-
input.committedFacts &&
|
|
3077
|
-
!collectFrontendPlanPhaseMissingFacts({
|
|
3078
|
-
phase: "ux-local",
|
|
3079
|
-
requirementIds: session.requirementSlice,
|
|
3080
|
-
committedFacts: input.committedFacts(),
|
|
3081
|
-
behaviorRequiredRequirementIds: input.behaviorRequiredRequirementIds,
|
|
3082
|
-
}).length) {
|
|
3083
|
-
index += 1;
|
|
3084
|
-
continue;
|
|
3085
|
-
}
|
|
3086
|
-
let prompt = session.prompt;
|
|
3087
|
-
if (session.coverageOnly && session.coverageSlice) {
|
|
3088
|
-
const promptSlice = [...new Set([...remaining, ...preexistingMissing.flatMap(item => item.requirementIds)])];
|
|
3089
|
-
prompt = buildCoveragePrompt(promptSlice, session.missingFacts ?? preexistingMissing);
|
|
3090
|
-
}
|
|
3091
|
-
else if (session.id === "compact-local") {
|
|
3092
|
-
prompt = buildCompactLocalPrompt(session.missingFacts);
|
|
3093
|
-
}
|
|
3094
|
-
else if (session.id === "ux-registry") {
|
|
3095
|
-
prompt = buildUxRegistryPrompt(session.missingFacts);
|
|
3096
|
-
}
|
|
3097
|
-
else if (session.id.startsWith("ux-local-")) {
|
|
3098
|
-
// Build this at execution time: coverage facts are committed by the
|
|
3099
|
-
// preceding sessions and must be visible to the UX-local model.
|
|
3100
|
-
prompt = session.requirementSlice
|
|
3101
|
-
? buildUxPrompt(session.requirementSlice, session.missingFacts)
|
|
3102
|
-
: buildPhasePrompt(uxSegment, session.missingFacts);
|
|
3103
|
-
}
|
|
3104
|
-
else {
|
|
3105
|
-
// Global phases consume the latest committed ledger;
|
|
3106
|
-
// constructing their prompt only when the session starts prevents a
|
|
3107
|
-
// stale queue entry from dropping facts written by earlier phases.
|
|
3108
|
-
const segment = FRONTEND_PLAN_SEGMENTS.find((candidate) => candidate.id === session.id || (candidate.id === "global-mock-data" && session.id.startsWith("global-mock-data-")));
|
|
3109
|
-
if (session.id === "global-policy") {
|
|
3110
|
-
const routePrompt = buildPhasePrompt(FRONTEND_PLAN_SEGMENTS.find((candidate) => candidate.id === "global-route"));
|
|
3111
|
-
prompt = `${routePrompt}\n\n${globalPolicyPrompt}`;
|
|
3112
|
-
}
|
|
3113
|
-
else if (segment) {
|
|
3114
|
-
prompt = buildPhasePrompt(segment, session.missingFacts, session.requirementSlice ?? allRequirementIds);
|
|
3115
|
-
}
|
|
3116
|
-
}
|
|
3117
|
-
const atomicFocus = session.atomicRecovery ? session.missingFacts?.[0] : undefined;
|
|
3118
|
-
if (atomicFocus) {
|
|
3119
|
-
prompt = [
|
|
3120
|
-
compactFrontendPlanPromptForRequirementSlice(input.basePrompt, atomicFocus.requirementIds, {
|
|
3121
|
-
includeChecklist: false,
|
|
3122
|
-
...(atomicFocus.kind === "plan-verification-target" && atomicFocus.id
|
|
3123
|
-
? { verificationTargetIds: [atomicFocus.id] } : {}),
|
|
3124
|
-
}),
|
|
3125
|
-
`ATOMIC FACT: ${atomicFocus.kind}${atomicFocus.id ? ` ${atomicFocus.id}` : ""}`,
|
|
3126
|
-
atomicFocus.reason,
|
|
3127
|
-
"Commit ONLY this missing fact with one record_* call, then end this session. Other facts are queued separately. Preserve canonical IDs and shared bindings; use read_plan_facts for existing values. Do not summarize or plan the entire requirement. Capacity exhaustion is not evidence of a requirement gap.",
|
|
3128
|
-
].join("\n\n");
|
|
3129
|
-
}
|
|
3130
|
-
const committedBefore = input.committedFactCount();
|
|
3131
|
-
input.setActiveRequirementScope?.(session.coverageOnly ? session.coverageSlice ?? [] : session.requirementSlice ?? []);
|
|
3132
|
-
if (invocationCount >= FRONTEND_PLAN_BATCH_MAX_SESSIONS)
|
|
3133
|
-
break;
|
|
3134
|
-
invocationCount += 1;
|
|
3135
|
-
const atomicTools = {
|
|
3136
|
-
"plan-requirement": "record_plan_requirement", "plan-verification-target": "record_plan_verification_target",
|
|
3137
|
-
"state-registry": "record_state_registry", "component-choice": "record_component_choice",
|
|
3138
|
-
"state-flow": "record_state_flow", "data-flow": "record_data_flow", "mock-api": "record_mock_api",
|
|
3139
|
-
};
|
|
3140
|
-
const customTools = input.segmentCustomTools(atomicFocus ? new Set([atomicTools[atomicFocus.kind]]) : session.toolNames);
|
|
3141
|
-
const result = await observeFrontendSession({
|
|
3142
|
-
...input.observation, phase: `plan/${session.id}`, dispatchReason: (session.retryCount ?? 0) > 0 || !!session.missingFacts?.length || /capacity|recovery|split/.test(session.id) ? "correction" : "initial", scopeIds: session.requirementSlice ?? session.coverageSlice ?? allRequirementIds,
|
|
3143
|
-
prompt, userMessage: input.sessionOptions.userMessage, customTools, committedCount: input.committedFactCount, durableCommittedCount: () => lastDurableCount,
|
|
3144
|
-
artifactPath: input.observation?.artifactPath ? `${input.observation.artifactPath}-${invocationCount}.json` : undefined,
|
|
3145
|
-
}, async (observer) => {
|
|
3146
|
-
const result = await input.piStepFn({
|
|
3147
|
-
...input.sessionOptions,
|
|
3148
|
-
onAttemptObservation: observer,
|
|
3149
|
-
prompt,
|
|
3150
|
-
...(customTools.length > 0
|
|
3151
|
-
? {
|
|
3152
|
-
writerToolPolicy: {
|
|
3153
|
-
requireSdk: true,
|
|
3154
|
-
customTools,
|
|
3155
|
-
},
|
|
3156
|
-
}
|
|
3157
|
-
: {}),
|
|
3158
|
-
});
|
|
3159
|
-
try {
|
|
3160
|
-
await input.flushLedger();
|
|
3161
|
-
lastDurableCount = input.committedFactCount();
|
|
3162
|
-
}
|
|
3163
|
-
catch {
|
|
3164
|
-
// best-effort: the node-level flush runs again after the attempt
|
|
3165
|
-
}
|
|
3166
|
-
return result;
|
|
3167
|
-
});
|
|
3168
|
-
accumulated = accumulated
|
|
3169
|
-
? combineSequentialPiResults(accumulated, result)
|
|
3170
|
-
: result;
|
|
3171
|
-
const committedAfter = input.committedFactCount();
|
|
3172
|
-
const committedFactsOnlySuccess = !(result.assistantText ?? "").trim() &&
|
|
3173
|
-
!result.stderr.trim() &&
|
|
3174
|
-
!result.timedOut &&
|
|
3175
|
-
committedAfter > committedBefore;
|
|
3176
|
-
const missingCoverage = session.coverageOnly && session.coverageSlice && input.committedFacts
|
|
3177
|
-
? collectFrontendPlanMissingFacts({
|
|
3178
|
-
requirementIds: session.coverageSlice,
|
|
3179
|
-
committedFacts: input.committedFacts(),
|
|
3180
|
-
})
|
|
3181
|
-
: [];
|
|
3182
|
-
const isCompactLocalSession = session.id === "compact-local";
|
|
3183
|
-
const isUxRegistrySession = session.id === "ux-registry";
|
|
3184
|
-
const isUxLocalSession = session.id.startsWith("ux-local-");
|
|
3185
|
-
const missingPhase = isCompactLocalSession && input.committedFacts
|
|
3186
|
-
? [
|
|
3187
|
-
...collectFrontendPlanMissingFacts({
|
|
3188
|
-
requirementIds: allRequirementIds,
|
|
3189
|
-
committedFacts: input.committedFacts(),
|
|
3190
|
-
}),
|
|
3191
|
-
...collectFrontendPlanPhaseMissingFacts({
|
|
3192
|
-
phase: "ux-registry",
|
|
3193
|
-
requirementIds: allRequirementIds,
|
|
3194
|
-
committedFacts: input.committedFacts(),
|
|
3195
|
-
}),
|
|
3196
|
-
...collectFrontendPlanPhaseMissingFacts({
|
|
3197
|
-
phase: "ux-local",
|
|
3198
|
-
requirementIds: allRequirementIds,
|
|
3199
|
-
committedFacts: input.committedFacts(),
|
|
3200
|
-
behaviorRequiredRequirementIds: input.behaviorRequiredRequirementIds,
|
|
3201
|
-
}),
|
|
3202
|
-
]
|
|
3203
|
-
: isUxRegistrySession && input.committedFacts
|
|
3204
|
-
? collectFrontendPlanPhaseMissingFacts({
|
|
3205
|
-
phase: "ux-registry",
|
|
3206
|
-
requirementIds: allRequirementIds,
|
|
3207
|
-
committedFacts: input.committedFacts(),
|
|
3208
|
-
})
|
|
3209
|
-
: isUxLocalSession && session.requirementSlice && input.committedFacts
|
|
3210
|
-
? collectFrontendPlanPhaseMissingFacts({
|
|
3211
|
-
phase: "ux-local",
|
|
3212
|
-
requirementIds: session.requirementSlice,
|
|
3213
|
-
committedFacts: input.committedFacts(),
|
|
3214
|
-
behaviorRequiredRequirementIds: input.behaviorRequiredRequirementIds,
|
|
3215
|
-
})
|
|
3216
|
-
: session.id.startsWith("global-mock-data") && allRequirementIds.length > 0 && input.committedFacts
|
|
3217
|
-
? collectFrontendPlanPhaseMissingFacts({
|
|
3218
|
-
phase: "global-mock-data",
|
|
3219
|
-
requirementIds: session.requirementSlice ?? allRequirementIds,
|
|
3220
|
-
committedFacts: input.committedFacts(),
|
|
3221
|
-
})
|
|
3222
|
-
: [];
|
|
3223
|
-
const missingPhaseFacts = [...missingCoverage, ...missingPhase];
|
|
3224
|
-
// Frozen verification commands must operate on files the planner has
|
|
3225
|
-
// committed verification targets for; otherwise the admission writeSet
|
|
3226
|
-
// cannot authorize the file and verify-shell deterministically fails.
|
|
3227
|
-
const verificationCommandFiles = collectFrontendVerificationCommandFiles(input.basePrompt);
|
|
3228
|
-
if (verificationCommandFiles.length > 0 && allRequirementIds.length > 0 && input.committedFacts) {
|
|
3229
|
-
const coveredVerificationFiles = new Set(input
|
|
3230
|
-
.committedFacts()
|
|
3231
|
-
.map(committedFactFromPlanRecord)
|
|
3232
|
-
.filter((fact) => Boolean(fact && fact.origin === "plan" && fact.kind === "plan-verification-target"))
|
|
3233
|
-
.map((fact) => fact.entry?.file)
|
|
3234
|
-
.filter((file) => typeof file === "string"));
|
|
3235
|
-
for (const file of verificationCommandFiles) {
|
|
3236
|
-
if (coveredVerificationFiles.has(file))
|
|
3237
|
-
continue;
|
|
3238
|
-
missingPhaseFacts.push({
|
|
3239
|
-
kind: "plan-verification-target",
|
|
3240
|
-
id: file,
|
|
3241
|
-
requirementIds: allRequirementIds.slice(0, 1),
|
|
3242
|
-
reason: `frozen verification command references ${file} but no committed verification target covers it; record a static verification target for this file so it joins the writeSet`,
|
|
3243
|
-
});
|
|
3244
|
-
}
|
|
3245
|
-
}
|
|
3246
|
-
const promptForMissingPhase = (missing) => session.coverageOnly
|
|
3247
|
-
? buildCoveragePrompt(session.coverageSlice ?? [], missing)
|
|
3248
|
-
: isCompactLocalSession
|
|
3249
|
-
? buildCompactLocalPrompt(missing)
|
|
3250
|
-
: isUxRegistrySession
|
|
3251
|
-
? buildUxRegistryPrompt(missing)
|
|
3252
|
-
: isUxLocalSession
|
|
3253
|
-
? buildUxPrompt(session.requirementSlice ?? [], missing)
|
|
3254
|
-
: buildPhasePrompt(globalMockDataSegment, missing, session.requirementSlice ?? allRequirementIds);
|
|
3255
|
-
const recovery = classifyFrontendPlanRecovery({ ...result, stopReason: readWriterThinkingExhaustionEvidence(result).stopReason });
|
|
3256
|
-
if (recovery === "stop")
|
|
3257
|
-
return { ...result, ok: false };
|
|
3258
|
-
if ((recovery === "output" || (session.atomicRecovery && result.ok)) && input.committedFacts &&
|
|
3259
|
-
!(missingPhaseFacts.length === 1 && missingPhaseFacts[0]?.kind === "plan-requirement") &&
|
|
3260
|
-
(session.coverageOnly || isUxRegistrySession || isUxLocalSession || session.id.startsWith("global-mock-data"))) {
|
|
3261
|
-
if (missingPhaseFacts.length === 0) {
|
|
3262
|
-
// A complete validated slice does not need a successful prose turn.
|
|
3263
|
-
// This never accepts finalize or bypasses the final contract validator.
|
|
3264
|
-
accumulated = { ...accumulated, ok: true, failureCategory: "success" };
|
|
3265
|
-
index += 1;
|
|
3266
|
-
continue;
|
|
3267
|
-
}
|
|
3268
|
-
const missingIds = [...new Set(missingPhaseFacts.flatMap(item => item.requirementIds))];
|
|
3269
|
-
const scope = session.coverageSlice ?? session.requirementSlice;
|
|
3270
|
-
if (scope && missingIds.length < scope.length) {
|
|
3271
|
-
queue[index] = { ...session, missingFacts: missingPhaseFacts,
|
|
3272
|
-
...(session.coverageOnly ? { coverageSlice: missingIds } : { requirementSlice: missingIds }),
|
|
3273
|
-
retryCount: 0 };
|
|
3274
|
-
continue;
|
|
3275
|
-
}
|
|
3276
|
-
if (scope && missingIds.length > 1 && (session.coverageOnly || isUxLocalSession)) {
|
|
3277
|
-
const half = Math.ceil(missingIds.length / 2);
|
|
3278
|
-
queue.splice(index, 1, ...[missingIds.slice(0, half), missingIds.slice(half)].map(ids => ({
|
|
3279
|
-
...session, retryCount: 0,
|
|
3280
|
-
...(session.coverageOnly ? { coverageSlice: ids } : { requirementSlice: ids }),
|
|
3281
|
-
missingFacts: missingPhaseFacts.filter(item => item.requirementIds.some(id => ids.includes(id))),
|
|
3282
|
-
})));
|
|
3283
|
-
continue;
|
|
3284
|
-
}
|
|
3285
|
-
const focusStillMissing = atomicFocus && missingPhaseFacts.some(item => item.kind === atomicFocus.kind && item.id === atomicFocus.id &&
|
|
3286
|
-
item.requirementIds.join("\0") === atomicFocus.requirementIds.join("\0"));
|
|
3287
|
-
const retries = focusStillMissing ? (session.retryCount ?? 0) + 1 : 0;
|
|
3288
|
-
if (retries < 2) {
|
|
3289
|
-
queue[index] = { ...session, atomicRecovery: true, missingFacts: missingPhaseFacts, retryCount: retries };
|
|
3290
|
-
continue;
|
|
3291
|
-
}
|
|
3292
|
-
return { ...accumulated, ok: false, failureCategory: OUTPUT_LIMIT_RETRY_CATEGORY,
|
|
3293
|
-
stderr: `${accumulated.stderr}\nfrontend plan capacity recovery exhausted: preserve committed facts; phase=${session.id}; scope=${missingIds.join(",")}; strategy=atomic-fact; missing=${JSON.stringify(missingPhaseFacts)}`.trim() };
|
|
3294
|
-
}
|
|
3295
|
-
const capacityExhausted = recovery === "output" || recovery === "context";
|
|
3296
|
-
if (capacityExhausted && !result.timedOut) {
|
|
3297
|
-
const failure = () => mapPlannerExhaustion({ ...result, ok: false, stderr: `${result.stderr}\nFRONTEND_INPUT_UNIT_TOO_LARGE: ${session.id}; no smaller complete scope can finish; unchanged retries are disabled` }, committedAfter > committedBefore);
|
|
3298
|
-
const missingCoverageIds = input.committedFacts ? [...new Set(collectFrontendPlanMissingFacts({ requirementIds: session.coverageSlice ?? session.requirementSlice ?? allRequirementIds, committedFacts: input.committedFacts() }).flatMap(f => f.requirementIds))] : [...(session.coverageSlice ?? session.requirementSlice ?? allRequirementIds)];
|
|
3299
|
-
const splitScope = (ids, allowMemberSplit) => {
|
|
3300
|
-
const groups = workGroups.map(g => g.requirementIds.filter(id => ids.includes(id))).filter(g => g.length);
|
|
3301
|
-
if (groups.length > 1) {
|
|
3302
|
-
const half = Math.ceil(groups.length / 2);
|
|
3303
|
-
return [groups.slice(0, half).flat(), groups.slice(half).flat()];
|
|
3304
|
-
}
|
|
3305
|
-
if (allowMemberSplit && ids.length > 1) {
|
|
3306
|
-
const half = Math.ceil(ids.length / 2);
|
|
3307
|
-
return [ids.slice(0, half), ids.slice(half)];
|
|
3308
|
-
}
|
|
3309
|
-
return [];
|
|
3310
|
-
};
|
|
3311
|
-
if (isCompactLocalSession) {
|
|
3312
|
-
let scopes = splitScope(missingCoverageIds, true);
|
|
3313
|
-
if (!scopes.length && missingCoverageIds.length) {
|
|
3314
|
-
if (missingCoverageIds.length === allRequirementIds.length && committedAfter === committedBefore)
|
|
3315
|
-
return failure();
|
|
3316
|
-
scopes = [missingCoverageIds];
|
|
3317
|
-
}
|
|
3318
|
-
// UX ownership is independent of coverage completion. Preserve all groups.
|
|
3319
|
-
const recovery = scopes.map((slice, i) => ({ id: `coverage-capacity-${invocationCount}-${i}`, coverageOnly: true, coverageSlice: slice, toolNames: coverageSegment.toolNames, prompt: buildCoveragePrompt(slice) }));
|
|
3320
|
-
recovery.push({ id: "ux-registry", toolNames: uxRegistrySegment.toolNames, prompt: buildUxRegistryPrompt() });
|
|
3321
|
-
recovery.push(...buildWorkBatches([...allRequirementIds]).map((slice, i) => ({ id: `ux-local-capacity-${invocationCount}-${i}`, requirementSlice: slice, toolNames: uxSegment.toolNames, prompt: buildUxPrompt(slice) })));
|
|
3322
|
-
queue.splice(index, 1, ...recovery);
|
|
3323
|
-
continue;
|
|
3324
|
-
}
|
|
3325
|
-
if (session.coverageOnly || isUxLocalSession) {
|
|
3326
|
-
const missingIds = session.coverageOnly ? missingCoverageIds : [...new Set(missingPhase.flatMap(f => f.requirementIds))];
|
|
3327
|
-
if (!missingIds.length) {
|
|
3328
|
-
index += 1;
|
|
3329
|
-
continue;
|
|
3330
|
-
}
|
|
3331
|
-
const scopes = splitScope(missingIds, Boolean(session.coverageOnly));
|
|
3332
|
-
if (scopes.length) {
|
|
3333
|
-
queue.splice(index, 1, ...scopes.map(slice => ({ ...session, ...(session.coverageOnly ? { coverageSlice: slice } : { requirementSlice: slice }), missingFacts: missingPhaseFacts.filter(f => f.requirementIds.some(id => slice.includes(id))), retryCount: 0 })));
|
|
3334
|
-
continue;
|
|
3335
|
-
}
|
|
3336
|
-
const originalIds = session.coverageSlice ?? session.requirementSlice ?? [];
|
|
3337
|
-
if ((missingIds.length < originalIds.length || committedAfter > committedBefore) && (session.retryCount ?? 0) < 2) {
|
|
3338
|
-
queue[index] = { ...session, ...(session.coverageOnly ? { coverageSlice: missingIds } : { requirementSlice: missingIds }), missingFacts: missingPhaseFacts, retryCount: (session.retryCount ?? 0) + 1 };
|
|
3339
|
-
continue;
|
|
3340
|
-
}
|
|
3341
|
-
}
|
|
3342
|
-
return failure();
|
|
3343
|
-
}
|
|
3344
|
-
if (result.ok) {
|
|
3345
|
-
if (missingPhaseFacts.length === 0) {
|
|
3346
|
-
index += 1;
|
|
3347
|
-
continue;
|
|
3348
|
-
}
|
|
3349
|
-
if ((session.retryCount ?? 0) < 2) {
|
|
3350
|
-
queue[index] = {
|
|
3351
|
-
...session,
|
|
3352
|
-
retryCount: (session.retryCount ?? 0) + 1,
|
|
3353
|
-
missingFacts: missingPhaseFacts,
|
|
3354
|
-
prompt: promptForMissingPhase(missingPhaseFacts),
|
|
3355
|
-
};
|
|
3356
|
-
continue;
|
|
3357
|
-
}
|
|
3358
|
-
return {
|
|
3359
|
-
...accumulated,
|
|
3360
|
-
ok: false,
|
|
3361
|
-
stderr: `${accumulated.stderr}\nfrontend plan completeness check failed: ${missingPhaseFacts.map((item) => item.reason).join("; ")}`.trim(),
|
|
3362
|
-
failureCategory: "invalid-output",
|
|
3363
|
-
};
|
|
3364
|
-
}
|
|
3365
|
-
if (committedFactsOnlySuccess) {
|
|
3366
|
-
// The session died but banked facts: keep the progress and move on.
|
|
3367
|
-
if (missingPhaseFacts.length > 0 && (session.retryCount ?? 0) < 2) {
|
|
3368
|
-
queue[index] = {
|
|
3369
|
-
...session,
|
|
3370
|
-
retryCount: (session.retryCount ?? 0) + 1,
|
|
3371
|
-
missingFacts: missingPhaseFacts,
|
|
3372
|
-
prompt: promptForMissingPhase(missingPhaseFacts),
|
|
3373
|
-
};
|
|
3374
|
-
continue;
|
|
3375
|
-
}
|
|
3376
|
-
if (missingPhaseFacts.length > 0) {
|
|
3377
|
-
return {
|
|
3378
|
-
...accumulated,
|
|
3379
|
-
ok: false,
|
|
3380
|
-
stderr: `${accumulated.stderr}\nfrontend plan completeness check failed: ${missingPhaseFacts.map((item) => item.reason).join("; ")}`.trim(),
|
|
3381
|
-
failureCategory: "invalid-output",
|
|
3382
|
-
};
|
|
3383
|
-
}
|
|
3384
|
-
index += 1;
|
|
3385
|
-
continue;
|
|
3386
|
-
}
|
|
3387
|
-
// Option 5: a multi-requirement coverage batch that failed with ZERO
|
|
3388
|
-
// new facts and no provider stderr is the upfront-reasoning burn —
|
|
3389
|
-
// halve the slice and retry instead of failing the attempt. The compact
|
|
3390
|
-
// local session participates through its requirementSlice so a
|
|
3391
|
-
// thinking-burned small plan degrades into smaller batched sessions
|
|
3392
|
-
// instead of replaying one full-scope prompt until the repair budget
|
|
3393
|
-
// runs out.
|
|
3394
|
-
const coverageSlice = session.coverageOnly
|
|
3395
|
-
? session.coverageSlice
|
|
3396
|
-
: isCompactLocalSession
|
|
3397
|
-
? session.requirementSlice
|
|
3398
|
-
: undefined;
|
|
3399
|
-
const zeroProgressBurn = coverageSlice !== undefined &&
|
|
3400
|
-
coverageSlice.length > 1 &&
|
|
3401
|
-
(capacityExhausted || (["empty-output", "unknown"].includes(result.failureCategory) && committedAfter === committedBefore && !(result.assistantText ?? "").trim() && !result.stderr.trim())) &&
|
|
3402
|
-
!result.timedOut;
|
|
3403
|
-
if (zeroProgressBurn && coverageSlice) {
|
|
3404
|
-
const missingIds = input.committedFacts ? new Set(collectFrontendPlanMissingFacts({ requirementIds: coverageSlice, committedFacts: input.committedFacts() }).flatMap(f => f.requirementIds)) : undefined;
|
|
3405
|
-
const unfinished = coverageSlice.filter(id => !missingIds || missingIds.has(id));
|
|
3406
|
-
if (unfinished.length <= 1)
|
|
3407
|
-
return mapPlannerExhaustion({ ...result, ok: false, stderr: `${result.stderr}\nFRONTEND_INPUT_UNIT_TOO_LARGE: ${unfinished[0] ?? session.id}; no smaller complete scope can finish` }, committedAfter > committedBefore);
|
|
3408
|
-
const half = Math.ceil(unfinished.length / 2);
|
|
3409
|
-
const firstSlice = unfinished.slice(0, half);
|
|
3410
|
-
const secondSlice = unfinished.slice(half);
|
|
3411
|
-
if (isCompactLocalSession) {
|
|
3412
|
-
queue.splice(index + 1, 0, { id: "ux-registry", toolNames: uxRegistrySegment.toolNames, prompt: buildUxRegistryPrompt() }, ...buildWorkBatches([...firstSlice, ...secondSlice]).map((slice, i) => ({ id: `ux-local-recovery-${i}`, toolNames: uxSegment.toolNames, requirementSlice: slice, prompt: buildUxPrompt(slice) })));
|
|
3413
|
-
// The split halves leave compact mode: continue them as ordinary
|
|
3414
|
-
// coverage sessions so every downstream ladder branch applies.
|
|
3415
|
-
queue.splice(index, 1, {
|
|
3416
|
-
...session,
|
|
3417
|
-
id: `coverage-compact-split-1`,
|
|
3418
|
-
toolNames: coverageSegment.toolNames, retryCount: 0,
|
|
3419
|
-
coverageOnly: true,
|
|
3420
|
-
coverageSlice: firstSlice,
|
|
3421
|
-
requirementSlice: firstSlice,
|
|
3422
|
-
prompt: buildCoveragePrompt(firstSlice),
|
|
3423
|
-
}, {
|
|
3424
|
-
...session,
|
|
3425
|
-
id: `coverage-compact-split-2`,
|
|
3426
|
-
toolNames: coverageSegment.toolNames, retryCount: 0,
|
|
3427
|
-
coverageOnly: true,
|
|
3428
|
-
coverageSlice: secondSlice,
|
|
3429
|
-
requirementSlice: secondSlice,
|
|
3430
|
-
prompt: buildCoveragePrompt(secondSlice),
|
|
3431
|
-
});
|
|
3432
|
-
continue;
|
|
3433
|
-
}
|
|
3434
|
-
queue.splice(index, 1, {
|
|
3435
|
-
...session,
|
|
3436
|
-
coverageSlice: firstSlice,
|
|
3437
|
-
prompt: buildCoveragePrompt(firstSlice),
|
|
3438
|
-
}, {
|
|
3439
|
-
...session,
|
|
3440
|
-
coverageSlice: secondSlice,
|
|
3441
|
-
prompt: buildCoveragePrompt(secondSlice),
|
|
3442
|
-
});
|
|
3443
|
-
continue;
|
|
3444
|
-
}
|
|
3445
|
-
if (readWriterThinkingExhaustionEvidence(result).stopReason === "length" || ["context-overflow", "context-budget-exhausted"].includes(result.failureCategory))
|
|
3446
|
-
return mapPlannerExhaustion({ ...result, ok: false, stderr: `${result.stderr}\nFRONTEND_INPUT_UNIT_TOO_LARGE: ${session.id}; refine the remaining complete scope; unchanged retries are disabled` }, committedAfter > committedBefore);
|
|
3447
|
-
if ((session.coverageOnly || isUxRegistrySession || isUxLocalSession || session.id.startsWith("global-mock-data")) &&
|
|
3448
|
-
missingPhaseFacts.length > 0 &&
|
|
3449
|
-
!result.stderr.trim() &&
|
|
3450
|
-
!result.timedOut &&
|
|
3451
|
-
(session.retryCount ?? 0) < 2) {
|
|
3452
|
-
queue[index] = {
|
|
3453
|
-
...session,
|
|
3454
|
-
retryCount: (session.retryCount ?? 0) + 1,
|
|
3455
|
-
missingFacts: missingPhaseFacts,
|
|
3456
|
-
prompt: promptForMissingPhase(missingPhaseFacts),
|
|
3457
|
-
};
|
|
3458
|
-
continue;
|
|
3459
|
-
}
|
|
3460
|
-
return mapPlannerExhaustion(accumulated, committedAfter > committedBefore);
|
|
3461
|
-
}
|
|
3462
|
-
if (index < queue.length) {
|
|
3463
|
-
return {
|
|
3464
|
-
...(accumulated ?? {
|
|
3465
|
-
ok: false,
|
|
3466
|
-
stdout: "",
|
|
3467
|
-
stderr: "",
|
|
3468
|
-
assistantText: "",
|
|
3469
|
-
command: [],
|
|
3470
|
-
durationMs: 0,
|
|
3471
|
-
exitCode: null,
|
|
3472
|
-
failureCategory: "invalid-output",
|
|
3473
|
-
modelDisplay: "unknown",
|
|
3474
|
-
parsedEvents: 0,
|
|
3475
|
-
timedOut: false,
|
|
3476
|
-
attemptedModels: [],
|
|
3477
|
-
fallbackUsed: false,
|
|
3478
|
-
tokensUsed: 0,
|
|
3479
|
-
}),
|
|
3480
|
-
ok: false,
|
|
3481
|
-
stderr: `${accumulated?.stderr ?? ""}\nfrontend plan segmentation exceeded the ${FRONTEND_PLAN_BATCH_MAX_SESSIONS}-session safety limit before finalize`.trim(),
|
|
3482
|
-
failureCategory: "invalid-output",
|
|
3483
|
-
};
|
|
3484
|
-
}
|
|
3485
|
-
if (!input.parallelCoverageOnly && input.finalizePlan) {
|
|
3486
|
-
// Preserve optional closeout context if an earlier correction or future
|
|
3487
|
-
// ledger producer committed it. The old model-only finalize session was
|
|
3488
|
-
// the only writer of these fields; runtime-owned finalization must not
|
|
3489
|
-
// silently erase them when they are already available.
|
|
3490
|
-
const optionalPlanFields = {};
|
|
3491
|
-
for (const record of input.committedFacts?.() ?? []) {
|
|
3492
|
-
const fact = committedFactFromPlanRecord(record);
|
|
3493
|
-
if (!fact)
|
|
3494
|
-
continue;
|
|
3495
|
-
if (Array.isArray(fact.residualRisks)) {
|
|
3496
|
-
optionalPlanFields.residualRisks = fact.residualRisks.filter((item) => typeof item === "string" && item.trim().length > 0);
|
|
3497
|
-
}
|
|
3498
|
-
if (typeof fact.realIntegrationGap === "string" && fact.realIntegrationGap.trim()) {
|
|
3499
|
-
optionalPlanFields.realIntegrationGap = fact.realIntegrationGap;
|
|
3500
|
-
}
|
|
3501
|
-
}
|
|
3502
|
-
let finalizationCount = 0;
|
|
3503
|
-
const finalize = () => observeFrontendFinalization({
|
|
3504
|
-
...input.observation,
|
|
3505
|
-
artifactPath: input.observation?.artifactPath ? `${input.observation.artifactPath}-runtime-finalize-${++finalizationCount}.json` : undefined,
|
|
3506
|
-
}, () => input.finalizePlan(optionalPlanFields));
|
|
3507
|
-
let finalizeResult = await finalize();
|
|
3508
|
-
const finalizeDetails = finalizeResult && typeof finalizeResult === "object"
|
|
3509
|
-
? (finalizeResult.details ?? finalizeResult)
|
|
3510
|
-
: finalizeResult;
|
|
3511
|
-
if (!finalizeDetails ||
|
|
3512
|
-
typeof finalizeDetails !== "object" ||
|
|
3513
|
-
finalizeDetails.ok !== true) {
|
|
3514
|
-
// Normal plans never open a finalize model session. Keep the old
|
|
3515
|
-
// correction/recovery semantic only for a rejected deterministic
|
|
3516
|
-
// compile: give the planner one bounded repair pass, then retry the
|
|
3517
|
-
// same runtime authority. The correction prompt explicitly forbids
|
|
3518
|
-
// calling finalize_plan, so terminal ownership remains deterministic.
|
|
3519
|
-
const correctionPrompt = [
|
|
3520
|
-
input.basePrompt,
|
|
3521
|
-
"PLAN FINALIZE CORRECTION — the runtime compile rejected the committed ledger.",
|
|
3522
|
-
`Runtime error: ${String(finalizeDetails?.error ?? "plan finalize rejected")}`,
|
|
3523
|
-
"Repair only the reported facts with the typed record_* tools. Do not call finalize_plan; the runtime will retry it after this correction.",
|
|
3524
|
-
input.committedFacts ? compactFrontendPlanLedgerContext({ committedFacts: input.committedFacts(), requirementIds: input.requirementIds ?? [], kinds: ["plan-requirement", "plan-verification-target", "state-registry", "component-choice", "state-flow", "data-flow", "mock-api", "design-deviation", "dependency", "target-surface"] }) : "",
|
|
3525
|
-
].filter(Boolean).join("\n\n");
|
|
3526
|
-
input.setActiveRequirementScope?.([]);
|
|
3527
|
-
const correctionTools = input.segmentCustomTools(null);
|
|
3528
|
-
const correction = await observeFrontendSession({
|
|
3529
|
-
...input.observation, phase: "plan/finalize-correction", dispatchReason: "correction",
|
|
3530
|
-
scopeIds: input.requirementIds, prompt: correctionPrompt, userMessage: input.sessionOptions.userMessage,
|
|
3531
|
-
customTools: correctionTools, committedCount: input.committedFactCount, durableCommittedCount: () => lastDurableCount,
|
|
3532
|
-
artifactPath: input.observation?.artifactPath ? `${input.observation.artifactPath}-finalize-correction.json` : undefined,
|
|
3533
|
-
}, async (observer) => {
|
|
3534
|
-
const result = await input.piStepFn({
|
|
3535
|
-
...input.sessionOptions, onAttemptObservation: observer, prompt: correctionPrompt,
|
|
3536
|
-
writerToolPolicy: { requireSdk: true, customTools: correctionTools },
|
|
3537
|
-
});
|
|
3538
|
-
try {
|
|
3539
|
-
await input.flushLedger();
|
|
3540
|
-
lastDurableCount = input.committedFactCount();
|
|
3541
|
-
}
|
|
3542
|
-
catch { /* node-level flush retries below */ }
|
|
3543
|
-
return result;
|
|
3544
|
-
});
|
|
3545
|
-
accumulated = accumulated
|
|
3546
|
-
? combineSequentialPiResults(accumulated, correction)
|
|
3547
|
-
: correction;
|
|
3548
|
-
if (!correction.ok && !["invalid-output", "empty-output", "success"].includes(correction.failureCategory))
|
|
3549
|
-
return { ...accumulated, ok: false, failureCategory: correction.failureCategory };
|
|
3550
|
-
if (correction.ok) {
|
|
3551
|
-
finalizeResult = await finalize();
|
|
3552
|
-
}
|
|
3553
|
-
const retriedDetails = finalizeResult && typeof finalizeResult === "object"
|
|
3554
|
-
? (finalizeResult.details ?? finalizeResult)
|
|
3555
|
-
: finalizeResult;
|
|
3556
|
-
if (retriedDetails && typeof retriedDetails === "object" && retriedDetails.ok === true) {
|
|
3557
|
-
return mapPlannerExhaustion(accumulated ?? { ok: true, stdout: "", stderr: "", assistantText: "", command: [], durationMs: 0, exitCode: 0, failureCategory: "success", modelDisplay: "runtime-finalize", parsedEvents: 0, timedOut: false, attemptedModels: [], fallbackUsed: false, tokensUsed: 0 }, true);
|
|
3558
|
-
}
|
|
3559
|
-
const detail = retriedDetails && typeof retriedDetails === "object"
|
|
3560
|
-
? String(retriedDetails.error ?? "plan finalize rejected")
|
|
3561
|
-
: "plan finalize rejected";
|
|
3562
|
-
return {
|
|
3563
|
-
...(accumulated ?? {
|
|
3564
|
-
ok: false,
|
|
3565
|
-
stdout: "",
|
|
3566
|
-
stderr: "",
|
|
3567
|
-
assistantText: "",
|
|
3568
|
-
command: [],
|
|
3569
|
-
durationMs: 0,
|
|
3570
|
-
exitCode: null,
|
|
3571
|
-
failureCategory: "invalid-output",
|
|
3572
|
-
modelDisplay: "unknown",
|
|
3573
|
-
parsedEvents: 0,
|
|
3574
|
-
timedOut: false,
|
|
3575
|
-
attemptedModels: [],
|
|
3576
|
-
fallbackUsed: false,
|
|
3577
|
-
tokensUsed: 0,
|
|
3578
|
-
}),
|
|
3579
|
-
ok: false,
|
|
3580
|
-
stderr: `${accumulated?.stderr ?? ""}\nfrontend plan deterministic finalize failed: ${detail}`.trim(),
|
|
3581
|
-
failureCategory: "invalid-output",
|
|
3582
|
-
};
|
|
3583
|
-
}
|
|
3584
|
-
}
|
|
3585
|
-
return mapPlannerExhaustion(accumulated ?? {
|
|
3586
|
-
ok: false,
|
|
3587
|
-
stdout: "",
|
|
3588
|
-
stderr: "frontend plan segmentation produced no session",
|
|
3589
|
-
failureCategory: "empty-output",
|
|
3590
|
-
durationMs: 0,
|
|
3591
|
-
}, false);
|
|
3592
|
-
}
|
|
3593
1717
|
/**
|
|
3594
1718
|
* Run the two independent Scout evidence surfaces concurrently while keeping
|
|
3595
1719
|
* their typed-event stores isolated. The main Scout store is the only store
|
|
@@ -3597,199 +1721,11 @@ export async function runFrontendPlanSegmentedSessions(input) {
|
|
|
3597
1721
|
* order after both sessions settle. This gives discovery real parallelism
|
|
3598
1722
|
* without allowing sibling models to race a shared revision counter.
|
|
3599
1723
|
*/
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
...representative,
|
|
3606
|
-
ok: failed === undefined,
|
|
3607
|
-
failureCategory: failed?.failureCategory ?? "success",
|
|
3608
|
-
durationMs: Math.max(...results.map((result) => result.durationMs), 0),
|
|
3609
|
-
exitCode: failed ? failed.exitCode : 0,
|
|
3610
|
-
stderr: results.map((result) => result.stderr).filter(Boolean).join("\n"),
|
|
3611
|
-
tokensUsed: results.reduce((total, result) => total + result.tokensUsed, 0),
|
|
3612
|
-
parsedEvents: results.reduce((total, result) => total + result.parsedEvents, 0),
|
|
3613
|
-
attemptedModels: [
|
|
3614
|
-
...new Set(results.flatMap((result) => result.attemptedModels)),
|
|
3615
|
-
],
|
|
3616
|
-
fallbackUsed: results.some((result) => result.fallbackUsed),
|
|
3617
|
-
timedOut: results.some((result) => result.timedOut),
|
|
3618
|
-
};
|
|
3619
|
-
}
|
|
3620
|
-
function combineSequentialPiResults(first, second) {
|
|
3621
|
-
return {
|
|
3622
|
-
...second,
|
|
3623
|
-
durationMs: first.durationMs + second.durationMs,
|
|
3624
|
-
stderr: [first.stderr, second.stderr].filter(Boolean).join("\n"),
|
|
3625
|
-
tokensUsed: first.tokensUsed + second.tokensUsed,
|
|
3626
|
-
parsedEvents: first.parsedEvents + second.parsedEvents,
|
|
3627
|
-
attemptedModels: [
|
|
3628
|
-
...new Set([...first.attemptedModels, ...second.attemptedModels]),
|
|
3629
|
-
],
|
|
3630
|
-
fallbackUsed: first.fallbackUsed || second.fallbackUsed,
|
|
3631
|
-
timedOut: first.timedOut || second.timedOut,
|
|
3632
|
-
};
|
|
3633
|
-
}
|
|
3634
|
-
async function runFrontendScoutParallelSessions(input) {
|
|
3635
|
-
const [{ createTypedEventStore }] = await Promise.all([
|
|
3636
|
-
import("../workflows/dag/frontend-typed-event-store.js"),
|
|
3637
|
-
]);
|
|
3638
|
-
const shards = [
|
|
3639
|
-
{
|
|
3640
|
-
id: "surface",
|
|
3641
|
-
toolName: "record_target_surface",
|
|
3642
|
-
instruction: [
|
|
3643
|
-
"PARALLEL SCOUT SHARD — target surface only.",
|
|
3644
|
-
"Inspect routes, entrypoints, implementation ownership, data source, and existing test entrypoints. Inspect project source/config first; do not recursively explore dependency internals merely to reconfirm standard test-runner behavior. If a concrete required capability cannot be established from project evidence, report the precise gap.",
|
|
3645
|
-
"Call record_target_surface exactly once with the complete runtime-evidenced surface. Do not call record_design_evidence.",
|
|
3646
|
-
].join(" "),
|
|
3647
|
-
},
|
|
3648
|
-
{
|
|
3649
|
-
id: "design",
|
|
3650
|
-
toolName: "record_design_evidence",
|
|
3651
|
-
instruction: [
|
|
3652
|
-
"PARALLEL SCOUT SHARD — design evidence only.",
|
|
3653
|
-
"Inspect project-owned frontend framework, styling/theme conventions, reusable components, and relevant design/spec files. The surface shard owns test-runner/environment discovery; do not duplicate its Vitest/happy-dom investigation. Stop discovery once the design evidence is sufficient and commit it.",
|
|
3654
|
-
"Call record_design_evidence for the evidence you actually read. Do not call record_target_surface.",
|
|
3655
|
-
].join(" "),
|
|
3656
|
-
},
|
|
3657
|
-
];
|
|
3658
|
-
const outcomes = await Promise.all(shards.map(async (shard) => {
|
|
3659
|
-
const cacheKey = JSON.stringify([input.runDir, input.nodeId, shard.id, input.sessionOptions.modelConfig, input.observation?.model, input.observation?.sourceDigest, input.sourceDeclaredPaths]);
|
|
3660
|
-
const cached = input.completedShards?.get(cacheKey);
|
|
3661
|
-
if (cached)
|
|
3662
|
-
return { shard, facts: cached.facts, result: { ...cached.result, durationMs: 0, tokensUsed: 0, parsedEvents: 0, attemptedModels: [], fallbackUsed: false } };
|
|
3663
|
-
let shardTools;
|
|
3664
|
-
let shardResult;
|
|
3665
|
-
try {
|
|
3666
|
-
const store = createTypedEventStore();
|
|
3667
|
-
const shardNodeId = `${input.nodeId}/parallel/${shard.id}`;
|
|
3668
|
-
shardTools = await createFrontendScoutEvidenceTools({
|
|
3669
|
-
attemptId: `${input.attemptId}:parallel:${shard.id}`,
|
|
3670
|
-
store,
|
|
3671
|
-
runDir: input.runDir,
|
|
3672
|
-
nodeId: shardNodeId,
|
|
3673
|
-
workspaceRoot: input.workspaceRoot,
|
|
3674
|
-
sourceDeclaredPaths: input.sourceDeclaredPaths,
|
|
3675
|
-
});
|
|
3676
|
-
const customTools = shardTools.customTools.filter((tool) => typeof tool === "object" &&
|
|
3677
|
-
tool !== null &&
|
|
3678
|
-
tool.name === shard.toolName);
|
|
3679
|
-
const sessionOptions = {
|
|
3680
|
-
...input.sessionOptions,
|
|
3681
|
-
sessionEventsPath: path.join(input.runDir, shardNodeId, "session-events.jsonl"),
|
|
3682
|
-
};
|
|
3683
|
-
const prompt = `${input.basePrompt}\n\n${shard.instruction}`;
|
|
3684
|
-
const tools = [...customTools, ...(input.readBudgetTools ?? [])];
|
|
3685
|
-
shardResult = await observeFrontendSession({
|
|
3686
|
-
...input.observation,
|
|
3687
|
-
phase: `scout/parallel/${shard.id}`,
|
|
3688
|
-
scopeIds: [shard.id],
|
|
3689
|
-
prompt,
|
|
3690
|
-
userMessage: sessionOptions.userMessage,
|
|
3691
|
-
customTools: tools,
|
|
3692
|
-
artifactPath: input.observation?.artifactPath ? `${input.observation.artifactPath}-${shard.id}.json` : undefined,
|
|
3693
|
-
committedCount: () => shardTools?.committedFacts().length ?? 0,
|
|
3694
|
-
durableCommittedCount: () => shardTools?.committedFacts().length ?? 0,
|
|
3695
|
-
}, observer => input.piStepFn({
|
|
3696
|
-
...sessionOptions,
|
|
3697
|
-
onAttemptObservation: observer,
|
|
3698
|
-
prompt,
|
|
3699
|
-
writerToolPolicy: {
|
|
3700
|
-
requireSdk: true,
|
|
3701
|
-
customTools: tools,
|
|
3702
|
-
},
|
|
3703
|
-
}));
|
|
3704
|
-
await shardTools.flush();
|
|
3705
|
-
const facts = shardTools.committedFacts();
|
|
3706
|
-
const expectedKind = shard.id === "design" ? "design-evidence" : "target-surface";
|
|
3707
|
-
if (shardResult.ok && facts.some(record => { const fact = record.fact; return fact.kind === expectedKind && (shard.id !== "surface" || fact.completeness === "complete"); })) {
|
|
3708
|
-
input.completedShards?.set(cacheKey, { result: structuredClone(shardResult), facts: structuredClone(facts) });
|
|
3709
|
-
}
|
|
3710
|
-
return { shard, result: shardResult, tools: shardTools, facts };
|
|
3711
|
-
}
|
|
3712
|
-
catch (error) {
|
|
3713
|
-
const crashMessage = `frontend scout parallel shard ${shard.id} crashed: ${error instanceof Error ? error.message : String(error)}`;
|
|
3714
|
-
return {
|
|
3715
|
-
shard,
|
|
3716
|
-
tools: shardTools,
|
|
3717
|
-
result: shardResult
|
|
3718
|
-
? {
|
|
3719
|
-
...shardResult,
|
|
3720
|
-
ok: false,
|
|
3721
|
-
failureCategory: shardResult.ok
|
|
3722
|
-
? "invalid-output"
|
|
3723
|
-
: shardResult.failureCategory,
|
|
3724
|
-
stderr: [shardResult.stderr, crashMessage]
|
|
3725
|
-
.filter(Boolean)
|
|
3726
|
-
.join("\n"),
|
|
3727
|
-
}
|
|
3728
|
-
: {
|
|
3729
|
-
ok: false,
|
|
3730
|
-
assistantText: "",
|
|
3731
|
-
command: [],
|
|
3732
|
-
durationMs: 0,
|
|
3733
|
-
exitCode: null,
|
|
3734
|
-
failureCategory: "tool-policy",
|
|
3735
|
-
modelDisplay: "unknown",
|
|
3736
|
-
parsedEvents: 0,
|
|
3737
|
-
stderr: crashMessage,
|
|
3738
|
-
stdout: "",
|
|
3739
|
-
timedOut: false,
|
|
3740
|
-
attemptedModels: [],
|
|
3741
|
-
fallbackUsed: false,
|
|
3742
|
-
tokensUsed: 0,
|
|
3743
|
-
},
|
|
3744
|
-
};
|
|
3745
|
-
}
|
|
3746
|
-
}));
|
|
3747
|
-
const ordered = [...outcomes].sort((left, right) => left.shard.id.localeCompare(right.shard.id));
|
|
3748
|
-
try {
|
|
3749
|
-
for (const outcome of ordered) {
|
|
3750
|
-
if (outcome.result.ok) {
|
|
3751
|
-
await input.mainTools.adoptCommittedFacts(outcome.facts ?? outcome.tools?.committedFacts() ?? []);
|
|
3752
|
-
}
|
|
3753
|
-
}
|
|
3754
|
-
}
|
|
3755
|
-
catch (error) {
|
|
3756
|
-
const aggregate = aggregateParallelPiResults(ordered.map((outcome) => outcome.result));
|
|
3757
|
-
return {
|
|
3758
|
-
...aggregate,
|
|
3759
|
-
ok: false,
|
|
3760
|
-
failureCategory: "invalid-output",
|
|
3761
|
-
stderr: [
|
|
3762
|
-
aggregate.stderr,
|
|
3763
|
-
`frontend scout parallel fact merge failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
3764
|
-
]
|
|
3765
|
-
.filter(Boolean)
|
|
3766
|
-
.join("\n"),
|
|
3767
|
-
};
|
|
3768
|
-
}
|
|
3769
|
-
const failed = ordered.find((outcome) => !outcome.result.ok);
|
|
3770
|
-
if (failed) {
|
|
3771
|
-
const aggregate = aggregateParallelPiResults(ordered.map((outcome) => outcome.result));
|
|
3772
|
-
return {
|
|
3773
|
-
...aggregate,
|
|
3774
|
-
ok: false,
|
|
3775
|
-
stderr: `${aggregate.stderr}\nfrontend scout parallel shard failed: ${failed.shard.id}`.trim(),
|
|
3776
|
-
};
|
|
3777
|
-
}
|
|
3778
|
-
const committedKinds = new Set(ordered.flatMap((outcome) => (outcome.facts ?? outcome.tools?.committedFacts() ?? [])
|
|
3779
|
-
.map((record) => record.fact?.kind)
|
|
3780
|
-
.filter((kind) => typeof kind === "string")));
|
|
3781
|
-
const missing = ["target-surface", "design-evidence"].filter((kind) => !committedKinds.has(kind));
|
|
3782
|
-
if (missing.length > 0) {
|
|
3783
|
-
const fallback = aggregateParallelPiResults(ordered.map((outcome) => outcome.result));
|
|
3784
|
-
return {
|
|
3785
|
-
...fallback,
|
|
3786
|
-
ok: false,
|
|
3787
|
-
failureCategory: "invalid-output",
|
|
3788
|
-
stderr: `${fallback.stderr}\nfrontend scout parallel shards committed no ${missing.join(" or ")} fact`.trim(),
|
|
3789
|
-
};
|
|
3790
|
-
}
|
|
3791
|
-
return aggregateParallelPiResults(ordered.map((outcome) => outcome.result));
|
|
3792
|
-
}
|
|
1724
|
+
/**
|
|
1725
|
+
* 主执行入口:四个阶段顺序组织(准备 → 会话执行 → 写证据判定 → 结果映射)。
|
|
1726
|
+
* 阶段边界即函数内注释界碑;进一步函数级拆分需把跨阶段可变状态打包为显式
|
|
1727
|
+
* state 对象,属高风险机械改写,暂缓(见执行计划 Step 5 决策)。
|
|
1728
|
+
*/
|
|
3793
1729
|
export async function executeDagPiNode(input, meta, piStepFn = executePiStep, writeGuardDependencies = DEFAULT_DAG_PI_WRITE_GUARD_DEPENDENCIES) {
|
|
3794
1730
|
const started = Date.now();
|
|
3795
1731
|
const persona = resolveDagPiPersona(input.task);
|
|
@@ -4971,6 +2907,7 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
4971
2907
|
}
|
|
4972
2908
|
return mapped;
|
|
4973
2909
|
}
|
|
2910
|
+
// ── 阶段 3/4:写证据判定(write guard / 变更清单 / writerOutcome / completeness)──
|
|
4974
2911
|
let writeGuardOk = true;
|
|
4975
2912
|
let writeGuardViolations = [];
|
|
4976
2913
|
let changeManifestAfterStatus;
|