@litfamily/litmuse 0.1.2 → 0.1.4

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.
@@ -1,20 +1,22 @@
1
1
  # lit-adhd status taxonomy
2
2
 
3
3
  The collector assigns every session exactly one status. The brief renders it in the user's language
4
- with the words below and never invents a fifth.
4
+ with the words below and never invents another.
5
5
 
6
6
  | Status | Set when | English | 한국어 |
7
7
  | --- | --- | --- | --- |
8
8
  | `waiting-on-user` | The last transcript entry is the assistant asking something (a closing `?`, `lit approve`, `확인해 주세요`, `할까요`…), or the session holds a paused workflow frame or a start-work boundary | waiting on you | 나를 기다림 |
9
9
  | `active` | A transcript write within the last 15 minutes and not waiting | active | 진행 중 |
10
- | `ended` | Quiet for longer than 15 minutes | ended | 끝남 |
10
+ | `idle` | Another session, otherwise active, whose last hook event is a Stop no older than its last log line | idle | 쉬는 중 |
11
+ | `done` | Another session, otherwise active, whose start-work plan was fully ticked after its last log line | done | 완료 |
12
+ | `ended` | Quiet for longer than 15 minutes, or its last hook event was SessionEnd | ended | 끝남 |
11
13
  | `unknown` | The transcript held no timestamp | unknown | 모름 |
12
14
 
13
15
  `waitingReason` says why a session waits: `asked the user`, `paused workflow` or
14
16
  `start-work boundary`. Name it in the card's status cell when present: `waiting on you (paused
