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.
- package/README.md +44 -60
- package/SECURITY.md +3 -3
- package/dist/a0/account-connect.js +180 -0
- package/dist/a0/account-credentials.js +86 -0
- package/dist/index.js +35 -1
- package/dist/lib/ambient-elicit.js +225 -0
- package/dist/lib/argus-dir.js +26 -8
- package/dist/lib/calendar.js +2 -2
- package/dist/lib/due-note.js +3 -3
- package/dist/lib/elicit.js +13 -1
- package/dist/lib/ledger-replay.js +19 -3
- package/dist/lib/locale-mismatch.js +52 -0
- package/dist/lib/locale.js +81 -10
- package/dist/lib/localize-result.js +120 -0
- package/dist/lib/localized-message.js +13 -0
- package/dist/lib/premises.js +0 -2
- package/dist/lib/push-account.js +4 -3
- package/dist/lib/review/extract-core.js +116 -0
- package/dist/lib/review/extract-file-node.js +145 -29
- package/dist/lib/review/index.js +1 -1
- package/dist/lib/review/ingest.js +84 -20
- package/dist/lib/review/lenses.js +18 -0
- package/dist/lib/review/prompts.js +199 -22
- package/dist/lib/review/reviewability.js +8 -7
- package/dist/lib/review/routing.js +38 -17
- package/dist/lib/review-path.js +2 -2
- package/dist/lib/spine.js +13 -21
- package/dist/lib/state-machine.js +12 -12
- package/dist/lib/surfaces.js +90 -94
- package/dist/lib/tool-presentation.js +35 -0
- package/dist/resources.js +52 -14
- package/dist/server.js +85 -31
- package/dist/tools/candidates.js +3 -3
- package/dist/tools/check-in.js +24 -12
- package/dist/tools/errors.js +17 -3
- package/dist/tools/index.js +34 -2
- package/dist/tools/init-config.js +39 -11
- package/dist/tools/open-decision.js +58 -25
- package/dist/tools/premises.js +18 -18
- package/dist/tools/public-tools.js +382 -0
- package/dist/tools/recall.js +99 -24
- package/dist/tools/recheck.js +3 -3
- package/dist/tools/review.js +133 -29
- package/dist/tools/seal.js +62 -26
- package/dist/tools/semantic-record.js +225 -0
- package/dist/tools/settle.js +49 -8
- package/dist/tools/sync.js +2 -2
- package/dist/tools/tool-types.js +174 -0
- package/dist/tools/watch.js +1 -1
- package/dist/v2/bridge.js +2 -2
- package/dist/v2/brief.js +1 -1
- package/dist/v2/candidate-capture.js +150 -0
- package/dist/v2/capture-cli.js +72 -0
- package/dist/v2/capture-runtime.js +73 -0
- package/dist/v2/connection-io.js +47 -0
- package/dist/v2/connection.js +93 -0
- package/dist/v2/evidence.js +5 -1
- package/dist/v2/harvest.js +26 -51
- package/dist/v2/lifecycle-cli.js +50 -0
- package/dist/v2/lifecycle.js +298 -2
- package/dist/v2/logbook.js +14 -14
- package/dist/v2/mirror.js +2 -2
- package/dist/v2/queue.js +71 -12
- package/dist/v2/v1-reader.js +6 -3
- package/dist/v3/fixtures/dkk-corpus.js +55 -0
- package/dist/v3/fixtures/p5-measurement-plan.js +42 -0
- package/dist/v3/index.js +5 -0
- package/dist/v3/legacy-v2.js +169 -0
- package/dist/v3/p5-gate.js +123 -0
- package/dist/v3/reducer.js +290 -0
- package/dist/v3/store.js +133 -0
- package/dist/v3/types.js +193 -0
- package/dist/v4/index.js +5 -0
- package/dist/v4/reducer.js +361 -0
- package/dist/v4/relation-validation.js +40 -0
- package/dist/v4/shadow.js +22 -0
- package/dist/v4/types.js +326 -0
- package/dist/v4/watch.js +18 -0
- package/package.json +6 -3
- package/dist/lib/discipline.js +0 -42
- package/dist/prompts.js +0 -72
- package/snippets/claude-code-watch.md +0 -47
package/dist/tools/premises.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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}건은 나중에 실제와 다시 대조해 확인합니다 (
|
|
273
|
-
: (sealedNow ? ` ${monitoredCount} will be re-checked against what actually happens (
|
|
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
|
|
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
|
|
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: ['
|
|
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
|
|
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: ['
|
|
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
|
|
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: '
|
|
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
|
|
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: ['
|
|
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
|
|
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
|
|
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
|
|
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: ['
|
|
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.');
|