tines 0.0.265 → 0.0.266

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 (2) hide show
  1. package/dist/index.js +182 -49
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -3368,6 +3368,9 @@ function useColor() {
3368
3368
  // ../../node_modules/.pnpm/commander@15.0.0/node_modules/commander/index.js
3369
3369
  var program = new Command();
3370
3370
 
3371
+ // src/commands/context.ts
3372
+ import { readFileSync as readFileSync4 } from "node:fs";
3373
+
3371
3374
  // src/body-value.ts
3372
3375
  import { readFileSync } from "node:fs";
3373
3376
  var processStdin = {
@@ -3432,6 +3435,7 @@ var LABEL_NAME_MAX = 50;
3432
3435
  var PROMPT_MAX_BYTES = 32 * 1024;
3433
3436
  var SKILL_MAX_FILES = 20;
3434
3437
  var SKILL_MAX_TOTAL_BYTES = 100 * 1024;
3438
+ var ENV_VALUE_MAX_BYTES = 16 * 1024;
3435
3439
  var AGENT_GUIDELINES_NAME = "agent-guidelines";
3436
3440
  var JOURNAL_NAME = "journal";
3437
3441
  var AGENT_GUIDELINES_BODY = `You are an agent working on a Tines issue over its HTTP API / CLI. Beyond doing the work, leave the workspace smarter than you found it. Four places to write, chosen by who should inherit what you learned:
@@ -5636,6 +5640,8 @@ function contextItemSummary(item) {
5636
5640
  return `${item.repo_url}${item.repo_branch ? `#${item.repo_branch}` : ""}`;
5637
5641
  case "artifact":
5638
5642
  return item.artifact_type ?? "artifact";
5643
+ case "env":
5644
+ return item.secret ? `secret \xB7 set${item.hint ? ` \xB7 ${item.hint}` : ""}` : `= ${item.value ?? ""}`;
5639
5645
  }
5640
5646
  }
5641
5647
  function recurrenceLabel(schedule2) {
@@ -6234,6 +6240,12 @@ async function resolveScopeFlags(api, opts) {
6234
6240
  if (opts.label !== void 0) scope.label_id = await resolveLabelFlag(api, opts.label);
6235
6241
  return scope;
6236
6242
  }
6243
+ function readEnvValue(raw) {
6244
+ if (raw === "-") return readFileSync4(0, "utf8").replace(/\r?\n$/, "");
6245
+ if (raw.startsWith("@@")) return raw.slice(1);
6246
+ if (raw.startsWith("@")) return readFileSync4(raw.slice(1), "utf8").replace(/\r?\n$/, "");
6247
+ return raw;
6248
+ }
6237
6249
  function printContextItem(item) {
6238
6250
  console.log(`${item.kind} "${item.name}" [${item.id}] v${item.version}`);
6239
6251
  if (item.description) console.log(item.description);
@@ -6256,6 +6268,15 @@ files (seeded at skills/${item.name}/):`);
6256
6268
  console.log(` ${f.path} (${Buffer.byteLength(f.content, "utf8")} bytes)`);
6257
6269
  }
6258
6270
  if ((item.files ?? []).length === 0) console.log(" (none)");
