mainflow 0.12.0 → 0.13.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +50 -3
- package/dist/facts.d.ts +29 -2
- package/dist/facts.js +27 -2
- package/dist/hooks.js +36 -12
- package/dist/{host-Dwf1wLMd.js → host-mlpW8M1s.js} +186 -100
- package/dist/host.js +1 -1
- package/dist/{launch-D5K564tw.js → launch-CJYDVfE9.js} +2 -2
- package/dist/launch.js +1 -1
- package/dist/{ui-CLMrfnhR.js → ui-BbIFSMKR.js} +2 -2
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { $ as
|
|
2
|
+
import { $ as setFindingStatus, A as addAction, C as setCriterionText, D as updateTask, E as supersedeDecision, F as setSessionArchived, J as createFinding, K as appendOps, L as setSessionStatus, M as handoffSession, O as sessionBriefing, P as readSession, Q as setFindingArchived, S as setCriterion, V as updateAction, X as planIssue, Y as createIssue, Z as reopenIssue, _ as createTask, b as setArchived, ct as TASK_FILE, et as setIssueArchived, f as acceptDecision, g as createMainline, h as createDecision, l as migrateHub, m as addRepo, nt as loadHub, p as addHub, pt as findHubRoot, q as closeIssue, st as MAINLINE_FILE, tt as HubError, ut as WriteError, v as dropChecklistItem, w as setMainlineStatus, x as setChecklistItem, y as initHub, z as startSession } from "./host-mlpW8M1s.js";
|
|
3
|
+
import { N as parseRef, P as refExample } from "./dist-BQmXEfAf.js";
|
|
3
4
|
import { t as listProgress } from "./progress-1BfPg2zN.js";
|
|
4
5
|
import { COMMAND_SPECS, OPTIONS } from "./facts.js";
|
|
5
6
|
import { runHook } from "./hooks.js";
|
|
6
|
-
import { l as CLI_VERSION, r as launchUi, s as startUiInBackground } from "./launch-
|
|
7
|
+
import { l as CLI_VERSION, r as launchUi, s as startUiInBackground } from "./launch-CJYDVfE9.js";
|
|
7
8
|
import { readFileSync, realpathSync } from "node:fs";
|
|
8
9
|
import { join, relative } from "node:path";
|
|
9
10
|
import { fileURLToPath } from "node:url";
|
|
@@ -54,6 +55,10 @@ const RUNS = {
|
|
|
54
55
|
});
|
|
55
56
|
return done(`started ${session.id}${session.actions.length ? ` with ${session.actions.length} carried actions` : ""}`);
|
|
56
57
|
},
|
|
58
|
+
"session handoff": ({ root, args, opts }) => {
|
|
59
|
+
const session = handoffSession(root, sessionOf(opts), args[0]);
|
|
60
|
+
return done(`${session.id} took over ${session.actions.filter((a) => a.status === "todo" || a.status === "doing").length} actions from ${args[0]}`);
|
|
61
|
+
},
|
|
57
62
|
"session show": ({ root, args, opts }) => {
|
|
58
63
|
const id = sessionOf(opts, args[0]);
|
|
59
64
|
const session = readSession(root, id);
|
|
@@ -155,6 +160,13 @@ const RUNS = {
|
|
|
155
160
|
setCriterion(root, args[0], false);
|
|
156
161
|
return done(`unchecked ${args[0]}`);
|
|
157
162
|
},
|
|
163
|
+
"criterion set": ({ root, args, opts }) => {
|
|
164
|
+
setCriterionText(root, args[0], {
|
|
165
|
+
text: opts.text,
|
|
166
|
+
doneWhen: opts["done-when"]
|
|
167
|
+
});
|
|
168
|
+
return done(`updated ${args[0]}`);
|
|
169
|
+
},
|
|
158
170
|
"task set": ({ root, args, opts }) => {
|
|
159
171
|
const blockedBy = opts.reason ? {
|
|
160
172
|
reason: opts.reason,
|
|
@@ -172,6 +184,18 @@ const RUNS = {
|
|
|
172
184
|
});
|
|
173
185
|
return done(`updated ${args[0]} (${t.status})`);
|
|
174
186
|
},
|
|
187
|
+
"task show": ({ root, args }) => {
|
|
188
|
+
const { mainline, task } = findTask(loadHub(root), args[0]);
|
|
189
|
+
console.log(`${mainline.id}/${task.id} ${task.status.padEnd(8)} ${task.kind} ${task.title}`);
|
|
190
|
+
if (task.checklist.length === 0) console.log(" (no checklist items)");
|
|
191
|
+
for (const c of task.checklist) {
|
|
192
|
+
const mark = c.done ? "[x]" : "[ ]";
|
|
193
|
+
const state = c.dropped ? " (dropped)" : "";
|
|
194
|
+
console.log(` ${mark} ${c.id} ${c.text}${state}`);
|
|
195
|
+
}
|
|
196
|
+
for (const link of task.links ?? []) console.log(` ${link.rel} → ${link.to}${link.note ? ` ${link.note}` : ""}`);
|
|
197
|
+
return done(`${task.checklist.length} checklist items`);
|
|
198
|
+
},
|
|
175
199
|
"item check": ({ root, args, opts }) => {
|
|
176
200
|
setChecklistItem(root, args[0], true, opts.evidence);
|
|
177
201
|
return done(`checked ${args[0]}`);
|
|
@@ -216,13 +240,18 @@ const RUNS = {
|
|
|
216
240
|
return done(`logged ${entry.at} ${entry.operation}`);
|
|
217
241
|
},
|
|
218
242
|
"ops list": ({ root, opts }) => {
|
|
219
|
-
const shown = loadHub(root).ops.filter((o) => !opts.month || o.at.startsWith(opts.month));
|
|
243
|
+
const shown = loadHub(root).ops.filter((o) => (!opts.month || o.at.startsWith(opts.month)) && (!opts.ref || o.ref === opts.ref));
|
|
220
244
|
for (const o of shown) console.log(` ${o.at} ${o.target} ${o.operation} ${o.result}${o.ref ? ` ${o.ref}` : ""}`);
|
|
221
245
|
return done(`${shown.length} operations`);
|
|
222
246
|
},
|
|
223
247
|
"decision supersede": ({ root, args, opts }) => {
|
|
224
248
|
const d = supersedeDecision(root, args[0], required(opts, "by", "--by d002"));
|
|
225
249
|
return done(`superseded ${args[0]} by ${d.supersededBy}`);
|
|
250
|
+
},
|
|
251
|
+
"decision list": ({ root, args }) => {
|
|
252
|
+
const mainline = findMainline(loadHub(root), args[0]);
|
|
253
|
+
for (const d of mainline.decisions) console.log(` ${mainline.id}/${d.id} ${d.status.padEnd(10)} ${d.title}`);
|
|
254
|
+
return done(`${mainline.decisions.length} decisions`);
|
|
226
255
|
}
|
|
227
256
|
};
|
|
228
257
|
const COMMANDS = Object.fromEntries(Object.keys(COMMAND_SPECS).map((name) => [name, {
|
|
@@ -300,6 +329,24 @@ function sessionOf(opts, arg) {
|
|
|
300
329
|
return id;
|
|
301
330
|
}
|
|
302
331
|
const list = (value) => (value ?? "").split(",").map((v) => v.trim()).filter(Boolean);
|
|
332
|
+
/** The mainline a reference names; throws with the same message resolve.ts gives when it is missing. */
|
|
333
|
+
function findMainline(hub, id) {
|
|
334
|
+
const mainline = hub.mainlines.find((m) => m.id === id);
|
|
335
|
+
if (!mainline) throw new WriteError(`mainline ${id} not found; run "mainflow status --all" to list mainlines`);
|
|
336
|
+
return mainline;
|
|
337
|
+
}
|
|
338
|
+
/** The task a `m/t` reference names, with the mainline it sits in. */
|
|
339
|
+
function findTask(hub, refInput) {
|
|
340
|
+
const ref = parseRef(refInput);
|
|
341
|
+
if (ref?.kind !== "task") throw new WriteError(`expected a task reference like ${refExample("task")}, got "${refInput}"`);
|
|
342
|
+
const mainline = findMainline(hub, ref.mainline);
|
|
343
|
+
const task = mainline.tasks.find((t) => t.id === ref.task);
|
|
344
|
+
if (!task) throw new WriteError(`task ${refInput} not found`);
|
|
345
|
+
return {
|
|
346
|
+
mainline,
|
|
347
|
+
task
|
|
348
|
+
};
|
|
349
|
+
}
|
|
303
350
|
function required(opts, name, example) {
|
|
304
351
|
const value = opts[name];
|
|
305
352
|
if (!value) throw new WriteError(`--${name} is required, e.g. ${example}`);
|
package/dist/facts.d.ts
CHANGED
|
@@ -73,6 +73,12 @@ export declare const COMMAND_SPECS: {
|
|
|
73
73
|
readonly args: 0;
|
|
74
74
|
readonly needsHub: true;
|
|
75
75
|
};
|
|
76
|
+
readonly 'session handoff': {
|
|
77
|
+
readonly usage: 'session handoff <session> [--session <session>]';
|
|
78
|
+
readonly summary: 'Take over the unfinished actions of another session into this one';
|
|
79
|
+
readonly args: 1;
|
|
80
|
+
readonly needsHub: true;
|
|
81
|
+
};
|
|
76
82
|
readonly 'session show': {
|
|
77
83
|
readonly usage: 'session show [session]';
|
|
78
84
|
readonly summary: 'Print the briefing a hook would give this session';
|
|
@@ -183,12 +189,24 @@ export declare const COMMAND_SPECS: {
|
|
|
183
189
|
readonly args: 1;
|
|
184
190
|
readonly needsHub: true;
|
|
185
191
|
};
|
|
192
|
+
readonly 'criterion set': {
|
|
193
|
+
readonly usage: 'criterion set <m#r> [--text <text>] [--done-when <text>]';
|
|
194
|
+
readonly summary: 'Rewrite a criterion\'s text or doneWhen; done stays as it is (use criterion check/uncheck)';
|
|
195
|
+
readonly args: 1;
|
|
196
|
+
readonly needsHub: true;
|
|
197
|
+
};
|
|
186
198
|
readonly 'task set': {
|
|
187
199
|
readonly usage: 'task set <m/t> [--status] [--reason --waiting-for --signal] [--repo] [--branch --worktree --base --merge]';
|
|
188
200
|
readonly summary: 'Change status, repository or branch state; --status blocked needs --reason';
|
|
189
201
|
readonly args: 1;
|
|
190
202
|
readonly needsHub: true;
|
|
191
203
|
};
|
|
204
|
+
readonly 'task show': {
|
|
205
|
+
readonly usage: 'task show <m/t>';
|
|
206
|
+
readonly summary: 'Print a task: status, kind, checklist and links';
|
|
207
|
+
readonly args: 1;
|
|
208
|
+
readonly needsHub: true;
|
|
209
|
+
};
|
|
192
210
|
readonly 'item check': {
|
|
193
211
|
readonly usage: 'item check <m/t#c> --evidence <evidence>';
|
|
194
212
|
readonly summary: 'Check an item with evidence';
|
|
@@ -280,8 +298,8 @@ export declare const COMMAND_SPECS: {
|
|
|
280
298
|
readonly needsHub: true;
|
|
281
299
|
};
|
|
282
300
|
readonly 'ops list': {
|
|
283
|
-
readonly usage: 'ops list [--month YYYY-MM]';
|
|
284
|
-
readonly summary: 'List logged external operations';
|
|
301
|
+
readonly usage: 'ops list [--month YYYY-MM] [--ref <ref>]';
|
|
302
|
+
readonly summary: 'List logged external operations, optionally filtered by month or by the exact ref that was logged';
|
|
285
303
|
readonly args: 0;
|
|
286
304
|
readonly needsHub: true;
|
|
287
305
|
};
|
|
@@ -291,6 +309,12 @@ export declare const COMMAND_SPECS: {
|
|
|
291
309
|
readonly args: 1;
|
|
292
310
|
readonly needsHub: true;
|
|
293
311
|
};
|
|
312
|
+
readonly 'decision list': {
|
|
313
|
+
readonly usage: 'decision list <m>';
|
|
314
|
+
readonly summary: 'List a mainline\'s decisions';
|
|
315
|
+
readonly args: 1;
|
|
316
|
+
readonly needsHub: true;
|
|
317
|
+
};
|
|
294
318
|
};
|
|
295
319
|
export type CommandName = keyof typeof COMMAND_SPECS;
|
|
296
320
|
export declare const OPTIONS: {
|
|
@@ -340,6 +364,9 @@ export declare const OPTIONS: {
|
|
|
340
364
|
readonly text: {
|
|
341
365
|
readonly type: 'string';
|
|
342
366
|
};
|
|
367
|
+
readonly 'done-when': {
|
|
368
|
+
readonly type: 'string';
|
|
369
|
+
};
|
|
343
370
|
readonly serves: {
|
|
344
371
|
readonly type: 'string';
|
|
345
372
|
};
|
package/dist/facts.js
CHANGED
|
@@ -49,6 +49,12 @@ const COMMAND_SPECS = {
|
|
|
49
49
|
args: 0,
|
|
50
50
|
needsHub: true
|
|
51
51
|
},
|
|
52
|
+
"session handoff": {
|
|
53
|
+
usage: "session handoff <session> [--session <session>]",
|
|
54
|
+
summary: "Take over the unfinished actions of another session into this one",
|
|
55
|
+
args: 1,
|
|
56
|
+
needsHub: true
|
|
57
|
+
},
|
|
52
58
|
"session show": {
|
|
53
59
|
usage: "session show [session]",
|
|
54
60
|
summary: "Print the briefing a hook would give this session",
|
|
@@ -159,12 +165,24 @@ const COMMAND_SPECS = {
|
|
|
159
165
|
args: 1,
|
|
160
166
|
needsHub: true
|
|
161
167
|
},
|
|
168
|
+
"criterion set": {
|
|
169
|
+
usage: "criterion set <m#r> [--text <text>] [--done-when <text>]",
|
|
170
|
+
summary: "Rewrite a criterion's text or doneWhen; done stays as it is (use criterion check/uncheck)",
|
|
171
|
+
args: 1,
|
|
172
|
+
needsHub: true
|
|
173
|
+
},
|
|
162
174
|
"task set": {
|
|
163
175
|
usage: "task set <m/t> [--status] [--reason --waiting-for --signal] [--repo] [--branch --worktree --base --merge]",
|
|
164
176
|
summary: "Change status, repository or branch state; --status blocked needs --reason",
|
|
165
177
|
args: 1,
|
|
166
178
|
needsHub: true
|
|
167
179
|
},
|
|
180
|
+
"task show": {
|
|
181
|
+
usage: "task show <m/t>",
|
|
182
|
+
summary: "Print a task: status, kind, checklist and links",
|
|
183
|
+
args: 1,
|
|
184
|
+
needsHub: true
|
|
185
|
+
},
|
|
168
186
|
"item check": {
|
|
169
187
|
usage: "item check <m/t#c> --evidence <evidence>",
|
|
170
188
|
summary: "Check an item with evidence",
|
|
@@ -256,8 +274,8 @@ const COMMAND_SPECS = {
|
|
|
256
274
|
needsHub: true
|
|
257
275
|
},
|
|
258
276
|
"ops list": {
|
|
259
|
-
usage: "ops list [--month YYYY-MM]",
|
|
260
|
-
summary: "List logged external operations",
|
|
277
|
+
usage: "ops list [--month YYYY-MM] [--ref <ref>]",
|
|
278
|
+
summary: "List logged external operations, optionally filtered by month or by the exact ref that was logged",
|
|
261
279
|
args: 0,
|
|
262
280
|
needsHub: true
|
|
263
281
|
},
|
|
@@ -266,6 +284,12 @@ const COMMAND_SPECS = {
|
|
|
266
284
|
summary: "Mark a decision as replaced by another",
|
|
267
285
|
args: 1,
|
|
268
286
|
needsHub: true
|
|
287
|
+
},
|
|
288
|
+
"decision list": {
|
|
289
|
+
usage: "decision list <m>",
|
|
290
|
+
summary: "List a mainline's decisions",
|
|
291
|
+
args: 1,
|
|
292
|
+
needsHub: true
|
|
269
293
|
}
|
|
270
294
|
};
|
|
271
295
|
const OPTIONS = {
|
|
@@ -287,6 +311,7 @@ const OPTIONS = {
|
|
|
287
311
|
"delegated-from": { type: "string" },
|
|
288
312
|
"handoff-from": { type: "string" },
|
|
289
313
|
text: { type: "string" },
|
|
314
|
+
"done-when": { type: "string" },
|
|
290
315
|
serves: { type: "string" },
|
|
291
316
|
before: { type: "string" },
|
|
292
317
|
note: { type: "string" },
|
package/dist/hooks.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { G as mayDecorate, H as decorateReply, I as setSessionInfo, L as setSessionStatus, O as sessionBriefing, P as readSession, R as settleSession, U as endsWithMarker, W as headerLine, dt as DATA_DIR, ft as currentBranch, i as hostName, j as ensureSession, k as turnReminder, mt as worktreesOf, nt as loadHub, pt as findHubRoot, tt as HubError } from "./host-mlpW8M1s.js";
|
|
2
2
|
import { appendFileSync, existsSync, readFileSync } from "node:fs";
|
|
3
3
|
import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
4
4
|
//#region src/hooks.ts
|
|
@@ -75,21 +75,45 @@ const displayReply = (root, { payload }) => {
|
|
|
75
75
|
displayContent: shown
|
|
76
76
|
} });
|
|
77
77
|
};
|
|
78
|
+
/** Mainline ids a reply mentions, e.g. m001 in `m001/t003`; not inside a longer word or path. */
|
|
79
|
+
const MAINLINE_MENTION = /(?<![\w/#.[-])(m\d{3})(?![\w\]])/g;
|
|
80
|
+
/** The open mainlines the text mentions; loads the hub only when the text mentions one. */
|
|
81
|
+
function openMainlinesIn(root, text) {
|
|
82
|
+
const mentioned = new Set([...text.matchAll(MAINLINE_MENTION)].map((m) => m[1]));
|
|
83
|
+
if (mentioned.size === 0) return [];
|
|
84
|
+
return loadHub(root, { remotes: false }).mainlines.filter((m) => mentioned.has(m.id) && !m.archivedAt && (m.status === "active" || m.status === "planning")).map((m) => m.id);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* What to ask for when a turn ends without its header line: the header itself when an action is in progress, or
|
|
88
|
+
* an action first when the queue is empty but the reply talks about an open mainline, since the header only names
|
|
89
|
+
* the session's own action in progress. Null when there is nothing to ask for.
|
|
90
|
+
*/
|
|
91
|
+
function missingHeaderRequest(root, { payload, cli }) {
|
|
92
|
+
if (!payload.session_id || payload.stop_hook_active) return null;
|
|
93
|
+
const message = payload.last_assistant_message ?? "";
|
|
94
|
+
if (endsWithMarker(message)) return null;
|
|
95
|
+
const session = readSession(root, claudeCodeSessionId(payload.session_id));
|
|
96
|
+
const header = headerLine(session);
|
|
97
|
+
if (header) return `[mainflow] 這一輪最後一則回覆的最後一行不是標頭。請只回一則內容為「${header}」的訊息,不加其他文字。`;
|
|
98
|
+
if (session && session.actions.length > 0) return null;
|
|
99
|
+
const mainlines = openMainlinesIn(root, message);
|
|
100
|
+
if (mainlines.length === 0) return null;
|
|
101
|
+
return `[mainflow] 這一輪的回覆提到 ${mainlines.join("、")},但這場會話沒有行動,所以回覆結尾沒有標頭。這一輪若在推進它,先用 ${cli} action add "<做法>" --serves <m/t#c> 列出行動並用 ${cli} action set <a編號> --status doing 標為進行中,或用 ${cli} session handoff <會話> 接手上一場沒做完的行動;再只回一則內容為「▸ <m/t> · <a編號>」的訊息,不加其他文字。只是提到、沒有推進時,不必處理。`;
|
|
102
|
+
}
|
|
78
103
|
/**
|
|
79
|
-
* Stop:
|
|
80
|
-
* turn
|
|
81
|
-
*
|
|
104
|
+
* Stop: ask for what the turn's header needs, then settle the session. Only the model knows which message closes a
|
|
105
|
+
* turn, so the header is its to write; this makes a forgotten one cost a single extra message. stop_hook_active
|
|
106
|
+
* stops it from asking twice in a row. A session asked to list an action is not settled yet: settling removes a
|
|
107
|
+
* session with no actions, and the action it is asked to add needs the session to exist.
|
|
82
108
|
*/
|
|
83
109
|
const settleAndCheckMarker = (root, context) => {
|
|
84
|
-
|
|
85
|
-
const
|
|
86
|
-
if (!
|
|
87
|
-
|
|
88
|
-
if (!header) return null;
|
|
89
|
-
return JSON.stringify({ hookSpecificOutput: {
|
|
110
|
+
const request = missingHeaderRequest(root, context);
|
|
111
|
+
const session = context.payload.session_id ? readSession(root, claudeCodeSessionId(context.payload.session_id)) : null;
|
|
112
|
+
if (!request || session && session.actions.length > 0) settleOnEnd(root, context);
|
|
113
|
+
return request ? JSON.stringify({ hookSpecificOutput: {
|
|
90
114
|
hookEventName: "Stop",
|
|
91
|
-
additionalContext:
|
|
92
|
-
} });
|
|
115
|
+
additionalContext: request
|
|
116
|
+
} }) : null;
|
|
93
117
|
};
|
|
94
118
|
const CLAUDE_CODE_EVENTS = {
|
|
95
119
|
"session-start": (root, { payload, envFile, cli, startUi }) => {
|
|
@@ -32,10 +32,11 @@ function currentBranch(cwd) {
|
|
|
32
32
|
}
|
|
33
33
|
/**
|
|
34
34
|
* The hub for `cwd`: the first directory upwards holding `.mainflow/config.yaml`, except that a linked
|
|
35
|
-
* worktree's checkout of a hub resolves to the same hub in the main working tree.
|
|
36
|
-
* checkout of the same repository, so without this every worktree would write its
|
|
37
|
-
* the copies would conflict on merge. A
|
|
38
|
-
*
|
|
35
|
+
* worktree's checkout of a hub at the repository root resolves to the same hub in the main working tree.
|
|
36
|
+
* A worktree is another checkout of the same repository, so without this every worktree would write its
|
|
37
|
+
* own copy of the hub and the copies would conflict on merge. A hub below the root (a sample hub under
|
|
38
|
+
* examples/, a scratch hub under tmp/) stays as found: it is data the worktree's own branch owns, and
|
|
39
|
+
* mapping it would let tests and migrations in the worktree write the main tree's copy.
|
|
39
40
|
*/
|
|
40
41
|
function findHubRoot(cwd) {
|
|
41
42
|
let found = null;
|
|
@@ -48,8 +49,8 @@ function findHubRoot(cwd) {
|
|
|
48
49
|
}
|
|
49
50
|
const trees = worktreesOf(cwd);
|
|
50
51
|
if (!trees || trees.main === trees.current) return found;
|
|
51
|
-
|
|
52
|
-
return isHub(
|
|
52
|
+
if (found && found !== trees.current) return found;
|
|
53
|
+
return isHub(trees.main) ? trees.main : found;
|
|
53
54
|
}
|
|
54
55
|
function subdirs(dir) {
|
|
55
56
|
if (!existsSync(dir)) return [];
|
|
@@ -846,7 +847,7 @@ function checkReferences(input) {
|
|
|
846
847
|
});
|
|
847
848
|
}
|
|
848
849
|
}
|
|
849
|
-
const isOpen = (kind, status) => !CLOSED_STATUS[kind].includes(status);
|
|
850
|
+
const isOpen$2 = (kind, status) => !CLOSED_STATUS[kind].includes(status);
|
|
850
851
|
/**
|
|
851
852
|
* What a reader without the conversation needs to pick the work up: every open task says what it is for
|
|
852
853
|
* and when it is done, and every open criterion or item says what has to be true to check it. Closed
|
|
@@ -871,10 +872,10 @@ function contentGaps(mainline, tasks) {
|
|
|
871
872
|
});
|
|
872
873
|
});
|
|
873
874
|
};
|
|
874
|
-
const mainlineOpen = isOpen("mainline", mainline.status);
|
|
875
|
+
const mainlineOpen = isOpen$2("mainline", mainline.status);
|
|
875
876
|
list(mainline.file, "criteria", mainline.criteria, mainlineOpen);
|
|
876
877
|
for (const task of tasks) {
|
|
877
|
-
const open = mainlineOpen && isOpen("task", task.status);
|
|
878
|
+
const open = mainlineOpen && isOpen$2("task", task.status);
|
|
878
879
|
list(task.file, "checklist", task.checklist, open);
|
|
879
880
|
if (!open) continue;
|
|
880
881
|
for (const section of TASK_REQUIRED_SECTIONS) if (!sectionText(task.body, section)) gaps.push({
|
|
@@ -1138,82 +1139,6 @@ function decorateReply(text, context) {
|
|
|
1138
1139
|
/** Cheap test for text that decorateReply could change, so a display hook can skip loading the hub. */
|
|
1139
1140
|
const mayDecorate = (text) => /m\d{3}|[if]\d{3}|\.mainflow\/mainlines/.test(text) || endsWithMarker(text);
|
|
1140
1141
|
//#endregion
|
|
1141
|
-
//#region ../core/dist/briefing.js
|
|
1142
|
-
function describe(root, ref) {
|
|
1143
|
-
const parsed = parseRef(ref);
|
|
1144
|
-
if (parsed?.kind !== "checklistItem") return null;
|
|
1145
|
-
try {
|
|
1146
|
-
const mainline = readFrontMatter(mainlineFile(root, parsed.mainline), mainlineSchema).data;
|
|
1147
|
-
const task = readFrontMatter(taskFile(root, parsed.mainline, parsed.task), taskSchema).data;
|
|
1148
|
-
const item = task.checklist.find((c) => c.id === parsed.item);
|
|
1149
|
-
if (!item) return null;
|
|
1150
|
-
return {
|
|
1151
|
-
ref,
|
|
1152
|
-
text: item.text,
|
|
1153
|
-
checked: item.done,
|
|
1154
|
-
where: `${mainline.id} ${mainline.title} › ${task.id} ${task.title}`
|
|
1155
|
-
};
|
|
1156
|
-
} catch {
|
|
1157
|
-
return null;
|
|
1158
|
-
}
|
|
1159
|
-
}
|
|
1160
|
-
/** Problems in a session's queue that the executor should fix before going on, each with the command that fixes it. */
|
|
1161
|
-
function queueWarnings(root, session, cli) {
|
|
1162
|
-
const warnings = [];
|
|
1163
|
-
const doing = session.actions.filter((a) => a.status === "doing");
|
|
1164
|
-
if (doing.length > 1) warnings.push(`同時有 ${doing.length} 個進行中的行動(${doing.map((a) => a.id).join("、")})。一次只推進一個,其餘改回 todo。`);
|
|
1165
|
-
for (const action of doing) {
|
|
1166
|
-
const served = action.serves.map((ref) => describe(root, ref));
|
|
1167
|
-
if (served.some((s) => s === null)) warnings.push(`${action.id} 服務的項目有的已不存在,用 ${cli} action set ${action.id} --serves <m/t#c> 改正。`);
|
|
1168
|
-
else if (served.length > 0 && served.every((s) => s.checked)) warnings.push(`${action.id} 服務的項目都已勾選。做完了就執行 ${cli} action set ${action.id} --status done。`);
|
|
1169
|
-
}
|
|
1170
|
-
const next = session.actions.find((a) => a.status === "todo");
|
|
1171
|
-
if (doing.length === 0 && next) warnings.push(`下一個行動是 ${next.id}。開始前先執行 ${cli} action set ${next.id} --status doing。`);
|
|
1172
|
-
return warnings;
|
|
1173
|
-
}
|
|
1174
|
-
const current = (session) => session.actions.find((a) => a.status === "doing");
|
|
1175
|
-
const servedLine = (root, action) => action.serves.map((ref) => {
|
|
1176
|
-
const s = describe(root, ref);
|
|
1177
|
-
return s ? `${ref} ${s.text}(${s.where})` : `${ref}(找不到這個項目)`;
|
|
1178
|
-
}).join(";") || "沒有指定服務的 checklist 項目";
|
|
1179
|
-
/** Full briefing for the start of a session, after resuming, clearing or compacting. */
|
|
1180
|
-
function sessionBriefing(root, session, options) {
|
|
1181
|
-
const { cli } = options;
|
|
1182
|
-
const lines = [`[mainflow] 這場會話的 id 是 ${session.id}${options.envNote ? `(${options.envNote})` : ""}。`];
|
|
1183
|
-
const doing = current(session);
|
|
1184
|
-
const upcoming = session.actions.filter((a) => a.status === "todo").slice(0, 3);
|
|
1185
|
-
if (session.actions.length === 0) {
|
|
1186
|
-
const active = loadHub(root).mainlines.filter((m) => !m.archivedAt && (m.status === "active" || m.status === "planning"));
|
|
1187
|
-
lines.push("這場會話還沒有行動。");
|
|
1188
|
-
if (active.length > 0) {
|
|
1189
|
-
lines.push("進行中的主脈絡:");
|
|
1190
|
-
for (const m of active) lines.push(`- ${m.id} ${m.title}`);
|
|
1191
|
-
lines.push(`要推進其中一個時,先用 ${cli} status 與它的任務 checklist 決定要做什麼,再列出行動。`);
|
|
1192
|
-
}
|
|
1193
|
-
} else {
|
|
1194
|
-
lines.push(doing ? `目前行動:${doing.id} ${doing.do}` : "目前沒有進行中的行動。");
|
|
1195
|
-
if (doing) lines.push(`服務:${servedLine(root, doing)}`);
|
|
1196
|
-
if (upcoming.length > 0) lines.push(`接下來:${upcoming.map((a) => `${a.id} ${a.do}`).join("、")}`);
|
|
1197
|
-
}
|
|
1198
|
-
const warnings = queueWarnings(root, session, cli);
|
|
1199
|
-
if (warnings.length > 0) lines.push("需要處理:", ...warnings.map((w) => `- ${w}`));
|
|
1200
|
-
lines.push("工作方式:", `- 開始一件新工作前,先列進佇列:${cli} action add "<做法>" --serves <m/t#c>。`, `- 開始做標 doing,做完標 done:${cli} action set <a編號> --status doing。`, `- 勾選 checklist 要附證據:${cli} item check <m/t#c> --evidence <commit、檔案:行號或指令輸出>。`, `- 內容直接寫進檔案:意圖或需求寫進 mainline.md 的內文,任務的目標與完成定義寫進 task.md,選擇寫成決策(${cli} new decision <m> <slug> --title <標題> 建檔後再寫內文)。`, `- 改完 .mainflow/ 的檔案跑 ${cli} check,把它列出的問題修到沒有。`, "- 用完其他 skill 或子流程後,回到上面的目前行動再繼續。", "- 要開主脈絡、拆任務、記決策、延後或收尾時,用 mainflow skill,它依序帶過並告訴你資訊該寫在哪裡。");
|
|
1201
|
-
return lines.join("\n");
|
|
1202
|
-
}
|
|
1203
|
-
/** One or two lines for every user turn; null when the session has no actions to anchor to. */
|
|
1204
|
-
function turnReminder(root, session, options) {
|
|
1205
|
-
if (session.actions.length === 0) return null;
|
|
1206
|
-
const doing = current(session);
|
|
1207
|
-
const open = session.actions.filter((a) => a.status === "todo" || a.status === "doing");
|
|
1208
|
-
const head = doing ? `[mainflow] 目前行動 ${doing.id} ${doing.do},服務 ${doing.serves.join("、") || "(未指定)"}。這一輪的工作若不屬於它,先調整佇列再做。` : open.length === 0 ? `[mainflow] ${session.id} 的行動都已結束。繼續工作前先用 ${options.cli} action add 列出下一個行動。` : `[mainflow] ${session.id} 目前沒有進行中的行動。`;
|
|
1209
|
-
const header = headerLine(session);
|
|
1210
|
-
return [
|
|
1211
|
-
head,
|
|
1212
|
-
...header ? [`- 這一輪最後一則回覆的最後一行寫「${header}」,回覆範例等其他內容都放在它前面。有顯示 hook 時,它會換成標題與連結。`] : [],
|
|
1213
|
-
...queueWarnings(root, session, options.cli).map((w) => `- ${w}`)
|
|
1214
|
-
].join("\n");
|
|
1215
|
-
}
|
|
1216
|
-
//#endregion
|
|
1217
1142
|
//#region ../core/dist/sessions.js
|
|
1218
1143
|
const sessionsDir = (root) => join(root, DATA_DIR, "local", "sessions");
|
|
1219
1144
|
const sessionFile = (root, id) => join(sessionsDir(root), `${id}.yaml`);
|
|
@@ -1290,8 +1215,44 @@ function requireServes(root, serves) {
|
|
|
1290
1215
|
if (!readFrontMatter(taskFile(root, parsed.mainline, parsed.task), taskSchema).data.checklist.some((c) => c.id === parsed.item)) throw new WriteError(`checklist item ${ref} not found`);
|
|
1291
1216
|
}
|
|
1292
1217
|
}
|
|
1218
|
+
const isOpen$1 = (a) => a.status === "todo" || a.status === "doing";
|
|
1219
|
+
/**
|
|
1220
|
+
* The previous session's unfinished actions, numbered after `existing`, with the previous session marked
|
|
1221
|
+
* handed-off. Each carried action is dropped in the previous session with a note naming its new reference,
|
|
1222
|
+
* so the work stays open in one place only and the old session still shows where it went. Runs inside the
|
|
1223
|
+
* caller's lock.
|
|
1224
|
+
*/
|
|
1225
|
+
function takeUnfinished(root, from, to, existing) {
|
|
1226
|
+
const previousFile = requireSession(root, from);
|
|
1227
|
+
const previous = readYaml(previousFile, sessionSchema).data;
|
|
1228
|
+
const ids = [...existing];
|
|
1229
|
+
const carried = [];
|
|
1230
|
+
const at = nowMinute();
|
|
1231
|
+
const left = previous.actions.map((a) => {
|
|
1232
|
+
if (!isOpen$1(a)) return a;
|
|
1233
|
+
const id = nextId("a", ids);
|
|
1234
|
+
ids.push(id);
|
|
1235
|
+
carried.push({
|
|
1236
|
+
...a,
|
|
1237
|
+
id
|
|
1238
|
+
});
|
|
1239
|
+
const handedOff = `handed off to ${to}#${id}`;
|
|
1240
|
+
return {
|
|
1241
|
+
...a,
|
|
1242
|
+
status: "dropped",
|
|
1243
|
+
note: a.note ? `${a.note}; ${handedOff}` : handedOff,
|
|
1244
|
+
doneAt: at
|
|
1245
|
+
};
|
|
1246
|
+
});
|
|
1247
|
+
writeAtomic(previousFile, toYaml(validate(sessionSchema, {
|
|
1248
|
+
...previous,
|
|
1249
|
+
status: "handed-off",
|
|
1250
|
+
actions: left
|
|
1251
|
+
}, previousFile)));
|
|
1252
|
+
return carried;
|
|
1253
|
+
}
|
|
1293
1254
|
/**
|
|
1294
|
-
* Start a session. Taking over from another session
|
|
1255
|
+
* Start a session. Taking over from another session moves its unfinished actions into this one and marks
|
|
1295
1256
|
* the other session handed-off, so the queue continues where it stopped.
|
|
1296
1257
|
*/
|
|
1297
1258
|
function startSession(root, input) {
|
|
@@ -1301,19 +1262,7 @@ function startSession(root, input) {
|
|
|
1301
1262
|
const file = sessionFile(root, id);
|
|
1302
1263
|
if (existsSync(file)) throw new WriteError(`session ${id} already exists`);
|
|
1303
1264
|
if (input.delegatedFrom) requireSession(root, input.delegatedFrom);
|
|
1304
|
-
|
|
1305
|
-
if (input.handoffFrom) {
|
|
1306
|
-
const previousFile = requireSession(root, input.handoffFrom);
|
|
1307
|
-
const previous = readYaml(previousFile, sessionSchema).data;
|
|
1308
|
-
carried = previous.actions.filter((a) => a.status === "todo" || a.status === "doing").map((a, i) => ({
|
|
1309
|
-
...a,
|
|
1310
|
-
id: `a${String(i + 1).padStart(3, "0")}`
|
|
1311
|
-
}));
|
|
1312
|
-
writeAtomic(previousFile, toYaml(validate(sessionSchema, {
|
|
1313
|
-
...previous,
|
|
1314
|
-
status: "handed-off"
|
|
1315
|
-
}, previousFile)));
|
|
1316
|
-
}
|
|
1265
|
+
const carried = input.handoffFrom ? takeUnfinished(root, input.handoffFrom, id, []) : [];
|
|
1317
1266
|
const session = validate(sessionSchema, {
|
|
1318
1267
|
id,
|
|
1319
1268
|
agent: input.agent,
|
|
@@ -1351,6 +1300,32 @@ function ensureSession(root, input) {
|
|
|
1351
1300
|
created: false
|
|
1352
1301
|
};
|
|
1353
1302
|
}
|
|
1303
|
+
/**
|
|
1304
|
+
* Take over another session's unfinished actions into a session that already exists, such as one a host hook
|
|
1305
|
+
* created when the conversation opened. The receiving session must have nothing unfinished of its own, so the
|
|
1306
|
+
* carried queue is not mixed into work already under way; its finished actions stay and the carried ones are
|
|
1307
|
+
* numbered after them.
|
|
1308
|
+
*/
|
|
1309
|
+
function handoffSession(root, id, from) {
|
|
1310
|
+
if (id === from) throw new WriteError(`session ${id} cannot take over from itself`);
|
|
1311
|
+
return withLock(root, () => {
|
|
1312
|
+
const file = requireSession(root, id);
|
|
1313
|
+
const session = readYaml(file, sessionSchema).data;
|
|
1314
|
+
if (session.handoffFrom && session.handoffFrom !== from) throw new WriteError(`session ${id} already took over from ${session.handoffFrom}`);
|
|
1315
|
+
const open = session.actions.filter(isOpen$1);
|
|
1316
|
+
if (open.length > 0) throw new WriteError(`session ${id} has unfinished actions (${open.map((a) => a.id).join(", ")}); finish or drop them before taking over from ${from}`);
|
|
1317
|
+
if (!readYaml(requireSession(root, from), sessionSchema).data.actions.some(isOpen$1)) throw new WriteError(`session ${from} has no unfinished actions to take over`);
|
|
1318
|
+
const carried = takeUnfinished(root, from, id, session.actions.map((a) => a.id));
|
|
1319
|
+
const next = validate(sessionSchema, {
|
|
1320
|
+
...session,
|
|
1321
|
+
status: "active",
|
|
1322
|
+
handoffFrom: from,
|
|
1323
|
+
actions: [...session.actions, ...carried]
|
|
1324
|
+
}, file);
|
|
1325
|
+
writeAtomic(file, toYaml(next));
|
|
1326
|
+
return next;
|
|
1327
|
+
});
|
|
1328
|
+
}
|
|
1354
1329
|
/** Where a session's transcript is on disk: a platform records an absolute path, a hub may record one relative to its root. */
|
|
1355
1330
|
function transcriptPath(root, transcript) {
|
|
1356
1331
|
return isAbsolute(transcript) ? transcript : join(root, transcript);
|
|
@@ -1455,6 +1430,106 @@ function updateAction(root, ref, patch) {
|
|
|
1455
1430
|
return updated;
|
|
1456
1431
|
}
|
|
1457
1432
|
//#endregion
|
|
1433
|
+
//#region ../core/dist/briefing.js
|
|
1434
|
+
function describe(root, ref) {
|
|
1435
|
+
const parsed = parseRef(ref);
|
|
1436
|
+
if (parsed?.kind !== "checklistItem") return null;
|
|
1437
|
+
try {
|
|
1438
|
+
const mainline = readFrontMatter(mainlineFile(root, parsed.mainline), mainlineSchema).data;
|
|
1439
|
+
const task = readFrontMatter(taskFile(root, parsed.mainline, parsed.task), taskSchema).data;
|
|
1440
|
+
const item = task.checklist.find((c) => c.id === parsed.item);
|
|
1441
|
+
if (!item) return null;
|
|
1442
|
+
return {
|
|
1443
|
+
ref,
|
|
1444
|
+
text: item.text,
|
|
1445
|
+
checked: item.done,
|
|
1446
|
+
where: `${mainline.id} ${mainline.title} › ${task.id} ${task.title}`
|
|
1447
|
+
};
|
|
1448
|
+
} catch {
|
|
1449
|
+
return null;
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
/** Problems in a session's queue that the executor should fix before going on, each with the command that fixes it. */
|
|
1453
|
+
function queueWarnings(root, session, cli) {
|
|
1454
|
+
const warnings = [];
|
|
1455
|
+
const doing = session.actions.filter((a) => a.status === "doing");
|
|
1456
|
+
if (doing.length > 1) warnings.push(`同時有 ${doing.length} 個進行中的行動(${doing.map((a) => a.id).join("、")})。一次只推進一個,其餘改回 todo。`);
|
|
1457
|
+
for (const action of doing) {
|
|
1458
|
+
const served = action.serves.map((ref) => describe(root, ref));
|
|
1459
|
+
if (served.some((s) => s === null)) warnings.push(`${action.id} 服務的項目有的已不存在,用 ${cli} action set ${action.id} --serves <m/t#c> 改正。`);
|
|
1460
|
+
else if (served.length > 0 && served.every((s) => s.checked)) warnings.push(`${action.id} 服務的項目都已勾選。做完了就執行 ${cli} action set ${action.id} --status done。`);
|
|
1461
|
+
}
|
|
1462
|
+
const next = session.actions.find((a) => a.status === "todo");
|
|
1463
|
+
if (doing.length === 0 && next) warnings.push(`下一個行動是 ${next.id}。開始前先執行 ${cli} action set ${next.id} --status doing。`);
|
|
1464
|
+
return warnings;
|
|
1465
|
+
}
|
|
1466
|
+
const current = (session) => session.actions.find((a) => a.status === "doing");
|
|
1467
|
+
const isOpen = (a) => a.status === "todo" || a.status === "doing";
|
|
1468
|
+
/** Mainlines a session could be pushing forward. */
|
|
1469
|
+
const openMainlines = (root) => loadHub(root).mainlines.filter((m) => !m.archivedAt && (m.status === "active" || m.status === "planning"));
|
|
1470
|
+
/** How recently a session's transcript must have been written for the session to count as possibly still running. */
|
|
1471
|
+
const RUNNING_WINDOW_MS = 9e5;
|
|
1472
|
+
/** Whether the session's transcript was written within the running window; false when no transcript is recorded or readable. */
|
|
1473
|
+
function mayBeRunning(root, session) {
|
|
1474
|
+
if (!session.transcript) return false;
|
|
1475
|
+
try {
|
|
1476
|
+
return Date.now() - statSync(transcriptPath(root, session.transcript)).mtimeMs < RUNNING_WINDOW_MS;
|
|
1477
|
+
} catch {
|
|
1478
|
+
return false;
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
/**
|
|
1482
|
+
* Lines naming the other top-level sessions that still hold unfinished actions, with the command that takes them
|
|
1483
|
+
* over into this one. Subagent sessions are left out: their work returns to the session that delegated it.
|
|
1484
|
+
*/
|
|
1485
|
+
function handoffLines(root, session, cli) {
|
|
1486
|
+
const others = loadSessions(root).sessions.filter((s) => s.id !== session.id && !s.delegatedFrom && !s.archivedAt && (s.status === "active" || s.status === "paused") && s.actions.some(isOpen));
|
|
1487
|
+
if (others.length === 0) return [];
|
|
1488
|
+
return [`其他會話還有沒做完的行動。要接續的是其中一場已經停下的工作時,先用 ${cli} session handoff <會話> 把它的行動接進這場會話,再開始做:`, ...others.map((s) => `- ${s.id}${mayBeRunning(root, s) ? "(15 分鐘內還有寫入對話紀錄,可能還在執行,不要接手)" : ""}:${s.actions.filter(isOpen).map((a) => `${a.id} ${a.do}(${a.status},服務 ${a.serves.join("、") || "未指定"})`).join(";")}`)];
|
|
1489
|
+
}
|
|
1490
|
+
const servedLine = (root, action) => action.serves.map((ref) => {
|
|
1491
|
+
const s = describe(root, ref);
|
|
1492
|
+
return s ? `${ref} ${s.text}(${s.where})` : `${ref}(找不到這個項目)`;
|
|
1493
|
+
}).join(";") || "沒有指定服務的 checklist 項目";
|
|
1494
|
+
/** Full briefing for the start of a session, after resuming, clearing or compacting. */
|
|
1495
|
+
function sessionBriefing(root, session, options) {
|
|
1496
|
+
const { cli } = options;
|
|
1497
|
+
const lines = [`[mainflow] 這場會話的 id 是 ${session.id}${options.envNote ? `(${options.envNote})` : ""}。`];
|
|
1498
|
+
const doing = current(session);
|
|
1499
|
+
const upcoming = session.actions.filter((a) => a.status === "todo").slice(0, 3);
|
|
1500
|
+
if (session.actions.length === 0) {
|
|
1501
|
+
const active = openMainlines(root);
|
|
1502
|
+
lines.push("這場會話還沒有行動。");
|
|
1503
|
+
if (active.length > 0) {
|
|
1504
|
+
lines.push("進行中的主脈絡:");
|
|
1505
|
+
for (const m of active) lines.push(`- ${m.id} ${m.title}`);
|
|
1506
|
+
lines.push(`要推進其中一個時,先用 ${cli} status 與它的任務 checklist 決定要做什麼,再列出行動。`);
|
|
1507
|
+
}
|
|
1508
|
+
} else {
|
|
1509
|
+
lines.push(doing ? `目前行動:${doing.id} ${doing.do}` : "目前沒有進行中的行動。");
|
|
1510
|
+
if (doing) lines.push(`服務:${servedLine(root, doing)}`);
|
|
1511
|
+
if (upcoming.length > 0) lines.push(`接下來:${upcoming.map((a) => `${a.id} ${a.do}`).join("、")}`);
|
|
1512
|
+
}
|
|
1513
|
+
if (!session.actions.some(isOpen)) lines.push(...handoffLines(root, session, cli));
|
|
1514
|
+
const warnings = queueWarnings(root, session, cli);
|
|
1515
|
+
if (warnings.length > 0) lines.push("需要處理:", ...warnings.map((w) => `- ${w}`));
|
|
1516
|
+
lines.push("工作方式:", `- 開始一件新工作前,先列進佇列:${cli} action add "<做法>" --serves <m/t#c>。`, `- 開始做標 doing,做完標 done:${cli} action set <a編號> --status doing。`, `- 勾選 checklist 要附證據:${cli} item check <m/t#c> --evidence <commit、檔案:行號或指令輸出>。`, `- 內容直接寫進檔案:意圖或需求寫進 mainline.md 的內文,任務的目標與完成定義寫進 task.md,選擇寫成決策(${cli} new decision <m> <slug> --title <標題> 建檔後再寫內文)。`, `- 改完 .mainflow/ 的檔案跑 ${cli} check,把它列出的問題修到沒有。`, "- 用完其他 skill 或子流程後,回到上面的目前行動再繼續。", "- 要開主脈絡、拆任務、記決策、延後或收尾時,用 mainflow skill,它依序帶過並告訴你資訊該寫在哪裡。");
|
|
1517
|
+
return lines.join("\n");
|
|
1518
|
+
}
|
|
1519
|
+
/** One or two lines for every user turn; null when the session has no actions and the hub has no open mainline. */
|
|
1520
|
+
function turnReminder(root, session, options) {
|
|
1521
|
+
if (session.actions.length === 0) return openMainlines(root).length > 0 ? `[mainflow] ${session.id} 還沒有行動。這一輪要推進主脈絡時,先用 ${options.cli} action add 列出行動並標為 doing,或用 ${options.cli} session handoff <會話> 接手上一場沒做完的行動,再開始做。回覆結尾的標頭只看這場會話進行中的行動。` : null;
|
|
1522
|
+
const doing = current(session);
|
|
1523
|
+
const open = session.actions.filter((a) => a.status === "todo" || a.status === "doing");
|
|
1524
|
+
const head = doing ? `[mainflow] 目前行動 ${doing.id} ${doing.do},服務 ${doing.serves.join("、") || "(未指定)"}。這一輪的工作若不屬於它,先調整佇列再做。` : open.length === 0 ? `[mainflow] ${session.id} 的行動都已結束。繼續工作前先用 ${options.cli} action add 列出下一個行動。` : `[mainflow] ${session.id} 目前沒有進行中的行動。`;
|
|
1525
|
+
const header = headerLine(session);
|
|
1526
|
+
return [
|
|
1527
|
+
head,
|
|
1528
|
+
...header ? [`- 這一輪最後一則回覆的最後一行寫「${header}」,回覆範例等其他內容都放在它前面。有顯示 hook 時,它會換成標題與連結。`] : [],
|
|
1529
|
+
...queueWarnings(root, session, options.cli).map((w) => `- ${w}`)
|
|
1530
|
+
].join("\n");
|
|
1531
|
+
}
|
|
1532
|
+
//#endregion
|
|
1458
1533
|
//#region ../core/dist/write.js
|
|
1459
1534
|
function checkTaskLinks(root, mainline, task) {
|
|
1460
1535
|
if (task.repo && !(task.repo in readConfig(root).repos)) throw new WriteError(`repo "${task.repo}" is not listed in .mainflow/config.yaml; add it under repos first`);
|
|
@@ -1583,6 +1658,17 @@ function setCriterion(root, refInput, done, evidence, expectedVersion) {
|
|
|
1583
1658
|
setListItem(criterion, done, evidence);
|
|
1584
1659
|
}, expectedVersion);
|
|
1585
1660
|
}
|
|
1661
|
+
/** Rewrite a criterion's text or doneWhen, leaving done and evidence as they are. */
|
|
1662
|
+
function setCriterionText(root, refInput, patch, expectedVersion) {
|
|
1663
|
+
const ref = requireRef(refInput, "criterion");
|
|
1664
|
+
if (patch.text === void 0 && patch.doneWhen === void 0) throw new WriteError(`${refInput} needs --text or --done-when, e.g. --text "CLI can create mainlines"`);
|
|
1665
|
+
return mutateFrontMatter(root, mainlineFile(root, ref.mainline), mainlineSchema, (m) => {
|
|
1666
|
+
const criterion = m.criteria.find((c) => c.id === ref.criterion);
|
|
1667
|
+
if (!criterion) throw new WriteError(`criterion ${refInput} not found`);
|
|
1668
|
+
if (patch.text !== void 0) criterion.text = patch.text;
|
|
1669
|
+
if (patch.doneWhen !== void 0) criterion.doneWhen = patch.doneWhen;
|
|
1670
|
+
}, expectedVersion);
|
|
1671
|
+
}
|
|
1586
1672
|
/** Check or uncheck a criterion or checklist item; checking clears a drop. */
|
|
1587
1673
|
function setListItem(item, done, evidence) {
|
|
1588
1674
|
item.done = done;
|
|
@@ -2054,4 +2140,4 @@ function openInApp(hub, target, closeAfterMs = 150) {
|
|
|
2054
2140
|
return true;
|
|
2055
2141
|
}
|
|
2056
2142
|
//#endregion
|
|
2057
|
-
export {
|
|
2143
|
+
export { setFindingStatus as $, addAction as A, transcriptPath as B, setCriterionText as C, updateTask as D, supersedeDecision as E, setSessionArchived as F, mayDecorate as G, decorateReply as H, setSessionInfo as I, createFinding as J, appendOps as K, setSessionStatus as L, handoffSession as M, loadSessions as N, sessionBriefing as O, readSession as P, setFindingArchived as Q, settleSession as R, setCriterion as S, setMainlineTitle as T, endsWithMarker as U, updateAction as V, headerLine as W, planIssue as X, createIssue as Y, reopenIssue as Z, createTask as _, openInApp as a, linksInto as at, setArchived as b, syncHost as c, TASK_FILE as ct, setArchivedMany as d, DATA_DIR as dt, setIssueArchived as et, acceptDecision as f, currentBranch as ft, createMainline as g, createDecision as h, hostName as i, mainlineEdges as it, ensureSession as j, turnReminder as k, migrateHub as l, ConflictError as lt, addRepo as m, worktreesOf as mt, DEFAULT_UI_PORT as n, loadHub as nt, removeHostLink as o, refIndex as ot, addHub as p, findHubRoot as pt, closeIssue as q, caddySites as r, dependencyGraph as rt, siteBlock as s, MAINLINE_FILE as st, APP_BUNDLE_ID as t, HubError as tt, ARCHIVE_KINDS as u, WriteError as ut, dropChecklistItem as v, setMainlineStatus as w, setChecklistItem as x, initHub as y, startSession as z };
|
package/dist/host.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as openInApp, c as syncHost, i as hostName, n as DEFAULT_UI_PORT, o as removeHostLink, r as caddySites, s as siteBlock, t as APP_BUNDLE_ID } from "./host-
|
|
1
|
+
import { a as openInApp, c as syncHost, i as hostName, n as DEFAULT_UI_PORT, o as removeHostLink, r as caddySites, s as siteBlock, t as APP_BUNDLE_ID } from "./host-mlpW8M1s.js";
|
|
2
2
|
export { APP_BUNDLE_ID, DEFAULT_UI_PORT, caddySites, hostName, openInApp, removeHostLink, siteBlock, syncHost };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { c as syncHost,
|
|
1
|
+
import { c as syncHost, dt as DATA_DIR, i as hostName, n as DEFAULT_UI_PORT, o as removeHostLink, ut as WriteError } from "./host-mlpW8M1s.js";
|
|
2
2
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { spawn, spawnSync } from "node:child_process";
|
|
@@ -176,7 +176,7 @@ async function launchUi(root, hub, configured, options) {
|
|
|
176
176
|
}
|
|
177
177
|
let server;
|
|
178
178
|
if (!reuse) {
|
|
179
|
-
const { serveUi } = await import("./ui-
|
|
179
|
+
const { serveUi } = await import("./ui-BbIFSMKR.js");
|
|
180
180
|
server = await serveUi(root, choice.port);
|
|
181
181
|
writeUiRecord(root, {
|
|
182
182
|
port: choice.port,
|
package/dist/launch.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as portCandidates, c as watchHubGone, i as networkProbe, n as choosePort, o as readUiRecord, r as launchUi, s as startUiInBackground, t as PORT_SPAN } from "./launch-
|
|
1
|
+
import { a as portCandidates, c as watchHubGone, i as networkProbe, n as choosePort, o as readUiRecord, r as launchUi, s as startUiInBackground, t as PORT_SPAN } from "./launch-CJYDVfE9.js";
|
|
2
2
|
export { PORT_SPAN, choosePort, launchUi, networkProbe, portCandidates, readUiRecord, startUiInBackground, watchHubGone };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { B as transcriptPath, D as updateTask, F as setSessionArchived, N as loadSessions, P as readSession, Q as setFindingArchived, S as setCriterion, T as setMainlineTitle, a as openInApp, at as linksInto, b as setArchived, d as setArchivedMany, dt as DATA_DIR, et as setIssueArchived, it as mainlineEdges, lt as ConflictError, nt as loadHub, ot as refIndex, rt as dependencyGraph, u as ARCHIVE_KINDS, ut as WriteError, w as setMainlineStatus, x as setChecklistItem } from "./host-mlpW8M1s.js";
|
|
2
2
|
import { t as listProgress } from "./progress-1BfPg2zN.js";
|
|
3
|
-
import { l as CLI_VERSION } from "./launch-
|
|
3
|
+
import { l as CLI_VERSION } from "./launch-CJYDVfE9.js";
|
|
4
4
|
import { createRequire } from "node:module";
|
|
5
5
|
import { closeSync, existsSync, openSync, readFileSync, readSync, statSync, watch } from "node:fs";
|
|
6
6
|
import { basename, dirname, extname, join, normalize, relative, sep } from "node:path";
|