argus-decision-mcp 1.7.0 → 1.9.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/dist/lib/ambient-prefs.js +37 -0
- package/dist/lib/spine.js +2 -2
- package/dist/tools/check-in.js +6 -4
- package/dist/tools/premises.js +51 -1
- package/dist/tools/public-tools.js +2 -2
- package/dist/tools/seal.js +51 -55
- package/dist/tools/settle.js +15 -0
- package/package.json +1 -1
|
@@ -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
|
@@ -56,7 +56,7 @@ export const SERVER_INSTRUCTIONS = [
|
|
|
56
56
|
'',
|
|
57
57
|
'Run these three as a CONTINUOUS BACKGROUND SENSE over the whole conversation — never as a checklist you make the user complete:',
|
|
58
58
|
'',
|
|
59
|
-
'1. NOTICE A PREDICTION AS IT PASSES. When the user, doing their work, makes a claim reality can later check — "this should cut costs ~10%", "we\'ll ship Friday", "churn drops once we launch" — that IS a prediction, whether or not they call it one. The tell is a claim that pairs a direction or target with a horizon or a number (a "will / should / by-Friday" plus something measurable, or a ship/close/hire event). But it must be a claim the user COMMITS TO as their own: a question they put to YOU — which option is better, whether something will work, "can you draft / build / configure X" — is soliciting your help, not a prediction they are making, however weighty the topic; answer it and stay out. Do not wait to be told "save this." On a CONSEQUENTIAL one, offer ONCE: turn it into a falsifiable line in THEIR words (e.g. "ship Friday" → "shipped to TestFlight by 2026-07-10"), take a check-by from the horizon they named, and call argus_predict with confirm_draft:true — the one-tap
|
|
59
|
+
'1. NOTICE A PREDICTION AS IT PASSES. When the user, doing their work, makes a claim reality can later check — "this should cut costs ~10%", "we\'ll ship Friday", "churn drops once we launch" — that IS a prediction, whether or not they call it one. The tell is a claim that pairs a direction or target with a horizon or a number (a "will / should / by-Friday" plus something measurable, or a ship/close/hire event). But it must be a claim the user COMMITS TO as their own: a question they put to YOU — which option is better, whether something will work, "can you draft / build / configure X" — is soliciting your help, not a prediction they are making, however weighty the topic; answer it and stay out. Do not wait to be told "save this." On a CONSEQUENTIAL one, offer ONCE: turn it into a falsifiable line in THEIR words (e.g. "ship Friday" → "shipped to TestFlight by 2026-07-10"), take a check-by from the horizon they named, and call argus_predict with confirm_draft:true — the one-tap confirm picker (Accept to keep, or reword / adjust the date, Decline to skip) IS the ask; never ask "want me to save this?" in prose and wait. When they dictate one word-for-word, just save it (predicate_owner:"user"). On a trivial or flat call, stay quiet; a blank is honest. Record exactly ONE falsifiable claim per predicate: if the turn carries two (e.g. a ship date AND closing deals, or two separate numbers), keep only the single most load-bearing one — never conjoin them with "and"/"so" into one predicate, which stacks two predictions that cannot settle cleanly. Notice it ONLY as it is spoken this turn: if the claim surfaced turns ago and the user has since moved to a different topic, the moment has passed — never reach back to resurface a past turn\'s claim mid-new-topic (a stale offer reads as not listening).',
|
|
60
60
|
'',
|
|
61
61
|
'2. NOTICE THE OUTCOME AS IT SURFACES. This is the whole reason you live in the conversation. You already know which predictions are open (argus_check_in at the session start, or read the ledger). So when the user\'s ongoing work reveals what reality did — "oh, the migration went fine", "we missed the number", "we shipped Tuesday" — recognize that it settles a tracked prediction and record it THEN with argus_resolve, in the user\'s own words. Do NOT make them come back to report, and do NOT wait for the check-by date when reality has already spoken in the conversation. Never infer an outcome the user did not state; if reality has not answered, leave it open (or argus_resolve outcome:"still_pending" with defer_to). Recording an outcome is neutral bookkeeping — never praise, never grade.',
|
|
62
62
|
'',
|
|
@@ -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
|
/**
|
package/dist/tools/check-in.js
CHANGED
|
@@ -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 {
|
|
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:
|
|
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:
|
|
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 저장소를 읽어 다른 프로젝트의 결정 원문을
|
package/dist/tools/premises.js
CHANGED
|
@@ -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,55 @@ 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
|
+
// Native Accept/Decline (2026-07-24), mirroring seal: Accept blank → keep
|
|
207
|
+
// (provenance ai_surfaced intact), Accept + reword → the user's words
|
|
208
|
+
// (user_stated, draft kept as ai_original), Decline → skip. One keystroke
|
|
209
|
+
// to keep — no required enum to expand.
|
|
210
|
+
const picked = await elicit(dLocale === 'ko'
|
|
211
|
+
? `이 결정이 딛고 선 전제로 기록할까요?\n"${draft.text}"\n\n그대로면 Accept · 고치려면 아래 칸에 쓰고 Accept · 기록 안 하려면 Decline.`
|
|
212
|
+
: `Record this as a premise the decision rests on?\n"${draft.text}"\n\nAccept to keep · to reword, type it below and Accept · Decline to skip.`, { type: 'object', properties: {
|
|
213
|
+
reword: {
|
|
214
|
+
type: 'string',
|
|
215
|
+
description: dLocale === 'ko' ? '전제를 고쳐 쓰려면 여기에 적으세요. 비우면 위 문장 그대로 기록합니다.' : 'To reword the premise, type it here. Leave blank to keep the statement above.',
|
|
216
|
+
},
|
|
217
|
+
} });
|
|
218
|
+
if (!picked) {
|
|
219
|
+
// Decline / cancel → drop ONLY the draft; the user's own premises in the
|
|
220
|
+
// same call still record below.
|
|
221
|
+
inputs.splice(inputs.indexOf(draft), 1);
|
|
222
|
+
if (inputs.length === 0) {
|
|
223
|
+
return envelope({ ok: true, tool: 'argus_premises', surface: dLocale === 'ko' ? '기록하지 않았습니다.' : 'Not recorded.', next_actions: ['stop'], data: { recorded: false, choice: 'declined' } });
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
const wording = typeof picked['reword'] === 'string' ? picked['reword'].trim() : '';
|
|
228
|
+
if (wording) {
|
|
229
|
+
if (wording.length < 4 || wording.length > 400) {
|
|
230
|
+
return envelope({ ok: true, tool: 'argus_premises', surface: dLocale === 'ko' ? '그럼 그 전제를 원하는 문장으로 알려주세요 (4~400자). 그 말 그대로 기록하겠습니다.' : 'Then tell me the premise in your own words (4–400 chars) and I will record exactly that.', next_actions: ['argus_capture'], data: { recorded: false, choice: 'reword' } });
|
|
231
|
+
}
|
|
232
|
+
draft.ai_original = draft.ai_original ?? draft.text;
|
|
233
|
+
draft.text = wording;
|
|
234
|
+
draft.source = 'user_stated';
|
|
235
|
+
}
|
|
236
|
+
// Accept blank → keep as drafted, provenance ai_surfaced intact.
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
191
240
|
// Dedup against the ledger by stable id. Three cases, kept distinct so a
|
|
192
241
|
// re-add is never silently swallowed behind a misleading "already recorded":
|
|
193
242
|
// - collides with an ACTIVE premise (same text) → true idempotent dup (skip)
|
|
@@ -273,6 +322,7 @@ async function opAdd(dir, id, today, now, state, existing, a) {
|
|
|
273
322
|
const echo = events.map((e) => ({
|
|
274
323
|
ref: `P${e.ordinal}`, premise_id: e.premise_id, kind: e.kind, text: e.text,
|
|
275
324
|
external: e.external, load_bearing: e.load_bearing, source: e.source,
|
|
325
|
+
...(e['ai_original'] ? { ai_original: e['ai_original'] } : {}),
|
|
276
326
|
monitored: e.kind === 'premise' && e.external === true && e.load_bearing === true,
|
|
277
327
|
}));
|
|
278
328
|
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 {
|
|
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'] =
|
|
302
|
+
data['standing_sense'] = tunedStandingSense();
|
|
303
303
|
result.content = [{ type: 'text', text: JSON.stringify(sc, null, 2) }];
|
|
304
304
|
}
|
|
305
305
|
catch { /* 라이더 실패는 침묵 — 본 결과를 해치지 않는다 */ }
|
package/dist/tools/seal.js
CHANGED
|
@@ -36,8 +36,8 @@ const inputSchema = z.strictObject({
|
|
|
36
36
|
id: zId.describe('A short slug you pick for this decision (e.g. "q3-cutover"). A fresh id starts the record on its own.'),
|
|
37
37
|
predicate: z.string().min(8).max(400).describe('A prediction reality can mark true/false. Good: "cutover downtime < 5 min". Bad: "it will go well".'),
|
|
38
38
|
check_by: zDate.describe('YYYY-MM-DD, a real future date when the result can be checked.'),
|
|
39
|
-
predicate_owner: z.enum(['user', 'ai_surfaced']).describe('Provenance. Never forge. "user" = the user wrote or affirmed it. "ai_surfaced" = Argus drafted, unconfirmed — on a host with a picker this AUTOMATICALLY shows one-tap
|
|
40
|
-
confirm_draft: z.boolean().optional().describe('Optional extra confirmation: force the one-tap
|
|
39
|
+
predicate_owner: z.enum(['user', 'ai_surfaced']).describe('Provenance. Never forge. "user" = the user wrote or affirmed it. "ai_surfaced" = Argus drafted, unconfirmed — on a host with a picker this AUTOMATICALLY shows a one-tap confirm before saving.'),
|
|
40
|
+
confirm_draft: z.boolean().optional().describe('Optional extra confirmation: force the one-tap confirm even for a "user" predicate. ai_surfaced predicates get it automatically on supporting hosts. The picker maps to the host\'s native Accept/Decline: Accept with both fields blank keeps the draft as theirs, Accept with `reword` saves the user\'s wording, Accept with `check_by` adjusts the date, Decline records nothing. Without picker support, saving proceeds — confirm in your own message first.'),
|
|
41
41
|
basis: z.enum(['judgment', 'luck', 'mixed', 'unsure']).optional(),
|
|
42
42
|
real_question: z.string().max(400).describe('The real question behind the answer (receipt).').optional(),
|
|
43
43
|
unverified_assumption: z.string().max(400).describe('The core assumption not yet verified (receipt).').optional(),
|
|
@@ -64,7 +64,7 @@ export const seal = {
|
|
|
64
64
|
return toolError({ ok: false, tool: 'argus_seal', error_code: vErr.code, message: vErr.message, recovery: vErr.recovery });
|
|
65
65
|
}
|
|
66
66
|
let predicate = String(a['predicate']);
|
|
67
|
-
|
|
67
|
+
let checkBy = String(a['check_by']);
|
|
68
68
|
// The DATE part of `now` must equal the tz-aware logical `today`. Plain
|
|
69
69
|
// new Date().toISOString() is always UTC, so a Korea (UTC+9) user sealing
|
|
70
70
|
// at 08:00 KST (= 23:00Z the day before) got a receipt dated YESTERDAY —
|
|
@@ -76,66 +76,62 @@ export const seal = {
|
|
|
76
76
|
let locale = resolveResponseLocale(dir, predicate);
|
|
77
77
|
let T = SURFACES[locale].tools.seal;
|
|
78
78
|
// One-tap confirm for a DRAFTED predicate (the activation fix; §9.7 O1
|
|
79
|
-
// 방4)
|
|
80
|
-
//
|
|
81
|
-
//
|
|
82
|
-
//
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
//
|
|
79
|
+
// 방4). Design (2026-07-24, 창업자 도그푸딩): the picker maps to
|
|
80
|
+
// elicitation's NATIVE Accept/Decline instead of a required 3-way enum —
|
|
81
|
+
// a required enum forced "expand → pick → Accept" (3-4 keystrokes) and
|
|
82
|
+
// rendered as an unset field the user had to hunt for. Now:
|
|
83
|
+
// Accept, both fields blank → KEEP the draft (the user affirmed it → theirs)
|
|
84
|
+
// Accept + `reword` → record the user's wording (user_stated)
|
|
85
|
+
// Accept + `check_by` → adjust the horizon inline (the "그 날짜 쎄"
|
|
86
|
+
// escape — keep the statement, fix only the date)
|
|
87
|
+
// Decline / cancel → SKIP (record nothing)
|
|
88
|
+
// Both edit fields are OPTIONAL, so Accept is actionable in one keystroke.
|
|
89
|
+
// STRUCTURAL trigger: an ai_surfaced predicate fires the picker on any
|
|
90
|
+
// capable host even if the model forgot confirm_draft. No elicitation →
|
|
91
|
+
// the seal proceeds with honest ai_surfaced provenance (friction escape
|
|
88
92
|
// stays; forced typing is NOT the invariant — honest provenance is).
|
|
89
|
-
let elicitedKeep = false; // v2 provenance
|
|
93
|
+
let elicitedKeep = false; // v2 provenance: only elicit-channel confirmation counts as elicited_user (II-B)
|
|
90
94
|
if ((a['confirm_draft'] === true || a['predicate_owner'] === 'ai_surfaced') && canElicit()) {
|
|
91
|
-
const picked = await elicit(locale === 'ko'
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
95
|
+
const picked = await elicit(locale === 'ko'
|
|
96
|
+
? `이 예측으로 기록할까요?\n"${predicate}"\n확인일 ${checkBy}\n\n그대로면 Accept · 문장이나 날짜를 고치려면 아래 칸에 쓰고 Accept · 기록 안 하려면 Decline.`
|
|
97
|
+
: `Record this prediction?\n"${predicate}"\ncheck-by ${checkBy}\n\nAccept to keep · to change the wording or date, fill a field below and Accept · Decline to skip.`, { type: 'object', properties: {
|
|
98
|
+
reword: {
|
|
99
|
+
type: 'string',
|
|
100
|
+
description: locale === 'ko' ? '예측 문장을 고쳐 쓰려면 여기에 적으세요. 비우면 위 문장 그대로 기록합니다.' : 'To reword the prediction, type it here. Leave blank to keep the statement above.',
|
|
96
101
|
},
|
|
97
|
-
|
|
102
|
+
check_by: {
|
|
98
103
|
type: 'string',
|
|
99
|
-
description: locale === 'ko' ?
|
|
104
|
+
description: locale === 'ko' ? `확인일을 바꾸려면 YYYY-MM-DD로 적으세요. 비우면 ${checkBy} 그대로.` : `To change the check-by date, type YYYY-MM-DD. Leave blank to keep ${checkBy}.`,
|
|
100
105
|
},
|
|
101
106
|
} });
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
if (wording) {
|
|
106
|
-
// One-shot reword: the user's own words replace the draft — but they
|
|
107
|
-
// pass the SAME falsifiability gate as any predicate (a vibe typed by
|
|
108
|
-
// the user is still unsettleable; refusing honestly beats recording a
|
|
109
|
-
// dead reminder).
|
|
110
|
-
if (wording.length < 8 || wording.length > 400) {
|
|
111
|
-
return toolError({ ok: false, tool: 'argus_seal', error_code: 'SEAL_INVALID', message: locale === 'ko' ? `다시 쓴 예측이 너무 ${wording.length < 8 ? '짧습니다' : '깁니다'} (8~400자).` : `The reworded prediction is too ${wording.length < 8 ? 'short' : 'long'} (8–400 chars).`, recovery: locale === 'ko' ? '예측 문장을 8~400자로 다시 알려주세요.' : 'Give the prediction again in 8–400 characters.' });
|
|
112
|
-
}
|
|
113
|
-
const rErr = validateSeal(wording, checkBy, today);
|
|
114
|
-
if (rErr) {
|
|
115
|
-
return toolError({ ok: false, tool: 'argus_seal', error_code: rErr.code, message: rErr.message, recovery: rErr.recovery });
|
|
116
|
-
}
|
|
117
|
-
predicate = wording;
|
|
118
|
-
a = { ...a, predicate: wording, predicate_owner: 'user' };
|
|
119
|
-
elicitedKeep = true; // typed through the elicit channel — evidenced user input
|
|
120
|
-
// The voice follows the USER's wording now (they may have reworded
|
|
121
|
-
// an English draft in Korean).
|
|
122
|
-
locale = resolveResponseLocale(dir, predicate);
|
|
123
|
-
T = SURFACES[locale].tools.seal;
|
|
124
|
-
}
|
|
125
|
-
else {
|
|
126
|
-
// Reword chosen but no wording typed (or the host renders enum-only
|
|
127
|
-
// forms) — fall back to the two-step: ask in chat, model re-calls.
|
|
128
|
-
return envelope({ ok: true, tool: 'argus_seal', surface: locale === 'ko' ? '그럼 원하는 예측 문장을 알려주세요. 그 말 그대로 저장하겠습니다.' : "Then tell me the prediction in your own words and I'll save exactly that.", next_actions: ['argus_predict'], data: { sealed: false, choice: 'reword' } });
|
|
129
|
-
}
|
|
107
|
+
if (!picked) {
|
|
108
|
+
// Decline / cancel / no-accept → record nothing (respect the non-yes).
|
|
109
|
+
return envelope({ ok: true, tool: 'argus_seal', surface: locale === 'ko' ? '기록하지 않았습니다.' : 'Not recorded.', next_actions: ['stop'], data: { sealed: false, choice: 'declined' } });
|
|
130
110
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
111
|
+
// Accept. Apply any optional edits, then re-gate through validateSeal —
|
|
112
|
+
// a vibe typed by the user is still unsettleable; refusing honestly beats
|
|
113
|
+
// recording a dead reminder.
|
|
114
|
+
const rw = typeof picked['reword'] === 'string' ? picked['reword'].trim() : '';
|
|
115
|
+
const cbEdit = typeof picked['check_by'] === 'string' ? picked['check_by'].trim() : '';
|
|
116
|
+
if (rw)
|
|
117
|
+
predicate = rw;
|
|
118
|
+
if (cbEdit)
|
|
119
|
+
checkBy = cbEdit;
|
|
120
|
+
if (rw && rw.length > 400) {
|
|
121
|
+
return toolError({ ok: false, tool: 'argus_seal', error_code: 'SEAL_INVALID', message: locale === 'ko' ? '다시 쓴 예측이 너무 깁니다 (최대 400자).' : 'The reworded prediction is too long (max 400 chars).', recovery: locale === 'ko' ? '예측 문장을 400자 이내로 다시 알려주세요.' : 'Give the prediction again within 400 characters.' });
|
|
122
|
+
}
|
|
123
|
+
if (rw || cbEdit) {
|
|
124
|
+
const rErr = validateSeal(predicate, checkBy, today);
|
|
125
|
+
if (rErr) {
|
|
126
|
+
return toolError({ ok: false, tool: 'argus_seal', error_code: rErr.code, message: rErr.message, recovery: rErr.recovery });
|
|
127
|
+
}
|
|
134
128
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
129
|
+
// Accept (blank or edited) = the user affirmed it → it is theirs now.
|
|
130
|
+
a = { ...a, predicate, check_by: checkBy, predicate_owner: 'user' };
|
|
131
|
+
elicitedKeep = true;
|
|
132
|
+
if (rw) { // voice follows the user's wording (they may have reworded EN→KO)
|
|
133
|
+
locale = resolveResponseLocale(dir, predicate);
|
|
134
|
+
T = SURFACES[locale].tools.seal;
|
|
139
135
|
}
|
|
140
136
|
}
|
|
141
137
|
await ensurePrivacyGitignore(dir);
|
package/dist/tools/settle.js
CHANGED
|
@@ -65,12 +65,27 @@ export const settle = {
|
|
|
65
65
|
: ['It held', 'Avoided', 'Partially', 'Still unclear', 'Missed: my read was wrong'],
|
|
66
66
|
description: pickerLocale === 'ko' ? '저장한 예측에 현실이 어떻게 답했는지 고르세요.' : 'What reality did to your sealed prediction.',
|
|
67
67
|
},
|
|
68
|
+
// Capture what-happened in the SAME picker so a settle that reached
|
|
69
|
+
// the picker doesn't dead-end on WHAT_HAPPENED_REQUIRED after the user
|
|
70
|
+
// already answered. Optional: if the model already passed what_happened
|
|
71
|
+
// from the conversation, the user can leave this blank. What the user
|
|
72
|
+
// types here is THEIR words (spine-safe — never model-inferred).
|
|
73
|
+
what_happened: {
|
|
74
|
+
type: 'string',
|
|
75
|
+
description: pickerLocale === 'ko' ? '무슨 일이 있었는지 당신의 말로 한 줄. (아직 불분명이면 비워도 됩니다.)' : "One line on what actually happened, in your words. (Leave blank if still unclear.)",
|
|
76
|
+
},
|
|
68
77
|
},
|
|
69
78
|
required: ['outcome'],
|
|
70
79
|
});
|
|
71
80
|
const v = picked?.['outcome'];
|
|
72
81
|
if (v === 'held' || v === 'avoided' || v === 'partial' || v === 'still_pending' || v === 'missed')
|
|
73
82
|
outcome = v;
|
|
83
|
+
// If the model didn't already supply what_happened, take the user's
|
|
84
|
+
// picker text (their own words) so the settle completes in one round.
|
|
85
|
+
const pickedWhat = typeof picked?.['what_happened'] === 'string' ? picked['what_happened'].trim() : '';
|
|
86
|
+
if (pickedWhat && !(typeof a['what_happened'] === 'string' && a['what_happened'].trim())) {
|
|
87
|
+
a = { ...a, what_happened: pickedWhat };
|
|
88
|
+
}
|
|
74
89
|
}
|
|
75
90
|
if (!outcome) {
|
|
76
91
|
return toolError({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "argus-decision-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.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",
|