6271
+ } else if (item.kind === "env") {
6272
+ if (item.secret) {
6273
+ console.log(`
6274
+ value: (secret, set)${item.hint ? ` hint: ${item.hint}` : ""}`);
6275
+ } else {
6276
+ console.log(`
6277
+ value: ${item.value ?? ""}`);
6278
+ if (item.hint) console.log(`hint: ${item.hint}`);
6279
+ }
6259
6280
  } else {
6260
6281
  console.log(`
6261
6282
  url: ${item.repo_url}`);
@@ -6277,13 +6298,13 @@ function withScopeFlags(cmd) {
6277
6298
  }
6278
6299
  function register(program3) {
6279
6300
  const context2 = program3.command("context").description(
6280
- "Manage context items (prompts, skills, repo pointers) scoped to projects, states, and issues"
6301
+ "Manage context items (prompts, skills, repo pointers, environment variables) scoped to projects, states, and issues"
6281
6302
  );
6282
6303
  withList(
6283
6304
  withScopeFlags(
6284
6305
  context2.command("list").description(
6285
6306
  "List context items (scope filters match every item whose scope includes the element)"
6286
- ).option("-k, --kind <kind>", "filter by kind: prompt, skill, or repo").option("--exact", "only items whose scope sets exactly the given dimensions").option("-q, --search <text>", "search names and descriptions")
6307
+ ).option("-k, --kind <kind>", "filter by kind: prompt, skill, repo, artifact, or env").option("--exact", "only items whose scope sets exactly the given dimensions").option("-q, --search <text>", "search names and descriptions")
6287
6308
  )
6288
6309
  ).action(
6289
6310
  async (opts) => {
@@ -6327,21 +6348,27 @@ function register(program3) {
6327
6348
  });
6328
6349
  withCommon(
6329
6350
  withScopeFlags(
6330
- context2.command("create").description("Create a context item scoped to a project, state, and/or issue").requiredOption("-k, --kind <kind>", "prompt, skill, or repo").requiredOption(
6351
+ context2.command("create").description("Create a context item scoped to a project, state, and/or issue").requiredOption("-k, --kind <kind>", "prompt, skill, repo, or env").requiredOption(
6331
6352
  "-n, --name <name>",
6332
- "item name (slug-like for skills; the dedup/override key)"
6353
+ "item name (slug-like for skills, the variable name for env; the dedup/override key)"
6333
6354
  ).option("-d, --description <text>", "one-liner shown in lists").option("--body <md>", "prompt body: inline Markdown or @file (escape a literal @ as @@)").option(
6334
6355
  "--file <path>=@<local>",
6335
6356
  "skill file: workspace path = local file (repeatable)",
6336
6357
  collect,
6337
6358
  []
6338
- ).option("--repo-url <url>", "repo: clone URL").option("--branch <branch>", "repo: branch to check out").option("--dir <dir>", "repo: checkout directory (defaults to the URL's basename)")
6359
+ ).option("--repo-url <url>", "repo: clone URL").option("--branch <branch>", "repo: branch to check out").option("--dir <dir>", "repo: checkout directory (defaults to the URL's basename)").option(
6360
+ "--value <v>",
6361
+ "env: the value \u2014 inline, @file, or - for stdin (keeps a secret out of shell history)"
6362
+ ).option("--secret", "env: encrypt the value at rest; it is write-only afterwards").option("--hint <text>", 'env: a safe display hint for a secret (e.g. "github_pat_\u2026abcd")')
6339
6363
  )
6340
6364
  ).action(
6341
6365
  async (opts) => {
6342
6366
  if (opts.kind === "repo" && opts.repoUrl === void 0) {
6343
6367
  die("--kind repo needs --repo-url <clone-url> (--url is the API base URL)");
6344
6368
  }
6369
+ if (opts.kind === "env" && opts.value === void 0) {
6370
+ die("--kind env needs --value <value|@file|->");
6371
+ }
6345
6372
  const api = client(opts);
6346
6373
  const scope = await resolveScopeFlags(api, opts);
6347
6374
  const body = {
@@ -6356,6 +6383,9 @@ function register(program3) {
6356
6383
  if (opts.repoUrl !== void 0) body.repo_url = opts.repoUrl;
6357
6384
  if (opts.branch !== void 0) body.repo_branch = opts.branch;
6358
6385
  if (opts.dir !== void 0) body.repo_dir = opts.dir;
6386
+ if (opts.value !== void 0) body.value = readEnvValue(opts.value);
6387
+ if (opts.secret) body.secret = true;
6388
+ if (opts.hint !== void 0) body.hint = opts.hint;
6359
6389
  const item = await api.createContextItem(body);
6360
6390
  if (opts.json) return printJson(item);
6361
6391
  console.log(`created ${item.kind} "${item.name}" (${item.id}) \u2014 scope: ${item.scope.label}`);
@@ -6363,7 +6393,7 @@ function register(program3) {
6363
6393
  );
6364
6394
  withCommon(
6365
6395
  withScopeFlags(
6366
- context2.command("edit <id>").description("Edit a context item: payload, name, description, or scope").option("-n, --name <name>", "rename the item").option("-d, --description <text>", "set the description").option("--body <md>", "prompt body: inline Markdown or @file (escape a literal @ as @@)").option("--file <path>=@<local>", "add or replace a skill file (repeatable)", collect, []).option("--remove-file <path>", "remove a skill file (repeatable)", collect, []).option("--repo-url <url>", "repo: clone URL").option("--branch <branch>", "repo: branch (empty string clears it)").option("--dir <dir>", "repo: checkout directory (empty string restores the URL default)").option(
6396
+ context2.command("edit <id>").description("Edit a context item: payload, name, description, or scope").option("-n, --name <name>", "rename the item").option("-d, --description <text>", "set the description").option("--body <md>", "prompt body: inline Markdown or @file (escape a literal @ as @@)").option("--file <path>=@<local>", "add or replace a skill file (repeatable)", collect, []).option("--remove-file <path>", "remove a skill file (repeatable)", collect, []).option("--repo-url <url>", "repo: clone URL").option("--branch <branch>", "repo: branch (empty string clears it)").option("--dir <dir>", "repo: checkout directory (empty string restores the URL default)").option("--value <v>", "env: replace the value (inline, @file, or - for stdin)").option("--secret", "env: encrypt the stored value (irreversible; delete to make public)").option("--hint <text>", "env: display hint for a secret (empty string clears it)").option(
6367
6397
  "--unset <dimension>",
6368
6398
  "drop a scope dimension: project, state, issue, or label (repeatable)",
6369
6399
  collect,
@@ -6413,6 +6443,9 @@ function register(program3) {
6413
6443
  if (opts.repoUrl !== void 0) body.repo_url = opts.repoUrl;
6414
6444
  if (opts.branch !== void 0) body.repo_branch = opts.branch === "" ? null : opts.branch;
6415
6445
  if (opts.dir !== void 0) body.repo_dir = opts.dir === "" ? null : opts.dir;
6446
+ if (opts.value !== void 0) body.value = readEnvValue(opts.value);
6447
+ if (opts.secret) body.secret = true;
6448
+ if (opts.hint !== void 0) body.hint = opts.hint === "" ? null : opts.hint;
6416
6449
  if (Object.keys(body).length === 0) {
6417
6450
  die(
6418
6451
  "nothing to update: pass payload flags, --name/--description, scope flags, and/or --unset"
@@ -6459,7 +6492,7 @@ function register(program3) {
6459
6492
  }
6460
6493
 
6461
6494
  // src/commands/issues.ts
6462
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, readdirSync, readFileSync as readFileSync4, statSync as statSync2, writeFileSync as writeFileSync2 } from "node:fs";
6495
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, readdirSync, readFileSync as readFileSync5, statSync as statSync2, writeFileSync as writeFileSync2 } from "node:fs";
6463
6496
  import { basename, dirname as dirname2, join as join2 } from "node:path";
6464
6497
  import { createInterface } from "node:readline/promises";
6465
6498
 
@@ -7171,7 +7204,7 @@ function readTextSource(source) {
7171
7204
  if (source.kind === "text-stdin") return readBodyValue("-");
7172
7205
  if (source.kind === "text-path") {
7173
7206
  try {
7174
- return readFileSync4(source.path, "utf8");
7207
+ return readFileSync5(source.path, "utf8");
7175
7208
  } catch (err) {
7176
7209
  die(`cannot read ${source.path}: ${err instanceof Error ? err.message : String(err)}`);
7177
7210
  }
@@ -7189,7 +7222,7 @@ function walkFolder(dir) {
7189
7222
  files2.push({
7190
7223
  path: nextRel,
7191
7224
  contentType: sniffContentType(entry.name),
7192
- bytes: readFileSync4(nextAbs)
7225
+ bytes: readFileSync5(nextAbs)
7193
7226
  });
7194
7227
  }
7195
7228
  }
@@ -7644,6 +7677,9 @@ function register2(program3) {
7644
7677
  console.log(`
7645
7678
  skills: ${context2.skills.map((s) => s.name).join(", ")}`);
7646
7679
  }
7680
+ for (const e of context2.env ?? []) {
7681
+ console.log(`env: ${e.name}${e.secret ? " (secret)" : ""}`);
7682
+ }
7647
7683
  for (const repo of context2.repos) {
7648
7684
  console.log(
7649
7685
  `repo: ${repo.name} ${repo.url}${repo.branch ? `#${repo.branch}` : ""} \u2192 ${repo.dir}/`
@@ -7827,10 +7863,10 @@ files (v${artifact.current_version.version}):`);
7827
7863
  let bytes2;
7828
7864
  if (fromPath === null) {
7829
7865
  if (process.stdin.isTTY) die('"-" reads the file from stdin, but stdin is a terminal');
7830
- bytes2 = readFileSync4(0);
7866
+ bytes2 = readFileSync5(0);
7831
7867
  } else {
7832
7868
  try {
7833
- bytes2 = readFileSync4(fromPath);
7869
+ bytes2 = readFileSync5(fromPath);
7834
7870
  } catch (err) {
7835
7871
  die(`cannot read ${fromPath}: ${err instanceof Error ? err.message : String(err)}`);
7836
7872
  }
@@ -8285,13 +8321,13 @@ function register4(program3) {
8285
8321
  }
8286
8322
 
8287
8323
  // src/commands/login.ts
8288
- import { readFileSync as readFileSync5 } from "node:fs";
8324
+ import { readFileSync as readFileSync6 } from "node:fs";
8289
8325
  function maskKey(key) {
8290
8326
  return `${key.slice(0, 14)}\u2026`;
8291
8327
  }
8292
8328
  function readKeyArg(value) {
8293
8329
  if (value !== "-") return value;
8294
- const key = readFileSync5(0, "utf8").trim();
8330
+ const key = readFileSync6(0, "utf8").trim();
8295
8331
  if (!key) die("no API key on stdin");
8296
8332
  return key;
8297
8333
  }
@@ -8731,7 +8767,7 @@ import {
8731
8767
  createWriteStream,
8732
8768
  existsSync as existsSync5,
8733
8769
  mkdirSync as mkdirSync4,
8734
- readFileSync as readFileSync9,
8770
+ readFileSync as readFileSync10,
8735
8771
  realpathSync,
8736
8772
  rmSync as rmSync3,
8737
8773
  statSync as statSync4,
@@ -8743,11 +8779,11 @@ import { dirname as dirname4, join as join6 } from "node:path";
8743
8779
  import { StringDecoder as StringDecoder2 } from "node:string_decoder";
8744
8780
 
8745
8781
  // src/version.ts
8746
- import { readFileSync as readFileSync6 } from "node:fs";
8782
+ import { readFileSync as readFileSync7 } from "node:fs";
8747
8783
  function cliVersion() {
8748
8784
  try {
8749
8785
  const manifest = new URL("../package.json", import.meta.url);
8750
- return JSON.parse(readFileSync6(manifest, "utf8")).version ?? "0.0.0-unknown";
8786
+ return JSON.parse(readFileSync7(manifest, "utf8")).version ?? "0.0.0-unknown";
8751
8787
  } catch {
8752
8788
  return "0.0.0-unknown";
8753
8789
  }
@@ -9761,7 +9797,7 @@ var EffortCapabilityRefresher = class {
9761
9797
 
9762
9798
  // src/daemon/cli-refresh.ts
9763
9799
  import { spawn as spawn2 } from "node:child_process";
9764
- import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync7 } from "node:fs";
9800
+ import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync8 } from "node:fs";
9765
9801
  import { dirname as dirname3, join as join4 } from "node:path";
9766
9802
 
9767
9803
  // src/daemon/support.ts
@@ -9946,10 +9982,12 @@ var LogBatcher = class {
9946
9982
  constructor(send, opts = {}) {
9947
9983
  this.send = send;
9948
9984
  this.opts = opts;
9985
+ this.redactor = new SecretRedactor(opts.redact ?? []);
9949
9986
  }
9950
9987
  send;
9951
9988
  opts;
9952
9989
  buffer = "";
9990
+ redactor;
9953
9991
  timer = null;
9954
9992
  sending = Promise.resolve();
9955
9993
  seq = 0;
@@ -9957,6 +9995,7 @@ var LogBatcher = class {
9957
9995
  pending = [];
9958
9996
  append(text4) {
9959
9997
  if (!text4) return;
9998
+ text4 = this.redactor.write(text4);
9960
9999
  this.buffer += text4;
9961
10000
  if (Buffer.byteLength(this.buffer, "utf8") >= (this.opts.maxBytes ?? 32 * 1024)) {
9962
10001
  void this.flush();
@@ -9965,6 +10004,11 @@ var LogBatcher = class {
9965
10004
  this.timer.unref?.();
9966
10005
  }
9967
10006
  }
10007
+ /** Release an incomplete final match only after the producer has ended. */
10008
+ finish() {
10009
+ this.buffer += this.redactor.end();
10010
+ return this.flush();
10011
+ }
9968
10012
  /** Sends whatever is buffered now; resolves when every send so far settled. */
9969
10013
  flush() {
9970
10014
  if (this.timer) {
@@ -10077,15 +10121,68 @@ function pathWithin(path2, dir) {
10077
10121
  function buildSpawnEnv(base, opts) {
10078
10122
  const env = {
10079
10123
  ...base,
10124
+ ...Object.fromEntries((opts.extra ?? []).map((e) => [e.name, e.value])),
10080
10125
  TINES_API_KEY: opts.apiKey,
10081
10126
  // Own canonicalization at the final child-process boundary too. This
10082
10127
  // keeps the spawned CLI safe even if a future daemon call site passes
10083
10128
  // the original --url value instead of its normalized local variable.
10084
10129
  TINES_API_URL: opts.apiUrl.replace(/\/+$/, "")
10085
10130
  };
10131
+ if (opts.extra?.some((e) => e.name === "PATH")) env.PATH = base.PATH;
10086
10132
  if (opts.binDir) env.PATH = base.PATH ? `${opts.binDir}${delimiter}${base.PATH}` : opts.binDir;
10087
10133
  return env;
10088
10134
  }
10135
+ function secretForms(secrets) {
10136
+ return [
10137
+ ...new Set(
10138
+ secrets.filter(Boolean).flatMap((secret) => [secret, JSON.stringify(secret).slice(1, -1)])
10139
+ )
10140
+ ].sort((a, b) => b.length - a.length);
10141
+ }
10142
+ function secretPattern(forms) {
10143
+ return forms.length ? new RegExp(forms.map((form) => form.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|"), "g") : null;
10144
+ }
10145
+ function redactSecrets(text4, secrets) {
10146
+ const pattern = secretPattern(secretForms(secrets));
10147
+ return pattern ? text4.replace(pattern, "***") : text4;
10148
+ }
10149
+ var SecretRedactor = class {
10150
+ pending = "";
10151
+ forms;
10152
+ pattern;
10153
+ constructor(secrets) {
10154
+ this.forms = secretForms(secrets);
10155
+ this.pattern = secretPattern(this.forms);
10156
+ }
10157
+ write(chunk) {
10158
+ if (!this.pattern) return chunk;
10159
+ const text4 = this.pending + chunk;
10160
+ let cut = text4.length;
10161
+ for (const form of this.forms) {
10162
+ for (let size = Math.min(form.length - 1, text4.length); size > text4.length - cut; size--) {
10163
+ if (text4.endsWith(form.slice(0, size))) {
10164
+ cut = text4.length - size;
10165
+ break;
10166
+ }
10167
+ }
10168
+ }
10169
+ this.pattern.lastIndex = 0;
10170
+ for (let match; match = this.pattern.exec(text4); ) {
10171
+ if (match.index >= cut) break;
10172
+ if (this.pattern.lastIndex > cut) {
10173
+ cut = match.index;
10174
+ break;
10175
+ }
10176
+ }
10177
+ this.pending = text4.slice(cut);
10178
+ return text4.slice(0, cut).replace(this.pattern, "***");
10179
+ }
10180
+ end() {
10181
+ const text4 = this.pending;
10182
+ this.pending = "";
10183
+ return this.pattern ? text4.replace(this.pattern, "***") : text4;
10184
+ }
10185
+ };
10089
10186
 
10090
10187
  // src/daemon/cli-refresh.ts
10091
10188
  var PACKAGE = "tines";
@@ -10098,7 +10195,7 @@ function binDirOf(prefix) {
10098
10195
  }
10099
10196
  function installedVersion(prefix) {
10100
10197
  try {
10101
- const pkg = readFileSync7(join4(prefix, "node_modules", PACKAGE, "package.json"), "utf8");
10198
+ const pkg = readFileSync8(join4(prefix, "node_modules", PACKAGE, "package.json"), "utf8");
10102
10199
  const version = JSON.parse(pkg).version;
10103
10200
  return typeof version === "string" ? version : null;
10104
10201
  } catch {
@@ -10178,7 +10275,7 @@ function message(err) {
10178
10275
  import { arch, hostname, platform } from "node:os";
10179
10276
 
10180
10277
  // src/daemon/store.ts
10181
- import { readdirSync as readdirSync2, readFileSync as readFileSync8, rmSync as rmSync2, statSync as statSync3 } from "node:fs";
10278
+ import { readdirSync as readdirSync2, readFileSync as readFileSync9, rmSync as rmSync2, statSync as statSync3 } from "node:fs";
10182
10279
  import { join as join5 } from "node:path";
10183
10280
  function credentialsKey(url, name2) {
10184
10281
  return `${url.replace(/\/+$/, "")}#${name2}`;
@@ -10238,10 +10335,10 @@ function saveDaemonDeclines(path2, runIds) {
10238
10335
  }
10239
10336
  function processStartTimeMs(pid) {
10240
10337
  try {
10241
- const stat2 = readFileSync8(`/proc/${pid}/stat`, "utf8");
10338
+ const stat2 = readFileSync9(`/proc/${pid}/stat`, "utf8");
10242
10339
  const afterComm = stat2.slice(stat2.lastIndexOf(")") + 2).split(" ");
10243
10340
  const startTicks = Number(afterComm[19]);
10244
- const btimeLine = readFileSync8("/proc/stat", "utf8").split("\n").find((line) => line.startsWith("btime "));
10341
+ const btimeLine = readFileSync9("/proc/stat", "utf8").split("\n").find((line) => line.startsWith("btime "));
10245
10342
  const btime = Number(btimeLine?.slice("btime ".length));
10246
10343
  if (!Number.isFinite(startTicks) || !Number.isFinite(btime)) return null;
10247
10344
  return btime * 1e3 + startTicks / 100 * 1e3;
@@ -10368,7 +10465,7 @@ async function uploadRawLog(run3) {
10368
10465
  await new Promise((resolve2) => run3.rawSpool?.end(resolve2) ?? resolve2());
10369
10466
  const size = statSync4(path2).size;
10370
10467
  if (size > 0) {
10371
- let body = readFileSync9(path2);
10468
+ let body = readFileSync10(path2);
10372
10469
  if (body.byteLength > RUN_LOG_RAW_MAX_BYTES) {
10373
10470
  const marker = Buffer.from(
10374
10471
  `{"type":"tines_truncated","dropped_bytes":${body.byteLength - RUN_LOG_RAW_MAX_BYTES}}
@@ -10535,7 +10632,7 @@ async function runDaemon(opts) {
10535
10632
  };
10536
10633
  run3.batcher.append(`[usage] ${JSON.stringify(accounting)}
10537
10634
  `);
10538
- await run3.batcher.flush();
10635
+ await run3.batcher.finish();
10539
10636
  const ended = await client2.finishRun(run3.runId, {
10540
10637
  status,
10541
10638
  ...run3.effortEvidence ? { effort_application: run3.effortEvidence } : {},
@@ -10636,6 +10733,7 @@ async function runDaemon(opts) {
10636
10733
  const effortRefresher = new EffortCapabilityRefresher(opts.harness, DAEMON_VERSION);
10637
10734
  let effortCapabilities = await effortRefresher.get();
10638
10735
  const launch = async (assignment) => {
10736
+ const secretEnvValues = (assignment.env ?? []).filter((e) => e.secret).map((e) => e.value);
10639
10737
  const runId = assignment.run.id;
10640
10738
  if (table2.has(runId)) return;
10641
10739
  const resume = assignment.resume && existsSync5(assignment.resume.workspace_path) ? assignment.resume : null;
@@ -10659,10 +10757,13 @@ async function runDaemon(opts) {
10659
10757
  batcher: new LogBatcher(
10660
10758
  (chunk, seq) => client2.appendRunLog(runId, { chunk, seq }).then(() => {
10661
10759
  }),
10662
- { onError: (err) => log(`log append for run ${runId} failed: ${message2(err)}`) }
10760
+ {
10761
+ onError: (err) => log(`log append for run ${runId} failed: ${message2(err)}`),
10762
+ ...secretEnvValues.length > 0 ? { redact: secretEnvValues } : {}
10763
+ }
10663
10764
  )
10664
10765
  };
10665
- run3.flush = () => run3.batcher.flush();
10766
+ run3.flush = () => run3.batcher.finish();
10666
10767
  table2.track(run3);
10667
10768
  if (assignment.effort) effortCapabilities = await effortRefresher.get(true);
10668
10769
  const effortRejection = assignmentEffortRejection(assignment, effortCapabilities, opts.harness);
@@ -10760,8 +10861,15 @@ async function runDaemon(opts) {
10760
10861
  const spawnEnv = buildSpawnEnv(process.env, {
10761
10862
  binDir: cli.binDir,
10762
10863
  apiKey: assignment.run_key,
10763
- apiUrl: baseUrl
10864
+ apiUrl: baseUrl,
10865
+ ...assignment.env ? { extra: assignment.env } : {}
10764
10866
  });
10867
+ if (assignment.env?.length) {
10868
+ run3.batcher.append(
10869
+ `[env] ${assignment.env.length} environment variable(s) set: ${assignment.env.map((e) => `${e.name}${e.secret ? " (secret)" : ""}`).join(", ")}
10870
+ `
10871
+ );
10872
+ }
10765
10873
  if (opts.harness === "codex") run3.codexHome = resolveCodexHome(spawnEnv, workspace);
10766
10874
  const child = spawn3(invocation.file, invocation.args, {
10767
10875
  cwd: workspace,
@@ -10800,14 +10908,20 @@ async function runDaemon(opts) {
10800
10908
  run3.rawSpool = createWriteStream(spoolPath);
10801
10909
  run3.rawUpload = (body) => client2.putRunLogRaw(runId, body);
10802
10910
  const decoder = new StringDecoder2("utf8");
10911
+ const rawRedactor = new SecretRedactor(secretEnvValues);
10803
10912
  run3.drain = () => {
10804
10913
  const trailing = decoder.end();
10805
- if (trailing) renderer.write(trailing);
10914
+ if (trailing) {
10915
+ run3.rawSpool?.write(rawRedactor.write(trailing));
10916
+ renderer.write(trailing);
10917
+ }
10918
+ run3.rawSpool?.write(rawRedactor.end());
10806
10919
  renderer.finish();
10807
10920
  };
10808
10921
  child.stdout?.on("data", (data) => {
10809
- run3.rawSpool?.write(data);
10810
- renderer.write(decoder.write(data));
10922
+ const text4 = decoder.write(data);
10923
+ run3.rawSpool?.write(rawRedactor.write(text4));
10924
+ renderer.write(text4);
10811
10925
  });
10812
10926
  } else if (opts.harness === "codex") {
10813
10927
  const renderer = new CodexStreamRenderer((line) => run3.batcher.append(line));
@@ -10820,11 +10934,26 @@ async function runDaemon(opts) {
10820
10934
  };
10821
10935
  child.stdout?.on("data", (data) => renderer.write(decoder.write(data)));
10822
10936
  } else {
10823
- child.stdout?.on("data", (data) => run3.batcher.append(data.toString("utf8")));
10937
+ const decoder = new StringDecoder2("utf8");
10938
+ const redactor = new SecretRedactor(secretEnvValues);
10939
+ child.stdout?.on(
10940
+ "data",
10941
+ (data) => run3.batcher.append(redactor.write(decoder.write(data)))
10942
+ );
10943
+ run3.drain = () => {
10944
+ run3.batcher.append(redactor.write(decoder.end()) + redactor.end());
10945
+ };
10824
10946
  }
10947
+ const stderrDecoder = new StringDecoder2("utf8");
10948
+ const stderrRedactor = new SecretRedactor(secretEnvValues);
10949
+ const drainStdout = run3.drain;
10950
+ run3.drain = () => {
10951
+ drainStdout?.();
10952
+ run3.batcher.append(stderrRedactor.write(stderrDecoder.end()) + stderrRedactor.end());
10953
+ };
10825
10954
  child.stderr?.on("data", (data) => {
10826
- const text4 = data.toString("utf8");
10827
- run3.batcher.append(text4);
10955
+ const text4 = stderrDecoder.write(data);
10956
+ run3.batcher.append(stderrRedactor.write(text4));
10828
10957
  run3.limiter?.noteStderr(text4);
10829
10958
  });
10830
10959
  run3.timeout = setTimeout(() => {
@@ -10835,7 +10964,7 @@ async function runDaemon(opts) {
10835
10964
  if (child.pid) setTimeout(() => killTree(child.pid, "SIGKILL"), 5e3).unref?.();
10836
10965
  }, assignment.timeout_minutes * 6e4);
10837
10966
  child.on("error", (err) => {
10838
- const reason = `failed to launch harness: ${message2(err)}`;
10967
+ const reason = redactSecrets(`failed to launch harness: ${message2(err)}`, secretEnvValues);
10839
10968
  if (assignment.effort)
10840
10969
  run3.effortEvidence = {
10841
10970
  status: "rejected",
@@ -10870,18 +10999,16 @@ async function runDaemon(opts) {
10870
10999
  const limited = signal ? null : run3.limiter?.signal() ?? null;
10871
11000
  const providerError = signal ? null : run3.limiter?.providerError() ?? null;
10872
11001
  if (limited) {
10873
- log(
10874
- `run ${runId}: harness rate limited (${limited.detail}); reporting without a strike`
10875
- );
10876
- void table2.finishAndCleanup(run3, "failed", `rate limited: ${limited.detail}`, {
11002
+ const detail = redactSecrets(limited.detail, secretEnvValues);
11003
+ log(`run ${runId}: harness rate limited (${detail}); reporting without a strike`);
11004
+ void table2.finishAndCleanup(run3, "failed", `rate limited: ${detail}`, {
10877
11005
  judgment: "rate_limited",
10878
11006
  ...limited.resumeAt !== null ? { resume_at: limited.resumeAt } : {}
10879
11007
  });
10880
11008
  } else if (providerError) {
10881
- log(
10882
- `run ${runId}: transient provider error (${providerError.detail}); reporting without a strike`
10883
- );
10884
- void table2.finishAndCleanup(run3, "failed", `provider error: ${providerError.detail}`, {
11009
+ const detail = redactSecrets(providerError.detail, secretEnvValues);
11010
+ log(`run ${runId}: transient provider error (${detail}); reporting without a strike`);
11011
+ void table2.finishAndCleanup(run3, "failed", `provider error: ${detail}`, {
10885
11012
  judgment: "interrupted"
10886
11013
  });
10887
11014
  } else {
@@ -10894,7 +11021,11 @@ async function runDaemon(opts) {
10894
11021
  }
10895
11022
  });
10896
11023
  } catch (err) {
10897
- void table2.finishAndCleanup(run3, "failed", `workspace setup failed: ${message2(err)}`);
11024
+ void table2.finishAndCleanup(
11025
+ run3,
11026
+ "failed",
11027
+ redactSecrets(`workspace setup failed: ${message2(err)}`, secretEnvValues)
11028
+ );
10898
11029
  }
10899
11030
  };
10900
11031
  const shutdown = async () => {
@@ -10934,6 +11065,8 @@ async function runDaemon(opts) {
10934
11065
  },
10935
11066
  ...declinedAssignments.size > 0 ? { declined_assignments: [...declinedAssignments].slice(0, 100) } : {},
10936
11067
  ...effortCapabilities ? { effort_capabilities: effortCapabilities } : {},
11068
+ // Capability: this daemon merges assignment.env into the harness environment.
11069
+ env_delivery: 1,
10937
11070
  // Stated on every poll while pending; absent otherwise, which
10938
11071
  // the server reads as "not draining" — so a daemon that died
10939
11072
  // mid-drain cannot pin its runner shut past its relaunch.
@@ -11112,7 +11245,7 @@ function harnessFlag(harness) {
11112
11245
 
11113
11246
  // src/commands/runner-service.ts
11114
11247
  import { execFile as execFile2 } from "node:child_process";
11115
- import { existsSync as existsSync7, mkdirSync as mkdirSync5, readFileSync as readFileSync10, rmSync as rmSync4, statSync as statSync6, writeFileSync as writeFileSync4 } from "node:fs";
11248
+ import { existsSync as existsSync7, mkdirSync as mkdirSync5, readFileSync as readFileSync11, rmSync as rmSync4, statSync as statSync6, writeFileSync as writeFileSync4 } from "node:fs";
11116
11249
  import { homedir as homedir3, hostname as hostname3, userInfo } from "node:os";
11117
11250
  import { dirname as dirname6 } from "node:path";
11118
11251
  import { promisify } from "node:util";
@@ -11359,7 +11492,7 @@ async function waitForStart(logPath, offset, name2) {
11359
11492
  const deadline = Date.now() + STARTUP_TIMEOUT_MS;
11360
11493
  while (Date.now() < deadline) {
11361
11494
  if (existsSync7(logPath)) {
11362
- const content = readFileSync10(logPath, "utf8").slice(offset);
11495
+ const content = readFileSync11(logPath, "utf8").slice(offset);
11363
11496
  if (pattern.test(content)) return true;
11364
11497
  }
11365
11498
  await new Promise((resolve2) => setTimeout(resolve2, 500));
@@ -12612,18 +12745,18 @@ function register10(program3) {
12612
12745
  }
12613
12746
 
12614
12747
  // src/commands/workflows.ts
12615
- import { readFileSync as readFileSync12 } from "node:fs";
12748
+ import { readFileSync as readFileSync13 } from "node:fs";
12616
12749
  import { createInterface as createInterface5 } from "node:readline/promises";
12617
12750
 
12618
12751
  // src/workflow-packages.ts
12619
12752
  import { createInterface as createInterface4 } from "node:readline/promises";
12620
12753
  import { createHash as createHash2 } from "node:crypto";
12621
- import { readFileSync as readFileSync11 } from "node:fs";
12754
+ import { readFileSync as readFileSync12 } from "node:fs";
12622
12755
  var PACKAGE_STDIN = "-";
12623
12756
  var MAX_LOCAL_JSON_BYTES = 32 * 1024 * 1024;
12624
12757
  function readPackageSource(path2) {
12625
12758
  try {
12626
- const bytes2 = readFileSync11(path2 === PACKAGE_STDIN ? 0 : path2);
12759
+ const bytes2 = readFileSync12(path2 === PACKAGE_STDIN ? 0 : path2);
12627
12760
  if (bytes2.byteLength > MAX_LOCAL_JSON_BYTES)
12628
12761
  throw new Error(`input exceeds ${MAX_LOCAL_JSON_BYTES} bytes`);
12629
12762
  return new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes2);
@@ -13262,14 +13395,14 @@ function readJsonBody(inline, file) {
13262
13395
  if (file !== void 0 && file !== "-") {
13263
13396
  let raw;
13264
13397
  try {
13265
- raw = readFileSync12(file, "utf8");
13398
+ raw = readFileSync13(file, "utf8");
13266
13399
  } catch (err) {
13267
13400
  die(`cannot read ${file}: ${err instanceof Error ? err.message : String(err)}`);
13268
13401
  }
13269
13402
  return parseJsonObject(raw, file);
13270
13403
  }
13271
13404
  if (file === "-" || !process.stdin.isTTY) {
13272
- const raw = readFileSync12(0, "utf8");
13405
+ const raw = readFileSync13(0, "utf8");
13273
13406
  if (raw.trim() === "") {
13274
13407
  if (file === "-") die("no JSON on stdin");
13275
13408
  return void 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tines",
3
- "version": "0.0.265",
3
+ "version": "0.0.266",
4
4
  "description": "CLI for Tines, an orchestration layer for AI agents",
5
5
  "repository": {
6
6
  "type": "git",