@mu-crew/mu 2.0.0 → 3.1.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.
package/dist/cli.js CHANGED
@@ -1823,6 +1823,7 @@ INSERT INTO _op_clock (now_ms)
1823
1823
  "local_id",
1824
1824
  "title",
1825
1825
  "status",
1826
+ "substate",
1826
1827
  "impact",
1827
1828
  "effort_days",
1828
1829
  "owner_id",
@@ -1837,6 +1838,54 @@ INSERT INTO _op_clock (now_ms)
1837
1838
  }
1838
1839
  });
1839
1840
 
1841
+ // src/tasks/status.ts
1842
+ function isTaskStatus(s) {
1843
+ return TASK_STATUSES.includes(s);
1844
+ }
1845
+ function isValidPair(status, substate) {
1846
+ if (!isTaskStatus(status)) return false;
1847
+ return TASK_SUBSTATES[status].includes(substate);
1848
+ }
1849
+ function mapLegacyStatus(value) {
1850
+ const pair = LEGACY_STATUS_PAIRS[value];
1851
+ return pair ? { ...pair } : null;
1852
+ }
1853
+ function resolvePair(status, substate) {
1854
+ const legacy = mapLegacyStatus(status);
1855
+ if (legacy) return legacy;
1856
+ if (!isTaskStatus(status)) return null;
1857
+ if (typeof substate === "string" && isValidPair(status, substate)) {
1858
+ return { status, substate };
1859
+ }
1860
+ return { status, substate: DEFAULT_SUBSTATE[status] };
1861
+ }
1862
+ function formatPair(pair) {
1863
+ return pair.substate === DEFAULT_SUBSTATE[pair.status] ? pair.status : `${pair.status}/${pair.substate}`;
1864
+ }
1865
+ var TASK_STATUSES, TASK_STATUS_LIST, TASK_SUBSTATES, DEFAULT_SUBSTATE, TASK_SUBSTATE_ROWS, LEGACY_STATUS_PAIRS;
1866
+ var init_status = __esm({
1867
+ "src/tasks/status.ts"() {
1868
+ "use strict";
1869
+ TASK_STATUSES = ["OPEN", "IN_PROGRESS", "CLOSED"];
1870
+ TASK_STATUS_LIST = TASK_STATUSES.join(" | ");
1871
+ TASK_SUBSTATES = {
1872
+ OPEN: ["todo", "parked"],
1873
+ IN_PROGRESS: ["active"],
1874
+ CLOSED: ["done", "rejected", "wontfix", "duplicate", "superseded"]
1875
+ };
1876
+ DEFAULT_SUBSTATE = { OPEN: "todo", IN_PROGRESS: "active", CLOSED: "done" };
1877
+ TASK_SUBSTATE_ROWS = TASK_STATUSES.flatMap(
1878
+ (status) => TASK_SUBSTATES[status].map(
1879
+ (substate) => [status, substate, substate === DEFAULT_SUBSTATE[status] ? 1 : 0]
1880
+ )
1881
+ );
1882
+ LEGACY_STATUS_PAIRS = {
1883
+ REJECTED: { status: "CLOSED", substate: "rejected" },
1884
+ DEFERRED: { status: "OPEN", substate: "parked" }
1885
+ };
1886
+ }
1887
+ });
1888
+
1840
1889
  // src/db.ts
1841
1890
  import { randomUUID } from "crypto";
1842
1891
  import { mkdirSync } from "fs";
