tickmarkr 2.8.3 → 2.8.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/claude-code.js +6 -2
- package/dist/adapters/codex-commit-check.js +2 -1
- package/dist/adapters/cursor-agent.js +2 -1
- package/dist/adapters/grok.js +2 -1
- package/dist/adapters/kimi.js +41 -10
- package/dist/adapters/opencode.js +2 -1
- package/dist/adapters/pi.js +3 -2
- package/dist/adapters/qwen.js +2 -1
- package/dist/adapters/registry.js +2 -1
- package/dist/adapters/types.d.ts +2 -0
- package/dist/adapters/types.js +18 -3
- package/dist/cli/commands/doctor.d.ts +49 -5
- package/dist/cli/commands/doctor.js +956 -94
- package/dist/cli/commands/fleet.js +11 -3
- package/dist/cli/commands/init.js +2 -2
- package/dist/cli/commands/verify.js +12 -3
- package/dist/compile/index.js +4 -1
- package/dist/config/config.d.ts +15 -0
- package/dist/config/config.js +67 -5
- package/dist/drivers/host-env.d.ts +3 -0
- package/dist/drivers/host-env.js +47 -0
- package/dist/drivers/orca.js +3 -1
- package/dist/drivers/subprocess.d.ts +1 -1
- package/dist/drivers/subprocess.js +4 -3
- package/dist/gates/acceptance.js +130 -12
- package/dist/gates/baseline.d.ts +1 -1
- package/dist/gates/llm.js +11 -6
- package/dist/run/git.js +12 -13
- package/dist/run/journal.js +40 -11
- package/dist/run/lock.d.ts +5 -0
- package/dist/run/lock.js +202 -9
- package/dist/run/protocol.d.ts +1 -1
- package/dist/tui/cockpit/live-store.js +12 -2
- package/package.json +1 -1
- package/skills/tickmarkr-auto/SKILL.md +65 -21
- package/skills/tickmarkr-loop/SKILL.md +65 -21
- package/skills/tickmarkr-overseer/SKILL.md +88 -32
|
@@ -7,7 +7,7 @@ import { allAdapters, discoverChannels, doctorAgeMs, initDoctorReuse, modelAuthE
|
|
|
7
7
|
import { catalogModelAdvisory, catalogTierRanking, declaredModelWindow, fleetUnclassifiedModels } from "../../adapters/model-lints.js";
|
|
8
8
|
import { CATALOG_REFRESH_TIMEOUT_MS, formatCatalogRefreshLegs, readCachedCatalog, refreshCatalogCommand } from "../../adapters/catalog-remote.js";
|
|
9
9
|
import { CLAUDE_ALIAS_IDENTITY_STAMPS, readClaudeAliasIdentity } from "../../adapters/claude-code.js";
|
|
10
|
-
import { ConfigError, DEFAULT_CONFIG, fleetEditableFromConfig, fleetEditableEquals, formatFleetPrint, globalConfigDir, DENY_SCOPES, initialDenyPropOf, loadConfigWithMode, lowerLayerModelOverrides, overlayBytesLoadError, readOverlayFile, stagedDenyKeyOf, renderFleetOverlayWrite, repoOverlayPath, repoShadowedUserKeys, ROUTING_MODES, TIER_RANK, unifiedYamlDiff, universeEntryMatches, } from "../../config/config.js";
|
|
10
|
+
import { ConfigError, DEFAULT_CONFIG, fleetEditableFromConfig, fleetEditableEquals, formatFleetPrint, globalConfigDir, DENY_SCOPES, initialDenyPropOf, loadConfigWithMode, lowerLayerModelOverrides, overlayBytesLoadError, ownRepoOverlayPath, readOverlayFile, stagedDenyKeyOf, renderFleetOverlayWrite, repoOverlayPath, repoShadowedUserKeys, ROUTING_MODES, TIER_RANK, unifiedYamlDiff, universeEntryMatches, } from "../../config/config.js";
|
|
11
11
|
import { exclusionReason, projectFleetWhy, renderFleetWhy } from "../../config/fleet-why.js";
|
|
12
12
|
import { overlayHoldingLine, overlayHoldsMap } from "../../config/fleet-overlay.js";
|
|
13
13
|
import { SHAPES, TIERS } from "../../graph/schema.js";
|
|
@@ -355,7 +355,10 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
355
355
|
// them (destinationEdits below) — the repository overlay when it declares the key or masks it
|
|
356
356
|
// with null at it or above, else the user overlay.
|
|
357
357
|
const userPath = join(globalDir, "config.yaml");
|
|
358
|
-
|
|
358
|
+
// Queue row 195 / D-2102: the save destination is the checkout's OWN file even when the reader
|
|
359
|
+
// fallback (repoOverlayPath) has this linked worktree reading its main checkout's overlay — a
|
|
360
|
+
// first save here creates the worktree's own overlay, which then wins wholesale.
|
|
361
|
+
const repoPath = ownRepoOverlayPath(cwd);
|
|
359
362
|
const currentUserText = () => (existsSync(userPath) ? readFileSync(userPath, "utf8") : "");
|
|
360
363
|
const pathOf = (destination) => (destination === "repo" ? repoPath : userPath);
|
|
361
364
|
// the candidate bytes stand in for the destination layer in the one loader path
|
|
@@ -1243,7 +1246,12 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
1243
1246
|
// T10: both overlays are guarded whichever one is written, so a holder that moved after the review
|
|
1244
1247
|
// (the destination itself) refuses instead of retargeting. Null = still current.
|
|
1245
1248
|
const stalePreview = (pending, reviewed, prior) => {
|
|
1246
|
-
|
|
1249
|
+
// Queue row 195 / D-2102: a repo-destination review renders against the EFFECTIVE repository
|
|
1250
|
+
// overlay — in a linked worktree with no overlay of its own that is the main checkout's file,
|
|
1251
|
+
// while the write lands in the worktree's own (ownRepoOverlayPath). The guard therefore
|
|
1252
|
+
// re-reads the effective layer, not the destination's prior bytes; either file moving after
|
|
1253
|
+
// the review moves that read (an own file appearing wins wholesale), so the refusal stands.
|
|
1254
|
+
const destinationNow = pending.destination === "repo" ? currentRepoOverlayText(cwd) : (prior ?? currentUserText());
|
|
1247
1255
|
const repoNow = pending.destination === "repo" ? destinationNow : currentRepoOverlayText(cwd);
|
|
1248
1256
|
const userNow = pending.destination === "user" ? destinationNow : currentUserText();
|
|
1249
1257
|
if (repoNow !== pending.repo) {
|
|
@@ -24,7 +24,7 @@ const ENVIRONMENTS_FOOTER = [
|
|
|
24
24
|
"environments:",
|
|
25
25
|
" herdr — the full cockpit — every worker, judge, and consult is a visible pane you can watch and unblock · https://herdr.dev · auto picks it first when HERDR_ENV=1",
|
|
26
26
|
" orca — visible terminals in the Orca app — auto picks it inside an Orca terminal after herdr and before subprocess; set driver: orca to force it · https://onorca.dev",
|
|
27
|
-
" claude code — tickmarkr init --agent installs the /tkr skills + AGENTS.md so Claude Code (or any agent CLI) drives the loop natively",
|
|
27
|
+
" claude code — tickmarkr init --agent installs the /tkr skills + AGENTS.md so Claude Code (or any agent CLI) drives the loop natively; in Claude Desktop's Code tab (neither herdr nor Orca) one single-tier session runs the loop on the headless subprocess driver, watched with tickmarkr status --watch --plain",
|
|
28
28
|
" anywhere — no herdr or Orca terminal? same fail-closed gates, headless subprocess driver (legacy: no herdr? same fail-closed gates, headless subprocess driver when Orca markers are absent)",
|
|
29
29
|
].join("\n");
|
|
30
30
|
/** Current run truth, lock first; falls back to an abandoned journal. */
|
|
@@ -115,7 +115,7 @@ A run is green only when ALL of these hold: the run-end event exists in the jour
|
|
|
115
115
|
|
|
116
116
|
### Verified handoffs
|
|
117
117
|
|
|
118
|
-
When relaying missions between agents, never use bare send-text (\`herdr agent send\` / pane send-text) — it omits Enter. Use \`herdr pane run <pane> "<message>"\` or \`herdr notification show "<message>"\`. Confirm delivery by reading the TARGET composer afterward; if the sent text still sits unsubmitted after ~15 s, retry the Enter only when that composer read shows the staged text and no permission prompt or numbered choice holds focus — an Enter sent onto an active prompt approves it or picks a choice instead of submitting the message (OBS-1119). Under that guard, send-keys enter to that pane once and read again (OBS-975); when a prompt or choice holds focus instead, resolve it first and re-read before retrying. Never report "relayed" without read-back.
|
|
118
|
+
When relaying missions between agents, never use bare send-text (\`herdr agent send\` / pane send-text) — it omits Enter. Use \`herdr pane run <pane> "<message>"\` or \`herdr notification show "<message>"\`. Confirm delivery by reading the TARGET composer afterward; if the sent text still sits unsubmitted after ~15 s, retry the Enter only when that composer read shows the staged text and no permission prompt or numbered choice holds focus — an Enter sent onto an active prompt approves it or picks a choice instead of submitting the message (OBS-1119). Under that guard, send-keys enter to that pane once and read again (OBS-975); when a prompt or choice holds focus instead, resolve it first and re-read before retrying. Never report "relayed" without read-back. On a plain host (neither herdr nor Orca) there is no pane to read back: hand off through the host's cross-session message tool, and count the handoff confirmed only by the peer's reply.
|
|
119
119
|
|
|
120
120
|
### Orient before you act — this block may be the ONLY guidance your host loaded
|
|
121
121
|
|
|
@@ -14,7 +14,7 @@ import { getTask, loadGraph } from "../../graph/graph.js";
|
|
|
14
14
|
import { GATE_NAMES } from "../../graph/schema.js";
|
|
15
15
|
import { executionSignal } from "../../run/execution-budget.js";
|
|
16
16
|
import { addDetachedWorktree, linkNodeModules, reflogWitness, removeWorktree, shGit, shGitOk } from "../../run/git.js";
|
|
17
|
-
import { foldOwedChecks, integrationMapped, owedCriteria, Journal, OWED_DISCHARGE_EVENT } from "../../run/journal.js";
|
|
17
|
+
import { foldOwedChecks, integrationMapped, owedCriteria, Journal, OWED_DISCHARGE_EVENT, parseRunId } from "../../run/journal.js";
|
|
18
18
|
import { describeLeaseWait, withRepositoryLease } from "../../run/lease.js";
|
|
19
19
|
import { isPidLive } from "../../run/lock.js";
|
|
20
20
|
import { LAUNCHSERVICES_PORT_STOP, probeLaunchServicesPorts, recordLaunchServices } from "../../run/launchservices-check.js";
|
|
@@ -195,8 +195,17 @@ export async function verify(argv, cwd = process.cwd(), options = {}) {
|
|
|
195
195
|
const stateRoot = await verifyStateRoot(cwd);
|
|
196
196
|
const fileRoot = (file) => existsSync(join(cwd, ".tickmarkr", file)) ? cwd : stateRoot;
|
|
197
197
|
if (resolve(stateRoot) !== resolve(cwd)) {
|
|
198
|
-
|
|
199
|
-
|
|
198
|
+
// Queue row 195 / D-2102: the fallback is no longer "read-only" — the one line names every
|
|
199
|
+
// state file with the path it was read from and its origin, AND every path this invocation
|
|
200
|
+
// writes: the shared verdict store always, doctor.json when present, the --record journal when given.
|
|
201
|
+
const reads = STATE_FILES.map(file => `${file}: ${join(fileRoot(file), ".tickmarkr", file)} (${fileRoot(file) === cwd ? "local" : "common root"})`).join("; ");
|
|
202
|
+
const doctorPath = join(fileRoot("doctor.json"), ".tickmarkr", "doctor.json");
|
|
203
|
+
const writes = [
|
|
204
|
+
`verdict store ${getVerdictStore(join(stateRoot, ".tickmarkr")).dir}`,
|
|
205
|
+
...(existsSync(doctorPath) ? [`doctor.json ${doctorPath}`] : []),
|
|
206
|
+
...(values.record ? [`record journal ${join(stateRoot, ".tickmarkr", "runs", parseRunId(values.record), "journal.jsonl")}`] : []),
|
|
207
|
+
].join("; ");
|
|
208
|
+
console.error(`verify: linked worktree — state files resolve from ${join(stateRoot, ".tickmarkr")} (reads: ${reads}; writes: ${writes})`);
|
|
200
209
|
}
|
|
201
210
|
const cfg = loadConfig(fileRoot("config.yaml"));
|
|
202
211
|
const head = (await shGitOk("git rev-parse HEAD", cwd)).trim();
|
package/dist/compile/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { parse } from "yaml";
|
|
4
|
+
import { repoOverlayPath } from "../config/config.js";
|
|
4
5
|
import { validateGraph } from "../graph/schema.js";
|
|
5
6
|
import { taskUnitContractErrors } from "./collateral.js";
|
|
6
7
|
import { blocksCompile, ownershipFindings, renderOwnershipFinding } from "./ownership.js";
|
|
@@ -37,7 +38,9 @@ function detect(src) {
|
|
|
37
38
|
function repoOverlayMode(repoRoot) {
|
|
38
39
|
if (!repoRoot)
|
|
39
40
|
return undefined;
|
|
40
|
-
|
|
41
|
+
// Queue row 195 / D-2102: the ONE repo-layer path rule — a linked worktree without an overlay of
|
|
42
|
+
// its own proves against the main checkout's file, never against defaults.
|
|
43
|
+
const path = repoOverlayPath(repoRoot);
|
|
41
44
|
if (!existsSync(path))
|
|
42
45
|
return undefined;
|
|
43
46
|
try {
|
package/dist/config/config.d.ts
CHANGED
|
@@ -459,6 +459,21 @@ export type FleetEditable = Partial<Record<DenyScopeKey, string[]>> & {
|
|
|
459
459
|
map: Record<string, MapEntry>;
|
|
460
460
|
floors: Record<string, Tier>;
|
|
461
461
|
};
|
|
462
|
+
/** The checkout's OWN repo-layer overlay — every writer's destination, and the reader's answer in
|
|
463
|
+
* every case but the one fallback below. */
|
|
464
|
+
export declare function ownRepoOverlayPath(repoRoot: string): string;
|
|
465
|
+
/**
|
|
466
|
+
* Queue row 195 / D-2102: .tickmarkr/ is gitignored, so a fresh session worktree has no overlay of
|
|
467
|
+
* its own and every command there would silently run on defaults plus the global layer. ONE rule,
|
|
468
|
+
* and every repo-layer reader goes through it: a checkout whose own state-dir config.yaml exists
|
|
469
|
+
* reads it; otherwise a LINKED worktree — a gitfile checkout whose trusted common dir
|
|
470
|
+
* (src/run/git-trust.ts trustedCommonDir) lies outside it — reads
|
|
471
|
+
* dirname(common dir)/<state dir>/config.yaml when that file exists; every other case (a main
|
|
472
|
+
* checkout or its subdirectory, no enclosing checkout, a git trust refusal, an absent main file)
|
|
473
|
+
* keeps the checkout's own path. WRITERS never follow this fallback: they use ownRepoOverlayPath.
|
|
474
|
+
* The trust check includes missing, redirected or symlinked commondir metadata and config.worktree
|
|
475
|
+
* under extensions.worktreeConfig; none can authorize a shared overlay.
|
|
476
|
+
*/
|
|
462
477
|
export declare function repoOverlayPath(repoRoot: string): string;
|
|
463
478
|
export declare function readOverlayFile(path: string): Record<string, unknown>;
|
|
464
479
|
/** A discovered fleet universe row: the served models of one adapter and, per model, the resolved
|
package/dist/config/config.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
1
|
+
import { existsSync, lstatSync, readFileSync, realpathSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
3
|
+
import { dirname, join, sep } from "node:path";
|
|
4
4
|
import { filesGlob } from "../graph/files-glob.js";
|
|
5
5
|
import { Document, isScalar, parse } from "yaml";
|
|
6
6
|
import { z } from "zod";
|
|
7
7
|
import { CITED_MODEL_WINDOWS } from "../adapters/model-windows.js";
|
|
8
8
|
import { stateDirName } from "../graph/graph.js";
|
|
9
|
+
import { assertGitTrust, trustedCommonDir } from "../run/git-trust.js";
|
|
9
10
|
import { EFFORTS, SHAPES, TIERS } from "../graph/schema.js";
|
|
10
11
|
const TierEnum = z.enum(TIERS, {
|
|
11
12
|
error: (iss) => `Invalid option: expected one of "cheap"|"mid"|"frontier" (got ${JSON.stringify(iss.input)})`,
|
|
@@ -744,7 +745,7 @@ export function globalConfigDir() {
|
|
|
744
745
|
}
|
|
745
746
|
export function overlayPreferShapes(repoRoot, opts = {}) {
|
|
746
747
|
const shapes = new Set();
|
|
747
|
-
for (const layer of [readYaml(join(opts.globalDir ?? globalConfigDir(), "config.yaml")), readYaml(
|
|
748
|
+
for (const layer of [readYaml(join(opts.globalDir ?? globalConfigDir(), "config.yaml")), readYaml(repoOverlayPath(repoRoot))]) {
|
|
748
749
|
const map = layer?.routing?.map;
|
|
749
750
|
if (!map)
|
|
750
751
|
continue;
|
|
@@ -835,7 +836,7 @@ export function loadConfigWithMode(repoRoot, opts = {}) {
|
|
|
835
836
|
// v1.52 T2: repoOverlayText substitutes candidate bytes for the on-disk repo layer — the fleet
|
|
836
837
|
// write guard validates EXACTLY what it is about to write through this one loader path.
|
|
837
838
|
const repoCfg = opts.repoOverlayText === undefined
|
|
838
|
-
? readYaml(
|
|
839
|
+
? readYaml(repoOverlayPath(repoRoot))
|
|
839
840
|
: parse(opts.repoOverlayText);
|
|
840
841
|
const merged = deepMerge(deepMerge(structuredClone(DEFAULT_CONFIG), globalCfg), repoCfg);
|
|
841
842
|
const r = TickmarkrConfigSchema.safeParse(merged);
|
|
@@ -1017,9 +1018,70 @@ export function configTemplate(overlay) {
|
|
|
1017
1018
|
}
|
|
1018
1019
|
// Fleet overlay mutation, legacy serialization, and diff rendering live together in fleet-overlay.ts.
|
|
1019
1020
|
export { FLEET_OVERLAY_KEYS, fleetEditableEquals, fleetRepoOverlayFromDelta, renderFleetOverlayWrite, repoOverlayYaml, serializeFleetOverlay, unifiedYamlDiff, } from "./fleet-overlay.js";
|
|
1020
|
-
|
|
1021
|
+
/** The checkout's OWN repo-layer overlay — every writer's destination, and the reader's answer in
|
|
1022
|
+
* every case but the one fallback below. */
|
|
1023
|
+
export function ownRepoOverlayPath(repoRoot) {
|
|
1021
1024
|
return join(repoRoot, stateDirName(repoRoot), "config.yaml");
|
|
1022
1025
|
}
|
|
1026
|
+
/**
|
|
1027
|
+
* Queue row 195 / D-2102: .tickmarkr/ is gitignored, so a fresh session worktree has no overlay of
|
|
1028
|
+
* its own and every command there would silently run on defaults plus the global layer. ONE rule,
|
|
1029
|
+
* and every repo-layer reader goes through it: a checkout whose own state-dir config.yaml exists
|
|
1030
|
+
* reads it; otherwise a LINKED worktree — a gitfile checkout whose trusted common dir
|
|
1031
|
+
* (src/run/git-trust.ts trustedCommonDir) lies outside it — reads
|
|
1032
|
+
* dirname(common dir)/<state dir>/config.yaml when that file exists; every other case (a main
|
|
1033
|
+
* checkout or its subdirectory, no enclosing checkout, a git trust refusal, an absent main file)
|
|
1034
|
+
* keeps the checkout's own path. WRITERS never follow this fallback: they use ownRepoOverlayPath.
|
|
1035
|
+
* The trust check includes missing, redirected or symlinked commondir metadata and config.worktree
|
|
1036
|
+
* under extensions.worktreeConfig; none can authorize a shared overlay.
|
|
1037
|
+
*/
|
|
1038
|
+
export function repoOverlayPath(repoRoot) {
|
|
1039
|
+
const own = ownRepoOverlayPath(repoRoot);
|
|
1040
|
+
if (existsSync(own))
|
|
1041
|
+
return own;
|
|
1042
|
+
let dotGit;
|
|
1043
|
+
try {
|
|
1044
|
+
for (let dir = realpathSync(repoRoot);;) {
|
|
1045
|
+
const candidate = join(dir, ".git");
|
|
1046
|
+
try {
|
|
1047
|
+
lstatSync(candidate);
|
|
1048
|
+
dotGit = candidate;
|
|
1049
|
+
break;
|
|
1050
|
+
}
|
|
1051
|
+
catch { /* keep walking */ }
|
|
1052
|
+
const up = dirname(dir);
|
|
1053
|
+
if (up === dir)
|
|
1054
|
+
break;
|
|
1055
|
+
dir = up;
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
catch {
|
|
1059
|
+
return own;
|
|
1060
|
+
}
|
|
1061
|
+
// A directory .git is a main checkout (or one of its subdirectories); only a gitfile can be linked.
|
|
1062
|
+
if (!dotGit)
|
|
1063
|
+
return own;
|
|
1064
|
+
try {
|
|
1065
|
+
if (!lstatSync(dotGit).isFile())
|
|
1066
|
+
return own;
|
|
1067
|
+
}
|
|
1068
|
+
catch {
|
|
1069
|
+
return own;
|
|
1070
|
+
}
|
|
1071
|
+
let common;
|
|
1072
|
+
try {
|
|
1073
|
+
assertGitTrust(repoRoot);
|
|
1074
|
+
common = trustedCommonDir(repoRoot);
|
|
1075
|
+
}
|
|
1076
|
+
catch {
|
|
1077
|
+
return own;
|
|
1078
|
+
}
|
|
1079
|
+
const checkoutRoot = dirname(dotGit);
|
|
1080
|
+
if (common === checkoutRoot || common.startsWith(`${checkoutRoot}${sep}`))
|
|
1081
|
+
return own; // not linked
|
|
1082
|
+
const shared = ownRepoOverlayPath(dirname(common));
|
|
1083
|
+
return existsSync(shared) ? shared : own;
|
|
1084
|
+
}
|
|
1023
1085
|
export function readOverlayFile(path) {
|
|
1024
1086
|
const raw = readYaml(path);
|
|
1025
1087
|
if (raw === undefined || raw === null)
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export declare const HOST_SESSION_VARS: readonly ["CLAUDECODE", "CLAUDE_CODE_ENTRYPOINT", "CLAUDE_CODE_SESSION_ID", "CLAUDE_CODE_HOST_SESSION_ID", "CLAUDE_CODE_CHILD_SESSION", "CLAUDE_CODE_MESSAGING_SOCKET", "CLAUDE_CODE_MESSAGING_TOKEN", "CLAUDE_CODE_SDK_HAS_HOST_AUTH_REFRESH", "CLAUDE_CODE_SESSION_ATTENDED", "CLAUDE_CODE_EXECPATH", "CLAUDE_CODE_DESKTOP_APP_VERSION", "CLAUDE_CODE_TERMINAL_MCP_TOOLS", "CLAUDE_CODE_ENABLE_ASK_USER_QUESTION_TOOL", "CLAUDE_CODE_EMIT_TOOL_USE_SUMMARIES", "CLAUDE_CODE_ENABLE_SDK_FILE_CHECKPOINTING", "CLAUDE_CODE_REPORT_FINDINGS", "CLAUDE_CODE_STARTUP_FAILURE_RESULTS", "CLAUDE_CODE_EAGER_FLUSH", "CLAUDE_CODE_DISABLE_CRON", "CLAUDE_CODE_SIMPLE", "CLAUDE_CODE_DISABLE_TERMINAL_TITLE", "CLAUDE_CODE_ACCOUNT_UUID", "CLAUDE_CODE_ORGANIZATION_UUID", "CLAUDE_CODE_USER_EMAIL", "CLAUDE_CODE_OAUTH_SCOPES", "CLAUDE_AGENT_SDK_VERSION", "CLAUDE_AGENT_SDK_MCP_NO_PREFIX", "CLAUDE_PID", "CLAUDE_EFFORT", "CLAUDE_PREVIEW_CLASSIFIER_FLOOR", "CLAUDE_CODE_BRIDGE_SESSION_ID"];
|
|
2
|
+
/** A copy of `env` without the host-session names; the input (the daemon's own process.env) is never mutated. */
|
|
3
|
+
export declare function withoutHostSession(env?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// Queue row 193 (D-2102 add.1): a daemon launched from an agent session (the Claude Desktop Code tab, or a
|
|
2
|
+
// claude CLI seat on Orca or herdr) carries that session's host variables, and an agent CLI that inherits
|
|
3
|
+
// them gains the host's tools and effort. This CLOSED denylist is stripped at every choke point an agent
|
|
4
|
+
// CLI or gate command passes: the shell seam, the subprocess driver's seal, the pane seeds and the bare
|
|
5
|
+
// probes. Exact names only — never a prefix: an unlisted CLAUDE_CODE variable and every user auth or
|
|
6
|
+
// provider variable reach children byte-equal. Declared residual: a host variable a future host adds
|
|
7
|
+
// reaches children until it is listed here. Leaf module: imports nothing from src/.
|
|
8
|
+
export const HOST_SESSION_VARS = [
|
|
9
|
+
"CLAUDECODE",
|
|
10
|
+
"CLAUDE_CODE_ENTRYPOINT",
|
|
11
|
+
"CLAUDE_CODE_SESSION_ID",
|
|
12
|
+
"CLAUDE_CODE_HOST_SESSION_ID",
|
|
13
|
+
"CLAUDE_CODE_CHILD_SESSION",
|
|
14
|
+
"CLAUDE_CODE_MESSAGING_SOCKET",
|
|
15
|
+
"CLAUDE_CODE_MESSAGING_TOKEN",
|
|
16
|
+
"CLAUDE_CODE_SDK_HAS_HOST_AUTH_REFRESH",
|
|
17
|
+
"CLAUDE_CODE_SESSION_ATTENDED",
|
|
18
|
+
"CLAUDE_CODE_EXECPATH",
|
|
19
|
+
"CLAUDE_CODE_DESKTOP_APP_VERSION",
|
|
20
|
+
"CLAUDE_CODE_TERMINAL_MCP_TOOLS",
|
|
21
|
+
"CLAUDE_CODE_ENABLE_ASK_USER_QUESTION_TOOL",
|
|
22
|
+
"CLAUDE_CODE_EMIT_TOOL_USE_SUMMARIES",
|
|
23
|
+
"CLAUDE_CODE_ENABLE_SDK_FILE_CHECKPOINTING",
|
|
24
|
+
"CLAUDE_CODE_REPORT_FINDINGS",
|
|
25
|
+
"CLAUDE_CODE_STARTUP_FAILURE_RESULTS",
|
|
26
|
+
"CLAUDE_CODE_EAGER_FLUSH",
|
|
27
|
+
"CLAUDE_CODE_DISABLE_CRON",
|
|
28
|
+
"CLAUDE_CODE_SIMPLE",
|
|
29
|
+
"CLAUDE_CODE_DISABLE_TERMINAL_TITLE",
|
|
30
|
+
"CLAUDE_CODE_ACCOUNT_UUID",
|
|
31
|
+
"CLAUDE_CODE_ORGANIZATION_UUID",
|
|
32
|
+
"CLAUDE_CODE_USER_EMAIL",
|
|
33
|
+
"CLAUDE_CODE_OAUTH_SCOPES",
|
|
34
|
+
"CLAUDE_AGENT_SDK_VERSION",
|
|
35
|
+
"CLAUDE_AGENT_SDK_MCP_NO_PREFIX",
|
|
36
|
+
"CLAUDE_PID",
|
|
37
|
+
"CLAUDE_EFFORT",
|
|
38
|
+
"CLAUDE_PREVIEW_CLASSIFIER_FLOOR",
|
|
39
|
+
"CLAUDE_CODE_BRIDGE_SESSION_ID",
|
|
40
|
+
];
|
|
41
|
+
/** A copy of `env` without the host-session names; the input (the daemon's own process.env) is never mutated. */
|
|
42
|
+
export function withoutHostSession(env = process.env) {
|
|
43
|
+
const out = { ...env };
|
|
44
|
+
for (const k of HOST_SESSION_VARS)
|
|
45
|
+
delete out[k];
|
|
46
|
+
return out;
|
|
47
|
+
}
|
package/dist/drivers/orca.js
CHANGED
|
@@ -7,6 +7,7 @@ import { createWorktree, FORK_CAP_ENV, resolvedForkCap, sh } from "../run/git.js
|
|
|
7
7
|
import { Journal, parseRunId } from "../run/journal.js";
|
|
8
8
|
import { stateDirName } from "../graph/graph.js";
|
|
9
9
|
import { readWatchBoard, requestWatchBoardStop, stopWatchBoard, supervisionPresencePath, WATCH_OWNER_ENV } from "../run/supervision.js";
|
|
10
|
+
import { HOST_SESSION_VARS } from "./host-env.js";
|
|
10
11
|
import { MAX_BUF } from "./subprocess.js";
|
|
11
12
|
import { formatOwnedName, panesToClose, parseOwnedName } from "./types.js";
|
|
12
13
|
// Orca (onorca.dev) as a third execution surface beside herdr and subprocess. tickmarkr keeps
|
|
@@ -966,7 +967,8 @@ export class OrcaDriver {
|
|
|
966
967
|
// UI's active worktree (`active`/`current`) nor the daemon's cwd — and the command itself moves
|
|
967
968
|
// into the checkout, because Orca cannot be asked to place a terminal in a path it does not
|
|
968
969
|
// track (OBS-1004). Shell startup can swallow that prefix; prove it from scrollback below.
|
|
969
|
-
|
|
970
|
+
// Queue row 193: the fork cap stays first; the agent session's host variables never reach the worker.
|
|
971
|
+
const payload = `export ${FORK_CAP_ENV}=${shq(process.env[FORK_CAP_ENV] ?? resolvedForkCap())}; unset ${HOST_SESSION_VARS.join(" ")}; ${cmd}`;
|
|
970
972
|
launchGuards.getStore()?.(); // v2.6.8 T1: eligibility is re-read here, after every awaited lookup above
|
|
971
973
|
const env = await this.call("create", [
|
|
972
974
|
"terminal", "create", "--worktree", `path:${tracked}`, "--title", st.title, "--command", inCheckout(st.cwd, payload),
|
|
@@ -2,7 +2,7 @@ import type { ExecutorDriver, NotifyOpts, Slot, TransportState } from "./types.j
|
|
|
2
2
|
export declare const MAX_BUF: number;
|
|
3
3
|
export declare const HERDR_CONTROL_VARS: readonly ["HERDR_ENV", "HERDR_SOCKET_PATH", "ORCA_TERMINAL_HANDLE", "ORCA_PANE_KEY", "ORCA_TAB_ID"];
|
|
4
4
|
/**
|
|
5
|
-
* Copy of worker env with the fork cap applied and host control-plane vars stripped.
|
|
5
|
+
* Copy of worker env with the fork cap applied and host control-plane and host-session vars stripped.
|
|
6
6
|
* Workers retain the run's concurrency-derived cap (resolvedForkCap), independently of
|
|
7
7
|
* verification's suite-window budget. The operator's own export still wins.
|
|
8
8
|
*/
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { shq } from "../adapters/types.js";
|
|
3
3
|
import { createWorktree, FORK_CAP_ENV, resolvedForkCap } from "../run/git.js";
|
|
4
|
+
import { HOST_SESSION_VARS, withoutHostSession } from "./host-env.js";
|
|
4
5
|
// HARD-03: cap retained worker output so a chatty worker can't grow tickmarkr's heap unbounded.
|
|
5
6
|
// 2MB is safe against BOTH consumers: the largest read() call site asks for 1000 lines
|
|
6
7
|
// (daemon.ts:217/247), and every marker (TICKMARKR_RESULT, TICKMARKR_EXIT_<nonce>) is emitted at the
|
|
@@ -21,12 +22,12 @@ export const HERDR_CONTROL_VARS = [
|
|
|
21
22
|
"ORCA_TAB_ID",
|
|
22
23
|
];
|
|
23
24
|
/**
|
|
24
|
-
* Copy of worker env with the fork cap applied and host control-plane vars stripped.
|
|
25
|
+
* Copy of worker env with the fork cap applied and host control-plane and host-session vars stripped.
|
|
25
26
|
* Workers retain the run's concurrency-derived cap (resolvedForkCap), independently of
|
|
26
27
|
* verification's suite-window budget. The operator's own export still wins.
|
|
27
28
|
*/
|
|
28
29
|
export function sealHerdrEnv(env = process.env) {
|
|
29
|
-
const out =
|
|
30
|
+
const out = withoutHostSession(env); // queue row 193
|
|
30
31
|
if (!(FORK_CAP_ENV in out))
|
|
31
32
|
out[FORK_CAP_ENV] = resolvedForkCap();
|
|
32
33
|
for (const k of HERDR_CONTROL_VARS)
|
|
@@ -37,7 +38,7 @@ export function sealHerdrEnv(env = process.env) {
|
|
|
37
38
|
export function herdrSealShellPrefix(env = process.env) {
|
|
38
39
|
const forkCap = sealHerdrEnv(env)[FORK_CAP_ENV] ?? resolvedForkCap();
|
|
39
40
|
return `export ${FORK_CAP_ENV}=${shq(forkCap)}; ` +
|
|
40
|
-
HERDR_CONTROL_VARS.map((k) => `unset ${k}`).join("; ") + "
|
|
41
|
+
HERDR_CONTROL_VARS.map((k) => `unset ${k}`).join("; ") + `; unset ${HOST_SESSION_VARS.join(" ")}; `;
|
|
41
42
|
}
|
|
42
43
|
export class SubprocessDriver {
|
|
43
44
|
id = "subprocess";
|
package/dist/gates/acceptance.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { lstatSync, mkdtempSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
3
5
|
import { z } from "zod";
|
|
4
6
|
import { channelKey, shq } from "../adapters/types.js";
|
|
5
7
|
import { compileNative } from "../compile/native.js";
|
|
6
8
|
import { DEFAULT_DIFF_CAP } from "../config/config.js";
|
|
7
9
|
import { renderAcceptanceItem } from "../graph/schema.js";
|
|
8
|
-
import { sh } from "../run/git.js";
|
|
10
|
+
import { DEFAULT_SHELL_TIMEOUT_MS, sh, shell } from "../run/git.js";
|
|
9
11
|
import { checkTaskDiffCaps, fetchTaskDiff, isProtectedEvidence, setAsideReceiptPath } from "./review.js";
|
|
10
12
|
import { appendAnchoredReview, COMPLETION_FAKING_CHECKLIST, extractVerdictJson, generateVerdictNonce, runLlm, SeatLaunchError, verdictNonceLine } from "./llm.js";
|
|
11
13
|
import { classifyVerdictCause } from "./verdict-cause.js";
|
|
@@ -13,6 +15,8 @@ import { reviewableLogicDiff } from "./artifact-manifest.js";
|
|
|
13
15
|
import { diffSidePath } from "./git-paths.js";
|
|
14
16
|
import { classifyFailureOutput } from "./baseline.js";
|
|
15
17
|
import { operationFactsAtHead, readOperationFacts } from "./operation-facts.js";
|
|
18
|
+
import { isVitestTestCommand, manifestChildEnv, readTestReport, singleForkRetryCommand, toManifestPath } from "./test-manifest.js";
|
|
19
|
+
import { TEST_REPORTER_SOURCE } from "./test-reporter.js";
|
|
16
20
|
// Fable F4: acceptance judge shares review's 900s timeout — 300s default killed frontier judges on cap-sized diffs.
|
|
17
21
|
const JUDGE_TIMEOUT_MS = 900_000;
|
|
18
22
|
const CitationSchema = z.object({ path: z.string(), line: z.number().int() });
|
|
@@ -98,6 +102,26 @@ export function testFiltered(testCmd, name) {
|
|
|
98
102
|
const fwd = wrapped ? "-- " : "";
|
|
99
103
|
return `${testCmd} ${fwd}-t ${shq(pattern)}`;
|
|
100
104
|
}
|
|
105
|
+
// Queue row 179: the criterion's suite file as the runner's file argument AHEAD of the title filter, appended to a
|
|
106
|
+
// base that already carries its forwarding separator (npmForwarded, under either runner). testFiltered keeps its
|
|
107
|
+
// two-argument form: a criterion with no suite file, and the eval harness.
|
|
108
|
+
const fileFiltered = (base, file, name) => `${base} ${shq(file)} -t ${shq(testFilterPattern(name))}`;
|
|
109
|
+
// The forwarding separator, decided on shell WORDS — the test gate's tokenizer (test-manifest.ts shellTokens/words,
|
|
110
|
+
// unexported) beside its unquoting — never on the raw text: behind any `NAME=value` assignments (whose value, even
|
|
111
|
+
// ` -- `, is not a word of the command), an npm manager word, bare, quoted or escaped, gets ONE `--` unless a later
|
|
112
|
+
// word already is `--` (bare, quoted or escaped). npx, pnpm, yarn and a direct runner forward their arguments as they
|
|
113
|
+
// are. Both runners' branches append through it; the Vitest branch asks the gate's parser only whether the reporter
|
|
114
|
+
// can be forwarded at all (V5), since that parser's own separator reads the raw text.
|
|
115
|
+
const SHELL_TOKEN = /(?:[^\s'"\\&|;<>]+|'[^']*'|"(?:\\.|[^"\\])*"|\\.)+|[&|;<>]+/g;
|
|
116
|
+
const unquoted = (token) => token.replace(/'([^']*)'|"((?:\\.|[^"\\])*)"|\\(.)/g, (_all, single, double, escaped) => single ?? (double !== undefined ? double.replace(/\\(["\\$`])/g, "$1") : escaped));
|
|
117
|
+
function npmForwarded(testCmd) {
|
|
118
|
+
const raw = testCmd.match(SHELL_TOKEN) ?? [];
|
|
119
|
+
let manager = 0;
|
|
120
|
+
while (/^[A-Za-z_]\w*=/.test(raw[manager] ?? ""))
|
|
121
|
+
manager++;
|
|
122
|
+
const argv = raw.slice(manager).map(unquoted);
|
|
123
|
+
return argv[0] === "npm" && !argv.includes("--") ? `${testCmd} --` : testCmd;
|
|
124
|
+
}
|
|
101
125
|
const VitestListedTestsSchema = z.array(z.object({
|
|
102
126
|
name: z.string(),
|
|
103
127
|
file: z.string(),
|
|
@@ -238,6 +262,99 @@ function oracleExecutionFailure(label, code, stdout, stderr) {
|
|
|
238
262
|
details: `oracle failed: ${label} (exit ${code})${tail(stderr || stdout)}`,
|
|
239
263
|
};
|
|
240
264
|
}
|
|
265
|
+
// today's rule for a test oracle's run: exit 0 AND a positive printed run count (OBS-55: a zero-match run is vacuous).
|
|
266
|
+
function countedTestRun(label, filter, r) {
|
|
267
|
+
if (r.code !== 0)
|
|
268
|
+
return oracleExecutionFailure(label, r.code, r.stdout, r.stderr);
|
|
269
|
+
const out = (r.stderr || "") + "\n" + (r.stdout || "");
|
|
270
|
+
const ran = testsRan(out);
|
|
271
|
+
if (ran === null || ran < 1) {
|
|
272
|
+
return { gate: "acceptance", pass: false,
|
|
273
|
+
details: `oracle failed: ${label} — name filter matched zero tests (filter: ${filter})${tail(out)}` };
|
|
274
|
+
}
|
|
275
|
+
return undefined;
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Queue row 179 (D-2018, D-2019): a test criterion that declares its suite file L is satisfied by L alone — run
|
|
279
|
+
* 1248's oracles named one file and a same-titled test in another satisfied them. Closed table:
|
|
280
|
+
* L1 L absent or not a regular file (a directory included) in the worktree: a work verdict naming L, under either
|
|
281
|
+
* runner, and the test command never starts.
|
|
282
|
+
* Vitest: L alone, file argument then title filter, under the test gate's nonce-bound per-file reporter, because
|
|
283
|
+
* Vitest's file filter selects EVERY path containing L. PASS only when that report, of this nonce and ending in
|
|
284
|
+
* its run-end certificate with exit 0, shows a passed body in module L and none run in any other module.
|
|
285
|
+
* V2 L without the title (another file holding it): no passed body in L. V3 a deeper path ending with L holding
|
|
286
|
+
* it, whether or not L does: a body run in another module. V4 report missing, another nonce or no certificate,
|
|
287
|
+
* and V5 a script body the reporter cannot be forwarded to: fail closed, unverified.
|
|
288
|
+
* Other runner: L as its file argument before the title filter, today's run-count rule. Declared residual: such a
|
|
289
|
+
* runner gives no per-file evidence, so a test of the title in a path containing L can still satisfy it there.
|
|
290
|
+
*/
|
|
291
|
+
async function landedTestOracle(testCmd, title, landing, worktree) {
|
|
292
|
+
const label = `test "${title}" in ${landing}`;
|
|
293
|
+
const passed = { gate: "acceptance", pass: true, details: `✓ test: ${title} (exit 0) in ${landing}` };
|
|
294
|
+
const work = (why) => ({ gate: "acceptance", pass: false, details: `oracle failed: ${label} — ${why}` });
|
|
295
|
+
const unverified = (why) => ({ gate: "acceptance", pass: false,
|
|
296
|
+
details: `oracle failed: ${label} — ${why}; unverified, failing closed`,
|
|
297
|
+
meta: { cause: "oracle-execution", classification: "infra", infra: true, retryable: false } });
|
|
298
|
+
const path = resolve(worktree, landing);
|
|
299
|
+
const within = relative(worktree, path);
|
|
300
|
+
let regular = false;
|
|
301
|
+
try {
|
|
302
|
+
regular = !within.startsWith("..") && !isAbsolute(within) && lstatSync(path).isFile();
|
|
303
|
+
}
|
|
304
|
+
catch { /* absent */ }
|
|
305
|
+
if (!regular)
|
|
306
|
+
return work(`suite file ${landing} is absent from the worktree or is not a regular file; the test command never started`);
|
|
307
|
+
if (!isVitestTestCommand(testCmd, worktree)) {
|
|
308
|
+
return countedTestRun(label, title, await sh(fileFiltered(npmForwarded(testCmd), landing, title), worktree)) ?? passed;
|
|
309
|
+
}
|
|
310
|
+
// V5: the test gate's own parser, through its one pure export, throws on a compound or non-direct script body,
|
|
311
|
+
// where an appended --reporter would reach some other command. Only its refusal is read: its separator reads the
|
|
312
|
+
// raw text, so a quoted `"--"` or an assignment's ` -- ` value would misplace the file, filter and reporter.
|
|
313
|
+
try {
|
|
314
|
+
singleForkRetryCommand(testCmd, worktree, [], []);
|
|
315
|
+
}
|
|
316
|
+
catch (error) {
|
|
317
|
+
return unverified(`the per-file reporter cannot be forwarded to this test command (${error instanceof Error ? error.message : String(error)})`);
|
|
318
|
+
}
|
|
319
|
+
const cmd = fileFiltered(npmForwarded(testCmd), landing, title);
|
|
320
|
+
const dir = mkdtempSync(join(tmpdir(), "tickmarkr-oracle-"));
|
|
321
|
+
try {
|
|
322
|
+
const nonce = randomBytes(16).toString("hex");
|
|
323
|
+
const reporterPath = join(dir, `test-reporter-${nonce}.mjs`);
|
|
324
|
+
const reportPath = join(dir, `test-report-${nonce}.json`);
|
|
325
|
+
writeFileSync(reporterPath, TEST_REPORTER_SOURCE);
|
|
326
|
+
const r = await shell(`${cmd} --reporter=${shq(reporterPath)} --outputFile=${shq(reportPath)}`, worktree, DEFAULT_SHELL_TIMEOUT_MS, false, { env: { ...manifestChildEnv(worktree), TICKMARKR_TEST_REPORT: reportPath, TICKMARKR_TEST_NONCE: nonce } });
|
|
327
|
+
const raw = readTestReport(reportPath);
|
|
328
|
+
const report = raw?.nonce === nonce ? raw : undefined;
|
|
329
|
+
if (r.code !== 0) {
|
|
330
|
+
// the reporter replaces the runner's own output: its failure identities are the evidence of a red.
|
|
331
|
+
const named = report ? [...Object.values(report.completed).flatMap((c) => c.failures ?? []), ...(report.certificate?.diagnostics ?? [])] : [];
|
|
332
|
+
return oracleExecutionFailure(label, r.code, r.stdout, [r.stderr, ...named].filter(Boolean).join("\n"));
|
|
333
|
+
}
|
|
334
|
+
if (!raw)
|
|
335
|
+
return unverified("the runner exited 0 but wrote no per-file report");
|
|
336
|
+
if (!report)
|
|
337
|
+
return unverified(`the per-file report carries nonce "${raw.nonce}", not this invocation's`);
|
|
338
|
+
if (report.certificate?.exitCode !== 0)
|
|
339
|
+
return unverified("the per-file report never reached its run-end certificate with exit 0");
|
|
340
|
+
const key = toManifestPath(within, worktree);
|
|
341
|
+
// a module completed more than once (one path in several projects) keeps only its LAST counters, so its record
|
|
342
|
+
// cannot show that no earlier completion ran a body: it counts as run.
|
|
343
|
+
const repeated = new Set(report.duplicateCompletions ?? []);
|
|
344
|
+
const elsewhere = [...new Set([...Object.keys(report.started), ...Object.keys(report.completed)])].filter((file) => {
|
|
345
|
+
const t = report.completed[file]?.tests;
|
|
346
|
+
return file !== key && (repeated.has(file) || !t || t.passed + t.failed > 0);
|
|
347
|
+
}).sort();
|
|
348
|
+
if (elsewhere.length)
|
|
349
|
+
return work(`the runner's per-file report shows a test body run outside ${landing}, in ${elsewhere.join(", ")} — only ${landing} can satisfy this criterion`);
|
|
350
|
+
if (!((report.completed[key]?.tests?.passed ?? 0) > 0))
|
|
351
|
+
return work(`the runner's per-file report shows no passing test of that title in ${landing}`);
|
|
352
|
+
return passed;
|
|
353
|
+
}
|
|
354
|
+
finally {
|
|
355
|
+
rmSync(dir, { recursive: true, force: true });
|
|
356
|
+
}
|
|
357
|
+
}
|
|
241
358
|
// v1.70 / OBS-129: the new-file line numbers each changed HUNK spans, per path — parsed from the
|
|
242
359
|
// unified-diff so a citation is validated against real changed regions, not a substring of the whole
|
|
243
360
|
// diff text (which also matches the diff's own +++/@@ headers). A hunk's span is every new-file line it
|
|
@@ -355,17 +472,18 @@ judge, via, opts = {}) {
|
|
|
355
472
|
return { gate: "acceptance", pass: false,
|
|
356
473
|
details: `oracle failed: test "${a.test}" — no test command configured to run it (failing closed)` };
|
|
357
474
|
}
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
475
|
+
// queue row 179: a declared suite file is the only file that can satisfy its criterion.
|
|
476
|
+
if (a.landing !== undefined) {
|
|
477
|
+
const landed = await landedTestOracle(opts.testCmd, a.test, a.landing, worktree);
|
|
478
|
+
if (!landed.pass)
|
|
479
|
+
return landed;
|
|
480
|
+
passedDet.push(landed.details);
|
|
481
|
+
continue;
|
|
362
482
|
}
|
|
363
483
|
// OBS-55: exit 0 alone is vacuous when the name filter matched zero tests — fail closed.
|
|
364
|
-
const
|
|
365
|
-
if (
|
|
366
|
-
return
|
|
367
|
-
details: `oracle failed: test "${a.test}" — name filter matched zero tests (filter: ${a.test})${tail(out)}` };
|
|
368
|
-
}
|
|
484
|
+
const red = countedTestRun(`test "${a.test}"`, a.test, await sh(testFiltered(opts.testCmd, a.test), worktree));
|
|
485
|
+
if (red)
|
|
486
|
+
return red;
|
|
369
487
|
passedDet.push(`✓ test: ${a.test} (exit 0)`);
|
|
370
488
|
}
|
|
371
489
|
}
|
package/dist/gates/baseline.d.ts
CHANGED
|
@@ -60,7 +60,7 @@ export declare function beginGateEvidence(cwd: string, gate: string, command: st
|
|
|
60
60
|
subjectCommit: string | null;
|
|
61
61
|
};
|
|
62
62
|
termination: {
|
|
63
|
-
kind: "unknown" | "
|
|
63
|
+
kind: "unknown" | "signal" | "timeout" | "not-started" | "exit";
|
|
64
64
|
exitCode: number | null;
|
|
65
65
|
signal: string | null;
|
|
66
66
|
timedOut: boolean | null;
|
package/dist/gates/llm.js
CHANGED
|
@@ -494,15 +494,20 @@ async function runViaDriverDetailed(adapter, model, prompt, cwd, via, timeoutMs
|
|
|
494
494
|
// OBS-1177: the beat is armed for an INTERACTIVE (pane) driver only. On the subprocess driver
|
|
495
495
|
// a headless `claude -p` buffers every byte until it exits, so 0 seat bytes at 30 s is not a
|
|
496
496
|
// dead launch; that seat keeps its full ceiling, which still bounds it (fail-closed by timeout).
|
|
497
|
-
|
|
497
|
+
// Queue row 184: an adapter whose pane review command buffers its whole output until exit
|
|
498
|
+
// (claude-code's `claude -p`) has written nothing of its own until its verdict, working or
|
|
499
|
+
// dead, so the beat cannot tell the two apart and does not arm for it; its ceiling bounds it.
|
|
500
|
+
if (reviewing && via.driver.interactive && adapter.paneReviewBuffersOutput !== true && !firstLivenessObserved && now - startedAt >= REVIEW_FIRST_LIVENESS_MS) {
|
|
498
501
|
firstLivenessObserved = true;
|
|
499
502
|
// RS-2: the beat reads seat-authored bytes ALONE. CPU evidence never holds a preamble-only
|
|
500
503
|
// capture open to the ceiling; a seat that has not written one byte of its own is re-routed.
|
|
501
|
-
// OBS-944
|
|
502
|
-
//
|
|
503
|
-
// seat
|
|
504
|
-
//
|
|
505
|
-
//
|
|
504
|
+
// OBS-944: the claude-code seat's 901 s capture was byte-identical across two legs and ended
|
|
505
|
+
// at the pane-identity line — that seat never started. Until 2.8.3 the orca checkout frame
|
|
506
|
+
// counted as seat bytes and kept every claude pane seat past this beat; once it stopped
|
|
507
|
+
// counting (row 135(a)) the beat killed working claude reviewers too, hence the declared
|
|
508
|
+
// exemption above (row 184), which restores that pre-2.8.3 bound for buffering adapters only.
|
|
509
|
+
// RULING-229-06 puts the beat on the PANE path only; a headless `-p` runner
|
|
510
|
+
// (runHeadlessDetailed, no pane, no beat) buffers every byte until completion and keeps its full ceiling.
|
|
506
511
|
if (seatAuthoredBytes === 0) {
|
|
507
512
|
launchNeverStarted = true;
|
|
508
513
|
forceClose = true;
|