argus-decision-mcp 1.7.0 → 1.8.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.
@@ -0,0 +1,37 @@
1
+ import fs from 'fs';
2
+ import os from 'os';
3
+ import path from 'path';
4
+ import { STANDING_SENSE_REFRESH } from './spine.js';
5
+ export function readAmbientPrefs() {
6
+ const fallback = { sensitivity: 'normal', optOut: false };
7
+ try {
8
+ const home = process.env['ARGUS_HOME'] || path.join(os.homedir(), '.argus');
9
+ const cfg = JSON.parse(fs.readFileSync(path.join(home, 'config.json'), 'utf8'));
10
+ const amb = cfg && typeof cfg['ambient'] === 'object' && cfg['ambient'] !== null
11
+ ? cfg['ambient'] : null;
12
+ if (!amb)
13
+ return fallback;
14
+ const s = amb['sensitivity'];
15
+ return {
16
+ sensitivity: s === 'low' || s === 'high' ? s : 'normal',
17
+ optOut: amb['opt_out'] === true,
18
+ };
19
+ }
20
+ catch {
21
+ return fallback; // 설정 없음/파손 = 기본 감도 (조용한 기본, 시끄러운 실패 불필요 — 선호 부재는 gap이 아니다)
22
+ }
23
+ }
24
+ const LOW_SUFFIX = ' (User sensitivity preference: LOW — offer only on the very clearest consequential commitments; when in any doubt, stay silent.)';
25
+ const HIGH_SUFFIX = ' (User sensitivity preference: HIGH — they asked for more surfacing; the restraint gate still applies, but on a consequential call do not skip the one offer out of excess caution.)';
26
+ const OFF_SUFFIX = ' (User preference: ambient offers are OFF — make no unsolicited predict/capture offers. Recording an outcome the user themselves states is bookkeeping and stays on.)';
27
+ /** 감도 선호를 반영한 배경감각 줄 — 모든 주입 지점은 이걸 쓴다(직접 상수 금지). */
28
+ export function tunedStandingSense() {
29
+ const p = readAmbientPrefs();
30
+ if (p.optOut)
31
+ return STANDING_SENSE_REFRESH + OFF_SUFFIX;
32
+ if (p.sensitivity === 'low')
33
+ return STANDING_SENSE_REFRESH + LOW_SUFFIX;
34
+ if (p.sensitivity === 'high')
35
+ return STANDING_SENSE_REFRESH + HIGH_SUFFIX;
36
+ return STANDING_SENSE_REFRESH;
37
+ }
package/dist/lib/spine.js CHANGED
@@ -80,7 +80,7 @@ export const SERVER_INSTRUCTIONS = [
80
80
  * 뿐이므로, Argus 툴이 불릴 때마다 이 한 줄이 배경감각을 되살린다). 지침이지
81
81
  * 평결이 아니며, SERVER_INSTRUCTIONS의 3감각과 같은 내용의 압축이다.
82
82
  */
83
- export const STANDING_SENSE_REFRESH = 'Background sense from here on: the open predictions above are unsettled. If the user\'s ongoing words reveal what reality did to one — even in passing, even by pronoun ("그거 결국 잘 됐어") — record it with argus_resolve in their words THEN; never infer an outcome they did not state. Keep noticing new checkable claims (argus_predict, offer once on a consequential one) and the load-bearing assumption a decision rests on — often unstated (argus_capture, at most one, as your ai_surfaced draft). But stay silent when the turn only asks you to do a task, poses a logistics or recommendation question, is small talk, or concerns a call the user already closed — and once they have moved on to another topic. A blank is honest; when unsure, do nothing.';
83
+ export const STANDING_SENSE_REFRESH = 'Background sense from here on: the open predictions above are unsettled. If the user\'s ongoing words reveal what reality did to one — even in passing, even by pronoun ("그거 결국 잘 됐어") — record it with argus_resolve in their words THEN; never infer an outcome they did not state. Keep noticing new checkable claims (argus_predict, offer once on a consequential one) and the load-bearing assumption a decision rests on — often unstated (argus_capture, at most one, as your ai_surfaced draft). But stay silent when the turn only asks you to do a task, poses a logistics or recommendation question, is small talk, or concerns a call the user already closed — and once they have moved on to another topic. ("Only a task" holds only while no consequential call rides under it: a plan handed to you as a to-do — "here is the plan, start with X" — is still a plan; do the task AND offer its single load-bearing premise once.) A blank is honest; when unsure, do nothing.';
84
84
  /** The schema version stamped on every persisted record (addendum N1). */
