@relay-baton/cli 1.0.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.
Files changed (82) hide show
  1. package/LICENSE +21 -0
  2. package/dist/commands/agentFor.d.ts +8 -0
  3. package/dist/commands/agentFor.js +28 -0
  4. package/dist/commands/auditApiKeyEnv.d.ts +6 -0
  5. package/dist/commands/auditApiKeyEnv.js +27 -0
  6. package/dist/commands/budget.d.ts +5 -0
  7. package/dist/commands/budget.js +86 -0
  8. package/dist/commands/chat.d.ts +5 -0
  9. package/dist/commands/chat.js +218 -0
  10. package/dist/commands/checkpoint.d.ts +10 -0
  11. package/dist/commands/checkpoint.js +78 -0
  12. package/dist/commands/compact.d.ts +4 -0
  13. package/dist/commands/compact.js +22 -0
  14. package/dist/commands/compress.d.ts +4 -0
  15. package/dist/commands/compress.js +61 -0
  16. package/dist/commands/compressContext.d.ts +8 -0
  17. package/dist/commands/compressContext.js +51 -0
  18. package/dist/commands/conversation.d.ts +8 -0
  19. package/dist/commands/conversation.js +90 -0
  20. package/dist/commands/diagnostics.d.ts +23 -0
  21. package/dist/commands/diagnostics.js +254 -0
  22. package/dist/commands/doctor.d.ts +5 -0
  23. package/dist/commands/doctor.js +104 -0
  24. package/dist/commands/execute.d.ts +9 -0
  25. package/dist/commands/execute.js +183 -0
  26. package/dist/commands/git.d.ts +6 -0
  27. package/dist/commands/git.js +82 -0
  28. package/dist/commands/guard.d.ts +7 -0
  29. package/dist/commands/guard.js +30 -0
  30. package/dist/commands/handoff.d.ts +10 -0
  31. package/dist/commands/handoff.js +133 -0
  32. package/dist/commands/handoffBundle.d.ts +12 -0
  33. package/dist/commands/handoffBundle.js +64 -0
  34. package/dist/commands/handoffHistory.d.ts +23 -0
  35. package/dist/commands/handoffHistory.js +129 -0
  36. package/dist/commands/handoffShow.d.ts +12 -0
  37. package/dist/commands/handoffShow.js +73 -0
  38. package/dist/commands/init.d.ts +2 -0
  39. package/dist/commands/init.js +19 -0
  40. package/dist/commands/inventory.d.ts +5 -0
  41. package/dist/commands/inventory.js +23 -0
  42. package/dist/commands/login.d.ts +3 -0
  43. package/dist/commands/login.js +80 -0
  44. package/dist/commands/migrate.d.ts +8 -0
  45. package/dist/commands/migrate.js +55 -0
  46. package/dist/commands/plan.d.ts +13 -0
  47. package/dist/commands/plan.js +159 -0
  48. package/dist/commands/profile.d.ts +5 -0
  49. package/dist/commands/profile.js +23 -0
  50. package/dist/commands/project.d.ts +18 -0
  51. package/dist/commands/project.js +173 -0
  52. package/dist/commands/projectOptions.d.ts +7 -0
  53. package/dist/commands/projectOptions.js +21 -0
  54. package/dist/commands/receipt.d.ts +8 -0
  55. package/dist/commands/receipt.js +48 -0
  56. package/dist/commands/replay.d.ts +8 -0
  57. package/dist/commands/replay.js +35 -0
  58. package/dist/commands/report.d.ts +6 -0
  59. package/dist/commands/report.js +54 -0
  60. package/dist/commands/review.d.ts +8 -0
  61. package/dist/commands/review.js +63 -0
  62. package/dist/commands/risk.d.ts +5 -0
  63. package/dist/commands/risk.js +25 -0
  64. package/dist/commands/run.d.ts +31 -0
  65. package/dist/commands/run.js +323 -0
  66. package/dist/commands/session.d.ts +40 -0
  67. package/dist/commands/session.js +158 -0
  68. package/dist/commands/sessionWorkspace.d.ts +25 -0
  69. package/dist/commands/sessionWorkspace.js +193 -0
  70. package/dist/commands/status.d.ts +5 -0
  71. package/dist/commands/status.js +116 -0
  72. package/dist/commands/tui.d.ts +4 -0
  73. package/dist/commands/tui.js +46 -0
  74. package/dist/commands/usage.d.ts +11 -0
  75. package/dist/commands/usage.js +40 -0
  76. package/dist/commands/verify.d.ts +15 -0
  77. package/dist/commands/verify.js +197 -0
  78. package/dist/commands/workspace.d.ts +5 -0
  79. package/dist/commands/workspace.js +27 -0
  80. package/dist/index.d.ts +2 -0
  81. package/dist/index.js +394 -0
  82. package/package.json +57 -0
