gentle-pi 2.7.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -6
- package/assets/agents/gentle-ai-worker.md +5 -1
- package/assets/agents/sdd-apply.md +9 -7
- package/assets/agents/sdd-archive.md +42 -23
- package/assets/agents/sdd-proposal.md +2 -2
- package/assets/agents/sdd-remediate.md +4 -4
- package/assets/agents/sdd-research.md +20 -48
- package/assets/agents/sdd-tasks.md +5 -5
- package/assets/agents/sdd-verify.md +6 -28
- package/assets/chains/sdd-full.chain.md +4 -22
- package/assets/chains/sdd-verify.chain.md +3 -12
- package/assets/orchestrator-delegation.md +33 -3
- package/assets/orchestrator-memory.md +20 -7
- package/assets/orchestrator.md +5 -3
- package/assets/sdd-orchestrator-workflow.md +25 -58
- package/assets/support/sdd-status-contract.md +9 -12
- package/docs/gentle-shell.md +14 -4
- package/docs/readme-reference.md +171 -32
- package/extensions/codegraph-tools.ts +2 -0
- package/extensions/gentle-agents.ts +281 -361
- package/extensions/gentle-ai.ts +588 -117
- package/extensions/gentle-shell.ts +74 -30
- package/extensions/pi-pretty.ts +63 -14
- package/extensions/quiet-tools.ts +1 -2
- package/extensions/startup-banner.ts +10 -9
- package/lib/agent-home.ts +8 -0
- package/lib/agent-profile-pin.ts +336 -0
- package/lib/agent-profiles.ts +28 -8
- package/lib/agents-config.ts +24 -2
- package/lib/agents-history.ts +3 -97
- package/lib/agents-keys.ts +27 -0
- package/lib/agents-protocol.ts +2 -15
- package/lib/agents-runner.ts +67 -111
- package/lib/agents-session-transport.ts +691 -0
- package/lib/command-palette-catalog.ts +87 -0
- package/lib/command-palette.ts +346 -0
- package/lib/native-choice-list.ts +5 -0
- package/lib/native-review-cli.ts +10 -97
- package/lib/review-publication-gate.ts +11 -1
- package/lib/review-repository.ts +1 -1
- package/lib/review-snapshot.ts +1 -0
- package/lib/review-transaction.ts +4 -2
- package/lib/sdd-preflight.ts +2 -1
- package/lib/sdd-research-capabilities.ts +18 -152
- package/lib/sdd-status.ts +7 -779
- package/lib/session-change-capture.ts +2 -1
- package/lib/session-changes.ts +8 -1
- package/lib/shell-bar.ts +21 -12
- package/lib/shell-card.ts +8 -12
- package/lib/shell-changes.ts +3 -2
- package/lib/shell-prompt.ts +25 -8
- package/lib/shell-sidebar-banner.ts +2 -2
- package/lib/shell-sidebar-layout.ts +5 -2
- package/lib/windows-session-transport.ts +877 -0
- package/package.json +3 -3
- package/runtime/native-review-cli.mjs +9 -96
- package/runtime/windows-session-transport.ps1 +791 -0
- package/scripts/test-packed-runner.mjs +1668 -20
- package/scripts/verify-package-files.mjs +0 -1
- package/tests/agent-home.test.ts +52 -0
- package/tests/agent-profiles.test.ts +30 -1
- package/tests/agents-config.test.ts +44 -0
- package/tests/agents-history.test.ts +12 -24
- package/tests/agents-runner.test.ts +307 -58
- package/tests/agents-session-transport-process.test.ts +249 -0
- package/tests/agents-session-transport.test.ts +823 -0
- package/tests/artifact-language.test.ts +10 -7
- package/tests/command-palette.test.ts +378 -0
- package/tests/delegated-key-learnings-contract.test.ts +2 -2
- package/tests/fixtures/agents-session-transport-process.mjs +108 -0
- package/tests/fixtures/legacy/sdd-research-v2.5.0.md +54 -0
- package/tests/fixtures/windows-session-bootstrap.ps1 +129 -0
- package/tests/fixtures/windows-session-compile.ps1 +110 -0
- package/tests/gentle-agents.test.ts +849 -356
- package/tests/gentle-ai.test.ts +472 -4
- package/tests/gentle-shell.test.ts +201 -8
- package/tests/native-choice-list.test.ts +13 -0
- package/tests/native-review-cli.test.ts +0 -33
- package/tests/odd-routing-contract.test.ts +208 -0
- package/tests/orchestrator-budget.test.ts +17 -2
- package/tests/package-manifest.test.ts +115 -27
- package/tests/persona-single-channel.test.ts +3 -3
- package/tests/pi-pretty.test.ts +45 -0
- package/tests/profile-pin.test.ts +370 -0
- package/tests/quiet-tool-rendering.test.ts +32 -5
- package/tests/review-contract-prompt.test.ts +9 -0
- package/tests/review-controller.test.ts +0 -44
- package/tests/review-session-standing-permission-ipc.test.ts +427 -13
- package/tests/runtime-harness.mjs +4 -4
- package/tests/sdd-agent-tools.test.ts +15 -36
- package/tests/sdd-archive-replay.test.ts +82 -0
- package/tests/sdd-classical-continuation.test.ts +74 -0
- package/tests/sdd-execution-routing-contract.test.ts +18 -2
- package/tests/sdd-managed-runtime-settlement.test.ts +42 -330
- package/tests/sdd-native-managed-uptake.test.ts +11 -21
- package/tests/sdd-no-attempts-contract.test.ts +15 -0
- package/tests/sdd-odd-integration.test.ts +33 -0
- package/tests/sdd-optional-research.test.ts +124 -0
- package/tests/sdd-planning-routing-contract.test.ts +1 -1
- package/tests/sdd-preflight-rpc-input.test.ts +125 -0
- package/tests/sdd-preflight.test.ts +1 -1
- package/tests/sdd-research-capabilities.test.ts +20 -162
- package/tests/sdd-selection-transport.test.ts +180 -88
- package/tests/sdd-status.test.ts +5 -778
- package/tests/sdd-task-truth.test.ts +43 -0
- package/tests/session-change-capture.test.ts +20 -2
- package/tests/session-changes.test.ts +11 -0
- package/tests/shell-bar.test.ts +21 -0
- package/tests/shell-card.test.ts +8 -6
- package/tests/shell-changes.test.ts +8 -0
- package/tests/shell-prompt.test.ts +41 -7
- package/tests/shell-sidebar-banner.test.ts +4 -4
- package/tests/shell-sidebar-layout.test.ts +97 -13
- package/tests/startup-banner.test.ts +55 -2
- package/tests/windows-hidden-processes.test.ts +303 -0
- package/tests/windows-session-bootstrap.test.ts +1772 -0
- package/tests/windows-session-compile.test.ts +170 -0
- package/tests/windows-session-transport.test.ts +754 -0
- package/assets/agents/sdd-sync.md +0 -146
- package/lib/openspec-guardrails.ts +0 -99
- package/tests/native-sdd-attempt-authority.test.ts +0 -240
- package/tests/openspec-guardrails.test.ts +0 -71
|
@@ -152,10 +152,10 @@ test("SDD proposal questions focus on business and PRD gaps", async () => {
|
|
|
152
152
|
test("all shipped SDD agents and chains require parent preflight transport", async () => {
|
|
153
153
|
const agents = [
|
|
154
154
|
"sdd-init", "sdd-onboard", "sdd-explore", "sdd-research", "sdd-proposal", "sdd-spec", "sdd-design",
|
|
155
|
-
"sdd-tasks", "sdd-status", "sdd-apply", "sdd-verify", "sdd-
|
|
155
|
+
"sdd-tasks", "sdd-status", "sdd-apply", "sdd-verify", "sdd-archive", "sdd-remediate",
|
|
156
156
|
];
|
|
157
157
|
const chains = ["sdd-full", "sdd-plan", "sdd-verify"];
|
|
158
|
-
assert.equal(agents.length,
|
|
158
|
+
assert.equal(agents.length, 13);
|
|
159
159
|
assert.equal(chains.length, 3);
|
|
160
160
|
for (const agent of agents) {
|
|
161
161
|
const source = await readFile(join(ROOT, "assets", "agents", `${agent}.md`), "utf8");
|
|
@@ -221,7 +221,7 @@ test("persistent harness prompt assets do not hardcode Spanish SDD artifact copy
|
|
|
221
221
|
assert.deepEqual(failures, []);
|
|
222
222
|
});
|
|
223
223
|
|
|
224
|
-
test("SDD assets route completed implementation
|
|
224
|
+
test("SDD assets route completed implementation through native archive with optional verification", async () => {
|
|
225
225
|
const [tasks, apply, status, contract, chain] = await Promise.all([
|
|
226
226
|
readFile(join(ROOT, "assets/agents/sdd-tasks.md"), "utf8"),
|
|
227
227
|
readFile(join(ROOT, "assets/agents/sdd-apply.md"), "utf8"),
|
|
@@ -231,13 +231,16 @@ test("SDD assets route completed implementation directly through verify, sync, a
|
|
|
231
231
|
]);
|
|
232
232
|
const assets = [tasks, apply, status, contract, chain].join("\n");
|
|
233
233
|
|
|
234
|
-
assert.match(tasks,
|
|
235
|
-
assert.
|
|
234
|
+
assert.match(tasks, /- \[ \] 1\. Implement and verify the behavior\./);
|
|
235
|
+
assert.doesNotMatch(tasks, /<!-- sdd-owner:/);
|
|
236
|
+
assert.match(apply, /fresh native recommendation/);
|
|
236
237
|
assert.match(status, /gentle-ai\.sdd-status.*v2/i);
|
|
237
238
|
assert.match(status, /read-only/i);
|
|
238
239
|
assert.match(contract, /native.*(?:apply|verify).*archive/is);
|
|
239
|
-
assert.
|
|
240
|
-
assert.match(
|
|
240
|
+
assert.doesNotMatch(contract, /local resolver/);
|
|
241
|
+
assert.match(contract, /verification is optional/);
|
|
242
|
+
assert.match(chain, /apply -> archive/);
|
|
243
|
+
assert.doesNotMatch(chain, /^## sdd-(verify|sync)$/m);
|
|
241
244
|
assert.doesNotMatch(assets, /<!-- sdd-owner: parent -->/);
|
|
242
245
|
assert.doesNotMatch(assets, /parent-lifecycle/);
|
|
243
246
|
assert.doesNotMatch(assets, /approved receipt|bounded review/i);
|
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
4
|
+
import { CommandPalette, commandsKey, rankPaletteGroups, type CommandPaletteGroup, type CommandPaletteItem, type CommandPaletteResult, type CommandPaletteTheme } from "../lib/command-palette.ts";
|
|
5
|
+
import { buildCommandPaletteGroups, COMMAND_PALETTE_CATALOG } from "../lib/command-palette-catalog.ts";
|
|
6
|
+
|
|
7
|
+
// Command palette: a pure, curated, grouped overlay component for
|
|
8
|
+
// `/gentle:commands` and its `alt+k` shortcut. These tests drive ranking,
|
|
9
|
+
// keyboard input, and rendering without any Pi extension API — the
|
|
10
|
+
// component only depends on pi-tui key/width helpers and plain data
|
|
11
|
+
// (groups of items) in, plain strings out.
|
|
12
|
+
|
|
13
|
+
const KEY = {
|
|
14
|
+
DOWN: "\x1b[B",
|
|
15
|
+
UP: "\x1b[A",
|
|
16
|
+
CTRL_J: "\n",
|
|
17
|
+
CTRL_K: "\x0b",
|
|
18
|
+
ENTER: "\r",
|
|
19
|
+
ESCAPE: "\x1b",
|
|
20
|
+
CTRL_C: "\x03",
|
|
21
|
+
BACKSPACE: "\x7f",
|
|
22
|
+
CTRL_U: "\x15",
|
|
23
|
+
} as const;
|
|
24
|
+
|
|
25
|
+
function type(palette: CommandPalette, text: string): void {
|
|
26
|
+
for (const ch of text) palette.handleInput(ch);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function createPalette(groups: readonly CommandPaletteGroup[], theme?: CommandPaletteTheme, rows?: () => number): { palette: CommandPalette; results: CommandPaletteResult[] } {
|
|
30
|
+
const results: CommandPaletteResult[] = [];
|
|
31
|
+
const palette = new CommandPalette(groups, (result) => results.push(result), theme, rows);
|
|
32
|
+
return { palette, results };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// --- rankPaletteGroups -------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
test("rankPaletteGroups ranks label-prefix, label-contains, label-subsequence, command-contains, then description-contains", () => {
|
|
38
|
+
const group: CommandPaletteGroup = {
|
|
39
|
+
title: "G",
|
|
40
|
+
items: [
|
|
41
|
+
// rank 4: only the description contains "mod".
|
|
42
|
+
{ command: "gentle:xyz-tool", label: "xyz", description: "custom mod support" },
|
|
43
|
+
// rank 3: only the command name contains "mod".
|
|
44
|
+
{ command: "gentle:mod-tool", label: "xyz-abc" },
|
|
45
|
+
// rank 2: the label matches "mod" only as a subsequence.
|
|
46
|
+
{ command: "gentle:whatever3", label: "my-old-doc" },
|
|
47
|
+
// rank 1: the label contains "mod", but does not start with it.
|
|
48
|
+
{ command: "gentle:whatever2", label: "gentle models here" },
|
|
49
|
+
// rank 0: the label starts with "mod".
|
|
50
|
+
{ command: "gentle:whatever1", label: "Modify a thing" },
|
|
51
|
+
],
|
|
52
|
+
};
|
|
53
|
+
const ranked = rankPaletteGroups([group], "mod");
|
|
54
|
+
assert.equal(ranked.length, 1);
|
|
55
|
+
assert.deepEqual(
|
|
56
|
+
ranked[0]?.items.map((item) => item.command),
|
|
57
|
+
["gentle:whatever1", "gentle:whatever2", "gentle:whatever3", "gentle:mod-tool", "gentle:xyz-tool"],
|
|
58
|
+
);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("rankPaletteGroups is case-insensitive and trims the query", () => {
|
|
62
|
+
const group: CommandPaletteGroup = { title: "G", items: [{ command: "a", label: "Alpha Mod" }, { command: "b", label: "beta" }] };
|
|
63
|
+
const lower = rankPaletteGroups([group], "mod").map((g) => g.items.map((i) => i.command));
|
|
64
|
+
assert.deepEqual(rankPaletteGroups([group], "MOD").map((g) => g.items.map((i) => i.command)), lower);
|
|
65
|
+
assert.deepEqual(rankPaletteGroups([group], " mod ").map((g) => g.items.map((i) => i.command)), lower);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("rankPaletteGroups keeps input order within a group for tied ranks", () => {
|
|
69
|
+
const group: CommandPaletteGroup = { title: "G", items: [{ command: "mod-b", label: "mod-b" }, { command: "mod-a", label: "mod-a" }] };
|
|
70
|
+
assert.deepEqual(rankPaletteGroups([group], "mod")[0]?.items.map((i) => i.command), ["mod-b", "mod-a"]);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("rankPaletteGroups returns every group unchanged for an empty or whitespace query", () => {
|
|
74
|
+
const groups: CommandPaletteGroup[] = [
|
|
75
|
+
{ title: "G1", items: [{ command: "a", label: "Alpha" }] },
|
|
76
|
+
{ title: "G2", items: [{ command: "b", label: "Beta" }] },
|
|
77
|
+
];
|
|
78
|
+
assert.deepEqual(rankPaletteGroups(groups, ""), groups);
|
|
79
|
+
assert.deepEqual(rankPaletteGroups(groups, " "), groups);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("rankPaletteGroups drops a group with no matching item, and never reorders groups", () => {
|
|
83
|
+
const groups: CommandPaletteGroup[] = [
|
|
84
|
+
{ title: "Configuration", items: [{ command: "gentle:models", label: "Assign models and effort" }, { command: "gentle:profiles", label: "Agent-model profiles" }] },
|
|
85
|
+
{ title: "Session", items: [{ command: "gentle:usage", label: "Subscription usage" }] },
|
|
86
|
+
{ title: "Diagnostics", items: [{ command: "gentle:status", label: "Gentle AI status" }] },
|
|
87
|
+
];
|
|
88
|
+
const ranked = rankPaletteGroups(groups, "model");
|
|
89
|
+
assert.deepEqual(ranked.map((g) => g.title), ["Configuration"]);
|
|
90
|
+
assert.deepEqual(ranked[0]?.items.map((i) => i.command), ["gentle:models", "gentle:profiles"]);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("rankPaletteGroups returns no groups for a query that matches nothing anywhere", () => {
|
|
94
|
+
const groups: CommandPaletteGroup[] = [{ title: "G", items: [{ command: "a", label: "Alpha", description: "first letter" }] }];
|
|
95
|
+
assert.deepEqual(rankPaletteGroups(groups, "qqqqzzz"), []);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// --- input flow --------------------------------------------------------------
|
|
99
|
+
|
|
100
|
+
const GROUPS: CommandPaletteGroup[] = [
|
|
101
|
+
{
|
|
102
|
+
title: "Configuration",
|
|
103
|
+
items: [
|
|
104
|
+
{ command: "gentle:models", label: "Assign models and effort" },
|
|
105
|
+
{ command: "gentle:profiles", label: "Agent-model profiles" },
|
|
106
|
+
],
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
title: "Session",
|
|
110
|
+
items: [{ command: "gentle:changes", label: "Browse captured changes", shortcut: "alt+g" }],
|
|
111
|
+
},
|
|
112
|
+
];
|
|
113
|
+
|
|
114
|
+
test("typing filters across groups and resets the selection to the first visible item", () => {
|
|
115
|
+
const { palette, results } = createPalette(GROUPS);
|
|
116
|
+
palette.handleInput(KEY.DOWN);
|
|
117
|
+
palette.handleInput(KEY.DOWN); // selection is now "gentle:changes" (index 2)
|
|
118
|
+
// "s" keeps every item (each label contains "s") in the same relative
|
|
119
|
+
// order, so this only exercises the selection reset: without it, enter
|
|
120
|
+
// would still pick "gentle:changes".
|
|
121
|
+
palette.handleInput("s");
|
|
122
|
+
palette.handleInput(KEY.ENTER);
|
|
123
|
+
assert.deepEqual(results, [{ type: "run", name: "gentle:models" }]);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("backspace removes the last query character", () => {
|
|
127
|
+
const { palette, results } = createPalette(GROUPS);
|
|
128
|
+
type(palette, "changez");
|
|
129
|
+
palette.handleInput(KEY.BACKSPACE);
|
|
130
|
+
palette.handleInput(KEY.ENTER);
|
|
131
|
+
assert.deepEqual(results, [{ type: "run", name: "gentle:changes" }]);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("ctrl+u clears the query", () => {
|
|
135
|
+
const { palette, results } = createPalette(GROUPS);
|
|
136
|
+
type(palette, "changes");
|
|
137
|
+
palette.handleInput(KEY.CTRL_U);
|
|
138
|
+
palette.handleInput(KEY.ENTER);
|
|
139
|
+
assert.deepEqual(results, [{ type: "run", name: "gentle:models" }]);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test("up and down move the selection across groups and clamp at the ends", () => {
|
|
143
|
+
const { palette, results } = createPalette(GROUPS);
|
|
144
|
+
palette.handleInput(KEY.UP); // clamps at 0, still "gentle:models"
|
|
145
|
+
palette.handleInput(KEY.DOWN); // gentle:profiles
|
|
146
|
+
palette.handleInput(KEY.DOWN); // gentle:changes (crosses into the Session group)
|
|
147
|
+
palette.handleInput(KEY.DOWN); // clamps at the last item
|
|
148
|
+
palette.handleInput(KEY.ENTER);
|
|
149
|
+
assert.deepEqual(results, [{ type: "run", name: "gentle:changes" }]);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("ctrl+j and ctrl+k also move the selection", () => {
|
|
153
|
+
const { palette, results } = createPalette(GROUPS);
|
|
154
|
+
palette.handleInput(KEY.CTRL_J); // gentle:profiles
|
|
155
|
+
palette.handleInput(KEY.CTRL_K); // gentle:models
|
|
156
|
+
palette.handleInput(KEY.ENTER);
|
|
157
|
+
assert.deepEqual(results, [{ type: "run", name: "gentle:models" }]);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("enter runs the highlighted item and is a no-op with no match", () => {
|
|
161
|
+
const { palette, results } = createPalette(GROUPS);
|
|
162
|
+
type(palette, "nothing-like-this");
|
|
163
|
+
palette.handleInput(KEY.ENTER);
|
|
164
|
+
assert.deepEqual(results, []);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test("escape and ctrl+c close without running", () => {
|
|
168
|
+
const escapePalette = createPalette(GROUPS);
|
|
169
|
+
escapePalette.palette.handleInput(KEY.ESCAPE);
|
|
170
|
+
assert.deepEqual(escapePalette.results, [{ type: "close" }]);
|
|
171
|
+
|
|
172
|
+
const ctrlCPalette = createPalette(GROUPS);
|
|
173
|
+
ctrlCPalette.palette.handleInput(KEY.CTRL_C);
|
|
174
|
+
assert.deepEqual(ctrlCPalette.results, [{ type: "close" }]);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test("a second input after completion is ignored", () => {
|
|
178
|
+
const { palette, results } = createPalette(GROUPS);
|
|
179
|
+
palette.handleInput(KEY.ESCAPE);
|
|
180
|
+
palette.handleInput(KEY.ENTER);
|
|
181
|
+
palette.handleInput("x");
|
|
182
|
+
assert.deepEqual(results, [{ type: "close" }]);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test("a key release event is ignored even when it would otherwise match a binding", () => {
|
|
186
|
+
const { palette, results } = createPalette(GROUPS);
|
|
187
|
+
// ":3B" flags a Kitty protocol key release for the down arrow.
|
|
188
|
+
palette.handleInput("\x1b[1;1:3B");
|
|
189
|
+
palette.handleInput(KEY.ENTER);
|
|
190
|
+
assert.deepEqual(results, [{ type: "run", name: "gentle:models" }]);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// --- rendering ---------------------------------------------------------------
|
|
194
|
+
|
|
195
|
+
function render(groups: readonly CommandPaletteGroup[], query: string, width = 60, theme?: CommandPaletteTheme, rows?: () => number): string[] {
|
|
196
|
+
const { palette } = createPalette(groups, theme, rows);
|
|
197
|
+
type(palette, query);
|
|
198
|
+
return palette.render(width);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
test("render shows the header (Commands / esc), the search hint, and the typed query", () => {
|
|
202
|
+
const lines = render(GROUPS, "");
|
|
203
|
+
const header = lines.find((line) => line.includes("Commands"));
|
|
204
|
+
assert.ok(header, "expected a header line with Commands");
|
|
205
|
+
assert.match(header!, /Commands/);
|
|
206
|
+
assert.match(header!, /esc\s*│?\s*$/);
|
|
207
|
+
assert.ok(lines.some((line) => line.includes("› Search")));
|
|
208
|
+
|
|
209
|
+
const typed = render(GROUPS, "models");
|
|
210
|
+
assert.ok(typed.some((line) => line.includes("› models")));
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test("render shows group titles and one row per item, with the shortcut right-aligned", () => {
|
|
214
|
+
const lines = render(GROUPS, "").join("\n");
|
|
215
|
+
assert.match(lines, /Configuration/);
|
|
216
|
+
assert.match(lines, /Session/);
|
|
217
|
+
assert.match(lines, /Assign models and effort/);
|
|
218
|
+
assert.match(lines, /Agent-model profiles/);
|
|
219
|
+
const changesLine = render(GROUPS, "").find((line) => line.includes("Browse captured changes"));
|
|
220
|
+
assert.ok(changesLine, "expected a row for Browse captured changes");
|
|
221
|
+
assert.match(changesLine!, /Browse captured changes[\s\S]*alt\+g/);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test("render omits the shortcut column for an item without one", () => {
|
|
225
|
+
const lines = render(GROUPS, "").join("\n");
|
|
226
|
+
const modelsLine = render(GROUPS, "")
|
|
227
|
+
.filter((line) => line.includes("Assign models and effort"))
|
|
228
|
+
.join("\n");
|
|
229
|
+
assert.doesNotMatch(modelsLine, /undefined/);
|
|
230
|
+
assert.ok(lines.length > 0);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test("render marks the selected row with a highlighted bar when the theme provides bg", () => {
|
|
234
|
+
const bgTheme: CommandPaletteTheme = {
|
|
235
|
+
fg: (_color, text) => text,
|
|
236
|
+
bg: (color, text) => `[bg:${color}]${text}[/bg]`,
|
|
237
|
+
};
|
|
238
|
+
const lines = render(GROUPS, "", 60, bgTheme);
|
|
239
|
+
const selectedLine = lines.find((line) => line.includes("[bg:selectedBg]"));
|
|
240
|
+
assert.ok(selectedLine, "expected a bg-highlighted selected row");
|
|
241
|
+
assert.match(selectedLine!, /Assign models and effort/);
|
|
242
|
+
assert.doesNotMatch(selectedLine!, /▸/);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test("render falls back to a ▸ marker when the theme has no bg", () => {
|
|
246
|
+
const fgOnlyTheme: CommandPaletteTheme = { fg: (_color, text) => text };
|
|
247
|
+
const lines = render(GROUPS, "", 60, fgOnlyTheme);
|
|
248
|
+
const selectedLine = lines.find((line) => line.includes("▸"));
|
|
249
|
+
assert.ok(selectedLine, "expected a ▸ marker when bg is unavailable");
|
|
250
|
+
assert.match(selectedLine!, /Assign models and effort/);
|
|
251
|
+
assert.doesNotMatch(lines.join("\n"), /\[bg:/);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
test("render shows a muted no-match row when nothing matches", () => {
|
|
255
|
+
const lines = render(GROUPS, "does-not-exist").join("\n");
|
|
256
|
+
assert.match(lines, /No commands match "does-not-exist"/);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
test("render escapes control characters in labels and shortcuts", () => {
|
|
260
|
+
const groups: CommandPaletteGroup[] = [{ title: "G", items: [{ command: "gentle:evil", label: "line1\x07line2", shortcut: "alt+\x07" }] }];
|
|
261
|
+
const lines = render(groups, "").join("\n");
|
|
262
|
+
assert.doesNotMatch(lines, /\x07/);
|
|
263
|
+
assert.match(lines, /\\x07/);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test("render footer lists the key bindings", () => {
|
|
267
|
+
const lines = render(GROUPS, "").join("\n");
|
|
268
|
+
assert.match(lines, /type to search • ↑\/↓ move • enter run • esc close/);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
test("render keeps the right border aligned when a label has wide characters", () => {
|
|
272
|
+
// Both labels are exactly 10 UTF-16 code units, so a length-based pad
|
|
273
|
+
// treats the two rows as equally wide and pads them by the same amount.
|
|
274
|
+
// Their display widths differ (20 columns of CJK vs. 10 ASCII columns),
|
|
275
|
+
// so only a visibleWidth-based pad keeps the right border aligned.
|
|
276
|
+
const groups: CommandPaletteGroup[] = [
|
|
277
|
+
{
|
|
278
|
+
title: "G",
|
|
279
|
+
items: [
|
|
280
|
+
{ command: "gentle:emoji", label: "中文中文中文中文中文" },
|
|
281
|
+
{ command: "gentle:plain", label: "plain desc" },
|
|
282
|
+
],
|
|
283
|
+
},
|
|
284
|
+
];
|
|
285
|
+
const lines = render(groups, "", 60);
|
|
286
|
+
const widths = new Set(lines.map((line) => visibleWidth(line)));
|
|
287
|
+
assert.equal(widths.size, 1, `expected every rendered line to share one visible width, got ${[...widths].join(", ")}`);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
test("height follows rows() with scroll indicators when 30 items do not fit in 24 rows", () => {
|
|
291
|
+
const groups: CommandPaletteGroup[] = Array.from({ length: 3 }, (_, groupIndex) => ({
|
|
292
|
+
title: `Group ${groupIndex}`,
|
|
293
|
+
items: Array.from({ length: 10 }, (_, itemIndex) => ({ command: `cmd:${groupIndex}-${itemIndex}`, label: `Item ${groupIndex}-${itemIndex}` })),
|
|
294
|
+
}));
|
|
295
|
+
const { palette } = createPalette(groups, undefined, () => 24);
|
|
296
|
+
for (let i = 0; i < 15; i++) palette.handleInput(KEY.DOWN);
|
|
297
|
+
const lines = palette.render(80);
|
|
298
|
+
assert.ok(lines.length <= 20, `expected at most 20 total lines for rows()=24, got ${lines.length}`);
|
|
299
|
+
assert.ok(lines.some((line) => /↑ \d+ more/.test(line)));
|
|
300
|
+
assert.ok(lines.some((line) => /↓ \d+ more/.test(line)));
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
test("a small terminal still renders at least the minimum 12-line card", () => {
|
|
304
|
+
const { palette } = createPalette(GROUPS, undefined, () => 4);
|
|
305
|
+
const lines = palette.render(60);
|
|
306
|
+
assert.ok(lines.length <= 12, `expected at most 12 total lines for a tiny terminal, got ${lines.length}`);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
// --- commandsKey ---------------------------------------------------------------
|
|
310
|
+
|
|
311
|
+
test("commandsKey defaults to alt+k", () => {
|
|
312
|
+
assert.equal(commandsKey({}), "alt+k");
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
test("commandsKey honors an override", () => {
|
|
316
|
+
assert.equal(commandsKey({ GENTLE_PI_COMMANDS_KEY: "ctrl+p" }), "ctrl+p");
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
test("commandsKey is disabled by an empty value or off (case-insensitive)", () => {
|
|
320
|
+
assert.equal(commandsKey({ GENTLE_PI_COMMANDS_KEY: "" }), undefined);
|
|
321
|
+
assert.equal(commandsKey({ GENTLE_PI_COMMANDS_KEY: "off" }), undefined);
|
|
322
|
+
assert.equal(commandsKey({ GENTLE_PI_COMMANDS_KEY: "OFF" }), undefined);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
// --- catalog -------------------------------------------------------------------
|
|
326
|
+
|
|
327
|
+
test("COMMAND_PALETTE_CATALOG matches the curated command set, in order", () => {
|
|
328
|
+
assert.deepEqual(
|
|
329
|
+
COMMAND_PALETTE_CATALOG.map((group) => group.title),
|
|
330
|
+
["Configuration", "Session", "Diagnostics", "SDD", "Skills"],
|
|
331
|
+
);
|
|
332
|
+
const byTitle = (title: string) => COMMAND_PALETTE_CATALOG.find((group) => group.title === title)?.items.map((item) => item.command);
|
|
333
|
+
assert.deepEqual(byTitle("Configuration"), [
|
|
334
|
+
"gentle:models",
|
|
335
|
+
"gentle:profiles",
|
|
336
|
+
"gentle:persona",
|
|
337
|
+
"gentle:review-mode",
|
|
338
|
+
"gentle:background-subagents",
|
|
339
|
+
"gentle:telemetry",
|
|
340
|
+
"gentle:banner",
|
|
341
|
+
"gentle:banner-color",
|
|
342
|
+
"gentle:toggle-rose",
|
|
343
|
+
"gentle:toggle-text-logo",
|
|
344
|
+
"gentle:dev-binary",
|
|
345
|
+
]);
|
|
346
|
+
assert.deepEqual(byTitle("Session"), ["gentle:changes", "gentle:agents", "gentle:usage", "gentle:review-session-permission"]);
|
|
347
|
+
assert.deepEqual(byTitle("Diagnostics"), ["gentle:status", "gentle:doctor"]);
|
|
348
|
+
assert.deepEqual(byTitle("SDD"), ["gentle:sdd-preflight", "gentle-sdd-status", "gentle-sdd-continue", "gentle-sdd-init"]);
|
|
349
|
+
assert.deepEqual(byTitle("Skills"), ["skill-registry:refresh"]);
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
test("buildCommandPaletteGroups keeps only registered commands, attaches descriptions and shortcuts, drops empty groups, preserves catalog order", () => {
|
|
353
|
+
const registered = [
|
|
354
|
+
{ name: "gentle:models", description: "Configure models" },
|
|
355
|
+
{ name: "gentle:changes", description: "Browse changes" },
|
|
356
|
+
{ name: "gentle:status", description: "Show status" },
|
|
357
|
+
{ name: "skill-registry:refresh", description: "Refresh registry" },
|
|
358
|
+
];
|
|
359
|
+
const shortcuts = { "gentle:changes": "alt+g" };
|
|
360
|
+
const groups = buildCommandPaletteGroups(registered, shortcuts);
|
|
361
|
+
assert.deepEqual(
|
|
362
|
+
groups.map((group) => group.title),
|
|
363
|
+
["Configuration", "Session", "Diagnostics", "Skills"],
|
|
364
|
+
);
|
|
365
|
+
const configuration = groups.find((group) => group.title === "Configuration");
|
|
366
|
+
assert.deepEqual(configuration?.items.map((item) => item.command), ["gentle:models"]);
|
|
367
|
+
assert.equal(configuration?.items[0]?.description, "Configure models");
|
|
368
|
+
assert.equal(configuration?.items[0]?.shortcut, undefined);
|
|
369
|
+
const session = groups.find((group) => group.title === "Session");
|
|
370
|
+
assert.deepEqual(session?.items.map((item) => item.command), ["gentle:changes"]);
|
|
371
|
+
assert.equal(session?.items[0]?.shortcut, "alt+g");
|
|
372
|
+
assert.equal(groups.some((group) => group.title === "SDD"), false);
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
test("buildCommandPaletteGroups drops every group when nothing in the catalog is registered", () => {
|
|
376
|
+
const groups = buildCommandPaletteGroups([{ name: "not-in-catalog", description: "n/a" }], {});
|
|
377
|
+
assert.deepEqual(groups, []);
|
|
378
|
+
});
|
|
@@ -93,7 +93,7 @@ test("SDD executor coverage is exhaustive against actual agent files", () => {
|
|
|
93
93
|
const expected = [
|
|
94
94
|
"sdd-apply.md", "sdd-archive.md", "sdd-design.md", "sdd-explore.md",
|
|
95
95
|
"sdd-init.md", "sdd-onboard.md", "sdd-proposal.md", "sdd-remediate.md", "sdd-research.md",
|
|
96
|
-
"sdd-spec.md", "sdd-status.md", "sdd-
|
|
96
|
+
"sdd-spec.md", "sdd-status.md", "sdd-tasks.md",
|
|
97
97
|
"sdd-verify.md",
|
|
98
98
|
];
|
|
99
99
|
assert.deepEqual(actual, expected, "SDD agent set must match the known allowlist");
|
|
@@ -231,7 +231,7 @@ test("modified SDD agents are packaged and installed by the existing installer",
|
|
|
231
231
|
"assets/agents/sdd-init.md", "assets/agents/sdd-onboard.md",
|
|
232
232
|
"assets/agents/sdd-proposal.md", "assets/agents/sdd-research.md",
|
|
233
233
|
"assets/agents/sdd-spec.md", "assets/agents/sdd-status.md",
|
|
234
|
-
"assets/agents/sdd-
|
|
234
|
+
"assets/agents/sdd-tasks.md",
|
|
235
235
|
"assets/agents/sdd-verify.md",
|
|
236
236
|
];
|
|
237
237
|
for (const path of expected) {
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { mkdir } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { ActiveSessionClient, ActiveSessionClientError, ActiveSessionListener, SessionPresenceRegistry } from "../../lib/agents-session-transport.ts";
|
|
4
|
+
|
|
5
|
+
const agentHome = process.env.GENTLE_AGENT_HOME;
|
|
6
|
+
const CONTROL_BYTES = 16_384;
|
|
7
|
+
if (!agentHome) throw new Error("GENTLE_AGENT_HOME is required");
|
|
8
|
+
|
|
9
|
+
await mkdir(join(agentHome, "gentle-agents"), { recursive: true, mode: 0o755 });
|
|
10
|
+
const registry = await SessionPresenceRegistry.create(agentHome);
|
|
11
|
+
let listener;
|
|
12
|
+
let client;
|
|
13
|
+
let blockShutdown = false;
|
|
14
|
+
let blocker;
|
|
15
|
+
let closed = false;
|
|
16
|
+
let input = Buffer.alloc(0);
|
|
17
|
+
|
|
18
|
+
const record = (value) => ({ ...value, presencePath: registry.presencePath(value) });
|
|
19
|
+
const controlLine = (value) => {
|
|
20
|
+
const line = JSON.stringify(value);
|
|
21
|
+
if (Buffer.byteLength(line) > CONTROL_BYTES) throw new Error("control frame exceeds bound");
|
|
22
|
+
return `${line}\n`;
|
|
23
|
+
};
|
|
24
|
+
const emit = (value) => { process.stdout.write(controlLine(value)); };
|
|
25
|
+
const flush = (value) => new Promise((resolveFlush, rejectFlush) => {
|
|
26
|
+
process.stdout.write(controlLine(value), (error) => error ? rejectFlush(error) : resolveFlush());
|
|
27
|
+
});
|
|
28
|
+
const stop = async () => {
|
|
29
|
+
const current = listener?.record;
|
|
30
|
+
client?.close();
|
|
31
|
+
client = undefined;
|
|
32
|
+
if (blocker) clearInterval(blocker);
|
|
33
|
+
blocker = undefined;
|
|
34
|
+
if (listener) await listener.close();
|
|
35
|
+
listener = undefined;
|
|
36
|
+
return current ? record(current) : {};
|
|
37
|
+
};
|
|
38
|
+
const reply = (requestId, ok, result, error) => emit({ requestId, ok, ...(ok ? { result } : { error }) });
|
|
39
|
+
const fatal = () => {
|
|
40
|
+
if (closed) return;
|
|
41
|
+
closed = true;
|
|
42
|
+
input = Buffer.alloc(0);
|
|
43
|
+
process.stdin.off("data", onData);
|
|
44
|
+
void stop().finally(() => process.exit(2));
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
async function handle(line) {
|
|
48
|
+
let text;
|
|
49
|
+
try { text = new TextDecoder("utf-8", { fatal: true }).decode(line); }
|
|
50
|
+
catch { fatal(); return; }
|
|
51
|
+
let command;
|
|
52
|
+
try {
|
|
53
|
+
command = JSON.parse(text);
|
|
54
|
+
if (!command || typeof command !== "object" || typeof command.requestId !== "string" || typeof command.operation !== "string") throw new Error("invalid control frame");
|
|
55
|
+
let result;
|
|
56
|
+
if (command.operation === "start") {
|
|
57
|
+
await stop();
|
|
58
|
+
listener = new ActiveSessionListener(registry, command.sessionId, async (notification) => {
|
|
59
|
+
emit({ event: "callback", requestId: notification.id, pid: process.pid, ...notification });
|
|
60
|
+
});
|
|
61
|
+
await listener.start();
|
|
62
|
+
client = new ActiveSessionClient(registry, command.sessionId);
|
|
63
|
+
result = record(listener.record);
|
|
64
|
+
} else if (command.operation === "send") {
|
|
65
|
+
if (!client) throw new Error("listener is not active");
|
|
66
|
+
result = await client.sendNotification(command.recipientSessionId, command.message, { id: command.id });
|
|
67
|
+
} else if (command.operation === "resolve") result = record(await registry.resolve(command.sessionId));
|
|
68
|
+
else if (command.operation === "stop") result = await stop();
|
|
69
|
+
else if (command.operation === "delay") {
|
|
70
|
+
if (!Number.isInteger(command.delayMs) || command.delayMs < 0 || command.delayMs > 2_500) throw new Error("invalid test delay");
|
|
71
|
+
await new Promise((resolveDelay) => setTimeout(resolveDelay, command.delayMs));
|
|
72
|
+
result = { delayed: true };
|
|
73
|
+
} else if (command.operation === "exit") process.exit(23);
|
|
74
|
+
else if (command.operation === "block-shutdown") {
|
|
75
|
+
blockShutdown = true;
|
|
76
|
+
blocker ??= setInterval(() => {}, 1_000);
|
|
77
|
+
result = { armed: true };
|
|
78
|
+
}
|
|
79
|
+
else if (command.operation === "shutdown") {
|
|
80
|
+
if (blockShutdown) return;
|
|
81
|
+
result = await stop();
|
|
82
|
+
await flush({ requestId: command.requestId, ok: true, result });
|
|
83
|
+
process.exit(0);
|
|
84
|
+
} else throw new Error("unknown operation");
|
|
85
|
+
reply(command.requestId, true, result);
|
|
86
|
+
} catch (error) {
|
|
87
|
+
const message = error instanceof ActiveSessionClientError ? error.code : "fixture_error";
|
|
88
|
+
reply(typeof command?.requestId === "string" ? command.requestId : "invalid", false, undefined, message);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
let queue = Promise.resolve();
|
|
93
|
+
function onData(chunk) {
|
|
94
|
+
let start = 0;
|
|
95
|
+
while (start < chunk.length) {
|
|
96
|
+
const newline = chunk.indexOf(10, start), end = newline < 0 ? chunk.length : newline;
|
|
97
|
+
const fragment = chunk.subarray(start, end);
|
|
98
|
+
if (input.length + fragment.length > CONTROL_BYTES) { fatal(); return; }
|
|
99
|
+
if (newline < 0) { input = Buffer.concat([input, fragment]); return; }
|
|
100
|
+
const line = input.length ? Buffer.concat([input, fragment]) : fragment;
|
|
101
|
+
input = Buffer.alloc(0);
|
|
102
|
+
queue = queue.then(() => handle(line)).catch(fatal);
|
|
103
|
+
start = newline + 1;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
process.stdin.on("data", onData);
|
|
108
|
+
process.stdin.on("error", fatal);
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: sdd-research
|
|
3
|
+
description: Collect auditable external evidence for a selected SDD research lane.
|
|
4
|
+
tools:
|
|
5
|
+
- read
|
|
6
|
+
- grep
|
|
7
|
+
- find
|
|
8
|
+
- edit
|
|
9
|
+
- write
|
|
10
|
+
- mem_search
|
|
11
|
+
- mem_get_observation
|
|
12
|
+
- mem_save
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
You are the SDD research executor for Gentle AI.
|
|
16
|
+
|
|
17
|
+
## Skill Resolution Contract
|
|
18
|
+
|
|
19
|
+
Use your assigned executor/phase skill for this SDD phase. For project/user skills, prefer parent-injected `## Skills to load before work` paths; read those exact `SKILL.md` files before work. Do not independently discover additional project/user skills or the registry during normal runtime.
|
|
20
|
+
|
|
21
|
+
If skill paths are missing, explicit fallback loading is allowed only as degraded self-healing. Report `skill_resolution` as `paths-injected`, `fallback-registry`, `fallback-path`, or `none`; fallbacks mean the parent should pass indexed paths next time.
|
|
22
|
+
|
|
23
|
+
- Run only when the orchestrator selects `sdd-research` and supplies the persisted research intent: the change name, the questions, the requested source classes, and the artifact store. Treat that intent as immutable; if it is absent, return `blocked` with no claims.
|
|
24
|
+
- Evidence grants for this runtime are `documentation=[]; open-web=[]`. Never infer evidence capability from bash, persistence tools, or any inherited tool; persistence tools are not evidence grants. Unsupported or undeclared classes deny admission and emit no claims.
|
|
25
|
+
- Because this runtime declares no evidence grants, retain the selected request, persist a `blocked` outcome with no claims, and stop.
|
|
26
|
+
- Admission denial, partial evidence, invalid sources, or persistence divergence emits no unvalidated claim and blocks proposal readiness.
|
|
27
|
+
- Keep evidence claims separate from non-authoritative product choices; the orchestrator owns product decisions and proposal admission.
|
|
28
|
+
- Do NOT launch child subagents. Parent/orchestrator owns delegation.
|
|
29
|
+
- Persist the research and pre-proposal artifacts per the Memory Contract below; never claim persistence you did not perform.
|
|
30
|
+
- Keep output concise and return the SDD result contract.
|
|
31
|
+
## Memory Contract
|
|
32
|
+
|
|
33
|
+
Read any input artifacts directly from the active backend before doing the phase work; do not wait for the parent to inline them. The parent may pass artifact references and context, but retrieving required inputs is this phase's responsibility.
|
|
34
|
+
|
|
35
|
+
Inputs to read (`engram`/`both`: use the injected Engram memory read tools for the topic key, then fetch the full observation; `openspec`: read the file under `openspec/changes/{change}/`):
|
|
36
|
+
- Exploration (when it exists): `sdd/{change}/explore` (openspec: the exploration file under `openspec/changes/{change}/`).
|
|
37
|
+
|
|
38
|
+
Persist this phase's artifact to the active backend before returning (mandatory):
|
|
39
|
+
- `engram`/`both`: call the injected Engram save tool with title and `topic_key` `"sdd/{change}/research"`, `type: "architecture"`, `project` from context, and `capture_prompt: false` when the tool schema supports it (omit the field if an older schema rejects it).
|
|
40
|
+
- `openspec`: write/update `openspec/changes/{change}/research.md`.
|
|
41
|
+
- `none`: return the research record inline.
|
|
42
|
+
|
|
43
|
+
The research artifact uses schema `gentle-ai.sdd-research/v1`: a positive `revision`, an explicit `done | partial | blocked` outcome, the questions, admission and the observed exact grants, sources, and validated claims where each claim maps to source IDs. For this runtime the outcome is `blocked` with an admission denial and no claims.
|
|
44
|
+
|
|
45
|
+
Also update the pre-proposal state (`engram`/`both`: topic `"sdd/{change}/preproposal"`; same save conventions) using schema `gentle-ai.sdd-preproposal/v1`: a positive `revision`, the exploration reference, the research request and classes, the admission outcome, evidence references, product decisions (`pending | confirmed`), and `proposal_ready`.
|
|
46
|
+
|
|
47
|
+
Hybrid (`both`) persistence means identical bytes in both stores. On hybrid mismatch or a one-sided write failure, never prefer one store: recover from the retained intent, not from a surviving store, and keep proposal readiness false for recovery.
|
|
48
|
+
|
|
49
|
+
Never claim persistence you did not perform.
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
## Key Learnings Closing
|
|
53
|
+
|
|
54
|
+
Close your final report text with a `## Key Learnings` block (no trailing colon). Use 1–5 numbered items, each a standalone factual sentence of at least 20 characters and at least 4 words. This applies to final report text only — not intermediate tool output or saved artifact content. The Engram memory provider automatically extracts and persists these items as passive capture; you do not parse the block or invoke passive-capture tools yourself. Omit the block when there is genuinely no reusable learning; no filler or speculation. This closing block is separate from explicit `mem_save` artifact/decision persistence.
|