tickmarkr 2.8.4 → 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 +3 -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 +11 -9
- 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 +1 -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/baseline.d.ts +1 -1
- 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/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/run/git.js
CHANGED
|
@@ -3,13 +3,14 @@ import { executionSignal, withoutExecutionBudget } from "./execution-budget.js";
|
|
|
3
3
|
import { commandLeaseEnvironment, repositoryLeaseEnvironment, registerRepositoryChild, withCommandLease } from "./lease.js";
|
|
4
4
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
5
5
|
import { execFile, execFileSync, spawn } from "node:child_process";
|
|
6
|
-
import { existsSync, lstatSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, readlinkSync, realpathSync, rmSync, statSync, symlinkSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { existsSync, lstatSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, readlinkSync, realpathSync, rmdirSync, rmSync, statSync, symlinkSync, writeFileSync } from "node:fs";
|
|
7
7
|
import { availableParallelism, homedir, tmpdir } from "node:os";
|
|
8
8
|
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
9
9
|
import { StringDecoder } from "node:string_decoder";
|
|
10
10
|
import { shq } from "../adapters/types.js";
|
|
11
11
|
import { tickmarkrDir } from "../graph/graph.js";
|
|
12
12
|
import { ROUTING_ENV_SEAMS } from "../route/router.js";
|
|
13
|
+
import { HOST_SESSION_VARS } from "../drivers/host-env.js";
|
|
13
14
|
import { titleEnvironment } from "./title-environment.js";
|
|
14
15
|
import { assertGitTrust, GitTrustRefusal, ownGitPin, protectedGitEnv, trustedCommonDir } from "./git-trust.js";
|
|
15
16
|
export { ROUTING_ENV_SEAMS };
|
|
@@ -330,6 +331,9 @@ function executeShell(cmd, cwd, timeoutMs, login, options, observation, ownGit)
|
|
|
330
331
|
const env = ownGit ? protectedGitEnv(payloadEnv) : payloadEnv;
|
|
331
332
|
for (const k of ROUTING_ENV_SEAMS)
|
|
332
333
|
delete env[k];
|
|
334
|
+
// Queue row 193 (D-2102 add.1): no gate or agent child inherits the launching agent session's host variables.
|
|
335
|
+
for (const k of HOST_SESSION_VARS)
|
|
336
|
+
delete env[k];
|
|
333
337
|
// A run freezes the operator override and cores at startup; admission can lower the round cap.
|
|
334
338
|
env[FORK_CAP_ENV] = verificationBudget.getStore()
|
|
335
339
|
? String(resolvedCapacity().forkCap)
|
|
@@ -1089,19 +1093,14 @@ export async function probeRefsWritable(cwd = process.cwd()) {
|
|
|
1089
1093
|
const error = (delRes.stderr || delRes.stdout).trim() || `git update-ref -d failed (${delRes.code})`;
|
|
1090
1094
|
return { ok: false, path: refsDir, refsDir, error };
|
|
1091
1095
|
}
|
|
1092
|
-
//
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
const tmDir = join(refsDir, "tickmarkr");
|
|
1099
|
-
if (existsSync(tmDir) && readdirSync(tmDir).length === 0) {
|
|
1100
|
-
rmSync(tmDir, { recursive: true, force: true });
|
|
1096
|
+
// Queue row 194 (D-2102): remove only EMPTY directories git left behind, non-recursively — rmdir(2)
|
|
1097
|
+
// refuses (ENOTEMPTY) a ref another process writes under refs/tickmarkr at this moment, where an
|
|
1098
|
+
// emptiness check followed by a recursive remove deleted it. Best-effort: a refusal leaves the tree.
|
|
1099
|
+
for (const dir of [join(refsDir, "tickmarkr", "preflight"), join(refsDir, "tickmarkr")]) {
|
|
1100
|
+
try {
|
|
1101
|
+
rmdirSync(dir);
|
|
1101
1102
|
}
|
|
1102
|
-
|
|
1103
|
-
catch {
|
|
1104
|
-
// best-effort cleanup of empty dirs
|
|
1103
|
+
catch { /* absent, or no longer empty */ }
|
|
1105
1104
|
}
|
|
1106
1105
|
return { ok: true, path: refsDir, refsDir };
|
|
1107
1106
|
}
|
package/dist/run/journal.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
2
|
import { execFileSync } from "node:child_process";
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
|
-
import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync } from "node:fs";
|
|
4
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, rmdirSync } from "node:fs";
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
import { z } from "zod";
|
|
7
7
|
import { channelKey, shq, TokenUsageSchema } from "../adapters/types.js";
|
|
@@ -11,7 +11,7 @@ import { channelRouteIdentity, modelProvider } from "../route/preference.js";
|
|
|
11
11
|
import { buildProfile, classify } from "../route/profile.js";
|
|
12
12
|
import { DecisionEventSchema, trackJournalRows, } from "./protocol.js";
|
|
13
13
|
import { PRESERVE_COMMIT_SUBJECT, PRESERVE_PRODUCER_TRAILER } from "./git.js";
|
|
14
|
-
import { assertGitTrust, protectedGitEnv } from "./git-trust.js";
|
|
14
|
+
import { assertGitTrust, protectedGitEnv, trustedCommonDir } from "./git-trust.js";
|
|
15
15
|
import { normalizeGateOutcome } from "./outcome.js";
|
|
16
16
|
import { redactSecrets } from "./redact.js";
|
|
17
17
|
export function phaseForGate(gate) {
|
|
@@ -1651,30 +1651,59 @@ export function engagementComparable(events, loadedHash) {
|
|
|
1651
1651
|
return recorded === loadedHash ? { comparable: true, recorded } : { comparable: false, reason: "mismatch", recorded };
|
|
1652
1652
|
}
|
|
1653
1653
|
const RUN_SEQUENCE_WIDTH = 16;
|
|
1654
|
+
const TICKET = /^\d{16}$/;
|
|
1655
|
+
const ticketsIn = (dir) => {
|
|
1656
|
+
try {
|
|
1657
|
+
return readdirSync(dir).filter((name) => TICKET.test(name));
|
|
1658
|
+
}
|
|
1659
|
+
catch (error) {
|
|
1660
|
+
if (error.code === "ENOENT")
|
|
1661
|
+
return [];
|
|
1662
|
+
throw error;
|
|
1663
|
+
}
|
|
1664
|
+
};
|
|
1665
|
+
const highest = (tickets) => tickets.reduce((max, name) => (BigInt(name) > max ? BigInt(name) : max), 0n);
|
|
1654
1666
|
// The daemon mints before acquiring graph.lock, and each CLI invocation has fresh module state. Claim
|
|
1655
|
-
// a
|
|
1656
|
-
//
|
|
1667
|
+
// a ticket with mkdir's atomic EEXIST boundary: a losing process advances and retries. Queue row 194
|
|
1668
|
+
// (D-2102): the sequence lives at the trusted common git dir, shared by the main checkout and every
|
|
1669
|
+
// linked worktree (a trust refusal refuses — never a per-checkout fallback), and its first ticket sits
|
|
1670
|
+
// above this checkout's own pre-change sequence. Pruning keeps it bounded, and the durable high-water
|
|
1671
|
+
// mark is the highest ticket itself: a prune removes a ticket only after seeing a higher one, so the
|
|
1672
|
+
// highest ever claimed survives. A stale reader can recreate a pruned ticket, but then sees that higher
|
|
1673
|
+
// one and claims again above it — only a ticket that is the highest after its own mkdir is issued.
|
|
1657
1674
|
function claimRunSequence(repoRoot = process.cwd()) {
|
|
1658
|
-
const dir = join(
|
|
1675
|
+
const dir = join(trustedCommonDir(repoRoot), "tickmarkr-run-id-sequence");
|
|
1659
1676
|
mkdirSync(dir, { recursive: true });
|
|
1660
|
-
const
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
return value > max ? value : max;
|
|
1664
|
-
}, 0n) + 1n;
|
|
1677
|
+
const own = highest(ticketsIn(join(repoRoot, stateDirName(repoRoot), "run-id-sequence")));
|
|
1678
|
+
const shared = highest(ticketsIn(dir));
|
|
1679
|
+
let candidate = (own > shared ? own : shared) + 1n;
|
|
1665
1680
|
while (true) {
|
|
1666
1681
|
const suffix = candidate.toString().padStart(RUN_SEQUENCE_WIDTH, "0");
|
|
1667
1682
|
if (suffix.length > RUN_SEQUENCE_WIDTH)
|
|
1668
1683
|
throw new Error("run id sequence exhausted");
|
|
1669
1684
|
try {
|
|
1670
1685
|
mkdirSync(join(dir, suffix));
|
|
1671
|
-
return suffix;
|
|
1672
1686
|
}
|
|
1673
1687
|
catch (error) {
|
|
1674
1688
|
if (error.code !== "EEXIST")
|
|
1675
1689
|
throw error;
|
|
1676
1690
|
candidate += 1n;
|
|
1691
|
+
continue;
|
|
1692
|
+
}
|
|
1693
|
+
const tickets = ticketsIn(dir);
|
|
1694
|
+
const top = highest(tickets);
|
|
1695
|
+
if (top > candidate) {
|
|
1696
|
+
candidate = top + 1n;
|
|
1697
|
+
continue;
|
|
1698
|
+
}
|
|
1699
|
+
for (const name of tickets) {
|
|
1700
|
+
if (BigInt(name) < candidate)
|
|
1701
|
+
try {
|
|
1702
|
+
rmdirSync(join(dir, name));
|
|
1703
|
+
}
|
|
1704
|
+
catch { /* a concurrent prune took it */ }
|
|
1677
1705
|
}
|
|
1706
|
+
return suffix;
|
|
1678
1707
|
}
|
|
1679
1708
|
}
|
|
1680
1709
|
export function newRunId(now = new Date()) {
|
package/dist/run/lock.d.ts
CHANGED
|
@@ -3,6 +3,8 @@ export declare const STALE_MS = 60000;
|
|
|
3
3
|
export interface Inspection {
|
|
4
4
|
pid?: number;
|
|
5
5
|
runId?: string;
|
|
6
|
+
startedAt?: number;
|
|
7
|
+
checkout?: string;
|
|
6
8
|
garbage: boolean;
|
|
7
9
|
dead: boolean;
|
|
8
10
|
expired: boolean;
|
|
@@ -15,6 +17,9 @@ export type RunLineEvent = {
|
|
|
15
17
|
};
|
|
16
18
|
export declare function shouldRefuse(i: Pick<Inspection, "garbage" | "dead">): boolean;
|
|
17
19
|
export declare function isPidLive(pid: number): boolean;
|
|
20
|
+
/** The repository run claim for the checkout enclosing `repoRoot`; undefined when no checkout encloses it
|
|
21
|
+
* (graph.lock alone guards there, as before). Any other git trust refusal propagates: fail closed. */
|
|
22
|
+
export declare function repositoryClaimPath(repoRoot: string): string | undefined;
|
|
18
23
|
export declare function acquireRunLock(repoRoot: string, runId: string): {
|
|
19
24
|
reclaimed?: {
|
|
20
25
|
pid: number;
|