@@ -0,0 +1,183 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.executeCommand = executeCommand;
37
+ const fs = __importStar(require("fs"));
38
+ const core_1 = require("@relay-baton/core");
39
+ const projectOptions_1 = require("./projectOptions");
40
+ const agentFor_1 = require("./agentFor");
41
+ async function executeCommand(opts) {
42
+ const projectContext = (0, projectOptions_1.resolveProjectContext)(opts, true);
43
+ const repoRoot = projectContext.repoRoot;
44
+ const { config } = core_1.ConfigLoader.load(repoRoot);
45
+ const sm = new core_1.SessionManager(repoRoot, config);
46
+ if (!sm.getMeta())
47
+ sm.init("");
48
+ const git = new core_1.GitService(repoRoot);
49
+ if (!git.isGitRepo()) {
50
+ console.error("[relay-baton] not a git repository. aborting.");
51
+ process.exit(2);
52
+ }
53
+ const meta = sm.getMeta();
54
+ const profileName = (opts.diet ?? meta.tokenDietProfile ?? projectContext.project?.defaultDiet ?? config.tokenDiet.profile);
55
+ if (!config.tokenDiet.profiles[profileName]) {
56
+ console.error(`unknown diet profile: ${profileName}`);
57
+ process.exit(2);
58
+ }
59
+ // A plan must exist.
60
+ const planPath = opts.from ?? sm.files.p("plan");
61
+ if (!fs.existsSync(planPath)) {
62
+ console.error(`[relay-baton] no plan found at ${planPath}. Run 'relay-baton plan "<task>"' first.`);
63
+ process.exit(2);
64
+ }
65
+ // If reading from a custom path, copy it into the session so the gate + agent see it.
66
+ if (opts.from && opts.from !== sm.files.p("plan")) {
67
+ fs.copyFileSync(opts.from, sm.files.p("plan"));
68
+ }
69
+ // Gate the plan before executing.
70
+ const planGate = new core_1.PlanQualityGate(repoRoot, config.tokenDiet.profiles[profileName]).check();
71
+ if (!planGate.ok) {
72
+ console.error("[relay-baton] Plan Quality Gate failed:");
73
+ for (const f of planGate.failures)
74
+ console.error(" - " + f);
75
+ if (!opts.force) {
76
+ console.error("[relay-baton] aborting. Fix plan.md or use --force.");
77
+ process.exit(3);
78
+ }
79
+ }
80
+ const executor = (opts.with ?? config.planExecute?.defaultExecutor ?? "codex");
81
+ const startedAt = new Date().toISOString();
82
+ sm.updateMeta({
83
+ workflowMode: "plan-execute",
84
+ executor,
85
+ status: "executing",
86
+ activeAgent: executor,
87
+ executeStartedAt: startedAt,
88
+ startedAt,
89
+ endedAt: undefined,
90
+ durationMs: undefined,
91
+ fallbackReason: null,
92
+ });
93
+ const adapter = (0, agentFor_1.adapterFor)(executor, config);
94
+ const prompt = core_1.PromptBuilder.executor();
95
+ const cmd = adapter.buildCommand({ task: prompt, prompt, repoRoot, sessionDir: sm.files.dir, dietProfile: profileName });
96
+ const detector = new core_1.FallbackDetector(config.fallbackPatterns);
97
+ console.log(`[relay-baton] executing plan with ${cmd.command} (${executor}) ...`);
98
+ const r = await (0, core_1.runAgent)({
99
+ command: cmd,
100
+ logFile: sm.files.p("commandsLog"),
101
+ authPolicy: config.authPolicy,
102
+ allowApiKeyEnv: opts.allowApiKeyEnv,
103
+ fallbackDetector: detector,
104
+ onStdout: l => process.stdout.write(l + "\n"),
105
+ onStderr: l => process.stderr.write(l + "\n"),
106
+ onFallback: hit => console.error(`[relay-baton] fallback pattern detected: ${hit.pattern}`),
107
+ });
108
+ const finish = (status, lastError, lastAgent) => {
109
+ const endedAt = new Date().toISOString();
110
+ sm.updateMeta({
111
+ status, lastError, lastAgent, activeAgent: "none",
112
+ endedAt, durationMs: Date.parse(endedAt) - Date.parse(startedAt),
113
+ });
114
+ };
115
+ if (r.error) {
116
+ console.error(r.error);
117
+ finish("failed", r.error, executor);
118
+ process.exit(1);
119
+ }
120
+ const shouldFallback = r.fallbackReason !== null;
121
+ if (!shouldFallback) {
122
+ finish(r.exitCode === 0 ? "completed" : "failed", r.exitCode === 0 ? null : `${executor} exited with ${r.exitCode}`, executor);
123
+ console.log("[relay-baton] execute finished without fallback.");
124
+ return;
125
+ }
126
+ // In-execute fallback: build a handoff and hand to the fallback agent,
127
+ // reusing the v0.4 quality gates (composition with fallback mode).
128
+ console.log("[relay-baton] building handoff for fallback agent...");
129
+ sm.updateMeta({ status: "fallback_detected", fallbackReason: r.fallbackReason, lastAgent: executor, activeAgent: "none" });
130
+ const wf = new core_1.BatonWorkflow(sm, config);
131
+ const h = wf.buildHandoff({
132
+ profileName,
133
+ fallbackReason: r.fallbackReason,
134
+ previousAgent: executor,
135
+ nextAgent: config.fallbackAgent,
136
+ });
137
+ const prevCount = sm.getMeta()?.handoffCount ?? 0;
138
+ sm.updateMeta({ handoffCount: prevCount + 1 });
139
+ const gate = new core_1.HandoffQualityGate(repoRoot).check();
140
+ const dietGate = new core_1.TokenDietQualityGate(repoRoot, profileName, config.tokenDiet.profiles[profileName])
141
+ .check({ wasTruncated: h.truncated });
142
+ let blocked = false;
143
+ if (!gate.ok) {
144
+ console.error("Handoff Quality Gate failed:");
145
+ for (const f of gate.failures)
146
+ console.error(" - " + f);
147
+ blocked = true;
148
+ }
149
+ if (!dietGate.ok) {
150
+ console.error("Token Diet Quality Gate failed:");
151
+ for (const f of dietGate.failures)
152
+ console.error(" - " + f);
153
+ blocked = true;
154
+ }
155
+ for (const w of dietGate.warnings)
156
+ console.error("warn: " + w);
157
+ if (blocked && !opts.force) {
158
+ console.error("[relay-baton] aborting fallback launch. Use --force to override.");
159
+ process.exit(3);
160
+ }
161
+ const fb = config.fallbackAgent;
162
+ sm.updateMeta({ status: "running_fallback", activeAgent: fb });
163
+ const fbAdapter = (0, agentFor_1.adapterFor)(fb, config);
164
+ const fbCmd = fbAdapter.buildCommand({
165
+ task: core_1.PromptBuilder.claudeContinuation(),
166
+ prompt: core_1.PromptBuilder.claudeContinuation(),
167
+ repoRoot, sessionDir: sm.files.dir,
168
+ });
169
+ const r2 = await (0, core_1.runAgent)({
170
+ command: fbCmd,
171
+ logFile: sm.files.p("commandsLog"),
172
+ authPolicy: config.authPolicy,
173
+ allowApiKeyEnv: opts.allowApiKeyEnv,
174
+ onStdout: l => process.stdout.write(l + "\n"),
175
+ onStderr: l => process.stderr.write(l + "\n"),
176
+ });
177
+ if (r2.error) {
178
+ console.error(r2.error);
179
+ finish("failed", r2.error, fb);
180
+ process.exit(1);
181
+ }
182
+ finish(r2.exitCode === 0 ? "completed" : "failed", r2.exitCode === 0 ? null : `${fb} exited with ${r2.exitCode}`, fb);
183
+ }
@@ -0,0 +1,6 @@
1
+ import { ProjectOpts } from "./projectOptions";
2
+ export interface GitStatusOpts extends ProjectOpts {
3
+ json?: boolean;
4
+ limit?: string;
5
+ }
6
+ export declare function gitStatusCommand(opts?: GitStatusOpts): Promise<void>;
@@ -0,0 +1,82 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.gitStatusCommand = gitStatusCommand;
37
+ const fs = __importStar(require("fs"));
38
+ const core_1 = require("@relay-baton/core");
39
+ const projectOptions_1 = require("./projectOptions");
40
+ async function gitStatusCommand(opts = {}) {
41
+ const repoRoot = (0, projectOptions_1.resolveRepoRoot)(opts);
42
+ const limit = Number.parseInt(opts.limit || "40", 10);
43
+ const git = new core_1.GitService(repoRoot);
44
+ const summary = git.summary(Number.isFinite(limit) ? limit : 40);
45
+ const tracking = git.compareBaseline(readBaseline(repoRoot));
46
+ if (opts.json) {
47
+ console.log(JSON.stringify({ repoRoot, ...summary, tracking }, null, 2));
48
+ return;
49
+ }
50
+ if (!summary.available) {
51
+ console.log("[relay-baton] git unavailable: current project is not a git repository.");
52
+ return;
53
+ }
54
+ console.log("branch:", summary.branch ?? "(detached)");
55
+ console.log("head:", summary.head ? summary.head.slice(0, 12) : "(none)");
56
+ console.log("upstream:", summary.upstream ?? "(none)");
57
+ console.log("ahead/behind:", `${summary.ahead}/${summary.behind}`);
58
+ console.log("clean:", summary.clean ? "yes" : "no");
59
+ console.log("changed:", summary.changed);
60
+ console.log("staged:", summary.staged);
61
+ console.log("unstaged:", summary.unstaged);
62
+ console.log("untracked:", summary.untracked);
63
+ if (tracking.baseline) {
64
+ console.log("changed since session start:", tracking.changedSinceBaseline ? "yes" : "no");
65
+ console.log("changed delta:", tracking.changedDelta ?? 0);
66
+ }
67
+ else {
68
+ console.log("session baseline:", "(none)");
69
+ }
70
+ for (const file of summary.files) {
71
+ console.log(`${file.code} ${file.path} (${file.label})`);
72
+ }
73
+ }
74
+ function readBaseline(repoRoot) {
75
+ try {
76
+ const raw = fs.readFileSync(new core_1.SessionFiles(repoRoot).p("gitBaseline"), "utf8");
77
+ return JSON.parse(raw);
78
+ }
79
+ catch {
80
+ return null;
81
+ }
82
+ }
@@ -0,0 +1,7 @@
1
+ import { ProjectOpts } from "./projectOptions";
2
+ export interface GuardOpts extends ProjectOpts {
3
+ json?: boolean;
4
+ /** Exit non-zero (10) when a stop condition is triggered. */
5
+ exitCode?: boolean;
6
+ }
7
+ export declare function guardCommand(opts?: GuardOpts): Promise<void>;
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.guardCommand = guardCommand;
4
+ const core_1 = require("@relay-baton/core");
5
+ const projectOptions_1 = require("./projectOptions");
6
+ async function guardCommand(opts = {}) {
7
+ const repoRoot = (0, projectOptions_1.resolveRepoRoot)(opts);
8
+ const { config } = core_1.ConfigLoader.load(repoRoot);
9
+ const report = new core_1.GuardrailPolicy(repoRoot, config).evaluate();
10
+ if (opts.json) {
11
+ console.log(JSON.stringify(report, null, 2));
12
+ }
13
+ else {
14
+ const verdict = report.blocked ? "BLOCKED" : "ok";
15
+ console.log(`[relay-baton] guardrails: ${verdict}`);
16
+ const { actual, limits } = report;
17
+ console.log(`steps: ${actual.steps}/${limits.maxSteps ?? "∞"}`);
18
+ console.log(`changed files: ${actual.changedFiles}/${limits.maxChangedFiles ?? "∞"}`);
19
+ console.log(`budget ratio: ${actual.budgetRatio == null ? "-" : actual.budgetRatio.toFixed(2)}/${limits.maxBudgetRatio ?? "∞"}`);
20
+ if (report.requireConfirmation)
21
+ console.log("mutating steps require explicit human confirmation");
22
+ for (const v of report.violations)
23
+ console.log(`- ${v.condition}: ${v.message}`);
24
+ }
25
+ // Advisory by default (read-only report). With --exit-code, a triggered stop
26
+ // condition sets a non-zero exit so a script/agent loop can halt — relay-baton
27
+ // still never halts an agent on its own.
28
+ if (opts.exitCode && report.blocked)
29
+ process.exitCode = 10;
30
+ }
@@ -0,0 +1,10 @@
1
+ import { ProjectOpts } from "./projectOptions";
2
+ export interface HandoffOpts extends ProjectOpts {
3
+ to: string;
4
+ diet?: string;
5
+ force?: boolean;
6
+ run?: boolean;
7
+ noRun?: boolean;
8
+ allowApiKeyEnv?: boolean;
9
+ }
10
+ export declare function handoffCommand(opts: HandoffOpts): Promise<void>;
@@ -0,0 +1,133 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.handoffCommand = handoffCommand;
4
+ const core_1 = require("@relay-baton/core");
5
+ const projectOptions_1 = require("./projectOptions");
6
+ const auditApiKeyEnv_1 = require("./auditApiKeyEnv");
7
+ async function handoffCommand(opts) {
8
+ const projectContext = (0, projectOptions_1.resolveProjectContext)(opts, true);
9
+ const repoRoot = projectContext.repoRoot;
10
+ const { config } = core_1.ConfigLoader.load(repoRoot);
11
+ const sm = new core_1.SessionManager(repoRoot, config);
12
+ if (!sm.getMeta())
13
+ sm.init("");
14
+ const git = new core_1.GitService(repoRoot);
15
+ if (!git.isGitRepo()) {
16
+ console.error("[relay-baton] not a git repository. aborting handoff.");
17
+ process.exit(2);
18
+ }
19
+ const profileName = (opts.diet ?? sm.getMeta()?.tokenDietProfile ?? projectContext.project?.defaultDiet ?? config.tokenDiet.profile);
20
+ if (!config.tokenDiet.profiles[profileName]) {
21
+ console.error(`unknown diet profile: ${profileName}`);
22
+ process.exit(2);
23
+ }
24
+ // v0.4 observability: stamp the start of this handoff and clear any stale
25
+ // endedAt/durationMs from a previous run/handoff in the same session.
26
+ const startedAt = new Date().toISOString();
27
+ sm.updateMeta({ startedAt, endedAt: undefined, durationMs: undefined });
28
+ const prevMeta = sm.getMeta();
29
+ // v2.5 pre-handoff hooks (opt-in, local-only; no-op when unconfigured).
30
+ const hooks = new core_1.HookRunner(repoRoot, config);
31
+ const runHooks = (phase) => {
32
+ for (const r of hooks.run(phase, {
33
+ allowApiKeyEnv: opts.allowApiKeyEnv,
34
+ onStdout: l => process.stdout.write(l + "\n"),
35
+ onStderr: l => process.stderr.write(l + "\n"),
36
+ })) {
37
+ if (!r.ok)
38
+ console.error(`[relay-baton] hook failed (${phase}): ${r.command} → ${r.exitCode ?? r.error}`);
39
+ }
40
+ };
41
+ runHooks("preHandoff");
42
+ const wf = new core_1.BatonWorkflow(sm, config);
43
+ const result = wf.buildHandoff({
44
+ profileName,
45
+ fallbackReason: prevMeta.fallbackReason,
46
+ previousAgent: prevMeta.lastAgent !== "none" ? prevMeta.lastAgent : "codex",
47
+ nextAgent: opts.to,
48
+ });
49
+ // v0.4 observability: count this successful handoff write.
50
+ const prevCount = sm.getMeta()?.handoffCount ?? 0;
51
+ sm.updateMeta({
52
+ status: "handoff_ready",
53
+ tokenDietProfile: profileName,
54
+ handoffCount: prevCount + 1,
55
+ });
56
+ console.log(`[relay-baton] handoff written: ${result.handoffPath} (${result.usedChars} chars, truncated=${result.truncated})`);
57
+ // v2.4 local usage insight (token proxy; never transmitted).
58
+ new core_1.UsageLedger(repoRoot).record("handoff", (0, core_1.isAgentId)(opts.to) ? opts.to : "none", result.usedChars, `${prevMeta.lastAgent}→${opts.to}`);
59
+ const gate = new core_1.HandoffQualityGate(repoRoot).check();
60
+ const dietGate = new core_1.TokenDietQualityGate(repoRoot, profileName, config.tokenDiet.profiles[profileName])
61
+ .check({ wasTruncated: result.truncated });
62
+ let blocked = false;
63
+ if (!gate.ok) {
64
+ console.error("[relay-baton] Handoff Quality Gate failed:");
65
+ for (const f of gate.failures)
66
+ console.error(" - " + f);
67
+ blocked = true;
68
+ }
69
+ if (!dietGate.ok) {
70
+ console.error("[relay-baton] Token Diet Quality Gate failed:");
71
+ for (const f of dietGate.failures)
72
+ console.error(" - " + f);
73
+ blocked = true;
74
+ }
75
+ for (const w of dietGate.warnings)
76
+ console.error("[relay-baton] warn: " + w);
77
+ // Redaction gate: never let high-severity secrets reach the next agent.
78
+ // Medium findings (home paths, oversized) are warnings only.
79
+ const redaction = result.redaction;
80
+ const highFindings = redaction.findings.filter(f => f.severity === "high");
81
+ if (highFindings.length > 0) {
82
+ console.error("[relay-baton] Redaction Gate failed (handoff would leak secrets to the next agent):");
83
+ for (const f of highFindings)
84
+ console.error(` - ${f.category}: ${f.file}${f.line ? ":" + f.line : ""} (${f.hint})`);
85
+ blocked = true;
86
+ }
87
+ for (const f of redaction.findings.filter(f => f.severity !== "high")) {
88
+ console.error(`[relay-baton] warn: redaction ${f.category} in ${f.file}${f.line ? ":" + f.line : ""} (${f.hint})`);
89
+ }
90
+ if (blocked && !opts.force) {
91
+ console.error("[relay-baton] aborting. Use --force to override.");
92
+ process.exit(3);
93
+ }
94
+ if (opts.run === false || opts.noRun)
95
+ return;
96
+ if (opts.to !== "claude") {
97
+ console.error("[relay-baton] MVP only supports --to claude");
98
+ process.exit(2);
99
+ }
100
+ const adapter = new core_1.ClaudeCodeAdapter(config.agents.claude);
101
+ const prompt = core_1.PromptBuilder.claudeContinuation();
102
+ const cmd = adapter.buildCommand({ task: prevMeta.task, repoRoot, sessionDir: sm.files.dir, prompt });
103
+ sm.updateMeta({ activeAgent: "claude", status: "running_fallback" });
104
+ const r = await (0, core_1.runAgent)({
105
+ command: cmd,
106
+ logFile: sm.files.p("commandsLog"),
107
+ authPolicy: config.authPolicy,
108
+ allowApiKeyEnv: opts.allowApiKeyEnv,
109
+ onStdout: l => process.stdout.write(l + "\n"),
110
+ onStderr: l => process.stderr.write(l + "\n"),
111
+ });
112
+ (0, auditApiKeyEnv_1.auditApiKeyEnv)(repoRoot, r.passedThroughEnvVars, sm.getMeta()?.id);
113
+ if (r.error) {
114
+ console.error(r.error);
115
+ const endedAt = new Date().toISOString();
116
+ sm.updateMeta({
117
+ status: "failed", lastError: r.error, lastAgent: "claude", activeAgent: "none",
118
+ endedAt, durationMs: Date.parse(endedAt) - Date.parse(startedAt),
119
+ });
120
+ process.exit(1);
121
+ }
122
+ if (r.exitCode === 0)
123
+ runHooks("postExecute");
124
+ const endedAt = new Date().toISOString();
125
+ sm.updateMeta({
126
+ status: r.exitCode === 0 ? "completed" : "failed",
127
+ lastAgent: "claude",
128
+ activeAgent: "none",
129
+ lastError: r.exitCode === 0 ? null : `claude exited with ${r.exitCode}`,
130
+ endedAt,
131
+ durationMs: Date.parse(endedAt) - Date.parse(startedAt),
132
+ });
133
+ }
@@ -0,0 +1,12 @@
1
+ import { ProjectOpts } from "./projectOptions";
2
+ export interface HandoffBundleOpts extends ProjectOpts {
3
+ json?: boolean;
4
+ dryRun?: boolean;
5
+ out?: string;
6
+ }
7
+ export declare function handoffBundleCommand(opts?: HandoffBundleOpts): Promise<void>;
8
+ export interface HandoffInspectOpts {
9
+ json?: boolean;
10
+ out?: string;
11
+ }
12
+ export declare function handoffInspectCommand(bundle: string, opts?: HandoffInspectOpts): Promise<void>;
@@ -0,0 +1,64 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.handoffBundleCommand = handoffBundleCommand;
4
+ exports.handoffInspectCommand = handoffInspectCommand;
5
+ const core_1 = require("@relay-baton/core");
6
+ const projectOptions_1 = require("./projectOptions");
7
+ async function handoffBundleCommand(opts = {}) {
8
+ const repoRoot = (0, projectOptions_1.resolveRepoRoot)(opts);
9
+ const result = new core_1.HandoffBundler(repoRoot).bundle({ bundleRoot: opts.out, dryRun: opts.dryRun });
10
+ if (opts.json) {
11
+ console.log(JSON.stringify(result, null, 2));
12
+ return;
13
+ }
14
+ if (!result.available) {
15
+ console.log(`[relay-baton] handoff bundle unavailable: ${result.reason}`);
16
+ return;
17
+ }
18
+ const count = result.manifest?.files.length ?? 0;
19
+ const prefix = result.dryRun ? "would bundle" : "bundled";
20
+ console.log(`[relay-baton] ${prefix} ${count} artifact(s)`);
21
+ console.log(`bundle: ${result.bundleDir}`);
22
+ const r = result.redaction;
23
+ if (r && !r.clean) {
24
+ console.log(`⚠ redaction: ${r.findings.length} finding(s) — review before sharing`);
25
+ for (const f of r.findings.slice(0, 10)) {
26
+ console.log(` - [${f.severity}] ${f.category}: ${f.file}${f.line ? ":" + f.line : ""} (${f.hint})`);
27
+ }
28
+ }
29
+ else if (r) {
30
+ console.log("redaction: clean");
31
+ }
32
+ if (!result.dryRun)
33
+ console.log("manifest: manifest.json");
34
+ }
35
+ async function handoffInspectCommand(bundle, opts = {}) {
36
+ const result = new core_1.HandoffBundleInspector({ bundleRoot: opts.out }).inspect(bundle);
37
+ if (opts.json) {
38
+ console.log(JSON.stringify(result, null, 2));
39
+ return;
40
+ }
41
+ if (!result.available) {
42
+ console.log(`[relay-baton] cannot inspect bundle: ${result.reason}`);
43
+ return;
44
+ }
45
+ console.log(`[relay-baton] handoff bundle ${result.id}`);
46
+ console.log(`repoRoot: ${result.repoRoot ?? "unknown"}`);
47
+ console.log(`createdAt: ${result.createdAt ?? "unknown"}`);
48
+ console.log(`git: ${result.git?.branch ?? "(no git)"}${result.git ? ` · ${result.git.changed} changed` : ""}`);
49
+ console.log(`files: ${result.fileCount}, total ${result.totalBytes} bytes`);
50
+ console.log(`integrity: ${result.intact ? "intact" : "DAMAGED"}`);
51
+ console.log(`trust: ${result.safe ? "safe" : "UNSAFE — do not import"}`);
52
+ if (result.missing.length)
53
+ console.log(`missing: ${result.missing.join(", ")}`);
54
+ if (result.corrupt.length)
55
+ console.log(`corrupt: ${result.corrupt.join(", ")}`);
56
+ if (result.unsafe.length) {
57
+ console.log(`⚠ unsafe targets (path traversal / oversized): ${result.unsafe.length}`);
58
+ for (const f of result.files.filter(x => !x.safe)) {
59
+ console.log(` - [${f.unsafeReason}] ${f.target}`);
60
+ }
61
+ }
62
+ if (result.redactionFindings != null)
63
+ console.log(`redaction findings recorded: ${result.redactionFindings}`);
64
+ }
@@ -0,0 +1,23 @@
1
+ import { ProjectOpts } from "./projectOptions";
2
+ /**
3
+ * Returns the first non-empty line of `content`, trimmed and clipped to
4
+ * `maxLen` characters. Skips the leading `#` heading markers so the label
5
+ * is more descriptive (e.g. "Relay Baton Handoff" instead of "#").
6
+ */
7
+ export declare function firstNonEmptyLine(content: string, maxLen?: number): string;
8
+ export interface HandoffHistoryEntry {
9
+ /** Filename inside `.ai-session/` — e.g. "handoff.md" or "handoff.2026-...md" */
10
+ name: string;
11
+ /** Absolute path on disk. */
12
+ path: string;
13
+ /** Modification time in milliseconds since epoch. */
14
+ mtimeMs: number;
15
+ /** File size in bytes. */
16
+ size: number;
17
+ /** First non-empty content line, used as a row label. */
18
+ label: string;
19
+ /** True when this is the current `handoff.md` (not a backup). */
20
+ current: boolean;
21
+ }
22
+ export declare function collectHandoffHistory(repoRoot: string): HandoffHistoryEntry[];
23
+ export declare function handoffHistoryCommand(opts?: ProjectOpts): Promise<void>;