free-pi-cli 0.4.1 → 0.4.3

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/index.js CHANGED
@@ -27,4 +27,4 @@ if (err) {
27
27
  console.error(err);
28
28
  process.exit(1);
29
29
  }
30
- await import("./main-r8jqgh5n.js");
30
+ await import("./main-xn8n5cb2.js");
@@ -44,7 +44,7 @@ function openBrowser(url, platform = process.platform, spawnImpl = spawn) {
44
44
  // package.json
45
45
  var package_default = {
46
46
  name: "free-pi-cli",
47
- version: "0.4.1",
47
+ version: "0.4.3",
48
48
  description: "Free, ad-supported distribution of the pi coding agent. Inference paid for by ads.",
49
49
  license: "MIT",
50
50
  type: "module",
@@ -53,6 +53,7 @@ var package_default = {
53
53
  },
54
54
  files: [
55
55
  "dist",
56
+ "skills",
56
57
  "README.md"
57
58
  ],
58
59
  engines: {
@@ -4828,6 +4829,7 @@ var MeResponseSchema = object({
4828
4829
  cap_usd_today: number2(),
4829
4830
  tier: AccountTierSchema,
4830
4831
  credit_usd: number2(),
4832
+ consent_version: string2().nullable().optional(),
4831
4833
  notice: string2().optional(),
4832
4834
  buy_url: string2().optional(),
4833
4835
  allowance_credits: number2().optional(),
@@ -5086,7 +5088,7 @@ function formatCreditsLeftLine(remaining, allowance, refillAt) {
5086
5088
  }
5087
5089
  var SLOW_LANE_METER_LINE = "Slow lane: cheapest model until refill";
5088
5090
  // ../pi-ads/src/api.ts
5089
- var REQUEST_TIMEOUT_MS = 3000;
5091
+ var REQUEST_TIMEOUT_MS = 5000;
5090
5092
  async function authedFetch(deps, path, init) {
5091
5093
  const token = await deps.getToken();
5092
5094
  const doFetch = deps.fetchImpl ?? fetch;
@@ -5202,6 +5204,8 @@ function createBannerRenderer(opts = {}) {
5202
5204
  return {
5203
5205
  async render(deps, ctx, config, columnsOverride) {
5204
5206
  const ad = await fetchAdNext(deps, "banner");
5207
+ if (opts.isActive && !opts.isActive())
5208
+ return;
5205
5209
  if (!ad) {
5206
5210
  ctx.ui.setWidget(BANNER_WIDGET_ID, undefined);
5207
5211
  opts.onBannerAd?.(undefined);
@@ -5361,6 +5365,7 @@ async function renderMeter(deps, ctx) {
5361
5365
 
5362
5366
  // ../pi-ads/src/index.ts
5363
5367
  var BANNER_REFRESH_MS = 10 * 60 * 1000;
5368
+ var BANNER_MIN_REFRESH_MS = 60 * 1000;
5364
5369
  function createAdsExtension(opts) {
5365
5370
  const deps = {
5366
5371
  baseUrl: opts.baseUrl,
@@ -5372,23 +5377,42 @@ function createAdsExtension(opts) {
5372
5377
  return {
5373
5378
  name: "free-pi-ads",
5374
5379
  factory(pi) {
5375
- const banner = createBannerRenderer({ onBannerAd: opts.onBannerAd });
5380
+ let stopped = false;
5381
+ const banner = createBannerRenderer({ onBannerAd: opts.onBannerAd, isActive: () => !stopped });
5376
5382
  const inline = createInlineRenderer();
5377
5383
  let bannerInterval;
5384
+ let lastBannerAt = 0;
5385
+ let bannerInFlight = false;
5386
+ const renderBanner = async (ctx, force = false) => {
5387
+ if (stopped)
5388
+ return;
5389
+ if (!force && (bannerInFlight || Date.now() - lastBannerAt < BANNER_MIN_REFRESH_MS))
5390
+ return;
5391
+ bannerInFlight = true;
5392
+ try {
5393
+ await banner.render(deps, ctx, config);
5394
+ } finally {
5395
+ bannerInFlight = false;
5396
+ lastBannerAt = Date.now();
5397
+ }
5398
+ };
5378
5399
  pi.on("session_start", async (_event, ctx) => {
5400
+ stopped = false;
5379
5401
  inline.reset(ctx);
5380
- await Promise.all([banner.render(deps, ctx, config), renderMeter(deps, ctx)]);
5402
+ await Promise.all([renderBanner(ctx, true), renderMeter(deps, ctx)]);
5381
5403
  if (bannerInterval)
5382
5404
  clearInterval(bannerInterval);
5383
5405
  bannerInterval = setInterval(() => {
5384
- banner.render(deps, ctx, config);
5406
+ renderBanner(ctx);
5385
5407
  }, BANNER_REFRESH_MS);
5386
5408
  bannerInterval.unref?.();
5387
5409
  });
5388
5410
  pi.on("turn_end", async (_event, ctx) => {
5411
+ renderBanner(ctx);
5389
5412
  await Promise.all([inline.onTurnEnd(deps, ctx, config), renderMeter(deps, ctx)]);
5390
5413
  });
5391
5414
  pi.on("session_shutdown", () => {
5415
+ stopped = true;
5392
5416
  if (bannerInterval) {
5393
5417
  clearInterval(bannerInterval);
5394
5418
  bannerInterval = undefined;
@@ -5399,7 +5423,8 @@ function createAdsExtension(opts) {
5399
5423
  }
5400
5424
 
5401
5425
  // src/pi-launch.ts
5402
- import { join as join3, resolve as resolve2 } from "node:path";
5426
+ import { dirname as dirname3, join as join4, resolve as resolve2 } from "node:path";
5427
+ import { fileURLToPath as fileURLToPath3 } from "node:url";
5403
5428
 
5404
5429
  // src/provider.ts
5405
5430
  var PROVIDER_NAME = "free-pi";
@@ -6211,6 +6236,64 @@ function createTodoToolExtension(options = {}) {
6211
6236
  };
6212
6237
  }
6213
6238
 
6239
+ // src/tool-aliases.ts
6240
+ import { homedir as homedir2 } from "node:os";
6241
+ import { join as join3 } from "node:path";
6242
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
6243
+ import {
6244
+ createBashToolDefinition,
6245
+ createReadToolDefinition
6246
+ } from "@earendil-works/pi-coding-agent";
6247
+ function aliasArgs(input, key, aliases) {
6248
+ if (!input || typeof input !== "object" || key in input)
6249
+ return input;
6250
+ const args = input;
6251
+ const alias = aliases.find((a) => (a in args));
6252
+ if (!alias)
6253
+ return input;
6254
+ const { [alias]: value, ...rest } = args;
6255
+ return { ...rest, [key]: value };
6256
+ }
6257
+ function normalizeShellPath(input, platform = process.platform, home = homedir2()) {
6258
+ let p = input;
6259
+ if (platform === "win32" && p.startsWith("/") && !p.startsWith("//") && !p.includes("\\")) {
6260
+ const m = p.match(/^\/(?:mnt\/|cygdrive\/)?([a-z])(?:\/(.*))?$/i);
6261
+ if (m)
6262
+ p = `${m[1].toUpperCase()}:\\${m[2]?.replaceAll("/", "\\") ?? ""}`;
6263
+ }
6264
+ if (p === "~")
6265
+ return home;
6266
+ if (p.startsWith("~/") || platform === "win32" && p.startsWith("~\\"))
6267
+ return join3(home, p.slice(2));
6268
+ if (/^file:\/\//.test(p))
6269
+ return fileURLToPath2(p);
6270
+ return p;
6271
+ }
6272
+ function createToolAliasesExtension() {
6273
+ return {
6274
+ name: "free-pi-tool-aliases",
6275
+ factory: (pi) => {
6276
+ const cwd = process.cwd();
6277
+ const bash = createBashToolDefinition(cwd);
6278
+ const read = createReadToolDefinition(cwd);
6279
+ pi.registerTool({
6280
+ ...bash,
6281
+ prepareArguments: (a) => aliasArgs(bash.prepareArguments ? bash.prepareArguments(a) : a, "command", ["cmd"]),
6282
+ execute: (...args) => {
6283
+ const s = pi.getSettings();
6284
+ const shellPath = s.shellPath ? normalizeShellPath(s.shellPath) : s.shellPath;
6285
+ return createBashToolDefinition(cwd, { commandPrefix: s.shellCommandPrefix, shellPath }).execute(...args);
6286
+ }
6287
+ });
6288
+ pi.registerTool({
6289
+ ...read,
6290
+ prepareArguments: (a) => aliasArgs(read.prepareArguments ? read.prepareArguments(a) : a, "path", ["file", "file_path"]),
6291
+ execute: (...args) => createReadToolDefinition(cwd, { autoResizeImages: pi.getSettings().images?.autoResize ?? true }).execute(...args)
6292
+ });
6293
+ }
6294
+ };
6295
+ }
6296
+
6214
6297
  // src/error-notice.ts
6215
6298
  var NOTICE_DEDUPE_MS = 15000;
6216
6299
  function parseErrorEnvelope(errorMessage) {
@@ -6253,6 +6336,16 @@ function createErrorNoticeExtension(opts = {}) {
6253
6336
  };
6254
6337
  }
6255
6338
 
6339
+ // src/agent-rules.ts
6340
+ var AGENT_RULES = `## Working rules
6341
+ - When you say you will run, check, or change something, make that tool call in the same
6342
+ turn. Never end a turn with only a plan or a script printed as text.
6343
+ - If the same error appears twice, stop retrying variants. Read the relevant source, docs
6344
+ or full error output, or try a different approach. Do not re-read a file you already read
6345
+ unless it has changed.
6346
+ - Before saying a task is done, check the result the user will see: run the program, open
6347
+ the output, or run the specific test. Say plainly what you checked and what you did not.`;
6348
+
6256
6349
  // src/tool-guard.ts
6257
6350
  async function reportToolGuardTrigger(opts, toolName) {
6258
6351
  try {
@@ -6351,8 +6444,37 @@ function registerBugAutocomplete(pi) {
6351
6444
  });
6352
6445
  }
6353
6446
 
6447
+ // src/footer-align.ts
6448
+ var ANSI = /\x1b\[[0-9;]*m/g;
6449
+ var SIMPLE = /^[\x20-\x7e\xa0-\xff]*$/;
6450
+ function installFooterRightAlign(mode) {
6451
+ const footer = mode.footer;
6452
+ const render = footer.render.bind(footer);
6453
+ footer.render = (w) => {
6454
+ const lines = render(w);
6455
+ if (lines.length <= 2)
6456
+ return lines;
6457
+ const last = lines[lines.length - 1];
6458
+ const text = last.replace(ANSI, "");
6459
+ if (!SIMPLE.test(text))
6460
+ return lines;
6461
+ return [...lines.slice(0, -1), " ".repeat(Math.max(0, w - text.length)) + last];
6462
+ };
6463
+ }
6464
+
6354
6465
  // src/changelog.ts
6355
6466
  var CHANGELOG_HIGHLIGHTS = [
6467
+ {
6468
+ version: "0.4.3",
6469
+ highlights: [
6470
+ "The ad banner now refreshes while you work, at most once a minute.",
6471
+ "If our terms have changed since you signed up, you'll be asked to review and accept them."
6472
+ ]
6473
+ },
6474
+ {
6475
+ version: "0.4.2",
6476
+ highlights: ["Credits now sit on the right of the footer, under the model name."]
6477
+ },
6356
6478
  {
6357
6479
  version: "0.4.1",
6358
6480
  highlights: [
@@ -6714,12 +6836,89 @@ function createHeaderExtension(opts) {
6714
6836
  };
6715
6837
  }
6716
6838
 
6839
+ // src/fusion.ts
6840
+ var FUSION_ID = "fusion";
6841
+ var FUSION_NAME = "Fusion · Argon plans, DeepSeek builds";
6842
+ var FUSION_PLANNER_ID = "gemini-4-argon-flex";
6843
+ var FUSION_EXECUTOR_ID = "deepseek-v4-flash";
6844
+ function editedThisTurn(messages) {
6845
+ for (let i = messages.length - 1;i >= 0; i--) {
6846
+ const message = messages[i];
6847
+ if (message.role === "user")
6848
+ break;
6849
+ if (message.role === "toolResult" && (message.toolName === "edit" || message.toolName === "write") && !message.isError) {
6850
+ return true;
6851
+ }
6852
+ }
6853
+ return false;
6854
+ }
6855
+ function hasNewUserInput(request) {
6856
+ const failedAt = request.failed?.message.timestamp;
6857
+ if (failedAt === undefined)
6858
+ return false;
6859
+ for (let i = request.messages.length - 1;i >= 0; i--) {
6860
+ const message = request.messages[i];
6861
+ if (message.role === "assistant")
6862
+ return false;
6863
+ if (message.role === "user")
6864
+ return message.timestamp > failedAt;
6865
+ }
6866
+ return false;
6867
+ }
6868
+ function routeFusion(request, models) {
6869
+ const phase = request.state?.phase ?? "plan";
6870
+ const restartPlan = request.reason === "user" || request.reason === "retry" && hasNewUserInput(request);
6871
+ const route = (model, nextPhase = phase) => ({
6872
+ model,
6873
+ thinkingLevel: request.thinkingLevel,
6874
+ state: request.reason === "direct" ? undefined : request.state?.phase === nextPhase ? request.state : { phase: nextPhase }
6875
+ });
6876
+ if (!models.planner || !models.executor) {
6877
+ return route(models.defaultModel, restartPlan ? "plan" : phase);
6878
+ }
6879
+ switch (request.reason) {
6880
+ case "user":
6881
+ return route(models.planner, "plan");
6882
+ case "direct":
6883
+ return route(models.executor);
6884
+ case "retry": {
6885
+ if (restartPlan)
6886
+ return route(models.planner, "plan");
6887
+ const failed = request.failed;
6888
+ if (!failed || failed.model.provider === PROVIDER_NAME && failed.model.id === FUSION_PLANNER_ID) {
6889
+ return route(models.executor, "build");
6890
+ }
6891
+ return { ...route(failed.model), thinkingLevel: failed.thinkingLevel ?? request.thinkingLevel };
6892
+ }
6893
+ case "continuation":
6894
+ return phase === "build" || editedThisTurn(request.messages) ? route(models.executor, "build") : route(models.planner, "plan");
6895
+ }
6896
+ }
6897
+ function createFusionExtension(defaultModelId) {
6898
+ return {
6899
+ name: "free-pi-fusion",
6900
+ factory: (pi) => {
6901
+ pi.registerVirtualModel({
6902
+ provider: PROVIDER_NAME,
6903
+ id: FUSION_ID,
6904
+ name: FUSION_NAME,
6905
+ input: ["text"],
6906
+ route: (request, ctx) => routeFusion(request, {
6907
+ planner: ctx.modelRegistry.find(PROVIDER_NAME, FUSION_PLANNER_ID),
6908
+ executor: ctx.modelRegistry.find(PROVIDER_NAME, FUSION_EXECUTOR_ID),
6909
+ defaultModel: ctx.modelRegistry.find(PROVIDER_NAME, defaultModelId)
6910
+ })
6911
+ });
6912
+ }
6913
+ };
6914
+ }
6915
+
6717
6916
  // src/provider-lock.ts
6718
6917
  function resolveFreePiScope(modelRuntime, providerName, modelIds, env = process.env) {
6719
6918
  if (env.FREEPI_KEEP_ENV_PROVIDERS)
6720
6919
  return [];
6721
6920
  const scoped = [];
6722
- for (const id of modelIds) {
6921
+ for (const id of new Set([...modelIds, FUSION_ID])) {
6723
6922
  const model = modelRuntime.getModel(providerName, id);
6724
6923
  if (model)
6725
6924
  scoped.push({ model });
@@ -6812,6 +7011,7 @@ var SDK_TOOLS = [
6812
7011
  "list_mcp_resource_templates",
6813
7012
  "read_mcp_resource"
6814
7013
  ];
7014
+ var SKILLS_DIR = resolve2(dirname3(fileURLToPath3(import.meta.url)), "..", "skills");
6815
7015
  function resolveModelName(opts) {
6816
7016
  return opts.models?.[0]?.name || opts.model || MODEL_ID;
6817
7017
  }
@@ -6873,11 +7073,13 @@ function buildRuntimeOptions(opts, sessionId) {
6873
7073
  updateLatest: opts.updateLatest
6874
7074
  });
6875
7075
  const todoToolExtension = createTodoToolExtension();
7076
+ const toolAliasesExtension = createToolAliasesExtension();
6876
7077
  const mcpExtension = { name: "pi-mcp", factory: createMcpExtension() };
6877
7078
  const codemodeExtension = { name: "pi-codemode", factory: createCodemodeExtension({ models: false }) };
6878
7079
  const toolSearchExtension = { name: "pi-tool-search", factory: createToolSearchExtension() };
6879
7080
  const extensionFactories = [
6880
7081
  providerExtension,
7082
+ createFusionExtension(models[0].id),
6881
7083
  adsExtension,
6882
7084
  usageToolExtension,
6883
7085
  buyToolExtension,
@@ -6887,6 +7089,7 @@ function buildRuntimeOptions(opts, sessionId) {
6887
7089
  commandsExtension,
6888
7090
  headerExtension,
6889
7091
  todoToolExtension,
7092
+ toolAliasesExtension,
6890
7093
  mcpExtension,
6891
7094
  codemodeExtension,
6892
7095
  toolSearchExtension
@@ -6894,7 +7097,11 @@ function buildRuntimeOptions(opts, sessionId) {
6894
7097
  const settingsManager = buildSettingsManager(opts, models, opts.projectTrusted === true);
6895
7098
  return {
6896
7099
  settingsManager,
6897
- resourceLoaderOptions: { extensionFactories },
7100
+ resourceLoaderOptions: {
7101
+ extensionFactories,
7102
+ additionalSkillPaths: [SKILLS_DIR],
7103
+ appendSystemPromptOverride: (base) => [...base, AGENT_RULES]
7104
+ },
6898
7105
  extensionNames: extensionFactories.map((e) => e.name),
6899
7106
  tools: SDK_TOOLS
6900
7107
  };
@@ -6902,7 +7109,7 @@ function buildRuntimeOptions(opts, sessionId) {
6902
7109
  function getSessionDir(cwd, agentDir) {
6903
7110
  const resolvedCwd = resolve2(cwd);
6904
7111
  const safePath = `--${resolvedCwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`;
6905
- return join3(resolve2(agentDir), "sessions", safePath);
7112
+ return join4(resolve2(agentDir), "sessions", safePath);
6906
7113
  }
6907
7114
  async function createSessionManager(cwd, sessionDir, sessionId, api = SessionManager) {
6908
7115
  if (sessionId) {
@@ -6961,6 +7168,7 @@ async function launchPi(opts) {
6961
7168
  initialMessages: []
6962
7169
  });
6963
7170
  installBugHandler(mode, { baseUrl: opts.baseUrl, getToken: () => opts.jwt });
7171
+ installFooterRightAlign(mode);
6964
7172
  await mode.run();
6965
7173
  return sm.getSessionId();
6966
7174
  }
@@ -7000,18 +7208,19 @@ var CONSENT_PROMPT = "Accept? [y/N] ";
7000
7208
  function parseConsentAnswer(answer) {
7001
7209
  return /^\s*y(es)?\s*$/i.test(answer);
7002
7210
  }
7211
+ var REPROMPT_CONSENT_TEXT = CONSENT_TEXT.replace("Decline and nothing is sent to the free-pi server.", "Decline and free-pi exits; you will be asked again next time.");
7003
7212
 
7004
7213
  // src/prompt.ts
7005
- async function promptConsent(deps) {
7214
+ async function promptConsent(deps, text = CONSENT_TEXT) {
7006
7215
  if (!deps.stdinIsTTY) {
7007
7216
  deps.log(NON_INTERACTIVE_CONSENT_TEXT);
7008
7217
  return false;
7009
7218
  }
7010
- deps.log(CONSENT_TEXT);
7219
+ deps.log(text);
7011
7220
  const answer = await deps.askLine(CONSENT_PROMPT);
7012
7221
  return parseConsentAnswer(answer);
7013
7222
  }
7014
- async function promptConsentInteractive() {
7223
+ async function promptConsentInteractive(text) {
7015
7224
  return promptConsent({
7016
7225
  stdinIsTTY: process.stdin.isTTY === true,
7017
7226
  log: (message) => console.log(message),
@@ -7023,7 +7232,7 @@ async function promptConsentInteractive() {
7023
7232
  rl.close();
7024
7233
  }
7025
7234
  }
7026
- });
7235
+ }, text);
7027
7236
  }
7028
7237
 
7029
7238
  // src/real-spawn.ts
@@ -7119,6 +7328,24 @@ async function checkToken(baseUrl, jwt) {
7119
7328
  });
7120
7329
  if (res.status === 401)
7121
7330
  throw new UnauthorizedError;
7331
+ if (!res.ok)
7332
+ return;
7333
+ const body = await res.json().catch(() => {
7334
+ return;
7335
+ });
7336
+ const v = body?.consent_version;
7337
+ return typeof v === "string" || v === null ? v : undefined;
7338
+ }
7339
+ async function submitUserConsent(baseUrl, jwt, consentVersion) {
7340
+ const res = await fetch(new URL("/me/consent", baseUrl), {
7341
+ method: "POST",
7342
+ headers: { authorization: `Bearer ${jwt}`, "content-type": "application/json" },
7343
+ body: JSON.stringify({ consent_version: consentVersion })
7344
+ });
7345
+ if (res.status === 401)
7346
+ throw new UnauthorizedError;
7347
+ if (!res.ok)
7348
+ throw new Error(`consent submission failed: HTTP ${res.status}`);
7122
7349
  }
7123
7350
 
7124
7351
  // src/run.ts
@@ -7152,7 +7379,7 @@ ${INTRO_TEXT}
7152
7379
 
7153
7380
  // src/whats-new.ts
7154
7381
  import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "node:fs";
7155
- import { dirname as dirname3 } from "node:path";
7382
+ import { dirname as dirname4 } from "node:path";
7156
7383
  function maybeWhatsNew(cliVersion, agentDir) {
7157
7384
  try {
7158
7385
  const path = getLastSeenVersionPath(agentDir);
@@ -7172,7 +7399,7 @@ function maybeWhatsNew(cliVersion, agentDir) {
7172
7399
  }
7173
7400
  }
7174
7401
  function writeLastSeen(path, version) {
7175
- mkdirSync(dirname3(path), { recursive: true });
7402
+ mkdirSync(dirname4(path), { recursive: true });
7176
7403
  writeFileSync(path, version, "utf-8");
7177
7404
  }
7178
7405
 
@@ -7212,7 +7439,19 @@ async function run(deps, argv = process.argv) {
7212
7439
  let jwt = await loadJwt(deps.credentialsPath);
7213
7440
  if (jwt) {
7214
7441
  try {
7215
- await checkToken(deps.baseUrl, jwt);
7442
+ const serverConsent = await checkToken(deps.baseUrl, jwt);
7443
+ if (serverConsent !== undefined && serverConsent !== CONSENT_VERSION) {
7444
+ if (!await deps.promptConsent(REPROMPT_CONSENT_TEXT)) {
7445
+ deps.log("Declined. free-pi needs you to accept the updated terms to continue; you will be asked again next time.");
7446
+ return 1;
7447
+ }
7448
+ try {
7449
+ await submitUserConsent(deps.baseUrl, jwt, CONSENT_VERSION);
7450
+ } catch (err) {
7451
+ if (err instanceof UnauthorizedError)
7452
+ throw err;
7453
+ }
7454
+ }
7216
7455
  } catch (err) {
7217
7456
  if (err instanceof UnauthorizedError) {
7218
7457
  await clearJwt(deps.credentialsPath);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "free-pi-cli",
3
- "version": "0.4.1",
3
+ "version": "0.4.3",
4
4
  "description": "Free, ad-supported distribution of the pi coding agent. Inference paid for by ads.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -9,6 +9,7 @@
9
9
  },
10
10
  "files": [
11
11
  "dist",
12
+ "skills",
12
13
  "README.md"
13
14
  ],
14
15
  "engines": {
@@ -0,0 +1,14 @@
1
+ ---
2
+ name: long-jobs
3
+ description: Use when running builds, test suites, dev servers, downloads, decompilers, or any command expected to take longer than about a minute. Covers checking tools first, running jobs in the background with a log, waiting once, and not hiding errors.
4
+ ---
5
+
6
+ # Long-running jobs
7
+
8
+ - Before relying on a tool, check it exists once (`command -v gh cmake timeout`) and note what is missing. Do not assume `sudo`.
9
+ - Run the specific test or target first. Run a full suite once, at the end.
10
+ - Before rerunning a long job, check whether its earlier output or log already exists.
11
+ - Start long processes in the background with output to a log file and the PID saved: `nohup cmd > job.log 2>&1 & echo $! > job.pid`. Stop them with `kill $(cat job.pid)`. Never use `pkill -f` with a pattern that also matches your own command line.
12
+ - Wait once, with a time limit, for a marker in the log: `timeout 600 sh -c 'until grep -q "done" job.log; do sleep 5; done'`. To wait for the process instead: `timeout 600 sh -c 'while kill -0 $(cat job.pid) 2>/dev/null; do sleep 5; done'`. Do not poll with repeated short sleep-and-tail calls, and do not use `tail -f | grep` (it keeps waiting after the match).
13
+ - Check a port is free before starting a server, and stop the servers you started.
14
+ - Do not hide errors: no `2>/dev/null` and no `| tail` on a command whose failure you need to diagnose. Test a new parser or script on a small sample first.