chore(wolf): checkpoint buglog.json (1062 bugs)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AgYRU3zDTqNGV5Jk8aiKLg
This commit is contained in:
parent
13246a89c8
commit
d8bf8fb2e1
27 changed files with 7702 additions and 897158 deletions
|
|
@ -1,15 +1,10 @@
|
|||
---
|
||||
description: OpenWolf protocol enforcement — active on all files
|
||||
description: OpenWolf protocol enforcement, active on all files
|
||||
globs: **/*
|
||||
---
|
||||
|
||||
- Check .wolf/anatomy.md before reading any project file
|
||||
- Check .wolf/cerebrum.md Do-Not-Repeat list before generating code
|
||||
- After writing or editing files, update .wolf/anatomy.md and append to .wolf/memory.md
|
||||
- After receiving a user correction, update .wolf/cerebrum.md immediately (Preferences, Learnings, or Do-Not-Repeat)
|
||||
- LEARN from every interaction: if you discover a convention, user preference, or project pattern, add it to .wolf/cerebrum.md. Low threshold — when in doubt, log it.
|
||||
- BEFORE fixing any bug or error: read .wolf/buglog.json for known fixes
|
||||
- AFTER fixing any bug, error, failed test, failed build, or user-reported problem: ALWAYS log to .wolf/buglog.json with error_message, root_cause, fix, and tags
|
||||
- If you edit a file more than twice in a session, that likely indicates a bug — log it to .wolf/buglog.json
|
||||
- When the user asks to check/evaluate UI design: run `openwolf designqc` to capture screenshots, then read them from .wolf/designqc-captures/
|
||||
- When the user asks to change/pick/migrate UI framework: read .wolf/reframe-frameworks.md, ask decision questions, recommend a framework, then execute with the framework's prompt
|
||||
- To locate a symbol or file, run `openwolf find <name>` first (ranked shortlist, under 1k tokens). For one file's description and symbol ranges: `openwolf find --file <path>`. Never read .wolf/anatomy.md whole; it is an index.
|
||||
- Check .wolf/cerebrum.md Do-Not-Repeat list before generating code (grep "## Do-Not-Repeat"); after a user correction, update cerebrum.md immediately.
|
||||
- Do NOT manually update .wolf/anatomy.md or .wolf/memory.md; the OpenWolf hooks maintain them.
|
||||
- BEFORE fixing any bug: run `openwolf bug search "<error>"` or grep .wolf/buglog.json. AFTER fixing one: log it there (error_message, root_cause, fix, tags).
|
||||
- When resuming a session, read .wolf/STATUS.md first; regenerate it with /handoff when a quest finishes.
|
||||
|
|
@ -10,7 +10,7 @@
|
|||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node \"$CLAUDE_PROJECT_DIR/.wolf/hooks/post-read.js\"",
|
||||
"command": "node \"/home/rshen/projects/xce/worktrees/scheduler-framework/.wolf/hooks/post-read.js\"",
|
||||
"timeout": 5
|
||||
}
|
||||
]
|
||||
|
|
@ -20,17 +20,17 @@
|
|||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node \"$CLAUDE_PROJECT_DIR/.wolf/hooks/post-write.js\"",
|
||||
"command": "node \"/home/rshen/projects/xce/worktrees/scheduler-framework/.wolf/hooks/post-write.js\"",
|
||||
"timeout": 10
|
||||
},
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": "Bash",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "FILE_PATH=$(python3 -c \"import json,sys; d=json.load(sys.stdin); print(d.get('tool_input',{}).get('file_path') or d.get('tool_input',{}).get('path') or '')\" 2>/dev/null); [[ \"$FILE_PATH\" == *.py ]] && ruff format --quiet \"$FILE_PATH\" 2>/dev/null; true",
|
||||
"timeout": 10
|
||||
},
|
||||
{
|
||||
"type": "command",
|
||||
"command": "FILE_PATH=$(python3 -c \"import json,sys; d=json.load(sys.stdin); print(d.get('tool_input',{}).get('file_path') or d.get('tool_input',{}).get('path') or '')\" 2>/dev/null); [[ \"$FILE_PATH\" == *.py ]] && ruff check --quiet \"$FILE_PATH\" 2>/dev/null; true",
|
||||
"command": "node \"/home/rshen/projects/xce/worktrees/scheduler-framework/.wolf/hooks/post-bash.js\"",
|
||||
"timeout": 10
|
||||
}
|
||||
]
|
||||
|
|
@ -50,7 +50,7 @@
|
|||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node \"$CLAUDE_PROJECT_DIR/.wolf/hooks/session-start.js\"",
|
||||
"command": "node \"/home/rshen/projects/xce/worktrees/scheduler-framework/.wolf/hooks/session-start.js\"",
|
||||
"timeout": 5
|
||||
}
|
||||
]
|
||||
|
|
@ -62,7 +62,7 @@
|
|||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node \"$CLAUDE_PROJECT_DIR/.wolf/hooks/pre-read.js\"",
|
||||
"command": "node \"/home/rshen/projects/xce/worktrees/scheduler-framework/.wolf/hooks/pre-read.js\"",
|
||||
"timeout": 5
|
||||
}
|
||||
]
|
||||
|
|
@ -72,7 +72,17 @@
|
|||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node \"$CLAUDE_PROJECT_DIR/.wolf/hooks/pre-write.js\"",
|
||||
"command": "node \"/home/rshen/projects/xce/worktrees/scheduler-framework/.wolf/hooks/pre-write.js\"",
|
||||
"timeout": 5
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": "Bash",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node \"/home/rshen/projects/xce/worktrees/scheduler-framework/.wolf/hooks/pre-bash.js\"",
|
||||
"timeout": 5
|
||||
}
|
||||
]
|
||||
|
|
@ -84,7 +94,55 @@
|
|||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node \"$CLAUDE_PROJECT_DIR/.wolf/hooks/stop.js\"",
|
||||
"command": "node \"/home/rshen/projects/xce/worktrees/scheduler-framework/.wolf/hooks/stop.js\"",
|
||||
"timeout": 10
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"UserPromptSubmit": [
|
||||
{
|
||||
"matcher": "",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node \"/home/rshen/projects/xce/worktrees/scheduler-framework/.wolf/hooks/user-prompt-submit.js\"",
|
||||
"timeout": 5
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"PostToolBatch": [
|
||||
{
|
||||
"matcher": "",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node \"/home/rshen/projects/xce/worktrees/scheduler-framework/.wolf/hooks/post-batch.js\"",
|
||||
"timeout": 5
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"PreCompact": [
|
||||
{
|
||||
"matcher": "",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node \"/home/rshen/projects/xce/worktrees/scheduler-framework/.wolf/hooks/precompact.js\"",
|
||||
"timeout": 5
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"SessionEnd": [
|
||||
{
|
||||
"matcher": "",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node \"/home/rshen/projects/xce/worktrees/scheduler-framework/.wolf/hooks/session-end.js\"",
|
||||
"timeout": 10
|
||||
}
|
||||
]
|
||||
|
|
@ -94,4 +152,4 @@
|
|||
"worktree": {
|
||||
"baseRef": "head"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -114,6 +114,26 @@ have historically found real violations are:
|
|||
reported fact entries are explicitly EXEMPT from the commit-first rule
|
||||
(pure Raft semantics, retained/retried indefinitely, no commit timeout)
|
||||
— don't flag those as violations.
|
||||
9. **Redirect-completeness on role-gated rejections** (new 2026-08-26,
|
||||
bug-1700) — for every opcode absent from a role's `ha_dispatch_gate()`
|
||||
allow-list (`ha.c`, e.g. the `CTRL_STANDBY` case's `switch`), does the
|
||||
generic gate-rejection reply path in `myreq_handler()` (`cluster.c`, right
|
||||
after the `ha_dispatch_gate()` call) populate `redirect_host`/
|
||||
`redirect_port` via `fill_redirect_reply()` (`join.c`) when the rejection
|
||||
reason is `-XCE_ERR_STANDBY`? Every client-side redirect-follower in this
|
||||
codebase (`xceadmin.discovery.follow_redirects()`, libxce's own failover
|
||||
path) is already generic/opcode-agnostic and starts working automatically
|
||||
the instant the server supplies a redirect — a bare rejection status with
|
||||
no redirect fields silently strands every one of them, and this is a
|
||||
defect regardless of how well the client is written. This exact gap
|
||||
shipped independently, under two different opcodes, before either was
|
||||
caught (`OPCode_upgradeConfig` — found and documented but left unfixed in
|
||||
`test_e2e_live_ha_push.py`'s module docstring; `OPCode_schedAdmin` —
|
||||
bug-1700, the one that actually got fixed) — audit method: check the ONE
|
||||
generic gate-rejection call site first (not opcode-by-opcode), then
|
||||
confirm every currently-shipped primary-only opcode benefits from
|
||||
whatever that site does. Also check `api-standards.md`'s "Adding a New
|
||||
Opcode" step 6 hasn't drifted from what the code actually does.
|
||||
|
||||
For `scheduler`, the dimensions are (added 2026-08-24, first used to close
|
||||
bug-1613/bug-1681 and confirm bug-1611's partial-fix status):
|
||||
|
|
|
|||
15
.wolf/.gitignore
vendored
Normal file
15
.wolf/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
# OpenWolf: machine-local state (committed state: cerebrum.md, STATUS.md,
|
||||
# memory.md, buglog.json, anatomy.md, anatomy-index.json, config.json, OPENWOLF.md)
|
||||
hooks/
|
||||
backups/
|
||||
cache/
|
||||
daemon.log
|
||||
daemon.pid
|
||||
dashboard-token
|
||||
token-ledger.json
|
||||
suggestions.json
|
||||
cron-state.json
|
||||
_scan-state.json
|
||||
_*.json
|
||||
*.lock
|
||||
*.tmp
|
||||
|
|
@ -4,136 +4,21 @@ You are working in an OpenWolf-managed project. These rules apply every turn.
|
|||
|
||||
## File Navigation
|
||||
|
||||
1. Check `.wolf/anatomy.md` BEFORE reading any file. It has a 2-3 line description and token estimate for every file in the project.
|
||||
2. If the description in anatomy.md is sufficient for your task, do NOT read the full file.
|
||||
3. If a file is not in anatomy.md, search with Grep/Glob, then update anatomy.md with the new entry.
|
||||
1. Check `.wolf/anatomy.md` BEFORE reading any file.
|
||||
2. If the description is sufficient, do NOT read the full file.
|
||||
3. If a file is not in anatomy.md, search with Grep/Glob.
|
||||
|
||||
## Code Generation
|
||||
|
||||
1. Before generating code, read `.wolf/cerebrum.md` and respect every entry.
|
||||
2. Check the `## Do-Not-Repeat` section — these are past mistakes that must not recur.
|
||||
3. Follow all conventions in `## Key Learnings` and `## User Preferences`.
|
||||
1. Read `.wolf/cerebrum.md` and respect every entry.
|
||||
2. Check `## Do-Not-Repeat` section.
|
||||
|
||||
## After Actions
|
||||
|
||||
1. After every significant action, append a one-line entry to `.wolf/memory.md`:
|
||||
`| HH:MM | description | file(s) | outcome | ~tokens |`
|
||||
2. After creating, deleting, or renaming files: update `.wolf/anatomy.md`.
|
||||
|
||||
## Cerebrum Learning (MANDATORY — every session)
|
||||
|
||||
OpenWolf's value comes from learning across sessions. You MUST update `.wolf/cerebrum.md` whenever you learn something useful. This is not optional.
|
||||
|
||||
**Update `## User Preferences` when the user:**
|
||||
- Corrects your approach ("no, do it this way instead")
|
||||
- Expresses a style preference (naming, structure, formatting)
|
||||
- Shows a preferred workflow or tool choice
|
||||
- Rejects a suggestion — record what they preferred instead
|
||||
- Asks for more/less detail, verbosity, explanation
|
||||
|
||||
**Update `## Key Learnings` when you discover:**
|
||||
- A project convention not obvious from the code (e.g., "tests go in __tests__/ not test/")
|
||||
- A framework-specific pattern this project uses
|
||||
- An API behavior that surprised you
|
||||
- A dependency quirk or version constraint
|
||||
- How modules connect or data flows through the system
|
||||
|
||||
**Update `## Do-Not-Repeat` (with date) when:**
|
||||
- The user corrects a mistake you made
|
||||
- You try something that fails and find the right approach
|
||||
- You discover a gotcha that would trip up a fresh session
|
||||
|
||||
**Update `## Decision Log` when:**
|
||||
- A significant architectural or technical choice is made
|
||||
- The user explains why they chose approach A over B
|
||||
- A trade-off is explicitly discussed
|
||||
|
||||
**The bar is LOW.** If in doubt, add it. A cerebrum entry that's slightly redundant costs nothing. A missing entry means the next session repeats the same discovery process.
|
||||
|
||||
## Bug Logging (MANDATORY)
|
||||
|
||||
**Log a bug to `.wolf/buglog.json` whenever ANY of these happen:**
|
||||
- The user reports an error, bug, or problem
|
||||
- A test fails or a command produces an error
|
||||
- You fix something that was broken
|
||||
- You edit a file more than twice to get it right
|
||||
- An import, module, or dependency is missing or wrong
|
||||
- A runtime error, type error, or syntax error occurs
|
||||
- A build or lint command fails
|
||||
- A feature doesn't work as expected
|
||||
- You change error handling, try/catch blocks, or validation logic
|
||||
- The user says something "doesn't work", "is broken", or "shows wrong X"
|
||||
|
||||
**Before fixing:** Read `.wolf/buglog.json` first — the fix may already be known.
|
||||
|
||||
**Before appending a new entry:** search existing bugs for the same `file` + overlapping `root_cause`/`error_message`. If a match exists, update THAT entry (`occurrences += 1`, `last_seen = now`, append context to `fix` if the resolution differs) instead of creating a new `bug-NNN`. Never create a second entry under an id that already exists — assign the next unused number after the current max `bug-NNN` in the file (never restart from `bug-001`; a restarted counter collides with existing history and is unrecoverable without a git-history merge). If you fixed something genuinely new, append with this structure:
|
||||
```json
|
||||
{
|
||||
"id": "bug-NNN",
|
||||
"timestamp": "ISO date",
|
||||
"error_message": "exact error or user complaint",
|
||||
"file": "file that was fixed",
|
||||
"root_cause": "why it broke",
|
||||
"fix": "what you changed to fix it",
|
||||
"tags": ["relevant", "keywords"],
|
||||
"related_bugs": [],
|
||||
"occurrences": 1,
|
||||
"last_seen": "ISO date"
|
||||
}
|
||||
```
|
||||
Always preserve the top-level `{"version": 1, "bugs": [...]}` wrapper — never write a bare array.
|
||||
|
||||
**The threshold is LOW.** When in doubt, log it. A false positive in the bug log costs nothing. A missed bug means repeating the same mistake later.
|
||||
|
||||
**Integrity is automated, not optional:** a Stop hook (`.wolf/hooks/stop.js`) validates `.wolf/buglog.json` (JSON validity, wrapper format, duplicate IDs, ID count/max going backwards vs. the last commit) after every turn and, if it's valid and grew, commits it standalone (`chore(wolf): checkpoint buglog.json (N bugs)`) so git history is always a recoverable backup. If it shrank or fails validation, the hook refuses to commit and prints why — treat that warning as a stop-and-investigate signal, not noise to scroll past.
|
||||
1. Append to `.wolf/memory.md`.
|
||||
2. After file changes: update `.wolf/anatomy.md`.
|
||||
|
||||
## Token Discipline
|
||||
|
||||
- Never re-read a file already read this session unless it was modified since.
|
||||
- Prefer anatomy.md descriptions over full file reads when possible.
|
||||
- Prefer targeted Grep over full file reads when searching for specific code.
|
||||
- If appending to a file, do not read the entire file first.
|
||||
|
||||
## Design QC
|
||||
|
||||
When the user asks you to check, evaluate, or improve the design/UI of their app:
|
||||
|
||||
1. Run `openwolf designqc` via Bash to capture screenshots.
|
||||
- The command auto-detects a running dev server, or starts one from package.json if needed
|
||||
- Use `--url <url>` only if auto-detection fails
|
||||
- The command saves compressed JPEG screenshots to `.wolf/designqc-captures/`
|
||||
- Full pages are captured as sectioned viewport-height images (top, section2, ..., bottom)
|
||||
2. Read the captured screenshot images from `.wolf/designqc-captures/` using the Read tool.
|
||||
3. Evaluate the design against modern standards (Shadcn UI, Tailwind, clean React patterns):
|
||||
- Spacing and whitespace consistency
|
||||
- Typography hierarchy and readability
|
||||
- Color contrast and accessibility (WCAG)
|
||||
- Visual hierarchy and focal points
|
||||
- Component consistency
|
||||
- Whether the design looks "dull" or "white-coded" (generic, no personality)
|
||||
4. Provide specific, actionable feedback with fix suggestions.
|
||||
5. If the user approves, implement the fixes directly in their code.
|
||||
6. After fixes, re-run `openwolf designqc` to capture new screenshots and verify improvement.
|
||||
|
||||
**Token awareness:** Each screenshot costs ~2500 tokens. The command compresses images (JPEG quality 70, max width 1200px) to minimize cost. For large apps, use `--routes / /specific-page` to limit captures.
|
||||
|
||||
## Reframe — UI Framework Selection
|
||||
|
||||
When the user asks to change, pick, migrate, or "reframe" their project's UI framework:
|
||||
|
||||
1. Read `.wolf/reframe-frameworks.md` for the full framework knowledge base.
|
||||
2. Ask the user the decision questions from the file (current stack, priority, Tailwind usage, theme preference, app type). Stop early once the choice narrows to 1-2 options.
|
||||
3. Present a recommendation with reasoning based on the comparison matrix.
|
||||
4. Once the user confirms, use the selected framework's prompt from the file — **adapted to the actual project** using `.wolf/anatomy.md` for real file paths, routes, and components.
|
||||
5. Execute the migration: install dependencies, update config, refactor components.
|
||||
6. After migration, run `openwolf designqc` to verify the new look.
|
||||
|
||||
**Do NOT read the entire reframe-frameworks.md into context upfront.** Read the decision questions and comparison matrix first (~50 lines). Only read the specific framework's prompt section after the user chooses.
|
||||
|
||||
## Session End
|
||||
|
||||
Before ending or when asked to wrap up:
|
||||
|
||||
1. Write a session summary to `.wolf/memory.md`.
|
||||
2. Review the session: did you learn anything? Did the user correct you? Did you fix a bug? If yes, update `.wolf/cerebrum.md` and/or `.wolf/buglog.json`.
|
||||
3. Do NOT manually `git add`/commit `.wolf/buglog.json` yourself as part of a feature/task commit — the Stop hook checkpoints it standalone once it validates cleanly (see Bug Logging above). Mixing it into a feature commit just duplicates that history under an unrelated message.
|
||||
- Never re-read a file already read this session.
|
||||
- Prefer anatomy.md descriptions over full reads.
|
||||
|
|
|
|||
39
.wolf/STATUS.md
Normal file
39
.wolf/STATUS.md
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
# STATUS
|
||||
|
||||
> Single source of truth for resuming work. Read this FIRST when starting a session.
|
||||
> Update at the end of every work phase so the next `/clear` resumes in 1 read.
|
||||
|
||||
---
|
||||
|
||||
## ✅ Done
|
||||
|
||||
- (nothing yet — fill in as work completes)
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Next phase
|
||||
|
||||
**Goal:** _<what we're building next>_
|
||||
|
||||
### Acceptance criteria
|
||||
1. _<concrete user-visible outcome>_
|
||||
|
||||
### Files to create / edit
|
||||
- _<path + purpose>_
|
||||
|
||||
### Open decisions
|
||||
- _<question to ask before coding>_
|
||||
|
||||
---
|
||||
|
||||
## 📁 Active architecture
|
||||
|
||||
- **Stack:** _<frameworks>_
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Useful commands
|
||||
|
||||
```bash
|
||||
# add the most-used commands here
|
||||
```
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"last_scanned": "2026-08-17T01:00:00.081Z",
|
||||
"git_head": "397d8e7f9c760ffc8c51df73948ee03aa5f7dfff",
|
||||
"last_scanned": "2026-08-27T07:00:04.610Z",
|
||||
"git_head": "446437bfce44e53b9cc622b397befb813a3d77bf",
|
||||
"file_count": 504
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
743
.wolf/anatomy.md
743
.wolf/anatomy.md
|
|
@ -1,77 +1,7 @@
|
|||
# anatomy.md
|
||||
|
||||
> Auto-maintained by OpenWolf. Last scanned: 2026-08-25T16:05:07.063Z
|
||||
> Files: 742 tracked | Anatomy hits: 0 | Misses: 0
|
||||
|
||||
## ../../../../../../tmp/
|
||||
|
||||
- `apiserver` (~27 tok)
|
||||
|
||||
## ../../../../../../tmp/claude-1000/-home-rshen-projects-xce-worktrees-scheduler-framework/01599b27-8a93-432e-a4a8-263066731f52/scratchpad/
|
||||
|
||||
- `add_bug1573.py` — Declares core_apply_worker_resvals (~901 tok)
|
||||
- `check_leftover.sh` — Standalone leftover-process checker -- avoids the bug-1576 self-match (~180 tok)
|
||||
- `crash_handler.c` — define _GNU_SOURCE (~485 tok)
|
||||
- `exe-watch.sh` — Watches for a REAL worker daemon process by checking each PID's /proc/exe (~193 tok)
|
||||
- `fake-apiserver.sh` — Deliberately-crashing fake apiserver binary for independent mid-abort (~96 tok)
|
||||
- `fix_bug_1574.py` (~799 tok)
|
||||
- `fix_bug1572.py` (~1026 tok)
|
||||
- `gdbcmds.txt` (~63 tok)
|
||||
- `proc_worker_scan.sh` — Scan /proc/<pid>/exe for any process whose REAL BINARY (not argv/cmdline (~300 tok)
|
||||
- `procscan.sh` — Scan /proc for real processes whose cmdline references the given (~261 tok)
|
||||
- `reviewer_synth_test.sh` — Independent reviewer synthetic test -- sources the REAL script (production (~1538 tok)
|
||||
- `stress_driver.sh` — Independent stress-test driver for run-resource-verify.sh's trap-based (~758 tok)
|
||||
- `synth_aggregate_test_round2.sh` — Round-2 synthetic regression suite for run-resource-verify.sh's task 7.1 (~2469 tok)
|
||||
- `synth_aggregate_test.sh` — Synthetic sanity-check for run-resource-verify.sh's aggregate_verdict() (~1559 tok)
|
||||
- `test_aggregate.sh` — Independent synthetic test of aggregate_verdict() / build_verdict_record() (~2105 tok)
|
||||
- `test-sched-check.sh` — Synthetic (no live deployment) test of query_scheduler_resvals() / poll_scheduler_log() (~783 tok)
|
||||
- `trap_repro.sh` (~301 tok)
|
||||
- `xtest_script.txt` (~52 tok)
|
||||
|
||||
## ../../../../../../tmp/claude-1000/-home-rshen-projects-xce-worktrees-scheduler-framework/0334ebbe-8d9c-4931-871d-87c678f83a79/scratchpad/
|
||||
|
||||
- `dryrun_funcs.sh` — Dry-run harness: extracts and exercises the pure functions from the new (~1407 tok)
|
||||
- `fix_bug1604.py` (~813 tok)
|
||||
- `log_bug_1602.py` (~1054 tok)
|
||||
|
||||
## ../../../../../../tmp/claude-1000/-home-rshen-projects-xce-worktrees-scheduler-framework/0334ebbe-8d9c-4931-871d-87c678f83a79/scratchpad/dfp-4.3-remediation/fakebin/
|
||||
|
||||
- `git` — Wrapper: fail ONLY `git diff` (and any `git diff ...` invocation), pass (~55 tok)
|
||||
|
||||
## ../../../../../../tmp/claude-1000/-home-rshen-projects-xce-worktrees-scheduler-framework/0334ebbe-8d9c-4931-871d-87c678f83a79/scratchpad/dfp_test/
|
||||
|
||||
- `Makefile.after` (~441 tok)
|
||||
- `Makefile.before` (~376 tok)
|
||||
|
||||
## ../../../../../../tmp/claude-1000/-home-rshen-projects-xce-worktrees-scheduler-framework/2455e81b-dd7c-4f96-b2cd-548167b22732/scratchpad/
|
||||
|
||||
- `close_out_bug1588.py` — Declares parameter (~1202 tok)
|
||||
- `fix_buglog_1629.py` (~405 tok)
|
||||
- `tasks-draft.md` — Implementation Plan (~2677 tok)
|
||||
- `update_bug1613.py` (~859 tok)
|
||||
- `update_buglog.py` (~1058 tok)
|
||||
|
||||
## ../../../../../../tmp/claude-1000/-home-rshen-projects-xce-worktrees-scheduler-framework/fe2ba1fe-c8fb-4d72-986e-1c8e2406aa3a/scratchpad/
|
||||
|
||||
- `strstr_probe.c` — define _GNU_SOURCE (~236 tok)
|
||||
|
||||
## ../../../../../claude/.claude/plugins/claude-hud/
|
||||
|
||||
- `config.json` (~310 tok)
|
||||
|
||||
## ../../../../../claude/.claude/projects/-home-rshen-projects-xce-xce/memory/
|
||||
|
||||
- `incident_subagent_scope_violation.md` (~1171 tok)
|
||||
- `MEMORY.md` (~83 tok)
|
||||
- `project_bug_tracker_buglog.md` (~369 tok)
|
||||
|
||||
## ../../xce/.claude/worktrees/fix-bug-1613/.kiro/steering/
|
||||
|
||||
- `scheduler-driver-equivalence.md` — Scheduler Driver Equivalence (~1520 tok)
|
||||
|
||||
## ../../xce/.claude/worktrees/fix-bug-1613/src/scheduler/
|
||||
|
||||
- `CLAUDE.md` — XCE Scheduler (~1264 tok)
|
||||
> Auto-maintained by OpenWolf. Last scanned: 2026-08-27T07:51:49.234Z
|
||||
> Files: 507 tracked | Anatomy hits: 0 | Misses: 0
|
||||
|
||||
## ./
|
||||
|
||||
|
|
@ -79,7 +9,7 @@
|
|||
- `.gitignore` — Git ignore rules (~402 tok)
|
||||
- `.mcp.json` (~7 tok)
|
||||
- `.opencode.json` (~7 tok)
|
||||
- `CLAUDE.md` — OpenWolf (~1484 tok)
|
||||
- `CLAUDE.md` — OpenWolf (~1526 tok)
|
||||
- `README.md` — Project documentation (~612 tok)
|
||||
|
||||
## .agent-shell/transcripts/
|
||||
|
|
@ -89,199 +19,16 @@
|
|||
- `2026-08-11-22-00-54.md` — Agent Shell Transcript (~9623 tok)
|
||||
- `2026-08-15-22-30-02.md` — Agent Shell Transcript (~438 tok)
|
||||
- `2026-08-16-06-11-30.md` — Agent Shell Transcript (~60273 tok)
|
||||
|
||||
## .claude/
|
||||
|
||||
- `settings.json` (~718 tok)
|
||||
|
||||
## .claude/rules/
|
||||
|
||||
- `openwolf.md` (~313 tok)
|
||||
|
||||
## .claude/skills/gitea-sync/
|
||||
|
||||
- `SKILL.md` — Gitea Sync Skill (~1875 tok)
|
||||
|
||||
## .claude/skills/kiro-debug/
|
||||
|
||||
- `SKILL.md` — kiro-debug (~1322 tok)
|
||||
|
||||
## .claude/skills/kiro-discovery/
|
||||
|
||||
- `SKILL.md` — kiro-discovery Skill (~3387 tok)
|
||||
|
||||
## .claude/skills/kiro-impl/
|
||||
|
||||
- `SKILL.md` — kiro-impl Skill (~4048 tok)
|
||||
|
||||
## .claude/skills/kiro-impl/templates/
|
||||
|
||||
- `debugger-prompt.md` — Debug Investigator (~812 tok)
|
||||
- `implementer-prompt.md` — TDD Task Implementer (~1659 tok)
|
||||
- `reviewer-prompt.md` — Task Implementation Reviewer (~1352 tok)
|
||||
|
||||
## .claude/skills/kiro-review/
|
||||
|
||||
- `SKILL.md` — kiro-review (~1756 tok)
|
||||
|
||||
## .claude/skills/kiro-spec-batch/
|
||||
|
||||
- `SKILL.md` — kiro-spec-batch Skill (~2293 tok)
|
||||
|
||||
## .claude/skills/kiro-spec-design/
|
||||
|
||||
- `SKILL.md` — kiro-spec-design Skill (~2869 tok)
|
||||
|
||||
## .claude/skills/kiro-spec-design/rules/
|
||||
|
||||
- `design-discovery-full.md` — Full Discovery Process for Technical Design (~949 tok)
|
||||
- `design-discovery-light.md` — Light Discovery Process for Extensions (~389 tok)
|
||||
- `design-principles.md` — Technical Design Rules and Principles (~2982 tok)
|
||||
- `design-review-gate.md` — Design Review Gate (~1016 tok)
|
||||
- `design-synthesis.md` — Design Synthesis (~464 tok)
|
||||
|
||||
## .claude/skills/kiro-spec-design/templates/specs/
|
||||
|
||||
- `design.md` — Design Document Template (~3303 tok)
|
||||
- `research.md` — Research & Design Decisions Template (~586 tok)
|
||||
|
||||
## .claude/skills/kiro-spec-init/
|
||||
|
||||
- `SKILL.md` — Spec Initialization (~783 tok)
|
||||
|
||||
## .claude/skills/kiro-spec-init/templates/specs/
|
||||
|
||||
- `init.json` (~125 tok)
|
||||
- `requirements-init.md` — Requirements Document (~40 tok)
|
||||
|
||||
## .claude/skills/kiro-spec-quick/
|
||||
|
||||
- `SKILL.md` — Quick Spec Generator (~1993 tok)
|
||||
|
||||
## .claude/skills/kiro-spec-quick/templates/specs/
|
||||
|
||||
- `init.json` (~125 tok)
|
||||
- `requirements-init.md` — Requirements Document (~40 tok)
|
||||
|
||||
## .claude/skills/kiro-spec-requirements/
|
||||
|
||||
- `SKILL.md` — kiro-spec-requirements Skill (~2122 tok)
|
||||
|
||||
## .claude/skills/kiro-spec-requirements/rules/
|
||||
|
||||
- `ears-format.md` — EARS Format Guidelines (~739 tok)
|
||||
- `requirements-review-gate.md` — Requirements Review Gate (~924 tok)
|
||||
|
||||
## .claude/skills/kiro-spec-requirements/templates/specs/
|
||||
|
||||
- `requirements.md` — Requirements Document (~325 tok)
|
||||
|
||||
## .claude/skills/kiro-spec-status/
|
||||
|
||||
- `SKILL.md` — kiro-spec-status Skill (~746 tok)
|
||||
|
||||
## .claude/skills/kiro-spec-tasks/
|
||||
|
||||
- `SKILL.md` — kiro-spec-tasks Skill (~2460 tok)
|
||||
|
||||
## .claude/skills/kiro-spec-tasks/rules/
|
||||
|
||||
- `tasks-generation.md` — Task Generation Rules (~2773 tok)
|
||||
- `tasks-parallel-analysis.md` — Parallel Task Analysis Rules (~633 tok)
|
||||
|
||||
## .claude/skills/kiro-spec-tasks/templates/specs/
|
||||
|
||||
- `tasks.md` — Implementation Plan (~318 tok)
|
||||
|
||||
## .claude/skills/kiro-steering-custom/
|
||||
|
||||
- `SKILL.md` — kiro-steering-custom Skill (~1222 tok)
|
||||
|
||||
## .claude/skills/kiro-steering-custom/rules/
|
||||
|
||||
- `steering-principles.md` — Steering Principles (~593 tok)
|
||||
|
||||
## .claude/skills/kiro-steering-custom/templates/steering-custom/
|
||||
|
||||
- `api-standards.md` — API Standards (~415 tok)
|
||||
- `authentication.md` — Authentication & Authorization Standards (~523 tok)
|
||||
- `database.md` — Database Standards (~377 tok)
|
||||
- `deployment.md` — Deployment Standards (~441 tok)
|
||||
- `error-handling.md` — Error Handling Standards (~549 tok)
|
||||
- `security.md` — Security Standards (~523 tok)
|
||||
- `testing.md` — Testing Standards (~325 tok)
|
||||
|
||||
## .claude/skills/kiro-steering/
|
||||
|
||||
- `SKILL.md` — kiro-steering Skill (~1185 tok)
|
||||
|
||||
## .claude/skills/kiro-steering/rules/
|
||||
|
||||
- `steering-principles.md` — Steering Principles (~593 tok)
|
||||
|
||||
## .claude/skills/kiro-steering/templates/steering/
|
||||
|
||||
- `product.md` — Product Overview (~89 tok)
|
||||
- `structure.md` — Project Structure (~218 tok)
|
||||
- `tech.md` — Technology Stack (~198 tok)
|
||||
|
||||
## .claude/skills/kiro-validate-design/
|
||||
|
||||
- `SKILL.md` — kiro-validate-design Skill (~1296 tok)
|
||||
|
||||
## .claude/skills/kiro-validate-design/rules/
|
||||
|
||||
- `design-review.md` — Design Review Process (~1090 tok)
|
||||
|
||||
## .claude/skills/kiro-validate-gap/
|
||||
|
||||
- `SKILL.md` — kiro-validate-gap Skill (~1385 tok)
|
||||
|
||||
## .claude/skills/kiro-validate-gap/rules/
|
||||
|
||||
- `gap-analysis.md` — Gap Analysis Process (~1266 tok)
|
||||
|
||||
## .claude/skills/kiro-validate-impl/
|
||||
|
||||
- `SKILL.md` — kiro-validate-impl Skill (~2826 tok)
|
||||
|
||||
## .claude/skills/kiro-verify-completion/
|
||||
|
||||
- `SKILL.md` — kiro-verify-completion (~972 tok)
|
||||
|
||||
## .claude/skills/weekly_report/
|
||||
|
||||
- `SKILL.md` — weekly_report Skill (~3367 tok)
|
||||
|
||||
## .claude/skills/xce-audit-invariants/
|
||||
|
||||
- `SKILL.md` — XCE Design-Invariant Audit & Fix (~3823 tok)
|
||||
|
||||
## .claude/skills/xce-build/
|
||||
|
||||
- `SKILL.md` — build xce source code (~2597 tok)
|
||||
|
||||
## .claude/skills/xce-deploy-ha/
|
||||
|
||||
- `SKILL.md` — XCE HA Cluster Deployment; now auto-picks real `xcecd` (sudo, root) vs `xcecd_virtual` per host via `/run/systemd/system` (~8200 tok)
|
||||
|
||||
## .claude/skills/xce-test-ha/
|
||||
|
||||
- `run-ha-soak.sh` — run-ha-soak.sh — driver for /xce-test-ha: deploy a 3-node XCE HA cluster, then (~44744 tok)
|
||||
- `SKILL.md` — XCE HA Failover Soak Test (~9766 tok)
|
||||
- `soak-verdict-selftest.sh` — soak-verdict-selftest.sh — deterministic unit test for the verdict parser in (~2825 tok)
|
||||
- `soak-verdict.sh` — soak-verdict.sh — pure (side-effect-free) verdict helpers for run-ha-soak.sh. (~2980 tok)
|
||||
|
||||
## .claude/skills/xce-test-resources/
|
||||
|
||||
- `run-resource-verify.sh` — run-resource-verify.sh -- driver for /xce-test-resources: end-to-end (~21445 tok)
|
||||
- `SKILL.md` — XCE Resource-Value Verification skill contract, FULLY UPDATED for task 7.1 completion: "Status: complete and functional", real VERDICT line format (`resources=N layers_checked=3 mismatches=M unreachable=U rejected=R result=PASS|FAIL`), real flag names (`--keep-running`, `--dir DIR`, `$XCE_RESVERIFY_*` env vars, all cross-checked against the actual script), FAILURE-line format, self-contained/sourceable `aggregate_verdict()` note for task 8.2, known-bugs section (1572/1574 open, 1579 fixed). No longer says "not yet implemented" anywhere. (~3400 tok)
|
||||
- `verdict-logic-selftest.sh` — verdict-logic-selftest.sh -- deterministic, no-deployment unit test for (~3135 tok)
|
||||
- `2026-08-23-15-56-15.md` — Agent Shell Transcript (~69291 tok)
|
||||
- `2026-08-26-17-36-06.md` — Agent Shell Transcript (~183 tok)
|
||||
|
||||
## .codegraph/
|
||||
|
||||
- `.gitignore` — Git ignore rules (~61 tok)
|
||||
- `daemon.log` (~6538 tok)
|
||||
- `codegraph.db-shm` (~26215 tok)
|
||||
- `codegraph.db-wal` (~0 tok)
|
||||
- `daemon.log` (~22564 tok)
|
||||
- `daemon.pid` (~45 tok)
|
||||
|
||||
## .kiro/settings/rules/
|
||||
|
||||
|
|
@ -398,6 +145,11 @@
|
|||
- `spec.json` (~129 tok)
|
||||
- `tasks.md` — Implementation Plan (~3693 tok)
|
||||
|
||||
## .kiro/specs/eliminate-frontend-makefile/
|
||||
|
||||
- `brief.md` — Brief: eliminate-frontend-makefile (~2334 tok)
|
||||
- `spec.json` (~236 tok)
|
||||
|
||||
## .kiro/specs/ha-commit-barrier-rollout/
|
||||
|
||||
- `brief.md` — Brief: ha-commit-barrier-rollout (~626 tok)
|
||||
|
|
@ -711,7 +463,7 @@
|
|||
|
||||
- `brief.md` — Brief: scheduler-sim-decision-parity (~2025 tok)
|
||||
- `design.md` — Design Document (~8189 tok)
|
||||
- `requirements.md` — Requirements Document, 5 reqs (accounting parity, capacity awareness, decision visibility, graceful degradation, no regression) (~1400 tok)
|
||||
- `requirements.md` — Requirements Document, 5 reqs (accounting parity, capacity awareness, decision visibility, graceful degradation, no regression) (~1652 tok)
|
||||
- `research.md` — Research & Design Decisions (~2853 tok)
|
||||
- `spec.json` (~134 tok)
|
||||
- `tasks.md` — Implementation Plan (~4736 tok)
|
||||
|
|
@ -722,17 +474,18 @@
|
|||
|
||||
## .kiro/specs/scheduler-sim-eager-reconciliation/
|
||||
|
||||
- `design.md` — Design Document (~5193 tok)
|
||||
- `requirements.md` — Requirements Document (~2312 tok)
|
||||
- `spec.json` (~137 tok)
|
||||
- `research.md` — Research & Design Decisions (~4657 tok)
|
||||
- `spec.json` (~133 tok)
|
||||
- `tasks.md` — Implementation Plan (~1748 tok)
|
||||
|
||||
## .kiro/specs/scheduler-sim-repl-verbosity/
|
||||
|
||||
- `design.md` — Design Document: REPL continue-mode output (event echo gate, round rollup + collapse-range flush interface, jobid range compression, sim_print_decision gate) (~6900 tok)
|
||||
- `design.md` — Design Document: REPL continue-mode output (event echo gate, round rollup + collapse-range flush interface, jobid range compression, sim_print_decision gate) (~6617 tok)
|
||||
- `requirements.md` — Requirements Document: 5 EARS requirement areas (mode-aware echo, per-round summary, collapsing, jobid ranges, no regression) (~1852 tok)
|
||||
- `research.md` — Research & Design Decisions, incl. the flush-interface gap found/fixed during task-graph sanity review (~3600 tok)
|
||||
- `spec.json` — phase: tasks-generated, ready_for_implementation: true (~220 tok)
|
||||
- `research.md` — Research & Design Decisions, incl. the flush-interface gap found/fixed during task-graph sanity review (~3238 tok)
|
||||
- `spec.json` (~131 tok)
|
||||
- `tasks.md` — Implementation Plan: 5 major tasks / 9 sub-tasks, TDD-paired, tasks 3-4 parallel-safe after task 2 (~1900 tok)
|
||||
- `tasks.md` — Implementation Plan (~1962 tok)
|
||||
|
||||
## .kiro/specs/worker-join-refactor/
|
||||
|
|
@ -748,6 +501,11 @@
|
|||
- `brief.md` — Brief: worker-rejoin-resync (~1394 tok)
|
||||
- `spec.json` (~200 tok)
|
||||
|
||||
## .kiro/specs/xce-test-cli-ux/
|
||||
|
||||
- `requirements.md` — Requirements Document (~3004 tok)
|
||||
- `spec.json` (~131 tok)
|
||||
|
||||
## .kiro/specs/xce-test-e2e-primitives/
|
||||
|
||||
- `design.md` — Design Document — xce-test-e2e-primitives (~2913 tok)
|
||||
|
|
@ -756,16 +514,25 @@
|
|||
- `spec.json` (~271 tok)
|
||||
- `tasks.md` — Implementation Plan (~4188 tok)
|
||||
|
||||
## .kiro/specs/xce-test-rc-file/
|
||||
|
||||
- `design.md` — Design Document (~4183 tok)
|
||||
- `requirements.md` — Requirements Document (~1477 tok)
|
||||
- `research.md` — Summary (~1771 tok)
|
||||
- `spec.json` (~128 tok)
|
||||
- `tasks.md` — Implementation Plan (~1896 tok)
|
||||
|
||||
## .kiro/steering/
|
||||
|
||||
- `api-standards.md` — API Standards (~1447 tok)
|
||||
- `api-standards.md` — API Standards (~1704 tok)
|
||||
- `build-deployment.md` — Build & Deployment (~2917 tok)
|
||||
- `config-design.md` — Configuration Design: What Belongs in Shared Config (~2053 tok)
|
||||
- `error-handling.md` — Error Handling Standards (~1367 tok)
|
||||
- `ha-async-io.md` — HA Protocol & Async I/O (~4721 tok)
|
||||
- `persistence.md` — WAL & Persistence Durability (~3425 tok)
|
||||
- `product.md` — Product Overview (~1509 tok)
|
||||
- `roadmap.md` — Roadmap (~15496 tok)
|
||||
- `roadmap.md` — Roadmap (~15324 tok)
|
||||
- `scheduler-driver-equivalence.md` — Scheduler Driver Equivalence (~1520 tok)
|
||||
- `security.md` — Security Standards (~1636 tok)
|
||||
- `structure.md` — Project Structure (~1348 tok)
|
||||
- `tech.md` — Technology Stack (~1500 tok)
|
||||
|
|
@ -805,11 +572,7 @@
|
|||
|
||||
- `flake.nix` (~1301 tok)
|
||||
- `meson.build` (~672 tok)
|
||||
- `meson.options` — dirty-content-fingerprint task 1: sibling `dirty_fingerprint` string option (default '') added next to commit_id/build_time (~135 tok)
|
||||
|
||||
## src/.claude/
|
||||
|
||||
- `ucai-formatter-cache.local.json` (~17 tok)
|
||||
- `meson.options` — dirty-content-fingerprint task 1: sibling `dirty_fingerprint` string option (default '') added next to commit_id/build_time (~124 tok)
|
||||
|
||||
## src/autoscaler/
|
||||
|
||||
|
|
@ -921,348 +684,116 @@
|
|||
- `requestMachines.sh` (~20 tok)
|
||||
- `requestReturnMachines.sh` (~38 tok)
|
||||
|
||||
## src/configs/
|
||||
## src/build-asan/
|
||||
|
||||
- `.resources.yaml.~undo-tree~` (~2957 tok)
|
||||
- `hosts.yaml` (~248 tok)
|
||||
- `limits.yaml` (~475 tok)
|
||||
- `queues.yaml` (~504 tok)
|
||||
- `resources.yaml` — Declares type (~1687 tok)
|
||||
- `scaler.yaml` (~102 tok)
|
||||
- `users.yaml` — define usergroup share (~47 tok)
|
||||
- `xce.yaml` (~33 tok)
|
||||
- `.gitignore` — Git ignore rules (~25 tok)
|
||||
- `.hgignore` — This file is autogenerated by Meson. If you change or delete it, it won't be recreated. (~29 tok)
|
||||
- `.ninja_log` — ninja log v7 (~34178 tok)
|
||||
- `build.ninja` — This is the build file for project "xce" (~238122 tok)
|
||||
- `compile_commands.json` (~247104 tok)
|
||||
- `xce` (~176 tok)
|
||||
|
||||
## src/configs/schema/
|
||||
## src/build-asan/controller/apiserver_push_unit_test.p/
|
||||
|
||||
- `hosts.schema.json` (~1016 tok)
|
||||
- `limits.schema.json` (~454 tok)
|
||||
- `queues.schema.json` (~1154 tok)
|
||||
- `resources.schema.json` (~342 tok)
|
||||
- `scaler.schema.json` (~232 tok)
|
||||
- `users.schema.json` (~284 tok)
|
||||
- `xce.schema.json` (~312 tok)
|
||||
- `apiserver.c.o` (~269434 tok)
|
||||
- `dataex.c.o` (~255400 tok)
|
||||
- `testsuite_test_job_array_apiserver_push.c.o` (~264685 tok)
|
||||
|
||||
## src/controller/
|
||||
## src/build-asan/controller/controller_unit_test.p/
|
||||
|
||||
- `apiserver.c` — include <errno.h> (~5966 tok)
|
||||
- `CLAUDE.md` — XCE Controller (~4927 tok)
|
||||
- `cluster.c` — include "config.h"; bug-1693 (2026-08-25): new `int schedadmin_signal_disconnect(void)` (thin wrapper around the existing static `schedadmin_signal(-ECONNRESET, NULL)`) lets `sched.c`'s `handle_scheduler_leave()` wake a pending `OPCode_schedAdmin` waiter immediately on scheduler disconnect instead of only at the 5s `SCHEDADMIN_TIMEOUT_MS` deadline; the post-wait dispatch gained a `got_status==-ECONNRESET` branch replying `SchedAdminReply{error="scheduler disconnected"}` (~25702 tok)
|
||||
- `commit.c` — Declares is (~13741 tok)
|
||||
- `config_repl.c` — define _GNU_SOURCE (~36913 tok)
|
||||
- `config_repl.h` — pragma once (~6952 tok)
|
||||
- `ctrl_metrics.c` — include "ctrl_metrics.h" (~767 tok)
|
||||
- `ctrlstatus.c` — include "config.h" (~3234 tok)
|
||||
- `dataex.c` — include <errno.h> (~2531 tok)
|
||||
- `egress.c` (~1130 tok)
|
||||
- `ha_convergence.c` — Declares char (~8389 tok)
|
||||
- `ha.c` — define _GNU_SOURCE (~126128 tok)
|
||||
- `ha.h` — ifndef HA_H (~34172 tok)
|
||||
- `job.c` — include <assert.h>; bug-1693 (2026-08-25): `fetchfile_pending_t` gained a `worker_t* worker` field (set in `on_fetchfile()` before registering); new `void fetchfile_signal_worker_disconnect(worker_t*)` walks `g_fetchfile_list` and wakes every pending waiter proxied to a disconnected worker with `-ECONNRESET`, called from `handle_worker_leave()` (worker.c); `XCE_TESTING`-only `fetchfile_wait_for_test()` seam added for regression coverage (~38719 tok)
|
||||
- `join.c` — include "config.h" (~2994 tok)
|
||||
- `main.c` — build-time-versioning task 3.2 (argp_program_version runtime-composed into static `version_buf[128]`, "My name is..." startup INFO reads that buffer) PLUS dirty-content-fingerprint task 3.2 (2026-08-21): new static `version_display_buf[128]` composed via `xce_compose_version_ex()` (adds `STR(VERSION_DIRTY_FINGERPRINT)`) feeds `argp_program_version` instead; `version_buf`'s composition + the startup INFO log are byte-identical/untouched (Req 3.2). Live-verified: `--version` -> `7ebcba6d-cdb7cc7d`, startup log -> `7ebcba6d-dirty` (~5346 tok)
|
||||
- `meson.build` (~3546 tok)
|
||||
- `misc.c` — include <errno.h> (~1359 tok)
|
||||
- `myapp.c` — define _GNU_SOURCE (~6277 tok)
|
||||
- `myconf.c` — define _GNU_SOURCE (~1180 tok)
|
||||
- `opget.c` — include "config.h" (~1730 tok)
|
||||
- `opupd.c` — include "config.h" (~714 tok)
|
||||
- `persist.c` — define _GNU_SOURCE (~106216 tok)
|
||||
- `queue.c` — include <errno.h> (~1554 tok)
|
||||
- `repl.c` — include <assert.h> (~9240 tok)
|
||||
- `reply.c` — include "config.h" (~590 tok)
|
||||
- `resources.c` — include <errno.h> (~50 tok)
|
||||
- `sched.c` — include <errno.h>; bug-1693 (2026-08-25): `handle_scheduler_leave()` now calls `schedadmin_signal_disconnect()` (cluster.c) BEFORE acquiring `c->sched.lock` (calling after would deadlock against the OPCode_schedAdmin dispatch case, which holds that lock for its whole wait) so a mid-wait scheduler disconnect wakes the pending waiter immediately instead of only at the 5s deadline (~6405 tok)
|
||||
- `slowlane.c` — define _GNU_SOURCE (~2392 tok)
|
||||
- `startup_validation.c` — validate_vcpus/peer_count/cert_files/startup_config/heartbeat_config; bug-1695: also hosts myapp_leader_already_known() (extracted from myapp.c so it links into controller_unit_test, which excludes myapp.c) (~3050 tok)
|
||||
- `task.c` — include <errno.h> (~1376 tok)
|
||||
- `worker.c` — include <errno.h>; bug-1693 (2026-08-25): `handle_worker_leave()` now calls `fetchfile_signal_worker_disconnect(w)` (job.c) right after nulling `w->ep`, so a mid-wait worker disconnect wakes any `on_fetchfile()` waiter proxied to it immediately instead of only at the 10s deadline (~8298 tok)
|
||||
- `commit.c.o` (~256615 tok)
|
||||
- `ctrl_metrics.c.o` (~14066 tok)
|
||||
- `ctrlstatus.c.o` (~253988 tok)
|
||||
- `egress.c.o` (~246954 tok)
|
||||
- `ha_convergence.c.o` (~259978 tok)
|
||||
- `join.c.o` (~259103 tok)
|
||||
- `opupd.c.o` (~252253 tok)
|
||||
- `queue.c.o` (~255055 tok)
|
||||
- `repl.c.o` (~256263 tok)
|
||||
- `slowlane.c.o` (~26286 tok)
|
||||
- `startup_validation.c.o` (~250657 tok)
|
||||
- `testsuite_configcompare.c.o` (~30589 tok)
|
||||
- `testsuite_controller_test.c.o` (~52147 tok)
|
||||
- `testsuite_rcfile.c.o` (~19766 tok)
|
||||
- `testsuite_snapdiff.c.o` (~30235 tok)
|
||||
- `testsuite_snapverify.c.o` (~40726 tok)
|
||||
- `testsuite_test_apply_reapply_idempotency.c.o` (~267824 tok)
|
||||
- `testsuite_test_apply_seam.c.o` (~259051 tok)
|
||||
- `testsuite_test_bug857_regression.c.o` (~271414 tok)
|
||||
- `testsuite_test_cluster_persist.c.o` (~269684 tok)
|
||||
- `testsuite_test_coldstart_lsn0.c.o` (~269200 tok)
|
||||
- `testsuite_test_commit_admission_pool.c.o` (~255382 tok)
|
||||
- `testsuite_test_configcompare.c.o` (~56534 tok)
|
||||
- `testsuite_test_ctrl_metrics.c.o` (~39039 tok)
|
||||
- `testsuite_test_dispatch_fail_terminal.c.o` (~265949 tok)
|
||||
- `testsuite_test_drive_async_config.c.o` (~258287 tok)
|
||||
- `testsuite_test_e2e_checkpoint.c.o` (~229647 tok)
|
||||
- `testsuite_test_e2e_gc.c.o` (~223678 tok)
|
||||
- `testsuite_test_e2e_wal.c.o` (~226506 tok)
|
||||
- `testsuite_test_egress_filter.c.o` (~272202 tok)
|
||||
- `testsuite_test_fetchfile_disconnect.c.o` (~261723 tok)
|
||||
- `testsuite_test_gc_retention.c.o` (~217751 tok)
|
||||
- `testsuite_test_ha_bootstrap_support.c.o` (~245136 tok)
|
||||
- `testsuite_test_ha_cluster.c.o` (~259030 tok)
|
||||
- `testsuite_test_ha_ctrlhello.c.o` (~268038 tok)
|
||||
- `testsuite_test_ha_elect_ctx_lifecycle.c.o` (~265129 tok)
|
||||
- `testsuite_test_ha_failover_prevote.c.o` (~274783 tok)
|
||||
- `testsuite_test_ha_failover.c.o` (~273020 tok)
|
||||
- `testsuite_test_ha_heartbeat_committed.c.o` (~266799 tok)
|
||||
- `testsuite_test_ha_redirect.c.o` (~252976 tok)
|
||||
- `testsuite_test_ha_repl_push.c.o` (~267090 tok)
|
||||
- `testsuite_test_ha_start_election_async.c.o` (~269563 tok)
|
||||
- `testsuite_test_ha_vote_floor_heartbeat.c.o` (~270577 tok)
|
||||
- `testsuite_test_hooks.c.o` (~248860 tok)
|
||||
- `testsuite_test_job_array_fence.c.o` (~270968 tok)
|
||||
- `testsuite_test_job_dispatch_fail_count.c.o` (~201471 tok)
|
||||
- `testsuite_test_job_dispatch.c.o` (~30881 tok)
|
||||
- `testsuite_test_jobdispatch_no_double_notify.c.o` (~273947 tok)
|
||||
- `testsuite_test_join_redirect.c.o` (~254908 tok)
|
||||
- `testsuite_test_p1_integration.c.o` (~254297 tok)
|
||||
- `testsuite_test_p2_integration.c.o` (~262497 tok)
|
||||
- `testsuite_test_p3_manual_failover.c.o` (~253028 tok)
|
||||
- `testsuite_test_persist_checkpoint_offload.c.o` (~274642 tok)
|
||||
- `testsuite_test_persist_checkpoint.c.o` (~223341 tok)
|
||||
- `testsuite_test_persist_recovery.c.o` (~274130 tok)
|
||||
- `testsuite_test_persist_repl.c.o` (~209772 tok)
|
||||
- `testsuite_test_persist_schema.c.o` (~247187 tok)
|
||||
- `testsuite_test_persist_types.c.o` (~200420 tok)
|
||||
- `testsuite_test_persist_wal.c.o` (~222899 tok)
|
||||
- `testsuite_test_rcfile.c.o` (~61221 tok)
|
||||
- `testsuite_test_repl_apply.c.o` (~217832 tok)
|
||||
- `testsuite_test_repl_catchup.c.o` (~42126 tok)
|
||||
- `testsuite_test_repl_majority.c.o` (~258306 tok)
|
||||
- `testsuite_test_repl_unit.c.o` (~269036 tok)
|
||||
- `testsuite_test_replay_apply_seam.c.o` (~269938 tok)
|
||||
- `testsuite_test_slowlane.c.o` (~51878 tok)
|
||||
- `testsuite_test_snapdiff.c.o` (~50170 tok)
|
||||
- `testsuite_test_snapverify.c.o` (~61878 tok)
|
||||
- `testsuite_test_term_adoption_vectors.c.o` (~273567 tok)
|
||||
- `testsuite_test_upgrade_snapshot_restore.c.o` (~267366 tok)
|
||||
- `testsuite_test_walcompare.c.o` (~243026 tok)
|
||||
- `testsuite_test_waldump.c.o` (~272426 tok)
|
||||
- `testsuite_walcompare.c.o` (~201332 tok)
|
||||
- `testsuite_waldump.c.o` (~267791 tok)
|
||||
- `worker.c.o` (~273718 tok)
|
||||
|
||||
## src/controller/include/
|
||||
## src/build-asan/controller/resval_typeresolve_unit_test.p/
|
||||
|
||||
- `cluster.h` — if !defined(_CLUSTER_H_); bug-1693: declares `fetchfile_signal_worker_disconnect(worker_t*)` and `schedadmin_signal_disconnect(void)` (~3400 tok)
|
||||
- `commit.h` — pragma once (~3894 tok)
|
||||
- `slowlane.h` — pragma once (~1149 tok)
|
||||
- `worker.h` — if !defined(_WORKER_H_) (~213 tok)
|
||||
- `testsuite_resval_typeresolve.c.o` (~228167 tok)
|
||||
- `testsuite_test_resval_typeresolve.c.o` (~245352 tok)
|
||||
|
||||
## src/controller/testsuite/
|
||||
## src/build-asan/controller/sched_push_unit_test.p/
|
||||
|
||||
- `clean_failover_preservation.sh` — clean_failover_preservation.sh — ha-term-safety task 5.2 live integration test. (~5327 tok)
|
||||
- `configcompare.c` — define _GNU_SOURCE (~2273 tok)
|
||||
- `configcompare.h` — pragma once (~1186 tok)
|
||||
- `controller_test.c` — include <argp.h> (~2506 tok)
|
||||
- `ctrlstatus_fmt.h` — ifndef _CTRLSTATUS_FMT_H_ (~631 tok)
|
||||
- `resval_typeresolve.c` — resval_resolve_type(): resource-name -> Value_u_which table lookup (14 known names unchanged) + task 2.1's numeric-default fallback / `-t` type_override for unrecognized names (~499 tok)
|
||||
- `resval_typeresolve.h` — declares resval_resolve_type()/RESVAL_TYPE_UNKNOWN; task 2.1 fallback now implemented (~676 tok)
|
||||
- `snapdiff.c` — define _GNU_SOURCE (~893 tok)
|
||||
- `snapdiff.h` — pragma once (~610 tok)
|
||||
- `snapverify.c` — define _GNU_SOURCE (~4322 tok)
|
||||
- `snapverify.h` — pragma once (~1339 tok)
|
||||
- `test_apply_committed_entry.c` — Declares directly (~11605 tok)
|
||||
- `test_apply_correctness_verification.c` — Declares driving (~19199 tok)
|
||||
- `test_apply_pump_integration.c` — Declares hook (~18120 tok)
|
||||
- `test_apply_pump.c` (~22429 tok)
|
||||
- `test_apply_reapply_idempotency.c` — Declares xce_job_t (~4312 tok)
|
||||
- `test_apply_seam.c` — raft-apply-decoupling task 2.1: calls apply_job_start/finish/fail/update_transition() directly on a bare jobmgr+job_t (no cluster_t), bypassing on_job_start/finish/fail, to prove the extracted guard cores work outside the receipt path (~3144 tok)
|
||||
- `test_bug857_regression.c` — Declares void (~6525 tok)
|
||||
- `test_cluster_persist.c` — define _GNU_SOURCE (~1755 tok)
|
||||
- `test_coldstart_lsn0.c` — Declares char (~4729 tok)
|
||||
- `test_commit_admission_pool.c` — Declares int (~2315 tok)
|
||||
- `test_commit_barrier_reply.c` — Declares uint8_t (~45624 tok)
|
||||
- `test_commit_barrier_rollout.c` — Declares explicitly (~29654 tok)
|
||||
- `test_commit_durability.c` — define _GNU_SOURCE (~33748 tok)
|
||||
- `test_commit_engine.c` (~19059 tok)
|
||||
- `test_commit_resolve.c` (~15028 tok)
|
||||
- `test_config_repl.c` — define _GNU_SOURCE (~93438 tok)
|
||||
- `test_configcompare.c` — define _GNU_SOURCE (~2224 tok)
|
||||
- `test_convergence_verification.c` — define _GNU_SOURCE (~20242 tok)
|
||||
- `test_ctrl_metrics.c` — define _GNU_SOURCE (~949 tok)
|
||||
- `test_ctrlstatus.c` — include <stdatomic.h> (~6287 tok)
|
||||
- `test_dispatch_commit.c` — Declares table (~7800 tok)
|
||||
- `test_dispatch_fail_retry.c` — task 5.2's fail->succeed->fail test (`test_rpc_failure_bound_resets_after_intervening_success`, toggles worker ep broken/working across `dispatch_a_job_test()` calls) proves Req 4.3 counter-reset end-to-end (GREEN). Its job.state==PEND/job.allocs==NULL assertions on the post-success retryable/terminal failures were REMOVED (task 5.2 remediation, 2026-08-23) after kiro-debug (HIGH confidence) found that sub-scenario unreachable via any real caller today -- see bug-1631 for the deferred analysis; the removed-assertion rationale is documented inline in the test (~13000 tok)
|
||||
- `test_dispatch_fail_terminal.c` — Declares table (~3499 tok)
|
||||
- `test_fetchfile_disconnect.c` — bug-1693 regression suite (NEW, 2026-08-25): drives job.c's real `fetchfile_signal_worker_disconnect()`/`fetchfile_wait_for_test()` (XCE_TESTING seam) directly with a plain stack `worker_t` (not the full `on_fetchfile()`/real jobmgr+workermgr chain, out of this fix's scope). Proves a worker disconnect wakes a pending fetchfile wait in <1s with `-ECONNRESET` (not the ~5-10s deadline's `-ETIMEDOUT`), and that a disconnect on a DIFFERENT worker never cross-wakes an unrelated waiter (~2197 tok)
|
||||
- `test_ha_durability.c` — Declares char (~5643 tok)
|
||||
- `test_ha_rdhup_integration.c` — Declares char (~8362 tok)
|
||||
- `test_ha_startup.c` — startup validation + dispatch gate + myapp_run building-block tests; bug-1695: 7 new tests for myapp_leader_already_known() (stale-restored-term regression, term-advance/primary-fd/first-boot/voted-for-name/self-vote cases) (~10750 tok)
|
||||
- `test_job_array_sched_push.c` — job-array-single-wal-record task 5.1: standalone binary (`sched_push_unit_test`) linking real `sched.c`+`dataex.c`+`egress.c` to test `sched_job_new_fun`/`xcesched_push_jobarray`; bug-1693 (2026-08-25): added a one-line link-satisfying `schedadmin_signal_disconnect()` stub (mirrors the file's existing `check_worker_()` stub) since `sched.c`'s `handle_scheduler_leave()` now calls the real one, defined in cluster.c, which this binary doesn't link (~2863 tok)
|
||||
- `test_job_dispatch_fail_count.c` (~1002 tok)
|
||||
- `test_job_dispatch_fail_count.c` — scheduler-dispatch-commit task 1.3: proves job_t.dispatch_fail_count (xce/types.h wrapper struct, new uint32_t next to submit_lsn) is a plain, directly settable/incrementable/resettable in-memory-only field; bare stack-local job_t, no jobmgr/RTTI, mirrors test_persist_types.c's test_slot_key_t_fields precedent. Data-model only -- no dispatch_a_job_() wiring (~1600 tok)
|
||||
- `test_job_dispatch.c` — Declares in (~1205 tok)
|
||||
- `test_jobdispatch_no_double_notify.c` — Declares table (~5490 tok)
|
||||
- `test_persist_checkpoint_offload.c` — bug-1694 regression suite: proves checkpoint_task_fn (persist.c) no longer blocks the delaymgr thread with real checkpoint I/O -- gated XCE_TESTING hook (persist_checkpoint_test_hook) + park/release probe mirrors test_ha_liveness.c's reconnect/configpush offload tests. 3 tests: GREEN (trigger returns promptly, body runs on a slow-lane thread), RED (flag OFF reproduces pre-fix inline blocking), no-cluster fallback (~3456 tok)
|
||||
- `test_repl_apply.c` — include <dirent.h> (~5812 tok)
|
||||
- `test_resval_typeresolve.c` — Declares override (~1118 tok)
|
||||
- `test_sched_admin_dispatch.c` — Unity tests for OPCode_schedAdmin dispatch (cluster.c) via `myreq_handler_for_test`: standby/not-connected/admission-exhausted rejects, success/timeout/rpc-status-only/business-error round trips. bug-1693 (2026-08-25): added `test_schedadmin_disconnect_wakes_immediately`, calling the real `schedadmin_signal_disconnect()` (cluster.c) from a second thread against a real pending wait -- proves a disconnect wakes it in <1s (not the real unshrunk 5s deadline) with a distinguishable `"scheduler disconnected"` SchedAdminReply. Calls `schedadmin_signal_disconnect()` directly rather than `handle_scheduler_leave()` since this binary doesn't link the real sched.c (test_config_repl.c's stub would silently swallow the call -- see cerebrum.md) (~8752 tok)
|
||||
- `test_sched_worker_change.c` — bug-1578 regression (sched_worker_change() dangling resvals_to_sched fix); standalone binary (`sched_worker_change_unit_test`) linking real sched.c+dataex.c+egress.c. bug-1693 (2026-08-25): upgraded its link-satisfying `schedadmin_signal_disconnect()` stub into a counting/capturing SPY, and added `test_scheduler_leave_signals_schedadmin_before_clearing_ep` proving the REAL `handle_scheduler_leave()` calls it exactly once and BEFORE nulling `c->sched.ep` (the deadlock-avoidance ordering) -- complements test_sched_admin_dispatch.c's test in controller_unit_test, where cluster.c is real but sched.c is stubbed; together the two prove both halves of the fix for real (~5279 tok)
|
||||
- `test_term_safety_adapter.c` — define _GNU_SOURCE (~29920 tok)
|
||||
- `test_wal_job_ckpt_fact.c` — bug-1691 regression suite: WAL_JOB_CKPT restart-drop fix for job.ckptinfo (plain char*, xstring-backed via makeString()). 7 tests: apply_committed_entry unconditional-decode (no-state-change is the key test, since on_job_ckpt() never changes state — mirrors bug-1690's RUSAGE divergence) + forward-transition + backward-compat, apply_wal_entry adopt + backward-compat, direct-payload restart replay, and a real on_job_ckpt() production-path restart-replay test (needed `persist_init(..., &c)` not NULL, since CKPT is hook-driven like RUSAGE, not a direct call site like DISPATCH/START/FINISH — persist_start() only registers persist_job_hook_fun when p->cluster != NULL) (~7000 tok)
|
||||
- `test_wal_job_dispatch_fact.c` — Declares in (~5222 tok)
|
||||
- `test_wal_job_finish_fact.c` — Declares as (~6904 tok)
|
||||
- `test_wal_job_preason_fact.c` — bug-1692 regression suite: WAL_JOB_PREASON restart-drop fix for job.pend_reasons (xce_job_preason_t*: host+resource-based reasons, plain calloc/strdup-backed, NOT xstring/RTTI). 11 tests: apply_committed_entry unconditional-decode (no-state-change is key, since set_preason()/do_clr_preason() never change state) + forward-transition + backward-compat + CLEAR (presence-byte=0 must reset to NULL — the one divergence from bug-1690/1691's precedent, since pend_reasons has a real clear op) + decode-edge-case, apply_wal_entry mirrors of same, direct-payload restart replay (host+resource reasons), and real do_set_preason()/do_clr_preason() production-path restart-replay tests (needed a minimal zeroed endpoint_t since do_set_preason() calls xce_reply() before processing — mirrors test_commit_barrier_reply.c's fixture) proving both SET and CLEAR survive restart (~11300 tok)
|
||||
- `test_wal_job_rusage_fact.c` — bug-1690 regression suite: WAL_JOB_RUSAGE restart-drop fix for job.run_usage. 8 tests covering both apply seams, backward compat, restart replay via the real persist_job_hook_fun(), and (key test) a merge-safety proof using a real makeString()'d host_name to confirm the fix never frees a live/xstring-backed pointer (~9250 tok)
|
||||
- `test_wal_job_start_fact.c` — Declares as (~6378 tok)
|
||||
- `test.c` — include <argp.h>. jobid-%lu-to-fmt_jobid task (2026-08-21): all 10 remaining raw `%lu` jobid printfs (cmd_jobkill, cmd_jobckpt, run_task_local, handle_notify x3, handle_runtasks x2, check_run_jobs, check_run_tasks) converted to `fmt_jobid(buf, sizeof(buf) - 1, ...)` + `%s`, matching this file's existing convention (buf/jobidStr/wantStr precedents at ~1559/1980/2405-2422); no new #include needed. Loop-scoped buffers repopulated fresh per iteration where the jobid varies (check_run_jobs, check_run_tasks, handle_notify's per-case union fields); reused a single populate when the jobid is loop-invariant (handle_runtasks: same runmsg->jobid across the task loop) (~40184 tok)
|
||||
- `dataex.c.o` (~255400 tok)
|
||||
- `egress.c.o` (~246954 tok)
|
||||
- `sched.c.o` (~267906 tok)
|
||||
- `testsuite_test_job_array_sched_push.c.o` (~264250 tok)
|
||||
|
||||
## src/frontend/
|
||||
## src/build-asan/controller/sched_worker_change_unit_test.p/
|
||||
|
||||
- `Makefile` — XCE project — test targets (~2823 tok)
|
||||
|
||||
## src/frontend/apiserver/
|
||||
|
||||
- `meson.build` — frontend/apiserver — Go REST API server with CGO backend integration. (~613 tok)
|
||||
|
||||
## src/frontend/apiserver/backend/
|
||||
|
||||
- `convert_pure.go` — Struct: rawStartInfo (~3599 tok)
|
||||
- `convert_test.go` — TestRawJobToModel_BasicFields, TestRawJobToModel_FromHost_EmptySafe, TestRawJobToModel_NilSubmit, Te (~7115 tok)
|
||||
- `convert.go` — go:build cgo (~3606 tok)
|
||||
- `handler_test.go` — TestOnHostConfigUpdated_UnregisteredHostIgnored, TestOnHostConfigUpdated_DefaultPseudoHostIgnored, T (~5356 tok)
|
||||
- `handler.go` — Struct: DefaultHandler (~2878 tok)
|
||||
- `integration_test.go` — go:build !cgo (~4494 tok)
|
||||
|
||||
## src/frontend/apiserver/cmd/
|
||||
|
||||
- `main_test.go` — dirty-content-fingerprint task 4.4: `TestComposeVersionDisplay`, table-driven, 6 cases covering strip-and-append, clean/no-fingerprint passthrough, degraded-dirty passthrough (`unknown`), append-with-nothing-to-strip, and an anchored-suffix (non-blind-substring) regression case; mutation-tested live (swapping the `"-dirty"` literal for `"-clean"` correctly fails 2/6 subtests) (~550 tok)
|
||||
- `main.go` (~1611 tok)
|
||||
|
||||
## src/frontend/apiserver/controllers/
|
||||
|
||||
- `jobs_test.go` — TestPreCheckJobRequest_LostAndFound, TestJobsListFieldValidation_UsesJobInfoNotSubmitRequest, TestPr (~1292 tok)
|
||||
- `jobs.go` — Struct: JobController (~30746 tok)
|
||||
|
||||
## src/frontend/apiserver/models/
|
||||
|
||||
- `jobs.go` — Struct: JobArrayID (~6787 tok)
|
||||
|
||||
## src/frontend/apiserver/store/
|
||||
|
||||
- `query.go` — ExecuteQuery, MatchesCondition, MatchesBitmaskCondition, IsBitmaskField, FilterRows (~2564 tok)
|
||||
|
||||
## src/frontend/xcecli/
|
||||
|
||||
- `meson.build` — frontend/xcecli — Go CLI tool (~195 tok)
|
||||
|
||||
## src/frontend/xcecli/cmd/
|
||||
|
||||
- `main_test.go` — dirty-content-fingerprint task 4.5: table-driven `TestComposeVersionDisplay` (6 cases incl. anchored-suffix regression), mirrors apiserver's task-4.4 test (~594 tok)
|
||||
- `main.go` (~1172 tok)
|
||||
|
||||
## src/frontend/xcecli/cmd/jobs/
|
||||
|
||||
- `submit.go` — Struct: JobSubmitter (~6572 tok)
|
||||
|
||||
## src/frontend/xcecli/common/request/
|
||||
|
||||
- `request_test.go` — TestExtractErrorMessage, TestAuthHeaderConsistency (~377 tok)
|
||||
- `request.go` — Struct: APIResponse (~2824 tok)
|
||||
|
||||
## src/frontend/xcecli/models/
|
||||
|
||||
- `jobs.go` — Struct: JobsResponse (~5211 tok)
|
||||
|
||||
## src/frontend/xcecli/router/
|
||||
|
||||
- `router.go` — Execute(cm, version string); build-time-versioning task 4.5: signature extended with `version string` param, sets `rootCmd.Version = version` before `rootCmd.Execute()` so Cobra's native `--version` handling prints/exits (~900 tok)
|
||||
|
||||
## src/include/
|
||||
|
||||
- `persist.h` — pragma once (~26099 tok)
|
||||
- `utils.h` — dirty-content-fingerprint task 3.1: new `xce_compose_version_ex()` extern decl (--version-only sibling of xce_compose_version(), adds optional `dirty_fingerprint` param); startup-log call sites keep calling the untouched xce_compose_version() (~2720 tok)
|
||||
- `wal_types.h` — pragma once (~4524 tok)
|
||||
|
||||
## src/include/xce/
|
||||
|
||||
- `types.h` — < NULL. (~5439 tok)
|
||||
|
||||
## src/lib/
|
||||
|
||||
- `group.c` — include <errno.h> (~2635 tok)
|
||||
- `jobj.c` — include <errno.h> (~6130 tok)
|
||||
- `utils.c` — dirty-content-fingerprint task 3.1: `xce_compose_version_ex()` added after xce_compose_version() -- when dirty_fingerprint empty, delegates straight to xce_compose_version() (byte-identical); when non-empty, strips a literal trailing "-dirty" off the commit field (anchored suffix check, not blind substring) and appends "-<dirty_fingerprint>" before composing. commit_id/xce_compose_version() itself untouched (~7035 tok)
|
||||
|
||||
## src/lib/testsuite/
|
||||
|
||||
- `test_group.c` — include <stdio.h> (~1659 tok)
|
||||
- `test_jobj.c` — include <stdio.h> (~3856 tok)
|
||||
- `test_utils.c` — dirty-content-fingerprint task 3.1: 5 new tests for xce_compose_version_ex() (strip-and-append with fingerprint, empty+clean, empty+dirty unchanged, NULL fingerprint, and a direct byte-for-byte regression proof against xce_compose_version()); all 5 pre-existing compose/version_field tests untouched and still passing (~2708 tok)
|
||||
|
||||
## src/nixSupport/
|
||||
|
||||
- `default.nix` — threads myCommitId AND (dirty-content-fingerprint task 2.2) myDirtyFingerprint to xce.nix/xcecli.nix callPackage calls; myBuildTime removed, no longer eval-time (~286 tok)
|
||||
- `xce.nix` — mesonFlags has `-Dcommit_id=` and (task 2.2) `-Ddirty_fingerprint=` (both eval-time, plain mesonFlags entries); a separate `preConfigure` hook computes `date -u +%Y-%m-%dT%H:%M:%SZ` at build time and appends `-Dbuild_time=` to `mesonFlagsArray` (fixes bug-1603: builtins.currentTime broke pure-eval) -- untouched by task 2.2 (~1190 tok)
|
||||
- `xcecli.nix` — same `preConfigure` build-time pattern as xce.nix, plus (task 2.2) `-Ddirty_fingerprint=` in mesonFlags (~292 tok)
|
||||
|
||||
## src/scheduler/
|
||||
|
||||
- `CLAUDE.md` — XCE Scheduler (~1188 tok)
|
||||
- `meson.build` (~1426 tok)
|
||||
|
||||
## src/scheduler/include/
|
||||
|
||||
- `core.h` — if !defined(_CORE_H_) (~8550 tok)
|
||||
- `plugin.h` — if !defined(_PLUGIN_H_) (~3583 tok)
|
||||
- `repl.h` — if !defined(_REPL_H_); task 12.5 added xce_repl_on_replay_complete()/xce_repl_check_replay_complete() (bug-1594) (~2258 tok)
|
||||
- `scheduler.h` — if !defined(_SCHEDULER_H_) (~4072 tok)
|
||||
|
||||
## src/scheduler/src/
|
||||
|
||||
- `core_clock.c` — include <stdarg.h> (~3441 tok)
|
||||
- `core_events.c` — include <errno.h> (~10963 tok)
|
||||
- `core_session.c` — include <errno.h> (~4367 tok)
|
||||
- `core.c` — include <errno.h> (~4310 tok)
|
||||
- `driver.c` — include <errno.h> (~9443 tok)
|
||||
- `handler.c` — include <errno.h> (~6283 tok)
|
||||
- `main.c` — build-time-versioning task 3.2 (argp_program_version runtime-composed into static `version_buf[128]`, both mutually-exclusive startup INFO lines read that buffer) PLUS dirty-content-fingerprint task 3.2 (2026-08-21): same split-buffer pattern as controller/main.c -- new static `version_display_buf[128]` via `xce_compose_version_ex()` feeds `argp_program_version`; `version_buf`/startup INFO lines untouched (Req 3.2). Live-verified: `--version` -> `7ebcba6d-cdb7cc7d`, startup log -> `7ebcba6d-dirty` (~5186 tok)
|
||||
- `plugin.c` — include <errno.h> (~4344 tok)
|
||||
- `recording_session.c` — include <stdint.h> (~2706 tok)
|
||||
- `repl_break.c` — include <stdio.h> (~7907 tok)
|
||||
- `repl_info.c` — include <stdio.h> (~9489 tok)
|
||||
- `repl_internal.h` — if !defined(_REPL_INTERNAL_H_); task 12.5 added replay_complete field + repl_job_state_name extern (~3602 tok)
|
||||
- `repl_params.c` — include <errno.h> (~4299 tok)
|
||||
- `repl.c` — include <ctype.h>; task 12.5 added xce_repl_on_replay_complete(), replay_complete guard on step/continue, quote-stripping in repl_split_tokens() (bug-1594/1596) (~6611 tok)
|
||||
- `resource.c` — include <errno.h> (~4509 tok)
|
||||
- `scheduler.c` — include <errno.h> (~15839 tok)
|
||||
- `sim_loop.c` — include <errno.h> (~13920 tok)
|
||||
- `sim_picker.c` — include <errno.h> (~1374 tok)
|
||||
- `sim_verify.c` — include <stdio.h> (~4694 tok)
|
||||
- `sim.c` — include <errno.h> (~6106 tok)
|
||||
- `snapshot_load.c` — include <errno.h> (~4005 tok)
|
||||
- `snapshot_wire.c` — include <errno.h> (~2611 tok)
|
||||
|
||||
## src/scheduler/src/plugins/
|
||||
|
||||
- `builtin_fcfs.c` — Declares signature. bug-1686 FIXED (2026-08-25): priv->cursor/job_cursor (raw ordq_item_t*) persist across separate next_job() calls, each with its own priv->lock acquisition; a properly-locked on_queue_update()/on_job_state_change()/fcfs_remove_job_everywhere() call in between could free the exact node a cursor rests on, causing a real heap-use-after-free on the next next_job() (live SIGSEGV in fcfs_cursor_enter_queue). Fixed by NULLing whichever cursor points at a node right before it's freed, at all 3 sites -- see file header comment + buglog bug-1686 (~11900 tok)
|
||||
- `limit.c` — plugin.h hooks: limit_update_config/limit_job_election/limit_prepare_allocation. bug-1685 FIXED (2026-08-25): limit_update_config()'s error path no longer calls kill_limit(limit) after a failed key_add() into privdata->limits (a kvmgr with .datakiller=kill_limit) -- that kvmgr's own xkvmgr_add() rollback (kvmgr.c) already frees `limit` once via xzarray_clear()'s killer callback (zarray.c) on any kvtab-level failure (e.g. duplicate name -> EEXIST); the removed second call was a double free (see limit.c's own inline comment + buglog bug-1685) (~4700 tok)
|
||||
|
||||
## src/scheduler/testsuite/
|
||||
|
||||
- `meson.build` — Declares vtask_entry (~19272 tok)
|
||||
- `scratch_gen_resource_replay.c` (~2735 tok)
|
||||
- `test_admin_request.c` — Declares level (~12713 tok)
|
||||
- `test_builtin_fcfs_cursor_uaf.c` — bug-1686 regression: white-box #include ../src/plugins/builtin_fcfs.c (always-ready sched_queue_is_ready/sched_job_is_ready doubles + plugin_register stub to satisfy the linker, same convention as test_picker_fcfs_session.c) driving priv->cursor/job_cursor directly. Single-threaded repro: next_job() (dispatches job 1, cursor parked on Q1's node) -> on_queue_update() re-sorting the SAME queue name (frees the OLD ordq_item_t node the cursor rests on) -> asserts priv->cursor/job_cursor now NULL -> next_job() again returns NULL cleanly, no crash (RED pre-fix: genuine ASAN heap-use-after-free in builtin_fcfs_next_job, matching the live SIGSEGV in fcfs_cursor_enter_queue) -> fresh begin_session()/next_job() round confirms job 1 and job 2 both still findable (no job permanently lost) (~2900 tok)
|
||||
- `test_clock.c` — Declares uint64_t (~5450 tok)
|
||||
- `test_core_events.c` — Declares and; real dup_val() reimpl added for link (bug-1585) (~7880 tok)
|
||||
- `test_core_isolation.c` — Declares char (~4069 tok)
|
||||
- `test_driver_decision_cb.c` — Declares is (~3683 tok)
|
||||
- `test_driver_session_ordering.c` — Declares started (~8520 tok)
|
||||
- `test_driver_translate.c` — Declares field (~9514 tok)
|
||||
- `test_handlers.c` — define UNIT_TEST 1 (~2728 tok)
|
||||
- `test_limit_plugin.c` — bug-1685 regression: white-box #include ../src/plugins/limit.c (stubs reset_sched_obj/get_resvals/put_resvals/plugin_register to satisfy the linker) driving the real limit_update_config()/limit_init() against a real xce_scheduler_t fixture (groupmgr_init()'d core.ugmgr/qgmgr/pgmgr/hgmgr). test_duplicate_limit_name_no_double_free: two xce_limit_config_t entries with the SAME name -> asserts rc==-EEXIST with no ASAN abort (RED pre-fix: genuine heap-use-after-free in kill_limit, confirmed live). test_distinct_limit_names_both_succeed: control case, two distinct names -> rc==0 (~2900 tok)
|
||||
- `test_no_bare_printf.c` — Declares char (~1702 tok)
|
||||
- `test_record_lifecycle.c` — Declares char (~12710 tok)
|
||||
- `test_repl_breakpoints_inspection.c` — Declares and (~19332 tok)
|
||||
- `test_repl_equivalence.c` — Declares char (~7658 tok)
|
||||
- `test_repl_execution_control.c` — Declares directly (~8901 tok)
|
||||
- `test_repl_snapshot_params.c` — Declares char (~8179 tok)
|
||||
- `test_resource_accounting.c` — Req 1.4 proof: sched_charge_account/sched_uncharge_account/sched_commit_job_alloc (resource.c) give identical resInUse/sharedInUse whether core comes from xce_scheduler_t or xce_sched_sim_driver_t; white-box core.c+resource.c (~3200 tok)
|
||||
- `test_resource_accounting.c` — Declares char (~4292 tok)
|
||||
- `test_resval_ownership.c` — Declares instead (~5478 tok)
|
||||
- `test_scheduler_dispatch_commit.c` — scheduler-dispatch-commit task 2.1 (Req 1.1-1.3): sched_commit_decision()'s DISPATCH branch sets job->state=JOB_STAT_RUN alongside allocs/accounts; whole-scheduler_srcs link, no sched_init()/xcecli_* wrap needed. Extended task 5.1 (Req 1.1-1.3,2.1,2.3, bug-1624 e2e regression): added test_dispatch_commit_survives_several_rounds_before_confirmation (GAP_ROUNDS=5 round-boundary loop, asserts resInUse restored every round) and test_dispatch_commit_multi_round_gap_matches_same_round_confirmation (explicit value comparison via run_dispatch_then_n_rounds(1) vs (GAP_ROUNDS) helper, not just a hardcoded-constant check); RED-verified by temporarily reverting scheduler.c's job->state=JOB_STAT_RUN line (4/5 tests failed as expected), restored clean (~5000 tok)
|
||||
- `test_sim_alt_events.c` — Declares char (~4966 tok)
|
||||
- `test_sim_contention_parity.c` — Declares calls (~14346 tok)
|
||||
- `test_sim_contention_parity.c` — scheduler-sim-decision-parity task 4.1 (Req 1.4, 2.1/2.2): record-then-replay-and-diff parity proof extending task 1.2's contention fixture (worker 10.0 capacity, jobs A/B/C wanting 8/5/2) to an ACTUAL round-by-round comparison -- Part A: real driven normal-mode recording (2 real rounds: A+C dispatch/B deny, then job D denied too, capacity fully saturated); Part B: hand-crafted `.replay` (JOB_STATE_CHANGE precedes matching DISPATCH for BOTH A and C, extending task 3's single-job technique) replayed via real unmodified sched_sim_driver_run(); real sched_account_jobs() reimpl augmented with a round-boundary resInUse snapshot spy (g_round_resinuse[]) giving per-round sim-side observability; asserts resInUse parity at EVERY round (not just final) plus decision-level (dispatch/pend) and sched_check_host_satisfy() capacity-check parity on both sides; white-box core.c+driver.c+resource.c+full sim/repl/recording stack; RED-phase discriminating power confirmed live (temporarily reverted sim_loop.c's reconciliation call site -- test failed "Expected 10 Was 0" -- restored, GREEN) (~9800 tok)
|
||||
- `test_sim_e2e_original_motivation.c` — scheduler-sim-decision-parity task 4.3 (FINAL task, Req 1.1-1.3, 3.1-3.3): live-verify substitute for the missing original bug-1611/bug-1613 motivating recording (`scheduler-replay-20260820-070103.replay`, confirmed absent from this filesystem before writing). A real, driven 24-round, 12-job, resource-constrained recording (worker 20.0 capacity; ANCHOR_ID holds 12.0 permanently; J2-J12 arrive/deny/dispatch/finish across the remaining 8.0 headroom via a capacity-aware allocator thread, same technique as test_sim_resource_contention.c) is replayed through the real, unmodified interactive REPL (fork+freopen stdin/stdout, test_repl_execution_control.c's own technique). ANCHOR_ID's own arrival+RUN-confirmation are pushed via the real dataq/translate path; its OWN "originating decision" is the one deliberately-injected piece (built with real make_charge_account/sched_commit_job_alloc/make_dispatch_decision constructors, fed through the real core->decisions cbuf), because reconciliation's successful-match path is PROVABLY unreachable from any organically-live-driven round (dispatch-round and RUN-confirmation-round can never coincide for a job a real/simulated picker itself decided) -- confirmed both by source analysis and empirically (J2-J12's own real dispatches never surface via resInUse during replay, only ANCHOR_ID's does, since only its trigger was engineered same-round). Asserts "info worker" shows used=12.00 (nonzero, ANCHOR_ID's charge, constant since no churn job is ever reconciled) at 4 checkpoints across the whole replay, plus ANCHOR_ID's own "[sim] DISPATCH jobid=..." line now visible in step/continue output (bug-1611's exact symptom, fixed). RED-phase discriminating power confirmed live (reverted sim_loop.c's reconciliation call site -- DISPATCH-visibility assertion failed for the right reason -- restored, git diff clean, GREEN) (~13500 tok)
|
||||
- `test_sim_e2e_original_motivation.c` (~12913 tok)
|
||||
- `test_sim_equivalence.c` — Declares BOTH (~8365 tok)
|
||||
- `test_sim_event_loop.c` — Declares char (~9352 tok)
|
||||
- `test_sim_isolation.c` — Declares names (~8174 tok)
|
||||
- `test_sim_reconcile_unaccounted.c` — Declares char (~9352 tok)
|
||||
- `test_sim_record_output.c` — Declares char (~5888 tok)
|
||||
- `test_sim_resource_contention.c` — scheduler-sim-decision-parity task 1.2 (Req 2.1/2.2): resource-contention replay fixture (worker w/ 10.0-unit capacity, 3 jobs wanting 8/5/2 -- job B genuinely denied by real sched_check_host_satisfy()/sched_charge_account()); proves fixture shape + dual-use (real normal-mode recording, also replayable via sim driver); white-box core.c+driver.c+resource.c+full sim/repl/recording stack (~9218 tok)
|
||||
- `test_sim_round_boundary_reconciliation.c` — scheduler-sim-decision-parity task 3 (Req 1.3/1.4, 2.1/2.2): end-to-end proof that reconciliation is wired before sched_account_jobs()/xce_core_signal_go() -- job A already RUN with no live decision (JOB_STATE_CHANGE event + matching recorded DISPATCH, hand-crafted `.replay`) gets its resInUse charged (via REAL make_job_account()/sched_charge_account(), including lazy resname->residx resolution through a pre-registered makeResource()) before sched_check_host_satisfy() correctly denies/grants hypothetical capacity; real (non-stub) sched_account_jobs() reimpl, unlike every sibling sim_loop.c-including test file (~7700 tok)
|
||||
- `test_sim_session_ordering.c` (~7772 tok)
|
||||
- `test_sim_snapshot_reconcile_gap.c` — scheduler-sim-decision-parity task 4.2 (Req 4.1/4.2 regression, the genuinely-new check (c)): a replay starting from a SNAPSHOT that already has a job (901) in RUN state with no in-window recorded decision anywhere -- pushes a QUEUE_UPDATE+JOBS_PUSH(job.state=JOB_STAT_RUN, no allocs) directly onto a throwaway xce_scheduler_t's core via real xce_core_push_events() BEFORE sched_record_start() snapshots it, so the written `.replay`'s initial SnapshotEntry carries the RUN job with job->accounts==NULL from byte one; asserts rc==0 (no crash), job->accounts/allocs stay NULL after replay (unavailable, not fabricated), job->state stays RUN, and a second ordinary job (902) is still decided normally -- proves builtin_fcfs's JOB_IS_PEND(job->state) guard in on_job_push also keeps this job out of live-picker candidacy, so reconciliation's "not found" branch (task 2.2) is the only path that ever touches it; white-box core.c+full sim/repl/recording stack, same set as test_sim_verify.c (~6117 tok)
|
||||
- `test_sim_startup.c` — Declares char (~5535 tok)
|
||||
- `test_sim_verify.c` — Declares mismatch; task 3 (Req 5.2) adds append_job_state_change_event()/append_replay_session_with_dispatch_and_pend() + test_verify_with_already_resolved_job_reports_no_mismatch(): --sim-verify against a recording with an already-resolved job reports zero mismatches now that reconciliation compacts decoded_session before the verify drain loop; task 4.2 (Req 5.1/5.2, check (b) extension) adds append_replay_session_with_two_dispatches_and_pend()/append_replay_session_with_two_pends() + test_verify_with_multiple_already_resolved_jobs_across_rounds_reports_no_mismatch(): TWO already-resolved jobs (A/C) in one round with a still-genuinely-PEND job's (B) own recorded PEND sandwiched BETWEEN the two DISPATCH entries (exercises non-adjacent memmove compaction, unlike task 3's single-leading-entry case), followed by an ordinary round 2 -- caught a real test-authoring gap live: builtin_fcfs's pendJobs discipline re-offers a still-PEND job EVERY subsequent round (not just its arrival round), so round 2's recorded fixture had to include B's recurring PEND too, not just the fresh job D's (~13500 tok)
|
||||
|
||||
## src/schema/
|
||||
|
||||
- `controller.capnp` — Declares OPCode (~15094 tok)
|
||||
- `dataex.c.o` (~255400 tok)
|
||||
- `egress.c.o` (~246954 tok)
|
||||
|
||||
## src/tools/
|
||||
|
||||
- `deploy-ha.sh` — auto-detects real `xcecd` vs `xcecd_virtual` (XCE_REAL_WORKER / /run/systemd/system), generates sudo-wrapped start-worker.sh for real (~8000 tok)
|
||||
- `go_build` — dirty-content-fingerprint task 4.1: reads `XCE_DIRTY_FINGERPRINT` (empty-safe like GitCommit/BuildTime), but falls back to plain `""` (NOT `"unknown"`, unlike the other two) and appends `-X main.DirtyFingerprint=$_dirty_fingerprint` to the existing `-ldflags` alongside GitCommit/BuildTime, unaffected (~730 tok)
|
||||
- `test-build-version-regression.sh` — build-time-versioning task 6.1: standalone regression script (follows `test-commit-durability.sh`/`test-heartbeat-tolerance.sh` precedent in this dir) running a REAL `nix develop $SRC_DIR --command bash xce-build.sh scheduler <asan|debug> <dedicated build dir>` for both profiles, then `xce_scheduler --version`, failing loudly if the parsed commit id is empty/`unknown`/mismatched vs `git rev-parse --short HEAD` (+`-dirty`). Also runs a detection-capability demonstration (deliberately-wrong hash / synthetic-empty / synthetic-`unknown` commit id must all be rejected; genuine match must pass). Tolerates xce-build.sh's own non-zero exit from its bundled scheduler-suite `meson test` (bug-1560, unrelated pre-existing flake) via `cmd || rc=$?` so that doesn't mask the version check. Dedicated build dirs cleaned up on exit unless `--keep-build-dirs` passed. Live-verified passing both profiles (~3373 tok)
|
||||
- `test-dirty-fingerprint-distinguishability.sh` — dirty-content-fingerprint task 6.1 (2026-08-21): standalone regression script (follows test-build-version-regression.sh's convention) proving the 4 required --version states hold via a real `xce-build.sh scheduler debug` build cycle. ISOLATION TECHNIQUE (the key design decision): since this shared repo is under continuous concurrent editing, `git status` on the ambient tree can't reliably represent "clean" -- creates its own throwaway `git worktree add --detach <tmp> HEAD` first (genuinely clean by construction), then invokes THAT worktree's own copy of xce-build.sh -- which works because xce-build.sh resolves SRC_DIR from `$0`'s location and `cd`s there itself, so every git command inside it auto-scopes to the isolated worktree with zero extra `-C`/`cd` bookkeeping. Edits `README.md` inside the isolated worktree only (`git checkout --` between edits for byte-exact reverts); shared tree's own README.md never touched (asserted via `git status --porcelain` at the end). 4 states: clean (`meson setup` fresh) -> edit-A -> edit-B (`--incremental` reconfigure for speed, safe since same single component/dir reused throughout) -> revert-to-A. `check_capture()` is the single function both the real per-state checks AND `demonstrate_detection_capability()` exercise (mirrors check_result/check_consistency precedent). Found+fixed bug-1615 live (a same-statement `local a=... b="$a/..."` cross-reference silently unbound under `set -u` -- split into two `local` statements). Live-verified: 2 full passing runs reproduce IDENTICAL fingerprints across independent runs (determinism proof); a 3rd run with edit-B deliberately forced to reproduce edit-A's content correctly FAILs (exit 1, differs-check catches the unexpected match) (~5200 tok)
|
||||
- `test-dirty-fingerprint-distinguishability.sh` — test-dirty-fingerprint-distinguishability.sh — dirty-content-fingerprint (~6112 tok)
|
||||
- `test-dirty-fingerprint-nix-production-path.sh` — test-dirty-fingerprint-nix-production-path.sh — dirty-content-fingerprint (~7683 tok)
|
||||
- `test-dirty-fingerprint-nix-production-path.sh` — dirty-content-fingerprint task 6.3 (2026-08-21, LAST task in spec): standalone regression script proving the NIX production path (`nix build ./src#xcecore_debug`, no `--impure`) carries task 2.2's `myDirtyFingerprint` into a real build attempt, not just `nix eval`. Reuses the family's isolated `git worktree add --detach` technique, but edits `flake.nix` itself (inside `src/`, the flake's own source) rather than README.md -- the exact edit target task 2.2's own research.md already validated for `self.narHash` sensitivity. SANDBOX FINDING (new, beyond task 2.2): `nix build` here dispatches to a REMOTE builder (`ssh://.../172.31.109.18`); `--keep-failed`'s leftover build dir (containing a real, linked `scheduler/xce_scheduler`) sits on that unreachable remote host (SSH connection times out), so unlike the dev-loop scripts this one can't extract-and-run a compiled binary -- mirrors task 2.2's own accepted technique instead: `nix eval --json <flake>#packages.x86_64-linux.xcecore_debug.mesonFlags` (fast, eval-time) cross-checked against the REAL build log's `mesonConfigurePhase flags:` line (via `-L --keep-going --keep-failed`, ~1-2 min per attempt thanks to ccache reuse across edits since only flake.nix's comment changes, not C/Go source). Treats the known bcctool/goproxy.cn failure as the expected PASSING outcome (`check_reached_known_limitation()` asserts every `FAILED: [code=` line -- nix's error epilogue reprints the same one 3x for xcecore_debug's 2 outputs -- references bcctool, not a new regression) and asserts `scheduler/xce_scheduler` was actually linked before that failure. States: clean (nix eval only, empty fingerprint) -> edit-A (eval+real build) -> edit-B different content (eval+real build, differs from A in BOTH eval and real build log) -> revert-to-A (eval only, matches A exactly). Found+fixed 3 live bugs: the same bug-1615 same-statement `local` cross-reference gotcha (in `extract_flag_from_json`), BusyBox `mktemp` rejecting a `.log` suffix after `XXXXXX` (GNU-only feature -- dropped the suffix), and a `git checkout -- flake.nix` pathspec bug (`-C "$ISO_DIR"` doesn't see `src/flake.nix` as `flake.nix` -- fixed to `-C "$ISO_SRC"`). Also caught and fixed a self-check false-positive: grepping the whole script for the literal string `--impure` matched its own header prose explaining the Observable -- narrowed to only lines beginning a real `nix eval/build/develop/log` invocation. Live-verified passing end-to-end (~1m54s total, well under the 10-min-per-build budget); zero leftover worktrees/tmp dirs/logs; shared repo's `src/flake.nix` confirmed untouched (~14000 tok)
|
||||
- `test-dirty-fingerprint-startup-log-unchanged.sh` — dirty-content-fingerprint task 6.2 (2026-08-21): standalone regression script proving the fingerprint (task 3.1-4.4) never leaks into either xce_scheduler's or xce_apiserver's startup log, even on a dirty tree where `--version`/`-version` clearly does show one. Reuses task 6.1's isolated `git worktree add --detach` technique (one controlled dirty edit to README.md inside the isolated worktree only) to build scheduler for real via `xce-build.sh scheduler debug`; builds apiserver via a direct `go build -ldflags` invocation (mirrors test-version-startup-consistency.sh's bug-1597 workaround) injecting the REAL fingerprint extracted from the scheduler's own `--version` output (not a second independent hash computation -- simpler and more faithful to a real single-session `xce-build.sh` run). `assert_version_shows_fingerprint()` confirms `--version`/`-version` shows `<commit>-<fingerprint>` (never the old `-dirty` format); `assert_startup_log_unchanged()` confirms the startup log's commit field is byte-identical to the original build-time-versioning format (`<commit>-dirty`) AND that the fingerprint string is absent anywhere in the line (not just the commit field). `demonstrate_detection_capability()` proves both functions reject: fingerprint leaking straight into the commit field, fingerprint leaking via an unrelated field while the commit field looks fine, and a commit-field format drift with no fingerprint involved at all -- plus a sanity pass on genuinely unchanged lines. Cleanup kills daemons by PID first, then scans `/proc/*/exe` (not cmdline text, to avoid bug-1576's self-match hazard) for any leftover process pointing at the built binaries before removing the isolated worktree. Live-verified passing twice end-to-end (real scheduler + apiserver builds and daemon starts each run); zero leftover processes/worktrees; shared repo's README.md confirmed untouched both times (~9800 tok)
|
||||
- `test-version-startup-consistency.sh` — test-version-startup-consistency.sh — build-time-versioning task 6.2 (~6131 tok)
|
||||
- `test-xceadmin-build-install-version.sh` — build-time-versioning task 6.3 (LAST task in spec): real `nix develop $SRC_DIR --command bash xce-build.sh xceadmin release <dir>` (meson-driven: schema sync, pinned venv install, pytest, wheel build), then installs the wheel into one fresh scratch venv and does `pip install -e src/xceadmin` into a second fresh scratch venv (both venv creation/pip install/`xceadmin --version` runs happen inside `nix develop` -- confirmed live that pycapnp's prebuilt PyPI wheel needs the nix devshell's libstdc++.so.6 resolution, absent on the bare host). Extracts the `(<commit>, <build_time>)` suffix from `xceadmin --version`'s "xceadmin, version <pkgver> (...)" output (isolating that line first via `VERSION_LINE_ANCHOR` grep before parsing parens, since captures are `2>&1` and include `nix develop`'s own "Git tree is dirty" stderr noise) and asserts: wheel install = real (non-`unknown`) fields; editable install = `unknown`/`unknown` (task 5.2 stub); the two DIFFER. `demonstrate_detection_capability()` proves synthetic unknown-in-wheel / real-in-editable / identical-outputs are all rejected. Cleanup always sweeps `.egg-info`/`__pycache__`/`.pytest_cache` left in the real `src/xceadmin` tree by the editable install (even with `--keep-artifacts`), plus the scratch venvs and meson build dir. Live-verified passing twice, `git status --porcelain` on `src/xceadmin` clean after each run (~4900 tok)
|
||||
- `test-xceadmin-build-install-version.sh` — test-xceadmin-build-install-version.sh — build-time-versioning task 6.3 (~5819 tok)
|
||||
- `xce-build.sh` — build-time-versioning task 2.1 computes commit_id (git rev-parse --short HEAD [+ -dirty]) + build_time (date -u) once per invocation, appended to MESON_OPTS for every profile. dirty-content-fingerprint task 2.1 (2026-08-21) adds a NEW, independent `dirty_fingerprint` computation right after that block (commit_id/build_time untouched): empty on a clean tree; sha256 (8 hex chars) over `git diff HEAD` + sorted raw content of `git ls-files --others --exclude-standard` when dirty; empty (never fabricated) if the hash computation itself fails (e.g. sha256sum missing) -- every intermediate command is guarded via explicit `if`/`elif` so a failure degrades gracefully instead of tripping the script's own `set -euo pipefail`. Uses two `mktemp` scratch files (cleaned up unconditionally), zero working-tree/index mutation. Appended as `-Ddirty_fingerprint="$dirty_fingerprint"` on the same MESON_OPTS line as commit_id/build_time, so it reaches every profile (asan/debug/release) identically. Live-verified via real `nix develop ... xce-build.sh scheduler debug`+`lib asan`+`lib release` builds against `compile_commands.json`'s `-DVERSION_DIRTY_FINGERPRINT=`: edit-A -> hash X, edit-B -> different hash Y, revert-to-A -> hash X again exactly, `.bak`-gitignored file added -> hash unchanged; `commit_id`'s `-dirty` suffix confirmed unaffected throughout. `set -euo pipefail` means its bundled scheduler-suite `meson test` failing (bug-1560) makes the WHOLE invocation exit non-zero even when compile succeeded (~4400 tok)
|
||||
|
||||
## src/worker/src/
|
||||
|
||||
- `controller_connector.c` — include "client.h"; jobid-readability (2026-08-21): 21 job-id `%lu` log sites across 10 functions (`dump_startjob_request`, `job_start_remote_task`, `job_start_local_task`, `handle_startjob`, `handle_runtasks`, `handle_fetchfile`, `handle_jobcontrol`, `xcecd_job_remote_start`, `xcecd_jobrusage_update`) now render via `fmt_jobid()` into a local `jobidStr[64]` instead of raw uint64; `handle_fetchfile`'s `offset=%lu` byte-offset field is NOT a jobid and was left raw (~18100 tok)
|
||||
- `main.c` — include "sqlite3.h"; jobid-readability (2026-08-21): 4 job-id `%lu` log sites (`master_disconnect_callback`, `master_job_status_callback`, `master_ckpt_result_callback`, `master_rusages_callback`) now render via `fmt_jobid()` into a local `jobidStr[64]` instead of raw uint64 (~6282 tok)
|
||||
- `resource_sync.c` — include "resource_sync.h" (~3926 tok)
|
||||
- `uv_ipc.c` — include "uv_ipc.h" (~12231 tok)
|
||||
- `xcecd_cgroups.c` — include "xcecd_cgroups.h"; jobid-readability (2026-08-21): `collect_job_rusages()`'s rusage TRACE log job-id `%lu` -> `fmt_jobid()`/`jobidStr[64]`; the `xcecd-worker-%lu.service` unit-name snprintf (line ~1406) deliberately left untouched (behavioral, not log formatting) (~13889 tok)
|
||||
- `xcecd_dbus_manager.c` — include "xcecd_dbus_manager.h"; jobid-readability (2026-08-21): `dbus_stop_all_workers()`'s "Job %lu killed" log -> `fmt_jobid()`/per-iteration `jobidStr[64]` (~3777 tok)
|
||||
- `xcecd_worker_utils.c` — include "xcecd_worker_utils.h"; jobid-readability (2026-08-21): 9 job-id `%lu` log sites across `update_worker_status()` (6, shared one `jobidStr[64]` populated once after `job->job_id` is set), `find_worker_by_jobid()`, `resend_job_status()` (per-iteration buffer), `check_worker_sleep_time_elapsed()` -> all now `fmt_jobid()` (~4658 tok)
|
||||
- `xcecd_worker.c` — include "inter_job_utils.h"; jobid-readability (2026-08-21): 30 job-id `%lu` log sites (31 conversions, `worker_signal_callback`'s "Signal for job X but current job is Y" line has two) across `on_rusage_timer`, `xjob_start_vnc`/`xjob_stop_vnc`, `worker_job_callback` (11, shared one `jobidStr[64]` populated once after `jobid = info->job_id`), `worker_signal_callback` (`jobidStr` for param `job_id` + separate `curJobidStr` for global `jobid`), `do_criu_ckpt`, `worker_ckpt_callback` -> all now `fmt_jobid()`; `snprintf(ckpt_dir, ..., "%s/%lu", CKPT_BASE, job_id)` (on-disk path, not a log) intentionally left raw (~12812 tok)
|
||||
- `xcecd.c` — fake pids used in virtual cluster; jobid-readability (2026-08-21): `xcecd_process_command()`'s "No startinfo provided, job %lu" log -> `fmt_jobid()`/`jobidStr[64]` (~5154 tok)
|
||||
|
||||
## src/worker/testsuite/
|
||||
|
||||
- `meson.build` — Worker unit test suite (component-failover-adoption, task 2.1). White-box (~1147 tok)
|
||||
- `test_dbus_workers_update.c` — Declares started (~3398 tok)
|
||||
- `test_resource_sync.c` (~1374 tok)
|
||||
|
||||
## src/xceadmin/
|
||||
|
||||
- `meson-build.sh` — Meson-driven xceadmin build: install + test out-of-tree, in the meson; dirty-content-fingerprint task 5.1 (2026-08-21): reads new $8 positional `dirty_fingerprint` (independent empty-string fallback, NOT "unknown"), writes `DIRTY_FINGERPRINT` as a third field into generated `_build_info.py`. Live-verified: dirty tree -> real hash matching compile_commands.json's `-DVERSION_DIRTY_FINGERPRINT=`; `-Ddirty_fingerprint=` explicit empty -> `DIRTY_FINGERPRINT = ""` while GIT_COMMIT/BUILD_TIME still fall back to "unknown" independently; 261 tests pass in all scenarios (~2100 tok)
|
||||
- `meson.build` — xceadmin (admin-config-tool): Python package, no C compilation; dirty-content-fingerprint task 5.1: custom_target gains a third positional arg `get_option('dirty_fingerprint')` after commit_id/build_time (~1140 tok)
|
||||
- `pyproject.toml` — XCE cluster configuration admin CLI (config build / config push) (~662 tok)
|
||||
|
||||
## src/xceadmin/src/xceadmin/
|
||||
|
||||
- `_build_info.py` — Committed fallback build-info stub (build-time-versioning task 5.2; (~360 tok)
|
||||
- `cli.py` — xceadmin command-line entry point (design.md, ``xceadmin.cli``; (~4225 tok)
|
||||
|
||||
## src/xceadmin/tests/
|
||||
|
||||
- `test_cli.py` — Tests for the xceadmin CLI (design.md, ``xceadmin.cli``; task 1.1's (~8975 tok)
|
||||
- `deploy-ha.sh` (~8298 tok)
|
||||
|
|
|
|||
|
|
@ -17963,7 +17963,7 @@
|
|||
"error_message": "Discovered analyzing a real 108-session replay (xce_scheduler -i -s scheduler-replay-20260820-070103.replay, continue to EOF): sim_print_decision() (sim_loop.c) never printed a single DISPATCH or PREEMPT line across the entire replay -- only PEND -- even though 22 of the 30 jobs pushed at round 32 clearly transitioned to running (confirmed by ongoing JOB_UPDATE/JOB_RUSAGE ticks for jobids [0]-[21] every few rounds through round 107). An operator watching only decision output has zero confirmation those 22 jobs ever dispatched; they just silently stop appearing in PEND.",
|
||||
"file": "src/scheduler/src/sim_loop.c",
|
||||
"root_cause": "Not yet root-caused with certainty. Plausible, benign explanation: in --sim/-s REPLAY mode, jobs already resolved (dispatched) in the ORIGINAL recorded run may be replayed purely as state deltas (JOB_UPDATE/JOB_RUSAGE) without the live picker/allocator re-deriving a fresh XCE_DECISION_DISPATCH -- only jobs still outstanding (PEND) get freshly re-decided every round, which is why PEND is the only decision op ever observed. Unconfirmed against sim_loop.c's actual replay-vs-live-decide branching; needs a source read of how sim_loop.c distinguishes replayed-decision playback from freshly-computed decisions before concluding this is 'working as designed' vs a real visibility gap.",
|
||||
"fix": "Not yet fixed. If confirmed benign (dispatch legitimately has no fresh decision object in replay mode), the round-summary rollup being designed for bug-1609/1610 should not label its dispatch count misleadingly (a permanent 'X dispatch' column reading 0 for an entire replay looks like a bug even when it isn't) -- consider deriving a running-job delta from info-jobs-equivalent state instead of the decision tally for replay-mode display. If NOT benign (a real decision-visibility gap), this is a separate, more serious finding than the formatting issues in bug-1609/1610.\n\n--- STATUS UPDATE 2026-08-24 (xce-audit-invariants scheduler audit) ---\n\nRoot cause confirmed and the literal mechanism is now built: sim_reconcile_unaccounted_jobs() (sim_loop.c:566-709, landed via scheduler-sim-decision-parity tasks 2.1-3, 2026-08-22) reconstructs and reports a decision for a RUN-state job whose DISPATCH decision was only in the recorded replay stream. BUT its trigger gate (sim_loop.c:592) requires JOB_IS_RUN(job->state) before searching, and the search only scans the CURRENT round's decoded_session (decoded fresh at sim_loop.c:871, freed at line 999 -- no cross-round persistence). So reconciliation only fires when a job's DISPATCH decision and its RUN confirmation land in the exact same replay round. This bug's own original scenario (dispatch at round 32, RUN confirmation through round 107) never matches -- the round-32 DISPATCH decision is gone by the time the job reaches RUN state. The scheduler-sim-decision-parity spec's own task 4.3 implementation notes explicitly acknowledge this residual does NOT fix bug-1611's dominant real-world manifestation. A follow-up spec already exists for this exact residual: .kiro/specs/scheduler-sim-eager-reconciliation/ (created 2026-08-22, phase=requirements-generated, ready_for_implementation=false -- design/tasks not yet generated). Its diagnosis: decouple allocation CAPTURE from the JOB_IS_RUN gate (capture as soon as a DISPATCH decision is seen for a job with job->accounts == NULL, regardless of state), leaving sched_account_jobs()'s existing JOB_IS_RUN + job->accounts != NULL gate to keep deferring actual charging until state transitions to RUN. Fix location: sim_reconcile_unaccounted_jobs() (sim_loop.c:566-709), trigger gate at line 592, plus its ownership-transfer logic (609-693) needs a double-capture guard since capture would no longer imply state==RUN. Do NOT fix inline as a drive-by patch per scheduler-driver-equivalence.md's testing discipline -- promote scheduler-sim-eager-reconciliation to design/tasks instead.",
|
||||
"fix": "Not yet fixed. If confirmed benign (dispatch legitimately has no fresh decision object in replay mode), the round-summary rollup being designed for bug-1609/1610 should not label its dispatch count misleadingly (a permanent 'X dispatch' column reading 0 for an entire replay looks like a bug even when it isn't) -- consider deriving a running-job delta from info-jobs-equivalent state instead of the decision tally for replay-mode display. If NOT benign (a real decision-visibility gap), this is a separate, more serious finding than the formatting issues in bug-1609/1610.\n\n--- STATUS UPDATE 2026-08-24 (xce-audit-invariants scheduler audit) ---\n\nRoot cause confirmed and the literal mechanism is now built: sim_reconcile_unaccounted_jobs() (sim_loop.c:566-709, landed via scheduler-sim-decision-parity tasks 2.1-3, 2026-08-22) reconstructs and reports a decision for a RUN-state job whose DISPATCH decision was only in the recorded replay stream. BUT its trigger gate (sim_loop.c:592) requires JOB_IS_RUN(job->state) before searching, and the search only scans the CURRENT round's decoded_session (decoded fresh at sim_loop.c:871, freed at line 999 -- no cross-round persistence). So reconciliation only fires when a job's DISPATCH decision and its RUN confirmation land in the exact same replay round. This bug's own original scenario (dispatch at round 32, RUN confirmation through round 107) never matches -- the round-32 DISPATCH decision is gone by the time the job reaches RUN state. The scheduler-sim-decision-parity spec's own task 4.3 implementation notes explicitly acknowledge this residual does NOT fix bug-1611's dominant real-world manifestation. A follow-up spec already exists for this exact residual: .kiro/specs/scheduler-sim-eager-reconciliation/ (created 2026-08-22, phase=requirements-generated, ready_for_implementation=false -- design/tasks not yet generated). Its diagnosis: decouple allocation CAPTURE from the JOB_IS_RUN gate (capture as soon as a DISPATCH decision is seen for a job with job->accounts == NULL, regardless of state), leaving sched_account_jobs()'s existing JOB_IS_RUN + job->accounts != NULL gate to keep deferring actual charging until state transitions to RUN. Fix location: sim_reconcile_unaccounted_jobs() (sim_loop.c:566-709), trigger gate at line 592, plus its ownership-transfer logic (609-693) needs a double-capture guard since capture would no longer imply state==RUN. Do NOT fix inline as a drive-by patch per scheduler-driver-equivalence.md's testing discipline -- promote scheduler-sim-eager-reconciliation to design/tasks instead.\n\n--- STATUS UPDATE 2026-08-27 (scheduler-sim-eager-reconciliation task 1.1) ---\n\nCore production fix landed: sim_reconcile_unaccounted_jobs() trigger gate (sim_loop.c:592) had !JOB_IS_RUN(job->state) removed, leaving job->accounts == NULL as the sole capture-eligibility gate -- exactly the diagnosis from the 2026-08-24 status update. Match/capture/report mechanics (609-693) were confirmed already safe for early (pre-RUN) capture without changes (see scheduler-sim-eager-reconciliation/research.md's uncharge-safety research log -- sched_uncharge_account() has zero production callers, accounting is reset-and-resum not incremental, so no double-capture guard beyond the existing job->accounts != NULL check was needed). Old test test_reconcile_skips_non_run_state_job (which asserted the bug as correct behavior) replaced with test_reconcile_captures_non_run_state_job_when_dispatch_recorded (inverted expectation). RED/GREEN verified: new test failed under the old gate (job->allocs stayed NULL, confirmed via a real build+run before the fix), then passed after the one-line fix; full sched_sim_reconcile_unaccounted_test suite green (10/10, all prior regression tests unchanged). This is task 1.1 of the spec only -- task 1.2 (capture-safety unit coverage), task 2.1 (resInUse deferral integration test), and task 3.x (contention/--sim-verify/full-regression validation) are still pending before the spec itself is implementation-complete.",
|
||||
"tags": [
|
||||
"repl",
|
||||
"sim",
|
||||
|
|
@ -17977,8 +17977,8 @@
|
|||
"bug-1609",
|
||||
"bug-1610"
|
||||
],
|
||||
"occurrences": 2,
|
||||
"last_seen": "2026-08-24T21:30:00Z"
|
||||
"occurrences": 3,
|
||||
"last_seen": "2026-08-27T03:35:00Z"
|
||||
},
|
||||
{
|
||||
"id": "bug-1612",
|
||||
|
|
@ -19563,6 +19563,108 @@
|
|||
"related_bugs": [],
|
||||
"occurrences": 1,
|
||||
"last_seen": "2026-08-26T14:25:00Z"
|
||||
},
|
||||
{
|
||||
"id": "bug-1717",
|
||||
"timestamp": "2026-08-26T00:00:00Z",
|
||||
"error_message": "ASAN heap-use-after-free abort in controller_unit_test: commit_resolve_pending (commit.c:807) racing fsync_worker (persist.c:867) against tap_teardown (test_apply_pump.c:243)",
|
||||
"file": "src/controller/commit.c",
|
||||
"root_cause": "Background fsync_worker thread appears to still be resolving a pending commit via commit_resolve_pending() while a test teardown (tap_teardown in test_apply_pump.c) is freeing/tearing down state concurrently -- a shutdown-ordering race between the async fsync/commit-resolution thread and per-test teardown. Observed once during xce-test-rc-file task 1.2 review (ad-hoc standalone controller_unit_test run); did NOT reproduce on a second standalone run or in the canonical xce-build.sh run (both completed cleanly with 1213 Tests 2 Failures 0 Ignored, the 2 failures being the unrelated tracked bug-1561). Not caused by and unrelated to rcfile.c/test_rcfile.c (xce-test-rc-file spec) -- flagged as a pre-existing flake surfaced incidentally during that task-local review.",
|
||||
"fix": "Not yet root-caused or fixed -- logged for tracking since it did not reproduce reliably enough to investigate further in that review pass. Next occurrence should capture the full ASAN report (not just the top frames) and check whether test_apply_pump.c's teardown waits for fsync_worker to fully quiesce before freeing shared state.",
|
||||
"tags": [
|
||||
"flaky",
|
||||
"asan",
|
||||
"use-after-free",
|
||||
"controller_unit_test",
|
||||
"commit.c",
|
||||
"persist.c",
|
||||
"fsync_worker",
|
||||
"test_apply_pump.c",
|
||||
"teardown-race",
|
||||
"unreproduced"
|
||||
],
|
||||
"related_bugs": [],
|
||||
"occurrences": 1,
|
||||
"last_seen": "2026-08-26T00:00:00Z"
|
||||
},
|
||||
{
|
||||
"id": "bug-1718",
|
||||
"timestamp": "2026-08-26T16:23:00Z",
|
||||
"error_message": "A python3 pty-driver embedded via a heredoc on a command that ALSO receives piped stdin silently discards the piped input",
|
||||
"file": "src/controller/testsuite/test_rcfile_integration.sh",
|
||||
"root_cause": "First draft of run_interactive_pty() did: printf '%s\\n' \"$script\" | python3 - \"$XCE_TEST\" ... <<PYEOF ... PYEOF. python3 - reads its ENTIRE program from stdin until EOF; a <<HEREDOC redirect on the same command line takes precedence over the pipe for that command's stdin (the pipe's implicit fd0 assignment is overridden by the explicit redirect), so the heredoc (the Python source) became the process's only stdin content. Once python3 finished reading its own program from that stream, stdin was already at EOF -- the script's own sys.stdin.read() (meant to receive the piped xce_test command script) returned empty every time, silently. No error was raised; it just produced an empty commands list, which would have made every interactive-scenario assertion trivially fail once real command sending began (caught before ever running the script standalone, via careful re-reading of the draft rather than via a failing run).",
|
||||
"fix": "Write the Python driver ONCE to its own temp file via mktemp + cat >\"$PTY_DRIVER\" <<PYEOF ... PYEOF at script startup (cleaned up by an EXIT trap), then invoke it as printf ... | python3 \"$PTY_DRIVER\" args... -- this leaves the pipe as the sole, unambiguous stdin source for the running Python process, since python3 <file> does not consume stdin to obtain its own source.",
|
||||
"tags": [
|
||||
"bash",
|
||||
"python",
|
||||
"pty",
|
||||
"heredoc",
|
||||
"stdin",
|
||||
"xce-test-rc-file",
|
||||
"task-3.1"
|
||||
],
|
||||
"related_bugs": [],
|
||||
"occurrences": 1,
|
||||
"last_seen": "2026-08-26T16:23:00Z"
|
||||
},
|
||||
{
|
||||
"id": "bug-1719",
|
||||
"timestamp": "2026-08-26T17:14:00Z",
|
||||
"error_message": "resource-value-verification skill's deployment failed: 'controller binary missing' immediately after a scheduler-only xce-build.sh run that had just succeeded",
|
||||
"file": "src/tools/xce-build.sh",
|
||||
"root_cause": "During xce-test-rc-file task 3.3 (compatibility verification, no product-code change), ran `nix develop ./src --command bash src/tools/xce-build.sh scheduler` (no --incremental) to fill a gap where xce.build/scheduler/xce_scheduler had never been linked. xce-build.sh's documented default behavior (see its own top-of-file comment) does a full `rm -rf \"$BUILD_DIR\"` + fresh `meson setup` on every invocation unless --incremental is passed, then `meson compile` only rebuilds the ONE requested component -- so the plain `scheduler` invocation silently wiped xce.build/controller/xce_controller and xce.build/controller/xce_test (built moments earlier by task 3.1/3.2's controller build, containing the rc-file feature under test) even though I never touched controller source. A prior attempt in the same session had additionally passed an explicit build_dir arg (`src/xce.build`) which -- combined with xce-build.sh's own internal cd into src/ -- produced a bogus nested `src/src/xce.build` directory instead of reusing the real one (harmless, fully gitignored via meson's own auto-generated per-builddir `.gitignore: *`, but wasted a build cycle and caused brief confusion investigating why `git status`/`git check-ignore` showed nothing for it).",
|
||||
"fix": "Rebuilt controller incrementally (`xce-build.sh controller asan xce.build --incremental`) to restore xce_controller/xce_test alongside the already-built scheduler, without wiping it back out. Confirmed both controller_unit_test (1213 Tests 0 Failures -- even bug-1561's usually-flaky 2 tests passed clean this run) and the scheduler suite (53/54, only the pre-existing tracked bug-1560 symlink-escape failure) came back healthy. General lesson for any task that needs a SECOND component built into an ALREADY-built xce.build dir: always pass --incremental, and never pass an explicit build_dir positional unless it already matches xce-build.sh's own default relative-path resolution (omit the 3rd arg and let it default to 'xce.build' resolved from src/tools/xce-build.sh's own cwd, don't hand it a repo-root-relative 'src/xce.build' path).",
|
||||
"tags": [
|
||||
"xce-build.sh",
|
||||
"incremental",
|
||||
"build-dir-clobber",
|
||||
"scheduler",
|
||||
"controller",
|
||||
"xce-test-rc-file",
|
||||
"task-3.3"
|
||||
],
|
||||
"related_bugs": [],
|
||||
"occurrences": 1,
|
||||
"last_seen": "2026-08-26T17:14:00Z"
|
||||
},
|
||||
{
|
||||
"id": "bug-1720",
|
||||
"timestamp": "2026-08-26T17:17:00Z",
|
||||
"error_message": "xce-test-resources run-resource-verify.sh: apiserver layer UNREACHABLE for all 9 injected resources; apiserver.log showed 'API Server fails to start: listen tcp :8080: bind: address already in use'",
|
||||
"file": "environment (stale orphaned process, not a code defect)",
|
||||
"root_cause": "A leftover /tmp/apiserver process (pid 1458981, launched via /tmp/xce-deploy-bug1701/scripts/start-apiserver.sh) from an unrelated, already-concluded prior session's bug-1701 investigation was still running and bound to :8080 -- 7.5 hours old (started 09:44, found at 17:18) with its own deploy directory (/tmp/xce-deploy-bug1701) already deleted, i.e. genuinely abandoned rather than an active concurrent session. This is a general sandbox hygiene gap: xce-test-resources' own trap-based cleanup only covers processes IT started: nothing in this sandbox periodically reaps orphaned daemons left behind by an unrelated script/session that itself never got to run its own cleanup (e.g. was killed externally, or its subagent's context ended before the trap fired).",
|
||||
"fix": "Identified via `lsof -i :8080` / `ss -ltnp`, confirmed genuinely orphaned (dead deploy dir, long uptime, no relation to this session's own work), then `kill -9` on both the wrapper script and the apiserver binary PID. Re-ran run-resource-verify.sh immediately after and got a clean PASS (9 resources, 3 layers, zero mismatches/unreachable/rejected). General lesson: an apiserver-layer UNREACHABLE result in this skill should prompt checking `lsof -i :8080` / `pgrep -af apiserver` for a stale bind-conflict BEFORE suspecting a propagation regression -- the skill's own FAILURE/UNREACHABLE line doesn't distinguish 'never started due to port conflict' from 'started but never logged the trace line', both surface identically as UNREACHABLE.",
|
||||
"tags": [
|
||||
"xce-test-resources",
|
||||
"apiserver",
|
||||
"port-conflict",
|
||||
"orphaned-process",
|
||||
"sandbox-hygiene",
|
||||
"xce-test-rc-file",
|
||||
"task-3.3"
|
||||
],
|
||||
"related_bugs": [],
|
||||
"occurrences": 1,
|
||||
"last_seen": "2026-08-26T17:17:00Z"
|
||||
},
|
||||
{
|
||||
"id": "bug-1721",
|
||||
"timestamp": "2026-08-26T00:00:00Z",
|
||||
"error_message": "clean_failover_preservation.sh FAIL: cluster.bin missing under <repo>/ha/data (deploy prerequisite)",
|
||||
"file": "src/controller/testsuite/clean_failover_preservation.sh",
|
||||
"root_cause": "The script hardcodes its DATADIR precondition default to $REPO_ROOT/ha/data (a pre-generated cluster.bin + TLS cert template directory it expects to already exist), but ha/data/ was deliberately deleted from the repo by an unrelated commit (ee8473bd, admin-config-tool: drop ha/data/ entirely, match real client TLS). The script was never updated afterward, so its own documented precondition check now fails unconditionally in any fresh checkout/worktree that has not manually recreated that directory out-of-band. Discovered while adding --norc to the script (xce-test-rc-file spec follow-up) and independently verifying the edit did not break anything -- confirmed via git history that the failure reproduces identically on the pre-edit script too, so it is unrelated to that change.",
|
||||
"fix": "Not fixed -- out of scope for the xce-test-rc-file spec. Needs either: (a) DATADIR default updated to point at a still-valid TLS/config template location consistent with admin-config-tool's TLS changes, or (b) the script updated to self-generate its own template via deploy-ha.sh/xce-deploy-ha before the precondition check, matching how run-ha-soak.sh and run-resource-verify.sh already self-deploy their own scratch clusters instead of depending on a fixed pre-existing directory.",
|
||||
"tags": [
|
||||
"test-harness",
|
||||
"stale-fixture",
|
||||
"clean_failover_preservation.sh",
|
||||
"ha-term-safety",
|
||||
"pre-existing",
|
||||
"admin-config-tool-regression"
|
||||
],
|
||||
"related_bugs": [],
|
||||
"occurrences": 1,
|
||||
"last_seen": "2026-08-26T00:00:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
- Keep test logs for analysis, don't skip intermediate output even if a test hangs.
|
||||
|
||||
## Key Learnings
|
||||
- **`fopen(path, "r")` SUCCEEDS on a directory path on this platform (glibc/Linux) — it does NOT return NULL/set errno the way "could not open the file" naively suggests.** (2026-08-26, xce-test-rc-file task 1.2, bug-1716.) The open failure only surfaces on the FIRST READ: `getline()`/`fread()` returns -1/0 with `errno==EISDIR` and `ferror(f)` set (`feof(f)` stays 0, so don't gate on that). Confirmed via a standalone repro (`fopen("/tmp","r")` then `fread`). Any function whose error-handling contract says "print a warning if the file can't be opened for reading" needs to check BOTH the `fopen()`-returns-NULL case AND a post-read `ferror(f)` check — checking only `if (!f)` right after `fopen()` silently misses the directory case (rc stays 0, warning never printed, but also no crash/runner-call — a quiet contract violation, not a loud failure, so it only surfaces via a test that actually asserts the warning text was printed, not just the return value).
|
||||
- **xcecli Cobra rootCmd location**: `src/frontend/xcecli/cmd/main.go` imports `cmd "xcecli/router"` (the import alias is `cmd`, but the actual package lives at `xcecli/router/router.go`, package-named `cmd`). Cobra's `rootCmd` is declared there as an **unexported** package-level var (`var rootCmd = &cobra.Command{...}`, router.go:21). ~~The only exported symbol is `Execute(cm *config.ConfigManager) error`, which takes no version param... boundary conflict~~ — **SUPERSEDED, resolved by build-time-versioning task 4.5 (commit fb09afe) and confirmed still true as of dirty-content-fingerprint task 4.5 (2026-08-21)**: `Execute` was extended to `Execute(cm *config.ConfigManager, version string) error`, which itself sets `rootCmd.Version = version` (router.go:29-34) before `rootCmd.Execute()`. main.go now composes the full version string (`GitCommit`/`BuildTime`, plus task 4.5's `composeVersionDisplay(GitCommit, DirtyFingerprint)` strip-and-append) and passes it into `cmd.Execute(configManager, version)` — no further router.go changes needed for version-string composition; only main.go is the boundary now.
|
||||
- **Build entry point**: `flake.nix` lives in `src/`, not the repo root. Always `nix develop /abs/path/to/src --command bash -c '...'` with an explicit path — Bash cwd isn't stable across calls, and a bare `cd src && nix develop` can silently short-circuit.
|
||||
- **Test coverage gaps**: documented full-suite commands (`xce-build.sh <component>`, `make test`) each cover only a subset — e.g. a component's default meson run misses newly-added standalone test binaries unless wired into its `RUN_TESTS` case, and the CGO/apiserver backend suite needs `make test-backend` with CGO_CFLAGS/LDFLAGS reconstructed via pkg-config (package is `c-capnproto`, not `capnp_c`). A green default build is not proof of full coverage — verify every new test binary actually runs.
|
||||
|
|
@ -131,9 +132,27 @@
|
|||
- **(2026-08-25, bug-1695) `meson.build`'s `controller_unit_test_src` deliberately does NOT link `myapp.c` (or `main.c`) — only `startup_validation.c`, `ha.c`, `cluster.c`, `persist.c`, etc.** `myapp_run()` is documented as an "untestable integration entry point" (test_ha_startup.c's own Task 5.1 comment) requiring a live network socket, so it's excluded from the unit-test binary entirely. If you need to unit-test logic that currently lives inline inside `myapp_run()`, do NOT just add a non-static function to `myapp.c` and forward-declare it in a test file — it will compile but fail to LINK (`undefined reference`) because `myapp.c` itself never gets compiled into `controller_unit_test`. Instead, extract the logic into `startup_validation.c` (already linked into both `xce_controller`/`main.c` AND `controller_unit_test`, and explicitly documented as existing "for use in main.c and the controller unit test suite") and have `myapp.c` forward-declare + call it — exactly mirroring the pre-existing `validate_startup_config()` pattern (defined in `startup_validation.c`, forward-declared at the top of `myapp.c`, called from `myapp_run()`). Confirmed via a real RED/GREEN round-trip: extracting into `myapp.c` first produced a clean compile + link FAILURE across all of `controller_unit_test`'s ~90 test files; moving the same function body to `startup_validation.c` fixed it with zero other changes.
|
||||
- **(2026-08-25, bug-1695) `ha_state_t` has TWO separate term fields that look similar but are written by different code paths and must not be conflated**: `ha->term` (top-level, `_Atomic uint64_t`, `ha.h:157`) is the adapter's own copy, written by `ha_restore_term()` at boot and by `raft_cb_persist()` on every `on_persist` callback; `ha->raft.term` (nested inside the embedded `raft_state_t`, `ha.h:198`/`raft.h:29`) is the raft LIBRARY's own internal term, seeded ONCE at `ha_init_raft()` time from `ha->term` (`rcfg.initial_term = atomic_load(&ha->term)`) and thereafter updated only by the library itself (`raft_start_election`/`raft_on_vote_request`/`raft_on_append_entries_request`, all via the SAME `on_persist` callback that also updates `ha->term`) — so they stay in sync after boot, but the ONLY correct place to read "the term as of right after this boot's restore" is `ha->raft.term` captured immediately after `ha_init_raft()` returns (not `ha->term`, and not before `ha_init_raft()` runs). myapp_run's bootstrap check reads `ha->raft.term` specifically (not `ha->term`) — a fix that snapshotted the wrong field would silently do nothing.
|
||||
- **(2026-08-25, bug-1695) A `--chaotic` HA soak's per-round "wait_for_new_primary window" log line is a WEAK signal on its own — always cross-check the raw controller logs (`grep -n "CTRL_PRIMARY\|CTRL_STANDBY\|PreVote round" logs/ctrl-*.log`) before trusting it.** The chaotic driver's own per-round polling window (~90s, 30×3s probes) can time out ("NO new primary within window") even when a genuinely NEW node DID win a real election shortly after — the log line only reflects whether the driver's OWN bounded poll happened to catch it, not whether the underlying HA mechanism worked. In this session's soak, the fixed run showed "NO new primary within window" on 5/6 rounds (superficially similar to the unfixed baseline's 6/6) while the RAW logs proved something structurally different was happening: term genuinely progressing 1→2→3→4→7 and a different node (`ctrl-2`) achieving a real, log-verified `CTRL_PRIMARY` transition via a won PreVote — vs. the baseline where term was frozen at a single value for the entire run and NEITHER peer ever showed any PreVote/election activity at all, anywhere. **The A/B technique that resolved this ambiguity**: `git stash push -- <the exact files you changed>` (reverts just those files, keeps everything else including the same freshly-built binaries' surrounding tree state), rebuild, re-run the IDENTICAL soak invocation against a fresh scratch dir, compare raw logs side by side, then `git stash pop` to restore the fix. Far more conclusive than trusting either run's summary line alone.
|
||||
- **(2026-08-25, bug-1695) A chaotic-soak `expect-job`/`verify_classify` "COMMITTED JOB LOSS" verdict is NOT self-verifying — cross-check the raw WAL with `xce_test`'s `waldump <data-dir>` REPL command (built into `xce_test`, not a separate binary) before treating it as a real controller data-loss bug.** `verify_classify` reads through the apply-pump/materialized-jobmgr layer (an RPC-level `expect-job` query), which per already-documented bug-1487 can legitimately lag behind the raw committed WAL for several seconds after a genuinely-new election (the new leader's OWN-term commit-barrier gate, `ha_arm_own_term_gate`, requires a fresh majority ack for its own-term no-op before folding in prior-term entries) — a RETRY-EXHAUSTED "not found" there does not by itself prove the entry is actually missing from the durable log. `walcompare`'s `committed_divergence` count (also built into `xce_test`) is the AUTHORITATIVE data-safety signal — it stayed 0 in this session's soak even while `verify_classify` counted 6 losses, and a direct `waldump` of all 3 nodes' `data/wal/seg-*.wal` files confirmed the ONE specific jobid in question genuinely does not appear on ANY node's WAL at all (not divergent between nodes — universally absent), tracing it to the cluster's very first cold-boot config-bootstrap window rather than anything this specific fix touched.
|
||||
- **(2026-08-25, bug-1695) A chaotic-soak `expect-job`/`verify_classify` "COMMITTED JOB LOSS" verdict is NOT self-verifying — cross-check the raw WAL with `xce_test`'s `waldump <data-dir>` REPL command (built into `xce_test`, not a separate binary) before treating it as a real controller data-loss bug.** `verify_classify` reads through the apply-pump/materialized-jobmgr layer (an RPC-level `expect-job` query), which per already-documented bug-1487 can legitimately lag behind the raw committed WAL for several seconds after a genuinely-new election (the new leader's OWN-term commit-barrier gate, `ha_arm_own_term_gate`, requires a fresh majority ack for its own-term no-op before folding in prior-term entries) — a RETRY-EXHAUSTED "not found" there does not by itself prove the entry is actually missing from the durable log. `walcompare`'s `committed_divergence` count (also built into `xce_test`) is the AUTHORITATIVE data-safety signal — it stayed 0 in this session's soak even while `verify_classify` counted 6 losses, and a direct `waldump` of all 3 nodes' `data/wal/seg-*.wal` files confirmed the ONE specific jobid in question genuinely does not appear on ANY node's WAL at all (not divergent between nodes — universally absent), tracing it to the cluster's very first cold-boot config-bootstrap window rather than anything this specific fix touched. **CRITICAL CAVEAT discovered later the same day (bug-1696): do not stop at "walcompare says 0 divergence" for a soak run driven by the bug-1695 fix specifically — that fix newly enables a restart-then-immediately-self-promote path that hits a SEPARATE, far more severe bug (WAL truncated to empty on promotion, see bug-1696 below). A truly clean soak needs BOTH committed_divergence=0 AND a raw `waldump` entry count that matches the real number of jobs submitted, not just "the specific jobid I went looking for is present somewhere."**
|
||||
- **(2026-08-25, bug-1696, CRITICAL — Leader Completeness / bug-1060-guard defeated by boot ordering) `raft_cb_become_leader()` (ha.c:1615) computes the promotion WAL-truncation floor and evaluates the ENTIRE promotion-eligibility gate using `persist_durable_floor()`/`persist_recovery_truncated()`/`persist_apply_verdict()` BEFORE `cluster_init_phase2()` — the only call that invokes `persist_recover()` for this process's first-ever boot — has run.** `is_first_boot` handling (`cluster_init_phase2()` call) sits at ha.c:~1873-1899, AFTER the safe_committed_lsn computation and `persist_truncate_wal_above()` call (~1637-1662) and AFTER the full eligibility gate (~1699-1823) in the SAME function. Before recovery, a freshly-`persist_init()`'d `persist_t` reports `durable_lsn=0` — not the real on-disk tip. So on a node that RESTARTS with real committed data on disk and then wins its OWN promotion on that same boot (a path bug-1695's fix newly makes reachable — previously a restarted node's stale term made it always defer to a peer instead), `ha_promotion_truncate_floor(safe_lsn≈0, durable_tip≈0)` computes ≈0 and `persist_truncate_wal_above(p, 0)` physically destroys the real on-disk WAL before it's ever been read. bug-1445's comment (ha.c:1901) already flagged this EXACT "gate evaluated blind before recovery" hazard for one flag (`apply_resource_fenced`) with a narrow re-check-after-phase2 patch — but never for the far more consequential `durable_floor`/truncation computation, which runs even earlier and physically deletes data rather than just making a stale eligibility decision. **General lesson: any promotion/election-safety code that reads `persist_durable_floor()`/`persist_recovery_truncated()`/`persist_apply_verdict()` must be checked against whether `cluster_init_phase2()` (this process's `persist_recover()`) has actually run yet THIS boot — `is_first_boot` is the tell; a function that both checks `is_first_boot` and reads recovery-derived persist state earlier in its own body than the `is_first_boot` phase2 call is almost certainly reading pre-recovery garbage.** Root-caused via source trace (not guesswork) after direct `waldump` evidence (entire on-disk WAL = 1 NOOP entry despite 21+ committed jobs, no snapshot dir, zero checkpoint log lines) plus the smoking-gun log line `ha_compute_safe_committed_lsn: safe_committed_lsn=0 ... (from 2 granted voters)` immediately preceding `raft_cb_become_leader: → CTRL_PRIMARY` on ctrl-1's SECOND boot. Fix pending — see bug-1696 in buglog.json for the exact proposed reorder and required A/B soak proof.
|
||||
|
||||
- **(2026-08-26, bug-1688) When a zarray's `.count` field does something numerically impossible (doubles across a repeat push carrying the exact same keys, or drifts above `.caps`), do NOT jump straight to "something external corrupted the array between the two calls" or "the same key must be resolving to two different indices" — first check whether the CURRENT call's own writes are storing NULL.** `zarray_put_ex()` (zarray.c:267-303) only increments `.count` when the slot being overwritten currently reads NULL; it has NO NOTION of "is the new value itself NULL" — writing NULL into an already-occupied slot leaves `.count` unchanged while silently zeroing that slot's data, and a LATER write to that now-NULL slot (even with a perfectly stable index) then legitimately increments `.count`, looking exactly like "the array got corrupted between calls" if you only watch `.count`/the slot's occupancy and not the actual VALUE being written. Root-caused here only after adding a `zarray_put_ex()`-level watch-trace (a temp global pointer comparison, since the bug lived in library code called from many sites) that printed `data[i]` itself, immediately revealing `new_data=(nil)` on every write of the "corrupting" push — the two more exotic hypotheses (idx-resolution bug; core_reset_workers()/session-boundary code clearing the array) were both live-traced and definitively ruled out first, costing significant time that a "trace the actual value being written, not just count/occupancy" instinct earlier would have saved. Separately: `decode_Value_ptr()` (controller.capnp.c:2966) legitimately decodes to NULL whenever the wire's capnp pointer for that field is `CAPN_NULL` — this is not a decode bug, and any C struct field populated via `decode_X_ptr()` from a capnp-generated decoder should be treated as "can legitimately be NULL even when the enclosing struct/list entry itself is non-NULL," with call sites guarding accordingly (the same discipline this file's existing `nv == NULL`/`nv->name == NULL` guards already applied to the OUTER struct, just not to this one nested pointer field).
|
||||
|
||||
- **(2026-08-26) `controller_unit_test`'s meson `test()` registration sets `env: ['ASAN_OPTIONS=detect_leaks=0']` (src/controller/meson.build:239) — running the built binary DIRECTLY (bypassing `meson test`) without setting that same env var yourself makes ASAN's default leak detection fire at process exit, reporting ~7400 pre-existing allocations (~954KB) across dozens of unrelated test files as "leaked" and returning a nonzero exit code, even when the actual Unity test count (`N Tests M Failures`) is 100% clean.** This is NOT a regression signal — it's the project's own accepted, documented convention (also referenced in `xce-build.sh`'s own "skipped tests" hint: `ASAN_OPTIONS=detect_leaks=0 ./$BUILD_DIR/controller/controller_unit_test`). Always grep the target's own `meson.build` `test()` block for its `env:` before treating a direct-binary-run's exit code or ASAN output as ground truth; the Unity summary line (`"<N> Tests <M> Failures <K> Ignored"`) is the real pass/fail signal, not the process exit code alone.
|
||||
- **(2026-08-26) A Unity `TEST_ASSERT_*` failure `longjmp`s out of the CURRENT test function immediately — skipping any teardown call written AFTER the assertion in that same function body.** If that teardown would have stopped a live background thread (e.g. `persist_stop()`/`persist_fini()` tearing down a real `fsync_worker` pthread), the thread leaks alive holding a pointer into that test's now-returned (and ASAN-poisoned) stack frame — the NEXT test in the same binary that happens to touch overlapping stack memory can then crash with a `stack-use-after-return` ASAN report that looks totally unrelated to the real, intentionally-failing assertion (confirmed while proving bug-1697's RED phase: a deliberately-reverted assertion failure in `test_wal_job_start_fact.c` cascaded into an unrelated ASAN abort deep inside `test_dispatch_commit.c`, later in the same run). This is a known, already-documented pathology in this codebase (see `test_upgrade_snapshot_apply.c`'s own header comment: "gather every assertion-relevant observable into a plain local variable FIRST, call teardown UNCONDITIONALLY, and only THEN assert") but most test files in this suite do NOT follow that discipline (assert-then-teardown is still the norm). When a RED-phase test run produces both the EXPECTED assertion failure(s) AND a seemingly-unrelated ASAN crash later in the same run, check whether the crash is this cascade (a live thread from an earlier failed-and-un-torn-down test) before treating it as a new bug — capture the actual observable into a variable, run teardown, THEN assert, if you need a RED-phase test to survive to completion without corrupting siblings.
|
||||
- **(2026-08-26) When a live HA soak or other long-running deployment is using `src/build-asan` (or any shared default build dir) from a concurrent session, do NOT run `/xce-build` (or any `xce-build.sh`/`meson compile`) against that same directory — even for a "just checking" build — since a concurrent `ninja`/`meson setup` can race the other session's own build, and relinking a binary a live soak's processes still hold open risks corrupting or confusing that process's on-disk image.** Always pass an explicit, unique custom `build_dir` (`xce-build.sh <component> <profile> <custom-dir>`) for any build done "on the side" of another session's live deployment — check `ps aux | grep xce_controller` (or the relevant daemon name) FIRST to see which build dir(s) are actually live before choosing where to build. Also verify a dispatched build `Agent` subagent's build_dir argument actually took effect (compare the binary's own mtime against your most recent source edit's mtime) rather than trusting its self-report — one such subagent in this session silently ended up building into the script's own default dir (`xce.build`, from a malformed/dropped custom-dir argument) and initially reported stale/incorrect RED results from a build that predated the real edits; independently re-running the build directly (not via a sub-dispatch) and cross-checking mtimes caught it.
|
||||
- **(2026-08-26) `deploy-ha.sh <binary> <top> <peers>`'s `peers` argument is a plain comma-separated `host:port` list — do NOT prefix each entry with its own `name=` (e.g. `ctrl-1=localhost:19991`); the script adds that prefix itself internally.** Passing an already-prefixed peers string produces a doubled `ctrl-1=ctrl-1=localhost:19991` in every generated start script, which silently breaks worker/scheduler cluster-join (`Failed to join cluster, retrying in 5 seconds` in the worker log) while the controller itself still starts and elects fine (its own `--peers` parsing tolerates/ignores the malformed self-reference for a single-node cluster) — the failure only surfaces once you try to verify a worker/scheduler joined, not at controller-startup health-check time. Confirmed the fix by re-deploying with a bare `host:port` peers string.
|
||||
- **(2026-08-26) `xce_test`'s REPL command set is SPLIT by connection mode — `jobsub`/`joblist`/`jobinfo`/`wait-primary`/etc. live under `client` mode's command table, while `jstart`/`jfinish`/`jfail`/`resupdate` live under a SEPARATE `worker` mode's command table (test.c's `client_commands[]` vs `worker_commands[]`).** Typing `jstart` while still in `client` mode fails with a generic `"jstart: No such command for xce test."`, not an argument or connection error — easy to misdiagnose as a jobid-format problem. To send a synthetic worker-side fact (e.g. to test a WAL_JOB_START collision scenario against a job a REAL worker is also concurrently racing to finish), you need a SEPARATE `xce_test` invocation that switches into `worker` mode (its own `join`) first — you cannot mix `jobsub` and `jstart` in one client-mode session. The `$jobid`/`$jobcount` REPL auto-variables (populated by the most recent `jobsub`/`joblist`) are real and usable across subsequent lines in the SAME script (test.c:166-179), useful for exactly this kind of immediate-follow-up scripting once you're in the right mode.
|
||||
|
||||
- **(2026-08-26, bug-1700/bug-1701) `ha_state_t.primary_host`/`primary_port` (ha.h:161-162) are NEVER WRITTEN by any production code path — they are only ever set by test fixtures manually poking the struct.** `fill_redirect_reply()` (join.c:27) and anything that calls it (OPCode_join's handler at join.c:277, and bug-1700's new myreq_handler gate-rejection call) is therefore a NO-OP in a real deployment: the redirect it sends is always an empty host / zero port, which the xceadmin client correctly (per its own documented contract) treats as "no redirect." This was independently confirmed by grepping every `.c`/`.h` in `src/` for an assignment site (none outside tests) and is already explicitly named in `ctrlstatus.c`'s own doc comment ("the vestigial, never-written ha->primary_host") and in the already-closed bug-854's root-cause writeup — but nobody had connected the dots that this ALSO silently breaks `fill_redirect_reply()`'s OTHER caller, `join.c:277`, not just whatever new caller you're adding. **Before wiring any new caller of `fill_redirect_reply()`/relying on `ha->primary_host` for anything live, verify with a real 3-node deploy (not just a unit test that manually sets the field) that the value is actually populated on a standby** — a unit test proves the helper copies the field correctly, not that the field ever holds anything real in production.
|
||||
- **(2026-08-26) When a task's own "critical correctness check" section asks you to verify a field's value on a standby "that has NOT yet learned of any live primary (e.g., very early boot, mid-election...)", read that as a hint to check the PERMANENT-vs-transient distinction, not just the transient one it names.** bug-1700's task prompt explicitly asked this question but framed it as a transient edge case; the actual answer was "permanently empty in every state, forever" — a much bigger finding than "empty during a brief startup window." Don't let the framing of a verification question limit how far you check.
|
||||
- **(2026-08-26, bug-1701 FIX) `ha_state_t.primary_host`/`primary_port` (bug-1700/bug-1701's vestigial-field finding above) is now actually populated in production, closing the gap.** Fix is a pure LOCAL lookup in `handle_ctrlhello()` (config_repl.c:2170-2246, inside the existing `primary_ctrl_fd == -1` idempotency guard) — no new wire field needed, because every node already statically knows every candidate's `host:port` from boot (`c->myconf->peers_cfg`, parsed once from `--peers`). Match `req->from->name` (populated for EVERY incoming request at `request.c:379`, `req->from = ep->peer;`) against `peers_cfg.candidates[]` by name and copy the matched candidate's `hostname`/`port`. **General pattern worth remembering**: when a per-connection identity field (`req->from->name`) needs to resolve to a richer static record, check whether that record is ALREADY fully known locally (a config parsed at boot) before assuming you need a new heartbeat/wire-protocol field to carry it — `ha_init_raft`'s existing `voted_for_name`-to-index lookup (ha.c:7822-7829) was the exact precedent pattern to copy, found by grepping for other `peers_cfg.candidates[i].name` comparisons in the same file family. Also: `ha.h`'s field-level doc comments (e.g. `pthread_mutex_t lock; /* Protects primary_host/port + voted_for_term/name */`) are authoritative for which lock protects a field — trust that over inferring locking convention purely from a nearby unrelated field's (`primary_ctrl_fd`'s) actual call-site behavior, which can itself be under-protected without it being the intended convention for the field you're adding.
|
||||
- **(2026-08-26) `xce-build.sh <component> <profile> <build_dir>`'s `build_dir` argument is resolved AFTER the script's own `cd "$SRC_DIR"` (`cd "$(dirname "$0")/.."`, i.e. `src/`) — passing an already-`src/`-relative path like `src/xce.build` from the repo root produces a bogus DOUBLED path (`src/src/xce.build`) that silently creates a whole second build tree instead of erroring.** Caught immediately (binary just wasn't at the expected path) but wasted a build cycle. When explicitly passing `build_dir` to `xce-build.sh`, give it relative to `src/` (e.g. bare `xce.build`), or omit it and let the profile default (`asan`→`xce.build`, `debug`→`build.dbg`, `release`→`build.release`) resolve it.
|
||||
- **(2026-08-26) To produce a clean "before/after" pair of binaries for live E2E verification when OTHER unrelated files have uncommitted in-flight changes you must not touch or rebuild against**, use `git stash push -- <only the files for this fix>` (explicit pathspec, NOT a bare `git stash`) to isolate exactly the fix's own diff — rebuild for the "before" binary, `git stash pop` to restore the fix, rebuild again for the "after" binary. This leaves every other uncommitted file (the unrelated in-flight `ha.c`/`job.c`/`persist.c` work in this session) completely undisturbed throughout, unlike a bare `git stash` which would have stashed everything. For a PYTHON-only fix specifically, this same before/after swap works without any rebuild at all — just `cp` the two scheduler.py variants in and out of `src/xceadmin/src/xceadmin/` and re-run the SAME editable-install venv's `xceadmin` entry point; a `pip install -e` venv reads live from the source tree, no reinstall needed between swaps.
|
||||
- **(2026-08-26, bug-1713) In this sandbox's local-loopback 3-node deploy, the FIRST-BOOT election-settling window varies wildly run to run (sometimes near-instant, sometimes ~10-12s via a term-1 election-timeout-then-PreVote-failover fallback path) — a fixed "wait for start-all.sh's health-check, then fire N requests" tight loop is NOT a reliable way to catch a transient `E_XCE_NOT_READY`/`E_XCE_NOMASTER` window: by the time start-all.sh's own 1s-granularity health-check first observes `CTRL_PRIMARY` in a log and returns, `ha_has_write_quorum()`'s `standby_endpoints` (populated synchronously inside `raft_cb_become_leader` from the raft library's own connected-voter set) is typically ALREADY satisfied, so the gate is already open. A REAL FAILOVER (kill -9 the current primary, then hammer the survivors with NO sleep, no health-check wait) reproduced the exact live symptom reliably instead — got a genuine bare `-10013` at ~t+8.6s post-kill on an unfixed client, and confirmed zero such failures surfaced across the equivalent window on the fixed client. The bug's own root-cause writeup already named this as an equally-valid trigger ("...or any time a real failover is in flight") — prefer the failover trigger over racing first-boot timing when a live repro is needed and cold-boot timing proves flaky.
|
||||
- **(2026-08-26) A hand-rolled multi-connection mock TCP/TLS server in a test file needs a genuinely FINITE accept loop (`for handler in handlers:`), not `while self._handlers:` guarding a list that's never mutated — the latter is an always-true condition that leaves the server thread blocked in `accept()` indefinitely once the test's real client stops connecting, which does not fail the test but silently adds `thread.join(timeout=N)`'s FULL timeout to every such test's teardown (measured: 3 tests each added a flat 5s of pure teardown wait with zero effect on pass/fail).** Match the mock's handler-list length exactly to the number of connections the test actually expects (assert on `connection_count` for the "stops on its own budget" property instead of over-provisioning handlers "just in case") and additionally set a short `socket.settimeout()` on the listener as a safety net so a hypothetical future regression that causes MORE connections than expected still fails within one timeout window instead of silently reintroducing the same multi-second stall.
|
||||
|
||||
## Do-Not-Repeat
|
||||
- **A TDD-implementer SUBAGENT (spawned by a kiro-impl coordinator) has NO external wakeup mechanism — arming a `Monitor` tool call or a `Bash run_in_background` call and then ending the turn to "wait for a notification" just stops the subagent forever; the coordinator has to manually resume it.** (2026-08-26, xce-test-rc-file task 1.2, recurred across multiple tasks in this same spec before being called out explicitly.) Only the top-level session genuinely receives async task-notification events between turns; a subagent's turn ending IS the end of its execution for that dispatch. When a validation command (e.g. `controller_unit_test`) risks exceeding the default 120-300s Bash timeout, pass an explicit large `timeout` (e.g. 300000-600000ms) to a PLAIN FOREGROUND `Bash` call and let the tool call itself block until real output returns — never `run_in_background`, never `Monitor`, never end a turn on an unfinished command in this role. If a command is auto-moved to background because the timeout already elapsed, immediately re-run it in the foreground with a longer timeout rather than trying to "wait" for the backgrounded one.
|
||||
- **When translating a `fp_ok`-tracked, per-step-gated failure-detection pattern (like `xce-build.sh`'s dirty-fingerprint computation) into a Make `$(shell sh -c '...')` one-liner, do NOT collapse two commands into `{ cmd1; cmd2; } >"$$fp_tmp"` and gate the whole group with one `if`** — a POSIX sequential group's exit status reflects only its LAST command, so `cmd1`'s failure is silently discarded whenever `cmd2` still succeeds (2026-08-21, bug-1612: `git diff HEAD` failing was swallowed because the trailing `git ls-files | sort | xargs cat` pipe still exited 0, producing a fabricated fingerprint instead of degrading to empty). Give each step its own explicit `if ! step; then fp_ok=0; fi` (or `&&`-chain), exactly mirroring the reference script's discipline, even inside a cramped one-liner. Caught by adversarial code review with a targeted failure-injection test (wrapper `git` on `PATH` that fails only the targeted subcommand) — that test pattern is the reusable way to verify this class of fix.
|
||||
- Don't run `clang-format -i` on whole existing files here — the repo isn't clang-format-clean at baseline, so a full reformat buries the real diff; recovering via `git checkout HEAD -- <file>` on a file with other uncommitted work destroys that work irrecoverably.
|
||||
- **(2026-08-22, scheduler-sim-decision-parity task 2.3) Actually ran `clang-format -i` on two files mid-task despite this rule already being logged above — caught it via `git diff` before finishing, not before running it.** If this happens again: do NOT `git checkout` the file (destroys real work). Instead: `git diff -- <file>` and read every hunk; any hunk whose ONLY change is whitespace/line-wrapping on code you did NOT intentionally write/edit this turn is collateral reformatting — manually `Edit` just that hunk back to its pre-format text (find each such hunk, revert by hand). Verify success with `git diff -- <file> | grep -c '^-[^-]'` on regions you didn't touch dropping to 0, then rebuild+retest to confirm nothing broke. Cheap enough to be a non-event once caught, but the real fix is: check this Do-Not-Repeat list (or just never run `clang-format -i` without `--dry-run`/`-output-replacements-xml` first to preview scope) BEFORE running it, not after.
|
||||
|
|
@ -153,6 +172,7 @@
|
|||
- **A REPL/replay test fixture that reuses the SAME jobid for both a "real" event (job push) and a "cheap padding" event (job state change, added purely to exercise a second event type) accidentally mutates the real job's state, not just decorates the log.** bug-1595 test regression (task 12.5, 2026-08-19): `test_step_echoes_events_before_session_decisions` pushed job 6161 then sent a JOBSTATECHANGE event for the SAME jobid 6161 (PEND->RUN) purely to get a second event type echoed — this genuinely transitioned the real job to RUNNING before the session ran, so the picker's `next_job()` never offered it and the session's own PEND decision (which the test also asserted on) was never produced. The sibling test `test_event_breakpoint_pauses_repl_at_matching_event` already documents the correct pattern in its own comment (`append_jobstatechange_event() establishes: core_events.c WARN+skips an unknown jobid rather than fail, so ... below needs no real job to exist`) — it deliberately used unrelated jobids (111/222) precisely to keep the padding event inert. When adding a "just need any event of type X to be logged" filler event to a fixture that also has a real job in flight, always target an unrelated/nonexistent jobid unless the state transition itself is the thing under test.
|
||||
- **`[ -n "${arr[key]+x}" ]` (associative-array key-EXISTENCE) is NOT a safe proxy for "the value is meaningful/reachable" when the producer unconditionally sets that key even on failure.** bug-1582 (resource-value-verification task 7.1, round-1 REJECTED, round-2 fixed): `main()`'s record-building loop derived `sched_ok`/`api_ok` from whether `SCHEDULER_OBSERVED[$vname]`/`APISERVER_OBSERVED[$vname]` existed — but `query_scheduler_resvals()`/`query_apiserver_resvals()` (already-approved tasks 6.3/6.4) always set that key, setting it to `""` on a genuine UNREACHABLE/timeout rather than leaving it unset. Key-existence was therefore always true, silently masking every real UNREACHABLE as a MISMATCH (comparing the injected value against an empty string). Fix: derive the "is this layer reachable" signal from the layer's own pre-classified status string (`SCHEDULER_CHECK_RESULTS[$vname]`/`APISERVER_CHECK_RESULTS[$vname]`, which DOES distinguish `UNREACHABLE:*` from `MATCH`/`MISMATCH:*`) instead of from a sibling data array's key presence. General lesson: before using `${arr[k]+x}`/`-v arr[k]` as a "was this checked / did this succeed" signal, verify the producer actually leaves the key UNSET on failure — if it sets it to an empty/placeholder value instead (a common pattern for "always populate every requested key so downstream code doesn't need existence-checks"), key-existence collapses to "was this key in the requested set" a wholly different, weaker signal than "did this succeed". Also: a synthetic test that only calls the pure aggregation function directly with hand-built records (task 8.2's original approach) cannot catch a bug living in the data-marshaling code that BUILDS those records — this class of bug needs the marshaling code itself extracted into a directly-callable function (`build_verdict_records_from_tsv()`) and exercised by its own test.
|
||||
|
||||
- **(2026-08-26, xce-test-rc-file task 1.1, bug-924) When `controller_unit_test` invoked DIRECTLY (not via `meson test`) dies instantly (well under 1s) with a plain exit code 1 and NO crash/ASAN signature — the raw log just stops mid-file, often right after some earlier, unrelated `test_*.c` group's last line — check `df -h /tmp` before suspecting a real regression.** This sandbox's `/tmp` accumulates `mkdtemp()`-created `xce_*` fixture directories across EVERY session indefinitely (no test file in `testsuite/` cleans up its own `/tmp/xce_*` dirs); once the filesystem backing `/tmp` hits 100% used, `mkdtemp()` fails with ENOSPC inside whichever test file's fixture setup runs next, and several such files (e.g. `test_repl_apply.c`'s `make_test_persist_for_apply()`/`make_test_persist_in()`) hard-`exit(1)` on that failure rather than reporting it through Unity — silently killing the ENTIRE combined-binary test process before any later `test_*` group (including one you just added) gets a chance to run, with no signal distinguishing it from your own new code being broken. Confirmed live at 14731 leftover dirs / 653.6G / 100% used, 0 available — `find /tmp -maxdepth 1 -name 'xce_*' -exec rm -rf {} +` reclaimed 630G (100%→29%) and the exact same binary then ran to completion (1208 tests) cleanly. Also check for and kill any orphaned `controller_unit_test`/`xce_controller` process left over from an earlier invocation in the SAME session (`ps aux | grep controller_unit_test`) before re-running — one was found stuck in `futex_do_wait`, presumably retry-looping a WAL fsync against the full disk, and could confuse a fresh run's process/port state. See buglog.json bug-924 (now updated with this occurrence) for the original, smaller-scale sighting.
|
||||
- **This coding-agent harness's interactive shell aliases plain `grep` to a PCRE-capable (ugrep-backed) wrapper via a `.bashrc`-sourced shell FUNCTION — but that function is interactive-shell-only and is NOT inherited by a script invoked as `bash script.sh`, which falls back to whatever the real `grep` binary on PATH is (in this environment: BusyBox, no `-P` support at all).** (2026-08-20, build-time-versioning task 6.2): `echo x | grep -oP '(?<=x)y'` succeeded fine when typed directly as a Bash-tool command, but the identical `grep -oP` call inside a standalone `.sh` file, run via `bash thefile.sh`, failed with `grep: invalid option -- 'P'` (BusyBox usage dump). Confirmed via `type grep` in an interactive call (shows a `_cc_a`-prefixed shell function shelling out to the Claude Code binary itself acting as ugrep) vs. `which -a grep`/`/sbin/grep --version` showing BusyBox as the real underlying binary. **Any shell script file meant to be run standalone (not just typed inline as a Bash-tool command) must assume POSIX-only grep/sed (BRE, `-E`, `-F`, `-o`) — no `-P`/PCRE, no lookbehind/`\K`.** Use `sed -n 's/.../\1/p'` (POSIX BRE, works under BusyBox) instead of `grep -oP '...\K...'` for field extraction inside any committed script.
|
||||
- **A direct `go build` (even a real, working one) without `-mod=readonly` can silently rewrite a COMMITTED `go.sum`, adding unrelated indirect-dependency checksum lines as a side effect of resolving the full module graph — even though `go env GOFLAGS` was empty and go.mod itself was never touched.** (2026-08-20, build-time-versioning task 6.2): building `apiserver/cmd/main.go` directly (bypassing the network-hung `go_build` wrapper, see below) added ~65 new `/go.mod`-hash-only lines to `src/frontend/apiserver/go.sum` on the first attempt — caught only via a post-hoc `git status --porcelain` sweep, not by the build itself (exit 0, no warning). Go 1.16+'s `-mod=readonly` default blocks go.mod EDITS but does NOT by itself block go.sum EXTENSION for modules already implied by go.mod's requirements. Fix: pass `-mod=readonly` EXPLICITLY on every ad-hoc `go build` invocation that isn't supposed to touch repo state — confirmed live that the same build still succeeds under explicit `-mod=readonly` with zero go.sum diff, so nothing was genuinely missing; the extra entries were pure side effect. **Any script/agent that runs `go build` directly against a checked-out repo (not via the project's own wrapped build path) must always pass `-mod=readonly` and verify `git status --porcelain` on go.sum/go.mod afterward, every time, no exceptions.**
|
||||
- **`src/tools/go_build`'s hardcoded `GOPROXY=https://goproxy.cn` + a fresh-per-invocation `GOCACHE`/`GOPATH` under meson's own `$TMPDIR` sandbox can hang indefinitely in this environment — not fail fast, HANG: an established (not refused/reset) TCP connection to goproxy.cn:443 sits in `futex_do_wait` with near-zero CPU time for 8-10+ minutes across repeated attempts.** (2026-08-20, build-time-versioning task 6.2): `xce-build.sh apiserver <profile> <dir>` (which builds apiserver AND xcecli together via this wrapper) reliably hung on xcecli's fresh module downloads; confirmed via `ss -tnp` showing `ESTAB` to `8.45.176.179:443` (=goproxy.cn, independently reachable fine via plain `curl`/`/dev/tcp` from the SAME shell) with the `go` process's CPU time frozen — a genuinely stuck data path, not slowness, since it never progressed no matter how long left running. Root cause isolated to `go_build`'s own env overrides (`export GOCACHE=$TMPDIR/go-cache; export GOPATH=$TMPDIR/go; export GOPROXY=https://goproxy.cn`), which bypass the ALREADY-POPULATED default `$GOPATH/pkg/mod` cache (`go list -m all` from an ordinary `nix develop` shell, no wrapper, resolved apiserver's full module graph in under a second, zero downloads). **Workaround (do NOT modify go_build/Makefile — this is this specific sandbox's networking quirk, not a real bug in those files): invoke `go build` directly with the default module cache (`GOPROXY=off` is fine since everything needed is already cached) plus the same CGO_CFLAGS/CGO_LDFLAGS pkg-config derivation `test-backend`/apiserver's own meson.build already uses (see bug-1597's documented fix) whenever a real apiserver/xcecli build is needed standalone in this sandbox and `xce-build.sh apiserver`/`make build-server` hang or fail.** Do not blindly increase the timeout and retry — a truly stuck TCP data path does not resolve itself no matter how long you wait; diagnose via `ss -tnp`/`/proc/<pid>/wchan` first.
|
||||
|
|
@ -164,11 +184,17 @@
|
|||
- **Reinforcement of the bug-1615 same-statement `local a=... b="$a..."` gotcha**: recurred independently in a brand-new file (`extract_flag_from_json()`, dirty-content-fingerprint task 6.3, logged as bug-1616) less than a day after being first documented — this pattern is easy to reach for out of habit when writing a "compute one derived value from a parameter" helper. When writing ANY new bash function with `local x="$1" y="...$x..."`, default to splitting the dependent assignment onto its own `local` statement, don't rely on remembering the gotcha in the moment.
|
||||
- **scheduler-sim-decision-parity task 1.1 (2026-08-22)**: relocating an `xce_scheduler_t*`-signature function to `xce_sched_core_t*` (the `sched_resolve_job_picker()` precedent, now repeated for `sched_account_jobs()`/`sched_charge_account()`/`sched_uncharge_account()`/`sched_update_account()`/`sched_update_res_accounts()`/`sched_commit_job_alloc()`) has a hidden fan-out cost: EVERY testsuite file that white-box `#include`s the driver/scheduler source and provides its OWN link-satisfying spy/stub for that function (grep the whole `testsuite/` dir for the function name, not just the file(s) named in the task boundary) must have its stub's signature updated too, or the build fails with a type mismatch/duplicate-declaration error — 6 separate test files needed this for `sched_account_jobs()` alone (test_driver_decision_cb.c, test_resval_ownership.c, test_admin_request.c, test_sim_equivalence.c, test_record_lifecycle.c, test_driver_translate.c), none of them named in the task's own boundary list but all mechanically required by it. This project's codebase convention on the relocated parameter's name is to rename it from `sched` to `core` (confirmed via `core.c`'s own `sched_get_job`/`sched_add_worker`/etc., not just keeping the old name with a retyped pointer) — match that, don't just retype in place.
|
||||
- **Testing "does function X behave identically given a core from container A vs container B" (Req 1.4-style cross-container proof) does NOT require white-box `#include`-ing the heavyweight driver file that defines the thin iterate-and-call wrapper** (here, `scheduler.c`'s ~20-line `sched_account_jobs()`) if the real accounting logic lives one level down in a lighter, already-provenincludable file (`resource.c`, per `test.c`'s own existing `#include "../src/resource.c"`) — no test in this project's `testsuite/` white-box-includes `scheduler.c` at all (confirmed via grep before writing), because its link-time dependency surface (controller-join, capnp publish, HA candidates) is much larger than `resource.c`'s. Test the real lower-level functions directly instead (`sched_charge_account()`/`sched_uncharge_account()`/`sched_commit_job_alloc()` here) and document in the new test's header comment exactly why the thin wrapper itself isn't independently exercised (already covered elsewhere, e.g. by an existing ordering-proof spy test) — this is honest, still exercises 100% real relocated code, and avoids a huge stub-writing burden.
|
||||
- **(2026-08-26) Before treating a fresh HA deploy's anomalous behavior (term jumping impossibly fast, a standby permanently failing to load config, a node never converging with peers) as a real product bug, check for and kill ANY leftover controller/scheduler/worker processes from EARLIER in the SAME session — a same-session soak/investigation left running and forgotten (`ps aux | grep xce_controller`) can bind the exact same default ports (`9991`/`9992`/`9993`, this project's `/xce-test-ha` skill default) a brand-new deploy also uses.** Concretely: an abandoned `xce-ha-chaotic2` 3-node cluster from an earlier bug-1696 investigation, still running hours later, was still bound to 9991-9993 when a fresh `run-ha-soak.sh --sequential` deploy was launched at the same default ports; the new cluster's vote traffic cross-talked with the ancient zombie (which had organically reached a high term over its own long runtime), producing an apparently-impossible term jump (term=1→23 within 50ms of a genuinely fresh first boot) and a standby that could never load cluster_config for the whole test — both fully explained once the zombies were found (`ss -tlnp | grep -E "999[123]"` showing FOUR processes across two unrelated deploys all bound to the same three ports) and killed; a clean re-run on the exact same code, same binaries, same script passed cleanly (`PASS: 2 rounds, 12 jobs, zero job loss; all 3 nodes led`). **Always run `ps aux | grep -i "xce_controller\|xce_scheduler\|xcecd"` (and `ss -tlnp` for the ports you're about to use) before launching a "fresh" HA deploy in this sandbox, especially after any earlier soak/investigation in the same session — a `/xce-test-ha` skill soak that "completed" or whose agent died without explicit cleanup does not guarantee its processes actually exited.** This directly extends the earlier bug-1487-era lesson ("a chaotic-soak verdict is not self-verifying, cross-check the raw WAL") one level further back: an anomalous LIVE symptom from a fresh-looking deploy is not self-verifying either until same-session process/port contamination is ruled out first.
|
||||
- **`xce_sched_core_init()` is safe to call twice in the same test process/function to build two independent, simultaneously-alive `xce_sched_core_t` instances for a side-by-side comparison** — established precedent `test_snapshot_save_load.c` (`core_a`/`core_b`) already relies on this; it does re-init a couple of process-global singletons internally (`xstring_table_init()`) but that's a harmless re-init, not a crash/corruption risk, confirmed by that pre-existing test passing. White-box-including `../src/core.c` + `../src/resource.c` (NOT `core_events.c`/`core_session.c`/`decision.c`/`plugin.c`, unneeded if the test only calls `xce_sched_core_init()`/`sched_add_worker()`/`sched_charge_account()`-family functions directly, bypassing the event-push/session-begin/decision-drain APIs entirely) needs exactly 7 link-satisfying no-op stubs: the `kill_sched_job/queue/worker/user/project` family + `kill_sched_resval` (core.c's kvmgr/zarray datakillers) + `plugin_unregister` (core.c's plugins-kvmgr datakiller) + `plugin_prepare_allocation`/`sched_ctx_get_uncomit_host` (resource.c's `sched_try_alloc_on_host_()`'s own callees, needed only because that whole file is pulled in, even though this test path never calls it) — the last two are the exact same stub pair `test.c`'s own resource.c-only white-box test already establishes needing.
|
||||
- **Never use `date -d` with a RELATIVE expression in this environment (`-7 days`, `+5 days`, `last monday`) — `date` first on PATH is BusyBox v1.36.1, and its failure is silent-by-default rather than loud.** (2026-08-23, `/weekly_report` skill authoring, bug-1628): BusyBox date *does* parse absolute dates (`date -d 2026-06-29 +%A` → `Monday`), which masks the gap during casual probing, but rejects all relative arithmetic with `invalid date` **on stderr** — so `START=$(date -d "-7 days" +%F)` captures only stdout and yields an EMPTY string. The empty value then flows into `git log --since="" --until=""`, which git treats as *no time filter at all* (unbounded history) instead of erroring, so a broken date computation silently produces a plausible-looking-but-wrong result. Note `/bin/date` IS GNU coreutils 9.8 — the nix profile bin dir simply precedes `/bin` on PATH. **Do date arithmetic in `python3` (`/bin/python3`, always present) via `datetime`/`timedelta`, emitting `VAR=value` lines consumed by `eval "$(python3 - <<'PY' ... PY\n)"`;** call `/bin/date` by absolute path only if GNU date is genuinely required. Corollary that generalizes beyond dates: whenever a computed shell variable feeds a `git log --since/--until`-style range filter, assert it is non-empty first — git's "empty arg means no filter" behavior turns an upstream failure into silent wrong output rather than a crash. Same BusyBox-shadows-GNU family as the `grep -P` and `mktemp` entries above.
|
||||
|
||||
- **A scheduler resource-accounting test fixture that lets a job's `xce_res_account_t.residx` resolve LAZILY (left at -1, relying on `sched_update_res_accounts()`'s own first-use `key_find()`/`add_vres()` fallback) can SEGV inside `zarray_get_ex()` instead of failing loudly.** (2026-08-23, scheduler-dispatch-commit task 2.1, bug-1629): a minimal test fixture that never pre-registers a resource name in `core->resmgr` (no real resource-config load, unlike production) hit a silent failure path — `sched_update_account()` discards `sched_update_res_accounts()`'s return value entirely, so if that inner call's `key_find()`/`add_vres()` resolution doesn't complete, `residx` silently stays `-1` with no error surfaced anywhere up the call chain (`sched_commit_decision()` still returns success). `zarray_get_ex()` (`lib/zarray.c:332`) only guards `idx >= array->caps`, NOT `idx < 0`, so passing that leftover `-1` to `get_resvals()`/`put_resvals()` reads `array->data[-1]` — an underflow read ASAN reports as a high-address SEGV, not a clean bounds error. Fix: in any test fixture exercising real charge/accounting code without loading real resource config, pre-register the resource explicitly first via `add_vres(&vres, core.resmgr, name, 0)` and use its returned (non-negative) index for `get_resvals()`/`put_resvals()`, rather than trusting the production lazy-resolution path to run to completion inside a minimal fixture.
|
||||
|
||||
- **(2026-08-26, bug-1715) Running xceadmin's pytest suite under `nix develop --command bash -c "... PYTHONPATH=src python3 -m pytest ..."` with a bare `PYTHONPATH=src` (no `:$PYTHONPATH`) silently DISCARDS the whole nix-provided PYTHONPATH (pycapnp/click/jsonschema/pytest itself all live there, per the PYTHONPATH-entries Key Learning above) instead of extending it — fails with `No module named pytest`, easy to misread as "pytest isn't installed in this devshell" when it actually is.** Always use `PYTHONPATH=src:$PYTHONPATH` (prepend, don't replace) when adding `src/xceadmin/src` to the path for a one-off `python3 -m pytest`/script invocation inside `nix develop`.
|
||||
- **(2026-08-26, bug-1715) The AUTHORITATIVE, complete list of every literal `sched_admin_reply_t.error = "..."` string the real controller/scheduler can send (confirmed by grepping `src/controller/` and `src/scheduler/` for the assignment, then reading each site's surrounding code — not from requirements.md's own worked examples, which under-lists them):** transient/connection-timing (self-resolving, safe to retry): `"scheduler disconnected"` (cluster.c:1062, bug-1693's disconnect path), `"scheduler not connected"` (cluster.c:958, `c->sched.ep==NULL`), `"request timeout"` (cluster.c:1078, `SCHEDADMIN_TIMEOUT_MS` wait expired), `"server busy, try again"` (cluster.c:937, `vtaskmgr_blocking_acquire()` admission-reservation exhaustion — NOT previously documented as transient anywhere, found by reading the admission-gate comment, not the string alone). Permanent/domain (must never retry): `"already recording"` / `"not recording"` (driver.c:390/426, `rc==-EALREADY`) and `validation_error_text()`'s six filename/path-rejection strings (driver.c:298-316: `"invalid record file name"`, `"record directory not found"`, `"record directory path is not a directory"`, `"permission denied accessing record file"`, `"a non-file entry with this name already exists"`, `"record file path too long"`), plus a dynamic (non-literal, can't exact-match) `"cannot write to %s: %s"` I/O-failure message (driver.c:398-404). When classifying an error-reply reason string as retry-safe vs not, read the ERROR-SITE's surrounding code/comments, not just the string's English wording — "server busy, try again" was only correctly classified transient by reading `vtaskmgr_blocking_acquire()`'s admission-gate design comment, not from requirements.md.
|
||||
- **(2026-08-26, xce-test-rc-file task 3.3, bug-1719) `src/tools/xce-build.sh <component>` with NO `--incremental` does a full `rm -rf "$BUILD_DIR"` + fresh `meson setup` by design (per its own top-of-file comment), then `meson compile` only rebuilds the ONE requested component — so building a second component (e.g. `scheduler`) into an `xce.build` dir that already has a different component's binaries (e.g. `controller`'s `xce_controller`/`xce_test`) silently WIPES the earlier ones.** Always pass `--incremental` when you need a component built alongside another already-built one in the same dir, and never pass an explicit build_dir positional arg unless it matches the script's own default relative-path resolution (`xce.build`, resolved from `src/tools/xce-build.sh`'s own cwd) — passing a repo-root-relative path like `src/xce.build` as the 3rd positional creates a bogus nested `src/src/xce.build` instead (harmless/gitignored via meson's own per-builddir auto `.gitignore: *`, but wastes a build cycle). Rebuilding the wiped component with `--incremental` restores it without disturbing the other.
|
||||
- **(2026-08-26, xce-test-rc-file task 3.3, bug-1720) `xce-test-resources`'s apiserver layer reporting UNREACHABLE for every resource does not by itself mean a propagation regression — check for a stale bind conflict on :8080 first (`lsof -i :8080` / `pgrep -af apiserver`).** An orphaned `/tmp/apiserver` process from an unrelated, long-concluded prior session (its own deploy dir already deleted, 7.5h uptime) was still bound to :8080, so THIS run's fresh apiserver instance failed `listen tcp :8080: bind: address already in use` and never logged anything — surfacing identically to a genuine trace-line-never-appeared UNREACHABLE. `run-resource-verify.sh`'s own trap-based cleanup only reaps processes IT started, not orphans left by an earlier, already-ended unrelated script/session.
|
||||
|
||||
## Decision Log
|
||||
- HA/consistency model follows Raft + CP strictly: majority ACK is the hard commit barrier for all state-changing writes (reject on timeout, never local-apply-and-degrade); the own-term commit rule (not a 4th "COMMITTING" state) is what gives a new leader safety.
|
||||
- A controller process must NEVER self-exit/self-abort as a designed response to an operational condition (only a genuine crash or admin/supervisor kill) — an unrecoverable/fenced member logs loudly, stays permanently non-promotable, stays alive, and keeps retrying.
|
||||
|
|
@ -197,3 +223,7 @@
|
|||
- **A buglog entry's own "root cause: NOT YET ROOT-CAUSED" (or similarly hedged) status is a real signal to re-derive the causal claim from primary evidence before citing it as settled, especially when later re-occurrences of the SAME symptom show it firing on completely unrelated diffs.** bug-1429 (cited by `config_repl.c`/`ha_convergence.c`'s catchup/demotion offload-flag comments as "the vtask pool that deadlocked controller_unit_test on 2026-07-10") is itself logged as never root-caused, and its OWN later occurrences (2026-08-20, 2026-08-23) reproduced the identical TIMEOUT/hang signature on diffs that touched neither the vtask offload code nor persist.c's checkpoint path at all — "strongly corroborates this is a pre-existing, systemic, timing/order-dependent hang in the controller_unit_test harness," per the buglog's own later entries, not proven caused by VTASK_LIVE_BLOCK dispatch specifically. Don't repeat a downstream comment's causal claim as fact in a new design doc/comment without checking the buglog entry it cites — the comment can be more confident than the evidence it's citing.
|
||||
- **`controller_unit_test` (the main shared meson.build `controller_unit_test_src` binary) does NOT link the real `sched.c`/`dataex.c` at all** (bug-1693, 2026-08-25) — `sched.c`'s own `on_scheduler_join()`/`collect_confvars()` and `dataex.c`'s `notify()` collide at link time with fake local definitions several OTHER files in that same binary already provide (`test_commit_barrier_reply.c`, `test_worker_join.c`), so `test_config_repl.c` instead ships a trivial no-op `handle_scheduler_leave(cluster_t *c) { (void)c; return 0; }` stub purely to satisfy the linker. **Any new/changed behavior added to a real `sched.c` function is therefore SILENTLY UNTESTED by any `controller_unit_test`-based test that calls that function by name** — the call resolves to the stub, not the real implementation, with no compile/link error to flag the mismatch (confirmed empirically: a new regression test asserting sub-second wake timing after calling `handle_scheduler_leave()` passed compilation and linking fine, then failed by taking the full 5s deadline — the stub had silently swallowed the call). `nm '<obj>.o' | grep ' T <symbol>$'` across every `.o` in the target's build dir is the fast way to confirm which TU actually provides a given symbol in a given binary before trusting a test's result. The two standalone binaries that DO link the real `sched.c` (`sched_push_unit_test`, `sched_worker_change_unit_test` — see their own `meson.build` comments for the collision rationale) don't link `cluster.c`, so neither binary alone can exercise a fix that spans both files for real; bug-1693's fix split verification across both (a direct-call test against `cluster.c`'s real new function in `controller_unit_test`, plus a small counting-spy test in `test_sched_worker_change.c` proving `sched.c`'s real caller invokes it) rather than inventing a third binary that links both (would need to duplicate most of `controller_unit_test_src`'s already-large dependency list to satisfy `cluster.c`'s own huge fan-out). General lesson: when a fix spans a function in `sched.c`/`dataex.c` AND a function in any of the ~15 other files real-linked into `controller_unit_test_src`, check with `nm`/grep for a same-name fake stub in a sibling test file in that binary BEFORE trusting a test that calls the `sched.c`/`dataex.c` side by name — it may be exercising a stub, not your change.
|
||||
- **(2026-08-25, bug-1695) A bootstrap "is a leader already known" fallback check must compare a term against a snapshot taken at THIS boot's start, never against a bare constant like `> 0`, whenever the compared value is restored from durable on-disk state that only ever increases (Raft terms, monotonic counters, etc.).** `myapp_run()`'s `raft.term > 0` fallback was trivially true forever after the cluster's first-ever election because `ha_restore_term()` restores a persisted, never-decreasing term at every boot. Fixed by capturing `term_at_boot = ha->raft.term` immediately after `ha_init_raft()` seeds it (right after restore, strictly before any new activity could legitimately advance it) and comparing `> term_at_boot` instead of `> 0` — the general pattern: "is X known/true" checks against monotonic restored state should ask "did X change SINCE I booted," not "is X nonzero," whenever a nonzero-forever steady state is a normal, expected outcome of prior successful operation (not an error state).
|
||||
- **(2026-08-25, bug-1697, CRITICAL — live, no-restart data bug) Whenever a semantic change makes one code path (e.g. a scheduler dispatch decision) start ANTICIPATING a state transition another, later code path (e.g. a worker's ack) used to be the sole author of, re-audit EVERY `result == JOB_TRANSITION_APPLIED`-gated (or equivalent "did this actually change anything" gated) side effect on that later path — the earlier path's own transition now permanently makes the later one a same-state no-op, silently killing every effect gated on "this was a real transition."** `dispatch_a_job_()` (job.c:1686, scheduler-dispatch-commit task 3.1) was changed to set `state = JOB_STAT_RUN` synchronously at DISPATCH time (the controller now decides RUN, it no longer waits for a worker ack) — but `apply_committed_entry()`'s WAL_JOB_START case (added by bug-1687, persist.c ~4594-4637) still gated its `start_time`/`pidinfo` wire-decode adoption on `result == JOB_TRANSITION_APPLIED` from `apply_job_update_transition()`, which now ALWAYS returns `JOB_TRANSITION_NOOP_DUPLICATE` for WAL_JOB_START (job.c:2795-2799, state already == RUN from dispatch) — so start_time/pidinfo silently never get set, for every job, live, no restart needed. The exact same class of bug had ALREADY been fixed for WAL_JOB_RUSAGE/CKPT/PREASON in this identical function (their own comments describe the identical hazard) — a fix for one wal_type in a shared-transition-guard case group does not imply sibling types sharing the SAME guard got the same fix; grep the whole case group whenever changing which code path is allowed to "arrive first" at a shared target state. bug-1679's own comment (job.c:1709-1719) had ALREADY independently hit and worked around the identical root collision for a different symptom (apiserver update-notification hook not firing) months/fixes apart from this one — the two were never connected as one root cause until a live "start_time is always 0" report forced re-deriving it from scratch. **General technique: after any change that makes state transition X reachable from a NEW, earlier code path, grep every existing consumer of "was this transition genuinely applied" (`result == *_APPLIED`, `state != cur`, etc.) that used to assume X's only author was the OLD path — each is a candidate for silently going permanently dead.** **FIXED (2026-08-26):** moved `start_time` ownership to the dispatch-time mutation itself (`WAL_JOB_DISPATCH`'s payload extended with the new `WAL_JOB_DISPATCH_HAS_START_TIME_FLAG` `_pad` bit, mirroring `WAL_JOB_FAIL_STAGE_FLAG` — NOT a length heuristic, since `JobAlloc`'s own variable length makes old-vs-new payload length ambiguous); `pidinfo` stays sourced from the worker's ack but its adoption is now unconditional like RUSAGE/CKPT/PREASON. RED confirmed by temp-reverting only the two defect lines (job.c's `start_time` stamp; persist.c's pidinfo `result==APPLIED` gate) and rebuilding — 5 genuine new failures at the exact production call sites (`dispatch_a_job_test`, `apply_committed_entry` seeded already-RUN). GREEN confirmed after restoring: "1198 Tests 3 Failures 0 Ignored" 3x direct runs, matching known baseline exactly.
|
||||
- **(2026-08-26, bug-1718, xce-test-rc-file task 3.1) A `python3 -` (or any "read program from stdin") invocation placed on the SAME command as a `<<HEREDOC` redirect while ALSO receiving piped input (`upstream | python3 - args <<EOF ... EOF`) silently loses the piped data, no error.** The heredoc redirect wins stdin over the pipe for that command; `python3 -` consumes the heredoc's content as its own program source, so by the time the script's own `sys.stdin.read()` runs, stdin is already at EOF — it just returns `""` with zero diagnostic. Caught by re-reading the draft before ever running it, not by a failing run. **Fix pattern: write the embedded program to its own temp file ONCE (`mktemp` + `cat >"$DRIVER" <<EOF...EOF`, cleaned via an `EXIT` trap), then invoke it as `upstream | python3 "$DRIVER" args...`** — a file argument doesn't compete with the pipe for stdin, unlike `-`. Applies to any interpreter's "read program from stdin" mode (`python3 -`, `perl -e -`, etc.) combined with a heredoc AND a real piped payload in the same pipeline.
|
||||
- **(2026-08-26, xce-test-rc-file task 3.1) `xce_test`'s `cmd_set(argc==1)` (`set` with no args) is the generic, no-live-cluster-required readback for ANY setting** (`test.c:3684`, format `"%-12s:%s default: %s\n"`, `KVAL()` prints `"<not set>"` for a NULL current value) — the right primitive for any integration test that needs to observe an rc-file/`set`-driven setting change (e.g. `masters`) without a live controller connection. Don't hardcode the exact column padding when grepping its output (fragile) — extract via `sed -E 's/^masters[[:space:]]*:(.*) default:.*/\1/'` (greedy `(.*)` correctly captures the embedded-space `"<not set>"` sentinel too, since there's only one literal `" default:"` substring per line to anchor against).
|
||||
- **(2026-08-26, xce-test-rc-file task 3.1) `xce_test` uses `libedit`'s `readline()`/`isatty(STDIN_FILENO)`-gated `g_interactive`, so a genuine interactive-mode integration test needs a REAL pty, not a piped stdin** — confirmed working via a small embedded Python `pty.fork()` driver (write commands to the master fd with a short settle delay after each, drain with `select()`, detect child exit via non-blocking `waitpid`, `os.waitstatus_to_exitcode()` for the real exit code). Same technique task 2.2's own implementer used for manual verification; this task turned it into a reusable scripted harness. The prompt itself (`fill_prompt`, top-level context) renders as literal `"xce>"` — grepping for that substring in the pty transcript is a good "this really was a TTY session, not a piped fallback" sanity check, distinct from the actual assertion under test.
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@
|
|||
"report_frequency": "weekly",
|
||||
"waste_threshold_percent": 15,
|
||||
"chars_per_token_code": 3.5,
|
||||
"chars_per_token_prose": 4.0
|
||||
"chars_per_token_prose": 4
|
||||
},
|
||||
"cron": {
|
||||
"enabled": true,
|
||||
|
|
@ -63,11 +63,22 @@
|
|||
"designqc": {
|
||||
"enabled": true,
|
||||
"viewports": [
|
||||
{ "name": "desktop", "width": 1440, "height": 900 },
|
||||
{ "name": "mobile", "width": 375, "height": 812 }
|
||||
{
|
||||
"name": "desktop",
|
||||
"width": 1440,
|
||||
"height": 900
|
||||
},
|
||||
{
|
||||
"name": "mobile",
|
||||
"width": 375,
|
||||
"height": 812
|
||||
}
|
||||
],
|
||||
"max_screenshots": 6,
|
||||
"chrome_path": null
|
||||
}
|
||||
},
|
||||
"agents": [
|
||||
"claude"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +1,7 @@
|
|||
{
|
||||
"last_heartbeat": "2026-08-17T15:18:42.178Z",
|
||||
"last_heartbeat": "2026-08-27T07:57:45.420Z",
|
||||
"engine_status": "running",
|
||||
"execution_log": [
|
||||
{
|
||||
"task_id": "anatomy-rescan",
|
||||
"status": "success",
|
||||
"timestamp": "2026-05-21T07:00:00.623Z",
|
||||
"duration_ms": 37
|
||||
},
|
||||
{
|
||||
"task_id": "project-suggestions",
|
||||
"status": "failed",
|
||||
|
|
@ -563,6 +557,70 @@
|
|||
"status": "success",
|
||||
"timestamp": "2026-08-17T01:00:00.082Z",
|
||||
"duration_ms": 52
|
||||
},
|
||||
{
|
||||
"task_id": "anatomy-rescan",
|
||||
"status": "success",
|
||||
"timestamp": "2026-08-26T19:00:00.780Z",
|
||||
"duration_ms": 770
|
||||
},
|
||||
{
|
||||
"task_id": "anatomy-rescan",
|
||||
"status": "success",
|
||||
"timestamp": "2026-08-27T01:00:00.016Z",
|
||||
"duration_ms": 15
|
||||
},
|
||||
{
|
||||
"task_id": "memory-consolidation",
|
||||
"status": "success",
|
||||
"timestamp": "2026-08-27T03:31:19.434Z",
|
||||
"duration_ms": 16
|
||||
},
|
||||
{
|
||||
"task_id": "token-audit",
|
||||
"status": "success",
|
||||
"timestamp": "2026-08-27T03:31:23.849Z",
|
||||
"duration_ms": 4401
|
||||
},
|
||||
{
|
||||
"task_id": "cerebrum-reflection",
|
||||
"status": "failed",
|
||||
"timestamp": "2026-08-27T03:31:23.903Z",
|
||||
"duration_ms": 5,
|
||||
"error": "ai_task is no longer supported: OpenWolf makes no model calls. Remove this task from .wolf/cron-manifest.json."
|
||||
},
|
||||
{
|
||||
"task_id": "project-suggestions",
|
||||
"status": "failed",
|
||||
"timestamp": "2026-08-27T03:31:23.926Z",
|
||||
"duration_ms": 3,
|
||||
"error": "ai_task is no longer supported: OpenWolf makes no model calls. Remove this task from .wolf/cron-manifest.json."
|
||||
},
|
||||
{
|
||||
"task_id": "anatomy-rescan",
|
||||
"status": "success",
|
||||
"timestamp": "2026-08-27T03:31:24.433Z",
|
||||
"duration_ms": 11808
|
||||
},
|
||||
{
|
||||
"task_id": "cerebrum-reflection",
|
||||
"status": "failed",
|
||||
"timestamp": "2026-08-27T03:31:36.511Z",
|
||||
"duration_ms": 1,
|
||||
"error": "ai_task is no longer supported: OpenWolf makes no model calls. Remove this task from .wolf/cron-manifest.json."
|
||||
},
|
||||
{
|
||||
"task_id": "project-suggestions",
|
||||
"status": "failed",
|
||||
"timestamp": "2026-08-27T03:31:38.593Z",
|
||||
"duration_ms": 1,
|
||||
"error": "ai_task is no longer supported: OpenWolf makes no model calls. Remove this task from .wolf/cron-manifest.json."
|
||||
},
|
||||
{
|
||||
"task_id": "anatomy-rescan",
|
||||
"status": "success",
|
||||
"timestamp": "2026-08-27T07:00:04.613Z",
|
||||
"duration_ms": 4599
|
||||
}
|
||||
],
|
||||
"dead_letter_queue": [],
|
||||
|
|
|
|||
228
.wolf/daemon.log
228
.wolf/daemon.log
|
|
@ -443,3 +443,231 @@
|
|||
[2026-08-17T14:53:37.553Z] [WARN] Skipping broadcast for large file: token-ledger.json (25615433 bytes)
|
||||
[2026-08-17T15:00:29.231Z] [WARN] Skipping broadcast for large file: token-ledger.json (25627432 bytes)
|
||||
[2026-08-17T15:13:14.029Z] [WARN] Skipping broadcast for large file: token-ledger.json (25641990 bytes)
|
||||
[2026-08-26T14:39:06.150Z] [INFO] Scheduled task: Full anatomy rescan (0 */6 * * *)
|
||||
[2026-08-26T14:39:06.151Z] [INFO] Scheduled task: Consolidate old memory (0 2 * * *)
|
||||
[2026-08-26T14:39:06.152Z] [INFO] Scheduled task: Token audit report (0 0 * * 1)
|
||||
[2026-08-26T14:39:06.152Z] [INFO] Scheduled task: Cerebrum reflection (0 3 * * 0)
|
||||
[2026-08-26T14:39:06.152Z] [INFO] Scheduled task: AI suggestions (0 4 * * 1)
|
||||
[2026-08-26T14:39:06.153Z] [INFO] File watcher started on .wolf/
|
||||
[2026-08-26T14:39:06.154Z] [INFO] OpenWolf daemon started
|
||||
[2026-08-26T14:39:06.157Z] [INFO] Dashboard server listening on 127.0.0.1:18791
|
||||
[2026-08-26T14:39:12.810Z] [INFO] WebSocket client connected
|
||||
[2026-08-26T14:40:40.400Z] [INFO] WebSocket client connected
|
||||
[2026-08-26T14:41:22.740Z] [INFO] Config changed — hot-reload not fully implemented yet
|
||||
[2026-08-26T14:42:38.585Z] [WARN] Skipping broadcast for large file: backups/2026-08-26T1441/token-ledger.json (38019592 bytes)
|
||||
[2026-08-26T14:42:38.587Z] [WARN] Skipping broadcast for large file: backups/2026-08-26T1441/buglog.json (1589811 bytes)
|
||||
[2026-08-26T14:42:38.588Z] [INFO] Config changed — hot-reload not fully implemented yet
|
||||
[2026-08-26T14:42:38.588Z] [INFO] Cron manifest changed — restart daemon to apply
|
||||
[2026-08-26T14:42:42.129Z] [WARN] Skipping broadcast for large file: token-ledger.json (3431917 bytes)
|
||||
[2026-08-26T14:43:13.045Z] [INFO] WebSocket client connected
|
||||
[2026-08-26T14:44:13.743Z] [WARN] Skipping broadcast for large file: backups/2026-08-26T1441/buglog.json (1589811 bytes)
|
||||
[2026-08-26T14:44:13.743Z] [WARN] Skipping broadcast for large file: backups/2026-08-26T1441/token-ledger.json (38019592 bytes)
|
||||
[2026-08-26T14:44:13.750Z] [WARN] Skipping broadcast for large file: backups/2026-08-26T1442/token-ledger.json (38019592 bytes)
|
||||
[2026-08-26T14:44:13.750Z] [WARN] Skipping broadcast for large file: backups/2026-08-26T1442/buglog.json (1589811 bytes)
|
||||
[2026-08-26T14:44:13.751Z] [INFO] Config changed — hot-reload not fully implemented yet
|
||||
[2026-08-26T14:44:13.751Z] [INFO] Cron manifest changed — restart daemon to apply
|
||||
[2026-08-26T14:44:13.757Z] [WARN] Skipping broadcast for large file: backups/2026-08-26T1443/token-ledger.json (3431917 bytes)
|
||||
[2026-08-26T14:44:13.757Z] [WARN] Skipping broadcast for large file: backups/2026-08-26T1443/buglog.json (1589811 bytes)
|
||||
[2026-08-26T14:44:13.757Z] [INFO] Config changed — hot-reload not fully implemented yet
|
||||
[2026-08-26T14:44:13.758Z] [INFO] Cron manifest changed — restart daemon to apply
|
||||
[2026-08-26T14:44:23.663Z] [INFO] WebSocket client connected
|
||||
[2026-08-26T15:00:24.317Z] [WARN] Skipping broadcast for large file: buglog.json (1591627 bytes)
|
||||
[2026-08-26T16:23:28.374Z] [WARN] Skipping broadcast for large file: buglog.json (1591288 bytes)
|
||||
[2026-08-26T16:24:02.661Z] [WARN] Skipping broadcast for large file: cache/buglog.db (3063808 bytes)
|
||||
[2026-08-26T17:20:09.118Z] [WARN] Skipping broadcast for large file: buglog.json (1598455 bytes)
|
||||
[2026-08-26T17:21:17.838Z] [WARN] Skipping broadcast for large file: cache/buglog.db (3096576 bytes)
|
||||
[2026-08-26T19:00:00.010Z] [INFO] Executing task: Full anatomy rescan
|
||||
[2026-08-26T19:00:00.780Z] [INFO] Task Full anatomy rescan completed in 770ms
|
||||
[2026-08-27T01:00:00.001Z] [INFO] Executing task: Full anatomy rescan
|
||||
[2026-08-27T01:00:00.015Z] [INFO] anatomy rescan skipped: index matches the tree (stat sweep + git HEAD)
|
||||
[2026-08-27T01:00:00.016Z] [INFO] Task Full anatomy rescan completed in 15ms
|
||||
[2026-08-27T02:24:19.145Z] [WARN] Skipping broadcast for large file: buglog.json (1600340 bytes)
|
||||
[2026-08-27T02:32:17.820Z] [WARN] Skipping broadcast for large file: cache/buglog.db (3096576 bytes)
|
||||
[2026-08-27T03:26:07.499Z] [INFO] Scheduled task: Full anatomy rescan (0 */6 * * *)
|
||||
[2026-08-27T03:26:07.501Z] [INFO] Scheduled task: Consolidate old memory (0 2 * * *)
|
||||
[2026-08-27T03:26:07.501Z] [INFO] Scheduled task: Token audit report (0 0 * * 1)
|
||||
[2026-08-27T03:26:07.502Z] [INFO] Scheduled task: Cerebrum reflection (0 3 * * 0)
|
||||
[2026-08-27T03:26:07.502Z] [INFO] Scheduled task: AI suggestions (0 4 * * 1)
|
||||
[2026-08-27T03:26:07.503Z] [INFO] File watcher started on .wolf/
|
||||
[2026-08-27T03:26:07.504Z] [INFO] OpenWolf daemon started
|
||||
[2026-08-27T03:26:07.509Z] [INFO] Dashboard server listening on 127.0.0.1:18791
|
||||
[2026-08-27T03:26:07.509Z] [ERROR] Dashboard server failed to bind 127.0.0.1:18791: EADDRINUSE
|
||||
[2026-08-27T03:26:07.686Z] [INFO] Scheduled task: Full anatomy rescan (0 */6 * * *)
|
||||
[2026-08-27T03:26:07.687Z] [INFO] Scheduled task: Consolidate old memory (0 2 * * *)
|
||||
[2026-08-27T03:26:07.688Z] [INFO] Scheduled task: Token audit report (0 0 * * 1)
|
||||
[2026-08-27T03:26:07.688Z] [INFO] Scheduled task: Cerebrum reflection (0 3 * * 0)
|
||||
[2026-08-27T03:26:07.689Z] [INFO] Scheduled task: AI suggestions (0 4 * * 1)
|
||||
[2026-08-27T03:26:07.689Z] [INFO] File watcher started on .wolf/
|
||||
[2026-08-27T03:26:07.690Z] [INFO] OpenWolf daemon started
|
||||
[2026-08-27T03:26:07.694Z] [INFO] Dashboard server listening on 127.0.0.1:18791
|
||||
[2026-08-27T03:26:07.694Z] [ERROR] Dashboard server failed to bind 127.0.0.1:18791: EADDRINUSE
|
||||
[2026-08-27T03:26:07.874Z] [INFO] Scheduled task: Full anatomy rescan (0 */6 * * *)
|
||||
[2026-08-27T03:26:07.875Z] [INFO] Scheduled task: Consolidate old memory (0 2 * * *)
|
||||
[2026-08-27T03:26:07.876Z] [INFO] Scheduled task: Token audit report (0 0 * * 1)
|
||||
[2026-08-27T03:26:07.876Z] [INFO] Scheduled task: Cerebrum reflection (0 3 * * 0)
|
||||
[2026-08-27T03:26:07.876Z] [INFO] Scheduled task: AI suggestions (0 4 * * 1)
|
||||
[2026-08-27T03:26:07.877Z] [INFO] File watcher started on .wolf/
|
||||
[2026-08-27T03:26:07.878Z] [INFO] OpenWolf daemon started
|
||||
[2026-08-27T03:26:07.882Z] [INFO] Dashboard server listening on 127.0.0.1:18791
|
||||
[2026-08-27T03:26:07.882Z] [ERROR] Dashboard server failed to bind 127.0.0.1:18791: EADDRINUSE
|
||||
[2026-08-27T03:26:08.054Z] [INFO] Scheduled task: Full anatomy rescan (0 */6 * * *)
|
||||
[2026-08-27T03:26:08.055Z] [INFO] Scheduled task: Consolidate old memory (0 2 * * *)
|
||||
[2026-08-27T03:26:08.055Z] [INFO] Scheduled task: Token audit report (0 0 * * 1)
|
||||
[2026-08-27T03:26:08.056Z] [INFO] Scheduled task: Cerebrum reflection (0 3 * * 0)
|
||||
[2026-08-27T03:26:08.056Z] [INFO] Scheduled task: AI suggestions (0 4 * * 1)
|
||||
[2026-08-27T03:26:08.057Z] [INFO] File watcher started on .wolf/
|
||||
[2026-08-27T03:26:08.058Z] [INFO] OpenWolf daemon started
|
||||
[2026-08-27T03:26:08.061Z] [INFO] Dashboard server listening on 127.0.0.1:18791
|
||||
[2026-08-27T03:26:08.061Z] [ERROR] Dashboard server failed to bind 127.0.0.1:18791: EADDRINUSE
|
||||
[2026-08-27T03:26:08.221Z] [INFO] Scheduled task: Full anatomy rescan (0 */6 * * *)
|
||||
[2026-08-27T03:26:08.222Z] [INFO] Scheduled task: Consolidate old memory (0 2 * * *)
|
||||
[2026-08-27T03:26:08.223Z] [INFO] Scheduled task: Token audit report (0 0 * * 1)
|
||||
[2026-08-27T03:26:08.223Z] [INFO] Scheduled task: Cerebrum reflection (0 3 * * 0)
|
||||
[2026-08-27T03:26:08.223Z] [INFO] Scheduled task: AI suggestions (0 4 * * 1)
|
||||
[2026-08-27T03:26:08.224Z] [INFO] File watcher started on .wolf/
|
||||
[2026-08-27T03:26:08.225Z] [INFO] OpenWolf daemon started
|
||||
[2026-08-27T03:26:08.228Z] [INFO] Dashboard server listening on 127.0.0.1:18791
|
||||
[2026-08-27T03:26:08.228Z] [ERROR] Dashboard server failed to bind 127.0.0.1:18791: EADDRINUSE
|
||||
[2026-08-27T03:26:08.383Z] [INFO] Scheduled task: Full anatomy rescan (0 */6 * * *)
|
||||
[2026-08-27T03:26:08.384Z] [INFO] Scheduled task: Consolidate old memory (0 2 * * *)
|
||||
[2026-08-27T03:26:08.385Z] [INFO] Scheduled task: Token audit report (0 0 * * 1)
|
||||
[2026-08-27T03:26:08.385Z] [INFO] Scheduled task: Cerebrum reflection (0 3 * * 0)
|
||||
[2026-08-27T03:26:08.385Z] [INFO] Scheduled task: AI suggestions (0 4 * * 1)
|
||||
[2026-08-27T03:26:08.386Z] [INFO] File watcher started on .wolf/
|
||||
[2026-08-27T03:26:08.387Z] [INFO] OpenWolf daemon started
|
||||
[2026-08-27T03:26:08.390Z] [INFO] Dashboard server listening on 127.0.0.1:18791
|
||||
[2026-08-27T03:26:08.390Z] [ERROR] Dashboard server failed to bind 127.0.0.1:18791: EADDRINUSE
|
||||
[2026-08-27T03:26:08.556Z] [INFO] Scheduled task: Full anatomy rescan (0 */6 * * *)
|
||||
[2026-08-27T03:26:08.557Z] [INFO] Scheduled task: Consolidate old memory (0 2 * * *)
|
||||
[2026-08-27T03:26:08.558Z] [INFO] Scheduled task: Token audit report (0 0 * * 1)
|
||||
[2026-08-27T03:26:08.558Z] [INFO] Scheduled task: Cerebrum reflection (0 3 * * 0)
|
||||
[2026-08-27T03:26:08.558Z] [INFO] Scheduled task: AI suggestions (0 4 * * 1)
|
||||
[2026-08-27T03:26:08.559Z] [INFO] File watcher started on .wolf/
|
||||
[2026-08-27T03:26:08.560Z] [INFO] OpenWolf daemon started
|
||||
[2026-08-27T03:26:08.564Z] [INFO] Dashboard server listening on 127.0.0.1:18791
|
||||
[2026-08-27T03:26:08.564Z] [ERROR] Dashboard server failed to bind 127.0.0.1:18791: EADDRINUSE
|
||||
[2026-08-27T03:26:08.725Z] [INFO] Scheduled task: Full anatomy rescan (0 */6 * * *)
|
||||
[2026-08-27T03:26:08.726Z] [INFO] Scheduled task: Consolidate old memory (0 2 * * *)
|
||||
[2026-08-27T03:26:08.727Z] [INFO] Scheduled task: Token audit report (0 0 * * 1)
|
||||
[2026-08-27T03:26:08.727Z] [INFO] Scheduled task: Cerebrum reflection (0 3 * * 0)
|
||||
[2026-08-27T03:26:08.728Z] [INFO] Scheduled task: AI suggestions (0 4 * * 1)
|
||||
[2026-08-27T03:26:08.728Z] [INFO] File watcher started on .wolf/
|
||||
[2026-08-27T03:26:08.729Z] [INFO] OpenWolf daemon started
|
||||
[2026-08-27T03:26:08.732Z] [INFO] Dashboard server listening on 127.0.0.1:18791
|
||||
[2026-08-27T03:26:08.732Z] [ERROR] Dashboard server failed to bind 127.0.0.1:18791: EADDRINUSE
|
||||
[2026-08-27T03:26:08.895Z] [INFO] Scheduled task: Full anatomy rescan (0 */6 * * *)
|
||||
[2026-08-27T03:26:08.896Z] [INFO] Scheduled task: Consolidate old memory (0 2 * * *)
|
||||
[2026-08-27T03:26:08.896Z] [INFO] Scheduled task: Token audit report (0 0 * * 1)
|
||||
[2026-08-27T03:26:08.896Z] [INFO] Scheduled task: Cerebrum reflection (0 3 * * 0)
|
||||
[2026-08-27T03:26:08.897Z] [INFO] Scheduled task: AI suggestions (0 4 * * 1)
|
||||
[2026-08-27T03:26:08.897Z] [INFO] File watcher started on .wolf/
|
||||
[2026-08-27T03:26:08.898Z] [INFO] OpenWolf daemon started
|
||||
[2026-08-27T03:26:08.901Z] [INFO] Dashboard server listening on 127.0.0.1:18791
|
||||
[2026-08-27T03:26:08.902Z] [ERROR] Dashboard server failed to bind 127.0.0.1:18791: EADDRINUSE
|
||||
[2026-08-27T03:26:09.058Z] [INFO] Scheduled task: Full anatomy rescan (0 */6 * * *)
|
||||
[2026-08-27T03:26:09.059Z] [INFO] Scheduled task: Consolidate old memory (0 2 * * *)
|
||||
[2026-08-27T03:26:09.060Z] [INFO] Scheduled task: Token audit report (0 0 * * 1)
|
||||
[2026-08-27T03:26:09.060Z] [INFO] Scheduled task: Cerebrum reflection (0 3 * * 0)
|
||||
[2026-08-27T03:26:09.061Z] [INFO] Scheduled task: AI suggestions (0 4 * * 1)
|
||||
[2026-08-27T03:26:09.061Z] [INFO] File watcher started on .wolf/
|
||||
[2026-08-27T03:26:09.062Z] [INFO] OpenWolf daemon started
|
||||
[2026-08-27T03:26:09.065Z] [INFO] Dashboard server listening on 127.0.0.1:18791
|
||||
[2026-08-27T03:26:09.066Z] [ERROR] Dashboard server failed to bind 127.0.0.1:18791: EADDRINUSE
|
||||
[2026-08-27T03:26:09.224Z] [INFO] Scheduled task: Full anatomy rescan (0 */6 * * *)
|
||||
[2026-08-27T03:26:09.225Z] [INFO] Scheduled task: Consolidate old memory (0 2 * * *)
|
||||
[2026-08-27T03:26:09.225Z] [INFO] Scheduled task: Token audit report (0 0 * * 1)
|
||||
[2026-08-27T03:26:09.226Z] [INFO] Scheduled task: Cerebrum reflection (0 3 * * 0)
|
||||
[2026-08-27T03:26:09.226Z] [INFO] Scheduled task: AI suggestions (0 4 * * 1)
|
||||
[2026-08-27T03:26:09.227Z] [INFO] File watcher started on .wolf/
|
||||
[2026-08-27T03:26:09.227Z] [INFO] OpenWolf daemon started
|
||||
[2026-08-27T03:26:09.230Z] [INFO] Dashboard server listening on 127.0.0.1:18791
|
||||
[2026-08-27T03:26:09.231Z] [ERROR] Dashboard server failed to bind 127.0.0.1:18791: EADDRINUSE
|
||||
[2026-08-27T03:26:09.388Z] [INFO] Scheduled task: Full anatomy rescan (0 */6 * * *)
|
||||
[2026-08-27T03:26:09.389Z] [INFO] Scheduled task: Consolidate old memory (0 2 * * *)
|
||||
[2026-08-27T03:26:09.390Z] [INFO] Scheduled task: Token audit report (0 0 * * 1)
|
||||
[2026-08-27T03:26:09.390Z] [INFO] Scheduled task: Cerebrum reflection (0 3 * * 0)
|
||||
[2026-08-27T03:26:09.390Z] [INFO] Scheduled task: AI suggestions (0 4 * * 1)
|
||||
[2026-08-27T03:26:09.391Z] [INFO] File watcher started on .wolf/
|
||||
[2026-08-27T03:26:09.392Z] [INFO] OpenWolf daemon started
|
||||
[2026-08-27T03:26:09.395Z] [INFO] Dashboard server listening on 127.0.0.1:18791
|
||||
[2026-08-27T03:26:09.395Z] [ERROR] Dashboard server failed to bind 127.0.0.1:18791: EADDRINUSE
|
||||
[2026-08-27T03:26:09.555Z] [INFO] Scheduled task: Full anatomy rescan (0 */6 * * *)
|
||||
[2026-08-27T03:26:09.556Z] [INFO] Scheduled task: Consolidate old memory (0 2 * * *)
|
||||
[2026-08-27T03:26:09.557Z] [INFO] Scheduled task: Token audit report (0 0 * * 1)
|
||||
[2026-08-27T03:26:09.557Z] [INFO] Scheduled task: Cerebrum reflection (0 3 * * 0)
|
||||
[2026-08-27T03:26:09.557Z] [INFO] Scheduled task: AI suggestions (0 4 * * 1)
|
||||
[2026-08-27T03:26:09.558Z] [INFO] File watcher started on .wolf/
|
||||
[2026-08-27T03:26:09.559Z] [INFO] OpenWolf daemon started
|
||||
[2026-08-27T03:26:09.562Z] [INFO] Dashboard server listening on 127.0.0.1:18791
|
||||
[2026-08-27T03:26:09.562Z] [ERROR] Dashboard server failed to bind 127.0.0.1:18791: EADDRINUSE
|
||||
[2026-08-27T03:26:09.726Z] [INFO] Scheduled task: Full anatomy rescan (0 */6 * * *)
|
||||
[2026-08-27T03:26:09.727Z] [INFO] Scheduled task: Consolidate old memory (0 2 * * *)
|
||||
[2026-08-27T03:26:09.727Z] [INFO] Scheduled task: Token audit report (0 0 * * 1)
|
||||
[2026-08-27T03:26:09.728Z] [INFO] Scheduled task: Cerebrum reflection (0 3 * * 0)
|
||||
[2026-08-27T03:26:09.728Z] [INFO] Scheduled task: AI suggestions (0 4 * * 1)
|
||||
[2026-08-27T03:26:09.729Z] [INFO] File watcher started on .wolf/
|
||||
[2026-08-27T03:26:09.730Z] [INFO] OpenWolf daemon started
|
||||
[2026-08-27T03:26:09.733Z] [INFO] Dashboard server listening on 127.0.0.1:18791
|
||||
[2026-08-27T03:26:09.733Z] [ERROR] Dashboard server failed to bind 127.0.0.1:18791: EADDRINUSE
|
||||
[2026-08-27T03:26:09.886Z] [INFO] Scheduled task: Full anatomy rescan (0 */6 * * *)
|
||||
[2026-08-27T03:26:09.887Z] [INFO] Scheduled task: Consolidate old memory (0 2 * * *)
|
||||
[2026-08-27T03:26:09.888Z] [INFO] Scheduled task: Token audit report (0 0 * * 1)
|
||||
[2026-08-27T03:26:09.888Z] [INFO] Scheduled task: Cerebrum reflection (0 3 * * 0)
|
||||
[2026-08-27T03:26:09.888Z] [INFO] Scheduled task: AI suggestions (0 4 * * 1)
|
||||
[2026-08-27T03:26:09.889Z] [INFO] File watcher started on .wolf/
|
||||
[2026-08-27T03:26:09.890Z] [INFO] OpenWolf daemon started
|
||||
[2026-08-27T03:26:09.893Z] [INFO] Dashboard server listening on 127.0.0.1:18791
|
||||
[2026-08-27T03:26:09.893Z] [ERROR] Dashboard server failed to bind 127.0.0.1:18791: EADDRINUSE
|
||||
[2026-08-27T03:26:10.057Z] [INFO] Scheduled task: Full anatomy rescan (0 */6 * * *)
|
||||
[2026-08-27T03:26:10.058Z] [INFO] Scheduled task: Consolidate old memory (0 2 * * *)
|
||||
[2026-08-27T03:26:10.058Z] [INFO] Scheduled task: Token audit report (0 0 * * 1)
|
||||
[2026-08-27T03:26:10.059Z] [INFO] Scheduled task: Cerebrum reflection (0 3 * * 0)
|
||||
[2026-08-27T03:26:10.059Z] [INFO] Scheduled task: AI suggestions (0 4 * * 1)
|
||||
[2026-08-27T03:26:10.060Z] [INFO] File watcher started on .wolf/
|
||||
[2026-08-27T03:26:10.061Z] [INFO] OpenWolf daemon started
|
||||
[2026-08-27T03:26:10.064Z] [INFO] Dashboard server listening on 127.0.0.1:18791
|
||||
[2026-08-27T03:26:10.064Z] [ERROR] Dashboard server failed to bind 127.0.0.1:18791: EADDRINUSE
|
||||
[2026-08-27T03:27:45.410Z] [INFO] Scheduled task: Full anatomy rescan (0 */6 * * *)
|
||||
[2026-08-27T03:27:45.412Z] [INFO] Scheduled task: Consolidate old memory (0 2 * * *)
|
||||
[2026-08-27T03:27:45.412Z] [INFO] Scheduled task: Token audit report (0 0 * * 1)
|
||||
[2026-08-27T03:27:45.412Z] [INFO] Scheduled task: Cerebrum reflection (0 3 * * 0)
|
||||
[2026-08-27T03:27:45.413Z] [INFO] Scheduled task: AI suggestions (0 4 * * 1)
|
||||
[2026-08-27T03:27:45.414Z] [INFO] File watcher started on .wolf/
|
||||
[2026-08-27T03:27:45.414Z] [INFO] OpenWolf daemon started
|
||||
[2026-08-27T03:27:45.418Z] [INFO] Dashboard server listening on 127.0.0.1:18791
|
||||
[2026-08-27T03:27:48.321Z] [INFO] WebSocket client connected
|
||||
[2026-08-27T03:28:32.135Z] [INFO] WebSocket client connected
|
||||
[2026-08-27T03:30:19.312Z] [WARN] Skipping broadcast for large file: backups/2026-08-26T1441/token-ledger.json (38019592 bytes)
|
||||
[2026-08-27T03:30:19.312Z] [WARN] Skipping broadcast for large file: backups/2026-08-26T1441/buglog.json (1589811 bytes)
|
||||
[2026-08-27T03:30:19.316Z] [WARN] Skipping broadcast for large file: backups/2026-08-27T0329/token-ledger.json (3431917 bytes)
|
||||
[2026-08-27T03:30:19.316Z] [WARN] Skipping broadcast for large file: backups/2026-08-27T0329/buglog.json (1600340 bytes)
|
||||
[2026-08-27T03:30:19.316Z] [INFO] Config changed — hot-reload not fully implemented yet
|
||||
[2026-08-27T03:30:19.316Z] [INFO] Cron manifest changed — restart daemon to apply
|
||||
[2026-08-27T03:30:19.319Z] [WARN] Skipping broadcast for large file: backups/2026-08-26T1442/buglog.json (1589811 bytes)
|
||||
[2026-08-27T03:30:19.319Z] [WARN] Skipping broadcast for large file: backups/2026-08-26T1442/token-ledger.json (38019592 bytes)
|
||||
[2026-08-27T03:30:19.319Z] [WARN] Skipping broadcast for large file: backups/2026-08-26T1443/token-ledger.json (3431917 bytes)
|
||||
[2026-08-27T03:30:19.319Z] [WARN] Skipping broadcast for large file: backups/2026-08-26T1443/buglog.json (1589811 bytes)
|
||||
[2026-08-27T03:30:19.328Z] [WARN] Skipping broadcast for large file: backups/2026-08-26T1444/token-ledger.json (3431917 bytes)
|
||||
[2026-08-27T03:30:19.328Z] [WARN] Skipping broadcast for large file: backups/2026-08-26T1444/buglog.json (1589811 bytes)
|
||||
[2026-08-27T03:30:19.331Z] [INFO] Config changed — hot-reload not fully implemented yet
|
||||
[2026-08-27T03:30:19.331Z] [INFO] Cron manifest changed — restart daemon to apply
|
||||
[2026-08-27T03:30:28.384Z] [INFO] WebSocket client connected
|
||||
[2026-08-27T03:31:12.624Z] [INFO] Executing task: Full anatomy rescan
|
||||
[2026-08-27T03:31:19.416Z] [INFO] Executing task: Consolidate old memory
|
||||
[2026-08-27T03:31:19.436Z] [INFO] Task Consolidate old memory completed in 16ms
|
||||
[2026-08-27T03:31:19.444Z] [INFO] Executing task: Token audit report
|
||||
[2026-08-27T03:31:23.851Z] [INFO] Task Token audit report completed in 4401ms
|
||||
[2026-08-27T03:31:23.895Z] [INFO] Executing task: Cerebrum reflection
|
||||
[2026-08-27T03:31:23.896Z] [ERROR] Task Cerebrum reflection failed (attempt 1): ai_task is no longer supported: OpenWolf makes no model calls. Remove this task from .wolf/cron-manifest.json.
|
||||
[2026-08-27T03:31:23.922Z] [INFO] Executing task: AI suggestions
|
||||
[2026-08-27T03:31:23.925Z] [ERROR] Task AI suggestions failed (attempt 1): ai_task is no longer supported: OpenWolf makes no model calls. Remove this task from .wolf/cron-manifest.json.
|
||||
[2026-08-27T03:31:24.435Z] [INFO] Task Full anatomy rescan completed in 11808ms
|
||||
[2026-08-27T03:31:24.440Z] [WARN] Skipping broadcast for large file: token-ledger.json (3109332 bytes)
|
||||
[2026-08-27T03:31:36.510Z] [INFO] Executing task: Cerebrum reflection
|
||||
[2026-08-27T03:31:36.511Z] [ERROR] Task Cerebrum reflection failed (attempt 1): ai_task is no longer supported: OpenWolf makes no model calls. Remove this task from .wolf/cron-manifest.json.
|
||||
[2026-08-27T03:31:38.592Z] [INFO] Executing task: AI suggestions
|
||||
[2026-08-27T03:31:38.593Z] [ERROR] Task AI suggestions failed (attempt 1): ai_task is no longer supported: OpenWolf makes no model calls. Remove this task from .wolf/cron-manifest.json.
|
||||
[2026-08-27T03:33:29.561Z] [WARN] Skipping broadcast for large file: buglog.json (1601790 bytes)
|
||||
[2026-08-27T03:40:01.458Z] [WARN] Skipping broadcast for large file: cache/buglog.db (3100672 bytes)
|
||||
[2026-08-27T07:00:00.012Z] [INFO] Executing task: Full anatomy rescan
|
||||
[2026-08-27T07:00:04.615Z] [INFO] Task Full anatomy rescan completed in 4599ms
|
||||
|
|
|
|||
|
|
@ -1,6 +0,0 @@
|
|||
{
|
||||
"captured_at": null,
|
||||
"captures": [],
|
||||
"total_size_kb": 0,
|
||||
"estimated_tokens": 0
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,33 +1,75 @@
|
|||
import * as path from "node:path";
|
||||
import { getWolfDir, ensureWolfDir, readJSON, writeJSON, readMarkdown, parseAnatomy, estimateTokens, readStdin, normalizePath } from "./shared.js";
|
||||
import { getWolfDir, ensureWolfDir, readJSON, writeJSON, estimateTokens, readStdin, normalizePath, getProjectDir, hookMain, getSessionFilePath } from "./shared.js";
|
||||
import { lookupEntry } from "./anatomy-store.js";
|
||||
/**
|
||||
* The PostToolUse payload carries the tool result in `tool_response`, whose
|
||||
* shape depends on the tool and harness version: a plain string, an array of
|
||||
* content blocks, or a structured object ({content} or {file:{content}}).
|
||||
* Older builds of this hook read a `tool_output` field that never existed in
|
||||
* Claude Code's payload, so read-token tracking was always zero (issue: the
|
||||
* ledger under-reported every session).
|
||||
*/
|
||||
function extractToolResponseText(resp) {
|
||||
if (typeof resp === "string")
|
||||
return resp;
|
||||
if (Array.isArray(resp)) {
|
||||
return resp
|
||||
.map((block) => (block && typeof block === "object" && typeof block.text === "string" ? block.text : ""))
|
||||
.join("");
|
||||
}
|
||||
if (resp && typeof resp === "object") {
|
||||
const obj = resp;
|
||||
if (typeof obj.content === "string")
|
||||
return obj.content;
|
||||
if (obj.file && typeof obj.file.content === "string")
|
||||
return obj.file.content;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
async function main() {
|
||||
ensureWolfDir();
|
||||
const wolfDir = getWolfDir();
|
||||
const hooksDir = path.join(wolfDir, "hooks");
|
||||
const sessionFile = path.join(hooksDir, "_session.json");
|
||||
const raw = await readStdin();
|
||||
let input;
|
||||
try {
|
||||
input = JSON.parse(raw);
|
||||
}
|
||||
catch {
|
||||
process.exit(0);
|
||||
return;
|
||||
}
|
||||
const sessionFile = getSessionFilePath(input);
|
||||
const filePath = input.tool_input?.file_path ?? input.tool_input?.path ?? "";
|
||||
const content = input.tool_output?.content ?? "";
|
||||
const content = extractToolResponseText(input.tool_response) || input.tool_output?.content || "";
|
||||
if (!filePath) {
|
||||
process.exit(0);
|
||||
return;
|
||||
}
|
||||
// Ranged reads: pre-read already recorded the contact with ranged:true.
|
||||
// Registering them as full reads here is what used to make a later
|
||||
// legitimate full read look like a duplicate (~20x warning inflation).
|
||||
if (input.tool_input?.offset !== undefined || input.tool_input?.limit !== undefined) {
|
||||
return;
|
||||
}
|
||||
const normalizedFile = normalizePath(filePath);
|
||||
// Skip tracking for .wolf/ internal files — consistent with pre-read
|
||||
const projectDir = normalizePath(process.env.CLAUDE_PROJECT_DIR || process.cwd());
|
||||
const projectDir = normalizePath(getProjectDir());
|
||||
const relToProject = normalizedFile.startsWith(projectDir)
|
||||
? normalizedFile.slice(projectDir.length).replace(/^\//, "")
|
||||
: "";
|
||||
if (relToProject.startsWith(".wolf/") || relToProject.startsWith(".wolf\\")) {
|
||||
process.exit(0);
|
||||
// 2.4: measure OpenWolf's own context cost instead of hiding it. Tagged
|
||||
// separately from project reads so anatomy hit-rates stay meaningful.
|
||||
try {
|
||||
if (content) {
|
||||
const session = readJSON(sessionFile, { files_read: {} });
|
||||
const tok = estimateTokens(content, "prose");
|
||||
session.wolf_internal_tokens = (session.wolf_internal_tokens ?? 0) + tok;
|
||||
const perFile = (session.wolf_internal_reads ?? {});
|
||||
perFile[relToProject] = (perFile[relToProject] ?? 0) + tok;
|
||||
session.wolf_internal_reads = perFile;
|
||||
writeJSON(sessionFile, session);
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
return;
|
||||
}
|
||||
const ext = path.extname(filePath).toLowerCase();
|
||||
|
|
@ -35,35 +77,26 @@ async function main() {
|
|||
const proseExts = new Set([".md", ".txt", ".rst"]);
|
||||
const type = codeExts.has(ext) ? "code" : proseExts.has(ext) ? "prose" : "mixed";
|
||||
let tokens = content ? estimateTokens(content, type) : 0;
|
||||
// Fallback: if tool_output had no content, use anatomy token estimate
|
||||
// Fallback: if tool_output had no content, use the anatomy token estimate
|
||||
if (tokens === 0) {
|
||||
const anatomyContent = readMarkdown(path.join(wolfDir, "anatomy.md"));
|
||||
const sections = parseAnatomy(anatomyContent);
|
||||
for (const [sectionKey, entries] of sections) {
|
||||
for (const entry of entries) {
|
||||
const entryRelPath = normalizePath(path.join(sectionKey, entry.file));
|
||||
if (normalizedFile.endsWith(entryRelPath) || normalizedFile.endsWith("/" + entryRelPath)) {
|
||||
tokens = entry.tokens;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (tokens > 0)
|
||||
break;
|
||||
}
|
||||
const entry = lookupEntry(wolfDir, projectDir, normalizedFile);
|
||||
if (entry)
|
||||
tokens = entry.tokens;
|
||||
}
|
||||
const session = readJSON(sessionFile, { files_read: {} });
|
||||
if (session.files_read[normalizedFile]) {
|
||||
session.files_read[normalizedFile].tokens = tokens;
|
||||
const existing = session.files_read[normalizedFile];
|
||||
if (existing && existing.ranged !== true) {
|
||||
existing.tokens = tokens;
|
||||
}
|
||||
else {
|
||||
// Fresh full read (or an upgrade of a ranged-only contact to a full read).
|
||||
session.files_read[normalizedFile] = {
|
||||
count: 1,
|
||||
tokens,
|
||||
first_read: new Date().toISOString(),
|
||||
first_read: existing?.first_read ?? new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
writeJSON(sessionFile, session);
|
||||
process.exit(0);
|
||||
}
|
||||
main().catch(() => process.exit(0));
|
||||
hookMain("post-read", main);
|
||||
//# sourceMappingURL=post-read.js.map
|
||||
|
|
@ -1,58 +1,86 @@
|
|||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import * as crypto from "node:crypto";
|
||||
import { getWolfDir, ensureWolfDir, readJSON, writeJSON, parseAnatomy, serializeAnatomy, extractDescription, estimateTokens, appendMarkdown, timeShort, readStdin, normalizePath } from "./shared.js";
|
||||
import { getWolfDir, ensureWolfDir, readJSON, writeJSON, readBugLogFile, extractDescription, estimateTokens, appendMarkdown, timeShort, readStdin, normalizePath, isSensitiveFile, getProjectDir, emitHookJSON, recordInjection, hookMain, getSessionFilePath } from "./shared.js";
|
||||
import { loadStoreReconciled, saveStore, renderToFile, sha256 } from "./anatomy-store.js";
|
||||
import { withAnatomyLock, HOOK_LOCK_BUDGET_MS } from "./anatomy-lock.js";
|
||||
import { extractSymbols, symbolsSupported, SYMBOL_MIN_TOKENS } from "./symbol-extractor.js";
|
||||
// File types where a value/string change is normal content editing, not a bug
|
||||
// fix — auto bug detection never runs on these (see autoDetectBugFix). Without
|
||||
// this, a version bump in a README or a key change in a JSON/YAML config is
|
||||
// logged as a "wrong-value" bug, since the detector matches quoted spans
|
||||
// (including markdown backticks) regardless of file type.
|
||||
const NON_CODE_EXTS = new Set([
|
||||
".md", ".mdx", ".markdown", ".txt", ".rst", ".adoc",
|
||||
".json", ".jsonc", ".yaml", ".yml", ".toml", ".ini", ".env",
|
||||
".lock", ".csv", ".tsv",
|
||||
]);
|
||||
async function main() {
|
||||
ensureWolfDir();
|
||||
const wolfDir = getWolfDir();
|
||||
const hooksDir = path.join(wolfDir, "hooks");
|
||||
const sessionFile = path.join(hooksDir, "_session.json");
|
||||
const projectRoot = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
||||
const projectRoot = getProjectDir();
|
||||
const raw = await readStdin();
|
||||
let input;
|
||||
try {
|
||||
input = JSON.parse(raw);
|
||||
}
|
||||
catch {
|
||||
process.exit(0);
|
||||
return;
|
||||
}
|
||||
const sessionFile = getSessionFilePath(input);
|
||||
const toolName = input.tool_name ?? "Write";
|
||||
const filePath = input.tool_input?.file_path ?? input.tool_input?.path ?? "";
|
||||
if (!filePath) {
|
||||
process.exit(0);
|
||||
return;
|
||||
}
|
||||
const absolutePath = path.isAbsolute(filePath) ? filePath : path.join(projectRoot, filePath);
|
||||
// Skip processing for .wolf/ internal files to avoid slow self-referential updates
|
||||
// .wolf/ state files skip anatomy/memory bookkeeping (self-referential),
|
||||
// but 2.4 adds the budget-enforcement loop the platform's native memory
|
||||
// uses: measure after every write, warn factually when a state file
|
||||
// outgrows its budget. Oversized state gets injected/read every session,
|
||||
// so bloat here is a recurring tax nothing else pushes back on.
|
||||
const relPath = normalizePath(path.relative(projectRoot, absolutePath));
|
||||
if (relPath.startsWith(".wolf/")) {
|
||||
process.exit(0);
|
||||
try {
|
||||
const budget = stateBudgetFor(wolfDir, relPath);
|
||||
if (budget !== null) {
|
||||
const content = fs.readFileSync(absolutePath, "utf-8");
|
||||
const tokens = estimateTokens(content, "prose");
|
||||
if (tokens > budget) {
|
||||
const session = readJSON(sessionFile, { files_written: [], edit_counts: {} });
|
||||
const warned = (session.budget_warned ?? {});
|
||||
if (!warned[relPath]) {
|
||||
warned[relPath] = true;
|
||||
session.budget_warned = warned;
|
||||
const warn = `OpenWolf: ${relPath} is now ~${tokens} tokens; its budget is ${budget}. It is read at session starts, so size is a recurring cost. Consolidate or move detail into topic files, keeping the most recent and most important entries.`;
|
||||
recordInjection(session, "budget_warn", warn);
|
||||
writeJSON(sessionFile, session);
|
||||
emitHookJSON("PostToolUse", { additionalContext: warn });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
return;
|
||||
}
|
||||
// Never track .env files in anatomy — they contain secrets
|
||||
// Never track files outside the project root (e.g. the Claude Code scratchpad under
|
||||
// /private/tmp). path.relative() yields ../.. section keys that pollute anatomy.md and are
|
||||
// wiped again by every full `openwolf scan`, so the index churns instead of converging.
|
||||
if (relPath.startsWith("..") || path.isAbsolute(relPath)) {
|
||||
return;
|
||||
}
|
||||
// Never track secret-bearing files in anatomy/memory (issue #54): .env is
|
||||
// not the only file whose *description* would leak sensitive content.
|
||||
const baseName = path.basename(absolutePath);
|
||||
if (baseName === ".env" || baseName.startsWith(".env.")) {
|
||||
process.exit(0);
|
||||
if (isSensitiveFile(baseName)) {
|
||||
return;
|
||||
}
|
||||
const oldStr = input.tool_input?.old_string ?? "";
|
||||
const newStr = input.tool_input?.new_string ?? "";
|
||||
// 1. Update anatomy.md
|
||||
// 1. Update the anatomy store, then re-render anatomy.md from it.
|
||||
// All of this happens under the anatomy lock; if the lock cannot be
|
||||
// acquired within budget we skip — a later writer converges the state.
|
||||
try {
|
||||
const anatomyPath = path.join(wolfDir, "anatomy.md");
|
||||
let anatomyContent;
|
||||
try {
|
||||
anatomyContent = fs.readFileSync(anatomyPath, "utf-8");
|
||||
}
|
||||
catch {
|
||||
anatomyContent = "# anatomy.md\n\n> Auto-maintained by OpenWolf.\n";
|
||||
}
|
||||
const sections = parseAnatomy(anatomyContent);
|
||||
const relPathLocal = normalizePath(path.relative(projectRoot, absolutePath));
|
||||
const dir = path.dirname(relPathLocal);
|
||||
const fileName = path.basename(relPathLocal);
|
||||
const sectionKey = dir === "." ? "./" : dir + "/";
|
||||
let fileContent = "";
|
||||
try {
|
||||
fileContent = fs.readFileSync(absolutePath, "utf-8");
|
||||
|
|
@ -66,40 +94,35 @@ async function main() {
|
|||
const proseExts = new Set([".md", ".txt", ".rst"]);
|
||||
const type = codeExts.has(ext) ? "code" : proseExts.has(ext) ? "prose" : "mixed";
|
||||
const tokens = estimateTokens(fileContent, type);
|
||||
if (!sections.has(sectionKey))
|
||||
sections.set(sectionKey, []);
|
||||
const entries = sections.get(sectionKey);
|
||||
const idx = entries.findIndex((e) => e.file === fileName);
|
||||
if (idx !== -1) {
|
||||
entries[idx] = { file: fileName, description: desc, tokens };
|
||||
}
|
||||
else {
|
||||
entries.push({ file: fileName, description: desc, tokens });
|
||||
}
|
||||
let fileCount = 0;
|
||||
for (const [, list] of sections)
|
||||
fileCount += list.length;
|
||||
const serialized = serializeAnatomy(sections, {
|
||||
lastScanned: new Date().toISOString(),
|
||||
fileCount,
|
||||
hits: 0,
|
||||
misses: 0,
|
||||
});
|
||||
const tmp = anatomyPath + "." + crypto.randomBytes(4).toString("hex") + ".tmp";
|
||||
let size;
|
||||
let mtimeMs;
|
||||
try {
|
||||
fs.writeFileSync(tmp, serialized, "utf-8");
|
||||
fs.renameSync(tmp, anatomyPath);
|
||||
}
|
||||
catch {
|
||||
try {
|
||||
fs.writeFileSync(anatomyPath, serialized, "utf-8");
|
||||
}
|
||||
catch { }
|
||||
try {
|
||||
fs.unlinkSync(tmp);
|
||||
}
|
||||
catch { }
|
||||
const st = fs.statSync(absolutePath);
|
||||
size = st.size;
|
||||
mtimeMs = st.mtimeMs;
|
||||
}
|
||||
catch { }
|
||||
// Symbols are recomputed on every write (never carried over — the
|
||||
// content just changed, so old line ranges would misdirect slice reads).
|
||||
const symbols = tokens >= SYMBOL_MIN_TOKENS && symbolsSupported(ext)
|
||||
? extractSymbols(fileContent, ext)
|
||||
: undefined;
|
||||
withAnatomyLock(wolfDir, HOOK_LOCK_BUDGET_MS, () => {
|
||||
const store = loadStoreReconciled(wolfDir, projectRoot);
|
||||
store.files[relPathLocal] = {
|
||||
description: desc,
|
||||
tokens,
|
||||
hash: sha256(fileContent).slice(0, 16),
|
||||
size,
|
||||
mtimeMs,
|
||||
updatedAt: new Date().toISOString(),
|
||||
source: "hook",
|
||||
symbols: symbols && symbols.length > 0 ? symbols : undefined,
|
||||
};
|
||||
store.meta.lastScanned = new Date().toISOString();
|
||||
renderToFile(wolfDir, store);
|
||||
saveStore(wolfDir, store);
|
||||
});
|
||||
}
|
||||
catch { }
|
||||
// 2. Append richer entry to memory.md
|
||||
|
|
@ -137,9 +160,23 @@ async function main() {
|
|||
});
|
||||
const editKey = normalizePath(path.relative(projectRoot, absolutePath));
|
||||
session.edit_counts[editKey] = (session.edit_counts[editKey] || 0) + 1;
|
||||
if (session.files_read && session.files_read[normalizedFile]) {
|
||||
delete session.files_read[normalizedFile];
|
||||
}
|
||||
// Once per file per session: firing on the 3rd edit AND every edit after
|
||||
// it would hit ~39% of all write operations (measured) — pure noise.
|
||||
if (!session.edit_warned)
|
||||
session.edit_warned = {};
|
||||
const editWarn = session.edit_counts[editKey] >= 3 && !session.edit_warned[editKey]
|
||||
? `OpenWolf: ${baseName} has been edited ${session.edit_counts[editKey]} times this session. If you're fixing a bug, log it to .wolf/buglog.json.`
|
||||
: "";
|
||||
if (editWarn)
|
||||
session.edit_warned[editKey] = true;
|
||||
if (editWarn)
|
||||
recordInjection(session, "edit_warn", editWarn);
|
||||
writeJSON(sessionFile, session);
|
||||
if (session.edit_counts[editKey] >= 3) {
|
||||
process.stderr.write(`⚠️ OpenWolf: ${baseName} has been edited ${session.edit_counts[editKey]} times this session. If you're fixing a bug, remember to log it to .wolf/buglog.json.\n`);
|
||||
if (editWarn) {
|
||||
emitHookJSON("PostToolUse", { additionalContext: editWarn });
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
|
|
@ -150,7 +187,16 @@ async function main() {
|
|||
}
|
||||
}
|
||||
catch { }
|
||||
process.exit(0);
|
||||
}
|
||||
/** Token budget for a .wolf state file, or null when unbudgeted (2.4). */
|
||||
function stateBudgetFor(wolfDir, relPath) {
|
||||
const defaults = {
|
||||
".wolf/cerebrum.md": 2000,
|
||||
".wolf/STATUS.md": 1000,
|
||||
};
|
||||
const cfg = readJSON(path.join(wolfDir, "config.json"), {});
|
||||
const merged = { ...defaults, ...(cfg.openwolf?.context?.state_budgets ?? {}) };
|
||||
return merged[relPath] ?? null;
|
||||
}
|
||||
// ─── Edit Summarizer ─────────────────────────────────────────────
|
||||
function summarizeEdit(oldStr, newStr, filename) {
|
||||
|
|
@ -160,7 +206,7 @@ function summarizeEdit(oldStr, newStr, filename) {
|
|||
const newCount = newLines.length;
|
||||
const ext = path.extname(filename).toLowerCase();
|
||||
// --- Structural fixes ---
|
||||
if (newStr.includes("try") && newStr.includes("catch") && !oldStr.includes("catch")) {
|
||||
if (/\btry\b/.test(newStr) && hasCatchConstruct(newStr, ext) && !hasCatchConstruct(oldStr, ext)) {
|
||||
return "added error handling";
|
||||
}
|
||||
if (newStr.includes("?.") && !oldStr.includes("?."))
|
||||
|
|
@ -241,14 +287,30 @@ function extractCalls(code) {
|
|||
.filter(n => n.length > 2 && !["if", "for", "while", "switch", "catch", "function", "return", "new", "typeof", "instanceof", "const", "let", "var"].includes(n)))];
|
||||
}
|
||||
// ─── Auto Bug Detection ──────────────────────────────────────────
|
||||
function bugAutoDetectEnabled(wolfDir) {
|
||||
try {
|
||||
const cfg = readJSON(path.join(wolfDir, "config.json"), {});
|
||||
// Default on; only an explicit `false` disables auto bug detection.
|
||||
return cfg.openwolf?.buglog?.auto_detect !== false;
|
||||
}
|
||||
catch {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
function autoDetectBugFix(wolfDir, absolutePath, projectRoot, oldStr, newStr) {
|
||||
const bugLogPath = path.join(wolfDir, "buglog.json");
|
||||
const bugLog = readJSON(bugLogPath, { version: 1, bugs: [] });
|
||||
const relFile = normalizePath(path.relative(projectRoot, absolutePath));
|
||||
const basename = path.basename(absolutePath);
|
||||
const ext = path.extname(basename).toLowerCase();
|
||||
// Bug-fix detection is a code concept — never fire on prose/docs/data files.
|
||||
if (NON_CODE_EXTS.has(ext))
|
||||
return;
|
||||
// Respect an explicit opt-out in .wolf/config.json (default: enabled).
|
||||
if (!bugAutoDetectEnabled(wolfDir))
|
||||
return;
|
||||
const bugLogPath = path.join(wolfDir, "buglog.json");
|
||||
const bugLog = readBugLogFile(wolfDir);
|
||||
const relFile = normalizePath(path.relative(projectRoot, absolutePath));
|
||||
// Detect what kind of fix this is
|
||||
const detection = detectFixPattern(oldStr, newStr, ext);
|
||||
const detection = detectFixPattern(oldStr, newStr, ext, basename);
|
||||
if (!detection)
|
||||
return;
|
||||
// Check for recent duplicate (same file + same category within 5 min)
|
||||
|
|
@ -287,15 +349,16 @@ function autoDetectBugFix(wolfDir, absolutePath, projectRoot, oldStr, newStr) {
|
|||
});
|
||||
writeJSON(bugLogPath, bugLog);
|
||||
}
|
||||
function detectFixPattern(oldStr, newStr, ext) {
|
||||
function detectFixPattern(oldStr, newStr, ext, basename) {
|
||||
const oldLines = oldStr.split("\n");
|
||||
const newLines = newStr.split("\n");
|
||||
const isTest = isTestFile(basename);
|
||||
// --- Error handling added ---
|
||||
if (newStr.includes("catch") && !oldStr.includes("catch")) {
|
||||
const fn = newStr.match(/(?:function|def|async)\s+(\w+)/)?.[1] || "unknown";
|
||||
if (!isTest && hasCatchConstruct(newStr, ext) && !hasCatchConstruct(oldStr, ext)) {
|
||||
const fn = newStr.match(/(?:function|def|async)\s+(\w+)/)?.[1];
|
||||
return {
|
||||
category: "error-handling",
|
||||
summary: `Missing error handling in ${path.basename(fn)}`,
|
||||
summary: `Missing error handling in ${fn ? `${fn}()` : basename}`,
|
||||
rootCause: "Code path had no error handling — exceptions would propagate uncaught",
|
||||
fix: `Added try/catch block`,
|
||||
context: extractChangedLines(oldStr, newStr),
|
||||
|
|
@ -307,14 +370,15 @@ function detectFixPattern(oldStr, newStr, ext) {
|
|||
(/!==?\s*(null|undefined)/.test(newStr) && !/!==?\s*(null|undefined)/.test(oldStr))) {
|
||||
return {
|
||||
category: "null-safety",
|
||||
summary: `Null/undefined access in ${path.basename(path.basename(""))}`,
|
||||
summary: `Null/undefined access in ${basename}`,
|
||||
rootCause: "Property access on potentially null/undefined value",
|
||||
fix: `Added null safety (optional chaining or null check)`,
|
||||
context: extractChangedLines(oldStr, newStr),
|
||||
};
|
||||
}
|
||||
// --- Guard clause / early return added ---
|
||||
if (/if\s*\([^)]*\)\s*(return|throw|continue|break)/.test(newStr) &&
|
||||
if (!isTest &&
|
||||
/if\s*\([^)]*\)\s*(return|throw|continue|break)/.test(newStr) &&
|
||||
!/if\s*\([^)]*\)\s*(return|throw|continue|break)/.test(oldStr)) {
|
||||
const condition = newStr.match(/if\s*\(([^)]+)\)/)?.[1]?.trim().slice(0, 60) || "condition";
|
||||
return {
|
||||
|
|
@ -460,7 +524,7 @@ function detectFixPattern(oldStr, newStr, ext) {
|
|||
if (removedLines.length >= 2) {
|
||||
return {
|
||||
category: "refactor",
|
||||
summary: `Significant refactor of ${path.basename("")}`,
|
||||
summary: `Significant refactor of ${basename}`,
|
||||
rootCause: `${removedLines.length} lines replaced/restructured`,
|
||||
fix: `Rewrote ${oldLines.length}→${newLines.length} lines (${removedLines.length} removed)`,
|
||||
context: removedLines.slice(0, 2).map(l => l.trim().slice(0, 50)).join("; "),
|
||||
|
|
@ -469,6 +533,15 @@ function detectFixPattern(oldStr, newStr, ext) {
|
|||
}
|
||||
return null;
|
||||
}
|
||||
function hasCatchConstruct(code, ext) {
|
||||
if (ext === ".py")
|
||||
return /\bexcept\b[^\n]*:/.test(code);
|
||||
return /\bcatch\s*[({]/.test(code);
|
||||
}
|
||||
function isTestFile(basename) {
|
||||
return /(\.test\.|\.spec\.|_test\.|_spec\.)/i.test(basename) ||
|
||||
/(Test|Tests|IT|Spec)\.\w+$/.test(basename);
|
||||
}
|
||||
function extractChangedLines(oldStr, newStr) {
|
||||
const oldLines = new Set(oldStr.split("\n").map(l => l.trim()).filter(Boolean));
|
||||
const newLines = newStr.split("\n").map(l => l.trim()).filter(Boolean);
|
||||
|
|
@ -499,5 +572,5 @@ function extractCSSProps(code) {
|
|||
}
|
||||
return props;
|
||||
}
|
||||
main().catch(() => process.exit(0));
|
||||
hookMain("post-write", main);
|
||||
//# sourceMappingURL=post-write.js.map
|
||||
|
|
@ -1,65 +1,181 @@
|
|||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import { getWolfDir, ensureWolfDir, readJSON, writeJSON, readMarkdown, parseAnatomy, readStdin, normalizePath } from "./shared.js";
|
||||
import { getWolfDir, ensureWolfDir, readJSON, writeJSON, readStdin, normalizePath, getProjectDir, emitHookJSON, recordInjection, hookMain, getSessionFilePath } from "./shared.js";
|
||||
import { lookupEntry } from "./anatomy-store.js";
|
||||
function duplicateMode(wolfDir) {
|
||||
const cfg = readJSON(path.join(wolfDir, "config.json"), {});
|
||||
const mode = cfg.openwolf?.reads?.duplicate_mode;
|
||||
return mode === "deny" || mode === "off" ? mode : "warn";
|
||||
}
|
||||
function skeletonHintsEnabled(wolfDir) {
|
||||
const cfg = readJSON(path.join(wolfDir, "config.json"), {});
|
||||
return cfg.openwolf?.reads?.skeleton_hints !== false;
|
||||
}
|
||||
async function main() {
|
||||
ensureWolfDir();
|
||||
const wolfDir = getWolfDir();
|
||||
const hooksDir = path.join(wolfDir, "hooks");
|
||||
const sessionFile = path.join(hooksDir, "_session.json");
|
||||
const raw = await readStdin();
|
||||
let input;
|
||||
try {
|
||||
input = JSON.parse(raw);
|
||||
}
|
||||
catch {
|
||||
process.exit(0);
|
||||
return;
|
||||
}
|
||||
const sessionFile = getSessionFilePath(input);
|
||||
const filePath = input.tool_input?.file_path ?? input.tool_input?.path ?? "";
|
||||
if (!filePath) {
|
||||
process.exit(0);
|
||||
return;
|
||||
}
|
||||
// Ranged reads (offset/limit) are exactly what the symbol hints steer the
|
||||
// model toward — never warn about, deny, or record them as full reads (a
|
||||
// ranged first contact must not make a later legitimate full read look like
|
||||
// a duplicate).
|
||||
const isRangedRead = input.tool_input?.offset !== undefined || input.tool_input?.limit !== undefined;
|
||||
// Subagents share this session file but not the main thread's context: a
|
||||
// file the main thread read is unseen by a fresh subagent, so duplicate
|
||||
// handling stays hands-off for them.
|
||||
const isSubagent = typeof input.agent_id === "string" && input.agent_id.length > 0;
|
||||
const normalizedFile = normalizePath(filePath);
|
||||
// Skip tracking for .wolf/ internal files — they're infrastructure, not project files.
|
||||
// Counting them inflates anatomy miss rates since .wolf/ is excluded from anatomy scanning.
|
||||
const projectDir = normalizePath(process.env.CLAUDE_PROJECT_DIR || process.cwd());
|
||||
const projectDir = normalizePath(getProjectDir());
|
||||
const relToProject = normalizedFile.startsWith(projectDir)
|
||||
? normalizedFile.slice(projectDir.length).replace(/^\//, "")
|
||||
: "";
|
||||
if (relToProject.startsWith(".wolf/") || relToProject.startsWith(".wolf\\")) {
|
||||
process.exit(0);
|
||||
// 2.4: .wolf reads used to be invisible to OpenWolf itself (~147k tokens
|
||||
// of unmeasured self-inflicted reads, including whole-file reads of the
|
||||
// two files the rules say never to read whole). Measure them (tagged
|
||||
// separately so anatomy metrics stay clean) and steer the expensive
|
||||
// pattern to the cheap one, once per file per session.
|
||||
try {
|
||||
const base = path.basename(normalizedFile);
|
||||
const isRanged = input.tool_input?.offset !== undefined || input.tool_input?.limit !== undefined;
|
||||
if (!isRanged && (base === "anatomy.md" || base === "cerebrum.md" || base === "memory.md")) {
|
||||
let sizeTokens = 0;
|
||||
try {
|
||||
sizeTokens = Math.ceil(fs.statSync(filePath).size / 3.5);
|
||||
}
|
||||
catch { }
|
||||
if (sizeTokens > 1500) {
|
||||
const session = readJSON(sessionFile, {
|
||||
session_id: "", files_read: {}, anatomy_hits: 0, anatomy_misses: 0, repeated_reads_warned: 0,
|
||||
});
|
||||
const warned = (session.wolf_read_advised ?? {});
|
||||
if (!warned[base]) {
|
||||
warned[base] = true;
|
||||
session.wolf_read_advised = warned;
|
||||
const note = base === "anatomy.md"
|
||||
? `OpenWolf: ${base} is ~${sizeTokens} tokens and is an index, not a document. \`openwolf find <symbol or path>\` answers location queries in under 1k tokens; grep it for a single path's line otherwise.`
|
||||
: `OpenWolf: ${base} is ~${sizeTokens} tokens. Grep the section you need (for example "## Do-Not-Repeat") or read with offset/limit; the whole file rarely pays for itself.`;
|
||||
recordInjection(session, "wolf_read_advice", note);
|
||||
writeJSON(sessionFile, session);
|
||||
emitHookJSON("PreToolUse", { additionalContext: note });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
return;
|
||||
}
|
||||
const session = readJSON(sessionFile, {
|
||||
session_id: "", files_read: {}, anatomy_hits: 0, anatomy_misses: 0,
|
||||
repeated_reads_warned: 0,
|
||||
});
|
||||
// Check if already read this session
|
||||
if (session.files_read[normalizedFile]) {
|
||||
const prev = session.files_read[normalizedFile];
|
||||
process.stderr.write(`⚡ OpenWolf: ${path.basename(normalizedFile)} was already read this session (~${prev.tokens} tokens). Consider using your existing knowledge of this file.\n`);
|
||||
session.files_read[normalizedFile].count++;
|
||||
session.repeated_reads_warned++;
|
||||
writeJSON(sessionFile, session);
|
||||
process.exit(0);
|
||||
if (isRangedRead) {
|
||||
// Record the contact so dedupe knows about it, but flagged: a ranged
|
||||
// first contact must never make a later full read look like a duplicate
|
||||
// (the old asymmetry with post-read inflated warnings ~20x).
|
||||
if (!session.files_read[normalizedFile]) {
|
||||
session.files_read[normalizedFile] = {
|
||||
count: 1, tokens: 0, first_read: new Date().toISOString(), ranged: true,
|
||||
};
|
||||
writeJSON(sessionFile, session);
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Check anatomy.md for this file
|
||||
const anatomyContent = readMarkdown(path.join(wolfDir, "anatomy.md"));
|
||||
const sections = parseAnatomy(anatomyContent);
|
||||
let found = false;
|
||||
for (const [sectionKey, entries] of sections) {
|
||||
for (const entry of entries) {
|
||||
// Build the full relative path from the section key + filename for accurate matching
|
||||
const entryRelPath = normalizePath(path.join(sectionKey, entry.file));
|
||||
if (normalizedFile.endsWith(entryRelPath) || normalizedFile.endsWith("/" + entryRelPath)) {
|
||||
process.stderr.write(`📋 OpenWolf anatomy: ${entry.file} — ${entry.description} (~${entry.tokens} tok)\n`);
|
||||
found = true;
|
||||
break;
|
||||
// Model-visible notes for this run — flushed as ONE additionalContext at exit.
|
||||
const notes = [];
|
||||
// Check if already read this session. Only a prior FULL read counts —
|
||||
// ranged-only contact means the model has never seen the whole file.
|
||||
if (session.files_read[normalizedFile] && session.files_read[normalizedFile].ranged !== true) {
|
||||
const prev = session.files_read[normalizedFile];
|
||||
let modifiedSinceRead = true;
|
||||
try {
|
||||
const mtime = fs.statSync(filePath).mtimeMs;
|
||||
modifiedSinceRead = prev.read_mtime === undefined || mtime > prev.read_mtime;
|
||||
}
|
||||
catch { }
|
||||
if (!modifiedSinceRead) {
|
||||
const mode = duplicateMode(wolfDir);
|
||||
session.repeated_reads_warned++;
|
||||
// Denial is the only path that actually saves the tokens, but it must
|
||||
// never strand the model: full reads only (ranged reads exited above),
|
||||
// main thread only, only when the earlier read verifiably delivered
|
||||
// content (tokens > 0), never after a compaction evicted that content,
|
||||
// and only ONCE per file per session — the next attempt passes through.
|
||||
const denyEligible = mode === "deny" && !isSubagent && prev.tokens > 0 &&
|
||||
prev.denied_once !== true && prev.compacted !== true;
|
||||
if (denyEligible) {
|
||||
prev.denied_once = true;
|
||||
session.reads_denied = (session.reads_denied ?? 0) + 1;
|
||||
session.denied_tokens_saved = (session.denied_tokens_saved ?? 0) + prev.tokens;
|
||||
const reason = `OpenWolf: ${path.basename(normalizedFile)} was already read this session (~${prev.tokens} tok) and is unchanged on disk. Reuse your earlier read, or use offset/limit for the exact lines you need. If you do need the full file again, a second attempt will pass through.`;
|
||||
recordInjection(session, "dup_deny", reason);
|
||||
writeJSON(sessionFile, session);
|
||||
emitHookJSON("PreToolUse", {
|
||||
permissionDecision: "deny",
|
||||
permissionDecisionReason: reason,
|
||||
});
|
||||
return;
|
||||
}
|
||||
prev.count++;
|
||||
if (mode !== "off") {
|
||||
notes.push(`OpenWolf: ${path.basename(normalizedFile)} was already read this session (~${prev.tokens} tok), unchanged since. If you only need the gist, your earlier read may suffice; for exact text (edit anchors, line numbers), the re-read is fine.`);
|
||||
}
|
||||
if (notes.length > 0)
|
||||
recordInjection(session, "dup_warn", notes.join("\n"));
|
||||
writeJSON(sessionFile, session);
|
||||
if (notes.length > 0) {
|
||||
emitHookJSON("PreToolUse", { additionalContext: notes.join("\n") });
|
||||
}
|
||||
return;
|
||||
}
|
||||
delete session.files_read[normalizedFile];
|
||||
}
|
||||
// Anatomy lookup: O(1) against the durable store, legacy md scan fallback.
|
||||
const entry = lookupEntry(wolfDir, projectDir, normalizedFile);
|
||||
const found = entry !== null;
|
||||
if (entry) {
|
||||
if (entry.description) {
|
||||
notes.push(`OpenWolf anatomy: ${entry.file}: ${entry.description} (~${entry.tokens} tok)`);
|
||||
}
|
||||
// Symbol hint (F2b Phase B): point at slices of big files. Suppressed if
|
||||
// the on-disk file no longer matches what was indexed — a stale line
|
||||
// range that misdirects an offset read is worse than no hint at all.
|
||||
if (entry.symbols && entry.symbols.length > 0) {
|
||||
let fresh = false;
|
||||
try {
|
||||
const st = fs.statSync(filePath);
|
||||
fresh = (entry.size === undefined || st.size === entry.size) &&
|
||||
(entry.mtimeMs === undefined || Math.abs(st.mtimeMs - entry.mtimeMs) < 1);
|
||||
}
|
||||
catch { }
|
||||
if (fresh) {
|
||||
// J2 skeleton hint: for big files with an indexed signature outline,
|
||||
// the outline replaces the "largest sections" line — often enough to
|
||||
// skip the full read entirely. Config-gated (reads.skeleton_hints).
|
||||
if (entry.skeleton && entry.tokens > 2000 && skeletonHintsEnabled(wolfDir)) {
|
||||
notes.push(`Signature outline of ${entry.file} (~${entry.tokens} tok full; symbols with line ranges follow):\n${entry.skeleton}\nRead with offset/limit to fetch just the part you need.`);
|
||||
}
|
||||
else {
|
||||
const top = [...entry.symbols].sort((a, b) => b.tokens - a.tokens).slice(0, 5);
|
||||
const list = top.map((s) => `${s.kind} ${s.name} L${s.startLine}-${s.endLine} ~${s.tokens} tok`).join("; ");
|
||||
notes.push(`Largest sections: ${list}. Read with offset/limit to fetch just the part you need.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (found)
|
||||
break;
|
||||
}
|
||||
if (found) {
|
||||
session.anatomy_hits++;
|
||||
|
|
@ -68,13 +184,24 @@ async function main() {
|
|||
session.anatomy_misses++;
|
||||
}
|
||||
// Record initial read entry (tokens will be updated in post-read)
|
||||
let readMtime;
|
||||
try {
|
||||
readMtime = fs.statSync(filePath).mtimeMs;
|
||||
}
|
||||
catch { }
|
||||
session.files_read[normalizedFile] = {
|
||||
count: 1,
|
||||
tokens: 0,
|
||||
first_read: new Date().toISOString(),
|
||||
read_mtime: readMtime,
|
||||
anatomy_hit: found,
|
||||
};
|
||||
if (notes.length > 0)
|
||||
recordInjection(session, "anatomy_hint", notes.join("\n"));
|
||||
writeJSON(sessionFile, session);
|
||||
process.exit(0);
|
||||
if (notes.length > 0) {
|
||||
emitHookJSON("PreToolUse", { additionalContext: notes.join("\n") });
|
||||
}
|
||||
}
|
||||
main().catch(() => process.exit(0));
|
||||
hookMain("pre-read", main);
|
||||
//# sourceMappingURL=pre-read.js.map
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import { getWolfDir, ensureWolfDir, readJSON, readMarkdown, readStdin } from "./shared.js";
|
||||
import { getWolfDir, ensureWolfDir, readBugLogFile, readMarkdown, readStdin, emitHookJSON, recordInjectionToSessionFile, hookMain, getSessionFilePath } from "./shared.js";
|
||||
import { searchBugsFTS } from "./bug-index.js";
|
||||
async function main() {
|
||||
ensureWolfDir();
|
||||
const wolfDir = getWolfDir();
|
||||
|
|
@ -10,7 +11,6 @@ async function main() {
|
|||
input = JSON.parse(raw);
|
||||
}
|
||||
catch {
|
||||
process.exit(0);
|
||||
return;
|
||||
}
|
||||
// For Edit tool, the meaningful content is old_string + new_string
|
||||
|
|
@ -20,26 +20,31 @@ async function main() {
|
|||
const filePath = input.tool_input?.file_path ?? input.tool_input?.path ?? "";
|
||||
const allContent = [content, oldStr, newStr].join("\n");
|
||||
if (!allContent.trim()) {
|
||||
process.exit(0);
|
||||
return;
|
||||
}
|
||||
// Model-visible notes — flushed as ONE additionalContext at exit.
|
||||
const notes = [];
|
||||
// 1. Cerebrum Do-Not-Repeat check
|
||||
checkCerebrum(wolfDir, allContent);
|
||||
notes.push(...checkCerebrum(wolfDir, allContent));
|
||||
// 2. Bug log: search for similar past bugs when editing code
|
||||
// This fires when Claude is about to edit a file — if the edit looks like a fix
|
||||
// (changing error handling, modifying catch blocks, etc.), check the bug log
|
||||
if (filePath && (oldStr || content)) {
|
||||
checkBugLog(wolfDir, filePath, oldStr, newStr, content);
|
||||
notes.push(...checkBugLog(wolfDir, filePath, oldStr, newStr, content));
|
||||
}
|
||||
if (notes.length > 0) {
|
||||
recordInjectionToSessionFile(getSessionFilePath(input), "cerebrum_buglog", notes.join("\n"));
|
||||
emitHookJSON("PreToolUse", { additionalContext: notes.join("\n") });
|
||||
}
|
||||
process.exit(0);
|
||||
}
|
||||
function checkCerebrum(wolfDir, content) {
|
||||
const cerebrumContent = readMarkdown(path.join(wolfDir, "cerebrum.md"));
|
||||
const doNotRepeatSection = cerebrumContent.split("## Do-Not-Repeat")[1];
|
||||
if (!doNotRepeatSection)
|
||||
return;
|
||||
return [];
|
||||
const entries = doNotRepeatSection.split("## ")[0];
|
||||
const lines = entries.split("\n").filter((l) => l.trim().startsWith("[") || l.trim().startsWith("-"));
|
||||
const warnings = [];
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim().replace(/^[-*]\s*/, "").replace(/^\[[\d-]+\]\s*/, "");
|
||||
if (!trimmed)
|
||||
|
|
@ -58,12 +63,14 @@ function checkCerebrum(wolfDir, content) {
|
|||
try {
|
||||
const regex = new RegExp(`\\b${pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`, "i");
|
||||
if (regex.test(content)) {
|
||||
process.stderr.write(`⚠️ OpenWolf cerebrum warning: "${trimmed}" — check your code before proceeding.\n`);
|
||||
warnings.push(`OpenWolf cerebrum warning: "${trimmed}". Check this edit against that rule before proceeding.`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
return warnings.slice(0, 3);
|
||||
}
|
||||
// Common words that appear in most code — must be excluded from similarity matching
|
||||
const STOP_WORDS = new Set([
|
||||
|
|
@ -77,45 +84,64 @@ const STOP_WORDS = new Set([
|
|||
function checkBugLog(wolfDir, filePath, oldStr, newStr, content) {
|
||||
const bugLogPath = path.join(wolfDir, "buglog.json");
|
||||
if (!fs.existsSync(bugLogPath))
|
||||
return;
|
||||
const bugLog = readJSON(bugLogPath, { version: 1, bugs: [] });
|
||||
return [];
|
||||
const bugLog = readBugLogFile(wolfDir);
|
||||
if (bugLog.bugs.length === 0)
|
||||
return;
|
||||
return [];
|
||||
const basename = path.basename(filePath);
|
||||
// ONLY surface bugs that match the SAME file being edited.
|
||||
// Cross-file matching is too noisy and risks misdirecting Claude.
|
||||
const fileMatches = bugLog.bugs.filter(b => {
|
||||
const bugBasename = path.basename(b.file);
|
||||
return bugBasename === basename;
|
||||
});
|
||||
if (fileMatches.length === 0)
|
||||
return;
|
||||
// Further filter: require tag or error_message overlap with the edit content
|
||||
const editText = (oldStr + " " + newStr + " " + content).toLowerCase();
|
||||
const editTokens = tokenize(editText);
|
||||
const relevant = fileMatches.filter(bug => {
|
||||
// Check if any bug tag appears in the edit content
|
||||
const tagHit = bug.tags.some(t => editText.includes(t.toLowerCase()));
|
||||
if (tagHit)
|
||||
return true;
|
||||
// Check meaningful word overlap (excluding stop words)
|
||||
const bugTokens = tokenize(bug.error_message + " " + bug.root_cause);
|
||||
const overlap = [...editTokens].filter(t => bugTokens.has(t));
|
||||
// Require at least 3 meaningful overlapping words
|
||||
return overlap.length >= 3;
|
||||
});
|
||||
if (relevant.length === 0)
|
||||
return;
|
||||
// Surface as a FYI, not a directive — Claude should evaluate, not blindly apply
|
||||
process.stderr.write(`📋 OpenWolf buglog: ${relevant.length} past bug(s) found for ${basename} — review for context, do NOT apply blindly:\n`);
|
||||
for (const bug of relevant.slice(0, 2)) {
|
||||
process.stderr.write(` [${bug.id}] "${bug.error_message.slice(0, 70)}"\n Cause: ${bug.root_cause.slice(0, 80)}\n Fix: ${bug.fix.slice(0, 80)}\n`);
|
||||
// J4: signature-keyed FTS retrieval first — finds relevant fixes across
|
||||
// files, ranked by relevance, not just same-basename matches. Falls back to
|
||||
// the legacy basename + overlap filter when node:sqlite is unavailable.
|
||||
let relevant = null;
|
||||
const ftsHits = searchBugsFTS(wolfDir, `${basename} ${editText.slice(0, 600)}`, 4);
|
||||
if (ftsHits !== null) {
|
||||
// Precision gate on the recall-oriented OR-query: keep same-file hits, and
|
||||
// cross-file hits only with a tag or 3-word overlap with the edit.
|
||||
const editTokens = tokenize(editText);
|
||||
relevant = ftsHits.filter((bug) => {
|
||||
if (path.basename(bug.file ?? "") === basename)
|
||||
return true;
|
||||
const tagHit = (bug.tags ?? []).some((t) => editText.includes(t.toLowerCase()));
|
||||
if (tagHit)
|
||||
return true;
|
||||
const bugTokens = tokenize(bug.error_message + " " + bug.root_cause);
|
||||
return [...editTokens].filter((t) => bugTokens.has(t)).length >= 3;
|
||||
});
|
||||
}
|
||||
if (relevant === null) {
|
||||
// Legacy path: ONLY surface bugs recorded against the SAME file.
|
||||
const fileMatches = bugLog.bugs.filter(b => {
|
||||
const bugBasename = path.basename(b.file);
|
||||
return bugBasename === basename;
|
||||
});
|
||||
if (fileMatches.length === 0)
|
||||
return [];
|
||||
const editTokens = tokenize(editText);
|
||||
relevant = fileMatches.filter(bug => {
|
||||
const tagHit = bug.tags.some(t => editText.includes(t.toLowerCase()));
|
||||
if (tagHit)
|
||||
return true;
|
||||
const bugTokens = tokenize(bug.error_message + " " + bug.root_cause);
|
||||
const overlap = [...editTokens].filter(t => bugTokens.has(t));
|
||||
return overlap.length >= 3;
|
||||
});
|
||||
}
|
||||
if (relevant.length === 0)
|
||||
return [];
|
||||
// Surface as a FYI, not a directive — Claude should evaluate, not blindly apply
|
||||
const lines = [
|
||||
`OpenWolf buglog: ${relevant.length} past bug(s) recorded for ${basename}. Review for context; do NOT apply blindly:`,
|
||||
];
|
||||
for (const bug of relevant.slice(0, 2)) {
|
||||
lines.push(`[${bug.id}] "${bug.error_message.slice(0, 70)}" | Cause: ${bug.root_cause.slice(0, 80)} | Fix: ${bug.fix.slice(0, 80)}`);
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
function tokenize(text) {
|
||||
return new Set(text.replace(/[^\w\s]/g, " ").split(/\s+/)
|
||||
.filter(w => w.length > 3 && !STOP_WORDS.has(w.toLowerCase()))
|
||||
.map(w => w.toLowerCase()));
|
||||
}
|
||||
main().catch(() => process.exit(0));
|
||||
hookMain("pre-write", main);
|
||||
//# sourceMappingURL=pre-write.js.map
|
||||
|
|
@ -1,6 +1,168 @@
|
|||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import { getWolfDir, ensureWolfDir, writeJSON, appendMarkdown, readJSON, timestamp, timeShort } from "./shared.js";
|
||||
import { getWolfDir, ensureWolfDir, writeJSON, appendMarkdown, readJSON, readBugLogFile, timestamp, timeShort, estimateTokens, readStdin, detectAgent, recordInjectionToSessionFile, getProjectDir, hookMain, getSessionFilePath, gcSessionFiles } from "./shared.js";
|
||||
import { topRules, scopedRulesForFiles } from "./rule-reinjection.js";
|
||||
function digestBudget(wolfDir) {
|
||||
const cfg = readJSON(path.join(wolfDir, "config.json"), {});
|
||||
const ctx = cfg.openwolf?.context ?? {};
|
||||
const agent = detectAgent();
|
||||
return ctx.budgets?.[agent] ?? ctx.session_digest_budget_tokens ?? 1500;
|
||||
}
|
||||
/** Extract one `## heading` section (heading line through the next ## or ---). */
|
||||
function extractSection(markdown, headingPattern) {
|
||||
const lines = markdown.split(/\r?\n/);
|
||||
const start = lines.findIndex((l) => headingPattern.test(l));
|
||||
if (start === -1)
|
||||
return "";
|
||||
const out = [lines[start]];
|
||||
for (let i = start + 1; i < lines.length; i++) {
|
||||
if (/^## /.test(lines[i]) || /^---\s*$/.test(lines[i]))
|
||||
break;
|
||||
out.push(lines[i]);
|
||||
}
|
||||
return out.join("\n").trim();
|
||||
}
|
||||
/**
|
||||
* True when this project's cerebrum was synced into Claude Code auto-memory
|
||||
* (openwolf update leaves a marker in the memory index). Claude then recalls
|
||||
* Do-Not-Repeat natively, so the digest must not inject it a second time.
|
||||
*/
|
||||
function claudeMemoryHasSync() {
|
||||
if (detectAgent() !== "claude")
|
||||
return false;
|
||||
try {
|
||||
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
||||
if (!home)
|
||||
return false;
|
||||
const slug = path.resolve(getProjectDir()).replace(/[^a-zA-Z0-9]/g, "-");
|
||||
const index = fs.readFileSync(path.join(home, ".claude", "projects", slug, "memory", "MEMORY.md"), "utf-8");
|
||||
return index.includes("<!-- openwolf:begin -->");
|
||||
}
|
||||
catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
/** Parse a leading YAML-ish frontmatter block for description/always keys (2.4). */
|
||||
function readFrontmatter(content) {
|
||||
if (!content.startsWith("---\n"))
|
||||
return { body: content };
|
||||
const end = content.indexOf("\n---", 4);
|
||||
if (end === -1)
|
||||
return { body: content };
|
||||
const fm = content.slice(4, end);
|
||||
const body = content.slice(content.indexOf("\n", end + 1) + 1);
|
||||
const description = fm.match(/^description:\s*(.+)$/m)?.[1]?.trim();
|
||||
const always = /^always:\s*true\s*$/m.test(fm);
|
||||
return { description, always, body };
|
||||
}
|
||||
/**
|
||||
* Progressive-disclosure digest (2.4). The old digest injected content
|
||||
* (mean 794 tok, 75% of it a staleness nag or placeholder text). The new
|
||||
* shape is an INDEX: one line per live .wolf file with what it is, how big,
|
||||
* and how fresh, so the model can decide what to pull with grep or find —
|
||||
* the retrieval pattern claude-mem, Letta, and the platform's own skills
|
||||
* design converged on. Only two things still inject content: the top
|
||||
* Do-Not-Repeat rules (small, highest value, unless native auto-memory
|
||||
* already carries them) and any .wolf file whose frontmatter says
|
||||
* `always: true` (budget-capped). Target: ~400 tokens.
|
||||
*/
|
||||
function buildSessionDigest(wolfDir, budget) {
|
||||
budget = Math.min(budget, 400 + Math.floor(budget / 4));
|
||||
const parts = [];
|
||||
let used = 0;
|
||||
const tryAdd = (text) => {
|
||||
if (!text)
|
||||
return false;
|
||||
const cost = estimateTokens(text, "prose");
|
||||
if (used + cost > budget)
|
||||
return false;
|
||||
parts.push(text);
|
||||
used += cost;
|
||||
return true;
|
||||
};
|
||||
const isPlaceholder = (l) => /_<[^>]*>_|\{\{[^}]*\}\}|^_?<[^>]*>_?$/.test(l.trim());
|
||||
// 1. STATUS.md "next phase": the one piece of content that always earns its
|
||||
// tokens on resume — but never template placeholders (8/28 measured digests
|
||||
// were pure placeholder before this filter).
|
||||
try {
|
||||
const status = fs.readFileSync(path.join(wolfDir, "STATUS.md"), "utf-8");
|
||||
const section = extractSection(readFrontmatter(status).body, /^## 🚀/);
|
||||
const meaningful = section.split("\n").filter((l) => l.trim() && !isPlaceholder(l));
|
||||
const contentLines = meaningful.filter((l) => !/^#|^\||^-+$/.test(l.trim()));
|
||||
if (contentLines.length > 0)
|
||||
tryAdd(meaningful.slice(0, 14).join("\n"));
|
||||
}
|
||||
catch { }
|
||||
// 2. Top Do-Not-Repeat rules (skipped when native auto-memory carries them).
|
||||
if (!claudeMemoryHasSync()) {
|
||||
try {
|
||||
const cerebrum = readFrontmatter(fs.readFileSync(path.join(wolfDir, "cerebrum.md"), "utf-8")).body;
|
||||
const dnr = extractSection(cerebrum, /^## Do-Not-Repeat/);
|
||||
const entries = dnr.split("\n").filter((l) => l.startsWith("- ") && !isPlaceholder(l));
|
||||
if (entries.length > 0) {
|
||||
tryAdd("Do-Not-Repeat (top rules from .wolf/cerebrum.md):\n" + entries.slice(-3).join("\n"));
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
// 3. always:true files — the Letta-style explicit always-loaded set, capped.
|
||||
try {
|
||||
for (const f of fs.readdirSync(wolfDir)) {
|
||||
if (!f.endsWith(".md") || f === "STATUS.md" || f === "OPENWOLF.md" || f === "anatomy.md")
|
||||
continue;
|
||||
const raw = fs.readFileSync(path.join(wolfDir, f), "utf-8");
|
||||
const fm = readFrontmatter(raw);
|
||||
if (fm.always) {
|
||||
tryAdd(`## .wolf/${f} (always)\n` + fm.body.trim().split("\n").slice(0, 40).join("\n"));
|
||||
}
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
// 4. The index: one line per live state file. Pointers, not content.
|
||||
try {
|
||||
const indexLines = [];
|
||||
const describe = {
|
||||
"cerebrum.md": "learned preferences, conventions, Do-Not-Repeat rules",
|
||||
"memory.md": "chronological action log per session",
|
||||
"STATUS.md": "session handoff (regenerate with /handoff)",
|
||||
"buglog.json": "known bugs with root causes and fixes (openwolf bug search)",
|
||||
"anatomy.md": "project file index (query with openwolf find, never read whole)",
|
||||
};
|
||||
for (const [file, fallback] of Object.entries(describe)) {
|
||||
const p = path.join(wolfDir, file);
|
||||
let st;
|
||||
try {
|
||||
st = fs.statSync(p);
|
||||
}
|
||||
catch {
|
||||
continue;
|
||||
}
|
||||
let desc = fallback;
|
||||
let extra = "";
|
||||
if (file === "buglog.json") {
|
||||
const bugs = readBugLogFile(wolfDir).bugs.length;
|
||||
if (bugs === 0)
|
||||
continue;
|
||||
extra = `, ${bugs} bugs`;
|
||||
}
|
||||
else if (file.endsWith(".md")) {
|
||||
const fm = readFrontmatter(fs.readFileSync(p, "utf-8"));
|
||||
if (fm.description)
|
||||
desc = fm.description;
|
||||
// A template-only file earns no index line.
|
||||
if (fm.body.trim().length < 80)
|
||||
continue;
|
||||
}
|
||||
const tok = Math.ceil(st.size / 4);
|
||||
indexLines.push(`- .wolf/${file}: ${desc} (~${tok} tok${extra}, updated ${st.mtime.toISOString().slice(0, 10)})`);
|
||||
}
|
||||
if (indexLines.length > 0) {
|
||||
tryAdd("OpenWolf state on disk (read on demand, not preloaded):\n" + indexLines.join("\n"));
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
return parts.join("\n\n");
|
||||
}
|
||||
async function main() {
|
||||
ensureWolfDir();
|
||||
const wolfDir = getWolfDir();
|
||||
|
|
@ -18,27 +180,65 @@ async function main() {
|
|||
}
|
||||
catch { }
|
||||
const hooksDir = path.join(wolfDir, "hooks");
|
||||
const sessionFile = path.join(hooksDir, "_session.json");
|
||||
const now = new Date();
|
||||
const sessionId = `session-${now.toISOString().slice(0, 10)}-${String(now.getHours()).padStart(2, "0")}${String(now.getMinutes()).padStart(2, "0")}`;
|
||||
// Create fresh session state
|
||||
writeJSON(sessionFile, {
|
||||
session_id: sessionId,
|
||||
started: timestamp(),
|
||||
files_read: {},
|
||||
files_written: [],
|
||||
edit_counts: {},
|
||||
anatomy_hits: 0,
|
||||
anatomy_misses: 0,
|
||||
repeated_reads_warned: 0,
|
||||
cerebrum_warnings: 0,
|
||||
stop_count: 0,
|
||||
});
|
||||
// Append session header to memory.md
|
||||
const memoryPath = path.join(wolfDir, "memory.md");
|
||||
const header = `\n## Session: ${now.toISOString().slice(0, 10)} ${timeShort()}\n\n| Time | Action | File(s) | Outcome | ~Tokens |\n|------|--------|---------|---------|--------|\n`;
|
||||
appendMarkdown(memoryPath, header);
|
||||
// Check cerebrum freshness — remind Claude to learn
|
||||
// SessionStart fires for startup/resume/clear/compact. Only startup and
|
||||
// clear begin a genuinely new session — on resume/compact the session is
|
||||
// still live, and resetting the session state would wipe read/write
|
||||
// tracking mid-flight (Workstream F3: compaction survival).
|
||||
let source = "startup";
|
||||
let hookInput = {};
|
||||
try {
|
||||
hookInput = JSON.parse(await readStdin());
|
||||
if (typeof hookInput.source === "string")
|
||||
source = hookInput.source;
|
||||
}
|
||||
catch { }
|
||||
// Session state is keyed by the harness session id (2.2): concurrent
|
||||
// sessions in one project no longer cross-contaminate each other.
|
||||
const sessionFile = getSessionFilePath(hookInput);
|
||||
const sessionId = hookInput.session_id ??
|
||||
`session-${now.toISOString().slice(0, 10)}-${String(now.getHours()).padStart(2, "0")}${String(now.getMinutes()).padStart(2, "0")}`;
|
||||
gcSessionFiles();
|
||||
// Hook install self-test (2.2): a missing dependency file once silently
|
||||
// killed a hook for 3 weeks across 440 invocations. Verify every installed
|
||||
// hook's relative imports resolve; surface breakage in the digest.
|
||||
const brokenHooks = selfTestHooks(hooksDir);
|
||||
const continuing = (source === "compact" || source === "resume") && fs.existsSync(sessionFile);
|
||||
// Compaction evicts verbatim file contents from the model's context, so a
|
||||
// post-compact re-read is legitimate: clear deny-eligibility on every
|
||||
// recorded read (the warn path still fires; only denial is disarmed).
|
||||
if (continuing && source === "compact") {
|
||||
try {
|
||||
const session = readJSON(sessionFile, {});
|
||||
if (session.files_read) {
|
||||
for (const entry of Object.values(session.files_read)) {
|
||||
entry.compacted = true;
|
||||
}
|
||||
writeJSON(sessionFile, session);
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
if (!continuing) {
|
||||
writeJSON(sessionFile, {
|
||||
session_id: sessionId,
|
||||
started: timestamp(),
|
||||
files_read: {},
|
||||
files_written: [],
|
||||
edit_counts: {},
|
||||
anatomy_hits: 0,
|
||||
anatomy_misses: 0,
|
||||
repeated_reads_warned: 0,
|
||||
stop_count: 0,
|
||||
});
|
||||
// Append session header to memory.md
|
||||
const memoryPath = path.join(wolfDir, "memory.md");
|
||||
const header = `\n## Session: ${now.toISOString().slice(0, 10)} ${timeShort()}\n\n| Time | Action | File(s) | Outcome | ~Tokens |\n|------|--------|---------|---------|--------|\n`;
|
||||
appendMarkdown(memoryPath, header);
|
||||
}
|
||||
// Startup nudges — folded into the injected digest below (stderr from an
|
||||
// exit-0 hook never reaches the model).
|
||||
const startupNotes = [];
|
||||
try {
|
||||
const cerebrumPath = path.join(wolfDir, "cerebrum.md");
|
||||
const cerebrumContent = fs.readFileSync(cerebrumPath, "utf-8");
|
||||
|
|
@ -50,28 +250,115 @@ async function main() {
|
|||
return t.startsWith("- ") || t.startsWith("* ") || (t.startsWith("[") && t.includes("]"));
|
||||
});
|
||||
if (entryLines.length < 3) {
|
||||
process.stderr.write(`💡 OpenWolf: cerebrum.md has only ${entryLines.length} entries. Learn from this session — record user preferences, project conventions, and mistakes to .wolf/cerebrum.md.\n`);
|
||||
startupNotes.push(`cerebrum.md has only ${entryLines.length} entries. Record user preferences, project conventions, and mistakes to .wolf/cerebrum.md as you learn them this session.`);
|
||||
}
|
||||
else if (daysSinceUpdate > 3) {
|
||||
process.stderr.write(`💡 OpenWolf: cerebrum.md hasn't been updated in ${Math.floor(daysSinceUpdate)} days. Look for opportunities to add learnings this session.\n`);
|
||||
startupNotes.push(`cerebrum.md hasn't been updated in ${Math.floor(daysSinceUpdate)} days. Look for opportunities to add learnings this session.`);
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
// Check buglog — remind if empty
|
||||
try {
|
||||
const buglogPath = path.join(wolfDir, "buglog.json");
|
||||
const buglog = readJSON(buglogPath, { bugs: [] });
|
||||
if (buglog.bugs.length === 0) {
|
||||
process.stderr.write(`📋 OpenWolf: buglog.json is empty. If you encounter or fix any bugs, errors, or failed tests this session, log them to .wolf/buglog.json.\n`);
|
||||
if (readBugLogFile(wolfDir).bugs.length === 0) {
|
||||
startupNotes.push(`buglog.json is empty. If you encounter or fix any bugs, errors, or failed tests this session, log them to .wolf/buglog.json.`);
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
// Increment total_sessions in token-ledger — only for a genuinely new
|
||||
// session. Resume/compact re-fires SessionStart for a session already
|
||||
// counted, which used to inflate the lifetime count.
|
||||
if (!continuing) {
|
||||
const ledgerPath = path.join(wolfDir, "token-ledger.json");
|
||||
const ledger = readJSON(ledgerPath, { version: 1, lifetime: { total_sessions: 0 } });
|
||||
ledger.lifetime.total_sessions++;
|
||||
writeJSON(ledgerPath, ledger);
|
||||
}
|
||||
// Inject the budget-capped digest into the model's context.
|
||||
try {
|
||||
let digest = buildSessionDigest(wolfDir, digestBudget(wolfDir));
|
||||
// (The anatomy-staleness banner is gone in 2.2: it led 21 of 28 measured
|
||||
// digests because a 6h rescan interval loses to normal commit cadence —
|
||||
// a warning firing 75% of the time is a tax, not a signal. Freshness is
|
||||
// the daemon's and scan's job, not the model's.)
|
||||
// Hook breakage is the one warning worth its tokens: without it, a broken
|
||||
// install silently loses every feature (happened for 3 weeks once).
|
||||
if (brokenHooks.length > 0) {
|
||||
digest = `OpenWolf hooks are degraded: ${brokenHooks.join("; ")}. Run \`openwolf update\` in this project to repair the install.\n\n` + digest;
|
||||
}
|
||||
// Post-compaction restore (F3 + 2.4): resurface what compaction erased.
|
||||
// The platform documents that paths-scoped rules and nested CLAUDE.md are
|
||||
// LOST at compaction until a matching file is read again — restore the
|
||||
// scoped rules for files this session already touched, plus the top
|
||||
// Do-Not-Repeat rules (decay countermeasure) and the in-flight state.
|
||||
if (continuing && source === "compact") {
|
||||
const session = readJSON(sessionFile, {});
|
||||
const restoreParts = [];
|
||||
const files = [...new Set((session.files_written ?? []).map((w) => w.file))];
|
||||
if (files.length > 0) {
|
||||
restoreParts.push(`## Session in progress (context was just compacted)\nFiles already modified this session: ${files.slice(-15).join(", ")}. Do not re-read them wholesale — check .wolf/memory.md for what was done.`);
|
||||
}
|
||||
try {
|
||||
const rules = topRules(fs.readFileSync(path.join(wolfDir, "cerebrum.md"), "utf-8"), 3);
|
||||
if (rules.length > 0) {
|
||||
restoreParts.push(`Project rules still in effect (from .wolf/cerebrum.md Do-Not-Repeat):\n${rules.join("\n")}`);
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
try {
|
||||
const projectDir = getProjectDir();
|
||||
const relFiles = [
|
||||
...Object.keys(session.files_read ?? {}),
|
||||
...files,
|
||||
].map((f) => path.isAbsolute(f) ? path.relative(projectDir, f).split(path.sep).join("/") : f);
|
||||
restoreParts.push(...scopedRulesForFiles(projectDir, relFiles));
|
||||
}
|
||||
catch { }
|
||||
if (restoreParts.length > 0) {
|
||||
digest = restoreParts.join("\n\n") + (digest ? "\n\n" + digest : "");
|
||||
}
|
||||
}
|
||||
if (startupNotes.length > 0) {
|
||||
digest = digest
|
||||
? `${digest}\n\n${startupNotes.map((n) => `- ${n}`).join("\n")}`
|
||||
: startupNotes.map((n) => `- ${n}`).join("\n");
|
||||
}
|
||||
if (digest) {
|
||||
recordInjectionToSessionFile(sessionFile, "digest", digest);
|
||||
process.stdout.write(JSON.stringify({
|
||||
hookSpecificOutput: { hookEventName: "SessionStart", additionalContext: digest },
|
||||
}));
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
// Increment total_sessions in token-ledger
|
||||
const ledgerPath = path.join(wolfDir, "token-ledger.json");
|
||||
const ledger = readJSON(ledgerPath, { version: 1, lifetime: { total_sessions: 0 } });
|
||||
ledger.lifetime.total_sessions++;
|
||||
writeJSON(ledgerPath, ledger);
|
||||
process.exit(0);
|
||||
}
|
||||
main().catch(() => process.exit(0));
|
||||
/**
|
||||
* Verify every installed hook script's relative imports resolve (2.2). The
|
||||
* 440-crash class was a missing DEPENDENCY file, not a missing hook, so a
|
||||
* bare existence check of registered hooks would have missed it. Cheap static
|
||||
* check: scan each installed hook script for relative import specifiers and
|
||||
* verify the target files exist. Returns problem strings (empty = healthy).
|
||||
*/
|
||||
function selfTestHooks(hooksDir) {
|
||||
const problems = [];
|
||||
try {
|
||||
const files = fs.readdirSync(hooksDir).filter((f) => f.endsWith(".js"));
|
||||
for (const f of files) {
|
||||
let src = "";
|
||||
try {
|
||||
src = fs.readFileSync(path.join(hooksDir, f), "utf-8");
|
||||
}
|
||||
catch {
|
||||
problems.push(`${f} unreadable`);
|
||||
continue;
|
||||
}
|
||||
for (const m of src.matchAll(/from\s+["']\.\/([\w.-]+\.js)["']/g)) {
|
||||
if (!fs.existsSync(path.join(hooksDir, m[1]))) {
|
||||
problems.push(`${f} cannot load ${m[1]}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
return [...new Set(problems)].slice(0, 3);
|
||||
}
|
||||
hookMain("session-start", main);
|
||||
//# sourceMappingURL=session-start.js.map
|
||||
|
|
@ -1,10 +1,53 @@
|
|||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import * as crypto from "node:crypto";
|
||||
import { fileURLToPath } from "node:url";
|
||||
// Prefer the harness-provided project dir so hooks work even if CWD changes
|
||||
// during a session. Each supported agent exposes its own env var; hooks are
|
||||
// provider-agnostic (Workstream C) so all are checked.
|
||||
//
|
||||
// None of those vars are guaranteed. Claude Code in particular does not put
|
||||
// CLAUDE_PROJECT_DIR in the hook process's environment on any platform: it
|
||||
// delivers project context through the stdin JSON payload instead. So before
|
||||
// falling back to CWD, derive the root from this script's own location. A hook
|
||||
// always runs as <project>/.wolf/hooks/<name>.js, which makes the project root
|
||||
// two directories up, verified by the .wolf/ directory being there.
|
||||
function projectDirFromScriptLocation() {
|
||||
try {
|
||||
const here = path.dirname(fileURLToPath(import.meta.url));
|
||||
if (path.basename(here) !== "hooks")
|
||||
return null;
|
||||
const root = path.resolve(here, "..", "..");
|
||||
if (path.basename(path.dirname(here)) !== ".wolf")
|
||||
return null;
|
||||
return fs.existsSync(path.join(root, ".wolf")) ? root : null;
|
||||
}
|
||||
catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
export function getProjectDir() {
|
||||
return (process.env.CLAUDE_PROJECT_DIR ||
|
||||
process.env.CODEX_PROJECT_ROOT ||
|
||||
process.env.OPENWOLF_PROJECT_ROOT ||
|
||||
projectDirFromScriptLocation() ||
|
||||
process.cwd());
|
||||
}
|
||||
export function getWolfDir() {
|
||||
// Prefer CLAUDE_PROJECT_DIR so hooks work even if CWD changes during a session
|
||||
const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
||||
return path.join(projectDir, ".wolf");
|
||||
return path.join(getProjectDir(), ".wolf");
|
||||
}
|
||||
/** Which agent harness invoked this hook — used for per-agent ledger attribution. */
|
||||
export function detectAgent() {
|
||||
// CLAUDECODE is set in every Claude Code hook process; CLAUDE_PROJECT_DIR is
|
||||
// not set at all, so checking it alone attributed real Claude sessions to
|
||||
// "default" and lost their per-agent ledger rows.
|
||||
if (process.env.CLAUDECODE || process.env.CLAUDE_CODE_ENTRYPOINT || process.env.CLAUDE_PROJECT_DIR)
|
||||
return "claude";
|
||||
if (process.env.CODEX_PROJECT_ROOT || process.env.CODEX_SANDBOX)
|
||||
return "codex";
|
||||
if (process.env.OPENCODE || process.env.OPENCODE_PROJECT_ROOT)
|
||||
return "opencode";
|
||||
return "default";
|
||||
}
|
||||
/**
|
||||
* Bail out silently if .wolf/ directory doesn't exist in the current project.
|
||||
|
|
@ -16,6 +59,86 @@ export function ensureWolfDir() {
|
|||
process.exit(0);
|
||||
}
|
||||
}
|
||||
// ─── Hook health (2.2): heartbeat + crash recording ──────────────────────────
|
||||
// The PostToolUse write hook crashed on 100% of 440 invocations for 3 weeks
|
||||
// with nothing noticing, because every hook swallowed its own errors
|
||||
// (main().catch(() => exit(0))). Every hook now runs through hookMain(), which
|
||||
// records a per-hook heartbeat (last success / last error / consecutive
|
||||
// failures) that session-start, update, and the dashboard can check.
|
||||
export const HEARTBEAT_FILE = "_heartbeat.json";
|
||||
export function recordHeartbeat(hookName, error) {
|
||||
try {
|
||||
const file = path.join(getWolfDir(), "hooks", HEARTBEAT_FILE);
|
||||
const beats = readJSON(file, {});
|
||||
const entry = beats[hookName] ?? { consecutive_failures: 0 };
|
||||
if (error === undefined) {
|
||||
entry.last_ok = new Date().toISOString();
|
||||
entry.consecutive_failures = 0;
|
||||
}
|
||||
else {
|
||||
entry.last_error = new Date().toISOString();
|
||||
entry.last_error_message = String(error instanceof Error ? error.stack ?? error.message : error).slice(0, 500);
|
||||
entry.consecutive_failures = (entry.consecutive_failures ?? 0) + 1;
|
||||
}
|
||||
beats[hookName] = entry;
|
||||
writeJSON(file, beats);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
/**
|
||||
* Standard hook entry point: runs the hook, records a heartbeat either way,
|
||||
* always exits 0 (hooks must never block the agent). `--selfcheck` exits
|
||||
* immediately after module load: reaching this code at all proves every static
|
||||
* import resolved, which is exactly the failure class that went undetected.
|
||||
*/
|
||||
export function hookMain(hookName, fn) {
|
||||
if (process.argv.includes("--selfcheck")) {
|
||||
process.stdout.write(`ok ${hookName}`);
|
||||
process.exit(0);
|
||||
}
|
||||
Promise.resolve()
|
||||
.then(fn)
|
||||
.then(() => {
|
||||
recordHeartbeat(hookName);
|
||||
process.exit(0);
|
||||
})
|
||||
.catch((err) => {
|
||||
recordHeartbeat(hookName, err);
|
||||
process.exit(0);
|
||||
});
|
||||
}
|
||||
// ─── Session-keyed state (2.2) ───────────────────────────────────────────────
|
||||
// _session.json used to be one per-PROJECT file, so concurrent Claude sessions
|
||||
// cross-contaminated each other's read tracking (one of the two causes of the
|
||||
// ledger's ~20x duplicate-warning inflation). State is now keyed by the
|
||||
// harness-provided session_id when present.
|
||||
/** Resolve the session state file for this hook invocation. */
|
||||
export function getSessionFilePath(hookInput) {
|
||||
const hooksDir = path.join(getWolfDir(), "hooks");
|
||||
const id = hookInput?.session_id;
|
||||
if (typeof id === "string" && /^[\w.-]{4,128}$/.test(id)) {
|
||||
return path.join(hooksDir, "sessions", `${id}.json`);
|
||||
}
|
||||
// Legacy fallback for agents that pass no session id.
|
||||
return path.join(hooksDir, "_session.json");
|
||||
}
|
||||
/** Delete session state files older than maxAgeDays (called from session-start). */
|
||||
export function gcSessionFiles(maxAgeDays = 7) {
|
||||
try {
|
||||
const dir = path.join(getWolfDir(), "hooks", "sessions");
|
||||
const cutoff = Date.now() - maxAgeDays * 24 * 3600 * 1000;
|
||||
for (const f of fs.readdirSync(dir)) {
|
||||
if (!f.endsWith(".json"))
|
||||
continue;
|
||||
try {
|
||||
if (fs.statSync(path.join(dir, f)).mtimeMs < cutoff)
|
||||
fs.unlinkSync(path.join(dir, f));
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
export function readJSON(filePath, fallback) {
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
||||
|
|
@ -24,6 +147,24 @@ export function readJSON(filePath, fallback) {
|
|||
return fallback;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Reads .wolf/buglog.json in any shape a project might have on disk and always
|
||||
* returns { version, bugs }. A hand-written log is often a bare array of
|
||||
* entries; that shape used to reach `bugLog.bugs.length` and throw, which is
|
||||
* how pre-write racked up 465 consecutive failures on one project before
|
||||
* anyone noticed (the hook heartbeat was the only witness).
|
||||
*/
|
||||
export function readBugLogFile(wolfDir) {
|
||||
const raw = readJSON(path.join(wolfDir, "buglog.json"), null);
|
||||
if (Array.isArray(raw))
|
||||
return { version: 1, bugs: raw };
|
||||
if (raw && typeof raw === "object") {
|
||||
const bugs = raw.bugs;
|
||||
if (Array.isArray(bugs))
|
||||
return { ...raw, version: 1, bugs };
|
||||
}
|
||||
return { version: 1, bugs: [] };
|
||||
}
|
||||
export function writeJSON(filePath, data) {
|
||||
const dir = path.dirname(filePath);
|
||||
if (!fs.existsSync(dir))
|
||||
|
|
@ -60,50 +201,30 @@ export function appendMarkdown(filePath, line) {
|
|||
fs.mkdirSync(dir, { recursive: true });
|
||||
fs.appendFileSync(filePath, line, "utf-8");
|
||||
}
|
||||
export function parseAnatomy(content) {
|
||||
const sections = new Map();
|
||||
let currentSection = "";
|
||||
for (const line of content.split("\n")) {
|
||||
const sm = line.match(/^## (.+)/);
|
||||
if (sm) {
|
||||
currentSection = sm[1].trim();
|
||||
if (!sections.has(currentSection))
|
||||
sections.set(currentSection, []);
|
||||
continue;
|
||||
}
|
||||
if (!currentSection)
|
||||
continue;
|
||||
const em = line.match(/^- `([^`]+)`(?:\s+—\s+(.+?))?\s*\(~(\d+)\s+tok\)$/);
|
||||
if (em) {
|
||||
sections.get(currentSection).push({
|
||||
file: em[1],
|
||||
description: em[2] || "",
|
||||
tokens: parseInt(em[3], 10),
|
||||
});
|
||||
}
|
||||
}
|
||||
return sections;
|
||||
}
|
||||
export function serializeAnatomy(sections, metadata) {
|
||||
const lines = [
|
||||
"# anatomy.md",
|
||||
"",
|
||||
`> Auto-maintained by OpenWolf. Last scanned: ${metadata.lastScanned}`,
|
||||
`> Files: ${metadata.fileCount} tracked | Anatomy hits: ${metadata.hits} | Misses: ${metadata.misses}`,
|
||||
"",
|
||||
];
|
||||
const keys = [...sections.keys()].sort();
|
||||
for (const key of keys) {
|
||||
lines.push(`## ${key}`);
|
||||
lines.push("");
|
||||
const entries = sections.get(key).sort((a, b) => a.file.localeCompare(b.file));
|
||||
for (const e of entries) {
|
||||
const desc = e.description ? ` — ${e.description}` : "";
|
||||
lines.push(`- \`${e.file}\`${desc} (~${e.tokens} tok)`);
|
||||
}
|
||||
lines.push("");
|
||||
}
|
||||
return lines.join("\n");
|
||||
// parseAnatomy / serializeAnatomy / AnatomyEntry moved to ./anatomy-store.ts —
|
||||
// the single canonical home of the anatomy format (OPENWOLF-2.0 §F2b).
|
||||
// Files whose contents (or content-derived descriptions) must never reach
|
||||
// anatomy.md / memory.md because they hold secrets (issue #54). Kept in sync
|
||||
// with the copy in src/scanner/anatomy-scanner.ts — hooks are standalone
|
||||
// scripts and the scanner cannot be imported from here.
|
||||
const SENSITIVE_EXTENSIONS = new Set([
|
||||
".pem", ".key", ".p8", ".p12", ".pfx", ".keystore", ".jks", ".ppk", ".kdbx", ".tfstate",
|
||||
]);
|
||||
const SENSITIVE_BASENAMES = new Set([".npmrc", ".netrc", ".htpasswd", ".pgpass"]);
|
||||
export function isSensitiveFile(basename) {
|
||||
const lower = basename.toLowerCase();
|
||||
if (lower === ".env" || lower.startsWith(".env."))
|
||||
return true;
|
||||
if (SENSITIVE_BASENAMES.has(lower))
|
||||
return true;
|
||||
const dot = lower.lastIndexOf(".");
|
||||
if (dot >= 0 && SENSITIVE_EXTENSIONS.has(lower.slice(dot)))
|
||||
return true;
|
||||
if (/^id_(rsa|dsa|ecdsa|ed25519)/.test(lower))
|
||||
return true;
|
||||
if (lower.includes("credential") || /^secrets\.(json|ya?ml|toml)$/.test(lower))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
export function extractDescription(filePath) {
|
||||
const MAX_DESC = 150;
|
||||
|
|
@ -611,4 +732,121 @@ export function readStdin() {
|
|||
export function normalizePath(p) {
|
||||
return p.replace(/\\/g, "/");
|
||||
}
|
||||
export function emitHookJSON(hookEventName, fields) {
|
||||
const out = { hookEventName };
|
||||
if (fields.additionalContext)
|
||||
out.additionalContext = fields.additionalContext;
|
||||
if (fields.updatedToolOutput !== undefined)
|
||||
out.updatedToolOutput = fields.updatedToolOutput;
|
||||
if (fields.permissionDecision) {
|
||||
out.permissionDecision = fields.permissionDecision;
|
||||
out.permissionDecisionReason = fields.permissionDecisionReason ?? "";
|
||||
}
|
||||
if (Object.keys(out).length === 1)
|
||||
return;
|
||||
process.stdout.write(JSON.stringify({ hookSpecificOutput: out }));
|
||||
}
|
||||
/** Record an injected text against the in-memory session (caller persists). */
|
||||
export function recordInjection(session, source, text) {
|
||||
if (!text)
|
||||
return;
|
||||
const tokens = estimateTokens(text, "prose");
|
||||
session.injected_tokens_estimated = (session.injected_tokens_estimated ?? 0) + tokens;
|
||||
const bySource = session.injected_by_source ?? {};
|
||||
bySource[source] = (bySource[source] ?? 0) + tokens;
|
||||
session.injected_by_source = bySource;
|
||||
}
|
||||
/** Same, for hooks that do not otherwise hold the session file open. */
|
||||
export function recordInjectionToSessionFile(sessionFile, source, text) {
|
||||
if (!text)
|
||||
return;
|
||||
try {
|
||||
const session = readJSON(sessionFile, {});
|
||||
recordInjection(session, source, text);
|
||||
writeJSON(sessionFile, session);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
/**
|
||||
* Count non-mechanical semantic entries written to memory.md this session.
|
||||
* Mechanical entries (auto-generated file ops, session-end lines) don't count.
|
||||
* Used by the stop hook to detect whether Claude wrote a meaningful summary.
|
||||
*/
|
||||
export function countSemanticEntries(wolfDir) {
|
||||
const memoryPath = path.join(wolfDir, "memory.md");
|
||||
try {
|
||||
const content = fs.readFileSync(memoryPath, "utf-8");
|
||||
const mechanical = /^\|\s*[\d\-: ]+\|\s*(Created|Edited|Multi-edited|Session end:|designqc:)/;
|
||||
const tableHeader = /^\|\s*Time\s*\|/i;
|
||||
const tableSeparator = /^\|[\s\-|]+\|?\s*$/;
|
||||
const lines = content.split("\n");
|
||||
let start = -1;
|
||||
for (let i = lines.length - 1; i >= 0; i--) {
|
||||
if (lines[i].startsWith("## Session: ")) {
|
||||
start = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
let count = 0;
|
||||
for (let i = start + 1; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
if (!line.startsWith("|"))
|
||||
continue;
|
||||
if (tableHeader.test(line) || tableSeparator.test(line) || mechanical.test(line))
|
||||
continue;
|
||||
count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
catch {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
export function readTranscriptUsage(transcriptPath) {
|
||||
let raw;
|
||||
try {
|
||||
raw = fs.readFileSync(transcriptPath, "utf-8");
|
||||
}
|
||||
catch {
|
||||
return null;
|
||||
}
|
||||
// One usage block per API call; streaming can emit several transcript lines
|
||||
// for one message id, and a resumed session can replay a message under a new
|
||||
// request — dedupe on message id + request id, keeping the last usage seen.
|
||||
const byId = new Map();
|
||||
let anon = 0;
|
||||
for (const line of raw.split("\n")) {
|
||||
if (!line.trim())
|
||||
continue;
|
||||
try {
|
||||
const entry = JSON.parse(line);
|
||||
const usage = entry?.message?.usage;
|
||||
if (usage && typeof usage === "object" && typeof usage.output_tokens === "number") {
|
||||
const key = `${entry.message.id ?? `anon-${anon++}`}:${entry.requestId ?? ""}`;
|
||||
byId.set(key, { ...usage, model: entry.message.model });
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
if (byId.size === 0)
|
||||
return null;
|
||||
const total = { input_tokens: 0, output_tokens: 0, cache_read_input_tokens: 0, cache_creation_input_tokens: 0, api_calls: byId.size };
|
||||
const perModel = {};
|
||||
for (const u of byId.values()) {
|
||||
total.input_tokens += u.input_tokens ?? 0;
|
||||
total.output_tokens += u.output_tokens ?? 0;
|
||||
total.cache_read_input_tokens += u.cache_read_input_tokens ?? 0;
|
||||
total.cache_creation_input_tokens += u.cache_creation_input_tokens ?? 0;
|
||||
const model = u.model ?? "unknown";
|
||||
const m = perModel[model] ?? (perModel[model] = { input_tokens: 0, output_tokens: 0, cache_read_input_tokens: 0, cache_creation_input_tokens: 0, api_calls: 0 });
|
||||
m.input_tokens += u.input_tokens ?? 0;
|
||||
m.output_tokens += u.output_tokens ?? 0;
|
||||
m.cache_read_input_tokens += u.cache_read_input_tokens ?? 0;
|
||||
m.cache_creation_input_tokens += u.cache_creation_input_tokens ?? 0;
|
||||
m.api_calls++;
|
||||
}
|
||||
if (Object.keys(perModel).length > 0)
|
||||
total.per_model = perModel;
|
||||
return total;
|
||||
}
|
||||
//# sourceMappingURL=shared.js.map
|
||||
|
|
@ -1,18 +1,18 @@
|
|||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { getWolfDir, ensureWolfDir, readJSON, writeJSON, appendMarkdown, timeShort } from "./shared.js";
|
||||
import { getWolfDir, ensureWolfDir, readJSON, writeJSON, countSemanticEntries, readStdin, hookMain, getSessionFilePath } from "./shared.js";
|
||||
import { buildSessionEntry, flushSessionToLedger } from "./ledger.js";
|
||||
import { verifyHookDelivery } from "./hook-attachments.js";
|
||||
async function main() {
|
||||
ensureWolfDir();
|
||||
const wolfDir = getWolfDir();
|
||||
const hooksDir = path.join(wolfDir, "hooks");
|
||||
const sessionFile = path.join(hooksDir, "_session.json");
|
||||
const projectRoot = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
||||
// Runs every stop regardless of read/write activity this turn, and
|
||||
// regardless of whether buglog.json was edited via the Write/Edit tool
|
||||
// or via a raw script (the common path for a file this large) — this
|
||||
// is the only guard that can't be bypassed by which tool touched it.
|
||||
checkAndCheckpointBuglog(wolfDir, projectRoot);
|
||||
// Stop payload → transcript path for real usage measurement (F1)
|
||||
let hookInput = {};
|
||||
try {
|
||||
hookInput = JSON.parse(await readStdin());
|
||||
}
|
||||
catch { }
|
||||
const sessionFile = getSessionFilePath(hookInput);
|
||||
const session = readJSON(sessionFile, {
|
||||
session_id: "",
|
||||
started: "",
|
||||
|
|
@ -22,8 +22,8 @@ async function main() {
|
|||
anatomy_hits: 0,
|
||||
anatomy_misses: 0,
|
||||
repeated_reads_warned: 0,
|
||||
cerebrum_warnings: 0,
|
||||
stop_count: 0,
|
||||
reminders_sent: {},
|
||||
});
|
||||
session.stop_count++;
|
||||
// Only write to ledger if there's been activity
|
||||
|
|
@ -31,206 +31,117 @@ async function main() {
|
|||
const writeCount = session.files_written.length;
|
||||
if (readCount === 0 && writeCount === 0) {
|
||||
writeJSON(sessionFile, session);
|
||||
process.exit(0);
|
||||
return;
|
||||
}
|
||||
// Check for files edited many times without a buglog entry
|
||||
checkForMissingBugLogs(wolfDir, session);
|
||||
// Check if cerebrum was updated this session (it should be if there were edits)
|
||||
checkCerebrumFreshness(wolfDir, session);
|
||||
// Build session entry for ledger
|
||||
const reads = Object.entries(session.files_read).map(([file, data]) => ({
|
||||
file,
|
||||
tokens_estimated: data.tokens,
|
||||
was_repeated: data.count > 1,
|
||||
anatomy_had_description: false, // simplified
|
||||
}));
|
||||
const writes = session.files_written.map((w) => ({
|
||||
file: w.file,
|
||||
tokens_estimated: w.tokens,
|
||||
action: w.action,
|
||||
}));
|
||||
const inputTokens = reads.reduce((sum, r) => sum + r.tokens_estimated, 0);
|
||||
const outputTokens = writes.reduce((sum, w) => sum + w.tokens_estimated, 0);
|
||||
const sessionEntry = {
|
||||
id: session.session_id,
|
||||
started: session.started,
|
||||
ended: new Date().toISOString(),
|
||||
reads,
|
||||
writes,
|
||||
totals: {
|
||||
input_tokens_estimated: inputTokens,
|
||||
output_tokens_estimated: outputTokens,
|
||||
reads_count: readCount,
|
||||
writes_count: writeCount,
|
||||
repeated_reads_blocked: session.repeated_reads_warned,
|
||||
anatomy_lookups: session.anatomy_hits,
|
||||
},
|
||||
};
|
||||
// Update token-ledger.json
|
||||
const ledgerPath = path.join(wolfDir, "token-ledger.json");
|
||||
const ledger = readJSON(ledgerPath, {
|
||||
version: 1,
|
||||
created_at: "",
|
||||
lifetime: {
|
||||
total_tokens_estimated: 0,
|
||||
total_reads: 0,
|
||||
total_writes: 0,
|
||||
total_sessions: 0,
|
||||
anatomy_hits: 0,
|
||||
anatomy_misses: 0,
|
||||
repeated_reads_blocked: 0,
|
||||
estimated_savings_vs_bare_cli: 0,
|
||||
},
|
||||
sessions: [],
|
||||
daemon_usage: [],
|
||||
waste_flags: [],
|
||||
optimization_report: { last_generated: null, patterns: [] },
|
||||
});
|
||||
ledger.sessions.push(sessionEntry);
|
||||
ledger.lifetime.total_reads += readCount;
|
||||
ledger.lifetime.total_writes += writeCount;
|
||||
ledger.lifetime.total_tokens_estimated += inputTokens + outputTokens;
|
||||
ledger.lifetime.anatomy_hits += session.anatomy_hits;
|
||||
ledger.lifetime.anatomy_misses += session.anatomy_misses;
|
||||
ledger.lifetime.repeated_reads_blocked += session.repeated_reads_warned;
|
||||
// Estimate savings: anatomy hits save ~200 tokens each, repeated reads blocked save their token count
|
||||
const savedFromAnatomy = session.anatomy_hits * 200;
|
||||
const savedFromRepeats = Object.values(session.files_read)
|
||||
.filter((r) => r.count > 1)
|
||||
.reduce((sum, r) => sum + r.tokens * (r.count - 1), 0);
|
||||
ledger.lifetime.estimated_savings_vs_bare_cli += savedFromAnatomy + savedFromRepeats;
|
||||
writeJSON(ledgerPath, ledger);
|
||||
// Write a session summary line to memory.md if there was meaningful activity
|
||||
if (writeCount > 0) {
|
||||
try {
|
||||
const uniqueFiles = new Set(session.files_written.map(w => path.basename(w.file)));
|
||||
const fileList = [...uniqueFiles].slice(0, 5).join(", ");
|
||||
const memoryPath = path.join(wolfDir, "memory.md");
|
||||
appendMarkdown(memoryPath, `| ${timeShort()} | Session end: ${writeCount} writes across ${uniqueFiles.size} files (${fileList}) | ${readCount} reads | ~${inputTokens + outputTokens} tok |\n`);
|
||||
}
|
||||
catch { }
|
||||
// Collect end-of-turn reminders. Each fires at most ONCE per session, and
|
||||
// they are QUEUED rather than emitted: Stop additionalContext forces a full
|
||||
// continuation turn (the model re-sends the whole conversation to respond),
|
||||
// so the UserPromptSubmit hook drains the queue into the next user turn's
|
||||
// context instead — same visibility, zero extra turns.
|
||||
if (!session.reminders_sent)
|
||||
session.reminders_sent = {};
|
||||
const reminderChecks = [
|
||||
["buglog", checkForMissingBugLogs(wolfDir, session)],
|
||||
["cerebrum", checkCerebrumFreshness(wolfDir, session)],
|
||||
["semantic", checkSemanticSummaries(wolfDir, session)],
|
||||
];
|
||||
const reminders = [];
|
||||
for (const [key, message] of reminderChecks) {
|
||||
if (message === null)
|
||||
continue;
|
||||
const sent = session.reminders_sent[key] ?? 0;
|
||||
if (sent >= 1)
|
||||
continue;
|
||||
session.reminders_sent[key] = sent + 1;
|
||||
reminders.push(message);
|
||||
}
|
||||
if (reminders.length > 0) {
|
||||
if (!session.pending_reminders)
|
||||
session.pending_reminders = [];
|
||||
session.pending_reminders.push(`OpenWolf end-of-turn reminders:\n${reminders.map((r) => `- ${r}`).join("\n")}`);
|
||||
}
|
||||
// Idempotent ledger write: the entry for this session id is REPLACED, not
|
||||
// appended — Stop fires every turn, and appending per turn is what used to
|
||||
// duplicate sessions and quadratically inflate lifetime totals.
|
||||
const entry = buildSessionEntry(session, hookInput.transcript_path);
|
||||
// Verified delivery (2.2): the transcript records every hook invocation as
|
||||
// an attachment line; that is ground truth for what fired, failed, and what
|
||||
// context actually reached the model — self-reported counters are estimates.
|
||||
if (hookInput.transcript_path) {
|
||||
const verified = verifyHookDelivery(hookInput.transcript_path);
|
||||
if (verified)
|
||||
entry.verified = verified;
|
||||
}
|
||||
flushSessionToLedger(wolfDir, entry);
|
||||
writeJSON(sessionFile, session);
|
||||
process.exit(0);
|
||||
}
|
||||
/**
|
||||
* Check if files were edited multiple times but buglog.json wasn't updated.
|
||||
* Emit a stderr reminder so Claude sees it in the next turn.
|
||||
* Returns a reminder string if action is needed, otherwise null.
|
||||
*/
|
||||
function checkForMissingBugLogs(wolfDir, session) {
|
||||
if (!session.edit_counts)
|
||||
return;
|
||||
return null;
|
||||
const multiEditFiles = Object.entries(session.edit_counts)
|
||||
.filter(([, count]) => count >= 3)
|
||||
.map(([file]) => path.basename(file));
|
||||
if (multiEditFiles.length === 0)
|
||||
return;
|
||||
// Check if buglog was written to this session
|
||||
const buglogWritten = session.files_written.some(w => w.file.includes("buglog.json"));
|
||||
if (!buglogWritten) {
|
||||
process.stderr.write(`⚠️ OpenWolf: Files edited 3+ times this session (${multiEditFiles.join(", ")}) but buglog.json was not updated. If you fixed bugs, please log them.\n`);
|
||||
return null;
|
||||
let buglogWritten = false;
|
||||
try {
|
||||
const stat = fs.statSync(path.join(wolfDir, "buglog.json"));
|
||||
const sessionStartMs = session.started ? Date.parse(session.started) : 0;
|
||||
buglogWritten = sessionStartMs > 0 && stat.mtimeMs >= sessionStartMs;
|
||||
}
|
||||
catch { }
|
||||
if (!buglogWritten) {
|
||||
return `ACTION REQUIRED: Files edited 3+ times this session (${multiEditFiles.join(", ")}) but buglog.json was not updated. Log the bug fixes to .wolf/buglog.json now.`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
/**
|
||||
* Check if STATUS.md is older than the session start AND there was meaningful
|
||||
* code activity (3+ writes outside .wolf/). If so, nudge Claude to update
|
||||
* STATUS.md so the next /clear has fresh handoff context.
|
||||
*/
|
||||
// (The STATUS.md staleness nag was removed in 2.1: STATUS.md is regenerated
|
||||
// on demand by the /handoff skill instead of being nagged about every turn.)
|
||||
/**
|
||||
* Check if cerebrum.md was updated recently. If it hasn't been updated in
|
||||
* a while and there was significant activity, emit a gentle reminder.
|
||||
* a while and there was significant activity, return a reminder.
|
||||
*/
|
||||
function checkCerebrumFreshness(wolfDir, session) {
|
||||
const cerebrumPath = path.join(wolfDir, "cerebrum.md");
|
||||
try {
|
||||
const stat = fs.statSync(cerebrumPath);
|
||||
const hoursSinceUpdate = (Date.now() - stat.mtimeMs) / (1000 * 60 * 60);
|
||||
// If cerebrum hasn't been updated in 24h+ and there were significant writes
|
||||
if (hoursSinceUpdate > 24 && session.files_written.length >= 3) {
|
||||
process.stderr.write(`💡 OpenWolf: cerebrum.md hasn't been updated in ${Math.floor(hoursSinceUpdate)}h. Did you learn any user preferences, conventions, or gotchas this session? Consider updating .wolf/cerebrum.md.\n`);
|
||||
return `ACTION REQUIRED: cerebrum.md hasn't been updated in ${Math.floor(hoursSinceUpdate)}h and ${session.files_written.length} files were modified. Update .wolf/cerebrum.md with any new user preferences, conventions, or gotchas discovered this session.`;
|
||||
}
|
||||
}
|
||||
catch {
|
||||
// cerebrum.md doesn't exist, that's ok
|
||||
}
|
||||
return null;
|
||||
}
|
||||
/**
|
||||
* Validate .wolf/buglog.json on disk and, if it's well-formed and grew
|
||||
* (or stayed the same) relative to the last commit, checkpoint it as its
|
||||
* own standalone commit. Never commits a shrinking or malformed file —
|
||||
* that would enshrine data loss (or corruption) into git history, which
|
||||
* is exactly the failure mode this guard exists to catch before it repeats.
|
||||
* Check if a semantic summary was written to memory.md this session.
|
||||
* Returns a reminder string if action is needed, otherwise null.
|
||||
*/
|
||||
function checkAndCheckpointBuglog(wolfDir, projectRoot) {
|
||||
const buglogPath = path.join(wolfDir, "buglog.json");
|
||||
if (!fs.existsSync(buglogPath))
|
||||
return;
|
||||
let raw;
|
||||
try {
|
||||
raw = fs.readFileSync(buglogPath, "utf-8");
|
||||
}
|
||||
catch {
|
||||
return;
|
||||
}
|
||||
let parsed;
|
||||
try {
|
||||
parsed = JSON.parse(raw);
|
||||
}
|
||||
catch (e) {
|
||||
process.stderr.write(`🛑 OpenWolf: .wolf/buglog.json is not valid JSON (${e.message}). NOT checkpointing — fix this before it's lost for good.\n`);
|
||||
return;
|
||||
}
|
||||
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed) || !Array.isArray(parsed.bugs)) {
|
||||
process.stderr.write(`🛑 OpenWolf: .wolf/buglog.json must be {"version":1,"bugs":[...]}, not a bare array or other shape. NOT checkpointing.\n`);
|
||||
return;
|
||||
}
|
||||
const bugs = parsed.bugs;
|
||||
const idNum = (id) => {
|
||||
const m = /^bug-(\d+)$/.exec(id || "");
|
||||
return m ? parseInt(m[1], 10) : 0;
|
||||
};
|
||||
const currentMaxId = bugs.reduce((max, b) => Math.max(max, idNum(b.id)), 0);
|
||||
const idCounts = {};
|
||||
for (const b of bugs)
|
||||
idCounts[b.id] = (idCounts[b.id] || 0) + 1;
|
||||
const dupes = Object.keys(idCounts).filter((id) => idCounts[id] > 1);
|
||||
if (dupes.length > 0) {
|
||||
process.stderr.write(`⚠️ OpenWolf: buglog.json has duplicate ids (${dupes.slice(0, 5).join(", ")}${dupes.length > 5 ? ", ..." : ""}). Before adding a new bug, search for an existing entry with the same file+root_cause and bump its occurrences instead of appending a new one under a colliding or fresh id.\n`);
|
||||
}
|
||||
let lastCount = null;
|
||||
let lastMaxId = null;
|
||||
try {
|
||||
const committed = execFileSync("git", ["show", "HEAD:.wolf/buglog.json"], {
|
||||
cwd: projectRoot,
|
||||
encoding: "utf-8",
|
||||
stdio: ["pipe", "pipe", "pipe"],
|
||||
});
|
||||
const lastParsed = JSON.parse(committed);
|
||||
const lastBugs = Array.isArray(lastParsed) ? lastParsed : (lastParsed.bugs || []);
|
||||
lastCount = lastBugs.length;
|
||||
lastMaxId = lastBugs.reduce((max, b) => Math.max(max, idNum(b.id)), 0);
|
||||
}
|
||||
catch {
|
||||
// No committed version yet (or git unavailable) — nothing to compare against.
|
||||
}
|
||||
if (lastCount !== null && bugs.length < lastCount) {
|
||||
process.stderr.write(`🛑 OpenWolf: buglog.json SHRANK from ${lastCount} to ${bugs.length} bugs since the last commit. NOT auto-committing — this looks like data loss, investigate (check git history / other worktrees) before committing manually.\n`);
|
||||
return;
|
||||
}
|
||||
if (lastMaxId !== null && currentMaxId < lastMaxId && bugs.length > 0) {
|
||||
process.stderr.write(`🛑 OpenWolf: buglog.json's highest id (bug-${currentMaxId}) is lower than the last commit's (bug-${lastMaxId}) — looks like the id counter was reset. NOT auto-committing.\n`);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const status = execFileSync("git", ["status", "--porcelain", "--", ".wolf/buglog.json"], {
|
||||
cwd: projectRoot,
|
||||
encoding: "utf-8",
|
||||
});
|
||||
if (!status.trim())
|
||||
return; // nothing changed since last commit
|
||||
execFileSync("git", ["add", ".wolf/buglog.json"], { cwd: projectRoot });
|
||||
execFileSync("git", ["commit", "-m", `chore(wolf): checkpoint buglog.json (${bugs.length} bugs)`], { cwd: projectRoot });
|
||||
process.stderr.write(`✅ OpenWolf: checkpointed buglog.json (${bugs.length} bugs) to git.\n`);
|
||||
}
|
||||
catch (e) {
|
||||
process.stderr.write(`⚠️ OpenWolf: buglog.json is valid but the checkpoint commit failed: ${e.message}\n`);
|
||||
function checkSemanticSummaries(wolfDir, session) {
|
||||
const writeCount = session.files_written.length;
|
||||
if (writeCount < 2)
|
||||
return null;
|
||||
const semanticCount = countSemanticEntries(wolfDir);
|
||||
if (semanticCount === 0) {
|
||||
return `ACTION REQUIRED: ${writeCount} files were modified this session but no semantic summary was written to memory.md. Append a one-line summary: | HH:MM | description | file(s) | outcome | ~tokens |`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
// Run only when executed as a hook script — never on import (tests import
|
||||
// from this module, and main() exits the process).
|
||||
import { pathToFileURL } from "node:url";
|
||||
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
||||
hookMain("stop", main);
|
||||
}
|
||||
main().catch(() => process.exit(0));
|
||||
//# sourceMappingURL=stop.js.map
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
# Identity
|
||||
|
||||
- **Name:** persistent_ha
|
||||
- **Role:** AI development assistant for persistent_ha
|
||||
- **Tone:** Direct, concise, technically precise
|
||||
- **Constraints:**
|
||||
- Never modify .env or secret files without explicit user confirmation
|
||||
- Never delete files without explicit user confirmation
|
||||
- Always explain why before making architectural changes
|
||||
2162
.wolf/memory.md
2162
.wolf/memory.md
File diff suppressed because it is too large
Load diff
|
|
@ -1,30 +0,0 @@
|
|||
{
|
||||
"generated_at": "2026-08-15T13:06:46.441Z",
|
||||
"achievements": [
|
||||
"scheduler-core spec: extracted xce_sched_core_t as a controller-I/O-free scheduling engine, implemented the builtin_fcfs picker plugin behind a stable xce_job_picker_iface_t, and wired it through 15+ rounds of TDD with a link-time isolation test (test_core_isolation.c) proving zero controller coupling",
|
||||
"scheduler-core: built a full session-replay subsystem (recording.c/recording_events.c/recording_session.c) with hardened path validation that closed two real CWE-59 symlink/hardlink-alias vulnerabilities (found and fixed across 3 review rounds), plus a simulation-mode replay engine (sim.c/sim_loop.c/sim_verify.c) and an interactive REPL debugger (repl.c/repl_break.c/repl_info.c/repl_params.c) with breakpoints and equivalence testing against real recorded sessions",
|
||||
"scheduler-core: added OPCode_schedAdmin end-to-end — scheduler-side handler, controller-side dispatch/timeout, and xceadmin CLI (`scheduler record start/stop/status`, `scheduler dump`) — closing the loop for remote recording control",
|
||||
"component-failover-adoption: unified the scheduler's multi-candidate connect/rejoin pipeline (decision-publish retry-until-success, decision redelivery, backpressure handling) and rewrote the apiserver's Go-side candidate/reconnect logic (candidates.go, clib.go, reconnect.go, heartbeat.go) with 100+ new Go unit tests, all verified against real xce-build.sh ASAN builds",
|
||||
"Documentation integrity sweep (Aug 8): audited and fixed 40+ drifted claims across deployment.md, api-standards.md, ha-async-io.md, and ~15 closed HA-spec design.md files (stale line citations, missing 'superseded by' notes tying old specs to raft-drive-async/ha-commit-durability/config-upgrade-snapshot)",
|
||||
"Landed and live-soak-verified several concrete bug fixes: bug-1462 (config-replication commit-tag clamp), bug-1454/1456 (scheduler --peers dangling-pointer regression), and multiple xceadmin config-tool correctness fixes (resource-unit mapping, config dump/build round-trip)",
|
||||
"Caught a real production-blocking gap before shipping: /kiro-validate-impl on component-failover-adoption returned NO-GO because the apiserver never re-issues its application-level rejoin after the transport layer recovers from a controller failover (bug-1547/1548) — validation process worked as intended"
|
||||
],
|
||||
"improvements": [
|
||||
"Fix bug-1548: wire apiserver's heartbeat/health-check loop to call checkAndPerformRejoin() on transport-reconnect, not only on candidate-generation change — this is the sole blocker for component-failover-adoption's GO",
|
||||
"Environment fragility is now a repeat offender: bug-1439 (concurrent xce-build.sh runs clobbering the shared xce.build dir) and bug-918 (nix capnpc hash-pin drift) have each recurred 3+ times across sessions — worth a durable fix (per-invocation build dirs or a build lockfile) instead of continuing to manually retry",
|
||||
"Apply the Unity nested-BEGIN/END fix (UnitySetTestFile) as a lint/CI check rather than a one-off sweep, since it was masking real test failures across ~80 controller test files until caught",
|
||||
"scheduler-core's REPL/sim modules (repl_info.c, repl_break.c, sim_loop.c) are growing fast under the 800-line-per-file convention — worth a periodic split-check as the spec approaches closure"
|
||||
],
|
||||
"next_tasks": [
|
||||
"Land the apiserver rejoin-after-reconnect fix for bug-1548 and re-run /kiro-validate-impl on component-failover-adoption to flip it to GO",
|
||||
"Continue scheduler-core to closure — remaining REPL/sim equivalence coverage and a final full-suite + live-HA-soak validation pass before marking implementation-complete",
|
||||
"Resume the scaffolded-but-not-started specs: ha-client-multimaster and admin-config-tool (both have requirements.md but no design/tasks yet)",
|
||||
"Check status of clusterconfig-schema-split (tasks.md exists) — confirm whether it's still blocked or ready for implementation given recent config-replication changes"
|
||||
],
|
||||
"risks": [
|
||||
"component-failover-adoption is currently NO-GO in production terms: shipping without the apiserver rejoin fix leaves a real HA failover gap (apiserver silently stops recovering after a controller election)",
|
||||
"Repeated build/toolchain fragility (nix capnpc pinning, concurrent xce-build.sh races) has cost multiple sessions of lost time and produced false negatives in validation runs — this is now a recurring operational risk, not a one-off",
|
||||
"The buglog has grown very large (1500+ entries) with a number of items explicitly logged as 'not fixed, out of scope, needs owner' — worth confirming these aren't silently accumulating into an unaddressed backlog",
|
||||
"scheduler-core is one of the largest specs run (30+ tasks across many subagent sessions); integration risk is high where it intersects with the concurrently-evolving component-failover-adoption and HA specs — needs a dedicated cross-spec regression pass before both are declared done"
|
||||
]
|
||||
}
|
||||
892621
.wolf/token-ledger.json
892621
.wolf/token-ledger.json
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue