@tablation/crew 0.53.4 → 0.53.6
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 +31 -12
- package/package.json +1 -1
- package/prompts/default/skills/grill-me.md +47 -8
- package/prompts/dev-qa/skills/grill-me.md +47 -8
package/dist/cli.js
CHANGED
|
@@ -6475,11 +6475,16 @@ function materializeBranchForTicket(dir, cfg, t, role, onProblem) {
|
|
|
6475
6475
|
import { writeFileSync as writeFileSync7 } from "fs";
|
|
6476
6476
|
import { join as join11 } from "path";
|
|
6477
6477
|
|
|
6478
|
+
// src/callout.ts
|
|
6479
|
+
var calloutHeader = (kind, label) => `> [!${kind}]
|
|
6480
|
+
> **Crew** \u2014 ${label}`;
|
|
6481
|
+
var calloutLabelLine = (label) => `> **Crew** \u2014 ${label}`;
|
|
6482
|
+
|
|
6478
6483
|
// src/stalled.ts
|
|
6479
6484
|
var STALLED_AFTER_CYCLES = 30;
|
|
6480
6485
|
var CYCLE_MS = 120 * 1e3;
|
|
6481
6486
|
var STALLED_AFTER_MS = STALLED_AFTER_CYCLES * CYCLE_MS;
|
|
6482
|
-
var STALLED_MARKER = "
|
|
6487
|
+
var STALLED_MARKER = calloutHeader("WARNING", "Stalled");
|
|
6483
6488
|
var stalledKey = (issueId) => `stalled:${issueId}`;
|
|
6484
6489
|
var ms = (iso) => {
|
|
6485
6490
|
const n = iso ? Date.parse(iso) : NaN;
|
|
@@ -6501,7 +6506,7 @@ function planStalled(tickets, comments, ships, contract, holds2 = /* @__PURE__ *
|
|
|
6501
6506
|
const lastMarker = Math.max(0, ...events.filter((e) => e.record_id === t.id && e.event_type === "stalled").map((e) => ms(e.occurred_at ?? e.created_at ?? "")));
|
|
6502
6507
|
const lastActivityAt = Math.max(
|
|
6503
6508
|
ms(t.updated_at),
|
|
6504
|
-
...mine.filter((c) => !(c.body ?? "").includes(
|
|
6509
|
+
...mine.filter((c) => !(c.body ?? "").includes(calloutLabelLine("Stalled"))).map((c) => ms(c.created_at))
|
|
6505
6510
|
);
|
|
6506
6511
|
if (now - lastActivityAt < STALLED_AFTER_MS) continue;
|
|
6507
6512
|
if (ships.some((s) => s.engaged && s.engaged_ticket_id === t.id)) continue;
|
|
@@ -6518,6 +6523,7 @@ function stalledComment(s, ships, now = Date.now()) {
|
|
|
6518
6523
|
const mins = Math.round((now - s.lastActivityAt) / 6e4);
|
|
6519
6524
|
const who2 = s.holderShipId ? `Held by ${shipName(ships, s.holderShipId)}, which is not engaged on it. That ship has an attention item open; the ticket is otherwise left as it is, and resumes the moment the ship picks it back up.` : `No ship holds it and none is engaged on it. It is waiting for the next ship that polls its lane; its status and assignee are left as they are.`;
|
|
6520
6525
|
return `${STALLED_MARKER}
|
|
6526
|
+
|
|
6521
6527
|
**Stalled \u2014 no activity on \`${s.ticket.issue_id}\` for ${mins} minutes (\`${s.ticket.status}\`).**
|
|
6522
6528
|
|
|
6523
6529
|
${who2}`;
|
|
@@ -6917,7 +6923,7 @@ async function resolveTopCandidate(tracker, candidates, seat, approvedStatus, bu
|
|
|
6917
6923
|
|
|
6918
6924
|
// src/conflict.ts
|
|
6919
6925
|
import { StaleWriteError as StaleWriteError5 } from "@tablation/client";
|
|
6920
|
-
var BOUNCE_MARKER = "
|
|
6926
|
+
var BOUNCE_MARKER = calloutHeader("WARNING", "Merge Conflict");
|
|
6921
6927
|
function worktreeForBranch(cwd, branch) {
|
|
6922
6928
|
for (const w of worktrees(cwd)) {
|
|
6923
6929
|
if (w.branch === branch && !w.prunable) return w.path;
|
|
@@ -6965,6 +6971,7 @@ function refreshBranch(worktree, base) {
|
|
|
6965
6971
|
function handBackComment(b, merged) {
|
|
6966
6972
|
const list = b.paths.length ? b.paths.map((p) => `\`${p}\``).join(", ") : "files this branch and the base both changed";
|
|
6967
6973
|
return `${BOUNCE_MARKER}
|
|
6974
|
+
|
|
6968
6975
|
**Back with the dev lane \u2014 this branch no longer merges.**
|
|
6969
6976
|
|
|
6970
6977
|
\`${b.branch}\` was verified, but squash-merging it into \`${b.base}\` conflicts in ${list}. Work that landed on \`${b.base}\` after this branch was cut has changed the same lines.
|
|
@@ -6975,6 +6982,7 @@ Nothing was written to \`${b.base}\`: the failed merge was rewound, and the rest
|
|
|
6975
6982
|
}
|
|
6976
6983
|
function escalateComment(b) {
|
|
6977
6984
|
return `${BOUNCE_MARKER}
|
|
6985
|
+
|
|
6978
6986
|
**Conflicting again after a hand-back \u2014 stopping rather than looping.**
|
|
6979
6987
|
|
|
6980
6988
|
\`${b.branch}\` was handed back once for a merge conflict, verified again, and conflicts against \`${b.base}\` a second time${b.paths.length ? ` (${b.paths.map((p) => `\`${p}\``).join(", ")})` : ""}.
|
|
@@ -7050,7 +7058,7 @@ async function applyConflictBounce(writer, b, contract, memberId, log, dryRun) {
|
|
|
7050
7058
|
|
|
7051
7059
|
// src/build-failure.ts
|
|
7052
7060
|
import { StaleWriteError as StaleWriteError6 } from "@tablation/client";
|
|
7053
|
-
var BUILD_FAILURE_MARKER = "
|
|
7061
|
+
var BUILD_FAILURE_MARKER = calloutHeader("CAUTION", "Build Failure");
|
|
7054
7062
|
function failingPaths(output, cwd) {
|
|
7055
7063
|
const found = /* @__PURE__ */ new Set();
|
|
7056
7064
|
const ext = String.raw`[\w.-]+\.[A-Za-z]{1,6}`;
|
|
@@ -7086,6 +7094,7 @@ function planBuildFailureBounce(cwd, c, base, remote) {
|
|
|
7086
7094
|
function buildFailureComment(b) {
|
|
7087
7095
|
const how = b.how === "files" ? `The failing output names files that only \`${b.branch}\` changed among the branches in that release.` : `Built alone on the previous \`${b.base}\`, \`${b.branch}\` fails the same hook.`;
|
|
7088
7096
|
return `${BUILD_FAILURE_MARKER}
|
|
7097
|
+
|
|
7089
7098
|
**Back with the dev lane \u2014 this branch builds on its own but not on current \`${b.base}\`.**
|
|
7090
7099
|
|
|
7091
7100
|
\`${b.branch}\` was verified, but the release's \`${b.hook}\` hook failed once it was merged with the other branches in the same release. ${how}
|
|
@@ -7125,7 +7134,7 @@ async function applyBuildFailureBounce(writer, b, contract, memberId, log, dryRu
|
|
|
7125
7134
|
}
|
|
7126
7135
|
|
|
7127
7136
|
// src/stranded-verified.ts
|
|
7128
|
-
var STRANDED_MARKER = "
|
|
7137
|
+
var STRANDED_MARKER = calloutHeader("WARNING", "Stranded Verified");
|
|
7129
7138
|
var STRANDED_ESCALATE_AFTER_MS = 30 * 60 * 1e3;
|
|
7130
7139
|
var priorStrandedFlags = (ticket, events) => events.filter((e) => e.record_id === ticket.id && e.event_type === "stranded_flagged").length;
|
|
7131
7140
|
function planStrandedVerified(candidate, base, remote, events) {
|
|
@@ -7141,6 +7150,7 @@ function planStrandedVerified(candidate, base, remote, events) {
|
|
|
7141
7150
|
function flagComment(s) {
|
|
7142
7151
|
const key = s.candidate.ticket.issue_id;
|
|
7143
7152
|
return `${STRANDED_MARKER}
|
|
7153
|
+
|
|
7144
7154
|
**Verified, but nothing to merge \u2014 flagging before this repeats forever.**
|
|
7145
7155
|
|
|
7146
7156
|
\`${key}\` is \`verified\` with no branch on this ship, and its key names no commit anywhere in this ship's \`${s.base}\` history (searched all of it, local commits included, for a bounded \`${key}\` in a commit subject or body). One of three things is true: it landed under a commit that never named the key, the branch was deleted before it merged, or it was verified in error \u2014 the crew cannot tell these apart and will not guess.
|
|
@@ -7150,6 +7160,7 @@ If it is still unplaceable next cycle, this ticket is escalated to a person. If
|
|
|
7150
7160
|
function escalateComment2(s) {
|
|
7151
7161
|
const key = s.candidate.ticket.issue_id;
|
|
7152
7162
|
return `${STRANDED_MARKER}
|
|
7163
|
+
|
|
7153
7164
|
**Still nothing to merge \u2014 stopping rather than repeating.**
|
|
7154
7165
|
|
|
7155
7166
|
\`${key}\` was flagged as stranded at \`verified\` on an earlier cycle and still has no branch on this ship or on \`${s.remote}\` and no commit in this ship's \`${s.base}\` history naming its key (searched all of it, local commits included). This needs a person: either the work landed under a commit that doesn't name the key and this ticket can simply close, or the branch is gone and the work needs redoing.
|
|
@@ -7197,7 +7208,8 @@ async function applyStrandedVerified(writer, s, contract, operatorId, memberId,
|
|
|
7197
7208
|
}
|
|
7198
7209
|
|
|
7199
7210
|
// src/gate-red.ts
|
|
7200
|
-
var GATE_RED_MARKER = "
|
|
7211
|
+
var GATE_RED_MARKER = calloutHeader("CAUTION", "Test Gate Red");
|
|
7212
|
+
var shaLine = (g) => `Head \`${g.sha.slice(0, 8)}\`.`;
|
|
7201
7213
|
var GATE_RED_AFTER = 3;
|
|
7202
7214
|
var lastLines = (output, n) => output.replace(/\u001b\[[0-9;]*m/g, "").trimEnd().split("\n").slice(-n).join("\n");
|
|
7203
7215
|
function gateRedTickets(merged, merges) {
|
|
@@ -7211,7 +7223,9 @@ function gateRedTickets(merged, merges) {
|
|
|
7211
7223
|
return out;
|
|
7212
7224
|
}
|
|
7213
7225
|
function gateRedComment(g) {
|
|
7214
|
-
return `${GATE_RED_MARKER}
|
|
7226
|
+
return `${GATE_RED_MARKER}
|
|
7227
|
+
> ${shaLine(g)}
|
|
7228
|
+
|
|
7215
7229
|
**This ticket is merged on the base, but the release test gate is red \u2014 nothing has shipped.**
|
|
7216
7230
|
|
|
7217
7231
|
The \`test\` hook (\`${g.hook}\`) has failed ${g.count} cycles running on the same head (\`${g.sha.slice(0, 8)}\`). The ticket's commit is on the base and will ship once the gate passes; it is not stranded, and re-setting it to \`verified\` will not help. Last lines of the hook's output:
|
|
@@ -7221,7 +7235,7 @@ ${g.tail}
|
|
|
7221
7235
|
\`\`\`
|
|
7222
7236
|
`;
|
|
7223
7237
|
}
|
|
7224
|
-
var alreadyReported = (ticket, g, events) => events.some((e) => e.record_id === ticket.id && e.event_type === "gate_red" && (e.message ?? "").includes(
|
|
7238
|
+
var alreadyReported = (ticket, g, events) => events.some((e) => e.record_id === ticket.id && e.event_type === "gate_red" && (e.message ?? "").includes(shaLine(g)));
|
|
7225
7239
|
|
|
7226
7240
|
// src/agent.ts
|
|
7227
7241
|
import { spawn as spawn2, spawnSync as spawnSync5 } from "child_process";
|
|
@@ -9115,7 +9129,7 @@ async function finishRunWorktree(dir, cfg, branch) {
|
|
|
9115
9129
|
}
|
|
9116
9130
|
|
|
9117
9131
|
// src/handoff-guard.ts
|
|
9118
|
-
var HANDOFF_BOUNCED_MARKER = "
|
|
9132
|
+
var HANDOFF_BOUNCED_MARKER = calloutHeader("WARNING", "Hand-off Bounced");
|
|
9119
9133
|
var pushedEventBody = (sha, remote, branch) => `pushed ${sha} to ${remote}/${branch}`;
|
|
9120
9134
|
function remoteTip(dir, remote, branch) {
|
|
9121
9135
|
const out = gitOk(dir, ["ls-remote", "--heads", remote, `refs/heads/${branch}`]);
|
|
@@ -9164,6 +9178,7 @@ function inspectHandoff(dir, cfg, ticket, role) {
|
|
|
9164
9178
|
}
|
|
9165
9179
|
function bounceComment(v, shipName2) {
|
|
9166
9180
|
return `${HANDOFF_BOUNCED_MARKER}
|
|
9181
|
+
|
|
9167
9182
|
**Hand-off bounced: the branch is not on \`${v.remote}\` at the tip this ticket was built to.**
|
|
9168
9183
|
|
|
9169
9184
|
${shipName2 ? `Ship: ${shipName2}
|
|
@@ -9298,7 +9313,7 @@ function repoConfigFor(route, t) {
|
|
|
9298
9313
|
}, target.dir);
|
|
9299
9314
|
return { target, cfg };
|
|
9300
9315
|
}
|
|
9301
|
-
var STRAY_HANDOFF_MARKER = "
|
|
9316
|
+
var STRAY_HANDOFF_MARKER = calloutHeader("WARNING", "Stray Hand-off");
|
|
9302
9317
|
async function guardStrayHandoffs(i) {
|
|
9303
9318
|
const st = i.contract.statuses;
|
|
9304
9319
|
if (i.role === "qa") return;
|
|
@@ -9311,6 +9326,7 @@ async function guardStrayHandoffs(i) {
|
|
|
9311
9326
|
await i.writer.postEvent(
|
|
9312
9327
|
t.id,
|
|
9313
9328
|
`${STRAY_HANDOFF_MARKER}
|
|
9329
|
+
|
|
9314
9330
|
**Hand-off bounced: this ticket is held by another ship.**
|
|
9315
9331
|
|
|
9316
9332
|
${i.shipName ? `${i.shipName}'s ` : "A "}${i.role} run set it to \`${st.handoff}\` without having claimed it. Set back to \`${st.building}\`; the holding ship's own hand-off is the one that counts.`,
|
|
@@ -10796,12 +10812,13 @@ async function mergeAndRelease(o, decision, fresh, releaseBranch, isolated = fal
|
|
|
10796
10812
|
}
|
|
10797
10813
|
|
|
10798
10814
|
// src/failure-alert.ts
|
|
10799
|
-
var FAILURE_ALERT_MARKER = "
|
|
10815
|
+
var FAILURE_ALERT_MARKER = calloutHeader("CAUTION", "Failure Alert");
|
|
10800
10816
|
function findOpenFailureAlert(tickets, headline) {
|
|
10801
10817
|
return tickets.find((t) => t.title === headline);
|
|
10802
10818
|
}
|
|
10803
10819
|
function ticketBody(n, route) {
|
|
10804
10820
|
return `${FAILURE_ALERT_MARKER}
|
|
10821
|
+
|
|
10805
10822
|
${n.detail ?? n.headline}
|
|
10806
10823
|
|
|
10807
10824
|
Filed automatically \u2014 no \`notify\` hook is configured for route \`${route}\`, so this is the crew's built-in fallback rather than a silent log line. Configure \`hooks.notify\` on this route to replace it.`;
|
|
@@ -10816,6 +10833,7 @@ async function applyFailureAlert(writer, n, route, openTickets, memberId, log, d
|
|
|
10816
10833
|
}
|
|
10817
10834
|
try {
|
|
10818
10835
|
await writer.postEvent(existing.id, `${FAILURE_ALERT_MARKER}
|
|
10836
|
+
|
|
10819
10837
|
Still failing: ${n.detail ?? n.headline}`, memberId, "failure_alert");
|
|
10820
10838
|
log.warn(`release failure recurring \u2014 commented on ${label}`, { step: "release" });
|
|
10821
10839
|
return { kind: "commented", issueId: label };
|
|
@@ -13287,10 +13305,11 @@ import { join as join25 } from "path";
|
|
|
13287
13305
|
// src/interrupted.ts
|
|
13288
13306
|
import { existsSync as existsSync25, readdirSync as readdirSync9, readFileSync as readFileSync18 } from "fs";
|
|
13289
13307
|
import { join as join24 } from "path";
|
|
13290
|
-
var INTERRUPTED_MARKER = "
|
|
13308
|
+
var INTERRUPTED_MARKER = calloutHeader("NOTE", "Interrupted");
|
|
13291
13309
|
function interruptedComment(b) {
|
|
13292
13310
|
const tree = b.dirty === null ? "The worktree could not be checked." : b.dirty ? "The worktree has uncommitted changes \u2014 they are this seat's own, from that run." : "The worktree is clean.";
|
|
13293
13311
|
return `${INTERRUPTED_MARKER}
|
|
13312
|
+
|
|
13294
13313
|
The agent run on this ticket was cut short (${b.reason}) before it handed on${b.branch ? `; its branch is \`${b.branch}\`` : ""}. ${tree} The next run on this seat resumes from the worktree rather than starting over.`;
|
|
13295
13314
|
}
|
|
13296
13315
|
var handled = /* @__PURE__ */ new Set();
|
package/package.json
CHANGED
|
@@ -31,9 +31,23 @@ prior conversation history exists.
|
|
|
31
31
|
on the Maps table), ordered by `version`. If one exists with
|
|
32
32
|
`status: active`, that's the current settled PRD — treat this session
|
|
33
33
|
as a **revision** interview, not a fresh one (see §5). If the most
|
|
34
|
-
recent one is `status: draft`
|
|
35
|
-
|
|
36
|
-
which they intend.
|
|
34
|
+
recent one is `status: draft` (never promoted) or `status: planning`
|
|
35
|
+
(an interview that was interrupted), you're likely continuing or
|
|
36
|
+
restarting that same attempt — ask the interviewee which they intend.
|
|
37
|
+
Ignore `abandoned` Maps when picking what to continue; they still count
|
|
38
|
+
when you compute the next `version`.
|
|
39
|
+
4. Before your first question, make sure a Map record for this session
|
|
40
|
+
exists and is at `status: planning`, so a grill in progress is visible
|
|
41
|
+
on the Maps table:
|
|
42
|
+
- Fresh grill: create the Map now (`epic_id`, a short `title`, the
|
|
43
|
+
`version` from §3 step 1, empty `content` and `conversation`) at
|
|
44
|
+
`planning`.
|
|
45
|
+
- Continuing a `draft` or an interrupted `planning` Map: set that same
|
|
46
|
+
record to `planning`.
|
|
47
|
+
- Restarting instead: set the old Map to `abandoned`, then create a new
|
|
48
|
+
version at `planning`.
|
|
49
|
+
- Revising an `active` Map: create the next version at `planning` now.
|
|
50
|
+
Leave the active one as it is until promotion.
|
|
37
51
|
|
|
38
52
|
## 2. Run the interview
|
|
39
53
|
|
|
@@ -52,11 +66,13 @@ and don't manufacture more questions once you've genuinely converged.
|
|
|
52
66
|
|
|
53
67
|
Once you've reached mutual agreement the session is done:
|
|
54
68
|
|
|
55
|
-
1. Compute the
|
|
69
|
+
1. Compute the Map's `version`: default to
|
|
56
70
|
`max(existing versions for this epic_id) + 1`. If the interviewee
|
|
57
71
|
wants a different version label (e.g. a deliberate jump for a major
|
|
58
|
-
revision), use what they say instead.
|
|
59
|
-
|
|
72
|
+
revision), use what they say instead. (You normally settled this when
|
|
73
|
+
you created the record in §1; change it there if the interviewee asks.)
|
|
74
|
+
2. Fill in the Maps record you created in §1 — update that same record,
|
|
75
|
+
do not create a second one:
|
|
60
76
|
- `epic_id` → this Epic
|
|
61
77
|
- `title` → short, human-readable
|
|
62
78
|
- `content` → the PRD itself, in Markdown. Write this as a real
|
|
@@ -66,8 +82,8 @@ Once you've reached mutual agreement the session is done:
|
|
|
66
82
|
actual back-and-forth (questions asked, answers given, points where
|
|
67
83
|
you changed your recommendation based on pushback). This is the
|
|
68
84
|
record of *how* you got to the PRD, not a duplicate of it.
|
|
69
|
-
- `status` →
|
|
70
|
-
`active` directly
|
|
85
|
+
- `status` → `draft`, now that the PRD is written. Never create a Map
|
|
86
|
+
as `active` directly, and never leave a finished PRD at `planning`.
|
|
71
87
|
|
|
72
88
|
## 4. Proposing child tickets
|
|
73
89
|
|
|
@@ -94,6 +110,9 @@ Alongside the draft Map, propose the child Issues implied by the PRD:
|
|
|
94
110
|
Before ending the session, ask the interviewee directly: **should this
|
|
95
111
|
Map go active now, or stay a draft?**
|
|
96
112
|
|
|
113
|
+
Only a `draft` Map can be promoted. A Map still at `planning` is an
|
|
114
|
+
unfinished interview and is never activated.
|
|
115
|
+
|
|
97
116
|
- **If they say stay draft:** leave everything as-is. Nothing else to
|
|
98
117
|
do. A later session (yours or someone else's) picks this back up.
|
|
99
118
|
- **If they say go active:**
|
|
@@ -124,6 +143,26 @@ Map go active now, or stay a draft?**
|
|
|
124
143
|
Do all of this yourself via direct MCP writes. There is no automation
|
|
125
144
|
watching for this — if you don't do it, it doesn't happen.
|
|
126
145
|
|
|
146
|
+
## 6. Abandoning a Map
|
|
147
|
+
|
|
148
|
+
A Map moves through `planning` (interview running) → `draft` (PRD written,
|
|
149
|
+
awaiting the promotion decision) → `active` (promoted) → `superseded` (a
|
|
150
|
+
later version went active). `abandoned` is the terminal state for a Map
|
|
151
|
+
that never got that far.
|
|
152
|
+
|
|
153
|
+
If the interviewee says to stop working on a Map that is at `planning` or
|
|
154
|
+
`draft`:
|
|
155
|
+
|
|
156
|
+
1. Set that Map to `status: abandoned`.
|
|
157
|
+
2. Ask whether its `draft` Issues should go to `closed_obsolete`, and
|
|
158
|
+
recommend yes. Apply their answer; leave Issues that have already left
|
|
159
|
+
`draft` alone.
|
|
160
|
+
3. Leave the Epic's status alone.
|
|
161
|
+
|
|
162
|
+
An abandoned Map is never reactivated. If work on the Epic resumes later,
|
|
163
|
+
that is a new Map at the next `version`. Do these writes directly via MCP,
|
|
164
|
+
as in §5.
|
|
165
|
+
|
|
127
166
|
## What this mode is never responsible for
|
|
128
167
|
|
|
129
168
|
- Writing or editing code.
|
|
@@ -31,9 +31,23 @@ prior conversation history exists.
|
|
|
31
31
|
on the Maps table), ordered by `version`. If one exists with
|
|
32
32
|
`status: active`, that's the current settled PRD — treat this session
|
|
33
33
|
as a **revision** interview, not a fresh one (see §5). If the most
|
|
34
|
-
recent one is `status: draft`
|
|
35
|
-
|
|
36
|
-
which they intend.
|
|
34
|
+
recent one is `status: draft` (never promoted) or `status: planning`
|
|
35
|
+
(an interview that was interrupted), you're likely continuing or
|
|
36
|
+
restarting that same attempt — ask the interviewee which they intend.
|
|
37
|
+
Ignore `abandoned` Maps when picking what to continue; they still count
|
|
38
|
+
when you compute the next `version`.
|
|
39
|
+
4. Before your first question, make sure a Map record for this session
|
|
40
|
+
exists and is at `status: planning`, so a grill in progress is visible
|
|
41
|
+
on the Maps table:
|
|
42
|
+
- Fresh grill: create the Map now (`epic_id`, a short `title`, the
|
|
43
|
+
`version` from §3 step 1, empty `content` and `conversation`) at
|
|
44
|
+
`planning`.
|
|
45
|
+
- Continuing a `draft` or an interrupted `planning` Map: set that same
|
|
46
|
+
record to `planning`.
|
|
47
|
+
- Restarting instead: set the old Map to `abandoned`, then create a new
|
|
48
|
+
version at `planning`.
|
|
49
|
+
- Revising an `active` Map: create the next version at `planning` now.
|
|
50
|
+
Leave the active one as it is until promotion.
|
|
37
51
|
|
|
38
52
|
## 2. Run the interview
|
|
39
53
|
|
|
@@ -52,11 +66,13 @@ and don't manufacture more questions once you've genuinely converged.
|
|
|
52
66
|
|
|
53
67
|
Once you've reached mutual agreement the session is done:
|
|
54
68
|
|
|
55
|
-
1. Compute the
|
|
69
|
+
1. Compute the Map's `version`: default to
|
|
56
70
|
`max(existing versions for this epic_id) + 1`. If the interviewee
|
|
57
71
|
wants a different version label (e.g. a deliberate jump for a major
|
|
58
|
-
revision), use what they say instead.
|
|
59
|
-
|
|
72
|
+
revision), use what they say instead. (You normally settled this when
|
|
73
|
+
you created the record in §1; change it there if the interviewee asks.)
|
|
74
|
+
2. Fill in the Maps record you created in §1 — update that same record,
|
|
75
|
+
do not create a second one:
|
|
60
76
|
- `epic_id` → this Epic
|
|
61
77
|
- `title` → short, human-readable
|
|
62
78
|
- `content` → the PRD itself, in Markdown. Write this as a real
|
|
@@ -66,8 +82,8 @@ Once you've reached mutual agreement the session is done:
|
|
|
66
82
|
actual back-and-forth (questions asked, answers given, points where
|
|
67
83
|
you changed your recommendation based on pushback). This is the
|
|
68
84
|
record of *how* you got to the PRD, not a duplicate of it.
|
|
69
|
-
- `status` →
|
|
70
|
-
`active` directly
|
|
85
|
+
- `status` → `draft`, now that the PRD is written. Never create a Map
|
|
86
|
+
as `active` directly, and never leave a finished PRD at `planning`.
|
|
71
87
|
|
|
72
88
|
## 4. Proposing child tickets
|
|
73
89
|
|
|
@@ -94,6 +110,9 @@ Alongside the draft Map, propose the child Issues implied by the PRD:
|
|
|
94
110
|
Before ending the session, ask the interviewee directly: **should this
|
|
95
111
|
Map go active now, or stay a draft?**
|
|
96
112
|
|
|
113
|
+
Only a `draft` Map can be promoted. A Map still at `planning` is an
|
|
114
|
+
unfinished interview and is never activated.
|
|
115
|
+
|
|
97
116
|
- **If they say stay draft:** leave everything as-is. Nothing else to
|
|
98
117
|
do. A later session (yours or someone else's) picks this back up.
|
|
99
118
|
- **If they say go active:**
|
|
@@ -124,6 +143,26 @@ Map go active now, or stay a draft?**
|
|
|
124
143
|
Do all of this yourself via direct MCP writes. There is no automation
|
|
125
144
|
watching for this — if you don't do it, it doesn't happen.
|
|
126
145
|
|
|
146
|
+
## 6. Abandoning a Map
|
|
147
|
+
|
|
148
|
+
A Map moves through `planning` (interview running) → `draft` (PRD written,
|
|
149
|
+
awaiting the promotion decision) → `active` (promoted) → `superseded` (a
|
|
150
|
+
later version went active). `abandoned` is the terminal state for a Map
|
|
151
|
+
that never got that far.
|
|
152
|
+
|
|
153
|
+
If the interviewee says to stop working on a Map that is at `planning` or
|
|
154
|
+
`draft`:
|
|
155
|
+
|
|
156
|
+
1. Set that Map to `status: abandoned`.
|
|
157
|
+
2. Ask whether its `draft` Issues should go to `closed_obsolete`, and
|
|
158
|
+
recommend yes. Apply their answer; leave Issues that have already left
|
|
159
|
+
`draft` alone.
|
|
160
|
+
3. Leave the Epic's status alone.
|
|
161
|
+
|
|
162
|
+
An abandoned Map is never reactivated. If work on the Epic resumes later,
|
|
163
|
+
that is a new Map at the next `version`. Do these writes directly via MCP,
|
|
164
|
+
as in §5.
|
|
165
|
+
|
|
127
166
|
## What this mode is never responsible for
|
|
128
167
|
|
|
129
168
|
- Writing or editing code.
|