argus-decision-mcp 1.8.0 → 1.10.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/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 Keep/Reword/Skip picker 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).',
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
  '',
@@ -8,6 +8,7 @@ import { z } from 'zod';
8
8
  import { tunedStandingSense } from '../lib/ambient-prefs.js';
9
9
  import { envelope } from '../lib/envelope.js';
10
10
  import { canElicit } from '../lib/elicit.js';
11
+ import { packageMeta } from '../lib/package-meta.js';
11
12
  import { ENVELOPE_OUTPUT_SCHEMA, zArgusDir, zDate } from './tool-types.js';
12
13
  import { handleToolException } from './errors.js';
13
14
  import { briefDivergence, readV2Brief } from '../v2/mirror.js';
@@ -28,6 +29,19 @@ const anchorMirrorShownFor = new Set();
28
29
  export function resetCheckInSession() {
29
30
  anchorMirrorShownFor.clear();
30
31
  }
32
+ /**
33
+ * The wire facts a session can see about ITSELF. `picker` answers "does this host
34
+ * show real pickers"; `server_version` answers "which build am I actually talking
35
+ * to" — the gap that let a founder dogfood 1.2.0 for twelve days while seven
36
+ * releases sat on npm (npx reuses a cached install whenever the spec is a RANGE,
37
+ * so `argus-decision-mcp@^1` never upgrades on its own). CI gates the repo and
38
+ * npm holds the latest; neither can see what a live session actually launched.
39
+ * Reported on every check_in so `/doctor` — and the user — can compare it to the
40
+ * version the plugin pins, instead of inferring staleness from missing behavior.
41
+ */
42
+ function wireFacts() {
43
+ return { picker: canElicit() ? 'one_tap' : 'text_fallback', server_version: packageMeta().version };
44
+ }
31
45
  const inputSchema = z.strictObject({
32
46
  argus_dir: zArgusDir,
33
47
  // max(365), not max(30): the handler clamps the WINDOW to 30 days, but the
@@ -272,14 +286,14 @@ export const checkIn = {
272
286
  ok: true, tool: 'argus_check_in',
273
287
  surface: S.first_run,
274
288
  next_actions: ['argus_capture'],
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' },
289
+ data: { due: [], due_count: 0, due_premises: [], due_premise_count: 0, due_open_questions: [], due_open_question_count: 0, first_run: true, today, ...wireFacts() },
276
290
  });
277
291
  }
278
292
  return envelope({
279
293
  ok: true, tool: 'argus_check_in',
280
294
  surface: mirrorLine + S.nothing_due + accountHint + upcomingLine + fleetLine + integrityLine,
281
295
  next_actions: ['stop'],
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)) } : {}) },
296
+ data: { due: [], due_count: 0, due_premises: [], due_premise_count: 0, due_open_questions: [], due_open_question_count: 0, ...wireFacts(), ...(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)) } : {}) },
283
297
  });
284
298
  }
285
299
  const parts = [];
@@ -313,7 +327,7 @@ export const checkIn = {
313
327
  surface: mirrorLine + parts.join(' ') + upcomingLine + fleetLine + integrityLine,
314
328
  next_actions: next,
315
329
  data: {
316
- picker: canElicit() ? 'one_tap' : 'text_fallback',
330
+ ...wireFacts(),
317
331
  due: dueEnriched, due_count: dueAll.length,
318
332
  ...(dueTruncated > 0 ? { due_truncated: `${dueAll.length} due, showing ${DUE_TOP} oldest` } : {}),
319
333
  due_premises: duePrem, due_premise_count: premiseGroups.length,
@@ -203,42 +203,38 @@ async function opAdd(dir, id, today, now, state, existing, a) {
203
203
  if (aiDrafts.length === 1 && canElicit()) {
204
204
  const draft = aiDrafts[0];
205
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.
206
210
  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: {
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: {
215
214
  type: 'string',
216
- description: dLocale === 'ko' ? '"직접 고쳐 쓰기"를 골랐다면 원하는 문장을 여기에 적어 주세요. 그대로 저장됩니다.' : 'If you chose "Let me reword", type the premise here. It is saved exactly as written.',
215
+ description: dLocale === 'ko' ? '전제를 고쳐 쓰려면 여기에 적으세요. 비우면 문장 그대로 기록합니다.' : 'To reword the premise, type it here. Leave blank to keep the statement above.',
217
216
  },
218
217
  } });
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) {
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
+ }
223
232
  draft.ai_original = draft.ai_original ?? draft.text;
224
233
  draft.text = wording;
225
234
  draft.source = 'user_stated';
226
235
  }
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
- }
236
+ // Accept blank → keep as drafted, provenance ai_surfaced intact.
240
237
  }
