@refactco/refact-os 1.5.0

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 (61) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/LICENSE +21 -0
  3. package/README.md +162 -0
  4. package/bin/refact-os.js +154 -0
  5. package/lib/adapters.js +302 -0
  6. package/lib/company.js +76 -0
  7. package/lib/frontmatter.js +30 -0
  8. package/lib/migrate.js +116 -0
  9. package/lib/project-utils.js +179 -0
  10. package/lib/refact-config.js +324 -0
  11. package/lib/scaffold.js +329 -0
  12. package/lib/validate.js +145 -0
  13. package/package.json +46 -0
  14. package/templates/base/AGENTS.md +9 -0
  15. package/templates/base/CLAUDE.md +3 -0
  16. package/templates/base/README.md +54 -0
  17. package/templates/base/agent/AGENTS.md +60 -0
  18. package/templates/base/agent/CLAUDE.md +7 -0
  19. package/templates/base/agent/claude-hooks.json +32 -0
  20. package/templates/base/agent/hooks/claude-sync-transcript.py +236 -0
  21. package/templates/base/agent/hooks/preflight-metadata.mjs +202 -0
  22. package/templates/base/agent/hooks/send-transcript-to-remote-server.py +238 -0
  23. package/templates/base/agent/hooks/sync-chat-transcript.py +188 -0
  24. package/templates/base/agent/hooks.json +29 -0
  25. package/templates/base/agent/scripts/import-project-chat-history.py +196 -0
  26. package/templates/base/agent/scripts/sync-asana.mjs +408 -0
  27. package/templates/base/agent/skills/adopt/SKILL.md +46 -0
  28. package/templates/base/agent/skills/close-ticket/SKILL.md +31 -0
  29. package/templates/base/agent/skills/extract-learnings/SKILL.md +90 -0
  30. package/templates/base/agent/skills/git-it/SKILL.md +138 -0
  31. package/templates/base/agent/skills/import-chat-history/SKILL.md +85 -0
  32. package/templates/base/agent/skills/ingest-input/SKILL.md +43 -0
  33. package/templates/base/agent/skills/open-ticket/SKILL.md +36 -0
  34. package/templates/base/agent/skills/process-docs/SKILL.md +69 -0
  35. package/templates/base/agent/skills/project-status/SKILL.md +35 -0
  36. package/templates/base/agent/skills/project-status/scripts/scan-status.mjs +153 -0
  37. package/templates/base/agent/skills/refact/SKILL.md +139 -0
  38. package/templates/base/agent/skills/setup-project/SKILL.md +140 -0
  39. package/templates/base/agent/skills/sync-asana/SKILL.md +106 -0
  40. package/templates/base/agent/skills/update-canonical-record/SKILL.md +28 -0
  41. package/templates/base/agent/skills/update-package/SKILL.md +51 -0
  42. package/templates/base/docs/context/project.md +30 -0
  43. package/templates/base/docs/decisions.md +22 -0
  44. package/templates/base/docs/index.md +31 -0
  45. package/templates/base/docs/sources/raw/.gitkeep +0 -0
  46. package/templates/base/docs/task/.gitkeep +0 -0
  47. package/templates/base/env.example +14 -0
  48. package/templates/base/gitignore +34 -0
  49. package/templates/overlays/client/agent/skills/create-deliverable/SKILL.md +29 -0
  50. package/templates/overlays/client/docs/deliverables/.gitkeep +0 -0
  51. package/templates/overlays/code/agent/skills/add-codebase/SKILL.md +239 -0
  52. package/templates/overlays/code/agent/skills/code-development/SKILL.md +58 -0
  53. package/templates/overlays/code/agent/skills/code-development/references/gitflow.md +144 -0
  54. package/templates/overlays/nextjs/agent/skills/nextjs-dev/SKILL.md +93 -0
  55. package/templates/overlays/nextjs/agent/skills/setup-netlify-deploy/SKILL.md +143 -0
  56. package/templates/overlays/nextjs/agent/skills/setup-nextjs-app/SKILL.md +118 -0
  57. package/templates/overlays/nextjs/agent/skills/setup-vercel-deploy/SKILL.md +116 -0
  58. package/templates/overlays/wordpress/agent/skills/install-wp-skills/SKILL.md +130 -0
  59. package/templates/overlays/wordpress/agent/skills/setup-kinsta-deploy/SKILL.md +201 -0
  60. package/templates/overlays/wordpress/agent/skills/wp-env/SKILL.md +478 -0
  61. package/templates/overlays/wordpress/wp-cli.yml.example +46 -0
