@tablation/crew 0.43.0 → 0.43.2
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 +139 -24
- package/package.json +1 -1
- package/prompts/default/personas/lane-qa.md +6 -3
- package/prompts/default/skills/crew-lane-agent.md +15 -6
- package/prompts/default/skills/ticket-attachments.md +14 -8
- package/prompts/dev-qa/personas/lane-qa.md +6 -3
- package/prompts/dev-qa/skills/crew-lane-agent.md +15 -6
- package/prompts/dev-qa/skills/ticket-attachments.md +14 -8
package/dist/cli.js
CHANGED
|
@@ -3184,6 +3184,7 @@ var DEFAULT_CONTRACT = {
|
|
|
3184
3184
|
blockedBy: "blocked_by",
|
|
3185
3185
|
updatedAt: "updated_at",
|
|
3186
3186
|
needsDesign: "needs_design",
|
|
3187
|
+
attachments: "attachments",
|
|
3187
3188
|
workMode: "work_mode",
|
|
3188
3189
|
reportType: "report_type",
|
|
3189
3190
|
epic: "epic_id",
|
|
@@ -4786,6 +4787,7 @@ var Tracker = class {
|
|
|
4786
4787
|
* so concurrent first writes share one round of field-metadata reads.
|
|
4787
4788
|
*/
|
|
4788
4789
|
authorship;
|
|
4790
|
+
attachments;
|
|
4789
4791
|
stateDir;
|
|
4790
4792
|
constructor(route, ship) {
|
|
4791
4793
|
this.route = route;
|
|
@@ -5238,6 +5240,28 @@ var Tracker = class {
|
|
|
5238
5240
|
shipId: shipRow?.id ?? null
|
|
5239
5241
|
};
|
|
5240
5242
|
}
|
|
5243
|
+
/**
|
|
5244
|
+
* The attachments probe (CREW-1558): which table the Issues `attachments`
|
|
5245
|
+
* column references, read once per process like the authorship probe. On
|
|
5246
|
+
* TABL-1557 two runs reported a ticket's screenshot as a 404 because, with
|
|
5247
|
+
* no table id in hand, they guessed `files/<attachment id>` paths; naming
|
|
5248
|
+
* the table in the Environment section is what makes the lookup mechanical.
|
|
5249
|
+
*/
|
|
5250
|
+
attachmentsProbe() {
|
|
5251
|
+
this.attachments ??= this.probeAttachments();
|
|
5252
|
+
return this.attachments;
|
|
5253
|
+
}
|
|
5254
|
+
async probeAttachments() {
|
|
5255
|
+
try {
|
|
5256
|
+
const model = await this.client.dataModels.get(this.models.issues);
|
|
5257
|
+
const field = model.fields.find((f) => f.columnName === this.contract.columns.attachments);
|
|
5258
|
+
const fieldType = field?.fieldType;
|
|
5259
|
+
const target = fieldType?.targetModelId;
|
|
5260
|
+
return { mediaLibraryModelId: typeof target === "string" && target ? target : null };
|
|
5261
|
+
} catch {
|
|
5262
|
+
return { mediaLibraryModelId: null };
|
|
5263
|
+
}
|
|
5264
|
+
}
|
|
5241
5265
|
async columnNames(modelId) {
|
|
5242
5266
|
try {
|
|
5243
5267
|
const model = await this.client.dataModels.get(modelId);
|
|
@@ -6874,6 +6898,7 @@ function renderEnvironment(i) {
|
|
|
6874
6898
|
const only = repos.length === 1 ? repos[0] : void 0;
|
|
6875
6899
|
const models = route.resolved?.models;
|
|
6876
6900
|
const epicsModelId = route.resolved?.epicsModelId;
|
|
6901
|
+
const mediaLibraryModelId = i.attachments?.mediaLibraryModelId ?? null;
|
|
6877
6902
|
const key = "ISSUE-000";
|
|
6878
6903
|
const lines = [
|
|
6879
6904
|
"# Your environment",
|
|
@@ -6898,7 +6923,8 @@ function renderEnvironment(i) {
|
|
|
6898
6923
|
`- Issues: \`${models.issues}\``,
|
|
6899
6924
|
`- Comments: \`${models.comments}\``,
|
|
6900
6925
|
`- Crew: \`${models.crew}\``,
|
|
6901
|
-
...epicsModelId ? [`- Epics: \`${epicsModelId}\``] : []
|
|
6926
|
+
...epicsModelId ? [`- Epics: \`${epicsModelId}\``] : [],
|
|
6927
|
+
...mediaLibraryModelId ? [`- Media Library: \`${mediaLibraryModelId}\` (what a ticket's \`${contract.columns.attachments}\` ids point into)`] : []
|
|
6902
6928
|
);
|
|
6903
6929
|
}
|
|
6904
6930
|
const workspaceId = route.resolved?.workspaceId;
|
|
@@ -6925,6 +6951,30 @@ function renderEnvironment(i) {
|
|
|
6925
6951
|
"status and response body in your comment \u2014 do not write that uploads are",
|
|
6926
6952
|
"impossible."
|
|
6927
6953
|
);
|
|
6954
|
+
if (mediaLibraryModelId) {
|
|
6955
|
+
lines.push(
|
|
6956
|
+
"",
|
|
6957
|
+
"## Opening a ticket's attachments",
|
|
6958
|
+
"",
|
|
6959
|
+
`A ticket's or a comment's \`${contract.columns.attachments}\` value is a list of Media Library`,
|
|
6960
|
+
"**record ids** \u2014 not file ids and not URLs. `files/<id>`, `files/<id>/download` and",
|
|
6961
|
+
"`media-library/<id>/...` do not exist for such an id and return 404. For each id,",
|
|
6962
|
+
"read the record, then fetch the file by its storage key:",
|
|
6963
|
+
"",
|
|
6964
|
+
"```sh",
|
|
6965
|
+
`curl -s -H "Authorization: Bearer $${API_KEY_VAR}" -H "User-Agent: ${i.userAgent}" \\`,
|
|
6966
|
+
` "${base}/api/workspaces/${workspaceId}/data-models/${mediaLibraryModelId}/records/<attachment id>"`,
|
|
6967
|
+
'# \u2192 JSON with `storage_key` ("<uuid>/<filename>") and `mime_type`',
|
|
6968
|
+
`curl -s -o <scratch dir>/<filename> -H "Authorization: Bearer $${API_KEY_VAR}" -H "User-Agent: ${i.userAgent}" \\`,
|
|
6969
|
+
` "${base}/api/workspaces/${workspaceId}/files/<storage_key>"`,
|
|
6970
|
+
"```",
|
|
6971
|
+
"",
|
|
6972
|
+
"The MCP forms are `records_controller_find_one` (`dataModelId` = the Media Library",
|
|
6973
|
+
"id above) and `files_controller_download` (`uuid` and `filename` = the two halves",
|
|
6974
|
+
"of the storage key). Save the file outside the repository and open it; where it",
|
|
6975
|
+
"disagrees with the description, the picture is the reproduction."
|
|
6976
|
+
);
|
|
6977
|
+
}
|
|
6928
6978
|
}
|
|
6929
6979
|
if (i.shipName) {
|
|
6930
6980
|
lines.push(
|
|
@@ -7275,6 +7325,12 @@ async function fetchLatestHistoryId(route, agentsModelId, recordId, userAgent) {
|
|
|
7275
7325
|
function promptSourceFields(model, prompt) {
|
|
7276
7326
|
return model.fields?.some((f) => f.columnName === "prompt_baseline") ? { prompt_source: "crew", prompt_baseline: prompt } : {};
|
|
7277
7327
|
}
|
|
7328
|
+
function hasPromptBaselineColumn(model) {
|
|
7329
|
+
return model.fields?.some((f) => f.columnName === "prompt_baseline") === true;
|
|
7330
|
+
}
|
|
7331
|
+
function needsBaselineBackfill(model, row) {
|
|
7332
|
+
return hasPromptBaselineColumn(model) && row.prompt_customized !== true && (typeof row.prompt_baseline !== "string" || !row.prompt_source);
|
|
7333
|
+
}
|
|
7278
7334
|
function isPromptGateRejection(e) {
|
|
7279
7335
|
return e instanceof TablationApiError && e.status === 422;
|
|
7280
7336
|
}
|
|
@@ -7319,8 +7375,16 @@ async function syncPersonas(route, opts) {
|
|
|
7319
7375
|
continue;
|
|
7320
7376
|
}
|
|
7321
7377
|
if (row.prompt === defaultPrompt) {
|
|
7378
|
+
let anchoredAt = row.updated_at;
|
|
7379
|
+
if (!opts.dryRun && needsBaselineBackfill(agentsModel, row)) {
|
|
7380
|
+
try {
|
|
7381
|
+
anchoredAt = (await client2.records.update(agentsModel.id, row.id, promptSourceFields(agentsModel, defaultPrompt), row.updated_at)).updated_at;
|
|
7382
|
+
} catch (e) {
|
|
7383
|
+
if (!(e instanceof StaleWriteError6) && !isPromptGateRejection(e)) throw e;
|
|
7384
|
+
}
|
|
7385
|
+
}
|
|
7322
7386
|
const historyId = await fetchLatestHistoryId(route, agentsModel.id, row.id, opts.userAgent);
|
|
7323
|
-
agentPersonas[role] = { agentId: row.id, lastSyncedUpdatedAt:
|
|
7387
|
+
agentPersonas[role] = { agentId: row.id, lastSyncedUpdatedAt: anchoredAt, ...historyId ? { historyId } : {} };
|
|
7324
7388
|
outcomes.push({ role, action: "unchanged", agentId: row.id });
|
|
7325
7389
|
continue;
|
|
7326
7390
|
}
|
|
@@ -7350,20 +7414,28 @@ async function syncPersonas(route, opts) {
|
|
|
7350
7414
|
continue;
|
|
7351
7415
|
}
|
|
7352
7416
|
const cached = cache[role]?.agentId === row.id ? cache[role] : void 0;
|
|
7353
|
-
|
|
7354
|
-
|
|
7355
|
-
|
|
7356
|
-
|
|
7357
|
-
|
|
7358
|
-
|
|
7359
|
-
|
|
7360
|
-
|
|
7361
|
-
|
|
7362
|
-
|
|
7363
|
-
|
|
7364
|
-
|
|
7365
|
-
|
|
7366
|
-
|
|
7417
|
+
const baselineKnown = hasPromptBaselineColumn(agentsModel) && typeof row.prompt_baseline === "string";
|
|
7418
|
+
if (baselineKnown) {
|
|
7419
|
+
if (row.prompt !== row.prompt_baseline) {
|
|
7420
|
+
outcomes.push({ role, action: "diverged", agentId: row.id });
|
|
7421
|
+
continue;
|
|
7422
|
+
}
|
|
7423
|
+
} else {
|
|
7424
|
+
if (!cached?.historyId) {
|
|
7425
|
+
outcomes.push({ role, action: "diverged", agentId: row.id });
|
|
7426
|
+
continue;
|
|
7427
|
+
}
|
|
7428
|
+
let lastSyncedPrompt;
|
|
7429
|
+
try {
|
|
7430
|
+
const entries = await fetchHistoryEntries(route, agentsModel.id, row.id, opts.userAgent);
|
|
7431
|
+
lastSyncedPrompt = entries.find((e) => e.history_id === cached.historyId)?.prompt;
|
|
7432
|
+
} catch {
|
|
7433
|
+
lastSyncedPrompt = void 0;
|
|
7434
|
+
}
|
|
7435
|
+
if (lastSyncedPrompt === void 0 || lastSyncedPrompt !== row.prompt) {
|
|
7436
|
+
outcomes.push({ role, action: "diverged", agentId: row.id });
|
|
7437
|
+
continue;
|
|
7438
|
+
}
|
|
7367
7439
|
}
|
|
7368
7440
|
if (opts.dryRun) {
|
|
7369
7441
|
outcomes.push({ role, action: "updated", agentId: row.id });
|
|
@@ -7385,7 +7457,7 @@ async function syncPersonas(route, opts) {
|
|
|
7385
7457
|
continue;
|
|
7386
7458
|
}
|
|
7387
7459
|
if (isPromptGateRejection(e)) {
|
|
7388
|
-
agentPersonas[role] = { agentId: row.id, lastSyncedUpdatedAt: row.updated_at, historyId: cached.historyId };
|
|
7460
|
+
agentPersonas[role] = { agentId: row.id, lastSyncedUpdatedAt: row.updated_at, ...cached?.historyId ? { historyId: cached.historyId } : {} };
|
|
7389
7461
|
outcomes.push({ role, action: "overridden", agentId: row.id });
|
|
7390
7462
|
continue;
|
|
7391
7463
|
}
|
|
@@ -7554,7 +7626,20 @@ async function syncSkills(route, opts) {
|
|
|
7554
7626
|
continue;
|
|
7555
7627
|
}
|
|
7556
7628
|
if (row.description === skill.description && row.prompt === skill.prompt) {
|
|
7557
|
-
|
|
7629
|
+
let anchoredAt = row.updated_at;
|
|
7630
|
+
if (!opts.dryRun && needsBaselineBackfill(skillsModel, row)) {
|
|
7631
|
+
try {
|
|
7632
|
+
anchoredAt = (await client2.records.update(
|
|
7633
|
+
skillsModel.id,
|
|
7634
|
+
row.id,
|
|
7635
|
+
promptSourceFields(skillsModel, skill.prompt),
|
|
7636
|
+
row.updated_at
|
|
7637
|
+
)).updated_at;
|
|
7638
|
+
} catch (e) {
|
|
7639
|
+
if (!(e instanceof StaleWriteError7) && !isPromptGateRejection(e)) throw e;
|
|
7640
|
+
}
|
|
7641
|
+
}
|
|
7642
|
+
agentSkills[skill.name] = { skillId: row.id, lastSyncedUpdatedAt: anchoredAt };
|
|
7558
7643
|
outcomes.push({ name: skill.name, action: "unchanged", skillId: row.id });
|
|
7559
7644
|
continue;
|
|
7560
7645
|
}
|
|
@@ -7563,7 +7648,32 @@ async function syncSkills(route, opts) {
|
|
|
7563
7648
|
outcomes.push({ name: skill.name, action: "overridden", skillId: row.id });
|
|
7564
7649
|
continue;
|
|
7565
7650
|
}
|
|
7566
|
-
const
|
|
7651
|
+
const body = { description: skill.description, prompt: skill.prompt, ...promptSourceFields(skillsModel, skill.prompt) };
|
|
7652
|
+
if (opts.force) {
|
|
7653
|
+
if (opts.dryRun) {
|
|
7654
|
+
outcomes.push({ name: skill.name, action: "forced", skillId: row.id });
|
|
7655
|
+
continue;
|
|
7656
|
+
}
|
|
7657
|
+
try {
|
|
7658
|
+
const updated = await client2.records.update(skillsModel.id, row.id, body);
|
|
7659
|
+
agentSkills[skill.name] = { skillId: row.id, lastSyncedUpdatedAt: updated.updated_at };
|
|
7660
|
+
outcomes.push({ name: skill.name, action: "forced", skillId: row.id });
|
|
7661
|
+
} catch (e) {
|
|
7662
|
+
if (!isPromptGateRejection(e)) throw e;
|
|
7663
|
+
outcomes.push({ name: skill.name, action: "overridden", skillId: row.id });
|
|
7664
|
+
}
|
|
7665
|
+
continue;
|
|
7666
|
+
}
|
|
7667
|
+
let knownBaseline;
|
|
7668
|
+
if (hasPromptBaselineColumn(skillsModel) && typeof row.prompt_baseline === "string") {
|
|
7669
|
+
if (row.prompt !== row.prompt_baseline) {
|
|
7670
|
+
outcomes.push({ name: skill.name, action: "diverged", skillId: row.id });
|
|
7671
|
+
continue;
|
|
7672
|
+
}
|
|
7673
|
+
knownBaseline = row.updated_at;
|
|
7674
|
+
} else {
|
|
7675
|
+
knownBaseline = cache[skill.name]?.skillId === row.id ? cache[skill.name]?.lastSyncedUpdatedAt : void 0;
|
|
7676
|
+
}
|
|
7567
7677
|
if (!knownBaseline) {
|
|
7568
7678
|
outcomes.push({ name: skill.name, action: "diverged", skillId: row.id });
|
|
7569
7679
|
continue;
|
|
@@ -7576,7 +7686,7 @@ async function syncSkills(route, opts) {
|
|
|
7576
7686
|
const updated = await client2.records.update(
|
|
7577
7687
|
skillsModel.id,
|
|
7578
7688
|
row.id,
|
|
7579
|
-
|
|
7689
|
+
body,
|
|
7580
7690
|
knownBaseline
|
|
7581
7691
|
);
|
|
7582
7692
|
agentSkills[skill.name] = { skillId: row.id, lastSyncedUpdatedAt: updated.updated_at };
|
|
@@ -7605,6 +7715,8 @@ function describeSkillSyncOutcome(o) {
|
|
|
7605
7715
|
return `${o.name}: already in sync`;
|
|
7606
7716
|
case "overridden":
|
|
7607
7717
|
return `${o.name}: override in place \u2014 prompt not synced (clear "Customize prompt" on the Agent Skills row to resume)`;
|
|
7718
|
+
case "forced":
|
|
7719
|
+
return `${o.name}: overwritten from crew's local file (--force)`;
|
|
7608
7720
|
case "diverged":
|
|
7609
7721
|
return `${o.name}: HAS LOCAL EDITS \u2014 crew's default is newer but was NOT applied; review ${o.skillId} in the app before syncing again`;
|
|
7610
7722
|
}
|
|
@@ -7629,6 +7741,7 @@ async function fetchSkillOverrides(route, opts) {
|
|
|
7629
7741
|
for (const row of rows) {
|
|
7630
7742
|
const name = row.name;
|
|
7631
7743
|
if (row.prompt === local.get(name)?.prompt) continue;
|
|
7744
|
+
if (row.prompt_customized !== true && row.prompt === row.prompt_baseline) continue;
|
|
7632
7745
|
const cached = cache[name];
|
|
7633
7746
|
if (cached?.skillId === row.id && cached.lastSyncedUpdatedAt === row.updated_at) continue;
|
|
7634
7747
|
out[name] = row.prompt;
|
|
@@ -11987,7 +12100,8 @@ async function buildEnvironment(route, ship, ticket) {
|
|
|
11987
12100
|
};
|
|
11988
12101
|
});
|
|
11989
12102
|
const authorship = await tracker.authorshipProbe().catch(() => null);
|
|
11990
|
-
|
|
12103
|
+
const attachments = await tracker.attachmentsProbe().catch(() => null);
|
|
12104
|
+
return renderEnvironment({ route, userAgent: ship.userAgent, repos, contract, sourceTicket: ticket, authorship, attachments, shipName: ship.name });
|
|
11991
12105
|
}
|
|
11992
12106
|
|
|
11993
12107
|
// src/cli.ts
|
|
@@ -12127,7 +12241,7 @@ function usage() {
|
|
|
12127
12241
|
crew inbox [--member NAME] your tickets across every workspace (or a colleague's)
|
|
12128
12242
|
crew connect resolve a workspace's ids into a crew.yaml block (--no-mcp skips registering the Tablation MCP server with Claude Code; --mcp replace overwrites a different one)
|
|
12129
12243
|
crew disconnect remove what connect installed for Pair (Claude MCP; Codex/Gemini instructions block and MCP entry per ship.pair)
|
|
12130
|
-
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)
|
|
12244
|
+
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 or skill; never one marked Customize prompt)
|
|
12131
12245
|
crew agents prompt R [route] print one persona's current prompt (e.g. R=pair, for a SessionStart hook; --harness H adds which CLI it serves)
|
|
12132
12246
|
crew skills sync [route] push only crew's skill files (e.g. grill-me) into the workspace Agent Skills table
|
|
12133
12247
|
crew repos add [ROUTE] [NAME] [PATH] attach a local checkout to a route in crew.yaml (--dry-run previews)
|
|
@@ -12620,6 +12734,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
12620
12734
|
contract,
|
|
12621
12735
|
sourceTicket: ticket,
|
|
12622
12736
|
authorship: await tracker.authorshipProbe().catch(() => null),
|
|
12737
|
+
attachments: await tracker.attachmentsProbe().catch(() => null),
|
|
12623
12738
|
shipName: cfg.ship.name
|
|
12624
12739
|
});
|
|
12625
12740
|
}
|
|
@@ -13567,7 +13682,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
13567
13682
|
break;
|
|
13568
13683
|
}
|
|
13569
13684
|
if (sub !== "sync") {
|
|
13570
|
-
process.stderr.write("crew agents sync [route] [--force] push crew's personas (Developer/Design/QA/Triage/Pair) AND skill files (e.g. grill-me) into the workspace Agents/Agent Skills tables (--force overwrites even a diverged persona)\n");
|
|
13685
|
+
process.stderr.write("crew agents sync [route] [--force] push crew's personas (Developer/Design/QA/Triage/Pair) AND skill files (e.g. grill-me) into the workspace Agents/Agent Skills tables (--force overwrites even a diverged persona or skill; never one marked Customize prompt)\n");
|
|
13571
13686
|
process.stderr.write(`crew agents prompt <role> [route] print a persona's current prompt (roles: ${ROLE_NAMES.join(", ")})
|
|
13572
13687
|
`);
|
|
13573
13688
|
process.exit(2);
|
|
@@ -13610,7 +13725,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
13610
13725
|
`);
|
|
13611
13726
|
let skillResult;
|
|
13612
13727
|
try {
|
|
13613
|
-
skillResult = await syncSkills(target, { userAgent: cfg.ship.userAgent, dryRun });
|
|
13728
|
+
skillResult = await syncSkills(target, { userAgent: cfg.ship.userAgent, dryRun, force });
|
|
13614
13729
|
} catch (e) {
|
|
13615
13730
|
if (e instanceof SkillSyncError) {
|
|
13616
13731
|
process.stderr.write(`crew agents sync: ${e.message}
|
package/package.json
CHANGED
|
@@ -47,9 +47,12 @@ pass.
|
|
|
47
47
|
builder's closing comment — what it changed, how it says it verified,
|
|
48
48
|
what it admits it could not test — is the thing you are checking.
|
|
49
49
|
Anything it skipped is your first test. **Open the ticket's own
|
|
50
|
-
attachments first
|
|
51
|
-
|
|
52
|
-
|
|
50
|
+
attachments first**, exactly as Step 3.4 in the shared policy says: read
|
|
51
|
+
the Media Library record for its `storage_key`, then fetch
|
|
52
|
+
`files/<storage_key>` — never a guessed `files/<id>` path. The reporter's
|
|
53
|
+
screenshot is the repro you are judging the fix against. If you cannot
|
|
54
|
+
open one, say so with the exact request and response and set `needs_info`
|
|
55
|
+
rather than passing it.
|
|
53
56
|
4. **Get the branch and read the diff.** The branch on the remote is what
|
|
54
57
|
you test, and you never need the builder's worktree: cut your own with
|
|
55
58
|
`crew worktree <number>` (it prints the path), unless the builder's
|
|
@@ -284,12 +284,21 @@ before going further, and never change a ticket's `report_type` yourself.
|
|
|
284
284
|
4. **Post a short comment** on what you are about to do (Comments table:
|
|
285
285
|
`ticket_id`, `team_member_id` = your id, markdown `body`), and further
|
|
286
286
|
comments at meaningful milestones — the operator wants visible progress,
|
|
287
|
-
not just status flips. **Open the ticket's attachments before you start
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
`
|
|
292
|
-
|
|
287
|
+
not just status flips. **Open the ticket's attachments before you start.**
|
|
288
|
+
An `attachments` value on the ticket or a comment is a list of Media
|
|
289
|
+
Library record ids — not file ids and not URLs, so no `files/<id>` path
|
|
290
|
+
will ever work. For each id: (1) read the Media Library record,
|
|
291
|
+
`GET /api/workspaces/<workspaceId>/data-models/<Media Library id>/records/<id>`
|
|
292
|
+
(the Environment section's "Opening a ticket's attachments" gives the
|
|
293
|
+
table id), and take its `storage_key`; (2) fetch the bytes with
|
|
294
|
+
`GET /api/workspaces/<workspaceId>/files/<storage_key>` (the MCP form is
|
|
295
|
+
`files_controller_download` with `uuid` and `filename` being the two
|
|
296
|
+
halves of the storage key) into a scratch directory and open it; (3)
|
|
297
|
+
compare it with the description — where they disagree, the picture is the
|
|
298
|
+
reproduction. If you cannot open one, that is a blocker (comment naming
|
|
299
|
+
it and the exact failure, `needs_info` plus `needs_planning`, stop); never
|
|
300
|
+
proceed on the prose alone. The `Ticket Attachments` skill has the edge
|
|
301
|
+
cases.
|
|
293
302
|
5. **Implement the fix.** Read the relevant code first; do not guess at the
|
|
294
303
|
architecture. **Never bump the version or edit the changelog on this
|
|
295
304
|
branch** — the release phase assigns the version once per release, over
|
|
@@ -12,14 +12,20 @@ you picked up, and on any comment that has them, before you start.
|
|
|
12
12
|
|
|
13
13
|
1. **Read the Media Library record for that id.** The `attachments` column is
|
|
14
14
|
a reference into the workspace's Media Library table; its `storage_key`
|
|
15
|
-
and `mime_type` are what you need
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
is
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
15
|
+
and `mime_type` are what you need:
|
|
16
|
+
`GET /api/workspaces/<workspaceId>/data-models/<Media Library id>/records/<id>`
|
|
17
|
+
(MCP: `records_controller_find_one`). The Environment section's "Opening
|
|
18
|
+
a ticket's attachments" names the table id; if it is absent, find the
|
|
19
|
+
table by name in a workspace-level model listing — never by reading the
|
|
20
|
+
whole Issues model definition, which is over a megabyte. The id is a
|
|
21
|
+
record id, not a file id: `files/<id>`, `files/<id>/download` and
|
|
22
|
+
`media-library/<id>/...` all 404 by design.
|
|
23
|
+
2. **Fetch the bytes:** the `storage_key` has the form `<uuid>/<filename>`;
|
|
24
|
+
`GET /api/workspaces/<workspaceId>/files/<uuid>/<filename>` with the usual
|
|
25
|
+
headers streams it (the MCP form is `files_controller_download` with
|
|
26
|
+
`workspaceId`, `uuid` and `filename`, which hands an image back as a
|
|
27
|
+
viewable block). Save the file in a scratch directory outside the
|
|
28
|
+
repository, then open it.
|
|
23
29
|
3. **Compare what the picture shows with what the description says.** Where
|
|
24
30
|
they disagree, the picture is the reproduction.
|
|
25
31
|
|
|
@@ -46,9 +46,12 @@ pass.
|
|
|
46
46
|
builder's closing comment — what it changed, how it says it verified,
|
|
47
47
|
what it admits it could not test — is the thing you are checking.
|
|
48
48
|
Anything it skipped is your first test. **Open the ticket's own
|
|
49
|
-
attachments first
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
attachments first**, exactly as Step 3.4 in the shared policy says: read
|
|
50
|
+
the Media Library record for its `storage_key`, then fetch
|
|
51
|
+
`files/<storage_key>` — never a guessed `files/<id>` path. The reporter's
|
|
52
|
+
screenshot is the repro you are judging the fix against. If you cannot
|
|
53
|
+
open one, say so with the exact request and response and set `needs_info`
|
|
54
|
+
rather than passing it.
|
|
52
55
|
4. **Get the branch and read the diff.** The branch on the remote is what
|
|
53
56
|
you test, and you never need the builder's worktree: cut your own with
|
|
54
57
|
`crew worktree <number>` (it prints the path), unless the builder's
|
|
@@ -284,12 +284,21 @@ before going further, and never change a ticket's `report_type` yourself.
|
|
|
284
284
|
4. **Post a short comment** on what you are about to do (Comments table:
|
|
285
285
|
`ticket_id`, `team_member_id` = your id, markdown `body`), and further
|
|
286
286
|
comments at meaningful milestones — the operator wants visible progress,
|
|
287
|
-
not just status flips. **Open the ticket's attachments before you start
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
`
|
|
292
|
-
|
|
287
|
+
not just status flips. **Open the ticket's attachments before you start.**
|
|
288
|
+
An `attachments` value on the ticket or a comment is a list of Media
|
|
289
|
+
Library record ids — not file ids and not URLs, so no `files/<id>` path
|
|
290
|
+
will ever work. For each id: (1) read the Media Library record,
|
|
291
|
+
`GET /api/workspaces/<workspaceId>/data-models/<Media Library id>/records/<id>`
|
|
292
|
+
(the Environment section's "Opening a ticket's attachments" gives the
|
|
293
|
+
table id), and take its `storage_key`; (2) fetch the bytes with
|
|
294
|
+
`GET /api/workspaces/<workspaceId>/files/<storage_key>` (the MCP form is
|
|
295
|
+
`files_controller_download` with `uuid` and `filename` being the two
|
|
296
|
+
halves of the storage key) into a scratch directory and open it; (3)
|
|
297
|
+
compare it with the description — where they disagree, the picture is the
|
|
298
|
+
reproduction. If you cannot open one, that is a blocker (comment naming
|
|
299
|
+
it and the exact failure, `needs_info` plus `needs_planning`, stop); never
|
|
300
|
+
proceed on the prose alone. The `Ticket Attachments` skill has the edge
|
|
301
|
+
cases.
|
|
293
302
|
5. **Implement the fix.** Read the relevant code first; do not guess at the
|
|
294
303
|
architecture. **Never bump the version or edit the changelog on this
|
|
295
304
|
branch** — the release phase assigns the version once per release, over
|
|
@@ -12,14 +12,20 @@ you picked up, and on any comment that has them, before you start.
|
|
|
12
12
|
|
|
13
13
|
1. **Read the Media Library record for that id.** The `attachments` column is
|
|
14
14
|
a reference into the workspace's Media Library table; its `storage_key`
|
|
15
|
-
and `mime_type` are what you need
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
is
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
15
|
+
and `mime_type` are what you need:
|
|
16
|
+
`GET /api/workspaces/<workspaceId>/data-models/<Media Library id>/records/<id>`
|
|
17
|
+
(MCP: `records_controller_find_one`). The Environment section's "Opening
|
|
18
|
+
a ticket's attachments" names the table id; if it is absent, find the
|
|
19
|
+
table by name in a workspace-level model listing — never by reading the
|
|
20
|
+
whole Issues model definition, which is over a megabyte. The id is a
|
|
21
|
+
record id, not a file id: `files/<id>`, `files/<id>/download` and
|
|
22
|
+
`media-library/<id>/...` all 404 by design.
|
|
23
|
+
2. **Fetch the bytes:** the `storage_key` has the form `<uuid>/<filename>`;
|
|
24
|
+
`GET /api/workspaces/<workspaceId>/files/<uuid>/<filename>` with the usual
|
|
25
|
+
headers streams it (the MCP form is `files_controller_download` with
|
|
26
|
+
`workspaceId`, `uuid` and `filename`, which hands an image back as a
|
|
27
|
+
viewable block). Save the file in a scratch directory outside the
|
|
28
|
+
repository, then open it.
|
|
23
29
|
3. **Compare what the picture shows with what the description says.** Where
|
|
24
30
|
they disagree, the picture is the reproduction.
|
|
25
31
|
|