tickmarkr 2.7.8 → 2.8.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/dist/adapters/codex.d.ts +1 -1
- package/dist/adapters/codex.js +18 -6
- package/dist/adapters/fake.js +42 -4
- package/dist/cli/commands/doctor.js +11 -2
- package/dist/cli/commands/fleet.js +206 -18
- package/dist/cli/commands/verify.js +9 -1
- package/dist/gates/acceptance.d.ts +1 -1
- package/dist/gates/acceptance.js +75 -68
- package/dist/gates/baseline.d.ts +42 -9
- package/dist/gates/baseline.js +261 -47
- package/dist/gates/operation-facts.d.ts +47 -0
- package/dist/gates/operation-facts.js +84 -0
- package/dist/gates/run-gates.js +20 -11
- package/dist/gates/test-manifest.js +2 -2
- package/dist/run/git.d.ts +5 -1
- package/dist/run/git.js +7 -3
- package/dist/run/journal.d.ts +1 -1
- package/dist/run/launchservices-check.d.ts +23 -1
- package/dist/run/launchservices-check.js +88 -3
- package/dist/run/merge.js +26 -12
- package/package.json +1 -1
package/dist/adapters/codex.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ export declare function readCodexModelsCache(path?: string): {
|
|
|
5
5
|
retirements?: Record<string, ModelRetirement | null>;
|
|
6
6
|
hidden?: string[];
|
|
7
7
|
};
|
|
8
|
-
export declare const CODEX_GIT_GRANT = "-c \"sandbox_workspace_write.writable_roots=[$(tkr_common=$(git rev-parse --path-format=absolute --git-common-dir)
|
|
8
|
+
export declare const CODEX_GIT_GRANT = "-c \"sandbox_workspace_write.writable_roots=[$(tkr_common=$(git rev-parse --path-format=absolute --git-common-dir && echo /) && tkr_gitdir=$(git rev-parse --absolute-git-dir && echo /) && tkr_common=${tkr_common%?/} && tkr_gitdir=${tkr_gitdir%?/} && { tkr_head=$(git symbolic-ref -q HEAD); [ $? -le 1 ]; } && case $tkr_head in (refs/heads/*) tkr_leaf=${tkr_head%/*};; (*) tkr_leaf=;; esac && [ \"$(printf %s \"$tkr_common$tkr_gitdir$tkr_leaf\" | LC_ALL=C tr -d '\\0-\\37\\177\"\\\\')\" = \"$tkr_common$tkr_gitdir$tkr_leaf\" ] && { [ \"$tkr_gitdir\" = \"$tkr_common\" ] || printf '\"%s\",' \"$tkr_gitdir\"; } && printf '\"%s/objects\"' \"$tkr_common\" && { [ -z \"$tkr_leaf\" ] || printf ',\"%s/%s\",\"%s/logs/%s\"' \"$tkr_common\" \"$tkr_leaf\" \"$tkr_common\" \"$tkr_leaf\"; })]\"";
|
|
9
9
|
export declare const CODEX_TRUST_DIALOG: TrustDialog;
|
|
10
10
|
export declare const CODEX_INPUT_BOX: import("./types.js").InputBox;
|
|
11
11
|
export declare function seedCodexTrust(repoRoot: string, configPath?: string): TrustVerdict;
|
package/dist/adapters/codex.js
CHANGED
|
@@ -106,13 +106,25 @@ function codexRetirement(upgrade) {
|
|
|
106
106
|
// worktree cwd, for Codex WORKERS only: v2.6.7 T4 (D-1046) — invoke() (the headless worker form, including
|
|
107
107
|
// the interactive size fallback) and interactiveCommand carry it; headlessCommand, which judge, review,
|
|
108
108
|
// consult and the model probe launch, carries no writable_roots at all (those seats never commit).
|
|
109
|
-
// v2.6.6 T9 (K, D-912): the grant is
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
//
|
|
109
|
+
// v2.6.6 T9 (K, D-912) → v2.8.0 T1 (rows 17/18, D-910): the grant is a LEAF of what a linked-worktree branch
|
|
110
|
+
// commit writes, never the shared hooks/config (the shipped [common] and D-910's [common, gitdir] let a seat
|
|
111
|
+
// write <common>/hooks and <common>/config, which unsandboxed git later runs) and never whole common/refs and
|
|
112
|
+
// common/logs (which let a seat move main, any branch or tag, or the stash). The closed root table, in order:
|
|
113
|
+
// - the worktree's own gitdir, only when it differs from the common dir (an ordinary checkout gets none);
|
|
114
|
+
// - <common>/objects;
|
|
115
|
+
// - only when HEAD is a symbolic ref refs/heads/<B>: <common>/refs/heads/<D> and <common>/logs/refs/heads/<D>,
|
|
116
|
+
// <D> being <B> without its last slash-separated segment (refs/heads itself when <B> holds no slash);
|
|
117
|
+
// a detached HEAD gets no refs or reflog root.
|
|
118
|
+
// A directory outside any repository, any failed git read, or any root holding a character of the closed
|
|
119
|
+
// forbidden set (U+0000 to U+001F, U+007F DEL, the double quote, the backslash) yields the EMPTY array `[]`:
|
|
120
|
+
// no git write root, so a commit fails closed — never a partial array, an escaped string or codex's
|
|
121
|
+
// literal-string fallback. Every emitted root is therefore a TOML basic string that parses back to the exact
|
|
122
|
+
// path (git's own trailing newline is stripped through a sentinel, never a path's). No common-root fallback;
|
|
123
|
+
// packed-refs is not granted (packed-ref ops fail closed); stash, tags, notes and branches outside the leaf
|
|
124
|
+
// are denied. Declared: sibling refs in the same leaf (other task branches, the run's integration branch
|
|
125
|
+
// under the default prefix) stay writable until 2.8.1's integration identity.
|
|
114
126
|
// doctor's probe (codex-commit-check.ts) executes these exact bytes; it never rebuilds the grant.
|
|
115
|
-
export const CODEX_GIT_GRANT = `-c "sandbox_workspace_write.writable_roots=[$(tkr_common=$(git rev-parse --path-format=absolute --git-common-dir)
|
|
127
|
+
export const CODEX_GIT_GRANT = `-c "sandbox_workspace_write.writable_roots=[$(tkr_common=$(git rev-parse --path-format=absolute --git-common-dir && echo /) && tkr_gitdir=$(git rev-parse --absolute-git-dir && echo /) && tkr_common=\${tkr_common%?/} && tkr_gitdir=\${tkr_gitdir%?/} && { tkr_head=$(git symbolic-ref -q HEAD); [ $? -le 1 ]; } && case $tkr_head in (refs/heads/*) tkr_leaf=\${tkr_head%/*};; (*) tkr_leaf=;; esac && [ "$(printf %s "$tkr_common$tkr_gitdir$tkr_leaf" | LC_ALL=C tr -d '\\0-\\37\\177"\\\\')" = "$tkr_common$tkr_gitdir$tkr_leaf" ] && { [ "$tkr_gitdir" = "$tkr_common" ] || printf '"%s",' "$tkr_gitdir"; } && printf '"%s/objects"' "$tkr_common" && { [ -z "$tkr_leaf" ] || printf ',"%s/%s","%s/logs/%s"' "$tkr_common" "$tkr_leaf" "$tkr_common" "$tkr_leaf"; })]"`;
|
|
116
128
|
// OBS-125: codex 0.144.x gates enabled hooks behind PER-PROJECT-PATH hook trust — a NEW gate distinct
|
|
117
129
|
// from the directory trust seedCodexTrust seeds. Every worker runs in a fresh ephemeral worktree, an
|
|
118
130
|
// untrusted project, so codex re-prompts "Hooks need review" and the interactive worker stalls forever
|
package/dist/adapters/fake.js
CHANGED
|
@@ -2,6 +2,40 @@ import { readFileSync, writeFileSync } from "node:fs";
|
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { parseWorkerResult } from "./prompt.js";
|
|
4
4
|
import { OPENCODE_BUSY_FRAME_MARKERS, shq, TokenUsageSchema } from "./types.js";
|
|
5
|
+
/**
|
|
6
|
+
* S23: the first citation of the gate's nonce-framed index — from the LAST line exactly `CITABLE_LINES_BEGIN <nonce>`
|
|
7
|
+
* to the next line exactly `CITABLE_LINES_END <nonce>`. Every entry must be a JSON object with a string path and a
|
|
8
|
+
* non-empty array of non-negative safe-integer lines before any is used; otherwise (no block, no END, an empty or
|
|
9
|
+
* invalid entry anywhere) there is no citation. Never throws.
|
|
10
|
+
*/
|
|
11
|
+
function firstCitation(prompt, nonce) {
|
|
12
|
+
if (!nonce)
|
|
13
|
+
return undefined;
|
|
14
|
+
const lines = prompt.split("\n");
|
|
15
|
+
const begin = lines.lastIndexOf(`CITABLE_LINES_BEGIN ${nonce}`);
|
|
16
|
+
const end = begin < 0 ? -1 : lines.indexOf(`CITABLE_LINES_END ${nonce}`, begin + 1);
|
|
17
|
+
if (end < 0)
|
|
18
|
+
return undefined;
|
|
19
|
+
const entries = [];
|
|
20
|
+
for (const line of lines.slice(begin + 1, end)) {
|
|
21
|
+
let entry;
|
|
22
|
+
try {
|
|
23
|
+
entry = JSON.parse(line);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
29
|
+
return undefined;
|
|
30
|
+
const { path, lines: cited } = entry;
|
|
31
|
+
if (typeof path !== "string" || !Array.isArray(cited) || cited.length === 0
|
|
32
|
+
|| !cited.every((n) => Number.isSafeInteger(n) && n >= 0))
|
|
33
|
+
return undefined;
|
|
34
|
+
entries.push({ path, lines: cited });
|
|
35
|
+
}
|
|
36
|
+
const first = entries[0];
|
|
37
|
+
return first ? { path: first.path, line: first.lines[0] } : undefined;
|
|
38
|
+
}
|
|
5
39
|
// ponytail: deterministic scripted adapter — the whole integration suite runs on it, zero tokens.
|
|
6
40
|
export class FakeAdapter {
|
|
7
41
|
scriptPath;
|
|
@@ -77,13 +111,15 @@ export class FakeAdapter {
|
|
|
77
111
|
val = { ...verdict, criteria: items.map((criterion) => ({ criterion, met: true, reason: "scripted fake pass" })) };
|
|
78
112
|
}
|
|
79
113
|
}
|
|
114
|
+
// S23: a scripted row without evidence cites the first line of the first entry of the gate's own citable
|
|
115
|
+
// index — never a string. An invalid or unterminated index yields no evidence (the gate then fails closed).
|
|
80
116
|
if (key === "judge" && isJudge && val && typeof val === "object" && !Array.isArray(val)) {
|
|
81
117
|
const verdict = val;
|
|
82
|
-
const
|
|
83
|
-
if (
|
|
118
|
+
const evidence = firstCitation(prompt, nonce);
|
|
119
|
+
if (evidence && Array.isArray(verdict.criteria)) {
|
|
84
120
|
val = {
|
|
85
121
|
...verdict,
|
|
86
|
-
criteria: verdict.criteria.map((row) => row && typeof row === "object" && !("evidence" in row) ? { ...row, evidence
|
|
122
|
+
criteria: verdict.criteria.map((row) => row && typeof row === "object" && !("evidence" in row) ? { ...row, evidence } : row),
|
|
87
123
|
};
|
|
88
124
|
}
|
|
89
125
|
}
|
|
@@ -152,10 +188,12 @@ export class FakeAdapter {
|
|
|
152
188
|
}
|
|
153
189
|
// the run nonce lives in the prompt writePrompt handed us; echo a matching trailer so
|
|
154
190
|
// parseWorkerResult(output, nonce) succeeds exactly as before the nonce existed
|
|
191
|
+
// S23: the verdict nonce is the LAST whole `VERDICT_NONCE: <hex>` line — the gate writes its own after the goal,
|
|
192
|
+
// criteria and diff, so a nonce line quoted in any of them never binds.
|
|
155
193
|
nonceFor(promptFile) {
|
|
156
194
|
try {
|
|
157
195
|
const prompt = readFileSync(promptFile, "utf8");
|
|
158
|
-
return
|
|
196
|
+
return prompt.split("\n").map((line) => /^VERDICT_NONCE: ([0-9a-f]+)$/.exec(line)?.[1]).filter(Boolean).at(-1)
|
|
159
197
|
?? /TICKMARKR_RESULT_([0-9a-z]+)/.exec(prompt)?.[1]
|
|
160
198
|
?? "";
|
|
161
199
|
}
|
|
@@ -22,7 +22,7 @@ import { sh } from "../../run/git.js";
|
|
|
22
22
|
import { auditNamedTestOracles, listVitestTests } from "../../gates/acceptance.js";
|
|
23
23
|
import { readReviewNoVerdictHistory, reviewNoVerdictRows } from "../../run/journal.js";
|
|
24
24
|
import { checkModelDiscovery } from "../../adapters/model-discovery-check.js";
|
|
25
|
-
import { recordLaunchServices } from "../../run/launchservices-check.js";
|
|
25
|
+
import { LAUNCHSERVICES_PORT_ADVISORY, probeLaunchServicesPorts, recordLaunchServices } from "../../run/launchservices-check.js";
|
|
26
26
|
/** Where a newer `table_<date>.csv` is discovered — the deployed site builds filenames by
|
|
27
27
|
* concatenation and publishes no index, so the release listing is the only enumerable surface. */
|
|
28
28
|
export const LIVEBENCH_RELEASES_URL = "https://api.github.com/repos/LiveBench/livebench.github.io/contents/public";
|
|
@@ -670,7 +670,8 @@ export async function doctor(_argv, cwd = process.cwd(), adapters = allAdapters(
|
|
|
670
670
|
const discovery = await checkModelDiscovery({ cwd, cfg, adapters, health, catalog, resolved: resolvedIdentitySnapshot, mayRefresh: !opts.catalog && !catalogRefreshLine && catalogRefreshAllowed, fetcher: opts.catalogFetcher, now: opts.catalogNow });
|
|
671
671
|
catalog = discovery.catalog;
|
|
672
672
|
aliasDriftFindings.push(...discovery.findings);
|
|
673
|
-
const
|
|
673
|
+
const portProbe = await probeLaunchServicesPorts();
|
|
674
|
+
const launchServices = await recordLaunchServices(readDoctor(cwd), health, portProbe.observation);
|
|
674
675
|
writeDoctor(cwd, launchServices.health);
|
|
675
676
|
const rows = adapters.map((a) => {
|
|
676
677
|
const h = health[a.id];
|
|
@@ -680,6 +681,14 @@ export async function doctor(_argv, cwd = process.cwd(), adapters = allAdapters(
|
|
|
680
681
|
const healthy = h.installed && (a.id !== kimi.id || h.authed) && !(a.id === codex.id && h.codexCommit === "escape");
|
|
681
682
|
return alignedStatusRow(healthy ? "pass" : "fail", a.id, state);
|
|
682
683
|
});
|
|
684
|
+
const ports = portProbe.ports;
|
|
685
|
+
const portLine = ports.status === "non-darwin" ? "launchservicesd ports: not applicable"
|
|
686
|
+
: ports.status === "read" ? `launchservicesd pid ${ports.pid}: ${ports.count} ports`
|
|
687
|
+
: `launchservicesd pid ${ports.status === "unknown" ? ports.pid ?? "unknown" : "unknown"}: ports unknown`;
|
|
688
|
+
if (ports.status !== "skipped") {
|
|
689
|
+
rows.push(alignedStatusRow(ports.status === "read" && ports.count < LAUNCHSERVICES_PORT_ADVISORY || ports.status === "non-darwin"
|
|
690
|
+
? "pass" : "warn", "launchservicesd", portLine));
|
|
691
|
+
}
|
|
683
692
|
const reviewDemotions = recentReviewDemotions(cwd);
|
|
684
693
|
if (reviewDemotions.length) {
|
|
685
694
|
rows.push(legend("recent review-seat demotions:"));
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
2
3
|
import { homedir } from "node:os";
|
|
3
4
|
import { dirname, join, relative } from "node:path";
|
|
4
5
|
import { parseArgs } from "node:util";
|
|
@@ -92,6 +93,131 @@ function formatFleetSteering(cfg) {
|
|
|
92
93
|
// T10: what every review says about where its bytes land and when they take effect
|
|
93
94
|
const REPO_DESTINATION_NOTE = (path) => `destination: the repository overlay ${path} holds this repository's routing.allow/deny membership, so the edit lands there — the file is tracked: Fleet never stages or commits it; commit it to share the change`;
|
|
94
95
|
const FUTURE_LOADS_NOTE = "the save changes future config loads only — a tickmarkr run already in progress keeps the config it loaded";
|
|
96
|
+
// Queue 134: routing.mode, review.prefer, consult.prefer and judge each follow the overlay that holds
|
|
97
|
+
// them. A YAML merge holds one only when that merge supplies it — an unrelated sibling merge does not —
|
|
98
|
+
// and the review refuses a write through a supplying merge (D-987). A scalar alias key is the name it
|
|
99
|
+
// resolves to, at the alias's own line; a parent alias is refused because the writer cannot update
|
|
100
|
+
// through it without replacing its siblings. A merge sequence is shallow and first-source-wins.
|
|
101
|
+
// Membership stays on heldLine.
|
|
102
|
+
const CONTROL_PATH = {
|
|
103
|
+
"routing.mode": ["routing", "mode"],
|
|
104
|
+
"review.prefer": ["review", "prefer"],
|
|
105
|
+
"consult.prefer": ["consult", "prefer"],
|
|
106
|
+
judge: ["judge"],
|
|
107
|
+
};
|
|
108
|
+
const CONTROL_NAMES = new Set(Object.keys(CONTROL_PATH));
|
|
109
|
+
const { isAlias: yamlIsAlias, isMap: yamlIsMap, isNode: yamlIsNode, isScalar: yamlIsScalar, isSeq: yamlIsSeq, parseDocument: yamlParseDocument, } = createRequire(import.meta.url)("yaml");
|
|
110
|
+
const controlHoldMemo = new Map();
|
|
111
|
+
const controlLineOf = (bytes, offset) => bytes.slice(0, offset).split("\n").length;
|
|
112
|
+
const resolveYaml = (doc, node) => yamlIsAlias(node) ? node.resolve(doc) : node;
|
|
113
|
+
// A plain scalar, or a scalar alias (`*modeKey` resolving to mode). The merge key `<<` is a symbol
|
|
114
|
+
// and is not a name. Callers that report a line keep the key node's own range — the alias as written,
|
|
115
|
+
// never the anchor it points at.
|
|
116
|
+
const scalarKeyName = (doc, key) => {
|
|
117
|
+
const resolved = resolveYaml(doc, key);
|
|
118
|
+
if (!yamlIsScalar(resolved) || typeof resolved.value === "symbol" || resolved.value == null)
|
|
119
|
+
return undefined;
|
|
120
|
+
return String(resolved.value);
|
|
121
|
+
};
|
|
122
|
+
const namedPair = (doc, map, name) => map.items.find((item) => scalarKeyName(doc, item.key) === name);
|
|
123
|
+
const mergePair = (map) => map.items.find((item) => yamlIsScalar(item.key) && typeof item.key.value === "symbol");
|
|
124
|
+
const mergeShapeOf = (bytes, value) => {
|
|
125
|
+
const range = yamlIsNode(value) ? value.range : null;
|
|
126
|
+
const text = range ? bytes.slice(range[0], range[1]).trim().replace(/\s+/g, " ") : "…";
|
|
127
|
+
return `<<: ${text}`;
|
|
128
|
+
};
|
|
129
|
+
const mergeSources = (doc, value) => {
|
|
130
|
+
const node = resolveYaml(doc, value);
|
|
131
|
+
return yamlIsSeq(node) ? node.items.map((item) => resolveYaml(doc, item)) : [node];
|
|
132
|
+
};
|
|
133
|
+
// YAML 1.1 merge: an explicit key wins over `<<`, and a sequence is shallow — the first source that
|
|
134
|
+
// declares a key owns that key whole. A later source's prefer or mode does not survive an earlier
|
|
135
|
+
// source that already declared the parent mapping. `seen` stops a merge that aliases itself.
|
|
136
|
+
const definesKey = (doc, node, key, seen) => {
|
|
137
|
+
const here = resolveYaml(doc, node);
|
|
138
|
+
if (!yamlIsMap(here) || seen.has(here))
|
|
139
|
+
return false;
|
|
140
|
+
if (namedPair(doc, here, key))
|
|
141
|
+
return true;
|
|
142
|
+
const merge = mergePair(here);
|
|
143
|
+
if (!merge)
|
|
144
|
+
return false;
|
|
145
|
+
const next = new Set(seen);
|
|
146
|
+
next.add(here);
|
|
147
|
+
return mergeSources(doc, merge.value).some((source) => definesKey(doc, source, key, next));
|
|
148
|
+
};
|
|
149
|
+
const controlSupplied = (doc, node, rest, seen = new Set()) => {
|
|
150
|
+
const here = resolveYaml(doc, node);
|
|
151
|
+
if (!yamlIsMap(here) || seen.has(here))
|
|
152
|
+
return false;
|
|
153
|
+
if (rest.length === 0)
|
|
154
|
+
return true;
|
|
155
|
+
const [head, ...tail] = rest;
|
|
156
|
+
if (head === undefined)
|
|
157
|
+
return false;
|
|
158
|
+
const pair = namedPair(doc, here, head);
|
|
159
|
+
if (pair) {
|
|
160
|
+
const value = resolveYaml(doc, pair.value);
|
|
161
|
+
if (yamlIsScalar(value) && value.value === null)
|
|
162
|
+
return true;
|
|
163
|
+
if (tail.length === 0)
|
|
164
|
+
return true;
|
|
165
|
+
const next = new Set(seen);
|
|
166
|
+
next.add(here);
|
|
167
|
+
return controlSupplied(doc, value, tail, next);
|
|
168
|
+
}
|
|
169
|
+
const merge = mergePair(here);
|
|
170
|
+
if (!merge)
|
|
171
|
+
return false;
|
|
172
|
+
const next = new Set(seen);
|
|
173
|
+
next.add(here);
|
|
174
|
+
for (const source of mergeSources(doc, merge.value)) {
|
|
175
|
+
if (!definesKey(doc, source, head, next))
|
|
176
|
+
continue;
|
|
177
|
+
return controlSupplied(doc, source, rest, next);
|
|
178
|
+
}
|
|
179
|
+
return false;
|
|
180
|
+
};
|
|
181
|
+
/** The repository (or user) overlay holds a control at its own key, at a null mask at it or above, or at a
|
|
182
|
+
* merge key that supplies it. An explicit key wins over a sibling merge. Parse errors hold nothing. */
|
|
183
|
+
function controlHold(bytes, path) {
|
|
184
|
+
const key = `${path.join(".")}\0${bytes}`;
|
|
185
|
+
if (controlHoldMemo.has(key))
|
|
186
|
+
return controlHoldMemo.get(key);
|
|
187
|
+
if (controlHoldMemo.size > 256)
|
|
188
|
+
controlHoldMemo.clear();
|
|
189
|
+
const doc = yamlParseDocument(bytes);
|
|
190
|
+
const hold = doc.errors.length ? undefined : walkControl(doc, bytes, doc.contents, path);
|
|
191
|
+
controlHoldMemo.set(key, hold);
|
|
192
|
+
return hold;
|
|
193
|
+
}
|
|
194
|
+
function walkControl(doc, bytes, node, rest) {
|
|
195
|
+
const here = resolveYaml(doc, node);
|
|
196
|
+
if (!yamlIsMap(here) || rest.length === 0)
|
|
197
|
+
return undefined;
|
|
198
|
+
const [head, ...tail] = rest;
|
|
199
|
+
const pair = head === undefined ? undefined : namedPair(doc, here, head);
|
|
200
|
+
if (pair) {
|
|
201
|
+
const range = yamlIsNode(pair.key) ? pair.key.range : null;
|
|
202
|
+
if (!range)
|
|
203
|
+
return undefined;
|
|
204
|
+
const line = controlLineOf(bytes, range[0]);
|
|
205
|
+
const value = resolveYaml(doc, pair.value);
|
|
206
|
+
if (yamlIsScalar(value) && value.value === null) {
|
|
207
|
+
return { line, via: yamlIsAlias(pair.key) && tail.length > 0 ? "alias-parent" : "mask" };
|
|
208
|
+
}
|
|
209
|
+
if (tail.length === 0)
|
|
210
|
+
return { line, via: "declared" };
|
|
211
|
+
const nested = walkControl(doc, bytes, value, tail);
|
|
212
|
+
return nested && yamlIsAlias(pair.key) ? { line, via: "alias-parent" } : nested;
|
|
213
|
+
}
|
|
214
|
+
const merge = mergePair(here);
|
|
215
|
+
if (!merge || !yamlIsScalar(merge.key) || !merge.key.range)
|
|
216
|
+
return undefined;
|
|
217
|
+
if (!controlSupplied(doc, here, rest))
|
|
218
|
+
return undefined;
|
|
219
|
+
return { line: controlLineOf(bytes, merge.key.range[0]), via: "merge", shape: mergeShapeOf(bytes, merge.value) };
|
|
220
|
+
}
|
|
95
221
|
const MODE_GLOSS = {
|
|
96
222
|
"partner-led": "every shape frontier · explore off",
|
|
97
223
|
"risk-based": "risk-tiered default floors",
|
|
@@ -224,8 +350,10 @@ export async function fleet(argv, cwd = process.cwd(), adapters = allAdapters(),
|
|
|
224
350
|
export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
225
351
|
const globalDir = opts.globalDir;
|
|
226
352
|
// B2: the editor's choices are the operator's MACHINE choices — a save lands in the USER overlay,
|
|
227
|
-
// which every repository on this machine inherits. T10 (D-925/D-926):
|
|
228
|
-
//
|
|
353
|
+
// which every repository on this machine inherits. T10 (D-925/D-926): a membership edit and a
|
|
354
|
+
// routing.mode, review.prefer, consult.prefer or judge edit each land in the overlay that holds
|
|
355
|
+
// them (destinationEdits below) — the repository overlay when it declares the key or masks it
|
|
356
|
+
// with null at it or above, else the user overlay.
|
|
229
357
|
const userPath = join(globalDir, "config.yaml");
|
|
230
358
|
const repoPath = repoOverlayPath(cwd);
|
|
231
359
|
const currentUserText = () => (existsSync(userPath) ? readFileSync(userPath, "utf8") : "");
|
|
@@ -552,11 +680,12 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
552
680
|
const home = homedir();
|
|
553
681
|
const shown = (path) => (path.startsWith(`${home}/`) ? `~${path.slice(home.length)}` : path);
|
|
554
682
|
const holderOf = (dotted) => {
|
|
555
|
-
const
|
|
556
|
-
const
|
|
683
|
+
const control = dotted in CONTROL_PATH ? CONTROL_PATH[dotted] : undefined;
|
|
684
|
+
const lineIn = (text) => control ? controlHold(text, control)?.line : heldLine(text, dotted.split("."));
|
|
685
|
+
const repoLine = lineIn(currentRepoOverlayText(cwd));
|
|
557
686
|
if (repoLine !== undefined)
|
|
558
687
|
return `${relative(cwd, repoPath)}:${repoLine}`;
|
|
559
|
-
const userLine =
|
|
688
|
+
const userLine = lineIn(currentUserText());
|
|
560
689
|
return userLine === undefined ? undefined : `${shown(userPath)}:${userLine}`;
|
|
561
690
|
};
|
|
562
691
|
// the allow + flat-deny family is ONE coupled write (the allow form is
|
|
@@ -615,7 +744,14 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
615
744
|
}
|
|
616
745
|
if (JSON.stringify(initial.efforts ?? {}) !== JSON.stringify(staged.efforts ?? {}))
|
|
617
746
|
edits.push({ name: "model efforts", destination: "user" });
|
|
618
|
-
|
|
747
|
+
// a control lands where it is held. A merge that supplies it is still the repository (the review
|
|
748
|
+
// then refuses the write); review.prefer and consult.prefer are two edits, never one
|
|
749
|
+
const controlDestination = (name) => {
|
|
750
|
+
if (!(name in CONTROL_PATH))
|
|
751
|
+
return "user";
|
|
752
|
+
return controlHold(currentRepoOverlayText(cwd), CONTROL_PATH[name]) !== undefined ? "repo" : "user";
|
|
753
|
+
};
|
|
754
|
+
return [...edits, ...controls.map((name) => ({ name, destination: controlDestination(name) }))];
|
|
619
755
|
};
|
|
620
756
|
// ONE review confirms ONE destination; a batch needing both is refused before any write (no
|
|
621
757
|
// multi-file transaction) and names what to save in separate sessions
|
|
@@ -800,8 +936,8 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
800
936
|
parts.push(`${unroutable} unroutable`);
|
|
801
937
|
return ` mix: ${mix} — ${parts.join(" · ")}`;
|
|
802
938
|
};
|
|
803
|
-
//
|
|
804
|
-
//
|
|
939
|
+
// Floors come from the candidate the chosen destination would load. A routing.mode the repository
|
|
940
|
+
// holds is previewed there, so those floors move; an unheld mode is previewed in the user overlay.
|
|
805
941
|
const floorPreview = (mode, map, deny, stage) => {
|
|
806
942
|
if (mode === rm.mode.mode)
|
|
807
943
|
return [];
|
|
@@ -818,9 +954,9 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
818
954
|
: [" (no floor changes)"]),
|
|
819
955
|
];
|
|
820
956
|
};
|
|
821
|
-
//
|
|
822
|
-
//
|
|
823
|
-
//
|
|
957
|
+
// `resolution` is the candidate destination's mode. A routing.mode the repository holds is written
|
|
958
|
+
// into that preview, so a changed mode pins the floors the loader will apply.
|
|
959
|
+
// `entry` is the slot the candidate's loader applies — a staged slot a repo declaration shadows
|
|
824
960
|
// is no operator pin here; the repo's winning declaration keeps its provenance
|
|
825
961
|
const whyDeclaration = (shape, resolution, entry, mapProducedValue) => {
|
|
826
962
|
const mapChanged = JSON.stringify(mapSlot(entry)) !== JSON.stringify(mapSlot(editable.map[shape]));
|
|
@@ -1154,7 +1290,9 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
1154
1290
|
// the diff and asks for confirmation, but owns neither filesystem access nor a writer.
|
|
1155
1291
|
// B2: defaults/user/repo are re-read here, at review. T10: so is the destination.
|
|
1156
1292
|
const modeChanged = state.selectedMode !== rm.mode.mode;
|
|
1157
|
-
const
|
|
1293
|
+
const reviewChanged = JSON.stringify(state.steering.review) !== JSON.stringify(initialSteering.review);
|
|
1294
|
+
const consultChanged = JSON.stringify(state.steering.consult) !== JSON.stringify(initialSteering.consult);
|
|
1295
|
+
const steeringChanged = reviewChanged || consultChanged;
|
|
1158
1296
|
const judgeSeat = state.judgeSeat;
|
|
1159
1297
|
const judgeChanged = judgeSeat !== undefined
|
|
1160
1298
|
&& `${judgeSeat.adapter}:${judgeSeat.model}` !== initialJudge;
|
|
@@ -1162,11 +1300,14 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
1162
1300
|
pendingWrite = null;
|
|
1163
1301
|
return { kind: "empty" };
|
|
1164
1302
|
}
|
|
1165
|
-
const
|
|
1303
|
+
const controlNames = [
|
|
1166
1304
|
...(modeChanged ? ["routing.mode"] : []),
|
|
1167
|
-
...(
|
|
1305
|
+
...(reviewChanged ? ["review.prefer"] : []),
|
|
1306
|
+
...(consultChanged ? ["consult.prefer"] : []),
|
|
1168
1307
|
...(judgeChanged ? ["judge"] : []),
|
|
1169
|
-
]
|
|
1308
|
+
];
|
|
1309
|
+
const edits = destinationEdits(staged, metadata.seeds, controlNames);
|
|
1310
|
+
const chosen = chooseDestination(edits);
|
|
1170
1311
|
if (!chosen.ok) {
|
|
1171
1312
|
pendingWrite = null;
|
|
1172
1313
|
return { kind: "refused", reason: chosen.reason };
|
|
@@ -1174,6 +1315,43 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
1174
1315
|
const destination = chosen.destination;
|
|
1175
1316
|
const user = currentUserText();
|
|
1176
1317
|
const repo = currentRepoOverlayText(cwd);
|
|
1318
|
+
// A merge that supplies a control or an alias used as its parent is a holder, but the writer
|
|
1319
|
+
// cannot update through either key. Refuse before any bytes change, naming the key's own line.
|
|
1320
|
+
if (destination === "repo") {
|
|
1321
|
+
const refusals = edits.flatMap((edit) => {
|
|
1322
|
+
if (!(edit.name in CONTROL_PATH))
|
|
1323
|
+
return [];
|
|
1324
|
+
const hold = controlHold(repo, CONTROL_PATH[edit.name]);
|
|
1325
|
+
if (hold?.via === "merge") {
|
|
1326
|
+
const shape = hold.shape ?? "<<: …";
|
|
1327
|
+
return [`${edit.name} takes keys through the YAML merge key \`${shape}\` at ${holderOf(edit.name)} — Fleet does not write through merge keys; inline the merged keys there by hand, then save again`];
|
|
1328
|
+
}
|
|
1329
|
+
if (hold?.via === "alias-parent") {
|
|
1330
|
+
return [`${edit.name} takes keys through a YAML alias parent key at ${holderOf(edit.name)} — Fleet does not write through alias parent keys; replace that key with its scalar name, then save again`];
|
|
1331
|
+
}
|
|
1332
|
+
return [];
|
|
1333
|
+
});
|
|
1334
|
+
if (refusals.length) {
|
|
1335
|
+
pendingWrite = null;
|
|
1336
|
+
return { kind: "refused", reason: refusals.join("; ") };
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
// clearing a repository-held prefer writes an explicit empty sequence, so a lower user list cannot
|
|
1340
|
+
// reappear. A user-held or unheld clear stays undefined and the writer deletes the key, as before.
|
|
1341
|
+
const steeringEdited = { ...state.steering };
|
|
1342
|
+
if (destination === "repo") {
|
|
1343
|
+
for (const key of ["review", "consult"]) {
|
|
1344
|
+
if (JSON.stringify(steeringEdited[key]) === JSON.stringify(initialSteering[key]))
|
|
1345
|
+
continue;
|
|
1346
|
+
const chain = steeringEdited[key];
|
|
1347
|
+
if (chain != null && chain.length > 0)
|
|
1348
|
+
continue;
|
|
1349
|
+
const name = `${key}.prefer`;
|
|
1350
|
+
const hold = controlHold(repo, CONTROL_PATH[name]);
|
|
1351
|
+
if (hold !== undefined && hold.via !== "merge")
|
|
1352
|
+
steeringEdited[key] = [];
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1177
1355
|
const before = destination === "repo" ? repo : user;
|
|
1178
1356
|
const path = pathOf(destination);
|
|
1179
1357
|
const lower = lowerLayers(destination);
|
|
@@ -1186,7 +1364,7 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
1186
1364
|
lowerOverrides: lowerOverrides[destination],
|
|
1187
1365
|
...(modeChanged ? { mode: state.selectedMode } : {}),
|
|
1188
1366
|
...(judgeChanged && judgeSeat ? { judge: judgeSeat } : {}),
|
|
1189
|
-
steering: { initial: initialSteering, edited:
|
|
1367
|
+
steering: { initial: initialSteering, edited: steeringEdited },
|
|
1190
1368
|
});
|
|
1191
1369
|
after = renderFleetOverlayWrite(before, write); // T10 (D-987): a merge-key path refuses here
|
|
1192
1370
|
}
|
|
@@ -1211,9 +1389,19 @@ export async function assembleFleetEditor(cwd, adapters, io, opts) {
|
|
|
1211
1389
|
catch {
|
|
1212
1390
|
poolNotes = [];
|
|
1213
1391
|
}
|
|
1392
|
+
const controlEdits = edits.filter((edit) => CONTROL_NAMES.has(edit.name));
|
|
1393
|
+
const controlNote = controlEdits.length === 0
|
|
1394
|
+
? undefined
|
|
1395
|
+
: `destination: ${controlEdits.map((edit) => `${edit.name} at ${holderOf(edit.name)}`).join("; ")} — the file is tracked and Fleet never commits it`;
|
|
1214
1396
|
const notes = [
|
|
1215
|
-
// T10:
|
|
1216
|
-
|
|
1397
|
+
// T10: a membership edit keeps the membership destination note. Every repository-destined
|
|
1398
|
+
// control in the same batch is named beside it, path:line from holderOf, file tracked.
|
|
1399
|
+
...(destination === "repo"
|
|
1400
|
+
? [
|
|
1401
|
+
...(edits.some((edit) => !CONTROL_NAMES.has(edit.name)) ? [REPO_DESTINATION_NOTE(repoPath)] : []),
|
|
1402
|
+
...(controlNote ? [controlNote] : []),
|
|
1403
|
+
]
|
|
1404
|
+
: repoShadowNotes(before, after)),
|
|
1217
1405
|
FUTURE_LOADS_NOTE,
|
|
1218
1406
|
...(lower.ok ? [] : [`${lower.error} — y will be refused`]),
|
|
1219
1407
|
...poolNotes,
|
|
@@ -17,7 +17,7 @@ import { addDetachedWorktree, linkNodeModules, removeWorktree, shGit, shGitOk }
|
|
|
17
17
|
import { foldOwedChecks, integrationMapped, owedCriteria, Journal, OWED_DISCHARGE_EVENT } from "../../run/journal.js";
|
|
18
18
|
import { describeLeaseWait, withRepositoryLease } from "../../run/lease.js";
|
|
19
19
|
import { isPidLive } from "../../run/lock.js";
|
|
20
|
-
import { recordLaunchServices } from "../../run/launchservices-check.js";
|
|
20
|
+
import { LAUNCHSERVICES_PORT_STOP, probeLaunchServicesPorts, recordLaunchServices } from "../../run/launchservices-check.js";
|
|
21
21
|
/**
|
|
22
22
|
* tickmarkr verify — the gate battery as a standalone command (OPERATING-MODEL-2026-08-11 item 3).
|
|
23
23
|
*
|
|
@@ -184,6 +184,14 @@ export async function verify(argv, cwd = process.cwd(), options = {}) {
|
|
|
184
184
|
},
|
|
185
185
|
allowPositionals: false,
|
|
186
186
|
});
|
|
187
|
+
const portProbe = await probeLaunchServicesPorts({ requireTop: true });
|
|
188
|
+
const ports = portProbe.ports;
|
|
189
|
+
if (ports.status === "read" || ports.status === "unknown") {
|
|
190
|
+
console.error(`verify: ports ${ports.status === "read" ? ports.count : "unknown"}`);
|
|
191
|
+
}
|
|
192
|
+
if (ports.status === "read" && ports.count >= LAUNCHSERVICES_PORT_STOP) {
|
|
193
|
+
return { out: `verify refused: launchservicesd has ${ports.count} ports (threshold ${LAUNCHSERVICES_PORT_STOP}); log out or restart to reset launchservicesd before verifying`, code: 1 };
|
|
194
|
+
}
|
|
187
195
|
const stateRoot = await verifyStateRoot(cwd);
|
|
188
196
|
const fileRoot = (file) => existsSync(join(cwd, ".tickmarkr", file)) ? cwd : stateRoot;
|
|
189
197
|
if (resolve(stateRoot) !== resolve(cwd)) {
|