15
17
  workflow)` / `나를 기다림 (멈춘 워크플로)`.
16
18
 
17
- Order in the brief: waiting sessions first, then active ones as full cards, newest first inside each group; ended sessions collapse into one short row each after the cards.
19
+ Order in the brief: waiting sessions first, then active, idle and done ones as full cards, newest first inside each group; ended sessions collapse into one short row each after the cards. A done card's next line names the plan its run finished (`start-work finished plans/a.md` / `plans/a.md start-work 완료`).
18
20
 
19
21
  ## Sources
20
22
 
@@ -29,7 +31,7 @@ line and never filled in from memory.
29
31
  | `handoffs` | `HANDOFF*.md` and `.litmuse/handoff/latest.md` headings |
30
32
  | `git` | `git log --since` |
31
33
  | `workflowStack` | `.litmuse/workflow-stack/*.json` |
32
- | `startWork` | the session's own stage record |
34
+ | `startWork` | the stage records: this session's live run, any session's completed run |
33
35
  | `herdr` | `herdr agent list`, only with `HERDR_ENV=1` |
34
36
 
35
37
  ## Next-step kinds
@@ -169,6 +169,17 @@ Detail earns its place when it removes executor judgment, protects a boundary or
169
169
  8. The final verification wave.
170
170
  9. Commit strategy, success criteria, DoneClaim, risks and open questions.
171
171
 
172
+ ### Scope roots for outside folders
173
+
174
+ When a todo names an absolute path outside the project (a shared notes folder, a sibling checkout, a temp area), the plan grants it before the work starts, with one line per folder anywhere outside a code fence:
175
+
176
+ ```text
177
+ Scope roots: /abs/path/to/folder (read)
178
+ Scope roots: /abs/path/to/scratch (read, write, execute)
179
+ ```
180
+
181
+ Grant the narrowest existing folder and only the actions the todos need. Paths inside the project need no line. A command quoted in a fence or an inline code span (it holds a space) is data and needs no line; a code span that is one bare path counts. Before saving, search the Todos for `/` paths and add the missing lines. `start-work` runs `node scripts/check-plan.mjs --check plans/<slug>.md`, which fails on an uncovered folder and prints the exact line to add; a filesystem-root, top-level or home folder is never suggested, so name a narrower one.
182
+
172
183
  A task that builds or changes a user-facing web interface names `frontend-ui-ux` in its verification (the interface probe across the responsive matrix, with no HIGH finding left) and hands the acceptance verdict to `visual-qa`. A CLI, library or backend-only task names neither.
173
184
 
174
185
  ### Plan-row grammar (machine-checkable)
@@ -337,7 +348,7 @@ Refused on this route: `create_goal`, `update_goal`, `write_todos`, `add_memory`
337
348
 
338
349
  Stop when the plan file is written and passes the checklist, or when a missing answer blocks every useful task (then the plan carries the question as row 1 and the reply says `BLOCKED: <question>`). After two failed attempts at one section, surface what you tried and ask.
339
350
 
340
- Reply in reader form: the plan path, the TL;DR, the number of tasks, any assumption the user should confirm, and the next action: run `/start-work` (or `/litmuse:start-work`) to execute it. Approval covers plan generation; implementation, commit, push, versioning and publishing each need their own go-ahead. Keep logs, lane verdicts and command lists in the draft and ledger unless the user asks for them.
351
+ Reply in reader form: the plan path, the TL;DR, the number of tasks, any assumption the user should confirm, and the next action. End the reply with this line, plan name filled in: "Type `go` within 30 minutes to start `plans/<slug>.md` with start-work, or run `/start-work` later." The Stop hook records the saved plan for this session only, so `go` from another session, or after 30 minutes, starts nothing. Approval covers plan generation; implementation, commit, push, versioning and publishing each need their own go-ahead. Keep logs, lane verdicts and command lists in the draft and ledger unless the user asks for them.
341
352
 
342
353
  ## Pitfalls
343
354
 
@@ -10,8 +10,9 @@
10
10
  // Exit 0 = pass, 1 = findings, 2 = usage. A planning turn cannot run this (the route policy refuses node);
11
11
  // start-work, a terminal and the test suite do.
12
12
 
13
- import { existsSync, lstatSync, readFileSync } from 'node:fs';
14
- import { dirname, join, resolve } from 'node:path';
13
+ import { existsSync, lstatSync, readFileSync, realpathSync, statSync } from 'node:fs';
14
+ import { homedir } from 'node:os';
15
+ import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:path';
15
16
  import { fileURLToPath } from 'node:url';
16
17
 
17
18
  export const SLUG_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/u;
@@ -56,7 +57,67 @@ export function visibleLines(text) {
56
57
  return out;
57
58
  }
58
59
 
59
- export function checkPlan(text, { complete = false } = {}) {
60
+ // `Scope roots: <path> (<action>, <action>)`: an outside folder the plan's todos reach, granted up front.
61
+ export const SCOPE_ROOT_LINE = /^Scope roots?:[ \t]*(\S+)[ \t]*\(([a-z, -]+)\)[ \t]*$/u;
62
+ // An absolute path with at least two segments. Inside an inline code span only a span that is one bare
63
+ // path counts; a span holding spaces is a quoted command, which is data rather than a folder.
64
+ const ABSOLUTE_PATH = /(?<![\w.~/-])\/[\w.@+%,=~-]+(?:\/[\w.@+%,=~-]+)+\/?/gu;
65
+ const pathsIn = (line) => line
66
+ .replace(/`([^`]*)`/gu, (_, span) => (/\s/u.test(span.trim()) ? ' ' : ` ${span.trim()} `))
67
+ .match(ABSOLUTE_PATH) ?? [];
68
+
69
+ const realOr = (path) => {
70
+ try {
71
+ return realpathSync(path);
72
+ } catch {
73
+ return path;
74
+ }
75
+ };
76
+ const within = (root, path) => {
77
+ const rel = relative(root, path);
78
+ return rel === '' || (!rel.startsWith('..') && !isAbsolute(rel));
79
+ };
80
+ const existingFolder = (path) => {
81
+ for (let at = path; ; at = dirname(at)) {
82
+ try {
83
+ if (statSync(at).isDirectory()) return at;
84
+ } catch { /* not there: climb */ }
85
+ if (dirname(at) === at) return at;
86
+ }
87
+ };
88
+
89
+ /**
90
+ * Absolute paths named under `## Todos` (up to `## Final verification wave`, fences skipped) that lie
91
+ * outside `projectRoot` and outside every `Scope roots:` line, one entry per folder: `{path, suggestion}`
92
+ * with the line to add, or `{path, problem}` when the nearest existing folder is too broad to grant.
93
+ */
94
+ export function uncoveredScopePaths(text, projectRoot) {
95
+ const project = realOr(resolve(projectRoot));
96
+ const lines = visibleLines(text);
97
+ const granted = lines.map(({ line }) => SCOPE_ROOT_LINE.exec(line.trim())).filter(Boolean)
98
+ .map(([, path]) => realOr(isAbsolute(path) ? path : resolve(project, path)));
99
+ const found = new Map();
100
+ let todos = false;
101
+ for (const { line } of lines) {
102
+ const heading = line.trimEnd();
103
+ if (heading === '## Todos') todos = true;
104
+ else if (/^## /u.test(heading)) todos = false;
105
+ if (!todos) continue;
106
+ for (const raw of pathsIn(line)) {
107
+ const path = raw.replace(/[.,:;)]+$/u, '');
108
+ const folder = realOr(existingFolder(path));
109
+ const covered = [realOr(path), folder].some((p) => within(project, p) || granted.some((root) => within(root, p)));
110
+ if (covered || found.has(folder)) continue;
111
+ const tooBroad = dirname(folder) === dirname(dirname(folder)) || folder === realOr(homedir());
112
+ found.set(folder, tooBroad
113
+ ? { path, problem: `${path} is outside the project; name a narrower existing folder on a Scope roots line` }
114
+ : { path, suggestion: `Scope roots: ${folder} (read)` });
115
+ }
116
+ }
117
+ return [...found.values()];
118
+ }
119
+
120
+ export function checkPlan(text, { complete = false, projectRoot = null } = {}) {
60
121
  const problems = [];
61
122
  const lines = visibleLines(text);
62
123
 
@@ -115,6 +176,10 @@ export function checkPlan(text, { complete = false } = {}) {
115
176
 
116
177
  if (/<fill\b/u.test(text)) problems.push('unfilled <fill ...> placeholders remain: the plan is not decision-complete');
117
178
 
179
+ if (projectRoot) {
180
+ for (const entry of uncoveredScopePaths(text, projectRoot)) problems.push(entry.problem ?? `${entry.path} is outside the project and no Scope roots line covers it; add: ${entry.suggestion}`);
181
+ }
182
+
118
183
  if (complete) {
119
184
  for (const { line } of marked) {
120
185
  if (/^- \[ \] /u.test(line)) problems.push(`row still open: ${line.trim()}`);
@@ -138,7 +203,9 @@ function checkFile(planPath, options) {
138
203
  }
139
204
  if (stat.isSymbolicLink() || !stat.isFile()) return fail(`FAIL PLAN_UNSAFE: ${path} must be a regular file`);
140
205
  if (stat.size > MAX_PLAN_BYTES) return fail(`FAIL PLAN_TOO_LARGE: ${path}`);
141
- const result = checkPlan(readFileSync(path, 'utf8'), options);
206
+ // The project is the folder holding plans/; a plan kept elsewhere is checked against the cwd.
207
+ const projectRoot = basename(dirname(path)) === 'plans' ? dirname(dirname(path)) : process.cwd();
208
+ const result = checkPlan(readFileSync(path, 'utf8'), { ...options, projectRoot });
142
209
  if (!result.ok) {
143
210
  for (const problem of result.problems) process.stderr.write(`FAIL ${problem}\n`);
144
211
  return 1;
@@ -1,27 +1,14 @@
1
1
  // state-root.mjs: where LitMuse keeps project-local working state.
2
2
  // State belongs at the outermost Git root (or the start directory when there is no repository), so a nested
3
- // package never grows its own .litmuse folder. Node builtins only.
3
+ // package never grows its own .litmuse folder; a linked worktree keeps its own. The rule lives once, in the
4
+ // hooks' workflow-stack module, so team state lands where the hooks and the status card look.
4
5
 
5
- import { closeSync, constants, existsSync, lstatSync, openSync, writeSync } from "node:fs";
6
+ import { closeSync, constants, lstatSync, openSync, writeSync } from "node:fs";
6
7
  import { basename, dirname, join, resolve } from "node:path";
7
8
 
8
- export const PROJECT_STATE_DIR = ".litmuse";
9
-
10
- const outermostGitRoot = (startDir) => {
11
- let root = null;
12
- let current = startDir;
13
- for (;;) {
14
- if (existsSync(join(current, ".git"))) root = current;
15
- const parent = dirname(current);
16
- if (parent === current) return root;
17
- current = parent;
18
- }
19
- };
9
+ export { resolveProjectStateRoot } from "../../../hooks/lib/workflow-stack.mjs";
20
10
 
21
- export const resolveProjectStateRoot = (startDir = process.cwd()) => {
22
- const start = resolve(startDir);
23
- return outermostGitRoot(start) ?? start;
24
- };
11
+ export const PROJECT_STATE_DIR = ".litmuse";
25
12
 
26
13
  const STATE_GITIGNORE = "# Created by LitMuse: local working state, not for commits.\n*\n";
27
14
 
@@ -74,7 +74,10 @@ okay. When the two disagree, ask.
74
74
 
75
75
  ### Authority envelope
76
76
 
77
- The envelope is the plan's Scope plus the user's messages. Work inside it freely:
77
+ The envelope is the plan's Scope, its `Scope roots: <path> (<actions>)` lines and the user's messages. A
78
+ `Scope roots` line grants exactly its listed actions under that folder outside the workspace; a todo that reaches
79
+ an uncovered outside folder fails the structure check (`check-plan.mjs --check` prints the line to add), so ask
80
+ the user before treating that folder as in scope. Work inside the envelope freely:
78
81
 
79
82
  - read: any file in the workspace;
80
83
  - write: the paths on the current row's Files line, new test files for that row, `.litmuse/` records, and the one