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/server.js
CHANGED
|
@@ -1,22 +1,26 @@
|
|
|
1
1
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
2
|
-
import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ListResourceTemplatesRequestSchema, ReadResourceRequestSchema,
|
|
3
|
-
import {
|
|
4
|
-
import { toolJsonSchema } from './tools/tool-types.js';
|
|
2
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ListResourceTemplatesRequestSchema, ReadResourceRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
3
|
+
import { PUBLIC_TOOLS, TOOL_MAP, servedPublicTools } from './tools/index.js';
|
|
5
4
|
import { listResources, listResourceTemplates, readResource } from './resources.js';
|
|
6
|
-
import { listPrompts, getPrompt } from './prompts.js';
|
|
7
5
|
import { SERVER_INSTRUCTIONS } from './lib/spine.js';
|
|
8
6
|
import { setElicitor } from './lib/elicit.js';
|
|
7
|
+
import { initAmbientElicit, armAmbientElicit } from './lib/ambient-elicit.js';
|
|
8
|
+
import { settle } from './tools/settle.js';
|
|
9
9
|
import { appendDueNote } from './lib/due-note.js';
|
|
10
10
|
import { logError } from './lib/log.js';
|
|
11
11
|
import { packageMeta } from './lib/package-meta.js';
|
|
12
|
+
import { localizeToolResult } from './lib/localize-result.js';
|
|
13
|
+
import { learnLocaleFromContent } from './lib/locale.js';
|
|
14
|
+
import { appendLocaleMismatchNote } from './lib/locale-mismatch.js';
|
|
15
|
+
import { resolveToolArgusDir } from './lib/argus-dir.js';
|
|
12
16
|
import { recordServerStart, recordToolCall } from './lib/telemetry.js';
|
|
13
17
|
/**
|
|
14
18
|
* Argus MCP server (blueprint §4). v1 surface = Tools only — the universal
|
|
15
19
|
* floor that works on every host. The spine bias is carried once by the
|
|
16
20
|
* `instructions` field (the one spec-sanctioned home for the killed
|
|
17
|
-
* paste-prompt), rendered from the single spine source. Resources
|
|
18
|
-
*
|
|
19
|
-
*
|
|
21
|
+
* paste-prompt), rendered from the single spine source. Resources provide one
|
|
22
|
+
* passive attention view. Separate MCP prompts were removed: they duplicated
|
|
23
|
+
* the tool surface and made users learn a second invocation system.
|
|
20
24
|
*/
|
|
21
25
|
export async function createServer() {
|
|
22
26
|
const meta = packageMeta();
|
|
@@ -25,33 +29,25 @@ export async function createServer() {
|
|
|
25
29
|
// a host never probes a no-op (addendum J). `elicitation` is a client
|
|
26
30
|
// capability we USE, not a server one we serve — advertised so the SDK
|
|
27
31
|
// permits elicitInput; tools degrade to text when the host lacks it.
|
|
28
|
-
capabilities: { tools: {}, resources: {}
|
|
32
|
+
capabilities: { tools: {}, resources: {} },
|
|
29
33
|
instructions: SERVER_INSTRUCTIONS,
|
|
30
34
|
});
|
|
31
|
-
|
|
35
|
+
const ec = server;
|
|
36
|
+
// The capability probe reads the client's DECLARED elicitation support (set at
|
|
37
|
+
// initialize). Gating canElicit() on it means a host that never declared the
|
|
38
|
+
// capability takes the text path instead of calling elicitInput (which the SDK
|
|
39
|
+
// throws on) and silently dropping a confirm_draft seal.
|
|
40
|
+
setElicitor((message, requestedSchema) => ec.elicitInput({ message, requestedSchema }), () => Boolean(ec.getClientCapabilities?.()?.elicitation));
|
|
32
41
|
// Resources — read-only context (blueprint §4.3).
|
|
33
42
|
server.setRequestHandler(ListResourcesRequestSchema, async () => listResources());
|
|
34
43
|
server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => listResourceTemplates());
|
|
35
44
|
server.setRequestHandler(ReadResourceRequestSchema, async (req) => readResource(req.params.uri));
|
|
36
|
-
// Prompts — user-triggered discipline rituals (blueprint §4.2).
|
|
37
|
-
server.setRequestHandler(ListPromptsRequestSchema, async () => listPrompts());
|
|
38
|
-
server.setRequestHandler(GetPromptRequestSchema, async (req) => getPrompt(req.params.name, req.params.arguments));
|
|
39
45
|
// Anonymous, opt-in activation signal (no-op unless ARGUS_TELEMETRY=1). Fire-
|
|
40
46
|
// and-forget: never blocks server startup, never throws. See lib/telemetry.ts.
|
|
41
47
|
recordServerStart();
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
// Top-level human-readable title (2025-06-18 spec; display priority
|
|
46
|
-
// title > annotations.title > name). Reuse the annotation we already set.
|
|
47
|
-
...(t.annotations?.title ? { title: t.annotations.title } : {}),
|
|
48
|
-
description: t.description,
|
|
49
|
-
// JSON Schema generated from the Zod source of truth (no hand-kept copy).
|
|
50
|
-
inputSchema: toolJsonSchema(t.inputSchema),
|
|
51
|
-
...(t.outputSchema ? { outputSchema: t.outputSchema } : {}),
|
|
52
|
-
...(t.annotations ? { annotations: t.annotations } : {}),
|
|
53
|
-
})),
|
|
54
|
-
}));
|
|
48
|
+
// Single source (tools/index.ts): builds the descriptors AND runs schemas
|
|
49
|
+
// through publicCopy so a legacy tool name in a field description can't leak.
|
|
50
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: servedPublicTools() }));
|
|
55
51
|
// Serialize tool calls so concurrent invocations can't interleave a
|
|
56
52
|
// read-replay-then-append against the same ledger (real hosts already wait
|
|
57
53
|
// for each response; this removes the foot-gun for batched/parallel clients).
|
|
@@ -61,6 +57,12 @@ export async function createServer() {
|
|
|
61
57
|
chain = run.then(() => undefined, () => undefined);
|
|
62
58
|
return run;
|
|
63
59
|
};
|
|
60
|
+
// Out-of-band ambient ask (lib/ambient-elicit.ts) — after a tool call ends
|
|
61
|
+
// and the session goes quiet, the server may ask the ONE due settlement
|
|
62
|
+
// question directly (spike-proven server→client elicitation). Recording rides
|
|
63
|
+
// the real settle handler through the SAME serialize chain, so an ambient
|
|
64
|
+
// write can never interleave with an in-band tool call.
|
|
65
|
+
initAmbientElicit({ settleHandler: (a) => settle.handler(a), serialize });
|
|
64
66
|
// The low-level SDK handler's expected return is a broad ServerResult union
|
|
65
67
|
// (incl. a task-augmented variant) our envelope type isn't nominally part of —
|
|
66
68
|
// `any` is the sanctioned boundary here; every return below is a real
|
|
@@ -69,7 +71,11 @@ export async function createServer() {
|
|
|
69
71
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
70
72
|
const { name, arguments: args } = request.params;
|
|
71
73
|
const tool = TOOL_MAP.get(name);
|
|
72
|
-
|
|
74
|
+
// The v6 semantic recorder is a P4 pilot. Keeping it out of discovery is
|
|
75
|
+
// not enough: a cached or hand-written client must not be able to invoke it
|
|
76
|
+
// until the operator explicitly opts in.
|
|
77
|
+
const pilotDisabled = name === 'argus_record' && process.env['ARGUS_DKK_V6_PILOT'] !== '1';
|
|
78
|
+
if (!tool || pilotDisabled) {
|
|
73
79
|
return {
|
|
74
80
|
content: [{ type: 'text', text: JSON.stringify({ ok: false, error_code: 'UNKNOWN_TOOL', message: `Unknown tool: ${name}` }) }],
|
|
75
81
|
isError: true,
|
|
@@ -79,28 +85,76 @@ export async function createServer() {
|
|
|
79
85
|
// A schema failure is a client bug → a clean, actionable tool-result error
|
|
80
86
|
// (not a protocol crash); the handler only ever sees validated, default-
|
|
81
87
|
// applied args.
|
|
82
|
-
const
|
|
88
|
+
const rawArgs = (args ?? {});
|
|
89
|
+
// Deterministic protocol tests need a logical clock, but that test-only
|
|
90
|
+
// control must not become part of the public MCP schema users and models
|
|
91
|
+
// have to understand.
|
|
92
|
+
const hiddenTestClock = process.env['NODE_ENV'] === 'test'
|
|
93
|
+
&& PUBLIC_TOOLS.some((candidate) => candidate.name === name)
|
|
94
|
+
&& typeof rawArgs['today_override'] === 'string';
|
|
95
|
+
const validationArgs = hiddenTestClock
|
|
96
|
+
? Object.fromEntries(Object.entries(rawArgs).filter(([key]) => key !== 'today_override'))
|
|
97
|
+
: rawArgs;
|
|
98
|
+
const parsed = tool.inputSchema.safeParse(validationArgs);
|
|
83
99
|
if (!parsed.success) {
|
|
84
100
|
const issues = parsed.error.issues.map((i) => `${i.path.join('.') || '(root)'}: ${i.message}`).join('; ');
|
|
101
|
+
// Carry the offending field(s) + machine-readable reason STRUCTURALLY, so
|
|
102
|
+
// the Korean localizer can name what to fix (not collapse every failure to
|
|
103
|
+
// a generic "invalid input"). Without this, a Korean user is told to "fix
|
|
104
|
+
// the flagged argument" but nothing is flagged — an unactionable dead end.
|
|
105
|
+
const invalidFields = parsed.error.issues.map((i) => {
|
|
106
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
107
|
+
const raw = i;
|
|
108
|
+
return {
|
|
109
|
+
field: i.path.join('.') || '(root)',
|
|
110
|
+
code: i.code,
|
|
111
|
+
message: i.message,
|
|
112
|
+
...(typeof raw.minimum === 'number' ? { minimum: raw.minimum } : {}),
|
|
113
|
+
...(typeof raw.maximum === 'number' ? { maximum: raw.maximum } : {}),
|
|
114
|
+
...(raw.expected !== undefined ? { expected: String(raw.expected) } : {}),
|
|
115
|
+
// Zod v4 tags size issues with `origin` ('string' | 'number' | 'array'…).
|
|
116
|
+
...(raw.origin !== undefined ? { origin: String(raw.origin) } : raw.type !== undefined ? { origin: String(raw.type) } : {}),
|
|
117
|
+
};
|
|
118
|
+
});
|
|
85
119
|
const error = {
|
|
86
120
|
ok: false,
|
|
87
121
|
tool: name,
|
|
88
122
|
error_code: 'INVALID_INPUT',
|
|
89
123
|
message: `Invalid arguments. ${issues}`,
|
|
124
|
+
invalid_fields: invalidFields,
|
|
90
125
|
recovery: 'Fix the named argument(s) and call the same tool again. Do not infer missing user-owned fields.',
|
|
91
126
|
};
|
|
92
|
-
return {
|
|
127
|
+
return localizeToolResult((args ?? {}), {
|
|
93
128
|
content: [{ type: 'text', text: JSON.stringify(error) }],
|
|
94
129
|
structuredContent: error,
|
|
95
130
|
isError: true,
|
|
96
|
-
};
|
|
131
|
+
});
|
|
97
132
|
}
|
|
98
133
|
try {
|
|
99
|
-
const
|
|
134
|
+
const callArgs = hiddenTestClock
|
|
135
|
+
? { ...parsed.data, today_override: rawArgs['today_override'] }
|
|
136
|
+
: parsed.data;
|
|
137
|
+
const raw = await serialize(() => tool.handler(callArgs));
|
|
138
|
+
// Learn the session's language from the user's OWN words (never env), so
|
|
139
|
+
// every later surface — including contentless ones (errors, recall) — stays
|
|
140
|
+
// in that language start to finish. Runs after the handler (auto-init has
|
|
141
|
+
// created config by now) and before localize, so even this call's result
|
|
142
|
+
// is localized to the just-learned locale.
|
|
143
|
+
const dirForLocale = resolveToolArgusDir(callArgs['argus_dir']);
|
|
144
|
+
learnLocaleFromContent(dirForLocale, callArgs);
|
|
145
|
+
// §9.7 O1: if an EXPLICIT pin contradicts the language the user is
|
|
146
|
+
// actually speaking, say so once (fact + argus_settings handle) — a pin
|
|
147
|
+
// is never silently overridden, but it must not be silently obeyed
|
|
148
|
+
// against the user's own words forever either.
|
|
149
|
+
const result = appendLocaleMismatchNote(dirForLocale, callArgs, localizeToolResult(callArgs, raw));
|
|
100
150
|
// Opt-in usage signal: which tool ran + that it didn't crash. Carries no
|
|
101
151
|
// arguments — never the decision content. Fire-and-forget (see telemetry.ts).
|
|
102
152
|
recordToolCall(name, true);
|
|
103
|
-
|
|
153
|
+
// Debounced out-of-band ask arms on every call and fires only after the
|
|
154
|
+
// session goes quiet; a check_in call spends the budget instead (the user
|
|
155
|
+
// just saw their dues). Never throws, never taxes this call.
|
|
156
|
+
armAmbientElicit(name, callArgs);
|
|
157
|
+
return appendDueNote(name, callArgs, result);
|
|
104
158
|
}
|
|
105
159
|
catch (e) {
|
|
106
160
|
recordToolCall(name, false);
|
package/dist/tools/candidates.js
CHANGED
|
@@ -48,7 +48,7 @@ export const candidates = {
|
|
|
48
48
|
today_override: zDate.optional(),
|
|
49
49
|
})
|
|
50
50
|
.refine((a) => a.action === 'list' || a.candidate_id !== undefined, { message: 'candidate_id is required for promote/drop/snooze' })
|
|
51
|
-
.refine((a) => a.action !== 'promote' || a.decision_id !== undefined, { message: 'promote links a candidate to an existing
|
|
51
|
+
.refine((a) => a.action !== 'promote' || a.decision_id !== undefined, { message: 'promote links a candidate to an existing saved prediction — pass decision_id (save it first with argus_predict)' })
|
|
52
52
|
.refine((a) => a.action !== 'snooze' || a.snooze_until !== undefined, { message: 'snooze requires snooze_until (YYYY-MM-DD)' }),
|
|
53
53
|
outputSchema: ENVELOPE_OUTPUT_SCHEMA,
|
|
54
54
|
annotations: { title: 'Captured decision candidates', readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
|
|
@@ -74,7 +74,7 @@ export const candidates = {
|
|
|
74
74
|
: [T.header(brief.candidates_active.length, brief.candidates_expired), ...rows, T.quote_note].join('\n');
|
|
75
75
|
return envelope({
|
|
76
76
|
ok: true, tool: 'argus_candidates', surface,
|
|
77
|
-
next_actions: rows.length === 0 ? ['stop'] : ['
|
|
77
|
+
next_actions: rows.length === 0 ? ['stop'] : ['argus_predict', 'stop'],
|
|
78
78
|
data: {
|
|
79
79
|
candidates: brief.candidates_active.map((c) => {
|
|
80
80
|
const rec = state.candidates.get(c.candidate_id);
|
|
@@ -99,7 +99,7 @@ export const candidates = {
|
|
|
99
99
|
return envelope({
|
|
100
100
|
ok: true, tool: 'argus_candidates',
|
|
101
101
|
surface: T.promoted(candidateId, String(a['decision_id'])),
|
|
102
|
-
next_actions: ['
|
|
102
|
+
next_actions: ['argus_predict'],
|
|
103
103
|
data: { candidate_id: candidateId, action, decision_id: a['decision_id'], today },
|
|
104
104
|
});
|
|
105
105
|
}
|
package/dist/tools/check-in.js
CHANGED
|
@@ -9,6 +9,7 @@ import { envelope } from '../lib/envelope.js';
|
|
|
9
9
|
import { ENVELOPE_OUTPUT_SCHEMA, zArgusDir, zDate } from './tool-types.js';
|
|
10
10
|
import { handleToolException } from './errors.js';
|
|
11
11
|
import { briefDivergence, readV2Brief } from '../v2/mirror.js';
|
|
12
|
+
import { drainCaptureOnCheckIn } from '../v2/capture-runtime.js';
|
|
12
13
|
/**
|
|
13
14
|
* The watch anchor is the one surface a user cannot close: a contract has
|
|
14
15
|
* settle/dismiss and an open_question has still_open, but an anchor has no ack
|
|
@@ -41,6 +42,7 @@ export const checkIn = {
|
|
|
41
42
|
try {
|
|
42
43
|
const dir = resolveToolArgusDir(a['argus_dir']);
|
|
43
44
|
const today = resolveToday({ override: a['today_override'] });
|
|
45
|
+
const captureStatus = await drainCaptureOnCheckIn(dir, today);
|
|
44
46
|
const ledger = replayLedger(dir, today);
|
|
45
47
|
const seeds = bearingContracts(dir, today, ledger);
|
|
46
48
|
const dueMap = new Map();
|
|
@@ -98,13 +100,23 @@ export const checkIn = {
|
|
|
98
100
|
// a due question came back framed in English around the user's own words.
|
|
99
101
|
const premiseGroups = groupDuePremises(duePremises(ledger));
|
|
100
102
|
const openQs = dueOpenQuestions(ledger);
|
|
101
|
-
//
|
|
102
|
-
//
|
|
103
|
-
//
|
|
104
|
-
//
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
103
|
+
// Sniff the user's LOCALE from the ledger's own words (never env/Intl for
|
|
104
|
+
// Korean users on an English OS). The old priority-OR chain silently fell
|
|
105
|
+
// through to `undefined` — English — whenever every earlier slot was empty
|
|
106
|
+
// AND the last resort read a field that doesn't exist on ContractEntry
|
|
107
|
+
// (`.predicate`; the entry stores `.text`). A Korean session whose only due
|
|
108
|
+
// item was an open question came back framed in English around its own
|
|
109
|
+
// Korean quote. Fix: pool ALL available ledger user-text defensively so any
|
|
110
|
+
// single Korean line is enough to detect. (Found via the check_in
|
|
111
|
+
// localization yellow, 2026-07-14.)
|
|
112
|
+
const ledgerVoiceSample = [
|
|
113
|
+
lastAnchor?.text,
|
|
114
|
+
...ledger.overdue.map((o) => o.text),
|
|
115
|
+
...due.map((d) => d.predicate),
|
|
116
|
+
...premiseGroups.flatMap((g) => g.premises.map((p) => p.decision_text)),
|
|
117
|
+
...openQs.map((q) => q.text),
|
|
118
|
+
...[...ledger.contracts.values()].map((c) => c.text),
|
|
119
|
+
].filter((t) => typeof t === 'string' && t.trim().length > 0).join(' ') || undefined;
|
|
108
120
|
const S = ledgerVoiceSample
|
|
109
121
|
? SURFACES[resolveResponseLocale(dir, ledgerVoiceSample)].checkin
|
|
110
122
|
: SURFACES[surfaceLocale(dir)].checkin;
|
|
@@ -227,7 +239,7 @@ export const checkIn = {
|
|
|
227
239
|
ok: true, tool: 'argus_check_in',
|
|
228
240
|
surface: mirrorLine + S.nothing_due + accountHint + upcomingLine + fleetLine + integrityLine,
|
|
229
241
|
next_actions: ['stop'],
|
|
230
|
-
data: { due: [], due_count: 0, due_premises: [], due_premise_count: 0, due_open_questions: [], due_open_question_count: 0, ...(upDays > 0 ? { upcoming } : {}), ...(a['fleet'] === true ? { fleet: fleetRows } : {}), ...watchData, today, v2_brief: readV2Brief(dir, today), v2_divergence: briefDivergence([], readV2Brief(dir, today)) },
|
|
242
|
+
data: { due: [], due_count: 0, due_premises: [], due_premise_count: 0, due_open_questions: [], due_open_question_count: 0, ...(upDays > 0 ? { upcoming } : {}), ...(a['fleet'] === true ? { fleet: fleetRows } : {}), ...watchData, today, capture_status: captureStatus, v2_brief: readV2Brief(dir, today), v2_divergence: briefDivergence([], readV2Brief(dir, today)) },
|
|
231
243
|
});
|
|
232
244
|
}
|
|
233
245
|
const parts = [];
|
|
@@ -252,10 +264,10 @@ export const checkIn = {
|
|
|
252
264
|
// Route to the tool that acts on whatever is due: settle a contract first,
|
|
253
265
|
// else reconsider/recall. argus_premises closes or defers an open question.
|
|
254
266
|
const next = due.length > 0
|
|
255
|
-
? ['
|
|
267
|
+
? ['argus_resolve']
|
|
256
268
|
: openQs.length > 0
|
|
257
|
-
? ['
|
|
258
|
-
: ['
|
|
269
|
+
? ['argus_capture', 'argus_patterns']
|
|
270
|
+
: ['argus_patterns'];
|
|
259
271
|
return envelope({
|
|
260
272
|
ok: true, tool: 'argus_check_in',
|
|
261
273
|
surface: mirrorLine + parts.join(' ') + upcomingLine + fleetLine + integrityLine,
|
|
@@ -270,7 +282,7 @@ export const checkIn = {
|
|
|
270
282
|
...(upDays > 0 ? { upcoming } : {}),
|
|
271
283
|
...(a['fleet'] === true ? { fleet: fleetRows } : {}),
|
|
272
284
|
...watchData,
|
|
273
|
-
today, integrity: ledger.integrity,
|
|
285
|
+
today, integrity: ledger.integrity, capture_status: captureStatus,
|
|
274
286
|
// v2 병기 (P2-3): v1이 여전히 정본이고 surface는 무접촉 — 관찰용.
|
|
275
287
|
// P2 읽기 전환 전에 두 원장의 답이 갈리는지 실사용에서 드러낸다.
|
|
276
288
|
v2_brief: readV2Brief(dir, today),
|
package/dist/tools/errors.js
CHANGED
|
@@ -3,6 +3,7 @@ import { ArgusDirError } from '../lib/argus-dir.js';
|
|
|
3
3
|
import { PathSafetyError } from '../lib/safe-path.js';
|
|
4
4
|
import { GuardError } from '../lib/state-machine.js';
|
|
5
5
|
import { logError } from '../lib/log.js';
|
|
6
|
+
import { localizedErrorCopy } from '../lib/localized-message.js';
|
|
6
7
|
/**
|
|
7
8
|
* Map a thrown exception to a spine-safe tool error envelope. Known typed
|
|
8
9
|
* errors carry their own code + recovery hint; anything else is an internal
|
|
@@ -10,14 +11,27 @@ import { logError } from '../lib/log.js';
|
|
|
10
11
|
*/
|
|
11
12
|
export function handleToolException(tool, e) {
|
|
12
13
|
if (e instanceof ArgusDirError) {
|
|
13
|
-
|
|
14
|
+
const copy = localizedErrorCopy(null, undefined, {
|
|
15
|
+
en: { message: e.message, recovery: 'Pass an absolute .argus path with no "..".' },
|
|
16
|
+
ko: { message: 'Argus 기록 경로가 올바르지 않습니다.', recovery: '".."이 없는 절대 .argus 경로를 전달하세요.' },
|
|
17
|
+
});
|
|
18
|
+
return toolError({ ok: false, tool, error_code: e.code, ...copy });
|
|
14
19
|
}
|
|
15
20
|
if (e instanceof PathSafetyError) {
|
|
16
|
-
|
|
21
|
+
const copy = localizedErrorCopy(null, undefined, {
|
|
22
|
+
en: { message: e.message, recovery: 'Use ids/labels matching [A-Za-z0-9._-] only.' },
|
|
23
|
+
ko: { message: '안전하지 않은 id 또는 label입니다.', recovery: 'id와 label에는 [A-Za-z0-9._-] 문자만 사용하세요.' },
|
|
24
|
+
});
|
|
25
|
+
return toolError({ ok: false, tool, error_code: e.code, ...copy });
|
|
17
26
|
}
|
|
18
27
|
if (e instanceof GuardError) {
|
|
19
28
|
return toolError({ ok: false, tool, error_code: e.code, message: e.message, recovery: e.recovery });
|
|
20
29
|
}
|
|
21
30
|
logError(`[${tool}] unhandled`, e);
|
|
22
|
-
|
|
31
|
+
const detail = String(e instanceof Error ? e.message : e);
|
|
32
|
+
const copy = localizedErrorCopy(null, undefined, {
|
|
33
|
+
en: { message: `Internal error: ${detail}`, recovery: 'Try the same operation again. If it repeats, inspect the server log.' },
|
|
34
|
+
ko: { message: `내부 오류가 발생했습니다: ${detail}`, recovery: '같은 작업을 다시 시도하세요. 반복되면 서버 로그를 확인하세요.' },
|
|
35
|
+
});
|
|
36
|
+
return toolError({ ok: false, tool, error_code: 'INTERNAL_ERROR', ...copy });
|
|
23
37
|
}
|
package/dist/tools/index.js
CHANGED
|
@@ -11,6 +11,38 @@ import { premises } from './premises.js';
|
|
|
11
11
|
import { recheck } from './recheck.js';
|
|
12
12
|
import { watch } from './watch.js';
|
|
13
13
|
import { candidates } from './candidates.js';
|
|
14
|
+
import { decide, history, settings, publicSeal, publicCheckIn, publicSettle, publicCopy } from './public-tools.js';
|
|
15
|
+
import { toolJsonSchema } from './tool-types.js';
|
|
16
|
+
import { bilingualToolPresentation } from '../lib/tool-presentation.js';
|
|
17
|
+
import { semanticRecord } from './semantic-record.js';
|
|
14
18
|
/** The full registered tool set. There is deliberately no verdict/grade/score tool. */
|
|
15
|
-
export const TOOLS = [openDecision, review, premises, seal, recheck, settle, checkIn, recall, sync, amend, dismiss, candidates, watch, init, config];
|
|
16
|
-
|
|
19
|
+
export const TOOLS = [openDecision, review, premises, seal, recheck, settle, checkIn, recall, sync, amend, dismiss, candidates, watch, init, config, semanticRecord];
|
|
20
|
+
/** The small, purpose-led surface returned by tools/list. Legacy tools stay in
|
|
21
|
+
* TOOL_MAP for cached clients and one-version compatibility, but new users and
|
|
22
|
+
* models no longer have to choose among internal state-machine parts. */
|
|
23
|
+
export const PUBLIC_TOOLS = [decide, publicSeal, publicCheckIn, publicSettle, history, settings];
|
|
24
|
+
/** The semantic vertical slice is deliberately opt-in until the P5 value gate. */
|
|
25
|
+
export const V3_PILOT_TOOLS = [semanticRecord];
|
|
26
|
+
export const TOOL_MAP = new Map([...TOOLS, ...PUBLIC_TOOLS].map((t) => [t.name, t]));
|
|
27
|
+
/**
|
|
28
|
+
* The exact tool descriptors returned by tools/list — single source shared by
|
|
29
|
+
* the server and the host-surface guard test. inputSchema runs through
|
|
30
|
+
* publicCopy so that a legacy tool name embedded in a Zod field description
|
|
31
|
+
* (e.g. seal's "no prior argus_open_decision is needed") is rewritten to the
|
|
32
|
+
* public name before it reaches a host. Without this, the tool-call RESULTS are
|
|
33
|
+
* translated but the tool SCHEMAS still leaked the old vocabulary.
|
|
34
|
+
*/
|
|
35
|
+
export function servedPublicTools() {
|
|
36
|
+
const served = process.env['ARGUS_DKK_V6_PILOT'] === '1' ? [...PUBLIC_TOOLS, ...V3_PILOT_TOOLS] : PUBLIC_TOOLS;
|
|
37
|
+
return served.map((t) => {
|
|
38
|
+
const presentation = bilingualToolPresentation(t.name, t.annotations?.title, t.description);
|
|
39
|
+
return {
|
|
40
|
+
name: t.name,
|
|
41
|
+
title: presentation.title,
|
|
42
|
+
description: publicCopy(presentation.description),
|
|
43
|
+
inputSchema: publicCopy(toolJsonSchema(t.inputSchema)),
|
|
44
|
+
...(t.outputSchema ? { outputSchema: t.outputSchema } : {}),
|
|
45
|
+
...(t.annotations ? { annotations: t.annotations } : {}),
|
|
46
|
+
};
|
|
47
|
+
});
|
|
48
|
+
}
|
|
@@ -14,6 +14,7 @@ import path from 'path';
|
|
|
14
14
|
import { envelope, toolError } from '../lib/envelope.js';
|
|
15
15
|
import { ENVELOPE_OUTPUT_SCHEMA, zArgusDir } from './tool-types.js';
|
|
16
16
|
import { handleToolException } from './errors.js';
|
|
17
|
+
import { localizedMessage } from '../lib/localized-message.js';
|
|
17
18
|
import { gitCommonDirOf } from '../v2/git-discovery.js';
|
|
18
19
|
import { initV2 } from '../v2/init.js';
|
|
19
20
|
import { argusHome, ledgerPath } from '../v2/ledger.js';
|
|
@@ -39,7 +40,19 @@ export const init = {
|
|
|
39
40
|
await ensurePrivacyGitignore(dir);
|
|
40
41
|
writeBoundMarker(dir);
|
|
41
42
|
if (!fsSync.existsSync(configPath(dir))) {
|
|
42
|
-
|
|
43
|
+
// Seed a locale ONLY on a positive Korean env signal (KST machine → ko
|
|
44
|
+
// once, so the very first contentless surface is Korean). On the default
|
|
45
|
+
// English/Intl fallback, OMIT locale so runtime content-detection stays
|
|
46
|
+
// live — otherwise a Korean user on an English-locale OS gets locale:en
|
|
47
|
+
// pinned on first use, and config-wins means their Korean text can NEVER
|
|
48
|
+
// reclaim a Korean surface. (Found via the check_in localization yellow,
|
|
49
|
+
// 2026-07-14: an all-Korean session came back in English forever.)
|
|
50
|
+
const seededLocale = detectLocale(dir);
|
|
51
|
+
const cfg = {
|
|
52
|
+
schema_version: SCHEMA_VERSION,
|
|
53
|
+
...(seededLocale === 'ko' ? { locale: 'ko' } : {}),
|
|
54
|
+
boss: null, team: null, archive: null,
|
|
55
|
+
};
|
|
43
56
|
await atomicWriteText(configPath(dir), yaml.dump(cfg));
|
|
44
57
|
}
|
|
45
58
|
// ── v2 바인딩 (P1 수술 1단계 — 파괴 없는 추가) ──
|
|
@@ -78,18 +91,18 @@ export const init = {
|
|
|
78
91
|
// "today is yesterday" immediately instead of at the first missed check-in.
|
|
79
92
|
// Default is system-local; ARGUS_TZ is the explicit override.
|
|
80
93
|
const tz = process.env['ARGUS_TZ'] || `${resolveDefaultTimeZone()} (system local; set ARGUS_TZ to override)`;
|
|
94
|
+
const locale = readConfig(dir)?.locale ?? detectLocale(dir);
|
|
81
95
|
return envelope({
|
|
82
96
|
ok: true, tool: 'argus_init',
|
|
83
97
|
surface: empty
|
|
84
|
-
?
|
|
85
|
-
|
|
86
|
-
|
|
98
|
+
? localizedMessage(dir, undefined, {
|
|
99
|
+
en: 'Argus is ready. Describe a decision you are weighing. It can clarify the decision, save a prediction with a check date, and later record what actually happened. No grades, just your own track record over time.',
|
|
100
|
+
ko: 'Argus가 준비됐습니다. 고민 중인 결정을 말해 주세요. 결정을 명료하게 정리하고 확인일이 있는 예측을 저장한 뒤, 그날 실제로 일어난 일을 기록할 수 있습니다. 점수나 평결 없이 당신의 판단 기록만 남습니다.',
|
|
101
|
+
})
|
|
102
|
+
: localizedMessage(dir, undefined, { en: 'Argus is ready.', ko: 'Argus가 준비됐습니다.' }),
|
|
103
|
+
next_actions: empty ? ['argus_capture'] : ['argus_check_in'],
|
|
87
104
|
data: {
|
|
88
|
-
initialized: true, argus_dir: dir, today, tz, v2,
|
|
89
|
-
// §9.3 — one quiet pointer, data-only, never a push: the daily-watch
|
|
90
|
-
// host snippets (CLAUDE.md block + SessionStart hook) ship in the
|
|
91
|
-
// package for users who want the watch rhythm carried by their host.
|
|
92
|
-
watch_snippets: 'optional — see snippets/claude-code-watch.md in the argus-decision-mcp package',
|
|
105
|
+
initialized: true, argus_dir: dir, today, tz, locale, v2,
|
|
93
106
|
},
|
|
94
107
|
});
|
|
95
108
|
}
|
|
@@ -118,7 +131,13 @@ export const config = {
|
|
|
118
131
|
const writeKeys = ['locale', 'boss', 'team', 'archive', 'ambient_mute', 'premise_sync'].filter((k) => k in a);
|
|
119
132
|
const existing = readConfig(dir) ?? { schema_version: SCHEMA_VERSION, locale: detectLocale(dir), boss: null, team: null, archive: null };
|
|
120
133
|
if (writeKeys.length === 0) {
|
|
121
|
-
return envelope({
|
|
134
|
+
return envelope({
|
|
135
|
+
ok: true,
|
|
136
|
+
tool: 'argus_config',
|
|
137
|
+
surface: localizedMessage(dir, undefined, { en: 'Config read.', ko: '설정을 읽었습니다.' }),
|
|
138
|
+
next_actions: ['stop'],
|
|
139
|
+
data: { config: existing, existed: !!readConfig(dir) },
|
|
140
|
+
});
|
|
122
141
|
}
|
|
123
142
|
if ('locale' in a && a['locale'] !== 'ko' && a['locale'] !== 'en') {
|
|
124
143
|
return toolError({ ok: false, tool: 'argus_config', error_code: 'INVALID_LOCALE', message: 'locale must be "ko" or "en".' });
|
|
@@ -134,7 +153,16 @@ export const config = {
|
|
|
134
153
|
...(('premise_sync' in a) ? { premise_sync: a['premise_sync'] } : {}),
|
|
135
154
|
};
|
|
136
155
|
await atomicWriteText(configPath(dir), yaml.dump(merged));
|
|
137
|
-
return envelope({
|
|
156
|
+
return envelope({
|
|
157
|
+
ok: true,
|
|
158
|
+
tool: 'argus_config',
|
|
159
|
+
surface: localizedMessage(dir, undefined, {
|
|
160
|
+
en: `Config updated: ${writeKeys.join(', ')}.`,
|
|
161
|
+
ko: `설정을 수정했습니다: ${writeKeys.join(', ')}.`,
|
|
162
|
+
}),
|
|
163
|
+
next_actions: ['stop'],
|
|
164
|
+
data: { config: merged },
|
|
165
|
+
});
|
|
138
166
|
}
|
|
139
167
|
catch (e) {
|
|
140
168
|
return handleToolException('argus_config', e);
|
|
@@ -6,6 +6,7 @@ import { resolveContract } from '../lib/resolve-contract.js';
|
|
|
6
6
|
import { overfireGate } from '../lib/overfire-gate.js';
|
|
7
7
|
import { validateCrux } from '../lib/validate-crux.js';
|
|
8
8
|
import { computeContinuity } from '../lib/continuity.js';
|
|
9
|
+
import { relatedOpenForPremises } from '../v2/connection-io.js';
|
|
9
10
|
import { resolveResponseLocale, SURFACES } from '../lib/surfaces.js';
|
|
10
11
|
import { appendLedger } from '../lib/ledger-append.js';
|
|
11
12
|
import { ensurePrivacyGitignore } from '../lib/privacy.js';
|
|
@@ -50,13 +51,14 @@ export const openDecision = {
|
|
|
50
51
|
const id = String(a['id'] ?? '');
|
|
51
52
|
const today = resolveToday({ override: a['today_override'] });
|
|
52
53
|
// Response voice follows the decision sentence (M4): config > text > env.
|
|
53
|
-
const
|
|
54
|
+
const locale = resolveResponseLocale(dir, a['decision']);
|
|
55
|
+
const T = SURFACES[locale].tools.open_decision;
|
|
54
56
|
const current = resolveContract(dir, id, today);
|
|
55
57
|
if (a['already_decided'] === true && (current.state === 'sealed' || current.state === 'due' || current.state === 'settled')) {
|
|
56
58
|
return toolError({
|
|
57
59
|
ok: false, tool: 'argus_open_decision', error_code: 'ALREADY_CLOSED',
|
|
58
60
|
message: 'This decision is already underway or closed.',
|
|
59
|
-
recovery: 'To
|
|
61
|
+
recovery: 'To record what reality did, call argus_resolve. Closed decisions are not reopened.',
|
|
60
62
|
});
|
|
61
63
|
}
|
|
62
64
|
const signals = {
|
|
@@ -65,16 +67,36 @@ export const openDecision = {
|
|
|
65
67
|
already_decided: a['already_decided'] === true,
|
|
66
68
|
};
|
|
67
69
|
const gate = overfireGate(signals);
|
|
70
|
+
// Validate any model-supplied crux BEFORE any side-effect — invalid input
|
|
71
|
+
// must error without persisting. (The public capture surface no longer
|
|
72
|
+
// sends a crux; this guards internal callers that still can.)
|
|
73
|
+
const cruxErr = validateCrux(a['crux_question']);
|
|
74
|
+
if (cruxErr) {
|
|
75
|
+
return toolError({ ok: false, tool: 'argus_open_decision', error_code: cruxErr.code, message: cruxErr.message, recovery: cruxErr.recovery });
|
|
76
|
+
}
|
|
68
77
|
const now = new Date().toISOString();
|
|
69
78
|
// Always log the gate inputs for post-hoc accuracy measurement (M2).
|
|
70
79
|
await appendLedger(dir, [{ id, event: 'gate_input', gate: { ...signals, verdict: gate.reason } }], now);
|
|
80
|
+
// 기록과 의식을 분리한다: the user's own decision and premise are recorded
|
|
81
|
+
// REGARDLESS of the gate — deciding it "isn't worth keeping" would itself
|
|
82
|
+
// be a judgment about the user's decision (zero-judgment violation). The
|
|
83
|
+
// over-fire gate now governs only the surface CEREMONY (whether a crux is
|
|
84
|
+
// offered and a seal is nudged), never whether the record is written.
|
|
85
|
+
await ensurePrivacyGitignore(dir);
|
|
86
|
+
await atomicWriteJson(sessionFilePath(dir, id), {
|
|
87
|
+
v: SCHEMA_VERSION, id, problem_text: a['decision'], status_quo: a['status_quo'],
|
|
88
|
+
load_bearing_assumption: a['load_bearing_assumption'] ?? null, created_at: now,
|
|
89
|
+
});
|
|
90
|
+
await appendLedger(dir, [{ id, event: 'harvest', decision: a['decision'] }], now);
|
|
91
|
+
const relatedIds = Array.isArray(a['related_to']) ? a['related_to'] : [];
|
|
92
|
+
const continuity = relatedIds.length ? computeContinuity(dir, relatedIds) : undefined;
|
|
71
93
|
if (gate.response === 'reconfirm') {
|
|
72
94
|
return envelope({
|
|
73
95
|
ok: true, tool: 'argus_open_decision',
|
|
74
96
|
surface: T.reconfirm,
|
|
75
|
-
next_actions: ['
|
|
97
|
+
next_actions: ['leave_as_is'],
|
|
76
98
|
over_fire_gate: { fired: false, reason: gate.reason },
|
|
77
|
-
data: { id, crux_question: null, restraint_option: a['status_quo'], fork_emitted: false, harvest_written:
|
|
99
|
+
data: { id, crux_question: null, restraint_option: a['status_quo'], fork_emitted: false, harvest_written: true, continuity },
|
|
78
100
|
});
|
|
79
101
|
}
|
|
80
102
|
if (!gate.fire) {
|
|
@@ -82,33 +104,40 @@ export const openDecision = {
|
|
|
82
104
|
ok: true, tool: 'argus_open_decision',
|
|
83
105
|
// Human sentence, not a snake_case enum (11 P2-1). Contract (§4): the
|
|
84
106
|
// line ENDS by naming the option and returning the handle — never a
|
|
85
|
-
// directive ("leave it") issued in the user's stead.
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
surface: `${T.reason[gate.reason] ?? T.reason_fallback} ${T.leave_coda}
|
|
89
|
-
next_actions: ['leave_as_is', '
|
|
107
|
+
// directive ("leave it") issued in the user's stead. The decision is
|
|
108
|
+
// now recorded quietly, so the old "jot a note if you want it kept"
|
|
109
|
+
// exit is gone (it IS kept); the gate only withholds the ceremony.
|
|
110
|
+
surface: `${T.reason[gate.reason] ?? T.reason_fallback} ${T.leave_coda}`,
|
|
111
|
+
next_actions: ['leave_as_is', 'skip'],
|
|
90
112
|
over_fire_gate: { fired: false, reason: gate.reason },
|
|
91
|
-
data: { id, crux_question: null, restraint_option: a['status_quo'], fork_emitted: false, harvest_written:
|
|
113
|
+
data: { id, crux_question: null, restraint_option: a['status_quo'], fork_emitted: false, harvest_written: true, continuity },
|
|
92
114
|
});
|
|
93
115
|
}
|
|
94
|
-
// FIRE:
|
|
95
|
-
|
|
96
|
-
if (cruxErr) {
|
|
97
|
-
return toolError({ ok: false, tool: 'argus_open_decision', error_code: cruxErr.code, message: cruxErr.message, recovery: cruxErr.recovery });
|
|
98
|
-
}
|
|
99
|
-
await ensurePrivacyGitignore(dir);
|
|
100
|
-
await atomicWriteJson(sessionFilePath(dir, id), {
|
|
101
|
-
v: SCHEMA_VERSION, id, problem_text: a['decision'], status_quo: a['status_quo'],
|
|
102
|
-
load_bearing_assumption: a['load_bearing_assumption'] ?? null, created_at: now,
|
|
103
|
-
});
|
|
104
|
-
await appendLedger(dir, [{ id, event: 'harvest', decision: a['decision'] }], now);
|
|
116
|
+
// FIRE: the ceremony — surface the one neutral crux (if supplied) and the
|
|
117
|
+
// seal path. Persistence already happened above.
|
|
105
118
|
const crux = a['crux_question'] ?? null;
|
|
106
|
-
|
|
107
|
-
|
|
119
|
+
// Capture-time connection (정본 §8-§11, §8-C): the same mechanical read the
|
|
120
|
+
// settle surface uses, moved to the front door. If the premise this
|
|
121
|
+
// decision rests on is one the user already tracks under another OPEN
|
|
122
|
+
// decision, name that fact + the handle — never a verdict, never "revisit
|
|
123
|
+
// it". best-effort: a non-git/uninit/failed v2 read just yields no line.
|
|
124
|
+
const premiseTexts = [
|
|
125
|
+
typeof a['load_bearing_assumption'] === 'string' ? a['load_bearing_assumption'] : '',
|
|
126
|
+
...(Array.isArray(a['premises']) ? a['premises'].map((p) => p?.text ?? '') : []),
|
|
127
|
+
];
|
|
128
|
+
const connections = relatedOpenForPremises(dir, today, premiseTexts, id);
|
|
129
|
+
let connectionLine = '';
|
|
130
|
+
if (connections.length > 0) {
|
|
131
|
+
const shown = connections.slice(0, 3).map((c) => c.decision_id);
|
|
132
|
+
const extra = connections.length - shown.length;
|
|
133
|
+
connectionLine = locale === 'ko'
|
|
134
|
+
? `\n이 결정이 기댄 전제와 같은 가정이나 근거에 선 다른 열린 결정: ${shown.join(', ')}${extra > 0 ? ` 외 ${extra}개` : ''}. argus_check_in으로 함께 볼 수 있어요.`
|
|
135
|
+
: `\nOther open decisions rest on the same assumption or fact this one leans on: ${shown.join(', ')}${extra > 0 ? ` (+${extra} more)` : ''}. Review them together with argus_check_in.`;
|
|
136
|
+
}
|
|
108
137
|
return envelope({
|
|
109
138
|
ok: true, tool: 'argus_open_decision',
|
|
110
|
-
surface: crux ? T.opened_with_crux(crux) : T.opened_bare,
|
|
111
|
-
next_actions: ['
|
|
139
|
+
surface: (crux ? T.opened_with_crux(crux) : T.opened_bare) + connectionLine,
|
|
140
|
+
next_actions: ['argus_predict', 'leave_as_is', 'skip'],
|
|
112
141
|
over_fire_gate: { fired: true, reason: gate.reason },
|
|
113
142
|
data: {
|
|
114
143
|
id,
|
|
@@ -119,6 +148,10 @@ export const openDecision = {
|
|
|
119
148
|
fork_emitted: false,
|
|
120
149
|
harvest_written: true,
|
|
121
150
|
continuity,
|
|
151
|
+
...(connections.length > 0 ? {
|
|
152
|
+
connections: connections.map((c) => c.decision_id),
|
|
153
|
+
connection_reasons: connections.map((c) => ({ id: c.decision_id, reason: c.reason, ...(c.via ? { via: c.via } : {}) })),
|
|
154
|
+
} : {}),
|
|
122
155
|
lean_disclosure: 'Naming the load-bearing question points faintly at the flip; that residual lean is a known limit, not a verdict.',
|
|
123
156
|
},
|
|
124
157
|
});
|