argus-decision-mcp 1.4.2 → 1.4.6
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/calendar.js +0 -0
- package/dist/lib/due-note.js +6 -1
- package/dist/lib/elicit.js +9 -1
- package/dist/lib/ledger-append.js +91 -19
- package/dist/lib/ledger-replay.js +32 -10
- package/dist/lib/localize-result.js +64 -14
- package/dist/lib/numeric-drift.js +2 -2
- package/dist/lib/receipt.js +17 -1
- package/dist/lib/render-receipt.js +36 -11
- package/dist/lib/resolve-today.js +20 -0
- package/dist/lib/safe-path.js +16 -0
- package/dist/lib/surfaces.js +18 -14
- package/dist/lib/tool-presentation.js +1 -1
- package/dist/lib/untrusted.js +3 -3
- package/dist/lib/validate-seal.js +6 -3
- package/dist/resources.js +6 -1
- package/dist/server.js +15 -10
- package/dist/tools/amend-dismiss.js +21 -0
- package/dist/tools/check-in.js +24 -1
- package/dist/tools/errors.js +4 -4
- package/dist/tools/init-config.js +14 -0
- package/dist/tools/premises.js +22 -4
- package/dist/tools/public-tools.js +20 -7
- package/dist/tools/recall.js +14 -6
- package/dist/tools/recheck.js +41 -6
- package/dist/tools/seal.js +11 -8
- package/dist/tools/settle.js +18 -10
- package/dist/tools/tool-types.js +31 -5
- package/package.json +1 -1
package/dist/lib/surfaces.js
CHANGED
|
@@ -88,6 +88,7 @@ export const SURFACES = {
|
|
|
88
88
|
},
|
|
89
89
|
checkin: {
|
|
90
90
|
nothing_due: 'Nothing is due right now.',
|
|
91
|
+
first_run: 'Argus is ready. Tell me a decision you are weighing. I can clarify what it rests on, save a prediction with a check-by date, and record what actually happened when that date arrives. Nothing is tracked yet; describe a decision to begin.',
|
|
91
92
|
account_hint: ' This screen reads the local decision record only. Predictions saved in your account show up with argus_settings action=sync.',
|
|
92
93
|
upcoming: (n, days) => ` ${n} coming due within ${days} day(s). Informational; no result is due yet.`,
|
|
93
94
|
due_contracts: (n) => `${n} saved prediction(s) past check-by. Time to record what happened (argus_resolve).`,
|
|
@@ -131,7 +132,8 @@ export const SURFACES = {
|
|
|
131
132
|
days_past: (n) => `${n}d past`,
|
|
132
133
|
waiting_group: (n) => `waiting for the outcome (${n})`,
|
|
133
134
|
answer_on: (date) => `due ${date}`,
|
|
134
|
-
settled_group: (n, held, avoided, partial) => `results recorded (${n}): held ${held} · avoided ${avoided} · partial ${partial}`,
|
|
135
|
+
settled_group: (n, held, avoided, partial, missed) => `results recorded (${n}): held ${held} · avoided ${avoided} · partial ${partial} · missed ${missed}`,
|
|
136
|
+
outcome_label: (o) => ({ held: 'held', avoided: 'avoided', partial: 'partial', missed: 'missed', still_pending: 'pending' })[o] ?? o,
|
|
135
137
|
more: (n) => `… (+${n})`,
|
|
136
138
|
record_since: (date) => `on record since ${date}`,
|
|
137
139
|
},
|
|
@@ -145,8 +147,8 @@ export const SURFACES = {
|
|
|
145
147
|
human_only: 'HUMAN-ONLY CALL',
|
|
146
148
|
made_by_label: '…made by',
|
|
147
149
|
made_by: 'Me. (not the model)',
|
|
148
|
-
called_as: '…
|
|
149
|
-
basis_label: (v) => ({ judgment: 'judgment', luck: 'luck', mixed: 'a mix of both', unsure: 'not sure' })[v] ?? v,
|
|
150
|
+
called_as: '…looking back',
|
|
151
|
+
basis_label: (v) => ({ judgment: 'mostly my judgment', luck: 'mostly luck', mixed: 'a mix of both', unsure: 'not sure' })[v] ?? v,
|
|
150
152
|
// A blank field, stated neutrally — "you skipped naming this" read as a
|
|
151
153
|
// nag about the user's completeness on a receipt they wanted plain
|
|
152
154
|
// (experience loop, settler: a zero-judgment surface must not grade even
|
|
@@ -176,7 +178,7 @@ export const SURFACES = {
|
|
|
176
178
|
watch_exit: ' Leaving it unrecorded is fine.',
|
|
177
179
|
reconfirm: 'These signals look contradictory (high stakes yet easily reversible). Re-confirm stakes and reversibility before going further.',
|
|
178
180
|
opened_with_crux: (crux) => `Opened. The one question that decides this: ${crux}`,
|
|
179
|
-
opened_bare: 'Opened.
|
|
181
|
+
opened_bare: 'Opened. If one neutral question decides this, naming it is the next step. Then save a falsifiable prediction.',
|
|
180
182
|
},
|
|
181
183
|
seal: {
|
|
182
184
|
sealed: (predicate, checkBy) => `Prediction saved. "${predicate}" Check-by is ${checkBy}. Come back then with argus_resolve to record what happened.`,
|
|
@@ -230,6 +232,7 @@ export const SURFACES = {
|
|
|
230
232
|
},
|
|
231
233
|
checkin: {
|
|
232
234
|
nothing_due: '지금 확인할 차례가 된 것은 없습니다.',
|
|
235
|
+
first_run: 'Argus가 준비됐습니다. 지금 고민 중인 결정을 하나 말해 주세요. 그 결정이 어떤 전제 위에 서 있는지 정리하고, 예측과 확인일을 저장한 뒤, 그 날이 오면 실제로 어떻게 됐는지 기록합니다. 아직 기록된 것은 없습니다. 결정을 말하면 시작합니다.',
|
|
233
236
|
account_hint: ' 이 화면은 로컬 판단 기록만 읽습니다. 계정에 저장한 예측은 argus_settings action=sync로 가져올 수 있습니다.',
|
|
234
237
|
upcoming: (n, days) => ` ${days}일 안에 확인일이 오는 예측이 ${n}건 있습니다. 아직 결과를 기록할 때는 아닙니다.`,
|
|
235
238
|
due_contracts: (n) => `저장한 예측 ${n}건이 확인일을 지났습니다. 실제 결과를 기록할 차례입니다 (argus_resolve).`,
|
|
@@ -273,9 +276,10 @@ export const SURFACES = {
|
|
|
273
276
|
days_past: (n) => `${n}일 경과`,
|
|
274
277
|
waiting_group: (n) => `결과를 기다리는 중 (${n})`,
|
|
275
278
|
answer_on: (date) => `답 ${date}`,
|
|
276
|
-
settled_group: (n, held, avoided, partial) => `결과 기록됨 (${n}): 그렇게 됨 ${held} · 피함 ${avoided} · 부분 ${partial}`,
|
|
279
|
+
settled_group: (n, held, avoided, partial, missed) => `결과 기록됨 (${n}): 그렇게 됨 ${held} · 피함 ${avoided} · 부분 ${partial} · 빗나감 ${missed}`,
|
|
280
|
+
outcome_label: (o) => ({ held: '그렇게 됨', avoided: '피함', partial: '부분', missed: '빗나감', still_pending: '대기' })[o] ?? o,
|
|
277
281
|
more: (n) => `… (+${n})`,
|
|
278
|
-
record_since: (date) =>
|
|
282
|
+
record_since: (date) => `${date}부터 기록`,
|
|
279
283
|
},
|
|
280
284
|
receipt: {
|
|
281
285
|
header: 'ARGUS · 판단 영수증',
|
|
@@ -287,8 +291,8 @@ export const SURFACES = {
|
|
|
287
291
|
human_only: '사람만의 판단',
|
|
288
292
|
made_by_label: '…내린 사람',
|
|
289
293
|
made_by: '나. (모델이 아니라)',
|
|
290
|
-
called_as: '
|
|
291
|
-
basis_label: (v) => ({ judgment: '
|
|
294
|
+
called_as: '…돌아보니',
|
|
295
|
+
basis_label: (v) => ({ judgment: '판단이 컸다', luck: '운이 컸다', mixed: '판단 반 운 반', unsure: '잘 모르겠다' })[v] ?? v,
|
|
292
296
|
// 빈 칸을 사실 그대로. "이름 붙이지 않고 넘어갔습니다"는 사용자의 완성도를
|
|
293
297
|
// 지적하는 잔소리로 읽혔다 (experience loop, settler).
|
|
294
298
|
skipped: '— (없음)',
|
|
@@ -305,23 +309,23 @@ export const SURFACES = {
|
|
|
305
309
|
tools: {
|
|
306
310
|
open_decision: {
|
|
307
311
|
reason: {
|
|
308
|
-
vent: '이건 소리 내어 말할 일이지, 억지로 만들
|
|
312
|
+
vent: '이건 소리 내어 말할 일이지, 억지로 결정으로 만들 일이 아닙니다.',
|
|
309
313
|
factual: '이건 답이 있는 질문이지, 열어둘 결정이 아닙니다.',
|
|
310
314
|
already_closed: '이미 내린 결정입니다. Argus는 이걸 다시 열지 않습니다.',
|
|
311
315
|
flat: '선택지가 거의 대등합니다. 억지로 만들 핵심 질문이 없습니다.',
|
|
312
316
|
reversible_low_stakes: '되돌리기 쉽고 걸린 것도 적습니다. 직접 해보는 것이 곧 검증입니다.',
|
|
313
317
|
low_stakes: '걸린 것이 별로 없습니다. 그대로 두는 편이 무난합니다.',
|
|
314
318
|
},
|
|
315
|
-
reason_fallback: '여기서 지어낼
|
|
319
|
+
reason_fallback: '여기서 억지로 지어낼 결정은 없습니다.',
|
|
316
320
|
leave_coda: '그대로 두는 것도 여전히 진짜 선택지입니다.',
|
|
317
321
|
watch_exit: ' 기록하지 않고 그대로 두어도 괜찮습니다.',
|
|
318
|
-
reconfirm: '신호가 서로 어긋납니다 (걸린 것은 큰데 되돌리기는 쉽습니다). 더 나아가기 전에
|
|
319
|
-
opened_with_crux: (crux) => `열었습니다.
|
|
320
|
-
opened_bare: '열었습니다.
|
|
322
|
+
reconfirm: '신호가 서로 어긋납니다 (걸린 것은 큰데 되돌리기는 쉽습니다). 더 나아가기 전에 이 둘을 다시 짚어 보세요.',
|
|
323
|
+
opened_with_crux: (crux) => `열었습니다. 이 결정을 좌우하는 단 하나의 질문: ${crux}`,
|
|
324
|
+
opened_bare: '열었습니다. 이 결정을 좌우하는 핵심 질문 하나가 있다면 그걸 짚어 보는 게 다음 단계입니다. 그다음 반증 가능한 예측을 저장하면 됩니다.',
|
|
321
325
|
},
|
|
322
326
|
seal: {
|
|
323
327
|
sealed: (predicate, checkBy) => `예측을 저장했습니다. "${predicate}" 확인일은 ${checkBy}입니다. 그날 argus_resolve로 실제로 어땠는지 기록하세요.`,
|
|
324
|
-
nudge_assumption: ' 원하면 핵심 전제를
|
|
328
|
+
nudge_assumption: ' 원하면 핵심 전제를 적어두면 나중에 같이 확인합니다.',
|
|
325
329
|
synced: ' 계정에 동기화했습니다. 확인일이 오면 이메일로 알려드립니다.',
|
|
326
330
|
sync_failed: (reason) => ` (계정 동기화가 안 됐습니다. ${reason}. 예측은 로컬에 안전합니다. 동기화되기 전까지는 이메일 알림이 오지 않습니다. 나중에 argus_settings action=sync를 시도하세요.)`,
|
|
327
331
|
},
|
|
@@ -6,7 +6,7 @@ const KO_TOOL_PRESENTATION = {
|
|
|
6
6
|
argus_predict: { titleKo: '예측 저장', descriptionKo: '현실이 확인할 수 있는 예측과 확인일을 저장하고 작성 주체를 정직하게 기록합니다.' },
|
|
7
7
|
argus_resolve: { titleKo: '실제 결과 기록', descriptionKo: '확인일이 된 예측에 실제로 일어난 일을 기록합니다. Argus는 결과를 평가하지 않습니다.' },
|
|
8
8
|
argus_patterns: { titleKo: '판단 기록 보기', descriptionKo: '진행 중인 결정, 전체 계약, 판단 영수증, 전제, 누적 기록을 읽고, 당신이 쓴 예측·전제와 그 결과를 되읽는 reflection을 봅니다.' },
|
|
9
|
-
argus_open_decision: { titleKo: '결정 열기', descriptionKo: '중요하고 되돌리기 어려운 진짜
|
|
9
|
+
argus_open_decision: { titleKo: '결정 열기', descriptionKo: '중요하고 되돌리기 어려운 진짜 결정인지 확인하고, 맞다면 중립적인 핵심 질문 하나로 결정을 엽니다.' },
|
|
10
10
|
argus_review: { titleKo: '문서 판단 검수', descriptionKo: '기존 문서의 주장·근거·숨은 전제·사람이 판단할 지점을 원문 위치에 연결해 검수합니다.' },
|
|
11
11
|
argus_premises: { titleKo: '결정 전제 추적', descriptionKo: '결정이 기대는 사실과 미결 질문을 출처와 함께 추가·수정·정리합니다.' },
|
|
12
12
|
argus_seal: { titleKo: '예측 봉인', descriptionKo: '현실이 확인할 수 있는 예측과 확인일을 봉인합니다. 예측의 작성 주체를 정직하게 기록합니다.' },
|
package/dist/lib/untrusted.js
CHANGED
|
@@ -36,14 +36,14 @@
|
|
|
36
36
|
* escape sequences.
|
|
37
37
|
*/
|
|
38
38
|
// eslint-disable-next-line no-control-regex
|
|
39
|
-
const CONTROL_CHARS = /[\u0000-\u0008\u000B-\u001F\u007F]/g;
|
|
39
|
+
const CONTROL_CHARS = /[\u0000-\u0008\u000B-\u001F\u007F-\u009F]/g;
|
|
40
40
|
/**
|
|
41
41
|
* Invisible and direction-controlling characters: zero-width space/joiners,
|
|
42
42
|
* LTR/RTL marks, bidi embeddings and isolates. These render as nothing (or
|
|
43
43
|
* reverse the surrounding text) for the human while the model reads them intact
|
|
44
44
|
* - the classic "the human and the model see different strings" gap.
|
|
45
45
|
*/
|
|
46
|
-
const INVISIBLE_CHARS = /[\u200B-\u200F\u202A-\u202E\u2066-\u2069\uFEFF]/g;
|
|
46
|
+
const INVISIBLE_CHARS = /[\u200B-\u200F\u202A-\u202E\u2028\u2029\u2066-\u2069\uFEFF]/g;
|
|
47
47
|
/** Strip the mechanical-injection vectors from one string. Newlines and tabs
|
|
48
48
|
* survive (a rendered receipt is multi-line ASCII art). */
|
|
49
49
|
export function stripUnsafeChars(s) {
|
|
@@ -52,7 +52,7 @@ export function stripUnsafeChars(s) {
|
|
|
52
52
|
/** True when the string carries anything stripUnsafeChars would remove. The
|
|
53
53
|
* surface lint uses this so a raw path fails loudly in CI. */
|
|
54
54
|
export function hasUnsafeChars(s) {
|
|
55
|
-
return /[\u0000-\u0008\u000B-\u001F\u007F\u200B-\u200F\u202A-\u202E\u2066-\u2069\uFEFF]/.test(s);
|
|
55
|
+
return /[\u0000-\u0008\u000B-\u001F\u007F-\u009F\u200B-\u200F\u202A-\u202E\u2028\u2029\u2066-\u2069\uFEFF]/.test(s);
|
|
56
56
|
}
|
|
57
57
|
/**
|
|
58
58
|
* Quote recorded text INLINE into a one-line surface: strip the unsafe
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { asDate } from './resolve-today.js';
|
|
1
|
+
import { asDate, isRealDate } from './resolve-today.js';
|
|
2
2
|
// Obvious non-falsifiable vibes. Weak/advisory only.
|
|
3
3
|
const VIBE = /\b(go well|be fine|be good|be great|work out|feel right|be successful|do better|improve somehow)\b/i;
|
|
4
4
|
// Korean vibe-predicates (12 P1-4): "잘 될 것 같다 아마도" sealed — and was
|
|
@@ -14,10 +14,13 @@ export function validateSeal(predicate, checkBy, today) {
|
|
|
14
14
|
};
|
|
15
15
|
}
|
|
16
16
|
const date = asDate(checkBy);
|
|
17
|
-
if (!date) {
|
|
17
|
+
if (!date || !isRealDate(date)) {
|
|
18
|
+
// A calendar-invalid but digit-shaped date (2026-13-01, 2026-09-31) must be
|
|
19
|
+
// refused here: it would otherwise seal a malformed .ics and a wrong due
|
|
20
|
+
// date, and only be caught by luck if it happened to sort before today.
|
|
18
21
|
return {
|
|
19
22
|
code: 'BAD_CHECK_BY',
|
|
20
|
-
message: 'check_by must be a real date in YYYY-MM-DD form.',
|
|
23
|
+
message: 'check_by must be a real calendar date in YYYY-MM-DD form.',
|
|
21
24
|
recovery: 'Pick the date when reality will answer, e.g. "2026-09-01".',
|
|
22
25
|
};
|
|
23
26
|
}
|
package/dist/resources.js
CHANGED
|
@@ -5,6 +5,7 @@ import { readReceipt } from './lib/receipt.js';
|
|
|
5
5
|
import { safeSegment } from './lib/safe-path.js';
|
|
6
6
|
import { logDebug } from './lib/log.js';
|
|
7
7
|
import { duePremises, groupDuePremises, isMonitored, isDueForRecheck } from './lib/premises.js';
|
|
8
|
+
import { sanitizeOutput } from './lib/untrusted.js';
|
|
8
9
|
/**
|
|
9
10
|
* MCP Resources (blueprint §4.3). Read-only context the host can auto-inject —
|
|
10
11
|
* the ledger, the due contracts, a receipt, the current bearing. They are
|
|
@@ -56,7 +57,11 @@ export function readResource(uri) {
|
|
|
56
57
|
return { contents: [unbound(uri)] };
|
|
57
58
|
}
|
|
58
59
|
const today = resolveToday({});
|
|
59
|
-
|
|
60
|
+
// Resources are model-facing (auto-injected context) and bypass the envelope()
|
|
61
|
+
// sanitizer. JSON.stringify escapes C0/ANSI but NOT bidi (U+202E) / zero-width
|
|
62
|
+
// (U+200B) — the "human and model see different strings" gap. Run the same
|
|
63
|
+
// chokepoint sanitizer over the payload first.
|
|
64
|
+
const payload = sanitizeOutput(computePayload(uri, dir, today));
|
|
60
65
|
return { contents: [{ uri, mimeType: JSON_MIME, text: JSON.stringify(payload, null, 2) }] };
|
|
61
66
|
}
|
|
62
67
|
function computePayload(uri, dir, today) {
|
package/dist/server.js
CHANGED
|
@@ -135,18 +135,23 @@ export async function createServer() {
|
|
|
135
135
|
? { ...parsed.data, today_override: rawArgs['today_override'] }
|
|
136
136
|
: parsed.data;
|
|
137
137
|
const raw = await serialize(() => tool.handler(callArgs));
|
|
138
|
+
let result = localizeToolResult(callArgs, raw);
|
|
138
139
|
// Learn the session's language from the user's OWN words (never env), so
|
|
139
140
|
// every later surface — including contentless ones (errors, recall) — stays
|
|
140
|
-
// in that language start to finish.
|
|
141
|
-
//
|
|
142
|
-
//
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
141
|
+
// in that language start to finish. This is BEST-EFFORT: a bad argus_dir
|
|
142
|
+
// (relative / unexpanded ${VAR} — the #1 setup mistake) already surfaced a
|
|
143
|
+
// proper, localized error from the handler; re-resolving it here to learn
|
|
144
|
+
// the locale must NOT re-throw and clobber that with a raw INTERNAL_ERROR.
|
|
145
|
+
try {
|
|
146
|
+
const dirForLocale = resolveToolArgusDir(callArgs['argus_dir']);
|
|
147
|
+
learnLocaleFromContent(dirForLocale, callArgs);
|
|
148
|
+
// §9.7 O1: if an EXPLICIT pin contradicts the language the user is
|
|
149
|
+
// actually speaking, say so once (fact + argus_settings handle) — a pin
|
|
150
|
+
// is never silently overridden, but it must not be silently obeyed
|
|
151
|
+
// against the user's own words forever either.
|
|
152
|
+
result = appendLocaleMismatchNote(dirForLocale, callArgs, result);
|
|
153
|
+
}
|
|
154
|
+
catch { /* invalid argus_dir — the handler already surfaced it; skip locale learning */ }
|
|
150
155
|
// Opt-in usage signal: which tool ran + that it didn't crash. Carries no
|
|
151
156
|
// arguments — never the decision content. Fire-and-forget (see telemetry.ts).
|
|
152
157
|
recordToolCall(name, true);
|
|
@@ -8,6 +8,7 @@ import { validateSeal } from '../lib/validate-seal.js';
|
|
|
8
8
|
import { appendLedger, withLedgerLock } from '../lib/ledger-append.js';
|
|
9
9
|
import { pushToAccount } from '../lib/push-account.js';
|
|
10
10
|
import { accountPushId } from '../lib/install-id.js';
|
|
11
|
+
import { writeReturnCalendarEvent } from '../lib/calendar.js';
|
|
11
12
|
import { resolveResponseLocale, SURFACES, humanizeSyncReason } from '../lib/surfaces.js';
|
|
12
13
|
import { SCHEMA_VERSION } from '../lib/spine.js';
|
|
13
14
|
import { z } from 'zod';
|
|
@@ -35,6 +36,19 @@ export const amend = {
|
|
|
35
36
|
const today = resolveToday({ override: a['today_override'] });
|
|
36
37
|
const current = resolveContract(dir, id, today);
|
|
37
38
|
guardTransition(current.state, 'amend'); // GOALPOST_MOVED / DECISION_CLOSED / ILLEGAL_TRANSITION
|
|
39
|
+
// Goalpost guard, extended past the `due` state: a still_pending defer
|
|
40
|
+
// re-arms the contract to `sealed`, which the state machine then treats
|
|
41
|
+
// like a never-due decision — so due→defer→sealed→amend let the PREDICATE
|
|
42
|
+
// be rewritten AFTER the original check-by had already arrived (reality had
|
|
43
|
+
// begun answering). Re-scheduling the date via defer stays legitimate;
|
|
44
|
+
// rewriting the claim does not. Refuse only the predicate change post-defer.
|
|
45
|
+
if (a['predicate'] != null && (current.entry?.defer_count ?? 0) > 0) {
|
|
46
|
+
return toolError({
|
|
47
|
+
ok: false, tool: 'argus_amend', error_code: 'GOALPOST_MOVED',
|
|
48
|
+
message: 'Cannot rewrite the prediction after the decision was deferred — its original check-by has passed.',
|
|
49
|
+
recovery: 'Re-schedule the date if the timeline moved, or settle it against reality. The claim itself is locked once its check-by first arrived.',
|
|
50
|
+
});
|
|
51
|
+
}
|
|
38
52
|
const predicate = a['predicate'] ?? current.predicate;
|
|
39
53
|
const checkBy = a['check_by'] ?? current.check_by;
|
|
40
54
|
if (a['check_by'] != null || a['predicate'] != null) {
|
|
@@ -54,6 +68,13 @@ export const amend = {
|
|
|
54
68
|
const v2Write = asV2WriteField(mirrorAmend);
|
|
55
69
|
// Response voice follows the (new or existing) predicate (M4).
|
|
56
70
|
const locale = resolveResponseLocale(dir, predicate);
|
|
71
|
+
// Regenerate the .ics so the return reminder rings on the AMENDED date /
|
|
72
|
+
// text. amend used to update the ledger + bearing + account but leave the
|
|
73
|
+
// on-disk calendar file — the only account-free return channel — pointing
|
|
74
|
+
// at the OLD check-by, so the reminder fired on the stale date.
|
|
75
|
+
if ((a['check_by'] != null || a['predicate'] != null) && predicate && checkBy) {
|
|
76
|
+
await writeReturnCalendarEvent(dir, { id, predicate, check_by: checkBy, created_at: now, locale });
|
|
77
|
+
}
|
|
57
78
|
const T = SURFACES[locale].tools.amend;
|
|
58
79
|
// Tell the ACCOUNT the date moved. Without this, argus_amend was silent to
|
|
59
80
|
// the account forever: the Companion Brief kept emailing on the ORIGINAL
|
package/dist/tools/check-in.js
CHANGED
|
@@ -37,7 +37,11 @@ export const checkIn = {
|
|
|
37
37
|
description: 'Return decision contracts whose check-by date has arrived (and optionally upcoming ones). A return nudge — reads and routes to argus_settle. If nothing is due, it says so and stops; it does not manufacture engagement.',
|
|
38
38
|
inputSchema,
|
|
39
39
|
outputSchema: ENVELOPE_OUTPUT_SCHEMA,
|
|
40
|
-
|
|
40
|
+
// readOnlyHint:false — honest: the FIRST call on an un-initialized dir routes
|
|
41
|
+
// through ensureInitialized, which creates .argus/ (dirs, config, .gitignore,
|
|
42
|
+
// bound marker, v2 registry). "read-only" must not lie (hardening principle),
|
|
43
|
+
// even though every subsequent call is a pure read.
|
|
44
|
+
annotations: { title: 'Check what is due', readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
41
45
|
handler: async (a) => {
|
|
42
46
|
try {
|
|
43
47
|
const dir = resolveToolArgusDir(a['argus_dir']);
|
|
@@ -235,6 +239,25 @@ export const checkIn = {
|
|
|
235
239
|
const accountHint = (process.env.ARGUS_TOKEN || '').trim()
|
|
236
240
|
? S.account_hint
|
|
237
241
|
: '';
|
|
242
|
+
// First-run vs caught-up: SERVER_INSTRUCTIONS routes EVERY session start
|
|
243
|
+
// to check_in, so a brand-new user used to land on the same "nothing due"
|
|
244
|
+
// + stop a veteran sees — a dead end at the flagship cold-start. Fire the
|
|
245
|
+
// on-ramp ONLY when the surface would otherwise be a BARE "nothing due":
|
|
246
|
+
// no decisions, no watch mirror, no account, no upcoming/fleet/integrity.
|
|
247
|
+
// Anything else (a caught-up veteran, a watch anchor, an account seal)
|
|
248
|
+
// keeps nothing_due, so the mirror/silence contracts are untouched.
|
|
249
|
+
// `!ledger.oldest_ts` = the ledger has NO events at all (not just no
|
|
250
|
+
// decisions — no watch anchors either), the one true "brand new" signal.
|
|
251
|
+
// Excluded under ARGUS_V2_DEBUG (the v2 observation channel expects its
|
|
252
|
+
// diagnostic payload even on an empty v1 ledger).
|
|
253
|
+
if (!ledger.oldest_ts && !mirrorLine && !accountHint && !upcomingLine && !fleetLine && !integrityLine && process.env['ARGUS_V2_DEBUG'] !== '1') {
|
|
254
|
+
return envelope({
|
|
255
|
+
ok: true, tool: 'argus_check_in',
|
|
256
|
+
surface: S.first_run,
|
|
257
|
+
next_actions: ['argus_capture'],
|
|
258
|
+
data: { due: [], due_count: 0, due_premises: [], due_premise_count: 0, due_open_questions: [], due_open_question_count: 0, first_run: true, today },
|
|
259
|
+
});
|
|
260
|
+
}
|
|
238
261
|
return envelope({
|
|
239
262
|
ok: true, tool: 'argus_check_in',
|
|
240
263
|
surface: mirrorLine + S.nothing_due + accountHint + upcomingLine + fleetLine + integrityLine,
|
package/dist/tools/errors.js
CHANGED
|
@@ -12,15 +12,15 @@ import { localizedErrorCopy } from '../lib/localized-message.js';
|
|
|
12
12
|
export function handleToolException(tool, e) {
|
|
13
13
|
if (e instanceof ArgusDirError) {
|
|
14
14
|
const copy = localizedErrorCopy(null, undefined, {
|
|
15
|
-
en: { message: e.message, recovery: '
|
|
16
|
-
ko: { message: 'Argus 기록
|
|
15
|
+
en: { message: e.message, recovery: 'Set ARGUS_DIR (or the per-call argus_dir) to an absolute path with no "..", e.g. C:\\Users\\you\\.argus or /Users/you/.argus — or remove ARGUS_DIR to use the default ~/.argus. A ${VAR} may be passed through unexpanded by your host.' },
|
|
16
|
+
ko: { message: 'Argus 기록 경로(argus_dir / ARGUS_DIR)가 올바르지 않습니다.', recovery: '절대 경로여야 하고 ".."을 포함할 수 없습니다. MCP 설정에서 절대 경로(예: C:\\Users\\이름\\.argus, /Users/이름/.argus)로 바꾸거나 ARGUS_DIR을 지워 기본값(~/.argus)을 쓰세요. ${...} 같은 변수는 호스트가 확장하지 못할 수 있습니다.' },
|
|
17
17
|
});
|
|
18
18
|
return toolError({ ok: false, tool, error_code: e.code, ...copy });
|
|
19
19
|
}
|
|
20
20
|
if (e instanceof PathSafetyError) {
|
|
21
21
|
const copy = localizedErrorCopy(null, undefined, {
|
|
22
|
-
en: { message: e.message, recovery: 'Use
|
|
23
|
-
ko: { message: '
|
|
22
|
+
en: { message: e.message, recovery: 'Use A-Za-z0-9._- only, with no trailing dot or space, and avoid reserved device names (con, nul, com1…). Example: "career-move".' },
|
|
23
|
+
ko: { message: '이 id 또는 label은 쓸 수 없습니다.', recovery: '영문·숫자·. _ - 만 쓰되, 끝에 마침표나 공백을 두지 말고 con·nul·com1 같은 예약어는 피하세요. 예: "career-move".' },
|
|
24
24
|
});
|
|
25
25
|
return toolError({ ok: false, tool, error_code: e.code, ...copy });
|
|
26
26
|
}
|
|
@@ -131,6 +131,20 @@ export const config = {
|
|
|
131
131
|
const writeKeys = ['locale', 'boss', 'team', 'archive', 'ambient_mute', 'premise_sync'].filter((k) => k in a);
|
|
132
132
|
const existing = readConfig(dir) ?? { schema_version: SCHEMA_VERSION, locale: detectLocale(dir), boss: null, team: null, archive: null };
|
|
133
133
|
if (writeKeys.length === 0) {
|
|
134
|
+
// action=update with no writable field is a MISTAKE (a read is
|
|
135
|
+
// action=status). Saying "Config read." reports the OPPOSITE operation
|
|
136
|
+
// as a success and hides that nothing changed — name the supported set.
|
|
137
|
+
if (a['action'] === 'update') {
|
|
138
|
+
return envelope({
|
|
139
|
+
ok: true, tool: 'argus_config',
|
|
140
|
+
surface: localizedMessage(dir, undefined, {
|
|
141
|
+
en: 'No setting was changed. Supported fields: locale (ko/en), ambient_mute (true/false), premise_sync (true/false).',
|
|
142
|
+
ko: '변경된 설정이 없습니다. 바꿀 수 있는 항목: locale(ko/en), ambient_mute(true/false), premise_sync(true/false).',
|
|
143
|
+
}),
|
|
144
|
+
next_actions: ['stop'],
|
|
145
|
+
data: { config: existing, existed: !!readConfig(dir), changed: [] },
|
|
146
|
+
});
|
|
147
|
+
}
|
|
134
148
|
return envelope({
|
|
135
149
|
ok: true,
|
|
136
150
|
tool: 'argus_config',
|
package/dist/tools/premises.js
CHANGED
|
@@ -4,7 +4,7 @@ import { replayLedger } from '../lib/ledger-replay.js';
|
|
|
4
4
|
import { resolveToday } from '../lib/resolve-today.js';
|
|
5
5
|
import { resolveContract } from '../lib/resolve-contract.js';
|
|
6
6
|
import { guardTransition } from '../lib/state-machine.js';
|
|
7
|
-
import { appendLedger } from '../lib/ledger-append.js';
|
|
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
9
|
import { elicit } from '../lib/elicit.js';
|
|
10
10
|
import { resolveResponseLocale } from '../lib/surfaces.js';
|
|
@@ -251,8 +251,21 @@ async function opAdd(dir, id, today, now, state, existing, a) {
|
|
|
251
251
|
// premise added under a today_override in real use.
|
|
252
252
|
anchor_date: today,
|
|
253
253
|
}));
|
|
254
|
-
if (events.length > 0)
|
|
255
|
-
|
|
254
|
+
if (events.length > 0) {
|
|
255
|
+
// §9.4 두 기기 안전: the ordinal base was read OUTSIDE any lock, so two
|
|
256
|
+
// concurrent sessions adding premises to the same decision both saw max=4
|
|
257
|
+
// and both assigned ordinal 5 — a DUPLICATE P5 whose second premise is
|
|
258
|
+
// unreferenceable (resolvePremiseRef returns only the first). Re-derive the
|
|
259
|
+
// ordinals under the ledger lock from a fresh fold, then append — the same
|
|
260
|
+
// read-check-append-under-lock discipline seal/settle use.
|
|
261
|
+
await withLedgerLock(dir, async () => {
|
|
262
|
+
const freshPremises = replayLedger(dir, today).contracts.get(id)?.premises ?? [];
|
|
263
|
+
let ord = freshPremises.reduce((m, p) => Math.max(m, p.ordinal), 0) + 1;
|
|
264
|
+
for (const e of events)
|
|
265
|
+
e.ordinal = ord++;
|
|
266
|
+
await appendLedger(dir, events, now);
|
|
267
|
+
});
|
|
268
|
+
}
|
|
256
269
|
// Full echo — the silent-premise defense: the host always has the material to
|
|
257
270
|
// show the user exactly what was recorded (plan v5 §2).
|
|
258
271
|
const echo = events.map((e) => ({
|
|
@@ -374,7 +387,12 @@ async function opResolve(dir, id, now, state, existing, a) {
|
|
|
374
387
|
if (!decision) {
|
|
375
388
|
// MCP-native elicitation: ask the USER directly. The question replays their
|
|
376
389
|
// own open question verbatim and takes free text — no options, no leans.
|
|
377
|
-
|
|
390
|
+
// Localize like the rest of the tool — a Korean user closing their own
|
|
391
|
+
// Korean question used to get an English form. Voice follows the question.
|
|
392
|
+
const qLocale = resolveResponseLocale(dir, premise.text);
|
|
393
|
+
const got = await elicit(qLocale === 'ko'
|
|
394
|
+
? `이 결정에 남겨둔 질문입니다: "${premise.text}". 지금은 어떻게 판단하시나요? 당신의 말로 적어주세요. (그대로 열어둬도 됩니다.)`
|
|
395
|
+
: `Your open question on this decision: "${premise.text}". What is your call now, in your own words? (You can also leave it open.)`, { type: 'object', properties: { decision: { type: 'string', maxLength: 400, description: qLocale === 'ko' ? '당신의 판단, 당신의 표현.' : 'Your call, your words.' } }, required: ['decision'] });
|
|
378
396
|
decision = typeof got?.['decision'] === 'string' ? got['decision'].trim() : '';
|
|
379
397
|
}
|
|
380
398
|
if (!decision) {
|
|
@@ -16,6 +16,7 @@ import { checkIn } from './check-in.js';
|
|
|
16
16
|
import { settle } from './settle.js';
|
|
17
17
|
import { detectLocaleFromText } from '../lib/locale.js';
|
|
18
18
|
import { gitCommonDirOf } from '../v2/git-discovery.js';
|
|
19
|
+
import { handleToolException } from './errors.js';
|
|
19
20
|
const premiseInput = z.strictObject({
|
|
20
21
|
text: z.string().min(3).max(400).describe('결정이 기대는 사실 또는 아직 답하지 못한 질문입니다. 사용자의 표현을 그대로 씁니다.').optional(),
|
|
21
22
|
kind: z.enum(['premise', 'open_question']).default('premise').describe('premise는 확인할 전제, open_question은 사용자가 아직 답하지 않은 질문입니다.'),
|
|
@@ -69,7 +70,7 @@ const decideSchema = z.discriminatedUnion('action', [
|
|
|
69
70
|
id: zId.describe('대상 결정 id입니다.'),
|
|
70
71
|
ref: z.string().max(64).describe('재확인할 전제 번호 또는 id입니다.'),
|
|
71
72
|
finding: z.string().min(1).max(800).describe('현재 확인한 사실을 비교 가능한 한 문장으로 적습니다.'),
|
|
72
|
-
numeric_value: z.number().describe('수치 사실의 현재 값을 명시적으로 전달합니다.').optional(),
|
|
73
|
+
numeric_value: z.number().finite().describe('수치 사실의 현재 값을 명시적으로 전달합니다.').optional(),
|
|
73
74
|
changed: z.boolean().describe('문장형 사실이 기준값에서 실질적으로 달라졌는지 표시합니다.').optional(),
|
|
74
75
|
// default user_stated: the runtime union validates BEFORE the handler-level
|
|
75
76
|
// default can apply, so a required source here made every real update_fact
|
|
@@ -112,7 +113,7 @@ const decidePublicSchema = z.strictObject({
|
|
|
112
113
|
ref: z.string().max(64).describe('답하거나 재확인할 전제 또는 미결 질문 번호입니다.').optional(),
|
|
113
114
|
reconsider_cadence_days: z.number().int().min(1).max(365).describe('미결 질문을 다시 볼 간격(일)입니다.').optional(),
|
|
114
115
|
finding: z.string().min(1).max(800).describe('현재 확인한 사실을 비교 가능한 한 문장으로 적습니다.').optional(),
|
|
115
|
-
numeric_value: z.number().describe('수치 사실의 현재 값을 명시적으로 전달합니다.').optional(),
|
|
116
|
+
numeric_value: z.number().finite().describe('수치 사실의 현재 값을 명시적으로 전달합니다.').optional(),
|
|
116
117
|
changed: z.boolean().describe('문장형 사실이 기준값에서 실질적으로 달라졌는지 표시합니다.').optional(),
|
|
117
118
|
source: z.enum(['url', 'user_stated', 'host_reported']).describe('현재 사실을 확인한 출처입니다.').optional(),
|
|
118
119
|
source_detail: z.string().max(1000).describe('출처 URL 또는 짧은 인용 정보입니다.').optional(),
|
|
@@ -266,10 +267,20 @@ async function ensureInitialized(args) {
|
|
|
266
267
|
return null;
|
|
267
268
|
}
|
|
268
269
|
async function runPublic(publicName, args, handler) {
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
270
|
+
try {
|
|
271
|
+
const initError = await ensureInitialized(args);
|
|
272
|
+
if (initError)
|
|
273
|
+
return rewriteResult(initError, publicName);
|
|
274
|
+
return rewriteResult(await handler(args), publicName);
|
|
275
|
+
}
|
|
276
|
+
catch (e) {
|
|
277
|
+
// ensureInitialized() → resolveToolArgusDir() THROWS ArgusDirError on a
|
|
278
|
+
// relative or unexpanded-${VAR} argus_dir — the #1 setup mistake. Without
|
|
279
|
+
// this catch the throw escaped to the server's String(e) fallback, so the
|
|
280
|
+
// user saw a raw "INTERNAL_ERROR: ArgusDirError: …" with no recovery. Route
|
|
281
|
+
// it through the typed handler to get the localized message + fix.
|
|
282
|
+
return rewriteResult(handleToolException(publicName, e), publicName);
|
|
283
|
+
}
|
|
273
284
|
}
|
|
274
285
|
export const decide = {
|
|
275
286
|
name: 'argus_capture',
|
|
@@ -335,7 +346,9 @@ export const history = {
|
|
|
335
346
|
description: 'Read decisions already on record: what is open, all contracts, one Judgment Receipt, one decision’s premises, the accumulated timeline, or a reflection that replays your own past predictions and premises next to what reality did. Read-only.',
|
|
336
347
|
inputSchema: historySchema,
|
|
337
348
|
outputSchema: ENVELOPE_OUTPUT_SCHEMA,
|
|
338
|
-
|
|
349
|
+
// readOnlyHint:false — like argus_check_in, the first call auto-initializes
|
|
350
|
+
// .argus/ via ensureInitialized, so it can write on first use (honest hint).
|
|
351
|
+
annotations: { title: 'View judgment history', readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
339
352
|
handler: (a) => {
|
|
340
353
|
const viewMap = {
|
|
341
354
|
active: 'bearing',
|
package/dist/tools/recall.js
CHANGED
|
@@ -18,9 +18,9 @@ const byCheckBy = (a, b) => (a.check_by || '9999-99-99') < (b.check_by || '9999-
|
|
|
18
18
|
function frequencyStatement(s, locale) {
|
|
19
19
|
const n = s.total_settled;
|
|
20
20
|
if (n === 0)
|
|
21
|
-
return locale === 'ko' ? '아직
|
|
21
|
+
return locale === 'ko' ? '아직 결과를 기록한 결정이 없습니다 — 요약할 것이 없습니다.' : 'No settled decisions yet — nothing to summarize.';
|
|
22
22
|
return locale === 'ko'
|
|
23
|
-
?
|
|
23
|
+
? `결과 기록 ${n}건 중: 그렇게 됨 ${s.held} · 피함 ${s.avoided} · 부분 ${s.partial} · 빗나감 ${s.missed}.`
|
|
24
24
|
: `Of ${n} settled: ${s.held} held, ${s.avoided} avoided, ${s.partial} partial, ${s.missed} missed.`;
|
|
25
25
|
}
|
|
26
26
|
/** Voice for a read view: the view's own text, else any ledger user-text, else
|
|
@@ -97,8 +97,11 @@ export const recall = {
|
|
|
97
97
|
// don't render it. The full formatted card stays in data.receipt_text as
|
|
98
98
|
// the keepsake. (User-value review, 2026-07-14.)
|
|
99
99
|
const clip = (s, n) => { const t = String(s ?? '').trim(); return t.length > n ? `${t.slice(0, n)}…` : t; };
|
|
100
|
+
// One word per outcome across the whole flow: surfaces.ts settle uses
|
|
101
|
+
// 부분/대기, so the receipt-recall surface must match (was 부분적/아직 —
|
|
102
|
+
// three words for `partial` in one decision's lifecycle).
|
|
100
103
|
const label = receiptLocale === 'ko'
|
|
101
|
-
? { held: '그렇게 됨', avoided: '피함', partial: '
|
|
104
|
+
? { held: '그렇게 됨', avoided: '피함', partial: '부분', missed: '빗나감', still_pending: '대기' }
|
|
102
105
|
: { held: 'held', avoided: 'avoided', partial: 'partial', missed: 'missed', still_pending: 'still pending' };
|
|
103
106
|
const outcomeKey = (r.outcome ?? 'still_pending');
|
|
104
107
|
const receiptSurface = receiptLocale === 'ko'
|
|
@@ -203,8 +206,13 @@ export const recall = {
|
|
|
203
206
|
const shown = all.slice(0, 60);
|
|
204
207
|
const locale = readVoice(dir, ledger, all[0]?.predicate);
|
|
205
208
|
const wake = wakeText(ledger, today, dir);
|
|
209
|
+
// Empty ledger → an on-ramp with a capture handle, not "0 decision(s)" +
|
|
210
|
+
// stop (a "show me my decisions" newcomer must get a next move).
|
|
211
|
+
const allSurface = all.length === 0
|
|
212
|
+
? (locale === 'ko' ? '아직 기록에 남은 결정이 없습니다. 고민 중인 결정을 말하면 argus_capture로 시작합니다.' : 'No decisions on record yet. Describe a decision you are weighing and argus_capture begins it.')
|
|
213
|
+
: (locale === 'ko' ? `기록에 남은 결정 ${all.length}건.` : `${all.length} decision(s) on record.`);
|
|
206
214
|
return envelope({
|
|
207
|
-
ok: true, tool: 'argus_recall', surface:
|
|
215
|
+
ok: true, tool: 'argus_recall', surface: allSurface, next_actions: all.length === 0 ? ['argus_capture'] : ['stop'],
|
|
208
216
|
data: { contracts: shown, ...(all.length > shown.length ? { truncated: all.length - shown.length } : {}), today, ...(wake ? { wake_text: wake } : {}) },
|
|
209
217
|
});
|
|
210
218
|
}
|
|
@@ -244,7 +252,7 @@ export const recall = {
|
|
|
244
252
|
return envelope({
|
|
245
253
|
ok: true, tool: 'argus_recall',
|
|
246
254
|
surface: rn > 0 ? `${framing} ${rfreq}` : framing,
|
|
247
|
-
next_actions: ['stop'],
|
|
255
|
+
next_actions: ledger.ids.size === 0 ? ['argus_capture'] : ['stop'], // empty ledger → a handle, not a dead end
|
|
248
256
|
data: {
|
|
249
257
|
judgment_tier: null, judgment_score: null, // spine rule 2 — never a verdict about who you are
|
|
250
258
|
reflections, reflection_count: reflections.length,
|
|
@@ -278,7 +286,7 @@ export const recall = {
|
|
|
278
286
|
return envelope({
|
|
279
287
|
ok: true, tool: 'argus_recall',
|
|
280
288
|
surface: premiseAttribution ? `${freq} ${premiseAttribution}` : freq,
|
|
281
|
-
next_actions: ['stop'],
|
|
289
|
+
next_actions: ledger.ids.size === 0 ? ['argus_capture'] : ['stop'], // empty ledger → a handle, not a dead end
|
|
282
290
|
data: {
|
|
283
291
|
judgment_tier: null, judgment_score: null, // drift-guard asserts these stay null
|
|
284
292
|
frequency_statement: freq,
|
package/dist/tools/recheck.js
CHANGED
|
@@ -10,6 +10,33 @@ import { resolveResponseLocale, SURFACES } from '../lib/surfaces.js';
|
|
|
10
10
|
import { envelope, toolError } from '../lib/envelope.js';
|
|
11
11
|
import { ENVELOPE_OUTPUT_SCHEMA, zArgusDir, zId, zDate } from './tool-types.js';
|
|
12
12
|
import { handleToolException } from './errors.js';
|
|
13
|
+
/**
|
|
14
|
+
* The materiality engine's `uncertain` reasons are hard-coded Korean and used to
|
|
15
|
+
* splice STRAIGHT into the English recheck surface (`T.uncertain(reason)`) — an
|
|
16
|
+
* English user rechecking an ambiguous numeric premise saw a Korean sentence
|
|
17
|
+
* mid-line. Localize at the surface: map each known reason to an English hint,
|
|
18
|
+
* and fall back to a generic "define a rule" line so no Korean can ever leak.
|
|
19
|
+
* (The raw `reason` stays in `data.reason` for diagnostics.)
|
|
20
|
+
*/
|
|
21
|
+
function localizeUncertainReason(reason, locale) {
|
|
22
|
+
if (locale === 'ko')
|
|
23
|
+
return reason;
|
|
24
|
+
if (reason.includes('비율(%)'))
|
|
25
|
+
return 'this reads as a percentage — say whether the change is in percentage-points or on the complement (100 − value) axis';
|
|
26
|
+
if (reason.includes('near-zero'))
|
|
27
|
+
return 'the values sit near zero — set a rule (a delta or a safety floor) so this can be judged mechanically';
|
|
28
|
+
if (reason.includes('임계 경계'))
|
|
29
|
+
return 'the relative change sits right on the threshold — set a rule or a dead-band';
|
|
30
|
+
if (reason.includes('zero_meaningful') || reason.includes('부호 전환'))
|
|
31
|
+
return 'the sign flipped, but whether zero is meaningful is undeclared — set a rule';
|
|
32
|
+
if (reason.includes('boundary') || reason.includes('경계 도달'))
|
|
33
|
+
return 'the value reached the threshold line, but inclusive/exclusive is unspecified — set the boundary';
|
|
34
|
+
if (reason.includes('기준값 0'))
|
|
35
|
+
return 'the baseline is 0, so a relative rule cannot apply — set a delta rule';
|
|
36
|
+
if (reason.includes('canonical scale') || reason.includes('비수치'))
|
|
37
|
+
return 'this label is non-numeric — set a canonical scale so it can be judged mechanically';
|
|
38
|
+
return 'this change is ambiguous under the current rule — define a materiality rule so it can be judged mechanically';
|
|
39
|
+
}
|
|
13
40
|
/**
|
|
14
41
|
* argus_recheck — re-check one premise against reality (plan v5 §2, §7.1).
|
|
15
42
|
*
|
|
@@ -30,7 +57,7 @@ const inputSchema = z.strictObject({
|
|
|
30
57
|
id: zId.describe('The decision id.'),
|
|
31
58
|
ref: z.string().max(64).describe('Which premise — ordinal ("P1"), premise_id, or unambiguous prefix.'),
|
|
32
59
|
finding: z.string().min(3).max(400).describe('The CURRENT state of the fact, one literal comparable sentence. e.g. "base rate 3.75% after a 25bp hike".'),
|
|
33
|
-
numeric_value: z.number().optional().describe('The fact\'s current number, named EXPLICITLY (e.g. 3.75). Never extracted from prose by regex. When present, drift is decided mechanically (>=10% move or sign flip).'),
|
|
60
|
+
numeric_value: z.number().finite().optional().describe('The fact\'s current number, named EXPLICITLY (e.g. 3.75). Never extracted from prose by regex. When present, drift is decided mechanically (>=10% move or sign flip).'),
|
|
34
61
|
changed: z.boolean().optional().describe('Text premises only: has the FACT materially changed vs the recorded baseline? A research finding about external reality (provenance required) — never a judgment of the user.'),
|
|
35
62
|
source: z.enum(['url', 'user_stated', 'host_reported']).describe('Where the finding comes from. host_reported = the model\'s own research without a citation — recorded honestly as such.'),
|
|
36
63
|
source_detail: z.string().max(300).optional().describe('URL or short citation when source="url".'),
|
|
@@ -63,8 +90,16 @@ export const recheck = {
|
|
|
63
90
|
}
|
|
64
91
|
const finding = String(a['finding']);
|
|
65
92
|
// Response voice follows the finding sentence (M4): config > text > env.
|
|
66
|
-
const
|
|
93
|
+
const rLocale = resolveResponseLocale(dir, String(a['finding']));
|
|
94
|
+
const T = SURFACES[rLocale].tools.recheck;
|
|
67
95
|
const source = String(a['source']);
|
|
96
|
+
// Localize the source enum before it reaches the surface — a recheck line
|
|
97
|
+
// printed the raw "(host_reported)" / "(user_stated)" token to the user.
|
|
98
|
+
const SOURCE_LABEL = {
|
|
99
|
+
ko: { url: '출처 링크', user_stated: '당신이 확인함', host_reported: '어시스턴트 조사' },
|
|
100
|
+
en: { url: 'from a cited source', user_stated: 'as you reported it', host_reported: "from the assistant's own research" },
|
|
101
|
+
};
|
|
102
|
+
const srcLabel = SOURCE_LABEL[rLocale]?.[source] ?? source;
|
|
68
103
|
const sourceDetail = a['source_detail'];
|
|
69
104
|
const numericValue = a['numeric_value'];
|
|
70
105
|
const changed = a['changed'];
|
|
@@ -143,14 +178,14 @@ export const recheck = {
|
|
|
143
178
|
? T.uncertain_heuristic_note
|
|
144
179
|
: '';
|
|
145
180
|
const surface = baselineOnly
|
|
146
|
-
? T.baseline(premise.ordinal, finding,
|
|
181
|
+
? T.baseline(premise.ordinal, finding, srcLabel, recheckCadenceDays(premise))
|
|
147
182
|
: status === 'material'
|
|
148
|
-
? T.material(premise.ordinal, prior.finding, finding,
|
|
183
|
+
? T.material(premise.ordinal, prior.finding, finding, srcLabel)
|
|
149
184
|
: status === 'uncertain'
|
|
150
185
|
// M2 §4/§7: uncertain surfaces the FACT only — no handle, no fork. The
|
|
151
186
|
// user decides whether to define a rule or leave it.
|
|
152
|
-
? `${T.uncertain(premise.ordinal, reason)}${heuristicNote}`
|
|
153
|
-
: T.unchanged(premise.ordinal,
|
|
187
|
+
? `${T.uncertain(premise.ordinal, localizeUncertainReason(reason, rLocale))}${heuristicNote}`
|
|
188
|
+
: T.unchanged(premise.ordinal, srcLabel);
|
|
154
189
|
// ── SPINE (M2 §4, mirror clause): the handle auto-attaches ONLY on
|
|
155
190
|
// `material`. `uncertain` (depends / boundary / rule-uncovered) NEVER
|
|
156
191
|
// auto-attaches argus_recall — that would manufacture a fork on a flat
|