@luckydraw/cumulus 1.0.38 → 1.0.40

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.
Files changed (46) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/gateway/adapters/webchat.d.ts +3 -0
  3. package/dist/gateway/adapters/webchat.d.ts.map +1 -1
  4. package/dist/gateway/adapters/webchat.js +84 -1
  5. package/dist/gateway/adapters/webchat.js.map +1 -1
  6. package/dist/gateway/config.d.ts +16 -2
  7. package/dist/gateway/config.d.ts.map +1 -1
  8. package/dist/gateway/config.js +12 -6
  9. package/dist/gateway/config.js.map +1 -1
  10. package/dist/gateway/daemon.d.ts +1 -0
  11. package/dist/gateway/daemon.d.ts.map +1 -1
  12. package/dist/gateway/daemon.js +4 -0
  13. package/dist/gateway/daemon.js.map +1 -1
  14. package/dist/gateway/one-shot-model.d.ts +92 -0
  15. package/dist/gateway/one-shot-model.d.ts.map +1 -0
  16. package/dist/gateway/one-shot-model.js +191 -0
  17. package/dist/gateway/one-shot-model.js.map +1 -0
  18. package/dist/gateway/server.d.ts +3 -0
  19. package/dist/gateway/server.d.ts.map +1 -1
  20. package/dist/gateway/server.js +4 -0
  21. package/dist/gateway/server.js.map +1 -1
  22. package/dist/gateway/stall-detection.d.ts +19 -50
  23. package/dist/gateway/stall-detection.d.ts.map +1 -1
  24. package/dist/gateway/stall-detection.js +27 -190
  25. package/dist/gateway/stall-detection.js.map +1 -1
  26. package/dist/gateway/static/widget.js +342 -35
  27. package/dist/gateway/tasks-refresh.d.ts +89 -0
  28. package/dist/gateway/tasks-refresh.d.ts.map +1 -0
  29. package/dist/gateway/tasks-refresh.js +224 -0
  30. package/dist/gateway/tasks-refresh.js.map +1 -0
  31. package/dist/gateway/tasks.d.ts +21 -1
  32. package/dist/gateway/tasks.d.ts.map +1 -1
  33. package/dist/gateway/tasks.js +62 -10
  34. package/dist/gateway/tasks.js.map +1 -1
  35. package/dist/gateway/worker.d.ts +86 -0
  36. package/dist/gateway/worker.d.ts.map +1 -0
  37. package/dist/gateway/worker.js +0 -0
  38. package/dist/gateway/worker.js.map +1 -0
  39. package/dist/lib/tasks-file.d.ts +23 -0
  40. package/dist/lib/tasks-file.d.ts.map +1 -1
  41. package/dist/lib/tasks-file.js +49 -0
  42. package/dist/lib/tasks-file.js.map +1 -1
  43. package/dist/lib/tasks-format.d.ts.map +1 -1
  44. package/dist/lib/tasks-format.js +12 -5
  45. package/dist/lib/tasks-format.js.map +1 -1
  46. package/package.json +1 -1
