Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@ All notable changes to Agent Wallclock are documented here.

Format loosely follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Versioning starts at **0.1.0** for the first public QA-ready release.

## [Unreleased]

### Added

- **CLI:** `wallclock circadian on|off|status` — opt-in Circadian context in Temporal Briefing (default off)
- **Core:** `config.json` prefs (`circadianEnabled`) separate from `store.json`; `classifyCircadian` fixed local-hour bands
- **Briefing:** optional `## Circadian` block (band / day / non-moralizing tone hint) when enabled; compact line `circadian <band> <dayKind>`

## [0.1.0] — 2026-07-31

First post-QA hardening release batch (`fix/pass-hardening-100`).
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ wallclock effort start auth-rewrite
wallclock session start
wallclock brief # print
wallclock brief --copy # copy to clipboard when available
wallclock circadian on # opt-in: add Circadian band/tone to brief (default off)
```

Example output (real CLI shape; durations use `d`/`h`/`m`/`s`):
Expand Down Expand Up @@ -168,6 +169,7 @@ When the skill/rule is on, Cursor should call `get_briefing` (or run the CLI) in
|---------|---------|
| `wallclock now` | Local date, time, timezone, weekday, ISO |
| `wallclock brief` | Full Temporal Briefing (`--copy`, `--json`, `--compact`) |
| `wallclock circadian on\|off\|status` | Opt-in Circadian block in briefing (default off; prefs in `config.json`) |
| `wallclock doctor [--repair]` | Health checks (store, builds, permissions) |
| `wallclock where` | Show store path and config hints |
| `wallclock --version` | Package version |
Expand Down Expand Up @@ -204,7 +206,7 @@ Full help: `wallclock --help`. Troubleshooting: [`TROUBLESHOOTING.md`](TROUBLESH

## Privacy

- State lives only under `~/.agent-wallclock/` (JSON; directory `0700`, file `0600` when the OS allows).
- State lives only under `~/.agent-wallclock/` (`store.json` ledger + optional `config.json` prefs; directory `0700`, file `0600` when the OS allows).
- CLI and MCP make **no network calls**.
- **Clipboard:** `wallclock brief --copy` puts briefing text on your local clipboard; you choose when to paste.
- **Paste trust:** pasting a briefing into ChatGPT, Claude, or similar **uploads** that time data to the host cloud.
Expand Down
3 changes: 2 additions & 1 deletion adapters/chatgpt-custom-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ Rules:
- Check **Generated at** / **Stale after**. If **Generated-at** is older than the stated freshness window (default 15 minutes), ask the user to paste a refreshed `wallclock brief` before making time-based claims.
- Never invent time of day or how long the user has been working.
- If duration or clock fields are missing, say unknown — do not guess.
- Do not tell the user to go to sleep or that they have been at something for days/hours unless the briefing supports it.
- Do not tell the user to go to sleep or that they have been at something for days/hours unless the briefing supports it. Never issue sleep/rest commands from guessed time.
- If a **Circadian** section is present in a fresh briefing, use band/day/tone for reply energy; if absent, do not invent circadian state.
- A new chat is not a new project if an active effort shows prior logged work.

When no briefing is present, avoid time-based coaching and ask for `wallclock brief` if temporal claims matter.
Expand Down
1 change: 1 addition & 0 deletions adapters/claude-project-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ When a Temporal Briefing is available (pasted or via MCP tools `get_now` / `get_
- Use it as the only source for wall-clock time, session age, and effort duration.
- Check **Generated at** / **Stale after**. If **Generated-at** is older than the stated freshness window (default 15 minutes), call `get_briefing` again or ask for a refreshed paste before time-based advice.
- Never invent circadian context (“it’s late”, “go to sleep”) against the briefing’s local time.
- If a **Circadian** section is present in a fresh briefing, use band/day/tone for reply energy; if absent, do not invent circadian state. Never issue sleep/rest commands from guessed time.
- Never invent session length (“you’ve been at this for days”) against session age.
- Never treat a fresh thread as zero history when an effort has accumulated logged time.
- Missing fields → say unknown or ask; never guess.
Expand Down
7 changes: 4 additions & 3 deletions adapters/cursor-skill/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,9 @@ description: >-
- session age (“hours”, “days”)
- effort history on multi-week work
4. If briefing data is missing, say **unknown** — never guess from priors.
5. Do not advise sleep or “you’ve done enough” based on invented duration.
6. Write tools (`start_effort`, `log_session`) require `AGENT_WALLCLOCK_WRITES=1` — default off.
5. Do not advise sleep or “you’ve done enough” based on invented duration. Never issue sleep/rest commands from guessed time.
6. If a **Circadian** section is present in a fresh briefing, use band/day/tone for reply energy; if absent, do not invent circadian state.
7. Write tools (`start_effort`, `log_session`) require `AGENT_WALLCLOCK_WRITES=1` — default off.