@@ -0,0 +1,138 @@
1
+ ---
2
+ name: git-it
3
+ description: Initialize the git repo, make the first commit, and create the GitHub remote.
4
+ pattern: procedure
5
+ requires_approval: true
6
+ when_to_use: /refact git it | set up the repo | create the remote | first commit | publish to GitHub.
7
+ when_not_to_use: Feature work on an existing repo (use code-development).
8
+ next_skills: []
9
+ sub_agents: []
10
+ ---
11
+
12
+ # Git It Reference
13
+
14
+ Use this reference when the user invokes `/refact git it` (or asks to "set up the repo", "create the remote", "make the first commit", "publish to GitHub").
15
+
16
+ ## Goal
17
+
18
+ Get this project from "no git history" to "pushed to a fresh GitHub repo" with one guided flow:
19
+ 1. Make sure `gh` is installed and authenticated.
20
+ 2. Ask the user 4 short questions.
21
+ 3. `git init` if needed, make the first commit if there isn't one, `gh repo create`, push.
22
+
23
+ ## Step 1 — Prerequisites
24
+
25
+ ### 1a. Check for the GitHub CLI
26
+
27
+ ```bash
28
+ command -v gh
29
+ ```
30
+
31
+ If it's missing, **detect the OS** and **show the right install command**, then **ask the user for permission** before running it. Never run install commands autonomously (especially anything with `sudo`).
32
+
33
+ | OS | Suggested install |
34
+ |---|---|
35
+ | macOS (Homebrew) | `brew install gh` |
36
+ | Debian / Ubuntu | follow https://cli.github.com/manual/installation (uses an apt repository, requires sudo) |
37
+ | Fedora / RHEL | `sudo dnf install gh` |
38
+ | Arch | `sudo pacman -S github-cli` |
39
+ | Windows (winget) | `winget install --id GitHub.cli` |
40
+
41
+ Detect OS with `uname -s` (Darwin / Linux) and, on Linux, `cat /etc/os-release` for the distro.
42
+
43
+ If the user declines or you can't auto-install, stop here and ask them to install `gh` and re-run `/refact git it`.
44
+
45
+ ### 1b. Check authentication
46
+
47
+ ```bash
48
+ gh auth status
49
+ ```
50
+
51
+ If it reports "not logged in" or fails: stop and tell the user to run `gh auth login` themselves — it's an interactive OAuth flow you can't drive for them. Once they confirm they've logged in, continue.
52
+
53
+ ## Step 2 — Ask the user 4 questions
54
+
55
+ Ask one at a time; accept the suggested default if the user just confirms.
56
+
57
+ ### Q1: Project name
58
+
59
+ - **Default suggestion:** humanize the current directory name. e.g. `flower-shop` → "Flower Shop".
60
+ - Use this for the eventual repo description and the README's H1 if it still has `<TODO: project name>`.
61
+
62
+ ### Q2: Slug (repo name on GitHub)
63
+
64
+ - **Default suggestion:** lowercase the project name, replace any non-alphanumeric run with a single `-`, strip leading/trailing `-`.
65
+ - The slug must be valid for GitHub (`^[a-zA-Z0-9._-]+$`). If it isn't, suggest a corrected one and re-ask.
66
+
67
+ ### Q3: Visibility
68
+
69
+ - **Default: `private`.**
70
+ - Other valid choice: `public`.
71
+ - Don't offer `internal` unless the user specifically asks for it.
72
+
73
+ ### Q4: Owner
74
+
75
+ Run:
76
+
77
+ ```bash
78
+ gh api user/orgs --jq '.[].login'
79
+ ```
80
+
81
+ - If the list is empty → owner is the authenticated user. Get it with `gh api user --jq '.login'`. No question needed; just confirm.
82
+ - If the list has one or more orgs → present `<user>, <org1>, <org2>, …` and ask which one. Default to the personal account.
83
+
84
+ ## Step 3 — Execute
85
+
86
+ Run the steps below in order. Stop on the first error and surface it; do not retry destructively.
87
+
88
+ ### 3a. Initialize git if needed
89
+
90
+ ```bash
91
+ test -d .git || git init -b main
92
+ ```
93
+
94
+ If `.git` already exists, leave the current branch alone.
95
+
96
+ ### 3b. Ensure a first commit exists
97
+
98
+ Check:
99
+
100
+ ```bash
101
+ git rev-parse --verify HEAD 2>/dev/null
102
+ ```
103
+
104
+ If that fails (no commits yet):
105
+
106
+ ```bash
107
+ git add -A
108
+ git commit -m "chore: initial commit (refact-os scaffold)"
109
+ ```
110
+
111
+ If commits already exist, **do not** create a synthetic "initial" commit on top — just continue.
112
+
113
+ ### 3c. Create the remote and push
114
+
115
+ ```bash
116
+ gh repo create <owner>/<slug> --<visibility> --source=. --remote=origin --push --description "<humanized project name>"
117
+ ```
118
+
119
+ - `<visibility>` is literally `--private` or `--public`.
120
+ - If a remote named `origin` already exists locally, do **not** overwrite it. Stop and ask the user.
121
+ - If `gh` reports the slug is taken on the chosen owner, surface the exact error and ask for a different slug. Do not invent a fallback name yourself.
122
+
123
+ ### 3d. Report
124
+
125
+ Print:
126
+
127
+ - Remote URL: `gh repo view --json url --jq .url`
128
+ - First commit hash + subject: `git log -1 --format='%h %s'`
129
+ - Two suggested next steps: "invite collaborators with `gh repo edit --add-collaborator …`" and "set branch protection in the GitHub UI".
130
+
131
+ ## Guardrails
132
+
133
+ - **Never** run `sudo` autonomously. Always ask permission first.
134
+ - **Never** force-push. If `git push` fails, stop and surface the error.
135
+ - **Never** overwrite an existing `origin` remote.
136
+ - **Never** commit `.env`, `*.pem`, `*.key`, or anything matched by `.gitignore`. The generated `.gitignore` covers these, but verify by inspecting `git status` before the first commit.
137
+ - **Never** invent the slug or organization on the user's behalf — always show the suggestion and let them confirm or override.
138
+ - If the working tree has nothing to commit AND no prior commits exist, stop and ask the user to add something first — `gh repo create --source=.` requires a non-empty initial commit.
@@ -0,0 +1,85 @@
1
+ ---
2
+ name: import-chat-history
3
+ description: Import the project's Claude Code / Cursor chat history into docs/sources/raw/agent-transcripts/.
4
+ pattern: procedure
5
+ when_to_use: /refact get chat history | import chats.
6
+ when_not_to_use: Saving brand-new inbound material (use ingest-input) — this is only for agent chat logs.
7
+ next_skills:
8
+ - process-docs
9
+ sub_agents: []
10
+ ---
11
+
12
+ # Chat History Reference
13
+
14
+ Backfill this project's agent chats into:
15
+
16
+ - `docs/sources/raw/agent-transcripts/`
17
+
18
+ Use this reference when handling requests such as `/refact get chat history` or `/refact import chats`.
19
+
20
+ > New **Claude Code** chats are mirrored here automatically by the
21
+ > `claude-sync-transcript` hook (on every Stop). This script is for backfilling
22
+ > history that predates the hook, and for importing **Cursor** chats. It is
23
+ > local-only — nothing is sent to the remote server.
24
+
25
+ ## Script
26
+
27
+ - `.claude/scripts/import-project-chat-history.py` (or `.cursor/scripts/…` — identical copies)
28
+
29
+ ## Default behavior
30
+
31
+ With `--tool auto` (the default) the script imports from whichever sources exist
32
+ for this repo, auto-detecting both:
33
+
34
+ - Claude Code: `~/.claude/projects/<encoded-cwd>/*.jsonl` (full native transcripts;
35
+ `<encoded-cwd>` is the absolute repo path with `/` and `.` replaced by `-`)
36
+ - Cursor: `~/.cursor/projects/<project-key>/agent-transcripts/*.jsonl`
37
+
38
+ ## Usage
39
+
40
+ From project root:
41
+
42
+ ```bash
43
+ npm run chats:import
44
+ # or:
45
+ python3 .claude/scripts/import-project-chat-history.py
46
+ ```
47
+
48
+ Restrict to one tool:
49
+
50
+ ```bash
51
+ python3 .claude/scripts/import-project-chat-history.py --tool claude
52
+ python3 .claude/scripts/import-project-chat-history.py --tool cursor
53
+ ```
54
+
55
+ Dry run:
56
+
57
+ ```bash
58
+ npm run chats:import:dry
59
+ # or:
60
+ python3 .claude/scripts/import-project-chat-history.py --dry-run
61
+ ```
62
+
63
+ Custom source (overrides auto-detection):
64
+
65
+ ```bash
66
+ python3 .claude/scripts/import-project-chat-history.py --source "/absolute/path/to/transcripts"
67
+ ```
68
+
69
+ Custom owner for generated meta files:
70
+
71
+ ```bash
72
+ python3 .claude/scripts/import-project-chat-history.py --owner "Owner Name"
73
+ ```
74
+
75
+ ## Optional environment variables
76
+
77
+ - `CLAUDE_PROJECT_TRANSCRIPTS_DIR`: override the Claude Code source directory.
78
+ - `CURSOR_PROJECT_TRANSCRIPTS_DIR`: override the Cursor source directory.
79
+ - `REFACT_CHAT_OWNER` / `CURSOR_CHAT_OWNER`: default owner for generated `.meta.json` files.
80
+
81
+ ## What it imports
82
+
83
+ - Copies all `*.jsonl` chat transcript files from the detected source(s) to the destination.
84
+ - Updates files only when content changed (SHA-256 compare).
85
+ - Creates `<chat-id>.meta.json` when missing (includes `session_id`, `owner`, `tool`, and source info).
@@ -0,0 +1,43 @@
1
+ ---
2
+ name: ingest-input
3
+ description: Classify and save any inbound material (email, transcript, deck, file, RFP, chat) into docs/sources/raw/ with a dated filename and a small frontmatter header, then suggest the next move.
4
+ pattern: procedure
5
+ when_to_use: The user pastes or points at new inbound material — "here's an email from the client", "save this transcript", "a new RFP came in", "add this file".
6
+ when_not_to_use: For material that already lives in docs/sources/raw/ (just read it), or when the user wants curated truth updated (use update-canonical-record), or to open a ticket (use open-ticket).
7
+ inputs:
8
+ - the pasted or referenced inbound material
9
+ outputs:
10
+ - a dated file under docs/sources/raw/<class>/ with a 3-line header
11
+ next_skills:
12
+ - open-ticket # if the material implies trackable work
13
+ - update-canonical-record # if it changes curated truth
14
+ sub_agents: []
15
+ ---
16
+
17
+ # Ingest Input
18
+
19
+ The universal entry point for any material arriving from outside the codebase. Capture it as Evidence *before* acting on it — agents work from saved files, not chat memory.
20
+
21
+ ## Steps
22
+
23
+ 1. **Classify the input type** and pick the destination + extension:
24
+ - email → `docs/sources/raw/email/<yyyy-mm-dd>-<slug>.email.md`
25
+ - meeting/call transcript → `docs/sources/raw/call-transcripts/<yyyy-mm-dd>-<slug>.transcript.md`
26
+ - agent chat history → `docs/sources/raw/agent-transcripts/` (usually synced by the hook; only save by hand if pasted)
27
+ - document / deck / RFP / misc file → `docs/sources/raw/<yyyy-mm-dd>-<slug>.<type>.md`
28
+ 2. **Write the file** with a 3-line header:
29
+ ```yaml
30
+ ---
31
+ source: gmail | fathom | asana | other
32
+ added-by: <name or "agent">
33
+ processed: false
34
+ ---
35
+ ```
36
+ 3. **Never edit** raw evidence after saving — it is received state.
37
+ 4. **Detect the pattern** in the content (quote request, scope change, bug report, decision) and surface the relevant `next_skills` to the user.
38
+
39
+ ## Hard rules
40
+
41
+ - Raw is evidence: write once, never rewrite.
42
+ - One file per item. Date-prefix the filename.
43
+ - Bucket by source class only when volume earns it; flat files under `raw/` are fine for light projects.
@@ -0,0 +1,36 @@
1
+ ---
2
+ name: open-ticket
3
+ description: Create a tracked ticket under docs/task/open/ with frontmatter, linking the source evidence that prompted it.
4
+ pattern: procedure
5
+ when_to_use: A piece of work needs tracking — a client request, a bug, a scoped task — and there isn't already an open ticket for it.
6
+ when_not_to_use: For closing a ticket (use close-ticket), or for work small enough to finish in the same turn without tracking.
7
+ inputs:
8
+ - the source evidence file under docs/sources/raw/ (if any)
9
+ outputs:
10
+ - docs/task/open/<yyyy-mm-dd>-<slug>.md with status frontmatter
11
+ next_skills: []
12
+ sub_agents: []
13
+ ---
14
+
15
+ # Open Ticket
16
+
17
+ ## Steps
18
+
19
+ 1. Create `docs/task/open/<yyyy-mm-dd>-<slug>.md`.
20
+ 2. Add frontmatter:
21
+ ```yaml
22
+ ---
23
+ date: <yyyy-mm-dd>
24
+ status: open
25
+ description: <one line: what needs doing and why>
26
+ source: <path to docs/sources/raw/... if this came from inbound material>
27
+ ---
28
+ ```
29
+ 3. Write a short body: the ask, acceptance criteria, and any links.
30
+ 4. Tell the user the ticket path.
31
+
32
+ ## Notes
33
+
34
+ - One markdown file per ticket. `docs/task/open/` holds active tickets.
35
+ - Status transitions (`open → in-progress`) are frontmatter, not folder moves.
36
+ - When the ticket closes, hand off to `close-ticket`.
@@ -0,0 +1,69 @@
1
+ ---
2
+ name: process-docs
3
+ description: Walk unprocessed files under docs/sources/raw/, integrate them into docs/context/, and flip the processed flag.
4
+ pattern: procedure
5
+ when_to_use: /refact process docs | ingest new emails | digest new inputs.
6
+ when_not_to_use: Saving material that hasn't been captured yet (use ingest-input first).
7
+ next_skills: []
8
+ sub_agents: []
9
+ ---
10
+
11
+ # Process Docs Reference
12
+
13
+ Use this reference when the user invokes `/refact process-docs` (or asks to process / ingest / digest new docs).
14
+
15
+ ## What "processing" means
16
+
17
+ Every file under `docs/` (except `agent-transcripts/`, which is for raw chat history) has a 3-line YAML header with a `processed` flag. "Processing" means reading an unprocessed file, integrating its information into `docs/context/`, and flipping the flag.
18
+
19
+ ## Workflow
20
+
21
+ ### 1. Find unprocessed files
22
+
23
+ Don't hand-walk folders to find them — "which files are unprocessed" is a deterministic question, so let a script answer it (a model eyeballing folders is right most times and silently off once). Run the shared scanner (the same one `project-status` uses) and read its `unprocessed.files` list:
24
+
25
+ ```bash
26
+ node agent/skills/project-status/scripts/scan-status.mjs --json
27
+ ```
28
+
29
+ Every path in `unprocessed.files` is a `.md` file with `processed: false` somewhere under `docs/` (raw chat logs in `agent-transcripts/` are already excluded). That list is your work queue for the steps below — the *judgment* about what each file means is yours; the *enumeration* is not.
30
+
31
+ ### 2. For each unprocessed file, decide what to update
32
+
33
+ Read the file. Then update one or more of these, as appropriate:
34
+
35
+ - **`docs/decisions.md`** — if the file records a finalized decision. Include the source file path under `docs/` as part of the **Data** field of the entry.
36
+ - **`docs/context/open-decisions.md`** — if the file raises a question, ambiguity, or request that needs a human's call. Tag the responsible person from `docs/context/people.md`. If the right person isn't in roles, ask the user.
37
+ - **`docs/context/people.md`** — if the file mentions a new person on either team. Append a bullet with name + role.
38
+ - **`docs/context/learnings.md`** — if the file contains a non-obvious project/customer preference or convention worth remembering.
39
+
40
+ A single file can update multiple `docs/context/` files. Some files may update none — if the content is purely informational and not actionable, no `docs/context/` change is needed, but you still flip the header (see step 3).
41
+
42
+ ### 3. Flip the header
43
+
44
+ After processing, change the file's header from `processed: false` to `processed: true`. Leave `source` and `added-by` untouched.
45
+
46
+ ### 4. Report
47
+
48
+ Print a concise summary at the end:
49
+
50
+ ```
51
+ Processed N files.
52
+
53
+ decisions.md: +X entries
54
+ open-decisions.md: +Y entries
55
+ people.md: +Z entries
56
+ learnings.md: +W bullets
57
+
58
+ Files processed:
59
+ - docs/sources/raw/email/2026-05-09-customer-feedback.md
60
+ - docs/sources/raw/call-transcripts/2026-05-10-weekly-sync.md
61
+ - ...
62
+ ```
63
+
64
+ ## Guardrails
65
+
66
+ - **Never** add an entry to `decisions.md` without including the source `docs/` file path as part of the **Data** field. Traceability is the whole point of that file.
67
+ - **Never** invent a person for `people.md`. If a name appears in a doc but the role is unclear, surface it as an open question instead.
68
+ - **Never** flip `processed: true` if you skipped the file due to an error — leave the flag as-is so it gets retried next time.
69
+ - If a file's content seems duplicated against an existing `docs/context/` entry, prefer **updating** the existing entry over adding a new one.
@@ -0,0 +1,35 @@
1
+ ---
2
+ name: project-status
3
+ description: Report what's unprocessed, open decisions and their owners, recent learnings, and unfilled placeholders.
4
+ pattern: procedure
5
+ when_to_use: /refact status | what's pending | what's unprocessed.
6
+ when_not_to_use: Making a change — this is read-only reporting.
7
+ next_skills: []
8
+ sub_agents: []
9
+ ---
10
+
11
+ # Status Reference
12
+
13
+ Use this when the user invokes `/refact status` (or asks "what's the status of the project context?").
14
+
15
+ ## How it works
16
+
17
+ The scan — counting unprocessed files, open decisions, and role placeholders, and pulling recent learnings — is **deterministic work, so a script does it**, not the model. Eyeballing folders and counting by hand is exactly the kind of mechanical task a model gets *plausibly* wrong (right most times, silently off once). Run:
18
+
19
+ ```bash
20
+ node agent/skills/project-status/scripts/scan-status.mjs
21
+ ```
22
+
23
+ It prints a ready-to-show snapshot: unprocessed docs (grouped by folder), open decisions, recent learnings, and unfilled role placeholders. Add `--json` if you want to post-process the facts instead of showing them.
24
+
25
+ ## What you do
26
+
27
+ 1. Run the script from the repo root.
28
+ 2. Present its output as the snapshot — it's already under ~20 lines and human-readable.
29
+ 3. **Then** add the one thing the script can't: judgment. If something stands out — a pile of unprocessed inbound, a decision that's been open a long time, roles still unfilled — say so in a line and suggest the next move (`/refact process docs`, or resolving a specific decision). That interpretation is the only part of this skill that belongs to you.
30
+
31
+ ## Scope
32
+
33
+ - This reports project **context** state, not repo health. Adapter drift, missing skill frontmatter, and structure checks are `refact-os validate`'s job — don't duplicate them here.
34
+ - Read-only. Don't modify any files.
35
+ - If the script reports a file as "not present," relay that — it's normal early in a project, since those `docs/context/` files are earned, not seeded.
@@ -0,0 +1,153 @@
1
+ #!/usr/bin/env node
2
+ // Deterministic status scan for the project-status skill (`/refact status`).
3
+ //
4
+ // Counts unprocessed docs, open decisions, and role placeholders, and lists the
5
+ // most recent learnings — the mechanical facts the model must NOT eyeball-count
6
+ // (see the standard's "Latent vs. Deterministic Work"). The skill body runs this
7
+ // and adds interpretation on top; it does not re-derive these numbers by hand.
8
+ //
9
+ // node agent/skills/project-status/scripts/scan-status.mjs # text snapshot
10
+ // node agent/skills/project-status/scripts/scan-status.mjs --json # machine-readable
11
+ //
12
+ // Paths resolve relative to this file, so it works whether it's run from the
13
+ // canonical agent/ copy or a generated .cursor/ / .claude/ mirror. Repo-structure
14
+ // health (adapter drift, skill frontmatter) is `refact-os validate`'s job, not
15
+ // this script's — this reports project *context* state only.
16
+
17
+ import fs from "node:fs";
18
+ import path from "node:path";
19
+ import { fileURLToPath } from "node:url";
20
+
21
+ const scriptDir = path.dirname(fileURLToPath(import.meta.url));
22
+ const root = path.resolve(scriptDir, "../../../..");
23
+ const docs = path.join(root, "docs");
24
+ const asJson = process.argv.includes("--json");
25
+
26
+ function read(p) {
27
+ try {
28
+ return fs.readFileSync(p, "utf8");
29
+ } catch {
30
+ return null;
31
+ }
32
+ }
33
+
34
+ function walk(dir, acc = []) {
35
+ let entries;
36
+ try {
37
+ entries = fs.readdirSync(dir, { withFileTypes: true });
38
+ } catch {
39
+ return acc;
40
+ }
41
+ for (const e of entries) {
42
+ const full = path.join(dir, e.name);
43
+ if (e.isDirectory()) {
44
+ if (e.name === "agent-transcripts") continue; // raw chat logs — never "processed"
45
+ walk(full, acc);
46
+ } else if (e.name.endsWith(".md")) {
47
+ acc.push(full);
48
+ }
49
+ }
50
+ return acc;
51
+ }
52
+
53
+ // 1. Unprocessed docs: files whose frontmatter carries `processed: false`.
54
+ // `byFolder` powers the snapshot count; `files` is the exact list process-docs
55
+ // consumes so it never has to hand-walk folders either.
56
+ const unprocessed = {};
57
+ const unprocessedFiles = [];
58
+ for (const file of walk(docs)) {
59
+ const head = (read(file) || "").slice(0, 800);
60
+ if (/^processed:\s*false\s*$/m.test(head)) {
61
+ const label = path.basename(path.dirname(file));
62
+ unprocessed[label] = (unprocessed[label] || 0) + 1;
63
+ unprocessedFiles.push(path.relative(root, file));
64
+ }
65
+ }
66
+ const unprocessedTotal = unprocessedFiles.length;
67
+
68
+ // 2. Open decisions: dated bullet entries in docs/context/open-decisions.md.
69
+ const openDecRaw = read(path.join(docs, "context", "open-decisions.md"));
70
+ const openDecisions = [];
71
+ if (openDecRaw != null) {
72
+ for (const line of openDecRaw.split("\n")) {
73
+ const m = line.match(/^-\s+(\d{4}-\d{2}-\d{2}\b.*)$/);
74
+ if (m) openDecisions.push(m[1].trim());
75
+ }
76
+ }
77
+
78
+ // 3. Recent learnings: newest 3 bullets under "## Entries" (newest-first by convention).
79
+ const learnRaw = read(path.join(docs, "context", "learnings.md"));
80
+ const learnings = [];
81
+ if (learnRaw != null) {
82
+ const idx = learnRaw.indexOf("## Entries");
83
+ const body = idx >= 0 ? learnRaw.slice(idx) : learnRaw;
84
+ for (const line of body.split("\n")) {
85
+ const m = line.match(/^-\s+(.+)$/);
86
+ if (m) {
87
+ learnings.push(m[1].trim());
88
+ if (learnings.length >= 3) break;
89
+ }
90
+ }
91
+ }
92
+
93
+ // 4. Role placeholders: unfilled <TODO> markers in docs/context/people.md.
94
+ const peopleRaw = read(path.join(docs, "context", "people.md"));
95
+ const rolePlaceholders = peopleRaw == null ? null : (peopleRaw.match(/<TODO>/g) || []).length;
96
+
97
+ const missing = [
98
+ openDecRaw == null && "docs/context/open-decisions.md",
99
+ learnRaw == null && "docs/context/learnings.md",
100
+ peopleRaw == null && "docs/context/people.md",
101
+ ].filter(Boolean);
102
+
103
+ if (asJson) {
104
+ process.stdout.write(
105
+ `${JSON.stringify(
106
+ {
107
+ unprocessed: { total: unprocessedTotal, byFolder: unprocessed, files: unprocessedFiles },
108
+ openDecisions: openDecRaw == null ? null : openDecisions,
109
+ recentLearnings: learnRaw == null ? null : learnings,
110
+ rolePlaceholders,
111
+ missing,
112
+ },
113
+ null,
114
+ 2,
115
+ )}\n`,
116
+ );
117
+ } else {
118
+ const lines = [];
119
+ if (unprocessedTotal === 0) {
120
+ lines.push("Unprocessed: all docs processed.");
121
+ } else {
122
+ const parts = Object.entries(unprocessed).map(([k, v]) => `${v} ${k}`);
123
+ lines.push(`Unprocessed: ${parts.join(", ")} (${unprocessedTotal} total).`);
124
+ }
125
+
126
+ if (openDecRaw == null) {
127
+ lines.push("Open decisions: (docs/context/open-decisions.md not present).");
128
+ } else if (openDecisions.length === 0) {
129
+ lines.push("Open decisions: none.");
130
+ } else {
131
+ lines.push(`Open decisions: ${openDecisions.length}.`);
132
+ for (const d of openDecisions) lines.push(` - ${d}`);
133
+ }
134
+
135
+ if (learnRaw == null) {
136
+ lines.push("Recent learnings: (docs/context/learnings.md not present).");
137
+ } else if (learnings.length === 0) {
138
+ lines.push("Recent learnings: none yet.");
139
+ } else {
140
+ lines.push("Recent learnings:");
141
+ for (const l of learnings) lines.push(` - ${l}`);
142
+ }
143
+
144
+ if (peopleRaw == null) {
145
+ lines.push("Roles: (docs/context/people.md not present).");
146
+ } else if (rolePlaceholders > 0) {
147
+ lines.push(`Roles: ${rolePlaceholders} <TODO> placeholder(s) in people.md.`);
148
+ } else {
149
+ lines.push("Roles: all filled.");
150
+ }
151
+
152
+ process.stdout.write(`${lines.join("\n")}\n`);
153
+ }