tickmarkr 1.84.0 → 1.86.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/dist/adapters/catalog-remote.d.ts +64 -0
- package/dist/adapters/catalog-remote.js +287 -0
- package/dist/adapters/catalog.d.ts +96 -0
- package/dist/adapters/catalog.js +176 -0
- package/dist/adapters/claude-code.d.ts +1 -0
- package/dist/adapters/claude-code.js +59 -1
- package/dist/adapters/fake.js +42 -4
- package/dist/adapters/model-lints.d.ts +25 -5
- package/dist/adapters/model-lints.js +184 -50
- package/dist/adapters/model-windows.d.ts +31 -0
- package/dist/adapters/model-windows.js +69 -0
- package/dist/adapters/prompt.d.ts +5 -1
- package/dist/adapters/prompt.js +13 -4
- package/dist/adapters/registry.d.ts +25 -26
- package/dist/adapters/registry.js +173 -110
- package/dist/adapters/types.d.ts +3 -0
- package/dist/adapters/types.js +36 -3
- package/dist/brand.d.ts +5 -1
- package/dist/brand.js +18 -2
- package/dist/cli/commands/doctor.d.ts +3 -0
- package/dist/cli/commands/doctor.js +43 -21
- package/dist/cli/commands/fleet.d.ts +7 -0
- package/dist/cli/commands/fleet.js +94 -74
- package/dist/cli/commands/init.js +118 -5
- package/dist/cli/commands/status.js +202 -46
- package/dist/compile/collateral.d.ts +86 -2
- package/dist/compile/collateral.js +294 -3
- package/dist/compile/gsd.d.ts +2 -1
- package/dist/compile/gsd.js +68 -2
- package/dist/compile/native.d.ts +14 -0
- package/dist/compile/native.js +161 -12
- package/dist/config/config.d.ts +82 -5
- package/dist/config/config.js +253 -66
- package/dist/config/fleet-overlay.d.ts +25 -20
- package/dist/config/fleet-overlay.js +195 -77
- package/dist/config/fleet-why.d.ts +23 -0
- package/dist/config/fleet-why.js +42 -0
- package/dist/drivers/herdr.d.ts +21 -3
- package/dist/drivers/herdr.js +344 -110
- package/dist/gates/acceptance.js +7 -2
- package/dist/gates/baseline.d.ts +1 -0
- package/dist/gates/baseline.js +91 -13
- package/dist/gates/llm.d.ts +0 -1
- package/dist/gates/llm.js +5 -30
- package/dist/gates/review.d.ts +9 -1
- package/dist/gates/review.js +105 -10
- package/dist/gates/run-gates.d.ts +9 -0
- package/dist/gates/run-gates.js +285 -41
- package/dist/gates/verdict-cause.d.ts +4 -0
- package/dist/gates/verdict-cause.js +63 -0
- package/dist/graph/schema.d.ts +6 -0
- package/dist/graph/schema.js +8 -5
- package/dist/route/router.d.ts +0 -5
- package/dist/route/router.js +16 -20
- package/dist/run/consult.d.ts +6 -0
- package/dist/run/consult.js +35 -25
- package/dist/run/daemon.d.ts +48 -2
- package/dist/run/daemon.js +1488 -330
- package/dist/run/journal.d.ts +56 -3
- package/dist/run/journal.js +358 -4
- package/dist/run/stall.d.ts +35 -1
- package/dist/run/stall.js +118 -8
- package/dist/tui/cockpit/capture.d.ts +12 -0
- package/dist/tui/cockpit/capture.js +37 -1
- package/dist/tui/cockpit/components.d.ts +2 -0
- package/dist/tui/cockpit/components.js +8 -8
- package/dist/tui/cockpit/derive.d.ts +29 -2
- package/dist/tui/cockpit/derive.js +219 -23
- package/dist/tui/cockpit/run-cockpit.js +128 -27
- package/dist/tui/cockpit/theme.d.ts +32 -26
- package/dist/tui/cockpit/theme.js +11 -5
- package/dist/tui/ink/components.d.ts +0 -15
- package/dist/tui/ink/components.js +0 -17
- package/dist/tui/ink/fleet-app.d.ts +4 -1
- package/dist/tui/ink/fleet-app.js +134 -13
- package/fixtures/sample.native.md +1 -1
- package/package.json +1 -1
- package/skills/tickmarkr-overseer/SKILL.md +354 -34
- package/skills/tickmarkr-overseer/scripts/watch-artifacts.sh +70 -0
- package/skills/tickmarkr-overseer/scripts/watch-panes.sh +1 -1
- package/dist/tui/ink/studio-app.d.ts +0 -59
- package/dist/tui/ink/studio-app.js +0 -320
- package/dist/tui/save.d.ts +0 -38
- package/dist/tui/save.js +0 -96
- package/dist/tui/staging.d.ts +0 -29
- package/dist/tui/staging.js +0 -78
package/dist/brand.d.ts
CHANGED
|
@@ -16,7 +16,11 @@ export declare function bannerShell(): string;
|
|
|
16
16
|
export declare const TICKMARKR_EXIT_TRAILER = "printf '\\nTICKMARKR_''EXIT:%s\\n' $?";
|
|
17
17
|
/** OBS-50: visible-pane bootstrap script — banner + agent command + byte-identical exit trailer. */
|
|
18
18
|
export declare function paneDispatchScript(body: string[]): string;
|
|
19
|
-
/**
|
|
19
|
+
/** Record that the command being passed directly to ExecutorDriver.run is a daemon-built launch. */
|
|
20
|
+
export declare function paneLaunchCommand(command: string): string;
|
|
21
|
+
/** Driver-side half of paneLaunchCommand's one-shot handoff. */
|
|
22
|
+
export declare function consumePaneLaunchIntent(): boolean;
|
|
23
|
+
/** OBS-50/342: one short, intent-bearing pane-run line; bootstrap lives beside the prompt. */
|
|
20
24
|
export declare function paneDispatchCommand(scriptPath: string): string;
|
|
21
25
|
/** The settled brand green ramp (256-color), bright → deep. */
|
|
22
26
|
export declare const BRAND_RAMP: readonly [84, 78, 41, 35];
|
package/dist/brand.js
CHANGED
|
@@ -78,9 +78,25 @@ export const TICKMARKR_EXIT_TRAILER = `printf '\\nTICKMARKR_''EXIT:%s\\n' $?`;
|
|
|
78
78
|
export function paneDispatchScript(body) {
|
|
79
79
|
return ["export BASH_SILENCE_DEPRECATION_WARNING=1", ...body, TICKMARKR_EXIT_TRAILER].join("\n");
|
|
80
80
|
}
|
|
81
|
-
|
|
81
|
+
const pendingPaneLaunchIntents = [];
|
|
82
|
+
/** Record that the command being passed directly to ExecutorDriver.run is a daemon-built launch. */
|
|
83
|
+
export function paneLaunchCommand(command) {
|
|
84
|
+
const intent = { kind: "launch" };
|
|
85
|
+
pendingPaneLaunchIntents.push(intent);
|
|
86
|
+
queueMicrotask(() => {
|
|
87
|
+
const index = pendingPaneLaunchIntents.indexOf(intent);
|
|
88
|
+
if (index >= 0)
|
|
89
|
+
pendingPaneLaunchIntents.splice(index, 1);
|
|
90
|
+
});
|
|
91
|
+
return command;
|
|
92
|
+
}
|
|
93
|
+
/** Driver-side half of paneLaunchCommand's one-shot handoff. */
|
|
94
|
+
export function consumePaneLaunchIntent() {
|
|
95
|
+
return pendingPaneLaunchIntents.shift()?.kind === "launch";
|
|
96
|
+
}
|
|
97
|
+
/** OBS-50/342: one short, intent-bearing pane-run line; bootstrap lives beside the prompt. */
|
|
82
98
|
export function paneDispatchCommand(scriptPath) {
|
|
83
|
-
return `bash ${shq(scriptPath)}
|
|
99
|
+
return paneLaunchCommand(`bash ${shq(scriptPath)}`);
|
|
84
100
|
}
|
|
85
101
|
// ── design system (v1.50) ── contract: docs/codebase/CLI-DESIGN.md ──────────
|
|
86
102
|
// Every cockpit surface styles through these tokens/glyphs/helpers. Styled only
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { type ClaudeAlias } from "../../adapters/claude-code.js";
|
|
2
2
|
import type { WorkerAdapter } from "../../adapters/types.js";
|
|
3
3
|
import { type KimiDoctorTurnResult } from "../../adapters/kimi.js";
|
|
4
|
+
import { type CatalogReadResult } from "../../adapters/catalog-remote.js";
|
|
4
5
|
export type DoctorOpts = {
|
|
5
6
|
banner?: boolean;
|
|
6
7
|
kimiTurnProbe?: (cwd: string) => Promise<KimiDoctorTurnResult>;
|
|
7
8
|
resolveClaudeAliasIdentity?: (cwd: string, alias: ClaudeAlias) => string | undefined;
|
|
9
|
+
catalog?: CatalogReadResult;
|
|
10
|
+
catalogNow?: () => Date;
|
|
8
11
|
};
|
|
9
12
|
export declare function doctor(_argv: string[], cwd?: string, adapters?: WorkerAdapter[], opts?: DoctorOpts): Promise<string>;
|
|
@@ -1,19 +1,29 @@
|
|
|
1
1
|
import { writeFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import { allAdapters, binaryShadowWarnings, detectCandidateClis, flagDriftWarnings, modelAliasExclusions, modelAliasLine, probeAll, probeModels,
|
|
3
|
+
import { allAdapters, binaryShadowWarnings, detectCandidateClis, flagDriftWarnings, modelAliasExclusions, modelAliasLine, probeAll, probeModels, servableExclusions, servabilityLine, writeDoctor } from "../../adapters/registry.js";
|
|
4
4
|
import { CLAUDE_ALIAS_IDENTITY_STAMPS, claudeCode, resolveClaudeAliasIdentity } from "../../adapters/claude-code.js";
|
|
5
5
|
import { BANNER, dim, fail, kvRow, legend, ok, rule, statusRow, title } from "../../brand.js";
|
|
6
6
|
import { tickmarkrDir, stateDirName } from "../../graph/graph.js";
|
|
7
|
-
import { declaredModelWindow, hasWindowsConfig, modelLints, suggestOverlay, ttyVisual } from "../../adapters/model-lints.js";
|
|
8
|
-
import {
|
|
7
|
+
import { catalogModelAdvisory, declaredModelWindow, hasWindowsConfig, modelLints, suggestOverlay, ttyVisual } from "../../adapters/model-lints.js";
|
|
8
|
+
import { loadConfig, overlayPreferShapes } from "../../config/config.js";
|
|
9
9
|
import { HerdrDriver } from "../../drivers/herdr.js";
|
|
10
10
|
import { kimi, probeKimiDoctorTurn } from "../../adapters/kimi.js";
|
|
11
11
|
import { denyPreferCollisionLine, denyPreferCollisions, disallowedBy, excludedChannels, exclusionLine, preferRanks } from "../../route/preference.js";
|
|
12
|
+
import { readCachedCatalog, refreshCatalogCommand } from "../../adapters/catalog-remote.js";
|
|
12
13
|
const visual = () => process.stdout.isTTY === true && process.env.NO_COLOR === undefined;
|
|
13
14
|
const alignedStatusRow = (verdict, key, value) => ` ${statusRow(verdict, kvRow(key, value).slice(2))}`;
|
|
14
15
|
const attentionRow = (text) => ` ${statusRow("warn", text)}`;
|
|
15
16
|
export async function doctor(_argv, cwd = process.cwd(), adapters = allAdapters(), opts = {}) {
|
|
17
|
+
if (_argv.length === 1 && _argv[0] === "--refresh-catalog") {
|
|
18
|
+
const refreshed = await refreshCatalogCommand({ repoRoot: cwd, now: opts.catalogNow });
|
|
19
|
+
return refreshed.updated
|
|
20
|
+
? `tickmarkr doctor --refresh-catalog: model catalog refreshed${refreshed.warning ? `; ${refreshed.warning}` : ""}`
|
|
21
|
+
: `tickmarkr doctor --refresh-catalog: catalog refresh unavailable — ${refreshed.warning ?? "unknown failure"}; retained ${refreshed.catalog.source} catalog`;
|
|
22
|
+
}
|
|
16
23
|
const cfg = loadConfig(cwd);
|
|
24
|
+
// T17: synchronous/cache-only by operator ruling. The only network-bearing catalog function is the
|
|
25
|
+
// separately invoked refreshCatalogCommand; ordinary doctor never calls it.
|
|
26
|
+
const catalog = opts.catalog ?? readCachedCatalog(cwd, { now: opts.catalogNow });
|
|
17
27
|
// banner at START — the logo greets the operator before the ~60s probe wait, never trailing it (operator report 2026-07-17)
|
|
18
28
|
if (opts.banner !== false && visual())
|
|
19
29
|
process.stdout.write(BANNER);
|
|
@@ -72,8 +82,12 @@ export async function doctor(_argv, cwd = process.cwd(), adapters = allAdapters(
|
|
|
72
82
|
const healthy = h.installed && (a.id !== kimi.id || h.authed);
|
|
73
83
|
return alignedStatusRow(healthy ? "pass" : "fail", a.id, state);
|
|
74
84
|
});
|
|
75
|
-
|
|
76
|
-
|
|
85
|
+
if (catalog.warning) {
|
|
86
|
+
rows.push(attentionRow(`model catalog cache unreadable — ${catalog.warning}; using vendored fallback (advisory — routing unchanged)`));
|
|
87
|
+
}
|
|
88
|
+
// v1.48 T1 / v1.86 T12: advisory sweep for known agent CLIs with no drive contract. Presence is
|
|
89
|
+
// resolved through the worker's login shell; advisory targets are never executed or written to health.
|
|
90
|
+
rows.push(...detectCandidateClis({ cwd }).map(({ binary, path }) => alignedStatusRow("warn", binary, `detected at ${path} (no drive contract — not routable)`)));
|
|
77
91
|
const herdr = HerdrDriver.available();
|
|
78
92
|
rows.push(alignedStatusRow(herdr ? "pass" : "fail", "herdr", herdr ? "driver available (HERDR_ENV=1)" : "not detected — subprocess driver will be used"));
|
|
79
93
|
// v1.22 T5: workspace-trust pre-flight — per installed adapter: trusted | seeded | action-required | n/a.
|
|
@@ -143,10 +157,23 @@ export async function doctor(_argv, cwd = process.cwd(), adapters = allAdapters(
|
|
|
143
157
|
}
|
|
144
158
|
}
|
|
145
159
|
}
|
|
160
|
+
const resolvedCatalogModel = (adapter, model) => {
|
|
161
|
+
if (adapter !== "claude-code" || !identityResolver || !(model in CLAUDE_ALIAS_IDENTITY_STAMPS))
|
|
162
|
+
return undefined;
|
|
163
|
+
try {
|
|
164
|
+
return identityResolver(cwd, model);
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
return undefined;
|
|
168
|
+
}
|
|
169
|
+
};
|
|
146
170
|
// MODEL-05/06: print-only drift fragment; advisory, whole-line-commented additions, tickmarkr NEVER applies it.
|
|
147
171
|
// TTY gets a one-line summary + the fragment as a file (the full dump drowned everything else,
|
|
148
172
|
// v1.33.1 onboarding); machine/CI surface keeps the inline dump — layout is pinned by tests.
|
|
149
|
-
const frag = suggestOverlay(cfg, health, adapters, stateDirName(cwd)
|
|
173
|
+
const frag = suggestOverlay(cfg, health, adapters, stateDirName(cwd), {
|
|
174
|
+
catalog,
|
|
175
|
+
resolvedModel: resolvedCatalogModel,
|
|
176
|
+
});
|
|
150
177
|
let drift = "";
|
|
151
178
|
if (frag) {
|
|
152
179
|
if (visual()) {
|
|
@@ -191,24 +218,19 @@ export async function doctor(_argv, cwd = process.cwd(), adapters = allAdapters(
|
|
|
191
218
|
: "";
|
|
192
219
|
rows.push(` ${m.padEnd(w)} ${classified[m].padEnd(8)}${windowCol} ${auth} ${dim("denied=")}${denied} ${dim("prefer=")}${pref}`);
|
|
193
220
|
}
|
|
194
|
-
if (unclassified.length)
|
|
221
|
+
if (unclassified.length) {
|
|
222
|
+
// Keep the historical count as an index, but it is no longer the whole report: every model gets
|
|
223
|
+
// a cache-backed evidence or explicit uncovered line. Neither branch changes cfg, health, or route().
|
|
195
224
|
rows.push(` ${dim(`(${unclassified.length} more listed, unclassified)`)}`);
|
|
225
|
+
for (const model of unclassified) {
|
|
226
|
+
const advisory = catalogModelAdvisory(cfg, catalog, a.id, model, resolvedCatalogModel(a.id, model));
|
|
227
|
+
rows.push(` ${dim(`catalog · ${advisory.display}`)}`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
196
230
|
return rows;
|
|
197
231
|
});
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
? Object.keys(cfg.routing.map).flatMap((shape) => {
|
|
201
|
-
const auto = autoPrefer[shape];
|
|
202
|
-
if (!Array.isArray(auto))
|
|
203
|
-
return [];
|
|
204
|
-
const seed = DEFAULT_CONFIG.routing.map[shape]?.prefer ?? [];
|
|
205
|
-
if (!auto.length && !seed.length)
|
|
206
|
-
return []; // nothing derived, nothing seeded — an empty line is noise
|
|
207
|
-
return [` ${dim(`prefer ${shape} (auto):`)} ${auto.join(" > ")} ${dim("— seed was")} [${seed.join(", ")}]`];
|
|
208
|
-
})
|
|
209
|
-
: [];
|
|
210
|
-
const modelSummary = modelStatus.length || preferStatus.length
|
|
211
|
-
? `\n${legend("model status:")}\n${[...modelStatus, ...preferStatus].join("\n")}`
|
|
232
|
+
const modelSummary = modelStatus.length
|
|
233
|
+
? `\n${legend("model status:")}\n${modelStatus.join("\n")}`
|
|
212
234
|
: "";
|
|
213
235
|
const header = visual()
|
|
214
236
|
? `${statusRow("pass", `${title("tickmarkr doctor")} ${legend("· capability matrix")}`)}\n${rule()}`
|
|
@@ -15,6 +15,13 @@ export type FleetIO = {
|
|
|
15
15
|
debug?: boolean;
|
|
16
16
|
reloadGuard?: (bytes: string) => string | null;
|
|
17
17
|
};
|
|
18
|
+
export type FleetWriteHooks = {
|
|
19
|
+
readPrior?: (path: string) => string;
|
|
20
|
+
beforeRename?: () => void;
|
|
21
|
+
};
|
|
22
|
+
/** Atomic sibling-temp writer. Reading and serializing happen before `${path}.tmp` exists, and
|
|
23
|
+
* any pre-rename interruption unlinks only that exact candidate while the original remains intact. */
|
|
24
|
+
export declare function writeFleetOverlay(path: string, serialize: (priorBytes: string) => string, hooks?: FleetWriteHooks): void;
|
|
18
25
|
export declare function fleet(argv: string[], cwd?: string, adapters?: WorkerAdapter[], io?: FleetIO): Promise<string | {
|
|
19
26
|
out: string;
|
|
20
27
|
code: number;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { dirname } from "node:path";
|
|
3
3
|
import { parseArgs } from "node:util";
|
|
4
|
-
import { allAdapters, discoverChannels, doctorAgeMs, initDoctorReuse
|
|
4
|
+
import { allAdapters, discoverChannels, doctorAgeMs, initDoctorReuse } from "../../adapters/registry.js";
|
|
5
5
|
import { fleetUnclassifiedModels } from "../../adapters/model-lints.js";
|
|
6
|
-
import { fleetEditableFromConfig, fleetEditableEquals,
|
|
6
|
+
import { fleetEditableFromConfig, fleetEditableEquals, formatFleetPrint, globalConfigDir, overlayBytesLoadError, renderFleetOverlayWrite, repoOverlayPath, ROUTING_MODES, unifiedYamlDiff, } from "../../config/config.js";
|
|
7
|
+
import { projectFleetWhy, renderFleetWhy } from "../../config/fleet-why.js";
|
|
7
8
|
import { SHAPES, TIERS } from "../../graph/schema.js";
|
|
8
9
|
import { candidateRow, costSignal, shapeCandidates } from "./fleet-picker.js";
|
|
9
10
|
import { route } from "../../route/router.js";
|
|
@@ -36,26 +37,30 @@ function currentRepoOverlayText(repoRoot) {
|
|
|
36
37
|
const p = repoOverlayPath(repoRoot);
|
|
37
38
|
return existsSync(p) ? readFileSync(p, "utf8") : "";
|
|
38
39
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
40
|
+
/** Atomic sibling-temp writer. Reading and serializing happen before `${path}.tmp` exists, and
|
|
41
|
+
* any pre-rename interruption unlinks only that exact candidate while the original remains intact. */
|
|
42
|
+
export function writeFleetOverlay(path, serialize, hooks = {}) {
|
|
43
|
+
const prior = hooks.readPrior
|
|
44
|
+
? hooks.readPrior(path)
|
|
45
|
+
: (existsSync(path) ? readFileSync(path, "utf8") : "");
|
|
46
|
+
const bytes = serialize(prior);
|
|
47
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
48
|
+
const tmp = `${path}.tmp`;
|
|
49
|
+
try {
|
|
50
|
+
writeFileSync(tmp, bytes);
|
|
51
|
+
hooks.beforeRename?.();
|
|
52
|
+
renameSync(tmp, path);
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
try {
|
|
56
|
+
if (existsSync(tmp))
|
|
57
|
+
unlinkSync(tmp);
|
|
47
58
|
}
|
|
59
|
+
catch {
|
|
60
|
+
// Preserve the write failure; cleanup is constrained to the exact sibling candidate.
|
|
61
|
+
}
|
|
62
|
+
throw error;
|
|
48
63
|
}
|
|
49
|
-
return out;
|
|
50
|
-
}
|
|
51
|
-
// v1.51 T4: serializeFleetOverlay predates routing.mode — splice the mode line under routing:
|
|
52
|
-
// so a repo-declared mode survives fleet writes and a mode selection lands as routing.mode.
|
|
53
|
-
function withModeLine(yaml, mode) {
|
|
54
|
-
if (!mode)
|
|
55
|
-
return yaml;
|
|
56
|
-
if (/^routing:$/m.test(yaml))
|
|
57
|
-
return yaml.replace(/^routing:$/m, `routing:\n mode: ${mode}`);
|
|
58
|
-
return `routing:\n mode: ${mode}\n${yaml}`;
|
|
59
64
|
}
|
|
60
65
|
function formatFleetSteering(cfg) {
|
|
61
66
|
const blocks = [];
|
|
@@ -76,12 +81,14 @@ export async function fleet(argv, cwd = process.cwd(), adapters = allAdapters(),
|
|
|
76
81
|
args: argv,
|
|
77
82
|
options: {
|
|
78
83
|
print: { type: "boolean" },
|
|
84
|
+
why: { type: "boolean" },
|
|
79
85
|
"global-dir": { type: "string" },
|
|
80
86
|
fresh: { type: "boolean" },
|
|
81
87
|
},
|
|
82
88
|
});
|
|
83
89
|
const globalDir = values["global-dir"] ?? globalConfigDir();
|
|
84
90
|
const print = values.print ?? false;
|
|
91
|
+
const why = values.why ?? false;
|
|
85
92
|
const input = io.input ?? process.stdin;
|
|
86
93
|
const output = io.output ?? process.stdout;
|
|
87
94
|
const interactive = input.isTTY === true && output.isTTY === true;
|
|
@@ -95,7 +102,7 @@ export async function fleet(argv, cwd = process.cwd(), adapters = allAdapters(),
|
|
|
95
102
|
// not from another parse of either raw overlay.
|
|
96
103
|
return `${body.slice(0, nl)}\n# mode: ${rm.mode.mode} (${rm.source})${body.slice(nl)}${formatFleetSteering(rm.cfg)}`;
|
|
97
104
|
}
|
|
98
|
-
if (!interactive)
|
|
105
|
+
if (!why && !interactive)
|
|
99
106
|
return { out: NON_TTY_MSG, code: 1 };
|
|
100
107
|
const fresh = values.fresh ?? false;
|
|
101
108
|
const { reuse, health: cached } = initDoctorReuse(cwd, fresh);
|
|
@@ -107,10 +114,7 @@ export async function fleet(argv, cwd = process.cwd(), adapters = allAdapters(),
|
|
|
107
114
|
}
|
|
108
115
|
const rm = resolveRunMode(cwd, { globalDir });
|
|
109
116
|
const cfg = rm.cfg;
|
|
110
|
-
|
|
111
|
-
// session must know about every prior note, not only its own edits, or the next write strips them
|
|
112
|
-
const harvested = harvestFleetProvenance(currentRepoOverlayText(cwd));
|
|
113
|
-
const initial = fleetEditableFromConfig(cfg, harvested.tiers);
|
|
117
|
+
const initial = fleetEditableFromConfig(cfg);
|
|
114
118
|
const editable = structuredClone(initial);
|
|
115
119
|
const health = cached;
|
|
116
120
|
const modelGroups = adapters
|
|
@@ -183,21 +187,52 @@ export async function fleet(argv, cwd = process.cwd(), adapters = allAdapters(),
|
|
|
183
187
|
: [" (no floor changes)"]),
|
|
184
188
|
];
|
|
185
189
|
};
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
const assignment = routed.assignment;
|
|
193
|
-
now = `${assignment.adapter}:${assignment.model} (${assignment.channel}, ${assignment.tier}) ${costSignal(assignment, cfg.pricing)}`;
|
|
194
|
-
}
|
|
195
|
-
catch (error) {
|
|
196
|
-
now = error.message;
|
|
190
|
+
const whyDeclaration = (shape, mode, map, mapProducedValue) => {
|
|
191
|
+
const mapChanged = JSON.stringify(map[shape] ?? {}) !== JSON.stringify(editable.map[shape] ?? {});
|
|
192
|
+
if (mapProducedValue) {
|
|
193
|
+
return mapChanged
|
|
194
|
+
? { operatorPinned: true }
|
|
195
|
+
: { declaredAt: `routing.map.${shape}` };
|
|
197
196
|
}
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
197
|
+
const floorSource = modeCfgs[mode].mode.provenance[shape];
|
|
198
|
+
if (floorSource === undefined)
|
|
199
|
+
return {};
|
|
200
|
+
if (floorSource === "config floors")
|
|
201
|
+
return { declaredAt: `routing.floors.${shape}` };
|
|
202
|
+
if (mode !== rm.mode.mode)
|
|
203
|
+
return { operatorPinned: true };
|
|
204
|
+
return rm.source === "default"
|
|
205
|
+
? { declaredAt: `routing.floors.${shape}` }
|
|
206
|
+
: { declaredAt: "routing.mode" };
|
|
207
|
+
};
|
|
208
|
+
const projectedShapeRows = (mode, map) => {
|
|
209
|
+
const values = SHAPES.map((shape) => {
|
|
210
|
+
try {
|
|
211
|
+
const routed = route(previewTask(shape), previewCfg(mode, map), channels, profile, undefined, undefined, PREVIEW_EXPLORE);
|
|
212
|
+
const assignment = routed.assignment;
|
|
213
|
+
const effective = `${assignment.adapter}:${assignment.model} (${assignment.channel}, ${assignment.tier}) ${costSignal(assignment, cfg.pricing)}`;
|
|
214
|
+
const mapProducedValue = map[shape]?.pin !== undefined || routed.provenance.includes("via prefer");
|
|
215
|
+
return {
|
|
216
|
+
id: shape,
|
|
217
|
+
effective,
|
|
218
|
+
...whyDeclaration(shape, mode, map, mapProducedValue),
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
catch (error) {
|
|
222
|
+
const mapProducedValue = map[shape]?.pin !== undefined || (map[shape]?.prefer?.length ?? 0) > 0;
|
|
223
|
+
return {
|
|
224
|
+
id: shape,
|
|
225
|
+
effective: error.message,
|
|
226
|
+
...whyDeclaration(shape, mode, map, mapProducedValue),
|
|
227
|
+
setupCommand: "tickmarkr fleet",
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
return projectFleetWhy(values, { repoRoot: cwd, globalDir });
|
|
232
|
+
};
|
|
233
|
+
const routedShapeRows = (mode, map) => projectedShapeRows(mode, map).map(({ id, label }) => ({ id, label }));
|
|
234
|
+
if (why)
|
|
235
|
+
return renderFleetWhy(projectedShapeRows(rm.mode.mode, editable.map));
|
|
201
236
|
const candidatesForShape = (shape, mode, map) => shapeCandidates(previewTask(shape), previewCfg(mode, map), channels, profile).map((candidate) => ({
|
|
202
237
|
id: `${candidate.assignment.adapter}:${candidate.assignment.model}`,
|
|
203
238
|
label: candidateRow(candidate, cfg.pricing),
|
|
@@ -220,6 +255,7 @@ export async function fleet(argv, cwd = process.cwd(), adapters = allAdapters(),
|
|
|
220
255
|
const discovered = which === "review" ? [...reviewAdapters, ...seats] : seats;
|
|
221
256
|
return [...discovered, ...current.filter((entry) => !discovered.includes(entry))];
|
|
222
257
|
};
|
|
258
|
+
let pendingWrite = null;
|
|
223
259
|
const reviewOverlay = (state) => {
|
|
224
260
|
const staged = structuredClone(initial);
|
|
225
261
|
staged.denyAdapters = state.denyAdapters;
|
|
@@ -237,42 +273,24 @@ export async function fleet(argv, cwd = process.cwd(), adapters = allAdapters(),
|
|
|
237
273
|
// the diff and asks for confirmation, but owns neither filesystem access nor a writer.
|
|
238
274
|
const before = currentRepoOverlayText(cwd);
|
|
239
275
|
const path = repoOverlayPath(cwd);
|
|
240
|
-
const existing = readOverlayFile(path);
|
|
241
276
|
const modeChanged = state.selectedMode !== rm.mode.mode;
|
|
242
|
-
const
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
? structuredClone(existing)
|
|
247
|
-
: fleetRepoOverlayFromDelta(initial, staged, existing);
|
|
248
|
-
let steeringChanged = false;
|
|
249
|
-
for (const key of ["review", "consult"]) {
|
|
250
|
-
if (JSON.stringify(state.steering[key]) === JSON.stringify(initialSteering[key]))
|
|
251
|
-
continue;
|
|
252
|
-
steeringChanged = true;
|
|
253
|
-
const block = { ...merged[key] };
|
|
254
|
-
if (state.steering[key])
|
|
255
|
-
block.prefer = state.steering[key];
|
|
256
|
-
else
|
|
257
|
-
delete block.prefer;
|
|
258
|
-
if (Object.keys(block).length)
|
|
259
|
-
merged[key] = block;
|
|
260
|
-
else
|
|
261
|
-
delete merged[key];
|
|
262
|
-
}
|
|
263
|
-
const tierNotes = structuredClone(harvested.tiers);
|
|
264
|
-
for (const [adapter, models] of Object.entries(provenanceMap(staged))) {
|
|
265
|
-
for (const [model, note] of Object.entries(models)) {
|
|
266
|
-
(tierNotes[adapter] ??= {})[model] = note;
|
|
267
|
-
}
|
|
277
|
+
const steeringChanged = ["review", "consult"].some((key) => JSON.stringify(state.steering[key]) !== JSON.stringify(initialSteering[key]));
|
|
278
|
+
if (!modeChanged && !steeringChanged && fleetEditableEquals(initial, staged)) {
|
|
279
|
+
pendingWrite = null;
|
|
280
|
+
return { kind: "empty" };
|
|
268
281
|
}
|
|
269
|
-
const
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
282
|
+
const write = {
|
|
283
|
+
initial,
|
|
284
|
+
edited: staged,
|
|
285
|
+
...(modeChanged ? { mode: state.selectedMode } : {}),
|
|
286
|
+
steering: { initial: initialSteering, edited: state.steering },
|
|
287
|
+
};
|
|
288
|
+
const after = renderFleetOverlayWrite(before, write);
|
|
289
|
+
if (before === after) {
|
|
290
|
+
pendingWrite = null;
|
|
274
291
|
return { kind: "empty" };
|
|
275
292
|
}
|
|
293
|
+
pendingWrite = write;
|
|
276
294
|
return {
|
|
277
295
|
kind: "diff",
|
|
278
296
|
before,
|
|
@@ -348,7 +366,9 @@ export async function fleet(argv, cwd = process.cwd(), adapters = allAdapters(),
|
|
|
348
366
|
return "fleet: discarded overlay changes";
|
|
349
367
|
// The command remains the single config actuator. Every interactive edit reaches this
|
|
350
368
|
// one write only after the component-rendered diff confirm and the production reload guard.
|
|
351
|
-
|
|
352
|
-
|
|
369
|
+
const write = pendingWrite;
|
|
370
|
+
if (!write)
|
|
371
|
+
throw new Error("fleet write reached confirmation without a staged overlay mutation");
|
|
372
|
+
writeFleetOverlay(result.review.path, (prior) => renderFleetOverlayWrite(prior, write));
|
|
353
373
|
return `fleet: wrote ${result.review.path}`;
|
|
354
374
|
}
|
|
@@ -109,6 +109,43 @@ A run is green only when the run-end event exists in the journal AND tip verify
|
|
|
109
109
|
### Verified handoffs
|
|
110
110
|
|
|
111
111
|
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 pane afterward; never report "relayed" without read-back.
|
|
112
|
+
|
|
113
|
+
### Orient before you act — this block may be the ONLY guidance your host loaded
|
|
114
|
+
|
|
115
|
+
These same bytes are written into EVERY repository guidance file this project has, because hosts disagree
|
|
116
|
+
about which one they read: some load only \`AGENTS.md\`, some also load a repo-level guidance file, some load
|
|
117
|
+
a user-level one instead. **Anything stated in only one file is invisible to some agent.** So do not assume
|
|
118
|
+
you were handed the whole picture — list the repository root, open every guidance file present, and then:
|
|
119
|
+
|
|
120
|
+
- **Read your host's PROJECT MEMORY before starting.** Hosts that keep one store it under a per-project
|
|
121
|
+
state directory keyed by the absolute working-directory path; find it and read its index plus every entry
|
|
122
|
+
whose name concerns METHOD or DISCIPLINE. It holds rules that cost real defects to learn. Entries may
|
|
123
|
+
predate a project rename, so **search by CONCEPT, not by the current product name.** A memory nobody opens
|
|
124
|
+
is worse than none: every seat assumes the lesson is recorded somewhere and no seat looks.
|
|
125
|
+
- **The gates are the product.** Seven, defined in \`src/graph/schema.ts\`:
|
|
126
|
+
\`build test lint evidence scope acceptance review\`. **That is DECLARATION order, not execution order** —
|
|
127
|
+
the first five run as a battery that stops at its first red, then \`acceptance\` and \`review\` run
|
|
128
|
+
CONCURRENTLY (\`run-gates.ts:39\`, *"judge ‖ review"*). The first five are MANDATORY; only \`acceptance\` and
|
|
129
|
+
\`review\` may be omitted per task. Implementations are in \`src/gates/\` — \`baseline.ts\` (build/test/lint,
|
|
130
|
+
diffed against a recorded baseline so pre-existing failures are forgiven), \`evidence.ts\`, \`scope.ts\`,
|
|
131
|
+
\`acceptance.ts\` (its judge reads the DIFF and every criterion must cite a changed hunk), \`review.ts\`
|
|
132
|
+
(cross-vendor). \`run-gates.ts\` drives them. **A declared gate is not a passed gate, and a gate that
|
|
133
|
+
returned zero findings is not the same as a gate that ran.**
|
|
134
|
+
- **Spec-authoring law ships in the spec template** that \`tickmarkr init\` writes: the hard bounds and which
|
|
135
|
+
direction each moves, what makes a criterion real, and why an absence or a source-text grep is never a
|
|
136
|
+
criterion. Read it before authoring or repairing acceptance items.
|
|
137
|
+
- **Editing \`src/gates/\`, \`src/compile/\` or \`src/graph/\`?** Read \`docs/codebase/ARCHITECTURE.md\` first.
|
|
138
|
+
- **EVERY fix gets a ship/no-ship decision, recorded, at the moment it is made.** Ask one question of each
|
|
139
|
+
one: *does a user hit this defect?* If yes, the fix belongs in \`src/**\` or \`skills/**\` — the only trees
|
|
140
|
+
the package carries (\`files: [dist, schema, skills, fixtures]\`). A script, overlay, config entry or
|
|
141
|
+
operator-side workaround that resolves the symptom **locally is not the fix; it is a decision to leave
|
|
142
|
+
every other user broken**, and it must say so in writing and name the condition that removes it.
|
|
143
|
+
**The default answer is SHIP.** A local remedy is the exception and carries the burden of proof.
|
|
144
|
+
Watch for the three shapes this hides in: a fix applied where you happened to be standing rather than
|
|
145
|
+
where the defect lives; an observation filed with a product fix named in its own text and queued
|
|
146
|
+
nowhere; and a local tool that quietly grows into a product feature nobody shipped. **A defect and its
|
|
147
|
+
fix must be recorded in the same place, or the queue silently becomes a list of things everyone assumed
|
|
148
|
+
someone else had shipped.**
|
|
112
149
|
${DOCS_END}
|
|
113
150
|
`;
|
|
114
151
|
// Every applicable host location gets the skills, each paired with its own repository guidance
|
|
@@ -120,6 +157,54 @@ const hostTargets = (cwd) => {
|
|
|
120
157
|
targets.push({ skillsDir: join(cwd, ".claude", "skills"), docPath: join(cwd, "CLAUDE.md") });
|
|
121
158
|
return targets;
|
|
122
159
|
};
|
|
160
|
+
const packagedSkillDir = (skill) => fileURLToPath(new URL(`../../../skills/${skill}`, import.meta.url));
|
|
161
|
+
// Every file the package ships for a skill, DERIVED from the tree rather than listed. OBS-373: a script
|
|
162
|
+
// was added to the shipped overseer skill long after repos had installed it, and because the SKILL.md
|
|
163
|
+
// that MANDATES that script was already on disk, every existence check reported "installed" while the
|
|
164
|
+
// thing the skill tells you to run was absent. A literal list here would be the same closed-set-restated
|
|
165
|
+
// defect one layer down, so this walks instead.
|
|
166
|
+
function skillFileList(root, prefix = "") {
|
|
167
|
+
return readdirSync(root, { withFileTypes: true })
|
|
168
|
+
.sort((a, b) => a.name.localeCompare(b.name))
|
|
169
|
+
.flatMap((entry) => {
|
|
170
|
+
const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
171
|
+
return entry.isDirectory() ? skillFileList(join(root, entry.name), rel) : [rel];
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
// Drift is only computed for skills that ARE installed. A skill never installed is not drift — `--agent`
|
|
175
|
+
// installs it and says so, and nagging about it would bury the case this exists for.
|
|
176
|
+
function skillDrift(cwd) {
|
|
177
|
+
const drifted = [];
|
|
178
|
+
for (const { skillsDir } of hostTargets(cwd)) {
|
|
179
|
+
for (const skill of AGENT_SKILLS) {
|
|
180
|
+
const dir = join(skillsDir, skill);
|
|
181
|
+
if (!existsSync(join(dir, "SKILL.md")))
|
|
182
|
+
continue;
|
|
183
|
+
const src = packagedSkillDir(skill);
|
|
184
|
+
const missing = [];
|
|
185
|
+
const modified = [];
|
|
186
|
+
for (const rel of skillFileList(src)) {
|
|
187
|
+
// readFileSync resolves symlinks, so a symlinked mirror compares equal to a copied one.
|
|
188
|
+
if (!existsSync(join(dir, rel)))
|
|
189
|
+
missing.push(rel);
|
|
190
|
+
else if (!readFileSync(join(dir, rel)).equals(readFileSync(join(src, rel))))
|
|
191
|
+
modified.push(rel);
|
|
192
|
+
}
|
|
193
|
+
if (missing.length || modified.length)
|
|
194
|
+
drifted.push({ skill, dir, missing, modified });
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return drifted;
|
|
198
|
+
}
|
|
199
|
+
// Name what drifted. "stale" alone sends the reader to diff three directories by hand, and the file that
|
|
200
|
+
// matters is usually the one they never knew shipped.
|
|
201
|
+
const describeDrift = (d) => [
|
|
202
|
+
d.missing.length ? `missing ${d.missing.join(", ")}` : "",
|
|
203
|
+
d.modified.length ? `modified ${d.modified.join(", ")}` : "",
|
|
204
|
+
].filter(Boolean).join(", ");
|
|
205
|
+
// PRESENCE, deliberately — the wizard's question is "install these?", and a repo that already has them
|
|
206
|
+
// should not be asked again just because a copy drifted. Staleness is a different question with a
|
|
207
|
+
// different answer (`--agent --force`), and it is reported by name on every init path below.
|
|
123
208
|
const skillsInstalled = (cwd) => hostTargets(cwd).every((t) => AGENT_SKILLS.every((s) => existsSync(join(t.skillsDir, s, "SKILL.md"))));
|
|
124
209
|
const wizardDriverDefault = () => process.env.HERDR_ENV === "1" ? "herdr" : "auto";
|
|
125
210
|
async function installAgentFiles(cwd, force, docs, notes) {
|
|
@@ -132,12 +217,20 @@ async function installAgentFiles(cwd, force, docs, notes) {
|
|
|
132
217
|
return /^(?:y|yes)$/i.test((await prompt.question(`${question} [y/N] `)).trim());
|
|
133
218
|
};
|
|
134
219
|
try {
|
|
220
|
+
const drift = skillDrift(cwd);
|
|
135
221
|
for (const { skillsDir, docPath } of hostTargets(cwd)) {
|
|
136
222
|
for (const skill of AGENT_SKILLS) {
|
|
137
223
|
const dest = join(skillsDir, skill, "SKILL.md");
|
|
138
224
|
const exists = existsSync(dest);
|
|
139
|
-
|
|
140
|
-
|
|
225
|
+
const stale = drift.find((d) => d.dir === join(skillsDir, skill));
|
|
226
|
+
// An install that already matches the package byte for byte needs no prompt and no write. Before
|
|
227
|
+
// this, every re-init asked to overwrite a current skill and the answer meant nothing either way.
|
|
228
|
+
if (exists && !stale) {
|
|
229
|
+
notes.push(`kept current ${dest}`);
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
if (exists && !force && !(await confirm(`Overwrite ${dest} (${describeDrift(stale)})?`))) {
|
|
233
|
+
notes.push(`skipped existing ${dest} — ${describeDrift(stale)}; pass --force to overwrite it`);
|
|
141
234
|
continue;
|
|
142
235
|
}
|
|
143
236
|
// whole skill dir, not just SKILL.md — the overseer ships its pane-watcher script
|
|
@@ -145,9 +238,21 @@ async function installAgentFiles(cwd, force, docs, notes) {
|
|
|
145
238
|
notes.push(`${exists ? "overwrote" : "wrote"} ${dest}`);
|
|
146
239
|
}
|
|
147
240
|
const current = existsSync(docPath) ? readFileSync(docPath, "utf8") : "";
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
241
|
+
// The block is CANONICAL guidance ("stated once here, restated nowhere"), so a repo that has
|
|
242
|
+
// run init once must still be able to receive corrections to it. Before v1.86 this branch only
|
|
243
|
+
// ever said "kept", with no lever — not even --force, which IS honoured for skills above. So
|
|
244
|
+
// the block was write-once and every later improvement reached new repos only. Under --force,
|
|
245
|
+
// replace the marked region in place and leave every human-authored line outside it untouched.
|
|
246
|
+
const marked = new RegExp(`<!-- (?:tickmarkr|${LEGACY_PREFIX}):agent-docs begin -->[\\s\\S]*?`
|
|
247
|
+
+ `<!-- (?:tickmarkr|${LEGACY_PREFIX}):agent-docs end -->`);
|
|
248
|
+
if (marked.test(current)) {
|
|
249
|
+
if (force) {
|
|
250
|
+
writeFileSync(docPath, current.replace(marked, AGENT_DOCS.trimEnd()));
|
|
251
|
+
notes.push(`refreshed tickmarkr agent docs in ${docPath}`);
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
notes.push(`kept existing tickmarkr agent docs in ${docPath}; pass --force to refresh them`);
|
|
255
|
+
}
|
|
151
256
|
}
|
|
152
257
|
else if (docs || await confirm(`Append tickmarkr agent docs to ${docPath}?`)) {
|
|
153
258
|
appendFileSync(docPath, `${current ? current.endsWith("\n") ? "\n" : "\n\n" : ""}${AGENT_DOCS}`);
|
|
@@ -249,6 +354,14 @@ export async function init(argv, cwd = process.cwd()) {
|
|
|
249
354
|
notes.push(`wrote ${specPath}`);
|
|
250
355
|
}
|
|
251
356
|
}
|
|
357
|
+
// Say it on EVERY init, not only the --agent path. The upgrade that produced OBS-373 is silent by
|
|
358
|
+
// shape: the package moves, the installed copy does not, and nothing in the repo changes to hint at it.
|
|
359
|
+
// --agent reports drift per skill below, so this only speaks when that path will not.
|
|
360
|
+
if (!values.agent) {
|
|
361
|
+
for (const d of skillDrift(cwd)) {
|
|
362
|
+
notes.push(`stale ${join(d.dir, "SKILL.md")} — ${describeDrift(d)}; run tickmarkr init --agent --force to refresh it`);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
252
365
|
const fresh = values.fresh ?? false;
|
|
253
366
|
const { reuse, ageMs, health } = initDoctorReuse(cwd, fresh);
|
|
254
367
|
const doc = reuse && health && ageMs !== null
|