## Install skill

Expand All @@ -38,5 +39,5 @@ Add the rule fragment from [`rule.md`](rule.md) to Cursor user rules if desired.
## Rule fragment (user rules)

```
Temporal context: Use Agent Wallclock. Never invent clock time, session age, or effort duration. Prefer MCP get_briefing or npm exec / absolute node path — not bare wallclock. Refresh if stale; missing fields are unknown.
Temporal context: Use Agent Wallclock. Never invent clock time, session age, or effort duration. Prefer MCP get_briefing or npm exec / absolute node path — not bare wallclock. Refresh if stale; missing fields are unknown. Use Circadian only when present; never invent circadian state or sleep advice.
```
2 changes: 1 addition & 1 deletion adapters/cursor-skill/rule.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# Cursor user rule fragment — Agent Wallclock

Temporal context: Use Agent Wallclock. Never invent clock time, session age, or effort duration. **MCP-first:** call `get_briefing` (or `get_session_status` / `get_timeline`) when the server is configured. Otherwise run `npm exec -w @agent-wallclock/cli -- wallclock brief` or `node /ABSOLUTE/PATH/TO/agent-wallclock/packages/cli/dist/bin.js brief` — do not assume bare `wallclock` is on PATH. Refresh if **Generated at** is outside the freshness window (default 15m). If fields are missing, say unknown. Do not invent sleep/rest advice from guessed time.
Temporal context: Use Agent Wallclock. Never invent clock time, session age, or effort duration. **MCP-first:** call `get_briefing` (or `get_session_status` / `get_timeline`) when the server is configured. Otherwise run `npm exec -w @agent-wallclock/cli -- wallclock brief` or `node /ABSOLUTE/PATH/TO/agent-wallclock/packages/cli/dist/bin.js brief` — do not assume bare `wallclock` is on PATH. Refresh if **Generated at** is outside the freshness window (default 15m). If fields are missing, say unknown. If a Circadian section is present, use band/day/tone for reply energy; if absent, do not invent circadian state. Never invent sleep/rest advice from guessed time.
9 changes: 5 additions & 4 deletions adapters/generic-system-prompt.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,15 @@ Paste this into any model’s system prompt or custom instructions. When a Tempo
2. Check **Generated at** / **Stale after**. If **Generated-at** is older than the stated freshness window (default 15 minutes), request a refreshed briefing before time-based claims.
3. Never invent the time of day, how long the user has been working in this chat, or how long a project has been underway.
4. If briefing fields are missing, say **unknown** or ask — do not guess from training priors.
5. Do not advise the user to sleep, stop, or “take a break because it is late” unless the briefing’s local time and session/effort data support that claim.
6. Do not equate a new chat with a new project when an active effort shows multi-day logged work.
7. Prefer precise durations from the briefing (e.g. “12 minutes”) over vague claims (“hours”, “days”) when numbers are available.
5. Do not advise the user to sleep, stop, or “take a break because it is late” unless the briefing’s local time and session/effort data support that claim. Never issue sleep/rest commands from guessed time.
6. If a **Circadian** section is present in a fresh briefing, use its band/day/tone for reply energy. If absent, do not invent circadian state.
7. Do not equate a new chat with a new project when an active effort shows multi-day logged work.
8. Prefer precise durations from the briefing (e.g. “12 minutes”) over vague claims (“hours”, “days”) when numbers are available.

