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