WorkSweep: project ISAs never reach the board, completed sessions land in Queued, goals regex cuts at "Z"
Upstream issue draft. Target: LIFEOS/TOOLS/WorkSweep.ts as shipped in LifeOS 7.40.4. Three defects in one file, one patch. Diff attached below under Patch.
1. The session catch-up walks one ISA home; canon has two
ISASystem.md sanctions two ISA homes: MEMORY/WORK/{slug}/ISA.md for tasks and <project>/ISA.md for persistent things ("The ISA lives in the project's repo as system of record"). hooks/ISASync.hook.ts was fixed for the second home in public issue #1807 and now records a project ISA in work.json under the slug project-<dirname>.
WorkSweep.ts still enumerates MEMORY/WORK/ only (listSessionDirs() is a readdirSync(WORK_DIR)). WorkSystem.md says the private SessionEnd hook is the first capture surface and "a fresh public install ships without it; sweep catches everything else within an hour". On a public install the sweep is therefore the only capture surface, and it cannot see a project ISA. Result: a project ISA is tracked in work.json and never gets an issue.
work.json cannot be the fix: syncToWorkJson writes task, sessionUUID, phase, progress, started, updatedAt, ratings, sessionName, lastToolActivity, ascent. There is no path field, so the registry cannot lead the sweep from project-<dirname> back to a file.
Fix. Enumerate the second home directly: every PROJECTS.md row (the | **Name** | \path` |contractparseProjectsMd()already parses for the project check) whose path holds anISA.md`. Derive the slug the same way ISASync does when the frontmatter has none, so both sides agree on the key. 17 changed lines, no new config, no new dependency.
2. A session already at phase complete is created open in Queued
The sweep creates every caught session with Status:queued. A session whose ISA is already at phase complete is a record of finished work, not a queue item. With no SessionEnd hook on a public install, nothing ever closes it, so Queued fills with finished folders.
Fix. For task ISAs only (directory under MEMORY/WORK/): when phaseBracket(fm.phase) === "cairn", create with Status:complete and close the issue at once with a comment naming the ISA (ghCloseIssue). Dry run prints [closed] on the line. A project ISA is never complete by doctrine (ISASystem.md: project ISAs "are never completed"), and ghIssueSearchSlug runs gh issue list --state all, so a closed project card would block the sweep from ever recreating it. Project ISAs always land open with Status:queued.
3. Active Goals regex uses \Z
content.match(/## Active Goals[\s\S]*?(?=\n## |\Z)/)\Z is not an end-of-string anchor in JavaScript; it is a literal Z. The section was cut at the first capital Z in the goals text: any goal containing a capital Z lost everything from that Z onward. $ without the m flag is end of string.
content.match(/## Active Goals[\s\S]*?(?=\n## |$)/)Test
Point LIFEOS_DIR at a fixture tree holding a PROJECTS.md row for a repo that contains an ISA.md, an empty MEMORY/WORK/, and the install's USER/WORK/work_repo.json. Run:
LIFEOS_DIR=<fixture> bun LIFEOS/TOOLS/WorkSweep.ts --dry-run --since 2000hExpected: one + would create [Sweep] <task> (project-<dirname>) labels=...,Status:queued line for the project ISA, no [closed], nothing created (dry run), worksweep.jsonl appended inside the fixture only. Before the patch the same command prints no line for it.
Patch
--- LIFEOS/TOOLS/WorkSweep.ts 2026-09-04 10:48:59
+++ LIFEOS/TOOLS/WorkSweep.ts (patched) 2026-09-04 16:09:54
@@ -34,7 +34,7 @@
import { existsSync, readFileSync, writeFileSync, readdirSync, statSync, mkdirSync, appendFileSync } from "fs";
import { getDAName } from "../../hooks/lib/identity"
-import { join } from "path";
+import { join, basename } from "path";
import { loadWorkConfig } from "../../hooks/lib/work-config";
import { phaseHasWorkStarted, phaseBracket } from "./ascent";
import { homedir } from "node:os";
@@ -109,6 +109,17 @@
.filter((p) => statSync(p).isDirectory() && existsSync(join(p, "ISA.md")));
}
+// Second ISA home. The Algorithm sanctions two: MEMORY/WORK/{slug}/ISA.md for tasks and
+// <project>/ISA.md for persistent things. ISASync records both in work.json (public issue
+// #1807), but this sweep walked MEMORY/WORK only, so a project ISA never reached the board.
+// work.json carries no path, so the home itself is enumerated: every PROJECTS.md row whose
+// path holds an ISA.md.
+function listProjectIsaDirs(): string[] {
+ return parseProjectsMd()
+ .map((r) => r.pathLocal)
+ .filter((p) => existsSync(join(p, "ISA.md")));
+}
+
function isMeaningfulWork(fm: ISAFm, isaPath: string): boolean {
// Skip empty/abandoned scaffolds — no progress, no phase advance.
// ALGORITHM sessions: the run must have left articulation. Resolved through
@@ -249,7 +260,7 @@
stats: SweepStats,
): Promise<void> {
const cutoff = Date.now() - sinceMs;
- const dirs = listSessionDirs();
+ const dirs = [...listSessionDirs(), ...listProjectIsaDirs()];
for (const dir of dirs) {
const isaPath = join(dir, "ISA.md");
const mtime = statSync(isaPath).mtimeMs;
@@ -258,6 +269,9 @@
const content = readFileSync(isaPath, "utf-8");
const fm = parseFrontmatter(content);
+ // A project ISA usually has no slug: derive it the way ISASync does, so the sweep and
+ // the registry agree on the key (`project-<dirname>`, public issue #1807).
+ if (fm && !fm.slug && !dir.startsWith(WORK_DIR)) fm.slug = "project-" + basename(dir).toLowerCase();
if (!fm || !fm.slug) continue;
if (fm.github_issue) continue; // already synced
@@ -271,7 +285,10 @@
// A session already at the cairn (phase complete) is a record of done work, not a
// queue item. It lands closed with Status:complete so Queued only holds live work
// (a finished folder merge sat in Queued with nothing to close it).
- const isComplete = phaseBracket(fm.phase) === "cairn";
+ // Task ISAs only: a project ISA is never complete by doctrine (ISASystem.md, Two Homes:
+ // project ISAs "are never \"completed\""), and ghIssueSearchSlug matches closed issues,
+ // so a closed project card could never come back. Project ISAs land open, Status:queued.
+ const isComplete = dir.startsWith(WORK_DIR) && phaseBracket(fm.phase) === "cairn";
const titlePrefix = isNative ? "[Native]" : "[Sweep]";
const title = `${titlePrefix} ${taskOrSlug(fm)} [slug:${fm.slug}]`;
const labels = filterLabels([Source: danielmiessler/LifeOS