@mu-crew/mu 2.0.0 → 3.0.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 +790 -161
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +169 -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 +39 -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", "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: "wontfix" },
|
|
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 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,11 @@ 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
|
+
{ id: "notDone", key: "W", rest: "ontfix+", color: "red" }
|
|
13982
|
+
];
|
|
13535
13983
|
}
|
|
13536
13984
|
});
|
|
13537
13985
|
|
|
@@ -13558,6 +14006,8 @@ function AllTasksPopup({
|
|
|
13558
14006
|
const [sortKey, setSortKey] = useState7("roi");
|
|
13559
14007
|
const flt = usePopupFilter({ onEditingChange: onFilterEditingChange });
|
|
13560
14008
|
const statusFilter = useStatusFilter();
|
|
14009
|
+
const { statuses, showParked, showNotDone } = statusFilter;
|
|
14010
|
+
const filterStrip = /* @__PURE__ */ jsx23(StatusFilterStrip, { statuses, showParked, showNotDone });
|
|
13561
14011
|
const sourceTasks = useMemo5(
|
|
13562
14012
|
() => allTasksFromSnapshotOrDb(snapshot2, db, workstream),
|
|
13563
14013
|
[snapshot2, db, workstream]
|
|
@@ -13568,15 +14018,26 @@ function AllTasksPopup({
|
|
|
13568
14018
|
);
|
|
13569
14019
|
const [blockedFilter, setBlockedFilter] = useState7("all");
|
|
13570
14020
|
const visibleTasks = useMemo5(() => {
|
|
13571
|
-
const filteredByStatus = sourceTasks.filter(
|
|
14021
|
+
const filteredByStatus = sourceTasks.filter(
|
|
14022
|
+
(t) => passesFilter(t, { statuses, showParked, showNotDone })
|
|
14023
|
+
);
|
|
13572
14024
|
const filteredByBlocked = applyBlockedFilter(filteredByStatus, blockedNames, blockedFilter);
|
|
13573
14025
|
const filteredByText = applyFilter(
|
|
13574
14026
|
filteredByBlocked,
|
|
13575
14027
|
flt.query,
|
|
13576
|
-
(t) => `${t.name} ${t.title} ${t
|
|
14028
|
+
(t) => `${t.name} ${t.title} ${formatPair(t)} ${t.ownerName ?? ""}`
|
|
13577
14029
|
);
|
|
13578
14030
|
return sortTasks(filteredByText, sortKey);
|
|
13579
|
-
}, [
|
|
14031
|
+
}, [
|
|
14032
|
+
sourceTasks,
|
|
14033
|
+
statuses,
|
|
14034
|
+
showParked,
|
|
14035
|
+
showNotDone,
|
|
14036
|
+
blockedFilter,
|
|
14037
|
+
blockedNames,
|
|
14038
|
+
flt.query,
|
|
14039
|
+
sortKey
|
|
14040
|
+
]);
|
|
13580
14041
|
const safeCursor = visibleTasks.length === 0 ? 0 : Math.min(cursor, visibleTasks.length - 1);
|
|
13581
14042
|
const focused = visibleTasks[safeCursor];
|
|
13582
14043
|
const [drilledTask, setDrilledTask] = useState7(null);
|
|
@@ -13646,7 +14107,7 @@ function AllTasksPopup({
|
|
|
13646
14107
|
}
|
|
13647
14108
|
if (sourceTasks.length === 0) {
|
|
13648
14109
|
return /* @__PURE__ */ jsx23(PopupShell, { title: "All tasks \xB7 popup", children: /* @__PURE__ */ jsxs12(Box9, { flexDirection: "column", flexGrow: 1, children: [
|
|
13649
|
-
|
|
14110
|
+
filterStrip,
|
|
13650
14111
|
/* @__PURE__ */ jsx23(BlockedFilterStrip, { mode: blockedFilter }),
|
|
13651
14112
|
/* @__PURE__ */ jsx23(SortStrip, { sortKey, visible: 0, total: 0 }),
|
|
13652
14113
|
/* @__PURE__ */ jsx23(Text16, { dimColor: true, children: "(no tasks)" })
|
|
@@ -13655,7 +14116,7 @@ function AllTasksPopup({
|
|
|
13655
14116
|
if (visibleTasks.length === 0) {
|
|
13656
14117
|
return /* @__PURE__ */ jsxs12(PopupShell, { title: "All tasks \xB7 popup", children: [
|
|
13657
14118
|
/* @__PURE__ */ jsxs12(Box9, { flexDirection: "column", flexGrow: 1, children: [
|
|
13658
|
-
|
|
14119
|
+
filterStrip,
|
|
13659
14120
|
/* @__PURE__ */ jsx23(BlockedFilterStrip, { mode: blockedFilter }),
|
|
13660
14121
|
/* @__PURE__ */ jsx23(SortStrip, { sortKey, visible: 0, total: sourceTasks.length }),
|
|
13661
14122
|
/* @__PURE__ */ jsx23(Text16, { dimColor: true, children: flt.query === "" ? "(no tasks match status filter)" : `(no matches for "${flt.query}")` })
|
|
@@ -13682,7 +14143,7 @@ function AllTasksPopup({
|
|
|
13682
14143
|
const agentLookup = agentByName(snapshot2);
|
|
13683
14144
|
const rows = windowed.map((t) => [
|
|
13684
14145
|
t.name,
|
|
13685
|
-
blockedNames.has(t.name)
|
|
14146
|
+
statusCell(t, blockedNames.has(t.name)),
|
|
13686
14147
|
formatAgentRefDisplayName(t.ownerName, agentLookup),
|
|
13687
14148
|
formatRoi(t.impact, t.effortDays),
|
|
13688
14149
|
t.title
|
|
@@ -13695,7 +14156,7 @@ function AllTasksPopup({
|
|
|
13695
14156
|
hint: focused ? allTasksYankCommand(focused.name, workstream) : void 0,
|
|
13696
14157
|
children: [
|
|
13697
14158
|
/* @__PURE__ */ jsxs12(Box9, { flexDirection: "column", flexGrow: 1, children: [
|
|
13698
|
-
|
|
14159
|
+
filterStrip,
|
|
13699
14160
|
/* @__PURE__ */ jsx23(BlockedFilterStrip, { mode: blockedFilter }),
|
|
13700
14161
|
/* @__PURE__ */ jsx23(SortStrip, { sortKey, visible: visibleTasks.length, total: sourceTasks.length }),
|
|
13701
14162
|
windowed.map((t, i) => {
|
|
@@ -13706,7 +14167,7 @@ function AllTasksPopup({
|
|
|
13706
14167
|
const colors = [
|
|
13707
14168
|
{ bold: true },
|
|
13708
14169
|
// id
|
|
13709
|
-
{ color: isBlocked ? "yellow" :
|
|
14170
|
+
{ color: isBlocked ? "yellow" : inkColorForPair(t) },
|
|
13710
14171
|
// status + blocked glyph
|
|
13711
14172
|
{ dimColor: true },
|
|
13712
14173
|
// owner
|
|
@@ -13732,6 +14193,11 @@ function AllTasksPopup({
|
|
|
13732
14193
|
}
|
|
13733
14194
|
);
|
|
13734
14195
|
}
|
|
14196
|
+
function statusCell(t, blocked) {
|
|
14197
|
+
const pair = formatPair(t);
|
|
14198
|
+
const parked = t.status === "OPEN" && t.substate === "parked" ? `${GLYPH.parked} ` : "";
|
|
14199
|
+
return `${parked}${pair}${blocked ? ` ${GLYPH.blocked}` : ""}`;
|
|
14200
|
+
}
|
|
13735
14201
|
function allTasksFromSnapshotOrDb(snapshot2, db, workstream) {
|
|
13736
14202
|
if (snapshot2?.allTasks && snapshot2.allTasks.length > 0) return snapshot2.allTasks;
|
|
13737
14203
|
return listTasks(db, workstream);
|
|
@@ -13806,6 +14272,7 @@ var init_all_tasks = __esm({
|
|
|
13806
14272
|
"use strict";
|
|
13807
14273
|
init_glyphs();
|
|
13808
14274
|
init_sort();
|
|
14275
|
+
init_status();
|
|
13809
14276
|
init_tasks();
|
|
13810
14277
|
init_format();
|
|
13811
14278
|
init_agent_display();
|
|
@@ -14013,10 +14480,7 @@ function BlockedPopup(props) {
|
|
|
14013
14480
|
const m = /* @__PURE__ */ new Map();
|
|
14014
14481
|
for (const t of sourceTasks) {
|
|
14015
14482
|
const gating = stillGating(getTaskEdgesWithStatus(db, t.name, workstream).blockers);
|
|
14016
|
-
m.set(
|
|
14017
|
-
t.name,
|
|
14018
|
-
gating.map((b) => b.name)
|
|
14019
|
-
);
|
|
14483
|
+
m.set(t.name, gating.map(blockerLabel));
|
|
14020
14484
|
}
|
|
14021
14485
|
return m;
|
|
14022
14486
|
}, [sourceTasks, db, workstream]);
|
|
@@ -14041,7 +14505,7 @@ function BlockedPopup(props) {
|
|
|
14041
14505
|
cells: [
|
|
14042
14506
|
GLYPH2,
|
|
14043
14507
|
t.name,
|
|
14044
|
-
t
|
|
14508
|
+
formatPair(t),
|
|
14045
14509
|
String(blockers.length),
|
|
14046
14510
|
top,
|
|
14047
14511
|
formatRoi(t.impact, t.effortDays),
|
|
@@ -14052,7 +14516,7 @@ function BlockedPopup(props) {
|
|
|
14052
14516
|
// glyph
|
|
14053
14517
|
{ bold: true },
|
|
14054
14518
|
// name
|
|
14055
|
-
{ color:
|
|
14519
|
+
{ color: inkColorForPair(t) },
|
|
14056
14520
|
// status
|
|
14057
14521
|
{ color: "yellow" },
|
|
14058
14522
|
// nblock
|
|
@@ -14075,6 +14539,7 @@ var init_blocked2 = __esm({
|
|
|
14075
14539
|
"src/cli/tui/popups/blocked.tsx"() {
|
|
14076
14540
|
"use strict";
|
|
14077
14541
|
init_state();
|
|
14542
|
+
init_status();
|
|
14078
14543
|
init_tasks();
|
|
14079
14544
|
init_format();
|
|
14080
14545
|
init_blocked();
|
|
@@ -14473,12 +14938,13 @@ function DagPopup({
|
|
|
14473
14938
|
}) {
|
|
14474
14939
|
const viewport = usePopupViewport();
|
|
14475
14940
|
const statusFilter = useStatusFilter();
|
|
14941
|
+
const { statuses, showParked, showNotDone } = statusFilter;
|
|
14476
14942
|
const { cols } = useTerminalSize();
|
|
14477
14943
|
const contentWidth = contentWidthFromCols(cols);
|
|
14478
14944
|
const { body, roots } = useMemo7(() => {
|
|
14479
14945
|
void fastTickNonce;
|
|
14480
|
-
return buildDagBody(db, workstream,
|
|
14481
|
-
}, [db, workstream,
|
|
14946
|
+
return buildDagBody(db, workstream, { statuses, showParked, showNotDone }, contentWidth);
|
|
14947
|
+
}, [db, workstream, statuses, showParked, showNotDone, contentWidth, fastTickNonce]);
|
|
14482
14948
|
const [focusedRoot, setFocusedRoot] = useState10(() => roots[0] ?? null);
|
|
14483
14949
|
const lineToRootRef = useRef5([]);
|
|
14484
14950
|
const rootsRef = useRef5([]);
|
|
@@ -14507,12 +14973,19 @@ function DagPopup({
|
|
|
14507
14973
|
});
|
|
14508
14974
|
if (roots.length === 0) {
|
|
14509
14975
|
return /* @__PURE__ */ jsx27(PopupShell, { title: `DAG \xB7 ${workstream}`, children: /* @__PURE__ */ jsxs15(Box12, { flexDirection: "column", flexGrow: 1, children: [
|
|
14510
|
-
/* @__PURE__ */ jsx27(
|
|
14976
|
+
/* @__PURE__ */ jsx27(
|
|
14977
|
+
StatusFilterStrip,
|
|
14978
|
+
{
|
|
14979
|
+
statuses,
|
|
14980
|
+
showParked,
|
|
14981
|
+
showNotDone
|
|
14982
|
+
}
|
|
14983
|
+
),
|
|
14511
14984
|
/* @__PURE__ */ jsx27(Text19, { dimColor: true, children: "(no tasks)" })
|
|
14512
14985
|
] }) });
|
|
14513
14986
|
}
|
|
14514
14987
|
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
|
|
14988
|
+
/* @__PURE__ */ jsx27(StatusFilterStrip, { statuses, showParked, showNotDone }),
|
|
14516
14989
|
/* @__PURE__ */ jsx27(
|
|
14517
14990
|
DrillScrollView,
|
|
14518
14991
|
{
|
|
@@ -14530,9 +15003,10 @@ function DagPopup({
|
|
|
14530
15003
|
function dagYankCommand(taskId, workstream) {
|
|
14531
15004
|
return `mu task tree ${taskId} -w ${workstream}`;
|
|
14532
15005
|
}
|
|
14533
|
-
function buildDagBody(db, workstream,
|
|
14534
|
-
const
|
|
14535
|
-
const
|
|
15006
|
+
function buildDagBody(db, workstream, filter, contentWidth = contentWidthFromCols(80)) {
|
|
15007
|
+
const f = "statuses" in filter ? filter : { statuses: filter, showParked: true, showNotDone: true };
|
|
15008
|
+
const dag = loadFullDag(db, workstream, { include: (t) => passesFilter(t, f) });
|
|
15009
|
+
const body = renderForest(dag.roots, dag.edges, (task) => colorPair(task), dag.tasks, {
|
|
14536
15010
|
includeTitle: false
|
|
14537
15011
|
});
|
|
14538
15012
|
return {
|
|
@@ -14768,6 +15242,7 @@ var COLUMN_SPECS16, config;
|
|
|
14768
15242
|
var init_inprogress2 = __esm({
|
|
14769
15243
|
"src/cli/tui/popups/inprogress.tsx"() {
|
|
14770
15244
|
"use strict";
|
|
15245
|
+
init_status();
|
|
14771
15246
|
init_format();
|
|
14772
15247
|
init_agent_display();
|
|
14773
15248
|
init_inprogress();
|
|
@@ -14808,7 +15283,7 @@ var init_inprogress2 = __esm({
|
|
|
14808
15283
|
cells: [
|
|
14809
15284
|
GLYPH3,
|
|
14810
15285
|
t.name,
|
|
14811
|
-
t
|
|
15286
|
+
formatPair(t),
|
|
14812
15287
|
formatAgentRefDisplayName(t.ownerName, agentLookup),
|
|
14813
15288
|
formatSinceClaim(age),
|
|
14814
15289
|
formatRoi(t.impact, t.effortDays),
|
|
@@ -14819,7 +15294,7 @@ var init_inprogress2 = __esm({
|
|
|
14819
15294
|
// glyph
|
|
14820
15295
|
{ bold: true },
|
|
14821
15296
|
// id
|
|
14822
|
-
{ color:
|
|
15297
|
+
{ color: inkColorForPair(t) },
|
|
14823
15298
|
// status
|
|
14824
15299
|
{ dimColor: true },
|
|
14825
15300
|
// owner
|
|
@@ -15032,6 +15507,7 @@ function ReadyPopup(props) {
|
|
|
15032
15507
|
function yankCommandForTask2(t, ws) {
|
|
15033
15508
|
switch (t.status) {
|
|
15034
15509
|
case "OPEN":
|
|
15510
|
+
if (t.substate === "parked") return `mu task unpark ${t.name} -w ${ws}`;
|
|
15035
15511
|
return t.ownerName === null ? `mu task claim ${t.name} -w ${ws}` : `mu task release ${t.name} -w ${ws}`;
|
|
15036
15512
|
case "IN_PROGRESS":
|
|
15037
15513
|
return `mu task close ${t.name} -w ${ws} --evidence "..."`;
|
|
@@ -15045,6 +15521,7 @@ var COLUMN_SPECS18, config2;
|
|
|
15045
15521
|
var init_ready2 = __esm({
|
|
15046
15522
|
"src/cli/tui/popups/ready.tsx"() {
|
|
15047
15523
|
"use strict";
|
|
15524
|
+
init_status();
|
|
15048
15525
|
init_format();
|
|
15049
15526
|
init_agent_display();
|
|
15050
15527
|
init_task_list_popup();
|
|
@@ -15062,18 +15539,18 @@ var init_ready2 = __esm({
|
|
|
15062
15539
|
label: "Tasks",
|
|
15063
15540
|
sourceTasks: (snapshot2) => [...snapshot2.ready, ...snapshot2.inProgress],
|
|
15064
15541
|
columnSpecs: COLUMN_SPECS18,
|
|
15065
|
-
filterBlob: (t) => `${t.name} ${t.title} ${t
|
|
15542
|
+
filterBlob: (t) => `${t.name} ${t.title} ${formatPair(t)} ${t.ownerName ?? ""}`,
|
|
15066
15543
|
emptyText: "(no open / in-progress tasks)",
|
|
15067
15544
|
yankCommand: (t, ws) => yankCommandForTask2(t, ws),
|
|
15068
15545
|
dynamicHint: (focused, ws) => yankCommandForTask2(focused, ws),
|
|
15069
15546
|
renderRows: (visible, _start, snapshot2) => {
|
|
15070
15547
|
const agentLookup = agentByName(snapshot2);
|
|
15071
15548
|
return visible.map((t) => ({
|
|
15072
|
-
cells: [t.name, t
|
|
15549
|
+
cells: [t.name, formatPair(t), formatAgentRefDisplayName(t.ownerName, agentLookup), t.title],
|
|
15073
15550
|
colors: [
|
|
15074
15551
|
{ bold: true },
|
|
15075
15552
|
// name
|
|
15076
|
-
{ color:
|
|
15553
|
+
{ color: inkColorForPair(t) },
|
|
15077
15554
|
// status
|
|
15078
15555
|
{ dimColor: true },
|
|
15079
15556
|
// owner
|
|
@@ -15098,6 +15575,7 @@ var COLUMN_SPECS19, config3;
|
|
|
15098
15575
|
var init_recent2 = __esm({
|
|
15099
15576
|
"src/cli/tui/popups/recent.tsx"() {
|
|
15100
15577
|
"use strict";
|
|
15578
|
+
init_status();
|
|
15101
15579
|
init_format();
|
|
15102
15580
|
init_recent();
|
|
15103
15581
|
init_format_helpers();
|
|
@@ -15135,7 +15613,7 @@ var init_recent2 = __esm({
|
|
|
15135
15613
|
cells: [
|
|
15136
15614
|
GLYPH4,
|
|
15137
15615
|
t.name,
|
|
15138
|
-
t
|
|
15616
|
+
formatPair(t),
|
|
15139
15617
|
formatWhen(ageMs(t, now)),
|
|
15140
15618
|
String(t.impact),
|
|
15141
15619
|
String(t.effortDays),
|
|
@@ -15147,7 +15625,7 @@ var init_recent2 = __esm({
|
|
|
15147
15625
|
// glyph
|
|
15148
15626
|
{ bold: true },
|
|
15149
15627
|
// id
|
|
15150
|
-
{ color:
|
|
15628
|
+
{ color: inkColorForPair(t) },
|
|
15151
15629
|
// status
|
|
15152
15630
|
{ dimColor: true },
|
|
15153
15631
|
// when
|
|
@@ -15170,6 +15648,16 @@ var init_recent2 = __esm({
|
|
|
15170
15648
|
import { Box as Box15, Text as Text22, useInput as useInput9 } from "ink";
|
|
15171
15649
|
import { useEffect as useEffect8, useMemo as useMemo8, useState as useState13 } from "react";
|
|
15172
15650
|
import { jsx as jsx33, jsxs as jsxs18 } from "react/jsx-runtime";
|
|
15651
|
+
function drillColors(t) {
|
|
15652
|
+
return [
|
|
15653
|
+
{ bold: true },
|
|
15654
|
+
// name
|
|
15655
|
+
{ color: inkColorForPair(t) },
|
|
15656
|
+
// status
|
|
15657
|
+
void 0
|
|
15658
|
+
// title
|
|
15659
|
+
];
|
|
15660
|
+
}
|
|
15173
15661
|
function TracksPopup({
|
|
15174
15662
|
yank: yank2,
|
|
15175
15663
|
onClose,
|
|
@@ -15204,7 +15692,7 @@ function TracksPopup({
|
|
|
15204
15692
|
const drillTasks = useMemo8(() => {
|
|
15205
15693
|
if (mode !== "drill" || !focusedTrack) return [];
|
|
15206
15694
|
const out = listTasks(db, workstream).filter((task) => focusedTrack.taskIds.has(task.name));
|
|
15207
|
-
out.sort((a, b) => statusRank(a
|
|
15695
|
+
out.sort((a, b) => statusRank(a) - statusRank(b) || a.name.localeCompare(b.name));
|
|
15208
15696
|
return out;
|
|
15209
15697
|
}, [mode, focusedTrack, db, workstream]);
|
|
15210
15698
|
const focusedTask = drillTasks[drillCursor];
|
|
@@ -15330,7 +15818,7 @@ function TracksPopup({
|
|
|
15330
15818
|
return /* @__PURE__ */ jsx33(PopupShell, { title: `${trackLabel} \xB7 ${goalSummary}`, children: /* @__PURE__ */ jsx33(Text22, { dimColor: true, children: "(no tasks resolved)" }) });
|
|
15331
15819
|
}
|
|
15332
15820
|
const { visible: visible2 } = centredVisibleSlice(drillTasks, drillCursor, viewport);
|
|
15333
|
-
const rows2 = visible2.map((t) => [t.name, t
|
|
15821
|
+
const rows2 = visible2.map((t) => [t.name, formatPair(t), t.title]);
|
|
15334
15822
|
const widths2 = layoutColumns(rows2, DRILL_COLUMN_SPECS, contentWidth);
|
|
15335
15823
|
return /* @__PURE__ */ jsxs18(
|
|
15336
15824
|
PopupShell,
|
|
@@ -15347,7 +15835,7 @@ function TracksPopup({
|
|
|
15347
15835
|
{
|
|
15348
15836
|
cells: padded,
|
|
15349
15837
|
contentWidth,
|
|
15350
|
-
colors:
|
|
15838
|
+
colors: drillColors(t),
|
|
15351
15839
|
selected: sel
|
|
15352
15840
|
},
|
|
15353
15841
|
t.name
|
|
@@ -15363,7 +15851,7 @@ function TracksPopup({
|
|
|
15363
15851
|
const absoluteIndex = start + i;
|
|
15364
15852
|
const goalNames = t.roots.map((r) => r.name).join(", ");
|
|
15365
15853
|
const diamond = t.roots.length > 1 ? GLYPH.merge : " ";
|
|
15366
|
-
const counts = `(${t.taskIds.size} tasks \xB7 ${t.readyCount} ready)`;
|
|
15854
|
+
const counts = `(${t.taskIds.size} tasks \xB7 ${t.readyCount} ready)${t.parked ? " (parked)" : ""}`;
|
|
15367
15855
|
return [`Track ${absoluteIndex + 1}`, diamond, goalNames, counts];
|
|
15368
15856
|
});
|
|
15369
15857
|
const widths = layoutColumns(rows, COLUMN_SPECS20, contentWidth);
|
|
@@ -15394,24 +15882,24 @@ function TracksPopup({
|
|
|
15394
15882
|
}
|
|
15395
15883
|
);
|
|
15396
15884
|
}
|
|
15397
|
-
function statusRank(
|
|
15398
|
-
switch (status) {
|
|
15885
|
+
function statusRank(pair) {
|
|
15886
|
+
switch (pair.status) {
|
|
15399
15887
|
case "IN_PROGRESS":
|
|
15400
15888
|
return 0;
|
|
15401
15889
|
case "OPEN":
|
|
15402
|
-
return 1;
|
|
15890
|
+
return pair.substate === "parked" ? 2 : 1;
|
|
15403
15891
|
case "CLOSED":
|
|
15404
|
-
return 2;
|
|
15405
|
-
default:
|
|
15406
15892
|
return 3;
|
|
15407
15893
|
}
|
|
15408
15894
|
}
|
|
15409
|
-
var COLUMN_SPECS20, DRILL_COLUMN_SPECS, TRACK_COLORS
|
|
15895
|
+
var COLUMN_SPECS20, DRILL_COLUMN_SPECS, TRACK_COLORS;
|
|
15410
15896
|
var init_tracks3 = __esm({
|
|
15411
15897
|
"src/cli/tui/popups/tracks.tsx"() {
|
|
15412
15898
|
"use strict";
|
|
15413
15899
|
init_glyphs();
|
|
15900
|
+
init_status();
|
|
15414
15901
|
init_tasks();
|
|
15902
|
+
init_format();
|
|
15415
15903
|
init_columns();
|
|
15416
15904
|
init_keys();
|
|
15417
15905
|
init_list_row();
|
|
@@ -15451,14 +15939,6 @@ var init_tracks3 = __esm({
|
|
|
15451
15939
|
{ dimColor: true }
|
|
15452
15940
|
// counts
|
|
15453
15941
|
];
|
|
15454
|
-
DRILL_COLORS = [
|
|
15455
|
-
{ bold: true },
|
|
15456
|
-
// name
|
|
15457
|
-
{ dimColor: true },
|
|
15458
|
-
// status
|
|
15459
|
-
void 0
|
|
15460
|
-
// title
|
|
15461
|
-
];
|
|
15462
15942
|
}
|
|
15463
15943
|
});
|
|
15464
15944
|
|
|
@@ -16078,6 +16558,7 @@ function snapshotKey(s) {
|
|
|
16078
16558
|
tracks: s.tracks.map((t) => ({
|
|
16079
16559
|
roots: t.roots.map((r) => r.name),
|
|
16080
16560
|
readyCount: t.readyCount,
|
|
16561
|
+
parked: t.parked,
|
|
16081
16562
|
// Set → sorted array; deterministic JSON.
|
|
16082
16563
|
taskIds: [...t.taskIds].sort()
|
|
16083
16564
|
})),
|
|
@@ -16085,6 +16566,7 @@ function snapshotKey(s) {
|
|
|
16085
16566
|
inProgress: s.inProgress.map(taskKey2),
|
|
16086
16567
|
blocked: s.blocked.map(taskKey2),
|
|
16087
16568
|
recentClosed: s.recentClosed.map(taskKey2),
|
|
16569
|
+
parkedCount: s.parkedCount,
|
|
16088
16570
|
allTasks: s.allTasks.map(taskKey2),
|
|
16089
16571
|
commitsBackend: s.commitsBackend ?? null,
|
|
16090
16572
|
recentCommits: s.recentCommits.map((c) => [c.sha, c.subject, c.author, c.relTime]),
|
|
@@ -16107,6 +16589,7 @@ function taskKey2(t) {
|
|
|
16107
16589
|
return [
|
|
16108
16590
|
t.name,
|
|
16109
16591
|
t.status,
|
|
16592
|
+
t.substate,
|
|
16110
16593
|
t.impact,
|
|
16111
16594
|
t.effortDays,
|
|
16112
16595
|
t.ownerName ?? "",
|
|
@@ -17093,7 +17576,7 @@ function partitionEdges(edges) {
|
|
|
17093
17576
|
function formatEdgeList(edges, dim) {
|
|
17094
17577
|
if (edges.length === 0) return pc.dim("\u2014");
|
|
17095
17578
|
const parts = edges.map((e) => {
|
|
17096
|
-
const piece = `${e.name} [${
|
|
17579
|
+
const piece = `${e.name} [${colorPair(e)}]`;
|
|
17097
17580
|
return dim ? pc.dim(piece) : piece;
|
|
17098
17581
|
});
|
|
17099
17582
|
return parts.join(", ");
|
|
@@ -17231,7 +17714,7 @@ async function cmdTaskShow(db, rawId, opts = {}) {
|
|
|
17231
17714
|
const roi = task.effortDays > 0 ? (task.impact / task.effortDays).toFixed(1) : "\u221E";
|
|
17232
17715
|
console.log(pc.bold(`${task.name} \u2014 ${task.title}`));
|
|
17233
17716
|
console.log(` workstream : ${task.workstreamName}`);
|
|
17234
|
-
console.log(` status : ${task
|
|
17717
|
+
console.log(` status : ${colorPair(task)}`);
|
|
17235
17718
|
const ownerLine = task.ownerName !== null ? task.ownerName : lastActor !== null ? pc.dim(`(self: ${lastActor})`) : pc.dim("(unowned)");
|
|
17236
17719
|
console.log(` owner : ${ownerLine}`);
|
|
17237
17720
|
console.log(` impact : ${task.impact}`);
|
|
@@ -17261,6 +17744,14 @@ async function cmdTaskShow(db, rawId, opts = {}) {
|
|
|
17261
17744
|
} else {
|
|
17262
17745
|
for (const n of notes) printNote(n);
|
|
17263
17746
|
}
|
|
17747
|
+
const ref = `${task.name} -w ${task.workstreamName}`;
|
|
17748
|
+
if (task.substate === "parked") {
|
|
17749
|
+
console.log("");
|
|
17750
|
+
printNextSteps([{ intent: "Return it to next", command: `mu task unpark ${ref}` }]);
|
|
17751
|
+
} else if (task.status === "CLOSED" && task.substate !== "done") {
|
|
17752
|
+
console.log("");
|
|
17753
|
+
printNextSteps([{ intent: "Reopen it", command: `mu task open ${ref}` }]);
|
|
17754
|
+
}
|
|
17264
17755
|
}
|
|
17265
17756
|
async function cmdTaskNotes(db, rawId, opts = {}) {
|
|
17266
17757
|
if (opts.since !== void 0 && opts.sinceClaim === true) {
|
|
@@ -17368,6 +17859,8 @@ async function cmdTaskList(db, opts) {
|
|
|
17368
17859
|
if (statuses !== void 0) {
|
|
17369
17860
|
listOpts.status = statuses;
|
|
17370
17861
|
}
|
|
17862
|
+
const substates = parseSubstatesOption(opts.substate);
|
|
17863
|
+
if (substates !== void 0) listOpts.substate = substates;
|
|
17371
17864
|
const sortKey = opts.sort === void 0 ? "id" : parseSortOption(opts.sort);
|
|
17372
17865
|
const tasks = sortTasks(listTasks(db, workstream, listOpts), sortKey);
|
|
17373
17866
|
if (opts.json) {
|
|
@@ -17389,6 +17882,8 @@ async function cmdTaskNext(db, opts) {
|
|
|
17389
17882
|
if (statuses !== void 0) {
|
|
17390
17883
|
readyOpts.status = statuses;
|
|
17391
17884
|
}
|
|
17885
|
+
const substates = parseSubstatesOption(opts.substate);
|
|
17886
|
+
if (substates !== void 0) readyOpts.substate = substates;
|
|
17392
17887
|
const sorted = sortTasks(listReady(db, workstream, readyOpts), sortKey);
|
|
17393
17888
|
const tasks = k === 0 ? sorted : sorted.slice(0, k);
|
|
17394
17889
|
if (opts.json) {
|
|
@@ -17396,7 +17891,18 @@ async function cmdTaskNext(db, opts) {
|
|
|
17396
17891
|
return;
|
|
17397
17892
|
}
|
|
17398
17893
|
if (tasks.length === 0) {
|
|
17399
|
-
|
|
17894
|
+
const parked = listTasks(db, workstream, { substate: "parked" }).length;
|
|
17895
|
+
if (parked === 0) {
|
|
17896
|
+
console.log(pc.dim("(no ready tasks)"));
|
|
17897
|
+
return;
|
|
17898
|
+
}
|
|
17899
|
+
console.log(pc.dim(`(no ready tasks; ${parked} parked)`));
|
|
17900
|
+
printNextSteps([
|
|
17901
|
+
{
|
|
17902
|
+
intent: "List parked tasks",
|
|
17903
|
+
command: `mu task list --substate parked -w ${workstream}`
|
|
17904
|
+
}
|
|
17905
|
+
]);
|
|
17400
17906
|
return;
|
|
17401
17907
|
}
|
|
17402
17908
|
const tableOpts = {};
|
|
@@ -17419,6 +17925,7 @@ async function cmdTaskOwnedBy(db, agent, opts = {}) {
|
|
|
17419
17925
|
}
|
|
17420
17926
|
|
|
17421
17927
|
// src/cli/tasks/wire.ts
|
|
17928
|
+
init_status();
|
|
17422
17929
|
init_tasks();
|
|
17423
17930
|
|
|
17424
17931
|
// src/cli/tasks/claim.ts
|
|
@@ -17502,6 +18009,7 @@ async function cmdClaim(db, rawId, opts) {
|
|
|
17502
18009
|
}
|
|
17503
18010
|
const sdkOpts = { workstream: ws };
|
|
17504
18011
|
if (forName !== void 0) sdkOpts.agentName = forName;
|
|
18012
|
+
if (opts.force) sdkOpts.force = true;
|
|
17505
18013
|
if (forWorkstream !== void 0) sdkOpts.agentWorkstream = forWorkstream;
|
|
17506
18014
|
if (opts.self) sdkOpts.self = true;
|
|
17507
18015
|
if (opts.actor !== void 0) sdkOpts.actor = opts.actor;
|
|
@@ -17567,6 +18075,9 @@ async function cmdClaim(db, rawId, opts) {
|
|
|
17567
18075
|
function qualifiedId(ref) {
|
|
17568
18076
|
return `${ref.workstreamName}/${ref.name}`;
|
|
17569
18077
|
}
|
|
18078
|
+
function isUndelivered(ref) {
|
|
18079
|
+
return ref.status === "CLOSED" && ref.substate !== "done";
|
|
18080
|
+
}
|
|
17570
18081
|
function cherryPickCommandForCommits(commits) {
|
|
17571
18082
|
const first = commits[0];
|
|
17572
18083
|
if (first === void 0) return null;
|
|
@@ -17680,7 +18191,12 @@ async function cmdTaskWait(db, ids, opts) {
|
|
|
17680
18191
|
const reachedRefs = result.refs.filter((t) => t.reachedTarget);
|
|
17681
18192
|
const unmetRefs = result.refs.filter((t) => !t.reachedTarget);
|
|
17682
18193
|
const nextSteps3 = [];
|
|
17683
|
-
if (!result.timedOut && firingRef !== null) {
|
|
18194
|
+
if (!result.timedOut && firingRef !== null && isUndelivered(firingRef)) {
|
|
18195
|
+
nextSteps3.push({
|
|
18196
|
+
intent: `Read why ${firingRef.name} closed ${formatPair(firingRef)} (no deliverable)`,
|
|
18197
|
+
command: `mu task notes ${firingRef.name} -w ${firingRef.workstreamName}`
|
|
18198
|
+
});
|
|
18199
|
+
} else if (!result.timedOut && firingRef !== null) {
|
|
17684
18200
|
const owner = firingRef.owner;
|
|
17685
18201
|
if (owner !== null) {
|
|
17686
18202
|
nextSteps3.push(await nextStepForFiringOwner(db, owner, firingRef.workstreamName));
|
|
@@ -17728,6 +18244,7 @@ async function cmdTaskWait(db, ids, opts) {
|
|
|
17728
18244
|
name: firingRef.name,
|
|
17729
18245
|
qualifiedId: qualifiedId(firingRef),
|
|
17730
18246
|
status: firingRef.status,
|
|
18247
|
+
substate: firingRef.substate,
|
|
17731
18248
|
owner: firingRef.owner
|
|
17732
18249
|
};
|
|
17733
18250
|
const timedOutArray = result.timedOut ? unmetRefs.map((t) => ({ ...t, qualifiedId: qualifiedId(t) })) : [];
|
|
@@ -17755,7 +18272,7 @@ async function cmdTaskWait(db, ids, opts) {
|
|
|
17755
18272
|
for (const t of result.refs) {
|
|
17756
18273
|
const marker = t.reachedTarget ? pc.green(GLYPH.ok) : pc.dim("\u2022");
|
|
17757
18274
|
const label2 = workstreamSet.size > 1 ? qualifiedId(t) : t.name;
|
|
17758
|
-
console.log(` ${marker} ${pc.bold(label2)} ${pc.dim(`(${t
|
|
18275
|
+
console.log(` ${marker} ${pc.bold(label2)} ${pc.dim(`(${formatPair(t)})`)}`);
|
|
17759
18276
|
}
|
|
17760
18277
|
printNextSteps(nextSteps3);
|
|
17761
18278
|
if (result.timedOut) throw new CliExitError(5);
|
|
@@ -17932,6 +18449,7 @@ async function cmdTaskDelete(db, rawId, opts = {}) {
|
|
|
17932
18449
|
// src/cli/tasks/lifecycle.ts
|
|
17933
18450
|
init_agents();
|
|
17934
18451
|
init_output();
|
|
18452
|
+
init_status();
|
|
17935
18453
|
init_tasks();
|
|
17936
18454
|
init_vcs2();
|
|
17937
18455
|
init_workspace();
|
|
@@ -17943,6 +18461,9 @@ async function cmdTaskClose(db, rawId, opts = {}) {
|
|
|
17943
18461
|
const sdkOpts = { workstream: ws };
|
|
17944
18462
|
if (opts.evidence !== void 0) sdkOpts.evidence = opts.evidence;
|
|
17945
18463
|
if (opts.ifReady) sdkOpts.ifReady = true;
|
|
18464
|
+
if (opts.as !== void 0) sdkOpts.as = opts.as.toLowerCase();
|
|
18465
|
+
if (opts.why !== void 0) sdkOpts.why = opts.why;
|
|
18466
|
+
if (opts.why !== void 0 && opts.why !== "") sdkOpts.author = actor;
|
|
17946
18467
|
if (opts.evidence !== void 0 && opts.evidence !== "") {
|
|
17947
18468
|
sdkOpts.author = actor;
|
|
17948
18469
|
}
|
|
@@ -17976,9 +18497,16 @@ async function cmdTaskClose(db, rawId, opts = {}) {
|
|
|
17976
18497
|
return;
|
|
17977
18498
|
}
|
|
17978
18499
|
if (r.changed && taskRow?.ownerName) await refreshAgentTitle(db, taskRow.ownerName, ws);
|
|
18500
|
+
const pickNext = {
|
|
18501
|
+
intent: r.unblocked.length > 0 ? "Pick up an unblocked task" : "Pick the next ready task",
|
|
18502
|
+
command: `mu task next -w ${ws}`
|
|
18503
|
+
};
|
|
18504
|
+
const reopen = {
|
|
18505
|
+
intent: "Reopen if needed",
|
|
18506
|
+
command: `mu task open ${localId} -w ${ws}`
|
|
18507
|
+
};
|
|
17979
18508
|
const nextSteps3 = [
|
|
17980
|
-
|
|
17981
|
-
{ intent: "Pick the next ready task", command: `mu task next -w ${ws}` },
|
|
18509
|
+
...r.unblocked.length > 0 ? [pickNext, reopen] : [reopen, pickNext],
|
|
17982
18510
|
{ intent: "See full state", command: `mu state -w ${ws}` }
|
|
17983
18511
|
];
|
|
17984
18512
|
if (r.changed && r.status === "CLOSED") {
|
|
@@ -17994,10 +18522,15 @@ async function cmdTaskClose(db, rawId, opts = {}) {
|
|
|
17994
18522
|
return;
|
|
17995
18523
|
}
|
|
17996
18524
|
const ev = opts.evidence ? pc.dim(` evidence: ${opts.evidence}`) : "";
|
|
17997
|
-
console.log(`Closed ${pc.bold(localId)} ${pc.dim(`(${r
|
|
18525
|
+
console.log(`Closed ${pc.bold(localId)} ${pc.dim(`(${transition(r)})`)}`);
|
|
17998
18526
|
if (ev) console.log(ev);
|
|
18527
|
+
if (r.unblocked.length > 0) console.log(`Unblocked: ${r.unblocked.join(", ")}`);
|
|
17999
18528
|
printNextSteps(nextSteps3);
|
|
18000
18529
|
}
|
|
18530
|
+
function transition(r) {
|
|
18531
|
+
const from = formatPair({ status: r.previousStatus, substate: r.previousSubstate });
|
|
18532
|
+
return `${from} \u2192 ${formatPair({ status: r.status, substate: r.substate })}`;
|
|
18533
|
+
}
|
|
18001
18534
|
async function maybeAppendDirtyWorkspaceCommitHint(db, nextSteps3, actor, workstream, taskTitle) {
|
|
18002
18535
|
if (actor.length === 0) return;
|
|
18003
18536
|
try {
|
|
@@ -18040,10 +18573,62 @@ async function cmdTaskOpen(db, rawId, opts = {}) {
|
|
|
18040
18573
|
return;
|
|
18041
18574
|
}
|
|
18042
18575
|
const ev = opts.evidence ? pc.dim(` evidence: ${opts.evidence}`) : "";
|
|
18043
|
-
console.log(`Reopened ${pc.bold(localId)} ${pc.dim(`(${r
|
|
18576
|
+
console.log(`Reopened ${pc.bold(localId)} ${pc.dim(`(${transition(r)})`)}`);
|
|
18044
18577
|
if (ev) console.log(ev);
|
|
18045
18578
|
printNextSteps(nextSteps3);
|
|
18046
18579
|
}
|
|
18580
|
+
async function cmdTaskPark(db, rawId, opts) {
|
|
18581
|
+
const { name: localId } = await resolveEntityRef(db, rawId, opts, "task");
|
|
18582
|
+
assertTaskInWorkstream(db, localId, opts.workstream);
|
|
18583
|
+
const ws = await resolveWorkstream(opts.workstream);
|
|
18584
|
+
const sdkOpts = {
|
|
18585
|
+
workstream: ws,
|
|
18586
|
+
why: opts.why,
|
|
18587
|
+
author: await resolveActorIdentity()
|
|
18588
|
+
};
|
|
18589
|
+
if (opts.evidence !== void 0) sdkOpts.evidence = opts.evidence;
|
|
18590
|
+
const r = parkTask(db, localId, sdkOpts);
|
|
18591
|
+
const nextSteps3 = [
|
|
18592
|
+
{ intent: "Unpark it later", command: `mu task unpark ${localId} -w ${ws}` },
|
|
18593
|
+
{ intent: "List parked tasks", command: `mu task list --substate parked -w ${ws}` }
|
|
18594
|
+
];
|
|
18595
|
+
if (opts.json) {
|
|
18596
|
+
emitJson({ taskName: localId, ...r, nextSteps: nextSteps3 });
|
|
18597
|
+
return;
|
|
18598
|
+
}
|
|
18599
|
+
if (!r.changed) {
|
|
18600
|
+
console.log(pc.dim(`${localId} already OPEN/parked (no-op)`));
|
|
18601
|
+
} else {
|
|
18602
|
+
console.log(`Parked ${pc.bold(localId)} ${pc.dim(`(${transition(r)})`)}`);
|
|
18603
|
+
}
|
|
18604
|
+
printNextSteps(nextSteps3);
|
|
18605
|
+
}
|
|
18606
|
+
async function cmdTaskUnpark(db, rawId, opts = {}) {
|
|
18607
|
+
const { name: localId } = await resolveEntityRef(db, rawId, opts, "task");
|
|
18608
|
+
assertTaskInWorkstream(db, localId, opts.workstream);
|
|
18609
|
+
const ws = await resolveWorkstream(opts.workstream);
|
|
18610
|
+
const sdkOpts = { workstream: ws };
|
|
18611
|
+
if (opts.evidence !== void 0) sdkOpts.evidence = opts.evidence;
|
|
18612
|
+
const r = unparkTask(db, localId, sdkOpts);
|
|
18613
|
+
const nextSteps3 = [
|
|
18614
|
+
{
|
|
18615
|
+
intent: "Claim it",
|
|
18616
|
+
command: `mu task claim ${localId} -w ${ws} (--self / --for <worker>)`
|
|
18617
|
+
},
|
|
18618
|
+
{ intent: "Pick the next ready task", command: `mu task next -w ${ws}` }
|
|
18619
|
+
];
|
|
18620
|
+
if (opts.json) {
|
|
18621
|
+
emitJson({ taskName: localId, ...r, nextSteps: nextSteps3 });
|
|
18622
|
+
return;
|
|
18623
|
+
}
|
|
18624
|
+
if (!r.changed) {
|
|
18625
|
+
const pair = formatPair({ status: r.status, substate: r.substate });
|
|
18626
|
+
console.log(pc.dim(`${localId} is ${pair}, not parked (no-op)`));
|
|
18627
|
+
} else {
|
|
18628
|
+
console.log(`Unparked ${pc.bold(localId)} ${pc.dim(`(${transition(r)})`)}`);
|
|
18629
|
+
}
|
|
18630
|
+
printNextSteps(nextSteps3);
|
|
18631
|
+
}
|
|
18047
18632
|
|
|
18048
18633
|
// src/cli/tasks/tree.ts
|
|
18049
18634
|
init_dag();
|
|
@@ -18068,13 +18653,7 @@ async function cmdTaskTree(db, rawId, opts) {
|
|
|
18068
18653
|
const swapHint = down ? "swap to --no-down for blockers" : "--down for dependents";
|
|
18069
18654
|
console.log(pc.bold(`Tree of ${rootId} ${pc.dim(`(${direction} below; ${swapHint})`)}`));
|
|
18070
18655
|
console.log(
|
|
18071
|
-
renderTaskTree(
|
|
18072
|
-
db,
|
|
18073
|
-
ws,
|
|
18074
|
-
root,
|
|
18075
|
-
down ? "dependents" : "blockers",
|
|
18076
|
-
(task) => colorStatus(task.status)
|
|
18077
|
-
)
|
|
18656
|
+
renderTaskTree(db, ws, root, down ? "dependents" : "blockers", (task) => colorPair(task))
|
|
18078
18657
|
);
|
|
18079
18658
|
}
|
|
18080
18659
|
function buildJsonTree(db, workstream, taskId, down, seen) {
|
|
@@ -18107,10 +18686,11 @@ function wireTaskCommands(program) {
|
|
|
18107
18686
|
return handle((db) => cmdTaskAdd(db, id, opts), this)();
|
|
18108
18687
|
});
|
|
18109
18688
|
const SORT_OPT_DESC = `sort key (${TASK_SORT_KEYS.join(" | ")})`;
|
|
18689
|
+
const SUBSTATE_OPT_DESC = `filter by substate (${[...new Set(Object.values(TASK_SUBSTATES).flat())].join(" | ")}; repeat or comma-separate)`;
|
|
18110
18690
|
task.command("list").description("List every task in the current workstream (id, status, ROI, owner)").option(...WORKSTREAM_OPT).option(
|
|
18111
18691
|
"--status <status...>",
|
|
18112
18692
|
`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() {
|
|
18693
|
+
).option("--substate <substate...>", SUBSTATE_OPT_DESC).option("--sort <key>", `${SORT_OPT_DESC} (default id)`).option(...JSON_OPT).action(function() {
|
|
18114
18694
|
const opts = this.opts();
|
|
18115
18695
|
return handle((db) => cmdTaskList(db, opts), this)();
|
|
18116
18696
|
});
|
|
@@ -18123,7 +18703,7 @@ function wireTaskCommands(program) {
|
|
|
18123
18703
|
).option(...WORKSTREAM_OPT).option("--sort <key>", `${SORT_OPT_DESC} (default roi)`).option(
|
|
18124
18704
|
"--status <status...>",
|
|
18125
18705
|
`filter by lifecycle status (${TASK_STATUS_LIST}; case-insensitive; repeat or comma-separate; or both)`
|
|
18126
|
-
).option(...JSON_OPT).action(function() {
|
|
18706
|
+
).option("--substate <substate...>", SUBSTATE_OPT_DESC).option(...JSON_OPT).action(function() {
|
|
18127
18707
|
const opts = this.opts();
|
|
18128
18708
|
return handle((db) => cmdTaskNext(db, opts), this)();
|
|
18129
18709
|
});
|
|
@@ -18185,8 +18765,11 @@ function wireTaskCommands(program) {
|
|
|
18185
18765
|
"record what the caller observed (e.g. 'tests pass: npm test exit 0'); appears verbatim in the event log"
|
|
18186
18766
|
];
|
|
18187
18767
|
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."
|
|
18768
|
+
"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
18769
|
).option(
|
|
18770
|
+
"--as <substate>",
|
|
18771
|
+
`closing substate (${TASK_SUBSTATES.CLOSED.join(" | ")}; default done)`
|
|
18772
|
+
).option("--why <text>", "reason, stored as a note; required unless --as done").option(
|
|
18190
18773
|
"--if-ready",
|
|
18191
18774
|
"only close when every blocker is CLOSED; otherwise no-op + list the still-blocking ids"
|
|
18192
18775
|
).option(...WORKSTREAM_OPT).option(...EVIDENCE_OPT).option(...JSON_OPT).action(function(id) {
|
|
@@ -18197,6 +18780,16 @@ function wireTaskCommands(program) {
|
|
|
18197
18780
|
const opts = this.opts();
|
|
18198
18781
|
return handle((db) => cmdTaskOpen(db, id, opts), this)();
|
|
18199
18782
|
});
|
|
18783
|
+
task.command("park <id>").description(
|
|
18784
|
+
"Park an OPEN task (OPEN/parked): it leaves `next` and `claim` refuses it without --force. Dependents stay blocked. Idempotent."
|
|
18785
|
+
).requiredOption("--why <text>", "why it is parked; stored as a note").option(...WORKSTREAM_OPT).option(...EVIDENCE_OPT).option(...JSON_OPT).action(function(id) {
|
|
18786
|
+
const opts = this.opts();
|
|
18787
|
+
return handle((db) => cmdTaskPark(db, id, opts), this)();
|
|
18788
|
+
});
|
|
18789
|
+
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) {
|
|
18790
|
+
const opts = this.opts();
|
|
18791
|
+
return handle((db) => cmdTaskUnpark(db, id, opts), this)();
|
|
18792
|
+
});
|
|
18200
18793
|
task.command("release <id>").description(
|
|
18201
18794
|
"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
18795
|
).option(
|
|
@@ -18217,7 +18810,7 @@ function wireTaskCommands(program) {
|
|
|
18217
18810
|
).option(
|
|
18218
18811
|
"--actor <name>",
|
|
18219
18812
|
"override the actor name used for the log (only valid with --self; defaults to pane title or $USER)"
|
|
18220
|
-
).option(
|
|
18813
|
+
).option("--force", "claim even when the task is parked (OPEN/parked)").option(
|
|
18221
18814
|
"--strict-staleness",
|
|
18222
18815
|
"refuse --for dispatch when the target agent's workspace is stale (default: warn and proceed)"
|
|
18223
18816
|
).option(...WORKSTREAM_OPT).option(...EVIDENCE_OPT).option(...JSON_OPT).action(function(taskId) {
|
|
@@ -18859,6 +19452,7 @@ init_tasks();
|
|
|
18859
19452
|
import { CommanderError } from "commander";
|
|
18860
19453
|
|
|
18861
19454
|
// src/undo.ts
|
|
19455
|
+
init_apply();
|
|
18862
19456
|
init_db();
|
|
18863
19457
|
init_legacy_ops();
|
|
18864
19458
|
init_logs();
|
|
@@ -19037,13 +19631,13 @@ function planUndo(db, groupId) {
|
|
|
19037
19631
|
if (Object.keys(fields3).length === 0) fields3 = payloadFields(row2.payload);
|
|
19038
19632
|
if (Object.keys(fields3).length === 0 && row2.entity === "note") {
|
|
19039
19633
|
const recovered = recoverLegacyNoteContents(db, row2.key, row2.hlc, seenLegacyNoteTasks);
|
|
19040
|
-
for (const
|
|
19634
|
+
for (const legacy2 of recovered) {
|
|
19041
19635
|
inverses.push({
|
|
19042
19636
|
entity: "note",
|
|
19043
|
-
key:
|
|
19637
|
+
key: legacy2.key,
|
|
19044
19638
|
op: "put",
|
|
19045
|
-
fields:
|
|
19046
|
-
summary: `note ${
|
|
19639
|
+
fields: legacy2.fields,
|
|
19640
|
+
summary: `note ${legacy2.key} (recovered from legacy history)`,
|
|
19047
19641
|
supersededBy: []
|
|
19048
19642
|
});
|
|
19049
19643
|
}
|
|
@@ -19051,7 +19645,11 @@ function planUndo(db, groupId) {
|
|
|
19051
19645
|
continue;
|
|
19052
19646
|
}
|
|
19053
19647
|
const status = fields3.status;
|
|
19054
|
-
|
|
19648
|
+
const legacy = typeof status === "string" ? mapLegacyStatus(status) : null;
|
|
19649
|
+
if (legacy) {
|
|
19650
|
+
fields3.status = legacy.status;
|
|
19651
|
+
fields3.substate = legacy.substate;
|
|
19652
|
+
}
|
|
19055
19653
|
const conflicts = laterRowWriters(db, row2.entity, row2.key, row2.hlc, groupId).map((w) => ({
|
|
19056
19654
|
field: "<row>",
|
|
19057
19655
|
groupId: w.groupId,
|
|
@@ -19271,15 +19869,23 @@ function restoreRow(db, inverse, table) {
|
|
|
19271
19869
|
if (id === null) {
|
|
19272
19870
|
const wsId = ensureWorkstream2(db, parsed.workstream);
|
|
19273
19871
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
19872
|
+
const pair = resolvePair(
|
|
19873
|
+
String(inverse.fields.status ?? "OPEN"),
|
|
19874
|
+
inverse.fields.substate
|
|
19875
|
+
) ?? {
|
|
19876
|
+
status: "OPEN",
|
|
19877
|
+
substate: "todo"
|
|
19878
|
+
};
|
|
19274
19879
|
db.prepare(
|
|
19275
|
-
`INSERT INTO tasks (workstream_id, local_id, title, status,
|
|
19276
|
-
owner_id, created_at, updated_at)
|
|
19277
|
-
VALUES (?, ?, ?, ?, ?, ?, NULL, ?, ?)`
|
|
19880
|
+
`INSERT INTO tasks (workstream_id, local_id, title, status, substate, impact,
|
|
19881
|
+
effort_days, owner_id, created_at, updated_at)
|
|
19882
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, NULL, ?, ?)`
|
|
19278
19883
|
).run(
|
|
19279
19884
|
wsId,
|
|
19280
19885
|
parsed.localId,
|
|
19281
19886
|
String(inverse.fields.title ?? parsed.localId),
|
|
19282
|
-
|
|
19887
|
+
pair.status,
|
|
19888
|
+
pair.substate,
|
|
19283
19889
|
Number(inverse.fields.impact ?? 50),
|
|
19284
19890
|
Number(inverse.fields.effort_days ?? 1),
|
|
19285
19891
|
String(inverse.fields.created_at ?? now),
|
|
@@ -19287,7 +19893,8 @@ function restoreRow(db, inverse, table) {
|
|
|
19287
19893
|
);
|
|
19288
19894
|
return true;
|
|
19289
19895
|
}
|
|
19290
|
-
|
|
19896
|
+
const updated = updateFields(db, "tasks", "id", id, inverse.fields);
|
|
19897
|
+
return repairTaskPair(db, id) || updated;
|
|
19291
19898
|
}
|
|
19292
19899
|
case "note": {
|
|
19293
19900
|
const parsed = parseNoteKey2(inverse.key);
|
|
@@ -19443,7 +20050,9 @@ function classifyError(err) {
|
|
|
19443
20050
|
if (err instanceof AgentNotFoundError || err instanceof TaskNotFoundError || err instanceof WorkstreamNotFoundError || err instanceof WorkspaceNotFoundError) {
|
|
19444
20051
|
return { label: "not found", exitCode: 3 };
|
|
19445
20052
|
}
|
|
19446
|
-
if (err instanceof NameAmbiguousError || err instanceof AgentExistsError || err instanceof TaskExistsError || err instanceof TaskAlreadyOwnedError ||
|
|
20053
|
+
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
|
|
20054
|
+
// phase, a claim on a parked task, a pair outside TASK_SUBSTATES.
|
|
20055
|
+
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
20056
|
// the source DB: both are name/state collisions, not usage errors.
|
|
19448
20057
|
err instanceof RebuildTargetExistsError || err instanceof RebuildTargetIsSourceError || // An abbreviated group id matching several groups: the operator must
|
|
19449
20058
|
// disambiguate. Guessing would be catastrophic for `mu undo`.
|
|
@@ -22053,6 +22662,7 @@ function wireWorkstreamCommands(program) {
|
|
|
22053
22662
|
// src/cli.ts
|
|
22054
22663
|
init_db();
|
|
22055
22664
|
init_sort();
|
|
22665
|
+
init_status();
|
|
22056
22666
|
init_tasks();
|
|
22057
22667
|
init_workstream();
|
|
22058
22668
|
init_format();
|
|
@@ -22150,6 +22760,23 @@ function parseStatusesOption(values, flag = "--status") {
|
|
|
22150
22760
|
}
|
|
22151
22761
|
return out;
|
|
22152
22762
|
}
|
|
22763
|
+
var ALL_SUBSTATES = [...new Set(Object.values(TASK_SUBSTATES).flat())];
|
|
22764
|
+
function parseSubstatesOption(values, flag = "--substate") {
|
|
22765
|
+
const fragments = parseCsvFlag(values, flag);
|
|
22766
|
+
if (fragments.length === 0) return void 0;
|
|
22767
|
+
const out = /* @__PURE__ */ new Set();
|
|
22768
|
+
for (const raw of fragments) {
|
|
22769
|
+
const lower = raw.toLowerCase();
|
|
22770
|
+
const match = ALL_SUBSTATES.find((s) => s === lower);
|
|
22771
|
+
if (match === void 0) {
|
|
22772
|
+
throw new UsageError(
|
|
22773
|
+
`${flag} must be one of ${ALL_SUBSTATES.join(" | ")} (got ${JSON.stringify(raw)})`
|
|
22774
|
+
);
|
|
22775
|
+
}
|
|
22776
|
+
out.add(match);
|
|
22777
|
+
}
|
|
22778
|
+
return [...out];
|
|
22779
|
+
}
|
|
22153
22780
|
function resolveSelfOptional(db) {
|
|
22154
22781
|
const paneId = process.env.TMUX_PANE;
|
|
22155
22782
|
if (!paneId) return null;
|
|
@@ -22425,6 +23052,7 @@ export {
|
|
|
22425
23052
|
buildProgram,
|
|
22426
23053
|
byRoiDesc,
|
|
22427
23054
|
classifyError,
|
|
23055
|
+
colorPair,
|
|
22428
23056
|
colorStatus,
|
|
22429
23057
|
emitJson,
|
|
22430
23058
|
emitJsonCollection,
|
|
@@ -22449,6 +23077,7 @@ export {
|
|
|
22449
23077
|
parseSortOption,
|
|
22450
23078
|
parseStatusOption,
|
|
22451
23079
|
parseStatusesOption,
|
|
23080
|
+
parseSubstatesOption,
|
|
22452
23081
|
printLogRow,
|
|
22453
23082
|
relTime,
|
|
22454
23083
|
relTimeBasisForSort,
|