#2063·LifeOS

WorkSweep: project ISAs never reach the board, completed sessions land in Queued, goals regex cuts at "Z"

Author: markb0Created Sep 4, 2026Updated Sep 17, 2026

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

typescript
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.

typescript
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 2000h

Expected: 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

diff
--- 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([