@askthew/mcp-plugin 0.4.9 → 0.4.10
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 +9 -11
- package/dist/index.js +74 -564
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ Connect a local coding agent to Ask The W. The fastest path is free and local-fi
|
|
|
6
6
|
npx -y --prefer-online --package @askthew/mcp-plugin@latest askthew-mcp install --host claude_code --free --email you@founder.com
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
-
This captures decisions and session signals to `~/.askthew/store.sqlite` and lets your agent
|
|
9
|
+
This captures decisions and session signals to `~/.askthew/store.sqlite` and lets your agent list the trail, keep decisions, and get limited local recap and coaching without onboarding into the web app.
|
|
10
10
|
|
|
11
11
|
Founder-friendly promise: install from npm, then ask your coding agent to review the last session. You should see value in under 60 seconds.
|
|
12
12
|
|
|
@@ -19,7 +19,7 @@ This package runs a small MCP server that lets Codex, Claude Code, Cursor, and o
|
|
|
19
19
|
- Adds marked project instructions so future coding-agent sessions know when to send Ask The W updates.
|
|
20
20
|
- Free mode stores full-fidelity signals and decisions locally in SQLite.
|
|
21
21
|
- Paid workspace mode sends a startup heartbeat so Ask The W can show the plugin as installed.
|
|
22
|
-
- Exposes
|
|
22
|
+
- Exposes a small MCP surface for capture, signals, decisions, recap, and coaching.
|
|
23
23
|
- Redacts obvious secrets from summaries, evidence excerpts, commands, and metadata before sending.
|
|
24
24
|
- Adds lightweight workspace metadata such as host type, repo name, app path, and server name.
|
|
25
25
|
|
|
@@ -27,10 +27,10 @@ This package runs a small MCP server that lets Codex, Claude Code, Cursor, and o
|
|
|
27
27
|
|
|
28
28
|
- It does not send full transcripts by default.
|
|
29
29
|
- It does not send local free-tier content to Ask The W unless you upgrade and run sync upload.
|
|
30
|
-
- It does not
|
|
30
|
+
- It does not expose internal workspace APIs through the plugin.
|
|
31
31
|
- It does not include the Ask The W app, private server code, Supabase code, or internal analytics code.
|
|
32
32
|
|
|
33
|
-
Ask The W
|
|
33
|
+
Ask The W keeps the local plugin focused on the trail your agent can use while it works.
|
|
34
34
|
|
|
35
35
|
## Free Local Install
|
|
36
36
|
|
|
@@ -183,15 +183,13 @@ The session-signal tool remains the main automatic capture path.
|
|
|
183
183
|
|
|
184
184
|
Use compact summaries and short evidence excerpts. Do not send full transcripts. By default, write tools return compact responses; use `echo: "full"` only when you need the larger payload for debugging.
|
|
185
185
|
|
|
186
|
-
The plugin
|
|
186
|
+
The plugin exposes a small public tool surface:
|
|
187
187
|
|
|
188
188
|
| Tool | Purpose |
|
|
189
189
|
|---|---|
|
|
190
|
-
| `
|
|
191
|
-
| `
|
|
192
|
-
| `
|
|
193
|
-
| `get_north_star`, `update_north_star` | Read or update the workspace north star. API updates are allowed only for private workspaces. |
|
|
194
|
-
| `list_signals`, `get_signal` | Read workspace signals. |
|
|
190
|
+
| `capture_session_signal`, `list_signals` | Capture and list the local signal trail. |
|
|
191
|
+
| `list_decisions`, `get_decision`, `create_decision`, `promote_signal_to_decision` | Keep the decisions worth remembering. |
|
|
192
|
+
| `recap`, `coach` | Get up to three local recaps and three coaching nudges. After that, the plugin returns `Limit reached. Upgrade to the paid plan.` |
|
|
195
193
|
|
|
196
194
|
Free PLG helpers:
|
|
197
195
|
|
|
@@ -202,7 +200,7 @@ npx -y --prefer-online --package @askthew/mcp-plugin@latest askthew-mcp digest -
|
|
|
202
200
|
|
|
203
201
|
The hook prompts when staged files recently had implementation signals but no linked decision. The weekly digest writes `~/Documents/askthew-digest-YYYY-WW.md`.
|
|
204
202
|
|
|
205
|
-
|
|
203
|
+
Decision writes are text-only and are recorded with the local trail.
|
|
206
204
|
|
|
207
205
|
## Troubleshooting
|
|
208
206
|
|
package/dist/index.js
CHANGED
|
@@ -11,7 +11,7 @@ import { resolveMcpMode } from "./lib/free-tier-policy.js";
|
|
|
11
11
|
import { LocalStore } from "./lib/local-store.js";
|
|
12
12
|
import { buildTelemetryPayload, flushTelemetryOutbox } from "./lib/telemetry.js";
|
|
13
13
|
import { ensureLocalIdentity } from "./lib/local-identity.js";
|
|
14
|
-
import {
|
|
14
|
+
import { paidFeatureNudge, toolJson } from "./lib/upgrade-nudge.js";
|
|
15
15
|
import { configPath, readJsonFile } from "./lib/paths.js";
|
|
16
16
|
const requirePackageJson = createRequire(import.meta.url);
|
|
17
17
|
function packageVersion() {
|
|
@@ -321,6 +321,8 @@ const echoSchema = z.enum(["summary", "full"]).optional();
|
|
|
321
321
|
const cursorSchema = z.string().optional();
|
|
322
322
|
const idempotencyKeySchema = z.string().min(1).max(200).optional();
|
|
323
323
|
const maxCharsSchema = z.number().int().positive().max(100000).optional();
|
|
324
|
+
const LIMITED_LOCAL_TOOL_LIMIT = 3;
|
|
325
|
+
const LIMIT_REACHED_MESSAGE = "Limit reached. Upgrade to the paid plan.";
|
|
324
326
|
function traceId() {
|
|
325
327
|
return `trace_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
|
|
326
328
|
}
|
|
@@ -622,6 +624,7 @@ export function createAskTheWMcpServer(options = {}) {
|
|
|
622
624
|
const compactWriteResponse = (input) => localResponse({
|
|
623
625
|
ok: input.ok !== false,
|
|
624
626
|
id: input.id ?? null,
|
|
627
|
+
...(input.decisionId ? { decisionId: input.decisionId } : {}),
|
|
625
628
|
...(input.sessionId ? { sessionId: input.sessionId } : {}),
|
|
626
629
|
...(typeof input.sequence === "number" ? { sequence: input.sequence } : {}),
|
|
627
630
|
...(typeof input.signalCount === "number" ? { signalCount: input.signalCount } : {}),
|
|
@@ -666,88 +669,27 @@ export function createAskTheWMcpServer(options = {}) {
|
|
|
666
669
|
}
|
|
667
670
|
return null;
|
|
668
671
|
};
|
|
669
|
-
const
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
const
|
|
683
|
-
const
|
|
684
|
-
const
|
|
685
|
-
if (
|
|
686
|
-
return
|
|
687
|
-
code: "free_tier_limit",
|
|
688
|
-
message: "The free plugin can analyze the latest three local sessions.",
|
|
689
|
-
retryable: false,
|
|
690
|
-
hint: "Upgrade to review more than three sessions in the workspace dashboard.",
|
|
691
|
-
extra: {
|
|
692
|
-
tool: "analyze_session",
|
|
693
|
-
limit: 3,
|
|
694
|
-
upgradeUrl: "https://askthew.com/plugin?utm_source=mcp-plugin&utm_medium=tool-nudge&utm_campaign=mcp-free&tool=analyze_session",
|
|
695
|
-
cta: "Upgrade to analyze more than three sessions in the workspace dashboard.",
|
|
696
|
-
},
|
|
697
|
-
});
|
|
698
|
-
}
|
|
699
|
-
const limit = Math.min(50, payload.limit ?? 50);
|
|
700
|
-
const signals = sessionId
|
|
701
|
-
? localStore.listSignals({ sessionId, scopeKey, cursor: payload.cursor, limit })
|
|
702
|
-
: [];
|
|
703
|
-
const allSignals = sessionId ? localStore.listSignals({ sessionId, scopeKey, limit: 100000 }) : [];
|
|
704
|
-
const decisions = sessionId
|
|
705
|
-
? localStore.listDecisions({ sessionId, scopeKey, limit: 100000 })
|
|
706
|
-
: [];
|
|
707
|
-
const decisionCandidates = listDecisionCandidates({ store: localStore, sessionId, scopeKey, limit: 25 }).candidates;
|
|
708
|
-
const counts = signals.reduce((accumulator, signal) => {
|
|
709
|
-
accumulator[signal.kind] = (accumulator[signal.kind] ?? 0) + 1;
|
|
710
|
-
return accumulator;
|
|
711
|
-
}, {});
|
|
712
|
-
const allCounts = allSignals.reduce((accumulator, signal) => {
|
|
713
|
-
accumulator[signal.kind] = (accumulator[signal.kind] ?? 0) + 1;
|
|
714
|
-
return accumulator;
|
|
715
|
-
}, {});
|
|
716
|
-
const nextCursor = signals.length >= limit ? signals.at(-1)?.capturedAt ?? null : null;
|
|
717
|
-
if ((payload.format ?? "markdown") === "json") {
|
|
718
|
-
return budgetedLocalResponse({
|
|
719
|
-
ok: true,
|
|
720
|
-
tier: "free",
|
|
721
|
-
sessionId,
|
|
722
|
-
format: "json",
|
|
723
|
-
signals: payload.compact
|
|
724
|
-
? signals.map((signal) => compactSignal(signal, localStore.getDecisionForSignal(signal.id)))
|
|
725
|
-
: signals.map((signal) => signalWithDecision(localStore, signal)),
|
|
726
|
-
decisions: payload.compact
|
|
727
|
-
? decisions.map((decision) => ({ id: decision.id, headline: decision.headline, status: decision.status, signalIds: decision.sourceSignalIds }))
|
|
728
|
-
: decisions.map((decision) => decisionWithSignals(localStore, decision)),
|
|
729
|
-
decisionCandidates,
|
|
730
|
-
nextCursor,
|
|
731
|
-
counts: {
|
|
732
|
-
totalSignals: allSignals.length,
|
|
733
|
-
byKind: allCounts,
|
|
734
|
-
},
|
|
735
|
-
}, payload.max_chars);
|
|
672
|
+
const limitReachedResponse = (tool) => localResponse({
|
|
673
|
+
ok: false,
|
|
674
|
+
code: "free_tier_limit_reached",
|
|
675
|
+
tool,
|
|
676
|
+
message: LIMIT_REACHED_MESSAGE,
|
|
677
|
+
limit: LIMITED_LOCAL_TOOL_LIMIT,
|
|
678
|
+
pricingUrl: "https://askthew.com/pricing",
|
|
679
|
+
upgradeUrl: `https://askthew.com/plugin?utm_source=mcp-plugin&utm_medium=tool-limit&utm_campaign=mcp-free&tool=${tool}`,
|
|
680
|
+
supportEmail: "support@askthew.com",
|
|
681
|
+
});
|
|
682
|
+
const consumeLimitedLocalUse = (tool) => {
|
|
683
|
+
if (!localStore)
|
|
684
|
+
return null;
|
|
685
|
+
const key = `usage:${tool}`;
|
|
686
|
+
const current = Number(localStore.getMeta(key) || "0");
|
|
687
|
+
const count = Number.isFinite(current) ? Math.max(0, Math.floor(current)) : 0;
|
|
688
|
+
if (count >= LIMITED_LOCAL_TOOL_LIMIT) {
|
|
689
|
+
return limitReachedResponse(tool);
|
|
736
690
|
}
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
tier: "free",
|
|
740
|
-
sessionId,
|
|
741
|
-
format: "markdown",
|
|
742
|
-
rendered: renderSessionMarkdown({ sessionId, signals: allSignals, decisions, decisionCandidates }),
|
|
743
|
-
...(payload.compact
|
|
744
|
-
? { signals: allSignals.map((signal) => compactSignal(signal, localStore.getDecisionForSignal(signal.id))) }
|
|
745
|
-
: {}),
|
|
746
|
-
counts: {
|
|
747
|
-
totalSignals: allSignals.length,
|
|
748
|
-
byKind: Object.keys(allCounts).length ? allCounts : counts,
|
|
749
|
-
},
|
|
750
|
-
}, payload.max_chars);
|
|
691
|
+
localStore.setMeta(key, String(count + 1));
|
|
692
|
+
return null;
|
|
751
693
|
};
|
|
752
694
|
server.tool("capture_session_signal", {
|
|
753
695
|
sessionId: z.string().min(1),
|
|
@@ -792,6 +734,19 @@ export function createAskTheWMcpServer(options = {}) {
|
|
|
792
734
|
scopeKey,
|
|
793
735
|
limit: 100000,
|
|
794
736
|
}).length;
|
|
737
|
+
const existingDecision = localStore.getDecisionForSignal(signal.id);
|
|
738
|
+
const decisionCandidate = candidateFromSignal(signal, existingDecision);
|
|
739
|
+
const autoDecision = decisionCandidate
|
|
740
|
+
? localStore.createDecision({
|
|
741
|
+
rawContent: signal.summary,
|
|
742
|
+
headline: signal.summary,
|
|
743
|
+
status: decisionCandidate.suggestedStatus,
|
|
744
|
+
sessionId: signal.sessionId,
|
|
745
|
+
files: signal.filesTouched,
|
|
746
|
+
sourceSignalIds: [signal.id],
|
|
747
|
+
scopeKey,
|
|
748
|
+
})
|
|
749
|
+
: null;
|
|
795
750
|
if (sessionSignal.kind === "final_summary" && mode.cliCredentials) {
|
|
796
751
|
localStore.enqueueTelemetry(buildTelemetryPayload({
|
|
797
752
|
store: localStore,
|
|
@@ -808,6 +763,7 @@ export function createAskTheWMcpServer(options = {}) {
|
|
|
808
763
|
if (!payload.echo) {
|
|
809
764
|
return compactWriteResponse({
|
|
810
765
|
id: signal.id,
|
|
766
|
+
decisionId: autoDecision?.id,
|
|
811
767
|
sessionId: signal.sessionId,
|
|
812
768
|
sequence: signal.sequence,
|
|
813
769
|
signalCount: sessionSignalCount,
|
|
@@ -822,12 +778,14 @@ export function createAskTheWMcpServer(options = {}) {
|
|
|
822
778
|
signalCount: sessionSignalCount,
|
|
823
779
|
summary: signal.summary,
|
|
824
780
|
kind: signal.kind,
|
|
781
|
+
...(autoDecision ? { decisionId: autoDecision.id } : {}),
|
|
825
782
|
});
|
|
826
783
|
}
|
|
827
784
|
return localResponse({
|
|
828
785
|
ok: true,
|
|
829
786
|
tier: "free",
|
|
830
787
|
signal,
|
|
788
|
+
...(autoDecision ? { decision: decisionWithSignals(localStore, autoDecision) } : {}),
|
|
831
789
|
note: localStore.usingJsonFallback
|
|
832
790
|
? "Captured locally in JSON fallback mode because SQLite was unavailable."
|
|
833
791
|
: "Captured locally in SQLite. Aggregate telemetry only may flush on final_summary unless opted out.",
|
|
@@ -1003,179 +961,6 @@ export function createAskTheWMcpServer(options = {}) {
|
|
|
1003
961
|
? { ok: true, id: upstreamId(upstream), sequence: upstreamSequence(upstream) ?? 1 }
|
|
1004
962
|
: upstream);
|
|
1005
963
|
});
|
|
1006
|
-
server.tool("update_decision", {
|
|
1007
|
-
id: z.string().min(1),
|
|
1008
|
-
headline: z.string().min(1).optional(),
|
|
1009
|
-
why: z.string().optional(),
|
|
1010
|
-
status: z.enum(["proposed", "committed", "shipped", "abandoned"]).optional(),
|
|
1011
|
-
alignment: z.enum(["aligned", "orthogonal", "conflicts", "ambiguous"]).optional(),
|
|
1012
|
-
outcomeId: z.string().min(1).optional(),
|
|
1013
|
-
idempotencyKey: idempotencyKeySchema,
|
|
1014
|
-
echo: echoSchema,
|
|
1015
|
-
}, async (payload) => {
|
|
1016
|
-
if (mode.mode !== "paid" && localStore) {
|
|
1017
|
-
const loginRequired = requireFreeIdentity();
|
|
1018
|
-
if (loginRequired)
|
|
1019
|
-
return loginRequired;
|
|
1020
|
-
const decision = localStore.updateDecision(payload.id, {
|
|
1021
|
-
...(payload.headline ? { headline: payload.headline } : {}),
|
|
1022
|
-
...(payload.why !== undefined ? { why: payload.why } : {}),
|
|
1023
|
-
...(payload.status ? { status: payload.status } : {}),
|
|
1024
|
-
...(payload.alignment !== undefined ? { alignment: payload.alignment } : {}),
|
|
1025
|
-
});
|
|
1026
|
-
if (!decision) {
|
|
1027
|
-
return localToolError({
|
|
1028
|
-
code: "not_found",
|
|
1029
|
-
message: "Decision not found in the local Ask The W store.",
|
|
1030
|
-
retryable: false,
|
|
1031
|
-
hint: "Check the decision id before updating.",
|
|
1032
|
-
});
|
|
1033
|
-
}
|
|
1034
|
-
const warnings = detectDecisionConflicts({
|
|
1035
|
-
decision,
|
|
1036
|
-
decisions: localStore.listDecisions({ limit: 100000, scopeKey: currentScopeKey() }),
|
|
1037
|
-
});
|
|
1038
|
-
if (!payload.echo) {
|
|
1039
|
-
return compactWriteResponse({ id: decision.id, sequence: localStore.stats().decisions, warnings });
|
|
1040
|
-
}
|
|
1041
|
-
return localResponse(payload.echo === "summary"
|
|
1042
|
-
? { ok: true, id: decision.id, sequence: localStore.stats().decisions, headline: decision.headline, warnings }
|
|
1043
|
-
: { ok: true, tier: "free", decision: decisionWithSignals(localStore, decision), warnings });
|
|
1044
|
-
}
|
|
1045
|
-
const upstream = await postToServer(payload.echo === "full"
|
|
1046
|
-
? `/api/decisions/${encodeURIComponent(payload.id)}`
|
|
1047
|
-
: withResponseShape(`/api/decisions/${encodeURIComponent(payload.id)}`), {
|
|
1048
|
-
headline: payload.headline,
|
|
1049
|
-
why: payload.why,
|
|
1050
|
-
status: payload.status,
|
|
1051
|
-
alignment: payload.alignment,
|
|
1052
|
-
outcomeId: payload.outcomeId,
|
|
1053
|
-
}, options, { method: "PATCH", idempotencyKey: payload.idempotencyKey });
|
|
1054
|
-
const failure = upstreamFailure(upstream);
|
|
1055
|
-
if (failure)
|
|
1056
|
-
return localResponse(failure);
|
|
1057
|
-
if (!payload.echo) {
|
|
1058
|
-
return compactWriteResponse({ id: upstreamId(upstream) ?? payload.id, sequence: upstreamSequence(upstream) ?? 1 });
|
|
1059
|
-
}
|
|
1060
|
-
return localResponse(payload.echo === "summary"
|
|
1061
|
-
? { ok: true, id: upstreamId(upstream) ?? payload.id, sequence: upstreamSequence(upstream) ?? 1 }
|
|
1062
|
-
: upstream);
|
|
1063
|
-
});
|
|
1064
|
-
server.tool("delete_decision", {
|
|
1065
|
-
id: z.string().min(1),
|
|
1066
|
-
confirmText: z.string().min(1),
|
|
1067
|
-
idempotencyKey: idempotencyKeySchema,
|
|
1068
|
-
}, async (payload) => {
|
|
1069
|
-
if (mode.mode !== "paid" && localStore) {
|
|
1070
|
-
return localResponse(paidFeatureNudge("delete_decision"));
|
|
1071
|
-
}
|
|
1072
|
-
return apiToolResponse(`/api/decisions/${encodeURIComponent(payload.id)}`, {
|
|
1073
|
-
confirmText: payload.confirmText,
|
|
1074
|
-
}, "DELETE", { idempotencyKey: payload.idempotencyKey });
|
|
1075
|
-
});
|
|
1076
|
-
server.tool("list_outcomes", paidDescription("List outcomes from your workspace.", mode.mode), {
|
|
1077
|
-
limit: z.number().int().positive().max(300).optional(),
|
|
1078
|
-
cursor: cursorSchema,
|
|
1079
|
-
}, async (payload) => {
|
|
1080
|
-
if (mode.mode === "free")
|
|
1081
|
-
return localResponse(paidFeatureNudge("list_outcomes"));
|
|
1082
|
-
return apiToolResponse(routeWithQuery("/api/outcomes", {
|
|
1083
|
-
limit: payload.limit,
|
|
1084
|
-
cursor: payload.cursor,
|
|
1085
|
-
}));
|
|
1086
|
-
});
|
|
1087
|
-
server.tool("get_outcome", paidDescription("Get outcome detail from your workspace.", mode.mode), {
|
|
1088
|
-
id: z.string().min(1),
|
|
1089
|
-
}, async (payload) => mode.mode === "free"
|
|
1090
|
-
? localResponse(paidFeatureNudge("get_outcome"))
|
|
1091
|
-
: apiToolResponse(`/api/outcomes/${encodeURIComponent(payload.id)}`));
|
|
1092
|
-
server.tool("list_outcome_signals", paidDescription("List signals linked to an outcome.", mode.mode), {
|
|
1093
|
-
id: z.string().min(1),
|
|
1094
|
-
limit: z.number().int().positive().max(300).optional(),
|
|
1095
|
-
cursor: cursorSchema,
|
|
1096
|
-
}, async (payload) => mode.mode === "free"
|
|
1097
|
-
? localResponse(paidFeatureNudge("list_outcome_signals"))
|
|
1098
|
-
: apiToolResponse(routeWithQuery(`/api/outcomes/${encodeURIComponent(payload.id)}/signals`, {
|
|
1099
|
-
limit: payload.limit,
|
|
1100
|
-
cursor: payload.cursor,
|
|
1101
|
-
})));
|
|
1102
|
-
server.tool("create_outcome", paidDescription("Create a new outcome.", mode.mode), {
|
|
1103
|
-
name: z.string().min(1),
|
|
1104
|
-
summary: z.string().optional(),
|
|
1105
|
-
causalHypothesis: z.string().optional(),
|
|
1106
|
-
suggestedAction: z.string().optional(),
|
|
1107
|
-
idempotencyKey: idempotencyKeySchema,
|
|
1108
|
-
}, async (payload) => {
|
|
1109
|
-
if (mode.mode === "free")
|
|
1110
|
-
return localResponse(paidFeatureNudge("create_outcome"));
|
|
1111
|
-
return apiToolResponse("/api/outcomes", {
|
|
1112
|
-
name: payload.name,
|
|
1113
|
-
summary: payload.summary,
|
|
1114
|
-
causalHypothesis: payload.causalHypothesis,
|
|
1115
|
-
suggestedAction: payload.suggestedAction,
|
|
1116
|
-
}, "POST", { idempotencyKey: payload.idempotencyKey });
|
|
1117
|
-
});
|
|
1118
|
-
server.tool("update_outcome", paidDescription("Update an outcome.", mode.mode), {
|
|
1119
|
-
id: z.string().min(1),
|
|
1120
|
-
name: z.string().min(1).optional(),
|
|
1121
|
-
summary: z.string().optional(),
|
|
1122
|
-
causalHypothesis: z.string().optional(),
|
|
1123
|
-
suggestedAction: z.string().optional(),
|
|
1124
|
-
status: z.enum(["active", "achieved", "abandoned", "archived"]).optional(),
|
|
1125
|
-
idempotencyKey: idempotencyKeySchema,
|
|
1126
|
-
echo: echoSchema,
|
|
1127
|
-
}, async (payload) => {
|
|
1128
|
-
if (mode.mode === "free")
|
|
1129
|
-
return localResponse(paidFeatureNudge("update_outcome"));
|
|
1130
|
-
const upstream = await postToServer(payload.echo === "full"
|
|
1131
|
-
? `/api/outcomes/${encodeURIComponent(payload.id)}`
|
|
1132
|
-
: withResponseShape(`/api/outcomes/${encodeURIComponent(payload.id)}`), {
|
|
1133
|
-
name: payload.name,
|
|
1134
|
-
summary: payload.summary,
|
|
1135
|
-
causalHypothesis: payload.causalHypothesis,
|
|
1136
|
-
suggestedAction: payload.suggestedAction,
|
|
1137
|
-
status: payload.status,
|
|
1138
|
-
}, options, { method: "PATCH", idempotencyKey: payload.idempotencyKey });
|
|
1139
|
-
const failure = upstreamFailure(upstream);
|
|
1140
|
-
if (failure)
|
|
1141
|
-
return localResponse(failure);
|
|
1142
|
-
if (!payload.echo) {
|
|
1143
|
-
return compactWriteResponse({ id: upstreamId(upstream) ?? payload.id, sequence: upstreamSequence(upstream) ?? 1 });
|
|
1144
|
-
}
|
|
1145
|
-
return localResponse(payload.echo === "summary"
|
|
1146
|
-
? { ok: true, id: upstreamId(upstream) ?? payload.id, sequence: upstreamSequence(upstream) ?? 1 }
|
|
1147
|
-
: upstream);
|
|
1148
|
-
});
|
|
1149
|
-
server.tool("delete_outcome", paidDescription("Delete an outcome.", mode.mode), {
|
|
1150
|
-
id: z.string().min(1),
|
|
1151
|
-
confirmText: z.string().min(1),
|
|
1152
|
-
idempotencyKey: idempotencyKeySchema,
|
|
1153
|
-
}, async (payload) => {
|
|
1154
|
-
if (mode.mode === "free")
|
|
1155
|
-
return localResponse(paidFeatureNudge("delete_outcome"));
|
|
1156
|
-
return apiToolResponse(`/api/outcomes/${encodeURIComponent(payload.id)}`, {
|
|
1157
|
-
confirmText: payload.confirmText,
|
|
1158
|
-
}, "DELETE", { idempotencyKey: payload.idempotencyKey });
|
|
1159
|
-
});
|
|
1160
|
-
server.tool("get_north_star", paidDescription("Read the workspace north-star metric.", mode.mode), {}, async () => mode.mode === "free"
|
|
1161
|
-
? localResponse(paidFeatureNudge("get_north_star"))
|
|
1162
|
-
: apiToolResponse("/api/north-star"));
|
|
1163
|
-
server.tool("update_north_star", paidDescription("Update the workspace north-star metric.", mode.mode), {
|
|
1164
|
-
metric: z.string().min(1),
|
|
1165
|
-
current: z.string().min(1),
|
|
1166
|
-
target: z.string().min(1),
|
|
1167
|
-
reason: z.string().min(1),
|
|
1168
|
-
idempotencyKey: idempotencyKeySchema,
|
|
1169
|
-
}, async (payload) => {
|
|
1170
|
-
if (mode.mode === "free")
|
|
1171
|
-
return localResponse(paidFeatureNudge("update_north_star"));
|
|
1172
|
-
return apiToolResponse("/api/north-star", {
|
|
1173
|
-
metric: payload.metric,
|
|
1174
|
-
current: payload.current,
|
|
1175
|
-
target: payload.target,
|
|
1176
|
-
reason: payload.reason,
|
|
1177
|
-
}, "POST", { idempotencyKey: payload.idempotencyKey });
|
|
1178
|
-
});
|
|
1179
964
|
server.tool("list_signals", {
|
|
1180
965
|
limit: z.number().int().positive().max(300).optional(),
|
|
1181
966
|
cursor: z.string().optional(),
|
|
@@ -1213,109 +998,7 @@ export function createAskTheWMcpServer(options = {}) {
|
|
|
1213
998
|
max_chars: payload.max_chars ?? 8000,
|
|
1214
999
|
}));
|
|
1215
1000
|
});
|
|
1216
|
-
server.tool("
|
|
1217
|
-
query: z.string().min(1),
|
|
1218
|
-
sessionId: z.string().optional(),
|
|
1219
|
-
limit: z.number().int().positive().max(50).default(5),
|
|
1220
|
-
compact: z.boolean().optional(),
|
|
1221
|
-
max_chars: maxCharsSchema,
|
|
1222
|
-
}, async (payload) => {
|
|
1223
|
-
if (mode.mode !== "paid" && localStore) {
|
|
1224
|
-
return localResponse(paidFeatureNudge("find_signal_by_summary"));
|
|
1225
|
-
}
|
|
1226
|
-
return apiToolResponse(routeWithQuery("/api/signals", {
|
|
1227
|
-
query: payload.query,
|
|
1228
|
-
sessionId: payload.sessionId,
|
|
1229
|
-
limit: payload.limit,
|
|
1230
|
-
compact: payload.compact ?? true,
|
|
1231
|
-
max_chars: payload.max_chars ?? 8000,
|
|
1232
|
-
}));
|
|
1233
|
-
});
|
|
1234
|
-
server.tool("get_signal", {
|
|
1235
|
-
id: z.string().min(1),
|
|
1236
|
-
}, async (payload) => {
|
|
1237
|
-
if (mode.mode !== "paid" && localStore) {
|
|
1238
|
-
const loginRequired = requireFreeIdentity();
|
|
1239
|
-
if (loginRequired)
|
|
1240
|
-
return loginRequired;
|
|
1241
|
-
const signal = localStore.getSignal(Number(payload.id));
|
|
1242
|
-
return signal
|
|
1243
|
-
? localResponse({ ok: true, tier: "free", signal: signalWithDecision(localStore, signal) })
|
|
1244
|
-
: localToolError({
|
|
1245
|
-
code: "not_found",
|
|
1246
|
-
message: "Signal not found in the local Ask The W store.",
|
|
1247
|
-
retryable: false,
|
|
1248
|
-
hint: "Check the signal id or list local signals first.",
|
|
1249
|
-
});
|
|
1250
|
-
}
|
|
1251
|
-
return apiToolResponse(`/api/signals/${encodeURIComponent(payload.id)}`);
|
|
1252
|
-
});
|
|
1253
|
-
server.tool("review_decisions", "Review captured decisions. Use for natural prompts like: What did I decide yesterday?", {
|
|
1254
|
-
since: z.string().optional(),
|
|
1255
|
-
status: z.enum(["proposed", "committed", "shipped", "abandoned"]).optional(),
|
|
1256
|
-
format: z.enum(["markdown", "json"]).default("markdown"),
|
|
1257
|
-
limit: z.number().int().positive().max(300).default(50),
|
|
1258
|
-
cursor: cursorSchema,
|
|
1259
|
-
sessionId: z.string().optional(),
|
|
1260
|
-
compact: z.boolean().optional(),
|
|
1261
|
-
max_chars: maxCharsSchema,
|
|
1262
|
-
}, async (payload) => {
|
|
1263
|
-
if (mode.mode !== "paid")
|
|
1264
|
-
return localResponse(paidFeatureNudge("review_decisions"));
|
|
1265
|
-
if (mode.mode === "paid") {
|
|
1266
|
-
return apiToolResponse(routeWithQuery("/api/decisions", {
|
|
1267
|
-
since: payload.since,
|
|
1268
|
-
status: payload.status,
|
|
1269
|
-
limit: payload.limit,
|
|
1270
|
-
cursor: payload.cursor,
|
|
1271
|
-
sessionId: payload.sessionId,
|
|
1272
|
-
compact: payload.compact,
|
|
1273
|
-
max_chars: payload.max_chars,
|
|
1274
|
-
}));
|
|
1275
|
-
}
|
|
1276
|
-
return localResponse(paidFeatureNudge("review_decisions"));
|
|
1277
|
-
});
|
|
1278
|
-
server.tool("review_session", "Review the current session trail. Use for natural prompts like: Show me my session trail.", {
|
|
1279
|
-
sessionId: z.string().optional(),
|
|
1280
|
-
format: z.enum(["markdown", "json"]).default("markdown"),
|
|
1281
|
-
cursor: cursorSchema,
|
|
1282
|
-
limit: z.number().int().positive().max(50).default(50),
|
|
1283
|
-
compact: z.boolean().optional(),
|
|
1284
|
-
max_chars: maxCharsSchema,
|
|
1285
|
-
}, async (payload) => {
|
|
1286
|
-
if (mode.mode !== "paid")
|
|
1287
|
-
return localResponse(paidFeatureNudge("review_session"));
|
|
1288
|
-
if (!localStore || mode.mode === "paid") {
|
|
1289
|
-
return apiToolResponse(routeWithQuery("/api/signals", {
|
|
1290
|
-
sessionId: payload.sessionId,
|
|
1291
|
-
cursor: payload.cursor,
|
|
1292
|
-
limit: payload.limit,
|
|
1293
|
-
compact: payload.compact,
|
|
1294
|
-
max_chars: payload.max_chars,
|
|
1295
|
-
}));
|
|
1296
|
-
}
|
|
1297
|
-
return localResponse(paidFeatureNudge("review_session"));
|
|
1298
|
-
});
|
|
1299
|
-
server.tool("analyze_session", "Analyze the latest local coding-agent session.", {
|
|
1300
|
-
sessionId: z.string().optional(),
|
|
1301
|
-
format: z.enum(["markdown", "json"]).default("markdown"),
|
|
1302
|
-
cursor: cursorSchema,
|
|
1303
|
-
limit: z.number().int().positive().max(50).default(50),
|
|
1304
|
-
compact: z.boolean().optional(),
|
|
1305
|
-
max_chars: maxCharsSchema,
|
|
1306
|
-
}, async (payload) => {
|
|
1307
|
-
if (!localStore || mode.mode === "paid") {
|
|
1308
|
-
return apiToolResponse(routeWithQuery("/api/signals", {
|
|
1309
|
-
sessionId: payload.sessionId,
|
|
1310
|
-
cursor: payload.cursor,
|
|
1311
|
-
limit: payload.limit,
|
|
1312
|
-
compact: payload.compact,
|
|
1313
|
-
max_chars: payload.max_chars,
|
|
1314
|
-
}));
|
|
1315
|
-
}
|
|
1316
|
-
return localSessionAnalysisResponse(payload);
|
|
1317
|
-
});
|
|
1318
|
-
server.tool("recap", "Summarize the latest local coding-agent session as a digest, standup, or share-ready recap.", {
|
|
1001
|
+
server.tool("recap", "Get a concise recap of the latest local coding-agent session.", {
|
|
1319
1002
|
format: z.enum(["digest", "standup", "share"]).default("digest"),
|
|
1320
1003
|
sessionId: z.string().optional(),
|
|
1321
1004
|
compact: z.boolean().optional(),
|
|
@@ -1326,6 +1009,9 @@ export function createAskTheWMcpServer(options = {}) {
|
|
|
1326
1009
|
const loginRequired = requireFreeIdentity();
|
|
1327
1010
|
if (loginRequired)
|
|
1328
1011
|
return loginRequired;
|
|
1012
|
+
const limited = consumeLimitedLocalUse("recap");
|
|
1013
|
+
if (limited)
|
|
1014
|
+
return limited;
|
|
1329
1015
|
const scopeKey = currentScopeKey();
|
|
1330
1016
|
const sessionId = payload.sessionId ?? localStore.mostRecentSessionId({ scopeKey });
|
|
1331
1017
|
const signals = sessionId ? localStore.listSignals({ sessionId, scopeKey, limit: 100000 }) : [];
|
|
@@ -1341,19 +1027,18 @@ export function createAskTheWMcpServer(options = {}) {
|
|
|
1341
1027
|
: {}),
|
|
1342
1028
|
}, payload.max_chars);
|
|
1343
1029
|
});
|
|
1344
|
-
server.tool("coach", "
|
|
1345
|
-
scope: z.enum(["session", "week", "patterns"]).default("session"),
|
|
1030
|
+
server.tool("coach", "Get one concise coaching nudge for the local coding-agent session.", {
|
|
1346
1031
|
sessionId: z.string().optional(),
|
|
1347
1032
|
max_chars: maxCharsSchema,
|
|
1348
1033
|
}, async (payload) => {
|
|
1349
|
-
if (payload.scope === "week" || payload.scope === "patterns") {
|
|
1350
|
-
return localResponse(paidFeatureNudge("coach"));
|
|
1351
|
-
}
|
|
1352
1034
|
if (!localStore || mode.mode === "paid")
|
|
1353
1035
|
return localResponse(paidFeatureNudge("coach"));
|
|
1354
1036
|
const loginRequired = requireFreeIdentity();
|
|
1355
1037
|
if (loginRequired)
|
|
1356
1038
|
return loginRequired;
|
|
1039
|
+
const limited = consumeLimitedLocalUse("coach");
|
|
1040
|
+
if (limited)
|
|
1041
|
+
return limited;
|
|
1357
1042
|
const scopeKey = currentScopeKey();
|
|
1358
1043
|
const sessionId = payload.sessionId ?? localStore.mostRecentSessionId({ scopeKey });
|
|
1359
1044
|
const signals = sessionId ? localStore.listSignals({ sessionId, scopeKey, limit: 100000 }) : [];
|
|
@@ -1362,12 +1047,9 @@ export function createAskTheWMcpServer(options = {}) {
|
|
|
1362
1047
|
return budgetedLocalResponse({
|
|
1363
1048
|
ok: true,
|
|
1364
1049
|
tier: "free",
|
|
1365
|
-
scope: "session",
|
|
1366
1050
|
sessionId,
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
failureMode: coaching.failureMode,
|
|
1370
|
-
rendered: `Decision quality score: ${coaching.qualityScore}/100\nBiggest gap: ${coaching.biggestGap}`,
|
|
1051
|
+
nudge: coaching.nudge,
|
|
1052
|
+
rendered: coaching.nudge,
|
|
1371
1053
|
}, payload.max_chars);
|
|
1372
1054
|
});
|
|
1373
1055
|
server.tool("promote_signal_to_decision", "Copy a captured signal summary into a linked local decision.", {
|
|
@@ -1401,6 +1083,21 @@ export function createAskTheWMcpServer(options = {}) {
|
|
|
1401
1083
|
extra: { tool: "promote_signal_to_decision" },
|
|
1402
1084
|
});
|
|
1403
1085
|
}
|
|
1086
|
+
const linkedDecision = localStore.getDecisionForSignal(signal.id);
|
|
1087
|
+
if (linkedDecision) {
|
|
1088
|
+
const decision = localStore.updateDecision(linkedDecision.id, {
|
|
1089
|
+
...(payload.why !== undefined ? { why: payload.why } : {}),
|
|
1090
|
+
status: payload.status,
|
|
1091
|
+
}) ?? linkedDecision;
|
|
1092
|
+
return localResponse({
|
|
1093
|
+
ok: true,
|
|
1094
|
+
id: decision.id,
|
|
1095
|
+
sequence: localStore.stats().decisions,
|
|
1096
|
+
decision: decisionWithSignals(localStore, decision),
|
|
1097
|
+
linkedSignalId: signal.id,
|
|
1098
|
+
reused: true,
|
|
1099
|
+
});
|
|
1100
|
+
}
|
|
1404
1101
|
if (payload.idempotencyKey) {
|
|
1405
1102
|
const existingId = localStore.getMeta(`idempotency:promote_signal_to_decision:${payload.idempotencyKey}`);
|
|
1406
1103
|
if (existingId) {
|
|
@@ -1443,90 +1140,8 @@ export function createAskTheWMcpServer(options = {}) {
|
|
|
1443
1140
|
warnings,
|
|
1444
1141
|
});
|
|
1445
1142
|
});
|
|
1446
|
-
server.tool("list_decision_candidates", "List local signals that look like decision moments and can be promoted.", {
|
|
1447
|
-
sessionId: z.string().optional(),
|
|
1448
|
-
limit: z.number().int().positive().max(300).default(50),
|
|
1449
|
-
cursor: cursorSchema,
|
|
1450
|
-
max_chars: maxCharsSchema,
|
|
1451
|
-
}, async (payload) => {
|
|
1452
|
-
if (mode.mode !== "paid")
|
|
1453
|
-
return localResponse(paidFeatureNudge("list_decision_candidates"));
|
|
1454
|
-
return apiToolResponse(routeWithQuery("/api/signals/decision-candidates", {
|
|
1455
|
-
sessionId: payload.sessionId,
|
|
1456
|
-
limit: payload.limit,
|
|
1457
|
-
cursor: payload.cursor,
|
|
1458
|
-
max_chars: payload.max_chars,
|
|
1459
|
-
}));
|
|
1460
|
-
});
|
|
1461
|
-
server.tool("search_trail", "Search local signals and decisions together.", {
|
|
1462
|
-
query: z.string().min(1),
|
|
1463
|
-
sessionId: z.string().optional(),
|
|
1464
|
-
limit: z.number().int().positive().max(100).default(25),
|
|
1465
|
-
cursor: cursorSchema,
|
|
1466
|
-
compact: z.boolean().optional(),
|
|
1467
|
-
max_chars: maxCharsSchema,
|
|
1468
|
-
}, async (payload) => {
|
|
1469
|
-
if (mode.mode !== "paid")
|
|
1470
|
-
return localResponse(paidFeatureNudge("search_trail"));
|
|
1471
|
-
return apiToolResponse(routeWithQuery("/api/search/trail", {
|
|
1472
|
-
query: payload.query,
|
|
1473
|
-
sessionId: payload.sessionId,
|
|
1474
|
-
limit: payload.limit,
|
|
1475
|
-
cursor: payload.cursor,
|
|
1476
|
-
compact: payload.compact,
|
|
1477
|
-
max_chars: payload.max_chars,
|
|
1478
|
-
}));
|
|
1479
|
-
});
|
|
1480
|
-
server.tool("export_decisions", paidDescription("Export decisions from your workspace.", mode.mode), {
|
|
1481
|
-
format: z.enum(["json", "markdown", "jsonl"]).default("json"),
|
|
1482
|
-
cursor: cursorSchema,
|
|
1483
|
-
limit: z.number().int().positive().max(300).optional(),
|
|
1484
|
-
max_chars: maxCharsSchema,
|
|
1485
|
-
}, async (payload) => mode.mode === "free"
|
|
1486
|
-
? localResponse(paidFeatureNudge("export_decisions"))
|
|
1487
|
-
: apiToolResponse(routeWithQuery("/api/export/timeline", {
|
|
1488
|
-
format: payload.format,
|
|
1489
|
-
cursor: payload.cursor,
|
|
1490
|
-
limit: payload.limit ?? 50,
|
|
1491
|
-
max_chars: payload.max_chars ?? 8000,
|
|
1492
|
-
})));
|
|
1493
|
-
server.tool("view_timeline", "View signals and decisions counts bucketed by session, day, or month.", {
|
|
1494
|
-
scope: z.enum(["day", "month", "session"]).default("day"),
|
|
1495
|
-
range: z.enum(["7D", "30D", "90D", "12M", "CUSTOM"]).default("30D"),
|
|
1496
|
-
start: z.string().optional(),
|
|
1497
|
-
end: z.string().optional(),
|
|
1498
|
-
limit: z.number().int().positive().max(300).optional(),
|
|
1499
|
-
outcomeId: z.string().optional(),
|
|
1500
|
-
decisionStatus: z.enum(["proposed", "committed", "shipped", "abandoned"]).optional(),
|
|
1501
|
-
signalSource: z.string().optional(),
|
|
1502
|
-
max_chars: maxCharsSchema,
|
|
1503
|
-
}, async (payload) => {
|
|
1504
|
-
if (mode.mode === "paid") {
|
|
1505
|
-
return apiToolResponse(routeWithQuery("/api/analytics/timeline-counts", {
|
|
1506
|
-
scope: payload.scope,
|
|
1507
|
-
range: payload.range,
|
|
1508
|
-
start: payload.start,
|
|
1509
|
-
end: payload.end,
|
|
1510
|
-
limit: payload.limit,
|
|
1511
|
-
outcomeId: payload.outcomeId,
|
|
1512
|
-
decisionStatus: payload.decisionStatus,
|
|
1513
|
-
signalSource: payload.signalSource,
|
|
1514
|
-
}));
|
|
1515
|
-
}
|
|
1516
|
-
return localResponse(paidFeatureNudge("view_timeline"));
|
|
1517
|
-
});
|
|
1518
1143
|
return server;
|
|
1519
1144
|
}
|
|
1520
|
-
function renderDecisionDigest(decisions) {
|
|
1521
|
-
if (decisions.length === 0) {
|
|
1522
|
-
return "# Decisions\n\nNo local decisions captured yet.";
|
|
1523
|
-
}
|
|
1524
|
-
return [
|
|
1525
|
-
"# Decisions",
|
|
1526
|
-
"",
|
|
1527
|
-
...decisions.map((decision) => [`## ${decision.headline}`, `- id: ${decision.id}`, `- status: ${decision.status}`, `- created: ${decision.createdAt}`, decision.why ? `- why: ${decision.why}` : "- why: not captured"].join("\n")),
|
|
1528
|
-
].join("\n\n");
|
|
1529
|
-
}
|
|
1530
1145
|
function buildSessionCoach(input) {
|
|
1531
1146
|
const verificationCount = input.signals.filter((signal) => signal.kind === "verification_result").length;
|
|
1532
1147
|
const implementationCount = input.signals.filter((signal) => signal.kind === "implementation_update").length;
|
|
@@ -1536,61 +1151,20 @@ function buildSessionCoach(input) {
|
|
|
1536
1151
|
const hasVerification = verificationCount > 0;
|
|
1537
1152
|
const hasDecision = decisionCount > 0;
|
|
1538
1153
|
const hasDirection = directionCount > 0;
|
|
1539
|
-
const
|
|
1540
|
-
Math.min(25, decisionCount * 12) +
|
|
1541
|
-
(hasVerification ? 25 : 0) +
|
|
1542
|
-
(hasDirection ? 10 : 0) -
|
|
1543
|
-
Math.max(0, implementationCount - decisionCount) * 3));
|
|
1544
|
-
const failureMode = implementationCount >= 3 && verificationCount === 0
|
|
1154
|
+
const nudge = implementationCount >= 3 && verificationCount === 0
|
|
1545
1155
|
? `You captured ${implementationCount} implementation updates but no verification_result; run one check and capture it before ending.`
|
|
1546
1156
|
: input.signals.length >= 6 && finalSummaryCount === 0
|
|
1547
1157
|
? `You captured ${input.signals.length} signals but no final_summary; close the session with the outcome and remaining risk.`
|
|
1548
1158
|
: decisionCount === 0 && directionCount > 0
|
|
1549
1159
|
? "Direction changed, but no decision was captured; promote the clearest direction_change signal."
|
|
1550
|
-
:
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
return { qualityScore, biggestGap, failureMode };
|
|
1559
|
-
}
|
|
1560
|
-
function renderSessionMarkdown(input) {
|
|
1561
|
-
const counts = input.signals.reduce((accumulator, signal) => {
|
|
1562
|
-
accumulator[signal.kind] = (accumulator[signal.kind] ?? 0) + 1;
|
|
1563
|
-
return accumulator;
|
|
1564
|
-
}, {});
|
|
1565
|
-
const activeDecisions = input.decisions.filter((decision) => decision.status !== "abandoned");
|
|
1566
|
-
const coaching = buildSessionCoach({
|
|
1567
|
-
signals: input.signals.map((signal) => ({ ...signal, filesTouched: [], commandsRun: [] })),
|
|
1568
|
-
decisions: input.decisions,
|
|
1569
|
-
});
|
|
1570
|
-
const lines = [
|
|
1571
|
-
"# Session Review",
|
|
1572
|
-
`Session: ${input.sessionId ?? "none"}`,
|
|
1573
|
-
`Signals: ${input.signals.length}`,
|
|
1574
|
-
`Signal kinds: ${Object.entries(counts).map(([kind, count]) => `${kind} ${count}`).join(", ") || "none"}`,
|
|
1575
|
-
`Active decisions: ${activeDecisions.length}`,
|
|
1576
|
-
`Coaching tip: ${coaching.biggestGap}`,
|
|
1577
|
-
"",
|
|
1578
|
-
"## Signals By Kind",
|
|
1579
|
-
...Object.entries(counts)
|
|
1580
|
-
.sort(([left], [right]) => left.localeCompare(right))
|
|
1581
|
-
.map(([kind, count]) => `- ${kind}: ${count}`),
|
|
1582
|
-
"",
|
|
1583
|
-
"## Decision Candidates",
|
|
1584
|
-
...(input.decisionCandidates?.length
|
|
1585
|
-
? input.decisionCandidates.slice(0, 10).map((candidate) => `- signal ${candidate.signalId}: ${candidate.summary} (${candidate.suggestedStatus})`)
|
|
1586
|
-
: ["- none"]),
|
|
1587
|
-
"",
|
|
1588
|
-
"## Active Decisions",
|
|
1589
|
-
...(activeDecisions.length
|
|
1590
|
-
? activeDecisions.map((decision) => `- ${decision.headline} (${decision.status})${decision.why ? ` - ${decision.why}` : ""}`)
|
|
1591
|
-
: ["- none"]),
|
|
1592
|
-
];
|
|
1593
|
-
return lines.slice(0, 300).join("\n");
|
|
1160
|
+
: !hasDecision
|
|
1161
|
+
? "Promote the clearest captured signal into a decision before the trail goes stale."
|
|
1162
|
+
: !hasVerification
|
|
1163
|
+
? "Capture one verification result so the decision trail records whether the work actually held."
|
|
1164
|
+
: implementationCount > decisionCount * 3
|
|
1165
|
+
? "There are many implementation updates per decision; collapse the important why into one decision."
|
|
1166
|
+
: "The trail is usable. Keep the next decision tied to a verification result.";
|
|
1167
|
+
return { nudge };
|
|
1594
1168
|
}
|
|
1595
1169
|
function decisionalWeight(signal) {
|
|
1596
1170
|
const kindWeight = signal.kind === "direction_change" ? 4 : signal.kind === "verification_result" ? 3 : signal.kind === "implementation_update" ? 2 : 1;
|
|
@@ -1709,21 +1283,6 @@ function candidateFromSignal(signal, linkedDecision) {
|
|
|
1709
1283
|
capturedAt: signal.capturedAt,
|
|
1710
1284
|
};
|
|
1711
1285
|
}
|
|
1712
|
-
function listDecisionCandidates(input) {
|
|
1713
|
-
const signals = input.store.listSignals({
|
|
1714
|
-
sessionId: input.sessionId ?? undefined,
|
|
1715
|
-
scopeKey: input.scopeKey,
|
|
1716
|
-
cursor: input.cursor,
|
|
1717
|
-
limit: Math.max(1, Math.min(300, input.limit ?? 50)),
|
|
1718
|
-
});
|
|
1719
|
-
const candidates = signals
|
|
1720
|
-
.map((signal) => candidateFromSignal(signal, input.store.getDecisionForSignal(signal.id)))
|
|
1721
|
-
.filter((candidate) => Boolean(candidate));
|
|
1722
|
-
return {
|
|
1723
|
-
candidates,
|
|
1724
|
-
nextCursor: signals.length >= (input.limit ?? 50) ? signals.at(-1)?.capturedAt ?? null : null,
|
|
1725
|
-
};
|
|
1726
|
-
}
|
|
1727
1286
|
function normalizedDecisionTerms(text) {
|
|
1728
1287
|
const stop = new Set(["the", "and", "for", "with", "this", "that", "from", "into", "keep", "use", "adopt", "remove", "drop", "replace", "defer", "ship", "commit"]);
|
|
1729
1288
|
return String(text ?? "")
|
|
@@ -1768,55 +1327,6 @@ function detectDecisionConflicts(input) {
|
|
|
1768
1327
|
overlappingTerms: entry.overlap.slice(0, 5),
|
|
1769
1328
|
}));
|
|
1770
1329
|
}
|
|
1771
|
-
function searchTrail(input) {
|
|
1772
|
-
const terms = String(input.query ?? "")
|
|
1773
|
-
.toLowerCase()
|
|
1774
|
-
.split(/\s+/)
|
|
1775
|
-
.filter(Boolean);
|
|
1776
|
-
const limit = Math.max(1, Math.min(100, input.limit ?? 25));
|
|
1777
|
-
const haystackMatches = (value) => terms.every((term) => value.toLowerCase().includes(term));
|
|
1778
|
-
const signals = input.store
|
|
1779
|
-
.listSignals({ scopeKey: input.scopeKey, sessionId: input.sessionId ?? undefined, cursor: input.cursor, limit: 100000 })
|
|
1780
|
-
.filter((signal) => haystackMatches([
|
|
1781
|
-
signal.summary,
|
|
1782
|
-
signal.kind,
|
|
1783
|
-
signal.filesTouched.join(" "),
|
|
1784
|
-
signal.commandsRun.join(" "),
|
|
1785
|
-
JSON.stringify(signal.evidence),
|
|
1786
|
-
JSON.stringify(signal.metadata),
|
|
1787
|
-
].join(" ")))
|
|
1788
|
-
.map((signal) => ({
|
|
1789
|
-
type: "signal",
|
|
1790
|
-
id: String(signal.id),
|
|
1791
|
-
createdAt: signal.capturedAt,
|
|
1792
|
-
score: terms.length,
|
|
1793
|
-
result: input.compact ? compactSignal(signal, input.store.getDecisionForSignal(signal.id)) : signalWithDecision(input.store, signal),
|
|
1794
|
-
}));
|
|
1795
|
-
const decisions = input.store
|
|
1796
|
-
.listDecisions({ scopeKey: input.scopeKey, sessionId: input.sessionId ?? undefined, cursor: input.cursor, limit: 100000 })
|
|
1797
|
-
.filter((decision) => haystackMatches([decision.headline, decision.why ?? "", decision.rawContent, decision.files.join(" "), decision.status].join(" ")))
|
|
1798
|
-
.map((decision) => ({
|
|
1799
|
-
type: "decision",
|
|
1800
|
-
id: decision.id,
|
|
1801
|
-
createdAt: decision.createdAt,
|
|
1802
|
-
score: terms.length,
|
|
1803
|
-
result: input.compact
|
|
1804
|
-
? {
|
|
1805
|
-
id: decision.id,
|
|
1806
|
-
headline: decision.headline,
|
|
1807
|
-
status: decision.status,
|
|
1808
|
-
signalIds: decision.sourceSignalIds,
|
|
1809
|
-
}
|
|
1810
|
-
: decisionWithSignals(input.store, decision),
|
|
1811
|
-
}));
|
|
1812
|
-
const matches = [...signals, ...decisions]
|
|
1813
|
-
.sort((left, right) => right.createdAt.localeCompare(left.createdAt))
|
|
1814
|
-
.slice(0, limit);
|
|
1815
|
-
return {
|
|
1816
|
-
matches,
|
|
1817
|
-
nextCursor: matches.length >= limit ? matches.at(-1)?.createdAt ?? null : null,
|
|
1818
|
-
};
|
|
1819
|
-
}
|
|
1820
1330
|
const isDirectIndexExecution = Boolean(process.argv[1]) &&
|
|
1821
1331
|
(() => {
|
|
1822
1332
|
const modulePath = fileURLToPath(import.meta.url);
|