gentle-pi 2.4.0 → 2.6.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 +292 -25
- package/assets/agents/gentle-ai-worker.md +13 -0
- package/assets/agents/jd-fix-agent.md +18 -0
- package/assets/agents/jd-judge-a.md +1 -1
- package/assets/agents/jd-judge-b.md +1 -1
- package/assets/agents/sdd-apply.md +7 -5
- package/assets/agents/sdd-archive.md +5 -3
- package/assets/agents/sdd-design.md +4 -0
- package/assets/agents/sdd-explore.md +4 -0
- package/assets/agents/sdd-init.md +4 -0
- package/assets/agents/sdd-onboard.md +4 -0
- package/assets/agents/sdd-proposal.md +4 -0
- package/assets/agents/sdd-remediate.md +37 -0
- package/assets/agents/sdd-research.md +26 -3
- package/assets/agents/sdd-spec.md +4 -0
- package/assets/agents/sdd-status.md +9 -75
- package/assets/agents/sdd-sync.md +4 -0
- package/assets/agents/sdd-tasks.md +4 -0
- package/assets/agents/sdd-verify.md +5 -3
- package/assets/chains/sdd-full.chain.md +4 -0
- package/assets/chains/sdd-plan.chain.md +4 -0
- package/assets/chains/sdd-verify.chain.md +4 -0
- package/assets/migrations/managed-assets-v2.5.0.json +7 -0
- package/assets/orchestrator-delegation.md +39 -11
- package/assets/orchestrator.md +5 -5
- package/assets/sdd-orchestrator-workflow.md +54 -21
- package/assets/support/sdd-status-contract.md +34 -90
- package/contracts/telemetry/runtime-aggregate-v1.schema.json +65 -0
- package/docs/delegated-verification.md +25 -0
- package/docs/telemetry.md +94 -0
- package/docs/windows-startup-console-visibility.md +18 -0
- package/extensions/ask-user-choice.ts +159 -25
- package/extensions/codegraph-tools.ts +95 -5
- package/extensions/gentle-agents.ts +1337 -0
- package/extensions/gentle-ai.ts +2916 -386
- package/extensions/gentle-shell.ts +650 -0
- package/extensions/gentle-todo.ts +234 -0
- package/extensions/quiet-tools.ts +2 -1
- package/extensions/runtime-metrics.ts +130 -0
- package/extensions/sdd-init.ts +2 -2
- package/extensions/startup-banner.ts +52 -75
- package/lib/agent-profiles.ts +550 -0
- package/lib/agents-completion-delivery.ts +72 -0
- package/lib/agents-config.ts +315 -0
- package/lib/agents-history.ts +88 -0
- package/lib/agents-messaging.ts +187 -0
- package/lib/agents-protocol.ts +501 -0
- package/lib/agents-runner.ts +1012 -0
- package/lib/agents-thread-view.ts +57 -0
- package/lib/agents-transcript.ts +87 -0
- package/lib/agents-view-layout.ts +40 -0
- package/lib/agents-view.ts +914 -0
- package/lib/agents-widget.ts +241 -0
- package/lib/gentle-ai-binary.ts +3 -1
- package/lib/gentle-ai-renderer.ts +143 -25
- package/lib/native-choice-list.ts +194 -0
- package/lib/native-fullscreen-interaction.ts +47 -0
- package/lib/native-pointer-region.ts +164 -0
- package/lib/native-review-cli.ts +371 -13
- package/lib/orchestrator-presence.ts +337 -0
- package/lib/profiles-orchestrator.ts +203 -0
- package/lib/review-candidate-view-owner.ts +427 -0
- package/lib/review-candidate-view.ts +150 -48
- package/lib/review-consent-component.ts +247 -0
- package/lib/review-consent-ui.ts +110 -0
- package/lib/review-host-relay.ts +28 -0
- package/lib/review-integration-v2.ts +243 -11
- package/lib/review-last-event-controller.ts +8 -4
- package/lib/review-relay-contract.ts +11 -0
- package/lib/review-reminder-receipt.ts +74 -0
- package/lib/review-repository.ts +2 -2
- package/lib/review-risk-assessment.ts +339 -0
- package/lib/review-session-standing-permission-ipc.ts +309 -0
- package/lib/review-session-standing-permission.ts +240 -0
- package/lib/runtime-metrics-children.ts +199 -0
- package/lib/runtime-metrics-delivery.ts +68 -0
- package/lib/runtime-metrics-native.ts +166 -0
- package/lib/runtime-metrics-pi-identity.ts +113 -0
- package/lib/runtime-metrics-policy.ts +51 -0
- package/lib/runtime-metrics.ts +255 -0
- package/lib/sdd-preflight.ts +362 -81
- package/lib/sdd-research-capabilities.ts +228 -0
- package/lib/sdd-status.ts +29 -7
- package/lib/session-worktree-registry.ts +118 -0
- package/lib/shell-bar.ts +184 -0
- package/lib/shell-card.ts +133 -0
- package/lib/shell-changes-view.ts +530 -0
- package/lib/shell-changes.ts +290 -0
- package/lib/shell-gauge.ts +40 -0
- package/lib/shell-prompt.ts +115 -0
- package/lib/shell-sidebar-banner.ts +11 -0
- package/lib/shell-sidebar-layout.ts +213 -0
- package/lib/shell-sidebar.ts +41 -0
- package/lib/shell-todo.ts +297 -0
- package/lib/shell-usage-view.ts +76 -0
- package/lib/shell-usage.ts +246 -0
- package/lib/telemetry-trigger.ts +153 -0
- package/package.json +8 -5
- package/runtime/gentle-ai-binary.mjs +3 -1
- package/runtime/native-review-cli.mjs +370 -12
- package/runtime/review-integration-v2.mjs +243 -11
- package/runtime/review-relay-contract.mjs +11 -0
- package/runtime/review-risk-assessment.mjs +340 -0
- package/runtime/telemetry-trigger.mjs +154 -0
- package/scripts/build-runtime-modules.mjs +11 -1
- package/scripts/check-types.mjs +125 -0
- package/scripts/gentle-ai-installer.mjs +10 -10
- package/scripts/install-gentle-ai.mjs +12 -0
- package/scripts/install-tui-mode-setting.mjs +114 -0
- package/scripts/test-packed-runner.mjs +38 -2
- package/scripts/types-baseline.json +99 -0
- package/scripts/verify-package-files.mjs +8 -2
- package/skills/_shared/review-ledger-contract.md +20 -2
- package/skills/issue-creation/SKILL.md +3 -3
- package/skills/judgment-day/SKILL.md +17 -3
- package/skills/judgment-day/references/prompts-and-formats.md +14 -3
- package/tests/agent-profiles.test.ts +722 -0
- package/tests/agents-completion-delivery.test.ts +94 -0
- package/tests/agents-config.test.ts +205 -0
- package/tests/agents-fake-child.ts +66 -0
- package/tests/agents-grouping.test.ts +179 -0
- package/tests/agents-history.test.ts +54 -0
- package/tests/agents-integration.test.ts +100 -0
- package/tests/agents-messaging.test.ts +94 -0
- package/tests/agents-protocol.test.ts +198 -0
- package/tests/agents-queries.test.ts +190 -0
- package/tests/agents-responsive.test.ts +43 -0
- package/tests/agents-runner-process.test.ts +111 -0
- package/tests/agents-runner.test.ts +959 -0
- package/tests/agents-thread-view.test.ts +45 -0
- package/tests/agents-transcript.test.ts +30 -0
- package/tests/agents-view.test.ts +685 -0
- package/tests/agents-widget.test.ts +141 -0
- package/tests/artifact-language.test.ts +25 -2
- package/tests/ask-user-choice.test.ts +325 -5
- package/tests/asset-installation-runtime.test.ts +108 -0
- package/tests/autonomous-guard.test.ts +116 -1
- package/tests/codegraph-tools.test.ts +112 -2
- package/tests/delegated-key-learnings-contract.test.ts +1 -1
- package/tests/devbinary/native-review-parity.devtest.ts +110 -0
- package/tests/feature-request-form.test.ts +67 -0
- package/tests/fixtures/agents-messaging-child.mjs +5 -0
- package/tests/fixtures/agents-process-child.mjs +23 -0
- package/tests/fixtures/runtime-metrics-native-batches.json +6 -0
- package/tests/gentle-agents.test.ts +2168 -0
- package/tests/gentle-ai-binary.test.ts +7 -2
- package/tests/gentle-ai-installer.test.ts +47 -47
- package/tests/gentle-ai-renderer.test.ts +103 -0
- package/tests/gentle-ai.test.ts +971 -15
- package/tests/gentle-card-text.ts +35 -0
- package/tests/gentle-shell.test.ts +818 -0
- package/tests/gentle-todo.test.ts +226 -0
- package/tests/install-tui-mode-setting.test.ts +324 -0
- package/tests/issue-creation-skill.test.ts +22 -0
- package/tests/model-routing-authority.test.ts +12 -0
- package/tests/native-choice-list.test.ts +202 -0
- package/tests/native-fullscreen-interaction.test.ts +125 -0
- package/tests/native-pointer-region.test.ts +245 -0
- package/tests/native-review-capability-contract.test.ts +27 -1
- package/tests/native-review-cli.test.ts +317 -3
- package/tests/native-review-consent.test.ts +91 -0
- package/tests/native-review-parity-runtime.test.ts +8 -2
- package/tests/native-review-parity.test.ts +43 -29
- package/tests/native-sdd-attempt-authority.test.ts +7 -2
- package/tests/orchestrator-budget.test.ts +69 -0
- package/tests/orchestrator-presence.test.ts +389 -0
- package/tests/orchestrator-rdd-ownership.test.ts +9 -0
- package/tests/package-manifest.test.ts +243 -7
- package/tests/profiles-orchestrator.test.ts +208 -0
- package/tests/quiet-tool-rendering.test.ts +97 -37
- package/tests/rdd-aware-verification-contract.test.ts +226 -0
- package/tests/rdd-status-line.test.ts +286 -0
- package/tests/review-agent-end-preflight.test.ts +332 -24
- package/tests/review-candidate-view.test.ts +751 -7
- package/tests/review-consent-ui.test.ts +352 -0
- package/tests/review-contract-prompt.test.ts +17 -0
- package/tests/review-controller-native-recovery.test.ts +29 -4
- package/tests/review-controller-native-routing.test.ts +884 -7
- package/tests/review-controller-workspace-root.test.ts +45 -2
- package/tests/review-controller.test.ts +26 -1
- package/tests/review-host-relay-restart-parity.test.ts +142 -1
- package/tests/review-host-relay-routing.test.ts +384 -8
- package/tests/review-host-relay.test.ts +29 -0
- package/tests/review-integration-v2-forward.test.ts +44 -0
- package/tests/review-integration-v2.test.ts +276 -0
- package/tests/review-last-event-closure.test.ts +112 -3
- package/tests/review-ledger-contract.test.ts +61 -6
- package/tests/review-relay-contract.test.ts +26 -0
- package/tests/review-reminder-receipt.test.ts +62 -0
- package/tests/review-repository.test.ts +28 -1
- package/tests/review-risk-assessment.test.ts +626 -0
- package/tests/review-session-standing-permission-controller.test.ts +656 -0
- package/tests/review-session-standing-permission-ipc.test.ts +233 -0
- package/tests/review-session-standing-permission-runtime.test.ts +212 -0
- package/tests/review-session-standing-permission.test.ts +156 -0
- package/tests/runtime-harness.mjs +447 -39
- package/tests/runtime-metrics-children.test.ts +206 -0
- package/tests/runtime-metrics-delivery.test.ts +85 -0
- package/tests/runtime-metrics-extension.test.ts +187 -0
- package/tests/runtime-metrics-native.test.ts +209 -0
- package/tests/runtime-metrics-pi-identity.test.ts +113 -0
- package/tests/runtime-metrics-policy.test.ts +62 -0
- package/tests/runtime-metrics.test.ts +184 -0
- package/tests/sdd-agent-tools.test.ts +10 -1
- package/tests/sdd-execution-routing-contract.test.ts +28 -0
- package/tests/sdd-managed-runtime-settlement.test.ts +331 -0
- package/tests/sdd-native-managed-uptake.test.ts +253 -0
- package/tests/sdd-planning-routing-contract.test.ts +45 -0
- package/tests/sdd-preflight.test.ts +252 -8
- package/tests/sdd-research-capabilities.test.ts +256 -0
- package/tests/sdd-research-live.test.ts +241 -0
- package/tests/sdd-selection-transport.test.ts +504 -0
- package/tests/sdd-status.test.ts +51 -0
- package/tests/session-worktree-registry.test.ts +135 -0
- package/tests/shell-bar.test.ts +176 -0
- package/tests/shell-card.test.ts +139 -0
- package/tests/shell-changes-view.test.ts +609 -0
- package/tests/shell-changes.test.ts +350 -0
- package/tests/shell-prompt.test.ts +140 -0
- package/tests/shell-sidebar-banner.test.ts +23 -0
- package/tests/shell-sidebar-layout.test.ts +387 -0
- package/tests/shell-sidebar.test.ts +50 -0
- package/tests/shell-todo.test.ts +259 -0
- package/tests/shell-usage-view.test.ts +62 -0
- package/tests/shell-usage.test.ts +197 -0
- package/tests/startup-banner.test.ts +126 -0
- package/tests/telemetry-trigger.test.ts +351 -0
|
@@ -11,8 +11,10 @@ import { matchesKey } from "@earendil-works/pi-tui";
|
|
|
11
11
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
12
12
|
import { stripAnsi } from "../lib/terminal-theme.ts";
|
|
13
13
|
import { domainHashV1 } from "../lib/review-canonical.ts";
|
|
14
|
+
import { canonicalHash } from "../lib/review-transaction.ts";
|
|
14
15
|
|
|
15
16
|
const ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
17
|
+
const { createGentleAiExtension } = await import(pathToFileURL(join(ROOT, "extensions/gentle-ai.ts")).href);
|
|
16
18
|
const EXTENSIONS = [
|
|
17
19
|
"extensions/gentle-ai.ts",
|
|
18
20
|
"extensions/quiet-tools.ts",
|
|
@@ -29,6 +31,8 @@ const EXPECTED_BANNER_COMMANDS = [
|
|
|
29
31
|
];
|
|
30
32
|
|
|
31
33
|
const EXPECTED_COMMANDS = [
|
|
34
|
+
"gentle:install-delegation",
|
|
35
|
+
"gentle:install-review",
|
|
32
36
|
"gentle:install-sdd",
|
|
33
37
|
"gentle:sdd-preflight",
|
|
34
38
|
"gentle-sdd-status",
|
|
@@ -43,6 +47,7 @@ const EXPECTED_COMMANDS = [
|
|
|
43
47
|
];
|
|
44
48
|
|
|
45
49
|
const FORBIDDEN_COMPAT_COMMANDS = [
|
|
50
|
+
"gentle:install-assets",
|
|
46
51
|
// The SDD entry points carry the gentle- prefix so they read identically in
|
|
47
52
|
// Claude Code and Pi. The bare names are retired without an alias.
|
|
48
53
|
"sdd-init",
|
|
@@ -308,7 +313,6 @@ async function run() {
|
|
|
308
313
|
},
|
|
309
314
|
};
|
|
310
315
|
const lastEventPi = createPi();
|
|
311
|
-
const { createGentleAiExtension } = await import(pathToFileURL(join(ROOT, "extensions/gentle-ai.ts")).href);
|
|
312
316
|
createGentleAiExtension({ nativeReviewCli })(lastEventPi.pi);
|
|
313
317
|
const controller = lastEventPi.tools.get("gentle_review");
|
|
314
318
|
const capture = lastEventPi.tools.get("gentle_review_capture");
|
|
@@ -489,11 +493,19 @@ async function run() {
|
|
|
489
493
|
assert.match(applyPromptResult.systemPrompt, /### apply instructions/);
|
|
490
494
|
const statusCtx = createCtx(promptCwd, true);
|
|
491
495
|
await commands.get("gentle-sdd-status").handler("status-demo --json", statusCtx);
|
|
492
|
-
assert.match(statusCtx.ui.notifications.at(-1).message, /"schemaName": "gentle-
|
|
496
|
+
assert.match(statusCtx.ui.notifications.at(-1).message, /"schemaName": "gentle-ai\.sdd-status"/);
|
|
493
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
|
+
};
|
|
494
505
|
await commands.get("gentle-sdd-continue").handler("status-demo", continueCtx);
|
|
495
|
-
assert.
|
|
496
|
-
assert.match(continueCtx.ui.notifications.at(-1).message, /
|
|
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"/);
|
|
497
509
|
const { execFileSync } = await import("node:child_process");
|
|
498
510
|
execFileSync("git", ["init"], { cwd: promptCwd, stdio: "ignore" });
|
|
499
511
|
const recoveryRequiredDirectory = join(promptCwd, ".git", "gentle-ai", "reviews", "control", "recovery-required-v1");
|
|
@@ -505,7 +517,7 @@ async function run() {
|
|
|
505
517
|
const blockedContinueCtx = createCtx(promptCwd, true);
|
|
506
518
|
await commands.get("gentle-sdd-continue").handler("status-demo", blockedContinueCtx);
|
|
507
519
|
assert.doesNotMatch(blockedContinueCtx.ui.notifications.at(-1).message, /resolve-review:/);
|
|
508
|
-
assert.match(blockedContinueCtx.ui.notifications.at(-1).message, /
|
|
520
|
+
assert.match(blockedContinueCtx.ui.notifications.at(-1).message, /"nextRecommended": "apply"/);
|
|
509
521
|
} finally {
|
|
510
522
|
await rm(promptCwd, { recursive: true, force: true });
|
|
511
523
|
}
|
|
@@ -583,6 +595,124 @@ async function run() {
|
|
|
583
595
|
undefined,
|
|
584
596
|
"a writer may dispatch when context carries narrow task-scoped repository-relative paths",
|
|
585
597
|
);
|
|
598
|
+
|
|
599
|
+
const interactiveSddDispatch = {
|
|
600
|
+
agent: "sdd-remediate",
|
|
601
|
+
task: "Correct the bound failed verification evidence.",
|
|
602
|
+
context: "Retain the parent-supplied remediation scope.",
|
|
603
|
+
mode: "task",
|
|
604
|
+
};
|
|
605
|
+
assert.equal(
|
|
606
|
+
await toolHook({ toolName: "subagent_run", input: interactiveSddDispatch }, createCtx(toolCwd, true, "interactive-sdd-parent")),
|
|
607
|
+
undefined,
|
|
608
|
+
"an interactive parent may dispatch an SDD child only after preflight resolves",
|
|
609
|
+
);
|
|
610
|
+
assert.match(
|
|
611
|
+
interactiveSddDispatch.context,
|
|
612
|
+
/^## SDD Session Preflight\nThese SDD preferences are explicit current-session choices\./,
|
|
613
|
+
"the parent-confirmed rendered preflight must be transported through the RPC child's context",
|
|
614
|
+
);
|
|
615
|
+
const rpcChildCwd = await tempWorkspace();
|
|
616
|
+
try {
|
|
617
|
+
const rpcChild = createPi();
|
|
618
|
+
createGentleAiExtension({ processEnv: { GENTLE_PI_AGENTS_CHILD: "1" } })(rpcChild.pi);
|
|
619
|
+
const rpcChildCtx = createCtx(rpcChildCwd, false, "delegated-rpc-sdd-child");
|
|
620
|
+
rpcChildCtx.mode = "rpc";
|
|
621
|
+
const rpcChildPrompt = await rpcChild.hooks.get("before_agent_start")[0](
|
|
622
|
+
{ agentName: "sdd-remediate", systemPrompt: "You are the SDD remediate executor for Gentle AI." },
|
|
623
|
+
rpcChildCtx,
|
|
624
|
+
);
|
|
625
|
+
assert.doesNotMatch(rpcChildPrompt.systemPrompt, /## SDD Session Preflight/);
|
|
626
|
+
assert.equal(
|
|
627
|
+
existsSync(join(rpcChildCwd, ".pi", "gentle-ai", "sdd-preflight.json")),
|
|
628
|
+
false,
|
|
629
|
+
"a delegated RPC child must not silently originate or persist defaults",
|
|
630
|
+
);
|
|
631
|
+
} finally {
|
|
632
|
+
await rm(rpcChildCwd, { recursive: true, force: true });
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
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."}';
|
|
636
|
+
const canonicalFrozenFindingRows = [JSON.parse(canonicalFrozenFindingRow)];
|
|
637
|
+
const canonicalFrozenLedgerHash = canonicalHash(canonicalFrozenFindingRows);
|
|
638
|
+
const canonicalJdFixTask = [
|
|
639
|
+
"Apply the controller-authorized fix.",
|
|
640
|
+
"",
|
|
641
|
+
"## Judgment Day activation",
|
|
642
|
+
"User explicitly requested Judgment Day.",
|
|
643
|
+
"",
|
|
644
|
+
"## Exact authorized severe IDs",
|
|
645
|
+
"- `JD-A-001`",
|
|
646
|
+
"",
|
|
647
|
+
"## Judgment Day correction batch",
|
|
648
|
+
"Round: 1 of 2.",
|
|
649
|
+
`Frozen ledger SHA-256: \`${canonicalFrozenLedgerHash}\``,
|
|
650
|
+
"",
|
|
651
|
+
"## Exact frozen finding rows",
|
|
652
|
+
canonicalFrozenFindingRow,
|
|
653
|
+
"",
|
|
654
|
+
"## Allowed edit surfaces",
|
|
655
|
+
"extensions/gentle-ai.ts",
|
|
656
|
+
"tests/runtime-harness.mjs",
|
|
657
|
+
].join("\n");
|
|
658
|
+
const orderedFrozenFindingRows = [
|
|
659
|
+
...canonicalFrozenFindingRows,
|
|
660
|
+
{ ...canonicalFrozenFindingRows[0], id: "JD-B-002", location: "tests/runtime-harness.mjs:1" },
|
|
661
|
+
];
|
|
662
|
+
const reversedFrozenFindingRows = [...orderedFrozenFindingRows].reverse();
|
|
663
|
+
const rowOrderMismatchJdFixTask = canonicalJdFixTask
|
|
664
|
+
.replace("- `JD-A-001`", "- `JD-A-001`\n- `JD-B-002`")
|
|
665
|
+
.replace(canonicalFrozenLedgerHash, canonicalHash(reversedFrozenFindingRows))
|
|
666
|
+
.replace(canonicalFrozenFindingRow, reversedFrozenFindingRows.map((row) => JSON.stringify(row)).join("\n"));
|
|
667
|
+
const incorrectFrozenLedgerHash = `${canonicalFrozenLedgerHash.slice(0, -1)}${canonicalFrozenLedgerHash.endsWith("0") ? "1" : "0"}`;
|
|
668
|
+
for (const [label, input] of [
|
|
669
|
+
["missing activation", { agent: "jd-fix-agent", task: "## Allowed edit surfaces\nextensions/gentle-ai.ts", mode: "task" }],
|
|
670
|
+
["duplicate activation", { agent: "jd-fix-agent", task: `${canonicalJdFixTask}\n\n## Judgment Day activation\nUser explicitly requested Judgment Day.`, mode: "task" }],
|
|
671
|
+
["missing severe IDs", { agent: "jd-fix-agent", task: canonicalJdFixTask.replace("## Exact authorized severe IDs\n- `JD-A-001`\n", ""), mode: "task" }],
|
|
672
|
+
["duplicate severe ID bindings", { agent: "jd-fix-agent", task: canonicalJdFixTask.replace("- `JD-A-001`", "- `JD-A-001`\n- `JD-A-001`"), mode: "task" }],
|
|
673
|
+
["duplicate severe ID sections", { agent: "jd-fix-agent", task: canonicalJdFixTask, context: "## Exact authorized severe IDs\n- `JD-B-002`", mode: "task" }],
|
|
674
|
+
["malformed activation", { agent: "jd-fix-agent", task: canonicalJdFixTask.replace("User explicitly requested Judgment Day.", "Judgment Day is explicitly activated for this dispatch."), mode: "task" }],
|
|
675
|
+
["extra activation line", { agent: "jd-fix-agent", task: canonicalJdFixTask.replace("User explicitly requested Judgment Day.", "User explicitly requested Judgment Day.\nUnexpected activation detail."), mode: "task" }],
|
|
676
|
+
["malformed severe ID", { agent: "jd-fix-agent", task: canonicalJdFixTask.replace("- `JD-A-001`", "JD-A-001"), mode: "task" }],
|
|
677
|
+
["empty severe IDs", { agent: "jd-fix-agent", task: canonicalJdFixTask.replace("- `JD-A-001`", ""), mode: "task" }],
|
|
678
|
+
["missing correction batch", { agent: "jd-fix-agent", task: canonicalJdFixTask.replace(`## Judgment Day correction batch\nRound: 1 of 2.\nFrozen ledger SHA-256: \`${canonicalFrozenLedgerHash}\`\n`, ""), mode: "task" }],
|
|
679
|
+
["duplicate correction batch", { agent: "jd-fix-agent", task: `${canonicalJdFixTask}\n## Judgment Day correction batch\nRound: 2 of 2.\nFrozen ledger SHA-256: \`bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\``, mode: "task" }],
|
|
680
|
+
["out-of-order correction batch", { agent: "jd-fix-agent", task: canonicalJdFixTask.replace(`## Judgment Day correction batch\nRound: 1 of 2.\nFrozen ledger SHA-256: \`${canonicalFrozenLedgerHash}\`\n\n## Exact frozen finding rows\n${canonicalFrozenFindingRow}`, `## Exact frozen finding rows\n${canonicalFrozenFindingRow}\n\n## Judgment Day correction batch\nRound: 1 of 2.\nFrozen ledger SHA-256: \`${canonicalFrozenLedgerHash}\``), mode: "task" }],
|
|
681
|
+
["invalid correction round", { agent: "jd-fix-agent", task: canonicalJdFixTask.replace("Round: 1 of 2.", "Round: 3 of 2."), mode: "task" }],
|
|
682
|
+
["uppercase frozen ledger hash", { agent: "jd-fix-agent", task: canonicalJdFixTask.replace(canonicalFrozenLedgerHash, canonicalFrozenLedgerHash.toUpperCase()), mode: "task" }],
|
|
683
|
+
["incorrect frozen ledger hash", { agent: "jd-fix-agent", task: canonicalJdFixTask.replace(canonicalFrozenLedgerHash, incorrectFrozenLedgerHash), mode: "task" }],
|
|
684
|
+
["extra correction batch line", { agent: "jd-fix-agent", task: canonicalJdFixTask.replace("Round: 1 of 2.", "Round: 1 of 2.\nUnexpected line"), mode: "task" }],
|
|
685
|
+
["missing frozen rows", { agent: "jd-fix-agent", task: canonicalJdFixTask.replace(`## Exact frozen finding rows\n${canonicalFrozenFindingRow}\n`, ""), mode: "task" }],
|
|
686
|
+
["malformed frozen row", { agent: "jd-fix-agent", task: canonicalJdFixTask.replace(canonicalFrozenFindingRow, "{"), mode: "task" }],
|
|
687
|
+
["duplicate frozen row", { agent: "jd-fix-agent", task: canonicalJdFixTask.replace(canonicalFrozenFindingRow, `${canonicalFrozenFindingRow}\n${canonicalFrozenFindingRow}`), mode: "task" }],
|
|
688
|
+
["mismatched frozen row ID", { agent: "jd-fix-agent", task: canonicalJdFixTask.replace(canonicalFrozenFindingRow, canonicalFrozenFindingRow.replace("JD-A-001", "JD-B-002")), mode: "task" }],
|
|
689
|
+
["frozen row order mismatch", { agent: "jd-fix-agent", task: rowOrderMismatchJdFixTask, mode: "task" }],
|
|
690
|
+
["extra frozen row field", { agent: "jd-fix-agent", task: canonicalJdFixTask.replace(canonicalFrozenFindingRow, canonicalFrozenFindingRow.replace("}", ",\"extra\":true}")), mode: "task" }],
|
|
691
|
+
["non-Judgment-Day frozen row", { agent: "jd-fix-agent", task: canonicalJdFixTask.replace("\"lens\":\"judgment-day\"", "\"lens\":\"review-risk\""), mode: "task" }],
|
|
692
|
+
["non-open frozen row", { agent: "jd-fix-agent", task: canonicalJdFixTask.replace("\"status_at_freeze\":\"open\"", "\"status_at_freeze\":\"closed\""), mode: "task" }],
|
|
693
|
+
["non-severe frozen row", { agent: "jd-fix-agent", task: canonicalJdFixTask.replace("\"severity\":\"CRITICAL\"", "\"severity\":\"WARNING\""), mode: "task" }],
|
|
694
|
+
["empty frozen evidence", { agent: "jd-fix-agent", task: canonicalJdFixTask.replace("\"evidence_claim\":\"The frozen finding has concrete user impact.\"", "\"evidence_claim\":\"\""), mode: "task" }],
|
|
695
|
+
["missing edit surface", { agent: "jd-fix-agent", task: canonicalJdFixTask.replace("## Allowed edit surfaces\nextensions/gentle-ai.ts\ntests/runtime-harness.mjs", ""), mode: "task" }],
|
|
696
|
+
["invalid edit surface", { agent: "jd-fix-agent", task: canonicalJdFixTask.replace("## Allowed edit surfaces\nextensions/gentle-ai.ts", "## Allowed edit surfaces\n."), mode: "task" }],
|
|
697
|
+
["mixed", { agent: ["jd-fix-agent", "gentle-ai-worker"], task: canonicalJdFixTask, mode: "task" }],
|
|
698
|
+
["agent array", { agent: ["jd-fix-agent"], task: canonicalJdFixTask, mode: "task" }],
|
|
699
|
+
["agents array", { agents: ["jd-fix-agent"], task: canonicalJdFixTask, mode: "task" }],
|
|
700
|
+
["duplicate agent binding", { agent: "jd-fix-agent", agents: "jd-fix-agent", task: canonicalJdFixTask, mode: "task" }],
|
|
701
|
+
]) {
|
|
702
|
+
const original = structuredClone(input);
|
|
703
|
+
const result = await toolHook({ toolName: "subagent_run", input }, createCtx(toolCwd));
|
|
704
|
+
assert.equal(result?.block, true, `${label} jd-fix-agent dispatch must be blocked`);
|
|
705
|
+
assert.match(result?.reason ?? "", /Judgment Day fix dispatch/i);
|
|
706
|
+
assert.deepEqual(input, original, `${label} jd-fix-agent rejection must not mutate input`);
|
|
707
|
+
}
|
|
708
|
+
assert.equal(
|
|
709
|
+
await toolHook(
|
|
710
|
+
{ toolName: "subagent_run", input: { agent: "jd-fix-agent", task: canonicalJdFixTask, mode: "task" } },
|
|
711
|
+
createCtx(toolCwd),
|
|
712
|
+
),
|
|
713
|
+
undefined,
|
|
714
|
+
"a canonical explicit Judgment Day fix dispatch with narrow edit surfaces may run",
|
|
715
|
+
);
|
|
586
716
|
for (const [label, input] of [
|
|
587
717
|
[
|
|
588
718
|
"valid scope followed by a repository-root scope",
|
|
@@ -883,7 +1013,10 @@ async function run() {
|
|
|
883
1013
|
}
|
|
884
1014
|
|
|
885
1015
|
const noUiCwd = await tempWorkspace();
|
|
1016
|
+
const startupAgentHome = join(noUiCwd, "agent-home");
|
|
1017
|
+
process.env.GENTLE_PI_AGENT_HOME = startupAgentHome;
|
|
886
1018
|
try {
|
|
1019
|
+
const globalAgentHome = startupAgentHome;
|
|
887
1020
|
for (const handler of hooks.get("session_start")) {
|
|
888
1021
|
await handler({ reason: "startup" }, createCtx(noUiCwd, false));
|
|
889
1022
|
}
|
|
@@ -897,7 +1030,15 @@ async function run() {
|
|
|
897
1030
|
false,
|
|
898
1031
|
"session_start must not install project-local SDD chains",
|
|
899
1032
|
);
|
|
900
|
-
assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-apply.md")),
|
|
1033
|
+
assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-apply.md")), false);
|
|
1034
|
+
assert.equal(existsSync(join(globalAgentHome, "chains", "sdd-full.chain.md")), false);
|
|
1035
|
+
assert.equal(existsSync(join(globalAgentHome, "gentle-ai", "support")), false);
|
|
1036
|
+
for (const diagnostic of ["gentle:status", "gentle:doctor"]) {
|
|
1037
|
+
const ctx = createCtx(noUiCwd, true);
|
|
1038
|
+
await commands.get(diagnostic).handler("", ctx);
|
|
1039
|
+
assert.match(ctx.ui.notifications.at(-1).message, /Global SDD assets: on demand/);
|
|
1040
|
+
assert.doesNotMatch(ctx.ui.notifications.at(-1).message, /install-sdd --force/);
|
|
1041
|
+
}
|
|
901
1042
|
// gentle-pi#311 P5: the Pi-authored adversarial role agents are retired;
|
|
902
1043
|
// installation must not (re)create them.
|
|
903
1044
|
const installedRefuterPath = join(globalAgentHome, "agents", "review-refuter.md");
|
|
@@ -920,6 +1061,8 @@ async function run() {
|
|
|
920
1061
|
);
|
|
921
1062
|
assert.match(installedRiskSource, /exactly once against the supplied `initial_review_tree`/);
|
|
922
1063
|
assert.match(installedRiskSource, /cannot authorize transitions, fixes, receipts, gates, or delivery/);
|
|
1064
|
+
await commands.get("gentle:sdd-preflight").handler("", createCtx(noUiCwd, false, "startup-sdd-install"));
|
|
1065
|
+
assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-apply.md")), true);
|
|
923
1066
|
assert.equal(existsSync(join(globalAgentHome, "chains", "sdd-full.chain.md")), true);
|
|
924
1067
|
assert.equal(existsSync(join(globalAgentHome, "gentle-ai", "support", "sdd-status-contract.md")), true);
|
|
925
1068
|
const managedAssetsManifestPath = join(globalAgentHome, "gentle-ai", "managed-assets.json");
|
|
@@ -968,10 +1111,24 @@ async function run() {
|
|
|
968
1111
|
for (const handler of hooks.get("session_start")) {
|
|
969
1112
|
await handler({ reason: "startup" }, createCtx(noUiCwd, false));
|
|
970
1113
|
}
|
|
1114
|
+
for (const [key, stale] of [
|
|
1115
|
+
["agents/sdd-apply.md", previousManagedApply],
|
|
1116
|
+
["chains/sdd-full.chain.md", previousManagedChain],
|
|
1117
|
+
["gentle-ai/support/sdd-status-contract.md", previousManagedSupport],
|
|
1118
|
+
]) {
|
|
1119
|
+
assert.equal(await readFile(join(globalAgentHome, key), "utf8"), stale,
|
|
1120
|
+
"startup must preserve existing SDD package content without explicit routing");
|
|
1121
|
+
}
|
|
1122
|
+
assert.equal(existsSync(retiredManagedValidatorPath), false, "review retirement still runs at startup");
|
|
1123
|
+
const driftCtx = createCtx(noUiCwd, true);
|
|
1124
|
+
await commands.get("gentle:status").handler("", driftCtx);
|
|
1125
|
+
assert.match(driftCtx.ui.notifications.at(-1).message, /Global SDD assets stale: 3 file\(s\).*install-sdd --force/);
|
|
1126
|
+
assert.doesNotMatch(driftCtx.ui.notifications.at(-1).message, /install-(delegation|review) --force/);
|
|
1127
|
+
await commands.get("gentle:sdd-preflight").handler("", createCtx(noUiCwd, false, "startup-sdd-refresh"));
|
|
971
1128
|
assert.notEqual(
|
|
972
1129
|
await readFile(join(globalAgentHome, "agents", "sdd-apply.md"), "utf8"),
|
|
973
1130
|
"stale global apply\n",
|
|
974
|
-
"
|
|
1131
|
+
"SDD preflight must refresh stale global SDD agents",
|
|
975
1132
|
);
|
|
976
1133
|
assert.equal(
|
|
977
1134
|
await readFile(installedRefuterPath, "utf8"),
|
|
@@ -981,12 +1138,12 @@ async function run() {
|
|
|
981
1138
|
assert.notEqual(
|
|
982
1139
|
await readFile(join(globalAgentHome, "chains", "sdd-full.chain.md"), "utf8"),
|
|
983
1140
|
"stale global chain\n",
|
|
984
|
-
"
|
|
1141
|
+
"SDD preflight must refresh stale global SDD chains",
|
|
985
1142
|
);
|
|
986
1143
|
assert.notEqual(
|
|
987
1144
|
await readFile(join(globalAgentHome, "gentle-ai", "support", "sdd-status-contract.md"), "utf8"),
|
|
988
1145
|
"stale global status contract\n",
|
|
989
|
-
"
|
|
1146
|
+
"SDD preflight must refresh stale global SDD support files",
|
|
990
1147
|
);
|
|
991
1148
|
assert.equal(
|
|
992
1149
|
await readFile(join(noUiCwd, ".pi", "agents", "sdd-apply.md"), "utf8"),
|
|
@@ -1022,11 +1179,16 @@ async function run() {
|
|
|
1022
1179
|
"a user-authored same-path retired asset must stay unowned",
|
|
1023
1180
|
);
|
|
1024
1181
|
} finally {
|
|
1182
|
+
process.env.GENTLE_PI_AGENT_HOME = globalAgentHome;
|
|
1025
1183
|
await rm(noUiCwd, { recursive: true, force: true });
|
|
1026
1184
|
}
|
|
1027
1185
|
|
|
1028
1186
|
const lazySddCwd = await tempWorkspace();
|
|
1187
|
+
const lazyAgentHome = join(lazySddCwd, "agent-home");
|
|
1188
|
+
process.env.GENTLE_PI_AGENT_HOME = lazyAgentHome;
|
|
1029
1189
|
try {
|
|
1190
|
+
// This scenario must not inherit SDD definitions from earlier startup tests.
|
|
1191
|
+
const globalAgentHome = lazyAgentHome;
|
|
1030
1192
|
await writeFile(
|
|
1031
1193
|
globalModelsPath,
|
|
1032
1194
|
JSON.stringify({ "sdd-apply": { model: "openai/gpt-5", thinking: "high" } }, null, 2),
|
|
@@ -1082,6 +1244,7 @@ async function run() {
|
|
|
1082
1244
|
{ action: "continue" },
|
|
1083
1245
|
);
|
|
1084
1246
|
assert.equal(existsSync(join(lazySddCwd, ".pi", "agents", "sdd-apply.md")), false);
|
|
1247
|
+
assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-apply.md")), false);
|
|
1085
1248
|
|
|
1086
1249
|
assert.deepEqual(
|
|
1087
1250
|
await inputHook({ text: "vamos con sdd", source: "interactive" }, ctx),
|
|
@@ -1094,13 +1257,13 @@ async function run() {
|
|
|
1094
1257
|
assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-sync.md")), true);
|
|
1095
1258
|
assert.equal(existsSync(join(globalAgentHome, "gentle-ai", "support", "sdd-status-contract.md")), true);
|
|
1096
1259
|
assert.equal(existsSync(join(globalAgentHome, "chains", "sdd-full.chain.md")), true);
|
|
1097
|
-
assert.equal(ctx.ui.selections.length,
|
|
1098
|
-
assert.match(ctx.ui.notifications.at(-1).message, /Preference source:
|
|
1260
|
+
assert.equal(ctx.ui.selections.length, 1, "first interactive SDD trigger confirms session suggestions");
|
|
1261
|
+
assert.match(ctx.ui.notifications.at(-1).message, /Preference source: explicit session choice/);
|
|
1099
1262
|
assert.deepEqual(
|
|
1100
1263
|
await inputHook({ text: "please use sdd for this change", source: "interactive" }, ctx),
|
|
1101
1264
|
{ action: "continue" },
|
|
1102
1265
|
);
|
|
1103
|
-
assert.equal(ctx.ui.selections.length,
|
|
1266
|
+
assert.equal(ctx.ui.selections.length, 1, "natural SDD triggers reuse confirmed session choices");
|
|
1104
1267
|
assert.deepEqual(
|
|
1105
1268
|
await inputHook({ text: "/sdd", source: "interactive" }, ctx),
|
|
1106
1269
|
{ action: "continue" },
|
|
@@ -1113,7 +1276,7 @@ async function run() {
|
|
|
1113
1276
|
await inputHook({ text: "/sdd:plan", source: "interactive" }, ctx),
|
|
1114
1277
|
{ action: "continue" },
|
|
1115
1278
|
);
|
|
1116
|
-
assert.equal(ctx.ui.selections.length,
|
|
1279
|
+
assert.equal(ctx.ui.selections.length, 1, "slash SDD triggers reuse confirmed session choices");
|
|
1117
1280
|
|
|
1118
1281
|
assert.deepEqual(
|
|
1119
1282
|
await inputHook({ text: "/sdd-plan this change", source: "interactive" }, ctx),
|
|
@@ -1141,14 +1304,14 @@ async function run() {
|
|
|
1141
1304
|
"global SDD model routing must be materialized in agent frontmatter, not project settings overrides",
|
|
1142
1305
|
);
|
|
1143
1306
|
}
|
|
1144
|
-
assert.equal(ctx.ui.selections.length,
|
|
1145
|
-
assert.match(ctx.ui.notifications.at(-1).message, /Preference source:
|
|
1307
|
+
assert.equal(ctx.ui.selections.length, 1, "automatic SDD routing reuses session confirmation");
|
|
1308
|
+
assert.match(ctx.ui.notifications.at(-1).message, /Preference source: explicit session choice/);
|
|
1146
1309
|
await commands.get("gentle:status").handler("", ctx);
|
|
1147
1310
|
assert.match(ctx.ui.notifications.at(-1).message, /Global SDD assets stale: 0 file\(s\)/);
|
|
1148
1311
|
assert.doesNotMatch(ctx.ui.notifications.at(-1).message, /install-sdd --force/);
|
|
1149
1312
|
|
|
1150
1313
|
await inputHook({ text: "/sdd-plan another change", source: "interactive" }, ctx);
|
|
1151
|
-
assert.equal(ctx.ui.selections.length,
|
|
1314
|
+
assert.equal(ctx.ui.selections.length, 1, "resolved preflight must not prompt again in this session");
|
|
1152
1315
|
const promptHook = hooks.get("before_agent_start")[0];
|
|
1153
1316
|
const promptResult = await promptHook({ systemPrompt: "base" }, ctx);
|
|
1154
1317
|
assert.match(promptResult.systemPrompt, /SDD Session Preflight/);
|
|
@@ -1163,6 +1326,7 @@ async function run() {
|
|
|
1163
1326
|
"non-SDD subagents must not receive parent harness or SDD preflight prompts",
|
|
1164
1327
|
);
|
|
1165
1328
|
} finally {
|
|
1329
|
+
process.env.GENTLE_PI_AGENT_HOME = globalAgentHome;
|
|
1166
1330
|
await rm(lazySddCwd, { recursive: true, force: true });
|
|
1167
1331
|
await rm(globalModelsPath, { force: true });
|
|
1168
1332
|
}
|
|
@@ -1177,23 +1341,63 @@ async function run() {
|
|
|
1177
1341
|
});
|
|
1178
1342
|
assert.equal(existsSync(join(slashSddCwd, ".pi", "agents", "sdd-apply.md")), false);
|
|
1179
1343
|
assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-apply.md")), true);
|
|
1180
|
-
assert.equal(ctx.ui.selections.length,
|
|
1344
|
+
assert.equal(ctx.ui.selections.length, 1, `${text} must confirm the fresh interactive session`);
|
|
1181
1345
|
} finally {
|
|
1182
1346
|
await rm(slashSddCwd, { recursive: true, force: true });
|
|
1183
1347
|
}
|
|
1184
1348
|
}
|
|
1185
1349
|
|
|
1350
|
+
const commandPreflightCwd = await tempWorkspace();
|
|
1351
|
+
try {
|
|
1352
|
+
const command = commands.get("gentle:sdd-preflight");
|
|
1353
|
+
const interactive = createCtx(commandPreflightCwd, true, "explicit-command-regression");
|
|
1354
|
+
await command.handler("", interactive);
|
|
1355
|
+
const first = interactive.ui.selections.length;
|
|
1356
|
+
await command.handler("", interactive);
|
|
1357
|
+
const reused = interactive.ui.selections.length;
|
|
1358
|
+
const select = interactive.ui.select;
|
|
1359
|
+
interactive.ui.select = async (label, options) => {
|
|
1360
|
+
const value = await select(label, options);
|
|
1361
|
+
return label === "SDD execution mode" ? "interactive" : value;
|
|
1362
|
+
};
|
|
1363
|
+
interactive.ui.input = async () => "650";
|
|
1364
|
+
await command.handler("--edit", interactive);
|
|
1365
|
+
const edited = interactive.ui.selections.length;
|
|
1366
|
+
await command.handler("", interactive);
|
|
1367
|
+
assert.equal(interactive.ui.selections.length, edited, "edited choices also reuse without prompting");
|
|
1368
|
+
const saved = JSON.parse(await readFile(join(commandPreflightCwd, ".pi", "gentle-ai", "sdd-preflight.json"), "utf8"));
|
|
1369
|
+
assert.equal(saved.executionMode, "interactive");
|
|
1370
|
+
assert.equal(saved.reviewBudgetLines, 650);
|
|
1371
|
+
const rpc = createCtx(commandPreflightCwd, true, "explicit-command-rpc-regression");
|
|
1372
|
+
rpc.mode = "rpc";
|
|
1373
|
+
let rpcInputs = 0;
|
|
1374
|
+
rpc.ui.input = async (_label, placeholder) => { rpcInputs += 1; return placeholder; };
|
|
1375
|
+
await command.handler("", rpc);
|
|
1376
|
+
await command.handler("--edit", rpc);
|
|
1377
|
+
const noUi = createCtx(commandPreflightCwd, false, "explicit-command-no-ui-regression");
|
|
1378
|
+
let noUiInputs = 0;
|
|
1379
|
+
noUi.ui.input = async (_label, placeholder) => { noUiInputs += 1; return placeholder; };
|
|
1380
|
+
await command.handler("", noUi);
|
|
1381
|
+
await command.handler("--edit", noUi);
|
|
1382
|
+
assert.deepEqual({ first, reused, edited, rpc: rpc.ui.selections.length, noUi: noUi.ui.selections.length },
|
|
1383
|
+
{ first: 1, reused: 1, edited: 3, rpc: 0, noUi: 0 },
|
|
1384
|
+
"explicit command confirms once, reuses unless --edit, and never prompts headless callers");
|
|
1385
|
+
assert.deepEqual({ rpcInputs, noUiInputs }, { rpcInputs: 0, noUiInputs: 0 });
|
|
1386
|
+
} finally {
|
|
1387
|
+
await rm(commandPreflightCwd, { recursive: true, force: true });
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1186
1390
|
const commandSddCwd = await tempWorkspace();
|
|
1187
1391
|
try {
|
|
1188
1392
|
const ctx = createCtx(commandSddCwd, true, "command-session");
|
|
1189
|
-
await commands.get("gentle:sdd-preflight").handler("", ctx);
|
|
1393
|
+
await commands.get("gentle:sdd-preflight").handler("--edit", ctx);
|
|
1190
1394
|
assert.equal(existsSync(join(commandSddCwd, ".pi", "agents", "sdd-apply.md")), false);
|
|
1191
1395
|
assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-apply.md")), true);
|
|
1192
1396
|
assert.equal(ctx.ui.selections.length, 2, "explicit preflight prompts intentional choice fields");
|
|
1193
1397
|
assert.equal(ctx.ui.selections.some(({ label }) => label === "SDD artifact store"), false, "one-option artifact store must be elided");
|
|
1194
1398
|
assert.match(ctx.ui.notifications.at(-1).message, /Preference source: explicit session choice/);
|
|
1195
|
-
await commands.get("gentle:sdd-preflight").handler("", ctx);
|
|
1196
|
-
assert.equal(ctx.ui.selections.length, 4, "
|
|
1399
|
+
await commands.get("gentle:sdd-preflight").handler("--edit", ctx);
|
|
1400
|
+
assert.equal(ctx.ui.selections.length, 4, "--edit remains an intentional re-prompt");
|
|
1197
1401
|
} finally {
|
|
1198
1402
|
await rm(commandSddCwd, { recursive: true, force: true });
|
|
1199
1403
|
}
|
|
@@ -1212,7 +1416,7 @@ async function run() {
|
|
|
1212
1416
|
assert.equal(existsSync(join(sddAgentGuardCwd, ".pi", "chains", "sdd-full.chain.md")), false);
|
|
1213
1417
|
assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-apply.md")), true);
|
|
1214
1418
|
assert.equal(existsSync(join(globalAgentHome, "chains", "sdd-full.chain.md")), true);
|
|
1215
|
-
assert.equal(ctx.ui.selections.length,
|
|
1419
|
+
assert.equal(ctx.ui.selections.length, 1, "fresh interactive SDD-agent startup requires confirmation");
|
|
1216
1420
|
assert.match(promptResult.systemPrompt, /SDD Session Preflight/);
|
|
1217
1421
|
assert.doesNotMatch(
|
|
1218
1422
|
promptResult.systemPrompt,
|
|
@@ -1233,7 +1437,7 @@ async function run() {
|
|
|
1233
1437
|
},
|
|
1234
1438
|
ctx,
|
|
1235
1439
|
);
|
|
1236
|
-
assert.equal(ctx.ui.selections.length,
|
|
1440
|
+
assert.equal(ctx.ui.selections.length, 1, "SDD-agent startup reuses confirmed session preferences");
|
|
1237
1441
|
assert.doesNotMatch(
|
|
1238
1442
|
reusedPromptResult.systemPrompt,
|
|
1239
1443
|
/el Gentleman Identity and Harness/,
|
|
@@ -1243,6 +1447,21 @@ async function run() {
|
|
|
1243
1447
|
await rm(sddAgentGuardCwd, { recursive: true, force: true });
|
|
1244
1448
|
}
|
|
1245
1449
|
|
|
1450
|
+
const unresolvedSddCwd = await tempWorkspace();
|
|
1451
|
+
try {
|
|
1452
|
+
const ctx = createCtx(unresolvedSddCwd, true, "unresolved-preflight-session");
|
|
1453
|
+
ctx.ui.select = async () => undefined;
|
|
1454
|
+
assert.deepEqual(await hooks.get("input")[0]({ text: "/sdd", source: "interactive" }, ctx), { action: "handled" });
|
|
1455
|
+
const result = await hooks.get("before_agent_start")[0]({ agentName: "sdd-explore", systemPrompt: "SDD executor" }, ctx);
|
|
1456
|
+
assert.match(result.systemPrompt, /SDD preflight unresolved/);
|
|
1457
|
+
assert.match(result.systemPrompt, /STOP: Do not initialize/);
|
|
1458
|
+
assert.doesNotMatch(result.systemPrompt, /## SDD Session Preflight/);
|
|
1459
|
+
assert.equal(existsSync(join(unresolvedSddCwd, "openspec", "config.yaml")), false);
|
|
1460
|
+
assert.equal(existsSync(join(unresolvedSddCwd, ".pi", "gentle-ai", "sdd-preflight.json")), false);
|
|
1461
|
+
} finally {
|
|
1462
|
+
await rm(unresolvedSddCwd, { recursive: true, force: true });
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1246
1465
|
const noUiSddAgentCwd = await tempWorkspace();
|
|
1247
1466
|
try {
|
|
1248
1467
|
const ctx = createCtx(noUiSddAgentCwd, false, "no-ui-sdd-agent-session");
|
|
@@ -1280,7 +1499,7 @@ async function run() {
|
|
|
1280
1499
|
try {
|
|
1281
1500
|
pi.setActiveTools(["read", "bash", "edit", "write", "mem_save"]);
|
|
1282
1501
|
const ctx = createCtx(engramSddCwd, true, "engram-session");
|
|
1283
|
-
await commands.get("gentle:sdd-preflight").handler("", ctx);
|
|
1502
|
+
await commands.get("gentle:sdd-preflight").handler("--edit", ctx);
|
|
1284
1503
|
assert.deepEqual(ctx.ui.selections[1].options, ["openspec", "engram", "hybrid"]);
|
|
1285
1504
|
} finally {
|
|
1286
1505
|
pi.setActiveTools(["read", "bash", "edit", "write"]);
|
|
@@ -1297,7 +1516,7 @@ async function run() {
|
|
|
1297
1516
|
if (label === "SDD artifact store") return "engram";
|
|
1298
1517
|
return options[0];
|
|
1299
1518
|
};
|
|
1300
|
-
await commands.get("gentle:sdd-preflight").handler("", ctx);
|
|
1519
|
+
await commands.get("gentle:sdd-preflight").handler("--edit", ctx);
|
|
1301
1520
|
await commands.get("gentle-sdd-init").handler("", ctx);
|
|
1302
1521
|
assert.equal(
|
|
1303
1522
|
existsSync(join(engramSddInitCwd, "openspec")),
|
|
@@ -1335,7 +1554,7 @@ async function run() {
|
|
|
1335
1554
|
if (label === "SDD artifact store") return "hybrid";
|
|
1336
1555
|
return options[0];
|
|
1337
1556
|
};
|
|
1338
|
-
await commands.get("gentle:sdd-preflight").handler("", ctx);
|
|
1557
|
+
await commands.get("gentle:sdd-preflight").handler("--edit", ctx);
|
|
1339
1558
|
await commands.get("gentle-sdd-init").handler("", ctx);
|
|
1340
1559
|
assert.equal(
|
|
1341
1560
|
existsSync(join(bothSddInitCwd, "openspec", "specs")),
|
|
@@ -1374,6 +1593,149 @@ async function run() {
|
|
|
1374
1593
|
await rm(directEngramToolCwd, { recursive: true, force: true });
|
|
1375
1594
|
}
|
|
1376
1595
|
|
|
1596
|
+
for (const owner of ["sdd", "delegation", "review"]) {
|
|
1597
|
+
const fixture = await tempWorkspace();
|
|
1598
|
+
const agentHome = join(fixture, "agent-home");
|
|
1599
|
+
const representatives = {
|
|
1600
|
+
sdd: "sdd-apply.md",
|
|
1601
|
+
delegation: "gentle-ai-worker.md",
|
|
1602
|
+
review: "review-risk.md",
|
|
1603
|
+
};
|
|
1604
|
+
try {
|
|
1605
|
+
process.env.GENTLE_PI_AGENT_HOME = agentHome;
|
|
1606
|
+
process.env.GENTLE_PI_CONFIG_HOME = join(fixture, "config");
|
|
1607
|
+
await mkdir(process.env.GENTLE_PI_CONFIG_HOME);
|
|
1608
|
+
await writeFile(join(process.env.GENTLE_PI_CONFIG_HOME, "models.json"),
|
|
1609
|
+
JSON.stringify({ [representatives[owner].replace(/\.md$/, "")]: "test/installer-must-not-apply" }));
|
|
1610
|
+
const ctx = createCtx(fixture, true);
|
|
1611
|
+
const command = commands.get(`gentle:install-${owner}`);
|
|
1612
|
+
assert.ok(command, `missing owner installer: ${owner}`);
|
|
1613
|
+
await command.handler("", ctx);
|
|
1614
|
+
for (const [candidate, name] of Object.entries(representatives)) {
|
|
1615
|
+
assert.equal(existsSync(join(agentHome, "agents", name)), candidate === owner,
|
|
1616
|
+
`${owner} installation must not install ${candidate} assets`);
|
|
1617
|
+
}
|
|
1618
|
+
assert.equal(existsSync(join(fixture, ".pi", "agents")), false);
|
|
1619
|
+
assert.match(ctx.ui.notifications.at(-1).message, /assets installed: \d+ agent\(s\), \d+ chain\(s\), \d+ support file\(s\)/);
|
|
1620
|
+
const selectedPath = join(agentHome, "agents", representatives[owner]);
|
|
1621
|
+
const packaged = await readFile(selectedPath, "utf8");
|
|
1622
|
+
assert.doesNotMatch(packaged, /test\/installer-must-not-apply/);
|
|
1623
|
+
const manifestPath = join(agentHome, "gentle-ai", "managed-assets.json");
|
|
1624
|
+
const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
|
|
1625
|
+
for (const [candidate, name] of Object.entries(representatives)) {
|
|
1626
|
+
const stale = `stale managed ${candidate}\n`;
|
|
1627
|
+
await writeFile(join(agentHome, "agents", name), stale);
|
|
1628
|
+
manifest.assets[`agents/${name}`] = sha256(stale);
|
|
1629
|
+
}
|
|
1630
|
+
await writeFile(manifestPath, JSON.stringify(manifest, null, 2));
|
|
1631
|
+
await command.handler("", ctx);
|
|
1632
|
+
assert.equal(await readFile(selectedPath, "utf8"), `stale managed ${owner}\n`,
|
|
1633
|
+
"repair without --force must preserve existing files");
|
|
1634
|
+
await command.handler("--force", ctx);
|
|
1635
|
+
assert.equal(await readFile(selectedPath, "utf8"), packaged);
|
|
1636
|
+
const refreshed = JSON.parse(await readFile(manifestPath, "utf8"));
|
|
1637
|
+
for (const [candidate, name] of Object.entries(representatives)) {
|
|
1638
|
+
if (candidate === owner) continue;
|
|
1639
|
+
assert.equal(await readFile(join(agentHome, "agents", name), "utf8"), `stale managed ${candidate}\n`);
|
|
1640
|
+
assert.equal(refreshed.assets[`agents/${name}`], manifest.assets[`agents/${name}`]);
|
|
1641
|
+
}
|
|
1642
|
+
const userEdit = `${packaged}\nUser-owned instructions must survive.\n`;
|
|
1643
|
+
await writeFile(selectedPath, userEdit);
|
|
1644
|
+
await command.handler("--force", ctx);
|
|
1645
|
+
assert.equal(await readFile(selectedPath, "utf8"), userEdit);
|
|
1646
|
+
assert.equal(existsSync(join(agentHome, "subagents.json")), false,
|
|
1647
|
+
"owner installers must not apply model routing");
|
|
1648
|
+
await commands.get("gentle:status").handler("", ctx);
|
|
1649
|
+
const label = owner === "sdd" ? "SDD" : owner;
|
|
1650
|
+
assert.ok(ctx.ui.notifications.at(-1).message.includes(`Global ${label} user overrides: 1 file(s)`));
|
|
1651
|
+
} finally {
|
|
1652
|
+
process.env.GENTLE_PI_CONFIG_HOME = globalConfigHome;
|
|
1653
|
+
process.env.GENTLE_PI_AGENT_HOME = globalAgentHome;
|
|
1654
|
+
await rm(fixture, { recursive: true, force: true });
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
const repairFixture = await tempWorkspace();
|
|
1659
|
+
try {
|
|
1660
|
+
process.env.GENTLE_PI_AGENT_HOME = join(repairFixture, "agent-home");
|
|
1661
|
+
const ctx = createCtx(repairFixture, true);
|
|
1662
|
+
await commands.get("gentle:install-sdd").handler("", ctx);
|
|
1663
|
+
for (const diagnostic of ["gentle:status", "gentle:doctor"]) {
|
|
1664
|
+
await commands.get(diagnostic).handler("", ctx);
|
|
1665
|
+
const message = ctx.ui.notifications.at(-1).message;
|
|
1666
|
+
assert.match(message, /\/gentle:install-delegation --force/,
|
|
1667
|
+
`${diagnostic} must provide a repair for missing delegation assets`);
|
|
1668
|
+
assert.match(message, /\/gentle:install-review --force/,
|
|
1669
|
+
`${diagnostic} must provide a repair for missing review assets`);
|
|
1670
|
+
assert.doesNotMatch(message, /install-sdd --force/);
|
|
1671
|
+
}
|
|
1672
|
+
const manifest = JSON.parse(await readFile(join(process.env.GENTLE_PI_AGENT_HOME, "gentle-ai", "managed-assets.json"), "utf8"));
|
|
1673
|
+
for (const key of Object.keys(manifest.assets)) {
|
|
1674
|
+
await rm(join(process.env.GENTLE_PI_AGENT_HOME, key));
|
|
1675
|
+
}
|
|
1676
|
+
for (const diagnostic of ["gentle:status", "gentle:doctor"]) {
|
|
1677
|
+
await commands.get(diagnostic).handler("", ctx);
|
|
1678
|
+
assert.match(ctx.ui.notifications.at(-1).message, /Global SDD assets stale: [1-9]\d* file\(s\).*install-sdd --force/);
|
|
1679
|
+
assert.doesNotMatch(ctx.ui.notifications.at(-1).message, /on demand/,
|
|
1680
|
+
"managed installation evidence must survive missing SDD files");
|
|
1681
|
+
}
|
|
1682
|
+
} finally {
|
|
1683
|
+
process.env.GENTLE_PI_AGENT_HOME = globalAgentHome;
|
|
1684
|
+
await rm(repairFixture, { recursive: true, force: true });
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
for (const trigger of ["gentle:sdd-preflight", "gentle-sdd-init"]) {
|
|
1688
|
+
const fixture = await tempWorkspace();
|
|
1689
|
+
const agentHome = join(fixture, "agent-home");
|
|
1690
|
+
try {
|
|
1691
|
+
process.env.GENTLE_PI_AGENT_HOME = agentHome;
|
|
1692
|
+
const command = commands.get(trigger);
|
|
1693
|
+
await command.handler("", createCtx(fixture, true, `${trigger}-install`));
|
|
1694
|
+
assert.equal(existsSync(join(agentHome, "agents", "gentle-ai-worker.md")), false,
|
|
1695
|
+
`${trigger} must not install delegation assets`);
|
|
1696
|
+
assert.equal(existsSync(join(agentHome, "agents", "review-risk.md")), false,
|
|
1697
|
+
`${trigger} must not install review assets`);
|
|
1698
|
+
const manifestPath = join(agentHome, "gentle-ai", "managed-assets.json");
|
|
1699
|
+
const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
|
|
1700
|
+
const managedKeys = [
|
|
1701
|
+
"agents/sdd-apply.md",
|
|
1702
|
+
"chains/sdd-full.chain.md",
|
|
1703
|
+
"gentle-ai/support/sdd-status-contract.md",
|
|
1704
|
+
];
|
|
1705
|
+
const originals = new Map();
|
|
1706
|
+
for (const key of managedKeys) {
|
|
1707
|
+
originals.set(key, await readFile(join(agentHome, key), "utf8"));
|
|
1708
|
+
}
|
|
1709
|
+
const untouchedKeys = ["agents/gentle-ai-worker.md", "agents/review-risk.md"];
|
|
1710
|
+
for (const key of [...managedKeys, ...untouchedKeys]) {
|
|
1711
|
+
await writeFile(join(agentHome, key), `stale ${key}\n`);
|
|
1712
|
+
manifest.assets[key] = sha256(`stale ${key}\n`);
|
|
1713
|
+
}
|
|
1714
|
+
await writeFile(manifestPath, JSON.stringify(manifest, null, 2));
|
|
1715
|
+
const userPath = join(agentHome, "agents", "sdd-spec.md");
|
|
1716
|
+
const userEdit = `${await readFile(userPath, "utf8")}\nUser-owned instructions.\n`;
|
|
1717
|
+
await writeFile(userPath, userEdit);
|
|
1718
|
+
const projectOverride = join(fixture, ".pi", "agents", "sdd-apply.md");
|
|
1719
|
+
await mkdir(dirname(projectOverride), { recursive: true });
|
|
1720
|
+
await writeFile(projectOverride, "Project override must survive.\n");
|
|
1721
|
+
await command.handler("", createCtx(fixture, true, `${trigger}-refresh`));
|
|
1722
|
+
for (const key of managedKeys) {
|
|
1723
|
+
assert.equal(await readFile(join(agentHome, key), "utf8"), originals.get(key),
|
|
1724
|
+
`${trigger} must refresh managed ${key} on demand`);
|
|
1725
|
+
}
|
|
1726
|
+
const refreshed = JSON.parse(await readFile(manifestPath, "utf8"));
|
|
1727
|
+
for (const key of untouchedKeys) {
|
|
1728
|
+
assert.equal(await readFile(join(agentHome, key), "utf8"), `stale ${key}\n`);
|
|
1729
|
+
assert.equal(refreshed.assets[key], manifest.assets[key]);
|
|
1730
|
+
}
|
|
1731
|
+
assert.equal(await readFile(userPath, "utf8"), userEdit);
|
|
1732
|
+
assert.equal(await readFile(projectOverride, "utf8"), "Project override must survive.\n");
|
|
1733
|
+
} finally {
|
|
1734
|
+
process.env.GENTLE_PI_AGENT_HOME = globalAgentHome;
|
|
1735
|
+
await rm(fixture, { recursive: true, force: true });
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1377
1739
|
const installCwd = await tempWorkspace();
|
|
1378
1740
|
try {
|
|
1379
1741
|
const ctx = createCtx(installCwd, true);
|
|
@@ -1388,19 +1750,51 @@ async function run() {
|
|
|
1388
1750
|
}
|
|
1389
1751
|
|
|
1390
1752
|
const staleAssetsCwd = await tempWorkspace();
|
|
1753
|
+
const previousAgentHome = process.env.GENTLE_PI_AGENT_HOME;
|
|
1754
|
+
const previousHome = process.env.HOME;
|
|
1755
|
+
const previousUserProfile = process.env.USERPROFILE;
|
|
1391
1756
|
try {
|
|
1392
|
-
|
|
1757
|
+
const diagnosticsAgentHome = join(staleAssetsCwd, "agent-home");
|
|
1758
|
+
const diagnosticsHome = staleAssetsCwd;
|
|
1759
|
+
process.env.GENTLE_PI_AGENT_HOME = diagnosticsAgentHome;
|
|
1760
|
+
process.env.HOME = diagnosticsHome;
|
|
1761
|
+
process.env.USERPROFILE = diagnosticsHome;
|
|
1762
|
+
for (const [dir, name] of [
|
|
1763
|
+
[join(diagnosticsAgentHome, "subagents"), "sdd-apply.md"],
|
|
1764
|
+
[join(diagnosticsHome, ".agents"), "sdd-archive.md"],
|
|
1765
|
+
[join(staleAssetsCwd, ".agents"), "sdd-design.md"],
|
|
1766
|
+
[join(staleAssetsCwd, ".pi", "agents"), "sdd-spec.md"],
|
|
1767
|
+
[join(staleAssetsCwd, ".pi", "subagents"), "sdd-sync.md"],
|
|
1768
|
+
]) {
|
|
1769
|
+
await mkdir(dir, { recursive: true });
|
|
1770
|
+
await writeFile(
|
|
1771
|
+
join(dir, name),
|
|
1772
|
+
`---\nname: ${name.replace(/\.md$/, "")}\n---\nintentional SDD override\n`,
|
|
1773
|
+
);
|
|
1774
|
+
}
|
|
1775
|
+
await mkdir(join(diagnosticsAgentHome, "subagents", "nested"), { recursive: true });
|
|
1776
|
+
await mkdir(join(diagnosticsAgentHome, "agents", "nested"), { recursive: true });
|
|
1777
|
+
await mkdir(join(staleAssetsCwd, ".agents", "skills", "ignored"), { recursive: true });
|
|
1778
|
+
await writeFile(
|
|
1779
|
+
join(diagnosticsAgentHome, "subagents", "nested", "renamed-agent.md"),
|
|
1780
|
+
"---\nname: sdd-apply\n---\nintentional nested SDD override\n",
|
|
1781
|
+
);
|
|
1782
|
+
await writeFile(
|
|
1783
|
+
join(diagnosticsAgentHome, "agents", "nested", "managed-copy.md"),
|
|
1784
|
+
"---\nname: sdd-apply\n---\npackage-managed root is excluded\n",
|
|
1785
|
+
);
|
|
1786
|
+
await writeFile(
|
|
1787
|
+
join(staleAssetsCwd, ".agents", "skills", "ignored", "SKILL.md"),
|
|
1788
|
+
"---\nname: sdd-apply\n---\nskills are not agent definitions\n",
|
|
1789
|
+
);
|
|
1393
1790
|
await mkdir(join(staleAssetsCwd, ".pi", "chains"), { recursive: true });
|
|
1394
1791
|
await mkdir(join(staleAssetsCwd, ".pi", "gentle-ai", "support"), { recursive: true });
|
|
1395
|
-
await writeFile(join(staleAssetsCwd, ".pi", "agents", "sdd-apply.md"), "stale apply\n");
|
|
1396
|
-
await writeFile(join(staleAssetsCwd, ".pi", "agents", "sdd-spec.md"), "stale spec\n");
|
|
1397
|
-
await writeFile(join(staleAssetsCwd, ".pi", "agents", "sdd-custom-debug.md"), "custom debug agent\n");
|
|
1398
1792
|
await writeFile(join(staleAssetsCwd, ".pi", "chains", "sdd-full.chain.md"), "stale chain\n");
|
|
1399
1793
|
await writeFile(join(staleAssetsCwd, ".pi", "gentle-ai", "support", "sdd-status-contract.md"), "stale status contract\n");
|
|
1400
1794
|
const ctx = createCtx(staleAssetsCwd, true);
|
|
1401
1795
|
await commands.get("gentle:status").handler("", ctx);
|
|
1402
|
-
assert.match(ctx.ui.notifications.at(-1).message, /
|
|
1403
|
-
assert.match(ctx.ui.notifications.at(-1).message, /
|
|
1796
|
+
assert.match(ctx.ui.notifications.at(-1).message, /Active SDD agent overrides: 6 file\(s\)/);
|
|
1797
|
+
assert.match(ctx.ui.notifications.at(-1).message, /active non-builtin SDD agents shadow package assets/);
|
|
1404
1798
|
await commands.get("gentle:doctor").handler("", ctx);
|
|
1405
1799
|
assert.match(ctx.ui.notifications.at(-1).message, /el Gentleman doctor/);
|
|
1406
1800
|
assert.match(ctx.ui.notifications.at(-1).message, /Sensitive-path guard active/);
|
|
@@ -1412,6 +1806,12 @@ async function run() {
|
|
|
1412
1806
|
assert.match(ctx.ui.notifications.at(-1).message, /Engram memory tools not active in this session/);
|
|
1413
1807
|
pi.setActiveTools(["read", "bash", "edit", "write"]);
|
|
1414
1808
|
} finally {
|
|
1809
|
+
if (previousAgentHome === undefined) delete process.env.GENTLE_PI_AGENT_HOME;
|
|
1810
|
+
else process.env.GENTLE_PI_AGENT_HOME = previousAgentHome;
|
|
1811
|
+
if (previousHome === undefined) delete process.env.HOME;
|
|
1812
|
+
else process.env.HOME = previousHome;
|
|
1813
|
+
if (previousUserProfile === undefined) delete process.env.USERPROFILE;
|
|
1814
|
+
else process.env.USERPROFILE = previousUserProfile;
|
|
1415
1815
|
await rm(staleAssetsCwd, { recursive: true, force: true });
|
|
1416
1816
|
}
|
|
1417
1817
|
|
|
@@ -1426,12 +1826,20 @@ async function run() {
|
|
|
1426
1826
|
assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-sync.md")), true);
|
|
1427
1827
|
assert.equal(existsSync(join(globalAgentHome, "gentle-ai", "support", "sdd-status-contract.md")), true);
|
|
1428
1828
|
assert.equal(existsSync(join(globalAgentHome, "chains", "sdd-full.chain.md")), true);
|
|
1429
|
-
assert.equal(ctx.ui.selections.length,
|
|
1430
|
-
assert.match(ctx.ui.notifications[
|
|
1829
|
+
assert.equal(ctx.ui.selections.length, 1, "sdd-init confirms session preflight before project initialization");
|
|
1830
|
+
assert.match(ctx.ui.notifications[1].message, /SDD preflight complete/);
|
|
1431
1831
|
assert.match(ctx.ui.notifications.at(-1).message, /Wrote openspec\/config\.yaml/);
|
|
1432
|
-
|
|
1433
|
-
await
|
|
1434
|
-
assert.
|
|
1832
|
+
const initializedConfig = await readFile(join(sddCwd, "openspec", "config.yaml"), "utf8");
|
|
1833
|
+
const explore = await hooks.get("before_agent_start")[0]({ agentName: "sdd-explore", systemPrompt: "You are the SDD explore executor for Gentle AI." }, ctx);
|
|
1834
|
+
assert.match(explore.systemPrompt, /explicit current-session choices/);
|
|
1835
|
+
assert.equal(ctx.ui.selections.length, 1, "confirmation -> sdd-init -> explore must reuse the resolved preflight");
|
|
1836
|
+
const nextSession = createCtx(sddCwd, true, "cold-start-with-saved-preferences");
|
|
1837
|
+
await hooks.get("input")[0]({ text: "/sdd", source: "interactive" }, nextSession);
|
|
1838
|
+
assert.equal(nextSession.ui.selections.length, 1, "saved preferences still require confirmation in a new session");
|
|
1839
|
+
assert.equal(await readFile(join(sddCwd, "openspec", "config.yaml"), "utf8"), initializedConfig, "new session confirmation must not reset project initialization");
|
|
1840
|
+
|
|
1841
|
+
await commands.get("gentle:sdd-preflight").handler("--edit", ctx);
|
|
1842
|
+
assert.equal(ctx.ui.selections.length, 3, "--edit permits changes after confirmed sdd-init");
|
|
1435
1843
|
} finally {
|
|
1436
1844
|
await rm(sddCwd, { recursive: true, force: true });
|
|
1437
1845
|
}
|
|
@@ -1446,9 +1854,9 @@ async function run() {
|
|
|
1446
1854
|
await writeFile(globalModelsPath, "{ invalid json");
|
|
1447
1855
|
const ctx = createCtx(invalidSddInitCwd, true, "invalid-sdd-init-session");
|
|
1448
1856
|
await commands.get("gentle-sdd-init").handler("", ctx);
|
|
1449
|
-
assert.equal(ctx.ui.notifications[
|
|
1450
|
-
assert.match(ctx.ui.notifications[
|
|
1451
|
-
assert.match(ctx.ui.notifications[
|
|
1857
|
+
assert.equal(ctx.ui.notifications[1].level, "warning");
|
|
1858
|
+
assert.match(ctx.ui.notifications[1].message, /Model routing skipped:/);
|
|
1859
|
+
assert.match(ctx.ui.notifications[1].message, /models\.json/);
|
|
1452
1860
|
assert.match(ctx.ui.notifications.at(-1).message, /Wrote openspec\/config\.yaml/);
|
|
1453
1861
|
const preservedAgent = await readFile(
|
|
1454
1862
|
join(invalidSddInitCwd, ".pi", "agents", "sdd-apply.md"),
|