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.
@@ -65,12 +65,15 @@ export const seal = {
65
65
  }
66
66
  let predicate = String(a['predicate']);
67
67
  const checkBy = String(a['check_by']);
68
- // Stamp the seal at the LOGICAL day when a today_override is given (sims,
69
- // tests), else real wall-clock. Without this the receipt showed a
70
- // wall-clock sealed date that diverged from the simulated timeline and
71
- // read as "the record misstates my dates" (experience loop, settler). Real
72
- // use passes no override, so created_at stays the true write time.
73
- const now = a['today_override'] ? `${today}T12:00:00.000Z` : new Date().toISOString();
68
+ // The DATE part of `now` must equal the tz-aware logical `today`. Plain
69
+ // new Date().toISOString() is always UTC, so a Korea (UTC+9) user sealing
70
+ // at 08:00 KST (= 23:00Z the day before) got a receipt dated YESTERDAY —
71
+ // the tool's own `today` disagreeing with the date it printed. recheck.ts
72
+ // already fixed this same class for premise cadences. Keep the real UTC
73
+ // time-of-day for intra-day ordering, but stamp the logical date.
74
+ const now = a['today_override']
75
+ ? `${today}T12:00:00.000Z`
76
+ : `${today}T${new Date().toISOString().slice(11)}`;
74
77
  // Response voice follows the predicate (M4): config > text > env.
75
78
  let locale = resolveResponseLocale(dir, predicate);
76
79
  let T = SURFACES[locale].tools.seal;