85
85
  export const SCHEMA_VERSION = 1;
86
86
  /**
@@ -5,8 +5,9 @@ import { duePremises, groupDuePremises, dueOpenQuestions } from '../lib/premises
5
5
  import { readReceipt, SKIPPED } from '../lib/receipt.js';
6
6
  import { SURFACES, resolveResponseLocale, surfaceLocale } from '../lib/surfaces.js';
7
7
  import { z } from 'zod';
8
- import { STANDING_SENSE_REFRESH } from '../lib/spine.js';
8
+ import { tunedStandingSense } from '../lib/ambient-prefs.js';
9
9
  import { envelope } from '../lib/envelope.js';
10
+ import { canElicit } from '../lib/elicit.js';
10
11
  import { ENVELOPE_OUTPUT_SCHEMA, zArgusDir, zDate } from './tool-types.js';
11
12
  import { handleToolException } from './errors.js';
12
13
  import { briefDivergence, readV2Brief } from '../v2/mirror.js';
@@ -271,14 +272,14 @@ export const checkIn = {
271
272
  ok: true, tool: 'argus_check_in',
272
273
  surface: S.first_run,
273
274
  next_actions: ['argus_capture'],
274
- data: { due: [], due_count: 0, due_premises: [], due_premise_count: 0, due_open_questions: [], due_open_question_count: 0, first_run: true, today },
275
+ data: { due: [], due_count: 0, due_premises: [], due_premise_count: 0, due_open_questions: [], due_open_question_count: 0, first_run: true, today, picker: canElicit() ? 'one_tap' : 'text_fallback' },
275
276
  });
276
277
  }
277
278
  return envelope({
278
279
  ok: true, tool: 'argus_check_in',
279
280
  surface: mirrorLine + S.nothing_due + accountHint + upcomingLine + fleetLine + integrityLine,
280
281
  next_actions: ['stop'],
281
- data: { due: [], due_count: 0, due_premises: [], due_premise_count: 0, due_open_questions: [], due_open_question_count: 0, ...(openWatch.length ? { open_predictions: openWatch, standing_sense: STANDING_SENSE_REFRESH } : {}), ...(upDays > 0 ? { upcoming } : {}), ...(a['fleet'] === true ? { fleet: fleetRows } : {}), ...watchData, today, integrity: ledger.integrity, ...(process.env['ARGUS_V2_DEBUG'] === '1' ? { capture_status: captureStatus, v2_brief: readV2Brief(dir, today), v2_divergence: briefDivergence([], readV2Brief(dir, today)) } : {}) },
282
+ data: { due: [], due_count: 0, due_premises: [], due_premise_count: 0, due_open_questions: [], due_open_question_count: 0, picker: canElicit() ? 'one_tap' : 'text_fallback', ...(openWatch.length ? { open_predictions: openWatch, standing_sense: tunedStandingSense() } : {}), ...(upDays > 0 ? { upcoming } : {}), ...(a['fleet'] === true ? { fleet: fleetRows } : {}), ...watchData, today, integrity: ledger.integrity, ...(process.env['ARGUS_V2_DEBUG'] === '1' ? { capture_status: captureStatus, v2_brief: readV2Brief(dir, today), v2_divergence: briefDivergence([], readV2Brief(dir, today)) } : {}) },
282
283
  });
283
284
  }
284
285
  const parts = [];
@@ -312,6 +313,7 @@ export const checkIn = {
312
313
  surface: mirrorLine + parts.join(' ') + upcomingLine + fleetLine + integrityLine,
313
314
  next_actions: next,
314
315
  data: {
316
+ picker: canElicit() ? 'one_tap' : 'text_fallback',
315
317
  due: dueEnriched, due_count: dueAll.length,
316
318
  ...(dueTruncated > 0 ? { due_truncated: `${dueAll.length} due, showing ${DUE_TOP} oldest` } : {}),
317
319
  due_premises: duePrem, due_premise_count: premiseGroups.length,
@@ -321,7 +323,7 @@ export const checkIn = {
321
323
  ...(upDays > 0 ? { upcoming } : {}),
322
324
  ...(a['fleet'] === true ? { fleet: fleetRows } : {}),
323
325
  ...watchData,
324
- ...(openWatch.length ? { open_predictions: openWatch, standing_sense: STANDING_SENSE_REFRESH } : {}),
326
+ ...(openWatch.length ? { open_predictions: openWatch, standing_sense: tunedStandingSense() } : {}),
325
327
  today, integrity: ledger.integrity,
326
328
  // v2 병기/진단은 ARGUS_V2_DEBUG=1 뒤로. 공개 payload에 싣던 v2_brief가
327
329
  // 머신-전역 durable-home 저장소를 읽어 다른 프로젝트의 결정 원문을
@@ -6,7 +6,7 @@ import { resolveContract } from '../lib/resolve-contract.js';
6
6
  import { guardTransition } from '../lib/state-machine.js';
7
7
  import { appendLedger, withLedgerLock } from '../lib/ledger-append.js';
8
8
  import { premiseId, resolvePremiseRef, normalizePremiseText, MAX_ACTIVE_PREMISES, MAX_LOAD_BEARING, } from '../lib/premises.js';
9
- import { elicit } from '../lib/elicit.js';
9
+ import { elicit, canElicit } from '../lib/elicit.js';
10
10
  import { resolveResponseLocale } from '../lib/surfaces.js';
11
11
  import { envelope, toolError } from '../lib/envelope.js';
12
12
  import { ENVELOPE_OUTPUT_SCHEMA, zArgusDir, zId, zDate } from './tool-types.js';
@@ -188,6 +188,59 @@ async function opAdd(dir, id, today, now, state, existing, a) {
188
188
  return toolError({ ok: false, tool: 'argus_premises', error_code: 'PROVENANCE_REQUIRED', message: `source="ai_surfaced" requires ai_original (the model's original wording): "${p.text.slice(0, 60)}"`, recovery: 'Set ai_original to the exact model-drafted sentence, or source="user_stated" if the user wrote it.' });
189
189
  }
190
190
  }
191
+ // One-tap confirm for a DRAFTED premise — seal's picker, mirrored (the ask
192
+ // must be a structural picker, not prose the model may skip). Fires only in
193
+ // the sense's canonical case: exactly ONE ai_surfaced draft in the call
194
+ // (multi-premise structured flows confirm in their own conversation). Keep
195
+ // records it with provenance ai_surfaced UNCHANGED — a tap approves the
196
+ // recording, it does not transfer authorship (predictions differ: a bet must
197
+ // become the user's; a premise is a mirror observation whose honest tag IS
198
+ // the invariant). Reword typed in the form → the user's words, user_stated,
199
+ // with the draft preserved as ai_original. Skip / declined / no elicitation
200
+ // → the friction escape stays: nothing forced, no dead end.
201
+ {
202
+ const aiDrafts = inputs.filter((p) => normalizePremiseSource(p.source) === 'ai_surfaced');
203
+ if (aiDrafts.length === 1 && canElicit()) {
204
+ const draft = aiDrafts[0];
205
+ const dLocale = resolveResponseLocale(dir, draft.text);
206
+ const picked = await elicit(dLocale === 'ko'
207
+ ? `이 결정이 딛고 선 전제로 기록할까요?\n"${draft.text}"`
208
+ : `Record this as a premise the decision rests on?\n"${draft.text}"`, { type: 'object', required: ['choice'], properties: {
209
+ choice: {
210
+ type: 'string', enum: ['keep', 'reword', 'skip'],
211
+ enumNames: dLocale === 'ko' ? ['그대로 기록', '직접 고쳐 쓰기', '건너뛰기'] : ['Keep it', 'Let me reword', 'Skip'],
212
+ description: dLocale === 'ko' ? '이 전제를 기록할지 고르세요.' : 'Whether to record this premise.',
213
+ },
214
+ your_wording: {
215
+ type: 'string',
216
+ description: dLocale === 'ko' ? '"직접 고쳐 쓰기"를 골랐다면 원하는 문장을 여기에 적어 주세요. 그 말 그대로 저장됩니다.' : 'If you chose "Let me reword", type the premise here. It is saved exactly as written.',
217
+ },
218
+ } });
219
+ const choice = picked?.['choice'];
220
+ if (choice === 'reword') {
221
+ const wording = typeof picked?.['your_wording'] === 'string' ? picked['your_wording'].trim() : '';
222
+ if (wording.length >= 4 && wording.length <= 400) {
223
+ draft.ai_original = draft.ai_original ?? draft.text;
224
+ draft.text = wording;
225
+ draft.source = 'user_stated';
226
+ }
227
+ else {
228
+ // Reword chosen but no usable wording (or an enum-only form) — the
229
+ // two-step fallback: ask in chat, the model re-calls with their words.
230
+ return envelope({ ok: true, tool: 'argus_premises', surface: dLocale === 'ko' ? '그럼 그 전제를 원하는 문장으로 알려주세요. 그 말 그대로 기록하겠습니다.' : 'Then tell me the premise in your own words and I will record exactly that.', next_actions: ['argus_capture'], data: { recorded: false, choice: 'reword' } });
231
+ }
232
+ }
233
+ else if (choice !== 'keep') {
234
+ // skip, or a declined/cancelled picker — drop ONLY the draft; the
235
+ // user's own premises in the same call still record below.
236
+ inputs.splice(inputs.indexOf(draft), 1);
237
+ if (inputs.length === 0) {
238
+ return envelope({ ok: true, tool: 'argus_premises', surface: dLocale === 'ko' ? '기록하지 않았습니다.' : 'Not recorded.', next_actions: ['stop'], data: { recorded: false, choice: choice ?? 'declined' } });
239
+ }
240
+ }
241
+ // keep → recorded as drafted below, provenance ai_surfaced intact.
242
+ }
243
+ }
191
244
  // Dedup against the ledger by stable id. Three cases, kept distinct so a
192
245
  // re-add is never silently swallowed behind a misleading "already recorded":
193
246
  // - collides with an ACTIVE premise (same text) → true idempotent dup (skip)
@@ -273,6 +326,7 @@ async function opAdd(dir, id, today, now, state, existing, a) {
273
326
  const echo = events.map((e) => ({
274
327
  ref: `P${e.ordinal}`, premise_id: e.premise_id, kind: e.kind, text: e.text,
275
328
  external: e.external, load_bearing: e.load_bearing, source: e.source,
329
+ ...(e['ai_original'] ? { ai_original: e['ai_original'] } : {}),
276
330
  monitored: e.kind === 'premise' && e.external === true && e.load_bearing === true,
277
331
  }));
278
332
  const monitoredCount = echo.filter((p) => p.monitored).length;
@@ -5,7 +5,7 @@ import { resolveToolArgusDir } from '../lib/argus-dir.js';
5
5
  import { sanitizeOutput } from '../lib/untrusted.js';
6
6
  import { replayLedger } from '../lib/ledger-replay.js';
7
7
  import { resolveToday } from '../lib/resolve-today.js';
8
- import { STANDING_SENSE_REFRESH } from '../lib/spine.js';
8
+ import { tunedStandingSense } from '../lib/ambient-prefs.js';
9
9
  import { configPath } from '../lib/layout.js';
10
10
  import { ENVELOPE_OUTPUT_SCHEMA, zArgusDir, zDate, zId } from './tool-types.js';
11
11
  import { openDecision } from './open-decision.js';
@@ -299,7 +299,7 @@ function attachOpenPredictions(result, args) {
299
299
  // 원장 predicate(사용자 저작 텍스트)를 직접 세탁해야 한다 — 안 하면 ANSI/bidi/
300
300
  // zero-width 벡터가 세탁 안 된 채 모델에 직행하는 새 경로가 된다.
301
301
  data['open_predictions'] = sanitizeOutput(open);
302
- data['standing_sense'] = STANDING_SENSE_REFRESH;
302
+ data['standing_sense'] = tunedStandingSense();
303
303
  result.content = [{ type: 'text', text: JSON.stringify(sc, null, 2) }];
304
304
  }
305
305
  catch { /* 라이더 실패는 침묵 — 본 결과를 해치지 않는다 */ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "argus-decision-mcp",
3
- "version": "1.7.0",
3
+ "version": "1.8.0",
4
4
  "description": "Argus decision-accountability MCP server — clarify a decision, save a falsifiable prediction, and record what reality did. The model never grades you.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",