gentle-pi 2.2.0 → 2.4.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 +76 -79
- package/assets/agents/review-readability.md +0 -2
- package/assets/agents/review-reliability.md +0 -2
- package/assets/agents/review-resilience.md +0 -2
- package/assets/agents/review-risk.md +0 -2
- package/assets/agents/sdd-apply.md +5 -3
- package/assets/agents/sdd-explore.md +1 -0
- package/assets/agents/sdd-proposal.md +2 -0
- package/assets/agents/sdd-research.md +54 -0
- package/assets/agents/sdd-status.md +4 -4
- package/assets/agents/sdd-tasks.md +2 -3
- package/assets/agents/sdd-verify.md +21 -1
- package/assets/chains/sdd-full.chain.md +1 -1
- package/assets/chains/sdd-verify.chain.md +1 -1
- package/assets/orchestrator-delegation.md +56 -249
- package/assets/orchestrator-memory.md +2 -0
- package/assets/orchestrator.md +20 -40
- package/assets/sdd-orchestrator-workflow.md +30 -37
- package/assets/support/sdd-status-contract.md +7 -7
- package/contracts/review-integration/v1/schemas/transition-execution.schema.json +42 -0
- package/contracts/review-integration/v2/schemas/last-event-closure.schema.json +66 -0
- package/contracts/review-integration/v2/schemas/opencode-provider-role.schema.json +14 -0
- package/contracts/review-provider-contract-mirror/provider-contract.lock.json +8 -7
- package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/README.md +10 -0
- package/contracts/review-provider-contract-mirror/v1.2.0/bundle/manifest.json +74 -0
- package/contracts/review-provider-contract-mirror/v1.2.0/bundle/orchestration/pi.md +53 -0
- package/contracts/review-provider-contract-mirror/v1.2.0/bundle/schemas/targeted-validator.schema.json +1 -0
- package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/generated/provider-capabilities.baseline.json +9 -2
- package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/generated/provider-roles.baseline.json +2 -2
- package/docs/native-authority-architecture.md +9 -11
- package/docs/review-integration.md +27 -373
- package/extensions/ask-user-choice.ts +151 -0
- package/extensions/gentle-ai.ts +2349 -3785
- package/extensions/quiet-tools.ts +515 -32
- package/extensions/sdd-init.ts +4 -8
- package/lib/agent-home.ts +8 -0
- package/lib/gentle-ai-renderer.ts +70 -0
- package/lib/model-routing-authority.ts +133 -0
- package/lib/native-review-cli.ts +387 -940
- package/lib/opaque-pi-reviewer-adapter.ts +284 -0
- package/lib/provider-contract-bundle.ts +88 -6
- package/lib/review-candidate-view.ts +341 -132
- package/lib/review-host-relay.ts +324 -96
- package/lib/review-integration-v2.ts +868 -307
- package/lib/review-last-event-controller.ts +35 -0
- package/lib/sdd-preflight.ts +179 -68
- package/lib/sdd-status.ts +66 -111
- package/lib/terminal-theme.ts +1 -1
- package/package.json +83 -82
- package/runtime/gentle-ai-binary.mjs +1 -1
- package/runtime/native-review-cli.mjs +337 -890
- package/runtime/review-integration-v2.mjs +816 -255
- package/runtime/review-relay-contract.mjs +1 -1
- package/scripts/{build-git-commit-transaction-runner.mjs → build-runtime-modules.mjs} +4 -5
- package/scripts/gentle-ai-installer.mjs +75 -22
- package/scripts/maintainer/provider-relay-matrix.mjs +195 -11
- package/scripts/test-packed-runner.mjs +4 -7
- package/scripts/verify-package-files.mjs +23 -23
- package/skills/_shared/review-ledger-contract.md +14 -14
- package/skills/chained-pr/SKILL.md +3 -0
- package/skills/cognitive-doc-design/SKILL.md +1 -1
- package/skills/comment-writer/SKILL.md +1 -1
- package/skills/gentle-ai/SKILL.md +7 -74
- package/skills/issue-creation/SKILL.md +53 -93
- package/skills/judgment-day/SKILL.md +5 -7
- package/skills/rdd-defect-workflow/SKILL.md +3 -3
- package/skills/release/SKILL.md +3 -3
- package/skills/skill-registry/SKILL.md +1 -1
- package/skills/work-unit-commits/SKILL.md +3 -1
- package/tests/artifact-language.test.ts +24 -11
- package/tests/ask-user-choice.test.ts +264 -0
- package/tests/codegraph-tools.test.ts +3 -3
- package/tests/crosslane/cross-lane.mjs +15 -1168
- package/tests/delegated-key-learnings-contract.test.ts +8 -6
- package/tests/devbinary/native-review-parity.devtest.ts +167 -250
- package/tests/devbinary/pi-host-relay.devtest.ts +867 -0
- package/tests/fixtures/devbinary/last-event-capture-correction-plan.captured.json +10 -0
- package/tests/fixtures/devbinary/last-event-capture-refuter-approved.captured.json +20 -0
- package/tests/fixtures/devbinary/last-event-capture-refuter-correction-required.captured.json +26 -0
- package/tests/fixtures/devbinary/last-event-capture-result-approved.captured.json +8 -0
- package/tests/fixtures/devbinary/last-event-capture-result-correction-required.captured.json +26 -0
- package/tests/fixtures/devbinary/last-event-capture-validation-approved.captured.json +8 -0
- package/tests/fixtures/devbinary/last-event-closure.provenance.md +13 -0
- package/tests/fixtures/devbinary/review-acknowledged-v1.captured.json +9 -0
- package/tests/fixtures/devbinary/review-acknowledged.provenance.md +31 -0
- package/tests/fixtures/devbinary/start-v3-zero-lens-closed.captured.json +21 -0
- package/tests/fixtures/native-review-cli/v2.5.0-rc.1/PROVENANCE.txt +15 -0
- package/tests/fixtures/provider-contract-bundle/v1.2.0/README.md +22 -0
- package/{contracts/review-provider-contract-mirror/v1.1.0/bundle → tests/fixtures/provider-contract-bundle/v1.2.0}/manifest.json +11 -2
- package/tests/fixtures/provider-contract-bundle/v1.2.0/orchestration/pi.md +97 -0
- package/tests/fixtures/provider-contract-bundle/v1.2.0/schemas/lens.schema.json +16 -0
- package/tests/fixtures/provider-contract-bundle/v1.2.0/schemas/refuter.schema.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.2.0/vectors/lens.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.2.0/vectors/refuter.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.2.0/vectors/targeted-validator.json +1 -0
- package/tests/gentle-ai-binary.test.ts +83 -13
- package/tests/gentle-ai-dev-binary.test.ts +51 -228
- package/tests/gentle-ai-installer.test.ts +137 -47
- package/tests/gentle-ai.test.ts +821 -131
- package/tests/gentle-theme.test.ts +133 -0
- package/tests/issue-creation-skill.test.ts +103 -0
- package/tests/maintainer/provider-relay.maintest.ts +340 -4
- package/tests/model-routing-authority.test.ts +257 -0
- package/tests/native-review-capability-contract.test.ts +45 -1
- package/tests/native-review-cli.test.ts +586 -962
- package/tests/native-review-consent.test.ts +150 -66
- package/tests/native-review-parity-runtime.test.ts +80 -359
- package/tests/native-review-parity.test.ts +674 -925
- package/tests/opaque-pi-reviewer-adapter.test.ts +266 -0
- package/tests/orchestrator-budget.test.ts +132 -88
- package/tests/orchestrator-rdd-ownership.test.ts +103 -0
- package/tests/package-manifest.test.ts +152 -36
- package/tests/provider-contract-bundle.test.ts +76 -0
- package/tests/provider-contract-mirror.test.ts +19 -0
- package/tests/provider-defect-handoff.test.ts +15 -118
- package/tests/quiet-tool-rendering.test.ts +1055 -28
- package/tests/review-agent-end-preflight.test.ts +408 -0
- package/tests/review-authority-recovery-docs.test.ts +1 -2
- package/tests/review-candidate-view.test.ts +409 -3
- package/tests/review-compact-contract.test.ts +4 -30
- package/tests/review-contract-prompt.test.ts +139 -0
- package/tests/review-controller-lock-status.test.ts +2 -2
- package/tests/review-controller-native-recovery.test.ts +363 -1450
- package/tests/review-controller-native-routing.test.ts +1251 -5642
- package/tests/review-controller-retired-ops.test.ts +1 -1
- package/tests/review-controller-workspace-root.test.ts +222 -71
- package/tests/review-controller.test.ts +26 -816
- package/tests/review-corrected-finalize-binding.test.ts +112 -153
- package/tests/review-dispatch-hydration-gap.test.ts +1 -53
- package/tests/review-gate.test.ts +0 -45
- package/tests/review-host-relay-restart-parity.test.ts +360 -0
- package/tests/review-host-relay-routing.test.ts +328 -91
- package/tests/review-host-relay.test.ts +436 -14
- package/tests/review-integration-v2-forward.test.ts +623 -553
- package/tests/review-integration-v2.test.ts +239 -165
- package/tests/review-last-event-closure.test.ts +408 -0
- package/tests/review-ledger-contract.test.ts +97 -35
- package/tests/review-recovered-lineage-routing.test.ts +0 -47
- package/tests/review-relay-transport-agent.test.ts +119 -46
- package/tests/review-snapshot.test.ts +3 -2
- package/tests/runtime-harness.mjs +362 -196
- package/tests/sdd-agent-tools.test.ts +36 -0
- package/tests/sdd-preflight.test.ts +81 -15
- package/tests/sdd-status.test.ts +109 -110
- package/tests/skill-collision-prefixes.test.ts +5 -8
- package/tests/writer-edit-surface-scope.test.ts +366 -0
- package/themes/Gentleman-Cute.json +94 -0
- package/themes/Gentleman-Sexy.json +92 -0
- package/lib/git-commit-transaction.ts +0 -861
- package/runtime/git-commit-transaction.mjs +0 -862
- package/scripts/run-git-commit-transaction.mjs +0 -35
- package/tests/fixtures/native-review-cli/v2.1.3/sdd-status-engram.json +0 -139
- package/tests/fixtures/native-review-cli/v2.1.3/sdd-status.json +0 -200
- package/tests/git-commit-transaction.test.ts +0 -530
- /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/schemas/lens.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/schemas/refuter.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/vectors/lens.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/vectors/refuter.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/vectors/targeted-validator.json +0 -0
- /package/{contracts/review-provider-contract-mirror/v1.1.0/bundle → tests/fixtures/provider-contract-bundle/v1.2.0}/schemas/targeted-validator.schema.json +0 -0
|
@@ -15,13 +15,13 @@ This skill guides public collaboration. It does not grant issue approval, label,
|
|
|
15
15
|
|
|
16
16
|
## Hard Rules
|
|
17
17
|
|
|
18
|
-
-
|
|
18
|
+
- Review and Judgment Day evidence is review-only. Pi never mints delivery authority: ordinary commit, push, PR, and release always follow repository policy, regardless of RDD mode.
|
|
19
19
|
- Require an approved issue (`status:approved`) and clean current `main` reproduction before implementation. Audit existing PRs for supersession or conflict; stop or narrow stale claims.
|
|
20
20
|
- Group by causal authority invariant. Use one issue and one PR or explicit chain per independent invariant and rollback boundary. Split independent causes; never merge a superseded or conflicting authority line.
|
|
21
21
|
- Inventory every operator flow claimed by the issue or PR, including entry, mode, environment, expectation, and negative controls. Require one truthful black-box bench journey per CLI or lifecycle flow, or actual runtime E2E proof when the core bench cannot represent it. Synthetic proxy coverage never proves another runtime.
|
|
22
22
|
- Use CodeGraph-first impact mapping, a dedicated worktree, and behavior-first tests. Run source-mutating normalization before candidate freeze.
|
|
23
23
|
- Forecast authored changes before edits. The hard limit is 400 additions plus deletions; above it, STOP for a chain or explicit maintainer-approved exception.
|
|
24
|
-
-
|
|
24
|
+
- When RDD is enabled, bind review receipts, lineage, correction, and recovery evidence to the exact candidate. Keep bounded review defects in one correction transaction; never treat that evidence as delivery authority.
|
|
25
25
|
- Require independent read-only candidate validation before publication. Validation cannot edit source or authority; findings require a new candidate.
|
|
26
26
|
- Keep communication humane and evidence-based. Repository labels and workflow metadata are maintainer-owned, never evidence of contributor blame.
|
|
27
27
|
|
|
@@ -29,7 +29,7 @@ This skill guides public collaboration. It does not grant issue approval, label,
|
|
|
29
29
|
|
|
30
30
|
| Condition | Action |
|
|
31
31
|
| --- | --- |
|
|
32
|
-
| RDD
|
|
32
|
+
| Any RDD mode | Review evidence remains review-only; ordinary commit, push, PR, and release follow repository policy with no Pi delivery authority. |
|
|
33
33
|
| Issue gate or reproduction fails | Wait, stop, or narrow with evidence. |
|
|
34
34
|
| Invariant or rollback is independent | Separate issue and authoritative PR line. |
|
|
35
35
|
| Core bench fits / does not fit | Bench journey / actual runtime E2E; never proxy. |
|
package/skills/release/SKILL.md
CHANGED
|
@@ -17,8 +17,8 @@ Use this skill when preparing, publishing, or verifying a `gentle-pi` release.
|
|
|
17
17
|
- npm publishing MUST go through the GitHub Actions workflow `.github/workflows/publish.yml` so provenance, environment protection, and registry credentials are controlled by GitHub.
|
|
18
18
|
- Dispatch the trusted workflow definition from protected default `main`, never from a release tag. Its only caller input is the exact annotated version tag.
|
|
19
19
|
- Use a clean worktree for release commits. Do not package unrelated local files or scratch artifacts.
|
|
20
|
-
-
|
|
21
|
-
-
|
|
20
|
+
- Review outcomes are informational. Release delivery follows ordinary repository policy and must not be blocked, authorized, or rewritten by RDD.
|
|
21
|
+
- Never infer the release tag target from local `HEAD`; use the freshly fetched `origin/main` commit and the repository's normal release safeguards.
|
|
22
22
|
- Never skip package verification. The publish workflow runs verification again, but local validation should still pass before tagging.
|
|
23
23
|
|
|
24
24
|
## Release Procedure
|
|
@@ -114,7 +114,7 @@ Use this skill when preparing, publishing, or verifying a `gentle-pi` release.
|
|
|
114
114
|
|
|
115
115
|
## Failure Handling
|
|
116
116
|
|
|
117
|
-
- A publication failure
|
|
117
|
+
- A publication failure is handled through ordinary repository policy. It does not reopen or alter a review lineage.
|
|
118
118
|
- Never attempt or retry `npm publish` locally. Re-dispatch from trusted `main` only when the same tag still targets the current remote `main` and the failure was publication-only.
|
|
119
119
|
- If remote `main` advances, do not move or recreate the existing tag. Prepare a new release commit/version and create a new annotated version tag.
|
|
120
120
|
- If the workflow fails, inspect logs with:
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
name: work-unit-commits
|
|
2
|
+
name: gentle-ai-work-unit-commits
|
|
3
3
|
description: "Plan commits as reviewable work units. Trigger: implementation, commit splitting, chained PRs, or keeping tests and docs with code."
|
|
4
4
|
license: Apache-2.0
|
|
5
5
|
metadata:
|
|
@@ -30,6 +30,7 @@ Use it for:
|
|
|
30
30
|
| Tell a story | A reviewer should understand why each commit exists from its diff and message. |
|
|
31
31
|
| Future PR-ready | Each commit should be a candidate chained PR when the change grows. |
|
|
32
32
|
| SDD workload guard | If SDD tasks forecast a >400-line change, group commits into chained PR slices before implementation. |
|
|
33
|
+
| Budget is not code-golf | Never shrink a diff by deleting comments, blank lines, docs, or tests, or by compressing code, to fit the review budget (400 by default, or the session `review_budget_lines`). Slice by work unit or report the overage. |
|
|
33
34
|
|
|
34
35
|
## Work Unit Checklist
|
|
35
36
|
|
|
@@ -66,6 +67,7 @@ When `sdd-tasks` produces a Review Workload Forecast:
|
|
|
66
67
|
- Low risk: keep work-unit commits inside one PR.
|
|
67
68
|
- Medium risk: commit by work unit and monitor changed lines before PR creation.
|
|
68
69
|
- High risk: follow SDD `delivery_strategy` — ask on `ask-on-risk`, auto-slice on `auto-chain`, require `size:exception` on over-budget `single-pr`, or record accepted `size:exception` on `exception-ok`.
|
|
70
|
+
- Splitting is bounded: after one honest slicing pass, if no cohesive work-unit split fits the budget, stop and report the smallest honest count with a `size:exception` recommendation. Do not iterate shrinking the code to reach the number.
|
|
69
71
|
|
|
70
72
|
Each SDD work unit should map cleanly to a commit or PR with:
|
|
71
73
|
|
|
@@ -67,14 +67,15 @@ test("rendered SDD preflight prompt is English artifact copy", () => {
|
|
|
67
67
|
const prefs: SddPreflightPreferences = {
|
|
68
68
|
executionMode: "interactive",
|
|
69
69
|
artifactStore: "openspec",
|
|
70
|
-
chainedPrStrategy: "ask-
|
|
70
|
+
chainedPrStrategy: "ask-on-risk",
|
|
71
71
|
reviewBudgetLines: 400,
|
|
72
72
|
engramAvailable: false,
|
|
73
73
|
prompted: true,
|
|
74
74
|
};
|
|
75
75
|
const prompt = renderSddPreflightPrompt(prefs);
|
|
76
76
|
|
|
77
|
-
assert.match(prompt, /
|
|
77
|
+
assert.match(prompt, /These SDD preferences are explicit current-session choices/);
|
|
78
|
+
assert.match(prompt, /Delivery strategy: ask-on-risk/);
|
|
78
79
|
assert.match(prompt, /Review budget: 400 changed lines/);
|
|
79
80
|
assert.match(prompt, /complete only the current SDD phase/i);
|
|
80
81
|
assert.match(prompt, /Do not start the next SDD phase/i);
|
|
@@ -82,9 +83,15 @@ test("rendered SDD preflight prompt is English artifact copy", () => {
|
|
|
82
83
|
assert.match(prompt, /offer the user a proposal question round/i);
|
|
83
84
|
assert.match(prompt, /business rules, implications, impact, edge cases/i);
|
|
84
85
|
assert.match(prompt, /second question round/i);
|
|
86
|
+
assert.match(prompt, /explicit acceptance of `size:exception`/);
|
|
87
|
+
assert.match(prompt, /human-controlled consent, authorization, security, destructive\/publishing/);
|
|
85
88
|
for (const pattern of SPANISH_PREFLIGHT_COPY) {
|
|
86
89
|
assert.doesNotMatch(prompt, pattern);
|
|
87
90
|
}
|
|
91
|
+
|
|
92
|
+
const headless = renderSddPreflightPrompt({ ...prefs, executionMode: "auto", prompted: false });
|
|
93
|
+
assert.match(headless, /canonical defaults or persisted choices/);
|
|
94
|
+
assert.match(headless, /ambiguous-scope/);
|
|
88
95
|
});
|
|
89
96
|
|
|
90
97
|
test("orchestrator Memory Contract carries the Engram memory lifecycle rule", async () => {
|
|
@@ -159,7 +166,12 @@ test("orchestrator lazy-loads detailed SDD workflow", async () => {
|
|
|
159
166
|
const workflow = await readFile(join(ROOT, "assets/sdd-orchestrator-workflow.md"), "utf8");
|
|
160
167
|
|
|
161
168
|
assert.match(orchestrator, /## SDD Workflow \(lazy-loaded\)/);
|
|
162
|
-
assert.match(orchestrator,
|
|
169
|
+
assert.match(orchestrator, /Package assets root: `\{\{GENTLE_PI_ASSETS_ROOT\}\}`\. Lazy asset paths below are relative to this root\./);
|
|
170
|
+
assert.match(orchestrator, /`sdd-orchestrator-workflow\.md`/);
|
|
171
|
+
assert.doesNotMatch(orchestrator, /\{\{GENTLE_PI_SDD_WORKFLOW_PATH\}\}/);
|
|
172
|
+
assert.match(orchestrator, /injected `## SDD Session Preflight` block or a canonical-authority resolution/);
|
|
173
|
+
assert.match(orchestrator, /Defaults and capability constraints may resolve fields without confirmation prompts/);
|
|
174
|
+
assert.doesNotMatch(orchestrator, /or an explicit user answer covering the preflight choices/);
|
|
163
175
|
assert.doesNotMatch(orchestrator, /## Native SDD Dispatcher/);
|
|
164
176
|
assert.match(workflow, /## Native SDD Dispatcher/);
|
|
165
177
|
assert.match(workflow, /## SDD Status Contract/);
|
|
@@ -188,7 +200,7 @@ test("persistent harness prompt assets do not hardcode Spanish SDD artifact copy
|
|
|
188
200
|
assert.deepEqual(failures, []);
|
|
189
201
|
});
|
|
190
202
|
|
|
191
|
-
test("SDD assets
|
|
203
|
+
test("SDD assets route completed implementation directly through verify, sync, and archive", async () => {
|
|
192
204
|
const [tasks, apply, status, contract, chain] = await Promise.all([
|
|
193
205
|
readFile(join(ROOT, "assets/agents/sdd-tasks.md"), "utf8"),
|
|
194
206
|
readFile(join(ROOT, "assets/agents/sdd-apply.md"), "utf8"),
|
|
@@ -196,15 +208,16 @@ test("SDD assets define ownership markers and yield post-apply lifecycle to the
|
|
|
196
208
|
readFile(join(ROOT, "assets/support/sdd-status-contract.md"), "utf8"),
|
|
197
209
|
readFile(join(ROOT, "assets/chains/sdd-full.chain.md"), "utf8"),
|
|
198
210
|
]);
|
|
211
|
+
const assets = [tasks, apply, status, contract, chain].join("\n");
|
|
199
212
|
|
|
200
213
|
assert.match(tasks, /<!-- sdd-owner: implementation -->/);
|
|
201
|
-
assert.match(
|
|
202
|
-
assert.match(
|
|
203
|
-
assert.match(
|
|
204
|
-
assert.match(
|
|
205
|
-
assert.
|
|
206
|
-
assert.
|
|
207
|
-
assert.
|
|
214
|
+
assert.match(apply, /next_recommended: "sdd-verify"/);
|
|
215
|
+
assert.match(status, /verify.*ready/i);
|
|
216
|
+
assert.match(contract, /apply.*verify.*sync.*archive/is);
|
|
217
|
+
assert.match(chain, /apply.*verification/is);
|
|
218
|
+
assert.doesNotMatch(assets, /<!-- sdd-owner: parent -->/);
|
|
219
|
+
assert.doesNotMatch(assets, /parent-lifecycle/);
|
|
220
|
+
assert.doesNotMatch(assets, /approved receipt|bounded review/i);
|
|
208
221
|
assert.doesNotMatch(chain, /## sdd-review/);
|
|
209
222
|
});
|
|
210
223
|
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import askUserChoice from "../extensions/ask-user-choice.ts";
|
|
4
|
+
|
|
5
|
+
interface ChoiceResult {
|
|
6
|
+
content: Array<{ type: string; text: string }>;
|
|
7
|
+
details: Record<string, unknown>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface ChoiceOptionSchema {
|
|
11
|
+
additionalProperties?: boolean;
|
|
12
|
+
properties?: Record<string, unknown>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface ChoiceParameters {
|
|
16
|
+
additionalProperties?: boolean;
|
|
17
|
+
properties?: {
|
|
18
|
+
question?: unknown;
|
|
19
|
+
options?: {
|
|
20
|
+
minItems?: number;
|
|
21
|
+
maxItems?: number;
|
|
22
|
+
items?: ChoiceOptionSchema;
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface ChoiceTool {
|
|
28
|
+
name: string;
|
|
29
|
+
parameters: ChoiceParameters;
|
|
30
|
+
execute: (...args: unknown[]) => Promise<ChoiceResult>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface ChoiceLifecycleEvent {
|
|
34
|
+
channel: string;
|
|
35
|
+
data: { active: boolean };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
type BeforeAgentStart = (event: unknown, ctx: { mode: string }) => void | Promise<void>;
|
|
39
|
+
|
|
40
|
+
function registerChoiceTool(
|
|
41
|
+
initialTools: string[] = [],
|
|
42
|
+
onLifecycleEvent?: (event: ChoiceLifecycleEvent) => void,
|
|
43
|
+
) {
|
|
44
|
+
let activeTools = initialTools;
|
|
45
|
+
let runtimeActionsAllowed = false;
|
|
46
|
+
let getActiveToolsCalls = 0;
|
|
47
|
+
let setActiveToolsCalls = 0;
|
|
48
|
+
let tool: ChoiceTool | undefined;
|
|
49
|
+
const hooks: BeforeAgentStart[] = [];
|
|
50
|
+
const registeredToolNames: string[] = [];
|
|
51
|
+
const emittedEvents: ChoiceLifecycleEvent[] = [];
|
|
52
|
+
const pi = {
|
|
53
|
+
getActiveTools: () => {
|
|
54
|
+
getActiveToolsCalls++;
|
|
55
|
+
if (!runtimeActionsAllowed) {
|
|
56
|
+
throw new Error("runtime actions are unavailable while the extension is loading");
|
|
57
|
+
}
|
|
58
|
+
return activeTools;
|
|
59
|
+
},
|
|
60
|
+
setActiveTools: (names: string[]) => {
|
|
61
|
+
setActiveToolsCalls++;
|
|
62
|
+
if (!runtimeActionsAllowed) {
|
|
63
|
+
throw new Error("runtime actions are unavailable while the extension is loading");
|
|
64
|
+
}
|
|
65
|
+
activeTools = names;
|
|
66
|
+
},
|
|
67
|
+
registerTool: (candidate: unknown) => {
|
|
68
|
+
tool = candidate as ChoiceTool;
|
|
69
|
+
registeredToolNames.push(tool.name);
|
|
70
|
+
},
|
|
71
|
+
on: (event: string, handler: BeforeAgentStart) => {
|
|
72
|
+
if (event === "before_agent_start") hooks.push(handler);
|
|
73
|
+
},
|
|
74
|
+
events: {
|
|
75
|
+
emit(channel: string, data: { active: boolean }) {
|
|
76
|
+
const event = { channel, data };
|
|
77
|
+
emittedEvents.push(event);
|
|
78
|
+
onLifecycleEvent?.(event);
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
askUserChoice(pi as never);
|
|
83
|
+
assert.ok(tool, "ask_user_choice must register");
|
|
84
|
+
return {
|
|
85
|
+
hooks,
|
|
86
|
+
tool,
|
|
87
|
+
registeredToolNames,
|
|
88
|
+
activeTools: () => [...activeTools],
|
|
89
|
+
emittedEvents: () => [...emittedEvents],
|
|
90
|
+
runtimeActionCalls: () => ({ getActiveTools: getActiveToolsCalls, setActiveTools: setActiveToolsCalls }),
|
|
91
|
+
allowRuntimeActions: () => {
|
|
92
|
+
runtimeActionsAllowed = true;
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const options = [
|
|
98
|
+
{ label: "Authorize observed hash", description: "Accept the baseline hash observed in this runtime.", value: "authorize_observed_hash" },
|
|
99
|
+
{ label: "Preserve requested hash", description: "Keep the hash from the original request.", value: "preserve_requested_hash" },
|
|
100
|
+
];
|
|
101
|
+
|
|
102
|
+
function tuiContext(inputs: readonly string[], rendered: { value: string }) {
|
|
103
|
+
return {
|
|
104
|
+
mode: "tui",
|
|
105
|
+
ui: {
|
|
106
|
+
custom: async (factory: (tui: { requestRender(): void }, theme: { fg(_color: string, text: string): string; bold(text: string): string }, keybindings: unknown, done: (value: unknown) => void) => { render(width: number): string[]; handleInput(data: string): void }) => {
|
|
107
|
+
let result: unknown;
|
|
108
|
+
const component = factory(
|
|
109
|
+
{ requestRender() {} },
|
|
110
|
+
{ fg: (_color, text) => text, bold: (text) => text },
|
|
111
|
+
{},
|
|
112
|
+
(value) => {
|
|
113
|
+
result = value;
|
|
114
|
+
},
|
|
115
|
+
);
|
|
116
|
+
rendered.value = component.render(100).join("\n");
|
|
117
|
+
for (const input of inputs) component.handleInput(input);
|
|
118
|
+
return result;
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
test("ask_user_choice registers without runtime actions or overriding the open questionnaire", () => {
|
|
125
|
+
const registration = registerChoiceTool(["read", "ask_user_question"]);
|
|
126
|
+
|
|
127
|
+
assert.deepEqual(registration.registeredToolNames, ["ask_user_choice"]);
|
|
128
|
+
assert.deepEqual(registration.runtimeActionCalls(), { getActiveTools: 0, setActiveTools: 0 });
|
|
129
|
+
assert.deepEqual(registration.activeTools(), ["read", "ask_user_question"]);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("ask_user_choice exposes a strict closed single-select schema", () => {
|
|
133
|
+
const { tool } = registerChoiceTool();
|
|
134
|
+
const optionsSchema = tool.parameters.properties?.options;
|
|
135
|
+
const optionSchema = optionsSchema?.items;
|
|
136
|
+
|
|
137
|
+
assert.equal(tool.name, "ask_user_choice");
|
|
138
|
+
assert.equal(tool.parameters.additionalProperties, false);
|
|
139
|
+
assert.deepEqual(Object.keys(tool.parameters.properties ?? {}).sort(), ["options", "question"]);
|
|
140
|
+
assert.equal(optionsSchema?.minItems, 2);
|
|
141
|
+
assert.equal(optionsSchema?.maxItems, 4);
|
|
142
|
+
assert.equal(optionSchema?.additionalProperties, false);
|
|
143
|
+
assert.deepEqual(Object.keys(optionSchema?.properties ?? {}).sort(), ["description", "label", "value"]);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("ask_user_choice handles a closed Kilo hash decision with an opaque envelope value", async () => {
|
|
147
|
+
const { tool } = registerChoiceTool(["read"]);
|
|
148
|
+
const rendered = { value: "" };
|
|
149
|
+
const result = await tool.execute("call", { question: "Proceed?", options }, new AbortController().signal, undefined, tuiContext(["\x1b[B", "\r"], rendered));
|
|
150
|
+
assert.match(rendered.value, /Proceed\?|Authorize observed hash|Accept the baseline hash|Preserve requested hash|Keep the hash/);
|
|
151
|
+
assert.doesNotMatch(rendered.value, /Type something|authorize_observed_hash|preserve_requested_hash/);
|
|
152
|
+
assert.deepEqual(result.details.selection, { value: "preserve_requested_hash", label: "Preserve requested hash", index: 2 });
|
|
153
|
+
assert.equal(result.content[0]?.text, "User selected: 2. Preserve requested hash (value: preserve_requested_hash)");
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("ask_user_choice cancels without a value and remains unavailable outside the TUI", async () => {
|
|
157
|
+
const { tool } = registerChoiceTool();
|
|
158
|
+
const rendered = { value: "" };
|
|
159
|
+
const cancelled = await tool.execute("call", { question: "Proceed?", options }, new AbortController().signal, undefined, tuiContext(["\x1b"], rendered));
|
|
160
|
+
assert.equal(cancelled.details.selection, undefined);
|
|
161
|
+
assert.equal(cancelled.details.cancelled, true);
|
|
162
|
+
await assert.rejects(
|
|
163
|
+
() => tool.execute("call", { question: "Proceed?", options }, new AbortController().signal, undefined, { mode: "print" }),
|
|
164
|
+
/unavailable outside the interactive TUI/,
|
|
165
|
+
);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test("ask_user_choice emits a private balanced lifecycle around selection and cancellation", async () => {
|
|
169
|
+
const sequence: string[] = [];
|
|
170
|
+
const selectedRegistration = registerChoiceTool([], ({ data }) => {
|
|
171
|
+
sequence.push(data.active ? "active" : "inactive");
|
|
172
|
+
});
|
|
173
|
+
const selected = await selectedRegistration.tool.execute(
|
|
174
|
+
"call",
|
|
175
|
+
{ question: "private choice question", options },
|
|
176
|
+
new AbortController().signal,
|
|
177
|
+
undefined,
|
|
178
|
+
{
|
|
179
|
+
mode: "tui",
|
|
180
|
+
ui: {
|
|
181
|
+
custom: async () => {
|
|
182
|
+
sequence.push("custom");
|
|
183
|
+
return { value: "preserve_requested_hash", label: "Preserve requested hash", index: 2 };
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
);
|
|
188
|
+
assert.equal(selected.details.selection?.value, "preserve_requested_hash");
|
|
189
|
+
assert.deepEqual(sequence, ["active", "custom", "inactive"]);
|
|
190
|
+
assert.deepEqual(selectedRegistration.emittedEvents(), [
|
|
191
|
+
{ channel: "gentle-pi:ask-user-choice:blocked", data: { active: true } },
|
|
192
|
+
{ channel: "gentle-pi:ask-user-choice:blocked", data: { active: false } },
|
|
193
|
+
]);
|
|
194
|
+
assert.doesNotMatch(
|
|
195
|
+
JSON.stringify(selectedRegistration.emittedEvents()),
|
|
196
|
+
/private choice question|Authorize observed hash|Accept the baseline hash|preserve_requested_hash/,
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
const cancelledRegistration = registerChoiceTool();
|
|
200
|
+
const cancelled = await cancelledRegistration.tool.execute(
|
|
201
|
+
"call",
|
|
202
|
+
{ question: "Proceed?", options },
|
|
203
|
+
new AbortController().signal,
|
|
204
|
+
undefined,
|
|
205
|
+
{ mode: "tui", ui: { custom: async () => undefined } },
|
|
206
|
+
);
|
|
207
|
+
assert.equal(cancelled.details.cancelled, true);
|
|
208
|
+
assert.deepEqual(cancelledRegistration.emittedEvents(), [
|
|
209
|
+
{ channel: "gentle-pi:ask-user-choice:blocked", data: { active: true } },
|
|
210
|
+
{ channel: "gentle-pi:ask-user-choice:blocked", data: { active: false } },
|
|
211
|
+
]);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test("ask_user_choice settles its lifecycle after a custom UI error and emits nothing outside the TUI", async () => {
|
|
215
|
+
const failedRegistration = registerChoiceTool();
|
|
216
|
+
const customError = new Error("custom UI failed");
|
|
217
|
+
await assert.rejects(
|
|
218
|
+
failedRegistration.tool.execute(
|
|
219
|
+
"call",
|
|
220
|
+
{ question: "Proceed?", options },
|
|
221
|
+
new AbortController().signal,
|
|
222
|
+
undefined,
|
|
223
|
+
{ mode: "tui", ui: { custom: async () => { throw customError; } } },
|
|
224
|
+
),
|
|
225
|
+
(error) => error === customError,
|
|
226
|
+
);
|
|
227
|
+
assert.deepEqual(failedRegistration.emittedEvents(), [
|
|
228
|
+
{ channel: "gentle-pi:ask-user-choice:blocked", data: { active: true } },
|
|
229
|
+
{ channel: "gentle-pi:ask-user-choice:blocked", data: { active: false } },
|
|
230
|
+
]);
|
|
231
|
+
|
|
232
|
+
const nonTuiRegistration = registerChoiceTool();
|
|
233
|
+
let customCalled = false;
|
|
234
|
+
await assert.rejects(
|
|
235
|
+
nonTuiRegistration.tool.execute(
|
|
236
|
+
"call",
|
|
237
|
+
{ question: "Proceed?", options },
|
|
238
|
+
new AbortController().signal,
|
|
239
|
+
undefined,
|
|
240
|
+
{
|
|
241
|
+
mode: "print",
|
|
242
|
+
ui: {
|
|
243
|
+
custom: async () => {
|
|
244
|
+
customCalled = true;
|
|
245
|
+
return undefined;
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
),
|
|
250
|
+
/unavailable outside the interactive TUI/,
|
|
251
|
+
);
|
|
252
|
+
assert.equal(customCalled, false);
|
|
253
|
+
assert.deepEqual(nonTuiRegistration.emittedEvents(), []);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
test("ask_user_choice is offered only for interactive TUI turns and preserves the open questionnaire", async () => {
|
|
257
|
+
const registration = registerChoiceTool(["read", "ask_user_question"]);
|
|
258
|
+
registration.allowRuntimeActions();
|
|
259
|
+
|
|
260
|
+
for (const hook of registration.hooks) await hook({}, { mode: "tui" });
|
|
261
|
+
assert.deepEqual(registration.activeTools(), ["read", "ask_user_question", "ask_user_choice"]);
|
|
262
|
+
for (const hook of registration.hooks) await hook({}, { mode: "print" });
|
|
263
|
+
assert.deepEqual(registration.activeTools(), ["read", "ask_user_question"]);
|
|
264
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import { execFileSync } from "node:child_process";
|
|
3
|
-
import { mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { mkdirSync, mkdtempSync, realpathSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { homedir, tmpdir } from "node:os";
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
import test from "node:test";
|
|
@@ -11,14 +11,14 @@ import codeGraphTools, {
|
|
|
11
11
|
} from "../extensions/codegraph-tools.ts";
|
|
12
12
|
|
|
13
13
|
function workspace(t: test.TestContext): string {
|
|
14
|
-
const cwd = mkdtempSync(join(tmpdir(), "gentle-pi-codegraph-"));
|
|
14
|
+
const cwd = realpathSync(mkdtempSync(join(tmpdir(), "gentle-pi-codegraph-")));
|
|
15
15
|
execFileSync("git", ["init", "-b", "main"], { cwd, stdio: "ignore" });
|
|
16
16
|
t.after(() => rmSync(cwd, { recursive: true, force: true }));
|
|
17
17
|
return cwd;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
test("CodeGraph tool rejects non-project, nested-project, HOME, and temporary workspaces before init", async (t) => {
|
|
21
|
-
const nonProject = mkdtempSync(join(tmpdir(), "gentle-pi-codegraph-non-project-"));
|
|
21
|
+
const nonProject = realpathSync(mkdtempSync(join(tmpdir(), "gentle-pi-codegraph-non-project-")));
|
|
22
22
|
t.after(() => rmSync(nonProject, { recursive: true, force: true }));
|
|
23
23
|
const root = workspace(t);
|
|
24
24
|
const nested = join(root, "nested");
|