@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/AGENTS.md +6 -5
- package/README.md +25 -7
- package/dist/cli.js +791 -161
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +170 -17
- package/dist/index.js +449 -59
- package/dist/index.js.map +1 -1
- package/docs/ARCHITECTURE.md +36 -6
- package/docs/USAGE_GUIDE.md +40 -17
- package/docs/VOCABULARY.md +11 -5
- package/docs/img/tui-dashboard.png +0 -0
- package/docs/specs/2026-09-29-task-substates.md +247 -0
- package/package.json +1 -1
- package/skills/mu/SKILL.md +10 -4
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
|
|
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 =
|
|
2013
|
-
var MIN_ACCEPTED_SCHEMA_VERSION =
|
|
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
|
-
|
|
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
|
-
() =>
|
|
5857
|
+
() => insertNote(db, taskLocalId, content, opts)
|
|
5671
5858
|
);
|
|
5672
5859
|
}
|
|
5673
|
-
function
|
|
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
|
-
|
|
5788
|
-
|
|
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 {
|
|
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
|
-
|
|
5820
|
-
|
|
5821
|
-
|
|
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(
|
|
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(
|
|
6474
|
-
|
|
6475
|
-
|
|
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 =
|
|
6717
|
-
|
|
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 {
|
|
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
|
-
|
|
8785
|
-
|
|
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
|
|
9741
|
+
for (const legacy2 of recovered) {
|
|
9378
9742
|
inverses.push({
|
|
9379
9743
|
entity: "note",
|
|
9380
|
-
key:
|
|
9744
|
+
key: legacy2.key,
|
|
9381
9745
|
op: "put",
|
|
9382
|
-
fields:
|
|
9383
|
-
summary: `note ${
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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,
|