@tablation/crew 0.6.1 → 0.7.1
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/cli.js
CHANGED
|
@@ -824,6 +824,7 @@ function parseResolved(raw, m, where) {
|
|
|
824
824
|
operator: m.req(raw.operator, `${where}.resolved.operator`),
|
|
825
825
|
holds: holds2,
|
|
826
826
|
contract: raw.contract ?? void 0,
|
|
827
|
+
...typeof raw.releaseSeat === "string" && raw.releaseSeat ? { releaseSeat: raw.releaseSeat } : {},
|
|
827
828
|
...Object.keys(agentPersonas).length > 0 ? { agentPersonas } : {},
|
|
828
829
|
...Object.keys(agentSkills).length > 0 ? { agentSkills } : {}
|
|
829
830
|
};
|
|
@@ -1083,6 +1084,10 @@ async function hydrateApiKeys(routes, getStore = getSessionStore) {
|
|
|
1083
1084
|
}
|
|
1084
1085
|
}
|
|
1085
1086
|
}
|
|
1087
|
+
function releaseSeat(rt) {
|
|
1088
|
+
const r = rt.resolved;
|
|
1089
|
+
return r?.releaseSeat ?? r?.seats.dev ?? r?.seats.qa ?? "";
|
|
1090
|
+
}
|
|
1086
1091
|
function configuredMembers(rt) {
|
|
1087
1092
|
const r = rt.resolved;
|
|
1088
1093
|
if (!r) throw new ConfigError(`route "${rt.route}" has no resolved ids \u2014 run \`crew connect\``);
|
|
@@ -1675,7 +1680,9 @@ var DEFAULT_CONTRACT = {
|
|
|
1675
1680
|
updatedAt: "updated_at",
|
|
1676
1681
|
needsDesign: "needs_design",
|
|
1677
1682
|
reportType: "report_type",
|
|
1678
|
-
epic: "epic_id"
|
|
1683
|
+
epic: "epic_id",
|
|
1684
|
+
author: "filed_by_id",
|
|
1685
|
+
ship: "ship_id"
|
|
1679
1686
|
},
|
|
1680
1687
|
comments: {
|
|
1681
1688
|
parent: "ticket_id",
|
|
@@ -1683,7 +1690,8 @@ var DEFAULT_CONTRACT = {
|
|
|
1683
1690
|
author: "team_member_id",
|
|
1684
1691
|
kind: "kind",
|
|
1685
1692
|
eventKind: "event",
|
|
1686
|
-
createdAt: "created_at"
|
|
1693
|
+
createdAt: "created_at",
|
|
1694
|
+
ship: "ship_id"
|
|
1687
1695
|
},
|
|
1688
1696
|
statuses: {
|
|
1689
1697
|
open: ["new", "accepted", "blocked", "in_progress", "needs_info", "fixed", "qa", "verified"],
|
|
@@ -1982,6 +1990,13 @@ function findOnPath(bin, searchPath) {
|
|
|
1982
1990
|
}
|
|
1983
1991
|
return void 0;
|
|
1984
1992
|
}
|
|
1993
|
+
var COMMON_DOCKER_DIRS = ["/usr/local/bin", "/opt/homebrew/bin", join5(homedir2(), ".docker", "bin")].join(":");
|
|
1994
|
+
function dockerPathProblem(ship, shellPath = process.env.PATH ?? "") {
|
|
1995
|
+
const schedulerPath = pathFor(ship);
|
|
1996
|
+
if (findOnPath("docker", schedulerPath)) return void 0;
|
|
1997
|
+
const inShell = findOnPath("docker", shellPath);
|
|
1998
|
+
return inShell ? `\`docker\` is not on the scheduler PATH (${schedulerPath}) but is at ${inShell} in your shell; add ${dirname3(inShell)} to ship.extraPath in crew.yaml and run \`crew install\`` : `\`docker\` is not on the scheduler PATH (${schedulerPath}) or your shell PATH; install Docker, then add its directory to ship.extraPath in crew.yaml and run \`crew install\``;
|
|
1999
|
+
}
|
|
1985
2000
|
function xmlEscape(s) {
|
|
1986
2001
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
1987
2002
|
}
|
|
@@ -2471,6 +2486,12 @@ var Tracker = class {
|
|
|
2471
2486
|
client;
|
|
2472
2487
|
models;
|
|
2473
2488
|
userAgent;
|
|
2489
|
+
shipName;
|
|
2490
|
+
/**
|
|
2491
|
+
* The once-per-process authorship probe (CREW-1371), memoised as a promise
|
|
2492
|
+
* so concurrent first writes share one round of field-metadata reads.
|
|
2493
|
+
*/
|
|
2494
|
+
authorship;
|
|
2474
2495
|
stateDir;
|
|
2475
2496
|
constructor(route, ship) {
|
|
2476
2497
|
this.route = route;
|
|
@@ -2482,6 +2503,7 @@ var Tracker = class {
|
|
|
2482
2503
|
this.models = route.resolved.models;
|
|
2483
2504
|
this.contract = resolveContract(route.contract);
|
|
2484
2505
|
this.userAgent = ship.userAgent;
|
|
2506
|
+
this.shipName = ship.name;
|
|
2485
2507
|
this.stateDir = ship.stateDir;
|
|
2486
2508
|
this.client = new TablationClient({
|
|
2487
2509
|
baseUrl: `${route.baseUrl}/api`,
|
|
@@ -2834,13 +2856,60 @@ var Tracker = class {
|
|
|
2834
2856
|
* someone else" wake signal.
|
|
2835
2857
|
*/
|
|
2836
2858
|
async postEvent(ticketId, body, memberId) {
|
|
2859
|
+
const auth = await this.authorshipProbe();
|
|
2837
2860
|
await this.client.records.create(this.models.comments, {
|
|
2838
2861
|
ticket_id: ticketId,
|
|
2839
2862
|
body,
|
|
2840
2863
|
team_member_id: memberId,
|
|
2841
|
-
kind: "event"
|
|
2864
|
+
kind: "event",
|
|
2865
|
+
...auth.commentShip && auth.shipId ? { [this.contract.comments.ship]: auth.shipId } : {}
|
|
2842
2866
|
});
|
|
2843
2867
|
}
|
|
2868
|
+
/**
|
|
2869
|
+
* The authorship probe (CREW-1371), read once per process: which optional
|
|
2870
|
+
* `filed_by_id`/`ship_id` columns exist on Issues and Comments, and this
|
|
2871
|
+
* ship's own Ships row id. Memoised as a promise so concurrent first writes
|
|
2872
|
+
* share one round of metadata reads. A table whose metadata cannot be read
|
|
2873
|
+
* reads as "column absent", the same way `connect.ts`'s `owner_id` probe does.
|
|
2874
|
+
*/
|
|
2875
|
+
authorshipProbe() {
|
|
2876
|
+
this.authorship ??= this.probeAuthorship();
|
|
2877
|
+
return this.authorship;
|
|
2878
|
+
}
|
|
2879
|
+
async probeAuthorship() {
|
|
2880
|
+
const [issueFields, commentFields, shipRow] = await Promise.all([
|
|
2881
|
+
this.columnNames(this.models.issues),
|
|
2882
|
+
this.columnNames(this.models.comments),
|
|
2883
|
+
// No name (a caller that only beats or patches, never stamps) means no row to find.
|
|
2884
|
+
this.shipName ? this.myShipRow(this.shipName) : Promise.resolve(void 0)
|
|
2885
|
+
]);
|
|
2886
|
+
return {
|
|
2887
|
+
issueAuthor: issueFields.has(this.contract.columns.author),
|
|
2888
|
+
issueShip: issueFields.has(this.contract.columns.ship),
|
|
2889
|
+
commentShip: commentFields.has(this.contract.comments.ship),
|
|
2890
|
+
shipId: shipRow?.id ?? null
|
|
2891
|
+
};
|
|
2892
|
+
}
|
|
2893
|
+
async columnNames(modelId) {
|
|
2894
|
+
try {
|
|
2895
|
+
const model = await this.client.dataModels.get(modelId);
|
|
2896
|
+
return new Set(model.fields.map((f) => f.columnName));
|
|
2897
|
+
} catch {
|
|
2898
|
+
return /* @__PURE__ */ new Set();
|
|
2899
|
+
}
|
|
2900
|
+
}
|
|
2901
|
+
/**
|
|
2902
|
+
* The authorship columns this workspace is missing, as `table.column` names
|
|
2903
|
+
* for `crew doctor` to report (CREW-1371). Empty on a fully updated install.
|
|
2904
|
+
*/
|
|
2905
|
+
async missingAuthorshipColumns() {
|
|
2906
|
+
const auth = await this.authorshipProbe();
|
|
2907
|
+
const missing = [];
|
|
2908
|
+
if (!auth.issueAuthor) missing.push(`Issues.${this.contract.columns.author}`);
|
|
2909
|
+
if (!auth.issueShip) missing.push(`Issues.${this.contract.columns.ship}`);
|
|
2910
|
+
if (!auth.commentShip) missing.push(`Comments.${this.contract.comments.ship}`);
|
|
2911
|
+
return missing;
|
|
2912
|
+
}
|
|
2844
2913
|
/**
|
|
2845
2914
|
* The public `/v/<slug>/r/<token>` link for one ticket (ISSUE-928) —
|
|
2846
2915
|
* fetch-or-create via `POST /views/:viewId/record-links`, so a repeated
|
|
@@ -2882,8 +2951,13 @@ var Tracker = class {
|
|
|
2882
2951
|
* not among them (`postEvent` above hardcodes its own fields the same
|
|
2883
2952
|
* way).
|
|
2884
2953
|
*/
|
|
2885
|
-
async fileTicket(fields) {
|
|
2886
|
-
|
|
2954
|
+
async fileTicket(fields, memberId) {
|
|
2955
|
+
const auth = await this.authorshipProbe();
|
|
2956
|
+
return this.client.records.create(this.models.issues, {
|
|
2957
|
+
...fields,
|
|
2958
|
+
...auth.issueAuthor && memberId ? { [this.contract.columns.author]: memberId } : {},
|
|
2959
|
+
...auth.issueShip && auth.shipId ? { [this.contract.columns.ship]: auth.shipId } : {}
|
|
2960
|
+
});
|
|
2887
2961
|
}
|
|
2888
2962
|
};
|
|
2889
2963
|
|
|
@@ -2933,7 +3007,8 @@ function rosterMarkdown(roster, meId) {
|
|
|
2933
3007
|
`You are **${selfName}**. Address your shipmates by name in ticket`,
|
|
2934
3008
|
"comments the way you would a colleague; add the role in parentheses only",
|
|
2935
3009
|
"where a reader would otherwise not know which seat you mean. Your own",
|
|
2936
|
-
"row id is what you write to `assignee_id` and `team_member_id
|
|
3010
|
+
"row id is what you write to `assignee_id` and `team_member_id`, and to",
|
|
3011
|
+
"`filed_by_id` on a ticket you file (where the Environment section names it).",
|
|
2937
3012
|
"",
|
|
2938
3013
|
"| seat | name | Crew row id |",
|
|
2939
3014
|
"|---|---|---|"
|
|
@@ -3390,20 +3465,36 @@ function attentionTransitions(previous, current) {
|
|
|
3390
3465
|
function displayKey2(t) {
|
|
3391
3466
|
return t.issue_tag ?? t.issue_id ?? "?";
|
|
3392
3467
|
}
|
|
3393
|
-
function author(c,
|
|
3468
|
+
function author(c, i) {
|
|
3469
|
+
const { me, roster } = i;
|
|
3394
3470
|
if (c.team_member_id === me) return "you";
|
|
3395
3471
|
if (c.team_member_id === null || c.team_member_id === void 0) {
|
|
3396
3472
|
const byIdentity = memberByIdentity(roster, c.created_by_id);
|
|
3397
3473
|
if (byIdentity) return byIdentity.id === me ? "you" : crewLabel(byIdentity) ?? byIdentity.name;
|
|
3398
3474
|
return `${c.reporter_name ?? "anon"} (no identity)`;
|
|
3399
3475
|
}
|
|
3400
|
-
return crewLabel(roster.get(c.team_member_id)) ?? "someone off this ship";
|
|
3476
|
+
return (crewLabel(roster.get(c.team_member_id)) ?? "someone off this ship") + shipSuffix(c, i);
|
|
3477
|
+
}
|
|
3478
|
+
function shipSuffix(c, i) {
|
|
3479
|
+
if (!c.ship_id || c.ship_id === i.myShipId) return "";
|
|
3480
|
+
const name = i.ships?.find((s) => s.id === c.ship_id)?.name;
|
|
3481
|
+
return name ? ` (${name})` : "";
|
|
3482
|
+
}
|
|
3483
|
+
function filedBy(t, i) {
|
|
3484
|
+
const member = (t.filed_by_id ? i.roster.get(t.filed_by_id) : void 0) ?? memberByIdentity(i.roster, t.created_by_id);
|
|
3485
|
+
if (member) {
|
|
3486
|
+
if (member.id === i.me) return "you";
|
|
3487
|
+
const label = crewLabel(member) ?? member.name;
|
|
3488
|
+
const ship = t.ship_id && t.ship_id !== i.myShipId ? i.ships?.find((s) => s.id === t.ship_id)?.name : void 0;
|
|
3489
|
+
return ship ? `${label} (${ship})` : label;
|
|
3490
|
+
}
|
|
3491
|
+
return t.reporter_name ? `${t.reporter_name} (unverified)` : "\u2014";
|
|
3401
3492
|
}
|
|
3402
3493
|
function lastComment(i, ticketId) {
|
|
3403
3494
|
const forTicket = i.comments.filter((c) => c.ticket_id === ticketId).sort((a, b) => a.created_at < b.created_at ? -1 : a.created_at > b.created_at ? 1 : 0);
|
|
3404
3495
|
const last = forTicket[forTicket.length - 1];
|
|
3405
3496
|
if (!last) return "\u2014";
|
|
3406
|
-
return `${last.created_at.slice(0, 16)}Z ${author(last, i
|
|
3497
|
+
return `${last.created_at.slice(0, 16)}Z ${author(last, i)}`;
|
|
3407
3498
|
}
|
|
3408
3499
|
function newFromOthers(i, ticketId) {
|
|
3409
3500
|
return i.comments.filter(
|
|
@@ -3435,14 +3526,14 @@ ${sorted(ts).map(row).join("\n")}
|
|
|
3435
3526
|
`;
|
|
3436
3527
|
}
|
|
3437
3528
|
function buildingDigest(i) {
|
|
3438
|
-
const header = "| ticket | repo | branch | worktree | status | assignee | sev | pri | eff | updated | last comment | new since last poll |\n
|
|
3529
|
+
const header = "| ticket | repo | branch | worktree | status | assignee | filed by | sev | pri | eff | updated | last comment | new since last poll |\n|---|---|---|---|---|---|---|---|---|---|---|---|---|";
|
|
3439
3530
|
const row = (t) => {
|
|
3440
3531
|
const n = newFromOthers(i, t.id);
|
|
3441
3532
|
const branch = i.branchFor ? i.branchFor(t) : "";
|
|
3442
3533
|
const worktree = i.worktreeFor ? i.worktreeFor(t) : "";
|
|
3443
3534
|
const dir = i.dirFor ? i.dirFor(t) : null;
|
|
3444
3535
|
const repo = i.dirFor ? dir ?? "**NO CHECKOUT**" : "";
|
|
3445
|
-
return `| ${displayKey2(t)} | ${repo} | ${branch} | ${worktree} | ${t.status} | ${who(t, i)} | ${t.severity ?? "\u2014"} | ${t.priority ?? "\u2014"} | p${effectivePriority(t)} | ${t.updated_at.slice(0, 16)}Z | ${lastComment(i, t.id)} | ${n > 0 ? `**${n} new**` : "\u2014"} |`;
|
|
3536
|
+
return `| ${displayKey2(t)} | ${repo} | ${branch} | ${worktree} | ${t.status} | ${who(t, i)} | ${filedBy(t, i)} | ${t.severity ?? "\u2014"} | ${t.priority ?? "\u2014"} | p${effectivePriority(t)} | ${t.updated_at.slice(0, 16)}Z | ${lastComment(i, t.id)} | ${n > 0 ? `**${n} new**` : "\u2014"} |`;
|
|
3446
3537
|
};
|
|
3447
3538
|
const blockedRow = (t) => `| ${displayKey2(t)} | ${t.status} | ${who(t, i)} | p${effectivePriority(t)} | ${blockers(t, i)} |`;
|
|
3448
3539
|
const blockedTable = (ts) => ts.length === 0 ? "\n_None._\n" : `
|
|
@@ -3474,7 +3565,7 @@ ${sorted(ts).map(blockedRow).join("\n")}
|
|
|
3474
3565
|
]);
|
|
3475
3566
|
}
|
|
3476
3567
|
function qaDigest(i) {
|
|
3477
|
-
const header = "| ticket | repo | status | built by | assignee | sev | pri | eff | branch | worktree | updated | last comment | new since last poll |\n
|
|
3568
|
+
const header = "| ticket | repo | status | built by | assignee | filed by | sev | pri | eff | branch | worktree | updated | last comment | new since last poll |\n|---|---|---|---|---|---|---|---|---|---|---|---|---|---|";
|
|
3478
3569
|
const branchCell = (t) => {
|
|
3479
3570
|
if (i.dirFor && i.dirFor(t) === null) return "\u2014";
|
|
3480
3571
|
if (!i.existingBranchFor) return "";
|
|
@@ -3489,7 +3580,7 @@ function qaDigest(i) {
|
|
|
3489
3580
|
const n = newFromOthers(i, t.id);
|
|
3490
3581
|
const dir = i.dirFor ? i.dirFor(t) : null;
|
|
3491
3582
|
const repo = i.dirFor ? dir ?? "**NO CHECKOUT**" : "";
|
|
3492
|
-
return `| ${displayKey2(t)} | ${repo} | ${t.status} | ${builtBy(t)} | ${who(t, i)} | ${t.severity ?? "\u2014"} | ${t.priority ?? "\u2014"} | p${effectivePriority(t)} | ${branchCell(t)} | ${worktreeCell(t)} | ${t.updated_at.slice(0, 16)}Z | ${lastComment(i, t.id)} | ${n > 0 ? `**${n} new**` : "\u2014"} |`;
|
|
3583
|
+
return `| ${displayKey2(t)} | ${repo} | ${t.status} | ${builtBy(t)} | ${who(t, i)} | ${filedBy(t, i)} | ${t.severity ?? "\u2014"} | ${t.priority ?? "\u2014"} | p${effectivePriority(t)} | ${branchCell(t)} | ${worktreeCell(t)} | ${t.updated_at.slice(0, 16)}Z | ${lastComment(i, t.id)} | ${n > 0 ? `**${n} new**` : "\u2014"} |`;
|
|
3493
3584
|
};
|
|
3494
3585
|
return stream([
|
|
3495
3586
|
"## Current queue \u2014 built for you by the poll\n",
|
|
@@ -3952,11 +4043,12 @@ async function decideCycle(o) {
|
|
|
3952
4043
|
const { route, ship, state, emit } = o;
|
|
3953
4044
|
const tracker = new Tracker(route, ship);
|
|
3954
4045
|
emit.enter("poll");
|
|
3955
|
-
const [tickets, comments, crewRows, epicRows] = await Promise.all([
|
|
4046
|
+
const [tickets, comments, crewRows, epicRows, ships] = await Promise.all([
|
|
3956
4047
|
tracker.openTickets(),
|
|
3957
4048
|
tracker.comments(200),
|
|
3958
4049
|
tracker.crewRows(),
|
|
3959
|
-
tracker.epicRows()
|
|
4050
|
+
tracker.epicRows(),
|
|
4051
|
+
tracker.shipRows()
|
|
3960
4052
|
]);
|
|
3961
4053
|
emit.emit(`${tickets.length} open ticket(s), ${comments.length} comment(s)`, {
|
|
3962
4054
|
data: { tickets: tickets.length, comments: comments.length }
|
|
@@ -4069,6 +4161,7 @@ async function decideCycle(o) {
|
|
|
4069
4161
|
tickets,
|
|
4070
4162
|
comments,
|
|
4071
4163
|
roster,
|
|
4164
|
+
ships,
|
|
4072
4165
|
blocked,
|
|
4073
4166
|
info,
|
|
4074
4167
|
sweep,
|
|
@@ -4147,6 +4240,10 @@ function writeDigest(o, d, role, stateDir) {
|
|
|
4147
4240
|
comments: d.comments,
|
|
4148
4241
|
me,
|
|
4149
4242
|
roster: d.roster,
|
|
4243
|
+
// CREW-1371: a comment from another ship is labelled with that ship's
|
|
4244
|
+
// name, so two ships under one API key are distinguishable on the queue.
|
|
4245
|
+
ships: d.ships,
|
|
4246
|
+
myShipId: d.ships.find((s) => (s.name ?? "").trim() === o.ship.name.trim())?.id ?? null,
|
|
4150
4247
|
watermark: d.watermark,
|
|
4151
4248
|
blocked: d.blocked,
|
|
4152
4249
|
blockerInfo: d.info
|
|
@@ -4422,6 +4519,14 @@ import { createHash as createHash2 } from "crypto";
|
|
|
4422
4519
|
// src/environment.ts
|
|
4423
4520
|
import { basename as basename4 } from "path";
|
|
4424
4521
|
var API_KEY_VAR = "CREW_API_KEY";
|
|
4522
|
+
function authorshipBullets(a, contract) {
|
|
4523
|
+
if (!a) return [];
|
|
4524
|
+
return [
|
|
4525
|
+
...a.issueAuthor ? [`- \`${contract.columns.author}\`: your own Crew row id, from "Your crew" above \u2014 who filed it`] : [],
|
|
4526
|
+
...a.issueShip && a.shipId ? [`- \`${contract.columns.ship}\`: \`${a.shipId}\` \u2014 this ship, where it was filed from`] : [],
|
|
4527
|
+
...a.commentShip && a.shipId ? [`- \`${contract.comments.ship}\` on every comment you post: \`${a.shipId}\` \u2014 this ship`] : []
|
|
4528
|
+
];
|
|
4529
|
+
}
|
|
4425
4530
|
var HOOK_PURPOSE = [
|
|
4426
4531
|
["setup", "make a fresh worktree usable \u2014 dependencies, generated clients"],
|
|
4427
4532
|
["isolate", "point this worktree at state of its own; prints KEY=value overrides to export"],
|
|
@@ -4628,6 +4733,7 @@ function renderEnvironment(i) {
|
|
|
4628
4733
|
...only?.id ? [`- \`${contract.columns.repo}\`: \`${only.id}\` (this repository)`] : repos.filter((r) => r.id).map(
|
|
4629
4734
|
(r) => `- \`${contract.columns.repo}\`: \`${r.id}\` \u2014 when the new ticket belongs to ${r.name}`
|
|
4630
4735
|
),
|
|
4736
|
+
...authorshipBullets(i.authorship, contract),
|
|
4631
4737
|
"",
|
|
4632
4738
|
...i.sourceTicket ? [
|
|
4633
4739
|
`Name ${i.sourceTicket} \u2014 the ticket you were working when you noticed this \u2014 in the new`,
|
|
@@ -5164,8 +5270,7 @@ async function runWizard(p, env) {
|
|
|
5164
5270
|
}
|
|
5165
5271
|
const nodeDir = dirname6(env.nodePath);
|
|
5166
5272
|
const pnpmPath = findOnPath2("pnpm", env.pathEnv, isExec);
|
|
5167
|
-
const
|
|
5168
|
-
if (extraPath) p.say(`Hooks will get ${extraPath} on their PATH (where node and pnpm live).`);
|
|
5273
|
+
const baseDirs = [nodeDir, pnpmPath ? dirname6(pnpmPath) : void 0];
|
|
5169
5274
|
p.say(
|
|
5170
5275
|
`
|
|
5171
5276
|
Host Passengers lets this machine run the agent sessions that other people's
|
|
@@ -5175,6 +5280,11 @@ Tablation workspaces ask for, in a container. More: ${HOST_PASSENGERS_DOCS_URL}`
|
|
|
5175
5280
|
if (hostPassengers && !env.dockerAvailable) {
|
|
5176
5281
|
p.say("Docker is not available here, so nothing will be hosted until it is. Your answer is recorded anyway.");
|
|
5177
5282
|
}
|
|
5283
|
+
const dockerPath = hostPassengers ? findOnPath2("docker", env.pathEnv, isExec) : void 0;
|
|
5284
|
+
const extraPath = deriveExtraPath([...baseDirs, dockerPath ? dirname6(dockerPath) : void 0]);
|
|
5285
|
+
if (extraPath) {
|
|
5286
|
+
p.say(`Hooks will get ${extraPath} on their PATH (where node, pnpm${dockerPath ? " and docker" : ""} live).`);
|
|
5287
|
+
}
|
|
5178
5288
|
return { name, platform: env.platform, agentBin, extraPath, hostPassengers };
|
|
5179
5289
|
}
|
|
5180
5290
|
function renderShipBlock(a) {
|
|
@@ -6128,7 +6238,7 @@ Still failing: ${n.detail ?? n.headline}`, memberId);
|
|
|
6128
6238
|
report_type: "bug",
|
|
6129
6239
|
severity: "s2",
|
|
6130
6240
|
reporter_name: "Crew release"
|
|
6131
|
-
});
|
|
6241
|
+
}, memberId);
|
|
6132
6242
|
const label = created.issue_id ? displayKey(created) : created.id;
|
|
6133
6243
|
log.warn(`release failure \u2014 filed ${label}`, { step: "release" });
|
|
6134
6244
|
return { kind: "filed", issueId: label };
|
|
@@ -7158,7 +7268,19 @@ async function discover(o) {
|
|
|
7158
7268
|
}
|
|
7159
7269
|
const pairId = seatFor(scoped, "pair") || (shipRowId ? seatFor(unclaimed, "pair") : void 0);
|
|
7160
7270
|
if (pairId) out.seats.pair = pairId;
|
|
7161
|
-
|
|
7271
|
+
if (shipRowId) {
|
|
7272
|
+
const releaseId = seatFor(scoped, "release");
|
|
7273
|
+
if (releaseId) out.releaseSeat = releaseId;
|
|
7274
|
+
else {
|
|
7275
|
+
const created = await post2(o, `/data-models/${out.models.crew}/records`, {
|
|
7276
|
+
name: "Release agent",
|
|
7277
|
+
ship_id: shipRowId
|
|
7278
|
+
});
|
|
7279
|
+
out.releaseSeat = created.id;
|
|
7280
|
+
provisioning.push("created Crew row for the release seat");
|
|
7281
|
+
}
|
|
7282
|
+
}
|
|
7283
|
+
const seatIds = /* @__PURE__ */ new Set([...Object.values(out.seats), ...out.releaseSeat ? [out.releaseSeat] : []]);
|
|
7162
7284
|
out.holds = rows.filter((r) => !seatIds.has(r.id)).map((r) => ({ id: r.id, name: r.name ?? "" }));
|
|
7163
7285
|
if (out.holds.length === 0) {
|
|
7164
7286
|
problems.push("no non-seat Crew rows \u2014 which row is the operator?");
|
|
@@ -8732,7 +8854,8 @@ async function runRoleAgent(role, decision, ctx) {
|
|
|
8732
8854
|
}
|
|
8733
8855
|
}
|
|
8734
8856
|
async function buildEnvironment(route, ship, ticket) {
|
|
8735
|
-
const
|
|
8857
|
+
const tracker = new Tracker(route, ship);
|
|
8858
|
+
const contract = tracker.contract;
|
|
8736
8859
|
const repos = reposOf(route).map((t) => {
|
|
8737
8860
|
const id = repoIdForName(route, t.name);
|
|
8738
8861
|
const o = route.repoOverrides[t.name];
|
|
@@ -8747,7 +8870,8 @@ async function buildEnvironment(route, ship, ticket) {
|
|
|
8747
8870
|
}, t.dir)
|
|
8748
8871
|
};
|
|
8749
8872
|
});
|
|
8750
|
-
|
|
8873
|
+
const authorship = await tracker.authorshipProbe().catch(() => null);
|
|
8874
|
+
return renderEnvironment({ route, userAgent: ship.userAgent, repos, contract, sourceTicket: ticket, authorship });
|
|
8751
8875
|
}
|
|
8752
8876
|
|
|
8753
8877
|
// src/cli.ts
|
|
@@ -9063,8 +9187,9 @@ if (command === "passenger-sync-daemon") {
|
|
|
9063
9187
|
const passengerRoutes = cfg.routes.filter((r) => r.enabled && r.hostPassengers);
|
|
9064
9188
|
if (passengerRoutes.length === 0) return;
|
|
9065
9189
|
if (!dockerAvailable()) {
|
|
9190
|
+
const pathProblem = dockerPathProblem(cfg.ship, COMMON_DOCKER_DIRS);
|
|
9066
9191
|
emit.warn(
|
|
9067
|
-
|
|
9192
|
+
`Host Passengers is on for at least one route but ${pathProblem ?? "Docker is not available"} \u2014 skipping container sync this cycle`,
|
|
9068
9193
|
{ step: "passengers" }
|
|
9069
9194
|
);
|
|
9070
9195
|
return;
|
|
@@ -9189,7 +9314,12 @@ if (command === "passenger-sync-daemon") {
|
|
|
9189
9314
|
process.stderr.write("crew install: no scheduler support yet for Windows (Task Scheduler is planned, not built)\n");
|
|
9190
9315
|
process.exit(2);
|
|
9191
9316
|
}
|
|
9192
|
-
|
|
9317
|
+
const installPathProblem = cfg.routes.some((r) => r.enabled && r.hostPassengers) ? dockerPathProblem(cfg.ship) : void 0;
|
|
9318
|
+
if (installPathProblem) {
|
|
9319
|
+
process.stderr.write(`crew install: Host Passengers is on but ${installPathProblem}
|
|
9320
|
+
`);
|
|
9321
|
+
process.exitCode = 1;
|
|
9322
|
+
} else if (!dryRun && cfg.routes.some((r) => r.enabled && r.hostPassengers) && dockerAvailable()) {
|
|
9193
9323
|
const image = passengerImageRef(cfg);
|
|
9194
9324
|
if (imagePresent(image)) {
|
|
9195
9325
|
process.stdout.write(`Host Passengers image ${image} is already present
|
|
@@ -9336,13 +9466,15 @@ ${changed2.map((c) => ` - ${c}
|
|
|
9336
9466
|
return (await resolvedRepos(c)).map((r) => `${r.name} requires ${r.config.platform}`).join(", ");
|
|
9337
9467
|
}
|
|
9338
9468
|
async function environmentFor(c, ticket) {
|
|
9339
|
-
const
|
|
9469
|
+
const tracker = new Tracker(c, cfg.ship);
|
|
9470
|
+
const contract = tracker.contract;
|
|
9340
9471
|
return renderEnvironment({
|
|
9341
9472
|
route: c,
|
|
9342
9473
|
userAgent: cfg.ship.userAgent,
|
|
9343
9474
|
repos: await resolvedRepos(c),
|
|
9344
9475
|
contract,
|
|
9345
|
-
sourceTicket: ticket
|
|
9476
|
+
sourceTicket: ticket,
|
|
9477
|
+
authorship: await tracker.authorshipProbe().catch(() => null)
|
|
9346
9478
|
});
|
|
9347
9479
|
}
|
|
9348
9480
|
const unplaceableReported = /* @__PURE__ */ new Set();
|
|
@@ -9444,7 +9576,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
9444
9576
|
try {
|
|
9445
9577
|
const g = outcome.gateRed;
|
|
9446
9578
|
const comments = await tracker.comments();
|
|
9447
|
-
const seat = c
|
|
9579
|
+
const seat = releaseSeat(c);
|
|
9448
9580
|
for (const t of gateRedTickets(outcome.merged, outcome.decision.merges)) {
|
|
9449
9581
|
if (alreadyReported(t, g, comments)) continue;
|
|
9450
9582
|
await tracker.postEvent(t.id, gateRedComment(g), seat);
|
|
@@ -9462,7 +9594,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
9462
9594
|
if (news) {
|
|
9463
9595
|
const notified = await notify(c, cfg.ship, news, remit, dryRun);
|
|
9464
9596
|
if (!notified && news.level === "fail") {
|
|
9465
|
-
const memberId = c
|
|
9597
|
+
const memberId = releaseSeat(c);
|
|
9466
9598
|
await applyFailureAlert(tracker, news, scope, all, memberId, remit, dryRun).catch((e) => {
|
|
9467
9599
|
remit.warn(`could not file/update a failure ticket: ${e.message}`, { step: "release" });
|
|
9468
9600
|
});
|
|
@@ -9470,7 +9602,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
9470
9602
|
}
|
|
9471
9603
|
if (outcome.conflicts?.length || outcome.unbuildable?.length) {
|
|
9472
9604
|
const comments = await tracker.comments();
|
|
9473
|
-
const seat = c
|
|
9605
|
+
const seat = releaseSeat(c);
|
|
9474
9606
|
for (const f of outcome.conflicts ?? []) {
|
|
9475
9607
|
try {
|
|
9476
9608
|
const bounce = planConflictBounce(
|
|
@@ -10616,6 +10748,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
10616
10748
|
models: found.models,
|
|
10617
10749
|
seats: found.seats,
|
|
10618
10750
|
holds: found.holds.map((h) => ({ id: h.id, role: h.name })),
|
|
10751
|
+
...found.releaseSeat ? { releaseSeat: found.releaseSeat } : {},
|
|
10619
10752
|
...operator ? { operator } : {},
|
|
10620
10753
|
...contract ? { contract } : {},
|
|
10621
10754
|
...reviewedStatuses.length > 0 ? { reviewedStatuses } : {},
|
|
@@ -11264,6 +11397,14 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
11264
11397
|
`);
|
|
11265
11398
|
}
|
|
11266
11399
|
}
|
|
11400
|
+
const missingAuthorship = await tracker.missingAuthorshipColumns();
|
|
11401
|
+
process.stdout.write(
|
|
11402
|
+
missingAuthorship.length === 0 ? "authorship: filed_by_id, ship_id present\n" : `authorship: missing ${missingAuthorship.join(", ")} \u2014 update the Issues template
|
|
11403
|
+
`
|
|
11404
|
+
);
|
|
11405
|
+
process.stdout.write(
|
|
11406
|
+
route.resolved?.releaseSeat ? "release seat: this ship has its own Release Crew row\n" : "release seat: none \u2014 release comments are credited to the dev seat; run `crew connect` once the Crew table has this ship's row\n"
|
|
11407
|
+
);
|
|
11267
11408
|
const [ships, crewRows] = await Promise.all([tracker.shipRows(), tracker.crewRows()]);
|
|
11268
11409
|
if (ships.length === 0) {
|
|
11269
11410
|
process.stdout.write("ship record: this workspace has no Ships table (fine)\n");
|
|
@@ -11339,6 +11480,20 @@ git: ${gitPath ? `OK (${gitPath})` : "NOT FOUND on the scheduler P
|
|
|
11339
11480
|
process.stdout.write("host passengers: off (no enabled route sets hostPassengers: true)\n");
|
|
11340
11481
|
} else {
|
|
11341
11482
|
const dockerOk = dockerAvailable();
|
|
11483
|
+
const dockerPathIssue = dockerPathProblem(cfg.ship);
|
|
11484
|
+
const dockerKey = "docker_missing";
|
|
11485
|
+
if (dockerPathIssue) {
|
|
11486
|
+
process.stdout.write(`docker on scheduler PATH: NOT FOUND \u2014 ${dockerPathIssue}
|
|
11487
|
+
`);
|
|
11488
|
+
await raiseShipAttention({ state, emit, route: passengerRoutes[0], ship: cfg.ship }, {
|
|
11489
|
+
kind: "docker_missing",
|
|
11490
|
+
key: dockerKey,
|
|
11491
|
+
message: `Host Passengers is on but ${dockerPathIssue}`,
|
|
11492
|
+
since: (/* @__PURE__ */ new Date()).toISOString()
|
|
11493
|
+
});
|
|
11494
|
+
} else {
|
|
11495
|
+
clearShipAttention(state, dockerKey);
|
|
11496
|
+
}
|
|
11342
11497
|
const keygenOk = sshKeygenAvailable();
|
|
11343
11498
|
const image = passengerImageRef(cfg);
|
|
11344
11499
|
const imageOk = dockerOk && imagePresent(image);
|
package/package.json
CHANGED
|
@@ -619,6 +619,11 @@ filed without them has nowhere to route to — it sits unclaimed by any
|
|
|
619
619
|
per-repo queue until a person notices and fixes it by hand, however
|
|
620
620
|
urgent its priority.
|
|
621
621
|
|
|
622
|
+
If that block also lists `filed_by_id` (your own Crew row) or `ship_id` (the
|
|
623
|
+
ship you are running on), set those too, and `ship_id` on every comment you
|
|
624
|
+
post. They record who filed the ticket and from where. The block lists a column
|
|
625
|
+
only when this workspace has it, so a column it does not name is not yours to set.
|
|
626
|
+
|
|
622
627
|
Name the ticket you were working when you noticed it in the new ticket's
|
|
623
628
|
description. Only put it in the new ticket's `blocked by` column if your
|
|
624
629
|
own ticket genuinely cannot proceed without the new one being fixed
|
|
@@ -600,6 +600,11 @@ filed without them has nowhere to route to — it sits unclaimed by any
|
|
|
600
600
|
per-repo queue until a person notices and fixes it by hand, however
|
|
601
601
|
urgent its priority.
|
|
602
602
|
|
|
603
|
+
If that block also lists `filed_by_id` (your own Crew row) or `ship_id` (the
|
|
604
|
+
ship you are running on), set those too, and `ship_id` on every comment you
|
|
605
|
+
post. They record who filed the ticket and from where. The block lists a column
|
|
606
|
+
only when this workspace has it, so a column it does not name is not yours to set.
|
|
607
|
+
|
|
603
608
|
Name the ticket you were working when you noticed it in the new ticket's
|
|
604
609
|
description. Only put it in the new ticket's `blocked by` column if your
|
|
605
610
|
own ticket genuinely cannot proceed without the new one being fixed
|