diff --git a/CHANGELOG.md b/CHANGELOG.md index eedf45c..f9fcdc6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,25 @@ ## Unreleased +### Builder Packet & Repository Code Retrieval — vNext.10.2 Phase 5 + +- Added a deterministic `BuilderPacketCompiler` that turns an explicitly + Secondary-selected Objective WorkUnit into a small implementation packet by + reusing `RepositoryContextIndex`, current worktree bytes, section extraction, + bounded structural expansion, and durable `ContextSelectionPlan` records. +- Added automatic target, test, direct-dependency, and nearby reference-pattern + selection with per-section repository/path/hash/range/reason provenance, + verified dependency summaries, trusted verification hints, bounded prior + failure evidence, multi-repository identity, and stable semantic packet hashes. +- Added honest `INSUFFICIENT_CONTEXT`, `AMBIGUOUS_TARGET`, and model-returned + `NEEDS_MORE_CONTEXT` outcomes. These outcomes create no edit candidate and do + not grant the direct model shell, Git, filesystem, repository browsing, or + other agent authority. +- Added deterministic fake-model, stale-index, budget/sectioning, + protected-path, multi-repository, hash-stability, and large-fixture coverage, + plus a gated real local-model qualification. Default strong-builder routing + and explicit-only Secondary selection remain unchanged. + ### Secondary Objective Builder — vNext.10.2 Phase 4 - Added an explicit-only, provider-neutral `SecondaryModelInference` and diff --git a/contracts/context-contract.json b/contracts/context-contract.json index 411ec30..fe1953b 100644 --- a/contracts/context-contract.json +++ b/contracts/context-contract.json @@ -141,6 +141,7 @@ "MODULE_PROXIMITY", "PRIOR_TASK_RELEVANCE", "PROGRESSIVE_EXPANSION", + "REFERENCE_PATTERN", "SYMBOL_MATCH", "TEST_SOURCE_PAIR", "TOKEN_OVERLAP" diff --git a/contracts/orchestration-contract.json b/contracts/orchestration-contract.json index 2d2157b..07606ad 100644 --- a/contracts/orchestration-contract.json +++ b/contracts/orchestration-contract.json @@ -111,6 +111,11 @@ "MODEL_DIAGNOSIS", "PROVIDER_SIGNAL" ], + "builderPacketCompilationFailures": [ + "AMBIGUOUS_TARGET", + "INSUFFICIENT_CONTEXT", + "STALE_SOURCE_CONTEXT" + ], "computeLocalities": [ "LOCAL", "REMOTE", @@ -1043,6 +1048,7 @@ ], "secondaryBuilderAttemptStatuses": [ "CANDIDATE_READY", + "CONTEXT_INSUFFICIENT", "EDITS_APPLIED", "FAILED", "INFERENCE_COMPLETED", @@ -1051,12 +1057,14 @@ "VERIFICATION_FAILED" ], "secondaryBuilderFailures": [ + "AMBIGUOUS_TARGET", "APPLY_FAILURE", "CANCELLED", "CONTEXT_TOO_LARGE", "EMPTY_EDIT_SET", "FORBIDDEN_EDIT", "INFERENCE_UNAVAILABLE", + "INSUFFICIENT_CONTEXT", "INVALID_STRUCTURED_OUTPUT", "STALE_APPROVED_PROJECTION", "STALE_SOURCE_CONTEXT", diff --git a/contracts/schema-versions.json b/contracts/schema-versions.json index 39f944e..a085464 100644 --- a/contracts/schema-versions.json +++ b/contracts/schema-versions.json @@ -7,12 +7,13 @@ "attemptRecord": "1.0.0", "autonomyTelemetry": "1.0.0", "browserScenario": "1.0.0", + "builderPacketCompilation": "1.0.0", "candidateArtifact": "1.1.0", "closureLedger": "1.0.0", "contextMetrics": "1.0.0", "contextPackage": "1.0.0", "contextProjection": "1.0.0", - "contextSelectionPlan": "1.0.0", + "contextSelectionPlan": "1.1.0", "contractConflict": "1.0.0", "controlPlaneRepair": "1.0.0", "dogfoodRun": "1.0.0", @@ -60,9 +61,9 @@ "runnerContextCapabilities": "1.0.0", "runnerOutput": "1.0.0", "schedulingDecision": "1.0.0", - "secondaryBuilderAttempt": "1.0.0", - "secondaryBuilderPacket": "1.0.0", - "secondaryBuilderResult": "1.0.0", + "secondaryBuilderAttempt": "1.1.0", + "secondaryBuilderPacket": "1.1.0", + "secondaryBuilderResult": "1.1.0", "specIntakeApproval": "1.0.0", "specIntakeDelta": "1.0.0", "specIntakeGrounding": "1.0.0", diff --git a/docs/README.md b/docs/README.md index 8259e9e..28370db 100644 --- a/docs/README.md +++ b/docs/README.md @@ -156,6 +156,9 @@ the intent in the evening, run one command, read the report in the morning. - [Secondary Objective Builder](orchestration/secondary-objective-builder.md) — explicit direct-model edits inside the existing governed worktree and candidate lifecycle; capability without automatic routing. +- [Builder Packet compilation](orchestration/builder-packet-compilation.md) + — deterministic, fresh, bounded source/test/dependency retrieval for an + explicitly selected Secondary Builder; context quality without routing. - [Context projection](orchestration/context-projection.md) — share truth, not context: what each isolated worker sees, hashed and staleness-checked. - [Sub-agent isolation](orchestration/subagent-isolation.md) — worker diff --git a/docs/local-model.md b/docs/local-model.md index a3f5403..88ce802 100644 --- a/docs/local-model.md +++ b/docs/local-model.md @@ -7,6 +7,9 @@ implementation still reaches the configured large agent; the explicit-only [Secondary Objective Builder](orchestration/secondary-objective-builder.md) can also use this endpoint for bounded worktree edits. One managed server serves every logical role — roles are prompts, not processes. +Its [Builder Packet compiler](orchestration/builder-packet-compilation.md) +selects fresh source and tests first, so the direct model needs no repository +tools for ordinary bounded implementation work. The local model is a **worker**, never an authority: its answers are schema-validated structured proposals that deterministic policy accepts, diff --git a/docs/orchestration/builder-packet-compilation.md b/docs/orchestration/builder-packet-compilation.md new file mode 100644 index 0000000..0733a13 --- /dev/null +++ b/docs/orchestration/builder-packet-compilation.md @@ -0,0 +1,148 @@ +# Builder Packet compilation + +Phase 5 of vNext.10.2 prepares the implementation evidence a bounded direct +model needs. It does not choose a model. An Objective WorkUnit reaches this +path only after a caller explicitly selects the Secondary Objective Builder. + +```text +WorkUnit + ContextProjection + verified dependency candidates + + trusted verification configuration + ↓ deterministic query +existing RepositoryContextIndex metadata + ↓ ranking + bounded structural expansion +fresh bytes from the isolated Objective worktree + ↓ sectioning + category/character budgets +small, explainable SecondaryBuilderPacket + ↓ one provider-neutral inference request +normal worktree → verification → candidate lifecycle +``` + +## Compiler boundary + +`BuilderPacketCompiler` is provider-neutral and independently testable. +`SecondaryBuilderContextCompiler` is the managed implementation. Compilation +does not invoke an LLM, run tests, execute commands, or grant repository +tools. It receives the WorkUnit, approved `ContextProjection`, attempt and +baseline identity, verified dependency summaries, bounded prior failure +evidence, trusted verification hints, and a capability-neutral budget. + +The compiler returns either a packet or an honest `INSUFFICIENT_CONTEXT` / +`AMBIGUOUS_TARGET` result. It does not guess a target and it does not decide +whether a strong model should run instead. + +## Existing index and deterministic retrieval + +There is no second search engine. Phase 5 reuses the context package's +`RepositoryContextIndex`, retrieval query, ranking, section extraction, +freshness, budget, and `ContextSelectionPlan` contracts. No vector database, +embedding model, semantic search service, or model reranker is required. + +The query is derived from durable WorkUnit evidence: title, goal, expected +artifacts and areas, contract/acceptance text, named paths and symbols, +verified dependency paths, prior selected paths, and bounded failure output. +Ranking preserves the existing hierarchy: explicit and failure references +are mandatory; symbol and test/source evidence outrank dependency and module +proximity; token overlap is only a hint. Nearby same-module implementations +with the same suffix/convention or shared imports may receive the bounded +`REFERENCE_PATTERN` reason. They are labeled as examples, never requirements. + +Default expansion stops at `ADJACENT_DEPENDENCIES`: target source, direct +dependencies/dependents, and paired tests. The API exposes the existing +closed expansion levels for a later bounded widening decision; Phase 5 does +not implement an agentic search loop. + +## Freshness and worktree state + +The canonical repository index is reusable metadata. For an Objective +attempt, the compiler overlays only paths changed between the canonical +checkout and the isolated worktree, including already-verified dependency +patches. This keeps dependency interfaces current without scanning the full +worktree. Unfinished sibling worktrees are never an input. + +Only selected files are materialized. Selection re-reads current worktree +bytes and compares SHA-256 hashes. Each packet section carries the complete +file hash plus a hash of the exact supplied whole file or section. Execution +checks those hashes again immediately before inference. A stale cache may be +refreshed, but stale source is never presented as current. + +## Packet contents and budgets + +Approved truth stays in `ContextProjection`; repository evidence does not +duplicate or replace it. Packet 1.1 contains: + +- objective and WorkUnit identity, approved requirements, constraints, ADRs, + decisions, and verified prior-work summaries; +- explicit targets; +- current implementation source or deterministic source sections; +- relevant tests as a first-class section; +- clearly labeled reference patterns; +- verified dependency-candidate summaries and changed paths; +- bounded prior failure evidence and trusted verification hints; +- retrieval-plan references, expansion level, retrieval metrics, and quality + facts. + +Category and character limits preserve targets and tests before low-value +siblings. Large files use deterministic declaration/line-centred sections; +small files may be included whole. Generated, vendor, ignored, binary, +credential-shaped, protected, unreadable, and oversized artifacts retain the +existing index exclusions. Compilation never runs verification; hints come +only from configured commands and actual verification remains after edits. + +## Explainability and semantic identity + +Every source, test, and reference item records repository identity, path, +selection reason, file hash, section hash, optional line range, and symbols. +The exact metadata plan is stored under the existing job-scoped context plan +store; source bodies remain repository-derived and are not copied into a new +cache. + +`contentHash`/`packetHash` canonically bind WorkUnit and approved-truth +identity, selected repositories/paths/hashes/ranges, dependency evidence, +verification hints, and packet schema. `createdAt` and observational timing +metrics are excluded, so recompiling unchanged logical inputs has the same +semantic identity. + +## Multi-repository behavior + +Repository identity namespaces every path. A justified cross-repository +WorkUnit may supply multiple bounded indexes and roots, and its packet can +therefore distinguish `backend:src/client.ts` from +`frontend:src/client.ts`. Secondary repositories must be explicitly supplied +and justified; compilation does not make all discovered repositories +available automatically. Edit authority remains with the primary isolated +worktree. + +## Context insufficiency + +Missing explicit targets, unresolved verified dependencies, protected or +unmaterializable target source, and equal plausible targets stop compilation. +During inference the model may also return `NEEDS_MORE_CONTEXT` with bounded +reasons and no edits. Both paths create no candidate. Phase 6 may use recorded +quality facts for eligibility; Phase 7 may decide widening, repair, or +fallback. Neither policy exists in Phase 5. + +Metrics record indexed files considered, ranked candidates, selected files +and sections, source/test characters, reference/dependency counts, budget +use, mandatory references retained, expansion depth, stale entries, +selection duration, and index reuse. Quality facts record target resolution, +ambiguity, tests, verification hints, reference patterns, dependency +completeness, budget utilization, and structural sufficiency. They are facts, +not routing eligibility. + +## Qualification + +Deterministic tests cover explicit target retrieval, dependency/test +expansion, reference patterns, budget pressure, sectioning, stale indexes, +missing and ambiguous targets, protected paths, multi-repo identity, stable +hashes, `NEEDS_MORE_CONTEXT`, large-index boundedness, and the automatic +compiler → Secondary Builder → verified candidate flow. + +The opt-in real llama.cpp/Qwen qualification now compiles the target source +automatically before inference and records packet metrics. CI skips it unless +the local server/model environment variables documented in +[Secondary Objective Builder](secondary-objective-builder.md#qualification) +are present. + +Builder Packet compilation does not decide which model should execute the +task. Phase 6 eligibility, Phase 7 routing/repair/fallback, LLM Gateway, +Vector RAG, and OpenMind are not implemented here. diff --git a/docs/orchestration/secondary-objective-builder.md b/docs/orchestration/secondary-objective-builder.md index 3082898..3a2bd82 100644 --- a/docs/orchestration/secondary-objective-builder.md +++ b/docs/orchestration/secondary-objective-builder.md @@ -5,7 +5,7 @@ Objective runtime. A bounded direct model can propose concrete file edits for one build WorkUnit, while SpecBridge retains every authority-bearing action: ```text -ContextProjection (approved truth) + explicit bounded source context +ContextProjection (approved truth) + bounded repository context ↓ one SecondaryModelInference request strict CREATE/REPLACE proposal ↓ SpecBridge validates and writes @@ -17,8 +17,9 @@ normal candidate → evaluation → aggregation → single-writer integration Phase 4 creates capability. It does **not** automatically route Objective work to the secondary backend. With no explicit `secondaryObjectiveBuilder` selection, BUILDER behavior remains the existing large-agent path. Automatic -packet compilation, eligibility, routing, repair, and fallback belong to later -phases. +eligibility, routing, repair, and fallback belong to later phases. Phase 5 +adds deterministic [Builder Packet compilation](builder-packet-compilation.md) +without changing this explicit-only selection rule. ## Not an Agent Harness @@ -43,12 +44,15 @@ heuristically. It carries WorkUnit and Objective identity, goal, expected artifacts/areas, acceptance criteria, projected contracts, ADRs and approved decisions, constitution constraints, verified dependency evidence, fixed forbidden -changes, verification names, and explicitly prepared source files. +changes, trusted verification names, and compiler-selected source sections, +tests, dependency evidence, and reference patterns. `ContextProjection` remains approved durable truth. Source is deliberately a -separate `sourceContext` list of worktree-relative path, SHA-256 content hash, -and bounded current UTF-8 content. Phase 4 callers choose those files -explicitly; there is no automatic repository retrieval or target inference. +separate repository context with repository identity, path, selection reason, +whole-file hash, exact section hash/range, and bounded current UTF-8 content. +Legacy Phase 4 callers may still provide `sourceContext` explicitly. When an +explicit Secondary selection omits it, Phase 5 compiles the packet from the +WorkUnit and existing `RepositoryContextIndex`. Immediately before inference, SpecBridge reloads approved Mission truth and checks projection freshness, then re-reads every source file and checks its hash and bytes. Stale approved truth or source fails the attempt before the @@ -105,7 +109,10 @@ it is not rebuilt merely because the process restarted. Failure kinds distinguish inference unavailability, timeout, cancellation, invalid structured output, empty edits, forbidden edits, stale approved/source -context, application failure, verification failure, and an oversized context. +context, insufficient or ambiguous retrieval, application failure, +verification failure, and an oversized context. A model may return the +structured `NEEDS_MORE_CONTEXT` status; this records an insufficient attempt +and creates no candidate. Phase 4 performs no secondary repair retry and no automatic large-model fallback. diff --git a/integrations/claude-code-plugin/specbridge/dist/checksums.json b/integrations/claude-code-plugin/specbridge/dist/checksums.json index 578dec9..6504090 100644 --- a/integrations/claude-code-plugin/specbridge/dist/checksums.json +++ b/integrations/claude-code-plugin/specbridge/dist/checksums.json @@ -7,12 +7,12 @@ "bytes": 155994 }, "cli.cjs": { - "sha256": "6379044267e76482b59da2c3b57ce97fe5f758815d10a9fefa0c8267bb53e9a9", - "bytes": 5588127 + "sha256": "8bc205156d51810ec082b0e9aa3be5d2f12c307706e680796ca4909360dee2e7", + "bytes": 5634305 }, "mcp-server.cjs": { - "sha256": "6b8a8ce0992a5cefa9e15aadbe2e0aa22a97bf341c0db8c8a599a5ba9a8024d5", - "bytes": 3644860 + "sha256": "3854958225b757a1e0a2197471faf308fbf4a97ab58c51507114e7009748aea5", + "bytes": 3654373 } } } diff --git a/integrations/claude-code-plugin/specbridge/dist/cli.cjs b/integrations/claude-code-plugin/specbridge/dist/cli.cjs index 8089068..93bd791 100644 --- a/integrations/claude-code-plugin/specbridge/dist/cli.cjs +++ b/integrations/claude-code-plugin/specbridge/dist/cli.cjs @@ -50310,8 +50310,6 @@ var import_fs34 = require("fs"); var import_path36 = __toESM(require("path"), 1); var import_fs35 = require("fs"); var import_path37 = __toESM(require("path"), 1); -var import_fs36 = require("fs"); -var import_path38 = __toESM(require("path"), 1); // ../../packages/context/dist/index.js var import_crypto10 = require("crypto"); @@ -50402,6 +50400,8 @@ var CONTEXT_SELECTION_REASONS = [ "DEPENDENCY_PROXIMITY", /** In the same module/package as an already-selected file. */ "MODULE_PROXIMITY", + /** A bounded nearby implementation with the same structural convention. */ + "REFERENCE_PATTERN", /** A declared symbol matched a query symbol. */ "SYMBOL_MATCH", /** Filename/path tokens overlapped the query tokens. */ @@ -51963,6 +51963,7 @@ var DEFAULT_RANKING_WEIGHTS = Object.freeze({ testSourcePair: 180, dependencyProximity: 120, moduleProximity: 40, + referencePattern: 150, tokenOverlapPerToken: 12, tokenOverlapCap: 96, priorTaskRelevance: 60, @@ -52100,6 +52101,31 @@ function rankCandidates(index, query, options = {}) { for (const dependent of index.dependentsOf(anchor)) { add2(dependent, "DEPENDENCY_PROXIMITY", weights.dependencyProximity, level2, anchor); } + const anchorEntry = index.get(anchor); + if (anchorEntry !== void 0 && anchorEntry.kind === "source") { + const suffixes = structuralSuffixes(anchorEntry.path, anchorEntry.symbols); + for (const sibling of index.siblingsIn(anchorEntry.module)) { + if (sibling === anchor) continue; + const entry2 = index.get(sibling); + if (entry2 === void 0 || entry2.kind !== "source" || entry2.language !== anchorEntry.language) { + continue; + } + const sharedImportCount = entry2.imports.filter( + (specifier) => anchorEntry.imports.includes(specifier) + ).length; + const sameConvention = structuralSuffixes(entry2.path, entry2.symbols).some( + (suffix) => suffixes.includes(suffix) + ); + if (!sameConvention && sharedImportCount < 2) continue; + add2( + sibling, + "REFERENCE_PATTERN", + weights.referencePattern + Math.min(30, sharedImportCount * 10), + level2, + sameConvention ? anchor : `${anchor} (${sharedImportCount} shared imports)` + ); + } + } } const anchorModules = new Set(anchors.map((anchor) => index.get(anchor)?.module ?? "")); for (const module2 of anchorModules) { @@ -52134,6 +52160,16 @@ function rankCandidates(index, query, options = {}) { ); return ranked.slice(0, maxCandidates); } +function structuralSuffixes(relativePath, symbols) { + const base = relativePath.split("/").at(-1)?.replace(/\.[^.]+$/, "") ?? ""; + const values = [base, ...symbols]; + const suffixes = /* @__PURE__ */ new Set(); + for (const value of values) { + const match = value.match(/([A-Z][a-z0-9]{2,})$/); + if (match?.[1] !== void 0) suffixes.add(match[1].toLowerCase()); + } + return [...suffixes]; +} var DEFAULT_SECTION_OPTIONS = { wholeFileUnderChars: 6e3, targetSectionChars: 4e3, @@ -52654,7 +52690,8 @@ function deduplicateItems(items, options = {}) { } const digest2 = contentDigest(item); const identity3 = sourceIdentity(item); - const contentIndex = byContent.get(digest2); + const contentKey = identity3?.startsWith("key:repo:") === true ? `${digest2}:${identity3}` : digest2; + const contentIndex = byContent.get(contentKey); if (contentIndex !== void 0) { const incumbent = survivors[contentIndex]; if (incumbent !== void 0) { @@ -52662,7 +52699,7 @@ function deduplicateItems(items, options = {}) { drop(incumbent, item, "identical-content"); survivors[contentIndex] = void 0; survivors.push(item); - byContent.set(digest2, survivors.length - 1); + byContent.set(contentKey, survivors.length - 1); if (identity3 !== void 0) bySource.set(identity3, survivors.length - 1); } else { drop(item, incumbent, "identical-content"); @@ -52681,7 +52718,7 @@ function deduplicateItems(items, options = {}) { survivors[sourceIndex] = void 0; survivors.push(item); bySource.set(identity3, survivors.length - 1); - byContent.set(digest2, survivors.length - 1); + byContent.set(contentKey, survivors.length - 1); } else { drop(item, incumbent, kind); } @@ -52689,7 +52726,7 @@ function deduplicateItems(items, options = {}) { } bySource.set(identity3, survivors.length); } - byContent.set(digest2, survivors.length); + byContent.set(contentKey, survivors.length); survivors.push(item); } const kept = survivors.filter((item) => item !== void 0); @@ -53033,9 +53070,11 @@ function initialExpansionState(input) { function beginAttemptExpansion(state, now7) { return { ...state, expansionsThisAttempt: 0, updatedAt: now7 }; } -var CONTEXT_SELECTION_PLAN_SCHEMA_VERSION = "1.0.0"; +var CONTEXT_SELECTION_PLAN_SCHEMA_VERSION = "1.1.0"; var shortText3 = external_exports.string().min(1).max(CONTEXT_LIMITS.maxShortTextChars); var selectedContextItemSchema = external_exports.object({ + /** Repository identity when a plan participates in a multi-repo packet. */ + repositoryId: shortText3.optional(), /** Workspace-relative path, forward slashes. */ path: shortText3, /** Content hash the selection was made against. Verified before use. */ @@ -53054,6 +53093,7 @@ var selectedContextItemSchema = external_exports.object({ detail: shortText3.optional() }).passthrough(); var contextPointerSchema = external_exports.object({ + repositoryId: shortText3.optional(), path: shortText3, reason: external_exports.enum(CONTEXT_SELECTION_REASONS), /** @@ -53071,6 +53111,7 @@ var contextPointerSchema = external_exports.object({ detail: shortText3.optional() }).passthrough(); var excludedCandidateSchema = external_exports.object({ + repositoryId: shortText3.optional(), path: shortText3, reason: external_exports.enum(CONTEXT_EXCLUSION_REASONS), score: external_exports.number().int(), @@ -53083,6 +53124,8 @@ var contextSelectionPlanSchema = external_exports.object({ taskId: shortText3, nodeId: shortText3.optional(), attemptId: shortText3.optional(), + /** Repository identity; paths are unique only within this namespace. */ + repositoryId: shortText3.optional(), strategy: external_exports.enum(CONTEXT_STRATEGIES), shape: external_exports.enum(CONTEXT_SHAPES), role: external_exports.enum(RETRIEVAL_ROLES), @@ -53129,6 +53172,17 @@ var contextSelectionPlanSchema = external_exports.object({ }).passthrough(), /** Stable identities of the reusable prompt components (observability). */ componentHashes: external_exports.record(external_exports.string().nullable()).default({}), + /** Phase 5 facts; metrics are evidence for later policy, never eligibility here. */ + builderPacket: external_exports.object({ + contextSufficient: external_exports.boolean(), + explicitTargetResolved: external_exports.boolean(), + targetAmbiguity: external_exports.boolean(), + testsFound: external_exports.boolean(), + verificationHintsAvailable: external_exports.boolean(), + referencePatternFound: external_exports.boolean(), + dependencyContextComplete: external_exports.boolean(), + sourceBudgetUtilization: external_exports.number().min(0) + }).passthrough().optional(), createdAt: shortText3 }).passthrough(); function truncate(value, max = CONTEXT_LIMITS.maxShortTextChars) { @@ -53156,6 +53210,10 @@ function selectWorkingSet(input) { }); continue; } + if (input.shape === "MATERIALIZED" && !candidate.mandatory && selected.length >= maxSelected) { + excluded.push({ path: candidate.path, reason: "RANKED_BELOW_CUTOFF", score: candidate.score }); + continue; + } const resolved2 = resolveFresh(input.rootDir, candidate.entry, { withContent: true }); if (resolved2.status === "missing") { excluded.push({ @@ -53952,6 +54010,8 @@ async function buildEfficientContext(input) { } // ../../packages/orchestration/dist/index.js +var import_fs36 = require("fs"); +var import_path38 = __toESM(require("path"), 1); var import_crypto20 = require("crypto"); var import_fs37 = require("fs"); var import_path39 = __toESM(require("path"), 1); @@ -62595,12 +62655,15 @@ ${command.stderrTail}`).join("\n"); escalated: false }; } -var SECONDARY_BUILDER_PACKET_SCHEMA_VERSION = "1.0.0"; -var SECONDARY_BUILDER_RESULT_SCHEMA_VERSION = "1.0.0"; -var SECONDARY_BUILDER_ATTEMPT_SCHEMA_VERSION = "1.0.0"; +var SECONDARY_BUILDER_PACKET_SCHEMA_VERSION = "1.1.0"; +var SECONDARY_BUILDER_RESULT_SCHEMA_VERSION = "1.1.0"; +var SECONDARY_BUILDER_ATTEMPT_SCHEMA_VERSION = "1.1.0"; var SECONDARY_BUILDER_LIMITS = { ...LOCAL_EXECUTION_LIMITS, maxSourceFiles: 16, + maxTests: 6, + maxReferencePatterns: 4, + maxDependencyContext: 12, maxSourceFileChars: 32768, maxSourceBytes: 262144, maxPacketCharacters: 524288, @@ -62611,11 +62674,60 @@ var boundedText = (max) => external_exports.string().min(1).max(max); var shortText42 = boundedText(512); var sha256 = external_exports.string().regex(/^[a-f0-9]{64}$/); var secondarySourceContextSchema = external_exports.object({ - /** Worktree-relative path. Whole-file source only in Phase 4. */ + /** Repository namespace. Paths are unique only within this identity. */ + repositoryId: shortText42.default("primary"), + /** Repository-relative path. */ path: boundedText(SECONDARY_BUILDER_LIMITS.maxPathChars), - /** Hash of the exact UTF-8 content below. */ + /** Hash of the complete current file bytes. */ contentHash: sha256, - content: external_exports.string().max(SECONDARY_BUILDER_LIMITS.maxSourceFileChars) + /** Hash of the exact bounded content below (whole file or section). */ + sectionHash: sha256.optional(), + content: external_exports.string().max(SECONDARY_BUILDER_LIMITS.maxSourceFileChars), + reason: external_exports.enum(CONTEXT_SELECTION_REASONS).default("EXPLICIT_ACTION_REFERENCE"), + startLine: external_exports.number().int().min(1).optional(), + endLine: external_exports.number().int().min(1).optional(), + symbols: external_exports.array(shortText42).max(12).default([]) +}).strict(); +var secondaryBuilderTargetSchema = external_exports.object({ + repositoryId: shortText42, + path: boundedText(SECONDARY_BUILDER_LIMITS.maxPathChars), + symbols: external_exports.array(shortText42).max(12).default([]), + reason: external_exports.enum(CONTEXT_SELECTION_REASONS) +}).strict(); +var secondaryDependencyContextSchema = external_exports.object({ + workUnitId: shortText42, + summary: boundedText(2e3), + changedFiles: external_exports.array( + external_exports.object({ repositoryId: shortText42, path: boundedText(SECONDARY_BUILDER_LIMITS.maxPathChars) }).strict() + ).max(60), + exportedSymbols: external_exports.array(shortText42).max(30).default([]), + verificationPassed: external_exports.literal(true) +}).strict(); +var secondaryBuilderPacketMetricsSchema = external_exports.object({ + indexedFilesConsidered: external_exports.number().int().min(0), + candidateCount: external_exports.number().int().min(0), + selectedFiles: external_exports.number().int().min(0), + selectedSections: external_exports.number().int().min(0), + sourceCharacters: external_exports.number().int().min(0), + testCharacters: external_exports.number().int().min(0), + referencePatternCount: external_exports.number().int().min(0), + dependencyContextCount: external_exports.number().int().min(0), + budgetUtilization: external_exports.number().min(0), + mandatoryRefsRetained: external_exports.number().int().min(0), + expansionDepth: external_exports.number().int().min(0).max(4), + staleEntriesEncountered: external_exports.number().int().min(0), + selectionDurationMs: external_exports.number().int().min(0), + indexReused: external_exports.boolean() +}).strict(); +var secondaryBuilderPacketQualitySchema = external_exports.object({ + explicitTargetResolved: external_exports.boolean(), + targetAmbiguity: external_exports.boolean(), + testsFound: external_exports.boolean(), + verificationHintsAvailable: external_exports.boolean(), + referencePatternFound: external_exports.boolean(), + dependencyContextComplete: external_exports.boolean(), + sourceBudgetUtilization: external_exports.number().min(0), + contextSufficient: external_exports.boolean() }).strict(); var projectedContractSchema = external_exports.object({ contractId: shortText42, @@ -62631,7 +62743,10 @@ var secondaryBuilderPacketSchema = external_exports.object({ projectionHash: sha256, contractSnapshotHash: sha256, sourceContextHash: sha256, + /** Semantic identity; excludes createdAt and observational metrics. */ + contentHash: sha256, packetHash: sha256, + createdAt: shortText42.optional(), objective: external_exports.object({ nodeId: shortText42, taskId: shortText42, @@ -62655,6 +62770,19 @@ var secondaryBuilderPacketSchema = external_exports.object({ priorWorkEvidence: external_exports.array(boundedText(2e3)).max(30) }).strict(), sourceContext: external_exports.array(secondarySourceContextSchema).max(SECONDARY_BUILDER_LIMITS.maxSourceFiles), + targets: external_exports.array(secondaryBuilderTargetSchema).max(SECONDARY_BUILDER_LIMITS.maxSourceFiles), + tests: external_exports.array(secondarySourceContextSchema).max(SECONDARY_BUILDER_LIMITS.maxTests), + referencePatterns: external_exports.array(secondarySourceContextSchema).max(SECONDARY_BUILDER_LIMITS.maxReferencePatterns), + dependencyContext: external_exports.array(secondaryDependencyContextSchema).max(SECONDARY_BUILDER_LIMITS.maxDependencyContext), + priorFailureEvidence: external_exports.array(boundedText(2e3)).max(20), + retrievalPlanRef: shortText42, + retrievalPlanRefs: external_exports.array(shortText42).max(12), + expansion: external_exports.object({ + level: external_exports.enum(CONTEXT_EXPANSION_LEVELS), + remainingLevels: external_exports.number().int().min(0).max(4) + }).strict(), + contextMetrics: secondaryBuilderPacketMetricsSchema, + quality: secondaryBuilderPacketQualitySchema, forbiddenChanges: external_exports.array(boundedText(1e3)).max(30), verificationHints: external_exports.array(boundedText(1e3)).max(30) }).strict(); @@ -62665,16 +62793,26 @@ var secondaryStructuredEditSchema = external_exports.object({ }).strict(); var secondaryBuilderResultSchema = external_exports.object({ schemaVersion: external_exports.literal(SECONDARY_BUILDER_RESULT_SCHEMA_VERSION), + status: external_exports.enum(["EDITS", "NEEDS_MORE_CONTEXT"]).default("EDITS"), summary: boundedText(SECONDARY_BUILDER_LIMITS.maxSummaryChars), edits: external_exports.array(secondaryStructuredEditSchema).max(SECONDARY_BUILDER_LIMITS.maxEdits), - notes: external_exports.array(external_exports.string().max(SECONDARY_BUILDER_LIMITS.maxNoteChars)).max(SECONDARY_BUILDER_LIMITS.maxNotes).optional() -}).strict(); + notes: external_exports.array(external_exports.string().max(SECONDARY_BUILDER_LIMITS.maxNoteChars)).max(SECONDARY_BUILDER_LIMITS.maxNotes).optional(), + needsMoreContextReasons: external_exports.array(external_exports.string().min(1).max(SECONDARY_BUILDER_LIMITS.maxNoteChars)).min(1).max(8).optional() +}).strict().superRefine((result, context) => { + if (result.status === "NEEDS_MORE_CONTEXT" && result.needsMoreContextReasons === void 0) { + context.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["needsMoreContextReasons"], message: "required when status is NEEDS_MORE_CONTEXT" }); + } + if (result.status === "NEEDS_MORE_CONTEXT" && result.edits.length > 0) { + context.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["edits"], message: "must be empty when more context is required" }); + } +}); var SECONDARY_BUILDER_JSON_SCHEMA = { type: "object", additionalProperties: false, - required: ["schemaVersion", "summary", "edits"], + required: ["schemaVersion", "status", "summary", "edits"], properties: { schemaVersion: { type: "string", const: SECONDARY_BUILDER_RESULT_SCHEMA_VERSION }, + status: { type: "string", enum: ["EDITS", "NEEDS_MORE_CONTEXT"] }, summary: { type: "string", minLength: 1, maxLength: SECONDARY_BUILDER_LIMITS.maxSummaryChars }, edits: { type: "array", @@ -62694,6 +62832,12 @@ var SECONDARY_BUILDER_JSON_SCHEMA = { type: "array", maxItems: SECONDARY_BUILDER_LIMITS.maxNotes, items: { type: "string", maxLength: SECONDARY_BUILDER_LIMITS.maxNoteChars } + }, + needsMoreContextReasons: { + type: "array", + minItems: 1, + maxItems: 8, + items: { type: "string", minLength: 1, maxLength: SECONDARY_BUILDER_LIMITS.maxNoteChars } } } }; @@ -62701,6 +62845,9 @@ var SECONDARY_BUILDER_SYSTEM_PROMPT = [ "You are a bounded SECONDARY OBJECTIVE BUILDER, not an agent harness.", "You have no shell, git, filesystem, package-manager, test, credential, or tool access.", "The packet contains all approved truth and source bytes you may use.", + "You have been given the repository context selected for this task; do not explore beyond it.", + "Do not invent files, APIs, symbols, or architecture unsupported by the packet.", + "If the packet is insufficient to implement safely, return NEEDS_MORE_CONTEXT with bounded reasons and no edits.", "Return exactly one JSON document matching the supplied schema.", "Return complete UTF-8 file contents using only CREATE or REPLACE.", "Never return Markdown, diffs, commands, deletes, renames, symlinks, or authority/config edits.", @@ -62764,6 +62911,8 @@ var SECONDARY_BUILDER_FAILURES = [ "VERIFICATION_FAILURE", "TIMEOUT", "CONTEXT_TOO_LARGE", + "INSUFFICIENT_CONTEXT", + "AMBIGUOUS_TARGET", "CANCELLED" ]; var SECONDARY_BUILDER_ATTEMPT_STATUSES = [ @@ -62772,6 +62921,7 @@ var SECONDARY_BUILDER_ATTEMPT_STATUSES = [ "PROPOSAL_VALIDATED", "EDITS_APPLIED", "VERIFICATION_FAILED", + "CONTEXT_INSUFFICIENT", "CANDIDATE_READY", "FAILED" ]; @@ -62833,18 +62983,30 @@ function stableStringify(value) { return JSON.stringify(stable(value)); } function packetBody(packet) { - return stableStringify(packet); + const { createdAt: _createdAt, contextMetrics: _contextMetrics, ...semantic } = packet; + return stableStringify(semantic); } function sourceContextHashOf(sourceContext) { + const normalized = external_exports.array(secondarySourceContextSchema).parse(sourceContext); return sha256Hex( stableStringify( - [...sourceContext].map((entry2) => ({ path: entry2.path.replace(/\\/g, "/"), contentHash: entry2.contentHash })).sort((left, right) => left.path.localeCompare(right.path)) + normalized.map((entry2) => ({ + repositoryId: entry2.repositoryId, + path: entry2.path.replace(/\\/g, "/"), + contentHash: entry2.contentHash, + sectionHash: entry2.sectionHash ?? sha256Hex(entry2.content), + startLine: entry2.startLine ?? null, + endLine: entry2.endLine ?? null + })).sort((left, right) => `${left.repositoryId}:${left.path}`.localeCompare(`${right.repositoryId}:${right.path}`)) ) ); } function buildSecondaryBuilderPacket(input) { const sourceContext = external_exports.array(secondarySourceContextSchema).parse(input.sourceContext); - const sourceBytes = sourceContext.reduce( + const tests = external_exports.array(secondarySourceContextSchema).parse(input.tests ?? []); + const referencePatterns = external_exports.array(secondarySourceContextSchema).parse(input.referencePatterns ?? []); + const allSourceContext = [...sourceContext, ...tests, ...referencePatterns]; + const sourceBytes = allSourceContext.reduce( (total, entry2) => total + Buffer.byteLength(entry2.content, "utf8"), 0 ); @@ -62856,7 +63018,7 @@ function buildSecondaryBuilderPacket(input) { packetId: `${input.projection.projectionId}-secondary`, projectionHash: input.projection.contentHash, contractSnapshotHash: input.projection.contractSnapshotHash, - sourceContextHash: sourceContextHashOf(sourceContext), + sourceContextHash: sourceContextHashOf(allSourceContext), objective: { nodeId: input.projection.objectiveNodeId, taskId: input.projection.objective.taskId, @@ -62880,47 +63042,114 @@ function buildSecondaryBuilderPacket(input) { priorWorkEvidence: [...input.projection.workEvidence] }, sourceContext, + targets: external_exports.array(secondaryBuilderTargetSchema).parse( + input.targets ?? sourceContext.map((entry2) => ({ + repositoryId: entry2.repositoryId, + path: entry2.path, + symbols: entry2.symbols, + reason: entry2.reason + })) + ), + tests, + referencePatterns, + dependencyContext: external_exports.array(secondaryDependencyContextSchema).parse(input.dependencyContext ?? []), + priorFailureEvidence: [...input.priorFailureEvidence ?? []].slice(0, 20), + retrievalPlanRef: input.retrievalPlanRefs?.[0] ?? "manual-source-selection", + retrievalPlanRefs: [...input.retrievalPlanRefs ?? ["manual-source-selection"]].slice(0, 12), + expansion: { + level: input.expansionLevel ?? "MINIMAL_BOOTSTRAP", + remainingLevels: Math.max(0, 4 - CONTEXT_EXPANSION_LEVELS.indexOf(input.expansionLevel ?? "MINIMAL_BOOTSTRAP")) + }, + contextMetrics: secondaryBuilderPacketMetricsSchema.parse(input.contextMetrics ?? { + indexedFilesConsidered: sourceContext.length, + candidateCount: sourceContext.length, + selectedFiles: allSourceContext.length, + selectedSections: allSourceContext.filter((entry2) => entry2.startLine !== void 0).length, + sourceCharacters: sourceContext.reduce((sum, entry2) => sum + entry2.content.length, 0), + testCharacters: tests.reduce((sum, entry2) => sum + entry2.content.length, 0), + referencePatternCount: referencePatterns.length, + dependencyContextCount: input.dependencyContext?.length ?? 0, + budgetUtilization: sourceBytes / SECONDARY_BUILDER_LIMITS.maxSourceBytes, + mandatoryRefsRetained: sourceContext.length, + expansionDepth: CONTEXT_EXPANSION_LEVELS.indexOf(input.expansionLevel ?? "MINIMAL_BOOTSTRAP"), + staleEntriesEncountered: 0, + selectionDurationMs: 0, + indexReused: false + }), + quality: secondaryBuilderPacketQualitySchema.parse(input.quality ?? { + explicitTargetResolved: sourceContext.length > 0, + targetAmbiguity: false, + testsFound: tests.length > 0, + verificationHintsAvailable: (input.verificationHints?.length ?? 0) > 0, + referencePatternFound: referencePatterns.length > 0, + dependencyContextComplete: true, + sourceBudgetUtilization: sourceBytes / SECONDARY_BUILDER_LIMITS.maxSourceBytes, + contextSufficient: sourceContext.length > 0 + }), forbiddenChanges: [ "Do not modify .git, .kiro, .specbridge, .codex, .claude, credentials, approvals, contracts, mission state, or closure state.", "Do not delete, rename, chmod, create symlinks, emit commands, or request tools.", ...input.forbiddenChanges ?? [] ], - verificationHints: [...input.verificationHints ?? []] + verificationHints: [...input.verificationHints ?? []], + ...input.createdAt !== void 0 ? { createdAt: input.createdAt } : {} }; - const packetHash = sha256Hex(packetBody(base)); - const packet = secondaryBuilderPacketSchema.parse({ ...base, packetHash }); + const contentHash = sha256Hex(packetBody(base)); + const packet = secondaryBuilderPacketSchema.parse({ ...base, contentHash, packetHash: contentHash }); if (JSON.stringify(packet).length > SECONDARY_BUILDER_LIMITS.maxPacketCharacters) { throw new Error(`secondary builder packet exceeds ${SECONDARY_BUILDER_LIMITS.maxPacketCharacters} characters`); } return packet; } function validatePacketIdentity(packet) { - const { packetHash: claimed, ...body } = packet; + const { packetHash: claimed, contentHash, ...body } = packet; const actual = sha256Hex(packetBody(body)); - if (actual !== claimed) return "the packet hash does not match its contents"; - if (sourceContextHashOf(packet.sourceContext) !== packet.sourceContextHash) { + if (actual !== claimed || actual !== contentHash) return "the packet semantic hash does not match its contents"; + if (sourceContextHashOf([...packet.sourceContext, ...packet.tests, ...packet.referencePatterns]) !== packet.sourceContextHash) { return "the source-context manifest hash does not match the packet"; } return void 0; } -function validateSourceFreshness(worktreeRoot, packet, protectedPaths) { +function validateSourceFreshness(worktreeRoot, packet, protectedPaths, repositoryRoots) { const problems = []; - const pathFailures = validateEditPaths( - { rootDir: worktreeRoot }, - packet.sourceContext.map((entry2) => ({ path: entry2.path, content: "" })), - protectedPaths - ); - problems.push(...pathFailures.map((failure3) => `${failure3.path}: ${failure3.problem}`)); - if (pathFailures.length > 0) return problems; - for (const source of packet.sourceContext) { - const target = import_path37.default.join(worktreeRoot, source.path.replace(/\\/g, "/")); + const allSources = [...packet.sourceContext, ...packet.tests, ...packet.referencePatterns]; + for (const repositoryId of new Set(allSources.map((source) => source.repositoryId))) { + const repositoryRoot = repositoryRoots[repositoryId] ?? (repositoryId === "primary" ? worktreeRoot : void 0); + if (repositoryRoot === void 0) { + problems.push(`${repositoryId}: repository root is unavailable`); + continue; + } + const pathFailures = validateEditPaths( + { rootDir: repositoryRoot }, + allSources.filter((entry2) => entry2.repositoryId === repositoryId).map((entry2) => ({ path: entry2.path, content: "" })), + protectedPaths + ); + problems.push(...pathFailures.map((failure3) => `${repositoryId}:${failure3.path}: ${failure3.problem}`)); + } + if (problems.length > 0) return problems; + for (const source of allSources) { + const repositoryRoot = repositoryRoots[source.repositoryId] ?? (source.repositoryId === "primary" ? worktreeRoot : void 0); + if (repositoryRoot === void 0) { + problems.push(`${source.repositoryId}:${source.path}: repository root is unavailable`); + continue; + } + const normalizedPath = source.path.replace(/\\/g, "/"); + const resolvedRoot = import_path37.default.resolve(repositoryRoot); + const target = import_path37.default.resolve(resolvedRoot, normalizedPath); + if (import_path37.default.isAbsolute(normalizedPath) || normalizedPath === ".." || normalizedPath.startsWith("../") || target !== resolvedRoot && !target.startsWith(`${resolvedRoot}${import_path37.default.sep}`)) { + problems.push(`${source.repositoryId}:${source.path}: source path escapes its repository`); + continue; + } try { if (!(0, import_fs35.lstatSync)(target).isFile()) { problems.push(`${source.path}: source is not a regular file`); continue; } const current = (0, import_fs35.readFileSync)(target, "utf8"); - if (sha256Hex(current) !== source.contentHash || current !== source.content) { + if (sha256Hex(source.content) !== (source.sectionHash ?? sha256Hex(source.content))) { + problems.push(`${source.repositoryId}:${source.path}: selected section hash does not match packet content`); + } + if (sha256Hex(current) !== source.contentHash || source.startLine === void 0 && current !== source.content) { problems.push(`${source.path}: repository bytes changed after source context was assembled`); } } catch { @@ -62933,7 +63162,7 @@ function emptyTelemetry(packet) { return { inputCharacters: 0, outputBytes: 0, - sourceFiles: packet.sourceContext.length, + sourceFiles: packet.sourceContext.length + packet.tests.length + packet.referencePatterns.length, editedFiles: 0, durationMs: 0, inputTokens: null, @@ -62958,13 +63187,14 @@ async function executeSecondaryObjectiveBuilder(input) { const packet = parsedPacket.data; const identityProblem = validatePacketIdentity(packet); if (identityProblem !== void 0) return failure(packet, "STALE_SOURCE_CONTEXT", identityProblem); - const stale = validateSourceFreshness(input.worktreeRoot, packet, input.protectedPaths ?? []); + const stale = validateSourceFreshness( + input.worktreeRoot, + packet, + input.protectedPaths ?? [], + input.repositoryRoots ?? {} + ); if (stale.length > 0) return failure(packet, "STALE_SOURCE_CONTEXT", stale.slice(0, 5).join("; ")); - const userPrompt = [ - "Implement the approved WorkUnit using only the bounded packet below.", - "Return the SECONDARY_BUILDER_RESULT JSON document now.", - stableStringify(packet) - ].join("\n\n"); + const userPrompt = renderSecondaryBuilderPrompt(packet); const inputCharacters = SECONDARY_BUILDER_SYSTEM_PROMPT.length + userPrompt.length; if (inputCharacters > input.maximumInputCharacters) { return failure( @@ -63003,7 +63233,7 @@ async function executeSecondaryObjectiveBuilder(input) { let telemetry = { inputCharacters, outputBytes, - sourceFiles: packet.sourceContext.length, + sourceFiles: packet.sourceContext.length + packet.tests.length + packet.referencePatterns.length, editedFiles: 0, durationMs: inferred.durationMs, inputTokens: inferred.usage?.inputTokens ?? null, @@ -63035,6 +63265,14 @@ async function executeSecondaryObjectiveBuilder(input) { const proposal = parsed.data; telemetry = { ...telemetry, editedFiles: proposal.edits.length }; input.onExecutionEvent?.({ stage: "PROPOSAL_VALIDATED", proposal, telemetry }); + if (proposal.status === "NEEDS_MORE_CONTEXT") { + return failure( + packet, + "INSUFFICIENT_CONTEXT", + `secondary builder requested more context: ${proposal.needsMoreContextReasons?.join("; ") ?? proposal.summary}`, + { rawOutput: inferred.text, proposal, telemetry } + ); + } if (proposal.edits.length === 0) { return failure(packet, "EMPTY_EDIT_SET", "an implementation WorkUnit must propose at least one edit", { rawOutput: inferred.text, @@ -63098,6 +63336,44 @@ async function executeSecondaryObjectiveBuilder(input) { input.onExecutionEvent?.({ stage: "EDITS_APPLIED", proposal, appliedFiles, telemetry }); return { ok: true, proposal, appliedFiles, telemetry }; } +function renderSecondaryBuilderPrompt(packet) { + const renderSources = (entries) => entries.length === 0 ? "(none)" : entries.map( + (entry2) => [ + `--- ${entry2.repositoryId}:${entry2.path}${entry2.startLine !== void 0 ? ` lines ${entry2.startLine}-${entry2.endLine}` : ""} ---`, + `Selected because: ${entry2.reason}`, + entry2.content + ].join("\n") + ).join("\n\n"); + return [ + "PACKET IDENTITY", + stableStringify({ workUnitId: packet.workUnit.workUnitId, packetHash: packet.packetHash }), + "TASK", + `${packet.workUnit.title} +${packet.workUnit.goal}`, + "REQUIRED BEHAVIOR", + [...packet.objective.acceptance, ...packet.approvedContext.contracts.flatMap((contract) => contract.requirements)].join("\n- "), + "TARGETS", + packet.targets.map((target) => `${target.repositoryId}:${target.path} (${target.reason})`).join("\n"), + "CURRENT SOURCE", + renderSources(packet.sourceContext), + "RELEVANT TESTS", + renderSources(packet.tests), + "REFERENCE PATTERNS (examples only; not requirements)", + renderSources(packet.referencePatterns), + "DEPENDENCY CONTEXT", + packet.dependencyContext.map((dependency) => `${dependency.workUnitId}: ${dependency.summary}`).join("\n") || "(none)", + "CONSTRAINTS", + packet.approvedContext.constraints.join("\n"), + "DO NOT CHANGE", + packet.forbiddenChanges.join("\n"), + "PRIOR FAILURE EVIDENCE", + packet.priorFailureEvidence.join("\n") || "(none)", + "VERIFICATION HINTS", + packet.verificationHints.join("\n") || "(none configured)", + "OUTPUT CONTRACT", + "Return exactly one SECONDARY_BUILDER_RESULT JSON document. Use status EDITS with complete CREATE/REPLACE contents, or NEEDS_MORE_CONTEXT with no edits when the supplied evidence is insufficient." + ].join("\n\n"); +} function secondaryBuilderInputCeiling(config2) { return effectiveLocalInputCharacters(config2.localInference); } @@ -74719,6 +74995,666 @@ async function pruneWorktrees(workspace, jobId) { await git3(workspace.rootDir, ["worktree", "prune"]); return removed; } +var BUILDER_PACKET_COMPILATION_SCHEMA_VERSION = "1.0.0"; +var BUILDER_PACKET_COMPILATION_FAILURES = [ + "INSUFFICIENT_CONTEXT", + "AMBIGUOUS_TARGET", + "STALE_SOURCE_CONTEXT" +]; +var builderPacketCompilationResultSchema = external_exports.discriminatedUnion("ok", [ + external_exports.object({ + ok: external_exports.literal(true), + schemaVersion: external_exports.literal(BUILDER_PACKET_COMPILATION_SCHEMA_VERSION), + packet: secondaryBuilderPacketSchema, + plans: external_exports.array(contextSelectionPlanSchema).max(12), + planRefs: external_exports.array(external_exports.string().min(1).max(512)).max(12), + metrics: secondaryBuilderPacketMetricsSchema, + quality: secondaryBuilderPacketQualitySchema, + repositoryRoots: external_exports.record(external_exports.string().min(1)) + }).strict(), + external_exports.object({ + ok: external_exports.literal(false), + schemaVersion: external_exports.literal(BUILDER_PACKET_COMPILATION_SCHEMA_VERSION), + failure: external_exports.object({ + kind: external_exports.enum(BUILDER_PACKET_COMPILATION_FAILURES), + reasons: external_exports.array(external_exports.string().min(1).max(2e3)).min(1).max(8) + }).strict(), + plans: external_exports.array(contextSelectionPlanSchema).max(12), + planRefs: external_exports.array(external_exports.string().min(1).max(512)).max(12), + metrics: secondaryBuilderPacketMetricsSchema, + quality: secondaryBuilderPacketQualitySchema + }).strict() +]); +var SecondaryBuilderContextCompiler = class { + compile(input) { + return compileSecondaryBuilderPacket(input); + } +}; +var defaultBudget = { + maxSelectedFiles: SECONDARY_BUILDER_LIMITS.maxSourceFiles, + maxTests: SECONDARY_BUILDER_LIMITS.maxTests, + maxReferencePatterns: SECONDARY_BUILDER_LIMITS.maxReferencePatterns, + maxSourceCharacters: SECONDARY_BUILDER_LIMITS.maxSourceBytes, + maxCharactersPerSection: SECONDARY_BUILDER_LIMITS.maxSourceFileChars +}; +function boundedBudget(input) { + const supplied = input.budget ?? {}; + return { + maxSelectedFiles: Math.max( + 1, + Math.min(SECONDARY_BUILDER_LIMITS.maxSourceFiles, supplied.maxSelectedFiles ?? defaultBudget.maxSelectedFiles) + ), + maxTests: Math.max(0, Math.min(SECONDARY_BUILDER_LIMITS.maxTests, supplied.maxTests ?? defaultBudget.maxTests)), + maxReferencePatterns: Math.max( + 0, + Math.min( + SECONDARY_BUILDER_LIMITS.maxReferencePatterns, + supplied.maxReferencePatterns ?? defaultBudget.maxReferencePatterns + ) + ), + maxSourceCharacters: Math.max( + 1e3, + Math.min(SECONDARY_BUILDER_LIMITS.maxSourceBytes, supplied.maxSourceCharacters ?? defaultBudget.maxSourceCharacters) + ), + maxCharactersPerSection: Math.max( + 500, + Math.min( + SECONDARY_BUILDER_LIMITS.maxSourceFileChars, + supplied.maxCharactersPerSection ?? defaultBudget.maxCharactersPerSection + ) + ) + }; +} +function normalizePath22(value) { + return value.replace(/\\/g, "/").replace(/^\.\//, "").trim(); +} +function unique(values) { + return [...new Set(values.filter((value) => value.length > 0))]; +} +function safeId(value) { + return value.replace(/[^A-Za-z0-9._-]/g, "-").slice(0, 80) || "repository"; +} +function nowIso2(input) { + return input.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(); +} +async function prepareRepositories(input, createdAt) { + if (input.repositories !== void 0) { + const seen = /* @__PURE__ */ new Set(); + return input.repositories.map((repository, position) => { + if (seen.has(repository.repositoryId)) { + throw new Error(`duplicate repository identity ${repository.repositoryId}`); + } + seen.add(repository.repositoryId); + if (position > 0 && repository.repositoryId !== "primary" && (repository.justification === void 0 || repository.justification.trim() === "")) { + throw new Error(`secondary repository ${repository.repositoryId} needs an explicit justification`); + } + return { + repositoryId: repository.repositoryId, + rootDir: repository.rootDir, + index: repository.index, + changedPaths: unique((repository.changedPaths ?? []).map(normalizePath22)), + indexReused: repository.indexReused ?? true, + staleMetadataPaths: [] + }; + }); + } + const canonicalSnapshot = await captureGitSnapshot(input.workspace.rootDir); + const base = ensureRepositoryIndex({ + workspace: input.workspace, + config: input.config, + now: createdAt, + gitSnapshot: canonicalSnapshot + }); + const worktreeSnapshot = await captureGitSnapshot(input.worktreeRoot); + const dependencyPaths = (input.dependencyContext ?? []).flatMap( + (dependency) => dependency.changedFiles.filter((file) => (file.repositoryId ?? "primary") === "primary").map((file) => normalizePath22(file.path)) + ); + const changedPaths = unique([ + ...canonicalSnapshot.entries.map((entry2) => normalizePath22(entry2.path)), + ...worktreeSnapshot.entries.map((entry2) => normalizePath22(entry2.path)), + ...dependencyPaths + ]); + const policy = input.config.orchestration.jobs.context.efficiency; + const overlaid = refreshRepositoryIndex( + { ...base.state, workspaceKey: workspaceKeyFor(input.worktreeRoot) }, + { + rootDir: input.worktreeRoot, + now: createdAt, + protectedPaths: indexProtectedPaths(input.config), + respectGitignore: policy.respectGitignore, + maxEntries: policy.maxIndexedFiles, + maxFileBytes: policy.maxIndexedFileBytes, + baselineRef: input.baselineRef ?? worktreeSnapshot.head ?? base.state.baselineRef, + changedPaths + } + ); + return [ + { + repositoryId: "primary", + rootDir: input.worktreeRoot, + index: new RepositoryContextIndex(overlaid.state), + changedPaths, + indexReused: true, + staleMetadataPaths: overlaid.refreshedPaths + } + ]; +} +function workUnitText(input) { + return [ + input.workUnit.title, + input.workUnit.goal, + ...input.workUnit.expectedArtifacts, + ...input.workUnit.expectedAreas + ].join("\n"); +} +function pathRepoHint(reference, repositoryId) { + const lower = reference.toLowerCase(); + const repo = repositoryId.toLowerCase(); + return lower.startsWith(`${repo}:`) || lower.startsWith(`${repo}/`); +} +function areaScore(input, repositoryId, candidatePath) { + const haystack = `${repositoryId}/${candidatePath}`.toLowerCase(); + return input.workUnit.expectedAreas.reduce((score, area) => { + const normalized = normalizePath22(area).toLowerCase().replace(/^[^:]+:/, ""); + return score + (normalized.length >= 2 && haystack.includes(normalized) ? 1 : 0); + }, 0); +} +function chooseUnique(input, candidates, reference) { + if (candidates.length === 0) return { ambiguous: false }; + if (candidates.length === 1) return { value: candidates[0], ambiguous: false }; + const hinted = candidates.filter((candidate) => pathRepoHint(reference, candidate.repositoryId)); + if (hinted.length === 1) return { value: hinted[0], ambiguous: false }; + const scored = candidates.map((candidate) => ({ candidate, score: areaScore(input, candidate.repositoryId, candidate.path) })).sort((left, right) => right.score - left.score); + if ((scored[0]?.score ?? 0) > (scored[1]?.score ?? 0)) { + return { value: scored[0].candidate, ambiguous: false }; + } + return { ambiguous: true }; +} +function locateExplicitTargets(input, repositories) { + const text93 = workUnitText(input); + const references = unique(extractPathReferences2(text93).map(normalizePath22)); + const symbols = unique(extractSymbolReferences(text93)); + const targets = /* @__PURE__ */ new Map(); + const missing = []; + const ambiguous = []; + for (const reference of references) { + const located = []; + for (const repository of repositories) { + const repoPrefix = `${repository.repositoryId}:`; + const withoutRepo = reference.startsWith(repoPrefix) ? reference.slice(repoPrefix.length) : reference; + const exact = repository.index.get(withoutRepo); + const paths = exact !== void 0 ? [exact.path] : withoutRepo.includes("/") ? [] : repository.index.namedExactly(withoutRepo); + for (const candidatePath of paths) { + const entry2 = repository.index.get(candidatePath); + if (entry2 !== void 0) { + located.push({ repositoryId: repository.repositoryId, path: entry2.path, symbols: entry2.symbols.slice(0, 12) }); + } + } + } + const selected = chooseUnique(input, located, reference); + if (selected.ambiguous) ambiguous.push(reference); + else if (selected.value !== void 0) { + targets.set(`${selected.value.repositoryId}:${selected.value.path}`, selected.value); + } else { + missing.push(reference); + } + } + for (const symbol of symbols) { + const located = []; + for (const repository of repositories) { + for (const candidatePath of repository.index.declaring(symbol)) { + const entry2 = repository.index.get(candidatePath); + if (entry2 !== void 0) { + located.push({ repositoryId: repository.repositoryId, path: entry2.path, symbols: [symbol] }); + } + } + } + if (located.length === 0) continue; + const alreadyBound = located.find( + (candidate) => targets.has(`${candidate.repositoryId}:${candidate.path}`) + ); + const selected = alreadyBound === void 0 ? chooseUnique(input, located, symbol) : { value: alreadyBound, ambiguous: false }; + if (selected.ambiguous) ambiguous.push(symbol); + else if (selected.value !== void 0) { + const key = `${selected.value.repositoryId}:${selected.value.path}`; + const previous = targets.get(key); + targets.set(key, { + ...selected.value, + symbols: unique([...previous?.symbols ?? [], symbol]).slice(0, 12) + }); + } + } + const createsNewArtifact = /\b(create|introduce|new file|add file)\b/i.test(text93); + return { + targets: [...targets.values()], + missing: createsNewArtifact ? [] : missing, + ambiguous, + explicitEvidence: references.length > 0 || symbols.length > 0 + }; +} +function queryFor(input, repository, targets, expansionLevel) { + const dependencyPaths = (input.dependencyContext ?? []).flatMap( + (dependency) => dependency.changedFiles.filter((file) => (file.repositoryId ?? "primary") === repository.repositoryId).map((file) => normalizePath22(file.path)) + ); + const approved = input.projection.contracts.flatMap((contract) => [ + contract.title, + contract.summary, + ...contract.requirements, + ...contract.invariants + ]); + const query = buildRetrievalQuery({ + taskId: input.workUnit.parentTaskId, + nodeId: input.objectiveNodeId, + attemptId: `${input.workUnit.workUnitId}-a${input.attempt}-secondary`, + role: "EXECUTOR", + contract: [ + ...input.workUnit.expectedArtifacts, + ...input.workUnit.expectedAreas, + ...input.workUnit.relevantContractIds, + ...approved + ].join("\n"), + objective: `${input.workUnit.title} +${input.workUnit.goal}`, + acceptanceCriteria: input.projection.objective.acceptance, + currentAction: [ + input.workUnit.goal, + ...targets.filter((target) => target.repositoryId === repository.repositoryId).map((target) => target.path) + ].join("\n"), + failureText: (input.priorFailureEvidence ?? []).join("\n").slice(0, 2e4), + changedPaths: dependencyPaths, + checkpointChangedPaths: dependencyPaths, + priorRelevantPaths: input.priorRelevantPaths, + expansionLevel + }); + query.actionPaths = unique([ + ...targets.filter((target) => target.repositoryId === repository.repositoryId).map((target) => target.path), + ...query.actionPaths + ]); + return query; +} +function contextBudget(input) { + const characterCeiling = Math.min( + SECONDARY_BUILDER_LIMITS.maxPacketCharacters, + input.maximumInputCharacters ?? SECONDARY_BUILDER_LIMITS.maxPacketCharacters + ); + return contextBudgetConfigSchema.parse({ + modelContextTokens: Math.max(1e3, Math.floor(characterCeiling / 4)), + reservedOutputTokens: 0, + reservedReasoningTokens: 0, + reservedGrowthTokens: 0 + }); +} +function itemFor(planEntry, result) { + return result.assembled.package.items.find( + (item) => item.provenance?.path === planEntry.path && item.layer === "WORKING_SET" + ); +} +function sectionFrom(repository, selected, content) { + const entry2 = repository.index.get(selected.path); + return secondarySourceContextSchema.parse({ + repositoryId: repository.repositoryId, + path: selected.path, + contentHash: selected.contentHash, + sectionHash: sha256Hex(content), + content, + reason: selected.reason, + ...selected.startLine !== void 0 ? { startLine: selected.startLine } : {}, + ...selected.endLine !== void 0 ? { endLine: selected.endLine } : {}, + symbols: unique([ + ...selected.symbol !== void 0 ? [selected.symbol] : [], + ...entry2?.symbols ?? [] + ]).slice(0, 12) + }); +} +function priority(targetKeys, repositoryId, selected, kind) { + if (targetKeys.has(`${repositoryId}:${selected.path}`)) return 0; + if (selected.reason === "EXPLICIT_FAILURE_REFERENCE") return 1; + if (kind === "test") return 2; + if (selected.reason === "DEPENDENCY_PROXIMITY") return 3; + if (selected.reason === "REFERENCE_PATTERN") return 4; + return 5; +} +function pairedTestMatches(testPath, targetPath) { + const stem2 = (value) => value.split("/").at(-1).replace(/\.(?:test|spec)\.[^.]+$/, "").replace(/\.[^.]+$/, "").replace(/(?:[-_.]?(?:test|tests|spec))$/i, "").toLowerCase(); + if (stem2(testPath) !== stem2(targetPath)) return false; + const meaningfulDirs = (value) => value.split("/").slice(0, -1).map((part) => part.toLowerCase()).filter((part) => !["src", "test", "tests", "__tests__"].includes(part)); + const targetDirs = meaningfulDirs(targetPath); + if (targetDirs.length === 0) return true; + const testDirs = new Set(meaningfulDirs(testPath)); + return targetDirs.some((part) => testDirs.has(part)); +} +async function compileSecondaryBuilderPacket(input) { + const startedAt = Date.now(); + const createdAt = nowIso2(input); + const budget = boundedBudget(input); + const expansionLevel = input.expansionLevel ?? "ADJACENT_DEPENDENCIES"; + const expansionDepth = CONTEXT_EXPANSION_LEVELS.indexOf(expansionLevel); + const repositories = await prepareRepositories(input, createdAt); + const located = locateExplicitTargets(input, repositories); + const prepared = repositories.map((repository) => { + const query = queryFor(input, repository, located.targets, expansionLevel); + return { repository, query, preliminary: rankCandidates(repository.index, query) }; + }); + let targets = [...located.targets]; + const ambiguity = [...located.ambiguous]; + if (targets.length === 0 && located.missing.length === 0) { + const plausible = prepared.flatMap( + (entry2) => entry2.preliminary.filter((candidate) => candidate.entry.kind === "source" || candidate.entry.kind === "config").slice(0, 2).map((candidate) => ({ repository: entry2.repository, candidate })) + ).sort( + (left, right) => right.candidate.score !== left.candidate.score ? right.candidate.score - left.candidate.score : `${left.repository.repositoryId}:${left.candidate.path}`.localeCompare( + `${right.repository.repositoryId}:${right.candidate.path}` + ) + ); + if (plausible[0] !== void 0 && plausible[1] !== void 0 && plausible[0].candidate.score === plausible[1].candidate.score && plausible[0].candidate.entry.module !== plausible[1].candidate.entry.module) { + ambiguity.push( + `${plausible[0].repository.repositoryId}:${plausible[0].candidate.path} or ${plausible[1].repository.repositoryId}:${plausible[1].candidate.path}` + ); + } else if (plausible[0] !== void 0) { + targets = [ + { + repositoryId: plausible[0].repository.repositoryId, + path: plausible[0].candidate.path, + symbols: plausible[0].candidate.entry.symbols.slice(0, 12) + } + ]; + const matching = prepared.find( + (entry2) => entry2.repository.repositoryId === plausible[0]?.repository.repositoryId + ); + if (matching !== void 0) { + matching.query.actionPaths = unique([plausible[0].candidate.path, ...matching.query.actionPaths]); + } + } + } + const policy = input.config.orchestration.jobs.context.efficiency; + const allocationPolicy = contextAllocationPolicySchema.parse({ + pinnedReserveRatio: policy.pinnedReserveRatio, + durableReserveRatio: policy.durableReserveRatio, + recoveryReserveRatio: policy.recoveryReserveRatio, + deltaReserveRatio: policy.deltaReserveRatio, + workingSetMaxRatio: policy.workingSetMaxRatio, + pointerShapeWorkingSetMaxRatio: policy.pointerShapeWorkingSetMaxRatio, + maxSingleItemRatio: policy.maxSingleItemRatio + }); + const results = []; + for (const entry2 of prepared) { + const planId = [ + "builder", + safeId(input.objectiveNodeId), + safeId(input.workUnit.workUnitId), + `a${input.attempt}`, + safeId(entry2.repository.repositoryId), + safeId(expansionLevel.toLowerCase()) + ].join("-").slice(0, 120); + const result = await buildEfficientContext({ + strategy: "SELECTIVE", + shape: "MATERIALIZED", + expansionLevel, + canonicalItems: [], + budget: contextBudget(input), + allocationPolicy, + createdAt, + planId, + taskId: input.workUnit.parentTaskId, + jobId: input.jobId, + nodeId: input.objectiveNodeId, + attemptId: `${input.workUnit.workUnitId}-a${input.attempt}-secondary`, + executionLane: "LOCAL", + executionMode: "DIRECT_MODEL", + runner: "secondary-objective-builder", + index: entry2.repository.index, + rootDir: entry2.repository.rootDir, + query: entry2.query, + rankOptions: { + maxCandidates: policy.maxCandidates, + excludedPaths: indexProtectedPaths(input.config) + }, + sectionOptions: { + wholeFileUnderChars: Math.min(policy.wholeFileUnderChars, budget.maxCharactersPerSection), + targetSectionChars: Math.min(policy.targetSectionChars, budget.maxCharactersPerSection) + }, + maxSelectedItems: budget.maxSelectedFiles, + maxPointers: 0 + }); + results.push({ repository: entry2.repository, result }); + } + const targetKeys = new Set(targets.map((target) => `${target.repositoryId}:${target.path}`)); + const selectedSections = results.flatMap( + ({ repository, result }) => result.plan.selectedWorkingItems.flatMap((selected) => { + const selectedKey = `${repository.repositoryId}:${selected.path}`; + const entryKind = repository.index.get(selected.path)?.kind; + if (targetKeys.size > 0 && !targetKeys.has(selectedKey) && (selected.reason === "TOKEN_OVERLAP" || selected.reason === "MODULE_PROXIMITY")) { + return []; + } + if (selected.reason === "TEST_SOURCE_PAIR" && !targetKeys.has(selectedKey)) { + if (entryKind !== "test") return []; + const matchesTarget = targets.some( + (target) => target.repositoryId === repository.repositoryId && (repository.index.testsFor(target.path).includes(selected.path) || repository.index.sourcesFor(selected.path).includes(target.path) || pairedTestMatches(selected.path, target.path)) + ); + if (!matchesTarget) return []; + } + const item = itemFor(selected, result); + if (item === void 0 || item.content.length > budget.maxCharactersPerSection) return []; + return [ + { + repository, + selected, + kind: entryKind, + section: sectionFrom(repository, selected, item.content) + } + ]; + }) + ).sort((left, right) => { + const leftPriority = priority(targetKeys, left.repository.repositoryId, left.selected, left.kind); + const rightPriority = priority(targetKeys, right.repository.repositoryId, right.selected, right.kind); + if (leftPriority !== rightPriority) return leftPriority - rightPriority; + return right.selected.score - left.selected.score; + }); + const sourceContext = []; + const tests = []; + const referencePatterns = []; + let usedCharacters = 0; + let selectedFileCount = 0; + for (const selected of selectedSections) { + if (selectedFileCount >= budget.maxSelectedFiles) continue; + if (usedCharacters + selected.section.content.length > budget.maxSourceCharacters) continue; + if (selected.kind === "test") { + if (tests.length >= budget.maxTests) continue; + tests.push(selected.section); + } else if (selected.selected.reason === "REFERENCE_PATTERN") { + if (referencePatterns.length >= budget.maxReferencePatterns) continue; + referencePatterns.push(selected.section); + } else { + sourceContext.push(selected.section); + } + usedCharacters += selected.section.content.length; + selectedFileCount += 1; + } + const selectedKeys = new Set( + [...sourceContext, ...tests, ...referencePatterns].map( + (section) => `${section.repositoryId}:${section.path}` + ) + ); + const unresolvedTargets = targets.filter( + (target) => !selectedKeys.has(`${target.repositoryId}:${target.path}`) + ); + const dependencyContext = (input.dependencyContext ?? []).filter((dependency) => dependency.verificationPassed).slice(0, SECONDARY_BUILDER_LIMITS.maxDependencyContext).map((dependency) => ({ + workUnitId: dependency.workUnitId, + summary: dependency.summary.slice(0, 2e3), + changedFiles: dependency.changedFiles.slice(0, 60).map((file) => ({ + repositoryId: file.repositoryId ?? "primary", + path: normalizePath22(file.path) + })), + exportedSymbols: [...dependency.exportedSymbols ?? []].slice(0, 30), + verificationPassed: true + })); + const missingDependencies = [...input.missingDependencyIds ?? []]; + const sourceCharacters = sourceContext.reduce((sum, section) => sum + section.content.length, 0); + const testCharacters = tests.reduce((sum, section) => sum + section.content.length, 0); + const planRefs = results.map(({ result }) => `context/plans/${result.plan.planId}`); + const metrics = secondaryBuilderPacketMetricsSchema.parse({ + indexedFilesConsidered: repositories.reduce((sum, repository) => sum + repository.index.size, 0), + candidateCount: results.reduce((sum, entry2) => sum + entry2.result.deterministicCandidates.length, 0), + selectedFiles: sourceContext.length + tests.length + referencePatterns.length, + selectedSections: [...sourceContext, ...tests, ...referencePatterns].filter( + (section) => section.startLine !== void 0 + ).length, + sourceCharacters, + testCharacters, + referencePatternCount: referencePatterns.length, + dependencyContextCount: dependencyContext.length, + budgetUtilization: usedCharacters / budget.maxSourceCharacters, + mandatoryRefsRetained: targets.length - unresolvedTargets.length, + expansionDepth, + staleEntriesEncountered: results.reduce((sum, entry2) => sum + entry2.result.refreshedPaths.length, 0) + repositories.reduce((sum, repository) => sum + repository.staleMetadataPaths.length, 0), + selectionDurationMs: Math.max(0, Date.now() - startedAt), + indexReused: repositories.every((repository) => repository.indexReused) + }); + const failureReasons = [ + ...located.missing.map((reference) => `explicit target ${reference} was not found`), + ...unresolvedTargets.map( + (target) => `target ${target.repositoryId}:${target.path} could not be materialized within policy` + ), + ...missingDependencies.map((dependency) => `verified dependency evidence for ${dependency} is missing`), + ...sourceContext.length === 0 ? ["no implementable source target was selected"] : [] + ]; + const contextSufficient = ambiguity.length === 0 && failureReasons.length === 0; + const quality = secondaryBuilderPacketQualitySchema.parse({ + explicitTargetResolved: located.explicitEvidence && located.missing.length === 0 && targets.length > 0, + targetAmbiguity: ambiguity.length > 0, + testsFound: tests.length > 0, + verificationHintsAvailable: (input.verificationHints?.length ?? 0) > 0, + referencePatternFound: referencePatterns.length > 0, + dependencyContextComplete: missingDependencies.length === 0, + sourceBudgetUtilization: usedCharacters / budget.maxSourceCharacters, + contextSufficient + }); + let plans = results.map(({ repository, result }) => { + const delivered = new Set( + [...sourceContext, ...tests, ...referencePatterns].filter((section) => section.repositoryId === repository.repositoryId).map((section) => section.path) + ); + const dropped = result.plan.selectedWorkingItems.filter((entry2) => !delivered.has(entry2.path)); + return contextSelectionPlanSchema.parse({ + ...result.plan, + repositoryId: repository.repositoryId, + selectedWorkingItems: result.plan.selectedWorkingItems.filter((entry2) => delivered.has(entry2.path)).map((entry2) => ({ ...entry2, repositoryId: repository.repositoryId })), + pointers: result.plan.pointers.map((entry2) => ({ ...entry2, repositoryId: repository.repositoryId })), + excludedCandidates: [ + ...result.plan.excludedCandidates.map((entry2) => ({ + ...entry2, + repositoryId: repository.repositoryId + })), + ...dropped.map((entry2) => ({ + repositoryId: repository.repositoryId, + path: entry2.path, + reason: "RANKED_BELOW_CUTOFF", + score: entry2.score, + detail: "excluded by Builder Packet category or character budget" + })) + ].slice(0, 200), + builderPacket: quality + }); + }); + const persistPlans = () => { + if (input.persist !== false) { + for (const plan of plans) writeContextSelectionPlan(input.workspace, plan); + } + }; + if (ambiguity.length > 0) { + persistPlans(); + return { + ok: false, + schemaVersion: BUILDER_PACKET_COMPILATION_SCHEMA_VERSION, + failure: { + kind: "AMBIGUOUS_TARGET", + reasons: ambiguity.slice(0, 8).map((entry2) => `equally plausible target: ${entry2}`) + }, + plans, + planRefs, + metrics, + quality + }; + } + if (!contextSufficient) { + persistPlans(); + return { + ok: false, + schemaVersion: BUILDER_PACKET_COMPILATION_SCHEMA_VERSION, + failure: { kind: "INSUFFICIENT_CONTEXT", reasons: failureReasons.slice(0, 8) }, + plans, + planRefs, + metrics, + quality + }; + } + let packet; + try { + packet = buildSecondaryBuilderPacket({ + projection: input.projection, + sourceContext, + targets: targets.map((target) => { + const selected = [...sourceContext, ...tests, ...referencePatterns].find( + (section) => section.repositoryId === target.repositoryId && section.path === target.path + ); + return { + repositoryId: target.repositoryId, + path: target.path, + symbols: target.symbols, + reason: selected?.reason ?? "EXPLICIT_ACTION_REFERENCE" + }; + }), + tests, + referencePatterns, + dependencyContext, + priorFailureEvidence: (input.priorFailureEvidence ?? []).map((entry2) => entry2.slice(0, 2e3)).slice(0, 20), + verificationHints: (input.verificationHints ?? []).map((hint) => hint.slice(0, 1e3)).slice(0, 30), + retrievalPlanRefs: planRefs, + expansionLevel, + contextMetrics: metrics, + quality, + createdAt + }); + const inputCharacters = SECONDARY_BUILDER_SYSTEM_PROMPT.length + renderSecondaryBuilderPrompt(packet).length; + const inputCeiling = input.maximumInputCharacters ?? SECONDARY_BUILDER_LIMITS.maxPacketCharacters; + if (inputCharacters > inputCeiling) { + throw new Error(`compiled input is ${inputCharacters} characters; limit is ${inputCeiling}`); + } + } catch (cause) { + const insufficientQuality = secondaryBuilderPacketQualitySchema.parse({ + ...quality, + contextSufficient: false + }); + plans = plans.map((plan) => contextSelectionPlanSchema.parse({ + ...plan, + builderPacket: insufficientQuality + })); + persistPlans(); + return { + ok: false, + schemaVersion: BUILDER_PACKET_COMPILATION_SCHEMA_VERSION, + failure: { + kind: "INSUFFICIENT_CONTEXT", + reasons: [`packet assembly could not fit within policy: ${cause instanceof Error ? cause.message : String(cause)}`.slice(0, 2e3)] + }, + plans, + planRefs, + metrics, + quality: insufficientQuality + }; + } + persistPlans(); + return { + ok: true, + schemaVersion: BUILDER_PACKET_COMPILATION_SCHEMA_VERSION, + packet, + plans, + planRefs, + metrics, + quality, + repositoryRoots: Object.fromEntries( + repositories.map((repository) => [repository.repositoryId, repository.rootDir]) + ) + }; +} function loadMissionTruth(workspace, mission) { return { mission, @@ -74748,7 +75684,7 @@ function decisionKindOf(raw) { function persistGraph2(input, graph) { return storeWorkGraph(input.workspace, input.jobId, graph); } -function nowIso2(input) { +function nowIso3(input) { return (input.clock ?? (() => /* @__PURE__ */ new Date()))().toISOString(); } function failResult(category, message2, source, output) { @@ -74759,7 +75695,7 @@ function failResult(category, message2, source, output) { }; } async function decomposeObjective(input, truth, relevantContractIds, acceptance) { - const at = nowIso2(input); + const at = nowIso3(input); const fallback = (reason) => singleUnitGraph({ jobId: input.jobId, node: input.node, @@ -75039,7 +75975,10 @@ function secondaryFailureCategory(kind) { case "VERIFICATION_FAILURE": return "VERIFICATION_FAILURE"; case "CONTEXT_TOO_LARGE": + case "INSUFFICIENT_CONTEXT": return "CAPABILITY_UNAVAILABLE"; + case "AMBIGUOUS_TARGET": + return "AMBIGUITY"; case "TIMEOUT": return "TRANSIENT_TRANSPORT"; case "INFERENCE_UNAVAILABLE": @@ -75055,34 +75994,75 @@ async function executeSelectedSecondaryBuilder(context, prepared, worktree) { if (selection === void 0) { return { prepared, result: builderFailureResult("secondary builder selection disappeared") }; } - let sourceContext; - try { - sourceContext = typeof selection.sourceContext === "function" ? await selection.sourceContext({ worktreeRoot: worktree.dir, projection: prepared.projection }) : selection.sourceContext; - } catch (cause) { - const problem = `source context could not be prepared: ${cause instanceof Error ? cause.message : String(cause)}`; - return { - prepared, - result: builderFailureResult(problem), - secondaryFailure: { kind: "STALE_SOURCE_CONTEXT", problem } - }; - } let packet; - try { - packet = buildSecondaryBuilderPacket({ - projection: prepared.projection, - sourceContext, - verificationHints: input.config.verification.commands.map((command) => command.name) + let repositoryRoots = { primary: worktree.dir }; + if (selection.sourceContext !== void 0) { + try { + const sourceContext = typeof selection.sourceContext === "function" ? await selection.sourceContext({ worktreeRoot: worktree.dir, projection: prepared.projection }) : selection.sourceContext; + packet = buildSecondaryBuilderPacket({ + projection: prepared.projection, + sourceContext, + verificationHints: input.config.verification.commands.map((command) => command.name) + }); + } catch (cause) { + const problem = `source context could not be prepared: ${cause instanceof Error ? cause.message : String(cause)}`; + return { + prepared, + result: builderFailureResult(problem), + secondaryFailure: { kind: "STALE_SOURCE_CONTEXT", problem } + }; + } + } else { + const compiler = selection.contextCompiler ?? new SecondaryBuilderContextCompiler(); + let compiled; + try { + compiled = await compiler.compile({ + workspace: input.workspace, + config: input.config, + jobId: input.jobId, + objectiveNodeId: input.node.nodeId, + workUnit: prepared.unit, + projection: prepared.projection, + attempt: prepared.attempt, + worktreeRoot: worktree.dir, + baselineRef: prepared.baselineCommit, + dependencyContext: prepared.dependencyContext, + missingDependencyIds: prepared.missingDependencyIds, + priorFailureEvidence: prepared.unit.latestFailure === void 0 ? [] : [`${prepared.unit.latestFailure.category}: ${prepared.unit.latestFailure.message}`], + verificationHints: input.config.verification.commands.map((command) => command.name), + maximumInputCharacters: secondaryBuilderInputCeiling(input.config), + createdAt: nowIso3(input) + }); + } catch (cause) { + const problem = `builder packet compilation failed: ${cause instanceof Error ? cause.message : String(cause)}`; + return { + prepared, + result: builderFailureResult(problem), + secondaryFailure: { kind: "INSUFFICIENT_CONTEXT", problem } + }; + } + input.recordEvent(compiled.ok ? "context_selected" : "context_insufficient", { + nodeId: input.node.nodeId, + workUnitId: prepared.unitId, + attempt: prepared.attempt, + planRefs: compiled.planRefs, + metrics: compiled.metrics, + quality: compiled.quality, + ...compiled.ok ? {} : { failure: compiled.failure.kind, reasons: compiled.failure.reasons } }); - } catch (cause) { - const problem = `secondary builder packet was refused: ${cause instanceof Error ? cause.message : String(cause)}`; - return { - prepared, - result: builderFailureResult(problem), - secondaryFailure: { kind: "CONTEXT_TOO_LARGE", problem } - }; + if (!compiled.ok) { + const problem = `${compiled.failure.kind}: ${compiled.failure.reasons.join("; ")}`; + return { + prepared, + result: builderFailureResult(problem), + secondaryFailure: { kind: compiled.failure.kind, problem } + }; + } + packet = compiled.packet; + repositoryRoots = compiled.repositoryRoots; } const inference = selection.inference ?? (input.localManager !== void 0 ? managedLocalSecondaryModelInference(input.localManager, input.config) : void 0); - const createdAt = nowIso2(input); + const createdAt = nowIso3(input); let artifact = secondaryBuilderAttemptSchema.parse({ schemaVersion: SECONDARY_BUILDER_ATTEMPT_SCHEMA_VERSION, attemptId: `${prepared.unitId}-a${String(prepared.attempt).padStart(2, "0")}-secondary`, @@ -75104,7 +76084,7 @@ async function executeSelectedSecondaryBuilder(context, prepared, worktree) { updatedAt: createdAt }); const persist32 = (update) => { - artifact = secondaryBuilderAttemptSchema.parse({ ...artifact, ...update, updatedAt: nowIso2(input) }); + artifact = secondaryBuilderAttemptSchema.parse({ ...artifact, ...update, updatedAt: nowIso3(input) }); storeSecondaryBuilderAttempt(input.workspace, input.jobId, input.node.nodeId, artifact); }; persist32({}); @@ -75143,6 +76123,7 @@ async function executeSelectedSecondaryBuilder(context, prepared, worktree) { maximumInputCharacters: secondaryBuilderInputCeiling(input.config), maxOutputBytes: input.config.localInference.maxOutputBytes, protectedPaths: input.config.execution.protectedPaths, + repositoryRoots, ...input.signal !== void 0 ? { signal: input.signal } : {}, onExecutionEvent: (event) => { if (event.stage === "INFERENCE_COMPLETED") { @@ -75165,7 +76146,7 @@ async function executeSelectedSecondaryBuilder(context, prepared, worktree) { }); if (!executed.ok) { persist32({ - status: "FAILED", + status: executed.failure.kind === "INSUFFICIENT_CONTEXT" ? "CONTEXT_INSUFFICIENT" : "FAILED", failure: executed.failure, ...executed.rawOutput !== void 0 ? { rawOutput: executed.rawOutput.slice(0, 1048576) } : {}, ...executed.proposal !== void 0 ? { proposal: executed.proposal } : {}, @@ -75279,9 +76260,11 @@ async function prepareUnitAttempt(context, graph, unitId) { const { input, truth } = context; const unit = requireUnit(graph, unitId); const attempt = unit.attempt + 1; - const at = nowIso2(input); + const at = nowIso3(input); const workEvidence = []; const dependencyPatches = []; + const dependencyContext = []; + const missingDependencyIds = []; for (const dependencyId of unit.dependsOn) { const dependency = findUnit(graph, dependencyId); if (dependency === void 0) continue; @@ -75311,6 +76294,18 @@ async function prepareUnitAttempt(context, graph, unitId) { if (patch !== void 0 && patch.trim().length > 0) { dependencyPatches.push({ workUnitId: resolved2.workUnitId, patch }); } + if (candidate.localVerification.passed) { + dependencyContext.push({ + workUnitId: resolved2.workUnitId, + summary: candidate.claims.summary, + changedFiles: candidate.changedFiles.map((file) => ({ repositoryId: "primary", path: file.path })), + verificationPassed: true + }); + } else { + missingDependencyIds.push(resolved2.workUnitId); + } + } else { + missingDependencyIds.push(resolved2.workUnitId); } } const projectedUnit = unit.relevantContractIds.length > 0 ? unit : { ...unit, relevantContractIds: context.objectiveContractIds.slice(0, 30) }; @@ -75379,6 +76374,7 @@ async function prepareUnitAttempt(context, graph, unitId) { graph: nextGraph, prepared: { unitId, + unit, kind: unit.kind, attempt, workerId, @@ -75386,7 +76382,9 @@ async function prepareUnitAttempt(context, graph, unitId) { ...worktree !== void 0 ? { worktree } : {}, baselineCommit, record: record32, - dependencyPatches + dependencyPatches, + dependencyContext, + missingDependencyIds } }; } @@ -75511,7 +76509,7 @@ async function foldBuilderOutcome(context, graph, executed) { }); } if (!result.ok) { - finishWorker(input.workspace, record32, "FAILED", nowIso2(input)); + finishWorker(input.workspace, record32, "FAILED", nowIso3(input)); input.recordEvent("candidate_failed", { nodeId: input.node.nodeId, workUnitId: unitId, @@ -75545,7 +76543,7 @@ async function foldBuilderOutcome(context, graph, executed) { workUnitId: unitId, attempt, workerId, - createdAt: nowIso2(input), + createdAt: nowIso3(input), baselineCommit: prepared.baselineCommit, contextProjectionHash: projection.contentHash, contractSnapshotHash: projection.contractSnapshotHash, @@ -75599,7 +76597,7 @@ async function foldBuilderOutcome(context, graph, executed) { secondaryBuilderAttemptSchema.parse({ ...executed.secondaryAttempt, status: "CANDIDATE_READY", - updatedAt: nowIso2(input) + updatedAt: nowIso3(input) }) ); input.recordEvent("secondary_candidate_succeeded", { @@ -75620,7 +76618,7 @@ async function foldBuilderOutcome(context, graph, executed) { contractSnapshotHash: candidate.contractSnapshotHash }); if (!acceptance.ok) { - finishWorker(input.workspace, record32, "FAILED", nowIso2(input)); + finishWorker(input.workspace, record32, "FAILED", nowIso3(input)); return persistGraph2( input, applyUnitRejection(input, graph, unitId, attempt, { @@ -75629,7 +76627,7 @@ async function foldBuilderOutcome(context, graph, executed) { }) ); } - finishWorker(input.workspace, record32, "FINISHED", nowIso2(input)); + finishWorker(input.workspace, record32, "FINISHED", nowIso3(input)); graph = persistGraph2(input, transitionUnit(graph, unitId, "CANDIDATE_READY")); input.recordEvent("candidate_ready", { nodeId: input.node.nodeId, @@ -75656,7 +76654,7 @@ async function foldBuilderOutcome(context, graph, executed) { latestFailure: { category: "AMBIGUITY", message: `The builder is blocked: ${[...result.output.blockingQuestions, result.output.summary].join("; ").slice(0, 1500)}`, - at: nowIso2(input) + at: nowIso3(input) } }) ); @@ -75692,7 +76690,7 @@ async function runUnitAttempt(context, graph, unitId) { } function applyUnitRejection(input, graph, unitId, attempt, failure3) { const unit = requireUnit(graph, unitId); - const at = nowIso2(input); + const at = nowIso3(input); const rejected = transitionUnit(graph, unitId, unit.status === "READY" ? "FAILED" : "REJECTED"); const withFailure = withUnit(rejected, { ...requireUnit(rejected, unitId), @@ -75760,7 +76758,7 @@ async function resumeStoredCandidate(context, graph, unitId) { async function evaluateCandidate(context, graph, unitId, attempt, candidate, projection, patch) { const { input, truth } = context; const unit = requireUnit(graph, unitId); - const at = nowIso2(input); + const at = nowIso3(input); const deterministic = evaluateDeterministically({ candidate, workUnit: unit, @@ -75858,7 +76856,7 @@ async function evaluateCandidate(context, graph, unitId, attempt, candidate, pro return persistGraph2(input, transitionUnit(graph, unitId, "EVALUATING")); } function recordConflict(input, graph, unitId, candidate, reasons, affectedContractIds, decisionKindRaw) { - const at = nowIso2(input); + const at = nowIso3(input); const kind = decisionKindOf(decisionKindRaw); const contractId = affectedContractIds[0] ?? "unknown-contract"; const conflict = storeConflict(input.workspace, input.jobId, input.node.nodeId, { @@ -75916,7 +76914,7 @@ async function runSemanticEvaluation(context, graph, unitId) { const { input, truth } = context; const unit = requireUnit(graph, unitId); const attempt = Math.max(1, unit.attempt); - const at = nowIso2(input); + const at = nowIso3(input); const candidate = readCandidate(input.workspace, input.jobId, input.node.nodeId, unitId, attempt); const projection = readProjection(input.workspace, input.jobId, input.node.nodeId, unitId, attempt); if (candidate === void 0 || projection === void 0) { @@ -76067,7 +77065,7 @@ ${correction}`; affectedContractIds: output.affectedContractIds, ...output.decisionKind !== void 0 ? { decisionKind: output.decisionKind } : {}, evaluatorWorkerId: selection.worker.workerId, - createdAt: nowIso2(input) + createdAt: nowIso3(input) }); graph = withUnit(graph, { ...requireUnit(graph, unitId), @@ -76122,7 +77120,7 @@ ${correction}`; latestFailure: { category: "AMBIGUITY", message: `The evaluator needs a ${kind} decision: ${output.reasons.join("; ").slice(0, 1200)}`, - at: nowIso2(input) + at: nowIso3(input) } }) ); @@ -76268,12 +77266,12 @@ async function driveObjective(input) { input.node.nodeId, workerRecords, unit.workUnitId, - nowIso2(input) + nowIso3(input) ); reconciled = transitionUnit(reconciled, unit.workUnitId, "READY"); continue; } - finishWorker(input.workspace, accepted.record, "FINISHED", nowIso2(input)); + finishWorker(input.workspace, accepted.record, "FINISHED", nowIso3(input)); } reconciled = transitionUnit(reconciled, unit.workUnitId, "CANDIDATE_READY"); reconciled = withUnit(reconciled, { @@ -76296,7 +77294,7 @@ async function driveObjective(input) { secondaryBuilderAttemptSchema.parse({ ...secondaryAttempt, status: "CANDIDATE_READY", - updatedAt: nowIso2(input) + updatedAt: nowIso3(input) }) ); } @@ -76312,7 +77310,7 @@ async function driveObjective(input) { continue; } } - supersedeWorkers(input.workspace, input.jobId, input.node.nodeId, workerRecords, unit.workUnitId, nowIso2(input)); + supersedeWorkers(input.workspace, input.jobId, input.node.nodeId, workerRecords, unit.workUnitId, nowIso3(input)); reconciled = transitionUnit(reconciled, unit.workUnitId, unit.status === "BUILDING" ? "READY" : "CANDIDATE_READY"); } else if (unit.status === "BLOCKED") { reconciled = transitionUnit( @@ -76475,7 +77473,7 @@ async function maybeAggregateSemantically(context, graph) { } const output = result.output; storeAggregationReport(input.workspace, input.jobId, input.node.nodeId, reportName, { - synthesizedAt: nowIso2(input), + synthesizedAt: nowIso3(input), aggregator: selection.worker.workerId, sources: reports.map((report) => report.workUnitId), ...output @@ -76524,7 +77522,7 @@ async function maybeAggregateSemantically(context, graph) { affectedWorkUnitIds: conflict.claims.map((claim2) => claim2.sourceWorkUnitId), decisionKind: kind, status: "OPEN", - createdAt: nowIso2(input) + createdAt: nowIso3(input) }); input.recordEvent("contract_conflict_detected", { nodeId: input.node.nodeId, @@ -76615,7 +77613,7 @@ async function integrateVerifiedCandidates(input, graph) { }; } let integrated = graph; - const at = nowIso2(input); + const at = nowIso3(input); for (const entry2 of candidates) { integrated = withUnit(transitionUnit(integrated, entry2.unit.workUnitId, "INTEGRATED"), { ...requireUnit(transitionUnit(integrated, entry2.unit.workUnitId, "INTEGRATED"), entry2.unit.workUnitId), @@ -90978,7 +91976,7 @@ function readTemplateRecords(workspace) { } return { records, diagnostics }; } -function nowIso3(clock) { +function nowIso4(clock) { return isoNow(clock); } function rethrowSpecExists(cause, specName) { @@ -91153,7 +92151,7 @@ function executeTemplateApplication(workspace, plan, clock = systemClock, record schemaVersion: "1.0.0", recordId: id, type: "template-apply", - createdAt: nowIso3(clock), + createdAt: nowIso4(clock), result: "ok", templateRef: plan.templateRef, templateId: plan.templateId, @@ -91273,7 +92271,7 @@ function executeTemplateInstall(workspace, plan, clock = systemClock, recordId) schemaVersion: "1.0.0", recordId: id, type: "template-install", - createdAt: nowIso3(clock), + createdAt: nowIso4(clock), result: "ok", templateRef: plan.ref, templateId: plan.templateId, @@ -91353,7 +92351,7 @@ function executeTemplateUninstall(workspace, plan, clock = systemClock, recordId schemaVersion: "1.0.0", recordId: id, type: "template-uninstall", - createdAt: nowIso3(clock), + createdAt: nowIso4(clock), result: "ok", templateRef: plan.ref, templateId: plan.templateId, @@ -91748,7 +92746,7 @@ function executeTemplateScaffold(plan, workspace, clock = systemClock, recordId) schemaVersion: "1.0.0", recordId: id, type: "template-scaffold", - createdAt: nowIso3(clock), + createdAt: nowIso4(clock), result: "ok", templateId: plan.templateId, kind: plan.kind, @@ -99862,7 +100860,7 @@ function autonomyPolicyOf(deps3) { function now5(deps3) { return (deps3.clock ?? (() => /* @__PURE__ */ new Date()))(); } -function nowIso4(deps3) { +function nowIso5(deps3) { return now5(deps3).toISOString(); } function newId5(deps3) { @@ -100167,7 +101165,7 @@ function draftSeal(deps3, request) { missionId: mission.missionId, ...mission.specName !== void 0 ? { specName: mission.specName } : {}, status: "DRAFT", - createdAt: nowIso4(deps3), + createdAt: nowIso5(deps3), ...request.supersedes !== void 0 ? { supersedes: request.supersedes } : {}, ...base, presentAuthorityKinds: presentAuthorityKinds(base), @@ -100254,7 +101252,7 @@ function sealMission(deps3, request) { const sealed = missionSealSchema.parse({ ...existing, status: "SEALED", - sealedAt: nowIso4(deps3), + sealedAt: nowIso5(deps3), sealedVia: (request.via ?? hostOf(deps3)).slice(0, SEAL_LIMITS.maxShortTextChars) }); writeJsonRecord(sealFile(deps3.workspace, existing.sealId), sealed); @@ -100276,7 +101274,7 @@ function revokeSeal(deps3, sealId, reason) { const revoked = missionSealSchema.parse({ ...seal, status: "REVOKED", - revokedAt: nowIso4(deps3), + revokedAt: nowIso5(deps3), revokedReason: reason.slice(0, SEAL_LIMITS.maxTextChars) }); writeJsonRecord(sealFile(deps3.workspace, sealId), revoked); @@ -100347,7 +101345,7 @@ function bindSealToJob(deps3, jobId, sealId) { jobId, sealId, missionId: seal.missionId, - boundAt: nowIso4(deps3), + boundAt: nowIso5(deps3), boundPolicyFingerprint: seal.delegatedAuthority.policyFingerprint }); writeJsonRecord(bindingFile(deps3.workspace, jobId), binding); @@ -100985,8 +101983,8 @@ function loadSupervisorState(deps3, ownerId) { return supervisorStateSchema.parse({ schemaVersion: SUPERVISOR_SCHEMA_VERSION, ownerId, - startedAt: nowIso4(deps3), - heartbeatAt: nowIso4(deps3), + startedAt: nowIso5(deps3), + heartbeatAt: nowIso5(deps3), pid: process.pid, hostname: safeHostname(), jobs: [] @@ -101009,7 +102007,7 @@ function findSupervisedJob(state, jobId) { } function appendSupervisionLog(deps3, entry2) { const validated = supervisionLogEntrySchema.parse({ - at: nowIso4(deps3), + at: nowIso5(deps3), ownerId: entry2.ownerId, ...entry2.jobId !== void 0 ? { jobId: entry2.jobId } : {}, action: entry2.action, @@ -101065,13 +102063,13 @@ function registerSupervisedJob(deps3, input) { specName: input.specName, ...input.sealId !== void 0 ? { sealId: input.sealId } : {}, status: "REGISTERED", - registeredAt: nowIso4(deps3) + registeredAt: nowIso5(deps3) } ); writeSupervisorState(deps3.workspace, { ...upsertSupervisedJob(state, supervised), schemaVersion: SUPERVISOR_SCHEMA_VERSION, - heartbeatAt: nowIso4(deps3) + heartbeatAt: nowIso5(deps3) }); return supervised; } @@ -101153,7 +102151,7 @@ async function superviseJob(deps3, jobId, options) { ); } emit22("lease", `lease acquired by ${ownerId} (generation ${acquisition.lease?.generation ?? 1})`); - const sessionStartedAt = options.sessionStartedAt ?? nowIso4(deps3); + const sessionStartedAt = options.sessionStartedAt ?? nowIso5(deps3); let job = requireJobState(deps3.workspace, jobId); let supervised = registerSupervisedJob(deps3, { jobId, @@ -101234,7 +102232,7 @@ async function applyDecision(deps3, input) { detail: decision.reason }); return { - supervised: { ...input.supervised, status: "RELEASED", releasedAt: nowIso4(deps3), releaseReason: decision.reason }, + supervised: { ...input.supervised, status: "RELEASED", releasedAt: nowIso5(deps3), releaseReason: decision.reason }, stop: job.status === "COMPLETED" ? { kind: "completed", status: job.status } : { kind: "released", reason: decision.reason } }; } @@ -101247,7 +102245,7 @@ async function applyDecision(deps3, input) { detail: decision.reason }); return { - supervised: { ...input.supervised, status: "RELEASED", releasedAt: nowIso4(deps3), releaseReason: decision.reason }, + supervised: { ...input.supervised, status: "RELEASED", releasedAt: nowIso5(deps3), releaseReason: decision.reason }, stop: { kind: "needs-human", status: decision.status, @@ -101263,7 +102261,7 @@ async function applyDecision(deps3, input) { detail: decision.reason }); return { - supervised: { ...input.supervised, status: "RELEASED", releasedAt: nowIso4(deps3), releaseReason: decision.reason }, + supervised: { ...input.supervised, status: "RELEASED", releasedAt: nowIso5(deps3), releaseReason: decision.reason }, stop: { kind: "gave-up", reason: decision.reason } }; } @@ -101357,10 +102355,10 @@ function foldDriverOutcome(deps3, input, outcome, action) { consecutiveRestarts: 0, backoffMs: 0, lastProgressFingerprint: after, - lastProgressAt: nowIso4(deps3), + lastProgressAt: nowIso5(deps3), nextAttemptAt: void 0, lastAction: unclean ? "DRIVER_DIED" : "DRIVER_EXITED_CLEANLY", - lastActionAt: nowIso4(deps3) + lastActionAt: nowIso5(deps3) }); } const backoffMs = nextBackoffMs( @@ -101378,7 +102376,7 @@ function foldDriverOutcome(deps3, input, outcome, action) { lastProgressFingerprint: after, nextAttemptAt: new Date(now5(deps3).getTime() + backoffMs).toISOString(), lastAction: unclean ? "DRIVER_DIED" : "DRIVER_EXITED_CLEANLY", - lastActionAt: nowIso4(deps3) + lastActionAt: nowIso5(deps3) }); } async function sleepInSlices(deps3, input) { @@ -101403,7 +102401,7 @@ function persistSupervised(deps3, ownerId, supervised) { writeSupervisorState(deps3.workspace, { ...upsertSupervisedJob(state, supervised), ownerId, - heartbeatAt: nowIso4(deps3) + heartbeatAt: nowIso5(deps3) }); } function fingerprintOf(deps3, job) { @@ -101594,7 +102592,7 @@ async function runOvernightPreflight(deps3, request) { const policy = autonomyPolicyOf(deps3); const projectDir = request.projectDir ?? deps3.workspace.rootDir; const run = request.probeRunner ?? createProcessProbeRunner(projectDir); - const at = nowIso4(deps3); + const at = nowIso5(deps3); const checks = []; const seal = resolveSeal(deps3, request); const add2 = (capability, outcome, observed, extra = {}) => { @@ -102177,7 +103175,7 @@ function requestToolsmithCapability(deps3, input) { grantsUsed: ledger.granted } ); - const at = nowIso4(deps3); + const at = nowIso5(deps3); const request = toolsmithRequestSchema.parse({ schemaVersion: TOOLSMITH_SCHEMA_VERSION, requestId: input.requestId ?? newRecordId(deps3, "ts"), @@ -102229,11 +103227,11 @@ function loadLedger(deps3, jobId) { return readToolsmithLedger(deps3.workspace, jobId) ?? toolsmithLedgerSchema.parse({ schemaVersion: TOOLSMITH_SCHEMA_VERSION, jobId, - updatedAt: nowIso4(deps3) + updatedAt: nowIso5(deps3) }); } function saveLedger(deps3, jobId, ledger) { - const next = toolsmithLedgerSchema.parse({ ...ledger, updatedAt: nowIso4(deps3) }); + const next = toolsmithLedgerSchema.parse({ ...ledger, updatedAt: nowIso5(deps3) }); writeJsonRecord(ledgerFile(deps3.workspace, jobId), next); return next; } @@ -102582,7 +103580,7 @@ async function provisionEnvironment(deps3, options) { planId: plan.planId, ...options.jobId !== void 0 ? { jobId: options.jobId } : {}, status: "PROVISIONING", - createdAt: nowIso4(deps3), + createdAt: nowIso5(deps3), services: ordered.map((service) => ({ serviceId: service.serviceId, status: "PENDING" })) }) ); @@ -102611,7 +103609,7 @@ async function provisionEnvironment(deps3, options) { services: instance.services.map((service) => ({ ...service, status: "STARTING", - startedAt: nowIso4(deps3) + startedAt: nowIso5(deps3) })) }); for (const service of ordered) { @@ -102643,7 +103641,7 @@ async function provisionEnvironment(deps3, options) { const ready = writeInstance(deps3, { ...instance, status: "READY", - readyAt: nowIso4(deps3) + readyAt: nowIso5(deps3) }); writeEvidence(deps3, plan, ready, now5(deps3).getTime() - startedAtMs, []); emitJobEvent(deps3, options.jobId, "environment_ready", { @@ -102659,7 +103657,7 @@ async function waitForService(deps3, input) { let state = { serviceId: service.serviceId, status: "WAITING_READY", - startedAt: nowIso4(deps3), + startedAt: nowIso5(deps3), restarts: 0, probeAttempts: 0 }; @@ -102687,7 +103685,7 @@ async function waitForService(deps3, input) { state: { ...state, status: "READY", - readyAt: nowIso4(deps3), + readyAt: nowIso5(deps3), lastProbeKind: lastKind, lastProbeDetail: lastDetail }, @@ -102795,7 +103793,7 @@ async function teardownEnvironment(deps3, input) { const stopped = writeInstance(deps3, { ...instance, status: "STOPPED", - stoppedAt: nowIso4(deps3), + stoppedAt: nowIso5(deps3), diagnosticsRetained: retain, services: instance.services.map((service) => ({ ...service, status: "STOPPED" })) }); @@ -102823,7 +103821,7 @@ function writeEvidence(deps3, plan, instance, totalMs, logRefs) { schemaVersion: ENVIRONMENT_SCHEMA_VERSION, instanceId: instance.instanceId, planId: plan.planId, - recordedAt: nowIso4(deps3), + recordedAt: nowIso5(deps3), status: instance.status, applicationLevelReady, livenessOnlyReady, @@ -103100,7 +104098,7 @@ async function runBrowserScenario(deps3, options) { ); } const resultId = options.resultId ?? newRecordId(deps3, "br"); - const startedAt = nowIso4(deps3); + const startedAt = nowIso5(deps3); const startedMs = now5(deps3).getTime(); emitJobEvent2(deps3, options.jobId, "browser_scenario_started", { scenarioId: scenario.scenarioId, @@ -103118,7 +104116,7 @@ async function runBrowserScenario(deps3, options) { ...options.jobId !== void 0 ? { jobId: options.jobId } : {}, status: "SKIPPED_NO_RUNTIME", startedAt, - finishedAt: nowIso4(deps3), + finishedAt: nowIso5(deps3), driver: options.driver.label, skipReason: availability.reason }), @@ -103148,7 +104146,7 @@ async function runBrowserScenario(deps3, options) { ...options.jobId !== void 0 ? { jobId: options.jobId } : {}, status: "ERRORED", startedAt, - finishedAt: nowIso4(deps3), + finishedAt: nowIso5(deps3), driver: options.driver.label, failureDetail: (cause instanceof Error ? cause.message : String(cause)).slice(0, 4e3) }), @@ -103232,7 +104230,7 @@ async function runBrowserScenario(deps3, options) { ...options.jobId !== void 0 ? { jobId: options.jobId } : {}, status, startedAt, - finishedAt: nowIso4(deps3), + finishedAt: nowIso5(deps3), driver: options.driver.label, steps, assertionsRun, @@ -103891,7 +104889,7 @@ function readClosureLedger(workspace, jobId) { ); } function buildClosureLedger(deps3, input) { - const at = nowIso4(deps3); + const at = nowIso5(deps3); const entries = []; const registry2 = readContractRegistry(deps3.workspace, input.seal.missionId); const byContract = new Map(registry2.map((contract) => [contract.contractId, contract])); @@ -103962,7 +104960,7 @@ function entry(at, input) { } function attributeNodeToItems(deps3, input) { const ledger = requireLedger(deps3.workspace, input.jobId); - const at = nowIso4(deps3); + const at = nowIso5(deps3); const entries = ledger.entries.map((existing) => { if (!input.itemIds.includes(existing.itemId)) return existing; if (existing.attributedNodeIds.includes(input.nodeId)) return existing; @@ -103977,7 +104975,7 @@ function attributeNodeToItems(deps3, input) { } function registerClosureEvidence(deps3, input) { const ledger = requireLedger(deps3.workspace, input.jobId); - const at = nowIso4(deps3); + const at = nowIso5(deps3); const entries = ledger.entries.map((existing) => { if (!input.itemIds.includes(existing.itemId)) return existing; return { @@ -104002,7 +105000,7 @@ function registerClosureEvidence(deps3, input) { } function waiveClosureItem(deps3, input) { const ledger = requireLedger(deps3.workspace, input.jobId); - const at = nowIso4(deps3); + const at = nowIso5(deps3); const entries = ledger.entries.map( (existing) => existing.itemId === input.itemId ? { ...existing, @@ -104022,7 +105020,7 @@ function runClosureAudit(deps3, input) { const policy = autonomyPolicyOf(deps3).closure; const ledger = requireLedger(deps3.workspace, input.jobId); const completed = new Set(input.completedNodeIds); - const at = nowIso4(deps3); + const at = nowIso5(deps3); const entries = ledger.entries.map((existing) => { const attributedNodesComplete = existing.attributedNodeIds.length > 0 && existing.attributedNodeIds.every((nodeId) => completed.has(nodeId)); const assessment = assessItemClosure( @@ -104128,7 +105126,7 @@ function generateGapWork(deps3, input) { const policy = autonomyPolicyOf(deps3).closure; const ledger = requireLedger(deps3.workspace, input.jobId); const byId = new Map(ledger.entries.map((existing) => [existing.itemId, existing])); - const at = nowIso4(deps3); + const at = nowIso5(deps3); const generated = []; for (const unclosed of input.audit.unclosed.slice(0, policy.maxGapWorkPerCycle)) { const existing = byId.get(unclosed.itemId); @@ -104196,7 +105194,7 @@ function requireLedger(workspace, jobId) { return ledger; } function saveLedger2(deps3, ledger) { - const next = closureLedgerSchema.parse({ ...ledger, updatedAt: nowIso4(deps3) }); + const next = closureLedgerSchema.parse({ ...ledger, updatedAt: nowIso5(deps3) }); writeJsonRecord(closureLedgerFile(deps3.workspace, next.jobId), next); return next; } @@ -104288,7 +105286,7 @@ function saveSystemScenario(deps3, input) { const scenario = systemScenarioSchema.parse({ schemaVersion: SYSTEM_SCENARIO_SCHEMA_VERSION, scenarioId: input.scenarioId ?? newRecordId(deps3, "ss"), - createdAt: nowIso4(deps3), + createdAt: nowIso5(deps3), ...input }); writeJsonRecord(scenarioFile(deps3.workspace, scenario.scenarioId), scenario); @@ -104312,7 +105310,7 @@ async function runSystemScenario(deps3, options) { throw new AutonomyError("SBA024", `No system scenario "${options.scenarioId}" exists.`); } const resultId = options.resultId ?? newRecordId(deps3, "sr"); - const startedAt = nowIso4(deps3); + const startedAt = nowIso5(deps3); emit2(deps3, options.jobId, "system_qualification_started", { scenarioId: scenario.scenarioId, resultId @@ -104453,7 +105451,7 @@ async function finish3(deps3, options, scenario, input) { schemaVersion: SYSTEM_SCENARIO_SCHEMA_VERSION, scenarioId: scenario.scenarioId, ...options.jobId !== void 0 ? { jobId: options.jobId } : {}, - finishedAt: nowIso4(deps3), + finishedAt: nowIso5(deps3), ...input }); writeJsonRecord(resultFile(deps3.workspace, result.resultId), result); @@ -104540,7 +105538,7 @@ function resultFile2(workspace, runId) { async function runReproducibilityQualification(deps3, options) { const policy = autonomyPolicyOf(deps3).closure; const runId = options.runId ?? newRecordId(deps3, "rp"); - const startedAt = nowIso4(deps3); + const startedAt = nowIso5(deps3); const deadline = now5(deps3).getTime() + policy.reproducibilityTimeoutMs; const run = options.commandRunner ?? (async (input) => { const [executable, ...argv2] = input.argv; @@ -104610,7 +105608,7 @@ async function runReproducibilityQualification(deps3, options) { ...options.jobId !== void 0 ? { jobId: options.jobId } : {}, status, startedAt, - finishedAt: nowIso4(deps3), + finishedAt: nowIso5(deps3), checkoutPath: options.checkoutPath.slice(0, 200), ...options.gitHead !== void 0 ? { gitHead: options.gitHead } : {}, dimensions: [...new Set(options.steps.map((step2) => step2.dimension))], @@ -105134,7 +106132,7 @@ function computeAutonomyTelemetry(deps3, input) { schemaVersion: TELEMETRY_SCHEMA_VERSION, jobId: input.jobId, ...binding !== void 0 ? { sealId: binding.sealId, missionId: binding.missionId } : {}, - recordedAt: nowIso4(deps3), + recordedAt: nowIso5(deps3), jobStatus: job?.status ?? "UNKNOWN", humanInterventionsAfterSeal: interventions.length, humanAuthorityEscalations: counters?.authorityEscalations ?? 0, @@ -105397,7 +106395,7 @@ async function runUnattendedMission(deps3, options) { completionGate: createClosureCompletionGate(deps3.workspace, policy.closure) }; const host = typeof options.host === "function" ? options.host(supervisedDeps) : options.host; - const sessionStartedAt = nowIso4(deps3); + const sessionStartedAt = nowIso5(deps3); const startedMs = now5(deps3).getTime(); const recoveries = []; const audits = []; @@ -106164,7 +107162,7 @@ function missionDepsOf(deps3) { function now6(deps3) { return (deps3.clock ?? (() => /* @__PURE__ */ new Date()))(); } -function nowIso5(deps3) { +function nowIso6(deps3) { return now6(deps3).toISOString(); } function hostOf2(deps3) { @@ -107703,7 +108701,7 @@ function groundInRepository(deps3, request) { return { schemaVersion: INTAKE_GROUNDING_SCHEMA_VERSION, intakeId: request.intakeId, - groundedAt: nowIso5(deps3), + groundedAt: nowIso6(deps3), baselineCommit: readGitHead(workspace.rootDir), existingProduct, evidence: evidence.slice(0, INTAKE_LIMITS.maxEvidence), @@ -107865,12 +108863,12 @@ function analyzeDeltaAuthority(request) { const counts = {}; for (const cls of DELTA_AUTHORITY_CLASSES) counts[cls] = 0; for (const item of items) counts[item.classification] = (counts[item.classification] ?? 0) + 1; - const modifiedContractIds = unique( + const modifiedContractIds = unique2( items.filter( (item) => item.classification === "EXISTING_SEALED_CONTRACT_CHANGE" || item.classification === "CONTRADICTION" ).map((item) => item.existingContractId).filter((id) => id !== void 0) ); - const extendedContractIds = unique( + const extendedContractIds = unique2( items.filter((item) => item.classification === "EXISTING_CONTRACT_EXTENSION").map((item) => item.existingContractId).filter((id) => id !== void 0) ); const owners = /* @__PURE__ */ new Map(); @@ -107896,7 +108894,7 @@ function analyzeDeltaAuthority(request) { relation }); } - const newSurfaces = unique( + const newSurfaces = unique2( items.filter((item) => item.classification === "NEW_DELEGATED_SURFACE").flatMap((item) => item.affectedSurfaces) ); const reasons = []; @@ -108135,7 +109133,7 @@ function raiseItemForQuestion(item, questionId, why) { function statementOf(chunk) { return chunk.text.replace(/^(\s*)([-*+]|\d+[.)])\s+/, "").trim(); } -function unique(values) { +function unique2(values) { return [...new Set(values)]; } function computeBasisDigest(request) { @@ -109427,7 +110425,7 @@ function recordDerivedApprovals(request) { return { approved, results }; } function emptyLedger(deps3, approval) { - const at = nowIso5(deps3); + const at = nowIso6(deps3); return { schemaVersion: INTAKE_LIFECYCLE_SCHEMA_VERSION, intakeId: approval.intakeId, @@ -109476,13 +110474,13 @@ async function runSealAndBuild(deps3, options) { outcome: void 0, finishedAt: void 0, humanPrerequisites: [], - updatedAt: nowIso5(deps3) + updatedAt: nowIso6(deps3) }); } const persist4 = (next) => { const written = writeLifecycle(deps3.workspace, { ...next, - updatedAt: nowIso5(deps3) + updatedAt: nowIso6(deps3) }); ledger = written; return written; @@ -109492,12 +110490,12 @@ async function runSealAndBuild(deps3, options) { persist4( withStep(ledger, step2, { status: "RUNNING", - startedAt: nowIso5(deps3), + startedAt: nowIso6(deps3), attempts: record5.attempts + 1 }) ); appendIntakeEvent(deps3.workspace, options.intakeId, { - at: nowIso5(deps3), + at: nowIso6(deps3), type: "build_step_started", step: step2 }); @@ -109507,13 +110505,13 @@ async function runSealAndBuild(deps3, options) { persist4( withStep(ledger, step2, { status, - settledAt: nowIso5(deps3), + settledAt: nowIso6(deps3), detail: detail.slice(0, INTAKE_LIMITS.maxTextChars), ...result !== void 0 ? { result } : {} }) ); appendIntakeEvent(deps3.workspace, options.intakeId, { - at: nowIso5(deps3), + at: nowIso6(deps3), type: "build_step_completed", step: step2, status, @@ -109525,12 +110523,12 @@ async function runSealAndBuild(deps3, options) { persist4( withStep(ledger, step2, { status: "FAILED", - settledAt: nowIso5(deps3), + settledAt: nowIso6(deps3), detail: detail.slice(0, INTAKE_LIMITS.maxTextChars) }) ); appendIntakeEvent(deps3.workspace, options.intakeId, { - at: nowIso5(deps3), + at: nowIso6(deps3), type: "build_step_failed", step: step2, detail: detail.slice(0, 600) @@ -109592,7 +110590,7 @@ async function runSealAndBuild(deps3, options) { writeProjectionEquivalence(deps3.workspace, equivalence); if (!equivalence.equivalent) { appendIntakeEvent(deps3.workspace, options.intakeId, { - at: nowIso5(deps3), + at: nowIso6(deps3), type: "projection_diverged", divergences: equivalence.divergences.length }); @@ -109603,7 +110601,7 @@ async function runSealAndBuild(deps3, options) { return finish4(deps3, options, ledger, "FAILED", { preflight }); } appendIntakeEvent(deps3.workspace, options.intakeId, { - at: nowIso5(deps3), + at: nowIso6(deps3), type: "projection_validated", checked: equivalence.checkedStatements, traced: equivalence.tracedStatements @@ -109635,7 +110633,7 @@ async function runSealAndBuild(deps3, options) { }); for (const stage of derived.approved) { appendIntakeEvent(deps3.workspace, options.intakeId, { - at: nowIso5(deps3), + at: nowIso6(deps3), type: "derived_approval_recorded", stage, approvalId: approval.approvalId, @@ -109685,7 +110683,7 @@ async function runSealAndBuild(deps3, options) { bindApprovalSeal(deps3.workspace, options.intakeId, seal.sealId); ledger = persist4({ ...ledger, sealId }); appendIntakeEvent(deps3.workspace, options.intakeId, { - at: nowIso5(deps3), + at: nowIso6(deps3), type: "seal_created", sealId: seal.sealId, approvalId: approval.approvalId, @@ -109710,7 +110708,7 @@ async function runSealAndBuild(deps3, options) { preflight = await runPreflight2(deps3, options, approval.missionId, sealId); ledger = persist4({ ...ledger, preflightReportId: preflight.reportId }); appendIntakeEvent(deps3.workspace, options.intakeId, { - at: nowIso5(deps3), + at: nowIso6(deps3), type: "preflight_completed", reportId: preflight.reportId, verdict: preflight.verdict @@ -109730,11 +110728,11 @@ async function runSealAndBuild(deps3, options) { withStep({ ...ledger, preflightReportId: preflight.reportId }, "PREFLIGHT", { detail: preflight.verdict, result: preflight.reportId, - settledAt: nowIso5(deps3) + settledAt: nowIso6(deps3) }) ); appendIntakeEvent(deps3.workspace, options.intakeId, { - at: nowIso5(deps3), + at: nowIso6(deps3), type: "preflight_completed", reportId: preflight.reportId, verdict: preflight.verdict, @@ -109749,7 +110747,7 @@ async function runSealAndBuild(deps3, options) { }); for (const resolved2 of resolution.resolved) { appendIntakeEvent(deps3.workspace, options.intakeId, { - at: nowIso5(deps3), + at: nowIso6(deps3), type: "prerequisite_resolved", detail: resolved2.slice(0, 400) }); @@ -109784,7 +110782,7 @@ async function runSealAndBuild(deps3, options) { const intake = requireIntakeState(deps3.workspace, options.intakeId); writeIntakeState(deps3.workspace, { ...intake, jobId, specName, sealId, status: "BUILDING" }); appendIntakeEvent(deps3.workspace, options.intakeId, { - at: nowIso5(deps3), + at: nowIso6(deps3), type: "job_created", jobId, specName @@ -109808,7 +110806,7 @@ async function runSealAndBuild(deps3, options) { if (retry.cleared) { emit3("lifecycle", `job ${jobId} unblocked and returned to the schedulable path`); appendIntakeEvent(deps3.workspace, options.intakeId, { - at: nowIso5(deps3), + at: nowIso6(deps3), type: "build_step_started", step: "LAUNCH", unblocked: true @@ -109845,7 +110843,7 @@ async function runSealAndBuild(deps3, options) { begin("LAUNCH"); try { appendIntakeEvent(deps3.workspace, options.intakeId, { - at: nowIso5(deps3), + at: nowIso6(deps3), type: "unattended_launched", jobId, sealId: sealId ?? null @@ -109886,7 +110884,7 @@ function validateProjection(deps3, approval, specName) { workspace: deps3.workspace, approval, specName, - checkedAt: nowIso5(deps3), + checkedAt: nowIso6(deps3), approvedElements: approvedElements( approval, contracts.map((contract) => ({ @@ -109979,11 +110977,11 @@ function finish4(deps3, options, ledger, outcome, extras) { const finished7 = writeLifecycle(deps3.workspace, { ...ledger, outcome, - updatedAt: nowIso5(deps3), - finishedAt: nowIso5(deps3) + updatedAt: nowIso6(deps3), + finishedAt: nowIso6(deps3) }); appendIntakeEvent(deps3.workspace, options.intakeId, { - at: nowIso5(deps3), + at: nowIso6(deps3), type: "build_finished", outcome, ...finished7.jobId !== void 0 ? { jobId: finished7.jobId } : {} @@ -110031,7 +111029,7 @@ function startSpecIntake(deps3, request) { const contentHash = sha256Hex(content); const storedPath = storeSourceText(deps3.workspace, intakeId, contentHash, content); const parsed = parseSpecificationDocument(content); - const at = nowIso5(deps3); + const at = nowIso6(deps3); const goal = request.goal?.trim() ?? deriveGoal(parsed.chunks, name); const mission = beginMission(missionDepsOf(deps3), { name, goal }); const source = writeSpecSource(deps3.workspace, { @@ -110126,7 +111124,7 @@ function runIntakeDiscovery(deps3, intakeId, options = {}) { throw new IntakeError("SBI004", `Spec intake ${intakeId} is ABANDONED and read-only.`); } const source = requireSpecSource(deps3.workspace, intakeId); - const at = nowIso5(deps3); + const at = nowIso6(deps3); const grounding = writeGrounding( deps3.workspace, groundInRepository(deps3, { @@ -110370,7 +111368,7 @@ function answerIntakeQuestion(deps3, intakeId, request, options = {}) { `Question ${request.questionId} was never mirrored into the mission and cannot record a governed answer.` ); } - const at = nowIso5(deps3); + const at = nowIso6(deps3); const result = answerQuestion(missionDepsOf(deps3), intake.missionId, { questionId: question.missionQuestionId, answer @@ -110442,7 +111440,7 @@ function approveIntake(deps3, request) { (decision) => decision.status === "active" ); const source = requireSpecSource(deps3.workspace, request.intakeId); - const at = nowIso5(deps3); + const at = nowIso6(deps3); const approval = writeApproval( deps3.workspace, buildIntakeApproval({ @@ -110533,7 +111531,7 @@ function requireIntakeFor(deps3, subject) { function abandonIntake(deps3, intakeId, reason) { const intake = requireIntakeState(deps3.workspace, intakeId); if (intake.status === "ABANDONED") return intake; - const at = nowIso5(deps3); + const at = nowIso6(deps3); appendIntakeEvent(deps3.workspace, intakeId, { at, type: "intake_abandoned", @@ -110673,7 +111671,7 @@ function recordFeatureOutcome(deps3, intakeId, outcome) { features[index] = updated; writeProductBaseline(deps3.workspace, { ...baseline, - updatedAt: nowIso5(deps3), + updatedAt: nowIso6(deps3), features }); } @@ -110693,7 +111691,7 @@ function computeIntakeTelemetry(deps3, intakeId) { return intakeTelemetrySchema.parse({ schemaVersion: INTAKE_TELEMETRY_SCHEMA_VERSION, intakeId, - recordedAt: nowIso5(deps3), + recordedAt: nowIso6(deps3), status: intake.status, discoveryHumanTurns: intake.counters.discoveryHumanTurns, productQuestionsAsked: questions.length, @@ -111499,7 +112497,7 @@ function bootstrapWorkspace(deps3, options = {}) { const ensured = ensureRepositoryIndex({ workspace, config: deps3.config, - now: nowIso5(deps3), + now: nowIso6(deps3), // Bootstrap has no Git snapshot to name additions, so the refresh walks // for them: a snapshot that missed a brand-new capability file would // claim currency over a repository it has not actually seen. @@ -111549,7 +112547,7 @@ function bootstrapWorkspace(deps3, options = {}) { schemaVersion: BOOTSTRAP_SCHEMA_VERSION, snapshotId: `snap-${contentHash.slice(0, 12)}`, workspaceKey: workspaceKeyFor(workspace.rootDir), - createdAt: nowIso5(deps3), + createdAt: nowIso6(deps3), ...material, indexStats: { entries: ensured.state.entries.length, @@ -111592,7 +112590,7 @@ function inspectWorkspace(deps3, options) { const ensured = ensureRepositoryIndex({ workspace, config: deps3.config, - now: nowIso5(deps3) + now: nowIso6(deps3) }); const query = buildRetrievalQuery({ taskId: "workspace-inspect", diff --git a/integrations/claude-code-plugin/specbridge/dist/mcp-server.cjs b/integrations/claude-code-plugin/specbridge/dist/mcp-server.cjs index b721e46..f167464 100644 --- a/integrations/claude-code-plugin/specbridge/dist/mcp-server.cjs +++ b/integrations/claude-code-plugin/specbridge/dist/mcp-server.cjs @@ -51606,6 +51606,8 @@ var CONTEXT_SELECTION_REASONS = [ "DEPENDENCY_PROXIMITY", /** In the same module/package as an already-selected file. */ "MODULE_PROXIMITY", + /** A bounded nearby implementation with the same structural convention. */ + "REFERENCE_PATTERN", /** A declared symbol matched a query symbol. */ "SYMBOL_MATCH", /** Filename/path tokens overlapped the query tokens. */ @@ -52900,6 +52902,7 @@ var DEFAULT_RANKING_WEIGHTS = Object.freeze({ testSourcePair: 180, dependencyProximity: 120, moduleProximity: 40, + referencePattern: 150, tokenOverlapPerToken: 12, tokenOverlapCap: 96, priorTaskRelevance: 60, @@ -53037,6 +53040,31 @@ function rankCandidates(index, query, options = {}) { for (const dependent of index.dependentsOf(anchor)) { add(dependent, "DEPENDENCY_PROXIMITY", weights.dependencyProximity, level2, anchor); } + const anchorEntry = index.get(anchor); + if (anchorEntry !== void 0 && anchorEntry.kind === "source") { + const suffixes = structuralSuffixes(anchorEntry.path, anchorEntry.symbols); + for (const sibling of index.siblingsIn(anchorEntry.module)) { + if (sibling === anchor) continue; + const entry = index.get(sibling); + if (entry === void 0 || entry.kind !== "source" || entry.language !== anchorEntry.language) { + continue; + } + const sharedImportCount = entry.imports.filter( + (specifier) => anchorEntry.imports.includes(specifier) + ).length; + const sameConvention = structuralSuffixes(entry.path, entry.symbols).some( + (suffix) => suffixes.includes(suffix) + ); + if (!sameConvention && sharedImportCount < 2) continue; + add( + sibling, + "REFERENCE_PATTERN", + weights.referencePattern + Math.min(30, sharedImportCount * 10), + level2, + sameConvention ? anchor : `${anchor} (${sharedImportCount} shared imports)` + ); + } + } } const anchorModules = new Set(anchors.map((anchor) => index.get(anchor)?.module ?? "")); for (const module2 of anchorModules) { @@ -53071,6 +53099,16 @@ function rankCandidates(index, query, options = {}) { ); return ranked.slice(0, maxCandidates); } +function structuralSuffixes(relativePath, symbols) { + const base = relativePath.split("/").at(-1)?.replace(/\.[^.]+$/, "") ?? ""; + const values = [base, ...symbols]; + const suffixes = /* @__PURE__ */ new Set(); + for (const value of values) { + const match = value.match(/([A-Z][a-z0-9]{2,})$/); + if (match?.[1] !== void 0) suffixes.add(match[1].toLowerCase()); + } + return [...suffixes]; +} var DEFAULT_SECTION_OPTIONS = { wholeFileUnderChars: 6e3, targetSectionChars: 4e3, @@ -53302,6 +53340,8 @@ function planContextExpansion(input) { } var shortText3 = external_exports.string().min(1).max(CONTEXT_LIMITS.maxShortTextChars); var selectedContextItemSchema = external_exports.object({ + /** Repository identity when a plan participates in a multi-repo packet. */ + repositoryId: shortText3.optional(), /** Workspace-relative path, forward slashes. */ path: shortText3, /** Content hash the selection was made against. Verified before use. */ @@ -53320,6 +53360,7 @@ var selectedContextItemSchema = external_exports.object({ detail: shortText3.optional() }).passthrough(); var contextPointerSchema = external_exports.object({ + repositoryId: shortText3.optional(), path: shortText3, reason: external_exports.enum(CONTEXT_SELECTION_REASONS), /** @@ -53337,6 +53378,7 @@ var contextPointerSchema = external_exports.object({ detail: shortText3.optional() }).passthrough(); var excludedCandidateSchema = external_exports.object({ + repositoryId: shortText3.optional(), path: shortText3, reason: external_exports.enum(CONTEXT_EXCLUSION_REASONS), score: external_exports.number().int(), @@ -53349,6 +53391,8 @@ var contextSelectionPlanSchema = external_exports.object({ taskId: shortText3, nodeId: shortText3.optional(), attemptId: shortText3.optional(), + /** Repository identity; paths are unique only within this namespace. */ + repositoryId: shortText3.optional(), strategy: external_exports.enum(CONTEXT_STRATEGIES), shape: external_exports.enum(CONTEXT_SHAPES), role: external_exports.enum(RETRIEVAL_ROLES), @@ -53395,6 +53439,17 @@ var contextSelectionPlanSchema = external_exports.object({ }).passthrough(), /** Stable identities of the reusable prompt components (observability). */ componentHashes: external_exports.record(external_exports.string().nullable()).default({}), + /** Phase 5 facts; metrics are evidence for later policy, never eligibility here. */ + builderPacket: external_exports.object({ + contextSufficient: external_exports.boolean(), + explicitTargetResolved: external_exports.boolean(), + targetAmbiguity: external_exports.boolean(), + testsFound: external_exports.boolean(), + verificationHintsAvailable: external_exports.boolean(), + referencePatternFound: external_exports.boolean(), + dependencyContextComplete: external_exports.boolean(), + sourceBudgetUtilization: external_exports.number().min(0) + }).passthrough().optional(), createdAt: shortText3 }).passthrough(); var RERANK_LIMITS = { @@ -57797,12 +57852,15 @@ var LOCAL_EXECUTOR_SYSTEM_PROMPT = [ " a verification cycle.", "- The response must be valid JSON for the provided schema." ].join("\n"); -var SECONDARY_BUILDER_PACKET_SCHEMA_VERSION = "1.0.0"; -var SECONDARY_BUILDER_RESULT_SCHEMA_VERSION = "1.0.0"; -var SECONDARY_BUILDER_ATTEMPT_SCHEMA_VERSION = "1.0.0"; +var SECONDARY_BUILDER_PACKET_SCHEMA_VERSION = "1.1.0"; +var SECONDARY_BUILDER_RESULT_SCHEMA_VERSION = "1.1.0"; +var SECONDARY_BUILDER_ATTEMPT_SCHEMA_VERSION = "1.1.0"; var SECONDARY_BUILDER_LIMITS = { ...LOCAL_EXECUTION_LIMITS, maxSourceFiles: 16, + maxTests: 6, + maxReferencePatterns: 4, + maxDependencyContext: 12, maxSourceFileChars: 32768, maxSourceBytes: 262144, maxPacketCharacters: 524288, @@ -57813,11 +57871,60 @@ var boundedText = (max) => external_exports.string().min(1).max(max); var shortText42 = boundedText(512); var sha256 = external_exports.string().regex(/^[a-f0-9]{64}$/); var secondarySourceContextSchema = external_exports.object({ - /** Worktree-relative path. Whole-file source only in Phase 4. */ + /** Repository namespace. Paths are unique only within this identity. */ + repositoryId: shortText42.default("primary"), + /** Repository-relative path. */ path: boundedText(SECONDARY_BUILDER_LIMITS.maxPathChars), - /** Hash of the exact UTF-8 content below. */ + /** Hash of the complete current file bytes. */ contentHash: sha256, - content: external_exports.string().max(SECONDARY_BUILDER_LIMITS.maxSourceFileChars) + /** Hash of the exact bounded content below (whole file or section). */ + sectionHash: sha256.optional(), + content: external_exports.string().max(SECONDARY_BUILDER_LIMITS.maxSourceFileChars), + reason: external_exports.enum(CONTEXT_SELECTION_REASONS).default("EXPLICIT_ACTION_REFERENCE"), + startLine: external_exports.number().int().min(1).optional(), + endLine: external_exports.number().int().min(1).optional(), + symbols: external_exports.array(shortText42).max(12).default([]) +}).strict(); +var secondaryBuilderTargetSchema = external_exports.object({ + repositoryId: shortText42, + path: boundedText(SECONDARY_BUILDER_LIMITS.maxPathChars), + symbols: external_exports.array(shortText42).max(12).default([]), + reason: external_exports.enum(CONTEXT_SELECTION_REASONS) +}).strict(); +var secondaryDependencyContextSchema = external_exports.object({ + workUnitId: shortText42, + summary: boundedText(2e3), + changedFiles: external_exports.array( + external_exports.object({ repositoryId: shortText42, path: boundedText(SECONDARY_BUILDER_LIMITS.maxPathChars) }).strict() + ).max(60), + exportedSymbols: external_exports.array(shortText42).max(30).default([]), + verificationPassed: external_exports.literal(true) +}).strict(); +var secondaryBuilderPacketMetricsSchema = external_exports.object({ + indexedFilesConsidered: external_exports.number().int().min(0), + candidateCount: external_exports.number().int().min(0), + selectedFiles: external_exports.number().int().min(0), + selectedSections: external_exports.number().int().min(0), + sourceCharacters: external_exports.number().int().min(0), + testCharacters: external_exports.number().int().min(0), + referencePatternCount: external_exports.number().int().min(0), + dependencyContextCount: external_exports.number().int().min(0), + budgetUtilization: external_exports.number().min(0), + mandatoryRefsRetained: external_exports.number().int().min(0), + expansionDepth: external_exports.number().int().min(0).max(4), + staleEntriesEncountered: external_exports.number().int().min(0), + selectionDurationMs: external_exports.number().int().min(0), + indexReused: external_exports.boolean() +}).strict(); +var secondaryBuilderPacketQualitySchema = external_exports.object({ + explicitTargetResolved: external_exports.boolean(), + targetAmbiguity: external_exports.boolean(), + testsFound: external_exports.boolean(), + verificationHintsAvailable: external_exports.boolean(), + referencePatternFound: external_exports.boolean(), + dependencyContextComplete: external_exports.boolean(), + sourceBudgetUtilization: external_exports.number().min(0), + contextSufficient: external_exports.boolean() }).strict(); var projectedContractSchema = external_exports.object({ contractId: shortText42, @@ -57833,7 +57940,10 @@ var secondaryBuilderPacketSchema = external_exports.object({ projectionHash: sha256, contractSnapshotHash: sha256, sourceContextHash: sha256, + /** Semantic identity; excludes createdAt and observational metrics. */ + contentHash: sha256, packetHash: sha256, + createdAt: shortText42.optional(), objective: external_exports.object({ nodeId: shortText42, taskId: shortText42, @@ -57857,6 +57967,19 @@ var secondaryBuilderPacketSchema = external_exports.object({ priorWorkEvidence: external_exports.array(boundedText(2e3)).max(30) }).strict(), sourceContext: external_exports.array(secondarySourceContextSchema).max(SECONDARY_BUILDER_LIMITS.maxSourceFiles), + targets: external_exports.array(secondaryBuilderTargetSchema).max(SECONDARY_BUILDER_LIMITS.maxSourceFiles), + tests: external_exports.array(secondarySourceContextSchema).max(SECONDARY_BUILDER_LIMITS.maxTests), + referencePatterns: external_exports.array(secondarySourceContextSchema).max(SECONDARY_BUILDER_LIMITS.maxReferencePatterns), + dependencyContext: external_exports.array(secondaryDependencyContextSchema).max(SECONDARY_BUILDER_LIMITS.maxDependencyContext), + priorFailureEvidence: external_exports.array(boundedText(2e3)).max(20), + retrievalPlanRef: shortText42, + retrievalPlanRefs: external_exports.array(shortText42).max(12), + expansion: external_exports.object({ + level: external_exports.enum(CONTEXT_EXPANSION_LEVELS), + remainingLevels: external_exports.number().int().min(0).max(4) + }).strict(), + contextMetrics: secondaryBuilderPacketMetricsSchema, + quality: secondaryBuilderPacketQualitySchema, forbiddenChanges: external_exports.array(boundedText(1e3)).max(30), verificationHints: external_exports.array(boundedText(1e3)).max(30) }).strict(); @@ -57867,16 +57990,26 @@ var secondaryStructuredEditSchema = external_exports.object({ }).strict(); var secondaryBuilderResultSchema = external_exports.object({ schemaVersion: external_exports.literal(SECONDARY_BUILDER_RESULT_SCHEMA_VERSION), + status: external_exports.enum(["EDITS", "NEEDS_MORE_CONTEXT"]).default("EDITS"), summary: boundedText(SECONDARY_BUILDER_LIMITS.maxSummaryChars), edits: external_exports.array(secondaryStructuredEditSchema).max(SECONDARY_BUILDER_LIMITS.maxEdits), - notes: external_exports.array(external_exports.string().max(SECONDARY_BUILDER_LIMITS.maxNoteChars)).max(SECONDARY_BUILDER_LIMITS.maxNotes).optional() -}).strict(); + notes: external_exports.array(external_exports.string().max(SECONDARY_BUILDER_LIMITS.maxNoteChars)).max(SECONDARY_BUILDER_LIMITS.maxNotes).optional(), + needsMoreContextReasons: external_exports.array(external_exports.string().min(1).max(SECONDARY_BUILDER_LIMITS.maxNoteChars)).min(1).max(8).optional() +}).strict().superRefine((result, context) => { + if (result.status === "NEEDS_MORE_CONTEXT" && result.needsMoreContextReasons === void 0) { + context.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["needsMoreContextReasons"], message: "required when status is NEEDS_MORE_CONTEXT" }); + } + if (result.status === "NEEDS_MORE_CONTEXT" && result.edits.length > 0) { + context.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["edits"], message: "must be empty when more context is required" }); + } +}); var SECONDARY_BUILDER_JSON_SCHEMA = { type: "object", additionalProperties: false, - required: ["schemaVersion", "summary", "edits"], + required: ["schemaVersion", "status", "summary", "edits"], properties: { schemaVersion: { type: "string", const: SECONDARY_BUILDER_RESULT_SCHEMA_VERSION }, + status: { type: "string", enum: ["EDITS", "NEEDS_MORE_CONTEXT"] }, summary: { type: "string", minLength: 1, maxLength: SECONDARY_BUILDER_LIMITS.maxSummaryChars }, edits: { type: "array", @@ -57896,6 +58029,12 @@ var SECONDARY_BUILDER_JSON_SCHEMA = { type: "array", maxItems: SECONDARY_BUILDER_LIMITS.maxNotes, items: { type: "string", maxLength: SECONDARY_BUILDER_LIMITS.maxNoteChars } + }, + needsMoreContextReasons: { + type: "array", + minItems: 1, + maxItems: 8, + items: { type: "string", minLength: 1, maxLength: SECONDARY_BUILDER_LIMITS.maxNoteChars } } } }; @@ -57903,6 +58042,9 @@ var SECONDARY_BUILDER_SYSTEM_PROMPT = [ "You are a bounded SECONDARY OBJECTIVE BUILDER, not an agent harness.", "You have no shell, git, filesystem, package-manager, test, credential, or tool access.", "The packet contains all approved truth and source bytes you may use.", + "You have been given the repository context selected for this task; do not explore beyond it.", + "Do not invent files, APIs, symbols, or architecture unsupported by the packet.", + "If the packet is insufficient to implement safely, return NEEDS_MORE_CONTEXT with bounded reasons and no edits.", "Return exactly one JSON document matching the supplied schema.", "Return complete UTF-8 file contents using only CREATE or REPLACE.", "Never return Markdown, diffs, commands, deletes, renames, symlinks, or authority/config edits.", @@ -57919,6 +58061,8 @@ var SECONDARY_BUILDER_FAILURES = [ "VERIFICATION_FAILURE", "TIMEOUT", "CONTEXT_TOO_LARGE", + "INSUFFICIENT_CONTEXT", + "AMBIGUOUS_TARGET", "CANCELLED" ]; var SECONDARY_BUILDER_ATTEMPT_STATUSES = [ @@ -57927,6 +58071,7 @@ var SECONDARY_BUILDER_ATTEMPT_STATUSES = [ "PROPOSAL_VALIDATED", "EDITS_APPLIED", "VERIFICATION_FAILED", + "CONTEXT_INSUFFICIENT", "CANDIDATE_READY", "FAILED" ]; @@ -62741,6 +62886,43 @@ var OBJECTIVE_OUTPUT_JSON_SCHEMAS = { } } }; +var BUILDER_PACKET_COMPILATION_SCHEMA_VERSION = "1.0.0"; +var BUILDER_PACKET_COMPILATION_FAILURES = [ + "INSUFFICIENT_CONTEXT", + "AMBIGUOUS_TARGET", + "STALE_SOURCE_CONTEXT" +]; +var builderPacketCompilationResultSchema = external_exports.discriminatedUnion("ok", [ + external_exports.object({ + ok: external_exports.literal(true), + schemaVersion: external_exports.literal(BUILDER_PACKET_COMPILATION_SCHEMA_VERSION), + packet: secondaryBuilderPacketSchema, + plans: external_exports.array(contextSelectionPlanSchema).max(12), + planRefs: external_exports.array(external_exports.string().min(1).max(512)).max(12), + metrics: secondaryBuilderPacketMetricsSchema, + quality: secondaryBuilderPacketQualitySchema, + repositoryRoots: external_exports.record(external_exports.string().min(1)) + }).strict(), + external_exports.object({ + ok: external_exports.literal(false), + schemaVersion: external_exports.literal(BUILDER_PACKET_COMPILATION_SCHEMA_VERSION), + failure: external_exports.object({ + kind: external_exports.enum(BUILDER_PACKET_COMPILATION_FAILURES), + reasons: external_exports.array(external_exports.string().min(1).max(2e3)).min(1).max(8) + }).strict(), + plans: external_exports.array(contextSelectionPlanSchema).max(12), + planRefs: external_exports.array(external_exports.string().min(1).max(512)).max(12), + metrics: secondaryBuilderPacketMetricsSchema, + quality: secondaryBuilderPacketQualitySchema + }).strict() +]); +var defaultBudget = { + maxSelectedFiles: SECONDARY_BUILDER_LIMITS.maxSourceFiles, + maxTests: SECONDARY_BUILDER_LIMITS.maxTests, + maxReferencePatterns: SECONDARY_BUILDER_LIMITS.maxReferencePatterns, + maxSourceCharacters: SECONDARY_BUILDER_LIMITS.maxSourceBytes, + maxCharactersPerSection: SECONDARY_BUILDER_LIMITS.maxSourceFileChars +}; var QUOTA_SNAPSHOT_SCHEMA_VERSION = "1.0.0"; var isoText = external_exports.string().min(1).max(64); var ratio = external_exports.number().min(0).max(1); diff --git a/integrations/codex-plugin/specbridge/dist/checksums.json b/integrations/codex-plugin/specbridge/dist/checksums.json index 159667c..ea8d290 100644 --- a/integrations/codex-plugin/specbridge/dist/checksums.json +++ b/integrations/codex-plugin/specbridge/dist/checksums.json @@ -7,16 +7,16 @@ "bytes": 155992 }, "cli.cjs": { - "sha256": "6379044267e76482b59da2c3b57ce97fe5f758815d10a9fefa0c8267bb53e9a9", - "bytes": 5588127 + "sha256": "8bc205156d51810ec082b0e9aa3be5d2f12c307706e680796ca4909360dee2e7", + "bytes": 5634305 }, "mcp-launcher.cjs": { "sha256": "205d2730178bcdb5a06e199a42a6a6fb0977f42d706f3d445aec801a60874b3e", "bytes": 4567 }, "mcp-server.cjs": { - "sha256": "6b8a8ce0992a5cefa9e15aadbe2e0aa22a97bf341c0db8c8a599a5ba9a8024d5", - "bytes": 3644860 + "sha256": "3854958225b757a1e0a2197471faf308fbf4a97ab58c51507114e7009748aea5", + "bytes": 3654373 } } } diff --git a/integrations/codex-plugin/specbridge/dist/cli.cjs b/integrations/codex-plugin/specbridge/dist/cli.cjs index 8089068..93bd791 100644 --- a/integrations/codex-plugin/specbridge/dist/cli.cjs +++ b/integrations/codex-plugin/specbridge/dist/cli.cjs @@ -50310,8 +50310,6 @@ var import_fs34 = require("fs"); var import_path36 = __toESM(require("path"), 1); var import_fs35 = require("fs"); var import_path37 = __toESM(require("path"), 1); -var import_fs36 = require("fs"); -var import_path38 = __toESM(require("path"), 1); // ../../packages/context/dist/index.js var import_crypto10 = require("crypto"); @@ -50402,6 +50400,8 @@ var CONTEXT_SELECTION_REASONS = [ "DEPENDENCY_PROXIMITY", /** In the same module/package as an already-selected file. */ "MODULE_PROXIMITY", + /** A bounded nearby implementation with the same structural convention. */ + "REFERENCE_PATTERN", /** A declared symbol matched a query symbol. */ "SYMBOL_MATCH", /** Filename/path tokens overlapped the query tokens. */ @@ -51963,6 +51963,7 @@ var DEFAULT_RANKING_WEIGHTS = Object.freeze({ testSourcePair: 180, dependencyProximity: 120, moduleProximity: 40, + referencePattern: 150, tokenOverlapPerToken: 12, tokenOverlapCap: 96, priorTaskRelevance: 60, @@ -52100,6 +52101,31 @@ function rankCandidates(index, query, options = {}) { for (const dependent of index.dependentsOf(anchor)) { add2(dependent, "DEPENDENCY_PROXIMITY", weights.dependencyProximity, level2, anchor); } + const anchorEntry = index.get(anchor); + if (anchorEntry !== void 0 && anchorEntry.kind === "source") { + const suffixes = structuralSuffixes(anchorEntry.path, anchorEntry.symbols); + for (const sibling of index.siblingsIn(anchorEntry.module)) { + if (sibling === anchor) continue; + const entry2 = index.get(sibling); + if (entry2 === void 0 || entry2.kind !== "source" || entry2.language !== anchorEntry.language) { + continue; + } + const sharedImportCount = entry2.imports.filter( + (specifier) => anchorEntry.imports.includes(specifier) + ).length; + const sameConvention = structuralSuffixes(entry2.path, entry2.symbols).some( + (suffix) => suffixes.includes(suffix) + ); + if (!sameConvention && sharedImportCount < 2) continue; + add2( + sibling, + "REFERENCE_PATTERN", + weights.referencePattern + Math.min(30, sharedImportCount * 10), + level2, + sameConvention ? anchor : `${anchor} (${sharedImportCount} shared imports)` + ); + } + } } const anchorModules = new Set(anchors.map((anchor) => index.get(anchor)?.module ?? "")); for (const module2 of anchorModules) { @@ -52134,6 +52160,16 @@ function rankCandidates(index, query, options = {}) { ); return ranked.slice(0, maxCandidates); } +function structuralSuffixes(relativePath, symbols) { + const base = relativePath.split("/").at(-1)?.replace(/\.[^.]+$/, "") ?? ""; + const values = [base, ...symbols]; + const suffixes = /* @__PURE__ */ new Set(); + for (const value of values) { + const match = value.match(/([A-Z][a-z0-9]{2,})$/); + if (match?.[1] !== void 0) suffixes.add(match[1].toLowerCase()); + } + return [...suffixes]; +} var DEFAULT_SECTION_OPTIONS = { wholeFileUnderChars: 6e3, targetSectionChars: 4e3, @@ -52654,7 +52690,8 @@ function deduplicateItems(items, options = {}) { } const digest2 = contentDigest(item); const identity3 = sourceIdentity(item); - const contentIndex = byContent.get(digest2); + const contentKey = identity3?.startsWith("key:repo:") === true ? `${digest2}:${identity3}` : digest2; + const contentIndex = byContent.get(contentKey); if (contentIndex !== void 0) { const incumbent = survivors[contentIndex]; if (incumbent !== void 0) { @@ -52662,7 +52699,7 @@ function deduplicateItems(items, options = {}) { drop(incumbent, item, "identical-content"); survivors[contentIndex] = void 0; survivors.push(item); - byContent.set(digest2, survivors.length - 1); + byContent.set(contentKey, survivors.length - 1); if (identity3 !== void 0) bySource.set(identity3, survivors.length - 1); } else { drop(item, incumbent, "identical-content"); @@ -52681,7 +52718,7 @@ function deduplicateItems(items, options = {}) { survivors[sourceIndex] = void 0; survivors.push(item); bySource.set(identity3, survivors.length - 1); - byContent.set(digest2, survivors.length - 1); + byContent.set(contentKey, survivors.length - 1); } else { drop(item, incumbent, kind); } @@ -52689,7 +52726,7 @@ function deduplicateItems(items, options = {}) { } bySource.set(identity3, survivors.length); } - byContent.set(digest2, survivors.length); + byContent.set(contentKey, survivors.length); survivors.push(item); } const kept = survivors.filter((item) => item !== void 0); @@ -53033,9 +53070,11 @@ function initialExpansionState(input) { function beginAttemptExpansion(state, now7) { return { ...state, expansionsThisAttempt: 0, updatedAt: now7 }; } -var CONTEXT_SELECTION_PLAN_SCHEMA_VERSION = "1.0.0"; +var CONTEXT_SELECTION_PLAN_SCHEMA_VERSION = "1.1.0"; var shortText3 = external_exports.string().min(1).max(CONTEXT_LIMITS.maxShortTextChars); var selectedContextItemSchema = external_exports.object({ + /** Repository identity when a plan participates in a multi-repo packet. */ + repositoryId: shortText3.optional(), /** Workspace-relative path, forward slashes. */ path: shortText3, /** Content hash the selection was made against. Verified before use. */ @@ -53054,6 +53093,7 @@ var selectedContextItemSchema = external_exports.object({ detail: shortText3.optional() }).passthrough(); var contextPointerSchema = external_exports.object({ + repositoryId: shortText3.optional(), path: shortText3, reason: external_exports.enum(CONTEXT_SELECTION_REASONS), /** @@ -53071,6 +53111,7 @@ var contextPointerSchema = external_exports.object({ detail: shortText3.optional() }).passthrough(); var excludedCandidateSchema = external_exports.object({ + repositoryId: shortText3.optional(), path: shortText3, reason: external_exports.enum(CONTEXT_EXCLUSION_REASONS), score: external_exports.number().int(), @@ -53083,6 +53124,8 @@ var contextSelectionPlanSchema = external_exports.object({ taskId: shortText3, nodeId: shortText3.optional(), attemptId: shortText3.optional(), + /** Repository identity; paths are unique only within this namespace. */ + repositoryId: shortText3.optional(), strategy: external_exports.enum(CONTEXT_STRATEGIES), shape: external_exports.enum(CONTEXT_SHAPES), role: external_exports.enum(RETRIEVAL_ROLES), @@ -53129,6 +53172,17 @@ var contextSelectionPlanSchema = external_exports.object({ }).passthrough(), /** Stable identities of the reusable prompt components (observability). */ componentHashes: external_exports.record(external_exports.string().nullable()).default({}), + /** Phase 5 facts; metrics are evidence for later policy, never eligibility here. */ + builderPacket: external_exports.object({ + contextSufficient: external_exports.boolean(), + explicitTargetResolved: external_exports.boolean(), + targetAmbiguity: external_exports.boolean(), + testsFound: external_exports.boolean(), + verificationHintsAvailable: external_exports.boolean(), + referencePatternFound: external_exports.boolean(), + dependencyContextComplete: external_exports.boolean(), + sourceBudgetUtilization: external_exports.number().min(0) + }).passthrough().optional(), createdAt: shortText3 }).passthrough(); function truncate(value, max = CONTEXT_LIMITS.maxShortTextChars) { @@ -53156,6 +53210,10 @@ function selectWorkingSet(input) { }); continue; } + if (input.shape === "MATERIALIZED" && !candidate.mandatory && selected.length >= maxSelected) { + excluded.push({ path: candidate.path, reason: "RANKED_BELOW_CUTOFF", score: candidate.score }); + continue; + } const resolved2 = resolveFresh(input.rootDir, candidate.entry, { withContent: true }); if (resolved2.status === "missing") { excluded.push({ @@ -53952,6 +54010,8 @@ async function buildEfficientContext(input) { } // ../../packages/orchestration/dist/index.js +var import_fs36 = require("fs"); +var import_path38 = __toESM(require("path"), 1); var import_crypto20 = require("crypto"); var import_fs37 = require("fs"); var import_path39 = __toESM(require("path"), 1); @@ -62595,12 +62655,15 @@ ${command.stderrTail}`).join("\n"); escalated: false }; } -var SECONDARY_BUILDER_PACKET_SCHEMA_VERSION = "1.0.0"; -var SECONDARY_BUILDER_RESULT_SCHEMA_VERSION = "1.0.0"; -var SECONDARY_BUILDER_ATTEMPT_SCHEMA_VERSION = "1.0.0"; +var SECONDARY_BUILDER_PACKET_SCHEMA_VERSION = "1.1.0"; +var SECONDARY_BUILDER_RESULT_SCHEMA_VERSION = "1.1.0"; +var SECONDARY_BUILDER_ATTEMPT_SCHEMA_VERSION = "1.1.0"; var SECONDARY_BUILDER_LIMITS = { ...LOCAL_EXECUTION_LIMITS, maxSourceFiles: 16, + maxTests: 6, + maxReferencePatterns: 4, + maxDependencyContext: 12, maxSourceFileChars: 32768, maxSourceBytes: 262144, maxPacketCharacters: 524288, @@ -62611,11 +62674,60 @@ var boundedText = (max) => external_exports.string().min(1).max(max); var shortText42 = boundedText(512); var sha256 = external_exports.string().regex(/^[a-f0-9]{64}$/); var secondarySourceContextSchema = external_exports.object({ - /** Worktree-relative path. Whole-file source only in Phase 4. */ + /** Repository namespace. Paths are unique only within this identity. */ + repositoryId: shortText42.default("primary"), + /** Repository-relative path. */ path: boundedText(SECONDARY_BUILDER_LIMITS.maxPathChars), - /** Hash of the exact UTF-8 content below. */ + /** Hash of the complete current file bytes. */ contentHash: sha256, - content: external_exports.string().max(SECONDARY_BUILDER_LIMITS.maxSourceFileChars) + /** Hash of the exact bounded content below (whole file or section). */ + sectionHash: sha256.optional(), + content: external_exports.string().max(SECONDARY_BUILDER_LIMITS.maxSourceFileChars), + reason: external_exports.enum(CONTEXT_SELECTION_REASONS).default("EXPLICIT_ACTION_REFERENCE"), + startLine: external_exports.number().int().min(1).optional(), + endLine: external_exports.number().int().min(1).optional(), + symbols: external_exports.array(shortText42).max(12).default([]) +}).strict(); +var secondaryBuilderTargetSchema = external_exports.object({ + repositoryId: shortText42, + path: boundedText(SECONDARY_BUILDER_LIMITS.maxPathChars), + symbols: external_exports.array(shortText42).max(12).default([]), + reason: external_exports.enum(CONTEXT_SELECTION_REASONS) +}).strict(); +var secondaryDependencyContextSchema = external_exports.object({ + workUnitId: shortText42, + summary: boundedText(2e3), + changedFiles: external_exports.array( + external_exports.object({ repositoryId: shortText42, path: boundedText(SECONDARY_BUILDER_LIMITS.maxPathChars) }).strict() + ).max(60), + exportedSymbols: external_exports.array(shortText42).max(30).default([]), + verificationPassed: external_exports.literal(true) +}).strict(); +var secondaryBuilderPacketMetricsSchema = external_exports.object({ + indexedFilesConsidered: external_exports.number().int().min(0), + candidateCount: external_exports.number().int().min(0), + selectedFiles: external_exports.number().int().min(0), + selectedSections: external_exports.number().int().min(0), + sourceCharacters: external_exports.number().int().min(0), + testCharacters: external_exports.number().int().min(0), + referencePatternCount: external_exports.number().int().min(0), + dependencyContextCount: external_exports.number().int().min(0), + budgetUtilization: external_exports.number().min(0), + mandatoryRefsRetained: external_exports.number().int().min(0), + expansionDepth: external_exports.number().int().min(0).max(4), + staleEntriesEncountered: external_exports.number().int().min(0), + selectionDurationMs: external_exports.number().int().min(0), + indexReused: external_exports.boolean() +}).strict(); +var secondaryBuilderPacketQualitySchema = external_exports.object({ + explicitTargetResolved: external_exports.boolean(), + targetAmbiguity: external_exports.boolean(), + testsFound: external_exports.boolean(), + verificationHintsAvailable: external_exports.boolean(), + referencePatternFound: external_exports.boolean(), + dependencyContextComplete: external_exports.boolean(), + sourceBudgetUtilization: external_exports.number().min(0), + contextSufficient: external_exports.boolean() }).strict(); var projectedContractSchema = external_exports.object({ contractId: shortText42, @@ -62631,7 +62743,10 @@ var secondaryBuilderPacketSchema = external_exports.object({ projectionHash: sha256, contractSnapshotHash: sha256, sourceContextHash: sha256, + /** Semantic identity; excludes createdAt and observational metrics. */ + contentHash: sha256, packetHash: sha256, + createdAt: shortText42.optional(), objective: external_exports.object({ nodeId: shortText42, taskId: shortText42, @@ -62655,6 +62770,19 @@ var secondaryBuilderPacketSchema = external_exports.object({ priorWorkEvidence: external_exports.array(boundedText(2e3)).max(30) }).strict(), sourceContext: external_exports.array(secondarySourceContextSchema).max(SECONDARY_BUILDER_LIMITS.maxSourceFiles), + targets: external_exports.array(secondaryBuilderTargetSchema).max(SECONDARY_BUILDER_LIMITS.maxSourceFiles), + tests: external_exports.array(secondarySourceContextSchema).max(SECONDARY_BUILDER_LIMITS.maxTests), + referencePatterns: external_exports.array(secondarySourceContextSchema).max(SECONDARY_BUILDER_LIMITS.maxReferencePatterns), + dependencyContext: external_exports.array(secondaryDependencyContextSchema).max(SECONDARY_BUILDER_LIMITS.maxDependencyContext), + priorFailureEvidence: external_exports.array(boundedText(2e3)).max(20), + retrievalPlanRef: shortText42, + retrievalPlanRefs: external_exports.array(shortText42).max(12), + expansion: external_exports.object({ + level: external_exports.enum(CONTEXT_EXPANSION_LEVELS), + remainingLevels: external_exports.number().int().min(0).max(4) + }).strict(), + contextMetrics: secondaryBuilderPacketMetricsSchema, + quality: secondaryBuilderPacketQualitySchema, forbiddenChanges: external_exports.array(boundedText(1e3)).max(30), verificationHints: external_exports.array(boundedText(1e3)).max(30) }).strict(); @@ -62665,16 +62793,26 @@ var secondaryStructuredEditSchema = external_exports.object({ }).strict(); var secondaryBuilderResultSchema = external_exports.object({ schemaVersion: external_exports.literal(SECONDARY_BUILDER_RESULT_SCHEMA_VERSION), + status: external_exports.enum(["EDITS", "NEEDS_MORE_CONTEXT"]).default("EDITS"), summary: boundedText(SECONDARY_BUILDER_LIMITS.maxSummaryChars), edits: external_exports.array(secondaryStructuredEditSchema).max(SECONDARY_BUILDER_LIMITS.maxEdits), - notes: external_exports.array(external_exports.string().max(SECONDARY_BUILDER_LIMITS.maxNoteChars)).max(SECONDARY_BUILDER_LIMITS.maxNotes).optional() -}).strict(); + notes: external_exports.array(external_exports.string().max(SECONDARY_BUILDER_LIMITS.maxNoteChars)).max(SECONDARY_BUILDER_LIMITS.maxNotes).optional(), + needsMoreContextReasons: external_exports.array(external_exports.string().min(1).max(SECONDARY_BUILDER_LIMITS.maxNoteChars)).min(1).max(8).optional() +}).strict().superRefine((result, context) => { + if (result.status === "NEEDS_MORE_CONTEXT" && result.needsMoreContextReasons === void 0) { + context.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["needsMoreContextReasons"], message: "required when status is NEEDS_MORE_CONTEXT" }); + } + if (result.status === "NEEDS_MORE_CONTEXT" && result.edits.length > 0) { + context.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["edits"], message: "must be empty when more context is required" }); + } +}); var SECONDARY_BUILDER_JSON_SCHEMA = { type: "object", additionalProperties: false, - required: ["schemaVersion", "summary", "edits"], + required: ["schemaVersion", "status", "summary", "edits"], properties: { schemaVersion: { type: "string", const: SECONDARY_BUILDER_RESULT_SCHEMA_VERSION }, + status: { type: "string", enum: ["EDITS", "NEEDS_MORE_CONTEXT"] }, summary: { type: "string", minLength: 1, maxLength: SECONDARY_BUILDER_LIMITS.maxSummaryChars }, edits: { type: "array", @@ -62694,6 +62832,12 @@ var SECONDARY_BUILDER_JSON_SCHEMA = { type: "array", maxItems: SECONDARY_BUILDER_LIMITS.maxNotes, items: { type: "string", maxLength: SECONDARY_BUILDER_LIMITS.maxNoteChars } + }, + needsMoreContextReasons: { + type: "array", + minItems: 1, + maxItems: 8, + items: { type: "string", minLength: 1, maxLength: SECONDARY_BUILDER_LIMITS.maxNoteChars } } } }; @@ -62701,6 +62845,9 @@ var SECONDARY_BUILDER_SYSTEM_PROMPT = [ "You are a bounded SECONDARY OBJECTIVE BUILDER, not an agent harness.", "You have no shell, git, filesystem, package-manager, test, credential, or tool access.", "The packet contains all approved truth and source bytes you may use.", + "You have been given the repository context selected for this task; do not explore beyond it.", + "Do not invent files, APIs, symbols, or architecture unsupported by the packet.", + "If the packet is insufficient to implement safely, return NEEDS_MORE_CONTEXT with bounded reasons and no edits.", "Return exactly one JSON document matching the supplied schema.", "Return complete UTF-8 file contents using only CREATE or REPLACE.", "Never return Markdown, diffs, commands, deletes, renames, symlinks, or authority/config edits.", @@ -62764,6 +62911,8 @@ var SECONDARY_BUILDER_FAILURES = [ "VERIFICATION_FAILURE", "TIMEOUT", "CONTEXT_TOO_LARGE", + "INSUFFICIENT_CONTEXT", + "AMBIGUOUS_TARGET", "CANCELLED" ]; var SECONDARY_BUILDER_ATTEMPT_STATUSES = [ @@ -62772,6 +62921,7 @@ var SECONDARY_BUILDER_ATTEMPT_STATUSES = [ "PROPOSAL_VALIDATED", "EDITS_APPLIED", "VERIFICATION_FAILED", + "CONTEXT_INSUFFICIENT", "CANDIDATE_READY", "FAILED" ]; @@ -62833,18 +62983,30 @@ function stableStringify(value) { return JSON.stringify(stable(value)); } function packetBody(packet) { - return stableStringify(packet); + const { createdAt: _createdAt, contextMetrics: _contextMetrics, ...semantic } = packet; + return stableStringify(semantic); } function sourceContextHashOf(sourceContext) { + const normalized = external_exports.array(secondarySourceContextSchema).parse(sourceContext); return sha256Hex( stableStringify( - [...sourceContext].map((entry2) => ({ path: entry2.path.replace(/\\/g, "/"), contentHash: entry2.contentHash })).sort((left, right) => left.path.localeCompare(right.path)) + normalized.map((entry2) => ({ + repositoryId: entry2.repositoryId, + path: entry2.path.replace(/\\/g, "/"), + contentHash: entry2.contentHash, + sectionHash: entry2.sectionHash ?? sha256Hex(entry2.content), + startLine: entry2.startLine ?? null, + endLine: entry2.endLine ?? null + })).sort((left, right) => `${left.repositoryId}:${left.path}`.localeCompare(`${right.repositoryId}:${right.path}`)) ) ); } function buildSecondaryBuilderPacket(input) { const sourceContext = external_exports.array(secondarySourceContextSchema).parse(input.sourceContext); - const sourceBytes = sourceContext.reduce( + const tests = external_exports.array(secondarySourceContextSchema).parse(input.tests ?? []); + const referencePatterns = external_exports.array(secondarySourceContextSchema).parse(input.referencePatterns ?? []); + const allSourceContext = [...sourceContext, ...tests, ...referencePatterns]; + const sourceBytes = allSourceContext.reduce( (total, entry2) => total + Buffer.byteLength(entry2.content, "utf8"), 0 ); @@ -62856,7 +63018,7 @@ function buildSecondaryBuilderPacket(input) { packetId: `${input.projection.projectionId}-secondary`, projectionHash: input.projection.contentHash, contractSnapshotHash: input.projection.contractSnapshotHash, - sourceContextHash: sourceContextHashOf(sourceContext), + sourceContextHash: sourceContextHashOf(allSourceContext), objective: { nodeId: input.projection.objectiveNodeId, taskId: input.projection.objective.taskId, @@ -62880,47 +63042,114 @@ function buildSecondaryBuilderPacket(input) { priorWorkEvidence: [...input.projection.workEvidence] }, sourceContext, + targets: external_exports.array(secondaryBuilderTargetSchema).parse( + input.targets ?? sourceContext.map((entry2) => ({ + repositoryId: entry2.repositoryId, + path: entry2.path, + symbols: entry2.symbols, + reason: entry2.reason + })) + ), + tests, + referencePatterns, + dependencyContext: external_exports.array(secondaryDependencyContextSchema).parse(input.dependencyContext ?? []), + priorFailureEvidence: [...input.priorFailureEvidence ?? []].slice(0, 20), + retrievalPlanRef: input.retrievalPlanRefs?.[0] ?? "manual-source-selection", + retrievalPlanRefs: [...input.retrievalPlanRefs ?? ["manual-source-selection"]].slice(0, 12), + expansion: { + level: input.expansionLevel ?? "MINIMAL_BOOTSTRAP", + remainingLevels: Math.max(0, 4 - CONTEXT_EXPANSION_LEVELS.indexOf(input.expansionLevel ?? "MINIMAL_BOOTSTRAP")) + }, + contextMetrics: secondaryBuilderPacketMetricsSchema.parse(input.contextMetrics ?? { + indexedFilesConsidered: sourceContext.length, + candidateCount: sourceContext.length, + selectedFiles: allSourceContext.length, + selectedSections: allSourceContext.filter((entry2) => entry2.startLine !== void 0).length, + sourceCharacters: sourceContext.reduce((sum, entry2) => sum + entry2.content.length, 0), + testCharacters: tests.reduce((sum, entry2) => sum + entry2.content.length, 0), + referencePatternCount: referencePatterns.length, + dependencyContextCount: input.dependencyContext?.length ?? 0, + budgetUtilization: sourceBytes / SECONDARY_BUILDER_LIMITS.maxSourceBytes, + mandatoryRefsRetained: sourceContext.length, + expansionDepth: CONTEXT_EXPANSION_LEVELS.indexOf(input.expansionLevel ?? "MINIMAL_BOOTSTRAP"), + staleEntriesEncountered: 0, + selectionDurationMs: 0, + indexReused: false + }), + quality: secondaryBuilderPacketQualitySchema.parse(input.quality ?? { + explicitTargetResolved: sourceContext.length > 0, + targetAmbiguity: false, + testsFound: tests.length > 0, + verificationHintsAvailable: (input.verificationHints?.length ?? 0) > 0, + referencePatternFound: referencePatterns.length > 0, + dependencyContextComplete: true, + sourceBudgetUtilization: sourceBytes / SECONDARY_BUILDER_LIMITS.maxSourceBytes, + contextSufficient: sourceContext.length > 0 + }), forbiddenChanges: [ "Do not modify .git, .kiro, .specbridge, .codex, .claude, credentials, approvals, contracts, mission state, or closure state.", "Do not delete, rename, chmod, create symlinks, emit commands, or request tools.", ...input.forbiddenChanges ?? [] ], - verificationHints: [...input.verificationHints ?? []] + verificationHints: [...input.verificationHints ?? []], + ...input.createdAt !== void 0 ? { createdAt: input.createdAt } : {} }; - const packetHash = sha256Hex(packetBody(base)); - const packet = secondaryBuilderPacketSchema.parse({ ...base, packetHash }); + const contentHash = sha256Hex(packetBody(base)); + const packet = secondaryBuilderPacketSchema.parse({ ...base, contentHash, packetHash: contentHash }); if (JSON.stringify(packet).length > SECONDARY_BUILDER_LIMITS.maxPacketCharacters) { throw new Error(`secondary builder packet exceeds ${SECONDARY_BUILDER_LIMITS.maxPacketCharacters} characters`); } return packet; } function validatePacketIdentity(packet) { - const { packetHash: claimed, ...body } = packet; + const { packetHash: claimed, contentHash, ...body } = packet; const actual = sha256Hex(packetBody(body)); - if (actual !== claimed) return "the packet hash does not match its contents"; - if (sourceContextHashOf(packet.sourceContext) !== packet.sourceContextHash) { + if (actual !== claimed || actual !== contentHash) return "the packet semantic hash does not match its contents"; + if (sourceContextHashOf([...packet.sourceContext, ...packet.tests, ...packet.referencePatterns]) !== packet.sourceContextHash) { return "the source-context manifest hash does not match the packet"; } return void 0; } -function validateSourceFreshness(worktreeRoot, packet, protectedPaths) { +function validateSourceFreshness(worktreeRoot, packet, protectedPaths, repositoryRoots) { const problems = []; - const pathFailures = validateEditPaths( - { rootDir: worktreeRoot }, - packet.sourceContext.map((entry2) => ({ path: entry2.path, content: "" })), - protectedPaths - ); - problems.push(...pathFailures.map((failure3) => `${failure3.path}: ${failure3.problem}`)); - if (pathFailures.length > 0) return problems; - for (const source of packet.sourceContext) { - const target = import_path37.default.join(worktreeRoot, source.path.replace(/\\/g, "/")); + const allSources = [...packet.sourceContext, ...packet.tests, ...packet.referencePatterns]; + for (const repositoryId of new Set(allSources.map((source) => source.repositoryId))) { + const repositoryRoot = repositoryRoots[repositoryId] ?? (repositoryId === "primary" ? worktreeRoot : void 0); + if (repositoryRoot === void 0) { + problems.push(`${repositoryId}: repository root is unavailable`); + continue; + } + const pathFailures = validateEditPaths( + { rootDir: repositoryRoot }, + allSources.filter((entry2) => entry2.repositoryId === repositoryId).map((entry2) => ({ path: entry2.path, content: "" })), + protectedPaths + ); + problems.push(...pathFailures.map((failure3) => `${repositoryId}:${failure3.path}: ${failure3.problem}`)); + } + if (problems.length > 0) return problems; + for (const source of allSources) { + const repositoryRoot = repositoryRoots[source.repositoryId] ?? (source.repositoryId === "primary" ? worktreeRoot : void 0); + if (repositoryRoot === void 0) { + problems.push(`${source.repositoryId}:${source.path}: repository root is unavailable`); + continue; + } + const normalizedPath = source.path.replace(/\\/g, "/"); + const resolvedRoot = import_path37.default.resolve(repositoryRoot); + const target = import_path37.default.resolve(resolvedRoot, normalizedPath); + if (import_path37.default.isAbsolute(normalizedPath) || normalizedPath === ".." || normalizedPath.startsWith("../") || target !== resolvedRoot && !target.startsWith(`${resolvedRoot}${import_path37.default.sep}`)) { + problems.push(`${source.repositoryId}:${source.path}: source path escapes its repository`); + continue; + } try { if (!(0, import_fs35.lstatSync)(target).isFile()) { problems.push(`${source.path}: source is not a regular file`); continue; } const current = (0, import_fs35.readFileSync)(target, "utf8"); - if (sha256Hex(current) !== source.contentHash || current !== source.content) { + if (sha256Hex(source.content) !== (source.sectionHash ?? sha256Hex(source.content))) { + problems.push(`${source.repositoryId}:${source.path}: selected section hash does not match packet content`); + } + if (sha256Hex(current) !== source.contentHash || source.startLine === void 0 && current !== source.content) { problems.push(`${source.path}: repository bytes changed after source context was assembled`); } } catch { @@ -62933,7 +63162,7 @@ function emptyTelemetry(packet) { return { inputCharacters: 0, outputBytes: 0, - sourceFiles: packet.sourceContext.length, + sourceFiles: packet.sourceContext.length + packet.tests.length + packet.referencePatterns.length, editedFiles: 0, durationMs: 0, inputTokens: null, @@ -62958,13 +63187,14 @@ async function executeSecondaryObjectiveBuilder(input) { const packet = parsedPacket.data; const identityProblem = validatePacketIdentity(packet); if (identityProblem !== void 0) return failure(packet, "STALE_SOURCE_CONTEXT", identityProblem); - const stale = validateSourceFreshness(input.worktreeRoot, packet, input.protectedPaths ?? []); + const stale = validateSourceFreshness( + input.worktreeRoot, + packet, + input.protectedPaths ?? [], + input.repositoryRoots ?? {} + ); if (stale.length > 0) return failure(packet, "STALE_SOURCE_CONTEXT", stale.slice(0, 5).join("; ")); - const userPrompt = [ - "Implement the approved WorkUnit using only the bounded packet below.", - "Return the SECONDARY_BUILDER_RESULT JSON document now.", - stableStringify(packet) - ].join("\n\n"); + const userPrompt = renderSecondaryBuilderPrompt(packet); const inputCharacters = SECONDARY_BUILDER_SYSTEM_PROMPT.length + userPrompt.length; if (inputCharacters > input.maximumInputCharacters) { return failure( @@ -63003,7 +63233,7 @@ async function executeSecondaryObjectiveBuilder(input) { let telemetry = { inputCharacters, outputBytes, - sourceFiles: packet.sourceContext.length, + sourceFiles: packet.sourceContext.length + packet.tests.length + packet.referencePatterns.length, editedFiles: 0, durationMs: inferred.durationMs, inputTokens: inferred.usage?.inputTokens ?? null, @@ -63035,6 +63265,14 @@ async function executeSecondaryObjectiveBuilder(input) { const proposal = parsed.data; telemetry = { ...telemetry, editedFiles: proposal.edits.length }; input.onExecutionEvent?.({ stage: "PROPOSAL_VALIDATED", proposal, telemetry }); + if (proposal.status === "NEEDS_MORE_CONTEXT") { + return failure( + packet, + "INSUFFICIENT_CONTEXT", + `secondary builder requested more context: ${proposal.needsMoreContextReasons?.join("; ") ?? proposal.summary}`, + { rawOutput: inferred.text, proposal, telemetry } + ); + } if (proposal.edits.length === 0) { return failure(packet, "EMPTY_EDIT_SET", "an implementation WorkUnit must propose at least one edit", { rawOutput: inferred.text, @@ -63098,6 +63336,44 @@ async function executeSecondaryObjectiveBuilder(input) { input.onExecutionEvent?.({ stage: "EDITS_APPLIED", proposal, appliedFiles, telemetry }); return { ok: true, proposal, appliedFiles, telemetry }; } +function renderSecondaryBuilderPrompt(packet) { + const renderSources = (entries) => entries.length === 0 ? "(none)" : entries.map( + (entry2) => [ + `--- ${entry2.repositoryId}:${entry2.path}${entry2.startLine !== void 0 ? ` lines ${entry2.startLine}-${entry2.endLine}` : ""} ---`, + `Selected because: ${entry2.reason}`, + entry2.content + ].join("\n") + ).join("\n\n"); + return [ + "PACKET IDENTITY", + stableStringify({ workUnitId: packet.workUnit.workUnitId, packetHash: packet.packetHash }), + "TASK", + `${packet.workUnit.title} +${packet.workUnit.goal}`, + "REQUIRED BEHAVIOR", + [...packet.objective.acceptance, ...packet.approvedContext.contracts.flatMap((contract) => contract.requirements)].join("\n- "), + "TARGETS", + packet.targets.map((target) => `${target.repositoryId}:${target.path} (${target.reason})`).join("\n"), + "CURRENT SOURCE", + renderSources(packet.sourceContext), + "RELEVANT TESTS", + renderSources(packet.tests), + "REFERENCE PATTERNS (examples only; not requirements)", + renderSources(packet.referencePatterns), + "DEPENDENCY CONTEXT", + packet.dependencyContext.map((dependency) => `${dependency.workUnitId}: ${dependency.summary}`).join("\n") || "(none)", + "CONSTRAINTS", + packet.approvedContext.constraints.join("\n"), + "DO NOT CHANGE", + packet.forbiddenChanges.join("\n"), + "PRIOR FAILURE EVIDENCE", + packet.priorFailureEvidence.join("\n") || "(none)", + "VERIFICATION HINTS", + packet.verificationHints.join("\n") || "(none configured)", + "OUTPUT CONTRACT", + "Return exactly one SECONDARY_BUILDER_RESULT JSON document. Use status EDITS with complete CREATE/REPLACE contents, or NEEDS_MORE_CONTEXT with no edits when the supplied evidence is insufficient." + ].join("\n\n"); +} function secondaryBuilderInputCeiling(config2) { return effectiveLocalInputCharacters(config2.localInference); } @@ -74719,6 +74995,666 @@ async function pruneWorktrees(workspace, jobId) { await git3(workspace.rootDir, ["worktree", "prune"]); return removed; } +var BUILDER_PACKET_COMPILATION_SCHEMA_VERSION = "1.0.0"; +var BUILDER_PACKET_COMPILATION_FAILURES = [ + "INSUFFICIENT_CONTEXT", + "AMBIGUOUS_TARGET", + "STALE_SOURCE_CONTEXT" +]; +var builderPacketCompilationResultSchema = external_exports.discriminatedUnion("ok", [ + external_exports.object({ + ok: external_exports.literal(true), + schemaVersion: external_exports.literal(BUILDER_PACKET_COMPILATION_SCHEMA_VERSION), + packet: secondaryBuilderPacketSchema, + plans: external_exports.array(contextSelectionPlanSchema).max(12), + planRefs: external_exports.array(external_exports.string().min(1).max(512)).max(12), + metrics: secondaryBuilderPacketMetricsSchema, + quality: secondaryBuilderPacketQualitySchema, + repositoryRoots: external_exports.record(external_exports.string().min(1)) + }).strict(), + external_exports.object({ + ok: external_exports.literal(false), + schemaVersion: external_exports.literal(BUILDER_PACKET_COMPILATION_SCHEMA_VERSION), + failure: external_exports.object({ + kind: external_exports.enum(BUILDER_PACKET_COMPILATION_FAILURES), + reasons: external_exports.array(external_exports.string().min(1).max(2e3)).min(1).max(8) + }).strict(), + plans: external_exports.array(contextSelectionPlanSchema).max(12), + planRefs: external_exports.array(external_exports.string().min(1).max(512)).max(12), + metrics: secondaryBuilderPacketMetricsSchema, + quality: secondaryBuilderPacketQualitySchema + }).strict() +]); +var SecondaryBuilderContextCompiler = class { + compile(input) { + return compileSecondaryBuilderPacket(input); + } +}; +var defaultBudget = { + maxSelectedFiles: SECONDARY_BUILDER_LIMITS.maxSourceFiles, + maxTests: SECONDARY_BUILDER_LIMITS.maxTests, + maxReferencePatterns: SECONDARY_BUILDER_LIMITS.maxReferencePatterns, + maxSourceCharacters: SECONDARY_BUILDER_LIMITS.maxSourceBytes, + maxCharactersPerSection: SECONDARY_BUILDER_LIMITS.maxSourceFileChars +}; +function boundedBudget(input) { + const supplied = input.budget ?? {}; + return { + maxSelectedFiles: Math.max( + 1, + Math.min(SECONDARY_BUILDER_LIMITS.maxSourceFiles, supplied.maxSelectedFiles ?? defaultBudget.maxSelectedFiles) + ), + maxTests: Math.max(0, Math.min(SECONDARY_BUILDER_LIMITS.maxTests, supplied.maxTests ?? defaultBudget.maxTests)), + maxReferencePatterns: Math.max( + 0, + Math.min( + SECONDARY_BUILDER_LIMITS.maxReferencePatterns, + supplied.maxReferencePatterns ?? defaultBudget.maxReferencePatterns + ) + ), + maxSourceCharacters: Math.max( + 1e3, + Math.min(SECONDARY_BUILDER_LIMITS.maxSourceBytes, supplied.maxSourceCharacters ?? defaultBudget.maxSourceCharacters) + ), + maxCharactersPerSection: Math.max( + 500, + Math.min( + SECONDARY_BUILDER_LIMITS.maxSourceFileChars, + supplied.maxCharactersPerSection ?? defaultBudget.maxCharactersPerSection + ) + ) + }; +} +function normalizePath22(value) { + return value.replace(/\\/g, "/").replace(/^\.\//, "").trim(); +} +function unique(values) { + return [...new Set(values.filter((value) => value.length > 0))]; +} +function safeId(value) { + return value.replace(/[^A-Za-z0-9._-]/g, "-").slice(0, 80) || "repository"; +} +function nowIso2(input) { + return input.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(); +} +async function prepareRepositories(input, createdAt) { + if (input.repositories !== void 0) { + const seen = /* @__PURE__ */ new Set(); + return input.repositories.map((repository, position) => { + if (seen.has(repository.repositoryId)) { + throw new Error(`duplicate repository identity ${repository.repositoryId}`); + } + seen.add(repository.repositoryId); + if (position > 0 && repository.repositoryId !== "primary" && (repository.justification === void 0 || repository.justification.trim() === "")) { + throw new Error(`secondary repository ${repository.repositoryId} needs an explicit justification`); + } + return { + repositoryId: repository.repositoryId, + rootDir: repository.rootDir, + index: repository.index, + changedPaths: unique((repository.changedPaths ?? []).map(normalizePath22)), + indexReused: repository.indexReused ?? true, + staleMetadataPaths: [] + }; + }); + } + const canonicalSnapshot = await captureGitSnapshot(input.workspace.rootDir); + const base = ensureRepositoryIndex({ + workspace: input.workspace, + config: input.config, + now: createdAt, + gitSnapshot: canonicalSnapshot + }); + const worktreeSnapshot = await captureGitSnapshot(input.worktreeRoot); + const dependencyPaths = (input.dependencyContext ?? []).flatMap( + (dependency) => dependency.changedFiles.filter((file) => (file.repositoryId ?? "primary") === "primary").map((file) => normalizePath22(file.path)) + ); + const changedPaths = unique([ + ...canonicalSnapshot.entries.map((entry2) => normalizePath22(entry2.path)), + ...worktreeSnapshot.entries.map((entry2) => normalizePath22(entry2.path)), + ...dependencyPaths + ]); + const policy = input.config.orchestration.jobs.context.efficiency; + const overlaid = refreshRepositoryIndex( + { ...base.state, workspaceKey: workspaceKeyFor(input.worktreeRoot) }, + { + rootDir: input.worktreeRoot, + now: createdAt, + protectedPaths: indexProtectedPaths(input.config), + respectGitignore: policy.respectGitignore, + maxEntries: policy.maxIndexedFiles, + maxFileBytes: policy.maxIndexedFileBytes, + baselineRef: input.baselineRef ?? worktreeSnapshot.head ?? base.state.baselineRef, + changedPaths + } + ); + return [ + { + repositoryId: "primary", + rootDir: input.worktreeRoot, + index: new RepositoryContextIndex(overlaid.state), + changedPaths, + indexReused: true, + staleMetadataPaths: overlaid.refreshedPaths + } + ]; +} +function workUnitText(input) { + return [ + input.workUnit.title, + input.workUnit.goal, + ...input.workUnit.expectedArtifacts, + ...input.workUnit.expectedAreas + ].join("\n"); +} +function pathRepoHint(reference, repositoryId) { + const lower = reference.toLowerCase(); + const repo = repositoryId.toLowerCase(); + return lower.startsWith(`${repo}:`) || lower.startsWith(`${repo}/`); +} +function areaScore(input, repositoryId, candidatePath) { + const haystack = `${repositoryId}/${candidatePath}`.toLowerCase(); + return input.workUnit.expectedAreas.reduce((score, area) => { + const normalized = normalizePath22(area).toLowerCase().replace(/^[^:]+:/, ""); + return score + (normalized.length >= 2 && haystack.includes(normalized) ? 1 : 0); + }, 0); +} +function chooseUnique(input, candidates, reference) { + if (candidates.length === 0) return { ambiguous: false }; + if (candidates.length === 1) return { value: candidates[0], ambiguous: false }; + const hinted = candidates.filter((candidate) => pathRepoHint(reference, candidate.repositoryId)); + if (hinted.length === 1) return { value: hinted[0], ambiguous: false }; + const scored = candidates.map((candidate) => ({ candidate, score: areaScore(input, candidate.repositoryId, candidate.path) })).sort((left, right) => right.score - left.score); + if ((scored[0]?.score ?? 0) > (scored[1]?.score ?? 0)) { + return { value: scored[0].candidate, ambiguous: false }; + } + return { ambiguous: true }; +} +function locateExplicitTargets(input, repositories) { + const text93 = workUnitText(input); + const references = unique(extractPathReferences2(text93).map(normalizePath22)); + const symbols = unique(extractSymbolReferences(text93)); + const targets = /* @__PURE__ */ new Map(); + const missing = []; + const ambiguous = []; + for (const reference of references) { + const located = []; + for (const repository of repositories) { + const repoPrefix = `${repository.repositoryId}:`; + const withoutRepo = reference.startsWith(repoPrefix) ? reference.slice(repoPrefix.length) : reference; + const exact = repository.index.get(withoutRepo); + const paths = exact !== void 0 ? [exact.path] : withoutRepo.includes("/") ? [] : repository.index.namedExactly(withoutRepo); + for (const candidatePath of paths) { + const entry2 = repository.index.get(candidatePath); + if (entry2 !== void 0) { + located.push({ repositoryId: repository.repositoryId, path: entry2.path, symbols: entry2.symbols.slice(0, 12) }); + } + } + } + const selected = chooseUnique(input, located, reference); + if (selected.ambiguous) ambiguous.push(reference); + else if (selected.value !== void 0) { + targets.set(`${selected.value.repositoryId}:${selected.value.path}`, selected.value); + } else { + missing.push(reference); + } + } + for (const symbol of symbols) { + const located = []; + for (const repository of repositories) { + for (const candidatePath of repository.index.declaring(symbol)) { + const entry2 = repository.index.get(candidatePath); + if (entry2 !== void 0) { + located.push({ repositoryId: repository.repositoryId, path: entry2.path, symbols: [symbol] }); + } + } + } + if (located.length === 0) continue; + const alreadyBound = located.find( + (candidate) => targets.has(`${candidate.repositoryId}:${candidate.path}`) + ); + const selected = alreadyBound === void 0 ? chooseUnique(input, located, symbol) : { value: alreadyBound, ambiguous: false }; + if (selected.ambiguous) ambiguous.push(symbol); + else if (selected.value !== void 0) { + const key = `${selected.value.repositoryId}:${selected.value.path}`; + const previous = targets.get(key); + targets.set(key, { + ...selected.value, + symbols: unique([...previous?.symbols ?? [], symbol]).slice(0, 12) + }); + } + } + const createsNewArtifact = /\b(create|introduce|new file|add file)\b/i.test(text93); + return { + targets: [...targets.values()], + missing: createsNewArtifact ? [] : missing, + ambiguous, + explicitEvidence: references.length > 0 || symbols.length > 0 + }; +} +function queryFor(input, repository, targets, expansionLevel) { + const dependencyPaths = (input.dependencyContext ?? []).flatMap( + (dependency) => dependency.changedFiles.filter((file) => (file.repositoryId ?? "primary") === repository.repositoryId).map((file) => normalizePath22(file.path)) + ); + const approved = input.projection.contracts.flatMap((contract) => [ + contract.title, + contract.summary, + ...contract.requirements, + ...contract.invariants + ]); + const query = buildRetrievalQuery({ + taskId: input.workUnit.parentTaskId, + nodeId: input.objectiveNodeId, + attemptId: `${input.workUnit.workUnitId}-a${input.attempt}-secondary`, + role: "EXECUTOR", + contract: [ + ...input.workUnit.expectedArtifacts, + ...input.workUnit.expectedAreas, + ...input.workUnit.relevantContractIds, + ...approved + ].join("\n"), + objective: `${input.workUnit.title} +${input.workUnit.goal}`, + acceptanceCriteria: input.projection.objective.acceptance, + currentAction: [ + input.workUnit.goal, + ...targets.filter((target) => target.repositoryId === repository.repositoryId).map((target) => target.path) + ].join("\n"), + failureText: (input.priorFailureEvidence ?? []).join("\n").slice(0, 2e4), + changedPaths: dependencyPaths, + checkpointChangedPaths: dependencyPaths, + priorRelevantPaths: input.priorRelevantPaths, + expansionLevel + }); + query.actionPaths = unique([ + ...targets.filter((target) => target.repositoryId === repository.repositoryId).map((target) => target.path), + ...query.actionPaths + ]); + return query; +} +function contextBudget(input) { + const characterCeiling = Math.min( + SECONDARY_BUILDER_LIMITS.maxPacketCharacters, + input.maximumInputCharacters ?? SECONDARY_BUILDER_LIMITS.maxPacketCharacters + ); + return contextBudgetConfigSchema.parse({ + modelContextTokens: Math.max(1e3, Math.floor(characterCeiling / 4)), + reservedOutputTokens: 0, + reservedReasoningTokens: 0, + reservedGrowthTokens: 0 + }); +} +function itemFor(planEntry, result) { + return result.assembled.package.items.find( + (item) => item.provenance?.path === planEntry.path && item.layer === "WORKING_SET" + ); +} +function sectionFrom(repository, selected, content) { + const entry2 = repository.index.get(selected.path); + return secondarySourceContextSchema.parse({ + repositoryId: repository.repositoryId, + path: selected.path, + contentHash: selected.contentHash, + sectionHash: sha256Hex(content), + content, + reason: selected.reason, + ...selected.startLine !== void 0 ? { startLine: selected.startLine } : {}, + ...selected.endLine !== void 0 ? { endLine: selected.endLine } : {}, + symbols: unique([ + ...selected.symbol !== void 0 ? [selected.symbol] : [], + ...entry2?.symbols ?? [] + ]).slice(0, 12) + }); +} +function priority(targetKeys, repositoryId, selected, kind) { + if (targetKeys.has(`${repositoryId}:${selected.path}`)) return 0; + if (selected.reason === "EXPLICIT_FAILURE_REFERENCE") return 1; + if (kind === "test") return 2; + if (selected.reason === "DEPENDENCY_PROXIMITY") return 3; + if (selected.reason === "REFERENCE_PATTERN") return 4; + return 5; +} +function pairedTestMatches(testPath, targetPath) { + const stem2 = (value) => value.split("/").at(-1).replace(/\.(?:test|spec)\.[^.]+$/, "").replace(/\.[^.]+$/, "").replace(/(?:[-_.]?(?:test|tests|spec))$/i, "").toLowerCase(); + if (stem2(testPath) !== stem2(targetPath)) return false; + const meaningfulDirs = (value) => value.split("/").slice(0, -1).map((part) => part.toLowerCase()).filter((part) => !["src", "test", "tests", "__tests__"].includes(part)); + const targetDirs = meaningfulDirs(targetPath); + if (targetDirs.length === 0) return true; + const testDirs = new Set(meaningfulDirs(testPath)); + return targetDirs.some((part) => testDirs.has(part)); +} +async function compileSecondaryBuilderPacket(input) { + const startedAt = Date.now(); + const createdAt = nowIso2(input); + const budget = boundedBudget(input); + const expansionLevel = input.expansionLevel ?? "ADJACENT_DEPENDENCIES"; + const expansionDepth = CONTEXT_EXPANSION_LEVELS.indexOf(expansionLevel); + const repositories = await prepareRepositories(input, createdAt); + const located = locateExplicitTargets(input, repositories); + const prepared = repositories.map((repository) => { + const query = queryFor(input, repository, located.targets, expansionLevel); + return { repository, query, preliminary: rankCandidates(repository.index, query) }; + }); + let targets = [...located.targets]; + const ambiguity = [...located.ambiguous]; + if (targets.length === 0 && located.missing.length === 0) { + const plausible = prepared.flatMap( + (entry2) => entry2.preliminary.filter((candidate) => candidate.entry.kind === "source" || candidate.entry.kind === "config").slice(0, 2).map((candidate) => ({ repository: entry2.repository, candidate })) + ).sort( + (left, right) => right.candidate.score !== left.candidate.score ? right.candidate.score - left.candidate.score : `${left.repository.repositoryId}:${left.candidate.path}`.localeCompare( + `${right.repository.repositoryId}:${right.candidate.path}` + ) + ); + if (plausible[0] !== void 0 && plausible[1] !== void 0 && plausible[0].candidate.score === plausible[1].candidate.score && plausible[0].candidate.entry.module !== plausible[1].candidate.entry.module) { + ambiguity.push( + `${plausible[0].repository.repositoryId}:${plausible[0].candidate.path} or ${plausible[1].repository.repositoryId}:${plausible[1].candidate.path}` + ); + } else if (plausible[0] !== void 0) { + targets = [ + { + repositoryId: plausible[0].repository.repositoryId, + path: plausible[0].candidate.path, + symbols: plausible[0].candidate.entry.symbols.slice(0, 12) + } + ]; + const matching = prepared.find( + (entry2) => entry2.repository.repositoryId === plausible[0]?.repository.repositoryId + ); + if (matching !== void 0) { + matching.query.actionPaths = unique([plausible[0].candidate.path, ...matching.query.actionPaths]); + } + } + } + const policy = input.config.orchestration.jobs.context.efficiency; + const allocationPolicy = contextAllocationPolicySchema.parse({ + pinnedReserveRatio: policy.pinnedReserveRatio, + durableReserveRatio: policy.durableReserveRatio, + recoveryReserveRatio: policy.recoveryReserveRatio, + deltaReserveRatio: policy.deltaReserveRatio, + workingSetMaxRatio: policy.workingSetMaxRatio, + pointerShapeWorkingSetMaxRatio: policy.pointerShapeWorkingSetMaxRatio, + maxSingleItemRatio: policy.maxSingleItemRatio + }); + const results = []; + for (const entry2 of prepared) { + const planId = [ + "builder", + safeId(input.objectiveNodeId), + safeId(input.workUnit.workUnitId), + `a${input.attempt}`, + safeId(entry2.repository.repositoryId), + safeId(expansionLevel.toLowerCase()) + ].join("-").slice(0, 120); + const result = await buildEfficientContext({ + strategy: "SELECTIVE", + shape: "MATERIALIZED", + expansionLevel, + canonicalItems: [], + budget: contextBudget(input), + allocationPolicy, + createdAt, + planId, + taskId: input.workUnit.parentTaskId, + jobId: input.jobId, + nodeId: input.objectiveNodeId, + attemptId: `${input.workUnit.workUnitId}-a${input.attempt}-secondary`, + executionLane: "LOCAL", + executionMode: "DIRECT_MODEL", + runner: "secondary-objective-builder", + index: entry2.repository.index, + rootDir: entry2.repository.rootDir, + query: entry2.query, + rankOptions: { + maxCandidates: policy.maxCandidates, + excludedPaths: indexProtectedPaths(input.config) + }, + sectionOptions: { + wholeFileUnderChars: Math.min(policy.wholeFileUnderChars, budget.maxCharactersPerSection), + targetSectionChars: Math.min(policy.targetSectionChars, budget.maxCharactersPerSection) + }, + maxSelectedItems: budget.maxSelectedFiles, + maxPointers: 0 + }); + results.push({ repository: entry2.repository, result }); + } + const targetKeys = new Set(targets.map((target) => `${target.repositoryId}:${target.path}`)); + const selectedSections = results.flatMap( + ({ repository, result }) => result.plan.selectedWorkingItems.flatMap((selected) => { + const selectedKey = `${repository.repositoryId}:${selected.path}`; + const entryKind = repository.index.get(selected.path)?.kind; + if (targetKeys.size > 0 && !targetKeys.has(selectedKey) && (selected.reason === "TOKEN_OVERLAP" || selected.reason === "MODULE_PROXIMITY")) { + return []; + } + if (selected.reason === "TEST_SOURCE_PAIR" && !targetKeys.has(selectedKey)) { + if (entryKind !== "test") return []; + const matchesTarget = targets.some( + (target) => target.repositoryId === repository.repositoryId && (repository.index.testsFor(target.path).includes(selected.path) || repository.index.sourcesFor(selected.path).includes(target.path) || pairedTestMatches(selected.path, target.path)) + ); + if (!matchesTarget) return []; + } + const item = itemFor(selected, result); + if (item === void 0 || item.content.length > budget.maxCharactersPerSection) return []; + return [ + { + repository, + selected, + kind: entryKind, + section: sectionFrom(repository, selected, item.content) + } + ]; + }) + ).sort((left, right) => { + const leftPriority = priority(targetKeys, left.repository.repositoryId, left.selected, left.kind); + const rightPriority = priority(targetKeys, right.repository.repositoryId, right.selected, right.kind); + if (leftPriority !== rightPriority) return leftPriority - rightPriority; + return right.selected.score - left.selected.score; + }); + const sourceContext = []; + const tests = []; + const referencePatterns = []; + let usedCharacters = 0; + let selectedFileCount = 0; + for (const selected of selectedSections) { + if (selectedFileCount >= budget.maxSelectedFiles) continue; + if (usedCharacters + selected.section.content.length > budget.maxSourceCharacters) continue; + if (selected.kind === "test") { + if (tests.length >= budget.maxTests) continue; + tests.push(selected.section); + } else if (selected.selected.reason === "REFERENCE_PATTERN") { + if (referencePatterns.length >= budget.maxReferencePatterns) continue; + referencePatterns.push(selected.section); + } else { + sourceContext.push(selected.section); + } + usedCharacters += selected.section.content.length; + selectedFileCount += 1; + } + const selectedKeys = new Set( + [...sourceContext, ...tests, ...referencePatterns].map( + (section) => `${section.repositoryId}:${section.path}` + ) + ); + const unresolvedTargets = targets.filter( + (target) => !selectedKeys.has(`${target.repositoryId}:${target.path}`) + ); + const dependencyContext = (input.dependencyContext ?? []).filter((dependency) => dependency.verificationPassed).slice(0, SECONDARY_BUILDER_LIMITS.maxDependencyContext).map((dependency) => ({ + workUnitId: dependency.workUnitId, + summary: dependency.summary.slice(0, 2e3), + changedFiles: dependency.changedFiles.slice(0, 60).map((file) => ({ + repositoryId: file.repositoryId ?? "primary", + path: normalizePath22(file.path) + })), + exportedSymbols: [...dependency.exportedSymbols ?? []].slice(0, 30), + verificationPassed: true + })); + const missingDependencies = [...input.missingDependencyIds ?? []]; + const sourceCharacters = sourceContext.reduce((sum, section) => sum + section.content.length, 0); + const testCharacters = tests.reduce((sum, section) => sum + section.content.length, 0); + const planRefs = results.map(({ result }) => `context/plans/${result.plan.planId}`); + const metrics = secondaryBuilderPacketMetricsSchema.parse({ + indexedFilesConsidered: repositories.reduce((sum, repository) => sum + repository.index.size, 0), + candidateCount: results.reduce((sum, entry2) => sum + entry2.result.deterministicCandidates.length, 0), + selectedFiles: sourceContext.length + tests.length + referencePatterns.length, + selectedSections: [...sourceContext, ...tests, ...referencePatterns].filter( + (section) => section.startLine !== void 0 + ).length, + sourceCharacters, + testCharacters, + referencePatternCount: referencePatterns.length, + dependencyContextCount: dependencyContext.length, + budgetUtilization: usedCharacters / budget.maxSourceCharacters, + mandatoryRefsRetained: targets.length - unresolvedTargets.length, + expansionDepth, + staleEntriesEncountered: results.reduce((sum, entry2) => sum + entry2.result.refreshedPaths.length, 0) + repositories.reduce((sum, repository) => sum + repository.staleMetadataPaths.length, 0), + selectionDurationMs: Math.max(0, Date.now() - startedAt), + indexReused: repositories.every((repository) => repository.indexReused) + }); + const failureReasons = [ + ...located.missing.map((reference) => `explicit target ${reference} was not found`), + ...unresolvedTargets.map( + (target) => `target ${target.repositoryId}:${target.path} could not be materialized within policy` + ), + ...missingDependencies.map((dependency) => `verified dependency evidence for ${dependency} is missing`), + ...sourceContext.length === 0 ? ["no implementable source target was selected"] : [] + ]; + const contextSufficient = ambiguity.length === 0 && failureReasons.length === 0; + const quality = secondaryBuilderPacketQualitySchema.parse({ + explicitTargetResolved: located.explicitEvidence && located.missing.length === 0 && targets.length > 0, + targetAmbiguity: ambiguity.length > 0, + testsFound: tests.length > 0, + verificationHintsAvailable: (input.verificationHints?.length ?? 0) > 0, + referencePatternFound: referencePatterns.length > 0, + dependencyContextComplete: missingDependencies.length === 0, + sourceBudgetUtilization: usedCharacters / budget.maxSourceCharacters, + contextSufficient + }); + let plans = results.map(({ repository, result }) => { + const delivered = new Set( + [...sourceContext, ...tests, ...referencePatterns].filter((section) => section.repositoryId === repository.repositoryId).map((section) => section.path) + ); + const dropped = result.plan.selectedWorkingItems.filter((entry2) => !delivered.has(entry2.path)); + return contextSelectionPlanSchema.parse({ + ...result.plan, + repositoryId: repository.repositoryId, + selectedWorkingItems: result.plan.selectedWorkingItems.filter((entry2) => delivered.has(entry2.path)).map((entry2) => ({ ...entry2, repositoryId: repository.repositoryId })), + pointers: result.plan.pointers.map((entry2) => ({ ...entry2, repositoryId: repository.repositoryId })), + excludedCandidates: [ + ...result.plan.excludedCandidates.map((entry2) => ({ + ...entry2, + repositoryId: repository.repositoryId + })), + ...dropped.map((entry2) => ({ + repositoryId: repository.repositoryId, + path: entry2.path, + reason: "RANKED_BELOW_CUTOFF", + score: entry2.score, + detail: "excluded by Builder Packet category or character budget" + })) + ].slice(0, 200), + builderPacket: quality + }); + }); + const persistPlans = () => { + if (input.persist !== false) { + for (const plan of plans) writeContextSelectionPlan(input.workspace, plan); + } + }; + if (ambiguity.length > 0) { + persistPlans(); + return { + ok: false, + schemaVersion: BUILDER_PACKET_COMPILATION_SCHEMA_VERSION, + failure: { + kind: "AMBIGUOUS_TARGET", + reasons: ambiguity.slice(0, 8).map((entry2) => `equally plausible target: ${entry2}`) + }, + plans, + planRefs, + metrics, + quality + }; + } + if (!contextSufficient) { + persistPlans(); + return { + ok: false, + schemaVersion: BUILDER_PACKET_COMPILATION_SCHEMA_VERSION, + failure: { kind: "INSUFFICIENT_CONTEXT", reasons: failureReasons.slice(0, 8) }, + plans, + planRefs, + metrics, + quality + }; + } + let packet; + try { + packet = buildSecondaryBuilderPacket({ + projection: input.projection, + sourceContext, + targets: targets.map((target) => { + const selected = [...sourceContext, ...tests, ...referencePatterns].find( + (section) => section.repositoryId === target.repositoryId && section.path === target.path + ); + return { + repositoryId: target.repositoryId, + path: target.path, + symbols: target.symbols, + reason: selected?.reason ?? "EXPLICIT_ACTION_REFERENCE" + }; + }), + tests, + referencePatterns, + dependencyContext, + priorFailureEvidence: (input.priorFailureEvidence ?? []).map((entry2) => entry2.slice(0, 2e3)).slice(0, 20), + verificationHints: (input.verificationHints ?? []).map((hint) => hint.slice(0, 1e3)).slice(0, 30), + retrievalPlanRefs: planRefs, + expansionLevel, + contextMetrics: metrics, + quality, + createdAt + }); + const inputCharacters = SECONDARY_BUILDER_SYSTEM_PROMPT.length + renderSecondaryBuilderPrompt(packet).length; + const inputCeiling = input.maximumInputCharacters ?? SECONDARY_BUILDER_LIMITS.maxPacketCharacters; + if (inputCharacters > inputCeiling) { + throw new Error(`compiled input is ${inputCharacters} characters; limit is ${inputCeiling}`); + } + } catch (cause) { + const insufficientQuality = secondaryBuilderPacketQualitySchema.parse({ + ...quality, + contextSufficient: false + }); + plans = plans.map((plan) => contextSelectionPlanSchema.parse({ + ...plan, + builderPacket: insufficientQuality + })); + persistPlans(); + return { + ok: false, + schemaVersion: BUILDER_PACKET_COMPILATION_SCHEMA_VERSION, + failure: { + kind: "INSUFFICIENT_CONTEXT", + reasons: [`packet assembly could not fit within policy: ${cause instanceof Error ? cause.message : String(cause)}`.slice(0, 2e3)] + }, + plans, + planRefs, + metrics, + quality: insufficientQuality + }; + } + persistPlans(); + return { + ok: true, + schemaVersion: BUILDER_PACKET_COMPILATION_SCHEMA_VERSION, + packet, + plans, + planRefs, + metrics, + quality, + repositoryRoots: Object.fromEntries( + repositories.map((repository) => [repository.repositoryId, repository.rootDir]) + ) + }; +} function loadMissionTruth(workspace, mission) { return { mission, @@ -74748,7 +75684,7 @@ function decisionKindOf(raw) { function persistGraph2(input, graph) { return storeWorkGraph(input.workspace, input.jobId, graph); } -function nowIso2(input) { +function nowIso3(input) { return (input.clock ?? (() => /* @__PURE__ */ new Date()))().toISOString(); } function failResult(category, message2, source, output) { @@ -74759,7 +75695,7 @@ function failResult(category, message2, source, output) { }; } async function decomposeObjective(input, truth, relevantContractIds, acceptance) { - const at = nowIso2(input); + const at = nowIso3(input); const fallback = (reason) => singleUnitGraph({ jobId: input.jobId, node: input.node, @@ -75039,7 +75975,10 @@ function secondaryFailureCategory(kind) { case "VERIFICATION_FAILURE": return "VERIFICATION_FAILURE"; case "CONTEXT_TOO_LARGE": + case "INSUFFICIENT_CONTEXT": return "CAPABILITY_UNAVAILABLE"; + case "AMBIGUOUS_TARGET": + return "AMBIGUITY"; case "TIMEOUT": return "TRANSIENT_TRANSPORT"; case "INFERENCE_UNAVAILABLE": @@ -75055,34 +75994,75 @@ async function executeSelectedSecondaryBuilder(context, prepared, worktree) { if (selection === void 0) { return { prepared, result: builderFailureResult("secondary builder selection disappeared") }; } - let sourceContext; - try { - sourceContext = typeof selection.sourceContext === "function" ? await selection.sourceContext({ worktreeRoot: worktree.dir, projection: prepared.projection }) : selection.sourceContext; - } catch (cause) { - const problem = `source context could not be prepared: ${cause instanceof Error ? cause.message : String(cause)}`; - return { - prepared, - result: builderFailureResult(problem), - secondaryFailure: { kind: "STALE_SOURCE_CONTEXT", problem } - }; - } let packet; - try { - packet = buildSecondaryBuilderPacket({ - projection: prepared.projection, - sourceContext, - verificationHints: input.config.verification.commands.map((command) => command.name) + let repositoryRoots = { primary: worktree.dir }; + if (selection.sourceContext !== void 0) { + try { + const sourceContext = typeof selection.sourceContext === "function" ? await selection.sourceContext({ worktreeRoot: worktree.dir, projection: prepared.projection }) : selection.sourceContext; + packet = buildSecondaryBuilderPacket({ + projection: prepared.projection, + sourceContext, + verificationHints: input.config.verification.commands.map((command) => command.name) + }); + } catch (cause) { + const problem = `source context could not be prepared: ${cause instanceof Error ? cause.message : String(cause)}`; + return { + prepared, + result: builderFailureResult(problem), + secondaryFailure: { kind: "STALE_SOURCE_CONTEXT", problem } + }; + } + } else { + const compiler = selection.contextCompiler ?? new SecondaryBuilderContextCompiler(); + let compiled; + try { + compiled = await compiler.compile({ + workspace: input.workspace, + config: input.config, + jobId: input.jobId, + objectiveNodeId: input.node.nodeId, + workUnit: prepared.unit, + projection: prepared.projection, + attempt: prepared.attempt, + worktreeRoot: worktree.dir, + baselineRef: prepared.baselineCommit, + dependencyContext: prepared.dependencyContext, + missingDependencyIds: prepared.missingDependencyIds, + priorFailureEvidence: prepared.unit.latestFailure === void 0 ? [] : [`${prepared.unit.latestFailure.category}: ${prepared.unit.latestFailure.message}`], + verificationHints: input.config.verification.commands.map((command) => command.name), + maximumInputCharacters: secondaryBuilderInputCeiling(input.config), + createdAt: nowIso3(input) + }); + } catch (cause) { + const problem = `builder packet compilation failed: ${cause instanceof Error ? cause.message : String(cause)}`; + return { + prepared, + result: builderFailureResult(problem), + secondaryFailure: { kind: "INSUFFICIENT_CONTEXT", problem } + }; + } + input.recordEvent(compiled.ok ? "context_selected" : "context_insufficient", { + nodeId: input.node.nodeId, + workUnitId: prepared.unitId, + attempt: prepared.attempt, + planRefs: compiled.planRefs, + metrics: compiled.metrics, + quality: compiled.quality, + ...compiled.ok ? {} : { failure: compiled.failure.kind, reasons: compiled.failure.reasons } }); - } catch (cause) { - const problem = `secondary builder packet was refused: ${cause instanceof Error ? cause.message : String(cause)}`; - return { - prepared, - result: builderFailureResult(problem), - secondaryFailure: { kind: "CONTEXT_TOO_LARGE", problem } - }; + if (!compiled.ok) { + const problem = `${compiled.failure.kind}: ${compiled.failure.reasons.join("; ")}`; + return { + prepared, + result: builderFailureResult(problem), + secondaryFailure: { kind: compiled.failure.kind, problem } + }; + } + packet = compiled.packet; + repositoryRoots = compiled.repositoryRoots; } const inference = selection.inference ?? (input.localManager !== void 0 ? managedLocalSecondaryModelInference(input.localManager, input.config) : void 0); - const createdAt = nowIso2(input); + const createdAt = nowIso3(input); let artifact = secondaryBuilderAttemptSchema.parse({ schemaVersion: SECONDARY_BUILDER_ATTEMPT_SCHEMA_VERSION, attemptId: `${prepared.unitId}-a${String(prepared.attempt).padStart(2, "0")}-secondary`, @@ -75104,7 +76084,7 @@ async function executeSelectedSecondaryBuilder(context, prepared, worktree) { updatedAt: createdAt }); const persist32 = (update) => { - artifact = secondaryBuilderAttemptSchema.parse({ ...artifact, ...update, updatedAt: nowIso2(input) }); + artifact = secondaryBuilderAttemptSchema.parse({ ...artifact, ...update, updatedAt: nowIso3(input) }); storeSecondaryBuilderAttempt(input.workspace, input.jobId, input.node.nodeId, artifact); }; persist32({}); @@ -75143,6 +76123,7 @@ async function executeSelectedSecondaryBuilder(context, prepared, worktree) { maximumInputCharacters: secondaryBuilderInputCeiling(input.config), maxOutputBytes: input.config.localInference.maxOutputBytes, protectedPaths: input.config.execution.protectedPaths, + repositoryRoots, ...input.signal !== void 0 ? { signal: input.signal } : {}, onExecutionEvent: (event) => { if (event.stage === "INFERENCE_COMPLETED") { @@ -75165,7 +76146,7 @@ async function executeSelectedSecondaryBuilder(context, prepared, worktree) { }); if (!executed.ok) { persist32({ - status: "FAILED", + status: executed.failure.kind === "INSUFFICIENT_CONTEXT" ? "CONTEXT_INSUFFICIENT" : "FAILED", failure: executed.failure, ...executed.rawOutput !== void 0 ? { rawOutput: executed.rawOutput.slice(0, 1048576) } : {}, ...executed.proposal !== void 0 ? { proposal: executed.proposal } : {}, @@ -75279,9 +76260,11 @@ async function prepareUnitAttempt(context, graph, unitId) { const { input, truth } = context; const unit = requireUnit(graph, unitId); const attempt = unit.attempt + 1; - const at = nowIso2(input); + const at = nowIso3(input); const workEvidence = []; const dependencyPatches = []; + const dependencyContext = []; + const missingDependencyIds = []; for (const dependencyId of unit.dependsOn) { const dependency = findUnit(graph, dependencyId); if (dependency === void 0) continue; @@ -75311,6 +76294,18 @@ async function prepareUnitAttempt(context, graph, unitId) { if (patch !== void 0 && patch.trim().length > 0) { dependencyPatches.push({ workUnitId: resolved2.workUnitId, patch }); } + if (candidate.localVerification.passed) { + dependencyContext.push({ + workUnitId: resolved2.workUnitId, + summary: candidate.claims.summary, + changedFiles: candidate.changedFiles.map((file) => ({ repositoryId: "primary", path: file.path })), + verificationPassed: true + }); + } else { + missingDependencyIds.push(resolved2.workUnitId); + } + } else { + missingDependencyIds.push(resolved2.workUnitId); } } const projectedUnit = unit.relevantContractIds.length > 0 ? unit : { ...unit, relevantContractIds: context.objectiveContractIds.slice(0, 30) }; @@ -75379,6 +76374,7 @@ async function prepareUnitAttempt(context, graph, unitId) { graph: nextGraph, prepared: { unitId, + unit, kind: unit.kind, attempt, workerId, @@ -75386,7 +76382,9 @@ async function prepareUnitAttempt(context, graph, unitId) { ...worktree !== void 0 ? { worktree } : {}, baselineCommit, record: record32, - dependencyPatches + dependencyPatches, + dependencyContext, + missingDependencyIds } }; } @@ -75511,7 +76509,7 @@ async function foldBuilderOutcome(context, graph, executed) { }); } if (!result.ok) { - finishWorker(input.workspace, record32, "FAILED", nowIso2(input)); + finishWorker(input.workspace, record32, "FAILED", nowIso3(input)); input.recordEvent("candidate_failed", { nodeId: input.node.nodeId, workUnitId: unitId, @@ -75545,7 +76543,7 @@ async function foldBuilderOutcome(context, graph, executed) { workUnitId: unitId, attempt, workerId, - createdAt: nowIso2(input), + createdAt: nowIso3(input), baselineCommit: prepared.baselineCommit, contextProjectionHash: projection.contentHash, contractSnapshotHash: projection.contractSnapshotHash, @@ -75599,7 +76597,7 @@ async function foldBuilderOutcome(context, graph, executed) { secondaryBuilderAttemptSchema.parse({ ...executed.secondaryAttempt, status: "CANDIDATE_READY", - updatedAt: nowIso2(input) + updatedAt: nowIso3(input) }) ); input.recordEvent("secondary_candidate_succeeded", { @@ -75620,7 +76618,7 @@ async function foldBuilderOutcome(context, graph, executed) { contractSnapshotHash: candidate.contractSnapshotHash }); if (!acceptance.ok) { - finishWorker(input.workspace, record32, "FAILED", nowIso2(input)); + finishWorker(input.workspace, record32, "FAILED", nowIso3(input)); return persistGraph2( input, applyUnitRejection(input, graph, unitId, attempt, { @@ -75629,7 +76627,7 @@ async function foldBuilderOutcome(context, graph, executed) { }) ); } - finishWorker(input.workspace, record32, "FINISHED", nowIso2(input)); + finishWorker(input.workspace, record32, "FINISHED", nowIso3(input)); graph = persistGraph2(input, transitionUnit(graph, unitId, "CANDIDATE_READY")); input.recordEvent("candidate_ready", { nodeId: input.node.nodeId, @@ -75656,7 +76654,7 @@ async function foldBuilderOutcome(context, graph, executed) { latestFailure: { category: "AMBIGUITY", message: `The builder is blocked: ${[...result.output.blockingQuestions, result.output.summary].join("; ").slice(0, 1500)}`, - at: nowIso2(input) + at: nowIso3(input) } }) ); @@ -75692,7 +76690,7 @@ async function runUnitAttempt(context, graph, unitId) { } function applyUnitRejection(input, graph, unitId, attempt, failure3) { const unit = requireUnit(graph, unitId); - const at = nowIso2(input); + const at = nowIso3(input); const rejected = transitionUnit(graph, unitId, unit.status === "READY" ? "FAILED" : "REJECTED"); const withFailure = withUnit(rejected, { ...requireUnit(rejected, unitId), @@ -75760,7 +76758,7 @@ async function resumeStoredCandidate(context, graph, unitId) { async function evaluateCandidate(context, graph, unitId, attempt, candidate, projection, patch) { const { input, truth } = context; const unit = requireUnit(graph, unitId); - const at = nowIso2(input); + const at = nowIso3(input); const deterministic = evaluateDeterministically({ candidate, workUnit: unit, @@ -75858,7 +76856,7 @@ async function evaluateCandidate(context, graph, unitId, attempt, candidate, pro return persistGraph2(input, transitionUnit(graph, unitId, "EVALUATING")); } function recordConflict(input, graph, unitId, candidate, reasons, affectedContractIds, decisionKindRaw) { - const at = nowIso2(input); + const at = nowIso3(input); const kind = decisionKindOf(decisionKindRaw); const contractId = affectedContractIds[0] ?? "unknown-contract"; const conflict = storeConflict(input.workspace, input.jobId, input.node.nodeId, { @@ -75916,7 +76914,7 @@ async function runSemanticEvaluation(context, graph, unitId) { const { input, truth } = context; const unit = requireUnit(graph, unitId); const attempt = Math.max(1, unit.attempt); - const at = nowIso2(input); + const at = nowIso3(input); const candidate = readCandidate(input.workspace, input.jobId, input.node.nodeId, unitId, attempt); const projection = readProjection(input.workspace, input.jobId, input.node.nodeId, unitId, attempt); if (candidate === void 0 || projection === void 0) { @@ -76067,7 +77065,7 @@ ${correction}`; affectedContractIds: output.affectedContractIds, ...output.decisionKind !== void 0 ? { decisionKind: output.decisionKind } : {}, evaluatorWorkerId: selection.worker.workerId, - createdAt: nowIso2(input) + createdAt: nowIso3(input) }); graph = withUnit(graph, { ...requireUnit(graph, unitId), @@ -76122,7 +77120,7 @@ ${correction}`; latestFailure: { category: "AMBIGUITY", message: `The evaluator needs a ${kind} decision: ${output.reasons.join("; ").slice(0, 1200)}`, - at: nowIso2(input) + at: nowIso3(input) } }) ); @@ -76268,12 +77266,12 @@ async function driveObjective(input) { input.node.nodeId, workerRecords, unit.workUnitId, - nowIso2(input) + nowIso3(input) ); reconciled = transitionUnit(reconciled, unit.workUnitId, "READY"); continue; } - finishWorker(input.workspace, accepted.record, "FINISHED", nowIso2(input)); + finishWorker(input.workspace, accepted.record, "FINISHED", nowIso3(input)); } reconciled = transitionUnit(reconciled, unit.workUnitId, "CANDIDATE_READY"); reconciled = withUnit(reconciled, { @@ -76296,7 +77294,7 @@ async function driveObjective(input) { secondaryBuilderAttemptSchema.parse({ ...secondaryAttempt, status: "CANDIDATE_READY", - updatedAt: nowIso2(input) + updatedAt: nowIso3(input) }) ); } @@ -76312,7 +77310,7 @@ async function driveObjective(input) { continue; } } - supersedeWorkers(input.workspace, input.jobId, input.node.nodeId, workerRecords, unit.workUnitId, nowIso2(input)); + supersedeWorkers(input.workspace, input.jobId, input.node.nodeId, workerRecords, unit.workUnitId, nowIso3(input)); reconciled = transitionUnit(reconciled, unit.workUnitId, unit.status === "BUILDING" ? "READY" : "CANDIDATE_READY"); } else if (unit.status === "BLOCKED") { reconciled = transitionUnit( @@ -76475,7 +77473,7 @@ async function maybeAggregateSemantically(context, graph) { } const output = result.output; storeAggregationReport(input.workspace, input.jobId, input.node.nodeId, reportName, { - synthesizedAt: nowIso2(input), + synthesizedAt: nowIso3(input), aggregator: selection.worker.workerId, sources: reports.map((report) => report.workUnitId), ...output @@ -76524,7 +77522,7 @@ async function maybeAggregateSemantically(context, graph) { affectedWorkUnitIds: conflict.claims.map((claim2) => claim2.sourceWorkUnitId), decisionKind: kind, status: "OPEN", - createdAt: nowIso2(input) + createdAt: nowIso3(input) }); input.recordEvent("contract_conflict_detected", { nodeId: input.node.nodeId, @@ -76615,7 +77613,7 @@ async function integrateVerifiedCandidates(input, graph) { }; } let integrated = graph; - const at = nowIso2(input); + const at = nowIso3(input); for (const entry2 of candidates) { integrated = withUnit(transitionUnit(integrated, entry2.unit.workUnitId, "INTEGRATED"), { ...requireUnit(transitionUnit(integrated, entry2.unit.workUnitId, "INTEGRATED"), entry2.unit.workUnitId), @@ -90978,7 +91976,7 @@ function readTemplateRecords(workspace) { } return { records, diagnostics }; } -function nowIso3(clock) { +function nowIso4(clock) { return isoNow(clock); } function rethrowSpecExists(cause, specName) { @@ -91153,7 +92151,7 @@ function executeTemplateApplication(workspace, plan, clock = systemClock, record schemaVersion: "1.0.0", recordId: id, type: "template-apply", - createdAt: nowIso3(clock), + createdAt: nowIso4(clock), result: "ok", templateRef: plan.templateRef, templateId: plan.templateId, @@ -91273,7 +92271,7 @@ function executeTemplateInstall(workspace, plan, clock = systemClock, recordId) schemaVersion: "1.0.0", recordId: id, type: "template-install", - createdAt: nowIso3(clock), + createdAt: nowIso4(clock), result: "ok", templateRef: plan.ref, templateId: plan.templateId, @@ -91353,7 +92351,7 @@ function executeTemplateUninstall(workspace, plan, clock = systemClock, recordId schemaVersion: "1.0.0", recordId: id, type: "template-uninstall", - createdAt: nowIso3(clock), + createdAt: nowIso4(clock), result: "ok", templateRef: plan.ref, templateId: plan.templateId, @@ -91748,7 +92746,7 @@ function executeTemplateScaffold(plan, workspace, clock = systemClock, recordId) schemaVersion: "1.0.0", recordId: id, type: "template-scaffold", - createdAt: nowIso3(clock), + createdAt: nowIso4(clock), result: "ok", templateId: plan.templateId, kind: plan.kind, @@ -99862,7 +100860,7 @@ function autonomyPolicyOf(deps3) { function now5(deps3) { return (deps3.clock ?? (() => /* @__PURE__ */ new Date()))(); } -function nowIso4(deps3) { +function nowIso5(deps3) { return now5(deps3).toISOString(); } function newId5(deps3) { @@ -100167,7 +101165,7 @@ function draftSeal(deps3, request) { missionId: mission.missionId, ...mission.specName !== void 0 ? { specName: mission.specName } : {}, status: "DRAFT", - createdAt: nowIso4(deps3), + createdAt: nowIso5(deps3), ...request.supersedes !== void 0 ? { supersedes: request.supersedes } : {}, ...base, presentAuthorityKinds: presentAuthorityKinds(base), @@ -100254,7 +101252,7 @@ function sealMission(deps3, request) { const sealed = missionSealSchema.parse({ ...existing, status: "SEALED", - sealedAt: nowIso4(deps3), + sealedAt: nowIso5(deps3), sealedVia: (request.via ?? hostOf(deps3)).slice(0, SEAL_LIMITS.maxShortTextChars) }); writeJsonRecord(sealFile(deps3.workspace, existing.sealId), sealed); @@ -100276,7 +101274,7 @@ function revokeSeal(deps3, sealId, reason) { const revoked = missionSealSchema.parse({ ...seal, status: "REVOKED", - revokedAt: nowIso4(deps3), + revokedAt: nowIso5(deps3), revokedReason: reason.slice(0, SEAL_LIMITS.maxTextChars) }); writeJsonRecord(sealFile(deps3.workspace, sealId), revoked); @@ -100347,7 +101345,7 @@ function bindSealToJob(deps3, jobId, sealId) { jobId, sealId, missionId: seal.missionId, - boundAt: nowIso4(deps3), + boundAt: nowIso5(deps3), boundPolicyFingerprint: seal.delegatedAuthority.policyFingerprint }); writeJsonRecord(bindingFile(deps3.workspace, jobId), binding); @@ -100985,8 +101983,8 @@ function loadSupervisorState(deps3, ownerId) { return supervisorStateSchema.parse({ schemaVersion: SUPERVISOR_SCHEMA_VERSION, ownerId, - startedAt: nowIso4(deps3), - heartbeatAt: nowIso4(deps3), + startedAt: nowIso5(deps3), + heartbeatAt: nowIso5(deps3), pid: process.pid, hostname: safeHostname(), jobs: [] @@ -101009,7 +102007,7 @@ function findSupervisedJob(state, jobId) { } function appendSupervisionLog(deps3, entry2) { const validated = supervisionLogEntrySchema.parse({ - at: nowIso4(deps3), + at: nowIso5(deps3), ownerId: entry2.ownerId, ...entry2.jobId !== void 0 ? { jobId: entry2.jobId } : {}, action: entry2.action, @@ -101065,13 +102063,13 @@ function registerSupervisedJob(deps3, input) { specName: input.specName, ...input.sealId !== void 0 ? { sealId: input.sealId } : {}, status: "REGISTERED", - registeredAt: nowIso4(deps3) + registeredAt: nowIso5(deps3) } ); writeSupervisorState(deps3.workspace, { ...upsertSupervisedJob(state, supervised), schemaVersion: SUPERVISOR_SCHEMA_VERSION, - heartbeatAt: nowIso4(deps3) + heartbeatAt: nowIso5(deps3) }); return supervised; } @@ -101153,7 +102151,7 @@ async function superviseJob(deps3, jobId, options) { ); } emit22("lease", `lease acquired by ${ownerId} (generation ${acquisition.lease?.generation ?? 1})`); - const sessionStartedAt = options.sessionStartedAt ?? nowIso4(deps3); + const sessionStartedAt = options.sessionStartedAt ?? nowIso5(deps3); let job = requireJobState(deps3.workspace, jobId); let supervised = registerSupervisedJob(deps3, { jobId, @@ -101234,7 +102232,7 @@ async function applyDecision(deps3, input) { detail: decision.reason }); return { - supervised: { ...input.supervised, status: "RELEASED", releasedAt: nowIso4(deps3), releaseReason: decision.reason }, + supervised: { ...input.supervised, status: "RELEASED", releasedAt: nowIso5(deps3), releaseReason: decision.reason }, stop: job.status === "COMPLETED" ? { kind: "completed", status: job.status } : { kind: "released", reason: decision.reason } }; } @@ -101247,7 +102245,7 @@ async function applyDecision(deps3, input) { detail: decision.reason }); return { - supervised: { ...input.supervised, status: "RELEASED", releasedAt: nowIso4(deps3), releaseReason: decision.reason }, + supervised: { ...input.supervised, status: "RELEASED", releasedAt: nowIso5(deps3), releaseReason: decision.reason }, stop: { kind: "needs-human", status: decision.status, @@ -101263,7 +102261,7 @@ async function applyDecision(deps3, input) { detail: decision.reason }); return { - supervised: { ...input.supervised, status: "RELEASED", releasedAt: nowIso4(deps3), releaseReason: decision.reason }, + supervised: { ...input.supervised, status: "RELEASED", releasedAt: nowIso5(deps3), releaseReason: decision.reason }, stop: { kind: "gave-up", reason: decision.reason } }; } @@ -101357,10 +102355,10 @@ function foldDriverOutcome(deps3, input, outcome, action) { consecutiveRestarts: 0, backoffMs: 0, lastProgressFingerprint: after, - lastProgressAt: nowIso4(deps3), + lastProgressAt: nowIso5(deps3), nextAttemptAt: void 0, lastAction: unclean ? "DRIVER_DIED" : "DRIVER_EXITED_CLEANLY", - lastActionAt: nowIso4(deps3) + lastActionAt: nowIso5(deps3) }); } const backoffMs = nextBackoffMs( @@ -101378,7 +102376,7 @@ function foldDriverOutcome(deps3, input, outcome, action) { lastProgressFingerprint: after, nextAttemptAt: new Date(now5(deps3).getTime() + backoffMs).toISOString(), lastAction: unclean ? "DRIVER_DIED" : "DRIVER_EXITED_CLEANLY", - lastActionAt: nowIso4(deps3) + lastActionAt: nowIso5(deps3) }); } async function sleepInSlices(deps3, input) { @@ -101403,7 +102401,7 @@ function persistSupervised(deps3, ownerId, supervised) { writeSupervisorState(deps3.workspace, { ...upsertSupervisedJob(state, supervised), ownerId, - heartbeatAt: nowIso4(deps3) + heartbeatAt: nowIso5(deps3) }); } function fingerprintOf(deps3, job) { @@ -101594,7 +102592,7 @@ async function runOvernightPreflight(deps3, request) { const policy = autonomyPolicyOf(deps3); const projectDir = request.projectDir ?? deps3.workspace.rootDir; const run = request.probeRunner ?? createProcessProbeRunner(projectDir); - const at = nowIso4(deps3); + const at = nowIso5(deps3); const checks = []; const seal = resolveSeal(deps3, request); const add2 = (capability, outcome, observed, extra = {}) => { @@ -102177,7 +103175,7 @@ function requestToolsmithCapability(deps3, input) { grantsUsed: ledger.granted } ); - const at = nowIso4(deps3); + const at = nowIso5(deps3); const request = toolsmithRequestSchema.parse({ schemaVersion: TOOLSMITH_SCHEMA_VERSION, requestId: input.requestId ?? newRecordId(deps3, "ts"), @@ -102229,11 +103227,11 @@ function loadLedger(deps3, jobId) { return readToolsmithLedger(deps3.workspace, jobId) ?? toolsmithLedgerSchema.parse({ schemaVersion: TOOLSMITH_SCHEMA_VERSION, jobId, - updatedAt: nowIso4(deps3) + updatedAt: nowIso5(deps3) }); } function saveLedger(deps3, jobId, ledger) { - const next = toolsmithLedgerSchema.parse({ ...ledger, updatedAt: nowIso4(deps3) }); + const next = toolsmithLedgerSchema.parse({ ...ledger, updatedAt: nowIso5(deps3) }); writeJsonRecord(ledgerFile(deps3.workspace, jobId), next); return next; } @@ -102582,7 +103580,7 @@ async function provisionEnvironment(deps3, options) { planId: plan.planId, ...options.jobId !== void 0 ? { jobId: options.jobId } : {}, status: "PROVISIONING", - createdAt: nowIso4(deps3), + createdAt: nowIso5(deps3), services: ordered.map((service) => ({ serviceId: service.serviceId, status: "PENDING" })) }) ); @@ -102611,7 +103609,7 @@ async function provisionEnvironment(deps3, options) { services: instance.services.map((service) => ({ ...service, status: "STARTING", - startedAt: nowIso4(deps3) + startedAt: nowIso5(deps3) })) }); for (const service of ordered) { @@ -102643,7 +103641,7 @@ async function provisionEnvironment(deps3, options) { const ready = writeInstance(deps3, { ...instance, status: "READY", - readyAt: nowIso4(deps3) + readyAt: nowIso5(deps3) }); writeEvidence(deps3, plan, ready, now5(deps3).getTime() - startedAtMs, []); emitJobEvent(deps3, options.jobId, "environment_ready", { @@ -102659,7 +103657,7 @@ async function waitForService(deps3, input) { let state = { serviceId: service.serviceId, status: "WAITING_READY", - startedAt: nowIso4(deps3), + startedAt: nowIso5(deps3), restarts: 0, probeAttempts: 0 }; @@ -102687,7 +103685,7 @@ async function waitForService(deps3, input) { state: { ...state, status: "READY", - readyAt: nowIso4(deps3), + readyAt: nowIso5(deps3), lastProbeKind: lastKind, lastProbeDetail: lastDetail }, @@ -102795,7 +103793,7 @@ async function teardownEnvironment(deps3, input) { const stopped = writeInstance(deps3, { ...instance, status: "STOPPED", - stoppedAt: nowIso4(deps3), + stoppedAt: nowIso5(deps3), diagnosticsRetained: retain, services: instance.services.map((service) => ({ ...service, status: "STOPPED" })) }); @@ -102823,7 +103821,7 @@ function writeEvidence(deps3, plan, instance, totalMs, logRefs) { schemaVersion: ENVIRONMENT_SCHEMA_VERSION, instanceId: instance.instanceId, planId: plan.planId, - recordedAt: nowIso4(deps3), + recordedAt: nowIso5(deps3), status: instance.status, applicationLevelReady, livenessOnlyReady, @@ -103100,7 +104098,7 @@ async function runBrowserScenario(deps3, options) { ); } const resultId = options.resultId ?? newRecordId(deps3, "br"); - const startedAt = nowIso4(deps3); + const startedAt = nowIso5(deps3); const startedMs = now5(deps3).getTime(); emitJobEvent2(deps3, options.jobId, "browser_scenario_started", { scenarioId: scenario.scenarioId, @@ -103118,7 +104116,7 @@ async function runBrowserScenario(deps3, options) { ...options.jobId !== void 0 ? { jobId: options.jobId } : {}, status: "SKIPPED_NO_RUNTIME", startedAt, - finishedAt: nowIso4(deps3), + finishedAt: nowIso5(deps3), driver: options.driver.label, skipReason: availability.reason }), @@ -103148,7 +104146,7 @@ async function runBrowserScenario(deps3, options) { ...options.jobId !== void 0 ? { jobId: options.jobId } : {}, status: "ERRORED", startedAt, - finishedAt: nowIso4(deps3), + finishedAt: nowIso5(deps3), driver: options.driver.label, failureDetail: (cause instanceof Error ? cause.message : String(cause)).slice(0, 4e3) }), @@ -103232,7 +104230,7 @@ async function runBrowserScenario(deps3, options) { ...options.jobId !== void 0 ? { jobId: options.jobId } : {}, status, startedAt, - finishedAt: nowIso4(deps3), + finishedAt: nowIso5(deps3), driver: options.driver.label, steps, assertionsRun, @@ -103891,7 +104889,7 @@ function readClosureLedger(workspace, jobId) { ); } function buildClosureLedger(deps3, input) { - const at = nowIso4(deps3); + const at = nowIso5(deps3); const entries = []; const registry2 = readContractRegistry(deps3.workspace, input.seal.missionId); const byContract = new Map(registry2.map((contract) => [contract.contractId, contract])); @@ -103962,7 +104960,7 @@ function entry(at, input) { } function attributeNodeToItems(deps3, input) { const ledger = requireLedger(deps3.workspace, input.jobId); - const at = nowIso4(deps3); + const at = nowIso5(deps3); const entries = ledger.entries.map((existing) => { if (!input.itemIds.includes(existing.itemId)) return existing; if (existing.attributedNodeIds.includes(input.nodeId)) return existing; @@ -103977,7 +104975,7 @@ function attributeNodeToItems(deps3, input) { } function registerClosureEvidence(deps3, input) { const ledger = requireLedger(deps3.workspace, input.jobId); - const at = nowIso4(deps3); + const at = nowIso5(deps3); const entries = ledger.entries.map((existing) => { if (!input.itemIds.includes(existing.itemId)) return existing; return { @@ -104002,7 +105000,7 @@ function registerClosureEvidence(deps3, input) { } function waiveClosureItem(deps3, input) { const ledger = requireLedger(deps3.workspace, input.jobId); - const at = nowIso4(deps3); + const at = nowIso5(deps3); const entries = ledger.entries.map( (existing) => existing.itemId === input.itemId ? { ...existing, @@ -104022,7 +105020,7 @@ function runClosureAudit(deps3, input) { const policy = autonomyPolicyOf(deps3).closure; const ledger = requireLedger(deps3.workspace, input.jobId); const completed = new Set(input.completedNodeIds); - const at = nowIso4(deps3); + const at = nowIso5(deps3); const entries = ledger.entries.map((existing) => { const attributedNodesComplete = existing.attributedNodeIds.length > 0 && existing.attributedNodeIds.every((nodeId) => completed.has(nodeId)); const assessment = assessItemClosure( @@ -104128,7 +105126,7 @@ function generateGapWork(deps3, input) { const policy = autonomyPolicyOf(deps3).closure; const ledger = requireLedger(deps3.workspace, input.jobId); const byId = new Map(ledger.entries.map((existing) => [existing.itemId, existing])); - const at = nowIso4(deps3); + const at = nowIso5(deps3); const generated = []; for (const unclosed of input.audit.unclosed.slice(0, policy.maxGapWorkPerCycle)) { const existing = byId.get(unclosed.itemId); @@ -104196,7 +105194,7 @@ function requireLedger(workspace, jobId) { return ledger; } function saveLedger2(deps3, ledger) { - const next = closureLedgerSchema.parse({ ...ledger, updatedAt: nowIso4(deps3) }); + const next = closureLedgerSchema.parse({ ...ledger, updatedAt: nowIso5(deps3) }); writeJsonRecord(closureLedgerFile(deps3.workspace, next.jobId), next); return next; } @@ -104288,7 +105286,7 @@ function saveSystemScenario(deps3, input) { const scenario = systemScenarioSchema.parse({ schemaVersion: SYSTEM_SCENARIO_SCHEMA_VERSION, scenarioId: input.scenarioId ?? newRecordId(deps3, "ss"), - createdAt: nowIso4(deps3), + createdAt: nowIso5(deps3), ...input }); writeJsonRecord(scenarioFile(deps3.workspace, scenario.scenarioId), scenario); @@ -104312,7 +105310,7 @@ async function runSystemScenario(deps3, options) { throw new AutonomyError("SBA024", `No system scenario "${options.scenarioId}" exists.`); } const resultId = options.resultId ?? newRecordId(deps3, "sr"); - const startedAt = nowIso4(deps3); + const startedAt = nowIso5(deps3); emit2(deps3, options.jobId, "system_qualification_started", { scenarioId: scenario.scenarioId, resultId @@ -104453,7 +105451,7 @@ async function finish3(deps3, options, scenario, input) { schemaVersion: SYSTEM_SCENARIO_SCHEMA_VERSION, scenarioId: scenario.scenarioId, ...options.jobId !== void 0 ? { jobId: options.jobId } : {}, - finishedAt: nowIso4(deps3), + finishedAt: nowIso5(deps3), ...input }); writeJsonRecord(resultFile(deps3.workspace, result.resultId), result); @@ -104540,7 +105538,7 @@ function resultFile2(workspace, runId) { async function runReproducibilityQualification(deps3, options) { const policy = autonomyPolicyOf(deps3).closure; const runId = options.runId ?? newRecordId(deps3, "rp"); - const startedAt = nowIso4(deps3); + const startedAt = nowIso5(deps3); const deadline = now5(deps3).getTime() + policy.reproducibilityTimeoutMs; const run = options.commandRunner ?? (async (input) => { const [executable, ...argv2] = input.argv; @@ -104610,7 +105608,7 @@ async function runReproducibilityQualification(deps3, options) { ...options.jobId !== void 0 ? { jobId: options.jobId } : {}, status, startedAt, - finishedAt: nowIso4(deps3), + finishedAt: nowIso5(deps3), checkoutPath: options.checkoutPath.slice(0, 200), ...options.gitHead !== void 0 ? { gitHead: options.gitHead } : {}, dimensions: [...new Set(options.steps.map((step2) => step2.dimension))], @@ -105134,7 +106132,7 @@ function computeAutonomyTelemetry(deps3, input) { schemaVersion: TELEMETRY_SCHEMA_VERSION, jobId: input.jobId, ...binding !== void 0 ? { sealId: binding.sealId, missionId: binding.missionId } : {}, - recordedAt: nowIso4(deps3), + recordedAt: nowIso5(deps3), jobStatus: job?.status ?? "UNKNOWN", humanInterventionsAfterSeal: interventions.length, humanAuthorityEscalations: counters?.authorityEscalations ?? 0, @@ -105397,7 +106395,7 @@ async function runUnattendedMission(deps3, options) { completionGate: createClosureCompletionGate(deps3.workspace, policy.closure) }; const host = typeof options.host === "function" ? options.host(supervisedDeps) : options.host; - const sessionStartedAt = nowIso4(deps3); + const sessionStartedAt = nowIso5(deps3); const startedMs = now5(deps3).getTime(); const recoveries = []; const audits = []; @@ -106164,7 +107162,7 @@ function missionDepsOf(deps3) { function now6(deps3) { return (deps3.clock ?? (() => /* @__PURE__ */ new Date()))(); } -function nowIso5(deps3) { +function nowIso6(deps3) { return now6(deps3).toISOString(); } function hostOf2(deps3) { @@ -107703,7 +108701,7 @@ function groundInRepository(deps3, request) { return { schemaVersion: INTAKE_GROUNDING_SCHEMA_VERSION, intakeId: request.intakeId, - groundedAt: nowIso5(deps3), + groundedAt: nowIso6(deps3), baselineCommit: readGitHead(workspace.rootDir), existingProduct, evidence: evidence.slice(0, INTAKE_LIMITS.maxEvidence), @@ -107865,12 +108863,12 @@ function analyzeDeltaAuthority(request) { const counts = {}; for (const cls of DELTA_AUTHORITY_CLASSES) counts[cls] = 0; for (const item of items) counts[item.classification] = (counts[item.classification] ?? 0) + 1; - const modifiedContractIds = unique( + const modifiedContractIds = unique2( items.filter( (item) => item.classification === "EXISTING_SEALED_CONTRACT_CHANGE" || item.classification === "CONTRADICTION" ).map((item) => item.existingContractId).filter((id) => id !== void 0) ); - const extendedContractIds = unique( + const extendedContractIds = unique2( items.filter((item) => item.classification === "EXISTING_CONTRACT_EXTENSION").map((item) => item.existingContractId).filter((id) => id !== void 0) ); const owners = /* @__PURE__ */ new Map(); @@ -107896,7 +108894,7 @@ function analyzeDeltaAuthority(request) { relation }); } - const newSurfaces = unique( + const newSurfaces = unique2( items.filter((item) => item.classification === "NEW_DELEGATED_SURFACE").flatMap((item) => item.affectedSurfaces) ); const reasons = []; @@ -108135,7 +109133,7 @@ function raiseItemForQuestion(item, questionId, why) { function statementOf(chunk) { return chunk.text.replace(/^(\s*)([-*+]|\d+[.)])\s+/, "").trim(); } -function unique(values) { +function unique2(values) { return [...new Set(values)]; } function computeBasisDigest(request) { @@ -109427,7 +110425,7 @@ function recordDerivedApprovals(request) { return { approved, results }; } function emptyLedger(deps3, approval) { - const at = nowIso5(deps3); + const at = nowIso6(deps3); return { schemaVersion: INTAKE_LIFECYCLE_SCHEMA_VERSION, intakeId: approval.intakeId, @@ -109476,13 +110474,13 @@ async function runSealAndBuild(deps3, options) { outcome: void 0, finishedAt: void 0, humanPrerequisites: [], - updatedAt: nowIso5(deps3) + updatedAt: nowIso6(deps3) }); } const persist4 = (next) => { const written = writeLifecycle(deps3.workspace, { ...next, - updatedAt: nowIso5(deps3) + updatedAt: nowIso6(deps3) }); ledger = written; return written; @@ -109492,12 +110490,12 @@ async function runSealAndBuild(deps3, options) { persist4( withStep(ledger, step2, { status: "RUNNING", - startedAt: nowIso5(deps3), + startedAt: nowIso6(deps3), attempts: record5.attempts + 1 }) ); appendIntakeEvent(deps3.workspace, options.intakeId, { - at: nowIso5(deps3), + at: nowIso6(deps3), type: "build_step_started", step: step2 }); @@ -109507,13 +110505,13 @@ async function runSealAndBuild(deps3, options) { persist4( withStep(ledger, step2, { status, - settledAt: nowIso5(deps3), + settledAt: nowIso6(deps3), detail: detail.slice(0, INTAKE_LIMITS.maxTextChars), ...result !== void 0 ? { result } : {} }) ); appendIntakeEvent(deps3.workspace, options.intakeId, { - at: nowIso5(deps3), + at: nowIso6(deps3), type: "build_step_completed", step: step2, status, @@ -109525,12 +110523,12 @@ async function runSealAndBuild(deps3, options) { persist4( withStep(ledger, step2, { status: "FAILED", - settledAt: nowIso5(deps3), + settledAt: nowIso6(deps3), detail: detail.slice(0, INTAKE_LIMITS.maxTextChars) }) ); appendIntakeEvent(deps3.workspace, options.intakeId, { - at: nowIso5(deps3), + at: nowIso6(deps3), type: "build_step_failed", step: step2, detail: detail.slice(0, 600) @@ -109592,7 +110590,7 @@ async function runSealAndBuild(deps3, options) { writeProjectionEquivalence(deps3.workspace, equivalence); if (!equivalence.equivalent) { appendIntakeEvent(deps3.workspace, options.intakeId, { - at: nowIso5(deps3), + at: nowIso6(deps3), type: "projection_diverged", divergences: equivalence.divergences.length }); @@ -109603,7 +110601,7 @@ async function runSealAndBuild(deps3, options) { return finish4(deps3, options, ledger, "FAILED", { preflight }); } appendIntakeEvent(deps3.workspace, options.intakeId, { - at: nowIso5(deps3), + at: nowIso6(deps3), type: "projection_validated", checked: equivalence.checkedStatements, traced: equivalence.tracedStatements @@ -109635,7 +110633,7 @@ async function runSealAndBuild(deps3, options) { }); for (const stage of derived.approved) { appendIntakeEvent(deps3.workspace, options.intakeId, { - at: nowIso5(deps3), + at: nowIso6(deps3), type: "derived_approval_recorded", stage, approvalId: approval.approvalId, @@ -109685,7 +110683,7 @@ async function runSealAndBuild(deps3, options) { bindApprovalSeal(deps3.workspace, options.intakeId, seal.sealId); ledger = persist4({ ...ledger, sealId }); appendIntakeEvent(deps3.workspace, options.intakeId, { - at: nowIso5(deps3), + at: nowIso6(deps3), type: "seal_created", sealId: seal.sealId, approvalId: approval.approvalId, @@ -109710,7 +110708,7 @@ async function runSealAndBuild(deps3, options) { preflight = await runPreflight2(deps3, options, approval.missionId, sealId); ledger = persist4({ ...ledger, preflightReportId: preflight.reportId }); appendIntakeEvent(deps3.workspace, options.intakeId, { - at: nowIso5(deps3), + at: nowIso6(deps3), type: "preflight_completed", reportId: preflight.reportId, verdict: preflight.verdict @@ -109730,11 +110728,11 @@ async function runSealAndBuild(deps3, options) { withStep({ ...ledger, preflightReportId: preflight.reportId }, "PREFLIGHT", { detail: preflight.verdict, result: preflight.reportId, - settledAt: nowIso5(deps3) + settledAt: nowIso6(deps3) }) ); appendIntakeEvent(deps3.workspace, options.intakeId, { - at: nowIso5(deps3), + at: nowIso6(deps3), type: "preflight_completed", reportId: preflight.reportId, verdict: preflight.verdict, @@ -109749,7 +110747,7 @@ async function runSealAndBuild(deps3, options) { }); for (const resolved2 of resolution.resolved) { appendIntakeEvent(deps3.workspace, options.intakeId, { - at: nowIso5(deps3), + at: nowIso6(deps3), type: "prerequisite_resolved", detail: resolved2.slice(0, 400) }); @@ -109784,7 +110782,7 @@ async function runSealAndBuild(deps3, options) { const intake = requireIntakeState(deps3.workspace, options.intakeId); writeIntakeState(deps3.workspace, { ...intake, jobId, specName, sealId, status: "BUILDING" }); appendIntakeEvent(deps3.workspace, options.intakeId, { - at: nowIso5(deps3), + at: nowIso6(deps3), type: "job_created", jobId, specName @@ -109808,7 +110806,7 @@ async function runSealAndBuild(deps3, options) { if (retry.cleared) { emit3("lifecycle", `job ${jobId} unblocked and returned to the schedulable path`); appendIntakeEvent(deps3.workspace, options.intakeId, { - at: nowIso5(deps3), + at: nowIso6(deps3), type: "build_step_started", step: "LAUNCH", unblocked: true @@ -109845,7 +110843,7 @@ async function runSealAndBuild(deps3, options) { begin("LAUNCH"); try { appendIntakeEvent(deps3.workspace, options.intakeId, { - at: nowIso5(deps3), + at: nowIso6(deps3), type: "unattended_launched", jobId, sealId: sealId ?? null @@ -109886,7 +110884,7 @@ function validateProjection(deps3, approval, specName) { workspace: deps3.workspace, approval, specName, - checkedAt: nowIso5(deps3), + checkedAt: nowIso6(deps3), approvedElements: approvedElements( approval, contracts.map((contract) => ({ @@ -109979,11 +110977,11 @@ function finish4(deps3, options, ledger, outcome, extras) { const finished7 = writeLifecycle(deps3.workspace, { ...ledger, outcome, - updatedAt: nowIso5(deps3), - finishedAt: nowIso5(deps3) + updatedAt: nowIso6(deps3), + finishedAt: nowIso6(deps3) }); appendIntakeEvent(deps3.workspace, options.intakeId, { - at: nowIso5(deps3), + at: nowIso6(deps3), type: "build_finished", outcome, ...finished7.jobId !== void 0 ? { jobId: finished7.jobId } : {} @@ -110031,7 +111029,7 @@ function startSpecIntake(deps3, request) { const contentHash = sha256Hex(content); const storedPath = storeSourceText(deps3.workspace, intakeId, contentHash, content); const parsed = parseSpecificationDocument(content); - const at = nowIso5(deps3); + const at = nowIso6(deps3); const goal = request.goal?.trim() ?? deriveGoal(parsed.chunks, name); const mission = beginMission(missionDepsOf(deps3), { name, goal }); const source = writeSpecSource(deps3.workspace, { @@ -110126,7 +111124,7 @@ function runIntakeDiscovery(deps3, intakeId, options = {}) { throw new IntakeError("SBI004", `Spec intake ${intakeId} is ABANDONED and read-only.`); } const source = requireSpecSource(deps3.workspace, intakeId); - const at = nowIso5(deps3); + const at = nowIso6(deps3); const grounding = writeGrounding( deps3.workspace, groundInRepository(deps3, { @@ -110370,7 +111368,7 @@ function answerIntakeQuestion(deps3, intakeId, request, options = {}) { `Question ${request.questionId} was never mirrored into the mission and cannot record a governed answer.` ); } - const at = nowIso5(deps3); + const at = nowIso6(deps3); const result = answerQuestion(missionDepsOf(deps3), intake.missionId, { questionId: question.missionQuestionId, answer @@ -110442,7 +111440,7 @@ function approveIntake(deps3, request) { (decision) => decision.status === "active" ); const source = requireSpecSource(deps3.workspace, request.intakeId); - const at = nowIso5(deps3); + const at = nowIso6(deps3); const approval = writeApproval( deps3.workspace, buildIntakeApproval({ @@ -110533,7 +111531,7 @@ function requireIntakeFor(deps3, subject) { function abandonIntake(deps3, intakeId, reason) { const intake = requireIntakeState(deps3.workspace, intakeId); if (intake.status === "ABANDONED") return intake; - const at = nowIso5(deps3); + const at = nowIso6(deps3); appendIntakeEvent(deps3.workspace, intakeId, { at, type: "intake_abandoned", @@ -110673,7 +111671,7 @@ function recordFeatureOutcome(deps3, intakeId, outcome) { features[index] = updated; writeProductBaseline(deps3.workspace, { ...baseline, - updatedAt: nowIso5(deps3), + updatedAt: nowIso6(deps3), features }); } @@ -110693,7 +111691,7 @@ function computeIntakeTelemetry(deps3, intakeId) { return intakeTelemetrySchema.parse({ schemaVersion: INTAKE_TELEMETRY_SCHEMA_VERSION, intakeId, - recordedAt: nowIso5(deps3), + recordedAt: nowIso6(deps3), status: intake.status, discoveryHumanTurns: intake.counters.discoveryHumanTurns, productQuestionsAsked: questions.length, @@ -111499,7 +112497,7 @@ function bootstrapWorkspace(deps3, options = {}) { const ensured = ensureRepositoryIndex({ workspace, config: deps3.config, - now: nowIso5(deps3), + now: nowIso6(deps3), // Bootstrap has no Git snapshot to name additions, so the refresh walks // for them: a snapshot that missed a brand-new capability file would // claim currency over a repository it has not actually seen. @@ -111549,7 +112547,7 @@ function bootstrapWorkspace(deps3, options = {}) { schemaVersion: BOOTSTRAP_SCHEMA_VERSION, snapshotId: `snap-${contentHash.slice(0, 12)}`, workspaceKey: workspaceKeyFor(workspace.rootDir), - createdAt: nowIso5(deps3), + createdAt: nowIso6(deps3), ...material, indexStats: { entries: ensured.state.entries.length, @@ -111592,7 +112590,7 @@ function inspectWorkspace(deps3, options) { const ensured = ensureRepositoryIndex({ workspace, config: deps3.config, - now: nowIso5(deps3) + now: nowIso6(deps3) }); const query = buildRetrievalQuery({ taskId: "workspace-inspect", diff --git a/integrations/codex-plugin/specbridge/dist/mcp-server.cjs b/integrations/codex-plugin/specbridge/dist/mcp-server.cjs index b721e46..f167464 100644 --- a/integrations/codex-plugin/specbridge/dist/mcp-server.cjs +++ b/integrations/codex-plugin/specbridge/dist/mcp-server.cjs @@ -51606,6 +51606,8 @@ var CONTEXT_SELECTION_REASONS = [ "DEPENDENCY_PROXIMITY", /** In the same module/package as an already-selected file. */ "MODULE_PROXIMITY", + /** A bounded nearby implementation with the same structural convention. */ + "REFERENCE_PATTERN", /** A declared symbol matched a query symbol. */ "SYMBOL_MATCH", /** Filename/path tokens overlapped the query tokens. */ @@ -52900,6 +52902,7 @@ var DEFAULT_RANKING_WEIGHTS = Object.freeze({ testSourcePair: 180, dependencyProximity: 120, moduleProximity: 40, + referencePattern: 150, tokenOverlapPerToken: 12, tokenOverlapCap: 96, priorTaskRelevance: 60, @@ -53037,6 +53040,31 @@ function rankCandidates(index, query, options = {}) { for (const dependent of index.dependentsOf(anchor)) { add(dependent, "DEPENDENCY_PROXIMITY", weights.dependencyProximity, level2, anchor); } + const anchorEntry = index.get(anchor); + if (anchorEntry !== void 0 && anchorEntry.kind === "source") { + const suffixes = structuralSuffixes(anchorEntry.path, anchorEntry.symbols); + for (const sibling of index.siblingsIn(anchorEntry.module)) { + if (sibling === anchor) continue; + const entry = index.get(sibling); + if (entry === void 0 || entry.kind !== "source" || entry.language !== anchorEntry.language) { + continue; + } + const sharedImportCount = entry.imports.filter( + (specifier) => anchorEntry.imports.includes(specifier) + ).length; + const sameConvention = structuralSuffixes(entry.path, entry.symbols).some( + (suffix) => suffixes.includes(suffix) + ); + if (!sameConvention && sharedImportCount < 2) continue; + add( + sibling, + "REFERENCE_PATTERN", + weights.referencePattern + Math.min(30, sharedImportCount * 10), + level2, + sameConvention ? anchor : `${anchor} (${sharedImportCount} shared imports)` + ); + } + } } const anchorModules = new Set(anchors.map((anchor) => index.get(anchor)?.module ?? "")); for (const module2 of anchorModules) { @@ -53071,6 +53099,16 @@ function rankCandidates(index, query, options = {}) { ); return ranked.slice(0, maxCandidates); } +function structuralSuffixes(relativePath, symbols) { + const base = relativePath.split("/").at(-1)?.replace(/\.[^.]+$/, "") ?? ""; + const values = [base, ...symbols]; + const suffixes = /* @__PURE__ */ new Set(); + for (const value of values) { + const match = value.match(/([A-Z][a-z0-9]{2,})$/); + if (match?.[1] !== void 0) suffixes.add(match[1].toLowerCase()); + } + return [...suffixes]; +} var DEFAULT_SECTION_OPTIONS = { wholeFileUnderChars: 6e3, targetSectionChars: 4e3, @@ -53302,6 +53340,8 @@ function planContextExpansion(input) { } var shortText3 = external_exports.string().min(1).max(CONTEXT_LIMITS.maxShortTextChars); var selectedContextItemSchema = external_exports.object({ + /** Repository identity when a plan participates in a multi-repo packet. */ + repositoryId: shortText3.optional(), /** Workspace-relative path, forward slashes. */ path: shortText3, /** Content hash the selection was made against. Verified before use. */ @@ -53320,6 +53360,7 @@ var selectedContextItemSchema = external_exports.object({ detail: shortText3.optional() }).passthrough(); var contextPointerSchema = external_exports.object({ + repositoryId: shortText3.optional(), path: shortText3, reason: external_exports.enum(CONTEXT_SELECTION_REASONS), /** @@ -53337,6 +53378,7 @@ var contextPointerSchema = external_exports.object({ detail: shortText3.optional() }).passthrough(); var excludedCandidateSchema = external_exports.object({ + repositoryId: shortText3.optional(), path: shortText3, reason: external_exports.enum(CONTEXT_EXCLUSION_REASONS), score: external_exports.number().int(), @@ -53349,6 +53391,8 @@ var contextSelectionPlanSchema = external_exports.object({ taskId: shortText3, nodeId: shortText3.optional(), attemptId: shortText3.optional(), + /** Repository identity; paths are unique only within this namespace. */ + repositoryId: shortText3.optional(), strategy: external_exports.enum(CONTEXT_STRATEGIES), shape: external_exports.enum(CONTEXT_SHAPES), role: external_exports.enum(RETRIEVAL_ROLES), @@ -53395,6 +53439,17 @@ var contextSelectionPlanSchema = external_exports.object({ }).passthrough(), /** Stable identities of the reusable prompt components (observability). */ componentHashes: external_exports.record(external_exports.string().nullable()).default({}), + /** Phase 5 facts; metrics are evidence for later policy, never eligibility here. */ + builderPacket: external_exports.object({ + contextSufficient: external_exports.boolean(), + explicitTargetResolved: external_exports.boolean(), + targetAmbiguity: external_exports.boolean(), + testsFound: external_exports.boolean(), + verificationHintsAvailable: external_exports.boolean(), + referencePatternFound: external_exports.boolean(), + dependencyContextComplete: external_exports.boolean(), + sourceBudgetUtilization: external_exports.number().min(0) + }).passthrough().optional(), createdAt: shortText3 }).passthrough(); var RERANK_LIMITS = { @@ -57797,12 +57852,15 @@ var LOCAL_EXECUTOR_SYSTEM_PROMPT = [ " a verification cycle.", "- The response must be valid JSON for the provided schema." ].join("\n"); -var SECONDARY_BUILDER_PACKET_SCHEMA_VERSION = "1.0.0"; -var SECONDARY_BUILDER_RESULT_SCHEMA_VERSION = "1.0.0"; -var SECONDARY_BUILDER_ATTEMPT_SCHEMA_VERSION = "1.0.0"; +var SECONDARY_BUILDER_PACKET_SCHEMA_VERSION = "1.1.0"; +var SECONDARY_BUILDER_RESULT_SCHEMA_VERSION = "1.1.0"; +var SECONDARY_BUILDER_ATTEMPT_SCHEMA_VERSION = "1.1.0"; var SECONDARY_BUILDER_LIMITS = { ...LOCAL_EXECUTION_LIMITS, maxSourceFiles: 16, + maxTests: 6, + maxReferencePatterns: 4, + maxDependencyContext: 12, maxSourceFileChars: 32768, maxSourceBytes: 262144, maxPacketCharacters: 524288, @@ -57813,11 +57871,60 @@ var boundedText = (max) => external_exports.string().min(1).max(max); var shortText42 = boundedText(512); var sha256 = external_exports.string().regex(/^[a-f0-9]{64}$/); var secondarySourceContextSchema = external_exports.object({ - /** Worktree-relative path. Whole-file source only in Phase 4. */ + /** Repository namespace. Paths are unique only within this identity. */ + repositoryId: shortText42.default("primary"), + /** Repository-relative path. */ path: boundedText(SECONDARY_BUILDER_LIMITS.maxPathChars), - /** Hash of the exact UTF-8 content below. */ + /** Hash of the complete current file bytes. */ contentHash: sha256, - content: external_exports.string().max(SECONDARY_BUILDER_LIMITS.maxSourceFileChars) + /** Hash of the exact bounded content below (whole file or section). */ + sectionHash: sha256.optional(), + content: external_exports.string().max(SECONDARY_BUILDER_LIMITS.maxSourceFileChars), + reason: external_exports.enum(CONTEXT_SELECTION_REASONS).default("EXPLICIT_ACTION_REFERENCE"), + startLine: external_exports.number().int().min(1).optional(), + endLine: external_exports.number().int().min(1).optional(), + symbols: external_exports.array(shortText42).max(12).default([]) +}).strict(); +var secondaryBuilderTargetSchema = external_exports.object({ + repositoryId: shortText42, + path: boundedText(SECONDARY_BUILDER_LIMITS.maxPathChars), + symbols: external_exports.array(shortText42).max(12).default([]), + reason: external_exports.enum(CONTEXT_SELECTION_REASONS) +}).strict(); +var secondaryDependencyContextSchema = external_exports.object({ + workUnitId: shortText42, + summary: boundedText(2e3), + changedFiles: external_exports.array( + external_exports.object({ repositoryId: shortText42, path: boundedText(SECONDARY_BUILDER_LIMITS.maxPathChars) }).strict() + ).max(60), + exportedSymbols: external_exports.array(shortText42).max(30).default([]), + verificationPassed: external_exports.literal(true) +}).strict(); +var secondaryBuilderPacketMetricsSchema = external_exports.object({ + indexedFilesConsidered: external_exports.number().int().min(0), + candidateCount: external_exports.number().int().min(0), + selectedFiles: external_exports.number().int().min(0), + selectedSections: external_exports.number().int().min(0), + sourceCharacters: external_exports.number().int().min(0), + testCharacters: external_exports.number().int().min(0), + referencePatternCount: external_exports.number().int().min(0), + dependencyContextCount: external_exports.number().int().min(0), + budgetUtilization: external_exports.number().min(0), + mandatoryRefsRetained: external_exports.number().int().min(0), + expansionDepth: external_exports.number().int().min(0).max(4), + staleEntriesEncountered: external_exports.number().int().min(0), + selectionDurationMs: external_exports.number().int().min(0), + indexReused: external_exports.boolean() +}).strict(); +var secondaryBuilderPacketQualitySchema = external_exports.object({ + explicitTargetResolved: external_exports.boolean(), + targetAmbiguity: external_exports.boolean(), + testsFound: external_exports.boolean(), + verificationHintsAvailable: external_exports.boolean(), + referencePatternFound: external_exports.boolean(), + dependencyContextComplete: external_exports.boolean(), + sourceBudgetUtilization: external_exports.number().min(0), + contextSufficient: external_exports.boolean() }).strict(); var projectedContractSchema = external_exports.object({ contractId: shortText42, @@ -57833,7 +57940,10 @@ var secondaryBuilderPacketSchema = external_exports.object({ projectionHash: sha256, contractSnapshotHash: sha256, sourceContextHash: sha256, + /** Semantic identity; excludes createdAt and observational metrics. */ + contentHash: sha256, packetHash: sha256, + createdAt: shortText42.optional(), objective: external_exports.object({ nodeId: shortText42, taskId: shortText42, @@ -57857,6 +57967,19 @@ var secondaryBuilderPacketSchema = external_exports.object({ priorWorkEvidence: external_exports.array(boundedText(2e3)).max(30) }).strict(), sourceContext: external_exports.array(secondarySourceContextSchema).max(SECONDARY_BUILDER_LIMITS.maxSourceFiles), + targets: external_exports.array(secondaryBuilderTargetSchema).max(SECONDARY_BUILDER_LIMITS.maxSourceFiles), + tests: external_exports.array(secondarySourceContextSchema).max(SECONDARY_BUILDER_LIMITS.maxTests), + referencePatterns: external_exports.array(secondarySourceContextSchema).max(SECONDARY_BUILDER_LIMITS.maxReferencePatterns), + dependencyContext: external_exports.array(secondaryDependencyContextSchema).max(SECONDARY_BUILDER_LIMITS.maxDependencyContext), + priorFailureEvidence: external_exports.array(boundedText(2e3)).max(20), + retrievalPlanRef: shortText42, + retrievalPlanRefs: external_exports.array(shortText42).max(12), + expansion: external_exports.object({ + level: external_exports.enum(CONTEXT_EXPANSION_LEVELS), + remainingLevels: external_exports.number().int().min(0).max(4) + }).strict(), + contextMetrics: secondaryBuilderPacketMetricsSchema, + quality: secondaryBuilderPacketQualitySchema, forbiddenChanges: external_exports.array(boundedText(1e3)).max(30), verificationHints: external_exports.array(boundedText(1e3)).max(30) }).strict(); @@ -57867,16 +57990,26 @@ var secondaryStructuredEditSchema = external_exports.object({ }).strict(); var secondaryBuilderResultSchema = external_exports.object({ schemaVersion: external_exports.literal(SECONDARY_BUILDER_RESULT_SCHEMA_VERSION), + status: external_exports.enum(["EDITS", "NEEDS_MORE_CONTEXT"]).default("EDITS"), summary: boundedText(SECONDARY_BUILDER_LIMITS.maxSummaryChars), edits: external_exports.array(secondaryStructuredEditSchema).max(SECONDARY_BUILDER_LIMITS.maxEdits), - notes: external_exports.array(external_exports.string().max(SECONDARY_BUILDER_LIMITS.maxNoteChars)).max(SECONDARY_BUILDER_LIMITS.maxNotes).optional() -}).strict(); + notes: external_exports.array(external_exports.string().max(SECONDARY_BUILDER_LIMITS.maxNoteChars)).max(SECONDARY_BUILDER_LIMITS.maxNotes).optional(), + needsMoreContextReasons: external_exports.array(external_exports.string().min(1).max(SECONDARY_BUILDER_LIMITS.maxNoteChars)).min(1).max(8).optional() +}).strict().superRefine((result, context) => { + if (result.status === "NEEDS_MORE_CONTEXT" && result.needsMoreContextReasons === void 0) { + context.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["needsMoreContextReasons"], message: "required when status is NEEDS_MORE_CONTEXT" }); + } + if (result.status === "NEEDS_MORE_CONTEXT" && result.edits.length > 0) { + context.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["edits"], message: "must be empty when more context is required" }); + } +}); var SECONDARY_BUILDER_JSON_SCHEMA = { type: "object", additionalProperties: false, - required: ["schemaVersion", "summary", "edits"], + required: ["schemaVersion", "status", "summary", "edits"], properties: { schemaVersion: { type: "string", const: SECONDARY_BUILDER_RESULT_SCHEMA_VERSION }, + status: { type: "string", enum: ["EDITS", "NEEDS_MORE_CONTEXT"] }, summary: { type: "string", minLength: 1, maxLength: SECONDARY_BUILDER_LIMITS.maxSummaryChars }, edits: { type: "array", @@ -57896,6 +58029,12 @@ var SECONDARY_BUILDER_JSON_SCHEMA = { type: "array", maxItems: SECONDARY_BUILDER_LIMITS.maxNotes, items: { type: "string", maxLength: SECONDARY_BUILDER_LIMITS.maxNoteChars } + }, + needsMoreContextReasons: { + type: "array", + minItems: 1, + maxItems: 8, + items: { type: "string", minLength: 1, maxLength: SECONDARY_BUILDER_LIMITS.maxNoteChars } } } }; @@ -57903,6 +58042,9 @@ var SECONDARY_BUILDER_SYSTEM_PROMPT = [ "You are a bounded SECONDARY OBJECTIVE BUILDER, not an agent harness.", "You have no shell, git, filesystem, package-manager, test, credential, or tool access.", "The packet contains all approved truth and source bytes you may use.", + "You have been given the repository context selected for this task; do not explore beyond it.", + "Do not invent files, APIs, symbols, or architecture unsupported by the packet.", + "If the packet is insufficient to implement safely, return NEEDS_MORE_CONTEXT with bounded reasons and no edits.", "Return exactly one JSON document matching the supplied schema.", "Return complete UTF-8 file contents using only CREATE or REPLACE.", "Never return Markdown, diffs, commands, deletes, renames, symlinks, or authority/config edits.", @@ -57919,6 +58061,8 @@ var SECONDARY_BUILDER_FAILURES = [ "VERIFICATION_FAILURE", "TIMEOUT", "CONTEXT_TOO_LARGE", + "INSUFFICIENT_CONTEXT", + "AMBIGUOUS_TARGET", "CANCELLED" ]; var SECONDARY_BUILDER_ATTEMPT_STATUSES = [ @@ -57927,6 +58071,7 @@ var SECONDARY_BUILDER_ATTEMPT_STATUSES = [ "PROPOSAL_VALIDATED", "EDITS_APPLIED", "VERIFICATION_FAILED", + "CONTEXT_INSUFFICIENT", "CANDIDATE_READY", "FAILED" ]; @@ -62741,6 +62886,43 @@ var OBJECTIVE_OUTPUT_JSON_SCHEMAS = { } } }; +var BUILDER_PACKET_COMPILATION_SCHEMA_VERSION = "1.0.0"; +var BUILDER_PACKET_COMPILATION_FAILURES = [ + "INSUFFICIENT_CONTEXT", + "AMBIGUOUS_TARGET", + "STALE_SOURCE_CONTEXT" +]; +var builderPacketCompilationResultSchema = external_exports.discriminatedUnion("ok", [ + external_exports.object({ + ok: external_exports.literal(true), + schemaVersion: external_exports.literal(BUILDER_PACKET_COMPILATION_SCHEMA_VERSION), + packet: secondaryBuilderPacketSchema, + plans: external_exports.array(contextSelectionPlanSchema).max(12), + planRefs: external_exports.array(external_exports.string().min(1).max(512)).max(12), + metrics: secondaryBuilderPacketMetricsSchema, + quality: secondaryBuilderPacketQualitySchema, + repositoryRoots: external_exports.record(external_exports.string().min(1)) + }).strict(), + external_exports.object({ + ok: external_exports.literal(false), + schemaVersion: external_exports.literal(BUILDER_PACKET_COMPILATION_SCHEMA_VERSION), + failure: external_exports.object({ + kind: external_exports.enum(BUILDER_PACKET_COMPILATION_FAILURES), + reasons: external_exports.array(external_exports.string().min(1).max(2e3)).min(1).max(8) + }).strict(), + plans: external_exports.array(contextSelectionPlanSchema).max(12), + planRefs: external_exports.array(external_exports.string().min(1).max(512)).max(12), + metrics: secondaryBuilderPacketMetricsSchema, + quality: secondaryBuilderPacketQualitySchema + }).strict() +]); +var defaultBudget = { + maxSelectedFiles: SECONDARY_BUILDER_LIMITS.maxSourceFiles, + maxTests: SECONDARY_BUILDER_LIMITS.maxTests, + maxReferencePatterns: SECONDARY_BUILDER_LIMITS.maxReferencePatterns, + maxSourceCharacters: SECONDARY_BUILDER_LIMITS.maxSourceBytes, + maxCharactersPerSection: SECONDARY_BUILDER_LIMITS.maxSourceFileChars +}; var QUOTA_SNAPSHOT_SCHEMA_VERSION = "1.0.0"; var isoText = external_exports.string().min(1).max(64); var ratio = external_exports.number().min(0).max(1); diff --git a/packages/context/src/dedupe.ts b/packages/context/src/dedupe.ts index 7630760..4d86d25 100644 --- a/packages/context/src/dedupe.ts +++ b/packages/context/src/dedupe.ts @@ -140,8 +140,9 @@ export function deduplicateItems( const digest = contentDigest(item); const identity = sourceIdentity(item); + const contentKey = identity?.startsWith('key:repo:') === true ? `${digest}:${identity}` : digest; - const contentIndex = byContent.get(digest); + const contentIndex = byContent.get(contentKey); if (contentIndex !== undefined) { const incumbent = survivors[contentIndex]; if (incumbent !== undefined) { @@ -151,7 +152,7 @@ export function deduplicateItems( drop(incumbent, item, 'identical-content'); survivors[contentIndex] = undefined; survivors.push(item); - byContent.set(digest, survivors.length - 1); + byContent.set(contentKey, survivors.length - 1); if (identity !== undefined) bySource.set(identity, survivors.length - 1); } else { drop(item, incumbent, 'identical-content'); @@ -171,7 +172,7 @@ export function deduplicateItems( survivors[sourceIndex] = undefined; survivors.push(item); bySource.set(identity, survivors.length - 1); - byContent.set(digest, survivors.length - 1); + byContent.set(contentKey, survivors.length - 1); } else { drop(item, incumbent, kind); } @@ -180,7 +181,7 @@ export function deduplicateItems( bySource.set(identity, survivors.length); } - byContent.set(digest, survivors.length); + byContent.set(contentKey, survivors.length); survivors.push(item); } diff --git a/packages/context/src/retrieval-rank.ts b/packages/context/src/retrieval-rank.ts index f9c460b..25d5d29 100644 --- a/packages/context/src/retrieval-rank.ts +++ b/packages/context/src/retrieval-rank.ts @@ -35,6 +35,7 @@ export interface RankingWeights { testSourcePair: number; dependencyProximity: number; moduleProximity: number; + referencePattern: number; tokenOverlapPerToken: number; tokenOverlapCap: number; priorTaskRelevance: number; @@ -54,6 +55,7 @@ export const DEFAULT_RANKING_WEIGHTS: Readonly = Object.freeze({ testSourcePair: 180, dependencyProximity: 120, moduleProximity: 40, + referencePattern: 150, tokenOverlapPerToken: 12, tokenOverlapCap: 96, priorTaskRelevance: 60, @@ -316,6 +318,31 @@ export function rankCandidates( for (const dependent of index.dependentsOf(anchor)) { add(dependent, 'DEPENDENCY_PROXIMITY', weights.dependencyProximity, level2, anchor); } + const anchorEntry = index.get(anchor); + if (anchorEntry !== undefined && anchorEntry.kind === 'source') { + const suffixes = structuralSuffixes(anchorEntry.path, anchorEntry.symbols); + for (const sibling of index.siblingsIn(anchorEntry.module)) { + if (sibling === anchor) continue; + const entry = index.get(sibling); + if (entry === undefined || entry.kind !== 'source' || entry.language !== anchorEntry.language) { + continue; + } + const sharedImportCount = entry.imports.filter((specifier) => + anchorEntry.imports.includes(specifier), + ).length; + const sameConvention = structuralSuffixes(entry.path, entry.symbols).some((suffix) => + suffixes.includes(suffix), + ); + if (!sameConvention && sharedImportCount < 2) continue; + add( + sibling, + 'REFERENCE_PATTERN', + weights.referencePattern + Math.min(30, sharedImportCount * 10), + level2, + sameConvention ? anchor : `${anchor} (${sharedImportCount} shared imports)`, + ); + } + } } const anchorModules = new Set(anchors.map((anchor) => index.get(anchor)?.module ?? '')); for (const module of anchorModules) { @@ -358,3 +385,15 @@ export function rankCandidates( ); return ranked.slice(0, maxCandidates); } + +/** Conservative naming conventions such as Mapper, Controller, DTO, Serializer. */ +function structuralSuffixes(relativePath: string, symbols: readonly string[]): string[] { + const base = relativePath.split('/').at(-1)?.replace(/\.[^.]+$/, '') ?? ''; + const values = [base, ...symbols]; + const suffixes = new Set(); + for (const value of values) { + const match = value.match(/([A-Z][a-z0-9]{2,})$/); + if (match?.[1] !== undefined) suffixes.add(match[1].toLowerCase()); + } + return [...suffixes]; +} diff --git a/packages/context/src/selection.ts b/packages/context/src/selection.ts index 15f38c9..710d5fb 100644 --- a/packages/context/src/selection.ts +++ b/packages/context/src/selection.ts @@ -38,12 +38,14 @@ import type { ContextExpansionLevel, ContextShape } from './vocabulary.js'; * diagnostic record that is meant to be safe to display. */ -export const CONTEXT_SELECTION_PLAN_SCHEMA_VERSION = '1.0.0'; +export const CONTEXT_SELECTION_PLAN_SCHEMA_VERSION = '1.1.0'; const shortText = z.string().min(1).max(CONTEXT_LIMITS.maxShortTextChars); export const selectedContextItemSchema = z .object({ + /** Repository identity when a plan participates in a multi-repo packet. */ + repositoryId: shortText.optional(), /** Workspace-relative path, forward slashes. */ path: shortText, /** Content hash the selection was made against. Verified before use. */ @@ -76,6 +78,7 @@ export type SelectedContextItem = z.infer; */ export const contextPointerSchema = z .object({ + repositoryId: shortText.optional(), path: shortText, reason: z.enum(CONTEXT_SELECTION_REASONS), /** @@ -97,6 +100,7 @@ export type ContextPointer = z.infer; export const excludedCandidateSchema = z .object({ + repositoryId: shortText.optional(), path: shortText, reason: z.enum(CONTEXT_EXCLUSION_REASONS), score: z.number().int(), @@ -113,6 +117,8 @@ export const contextSelectionPlanSchema = z taskId: shortText, nodeId: shortText.optional(), attemptId: shortText.optional(), + /** Repository identity; paths are unique only within this namespace. */ + repositoryId: shortText.optional(), strategy: z.enum(CONTEXT_STRATEGIES), shape: z.enum(CONTEXT_SHAPES), role: z.enum(RETRIEVAL_ROLES), @@ -163,6 +169,20 @@ export const contextSelectionPlanSchema = z .passthrough(), /** Stable identities of the reusable prompt components (observability). */ componentHashes: z.record(z.string().nullable()).default({}), + /** Phase 5 facts; metrics are evidence for later policy, never eligibility here. */ + builderPacket: z + .object({ + contextSufficient: z.boolean(), + explicitTargetResolved: z.boolean(), + targetAmbiguity: z.boolean(), + testsFound: z.boolean(), + verificationHintsAvailable: z.boolean(), + referencePatternFound: z.boolean(), + dependencyContextComplete: z.boolean(), + sourceBudgetUtilization: z.number().min(0), + }) + .passthrough() + .optional(), createdAt: shortText, }) .passthrough(); @@ -247,6 +267,15 @@ export function selectWorkingSet(input: SelectWorkingSetInput): SelectWorkingSet continue; } + // Once the materialized file bound is full, ranking metadata is enough + // to explain the remainder. Do not read bodies merely to reject them. + // Mandatory references sort ahead of optional candidates and therefore + // cannot be hidden by this optimization. + if (input.shape === 'MATERIALIZED' && !candidate.mandatory && selected.length >= maxSelected) { + excluded.push({ path: candidate.path, reason: 'RANKED_BELOW_CUTOFF', score: candidate.score }); + continue; + } + const resolved = resolveFresh(input.rootDir, candidate.entry, { withContent: true }); if (resolved.status === 'missing') { excluded.push({ diff --git a/packages/context/src/vocabulary.ts b/packages/context/src/vocabulary.ts index 87e37ac..027ff8d 100644 --- a/packages/context/src/vocabulary.ts +++ b/packages/context/src/vocabulary.ts @@ -261,6 +261,8 @@ export const CONTEXT_SELECTION_REASONS = [ 'DEPENDENCY_PROXIMITY', /** In the same module/package as an already-selected file. */ 'MODULE_PROXIMITY', + /** A bounded nearby implementation with the same structural convention. */ + 'REFERENCE_PATTERN', /** A declared symbol matched a query symbol. */ 'SYMBOL_MATCH', /** Filename/path tokens overlapped the query tokens. */ diff --git a/packages/orchestration/src/objectives/builder-packet-compiler.ts b/packages/orchestration/src/objectives/builder-packet-compiler.ts new file mode 100644 index 0000000..a671293 --- /dev/null +++ b/packages/orchestration/src/objectives/builder-packet-compiler.ts @@ -0,0 +1,959 @@ +import { z } from 'zod'; +import type { AgentConfig, WorkspaceInfo } from '@specbridge/core'; +import { sha256Hex } from '@specbridge/core'; +import type { + ContextExpansionLevel, + ContextRetrievalQuery, + ContextSelectionPlan, + RepositoryContextIndex, + SelectedContextItem, +} from '@specbridge/context'; +import { + CONTEXT_EXPANSION_LEVELS, + buildEfficientContext, + buildRetrievalQuery, + contextAllocationPolicySchema, + contextBudgetConfigSchema, + contextSelectionPlanSchema, + extractPathReferences, + extractSymbolReferences, + rankCandidates, + refreshRepositoryIndex, + workspaceKeyFor, + RepositoryContextIndex as RepositoryContextIndexValue, +} from '@specbridge/context'; +import { captureGitSnapshot } from '@specbridge/evidence'; +import { ensureRepositoryIndex, indexProtectedPaths } from '../context/index-service.js'; +import { writeContextSelectionPlan } from '../context/store.js'; +import type { ContextProjection, WorkUnit } from './state.js'; +import { + SECONDARY_BUILDER_LIMITS, + SECONDARY_BUILDER_SYSTEM_PROMPT, + buildSecondaryBuilderPacket, + renderSecondaryBuilderPrompt, + secondaryBuilderPacketSchema, + secondaryBuilderPacketMetricsSchema, + secondaryBuilderPacketQualitySchema, + secondarySourceContextSchema, +} from './secondary-builder.js'; +import type { + NormalizedSecondarySourceContext, + SecondaryBuilderPacket, +} from './secondary-builder.js'; + +/** + * Phase 5's deterministic compiler: + * + * WorkUnit + approved projection + repository-index metadata + * -> bounded ranking/structural expansion + * -> fresh worktree sections + * -> explainable implementation packet + * + * It never invokes a model and never gives the eventual model repository + * authority. The existing context package owns indexing, ranking, section + * extraction, protected-path exclusion, and freshness checks. + */ + +export const BUILDER_PACKET_COMPILATION_SCHEMA_VERSION = '1.0.0'; + +export const BUILDER_PACKET_COMPILATION_FAILURES = [ + 'INSUFFICIENT_CONTEXT', + 'AMBIGUOUS_TARGET', + 'STALE_SOURCE_CONTEXT', +] as const; +export type BuilderPacketCompilationFailureKind = + (typeof BUILDER_PACKET_COMPILATION_FAILURES)[number]; + +export interface BuilderRepositoryContext { + /** Stable repository identity carried into every selected item. */ + repositoryId: string; + /** Root whose current bytes will be materialized. */ + rootDir: string; + /** Existing metadata index. Supplying it is the multi-repo/caller seam. */ + index: RepositoryContextIndex; + /** Paths known to differ from the index baseline. */ + changedPaths?: readonly string[] | undefined; + /** Whether this metadata came from a reusable index rather than a scan. */ + indexReused?: boolean | undefined; + /** Explicit justification; required for non-primary repositories. */ + justification?: string | undefined; +} + +export interface VerifiedDependencyContextInput { + workUnitId: string; + summary: string; + changedFiles: readonly { repositoryId?: string | undefined; path: string }[]; + exportedSymbols?: readonly string[] | undefined; + /** Only true dependency candidates are accepted by the compiler. */ + verificationPassed: true; +} + +export interface BuilderPacketBudget { + maxSelectedFiles: number; + maxTests: number; + maxReferencePatterns: number; + maxSourceCharacters: number; + maxCharactersPerSection: number; +} + +export interface BuilderPacketCompilationInput { + workspace: WorkspaceInfo; + config: AgentConfig; + jobId: string; + objectiveNodeId: string; + workUnit: WorkUnit; + projection: ContextProjection; + attempt: number; + /** Isolated worktree after verified dependency patches were applied. */ + worktreeRoot: string; + baselineRef?: string | undefined; + dependencyContext?: readonly VerifiedDependencyContextInput[] | undefined; + missingDependencyIds?: readonly string[] | undefined; + priorFailureEvidence?: readonly string[] | undefined; + priorRelevantPaths?: readonly string[] | undefined; + verificationHints?: readonly string[] | undefined; + repositories?: readonly BuilderRepositoryContext[] | undefined; + expansionLevel?: ContextExpansionLevel | undefined; + budget?: Partial | undefined; + maximumInputCharacters?: number | undefined; + createdAt?: string | undefined; + persist?: boolean | undefined; +} + +export type BuilderPacketCompilationResult = + | { + ok: true; + schemaVersion: typeof BUILDER_PACKET_COMPILATION_SCHEMA_VERSION; + packet: SecondaryBuilderPacket; + plans: ContextSelectionPlan[]; + planRefs: string[]; + metrics: z.infer; + quality: z.infer; + repositoryRoots: Readonly>; + } + | { + ok: false; + schemaVersion: typeof BUILDER_PACKET_COMPILATION_SCHEMA_VERSION; + failure: { kind: BuilderPacketCompilationFailureKind; reasons: string[] }; + plans: ContextSelectionPlan[]; + planRefs: string[]; + metrics: z.infer; + quality: z.infer; + }; + +/** Versioned public shape for durable/tooling validation of compiler output. */ +export const builderPacketCompilationResultSchema = z.discriminatedUnion('ok', [ + z + .object({ + ok: z.literal(true), + schemaVersion: z.literal(BUILDER_PACKET_COMPILATION_SCHEMA_VERSION), + packet: secondaryBuilderPacketSchema, + plans: z.array(contextSelectionPlanSchema).max(12), + planRefs: z.array(z.string().min(1).max(512)).max(12), + metrics: secondaryBuilderPacketMetricsSchema, + quality: secondaryBuilderPacketQualitySchema, + repositoryRoots: z.record(z.string().min(1)), + }) + .strict(), + z + .object({ + ok: z.literal(false), + schemaVersion: z.literal(BUILDER_PACKET_COMPILATION_SCHEMA_VERSION), + failure: z + .object({ + kind: z.enum(BUILDER_PACKET_COMPILATION_FAILURES), + reasons: z.array(z.string().min(1).max(2_000)).min(1).max(8), + }) + .strict(), + plans: z.array(contextSelectionPlanSchema).max(12), + planRefs: z.array(z.string().min(1).max(512)).max(12), + metrics: secondaryBuilderPacketMetricsSchema, + quality: secondaryBuilderPacketQualitySchema, + }) + .strict(), +]); + +export interface BuilderPacketCompiler { + compile(input: BuilderPacketCompilationInput): Promise; +} + +export class SecondaryBuilderContextCompiler implements BuilderPacketCompiler { + compile(input: BuilderPacketCompilationInput): Promise { + return compileSecondaryBuilderPacket(input); + } +} + +interface PreparedRepository { + repositoryId: string; + rootDir: string; + index: RepositoryContextIndex; + changedPaths: string[]; + indexReused: boolean; + staleMetadataPaths: string[]; +} + +interface LocatedTarget { + repositoryId: string; + path: string; + symbols: string[]; +} + +interface PreparedRetrieval { + repository: PreparedRepository; + query: ContextRetrievalQuery; + preliminary: ReturnType; +} + +const defaultBudget: BuilderPacketBudget = { + maxSelectedFiles: SECONDARY_BUILDER_LIMITS.maxSourceFiles, + maxTests: SECONDARY_BUILDER_LIMITS.maxTests, + maxReferencePatterns: SECONDARY_BUILDER_LIMITS.maxReferencePatterns, + maxSourceCharacters: SECONDARY_BUILDER_LIMITS.maxSourceBytes, + maxCharactersPerSection: SECONDARY_BUILDER_LIMITS.maxSourceFileChars, +}; + +function boundedBudget(input: BuilderPacketCompilationInput): BuilderPacketBudget { + const supplied = input.budget ?? {}; + return { + maxSelectedFiles: Math.max( + 1, + Math.min(SECONDARY_BUILDER_LIMITS.maxSourceFiles, supplied.maxSelectedFiles ?? defaultBudget.maxSelectedFiles), + ), + maxTests: Math.max(0, Math.min(SECONDARY_BUILDER_LIMITS.maxTests, supplied.maxTests ?? defaultBudget.maxTests)), + maxReferencePatterns: Math.max( + 0, + Math.min( + SECONDARY_BUILDER_LIMITS.maxReferencePatterns, + supplied.maxReferencePatterns ?? defaultBudget.maxReferencePatterns, + ), + ), + maxSourceCharacters: Math.max( + 1_000, + Math.min(SECONDARY_BUILDER_LIMITS.maxSourceBytes, supplied.maxSourceCharacters ?? defaultBudget.maxSourceCharacters), + ), + maxCharactersPerSection: Math.max( + 500, + Math.min( + SECONDARY_BUILDER_LIMITS.maxSourceFileChars, + supplied.maxCharactersPerSection ?? defaultBudget.maxCharactersPerSection, + ), + ), + }; +} + +function normalizePath(value: string): string { + return value.replace(/\\/g, '/').replace(/^\.\//, '').trim(); +} + +function unique(values: readonly string[]): string[] { + return [...new Set(values.filter((value) => value.length > 0))]; +} + +function safeId(value: string): string { + return value.replace(/[^A-Za-z0-9._-]/g, '-').slice(0, 80) || 'repository'; +} + +function nowIso(input: BuilderPacketCompilationInput): string { + return input.createdAt ?? new Date().toISOString(); +} + +async function prepareRepositories( + input: BuilderPacketCompilationInput, + createdAt: string, +): Promise { + if (input.repositories !== undefined) { + const seen = new Set(); + return input.repositories.map((repository, position) => { + if (seen.has(repository.repositoryId)) { + throw new Error(`duplicate repository identity ${repository.repositoryId}`); + } + seen.add(repository.repositoryId); + if ( + position > 0 && + repository.repositoryId !== 'primary' && + (repository.justification === undefined || repository.justification.trim() === '') + ) { + throw new Error(`secondary repository ${repository.repositoryId} needs an explicit justification`); + } + return { + repositoryId: repository.repositoryId, + rootDir: repository.rootDir, + index: repository.index, + changedPaths: unique((repository.changedPaths ?? []).map(normalizePath)), + indexReused: repository.indexReused ?? true, + staleMetadataPaths: [], + }; + }); + } + + // Reuse the canonical metadata index, then overlay only the paths whose + // bytes differ in the isolated builder baseline. This is the important + // performance/correctness combination: no per-attempt repository scan, + // but dependency-patched declarations/imports are current before ranking. + const canonicalSnapshot = await captureGitSnapshot(input.workspace.rootDir); + const base = ensureRepositoryIndex({ + workspace: input.workspace, + config: input.config, + now: createdAt, + gitSnapshot: canonicalSnapshot, + }); + const worktreeSnapshot = await captureGitSnapshot(input.worktreeRoot); + const dependencyPaths = (input.dependencyContext ?? []).flatMap((dependency) => + dependency.changedFiles + .filter((file) => (file.repositoryId ?? 'primary') === 'primary') + .map((file) => normalizePath(file.path)), + ); + const changedPaths = unique([ + ...canonicalSnapshot.entries.map((entry) => normalizePath(entry.path)), + ...worktreeSnapshot.entries.map((entry) => normalizePath(entry.path)), + ...dependencyPaths, + ]); + const policy = input.config.orchestration.jobs.context.efficiency; + const overlaid = refreshRepositoryIndex( + { ...base.state, workspaceKey: workspaceKeyFor(input.worktreeRoot) }, + { + rootDir: input.worktreeRoot, + now: createdAt, + protectedPaths: indexProtectedPaths(input.config), + respectGitignore: policy.respectGitignore, + maxEntries: policy.maxIndexedFiles, + maxFileBytes: policy.maxIndexedFileBytes, + baselineRef: input.baselineRef ?? worktreeSnapshot.head ?? base.state.baselineRef, + changedPaths, + }, + ); + return [ + { + repositoryId: 'primary', + rootDir: input.worktreeRoot, + index: new RepositoryContextIndexValue(overlaid.state), + changedPaths, + indexReused: true, + staleMetadataPaths: overlaid.refreshedPaths, + }, + ]; +} + +function workUnitText(input: BuilderPacketCompilationInput): string { + return [ + input.workUnit.title, + input.workUnit.goal, + ...input.workUnit.expectedArtifacts, + ...input.workUnit.expectedAreas, + ].join('\n'); +} + +function pathRepoHint(reference: string, repositoryId: string): boolean { + const lower = reference.toLowerCase(); + const repo = repositoryId.toLowerCase(); + return lower.startsWith(`${repo}:`) || lower.startsWith(`${repo}/`); +} + +function areaScore(input: BuilderPacketCompilationInput, repositoryId: string, candidatePath: string): number { + const haystack = `${repositoryId}/${candidatePath}`.toLowerCase(); + return input.workUnit.expectedAreas.reduce((score, area) => { + const normalized = normalizePath(area).toLowerCase().replace(/^[^:]+:/, ''); + return score + (normalized.length >= 2 && haystack.includes(normalized) ? 1 : 0); + }, 0); +} + +function chooseUnique( + input: BuilderPacketCompilationInput, + candidates: readonly T[], + reference: string, +): { value?: T; ambiguous: boolean } { + if (candidates.length === 0) return { ambiguous: false }; + if (candidates.length === 1) return { value: candidates[0] as T, ambiguous: false }; + const hinted = candidates.filter((candidate) => pathRepoHint(reference, candidate.repositoryId)); + if (hinted.length === 1) return { value: hinted[0] as T, ambiguous: false }; + const scored = candidates + .map((candidate) => ({ candidate, score: areaScore(input, candidate.repositoryId, candidate.path) })) + .sort((left, right) => right.score - left.score); + if ((scored[0]?.score ?? 0) > (scored[1]?.score ?? 0)) { + return { value: scored[0]!.candidate, ambiguous: false }; + } + return { ambiguous: true }; +} + +function locateExplicitTargets( + input: BuilderPacketCompilationInput, + repositories: readonly PreparedRepository[], +): { targets: LocatedTarget[]; missing: string[]; ambiguous: string[]; explicitEvidence: boolean } { + const text = workUnitText(input); + const references = unique(extractPathReferences(text).map(normalizePath)); + const symbols = unique(extractSymbolReferences(text)); + const targets = new Map(); + const missing: string[] = []; + const ambiguous: string[] = []; + + for (const reference of references) { + const located: LocatedTarget[] = []; + for (const repository of repositories) { + const repoPrefix = `${repository.repositoryId}:`; + const withoutRepo = reference.startsWith(repoPrefix) ? reference.slice(repoPrefix.length) : reference; + const exact = repository.index.get(withoutRepo); + const paths = exact !== undefined + ? [exact.path] + : withoutRepo.includes('/') + ? [] + : repository.index.namedExactly(withoutRepo); + for (const candidatePath of paths) { + const entry = repository.index.get(candidatePath); + if (entry !== undefined) { + located.push({ repositoryId: repository.repositoryId, path: entry.path, symbols: entry.symbols.slice(0, 12) }); + } + } + } + const selected = chooseUnique(input, located, reference); + if (selected.ambiguous) ambiguous.push(reference); + else if (selected.value !== undefined) { + targets.set(`${selected.value.repositoryId}:${selected.value.path}`, selected.value); + } else { + missing.push(reference); + } + } + + for (const symbol of symbols) { + const located: LocatedTarget[] = []; + for (const repository of repositories) { + for (const candidatePath of repository.index.declaring(symbol)) { + const entry = repository.index.get(candidatePath); + if (entry !== undefined) { + located.push({ repositoryId: repository.repositoryId, path: entry.path, symbols: [symbol] }); + } + } + } + if (located.length === 0) continue; + const alreadyBound = located.find((candidate) => + targets.has(`${candidate.repositoryId}:${candidate.path}`), + ); + const selected = alreadyBound === undefined ? chooseUnique(input, located, symbol) : { value: alreadyBound, ambiguous: false }; + if (selected.ambiguous) ambiguous.push(symbol); + else if (selected.value !== undefined) { + const key = `${selected.value.repositoryId}:${selected.value.path}`; + const previous = targets.get(key); + targets.set(key, { + ...selected.value, + symbols: unique([...(previous?.symbols ?? []), symbol]).slice(0, 12), + }); + } + } + + // A named existing implementation is a target. A missing path may be a + // CREATE artifact only when the WorkUnit actually says so. + const createsNewArtifact = /\b(create|introduce|new file|add file)\b/i.test(text); + return { + targets: [...targets.values()], + missing: createsNewArtifact ? [] : missing, + ambiguous, + explicitEvidence: references.length > 0 || symbols.length > 0, + }; +} + +function queryFor( + input: BuilderPacketCompilationInput, + repository: PreparedRepository, + targets: readonly LocatedTarget[], + expansionLevel: ContextExpansionLevel, +): ContextRetrievalQuery { + const dependencyPaths = (input.dependencyContext ?? []).flatMap((dependency) => + dependency.changedFiles + .filter((file) => (file.repositoryId ?? 'primary') === repository.repositoryId) + .map((file) => normalizePath(file.path)), + ); + const approved = input.projection.contracts.flatMap((contract) => [ + contract.title, + contract.summary, + ...contract.requirements, + ...contract.invariants, + ]); + const query = buildRetrievalQuery({ + taskId: input.workUnit.parentTaskId, + nodeId: input.objectiveNodeId, + attemptId: `${input.workUnit.workUnitId}-a${input.attempt}-secondary`, + role: 'EXECUTOR', + contract: [ + ...input.workUnit.expectedArtifacts, + ...input.workUnit.expectedAreas, + ...input.workUnit.relevantContractIds, + ...approved, + ].join('\n'), + objective: `${input.workUnit.title}\n${input.workUnit.goal}`, + acceptanceCriteria: input.projection.objective.acceptance, + currentAction: [ + input.workUnit.goal, + ...targets.filter((target) => target.repositoryId === repository.repositoryId).map((target) => target.path), + ].join('\n'), + failureText: (input.priorFailureEvidence ?? []).join('\n').slice(0, 20_000), + changedPaths: dependencyPaths, + checkpointChangedPaths: dependencyPaths, + priorRelevantPaths: input.priorRelevantPaths, + expansionLevel, + }); + query.actionPaths = unique([ + ...targets.filter((target) => target.repositoryId === repository.repositoryId).map((target) => target.path), + ...query.actionPaths, + ]); + return query; +} + +function contextBudget(input: BuilderPacketCompilationInput) { + const characterCeiling = Math.min( + SECONDARY_BUILDER_LIMITS.maxPacketCharacters, + input.maximumInputCharacters ?? SECONDARY_BUILDER_LIMITS.maxPacketCharacters, + ); + return contextBudgetConfigSchema.parse({ + modelContextTokens: Math.max(1_000, Math.floor(characterCeiling / 4)), + reservedOutputTokens: 0, + reservedReasoningTokens: 0, + reservedGrowthTokens: 0, + }); +} + +function itemFor( + planEntry: SelectedContextItem, + result: Awaited>, +) { + return result.assembled.package.items.find( + (item) => item.provenance?.path === planEntry.path && item.layer === 'WORKING_SET', + ); +} + +function sectionFrom( + repository: PreparedRepository, + selected: SelectedContextItem, + content: string, +): NormalizedSecondarySourceContext { + const entry = repository.index.get(selected.path); + return secondarySourceContextSchema.parse({ + repositoryId: repository.repositoryId, + path: selected.path, + contentHash: selected.contentHash, + sectionHash: sha256Hex(content), + content, + reason: selected.reason, + ...(selected.startLine !== undefined ? { startLine: selected.startLine } : {}), + ...(selected.endLine !== undefined ? { endLine: selected.endLine } : {}), + symbols: unique([ + ...(selected.symbol !== undefined ? [selected.symbol] : []), + ...(entry?.symbols ?? []), + ]).slice(0, 12), + }); +} + +function priority( + targetKeys: ReadonlySet, + repositoryId: string, + selected: SelectedContextItem, + kind: string | undefined, +): number { + if (targetKeys.has(`${repositoryId}:${selected.path}`)) return 0; + if (selected.reason === 'EXPLICIT_FAILURE_REFERENCE') return 1; + if (kind === 'test') return 2; + if (selected.reason === 'DEPENDENCY_PROXIMITY') return 3; + if (selected.reason === 'REFERENCE_PATTERN') return 4; + return 5; +} + +function pairedTestMatches(testPath: string, targetPath: string): boolean { + const stem = (value: string): string => + value + .split('/') + .at(-1)! + .replace(/\.(?:test|spec)\.[^.]+$/, '') + .replace(/\.[^.]+$/, '') + .replace(/(?:[-_.]?(?:test|tests|spec))$/i, '') + .toLowerCase(); + if (stem(testPath) !== stem(targetPath)) return false; + const meaningfulDirs = (value: string): string[] => + value + .split('/') + .slice(0, -1) + .map((part) => part.toLowerCase()) + .filter((part) => !['src', 'test', 'tests', '__tests__'].includes(part)); + const targetDirs = meaningfulDirs(targetPath); + if (targetDirs.length === 0) return true; + const testDirs = new Set(meaningfulDirs(testPath)); + return targetDirs.some((part) => testDirs.has(part)); +} + +/** Compile one implementation-ready packet without inference or verification. */ +export async function compileSecondaryBuilderPacket( + input: BuilderPacketCompilationInput, +): Promise { + const startedAt = Date.now(); + const createdAt = nowIso(input); + const budget = boundedBudget(input); + const expansionLevel = input.expansionLevel ?? 'ADJACENT_DEPENDENCIES'; + const expansionDepth = CONTEXT_EXPANSION_LEVELS.indexOf(expansionLevel); + const repositories = await prepareRepositories(input, createdAt); + const located = locateExplicitTargets(input, repositories); + const prepared: PreparedRetrieval[] = repositories.map((repository) => { + const query = queryFor(input, repository, located.targets, expansionLevel); + return { repository, query, preliminary: rankCandidates(repository.index, query) }; + }); + + let targets = [...located.targets]; + const ambiguity = [...located.ambiguous]; + if (targets.length === 0 && located.missing.length === 0) { + const plausible = prepared + .flatMap((entry) => + entry.preliminary + .filter((candidate) => candidate.entry.kind === 'source' || candidate.entry.kind === 'config') + .slice(0, 2) + .map((candidate) => ({ repository: entry.repository, candidate })), + ) + .sort((left, right) => + right.candidate.score !== left.candidate.score + ? right.candidate.score - left.candidate.score + : `${left.repository.repositoryId}:${left.candidate.path}`.localeCompare( + `${right.repository.repositoryId}:${right.candidate.path}`, + ), + ); + if ( + plausible[0] !== undefined && + plausible[1] !== undefined && + plausible[0].candidate.score === plausible[1].candidate.score && + plausible[0].candidate.entry.module !== plausible[1].candidate.entry.module + ) { + ambiguity.push( + `${plausible[0].repository.repositoryId}:${plausible[0].candidate.path} or ${plausible[1].repository.repositoryId}:${plausible[1].candidate.path}`, + ); + } else if (plausible[0] !== undefined) { + targets = [ + { + repositoryId: plausible[0].repository.repositoryId, + path: plausible[0].candidate.path, + symbols: plausible[0].candidate.entry.symbols.slice(0, 12), + }, + ]; + const matching = prepared.find( + (entry) => entry.repository.repositoryId === plausible[0]?.repository.repositoryId, + ); + if (matching !== undefined) { + matching.query.actionPaths = unique([plausible[0].candidate.path, ...matching.query.actionPaths]); + } + } + } + + const policy = input.config.orchestration.jobs.context.efficiency; + const allocationPolicy = contextAllocationPolicySchema.parse({ + pinnedReserveRatio: policy.pinnedReserveRatio, + durableReserveRatio: policy.durableReserveRatio, + recoveryReserveRatio: policy.recoveryReserveRatio, + deltaReserveRatio: policy.deltaReserveRatio, + workingSetMaxRatio: policy.workingSetMaxRatio, + pointerShapeWorkingSetMaxRatio: policy.pointerShapeWorkingSetMaxRatio, + maxSingleItemRatio: policy.maxSingleItemRatio, + }); + const results: { + repository: PreparedRepository; + result: Awaited>; + }[] = []; + for (const entry of prepared) { + const planId = [ + 'builder', + safeId(input.objectiveNodeId), + safeId(input.workUnit.workUnitId), + `a${input.attempt}`, + safeId(entry.repository.repositoryId), + safeId(expansionLevel.toLowerCase()), + ].join('-').slice(0, 120); + const result = await buildEfficientContext({ + strategy: 'SELECTIVE', + shape: 'MATERIALIZED', + expansionLevel, + canonicalItems: [], + budget: contextBudget(input), + allocationPolicy, + createdAt, + planId, + taskId: input.workUnit.parentTaskId, + jobId: input.jobId, + nodeId: input.objectiveNodeId, + attemptId: `${input.workUnit.workUnitId}-a${input.attempt}-secondary`, + executionLane: 'LOCAL', + executionMode: 'DIRECT_MODEL', + runner: 'secondary-objective-builder', + index: entry.repository.index, + rootDir: entry.repository.rootDir, + query: entry.query, + rankOptions: { + maxCandidates: policy.maxCandidates, + excludedPaths: indexProtectedPaths(input.config), + }, + sectionOptions: { + wholeFileUnderChars: Math.min(policy.wholeFileUnderChars, budget.maxCharactersPerSection), + targetSectionChars: Math.min(policy.targetSectionChars, budget.maxCharactersPerSection), + }, + maxSelectedItems: budget.maxSelectedFiles, + maxPointers: 0, + }); + results.push({ repository: entry.repository, result }); + } + + const targetKeys = new Set(targets.map((target) => `${target.repositoryId}:${target.path}`)); + const selectedSections = results + .flatMap(({ repository, result }) => + result.plan.selectedWorkingItems.flatMap((selected) => { + const selectedKey = `${repository.repositoryId}:${selected.path}`; + const entryKind = repository.index.get(selected.path)?.kind; + if ( + targetKeys.size > 0 && + !targetKeys.has(selectedKey) && + (selected.reason === 'TOKEN_OVERLAP' || selected.reason === 'MODULE_PROXIMITY') + ) { + return []; + } + if (selected.reason === 'TEST_SOURCE_PAIR' && !targetKeys.has(selectedKey)) { + if (entryKind !== 'test') return []; + const matchesTarget = targets.some( + (target) => + target.repositoryId === repository.repositoryId && + (repository.index.testsFor(target.path).includes(selected.path) || + repository.index.sourcesFor(selected.path).includes(target.path) || + pairedTestMatches(selected.path, target.path)), + ); + if (!matchesTarget) return []; + } + const item = itemFor(selected, result); + if (item === undefined || item.content.length > budget.maxCharactersPerSection) return []; + return [ + { + repository, + selected, + kind: entryKind, + section: sectionFrom(repository, selected, item.content), + }, + ]; + }), + ) + .sort((left, right) => { + const leftPriority = priority(targetKeys, left.repository.repositoryId, left.selected, left.kind); + const rightPriority = priority(targetKeys, right.repository.repositoryId, right.selected, right.kind); + if (leftPriority !== rightPriority) return leftPriority - rightPriority; + return right.selected.score - left.selected.score; + }); + + const sourceContext: NormalizedSecondarySourceContext[] = []; + const tests: NormalizedSecondarySourceContext[] = []; + const referencePatterns: NormalizedSecondarySourceContext[] = []; + let usedCharacters = 0; + let selectedFileCount = 0; + for (const selected of selectedSections) { + // This capability-neutral file bound applies across all repositories, + // not once per repository. A target that cannot fit is reported as + // insufficient below instead of silently expanding the packet ceiling. + if (selectedFileCount >= budget.maxSelectedFiles) continue; + if (usedCharacters + selected.section.content.length > budget.maxSourceCharacters) continue; + if (selected.kind === 'test') { + if (tests.length >= budget.maxTests) continue; + tests.push(selected.section); + } else if (selected.selected.reason === 'REFERENCE_PATTERN') { + if (referencePatterns.length >= budget.maxReferencePatterns) continue; + referencePatterns.push(selected.section); + } else { + sourceContext.push(selected.section); + } + usedCharacters += selected.section.content.length; + selectedFileCount += 1; + } + + const selectedKeys = new Set( + [...sourceContext, ...tests, ...referencePatterns].map( + (section) => `${section.repositoryId}:${section.path}`, + ), + ); + const unresolvedTargets = targets.filter( + (target) => !selectedKeys.has(`${target.repositoryId}:${target.path}`), + ); + const dependencyContext = (input.dependencyContext ?? []) + .filter((dependency) => dependency.verificationPassed) + .slice(0, SECONDARY_BUILDER_LIMITS.maxDependencyContext) + .map((dependency) => ({ + workUnitId: dependency.workUnitId, + summary: dependency.summary.slice(0, 2_000), + changedFiles: dependency.changedFiles.slice(0, 60).map((file) => ({ + repositoryId: file.repositoryId ?? 'primary', + path: normalizePath(file.path), + })), + exportedSymbols: [...(dependency.exportedSymbols ?? [])].slice(0, 30), + verificationPassed: true as const, + })); + const missingDependencies = [...(input.missingDependencyIds ?? [])]; + const sourceCharacters = sourceContext.reduce((sum, section) => sum + section.content.length, 0); + const testCharacters = tests.reduce((sum, section) => sum + section.content.length, 0); + const planRefs = results.map(({ result }) => `context/plans/${result.plan.planId}`); + const metrics = secondaryBuilderPacketMetricsSchema.parse({ + indexedFilesConsidered: repositories.reduce((sum, repository) => sum + repository.index.size, 0), + candidateCount: results.reduce((sum, entry) => sum + entry.result.deterministicCandidates.length, 0), + selectedFiles: sourceContext.length + tests.length + referencePatterns.length, + selectedSections: [...sourceContext, ...tests, ...referencePatterns].filter( + (section) => section.startLine !== undefined, + ).length, + sourceCharacters, + testCharacters, + referencePatternCount: referencePatterns.length, + dependencyContextCount: dependencyContext.length, + budgetUtilization: usedCharacters / budget.maxSourceCharacters, + mandatoryRefsRetained: targets.length - unresolvedTargets.length, + expansionDepth, + staleEntriesEncountered: + results.reduce((sum, entry) => sum + entry.result.refreshedPaths.length, 0) + + repositories.reduce((sum, repository) => sum + repository.staleMetadataPaths.length, 0), + selectionDurationMs: Math.max(0, Date.now() - startedAt), + indexReused: repositories.every((repository) => repository.indexReused), + }); + const failureReasons = [ + ...located.missing.map((reference) => `explicit target ${reference} was not found`), + ...unresolvedTargets.map( + (target) => `target ${target.repositoryId}:${target.path} could not be materialized within policy`, + ), + ...missingDependencies.map((dependency) => `verified dependency evidence for ${dependency} is missing`), + ...(sourceContext.length === 0 ? ['no implementable source target was selected'] : []), + ]; + const contextSufficient = ambiguity.length === 0 && failureReasons.length === 0; + const quality = secondaryBuilderPacketQualitySchema.parse({ + explicitTargetResolved: located.explicitEvidence && located.missing.length === 0 && targets.length > 0, + targetAmbiguity: ambiguity.length > 0, + testsFound: tests.length > 0, + verificationHintsAvailable: (input.verificationHints?.length ?? 0) > 0, + referencePatternFound: referencePatterns.length > 0, + dependencyContextComplete: missingDependencies.length === 0, + sourceBudgetUtilization: usedCharacters / budget.maxSourceCharacters, + contextSufficient, + }); + + let plans = results.map(({ repository, result }) => { + const delivered = new Set( + [...sourceContext, ...tests, ...referencePatterns] + .filter((section) => section.repositoryId === repository.repositoryId) + .map((section) => section.path), + ); + const dropped = result.plan.selectedWorkingItems.filter((entry) => !delivered.has(entry.path)); + return contextSelectionPlanSchema.parse({ + ...result.plan, + repositoryId: repository.repositoryId, + selectedWorkingItems: result.plan.selectedWorkingItems + .filter((entry) => delivered.has(entry.path)) + .map((entry) => ({ ...entry, repositoryId: repository.repositoryId })), + pointers: result.plan.pointers.map((entry) => ({ ...entry, repositoryId: repository.repositoryId })), + excludedCandidates: [ + ...result.plan.excludedCandidates.map((entry) => ({ + ...entry, + repositoryId: repository.repositoryId, + })), + ...dropped.map((entry) => ({ + repositoryId: repository.repositoryId, + path: entry.path, + reason: 'RANKED_BELOW_CUTOFF' as const, + score: entry.score, + detail: 'excluded by Builder Packet category or character budget', + })), + ].slice(0, 200), + builderPacket: quality, + }); + }); + const persistPlans = (): void => { + if (input.persist !== false) { + for (const plan of plans) writeContextSelectionPlan(input.workspace, plan); + } + }; + + if (ambiguity.length > 0) { + persistPlans(); + return { + ok: false, + schemaVersion: BUILDER_PACKET_COMPILATION_SCHEMA_VERSION, + failure: { + kind: 'AMBIGUOUS_TARGET', + reasons: ambiguity.slice(0, 8).map((entry) => `equally plausible target: ${entry}`), + }, + plans, + planRefs, + metrics, + quality, + }; + } + if (!contextSufficient) { + persistPlans(); + return { + ok: false, + schemaVersion: BUILDER_PACKET_COMPILATION_SCHEMA_VERSION, + failure: { kind: 'INSUFFICIENT_CONTEXT', reasons: failureReasons.slice(0, 8) }, + plans, + planRefs, + metrics, + quality, + }; + } + + let packet: SecondaryBuilderPacket; + try { + packet = buildSecondaryBuilderPacket({ + projection: input.projection, + sourceContext, + targets: targets.map((target) => { + const selected = [...sourceContext, ...tests, ...referencePatterns].find( + (section) => section.repositoryId === target.repositoryId && section.path === target.path, + ); + return { + repositoryId: target.repositoryId, + path: target.path, + symbols: target.symbols, + reason: selected?.reason ?? 'EXPLICIT_ACTION_REFERENCE', + }; + }), + tests, + referencePatterns, + dependencyContext, + priorFailureEvidence: (input.priorFailureEvidence ?? []).map((entry) => entry.slice(0, 2_000)).slice(0, 20), + verificationHints: (input.verificationHints ?? []).map((hint) => hint.slice(0, 1_000)).slice(0, 30), + retrievalPlanRefs: planRefs, + expansionLevel, + contextMetrics: metrics, + quality, + createdAt, + }); + const inputCharacters = SECONDARY_BUILDER_SYSTEM_PROMPT.length + renderSecondaryBuilderPrompt(packet).length; + const inputCeiling = input.maximumInputCharacters ?? SECONDARY_BUILDER_LIMITS.maxPacketCharacters; + if (inputCharacters > inputCeiling) { + throw new Error(`compiled input is ${inputCharacters} characters; limit is ${inputCeiling}`); + } + } catch (cause) { + const insufficientQuality = secondaryBuilderPacketQualitySchema.parse({ + ...quality, + contextSufficient: false, + }); + plans = plans.map((plan) => contextSelectionPlanSchema.parse({ + ...plan, + builderPacket: insufficientQuality, + })); + persistPlans(); + return { + ok: false, + schemaVersion: BUILDER_PACKET_COMPILATION_SCHEMA_VERSION, + failure: { + kind: 'INSUFFICIENT_CONTEXT', + reasons: [`packet assembly could not fit within policy: ${cause instanceof Error ? cause.message : String(cause)}`.slice(0, 2_000)], + }, + plans, + planRefs, + metrics, + quality: insufficientQuality, + }; + } + persistPlans(); + return { + ok: true, + schemaVersion: BUILDER_PACKET_COMPILATION_SCHEMA_VERSION, + packet, + plans, + planRefs, + metrics, + quality, + repositoryRoots: Object.fromEntries( + repositories.map((repository) => [repository.repositoryId, repository.rootDir]), + ), + }; +} diff --git a/packages/orchestration/src/objectives/index.ts b/packages/orchestration/src/objectives/index.ts index c0c19c5..292269d 100644 --- a/packages/orchestration/src/objectives/index.ts +++ b/packages/orchestration/src/objectives/index.ts @@ -13,3 +13,4 @@ export * from './workers.js'; export * from './integrator.js'; export * from './objective-driver.js'; export * from './secondary-builder.js'; +export * from './builder-packet-compiler.js'; diff --git a/packages/orchestration/src/objectives/objective-driver.ts b/packages/orchestration/src/objectives/objective-driver.ts index d1a900c..f9f75e3 100644 --- a/packages/orchestration/src/objectives/objective-driver.ts +++ b/packages/orchestration/src/objectives/objective-driver.ts @@ -100,6 +100,8 @@ import type { SecondaryBuilderFailureKind, SecondaryObjectiveBuilderSelection, } from './secondary-builder.js'; +import { SecondaryBuilderContextCompiler } from './builder-packet-compiler.js'; +import type { VerifiedDependencyContextInput } from './builder-packet-compiler.js'; import { SECONDARY_BUILDER_ATTEMPT_SCHEMA_VERSION, buildSecondaryBuilderPacket, @@ -442,6 +444,7 @@ interface UnitAttemptContext { interface PreparedAttempt { unitId: string; + unit: WorkUnit; kind: WorkUnit['kind']; attempt: number; workerId: string; @@ -450,6 +453,8 @@ interface PreparedAttempt { baselineCommit: string; record: ReturnType; dependencyPatches: { workUnitId: string; patch: string }[]; + dependencyContext: VerifiedDependencyContextInput[]; + missingDependencyIds: string[]; } interface ExecutedAttempt { @@ -630,7 +635,10 @@ function secondaryFailureCategory(kind: SecondaryBuilderFailureKind): FailureCat case 'VERIFICATION_FAILURE': return 'VERIFICATION_FAILURE'; case 'CONTEXT_TOO_LARGE': + case 'INSUFFICIENT_CONTEXT': return 'CAPABILITY_UNAVAILABLE'; + case 'AMBIGUOUS_TARGET': + return 'AMBIGUITY'; case 'TIMEOUT': return 'TRANSIENT_TRANSPORT'; case 'INFERENCE_UNAVAILABLE': @@ -653,35 +661,78 @@ async function executeSelectedSecondaryBuilder( return { prepared, result: builderFailureResult('secondary builder selection disappeared') }; } - let sourceContext; - try { - sourceContext = - typeof selection.sourceContext === 'function' - ? await selection.sourceContext({ worktreeRoot: worktree.dir, projection: prepared.projection }) - : selection.sourceContext; - } catch (cause) { - const problem = `source context could not be prepared: ${cause instanceof Error ? cause.message : String(cause)}`; - return { - prepared, - result: builderFailureResult(problem), - secondaryFailure: { kind: 'STALE_SOURCE_CONTEXT', problem }, - }; - } - let packet; - try { - packet = buildSecondaryBuilderPacket({ - projection: prepared.projection, - sourceContext, - verificationHints: input.config.verification.commands.map((command) => command.name), + let repositoryRoots: Readonly> = { primary: worktree.dir }; + if (selection.sourceContext !== undefined) { + try { + const sourceContext = + typeof selection.sourceContext === 'function' + ? await selection.sourceContext({ worktreeRoot: worktree.dir, projection: prepared.projection }) + : selection.sourceContext; + packet = buildSecondaryBuilderPacket({ + projection: prepared.projection, + sourceContext, + verificationHints: input.config.verification.commands.map((command) => command.name), + }); + } catch (cause) { + const problem = `source context could not be prepared: ${cause instanceof Error ? cause.message : String(cause)}`; + return { + prepared, + result: builderFailureResult(problem), + secondaryFailure: { kind: 'STALE_SOURCE_CONTEXT', problem }, + }; + } + } else { + const compiler = selection.contextCompiler ?? new SecondaryBuilderContextCompiler(); + let compiled; + try { + compiled = await compiler.compile({ + workspace: input.workspace, + config: input.config, + jobId: input.jobId, + objectiveNodeId: input.node.nodeId, + workUnit: prepared.unit, + projection: prepared.projection, + attempt: prepared.attempt, + worktreeRoot: worktree.dir, + baselineRef: prepared.baselineCommit, + dependencyContext: prepared.dependencyContext, + missingDependencyIds: prepared.missingDependencyIds, + priorFailureEvidence: + prepared.unit.latestFailure === undefined + ? [] + : [`${prepared.unit.latestFailure.category}: ${prepared.unit.latestFailure.message}`], + verificationHints: input.config.verification.commands.map((command) => command.name), + maximumInputCharacters: secondaryBuilderInputCeiling(input.config), + createdAt: nowIso(input), + }); + } catch (cause) { + const problem = `builder packet compilation failed: ${cause instanceof Error ? cause.message : String(cause)}`; + return { + prepared, + result: builderFailureResult(problem), + secondaryFailure: { kind: 'INSUFFICIENT_CONTEXT', problem }, + }; + } + input.recordEvent(compiled.ok ? 'context_selected' : 'context_insufficient', { + nodeId: input.node.nodeId, + workUnitId: prepared.unitId, + attempt: prepared.attempt, + planRefs: compiled.planRefs, + metrics: compiled.metrics, + quality: compiled.quality, + ...(compiled.ok ? {} : { failure: compiled.failure.kind, reasons: compiled.failure.reasons }), }); - } catch (cause) { - const problem = `secondary builder packet was refused: ${cause instanceof Error ? cause.message : String(cause)}`; - return { - prepared, - result: builderFailureResult(problem), - secondaryFailure: { kind: 'CONTEXT_TOO_LARGE', problem }, - }; + if (!compiled.ok) { + const problem = `${compiled.failure.kind}: ${compiled.failure.reasons.join('; ')}`; + return { + prepared, + result: builderFailureResult(problem), + secondaryFailure: { kind: compiled.failure.kind, problem }, + }; + } + packet = compiled.packet; + repositoryRoots = compiled.repositoryRoots; } const inference = @@ -756,6 +807,7 @@ async function executeSelectedSecondaryBuilder( maximumInputCharacters: secondaryBuilderInputCeiling(input.config), maxOutputBytes: input.config.localInference.maxOutputBytes, protectedPaths: input.config.execution.protectedPaths, + repositoryRoots, ...(input.signal !== undefined ? { signal: input.signal } : {}), onExecutionEvent: (event) => { if (event.stage === 'INFERENCE_COMPLETED') { @@ -778,7 +830,7 @@ async function executeSelectedSecondaryBuilder( }); if (!executed.ok) { persist({ - status: 'FAILED', + status: executed.failure.kind === 'INSUFFICIENT_CONTEXT' ? 'CONTEXT_INSUFFICIENT' : 'FAILED', failure: executed.failure, ...(executed.rawOutput !== undefined ? { rawOutput: executed.rawOutput.slice(0, 1_048_576) } : {}), ...(executed.proposal !== undefined ? { proposal: executed.proposal } : {}), @@ -910,6 +962,8 @@ async function prepareUnitAttempt( // Dependency evidence: verified candidates only — never chat. const workEvidence: string[] = []; const dependencyPatches: { workUnitId: string; patch: string }[] = []; + const dependencyContext: VerifiedDependencyContextInput[] = []; + const missingDependencyIds: string[] = []; for (const dependencyId of unit.dependsOn) { const dependency = findUnit(graph, dependencyId); if (dependency === undefined) continue; @@ -942,6 +996,18 @@ async function prepareUnitAttempt( if (patch !== undefined && patch.trim().length > 0) { dependencyPatches.push({ workUnitId: resolved.workUnitId, patch }); } + if (candidate.localVerification.passed) { + dependencyContext.push({ + workUnitId: resolved.workUnitId, + summary: candidate.claims.summary, + changedFiles: candidate.changedFiles.map((file) => ({ repositoryId: 'primary', path: file.path })), + verificationPassed: true, + }); + } else { + missingDependencyIds.push(resolved.workUnitId); + } + } else { + missingDependencyIds.push(resolved.workUnitId); } } @@ -1020,6 +1086,7 @@ async function prepareUnitAttempt( graph: nextGraph, prepared: { unitId, + unit, kind: unit.kind, attempt, workerId, @@ -1028,6 +1095,8 @@ async function prepareUnitAttempt( baselineCommit, record, dependencyPatches, + dependencyContext, + missingDependencyIds, }, }; } diff --git a/packages/orchestration/src/objectives/secondary-builder.ts b/packages/orchestration/src/objectives/secondary-builder.ts index ecc4590..42d216f 100644 --- a/packages/orchestration/src/objectives/secondary-builder.ts +++ b/packages/orchestration/src/objectives/secondary-builder.ts @@ -1,6 +1,7 @@ import { existsSync, lstatSync, readFileSync } from 'node:fs'; import path from 'node:path'; import { z } from 'zod'; +import { CONTEXT_EXPANSION_LEVELS, CONTEXT_SELECTION_REASONS } from '@specbridge/context'; import type { AgentConfig } from '@specbridge/core'; import { effectiveLocalInputCharacters, sha256Hex } from '@specbridge/core'; import type { LocalModelManager } from '@specbridge/runners'; @@ -10,6 +11,7 @@ import { applyValidatedEdits, validateEditPaths, } from '../scheduling/local-execution.js'; +import type { BuilderPacketCompiler } from './builder-packet-compiler.js'; import type { ContextProjection } from './state.js'; /** @@ -23,13 +25,16 @@ import type { ContextProjection } from './state.js'; * runtime. */ -export const SECONDARY_BUILDER_PACKET_SCHEMA_VERSION = '1.0.0'; -export const SECONDARY_BUILDER_RESULT_SCHEMA_VERSION = '1.0.0'; -export const SECONDARY_BUILDER_ATTEMPT_SCHEMA_VERSION = '1.0.0'; +export const SECONDARY_BUILDER_PACKET_SCHEMA_VERSION = '1.1.0'; +export const SECONDARY_BUILDER_RESULT_SCHEMA_VERSION = '1.1.0'; +export const SECONDARY_BUILDER_ATTEMPT_SCHEMA_VERSION = '1.1.0'; export const SECONDARY_BUILDER_LIMITS = { ...LOCAL_EXECUTION_LIMITS, maxSourceFiles: 16, + maxTests: 6, + maxReferencePatterns: 4, + maxDependencyContext: 12, maxSourceFileChars: 32_768, maxSourceBytes: 262_144, maxPacketCharacters: 524_288, @@ -43,14 +48,81 @@ const sha256 = z.string().regex(/^[a-f0-9]{64}$/); export const secondarySourceContextSchema = z .object({ - /** Worktree-relative path. Whole-file source only in Phase 4. */ + /** Repository namespace. Paths are unique only within this identity. */ + repositoryId: shortText.default('primary'), + /** Repository-relative path. */ path: boundedText(SECONDARY_BUILDER_LIMITS.maxPathChars), - /** Hash of the exact UTF-8 content below. */ + /** Hash of the complete current file bytes. */ contentHash: sha256, + /** Hash of the exact bounded content below (whole file or section). */ + sectionHash: sha256.optional(), content: z.string().max(SECONDARY_BUILDER_LIMITS.maxSourceFileChars), + reason: z.enum(CONTEXT_SELECTION_REASONS).default('EXPLICIT_ACTION_REFERENCE'), + startLine: z.number().int().min(1).optional(), + endLine: z.number().int().min(1).optional(), + symbols: z.array(shortText).max(12).default([]), + }) + .strict(); +/** Additive input surface keeps Phase 4's three-field source records valid. */ +export type SecondarySourceContext = z.input; +export type NormalizedSecondarySourceContext = z.output; + +export const secondaryBuilderTargetSchema = z + .object({ + repositoryId: shortText, + path: boundedText(SECONDARY_BUILDER_LIMITS.maxPathChars), + symbols: z.array(shortText).max(12).default([]), + reason: z.enum(CONTEXT_SELECTION_REASONS), + }) + .strict(); + +export const secondaryDependencyContextSchema = z + .object({ + workUnitId: shortText, + summary: boundedText(2_000), + changedFiles: z + .array( + z + .object({ repositoryId: shortText, path: boundedText(SECONDARY_BUILDER_LIMITS.maxPathChars) }) + .strict(), + ) + .max(60), + exportedSymbols: z.array(shortText).max(30).default([]), + verificationPassed: z.literal(true), + }) + .strict(); + +export const secondaryBuilderPacketMetricsSchema = z + .object({ + indexedFilesConsidered: z.number().int().min(0), + candidateCount: z.number().int().min(0), + selectedFiles: z.number().int().min(0), + selectedSections: z.number().int().min(0), + sourceCharacters: z.number().int().min(0), + testCharacters: z.number().int().min(0), + referencePatternCount: z.number().int().min(0), + dependencyContextCount: z.number().int().min(0), + budgetUtilization: z.number().min(0), + mandatoryRefsRetained: z.number().int().min(0), + expansionDepth: z.number().int().min(0).max(4), + staleEntriesEncountered: z.number().int().min(0), + selectionDurationMs: z.number().int().min(0), + indexReused: z.boolean(), + }) + .strict(); + +export const secondaryBuilderPacketQualitySchema = z + .object({ + explicitTargetResolved: z.boolean(), + targetAmbiguity: z.boolean(), + testsFound: z.boolean(), + verificationHintsAvailable: z.boolean(), + referencePatternFound: z.boolean(), + dependencyContextComplete: z.boolean(), + sourceBudgetUtilization: z.number().min(0), + contextSufficient: z.boolean(), }) .strict(); -export type SecondarySourceContext = z.infer; const projectedContractSchema = z .object({ @@ -70,7 +142,10 @@ export const secondaryBuilderPacketSchema = z projectionHash: sha256, contractSnapshotHash: sha256, sourceContextHash: sha256, + /** Semantic identity; excludes createdAt and observational metrics. */ + contentHash: sha256, packetHash: sha256, + createdAt: shortText.optional(), objective: z .object({ nodeId: shortText, @@ -104,6 +179,25 @@ export const secondaryBuilderPacketSchema = z }) .strict(), sourceContext: z.array(secondarySourceContextSchema).max(SECONDARY_BUILDER_LIMITS.maxSourceFiles), + targets: z.array(secondaryBuilderTargetSchema).max(SECONDARY_BUILDER_LIMITS.maxSourceFiles), + tests: z.array(secondarySourceContextSchema).max(SECONDARY_BUILDER_LIMITS.maxTests), + referencePatterns: z + .array(secondarySourceContextSchema) + .max(SECONDARY_BUILDER_LIMITS.maxReferencePatterns), + dependencyContext: z + .array(secondaryDependencyContextSchema) + .max(SECONDARY_BUILDER_LIMITS.maxDependencyContext), + priorFailureEvidence: z.array(boundedText(2_000)).max(20), + retrievalPlanRef: shortText, + retrievalPlanRefs: z.array(shortText).max(12), + expansion: z + .object({ + level: z.enum(CONTEXT_EXPANSION_LEVELS), + remainingLevels: z.number().int().min(0).max(4), + }) + .strict(), + contextMetrics: secondaryBuilderPacketMetricsSchema, + quality: secondaryBuilderPacketQualitySchema, forbiddenChanges: z.array(boundedText(1_000)).max(30), verificationHints: z.array(boundedText(1_000)).max(30), }) @@ -125,22 +219,37 @@ export type SecondaryStructuredEdit = z.infer { + if (result.status === 'NEEDS_MORE_CONTEXT' && result.needsMoreContextReasons === undefined) { + context.addIssue({ code: z.ZodIssueCode.custom, path: ['needsMoreContextReasons'], message: 'required when status is NEEDS_MORE_CONTEXT' }); + } + if (result.status === 'NEEDS_MORE_CONTEXT' && result.edits.length > 0) { + context.addIssue({ code: z.ZodIssueCode.custom, path: ['edits'], message: 'must be empty when more context is required' }); + } + }); export type SecondaryBuilderResult = z.infer; export const SECONDARY_BUILDER_JSON_SCHEMA: Record = { type: 'object', additionalProperties: false, - required: ['schemaVersion', 'summary', 'edits'], + required: ['schemaVersion', 'status', 'summary', 'edits'], properties: { schemaVersion: { type: 'string', const: SECONDARY_BUILDER_RESULT_SCHEMA_VERSION }, + status: { type: 'string', enum: ['EDITS', 'NEEDS_MORE_CONTEXT'] }, summary: { type: 'string', minLength: 1, maxLength: SECONDARY_BUILDER_LIMITS.maxSummaryChars }, edits: { type: 'array', @@ -161,6 +270,12 @@ export const SECONDARY_BUILDER_JSON_SCHEMA: Record = { maxItems: SECONDARY_BUILDER_LIMITS.maxNotes, items: { type: 'string', maxLength: SECONDARY_BUILDER_LIMITS.maxNoteChars }, }, + needsMoreContextReasons: { + type: 'array', + minItems: 1, + maxItems: 8, + items: { type: 'string', minLength: 1, maxLength: SECONDARY_BUILDER_LIMITS.maxNoteChars }, + }, }, }; @@ -168,6 +283,9 @@ export const SECONDARY_BUILDER_SYSTEM_PROMPT = [ 'You are a bounded SECONDARY OBJECTIVE BUILDER, not an agent harness.', 'You have no shell, git, filesystem, package-manager, test, credential, or tool access.', 'The packet contains all approved truth and source bytes you may use.', + 'You have been given the repository context selected for this task; do not explore beyond it.', + 'Do not invent files, APIs, symbols, or architecture unsupported by the packet.', + 'If the packet is insufficient to implement safely, return NEEDS_MORE_CONTEXT with bounded reasons and no edits.', 'Return exactly one JSON document matching the supplied schema.', 'Return complete UTF-8 file contents using only CREATE or REPLACE.', 'Never return Markdown, diffs, commands, deletes, renames, symlinks, or authority/config edits.', @@ -214,12 +332,14 @@ export interface SecondaryModelInference { export interface SecondaryObjectiveBuilderSelection { selectionReason: string; workUnitIds?: readonly string[] | undefined; - sourceContext: + sourceContext?: | readonly SecondarySourceContext[] | ((input: { worktreeRoot: string; projection: ContextProjection; }) => readonly SecondarySourceContext[] | Promise); + /** Optional provider-neutral compiler seam; the managed deterministic compiler is the default. */ + contextCompiler?: BuilderPacketCompiler | undefined; /** Deterministic fake/custom provider seam; omitted means managed local. */ inference?: SecondaryModelInference | undefined; } @@ -294,6 +414,8 @@ export const SECONDARY_BUILDER_FAILURES = [ 'VERIFICATION_FAILURE', 'TIMEOUT', 'CONTEXT_TOO_LARGE', + 'INSUFFICIENT_CONTEXT', + 'AMBIGUOUS_TARGET', 'CANCELLED', ] as const; export type SecondaryBuilderFailureKind = (typeof SECONDARY_BUILDER_FAILURES)[number]; @@ -304,6 +426,7 @@ export const SECONDARY_BUILDER_ATTEMPT_STATUSES = [ 'PROPOSAL_VALIDATED', 'EDITS_APPLIED', 'VERIFICATION_FAILED', + 'CONTEXT_INSUFFICIENT', 'CANDIDATE_READY', 'FAILED', ] as const; @@ -413,6 +536,8 @@ export interface ExecuteSecondaryBuilderInput { maximumInputCharacters: number; maxOutputBytes: number; protectedPaths?: readonly string[] | undefined; + /** Roots for explicitly justified secondary repositories in a cross-repo packet. */ + repositoryRoots?: Readonly> | undefined; signal?: AbortSignal | undefined; /** Synchronous durability hook called before mutation and after application. */ onExecutionEvent?: ((event: SecondaryBuilderExecutionEvent) => void) | undefined; @@ -433,16 +558,25 @@ function stableStringify(value: unknown): string { return JSON.stringify(stable(value)); } -function packetBody(packet: Omit): string { - return stableStringify(packet); +function packetBody(packet: Omit): string { + const { createdAt: _createdAt, contextMetrics: _contextMetrics, ...semantic } = packet; + return stableStringify(semantic); } export function sourceContextHashOf(sourceContext: readonly SecondarySourceContext[]): string { + const normalized = z.array(secondarySourceContextSchema).parse(sourceContext); return sha256Hex( stableStringify( - [...sourceContext] - .map((entry) => ({ path: entry.path.replace(/\\/g, '/'), contentHash: entry.contentHash })) - .sort((left, right) => left.path.localeCompare(right.path)), + normalized + .map((entry) => ({ + repositoryId: entry.repositoryId, + path: entry.path.replace(/\\/g, '/'), + contentHash: entry.contentHash, + sectionHash: entry.sectionHash ?? sha256Hex(entry.content), + startLine: entry.startLine ?? null, + endLine: entry.endLine ?? null, + })) + .sort((left, right) => `${left.repositoryId}:${left.path}`.localeCompare(`${right.repositoryId}:${right.path}`)), ), ); } @@ -489,9 +623,22 @@ export function buildSecondaryBuilderPacket(input: { sourceContext: readonly SecondarySourceContext[]; forbiddenChanges?: readonly string[] | undefined; verificationHints?: readonly string[] | undefined; + targets?: readonly z.input[] | undefined; + tests?: readonly SecondarySourceContext[] | undefined; + referencePatterns?: readonly SecondarySourceContext[] | undefined; + dependencyContext?: readonly z.input[] | undefined; + priorFailureEvidence?: readonly string[] | undefined; + retrievalPlanRefs?: readonly string[] | undefined; + expansionLevel?: (typeof CONTEXT_EXPANSION_LEVELS)[number] | undefined; + contextMetrics?: z.input | undefined; + quality?: z.input | undefined; + createdAt?: string | undefined; }): SecondaryBuilderPacket { const sourceContext = z.array(secondarySourceContextSchema).parse(input.sourceContext); - const sourceBytes = sourceContext.reduce( + const tests = z.array(secondarySourceContextSchema).parse(input.tests ?? []); + const referencePatterns = z.array(secondarySourceContextSchema).parse(input.referencePatterns ?? []); + const allSourceContext = [...sourceContext, ...tests, ...referencePatterns]; + const sourceBytes = allSourceContext.reduce( (total, entry) => total + Buffer.byteLength(entry.content, 'utf8'), 0, ); @@ -503,7 +650,7 @@ export function buildSecondaryBuilderPacket(input: { packetId: `${input.projection.projectionId}-secondary`, projectionHash: input.projection.contentHash, contractSnapshotHash: input.projection.contractSnapshotHash, - sourceContextHash: sourceContextHashOf(sourceContext), + sourceContextHash: sourceContextHashOf(allSourceContext), objective: { nodeId: input.projection.objectiveNodeId, taskId: input.projection.objective.taskId, @@ -527,15 +674,60 @@ export function buildSecondaryBuilderPacket(input: { priorWorkEvidence: [...input.projection.workEvidence], }, sourceContext, + targets: z.array(secondaryBuilderTargetSchema).parse( + input.targets ?? sourceContext.map((entry) => ({ + repositoryId: entry.repositoryId, + path: entry.path, + symbols: entry.symbols, + reason: entry.reason, + })), + ), + tests, + referencePatterns, + dependencyContext: z.array(secondaryDependencyContextSchema).parse(input.dependencyContext ?? []), + priorFailureEvidence: [...(input.priorFailureEvidence ?? [])].slice(0, 20), + retrievalPlanRef: input.retrievalPlanRefs?.[0] ?? 'manual-source-selection', + retrievalPlanRefs: [...(input.retrievalPlanRefs ?? ['manual-source-selection'])].slice(0, 12), + expansion: { + level: input.expansionLevel ?? 'MINIMAL_BOOTSTRAP', + remainingLevels: Math.max(0, 4 - CONTEXT_EXPANSION_LEVELS.indexOf(input.expansionLevel ?? 'MINIMAL_BOOTSTRAP')), + }, + contextMetrics: secondaryBuilderPacketMetricsSchema.parse(input.contextMetrics ?? { + indexedFilesConsidered: sourceContext.length, + candidateCount: sourceContext.length, + selectedFiles: allSourceContext.length, + selectedSections: allSourceContext.filter((entry) => entry.startLine !== undefined).length, + sourceCharacters: sourceContext.reduce((sum, entry) => sum + entry.content.length, 0), + testCharacters: tests.reduce((sum, entry) => sum + entry.content.length, 0), + referencePatternCount: referencePatterns.length, + dependencyContextCount: input.dependencyContext?.length ?? 0, + budgetUtilization: sourceBytes / SECONDARY_BUILDER_LIMITS.maxSourceBytes, + mandatoryRefsRetained: sourceContext.length, + expansionDepth: CONTEXT_EXPANSION_LEVELS.indexOf(input.expansionLevel ?? 'MINIMAL_BOOTSTRAP'), + staleEntriesEncountered: 0, + selectionDurationMs: 0, + indexReused: false, + }), + quality: secondaryBuilderPacketQualitySchema.parse(input.quality ?? { + explicitTargetResolved: sourceContext.length > 0, + targetAmbiguity: false, + testsFound: tests.length > 0, + verificationHintsAvailable: (input.verificationHints?.length ?? 0) > 0, + referencePatternFound: referencePatterns.length > 0, + dependencyContextComplete: true, + sourceBudgetUtilization: sourceBytes / SECONDARY_BUILDER_LIMITS.maxSourceBytes, + contextSufficient: sourceContext.length > 0, + }), forbiddenChanges: [ 'Do not modify .git, .kiro, .specbridge, .codex, .claude, credentials, approvals, contracts, mission state, or closure state.', 'Do not delete, rename, chmod, create symlinks, emit commands, or request tools.', ...(input.forbiddenChanges ?? []), ], verificationHints: [...(input.verificationHints ?? [])], + ...(input.createdAt !== undefined ? { createdAt: input.createdAt } : {}), }; - const packetHash = sha256Hex(packetBody(base as Omit)); - const packet = secondaryBuilderPacketSchema.parse({ ...base, packetHash }); + const contentHash = sha256Hex(packetBody(base as Omit)); + const packet = secondaryBuilderPacketSchema.parse({ ...base, contentHash, packetHash: contentHash }); if (JSON.stringify(packet).length > SECONDARY_BUILDER_LIMITS.maxPacketCharacters) { throw new Error(`secondary builder packet exceeds ${SECONDARY_BUILDER_LIMITS.maxPacketCharacters} characters`); } @@ -543,10 +735,10 @@ export function buildSecondaryBuilderPacket(input: { } function validatePacketIdentity(packet: SecondaryBuilderPacket): string | undefined { - const { packetHash: claimed, ...body } = packet; + const { packetHash: claimed, contentHash, ...body } = packet; const actual = sha256Hex(packetBody(body)); - if (actual !== claimed) return 'the packet hash does not match its contents'; - if (sourceContextHashOf(packet.sourceContext) !== packet.sourceContextHash) { + if (actual !== claimed || actual !== contentHash) return 'the packet semantic hash does not match its contents'; + if (sourceContextHashOf([...packet.sourceContext, ...packet.tests, ...packet.referencePatterns]) !== packet.sourceContextHash) { return 'the source-context manifest hash does not match the packet'; } return undefined; @@ -556,24 +748,54 @@ function validateSourceFreshness( worktreeRoot: string, packet: SecondaryBuilderPacket, protectedPaths: readonly string[], + repositoryRoots: Readonly>, ): string[] { const problems: string[] = []; - const pathFailures = validateEditPaths( - { rootDir: worktreeRoot }, - packet.sourceContext.map((entry) => ({ path: entry.path, content: '' })), - protectedPaths, - ); - problems.push(...pathFailures.map((failure) => `${failure.path}: ${failure.problem}`)); - if (pathFailures.length > 0) return problems; - for (const source of packet.sourceContext) { - const target = path.join(worktreeRoot, source.path.replace(/\\/g, '/')); + const allSources = [...packet.sourceContext, ...packet.tests, ...packet.referencePatterns]; + for (const repositoryId of new Set(allSources.map((source) => source.repositoryId))) { + const repositoryRoot = repositoryRoots[repositoryId] ?? (repositoryId === 'primary' ? worktreeRoot : undefined); + if (repositoryRoot === undefined) { + problems.push(`${repositoryId}: repository root is unavailable`); + continue; + } + const pathFailures = validateEditPaths( + { rootDir: repositoryRoot }, + allSources + .filter((entry) => entry.repositoryId === repositoryId) + .map((entry) => ({ path: entry.path, content: '' })), + protectedPaths, + ); + problems.push(...pathFailures.map((failure) => `${repositoryId}:${failure.path}: ${failure.problem}`)); + } + if (problems.length > 0) return problems; + for (const source of allSources) { + const repositoryRoot = repositoryRoots[source.repositoryId] ?? (source.repositoryId === 'primary' ? worktreeRoot : undefined); + if (repositoryRoot === undefined) { + problems.push(`${source.repositoryId}:${source.path}: repository root is unavailable`); + continue; + } + const normalizedPath = source.path.replace(/\\/g, '/'); + const resolvedRoot = path.resolve(repositoryRoot); + const target = path.resolve(resolvedRoot, normalizedPath); + if ( + path.isAbsolute(normalizedPath) || + normalizedPath === '..' || + normalizedPath.startsWith('../') || + (target !== resolvedRoot && !target.startsWith(`${resolvedRoot}${path.sep}`)) + ) { + problems.push(`${source.repositoryId}:${source.path}: source path escapes its repository`); + continue; + } try { if (!lstatSync(target).isFile()) { problems.push(`${source.path}: source is not a regular file`); continue; } const current = readFileSync(target, 'utf8'); - if (sha256Hex(current) !== source.contentHash || current !== source.content) { + if (sha256Hex(source.content) !== (source.sectionHash ?? sha256Hex(source.content))) { + problems.push(`${source.repositoryId}:${source.path}: selected section hash does not match packet content`); + } + if (sha256Hex(current) !== source.contentHash || (source.startLine === undefined && current !== source.content)) { problems.push(`${source.path}: repository bytes changed after source context was assembled`); } } catch { @@ -587,7 +809,7 @@ function emptyTelemetry(packet: SecondaryBuilderPacket): SecondaryBuilderTelemet return { inputCharacters: 0, outputBytes: 0, - sourceFiles: packet.sourceContext.length, + sourceFiles: packet.sourceContext.length + packet.tests.length + packet.referencePatterns.length, editedFiles: 0, durationMs: 0, inputTokens: null, @@ -622,14 +844,15 @@ export async function executeSecondaryObjectiveBuilder( const packet = parsedPacket.data; const identityProblem = validatePacketIdentity(packet); if (identityProblem !== undefined) return failure(packet, 'STALE_SOURCE_CONTEXT', identityProblem); - const stale = validateSourceFreshness(input.worktreeRoot, packet, input.protectedPaths ?? []); + const stale = validateSourceFreshness( + input.worktreeRoot, + packet, + input.protectedPaths ?? [], + input.repositoryRoots ?? {}, + ); if (stale.length > 0) return failure(packet, 'STALE_SOURCE_CONTEXT', stale.slice(0, 5).join('; ')); - const userPrompt = [ - 'Implement the approved WorkUnit using only the bounded packet below.', - 'Return the SECONDARY_BUILDER_RESULT JSON document now.', - stableStringify(packet), - ].join('\n\n'); + const userPrompt = renderSecondaryBuilderPrompt(packet); const inputCharacters = SECONDARY_BUILDER_SYSTEM_PROMPT.length + userPrompt.length; if (inputCharacters > input.maximumInputCharacters) { return failure( @@ -677,7 +900,7 @@ export async function executeSecondaryObjectiveBuilder( let telemetry: SecondaryBuilderTelemetry = { inputCharacters, outputBytes, - sourceFiles: packet.sourceContext.length, + sourceFiles: packet.sourceContext.length + packet.tests.length + packet.referencePatterns.length, editedFiles: 0, durationMs: inferred.durationMs, inputTokens: inferred.usage?.inputTokens ?? null, @@ -709,6 +932,14 @@ export async function executeSecondaryObjectiveBuilder( const proposal = parsed.data; telemetry = { ...telemetry, editedFiles: proposal.edits.length }; input.onExecutionEvent?.({ stage: 'PROPOSAL_VALIDATED', proposal, telemetry }); + if (proposal.status === 'NEEDS_MORE_CONTEXT') { + return failure( + packet, + 'INSUFFICIENT_CONTEXT', + `secondary builder requested more context: ${proposal.needsMoreContextReasons?.join('; ') ?? proposal.summary}`, + { rawOutput: inferred.text, proposal, telemetry }, + ); + } if (proposal.edits.length === 0) { return failure(packet, 'EMPTY_EDIT_SET', 'an implementation WorkUnit must propose at least one edit', { rawOutput: inferred.text, @@ -773,6 +1004,50 @@ export async function executeSecondaryObjectiveBuilder( return { ok: true, proposal, appliedFiles, telemetry }; } +/** Explicit, stable prompt sections keep requirements distinct from examples. */ +export function renderSecondaryBuilderPrompt(packet: SecondaryBuilderPacket): string { + const renderSources = (entries: readonly NormalizedSecondarySourceContext[]): string => + entries.length === 0 + ? '(none)' + : entries + .map((entry) => + [ + `--- ${entry.repositoryId}:${entry.path}${entry.startLine !== undefined ? ` lines ${entry.startLine}-${entry.endLine}` : ''} ---`, + `Selected because: ${entry.reason}`, + entry.content, + ].join('\n'), + ) + .join('\n\n'); + return [ + 'PACKET IDENTITY', + stableStringify({ workUnitId: packet.workUnit.workUnitId, packetHash: packet.packetHash }), + 'TASK', + `${packet.workUnit.title}\n${packet.workUnit.goal}`, + 'REQUIRED BEHAVIOR', + [...packet.objective.acceptance, ...packet.approvedContext.contracts.flatMap((contract) => contract.requirements)].join('\n- '), + 'TARGETS', + packet.targets.map((target) => `${target.repositoryId}:${target.path} (${target.reason})`).join('\n'), + 'CURRENT SOURCE', + renderSources(packet.sourceContext), + 'RELEVANT TESTS', + renderSources(packet.tests), + 'REFERENCE PATTERNS (examples only; not requirements)', + renderSources(packet.referencePatterns), + 'DEPENDENCY CONTEXT', + packet.dependencyContext.map((dependency) => `${dependency.workUnitId}: ${dependency.summary}`).join('\n') || '(none)', + 'CONSTRAINTS', + packet.approvedContext.constraints.join('\n'), + 'DO NOT CHANGE', + packet.forbiddenChanges.join('\n'), + 'PRIOR FAILURE EVIDENCE', + packet.priorFailureEvidence.join('\n') || '(none)', + 'VERIFICATION HINTS', + packet.verificationHints.join('\n') || '(none configured)', + 'OUTPUT CONTRACT', + 'Return exactly one SECONDARY_BUILDER_RESULT JSON document. Use status EDITS with complete CREATE/REPLACE contents, or NEEDS_MORE_CONTEXT with no edits when the supplied evidence is insufficient.', + ].join('\n\n'); +} + /** Effective production ceiling retains the existing localInference semantics. */ export function secondaryBuilderInputCeiling(config: AgentConfig): number { return effectiveLocalInputCharacters(config.localInference); diff --git a/scripts/check-public-contracts.mjs b/scripts/check-public-contracts.mjs index 4e4a56a..5988129 100644 --- a/scripts/check-public-contracts.mjs +++ b/scripts/check-public-contracts.mjs @@ -233,6 +233,7 @@ async function buildSnapshots() { secondaryBuilderPacket: orchestration.SECONDARY_BUILDER_PACKET_SCHEMA_VERSION, secondaryBuilderResult: orchestration.SECONDARY_BUILDER_RESULT_SCHEMA_VERSION, secondaryBuilderAttempt: orchestration.SECONDARY_BUILDER_ATTEMPT_SCHEMA_VERSION, + builderPacketCompilation: orchestration.BUILDER_PACKET_COMPILATION_SCHEMA_VERSION, // Mission families (persisted under .specbridge/missions/). missionState: mission.MISSION_STATE_SCHEMA_VERSION, missionCoverage: mission.MISSION_COVERAGE_SCHEMA_VERSION, @@ -360,6 +361,7 @@ async function buildSnapshots() { evaluationLayers: [...orchestration.EVALUATION_LAYERS].sort(), secondaryBuilderFailures: [...orchestration.SECONDARY_BUILDER_FAILURES].sort(), secondaryBuilderAttemptStatuses: [...orchestration.SECONDARY_BUILDER_ATTEMPT_STATUSES].sort(), + builderPacketCompilationFailures: [...orchestration.BUILDER_PACKET_COMPILATION_FAILURES].sort(), // Survival-runtime vocabulary (vNext.1; additive within 1.x). taskAttemptStatuses: [...orchestration.TASK_ATTEMPT_STATUSES].sort(), taskCheckpointReasons: [...orchestration.TASK_CHECKPOINT_REASONS].sort(), diff --git a/tests/context/context-efficiency.test.ts b/tests/context/context-efficiency.test.ts index b993099..bc7fae2 100644 --- a/tests/context/context-efficiency.test.ts +++ b/tests/context/context-efficiency.test.ts @@ -179,6 +179,15 @@ describe('authority-aware deduplication', () => { expect(result.savedChars).toBe('the same body'.length); }); + it('retains byte-identical repository artifacts at distinct paths', () => { + const result = deduplicateItems([ + item({ layer: 'WORKING_SET', content: 'export const stub = true;', dedupeKey: 'repo:src/a.ts' }), + item({ layer: 'WORKING_SET', content: 'export const stub = true;', dedupeKey: 'repo:src/b.ts' }), + ]); + expect(result.items).toHaveLength(2); + expect(result.duplicates).toHaveLength(0); + }); + it('keeps the higher-authority representation when two items conflict', () => { const canonical = item({ layer: 'COMPACTED_HISTORY', diff --git a/tests/orchestration/builder-packet-compiler.test.ts b/tests/orchestration/builder-packet-compiler.test.ts new file mode 100644 index 0000000..fefa804 --- /dev/null +++ b/tests/orchestration/builder-packet-compiler.test.ts @@ -0,0 +1,449 @@ +import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'; +import path from 'node:path'; +import { describe, expect, it } from 'vitest'; +import { sha256Hex } from '@specbridge/core'; +import { RepositoryContextIndex, buildRepositoryIndex } from '@specbridge/context'; +import type { ContextProjection, WorkUnit } from '@specbridge/orchestration'; +import { + SECONDARY_BUILDER_RESULT_SCHEMA_VERSION, + compileSecondaryBuilderPacket, + contextProjectionSchema, + contractSnapshotHashOf, + executeSecondaryObjectiveBuilder, + workUnitSchema, +} from '@specbridge/orchestration'; +import { emptyTempDir } from '../helpers.js'; +import { setupExecutionFixture } from '../helpers-execution.js'; + +const NOW = '2026-08-30T00:00:00.000Z'; + +function writeFiles(root: string, files: Record): void { + for (const [relativePath, content] of Object.entries(files)) { + const target = path.join(root, relativePath); + mkdirSync(path.dirname(target), { recursive: true }); + writeFileSync(target, content, 'utf8'); + } +} + +function index(root: string, protectedPaths: readonly string[] = []): RepositoryContextIndex { + return new RepositoryContextIndex( + buildRepositoryIndex({ rootDir: root, now: NOW, protectedPaths, respectGitignore: true }), + ); +} + +function unit(overrides: Partial = {}): WorkUnit { + return workUnitSchema.parse({ + workUnitId: 'wu-1', + objectiveNodeId: 'node-1', + parentTaskId: 'task-1', + kind: 'build', + title: 'Update FooMapper', + goal: 'Update FooMapper deterministically.', + dependsOn: [], + expectedArtifacts: ['src/foo-mapper.ts'], + relevantContractIds: [], + relevantAdrIds: [], + relevantConstitutionRuleIds: [], + expectedAreas: ['src'], + status: 'READY', + attempt: 0, + ...overrides, + }); +} + +function projection(workUnit: WorkUnit): ContextProjection { + const body = { + schemaVersion: '1.0.0', + projectionId: `${workUnit.workUnitId}-a01`, + jobId: 'job-1', + objectiveNodeId: workUnit.objectiveNodeId, + workUnitId: workUnit.workUnitId, + attempt: 1, + createdAt: NOW, + constitution: { version: 1, rules: [] }, + objective: { + taskId: workUnit.parentTaskId, + title: workUnit.title, + acceptance: ['The mapper follows the approved DTO contract.'], + }, + workUnit: { + title: workUnit.title, + goal: workUnit.goal, + kind: workUnit.kind, + expectedArtifacts: workUnit.expectedArtifacts, + expectedAreas: workUnit.expectedAreas, + }, + contracts: [], + adrs: [], + decisions: [], + specExcerpts: [], + workEvidence: [], + contractSnapshotHash: contractSnapshotHashOf([], 1), + }; + return contextProjectionSchema.parse({ ...body, contentHash: sha256Hex(JSON.stringify(body)) }); +} + +function compilerInput( + root: string, + workUnit: WorkUnit, + repositoryIndex = index(root), + overrides: Partial[0]> = {}, +) { + const fixture = setupExecutionFixture(); + return { + workspace: fixture.workspace, + config: fixture.config, + jobId: 'job-1', + objectiveNodeId: workUnit.objectiveNodeId, + workUnit, + projection: projection(workUnit), + attempt: 1, + worktreeRoot: root, + repositories: [ + { + repositoryId: 'primary', + rootDir: root, + index: repositoryIndex, + indexReused: true, + }, + ], + verificationHints: ['targeted-test'], + createdAt: NOW, + persist: false, + ...overrides, + }; +} + +describe('Phase 5 Builder Packet compiler', () => { + it('retrieves an explicit target, paired test, direct dependency, and bounded reference pattern', async () => { + const root = emptyTempDir(); + writeFiles(root, { + 'src/foo-mapper.ts': + "import { FooRepository } from './foo-repository.js';\nexport class FooMapper { map() { return FooRepository.name; } }\n", + 'src/foo-repository.ts': 'export class FooRepository { static name = "repo"; }\n', + 'src/existing-bar-mapper.ts': 'export class ExistingBarMapper { map() { return "bar"; } }\n', + 'src/foo-service.ts': 'export class FooService {}\n', + 'tests/foo-mapper.test.ts': + "import { FooMapper } from '../src/foo-mapper.js';\ntest('maps', () => expect(new FooMapper().map()).toBe('repo'));\n", + }); + const result = await compileSecondaryBuilderPacket( + compilerInput(root, unit(), index(root), { + dependencyContext: [ + { + workUnitId: 'wu-dependency', + summary: 'Verified repository interface used by the mapper.', + changedFiles: [{ path: 'src/foo-repository.ts' }], + verificationPassed: true, + }, + ], + }), + ); + expect(result.ok).toBe(true); + if (!result.ok) return; + expect(result.packet.targets.map((target) => target.path)).toEqual(['src/foo-mapper.ts']); + expect(result.packet.sourceContext.map((entry) => entry.path)).toContain('src/foo-repository.ts'); + expect(result.packet.tests.map((entry) => entry.path)).toContain('tests/foo-mapper.test.ts'); + expect(result.packet.referencePatterns.map((entry) => entry.path)).toContain( + 'src/existing-bar-mapper.ts', + ); + expect(result.packet.dependencyContext).toMatchObject([ + { workUnitId: 'wu-dependency', verificationPassed: true }, + ]); + expect(result.packet.sourceContext.filter((entry) => entry.path === 'src/foo-service.ts')).toEqual([]); + for (const entry of [ + ...result.packet.sourceContext, + ...result.packet.tests, + ...result.packet.referencePatterns, + ]) { + expect(entry).toMatchObject({ repositoryId: 'primary' }); + expect(entry.reason).toBeTruthy(); + expect(entry.contentHash).toMatch(/^[a-f0-9]{64}$/); + expect(entry.sectionHash).toMatch(/^[a-f0-9]{64}$/); + } + }); + + it('discovers conventional Java FooMapperTest-style test names', async () => { + const root = emptyTempDir(); + writeFiles(root, { + 'src/FooMapper.java': 'public class FooMapper { public int map() { return 1; } }\n', + 'tests/FooMapperTest.java': 'public class FooMapperTest { void maps() { new FooMapper().map(); } }\n', + }); + const workUnit = unit({ + title: 'Update FooMapper', + goal: 'Update FooMapper deterministically.', + expectedArtifacts: ['src/FooMapper.java'], + }); + const result = await compileSecondaryBuilderPacket(compilerInput(root, workUnit)); + expect(result.ok).toBe(true); + if (!result.ok) return; + expect(result.packet.tests.map((entry) => entry.path)).toContain('tests/FooMapperTest.java'); + }); + + it('materializes current worktree bytes when a reusable index entry is stale', async () => { + const root = emptyTempDir(); + writeFiles(root, { 'src/foo-mapper.ts': 'export class FooMapper { old = true; }\n' }); + const stale = index(root); + writeFiles(root, { 'src/foo-mapper.ts': 'export class FooMapper { current = true; }\n' }); + const result = await compileSecondaryBuilderPacket(compilerInput(root, unit(), stale)); + expect(result.ok).toBe(true); + if (!result.ok) return; + expect(result.packet.sourceContext[0]?.content).toContain('current = true'); + expect(result.packet.sourceContext[0]?.contentHash).toBe( + sha256Hex(readFileSync(path.join(root, 'src/foo-mapper.ts'), 'utf8')), + ); + expect(result.metrics.staleEntriesEncountered).toBeGreaterThan(0); + }); + + it('uses bounded prior failure paths as strong retrieval evidence', async () => { + const root = emptyTempDir(); + writeFiles(root, { + 'src/foo-mapper.ts': 'export class FooMapper { map() { return 1; } }\n', + 'src/other.ts': 'export const other = true;\n', + }); + const result = await compileSecondaryBuilderPacket( + compilerInput(root, unit(), index(root), { + priorFailureEvidence: [ + 'TypeScript compiler error at src/foo-mapper.ts:1: FooMapper.map returned the wrong type.', + 'x'.repeat(3_000), + ], + }), + ); + expect(result.ok).toBe(true); + if (!result.ok) return; + const target = result.packet.sourceContext.find((entry) => entry.path === 'src/foo-mapper.ts'); + expect(target?.reason).toBe('EXPLICIT_FAILURE_REFERENCE'); + expect(target?.startLine).toBeUndefined(); + expect(result.packet.priorFailureEvidence).toHaveLength(2); + expect(result.packet.priorFailureEvidence[1]).toHaveLength(2_000); + }); + + it('reports a missing explicit target honestly', async () => { + const root = emptyTempDir(); + writeFiles(root, { 'src/other.ts': 'export const other = true;\n' }); + const workUnit = unit({ + goal: 'Update MissingMapper in place.', + expectedArtifacts: ['src/missing-mapper.ts'], + }); + const result = await compileSecondaryBuilderPacket(compilerInput(root, workUnit)); + expect(result.ok).toBe(false); + if (result.ok) return; + expect(result.failure.kind).toBe('INSUFFICIENT_CONTEXT'); + expect(result.failure.reasons.join(' ')).toContain('missing-mapper.ts'); + }); + + it('never materializes a protected matching target', async () => { + const root = emptyTempDir(); + writeFiles(root, { + 'protected/secret-mapper.ts': 'export class SecretMapper { token = "credential"; }\n', + 'src/safe.ts': 'export const safe = true;\n', + }); + const workUnit = unit({ + title: 'Update SecretMapper', + goal: 'Update SecretMapper in place.', + expectedArtifacts: ['protected/secret-mapper.ts'], + expectedAreas: ['protected'], + }); + const result = await compileSecondaryBuilderPacket( + compilerInput(root, workUnit, index(root, ['protected'])), + ); + expect(result.ok).toBe(false); + if (result.ok) return; + expect(result.failure.kind).toBe('INSUFFICIENT_CONTEXT'); + expect(result.plans.flatMap((plan) => plan.selectedWorkingItems)).toEqual([]); + }); + + it('reports an ambiguous symbol instead of choosing an arbitrary module', async () => { + const root = emptyTempDir(); + writeFiles(root, { + 'alpha/user-service.ts': 'export class UserService {}\n', + 'beta/user-service.ts': 'export class UserService {}\n', + }); + const workUnit = unit({ + title: 'Update UserService', + goal: 'Change UserService behavior.', + expectedArtifacts: [], + expectedAreas: [], + }); + const result = await compileSecondaryBuilderPacket(compilerInput(root, workUnit)); + expect(result.ok).toBe(false); + if (result.ok) return; + expect(result.failure.kind).toBe('AMBIGUOUS_TARGET'); + expect(result.quality.targetAmbiguity).toBe(true); + }); + + it('keeps the target and tests under budget pressure and sections a large target', async () => { + const root = emptyTempDir(); + const large = [ + "import { helper } from './helper.js';", + ...Array.from({ length: 300 }, (_, index) => `export const noise${index} = ${index};`), + 'export class FooMapper { map() { return helper(); } }', + ...Array.from({ length: 300 }, (_, index) => `export const tail${index} = ${index};`), + ].join('\n'); + writeFiles(root, { + 'src/foo-mapper.ts': large, + 'src/helper.ts': 'export const helper = () => 1;\n', + 'tests/foo-mapper.test.ts': "import '../src/foo-mapper.js';\ntest('mapper', () => {});\n", + ...Object.fromEntries( + Array.from({ length: 80 }, (_, index) => [ + `src/unrelated-${index}.ts`, + `export class Unrelated${index} {}\n`, + ]), + ), + }); + const result = await compileSecondaryBuilderPacket( + compilerInput(root, unit(), index(root), { + budget: { + maxSelectedFiles: 3, + maxTests: 1, + maxReferencePatterns: 0, + maxSourceCharacters: 8_000, + maxCharactersPerSection: 2_000, + }, + }), + ); + expect(result.ok).toBe(true); + if (!result.ok) return; + const target = result.packet.sourceContext.find((entry) => entry.path === 'src/foo-mapper.ts'); + expect(target?.startLine).toBeDefined(); + expect(target?.content.length).toBeLessThan(large.length); + expect(result.packet.tests).toHaveLength(1); + expect(result.metrics.selectedFiles).toBeLessThanOrEqual(3); + expect(result.metrics.indexedFilesConsidered).toBeGreaterThan(80); + expect(result.metrics.indexReused).toBe(true); + }); + + it('produces a stable semantic hash and changes it when selected source changes', async () => { + const root = emptyTempDir(); + writeFiles(root, { 'src/foo-mapper.ts': 'export class FooMapper { value = 1; }\n' }); + const workUnit = unit(); + const first = await compileSecondaryBuilderPacket( + compilerInput(root, workUnit, index(root), { createdAt: '2026-08-30T01:00:00.000Z' }), + ); + const second = await compileSecondaryBuilderPacket( + compilerInput(root, workUnit, index(root), { createdAt: '2026-08-30T02:00:00.000Z' }), + ); + expect(first.ok && second.ok).toBe(true); + if (!first.ok || !second.ok) return; + expect(first.packet.contentHash).toBe(second.packet.contentHash); + writeFiles(root, { 'src/foo-mapper.ts': 'export class FooMapper { value = 2; }\n' }); + const third = await compileSecondaryBuilderPacket(compilerInput(root, workUnit, index(root))); + expect(third.ok).toBe(true); + if (!third.ok) return; + expect(third.packet.contentHash).not.toBe(first.packet.contentHash); + }); + + it('preserves repository identity for a bounded cross-repo WorkUnit', async () => { + const backend = emptyTempDir(); + const frontend = emptyTempDir(); + writeFiles(backend, { 'src/client.ts': 'export class BackendApiClient {}\n' }); + writeFiles(frontend, { 'src/client.ts': 'export class FrontendApiClient {}\n' }); + const workUnit = unit({ + title: 'Update BackendApiClient and FrontendApiClient', + goal: 'Keep BackendApiClient and FrontendApiClient compatible.', + expectedArtifacts: [], + expectedAreas: ['backend', 'frontend'], + }); + const base = compilerInput(backend, workUnit); + const result = await compileSecondaryBuilderPacket({ + ...base, + repositories: [ + { repositoryId: 'backend', rootDir: backend, index: index(backend), indexReused: true }, + { + repositoryId: 'frontend', + rootDir: frontend, + index: index(frontend), + indexReused: true, + justification: 'The approved WorkUnit spans the API and its client.', + }, + ], + }); + expect(result.ok).toBe(true); + if (!result.ok) return; + expect(result.packet.targets.map((target) => `${target.repositoryId}:${target.path}`).sort()).toEqual([ + 'backend:src/client.ts', + 'frontend:src/client.ts', + ]); + expect(result.planRefs).toHaveLength(2); + }); + + it('applies the selected-file ceiling globally across repositories', async () => { + const backend = emptyTempDir(); + const frontend = emptyTempDir(); + writeFiles(backend, { 'src/client.ts': 'export class BackendApiClient {}\n' }); + writeFiles(frontend, { 'src/client.ts': 'export class FrontendApiClient {}\n' }); + const workUnit = unit({ + title: 'Update BackendApiClient and FrontendApiClient', + goal: 'Keep BackendApiClient and FrontendApiClient compatible.', + expectedArtifacts: [], + expectedAreas: ['backend', 'frontend'], + }); + const base = compilerInput(backend, workUnit); + const result = await compileSecondaryBuilderPacket({ + ...base, + budget: { maxSelectedFiles: 1 }, + repositories: [ + { repositoryId: 'backend', rootDir: backend, index: index(backend) }, + { + repositoryId: 'frontend', + rootDir: frontend, + index: index(frontend), + justification: 'The approved WorkUnit spans the API and its client.', + }, + ], + }); + expect(result.ok).toBe(false); + expect(result.metrics.selectedFiles).toBe(1); + if (result.ok) return; + expect(result.failure.kind).toBe('INSUFFICIENT_CONTEXT'); + }); + + it('reports a packet that cannot fit the configured input ceiling as insufficient', async () => { + const root = emptyTempDir(); + writeFiles(root, { 'src/foo-mapper.ts': 'export class FooMapper { map() { return 1; } }\n' }); + const result = await compileSecondaryBuilderPacket( + compilerInput(root, unit(), index(root), { maximumInputCharacters: 1_000 }), + ); + expect(result.ok).toBe(false); + expect(result.quality.contextSufficient).toBe(false); + if (result.ok) return; + expect(result.failure.kind).toBe('INSUFFICIENT_CONTEXT'); + expect(result.failure.reasons.join(' ')).toContain('could not fit within policy'); + }); + + it('treats NEEDS_MORE_CONTEXT as a non-candidate outcome with no writes', async () => { + const root = emptyTempDir(); + writeFiles(root, { 'src/foo-mapper.ts': 'export class FooMapper {}\n' }); + const compiled = await compileSecondaryBuilderPacket(compilerInput(root, unit())); + expect(compiled.ok).toBe(true); + if (!compiled.ok) return; + const execution = await executeSecondaryObjectiveBuilder({ + worktreeRoot: root, + packet: compiled.packet, + inference: { + profile: 'fake', + provider: 'fake', + async infer() { + return { + ok: true, + text: JSON.stringify({ + schemaVersion: SECONDARY_BUILDER_RESULT_SCHEMA_VERSION, + status: 'NEEDS_MORE_CONTEXT', + summary: 'The required interface is missing.', + edits: [], + needsMoreContextReasons: ['The target interface declaration is absent.'], + }), + durationMs: 1, + }; + }, + }, + maximumInputCharacters: 524_288, + maxOutputBytes: 1_048_576, + }); + expect(execution.ok).toBe(false); + if (execution.ok) return; + expect(execution.failure.kind).toBe('INSUFFICIENT_CONTEXT'); + expect(readFileSync(path.join(root, 'src/foo-mapper.ts'), 'utf8')).toBe( + 'export class FooMapper {}\n', + ); + }); +}); diff --git a/tests/orchestration/secondary-objective-builder.test.ts b/tests/orchestration/secondary-objective-builder.test.ts index 1deee96..55baf94 100644 --- a/tests/orchestration/secondary-objective-builder.test.ts +++ b/tests/orchestration/secondary-objective-builder.test.ts @@ -37,6 +37,7 @@ import { candidateArtifactSchema, captureSecondarySourceContext, collectWorktreeChanges, + compileSecondaryBuilderPacket, contractSnapshotHashOf, contextProjectionSchema, createWorkerWorktree, @@ -60,6 +61,7 @@ import { storeSecondaryBuilderAttempt, storeWorkerRecord, storeWorkGraph, + workUnitSchema, } from '@specbridge/orchestration'; import { emptyTempDir } from '../helpers.js'; import { failingCommand, setupExecutionFixture } from '../helpers-execution.js'; @@ -438,6 +440,7 @@ function secondaryMissionFixture( verificationFails?: boolean; staleProjection?: boolean; semanticEvaluationAlways?: boolean; + automaticContext?: boolean; } = {}, ): { root: string; @@ -463,6 +466,16 @@ function secondaryMissionFixture( }, }, }); + if (options.automaticContext === true) { + mkdirSync(path.join(base.root, 'src', 'envelope'), { recursive: true }); + mkdirSync(path.join(base.root, 'src', 'transport'), { recursive: true }); + writeFileSync(path.join(base.root, 'src', 'envelope', 'implementation.js'), 'module.exports = { source: "baseline" };\n', 'utf8'); + writeFileSync(path.join(base.root, 'src', 'transport', 'implementation.js'), 'module.exports = { source: "baseline" };\n', 'utf8'); + writeFileSync(path.join(base.root, 'src', 'envelope', 'implementation.test.js'), "require('./implementation.js');\n", 'utf8'); + writeFileSync(path.join(base.root, 'src', 'transport', 'implementation.test.js'), "require('./implementation.js');\n", 'utf8'); + git(base.root, 'add', 'src'); + git(base.root, 'commit', '-q', '-m', 'automatic packet fixture source'); + } const missionDeps: MissionDeps = { workspace: base.workspace, clock: base.clock, @@ -532,13 +545,17 @@ function secondaryMissionFixture( : 'src/transport/implementation.js'; // No candidate has reached integration while either direct-model call // is running: the canonical checkout remains byte-identical. - expect(existsSync(path.join(base.root, target))).toBe(false); + if (options.automaticContext === true) { + expect(readFileSync(path.join(base.root, target), 'utf8')).toContain('baseline'); + } else { + expect(existsSync(path.join(base.root, target))).toBe(false); + } return { ok: true, text: validProposal([ { path: target, - operation: 'CREATE', + operation: options.automaticContext === true ? 'REPLACE' : 'CREATE', content: `module.exports = { source: "secondary", unit: "${envelope ? 'envelope' : 'transport'}" };\n`, }, ]), @@ -562,19 +579,23 @@ function secondaryMissionFixture( host: 'test', secondaryObjectiveBuilder: { selectionReason: 'Phase 4 deterministic qualification explicitly selected this backend.', - sourceContext: () => { - if (options.staleProjection === true && !contractMutated) { - const current = readContractRegistry(base.workspace, mission.missionId)[0]!; - storeContractRevision(base.workspace, mission.missionId, { - ...current, - revision: current.revision + 1, - supersedesRevision: current.revision, - recordedAt: base.clock().toISOString(), - }); - contractMutated = true; - } - return []; - }, + ...(options.automaticContext === true + ? {} + : { + sourceContext: () => { + if (options.staleProjection === true && !contractMutated) { + const current = readContractRegistry(base.workspace, mission.missionId)[0]!; + storeContractRevision(base.workspace, mission.missionId, { + ...current, + revision: current.revision + 1, + supersedesRevision: current.revision, + recordedAt: base.clock().toISOString(), + }); + contractMutated = true; + } + return []; + }, + }), inference: directInference, }, }, @@ -582,6 +603,35 @@ function secondaryMissionFixture( } describe('Secondary Objective Builder governed lifecycle', () => { + it('automatically compiles a fresh packet before the governed candidate lifecycle', async () => { + const priorScenario = process.env['FAKE_CLAUDE_SCENARIO']; + process.env['FAKE_CLAUDE_SCENARIO'] = 'objective-multi'; + try { + const fixture = secondaryMissionFixture({ automaticContext: true }); + const job = createJob(fixture.driverDeps, { + specName: 'steprelay-secondary', + goal: 'Exercise automatic Secondary Builder packet compilation.', + }); + const result = await driveJob(fixture.driverDeps, job.jobId, {}); + const graph = requireGraphRevision(fixture.workspace, job.jobId, result.job.graphRevision); + const attempts = readSecondaryBuilderAttempts( + fixture.workspace, + job.jobId, + graph.nodes[0]!.nodeId, + ); + expect(result.stop.kind).toBe('completed'); + expect(attempts).toHaveLength(2); + expect(attempts.every((attempt) => attempt.packet.sourceContext.length > 0)).toBe(true); + expect(attempts.every((attempt) => attempt.packet.targets.length === 1)).toBe(true); + expect(attempts.every((attempt) => attempt.packet.retrievalPlanRefs.length > 0)).toBe(true); + expect(attempts.every((attempt) => attempt.packet.quality.contextSufficient)).toBe(true); + expect(attempts.every((attempt) => attempt.status === 'CANDIDATE_READY')).toBe(true); + } finally { + if (priorScenario === undefined) delete process.env['FAKE_CLAUDE_SCENARIO']; + else process.env['FAKE_CLAUDE_SCENARIO'] = priorScenario; + } + }, 300_000); + it('flows through normal candidate evaluation, aggregation, integration, and trusted verification', async () => { const priorScenario = process.env['FAKE_CLAUDE_SCENARIO']; process.env['FAKE_CLAUDE_SCENARIO'] = 'objective-multi'; @@ -971,6 +1021,14 @@ describe('real managed-local Secondary Objective Builder qualification', () => { throw new Error('set SPECBRIDGE_TEST_LLAMA_SERVER and SPECBRIDGE_TEST_QWEN_GGUF'); } const base = setupExecutionFixture({ git: true }); + mkdirSync(path.join(base.root, 'src'), { recursive: true }); + writeFileSync( + path.join(base.root, 'src', 'mapper.ts'), + 'export interface UserDto { name: string }\nexport const mapUser = (name: string): UserDto => ({ name });\n', + 'utf8', + ); + git(base.root, 'add', 'src/mapper.ts'); + git(base.root, 'commit', '-q', '-m', 'real packet qualification fixture'); const handle = await createWorkerWorktree({ workspace: base.workspace, jobId: 'job-real-secondary', @@ -989,19 +1047,38 @@ describe('real managed-local Secondary Objective Builder qualification', () => { const manager = new LocalModelManager({ config: localInference }); realManagers.push(manager); try { - mkdirSync(path.join(handle.dir, 'src'), { recursive: true }); - writeFileSync( - path.join(handle.dir, 'src', 'mapper.ts'), - 'export interface UserDto { name: string }\nexport const mapUser = (name: string): UserDto => ({ name });\n', - 'utf8', - ); - const packet = buildSecondaryBuilderPacket({ + const compiled = await compileSecondaryBuilderPacket({ + workspace: base.workspace, + config, + jobId: 'job-real-secondary', + objectiveNodeId: 'node-1', + workUnit: workUnitSchema.parse({ + workUnitId: 'wu-1', + objectiveNodeId: 'node-1', + parentTaskId: '1', + kind: 'build', + title: 'Add id to UserDto mapper', + goal: 'Update mapUser in src/mapper.ts to return id and name.', + expectedArtifacts: ['src/mapper.ts'], + expectedAreas: ['src'], + status: 'READY', + }), projection: projection(), - sourceContext: captureSecondarySourceContext(handle.dir, ['src/mapper.ts']), + attempt: 1, + worktreeRoot: handle.dir, + baselineRef: handle.baselineCommit, + verificationHints: base.config.verification.commands.map((command) => command.name), + maximumInputCharacters: 32_000, + createdAt: '2026-08-30T00:00:00.000Z', + persist: false, }); + expect(compiled.ok).toBe(true); + if (!compiled.ok) throw new Error(compiled.failure.reasons.join('; ')); + expect(compiled.metrics.selectedFiles).toBeGreaterThan(0); + expect(compiled.packet.sourceContext.map((entry) => entry.path)).toContain('src/mapper.ts'); const result = await executeSecondaryObjectiveBuilder({ worktreeRoot: handle.dir, - packet, + packet: compiled.packet, inference: managedLocalSecondaryModelInference(manager, config), maximumInputCharacters: 32_000, maxOutputBytes: localInference.maxOutputBytes,