gentle-pi 2.1.2 → 2.2.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 +70 -12
- package/assets/agents/gentle-ai-worker.md +7 -3
- package/assets/agents/jd-fix-agent.md +1 -1
- package/assets/agents/jd-judge-a.md +3 -1
- package/assets/agents/jd-judge-b.md +3 -1
- package/assets/agents/review-readability.md +4 -1
- package/assets/agents/review-reliability.md +4 -1
- package/assets/agents/review-resilience.md +4 -1
- package/assets/agents/review-risk.md +4 -1
- package/assets/agents/sdd-apply.md +6 -1
- package/assets/agents/sdd-archive.md +6 -1
- package/assets/agents/sdd-design.md +6 -1
- package/assets/agents/sdd-explore.md +6 -2
- package/assets/agents/sdd-init.md +10 -2
- package/assets/agents/sdd-onboard.md +6 -1
- package/assets/agents/sdd-proposal.md +6 -1
- package/assets/agents/sdd-spec.md +6 -1
- package/assets/agents/sdd-status.md +6 -1
- package/assets/agents/sdd-sync.md +6 -1
- package/assets/agents/sdd-tasks.md +6 -1
- package/assets/agents/sdd-verify.md +6 -1
- package/assets/chains/4r-review.chain.md +2 -0
- package/assets/chains/sdd-full.chain.md +1 -1
- package/assets/chains/sdd-plan.chain.md +1 -1
- package/assets/chains/sdd-verify.chain.md +1 -1
- package/assets/orchestrator-delegation.md +246 -67
- package/assets/orchestrator.md +7 -14
- package/assets/sdd-orchestrator-workflow.md +154 -9
- package/assets/support/sdd-status-contract.md +19 -1
- package/contracts/review-integration/v1/fixtures/consent.fixture.json +3 -3
- package/contracts/review-integration/v1/fixtures/start-v2.fixture.json +19 -28
- package/contracts/review-integration/v1/fixtures/start.fixture.json +1 -10
- package/contracts/review-integration/v1/fixtures/status-v2.fixture.json +12 -21
- package/contracts/review-integration/v1/schemas/correction-plan-request.schema.json +49 -0
- package/contracts/review-integration/v1/schemas/operation.schema.json +76 -0
- package/contracts/review-integration/v1/schemas/repair.schema.json +39 -0
- package/contracts/review-integration/v1/schemas/status-v2.schema.json +4 -2
- package/contracts/review-integration/v1/schemas/status.schema.json +4 -2
- package/contracts/review-integration/v2/fixtures/consent.fixture.json +1 -1
- package/contracts/review-integration/v2/fixtures/start.fixture.json +1 -10
- package/contracts/review-integration/v2/fixtures/status.fixture.json +1 -10
- package/contracts/review-integration/v2/schemas/failure.schema.json +5 -1
- package/contracts/review-integration/v2/schemas/operation.schema.json +6 -1
- package/contracts/review-integration/v2/schemas/repair.schema.json +4 -2
- package/contracts/review-integration/v2/schemas/start.schema.json +5 -2
- package/contracts/review-integration/v2/schemas/status.schema.json +4 -2
- package/contracts/review-provider-contract-mirror/provider-contract.lock.json +30 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/README.md +12 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/manifest.json +65 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/schemas/lens.schema.json +16 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/schemas/refuter.schema.json +1 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/schemas/targeted-validator.schema.json +1 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/vectors/lens.json +1 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/vectors/refuter.json +1 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/vectors/targeted-validator.json +1 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/generated/provider-capabilities.baseline.json +15 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/generated/provider-roles.baseline.json +42 -0
- package/docs/native-authority-architecture.md +5 -5
- package/docs/review-integration.md +22 -2
- package/extensions/gentle-ai.ts +1595 -201
- package/extensions/sdd-init.ts +19 -6
- package/extensions/skill-registry.ts +10 -2
- package/extensions/startup-banner.ts +10 -4
- package/lib/gentle-ai-binary.ts +173 -2
- package/lib/git-commit-transaction.ts +77 -17
- package/lib/native-review-cli.ts +528 -65
- package/lib/provider-contract-bundle.ts +704 -0
- package/lib/review-candidate-view.ts +527 -18
- package/lib/review-compact-contract.ts +59 -248
- package/lib/review-host-relay.ts +436 -0
- package/lib/review-integration-v2.ts +537 -36
- package/lib/review-relay-contract.ts +16 -0
- package/lib/sdd-preflight.ts +53 -1
- package/package.json +5 -2
- package/runtime/gentle-ai-binary.mjs +173 -2
- package/runtime/git-commit-transaction.mjs +75 -15
- package/runtime/native-review-cli.mjs +524 -61
- package/runtime/review-integration-v2.mjs +536 -35
- package/runtime/review-relay-contract.mjs +17 -0
- package/scripts/build-git-commit-transaction-runner.mjs +1 -0
- package/scripts/check-provider-contract.mjs +138 -0
- package/scripts/gentle-ai-installer.mjs +23 -13
- package/scripts/maintainer/provider-relay-matrix.mjs +219 -0
- package/scripts/mirror-provider-contract.mjs +143 -0
- package/scripts/test-packed-runner.mjs +16 -2
- package/scripts/verify-package-files.mjs +110 -33
- package/skills/_shared/review-ledger-contract.md +4 -6
- package/skills/gentle-ai/SKILL.md +4 -4
- package/skills/issue-creation/SKILL.md +94 -168
- package/skills/judgment-day/SKILL.md +7 -1
- package/skills/judgment-day/references/prompts-and-formats.md +2 -0
- package/skills/rdd-defect-workflow/SKILL.md +54 -0
- package/tests/background-subagents.test.ts +771 -0
- package/tests/crosslane/cross-lane.mjs +1169 -0
- package/tests/delegated-key-learnings-contract.test.ts +238 -0
- package/tests/fixtures/devbinary/capabilities-v2.1.derived.json +331 -0
- package/tests/fixtures/devbinary/capabilities-v2.2.captured.json +340 -0
- package/tests/fixtures/devbinary/consent-v3.captured.json +37 -0
- package/tests/fixtures/devbinary/failure-v2-capture-evidence.captured.json +16 -0
- package/tests/fixtures/devbinary/result-artifact-v2-path.captured.json +12 -0
- package/tests/fixtures/devbinary/result-artifact-v2.captured.json +12 -0
- package/tests/fixtures/devbinary/start-v3-consent-declined.captured.json +19 -0
- package/tests/fixtures/devbinary/start-v3-consent-granted.captured.json +109 -0
- package/tests/fixtures/devbinary/status-v5-capture-result-submission.captured.json +184 -0
- package/tests/fixtures/devbinary/status-v5-repository-context.captured.json +138 -0
- package/tests/fixtures/devbinary/status-v5.captured.json +88 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/README.md +12 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/manifest.json +65 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/schemas/lens.schema.json +16 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/schemas/refuter.schema.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/schemas/targeted-validator.schema.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/vectors/lens.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/vectors/refuter.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/vectors/targeted-validator.json +1 -0
- package/tests/gentle-ai-binary.test.ts +1 -1
- package/tests/gentle-ai-dev-binary-surfacing.test.ts +195 -0
- package/tests/gentle-ai-dev-binary.test.ts +336 -0
- package/tests/gentle-ai-installer.test.ts +46 -46
- package/tests/git-commit-transaction.test.ts +229 -1
- package/tests/maintainer/provider-relay.maintest.ts +265 -0
- package/tests/native-review-capability-contract.test.ts +48 -2
- package/tests/native-review-cli.test.ts +56 -0
- package/tests/native-review-consent.test.ts +164 -3
- package/tests/native-review-parity-runtime.test.ts +37 -0
- package/tests/native-review-parity.test.ts +218 -15
- package/tests/native-sdd-attempt-authority.test.ts +235 -0
- package/tests/orchestrator-budget.test.ts +30 -5
- package/tests/package-manifest.test.ts +98 -72
- package/tests/provider-contract-bundle.test.ts +385 -0
- package/tests/provider-contract-mirror.test.ts +206 -0
- package/tests/provider-defect-handoff.test.ts +355 -0
- package/tests/review-actor-tool-deny.test.ts +12 -13
- package/tests/review-candidate-view.test.ts +489 -9
- package/tests/review-compact-contract.test.ts +52 -119
- package/tests/review-controller-native-recovery.test.ts +643 -47
- package/tests/review-controller-native-routing.test.ts +1667 -222
- package/tests/review-controller-workspace-root.test.ts +17 -2
- package/tests/review-corrected-finalize-binding.test.ts +175 -0
- package/tests/review-dispatch-hydration-gap.test.ts +197 -0
- package/tests/review-host-relay-routing.test.ts +317 -0
- package/tests/review-host-relay.test.ts +520 -0
- package/tests/review-integration-v2-forward.test.ts +631 -0
- package/tests/review-integration-v2.test.ts +114 -0
- package/tests/review-ledger-contract.test.ts +12 -28
- package/tests/review-recovered-lineage-routing.test.ts +246 -0
- package/tests/review-relay-transport-agent.test.ts +249 -0
- package/tests/runtime-harness.mjs +242 -14
- package/tests/sdd-agent-tools.test.ts +18 -33
- package/tests/skill-collision-prefixes.test.ts +1 -0
- package/tests/skill-registry.test.ts +50 -1
- package/tests/verify-package-files.test.ts +62 -0
- package/assets/agents/review-refuter.md +0 -40
- package/assets/agents/review-validator.md +0 -23
- package/lib/native-review-remediation.ts +0 -49
- package/lib/review-compact.ts +0 -947
- package/lib/review-refuter-adapter.ts +0 -129
- package/lib/review-runtime-contract.ts +0 -68
- package/prompts/gcl.md +0 -54
- package/prompts/gis.md +0 -25
- package/prompts/gpr.md +0 -41
- package/prompts/gwr.md +0 -31
- package/tests/fixtures/native-review-cli/v2.1.2/bind-sdd.json +0 -25
- package/tests/fixtures/native-review-cli/v2.1.2/finalize.json +0 -8
- package/tests/fixtures/native-review-cli/v2.1.2/sdd-status-engram.json +0 -139
- package/tests/fixtures/native-review-cli/v2.1.2/sdd-status.json +0 -200
- package/tests/fixtures/native-review-cli/v2.1.2/start.json +0 -12
- package/tests/fixtures/native-review-cli/v2.1.2/validate-allow.json +0 -24
- package/tests/fixtures/native-review-cli/v2.1.2/validate-deny-empty-context.json +0 -20
- package/tests/fixtures/native-review-cli/v2.1.2/validate-deny.json +0 -28
- package/tests/review-compact.test.ts +0 -243
- package/tests/review-refuter-adapter.test.ts +0 -89
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { execFileSync } from "node:child_process";
|
|
3
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import test from "node:test";
|
|
7
|
+
import { __testing } from "../extensions/gentle-ai.ts";
|
|
8
|
+
import { NativeReviewIntegrationError, type NativeReviewCli } from "../lib/native-review-cli.ts";
|
|
9
|
+
import { CandidateViewRegistry } from "../lib/review-candidate-view.ts";
|
|
10
|
+
import type { ReviewCollectInputV3, ReviewStatusV3 } from "../lib/review-integration-v2.ts";
|
|
11
|
+
import type { ReviewHostRelayRequest } from "../lib/review-host-relay.ts";
|
|
12
|
+
|
|
13
|
+
// Third field failure on the recovered-lineage defect (2026-08-16, gentle-pi
|
|
14
|
+
// main 402f9f77 + gentle-ai 2.4.0-main.20278905): STATUS recognises the
|
|
15
|
+
// lineage, the Pi RELAY never runs, no lens is launched, zero mutations.
|
|
16
|
+
//
|
|
17
|
+
// Measured against the live binary on a faithful reproduction (linked
|
|
18
|
+
// worktree, uncommitted tracked files, externally recovered successor):
|
|
19
|
+
//
|
|
20
|
+
// agent-less STATUS -> collect input arguments:
|
|
21
|
+
// lineage, expected-revision, target, repository-context, lens, order,
|
|
22
|
+
// subject-hash (no agent, no materialize, no submission)
|
|
23
|
+
// `--agent pi` STATUS -> the same input PLUS agent=pi, materialize=true and
|
|
24
|
+
// the provider submission (operation_token=capture-result)
|
|
25
|
+
//
|
|
26
|
+
// The adapter's negotiated targetStatus never sent `--agent pi`, so the
|
|
27
|
+
// provider never offered the materialize-marked relay slot,
|
|
28
|
+
// reviewHostRelaySlots() returned 0 for every real lineage, and the host relay
|
|
29
|
+
// was unreachable. The prior fixes all exercised dispatch-shaped slots.
|
|
30
|
+
//
|
|
31
|
+
// Binaries older than v2.4.0 do not define `--agent` on `review status` and
|
|
32
|
+
// refuse it outright, so the agent is probed rather than version-sniffed and
|
|
33
|
+
// the typed refusal must reach the user instead of being swallowed.
|
|
34
|
+
|
|
35
|
+
const SHA = `sha256:${"1".repeat(64)}`;
|
|
36
|
+
const TRANSPORT_REFUSAL_CODE = "immutable_review_transport_unsupported";
|
|
37
|
+
|
|
38
|
+
function repository(t: test.TestContext): string {
|
|
39
|
+
const cwd = mkdtempSync(join(tmpdir(), "gentle-pi-relay-transport-"));
|
|
40
|
+
t.after(() => rmSync(cwd, { recursive: true, force: true }));
|
|
41
|
+
execFileSync("git", ["init", "-b", "main"], { cwd });
|
|
42
|
+
writeFileSync(join(cwd, "app.ts"), "export const value = 1;\n");
|
|
43
|
+
execFileSync("git", ["add", "."], { cwd });
|
|
44
|
+
execFileSync("git", ["-c", "user.name=Relay", "-c", "user.email=relay@example.invalid", "commit", "-m", "base"], { cwd });
|
|
45
|
+
return cwd;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function collectInput(lineageId: string, materialize: boolean): ReviewCollectInputV3 {
|
|
49
|
+
const binding = [
|
|
50
|
+
{ name: "lineage", value: lineageId, token: `--lineage=${lineageId}` },
|
|
51
|
+
{ name: "expected-revision", value: SHA, token: `--expected-revision=${SHA}` },
|
|
52
|
+
{ name: "target", value: SHA, token: `--target=${SHA}` },
|
|
53
|
+
{ name: "repository-context", value: `rctx1_${"e".repeat(64)}`, token: `--repository-context=rctx1_${"e".repeat(64)}` },
|
|
54
|
+
{ name: "lens", value: "review-reliability", token: "--lens=review-reliability" },
|
|
55
|
+
{ name: "order", value: "0", token: "--order=0" },
|
|
56
|
+
{ name: "subject-hash", value: SHA, token: `--subject-hash=${SHA}` },
|
|
57
|
+
];
|
|
58
|
+
return {
|
|
59
|
+
name: "reviewer_result",
|
|
60
|
+
schema: "https://gentle-ai.dev/schema/review/reviewer/v1",
|
|
61
|
+
captureOperation: "review.capture-result",
|
|
62
|
+
arguments: materialize
|
|
63
|
+
? [...binding, { name: "agent", value: "pi", token: "--agent=pi" }, { name: "materialize", value: "true", token: "--materialize=true" }]
|
|
64
|
+
: binding,
|
|
65
|
+
artifactSubject: {
|
|
66
|
+
schema: "gentle-ai.review-artifact-subject/v2",
|
|
67
|
+
subjectHash: SHA,
|
|
68
|
+
lineageId,
|
|
69
|
+
authorityRevision: SHA,
|
|
70
|
+
targetIdentity: SHA,
|
|
71
|
+
baseTree: "3".repeat(40),
|
|
72
|
+
candidateTree: "4".repeat(40),
|
|
73
|
+
changedPathManifestSha256: SHA,
|
|
74
|
+
lens: "review-reliability",
|
|
75
|
+
selectedOrder: 0,
|
|
76
|
+
},
|
|
77
|
+
...(materialize
|
|
78
|
+
? {
|
|
79
|
+
submission: {
|
|
80
|
+
operationToken: "capture-result",
|
|
81
|
+
argumentTokens: [...binding.map((argument) => argument.token), "--input={{value}}"],
|
|
82
|
+
values: [{ slot: "reviewer_result", domain: "artifact_path_or_stdin", substitutionLocation: binding.length }],
|
|
83
|
+
},
|
|
84
|
+
}
|
|
85
|
+
: {}),
|
|
86
|
+
} as unknown as ReviewCollectInputV3;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function recoveredStatus(lineageId: string, materialize: boolean): ReviewStatusV3 {
|
|
90
|
+
return {
|
|
91
|
+
contract: "gentle-ai.review-integration/v2",
|
|
92
|
+
applicability: "current_target",
|
|
93
|
+
authority: { version: "compact-v2", lineageId, state: "reviewer_results_required", generation: 2, revision: SHA },
|
|
94
|
+
receipt: { status: "expected_missing" },
|
|
95
|
+
action: "finalize",
|
|
96
|
+
replayability: "not_replayable",
|
|
97
|
+
targetIdentity: SHA,
|
|
98
|
+
projection: {
|
|
99
|
+
schema: "gentle-ai.review-integration.projection/v1",
|
|
100
|
+
kind: "current-changes",
|
|
101
|
+
projection: "workspace",
|
|
102
|
+
baseTree: "3".repeat(40),
|
|
103
|
+
initialReviewTree: "4".repeat(40),
|
|
104
|
+
currentCandidateTree: "4".repeat(40),
|
|
105
|
+
pathsDigest: SHA,
|
|
106
|
+
paths: ["app.ts"],
|
|
107
|
+
intendedUntracked: [],
|
|
108
|
+
intendedUntrackedProof: SHA,
|
|
109
|
+
initialSnapshotIdentity: SHA,
|
|
110
|
+
currentSnapshotIdentity: SHA,
|
|
111
|
+
},
|
|
112
|
+
candidates: [],
|
|
113
|
+
nextTransition: { kind: "collect", reasonCode: "reviewer_results_required", collect: { inputs: [collectInput(lineageId, materialize)] } },
|
|
114
|
+
raw: { schema: "gentle-ai.review-integration.status/v5", action: "finalize", lineage_id: lineageId },
|
|
115
|
+
} as unknown as ReviewStatusV3;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Mirrors the MEASURED live provider: the materialize-marked relay slot is
|
|
120
|
+
* offered only when the caller asks for the pi agent.
|
|
121
|
+
*/
|
|
122
|
+
function transportAwareNative(options: { refusePiAgent?: boolean } = {}): { native: NativeReviewCli; agents: Array<string | undefined> } {
|
|
123
|
+
const agents: Array<string | undefined> = [];
|
|
124
|
+
const native = {
|
|
125
|
+
targetStatus: async (request: { lineageId?: string; agent?: string }) => {
|
|
126
|
+
agents.push(request.agent);
|
|
127
|
+
if (request.agent === "pi" && options.refusePiAgent === true) {
|
|
128
|
+
throw new NativeReviewIntegrationError({
|
|
129
|
+
schema: "gentle-ai.review-integration.failure/v2",
|
|
130
|
+
contract: "gentle-ai.review-integration/v2",
|
|
131
|
+
operation: "review.status",
|
|
132
|
+
phase: "pre_native",
|
|
133
|
+
code: TRANSPORT_REFUSAL_CODE,
|
|
134
|
+
message: "supported immutable review runtimes: claude-code, opencode, codex",
|
|
135
|
+
mutationOutcome: "none",
|
|
136
|
+
authorityApplicability: "current_target",
|
|
137
|
+
retrySafe: true,
|
|
138
|
+
replayability: "not_replayable",
|
|
139
|
+
nextAction: "stop",
|
|
140
|
+
raw: {},
|
|
141
|
+
} as never);
|
|
142
|
+
}
|
|
143
|
+
return recoveredStatus(request.lineageId ?? "relay-lineage", request.agent === "pi");
|
|
144
|
+
},
|
|
145
|
+
finalize: async () => { throw new Error("native finalize must not run while reviewer results are outstanding"); },
|
|
146
|
+
} as unknown as NativeReviewCli;
|
|
147
|
+
return { native, agents };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function runFinalize(cwd: string, native: NativeReviewCli, lineageId: string, input: Record<string, unknown> = { reviewer_run_acknowledged: true }): Promise<Record<string, unknown>> {
|
|
151
|
+
return await __testing.executeReviewControllerOperation(
|
|
152
|
+
{ operation: "finalize", lineageId, input: JSON.stringify(input) },
|
|
153
|
+
cwd, new Map(), native, undefined, undefined, undefined, new CandidateViewRegistry(),
|
|
154
|
+
) as Record<string, unknown>;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
test("the negotiated status asks for the pi agent so the provider offers its materialize relay slot", async (t) => {
|
|
158
|
+
t.after(() => __testing.setReviewHostRelayRunnerForTesting());
|
|
159
|
+
const cwd = repository(t);
|
|
160
|
+
const lineageId = "relay-recovered-successor";
|
|
161
|
+
const { native, agents } = transportAwareNative();
|
|
162
|
+
const relayed: ReviewHostRelayRequest[] = [];
|
|
163
|
+
__testing.setReviewHostRelayRunnerForTesting(async (request: ReviewHostRelayRequest) => {
|
|
164
|
+
relayed.push(request);
|
|
165
|
+
return { promptByteLength: 128, resultByteLength: 64, submission: '{"admission_decision":"completed"}' };
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
const result = await runFinalize(cwd, native, lineageId);
|
|
169
|
+
|
|
170
|
+
assert.ok(agents.includes("pi"), "the controller must negotiate STATUS for the pi reviewer transport");
|
|
171
|
+
assert.equal(relayed.length, 1, "the materialize-marked slot must reach the host relay");
|
|
172
|
+
assert.ok(relayed[0]!.captureArgumentTokens.includes("--materialize=true"));
|
|
173
|
+
assert.ok(relayed[0]!.submission !== undefined, "the provider submission drives the completing form");
|
|
174
|
+
const hostRelay = result.host_relay as { transport: string; captured_slots: readonly unknown[] } | undefined;
|
|
175
|
+
assert.ok(hostRelay !== undefined, "the envelope reports the relay capture");
|
|
176
|
+
assert.equal(hostRelay.transport, "pi_host_relay");
|
|
177
|
+
assert.equal(hostRelay.captured_slots.length, 1);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test("finalize forecasts the reviewer model run once and spends nothing until it is acknowledged", async (t) => {
|
|
181
|
+
t.after(() => __testing.setReviewHostRelayRunnerForTesting());
|
|
182
|
+
const cwd = repository(t);
|
|
183
|
+
const lineageId = "forecast-lineage";
|
|
184
|
+
const { native } = transportAwareNative();
|
|
185
|
+
let launches = 0;
|
|
186
|
+
__testing.setReviewHostRelayRunnerForTesting(async () => {
|
|
187
|
+
launches += 1;
|
|
188
|
+
return { promptByteLength: 128, resultByteLength: 64, submission: '{"admission_decision":"completed"}' };
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// An unacknowledged finalize must forecast, never spend.
|
|
192
|
+
const forecast = await runFinalize(cwd, native, lineageId, {});
|
|
193
|
+
assert.equal(launches, 0, "no reviewer model run may start before the forecast is acknowledged");
|
|
194
|
+
assert.equal(forecast.status, "blocked");
|
|
195
|
+
assert.equal(forecast.outcome, "reviewer-model-run-forecast");
|
|
196
|
+
assert.equal(forecast.mutation_performed, false);
|
|
197
|
+
assert.equal(forecast.mutation_outcome, "none");
|
|
198
|
+
const cost = forecast.cost_forecast as { transport: string; model_runs: number; lenses: readonly string[]; side_effects: readonly string[] };
|
|
199
|
+
assert.equal(cost.transport, "pi_host_relay");
|
|
200
|
+
assert.equal(cost.model_runs, 1, "one model run per outstanding lens, forecast once before launch");
|
|
201
|
+
assert.deepEqual(cost.lenses, ["review-reliability"]);
|
|
202
|
+
assert.ok(cost.side_effects.length > 0);
|
|
203
|
+
assert.match(String(forecast.reason), /model run/i);
|
|
204
|
+
assert.match(String(forecast.next_action), /reviewer_run_acknowledged/);
|
|
205
|
+
|
|
206
|
+
// The acknowledged finalize runs exactly the forecast work.
|
|
207
|
+
const acknowledged = await runFinalize(cwd, native, lineageId, { reviewer_run_acknowledged: true });
|
|
208
|
+
assert.equal(launches, 1, "the acknowledged finalize runs the forecast model run");
|
|
209
|
+
assert.equal((acknowledged.host_relay as { captured_slots: readonly unknown[] }).captured_slots.length, 1);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test("a provider that refuses the pi transport surfaces the typed cause and still returns status", async (t) => {
|
|
213
|
+
t.after(() => __testing.setReviewHostRelayRunnerForTesting());
|
|
214
|
+
const cwd = repository(t);
|
|
215
|
+
const lineageId = "legacy-transport-lineage";
|
|
216
|
+
const { native, agents } = transportAwareNative({ refusePiAgent: true });
|
|
217
|
+
let relayCalls = 0;
|
|
218
|
+
__testing.setReviewHostRelayRunnerForTesting(async () => {
|
|
219
|
+
relayCalls += 1;
|
|
220
|
+
return { promptByteLength: 1, resultByteLength: 1, submission: "{}" };
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
const result = await runFinalize(cwd, native, lineageId);
|
|
224
|
+
|
|
225
|
+
assert.ok(agents.includes("pi"), "the transport is probed once");
|
|
226
|
+
assert.ok(agents.includes(undefined), "a refusal falls back to the agent-less status instead of failing the operation");
|
|
227
|
+
assert.equal(relayCalls, 0, "no relay slot exists on a provider without the pi transport");
|
|
228
|
+
// Deliverable: the user sees the real typed cause, never a generic
|
|
229
|
+
// candidate-view message.
|
|
230
|
+
const transport = result.relay_transport as { supported: boolean; code: string; message: string } | undefined;
|
|
231
|
+
assert.ok(transport !== undefined, "the envelope must report the transport refusal");
|
|
232
|
+
assert.equal(transport.supported, false);
|
|
233
|
+
assert.equal(transport.code, TRANSPORT_REFUSAL_CODE);
|
|
234
|
+
assert.match(transport.message, /immutable review runtimes/i);
|
|
235
|
+
assert.doesNotMatch(JSON.stringify(result), /no current controller-owned candidate view/);
|
|
236
|
+
// The lifecycle still routes: reviewer results remain outstanding.
|
|
237
|
+
assert.equal(result.outcome, "reviewer-results-required");
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test("the pi transport is probed once per provider and the refusal is remembered", async (t) => {
|
|
241
|
+
const cwd = repository(t);
|
|
242
|
+
const { native, agents } = transportAwareNative({ refusePiAgent: true });
|
|
243
|
+
await runFinalize(cwd, native, "probe-lineage");
|
|
244
|
+
const afterFirst = agents.filter((agent) => agent === "pi").length;
|
|
245
|
+
await runFinalize(cwd, native, "probe-lineage");
|
|
246
|
+
const afterSecond = agents.filter((agent) => agent === "pi").length;
|
|
247
|
+
assert.equal(afterFirst, 1, "the first flow probes the pi transport exactly once");
|
|
248
|
+
assert.equal(afterSecond, 1, "a remembered refusal is never re-probed for the same provider");
|
|
249
|
+
});
|
|
@@ -11,6 +11,7 @@ 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 { resolveGentleAiBinary } from "../lib/gentle-ai-binary.ts";
|
|
14
15
|
import { NativeReviewCliV216, NATIVE_REVIEW_ERROR_CODE, NativeReviewCliError } from "../lib/native-review-cli.ts";
|
|
15
16
|
|
|
16
17
|
const ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
@@ -196,6 +197,23 @@ async function run() {
|
|
|
196
197
|
process.env.GENTLE_PI_CONFIG_HOME = globalConfigHome;
|
|
197
198
|
process.env.GENTLE_PI_AGENT_HOME = globalAgentHome;
|
|
198
199
|
process.env.GENTLE_PI_TEST_ASSETS_DIR = ambientTestAssetsDir;
|
|
200
|
+
// The harness already sandboxes its config and agent homes; the review kill
|
|
201
|
+
// switch is the one piece of machine state it was still inheriting. Several
|
|
202
|
+
// lifecycle-gate assertions below only reach their subject while
|
|
203
|
+
// receipt-driven development is on — with it off, gateLifecycleCommand
|
|
204
|
+
// returns undefined before deriving any target, which is correct behavior
|
|
205
|
+
// but not what those assertions are about.
|
|
206
|
+
//
|
|
207
|
+
// gentle-ai v2.4.0 made the switch opt-in, so an unconfigured machine now
|
|
208
|
+
// resolves to off. Until then the harness passed or failed on whatever the
|
|
209
|
+
// operator happened to have set, which made it green locally and red on a
|
|
210
|
+
// fresh CI runner from identical bytes. It now owns a sandbox HOME and opts
|
|
211
|
+
// in explicitly, the same way a user does, so the gates are what is under
|
|
212
|
+
// test rather than the machine.
|
|
213
|
+
const reviewHome = await tempWorkspace();
|
|
214
|
+
process.env.HOME = reviewHome;
|
|
215
|
+
const reviewModeEnable = execFileSync(resolveGentleAiBinary(ROOT, process.platform), ["review", "mode", "enable", "--scope", "global", "--json"], { cwd: ROOT, encoding: "utf8", env: { ...process.env, HOME: reviewHome } });
|
|
216
|
+
assert.match(reviewModeEnable, /"effective": "on"/, "the harness sandbox HOME must have receipt-driven development explicitly enabled");
|
|
199
217
|
const globalModelsPath = join(globalConfigHome, "models.json");
|
|
200
218
|
const globalSubagentsPath = join(globalAgentHome, "subagents.json");
|
|
201
219
|
const { pi, hooks, commands, flags, tools } = createPi();
|
|
@@ -217,6 +235,7 @@ async function run() {
|
|
|
217
235
|
assert.ok(tools.has(toolName), `missing quiet built-in tool renderer ${toolName}`);
|
|
218
236
|
}
|
|
219
237
|
assert.ok(tools.has("gentle_review"), "missing registered bounded review controller tool");
|
|
238
|
+
assert.ok(tools.has("gentle_review_scope"), "missing registered bounded review scope tool");
|
|
220
239
|
assert.deepEqual(
|
|
221
240
|
tools.get("gentle_review").parameters.properties.operation.enum.filter((operation) => operation.includes("supersession") || operation === "supersede" || operation === "reconcile-authority"),
|
|
222
241
|
["reconcile-authority"],
|
|
@@ -536,7 +555,7 @@ async function run() {
|
|
|
536
555
|
const executableDigest = "dcc846103b16d365eaeeb9d7f289c23fc4f2897f23def1cb3fe7f05557b64705";
|
|
537
556
|
const capabilitiesBody = {
|
|
538
557
|
...capabilitiesFixture,
|
|
539
|
-
package: { ...capabilitiesFixture.package, version: "2.
|
|
558
|
+
package: { ...capabilitiesFixture.package, version: "2.4.0" },
|
|
540
559
|
};
|
|
541
560
|
const statusBody = {
|
|
542
561
|
schema: "gentle-ai.review-integration.status/v3",
|
|
@@ -624,6 +643,76 @@ async function run() {
|
|
|
624
643
|
await rm(join(globalConfigHome, "banner.json"), { force: true });
|
|
625
644
|
}
|
|
626
645
|
|
|
646
|
+
// issue-301: cancelling the color picker must be a no-op — no write,
|
|
647
|
+
// no notify, and the previously saved color must survive byte/semantically
|
|
648
|
+
// unchanged. Covers both entry points: /gentle:banner-color picker
|
|
649
|
+
// (cancelled), and /gentle:banner -> Color row -> nested picker (cancelled).
|
|
650
|
+
// Also covers an invalid non-empty argument, which must still open the
|
|
651
|
+
// picker and treat its cancellation as a no-op.
|
|
652
|
+
const cancelPickerCwd = await tempWorkspace();
|
|
653
|
+
try {
|
|
654
|
+
const bannerConfigPath = join(globalConfigHome, "banner.json");
|
|
655
|
+
const seeded = {
|
|
656
|
+
showRose: false,
|
|
657
|
+
showTextLogo: false,
|
|
658
|
+
color: "green",
|
|
659
|
+
};
|
|
660
|
+
const seededJson = `${JSON.stringify(seeded, null, 2)}\n`;
|
|
661
|
+
await writeFile(bannerConfigPath, seededJson, "utf8");
|
|
662
|
+
|
|
663
|
+
const cancelCtx = createCtx(cancelPickerCwd, true);
|
|
664
|
+
|
|
665
|
+
// (a) /gentle:banner-color picker cancelled: seeded color unchanged, no notify.
|
|
666
|
+
cancelCtx.ui.notifications.length = 0;
|
|
667
|
+
cancelCtx.ui.selections.length = 0;
|
|
668
|
+
cancelCtx.ui.select = async (label, options) => {
|
|
669
|
+
cancelCtx.ui.selections.push({ label, options });
|
|
670
|
+
return undefined;
|
|
671
|
+
};
|
|
672
|
+
await commands.get("gentle:banner-color").handler("", cancelCtx);
|
|
673
|
+
let afterCancel = await readFile(bannerConfigPath, "utf8");
|
|
674
|
+
assert.equal(afterCancel, seededJson, "banner-color cancel must not rewrite banner.json");
|
|
675
|
+
assert.equal(cancelCtx.ui.selections.length, 1, "banner-color cancel must open the picker once");
|
|
676
|
+
assert.equal(cancelCtx.ui.notifications.length, 0, "banner-color cancel must not notify");
|
|
677
|
+
|
|
678
|
+
// (d) invalid non-empty /gentle:banner-color input still opens picker;
|
|
679
|
+
// cancelling it is a no-op.
|
|
680
|
+
cancelCtx.ui.notifications.length = 0;
|
|
681
|
+
cancelCtx.ui.selections.length = 0;
|
|
682
|
+
await commands.get("gentle:banner-color").handler("purple", cancelCtx);
|
|
683
|
+
afterCancel = await readFile(bannerConfigPath, "utf8");
|
|
684
|
+
assert.equal(afterCancel, seededJson, "banner-color invalid+cancel must not rewrite banner.json");
|
|
685
|
+
assert.equal(cancelCtx.ui.selections.length, 1, "invalid banner-color arg must still open the picker");
|
|
686
|
+
assert.equal(cancelCtx.ui.notifications.length, 0, "banner-color invalid+cancel must not notify");
|
|
687
|
+
|
|
688
|
+
// (b) /gentle:banner selects the Color row, then the nested picker is
|
|
689
|
+
// cancelled: seeded color unchanged, no notify. The outer select
|
|
690
|
+
// returns the Color row; the nested select returns undefined.
|
|
691
|
+
cancelCtx.ui.notifications.length = 0;
|
|
692
|
+
cancelCtx.ui.selections.length = 0;
|
|
693
|
+
let selectCall = 0;
|
|
694
|
+
cancelCtx.ui.select = async (label, options) => {
|
|
695
|
+
cancelCtx.ui.selections.push({ label, options });
|
|
696
|
+
selectCall += 1;
|
|
697
|
+
// First call: outer "Startup banner" menu -> pick the Color row.
|
|
698
|
+
// Second call: nested color picker -> cancel (undefined).
|
|
699
|
+
return selectCall === 1 ? options[options.length - 1] : undefined;
|
|
700
|
+
};
|
|
701
|
+
await commands.get("gentle:banner").handler("", cancelCtx);
|
|
702
|
+
afterCancel = await readFile(bannerConfigPath, "utf8");
|
|
703
|
+
assert.equal(afterCancel, seededJson, "banner Color-row cancel must not rewrite banner.json");
|
|
704
|
+
assert.equal(cancelCtx.ui.selections.length, 2, "banner Color-row flow must open outer then nested picker");
|
|
705
|
+
assert.equal(cancelCtx.ui.notifications.length, 0, "banner Color-row cancel must not notify");
|
|
706
|
+
|
|
707
|
+
// Sanity: the seeded config round-trips through normalization unchanged,
|
|
708
|
+
// proving the byte equality above is semantic, not a test artifact.
|
|
709
|
+
const reparsed = JSON.parse(await readFile(bannerConfigPath, "utf8"));
|
|
710
|
+
assert.deepEqual(reparsed, seeded, "seeded non-default color must round-trip semantically");
|
|
711
|
+
} finally {
|
|
712
|
+
await rm(cancelPickerCwd, { recursive: true, force: true });
|
|
713
|
+
await rm(join(globalConfigHome, "banner.json"), { force: true });
|
|
714
|
+
}
|
|
715
|
+
|
|
627
716
|
const noUiCwd = await tempWorkspace();
|
|
628
717
|
try {
|
|
629
718
|
for (const handler of hooks.get("session_start")) {
|
|
@@ -640,22 +729,22 @@ async function run() {
|
|
|
640
729
|
"session_start must not install project-local SDD chains",
|
|
641
730
|
);
|
|
642
731
|
assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-apply.md")), true);
|
|
732
|
+
// gentle-pi#311 P5: the Pi-authored adversarial role agents are retired;
|
|
733
|
+
// installation must not (re)create them.
|
|
643
734
|
const installedRefuterPath = join(globalAgentHome, "agents", "review-refuter.md");
|
|
644
|
-
assert.equal(existsSync(installedRefuterPath),
|
|
645
|
-
assert.
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
"
|
|
735
|
+
assert.equal(existsSync(installedRefuterPath), false, "the retired review-refuter agent must not be installed");
|
|
736
|
+
assert.equal(
|
|
737
|
+
existsSync(join(globalAgentHome, "agents", "review-validator.md")),
|
|
738
|
+
false,
|
|
739
|
+
"the retired review-validator agent must not be installed",
|
|
649
740
|
);
|
|
650
|
-
const
|
|
651
|
-
assert.
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
741
|
+
const installedExplorePath = join(globalAgentHome, "agents", "gentle-ai-explore.md");
|
|
742
|
+
assert.equal(existsSync(installedExplorePath), true);
|
|
743
|
+
assert.deepEqual(
|
|
744
|
+
readAgentDefinition(await readFile(installedExplorePath, "utf8")),
|
|
745
|
+
{ name: "gentle-ai-explore", tools: ["read", "grep", "find", "codegraph"] },
|
|
746
|
+
"isolated package installation must activate only the explorer inspection tools",
|
|
655
747
|
);
|
|
656
|
-
assert.match(installedRefuterSource, /complete inferential-severe frozen-row list once/);
|
|
657
|
-
assert.match(installedRefuterSource, /refuted \| corroborated \| inconclusive/);
|
|
658
|
-
assert.doesNotMatch(installedRefuterSource, /general, correctness, impact\/exploitability, or reproducibility/);
|
|
659
748
|
const installedRiskSource = await readFile(
|
|
660
749
|
join(globalAgentHome, "agents", "review-risk.md"),
|
|
661
750
|
"utf8",
|
|
@@ -685,6 +774,13 @@ async function run() {
|
|
|
685
774
|
].join("\n");
|
|
686
775
|
await writeFile(installedRefuterPath, samePathUserRefuter);
|
|
687
776
|
delete managedAssetsManifest.assets["agents/review-refuter.md"];
|
|
777
|
+
// Retirement sweep: a hash-proven package-managed copy of a retired
|
|
778
|
+
// asset is deleted on refresh; the user-authored same-path refuter
|
|
779
|
+
// above must survive because its hash proves nothing.
|
|
780
|
+
const retiredManagedValidatorPath = join(globalAgentHome, "agents", "review-validator.md");
|
|
781
|
+
const retiredManagedValidator = "stale managed validator\n";
|
|
782
|
+
await writeFile(retiredManagedValidatorPath, retiredManagedValidator);
|
|
783
|
+
managedAssetsManifest.assets["agents/review-validator.md"] = sha256(retiredManagedValidator);
|
|
688
784
|
await mkdir(join(globalAgentHome, "subagents"), { recursive: true });
|
|
689
785
|
const userRefuterOverride = join(globalAgentHome, "subagents", "review-refuter.md");
|
|
690
786
|
await writeFile(userRefuterOverride, "user refuter override must stay\n");
|
|
@@ -738,6 +834,24 @@ async function run() {
|
|
|
738
834
|
"user refuter override must stay\n",
|
|
739
835
|
"package refresh must not rewrite or certify an explicit user refuter",
|
|
740
836
|
);
|
|
837
|
+
assert.equal(
|
|
838
|
+
existsSync(retiredManagedValidatorPath),
|
|
839
|
+
false,
|
|
840
|
+
"session refresh must delete a hash-proven package-managed copy of a retired asset",
|
|
841
|
+
);
|
|
842
|
+
const refreshedManagedAssets = JSON.parse(
|
|
843
|
+
await readFile(managedAssetsManifestPath, "utf8"),
|
|
844
|
+
);
|
|
845
|
+
assert.equal(
|
|
846
|
+
refreshedManagedAssets.assets["agents/review-validator.md"],
|
|
847
|
+
undefined,
|
|
848
|
+
"a retired asset must lose package-managed ownership",
|
|
849
|
+
);
|
|
850
|
+
assert.equal(
|
|
851
|
+
refreshedManagedAssets.assets["agents/review-refuter.md"],
|
|
852
|
+
undefined,
|
|
853
|
+
"a user-authored same-path retired asset must stay unowned",
|
|
854
|
+
);
|
|
741
855
|
} finally {
|
|
742
856
|
await rm(noUiCwd, { recursive: true, force: true });
|
|
743
857
|
}
|
|
@@ -1007,6 +1121,82 @@ async function run() {
|
|
|
1007
1121
|
await rm(engramSddCwd, { recursive: true, force: true });
|
|
1008
1122
|
}
|
|
1009
1123
|
|
|
1124
|
+
// Issue #64: selecting engram as the artifact store must not cause
|
|
1125
|
+
// /sdd-init to create openspec/ or openspec/config.yaml.
|
|
1126
|
+
const engramSddInitCwd = await tempWorkspace();
|
|
1127
|
+
try {
|
|
1128
|
+
pi.setActiveTools(["read", "bash", "edit", "write", "mem_save"]);
|
|
1129
|
+
const ctx = createCtx(engramSddInitCwd, true, "engram-sdd-init-session");
|
|
1130
|
+
ctx.ui.select = async (label, options) => {
|
|
1131
|
+
if (label === "SDD artifact store") return "engram";
|
|
1132
|
+
return options[0];
|
|
1133
|
+
};
|
|
1134
|
+
await commands.get("gentle:sdd-preflight").handler("", ctx);
|
|
1135
|
+
await commands.get("sdd-init").handler("", ctx);
|
|
1136
|
+
assert.equal(
|
|
1137
|
+
existsSync(join(engramSddInitCwd, "openspec")),
|
|
1138
|
+
false,
|
|
1139
|
+
"/sdd-init must not create openspec/ when artifactStore is engram",
|
|
1140
|
+
);
|
|
1141
|
+
assert.equal(
|
|
1142
|
+
existsSync(join(engramSddInitCwd, "openspec", "config.yaml")),
|
|
1143
|
+
false,
|
|
1144
|
+
"/sdd-init must not write openspec/config.yaml when artifactStore is engram",
|
|
1145
|
+
);
|
|
1146
|
+
assert.doesNotMatch(
|
|
1147
|
+
ctx.ui.notifications.at(-1).message,
|
|
1148
|
+
/Wrote openspec\/config\.yaml/,
|
|
1149
|
+
"/sdd-init must not announce openspec/config.yaml when artifactStore is engram",
|
|
1150
|
+
);
|
|
1151
|
+
assert.match(
|
|
1152
|
+
ctx.ui.notifications.at(-1).message,
|
|
1153
|
+
/SDD initialized for engram:/,
|
|
1154
|
+
);
|
|
1155
|
+
assert.equal(ctx.ui.notifications.at(-1).level, "info");
|
|
1156
|
+
} finally {
|
|
1157
|
+
pi.setActiveTools(["read", "bash", "edit", "write"]);
|
|
1158
|
+
await rm(engramSddInitCwd, { recursive: true, force: true });
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
// Issue #64 counterpart: the engram skip must stay narrow. Selecting both
|
|
1162
|
+
// still has to create the full openspec/ scaffold and write config.yaml,
|
|
1163
|
+
// so an over-broad skip is caught here instead of in the field.
|
|
1164
|
+
const bothSddInitCwd = await tempWorkspace();
|
|
1165
|
+
try {
|
|
1166
|
+
pi.setActiveTools(["read", "bash", "edit", "write", "mem_save"]);
|
|
1167
|
+
const ctx = createCtx(bothSddInitCwd, true, "both-sdd-init-session");
|
|
1168
|
+
ctx.ui.select = async (label, options) => {
|
|
1169
|
+
if (label === "SDD artifact store") return "both";
|
|
1170
|
+
return options[0];
|
|
1171
|
+
};
|
|
1172
|
+
await commands.get("gentle:sdd-preflight").handler("", ctx);
|
|
1173
|
+
await commands.get("sdd-init").handler("", ctx);
|
|
1174
|
+
assert.equal(
|
|
1175
|
+
existsSync(join(bothSddInitCwd, "openspec", "specs")),
|
|
1176
|
+
true,
|
|
1177
|
+
"/sdd-init must create openspec/specs when artifactStore is both",
|
|
1178
|
+
);
|
|
1179
|
+
assert.equal(
|
|
1180
|
+
existsSync(join(bothSddInitCwd, "openspec", "changes", "archive")),
|
|
1181
|
+
true,
|
|
1182
|
+
"/sdd-init must create openspec/changes/archive when artifactStore is both",
|
|
1183
|
+
);
|
|
1184
|
+
assert.equal(
|
|
1185
|
+
existsSync(join(bothSddInitCwd, "openspec", "config.yaml")),
|
|
1186
|
+
true,
|
|
1187
|
+
"/sdd-init must write openspec/config.yaml when artifactStore is both",
|
|
1188
|
+
);
|
|
1189
|
+
assert.match(
|
|
1190
|
+
ctx.ui.notifications.at(-1).message,
|
|
1191
|
+
/Wrote openspec\/config\.yaml/,
|
|
1192
|
+
"/sdd-init must announce openspec/config.yaml when artifactStore is both",
|
|
1193
|
+
);
|
|
1194
|
+
assert.equal(ctx.ui.notifications.at(-1).level, "info");
|
|
1195
|
+
} finally {
|
|
1196
|
+
pi.setActiveTools(["read", "bash", "edit", "write"]);
|
|
1197
|
+
await rm(bothSddInitCwd, { recursive: true, force: true });
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1010
1200
|
const directEngramToolCwd = await tempWorkspace();
|
|
1011
1201
|
try {
|
|
1012
1202
|
pi.setActiveTools(["read", "bash", "edit", "write", "engram_mem_save"]);
|
|
@@ -1699,6 +1889,44 @@ async function run() {
|
|
|
1699
1889
|
const restoredAgent = await readFile(join(modelsCwd, ".pi", "agents", "sdd-apply.md"), "utf8");
|
|
1700
1890
|
assert.match(restoredAgent, /model: restore\/provider/);
|
|
1701
1891
|
assert.match(restoredAgent, /thinking: high/);
|
|
1892
|
+
|
|
1893
|
+
// issue #286: `thinking: "max"` must survive save normalization and
|
|
1894
|
+
// reach both subagents.json (effort) and agent frontmatter (thinking).
|
|
1895
|
+
ctx.ui.custom = () =>
|
|
1896
|
+
Promise.resolve({
|
|
1897
|
+
type: "save",
|
|
1898
|
+
config: { "sdd-apply": { model: "openai/gpt-5", thinking: "max" } },
|
|
1899
|
+
});
|
|
1900
|
+
await commands.get("gentle:models").handler("", ctx);
|
|
1901
|
+
const maxSavedConfig = JSON.parse(await readFile(globalModelsPath, "utf8"));
|
|
1902
|
+
assert.equal(maxSavedConfig["sdd-apply"].thinking, "max");
|
|
1903
|
+
const maxSubagents = JSON.parse(
|
|
1904
|
+
await readFile(join(modelsCwd, ".pi", "subagents.json"), "utf8"),
|
|
1905
|
+
);
|
|
1906
|
+
assert.equal(maxSubagents.model_profiles["sdd-apply"].effort, "max");
|
|
1907
|
+
const maxApplyAgent = await readFile(
|
|
1908
|
+
join(modelsCwd, ".pi", "agents", "sdd-apply.md"),
|
|
1909
|
+
"utf8",
|
|
1910
|
+
);
|
|
1911
|
+
assert.match(maxApplyAgent, /thinking: max/);
|
|
1912
|
+
|
|
1913
|
+
// issue #286: effort picker must offer `max` after `xhigh` and save it.
|
|
1914
|
+
let maxPickerCalls = 0;
|
|
1915
|
+
ctx.ui.custom = (factory) =>
|
|
1916
|
+
new Promise((resolve) => {
|
|
1917
|
+
maxPickerCalls += 1;
|
|
1918
|
+
const panel = factory(null, null, null, resolve);
|
|
1919
|
+
if (maxPickerCalls === 1) {
|
|
1920
|
+
panel.handleInput(kittyE); // open effort picker (set-all row)
|
|
1921
|
+
for (let i = 0; i < 7; i++) panel.handleInput("j"); // max
|
|
1922
|
+
panel.handleInput("\r");
|
|
1923
|
+
panel.handleInput("\u0013"); // ctrl+s saves the draft
|
|
1924
|
+
return;
|
|
1925
|
+
}
|
|
1926
|
+
});
|
|
1927
|
+
await commands.get("gentle:models").handler("", ctx);
|
|
1928
|
+
const pickerMaxConfig = JSON.parse(await readFile(globalModelsPath, "utf8"));
|
|
1929
|
+
assert.equal(pickerMaxConfig["sdd-apply"].thinking, "max");
|
|
1702
1930
|
} finally {
|
|
1703
1931
|
await rm(modelsCwd, { recursive: true, force: true });
|
|
1704
1932
|
await rm(globalModelsPath, { force: true });
|
|
@@ -5,7 +5,6 @@ import test from "node:test";
|
|
|
5
5
|
|
|
6
6
|
const repoRoot = process.cwd();
|
|
7
7
|
const assetsAgentsDir = join(repoRoot, "assets", "agents");
|
|
8
|
-
const REVIEW_REFUTER_TOOLS = ["read", "grep", "find"];
|
|
9
8
|
const GENERIC_ROLE_TOOLS: Record<string, string[]> = {
|
|
10
9
|
"gentle-ai-explore.md": ["read", "grep", "find", "codegraph"],
|
|
11
10
|
"gentle-ai-worker.md": ["read", "grep", "find", "edit", "write", "bash", "mem_save"],
|
|
@@ -61,18 +60,18 @@ function assertGenericRoleBody(fileName: string, source: string): void {
|
|
|
61
60
|
}
|
|
62
61
|
|
|
63
62
|
const requiredToolsByAgent: Record<string, string[]> = {
|
|
64
|
-
"sdd-apply.md": ["read", "grep", "
|
|
65
|
-
"sdd-archive.md": ["read", "grep", "
|
|
66
|
-
"sdd-design.md": ["read", "grep", "
|
|
67
|
-
"sdd-explore.md": ["read", "grep", "
|
|
68
|
-
"sdd-init.md": ["read", "grep", "
|
|
69
|
-
"sdd-onboard.md": ["read", "grep", "
|
|
70
|
-
"sdd-proposal.md": ["read", "grep", "
|
|
71
|
-
"sdd-spec.md": ["read", "grep", "
|
|
72
|
-
"sdd-status.md": ["read", "grep", "
|
|
73
|
-
"sdd-sync.md": ["read", "grep", "
|
|
74
|
-
"sdd-tasks.md": ["read", "grep", "
|
|
75
|
-
"sdd-verify.md": ["read", "grep", "
|
|
63
|
+
"sdd-apply.md": ["read", "grep", "find", "edit", "write", "bash", "mem_search", "mem_get_observation", "mem_save", "mem_update"],
|
|
64
|
+
"sdd-archive.md": ["read", "grep", "find", "edit", "write", "bash", "mem_search", "mem_get_observation", "mem_save"],
|
|
65
|
+
"sdd-design.md": ["read", "grep", "find", "edit", "write", "mem_search", "mem_get_observation", "mem_save"],
|
|
66
|
+
"sdd-explore.md": ["read", "grep", "find", "edit", "write", "mem_save"],
|
|
67
|
+
"sdd-init.md": ["read", "grep", "find", "edit", "write", "bash", "mem_search", "mem_get_observation", "mem_save", "mem_update"],
|
|
68
|
+
"sdd-onboard.md": ["read", "grep", "find", "edit", "write", "bash", "mem_search", "mem_get_observation", "mem_save", "mem_update"],
|
|
69
|
+
"sdd-proposal.md": ["read", "grep", "find", "edit", "write", "mem_search", "mem_get_observation", "mem_save"],
|
|
70
|
+
"sdd-spec.md": ["read", "grep", "find", "edit", "write", "mem_search", "mem_get_observation", "mem_save"],
|
|
71
|
+
"sdd-status.md": ["read", "grep", "find", "bash", "mem_search", "mem_get_observation"],
|
|
72
|
+
"sdd-sync.md": ["read", "grep", "find", "edit", "write", "bash", "mem_search", "mem_get_observation", "mem_save", "mem_update"],
|
|
73
|
+
"sdd-tasks.md": ["read", "grep", "find", "edit", "write", "mem_search", "mem_get_observation", "mem_save"],
|
|
74
|
+
"sdd-verify.md": ["read", "grep", "find", "edit", "write", "bash", "mem_search", "mem_get_observation", "mem_save"],
|
|
76
75
|
};
|
|
77
76
|
|
|
78
77
|
test("SDD package agents declare role-appropriate tools as YAML arrays", () => {
|
|
@@ -123,25 +122,11 @@ test("generic non-SDD agents declare exact role tool allowlists", () => {
|
|
|
123
122
|
}
|
|
124
123
|
});
|
|
125
124
|
|
|
126
|
-
test("
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
const frontmatter = readFrontmatter(path);
|
|
134
|
-
assert.match(frontmatter, /^name:\s*review-refuter$/m);
|
|
135
|
-
for (const forbidden of [
|
|
136
|
-
"bash",
|
|
137
|
-
"edit",
|
|
138
|
-
"write",
|
|
139
|
-
"task",
|
|
140
|
-
"subagent",
|
|
141
|
-
"subagent_run",
|
|
142
|
-
"mem_save",
|
|
143
|
-
"mem_update",
|
|
144
|
-
]) {
|
|
145
|
-
assert.doesNotMatch(frontmatter, new RegExp(`^ - ${forbidden}$`, "m"));
|
|
125
|
+
test("the retired Pi adversarial role agents are not packaged", () => {
|
|
126
|
+
// gentle-pi#311 P5: the refuter and targeted validator verdicts execute
|
|
127
|
+
// through Go-owned pi processes via provider-rendered self-contained
|
|
128
|
+
// vectors; no Pi agent definition may reintroduce a Pi-authored verdict.
|
|
129
|
+
for (const retired of ["review-refuter.md", "review-validator.md"]) {
|
|
130
|
+
assert.ok(!existsSync(join(assetsAgentsDir, retired)), `${retired} must stay deleted`);
|
|
146
131
|
}
|
|
147
132
|
});
|
|
@@ -16,6 +16,7 @@ const PREFIXED_NAMES: Record<string, string> = {
|
|
|
16
16
|
"chained-pr": "gentle-ai-chained-pr",
|
|
17
17
|
"issue-creation": "gentle-ai-issue-creation",
|
|
18
18
|
"judgment-day": "gentle-ai-judgment-day",
|
|
19
|
+
"rdd-defect-workflow": "gentle-ai-rdd-defect-workflow",
|
|
19
20
|
"skill-creator": "gentle-ai-skill-creator",
|
|
20
21
|
"skill-improver": "gentle-ai-skill-improver",
|
|
21
22
|
};
|