argus-decision-mcp 1.0.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 +21 -0
- package/README.md +168 -0
- package/SECURITY.md +35 -0
- package/dist/index.js +13 -0
- package/dist/lib/argus-dir.js +85 -0
- package/dist/lib/atomic-write.js +19 -0
- package/dist/lib/continuity.js +31 -0
- package/dist/lib/deBom.js +3 -0
- package/dist/lib/discipline.js +42 -0
- package/dist/lib/due-note.js +54 -0
- package/dist/lib/elicit.js +30 -0
- package/dist/lib/envelope.js +13 -0
- package/dist/lib/layout.js +32 -0
- package/dist/lib/ledger-append.js +27 -0
- package/dist/lib/ledger-replay.js +295 -0
- package/dist/lib/locale.js +20 -0
- package/dist/lib/log.js +14 -0
- package/dist/lib/numeric-drift.js +32 -0
- package/dist/lib/overfire-gate.js +39 -0
- package/dist/lib/premises.js +153 -0
- package/dist/lib/privacy.js +22 -0
- package/dist/lib/push-account.js +70 -0
- package/dist/lib/receipt.js +83 -0
- package/dist/lib/render-receipt.js +144 -0
- package/dist/lib/resolve-contract.js +17 -0
- package/dist/lib/resolve-today.js +47 -0
- package/dist/lib/review/ids.js +29 -0
- package/dist/lib/review/index.js +18 -0
- package/dist/lib/review/ingest.js +294 -0
- package/dist/lib/review/lenses.js +132 -0
- package/dist/lib/review/prompts.js +155 -0
- package/dist/lib/review/render.js +77 -0
- package/dist/lib/review/reviewability.js +83 -0
- package/dist/lib/review/routing.js +147 -0
- package/dist/lib/review/schema.js +36 -0
- package/dist/lib/safe-path.js +70 -0
- package/dist/lib/spine.js +79 -0
- package/dist/lib/state-machine.js +80 -0
- package/dist/lib/surfaces.js +106 -0
- package/dist/lib/validate-crux.js +35 -0
- package/dist/lib/validate-seal.js +48 -0
- package/dist/prompts.js +72 -0
- package/dist/resources.js +122 -0
- package/dist/server.js +107 -0
- package/dist/tools/amend-dismiss.js +90 -0
- package/dist/tools/check-in.js +158 -0
- package/dist/tools/errors.js +23 -0
- package/dist/tools/index.js +14 -0
- package/dist/tools/init-config.js +99 -0
- package/dist/tools/open-decision.js +128 -0
- package/dist/tools/premises.js +209 -0
- package/dist/tools/recall.js +157 -0
- package/dist/tools/recheck.js +147 -0
- package/dist/tools/review.js +182 -0
- package/dist/tools/seal.js +152 -0
- package/dist/tools/settle.js +126 -0
- package/dist/tools/sync.js +129 -0
- package/dist/tools/tool-types.js +32 -0
- package/package.json +64 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Argus
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# argus-decision-mcp
|
|
2
|
+
|
|
3
|
+
> **Your AI gives you an answer. Argus gives you a receipt — and checks it against reality on the date you set.**
|
|
4
|
+
|
|
5
|
+
Argus is an MCP server for **decision accountability**. Instead of grading your
|
|
6
|
+
choices, it makes you write down a *falsifiable prediction* and a *check-by
|
|
7
|
+
date*, then brings you back on that date to compare what you predicted against
|
|
8
|
+
what actually happened. The artifact it produces — a **Judgment Receipt** —
|
|
9
|
+
carries one line no other AI tool will: `AI VERDICT … NONE`. The model never
|
|
10
|
+
graded you. Reality did.
|
|
11
|
+
|
|
12
|
+
Works with any MCP host — Claude, ChatGPT, Gemini, or any MCP-compatible client.
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
┌─ ARGUS · JUDGMENT RECEIPT ────────────────────────────────┐
|
|
16
|
+
Sealed 2026-04-02 Settled 2026-06-30
|
|
17
|
+
|
|
18
|
+
THE REAL QUESTION
|
|
19
|
+
Can we cut over without a maintenance window users notice?
|
|
20
|
+
THE UNVERIFIED ASSUMPTION
|
|
21
|
+
The index rebuild fits inside the replication lag budget.
|
|
22
|
+
HUMAN-ONLY CALL Whether a 5-minute blip is acceptable.
|
|
23
|
+
…made by Me. (not the model)
|
|
24
|
+
|
|
25
|
+
YOU PREDICTED "Cutover downtime is under 5 minutes" (check-by 2026-06-30)
|
|
26
|
+
WHAT HAPPENED Cutover took 3 minutes, no customer reports.
|
|
27
|
+
─────────────────────────────────────────────────────────
|
|
28
|
+
AI VERDICT ON THIS DECISION ······················ NONE
|
|
29
|
+
The model never graded you. Reality did.
|
|
30
|
+
└────────────────────────────────── argus · seal → settle ─┘
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Why it's different
|
|
34
|
+
|
|
35
|
+
Most decision tools compete on a *better answer*, a *score*, a *confidence*.
|
|
36
|
+
Argus does the opposite, and the opposite is enforced **structurally**, not
|
|
37
|
+
promised in prose:
|
|
38
|
+
|
|
39
|
+
- **There is no verdict tool.** The model cannot grade your decision because no
|
|
40
|
+
`argus_verdict` / `argus_score` tool exists to call. `grep dist/` and see.
|
|
41
|
+
- **You can't settle what you never sealed.** `argus_settle` hard-errors without
|
|
42
|
+
a prior `argus_seal` — "no judgment without a falsifiable bet" is a
|
|
43
|
+
precondition, not a suggestion.
|
|
44
|
+
- **State is the ledger, not a flag.** A decision's status is the fold of an
|
|
45
|
+
append-only event log, so it can't be faked by calling tools out of order.
|
|
46
|
+
|
|
47
|
+
## Install
|
|
48
|
+
|
|
49
|
+
Claude Code:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
claude mcp add argus -- npx -y argus-decision-mcp
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Or add to your host's MCP config:
|
|
56
|
+
|
|
57
|
+
```jsonc
|
|
58
|
+
{
|
|
59
|
+
"mcpServers": {
|
|
60
|
+
"argus": {
|
|
61
|
+
"command": "npx",
|
|
62
|
+
"args": ["-y", "argus-decision-mcp"],
|
|
63
|
+
"env": {
|
|
64
|
+
// Set this ONCE and you never pass argus_dir again — every tool falls
|
|
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).
|
|
67
|
+
"ARGUS_DIR": "${CLAUDE_PROJECT_DIR}/.argus",
|
|
68
|
+
// OPTIONAL — connect to your Argus account so sealed predictions get an
|
|
69
|
+
// email at their check-by date (the Companion Brief) and show up in the
|
|
70
|
+
// web dashboard. Issue the token in the web app. Leave it unset to stay
|
|
71
|
+
// fully local (the privacy-preserving default).
|
|
72
|
+
"ARGUS_TOKEN": "argus_pat_…",
|
|
73
|
+
// OPTIONAL — the timezone that decides when a check-by date becomes
|
|
74
|
+
// "today". Unset = UTC, which for Korean users means the day flips at
|
|
75
|
+
// 9am KST (a due decision won't show until then). Set your zone:
|
|
76
|
+
"ARGUS_TZ": "Asia/Seoul"
|
|
77
|
+
// "ARGUS_API_URL": "https://argus.voyage" // override only for self-host
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
> `argus_dir` is **optional** on every tool: omit it and it resolves from
|
|
85
|
+
> `ARGUS_DIR`. A per-call `argus_dir` still wins — so Argus works on any host
|
|
86
|
+
> even when env-variable interpolation doesn't.
|
|
87
|
+
|
|
88
|
+
## The loop
|
|
89
|
+
|
|
90
|
+
| Tool | What it does |
|
|
91
|
+
|------|--------------|
|
|
92
|
+
| `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
|
+
| `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
|
+
| `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. |
|
|
95
|
+
| `argus_premises` | Tracks the **premises** a decision rests on — the facts and open questions behind it. `add` records them (echoed back in full); `amend` takes your correction verbatim (the AI's original stays on the record — the edit is the signal); `resolve` closes an open question **in your own words** (it cannot offer options or leans — that shape doesn't exist here). Premises lock once the check-by arrives: no retroactive premise-planting, no retiring the one about to be proven wrong. |
|
|
96
|
+
| `argus_recheck` | Re-checks one premise against reality between seal and settle. The host researches the current fact; the tool compares **explicit numbers** mechanically (never regex-parses prose) or records the host's provenance-tagged `changed` assertion for text facts. First check is a baseline and never alerts. When the fact drifted: it says so and returns the handle — whether to revisit the decision stays your call. `apply_to_matching` re-checks the same fact under your other decisions at once. |
|
|
97
|
+
| `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
|
+
| `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
|
+
| `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. |
|
|
101
|
+
| `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
|
+
| `argus_recall` | Reads your own history: a receipt, the open contracts, or a sample-size-caveated track record (never a tier or score). |
|
|
103
|
+
| `argus_init` / `argus_config` | Initialize the `.argus` directory; read/write non-spine settings. |
|
|
104
|
+
|
|
105
|
+
## Living premises
|
|
106
|
+
|
|
107
|
+
The receipt's sharpest line — `THE UNVERIFIED ASSUMPTION` — used to be written
|
|
108
|
+
once at seal and then go dead. Now it's a **tracked object**: name the premises
|
|
109
|
+
a decision rests on, correct the ones the model got wrong (your edit is part of
|
|
110
|
+
the record), and re-check the load-bearing external facts against reality while
|
|
111
|
+
the bet is still open. When a rate hike breaks the premise under three of your
|
|
112
|
+
decisions, that's **one** re-check, not three.
|
|
113
|
+
|
|
114
|
+
Honest limits, stated up front: an MCP server is passive — nothing wakes it
|
|
115
|
+
between seal and settle. The return loop rides four levers: every tool response
|
|
116
|
+
carries a quiet `due_note`; `argus_check_in` reports due premises;
|
|
117
|
+
the `argus://premises/due` resource lets hosts auto-inject them; and the
|
|
118
|
+
`/argus-settle` ritual includes the re-check choreography. Anything more
|
|
119
|
+
periodic (cron, reminders) belongs to your host or habits, not this server.
|
|
120
|
+
A premise that never gets re-checked shows up honestly as `never re-checked` —
|
|
121
|
+
Argus does not pretend liveness.
|
|
122
|
+
|
|
123
|
+
## Data
|
|
124
|
+
|
|
125
|
+
Everything is local, under `.argus/` in your project (gitignored by default) —
|
|
126
|
+
an append-only `ledger.jsonl` **you own**: plain JSON lines, no lock-in,
|
|
127
|
+
receipts render to shareable text. No telemetry. The **only** network call
|
|
128
|
+
Argus ever makes is the opt-in account sync: if — and only if — you set
|
|
129
|
+
`ARGUS_TOKEN`, a sealed/settled prediction is POSTed to your own Argus account
|
|
130
|
+
so it can email you at its check-by date. **Premise data never leaves your
|
|
131
|
+
machine** — it is not part of the sync payload. Unset the token and Argus never
|
|
132
|
+
touches the network. See [SECURITY.md](SECURITY.md).
|
|
133
|
+
|
|
134
|
+
## Measured
|
|
135
|
+
|
|
136
|
+
The structural claims are tested, not asserted — `npm test` runs deterministic
|
|
137
|
+
gates (no verdict tool exists, settle-without-seal refused, path traversal
|
|
138
|
+
blocked, receipts carry `ai_verdict: null`). A model-in-the-loop spine eval
|
|
139
|
+
(`npm run eval`, 12 scenarios, opus judge) measured, across Sonnet 4.6 and
|
|
140
|
+
Haiku 4.5:
|
|
141
|
+
|
|
142
|
+
| | over-fire on flat cases | crux carries a lean | free-text verdict leak |
|
|
143
|
+
|---|---|---|---|
|
|
144
|
+
| Sonnet 4.6 | 0 / 6 | 0 / 12 | 0 / 12 |
|
|
145
|
+
| Haiku 4.5 | 0 / 6 | 0 / 12 | 1 / 12 |
|
|
146
|
+
|
|
147
|
+
Tool-surface verdict leak is **0 by construction**. The one free-text leak
|
|
148
|
+
(Haiku, on "salad or sandwich") is exactly the limit below — a model can still
|
|
149
|
+
type an opinion in chat, and Argus reports that number rather than hiding it.
|
|
150
|
+
|
|
151
|
+
## An honest limit
|
|
152
|
+
|
|
153
|
+
Argus removes the verdict from its *tool surface* and walls off settling a bet
|
|
154
|
+
that was never made. It cannot stop a model from typing an opinion in free chat
|
|
155
|
+
between tool calls — no MCP server can. So Argus doesn't claim "zero judgment";
|
|
156
|
+
it surfaces one question, names any faint lean as a known limit, and lets
|
|
157
|
+
reality do the grading. `zero judgment` is an asymptote, disclosed — not a badge.
|
|
158
|
+
|
|
159
|
+
## Develop
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
npm install
|
|
163
|
+
npm run build
|
|
164
|
+
npm test # deterministic spine + state-machine + path-safety gates
|
|
165
|
+
npx @modelcontextprotocol/inspector node dist/index.js
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
MIT licensed.
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Security
|
|
2
|
+
|
|
3
|
+
## Data & privacy
|
|
4
|
+
|
|
5
|
+
- **All data is local.** Argus writes only under the `.argus/` directory you
|
|
6
|
+
point it at: an append-only `ledger/ledger.jsonl`, per-decision
|
|
7
|
+
`sessions/{id}/` files, and a `config.yaml`. Nothing leaves the machine.
|
|
8
|
+
- **No telemetry, no network.** The runtime dependency surface is two packages
|
|
9
|
+
(`@modelcontextprotocol/sdk`, `js-yaml`). `npm ls --prod` should show no
|
|
10
|
+
analytics or network client. CI fails if the production dependency tree grows.
|
|
11
|
+
- **Private by default.** `argus_init` adds `sessions/`, `ledger/`, and `.bound`
|
|
12
|
+
to `.argus/.gitignore` so your decisions are never committed.
|
|
13
|
+
|
|
14
|
+
## Threat model
|
|
15
|
+
|
|
16
|
+
Argus runs as a local stdio MCP server. Tool arguments are produced by an LLM
|
|
17
|
+
and may be influenced by untrusted context, so:
|
|
18
|
+
|
|
19
|
+
- **Path traversal.** Every `id` / label flows through a single `safeSegment`
|
|
20
|
+
validator (`[A-Za-z0-9._-]`, no `..`, no separators, no percent-encoding) and
|
|
21
|
+
every built path is checked with `assertInside`, which resolves the deepest
|
|
22
|
+
existing ancestor via `realpath` so a symlink/junction can't be used to escape
|
|
23
|
+
`.argus/`. Covered by `safe-path.test.ts` (incl. Windows `..\`, `%2e`).
|
|
24
|
+
- **Ledger integrity.** The ledger is trusted local state; its integrity rests
|
|
25
|
+
on filesystem permissions. v1 does **not** sign entries — a process with write
|
|
26
|
+
access to `.argus/` could append a forged `settle`. Replay counts unparseable
|
|
27
|
+
lines and surfaces `integrity.dropped_lines` rather than silently swallowing a
|
|
28
|
+
torn record. If you need tamper-evidence, keep `.argus/` on a controlled path.
|
|
29
|
+
- **Diagnostics never touch stdout.** A stdio server must keep stdout clean for
|
|
30
|
+
JSON-RPC framing; all logging goes to stderr (`ARGUS_DEBUG` for verbose).
|
|
31
|
+
|
|
32
|
+
## Reporting
|
|
33
|
+
|
|
34
|
+
Open a private security advisory on the repository, or email the maintainer.
|
|
35
|
+
Please don't file public issues for vulnerabilities.
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createServer } from './server.js';
|
|
3
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
+
async function main() {
|
|
5
|
+
const server = await createServer();
|
|
6
|
+
const transport = new StdioServerTransport();
|
|
7
|
+
await server.connect(transport);
|
|
8
|
+
process.on('SIGINT', async () => {
|
|
9
|
+
await server.close();
|
|
10
|
+
process.exit(0);
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
main().catch(console.error);
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { boundMarkerPath } from './layout.js';
|
|
4
|
+
/**
|
|
5
|
+
* argus_dir resolution + precedence (blueprint §4.0 + addendum A/B).
|
|
6
|
+
*
|
|
7
|
+
* The `${workspaceFolder}` config variable only expands in VS Code — Claude
|
|
8
|
+
* Code expands `${CLAUDE_PROJECT_DIR}`, others expand nothing. So env
|
|
9
|
+
* interpolation is the LEAST reliable channel and must come last. Tools always
|
|
10
|
+
* receive a per-call `argus_dir`, which works on every host, so that wins.
|
|
11
|
+
*
|
|
12
|
+
* Precedence:
|
|
13
|
+
* 1. per-call `argus_dir` argument (works on every host)
|
|
14
|
+
* 2. last dir written to `.bound` by argus_init (read-side for Resources)
|
|
15
|
+
* 3. ARGUS_DIR env (host-dependent interpolation)
|
|
16
|
+
*/
|
|
17
|
+
export class ArgusDirError extends Error {
|
|
18
|
+
code = 'ARGUS_DIR_INVALID';
|
|
19
|
+
constructor(message) {
|
|
20
|
+
super(message);
|
|
21
|
+
this.name = 'ArgusDirError';
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/** Validate a per-call argus_dir: must be an absolute path, no traversal segments. */
|
|
25
|
+
export function requireArgusDir(callArg) {
|
|
26
|
+
if (typeof callArg !== 'string' || callArg.length === 0) {
|
|
27
|
+
throw new ArgusDirError('argus_dir is required (absolute path to the .argus directory).');
|
|
28
|
+
}
|
|
29
|
+
const resolved = path.resolve(callArg);
|
|
30
|
+
if (!path.isAbsolute(callArg)) {
|
|
31
|
+
throw new ArgusDirError('argus_dir must be an absolute path.');
|
|
32
|
+
}
|
|
33
|
+
if (callArg.split(/[\\/]/).some((seg) => seg === '..')) {
|
|
34
|
+
throw new ArgusDirError("argus_dir must not contain '..'.");
|
|
35
|
+
}
|
|
36
|
+
return resolved;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Resolve argus_dir for a TOOL call, implementing the full precedence the
|
|
40
|
+
* blueprint promised (§4.0): a per-call `argus_dir` wins, else the `ARGUS_DIR`
|
|
41
|
+
* env var. This is the ergonomic win — set ARGUS_DIR once in the MCP config and
|
|
42
|
+
* never pass the path again. Both channels get the same path-safety validation;
|
|
43
|
+
* omitting both yields one clear, actionable error.
|
|
44
|
+
*/
|
|
45
|
+
export function resolveToolArgusDir(callArg) {
|
|
46
|
+
if (typeof callArg === 'string' && callArg.length > 0)
|
|
47
|
+
return requireArgusDir(callArg);
|
|
48
|
+
const env = process.env['ARGUS_DIR'];
|
|
49
|
+
if (typeof env === 'string' && env.length > 0)
|
|
50
|
+
return requireArgusDir(env);
|
|
51
|
+
throw new ArgusDirError('No argus_dir given and ARGUS_DIR is not set. Pass an absolute argus_dir, or set "ARGUS_DIR" in your MCP config env (then you can omit it every call).');
|
|
52
|
+
}
|
|
53
|
+
/** Record the bound dir so Resources (which get no args) can find it later. */
|
|
54
|
+
export function writeBoundMarker(argusDir) {
|
|
55
|
+
try {
|
|
56
|
+
const envDirs = readBoundList(argusDir);
|
|
57
|
+
if (!envDirs.includes(argusDir))
|
|
58
|
+
envDirs.unshift(argusDir);
|
|
59
|
+
fs.writeFileSync(boundMarkerPath(argusDir), JSON.stringify({ bound: envDirs.slice(0, 8) }), 'utf8');
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
/* non-critical */
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function readBoundList(argusDir) {
|
|
66
|
+
try {
|
|
67
|
+
const raw = fs.readFileSync(boundMarkerPath(argusDir), 'utf8');
|
|
68
|
+
const parsed = JSON.parse(raw);
|
|
69
|
+
return Array.isArray(parsed.bound) ? parsed.bound.filter((x) => typeof x === 'string') : [];
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Resolve argus_dir for a Resource read (no per-call arg available).
|
|
77
|
+
* Returns null (unbound) rather than throwing — the Resource degrades cleanly.
|
|
78
|
+
*/
|
|
79
|
+
export function resolveArgusDirForResource() {
|
|
80
|
+
const env = process.env['ARGUS_DIR'];
|
|
81
|
+
if (typeof env === 'string' && env.length > 0 && path.isAbsolute(env)) {
|
|
82
|
+
return path.resolve(env);
|
|
83
|
+
}
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
export async function atomicWriteJson(filePath, data) {
|
|
4
|
+
const dir = path.dirname(filePath);
|
|
5
|
+
await fs.mkdir(dir, { recursive: true });
|
|
6
|
+
const tmp = filePath + '.tmp.' + process.pid;
|
|
7
|
+
const json = JSON.stringify(data, null, 2);
|
|
8
|
+
// Verify it round-trips before committing
|
|
9
|
+
JSON.parse(json);
|
|
10
|
+
await fs.writeFile(tmp, json, 'utf8');
|
|
11
|
+
await fs.rename(tmp, filePath);
|
|
12
|
+
}
|
|
13
|
+
export async function atomicWriteText(filePath, text) {
|
|
14
|
+
const dir = path.dirname(filePath);
|
|
15
|
+
await fs.mkdir(dir, { recursive: true });
|
|
16
|
+
const tmp = filePath + '.tmp.' + process.pid;
|
|
17
|
+
await fs.writeFile(tmp, text, 'utf8');
|
|
18
|
+
await fs.rename(tmp, filePath);
|
|
19
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { readReceipt } from './receipt.js';
|
|
2
|
+
/**
|
|
3
|
+
* Track-record continuity (addendum L — the n=1 moat). Given past decision ids
|
|
4
|
+
* the user tags as similar, report a sample-size-scaled frequency statement.
|
|
5
|
+
* NEVER a verdict, never "you tend to..." — frequency only, with a small-sample
|
|
6
|
+
* caveat (spine rule 2).
|
|
7
|
+
*/
|
|
8
|
+
export function computeContinuity(argusDir, relatedIds) {
|
|
9
|
+
const related = [];
|
|
10
|
+
let held = 0, settled = 0;
|
|
11
|
+
for (const id of relatedIds) {
|
|
12
|
+
const r = readReceipt(argusDir, id);
|
|
13
|
+
if (!r)
|
|
14
|
+
continue;
|
|
15
|
+
related.push({ id, outcome: r.outcome, predicate: r.predicate });
|
|
16
|
+
if (r.outcome) {
|
|
17
|
+
settled++;
|
|
18
|
+
if (r.outcome === 'held')
|
|
19
|
+
held++;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
const frequency_statement = settled === 0
|
|
23
|
+
? 'No settled history among the decisions you tagged as similar.'
|
|
24
|
+
: `Of ${settled} similar decision(s) you settled: ${held} held, ${settled - held} did not.`;
|
|
25
|
+
return {
|
|
26
|
+
related,
|
|
27
|
+
frequency_statement,
|
|
28
|
+
sample_size: settled,
|
|
29
|
+
sample_size_caveat: settled > 0 && settled < 10 ? 'Sample is small — read this as history, not a pattern about you.' : undefined,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single source for the spine DISCIPLINE prose (blueprint §4.2 + addendum K).
|
|
3
|
+
*
|
|
4
|
+
* This is where the killed copy-paste system prompt now lives — but as
|
|
5
|
+
* server-defined MCP Prompts the user explicitly turns on, not a string they
|
|
6
|
+
* paste. Every prompt message and the reframe thesis render from here so the
|
|
7
|
+
* three surfaces (webapp / plugin / mcp) cannot drift. The server `instructions`
|
|
8
|
+
* field (spine.ts) and these prompts are the only prose; everything load-bearing
|
|
9
|
+
* is structure.
|
|
10
|
+
*/
|
|
11
|
+
export const BIND_DISCIPLINE = [
|
|
12
|
+
'You are running the Argus BIND ritual. Hold this order, do not skip a step:',
|
|
13
|
+
'',
|
|
14
|
+
'STEP 0 — fire or not. Is this a genuinely consequential, hard-to-reverse fork? If it is flat, low-stakes, easily reversible, or already decided, say so and stop — recommend leaving it as is. Do not manufacture a decision.',
|
|
15
|
+
'STEP 1 — one question. If it fires, surface the SINGLE load-bearing assumption as ONE neutral question. Not a fork ("A or B?"), not a lean ("the stronger case is..."), not advice. A question.',
|
|
16
|
+
'STEP 2 — seal a bet. Help the user commit a falsifiable prediction (a predicate reality can mark true/false) and a check-by date, then call argus_seal. The prediction is the user\'s — never relabel an Argus-drafted line as theirs.',
|
|
17
|
+
'',
|
|
18
|
+
'You are the recorder, not the judge. Never tell the user their decision is right or wrong.',
|
|
19
|
+
].join('\n');
|
|
20
|
+
export const SETTLE_DISCIPLINE = [
|
|
21
|
+
'You are running the Argus SETTLE ritual.',
|
|
22
|
+
'',
|
|
23
|
+
'For each contract past its check-by date, ask the user what reality did — held, avoided, partial, or still pending. Record what they say with argus_settle; never infer the outcome yourself.',
|
|
24
|
+
'Settlement is a single commitment against reality, not a debate with the model. The outcome belongs to the user.',
|
|
25
|
+
'The receipt carries no AI verdict. Reality settles it.',
|
|
26
|
+
].join('\n');
|
|
27
|
+
export const REVIEW_DISCIPLINE = [
|
|
28
|
+
'You are running the Argus REVIEW ritual on an existing document (strategy memo / PRD / deck text / AI answer).',
|
|
29
|
+
'',
|
|
30
|
+
'STEP 0 — call argus_review with the document text (or a .md/.txt path). It returns a reviewability score, the routed lenses, and the source units with anchors. If it degrades honestly (unextractable / too thin), surface what is missing and stop — do not fake a review.',
|
|
31
|
+
'STEP 1 — build the judgment map. Run the returned extraction_prompt over the units: profile, core question, claims (supported/weak/unsupported), unspoken assumptions, decision points. Anchor everything to a unit; never expose a unit_id in prose.',
|
|
32
|
+
'STEP 2 — apply each routed lens. Emit only findings that reference a specific claim/unit. No generic advice ("리스크를 고려하세요"). Separate what only a HUMAN can judge (judgment obligations) — do not decide it for them.',
|
|
33
|
+
'STEP 3 — seal one bet. Pull the single most falsifiable follow-up prediction and seal it with argus_seal (predicate + pass/fail + check-by). The prediction is the user\'s.',
|
|
34
|
+
'',
|
|
35
|
+
'You are the recorder, not the judge. No verdict on the document ("이 전략은 틀렸다", "진행하세요"). Surface the risks anchored to the source; the judgment stays the user\'s.',
|
|
36
|
+
].join('\n');
|
|
37
|
+
export const REFRAME_DISCIPLINE = [
|
|
38
|
+
'You are running the Argus REFRAME lens — the generative half of the spine (surface assumptions; do not judge).',
|
|
39
|
+
'',
|
|
40
|
+
'Surface the hidden assumptions buried in the user\'s question — the ones that, if false, change the answer. For each, name the axis it sits on and what becomes true if it is wrong. Tag them as ai_surfaced (Argus raised them, the user has not confirmed them).',
|
|
41
|
+
'Frame as "참고:" / "worth noticing:" — reference, not directive. Do NOT recommend a direction, do NOT rank the options, do NOT decide. The point is to sharpen the question, not to answer it.',
|
|
42
|
+
].join('\n');
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { replayLedger } from './ledger-replay.js';
|
|
2
|
+
import { duePremises, groupDuePremises } from './premises.js';
|
|
3
|
+
import { resolveToolArgusDir } from './argus-dir.js';
|
|
4
|
+
import { resolveToday } from './resolve-today.js';
|
|
5
|
+
/**
|
|
6
|
+
* Dispatch-level due-note piggyback (plan v5 §5-2, restraint rules §3.4).
|
|
7
|
+
*
|
|
8
|
+
* An MCP server is passive — nothing wakes it between seal and settle. The one
|
|
9
|
+
* moment it reliably has is WHEN THE USER IS ALREADY HERE: any successful tool
|
|
10
|
+
* call. So the dispatcher quietly annotates every ok-envelope with what is due.
|
|
11
|
+
*
|
|
12
|
+
* Restraint, by construction:
|
|
13
|
+
* - counts only, in `data.due_note` — the surface line stays about the tool
|
|
14
|
+
* that was called; whether to raise the note is the HOST's choice
|
|
15
|
+
* - absent entirely at zero (an empty nag cannot be expressed)
|
|
16
|
+
* - argus_check_in is skipped (it IS the due surface — no duplication)
|
|
17
|
+
* - never overwrites a due_note a tool set itself; never touches errors
|
|
18
|
+
* - any internal failure returns the result untouched (a broken note must
|
|
19
|
+
* never tax the tool call that succeeded)
|
|
20
|
+
*/
|
|
21
|
+
const SKIP_TOOLS = new Set(['argus_check_in']);
|
|
22
|
+
export function appendDueNote(toolName, args, result) {
|
|
23
|
+
try {
|
|
24
|
+
if (result.isError || SKIP_TOOLS.has(toolName))
|
|
25
|
+
return result;
|
|
26
|
+
const sc = result.structuredContent;
|
|
27
|
+
if (!sc || sc['ok'] !== true)
|
|
28
|
+
return result;
|
|
29
|
+
const dir = resolveToolArgusDir(args['argus_dir']); // unbound → throws → untouched
|
|
30
|
+
const today = resolveToday({ override: args['today_override'] });
|
|
31
|
+
const state = replayLedger(dir, today);
|
|
32
|
+
const contractsDue = state.overdue.length;
|
|
33
|
+
const premiseFactsDue = groupDuePremises(duePremises(state)).length;
|
|
34
|
+
if (contractsDue === 0 && premiseFactsDue === 0)
|
|
35
|
+
return result;
|
|
36
|
+
const parts = [];
|
|
37
|
+
if (premiseFactsDue > 0)
|
|
38
|
+
parts.push(`${premiseFactsDue} premise fact(s) to re-check (argus_recheck)`);
|
|
39
|
+
if (contractsDue > 0)
|
|
40
|
+
parts.push(`${contractsDue} contract(s) to settle (argus_settle)`);
|
|
41
|
+
const data = (sc['data'] ??= {});
|
|
42
|
+
if (!('due_note' in data))
|
|
43
|
+
data['due_note'] = parts.join(' · ');
|
|
44
|
+
const na = sc['next_actions'];
|
|
45
|
+
if (Array.isArray(na) && !na.includes('argus_check_in'))
|
|
46
|
+
na.push('argus_check_in');
|
|
47
|
+
// Refresh the text mirror — envelope() serialized before this annotation.
|
|
48
|
+
result.content = [{ type: 'text', text: JSON.stringify(sc, null, 2) }];
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return result;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Elicitation seam (MCP elicitation/create). Lets a tool ask the END USER a
|
|
3
|
+
* structured choice — the MCP-native equivalent of a choice+free-text picker —
|
|
4
|
+
* for spine-SAFE inputs only: settlement outcome, concern selection, predicate
|
|
5
|
+
* wording. Never for the decision crux (a multiple-choice crux is a fork, which
|
|
6
|
+
* the spine forbids).
|
|
7
|
+
*
|
|
8
|
+
* The server injects the real elicitor at startup. When the host doesn't support
|
|
9
|
+
* elicitation (many don't yet), `elicit` returns null and callers fall back to
|
|
10
|
+
* the text flow — no crash, no dead end.
|
|
11
|
+
*/
|
|
12
|
+
let _elicit = null;
|
|
13
|
+
export function setElicitor(fn) {
|
|
14
|
+
_elicit = fn;
|
|
15
|
+
}
|
|
16
|
+
export function canElicit() {
|
|
17
|
+
return _elicit !== null;
|
|
18
|
+
}
|
|
19
|
+
/** Ask the user; returns null if unsupported / errored / not accepted. */
|
|
20
|
+
export async function elicit(message, requestedSchema) {
|
|
21
|
+
if (!_elicit)
|
|
22
|
+
return null;
|
|
23
|
+
try {
|
|
24
|
+
const res = await _elicit(message, requestedSchema);
|
|
25
|
+
return res.action === 'accept' && res.content ? res.content : null;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return null; // host declared support but failed — fall back to text
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export function envelope(e) {
|
|
2
|
+
return {
|
|
3
|
+
content: [{ type: 'text', text: JSON.stringify(e, null, 2) }],
|
|
4
|
+
structuredContent: e,
|
|
5
|
+
};
|
|
6
|
+
}
|
|
7
|
+
export function toolError(e) {
|
|
8
|
+
return {
|
|
9
|
+
content: [{ type: 'text', text: JSON.stringify(e, null, 2) }],
|
|
10
|
+
structuredContent: e,
|
|
11
|
+
isError: true,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { safeSegment, assertInside } from './safe-path.js';
|
|
3
|
+
/**
|
|
4
|
+
* Filesystem layout (blueprint §3.0). `argus_dir` IS the `.argus/` directory.
|
|
5
|
+
*
|
|
6
|
+
* Single-identity model: one decision = one `id` = one directory
|
|
7
|
+
* `sessions/{id}/`. There is no `versions/{label}/` tier anymore (clean break).
|
|
8
|
+
* The bearing and the receipt live at fixed names inside the session dir.
|
|
9
|
+
*
|
|
10
|
+
* Every `id` flows through `safeSegment`, and every built path is checked with
|
|
11
|
+
* `assertInside` so a crafted id can never escape `.argus/`.
|
|
12
|
+
*/
|
|
13
|
+
export const sessionsRoot = (argusDir) => path.join(argusDir, 'sessions');
|
|
14
|
+
export const ledgerDir = (argusDir) => path.join(argusDir, 'ledger');
|
|
15
|
+
export const ledgerPath = (argusDir) => path.join(ledgerDir(argusDir), 'ledger.jsonl');
|
|
16
|
+
export const configPath = (argusDir) => path.join(argusDir, 'config.yaml');
|
|
17
|
+
export const boundMarkerPath = (argusDir) => path.join(argusDir, '.bound');
|
|
18
|
+
export function sessionDir(argusDir, id) {
|
|
19
|
+
const root = sessionsRoot(argusDir);
|
|
20
|
+
const dir = path.join(root, safeSegment(id, 'id'));
|
|
21
|
+
assertInside(root, dir);
|
|
22
|
+
return dir;
|
|
23
|
+
}
|
|
24
|
+
export function bearingPath(argusDir, id) {
|
|
25
|
+
return path.join(sessionDir(argusDir, id), 'current_bearing.json');
|
|
26
|
+
}
|
|
27
|
+
export function receiptPath(argusDir, id) {
|
|
28
|
+
return path.join(sessionDir(argusDir, id), 'receipt.json');
|
|
29
|
+
}
|
|
30
|
+
export function sessionFilePath(argusDir, id) {
|
|
31
|
+
return path.join(sessionDir(argusDir, id), 'session.json');
|
|
32
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import fsP from 'fs/promises';
|
|
3
|
+
import { ledgerPath, ledgerDir } from './layout.js';
|
|
4
|
+
import { SCHEMA_VERSION } from './spine.js';
|
|
5
|
+
export async function appendLedger(argusDir, events, now) {
|
|
6
|
+
await fsP.mkdir(ledgerDir(argusDir), { recursive: true });
|
|
7
|
+
const lPath = ledgerPath(argusDir);
|
|
8
|
+
const lines = events
|
|
9
|
+
.map((ev) => JSON.stringify({ v: SCHEMA_VERSION, ts: ev.ts || now, ...ev }))
|
|
10
|
+
.join('\n') + '\n';
|
|
11
|
+
await new Promise((resolve, reject) => {
|
|
12
|
+
let fd;
|
|
13
|
+
try {
|
|
14
|
+
fd = fs.openSync(lPath, fs.constants.O_APPEND | fs.constants.O_CREAT | fs.constants.O_WRONLY);
|
|
15
|
+
fs.writeSync(fd, lines, null, 'utf8');
|
|
16
|
+
resolve();
|
|
17
|
+
}
|
|
18
|
+
catch (e) {
|
|
19
|
+
reject(e);
|
|
20
|
+
}
|
|
21
|
+
finally {
|
|
22
|
+
if (fd !== undefined)
|
|
23
|
+
fs.closeSync(fd);
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
return { written: events.length };
|
|
27
|
+
}
|