@@ -196,7 +199,7 @@ export const seal = {
196
199
  await atomicWriteJson(bearingPath(dir, id), {
197
200
  v: SCHEMA_VERSION, id, contract_seed: { predicate, check_by: checkBy }, predicate_owner: a['predicate_owner'],
198
201
  });
199
- const calendarPathW = await writeReturnCalendarEvent(dir, { id, predicate, check_by: checkBy, created_at: now });
202
+ const calendarPathW = await writeReturnCalendarEvent(dir, { id, predicate, check_by: checkBy, created_at: now, locale });
200
203
  // 미러 힌트: 원장 이벤트에 없는 elicit 목격 여부 + 영수증 필드만 —
201
204
  // 미러 자체는 appendLedger의 단일 관문이 수행한다 (mirror.ts).
202
205
  const appended = await appendLedger(dir, events, now, { seal: {
@@ -261,7 +264,7 @@ export const seal = {
261
264
  // dumping the absolute path — and the English label "Calendar file:" — into
262
265
  // a one-line surface was noise, and broke the Korean voice (copy-audit /
263
266
  // loop find). Mention it briefly, localized; keep the path in data.
264
- const calNote = locale === 'ko' ? ' 달력 리마인더(.ics)도 저장했어요.' : ' Saved a calendar reminder (.ics).';
267
+ const calNote = locale === 'ko' ? ' 달력 리마인더(.ics)도 저장했습니다.' : ' Saved a calendar reminder (.ics).';
265
268
  return envelope({
266
269
  ok: true, tool: 'argus_seal',
267
270
  surface: `${T.sealed(predicate, checkBy)}${calNote}${nudge}${syncLine}`,
@@ -21,8 +21,8 @@ import { handleToolException } from './errors.js';
21
21
  const inputSchema = z.strictObject({
22
22
  argus_dir: zArgusDir,
23
23
  id: zId,
24
- outcome: z.enum(['held', 'avoided', 'partial', 'still_pending', 'missed']).describe("What reality did to the prediction. Record the user's words never infer. 'missed' means the saved prediction was wrong; 'still_pending' means reality has not answered yet and requires a future defer_to. If omitted, Argus asks the user directly on hosts that support elicitation.").optional(),
25
- outcome_source: z.literal('user_stated').describe('Single value "user_stated". An AI-inferred outcome cannot be expressed.'),
24
+ outcome: z.enum(['held', 'avoided', 'partial', 'still_pending', 'missed']).describe("What reality did to the prediction record the user's words, never infer. Definitions (held and avoided are counted separately, so pick the right one): held = the predicted thing actually happened; avoided = the predicted RISK did not occur (the bad thing you predicted was dodged); partial = it half-happened / mixed; missed = the saved prediction was simply wrong; still_pending = reality has not answered yet, so pass a future defer_to and it comes back. If omitted, Argus asks the user directly on hosts that support elicitation.").optional(),
25
+ outcome_source: z.literal('user_stated').default('user_stated').describe('Always "user_stated" an AI-inferred outcome cannot be expressed. Defaulted, so you may omit it.'),
26
26
  what_happened: z.string().min(1).max(600).optional().describe("What reality did, in the user's words. Required when recording held/avoided/partial/missed. Omit for still_pending and pass defer_to instead."),
27
27
  broken_premise_ref: z.string().max(64).optional().describe('Optional, USER-attributed: which tracked premise (ordinal like "P1"), if any, broke and drove the outcome. Never inferred by the model — ask, or omit.'),
28
28
  defer_to: zDate.optional().describe("Only with outcome='still_pending': the new check-by (YYYY-MM-DD, a real future date) — when to look again, taken from the horizon the user names (\"the data lands next Friday\"). The decision stays alive and comes due again then. Omit only if the user has not said when; on elicitation hosts Argus will ask."),
@@ -50,14 +50,20 @@ export const settle = {
50
50
  // without elicitation.
51
51
  let outcome = a['outcome'];
52
52
  if (!outcome && canElicit()) {
53
- const picked = await elicit('현실이 어떻게 답했나요? (What did reality do?)', {
53
+ // Localize the picker like every other elicitation (ambient-elicit does):
54
+ // a bilingual "그렇게 됐다 (held)" mishmash showed to BOTH a Korean and an
55
+ // English user. Voice follows the language the decision was sealed in.
56
+ const pickerLocale = resolveResponseLocale(dir, current.predicate ?? null);
57
+ const picked = await elicit(pickerLocale === 'ko' ? '현실이 어떻게 답했나요?' : 'What did reality do?', {
54
58
  type: 'object',
55
59
  properties: {
56
60
  outcome: {
57
61
  type: 'string',
58
62
  enum: ['held', 'avoided', 'partial', 'still_pending', 'missed'],
59
- enumNames: ['그렇게 됐다 (held)', '피했다 (avoided)', '부분적으로 (partial)', '아직 불분명 (still pending)', '빗나갔다 (missed — my read was wrong)'],
60
- description: 'What reality did to your sealed prediction.',
63
+ enumNames: pickerLocale === 'ko'
64
+ ? ['그렇게 됐다', '피했다', '부분적으로', '아직 불분명', '빗나갔다 (내 예측이 틀렸다)']
65
+ : ['It held', 'Avoided', 'Partially', 'Still unclear', 'Missed — my read was wrong'],
66
+ description: pickerLocale === 'ko' ? '봉인한 예측에 현실이 어떻게 답했는지 고르세요.' : 'What reality did to your sealed prediction.',
61
67
  },
62
68
  },
63
69
  required: ['outcome'],
@@ -77,11 +83,13 @@ export const settle = {
77
83
  // Response voice follows what-happened (M4): config > text > env.
78
84
  const locale = resolveResponseLocale(dir, a['what_happened']);
79
85
  const T = SURFACES[locale].tools.settle;
80
- // Settle at the LOGICAL day under a today_override (sims/tests), else real
81
- // wall-clock the receipt's settled date should match the user's timeline,
82
- // not the simulator's clock (experience loop, settler). Real use has no
83
- // override, so settled_at stays the true write time.
84
- const now = a['today_override'] ? `${today}T12:00:00.000Z` : new Date().toISOString();
86
+ // The DATE part of `now` must equal the tz-aware logical `today`, else a
87
+ // Korea (UTC+9) user settling at 08:00 KST gets a receipt dated yesterday
88
+ // (raw UTC). Keep the real UTC time-of-day for ordering; stamp the logical
89
+ // date. (Same fix as seal.ts; recheck.ts fixed it for premise cadences.)
90
+ const now = a['today_override']
91
+ ? `${today}T12:00:00.000Z`
92
+ : `${today}T${new Date().toISOString().slice(11)}`;
85
93
  // still_pending = reality has NOT answered yet. This is NOT a settlement —
86
94
  // filing it as `settled` (terminal) silently closed the loop and dropped
87
95
  // the decision off check_in forever, while the surface lied "what actually
@@ -7,7 +7,11 @@ export const zArgusDir = z
7
7
  .string()
8
8
  .describe('Absolute path to the .argus directory (no ".."). Omit to use the ARGUS_DIR env var from your MCP config.')
9
9
  .optional();
10
- export const zId = z.string().regex(/^[A-Za-z0-9._-]+$/, 'id may only contain A-Z a-z 0-9 . _ -');
10
+ // The single id type: 1–128 chars, [A-Za-z0-9._-]. The bound lives here so
11
+ // every tool (predict/amend/dismiss/recheck) advertises the SAME limit the
12
+ // runtime path guard enforces — previously bare `zId` promised an unbounded id
13
+ // that then threw a PathSafetyError deep in the write path.
14
+ export const zId = z.string().min(1).max(128).regex(/^[A-Za-z0-9._-]+$/, 'id may only contain A-Z a-z 0-9 . _ -');
11
15
  export const zDate = z.string().regex(/^\d{4}-\d{2}-\d{2}$/, 'must be YYYY-MM-DD');
12
16
  const KO_FIELD_DESCRIPTIONS = {
13
17
  argus_dir: '프로젝트의 .argus 절대 경로입니다. 생략하면 MCP 설정의 ARGUS_DIR을 사용합니다.',
@@ -17,7 +21,7 @@ const KO_FIELD_DESCRIPTIONS = {
17
21
  reversibility: '결정을 되돌릴 수 있는 정도입니다.',
18
22
  status_quo: '아무것도 하지 않을 때 일어나는 일입니다.',
19
23
  already_decided: '이미 결정을 내렸는지 여부입니다.',
20
- crux_question: '결정을 가르는 중립적인 핵심 질문 하나입니다.',
24
+ crux_question: '결정을 좌우하는 중립적인 핵심 질문 하나입니다.',
21
25
  load_bearing_assumption: '결정이 가장 크게 기대는 전제 하나입니다.',
22
26
  related_to: '사용자가 비슷하다고 본 과거 결정 id입니다.',
23
27
  today_override: '테스트 또는 명시적 기준일에만 사용하는 오늘 날짜입니다.',
@@ -105,7 +109,7 @@ const EN_FIELD_DESCRIPTIONS = {
105
109
  reversibility: 'How difficult the decision is to reverse.',
106
110
  status_quo: 'What happens if nothing changes.',
107
111
  already_decided: 'Whether the user has already made the decision.',
108
- crux_question: 'One neutral question that separates the decision paths.',
112
+ crux_question: 'The one neutral, load-bearing question the decision turns on.',
109
113
  load_bearing_assumption: 'The single assumption the decision depends on most.',
110
114
  related_to: 'Past decision ids the user considers related.',
111
115
  action: 'The operation to perform.',
@@ -158,7 +162,14 @@ const EN_FIELD_DESCRIPTIONS = {
158
162
  };
159
163
  /** JSON Schema for tools/list, generated from the Zod source (drop $schema noise). */
160
164
  export function toolJsonSchema(schema) {
161
- const json = z.toJSONSchema(schema);
165
+ // io:'input' this schema describes the tool's ARGUMENTS. Zod v4 defaults
166
+ // z.toJSONSchema to io:'output', which marks every `.default()` field as
167
+ // REQUIRED (while still emitting its default) — so a strict host / the MCP
168
+ // Inspector would reject `argus_check_in {}` (the mandated session-start call),
169
+ // `argus_patterns {}`, and a premise without kind/external/load_bearing, even
170
+ // though the runtime validator fills those defaults. Input mode advertises them
171
+ // as optional; additionalProperties:false (strictObject) is preserved.
172
+ const json = z.toJSONSchema(schema, { io: 'input' });
162
173
  delete json['$schema'];
163
174
  const visit = (node) => {
164
175
  if (!node || typeof node !== 'object')
@@ -174,7 +185,16 @@ export function toolJsonSchema(schema) {
174
185
  if (ko) {
175
186
  const existing = typeof field['description'] === 'string' ? field['description'].trim() : '';
176
187
  const en = EN_FIELD_DESCRIPTIONS[key] ?? existing;
177
- field['description'] = en ? `${ko}\n\n${en}` : ko;
188
+ const base = en ? `${ko}\n\n${en}` : ko;
189
+ // A richer Zod .describe() (e.g. the argus_patterns `view` enum-value
190
+ // glossary) must not be dropped by the short bilingual map — append it
191
+ // when it carries more than the base already says. Never append a
192
+ // describe that references an internal tool name (argus_*): the
193
+ // bilingual map masks those on purpose, and re-surfacing one leaks an
194
+ // internal name into tools/list (public-surface-names guard).
195
+ field['description'] = existing.length > base.length && !base.includes(existing) && !/argus_/.test(existing)
196
+ ? `${base}\n\n${existing}`
197
+ : base;
178
198
  }
179
199
  visit(field);
180
200
  }
@@ -201,6 +221,12 @@ export const ENVELOPE_OUTPUT_SCHEMA = {
201
221
  over_fire_gate: { type: 'object' },
202
222
  error_code: { type: 'string' },
203
223
  message: { type: 'string' },
224
+ // error results also carry these — declared so a host that validates
225
+ // structuredContent against outputSchema on ERROR results (the SDK does, even
226
+ // when isError) would still pass if the schema were ever hardened to
227
+ // additionalProperties:false.
228
+ recovery: { type: 'string' },
229
+ invalid_fields: { type: 'array', items: { type: 'object' } },
204
230
  },
205
231
  required: ['ok', 'tool'],
206
232
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "argus-decision-mcp",
3
- "version": "1.4.2",
3
+ "version": "1.4.6",
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",