tickmarkr 1.49.0 → 1.51.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/model-lints.d.ts +1 -0
- package/dist/adapters/model-lints.js +9 -0
- package/dist/brand.d.ts +62 -0
- package/dist/brand.js +74 -2
- package/dist/cli/commands/doctor.js +33 -46
- package/dist/cli/commands/fleet.js +89 -30
- package/dist/cli/commands/init.js +22 -2
- package/dist/cli/commands/plan.d.ts +1 -1
- package/dist/cli/commands/plan.js +67 -7
- package/dist/cli/commands/report.js +13 -1
- package/dist/cli/commands/run.d.ts +2 -0
- package/dist/cli/commands/run.js +48 -13
- package/dist/cli/commands/status.js +38 -41
- package/dist/compile/native.js +18 -2
- package/dist/config/config.d.ts +28 -0
- package/dist/config/config.js +103 -8
- package/dist/drivers/herdr.js +6 -2
- package/dist/graph/schema.d.ts +6 -0
- package/dist/graph/schema.js +6 -0
- package/dist/route/profile.d.ts +12 -0
- package/dist/route/profile.js +31 -0
- package/dist/run/daemon.d.ts +16 -0
- package/dist/run/daemon.js +61 -13
- package/package.json +1 -1
|
@@ -17,6 +17,7 @@ export type RoutedAssignment = {
|
|
|
17
17
|
/** Advisory only — absent windows config or undeclared model window ⇒ no lint. */
|
|
18
18
|
export declare function contextWindowLints(tasks: ReadonlyArray<Task>, assignments: ReadonlyArray<RoutedAssignment>, cfg: TickmarkrConfig, repoRoot: string): string[];
|
|
19
19
|
export declare function seedPreferLints(cfg: TickmarkrConfig, health: Record<string, AuthHealth>, adapters: WorkerAdapter[], overlayPreferShapes?: ReadonlySet<string>): string[];
|
|
20
|
+
export declare function mapTierLints(cfg: TickmarkrConfig): string[];
|
|
20
21
|
export declare function modelLints(cfg: TickmarkrConfig, health: Record<string, AuthHealth>, adapters: WorkerAdapter[], opts?: {
|
|
21
22
|
tty?: boolean;
|
|
22
23
|
stateDir?: string;
|
|
@@ -95,6 +95,14 @@ export function seedPreferLints(cfg, health, adapters, overlayPreferShapes = new
|
|
|
95
95
|
}
|
|
96
96
|
return lints;
|
|
97
97
|
}
|
|
98
|
+
// v1.51 T3: built-in defaults no longer carry map tiers, so any tier surviving the merge came from an
|
|
99
|
+
// overlay. Deprecated — routing.floors is the single band authority; MapEntrySchema.tier removal itself
|
|
100
|
+
// is deferred to the next version. One lint per shape, identical text on every surface (plan + doctor).
|
|
101
|
+
export function mapTierLints(cfg) {
|
|
102
|
+
return Object.entries(cfg.routing.map)
|
|
103
|
+
.filter(([, entry]) => entry.tier !== undefined)
|
|
104
|
+
.map(([shape, entry]) => `routing.map.${shape}.tier: ${entry.tier} is deprecated — move this value to routing.floors.${shape} (single band authority; tier removal deferred to next version)`);
|
|
105
|
+
}
|
|
98
106
|
// Diffs detected models (doctor.json) against configured tiers, both directions, per adapter id in cfg.tiers.
|
|
99
107
|
// No ` ! ` prefix here — the consumer (doctor rows / plan lints) owns that. Pre-v1.5 doctor.json (models:[], no
|
|
100
108
|
// modelsDetectedAt) is the compat baseline: `?.`/`?? []` everywhere, no zod (would reject old files).
|
|
@@ -137,6 +145,7 @@ export function modelLints(cfg, health, adapters, opts) {
|
|
|
137
145
|
}
|
|
138
146
|
}
|
|
139
147
|
lints.push(...seedPreferLints(cfg, health, adapters, opts?.overlayPreferShapes));
|
|
148
|
+
lints.push(...mapTierLints(cfg)); // v1.51 T3: shared here so plan and doctor draw the exact same message
|
|
140
149
|
return lints;
|
|
141
150
|
}
|
|
142
151
|
// T2/T6: one lint per exclusion, naming the probe reason and date. TTY truncates reasons to 60 chars and
|
package/dist/brand.d.ts
CHANGED
|
@@ -1,9 +1,71 @@
|
|
|
1
|
+
import type { OwnedName } from "./drivers/types.js";
|
|
1
2
|
export declare const BANNER: string;
|
|
2
3
|
/** ANSI-stripped, trailing-space-trimmed twin of BANNER — README hero and other plain surfaces. */
|
|
3
4
|
export declare const PLAIN_BANNER: string;
|
|
5
|
+
export declare const PANE_IDENTITY_ENV = "TICKMARKR_PANE_IDENTITY";
|
|
6
|
+
/** One-line pane identity through the T1 ownership contract: role · task · attempt · run. */
|
|
7
|
+
export declare function paneIdentityLine(o: OwnedName): string;
|
|
4
8
|
export declare function bannerShell(): string;
|
|
5
9
|
export declare const TICKMARKR_EXIT_TRAILER = "printf '\\nTICKMARKR_''EXIT:%s\\n' $?";
|
|
6
10
|
/** OBS-50: visible-pane bootstrap script — banner + agent command + byte-identical exit trailer. */
|
|
7
11
|
export declare function paneDispatchScript(body: string[]): string;
|
|
8
12
|
/** OBS-50: one short herdr pane-run line; bootstrap lives in the script file beside the prompt. */
|
|
9
13
|
export declare function paneDispatchCommand(scriptPath: string): string;
|
|
14
|
+
/** The settled brand green ramp (256-color), bright → deep — the BANNER hues. */
|
|
15
|
+
export declare const BRAND_RAMP: readonly [84, 78, 41, 35];
|
|
16
|
+
/** Brand green (ramp anchor 41) — the tickmark hue; also the ok/pass/authed verdict color. */
|
|
17
|
+
export declare const brand: (s: string) => string;
|
|
18
|
+
/** Ok verdicts (pass/authed/green) render in the brand green ramp — same hue as the tickmark. */
|
|
19
|
+
export declare const ok: (s: string) => string;
|
|
20
|
+
/** Fail verdicts (unauthed/red) — red, always paired with the ✗ shape. */
|
|
21
|
+
export declare const fail: (s: string) => string;
|
|
22
|
+
/** Attention (warn/lint) — amber, always paired with the ! shape. */
|
|
23
|
+
export declare const warn: (s: string) => string;
|
|
24
|
+
/** Chrome (legends, rules, parentheticals, inactive state) — dim. */
|
|
25
|
+
export declare const dim: (s: string) => string;
|
|
26
|
+
/** Emphasis (titles, selection, the product name) — bold. */
|
|
27
|
+
export declare const bold: (s: string) => string;
|
|
28
|
+
/** Every semantic color token, for sweeps: each is TTY-gated and NO_COLOR-aware. */
|
|
29
|
+
export declare const TOKENS: {
|
|
30
|
+
readonly brand: (s: string) => string;
|
|
31
|
+
readonly ok: (s: string) => string;
|
|
32
|
+
readonly fail: (s: string) => string;
|
|
33
|
+
readonly warn: (s: string) => string;
|
|
34
|
+
readonly dim: (s: string) => string;
|
|
35
|
+
readonly bold: (s: string) => string;
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* The glyph vocabulary — plain characters only; color layers on via tokens so
|
|
39
|
+
* shape survives NO_COLOR. Bracket toggles ([x]/[ ]) are forbidden on every surface.
|
|
40
|
+
*/
|
|
41
|
+
export declare const GLYPHS: {
|
|
42
|
+
/** Cursor row pointer in list pickers. */
|
|
43
|
+
readonly pointer: "❯";
|
|
44
|
+
/** Active toggle: THE brand tickmark — always rendered via brand(), never brackets. */
|
|
45
|
+
readonly toggleActive: "✓";
|
|
46
|
+
/** Inactive toggle: dim circle — always rendered via dim(). */
|
|
47
|
+
readonly toggleInactive: "○";
|
|
48
|
+
/** Pass verdict. */
|
|
49
|
+
readonly pass: "✓";
|
|
50
|
+
/** Fail verdict. */
|
|
51
|
+
readonly fail: "✗";
|
|
52
|
+
/** Attention/warn verdict. */
|
|
53
|
+
readonly attention: "!";
|
|
54
|
+
/** Neutral/skip — the dash. */
|
|
55
|
+
readonly neutral: "-";
|
|
56
|
+
};
|
|
57
|
+
/** The active toggle as rendered: brand green tickmark on a TTY, plain ✓ otherwise. */
|
|
58
|
+
export declare const toggleActive: () => string;
|
|
59
|
+
/** The inactive toggle as rendered: dim circle on a TTY, plain ○ otherwise. */
|
|
60
|
+
export declare const toggleInactive: () => string;
|
|
61
|
+
/** Dominant title line — one glance answers "what am I looking at". Bold on a TTY. */
|
|
62
|
+
export declare const title: (text: string) => string;
|
|
63
|
+
/** Single dim legend line under a title (key hints) — never scattered, never a paragraph. */
|
|
64
|
+
export declare const legend: (text: string) => string;
|
|
65
|
+
/** Horizontal rule sized to the terminal (capped at 100 cols) — dim chrome. */
|
|
66
|
+
export declare const rule: (width?: number) => string;
|
|
67
|
+
/** Aligned key-value row: dim key, plain value. Padding applied BEFORE styling so columns hold. */
|
|
68
|
+
export declare const kvRow: (key: string, value: string, keyWidth?: number) => string;
|
|
69
|
+
export type Verdict = "pass" | "fail" | "warn" | "neutral";
|
|
70
|
+
/** Status row: verdict glyph FIRST, then the label — glyph-first, distinct shape per verdict. */
|
|
71
|
+
export declare const statusRow: (verdict: Verdict, label: string) => string;
|
package/dist/brand.js
CHANGED
|
@@ -12,11 +12,21 @@ export const BANNER = [
|
|
|
12
12
|
].join("\n");
|
|
13
13
|
/** ANSI-stripped, trailing-space-trimmed twin of BANNER — README hero and other plain surfaces. */
|
|
14
14
|
export const PLAIN_BANNER = BANNER.replace(/\x1b\[[0-9;]*m/g, "").replace(/[ \t]+$/gm, "");
|
|
15
|
-
//
|
|
15
|
+
// T5: the pane identity every visible pane announces under the logo. HerdrDriver seeds this env var
|
|
16
|
+
// into each pane shell at slot() time (paneIdentityLine of the pane's T1 owned name); the banner's
|
|
17
|
+
// identity line reads it at pane runtime, so every role — worker/judge/review/consult — wears the
|
|
18
|
+
// same header without the dispatch call sites threading identity through the script.
|
|
19
|
+
export const PANE_IDENTITY_ENV = "TICKMARKR_PANE_IDENTITY";
|
|
20
|
+
/** One-line pane identity through the T1 ownership contract: role · task · attempt · run. */
|
|
21
|
+
export function paneIdentityLine(o) {
|
|
22
|
+
return `${o.role} · ${o.taskId} · attempt ${o.attempt}${o.runId ? ` · ${o.runId}` : ""}`;
|
|
23
|
+
}
|
|
24
|
+
// Shell one-liner that prints the banner inside a pane before a gate command runs, followed by ONE
|
|
25
|
+
// dim identity line (the seeded $TICKMARKR_PANE_IDENTITY, else a bare "tickmarkr"). ESC bytes are
|
|
16
26
|
// carried as printf %b escapes (never raw control bytes in a command string crossing the herdr socket).
|
|
17
27
|
export function bannerShell() {
|
|
18
28
|
const printable = BANNER.replaceAll("\x1b", "\\033").replaceAll("\n", "\\n");
|
|
19
|
-
return `printf '%b\\n' '${printable}'`;
|
|
29
|
+
return `printf '%b\\n' '${printable}' "\\033[2m\${${PANE_IDENTITY_ENV}:-tickmarkr}\\033[0m"`;
|
|
20
30
|
}
|
|
21
31
|
// OBS-50: quote-split exit marker — herdr echoes the typed command into the transcript that
|
|
22
32
|
// waitOutput matches, so the literal must not appear unsplit in the dispatch line.
|
|
@@ -29,3 +39,65 @@ export function paneDispatchScript(body) {
|
|
|
29
39
|
export function paneDispatchCommand(scriptPath) {
|
|
30
40
|
return `bash ${shq(scriptPath)}`;
|
|
31
41
|
}
|
|
42
|
+
// ── design system (v1.50) ── contract: docs/codebase/CLI-DESIGN.md ──────────
|
|
43
|
+
// Every cockpit surface styles through these tokens/glyphs/helpers. Styled only
|
|
44
|
+
// on a real TTY with NO_COLOR unset; otherwise output is the plain text itself
|
|
45
|
+
// (non-TTY surfaces stay byte-pinned and machine-consumable).
|
|
46
|
+
/** The settled brand green ramp (256-color), bright → deep — the BANNER hues. */
|
|
47
|
+
export const BRAND_RAMP = [84, 78, 41, 35];
|
|
48
|
+
const visual = () => process.stdout.isTTY === true && process.env.NO_COLOR === undefined;
|
|
49
|
+
const sgr = (code) => (s) => visual() ? `\x1b[${code}m${s}${R}` : s;
|
|
50
|
+
/** Brand green (ramp anchor 41) — the tickmark hue; also the ok/pass/authed verdict color. */
|
|
51
|
+
export const brand = sgr(`38;5;${BRAND_RAMP[2]}`);
|
|
52
|
+
/** Ok verdicts (pass/authed/green) render in the brand green ramp — same hue as the tickmark. */
|
|
53
|
+
export const ok = brand;
|
|
54
|
+
/** Fail verdicts (unauthed/red) — red, always paired with the ✗ shape. */
|
|
55
|
+
export const fail = sgr("31");
|
|
56
|
+
/** Attention (warn/lint) — amber, always paired with the ! shape. */
|
|
57
|
+
export const warn = sgr("33");
|
|
58
|
+
/** Chrome (legends, rules, parentheticals, inactive state) — dim. */
|
|
59
|
+
export const dim = sgr("2");
|
|
60
|
+
/** Emphasis (titles, selection, the product name) — bold. */
|
|
61
|
+
export const bold = sgr("1");
|
|
62
|
+
/** Every semantic color token, for sweeps: each is TTY-gated and NO_COLOR-aware. */
|
|
63
|
+
export const TOKENS = { brand, ok, fail, warn, dim, bold };
|
|
64
|
+
/**
|
|
65
|
+
* The glyph vocabulary — plain characters only; color layers on via tokens so
|
|
66
|
+
* shape survives NO_COLOR. Bracket toggles ([x]/[ ]) are forbidden on every surface.
|
|
67
|
+
*/
|
|
68
|
+
export const GLYPHS = {
|
|
69
|
+
/** Cursor row pointer in list pickers. */
|
|
70
|
+
pointer: "❯",
|
|
71
|
+
/** Active toggle: THE brand tickmark — always rendered via brand(), never brackets. */
|
|
72
|
+
toggleActive: "✓",
|
|
73
|
+
/** Inactive toggle: dim circle — always rendered via dim(). */
|
|
74
|
+
toggleInactive: "○",
|
|
75
|
+
/** Pass verdict. */
|
|
76
|
+
pass: "✓",
|
|
77
|
+
/** Fail verdict. */
|
|
78
|
+
fail: "✗",
|
|
79
|
+
/** Attention/warn verdict. */
|
|
80
|
+
attention: "!",
|
|
81
|
+
/** Neutral/skip — the dash. */
|
|
82
|
+
neutral: "-",
|
|
83
|
+
};
|
|
84
|
+
/** The active toggle as rendered: brand green tickmark on a TTY, plain ✓ otherwise. */
|
|
85
|
+
export const toggleActive = () => brand(GLYPHS.toggleActive);
|
|
86
|
+
/** The inactive toggle as rendered: dim circle on a TTY, plain ○ otherwise. */
|
|
87
|
+
export const toggleInactive = () => dim(GLYPHS.toggleInactive);
|
|
88
|
+
/** Dominant title line — one glance answers "what am I looking at". Bold on a TTY. */
|
|
89
|
+
export const title = (text) => bold(text);
|
|
90
|
+
/** Single dim legend line under a title (key hints) — never scattered, never a paragraph. */
|
|
91
|
+
export const legend = (text) => dim(text);
|
|
92
|
+
/** Horizontal rule sized to the terminal (capped at 100 cols) — dim chrome. */
|
|
93
|
+
export const rule = (width = Math.min(process.stdout.columns ?? 80, 100)) => dim("─".repeat(width));
|
|
94
|
+
/** Aligned key-value row: dim key, plain value. Padding applied BEFORE styling so columns hold. */
|
|
95
|
+
export const kvRow = (key, value, keyWidth = 14) => ` ${dim(key.padEnd(keyWidth))} ${value}`;
|
|
96
|
+
const VERDICT = {
|
|
97
|
+
pass: () => ok(GLYPHS.pass),
|
|
98
|
+
fail: () => fail(GLYPHS.fail),
|
|
99
|
+
warn: () => warn(GLYPHS.attention),
|
|
100
|
+
neutral: () => dim(GLYPHS.neutral),
|
|
101
|
+
};
|
|
102
|
+
/** Status row: verdict glyph FIRST, then the label — glyph-first, distinct shape per verdict. */
|
|
103
|
+
export const statusRow = (verdict, label) => `${VERDICT[verdict]()} ${label}`;
|
|
@@ -1,36 +1,15 @@
|
|
|
1
1
|
import { writeFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import { allAdapters, detectCandidateClis,
|
|
4
|
-
import { BANNER } from "../../brand.js";
|
|
3
|
+
import { allAdapters, detectCandidateClis, probeAll, probeModels, readAutoPrefer, servableExclusions, servabilityLine, writeDoctor } from "../../adapters/registry.js";
|
|
4
|
+
import { BANNER, dim, fail, kvRow, legend, ok, rule, statusRow, title } from "../../brand.js";
|
|
5
5
|
import { tickmarkrDir, stateDirName } from "../../graph/graph.js";
|
|
6
6
|
import { declaredModelWindow, hasWindowsConfig, modelLints, suggestOverlay, ttyVisual } from "../../adapters/model-lints.js";
|
|
7
7
|
import { DEFAULT_CONFIG, loadConfig, overlayPreferShapes } from "../../config/config.js";
|
|
8
8
|
import { HerdrDriver } from "../../drivers/herdr.js";
|
|
9
9
|
import { disallowedBy, excludedChannels, exclusionLine, preferRanks } from "../../route/preference.js";
|
|
10
|
-
// same gate + palette as status's audit-ledger frame: styled only on a real TTY, plain otherwise
|
|
11
10
|
const visual = () => process.stdout.isTTY === true && process.env.NO_COLOR === undefined;
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
// n/a rows). Padding is untouched — every rewrite swaps equal-width text or wraps it in ANSI.
|
|
15
|
-
function stylize(out) {
|
|
16
|
-
if (!visual())
|
|
17
|
-
return out;
|
|
18
|
-
const rule = color("─".repeat(Math.min(process.stdout.columns ?? 80, 100)), 2);
|
|
19
|
-
return out.split("\n").map((line) => {
|
|
20
|
-
if (line.startsWith("tickmarkr doctor"))
|
|
21
|
-
return ` ${color("✓", 32)} ${color("tickmarkr", 1)} doctor ${color("· capability matrix", 2)}\n${rule}`;
|
|
22
|
-
if (/^(workspace trust|model status|model drift)/.test(line))
|
|
23
|
-
return color(line, 2);
|
|
24
|
-
return line
|
|
25
|
-
.replace(/^(\s+)✓ /, (_, s) => `${s}${color("✓", 32)} `)
|
|
26
|
-
.replace(/^(\s+)✗ /, (_, s) => `${s}${color("✗", 31)} `)
|
|
27
|
-
.replace(/^(\s+)! /, (_, s) => `${s}${color("!", 33)} `)
|
|
28
|
-
.replace(/^(\s+)= (.*)$/, (_, s, rest) => `${s}${color(`= ${rest}`, 2)}`)
|
|
29
|
-
.replace(/(\(auth assumed; verified at dispatch \(failover on auth\/quota errors\)\))/, (m) => color(m, 2))
|
|
30
|
-
.replace(/\bauthed\b(?!:)/, color("authed", 32))
|
|
31
|
-
.replace(/\bunauthed:/, color("unauthed:", 33));
|
|
32
|
-
}).join("\n");
|
|
33
|
-
}
|
|
11
|
+
const alignedStatusRow = (verdict, key, value) => ` ${statusRow(verdict, kvRow(key, value).slice(2))}`;
|
|
12
|
+
const attentionRow = (text) => ` ${statusRow("warn", text)}`;
|
|
34
13
|
export async function doctor(_argv, cwd = process.cwd(), adapters = allAdapters(), opts = {}) {
|
|
35
14
|
const cfg = loadConfig(cwd);
|
|
36
15
|
// banner at START — the logo greets the operator before the ~60s probe wait, never trailing it (operator report 2026-07-17)
|
|
@@ -63,48 +42,49 @@ export async function doctor(_argv, cwd = process.cwd(), adapters = allAdapters(
|
|
|
63
42
|
const rows = adapters.map((a) => {
|
|
64
43
|
const h = health[a.id];
|
|
65
44
|
const state = !h.installed ? "not installed" : `${h.version ?? "installed"}${h.note ? ` (${h.note})` : ""}`;
|
|
66
|
-
return
|
|
45
|
+
return alignedStatusRow(h.installed ? "pass" : "fail", a.id, state);
|
|
67
46
|
});
|
|
68
47
|
// v1.48 T1: advisory sweep for known agent CLIs with no adapter — never written to doctor.json health.
|
|
69
|
-
rows.push(...detectCandidateClis().map(
|
|
70
|
-
|
|
48
|
+
rows.push(...detectCandidateClis().map(({ binary, version }) => alignedStatusRow("warn", binary, `detected: ${version ?? "version unknown"} (no tickmarkr adapter — not routable)`)));
|
|
49
|
+
const herdr = HerdrDriver.available();
|
|
50
|
+
rows.push(alignedStatusRow(herdr ? "pass" : "fail", "herdr", herdr ? "driver available (HERDR_ENV=1)" : "not detected — subprocess driver will be used"));
|
|
71
51
|
// v1.22 T5: workspace-trust pre-flight — per installed adapter: trusted | seeded | action-required | n/a.
|
|
72
52
|
// action-required names the exact one-time command (or dialog) the operator must run once.
|
|
73
|
-
rows.push("workspace trust:");
|
|
53
|
+
rows.push(legend("workspace trust:"));
|
|
74
54
|
for (const a of adapters) {
|
|
75
55
|
if (!health[a.id]?.installed)
|
|
76
56
|
continue;
|
|
77
57
|
if (!a.trust) {
|
|
78
|
-
rows.push(` = ${a.id
|
|
58
|
+
rows.push(` ${dim("=")} ${kvRow(a.id, "trust: n/a").slice(2)}`);
|
|
79
59
|
continue;
|
|
80
60
|
}
|
|
81
61
|
try {
|
|
82
62
|
const v = a.trust(cwd);
|
|
83
63
|
if (v.status === "trusted")
|
|
84
|
-
rows.push(
|
|
64
|
+
rows.push(alignedStatusRow("pass", a.id, "trust: trusted"));
|
|
85
65
|
else if (v.status === "seeded")
|
|
86
|
-
rows.push(
|
|
66
|
+
rows.push(alignedStatusRow("pass", a.id, "trust: seeded"));
|
|
87
67
|
else
|
|
88
|
-
rows.push(
|
|
68
|
+
rows.push(alignedStatusRow("warn", a.id, `trust: action-required — run ONCE: ${v.command}`));
|
|
89
69
|
}
|
|
90
70
|
catch (e) {
|
|
91
71
|
// fail closed on the trust line only — never abort the rest of doctor
|
|
92
|
-
rows.push(
|
|
72
|
+
rows.push(alignedStatusRow("warn", a.id, `trust: action-required — run ONCE: (trust check failed: ${e instanceof Error ? e.message : String(e)})`));
|
|
93
73
|
}
|
|
94
74
|
}
|
|
95
75
|
for (const [role, sel] of [["judge", cfg.judge], ["consult", cfg.consult]]) {
|
|
96
76
|
if (!health[sel.adapter]?.installed) {
|
|
97
|
-
rows.push(
|
|
77
|
+
rows.push(attentionRow(`${role} runs on ${sel.adapter}:${sel.model} — NOT installed; that gate will fail closed until you install it or remap cfg.${role}`));
|
|
98
78
|
}
|
|
99
79
|
}
|
|
100
|
-
rows.push(...modelLints(cfg, health, adapters, { tty: ttyVisual(), stateDir: stateDirName(cwd), overlayPreferShapes: overlayPreferShapes(cwd) }).map(
|
|
80
|
+
rows.push(...modelLints(cfg, health, adapters, { tty: ttyVisual(), stateDir: stateDirName(cwd), overlayPreferShapes: overlayPreferShapes(cwd) }).map(attentionRow));
|
|
101
81
|
const excluded = excludedChannels(cfg, adapters, health);
|
|
102
82
|
if (excluded.length)
|
|
103
|
-
rows.push(
|
|
83
|
+
rows.push(attentionRow(exclusionLine(excluded)));
|
|
104
84
|
// HYG-07(a): doctor just probed fresh (probeAll above), so servability attribution is current by construction.
|
|
105
85
|
const servable = servableExclusions(cfg, adapters, health);
|
|
106
86
|
if (servable.length)
|
|
107
|
-
rows.push(
|
|
87
|
+
rows.push(attentionRow(servabilityLine(servable)));
|
|
108
88
|
// MODEL-05/06: print-only drift fragment; advisory, whole-line-commented additions, tickmarkr NEVER applies it.
|
|
109
89
|
// TTY gets a one-line summary + the fragment as a file (the full dump drowned everything else,
|
|
110
90
|
// v1.33.1 onboarding); machine/CI surface keeps the inline dump — layout is pinned by tests.
|
|
@@ -114,7 +94,7 @@ export async function doctor(_argv, cwd = process.cwd(), adapters = allAdapters(
|
|
|
114
94
|
if (visual()) {
|
|
115
95
|
const overlayPath = join(tickmarkrDir(cwd), "doctor-overlay.yaml");
|
|
116
96
|
writeFileSync(overlayPath, frag);
|
|
117
|
-
drift = `\
|
|
97
|
+
drift = `\n ${statusRow("warn", `model drift: unclassified models detected — paste-ready overlay written to ${overlayPath} ${dim("(advisory; tickmarkr never applies it)")}`)}`;
|
|
118
98
|
}
|
|
119
99
|
else {
|
|
120
100
|
drift = `\nmodel drift — paste-ready overlay (advisory; tickmarkr never applies):\n${frag}`;
|
|
@@ -137,20 +117,24 @@ export async function doctor(_argv, cwd = process.cwd(), adapters = allAdapters(
|
|
|
137
117
|
if (!models.length && !unclassified.length)
|
|
138
118
|
return [];
|
|
139
119
|
const w = Math.max(8, ...models.map((m) => m.length));
|
|
140
|
-
const rows = [` ${a.id}`];
|
|
120
|
+
const rows = [` ${dim(a.id)}`];
|
|
141
121
|
for (const m of models) {
|
|
142
122
|
const v = h.modelAuth?.[m];
|
|
143
|
-
const auth = !v
|
|
123
|
+
const auth = !v
|
|
124
|
+
? dim("unknown")
|
|
125
|
+
: v.authed
|
|
126
|
+
? ok("authed")
|
|
127
|
+
: `${fail("unauthed:")} ${trunc(v.reason ?? "probe failed", 40)} (${dateOf(v.probedAt)})`;
|
|
144
128
|
const d = disallowedBy({ adapter: a.id, model: m }, cfg.routing);
|
|
145
129
|
const denied = d?.by === "deny" ? d.entry : "—";
|
|
146
130
|
const pref = preferRanks({ adapter: a.id, model: m }, cfg).map((p) => `${p.shape}#${p.rank}`).join(",") || "—";
|
|
147
131
|
const windowCol = showWindows
|
|
148
132
|
? ` ${String(declaredModelWindow(cfg, a.id, m) ?? "—").padEnd(8)}`
|
|
149
133
|
: "";
|
|
150
|
-
rows.push(` ${m.padEnd(w)} ${classified[m].padEnd(8)}${windowCol} ${auth} denied
|
|
134
|
+
rows.push(` ${m.padEnd(w)} ${classified[m].padEnd(8)}${windowCol} ${auth} ${dim("denied=")}${denied} ${dim("prefer=")}${pref}`);
|
|
151
135
|
}
|
|
152
136
|
if (unclassified.length)
|
|
153
|
-
rows.push(` (${unclassified.length} more listed, unclassified)`);
|
|
137
|
+
rows.push(` ${dim(`(${unclassified.length} more listed, unclassified)`)}`);
|
|
154
138
|
return rows;
|
|
155
139
|
});
|
|
156
140
|
const autoPrefer = readAutoPrefer(cwd);
|
|
@@ -162,11 +146,14 @@ export async function doctor(_argv, cwd = process.cwd(), adapters = allAdapters(
|
|
|
162
146
|
const seed = DEFAULT_CONFIG.routing.map[shape]?.prefer ?? [];
|
|
163
147
|
if (!auto.length && !seed.length)
|
|
164
148
|
return []; // nothing derived, nothing seeded — an empty line is noise
|
|
165
|
-
return [` prefer ${shape} (auto)
|
|
149
|
+
return [` ${dim(`prefer ${shape} (auto):`)} ${auto.join(" > ")} ${dim("— seed was")} [${seed.join(", ")}]`];
|
|
166
150
|
})
|
|
167
151
|
: [];
|
|
168
152
|
const modelSummary = modelStatus.length || preferStatus.length
|
|
169
|
-
? `\
|
|
153
|
+
? `\n${legend("model status:")}\n${[...modelStatus, ...preferStatus].join("\n")}`
|
|
170
154
|
: "";
|
|
171
|
-
|
|
155
|
+
const header = visual()
|
|
156
|
+
? `${statusRow("pass", `${title("tickmarkr doctor")} ${legend("· capability matrix")}`)}\n${rule()}`
|
|
157
|
+
: "tickmarkr doctor — capability matrix:";
|
|
158
|
+
return `${header}\n${rows.join("\n")}${modelSummary}${drift}\nwrote ${stateDirName(cwd)}/doctor.json`;
|
|
172
159
|
}
|
|
@@ -5,15 +5,14 @@ import { createInterface } from "node:readline/promises";
|
|
|
5
5
|
import { parseArgs } from "node:util";
|
|
6
6
|
import { allAdapters, discoverChannels, doctorAgeMs, initDoctorReuse, readAutoPrefer } from "../../adapters/registry.js";
|
|
7
7
|
import { fleetUnclassifiedModels } from "../../adapters/model-lints.js";
|
|
8
|
-
import {
|
|
8
|
+
import { GLYPHS, bold, legend as legendText, toggleActive, toggleInactive } from "../../brand.js";
|
|
9
|
+
import { fleetEditableFromConfig, fleetEditableEquals, fleetRepoOverlayFromDelta, formatFleetPrint, globalConfigDir, overlayPreferShapes, readOverlayFile, repoOverlayPath, repoOverlayYaml, ROUTING_MODES, unifiedYamlDiff, } from "../../config/config.js";
|
|
9
10
|
import { SHAPES, TIERS } from "../../graph/schema.js";
|
|
10
11
|
import { route } from "../../route/router.js";
|
|
12
|
+
import { resolveRunMode } from "../../run/daemon.js";
|
|
11
13
|
import { loadRoutingProfile } from "../../run/journal.js";
|
|
12
14
|
const NON_TTY_MSG = "tickmarkr fleet: interactive fleet editor requires a TTY — use `tickmarkr fleet --print` for non-interactive output";
|
|
13
15
|
const QUIT = "fleet: quit without writing";
|
|
14
|
-
const BOLD = (s) => `\x1b[1m${s}\x1b[0m`;
|
|
15
|
-
const DIM = (s) => `\x1b[2m${s}\x1b[0m`;
|
|
16
|
-
const POINTER = "❯";
|
|
17
16
|
const isDeniedAdapter = (id, editable) => editable.denyAdapters.includes(id);
|
|
18
17
|
const isDeniedModel = (adapter, model, editable) => editable.denyModels.includes(`${adapter}:${model}`);
|
|
19
18
|
function previewTask(shape) {
|
|
@@ -57,10 +56,21 @@ function provenanceMap(editable) {
|
|
|
57
56
|
}
|
|
58
57
|
return out;
|
|
59
58
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
59
|
+
// v1.51 T4: serializeFleetOverlay predates routing.mode — splice the mode line under routing:
|
|
60
|
+
// so a repo-declared mode survives fleet writes and a mode selection lands as routing.mode.
|
|
61
|
+
function withModeLine(yaml, mode) {
|
|
62
|
+
if (!mode)
|
|
63
|
+
return yaml;
|
|
64
|
+
if (/^routing:$/m.test(yaml))
|
|
65
|
+
return yaml.replace(/^routing:$/m, `routing:\n mode: ${mode}`);
|
|
66
|
+
return `routing:\n mode: ${mode}\n${yaml}`;
|
|
63
67
|
}
|
|
68
|
+
// v1.51 T4: one gloss per routing mode on the fleet mode screen — mirrors the preset compiler.
|
|
69
|
+
const MODE_GLOSS = {
|
|
70
|
+
"partner-led": "every shape frontier · explore off",
|
|
71
|
+
"risk-based": "risk-tiered default floors",
|
|
72
|
+
"staff-led": "implement/refactor one band down · integrity shapes hold frontier",
|
|
73
|
+
};
|
|
64
74
|
const isAbort = (k) => k.name === "escape" || k.name === "q" || (k.ctrl === true && k.name === "c");
|
|
65
75
|
const isNext = (k) => k.name === "return" || k.name === "enter";
|
|
66
76
|
const isToggle = (k) => k.name === "space";
|
|
@@ -75,8 +85,8 @@ function moveCursor(k, cursor, count) {
|
|
|
75
85
|
}
|
|
76
86
|
// step title (bold) + one dim key legend + rows; exactly one pointer glyph on the cursor row
|
|
77
87
|
function listFrame(title, legend, rows, cursor) {
|
|
78
|
-
const lines = [
|
|
79
|
-
rows.forEach((r, i) => lines.push(i === cursor ? `${
|
|
88
|
+
const lines = [bold(title), legendText(legend)];
|
|
89
|
+
rows.forEach((r, i) => lines.push(i === cursor ? `${GLYPHS.pointer} ${bold(r)}` : ` ${r}`));
|
|
80
90
|
return lines;
|
|
81
91
|
}
|
|
82
92
|
// Keypress terminal over the (injectable) input stream. Keypress events are decoded by
|
|
@@ -156,8 +166,14 @@ export async function fleet(argv, cwd = process.cwd(), adapters = allAdapters(),
|
|
|
156
166
|
const input = io.input ?? process.stdin;
|
|
157
167
|
const output = io.output ?? process.stdout;
|
|
158
168
|
const interactive = input.isTTY === true && output.isTTY === true;
|
|
159
|
-
if (print)
|
|
160
|
-
|
|
169
|
+
if (print) {
|
|
170
|
+
// v1.51 T4: the print surface names the mode and its source layer right under the header —
|
|
171
|
+
// comment-prefixed so the YAML body stays machine-parseable and regex-stable.
|
|
172
|
+
const rm = resolveRunMode(cwd, { globalDir });
|
|
173
|
+
const body = formatFleetPrint(cwd, { globalDir });
|
|
174
|
+
const nl = body.indexOf("\n");
|
|
175
|
+
return `${body.slice(0, nl)}\n# mode: ${rm.mode.mode} (${rm.source})${body.slice(nl)}`;
|
|
176
|
+
}
|
|
161
177
|
if (!interactive)
|
|
162
178
|
return { out: NON_TTY_MSG, code: 1 };
|
|
163
179
|
const fresh = values.fresh ?? false;
|
|
@@ -168,15 +184,16 @@ export async function fleet(argv, cwd = process.cwd(), adapters = allAdapters(),
|
|
|
168
184
|
code: 1,
|
|
169
185
|
};
|
|
170
186
|
}
|
|
171
|
-
const
|
|
187
|
+
const rm = resolveRunMode(cwd, { globalDir });
|
|
188
|
+
const cfg = rm.cfg;
|
|
172
189
|
const initial = fleetEditableFromConfig(cfg);
|
|
173
190
|
const editable = structuredClone(initial);
|
|
174
191
|
const health = cached;
|
|
175
192
|
const term = openTerm(input, output);
|
|
176
193
|
try {
|
|
177
|
-
// step 1/
|
|
194
|
+
// step 1/5 — probe data
|
|
178
195
|
const ageLine = formatAge(doctorAgeMs(cwd));
|
|
179
|
-
term.frame(listFrame("step 1/
|
|
196
|
+
term.frame(listFrame("step 1/5 · probe data", "enter continue · r refresh via doctor · esc/q quit", [`probe data: ${ageLine} (.tickmarkr/doctor.json)`], 0));
|
|
180
197
|
for (;;) {
|
|
181
198
|
const k = await term.key();
|
|
182
199
|
if (isAbort(k))
|
|
@@ -187,16 +204,16 @@ export async function fleet(argv, cwd = process.cwd(), adapters = allAdapters(),
|
|
|
187
204
|
if (isNext(k))
|
|
188
205
|
break;
|
|
189
206
|
}
|
|
190
|
-
// step 2/
|
|
207
|
+
// step 2/5 — agent CLIs (space toggles adapter deny)
|
|
191
208
|
const installed = adapters.filter((a) => health[a.id]?.installed);
|
|
192
209
|
const adapterRows = () => installed.map((a) => {
|
|
193
210
|
const h = health[a.id];
|
|
194
211
|
const on = !isDeniedAdapter(a.id, editable);
|
|
195
|
-
return
|
|
212
|
+
return `${on ? toggleActive() : toggleInactive()} ${a.id} ${h?.version ?? "installed"} ${h?.authed ? "authed" : "unauthed"}`;
|
|
196
213
|
});
|
|
197
214
|
const listLegend = "↑↓/jk move · space toggle · enter next · esc/q quit";
|
|
198
215
|
let aCursor = 0;
|
|
199
|
-
term.frame(listFrame("step 2/
|
|
216
|
+
term.frame(listFrame("step 2/5 · agent CLIs", listLegend, adapterRows(), aCursor));
|
|
200
217
|
for (;;) {
|
|
201
218
|
const k = await term.key();
|
|
202
219
|
if (isAbort(k))
|
|
@@ -220,9 +237,9 @@ export async function fleet(argv, cwd = process.cwd(), adapters = allAdapters(),
|
|
|
220
237
|
changed = true;
|
|
221
238
|
}
|
|
222
239
|
if (changed)
|
|
223
|
-
term.frame(listFrame("step 2/
|
|
240
|
+
term.frame(listFrame("step 2/5 · agent CLIs", listLegend, adapterRows(), aCursor));
|
|
224
241
|
}
|
|
225
|
-
// step 3/
|
|
242
|
+
// step 3/5 — models per enabled adapter (space toggles deny, t assigns tier from the row)
|
|
226
243
|
const enabledAdapters = installed.filter((a) => !isDeniedAdapter(a.id, editable));
|
|
227
244
|
for (const a of enabledAdapters) {
|
|
228
245
|
const unclassifiedAll = fleetUnclassifiedModels(cfg, health, adapters).filter((u) => u.adapter === a.id);
|
|
@@ -238,9 +255,9 @@ export async function fleet(argv, cwd = process.cwd(), adapters = allAdapters(),
|
|
|
238
255
|
}
|
|
239
256
|
const tier = editable.tiers[a.id]?.[r.model];
|
|
240
257
|
const denied = isDeniedModel(a.id, r.model, editable);
|
|
241
|
-
return
|
|
258
|
+
return `${denied ? toggleInactive() : toggleActive()} ${r.model} ${tier?.tier ?? "???"} ${denied ? "denied" : "allowed"}`;
|
|
242
259
|
});
|
|
243
|
-
const title = `step 3/
|
|
260
|
+
const title = `step 3/5 · models · ${a.id}`;
|
|
244
261
|
const modelsLegend = "↑↓/jk move · space toggle · t tier · enter next · esc/q quit";
|
|
245
262
|
let mCursor = 0;
|
|
246
263
|
term.frame(listFrame(title, modelsLegend, renderRows(rowsData()), mCursor));
|
|
@@ -285,7 +302,44 @@ export async function fleet(argv, cwd = process.cwd(), adapters = allAdapters(),
|
|
|
285
302
|
term.frame(listFrame(title, modelsLegend, renderRows(rowsData()), mCursor));
|
|
286
303
|
}
|
|
287
304
|
}
|
|
288
|
-
// step 4/
|
|
305
|
+
// step 4/5 — routing mode (selection is in-memory; the write happens only through the diff confirm).
|
|
306
|
+
// Candidate floor tables come from the ONE preset compiler via resolveRunMode — no mode math here.
|
|
307
|
+
const modeCfgs = Object.fromEntries(ROUTING_MODES.map((m) => [m, m === rm.mode.mode ? rm : resolveRunMode(cwd, { flag: m, globalDir })]));
|
|
308
|
+
const modeRows = () => ROUTING_MODES.map((m) => `${m === rm.mode.mode ? toggleActive() : " "} ${m.padEnd(11)} ${MODE_GLOSS[m]}`);
|
|
309
|
+
// preview: the highlighted mode's resolved floor table diffed against the current mode
|
|
310
|
+
const floorPreview = (cand) => {
|
|
311
|
+
if (cand === rm.mode.mode)
|
|
312
|
+
return [];
|
|
313
|
+
const cur = cfg.routing.floors;
|
|
314
|
+
const next = modeCfgs[cand].cfg.routing.floors;
|
|
315
|
+
const changed = SHAPES.filter((s) => cur[s] !== next[s]);
|
|
316
|
+
return [
|
|
317
|
+
` floors vs ${rm.mode.mode}:`,
|
|
318
|
+
...(changed.length ? changed.map((s) => ` ${s}: ${cur[s]} → ${next[s]}`) : [" (no floor changes)"]),
|
|
319
|
+
];
|
|
320
|
+
};
|
|
321
|
+
const modeTitle = "step 4/5 · routing mode";
|
|
322
|
+
const modeLegend = "↑↓/jk move · enter select · esc/q quit";
|
|
323
|
+
let modeCursor = ROUTING_MODES.indexOf(rm.mode.mode);
|
|
324
|
+
let selectedMode = rm.mode.mode;
|
|
325
|
+
const modeFrame = () => [...listFrame(modeTitle, modeLegend, modeRows(), modeCursor), ...floorPreview(ROUTING_MODES[modeCursor])];
|
|
326
|
+
term.frame(modeFrame());
|
|
327
|
+
for (;;) {
|
|
328
|
+
const k = await term.key();
|
|
329
|
+
if (isAbort(k))
|
|
330
|
+
return QUIT;
|
|
331
|
+
if (isNext(k)) {
|
|
332
|
+
selectedMode = ROUTING_MODES[modeCursor];
|
|
333
|
+
break;
|
|
334
|
+
}
|
|
335
|
+
const moved = moveCursor(k, modeCursor, ROUTING_MODES.length);
|
|
336
|
+
if (moved !== modeCursor) {
|
|
337
|
+
modeCursor = moved;
|
|
338
|
+
term.frame(modeFrame());
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
// step 5/5 — shape routing (typed entry for pin/tier/prefer from the highlighted row);
|
|
342
|
+
// previews route under the SELECTED mode's resolved floors, never a floor edit of its own
|
|
289
343
|
const channels = discoverChannels(cfg, adapters, health);
|
|
290
344
|
const profile = loadRoutingProfile(cwd, cfg, { preview: true });
|
|
291
345
|
const autoPrefer = readAutoPrefer(cwd);
|
|
@@ -293,7 +347,7 @@ export async function fleet(argv, cwd = process.cwd(), adapters = allAdapters(),
|
|
|
293
347
|
const shapeRows = () => SHAPES.map((shape) => {
|
|
294
348
|
let now;
|
|
295
349
|
try {
|
|
296
|
-
const r = route(previewTask(shape), { ...cfg, routing: { ...cfg.routing, map: editable.map, floors:
|
|
350
|
+
const r = route(previewTask(shape), { ...cfg, routing: { ...cfg.routing, map: editable.map, floors: modeCfgs[selectedMode].cfg.routing.floors } }, channels, profile);
|
|
297
351
|
now = `${r.assignment.adapter}:${r.assignment.model} (${r.assignment.channel}, ${r.assignment.tier})`;
|
|
298
352
|
}
|
|
299
353
|
catch (e) {
|
|
@@ -302,7 +356,7 @@ export async function fleet(argv, cwd = process.cwd(), adapters = allAdapters(),
|
|
|
302
356
|
const auto = autoPrefer?.[shape] && !overlayShapes.has(shape) ? " (auto-prefer active)" : "";
|
|
303
357
|
return `${shape} → ${now}${auto}`;
|
|
304
358
|
});
|
|
305
|
-
const shapesTitle = "step
|
|
359
|
+
const shapesTitle = "step 5/5 · shape routing";
|
|
306
360
|
const shapesLegend = "↑↓/jk move · a auto · p pin · t tier · f prefer · enter next · esc/q quit";
|
|
307
361
|
let sCursor = 0;
|
|
308
362
|
term.frame(listFrame(shapesTitle, shapesLegend, shapeRows(), sCursor));
|
|
@@ -351,10 +405,17 @@ export async function fleet(argv, cwd = process.cwd(), adapters = allAdapters(),
|
|
|
351
405
|
if (changed)
|
|
352
406
|
term.frame(listFrame(shapesTitle, shapesLegend, shapeRows(), sCursor));
|
|
353
407
|
}
|
|
354
|
-
// review — unified diff + typed confirm
|
|
408
|
+
// review — unified diff + typed confirm (the ONLY write path; the mode screen funnels through here)
|
|
355
409
|
const before = currentRepoOverlayText(cwd);
|
|
356
|
-
const
|
|
357
|
-
|
|
410
|
+
const existing = readOverlayFile(repoOverlayPath(cwd));
|
|
411
|
+
const modeChanged = selectedMode !== rm.mode.mode;
|
|
412
|
+
// preserve a repo-declared mode line verbatim; write a new one only when the selection changed it
|
|
413
|
+
const writeMode = modeChanged ? selectedMode : existing.routing?.mode;
|
|
414
|
+
const merged = fleetEditableEquals(initial, editable)
|
|
415
|
+
? structuredClone(existing)
|
|
416
|
+
: fleetRepoOverlayFromDelta(initial, editable, existing);
|
|
417
|
+
const after = withModeLine(repoOverlayYaml(merged, provenanceMap(editable)), writeMode);
|
|
418
|
+
if ((!modeChanged && fleetEditableEquals(initial, editable)) || before === after) {
|
|
358
419
|
return "fleet: no overlay changes (empty diff)";
|
|
359
420
|
}
|
|
360
421
|
const diff = unifiedYamlDiff(before, after, repoOverlayPath(cwd));
|
|
@@ -362,11 +423,9 @@ export async function fleet(argv, cwd = process.cwd(), adapters = allAdapters(),
|
|
|
362
423
|
const confirm = await term.askTyped("write overlay? [y/N] ");
|
|
363
424
|
if (!/^y(?:es)?$/i.test(confirm))
|
|
364
425
|
return "fleet: discarded overlay changes";
|
|
365
|
-
const merged = fleetRepoOverlayFromDelta(initial, editable, readOverlayFile(repoOverlayPath(cwd)));
|
|
366
|
-
const body = repoOverlayYaml(merged, provenanceMap(editable));
|
|
367
426
|
const path = repoOverlayPath(cwd);
|
|
368
427
|
mkdirSync(dirname(path), { recursive: true });
|
|
369
|
-
writeFileSync(path,
|
|
428
|
+
writeFileSync(path, after);
|
|
370
429
|
return `fleet: wrote ${path}`;
|
|
371
430
|
}
|
|
372
431
|
finally {
|
|
@@ -5,7 +5,7 @@ import { parseArgs } from "node:util";
|
|
|
5
5
|
import { allAdapters, formatDoctorAgeForInit, formatDoctorReport, initDoctorReuse } from "../../adapters/registry.js";
|
|
6
6
|
import { configTemplate, DEFAULT_CONFIG, globalConfigDir, loadConfig } from "../../config/config.js";
|
|
7
7
|
import { LEGACY_PREFIX, specTemplate } from "../../compile/native.js";
|
|
8
|
-
import { BANNER } from "../../brand.js";
|
|
8
|
+
import { BANNER, kvRow, legend, rule, statusRow, title } from "../../brand.js";
|
|
9
9
|
import { tickmarkrDir } from "../../graph/graph.js";
|
|
10
10
|
import { Journal } from "../../run/journal.js";
|
|
11
11
|
import { doctor } from "./doctor.js";
|
|
@@ -264,5 +264,25 @@ export async function init(argv, cwd = process.cwd()) {
|
|
|
264
264
|
}
|
|
265
265
|
if (values.agent)
|
|
266
266
|
await installAgentFiles(cwd, values.force ?? false, values.docs ?? false, notes);
|
|
267
|
-
|
|
267
|
+
const next = nextSteps(cwd, SCAFFOLD_SPEC);
|
|
268
|
+
if (!visual())
|
|
269
|
+
return `${notes.join("\n")}\n${doc}\n${next}\n${ENVIRONMENTS_FOOTER}`;
|
|
270
|
+
const noteRows = notes.map((note) => ` ${statusRow(/^(?:wrote|overwrote|appended)/.test(note) ? "pass" : note.startsWith("skipped") ? "warn" : "neutral", note)}`);
|
|
271
|
+
const footerRows = ENVIRONMENTS_FOOTER.split("\n").slice(1).map((line) => {
|
|
272
|
+
const separator = line.indexOf(" — ");
|
|
273
|
+
return kvRow(line.slice(2, separator), line.slice(separator + 1), 12);
|
|
274
|
+
});
|
|
275
|
+
return [
|
|
276
|
+
title("tickmarkr init"),
|
|
277
|
+
legend("· setup notes"),
|
|
278
|
+
rule(),
|
|
279
|
+
...noteRows,
|
|
280
|
+
doc,
|
|
281
|
+
title("next steps"),
|
|
282
|
+
legend("· continue from the repository's current state"),
|
|
283
|
+
rule(),
|
|
284
|
+
kvRow("next", next.replace(/^next:\s*/, "")),
|
|
285
|
+
legend("environments:"),
|
|
286
|
+
...footerRows,
|
|
287
|
+
].join("\n");
|
|
268
288
|
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import type { WorkerAdapter } from "../../adapters/types.js";
|
|
2
|
-
export declare function plan(
|
|
2
|
+
export declare function plan(argv: string[], cwd?: string, adapters?: WorkerAdapter[]): Promise<string>;
|