@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/index.js CHANGED
@@ -1762,6 +1762,7 @@ var CAPTURED_COLUMNS = {
1762
1762
  "local_id",
1763
1763
  "title",
1764
1764
  "status",
1765
+ "substate",
1765
1766
  "impact",
1766
1767
  "effort_days",
1767
1768
  "owner_id",
@@ -1872,6 +1873,48 @@ function installCapture(db) {
1872
1873
  db.exec(CAPTURE_TRIGGER_DDL);
1873
1874
  }
1874
1875
 
1876
+ // src/tasks/status.ts
1877
+ var TASK_STATUSES = ["OPEN", "IN_PROGRESS", "CLOSED"];
1878
+ function isTaskStatus(s) {
1879
+ return TASK_STATUSES.includes(s);
1880
+ }
1881
+ var TASK_STATUS_LIST = TASK_STATUSES.join(" | ");
1882
+ var TASK_SUBSTATES = {
1883
+ OPEN: ["todo", "parked"],
1884
+ IN_PROGRESS: ["active"],
1885
+ CLOSED: ["done", "rejected", "wontfix", "duplicate", "superseded"]
1886
+ };
1887
+ var DEFAULT_SUBSTATE = { OPEN: "todo", IN_PROGRESS: "active", CLOSED: "done" };
1888
+ var TASK_SUBSTATE_ROWS = TASK_STATUSES.flatMap(
1889
+ (status) => TASK_SUBSTATES[status].map(
1890
+ (substate) => [status, substate, substate === DEFAULT_SUBSTATE[status] ? 1 : 0]
1891
+ )
1892
+ );
1893
+ function isValidPair(status, substate) {
1894
+ if (!isTaskStatus(status)) return false;
1895
+ return TASK_SUBSTATES[status].includes(substate);
1896
+ }
1897
+ var LEGACY_STATUS_PAIRS = {
1898
+ REJECTED: { status: "CLOSED", substate: "rejected" },
1899
+ DEFERRED: { status: "OPEN", substate: "parked" }
1900
+ };
1901
+ function mapLegacyStatus(value) {
1902
+ const pair = LEGACY_STATUS_PAIRS[value];
1903
+ return pair ? { ...pair } : null;
1904
+ }
1905
+ function resolvePair(status, substate) {
1906
+ const legacy = mapLegacyStatus(status);
1907
+ if (legacy) return legacy;
1908
+ if (!isTaskStatus(status)) return null;
1909
+ if (typeof substate === "string" && isValidPair(status, substate)) {
1910
+ return { status, substate };
1911
+ }
1912
+ return { status, substate: DEFAULT_SUBSTATE[status] };
1913
+ }
1914
+ function formatPair(pair) {
1915
+ return pair.substate === DEFAULT_SUBSTATE[pair.status] ? pair.status : `${pair.status}/${pair.substate}`;
1916
+ }
1917
+
1875
1918
  // src/db.ts
1876
1919
  function defaultStateDir() {
1877
1920
  if (process.env.MU_STATE_DIR) return process.env.MU_STATE_DIR;
@@ -1892,6 +1935,13 @@ function openDb(options = {}) {
1892
1935
  db.pragma("foreign_keys = ON");
1893
1936
  db.pragma("busy_timeout = 5000");
1894
1937
  const detectedVersion = detectExistingSchemaVersion(db);
1938
+ if (detectedVersion !== null && detectedVersion > CURRENT_SCHEMA_VERSION) {
1939
+ try {
1940
+ db.close();
1941
+ } catch {
1942
+ }
1943
+ throw new SchemaTooNewError(detectedVersion, CURRENT_SCHEMA_VERSION);
1944
+ }
1895
1945
  if (detectedVersion !== null && detectedVersion < MIN_ACCEPTED_SCHEMA_VERSION) {
1896
1946
  try {
1897
1947
  db.close();
@@ -1967,7 +2017,7 @@ var SchemaTooOldError = class extends Error {
1967
2017
  command: `mv "\${MU_DB_PATH:-$HOME/.local/state/mu/mu.db}" "\${MU_DB_PATH:-$HOME/.local/state/mu/mu.db}.old"`
1968
2018
  },
1969
2019
  {
1970
- intent: `Migrate the preserved DB into a fresh v${this.requiredVersion} file`,
2020
+ intent: `Migrate the old DB into a fresh v${this.requiredVersion} DB`,
1971
2021
  command: `npx tsx scripts/migrate.ts "\${MU_DB_PATH:-$HOME/.local/state/mu/mu.db}.old" --out /tmp/mu-v${this.requiredVersion}.db`
1972
2022
  },
1973
2023
  {
@@ -1977,6 +2027,19 @@ var SchemaTooOldError = class extends Error {
1977
2027
  ];
1978
2028
  }
1979
2029
  };
2030
+ var SchemaTooNewError = class extends Error {
2031
+ constructor(detectedVersion, supportedVersion) {
2032
+ super(`DB is v${detectedVersion}; this mu understands up to v${supportedVersion}. Upgrade mu.`);
2033
+ this.detectedVersion = detectedVersion;
2034
+ this.supportedVersion = supportedVersion;
2035
+ }
2036
+ detectedVersion;
2037
+ supportedVersion;
2038
+ name = "SchemaTooNewError";
2039
+ errorNextSteps() {
2040
+ return [{ intent: "Check which mu is on PATH", command: "which mu && mu --version" }];
2041
+ }
2042
+ };
1980
2043
  function detectExistingSchemaVersion(db) {
1981
2044
  const hasVersionTable = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='schema_version'").get();
1982
2045
  if (hasVersionTable) {
@@ -1999,6 +2062,12 @@ function applySchema(db) {
1999
2062
  db.exec("BEGIN IMMEDIATE");
2000
2063
  try {
2001
2064
  db.exec(CURRENT_SCHEMA);
2065
+ const seed = db.prepare(
2066
+ "INSERT OR IGNORE INTO task_substates (status, substate, is_default) VALUES (?, ?, ?)"
2067
+ );
2068
+ for (const [status, substate, isDefault] of TASK_SUBSTATE_ROWS) {
2069
+ seed.run(status, substate, isDefault);
2070
+ }
2002
2071
  db.exec("COMMIT");
2003
2072
  } catch (err) {
2004
2073
  db.exec("ROLLBACK");
@@ -2009,8 +2078,8 @@ function applySchema(db) {
2009
2078
  CURRENT_SCHEMA_VERSION
2010
2079
  );
2011
2080
  }
2012
- var CURRENT_SCHEMA_VERSION = 10;
2013
- var MIN_ACCEPTED_SCHEMA_VERSION = 10;
2081
+ var CURRENT_SCHEMA_VERSION = 11;
2082
+ var MIN_ACCEPTED_SCHEMA_VERSION = 11;
2014
2083
  var EXPECTED_TABLES = [
2015
2084
  "agents",
2016
2085
  "machine_identity",
@@ -2019,6 +2088,7 @@ var EXPECTED_TABLES = [
2019
2088
  "sync_peers",
2020
2089
  "task_edges",
2021
2090
  "task_notes",
2091
+ "task_substates",
2022
2092
  "tasks",
2023
2093
  "vcs_workspaces",
2024
2094
  "workstreams"
@@ -2032,6 +2102,7 @@ var MACHINE_LOCAL_TABLES = [
2032
2102
  "ops",
2033
2103
  "schema_version",
2034
2104
  "sync_peers",
2105
+ "task_substates",
2035
2106
  "vcs_workspaces"
2036
2107
  ];
2037
2108
  var READY_VIEW_SQL = `
@@ -2040,6 +2111,7 @@ CREATE VIEW ready AS
2040
2111
  SELECT t.*
2041
2112
  FROM tasks t
2042
2113
  WHERE t.status = 'OPEN'
2114
+ AND t.substate <> 'parked'
2043
2115
  AND NOT EXISTS (
2044
2116
  SELECT 1
2045
2117
  FROM task_edges e
@@ -2135,6 +2207,19 @@ CREATE TABLE IF NOT EXISTS agents (
2135
2207
  CREATE INDEX IF NOT EXISTS idx_agents_workstream ON agents (workstream_id);
2136
2208
  CREATE INDEX IF NOT EXISTS idx_agents_status ON agents (status);
2137
2209
 
2210
+ -- task_substates: the legal (status, substate) pairs. Seeded from
2211
+ -- TASK_SUBSTATE_ROWS (src/tasks/status.ts) by applySchema; exactly one
2212
+ -- default per status.
2213
+ CREATE TABLE IF NOT EXISTS task_substates (
2214
+ status TEXT NOT NULL,
2215
+ substate TEXT NOT NULL,
2216
+ is_default INTEGER NOT NULL DEFAULT 0 CHECK (is_default IN (0, 1)),
2217
+ PRIMARY KEY (status, substate)
2218
+ );
2219
+
2220
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_task_substates_one_default
2221
+ ON task_substates (status) WHERE is_default = 1;
2222
+
2138
2223
  -- tasks: per-workstream unique on local_id (TRULY local now \u2014
2139
2224
  -- different workstreams may reuse the same local_id).
2140
2225
  CREATE TABLE IF NOT EXISTS tasks (
@@ -2144,6 +2229,9 @@ CREATE TABLE IF NOT EXISTS tasks (
2144
2229
  title TEXT NOT NULL,
2145
2230
  status TEXT NOT NULL DEFAULT 'OPEN',
2146
2231
  -- OPEN | IN_PROGRESS | CLOSED \u2014 see VOCABULARY.md.
2232
+ substate TEXT NOT NULL,
2233
+ -- Qualifies status. No DEFAULT on purpose: a writer that forgets it
2234
+ -- must fail loudly rather than silently pick one.
2147
2235
  impact INTEGER NOT NULL,
2148
2236
  effort_days REAL NOT NULL,
2149
2237
  owner_id INTEGER REFERENCES agents (id) ON DELETE SET NULL,
@@ -2152,7 +2240,8 @@ CREATE TABLE IF NOT EXISTS tasks (
2152
2240
  UNIQUE (workstream_id, local_id),
2153
2241
  CHECK (impact BETWEEN 1 AND 100),
2154
2242
  CHECK (effort_days > 0),
2155
- CHECK (status IN ('OPEN', 'IN_PROGRESS', 'CLOSED'))
2243
+ FOREIGN KEY (status, substate) REFERENCES task_substates (status, substate)
2244
+ DEFERRABLE INITIALLY DEFERRED
2156
2245
  );
2157
2246
 
2158
2247
  CREATE INDEX IF NOT EXISTS idx_tasks_workstream ON tasks (workstream_id);
@@ -2300,6 +2389,9 @@ var GLYPH = {
2300
2389
  /** Task is blocked by an incoming edge. */
2301
2390
  blocked: "\uF0C1",
2302
2391
  // nf-fa-link (chain)
2392
+ /** Task is parked (OPEN/parked): open, but set aside on purpose. */
2393
+ parked: "\uF04C",
2394
+ // nf-fa-pause
2303
2395
  /** Track whose roots merged (diamond dependency). */
2304
2396
  merge: "\uF074",
2305
2397
  // nf-fa-random
@@ -4733,6 +4825,7 @@ var SELECT_TASK_COLS = `
4733
4825
  ws.name AS workstream,
4734
4826
  t.title AS title,
4735
4827
  t.status AS status,
4828
+ t.substate AS substate,
4736
4829
  t.impact AS impact,
4737
4830
  t.effort_days AS effort_days,
4738
4831
  ag.name AS owner,
@@ -4755,6 +4848,7 @@ function rowFromDb3(row) {
4755
4848
  workstreamName: row.workstream,
4756
4849
  title: row.title,
4757
4850
  status: row.status,
4851
+ substate: row.substate,
4758
4852
  impact: row.impact,
4759
4853
  effortDays: row.effort_days,
4760
4854
  ownerName: row.owner,
@@ -4797,6 +4891,10 @@ function getTask(db, localId, workstream) {
4797
4891
  ).get(wsId, localId);
4798
4892
  return row ? rowFromDb3(row) : void 0;
4799
4893
  }
4894
+ function asList(v) {
4895
+ if (v === void 0) return void 0;
4896
+ return typeof v === "string" ? [v] : v;
4897
+ }
4800
4898
  function listTasks(db, workstream, opts = {}) {
4801
4899
  const statuses = opts.status === void 0 ? void 0 : Array.isArray(opts.status) ? opts.status : [opts.status];
4802
4900
  const where = [];
@@ -4811,6 +4909,11 @@ function listTasks(db, workstream, opts = {}) {
4811
4909
  where.push(`t.status IN (${statuses.map(() => "?").join(", ")})`);
4812
4910
  params.push(...statuses);
4813
4911
  }
4912
+ const substates = asList(opts.substate);
4913
+ if (substates !== void 0 && substates.length > 0) {
4914
+ where.push(`t.substate IN (${substates.map(() => "?").join(", ")})`);
4915
+ params.push(...substates);
4916
+ }
4814
4917
  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`;
4815
4918
  const rows = db.prepare(sql).all(...params);
4816
4919
  return rows.map(rowFromDb3);
@@ -4826,6 +4929,7 @@ var SELECT_VIEW_COLS = `
4826
4929
  ws.name AS workstream,
4827
4930
  v.title AS title,
4828
4931
  v.status AS status,
4932
+ v.substate AS substate,
4829
4933
  v.impact AS impact,
4830
4934
  v.effort_days AS effort_days,
4831
4935
  ag.name AS owner,
@@ -4842,6 +4946,11 @@ function listReady(db, workstream, opts = {}) {
4842
4946
  where.push(`v.status IN (${statuses.map(() => "?").join(", ")})`);
4843
4947
  params.push(...statuses);
4844
4948
  }
4949
+ const substates = asList(opts.substate);
4950
+ if (substates !== void 0 && substates.length > 0) {
4951
+ where.push(`v.substate IN (${substates.map(() => "?").join(", ")})`);
4952
+ params.push(...substates);
4953
+ }
4845
4954
  const rows = db.prepare(
4846
4955
  `SELECT ${SELECT_VIEW_COLS} ${VIEW_FROM_JOIN("ready")} WHERE ${where.join(" AND ")} ORDER BY v.local_id`
4847
4956
  ).all(...params);
@@ -5202,6 +5311,84 @@ var CrossWorkstreamEdgeError = class extends Error {
5202
5311
  ];
5203
5312
  }
5204
5313
  };
5314
+ var SubstateReasonRequiredError = class extends Error {
5315
+ constructor(verb, substate, taskId) {
5316
+ super(
5317
+ verb === "close" ? `closing ${taskId} as ${substate} needs a reason (--why "...")` : `parking ${taskId} needs a reason (--why "...")`
5318
+ );
5319
+ this.verb = verb;
5320
+ this.substate = substate;
5321
+ this.taskId = taskId;
5322
+ }
5323
+ verb;
5324
+ substate;
5325
+ taskId;
5326
+ name = "SubstateReasonRequiredError";
5327
+ errorNextSteps() {
5328
+ const cmd = this.verb === "close" ? `mu task close ${this.taskId} --as ${this.substate} --why "<reason>"` : `mu task park ${this.taskId} --why "<reason>"`;
5329
+ return [{ intent: "Retry with a reason", command: cmd }];
5330
+ }
5331
+ };
5332
+ var TaskParkStateError = class extends Error {
5333
+ constructor(taskId, status, workstream) {
5334
+ super(
5335
+ status === "IN_PROGRESS" ? `cannot park ${taskId}: it is IN_PROGRESS (release it first)` : `cannot park ${taskId}: it is CLOSED (open it first)`
5336
+ );
5337
+ this.taskId = taskId;
5338
+ this.status = status;
5339
+ this.workstream = workstream;
5340
+ }
5341
+ taskId;
5342
+ status;
5343
+ workstream;
5344
+ name = "TaskParkStateError";
5345
+ errorNextSteps() {
5346
+ const ws = this.workstream;
5347
+ 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}` };
5348
+ return [
5349
+ first,
5350
+ {
5351
+ intent: "Then park it",
5352
+ command: `mu task park ${this.taskId} --why "<reason>" -w ${ws}`
5353
+ }
5354
+ ];
5355
+ }
5356
+ };
5357
+ var TaskParkedError = class extends Error {
5358
+ constructor(taskId, workstream) {
5359
+ super(`task ${taskId} is parked (OPEN/parked); unpark it or claim with --force`);
5360
+ this.taskId = taskId;
5361
+ this.workstream = workstream;
5362
+ }
5363
+ taskId;
5364
+ workstream;
5365
+ name = "TaskParkedError";
5366
+ errorNextSteps() {
5367
+ const ws = this.workstream;
5368
+ return [
5369
+ { intent: "Unpark it, then claim", command: `mu task unpark ${this.taskId} -w ${ws}` },
5370
+ { intent: "Claim it anyway", command: `mu task claim ${this.taskId} --force -w ${ws}` }
5371
+ ];
5372
+ }
5373
+ };
5374
+ var InvalidSubstateError = class extends Error {
5375
+ constructor(status, substate) {
5376
+ super(
5377
+ `invalid substate for ${status}: ${JSON.stringify(substate)} (expected one of: ${TASK_SUBSTATES[status].join(", ")})`
5378
+ );
5379
+ this.status = status;
5380
+ this.substate = substate;
5381
+ }
5382
+ status;
5383
+ substate;
5384
+ name = "InvalidSubstateError";
5385
+ errorNextSteps() {
5386
+ return TASK_SUBSTATES[this.status].map((s) => ({
5387
+ intent: `Use ${this.status}/${s}`,
5388
+ command: this.status === "CLOSED" ? `mu task close <id> --as ${s}` : `mu task list --substate ${s}`
5389
+ }));
5390
+ }
5391
+ };
5205
5392
 
5206
5393
  // src/tasks/edges.ts
5207
5394
  function dedupeBlockersById(blockers) {
@@ -5239,12 +5426,12 @@ function getTaskEdgesWithStatus(db, taskLocalId, workstream) {
5239
5426
  const taskId = taskIdFor(db, taskLocalId, workstream);
5240
5427
  if (taskId === null) return { blockers: [], dependents: [] };
5241
5428
  const blockers = db.prepare(
5242
- `SELECT t.local_id AS name, t.status AS status FROM task_edges e
5429
+ `SELECT t.local_id AS name, t.status AS status, t.substate AS substate FROM task_edges e
5243
5430
  JOIN tasks t ON t.id = e.from_task_id
5244
5431
  WHERE e.to_task_id = ? ORDER BY t.local_id`
5245
5432
  ).all(taskId);
5246
5433
  const dependents = db.prepare(
5247
- `SELECT t.local_id AS name, t.status AS status FROM task_edges e
5434
+ `SELECT t.local_id AS name, t.status AS status, t.substate AS substate FROM task_edges e
5248
5435
  JOIN tasks t ON t.id = e.to_task_id
5249
5436
  WHERE e.from_task_id = ? ORDER BY t.local_id`
5250
5437
  ).all(taskId);
@@ -5617,8 +5804,8 @@ function addTaskImpl(db, opts) {
5617
5804
  }
5618
5805
  const now = (/* @__PURE__ */ new Date()).toISOString();
5619
5806
  const insertResult = db.prepare(
5620
- `INSERT INTO tasks (workstream_id, local_id, title, status, impact, effort_days, created_at, updated_at)
5621
- VALUES (?, ?, ?, 'OPEN', ?, ?, ?, ?)`
5807
+ `INSERT INTO tasks (workstream_id, local_id, title, status, substate, impact, effort_days, created_at, updated_at)
5808
+ VALUES (?, ?, ?, 'OPEN', 'todo', ?, ?, ?, ?)`
5622
5809
  ).run(wsId, opts.localId, opts.title, opts.impact, opts.effortDays, now, now);
5623
5810
  const newTaskId = Number(insertResult.lastInsertRowid);
5624
5811
  if (opts.blockedBy && opts.blockedBy.length > 0) {
@@ -5667,10 +5854,10 @@ function addNote(db, taskLocalId, content, opts) {
5667
5854
  return withOpContext(
5668
5855
  db,
5669
5856
  { intent: "task.note", actor: opts.author, group: "new" },
5670
- () => addNoteImpl(db, taskLocalId, content, opts)
5857
+ () => insertNote(db, taskLocalId, content, opts)
5671
5858
  );
5672
5859
  }
5673
- function addNoteImpl(db, taskLocalId, content, opts) {
5860
+ function insertNote(db, taskLocalId, content, opts) {
5674
5861
  const task = getTask(db, taskLocalId, opts.workstream);
5675
5862
  if (!task) {
5676
5863
  throw new TaskNotFoundError(taskLocalId);
@@ -5782,19 +5969,32 @@ function setTaskStatus(db, localId, status, opts) {
5782
5969
  );
5783
5970
  }
5784
5971
  function setTaskStatusImpl(db, localId, status, opts) {
5972
+ const substate = opts.substate ?? DEFAULT_SUBSTATE[status];
5973
+ if (!isValidPair(status, substate)) throw new InvalidSubstateError(status, substate);
5785
5974
  const before = getTask(db, localId, opts.workstream);
5786
5975
  if (!before) throw new TaskNotFoundError(localId);
5787
- if (before.status === status) {
5788
- return { previousStatus: before.status, status, changed: false };
5976
+ const base = {
5977
+ previousStatus: before.status,
5978
+ status,
5979
+ previousSubstate: before.substate,
5980
+ substate
5981
+ };
5982
+ if (before.status === status && before.substate === substate) {
5983
+ return { ...base, changed: false };
5789
5984
  }
5790
5985
  db.prepare(
5791
- `UPDATE tasks SET status = ?, updated_at = ?
5986
+ `UPDATE tasks SET status = ?, substate = ?, updated_at = ?
5792
5987
  WHERE local_id = ?
5793
5988
  AND workstream_id = (SELECT id FROM workstreams WHERE name = ?)`
5794
- ).run(status, (/* @__PURE__ */ new Date()).toISOString(), localId, before.workstreamName);
5795
- return { previousStatus: before.status, status, changed: true };
5989
+ ).run(status, substate, (/* @__PURE__ */ new Date()).toISOString(), localId, before.workstreamName);
5990
+ return { ...base, changed: true };
5796
5991
  }
5797
5992
  function closeTask(db, localId, opts) {
5993
+ const as = opts.as ?? "done";
5994
+ if (!isValidPair("CLOSED", as)) throw new InvalidSubstateError("CLOSED", as);
5995
+ if (as !== "done" && (opts.why === void 0 || opts.why.trim() === "")) {
5996
+ throw new SubstateReasonRequiredError("close", as, localId);
5997
+ }
5798
5998
  return withOpContext(
5799
5999
  db,
5800
6000
  { intent: "task.close", actor: opts.author, group: "new" },
@@ -5802,6 +6002,7 @@ function closeTask(db, localId, opts) {
5802
6002
  );
5803
6003
  }
5804
6004
  function closeTaskImpl(db, localId, opts) {
6005
+ const as = opts.as ?? "done";
5805
6006
  const before = getTask(db, localId, opts.workstream);
5806
6007
  if (opts.ifReady && before) {
5807
6008
  const edges = getTaskEdgesWithStatus(db, localId, before.workstreamName);
@@ -5811,14 +6012,43 @@ function closeTaskImpl(db, localId, opts) {
5811
6012
  skipped: "not_ready",
5812
6013
  previousStatus: before.status,
5813
6014
  status: before.status,
6015
+ previousSubstate: before.substate,
6016
+ substate: before.substate,
5814
6017
  changed: false,
5815
6018
  blockingIds: blocking
5816
6019
  };
5817
6020
  }
5818
6021
  }
5819
- const r = setTaskStatus(db, localId, "CLOSED", opts);
5820
- if (r.changed && before) recordEvidenceNote(db, localId, before.workstreamName, "CLOSE", opts);
5821
- return r;
6022
+ return db.transaction(() => {
6023
+ const track = as !== "done" && before !== void 0;
6024
+ const readyBefore = track ? readyDependents(db, localId, before.workstreamName) : [];
6025
+ const r = setTaskStatus(db, localId, "CLOSED", {
6026
+ workstream: opts.workstream,
6027
+ substate: as,
6028
+ ...opts.evidence !== void 0 ? { evidence: opts.evidence } : {}
6029
+ });
6030
+ if (!r.changed || !before) return { ...r, unblocked: [] };
6031
+ recordReasonNote(db, localId, before.workstreamName, as, opts.why, opts.author);
6032
+ recordEvidenceNote(db, localId, before.workstreamName, "CLOSE", opts);
6033
+ const unblocked = track ? readyDependents(db, localId, before.workstreamName).filter((n) => !readyBefore.includes(n)) : [];
6034
+ return { ...r, unblocked };
6035
+ })();
6036
+ }
6037
+ function readyDependents(db, localId, workstream) {
6038
+ const id = taskIdFor(db, localId, workstream);
6039
+ if (id === null) return [];
6040
+ const rows = db.prepare(
6041
+ `SELECT r.local_id AS name FROM task_edges e
6042
+ JOIN ready r ON r.id = e.to_task_id
6043
+ WHERE e.from_task_id = ? ORDER BY r.local_id`
6044
+ ).all(id);
6045
+ return rows.map((r) => r.name);
6046
+ }
6047
+ function recordReasonNote(db, localId, workstream, substate, why, author) {
6048
+ if (why === void 0 || why.trim() === "") return;
6049
+ const noteOpts = { workstream };
6050
+ if (author !== void 0 && author !== "") noteOpts.author = author;
6051
+ insertNote(db, localId, `${substate.toUpperCase()}: ${why}`, noteOpts);
5822
6052
  }
5823
6053
  function openTask(db, localId, opts) {
5824
6054
  return withOpContext(db, { intent: "task.open", group: "new" }, () => {
@@ -5828,6 +6058,51 @@ function openTask(db, localId, opts) {
5828
6058
  return r;
5829
6059
  });
5830
6060
  }
6061
+ function parkTask(db, localId, opts) {
6062
+ if (opts.why.trim() === "") throw new SubstateReasonRequiredError("park", "parked", localId);
6063
+ return withOpContext(
6064
+ db,
6065
+ { intent: "task.park", actor: opts.author, group: "new" },
6066
+ () => db.transaction(() => {
6067
+ const before = getTask(db, localId, opts.workstream);
6068
+ if (!before) throw new TaskNotFoundError(localId);
6069
+ if (before.status !== "OPEN") {
6070
+ throw new TaskParkStateError(localId, before.status, before.workstreamName);
6071
+ }
6072
+ const r = setTaskStatus(db, localId, "OPEN", {
6073
+ workstream: opts.workstream,
6074
+ substate: "parked"
6075
+ });
6076
+ if (r.changed) {
6077
+ recordReasonNote(db, localId, before.workstreamName, "parked", opts.why, opts.author);
6078
+ recordEvidenceNote(db, localId, before.workstreamName, "PARK", opts);
6079
+ }
6080
+ return r;
6081
+ })()
6082
+ );
6083
+ }
6084
+ function unparkTask(db, localId, opts) {
6085
+ return withOpContext(
6086
+ db,
6087
+ { intent: "task.unpark", group: "new" },
6088
+ () => db.transaction(() => {
6089
+ const before = getTask(db, localId, opts.workstream);
6090
+ if (!before) throw new TaskNotFoundError(localId);
6091
+ if (before.status !== "OPEN" || before.substate !== "parked") {
6092
+ return {
6093
+ previousStatus: before.status,
6094
+ status: before.status,
6095
+ previousSubstate: before.substate,
6096
+ substate: before.substate,
6097
+ changed: false
6098
+ };
6099
+ }
6100
+ const r = setTaskStatus(db, localId, "OPEN", { workstream: opts.workstream });
6101
+ recordEvidenceNote(db, localId, before.workstreamName, "UNPARK", opts);
6102
+ return r;
6103
+ })()
6104
+ );
6105
+ }
5831
6106
 
5832
6107
  // src/tasks/claim.ts
5833
6108
  function releaseTask(db, localId, opts) {
@@ -5852,10 +6127,16 @@ function releaseTaskImpl(db, localId, opts) {
5852
6127
  };
5853
6128
  }
5854
6129
  db.prepare(
5855
- `UPDATE tasks SET owner_id = NULL, status = ?, updated_at = ?
6130
+ `UPDATE tasks SET owner_id = NULL, status = ?, substate = ?, updated_at = ?
5856
6131
  WHERE local_id = ?
5857
6132
  AND workstream_id = (SELECT id FROM workstreams WHERE name = ?)`
5858
- ).run(newStatus, (/* @__PURE__ */ new Date()).toISOString(), localId, before.workstreamName);
6133
+ ).run(
6134
+ newStatus,
6135
+ statusChanges ? DEFAULT_SUBSTATE[newStatus] : before.substate,
6136
+ (/* @__PURE__ */ new Date()).toISOString(),
6137
+ localId,
6138
+ before.workstreamName
6139
+ );
5859
6140
  recordEvidenceNote(db, localId, before.workstreamName, "RELEASE", opts);
5860
6141
  return {
5861
6142
  previousOwnerName: before.ownerName,
@@ -5896,11 +6177,15 @@ async function claimTaskImpl(db, localId, opts) {
5896
6177
  () => db.transaction(() => {
5897
6178
  const before = getTask(db, localId, opts.workstream);
5898
6179
  if (!before) throw new TaskNotFoundError(localId);
6180
+ assertNotParked(before, opts);
5899
6181
  const now = (/* @__PURE__ */ new Date()).toISOString();
5900
6182
  const result = db.prepare(
5901
6183
  `UPDATE tasks
5902
6184
  SET owner_id = ?,
5903
6185
  status = CASE WHEN status = 'OPEN' THEN 'IN_PROGRESS' ELSE status END,
6186
+ -- SQLite evaluates every SET expression against the OLD
6187
+ -- row, so this CASE still sees the pre-claim status.
6188
+ substate = CASE WHEN status = 'OPEN' THEN 'active' ELSE substate END,
5904
6189
  updated_at = ?
5905
6190
  WHERE local_id = ?
5906
6191
  AND workstream_id = (SELECT id FROM workstreams WHERE name = ?)
@@ -5922,6 +6207,11 @@ async function claimTaskImpl(db, localId, opts) {
5922
6207
  })()
5923
6208
  );
5924
6209
  }
6210
+ function assertNotParked(task, opts) {
6211
+ if (task.status === "OPEN" && task.substate === "parked" && opts.force !== true) {
6212
+ throw new TaskParkedError(task.name, task.workstreamName);
6213
+ }
6214
+ }
5925
6215
  async function resolveActorIdentity() {
5926
6216
  const worker = await resolveWorkerIdentity();
5927
6217
  if (worker !== void 0) return worker;
@@ -5947,10 +6237,12 @@ async function claimSelf(db, localId, opts) {
5947
6237
  () => db.transaction(() => {
5948
6238
  const before = getTask(db, localId, opts.workstream);
5949
6239
  if (!before) throw new TaskNotFoundError(localId);
6240
+ assertNotParked(before, opts);
5950
6241
  const now = (/* @__PURE__ */ new Date()).toISOString();
5951
6242
  const result = db.prepare(
5952
6243
  `UPDATE tasks
5953
6244
  SET status = CASE WHEN status = 'OPEN' THEN 'IN_PROGRESS' ELSE status END,
6245
+ substate = CASE WHEN status = 'OPEN' THEN 'active' ELSE substate END,
5954
6246
  updated_at = ?
5955
6247
  WHERE local_id = ?
5956
6248
  AND workstream_id = (SELECT id FROM workstreams WHERE name = ?)
@@ -5973,17 +6265,6 @@ async function claimSelf(db, localId, opts) {
5973
6265
  );
5974
6266
  }
5975
6267
 
5976
- // src/tasks/status.ts
5977
- var TASK_STATUSES = ["OPEN", "IN_PROGRESS", "CLOSED"];
5978
- function isTaskStatus(s) {
5979
- return TASK_STATUSES.includes(s);
5980
- }
5981
- var RETIRED_STATUSES = /* @__PURE__ */ new Set(["REJECTED", "DEFERRED"]);
5982
- function normalizeTaskStatus(value) {
5983
- return RETIRED_STATUSES.has(value) ? "OPEN" : value;
5984
- }
5985
- var TASK_STATUS_LIST = TASK_STATUSES.join(" | ");
5986
-
5987
6268
  // src/tasks/wait.ts
5988
6269
  var currentWaitSleep = (ms) => new Promise((resolve6) => setTimeout(resolve6, ms));
5989
6270
  var pollCount = 0;
@@ -6054,6 +6335,7 @@ async function waitForTasks(db, input, opts) {
6054
6335
  for (const ref of refs) {
6055
6336
  const row = getTask(db, ref.name, ref.workstreamName);
6056
6337
  const status = row?.status ?? "OPEN";
6338
+ const substate = row?.substate ?? DEFAULT_SUBSTATE[status];
6057
6339
  const owner = row?.ownerName ?? null;
6058
6340
  const ageMs = await stuckAgeMs(status, owner, ref.workstreamName);
6059
6341
  const stuck = ageMs !== null;
@@ -6082,6 +6364,7 @@ async function waitForTasks(db, input, opts) {
6082
6364
  workstreamName: ref.workstreamName,
6083
6365
  name: ref.name,
6084
6366
  status,
6367
+ substate,
6085
6368
  owner,
6086
6369
  reachedTarget: status === target,
6087
6370
  stuck
@@ -6470,10 +6753,9 @@ function deleteAgent(db, name, workstream) {
6470
6753
  const result = db.prepare("DELETE FROM agents WHERE id = ?").run(agentId);
6471
6754
  if (result.changes === 0) return false;
6472
6755
  for (const t of stuck) {
6473
- db.prepare("UPDATE tasks SET status = 'OPEN', updated_at = ? WHERE id = ?").run(
6474
- (/* @__PURE__ */ new Date()).toISOString(),
6475
- t.taskId
6476
- );
6756
+ db.prepare(
6757
+ "UPDATE tasks SET status = 'OPEN', substate = 'todo', updated_at = ? WHERE id = ?"
6758
+ ).run((/* @__PURE__ */ new Date()).toISOString(), t.taskId);
6477
6759
  addNote(
6478
6760
  db,
6479
6761
  t.localId,
@@ -6704,7 +6986,7 @@ function applyFieldLww(db, op, table, rowId, entries) {
6704
6986
  return applied;
6705
6987
  }
6706
6988
  var APPLIABLE_COLUMNS = {
6707
- tasks: ["title", "status", "impact", "effort_days", "created_at", "updated_at"],
6989
+ tasks: ["title", "status", "substate", "impact", "effort_days", "created_at", "updated_at"],
6708
6990
  workstreams: ["created_at"]
6709
6991
  };
6710
6992
  function filterAppliable(table, entries) {
@@ -6713,9 +6995,20 @@ function filterAppliable(table, entries) {
6713
6995
  }
6714
6996
  function applyTaskPut(db, op) {
6715
6997
  const { workstream, localId } = parseTaskKey(op.key);
6716
- const entries = filterAppliable("tasks", decodePayload(op.payload)).map(
6717
- ([field, value]) => field === "status" && typeof value === "string" ? [field, normalizeTaskStatus(value)] : [field, value]
6718
- );
6998
+ const entries = [];
6999
+ for (const entry of filterAppliable("tasks", decodePayload(op.payload))) {
7000
+ const [field, value] = entry;
7001
+ if (field === "substate") continue;
7002
+ if (field === "status") {
7003
+ if (typeof value !== "string") continue;
7004
+ const legacy = mapLegacyStatus(value);
7005
+ if (legacy) entries.push(["status", legacy.status]);
7006
+ else if (isTaskStatus(value)) entries.push(entry);
7007
+ continue;
7008
+ }
7009
+ entries.push(entry);
7010
+ }
7011
+ const pending = { hlc: op.hlc, substate: carriedSubstate(JSON.parse(op.payload)) };
6719
7012
  const existing = taskRowId(db, op.key);
6720
7013
  if (existing === null) {
6721
7014
  const tomb = tombstoneHlc(db, op);
@@ -6725,18 +7018,60 @@ function applyTaskPut(db, op) {
6725
7018
  const wsId = ensureWorkstreamRow(db, workstream);
6726
7019
  const now = (/* @__PURE__ */ new Date()).toISOString();
6727
7020
  db.prepare(
6728
- `INSERT INTO tasks (workstream_id, local_id, title, status, impact, effort_days,
7021
+ `INSERT INTO tasks (workstream_id, local_id, title, status, substate, impact, effort_days,
6729
7022
  owner_id, created_at, updated_at)
6730
- VALUES (?, ?, ?, 'OPEN', 50, 1, NULL, ?, ?)`
7023
+ VALUES (?, ?, ?, 'OPEN', 'todo', 50, 1, NULL, ?, ?)`
6731
7024
  ).run(wsId, localId, localId, now, now);
6732
7025
  const rowId = taskRowId(db, op.key);
6733
7026
  if (rowId === null) throw new Error(`failed to create task row for ${op.key}`);
6734
7027
  const applied2 = applyFieldLww(db, op, "tasks", rowId, entries);
7028
+ if (repairTaskPair(db, rowId, pending)) applied2.push("substate");
6735
7029
  return { changed: true, appliedFields: applied2 };
6736
7030
  }
6737
7031
  const applied = applyFieldLww(db, op, "tasks", existing, entries);
7032
+ if (repairTaskPair(db, existing, pending)) applied.push("substate");
6738
7033
  return applied.length > 0 ? { changed: true, appliedFields: applied } : { changed: false, appliedFields: [], skipped: "older-than-current" };
6739
7034
  }
7035
+ function carriedSubstate(payload) {
7036
+ if (payload === null || typeof payload !== "object") return null;
7037
+ const { status, substate } = payload;
7038
+ if (typeof status === "string") {
7039
+ const legacy = mapLegacyStatus(status);
7040
+ if (legacy) return legacy.substate;
7041
+ if (typeof substate !== "string" && isTaskStatus(status)) return DEFAULT_SUBSTATE[status];
7042
+ }
7043
+ return typeof substate === "string" ? substate : null;
7044
+ }
7045
+ function repairTaskPair(db, rowId, pending) {
7046
+ const row = db.prepare(
7047
+ `SELECT t.status, t.substate, w.name || '/' || t.local_id AS key
7048
+ FROM tasks t JOIN workstreams w ON w.id = t.workstream_id
7049
+ WHERE t.id = ?`
7050
+ ).get(rowId);
7051
+ if (!row) return false;
7052
+ let winner = null;
7053
+ const writers = db.prepare(
7054
+ `SELECT hlc, payload FROM ops
7055
+ WHERE entity = 'task' AND key = ? AND op = 'put' AND hlc <> ?
7056
+ AND ${LEGACY_LOG_ONLY_SQL_EXCLUSION}
7057
+ AND (json_type(payload, '$.substate') IS NOT NULL
7058
+ OR json_type(payload, '$.status') IS NOT NULL)
7059
+ ORDER BY hlc DESC`
7060
+ ).iterate(row.key, pending?.hlc ?? "");
7061
+ for (const w of writers) {
7062
+ const substate = carriedSubstate(JSON.parse(w.payload));
7063
+ if (substate === null) continue;
7064
+ winner = { hlc: w.hlc, substate };
7065
+ break;
7066
+ }
7067
+ if (pending && pending.substate !== null && wins(pending.hlc, winner?.hlc ?? null)) {
7068
+ winner = { hlc: pending.hlc, substate: pending.substate };
7069
+ }
7070
+ const target = resolvePair(row.status, winner?.substate ?? row.substate);
7071
+ if (target === null || target.substate === row.substate) return false;
7072
+ db.prepare("UPDATE tasks SET substate = ? WHERE id = ?").run(target.substate, rowId);
7073
+ return true;
7074
+ }
6740
7075
  function applyWorkstreamPut(db, op) {
6741
7076
  const name = op.key;
6742
7077
  if (name.length === 0) throw new OpKeyMalformedError("workstream", name);
@@ -6878,7 +7213,7 @@ function applyOp(db, op) {
6878
7213
  case "workstream":
6879
7214
  return applyWorkstreamPut(db, op);
6880
7215
  case "task":
6881
- return applyTaskPut(db, op);
7216
+ return db.transaction(() => applyTaskPut(db, op))();
6882
7217
  case "note":
6883
7218
  return applyNotePut(db, op);
6884
7219
  case "edge":
@@ -6992,7 +7327,7 @@ import pc2 from "picocolors";
6992
7327
  var RECURRENCE_MARKER = ` ${pc2.dim("(\u21BB)")}`;
6993
7328
  function loadFullDag(db, workstream, opts = {}) {
6994
7329
  const tasks = listTasks(db, workstream).filter(
6995
- (t) => opts.statuses === void 0 || opts.statuses.has(t.status)
7330
+ (t) => (opts.statuses === void 0 || opts.statuses.has(t.status)) && (opts.include === void 0 || opts.include(t))
6996
7331
  );
6997
7332
  const byName = new Map(tasks.map((t) => [t.name, t]));
6998
7333
  const incoming = /* @__PURE__ */ new Set();
@@ -7327,7 +7662,7 @@ function checkCheapDriftInvariant(db) {
7327
7662
  }
7328
7663
  var COMPARED = {
7329
7664
  workstreams: ["created_at"],
7330
- tasks: ["title", "status", "impact", "effort_days", "created_at", "updated_at"],
7665
+ tasks: ["title", "status", "substate", "impact", "effort_days", "created_at", "updated_at"],
7331
7666
  task_notes: ["author", "content", "created_at"],
7332
7667
  task_edges: ["created_at"]
7333
7668
  };
@@ -7335,7 +7670,7 @@ var SNAPSHOT_SQL = {
7335
7670
  workstreams: `SELECT w.name AS key, w.created_at
7336
7671
  FROM workstreams w`,
7337
7672
  tasks: `SELECT w.name || '/' || t.local_id AS key,
7338
- t.title, t.status, t.impact, t.effort_days, t.created_at, t.updated_at
7673
+ t.title, t.status, t.substate, t.impact, t.effort_days, t.created_at, t.updated_at
7339
7674
  FROM tasks t JOIN workstreams w ON w.id = t.workstream_id`,
7340
7675
  // Notes are a GROW-ONLY SET whose local surrogate id is not portable,
7341
7676
  // so identity for diffing is (task, author, content) — exactly the
@@ -8055,6 +8390,8 @@ var VERBS = {
8055
8390
  "task.delete": "task delete",
8056
8391
  "task.close": "task close",
8057
8392
  "task.open": "task open",
8393
+ "task.park": "task park",
8394
+ "task.unpark": "task unpark",
8058
8395
  "task.reject": "task reject",
8059
8396
  "task.defer": "task defer",
8060
8397
  "task.claim": "task claim",
@@ -8141,7 +8478,16 @@ function renderKnown(row, intent) {
8141
8478
  case "task.reject":
8142
8479
  case "task.defer": {
8143
8480
  const status = str(bag, "status");
8144
- return { verb: VERBS[intent], subject, detail: status === void 0 ? "" : `\u2192 ${status}` };
8481
+ return {
8482
+ verb: VERBS[intent],
8483
+ subject,
8484
+ detail: status === void 0 ? "" : `\u2192 ${pairText(bag, status)}`
8485
+ };
8486
+ }
8487
+ case "task.park":
8488
+ case "task.unpark": {
8489
+ const detail = str(bag, "substate") === void 0 ? "" : `\u2192 ${pairText(bag, "OPEN")}`;
8490
+ return { verb: VERBS[intent], subject, detail };
8145
8491
  }
8146
8492
  case "task.claim": {
8147
8493
  const by = row.source === "" ? "" : `by ${row.source}`;
@@ -8200,6 +8546,10 @@ function renderKnown(row, intent) {
8200
8546
  }
8201
8547
  }
8202
8548
  }
8549
+ function pairText(bag, status) {
8550
+ const pair = resolvePair(status, str(bag, "substate"));
8551
+ return pair === null ? status : formatPair(pair);
8552
+ }
8203
8553
  function oneLine(text, max = 60) {
8204
8554
  const flat = text.replace(/\s+/g, " ").trim();
8205
8555
  return flat.length <= max ? flat : `${flat.slice(0, max - 1)}\u2026`;
@@ -8746,7 +9096,8 @@ function getParallelTracks(db, workstream) {
8746
9096
  FROM reach r
8747
9097
  JOIN task_edges e ON e.to_task_id = r.node_id
8748
9098
  )
8749
- SELECT goal.local_id AS goal_id, node.local_id AS task_id
9099
+ SELECT goal.local_id AS goal_id, node.local_id AS task_id,
9100
+ node.status AS status, node.substate AS substate
8750
9101
  FROM reach r
8751
9102
  JOIN tasks goal ON goal.id = r.goal_id
8752
9103
  JOIN tasks node ON node.id = r.node_id`
@@ -8754,7 +9105,9 @@ function getParallelTracks(db, workstream) {
8754
9105
  const subgraphs = new Map(goals.map((goal) => [goal.name, /* @__PURE__ */ new Set()]));
8755
9106
  const uf = new UnionFind(goals.map((goal) => goal.name));
8756
9107
  const firstGoalByTask = /* @__PURE__ */ new Map();
9108
+ const openSubstate = /* @__PURE__ */ new Map();
8757
9109
  for (const row of reach) {
9110
+ if (row.status !== "CLOSED") openSubstate.set(row.task_id, row.substate);
8758
9111
  subgraphs.get(row.goal_id)?.add(row.task_id);
8759
9112
  const firstGoal = firstGoalByTask.get(row.task_id);
8760
9113
  if (firstGoal === void 0) firstGoalByTask.set(row.task_id, row.goal_id);
@@ -8781,8 +9134,17 @@ function getParallelTracks(db, workstream) {
8781
9134
  for (const [root, taskIds] of componentTaskIds) {
8782
9135
  const trackRoots = componentRoots.get(root) ?? [];
8783
9136
  let readyCount = 0;
8784
- for (const id of taskIds) if (readyIds.has(id)) readyCount++;
8785
- tracks.push({ roots: trackRoots, taskIds, readyCount });
9137
+ let openCount = 0;
9138
+ let parkedCount = 0;
9139
+ for (const id of taskIds) {
9140
+ if (readyIds.has(id)) readyCount++;
9141
+ const sub = openSubstate.get(id);
9142
+ if (sub === void 0) continue;
9143
+ openCount++;
9144
+ if (sub === "parked") parkedCount++;
9145
+ }
9146
+ const parked = openCount > 0 && parkedCount === openCount;
9147
+ tracks.push({ roots: trackRoots, taskIds, readyCount, parked });
8786
9148
  }
8787
9149
  tracks.sort((a, b) => {
8788
9150
  const an = a.roots[0]?.name ?? "";
@@ -8844,6 +9206,7 @@ async function loadWorkstreamSnapshotFast(db, workstream, opts = {}) {
8844
9206
  inProgress: listInProgress(db, workstream),
8845
9207
  blocked: listBlocked(db, workstream),
8846
9208
  recentClosed: listRecentClosed(db, workstream),
9209
+ parkedCount: listTasks(db, workstream, { status: "OPEN", substate: "parked" }).length,
8847
9210
  allTasks: opts.withAllTasks === true ? listTasks(db, workstream) : [],
8848
9211
  workspaces: listWorkspaces2(db, workstream),
8849
9212
  workspaceOrphans: listWorkspaceOrphans(db, workstream),
@@ -8916,6 +9279,7 @@ function minimalSnapshot(workstream) {
8916
9279
  inProgress: [],
8917
9280
  blocked: [],
8918
9281
  recentClosed: [],
9282
+ parkedCount: 0,
8919
9283
  allTasks: [],
8920
9284
  workspaces: [],
8921
9285
  workspaceOrphans: [],
@@ -9374,13 +9738,13 @@ function planUndo(db, groupId) {
9374
9738
  if (Object.keys(fields3).length === 0) fields3 = payloadFields(row.payload);
9375
9739
  if (Object.keys(fields3).length === 0 && row.entity === "note") {
9376
9740
  const recovered = recoverLegacyNoteContents(db, row.key, row.hlc, seenLegacyNoteTasks);
9377
- for (const legacy of recovered) {
9741
+ for (const legacy2 of recovered) {
9378
9742
  inverses.push({
9379
9743
  entity: "note",
9380
- key: legacy.key,
9744
+ key: legacy2.key,
9381
9745
  op: "put",
9382
- fields: legacy.fields,
9383
- summary: `note ${legacy.key} (recovered from legacy history)`,
9746
+ fields: legacy2.fields,
9747
+ summary: `note ${legacy2.key} (recovered from legacy history)`,
9384
9748
  supersededBy: []
9385
9749
  });
9386
9750
  }
@@ -9388,7 +9752,11 @@ function planUndo(db, groupId) {
9388
9752
  continue;
9389
9753
  }
9390
9754
  const status = fields3.status;
9391
- if (typeof status === "string") fields3.status = normalizeTaskStatus(status);
9755
+ const legacy = typeof status === "string" ? mapLegacyStatus(status) : null;
9756
+ if (legacy) {
9757
+ fields3.status = legacy.status;
9758
+ fields3.substate = legacy.substate;
9759
+ }
9392
9760
  const conflicts = laterRowWriters(db, row.entity, row.key, row.hlc, groupId).map((w) => ({
9393
9761
  field: "<row>",
9394
9762
  groupId: w.groupId,
@@ -9608,15 +9976,23 @@ function restoreRow(db, inverse, table) {
9608
9976
  if (id === null) {
9609
9977
  const wsId = ensureWorkstream2(db, parsed.workstream);
9610
9978
  const now = (/* @__PURE__ */ new Date()).toISOString();
9979
+ const pair = resolvePair(
9980
+ String(inverse.fields.status ?? "OPEN"),
9981
+ inverse.fields.substate
9982
+ ) ?? {
9983
+ status: "OPEN",
9984
+ substate: "todo"
9985
+ };
9611
9986
  db.prepare(
9612
- `INSERT INTO tasks (workstream_id, local_id, title, status, impact, effort_days,
9613
- owner_id, created_at, updated_at)
9614
- VALUES (?, ?, ?, ?, ?, ?, NULL, ?, ?)`
9987
+ `INSERT INTO tasks (workstream_id, local_id, title, status, substate, impact,
9988
+ effort_days, owner_id, created_at, updated_at)
9989
+ VALUES (?, ?, ?, ?, ?, ?, ?, NULL, ?, ?)`
9615
9990
  ).run(
9616
9991
  wsId,
9617
9992
  parsed.localId,
9618
9993
  String(inverse.fields.title ?? parsed.localId),
9619
- String(inverse.fields.status ?? "OPEN"),
9994
+ pair.status,
9995
+ pair.substate,
9620
9996
  Number(inverse.fields.impact ?? 50),
9621
9997
  Number(inverse.fields.effort_days ?? 1),
9622
9998
  String(inverse.fields.created_at ?? now),
@@ -9624,7 +10000,8 @@ function restoreRow(db, inverse, table) {
9624
10000
  );
9625
10001
  return true;
9626
10002
  }
9627
- return updateFields(db, "tasks", "id", id, inverse.fields);
10003
+ const updated = updateFields(db, "tasks", "id", id, inverse.fields);
10004
+ return repairTaskPair(db, id) || updated;
9628
10005
  }
9629
10006
  case "note": {
9630
10007
  const parsed = parseNoteKey2(inverse.key);
@@ -9726,6 +10103,7 @@ export {
9726
10103
  ClaimerNotRegisteredError,
9727
10104
  CrossWorkstreamEdgeError,
9728
10105
  CycleError,
10106
+ DEFAULT_SUBSTATE,
9729
10107
  DRIFT_REPORT_CAP,
9730
10108
  DriftDetectedError,
9731
10109
  EXPECTED_TABLES,
@@ -9733,6 +10111,7 @@ export {
9733
10111
  HlcOverflowError,
9734
10112
  HlcParseError,
9735
10113
  HomeDirAsProjectRootError,
10114
+ InvalidSubstateError,
9736
10115
  KNOWN_INTENTS,
9737
10116
  KNOWN_VERBS,
9738
10117
  MACHINE_LOCAL_ENTITIES,
@@ -9755,15 +10134,20 @@ export {
9755
10134
  SEGMENT_FORMAT_VERSION,
9756
10135
  SYNCED_ENTITIES,
9757
10136
  SchemaTooOldError,
10137
+ SubstateReasonRequiredError,
9758
10138
  SyncPeerNotFoundError,
9759
10139
  SyncPeerRefAmbiguousError,
9760
10140
  SyncSourceNotFoundError,
9761
10141
  TASK_STATUSES,
9762
10142
  TASK_STATUS_LIST,
10143
+ TASK_SUBSTATES,
10144
+ TASK_SUBSTATE_ROWS,
9763
10145
  TaskAlreadyOwnedError,
9764
10146
  TaskExistsError,
9765
10147
  TaskNotFoundError,
9766
10148
  TaskNotInWorkstreamError,
10149
+ TaskParkStateError,
10150
+ TaskParkedError,
9767
10151
  TmuxError,
9768
10152
  UNKNOWN_REASON,
9769
10153
  UndoGroupNotFoundError,
@@ -9833,6 +10217,7 @@ export {
9833
10217
  foregroundPgid,
9834
10218
  formatDriftRecord,
9835
10219
  formatHlc,
10220
+ formatPair,
9836
10221
  freeWorkspace,
9837
10222
  getAgent,
9838
10223
  getAgentByPane,
@@ -9857,6 +10242,7 @@ export {
9857
10242
  isPathInside,
9858
10243
  isTaskStatus,
9859
10244
  isValidAgentName,
10245
+ isValidPair,
9860
10246
  isValidPaneId,
9861
10247
  isValidTaskId,
9862
10248
  isValidWorkstreamName,
@@ -9894,6 +10280,7 @@ export {
9894
10280
  loadWorkstreamSnapshotSlow,
9895
10281
  localMachineId,
9896
10282
  locksDir,
10283
+ mapLegacyStatus,
9897
10284
  mergeSnapshotFastSlow,
9898
10285
  mostRecentGroup,
9899
10286
  murmurAvailable,
@@ -9909,6 +10296,7 @@ export {
9909
10296
  openTask,
9910
10297
  paneExists,
9911
10298
  paneTTY,
10299
+ parkTask,
9912
10300
  parseAgentNameFromTitle,
9913
10301
  parseHlc,
9914
10302
  parseOpKey,
@@ -9945,6 +10333,7 @@ export {
9945
10333
  resolveCliCommand,
9946
10334
  resolveCliCommandWithSource,
9947
10335
  resolveGroupId,
10336
+ resolvePair,
9948
10337
  resolvePeerRef,
9949
10338
  roiBucket,
9950
10339
  segmentLineCount,
@@ -9979,6 +10368,7 @@ export {
9979
10368
  tmuxBackend,
9980
10369
  transportNextSteps,
9981
10370
  undoGroup,
10371
+ unparkTask,
9982
10372
  updateTask,
9983
10373
  verifyAgainstManifest,
9984
10374
  waitForTasks,