argus-decision-mcp 1.0.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +130 -20
- package/SECURITY.md +22 -8
- package/dist/lib/ambient-due.js +63 -0
- package/dist/lib/argus-dir.js +50 -1
- package/dist/lib/calendar.js +75 -0
- package/dist/lib/canonical-scales.js +105 -0
- package/dist/lib/due-note.js +62 -13
- package/dist/lib/envelope.js +19 -4
- package/dist/lib/install-id.js +53 -0
- package/dist/lib/layout.js +7 -0
- package/dist/lib/ledger-append.js +134 -3
- package/dist/lib/ledger-replay.js +120 -8
- package/dist/lib/locale.js +45 -0
- package/dist/lib/log.js +2 -2
- package/dist/lib/numeric-drift.js +343 -22
- package/dist/lib/package-meta.js +14 -0
- package/dist/lib/premise-sync.js +23 -0
- package/dist/lib/premises-core.js +174 -0
- package/dist/lib/premises.js +51 -48
- package/dist/lib/privacy.js +1 -1
- package/dist/lib/push-account.js +25 -2
- package/dist/lib/receipt.js +5 -1
- package/dist/lib/render-receipt.js +32 -22
- package/dist/lib/resolve-today.js +11 -3
- package/dist/lib/review/extract-core.js +229 -0
- package/dist/lib/review/extract-file-node.js +172 -0
- package/dist/lib/review/prompts.js +65 -3
- package/dist/lib/review/render.js +86 -38
- package/dist/lib/review/schema.js +4 -1
- package/dist/lib/review-path.js +108 -0
- package/dist/lib/spine.js +15 -3
- package/dist/lib/state-machine.js +54 -8
- package/dist/lib/surface-lint.js +97 -0
- package/dist/lib/surfaces.js +307 -38
- package/dist/lib/telemetry.js +169 -0
- package/dist/lib/untrusted.js +89 -0
- package/dist/lib/validate-crux.js +32 -7
- package/dist/server.js +23 -14
- package/dist/tools/amend-dismiss.js +46 -9
- package/dist/tools/candidates.js +129 -0
- package/dist/tools/check-in.js +165 -19
- package/dist/tools/index.js +3 -1
- package/dist/tools/init-config.js +49 -5
- package/dist/tools/open-decision.js +20 -18
- package/dist/tools/premises.js +239 -22
- package/dist/tools/recall.js +79 -15
- package/dist/tools/recheck.js +49 -13
- package/dist/tools/review.js +116 -19
- package/dist/tools/seal.js +128 -28
- package/dist/tools/settle.js +161 -18
- package/dist/tools/sync.js +245 -13
- package/dist/tools/watch.js +117 -0
- package/dist/v2/bridge.js +240 -0
- package/dist/v2/brief.js +122 -0
- package/dist/v2/events.js +187 -0
- package/dist/v2/evidence.js +51 -0
- package/dist/v2/gate-keywords.js +48 -0
- package/dist/v2/gate.js +98 -0
- package/dist/v2/git-discovery.js +80 -0
- package/dist/v2/harvest.js +147 -0
- package/dist/v2/init.js +42 -0
- package/dist/v2/ledger.js +290 -0
- package/dist/v2/lifecycle.js +122 -0
- package/dist/v2/logbook.js +142 -0
- package/dist/v2/mirror.js +305 -0
- package/dist/v2/outbox.js +52 -0
- package/dist/v2/queue.js +124 -0
- package/dist/v2/reducer.js +462 -0
- package/dist/v2/sanitize.js +44 -0
- package/dist/v2/v1-reader.js +297 -0
- package/package.json +17 -6
- package/snippets/claude-code-watch.md +47 -0
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# argus-decision-mcp
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/argus-decision-mcp)
|
|
4
|
+
[](https://www.npmjs.com/package/argus-decision-mcp)
|
|
5
|
+
[](./LICENSE)
|
|
6
|
+
|
|
3
7
|
> **Your AI gives you an answer. Argus gives you a receipt — and checks it against reality on the date you set.**
|
|
4
8
|
|
|
5
9
|
Argus is an MCP server for **decision accountability**. Instead of grading your
|
|
@@ -9,7 +13,9 @@ what actually happened. The artifact it produces — a **Judgment Receipt** —
|
|
|
9
13
|
carries one line no other AI tool will: `AI VERDICT … NONE`. The model never
|
|
10
14
|
graded you. Reality did.
|
|
11
15
|
|
|
12
|
-
|
|
16
|
+
Runs on any MCP host that supports local **stdio** servers — Claude Desktop,
|
|
17
|
+
Claude Code, and other clients that launch a local process. (Remote-only
|
|
18
|
+
connectors that require an HTTP transport aren't supported yet — see the roadmap.)
|
|
13
19
|
|
|
14
20
|
```
|
|
15
21
|
┌─ ARGUS · JUDGMENT RECEIPT ────────────────────────────────┐
|
|
@@ -32,6 +38,14 @@ Works with any MCP host — Claude, ChatGPT, Gemini, or any MCP-compatible clien
|
|
|
32
38
|
|
|
33
39
|
## Why it's different
|
|
34
40
|
|
|
41
|
+
**It is not a receipt of what your *agent* did.** A growing set of tools now
|
|
42
|
+
log an AI coding run — prompt captured, files touched, checkpoint saved, replay
|
|
43
|
+
path written down. That receipt is about the *machine's* actions over one run.
|
|
44
|
+
Argus receipts something no run-logger can: *your* judgment call, in your own
|
|
45
|
+
words, opened again on a date you set and settled against **reality** — not
|
|
46
|
+
against a model's opinion, and not against a diff. Machine-action receipts and
|
|
47
|
+
judgment receipts are different primitives; you can keep both.
|
|
48
|
+
|
|
35
49
|
Most decision tools compete on a *better answer*, a *score*, a *confidence*.
|
|
36
50
|
Argus does the opposite, and the opposite is enforced **structurally**, not
|
|
37
51
|
promised in prose:
|
|
@@ -39,7 +53,7 @@ promised in prose:
|
|
|
39
53
|
- **There is no verdict tool.** The model cannot grade your decision because no
|
|
40
54
|
`argus_verdict` / `argus_score` tool exists to call. `grep dist/` and see.
|
|
41
55
|
- **You can't settle what you never sealed.** `argus_settle` hard-errors without
|
|
42
|
-
a prior `argus_seal` — "no judgment without a falsifiable
|
|
56
|
+
a prior `argus_seal` — "no judgment without a falsifiable prediction" is a
|
|
43
57
|
precondition, not a suggestion.
|
|
44
58
|
- **State is the ledger, not a flag.** A decision's status is the fold of an
|
|
45
59
|
append-only event log, so it can't be faked by calling tools out of order.
|
|
@@ -52,7 +66,10 @@ Claude Code:
|
|
|
52
66
|
claude mcp add argus -- npx -y argus-decision-mcp
|
|
53
67
|
```
|
|
54
68
|
|
|
55
|
-
Or add to your host's MCP config
|
|
69
|
+
Or add to your host's MCP config. **Zero config works**: with no `env` at all,
|
|
70
|
+
your ledger lives in `~/.argus`.
|
|
71
|
+
|
|
72
|
+
**Claude Code** (expands `${CLAUDE_PROJECT_DIR}`, so a per-project ledger works):
|
|
56
73
|
|
|
57
74
|
```jsonc
|
|
58
75
|
{
|
|
@@ -61,19 +78,21 @@ Or add to your host's MCP config:
|
|
|
61
78
|
"command": "npx",
|
|
62
79
|
"args": ["-y", "argus-decision-mcp"],
|
|
63
80
|
"env": {
|
|
64
|
-
//
|
|
65
|
-
// back to it. Claude Code expands ${CLAUDE_PROJECT_DIR}; on other hosts
|
|
66
|
-
// put an absolute path here (or pass argus_dir per call).
|
|
81
|
+
// OPTIONAL — per-project ledger. Omit entirely to use ~/.argus.
|
|
67
82
|
"ARGUS_DIR": "${CLAUDE_PROJECT_DIR}/.argus",
|
|
68
83
|
// OPTIONAL — connect to your Argus account so sealed predictions get an
|
|
69
84
|
// email at their check-by date (the Companion Brief) and show up in the
|
|
70
|
-
// web dashboard. Issue the token in the web app
|
|
71
|
-
// fully local (the privacy-preserving default).
|
|
85
|
+
// web dashboard. Issue the token in the web app (Settings → sync token).
|
|
86
|
+
// Leave it unset to stay fully local (the privacy-preserving default).
|
|
72
87
|
"ARGUS_TOKEN": "argus_pat_…",
|
|
73
88
|
// OPTIONAL — the timezone that decides when a check-by date becomes
|
|
74
|
-
// "today". Unset =
|
|
75
|
-
//
|
|
76
|
-
"ARGUS_TZ": "Asia/Seoul"
|
|
89
|
+
// "today". Unset = your machine's local timezone (usually right).
|
|
90
|
+
// Set it only if your machine's clock zone isn't where you live:
|
|
91
|
+
"ARGUS_TZ": "Asia/Seoul",
|
|
92
|
+
// OPTIONAL — opt in to anonymous usage telemetry (OFF by default). Sends
|
|
93
|
+
// a random install id + which tool ran + version/platform; never your
|
|
94
|
+
// decisions or token. Honors DO_NOT_TRACK. See SECURITY.md → Telemetry.
|
|
95
|
+
"ARGUS_TELEMETRY": "1"
|
|
77
96
|
// "ARGUS_API_URL": "https://argus.voyage" // override only for self-host
|
|
78
97
|
}
|
|
79
98
|
}
|
|
@@ -81,14 +100,98 @@ Or add to your host's MCP config:
|
|
|
81
100
|
}
|
|
82
101
|
```
|
|
83
102
|
|
|
103
|
+
**Claude Desktop** does **not** expand `${...}` variables — a literal
|
|
104
|
+
`${CLAUDE_PROJECT_DIR}` would fail on every call (Argus names this error when
|
|
105
|
+
it happens). Either omit `ARGUS_DIR` (→ `~/.argus`) or use an absolute path:
|
|
106
|
+
|
|
107
|
+
```jsonc
|
|
108
|
+
// macOS
|
|
109
|
+
"env": { "ARGUS_DIR": "/Users/you/.argus" }
|
|
110
|
+
// Windows — also note the command form below
|
|
111
|
+
"env": { "ARGUS_DIR": "C:\\Users\\you\\.argus" }
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
**Windows** hosts often can't launch bare `npx` (it's `npx.cmd`). If the server
|
|
115
|
+
fails to start, use:
|
|
116
|
+
|
|
117
|
+
```jsonc
|
|
118
|
+
{
|
|
119
|
+
"command": "cmd",
|
|
120
|
+
"args": ["/c", "npx", "-y", "argus-decision-mcp"]
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
84
124
|
> `argus_dir` is **optional** on every tool: omit it and it resolves from
|
|
85
|
-
> `ARGUS_DIR`. A per-call `argus_dir` still
|
|
86
|
-
> even when env-variable interpolation
|
|
125
|
+
> `ARGUS_DIR`, then falls back to `~/.argus`. A per-call `argus_dir` still
|
|
126
|
+
> wins — so Argus works on any host even when env-variable interpolation
|
|
127
|
+
> doesn't.
|
|
128
|
+
|
|
129
|
+
## Your first receipt (2 minutes)
|
|
130
|
+
|
|
131
|
+
You never call these tools by name. Once the server is connected, you just
|
|
132
|
+
talk to your AI in plain language and it calls the right tool for you. One
|
|
133
|
+
full loop — from sealing a prediction to letting reality grade it — looks like
|
|
134
|
+
this:
|
|
135
|
+
|
|
136
|
+
**1 · Seal a prediction** — before you commit to a decision, tell your AI:
|
|
137
|
+
|
|
138
|
+
> **You:** "I'm shipping the new onboarding flow next week. Seal a prediction:
|
|
139
|
+
> signups go up at least 10% by the end of the month."
|
|
140
|
+
|
|
141
|
+
Argus records your predicate and a check-by date. Nothing is scored — it's a
|
|
142
|
+
prediction against reality, not a grade from the model.
|
|
143
|
+
|
|
144
|
+
**2 · Re-check a premise** *(optional, any time before the date)* — if a fact
|
|
145
|
+
your decision leaned on might have moved:
|
|
146
|
+
|
|
147
|
+
> **You:** "Re-check my onboarding decision — did the weekly signup number
|
|
148
|
+
> change since I sealed it?"
|
|
149
|
+
|
|
150
|
+
Argus compares the new number to the baseline and tells you if it drifted. It
|
|
151
|
+
returns the handle — whether to revisit the decision stays your call.
|
|
152
|
+
|
|
153
|
+
**3 · Settle on the check-by date** — when the date arrives:
|
|
154
|
+
|
|
155
|
+
> **You:** "Settle my onboarding prediction. Signups went up 14%."
|
|
156
|
+
|
|
157
|
+
Argus prints the **Judgment Receipt** at the top of this page — with
|
|
158
|
+
`AI VERDICT … NONE`. You made the call; reality graded it, not the model.
|
|
159
|
+
|
|
160
|
+
That's the whole spine. Everything below is detail on top of these three steps.
|
|
161
|
+
|
|
162
|
+
## Two loops
|
|
163
|
+
|
|
164
|
+
Argus runs two loops side by side — the same three habits its author
|
|
165
|
+
uses daily: **봉인하기** (seal it before you ask the AI) · **드러내기**
|
|
166
|
+
(surface claims you accepted without checking) · **돌아보기** (return when
|
|
167
|
+
reality has answered):
|
|
168
|
+
|
|
169
|
+
- **The daily watch** (`argus_watch`) — turns over once a *day*. In the morning
|
|
170
|
+
(or whenever a session starts) you anchor one line: today's aim, your working
|
|
171
|
+
hypothesis, where you lean. While you work, you capture the things that
|
|
172
|
+
usually evaporate: a claim you accepted without checking, a premise you
|
|
173
|
+
haven't verified, a question you're deferring. Tomorrow's `check_in` mirrors
|
|
174
|
+
yesterday's line back — *"so, how did it go?"* — as a question, never a grade.
|
|
175
|
+
An anchor is a **note, not a prediction**: nothing about it is scored, counted, or
|
|
176
|
+
streaked, ever.
|
|
177
|
+
- **The decision loop** (everything below) — plays out over days to months.
|
|
178
|
+
When a capture turns out to be load-bearing, *you* promote it: open the
|
|
179
|
+
decision, seal a falsifiable prediction, track its premises, and let reality
|
|
180
|
+
settle it into a Judgment Receipt.
|
|
181
|
+
|
|
182
|
+
The watch builds the habit; the decision loop builds the record.
|
|
87
183
|
|
|
88
184
|
## The loop
|
|
89
185
|
|
|
186
|
+
**The spine is three tools:** **`argus_seal`** (write the falsifiable prediction) →
|
|
187
|
+
optional **`argus_recheck`** (has a load-bearing premise moved?) →
|
|
188
|
+
**`argus_settle`** (reality answers, on the date). If you only ever learn
|
|
189
|
+
these three, Argus works. Everything else in the table supports that spine —
|
|
190
|
+
reach for it when you need it, not before.
|
|
191
|
+
|
|
90
192
|
| Tool | What it does |
|
|
91
193
|
|------|--------------|
|
|
194
|
+
| `argus_watch` | The daily watch. `anchor` keeps today's one-line aim (your words, verbatim); `capture` notes an unchecked claim / unverified premise / deferred question mid-work without opening a decision; `list` reads the recent log. Anchors are mirrored back by the next `check_in` as a question — never evaluated, never counted in any record. |
|
|
92
195
|
| `argus_open_decision` | Opens a consequential decision. Runs a restraint gate first — on a flat / low-stakes / reversible / already-closed call it tells you to leave it as is. If it fires, it surfaces **one** neutral question, never a fork or a lean. |
|
|
93
196
|
| `argus_review` | Reviews an existing document (strategy memo / PRD / deck text / AI answer) for judgment risk: reviewability score, routed lenses, source units with anchors, and the extraction prompt — then hands the analysis to you. Degrades honestly on unextractable input; never a verdict. End by sealing one follow-up. |
|
|
94
197
|
| `argus_seal` | Seals a falsifiable prediction (`predicate` + `check_by`) and captures the receipt's real-question / unverified-assumption / human-only / your-call fields. Refuses an empty predicate or a non-future date. If you seal without naming the assumption, it's recorded as an explicit **skip** — never a forced gate (which would just eject the tiredest user), never a silent blank. With `ARGUS_TOKEN` set, the prediction also syncs to your account so the Companion Brief can email you at its check-by date. |
|
|
@@ -97,7 +200,7 @@ Or add to your host's MCP config:
|
|
|
97
200
|
| `argus_settle` | On the check-by date, records what reality did and issues the Judgment Receipt. Hard-errors without a prior seal. Optionally records **which premise broke** (your attribution, never inferred) — over time your track record can say "3 of your 4 missed bets traced to a broken external premise": a frequency, never a diagnosis. |
|
|
98
201
|
| `argus_amend` | Changes the predicate or check-by date **before** reality answers — a course change, not an erasure (the original stays on the append-only ledger). Hard-errors once the decision is settled. |
|
|
99
202
|
| `argus_dismiss` | Closes a decision **without settling** — it became irrelevant, was decided elsewhere, or you changed your mind. No verdict is recorded; terminal, not reopened. |
|
|
100
|
-
| `argus_check_in` | Returns contracts past their check-by date **and premise facts due for a re-check** (the same fact under several decisions is one re-check). If nothing is due, it says so and stops — it doesn't manufacture engagement. |
|
|
203
|
+
| `argus_check_in` | Returns contracts past their check-by date **and premise facts due for a re-check** (the same fact under several decisions is one re-check), and mirrors yesterday's watch anchor back as a question. If nothing is due, it says so and stops — it doesn't manufacture engagement. |
|
|
101
204
|
| `argus_sync` | Pulls your account receipts into the terminal (live judgments + what's due) so you can settle here. Seals push automatically; this is the read side. Requires `ARGUS_TOKEN`. |
|
|
102
205
|
| `argus_recall` | Reads your own history: a receipt, the open contracts, or a sample-size-caveated track record (never a tier or score). |
|
|
103
206
|
| `argus_init` / `argus_config` | Initialize the `.argus` directory; read/write non-spine settings. |
|
|
@@ -124,12 +227,19 @@ Argus does not pretend liveness.
|
|
|
124
227
|
|
|
125
228
|
Everything is local, under `.argus/` in your project (gitignored by default) —
|
|
126
229
|
an append-only `ledger.jsonl` **you own**: plain JSON lines, no lock-in,
|
|
127
|
-
receipts render to shareable text. No telemetry
|
|
128
|
-
Argus
|
|
129
|
-
`ARGUS_TOKEN`, a sealed/settled prediction is POSTed to your own Argus
|
|
130
|
-
so it can email you at its check-by date.
|
|
131
|
-
|
|
132
|
-
|
|
230
|
+
receipts render to shareable text. **No telemetry by default.** The only network
|
|
231
|
+
call Argus makes out of the box is the opt-in account sync: if — and only if —
|
|
232
|
+
you set `ARGUS_TOKEN`, a sealed/settled prediction is POSTed to your own Argus
|
|
233
|
+
account so it can email you at its check-by date. Separately, you can opt in to
|
|
234
|
+
**anonymous** usage telemetry with `ARGUS_TELEMETRY=1` (a random install id +
|
|
235
|
+
which tool ran + version/platform, never your decisions or token; honors
|
|
236
|
+
`DO_NOT_TRACK`) — see SECURITY.md. **Premise data stays on your machine
|
|
237
|
+
by default** — it is not part of the sync payload. There is exactly one switch
|
|
238
|
+
that changes this: `argus_config premise_sync:true` (off unless you set it)
|
|
239
|
+
sends a sealed decision's *monitored* premises along, so your account's
|
|
240
|
+
autonomous premise-watch can re-check them against reality and email you when
|
|
241
|
+
one materially moves. Unset the token and Argus never touches the network.
|
|
242
|
+
See [SECURITY.md](SECURITY.md).
|
|
133
243
|
|
|
134
244
|
## Measured
|
|
135
245
|
|
package/SECURITY.md
CHANGED
|
@@ -2,14 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
## Data & privacy
|
|
4
4
|
|
|
5
|
-
- **All data is local.** Argus writes only under the `.argus/`
|
|
6
|
-
point it at: an append-only `ledger/ledger.jsonl`,
|
|
7
|
-
`sessions/{id}/` files, and a `config.yaml`. Nothing leaves the
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
5
|
+
- **All data is local by default.** Argus writes only under the `.argus/`
|
|
6
|
+
directory you point it at: an append-only `ledger/ledger.jsonl`,
|
|
7
|
+
per-decision `sessions/{id}/` files, and a `config.yaml`. Nothing leaves the
|
|
8
|
+
machine unless you explicitly set `ARGUS_TOKEN` for account sync (or opt in to
|
|
9
|
+
anonymous telemetry with `ARGUS_TELEMETRY=1`; see below).
|
|
10
|
+
- **No telemetry by default.** With no `ARGUS_TOKEN` and no `ARGUS_TELEMETRY=1`,
|
|
11
|
+
the server makes no network calls. Telemetry is strictly **opt-in**: set
|
|
12
|
+
`ARGUS_TELEMETRY=1` and it sends an anonymous, content-free ping — a random,
|
|
13
|
+
machine-local install id (`~/.argus/.telemetry-id`, not tied to your account
|
|
14
|
+
or token, regenerable by deleting the file), which of the built-in tools ran
|
|
15
|
+
and whether it succeeded, plus the package version, coarse OS platform, and
|
|
16
|
+
Node major. It **never** sends decision content, titles, predicates, file
|
|
17
|
+
paths, `argus_dir`, or the account token. `DO_NOT_TRACK=1` disables it even if
|
|
18
|
+
the flag is set, and the sink stores no `user_id` (nothing to identify).
|
|
19
|
+
Telemetry rows are **deleted automatically after 90 days** (a daily database
|
|
20
|
+
job — retention is enforced by the database itself, not by policy prose). The
|
|
21
|
+
runtime dependency surface is intentionally small and visible in
|
|
22
|
+
`package.json`: MCP SDK, schema/config helpers, and document parsers for
|
|
23
|
+
`argus_review`. `npm audit --omit=dev` should stay clean before publishing.
|
|
24
|
+
- **Private by default.** `argus_init` adds `sessions/`, `ledger/`,
|
|
25
|
+
`config.yaml`, and `.bound` to `.argus/.gitignore` so your decisions and
|
|
26
|
+
local settings are never committed accidentally.
|
|
13
27
|
|
|
14
28
|
## Threat model
|
|
15
29
|
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { replayLedger } from './ledger-replay.js';
|
|
2
|
+
import { duePremises, groupDuePremises, dueOpenQuestions } from './premises.js';
|
|
3
|
+
import { surfacesFor, SURFACES, resolveResponseLocale } from './surfaces.js';
|
|
4
|
+
/** Count from an already-replayed ledger (when the caller has one in hand). */
|
|
5
|
+
export function ambientDueFromState(state) {
|
|
6
|
+
return {
|
|
7
|
+
contractsDue: state.overdue.length,
|
|
8
|
+
premiseFactsDue: groupDuePremises(duePremises(state)).length,
|
|
9
|
+
openQuestionsDue: dueOpenQuestions(state).length,
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
/** Count by replaying the ledger for `dir` as of `today`. */
|
|
13
|
+
export function ambientDue(dir, today) {
|
|
14
|
+
return ambientDueFromState(replayLedger(dir, today));
|
|
15
|
+
}
|
|
16
|
+
/** True when nothing at all is due — the silence gate (restraint: zero renders
|
|
17
|
+
* nothing, never an empty nag). */
|
|
18
|
+
export function isSilent(due) {
|
|
19
|
+
return due.contractsDue === 0 && due.premiseFactsDue === 0 && due.openQuestionsDue === 0;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* The user's own language, read from the LEDGER's user-authored text — the
|
|
23
|
+
* one deterministic voice source for surfaces that have no fresh input (the
|
|
24
|
+
* ambient tail, recall's textless views). Chain: the latest watch anchor, an
|
|
25
|
+
* overdue predicate, a due premise's decision, a due question, else ANY
|
|
26
|
+
* contract predicate (replay order = deterministic). Never env/Intl — an
|
|
27
|
+
* English user on a Korean machine must not get a Korean frame (experience
|
|
28
|
+
* loop caught exactly that).
|
|
29
|
+
*/
|
|
30
|
+
export function ledgerVoiceText(state) {
|
|
31
|
+
const anchors = [...state.watch.anchors.values()].sort((a, b) => (a.date < b.date ? 1 : -1));
|
|
32
|
+
return anchors[0]?.text
|
|
33
|
+
|| state.overdue[0]?.text
|
|
34
|
+
|| duePremises(state)[0]?.decision_text
|
|
35
|
+
|| dueOpenQuestions(state)[0]?.text
|
|
36
|
+
|| [...state.contracts.values()].find((c) => typeof c.predicate === 'string' && c.predicate)?.predicate;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The localized one-line ambient fact for a due count, or '' when silent.
|
|
40
|
+
* Leads with a space so it appends cleanly to the END of a tool's surface
|
|
41
|
+
* (M1 §1.3, §6: never obscure the result — the line is last, and it is a fact
|
|
42
|
+
* + the argus_check_in handle, never a directive).
|
|
43
|
+
* `state` (when the caller has one) lends the ledger's own voice — without it
|
|
44
|
+
* the tail fell back to config-or-EN and read "By the way — 2 to settle" to a
|
|
45
|
+
* user whose every decision is Korean (experience-loop worst-moment).
|
|
46
|
+
*/
|
|
47
|
+
export function ambientLine(dir, due, state) {
|
|
48
|
+
if (isSilent(due))
|
|
49
|
+
return '';
|
|
50
|
+
const voice = state ? ledgerVoiceText(state) : undefined;
|
|
51
|
+
const A = (voice ? SURFACES[resolveResponseLocale(dir, voice)] : surfacesFor(dir)).ambient;
|
|
52
|
+
const { contractsDue: c, premiseFactsDue: p, openQuestionsDue: q } = due;
|
|
53
|
+
// Fragment-composed so any subset of the three due kinds reads naturally in
|
|
54
|
+
// both locales (M3 adds a third kind — a fixed combo table would be 7 cases).
|
|
55
|
+
const frags = [];
|
|
56
|
+
if (c > 0)
|
|
57
|
+
frags.push(A.frag_contracts(c));
|
|
58
|
+
if (p > 0)
|
|
59
|
+
frags.push(A.frag_premises(p));
|
|
60
|
+
if (q > 0)
|
|
61
|
+
frags.push(A.frag_open_questions(q));
|
|
62
|
+
return A.wrap(frags);
|
|
63
|
+
}
|
package/dist/lib/argus-dir.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
|
+
import os from 'os';
|
|
2
3
|
import path from 'path';
|
|
3
4
|
import { boundMarkerPath } from './layout.js';
|
|
4
5
|
/**
|
|
@@ -26,6 +27,14 @@ export function requireArgusDir(callArg) {
|
|
|
26
27
|
if (typeof callArg !== 'string' || callArg.length === 0) {
|
|
27
28
|
throw new ArgusDirError('argus_dir is required (absolute path to the .argus directory).');
|
|
28
29
|
}
|
|
30
|
+
// An unexpanded config variable is the #1 Claude Desktop first-run failure:
|
|
31
|
+
// only Claude Code expands ${CLAUDE_PROJECT_DIR}, so Desktop passes the
|
|
32
|
+
// literal string through. Name the actual problem instead of "not absolute".
|
|
33
|
+
if (/\$\{[^}]*\}|%[A-Za-z_]+%/.test(callArg)) {
|
|
34
|
+
throw new ArgusDirError(`Your MCP host did not expand the variable in "${callArg}" (only some hosts interpolate env vars). ` +
|
|
35
|
+
'Replace it with an absolute path in your MCP config (e.g. "C:\\Users\\you\\.argus" or "/Users/you/.argus"), ' +
|
|
36
|
+
'or remove ARGUS_DIR entirely to use the default ~/.argus.');
|
|
37
|
+
}
|
|
29
38
|
const resolved = path.resolve(callArg);
|
|
30
39
|
if (!path.isAbsolute(callArg)) {
|
|
31
40
|
throw new ArgusDirError('argus_dir must be an absolute path.');
|
|
@@ -48,7 +57,10 @@ export function resolveToolArgusDir(callArg) {
|
|
|
48
57
|
const env = process.env['ARGUS_DIR'];
|
|
49
58
|
if (typeof env === 'string' && env.length > 0)
|
|
50
59
|
return requireArgusDir(env);
|
|
51
|
-
|
|
60
|
+
// Zero-config default (blueprint §9.4 "첫 설치의 문"): a brand-new user on a
|
|
61
|
+
// host without env interpolation still gets a working home for their ledger.
|
|
62
|
+
// Per-call argus_dir and ARGUS_DIR both keep winning over this.
|
|
63
|
+
return path.join(os.homedir(), '.argus');
|
|
52
64
|
}
|
|
53
65
|
/** Record the bound dir so Resources (which get no args) can find it later. */
|
|
54
66
|
export function writeBoundMarker(argusDir) {
|
|
@@ -61,6 +73,43 @@ export function writeBoundMarker(argusDir) {
|
|
|
61
73
|
catch {
|
|
62
74
|
/* non-critical */
|
|
63
75
|
}
|
|
76
|
+
// Global registry (M2 fleet, §9.4): every init'd project dir also lands in
|
|
77
|
+
// ~/.argus/.bound so ONE check_in can report due items across all of a
|
|
78
|
+
// user's projects. Local paths only, stays on this machine, best-effort.
|
|
79
|
+
try {
|
|
80
|
+
const home = path.join(os.homedir(), '.argus');
|
|
81
|
+
fs.mkdirSync(home, { recursive: true });
|
|
82
|
+
const reg = path.join(home, '.bound');
|
|
83
|
+
let dirs = [];
|
|
84
|
+
try {
|
|
85
|
+
const parsed = JSON.parse(fs.readFileSync(reg, 'utf8'));
|
|
86
|
+
dirs = Array.isArray(parsed.bound) ? parsed.bound.filter((x) => typeof x === 'string') : [];
|
|
87
|
+
}
|
|
88
|
+
catch { /* fresh registry */ }
|
|
89
|
+
if (!dirs.includes(argusDir))
|
|
90
|
+
dirs.unshift(argusDir);
|
|
91
|
+
fs.writeFileSync(reg, JSON.stringify({ bound: dirs.slice(0, 16) }), 'utf8');
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
/* non-critical */
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/** The cross-project registry (~/.argus/.bound) — existing dirs only. */
|
|
98
|
+
export function readGlobalBoundList() {
|
|
99
|
+
try {
|
|
100
|
+
const reg = path.join(os.homedir(), '.argus', '.bound');
|
|
101
|
+
const parsed = JSON.parse(fs.readFileSync(reg, 'utf8'));
|
|
102
|
+
const dirs = Array.isArray(parsed.bound) ? parsed.bound.filter((x) => typeof x === 'string') : [];
|
|
103
|
+
return dirs.filter((d) => { try {
|
|
104
|
+
return fs.existsSync(d);
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return false;
|
|
108
|
+
} });
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return [];
|
|
112
|
+
}
|
|
64
113
|
}
|
|
65
114
|
function readBoundList(argusDir) {
|
|
66
115
|
try {
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { calendarPath } from './layout.js';
|
|
4
|
+
function ymdCompact(ymd) {
|
|
5
|
+
return ymd.replace(/-/g, '');
|
|
6
|
+
}
|
|
7
|
+
function addOneDay(ymd) {
|
|
8
|
+
const t = Date.parse(`${ymd}T00:00:00Z`);
|
|
9
|
+
if (Number.isNaN(t))
|
|
10
|
+
return ymd;
|
|
11
|
+
return new Date(t + 86_400_000).toISOString().slice(0, 10);
|
|
12
|
+
}
|
|
13
|
+
function escapeIcs(value) {
|
|
14
|
+
return value
|
|
15
|
+
.replace(/\\/g, '\\\\')
|
|
16
|
+
.replace(/\r?\n/g, '\\n')
|
|
17
|
+
.replace(/,/g, '\\,')
|
|
18
|
+
.replace(/;/g, '\\;');
|
|
19
|
+
}
|
|
20
|
+
function foldLine(line) {
|
|
21
|
+
if (line.length <= 74)
|
|
22
|
+
return line;
|
|
23
|
+
const chunks = [];
|
|
24
|
+
let rest = line;
|
|
25
|
+
while (rest.length > 74) {
|
|
26
|
+
chunks.push(rest.slice(0, 74));
|
|
27
|
+
rest = rest.slice(74);
|
|
28
|
+
}
|
|
29
|
+
chunks.push(rest);
|
|
30
|
+
return chunks.map((chunk, i) => (i === 0 ? chunk : ` ${chunk}`)).join('\r\n');
|
|
31
|
+
}
|
|
32
|
+
export function renderReturnCalendarEvent(args) {
|
|
33
|
+
const start = ymdCompact(args.check_by);
|
|
34
|
+
const end = ymdCompact(addOneDay(args.check_by));
|
|
35
|
+
const stamp = args.created_at.replace(/[-:]/g, '').replace(/\.\d{3}Z$/, 'Z');
|
|
36
|
+
const summary = `Argus return: ${args.predicate.slice(0, 80)}`;
|
|
37
|
+
const description = [
|
|
38
|
+
'Your sealed Argus decision is due today.',
|
|
39
|
+
`Predicate: ${args.predicate}`,
|
|
40
|
+
'Open your terminal and run argus_check_in, then argus_settle if reality is clear.',
|
|
41
|
+
].join('\n');
|
|
42
|
+
const lines = [
|
|
43
|
+
'BEGIN:VCALENDAR',
|
|
44
|
+
'VERSION:2.0',
|
|
45
|
+
'PRODID:-//Argus//Return Calendar//EN',
|
|
46
|
+
'CALSCALE:GREGORIAN',
|
|
47
|
+
'METHOD:PUBLISH',
|
|
48
|
+
'BEGIN:VEVENT',
|
|
49
|
+
`UID:${escapeIcs(args.id)}@argus.local`,
|
|
50
|
+
`DTSTAMP:${stamp}`,
|
|
51
|
+
`DTSTART;VALUE=DATE:${start}`,
|
|
52
|
+
`DTEND;VALUE=DATE:${end}`,
|
|
53
|
+
`SUMMARY:${escapeIcs(summary)}`,
|
|
54
|
+
`DESCRIPTION:${escapeIcs(description)}`,
|
|
55
|
+
'BEGIN:VALARM',
|
|
56
|
+
// DTSTART is local midnight of the check-by (an all-day event), so a POSITIVE
|
|
57
|
+
// offset lands on the morning OF the due date. `-PT9H` fired nine hours
|
|
58
|
+
// BEFORE midnight — 15:00 the previous afternoon — while the alarm text said
|
|
59
|
+
// "due today". This .ics is the only thing that brings a sealed bet back
|
|
60
|
+
// without an account, so it has to ring on the right day.
|
|
61
|
+
'TRIGGER;RELATED=START:PT9H',
|
|
62
|
+
'ACTION:DISPLAY',
|
|
63
|
+
`DESCRIPTION:${escapeIcs(summary)}`,
|
|
64
|
+
'END:VALARM',
|
|
65
|
+
'END:VEVENT',
|
|
66
|
+
'END:VCALENDAR',
|
|
67
|
+
];
|
|
68
|
+
return `${lines.map(foldLine).join('\r\n')}\r\n`;
|
|
69
|
+
}
|
|
70
|
+
export async function writeReturnCalendarEvent(argusDir, args) {
|
|
71
|
+
const file = calendarPath(argusDir, args.id);
|
|
72
|
+
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
73
|
+
await fs.writeFile(file, renderReturnCalendarEvent(args), 'utf8');
|
|
74
|
+
return file;
|
|
75
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical scales & units — the pre-rule normalization layer for M2 drift
|
|
3
|
+
* materiality (internal design notes §3, §10.4).
|
|
4
|
+
*
|
|
5
|
+
* Rules compare CANONICAL values, never raw labels. This module turns an ordinal
|
|
6
|
+
* label ("BBB", "Baa2", "Enterprise", "Maintenance") into a comparable number via
|
|
7
|
+
* a built-in ordinal map, and converts a few units into a common axis before the
|
|
8
|
+
* rule ever runs. When a label can't be placed on a known scale, this returns
|
|
9
|
+
* `null` — the caller routes that to `uncertain` (host-judgment lane), never a
|
|
10
|
+
* silent guess (§3.1, "규칙이 아니라 매번 모델 판정 = 스파인 위반").
|
|
11
|
+
*
|
|
12
|
+
* Alias normalization is the load-bearing win: Baa2 ≡ BBB, so a Moody's→S&P
|
|
13
|
+
* relabel is `unchanged`, not a manufactured downgrade (CAT-06).
|
|
14
|
+
*/
|
|
15
|
+
/** Normalize a label for lookup: trim, collapse spaces, lowercase. Ordinal maps
|
|
16
|
+
* are keyed by this normalized form so "BBB ", "bbb", "BBB" all hit. */
|
|
17
|
+
function normLabel(label) {
|
|
18
|
+
return label.trim().toLowerCase().replace(/\s+/g, ' ');
|
|
19
|
+
}
|
|
20
|
+
// ── S&P / Moody's credit ratings (AAA=1 … D=22), with cross-agency aliases ──
|
|
21
|
+
// The canonical axis is S&P notation; Moody's labels are aliases onto the same
|
|
22
|
+
// rank so Baa2 ≡ BBB (CAT-06), A2 ≡ A, Ba1 ≡ BB+, etc.
|
|
23
|
+
const SP_ORDER = [
|
|
24
|
+
'AAA', 'AA+', 'AA', 'AA-', 'A+', 'A', 'A-', 'BBB+', 'BBB', 'BBB-',
|
|
25
|
+
'BB+', 'BB', 'BB-', 'B+', 'B', 'B-', 'CCC+', 'CCC', 'CCC-', 'CC', 'C', 'D',
|
|
26
|
+
];
|
|
27
|
+
// Moody's, index-aligned to SP_ORDER above.
|
|
28
|
+
const MOODYS_ORDER = [
|
|
29
|
+
'Aaa', 'Aa1', 'Aa2', 'Aa3', 'A1', 'A2', 'A3', 'Baa1', 'Baa2', 'Baa3',
|
|
30
|
+
'Ba1', 'Ba2', 'Ba3', 'B1', 'B2', 'B3', 'Caa1', 'Caa2', 'Caa3', 'Ca', 'C', 'DDD',
|
|
31
|
+
];
|
|
32
|
+
function buildCreditRanks() {
|
|
33
|
+
const r = {};
|
|
34
|
+
SP_ORDER.forEach((lbl, i) => {
|
|
35
|
+
r[normLabel(lbl)] = i + 1;
|
|
36
|
+
const m = MOODYS_ORDER[i];
|
|
37
|
+
if (m)
|
|
38
|
+
r[normLabel(m)] = i + 1; // alias onto the same rank
|
|
39
|
+
});
|
|
40
|
+
return r;
|
|
41
|
+
}
|
|
42
|
+
// ── LTS / lifecycle stages (Active=1 … EOL highest) ──
|
|
43
|
+
const LTS_ORDER = ['Active', 'Maintenance', 'Security', 'Deprecated', 'EOL'];
|
|
44
|
+
// ── Product tiers (Free=1 … Enterprise highest) ──
|
|
45
|
+
const TIER_ORDER = ['Free', 'Trial', 'Starter', 'Pro', 'Business', 'Team', 'Enterprise'];
|
|
46
|
+
// ── Generic quality letter grades (A=1 … F) ──
|
|
47
|
+
const QUALITY_ORDER = ['A', 'B', 'C', 'D', 'E', 'F'];
|
|
48
|
+
function buildOrdinal(order) {
|
|
49
|
+
const r = {};
|
|
50
|
+
order.forEach((lbl, i) => { r[normLabel(lbl)] = i + 1; });
|
|
51
|
+
return r;
|
|
52
|
+
}
|
|
53
|
+
/** Built-in scales, addressable by name from a premise's `scale` modifier. */
|
|
54
|
+
export const BUILTIN_SCALES = {
|
|
55
|
+
sp_credit: { name: 'S&P/Moody\'s credit rating', kind: 'credit', ranks: buildCreditRanks() },
|
|
56
|
+
lts: { name: 'lifecycle stage', kind: 'lts', ranks: buildOrdinal(LTS_ORDER) },
|
|
57
|
+
tier: { name: 'product tier', kind: 'tier', ranks: buildOrdinal(TIER_ORDER) },
|
|
58
|
+
quality: { name: 'quality grade', kind: 'quality', ranks: buildOrdinal(QUALITY_ORDER) },
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* Map a label onto a named scale's ordinal rank. Returns null when the scale is
|
|
62
|
+
* unknown OR the label isn't on it — the caller routes null to `uncertain`
|
|
63
|
+
* (host-judgment lane), never a silent numeric guess (§3.1).
|
|
64
|
+
*
|
|
65
|
+
* A custom scale (user-provided ordered labels) can be passed inline via
|
|
66
|
+
* `customScales` so a premise isn't limited to the built-ins.
|
|
67
|
+
*/
|
|
68
|
+
export function normalizeLabel(scale, label, customScales) {
|
|
69
|
+
if (!scale)
|
|
70
|
+
return null;
|
|
71
|
+
const custom = customScales?.[scale];
|
|
72
|
+
if (custom) {
|
|
73
|
+
const ranks = Array.isArray(custom) ? buildOrdinal(custom) : custom.ranks;
|
|
74
|
+
const hit = ranks[normLabel(label)];
|
|
75
|
+
return typeof hit === 'number' ? hit : null;
|
|
76
|
+
}
|
|
77
|
+
const builtin = BUILTIN_SCALES[scale];
|
|
78
|
+
if (!builtin)
|
|
79
|
+
return null;
|
|
80
|
+
const hit = builtin.ranks[normLabel(label)];
|
|
81
|
+
return typeof hit === 'number' ? hit : null;
|
|
82
|
+
}
|
|
83
|
+
const UNIT_TABLE = {
|
|
84
|
+
// temperature: Kelvin ↔ Celsius (offset). Canonical = Celsius for delta axes.
|
|
85
|
+
k: { to: 'c', convert: (v) => v - 273.15 },
|
|
86
|
+
kelvin: { to: 'c', convert: (v) => v - 273.15 },
|
|
87
|
+
// fuel economy: mpg (US) → L/100km (reciprocal). Canonical = L/100km.
|
|
88
|
+
mpg: { to: 'l/100km', convert: (v) => (v === 0 ? Infinity : 235.215 / v) },
|
|
89
|
+
// percentage points and percent share a numeric magnitude; the AXIS (not the
|
|
90
|
+
// unit) disambiguates — handled by unit_axis, not here.
|
|
91
|
+
};
|
|
92
|
+
/**
|
|
93
|
+
* Convert a value from a source unit to its canonical unit. Returns null when the
|
|
94
|
+
* unit is unknown (caller keeps host_reported + low_confidence). When from===to
|
|
95
|
+
* or from is already canonical, returns the value unchanged.
|
|
96
|
+
*/
|
|
97
|
+
export function normalizeUnit(value, from) {
|
|
98
|
+
if (!from)
|
|
99
|
+
return null;
|
|
100
|
+
const key = from.trim().toLowerCase();
|
|
101
|
+
const conv = UNIT_TABLE[key];
|
|
102
|
+
if (!conv)
|
|
103
|
+
return { value, unit: key }; // unknown but not an error — pass through as-is unit-tagged
|
|
104
|
+
return { value: conv.convert(value), unit: conv.to };
|
|
105
|
+
}
|