## When no briefing is available

Say that wall-clock and effort duration are unknown, and suggest the user run `wallclock brief` (or call `get_briefing` via MCP) before making time-based claims.

## MODEL_RULES alignment

Trust only the Temporal Briefing for clock, session age, and effort duration. Never invent time. Missing fields → unknown. Refresh if stale.
Trust only the Temporal Briefing for clock, session age, and effort duration. Never invent time. If a Circadian block is present, tone may follow it; if absent, do not invent circadian state. Never moralize about sleep from the clock alone. Missing fields → unknown. Refresh if stale.
7 changes: 5 additions & 2 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ Agent Wallclock is a **local-only** temporal context layer for language models.
┌───────────────────────────▼─────────────────────────────────┐
│ ~/.agent-wallclock/store.json (+ store.lock) │
│ ~/.agent-wallclock/config.json (prefs; e.g. circadian) │
│ system clock (Date / Intl) │
└─────────────────────────────────────────────────────────────┘
```
Expand All @@ -36,9 +37,10 @@ Agent Wallclock is a **local-only** temporal context layer for language models.

- **Location:** `~/.agent-wallclock/` by default; override with `AGENT_WALLCLOCK_HOME`.
- **File:** `store.json` — efforts, sessions, active pointers, schema version.
- **Prefs:** `config.json` — local toggles (e.g. `circadianEnabled`); separate from the ledger so effort backups do not flip prefs.
- **Lock:** `store.lock` — exclusive lock for atomic read-modify-write (CLI + MCP).
- **Permissions:** directory `0700`, file `0600` (best effort on Unix).
- **Backup/restore:** `wallclock store backup|restore` for user-managed snapshots.
- **Backup/restore:** `wallclock store backup|restore` for user-managed snapshots of `store.json`.

Store operations are synchronous and local. Corruption triggers `StoreCorruptError`; `wallclock doctor --repair` attempts normalization.

Expand All @@ -50,8 +52,9 @@ The **Temporal Briefing** is markdown (or JSON with `--json`) built from:
2. Open session age (if any)
3. Active effort logged time and calendar age
4. **Freshness** metadata: `Generated at`, `Stale after` (default 15 minutes via `AGENT_WALLCLOCK_STALE_AFTER_MS`)
5. Optional **Circadian** block (band / weekday-weekend / tone hint) — only when `wallclock circadian on` has set `circadianEnabled` in `config.json` (default off)

`MODEL_RULES` in the briefing instruct models to trust the briefing only while fresh and never invent durations.
`MODEL_RULES` in the briefing instruct models to trust the briefing only while fresh, never invent durations, and use Circadian tone only when that section is present.

Adapters in `adapters/` propagate the same rules into host-specific instructions.

Expand Down
44 changes: 44 additions & 0 deletions packages/cli/src/bin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,48 @@ describe("wallclock CLI", () => {
const usage = run(["effort", "log"]);
assert.equal(usage.status, 2, usage.stderr || usage.stdout);
});

it("circadian defaults off and opt-in adds briefing block", () => {
home = mkdtempSync(join(tmpdir(), "wallclock-cli-test-"));
assert.equal(run(["init"]).status, 0);

const statusOff = run(["circadian", "status"]);
assert.equal(statusOff.status, 0, statusOff.stderr || statusOff.stdout);
assert.match(statusOff.stdout, /circadian: off/);

const briefOff = run(["brief"]);
assert.equal(briefOff.status, 0, briefOff.stderr || briefOff.stdout);
assert.doesNotMatch(briefOff.stdout, /## Circadian/);

const on = run(["circadian", "on"]);
assert.equal(on.status, 0, on.stderr || on.stdout);
assert.match(on.stdout, /circadian: on/);

const statusOn = run(["circadian", "status"]);
assert.equal(statusOn.status, 0, statusOn.stderr || statusOn.stdout);
assert.match(statusOn.stdout, /circadian: on/);
assert.match(statusOn.stdout, /band: /);
assert.match(statusOn.stdout, /day: /);

const briefOn = run(["brief"]);
assert.equal(briefOn.status, 0, briefOn.stderr || briefOn.stdout);
assert.match(briefOn.stdout, /## Circadian/);
assert.match(briefOn.stdout, /Band: /);
assert.match(briefOn.stdout, /Day: /);
assert.match(briefOn.stdout, /Tone: /);

const json = run(["brief", "--json"]);
assert.equal(json.status, 0, json.stderr || json.stdout);
const parsed = JSON.parse(json.stdout) as { circadian: { band: string } | null };
assert.ok(parsed.circadian);
assert.ok(parsed.circadian?.band);

const compact = run(["brief", "--compact"]);
assert.equal(compact.status, 0, compact.stderr || compact.stdout);
assert.match(compact.stdout, /circadian \w+ \w+/);

assert.equal(run(["circadian", "off"]).status, 0);
const briefAgain = run(["brief"]);
assert.doesNotMatch(briefAgain.stdout, /## Circadian/);
});
});
54 changes: 49 additions & 5 deletions packages/cli/src/bin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ import {
restoreStore,
runDoctor,
loadConfig,
loadPrefs,
savePrefs,
classifyCircadian,
classifyError,
CliError,
ExitCode,
Expand Down Expand Up @@ -66,6 +69,7 @@ function printHelp(): void {
Usage:
wallclock now
wallclock brief [--copy] [--json] [--compact]
wallclock circadian on|off|status
wallclock effort start <name>
wallclock effort list [--json] [--all]
wallclock effort status [name]
Expand Down Expand Up @@ -93,6 +97,8 @@ Examples:
wallclock session start
wallclock brief --copy
wallclock brief --json | jq .generatedAt
wallclock circadian on
wallclock circadian status
wallclock timeline 10 --effort auth-rewrite
wallclock mcp-config --print cursor --check
wallclock doctor
Expand Down Expand Up @@ -173,20 +179,25 @@ function cmdNow(): void {
console.log(`ISO (UTC): ${now.iso}`);
}

function briefingOpts() {
return { storeDir: STORE_DIR };
}

function cmdBrief(args: string[]): void {
const copy = takeFlag(args, "--copy");
const asJson = takeFlag(args, "--json");
const compact = takeFlag(args, "--compact");
const store = loadStore(STORE_DIR);
const opts = briefingOpts();

let text: string;
if (asJson) {
const input = buildBriefingInput(store);
const input = buildBriefingInput(store, opts);
text = `${JSON.stringify({ ...input, modelRules: MODEL_RULES }, null, 2)}\n`;
} else if (compact) {
text = `${renderBriefingCompact(store)}\n`;
text = `${renderBriefingCompact(store, opts)}\n`;
} else {
text = renderBriefing(store);
text = renderBriefing(store, opts);
}

if (copy) {
Expand Down Expand Up @@ -638,16 +649,43 @@ function cmdInit(): void {
}
}

function cmdCircadian(args: string[]): void {
const action = args[0];
if (action === "on") {
savePrefs({ circadianEnabled: true }, STORE_DIR);
console.log("circadian: on");
return;
}
if (action === "off") {
savePrefs({ circadianEnabled: false }, STORE_DIR);
console.log("circadian: off");
return;
}
if (action === "status") {
const enabled = loadPrefs(STORE_DIR).circadianEnabled;
console.log(`circadian: ${enabled ? "on" : "off"}`);
if (enabled) {
const ctx = classifyCircadian();
console.log(`band: ${ctx.band}`);
console.log(`day: ${ctx.dayKind}`);
console.log(`local hour: ${String(ctx.localHour).padStart(2, "0")}`);
}
return;
}
throw new CliError("Usage: wallclock circadian on|off|status", ExitCode.USAGE);
}

function cmdCompletion(args: string[]): void {
const shell = args[0];
if (shell === "bash") {
console.log(`# wallclock bash completion
_wallclock() {
local cur="\${COMP_WORDS[COMP_CWORD]}"
local cmds="now brief effort session timeline store doctor where mcp-config init completion help"
local cmds="now brief circadian effort session timeline store doctor where mcp-config init completion help"
local effort_subs="start list status log rename archive unarchive delete"
local session_subs="start end status"
local store_subs="backup restore"
local circadian_subs="on off status"
if [[ \${COMP_CWORD} -eq 1 ]]; then
COMPREPLY=( $(compgen -W "\$cmds" -- "\$cur") )
elif [[ \${COMP_WORDS[1]} == effort && \${COMP_CWORD} -eq 2 ]]; then
Expand All @@ -656,6 +694,8 @@ _wallclock() {
COMPREPLY=( $(compgen -W "\$session_subs" -- "\$cur") )
elif [[ \${COMP_WORDS[1]} == store && \${COMP_CWORD} -eq 2 ]]; then
COMPREPLY=( $(compgen -W "\$store_subs" -- "\$cur") )
elif [[ \${COMP_WORDS[1]} == circadian && \${COMP_CWORD} -eq 2 ]]; then
COMPREPLY=( $(compgen -W "\$circadian_subs" -- "\$cur") )
elif [[ \${COMP_WORDS[1]} == mcp-config ]]; then
COMPREPLY=( $(compgen -W "--print --check claude cursor vscode" -- "\$cur") )
elif [[ \${COMP_WORDS[1]} == completion ]]; then
Expand All @@ -670,12 +710,13 @@ complete -F _wallclock wallclock
console.log(`#compdef wallclock
_wallclock() {
local -a cmds
cmds=(now brief effort session timeline store doctor where mcp-config init completion help)
cmds=(now brief circadian effort session timeline store doctor where mcp-config init completion help)
_arguments '1:command:(\${cmds})' '*::arg:->args'
case \$words[1] in
effort) _values 'effort' start list status log rename archive unarchive delete ;;
session) _values 'session' start end status ;;
store) _values 'store' backup restore ;;
circadian) _values 'circadian' on off status ;;
mcp-config) _values 'mcp' --print --check claude cursor vscode ;;
completion) _values 'shell' bash zsh ;;
esac
Expand Down Expand Up @@ -715,6 +756,9 @@ function main(argv: string[]): void {
case "brief":
cmdBrief(rest);
break;
case "circadian":
cmdCircadian(rest);
break;
case "effort":
cmdEffort(rest);
break;
Expand Down
3 changes: 2 additions & 1 deletion packages/core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,12 @@ Core library for Agent Wallclock: system clock, local JSON store, efforts, sessi
- `loadStore`, `updateStore`, `initStore`, `backupStore`, `restoreStore`
- Effort and session operations (`startEffort`, `startSession`, `endSession`, …)
- `renderBriefing`, `renderBriefingCompact`, `buildBriefingInput`, `MODEL_RULES`
- `classifyCircadian`, `loadPrefs`, `savePrefs` (opt-in circadian; `config.json`)
- `runDoctor`, `loadConfig`, typed errors (`CliError`, `StoreCorruptError`, …)

## Store

Default directory: `~/.agent-wallclock/` (`store.json`, `store.lock`). Override with `AGENT_WALLCLOCK_HOME`.
Default directory: `~/.agent-wallclock/` (`store.json`, `store.lock`, optional `config.json` prefs). Override with `AGENT_WALLCLOCK_HOME`.

## Config (environment)

Expand Down
Loading