Skip to content

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

Description

@markb0

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

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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions