gentle-pi 3.7.0 → 4.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 +37 -6
- package/assets/agents/gentle-ai-explore.md +4 -4
- package/assets/agents/gentle-ai-verify.md +6 -4
- package/assets/agents/gentle-ai-worker.md +7 -9
- package/assets/orchestrator-delegation.md +40 -41
- package/assets/orchestrator-memory.md +1 -22
- package/assets/orchestrator-skills.md +1 -1
- package/assets/orchestrator.md +12 -24
- package/assets/support/strict-tdd-verify.md +4 -266
- package/assets/support/strict-tdd.md +8 -360
- package/bin/gentle-shell.mjs +254 -29
- package/docs/delegated-verification.md +26 -1
- package/docs/gentle-agents-activity.md +24 -0
- package/docs/gentle-shell.md +92 -28
- package/docs/native-authority-architecture.md +2 -2
- package/docs/prompt-history.md +280 -0
- package/docs/readme-reference.md +112 -206
- package/docs/telemetry.md +1 -1
- package/docs/yolo-mode.md +86 -0
- package/extensions/child-context.ts +26 -0
- package/extensions/child-safety.ts +23 -0
- package/extensions/gentle-agents.ts +429 -262
- package/extensions/gentle-ai.ts +781 -683
- package/extensions/gentle-shell.ts +1375 -88
- package/extensions/gentle-stats.ts +101 -0
- package/extensions/gentle-todo.ts +18 -7
- package/extensions/history/atomic-write.ts +38 -0
- package/extensions/history/hide-prompts.ts +183 -0
- package/extensions/history/index.ts +1419 -0
- package/extensions/history/load-shared-history.ts +39 -0
- package/extensions/history/selector-helpers.ts +538 -0
- package/extensions/history/session-scan.ts +233 -0
- package/extensions/history/store.ts +1119 -0
- package/extensions/nan-provider.ts +6 -0
- package/extensions/quiet-tools.ts +179 -87
- package/extensions/resume-hint.ts +60 -0
- package/extensions/skill-registry.ts +16 -12
- package/extensions/startup-banner.ts +60 -31
- package/lib/agent-assets.ts +604 -0
- package/lib/agent-profile-pin.ts +12 -0
- package/lib/agents-message-delivery.ts +181 -0
- package/lib/agents-protocol.ts +60 -0
- package/lib/agents-runner.ts +96 -98
- package/lib/agents-view.ts +26 -3
- package/lib/agents-widget.ts +16 -9
- package/lib/append-system-prompt.ts +21 -0
- package/lib/bounded-writer-admission.ts +147 -0
- package/lib/card-style-policy.ts +60 -0
- package/lib/child-context-files.ts +166 -0
- package/lib/codemode-renderer.ts +185 -0
- package/lib/command-palette-catalog.ts +3 -9
- package/lib/command-palette.ts +25 -14
- package/lib/destructive-command-guard.ts +144 -0
- package/lib/gentle-ai-elapsed-store.ts +87 -0
- package/lib/gentle-ai-renderer.ts +196 -39
- package/lib/gentle-shell-launcher.ts +24 -13
- package/lib/gentle-shell-resume-hint.ts +176 -0
- package/lib/history-capture-policy.ts +95 -0
- package/lib/model-routing-authority.ts +5 -1
- package/lib/nan-provider.ts +227 -0
- package/lib/native-review-cli.ts +49 -108
- package/lib/odd-phase-inference.ts +231 -0
- package/lib/odd-phase.ts +141 -0
- package/lib/overlay-repaint.ts +26 -0
- package/lib/pi-tui-keys.ts +53 -0
- package/lib/review-candidate-view-owner.ts +67 -17
- package/lib/review-candidate-view.ts +112 -25
- package/lib/review-reminder-receipt.ts +48 -8
- package/lib/review-risk-assessment.ts +156 -11
- package/lib/review-sidebar-state.ts +223 -0
- package/lib/selection-engine.ts +515 -0
- package/lib/session-messaging-grants.ts +135 -0
- package/lib/session-worktree-registry.ts +14 -2
- package/lib/shell-bar.ts +179 -24
- package/lib/shell-card.ts +287 -18
- package/lib/shell-changes-view.ts +2 -1
- package/lib/shell-prompt.ts +102 -6
- package/lib/shell-sidebar-layout.ts +78 -14
- package/lib/shell-sidebar.ts +15 -1
- package/lib/shell-todo.ts +23 -13
- package/lib/shell-usage-view.ts +9 -4
- package/lib/shell-usage.ts +66 -10
- package/lib/stats-collector.ts +381 -0
- package/lib/stats-view.ts +431 -0
- package/lib/theme-customization.ts +52 -0
- package/lib/vim-editor-adapter.ts +379 -0
- package/lib/vim-normal-engine.ts +154 -0
- package/lib/vim-operator-engine.ts +416 -0
- package/lib/vim-policy.ts +49 -0
- package/lib/vim-visual-engine.ts +107 -0
- package/lib/visual-customization-policy.ts +108 -0
- package/lib/visual-customize-view.ts +330 -0
- package/lib/visual-profiles.ts +228 -0
- package/lib/yolo-session-policy.ts +240 -0
- package/package.json +20 -8
- package/runtime/gentle-shell-launcher.mjs +23 -12
- package/runtime/gentle-shell-resume-hint.mjs +177 -0
- package/runtime/native-review-cli.mjs +49 -108
- package/runtime/review-risk-assessment.mjs +154 -9
- package/scripts/build-runtime-modules.mjs +1 -0
- package/scripts/gentle-ai-installer.mjs +14 -13
- package/scripts/mirror-odd-routing.mjs +2 -2
- package/scripts/run-test-suite.mjs +76 -0
- package/scripts/test-packed-runner.mjs +31 -14
- package/scripts/verify-package-files.mjs +11 -22
- package/skills/branch-pr/SKILL.md +24 -52
- package/skills/chained-pr/SKILL.md +31 -15
- package/skills/chained-pr/references/chaining-details.md +31 -20
- package/skills/gentle-ai/SKILL.md +9 -15
- package/skills/issue-creation/SKILL.md +8 -2
- package/skills/issue-creation/references/delegated-workflow-actions.md +19 -0
- package/skills/work-unit-commits/SKILL.md +4 -3
- package/tests/agent-profiles.test.ts +18 -0
- package/tests/agents-fake-child.ts +2 -2
- package/tests/agents-message-delivery.test.ts +106 -0
- package/tests/agents-protocol.test.ts +40 -0
- package/tests/agents-runner.test.ts +378 -89
- package/tests/agents-view-thread-identity.test.ts +169 -0
- package/tests/agents-view.test.ts +8 -2
- package/tests/agents-widget.test.ts +154 -15
- package/tests/append-system-prompt-route.test.ts +160 -0
- package/tests/append-system-prompt.test.ts +46 -0
- package/tests/artifact-language.test.ts +19 -213
- package/tests/ask-user-question.test.ts +44 -1
- package/tests/asset-installation-runtime.test.ts +5 -16
- package/tests/autonomous-guard.test.ts +69 -1
- package/tests/bounded-writer-admission.test.ts +95 -0
- package/tests/branch-pr-skill.test.ts +43 -0
- package/tests/card-style-policy.test.ts +55 -0
- package/tests/chained-pr-skill.test.ts +124 -0
- package/tests/child-context-files.test.ts +255 -0
- package/tests/child-safety.test.ts +82 -0
- package/tests/codemode-rendering.test.ts +491 -0
- package/tests/command-palette.test.ts +39 -3
- package/tests/delegated-key-learnings-contract.test.ts +0 -76
- package/tests/destructive-command-guard.test.ts +84 -0
- package/tests/devbinary/native-review-parity.devtest.ts +170 -2
- package/tests/devbinary/non-git-subagent-bootstrap.devtest.ts +193 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-28T10-00-00-000Z_aaa.jsonl +7 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-29T23-00-00-000Z_bbb.jsonl +3 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-30T08-00-00-000Z_ddd.jsonl +2 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-30T09-00-00-000Z_eee.jsonl +3 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/run-1/session.jsonl +2 -0
- package/tests/fixtures/stats/sessions/--work-beta--/2026-09-01T12-00-00-000Z_ccc.jsonl +2 -0
- package/tests/fixtures/stats/user-pi/sessions/--work-alpha--/2026-09-28T10-00-00-000Z_aaa.jsonl +2 -0
- package/tests/fixtures/stats/user-pi/sessions/--work-alpha--/2026-09-29T23-00-00-000Z_bbb.jsonl +4 -0
- package/tests/fixtures/stats/user-pi/sessions/--work-gamma--/2026-09-20T09-00-00-000Z_fff.jsonl +3 -0
- package/tests/generic-agent-tools.test.ts +54 -0
- package/tests/gentle-agents.test.ts +1047 -252
- package/tests/gentle-ai-binary.test.ts +3 -3
- package/tests/gentle-ai-elapsed-store.test.ts +68 -0
- package/tests/gentle-ai-installer.test.ts +68 -54
- package/tests/gentle-ai-renderer.test.ts +487 -8
- package/tests/gentle-ai.test.ts +288 -65
- package/tests/gentle-card-text.ts +2 -1
- package/tests/gentle-shell-bin.test.ts +651 -114
- package/tests/gentle-shell-launcher.test.ts +99 -52
- package/tests/gentle-shell-resume-hint.test.ts +270 -0
- package/tests/gentle-shell.test.ts +3839 -187
- package/tests/gentle-stats.test.ts +152 -0
- package/tests/gentle-theme.test.ts +4 -1
- package/tests/gentle-todo.test.ts +80 -8
- package/tests/history-atomic-write.test.ts +57 -0
- package/tests/history-capture-policy.test.ts +102 -0
- package/tests/history-command-registration.test.ts +164 -0
- package/tests/history-dedupe-entries.test.ts +123 -0
- package/tests/history-delete-backfill.test.ts +190 -0
- package/tests/history-delete-confirm.test.ts +460 -0
- package/tests/history-dispatch.test.ts +180 -0
- package/tests/history-drain-hidden.test.ts +110 -0
- package/tests/history-drain-order.test.ts +98 -0
- package/tests/history-expanded-globals.test.ts +62 -0
- package/tests/history-gc.test.ts +832 -0
- package/tests/history-header-layout.test.ts +265 -0
- package/tests/history-hide-prompts.test.ts +275 -0
- package/tests/history-lazy-windowing.test.ts +508 -0
- package/tests/history-legacy-migrate-v2.test.ts +297 -0
- package/tests/history-load-shared-history.test.ts +53 -0
- package/tests/history-max-results-cap.test.ts +76 -0
- package/tests/history-multi-reader.test.ts +203 -0
- package/tests/history-off-path.test.ts +170 -0
- package/tests/history-openflow-integration.test.ts +173 -0
- package/tests/history-overlay-margin.test.ts +326 -0
- package/tests/history-preview-layout.test.ts +93 -0
- package/tests/history-registry.test.ts +143 -0
- package/tests/history-scope-delete.test.ts +411 -0
- package/tests/history-search-caret-keys.test.ts +142 -0
- package/tests/history-seed-bootstrap.test.ts +170 -0
- package/tests/history-seed-regen.test.ts +129 -0
- package/tests/history-selector-windowing.test.ts +94 -0
- package/tests/history-session-scan-directory.test.ts +87 -0
- package/tests/history-session-scan-extract.test.ts +583 -0
- package/tests/history-session-writer.test.ts +351 -0
- package/tests/history-store-paths.test.ts +79 -0
- package/tests/history-tombstone-exact.test.ts +139 -0
- package/tests/history-wheel-mouse.test.ts +242 -0
- package/tests/inprocess-reviewer.test.ts +29 -19
- package/tests/issue-creation-skill.test.ts +61 -0
- package/tests/model-routing-authority.test.ts +16 -0
- package/tests/nan-provider.test.ts +471 -0
- package/tests/native-review-capability-contract.test.ts +7 -1
- package/tests/native-review-cli.test.ts +6 -120
- package/tests/native-review-parity-runtime.test.ts +100 -3
- package/tests/odd-integration.test.ts +67 -0
- package/tests/odd-phase-inference.test.ts +213 -0
- package/tests/odd-phase-loader.test.ts +253 -0
- package/tests/odd-phase.test.ts +307 -0
- package/tests/odd-routing-canonical-ratchet.test.ts +11 -6
- package/tests/odd-routing-contract.test.ts +86 -35
- package/tests/orchestrator-budget.test.ts +14 -39
- package/tests/orchestrator-rdd-ownership.test.ts +3 -3
- package/tests/overlay-repaint.test.ts +74 -0
- package/tests/package-manifest.test.ts +252 -115
- package/tests/packed-runner-owned-path.test.ts +46 -0
- package/tests/persona-single-channel.test.ts +6 -6
- package/tests/provider-defect-handoff.test.ts +3 -11
- package/tests/quiet-bash-runtime.test.ts +76 -0
- package/tests/quiet-tool-rendering.test.ts +409 -184
- package/tests/rdd-aware-verification-contract.test.ts +76 -1
- package/tests/rdd-status-line.test.ts +9 -4
- package/tests/resume-hint-extension.test.ts +122 -0
- package/tests/review-agent-end-preflight.test.ts +176 -12
- package/tests/review-candidate-owner-retry.test.ts +22 -1
- package/tests/review-candidate-view.test.ts +298 -0
- package/tests/review-contract-prompt.test.ts +108 -43
- package/tests/review-controller-lock-status.test.ts +0 -1
- package/tests/review-controller-native-routing.test.ts +611 -5
- package/tests/review-controller-workspace-root.test.ts +163 -4
- package/tests/review-host-relay-routing.test.ts +338 -2
- package/tests/review-integration-v2-forward.test.ts +200 -0
- package/tests/review-ledger-contract.test.ts +10 -34
- package/tests/review-reminder-receipt.test.ts +47 -1
- package/tests/review-risk-assessment.test.ts +498 -6
- package/tests/review-sidebar-state.test.ts +402 -0
- package/tests/run-test-suite.test.ts +124 -0
- package/tests/runtime-harness.mjs +145 -786
- package/tests/runtime-metrics-children.test.ts +16 -23
- package/tests/selection-engine.test.ts +421 -0
- package/tests/session-messaging-grants.test.ts +255 -0
- package/tests/session-worktree-registry.test.ts +77 -0
- package/tests/shell-bar.test.ts +382 -1
- package/tests/shell-card.test.ts +353 -1
- package/tests/shell-changes-view.test.ts +52 -0
- package/tests/shell-prompt.test.ts +94 -2
- package/tests/shell-sidebar-layout.test.ts +325 -21
- package/tests/shell-sidebar-scroll-benchmark.test.ts +255 -0
- package/tests/shell-todo.test.ts +87 -1
- package/tests/shell-usage-view.test.ts +27 -0
- package/tests/shell-usage.test.ts +73 -0
- package/tests/skill-registry.test.ts +50 -1
- package/tests/startup-banner.test.ts +130 -2
- package/tests/stats-collector.test.ts +195 -0
- package/tests/stats-view.test.ts +202 -0
- package/tests/telemetry-trigger.test.ts +81 -20
- package/tests/theme-customization.test.ts +72 -0
- package/tests/vim-editor-adapter-host-resolution.test.ts +37 -0
- package/tests/vim-editor-adapter.test.ts +804 -0
- package/tests/vim-normal-engine.test.ts +101 -0
- package/tests/vim-operator-engine.test.ts +215 -0
- package/tests/vim-policy.test.ts +19 -0
- package/tests/vim-visual-engine.test.ts +52 -0
- package/tests/visual-customization-policy.test.ts +110 -0
- package/tests/visual-customize-view.test.ts +418 -0
- package/tests/visual-profiles.test.ts +87 -0
- package/tests/yolo-customize.test.ts +256 -0
- package/tests/yolo-mode-runtime.test.ts +161 -0
- package/tests/yolo-mode.test.ts +261 -0
- package/tests/yolo-session-policy.test.ts +59 -0
- package/themes/Gentle.json +2 -1
- package/themes/Gentleman-Cute.json +2 -1
- package/themes/Gentleman-Sexy.json +2 -1
- package/assets/agents/sdd-apply.md +0 -159
- package/assets/agents/sdd-archive.md +0 -228
- package/assets/agents/sdd-design.md +0 -49
- package/assets/agents/sdd-explore.md +0 -48
- package/assets/agents/sdd-init.md +0 -56
- package/assets/agents/sdd-onboard.md +0 -52
- package/assets/agents/sdd-proposal.md +0 -64
- package/assets/agents/sdd-remediate.md +0 -37
- package/assets/agents/sdd-research.md +0 -49
- package/assets/agents/sdd-spec.md +0 -192
- package/assets/agents/sdd-status.md +0 -54
- package/assets/agents/sdd-tasks.md +0 -108
- package/assets/agents/sdd-verify.md +0 -124
- package/assets/chains/sdd-full.chain.md +0 -83
- package/assets/chains/sdd-plan.chain.md +0 -56
- package/assets/chains/sdd-verify.chain.md +0 -43
- package/assets/sdd-orchestrator-workflow.md +0 -319
- package/assets/support/sdd-status-contract.md +0 -77
- package/docs/assets/diagrams/sdd-cycle.svg +0 -14
- package/extensions/sdd-init.ts +0 -816
- package/lib/openspec-deltas.ts +0 -156
- package/lib/sdd-preflight.ts +0 -1066
- package/lib/sdd-research-capabilities.ts +0 -94
- package/lib/sdd-status.ts +0 -26
- package/tests/fixtures/legacy/sdd-research-v2.5.0.md +0 -54
- package/tests/fixtures/native-review-cli/v2.1.3/bind-sdd.json +0 -25
- package/tests/fixtures/v0.10.7/assets/agents/sdd-apply.md +0 -132
- package/tests/openspec-deltas.test.ts +0 -209
- package/tests/sdd-agent-tools.test.ts +0 -156
- package/tests/sdd-archive-replay.test.ts +0 -82
- package/tests/sdd-classical-continuation.test.ts +0 -74
- package/tests/sdd-execution-routing-contract.test.ts +0 -44
- package/tests/sdd-managed-runtime-settlement.test.ts +0 -155
- package/tests/sdd-native-managed-uptake.test.ts +0 -243
- package/tests/sdd-no-attempts-contract.test.ts +0 -15
- package/tests/sdd-odd-integration.test.ts +0 -33
- package/tests/sdd-optional-research.test.ts +0 -124
- package/tests/sdd-planning-routing-contract.test.ts +0 -45
- package/tests/sdd-preflight-rpc-input.test.ts +0 -125
- package/tests/sdd-preflight.test.ts +0 -541
- package/tests/sdd-research-capabilities.test.ts +0 -114
- package/tests/sdd-research-live.test.ts +0 -241
- package/tests/sdd-selection-transport.test.ts +0 -653
- package/tests/sdd-status.test.ts +0 -9
- package/tests/sdd-task-truth.test.ts +0 -43
|
@@ -17,9 +17,9 @@ const ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
|
17
17
|
const { createGentleAiExtension } = await import(pathToFileURL(join(ROOT, "extensions/gentle-ai.ts")).href);
|
|
18
18
|
const EXTENSIONS = [
|
|
19
19
|
"extensions/gentle-ai.ts",
|
|
20
|
+
"extensions/nan-provider.ts",
|
|
20
21
|
"extensions/quiet-tools.ts",
|
|
21
22
|
"extensions/skill-registry.ts",
|
|
22
|
-
"extensions/sdd-init.ts",
|
|
23
23
|
"extensions/startup-banner.ts",
|
|
24
24
|
];
|
|
25
25
|
|
|
@@ -33,30 +33,16 @@ const EXPECTED_BANNER_COMMANDS = [
|
|
|
33
33
|
const EXPECTED_COMMANDS = [
|
|
34
34
|
"gentle:install-delegation",
|
|
35
35
|
"gentle:install-review",
|
|
36
|
-
"gentle:install-sdd",
|
|
37
|
-
"gentle:sdd-preflight",
|
|
38
|
-
"gentle-sdd-status",
|
|
39
|
-
"gentle-sdd-continue",
|
|
40
36
|
"gentle:models",
|
|
41
37
|
"gentle:persona",
|
|
42
38
|
"gentle:status",
|
|
43
39
|
"gentle:doctor",
|
|
44
|
-
"gentle-sdd-init",
|
|
45
40
|
"skill-registry:refresh",
|
|
46
41
|
...EXPECTED_BANNER_COMMANDS,
|
|
47
42
|
];
|
|
48
43
|
|
|
49
44
|
const FORBIDDEN_COMPAT_COMMANDS = [
|
|
50
45
|
"gentle:install-assets",
|
|
51
|
-
// The SDD entry points carry the gentle- prefix so they read identically in
|
|
52
|
-
// Claude Code and Pi. The bare names are retired without an alias.
|
|
53
|
-
"sdd-init",
|
|
54
|
-
"sdd-continue",
|
|
55
|
-
"sdd-status",
|
|
56
|
-
"gentle-ai:install-sdd",
|
|
57
|
-
"gentle-ai:sdd-preflight",
|
|
58
|
-
"gentle-ai:sdd-status",
|
|
59
|
-
"gentle-ai:sdd-continue",
|
|
60
46
|
"gentle-ai:models",
|
|
61
47
|
"gentleman:models",
|
|
62
48
|
"gentle-ai:persona",
|
|
@@ -72,6 +58,7 @@ const FORBIDDEN_COMPAT_COMMANDS = [
|
|
|
72
58
|
function createPi() {
|
|
73
59
|
const hooks = new Map();
|
|
74
60
|
const commands = new Map();
|
|
61
|
+
const providers = new Map();
|
|
75
62
|
const flags = new Map();
|
|
76
63
|
const tools = new Map();
|
|
77
64
|
const eventHandlers = new Map();
|
|
@@ -104,6 +91,10 @@ function createPi() {
|
|
|
104
91
|
registerCommand(name, definition) {
|
|
105
92
|
commands.set(name, definition);
|
|
106
93
|
},
|
|
94
|
+
registerProvider(name, config) {
|
|
95
|
+
if (typeof name === "object") providers.set(name.id, name);
|
|
96
|
+
else providers.set(name, config);
|
|
97
|
+
},
|
|
107
98
|
registerFlag(name, definition) {
|
|
108
99
|
flags.set(name, definition);
|
|
109
100
|
},
|
|
@@ -136,7 +127,7 @@ function createPi() {
|
|
|
136
127
|
},
|
|
137
128
|
};
|
|
138
129
|
|
|
139
|
-
return { pi, hooks, commands, flags, tools, emittedEvents };
|
|
130
|
+
return { pi, hooks, commands, providers, flags, tools, emittedEvents };
|
|
140
131
|
}
|
|
141
132
|
|
|
142
133
|
function createUi() {
|
|
@@ -217,25 +208,56 @@ function restoreWorkspaceWritePermissions(cwd) {
|
|
|
217
208
|
}
|
|
218
209
|
}
|
|
219
210
|
|
|
220
|
-
|
|
211
|
+
const ownedFixtureRoots = [];
|
|
212
|
+
const previousFixtureEnvironment = new Map([
|
|
213
|
+
"HOME", "USERPROFILE", "XDG_CONFIG_HOME", "PI_CODING_AGENT_DIR",
|
|
214
|
+
"GENTLE_PI_CONFIG_HOME", "GENTLE_PI_AGENT_HOME", "GENTLE_PI_TEST_ASSETS_DIR",
|
|
215
|
+
"GENTLE_PI_AGENTS_PARENT_PERMISSION_FD",
|
|
216
|
+
].map((key) => [key, process.env[key]]));
|
|
217
|
+
|
|
218
|
+
async function loadExtensions(pi, fixtureDependencies) {
|
|
221
219
|
for (const [index, rel] of EXTENSIONS.entries()) {
|
|
222
220
|
const mod = await import(`${pathToFileURL(join(ROOT, rel)).href}?runtime-harness=${index}`);
|
|
223
221
|
assert.equal(typeof mod.default, "function", `${rel} must export a default function`);
|
|
224
|
-
|
|
222
|
+
if (rel === "extensions/gentle-ai.ts") createGentleAiExtension(fixtureDependencies)(pi);
|
|
223
|
+
else mod.default(pi);
|
|
225
224
|
}
|
|
226
225
|
}
|
|
227
226
|
|
|
228
227
|
async function run() {
|
|
229
228
|
const globalConfigHome = await tempWorkspace();
|
|
229
|
+
ownedFixtureRoots.push(globalConfigHome);
|
|
230
230
|
const globalAgentHome = await tempWorkspace();
|
|
231
|
+
ownedFixtureRoots.push(globalAgentHome);
|
|
231
232
|
const ambientTestAssetsDir = await tempWorkspace();
|
|
233
|
+
ownedFixtureRoots.push(ambientTestAssetsDir);
|
|
232
234
|
process.env.GENTLE_PI_CONFIG_HOME = globalConfigHome;
|
|
233
235
|
process.env.GENTLE_PI_AGENT_HOME = globalAgentHome;
|
|
234
236
|
process.env.GENTLE_PI_TEST_ASSETS_DIR = ambientTestAssetsDir;
|
|
237
|
+
process.env.HOME = globalAgentHome;
|
|
238
|
+
process.env.USERPROFILE = globalAgentHome;
|
|
239
|
+
process.env.XDG_CONFIG_HOME = join(globalAgentHome, "xdg");
|
|
240
|
+
process.env.PI_CODING_AGENT_DIR = join(globalAgentHome, "pi");
|
|
241
|
+
// Discovery still exercises the default exports under the actual worker
|
|
242
|
+
// role, but must never acquire the worker's parent-permission channel.
|
|
243
|
+
delete process.env.GENTLE_PI_AGENTS_PARENT_PERMISSION_FD;
|
|
244
|
+
const fixtureDependencies = {
|
|
245
|
+
nativeReviewCli: {},
|
|
246
|
+
processEnv: {
|
|
247
|
+
HOME: globalAgentHome, USERPROFILE: globalAgentHome,
|
|
248
|
+
GENTLE_PI_CONFIG_HOME: globalConfigHome, GENTLE_PI_AGENT_HOME: globalAgentHome,
|
|
249
|
+
PI_CODING_AGENT_DIR: process.env.PI_CODING_AGENT_DIR,
|
|
250
|
+
XDG_CONFIG_HOME: process.env.XDG_CONFIG_HOME,
|
|
251
|
+
GENTLE_PI_AGENTS_CHILD: "0", GENTLE_AI_TELEMETRY: "0",
|
|
252
|
+
},
|
|
253
|
+
resolveTelemetryTriggerBinary: () => join(globalAgentHome, "never-executed"),
|
|
254
|
+
telemetryTriggerSpawn: () => assert.fail("Runtime fixtures must not spawn telemetry"),
|
|
255
|
+
};
|
|
235
256
|
const globalModelsPath = join(globalConfigHome, "models.json");
|
|
236
257
|
const globalSubagentsPath = join(globalAgentHome, "subagents.json");
|
|
237
|
-
const { pi, hooks, commands, flags, tools, emittedEvents } = createPi();
|
|
238
|
-
await loadExtensions(pi);
|
|
258
|
+
const { pi, hooks, commands, providers, flags, tools, emittedEvents } = createPi();
|
|
259
|
+
await loadExtensions(pi, fixtureDependencies);
|
|
260
|
+
assert.equal(providers.get("nan")?.getModels()[0]?.api, "openai-completions", "runtime extension loading registers the NaN provider");
|
|
239
261
|
|
|
240
262
|
// gentle-pi#404: a collect binding that returns the native last-event
|
|
241
263
|
// closure must terminate after one capture. It must not re-enter a public
|
|
@@ -313,7 +335,7 @@ async function run() {
|
|
|
313
335
|
},
|
|
314
336
|
};
|
|
315
337
|
const lastEventPi = createPi();
|
|
316
|
-
createGentleAiExtension({ nativeReviewCli })(lastEventPi.pi);
|
|
338
|
+
createGentleAiExtension({ ...fixtureDependencies, nativeReviewCli })(lastEventPi.pi);
|
|
317
339
|
const controller = lastEventPi.tools.get("gentle_review");
|
|
318
340
|
const capture = lastEventPi.tools.get("gentle_review_capture");
|
|
319
341
|
assert.ok(controller, "runtime must register the public status controller");
|
|
@@ -356,15 +378,32 @@ async function run() {
|
|
|
356
378
|
for (const name of FORBIDDEN_COMPAT_COMMANDS) {
|
|
357
379
|
assert.equal(commands.has(name), false, `compat command should not be registered: ${name}`);
|
|
358
380
|
}
|
|
381
|
+
// Retired SDD command entry points must not register, regardless of unrelated prompt text.
|
|
382
|
+
for (const name of [
|
|
383
|
+
"gentle:install-sdd", "gentle:sdd-preflight", "gentle-sdd-status",
|
|
384
|
+
"gentle-sdd-continue", "gentle-sdd-init", "sdd-init", "sdd-continue",
|
|
385
|
+
"sdd-status", "gentle-ai:install-sdd", "gentle-ai:sdd-preflight",
|
|
386
|
+
"gentle-ai:sdd-status", "gentle-ai:sdd-continue",
|
|
387
|
+
]) {
|
|
388
|
+
assert.equal(commands.has(name), false, `retired SDD command registered: ${name}`);
|
|
389
|
+
}
|
|
359
390
|
assert.ok(flags.has("no-skill-registry"), "missing no-skill-registry flag");
|
|
360
391
|
assert.ok(hooks.has("session_start"), "missing session_start hook");
|
|
361
392
|
assert.ok(hooks.has("session_shutdown"), "missing session_shutdown hook");
|
|
362
|
-
assert.
|
|
393
|
+
assert.equal(hooks.has("input"), false, "retired SDD slash input must not be intercepted");
|
|
363
394
|
assert.ok(hooks.has("before_agent_start"), "missing before_agent_start hook");
|
|
364
395
|
assert.ok(hooks.has("tool_call"), "missing tool_call hook");
|
|
365
|
-
for (const toolName of ["read", "
|
|
396
|
+
for (const toolName of ["read", "grep", "find", "ls", "edit", "write"]) {
|
|
366
397
|
assert.ok(tools.has(toolName), `missing quiet built-in tool renderer ${toolName}`);
|
|
367
398
|
}
|
|
399
|
+
const codemode = tools.get("codemode");
|
|
400
|
+
assert.ok(codemode, "quiet-tools must decorate the upstream codemode registration");
|
|
401
|
+
assert.equal(codemode.defaultActive, false, "rendering must not activate codemode");
|
|
402
|
+
assert.equal(codemode.renderShell, "self");
|
|
403
|
+
assert.equal(typeof codemode.renderCall, "function");
|
|
404
|
+
assert.equal(typeof codemode.renderResult, "function");
|
|
405
|
+
assert.equal(typeof codemode.prepareLoadout, "function", "upstream loadout policy must survive decoration");
|
|
406
|
+
assert.deepEqual(pi.getActiveTools(), ["read", "bash", "edit", "write"], "card registration must not change tool activation");
|
|
368
407
|
assert.ok(tools.has("gentle_review"), "missing registered bounded review controller tool");
|
|
369
408
|
assert.ok(tools.has("gentle_review_scope"), "missing registered bounded review scope tool");
|
|
370
409
|
assert.deepEqual(
|
|
@@ -389,6 +428,22 @@ async function run() {
|
|
|
389
428
|
[],
|
|
390
429
|
"declared extension directory must load without invalid helper modules",
|
|
391
430
|
);
|
|
431
|
+
assert.ok(
|
|
432
|
+
discovered.extensions.some((extension) => extension.resolvedPath.endsWith(join("extensions", "nan-provider.ts"))),
|
|
433
|
+
"declared extension directory must discover the NaN provider",
|
|
434
|
+
);
|
|
435
|
+
|
|
436
|
+
const nativeNan = discovered.runtime.pendingNativeProviderRegistrations
|
|
437
|
+
.find((entry) => entry.provider.id === "nan")?.provider;
|
|
438
|
+
assert.ok(nativeNan, "actual Pi loader must queue native NaN registration");
|
|
439
|
+
assert.ok(!discovered.runtime.pendingProviderRegistrations.some((entry) => entry.name === "nan"),
|
|
440
|
+
"NaN must not fall back to the legacy empty-key login route");
|
|
441
|
+
await assert.rejects(nativeNan.auth.apiKey.login({
|
|
442
|
+
signal: new AbortController().signal, prompt: async () => "", notify() {},
|
|
443
|
+
}), /non-empty/);
|
|
444
|
+
assert.deepEqual(await nativeNan.auth.apiKey.login({
|
|
445
|
+
signal: new AbortController().signal, prompt: async () => " synthetic-loader-key ", notify() {},
|
|
446
|
+
}), { type: "api_key", key: "synthetic-loader-key" });
|
|
392
447
|
|
|
393
448
|
// orchestrator-lazy-diet: Pi Subagent Model Routing detail (the "do not
|
|
394
449
|
// pass the `model` parameter by default" / SDD-model-assignment-scoping
|
|
@@ -400,62 +455,62 @@ async function run() {
|
|
|
400
455
|
const promptCwd = await tempWorkspace();
|
|
401
456
|
try {
|
|
402
457
|
const promptHook = hooks.get("before_agent_start")[0];
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
assert.
|
|
412
|
-
assert.
|
|
413
|
-
assert.
|
|
414
|
-
assert.match(
|
|
458
|
+
// gentle-shell#1485: pi-claude-bridge drops a handler-returned systemPrompt
|
|
459
|
+
// and forwards only structured systemPromptOptions, so the harness must
|
|
460
|
+
// land in appendSystemPrompt and the hook must never return a replacement.
|
|
461
|
+
const promptEvent = { systemPrompt: "base", systemPromptOptions: { appendSystemPrompt: "" } };
|
|
462
|
+
const promptResult = await promptHook(promptEvent, createCtx(promptCwd));
|
|
463
|
+
assert.equal(promptResult, undefined, "before_agent_start must not return a replacement systemPrompt");
|
|
464
|
+
assert.equal(promptEvent.systemPrompt, "base", "the original systemPrompt field must be left untouched");
|
|
465
|
+
const promptAppended = promptEvent.systemPromptOptions.appendSystemPrompt;
|
|
466
|
+
assert.match(promptAppended, /el Gentleman/);
|
|
467
|
+
assert.match(promptAppended, /Organic Driven Development/);
|
|
468
|
+
assert.doesNotMatch(promptAppended, /## SDD Research Capabilities/);
|
|
469
|
+
assert.match(promptAppended, /review execution contract/);
|
|
470
|
+
assert.doesNotMatch(await readFile(join(ROOT, "extensions", "gentle-ai.ts"), "utf8"), /readCommandSddStatus/);
|
|
471
|
+
assert.match(promptAppended + delegationDetail, /do not pass the `model` parameter by default/);
|
|
472
|
+
assert.doesNotMatch(promptAppended, /Every Agent tool call MUST include `model`/);
|
|
415
473
|
assert.ok(
|
|
416
|
-
|
|
474
|
+
promptAppended.includes(
|
|
417
475
|
`Package assets root: \`${join(ROOT, "assets")}\`. Lazy asset paths below are relative to this root.`,
|
|
418
476
|
),
|
|
419
477
|
"parent prompt must declare the one absolute root for relative lazy asset paths",
|
|
420
478
|
);
|
|
421
|
-
assert.match(promptResult.systemPrompt, /`sdd-orchestrator-workflow\.md`/);
|
|
422
479
|
assert.doesNotMatch(
|
|
423
|
-
|
|
480
|
+
promptAppended,
|
|
424
481
|
new RegExp(ambientTestAssetsDir.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")),
|
|
425
482
|
"normal runtime must ignore ambient GENTLE_PI_TEST_ASSETS_DIR",
|
|
426
483
|
);
|
|
427
|
-
assert.doesNotMatch(promptResult.systemPrompt, /\{\{GENTLE_PI_SDD_WORKFLOW_PATH\}\}/);
|
|
428
484
|
delete process.env.GENTLE_PI_TEST_ASSETS_DIR;
|
|
429
485
|
await rm(ambientTestAssetsDir, { recursive: true, force: true });
|
|
430
486
|
await writeFile(
|
|
431
487
|
join(globalConfigHome, "persona.json"),
|
|
432
488
|
'{"mode":"neutral"}\n',
|
|
433
489
|
);
|
|
434
|
-
const
|
|
435
|
-
|
|
490
|
+
const neutralPromptEvent = { systemPrompt: "base", systemPromptOptions: { appendSystemPrompt: "" } };
|
|
491
|
+
const neutralPromptResult = await promptHook(neutralPromptEvent, createCtx(promptCwd));
|
|
492
|
+
assert.equal(neutralPromptResult, undefined, "before_agent_start must not return a replacement systemPrompt");
|
|
493
|
+
const neutralAppended = neutralPromptEvent.systemPromptOptions.appendSystemPrompt;
|
|
494
|
+
assert.match(neutralAppended, /Do not use slang or regional expressions/);
|
|
436
495
|
assert.doesNotMatch(
|
|
437
|
-
|
|
496
|
+
neutralAppended,
|
|
438
497
|
/When the user writes Spanish, answer in natural Rioplatense Spanish with voseo/,
|
|
439
498
|
"neutral persona prompt must not include unconditional voseo instructions after reload",
|
|
440
499
|
);
|
|
441
|
-
const
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
);
|
|
445
|
-
assert.equal(subagentPromptResult.systemPrompt, "worker base");
|
|
446
|
-
assert.equal(
|
|
447
|
-
existsSync(join(promptCwd, ".pi", "agents", "sdd-apply.md")),
|
|
448
|
-
false,
|
|
449
|
-
"normal agent startup must not run SDD preflight",
|
|
450
|
-
);
|
|
500
|
+
const subagentEvent = { agentName: "worker", systemPrompt: "worker base", systemPromptOptions: { appendSystemPrompt: "" } };
|
|
501
|
+
const subagentPromptResult = await promptHook(subagentEvent, createCtx(promptCwd));
|
|
502
|
+
assert.equal(subagentPromptResult, undefined, "before_agent_start must not return a replacement systemPrompt");
|
|
503
|
+
assert.equal(subagentEvent.systemPromptOptions.appendSystemPrompt, "", "a named agent gets nothing appended");
|
|
451
504
|
await mkdir(join(promptCwd, ".pi", "gentle-ai"), { recursive: true });
|
|
452
505
|
await writeFile(
|
|
453
506
|
join(promptCwd, ".pi", "gentle-ai", "persona.json"),
|
|
454
507
|
'{"mode":"gentleman"}\n',
|
|
455
508
|
);
|
|
456
|
-
const
|
|
509
|
+
const localOverrideEvent = { systemPrompt: "base", systemPromptOptions: { appendSystemPrompt: "" } };
|
|
510
|
+
const localOverridePromptResult = await promptHook(localOverrideEvent, createCtx(promptCwd));
|
|
511
|
+
assert.equal(localOverridePromptResult, undefined, "before_agent_start must not return a replacement systemPrompt");
|
|
457
512
|
assert.match(
|
|
458
|
-
|
|
513
|
+
localOverrideEvent.systemPromptOptions.appendSystemPrompt,
|
|
459
514
|
/When the user writes Spanish, answer in natural Rioplatense Spanish with voseo/,
|
|
460
515
|
);
|
|
461
516
|
const personaCtx = createCtx(promptCwd, true);
|
|
@@ -470,54 +525,6 @@ async function run() {
|
|
|
470
525
|
'{\n "mode": "neutral"\n}\n',
|
|
471
526
|
);
|
|
472
527
|
assert.match(personaCtx.ui.notifications.at(-1).message, /Global config:/);
|
|
473
|
-
const onboardCtx = createCtx(promptCwd, true, "sdd-onboard-session");
|
|
474
|
-
onboardCtx.ui.select = async (_label, options) => options[0];
|
|
475
|
-
const onboardPromptResult = await promptHook(
|
|
476
|
-
{ agentName: "sdd-onboard", systemPrompt: "onboard base" },
|
|
477
|
-
onboardCtx,
|
|
478
|
-
);
|
|
479
|
-
assert.match(onboardPromptResult.systemPrompt, /onboard base/);
|
|
480
|
-
assert.match(onboardPromptResult.systemPrompt, /## SDD Session Preflight/);
|
|
481
|
-
assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-onboard.md")), true);
|
|
482
|
-
await mkdir(join(promptCwd, "openspec", "changes", "status-demo", "specs", "demo"), { recursive: true });
|
|
483
|
-
await writeFile(join(promptCwd, "openspec", "changes", "status-demo", "proposal.md"), "# Proposal\n");
|
|
484
|
-
await writeFile(join(promptCwd, "openspec", "changes", "status-demo", "specs", "demo", "spec.md"), "# Spec\n");
|
|
485
|
-
await writeFile(join(promptCwd, "openspec", "changes", "status-demo", "design.md"), "# Design\n");
|
|
486
|
-
await writeFile(join(promptCwd, "openspec", "changes", "status-demo", "tasks.md"), "# Tasks\n\n- [ ] 1.1 Implement demo\n");
|
|
487
|
-
const applyPromptResult = await promptHook(
|
|
488
|
-
{ agentName: "sdd-apply", systemPrompt: "apply base" },
|
|
489
|
-
createCtx(promptCwd, true, "sdd-apply-session"),
|
|
490
|
-
);
|
|
491
|
-
assert.match(applyPromptResult.systemPrompt, /## Native SDD Status Engine/);
|
|
492
|
-
assert.match(applyPromptResult.systemPrompt, /"changeName": "status-demo"/);
|
|
493
|
-
assert.match(applyPromptResult.systemPrompt, /### apply instructions/);
|
|
494
|
-
const statusCtx = createCtx(promptCwd, true);
|
|
495
|
-
await commands.get("gentle-sdd-status").handler("status-demo --json", statusCtx);
|
|
496
|
-
assert.match(statusCtx.ui.notifications.at(-1).message, /"schemaName": "gentle-ai\.sdd-status"/);
|
|
497
|
-
const continueCtx = createCtx(promptCwd, true);
|
|
498
|
-
let markerConfirmations = 0;
|
|
499
|
-
continueCtx.ui.confirm = async (_title, message) => {
|
|
500
|
-
assert.ok(message.includes(join(promptCwd, "openspec/changes/status-demo/.gentle-ai-instance")));
|
|
501
|
-
assert.match(message, /no source roots and no persistent authority/);
|
|
502
|
-
markerConfirmations += 1;
|
|
503
|
-
return true;
|
|
504
|
-
};
|
|
505
|
-
await commands.get("gentle-sdd-continue").handler("status-demo", continueCtx);
|
|
506
|
-
assert.equal(markerConfirmations, 1);
|
|
507
|
-
assert.match(continueCtx.ui.notifications.at(-1).message, /Native SDD Status Engine/);
|
|
508
|
-
assert.match(continueCtx.ui.notifications.at(-1).message, /"nextRecommended": "apply"/);
|
|
509
|
-
const { execFileSync } = await import("node:child_process");
|
|
510
|
-
execFileSync("git", ["init"], { cwd: promptCwd, stdio: "ignore" });
|
|
511
|
-
const recoveryRequiredDirectory = join(promptCwd, ".git", "gentle-ai", "reviews", "control", "recovery-required-v1");
|
|
512
|
-
await mkdir(recoveryRequiredDirectory, { recursive: true });
|
|
513
|
-
await writeFile(
|
|
514
|
-
join(recoveryRequiredDirectory, `${domainHashV1("openspec-change-name", "status-demo")}.json`),
|
|
515
|
-
'{"schema":"gentle-ai.recovery-required/v1","change_name":"status-demo"}',
|
|
516
|
-
);
|
|
517
|
-
const blockedContinueCtx = createCtx(promptCwd, true);
|
|
518
|
-
await commands.get("gentle-sdd-continue").handler("status-demo", blockedContinueCtx);
|
|
519
|
-
assert.doesNotMatch(blockedContinueCtx.ui.notifications.at(-1).message, /resolve-review:/);
|
|
520
|
-
assert.match(blockedContinueCtx.ui.notifications.at(-1).message, /"nextRecommended": "apply"/);
|
|
521
528
|
} finally {
|
|
522
529
|
await rm(promptCwd, { recursive: true, force: true });
|
|
523
530
|
}
|
|
@@ -606,42 +613,6 @@ async function run() {
|
|
|
606
613
|
"a writer may dispatch when context carries narrow task-scoped repository-relative paths",
|
|
607
614
|
);
|
|
608
615
|
|
|
609
|
-
const interactiveSddDispatch = {
|
|
610
|
-
agent: "sdd-remediate",
|
|
611
|
-
task: "Correct the bound failed verification evidence.",
|
|
612
|
-
context: "Retain the parent-supplied remediation scope.",
|
|
613
|
-
mode: "task",
|
|
614
|
-
};
|
|
615
|
-
assert.equal(
|
|
616
|
-
await toolHook({ toolName: "subagent_run", input: interactiveSddDispatch }, createCtx(toolCwd, true, "interactive-sdd-parent")),
|
|
617
|
-
undefined,
|
|
618
|
-
"an interactive parent may dispatch an SDD child only after preflight resolves",
|
|
619
|
-
);
|
|
620
|
-
assert.match(
|
|
621
|
-
interactiveSddDispatch.context,
|
|
622
|
-
/^## SDD Session Preflight\nThese SDD preferences are explicit current-session choices\./,
|
|
623
|
-
"the parent-confirmed rendered preflight must be transported through the RPC child's context",
|
|
624
|
-
);
|
|
625
|
-
const rpcChildCwd = await tempWorkspace();
|
|
626
|
-
try {
|
|
627
|
-
const rpcChild = createPi();
|
|
628
|
-
createGentleAiExtension({ processEnv: { GENTLE_PI_AGENTS_CHILD: "1" } })(rpcChild.pi);
|
|
629
|
-
const rpcChildCtx = createCtx(rpcChildCwd, false, "delegated-rpc-sdd-child");
|
|
630
|
-
rpcChildCtx.mode = "rpc";
|
|
631
|
-
const rpcChildPrompt = await rpcChild.hooks.get("before_agent_start")[0](
|
|
632
|
-
{ agentName: "sdd-remediate", systemPrompt: "You are the SDD remediate executor for Gentle AI." },
|
|
633
|
-
rpcChildCtx,
|
|
634
|
-
);
|
|
635
|
-
assert.doesNotMatch(rpcChildPrompt.systemPrompt, /## SDD Session Preflight/);
|
|
636
|
-
assert.equal(
|
|
637
|
-
existsSync(join(rpcChildCwd, ".pi", "gentle-ai", "sdd-preflight.json")),
|
|
638
|
-
false,
|
|
639
|
-
"a delegated RPC child must not silently originate or persist defaults",
|
|
640
|
-
);
|
|
641
|
-
} finally {
|
|
642
|
-
await rm(rpcChildCwd, { recursive: true, force: true });
|
|
643
|
-
}
|
|
644
|
-
|
|
645
616
|
const canonicalFrozenFindingRow = '{"id":"JD-A-001","lens":"judgment-day","location":"extensions/gentle-ai.ts:1","severity":"CRITICAL","status_at_freeze":"open","evidence_class":"deterministic","evidence_claim":"The frozen finding has concrete user impact."}';
|
|
646
617
|
const canonicalFrozenFindingRows = [JSON.parse(canonicalFrozenFindingRow)];
|
|
647
618
|
const canonicalFrozenLedgerHash = canonicalHash(canonicalFrozenFindingRows);
|
|
@@ -899,7 +870,7 @@ async function run() {
|
|
|
899
870
|
registry.bindCurrent({ token: view.token, lineageId: "harness-candidate-drift", selectedLenses: ["review-risk"] });
|
|
900
871
|
|
|
901
872
|
const dispatchPi = createPi();
|
|
902
|
-
createGentleAiExtension({ candidateViews: registry })(dispatchPi.pi);
|
|
873
|
+
createGentleAiExtension({ ...fixtureDependencies, candidateViews: registry })(dispatchPi.pi);
|
|
903
874
|
const dispatchToolHook = dispatchPi.hooks.get("tool_call")[0];
|
|
904
875
|
|
|
905
876
|
// The contributor edits the tracked file strictly after the candidate
|
|
@@ -1030,24 +1001,9 @@ async function run() {
|
|
|
1030
1001
|
for (const handler of hooks.get("session_start")) {
|
|
1031
1002
|
await handler({ reason: "startup" }, createCtx(noUiCwd, false));
|
|
1032
1003
|
}
|
|
1033
|
-
assert.equal(
|
|
1034
|
-
existsSync(join(noUiCwd, ".pi", "agents", "sdd-apply.md")),
|
|
1035
|
-
false,
|
|
1036
|
-
"session_start must not install project-local SDD agents",
|
|
1037
|
-
);
|
|
1038
|
-
assert.equal(
|
|
1039
|
-
existsSync(join(noUiCwd, ".pi", "chains", "sdd-full.chain.md")),
|
|
1040
|
-
false,
|
|
1041
|
-
"session_start must not install project-local SDD chains",
|
|
1042
|
-
);
|
|
1043
|
-
assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-apply.md")), false);
|
|
1044
|
-
assert.equal(existsSync(join(globalAgentHome, "chains", "sdd-full.chain.md")), false);
|
|
1045
|
-
assert.equal(existsSync(join(globalAgentHome, "gentle-ai", "support")), false);
|
|
1046
1004
|
for (const diagnostic of ["gentle:status", "gentle:doctor"]) {
|
|
1047
1005
|
const ctx = createCtx(noUiCwd, true);
|
|
1048
1006
|
await commands.get(diagnostic).handler("", ctx);
|
|
1049
|
-
assert.match(ctx.ui.notifications.at(-1).message, /Global SDD assets: on demand/);
|
|
1050
|
-
assert.doesNotMatch(ctx.ui.notifications.at(-1).message, /install-sdd --force/);
|
|
1051
1007
|
}
|
|
1052
1008
|
// gentle-pi#311 P5: the Pi-authored adversarial role agents are retired;
|
|
1053
1009
|
// installation must not (re)create them.
|
|
@@ -1071,19 +1027,10 @@ async function run() {
|
|
|
1071
1027
|
);
|
|
1072
1028
|
assert.match(installedRiskSource, /exactly once against the supplied `initial_review_tree`/);
|
|
1073
1029
|
assert.match(installedRiskSource, /cannot authorize transitions, fixes, receipts, gates, or delivery/);
|
|
1074
|
-
await commands.get("gentle:sdd-preflight").handler("", createCtx(noUiCwd, false, "startup-sdd-install"));
|
|
1075
|
-
assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-apply.md")), true);
|
|
1076
|
-
assert.equal(existsSync(join(globalAgentHome, "chains", "sdd-full.chain.md")), true);
|
|
1077
|
-
assert.equal(existsSync(join(globalAgentHome, "gentle-ai", "support", "sdd-status-contract.md")), true);
|
|
1078
1030
|
const managedAssetsManifestPath = join(globalAgentHome, "gentle-ai", "managed-assets.json");
|
|
1079
1031
|
const managedAssetsManifest = JSON.parse(
|
|
1080
1032
|
await readFile(managedAssetsManifestPath, "utf8"),
|
|
1081
1033
|
);
|
|
1082
|
-
const previousManagedApply = "stale global apply\n";
|
|
1083
|
-
const previousManagedChain = "stale global chain\n";
|
|
1084
|
-
const previousManagedSupport = "stale global status contract\n";
|
|
1085
|
-
await writeFile(join(globalAgentHome, "agents", "sdd-apply.md"), previousManagedApply);
|
|
1086
|
-
managedAssetsManifest.assets["agents/sdd-apply.md"] = sha256(previousManagedApply);
|
|
1087
1034
|
const samePathUserRefuter = [
|
|
1088
1035
|
"---",
|
|
1089
1036
|
"name: review-refuter",
|
|
@@ -1106,60 +1053,25 @@ async function run() {
|
|
|
1106
1053
|
await mkdir(join(globalAgentHome, "subagents"), { recursive: true });
|
|
1107
1054
|
const userRefuterOverride = join(globalAgentHome, "subagents", "review-refuter.md");
|
|
1108
1055
|
await writeFile(userRefuterOverride, "user refuter override must stay\n");
|
|
1109
|
-
await writeFile(join(globalAgentHome, "chains", "sdd-full.chain.md"), previousManagedChain);
|
|
1110
|
-
managedAssetsManifest.assets["chains/sdd-full.chain.md"] = sha256(previousManagedChain);
|
|
1111
|
-
await writeFile(join(globalAgentHome, "gentle-ai", "support", "sdd-status-contract.md"), previousManagedSupport);
|
|
1112
|
-
managedAssetsManifest.assets["gentle-ai/support/sdd-status-contract.md"] = sha256(previousManagedSupport);
|
|
1113
1056
|
await writeFile(
|
|
1114
1057
|
managedAssetsManifestPath,
|
|
1115
1058
|
JSON.stringify(managedAssetsManifest, null, 2),
|
|
1116
1059
|
);
|
|
1117
|
-
await mkdir(join(noUiCwd, ".pi", "agents"), { recursive: true });
|
|
1118
|
-
await writeFile(join(noUiCwd, ".pi", "agents", "sdd-apply.md"), "project override must stay\n");
|
|
1119
1060
|
const projectRefuterOverride = join(noUiCwd, ".pi", "agents", "review-refuter.md");
|
|
1061
|
+
await mkdir(dirname(projectRefuterOverride), { recursive: true });
|
|
1120
1062
|
await writeFile(projectRefuterOverride, "project refuter override must stay\n");
|
|
1121
1063
|
for (const handler of hooks.get("session_start")) {
|
|
1122
1064
|
await handler({ reason: "startup" }, createCtx(noUiCwd, false));
|
|
1123
1065
|
}
|
|
1124
|
-
for (const [key, stale] of [
|
|
1125
|
-
["agents/sdd-apply.md", previousManagedApply],
|
|
1126
|
-
["chains/sdd-full.chain.md", previousManagedChain],
|
|
1127
|
-
["gentle-ai/support/sdd-status-contract.md", previousManagedSupport],
|
|
1128
|
-
]) {
|
|
1129
|
-
assert.equal(await readFile(join(globalAgentHome, key), "utf8"), stale,
|
|
1130
|
-
"startup must preserve existing SDD package content without explicit routing");
|
|
1131
|
-
}
|
|
1132
1066
|
assert.equal(existsSync(retiredManagedValidatorPath), false, "review retirement still runs at startup");
|
|
1133
1067
|
const driftCtx = createCtx(noUiCwd, true);
|
|
1134
1068
|
await commands.get("gentle:status").handler("", driftCtx);
|
|
1135
|
-
assert.match(driftCtx.ui.notifications.at(-1).message, /Global SDD assets stale: 3 file\(s\).*install-sdd --force/);
|
|
1136
1069
|
assert.doesNotMatch(driftCtx.ui.notifications.at(-1).message, /install-(delegation|review) --force/);
|
|
1137
|
-
await commands.get("gentle:sdd-preflight").handler("", createCtx(noUiCwd, false, "startup-sdd-refresh"));
|
|
1138
|
-
assert.notEqual(
|
|
1139
|
-
await readFile(join(globalAgentHome, "agents", "sdd-apply.md"), "utf8"),
|
|
1140
|
-
"stale global apply\n",
|
|
1141
|
-
"SDD preflight must refresh stale global SDD agents",
|
|
1142
|
-
);
|
|
1143
1070
|
assert.equal(
|
|
1144
1071
|
await readFile(installedRefuterPath, "utf8"),
|
|
1145
1072
|
samePathUserRefuter,
|
|
1146
1073
|
"session refresh must preserve a same-path user-authored agent byte-for-byte",
|
|
1147
1074
|
);
|
|
1148
|
-
assert.notEqual(
|
|
1149
|
-
await readFile(join(globalAgentHome, "chains", "sdd-full.chain.md"), "utf8"),
|
|
1150
|
-
"stale global chain\n",
|
|
1151
|
-
"SDD preflight must refresh stale global SDD chains",
|
|
1152
|
-
);
|
|
1153
|
-
assert.notEqual(
|
|
1154
|
-
await readFile(join(globalAgentHome, "gentle-ai", "support", "sdd-status-contract.md"), "utf8"),
|
|
1155
|
-
"stale global status contract\n",
|
|
1156
|
-
"SDD preflight must refresh stale global SDD support files",
|
|
1157
|
-
);
|
|
1158
|
-
assert.equal(
|
|
1159
|
-
await readFile(join(noUiCwd, ".pi", "agents", "sdd-apply.md"), "utf8"),
|
|
1160
|
-
"project override must stay\n",
|
|
1161
|
-
"session_start must not overwrite project-local SDD overrides",
|
|
1162
|
-
);
|
|
1163
1075
|
assert.equal(
|
|
1164
1076
|
await readFile(projectRefuterOverride, "utf8"),
|
|
1165
1077
|
"project refuter override must stay\n",
|
|
@@ -1193,423 +1105,10 @@ async function run() {
|
|
|
1193
1105
|
await rm(noUiCwd, { recursive: true, force: true });
|
|
1194
1106
|
}
|
|
1195
1107
|
|
|
1196
|
-
const
|
|
1197
|
-
const lazyAgentHome = join(lazySddCwd, "agent-home");
|
|
1198
|
-
process.env.GENTLE_PI_AGENT_HOME = lazyAgentHome;
|
|
1199
|
-
try {
|
|
1200
|
-
// This scenario must not inherit SDD definitions from earlier startup tests.
|
|
1201
|
-
const globalAgentHome = lazyAgentHome;
|
|
1202
|
-
await writeFile(
|
|
1203
|
-
globalModelsPath,
|
|
1204
|
-
JSON.stringify({ "sdd-apply": { model: "openai/gpt-5", thinking: "high" } }, null, 2),
|
|
1205
|
-
);
|
|
1206
|
-
const ctx = createCtx(lazySddCwd, true);
|
|
1207
|
-
const inputHook = hooks.get("input")[0];
|
|
1208
|
-
assert.deepEqual(
|
|
1209
|
-
await inputHook({ text: "hola, solo mirando", source: "interactive" }, ctx),
|
|
1210
|
-
{ action: "continue" },
|
|
1211
|
-
);
|
|
1212
|
-
assert.deepEqual(
|
|
1213
|
-
await inputHook({ text: "what is SDD?", source: "interactive" }, ctx),
|
|
1214
|
-
{ action: "continue" },
|
|
1215
|
-
);
|
|
1216
|
-
assert.deepEqual(
|
|
1217
|
-
await inputHook({ text: "what can I do with SDD?", source: "interactive" }, ctx),
|
|
1218
|
-
{ action: "continue" },
|
|
1219
|
-
);
|
|
1220
|
-
assert.deepEqual(
|
|
1221
|
-
await inputHook({ text: "how do I use SDD?", source: "interactive" }, ctx),
|
|
1222
|
-
{ action: "continue" },
|
|
1223
|
-
);
|
|
1224
|
-
assert.deepEqual(
|
|
1225
|
-
await inputHook({ text: "Can I use SDD?", source: "interactive" }, ctx),
|
|
1226
|
-
{ action: "continue" },
|
|
1227
|
-
);
|
|
1228
|
-
assert.deepEqual(
|
|
1229
|
-
await inputHook({ text: "don't use sdd for this", source: "interactive" }, ctx),
|
|
1230
|
-
{ action: "continue" },
|
|
1231
|
-
);
|
|
1232
|
-
assert.deepEqual(
|
|
1233
|
-
await inputHook({ text: "sin usar SDD por ahora", source: "interactive" }, ctx),
|
|
1234
|
-
{ action: "continue" },
|
|
1235
|
-
);
|
|
1236
|
-
assert.deepEqual(
|
|
1237
|
-
await inputHook({ text: "let's not use SDD for this", source: "interactive" }, ctx),
|
|
1238
|
-
{ action: "continue" },
|
|
1239
|
-
);
|
|
1240
|
-
assert.deepEqual(
|
|
1241
|
-
await inputHook({ text: "never use SDD here", source: "interactive" }, ctx),
|
|
1242
|
-
{ action: "continue" },
|
|
1243
|
-
);
|
|
1244
|
-
assert.deepEqual(
|
|
1245
|
-
await inputHook({ text: "no quiero usar SDD por ahora", source: "interactive" }, ctx),
|
|
1246
|
-
{ action: "continue" },
|
|
1247
|
-
);
|
|
1248
|
-
assert.deepEqual(
|
|
1249
|
-
await inputHook({ text: "I use SDD sometimes", source: "interactive" }, ctx),
|
|
1250
|
-
{ action: "continue" },
|
|
1251
|
-
);
|
|
1252
|
-
assert.deepEqual(
|
|
1253
|
-
await inputHook({ text: "I'm using SDD in another repo", source: "interactive" }, ctx),
|
|
1254
|
-
{ action: "continue" },
|
|
1255
|
-
);
|
|
1256
|
-
assert.equal(existsSync(join(lazySddCwd, ".pi", "agents", "sdd-apply.md")), false);
|
|
1257
|
-
assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-apply.md")), false);
|
|
1258
|
-
|
|
1259
|
-
assert.deepEqual(
|
|
1260
|
-
await inputHook({ text: "vamos con sdd", source: "interactive" }, ctx),
|
|
1261
|
-
{ action: "continue" },
|
|
1262
|
-
);
|
|
1263
|
-
assert.deepEqual(
|
|
1264
|
-
await inputHook({ text: "please use sdd for this change", source: "interactive" }, ctx),
|
|
1265
|
-
{ action: "continue" },
|
|
1266
|
-
);
|
|
1267
|
-
assert.equal(existsSync(join(lazySddCwd, ".pi", "agents", "sdd-apply.md")), false);
|
|
1268
|
-
assert.equal(existsSync(join(lazySddCwd, ".pi", "chains", "sdd-full.chain.md")), false);
|
|
1269
|
-
assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-apply.md")), false);
|
|
1270
|
-
assert.equal(ctx.ui.selections.length, 0, "natural-language SDD text has no input-hook side effect");
|
|
1271
|
-
|
|
1272
|
-
assert.deepEqual(
|
|
1273
|
-
await inputHook({ text: "/sdd", source: "interactive" }, ctx),
|
|
1274
|
-
{ action: "continue" },
|
|
1275
|
-
);
|
|
1276
|
-
assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-apply.md")), true);
|
|
1277
|
-
assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-status.md")), true);
|
|
1278
|
-
assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-sync.md")), false);
|
|
1279
|
-
assert.equal(existsSync(join(globalAgentHome, "gentle-ai", "support", "sdd-status-contract.md")), true);
|
|
1280
|
-
assert.equal(existsSync(join(globalAgentHome, "chains", "sdd-full.chain.md")), true);
|
|
1281
|
-
assert.equal(ctx.ui.selections.length, 1, "first slash SDD trigger confirms session suggestions");
|
|
1282
|
-
assert.match(ctx.ui.notifications.at(-1).message, /Preference source: explicit session choice/);
|
|
1283
|
-
assert.deepEqual(
|
|
1284
|
-
await inputHook({ text: "/sdd plan", source: "interactive" }, ctx),
|
|
1285
|
-
{ action: "continue" },
|
|
1286
|
-
);
|
|
1287
|
-
assert.deepEqual(
|
|
1288
|
-
await inputHook({ text: "/sdd:plan", source: "interactive" }, ctx),
|
|
1289
|
-
{ action: "continue" },
|
|
1290
|
-
);
|
|
1291
|
-
assert.equal(ctx.ui.selections.length, 1, "slash SDD triggers reuse confirmed session choices");
|
|
1292
|
-
|
|
1293
|
-
assert.deepEqual(
|
|
1294
|
-
await inputHook({ text: "/sdd-plan this change", source: "interactive" }, ctx),
|
|
1295
|
-
{ action: "continue" },
|
|
1296
|
-
);
|
|
1297
|
-
assert.equal(existsSync(join(lazySddCwd, ".pi", "agents", "sdd-apply.md")), false);
|
|
1298
|
-
assert.equal(existsSync(join(lazySddCwd, ".pi", "chains", "sdd-full.chain.md")), false);
|
|
1299
|
-
assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-apply.md")), true);
|
|
1300
|
-
assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-status.md")), true);
|
|
1301
|
-
assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-sync.md")), false);
|
|
1302
|
-
assert.equal(existsSync(join(globalAgentHome, "gentle-ai", "support", "sdd-status-contract.md")), true);
|
|
1303
|
-
assert.equal(existsSync(join(globalAgentHome, "chains", "sdd-full.chain.md")), true);
|
|
1304
|
-
const globalSddApply = await readFile(
|
|
1305
|
-
join(globalAgentHome, "agents", "sdd-apply.md"),
|
|
1306
|
-
"utf8",
|
|
1307
|
-
);
|
|
1308
|
-
assert.match(globalSddApply, /model: openai\/gpt-5/);
|
|
1309
|
-
assert.match(globalSddApply, /thinking: high/);
|
|
1310
|
-
const lazySettingsPath = join(lazySddCwd, ".pi", "settings.json");
|
|
1311
|
-
if (existsSync(lazySettingsPath)) {
|
|
1312
|
-
const lazySettings = JSON.parse(await readFile(lazySettingsPath, "utf8"));
|
|
1313
|
-
assert.equal(
|
|
1314
|
-
lazySettings.subagents?.agentOverrides?.["sdd-apply"],
|
|
1315
|
-
undefined,
|
|
1316
|
-
"global SDD model routing must be materialized in agent frontmatter, not project settings overrides",
|
|
1317
|
-
);
|
|
1318
|
-
}
|
|
1319
|
-
assert.equal(ctx.ui.selections.length, 1, "automatic SDD routing reuses session confirmation");
|
|
1320
|
-
assert.match(ctx.ui.notifications.at(-1).message, /Preference source: explicit session choice/);
|
|
1321
|
-
await commands.get("gentle:status").handler("", ctx);
|
|
1322
|
-
assert.match(ctx.ui.notifications.at(-1).message, /Global SDD assets stale: 0 file\(s\)/);
|
|
1323
|
-
assert.doesNotMatch(ctx.ui.notifications.at(-1).message, /install-sdd --force/);
|
|
1324
|
-
|
|
1325
|
-
await inputHook({ text: "/sdd-plan another change", source: "interactive" }, ctx);
|
|
1326
|
-
assert.equal(ctx.ui.selections.length, 1, "resolved preflight must not prompt again in this session");
|
|
1327
|
-
const promptHook = hooks.get("before_agent_start")[0];
|
|
1328
|
-
const promptResult = await promptHook({ systemPrompt: "base" }, ctx);
|
|
1329
|
-
assert.match(promptResult.systemPrompt, /SDD Session Preflight/);
|
|
1330
|
-
assert.match(promptResult.systemPrompt, /Execution mode: auto/);
|
|
1331
|
-
const workerPromptResult = await promptHook(
|
|
1332
|
-
{ agentName: "worker", systemPrompt: "worker base" },
|
|
1333
|
-
ctx,
|
|
1334
|
-
);
|
|
1335
|
-
assert.equal(
|
|
1336
|
-
workerPromptResult.systemPrompt,
|
|
1337
|
-
"worker base",
|
|
1338
|
-
"non-SDD subagents must not receive parent harness or SDD preflight prompts",
|
|
1339
|
-
);
|
|
1340
|
-
} finally {
|
|
1341
|
-
process.env.GENTLE_PI_AGENT_HOME = globalAgentHome;
|
|
1342
|
-
await rm(lazySddCwd, { recursive: true, force: true });
|
|
1343
|
-
await rm(globalModelsPath, { force: true });
|
|
1344
|
-
}
|
|
1345
|
-
|
|
1346
|
-
for (const [index, text] of ["/sdd", "/sdd plan", "/sdd:plan", "/sdd-plan this change", "/gentle-sdd-continue", "/gentle-sdd-status fix-rose --json", "/gentle-sdd-init"].entries()) {
|
|
1347
|
-
const slashSddCwd = await tempWorkspace();
|
|
1348
|
-
try {
|
|
1349
|
-
const ctx = createCtx(slashSddCwd, true, `slash-sdd-session-${index}`);
|
|
1350
|
-
const inputHook = hooks.get("input")[0];
|
|
1351
|
-
assert.deepEqual(await inputHook({ text, source: "interactive" }, ctx), {
|
|
1352
|
-
action: "continue",
|
|
1353
|
-
});
|
|
1354
|
-
assert.equal(existsSync(join(slashSddCwd, ".pi", "agents", "sdd-apply.md")), false);
|
|
1355
|
-
assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-apply.md")), true);
|
|
1356
|
-
assert.equal(ctx.ui.selections.length, 1, `${text} must confirm the fresh interactive session`);
|
|
1357
|
-
} finally {
|
|
1358
|
-
await rm(slashSddCwd, { recursive: true, force: true });
|
|
1359
|
-
}
|
|
1360
|
-
}
|
|
1361
|
-
|
|
1362
|
-
const commandPreflightCwd = await tempWorkspace();
|
|
1363
|
-
try {
|
|
1364
|
-
const command = commands.get("gentle:sdd-preflight");
|
|
1365
|
-
const interactive = createCtx(commandPreflightCwd, true, "explicit-command-regression");
|
|
1366
|
-
await command.handler("", interactive);
|
|
1367
|
-
const first = interactive.ui.selections.length;
|
|
1368
|
-
await command.handler("", interactive);
|
|
1369
|
-
const reused = interactive.ui.selections.length;
|
|
1370
|
-
const select = interactive.ui.select;
|
|
1371
|
-
interactive.ui.select = async (label, options) => {
|
|
1372
|
-
const value = await select(label, options);
|
|
1373
|
-
return label === "SDD execution mode" ? "interactive" : value;
|
|
1374
|
-
};
|
|
1375
|
-
interactive.ui.input = async () => "650";
|
|
1376
|
-
await command.handler("--edit", interactive);
|
|
1377
|
-
const edited = interactive.ui.selections.length;
|
|
1378
|
-
await command.handler("", interactive);
|
|
1379
|
-
assert.equal(interactive.ui.selections.length, edited, "edited choices also reuse without prompting");
|
|
1380
|
-
const saved = JSON.parse(await readFile(join(commandPreflightCwd, ".pi", "gentle-ai", "sdd-preflight.json"), "utf8"));
|
|
1381
|
-
assert.equal(saved.executionMode, "interactive");
|
|
1382
|
-
assert.equal(saved.reviewBudgetLines, 650);
|
|
1383
|
-
const rpc = createCtx(commandPreflightCwd, true, "explicit-command-rpc-regression");
|
|
1384
|
-
rpc.mode = "rpc";
|
|
1385
|
-
let rpcInputs = 0;
|
|
1386
|
-
rpc.ui.input = async (_label, placeholder) => { rpcInputs += 1; return placeholder; };
|
|
1387
|
-
await command.handler("", rpc);
|
|
1388
|
-
await command.handler("--edit", rpc);
|
|
1389
|
-
const noUi = createCtx(commandPreflightCwd, false, "explicit-command-no-ui-regression");
|
|
1390
|
-
let noUiInputs = 0;
|
|
1391
|
-
noUi.ui.input = async (_label, placeholder) => { noUiInputs += 1; return placeholder; };
|
|
1392
|
-
await command.handler("", noUi);
|
|
1393
|
-
await command.handler("--edit", noUi);
|
|
1394
|
-
assert.deepEqual({ first, reused, edited, rpc: rpc.ui.selections.length, noUi: noUi.ui.selections.length },
|
|
1395
|
-
{ first: 1, reused: 1, edited: 3, rpc: 0, noUi: 0 },
|
|
1396
|
-
"explicit command confirms once, reuses unless --edit, and never prompts headless callers");
|
|
1397
|
-
assert.deepEqual({ rpcInputs, noUiInputs }, { rpcInputs: 0, noUiInputs: 0 });
|
|
1398
|
-
} finally {
|
|
1399
|
-
await rm(commandPreflightCwd, { recursive: true, force: true });
|
|
1400
|
-
}
|
|
1401
|
-
|
|
1402
|
-
const commandSddCwd = await tempWorkspace();
|
|
1403
|
-
try {
|
|
1404
|
-
const ctx = createCtx(commandSddCwd, true, "command-session");
|
|
1405
|
-
await commands.get("gentle:sdd-preflight").handler("--edit", ctx);
|
|
1406
|
-
assert.equal(existsSync(join(commandSddCwd, ".pi", "agents", "sdd-apply.md")), false);
|
|
1407
|
-
assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-apply.md")), true);
|
|
1408
|
-
assert.equal(ctx.ui.selections.length, 2, "explicit preflight prompts intentional choice fields");
|
|
1409
|
-
assert.equal(ctx.ui.selections.some(({ label }) => label === "SDD artifact store"), false, "one-option artifact store must be elided");
|
|
1410
|
-
assert.match(ctx.ui.notifications.at(-1).message, /Preference source: explicit session choice/);
|
|
1411
|
-
await commands.get("gentle:sdd-preflight").handler("--edit", ctx);
|
|
1412
|
-
assert.equal(ctx.ui.selections.length, 4, "--edit remains an intentional re-prompt");
|
|
1413
|
-
} finally {
|
|
1414
|
-
await rm(commandSddCwd, { recursive: true, force: true });
|
|
1415
|
-
}
|
|
1416
|
-
|
|
1417
|
-
const sddAgentGuardCwd = await tempWorkspace();
|
|
1418
|
-
try {
|
|
1419
|
-
const ctx = createCtx(sddAgentGuardCwd, true, "sdd-agent-guard-session");
|
|
1420
|
-
const promptHook = hooks.get("before_agent_start")[0];
|
|
1421
|
-
const promptResult = await promptHook(
|
|
1422
|
-
{
|
|
1423
|
-
systemPrompt: "You are the SDD proposal executor for Gentle AI.",
|
|
1424
|
-
},
|
|
1425
|
-
ctx,
|
|
1426
|
-
);
|
|
1427
|
-
assert.equal(existsSync(join(sddAgentGuardCwd, ".pi", "agents", "sdd-apply.md")), false);
|
|
1428
|
-
assert.equal(existsSync(join(sddAgentGuardCwd, ".pi", "chains", "sdd-full.chain.md")), false);
|
|
1429
|
-
assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-apply.md")), true);
|
|
1430
|
-
assert.equal(existsSync(join(globalAgentHome, "chains", "sdd-full.chain.md")), true);
|
|
1431
|
-
assert.equal(ctx.ui.selections.length, 1, "fresh interactive SDD-agent startup requires confirmation");
|
|
1432
|
-
assert.match(promptResult.systemPrompt, /SDD Session Preflight/);
|
|
1433
|
-
assert.doesNotMatch(
|
|
1434
|
-
promptResult.systemPrompt,
|
|
1435
|
-
/el Gentleman Identity and Harness/,
|
|
1436
|
-
"SDD executor startup must not receive the parent orchestrator prompt",
|
|
1437
|
-
);
|
|
1438
|
-
assert.doesNotMatch(
|
|
1439
|
-
promptResult.systemPrompt,
|
|
1440
|
-
/Work Routing Ladder/,
|
|
1441
|
-
"SDD executor startup must not receive parent routing instructions",
|
|
1442
|
-
);
|
|
1443
|
-
assert.match(ctx.ui.notifications.at(-1).message, /SDD preflight complete/);
|
|
1444
|
-
|
|
1445
|
-
const reusedPromptResult = await promptHook(
|
|
1446
|
-
{
|
|
1447
|
-
agentName: "sdd-tasks",
|
|
1448
|
-
systemPrompt: "You are the SDD tasks executor for Gentle AI.",
|
|
1449
|
-
},
|
|
1450
|
-
ctx,
|
|
1451
|
-
);
|
|
1452
|
-
assert.equal(ctx.ui.selections.length, 1, "SDD-agent startup reuses confirmed session preferences");
|
|
1453
|
-
assert.doesNotMatch(
|
|
1454
|
-
reusedPromptResult.systemPrompt,
|
|
1455
|
-
/el Gentleman Identity and Harness/,
|
|
1456
|
-
"named SDD executor startup must not receive the parent orchestrator prompt",
|
|
1457
|
-
);
|
|
1458
|
-
} finally {
|
|
1459
|
-
await rm(sddAgentGuardCwd, { recursive: true, force: true });
|
|
1460
|
-
}
|
|
1461
|
-
|
|
1462
|
-
const unresolvedSddCwd = await tempWorkspace();
|
|
1463
|
-
try {
|
|
1464
|
-
const ctx = createCtx(unresolvedSddCwd, true, "unresolved-preflight-session");
|
|
1465
|
-
ctx.ui.select = async () => undefined;
|
|
1466
|
-
assert.deepEqual(await hooks.get("input")[0]({ text: "/sdd", source: "interactive" }, ctx), { action: "handled" });
|
|
1467
|
-
const result = await hooks.get("before_agent_start")[0]({ agentName: "sdd-explore", systemPrompt: "SDD executor" }, ctx);
|
|
1468
|
-
assert.match(result.systemPrompt, /SDD preflight unresolved/);
|
|
1469
|
-
assert.match(result.systemPrompt, /STOP: Do not initialize/);
|
|
1470
|
-
assert.doesNotMatch(result.systemPrompt, /## SDD Session Preflight/);
|
|
1471
|
-
assert.equal(existsSync(join(unresolvedSddCwd, "openspec", "config.yaml")), false);
|
|
1472
|
-
assert.equal(existsSync(join(unresolvedSddCwd, ".pi", "gentle-ai", "sdd-preflight.json")), false);
|
|
1473
|
-
} finally {
|
|
1474
|
-
await rm(unresolvedSddCwd, { recursive: true, force: true });
|
|
1475
|
-
}
|
|
1476
|
-
|
|
1477
|
-
const noUiSddAgentCwd = await tempWorkspace();
|
|
1478
|
-
try {
|
|
1479
|
-
const ctx = createCtx(noUiSddAgentCwd, false, "no-ui-sdd-agent-session");
|
|
1480
|
-
const promptHook = hooks.get("before_agent_start")[0];
|
|
1481
|
-
const promptResult = await promptHook(
|
|
1482
|
-
{
|
|
1483
|
-
agentName: "sdd-proposal",
|
|
1484
|
-
systemPrompt: "You are the SDD proposal executor for Gentle AI.",
|
|
1485
|
-
},
|
|
1486
|
-
ctx,
|
|
1487
|
-
);
|
|
1488
|
-
assert.match(promptResult.systemPrompt, /SDD Session Preflight/);
|
|
1489
|
-
assert.match(promptResult.systemPrompt, /canonical defaults or persisted choices/);
|
|
1490
|
-
assert.equal(ctx.ui.selections.length, 0);
|
|
1491
|
-
assert.equal(existsSync(join(noUiSddAgentCwd, ".pi", "agents", "sdd-apply.md")), false);
|
|
1492
|
-
assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-apply.md")), true);
|
|
1493
|
-
} finally {
|
|
1494
|
-
await rm(noUiSddAgentCwd, { recursive: true, force: true });
|
|
1495
|
-
}
|
|
1496
|
-
|
|
1497
|
-
const invalidPreflightCwd = await tempWorkspace();
|
|
1498
|
-
try {
|
|
1499
|
-
await writeFile(globalModelsPath, "{ invalid json");
|
|
1500
|
-
const ctx = createCtx(invalidPreflightCwd, true, "invalid-preflight-session");
|
|
1501
|
-
await commands.get("gentle:sdd-preflight").handler("", ctx);
|
|
1502
|
-
assert.equal(ctx.ui.notifications.at(-1).level, "warning");
|
|
1503
|
-
assert.match(ctx.ui.notifications.at(-1).message, /Model routing skipped:/);
|
|
1504
|
-
assert.match(ctx.ui.notifications.at(-1).message, /invalid JSON or not an object/);
|
|
1505
|
-
} finally {
|
|
1506
|
-
await rm(invalidPreflightCwd, { recursive: true, force: true });
|
|
1507
|
-
await rm(globalModelsPath, { force: true });
|
|
1508
|
-
}
|
|
1509
|
-
|
|
1510
|
-
const engramSddCwd = await tempWorkspace();
|
|
1511
|
-
try {
|
|
1512
|
-
pi.setActiveTools(["read", "bash", "edit", "write", "mem_save"]);
|
|
1513
|
-
const ctx = createCtx(engramSddCwd, true, "engram-session");
|
|
1514
|
-
await commands.get("gentle:sdd-preflight").handler("--edit", ctx);
|
|
1515
|
-
assert.deepEqual(ctx.ui.selections[1].options, ["openspec", "engram", "hybrid"]);
|
|
1516
|
-
} finally {
|
|
1517
|
-
pi.setActiveTools(["read", "bash", "edit", "write"]);
|
|
1518
|
-
await rm(engramSddCwd, { recursive: true, force: true });
|
|
1519
|
-
}
|
|
1520
|
-
|
|
1521
|
-
// Issue #64: selecting engram as the artifact store must not cause
|
|
1522
|
-
// /gentle-sdd-init to create openspec/ or openspec/config.yaml.
|
|
1523
|
-
const engramSddInitCwd = await tempWorkspace();
|
|
1524
|
-
try {
|
|
1525
|
-
pi.setActiveTools(["read", "bash", "edit", "write", "mem_save"]);
|
|
1526
|
-
const ctx = createCtx(engramSddInitCwd, true, "engram-sdd-init-session");
|
|
1527
|
-
ctx.ui.select = async (label, options) => {
|
|
1528
|
-
if (label === "SDD artifact store") return "engram";
|
|
1529
|
-
return options[0];
|
|
1530
|
-
};
|
|
1531
|
-
await commands.get("gentle:sdd-preflight").handler("--edit", ctx);
|
|
1532
|
-
await commands.get("gentle-sdd-init").handler("", ctx);
|
|
1533
|
-
assert.equal(
|
|
1534
|
-
existsSync(join(engramSddInitCwd, "openspec")),
|
|
1535
|
-
false,
|
|
1536
|
-
"/gentle-sdd-init must not create openspec/ when artifactStore is engram",
|
|
1537
|
-
);
|
|
1538
|
-
assert.equal(
|
|
1539
|
-
existsSync(join(engramSddInitCwd, "openspec", "config.yaml")),
|
|
1540
|
-
false,
|
|
1541
|
-
"/gentle-sdd-init must not write openspec/config.yaml when artifactStore is engram",
|
|
1542
|
-
);
|
|
1543
|
-
assert.doesNotMatch(
|
|
1544
|
-
ctx.ui.notifications.at(-1).message,
|
|
1545
|
-
/Wrote openspec\/config\.yaml/,
|
|
1546
|
-
"/gentle-sdd-init must not announce openspec/config.yaml when artifactStore is engram",
|
|
1547
|
-
);
|
|
1548
|
-
assert.match(
|
|
1549
|
-
ctx.ui.notifications.at(-1).message,
|
|
1550
|
-
/SDD initialized for engram:/,
|
|
1551
|
-
);
|
|
1552
|
-
assert.equal(ctx.ui.notifications.at(-1).level, "info");
|
|
1553
|
-
} finally {
|
|
1554
|
-
pi.setActiveTools(["read", "bash", "edit", "write"]);
|
|
1555
|
-
await rm(engramSddInitCwd, { recursive: true, force: true });
|
|
1556
|
-
}
|
|
1557
|
-
|
|
1558
|
-
// Issue #64 counterpart: the engram skip must stay narrow. Selecting both
|
|
1559
|
-
// still has to create the full openspec/ scaffold and write config.yaml,
|
|
1560
|
-
// so an over-broad skip is caught here instead of in the field.
|
|
1561
|
-
const bothSddInitCwd = await tempWorkspace();
|
|
1562
|
-
try {
|
|
1563
|
-
pi.setActiveTools(["read", "bash", "edit", "write", "mem_save"]);
|
|
1564
|
-
const ctx = createCtx(bothSddInitCwd, true, "both-sdd-init-session");
|
|
1565
|
-
ctx.ui.select = async (label, options) => {
|
|
1566
|
-
if (label === "SDD artifact store") return "hybrid";
|
|
1567
|
-
return options[0];
|
|
1568
|
-
};
|
|
1569
|
-
await commands.get("gentle:sdd-preflight").handler("--edit", ctx);
|
|
1570
|
-
await commands.get("gentle-sdd-init").handler("", ctx);
|
|
1571
|
-
assert.equal(
|
|
1572
|
-
existsSync(join(bothSddInitCwd, "openspec", "specs")),
|
|
1573
|
-
true,
|
|
1574
|
-
"/gentle-sdd-init must create openspec/specs when artifactStore is both",
|
|
1575
|
-
);
|
|
1576
|
-
assert.equal(
|
|
1577
|
-
existsSync(join(bothSddInitCwd, "openspec", "changes", "archive")),
|
|
1578
|
-
true,
|
|
1579
|
-
"/gentle-sdd-init must create openspec/changes/archive when artifactStore is both",
|
|
1580
|
-
);
|
|
1581
|
-
assert.equal(
|
|
1582
|
-
existsSync(join(bothSddInitCwd, "openspec", "config.yaml")),
|
|
1583
|
-
true,
|
|
1584
|
-
"/gentle-sdd-init must write openspec/config.yaml when artifactStore is both",
|
|
1585
|
-
);
|
|
1586
|
-
assert.match(
|
|
1587
|
-
ctx.ui.notifications.at(-1).message,
|
|
1588
|
-
/Wrote openspec\/config\.yaml/,
|
|
1589
|
-
"/gentle-sdd-init must announce openspec/config.yaml when artifactStore is both",
|
|
1590
|
-
);
|
|
1591
|
-
assert.equal(ctx.ui.notifications.at(-1).level, "info");
|
|
1592
|
-
} finally {
|
|
1593
|
-
pi.setActiveTools(["read", "bash", "edit", "write"]);
|
|
1594
|
-
await rm(bothSddInitCwd, { recursive: true, force: true });
|
|
1595
|
-
}
|
|
1596
|
-
|
|
1597
|
-
const directEngramToolCwd = await tempWorkspace();
|
|
1598
|
-
try {
|
|
1599
|
-
pi.setActiveTools(["read", "bash", "edit", "write", "engram_mem_save"]);
|
|
1600
|
-
const ctx = createCtx(directEngramToolCwd, true, "direct-engram-session");
|
|
1601
|
-
await commands.get("gentle:sdd-preflight").handler("", ctx);
|
|
1602
|
-
assert.equal(ctx.ui.selections.some(({ label }) => label === "SDD artifact store"), false, "unrecognized Engram capability must elide the artifact selector");
|
|
1603
|
-
} finally {
|
|
1604
|
-
pi.setActiveTools(["read", "bash", "edit", "write"]);
|
|
1605
|
-
await rm(directEngramToolCwd, { recursive: true, force: true });
|
|
1606
|
-
}
|
|
1607
|
-
|
|
1608
|
-
for (const owner of ["sdd", "delegation", "review"]) {
|
|
1108
|
+
for (const owner of ["delegation", "review"]) {
|
|
1609
1109
|
const fixture = await tempWorkspace();
|
|
1610
1110
|
const agentHome = join(fixture, "agent-home");
|
|
1611
1111
|
const representatives = {
|
|
1612
|
-
sdd: "sdd-apply.md",
|
|
1613
1112
|
delegation: "gentle-ai-worker.md",
|
|
1614
1113
|
review: "review-risk.md",
|
|
1615
1114
|
};
|
|
@@ -1658,7 +1157,7 @@ async function run() {
|
|
|
1658
1157
|
assert.equal(existsSync(join(agentHome, "subagents.json")), false,
|
|
1659
1158
|
"owner installers must not apply model routing");
|
|
1660
1159
|
await commands.get("gentle:status").handler("", ctx);
|
|
1661
|
-
const label = owner
|
|
1160
|
+
const label = owner;
|
|
1662
1161
|
assert.ok(ctx.ui.notifications.at(-1).message.includes(`Global ${label} user overrides: 1 file(s)`));
|
|
1663
1162
|
} finally {
|
|
1664
1163
|
process.env.GENTLE_PI_CONFIG_HOME = globalConfigHome;
|
|
@@ -1671,153 +1170,60 @@ async function run() {
|
|
|
1671
1170
|
try {
|
|
1672
1171
|
process.env.GENTLE_PI_AGENT_HOME = join(repairFixture, "agent-home");
|
|
1673
1172
|
const ctx = createCtx(repairFixture, true);
|
|
1674
|
-
await commands.get("gentle:install-sdd").handler("", ctx);
|
|
1675
1173
|
for (const diagnostic of ["gentle:status", "gentle:doctor"]) {
|
|
1676
1174
|
await commands.get(diagnostic).handler("", ctx);
|
|
1677
1175
|
const message = ctx.ui.notifications.at(-1).message;
|
|
1678
|
-
assert.match(message,
|
|
1176
|
+
assert.match(message, /Global delegation assets stale: [1-9]\d* file\(s\).*\/gentle:install-delegation --force/,
|
|
1679
1177
|
`${diagnostic} must provide a repair for missing delegation assets`);
|
|
1680
|
-
assert.match(message,
|
|
1178
|
+
assert.match(message, /Global review assets stale: [1-9]\d* file\(s\).*\/gentle:install-review --force/,
|
|
1681
1179
|
`${diagnostic} must provide a repair for missing review assets`);
|
|
1682
1180
|
assert.doesNotMatch(message, /install-sdd --force/);
|
|
1683
1181
|
}
|
|
1182
|
+
await commands.get("gentle:install-delegation").handler("", ctx);
|
|
1183
|
+
await commands.get("gentle:install-review").handler("", ctx);
|
|
1684
1184
|
const manifest = JSON.parse(await readFile(join(process.env.GENTLE_PI_AGENT_HOME, "gentle-ai", "managed-assets.json"), "utf8"));
|
|
1685
1185
|
for (const key of Object.keys(manifest.assets)) {
|
|
1686
1186
|
await rm(join(process.env.GENTLE_PI_AGENT_HOME, key));
|
|
1687
1187
|
}
|
|
1688
1188
|
for (const diagnostic of ["gentle:status", "gentle:doctor"]) {
|
|
1689
1189
|
await commands.get(diagnostic).handler("", ctx);
|
|
1690
|
-
|
|
1691
|
-
assert.
|
|
1692
|
-
|
|
1190
|
+
const message = ctx.ui.notifications.at(-1).message;
|
|
1191
|
+
assert.match(message, /Global delegation assets stale: [1-9]\d* file\(s\).*\/gentle:install-delegation --force/);
|
|
1192
|
+
assert.match(message, /Global review assets stale: [1-9]\d* file\(s\).*\/gentle:install-review --force/);
|
|
1193
|
+
assert.doesNotMatch(message, /install-sdd --force|on demand/,
|
|
1194
|
+
"managed installation evidence must survive missing delegation and review files");
|
|
1693
1195
|
}
|
|
1694
1196
|
} finally {
|
|
1695
1197
|
process.env.GENTLE_PI_AGENT_HOME = globalAgentHome;
|
|
1696
1198
|
await rm(repairFixture, { recursive: true, force: true });
|
|
1697
1199
|
}
|
|
1698
1200
|
|
|
1699
|
-
for (const trigger of ["gentle:sdd-preflight", "gentle-sdd-init"]) {
|
|
1700
|
-
const fixture = await tempWorkspace();
|
|
1701
|
-
const agentHome = join(fixture, "agent-home");
|
|
1702
|
-
try {
|
|
1703
|
-
process.env.GENTLE_PI_AGENT_HOME = agentHome;
|
|
1704
|
-
const command = commands.get(trigger);
|
|
1705
|
-
await command.handler("", createCtx(fixture, true, `${trigger}-install`));
|
|
1706
|
-
assert.equal(existsSync(join(agentHome, "agents", "gentle-ai-worker.md")), false,
|
|
1707
|
-
`${trigger} must not install delegation assets`);
|
|
1708
|
-
assert.equal(existsSync(join(agentHome, "agents", "review-risk.md")), false,
|
|
1709
|
-
`${trigger} must not install review assets`);
|
|
1710
|
-
const manifestPath = join(agentHome, "gentle-ai", "managed-assets.json");
|
|
1711
|
-
const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
|
|
1712
|
-
const managedKeys = [
|
|
1713
|
-
"agents/sdd-apply.md",
|
|
1714
|
-
"chains/sdd-full.chain.md",
|
|
1715
|
-
"gentle-ai/support/sdd-status-contract.md",
|
|
1716
|
-
];
|
|
1717
|
-
const originals = new Map();
|
|
1718
|
-
for (const key of managedKeys) {
|
|
1719
|
-
originals.set(key, await readFile(join(agentHome, key), "utf8"));
|
|
1720
|
-
}
|
|
1721
|
-
const untouchedKeys = ["agents/gentle-ai-worker.md", "agents/review-risk.md"];
|
|
1722
|
-
for (const key of [...managedKeys, ...untouchedKeys]) {
|
|
1723
|
-
await writeFile(join(agentHome, key), `stale ${key}\n`);
|
|
1724
|
-
manifest.assets[key] = sha256(`stale ${key}\n`);
|
|
1725
|
-
}
|
|
1726
|
-
await writeFile(manifestPath, JSON.stringify(manifest, null, 2));
|
|
1727
|
-
const userPath = join(agentHome, "agents", "sdd-spec.md");
|
|
1728
|
-
const userEdit = `${await readFile(userPath, "utf8")}\nUser-owned instructions.\n`;
|
|
1729
|
-
await writeFile(userPath, userEdit);
|
|
1730
|
-
const projectOverride = join(fixture, ".pi", "agents", "sdd-apply.md");
|
|
1731
|
-
await mkdir(dirname(projectOverride), { recursive: true });
|
|
1732
|
-
await writeFile(projectOverride, "Project override must survive.\n");
|
|
1733
|
-
await command.handler("", createCtx(fixture, true, `${trigger}-refresh`));
|
|
1734
|
-
for (const key of managedKeys) {
|
|
1735
|
-
assert.equal(await readFile(join(agentHome, key), "utf8"), originals.get(key),
|
|
1736
|
-
`${trigger} must refresh managed ${key} on demand`);
|
|
1737
|
-
}
|
|
1738
|
-
const refreshed = JSON.parse(await readFile(manifestPath, "utf8"));
|
|
1739
|
-
for (const key of untouchedKeys) {
|
|
1740
|
-
assert.equal(await readFile(join(agentHome, key), "utf8"), `stale ${key}\n`);
|
|
1741
|
-
assert.equal(refreshed.assets[key], manifest.assets[key]);
|
|
1742
|
-
}
|
|
1743
|
-
assert.equal(await readFile(userPath, "utf8"), userEdit);
|
|
1744
|
-
assert.equal(await readFile(projectOverride, "utf8"), "Project override must survive.\n");
|
|
1745
|
-
} finally {
|
|
1746
|
-
process.env.GENTLE_PI_AGENT_HOME = globalAgentHome;
|
|
1747
|
-
await rm(fixture, { recursive: true, force: true });
|
|
1748
|
-
}
|
|
1749
|
-
}
|
|
1750
|
-
|
|
1751
|
-
const installCwd = await tempWorkspace();
|
|
1752
|
-
try {
|
|
1753
|
-
const ctx = createCtx(installCwd, true);
|
|
1754
|
-
await commands.get("gentle:install-sdd").handler("", ctx);
|
|
1755
|
-
assert.match(ctx.ui.notifications.at(-1).message, /Global Gentle AI SDD assets installed/);
|
|
1756
|
-
assert.equal(existsSync(join(installCwd, ".pi", "agents", "sdd-apply.md")), false);
|
|
1757
|
-
assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-apply.md")), true);
|
|
1758
|
-
assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-status.md")), true);
|
|
1759
|
-
assert.equal(existsSync(join(globalAgentHome, "gentle-ai", "support", "sdd-status-contract.md")), true);
|
|
1760
|
-
} finally {
|
|
1761
|
-
await rm(installCwd, { recursive: true, force: true });
|
|
1762
|
-
}
|
|
1763
|
-
|
|
1764
1201
|
const staleAssetsCwd = await tempWorkspace();
|
|
1765
1202
|
const previousAgentHome = process.env.GENTLE_PI_AGENT_HOME;
|
|
1766
1203
|
const previousHome = process.env.HOME;
|
|
1767
1204
|
const previousUserProfile = process.env.USERPROFILE;
|
|
1768
1205
|
try {
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
process.env.
|
|
1772
|
-
process.env.HOME = diagnosticsHome;
|
|
1773
|
-
process.env.USERPROFILE = diagnosticsHome;
|
|
1774
|
-
for (const [dir, name] of [
|
|
1775
|
-
[join(diagnosticsAgentHome, "subagents"), "sdd-apply.md"],
|
|
1776
|
-
[join(diagnosticsHome, ".agents"), "sdd-archive.md"],
|
|
1777
|
-
[join(staleAssetsCwd, ".agents"), "sdd-design.md"],
|
|
1778
|
-
[join(staleAssetsCwd, ".pi", "agents"), "sdd-spec.md"],
|
|
1779
|
-
[join(staleAssetsCwd, ".pi", "subagents"), "sdd-sync.md"],
|
|
1780
|
-
]) {
|
|
1781
|
-
await mkdir(dir, { recursive: true });
|
|
1782
|
-
await writeFile(
|
|
1783
|
-
join(dir, name),
|
|
1784
|
-
`---\nname: ${name.replace(/\.md$/, "")}\n---\nintentional SDD override\n`,
|
|
1785
|
-
);
|
|
1786
|
-
}
|
|
1787
|
-
await mkdir(join(diagnosticsAgentHome, "subagents", "nested"), { recursive: true });
|
|
1788
|
-
await mkdir(join(diagnosticsAgentHome, "agents", "nested"), { recursive: true });
|
|
1789
|
-
await mkdir(join(staleAssetsCwd, ".agents", "skills", "ignored"), { recursive: true });
|
|
1790
|
-
await writeFile(
|
|
1791
|
-
join(diagnosticsAgentHome, "subagents", "nested", "renamed-agent.md"),
|
|
1792
|
-
"---\nname: sdd-apply\n---\nintentional nested SDD override\n",
|
|
1793
|
-
);
|
|
1794
|
-
await writeFile(
|
|
1795
|
-
join(diagnosticsAgentHome, "agents", "nested", "managed-copy.md"),
|
|
1796
|
-
"---\nname: sdd-apply\n---\npackage-managed root is excluded\n",
|
|
1797
|
-
);
|
|
1798
|
-
await writeFile(
|
|
1799
|
-
join(staleAssetsCwd, ".agents", "skills", "ignored", "SKILL.md"),
|
|
1800
|
-
"---\nname: sdd-apply\n---\nskills are not agent definitions\n",
|
|
1801
|
-
);
|
|
1802
|
-
await mkdir(join(staleAssetsCwd, ".pi", "chains"), { recursive: true });
|
|
1803
|
-
await mkdir(join(staleAssetsCwd, ".pi", "gentle-ai", "support"), { recursive: true });
|
|
1804
|
-
await writeFile(join(staleAssetsCwd, ".pi", "chains", "sdd-full.chain.md"), "stale chain\n");
|
|
1805
|
-
await writeFile(join(staleAssetsCwd, ".pi", "gentle-ai", "support", "sdd-status-contract.md"), "stale status contract\n");
|
|
1206
|
+
process.env.GENTLE_PI_AGENT_HOME = join(staleAssetsCwd, "agent-home");
|
|
1207
|
+
process.env.HOME = staleAssetsCwd;
|
|
1208
|
+
process.env.USERPROFILE = staleAssetsCwd;
|
|
1806
1209
|
const ctx = createCtx(staleAssetsCwd, true);
|
|
1807
|
-
await commands.get("gentle:status").handler("", ctx);
|
|
1808
|
-
assert.match(ctx.ui.notifications.at(-1).message, /Active SDD agent overrides: 5 file\(s\)/);
|
|
1809
|
-
assert.match(ctx.ui.notifications.at(-1).message, /active non-builtin SDD agents shadow package assets/);
|
|
1810
1210
|
await commands.get("gentle:doctor").handler("", ctx);
|
|
1811
1211
|
assert.match(ctx.ui.notifications.at(-1).message, /el Gentleman doctor/);
|
|
1812
1212
|
assert.match(ctx.ui.notifications.at(-1).message, /Sensitive-path guard active/);
|
|
1213
|
+
for (const diagnostic of ["gentle:status", "gentle:doctor"]) {
|
|
1214
|
+
await commands.get(diagnostic).handler("", ctx);
|
|
1215
|
+
const message = ctx.ui.notifications.at(-1).message;
|
|
1216
|
+
assert.match(message, /Organic Driven Development \(ODD\): active/);
|
|
1217
|
+
assert.doesNotMatch(message, /OpenSpec|openspec\/config\.yaml|SDD/);
|
|
1218
|
+
}
|
|
1813
1219
|
pi.setActiveTools([{ name: "engram.mem_save" }]);
|
|
1814
1220
|
await commands.get("gentle:doctor").handler("", ctx);
|
|
1815
1221
|
assert.match(ctx.ui.notifications.at(-1).message, /Engram memory tools active/);
|
|
1816
1222
|
pi.setActiveTools([{ name: "engram_mem_save" }]);
|
|
1817
1223
|
await commands.get("gentle:doctor").handler("", ctx);
|
|
1818
1224
|
assert.match(ctx.ui.notifications.at(-1).message, /Engram memory tools not active in this session/);
|
|
1819
|
-
pi.setActiveTools(["read", "bash", "edit", "write"]);
|
|
1820
1225
|
} finally {
|
|
1226
|
+
pi.setActiveTools(["read", "bash", "edit", "write"]);
|
|
1821
1227
|
if (previousAgentHome === undefined) delete process.env.GENTLE_PI_AGENT_HOME;
|
|
1822
1228
|
else process.env.GENTLE_PI_AGENT_HOME = previousAgentHome;
|
|
1823
1229
|
if (previousHome === undefined) delete process.env.HOME;
|
|
@@ -1827,59 +1233,6 @@ async function run() {
|
|
|
1827
1233
|
await rm(staleAssetsCwd, { recursive: true, force: true });
|
|
1828
1234
|
}
|
|
1829
1235
|
|
|
1830
|
-
const sddCwd = await tempWorkspace();
|
|
1831
|
-
try {
|
|
1832
|
-
const ctx = createCtx(sddCwd, true);
|
|
1833
|
-
await commands.get("gentle-sdd-init").handler("", ctx);
|
|
1834
|
-
assert.equal(existsSync(join(sddCwd, ".pi", "agents", "sdd-apply.md")), false);
|
|
1835
|
-
assert.equal(existsSync(join(sddCwd, ".pi", "chains", "sdd-full.chain.md")), false);
|
|
1836
|
-
assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-apply.md")), true);
|
|
1837
|
-
assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-status.md")), true);
|
|
1838
|
-
assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-sync.md")), false);
|
|
1839
|
-
assert.equal(existsSync(join(globalAgentHome, "gentle-ai", "support", "sdd-status-contract.md")), true);
|
|
1840
|
-
assert.equal(existsSync(join(globalAgentHome, "chains", "sdd-full.chain.md")), true);
|
|
1841
|
-
assert.equal(ctx.ui.selections.length, 1, "sdd-init confirms session preflight before project initialization");
|
|
1842
|
-
assert.match(ctx.ui.notifications[1].message, /SDD preflight complete/);
|
|
1843
|
-
assert.match(ctx.ui.notifications.at(-1).message, /Wrote openspec\/config\.yaml/);
|
|
1844
|
-
const initializedConfig = await readFile(join(sddCwd, "openspec", "config.yaml"), "utf8");
|
|
1845
|
-
const explore = await hooks.get("before_agent_start")[0]({ agentName: "sdd-explore", systemPrompt: "You are the SDD explore executor for Gentle AI." }, ctx);
|
|
1846
|
-
assert.match(explore.systemPrompt, /explicit current-session choices/);
|
|
1847
|
-
assert.equal(ctx.ui.selections.length, 1, "confirmation -> sdd-init -> explore must reuse the resolved preflight");
|
|
1848
|
-
const nextSession = createCtx(sddCwd, true, "cold-start-with-saved-preferences");
|
|
1849
|
-
await hooks.get("input")[0]({ text: "/sdd", source: "interactive" }, nextSession);
|
|
1850
|
-
assert.equal(nextSession.ui.selections.length, 1, "saved preferences still require confirmation in a new session");
|
|
1851
|
-
assert.equal(await readFile(join(sddCwd, "openspec", "config.yaml"), "utf8"), initializedConfig, "new session confirmation must not reset project initialization");
|
|
1852
|
-
|
|
1853
|
-
await commands.get("gentle:sdd-preflight").handler("--edit", ctx);
|
|
1854
|
-
assert.equal(ctx.ui.selections.length, 3, "--edit permits changes after confirmed sdd-init");
|
|
1855
|
-
} finally {
|
|
1856
|
-
await rm(sddCwd, { recursive: true, force: true });
|
|
1857
|
-
}
|
|
1858
|
-
|
|
1859
|
-
const invalidSddInitCwd = await tempWorkspace();
|
|
1860
|
-
try {
|
|
1861
|
-
await mkdir(join(invalidSddInitCwd, ".pi", "agents"), { recursive: true });
|
|
1862
|
-
await writeFile(
|
|
1863
|
-
join(invalidSddInitCwd, ".pi", "agents", "sdd-apply.md"),
|
|
1864
|
-
`---\nname: sdd-apply\ndescription: Apply phase\nmodel: keep/provider-model\n---\n\nbody\n`,
|
|
1865
|
-
);
|
|
1866
|
-
await writeFile(globalModelsPath, "{ invalid json");
|
|
1867
|
-
const ctx = createCtx(invalidSddInitCwd, true, "invalid-sdd-init-session");
|
|
1868
|
-
await commands.get("gentle-sdd-init").handler("", ctx);
|
|
1869
|
-
assert.equal(ctx.ui.notifications[1].level, "warning");
|
|
1870
|
-
assert.match(ctx.ui.notifications[1].message, /Model routing skipped:/);
|
|
1871
|
-
assert.match(ctx.ui.notifications[1].message, /models\.json/);
|
|
1872
|
-
assert.match(ctx.ui.notifications.at(-1).message, /Wrote openspec\/config\.yaml/);
|
|
1873
|
-
const preservedAgent = await readFile(
|
|
1874
|
-
join(invalidSddInitCwd, ".pi", "agents", "sdd-apply.md"),
|
|
1875
|
-
"utf8",
|
|
1876
|
-
);
|
|
1877
|
-
assert.match(preservedAgent, /model: keep\/provider-model/);
|
|
1878
|
-
} finally {
|
|
1879
|
-
await rm(invalidSddInitCwd, { recursive: true, force: true });
|
|
1880
|
-
await rm(globalModelsPath, { force: true });
|
|
1881
|
-
}
|
|
1882
|
-
|
|
1883
1236
|
const legacyModelsCwd = await tempWorkspace();
|
|
1884
1237
|
try {
|
|
1885
1238
|
await mkdir(join(legacyModelsCwd, ".pi", "agents"), { recursive: true });
|
|
@@ -2240,7 +1593,7 @@ async function run() {
|
|
|
2240
1593
|
{ provider: "evil\u001b]52;c;Zm9v\u0007", id: "model" },
|
|
2241
1594
|
];
|
|
2242
1595
|
ctx.ui.custom = (factory) => {
|
|
2243
|
-
const panel = factory(
|
|
1596
|
+
const panel = factory({ terminal: { rows: 24 } }, null, null, () => undefined);
|
|
2244
1597
|
const initialLines = panel.render(120);
|
|
2245
1598
|
const plainInitialLines = initialLines.map(stripAnsi);
|
|
2246
1599
|
assert.ok(
|
|
@@ -2248,8 +1601,8 @@ async function run() {
|
|
|
2248
1601
|
"model panel should render inside a bordered card",
|
|
2249
1602
|
);
|
|
2250
1603
|
assert.ok(
|
|
2251
|
-
initialLines.length
|
|
2252
|
-
"
|
|
1604
|
+
initialLines.length === 24,
|
|
1605
|
+
"model panel should fill the 24-row terminal like /gentle:profiles",
|
|
2253
1606
|
);
|
|
2254
1607
|
assert.ok(
|
|
2255
1608
|
plainInitialLines.some((line) => /↓ \d+ more agent\(s\)/.test(line)),
|
|
@@ -2273,8 +1626,8 @@ async function run() {
|
|
|
2273
1626
|
const scrolledLines = panel.render(120);
|
|
2274
1627
|
const plainScrolledLines = scrolledLines.map(stripAnsi);
|
|
2275
1628
|
assert.ok(
|
|
2276
|
-
scrolledLines.length
|
|
2277
|
-
"scrolled model agent list should
|
|
1629
|
+
scrolledLines.length === 24,
|
|
1630
|
+
"scrolled model agent list should keep filling the terminal",
|
|
2278
1631
|
);
|
|
2279
1632
|
assert.ok(
|
|
2280
1633
|
plainScrolledLines.some((line) => /↑ \d+ more agent\(s\)/.test(line)),
|
|
@@ -2284,8 +1637,8 @@ async function run() {
|
|
|
2284
1637
|
const bottomLines = panel.render(120);
|
|
2285
1638
|
const plainBottomLines = bottomLines.map(stripAnsi);
|
|
2286
1639
|
assert.ok(
|
|
2287
|
-
bottomLines.length
|
|
2288
|
-
"bottom model agent list should
|
|
1640
|
+
bottomLines.length === 24,
|
|
1641
|
+
"bottom model agent list should keep filling the terminal",
|
|
2289
1642
|
);
|
|
2290
1643
|
assert.ok(
|
|
2291
1644
|
plainBottomLines.some((line) => line.includes("▸ ← Back")),
|
|
@@ -2532,4 +1885,10 @@ async function run() {
|
|
|
2532
1885
|
run().catch((error) => {
|
|
2533
1886
|
console.error(error);
|
|
2534
1887
|
process.exitCode = 1;
|
|
1888
|
+
}).finally(async () => {
|
|
1889
|
+
for (const [key, value] of previousFixtureEnvironment) {
|
|
1890
|
+
if (value === undefined) delete process.env[key];
|
|
1891
|
+
else process.env[key] = value;
|
|
1892
|
+
}
|
|
1893
|
+
for (const root of ownedFixtureRoots) await rm(root, { recursive: true, force: true });
|
|
2535
1894
|
});
|