argus-decision-mcp 1.2.0 → 1.3.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.
Files changed (82) hide show
  1. package/README.md +44 -60
  2. package/SECURITY.md +3 -3
  3. package/dist/a0/account-connect.js +180 -0
  4. package/dist/a0/account-credentials.js +86 -0
  5. package/dist/index.js +35 -1
  6. package/dist/lib/ambient-elicit.js +225 -0
  7. package/dist/lib/argus-dir.js +26 -8
  8. package/dist/lib/calendar.js +2 -2
  9. package/dist/lib/due-note.js +3 -3
  10. package/dist/lib/elicit.js +13 -1
  11. package/dist/lib/ledger-replay.js +19 -3
  12. package/dist/lib/locale-mismatch.js +52 -0
  13. package/dist/lib/locale.js +81 -10
  14. package/dist/lib/localize-result.js +120 -0
  15. package/dist/lib/localized-message.js +13 -0
  16. package/dist/lib/premises.js +0 -2
  17. package/dist/lib/push-account.js +4 -3
  18. package/dist/lib/review/extract-core.js +116 -0
  19. package/dist/lib/review/extract-file-node.js +145 -29
  20. package/dist/lib/review/index.js +1 -1
  21. package/dist/lib/review/ingest.js +84 -20
  22. package/dist/lib/review/lenses.js +18 -0
  23. package/dist/lib/review/prompts.js +199 -22
  24. package/dist/lib/review/reviewability.js +8 -7
  25. package/dist/lib/review/routing.js +38 -17
  26. package/dist/lib/review-path.js +2 -2
  27. package/dist/lib/spine.js +13 -21
  28. package/dist/lib/state-machine.js +12 -12
  29. package/dist/lib/surfaces.js +90 -94
  30. package/dist/lib/tool-presentation.js +35 -0
  31. package/dist/resources.js +52 -14
  32. package/dist/server.js +85 -31
  33. package/dist/tools/candidates.js +3 -3
  34. package/dist/tools/check-in.js +24 -12
  35. package/dist/tools/errors.js +17 -3
  36. package/dist/tools/index.js +34 -2
  37. package/dist/tools/init-config.js +39 -11
  38. package/dist/tools/open-decision.js +58 -25
  39. package/dist/tools/premises.js +18 -18
  40. package/dist/tools/public-tools.js +382 -0
  41. package/dist/tools/recall.js +99 -24
  42. package/dist/tools/recheck.js +3 -3
  43. package/dist/tools/review.js +133 -29
  44. package/dist/tools/seal.js +62 -26
  45. package/dist/tools/semantic-record.js +225 -0
  46. package/dist/tools/settle.js +49 -8
  47. package/dist/tools/sync.js +2 -2
  48. package/dist/tools/tool-types.js +174 -0
  49. package/dist/tools/watch.js +1 -1
  50. package/dist/v2/bridge.js +2 -2
  51. package/dist/v2/brief.js +1 -1
  52. package/dist/v2/candidate-capture.js +150 -0
  53. package/dist/v2/capture-cli.js +72 -0
  54. package/dist/v2/capture-runtime.js +73 -0
  55. package/dist/v2/connection-io.js +47 -0
  56. package/dist/v2/connection.js +93 -0
  57. package/dist/v2/evidence.js +5 -1
  58. package/dist/v2/harvest.js +26 -51
  59. package/dist/v2/lifecycle-cli.js +50 -0
  60. package/dist/v2/lifecycle.js +298 -2
  61. package/dist/v2/logbook.js +14 -14
  62. package/dist/v2/mirror.js +2 -2
  63. package/dist/v2/queue.js +71 -12
  64. package/dist/v2/v1-reader.js +6 -3
  65. package/dist/v3/fixtures/dkk-corpus.js +55 -0
  66. package/dist/v3/fixtures/p5-measurement-plan.js +42 -0
  67. package/dist/v3/index.js +5 -0
  68. package/dist/v3/legacy-v2.js +169 -0
  69. package/dist/v3/p5-gate.js +123 -0
  70. package/dist/v3/reducer.js +290 -0
  71. package/dist/v3/store.js +133 -0
  72. package/dist/v3/types.js +193 -0
  73. package/dist/v4/index.js +5 -0
  74. package/dist/v4/reducer.js +361 -0
  75. package/dist/v4/relation-validation.js +40 -0
  76. package/dist/v4/shadow.js +22 -0
  77. package/dist/v4/types.js +326 -0
  78. package/dist/v4/watch.js +18 -0
  79. package/package.json +6 -3
  80. package/dist/lib/discipline.js +0 -42
  81. package/dist/prompts.js +0 -72
  82. package/snippets/claude-code-watch.md +0 -47
@@ -122,10 +122,10 @@ export const premises = {
122
122
  }
123
123
  const hits = captures.filter((c) => c.id === ref || (c.id && c.id.startsWith(ref)) || c.text === ref);
124
124
  if (hits.length === 0) {
125
- return toolError({ ok: false, tool: 'argus_premises', error_code: 'CAPTURE_NOT_FOUND', message: `No watch capture matches "${ref}".`, recovery: 'List captures with argus_watch op=list and pass the wc- id.' });
125
+ return toolError({ ok: false, tool: 'argus_premises', error_code: 'CAPTURE_NOT_FOUND', message: `No internal capture matches "${ref}".`, recovery: 'Pass the premise sentence directly in `text`.' });
126
126
  }