@@ -0,0 +1,224 @@
1
+ /**
2
+ * Task 179 — the Refresh button's actual work.
3
+ *
4
+ * Karl's three answers set the shape of this file:
5
+ *
6
+ * 1. *"Write it straight away, but commit to git first so I can undo."* — so a
7
+ * snapshot commit is a PRECONDITION, and a file outside a git repo is
8
+ * refused outright. The permission to write without asking was bought with
9
+ * the undo; without a repo there is no undo, so the permission lapses.
10
+ * 2. *"Both — convert to the new format AND re-check every task's state."* — one
11
+ * model call does both, because they are the same rewrite.
12
+ * 3. *"Build it as a general facility."* — which is `worker.ts`; this module is
13
+ * only its first caller.
14
+ *
15
+ * The load-bearing part is not the prompt. A model handed a whole file and asked
16
+ * for a whole file back will, sooner or later, quietly drop a row — so the answer
17
+ * is checked before it is written, and a run that loses a task id, fails to
18
+ * parse, or comes back without the marker is refused WHOLE. The git snapshot is
19
+ * the belt; these checks are the braces.
20
+ */
21
+ import { execFileSync } from 'child_process';
22
+ import * as fs from 'fs';
23
+ import * as path from 'path';
24
+ import { TASKS_FORMAT_SECTION, hasTasksMarker, parseTasksFile } from '../lib/tasks-file.js';
25
+ import { getCurrentVersion } from '../lib/version-check.js';
26
+ import { readTasks, resolveRefreshTarget } from './tasks.js';
27
+ import { runWorker } from './worker.js';
28
+ /** A refusal with a reason the drawer shows verbatim. */
29
+ export class TasksRefreshError extends Error {
30
+ }
31
+ const SNAPSHOT_MESSAGE = 'chore(tasks): snapshot before refresh';
32
+ /** Task rows in either the schema format or the free-form one it replaced. */
33
+ const ANY_TASK_ID_RE = /^\s*-\s*\[[^\]]*\]\s*(\d{1,4})\b/;
34
+ /**
35
+ * Every task id the file currently carries.
36
+ *
37
+ * Deliberately permissive, and matched against the RAW text rather than a parse:
38
+ * the un-migrated files this exists to convert do not parse at all, and they are
39
+ * the ones with the most to lose. Both shapes seen in the wild are covered —
40
+ * `- [ ] 179 · title` (schema) and `- [◕] 178: title` (what came before).
41
+ *
42
+ * The `## Format` section is skipped, and that is not a nicety: the manual
43
+ * carried inside every migrated file demonstrates the grammar with EXAMPLE rows
44
+ * (`- [ ] 073 · StorePool reaping`, `- [⊘] 084 · Creative-writing benchmark`).
45
+ * Counting those as tasks would demand the model preserve four ids that do not
46
+ * exist, so every already-migrated file would fail the drop check and Refresh
47
+ * would work only on un-migrated ones. Found by test, not by reading.
48
+ */
49
+ export function taskIdsIn(content) {
50
+ const ids = [];
51
+ let inFormat = false;
52
+ for (const line of content.split('\n')) {
53
+ const heading = /^## +(.+?) *$/.exec(line);
54
+ if (heading)
55
+ inFormat = heading[1] === 'Format';
56
+ if (inFormat)
57
+ continue;
58
+ const m = ANY_TASK_ID_RE.exec(line);
59
+ if (m)
60
+ ids.push(m[1]);
61
+ }
62
+ return ids;
63
+ }
64
+ /** Strip a ```-fenced wrapper, which a chat-tuned model adds however firmly it is told not to. */
65
+ export function unfence(answer) {
66
+ const trimmed = answer.trim();
67
+ if (!trimmed.startsWith('```'))
68
+ return trimmed;
69
+ const lines = trimmed.split('\n');
70
+ lines.shift();
71
+ if (lines[lines.length - 1]?.trim().startsWith('```'))
72
+ lines.pop();
73
+ return lines.join('\n').trim();
74
+ }
75
+ function git(cwd, args) {
76
+ return execFileSync('git', args, { cwd, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'] });
77
+ }
78
+ /**
79
+ * Commit the file as it stands, and return whether there was anything to commit.
80
+ *
81
+ * Scoped by pathspec on BOTH commands, so a refresh never sweeps up whatever else
82
+ * the user happened to have staged. "Nothing to commit" is success, not failure:
83
+ * the file is already identical to HEAD, so HEAD *is* the undo point.
84
+ *
85
+ * Not being in a git repo is the one hard refusal here — see the module comment.
86
+ */
87
+ export function snapshotBeforeRefresh(filePath) {
88
+ const dir = path.dirname(filePath);
89
+ let root;
90
+ try {
91
+ root = git(dir, ['rev-parse', '--show-toplevel']).trim();
92
+ }
93
+ catch {
94
+ throw new TasksRefreshError(`Refresh needs a git repository to snapshot into, and ${filePath} is not in one. ` +
95
+ `It rewrites the whole file, so without an undo point it will not run.`);
96
+ }
97
+ const rel = path.relative(root, filePath);
98
+ try {
99
+ git(root, ['add', '--', rel]);
100
+ git(root, ['commit', '-m', SNAPSHOT_MESSAGE, '--', rel]);
101
+ return true;
102
+ }
103
+ catch (err) {
104
+ // git says "nothing to commit" on STDOUT and exits non-zero, so the throw is
105
+ // the success path here as often as it is the failure one.
106
+ const e = err;
107
+ const output = `${String(e.stdout ?? '')}\n${String(e.stderr ?? '')}`;
108
+ if (/nothing to commit|no changes added|nothing added to commit/i.test(output))
109
+ return false;
110
+ throw new TasksRefreshError(`Could not commit a snapshot of ${rel} before refreshing: ${err instanceof Error ? err.message : String(err)}`);
111
+ }
112
+ }
113
+ /** Filenames under `docs/tasks/`, so the model can attach `doc` fields it can verify exist. */
114
+ function taskDocListing(cwd) {
115
+ try {
116
+ return fs
117
+ .readdirSync(path.join(cwd, 'docs', 'tasks'))
118
+ .filter(name => name.endsWith('.md'))
119
+ .sort();
120
+ }
121
+ catch {
122
+ return [];
123
+ }
124
+ }
125
+ /**
126
+ * The whole prompt, as one pure function so the instructions are testable
127
+ * without a model.
128
+ *
129
+ * The three facts at the end are the ones the model cannot get from the file and
130
+ * would otherwise invent: which task docs exist, and what the gateway is actually
131
+ * running. That last one is the thing Karl complained about — rows still saying
132
+ * "needs reload" long after the reload happened.
133
+ */
134
+ export function buildRefreshPrompt(input) {
135
+ const docs = input.docs.length
136
+ ? input.docs.map(d => `- docs/tasks/${d}`).join('\n')
137
+ : '(no docs/tasks directory)';
138
+ return [
139
+ 'You are rewriting a project task file into the exact format specified below.',
140
+ 'Return ONLY the complete new file. No preamble, no explanation, no code fence.',
141
+ '',
142
+ '=== THE FORMAT (this section must appear verbatim in your output) ===',
143
+ TASKS_FORMAT_SECTION,
144
+ '',
145
+ '=== RULES ===',
146
+ '1. Output the whole file: a `# <name> Tasks` title, the marker line',
147
+ ' `<!-- cumulus:tasks v1 -->` alone on its own line within the first ten lines,',
148
+ ' then `## Format` (verbatim as given above), `## Queue`, and `## Done`.',
149
+ '2. EVERY task in the current file must appear in your output, with the SAME id.',
150
+ ' Losing an id fails the whole refresh and nothing is written. If a task has no',
151
+ ' id, mint one that does not collide with any other id in the file.',
152
+ '3. Prose belongs in `note` lines and nowhere else. A task row is one line.',
153
+ "4. Re-check each task's status against the evidence below. If you CANNOT ground a",
154
+ ' status change in that evidence, leave the status exactly as it is.',
155
+ '5. When you do change a status, add a `note` saying why, so the change is auditable.',
156
+ '6. Finished work belongs in `## Done` with a `done <YYYY-MM-DD>` field. Work still',
157
+ ' to do belongs in `## Queue`, in build order, most important first.',
158
+ '',
159
+ '=== EVIDENCE YOU CANNOT GET FROM THE FILE ===',
160
+ `The gateway is currently running version ${input.version}. Any note claiming a`,
161
+ 'change is "published, needs reload" for a version at or below this one is stale —',
162
+ 'the reload has happened. Say so rather than repeating the claim.',
163
+ '',
164
+ 'Task documents that exist on disk (use these for `doc` fields; do not invent paths):',
165
+ docs,
166
+ '',
167
+ input.migrated
168
+ ? '=== THE CURRENT FILE (already in this format — re-check it) ==='
169
+ : '=== THE CURRENT FILE (free-form — convert it) ===',
170
+ input.content,
171
+ ].join('\n');
172
+ }
173
+ /**
174
+ * Convert and re-check a thread's tasks file in place.
175
+ *
176
+ * Order matters and is the design: resolve → snapshot → ask → CHECK → write.
177
+ * Nothing between the snapshot and the write touches disk, so every refusal below
178
+ * leaves the file byte-identical to what was just committed.
179
+ */
180
+ export async function refreshTasksFile(threadName, opts = {}) {
181
+ const target = await resolveRefreshTarget(threadName);
182
+ if (!target) {
183
+ throw new TasksRefreshError('This thread has no Tasks.md in its always-include list, so there is nothing to refresh.');
184
+ }
185
+ const committed = snapshotBeforeRefresh(target.filePath);
186
+ const prompt = buildRefreshPrompt({
187
+ content: target.content,
188
+ migrated: target.migrated,
189
+ docs: taskDocListing(target.cwd),
190
+ version: getCurrentVersion(),
191
+ });
192
+ const raw = opts.runModel
193
+ ? await opts.runModel(prompt)
194
+ : await runWorker(threadName, 'tasks-refresh', prompt, opts);
195
+ const next = unfence(raw);
196
+ // ── The guard rails. Each one refuses the WHOLE answer; none patches it up. ──
197
+ if (!next) {
198
+ throw new TasksRefreshError('The worker returned an empty file. Nothing was written.');
199
+ }
200
+ if (!hasTasksMarker(next)) {
201
+ throw new TasksRefreshError("The worker's answer has no `<!-- cumulus:tasks v1 -->` marker on its own line in the " +
202
+ 'first ten lines, so it is not a tasks file. Nothing was written.');
203
+ }
204
+ const parsed = parseTasksFile(next);
205
+ if (parsed.errors.length > 0) {
206
+ const first = parsed.errors[0];
207
+ throw new TasksRefreshError(`The worker's answer does not parse (${parsed.errors.length} bad line${parsed.errors.length === 1 ? '' : 's'}, first at line ${first.line}: ${first.reason}). Nothing was written.`);
208
+ }
209
+ // The one check that catches the failure this whole task is built around: a
210
+ // wholesale rewrite that silently loses a row. Compared against the RAW ids of
211
+ // the file that went in, so it holds for un-migrated files too.
212
+ const before = new Set(taskIdsIn(target.content));
213
+ const after = new Set(parsed.queue.concat(parsed.done).map(row => row.id));
214
+ const droppedIds = [...before].filter(id => !after.has(id));
215
+ if (droppedIds.length > 0) {
216
+ throw new TasksRefreshError(`The worker dropped ${droppedIds.length} task${droppedIds.length === 1 ? '' : 's'} (${droppedIds.join(', ')}). Nothing was written — the file is exactly as it was.`);
217
+ }
218
+ if (after.size === 0 && before.size > 0) {
219
+ throw new TasksRefreshError('The worker returned a file with no tasks in it. Nothing was written.');
220
+ }
221
+ fs.writeFileSync(target.filePath, next.endsWith('\n') ? next : `${next}\n`, 'utf-8');
222
+ return { payload: await readTasks(threadName), committed, droppedIds: [] };
223
+ }
224
+ //# sourceMappingURL=tasks-refresh.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tasks-refresh.js","sourceRoot":"","sources":["../../src/gateway/tasks-refresh.ts"],"names":[],"mappings":"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"}
@@ -7,7 +7,7 @@
7
7
  * resolution rule, so the drawer and the prompt can never disagree about which
8
8
  * file is the queue (Rule #8).
9
9
  */
10
- import { TaskRow, TasksFile } from '../lib/tasks-file.js';
10
+ import { TaskDisposition, TaskRow, TasksFile } from '../lib/tasks-file.js';
11
11
  export interface ResolvedTasksFile {
12
12
  filePath: string;
13
13
  file: TasksFile;
@@ -20,6 +20,24 @@ export interface ResolvedTasksFile {
20
20
  * under any other name is.
21
21
  */
22
22
  export declare function resolveTasksFile(threadName: string): Promise<ResolvedTasksFile | null>;
23
+ /** What task 179's Refresh is pointed at, and whether it has been migrated yet. */
24
+ export interface RefreshTarget {
25
+ filePath: string;
26
+ content: string;
27
+ cwd: string;
28
+ /** True when the file already carries the marker — Refresh re-checks rather than converts. */
29
+ migrated: boolean;
30
+ }
31
+ /**
32
+ * The file Refresh acts on (task 179).
33
+ *
34
+ * A superset of {@link resolveTasksFile}: a marked file wins wherever it sits,
35
+ * and failing that, an always-included file *named* `Tasks.md` — which is
36
+ * precisely the un-migrated case Refresh exists to convert. Filename is the
37
+ * fallback ONLY here; the prompt projection still keys on the marker alone, so
38
+ * conversion never happens by accident on a file nobody pointed at.
39
+ */
40
+ export declare function resolveRefreshTarget(threadName: string): Promise<RefreshTarget | null>;
23
41
  /** Wire shape for the drawer — flat rows, depth carried explicitly. */
24
42
  export interface TasksPayload {
25
43
  filePath: string | null;
@@ -32,4 +50,6 @@ export interface TasksPayload {
32
50
  export declare function readTasks(threadName: string): Promise<TasksPayload>;
33
51
  export declare function moveTaskInThread(threadName: string, dragId: string, targetId: string, position: 'before' | 'after'): Promise<TasksPayload>;
34
52
  export declare function resolveDepInThread(threadName: string, id: string, accept: boolean): Promise<TasksPayload>;
53
+ /** Task 178 — the drawer's Closed ✓ / Archive buttons. */
54
+ export declare function dispositionTasksInThread(threadName: string, ids: string[], disposition: TaskDisposition): Promise<TasksPayload>;
35
55
  //# sourceMappingURL=tasks.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"tasks.d.ts","sourceRoot":"","sources":["../../src/gateway/tasks.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAYH,OAAO,EACL,OAAO,EACP,SAAS,EAQV,MAAM,sBAAsB,CAAC;AAG9B,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,SAAS,CAAC;CACjB;AAED;;;;;;GAMG;AACH,wBAAsB,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC,CAkB5F;AAED,uEAAuE;AACvE,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,KAAK,EAAE,OAAO,EAAE,CAAC;IACjB,IAAI,EAAE,OAAO,EAAE,CAAC;IAChB,MAAM,EAAE,SAAS,CAAC,QAAQ,CAAC,CAAC;IAC5B,mEAAmE;IACnE,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,wBAAsB,SAAS,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAWzE;AA8BD,wBAAgB,gBAAgB,CAC9B,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,QAAQ,GAAG,OAAO,GAC3B,OAAO,CAAC,YAAY,CAAC,CAEvB;AAED,wBAAgB,kBAAkB,CAChC,UAAU,EAAE,MAAM,EAClB,EAAE,EAAE,MAAM,EACV,MAAM,EAAE,OAAO,GACd,OAAO,CAAC,YAAY,CAAC,CAEvB"}
1
+ {"version":3,"file":"tasks.d.ts","sourceRoot":"","sources":["../../src/gateway/tasks.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAYH,OAAO,EACL,eAAe,EACf,OAAO,EACP,SAAS,EASV,MAAM,sBAAsB,CAAC;AAG9B,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,SAAS,CAAC;CACjB;AAmCD;;;;;;GAMG;AACH,wBAAsB,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC,CAO5F;AAED,mFAAmF;AACnF,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;IACZ,8FAA8F;IAC9F,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED;;;;;;;;GAQG;AACH,wBAAsB,oBAAoB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,CAW5F;AAED,uEAAuE;AACvE,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,KAAK,EAAE,OAAO,EAAE,CAAC;IACjB,IAAI,EAAE,OAAO,EAAE,CAAC;IAChB,MAAM,EAAE,SAAS,CAAC,QAAQ,CAAC,CAAC;IAC5B,mEAAmE;IACnE,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,wBAAsB,SAAS,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAWzE;AAgDD,wBAAgB,gBAAgB,CAC9B,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,QAAQ,GAAG,OAAO,GAC3B,OAAO,CAAC,YAAY,CAAC,CAEvB;AAED,wBAAgB,kBAAkB,CAChC,UAAU,EAAE,MAAM,EAClB,EAAE,EAAE,MAAM,EACV,MAAM,EAAE,OAAO,GACd,OAAO,CAAC,YAAY,CAAC,CAEvB;AAED,0DAA0D;AAC1D,wBAAgB,wBAAwB,CACtC,UAAU,EAAE,MAAM,EAClB,GAAG,EAAE,MAAM,EAAE,EACb,WAAW,EAAE,eAAe,GAC3B,OAAO,CAAC,YAAY,CAAC,CAEvB"}
@@ -11,36 +11,71 @@ import * as fs from 'fs';
11
11
  import * as os from 'os';
12
12
  import * as path from 'path';
13
13
  import { loadGlobalConfig, loadThreadConfig, mergeConfigs, resolveFilePath, } from '../lib/config.js';
14
- import { TasksFileError, assertWritable, hasTasksMarker, moveTask, parseTasksFile, resolveProposedDep, serializeTasksFile, } from '../lib/tasks-file.js';
14
+ import { TasksFileError, assertWritable, dispositionTasks, hasTasksMarker, moveTask, parseTasksFile, resolveProposedDep, serializeTasksFile, } from '../lib/tasks-file.js';
15
15
  import { resolveThreadCwd } from '../lib/worktree.js';
16
16
  /**
17
- * Find the thread's schema tasks file, or null when it has none.
17
+ * The thread's always-include files, in configured order, with their contents.
18
18
  *
19
- * Detection is the marker inside the file, never the filename: a project whose
20
- * `Tasks.md` has not been migrated is not a tasks file, and a migrated file
21
- * under any other name is.
19
+ * The single place that answers "which files could be this thread's task list",
20
+ * so the marker rule below and task 179's refresh target cannot resolve against
21
+ * different sets (Rule #8).
22
22
  */
23
- export async function resolveTasksFile(threadName) {
23
+ async function threadIncludeFiles(threadName) {
24
24
  const merged = mergeConfigs(await loadGlobalConfig(), await loadThreadConfig(threadName));
25
25
  const resolved = await resolveThreadCwd(threadName, merged.projectDir);
26
26
  const cwd = resolved.cwd || merged.projectDir || path.join(os.homedir(), 'projects', threadName);
27
+ const files = [];
27
28
  for (const entry of merged.alwaysInclude ?? []) {
28
29
  const filePath = resolveFilePath(entry, cwd);
29
- let content;
30
30
  try {
31
31
  if (!fs.existsSync(filePath) || fs.statSync(filePath).isDirectory())
32
32
  continue;
33
- content = fs.readFileSync(filePath, 'utf-8');
33
+ files.push({ filePath, content: fs.readFileSync(filePath, 'utf-8') });
34
34
  }
35
35
  catch {
36
36
  continue;
37
37
  }
38
+ }
39
+ return { cwd, files };
40
+ }
41
+ /**
42
+ * Find the thread's schema tasks file, or null when it has none.
43
+ *
44
+ * Detection is the marker inside the file, never the filename: a project whose
45
+ * `Tasks.md` has not been migrated is not a tasks file, and a migrated file
46
+ * under any other name is.
47
+ */
48
+ export async function resolveTasksFile(threadName) {
49
+ const { files } = await threadIncludeFiles(threadName);
50
+ for (const { filePath, content } of files) {
38
51
  if (!hasTasksMarker(content))
39
52
  continue;
40
53
  return { filePath, file: parseTasksFile(content) };
41
54
  }
42
55
  return null;
43
56
  }
57
+ /**
58
+ * The file Refresh acts on (task 179).
59
+ *
60
+ * A superset of {@link resolveTasksFile}: a marked file wins wherever it sits,
61
+ * and failing that, an always-included file *named* `Tasks.md` — which is
62
+ * precisely the un-migrated case Refresh exists to convert. Filename is the
63
+ * fallback ONLY here; the prompt projection still keys on the marker alone, so
64
+ * conversion never happens by accident on a file nobody pointed at.
65
+ */
66
+ export async function resolveRefreshTarget(threadName) {
67
+ const { cwd, files } = await threadIncludeFiles(threadName);
68
+ for (const { filePath, content } of files) {
69
+ if (hasTasksMarker(content))
70
+ return { filePath, content, cwd, migrated: true };
71
+ }
72
+ for (const { filePath, content } of files) {
73
+ if (path.basename(filePath).toLowerCase() === 'tasks.md') {
74
+ return { filePath, content, cwd, migrated: false };
75
+ }
76
+ }
77
+ return null;
78
+ }
44
79
  export async function readTasks(threadName) {
45
80
  const resolved = await resolveTasksFile(threadName);
46
81
  if (!resolved) {
@@ -60,13 +95,13 @@ export async function readTasks(threadName) {
60
95
  * parser did not understand is refused whole rather than rewritten without them,
61
96
  * because serializing a partial parse is how a drag silently deletes a line.
62
97
  */
63
- async function mutateQueue(threadName, mutate) {
98
+ async function mutateFile(threadName, mutate) {
64
99
  const resolved = await resolveTasksFile(threadName);
65
100
  if (!resolved) {
66
101
  throw new TasksFileError('This thread has no tasks file in its always-include list');
67
102
  }
68
103
  assertWritable(resolved.file);
69
- const next = { ...resolved.file, queue: mutate(resolved.file.queue) };
104
+ const next = mutate(resolved.file);
70
105
  fs.writeFileSync(resolved.filePath, serializeTasksFile(next), 'utf-8');
71
106
  return {
72
107
  filePath: resolved.filePath,
@@ -75,10 +110,27 @@ async function mutateQueue(threadName, mutate) {
75
110
  errors: [],
76
111
  };
77
112
  }
113
+ /** The common case: a mutation that only reorders or re-nests the queue. */
114
+ function mutateQueue(threadName, mutate) {
115
+ return mutateFile(threadName, file => ({ ...file, queue: mutate(file.queue) }));
116
+ }
117
+ /**
118
+ * Local calendar date, not UTC. `done 2026-09-02` is read by a person against
119
+ * their own day; stamping a UTC date makes an evening's work land on tomorrow.
120
+ */
121
+ function today() {
122
+ const now = new Date();
123
+ const pad = (n) => String(n).padStart(2, '0');
124
+ return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`;
125
+ }
78
126
  export function moveTaskInThread(threadName, dragId, targetId, position) {
79
127
  return mutateQueue(threadName, queue => moveTask(queue, dragId, targetId, position));
80
128
  }
81
129
  export function resolveDepInThread(threadName, id, accept) {
82
130
  return mutateQueue(threadName, queue => resolveProposedDep(queue, id, accept));
83
131
  }
132
+ /** Task 178 — the drawer's Closed ✓ / Archive buttons. */
133
+ export function dispositionTasksInThread(threadName, ids, disposition) {
134
+ return mutateFile(threadName, file => dispositionTasks(file, ids, disposition, today()));
135
+ }
84
136
  //# sourceMappingURL=tasks.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"tasks.js","sourceRoot":"","sources":["../../src/gateway/tasks.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"tasks.js","sourceRoot":"","sources":["../../src/gateway/tasks.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,MAAM,IAAI,CAAC;AACzB,OAAO,KAAK,EAAE,MAAM,IAAI,CAAC;AACzB,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAE7B,OAAO,EACL,gBAAgB,EAChB,gBAAgB,EAChB,YAAY,EACZ,eAAe,GAChB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAIL,cAAc,EACd,cAAc,EACd,gBAAgB,EAChB,cAAc,EACd,QAAQ,EACR,cAAc,EACd,kBAAkB,EAClB,kBAAkB,GACnB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAatD;;;;;;GAMG;AACH,KAAK,UAAU,kBAAkB,CAC/B,UAAkB;IAElB,MAAM,MAAM,GAAG,YAAY,CAAC,MAAM,gBAAgB,EAAE,EAAE,MAAM,gBAAgB,CAAC,UAAU,CAAC,CAAC,CAAC;IAC1F,MAAM,QAAQ,GAAG,MAAM,gBAAgB,CAAC,UAAU,EAAE,MAAM,CAAC,UAAU,CAAC,CAAC;IACvE,MAAM,GAAG,GAAG,QAAQ,CAAC,GAAG,IAAI,MAAM,CAAC,UAAU,IAAI,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,UAAU,EAAE,UAAU,CAAC,CAAC;IAEjG,MAAM,KAAK,GAAwB,EAAE,CAAC;IACtC,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,aAAa,IAAI,EAAE,EAAE,CAAC;QAC/C,MAAM,QAAQ,GAAG,eAAe,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;QAC7C,IAAI,CAAC;YACH,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,WAAW,EAAE;gBAAE,SAAS;YAC9E,KAAK,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,OAAO,EAAE,EAAE,CAAC,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,EAAE,CAAC,CAAC;QACxE,CAAC;QAAC,MAAM,CAAC;YACP,SAAS;QACX,CAAC;IACH,CAAC;IACD,OAAO,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC;AACxB,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,UAAkB;IACvD,MAAM,EAAE,KAAK,EAAE,GAAG,MAAM,kBAAkB,CAAC,UAAU,CAAC,CAAC;IACvD,KAAK,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,KAAK,EAAE,CAAC;QAC1C,IAAI,CAAC,cAAc,CAAC,OAAO,CAAC;YAAE,SAAS;QACvC,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,cAAc,CAAC,OAAO,CAAC,EAAE,CAAC;IACrD,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAWD;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB,CAAC,UAAkB;IAC3D,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG,MAAM,kBAAkB,CAAC,UAAU,CAAC,CAAC;IAC5D,KAAK,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,KAAK,EAAE,CAAC;QAC1C,IAAI,cAAc,CAAC,OAAO,CAAC;YAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IACjF,CAAC;IACD,KAAK,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,KAAK,EAAE,CAAC;QAC1C,IAAI,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,WAAW,EAAE,KAAK,UAAU,EAAE,CAAC;YACzD,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC;QACrD,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAYD,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,UAAkB;IAChD,MAAM,QAAQ,GAAG,MAAM,gBAAgB,CAAC,UAAU,CAAC,CAAC;IACpD,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;IAC5E,CAAC;IACD,OAAO;QACL,QAAQ,EAAE,QAAQ,CAAC,QAAQ;QAC3B,KAAK,EAAE,QAAQ,CAAC,IAAI,CAAC,KAAK;QAC1B,IAAI,EAAE,QAAQ,CAAC,IAAI,CAAC,IAAI;QACxB,MAAM,EAAE,QAAQ,CAAC,IAAI,CAAC,MAAM;KAC7B,CAAC;AACJ,CAAC;AAED;;;;;;GAMG;AACH,KAAK,UAAU,UAAU,CACvB,UAAkB,EAClB,MAAsC;IAEtC,MAAM,QAAQ,GAAG,MAAM,gBAAgB,CAAC,UAAU,CAAC,CAAC;IACpD,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,MAAM,IAAI,cAAc,CAAC,0DAA0D,CAAC,CAAC;IACvF,CAAC;IACD,cAAc,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;IAE9B,MAAM,IAAI,GAAG,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;IACnC,EAAE,CAAC,aAAa,CAAC,QAAQ,CAAC,QAAQ,EAAE,kBAAkB,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC,CAAC;IAEvE,OAAO;QACL,QAAQ,EAAE,QAAQ,CAAC,QAAQ;QAC3B,KAAK,EAAE,IAAI,CAAC,KAAK;QACjB,IAAI,EAAE,IAAI,CAAC,IAAI;QACf,MAAM,EAAE,EAAE;KACX,CAAC;AACJ,CAAC;AAED,4EAA4E;AAC5E,SAAS,WAAW,CAClB,UAAkB,EAClB,MAAuC;IAEvC,OAAO,UAAU,CAAC,UAAU,EAAE,IAAI,CAAC,EAAE,CAAC,CAAC,EAAE,GAAG,IAAI,EAAE,KAAK,EAAE,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC;AAClF,CAAC;AAED;;;GAGG;AACH,SAAS,KAAK;IACZ,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC;IACvB,MAAM,GAAG,GAAG,CAAC,CAAS,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;IACtD,OAAO,GAAG,GAAG,CAAC,WAAW,EAAE,IAAI,GAAG,CAAC,GAAG,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC,IAAI,GAAG,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC;AACjF,CAAC;AAED,MAAM,UAAU,gBAAgB,CAC9B,UAAkB,EAClB,MAAc,EACd,QAAgB,EAChB,QAA4B;IAE5B,OAAO,WAAW,CAAC,UAAU,EAAE,KAAK,CAAC,EAAE,CAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,CAAC,CAAC,CAAC;AACvF,CAAC;AAED,MAAM,UAAU,kBAAkB,CAChC,UAAkB,EAClB,EAAU,EACV,MAAe;IAEf,OAAO,WAAW,CAAC,UAAU,EAAE,KAAK,CAAC,EAAE,CAAC,kBAAkB,CAAC,KAAK,EAAE,EAAE,EAAE,MAAM,CAAC,CAAC,CAAC;AACjF,CAAC;AAED,0DAA0D;AAC1D,MAAM,UAAU,wBAAwB,CACtC,UAAkB,EAClB,GAAa,EACb,WAA4B;IAE5B,OAAO,UAAU,CAAC,UAAU,EAAE,IAAI,CAAC,EAAE,CAAC,gBAAgB,CAAC,IAAI,EAAE,GAAG,EAAE,WAAW,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC;AAC3F,CAAC"}
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Task 179 — the invisible background worker.
3
+ *
4
+ * Karl asked whether every thread should have "some kind of system co-thread for
5
+ * doing this kind of work, but that it wouldn't necessarily be visible in the
6
+ * threads UI", and answered his own poll with "yes — build it as a general
7
+ * facility, Refresh is just the first user."
8
+ *
9
+ * A co-thread is the wrong engine for it, on four grounded counts: its writes
10
+ * land in its own git worktree on its own branch (task 143 P4), it is drawn in
11
+ * the sidebar under its master, it shares the master's stored content in BOTH
12
+ * directions — so a worker that reads a whole file pours it into the master's
13
+ * memory — and only a thread already mid-turn can create one, which a button
14
+ * click is not.
15
+ *
16
+ * So this is not a chat thread at all. It is a gateway-owned one-shot model call
17
+ * with no conversation, no history and no sidebar row — `runOneShotModel` plus
18
+ * the two things that make it a *facility* rather than a bare call:
19
+ *
20
+ * 1. **One job per `{thread, kind}` at a time.** A second request while one is
21
+ * in flight is REFUSED, not queued. Two Refreshes racing on one `Tasks.md`
22
+ * is the exact failure the whole task exists to prevent, and a queue would
23
+ * merely delay it.
24
+ * 2. **A status push** (`running | done | failed`) to whoever is watching, so a
25
+ * button can spin without the UI inventing a second notion of "busy".
26
+ *
27
+ * It deliberately never touches `threadBusy`. That flag drives the sidebar dots
28
+ * (task 146), the stall check (task 164) and the deferred-message drain (task
29
+ * 144); a worker that set it would make the sidebar claim a turn was running,
30
+ * suppress a real stall check, and drain a user's queued message into nothing.
31
+ */
32
+ import { type OneShotModelContext } from './one-shot-model.js';
33
+ /**
34
+ * What a worker run is for. A closed union, not a free string: the key is half of
35
+ * the in-flight identity, so a typo would silently buy a second concurrent slot
36
+ * on the same thread — the one thing this module exists to deny.
37
+ */
38
+ export type WorkerKind = 'tasks-refresh';
39
+ export type WorkerState = 'running' | 'done' | 'failed';
40
+ export interface WorkerStatus {
41
+ threadName: string;
42
+ kind: WorkerKind;
43
+ state: WorkerState;
44
+ /** Present on `failed`, and on `done` when there is something worth saying. */
45
+ detail?: string;
46
+ }
47
+ /** Thrown by {@link runWorker} when this `{thread, kind}` is already working. */
48
+ export declare class WorkerBusyError extends Error {
49
+ constructor(kind: WorkerKind);
50
+ }
51
+ /** Thrown when the model produced no answer at all (spawn failure, timeout, empty). */
52
+ export declare class WorkerNoAnswerError extends Error {
53
+ constructor();
54
+ }
55
+ export declare function isWorkerRunning(threadName: string, kind: WorkerKind): boolean;
56
+ /**
57
+ * Where worker progress goes. Registered by the webchat adapter at startup for
58
+ * the same reason `setThreadActivityListener` and `setUserQueueDelivery` are:
59
+ * only the adapter can reach the sockets, and this module must not know they
60
+ * exist.
61
+ */
62
+ type WorkerStatusListener = (status: WorkerStatus) => void;
63
+ export declare function setWorkerStatusListener(fn: WorkerStatusListener | undefined): void;
64
+ export interface RunWorkerOptions {
65
+ /** Configured worker model id (`workerModel`), resolved against both catalogs. */
66
+ model?: string;
67
+ context?: OneShotModelContext;
68
+ timeoutMs?: number;
69
+ maxTokens?: number;
70
+ }
71
+ /**
72
+ * Run one prompt as this thread's `kind` worker and return the model's raw text.
73
+ *
74
+ * Throws {@link WorkerBusyError} if that slot is taken and
75
+ * {@link WorkerNoAnswerError} if the model produced nothing — both before any
76
+ * caller-visible side effect, so a refused run leaves no trace.
77
+ *
78
+ * The slot is released in a `finally`, so a caller that throws while INTERPRETING
79
+ * the answer (which is where task 179's guard rails live) still frees the button.
80
+ */
81
+ export declare function runWorker(threadName: string, kind: WorkerKind, prompt: string, opts?: RunWorkerOptions): Promise<string>;
82
+ /** Announce the end of a worker run. Separate from {@link runWorker} because the
83
+ * caller — not this module — knows whether interpreting the answer succeeded. */
84
+ export declare function reportWorkerResult(threadName: string, kind: WorkerKind, state: 'done' | 'failed', detail?: string): void;
85
+ export {};
86
+ //# sourceMappingURL=worker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"worker.d.ts","sourceRoot":"","sources":["../../src/gateway/worker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,OAAO,EAAmB,KAAK,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAEhF;;;;GAIG;AACH,MAAM,MAAM,UAAU,GAAG,eAAe,CAAC;AAEzC,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG,MAAM,GAAG,QAAQ,CAAC;AAExD,MAAM,WAAW,YAAY;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,UAAU,CAAC;IACjB,KAAK,EAAE,WAAW,CAAC;IACnB,+EAA+E;IAC/E,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,iFAAiF;AACjF,qBAAa,eAAgB,SAAQ,KAAK;gBAC5B,IAAI,EAAE,UAAU;CAI7B;AAED,uFAAuF;AACvF,qBAAa,mBAAoB,SAAQ,KAAK;;CAK7C;AAQD,wBAAgB,eAAe,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,GAAG,OAAO,CAE7E;AAED;;;;;GAKG;AACH,KAAK,oBAAoB,GAAG,CAAC,MAAM,EAAE,YAAY,KAAK,IAAI,CAAC;AAG3D,wBAAgB,uBAAuB,CAAC,EAAE,EAAE,oBAAoB,GAAG,SAAS,GAAG,IAAI,CAElF;AAmBD,MAAM,WAAW,gBAAgB;IAC/B,kFAAkF;IAClF,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,mBAAmB,CAAC;IAC9B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAYD;;;;;;;;;GASG;AACH,wBAAsB,SAAS,CAC7B,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,UAAU,EAChB,MAAM,EAAE,MAAM,EACd,IAAI,GAAE,gBAAqB,GAC1B,OAAO,CAAC,MAAM,CAAC,CAkBjB;AAED;iFACiF;AACjF,wBAAgB,kBAAkB,CAChC,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,UAAU,EAChB,KAAK,EAAE,MAAM,GAAG,QAAQ,EACxB,MAAM,CAAC,EAAE,MAAM,GACd,IAAI,CAEN"}
Binary file
@@ -0,0 +1 @@
1
+ {"version":3,"file":"worker.js","sourceRoot":"","sources":["../../src/gateway/worker.ts"],"names":[],"mappings":"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"}
@@ -29,6 +29,16 @@ export declare const TASKS_FILE_MARKER = "<!-- cumulus:tasks v1 -->";
29
29
  /** Field keys the parser accepts. Nothing else parses. */
30
30
  export declare const TASK_FIELD_KEYS: readonly ["doc", "phase", "note", "dep", "done"];
31
31
  export type TaskFieldKey = (typeof TASK_FIELD_KEYS)[number];
32
+ /** Task 178 — a row that left the queue having worked. */
33
+ export declare const TASK_STATUS_CLOSED = "\u2705";
34
+ /**
35
+ * Task 178 — a row that left the queue WITHOUT working: dropped, superseded, or
36
+ * gone stale. Distinct from `❌` (implemented, then failed its test), which is a
37
+ * verdict on work that happened; this is the absence of a verdict.
38
+ */
39
+ export declare const TASK_STATUS_ARCHIVED = "\u2298";
40
+ /** What the drawer's two disposition buttons do to a row. */
41
+ export type TaskDisposition = 'closed' | 'archived';
32
42
  export interface TaskRow {
33
43
  /** Digits, unique across the whole file. */
34
44
  id: string;
@@ -108,6 +118,19 @@ export declare function moveTask(rows: TaskRow[], dragId: string, targetId: stri
108
118
  * lands them after the cluster they just left.
109
119
  */
110
120
  export declare function resolveProposedDep(rows: TaskRow[], id: string, accept: boolean): TaskRow[];
121
+ /**
122
+ * Task 178 — take rows out of the queue and put them at the top of `## Done`.
123
+ *
124
+ * `closed` stamps `[✅]`, `archived` stamps `[⊘]`; both stamp `done <date>`, which
125
+ * is exactly what the manual tells an agent to do by hand. Ordering is
126
+ * newest-first, matching how the file has been kept since 176.
127
+ *
128
+ * A dispositioned row's children are PROMOTED one level rather than removed: the
129
+ * thing that blocked them is finished, so they are now blocked by whatever blocked
130
+ * it — or by nothing. That keeps {@link assertValidQueue} true without a special
131
+ * case, and it is the only edit that does not silently drop work.
132
+ */
133
+ export declare function dispositionTasks(file: TasksFile, ids: string[], disposition: TaskDisposition, today: string): TasksFile;
111
134
  /** Structural invariants every write must preserve. */
112
135
  export declare function assertValidQueue(rows: TaskRow[]): void;
113
136
  /** Refuses writes to a file the parser did not fully understand. */
@@ -1 +1 @@
1
- {"version":3,"file":"tasks-file.d.ts","sourceRoot":"","sources":["../../src/lib/tasks-file.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"tasks-file.d.ts","sourceRoot":"","sources":["../../src/lib/tasks-file.ts"],"names":[],"mappings":"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"}
@@ -35,6 +35,14 @@ export const TASKS_FILE_MARKER = '<!-- cumulus:tasks v1 -->';
35
35
  const MARKER_MAX_LINE = 10;
36
36
  /** Field keys the parser accepts. Nothing else parses. */
37
37
  export const TASK_FIELD_KEYS = ['doc', 'phase', 'note', 'dep', 'done'];
38
+ /** Task 178 — a row that left the queue having worked. */
39
+ export const TASK_STATUS_CLOSED = '✅';
40
+ /**
41
+ * Task 178 — a row that left the queue WITHOUT working: dropped, superseded, or
42
+ * gone stale. Distinct from `❌` (implemented, then failed its test), which is a
43
+ * verdict on work that happened; this is the absence of a verdict.
44
+ */
45
+ export const TASK_STATUS_ARCHIVED = '⊘';
38
46
  /** Two spaces of indent per level of dependency nesting. */
39
47
  const INDENT_UNIT = 2;
40
48
  /** Fields sit one further indent unit past their row, for legibility only. */
@@ -382,6 +390,47 @@ export function resolveProposedDep(rows, id, accept) {
382
390
  assertValidQueue(next);
383
391
  return next;
384
392
  }
393
+ /**
394
+ * Task 178 — take rows out of the queue and put them at the top of `## Done`.
395
+ *
396
+ * `closed` stamps `[✅]`, `archived` stamps `[⊘]`; both stamp `done <date>`, which
397
+ * is exactly what the manual tells an agent to do by hand. Ordering is
398
+ * newest-first, matching how the file has been kept since 176.
399
+ *
400
+ * A dispositioned row's children are PROMOTED one level rather than removed: the
401
+ * thing that blocked them is finished, so they are now blocked by whatever blocked
402
+ * it — or by nothing. That keeps {@link assertValidQueue} true without a special
403
+ * case, and it is the only edit that does not silently drop work.
404
+ */
405
+ export function dispositionTasks(file, ids, disposition, today) {
406
+ if (ids.length === 0)
407
+ throw new TasksFileError('No tasks given to close');
408
+ let queue = file.queue.slice();
409
+ const finished = [];
410
+ for (const id of ids) {
411
+ if (file.done.some(r => r.id === id)) {
412
+ throw new TasksFileError(`Task ${id} is already in Done`);
413
+ }
414
+ const i = indexOfId(queue, id);
415
+ const row = at(queue, i);
416
+ const end = subtreeEnd(queue, i);
417
+ const promoted = queue
418
+ .slice(i + 1, end + 1)
419
+ .map(child => ({ ...child, depth: child.depth - 1, depProposed: false }));
420
+ queue = [...queue.slice(0, i), ...promoted, ...queue.slice(end + 1)];
421
+ finished.push({
422
+ ...row,
423
+ depth: 0,
424
+ depProposed: false,
425
+ status: disposition === 'closed' ? TASK_STATUS_CLOSED : TASK_STATUS_ARCHIVED,
426
+ done: today,
427
+ });
428
+ }
429
+ assertValidQueue(queue);
430
+ // Newest first. A batch lands together, in the order it was given, so several
431
+ // rows closed in one click read as one event rather than as a reversed list.
432
+ return { ...file, queue, done: [...finished, ...file.done] };
433
+ }
385
434
  /** Structural invariants every write must preserve. */
386
435
  export function assertValidQueue(rows) {
387
436
  if (rows.length === 0)