brainclaw 1.17.0 → 1.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -5
- package/dist/brainclaw-vscode.vsix +0 -0
- package/dist/commands/code-map.js +4 -1
- package/dist/commands/codev.js +61 -30
- package/dist/commands/doctor.js +14 -1
- package/dist/commands/harvest.js +223 -43
- package/dist/commands/inbox.js +10 -4
- package/dist/commands/install-hooks.js +184 -27
- package/dist/commands/loop.js +2 -2
- package/dist/commands/loops-handlers.js +82 -1
- package/dist/commands/mcp-catalog.js +12 -4
- package/dist/commands/mcp-read-handlers.js +90 -7
- package/dist/commands/mcp-schemas.generated.js +3 -0
- package/dist/commands/mcp-write-claims.js +57 -0
- package/dist/commands/mcp-write-coordination.js +216 -57
- package/dist/commands/mcp-write-entities.js +11 -0
- package/dist/commands/mcp.js +29 -2
- package/dist/commands/session-end.js +15 -0
- package/dist/commands/session-start.js +19 -0
- package/dist/core/agentrun-reconciler.js +171 -7
- package/dist/core/agentruns.js +6 -1
- package/dist/core/claim-conformity.js +193 -0
- package/dist/core/claim-scope.js +155 -0
- package/dist/core/claims.js +127 -2
- package/dist/core/code-map/aggregate.js +473 -0
- package/dist/core/code-map/backend.js +36 -10
- package/dist/core/code-map/freshness.js +36 -1
- package/dist/core/code-map/lang/c/imports.scm +12 -0
- package/dist/core/code-map/lang/c/index.js +150 -0
- package/dist/core/code-map/lang/c/tags.scm +68 -0
- package/dist/core/code-map/lang/cpp/imports.scm +14 -0
- package/dist/core/code-map/lang/cpp/index.js +149 -0
- package/dist/core/code-map/lang/cpp/tags.scm +87 -0
- package/dist/core/code-map/lang/csharp/imports.scm +20 -0
- package/dist/core/code-map/lang/csharp/index.js +224 -0
- package/dist/core/code-map/lang/csharp/tags.scm +63 -0
- package/dist/core/code-map/lang/go/imports.scm +13 -0
- package/dist/core/code-map/lang/go/index.js +139 -0
- package/dist/core/code-map/lang/go/tags.scm +36 -0
- package/dist/core/code-map/lang/providers.js +12 -1
- package/dist/core/code-map/lang/ruby/imports.scm +24 -0
- package/dist/core/code-map/lang/ruby/index.js +198 -0
- package/dist/core/code-map/lang/ruby/tags.scm +49 -0
- package/dist/core/code-map/lang/rust/imports.scm +44 -0
- package/dist/core/code-map/lang/rust/index.js +136 -0
- package/dist/core/code-map/lang/rust/tags.scm +47 -0
- package/dist/core/code-map/query.js +229 -80
- package/dist/core/code-map/types.js +18 -0
- package/dist/core/code-map/work-section.js +8 -7
- package/dist/core/codev-responses.js +16 -0
- package/dist/core/dispatcher.js +176 -22
- package/dist/core/execution-adapters.js +29 -3
- package/dist/core/facade-schema.js +32 -0
- package/dist/core/guidance-telemetry.js +197 -0
- package/dist/core/ideation-loop-close.js +152 -0
- package/dist/core/instruction-templates.js +11 -3
- package/dist/core/loops/artifact-resolver.js +197 -0
- package/dist/core/loops/attempt-reservation.js +576 -0
- package/dist/core/loops/commit-intent.js +494 -0
- package/dist/core/loops/facade-schema.js +48 -0
- package/dist/core/loops/impl-bind.js +144 -0
- package/dist/core/loops/index.js +1 -1
- package/dist/core/loops/iteration-engine.js +29 -0
- package/dist/core/loops/lock.js +14 -0
- package/dist/core/loops/project-resolution.js +157 -0
- package/dist/core/loops/reconcile-turn.js +369 -0
- package/dist/core/loops/result-reducers.js +88 -0
- package/dist/core/loops/store.js +46 -7
- package/dist/core/loops/types.js +139 -11
- package/dist/core/loops/verbs.js +49 -4
- package/dist/core/loops/verify-command.js +209 -0
- package/dist/core/messaging.js +58 -5
- package/dist/core/next-actions.js +157 -0
- package/dist/core/review-loop-close.js +27 -6
- package/dist/core/review-loop-turn-dispatch.js +290 -28
- package/dist/core/runtime-signals.js +68 -0
- package/dist/core/schema.js +64 -0
- package/dist/core/surface-freshness.js +150 -0
- package/dist/core/warnings.js +98 -0
- package/dist/core/worktree.js +24 -0
- package/dist/facts.js +9 -9
- package/dist/facts.json +8 -8
- package/dist/wasm/tree-sitter-c.wasm +0 -0
- package/dist/wasm/tree-sitter-c_sharp.wasm +0 -0
- package/dist/wasm/tree-sitter-cpp.wasm +0 -0
- package/dist/wasm/tree-sitter-go.wasm +0 -0
- package/dist/wasm/tree-sitter-ruby.wasm +0 -0
- package/dist/wasm/tree-sitter-rust.wasm +0 -0
- package/docs/cli.md +1 -1
- package/docs/code-map.md +22 -6
- package/docs/concepts/loop-engine.md +24 -0
- package/docs/concepts/observer-protocol.md +22 -0
- package/docs/concepts/plans-and-claims.md +57 -0
- package/docs/integrations/claude-code.md +53 -0
- package/docs/integrations/mcp.md +45 -0
- package/docs/mcp-schema-changelog.md +118 -2
- package/package.json +1 -1
package/dist/commands/mcp.js
CHANGED
|
@@ -27,7 +27,8 @@ import { switchProject } from './switch.js';
|
|
|
27
27
|
import { assessBootstrapNeed, resolveEmptyMemoryRecommendation } from '../core/setup-flow.js';
|
|
28
28
|
import { WorkRequestSchema } from '../core/facade-schema.js';
|
|
29
29
|
import { codeMapWorkSection, codeMapRefreshNextActions } from '../core/code-map/work-section.js';
|
|
30
|
-
import { sweepDeadPidRunningAgentRunsAtRead } from '../core/agentrun-reconciler.js';
|
|
30
|
+
import { sweepDeadPidRunningAgentRunsAtRead, sweepTurnOwnedPreRunLeaseAtRead } from '../core/agentrun-reconciler.js';
|
|
31
|
+
import { extractSuggestedTools, observeToolCall, recordSuggestion } from '../core/guidance-telemetry.js';
|
|
31
32
|
import { bumpActiveAssignmentHeartbeat } from '../core/assignments.js';
|
|
32
33
|
import { handleBclawAckMessage, handleBclawCoordinate, handleBclawDispatch, handleBclawLoop, handleBclawSendMessage, } from './mcp-write-coordination.js';
|
|
33
34
|
import { ensureTrust, resolveMutationIdentity, explicitSessionIdFromEnv, projectInfoForCwd, scopeMetadataForTarget, } from './mcp-write-support.js';
|
|
@@ -1556,7 +1557,10 @@ async function _executeMcpToolCallInner(payload) {
|
|
|
1556
1557
|
};
|
|
1557
1558
|
}
|
|
1558
1559
|
if (name === 'bclaw_coordinate') {
|
|
1559
|
-
|
|
1560
|
+
// pln#521 P1 — scopeInfo carries WHICH selector produced `cwd`, which the
|
|
1561
|
+
// review open_loop project gate needs to distinguish a chosen project
|
|
1562
|
+
// from the bare cwd fallback.
|
|
1563
|
+
return await handleBclawCoordinate(args, { cwd, connectionSessionId, currentModel, effectiveScope: scopeInfo });
|
|
1560
1564
|
}
|
|
1561
1565
|
if (name === 'bclaw_loop') {
|
|
1562
1566
|
return await handleBclawLoop(args, { cwd, connectionSessionId });
|
|
@@ -1871,7 +1875,21 @@ export async function executeMcpToolCall(payload) {
|
|
|
1871
1875
|
sweepDeadPidRunningAgentRunsAtRead(cwd);
|
|
1872
1876
|
}
|
|
1873
1877
|
catch { /* best-effort */ }
|
|
1878
|
+
// pln#630 PR2c-lease: converge turn-owned runs still `created`/`launching`
|
|
1879
|
+
// on their dispatch/launch lease (inert until live turn-owned dispatch).
|
|
1880
|
+
try {
|
|
1881
|
+
sweepTurnOwnedPreRunLeaseAtRead(cwd);
|
|
1882
|
+
}
|
|
1883
|
+
catch { /* best-effort */ }
|
|
1884
|
+
}
|
|
1885
|
+
// pln#634 PR2 — guidance adherence. Judge the PREVIOUS response's suggestion
|
|
1886
|
+
// against the call actually being made now, before delegating. Tool names
|
|
1887
|
+
// only, never arguments or content. Best-effort: telemetry may not break a
|
|
1888
|
+
// tool call.
|
|
1889
|
+
try {
|
|
1890
|
+
observeToolCall({ sessionId: effectiveConnectionSessionId, tool: payload.name, cwd });
|
|
1874
1891
|
}
|
|
1892
|
+
catch { /* never break the tool path */ }
|
|
1875
1893
|
// ── Delegate to inner handler ───────────────────────────────────────────────
|
|
1876
1894
|
const outcome = await _executeMcpToolCallInner({
|
|
1877
1895
|
...payload,
|
|
@@ -1879,6 +1897,15 @@ export async function executeMcpToolCall(payload) {
|
|
|
1879
1897
|
connectionSessionId: effectiveConnectionSessionId,
|
|
1880
1898
|
effectiveScope: effective,
|
|
1881
1899
|
});
|
|
1900
|
+
// Remember what THIS response suggested, so the next call can be judged.
|
|
1901
|
+
try {
|
|
1902
|
+
recordSuggestion({
|
|
1903
|
+
sessionId: effectiveConnectionSessionId,
|
|
1904
|
+
tool: payload.name,
|
|
1905
|
+
suggested: extractSuggestedTools(outcome.response),
|
|
1906
|
+
});
|
|
1907
|
+
}
|
|
1908
|
+
catch { /* never break the tool path */ }
|
|
1882
1909
|
// Apply legacy deprecation warning uniformly (Phase 3 slice 3g). Read tools
|
|
1883
1910
|
// already get it at line 2560; write tools historically did not. This
|
|
1884
1911
|
// wrapper ensures every call through a deprecated name surfaces the
|
|
@@ -21,6 +21,8 @@ import { memoryExists } from '../core/io.js';
|
|
|
21
21
|
import { buildOperationalIdentity, clearCurrentSession } from '../core/identity.js';
|
|
22
22
|
import { buildContextDiff } from '../core/context-diff.js';
|
|
23
23
|
import { listClaims, releaseClaim } from '../core/claims.js';
|
|
24
|
+
import { reconcileClaimConformity } from '../core/claim-conformity.js';
|
|
25
|
+
import { toWarningDetail } from '../core/warnings.js';
|
|
24
26
|
import { listRuntimeNotes, saveRuntimeNote, generateRuntimeNoteId } from '../core/runtime.js';
|
|
25
27
|
import { loadState, persistState } from '../core/state.js';
|
|
26
28
|
import { listArchivedCandidates, listCandidates } from '../core/candidates.js';
|
|
@@ -147,6 +149,18 @@ export async function endSession(options = {}) {
|
|
|
147
149
|
const state = loadState(options.cwd);
|
|
148
150
|
const claimPlanIds = new Set(activeClaims.map((c) => c.plan_id).filter(Boolean));
|
|
149
151
|
const inProgressPlans = state.plan_items.filter((p) => p.status === 'in_progress' && (p.assignee === registered.agent_name || claimPlanIds.has(p.id)));
|
|
152
|
+
// pln#636 C2 — sweep this session's own claims before auto-release closes
|
|
153
|
+
// them. session-end is the backstop trigger: it catches a claim whose worker
|
|
154
|
+
// neither released it via MCP nor reported through a LANE-RESULT.
|
|
155
|
+
const conformityWarnings = [];
|
|
156
|
+
for (const c of activeClaims) {
|
|
157
|
+
try {
|
|
158
|
+
const conformity = reconcileClaimConformity(c, options.cwd ?? process.cwd());
|
|
159
|
+
if (conformity.warning)
|
|
160
|
+
conformityWarnings.push(toWarningDetail(conformity.warning));
|
|
161
|
+
}
|
|
162
|
+
catch { /* advisory only — a session must always be able to end */ }
|
|
163
|
+
}
|
|
150
164
|
let openWorkWarning;
|
|
151
165
|
if (activeClaims.length > 0 || inProgressPlans.length > 0) {
|
|
152
166
|
if (options.autoRelease) {
|
|
@@ -369,6 +383,7 @@ export async function endSession(options = {}) {
|
|
|
369
383
|
open_work_warning: openWorkWarning,
|
|
370
384
|
session_stats: sessionStats,
|
|
371
385
|
compaction_hint: compactionHint,
|
|
386
|
+
...(conformityWarnings.length ? { scope_warnings: conformityWarnings } : {}),
|
|
372
387
|
...(reflectedHandoff ? { handoff: reflectedHandoff } : {}),
|
|
373
388
|
};
|
|
374
389
|
// pln#564 — session_end pushes the agent into a short dogfooding reflection
|
|
@@ -18,6 +18,9 @@ import { auditLocalAgentWorkspaceFiles } from '../core/agent-files.js';
|
|
|
18
18
|
import { buildAgentInventory, loadAgentInventory, saveAgentInventory, diffInventory } from '../core/agent-inventory.js';
|
|
19
19
|
import { checkMemoryPressure, enforceRuntimeNoteRetention, parkClosedAutoHandoffs } from '../core/gc-semantic.js';
|
|
20
20
|
import { sweepAssignments } from '../core/assignment-sweeper.js';
|
|
21
|
+
import { getInstalledBrainclawVersion } from '../core/brainclaw-version.js';
|
|
22
|
+
import { reconcileSurfaceFreshness, staleSurfaceWarning } from '../core/surface-freshness.js';
|
|
23
|
+
import { toWarningDetail } from '../core/warnings.js';
|
|
21
24
|
import { loadHygienePolicy } from '../core/hygiene-policy.js';
|
|
22
25
|
import { maybeCreateCheckpoint } from '../core/events/checkpoint.js';
|
|
23
26
|
import { pullSignalsFromLinkedProjects, markSignalProcessed } from '../core/federation-transport.js';
|
|
@@ -282,6 +285,21 @@ export async function startSession(options = {}) {
|
|
|
282
285
|
}
|
|
283
286
|
catch { /* non-fatal */ }
|
|
284
287
|
}
|
|
288
|
+
// pln#638 volet 2b — LAZY freshness reconcile of the generated guidance
|
|
289
|
+
// surfaces. session-start is the right trigger because it is the moment the
|
|
290
|
+
// agent is about to READ that guidance, and it is a path we already visit — no
|
|
291
|
+
// daemon, no watcher (feedback_lazy_reconcile_pattern). Advisory only: nothing
|
|
292
|
+
// is regenerated here, because regeneration is an explicit act and silently
|
|
293
|
+
// rewriting a file the operator may have edited would be worse than a warning.
|
|
294
|
+
let staleSurfaces;
|
|
295
|
+
if (maintenanceMode === 'full') {
|
|
296
|
+
try {
|
|
297
|
+
const currentVersion = getInstalledBrainclawVersion();
|
|
298
|
+
const freshness = reconcileSurfaceFreshness(options.cwd ?? process.cwd(), currentVersion);
|
|
299
|
+
staleSurfaces = staleSurfaceWarning(freshness, currentVersion);
|
|
300
|
+
}
|
|
301
|
+
catch { /* non-fatal */ }
|
|
302
|
+
}
|
|
285
303
|
// Materialize incoming federation signals from linked projects (Phase 0 — local)
|
|
286
304
|
if (maintenanceMode === 'full') {
|
|
287
305
|
try {
|
|
@@ -338,6 +356,7 @@ export async function startSession(options = {}) {
|
|
|
338
356
|
...(sharedCheckoutWarning ? { shared_checkout_warning: sharedCheckoutWarning } : {}),
|
|
339
357
|
...(staleClaimsReleased ? { stale_claims_released: staleClaimsReleased } : {}),
|
|
340
358
|
...(memoryPressure ? { memory_pressure: memoryPressure } : {}),
|
|
359
|
+
...(staleSurfaces ? { stale_surfaces: toWarningDetail(staleSurfaces) } : {}),
|
|
341
360
|
...(autoRegistered ? { auto_registered: true } : {}),
|
|
342
361
|
};
|
|
343
362
|
}
|
|
@@ -38,7 +38,8 @@ import { loadClaim, releaseClaim } from './claims.js';
|
|
|
38
38
|
import { loadAssignment } from './assignments.js';
|
|
39
39
|
import { createRuntimeEvent } from './events.js';
|
|
40
40
|
import { nowISO } from './ids.js';
|
|
41
|
-
import { readHeartbeat, readLogTail, signalExists, latestActivityMs } from './runtime-signals.js';
|
|
41
|
+
import { readHeartbeat, readLogTail, signalExists, latestActivityMs, readCompletionSignals } from './runtime-signals.js';
|
|
42
|
+
import { findReservationByRunId, evidenceMatchesAttempt, launchGrant, revokeLaunchGrant } from './loops/attempt-reservation.js';
|
|
42
43
|
// ── Constants ──────────────────────────────────────────────────────────────
|
|
43
44
|
/**
|
|
44
45
|
* Minimum age before a run is eligible for reconciliation. Below this, the
|
|
@@ -188,9 +189,51 @@ export function collectEvidence(run, cwd, options) {
|
|
|
188
189
|
fs_activity_age_ms = now - lastFs;
|
|
189
190
|
}
|
|
190
191
|
catch { /* defensive */ }
|
|
192
|
+
// pln#630 PR2b-c (§13 R3) — read-strict gate. A run owned by a turn-attempt
|
|
193
|
+
// reservation is completed ONLY on turn-keyed evidence: a `completed` sentinel
|
|
194
|
+
// body whose turn_id/run_id/nonce match the attempt's CURRENT launch
|
|
195
|
+
// generation. A bare assignment-keyed presence sentinel — or a stale prior
|
|
196
|
+
// generation's body — never counts (closes phantom-completion). Legacy runs
|
|
197
|
+
// (no owning reservation) keep presence-based acceptance.
|
|
198
|
+
let turn_owned = false;
|
|
199
|
+
let turn_keyed_completed = false;
|
|
200
|
+
try {
|
|
201
|
+
const reservation = findReservationByRunId(run.id, cwd);
|
|
202
|
+
if (reservation) {
|
|
203
|
+
turn_owned = true;
|
|
204
|
+
const bodies = readCompletionSignals(signalRoot, run.assignment_id);
|
|
205
|
+
// Evidence must be turn-keyed AND carry the RIGHT status (a `.completed`
|
|
206
|
+
// file whose body says status:'failed' is not completion evidence —
|
|
207
|
+
// read-strict trusts the body, not the filename, review PR2b-c #C2).
|
|
208
|
+
const matchedCompleted = bodies.completed !== undefined
|
|
209
|
+
&& bodies.completed.status === 'completed'
|
|
210
|
+
&& evidenceMatchesAttempt(reservation, bodies.completed);
|
|
211
|
+
const matchedFailed = bodies.failed !== undefined
|
|
212
|
+
&& bodies.failed.status === 'failed'
|
|
213
|
+
&& evidenceMatchesAttempt(reservation, bodies.failed);
|
|
214
|
+
if (matchedCompleted && matchedFailed) {
|
|
215
|
+
// §13 R4 — a completed+failed contradiction WITHHOLDS both (never a
|
|
216
|
+
// silent accept). Conflict-event journaling is deferred to
|
|
217
|
+
// reconcileTurn (PR3); collectEvidence stays side-effect-free.
|
|
218
|
+
turn_keyed_completed = false;
|
|
219
|
+
failed_signal = false;
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
turn_keyed_completed = matchedCompleted;
|
|
223
|
+
// Read-strict governs FAILURE too for a turn-owned run: override the
|
|
224
|
+
// presence-based failed_signal so a stale/legacy bare `.failed` marker
|
|
225
|
+
// cannot phantom-FAIL a healthy generation (symmetric to completed,
|
|
226
|
+
// review PR2b-c #B). A genuine death with no turn-keyed evidence still
|
|
227
|
+
// fails via the process-dead + stale path below.
|
|
228
|
+
failed_signal = matchedFailed;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
catch { /* defensive — fall back to legacy (non-turn-owned) behavior */ }
|
|
191
233
|
return {
|
|
192
234
|
age_ms, has_post_start_commit, claim_released, assignment_completed, process_alive,
|
|
193
235
|
completed_signal, failed_signal, heartbeat_exists, heartbeat_age_ms, fs_activity_age_ms,
|
|
236
|
+
turn_owned, turn_keyed_completed,
|
|
194
237
|
};
|
|
195
238
|
}
|
|
196
239
|
/**
|
|
@@ -209,7 +252,7 @@ function fsActiveWithin(evidence, windowMs) {
|
|
|
209
252
|
* Inference only fires after the stale window with no life evidence, so this is
|
|
210
253
|
* conservative. (Loop auto-close on failure is a follow-up.)
|
|
211
254
|
*/
|
|
212
|
-
function cascadeReleaseOnFailure(run, actor, cwd) {
|
|
255
|
+
function cascadeReleaseOnFailure(run, actor, cwd, terminalStatus = 'failed') {
|
|
213
256
|
if (!run.claim_id)
|
|
214
257
|
return;
|
|
215
258
|
try {
|
|
@@ -219,8 +262,8 @@ function cascadeReleaseOnFailure(run, actor, cwd) {
|
|
|
219
262
|
createRuntimeEvent({
|
|
220
263
|
agent: actor,
|
|
221
264
|
session_id: run.session_id,
|
|
222
|
-
event_type: 'run_failed',
|
|
223
|
-
text: `Auto-released claim ${run.claim_id} after run ${run.id} was reconciled to
|
|
265
|
+
event_type: terminalStatus === 'cancelled' ? 'run_cancelled' : 'run_failed',
|
|
266
|
+
text: `Auto-released claim ${run.claim_id} after run ${run.id} was reconciled to ${terminalStatus} (trp#433 GC cascade)`,
|
|
224
267
|
tags: ['reconciler', 'gc', 'claim-release'],
|
|
225
268
|
assignment_id: run.assignment_id,
|
|
226
269
|
run_id: run.id,
|
|
@@ -232,6 +275,13 @@ function cascadeReleaseOnFailure(run, actor, cwd) {
|
|
|
232
275
|
catch { /* best-effort — never let GC break reconciliation */ }
|
|
233
276
|
}
|
|
234
277
|
function anyCompletionEvidence(evidence) {
|
|
278
|
+
// pln#630 PR2b-c (§13 R3): a turn-owned run is completed ONLY on turn-keyed
|
|
279
|
+
// evidence — never a bare presence sentinel or an assignment-keyed proxy
|
|
280
|
+
// (claim_released / assignment_completed / post_start_commit). This is the
|
|
281
|
+
// read-strict split that closes the stale-sentinel + presence-only
|
|
282
|
+
// phantom-completion holes. Legacy (non-turn-owned) runs are unchanged.
|
|
283
|
+
if (evidence.turn_owned)
|
|
284
|
+
return evidence.turn_keyed_completed;
|
|
235
285
|
return evidence.completed_signal
|
|
236
286
|
|| evidence.has_post_start_commit
|
|
237
287
|
|| evidence.claim_released
|
|
@@ -345,7 +395,7 @@ export function reconcileAgentRun(runId, cwd, options = {}) {
|
|
|
345
395
|
const evidence = {
|
|
346
396
|
age_ms: 0, has_post_start_commit: false, claim_released: false,
|
|
347
397
|
assignment_completed: false, process_alive: undefined,
|
|
348
|
-
completed_signal: false, failed_signal: false, heartbeat_exists: false,
|
|
398
|
+
completed_signal: false, failed_signal: false, heartbeat_exists: false, turn_owned: false, turn_keyed_completed: false,
|
|
349
399
|
};
|
|
350
400
|
return {
|
|
351
401
|
run_id: runId, action: 'no_op', reason: 'run not found', evidence,
|
|
@@ -361,6 +411,18 @@ export function reconcileAgentRun(runId, cwd, options = {}) {
|
|
|
361
411
|
evidence, previous_status, current_status: run.status,
|
|
362
412
|
};
|
|
363
413
|
}
|
|
414
|
+
// pln#630 PR2c-lease (§4 + R5): a turn-owned run preallocated `created`/
|
|
415
|
+
// `launching` converges on its DISPATCH/LAUNCH LEASE, not the pid/heartbeat
|
|
416
|
+
// heuristics below (which assume a worker already spawned and can emit
|
|
417
|
+
// life-signs). Delegate before the generic evidence path so these runs never
|
|
418
|
+
// orphan `created`/`launching` forever. Inert for non-turn-owned runs
|
|
419
|
+
// (evidence.turn_owned=false → fall through to the unchanged generic path).
|
|
420
|
+
if ((run.status === 'created' || run.status === 'launching') && evidence.turn_owned) {
|
|
421
|
+
const reservation = findReservationByRunId(run.id, cwd);
|
|
422
|
+
if (reservation) {
|
|
423
|
+
return reconcileTurnOwnedPreRunLease(run, reservation, evidence, cwd, options);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
364
426
|
const grace = options.healthCheckGraceMs ?? DEFAULT_HEALTH_CHECK_GRACE_MS;
|
|
365
427
|
const stale = options.staleAfterMs ?? DEFAULT_STALE_AFTER_MS;
|
|
366
428
|
const actor = options.actor ?? 'reconciler';
|
|
@@ -450,6 +512,84 @@ export function reconcileAgentRun(runId, cwd, options = {}) {
|
|
|
450
512
|
evidence, previous_status, current_status: run.status,
|
|
451
513
|
};
|
|
452
514
|
}
|
|
515
|
+
/**
|
|
516
|
+
* pln#630 PR2c-lease (§4 + R5) — converge a TURN-OWNED run still in `created`/
|
|
517
|
+
* `launching` against its dispatch/launch LEASE, not the pid/heartbeat heuristics
|
|
518
|
+
* (which presuppose a spawned, life-sign-emitting worker). Reached only from
|
|
519
|
+
* `reconcileAgentRun`'s turn-owned delegate branch, so it always has the owning
|
|
520
|
+
* reservation + pre-collected evidence.
|
|
521
|
+
*
|
|
522
|
+
* Outcomes (never `completed` — no phantom success from an unspawned/unproven run):
|
|
523
|
+
* - turn-keyed completion present → NO-OP (finalization is reconcileTurn's job, PR3);
|
|
524
|
+
* - within lease → NO-OP (the worker may yet cross the launch fence / start);
|
|
525
|
+
* - past lease + launch grant CROSSED → `failed` / `launch_attempted_unknown`
|
|
526
|
+
* (the worker WAS invoked; its outcome is unknowable, so it must not complete);
|
|
527
|
+
* - past lease + grant not crossed → `cancelled` / `reserved_never_launched`
|
|
528
|
+
* (no launch receipt — nothing ran; matches attemptStatus(revoked)='cancelled').
|
|
529
|
+
*/
|
|
530
|
+
function reconcileTurnOwnedPreRunLease(run, reservation, evidence, cwd, options) {
|
|
531
|
+
const now = options.nowMs ?? Date.now();
|
|
532
|
+
const actor = options.actor ?? 'reconciler';
|
|
533
|
+
const previous_status = run.status;
|
|
534
|
+
// A genuine turn-keyed completion is NOT an expiry: never fail/cancel a run
|
|
535
|
+
// that produced accepted evidence. Its finalization (assignment/claim/artifacts)
|
|
536
|
+
// is reconcileTurn's responsibility (PR3); here we simply decline to expire it.
|
|
537
|
+
if (evidence.turn_keyed_completed) {
|
|
538
|
+
return {
|
|
539
|
+
run_id: run.id, action: 'no_op',
|
|
540
|
+
reason: 'turn-keyed completion present — defer finalization to reconcileTurn',
|
|
541
|
+
evidence, previous_status, current_status: run.status,
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
// Effective deadline: once armed, the launch lease is the tighter authoritative
|
|
545
|
+
// bound; before arm, the dispatch lease bounds how long a committed reservation
|
|
546
|
+
// may wait to spawn. A non-parseable lease cannot expire a run (fail-safe: leave
|
|
547
|
+
// it for explicit reconcile rather than converge on garbage).
|
|
548
|
+
const leaseISO = reservation.launch?.lease_deadline ?? reservation.lease_deadline;
|
|
549
|
+
const leaseMs = Date.parse(leaseISO);
|
|
550
|
+
if (!Number.isFinite(leaseMs) || now < leaseMs) {
|
|
551
|
+
return {
|
|
552
|
+
run_id: run.id, action: 'no_op',
|
|
553
|
+
reason: Number.isFinite(leaseMs)
|
|
554
|
+
? `within lease (deadline ${leaseISO})`
|
|
555
|
+
: `lease deadline unparseable (${leaseISO}) — cannot expire`,
|
|
556
|
+
evidence, previous_status, current_status: run.status,
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
// Past lease, no accepted completion — the launch-grant status (authoritative,
|
|
560
|
+
// decision-file reconciled) decides the terminal.
|
|
561
|
+
const grant = launchGrant(reservation.turn_id, cwd);
|
|
562
|
+
const crossed = grant?.status === 'crossed';
|
|
563
|
+
const targetStatus = crossed ? 'failed' : 'cancelled';
|
|
564
|
+
const action = crossed ? 'inferred_failed' : 'inferred_cancelled';
|
|
565
|
+
const reason = crossed
|
|
566
|
+
? `launch_attempted_unknown: launch grant crossed but run never reached running by lease ${leaseISO} — outcome unknown, never completed`
|
|
567
|
+
: `reserved_never_launched: no launch receipt by lease ${leaseISO} (grant=${grant?.status ?? 'none'})`;
|
|
568
|
+
// pln#630 dec#149 R1 (review Finding 2) — make the reserved_never_launched strand REACHABLE
|
|
569
|
+
// through the WIRED lazy reconciler: revoke the still-armed grant so its authoritative status
|
|
570
|
+
// becomes `revoked`. That is what lets reconcileTurn's fix-cycle strand detector see the strand
|
|
571
|
+
// and re-emit, and lets the re-dispatch re-arm a fresh generation (dec#149 R1). Idempotent +
|
|
572
|
+
// best-effort: a race to crossed/revoked is fine (the decision file governs) and must never
|
|
573
|
+
// block the run's terminal transition below.
|
|
574
|
+
if (!crossed && grant?.status === 'armed') {
|
|
575
|
+
try {
|
|
576
|
+
revokeLaunchGrant(reservation.turn_id, grant.epoch, 'reserved_never_launched', cwd, actor);
|
|
577
|
+
}
|
|
578
|
+
catch { /* raced to crossed/revoked — authoritative status governs */ }
|
|
579
|
+
}
|
|
580
|
+
try {
|
|
581
|
+
transitionAgentRun(run.id, targetStatus, { actor, status_reason: reason }, cwd);
|
|
582
|
+
cascadeReleaseOnFailure(run, actor, cwd, targetStatus);
|
|
583
|
+
return { run_id: run.id, action, reason, evidence, previous_status, current_status: targetStatus };
|
|
584
|
+
}
|
|
585
|
+
catch (err) {
|
|
586
|
+
return {
|
|
587
|
+
run_id: run.id, action: 'no_op',
|
|
588
|
+
reason: `lease-expiry transition rejected: ${err instanceof Error ? err.message : String(err)}`,
|
|
589
|
+
evidence, previous_status, current_status: run.status,
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
}
|
|
453
593
|
/**
|
|
454
594
|
* Read-path reconciliation for a `running` run whose tracked PID reads dead.
|
|
455
595
|
*
|
|
@@ -478,7 +618,7 @@ export function reconcileDeadPidRunningAgentRunAtRead(runId, cwd, options = {})
|
|
|
478
618
|
const evidence = {
|
|
479
619
|
age_ms: 0, has_post_start_commit: false, claim_released: false,
|
|
480
620
|
assignment_completed: false, process_alive: undefined,
|
|
481
|
-
completed_signal: false, failed_signal: false, heartbeat_exists: false,
|
|
621
|
+
completed_signal: false, failed_signal: false, heartbeat_exists: false, turn_owned: false, turn_keyed_completed: false,
|
|
482
622
|
};
|
|
483
623
|
return {
|
|
484
624
|
run_id: runId, action: 'no_op', reason: 'run not found', evidence,
|
|
@@ -610,6 +750,30 @@ export function sweepDeadPidRunningAgentRunsAtRead(cwd, options = {}) {
|
|
|
610
750
|
.slice(0, limit);
|
|
611
751
|
return candidates.map((run) => reconcileDeadPidRunningAgentRunAtRead(run.id, cwd, options));
|
|
612
752
|
}
|
|
753
|
+
/**
|
|
754
|
+
* pln#630 PR2c-lease — lazy read-path sweep for TURN-OWNED runs still in
|
|
755
|
+
* `created`/`launching`, converging them on their dispatch/launch lease via
|
|
756
|
+
* `reconcileAgentRun`'s turn-owned delegate. Strictly turn-owned: a legacy
|
|
757
|
+
* (non-reservation-owned) `created`/`launching` run is SKIPPED here, so this
|
|
758
|
+
* sweep changes nothing for existing runs and is fully inert until live
|
|
759
|
+
* turn-owned dispatch (PR2c-b) mints such a run. Errors are isolated per run.
|
|
760
|
+
*/
|
|
761
|
+
export function sweepTurnOwnedPreRunLeaseAtRead(cwd, options = {}) {
|
|
762
|
+
const results = [];
|
|
763
|
+
for (const status of ['created', 'launching']) {
|
|
764
|
+
for (const run of listAgentRuns(cwd, { status })) {
|
|
765
|
+
// Only turn-owned runs converge on the lease here — legacy runs keep their
|
|
766
|
+
// existing (non-read-path) reconciliation route untouched.
|
|
767
|
+
if (!findReservationByRunId(run.id, cwd))
|
|
768
|
+
continue;
|
|
769
|
+
try {
|
|
770
|
+
results.push(reconcileAgentRun(run.id, cwd, options));
|
|
771
|
+
}
|
|
772
|
+
catch { /* best-effort — never block reads on reconciliation errors */ }
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
return results;
|
|
776
|
+
}
|
|
613
777
|
/**
|
|
614
778
|
* Reconcile every non-terminal agent_run matching `filter`. Useful for
|
|
615
779
|
* batch sweeps from `bclaw_assignment_events` or `brainclaw doctor --dispatch`.
|
|
@@ -628,7 +792,7 @@ export function reconcileAllOpenRuns(cwd, filter = {}, options = {}) {
|
|
|
628
792
|
catch {
|
|
629
793
|
results.push({
|
|
630
794
|
run_id: run.id, action: 'no_op', reason: 'reconcile threw — skipped',
|
|
631
|
-
evidence: { age_ms: 0, has_post_start_commit: false, claim_released: false, assignment_completed: false, process_alive: undefined, completed_signal: false, failed_signal: false, heartbeat_exists: false },
|
|
795
|
+
evidence: { age_ms: 0, has_post_start_commit: false, claim_released: false, assignment_completed: false, process_alive: undefined, completed_signal: false, failed_signal: false, heartbeat_exists: false, turn_owned: false, turn_keyed_completed: false },
|
|
632
796
|
previous_status: run.status, current_status: run.status,
|
|
633
797
|
});
|
|
634
798
|
}
|
package/dist/core/agentruns.js
CHANGED
|
@@ -101,7 +101,12 @@ export function findLatestAgentRunForAssignment(assignmentId, cwd) {
|
|
|
101
101
|
})[0];
|
|
102
102
|
}
|
|
103
103
|
const VALID_TRANSITIONS = new Map([
|
|
104
|
-
|
|
104
|
+
// `failed` added (pln#630 PR2c-lease): a turn-owned run preallocated `created`
|
|
105
|
+
// whose launch grant CROSSED but crashed before the created→launching
|
|
106
|
+
// transition is `launch_attempted_unknown` — a failure, reconciled straight
|
|
107
|
+
// from `created`. (reserved_never_launched, where the grant never crossed,
|
|
108
|
+
// goes to `cancelled`, already permitted.)
|
|
109
|
+
['created', new Set(['launching', 'waiting_input', 'running', 'failed', 'cancelled', 'interrupted'])],
|
|
105
110
|
['launching', new Set(['waiting_input', 'running', 'failed', 'cancelled', 'timed_out', 'interrupted'])],
|
|
106
111
|
['waiting_input', new Set(['launching', 'running', 'blocked', 'cancelled', 'timed_out', 'interrupted'])],
|
|
107
112
|
['running', new Set(['blocked', 'completed', 'failed', 'cancelled', 'timed_out', 'interrupted'])],
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pln#636 C2 — server-side lazy conformity reconcile.
|
|
3
|
+
*
|
|
4
|
+
* WHY SERVER-SIDE AT ALL. C1's PreToolUse hook only reaches hook-capable hosts.
|
|
5
|
+
* The workers that most need a scope signal are the ones that reach nothing: a
|
|
6
|
+
* spawned sandboxed lane never sees MCP, never loads a hook, and reports through
|
|
7
|
+
* a file. So the universal net has to live where the *outcome* is ingested, not
|
|
8
|
+
* where the write happens. Reconcile at the lifecycle boundaries every tier
|
|
9
|
+
* eventually crosses — release, assignment completion, harvest ingestion,
|
|
10
|
+
* session end — per the validated lazy-reconcile pattern. No daemon, no watcher.
|
|
11
|
+
*
|
|
12
|
+
* WHY POST-HOC IS THE HONEST SHAPE. By the time any of these fire the write has
|
|
13
|
+
* already landed. The only truthful output is an advisory that names the strays
|
|
14
|
+
* and the two calls that resolve them — never an error, never a block
|
|
15
|
+
* (trp_5f342186 is the scar tissue).
|
|
16
|
+
*
|
|
17
|
+
* THE BASELINE PROBLEM, and why `base_sha` exists (C0-b, review F3). Neither
|
|
18
|
+
* `git diff HEAD` nor the worktree's dirty set is authoritative: a lane that
|
|
19
|
+
* commits mid-work moves the ground under both, so the same claim would read
|
|
20
|
+
* "touched nothing" the moment it committed. The comparison runs against the
|
|
21
|
+
* commit recorded at claim creation — a fixed point — and unions in the dirty
|
|
22
|
+
* set so uncommitted work counts too.
|
|
23
|
+
*
|
|
24
|
+
* SILENT ON DOUBT. Every degradation path (no baseline, no git, detached
|
|
25
|
+
* worktree, unreadable repo) yields `unverifiable`, which emits NOTHING. The
|
|
26
|
+
* acceptance bar for this whole design is a zero false-positive rate on the real
|
|
27
|
+
* 613-claim corpus, and 42.4% of that corpus is not path-resolvable at all.
|
|
28
|
+
*
|
|
29
|
+
* @module
|
|
30
|
+
*/
|
|
31
|
+
import { spawnSync } from 'node:child_process';
|
|
32
|
+
import fs from 'node:fs';
|
|
33
|
+
import { assessScopeConformity } from './claim-scope.js';
|
|
34
|
+
/** Cap on how many stray paths ride along in a warning payload. */
|
|
35
|
+
const MAX_REPORTED_PATHS = 10;
|
|
36
|
+
/**
|
|
37
|
+
* Run git and return stdout, or undefined on ANY failure.
|
|
38
|
+
*
|
|
39
|
+
* Never throws and never inspects stderr: a conformity nicety may not degrade
|
|
40
|
+
* the workflow it observes, so an unavailable git, a detached worktree or a
|
|
41
|
+
* garbage-collected branch all read as "cannot tell".
|
|
42
|
+
*/
|
|
43
|
+
function git(cwd, args) {
|
|
44
|
+
try {
|
|
45
|
+
const r = spawnSync('git', args, { cwd, encoding: 'utf-8', windowsHide: true });
|
|
46
|
+
if (r.status !== 0 || typeof r.stdout !== 'string')
|
|
47
|
+
return undefined;
|
|
48
|
+
return r.stdout;
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function splitPaths(out) {
|
|
55
|
+
if (!out)
|
|
56
|
+
return [];
|
|
57
|
+
return out.split(/\r?\n/).map((l) => l.trim()).filter((l) => l.length > 0);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Split WITHOUT trimming, for `--porcelain` output.
|
|
61
|
+
*
|
|
62
|
+
* The porcelain prefix is fixed-width — status codes in columns 1-2, a space in
|
|
63
|
+
* column 3, path from index 3 — so a leading space is DATA. Trimming ` M
|
|
64
|
+
* src/x.ts` first turns the subsequent `slice(3)` into `rc/x.ts`: a path that
|
|
65
|
+
* matches no pathspec and reads as a stray, i.e. a false accusation on every
|
|
66
|
+
* unstaged edit.
|
|
67
|
+
*/
|
|
68
|
+
function splitLinesRaw(out) {
|
|
69
|
+
if (!out)
|
|
70
|
+
return [];
|
|
71
|
+
return out.split(/\r?\n/).filter((l) => l.trim().length > 0);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Where a claim's work physically happened: its own worktree when it has one,
|
|
75
|
+
* otherwise the project root. A lane claim's diff is meaningless read from the
|
|
76
|
+
* coordinator's checkout.
|
|
77
|
+
*/
|
|
78
|
+
function claimWorkdir(claim, cwd) {
|
|
79
|
+
const dir = claim.worktree_path ?? cwd;
|
|
80
|
+
try {
|
|
81
|
+
return fs.existsSync(dir) ? dir : undefined;
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Files this claim's worker touched since the claim was created.
|
|
89
|
+
*
|
|
90
|
+
* Union of two sources, because either alone lies:
|
|
91
|
+
* - `git diff --name-only <base_sha>` — everything committed since the
|
|
92
|
+
* baseline, which the dirty set loses the instant a lane commits.
|
|
93
|
+
* - `git status --porcelain` — uncommitted work, which the diff cannot see.
|
|
94
|
+
*
|
|
95
|
+
* A claim with no `base_sha` (created outside a repo, or before C0-b shipped) is
|
|
96
|
+
* unverifiable rather than compared against a guessed baseline.
|
|
97
|
+
*/
|
|
98
|
+
export function collectTouchedPaths(claim, cwd) {
|
|
99
|
+
const workdir = claimWorkdir(claim, cwd);
|
|
100
|
+
if (!workdir)
|
|
101
|
+
return { paths: [], unverifiableReason: 'claim worktree no longer exists' };
|
|
102
|
+
if (!claim.base_sha)
|
|
103
|
+
return { paths: [], unverifiableReason: 'claim has no recorded base_sha baseline' };
|
|
104
|
+
// Confirm the baseline is still reachable before trusting a diff against it —
|
|
105
|
+
// a pruned worktree branch would otherwise make git fail and read as "clean".
|
|
106
|
+
if (git(workdir, ['cat-file', '-e', `${claim.base_sha}^{commit}`]) === undefined) {
|
|
107
|
+
return { paths: [], unverifiableReason: 'recorded base_sha is no longer reachable in this worktree' };
|
|
108
|
+
}
|
|
109
|
+
const committed = splitPaths(git(workdir, ['diff', '--name-only', claim.base_sha]));
|
|
110
|
+
// -uall so a whole new untracked directory is reported file-by-file rather
|
|
111
|
+
// than collapsed to its directory name, which no pathspec would match.
|
|
112
|
+
const dirty = splitLinesRaw(git(workdir, ['status', '--porcelain', '-uall']))
|
|
113
|
+
.map((line) => line.slice(3).trim())
|
|
114
|
+
// A rename reads `R old -> new`; the destination is what was written.
|
|
115
|
+
.map((p) => (p.includes(' -> ') ? p.split(' -> ')[1] : p))
|
|
116
|
+
.map((p) => p.replace(/^"|"$/g, ''));
|
|
117
|
+
const paths = [...new Set([...committed, ...dirty])].filter((p) => p.length > 0);
|
|
118
|
+
return { paths };
|
|
119
|
+
}
|
|
120
|
+
function widenNextActions(claim, unexpected) {
|
|
121
|
+
return [
|
|
122
|
+
{
|
|
123
|
+
tool: 'bclaw_update',
|
|
124
|
+
args: {
|
|
125
|
+
entity: 'claim',
|
|
126
|
+
id: claim.id,
|
|
127
|
+
// Widening means declaring the footprint, not rewriting the prose scope:
|
|
128
|
+
// `paths[]` is the machine-readable half (C0-b) and is additive.
|
|
129
|
+
paths: unexpected.slice(0, MAX_REPORTED_PATHS),
|
|
130
|
+
},
|
|
131
|
+
when: 'the work legitimately spans these paths — declare them so the next reconcile is silent',
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
tool: 'bclaw_create',
|
|
135
|
+
args: {
|
|
136
|
+
entity: 'trap',
|
|
137
|
+
title: `Work on ${claim.scope} pulls in ${unexpected[0]}`,
|
|
138
|
+
body: 'Recurring coupling found by a claim-scope reconcile. Record why these move together.',
|
|
139
|
+
},
|
|
140
|
+
when: 'the strays reveal a real coupling worth warning the next agent about',
|
|
141
|
+
},
|
|
142
|
+
];
|
|
143
|
+
}
|
|
144
|
+
export function reconcileClaimConformity(claim, cwd, options = {}) {
|
|
145
|
+
const touched = options.touchedPaths
|
|
146
|
+
? { paths: [...options.touchedPaths].filter((p) => p.trim().length > 0) }
|
|
147
|
+
: collectTouchedPaths(claim, cwd);
|
|
148
|
+
if (touched.unverifiableReason) {
|
|
149
|
+
return {
|
|
150
|
+
verdict: { kind: 'unverifiable', reason: touched.unverifiableReason },
|
|
151
|
+
touchedPaths: [],
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
// A declared `paths[]` footprint is the claim's own machine-readable statement
|
|
155
|
+
// of intent, so it outranks the prose scope when present — that is the entire
|
|
156
|
+
// reason C0-b made it optional-but-additive.
|
|
157
|
+
//
|
|
158
|
+
// Comma, not space: `resolveScopeToPathspecs` splits on ',' and treats any
|
|
159
|
+
// whitespace inside a token as proof of prose (dirty-scope.ts:143-154), so a
|
|
160
|
+
// space-joined list would silently classify as unverifiable.
|
|
161
|
+
const declared = claim.paths?.length ? claim.paths.join(',') : claim.scope;
|
|
162
|
+
const verdict = assessScopeConformity({
|
|
163
|
+
scope: declared,
|
|
164
|
+
cwd: claimWorkdir(claim, cwd) ?? cwd,
|
|
165
|
+
touchedPaths: touched.paths,
|
|
166
|
+
});
|
|
167
|
+
if (verdict.kind !== 'out_of_scope') {
|
|
168
|
+
return { verdict, touchedPaths: touched.paths };
|
|
169
|
+
}
|
|
170
|
+
const shown = verdict.unexpected.slice(0, MAX_REPORTED_PATHS);
|
|
171
|
+
const overflow = verdict.unexpected.length - shown.length;
|
|
172
|
+
return {
|
|
173
|
+
verdict,
|
|
174
|
+
touchedPaths: touched.paths,
|
|
175
|
+
warning: {
|
|
176
|
+
code: 'wrote_outside_claim_scope',
|
|
177
|
+
message: `Claim ${claim.id} declared '${claim.scope}' but ${verdict.unexpected.length} touched `
|
|
178
|
+
+ `file(s) sit outside it: ${shown.join(', ')}`
|
|
179
|
+
+ (overflow > 0 ? ` (+${overflow} more)` : '')
|
|
180
|
+
+ '. Advisory only — the work is already written.',
|
|
181
|
+
data: {
|
|
182
|
+
claim_id: claim.id,
|
|
183
|
+
scope: claim.scope,
|
|
184
|
+
declared_pathspecs: verdict.pathspecs,
|
|
185
|
+
unexpected_paths: shown,
|
|
186
|
+
...(overflow > 0 ? { unexpected_paths_omitted: overflow } : {}),
|
|
187
|
+
base_sha: claim.base_sha,
|
|
188
|
+
},
|
|
189
|
+
next_actions: widenNextActions(claim, verdict.unexpected),
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
//# sourceMappingURL=claim-conformity.js.map
|