127
127
  if (hits.length > 1) {
128
- return toolError({ ok: false, tool: 'argus_premises', error_code: 'AMBIGUOUS_REF', message: `"${ref}" matches ${hits.length} captures.`, recovery: 'Pass the full wc- id from argus_watch op=list.' });
128
+ return toolError({ ok: false, tool: 'argus_premises', error_code: 'AMBIGUOUS_REF', message: `"${ref}" matches ${hits.length} internal captures.`, recovery: 'Pass the premise sentence directly in `text`.' });
129
129
  }
130
130
  const c = hits[0];
131
131
  resolved.push({
@@ -150,7 +150,7 @@ export const premises = {
150
150
  if (op === 'add' && Array.isArray(a['premises'])) {
151
151
  for (const p of a['premises']) {
152
152
  if (!(typeof p['text'] === 'string' && p['text'].trim().length > 0)) {
153
- return toolError({ ok: false, tool: 'argus_premises', error_code: 'INVALID_INPUT', message: 'Each premise needs `text` (or a resolvable `from_capture`).', recovery: 'Pass the premise sentence, or a wc- capture id from argus_watch op=list.' });
153
+ return toolError({ ok: false, tool: 'argus_premises', error_code: 'INVALID_INPUT', message: 'Each premise needs `text` (or a resolvable internal capture).', recovery: 'Pass the premise sentence directly in `text`.' });
154
154
  }
155
155
  if (typeof p['source'] !== 'string') {
156
156
  return toolError({ ok: false, tool: 'argus_premises', error_code: 'PROVENANCE_REQUIRED', message: `Each premise needs \`source\` (user_stated | ai_surfaced): "${String(p['text']).slice(0, 60)}"`, recovery: 'Say who said it — never forge provenance. (from_capture carries the capture\'s provenance automatically.)' });
@@ -269,8 +269,8 @@ async function opAdd(dir, id, today, now, state, existing, a) {
269
269
  const sealedNow = state === 'sealed';
270
270
  const refRange = events.length > 0 ? `${echo[0].ref}${echo.length > 1 ? `–${echo[echo.length - 1].ref}` : ''}` : '';
271
271
  const monitoredNote = monitoredCount === 0 ? '' : ko
272
- ? (sealedNow ? ` 그중 ${monitoredCount}건은 나중에 실제와 다시 대조해 확인합니다 (이미 봉인됨).` : ` 그중 ${monitoredCount}건은 봉인한 실제와 다시 대조해 확인합니다.`)
273
- : (sealedNow ? ` ${monitoredCount} will be re-checked against what actually happens (this decision is sealed).` : ` ${monitoredCount} will be re-checked against what actually happens once you seal the decision.`);
272
+ ? (sealedNow ? ` 그중 ${monitoredCount}건은 나중에 실제와 다시 대조해 확인합니다 (예측 저장됨).` : ` 예측을 저장하면 그중 ${monitoredCount}건을 나중에 실제와 다시 대조해 확인합니다.`)
273
+ : (sealedNow ? ` ${monitoredCount} will be re-checked against what actually happens (prediction saved).` : ` After saving a prediction, ${monitoredCount} will be re-checked against what actually happens.`);
274
274
  const oneLine = (s) => {
275
275
  const t = s.replace(/\s+/g, ' ').trim();
276
276
  return t.length > 70 ? t.slice(0, 69) + '…' : t;
@@ -289,14 +289,14 @@ async function opAdd(dir, id, today, now, state, existing, a) {
289
289
  : (events.length === 1
290
290
  ? (ko
291
291
  ? `방금 적어뒀어요: '${oneLine(echo[0]?.text ?? '')}'. 고칠 게 있으면 op=amend로 바꿀 수 있어요.${monitoredNote}`
292
- : `Noted: "${oneLine(echo[0]?.text ?? '')}". Fix anything wrong with op=amend.${monitoredNote}`)
292
+ : `Noted: "${oneLine(echo[0]?.text ?? '')}". Fix anything wrong with argus_capture.${monitoredNote}`)
293
293
  : (ko
294
294
  ? `전제 ${events.length}건을 기록했습니다 (${refRange}). 틀린 것이 있으면 op=amend로 고치세요. 고친 내용도 기록에 남습니다.${monitoredNote}`
295
- : `${events.length} premise(s) recorded (${refRange}). Fix anything wrong with op=amend; your correction stays on the record too.${monitoredNote}`));
295
+ : `${events.length} premise(s) recorded (${refRange}). Fix anything wrong with argus_capture; your correction stays on the record too.${monitoredNote}`));
296
296
  return envelope({
297
297
  ok: true, tool: 'argus_premises',
298
298
  surface,
299
- next_actions: ['argus_seal', 'argus_recall', 'leave_as_is'],
299
+ next_actions: ['argus_predict', 'argus_patterns', 'leave_as_is'],
300
300
  data: { id, premises: echo, skipped_duplicates: skippedDup, ...(dupRetired.length ? { skipped_retired: dupRetired } : {}), ledger_events_written: events.map(() => 'premise_add') },
301
301
  });
302
302
  }
@@ -306,7 +306,7 @@ async function opAmend(dir, id, now, state, existing, a) {
306
306
  const ref = a['ref'];
307
307
  const action = a['action'];
308
308
  if (typeof ref !== 'string' || !action) {
309
- return toolError({ ok: false, tool: 'argus_premises', error_code: 'AMEND_NEEDS_REF', message: 'op=amend needs `ref` (e.g. "P1") and `action`.', recovery: 'List premises via argus_recall view="premises", then amend by ordinal.' });
309
+ return toolError({ ok: false, tool: 'argus_premises', error_code: 'AMEND_NEEDS_REF', message: 'op=amend needs `ref` (e.g. "P1") and `action`.', recovery: 'List premises via argus_patterns view="decision_context", then amend by ordinal.' });
310
310
  }
311
311
  const premise = resolvePremiseRef(existing, ref);
312
312
  if (premise.status !== 'active' && action !== 'accept') {
@@ -349,7 +349,7 @@ async function opAmend(dir, id, now, state, existing, a) {
349
349
  return envelope({
350
350
  ok: true, tool: 'argus_premises',
351
351
  surface,
352
- next_actions: ['argus_recall', 'leave_as_is'],
352
+ next_actions: ['argus_patterns', 'leave_as_is'],
353
353
  data: {
354
354
  id, ref: `P${premise.ordinal}`, premise_id: premise.premise_id, action,
355
355
  ...(text ? { text } : {}), monitored_after: armed,
@@ -361,14 +361,14 @@ async function opResolve(dir, id, now, state, existing, a) {
361
361
  guardTransition(state, 'premise_resolve');
362
362
  const ref = a['ref'];
363
363
  if (typeof ref !== 'string') {
364
- return toolError({ ok: false, tool: 'argus_premises', error_code: 'RESOLVE_NEEDS_REF', message: 'op=resolve needs `ref`.', recovery: 'List open questions via argus_recall view="premises", then resolve by ordinal.' });
364
+ return toolError({ ok: false, tool: 'argus_premises', error_code: 'RESOLVE_NEEDS_REF', message: 'op=resolve needs `ref`.', recovery: 'List open questions via argus_patterns view="decision_context", then resolve by ordinal.' });
365
365
  }
366
366
  const premise = resolvePremiseRef(existing, ref);
367
367
  if (premise.kind !== 'open_question') {
368
- return toolError({ ok: false, tool: 'argus_premises', error_code: 'NOT_AN_OPEN_QUESTION', message: `P${premise.ordinal} is a premise, not an open question.`, recovery: 'Premises are re-checked against reality (argus_recheck), not resolved. Only an open_question takes the user\'s closing call.' });
368
+ return toolError({ ok: false, tool: 'argus_premises', error_code: 'NOT_AN_OPEN_QUESTION', message: `P${premise.ordinal} is a premise, not an open question.`, recovery: 'Re-check premises with argus_capture action="update_fact". Only an open question takes the user\'s closing call.' });
369
369
  }
370
370
  if (premise.status !== 'active') {
371
- return toolError({ ok: false, tool: 'argus_premises', error_code: 'PREMISE_RETIRED', message: `P${premise.ordinal} is already ${premise.status}.`, recovery: 'Read it via argus_recall view="premises".' });
371
+ return toolError({ ok: false, tool: 'argus_premises', error_code: 'PREMISE_RETIRED', message: `P${premise.ordinal} is already ${premise.status}.`, recovery: 'Read it via argus_patterns view="decision_context".' });
372
372
  }
373
373
  let decision = typeof a['decision'] === 'string' ? a['decision'].trim() : '';
374
374
  if (!decision) {
@@ -388,7 +388,7 @@ async function opResolve(dir, id, now, state, existing, a) {
388
388
  surface: ko
389
389
  ? `미결 질문을 당신의 말로 닫았습니다 (P${premise.ordinal}): "${decision}".`
390
390
  : `Open question P${premise.ordinal} closed in your words: "${decision}".`,
391
- next_actions: ['argus_recall', 'leave_as_is'],
391
+ next_actions: ['argus_patterns', 'leave_as_is'],
392
392
  data: { id, ref: `P${premise.ordinal}`, premise_id: premise.premise_id, decision, decision_owner: 'user' },
393
393
  });
394
394
  }
@@ -401,14 +401,14 @@ async function opStillOpen(dir, id, today, now, state, existing, a) {
401
401
  guardTransition(state, 'premise_reconsider');
402
402
  const ref = a['ref'];
403
403
  if (typeof ref !== 'string') {
404
- return toolError({ ok: false, tool: 'argus_premises', error_code: 'STILL_OPEN_NEEDS_REF', message: 'op=still_open needs `ref`.', recovery: 'List open questions via argus_recall view="premises", then defer by ordinal.' });
404
+ return toolError({ ok: false, tool: 'argus_premises', error_code: 'STILL_OPEN_NEEDS_REF', message: 'op=still_open needs `ref`.', recovery: 'List open questions via argus_patterns view="decision_context", then defer by ordinal.' });
405
405
  }
406
406
  const premise = resolvePremiseRef(existing, ref);
407
407
  if (premise.kind !== 'open_question') {
408
- return toolError({ ok: false, tool: 'argus_premises', error_code: 'NOT_AN_OPEN_QUESTION', message: `P${premise.ordinal} is a premise, not an open question.`, recovery: 'Only an open_question can be left open. A premise is re-checked against reality (argus_recheck).' });
408
+ return toolError({ ok: false, tool: 'argus_premises', error_code: 'NOT_AN_OPEN_QUESTION', message: `P${premise.ordinal} is a premise, not an open question.`, recovery: 'Only an open question can be left open. Re-check a premise with argus_capture action="update_fact".' });
409
409
  }
410
410
  if (premise.status !== 'active') {
411
- return toolError({ ok: false, tool: 'argus_premises', error_code: 'PREMISE_RETIRED', message: `P${premise.ordinal} is already ${premise.status}.`, recovery: 'Read it via argus_recall view="premises".' });
411
+ return toolError({ ok: false, tool: 'argus_premises', error_code: 'PREMISE_RETIRED', message: `P${premise.ordinal} is already ${premise.status}.`, recovery: 'Read it via argus_patterns view="decision_context".' });
412
412
  }
413
413
  await appendLedger(dir, [{
414
414
  id, event: 'premise_reconsider', premise_id: premise.premise_id, anchor_date: today,
@@ -420,7 +420,7 @@ async function opStillOpen(dir, id, today, now, state, existing, a) {
420
420
  surface: ko
421
421
  ? `P${premise.ordinal}, 열린 채로 둡니다. 평결도 압박도 없습니다. 한참 뒤에 다시 보여드리고, 그 전에는 조용히 있겠습니다. 질문을 열어두는 것도 진짜 선택입니다.`
422
422
  : `P${premise.ordinal} stays open. No verdict, no pressure. Argus brings it back after a while, not before you're ready. Leaving a question open is a real choice.`,
423
- next_actions: ['argus_recall', 'leave_as_is'],
423
+ next_actions: ['argus_patterns', 'leave_as_is'],
424
424
  data: { id, ref: `P${premise.ordinal}`, premise_id: premise.premise_id, deferred: true },
425
425
  });
426
426
  }
@@ -0,0 +1,382 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { z } from 'zod';
4
+ import { resolveToolArgusDir } from '../lib/argus-dir.js';
5
+ import { configPath } from '../lib/layout.js';
6
+ import { ENVELOPE_OUTPUT_SCHEMA, zArgusDir, zDate, zId } from './tool-types.js';
7
+ import { openDecision } from './open-decision.js';
8
+ import { premises } from './premises.js';
9
+ import { recheck } from './recheck.js';
10
+ import { amend, dismiss } from './amend-dismiss.js';
11
+ import { recall } from './recall.js';
12
+ import { init, config } from './init-config.js';
13
+ import { sync } from './sync.js';
14
+ import { seal } from './seal.js';
15
+ import { checkIn } from './check-in.js';
16
+ import { settle } from './settle.js';
17
+ import { detectLocaleFromText } from '../lib/locale.js';
18
+ import { gitCommonDirOf } from '../v2/git-discovery.js';
19
+ const premiseInput = z.strictObject({
20
+ text: z.string().min(3).max(400).describe('결정이 기대는 사실 또는 아직 답하지 못한 질문입니다. 사용자의 표현을 그대로 씁니다.').optional(),
21
+ kind: z.enum(['premise', 'open_question']).default('premise').describe('premise는 확인할 전제, open_question은 사용자가 아직 답하지 않은 질문입니다.'),
22
+ external: z.boolean().default(false).describe('외부 현실에서 나중에 다시 확인할 수 있는 사실인지 표시합니다.'),
23
+ load_bearing: z.boolean().default(false).describe('틀리면 결정이 바뀌는 핵심 전제인지 표시합니다.'),
24
+ source: z.enum(['user_stated', 'ai_surfaced']).describe('문장을 말한 주체입니다. 사용자의 말을 AI의 말로, AI의 말을 사용자의 말로 바꾸지 않습니다.').optional(),
25
+ ai_original: z.string().max(400).describe('source가 ai_surfaced일 때 AI가 처음 제시한 원문입니다.').optional(),
26
+ recheck_cadence_days: z.number().int().min(1).max(365).describe('이 사실을 다시 확인할 간격(일)입니다.').optional(),
27
+ reconsider_cadence_days: z.number().int().min(1).max(365).describe('미결 질문을 다시 볼 간격(일)입니다.').optional(),
28
+ });
29
+ const common = {
30
+ argus_dir: zArgusDir,
31
+ };
32
+ const decideSchema = z.discriminatedUnion('action', [
33
+ z.strictObject({
34
+ ...common,
35
+ action: z.literal('open').describe('새 결정을 검토합니다.'),
36
+ id: zId.min(1).max(128).describe('이 결정의 짧고 고유한 식별자입니다.'),
37
+ decision: z.string().min(1).max(600).describe('사용자가 실제로 마주한 선택을 중립적인 한 문장으로 적습니다.'),
38
+ stakes: z.enum(['trivial', 'low', 'moderate', 'high']).describe('틀렸을 때의 비용입니다. 애매하면 낮은 쪽을 선택합니다.'),
39
+ reversibility: z.enum(['one_way_door', 'costly_to_reverse', 'easily_reversible']).describe('결정을 되돌릴 수 있는 정도입니다.'),
40
+ status_quo: z.string().min(1).max(300).describe('아무것도 하지 않을 때 일어나는 일입니다.'),
41
+ already_decided: z.boolean().default(false).describe('사용자가 이미 결정을 끝냈는지 표시합니다.'),
42
+ load_bearing_assumption: z.string().max(400).describe('결정이 가장 크게 기대는 전제 하나입니다.').optional(),
43
+ related_to: z.array(zId).max(20).describe('사용자가 비슷하다고 본 과거 결정 id입니다.').optional(),
44
+ premises: z.array(premiseInput).min(1).max(5).describe('결정이 기대는 전제와 미결 질문입니다. 선택 사항이며, 있으면 결정과 함께 기록합니다.').optional(),
45
+ }),
46
+ z.strictObject({
47
+ ...common,
48
+ action: z.literal('add_context').describe('결정이 기대는 전제나 미결 질문을 추가합니다.'),
49
+ id: zId.describe('대상 결정 id입니다.'),
50
+ premises: z.array(premiseInput).min(1).max(5).describe('추가할 전제와 미결 질문입니다.'),
51
+ }),
52
+ z.strictObject({
53
+ ...common,
54
+ action: z.literal('answer_question').describe('미결 질문을 사용자의 판단으로 닫습니다.'),
55
+ id: zId.describe('대상 결정 id입니다.'),
56
+ ref: z.string().max(64).describe('답할 미결 질문 번호 또는 id입니다.'),
57
+ decision: z.string().min(1).max(400).describe('사용자가 직접 내린 판단입니다. AI가 대신 작성하지 않습니다.'),
58
+ }),
59
+ z.strictObject({
60
+ ...common,
61
+ action: z.literal('keep_question_open').describe('미결 질문을 지금은 열린 채로 둡니다.'),
62
+ id: zId.describe('대상 결정 id입니다.'),
63
+ ref: z.string().max(64).describe('열어둘 미결 질문 번호 또는 id입니다.'),
64
+ reconsider_cadence_days: z.number().int().min(1).max(365).describe('다시 물어볼 간격(일)입니다.').optional(),
65
+ }),
66
+ z.strictObject({
67
+ ...common,
68
+ action: z.literal('update_fact').describe('결정이 기대는 외부 사실을 현재 현실과 다시 확인합니다.'),
69
+ id: zId.describe('대상 결정 id입니다.'),
70
+ ref: z.string().max(64).describe('재확인할 전제 번호 또는 id입니다.'),
71
+ finding: z.string().min(1).max(800).describe('현재 확인한 사실을 비교 가능한 한 문장으로 적습니다.'),
72
+ numeric_value: z.number().describe('수치 사실의 현재 값을 명시적으로 전달합니다.').optional(),
73
+ changed: z.boolean().describe('문장형 사실이 기준값에서 실질적으로 달라졌는지 표시합니다.').optional(),
74
+ source: z.enum(['url', 'user_stated', 'host_reported']).describe('현재 사실을 확인한 출처입니다.'),
75
+ source_detail: z.string().max(1000).describe('출처 URL 또는 짧은 인용 정보입니다.').optional(),
76
+ apply_to_matching: z.boolean().default(false).describe('같은 사실을 추적하는 다른 결정에도 이 확인 결과를 적용합니다.'),
77
+ }),
78
+ z.strictObject({
79
+ ...common,
80
+ action: z.literal('change_prediction').describe('현실이 답하기 전에 기록한 예측이나 확인일을 수정합니다.'),
81
+ id: zId.describe('대상 결정 id입니다.'),
82
+ predicate: z.string().min(8).max(500).describe('수정할 예측 문장입니다.').optional(),
83
+ check_by: zDate.describe('수정할 미래 확인일입니다.').optional(),
84
+ }),
85
+ z.strictObject({
86
+ ...common,
87
+ action: z.literal('close').describe('더는 답이 필요 없는 결정을 평결 없이 닫습니다.'),
88
+ id: zId.describe('대상 결정 id입니다.'),
89
+ dismiss_reason: z.enum(['became_irrelevant', 'decided_elsewhere', 'superseded', 'user_declined']).describe('결정을 더는 추적하지 않는 이유입니다.'),
90
+ note: z.string().max(500).describe('선택적인 사용자 메모입니다.').optional(),
91
+ }),
92
+ ]);
93
+ // MCP requires a top-level object inputSchema. Keep the action-specific union
94
+ // as the runtime validator, while advertising one compatible object whose
95
+ // field descriptions state the purpose. This avoids host rejection of a
96
+ // top-level oneOf without weakening runtime validation.
97
+ const decidePublicSchema = z.strictObject({
98
+ argus_dir: zArgusDir,
99
+ action: z.enum(['open', 'add_context', 'answer_question', 'keep_question_open', 'update_fact', 'change_prediction', 'close']).describe('수행할 결정 작업입니다. 선택한 작업에 필요한 필드만 전달합니다.'),
100
+ id: zId.min(1).max(128).describe('대상 결정의 짧고 고유한 식별자입니다.').optional(),
101
+ decision: z.string().min(1).max(600).describe('새 결정 또는 미결 질문에 대한 사용자의 판단입니다.').optional(),
102
+ stakes: z.enum(['trivial', 'low', 'moderate', 'high']).describe('틀렸을 때의 비용입니다. 새 결정을 열 때 사용합니다.').optional(),
103
+ reversibility: z.enum(['one_way_door', 'costly_to_reverse', 'easily_reversible']).describe('결정을 되돌릴 수 있는 정도입니다. 새 결정을 열 때 사용합니다.').optional(),
104
+ status_quo: z.string().min(1).max(300).describe('아무것도 하지 않을 때 일어나는 일입니다. 새 결정을 열 때 사용합니다.').optional(),
105
+ already_decided: z.boolean().describe('사용자가 이미 결정을 끝냈는지 표시합니다.').optional(),
106
+ load_bearing_assumption: z.string().max(400).describe('결정이 가장 크게 기대는 전제 하나입니다.').optional(),
107
+ related_to: z.array(zId).max(20).describe('사용자가 비슷하다고 본 과거 결정 id입니다.').optional(),
108
+ premises: z.array(premiseInput).min(1).max(5).describe('추가할 전제와 미결 질문입니다.').optional(),
109
+ ref: z.string().max(64).describe('답하거나 재확인할 전제 또는 미결 질문 번호입니다.').optional(),
110
+ reconsider_cadence_days: z.number().int().min(1).max(365).describe('미결 질문을 다시 볼 간격(일)입니다.').optional(),
111
+ finding: z.string().min(1).max(800).describe('현재 확인한 사실을 비교 가능한 한 문장으로 적습니다.').optional(),
112
+ numeric_value: z.number().describe('수치 사실의 현재 값을 명시적으로 전달합니다.').optional(),
113
+ changed: z.boolean().describe('문장형 사실이 기준값에서 실질적으로 달라졌는지 표시합니다.').optional(),
114
+ source: z.enum(['url', 'user_stated', 'host_reported']).describe('현재 사실을 확인한 출처입니다.').optional(),
115
+ source_detail: z.string().max(1000).describe('출처 URL 또는 짧은 인용 정보입니다.').optional(),
116
+ apply_to_matching: z.boolean().describe('같은 사실을 추적하는 다른 결정에도 적용합니다.').optional(),
117
+ predicate: z.string().min(8).max(500).describe('수정할 예측 문장입니다.').optional(),
118
+ check_by: zDate.describe('수정할 미래 확인일입니다.').optional(),
119
+ dismiss_reason: z.enum(['became_irrelevant', 'decided_elsewhere', 'superseded', 'user_declined']).describe('결정을 더는 추적하지 않는 이유입니다.').optional(),
120
+ note: z.string().max(500).describe('선택적인 사용자 메모입니다.').optional(),
121
+ }).superRefine((value, ctx) => {
122
+ const parsed = decideSchema.safeParse(value);
123
+ if (parsed.success)
124
+ return;
125
+ for (const issue of parsed.error.issues) {
126
+ ctx.addIssue({ code: 'custom', path: issue.path, message: issue.message });
127
+ }
128
+ });
129
+ const historySchema = z.strictObject({
130
+ argus_dir: zArgusDir,
131
+ view: z.enum(['active', 'all', 'receipt', 'decision_context', 'timeline', 'reflection']).default('active').describe('active는 진행 중인 결정, all은 전체 기록, receipt는 판단 영수증, decision_context는 결정의 전제와 미결 질문, timeline은 시간순 기록, reflection은 당신이 쓴 예측·전제와 그 결과를 되읽는 기록입니다.'),
132
+ id: zId.describe('receipt 또는 decision_context를 볼 때 필요한 결정 id입니다.').optional(),
133
+ });
134
+ const settingsSchema = z.discriminatedUnion('action', [
135
+ z.strictObject({
136
+ argus_dir: zArgusDir,
137
+ action: z.literal('status').describe('현재 Argus 설정을 확인합니다.'),
138
+ }),
139
+ z.strictObject({
140
+ argus_dir: zArgusDir,
141
+ action: z.literal('update').describe('사용자가 선택한 설정을 수정합니다.'),
142
+ locale: z.enum(['ko', 'en']).describe('사용자 표면 언어입니다.').optional(),
143
+ ambient_mute: z.boolean().describe('세션 중 확인일 알림 문장을 숨길지 정합니다.').optional(),
144
+ premise_sync: z.boolean().describe('추적 전제를 계정과 동기화할지 명시적으로 선택합니다.').optional(),
145
+ }),
146
+ z.strictObject({
147
+ argus_dir: zArgusDir,
148
+ action: z.literal('sync').describe('로컬 기록과 Argus 계정 기록을 지금 동기화합니다.'),
149
+ due_only: z.boolean().default(false).describe('확인일이 된 기록만 가져옵니다.'),
150
+ import_settlements: z.boolean().default(true).describe('웹에서 기록한 실제 결과를 로컬 원장에 반영합니다.'),
151
+ push_local: z.boolean().default(true).describe('계정에 닿지 못한 로컬 변경을 다시 보냅니다.'),
152
+ }),
153
+ ]);
154
+ const settingsPublicSchema = z.strictObject({
155
+ argus_dir: zArgusDir,
156
+ action: z.enum(['status', 'update', 'sync']).describe('설정을 확인하거나 수정하거나 계정과 동기화합니다.'),
157
+ locale: z.enum(['ko', 'en']).describe('사용자 표면 언어입니다.').optional(),
158
+ ambient_mute: z.boolean().describe('세션 중 확인일 알림 문장을 숨길지 정합니다.').optional(),
159
+ premise_sync: z.boolean().describe('추적 전제를 계정과 동기화할지 명시적으로 선택합니다.').optional(),
160
+ due_only: z.boolean().describe('동기화할 때 확인일이 된 기록만 가져옵니다.').optional(),
161
+ import_settlements: z.boolean().describe('웹에서 기록한 실제 결과를 로컬 원장에 반영합니다.').optional(),
162
+ push_local: z.boolean().describe('계정에 닿지 못한 로컬 변경을 다시 보냅니다.').optional(),
163
+ }).superRefine((value, ctx) => {
164
+ const parsed = settingsSchema.safeParse(value);
165
+ if (parsed.success)
166
+ return;
167
+ for (const issue of parsed.error.issues) {
168
+ ctx.addIssue({ code: 'custom', path: issue.path, message: issue.message });
169
+ }
170
+ });
171
+ const PUBLIC_NAME_MAP = {
172
+ argus_open_decision: 'argus_capture',
173
+ argus_premises: 'argus_capture',
174
+ argus_recheck: 'argus_capture',
175
+ argus_amend: 'argus_capture',
176
+ argus_dismiss: 'argus_capture',
177
+ argus_clarify_decision: 'argus_capture',
178
+ argus_seal: 'argus_predict',
179
+ argus_save_prediction: 'argus_predict',
180
+ argus_settle: 'argus_resolve',
181
+ argus_record_result: 'argus_resolve',
182
+ argus_recall: 'argus_patterns',
183
+ argus_history: 'argus_patterns',
184
+ argus_init: 'argus_settings',
185
+ argus_config: 'argus_settings',
186
+ argus_sync: 'argus_settings',
187
+ };
188
+ /** Rewrite only machine-owned routing fields. Free-form strings can contain
189
+ * verbatim user evidence, so a recursive text replacement would corrupt
190
+ * provenance whenever a user happened to mention an old tool name. */
191
+ export function publicCopy(value) {
192
+ if (typeof value === 'string') {
193
+ if (value === '기록하지 않았어요. 남기고 싶으면 argus_watch로 한 줄만 적어둘 수도 있어요.')
194
+ return '기록하지 않았어요.';
195
+ if (value === 'Not recorded. If you want, jot a one-line note with argus_watch instead.')
196
+ return 'Not recorded.';
197
+ return value;
198
+ }
199
+ if (Array.isArray(value))
200
+ return value.map(publicCopy);
201
+ if (!value || typeof value !== 'object')
202
+ return value;
203
+ return Object.fromEntries(Object.entries(value).map(([key, child]) => {
204
+ if (key === 'settle_path')
205
+ return ['result_path', publicCopy(child)];
206
+ if ((key === 'tool' || key === 'next_action') && typeof child === 'string') {
207
+ return [key, PUBLIC_NAME_MAP[child] ?? child];
208
+ }
209
+ if (key === 'next_actions' && Array.isArray(child)) {
210
+ return [key, child.map((item) => typeof item === 'string' ? (PUBLIC_NAME_MAP[item] ?? item) : item)];
211
+ }
212
+ return [key, publicCopy(child)];
213
+ }));
214
+ }
215
+ function rewriteResult(result, publicName) {
216
+ const sc = result.structuredContent;
217
+ if (!sc)
218
+ return result;
219
+ const next = Array.isArray(sc['next_actions'])
220
+ ? [...new Set(sc['next_actions'].map((value) => {
221
+ const name = String(value);
222
+ if (name === 'argus_watch')
223
+ return 'stop';
224
+ return PUBLIC_NAME_MAP[name] ?? name;
225
+ }))]
226
+ : undefined;
227
+ const rewritten = {
228
+ ...publicCopy(sc),
229
+ tool: publicName,
230
+ ...(next ? { next_actions: next } : {}),
231
+ };
232
+ result.structuredContent = rewritten;
233
+ result.content = [{ type: 'text', text: JSON.stringify(rewritten, null, 2) }];
234
+ return result;
235
+ }
236
+ async function ensureInitialized(args) {
237
+ const dir = resolveToolArgusDir(args['argus_dir']);
238
+ const hasConfig = fs.existsSync(configPath(dir));
239
+ const insideGit = gitCommonDirOf(dir) !== null;
240
+ let hasValidBinding = false;
241
+ if (insideGit) {
242
+ try {
243
+ const binding = JSON.parse(fs.readFileSync(path.join(dir, 'project.json'), 'utf8'));
244
+ hasValidBinding = typeof binding['repository_id'] === 'string' && typeof binding['workspace_id'] === 'string';
245
+ }
246
+ catch { /* missing or damaged projection: safe init repairs it */ }
247
+ }
248
+ if (hasConfig && (!insideGit || hasValidBinding))
249
+ return null;
250
+ const result = await init.handler({ argus_dir: dir });
251
+ if (result.isError)
252
+ return result;
253
+ const sample = ['decision', 'predicate', 'what_happened', 'finding', 'text', 'title']
254
+ .map((key) => args[key])
255
+ .filter((value) => typeof value === 'string' && value.trim().length > 0)
256
+ .join('\n');
257
+ const locale = detectLocaleFromText(sample);
258
+ if (locale) {
259
+ const configured = await config.handler({ argus_dir: dir, locale });
260
+ if (configured.isError)
261
+ return configured;
262
+ }
263
+ return null;
264
+ }
265
+ async function runPublic(publicName, args, handler) {
266
+ const initError = await ensureInitialized(args);
267
+ if (initError)
268
+ return rewriteResult(initError, publicName);
269
+ return rewriteResult(await handler(args), publicName);
270
+ }
271
+ export const decide = {
272
+ name: 'argus_capture',
273
+ description: 'Capture the reasoning behind a decision in the user\'s own words — the premises it rests on and the questions still open — without deciding for the user. Use action=open for a new decision; add_context, answer_question, keep_question_open, update_fact, change_prediction, or close for a decision already on record.',
274
+ inputSchema: decidePublicSchema,
275
+ outputSchema: ENVELOPE_OUTPUT_SCHEMA,
276
+ annotations: { title: 'Work with a decision', readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
277
+ handler: async (a) => {
278
+ const action = String(a['action']);
279
+ if (action === 'open') {
280
+ const result = await runPublic('argus_capture', a, openDecision.handler);
281
+ if (result.isError || !Array.isArray(a['premises']))
282
+ return result;
283
+ const sc = result.structuredContent;
284
+ if (sc?.['over_fire_gate'] && sc['over_fire_gate']['fired'] !== true)
285
+ return result;
286
+ const premiseResult = await premises.handler({
287
+ argus_dir: a['argus_dir'],
288
+ id: a['id'],
289
+ op: 'add',
290
+ premises: a['premises'],
291
+ today_override: a['today_override'],
292
+ });
293
+ if (premiseResult.isError)
294
+ return rewriteResult(premiseResult, 'argus_capture');
295
+ const premiseData = premiseResult.structuredContent?.['data'];
296
+ // The premise-add result is spliced in RAW here, so — unlike the runPublic
297
+ // path — it never passed through the public-name translation. Its surface
298
+ // says "argus_premises"; publicCopy the whole merged object so that internal
299
+ // name (and any other) is rewritten to the public one before it reaches a host.
300
+ const merged = publicCopy({
301
+ ...sc,
302
+ tool: 'argus_capture',
303
+ surface: `${String(sc?.['surface'] ?? '')} ${String(premiseResult.structuredContent?.['surface'] ?? '')}`.trim(),
304
+ data: { ...(sc?.['data'] ?? {}), premises: premiseData },
305
+ });
306
+ result.structuredContent = merged;
307
+ result.content = [{ type: 'text', text: JSON.stringify(merged, null, 2) }];
308
+ return result;
309
+ }
310
+ if (action === 'add_context')
311
+ return runPublic('argus_capture', { ...a, op: 'add' }, premises.handler);
312
+ if (action === 'answer_question')
313
+ return runPublic('argus_capture', { ...a, op: 'resolve' }, premises.handler);
314
+ if (action === 'keep_question_open') {
315
+ return runPublic('argus_capture', { ...a, op: 'still_open', reponder_cadence_days: a['reconsider_cadence_days'] }, premises.handler);
316
+ }
317
+ if (action === 'update_fact')
318
+ return runPublic('argus_capture', a, recheck.handler);
319
+ if (action === 'change_prediction')
320
+ return runPublic('argus_capture', a, amend.handler);
321
+ return runPublic('argus_capture', a, dismiss.handler);
322
+ },
323
+ };
324
+ export const history = {
325
+ name: 'argus_patterns',
326
+ 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.',
327
+ inputSchema: historySchema,
328
+ outputSchema: ENVELOPE_OUTPUT_SCHEMA,
329
+ annotations: { title: 'View judgment history', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
330
+ handler: (a) => {
331
+ const viewMap = {
332
+ active: 'bearing',
333
+ all: 'contracts',
334
+ decision_context: 'premises',
335
+ timeline: 'track_record',
336
+ };
337
+ return runPublic('argus_patterns', { ...a, view: viewMap[String(a['view'])] ?? a['view'] }, recall.handler);
338
+ },
339
+ };
340
+ export const settings = {
341
+ name: 'argus_settings',
342
+ description: 'Read or update the few settings a user may need: response language, quiet due reminders, opt-in premise sync, and an explicit account sync. Argus initializes itself on first use.',
343
+ inputSchema: settingsPublicSchema,
344
+ outputSchema: ENVELOPE_OUTPUT_SCHEMA,
345
+ annotations: { title: 'Argus settings', readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
346
+ handler: async (a) => {
347
+ const action = String(a['action']);
348
+ if (action === 'status') {
349
+ // Status is also the single public repair handle. init is idempotent and
350
+ // recreates a missing/corrupt v2 binding plus any pending v1 migration
351
+ // marker, without teaching users a separate initialization tool.
352
+ const repaired = await init.handler({ argus_dir: a['argus_dir'] });
353
+ if (repaired.isError)
354
+ return rewriteResult(repaired, 'argus_settings');
355
+ return runPublic('argus_settings', { argus_dir: a['argus_dir'] }, config.handler);
356
+ }
357
+ if (action === 'update') {
358
+ const args = Object.fromEntries(Object.entries(a).filter(([key]) => !['action'].includes(key)));
359
+ return runPublic('argus_settings', args, config.handler);
360
+ }
361
+ const args = Object.fromEntries(Object.entries(a).filter(([key]) => key !== 'action'));
362
+ return runPublic('argus_settings', args, sync.handler);
363
+ },
364
+ };
365
+ function withoutTestClock(schema) {
366
+ if (!(schema instanceof z.ZodObject))
367
+ return schema;
368
+ const { today_override: _hidden, ...publicShape } = schema.shape;
369
+ return z.strictObject(publicShape);
370
+ }
371
+ function publicWrapper(tool, name, description) {
372
+ return {
373
+ ...tool,
374
+ name,
375
+ description,
376
+ inputSchema: withoutTestClock(tool.inputSchema),
377
+ handler: (args) => runPublic(name, args, tool.handler),
378
+ };
379
+ }
380
+ export const publicSeal = publicWrapper(seal, 'argus_predict', 'Make a falsifiable prediction and the date when reality can answer it. Use the user\'s own wording whenever possible.');
381
+ export const publicCheckIn = publicWrapper(checkIn, 'argus_check_in', 'Show only decisions, facts, and open questions that need attention now. Read-only.');
382
+ export const publicSettle = publicWrapper(settle, 'argus_resolve', 'Record what actually happened after a prediction reaches its check date. Reality supplies the result; Argus does not grade it.');