@@ -1862,6 +1911,13 @@ function openDb(options = {}) {
1862
1911
  db.pragma("foreign_keys = ON");
1863
1912
  db.pragma("busy_timeout = 5000");
1864
1913
  const detectedVersion = detectExistingSchemaVersion(db);
1914
+ if (detectedVersion !== null && detectedVersion > CURRENT_SCHEMA_VERSION) {
1915
+ try {
1916
+ db.close();
1917
+ } catch {
1918
+ }
1919
+ throw new SchemaTooNewError(detectedVersion, CURRENT_SCHEMA_VERSION);
1920
+ }
1865
1921
  if (detectedVersion !== null && detectedVersion < MIN_ACCEPTED_SCHEMA_VERSION) {
1866
1922
  try {
1867
1923
  db.close();
@@ -1920,6 +1976,12 @@ function applySchema(db) {
1920
1976
  db.exec("BEGIN IMMEDIATE");
1921
1977
  try {
1922
1978
  db.exec(CURRENT_SCHEMA);
1979
+ const seed = db.prepare(
1980
+ "INSERT OR IGNORE INTO task_substates (status, substate, is_default) VALUES (?, ?, ?)"
1981
+ );
1982
+ for (const [status, substate, isDefault] of TASK_SUBSTATE_ROWS) {
1983
+ seed.run(status, substate, isDefault);
1984
+ }
1923
1985
  db.exec("COMMIT");
1924
1986
  } catch (err) {
1925
1987
  db.exec("ROLLBACK");
@@ -1930,11 +1992,12 @@ function applySchema(db) {
1930
1992
  CURRENT_SCHEMA_VERSION
1931
1993
  );
1932
1994
  }
1933
- var WorkstreamNotFoundError, SchemaTooOldError, CURRENT_SCHEMA_VERSION, MIN_ACCEPTED_SCHEMA_VERSION, EXPECTED_TABLES, SYNCED_ENTITIES, MACHINE_LOCAL_ENTITIES, READY_VIEW_SQL, BLOCKED_VIEW_SQL, GOALS_VIEW_SQL, CURRENT_SCHEMA;
1995
+ var WorkstreamNotFoundError, SchemaTooOldError, SchemaTooNewError, CURRENT_SCHEMA_VERSION, MIN_ACCEPTED_SCHEMA_VERSION, EXPECTED_TABLES, SYNCED_ENTITIES, MACHINE_LOCAL_ENTITIES, READY_VIEW_SQL, BLOCKED_VIEW_SQL, GOALS_VIEW_SQL, CURRENT_SCHEMA;
1934
1996
  var init_db = __esm({
1935
1997
  "src/db.ts"() {
1936
1998
  "use strict";
1937
1999
  init_capture();
2000
+ init_status();
1938
2001
  WorkstreamNotFoundError = class extends Error {
1939
2002
  constructor(workstream) {
1940
2003
  super(`no such workstream: ${workstream}`);
@@ -1974,7 +2037,7 @@ var init_db = __esm({
1974
2037
  command: `mv "\${MU_DB_PATH:-$HOME/.local/state/mu/mu.db}" "\${MU_DB_PATH:-$HOME/.local/state/mu/mu.db}.old"`
1975
2038
  },
1976
2039
  {
1977
- intent: `Migrate the preserved DB into a fresh v${this.requiredVersion} file`,
2040
+ intent: `Migrate the old DB into a fresh v${this.requiredVersion} DB`,
1978
2041
  command: `npx tsx scripts/migrate.ts "\${MU_DB_PATH:-$HOME/.local/state/mu/mu.db}.old" --out /tmp/mu-v${this.requiredVersion}.db`
1979
2042
  },
1980
2043
  {
@@ -1984,8 +2047,21 @@ var init_db = __esm({
1984
2047
  ];
1985
2048
  }
1986
2049
  };
1987
- CURRENT_SCHEMA_VERSION = 10;
1988
- MIN_ACCEPTED_SCHEMA_VERSION = 10;
2050
+ SchemaTooNewError = class extends Error {
2051
+ constructor(detectedVersion, supportedVersion) {
2052
+ super(`DB is v${detectedVersion}; this mu understands up to v${supportedVersion}. Upgrade mu.`);
2053
+ this.detectedVersion = detectedVersion;
2054
+ this.supportedVersion = supportedVersion;
2055
+ }
2056
+ detectedVersion;
2057
+ supportedVersion;
2058
+ name = "SchemaTooNewError";
2059
+ errorNextSteps() {
2060
+ return [{ intent: "Check which mu is on PATH", command: "which mu && mu --version" }];
2061
+ }
2062
+ };
2063
+ CURRENT_SCHEMA_VERSION = 11;
2064
+ MIN_ACCEPTED_SCHEMA_VERSION = 11;
1989
2065
  EXPECTED_TABLES = [
1990
2066
  "agents",
1991
2067
  "machine_identity",
@@ -1994,6 +2070,7 @@ var init_db = __esm({
1994
2070
  "sync_peers",
1995
2071
  "task_edges",
1996
2072
  "task_notes",
2073
+ "task_substates",
1997
2074
  "tasks",
1998
2075
  "vcs_workspaces",
1999
2076
  "workstreams"
@@ -2006,6 +2083,7 @@ CREATE VIEW ready AS
2006
2083
  SELECT t.*
2007
2084
  FROM tasks t
2008
2085
  WHERE t.status = 'OPEN'
2086
+ AND t.substate <> 'parked'
2009
2087
  AND NOT EXISTS (
2010
2088
  SELECT 1
2011
2089
  FROM task_edges e
@@ -2101,6 +2179,19 @@ CREATE TABLE IF NOT EXISTS agents (
2101
2179
  CREATE INDEX IF NOT EXISTS idx_agents_workstream ON agents (workstream_id);
2102
2180
  CREATE INDEX IF NOT EXISTS idx_agents_status ON agents (status);
2103
2181
 
2182
+ -- task_substates: the legal (status, substate) pairs. Seeded from
2183
+ -- TASK_SUBSTATE_ROWS (src/tasks/status.ts) by applySchema; exactly one
2184
+ -- default per status.
2185
+ CREATE TABLE IF NOT EXISTS task_substates (
2186
+ status TEXT NOT NULL,
2187
+ substate TEXT NOT NULL,
2188
+ is_default INTEGER NOT NULL DEFAULT 0 CHECK (is_default IN (0, 1)),
2189
+ PRIMARY KEY (status, substate)
2190
+ );
2191
+
2192
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_task_substates_one_default
2193
+ ON task_substates (status) WHERE is_default = 1;
2194
+
2104
2195
  -- tasks: per-workstream unique on local_id (TRULY local now \u2014
2105
2196
  -- different workstreams may reuse the same local_id).
2106
2197
  CREATE TABLE IF NOT EXISTS tasks (
@@ -2110,6 +2201,9 @@ CREATE TABLE IF NOT EXISTS tasks (
2110
2201
  title TEXT NOT NULL,
2111
2202
  status TEXT NOT NULL DEFAULT 'OPEN',
2112
2203
  -- OPEN | IN_PROGRESS | CLOSED \u2014 see VOCABULARY.md.
2204
+ substate TEXT NOT NULL,
2205
+ -- Qualifies status. No DEFAULT on purpose: a writer that forgets it
2206
+ -- must fail loudly rather than silently pick one.
2113
2207
  impact INTEGER NOT NULL,
2114
2208
  effort_days REAL NOT NULL,
2115
2209
  owner_id INTEGER REFERENCES agents (id) ON DELETE SET NULL,
@@ -2118,7 +2212,8 @@ CREATE TABLE IF NOT EXISTS tasks (
2118
2212
  UNIQUE (workstream_id, local_id),
2119
2213
  CHECK (impact BETWEEN 1 AND 100),
2120
2214
  CHECK (effort_days > 0),
2121
- CHECK (status IN ('OPEN', 'IN_PROGRESS', 'CLOSED'))
2215
+ FOREIGN KEY (status, substate) REFERENCES task_substates (status, substate)
2216
+ DEFERRABLE INITIALLY DEFERRED
2122
2217
  );
2123
2218
 
2124
2219
  CREATE INDEX IF NOT EXISTS idx_tasks_workstream ON tasks (workstream_id);
@@ -2282,6 +2377,9 @@ var init_glyphs = __esm({
2282
2377
  /** Task is blocked by an incoming edge. */
2283
2378
  blocked: "\uF0C1",
2284
2379
  // nf-fa-link (chain)
2380
+ /** Task is parked (OPEN/parked): open, but set aside on purpose. */
2381
+ parked: "\uF04C",
2382
+ // nf-fa-pause
2285
2383
  /** Track whose roots merged (diamond dependency). */
2286
2384
  merge: "\uF074",
2287
2385
  // nf-fa-random
@@ -4963,6 +5061,7 @@ function rowFromDb3(row2) {
4963
5061
  workstreamName: row2.workstream,
4964
5062
  title: row2.title,
4965
5063
  status: row2.status,
5064
+ substate: row2.substate,
4966
5065
  impact: row2.impact,
4967
5066
  effortDays: row2.effort_days,
4968
5067
  ownerName: row2.owner,
@@ -5006,6 +5105,7 @@ var init_core2 = __esm({
5006
5105
  ws.name AS workstream,
5007
5106
  t.title AS title,
5008
5107
  t.status AS status,
5108
+ t.substate AS substate,
5009
5109
  t.impact AS impact,
5010
5110
  t.effort_days AS effort_days,
5011
5111
  ag.name AS owner,
@@ -5034,6 +5134,10 @@ function getTask(db, localId, workstream) {
5034
5134
  ).get(wsId, localId);
5035
5135
  return row2 ? rowFromDb3(row2) : void 0;
5036
5136
  }
5137
+ function asList(v) {
5138
+ if (v === void 0) return void 0;
5139
+ return typeof v === "string" ? [v] : v;
5140
+ }
5037
5141
  function listTasks(db, workstream, opts = {}) {
5038
5142
  const statuses = opts.status === void 0 ? void 0 : Array.isArray(opts.status) ? opts.status : [opts.status];
5039
5143
  const where = [];
@@ -5048,6 +5152,11 @@ function listTasks(db, workstream, opts = {}) {
5048
5152
  where.push(`t.status IN (${statuses.map(() => "?").join(", ")})`);
5049
5153
  params.push(...statuses);
5050
5154
  }
5155
+ const substates = asList(opts.substate);
5156
+ if (substates !== void 0 && substates.length > 0) {
5157
+ where.push(`t.substate IN (${substates.map(() => "?").join(", ")})`);
5158
+ params.push(...substates);
5159
+ }
5051
5160
  const sql = where.length === 0 ? `SELECT ${SELECT_TASK_COLS} ${TASK_FROM_JOIN} ORDER BY t.local_id` : `SELECT ${SELECT_TASK_COLS} ${TASK_FROM_JOIN} WHERE ${where.join(" AND ")} ORDER BY t.local_id`;
5052
5161
  const rows = db.prepare(sql).all(...params);
5053
5162
  return rows.map(rowFromDb3);
@@ -5062,6 +5171,11 @@ function listReady(db, workstream, opts = {}) {
5062
5171
  where.push(`v.status IN (${statuses.map(() => "?").join(", ")})`);
5063
5172
  params.push(...statuses);
5064
5173
  }
5174
+ const substates = asList(opts.substate);
5175
+ if (substates !== void 0 && substates.length > 0) {
5176
+ where.push(`v.substate IN (${substates.map(() => "?").join(", ")})`);
5177
+ params.push(...substates);
5178
+ }
5065
5179
  const rows = db.prepare(
5066
5180
  `SELECT ${SELECT_VIEW_COLS} ${VIEW_FROM_JOIN("ready")} WHERE ${where.join(" AND ")} ORDER BY v.local_id`
5067
5181
  ).all(...params);
@@ -5154,6 +5268,7 @@ var init_queries = __esm({
5154
5268
  ws.name AS workstream,
5155
5269
  v.title AS title,
5156
5270
  v.status AS status,
5271
+ v.substate AS substate,
5157
5272
  v.impact AS impact,
5158
5273
  v.effort_days AS effort_days,
5159
5274
  ag.name AS owner,
@@ -5216,12 +5331,13 @@ var init_id = __esm({
5216
5331
  });
5217
5332
 
5218
5333
  // src/tasks/errors.ts
5219
- var TaskNotFoundError, TaskIdInvalidError, TaskExistsError, TaskNotInWorkstreamError, TaskAlreadyOwnedError, ClaimerNotRegisteredError, TaskClaimStaleWorkspaceError, CycleError, ReaperDetectedDuringWaitError, StallDetectedDuringWaitError, CrossWorkstreamEdgeError;
5334
+ var TaskNotFoundError, TaskIdInvalidError, TaskExistsError, TaskNotInWorkstreamError, TaskAlreadyOwnedError, ClaimerNotRegisteredError, TaskClaimStaleWorkspaceError, CycleError, ReaperDetectedDuringWaitError, StallDetectedDuringWaitError, CrossWorkstreamEdgeError, SubstateReasonRequiredError, TaskParkStateError, TaskParkedError, InvalidSubstateError;
5220
5335
  var init_errors2 = __esm({
5221
5336
  "src/tasks/errors.ts"() {
5222
5337
  "use strict";
5223
5338
  init_workspace();
5224
5339
  init_id();
5340
+ init_status();
5225
5341
  TaskNotFoundError = class extends Error {
5226
5342
  constructor(taskId) {
5227
5343
  super(`no such task: ${taskId}`);
@@ -5512,6 +5628,84 @@ var init_errors2 = __esm({
5512
5628
  ];
5513
5629
  }
5514
5630
  };
5631
+ SubstateReasonRequiredError = class extends Error {
5632
+ constructor(verb, substate, taskId) {
5633
+ super(
5634
+ verb === "close" ? `closing ${taskId} as ${substate} needs a reason (--why "...")` : `parking ${taskId} needs a reason (--why "...")`
5635
+ );
5636
+ this.verb = verb;
5637
+ this.substate = substate;
5638
+ this.taskId = taskId;
5639
+ }
5640
+ verb;
5641
+ substate;
5642
+ taskId;
5643
+ name = "SubstateReasonRequiredError";
5644
+ errorNextSteps() {
5645
+ const cmd = this.verb === "close" ? `mu task close ${this.taskId} --as ${this.substate} --why "<reason>"` : `mu task park ${this.taskId} --why "<reason>"`;
5646
+ return [{ intent: "Retry with a reason", command: cmd }];
5647
+ }
5648
+ };
5649
+ TaskParkStateError = class extends Error {
5650
+ constructor(taskId, status, workstream) {
5651
+ super(
5652
+ status === "IN_PROGRESS" ? `cannot park ${taskId}: it is IN_PROGRESS (release it first)` : `cannot park ${taskId}: it is CLOSED (open it first)`
5653
+ );
5654
+ this.taskId = taskId;
5655
+ this.status = status;
5656
+ this.workstream = workstream;
5657
+ }
5658
+ taskId;
5659
+ status;
5660
+ workstream;
5661
+ name = "TaskParkStateError";
5662
+ errorNextSteps() {
5663
+ const ws = this.workstream;
5664
+ const first = this.status === "IN_PROGRESS" ? { intent: "Release the claim first", command: `mu task release ${this.taskId} -w ${ws}` } : { intent: "Reopen the task first", command: `mu task open ${this.taskId} -w ${ws}` };
5665
+ return [
5666
+ first,
5667
+ {
5668
+ intent: "Then park it",
5669
+ command: `mu task park ${this.taskId} --why "<reason>" -w ${ws}`
5670
+ }
5671
+ ];
5672
+ }
5673
+ };
5674
+ TaskParkedError = class extends Error {
5675
+ constructor(taskId, workstream) {
5676
+ super(`task ${taskId} is parked (OPEN/parked); unpark it or claim with --force`);
5677
+ this.taskId = taskId;
5678
+ this.workstream = workstream;
5679
+ }
5680
+ taskId;
5681
+ workstream;
5682
+ name = "TaskParkedError";
5683
+ errorNextSteps() {
5684
+ const ws = this.workstream;
5685
+ return [
5686
+ { intent: "Unpark it, then claim", command: `mu task unpark ${this.taskId} -w ${ws}` },
5687
+ { intent: "Claim it anyway", command: `mu task claim ${this.taskId} --force -w ${ws}` }
5688
+ ];
5689
+ }
5690
+ };
5691
+ InvalidSubstateError = class extends Error {
5692
+ constructor(status, substate) {
5693
+ super(
5694
+ `invalid substate for ${status}: ${JSON.stringify(substate)} (expected one of: ${TASK_SUBSTATES[status].join(", ")})`
5695
+ );
5696
+ this.status = status;
5697
+ this.substate = substate;
5698
+ }
5699
+ status;
5700
+ substate;
5701
+ name = "InvalidSubstateError";
5702
+ errorNextSteps() {
5703
+ return TASK_SUBSTATES[this.status].map((s) => ({
5704
+ intent: `Use ${this.status}/${s}`,
5705
+ command: this.status === "CLOSED" ? `mu task close <id> --as ${s}` : `mu task list --substate ${s}`
5706
+ }));
5707
+ }
5708
+ };
5515
5709
  }
5516
5710
  });
5517
5711
 
@@ -5551,12 +5745,12 @@ function getTaskEdgesWithStatus(db, taskLocalId, workstream) {
5551
5745
  const taskId = taskIdFor(db, taskLocalId, workstream);
5552
5746
  if (taskId === null) return { blockers: [], dependents: [] };
5553
5747
  const blockers = db.prepare(
5554
- `SELECT t.local_id AS name, t.status AS status FROM task_edges e
5748
+ `SELECT t.local_id AS name, t.status AS status, t.substate AS substate FROM task_edges e
5555
5749
  JOIN tasks t ON t.id = e.from_task_id
5556
5750
  WHERE e.to_task_id = ? ORDER BY t.local_id`
5557
5751
  ).all(taskId);
5558
5752
  const dependents = db.prepare(
5559
- `SELECT t.local_id AS name, t.status AS status FROM task_edges e
5753
+ `SELECT t.local_id AS name, t.status AS status, t.substate AS substate FROM task_edges e
5560
5754
  JOIN tasks t ON t.id = e.to_task_id
5561
5755
  WHERE e.from_task_id = ? ORDER BY t.local_id`
5562
5756
  ).all(taskId);
@@ -6038,8 +6232,8 @@ function addTaskImpl(db, opts) {
6038
6232
  }
6039
6233
  const now = (/* @__PURE__ */ new Date()).toISOString();
6040
6234
  const insertResult = db.prepare(
6041
- `INSERT INTO tasks (workstream_id, local_id, title, status, impact, effort_days, created_at, updated_at)
6042
- VALUES (?, ?, ?, 'OPEN', ?, ?, ?, ?)`
6235
+ `INSERT INTO tasks (workstream_id, local_id, title, status, substate, impact, effort_days, created_at, updated_at)
6236
+ VALUES (?, ?, ?, 'OPEN', 'todo', ?, ?, ?, ?)`
6043
6237
  ).run(wsId, opts.localId, opts.title, opts.impact, opts.effortDays, now, now);
6044
6238
  const newTaskId = Number(insertResult.lastInsertRowid);
6045
6239
  if (opts.blockedBy && opts.blockedBy.length > 0) {
@@ -6088,10 +6282,10 @@ function addNote(db, taskLocalId, content, opts) {
6088
6282
  return withOpContext(
6089
6283
  db,
6090
6284
  { intent: "task.note", actor: opts.author, group: "new" },
6091
- () => addNoteImpl(db, taskLocalId, content, opts)
6285
+ () => insertNote(db, taskLocalId, content, opts)
6092
6286
  );
6093
6287
  }
6094
- function addNoteImpl(db, taskLocalId, content, opts) {
6288
+ function insertNote(db, taskLocalId, content, opts) {
6095
6289
  const task = getTask(db, taskLocalId, opts.workstream);
6096
6290
  if (!task) {
6097
6291
  throw new TaskNotFoundError(taskLocalId);
@@ -6216,19 +6410,32 @@ function setTaskStatus(db, localId, status, opts) {
6216
6410
  );
6217
6411
  }
6218
6412
  function setTaskStatusImpl(db, localId, status, opts) {
6413
+ const substate = opts.substate ?? DEFAULT_SUBSTATE[status];
6414
+ if (!isValidPair(status, substate)) throw new InvalidSubstateError(status, substate);
6219
6415
  const before = getTask(db, localId, opts.workstream);
6220
6416
  if (!before) throw new TaskNotFoundError(localId);
6221
- if (before.status === status) {
6222
- return { previousStatus: before.status, status, changed: false };
6417
+ const base = {
6418
+ previousStatus: before.status,
6419
+ status,
6420
+ previousSubstate: before.substate,
6421
+ substate
6422
+ };
6423
+ if (before.status === status && before.substate === substate) {
6424
+ return { ...base, changed: false };
6223
6425
  }
6224
6426
  db.prepare(
6225
- `UPDATE tasks SET status = ?, updated_at = ?
6427
+ `UPDATE tasks SET status = ?, substate = ?, updated_at = ?
6226
6428
  WHERE local_id = ?
6227
6429
  AND workstream_id = (SELECT id FROM workstreams WHERE name = ?)`
6228
- ).run(status, (/* @__PURE__ */ new Date()).toISOString(), localId, before.workstreamName);
6229
- return { previousStatus: before.status, status, changed: true };
6430
+ ).run(status, substate, (/* @__PURE__ */ new Date()).toISOString(), localId, before.workstreamName);
6431
+ return { ...base, changed: true };
6230
6432
  }
6231
6433
  function closeTask(db, localId, opts) {
6434
+ const as = opts.as ?? "done";
6435
+ if (!isValidPair("CLOSED", as)) throw new InvalidSubstateError("CLOSED", as);
6436
+ if (as !== "done" && (opts.why === void 0 || opts.why.trim() === "")) {
6437
+ throw new SubstateReasonRequiredError("close", as, localId);
6438
+ }
6232
6439
  return withOpContext(
6233
6440
  db,
6234
6441
  { intent: "task.close", actor: opts.author, group: "new" },
@@ -6236,6 +6443,7 @@ function closeTask(db, localId, opts) {
6236
6443
  );
6237
6444
  }
6238
6445
  function closeTaskImpl(db, localId, opts) {
6446
+ const as = opts.as ?? "done";
6239
6447
  const before = getTask(db, localId, opts.workstream);
6240
6448
  if (opts.ifReady && before) {
6241
6449
  const edges = getTaskEdgesWithStatus(db, localId, before.workstreamName);
@@ -6245,14 +6453,43 @@ function closeTaskImpl(db, localId, opts) {
6245
6453
  skipped: "not_ready",
6246
6454
  previousStatus: before.status,
6247
6455
  status: before.status,
6456
+ previousSubstate: before.substate,
6457
+ substate: before.substate,
6248
6458
  changed: false,
6249
6459
  blockingIds: blocking
6250
6460
  };
6251
6461
  }
6252
6462
  }
6253
- const r = setTaskStatus(db, localId, "CLOSED", opts);
6254
- if (r.changed && before) recordEvidenceNote(db, localId, before.workstreamName, "CLOSE", opts);
6255
- return r;
6463
+ return db.transaction(() => {
6464
+ const track = as !== "done" && before !== void 0;
6465
+ const readyBefore = track ? readyDependents(db, localId, before.workstreamName) : [];
6466
+ const r = setTaskStatus(db, localId, "CLOSED", {
6467
+ workstream: opts.workstream,
6468
+ substate: as,
6469
+ ...opts.evidence !== void 0 ? { evidence: opts.evidence } : {}
6470
+ });
6471
+ if (!r.changed || !before) return { ...r, unblocked: [] };
6472
+ recordReasonNote(db, localId, before.workstreamName, as, opts.why, opts.author);
6473
+ recordEvidenceNote(db, localId, before.workstreamName, "CLOSE", opts);
6474
+ const unblocked = track ? readyDependents(db, localId, before.workstreamName).filter((n) => !readyBefore.includes(n)) : [];
6475
+ return { ...r, unblocked };
6476
+ })();
6477
+ }
6478
+ function readyDependents(db, localId, workstream) {
6479
+ const id = taskIdFor(db, localId, workstream);
6480
+ if (id === null) return [];
6481
+ const rows = db.prepare(
6482
+ `SELECT r.local_id AS name FROM task_edges e
6483
+ JOIN ready r ON r.id = e.to_task_id
6484
+ WHERE e.from_task_id = ? ORDER BY r.local_id`
6485
+ ).all(id);
6486
+ return rows.map((r) => r.name);
6487
+ }
6488
+ function recordReasonNote(db, localId, workstream, substate, why, author) {
6489
+ if (why === void 0 || why.trim() === "") return;
6490
+ const noteOpts = { workstream };
6491
+ if (author !== void 0 && author !== "") noteOpts.author = author;
6492
+ insertNote(db, localId, `${substate.toUpperCase()}: ${why}`, noteOpts);
6256
6493
  }
6257
6494
  function openTask(db, localId, opts) {
6258
6495
  return withOpContext(db, { intent: "task.open", group: "new" }, () => {
@@ -6262,14 +6499,61 @@ function openTask(db, localId, opts) {
6262
6499
  return r;
6263
6500
  });
6264
6501
  }
6502
+ function parkTask(db, localId, opts) {
6503
+ if (opts.why.trim() === "") throw new SubstateReasonRequiredError("park", "parked", localId);
6504
+ return withOpContext(
6505
+ db,
6506
+ { intent: "task.park", actor: opts.author, group: "new" },
6507
+ () => db.transaction(() => {
6508
+ const before = getTask(db, localId, opts.workstream);
6509
+ if (!before) throw new TaskNotFoundError(localId);
6510
+ if (before.status !== "OPEN") {
6511
+ throw new TaskParkStateError(localId, before.status, before.workstreamName);
6512
+ }
6513
+ const r = setTaskStatus(db, localId, "OPEN", {
6514
+ workstream: opts.workstream,
6515
+ substate: "parked"
6516
+ });
6517
+ if (r.changed) {
6518
+ recordReasonNote(db, localId, before.workstreamName, "parked", opts.why, opts.author);
6519
+ recordEvidenceNote(db, localId, before.workstreamName, "PARK", opts);
6520
+ }
6521
+ return r;
6522
+ })()
6523
+ );
6524
+ }
6525
+ function unparkTask(db, localId, opts) {
6526
+ return withOpContext(
6527
+ db,
6528
+ { intent: "task.unpark", group: "new" },
6529
+ () => db.transaction(() => {
6530
+ const before = getTask(db, localId, opts.workstream);
6531
+ if (!before) throw new TaskNotFoundError(localId);
6532
+ if (before.status !== "OPEN" || before.substate !== "parked") {
6533
+ return {
6534
+ previousStatus: before.status,
6535
+ status: before.status,
6536
+ previousSubstate: before.substate,
6537
+ substate: before.substate,
6538
+ changed: false
6539
+ };
6540
+ }
6541
+ const r = setTaskStatus(db, localId, "OPEN", { workstream: opts.workstream });
6542
+ recordEvidenceNote(db, localId, before.workstreamName, "UNPARK", opts);
6543
+ return r;
6544
+ })()
6545
+ );
6546
+ }
6265
6547
  var init_lifecycle = __esm({
6266
6548
  "src/tasks/lifecycle.ts"() {
6267
6549
  "use strict";
6268
6550
  init_op_context();
6551
+ init_core2();
6269
6552
  init_edges();
6270
6553
  init_edit();
6271
6554
  init_errors2();
6272
6555
  init_queries();
6556
+ init_status();
6273
6557
  }
6274
6558
  });
6275
6559
 
@@ -6296,10 +6580,16 @@ function releaseTaskImpl(db, localId, opts) {
6296
6580
  };
6297
6581
  }
6298
6582
  db.prepare(
6299
- `UPDATE tasks SET owner_id = NULL, status = ?, updated_at = ?
6583
+ `UPDATE tasks SET owner_id = NULL, status = ?, substate = ?, updated_at = ?
6300
6584
  WHERE local_id = ?
6301
6585
  AND workstream_id = (SELECT id FROM workstreams WHERE name = ?)`
6302
- ).run(newStatus, (/* @__PURE__ */ new Date()).toISOString(), localId, before.workstreamName);
6586
+ ).run(
6587
+ newStatus,
6588
+ statusChanges ? DEFAULT_SUBSTATE[newStatus] : before.substate,
6589
+ (/* @__PURE__ */ new Date()).toISOString(),
6590
+ localId,
6591
+ before.workstreamName
6592
+ );
6303
6593
  recordEvidenceNote(db, localId, before.workstreamName, "RELEASE", opts);
6304
6594
  return {
6305
6595
  previousOwnerName: before.ownerName,
@@ -6340,11 +6630,15 @@ async function claimTaskImpl(db, localId, opts) {
6340
6630
  () => db.transaction(() => {
6341
6631
  const before = getTask(db, localId, opts.workstream);
6342
6632
  if (!before) throw new TaskNotFoundError(localId);
6633
+ assertNotParked(before, opts);
6343
6634
  const now = (/* @__PURE__ */ new Date()).toISOString();
6344
6635
  const result = db.prepare(
6345
6636
  `UPDATE tasks
6346
6637
  SET owner_id = ?,
6347
6638
  status = CASE WHEN status = 'OPEN' THEN 'IN_PROGRESS' ELSE status END,
6639
+ -- SQLite evaluates every SET expression against the OLD
6640
+ -- row, so this CASE still sees the pre-claim status.
6641
+ substate = CASE WHEN status = 'OPEN' THEN 'active' ELSE substate END,
6348
6642
  updated_at = ?
6349
6643
  WHERE local_id = ?
6350
6644
  AND workstream_id = (SELECT id FROM workstreams WHERE name = ?)
@@ -6366,6 +6660,11 @@ async function claimTaskImpl(db, localId, opts) {
6366
6660
  })()
6367
6661
  );
6368
6662
  }
6663
+ function assertNotParked(task, opts) {
6664
+ if (task.status === "OPEN" && task.substate === "parked" && opts.force !== true) {
6665
+ throw new TaskParkedError(task.name, task.workstreamName);
6666
+ }
6667
+ }
6369
6668
  async function resolveActorIdentity() {
6370
6669
  const worker = await resolveWorkerIdentity();
6371
6670
  if (worker !== void 0) return worker;
@@ -6391,10 +6690,12 @@ async function claimSelf(db, localId, opts) {
6391
6690
  () => db.transaction(() => {
6392
6691
  const before = getTask(db, localId, opts.workstream);
6393
6692
  if (!before) throw new TaskNotFoundError(localId);
6693
+ assertNotParked(before, opts);
6394
6694
  const now = (/* @__PURE__ */ new Date()).toISOString();
6395
6695
  const result = db.prepare(
6396
6696
  `UPDATE tasks
6397
6697
  SET status = CASE WHEN status = 'OPEN' THEN 'IN_PROGRESS' ELSE status END,
6698
+ substate = CASE WHEN status = 'OPEN' THEN 'active' ELSE substate END,
6398
6699
  updated_at = ?
6399
6700
  WHERE local_id = ?
6400
6701
  AND workstream_id = (SELECT id FROM workstreams WHERE name = ?)
@@ -6424,23 +6725,7 @@ var init_claim = __esm({
6424
6725
  init_errors2();
6425
6726
  init_lifecycle();
6426
6727
  init_queries();
6427
- }
6428
- });
6429
-
6430
- // src/tasks/status.ts
6431
- function isTaskStatus(s) {
6432
- return TASK_STATUSES.includes(s);
6433
- }
6434
- function normalizeTaskStatus(value) {
6435
- return RETIRED_STATUSES.has(value) ? "OPEN" : value;
6436
- }
6437
- var TASK_STATUSES, RETIRED_STATUSES, TASK_STATUS_LIST;
6438
- var init_status = __esm({
6439
- "src/tasks/status.ts"() {
6440
- "use strict";
6441
- TASK_STATUSES = ["OPEN", "IN_PROGRESS", "CLOSED"];
6442
- RETIRED_STATUSES = /* @__PURE__ */ new Set(["REJECTED", "DEFERRED"]);
6443
- TASK_STATUS_LIST = TASK_STATUSES.join(" | ");
6728
+ init_status();
6444
6729
  }
6445
6730
  });
6446
6731
 
@@ -6492,6 +6777,7 @@ async function waitForTasks(db, input, opts) {
6492
6777
  for (const ref of refs) {
6493
6778
  const row2 = getTask(db, ref.name, ref.workstreamName);
6494
6779
  const status = row2?.status ?? "OPEN";
6780
+ const substate = row2?.substate ?? DEFAULT_SUBSTATE[status];
6495
6781
  const owner = row2?.ownerName ?? null;
6496
6782
  const ageMs2 = await stuckAgeMs(status, owner, ref.workstreamName);
6497
6783
  const stuck = ageMs2 !== null;
@@ -6520,6 +6806,7 @@ async function waitForTasks(db, input, opts) {
6520
6806
  workstreamName: ref.workstreamName,
6521
6807
  name: ref.name,
6522
6808
  status,
6809
+ substate,
6523
6810
  owner,
6524
6811
  reachedTarget: status === target,
6525
6812
  stuck
@@ -6559,6 +6846,7 @@ var init_wait = __esm({
6559
6846
  init_logs();
6560
6847
  init_errors2();
6561
6848
  init_queries();
6849
+ init_status();
6562
6850
  currentWaitSleep = (ms) => new Promise((resolve8) => setTimeout(resolve8, ms));
6563
6851
  pollCount = 0;
6564
6852
  defaultStuckWarn = (msg) => {
@@ -7021,10 +7309,9 @@ function deleteAgent(db, name, workstream) {
7021
7309
  const result = db.prepare("DELETE FROM agents WHERE id = ?").run(agentId);
7022
7310
  if (result.changes === 0) return false;
7023
7311
  for (const t of stuck) {
7024
- db.prepare("UPDATE tasks SET status = 'OPEN', updated_at = ? WHERE id = ?").run(
7025
- (/* @__PURE__ */ new Date()).toISOString(),
7026
- t.taskId
7027
- );
7312
+ db.prepare(
7313
+ "UPDATE tasks SET status = 'OPEN', substate = 'todo', updated_at = ? WHERE id = ?"
7314
+ ).run((/* @__PURE__ */ new Date()).toISOString(), t.taskId);
7028
7315
  addNote(
7029
7316
  db,
7030
7317
  t.localId,
@@ -7275,9 +7562,20 @@ function filterAppliable(table, entries) {
7275
7562
  }
7276
7563
  function applyTaskPut(db, op) {
7277
7564
  const { workstream, localId } = parseTaskKey(op.key);
7278
- const entries = filterAppliable("tasks", decodePayload(op.payload)).map(
7279
- ([field, value]) => field === "status" && typeof value === "string" ? [field, normalizeTaskStatus(value)] : [field, value]
7280
- );
7565
+ const entries = [];
7566
+ for (const entry of filterAppliable("tasks", decodePayload(op.payload))) {
7567
+ const [field, value] = entry;
7568
+ if (field === "substate") continue;
7569
+ if (field === "status") {
7570
+ if (typeof value !== "string") continue;
7571
+ const legacy = mapLegacyStatus(value);
7572
+ if (legacy) entries.push(["status", legacy.status]);
7573
+ else if (isTaskStatus(value)) entries.push(entry);
7574
+ continue;
7575
+ }
7576
+ entries.push(entry);
7577
+ }
7578
+ const pending2 = { hlc: op.hlc, substate: carriedSubstate(JSON.parse(op.payload)) };
7281
7579
  const existing = taskRowId(db, op.key);
7282
7580
  if (existing === null) {
7283
7581
  const tomb = tombstoneHlc(db, op);
@@ -7287,18 +7585,60 @@ function applyTaskPut(db, op) {
7287
7585
  const wsId = ensureWorkstreamRow(db, workstream);
7288
7586
  const now = (/* @__PURE__ */ new Date()).toISOString();
7289
7587
  db.prepare(
7290
- `INSERT INTO tasks (workstream_id, local_id, title, status, impact, effort_days,
7588
+ `INSERT INTO tasks (workstream_id, local_id, title, status, substate, impact, effort_days,
7291
7589
  owner_id, created_at, updated_at)
7292
- VALUES (?, ?, ?, 'OPEN', 50, 1, NULL, ?, ?)`
7590
+ VALUES (?, ?, ?, 'OPEN', 'todo', 50, 1, NULL, ?, ?)`
7293
7591
  ).run(wsId, localId, localId, now, now);
7294
7592
  const rowId = taskRowId(db, op.key);
7295
7593
  if (rowId === null) throw new Error(`failed to create task row for ${op.key}`);
7296
7594
  const applied2 = applyFieldLww(db, op, "tasks", rowId, entries);
7595
+ if (repairTaskPair(db, rowId, pending2)) applied2.push("substate");
7297
7596
  return { changed: true, appliedFields: applied2 };
7298
7597
  }
7299
7598
  const applied = applyFieldLww(db, op, "tasks", existing, entries);
7599
+ if (repairTaskPair(db, existing, pending2)) applied.push("substate");
7300
7600
  return applied.length > 0 ? { changed: true, appliedFields: applied } : { changed: false, appliedFields: [], skipped: "older-than-current" };
7301
7601
  }
7602
+ function carriedSubstate(payload) {
7603
+ if (payload === null || typeof payload !== "object") return null;
7604
+ const { status, substate } = payload;
7605
+ if (typeof status === "string") {
7606
+ const legacy = mapLegacyStatus(status);
7607
+ if (legacy) return legacy.substate;
7608
+ if (typeof substate !== "string" && isTaskStatus(status)) return DEFAULT_SUBSTATE[status];
7609
+ }
7610
+ return typeof substate === "string" ? substate : null;
7611
+ }
7612
+ function repairTaskPair(db, rowId, pending2) {
7613
+ const row2 = db.prepare(
7614
+ `SELECT t.status, t.substate, w.name || '/' || t.local_id AS key
7615
+ FROM tasks t JOIN workstreams w ON w.id = t.workstream_id
7616
+ WHERE t.id = ?`
7617
+ ).get(rowId);
7618
+ if (!row2) return false;
7619
+ let winner = null;
7620
+ const writers = db.prepare(
7621
+ `SELECT hlc, payload FROM ops
7622
+ WHERE entity = 'task' AND key = ? AND op = 'put' AND hlc <> ?
7623
+ AND ${LEGACY_LOG_ONLY_SQL_EXCLUSION}
7624
+ AND (json_type(payload, '$.substate') IS NOT NULL
7625
+ OR json_type(payload, '$.status') IS NOT NULL)
7626
+ ORDER BY hlc DESC`
7627
+ ).iterate(row2.key, pending2?.hlc ?? "");
7628
+ for (const w of writers) {
7629
+ const substate = carriedSubstate(JSON.parse(w.payload));
7630
+ if (substate === null) continue;
7631
+ winner = { hlc: w.hlc, substate };
7632
+ break;
7633
+ }
7634
+ if (pending2 && pending2.substate !== null && wins(pending2.hlc, winner?.hlc ?? null)) {
7635
+ winner = { hlc: pending2.hlc, substate: pending2.substate };
7636
+ }
7637
+ const target = resolvePair(row2.status, winner?.substate ?? row2.substate);
7638
+ if (target === null || target.substate === row2.substate) return false;
7639
+ db.prepare("UPDATE tasks SET substate = ? WHERE id = ?").run(target.substate, rowId);
7640
+ return true;
7641
+ }
7302
7642
  function applyWorkstreamPut(db, op) {
7303
7643
  const name = op.key;
7304
7644
  if (name.length === 0) throw new OpKeyMalformedError("workstream", name);
@@ -7440,7 +7780,7 @@ function applyOp(db, op) {
7440
7780
  case "workstream":
7441
7781
  return applyWorkstreamPut(db, op);
7442
7782
  case "task":
7443
- return applyTaskPut(db, op);
7783
+ return db.transaction(() => applyTaskPut(db, op))();
7444
7784
  case "note":
7445
7785
  return applyNotePut(db, op);
7446
7786
  case "edge":
@@ -7574,7 +7914,7 @@ var init_apply = __esm({
7574
7914
  };
7575
7915
  NEVER_APPLY = /* @__PURE__ */ new Set(["owner_id", "local_id", "name", "id", "workstream_id", "task_id"]);
7576
7916
  APPLIABLE_COLUMNS = {
7577
- tasks: ["title", "status", "impact", "effort_days", "created_at", "updated_at"],
7917
+ tasks: ["title", "status", "substate", "impact", "effort_days", "created_at", "updated_at"],
7578
7918
  workstreams: ["created_at"]
7579
7919
  };
7580
7920
  }
@@ -7944,7 +8284,7 @@ var init_drift = __esm({
7944
8284
  DRIFT_REPORT_CAP = 20;
7945
8285
  COMPARED = {
7946
8286
  workstreams: ["created_at"],
7947
- tasks: ["title", "status", "impact", "effort_days", "created_at", "updated_at"],
8287
+ tasks: ["title", "status", "substate", "impact", "effort_days", "created_at", "updated_at"],
7948
8288
  task_notes: ["author", "content", "created_at"],
7949
8289
  task_edges: ["created_at"]
7950
8290
  };
@@ -7952,7 +8292,7 @@ var init_drift = __esm({
7952
8292
  workstreams: `SELECT w.name AS key, w.created_at
7953
8293
  FROM workstreams w`,
7954
8294
  tasks: `SELECT w.name || '/' || t.local_id AS key,
7955
- t.title, t.status, t.impact, t.effort_days, t.created_at, t.updated_at
8295
+ t.title, t.status, t.substate, t.impact, t.effort_days, t.created_at, t.updated_at
7956
8296
  FROM tasks t JOIN workstreams w ON w.id = t.workstream_id`,
7957
8297
  // Notes are a GROW-ONLY SET whose local surrogate id is not portable,
7958
8298
  // so identity for diffing is (task, author, content) — exactly the
@@ -8554,7 +8894,8 @@ function getParallelTracks(db, workstream) {
8554
8894
  FROM reach r
8555
8895
  JOIN task_edges e ON e.to_task_id = r.node_id
8556
8896
  )
8557
- SELECT goal.local_id AS goal_id, node.local_id AS task_id
8897
+ SELECT goal.local_id AS goal_id, node.local_id AS task_id,
8898
+ node.status AS status, node.substate AS substate
8558
8899
  FROM reach r
8559
8900
  JOIN tasks goal ON goal.id = r.goal_id
8560
8901
  JOIN tasks node ON node.id = r.node_id`
@@ -8562,7 +8903,9 @@ function getParallelTracks(db, workstream) {
8562
8903
  const subgraphs = new Map(goals.map((goal) => [goal.name, /* @__PURE__ */ new Set()]));
8563
8904
  const uf = new UnionFind(goals.map((goal) => goal.name));
8564
8905
  const firstGoalByTask = /* @__PURE__ */ new Map();
8906
+ const openSubstate = /* @__PURE__ */ new Map();
8565
8907
  for (const row2 of reach) {
8908
+ if (row2.status !== "CLOSED") openSubstate.set(row2.task_id, row2.substate);
8566
8909
  subgraphs.get(row2.goal_id)?.add(row2.task_id);
8567
8910
  const firstGoal = firstGoalByTask.get(row2.task_id);
8568
8911
  if (firstGoal === void 0) firstGoalByTask.set(row2.task_id, row2.goal_id);
@@ -8589,8 +8932,17 @@ function getParallelTracks(db, workstream) {
8589
8932
  for (const [root, taskIds] of componentTaskIds) {
8590
8933
  const trackRoots = componentRoots.get(root) ?? [];
8591
8934
  let readyCount = 0;
8592
- for (const id of taskIds) if (readyIds.has(id)) readyCount++;
8593
- tracks.push({ roots: trackRoots, taskIds, readyCount });
8935
+ let openCount = 0;
8936
+ let parkedCount = 0;
8937
+ for (const id of taskIds) {
8938
+ if (readyIds.has(id)) readyCount++;
8939
+ const sub = openSubstate.get(id);
8940
+ if (sub === void 0) continue;
8941
+ openCount++;
8942
+ if (sub === "parked") parkedCount++;
8943
+ }
8944
+ const parked = openCount > 0 && parkedCount === openCount;
8945
+ tracks.push({ roots: trackRoots, taskIds, readyCount, parked });
8594
8946
  }
8595
8947
  tracks.sort((a, b) => {
8596
8948
  const an = a.roots[0]?.name ?? "";
@@ -8705,6 +9057,7 @@ async function loadWorkstreamSnapshotFast(db, workstream, opts = {}) {
8705
9057
  inProgress: listInProgress(db, workstream),
8706
9058
  blocked: listBlocked(db, workstream),
8707
9059
  recentClosed: listRecentClosed(db, workstream),
9060
+ parkedCount: listTasks(db, workstream, { status: "OPEN", substate: "parked" }).length,
8708
9061
  allTasks: opts.withAllTasks === true ? listTasks(db, workstream) : [],
8709
9062
  workspaces: listWorkspaces2(db, workstream),
8710
9063
  workspaceOrphans: listWorkspaceOrphans(db, workstream),
@@ -8777,6 +9130,7 @@ function minimalSnapshot(workstream) {
8777
9130
  inProgress: [],
8778
9131
  blocked: [],
8779
9132
  recentClosed: [],
9133
+ parkedCount: 0,
8780
9134
  allTasks: [],
8781
9135
  workspaces: [],
8782
9136
  workspaceOrphans: [],
@@ -8855,7 +9209,7 @@ var init_state = __esm({
8855
9209
  import pc2 from "picocolors";
8856
9210
  function loadFullDag(db, workstream, opts = {}) {
8857
9211
  const tasks = listTasks(db, workstream).filter(
8858
- (t) => opts.statuses === void 0 || opts.statuses.has(t.status)
9212
+ (t) => (opts.statuses === void 0 || opts.statuses.has(t.status)) && (opts.include === void 0 || opts.include(t))
8859
9213
  );
8860
9214
  const byName = new Map(tasks.map((t) => [t.name, t]));
8861
9215
  const incoming = /* @__PURE__ */ new Set();
@@ -9801,7 +10155,16 @@ function renderKnown(row2, intent) {
9801
10155
  case "task.reject":
9802
10156
  case "task.defer": {
9803
10157
  const status = str(bag, "status");
9804
- return { verb: VERBS[intent], subject, detail: status === void 0 ? "" : `\u2192 ${status}` };
10158
+ return {
10159
+ verb: VERBS[intent],
10160
+ subject,
10161
+ detail: status === void 0 ? "" : `\u2192 ${pairText(bag, status)}`
10162
+ };
10163
+ }
10164
+ case "task.park":
10165
+ case "task.unpark": {
10166
+ const detail = str(bag, "substate") === void 0 ? "" : `\u2192 ${pairText(bag, "OPEN")}`;
10167
+ return { verb: VERBS[intent], subject, detail };
9805
10168
  }
9806
10169
  case "task.claim": {
9807
10170
  const by = row2.source === "" ? "" : `by ${row2.source}`;
@@ -9860,6 +10223,10 @@ function renderKnown(row2, intent) {
9860
10223
  }
9861
10224
  }
9862
10225
  }
10226
+ function pairText(bag, status) {
10227
+ const pair = resolvePair(status, str(bag, "substate"));
10228
+ return pair === null ? status : formatPair(pair);
10229
+ }
9863
10230
  function oneLine(text2, max = 60) {
9864
10231
  const flat = text2.replace(/\s+/g, " ").trim();
9865
10232
  return flat.length <= max ? flat : `${flat.slice(0, max - 1)}\u2026`;
@@ -9886,6 +10253,7 @@ var NOISE_FIELDS, VERBS, KNOWN_INTENTS, KNOWN_VERBS;
9886
10253
  var init_log_render = __esm({
9887
10254
  "src/log-render.ts"() {
9888
10255
  "use strict";
10256
+ init_status();
9889
10257
  NOISE_FIELDS = /* @__PURE__ */ new Set(["updated_at", "created_at"]);
9890
10258
  VERBS = {
9891
10259
  "task.add": "task add",
@@ -9894,6 +10262,8 @@ var init_log_render = __esm({
9894
10262
  "task.delete": "task delete",
9895
10263
  "task.close": "task close",
9896
10264
  "task.open": "task open",
10265
+ "task.park": "task park",
10266
+ "task.unpark": "task unpark",
9897
10267
  "task.reject": "task reject",
9898
10268
  "task.defer": "task defer",
9899
10269
  "task.claim": "task claim",
@@ -9925,6 +10295,17 @@ var init_log_render = __esm({
9925
10295
  function statusIcon(state) {
9926
10296
  return STATUS_COLORS[state](agentStateGlyph(state));
9927
10297
  }
10298
+ function colorPair(pair) {
10299
+ const text2 = formatPair(pair);
10300
+ switch (pair.status) {
10301
+ case "OPEN":
10302
+ return pair.substate === "parked" ? pc.dim(text2) : pc.cyan(text2);
10303
+ case "IN_PROGRESS":
10304
+ return pc.yellow(text2);
10305
+ case "CLOSED":
10306
+ return pair.substate === "done" ? pc.green(text2) : pc.red(text2);
10307
+ }
10308
+ }
9928
10309
  function colorStatus(status) {
9929
10310
  switch (status) {
9930
10311
  case "OPEN":
@@ -9935,14 +10316,14 @@ function colorStatus(status) {
9935
10316
  return pc.green(status);
9936
10317
  }
9937
10318
  }
9938
- function inkColorForStatus(status) {
9939
- switch (status) {
10319
+ function inkColorForPair(pair) {
10320
+ switch (pair.status) {
9940
10321
  case "OPEN":
9941
- return "cyan";
10322
+ return pair.substate === "parked" ? "gray" : "cyan";
9942
10323
  case "IN_PROGRESS":
9943
10324
  return "yellow";
9944
10325
  case "CLOSED":
9945
- return "green";
10326
+ return pair.substate === "done" ? "green" : "red";
9946
10327
  }
9947
10328
  }
9948
10329
  function terminalWidth() {
@@ -10056,8 +10437,9 @@ function formatTracks(tracks) {
10056
10437
  tracks.forEach((track, i) => {
10057
10438
  const rootNames = track.roots.map((r) => r.name).join(", ");
10058
10439
  const verb = track.roots.length > 1 ? "merged" : "track";
10440
+ const parked = track.parked ? ` ${pc.dim("(parked)")}` : "";
10059
10441
  lines.push(
10060
- ` Track ${i + 1}: ${pc.bold(rootNames)} ${pc.dim(`(${track.taskIds.size} tasks, ${track.readyCount} ready, ${verb})`)}`
10442
+ ` Track ${i + 1}: ${pc.bold(rootNames)} ${pc.dim(`(${track.taskIds.size} tasks, ${track.readyCount} ready, ${verb})`)}${parked}`
10061
10443
  );
10062
10444
  });
10063
10445
  return lines.join("\n");
@@ -10191,7 +10573,7 @@ function formatTaskListTable(tasks, opts = {}) {
10191
10573
  const baseRow = opts.withWorkstream ? [
10192
10574
  t.name,
10193
10575
  t.workstreamName,
10194
- colorStatus(t.status),
10576
+ colorPair(t),
10195
10577
  title,
10196
10578
  String(t.impact),
10197
10579
  String(t.effortDays),
@@ -10199,7 +10581,7 @@ function formatTaskListTable(tasks, opts = {}) {
10199
10581
  t.ownerName ?? pc.dim("\u2014")
10200
10582
  ] : [
10201
10583
  t.name,
10202
- colorStatus(t.status),
10584
+ colorPair(t),
10203
10585
  title,
10204
10586
  String(t.impact),
10205
10587
  String(t.effortDays),
@@ -10219,6 +10601,7 @@ var init_format = __esm({
10219
10601
  init_glyphs();
10220
10602
  init_log_render();
10221
10603
  init_output();
10604
+ init_status();
10222
10605
  STATUS_COLORS = {
10223
10606
  busy: pc.cyan,
10224
10607
  needs_input: pc.dim,
@@ -11117,7 +11500,8 @@ function BlockedCard({
11117
11500
  (t) => stillGating(getTaskEdgesWithStatus(db, t.name, workstream).blockers)
11118
11501
  );
11119
11502
  const topBlocker = pickTopBlocker(blockerLists);
11120
- const subtitle = formatSubtitle(blocked.length, topBlocker);
11503
+ const topEdge = blockerLists.flat().find((b) => b.name === topBlocker);
11504
+ const subtitle = formatSubtitle(blocked.length, topEdge ? blockerLabel(topEdge) : topBlocker);
11121
11505
  const meta = shown.map((t) => {
11122
11506
  const bucket = roiBucket(t.impact, t.effortDays);
11123
11507
  const roiText = formatRoi(t.impact, t.effortDays);
@@ -11126,7 +11510,7 @@ function BlockedCard({
11126
11510
  const rows = shown.map((t, i) => [
11127
11511
  GLYPH2,
11128
11512
  t.name,
11129
- t.status,
11513
+ formatPair(t),
11130
11514
  String(blockerLists[i]?.length ?? 0),
11131
11515
  meta[i]?.roiText ?? "",
11132
11516
  t.title
@@ -11151,7 +11535,7 @@ function BlockedCard({
11151
11535
  // glyph
11152
11536
  { bold: true },
11153
11537
  // id
11154
- { color: inkColorForStatus(t.status) },
11538
+ { color: inkColorForPair(t) },
11155
11539
  // status
11156
11540
  { color: "yellow" },
11157
11541
  // blocks
@@ -11168,6 +11552,9 @@ function BlockedCard({
11168
11552
  function stillGating(blockers) {
11169
11553
  return blockers.filter((b) => b.status !== "CLOSED");
11170
11554
  }
11555
+ function blockerLabel(b) {
11556
+ return b.status === "OPEN" && b.substate === "parked" ? `${b.name} (parked)` : b.name;
11557
+ }
11171
11558
  function pickTopBlocker(blockerLists) {
11172
11559
  const counts = /* @__PURE__ */ new Map();
11173
11560
  for (const list of blockerLists) {
@@ -11198,6 +11585,7 @@ var init_blocked = __esm({
11198
11585
  "use strict";
11199
11586
  init_glyphs();
11200
11587
  init_state();
11588
+ init_status();
11201
11589
  init_tasks();
11202
11590
  init_format();
11203
11591
  init_columns();
@@ -11473,7 +11861,7 @@ function InProgressCard({ snapshot: snapshot2, rowBudget, cols }) {
11473
11861
  const rows = shown.map((t, i) => [
11474
11862
  GLYPH3,
11475
11863
  t.name,
11476
- t.status,
11864
+ formatPair(t),
11477
11865
  formatAgentRefDisplayName(t.ownerName, agentLookup),
11478
11866
  formatSinceClaim(ages[i] ?? null),
11479
11867
  t.title
@@ -11499,7 +11887,7 @@ function InProgressCard({ snapshot: snapshot2, rowBudget, cols }) {
11499
11887
  // glyph
11500
11888
  { bold: true },
11501
11889
  // id
11502
- { color: inkColorForStatus(t.status) },
11890
+ { color: inkColorForPair(t) },
11503
11891
  // status
11504
11892
  { dimColor: true },
11505
11893
  // owner
@@ -11525,6 +11913,7 @@ var cardConfig5, GLYPH3, STALE_CLAIM_THRESHOLD_MS, COLUMN_SPECS5;
11525
11913
  var init_inprogress = __esm({
11526
11914
  "src/cli/tui/cards/inprogress.tsx"() {
11527
11915
  "use strict";
11916
+ init_status();
11528
11917
  init_format();
11529
11918
  init_agent_display();
11530
11919
  init_columns();
@@ -11661,7 +12050,11 @@ function ReadyCard({ snapshot: snapshot2, rowBudget, cols }) {
11661
12050
  config: cardConfig7,
11662
12051
  rowBudget,
11663
12052
  cols,
11664
- children: /* @__PURE__ */ jsx12(Text8, { dimColor: true, children: "(no ready tasks) every blocker is OPEN/IN_PROGRESS or every task is closed" })
12053
+ children: (
12054
+ // truncate-end: a wrapped second line would spill over the
12055
+ // card's top border and erase its title.
12056
+ /* @__PURE__ */ jsx12(Text8, { dimColor: true, wrap: "truncate-end", children: snapshot2.parkedCount > 0 ? `(no ready tasks) ${snapshot2.parkedCount} parked \xB7 the rest are blocked or closed` : "(no ready tasks) every blocker is OPEN/IN_PROGRESS or every task is closed" })
12057
+ )
11665
12058
  });
11666
12059
  }
11667
12060
  const shown = ready.slice(0, rowBudget ?? cardConfig7.maxRows);
@@ -11675,7 +12068,7 @@ function ReadyCard({ snapshot: snapshot2, rowBudget, cols }) {
11675
12068
  const agentLookup = agentByName(snapshot2);
11676
12069
  const rows = shown.map((t, i) => [
11677
12070
  t.name,
11678
- t.status,
12071
+ formatPair(t),
11679
12072
  `ROI ${meta[i]?.roiText ?? ""}`,
11680
12073
  t.title,
11681
12074
  formatAgentRefDisplayName(t.ownerName, agentLookup)
@@ -11698,7 +12091,7 @@ function ReadyCard({ snapshot: snapshot2, rowBudget, cols }) {
11698
12091
  const colors = [
11699
12092
  { bold: true },
11700
12093
  // name
11701
- { color: inkColorForStatus(t.status) },
12094
+ { color: inkColorForPair(t) },
11702
12095
  // status
11703
12096
  { color: colorForBucket(m.bucket) },
11704
12097
  // roi
@@ -11717,6 +12110,7 @@ var init_ready = __esm({
11717
12110
  "src/cli/tui/cards/ready.tsx"() {
11718
12111
  "use strict";
11719
12112
  init_state();
12113
+ init_status();
11720
12114
  init_format();
11721
12115
  init_agent_display();
11722
12116
  init_columns();
@@ -11772,7 +12166,13 @@ function RecentCard({ snapshot: snapshot2, rowBudget, cols }) {
11772
12166
  const shown = recentClosed.slice(0, rowBudget ?? cardConfig8.maxRows);
11773
12167
  const more = recentClosed.length - shown.length;
11774
12168
  const bottomLabel = more > 0 ? `+${more} more \xB7 Shift+8` : void 0;
11775
- const rows = shown.map((t, i) => [GLYPH4, t.name, t.status, formatWhen(ages[i] ?? null), t.title]);
12169
+ const rows = shown.map((t, i) => [
12170
+ GLYPH4,
12171
+ t.name,
12172
+ formatPair(t),
12173
+ formatWhen(ages[i] ?? null),
12174
+ t.title
12175
+ ]);
11776
12176
  const widths = layoutColumns(rows, COLUMN_SPECS8, contentWidth);
11777
12177
  return /* @__PURE__ */ jsx13(
11778
12178
  TitledBox,
@@ -11792,7 +12192,7 @@ function RecentCard({ snapshot: snapshot2, rowBudget, cols }) {
11792
12192
  // glyph
11793
12193
  { bold: true },
11794
12194
  // id
11795
- { color: inkColorForStatus(t.status) },
12195
+ { color: inkColorForPair(t) },
11796
12196
  // status
11797
12197
  { dimColor: true },
11798
12198
  // when
@@ -11814,6 +12214,7 @@ var init_recent = __esm({
11814
12214
  "src/cli/tui/cards/recent.tsx"() {
11815
12215
  "use strict";
11816
12216
  init_glyphs();
12217
+ init_status();
11817
12218
  init_format();
11818
12219
  init_columns();
11819
12220
  init_format_helpers();
@@ -11876,7 +12277,8 @@ function TracksCard({ snapshot: snapshot2, rowBudget, cols }) {
11876
12277
  const goalNames = t.roots.slice(0, 2).map((r) => r.name).join(", ");
11877
12278
  const diamond = t.roots.length > 1 ? GLYPH.merge : " ";
11878
12279
  const taskNoun = t.taskIds.size === 1 ? "task" : "tasks";
11879
- const counts = `(${t.taskIds.size} ${taskNoun} \xB7 ${t.readyCount} ready)`;
12280
+ const parked = t.parked ? " (parked)" : "";
12281
+ const counts = `(${t.taskIds.size} ${taskNoun} \xB7 ${t.readyCount} ready)${parked}`;
11880
12282
  return [`Track ${i + 1}`, diamond, goalNames, counts];
11881
12283
  });
11882
12284
  const widths = layoutColumns(rows, COLUMN_SPECS9, contentWidth);
@@ -12139,7 +12541,7 @@ var init_keymap_spec = __esm({
12139
12541
  DAG_HINTS = [
12140
12542
  label("drill", "magenta"),
12141
12543
  hint("j/k", "scroll", ["j", "k"]),
12142
- hint("o/i/c", "filter", ["o", "i", "c"]),
12544
+ hint("o/i/c/p/w", "filter", ["o", "i", "c", "p", "w"]),
12143
12545
  hint("y", "yank", ["y"]),
12144
12546
  hint("?", "help", ["?"]),
12145
12547
  hint("Esc", "back", ["Esc"])
@@ -12147,7 +12549,7 @@ var init_keymap_spec = __esm({
12147
12549
  ALL_TASKS_HINTS = [
12148
12550
  label("drill", "magenta"),
12149
12551
  hint("j/k", "nav", ["j", "k"]),
12150
- hint("o/i/c", "filter", ["o", "i", "c"]),
12552
+ hint("o/i/c/p/w", "filter", ["o", "i", "c", "p", "w"]),
12151
12553
  hint("s", "sort", ["s"]),
12152
12554
  hint("/", "search", ["/"]),
12153
12555
  hint("Enter", "drill", ["Enter"]),
@@ -12223,7 +12625,11 @@ var init_keymap_spec = __esm({
12223
12625
  {
12224
12626
  title: "keys \xB7 DAG / all-tasks",
12225
12627
  rows: [
12226
- row("o/i/c", "toggle OPEN / IN_PROGRESS / CLOSED status filters", ["o", "i", "c"]),
12628
+ row(
12629
+ "o/i/c/p/w",
12630
+ "toggle OPEN / IN_PROGRESS / CLOSED status filters; p parked; w won't do (closed, not done)",
12631
+ ["o", "i", "c", "p", "w"]
12632
+ ),
12227
12633
  row("b", "all-tasks blocked filter cycle (all \u2192 only \u2192 hide)", ["b"]),
12228
12634
  row("s", "all-tasks sort cycle (roi \u2192 recency \u2192 age \u2192 id)", ["s"]),
12229
12635
  row("/", "all-tasks row search", ["/"])
@@ -13481,23 +13887,48 @@ function statusForToggleKey(input, key) {
13481
13887
  if (key.ctrl === true || key.meta === true) return void 0;
13482
13888
  return STATUS_BY_KEY[input.toLowerCase()];
13483
13889
  }
13890
+ function substateToggleForKey(input, key) {
13891
+ if (key.ctrl === true || key.meta === true) return void 0;
13892
+ return SUBSTATE_BY_KEY[input.toLowerCase()];
13893
+ }
13894
+ function passesFilter(t, f) {
13895
+ if (!f.statuses.has(t.status)) return false;
13896
+ if (!f.showParked && t.status === "OPEN" && t.substate === "parked") return false;
13897
+ if (!f.showNotDone && t.status === "CLOSED" && t.substate !== "done") return false;
13898
+ return true;
13899
+ }
13484
13900
  function useStatusFilter() {
13485
13901
  const [statuses, setStatuses] = useState6(() => new Set(TASK_STATUSES));
13902
+ const [showParked, setShowParked] = useState6(true);
13903
+ const [showNotDone, setShowNotDone] = useState6(true);
13486
13904
  const toggle = useCallback4((status) => {
13487
13905
  setStatuses((prev) => toggleStatusSet(prev, status));
13488
13906
  }, []);
13489
13907
  const onKey = useCallback4(
13490
13908
  (input, key) => {
13491
13909
  const status = statusForToggleKey(input, key);
13492
- if (status === void 0) return false;
13493
- toggle(status);
13494
- return true;
13910
+ if (status !== void 0) {
13911
+ toggle(status);
13912
+ return true;
13913
+ }
13914
+ const sub = substateToggleForKey(input, key);
13915
+ if (sub === "parked") setShowParked((v) => !v);
13916
+ else if (sub === "notDone") setShowNotDone((v) => !v);
13917
+ return sub !== void 0;
13495
13918
  },
13496
13919
  [toggle]
13497
13920
  );
13498
- return { statuses, toggle, onKey };
13921
+ return { statuses, showParked, showNotDone, toggle, onKey };
13499
13922
  }
13500
- function StatusFilterStrip({ statuses }) {
13923
+ function StatusFilterStrip({
13924
+ statuses,
13925
+ showParked = true,
13926
+ showNotDone = true
13927
+ }) {
13928
+ const substateOn = {
13929
+ parked: showParked,
13930
+ notDone: showNotDone
13931
+ };
13501
13932
  return /* @__PURE__ */ jsxs11(Box8, { children: [
13502
13933
  /* @__PURE__ */ jsx22(Text15, { dimColor: true, children: "filters: " }),
13503
13934
  TASK_STATUSES.map((status, i) => {
@@ -13512,10 +13943,22 @@ function StatusFilterStrip({ statuses }) {
13512
13943
  /* @__PURE__ */ jsx22(Text15, { children: " " }),
13513
13944
  /* @__PURE__ */ jsx22(Text15, { color: enabled ? "green" : "gray", children: enabled ? GLYPH.on : GLYPH.off })
13514
13945
  ] }, status);
13946
+ }),
13947
+ SUBSTATE_LABELS.map((label2, i) => {
13948
+ const enabled = substateOn[label2.id];
13949
+ return /* @__PURE__ */ jsxs11(Text15, { children: [
13950
+ i === 0 ? " " : " ",
13951
+ /* @__PURE__ */ jsx22(Text15, { children: "[" }),
13952
+ /* @__PURE__ */ jsx22(Text15, { color: label2.color, children: label2.key }),
13953
+ /* @__PURE__ */ jsx22(Text15, { children: "]" }),
13954
+ /* @__PURE__ */ jsx22(Text15, { children: label2.rest }),
13955
+ /* @__PURE__ */ jsx22(Text15, { children: " " }),
13956
+ /* @__PURE__ */ jsx22(Text15, { color: enabled ? "green" : "gray", children: enabled ? GLYPH.on : GLYPH.off })
13957
+ ] }, label2.id);
13515
13958
  })
13516
13959
  ] });
13517
13960
  }
13518
- var STATUS_BY_KEY, STATUS_LABELS;
13961
+ var STATUS_BY_KEY, STATUS_LABELS, SUBSTATE_BY_KEY, SUBSTATE_LABELS;
13519
13962
  var init_use_status_filter = __esm({
13520
13963
  "src/cli/tui/use-status-filter.tsx"() {
13521
13964
  "use strict";
@@ -13532,6 +13975,12 @@ var init_use_status_filter = __esm({
13532
13975
  IN_PROGRESS: { key: "I", rest: "n_progress" },
13533
13976
  CLOSED: { key: "C", rest: "losed" }
13534
13977
  };
13978
+ SUBSTATE_BY_KEY = { p: "parked", w: "notDone" };
13979
+ SUBSTATE_LABELS = [
13980
+ { id: "parked", key: "P", rest: "arked", color: "gray" },
13981
+ // Covers every non-done close (rejected, wontfix, duplicate, superseded).
13982
+ { id: "notDone", key: "W", rest: "on't do", color: "red" }
13983
+ ];
13535
13984
  }
13536
13985
  });
13537
13986
 
@@ -13558,6 +14007,8 @@ function AllTasksPopup({
13558
14007
  const [sortKey, setSortKey] = useState7("roi");
13559
14008
  const flt = usePopupFilter({ onEditingChange: onFilterEditingChange });
13560
14009
  const statusFilter = useStatusFilter();
14010
+ const { statuses, showParked, showNotDone } = statusFilter;
14011
+ const filterStrip = /* @__PURE__ */ jsx23(StatusFilterStrip, { statuses, showParked, showNotDone });
13561
14012
  const sourceTasks = useMemo5(
13562
14013
  () => allTasksFromSnapshotOrDb(snapshot2, db, workstream),
13563
14014
  [snapshot2, db, workstream]
@@ -13568,15 +14019,26 @@ function AllTasksPopup({
13568
14019
  );
13569
14020
  const [blockedFilter, setBlockedFilter] = useState7("all");
13570
14021
  const visibleTasks = useMemo5(() => {
13571
- const filteredByStatus = sourceTasks.filter((t) => statusFilter.statuses.has(t.status));
14022
+ const filteredByStatus = sourceTasks.filter(
14023
+ (t) => passesFilter(t, { statuses, showParked, showNotDone })
14024
+ );
13572
14025
  const filteredByBlocked = applyBlockedFilter(filteredByStatus, blockedNames, blockedFilter);
13573
14026
  const filteredByText = applyFilter(
13574
14027
  filteredByBlocked,
13575
14028
  flt.query,
13576
- (t) => `${t.name} ${t.title} ${t.status} ${t.ownerName ?? ""}`
14029
+ (t) => `${t.name} ${t.title} ${formatPair(t)} ${t.ownerName ?? ""}`
13577
14030
  );
13578
14031
  return sortTasks(filteredByText, sortKey);
13579
- }, [sourceTasks, statusFilter.statuses, blockedFilter, blockedNames, flt.query, sortKey]);
14032
+ }, [
14033
+ sourceTasks,
14034
+ statuses,
14035
+ showParked,
14036
+ showNotDone,
14037
+ blockedFilter,
14038
+ blockedNames,
14039
+ flt.query,
14040
+ sortKey
14041
+ ]);
13580
14042
  const safeCursor = visibleTasks.length === 0 ? 0 : Math.min(cursor, visibleTasks.length - 1);
13581
14043
  const focused = visibleTasks[safeCursor];
13582
14044
  const [drilledTask, setDrilledTask] = useState7(null);
@@ -13646,7 +14108,7 @@ function AllTasksPopup({
13646
14108
  }
13647
14109
  if (sourceTasks.length === 0) {
13648
14110
  return /* @__PURE__ */ jsx23(PopupShell, { title: "All tasks \xB7 popup", children: /* @__PURE__ */ jsxs12(Box9, { flexDirection: "column", flexGrow: 1, children: [
13649
- /* @__PURE__ */ jsx23(StatusFilterStrip, { statuses: statusFilter.statuses }),
14111
+ filterStrip,
13650
14112
  /* @__PURE__ */ jsx23(BlockedFilterStrip, { mode: blockedFilter }),
13651
14113
  /* @__PURE__ */ jsx23(SortStrip, { sortKey, visible: 0, total: 0 }),
13652
14114
  /* @__PURE__ */ jsx23(Text16, { dimColor: true, children: "(no tasks)" })
@@ -13655,7 +14117,7 @@ function AllTasksPopup({
13655
14117
  if (visibleTasks.length === 0) {
13656
14118
  return /* @__PURE__ */ jsxs12(PopupShell, { title: "All tasks \xB7 popup", children: [
13657
14119
  /* @__PURE__ */ jsxs12(Box9, { flexDirection: "column", flexGrow: 1, children: [
13658
- /* @__PURE__ */ jsx23(StatusFilterStrip, { statuses: statusFilter.statuses }),
14120
+ filterStrip,
13659
14121
  /* @__PURE__ */ jsx23(BlockedFilterStrip, { mode: blockedFilter }),
13660
14122
  /* @__PURE__ */ jsx23(SortStrip, { sortKey, visible: 0, total: sourceTasks.length }),
13661
14123
  /* @__PURE__ */ jsx23(Text16, { dimColor: true, children: flt.query === "" ? "(no tasks match status filter)" : `(no matches for "${flt.query}")` })
@@ -13682,7 +14144,7 @@ function AllTasksPopup({
13682
14144
  const agentLookup = agentByName(snapshot2);
13683
14145
  const rows = windowed.map((t) => [
13684
14146
  t.name,
13685
- blockedNames.has(t.name) ? `${t.status} ${GLYPH.blocked}` : t.status,
14147
+ statusCell(t, blockedNames.has(t.name)),
13686
14148
  formatAgentRefDisplayName(t.ownerName, agentLookup),
13687
14149
  formatRoi(t.impact, t.effortDays),
13688
14150
  t.title
@@ -13695,7 +14157,7 @@ function AllTasksPopup({
13695
14157
  hint: focused ? allTasksYankCommand(focused.name, workstream) : void 0,
13696
14158
  children: [
13697
14159
  /* @__PURE__ */ jsxs12(Box9, { flexDirection: "column", flexGrow: 1, children: [
13698
- /* @__PURE__ */ jsx23(StatusFilterStrip, { statuses: statusFilter.statuses }),
14160
+ filterStrip,
13699
14161
  /* @__PURE__ */ jsx23(BlockedFilterStrip, { mode: blockedFilter }),
13700
14162
  /* @__PURE__ */ jsx23(SortStrip, { sortKey, visible: visibleTasks.length, total: sourceTasks.length }),
13701
14163
  windowed.map((t, i) => {
@@ -13706,7 +14168,7 @@ function AllTasksPopup({
13706
14168
  const colors = [
13707
14169
  { bold: true },
13708
14170
  // id
13709
- { color: isBlocked ? "yellow" : inkColorForStatus(t.status) },
14171
+ { color: isBlocked ? "yellow" : inkColorForPair(t) },
13710
14172
  // status + blocked glyph
13711
14173
  { dimColor: true },
13712
14174
  // owner
@@ -13732,6 +14194,11 @@ function AllTasksPopup({
13732
14194
  }
13733
14195
  );
13734
14196
  }
14197
+ function statusCell(t, blocked) {
14198
+ const pair = formatPair(t);
14199
+ const parked = t.status === "OPEN" && t.substate === "parked" ? `${GLYPH.parked} ` : "";
14200
+ return `${parked}${pair}${blocked ? ` ${GLYPH.blocked}` : ""}`;
14201
+ }
13735
14202
  function allTasksFromSnapshotOrDb(snapshot2, db, workstream) {
13736
14203
  if (snapshot2?.allTasks && snapshot2.allTasks.length > 0) return snapshot2.allTasks;
13737
14204
  return listTasks(db, workstream);
@@ -13806,6 +14273,7 @@ var init_all_tasks = __esm({
13806
14273
  "use strict";
13807
14274
  init_glyphs();
13808
14275
  init_sort();
14276
+ init_status();
13809
14277
  init_tasks();
13810
14278
  init_format();
13811
14279
  init_agent_display();
@@ -14013,10 +14481,7 @@ function BlockedPopup(props) {
14013
14481
  const m = /* @__PURE__ */ new Map();
14014
14482
  for (const t of sourceTasks) {
14015
14483
  const gating = stillGating(getTaskEdgesWithStatus(db, t.name, workstream).blockers);
14016
- m.set(
14017
- t.name,
14018
- gating.map((b) => b.name)
14019
- );
14484
+ m.set(t.name, gating.map(blockerLabel));
14020
14485
  }
14021
14486
  return m;
14022
14487
  }, [sourceTasks, db, workstream]);
@@ -14041,7 +14506,7 @@ function BlockedPopup(props) {
14041
14506
  cells: [
14042
14507
  GLYPH2,
14043
14508
  t.name,
14044
- t.status,
14509
+ formatPair(t),
14045
14510
  String(blockers.length),
14046
14511
  top,
14047
14512
  formatRoi(t.impact, t.effortDays),
@@ -14052,7 +14517,7 @@ function BlockedPopup(props) {
14052
14517
  // glyph
14053
14518
  { bold: true },
14054
14519
  // name
14055
- { color: inkColorForStatus(t.status) },
14520
+ { color: inkColorForPair(t) },
14056
14521
  // status
14057
14522
  { color: "yellow" },
14058
14523
  // nblock
@@ -14075,6 +14540,7 @@ var init_blocked2 = __esm({
14075
14540
  "src/cli/tui/popups/blocked.tsx"() {
14076
14541
  "use strict";
14077
14542
  init_state();
14543
+ init_status();
14078
14544
  init_tasks();
14079
14545
  init_format();
14080
14546
  init_blocked();
@@ -14473,12 +14939,13 @@ function DagPopup({
14473
14939
  }) {
14474
14940
  const viewport = usePopupViewport();
14475
14941
  const statusFilter = useStatusFilter();
14942
+ const { statuses, showParked, showNotDone } = statusFilter;
14476
14943
  const { cols } = useTerminalSize();
14477
14944
  const contentWidth = contentWidthFromCols(cols);
14478
14945
  const { body, roots } = useMemo7(() => {
14479
14946
  void fastTickNonce;
14480
- return buildDagBody(db, workstream, statusFilter.statuses, contentWidth);
14481
- }, [db, workstream, statusFilter.statuses, contentWidth, fastTickNonce]);
14947
+ return buildDagBody(db, workstream, { statuses, showParked, showNotDone }, contentWidth);
14948
+ }, [db, workstream, statuses, showParked, showNotDone, contentWidth, fastTickNonce]);
14482
14949
  const [focusedRoot, setFocusedRoot] = useState10(() => roots[0] ?? null);
14483
14950
  const lineToRootRef = useRef5([]);
14484
14951
  const rootsRef = useRef5([]);
@@ -14507,12 +14974,19 @@ function DagPopup({
14507
14974
  });
14508
14975
  if (roots.length === 0) {
14509
14976
  return /* @__PURE__ */ jsx27(PopupShell, { title: `DAG \xB7 ${workstream}`, children: /* @__PURE__ */ jsxs15(Box12, { flexDirection: "column", flexGrow: 1, children: [
14510
- /* @__PURE__ */ jsx27(StatusFilterStrip, { statuses: statusFilter.statuses }),
14977
+ /* @__PURE__ */ jsx27(
14978
+ StatusFilterStrip,
14979
+ {
14980
+ statuses,
14981
+ showParked,
14982
+ showNotDone
14983
+ }
14984
+ ),
14511
14985
  /* @__PURE__ */ jsx27(Text19, { dimColor: true, children: "(no tasks)" })
14512
14986
  ] }) });
14513
14987
  }
14514
14988
  return /* @__PURE__ */ jsx27(PopupShell, { title: `DAG \xB7 ${workstream}`, hint: "y yanks `mu task tree <root-id>`", children: /* @__PURE__ */ jsxs15(Box12, { flexDirection: "column", flexGrow: 1, children: [
14515
- /* @__PURE__ */ jsx27(StatusFilterStrip, { statuses: statusFilter.statuses }),
14989
+ /* @__PURE__ */ jsx27(StatusFilterStrip, { statuses, showParked, showNotDone }),
14516
14990
  /* @__PURE__ */ jsx27(
14517
14991
  DrillScrollView,
14518
14992
  {
@@ -14530,9 +15004,10 @@ function DagPopup({
14530
15004
  function dagYankCommand(taskId, workstream) {
14531
15005
  return `mu task tree ${taskId} -w ${workstream}`;
14532
15006
  }
14533
- function buildDagBody(db, workstream, statuses, contentWidth = contentWidthFromCols(80)) {
14534
- const dag = loadFullDag(db, workstream, { statuses });
14535
- const body = renderForest(dag.roots, dag.edges, (task) => colorStatus(task.status), dag.tasks, {
15007
+ function buildDagBody(db, workstream, filter, contentWidth = contentWidthFromCols(80)) {
15008
+ const f = "statuses" in filter ? filter : { statuses: filter, showParked: true, showNotDone: true };
15009
+ const dag = loadFullDag(db, workstream, { include: (t) => passesFilter(t, f) });
15010
+ const body = renderForest(dag.roots, dag.edges, (task) => colorPair(task), dag.tasks, {
14536
15011
  includeTitle: false
14537
15012
  });
14538
15013
  return {
@@ -14768,6 +15243,7 @@ var COLUMN_SPECS16, config;
14768
15243
  var init_inprogress2 = __esm({
14769
15244
  "src/cli/tui/popups/inprogress.tsx"() {
14770
15245
  "use strict";
15246
+ init_status();
14771
15247
  init_format();
14772
15248
  init_agent_display();
14773
15249
  init_inprogress();
@@ -14808,7 +15284,7 @@ var init_inprogress2 = __esm({
14808
15284
  cells: [
14809
15285
  GLYPH3,
14810
15286
  t.name,
14811
- t.status,
15287
+ formatPair(t),
14812
15288
  formatAgentRefDisplayName(t.ownerName, agentLookup),
14813
15289
  formatSinceClaim(age),
14814
15290
  formatRoi(t.impact, t.effortDays),
@@ -14819,7 +15295,7 @@ var init_inprogress2 = __esm({
14819
15295
  // glyph
14820
15296
  { bold: true },
14821
15297
  // id
14822
- { color: inkColorForStatus(t.status) },
15298
+ { color: inkColorForPair(t) },
14823
15299
  // status
14824
15300
  { dimColor: true },
14825
15301
  // owner
@@ -15032,6 +15508,7 @@ function ReadyPopup(props) {
15032
15508
  function yankCommandForTask2(t, ws) {
15033
15509
  switch (t.status) {
15034
15510
  case "OPEN":
15511
+ if (t.substate === "parked") return `mu task unpark ${t.name} -w ${ws}`;
15035
15512
  return t.ownerName === null ? `mu task claim ${t.name} -w ${ws}` : `mu task release ${t.name} -w ${ws}`;
15036
15513
  case "IN_PROGRESS":
15037
15514
  return `mu task close ${t.name} -w ${ws} --evidence "..."`;
@@ -15045,6 +15522,7 @@ var COLUMN_SPECS18, config2;
15045
15522
  var init_ready2 = __esm({
15046
15523
  "src/cli/tui/popups/ready.tsx"() {
15047
15524
  "use strict";
15525
+ init_status();
15048
15526
  init_format();
15049
15527
  init_agent_display();
15050
15528
  init_task_list_popup();
@@ -15062,18 +15540,18 @@ var init_ready2 = __esm({
15062
15540
  label: "Tasks",
15063
15541
  sourceTasks: (snapshot2) => [...snapshot2.ready, ...snapshot2.inProgress],
15064
15542
  columnSpecs: COLUMN_SPECS18,
15065
- filterBlob: (t) => `${t.name} ${t.title} ${t.status} ${t.ownerName ?? ""}`,
15543
+ filterBlob: (t) => `${t.name} ${t.title} ${formatPair(t)} ${t.ownerName ?? ""}`,
15066
15544
  emptyText: "(no open / in-progress tasks)",
15067
15545
  yankCommand: (t, ws) => yankCommandForTask2(t, ws),
15068
15546
  dynamicHint: (focused, ws) => yankCommandForTask2(focused, ws),
15069
15547
  renderRows: (visible, _start, snapshot2) => {
15070
15548
  const agentLookup = agentByName(snapshot2);
15071
15549
  return visible.map((t) => ({
15072
- cells: [t.name, t.status, formatAgentRefDisplayName(t.ownerName, agentLookup), t.title],
15550
+ cells: [t.name, formatPair(t), formatAgentRefDisplayName(t.ownerName, agentLookup), t.title],
15073
15551
  colors: [
15074
15552
  { bold: true },
15075
15553
  // name
15076
- { color: inkColorForStatus(t.status) },
15554
+ { color: inkColorForPair(t) },
15077
15555
  // status
15078
15556
  { dimColor: true },
15079
15557
  // owner
@@ -15098,6 +15576,7 @@ var COLUMN_SPECS19, config3;
15098
15576
  var init_recent2 = __esm({
15099
15577
  "src/cli/tui/popups/recent.tsx"() {
15100
15578
  "use strict";
15579
+ init_status();
15101
15580
  init_format();
15102
15581
  init_recent();
15103
15582
  init_format_helpers();
@@ -15135,7 +15614,7 @@ var init_recent2 = __esm({
15135
15614
  cells: [
15136
15615
  GLYPH4,
15137
15616
  t.name,
15138
- t.status,
15617
+ formatPair(t),
15139
15618
  formatWhen(ageMs(t, now)),
15140
15619
  String(t.impact),
15141
15620
  String(t.effortDays),
@@ -15147,7 +15626,7 @@ var init_recent2 = __esm({
15147
15626
  // glyph
15148
15627
  { bold: true },
15149
15628
  // id
15150
- { color: inkColorForStatus(t.status) },
15629
+ { color: inkColorForPair(t) },
15151
15630
  // status
15152
15631
  { dimColor: true },
15153
15632
  // when
@@ -15170,6 +15649,16 @@ var init_recent2 = __esm({
15170
15649
  import { Box as Box15, Text as Text22, useInput as useInput9 } from "ink";
15171
15650
  import { useEffect as useEffect8, useMemo as useMemo8, useState as useState13 } from "react";
15172
15651
  import { jsx as jsx33, jsxs as jsxs18 } from "react/jsx-runtime";
15652
+ function drillColors(t) {
15653
+ return [
15654
+ { bold: true },
15655
+ // name
15656
+ { color: inkColorForPair(t) },
15657
+ // status
15658
+ void 0
15659
+ // title
15660
+ ];
15661
+ }
15173
15662
  function TracksPopup({
15174
15663
  yank: yank2,
15175
15664
  onClose,
@@ -15204,7 +15693,7 @@ function TracksPopup({
15204
15693
  const drillTasks = useMemo8(() => {
15205
15694
  if (mode !== "drill" || !focusedTrack) return [];
15206
15695
  const out = listTasks(db, workstream).filter((task) => focusedTrack.taskIds.has(task.name));
15207
- out.sort((a, b) => statusRank(a.status) - statusRank(b.status) || a.name.localeCompare(b.name));
15696
+ out.sort((a, b) => statusRank(a) - statusRank(b) || a.name.localeCompare(b.name));
15208
15697
  return out;
15209
15698
  }, [mode, focusedTrack, db, workstream]);
15210
15699
  const focusedTask = drillTasks[drillCursor];
@@ -15330,7 +15819,7 @@ function TracksPopup({
15330
15819
  return /* @__PURE__ */ jsx33(PopupShell, { title: `${trackLabel} \xB7 ${goalSummary}`, children: /* @__PURE__ */ jsx33(Text22, { dimColor: true, children: "(no tasks resolved)" }) });
15331
15820
  }
15332
15821
  const { visible: visible2 } = centredVisibleSlice(drillTasks, drillCursor, viewport);
15333
- const rows2 = visible2.map((t) => [t.name, t.status, t.title]);
15822
+ const rows2 = visible2.map((t) => [t.name, formatPair(t), t.title]);
15334
15823
  const widths2 = layoutColumns(rows2, DRILL_COLUMN_SPECS, contentWidth);
15335
15824
  return /* @__PURE__ */ jsxs18(
15336
15825
  PopupShell,
@@ -15347,7 +15836,7 @@ function TracksPopup({
15347
15836
  {
15348
15837
  cells: padded,
15349
15838
  contentWidth,
15350
- colors: DRILL_COLORS,
15839
+ colors: drillColors(t),
15351
15840
  selected: sel
15352
15841
  },
15353
15842
  t.name
@@ -15363,7 +15852,7 @@ function TracksPopup({
15363
15852
  const absoluteIndex = start + i;
15364
15853
  const goalNames = t.roots.map((r) => r.name).join(", ");
15365
15854
  const diamond = t.roots.length > 1 ? GLYPH.merge : " ";
15366
- const counts = `(${t.taskIds.size} tasks \xB7 ${t.readyCount} ready)`;
15855
+ const counts = `(${t.taskIds.size} tasks \xB7 ${t.readyCount} ready)${t.parked ? " (parked)" : ""}`;
15367
15856
  return [`Track ${absoluteIndex + 1}`, diamond, goalNames, counts];
15368
15857
  });
15369
15858
  const widths = layoutColumns(rows, COLUMN_SPECS20, contentWidth);
@@ -15394,24 +15883,24 @@ function TracksPopup({
15394
15883
  }
15395
15884
  );
15396
15885
  }
15397
- function statusRank(status) {
15398
- switch (status) {
15886
+ function statusRank(pair) {
15887
+ switch (pair.status) {
15399
15888
  case "IN_PROGRESS":
15400
15889
  return 0;
15401
15890
  case "OPEN":
15402
- return 1;
15891
+ return pair.substate === "parked" ? 2 : 1;
15403
15892
  case "CLOSED":
15404
- return 2;
15405
- default:
15406
15893
  return 3;
15407
15894
  }
15408
15895
  }
15409
- var COLUMN_SPECS20, DRILL_COLUMN_SPECS, TRACK_COLORS, DRILL_COLORS;
15896
+ var COLUMN_SPECS20, DRILL_COLUMN_SPECS, TRACK_COLORS;
15410
15897
  var init_tracks3 = __esm({
15411
15898
  "src/cli/tui/popups/tracks.tsx"() {
15412
15899
  "use strict";
15413
15900
  init_glyphs();
15901
+ init_status();
15414
15902
  init_tasks();
15903
+ init_format();
15415
15904
  init_columns();
15416
15905
  init_keys();
15417
15906
  init_list_row();
@@ -15451,14 +15940,6 @@ var init_tracks3 = __esm({
15451
15940
  { dimColor: true }
15452
15941
  // counts
15453
15942
  ];
15454
- DRILL_COLORS = [
15455
- { bold: true },
15456
- // name
15457
- { dimColor: true },
15458
- // status
15459
- void 0
15460
- // title
15461
- ];
15462
15943
  }
15463
15944
  });
15464
15945
 
@@ -16078,6 +16559,7 @@ function snapshotKey(s) {
16078
16559
  tracks: s.tracks.map((t) => ({
16079
16560
  roots: t.roots.map((r) => r.name),
16080
16561
  readyCount: t.readyCount,
16562
+ parked: t.parked,
16081
16563
  // Set → sorted array; deterministic JSON.
16082
16564
  taskIds: [...t.taskIds].sort()
16083
16565
  })),
@@ -16085,6 +16567,7 @@ function snapshotKey(s) {
16085
16567
  inProgress: s.inProgress.map(taskKey2),
16086
16568
  blocked: s.blocked.map(taskKey2),
16087
16569
  recentClosed: s.recentClosed.map(taskKey2),
16570
+ parkedCount: s.parkedCount,
16088
16571
  allTasks: s.allTasks.map(taskKey2),
16089
16572
  commitsBackend: s.commitsBackend ?? null,
16090
16573
  recentCommits: s.recentCommits.map((c) => [c.sha, c.subject, c.author, c.relTime]),
@@ -16107,6 +16590,7 @@ function taskKey2(t) {
16107
16590
  return [
16108
16591
  t.name,
16109
16592
  t.status,
16593
+ t.substate,
16110
16594
  t.impact,
16111
16595
  t.effortDays,
16112
16596
  t.ownerName ?? "",
@@ -17093,7 +17577,7 @@ function partitionEdges(edges) {
17093
17577
  function formatEdgeList(edges, dim) {
17094
17578
  if (edges.length === 0) return pc.dim("\u2014");
17095
17579
  const parts = edges.map((e) => {
17096
- const piece = `${e.name} [${colorStatus(e.status)}]`;
17580
+ const piece = `${e.name} [${colorPair(e)}]`;
17097
17581
  return dim ? pc.dim(piece) : piece;
17098
17582
  });
17099
17583
  return parts.join(", ");
@@ -17231,7 +17715,7 @@ async function cmdTaskShow(db, rawId, opts = {}) {
17231
17715
  const roi = task.effortDays > 0 ? (task.impact / task.effortDays).toFixed(1) : "\u221E";
17232
17716
  console.log(pc.bold(`${task.name} \u2014 ${task.title}`));
17233
17717
  console.log(` workstream : ${task.workstreamName}`);
17234
- console.log(` status : ${task.status}`);
17718
+ console.log(` status : ${colorPair(task)}`);
17235
17719
  const ownerLine = task.ownerName !== null ? task.ownerName : lastActor !== null ? pc.dim(`(self: ${lastActor})`) : pc.dim("(unowned)");
17236
17720
  console.log(` owner : ${ownerLine}`);
17237
17721
  console.log(` impact : ${task.impact}`);
@@ -17261,6 +17745,14 @@ async function cmdTaskShow(db, rawId, opts = {}) {
17261
17745
  } else {
17262
17746
  for (const n of notes) printNote(n);
17263
17747
  }
17748
+ const ref = `${task.name} -w ${task.workstreamName}`;
17749
+ if (task.substate === "parked") {
17750
+ console.log("");
17751
+ printNextSteps([{ intent: "Return it to next", command: `mu task unpark ${ref}` }]);
17752
+ } else if (task.status === "CLOSED" && task.substate !== "done") {
17753
+ console.log("");
17754
+ printNextSteps([{ intent: "Reopen it", command: `mu task open ${ref}` }]);
17755
+ }
17264
17756
  }
17265
17757
  async function cmdTaskNotes(db, rawId, opts = {}) {
17266
17758
  if (opts.since !== void 0 && opts.sinceClaim === true) {
@@ -17368,6 +17860,8 @@ async function cmdTaskList(db, opts) {
17368
17860
  if (statuses !== void 0) {
17369
17861
  listOpts.status = statuses;
17370
17862
  }
17863
+ const substates = parseSubstatesOption(opts.substate);
17864
+ if (substates !== void 0) listOpts.substate = substates;
17371
17865
  const sortKey = opts.sort === void 0 ? "id" : parseSortOption(opts.sort);
17372
17866
  const tasks = sortTasks(listTasks(db, workstream, listOpts), sortKey);
17373
17867
  if (opts.json) {
@@ -17389,6 +17883,8 @@ async function cmdTaskNext(db, opts) {
17389
17883
  if (statuses !== void 0) {
17390
17884
  readyOpts.status = statuses;
17391
17885
  }
17886
+ const substates = parseSubstatesOption(opts.substate);
17887
+ if (substates !== void 0) readyOpts.substate = substates;
17392
17888
  const sorted = sortTasks(listReady(db, workstream, readyOpts), sortKey);
17393
17889
  const tasks = k === 0 ? sorted : sorted.slice(0, k);
17394
17890
  if (opts.json) {
@@ -17396,7 +17892,18 @@ async function cmdTaskNext(db, opts) {
17396
17892
  return;
17397
17893
  }
17398
17894
  if (tasks.length === 0) {
17399
- console.log(pc.dim("(no ready tasks)"));
17895
+ const parked = listTasks(db, workstream, { substate: "parked" }).length;
17896
+ if (parked === 0) {
17897
+ console.log(pc.dim("(no ready tasks)"));
17898
+ return;
17899
+ }
17900
+ console.log(pc.dim(`(no ready tasks; ${parked} parked)`));
17901
+ printNextSteps([
17902
+ {
17903
+ intent: "List parked tasks",
17904
+ command: `mu task list --substate parked -w ${workstream}`
17905
+ }
17906
+ ]);
17400
17907
  return;
17401
17908
  }
17402
17909
  const tableOpts = {};
@@ -17419,6 +17926,7 @@ async function cmdTaskOwnedBy(db, agent, opts = {}) {
17419
17926
  }
17420
17927
 
17421
17928
  // src/cli/tasks/wire.ts
17929
+ init_status();
17422
17930
  init_tasks();
17423
17931
 
17424
17932
  // src/cli/tasks/claim.ts
@@ -17502,6 +18010,7 @@ async function cmdClaim(db, rawId, opts) {
17502
18010
  }
17503
18011
  const sdkOpts = { workstream: ws };
17504
18012
  if (forName !== void 0) sdkOpts.agentName = forName;
18013
+ if (opts.force) sdkOpts.force = true;
17505
18014
  if (forWorkstream !== void 0) sdkOpts.agentWorkstream = forWorkstream;
17506
18015
  if (opts.self) sdkOpts.self = true;
17507
18016
  if (opts.actor !== void 0) sdkOpts.actor = opts.actor;
@@ -17567,6 +18076,9 @@ async function cmdClaim(db, rawId, opts) {
17567
18076
  function qualifiedId(ref) {
17568
18077
  return `${ref.workstreamName}/${ref.name}`;
17569
18078
  }
18079
+ function isUndelivered(ref) {
18080
+ return ref.status === "CLOSED" && ref.substate !== "done";
18081
+ }
17570
18082
  function cherryPickCommandForCommits(commits) {
17571
18083
  const first = commits[0];
17572
18084
  if (first === void 0) return null;
@@ -17680,7 +18192,12 @@ async function cmdTaskWait(db, ids, opts) {
17680
18192
  const reachedRefs = result.refs.filter((t) => t.reachedTarget);
17681
18193
  const unmetRefs = result.refs.filter((t) => !t.reachedTarget);
17682
18194
  const nextSteps3 = [];
17683
- if (!result.timedOut && firingRef !== null) {
18195
+ if (!result.timedOut && firingRef !== null && isUndelivered(firingRef)) {
18196
+ nextSteps3.push({
18197
+ intent: `Read why ${firingRef.name} closed ${formatPair(firingRef)} (no deliverable)`,
18198
+ command: `mu task notes ${firingRef.name} -w ${firingRef.workstreamName}`
18199
+ });
18200
+ } else if (!result.timedOut && firingRef !== null) {
17684
18201
  const owner = firingRef.owner;
17685
18202
  if (owner !== null) {
17686
18203
  nextSteps3.push(await nextStepForFiringOwner(db, owner, firingRef.workstreamName));
@@ -17728,6 +18245,7 @@ async function cmdTaskWait(db, ids, opts) {
17728
18245
  name: firingRef.name,
17729
18246
  qualifiedId: qualifiedId(firingRef),
17730
18247
  status: firingRef.status,
18248
+ substate: firingRef.substate,
17731
18249
  owner: firingRef.owner
17732
18250
  };
17733
18251
  const timedOutArray = result.timedOut ? unmetRefs.map((t) => ({ ...t, qualifiedId: qualifiedId(t) })) : [];
@@ -17755,7 +18273,7 @@ async function cmdTaskWait(db, ids, opts) {
17755
18273
  for (const t of result.refs) {
17756
18274
  const marker = t.reachedTarget ? pc.green(GLYPH.ok) : pc.dim("\u2022");
17757
18275
  const label2 = workstreamSet.size > 1 ? qualifiedId(t) : t.name;
17758
- console.log(` ${marker} ${pc.bold(label2)} ${pc.dim(`(${t.status})`)}`);
18276
+ console.log(` ${marker} ${pc.bold(label2)} ${pc.dim(`(${formatPair(t)})`)}`);
17759
18277
  }
17760
18278
  printNextSteps(nextSteps3);
17761
18279
  if (result.timedOut) throw new CliExitError(5);
@@ -17932,6 +18450,7 @@ async function cmdTaskDelete(db, rawId, opts = {}) {
17932
18450
  // src/cli/tasks/lifecycle.ts
17933
18451
  init_agents();
17934
18452
  init_output();
18453
+ init_status();
17935
18454
  init_tasks();
17936
18455
  init_vcs2();
17937
18456
  init_workspace();
@@ -17943,6 +18462,9 @@ async function cmdTaskClose(db, rawId, opts = {}) {
17943
18462
  const sdkOpts = { workstream: ws };
17944
18463
  if (opts.evidence !== void 0) sdkOpts.evidence = opts.evidence;
17945
18464
  if (opts.ifReady) sdkOpts.ifReady = true;
18465
+ if (opts.as !== void 0) sdkOpts.as = opts.as.toLowerCase();
18466
+ if (opts.why !== void 0) sdkOpts.why = opts.why;
18467
+ if (opts.why !== void 0 && opts.why !== "") sdkOpts.author = actor;
17946
18468
  if (opts.evidence !== void 0 && opts.evidence !== "") {
17947
18469
  sdkOpts.author = actor;
17948
18470
  }
@@ -17976,9 +18498,16 @@ async function cmdTaskClose(db, rawId, opts = {}) {
17976
18498
  return;
17977
18499
  }
17978
18500
  if (r.changed && taskRow?.ownerName) await refreshAgentTitle(db, taskRow.ownerName, ws);
18501
+ const pickNext = {
18502
+ intent: r.unblocked.length > 0 ? "Pick up an unblocked task" : "Pick the next ready task",
18503
+ command: `mu task next -w ${ws}`
18504
+ };
18505
+ const reopen = {
18506
+ intent: "Reopen if needed",
18507
+ command: `mu task open ${localId} -w ${ws}`
18508
+ };
17979
18509
  const nextSteps3 = [
17980
- { intent: "Reopen if needed", command: `mu task open ${localId} -w ${ws}` },
17981
- { intent: "Pick the next ready task", command: `mu task next -w ${ws}` },
18510
+ ...r.unblocked.length > 0 ? [pickNext, reopen] : [reopen, pickNext],
17982
18511
  { intent: "See full state", command: `mu state -w ${ws}` }
17983
18512
  ];
17984
18513
  if (r.changed && r.status === "CLOSED") {
@@ -17994,10 +18523,15 @@ async function cmdTaskClose(db, rawId, opts = {}) {
17994
18523
  return;
17995
18524
  }
17996
18525
  const ev = opts.evidence ? pc.dim(` evidence: ${opts.evidence}`) : "";
17997
- console.log(`Closed ${pc.bold(localId)} ${pc.dim(`(${r.previousStatus} \u2192 ${r.status})`)}`);
18526
+ console.log(`Closed ${pc.bold(localId)} ${pc.dim(`(${transition(r)})`)}`);
17998
18527
  if (ev) console.log(ev);
18528
+ if (r.unblocked.length > 0) console.log(`Unblocked: ${r.unblocked.join(", ")}`);
17999
18529
  printNextSteps(nextSteps3);
18000
18530
  }
18531
+ function transition(r) {
18532
+ const from = formatPair({ status: r.previousStatus, substate: r.previousSubstate });
18533
+ return `${from} \u2192 ${formatPair({ status: r.status, substate: r.substate })}`;
18534
+ }
18001
18535
  async function maybeAppendDirtyWorkspaceCommitHint(db, nextSteps3, actor, workstream, taskTitle) {
18002
18536
  if (actor.length === 0) return;
18003
18537
  try {
@@ -18040,10 +18574,62 @@ async function cmdTaskOpen(db, rawId, opts = {}) {
18040
18574
  return;
18041
18575
  }
18042
18576
  const ev = opts.evidence ? pc.dim(` evidence: ${opts.evidence}`) : "";
18043
- console.log(`Reopened ${pc.bold(localId)} ${pc.dim(`(${r.previousStatus} \u2192 ${r.status})`)}`);
18577
+ console.log(`Reopened ${pc.bold(localId)} ${pc.dim(`(${transition(r)})`)}`);
18044
18578
  if (ev) console.log(ev);
18045
18579
  printNextSteps(nextSteps3);
18046
18580
  }
18581
+ async function cmdTaskPark(db, rawId, opts) {
18582
+ const { name: localId } = await resolveEntityRef(db, rawId, opts, "task");
18583
+ assertTaskInWorkstream(db, localId, opts.workstream);
18584
+ const ws = await resolveWorkstream(opts.workstream);
18585
+ const sdkOpts = {
18586
+ workstream: ws,
18587
+ why: opts.why,
18588
+ author: await resolveActorIdentity()
18589
+ };
18590
+ if (opts.evidence !== void 0) sdkOpts.evidence = opts.evidence;
18591
+ const r = parkTask(db, localId, sdkOpts);
18592
+ const nextSteps3 = [
18593
+ { intent: "Unpark it later", command: `mu task unpark ${localId} -w ${ws}` },
18594
+ { intent: "List parked tasks", command: `mu task list --substate parked -w ${ws}` }
18595
+ ];
18596
+ if (opts.json) {
18597
+ emitJson({ taskName: localId, ...r, nextSteps: nextSteps3 });
18598
+ return;
18599
+ }
18600
+ if (!r.changed) {
18601
+ console.log(pc.dim(`${localId} already OPEN/parked (no-op)`));
18602
+ } else {
18603
+ console.log(`Parked ${pc.bold(localId)} ${pc.dim(`(${transition(r)})`)}`);
18604
+ }
18605
+ printNextSteps(nextSteps3);
18606
+ }
18607
+ async function cmdTaskUnpark(db, rawId, opts = {}) {
18608
+ const { name: localId } = await resolveEntityRef(db, rawId, opts, "task");
18609
+ assertTaskInWorkstream(db, localId, opts.workstream);
18610
+ const ws = await resolveWorkstream(opts.workstream);
18611
+ const sdkOpts = { workstream: ws };
18612
+ if (opts.evidence !== void 0) sdkOpts.evidence = opts.evidence;
18613
+ const r = unparkTask(db, localId, sdkOpts);
18614
+ const nextSteps3 = [
18615
+ {
18616
+ intent: "Claim it",
18617
+ command: `mu task claim ${localId} -w ${ws} (--self / --for <worker>)`
18618
+ },
18619
+ { intent: "Pick the next ready task", command: `mu task next -w ${ws}` }
18620
+ ];
18621
+ if (opts.json) {
18622
+ emitJson({ taskName: localId, ...r, nextSteps: nextSteps3 });
18623
+ return;
18624
+ }
18625
+ if (!r.changed) {
18626
+ const pair = formatPair({ status: r.status, substate: r.substate });
18627
+ console.log(pc.dim(`${localId} is ${pair}, not parked (no-op)`));
18628
+ } else {
18629
+ console.log(`Unparked ${pc.bold(localId)} ${pc.dim(`(${transition(r)})`)}`);
18630
+ }
18631
+ printNextSteps(nextSteps3);
18632
+ }
18047
18633
 
18048
18634
  // src/cli/tasks/tree.ts
18049
18635
  init_dag();
@@ -18068,13 +18654,7 @@ async function cmdTaskTree(db, rawId, opts) {
18068
18654
  const swapHint = down ? "swap to --no-down for blockers" : "--down for dependents";
18069
18655
  console.log(pc.bold(`Tree of ${rootId} ${pc.dim(`(${direction} below; ${swapHint})`)}`));
18070
18656
  console.log(
18071
- renderTaskTree(
18072
- db,
18073
- ws,
18074
- root,
18075
- down ? "dependents" : "blockers",
18076
- (task) => colorStatus(task.status)
18077
- )
18657
+ renderTaskTree(db, ws, root, down ? "dependents" : "blockers", (task) => colorPair(task))
18078
18658
  );
18079
18659
  }
18080
18660
  function buildJsonTree(db, workstream, taskId, down, seen) {
@@ -18107,10 +18687,11 @@ function wireTaskCommands(program) {
18107
18687
  return handle((db) => cmdTaskAdd(db, id, opts), this)();
18108
18688
  });
18109
18689
  const SORT_OPT_DESC = `sort key (${TASK_SORT_KEYS.join(" | ")})`;
18690
+ const SUBSTATE_OPT_DESC = `filter by substate (${[...new Set(Object.values(TASK_SUBSTATES).flat())].join(" | ")}; repeat or comma-separate)`;
18110
18691
  task.command("list").description("List every task in the current workstream (id, status, ROI, owner)").option(...WORKSTREAM_OPT).option(
18111
18692
  "--status <status...>",
18112
18693
  `filter by lifecycle status (${TASK_STATUS_LIST}; case-insensitive; repeat or comma-separate; or both)`
18113
- ).option("--sort <key>", `${SORT_OPT_DESC} (default id)`).option(...JSON_OPT).action(function() {
18694
+ ).option("--substate <substate...>", SUBSTATE_OPT_DESC).option("--sort <key>", `${SORT_OPT_DESC} (default id)`).option(...JSON_OPT).action(function() {
18114
18695
  const opts = this.opts();
18115
18696
  return handle((db) => cmdTaskList(db, opts), this)();
18116
18697
  });
@@ -18123,7 +18704,7 @@ function wireTaskCommands(program) {
18123
18704
  ).option(...WORKSTREAM_OPT).option("--sort <key>", `${SORT_OPT_DESC} (default roi)`).option(
18124
18705
  "--status <status...>",
18125
18706
  `filter by lifecycle status (${TASK_STATUS_LIST}; case-insensitive; repeat or comma-separate; or both)`
18126
- ).option(...JSON_OPT).action(function() {
18707
+ ).option("--substate <substate...>", SUBSTATE_OPT_DESC).option(...JSON_OPT).action(function() {
18127
18708
  const opts = this.opts();
18128
18709
  return handle((db) => cmdTaskNext(db, opts), this)();
18129
18710
  });
@@ -18185,8 +18766,11 @@ function wireTaskCommands(program) {
18185
18766
  "record what the caller observed (e.g. 'tests pass: npm test exit 0'); appears verbatim in the event log"
18186
18767
  ];
18187
18768
  task.command("close <id>").description(
18188
- "Mark a task CLOSED (idempotent). --if-ready no-ops unless every blocker is CLOSED \u2014 the umbrella-on-wave-done pattern."
18769
+ "Mark a task CLOSED (idempotent). --as records why it closed (not every close is done work); any CLOSED substate satisfies blockers. --if-ready no-ops unless every blocker is CLOSED \u2014 the umbrella-on-wave-done pattern."
18189
18770
  ).option(
18771
+ "--as <substate>",
18772
+ `closing substate (${TASK_SUBSTATES.CLOSED.join(" | ")}; default done)`
18773
+ ).option("--why <text>", "reason, stored as a note; required unless --as done").option(
18190
18774
  "--if-ready",
18191
18775
  "only close when every blocker is CLOSED; otherwise no-op + list the still-blocking ids"
18192
18776
  ).option(...WORKSTREAM_OPT).option(...EVIDENCE_OPT).option(...JSON_OPT).action(function(id) {
@@ -18197,6 +18781,16 @@ function wireTaskCommands(program) {
18197
18781
  const opts = this.opts();
18198
18782
  return handle((db) => cmdTaskOpen(db, id, opts), this)();
18199
18783
  });
18784
+ task.command("park <id>").description(
18785
+ "Park an OPEN task (OPEN/parked): it leaves `next` and `claim` refuses it without --force. Dependents stay blocked. Idempotent."
18786
+ ).requiredOption("--why <text>", "why it is parked; stored as a note").option(...WORKSTREAM_OPT).option(...EVIDENCE_OPT).option(...JSON_OPT).action(function(id) {
18787
+ const opts = this.opts();
18788
+ return handle((db) => cmdTaskPark(db, id, opts), this)();
18789
+ });
18790
+ task.command("unpark <id>").description("Return a parked task to OPEN (back in `next`). No-op on any other state.").option(...WORKSTREAM_OPT).option(...EVIDENCE_OPT).option(...JSON_OPT).action(function(id) {
18791
+ const opts = this.opts();
18792
+ return handle((db) => cmdTaskUnpark(db, id, opts), this)();
18793
+ });
18200
18794
  task.command("release <id>").description(
18201
18795
  "Clear a task's owner. IN_PROGRESS auto-flips to OPEN so the task re-enters the ready set; other statuses preserved. Use --reopen to force OPEN from CLOSED. Idempotent."
18202
18796
  ).option(
@@ -18217,7 +18811,7 @@ function wireTaskCommands(program) {
18217
18811
  ).option(
18218
18812
  "--actor <name>",
18219
18813
  "override the actor name used for the log (only valid with --self; defaults to pane title or $USER)"
18220
- ).option(
18814
+ ).option("--force", "claim even when the task is parked (OPEN/parked)").option(
18221
18815
  "--strict-staleness",
18222
18816
  "refuse --for dispatch when the target agent's workspace is stale (default: warn and proceed)"
18223
18817
  ).option(...WORKSTREAM_OPT).option(...EVIDENCE_OPT).option(...JSON_OPT).action(function(taskId) {
@@ -18859,6 +19453,7 @@ init_tasks();
18859
19453
  import { CommanderError } from "commander";
18860
19454
 
18861
19455
  // src/undo.ts
19456
+ init_apply();
18862
19457
  init_db();
18863
19458
  init_legacy_ops();
18864
19459
  init_logs();
@@ -19037,13 +19632,13 @@ function planUndo(db, groupId) {
19037
19632
  if (Object.keys(fields3).length === 0) fields3 = payloadFields(row2.payload);
19038
19633
  if (Object.keys(fields3).length === 0 && row2.entity === "note") {
19039
19634
  const recovered = recoverLegacyNoteContents(db, row2.key, row2.hlc, seenLegacyNoteTasks);
19040
- for (const legacy of recovered) {
19635
+ for (const legacy2 of recovered) {
19041
19636
  inverses.push({
19042
19637
  entity: "note",
19043
- key: legacy.key,
19638
+ key: legacy2.key,
19044
19639
  op: "put",
19045
- fields: legacy.fields,
19046
- summary: `note ${legacy.key} (recovered from legacy history)`,
19640
+ fields: legacy2.fields,
19641
+ summary: `note ${legacy2.key} (recovered from legacy history)`,
19047
19642
  supersededBy: []
19048
19643
  });
19049
19644
  }
@@ -19051,7 +19646,11 @@ function planUndo(db, groupId) {
19051
19646
  continue;
19052
19647
  }
19053
19648
  const status = fields3.status;
19054
- if (typeof status === "string") fields3.status = normalizeTaskStatus(status);
19649
+ const legacy = typeof status === "string" ? mapLegacyStatus(status) : null;
19650
+ if (legacy) {
19651
+ fields3.status = legacy.status;
19652
+ fields3.substate = legacy.substate;
19653
+ }
19055
19654
  const conflicts = laterRowWriters(db, row2.entity, row2.key, row2.hlc, groupId).map((w) => ({
19056
19655
  field: "<row>",
19057
19656
  groupId: w.groupId,
@@ -19271,15 +19870,23 @@ function restoreRow(db, inverse, table) {
19271
19870
  if (id === null) {
19272
19871
  const wsId = ensureWorkstream2(db, parsed.workstream);
19273
19872
  const now = (/* @__PURE__ */ new Date()).toISOString();
19873
+ const pair = resolvePair(
19874
+ String(inverse.fields.status ?? "OPEN"),
19875
+ inverse.fields.substate
19876
+ ) ?? {
19877
+ status: "OPEN",
19878
+ substate: "todo"
19879
+ };
19274
19880
  db.prepare(
19275
- `INSERT INTO tasks (workstream_id, local_id, title, status, impact, effort_days,
19276
- owner_id, created_at, updated_at)
19277
- VALUES (?, ?, ?, ?, ?, ?, NULL, ?, ?)`
19881
+ `INSERT INTO tasks (workstream_id, local_id, title, status, substate, impact,
19882
+ effort_days, owner_id, created_at, updated_at)
19883
+ VALUES (?, ?, ?, ?, ?, ?, ?, NULL, ?, ?)`
19278
19884
  ).run(
19279
19885
  wsId,
19280
19886
  parsed.localId,
19281
19887
  String(inverse.fields.title ?? parsed.localId),
19282
- String(inverse.fields.status ?? "OPEN"),
19888
+ pair.status,
19889
+ pair.substate,
19283
19890
  Number(inverse.fields.impact ?? 50),
19284
19891
  Number(inverse.fields.effort_days ?? 1),
19285
19892
  String(inverse.fields.created_at ?? now),
@@ -19287,7 +19894,8 @@ function restoreRow(db, inverse, table) {
19287
19894
  );
19288
19895
  return true;
19289
19896
  }
19290
- return updateFields(db, "tasks", "id", id, inverse.fields);
19897
+ const updated = updateFields(db, "tasks", "id", id, inverse.fields);
19898
+ return repairTaskPair(db, id) || updated;
19291
19899
  }
19292
19900
  case "note": {
19293
19901
  const parsed = parseNoteKey2(inverse.key);
@@ -19443,7 +20051,9 @@ function classifyError(err) {
19443
20051
  if (err instanceof AgentNotFoundError || err instanceof TaskNotFoundError || err instanceof WorkstreamNotFoundError || err instanceof WorkspaceNotFoundError) {
19444
20052
  return { label: "not found", exitCode: 3 };
19445
20053
  }
19446
- if (err instanceof NameAmbiguousError || err instanceof AgentExistsError || err instanceof TaskExistsError || err instanceof TaskAlreadyOwnedError || err instanceof TaskClaimStaleWorkspaceError || err instanceof TaskNotInWorkstreamError || err instanceof AgentNotInWorkstreamError || err instanceof CycleError || err instanceof CrossWorkstreamEdgeError || err instanceof WorkspaceExistsError || err instanceof WorkspacePathNotEmptyError || err instanceof WorkspacePreservedError || err instanceof HomeDirAsProjectRootError || err instanceof WorkspaceVcsRequiredError || err instanceof WorkspaceDirtyError || err instanceof ClaimerNotRegisteredError || err instanceof SchemaTooOldError || err instanceof TaskIdInvalidError || err instanceof WorkstreamExistsError || // Rebuild refuses to overwrite an existing target or to write onto
20054
+ if (err instanceof NameAmbiguousError || err instanceof AgentExistsError || err instanceof TaskExistsError || err instanceof TaskAlreadyOwnedError || // Substate refusals (ts_4): a missing --why, a park from the wrong
20055
+ // phase, a claim on a parked task, a pair outside TASK_SUBSTATES.
20056
+ err instanceof SubstateReasonRequiredError || err instanceof TaskParkStateError || err instanceof TaskParkedError || err instanceof InvalidSubstateError || err instanceof TaskClaimStaleWorkspaceError || err instanceof TaskNotInWorkstreamError || err instanceof AgentNotInWorkstreamError || err instanceof CycleError || err instanceof CrossWorkstreamEdgeError || err instanceof WorkspaceExistsError || err instanceof WorkspacePathNotEmptyError || err instanceof WorkspacePreservedError || err instanceof HomeDirAsProjectRootError || err instanceof WorkspaceVcsRequiredError || err instanceof WorkspaceDirtyError || err instanceof ClaimerNotRegisteredError || err instanceof SchemaTooOldError || err instanceof SchemaTooNewError || err instanceof TaskIdInvalidError || err instanceof WorkstreamExistsError || // Rebuild refuses to overwrite an existing target or to write onto
19447
20057
  // the source DB: both are name/state collisions, not usage errors.
19448
20058
  err instanceof RebuildTargetExistsError || err instanceof RebuildTargetIsSourceError || // An abbreviated group id matching several groups: the operator must
19449
20059
  // disambiguate. Guessing would be catastrophic for `mu undo`.
@@ -22053,6 +22663,7 @@ function wireWorkstreamCommands(program) {
22053
22663
  // src/cli.ts
22054
22664
  init_db();
22055
22665
  init_sort();
22666
+ init_status();
22056
22667
  init_tasks();
22057
22668
  init_workstream();
22058
22669
  init_format();
@@ -22150,6 +22761,23 @@ function parseStatusesOption(values, flag = "--status") {
22150
22761
  }
22151
22762
  return out;
22152
22763
  }
22764
+ var ALL_SUBSTATES = [...new Set(Object.values(TASK_SUBSTATES).flat())];
22765
+ function parseSubstatesOption(values, flag = "--substate") {
22766
+ const fragments = parseCsvFlag(values, flag);
22767
+ if (fragments.length === 0) return void 0;
22768
+ const out = /* @__PURE__ */ new Set();
22769
+ for (const raw of fragments) {
22770
+ const lower = raw.toLowerCase();
22771
+ const match = ALL_SUBSTATES.find((s) => s === lower);
22772
+ if (match === void 0) {
22773
+ throw new UsageError(
22774
+ `${flag} must be one of ${ALL_SUBSTATES.join(" | ")} (got ${JSON.stringify(raw)})`
22775
+ );
22776
+ }
22777
+ out.add(match);
22778
+ }
22779
+ return [...out];
22780
+ }
22153
22781
  function resolveSelfOptional(db) {
22154
22782
  const paneId = process.env.TMUX_PANE;
22155
22783
  if (!paneId) return null;
@@ -22425,6 +23053,7 @@ export {
22425
23053
  buildProgram,
22426
23054
  byRoiDesc,
22427
23055
  classifyError,
23056
+ colorPair,
22428
23057
  colorStatus,
22429
23058
  emitJson,
22430
23059
  emitJsonCollection,
@@ -22449,6 +23078,7 @@ export {
22449
23078
  parseSortOption,
22450
23079
  parseStatusOption,
22451
23080
  parseStatusesOption,
23081
+ parseSubstatesOption,
22452
23082
  printLogRow,
22453
23083
  relTime,
22454
23084
  relTimeBasisForSort,