@tablation/crew 0.1.6 → 0.2.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 (3) hide show
  1. package/README.md +1 -1
  2. package/dist/cli.js +65 -17
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -211,7 +211,7 @@ crew pause|resume [route] [R] pause everything, or one role
211
211
  crew log [route] tail the log
212
212
  crew inbox [--member NAME] your tickets across every workspace
213
213
  crew connect WS[/PROJECT] resolve a workspace/project's ids into the state tree
214
- crew repos add ROUTE PATH attach a local checkout to a route in crew.yaml, creating its Repos row if missing (--dry-run previews)
214
+ crew repos add [ROUTE] [NAME] [PATH] attach a local checkout to a route in crew.yaml, creating its Repos row if missing (--dry-run previews)
215
215
  crew repos list ROUTE the checkouts a route has, and the tracker Repos row each matches
216
216
  crew install write and load this platform's scheduler unit
217
217
  crew uninstall unload and remove it
package/dist/cli.js CHANGED
@@ -2793,6 +2793,12 @@ function gitOk(cwd, args) {
2793
2793
  return null;
2794
2794
  }
2795
2795
  }
2796
+ function commitDate(cwd, sha) {
2797
+ const out = gitOk(cwd, ["show", "-s", "--format=%cI", sha]);
2798
+ if (!out) return null;
2799
+ const t = new Date(out);
2800
+ return Number.isNaN(t.getTime()) ? null : t.toISOString();
2801
+ }
2796
2802
  var currentBranch = (cwd) => git(cwd, ["rev-parse", "--abbrev-ref", "HEAD"]);
2797
2803
  var headSha = (cwd) => git(cwd, ["rev-parse", "HEAD"]);
