@polycode-projects/the-mechanical-code-talker 0.9.4 → 0.9.6
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/ROADMAP.md +11 -6
- package/bin/tmct.mjs +11 -2
- package/package.json +1 -1
- package/src/ask-vocab.mjs +1 -0
- package/src/chat.mjs +376 -48
package/ROADMAP.md
CHANGED
|
@@ -14,8 +14,9 @@ the file has been deleted.
|
|
|
14
14
|
|
|
15
15
|
## Where we are now (2026-07-07)
|
|
16
16
|
|
|
17
|
-
`npm test` green (**
|
|
18
|
-
`HANDOVER.md` for the exact release
|
|
17
|
+
`npm test` green (**1042**). **v0.9.5, pushed** (0.8.2 → 0.9.0 for this session's main wave, then
|
|
18
|
+
one patch per shipped playtest-sprint fix — see `HANDOVER.md` for the exact release chain and the
|
|
19
|
+
"Playtest sprint" section there for what each patch fixed).
|
|
19
20
|
|
|
20
21
|
### Now: shipped this session
|
|
21
22
|
|
|
@@ -26,7 +27,7 @@ the file has been deleted.
|
|
|
26
27
|
- **A new feature: predicate-based "find" queries.** "find me the payment class" now works:
|
|
27
28
|
type-filtered, fuzzy property-surface matching, with a narrow-then-broaden inheritance-aware
|
|
28
29
|
cascade and a boolean-fold generalization for compositional predicate queries. Design:
|
|
29
|
-
`PLAN_PREDICATE_QUERIES.md`.
|
|
30
|
+
`archive/PLAN_PREDICATE_QUERIES.md`.
|
|
30
31
|
- **Two research tracks landed as code.** The ontology plan's two inert synonym resources
|
|
31
32
|
(ConceptNet synonym/similar-to rows, phrasebook synonym families) are now wired into query-time
|
|
32
33
|
matching, and the disjointness premise set plus numeric vocabulary grew
|
|
@@ -41,7 +42,7 @@ the file has been deleted.
|
|
|
41
42
|
end to end, and surfaced a real goal-reasoner honesty gap (Bug 8, below).
|
|
42
43
|
- **A session-loop fix.** A throwing turn no longer aborts a piped/non-interactive session.
|
|
43
44
|
- **4 new plan docs.** `PLAN_ontology-hierarchies.md`, `PLAN_INFERENCE_TESTING.md` (revised so
|
|
44
|
-
infbench generation is mechanical, not hand-authored), `PLAN_PREDICATE_QUERIES.md`, and
|
|
45
|
+
infbench generation is mechanical, not hand-authored), `archive/PLAN_PREDICATE_QUERIES.md`, and
|
|
45
46
|
`PLAN_CODE.md` (new, program synthesis over tmct's closed DSLs, gated on explicit operator
|
|
46
47
|
sign-off per track, not built yet).
|
|
47
48
|
|
|
@@ -62,8 +63,12 @@ In priority order (full detail and measured targets in `HANDOVER.md`):
|
|
|
62
63
|
5. **`PLAN_CODE.md`'s sign-off decision.** Track 1 (rule/frame synthesis) is the lowest-risk
|
|
63
64
|
candidate; decide with the operator whether to greenlight it.
|
|
64
65
|
6. Smaller chat-feel residuals from the 0.8.2 confirmation playtest, the Track-1 trio (pronoun,
|
|
65
|
-
temporal, discourse-count, measured red sets), `edgesOfKind` memoization for monorepo-scale
|
|
66
|
-
latency
|
|
66
|
+
temporal, discourse-count, measured red sets), and `edgesOfKind` memoization for monorepo-scale
|
|
67
|
+
latency. (The version bump plus push is done — see "Now" above.)
|
|
68
|
+
7. **`SKILL_PLAYTEST_SPRINT.md`, in progress.** A capped, delegated, chained playtest loop (each
|
|
69
|
+
round a background chat session against `examples/mini-webapp`, appraised and fixed+shipped
|
|
70
|
+
live). Rounds 1-3 shipped 3 real fixes (0.9.3-0.9.5); cap raised from 3 to 8 rounds mid-run;
|
|
71
|
+
continuing.
|
|
67
72
|
|
|
68
73
|
### Later: deferred by design, staged inside each plan
|
|
69
74
|
|
package/bin/tmct.mjs
CHANGED
|
@@ -35,6 +35,10 @@ Usage:
|
|
|
35
35
|
tmct interactive chat (the headline surface)
|
|
36
36
|
tmct chat [--repo <abs>] chat over a specific repo's graph
|
|
37
37
|
[--ephemeral] read the graph but write nothing back (demo/read-only)
|
|
38
|
+
[--narrate] start with narrate mode on — a verbose, developer-facing
|
|
39
|
+
trace of decision points/matched pattern/results/goal per
|
|
40
|
+
turn, appended under a "--- narrate ---" marker (also
|
|
41
|
+
TMCT_NARRATE=1; toggle mid-session with /narrate on|off)
|
|
38
42
|
[--plain] force the plain readline shell (the default when
|
|
39
43
|
stdin/stdout is not a terminal)
|
|
40
44
|
tmct memory [--repo <abs>] what tmct remembers: facts, utterances, sessions,
|
|
@@ -277,16 +281,21 @@ async function main() {
|
|
|
277
281
|
// code graph, no .tmct/memory dropped under it. A demo you can run repeatedly
|
|
278
282
|
// on a checked-in example without ever dirtying it.
|
|
279
283
|
const ephemeral = rest.includes("--ephemeral");
|
|
284
|
+
// `--narrate` (or TMCT_NARRATE=1, read directly by createSession from
|
|
285
|
+
// process.env — no extra wiring needed for the env-var form): start the
|
|
286
|
+
// session with the verbose developer/debug narrate mode already on. Default
|
|
287
|
+
// OFF; `/narrate on`/`/narrate off` also toggles it mid-session.
|
|
288
|
+
const narrate = rest.includes("--narrate");
|
|
280
289
|
// The shell gate: a real terminal gets the full-screen Ink TUI; `--plain` or a
|
|
281
290
|
// non-TTY stream (pipes, scripts, the test suite) gets the readline shell. Both
|
|
282
291
|
// drive the same createSession sink — only the drawing differs.
|
|
283
292
|
const plain = rest.includes("--plain") || !process.stdin.isTTY || !process.stdout.isTTY;
|
|
284
293
|
if (plain) {
|
|
285
294
|
const { runChat } = await import("../src/chat.mjs");
|
|
286
|
-
await runChat({ repoPath, ephemeral });
|
|
295
|
+
await runChat({ repoPath, ephemeral, narrate });
|
|
287
296
|
} else {
|
|
288
297
|
const { runTui } = await import("../src/tui/app.mjs");
|
|
289
|
-
await runTui({ repoPath, ephemeral });
|
|
298
|
+
await runTui({ repoPath, ephemeral, narrate });
|
|
290
299
|
}
|
|
291
300
|
return;
|
|
292
301
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@polycode-projects/the-mechanical-code-talker",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.6",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "The Mechanical Code Talker (tmct) — a tolerant, offline, $0 chat surface that guides you toward precision queries about a software repository. ELIZA/PARRY-style but domain-obsessed with code. No model calls; no codebase index of its own.",
|
package/src/ask-vocab.mjs
CHANGED
|
@@ -380,6 +380,7 @@ export const FILLER_WORDS = Object.freeze([
|
|
|
380
380
|
"um", "uh", "erm", "so", "like", "yo", "hey", "bru", "bro", "fam", "mate",
|
|
381
381
|
"please", "could you", "can you", "would you", "tell me", "i wonder",
|
|
382
382
|
"just wondering", "quickly", "real quick", "kinda", "sorta",
|
|
383
|
+
"btw", "by the way",
|
|
383
384
|
]);
|
|
384
385
|
|
|
385
386
|
/** Deictic/pronoun terms that refer to a context entity rather than naming one
|
package/src/chat.mjs
CHANGED
|
@@ -79,6 +79,144 @@ const ASK_ENVELOPE_DELIM = "\n\n---tmct_ask---\n";
|
|
|
79
79
|
const CONTEXT_WORDS = new Set(["it", "this", "that", "here"]);
|
|
80
80
|
const isPronoun = (s) => CONTEXT_WORDS.has(String(s || "").trim().toLowerCase());
|
|
81
81
|
|
|
82
|
+
// ---- narrate mode (opt-in, developer/debug-facing) -------------------------
|
|
83
|
+
// "in the tmct interface let's be a lot more verbose, you and I are the only
|
|
84
|
+
// users" — a narrative of decision points, the matched pattern, the results +
|
|
85
|
+
// sources, and a deduced per-turn goal, appended to the answer. OFF by default
|
|
86
|
+
// (a `/narrate on`/`/narrate off` toggle, or a `--narrate`/TMCT_NARRATE=1
|
|
87
|
+
// session start): the DEFAULT (narrate:false) path must stay byte-identical
|
|
88
|
+
// to before this feature existed, so every site below is a cheap `trace?.push`
|
|
89
|
+
// no-op when tracing is off — runTurn only allocates the `trace` array at all
|
|
90
|
+
// when narrate is true. Design: a single mutable `trace` array threaded
|
|
91
|
+
// through runTurn -> runAsk/runCommand/conversationalTurn (via `ctx.trace`);
|
|
92
|
+
// each stage pushes plain, already-formatted lines tagged with their own
|
|
93
|
+
// category prefix ("goal:", "lane:", "pattern:", "result:", "source:",
|
|
94
|
+
// "intermediate:") — the trace array IS the narrative, in decision order; no
|
|
95
|
+
// separate structured side-channel to keep in sync. renderNarration (below,
|
|
96
|
+
// next to runTurn) buckets by that prefix into the sections the operator
|
|
97
|
+
// asked for and appends the block under NARRATE_MARKER, AFTER finish() and
|
|
98
|
+
// OUTSIDE of `last.answer` — so a narrated turn's repeat-detection / why-
|
|
99
|
+
// re-render logic (which compares `last.answer` bytes) is unaffected by
|
|
100
|
+
// whether narrate happens to be on.
|
|
101
|
+
export const NARRATE_MARKER = "--- narrate ---";
|
|
102
|
+
|
|
103
|
+
/** Push one narrative line, only when tracing is on (`trace` is the mutable
|
|
104
|
+
* array runTurn allocates for a narrate:true turn, else null/undefined). */
|
|
105
|
+
function note(trace, text) { if (trace) trace.push(text); }
|
|
106
|
+
|
|
107
|
+
/** relation `kind` (ask-vocab.mjs RELATIONS) -> a short, deterministic
|
|
108
|
+
* statement of what a person asking that KIND of question is probably after.
|
|
109
|
+
* Deliberately a small, honest bucket lookup over the query SHAPE the engine
|
|
110
|
+
* already computed — tmct is no-LLM, so goal deduction is table-driven, never
|
|
111
|
+
* free-text generation. A kind/shape this table doesn't recognise falls
|
|
112
|
+
* through to a generic line in deduceGoalFromParsed, never a fabricated guess. */
|
|
113
|
+
const GOAL_BY_KIND = {
|
|
114
|
+
imports: "understand a dependency/import relationship",
|
|
115
|
+
uses: "understand a dependency/usage relationship (imports and/or calls)",
|
|
116
|
+
calls: "understand a call relationship",
|
|
117
|
+
callsSymbol: "understand a call relationship",
|
|
118
|
+
defines: "locate what a module/class defines",
|
|
119
|
+
contains: "understand class membership (methods/attributes)",
|
|
120
|
+
tests: "assess test coverage",
|
|
121
|
+
inherits: "understand a class hierarchy/inheritance relationship",
|
|
122
|
+
touches: "understand commit/change history",
|
|
123
|
+
touchesSymbol: "understand commit/change history",
|
|
124
|
+
cochange: "understand change-coupling between modules",
|
|
125
|
+
reexports: "understand a module's public exports/API surface",
|
|
126
|
+
};
|
|
127
|
+
const goalNoun = (entityType) => (entityType ? `${String(entityType).toLowerCase()}(s)` : "entities");
|
|
128
|
+
|
|
129
|
+
/** Deduce a one-line goal statement from the ask engine's parsed AST — either
|
|
130
|
+
* the plain-clause form ({shape,kind,entityType,object[,subject]}) or the
|
|
131
|
+
* compositional form ({node:...}, ask.mjs's §compositional grammar). Returns
|
|
132
|
+
* null when there's nothing to bucket on (no parse stood at all); the caller
|
|
133
|
+
* supplies its own honest "didn't resolve" wording in that case. */
|
|
134
|
+
function deduceGoalFromParsed(parsed) {
|
|
135
|
+
if (!parsed) return null;
|
|
136
|
+
const { node, shape, kind } = parsed;
|
|
137
|
+
if (node === "find") return `locate a specific named entity ("${parsed.term}")`;
|
|
138
|
+
if (node === "count") return `get a count of ${goalNoun(parsed.entityType)}`;
|
|
139
|
+
if (node === "list") return `list/enumerate ${goalNoun(parsed.entityType)} matching a condition`;
|
|
140
|
+
if (node === "superlative") return `rank/compare ${goalNoun(parsed.entityType)} by ${parsed.metricNoun || parsed.metric || "a metric"}`;
|
|
141
|
+
if (node === "anaphora") return "follow up on the previous answer's result set (discourse anaphora)";
|
|
142
|
+
if (node === "membership") return `understand "${parsed.term || "an entity"}"'s membership/relationship`;
|
|
143
|
+
if (node === "clause") return deduceGoalFromParsed(parsed.clause);
|
|
144
|
+
if (node === "miss") return null;
|
|
145
|
+
if (node === "boolean" || node === "qualifier" || node === "reverseSet" || node === "forwardSet" || node === "allOfClass" || node === "temporal") {
|
|
146
|
+
const k = kind || parsed.inner?.kind;
|
|
147
|
+
return k && GOAL_BY_KIND[k] ? GOAL_BY_KIND[k] : `filter/traverse ${goalNoun(parsed.entityType)} by a relationship`;
|
|
148
|
+
}
|
|
149
|
+
// plain (non-compositional) clause
|
|
150
|
+
if (shape === "meta") return `understand a vocabulary/definition term ("${parsed.object}")`;
|
|
151
|
+
if (shape === "where") return `locate where something is defined ("${parsed.object}")`;
|
|
152
|
+
if (shape === "when") return "understand when something last changed (history)";
|
|
153
|
+
if (shape === "mentions") return `find where something is mentioned in prose ("${parsed.object}")`;
|
|
154
|
+
if (shape === "ask") return (kind && GOAL_BY_KIND[kind]) || "check a specific subject/object relationship";
|
|
155
|
+
if ((shape === "reverse" || shape === "forward") && kind) return GOAL_BY_KIND[kind] || `understand a "${kind}" relationship`;
|
|
156
|
+
return "understand a graph relationship";
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Split the collected trace into buckets by its own leading category tag, so
|
|
160
|
+
* renderNarration can group like with like while the trace array itself stays
|
|
161
|
+
* a flat, chronological narrative — no structured side-channel to keep in
|
|
162
|
+
* sync with the notes pushed at each call site. */
|
|
163
|
+
function bucketTrace(trace) {
|
|
164
|
+
const buckets = { goal: [], lane: [], pattern: [], result: [], source: [], intermediate: [] };
|
|
165
|
+
const other = [];
|
|
166
|
+
for (const line of trace) {
|
|
167
|
+
const m = /^([a-z]+):\s/.exec(String(line));
|
|
168
|
+
if (m && buckets[m[1]]) buckets[m[1]].push(line); else other.push(line);
|
|
169
|
+
}
|
|
170
|
+
return { ...buckets, other };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Render the collected trace into the human-readable block appended to a
|
|
174
|
+
* narrated turn's answer (see runTurn's withLast — this runs AFTER finish()
|
|
175
|
+
* and never touches `last.answer`). `fallbackGoal` covers turn types that
|
|
176
|
+
* push no "goal:" note of their own (a bare slash-command, a count, an
|
|
177
|
+
* assert) with a generic via-derived line — every narrated turn gets a goal
|
|
178
|
+
* line, never a silent gap. */
|
|
179
|
+
function renderNarration(trace, { record, detail, fallbackGoal }) {
|
|
180
|
+
const b = bucketTrace(trace);
|
|
181
|
+
const lines = [NARRATE_MARKER];
|
|
182
|
+
lines.push(...(b.goal.length ? b.goal : [`goal: ${fallbackGoal}`]));
|
|
183
|
+
lines.push(`decision: via=${record.via || "?"}${record.command ? ` command=/${record.command}` : ""}${record.miss ? " (miss)" : ""}`);
|
|
184
|
+
lines.push(...b.lane, ...b.pattern);
|
|
185
|
+
if (detail?.traversal) lines.push(`result: traversal — ${detail.traversal}`);
|
|
186
|
+
if (Array.isArray(detail?.matches) && detail.matches.length) {
|
|
187
|
+
const shown = detail.matches.slice(0, 5).map((m) => `${m.label}${m.type ? ` [${m.type}]` : ""}`);
|
|
188
|
+
lines.push(`result: ${detail.matches.length} match(es) — ${shown.join(", ")}${detail.matches.length > shown.length ? ", …" : ""}`);
|
|
189
|
+
}
|
|
190
|
+
if (Array.isArray(record.resolvedIds) && record.resolvedIds.length) {
|
|
191
|
+
lines.push(`result: resolved entity id(s) — ${record.resolvedIds.join(", ")}`);
|
|
192
|
+
}
|
|
193
|
+
if (Array.isArray(record.answeredIds) && record.answeredIds.length && record.answeredIds.length !== (detail?.matches?.length || 0)) {
|
|
194
|
+
lines.push(`result: answered entity id(s) — ${record.answeredIds.join(", ")}`);
|
|
195
|
+
}
|
|
196
|
+
lines.push(...b.result, ...b.source, ...b.intermediate, ...b.other);
|
|
197
|
+
return lines.join("\n");
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Append the narrate-mode block to a turn's OUTWARD-FACING answer/logLines —
|
|
201
|
+
* used at every runTurn return site, AFTER finish() (or, for a conversational
|
|
202
|
+
* turn, after its own render). Deliberately never touches `last.answer` /
|
|
203
|
+
* `last.detail` (the caller builds `last` from the PRE-narration `result`) so
|
|
204
|
+
* a narrated turn's own repeat-detection and why/say-more re-render (both of
|
|
205
|
+
* which compare `last.answer` bytes — see ORIENTATION_REPEAT_ONELINER and
|
|
206
|
+
* renderVerbose) see the exact same text a narrate:false run would have
|
|
207
|
+
* produced; narrate is purely additive to what's PRINTED, never to what's
|
|
208
|
+
* REMEMBERED. No-op (returns `result` unchanged, by reference) when `trace`
|
|
209
|
+
* is null (narrate off) or empty (nothing was traced). */
|
|
210
|
+
function withNarration(result, trace, fallbackGoal) {
|
|
211
|
+
if (!trace || !trace.length) return result;
|
|
212
|
+
const narrative = renderNarration(trace, { record: result.record, detail: result.detail, fallbackGoal });
|
|
213
|
+
const answer = `${result.answer}\n\n${narrative}`;
|
|
214
|
+
const logLines = Array.isArray(result.logLines)
|
|
215
|
+
? result.logLines.map((l) => (l === result.answer ? answer : l))
|
|
216
|
+
: result.logLines;
|
|
217
|
+
return { ...result, answer, logLines };
|
|
218
|
+
}
|
|
219
|
+
|
|
82
220
|
/** Slash-command → (dispatchTool name, arg key). Arg keys are the EXACT ones the
|
|
83
221
|
* server.mjs dispatchTool switch reads (members/subclasses take `class`;
|
|
84
222
|
* impact/exports take `module`; architecture takes `package`; search takes
|
|
@@ -517,26 +655,48 @@ function conversationalTurn(line, ctx) {
|
|
|
517
655
|
...(end ? { end: true } : {}),
|
|
518
656
|
};
|
|
519
657
|
};
|
|
520
|
-
if (BYE.has(q))
|
|
658
|
+
if (BYE.has(q)) {
|
|
659
|
+
note(ctx.trace, "goal: casual/social — ending the session (no graph intent)");
|
|
660
|
+
note(ctx.trace, "lane: conversational — farewell (BYE closed set)");
|
|
661
|
+
return mk(t(T_FAREWELL), { end: true });
|
|
662
|
+
}
|
|
521
663
|
if (WHY.has(q)) {
|
|
664
|
+
note(ctx.trace, "goal: elaborate on the previous answer (why/say-more)");
|
|
665
|
+
note(ctx.trace, "lane: conversational — why/say-more (WHY closed set)");
|
|
522
666
|
const v = renderVerbose(ctx.last);
|
|
523
667
|
// The empty-state hint is template wording (via:"template", the data row wins;
|
|
524
668
|
// renderVerbose's own string is the degraded fallback for direct library callers).
|
|
525
669
|
// A real expansion re-renders the LAST ANSWER — its wording is the prior answer's,
|
|
526
670
|
// not a template's, so it carries via:"conversational".
|
|
527
|
-
if (v.empty)
|
|
671
|
+
if (v.empty) {
|
|
672
|
+
note(ctx.trace, "intermediate: no previous answer held on ctx.last — nothing to expand");
|
|
673
|
+
return mk(tRender(ctx.templates, T_WHY_EMPTY) ?? v.text, { miss: true });
|
|
674
|
+
}
|
|
675
|
+
note(ctx.trace, `result: re-rendering the previous answer to "${ctx.last?.query ?? "?"}" verbosely`);
|
|
528
676
|
return mk(v.text, { via: "conversational" });
|
|
529
677
|
}
|
|
530
678
|
if (GREET.has(q)) {
|
|
679
|
+
note(ctx.trace, "goal: casual/social — greeting, no graph intent");
|
|
680
|
+
note(ctx.trace, "lane: conversational — greeting (GREET closed set)");
|
|
531
681
|
// #3 empty/degenerate-graph greeting: a plain "hi"/"hello" over a graph with 0
|
|
532
682
|
// modules orients toward --repo/tmct init instead of over-promising "ask me
|
|
533
683
|
// about this codebase". Phrase-specific variants (good morning, hello there)
|
|
534
684
|
// keep their wording; only the default greeting swaps.
|
|
535
685
|
const id = (!T_GREETING_BY_PHRASE[q] && noCodeGraph(ctx.graph)) ? T_GREETING_EMPTY : (T_GREETING_BY_PHRASE[q] || T_GREETING);
|
|
686
|
+
note(ctx.trace, `pattern: template "${id}" (data/templates/responses.jsonl)`);
|
|
536
687
|
return mk(t(id));
|
|
537
688
|
}
|
|
538
|
-
if (THANKS.has(q))
|
|
539
|
-
|
|
689
|
+
if (THANKS.has(q)) {
|
|
690
|
+
note(ctx.trace, "goal: casual/social — acknowledgement, no graph intent");
|
|
691
|
+
note(ctx.trace, "lane: conversational — thanks/acknowledgement (THANKS closed set)");
|
|
692
|
+
note(ctx.trace, `pattern: template "${T_THANKS}" (data/templates/responses.jsonl)`);
|
|
693
|
+
return mk(t(T_THANKS));
|
|
694
|
+
}
|
|
695
|
+
if (q === "help" || q === "?" || HELP_PHRASES.some((re) => re.test(raw))) {
|
|
696
|
+
note(ctx.trace, "goal: get oriented — what can tmct answer, how do I start");
|
|
697
|
+
note(ctx.trace, "lane: conversational — help/orientation (HELP_PHRASES / bare help / ?)");
|
|
698
|
+
return mk(orientationAnswer(ctx.templates, ctx.graph));
|
|
699
|
+
}
|
|
540
700
|
return null;
|
|
541
701
|
}
|
|
542
702
|
|
|
@@ -1154,6 +1314,7 @@ export async function helpText() {
|
|
|
1154
1314
|
["/stats", "a one-screen overview: entity counts, relationship counts, packages"],
|
|
1155
1315
|
["/memory [verbose]", "what tmct remembers: facts, utterances, sessions, folded blocks"],
|
|
1156
1316
|
["/focus <symbol>", "set the current focus (reused by 'it'/'this' and no-arg entity commands)"],
|
|
1317
|
+
["/narrate on|off", "verbose developer/debug mode: decision points, matched pattern, results+sources, goal per turn"],
|
|
1157
1318
|
["/help", "this list"],
|
|
1158
1319
|
["/exit", "leave the session (also Ctrl+C / Ctrl+D)"],
|
|
1159
1320
|
];
|
|
@@ -2062,9 +2223,12 @@ function relationTermOf(query, envelope) {
|
|
|
2062
2223
|
* (normalize.mjs). Deliberately used only as a LAST-RESORT lane (see its call
|
|
2063
2224
|
* site below) — "tell me about X" is ALSO the relation/concept force's own
|
|
2064
2225
|
* trigger phrase for enumerable concepts ("tell me about inheritance"), so
|
|
2065
|
-
* this must never run before those have had their chance.
|
|
2226
|
+
* this must never run before those have had their chance. Trails an optional
|
|
2227
|
+
* "please" as well as "for me" (playtest sprint round 3): this lane reads the
|
|
2228
|
+
* RAW turn text, not normalize.mjs's FILLER_WORDS-stripped one, so "could you
|
|
2229
|
+
* tell me more about Router please" needs its own trailing-politeness strip. */
|
|
2066
2230
|
const DESCRIBE_WRAPPER_RE =
|
|
2067
|
-
/^(?:(?:can|could|would)\s+you\s+(?:please\s+)?|please\s+)?(?:tell\s+me\s+(?:more\s+)?about|describe)\s+(.+?)(?:\s+for\s+me)?\s*\??$/i;
|
|
2231
|
+
/^(?:(?:can|could|would)\s+you\s+(?:please\s+)?|please\s+)?(?:tell\s+me\s+(?:more\s+)?about|describe)\s+(.+?)(?:\s+for\s+me)?(?:\s+please)?\s*\??$/i;
|
|
2068
2232
|
|
|
2069
2233
|
async function describeWrapperAnswer(query, { config, source }) {
|
|
2070
2234
|
const m = DESCRIBE_WRAPPER_RE.exec(String(query || "").trim());
|
|
@@ -2166,7 +2330,7 @@ async function conceptForceAnswer(query, envelope, { graph, config, source, memo
|
|
|
2166
2330
|
* otherwise the unchanged dispatchTool path (which also yields the no-graph error).
|
|
2167
2331
|
* A hit updates the focus to the resolved object. Grammar miss / ToolError → a
|
|
2168
2332
|
* normal answer, never a crash. */
|
|
2169
|
-
async function runAsk(query, { config, source, graph, focus, last, templates, memoryDir, sessionId = "", lexicon = null, env }) {
|
|
2333
|
+
async function runAsk(query, { config, source, graph, focus, last, templates, memoryDir, sessionId = "", lexicon = null, env, trace }) {
|
|
2170
2334
|
const ts = new Date().toISOString();
|
|
2171
2335
|
// DISCOURSE ANAPHORA (CHATBENCH_006 levers 1+2): a follow-up like "which of those
|
|
2172
2336
|
// are tested" / "how many of those" / "count them" filters or counts the PREVIOUS
|
|
@@ -2186,7 +2350,10 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
2186
2350
|
// W2: the explicit recall forms are answered from memory's folded blocks, never
|
|
2187
2351
|
// the graph. Gated on memoryDir — a bare runTurn (no session shell) stays pure.
|
|
2188
2352
|
if (memoryDir && RECALL_ASK_RE.test(String(query).trim())) {
|
|
2353
|
+
note(trace, "goal: recall what was discussed earlier (explicit recall phrasing)");
|
|
2354
|
+
note(trace, "lane: RECALL_ASK_RE matched — answered from folded-session memory, never the graph");
|
|
2189
2355
|
const summary = await recallSummary(memoryDir);
|
|
2356
|
+
note(trace, summary ? "source: memory/fold.mjs recallSummary" : "intermediate: no folded session blocks yet — nothing to recall");
|
|
2190
2357
|
return plainTurn(query, summary ?? "nothing to recall yet — no earlier session has been folded into memory.", {
|
|
2191
2358
|
via: "recall", miss: !summary, focus,
|
|
2192
2359
|
});
|
|
@@ -2211,7 +2378,31 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
2211
2378
|
if (envJson) { try { envelope = JSON.parse(envJson); } catch { envelope = null; } }
|
|
2212
2379
|
} catch (e) {
|
|
2213
2380
|
answer = String(e?.message || e);
|
|
2214
|
-
|
|
2381
|
+
note(trace, `intermediate: the ask engine threw — ${answer}`);
|
|
2382
|
+
}
|
|
2383
|
+
// NARRATE: the direct parse/traversal receipt, straight off ask()'s own envelope
|
|
2384
|
+
// (§6.2) — this alone covers most of "the version of prompt that matched" and
|
|
2385
|
+
// "intermediate information" with ZERO extra instrumentation of ask.mjs: `parsed`
|
|
2386
|
+
// is the compiled AST (shape/kind/entityType or a compositional {node:...}),
|
|
2387
|
+
// `relaxed` is the FULL relaxation-cascade trace (what noise/unmatched tokens the
|
|
2388
|
+
// engine stripped/corrected before it found an answerable parse — exactly the
|
|
2389
|
+
// "almost resolved but failed" near-miss detail a playtest debugging session
|
|
2390
|
+
// wants), and `matchedVia` names the confidence tier (prose/fuzzy) a resolution
|
|
2391
|
+
// fell through to.
|
|
2392
|
+
if (envelope?.parsed) {
|
|
2393
|
+
const p = envelope.parsed;
|
|
2394
|
+
const shape = p.node ? `node=${p.node}` : `shape=${p.shape}`;
|
|
2395
|
+
note(trace, `pattern: parsed AST — ${shape}${p.kind ? ` kind=${p.kind}` : ""}${p.entityType ? ` entityType=${p.entityType}` : ""}${p.object != null ? ` object="${p.object}"` : ""}${p.term != null ? ` term="${p.term}"` : ""}`);
|
|
2396
|
+
} else {
|
|
2397
|
+
note(trace, "pattern: no parse stood (direct grammar miss — every registered strategy declined)");
|
|
2398
|
+
}
|
|
2399
|
+
if (envelope?.relaxed) {
|
|
2400
|
+
const r = envelope.relaxed;
|
|
2401
|
+
note(trace, `intermediate: the direct parse missed — the relaxation cascade rescued it: "${r.from}" -> "${r.to}"${r.dropped?.length ? ` (dropped: ${r.dropped.join(", ")})` : ""}`);
|
|
2402
|
+
if (r.steps?.length) note(trace, `intermediate: relaxation steps — ${r.steps.join(" | ")}`);
|
|
2403
|
+
}
|
|
2404
|
+
if (envelope?.matchedVia) note(trace, `source: term resolved via the "${envelope.matchedVia}" confidence tier (not a literal identifier match)`);
|
|
2405
|
+
if (envelope?.ambiguous) note(trace, "intermediate: the resolved term was AMBIGUOUS — multiple candidates matched, see the answer's disambiguation prompt");
|
|
2215
2406
|
// A grammar miss has parsed:null → stays []. An empty-RESULT query (object resolved,
|
|
2216
2407
|
// no edges) still records the resolved subject — that IS the asksAbout signal, and
|
|
2217
2408
|
// it becomes the new focus so a follow-up "what calls it" can reuse it.
|
|
@@ -2230,6 +2421,9 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
2230
2421
|
// Class-gate the focus update: a Commit/Session/schema object never displaces a
|
|
2231
2422
|
// standing code-entity focus (see nextFocus).
|
|
2232
2423
|
newFocus = nextFocus(graph, focus, ent);
|
|
2424
|
+
note(trace, `result: resolved object "${obj}" -> ${ent.label} (${ent.id}) — becomes the new focus`);
|
|
2425
|
+
} else if (!isPronoun(obj)) {
|
|
2426
|
+
note(trace, `intermediate: object "${obj}" did NOT resolve to a graph entity — this is why an otherwise-parsed query still misses`);
|
|
2233
2427
|
}
|
|
2234
2428
|
}
|
|
2235
2429
|
const answeredIds = (envelope?.matches || []).map((m) => m?.id).filter(Boolean);
|
|
@@ -2239,6 +2433,17 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
2239
2433
|
let via = "composed";
|
|
2240
2434
|
let recordMiss = miss;
|
|
2241
2435
|
let factPending = null; // a truncated fact listing's held remainder (for "more" paging)
|
|
2436
|
+
// GOAL DEDUCTION: from the parsed AST when one stood (deterministic, table-driven —
|
|
2437
|
+
// see deduceGoalFromParsed); a total grammar miss (no parse at all) gets the honest
|
|
2438
|
+
// "didn't resolve" goal line verbatim, matching the operator's own wording for that
|
|
2439
|
+
// case. Pushed once, EARLY (before the miss cascade below may go on to answer via a
|
|
2440
|
+
// completely different lane — an intent lane's own goal note, when it pushes one,
|
|
2441
|
+
// stays the more specific of the two since bucketTrace keeps every "goal:" line and
|
|
2442
|
+
// renderNarration shows them all, most-specific-last-written).
|
|
2443
|
+
{
|
|
2444
|
+
const deduced = deduceGoalFromParsed(envelope?.parsed);
|
|
2445
|
+
note(trace, `goal: ${deduced ?? "unclear — the phrasing didn't resolve to a known query shape"}`);
|
|
2446
|
+
}
|
|
2242
2447
|
// MISS handling. The intent lanes + short-miss are RECOGNIZER-gated on the query
|
|
2243
2448
|
// text AND only consulted on a would-miss, so a real graph query — a hit, an honest
|
|
2244
2449
|
// empty with a receipt, a fuzzy repair — is never hijacked. Order: (1) META/SELF
|
|
@@ -2251,7 +2456,10 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
2251
2456
|
// fact-dump readers so "what do you know" gets a summary, not raw facts.
|
|
2252
2457
|
if (miss) {
|
|
2253
2458
|
const meta = await metaLane(query, { graph, memoryDir });
|
|
2254
|
-
if (meta) {
|
|
2459
|
+
if (meta) {
|
|
2460
|
+
answer = meta.text; via = meta.via; recordMiss = false; handled = true;
|
|
2461
|
+
note(trace, `lane: (1) META/SELF — bare self/session question recognized, answered via="${meta.via}"`);
|
|
2462
|
+
}
|
|
2255
2463
|
}
|
|
2256
2464
|
if (!handled && miss && isConversational(query)) {
|
|
2257
2465
|
// A conversational miss (a greeting, "what can you do", a very short non-code
|
|
@@ -2261,8 +2469,11 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
2261
2469
|
// identical orientation-class turn used to repeat the full blurb verbatim —
|
|
2262
2470
|
// collapse to a one-liner on that repeat, mirroring WALL_REPEAT_ONELINER.
|
|
2263
2471
|
const orientation = orientationAnswer(templates, graph);
|
|
2264
|
-
|
|
2472
|
+
const repeat = last?.answer === orientation;
|
|
2473
|
+
answer = repeat ? ORIENTATION_REPEAT_ONELINER : orientation;
|
|
2265
2474
|
via = "template"; handled = true;
|
|
2475
|
+
note(trace, `lane: (2) conversational orientation — isConversational() matched a would-miss; ${repeat ? "REPEAT collapsed to one-liner" : "full orientation card"}`);
|
|
2476
|
+
note(trace, "goal: casual/social or too-short-to-be-structural — no graph intent");
|
|
2266
2477
|
} else if (!handled && memoryDir) {
|
|
2267
2478
|
// W4: vocabulary/definition questions consult the MEMORY graph's Facts alongside
|
|
2268
2479
|
// the schema-docs surface — a remembered fact answers a miss OR extends a (non-
|
|
@@ -2277,6 +2488,8 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
2277
2488
|
via = "fact";
|
|
2278
2489
|
recordMiss = false;
|
|
2279
2490
|
if (fact.pending) factPending = fact.pending; // a truncated fact list → paginable remainder
|
|
2491
|
+
note(trace, `lane: (3) memory facts — factAnswer/factReadBack matched (memoryDir=${memoryDir})`);
|
|
2492
|
+
note(trace, "source: .tmct/memory Facts (see /memory for provenance per line)");
|
|
2280
2493
|
} else if (miss) {
|
|
2281
2494
|
// W2: after the honest miss is composed, consult the folded-session memory. A
|
|
2282
2495
|
// relevant enough block ANSWERS — recalled Q/A framed + cited first, with the
|
|
@@ -2302,6 +2515,8 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
2302
2515
|
answer = `${recalled}\n\n${trailing}`;
|
|
2303
2516
|
via = "recall";
|
|
2304
2517
|
recordMiss = false; // memory answered it, cited — no longer a blank
|
|
2518
|
+
note(trace, "lane: (3) memory recall — recallFromBlocks matched a folded-session Q/A above the relevance floor");
|
|
2519
|
+
note(trace, "source: .tmct/memory folded session blocks (fold.mjs)");
|
|
2305
2520
|
}
|
|
2306
2521
|
}
|
|
2307
2522
|
}
|
|
@@ -2313,7 +2528,11 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
2313
2528
|
// conversational lanes (via:"meta"/"template"), which answer a different question.
|
|
2314
2529
|
if (via === "composed" || via === "fact") {
|
|
2315
2530
|
const def = await curatedDefinitionAnswer(query, envelope, { memoryDir, lexicon });
|
|
2316
|
-
if (def) {
|
|
2531
|
+
if (def) {
|
|
2532
|
+
answer = def.text; via = "corpus/seon"; recordMiss = false;
|
|
2533
|
+
note(trace, "lane: CURATED SEON DEFINITION — curatedDefinitionAnswer matched a lexicon term");
|
|
2534
|
+
note(trace, "source: corpus/seon (curated prose definition, licensed per data/corpus/seon)");
|
|
2535
|
+
}
|
|
2317
2536
|
}
|
|
2318
2537
|
// THE CONCEPT FORCE (concept.mjs) — a vague "what is a X" / "tell me about X" that
|
|
2319
2538
|
// names a KNOWN code concept WITH real instances composes the three-band answer
|
|
@@ -2333,6 +2552,8 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
2333
2552
|
conceptInstances = concept.instances;
|
|
2334
2553
|
conceptAllIds = concept.allIds;
|
|
2335
2554
|
conceptPending = concept.pending;
|
|
2555
|
+
note(trace, `lane: THE CONCEPT FORCE — a known code concept with ${concept.instances?.length ?? 0} real instance(s) composed the 3-band answer`);
|
|
2556
|
+
note(trace, "source: concept.mjs composeConcept — graph instances + corpus/seon definition");
|
|
2336
2557
|
} else {
|
|
2337
2558
|
// THE RELATION CONCEPT FORCE — the noun force declined, so try the edge-kind
|
|
2338
2559
|
// touch ("what about imports", "what are the calls", "tell me about contains").
|
|
@@ -2346,6 +2567,8 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
2346
2567
|
if (relation) {
|
|
2347
2568
|
answer = relation.text; via = "corpus/seon"; recordMiss = false;
|
|
2348
2569
|
conceptPending = relation.pending;
|
|
2570
|
+
note(trace, "lane: THE RELATION CONCEPT FORCE — the touched word named a known, edge-bearing relation kind");
|
|
2571
|
+
note(trace, "source: relationForceAnswer over the loaded graph's own edges (not corpus)");
|
|
2349
2572
|
}
|
|
2350
2573
|
}
|
|
2351
2574
|
}
|
|
@@ -2360,13 +2583,19 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
2360
2583
|
if (syn) {
|
|
2361
2584
|
answer = syn.text; via = "fact"; recordMiss = false;
|
|
2362
2585
|
if (syn.pending) factPending = syn.pending;
|
|
2586
|
+
note(trace, "lane: (3b) ONTOLOGY SYNONYM EXPANSION — a last-resort synonym of the term had direct facts");
|
|
2587
|
+
note(trace, "source: .tmct/memory Facts, reached via a known synonym (cited in the answer itself)");
|
|
2363
2588
|
}
|
|
2364
2589
|
}
|
|
2365
2590
|
// (4) #2 TEACH lane — a teach-shaped would-miss nothing above answered: route to
|
|
2366
2591
|
// memory, or say what CAN be remembered (LOUD), never the wall / a silent drop.
|
|
2367
2592
|
if (miss && recordMiss && via === "composed") {
|
|
2368
2593
|
const taught = await teachLane(query, { memoryDir, sessionId, lexicon });
|
|
2369
|
-
if (taught) {
|
|
2594
|
+
if (taught) {
|
|
2595
|
+
answer = taught.text; via = taught.via; recordMiss = taught.miss;
|
|
2596
|
+
note(trace, `lane: (4) TEACH — TEACH_RE/OWNS_TEACH_RE/BARE_DECLARATIVE_RE matched, ${taught.miss ? "but the payload could not be stored" : "reified into .tmct/memory"}`);
|
|
2597
|
+
note(trace, "goal: teach/remember a new fact");
|
|
2598
|
+
}
|
|
2370
2599
|
}
|
|
2371
2600
|
// (4b) #4 AUTHOR lane (0.8.2 WS4) — "who is <Name>", "what did <Name> touch",
|
|
2372
2601
|
// "who authored <sha>": the Commit author ATTRIBUTE answered as a person, off
|
|
@@ -2375,7 +2604,12 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
2375
2604
|
// honest miss below (never a guess).
|
|
2376
2605
|
if (miss && recordMiss && via === "composed") {
|
|
2377
2606
|
const authored = authorLane(query, { graph });
|
|
2378
|
-
if (authored) {
|
|
2607
|
+
if (authored) {
|
|
2608
|
+
answer = authored.text; via = authored.via; recordMiss = false;
|
|
2609
|
+
note(trace, "lane: (4b) AUTHOR — a who-is/what-did-<Name>-touch/who-authored-<sha> pattern matched a commit author");
|
|
2610
|
+
note(trace, "source: codegraph.mjs authorIndex (derived from Commit individuals)");
|
|
2611
|
+
note(trace, "goal: identify a person and/or what they touched (authorship/history)");
|
|
2612
|
+
}
|
|
2379
2613
|
}
|
|
2380
2614
|
// (4b2) #5(f) PRESUPPOSITION HONEST-NUDGE (ADVANCED_GRAMMAR track f) — "why
|
|
2381
2615
|
// does X still/again import Y": names the presupposition being checked
|
|
@@ -2384,7 +2618,11 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
2384
2618
|
// one is still an honest, confident correction, not a miss.
|
|
2385
2619
|
if (miss && recordMiss && via === "composed") {
|
|
2386
2620
|
const presup = await presuppositionNudge(query, { graph, memoryDir });
|
|
2387
|
-
if (presup) {
|
|
2621
|
+
if (presup) {
|
|
2622
|
+
answer = presup.text; via = "presupposition"; recordMiss = false;
|
|
2623
|
+
note(trace, "lane: (4b2) PRESUPPOSITION HONEST-NUDGE — a still/again-marked question's presupposition was checked against the graph");
|
|
2624
|
+
note(trace, "goal: verify an assumption baked into the question, then answer what survives");
|
|
2625
|
+
}
|
|
2388
2626
|
}
|
|
2389
2627
|
// (4c) CAPABILITY NUDGES (0.8.2 WS4) — risk scoring / code opinions / "write me
|
|
2390
2628
|
// code" imperatives / motive-"why": an honest wall pointing at the nearest real
|
|
@@ -2393,7 +2631,11 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
2393
2631
|
// short-miss's "is a <thing> a <kind>" membership hint could claim the line.
|
|
2394
2632
|
if (miss && recordMiss && via === "composed") {
|
|
2395
2633
|
const nudged = nudgeAnswer(query, newFocus);
|
|
2396
|
-
if (nudged) {
|
|
2634
|
+
if (nudged) {
|
|
2635
|
+
answer = nudged; via = "miss";
|
|
2636
|
+
note(trace, "lane: (4c) CAPABILITY NUDGE — the question asked tmct to do something outside its scope (opinion/generation/risk-scoring)");
|
|
2637
|
+
note(trace, "goal: out of scope for a no-LLM graph reader — pointed at the nearest real query shapes");
|
|
2638
|
+
}
|
|
2397
2639
|
}
|
|
2398
2640
|
// (4d) DESCRIBE-WRAPPER RESCUE (playtest sprint round 2, SKILL_PLAYTEST_SPRINT.md)
|
|
2399
2641
|
// — "can you describe X for me" / "tell me more about X": a closed wrapper
|
|
@@ -2407,7 +2649,11 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
2407
2649
|
// it only claims the turn if /describe actually resolves the captured term.
|
|
2408
2650
|
if (miss && recordMiss && via === "composed") {
|
|
2409
2651
|
const described = await describeWrapperAnswer(query, { config, source });
|
|
2410
|
-
if (described) {
|
|
2652
|
+
if (described) {
|
|
2653
|
+
answer = described.text; via = "describe"; recordMiss = false;
|
|
2654
|
+
note(trace, "lane: (4d) DESCRIBE-WRAPPER RESCUE — a polite wrapper around \"describe/tell me about <symbol>\" resolved via /describe, tried last after every other lane declined");
|
|
2655
|
+
note(trace, "goal: get a symbol's definition/kind/relations (phrased conversationally)");
|
|
2656
|
+
}
|
|
2411
2657
|
}
|
|
2412
2658
|
// (5) #1 SHORT TAILORED MISS — replace ONLY the engine's full grammar cheat-sheet
|
|
2413
2659
|
// wall (WALL_MISS_RE). Receipt-bearing misses keep their specific wording.
|
|
@@ -2416,10 +2662,10 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
2416
2662
|
// to a one-liner whose text does NOT match WALL_MISS_RE — self-limiting, so a
|
|
2417
2663
|
// third consecutive miss re-offers the tailored hint instead of droning.
|
|
2418
2664
|
if (miss && recordMiss && via === "composed" && WALL_MISS_RE.test(answer)) {
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
: shortMissHint(query);
|
|
2665
|
+
const repeat = last?.answer && WALL_MISS_RE.test(String(last.answer));
|
|
2666
|
+
answer = repeat ? WALL_REPEAT_ONELINER : shortMissHint(query);
|
|
2422
2667
|
via = "miss";
|
|
2668
|
+
note(trace, `lane: (5) SHORT TAILORED MISS — every lane above declined; ${repeat ? "REPEAT collapsed to one-liner (wall kindness)" : "the full grammar wall was shortened + tailored to the query's keywords"}`);
|
|
2423
2669
|
}
|
|
2424
2670
|
// #4 HONEST-EMPTY POLISH — an empty CODE graph: any still-standing engine
|
|
2425
2671
|
// dead-end (an honest empty, the short miss, the bootstrap note) carries the exit
|
|
@@ -2427,6 +2673,7 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
2427
2673
|
if (recordMiss && (via === "composed" || via === "miss")
|
|
2428
2674
|
&& noCodeGraph(graph) && !/--repo|tmct init|no code graph/i.test(answer)) {
|
|
2429
2675
|
answer = `${answer}\n(this repo has no code graph — for structure, point me at a \`.tmct/graph.json\` with \`--repo <path>\` or run \`npm run example:mini\`; tmct doesn't index code itself.)`;
|
|
2676
|
+
note(trace, "intermediate: HONEST-EMPTY POLISH — the loaded graph has 0 modules, so the dead-end got a --repo/tmct init pointer appended");
|
|
2430
2677
|
}
|
|
2431
2678
|
// W5 (flag-gated, default OFF): an unknown-term miss may consult the LOCAL
|
|
2432
2679
|
// committed corpus slice — a hit APPENDS a grounded, licence-cited aside under
|
|
@@ -2436,6 +2683,8 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
2436
2683
|
if (aside) {
|
|
2437
2684
|
answer = `${answer}\n${aside}`;
|
|
2438
2685
|
via = "corpus";
|
|
2686
|
+
note(trace, "lane: W5 corpus aside — an unknown term matched the local committed corpus slice (TMCT_CORPUS_LOOKUP=1)");
|
|
2687
|
+
note(trace, "source: local committed corpus slice (licence-cited in the aside itself)");
|
|
2439
2688
|
}
|
|
2440
2689
|
}
|
|
2441
2690
|
// ADVANCED_GRAMMAR track (a) — counterfactual marker (PLAN_ADVANCED_GRAMMAR.md
|
|
@@ -2451,6 +2700,7 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
2451
2700
|
const counterfactualSubject = String(query).trim().match(COUNTERFACTUAL_RE);
|
|
2452
2701
|
if (!recordMiss && via === "composed" && counterfactualSubject) {
|
|
2453
2702
|
answer = `hypothetically, if ${counterfactualSubject[1].trim()} were removed: ${answer}`;
|
|
2703
|
+
note(trace, `intermediate: COUNTERFACTUAL_RE matched — compiled to a real traversal, wrapped as hypothetical ("${counterfactualSubject[1].trim()}" removed)`);
|
|
2454
2704
|
}
|
|
2455
2705
|
// The concept force answers WITH real example instances — those are the entities the
|
|
2456
2706
|
// turn "asked about" (the SchemaClass meta-node is documentation, not a code entity),
|
|
@@ -2490,28 +2740,52 @@ function plainTurn(query, answer, { command, via = "composed", miss = false, foc
|
|
|
2490
2740
|
};
|
|
2491
2741
|
}
|
|
2492
2742
|
|
|
2493
|
-
/** A slash-command → the mapped tool (or the /help, /focus, unknown
|
|
2494
|
-
* the same { answer, logLines, record, focus } shape as
|
|
2495
|
-
* the command name and the resolved entity id
|
|
2496
|
-
* slash-command turn becomes asksAbout graph data
|
|
2497
|
-
|
|
2743
|
+
/** A slash-command → the mapped tool (or the /help, /focus, /narrate, unknown
|
|
2744
|
+
* cases). Returns the same { answer, logLines, record, focus } shape as
|
|
2745
|
+
* runAsk; the record carries the command name and the resolved entity id
|
|
2746
|
+
* (for entity commands) so a slash-command turn becomes asksAbout graph data
|
|
2747
|
+
* wherever it resolves an entity. `ctx.trace` (narrate mode, or undefined
|
|
2748
|
+
* when off) gets one "goal:"/"lane:" note per branch — a slash-command's
|
|
2749
|
+
* "decision" is simply which command+tool ran, so this is intentionally
|
|
2750
|
+
* lighter than runAsk's miss-cascade instrumentation. */
|
|
2751
|
+
async function runCommand(line, { config, source, graph, focus, memoryDir, trace, narrate = false }) {
|
|
2498
2752
|
const ts = new Date().toISOString();
|
|
2499
2753
|
const sp = line.indexOf(" ");
|
|
2500
2754
|
const name = (sp === -1 ? line.slice(1) : line.slice(1, sp)).toLowerCase();
|
|
2501
2755
|
const argText = (sp === -1 ? "" : line.slice(sp + 1)).trim();
|
|
2502
|
-
const mk = (answer, { resolvedIds = [], miss = false, newFocus = focus } = {}) => ({
|
|
2756
|
+
const mk = (answer, { resolvedIds = [], miss = false, newFocus = focus, narrateNext } = {}) => ({
|
|
2503
2757
|
answer,
|
|
2504
2758
|
logLines: [ts, `> ${line}`, answer, ""],
|
|
2505
2759
|
record: { type: "turn", ts, query: line, command: name, via: "command", resolvedIds, answeredIds: [], miss },
|
|
2506
2760
|
focus: newFocus,
|
|
2761
|
+
...(narrateNext !== undefined ? { narrate: narrateNext } : {}),
|
|
2507
2762
|
});
|
|
2508
2763
|
|
|
2509
|
-
if (name === "help") return mk(await helpText());
|
|
2510
|
-
if (name === "stats")
|
|
2764
|
+
if (name === "help") { note(trace, "goal: get oriented / learn available commands"); return mk(await helpText()); }
|
|
2765
|
+
if (name === "stats") {
|
|
2766
|
+
note(trace, "goal: get a one-screen overview of the loaded graph");
|
|
2767
|
+
return graph ? mk(renderStats(graph)) : mk("no graph loaded — /stats needs an index.", { miss: true });
|
|
2768
|
+
}
|
|
2769
|
+
|
|
2770
|
+
// /narrate on|off — the debug-mode toggle itself (session-scoped, mirrors the
|
|
2771
|
+
// /focus pattern: the new state rides the turn RESULT as `narrate`, and
|
|
2772
|
+
// createSession's turn() applies it to its own mutable state; a bare
|
|
2773
|
+
// runTurn caller threads it the same way it threads `focus`/`last`). A
|
|
2774
|
+
// status-only "/narrate" (no on/off) reports the CURRENT state and changes
|
|
2775
|
+
// nothing — never silently flips it.
|
|
2776
|
+
if (name === "narrate") {
|
|
2777
|
+
const arg = argText.toLowerCase();
|
|
2778
|
+
if (arg !== "on" && arg !== "off") {
|
|
2779
|
+
return mk(`narrate mode is ${narrate ? "on" : "off"} — /narrate on or /narrate off to change it.`);
|
|
2780
|
+
}
|
|
2781
|
+
const next = arg === "on";
|
|
2782
|
+
return mk(`narrate mode ${next ? "on" : "off"}.`, { narrateNext: next });
|
|
2783
|
+
}
|
|
2511
2784
|
|
|
2512
2785
|
// /memory [verbose] — what tmct remembers, as text (the ROADMAP "Memory
|
|
2513
2786
|
// inspection" surface; the same renderer serves the `tmct memory` CLI).
|
|
2514
2787
|
if (name === "memory") {
|
|
2788
|
+
note(trace, "goal: inspect tmct's memory store (facts/utterances/sessions)");
|
|
2515
2789
|
if (!memoryDir) return mk("no memory store here — /memory works inside a repo session.", { miss: true });
|
|
2516
2790
|
try {
|
|
2517
2791
|
const { inspectMemory } = await import("./memory/inspect.mjs");
|
|
@@ -2522,18 +2796,28 @@ async function runCommand(line, { config, source, graph, focus, memoryDir }) {
|
|
|
2522
2796
|
}
|
|
2523
2797
|
|
|
2524
2798
|
if (name === "focus") {
|
|
2799
|
+
note(trace, "goal: set the working focus entity for follow-up pronouns (it/this/that)");
|
|
2525
2800
|
if (!argText) return mk(focus ? `focus is ${focus.label}` : "no focus set — /focus <symbol> to set one.");
|
|
2526
2801
|
const ent = await resolveEntity(graph, isPronoun(argText) ? focus?.label : argText);
|
|
2527
2802
|
if (!ent) return mk(`could not resolve "${argText}" to a single entity — focus unchanged${focus ? ` (still ${focus.label})` : ""}.`, { miss: true });
|
|
2803
|
+
note(trace, `result: resolved "${argText}" -> ${ent.label} (${ent.id})`);
|
|
2528
2804
|
return mk(`focus set to ${ent.label}.`, { resolvedIds: [ent.id], newFocus: ent });
|
|
2529
2805
|
}
|
|
2530
2806
|
|
|
2531
2807
|
const spec = COMMANDS[name];
|
|
2532
|
-
if (!spec)
|
|
2808
|
+
if (!spec) {
|
|
2809
|
+
note(trace, `pattern: /${name} is not a registered command (see COMMANDS in src/chat.mjs)`);
|
|
2810
|
+
return mk(`unknown command /${name} — type /help for the list of commands.`, { miss: true });
|
|
2811
|
+
}
|
|
2812
|
+
note(trace, `goal: ${spec.help}`);
|
|
2813
|
+
note(trace, `lane: slash-command /${name} -> dispatchTool("${spec.tool}"${spec.arg ? `, {${spec.arg}}` : ""})`);
|
|
2533
2814
|
|
|
2534
2815
|
const entityArg = ENTITY_ARGS.has(spec.arg);
|
|
2535
2816
|
let value = argText;
|
|
2536
|
-
if (entityArg && (!value || isPronoun(value)))
|
|
2817
|
+
if (entityArg && (!value || isPronoun(value))) {
|
|
2818
|
+
value = focus?.label || "";
|
|
2819
|
+
if (value) note(trace, `intermediate: no/pronoun argument -> fell back to the standing focus "${value}"`);
|
|
2820
|
+
}
|
|
2537
2821
|
if (spec.arg && !spec.optional && !value) {
|
|
2538
2822
|
const need = entityArg ? `${spec.arg} (none given and no focus set — /focus <x> or pass one)` : spec.arg;
|
|
2539
2823
|
return mk(`/${name} needs a ${need}.`, { miss: true });
|
|
@@ -2543,6 +2827,7 @@ async function runCommand(line, { config, source, graph, focus, memoryDir }) {
|
|
|
2543
2827
|
try {
|
|
2544
2828
|
answer = await dispatchTool(spec.tool, spec.arg ? { [spec.arg]: value } : {}, { config, source });
|
|
2545
2829
|
} catch (e) {
|
|
2830
|
+
note(trace, `intermediate: dispatchTool("${spec.tool}") threw — ${String(e?.message || e)}`);
|
|
2546
2831
|
return mk(String(e?.message || e), { miss: true }); // the tool's own clean error, never a stack
|
|
2547
2832
|
}
|
|
2548
2833
|
// Entity commands resolve their subject for the sidecar/graph AND set the focus so a
|
|
@@ -2553,16 +2838,18 @@ async function runCommand(line, { config, source, graph, focus, memoryDir }) {
|
|
|
2553
2838
|
// Commit/Session/schema node records the resolution but does not displace a
|
|
2554
2839
|
// standing code-entity focus that "it" is meant to keep binding to.
|
|
2555
2840
|
if (ent) {
|
|
2841
|
+
note(trace, `result: resolved "${value}" -> ${ent.label} (${ent.id}, class=${graph?.byId?.get?.(ent.id)?.class || "?"})`);
|
|
2556
2842
|
// Bug B4 (0.8.2 follow-up): /describe's code-map render never sees memory,
|
|
2557
2843
|
// so a taught fact about the resolved entity is invisible to it — append
|
|
2558
2844
|
// matching taught facts (subject === the resolved entity, trust-ranked)
|
|
2559
2845
|
// under the code-map answer, mirroring the ask-path's fact-append pattern.
|
|
2560
2846
|
if (name === "describe" && memoryDir) {
|
|
2561
2847
|
const facts = await describedFacts(memoryDir, ent.label);
|
|
2562
|
-
if (facts) answer = `${answer}\n${facts}`;
|
|
2848
|
+
if (facts) { answer = `${answer}\n${facts}`; note(trace, "source: memory facts (describedFacts) appended to the code-map answer"); }
|
|
2563
2849
|
}
|
|
2564
2850
|
return mk(answer, { resolvedIds: [ent.id], newFocus: nextFocus(graph, focus, ent) });
|
|
2565
2851
|
}
|
|
2852
|
+
note(trace, `intermediate: "${value}" did not resolve to a single entity — the tool's own (unresolved) answer stands`);
|
|
2566
2853
|
}
|
|
2567
2854
|
return mk(answer);
|
|
2568
2855
|
}
|
|
@@ -2637,10 +2924,17 @@ function morePage(query, { last, focus }) {
|
|
|
2637
2924
|
return turn;
|
|
2638
2925
|
}
|
|
2639
2926
|
|
|
2640
|
-
export async function runTurn(input, { config, source = defaultSource, graph = null, focus = null, last = null, memoryDir = null, sessionId = "", env = process.env, lexicon = null } = {}) {
|
|
2927
|
+
export async function runTurn(input, { config, source = defaultSource, graph = null, focus = null, last = null, memoryDir = null, sessionId = "", env = process.env, lexicon = null, narrate = false } = {}) {
|
|
2641
2928
|
const line = String(input ?? "").trim();
|
|
2642
2929
|
const templates = await chatTemplates(); // failure-tolerated: null degrades, never throws
|
|
2643
|
-
|
|
2930
|
+
// narrate mode: allocate the mutable trace array ONLY when on (`null` when off,
|
|
2931
|
+
// matching every OTHER optional collaborator here — templates/memoryDir/lexicon
|
|
2932
|
+
// all null-degrade the same way). Every note()/withNarration() call below is a
|
|
2933
|
+
// cheap `if (trace)`/`if (!trace || !trace.length)` no-op when this is null, so
|
|
2934
|
+
// the narrate:false path allocates nothing extra and renders byte-identically to
|
|
2935
|
+
// before this feature existed — see the "---- narrate mode ----" section above.
|
|
2936
|
+
const trace = narrate ? [] : null;
|
|
2937
|
+
const ctx = { config, source, graph, focus, last, memoryDir, sessionId, templates, env, lexicon, trace, narrate };
|
|
2644
2938
|
// A DISPATCHED turn (count / slash-command / ask) becomes the new "last answer"
|
|
2645
2939
|
// that why/say-more re-renders; a conversational turn does not (it preserves it).
|
|
2646
2940
|
// FINISH SEAM (PLAN_RESPONSE_FINISHING §"Where it lives"): every dispatched turn's
|
|
@@ -2648,10 +2942,13 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
2648
2942
|
// finished answer becomes the `last` we expand. finish() owns the prose-span
|
|
2649
2943
|
// grammar pass (src/finish.mjs); it rewrites result.answer/logLines and leaves the
|
|
2650
2944
|
// protected spans (entities, paths, numbers, receipts, provenance) byte-invariant,
|
|
2651
|
-
// so `last` and the transcript stay consistent with what the shell prints.
|
|
2652
|
-
|
|
2945
|
+
// so `last` and the transcript stay consistent with what the shell prints. The
|
|
2946
|
+
// narrate block (withNarration, above) is applied AFTER `last` is captured from
|
|
2947
|
+
// the PRE-narration finished result — see withNarration's docblock for why.
|
|
2948
|
+
const withLast = (result, fallbackGoal = "unclear — no goal signal for this turn type") => {
|
|
2653
2949
|
const finished = finish(result, { graph });
|
|
2654
|
-
|
|
2950
|
+
const nextLast = { query: line, answer: finished.answer, detail: finished.detail ?? null };
|
|
2951
|
+
return { ...withNarration(finished, trace, fallbackGoal), last: nextLast };
|
|
2655
2952
|
};
|
|
2656
2953
|
|
|
2657
2954
|
// Slash-optional system commands: a bare leading command word ("stats",
|
|
@@ -2659,28 +2956,36 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
2659
2956
|
// layer, so a forgiving shell answers "stats" the way it answers "/stats" instead
|
|
2660
2957
|
// of falling through to the generic orientation.
|
|
2661
2958
|
const bareCmd = asBareCommand(line);
|
|
2662
|
-
if (bareCmd) return withLast(await runCommand(bareCmd, ctx));
|
|
2959
|
+
if (bareCmd) return withLast(await runCommand(bareCmd, ctx), "use a specific tool/command directly");
|
|
2663
2960
|
|
|
2664
2961
|
// Conversational layer next (greetings, thanks, help, bye, why/say-more) — these
|
|
2665
|
-
// resolve no entity and carry their own preserved `last`.
|
|
2962
|
+
// resolve no entity and carry their own preserved `last`. Bypasses withLast (a
|
|
2963
|
+
// conversational turn is never finish()'d / never becomes a new `last`), so the
|
|
2964
|
+
// narrate block is applied directly here instead.
|
|
2666
2965
|
const convo = conversationalTurn(line, ctx);
|
|
2667
|
-
if (convo) return convo;
|
|
2966
|
+
if (convo) return withNarration(convo, trace, "casual/social — no graph intent");
|
|
2668
2967
|
|
|
2669
2968
|
// "more" — page the remainder of a previous long listing, if one is held. Gated on
|
|
2670
2969
|
// an actual pending remainder so a bare "more" with nothing to continue falls through
|
|
2671
2970
|
// to the ordinary path (an honest miss), never a pretend page.
|
|
2672
2971
|
if (MORE_RE.test(line) && Array.isArray(last?.detail?.pending?.items) && last.detail.pending.items.length) {
|
|
2673
|
-
|
|
2972
|
+
note(trace, "goal: continue viewing a previous long listing (pagination)");
|
|
2973
|
+
note(trace, "lane: MORE_RE matched a held pending remainder from the previous turn's detail.pending");
|
|
2974
|
+
return withLast(morePage(line, ctx), "continue viewing a previous long listing");
|
|
2674
2975
|
}
|
|
2675
2976
|
|
|
2676
|
-
if (line.startsWith("/")) return withLast(await runCommand(line, ctx));
|
|
2977
|
+
if (line.startsWith("/")) return withLast(await runCommand(line, ctx), "use a specific tool/command directly");
|
|
2677
2978
|
// Declarative ACE sentences ("every module is a artifact") ASSERT into tmct's
|
|
2678
2979
|
// own memory and confirm — they are statements to remember, not graph queries.
|
|
2679
2980
|
// Gated on memoryDir: only a session shell provides a write target, so a bare
|
|
2680
2981
|
// runTurn (tests, library callers) stays pure and falls through to the engine.
|
|
2681
2982
|
if (memoryDir) {
|
|
2682
2983
|
const asserted = await assertTurn(line, ctx);
|
|
2683
|
-
if (asserted)
|
|
2984
|
+
if (asserted) {
|
|
2985
|
+
note(trace, "goal: teach/remember a new fact (declarative ACE sentence)");
|
|
2986
|
+
note(trace, "lane: assertTurn — grammar/ace.mjs parseAce matched a full triple with no residue");
|
|
2987
|
+
return withLast(asserted, "teach/remember a new fact");
|
|
2988
|
+
}
|
|
2684
2989
|
}
|
|
2685
2990
|
// MEMORY-STORE counts first ("how many facts / utterances do you know") — the
|
|
2686
2991
|
// memory graph owns Facts + Utterances, so these are answerable and consistent
|
|
@@ -2689,7 +2994,11 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
2689
2994
|
// structural counts (classes/functions/…) and sessions fall through unaffected.
|
|
2690
2995
|
if (memoryDir) {
|
|
2691
2996
|
const memCount = await answerMemoryCount(memoryDir, line);
|
|
2692
|
-
if (memCount != null)
|
|
2997
|
+
if (memCount != null) {
|
|
2998
|
+
note(trace, "goal: get a count of a memory-store kind (facts/utterances)");
|
|
2999
|
+
note(trace, "lane: answerMemoryCount — matched a MEMORY_COUNT_NOUNS entry, answered off the .tmct/memory graph header");
|
|
3000
|
+
return withLast(plainTurn(line, memCount, { via: "count", focus }), "get a count of a memory-store kind");
|
|
3001
|
+
}
|
|
2693
3002
|
}
|
|
2694
3003
|
// Aggregate/count questions are answered mechanically off the loaded graph header,
|
|
2695
3004
|
// BEFORE falling through to the ask engine (focus unchanged — a count names no entity).
|
|
@@ -2700,10 +3009,16 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
2700
3009
|
// class count). countFromFacts declines on a real graph kind, so ordinary
|
|
2701
3010
|
// counts are unaffected; it only speaks for a remembered object noun.
|
|
2702
3011
|
const viaFact = memoryDir ? await countFromFacts(graph, memoryDir, line) : null;
|
|
2703
|
-
if (viaFact != null)
|
|
2704
|
-
|
|
3012
|
+
if (viaFact != null) {
|
|
3013
|
+
note(trace, 'goal: get a count of an asserted-vocabulary kind ("every X is a Y" inherited cardinality)');
|
|
3014
|
+
note(trace, "lane: countFromFacts — the counted noun matched a remembered isa-fact's SUBJECT, whose class IS countable");
|
|
3015
|
+
return withLast(plainTurn(line, viaFact, { via: "fact", focus }), "get a count");
|
|
3016
|
+
}
|
|
3017
|
+
note(trace, "goal: get a count of a graph kind (classes/functions/modules/…)");
|
|
3018
|
+
note(trace, "lane: answerCount — a header-count aggregate question, answered mechanically off the graph header, never dispatched to the ask engine");
|
|
3019
|
+
return withLast(plainTurn(line, count, { via: "count", focus }), "get a count of a graph kind");
|
|
2705
3020
|
}
|
|
2706
|
-
return withLast(await runAsk(line, ctx));
|
|
3021
|
+
return withLast(await runAsk(line, ctx), "unclear — no goal signal computed by the ask engine");
|
|
2707
3022
|
}
|
|
2708
3023
|
|
|
2709
3024
|
// ---- W3: seedMemory → bootstrap (first run in a graph-less repo) ----
|
|
@@ -2821,6 +3136,7 @@ export async function createSession({
|
|
|
2821
3136
|
cwd = process.cwd(),
|
|
2822
3137
|
gitRoot = gitToplevel,
|
|
2823
3138
|
ephemeral = false,
|
|
3139
|
+
narrate = false,
|
|
2824
3140
|
} = {}) {
|
|
2825
3141
|
// EPHEMERAL mode (--ephemeral, or TMCT_EPHEMERAL=1): read the target graph but
|
|
2826
3142
|
// write NOTHING back into it. The shipped examples run this way so a demo never
|
|
@@ -2829,6 +3145,13 @@ export async function createSession({
|
|
|
2829
3145
|
// for structure; only the WRITE base (logs, memory, sessions) is diverted to an OS
|
|
2830
3146
|
// temp dir and the read-time graph upsert is suppressed.
|
|
2831
3147
|
ephemeral = ephemeral || /^(1|true|yes)$/i.test(String(env.TMCT_EPHEMERAL || ""));
|
|
3148
|
+
// NARRATE mode (--narrate, or TMCT_NARRATE=1 — same on/off convention as
|
|
3149
|
+
// TMCT_EPHEMERAL/TMCT_NO_SEED): start the session with narrate mode already
|
|
3150
|
+
// on. Session-scoped and mutable from here — `/narrate on`/`/narrate off`
|
|
3151
|
+
// flips it turn-to-turn the same way `/focus` mutates the session's focus
|
|
3152
|
+
// (see `turn()` below: a turn result's `narrate` field, when present,
|
|
3153
|
+
// updates this closure-private variable). Default OFF, as the operator asked.
|
|
3154
|
+
let narrateOn = narrate || /^(1|true|yes)$/i.test(String(env.TMCT_NARRATE || ""));
|
|
2832
3155
|
// Graph resolution order for the chat surface (documented; --repo wins):
|
|
2833
3156
|
// 1. --repo <path> → pins <path>/.tmct/graph.json (repo AND graph).
|
|
2834
3157
|
// 2. TMCT_GRAPH_FILE env → loads that graph anywhere (loadConfig reads it), so
|
|
@@ -2954,6 +3277,7 @@ export async function createSession({
|
|
|
2954
3277
|
get focus() { return focus; },
|
|
2955
3278
|
get lastAnswer() { return last; },
|
|
2956
3279
|
get turns() { return turns; },
|
|
3280
|
+
get narrate() { return narrateOn; },
|
|
2957
3281
|
promptFor: () => promptFor(focus),
|
|
2958
3282
|
|
|
2959
3283
|
/** One dispatched turn through the FULL sink sequencing (writeLog → writeSidecar
|
|
@@ -2965,7 +3289,7 @@ export async function createSession({
|
|
|
2965
3289
|
async turn(line) {
|
|
2966
3290
|
let result;
|
|
2967
3291
|
try {
|
|
2968
|
-
result = await runTurn(line, { config, source, graph, focus, last, memoryDir: repo, sessionId, env, lexicon });
|
|
3292
|
+
result = await runTurn(line, { config, source, graph, focus, last, memoryDir: repo, sessionId, env, lexicon, narrate: narrateOn });
|
|
2969
3293
|
} catch (e) {
|
|
2970
3294
|
const ts = new Date().toISOString();
|
|
2971
3295
|
const message = e instanceof Error ? e.message : String(e);
|
|
@@ -2976,9 +3300,12 @@ export async function createSession({
|
|
|
2976
3300
|
turns += 1;
|
|
2977
3301
|
return { answer: `Something went wrong answering that (${message}). Try rephrasing, or /help.`, end: false, prompt: promptFor(focus) };
|
|
2978
3302
|
}
|
|
2979
|
-
const { answer, logLines, record, focus: nextFocus, last: nextLast, end } = result;
|
|
3303
|
+
const { answer, logLines, record, focus: nextFocus, last: nextLast, end, narrate: nextNarrate } = result;
|
|
2980
3304
|
focus = nextFocus;
|
|
2981
3305
|
last = nextLast;
|
|
3306
|
+
// /narrate on|off (runCommand) rides the turn RESULT the same way a focus
|
|
3307
|
+
// update does — apply it to this handle's session-scoped state.
|
|
3308
|
+
if (typeof nextNarrate === "boolean") narrateOn = nextNarrate;
|
|
2982
3309
|
await writeLog(logLines.join("\n") + "\n");
|
|
2983
3310
|
await writeSidecar(record);
|
|
2984
3311
|
turnRecords.push(record);
|
|
@@ -3026,6 +3353,7 @@ export async function runChat({
|
|
|
3026
3353
|
cwd = process.cwd(),
|
|
3027
3354
|
gitRoot = gitToplevel,
|
|
3028
3355
|
ephemeral = false,
|
|
3356
|
+
narrate = false,
|
|
3029
3357
|
} = {}) {
|
|
3030
3358
|
// createSession's first-run seed (~2-3s, corpus/seon + ConceptNet) produces ZERO
|
|
3031
3359
|
// output until it fully resolves — found live: an operator reported `npm run chat`
|
|
@@ -3033,7 +3361,7 @@ export async function runChat({
|
|
|
3033
3361
|
// fast subsequent run just flashes it briefly) and removes the "is this even
|
|
3034
3362
|
// running" uncertainty during the one case that's genuinely slow.
|
|
3035
3363
|
output.write("tmct — starting…\n");
|
|
3036
|
-
const session = await createSession({ repoPath, source, env, cwd, gitRoot, ephemeral });
|
|
3364
|
+
const session = await createSession({ repoPath, source, env, cwd, gitRoot, ephemeral, narrate });
|
|
3037
3365
|
|
|
3038
3366
|
const dim = (s) => (env.NO_COLOR || !output.isTTY ? s : `\x1b[2m${s}\x1b[0m`);
|
|
3039
3367
|
for (const line of session.bannerLines) output.write(dim(line) + "\n");
|