241
- // keep → recorded as drafted below, provenance ai_surfaced intact.
242
238
  }
243
239
  }
244
240
  // Dedup against the ledger by stable id. Three cases, kept distinct so a
@@ -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 Keep/Reword/Skip before saving.'),
40
- confirm_draft: z.boolean().optional().describe('Optional extra confirmation: force the one-tap picker even for a "user" predicate. ai_surfaced predicates get the picker automatically on supporting hosts. In the picker, Keep affirms and saves it as theirs, Reword saves the wording the user types (or asks in chat if left blank), Skip records nothing. Without picker support, saving proceeds — confirm in your own message first.'),
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
- const checkBy = String(a['check_by']);
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): show the draft with Keep / Reword / Skip before it lands.
80
- // Keep = the user affirmed it record as THEIRS. Reword with wording
81
- // typed in the same form = record THAT, user-authored, one round-trip.
82
- // Reword without wording / Skip / a declined picker = record nothing
83
- // (respect the non-yes). STRUCTURAL trigger: an ai_surfaced predicate
84
- // fires the picker on any capable host even when the model forgot
85
- // confirm_draft "the draft was confirmed" must not depend on prose
86
- // compliance. On a host with no elicitation, this is skipped and the
87
- // seal proceeds with honest ai_surfaced provenance (the friction escape
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용: elicit 채널로 확인된 것만 elicited_user (II-B) — Keep 또는 폼에 직접 쓴 reword
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' ? `이 예측으로 기록할까요?\n"${predicate}" (확인일 ${checkBy})` : `Record this prediction?\n"${predicate}" (check-by ${checkBy})`, { type: 'object', required: ['choice'], properties: {
92
- choice: {
93
- type: 'string', enum: ['keep', 'reword', 'skip'],
94
- enumNames: locale === 'ko' ? ['그대로 기록', '직접 고쳐 쓰기', '건너뛰기'] : ['Keep it', 'Let me reword', 'Skip'],
95
- description: locale === 'ko' ? '이 예측을 기록할지 고르세요.' : 'Whether to record this prediction.',
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
- your_wording: {
102
+ check_by: {
98
103
  type: 'string',
99
- description: locale === 'ko' ? '"직접 고쳐 쓰기"를 골랐다면, 원하는 예측 문장을 여기에 적어 주세요. 그 말 그대로 저장됩니다.' : 'If you chose "Let me reword", type your prediction here. It is saved exactly as written.',
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
- const choice = picked?.['choice'];
103
- if (choice === 'reword') {
104
- const wording = typeof picked?.['your_wording'] === 'string' ? picked['your_wording'].trim() : '';
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
- else if (choice !== 'keep') {
132
- // skip, or a declined/cancelled picker record nothing.
133
- return envelope({ ok: true, tool: 'argus_seal', surface: locale === 'ko' ? '기록하지 않았습니다.' : 'Not recorded.', next_actions: ['stop'], data: { sealed: false, choice: choice ?? 'declined' } });
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
- else {
136
- // keep the user affirmed the draft, so it is theirs now.
137
- a = { ...a, predicate_owner: 'user' };
138
- elicitedKeep = true;
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);
@@ -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.8.0",
3
+ "version": "1.10.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",