2798
2804
  function status(cwd) {
@@ -4980,7 +4986,10 @@ function planStamp(cwd, tickets, contract, fromSha, toSha, merged = /* @__PURE__
4980
4986
  return null;
4981
4987
  }).filter((p) => p !== null);
4982
4988
  }
4983
- async function applyStamp(tracker, plan, version, contract, emit, dryRun) {
4989
+ function mergedAtFor(cwd, sha, fallback) {
4990
+ return cwd && commitDate(cwd, sha) || fallback;
4991
+ }
4992
+ async function applyStamp(tracker, plan, version, contract, emit, dryRun, cwd) {
4984
4993
  let stamped = 0;
4985
4994
  const at = (/* @__PURE__ */ new Date()).toISOString();
4986
4995
  for (const { ticket, sha } of plan) {
@@ -5000,10 +5009,9 @@ async function applyStamp(tracker, plan, version, contract, emit, dryRun) {
5000
5009
  ...version ? { released_version: version } : {},
5001
5010
  // Written here rather than only on the external path (below): a
5002
5011
  // ticket's record should mean the same thing regardless of which
5003
- // release mode closed it (ISSUE-218). `merged_at` is stamped
5004
- // alongside — not read off the commit itself — since that is the
5005
- // moment the tracker learns of it, same as the external path.
5006
- ...sha ? { commit_sha: sha, merged_at: at } : {}
5012
+ // release mode closed it (ISSUE-218). `merged_at` is the commit's own
5013
+ // date, not the stamp time (which trails the deploy in ci_* modes).
5014
+ ...sha ? { commit_sha: sha, merged_at: mergedAtFor(cwd, sha, at) } : {}
5007
5015
  });
5008
5016
  emit.emit(`stamped ${contract.statuses.deployed}${version ? ` (${version})` : ""}${shaNote}`, {
5009
5017
  ticket: ticket.issue_id
@@ -5015,7 +5023,7 @@ async function applyStamp(tracker, plan, version, contract, emit, dryRun) {
5015
5023
  }
5016
5024
  return stamped;
5017
5025
  }
5018
- async function applyExternalClosures(tracker, closures, contract, emit, dryRun) {
5026
+ async function applyExternalClosures(tracker, closures, contract, emit, dryRun, cwd) {
5019
5027
  if (!closures?.length) return 0;
5020
5028
  let stamped = 0;
5021
5029
  const at = (/* @__PURE__ */ new Date()).toISOString();
@@ -5030,7 +5038,7 @@ async function applyExternalClosures(tracker, closures, contract, emit, dryRun)
5030
5038
  try {
5031
5039
  await tracker.updateTicket(ticket.id, {
5032
5040
  commit_sha: closure.mergedAt,
5033
- merged_at: at,
5041
+ merged_at: mergedAtFor(cwd, closure.mergedAt, at),
5034
5042
  ...confirmed ? { status: contract.statuses.deployed, released_at: at, ...version ? { released_version: version } : {} } : {}
5035
5043
  });
5036
5044
  emit.emit(`stamped commit_sha (${closure.mergedAt.slice(0, 8)})${shippedNote}`, { ticket: ticket.issue_id });
@@ -7034,6 +7042,27 @@ function matchRepoName(origin, resolved) {
7034
7042
  }
7035
7043
  return void 0;
7036
7044
  }
7045
+ function routeNamesOf(text) {
7046
+ const routes = parseDocument(text).get("routes", true);
7047
+ if (!isSeq(routes)) return [];
7048
+ return routes.items.flatMap((i) => isMap(i) && typeof i.get("route") === "string" ? [i.get("route")] : []);
7049
+ }
7050
+ function parseAddArgs(args, routes, isDir) {
7051
+ const rest = [...args];
7052
+ let route;
7053
+ if (rest.length > 0 && routes.includes(rest[0])) route = rest.shift();
7054
+ else if (rest.length > 0 && /^[^/\s]+\/[^/\s]+$/.test(rest[0]) && !isDir(rest[0])) {
7055
+ throw new ReposError(`no route "${rest[0]}" in the config (have: ${routes.join(", ") || "none"})`);
7056
+ }
7057
+ if (!route) {
7058
+ if (routes.length === 1) route = routes[0];
7059
+ else throw new ReposError(routes.length === 0 ? "the config has no routes \u2014 run `crew connect` first" : `several routes are configured; name one: ${routes.join(", ")}`);
7060
+ }
7061
+ if (rest.length > 2) throw new ReposError("too many arguments: expected [route] [name] [path]");
7062
+ if (rest.length === 2) return { route, name: rest[0], path: rest[1] };
7063
+ if (rest.length === 1) return isDir(rest[0]) ? { route, path: rest[0] } : { route, name: rest[0], path: "." };
7064
+ return { route, path: "." };
7065
+ }
7037
7066
  function expandDir(p, base) {
7038
7067
  const home = p === "~" ? homedir3() : p.startsWith("~/") ? `${homedir3()}/${p.slice(2)}` : p;
7039
7068
  return resolve3(base, home);
@@ -7073,7 +7102,7 @@ function planRepoAdd(i) {
7073
7102
  throw new ReposError(`${target} is already configured on route ${i.route}`);
7074
7103
  }
7075
7104
  const matched = matchRepoName(i.origin, i.resolved);
7076
- const name = matched ?? i.name ?? basename5(target);
7105
+ const name = matched ?? i.name ?? normalizeRemote(i.origin)?.split("/").pop() ?? basename5(target);
7077
7106
  let warning;
7078
7107
  if (!matched) {
7079
7108
  const known = Object.values(i.resolved?.repoNames ?? {});
@@ -8319,7 +8348,7 @@ function usage() {
8319
8348
  crew agents sync [route] [--force] push crew's personas AND skill files into the workspace Agents/Agent Skills tables (--force overwrites even a diverged persona)
8320
8349
  crew agents prompt R [route] print one persona's current prompt (e.g. R=pair, for a SessionStart hook)
8321
8350
  crew skills sync [route] push only crew's skill files (e.g. grill-me) into the workspace Agent Skills table
8322
- crew repos add ROUTE PATH attach a local checkout to a route in crew.yaml (--dry-run previews)
8351
+ crew repos add [ROUTE] [NAME] [PATH] attach a local checkout to a route in crew.yaml (--dry-run previews)
8323
8352
  crew repos list ROUTE the checkouts a route has, with the tracker's Repos row each matches
8324
8353
  crew logbook list [route] recent Agent Log entries, filterable by --role/--ticket
8325
8354
  crew logbook show [route] ID [--prompt] one entry; --prompt reconstructs and verifies its prompt
@@ -8822,12 +8851,12 @@ ${changed2.map((c) => ` - ${c}
8822
8851
  outcome.decision.head,
8823
8852
  new Map(outcome.merged.map((m) => [m.ticket.issue_id, m.sha]))
8824
8853
  );
8825
- if (plan.length) await applyStamp(tracker, plan, outcome.version, tracker.contract, remit, dryRun);
8854
+ if (plan.length) await applyStamp(tracker, plan, outcome.version, tracker.contract, remit, dryRun, target.dir);
8826
8855
  } else if (outcome.stopped) {
8827
8856
  remit.emit(`nothing stamped \u2014 ${outcome.stopped}`);
8828
8857
  }
8829
8858
  if (outcome.externalClosures?.length) {
8830
- await applyExternalClosures(tracker, outcome.externalClosures, tracker.contract, remit, dryRun);
8859
+ await applyExternalClosures(tracker, outcome.externalClosures, tracker.contract, remit, dryRun, target.dir);
8831
8860
  }
8832
8861
  const news = describeRelease(outcome, scope);
8833
8862
  if (news) {
@@ -9605,12 +9634,31 @@ ${changed2.map((c) => ` - ${c}
9605
9634
  }
9606
9635
  case "repos": {
9607
9636
  const sub = positional[1];
9608
- const routeName = positional[2];
9609
9637
  const usage2 = () => {
9610
- process.stderr.write("crew repos add <workspace/project> <path> [--name N] [--project P] [--dry-run] [--no-create]\ncrew repos list <workspace/project>\n");
9638
+ process.stderr.write("crew repos add [workspace/project] [name] [path] [--name N] [--project P] [--dry-run] [--no-create]\ncrew repos list <workspace/project>\n");
9611
9639
  process.exit(2);
9612
9640
  };
9613
- if (sub !== "add" && sub !== "list" || !routeName || sub === "add" && !positional[3]) usage2();
9641
+ if (sub !== "add" && sub !== "list") usage2();
9642
+ const configText = readFileSync12(cfg.configFile, "utf8");
9643
+ let routeName = positional[2];
9644
+ let addPath = ".";
9645
+ let addName;
9646
+ if (sub === "list" && !routeName) usage2();
9647
+ if (sub === "add") {
9648
+ try {
9649
+ const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) => existsSync18(resolvePath(p.replace(/^~(?=\/|$)/, homedir4()))));
9650
+ routeName = a.route;
9651
+ addPath = a.path;
9652
+ addName = a.name;
9653
+ } catch (e) {
9654
+ if (e instanceof ReposError) {
9655
+ process.stderr.write(`crew repos: ${e.message}
9656
+ `);
9657
+ process.exit(2);
9658
+ }
9659
+ throw e;
9660
+ }
9661
+ }
9614
9662
  const resolvedFile = resolvedPathFor(cfg.ship.stateDir, routeName);
9615
9663
  let resolvedRemotes;
9616
9664
  if (existsSync18(resolvedFile)) {
@@ -9620,7 +9668,7 @@ ${changed2.map((c) => ` - ${c}
9620
9668
  }
9621
9669
  }
9622
9670
  const originOf = (dir) => gitOk(dir, ["remote", "get-url", "origin"]) ?? void 0;
9623
- const text = readFileSync12(cfg.configFile, "utf8");
9671
+ const text = configText;
9624
9672
  const base = dirname9(cfg.configFile);
9625
9673
  try {
9626
9674
  if (sub === "list") {
@@ -9636,7 +9684,7 @@ ${changed2.map((c) => ` - ${c}
9636
9684
  }
9637
9685
  break;
9638
9686
  }
9639
- const target = resolvePath(positional[3].replace(/^~(?=\/|$)/, homedir4()));
9687
+ const target = resolvePath(addPath.replace(/^~(?=\/|$)/, homedir4()));
9640
9688
  const top = existsSync18(target) ? gitOk(target, ["rev-parse", "--show-toplevel"]) : null;
9641
9689
  if (!top) throw new ReposError(`${target} is not a git checkout`);
9642
9690
  const origin = originOf(top);
@@ -9647,7 +9695,7 @@ ${changed2.map((c) => ` - ${c}
9647
9695
  path: top,
9648
9696
  origin,
9649
9697
  resolved: resolvedRemotes,
9650
- name: value("name"),
9698
+ name: value("name") ?? addName,
9651
9699
  nameForExisting: (d) => matchRepoName(originOf(d), resolvedRemotes)
9652
9700
  });
9653
9701
  let rowTracker;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tablation/crew",
3
- "version": "0.1.6",
3
+ "version": "0.2.0",
4
4
  "description": "A standing team of headless agents that picks work off a Tablation board, does it on a machine you control, and reports back.",
5
5
  "type": "module",
6
6
  "license": "MIT",