gentle-pi 2.7.0 → 3.1.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 +175 -36
- 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 +28 -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 +27 -96
- package/runtime/windows-session-transport.ps1 +791 -0
- package/scripts/gentle-ai-installer.mjs +12 -12
- package/scripts/test-packed-runner.mjs +1668 -20
- package/scripts/verify-package-files.mjs +2 -3
- 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-binary.test.ts +3 -3
- package/tests/gentle-ai-installer.test.ts +51 -51
- 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-capability-contract.test.ts +30 -1
- 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 +119 -31
- 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
|
@@ -4,7 +4,7 @@ import { mkdirSync, mkdtempSync, realpathSync, renameSync, rmSync, writeFileSync
|
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
import test from "node:test";
|
|
7
|
-
import { initTheme, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
7
|
+
import { initTheme, type ExtensionAPI, type ExtensionContext, type SlashCommandInfo, type SourceInfo } from "@earendil-works/pi-coding-agent";
|
|
8
8
|
import type { TUI } from "@earendil-works/pi-tui";
|
|
9
9
|
import installGentleShell, { buildShellBarModel, createActiveProfileReader, changesShortcut, devBinaryCard, fetchCodexUsage, loadFileDiff, shellGitRunner, openInExternalEditor, type GentlePromptEditor } from "../extensions/gentle-shell.ts";
|
|
10
10
|
import { CHANGE_STATUS } from "../lib/shell-changes.ts";
|
|
@@ -63,12 +63,25 @@ interface ShortcutRegistration {
|
|
|
63
63
|
type MessageRenderer = (message: { customType: string; content: unknown }, options: { expanded: boolean }, theme: unknown) => { render(width: number): string[] };
|
|
64
64
|
const renderers = new Map<string, MessageRenderer>();
|
|
65
65
|
|
|
66
|
-
|
|
66
|
+
const FAKE_SOURCE_INFO: SourceInfo = { path: "extensions/gentle-shell.ts", source: "gentle-shell", scope: "project", origin: "top-level" };
|
|
67
|
+
|
|
68
|
+
const DEFAULT_COMMANDS: SlashCommandInfo[] = [
|
|
69
|
+
{ name: "gentle:models", description: "Configure models", source: "extension", sourceInfo: FAKE_SOURCE_INFO },
|
|
70
|
+
{ name: "gentle:changes", description: "Browse changes", source: "extension", sourceInfo: FAKE_SOURCE_INFO },
|
|
71
|
+
{ name: "gentle:status", description: "Show Gentle AI status", source: "extension", sourceInfo: FAKE_SOURCE_INFO },
|
|
72
|
+
{ name: "skill-registry:refresh", description: "Regenerate the skill registry", source: "extension", sourceInfo: FAKE_SOURCE_INFO },
|
|
73
|
+
{ name: "gentle:commands", description: "Open the command palette", source: "extension", sourceInfo: FAKE_SOURCE_INFO },
|
|
74
|
+
{ name: "gentle:not-in-catalog", description: "Not a curated command", source: "extension", sourceInfo: FAKE_SOURCE_INFO },
|
|
75
|
+
{ name: "skill:foo", description: "A skill", source: "skill", sourceInfo: FAKE_SOURCE_INFO },
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
function fakePi(script: GitScript[] = [{ numstat: "", porcelain: "" }], commandsList: SlashCommandInfo[] = DEFAULT_COMMANDS) {
|
|
67
79
|
const handlers = new Map<string, Array<(event: unknown, ctx: ExtensionContext) => unknown>>();
|
|
68
80
|
const commands = new Map<string, CommandRegistration>();
|
|
69
81
|
const shortcuts = new Map<string, ShortcutRegistration>();
|
|
70
82
|
const git: string[][] = [];
|
|
71
83
|
let entries: unknown[] = [];
|
|
84
|
+
const sentMessages: Array<{ content: string; options?: { deliverAs?: "steer" | "followUp"; expandPromptTemplates?: boolean } }> = [];
|
|
72
85
|
const tools = new Map<string, { renderShell?: string; execute(id: string, params: unknown, signal: undefined, update: undefined, ctx: ExtensionContext): Promise<unknown> }>();
|
|
73
86
|
const listeners = new Map<string, (data: unknown) => void>();
|
|
74
87
|
let round = 0;
|
|
@@ -94,6 +107,12 @@ function fakePi(script: GitScript[] = [{ numstat: "", porcelain: "" }]) {
|
|
|
94
107
|
getThinkingLevel() {
|
|
95
108
|
return "medium";
|
|
96
109
|
},
|
|
110
|
+
getCommands() {
|
|
111
|
+
return commandsList;
|
|
112
|
+
},
|
|
113
|
+
sendUserMessage(content: string, options?: { deliverAs?: "steer" | "followUp"; expandPromptTemplates?: boolean }) {
|
|
114
|
+
sentMessages.push({ content, options });
|
|
115
|
+
},
|
|
97
116
|
async exec(_command: string, args: string[]) {
|
|
98
117
|
git.push(args);
|
|
99
118
|
if (args.includes("worktree")) return { stdout: "worktree /repo\0branch refs/heads/main\0\0", stderr: "", code: 0, killed: false };
|
|
@@ -102,7 +121,7 @@ function fakePi(script: GitScript[] = [{ numstat: "", porcelain: "" }]) {
|
|
|
102
121
|
return { stdout: isNumstat ? step.numstat : step.porcelain, stderr: "", code: 0, killed: false };
|
|
103
122
|
},
|
|
104
123
|
} as unknown as ExtensionAPI;
|
|
105
|
-
return { pi, handlers, git, commands, shortcuts, tools };
|
|
124
|
+
return { pi, handlers, git, commands, shortcuts, tools, sentMessages };
|
|
106
125
|
}
|
|
107
126
|
|
|
108
127
|
async function fire(handlers: Map<string, Array<(event: unknown, ctx: ExtensionContext) => unknown>>, event: string, ctx: ExtensionContext): Promise<void> {
|
|
@@ -149,11 +168,15 @@ function fakeContext(options: { hasUI?: boolean; entries?: unknown[]; oauth?: bo
|
|
|
149
168
|
setWorkingVisible(visible: boolean) {
|
|
150
169
|
ui.workingVisible = visible;
|
|
151
170
|
},
|
|
152
|
-
|
|
171
|
+
// Generic over the result type: real callers resolve `custom` with
|
|
172
|
+
// whatever their `done` callback is given (see ExtensionUIContext.custom
|
|
173
|
+
// in pi-coding-agent), not always null. `closeOverlay` stays a
|
|
174
|
+
// null-resolving escape hatch for tests that only need to end the wait.
|
|
175
|
+
custom<T>(factory: (tui: unknown, theme: unknown, keybindings: unknown, done: (value: T) => void) => { render(width: number): string[]; handleInput(data: string): void }) {
|
|
153
176
|
ui.overlay = factory;
|
|
154
|
-
return new Promise<null>((resolve) => {
|
|
177
|
+
return new Promise<T | null>((resolve) => {
|
|
155
178
|
ui.closeOverlay = () => resolve(null);
|
|
156
|
-
ui.overlayView = factory(fakeTui, plainTheme, fakeKeybindings, () => resolve(
|
|
179
|
+
ui.overlayView = factory(fakeTui, plainTheme, fakeKeybindings, (value: T) => resolve(value));
|
|
157
180
|
resolveOverlay();
|
|
158
181
|
});
|
|
159
182
|
},
|
|
@@ -382,15 +405,41 @@ test("gentleShell shows working while the agent runs and queued when messages wa
|
|
|
382
405
|
const editor = installedPrompt(ctx, ui, handlers);
|
|
383
406
|
|
|
384
407
|
for (const handler of handlers.get("agent_start") ?? []) handler({}, ctx);
|
|
385
|
-
assert.match(stripAnsi(editor.render(60)[0]), /^╭─
|
|
408
|
+
assert.match(stripAnsi(editor.render(60)[0]), /^╭─ ✿ working… ─+╮$/);
|
|
386
409
|
pending.value = true;
|
|
387
410
|
assert.match(stripAnsi(editor.render(60)[0]), /^╭─ [✿❀❁✾] queued ─+╮$/);
|
|
388
411
|
for (const handler of handlers.get("agent_end") ?? []) handler({}, ctx);
|
|
412
|
+
assert.match(stripAnsi(editor.render(60)[0]), /queued/, "low-level run end is not settled");
|
|
389
413
|
pending.value = false;
|
|
414
|
+
for (const handler of handlers.get("agent_settled") ?? []) handler({}, ctx);
|
|
390
415
|
assert.match(stripAnsi(editor.render(60)[0]), /^╭─ ✿ ─+╮$/);
|
|
391
416
|
editor.dispose();
|
|
392
417
|
});
|
|
393
418
|
|
|
419
|
+
test("prompt uses the compact banner cadence and releases its unref timer at settlement", (t) => {
|
|
420
|
+
const delays: number[] = [];
|
|
421
|
+
let active = 0;
|
|
422
|
+
let unrefs = 0;
|
|
423
|
+
t.mock.method(globalThis, "setInterval", (_callback: () => void, delay: number) => {
|
|
424
|
+
delays.push(delay);
|
|
425
|
+
active++;
|
|
426
|
+
return { unref() { unrefs++; } };
|
|
427
|
+
});
|
|
428
|
+
t.mock.method(globalThis, "clearInterval", () => { active--; });
|
|
429
|
+
const { pi, handlers } = fakePi();
|
|
430
|
+
gentleShell(pi, {});
|
|
431
|
+
const { ctx, ui } = fakeContext();
|
|
432
|
+
const editor = installedPrompt(ctx, ui, handlers);
|
|
433
|
+
assert.equal(active, 0);
|
|
434
|
+
for (const handler of handlers.get("agent_start") ?? []) handler({}, ctx);
|
|
435
|
+
assert.deepEqual(delays, [80]);
|
|
436
|
+
assert.equal(unrefs, 1);
|
|
437
|
+
for (const handler of handlers.get("agent_settled") ?? []) handler({}, ctx);
|
|
438
|
+
assert.equal(active, 0);
|
|
439
|
+
editor.dispose();
|
|
440
|
+
assert.equal(active, 0);
|
|
441
|
+
});
|
|
442
|
+
|
|
394
443
|
test("gentleShell leaves an editor another extension already installed", () => {
|
|
395
444
|
const { pi, handlers } = fakePi();
|
|
396
445
|
gentleShell(pi, {});
|
|
@@ -398,6 +447,26 @@ test("gentleShell leaves an editor another extension already installed", () => {
|
|
|
398
447
|
const { ctx, ui } = fakeContext({ editorFactory: theirs });
|
|
399
448
|
for (const handler of handlers.get("session_start") ?? []) handler({}, ctx);
|
|
400
449
|
assert.equal(ui.editorFactory, theirs);
|
|
450
|
+
assert.notEqual(ui.workingVisible, false, "a custom owner still needs native working feedback");
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
test("Gentle replaces its retained factory on reload so new lifecycle handlers own the prompt", () => {
|
|
454
|
+
const first = fakePi();
|
|
455
|
+
gentleShell(first.pi, {});
|
|
456
|
+
const { ctx, ui } = fakeContext();
|
|
457
|
+
const oldEditor = installedPrompt(ctx, ui, first.handlers);
|
|
458
|
+
const previousFactory = ui.editorFactory;
|
|
459
|
+
const next = fakePi();
|
|
460
|
+
gentleShell(next.pi, {});
|
|
461
|
+
const editor = installedPrompt(ctx, ui, next.handlers);
|
|
462
|
+
try {
|
|
463
|
+
assert.notEqual(ui.editorFactory, previousFactory);
|
|
464
|
+
for (const handler of next.handlers.get("agent_start") ?? []) handler({}, ctx);
|
|
465
|
+
assert.match(stripAnsi(editor.render(60)[0]), /working/);
|
|
466
|
+
} finally {
|
|
467
|
+
oldEditor.dispose();
|
|
468
|
+
editor.dispose();
|
|
469
|
+
}
|
|
401
470
|
});
|
|
402
471
|
|
|
403
472
|
const footerData = { getGitBranch: () => "main", getExtensionStatuses: () => new Map(), getAvailableProviderCount: () => 1, onBranchChange: () => () => {} };
|
|
@@ -428,6 +497,13 @@ test("captured changes update the widget and bar without repository scans", asyn
|
|
|
428
497
|
const factory=ui.widgets.get("gentle-shell-changes") as any;
|
|
429
498
|
assert.match(factory(fakeTui,plainTheme).render(140)[0],/1 file · \+2 −0/);
|
|
430
499
|
assert.match(renderFooter(ui),/main ±1/);
|
|
500
|
+
const rail = sidebarState(fakeTui as unknown as TUI).parts.get("footer")!;
|
|
501
|
+
const digest = rail.digest!();
|
|
502
|
+
assert.match(rail.render(46).join("\n"), /1 file · \+2 −0/);
|
|
503
|
+
sessionChange(ctx,"b","/repo","lib/b.ts","one\ntwo\n","one\ntwo\nthree\n");
|
|
504
|
+
await fire(handlers,"agent_end",ctx);
|
|
505
|
+
assert.notEqual(rail.digest!(), digest, "same-count line changes invalidate the unified Status");
|
|
506
|
+
assert.match(rail.render(46).join("\n"), /1 file · \+3 −0/);
|
|
431
507
|
assert.equal(git.length,0);
|
|
432
508
|
await fire(handlers,"session_shutdown",ctx);
|
|
433
509
|
});
|
|
@@ -619,7 +695,7 @@ test("gentleShell binds the changes shortcut to the same handler as the command"
|
|
|
619
695
|
|
|
620
696
|
const silent = fakePi();
|
|
621
697
|
gentleShell(silent.pi, { GENTLE_PI_SHELL_CHANGES_KEY: "off" });
|
|
622
|
-
assert.equal(silent.shortcuts.
|
|
698
|
+
assert.equal(silent.shortcuts.has("alt+g"), false, "the changes shortcut must not register when disabled");
|
|
623
699
|
});
|
|
624
700
|
|
|
625
701
|
test("external edits do not pollute Changes or trigger background Git scans", async () => {
|
|
@@ -756,3 +832,120 @@ test("gentleShell keeps a dev-binary override visible above the editor for the w
|
|
|
756
832
|
|
|
757
833
|
assert.equal(devBinaryCard({ state: "invalid", reason: "binary missing" }).tone, "error");
|
|
758
834
|
});
|
|
835
|
+
|
|
836
|
+
test("gentle:commands registers alt+k by default", () => {
|
|
837
|
+
const { pi, shortcuts } = fakePi();
|
|
838
|
+
gentleShell(pi, {});
|
|
839
|
+
assert.ok(shortcuts.has("alt+k"));
|
|
840
|
+
});
|
|
841
|
+
|
|
842
|
+
test("gentle:commands honors GENTLE_PI_COMMANDS_KEY", () => {
|
|
843
|
+
const { pi, shortcuts } = fakePi();
|
|
844
|
+
gentleShell(pi, { GENTLE_PI_COMMANDS_KEY: "ctrl+p" });
|
|
845
|
+
assert.ok(shortcuts.has("ctrl+p"));
|
|
846
|
+
assert.equal(shortcuts.has("alt+k"), false);
|
|
847
|
+
});
|
|
848
|
+
|
|
849
|
+
test("GENTLE_PI_COMMANDS_KEY=off registers no command-palette shortcut", () => {
|
|
850
|
+
const { pi, shortcuts } = fakePi();
|
|
851
|
+
gentleShell(pi, { GENTLE_PI_COMMANDS_KEY: "off" });
|
|
852
|
+
assert.equal(shortcuts.has("alt+k"), false);
|
|
853
|
+
assert.ok(shortcuts.has("alt+g"), "the unrelated changes shortcut still registers");
|
|
854
|
+
});
|
|
855
|
+
|
|
856
|
+
test("/gentle:commands shows only curated, registered commands, grouped, by their labels", async () => {
|
|
857
|
+
const { pi, commands } = fakePi();
|
|
858
|
+
gentleShell(pi, {});
|
|
859
|
+
const { ctx, ui, overlayReady } = fakeContext();
|
|
860
|
+
const opened = commands.get("gentle:commands")!.handler("", ctx);
|
|
861
|
+
await overlayReady;
|
|
862
|
+
const lines = ui.overlayView!.render(100);
|
|
863
|
+
const rendered = lines.join("\n");
|
|
864
|
+
assert.match(rendered, /Configuration/);
|
|
865
|
+
assert.match(rendered, /Session/);
|
|
866
|
+
assert.match(rendered, /Diagnostics/);
|
|
867
|
+
assert.match(rendered, /Skills/);
|
|
868
|
+
assert.doesNotMatch(rendered, /\bSDD\b/, "the SDD group has no registered commands and must not appear");
|
|
869
|
+
assert.match(rendered, /Assign models and effort/);
|
|
870
|
+
assert.match(rendered, /Browse captured changes/);
|
|
871
|
+
assert.match(rendered, /Gentle AI status/);
|
|
872
|
+
assert.match(rendered, /Refresh skill registry/);
|
|
873
|
+
assert.doesNotMatch(rendered, /gentle:models|gentle:changes|gentle:status|skill-registry:refresh/, "raw command names must not leak; only labels are shown");
|
|
874
|
+
assert.doesNotMatch(rendered, /gentle:not-in-catalog/);
|
|
875
|
+
assert.doesNotMatch(rendered, /skill:foo/);
|
|
876
|
+
const changesLine = lines.find((line) => line.includes("Browse captured changes"));
|
|
877
|
+
assert.match(changesLine ?? "", /alt\+g/, "expected the configured alt+g shortcut hint next to Browse captured changes");
|
|
878
|
+
ui.overlayView!.handleInput("\x1b");
|
|
879
|
+
await opened;
|
|
880
|
+
});
|
|
881
|
+
|
|
882
|
+
test("selecting a command from the palette by its label sends the underlying command as a slash message", async () => {
|
|
883
|
+
const { pi, commands, sentMessages } = fakePi();
|
|
884
|
+
gentleShell(pi, {});
|
|
885
|
+
const { ctx, ui, overlayReady } = fakeContext();
|
|
886
|
+
const opened = commands.get("gentle:commands")!.handler("", ctx);
|
|
887
|
+
await overlayReady;
|
|
888
|
+
// "mod" only matches the "Assign models and effort" label (it contains
|
|
889
|
+
// "mod" via "models"); nothing else in the fixture does.
|
|
890
|
+
for (const ch of "mod") ui.overlayView!.handleInput(ch);
|
|
891
|
+
assert.match(ui.overlayView!.render(100).join("\n"), /Assign models and effort/);
|
|
892
|
+
ui.overlayView!.handleInput("\r");
|
|
893
|
+
await opened;
|
|
894
|
+
assert.deepEqual(sentMessages, [{ content: "/gentle:models", options: { expandPromptTemplates: true } }]);
|
|
895
|
+
});
|
|
896
|
+
|
|
897
|
+
test("escaping the palette sends no message", async () => {
|
|
898
|
+
const { pi, commands, sentMessages } = fakePi();
|
|
899
|
+
gentleShell(pi, {});
|
|
900
|
+
const { ctx, ui, overlayReady } = fakeContext();
|
|
901
|
+
const opened = commands.get("gentle:commands")!.handler("", ctx);
|
|
902
|
+
await overlayReady;
|
|
903
|
+
ui.overlayView!.handleInput("\x1b");
|
|
904
|
+
await opened;
|
|
905
|
+
assert.deepEqual(sentMessages, []);
|
|
906
|
+
});
|
|
907
|
+
|
|
908
|
+
test("/gentle:commands notifies when nothing in the catalog is registered", async () => {
|
|
909
|
+
const { pi, commands } = fakePi(undefined, [
|
|
910
|
+
{ name: "skill:foo", description: "A skill", source: "skill", sourceInfo: FAKE_SOURCE_INFO },
|
|
911
|
+
{ name: "gentle:not-in-catalog", description: "Not curated", source: "extension", sourceInfo: FAKE_SOURCE_INFO },
|
|
912
|
+
]);
|
|
913
|
+
gentleShell(pi, {});
|
|
914
|
+
const { ctx, ui } = fakeContext();
|
|
915
|
+
await commands.get("gentle:commands")!.handler("", ctx);
|
|
916
|
+
assert.match(ui.notices.join("\n"), /No Gentle commands are registered\./);
|
|
917
|
+
assert.equal(ui.overlay, undefined);
|
|
918
|
+
});
|
|
919
|
+
|
|
920
|
+
test("/gentle:commands does nothing in a headless context", async () => {
|
|
921
|
+
const { pi, commands, sentMessages } = fakePi();
|
|
922
|
+
gentleShell(pi, {});
|
|
923
|
+
const { ctx, ui } = fakeContext({ hasUI: false });
|
|
924
|
+
await commands.get("gentle:commands")!.handler("", ctx);
|
|
925
|
+
assert.equal(ui.overlay, undefined);
|
|
926
|
+
assert.deepEqual(sentMessages, []);
|
|
927
|
+
});
|
|
928
|
+
|
|
929
|
+
test("the alt+k shortcut opens the same command palette as the command", async () => {
|
|
930
|
+
const { pi, shortcuts } = fakePi();
|
|
931
|
+
gentleShell(pi, {});
|
|
932
|
+
const { ctx, ui, overlayReady } = fakeContext();
|
|
933
|
+
const shortcut = shortcuts.get("alt+k");
|
|
934
|
+
assert.ok(shortcut, "alt+k not registered");
|
|
935
|
+
const opened = shortcut.handler(ctx);
|
|
936
|
+
await overlayReady;
|
|
937
|
+
assert.match(ui.overlayView!.render(100).join("\n"), /Assign models and effort/);
|
|
938
|
+
ui.overlayView!.handleInput("\x1b");
|
|
939
|
+
await opened;
|
|
940
|
+
});
|
|
941
|
+
|
|
942
|
+
test("resolving the overlay through closeOverlay sends nothing and does not throw", async () => {
|
|
943
|
+
const { pi, commands, sentMessages } = fakePi();
|
|
944
|
+
gentleShell(pi, {});
|
|
945
|
+
const { ctx, ui, overlayReady } = fakeContext();
|
|
946
|
+
const opened = commands.get("gentle:commands")!.handler("", ctx);
|
|
947
|
+
await overlayReady;
|
|
948
|
+
ui.closeOverlay?.();
|
|
949
|
+
await opened;
|
|
950
|
+
assert.deepEqual(sentMessages, []);
|
|
951
|
+
});
|
|
@@ -85,6 +85,19 @@ test("native choice list presents one hovered wrapped option without changing se
|
|
|
85
85
|
assert.doesNotMatch(list.render(56).join("\n"), new RegExp(hoverBackground.replace(/[\[\]]/g, "\\$&")));
|
|
86
86
|
});
|
|
87
87
|
|
|
88
|
+
test("native choice list refreshes existing item content without changing selection", () => {
|
|
89
|
+
const items = [
|
|
90
|
+
{ id: "first", label: "First", description: "Initial detail." },
|
|
91
|
+
{ id: "second", label: "Second", description: "Second detail." },
|
|
92
|
+
];
|
|
93
|
+
const list = new NativeChoiceList(items, theme);
|
|
94
|
+
list.setSelectedIndex(1);
|
|
95
|
+
items[1]!.description = "Updated detail.";
|
|
96
|
+
list.refreshItems();
|
|
97
|
+
assert.equal(list.getSelectedItem()?.id, "second");
|
|
98
|
+
assert.match(stripTerminalSequences(list.render(40).join("\n")), /Updated detail\./);
|
|
99
|
+
});
|
|
100
|
+
|
|
88
101
|
test("native choice list ignores Kitty key releases", () => {
|
|
89
102
|
const list = new NativeChoiceList(
|
|
90
103
|
[{ id: "first", label: "First" }, { id: "second", label: "Second" }],
|
|
@@ -246,11 +246,40 @@ test("2.9.1 repeats 2.9.0 because the negotiated lane Pi consumes is unchanged",
|
|
|
246
246
|
assert.deepEqual(contract, NATIVE_CLI_CONTRACTS["2.9.0"] as Record<string, boolean>);
|
|
247
247
|
});
|
|
248
248
|
|
|
249
|
+
test("3.0.0 repeats 2.9.1 because the negotiated lane Pi consumes is unchanged", () => {
|
|
250
|
+
// v3.0.0 shipped ODD as the orchestrator's mandatory default protocol and
|
|
251
|
+
// integrated the simplified SDD workflow into it (gentle-ai #4642, #4644),
|
|
252
|
+
// with the provider contract byte-frozen at 1.2.0. Diffing
|
|
253
|
+
// contracts/review-integration/v2 and contracts/review-provider-contract
|
|
254
|
+
// between the v2.9.1 and v3.0.0 tags in the gentle-ai source tree showed
|
|
255
|
+
// zero byte changes, so this row repeats 2.9.1. riskEvidence and hint
|
|
256
|
+
// remain dark because neither is proven to reach Pi's negotiated START
|
|
257
|
+
// path.
|
|
258
|
+
const contract = NATIVE_CLI_CONTRACTS["3.0.0"] as Record<string, boolean>;
|
|
259
|
+
assert.equal(contract.riskEvidence, false);
|
|
260
|
+
assert.equal(contract.hint, false);
|
|
261
|
+
assert.deepEqual(contract, NATIVE_CLI_CONTRACTS["2.9.1"] as Record<string, boolean>);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
test("3.0.1 repeats 3.0.0 because the negotiated lane Pi consumes is unchanged", () => {
|
|
265
|
+
// v3.0.1 moved the Go module path to
|
|
266
|
+
// github.com/gentleman-programming/gentle-ai/v3 with no contract change
|
|
267
|
+
// (gentle-ai #4683). Diffing contracts/review-integration/v2 and
|
|
268
|
+
// contracts/review-provider-contract between the v3.0.0 and v3.0.1 tags
|
|
269
|
+
// in the gentle-ai source tree showed zero byte changes, so this row
|
|
270
|
+
// repeats 3.0.0. riskEvidence and hint remain dark because neither is
|
|
271
|
+
// proven to reach Pi's negotiated START path.
|
|
272
|
+
const contract = NATIVE_CLI_CONTRACTS["3.0.1"] as Record<string, boolean>;
|
|
273
|
+
assert.equal(contract.riskEvidence, false);
|
|
274
|
+
assert.equal(contract.hint, false);
|
|
275
|
+
assert.deepEqual(contract, NATIVE_CLI_CONTRACTS["3.0.0"] as Record<string, boolean>);
|
|
276
|
+
});
|
|
277
|
+
|
|
249
278
|
test("no shipped version key was added beyond the pin bump", () => {
|
|
250
279
|
// Rows are promises to consumers, so a new key only ever appears in a
|
|
251
280
|
// dedicated commit alongside a pin bump, never as a side effect. v2.2.4 and
|
|
252
281
|
// v2.3.0 shipped upstream while Pi stayed on 2.2.3 and were never pinned,
|
|
253
282
|
// so they get no row: a row asserts ground truth measured against a binary
|
|
254
283
|
// Pi actually ran, and the table only has to be ascending, not gapless.
|
|
255
|
-
assert.deepEqual(Object.keys(NATIVE_CLI_CONTRACTS), [...DARK_VERSIONS, "2.2.0", "2.2.1", "2.2.2", "2.2.3", "2.4.0", "2.5.0-rc.3", "2.5.0", "2.6.0", "2.7.0", "2.8.0", "2.8.1", "2.8.2", "2.9.0", "2.9.1"]);
|
|
284
|
+
assert.deepEqual(Object.keys(NATIVE_CLI_CONTRACTS), [...DARK_VERSIONS, "2.2.0", "2.2.1", "2.2.2", "2.2.3", "2.4.0", "2.5.0-rc.3", "2.5.0", "2.6.0", "2.7.0", "2.8.0", "2.8.1", "2.8.2", "2.9.0", "2.9.1", "3.0.0", "3.0.1"]);
|
|
256
285
|
});
|
|
@@ -1023,36 +1023,3 @@ test("all twelve native actions retain their exact tokens without prose routing"
|
|
|
1023
1023
|
assert.equal(decodeNativeSddStatusV2(status, { workspaceRoot: "/repo" }).nextRecommended, nextRecommended);
|
|
1024
1024
|
}
|
|
1025
1025
|
});
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
test("compact SDD bracket transports exact selection and remediation binding", async () => {
|
|
1029
|
-
const revision = `sha256:${"a".repeat(64)}`;
|
|
1030
|
-
const queued = queuedAdapter([{ stdout: JSON.stringify({ state: "proceed", token: revision }) }, { stdout: '{"state":"complete"}' }]);
|
|
1031
|
-
const cli = client(queued.adapter);
|
|
1032
|
-
const request = { workspaceRoot: "/repo", changeName: "fix", requestId: "acquire-one", workUnit: "correction", evidenceGoal: "Observed correction", maxAttempts: 1, maxChangedLines: 300, remediatesEvidenceRevision: revision };
|
|
1033
|
-
assert.deepEqual(await cli.sddAttemptAcquire(request), { state: "proceed", token: revision });
|
|
1034
|
-
assert.deepEqual(queued.calls[0].arguments, ["sdd-attempt", "acquire", "--cwd", "/repo", "--change", "fix", "--request-id", "acquire-one", "--work-unit", "correction", "--evidence-goal", "Observed correction", "--max-attempts", "1", "--max-changed-lines", "300", "--remediates-evidence-revision", revision]);
|
|
1035
|
-
assert.deepEqual(await cli.sddAttemptSettle({ workspaceRoot: "/repo", changeName: "fix", token: revision, requestId: "settle-one", outcome: "interrupted", diagnosis: "Child could not spawn", harnessDisposition: "invalidated", cleanupEvidence: "No process created", processEvidence: "Spawn rejected", remediatesEvidenceRevision: revision }), { state: "complete" });
|
|
1036
|
-
assert.equal(queued.calls[1].arguments.includes("--evidence-revision"), false);
|
|
1037
|
-
assert.equal(queued.calls[1].timeoutMs, undefined);
|
|
1038
|
-
});
|
|
1039
|
-
|
|
1040
|
-
test("compact SDD refuses malformed result and invalid terminal evidence before execution", async () => {
|
|
1041
|
-
const queued = queuedAdapter([{ stdout: '{"state":"proceed"}' }, { stdout: '{"state":"blocked","reason":"budget"}' }]);
|
|
1042
|
-
const cli = client(queued.adapter);
|
|
1043
|
-
const request = { workspaceRoot: "/repo", changeName: "fix", requestId: "one", workUnit: "correction", evidenceGoal: "Observed correction" };
|
|
1044
|
-
await assert.rejects(cli.sddAttemptAcquire(request), /schema incompatible/);
|
|
1045
|
-
assert.deepEqual(await cli.sddAttemptAcquire(request), { state: "blocked", reason: "budget" });
|
|
1046
|
-
await assert.rejects(cli.sddAttemptSettle({ ...request, token: `sha256:${"a".repeat(64)}`, outcome: "failed", diagnosis: "Test failed", harnessDisposition: "reused", cleanupEvidence: "Process exited", processEvidence: "Exit one" }), /evidence/i);
|
|
1047
|
-
assert.equal(queued.calls.length, 2);
|
|
1048
|
-
});
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
test("compact admission accepts native empty CAS and never redirects uncertainty into review", async () => {
|
|
1052
|
-
const queued = queuedAdapter([{ stdout: '{"state":"blocked","reason":"budget"}' }, { stdout: "", timedOut: true }]);
|
|
1053
|
-
const cli = client(queued.adapter);
|
|
1054
|
-
const request = { workspaceRoot: "/repo", changeName: "fix", requestId: "one", workUnit: "fix", evidenceGoal: "Observed correction", expectedRevision: "" };
|
|
1055
|
-
await cli.sddAttemptAcquire(request);
|
|
1056
|
-
assert.equal(queued.calls[0].arguments[queued.calls[0].arguments.indexOf("--expected-revision") + 1], "");
|
|
1057
|
-
await assert.rejects(cli.sddAttemptAcquire({ ...request, expectedRevision: undefined }), error => error instanceof NativeReviewCliError && error.mutationOutcome === "unknown" && error.nextAction === undefined);
|
|
1058
|
-
});
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import test from "node:test";
|
|
5
|
+
import { __testing } from "../extensions/gentle-ai.ts";
|
|
6
|
+
|
|
7
|
+
// These are instruction-delivery contracts, not proof of autonomous model adherence.
|
|
8
|
+
const read = (path: string) => readFileSync(join(import.meta.dirname, "..", path), "utf8");
|
|
9
|
+
const core = read("assets/orchestrator.md");
|
|
10
|
+
const delegation = read("assets/orchestrator-delegation.md");
|
|
11
|
+
const memory = read("assets/orchestrator-memory.md");
|
|
12
|
+
const wrapper = read("extensions/gentle-ai.ts");
|
|
13
|
+
|
|
14
|
+
function containsAll(text: string, clauses: readonly string[]): void {
|
|
15
|
+
for (const clause of clauses) assert.ok(text.includes(clause), `missing contract: ${clause}`);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
test("organic entry stays read-only without authorization and loads detail before work", () => {
|
|
19
|
+
containsAll(core, [
|
|
20
|
+
"Substantial authorized work: use ODD",
|
|
21
|
+
"ODD (Default Workflow, harness section above) is mandatory on every request",
|
|
22
|
+
"orchestrator-delegation.md",
|
|
23
|
+
"orchestrator-memory.md",
|
|
24
|
+
]);
|
|
25
|
+
containsAll(delegation, [
|
|
26
|
+
"Investigation, explanation, review, comparison, and proposal-only requests remain read-only",
|
|
27
|
+
"without a task or storage permission prompt",
|
|
28
|
+
"Small, understood work creates no durable task artifacts",
|
|
29
|
+
]);
|
|
30
|
+
assert.doesNotMatch(core + wrapper, /Prefer SDD\/OpenSpec artifacts|Substantial feature: suggest SDD organically/);
|
|
31
|
+
assert.doesNotMatch(core + delegation, /Suggest it when proposal\/spec\/design\/tasks|propose SDD only when durable proposal\/spec\/design\/tasks/);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("research uses adaptive evidence gathering and existing general workers only", () => {
|
|
35
|
+
containsAll(delegation, [
|
|
36
|
+
"problem, intended outcome, constraints, and current evidence",
|
|
37
|
+
"no fixed questionnaire or mandatory rounds",
|
|
38
|
+
"one focused user question",
|
|
39
|
+
"stop and wait",
|
|
40
|
+
"workers return gaps to the parent",
|
|
41
|
+
"available authorized documentation/web tools",
|
|
42
|
+
"prefer primary sources",
|
|
43
|
+
"URLs or code locations",
|
|
44
|
+
"verified facts, assumptions, contradictions, freshness, and gaps",
|
|
45
|
+
"recommendation, tradeoffs, open questions, and implementation implications",
|
|
46
|
+
"Forward these research instructions",
|
|
47
|
+
"existing fresh general exploration/research worker",
|
|
48
|
+
"do not create a specialized agent or invoke `sdd-research`",
|
|
49
|
+
"no new persistence or readiness machinery",
|
|
50
|
+
]);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("task sizing is explicitly advisory and forwarded without cosmetic savings", () => {
|
|
54
|
+
containsAll(delegation, [
|
|
55
|
+
"about 400 authored changed lines",
|
|
56
|
+
"additions plus deletions",
|
|
57
|
+
"not a task acceptance criterion, hard cap, counter-trigger, automatic stop, forced split, or RDD trigger",
|
|
58
|
+
"Forward this same advisory-only instruction",
|
|
59
|
+
"Never delete spaces, blank lines, or comments",
|
|
60
|
+
"never omit tests, minify, add gratuitous abstractions, or split artificially",
|
|
61
|
+
"Existing PR size gates remain unchanged",
|
|
62
|
+
]);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("organic progress preserves both complete feature copies and reconciles actual evidence", () => {
|
|
66
|
+
containsAll(memory, [
|
|
67
|
+
"odd/tasks/<feature-name>.md",
|
|
68
|
+
"odd/<feature-name>/tasks",
|
|
69
|
+
"stable task IDs",
|
|
70
|
+
"full current document",
|
|
71
|
+
"repository-relative file locator",
|
|
72
|
+
"preserve valid completed and unrelated work",
|
|
73
|
+
"reopen invalidated items",
|
|
74
|
+
"Check off only observed outcomes",
|
|
75
|
+
"Read back both writes",
|
|
76
|
+
"not atomic",
|
|
77
|
+
"mirror pending",
|
|
78
|
+
"Preserve both versions",
|
|
79
|
+
"mem_context",
|
|
80
|
+
"mem_search",
|
|
81
|
+
"mem_get_observation",
|
|
82
|
+
"read the actual task file",
|
|
83
|
+
"not a third authority",
|
|
84
|
+
]);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("assumption challenge and task checks do not activate or duplicate native review", () => {
|
|
88
|
+
containsAll(delegation, [
|
|
89
|
+
"at most one scoped independent read-only assumption challenge",
|
|
90
|
+
"high-consequence unproven premise",
|
|
91
|
+
"Deterministic failures need fixes",
|
|
92
|
+
"native RDD refuter",
|
|
93
|
+
"functional checks per task, not an RDD cycle per TODO",
|
|
94
|
+
"deliverable candidate boundary",
|
|
95
|
+
"native candidate risk assessment",
|
|
96
|
+
"gentle_review` with `{\"operation\":\"assess\"}",
|
|
97
|
+
"Passive/low",
|
|
98
|
+
"no reviewer or consent ceremony",
|
|
99
|
+
"only on grant",
|
|
100
|
+
"decline continues under ordinary policy",
|
|
101
|
+
"never infer low risk from a failed assessment",
|
|
102
|
+
"When RDD is disabled, do not start or prompt for RDD",
|
|
103
|
+
]);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("user documentation shows recovery and candidate-level consent without claiming model proof", () => {
|
|
107
|
+
const docs = read("docs/readme-reference.md");
|
|
108
|
+
containsAll(docs, [
|
|
109
|
+
"## Organic Driven Development",
|
|
110
|
+
"```mermaid",
|
|
111
|
+
"Full feature memory and actual task file",
|
|
112
|
+
"Native candidate risk",
|
|
113
|
+
"advisory",
|
|
114
|
+
"Static prompt tests",
|
|
115
|
+
"autonomous",
|
|
116
|
+
]);
|
|
117
|
+
assert.ok(read("README.md").includes("#organic-driven-development"));
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
test("one feature document carries intent, accepted rationale and worker context", () => {
|
|
122
|
+
containsAll(memory, [
|
|
123
|
+
"one feature document, not a separate plan file or topic",
|
|
124
|
+
"objective, problem, why, scope, constraints",
|
|
125
|
+
"progress, verification evidence, and next step",
|
|
126
|
+
"concise rationale for meaningful accepted changes",
|
|
127
|
+
"Routine corrections stay with their tasks; no exhaustive decision journal",
|
|
128
|
+
"Accepted user, review, or verification changes",
|
|
129
|
+
"automatically update affected intent and TODOs",
|
|
130
|
+
"add genuinely new tasks or reopen invalidated items with a reason",
|
|
131
|
+
"Findings alone never authorize scope expansion or automatic acceptance",
|
|
132
|
+
"Before implementation or resume, the parent reads both the actual file and full observation",
|
|
133
|
+
"passes the locator and relevant context; workers read the document before edits",
|
|
134
|
+
]);
|
|
135
|
+
containsAll(read("assets/agents/gentle-ai-worker.md"), [
|
|
136
|
+
"Read the parent's ODD feature document locator before edits",
|
|
137
|
+
"Preserve valid completed work; return proposed intent/task changes and their reasons",
|
|
138
|
+
]);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("ODD forwards configured TDD without equating test presence with enablement", () => {
|
|
142
|
+
containsAll(delegation, [
|
|
143
|
+
"Resolve effective TDD on/off from existing project/session configuration or explicit user choice",
|
|
144
|
+
"retain its source and exact test runner",
|
|
145
|
+
"Record resolved mode, source, and runner in the feature document when present",
|
|
146
|
+
"Tests or frameworks being present does not enable TDD",
|
|
147
|
+
"Forward mode, source, and runner on every implementation delegation; refresh on resume",
|
|
148
|
+
"When enabled, require observed RED before implementation, GREEN, then REFACTOR",
|
|
149
|
+
"When disabled, run ordinary functional checks, not no checks",
|
|
150
|
+
"If mode is unknown/conflicting or the runner is missing",
|
|
151
|
+
"resolve only the ambiguity affecting the next action",
|
|
152
|
+
"never invent precedence or a command, and never invoke sdd-init to determine ODD TDD",
|
|
153
|
+
]);
|
|
154
|
+
containsAll(read("assets/agents/gentle-ai-worker.md"), [
|
|
155
|
+
"Consume the parent's effective TDD mode, configuration/choice source, and exact runner",
|
|
156
|
+
"Missing or conflicting mode/source/runner is not disabled TDD",
|
|
157
|
+
]);
|
|
158
|
+
assert.doesNotMatch(wrapper, /If tests exist, use strict TDD/);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test("ODD protocol is always-on in the rendered system prompt and runs by default", () => {
|
|
162
|
+
const orderedClauses = [
|
|
163
|
+
"Default workflow: Organic Driven Development (MANDATORY)",
|
|
164
|
+
"predefined workflow of this orchestrator",
|
|
165
|
+
"SDD is a branch inside ODD",
|
|
166
|
+
"Never describe this workflow only when asked about it: run it.",
|
|
167
|
+
"1. **Authorize.**",
|
|
168
|
+
"2. **Explore.**",
|
|
169
|
+
"3. **Resolve uncertainty.**",
|
|
170
|
+
"4. **Classify.**",
|
|
171
|
+
"two or more meaningful implementation steps",
|
|
172
|
+
"5. **Track before the first write.**",
|
|
173
|
+
"Tell the user in one line which feature document was created and how many tasks it holds",
|
|
174
|
+
"6. **Implement task by task.**",
|
|
175
|
+
"7. **Close.**",
|
|
176
|
+
"Harness principles:",
|
|
177
|
+
"# el Gentleman Orchestrator",
|
|
178
|
+
];
|
|
179
|
+
for (const persona of ["gentleman", "neutral"] as const) {
|
|
180
|
+
const prompt = __testing.buildGentlePrompt(persona);
|
|
181
|
+
let cursor = -1;
|
|
182
|
+
for (const clause of orderedClauses) {
|
|
183
|
+
const index = prompt.indexOf(clause);
|
|
184
|
+
assert.ok(index !== -1, `[${persona}] missing contract: ${clause}`);
|
|
185
|
+
assert.ok(
|
|
186
|
+
index > cursor,
|
|
187
|
+
`[${persona}] clause out of order (must appear after the previous one): ${clause}`,
|
|
188
|
+
);
|
|
189
|
+
cursor = index;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
assert.ok(
|
|
194
|
+
wrapper.includes("Organic Driven Development (ODD) is the predefined workflow for every request"),
|
|
195
|
+
"missing contract: extensions/gentle-ai.ts harness principle",
|
|
196
|
+
);
|
|
197
|
+
assert.ok(
|
|
198
|
+
wrapper.includes(
|
|
199
|
+
"I run Organic Driven Development by default and SDD/OpenSpec when explicitly selected",
|
|
200
|
+
),
|
|
201
|
+
"missing contract: extensions/gentle-ai.ts identity sentence",
|
|
202
|
+
);
|
|
203
|
+
assert.ok(
|
|
204
|
+
core.includes("ODD (Default Workflow, harness section above) is mandatory on every request"),
|
|
205
|
+
"missing contract: assets/orchestrator.md pointer sentence",
|
|
206
|
+
);
|
|
207
|
+
containsAll(core, ["orchestrator-delegation.md", "orchestrator-memory.md"]);
|
|
208
|
+
});
|
|
@@ -281,8 +281,8 @@ function isNormativeLine(line: string): boolean {
|
|
|
281
281
|
}
|
|
282
282
|
|
|
283
283
|
const fixtureLines = readFileSync(FIXTURE_PATH, "utf8").split("\n");
|
|
284
|
-
// Fixture
|
|
285
|
-
// canonical-authority resolution. Keep
|
|
284
|
+
// Fixture line 36 is superseded by ODD (#1035); 187 and 191 predate
|
|
285
|
+
// root-relative lazy assets and canonical-authority resolution. Keep coverage by asserting the
|
|
286
286
|
// intentionally updated production wording instead of weakening the range.
|
|
287
287
|
const CURRENT_SDD_WORKFLOW_PATH = "`sdd-orchestrator-workflow.md`";
|
|
288
288
|
const CURRENT_HARD_PREFLIGHT_INVARIANT = "Hard preflight invariant: `openspec/config.yaml`, existing SDD changes, installed `.pi`/global SDD assets, or a todo named \"preflight\" are not session preflight. Do not mark SDD preflight complete, start `sdd-init`, launch SDD subagents/chains, or move to explore/proposal/spec/design/tasks until this session has an injected `## SDD Session Preflight` block or a canonical-authority resolution. Defaults and capability constraints may resolve fields without confirmation prompts; preserve unresolved-choice and safety gates.";
|
|
@@ -320,11 +320,26 @@ for (const range of DISPOSITION_MAP) {
|
|
|
320
320
|
if (raw === undefined || !isNormativeLine(raw)) continue;
|
|
321
321
|
const trimmed = raw.trim();
|
|
322
322
|
const expected =
|
|
323
|
+
ln === 36 ? "- Substantial authorized work: use ODD; track feature progress automatically." :
|
|
324
|
+
// #1051 keeps the selected store when memory is unavailable.
|
|
325
|
+
ln === 221 ? "do not switch the selected store" :
|
|
326
|
+
// Research returns findings; other phases keep direct backend ownership.
|
|
327
|
+
ln === 205 ? trimmed.replace("Each SDD phase", "Except for output-only `sdd-research`, each SDD phase") :
|
|
328
|
+
ln === 185 ? trimmed.replace("apply/verify/sync/archive", "apply/verify/archive") :
|
|
323
329
|
ln === 187
|
|
324
330
|
? CURRENT_SDD_WORKFLOW_PATH
|
|
325
331
|
: ln === 191
|
|
326
332
|
? CURRENT_HARD_PREFLIGHT_INVARIANT
|
|
327
333
|
: trimmed;
|
|
334
|
+
// #1051 retires the standalone sync row and its artifact key, not
|
|
335
|
+
// the surrounding memory/recovery contract or historical fixture.
|
|
336
|
+
if (ln === 216 || ln === 220) {
|
|
337
|
+
assert.ok(!targetContent.includes(trimmed), `retired sync contract remains at fixture:${ln}`);
|
|
338
|
+
assert.match(targetContent, /sdd\/<change>\/archive-report/);
|
|
339
|
+
assert.match(targetContent, /sdd\/<change>\/verify-report/);
|
|
340
|
+
assert.doesNotMatch(targetContent, /sdd\/<change>\/sync-report|\| `sdd-sync`/);
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
328
343
|
if (SUPERSEDED_LIFECYCLE_REVIEW_LINES.has(ln)) {
|
|
329
344
|
assert.ok(
|
|
330
345
|
!targetContent.includes(trimmed),
|