gentle-pi 2.3.0 → 2.5.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 +195 -11
- package/assets/agents/gentle-ai-worker.md +9 -0
- package/assets/agents/sdd-explore.md +1 -0
- package/assets/orchestrator-delegation.md +21 -10
- package/assets/orchestrator.md +8 -12
- 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/delegated-verification.md +25 -0
- package/docs/review-integration.md +1 -1
- package/docs/telemetry.md +38 -0
- package/extensions/ask-user-choice.ts +26 -20
- package/extensions/codegraph-tools.ts +94 -5
- package/extensions/gentle-agents.ts +588 -0
- package/extensions/gentle-ai.ts +1421 -143
- package/extensions/gentle-shell.ts +547 -0
- package/extensions/gentle-todo.ts +199 -0
- package/extensions/quiet-tools.ts +1 -1
- package/lib/agent-home.ts +8 -0
- package/lib/agents-config.ts +318 -0
- package/lib/agents-history.ts +80 -0
- package/lib/agents-protocol.ts +429 -0
- package/lib/agents-runner.ts +490 -0
- package/lib/agents-transcript.ts +87 -0
- package/lib/agents-view.ts +557 -0
- package/lib/agents-widget.ts +222 -0
- package/lib/gentle-ai-renderer.ts +142 -26
- 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 +103 -12
- package/lib/provider-contract-bundle.ts +88 -6
- package/lib/review-candidate-view-owner.ts +177 -0
- package/lib/review-candidate-view.ts +127 -35
- package/lib/review-consent-ui.ts +65 -0
- package/lib/review-host-relay.ts +146 -60
- package/lib/review-integration-v2.ts +92 -13
- package/lib/review-last-event-controller.ts +1 -0
- package/lib/review-relay-contract.ts +11 -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 +219 -0
- package/lib/sdd-preflight.ts +2 -2
- package/lib/shell-bar.ts +138 -0
- package/lib/shell-card.ts +136 -0
- package/lib/shell-changes-view.ts +205 -0
- package/lib/shell-changes.ts +210 -0
- package/lib/shell-gauge.ts +40 -0
- package/lib/shell-prompt.ts +119 -0
- package/lib/shell-todo.ts +280 -0
- package/lib/shell-usage-view.ts +76 -0
- package/lib/shell-usage.ts +246 -0
- package/lib/telemetry-trigger.ts +151 -0
- package/package.json +4 -4
- package/runtime/native-review-cli.mjs +102 -11
- package/runtime/review-integration-v2.mjs +92 -13
- package/runtime/review-relay-contract.mjs +11 -0
- package/runtime/review-risk-assessment.mjs +340 -0
- package/runtime/telemetry-trigger.mjs +152 -0
- package/scripts/build-runtime-modules.mjs +2 -0
- package/scripts/gentle-ai-installer.mjs +10 -10
- package/scripts/test-packed-runner.mjs +22 -0
- package/scripts/verify-package-files.mjs +18 -13
- package/skills/_shared/review-ledger-contract.md +9 -1
- package/skills/issue-creation/SKILL.md +53 -93
- package/tests/agents-config.test.ts +143 -0
- package/tests/agents-fake-child.ts +52 -0
- package/tests/agents-history.test.ts +54 -0
- package/tests/agents-protocol.test.ts +153 -0
- package/tests/agents-runner-process.test.ts +111 -0
- package/tests/agents-runner.test.ts +402 -0
- package/tests/agents-transcript.test.ts +30 -0
- package/tests/agents-view.test.ts +274 -0
- package/tests/agents-widget.test.ts +111 -0
- package/tests/ask-user-choice.test.ts +157 -3
- package/tests/codegraph-tools.test.ts +110 -1
- package/tests/devbinary/native-review-parity.devtest.ts +108 -0
- package/tests/fixtures/agents-process-child.mjs +23 -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-agents.test.ts +741 -0
- package/tests/gentle-ai-binary.test.ts +1 -1
- package/tests/gentle-ai-installer.test.ts +47 -47
- package/tests/gentle-ai-renderer.test.ts +65 -0
- package/tests/gentle-ai.test.ts +31 -14
- package/tests/gentle-card-text.ts +35 -0
- package/tests/gentle-shell.test.ts +527 -0
- package/tests/gentle-todo.test.ts +182 -0
- package/tests/issue-creation-skill.test.ts +103 -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 +33 -1
- package/tests/native-review-cli.test.ts +40 -0
- 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 +29 -22
- package/tests/orchestrator-budget.test.ts +71 -2
- package/tests/orchestrator-rdd-ownership.test.ts +10 -1
- package/tests/package-manifest.test.ts +134 -9
- package/tests/provider-contract-bundle.test.ts +76 -0
- package/tests/provider-contract-mirror.test.ts +19 -0
- package/tests/quiet-tool-rendering.test.ts +96 -37
- package/tests/rdd-aware-verification-contract.test.ts +216 -0
- package/tests/rdd-status-line.test.ts +286 -0
- package/tests/review-agent-end-preflight.test.ts +408 -0
- package/tests/review-candidate-view.test.ts +452 -6
- package/tests/review-contract-prompt.test.ts +142 -0
- package/tests/review-controller-native-recovery.test.ts +29 -4
- package/tests/review-controller-native-routing.test.ts +321 -4
- package/tests/review-controller-workspace-root.test.ts +45 -2
- package/tests/review-controller.test.ts +26 -1
- package/tests/review-host-relay-routing.test.ts +229 -11
- package/tests/review-host-relay.test.ts +195 -7
- package/tests/review-integration-v2-forward.test.ts +47 -0
- package/tests/review-integration-v2.test.ts +112 -0
- package/tests/review-last-event-closure.test.ts +7 -2
- package/tests/review-ledger-contract.test.ts +1 -1
- package/tests/review-relay-contract.test.ts +26 -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 +608 -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 +126 -0
- package/tests/runtime-harness.mjs +1 -0
- package/tests/shell-bar.test.ts +176 -0
- package/tests/shell-card.test.ts +118 -0
- package/tests/shell-changes-view.test.ts +146 -0
- package/tests/shell-changes.test.ts +182 -0
- package/tests/shell-prompt.test.ts +118 -0
- package/tests/shell-todo.test.ts +170 -0
- package/tests/shell-usage-view.test.ts +62 -0
- package/tests/shell-usage.test.ts +197 -0
- package/tests/telemetry-trigger.test.ts +349 -0
- package/tests/writer-edit-surface-scope.test.ts +153 -17
- /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
|
@@ -46,6 +46,7 @@ const requiredPaths = [
|
|
|
46
46
|
"assets/support/sdd-status-contract.md",
|
|
47
47
|
"assets/support/strict-tdd.md",
|
|
48
48
|
"assets/support/strict-tdd-verify.md",
|
|
49
|
+
"docs/delegated-verification.md",
|
|
49
50
|
"docs/native-authority-architecture.md",
|
|
50
51
|
"docs/skill-style-guide.md",
|
|
51
52
|
"docs/review-integration.md",
|
|
@@ -59,10 +60,13 @@ const requiredPaths = [
|
|
|
59
60
|
"lib/review-integration-v2.ts",
|
|
60
61
|
"lib/review-relay-contract.ts",
|
|
61
62
|
"lib/sdd-preflight.ts",
|
|
63
|
+
"lib/telemetry-trigger.ts",
|
|
62
64
|
"runtime/gentle-ai-binary.mjs",
|
|
63
65
|
"runtime/native-review-cli.mjs",
|
|
64
66
|
"runtime/review-integration-v2.mjs",
|
|
67
|
+
"runtime/review-risk-assessment.mjs",
|
|
65
68
|
"runtime/review-relay-contract.mjs",
|
|
69
|
+
"runtime/telemetry-trigger.mjs",
|
|
66
70
|
"scripts/check-provider-contract.mjs",
|
|
67
71
|
"scripts/gentle-ai-installer.mjs",
|
|
68
72
|
"scripts/install-gentle-ai.mjs",
|
|
@@ -80,16 +84,17 @@ const requiredPaths = [
|
|
|
80
84
|
// exact bytes are pinned by the lock-driven scripts/check-provider-contract.mjs
|
|
81
85
|
// drift check, which runs in the same pnpm test flow.
|
|
82
86
|
"contracts/review-provider-contract-mirror/provider-contract.lock.json",
|
|
83
|
-
"contracts/review-provider-contract-mirror/v1.
|
|
84
|
-
"contracts/review-provider-contract-mirror/v1.
|
|
85
|
-
"contracts/review-provider-contract-mirror/v1.
|
|
86
|
-
"contracts/review-provider-contract-mirror/v1.
|
|
87
|
-
"contracts/review-provider-contract-mirror/v1.
|
|
88
|
-
"contracts/review-provider-contract-mirror/v1.
|
|
89
|
-
"contracts/review-provider-contract-mirror/v1.
|
|
90
|
-
"contracts/review-provider-contract-mirror/v1.
|
|
91
|
-
"contracts/review-provider-contract-mirror/v1.
|
|
92
|
-
"contracts/review-provider-contract-mirror/v1.
|
|
87
|
+
"contracts/review-provider-contract-mirror/v1.2.0/bundle/README.md",
|
|
88
|
+
"contracts/review-provider-contract-mirror/v1.2.0/bundle/manifest.json",
|
|
89
|
+
"contracts/review-provider-contract-mirror/v1.2.0/bundle/orchestration/pi.md",
|
|
90
|
+
"contracts/review-provider-contract-mirror/v1.2.0/bundle/schemas/lens.schema.json",
|
|
91
|
+
"contracts/review-provider-contract-mirror/v1.2.0/bundle/schemas/refuter.schema.json",
|
|
92
|
+
"contracts/review-provider-contract-mirror/v1.2.0/bundle/schemas/targeted-validator.schema.json",
|
|
93
|
+
"contracts/review-provider-contract-mirror/v1.2.0/bundle/vectors/lens.json",
|
|
94
|
+
"contracts/review-provider-contract-mirror/v1.2.0/bundle/vectors/refuter.json",
|
|
95
|
+
"contracts/review-provider-contract-mirror/v1.2.0/bundle/vectors/targeted-validator.json",
|
|
96
|
+
"contracts/review-provider-contract-mirror/v1.2.0/generated/provider-capabilities.baseline.json",
|
|
97
|
+
"contracts/review-provider-contract-mirror/v1.2.0/generated/provider-roles.baseline.json",
|
|
93
98
|
"prompts/skill-creation.md",
|
|
94
99
|
"skills/_shared/review-ledger-contract.md",
|
|
95
100
|
"skills/branch-pr/SKILL.md",
|
|
@@ -175,7 +180,7 @@ const contractHashes = {
|
|
|
175
180
|
"contracts/review-integration/v2/schemas/repair.schema.json": "98a85fd45a8ae7f6211ffeeb3f9c478fa1dd1c17f385751f15f2111e6c3ab167",
|
|
176
181
|
"contracts/review-integration/v2/schemas/start.schema.json": "2991e3fcca672d9257d61b6a336fb34e58b15a8e03f8a09a7adf892cae6a8085",
|
|
177
182
|
"contracts/review-integration/v2/schemas/status.schema.json": "c4dcc736cfc6300560a3c4262d2d982368529d5c49d58d499552a3b0beef9212",
|
|
178
|
-
"docs/review-integration.md": "
|
|
183
|
+
"docs/review-integration.md": "95a3df92785bc4d9f3b99e702aaf817ae0440bd16c83218d2c3f2aca67c280fb",
|
|
179
184
|
};
|
|
180
185
|
|
|
181
186
|
requiredPaths.push(...Object.keys(contractHashes));
|
|
@@ -333,7 +338,7 @@ async function main() {
|
|
|
333
338
|
});
|
|
334
339
|
|
|
335
340
|
if (driftedContracts.length > 0) {
|
|
336
|
-
console.error("gentle-pi packaged review-integration/v1 and review-integration/v2 contract bytes drifted from the pinned v2.
|
|
341
|
+
console.error("gentle-pi packaged review-integration/v1 and review-integration/v2 contract bytes drifted from the pinned v2.7.0 runtime's vendored Gentle AI contract artifacts:");
|
|
337
342
|
for (const drift of driftedContracts) console.error(`- ${drift.relativePath}: expected ${drift.expected}, got ${drift.actual}`);
|
|
338
343
|
process.exit(1);
|
|
339
344
|
}
|
|
@@ -378,7 +383,7 @@ async function main() {
|
|
|
378
383
|
process.exit(1);
|
|
379
384
|
}
|
|
380
385
|
|
|
381
|
-
console.log(`gentle-pi package resource check passed (${requiredPaths.length} files; ${Object.keys(contractHashes).length} exact byte-pinned contract artifacts for the v2.
|
|
386
|
+
console.log(`gentle-pi package resource check passed (${requiredPaths.length} files; ${Object.keys(contractHashes).length} exact byte-pinned contract artifacts for the v2.7.0 runtime).`);
|
|
382
387
|
}
|
|
383
388
|
|
|
384
389
|
const isMainModule = process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href;
|
|
@@ -20,10 +20,18 @@ Risk routing is deterministic:
|
|
|
20
20
|
|
|
21
21
|
Generated files matching `testdata/golden/**` remain in snapshot identity but do not count as authored risk lines. Ordinary tests, fixtures, and snapshots are never broadly excluded. The correction budget is frozen as `min(200, ceil(original_changed_lines / 2))`.
|
|
22
22
|
|
|
23
|
-
Before status/START, consult effective review mode. `off` creates no authority or authorization and yields organic `disabled/unmanaged`, never approval. Ordinary START declares `--consent relay`; low risk stays silent. A medium/high `consent/
|
|
23
|
+
Before status/START, consult effective review mode. `off` creates no authority or authorization and yields organic `disabled/unmanaged`, never approval. Ordinary START declares `--consent relay`; low risk stays silent. A medium/high `consent/v3` result carries the complete raw two-choice provider envelope plus an opaque in-memory candidate binding. In an eligible interactive parent Pi session, the host displays both provider labels and effects unchanged plus one clearly separate host-owned action: **Run this review and allow reviews for this Pi session**. The first two actions stay candidate-only. Direct human selection of the third action executes that fresh envelope's exact existing `granted` invocation through `answer-consent`, then grants only future fresh validated envelopes for the same live SessionManager object, exact nonempty session ID, and canonical Git worktree root. Every envelope is consumed once before provider mutation and rechecks repository, target, projection, lineage, answer binding, and live session identity; ambiguity reconciles through STATUS and never replays.
|
|
24
|
+
|
|
25
|
+
The host grant lives only in a schema-checked `globalThis[Symbol.for(...)]` WeakMap registry. Reload preserves the SessionManager and reconnects the grant; quit, new, resume, fork, explicit revoke, or process restart removes it. `/tree` retains it. Child-agent processes, headless/RPC/unsupported UI, explicit cross-repository `workspaceRoot`, model prose, and tool arguments cannot offer, create, or consume it. It grants no provider mode, verdict, cost forecast, acknowledgement, maintenance, delivery, or cross-repository authority. If host UI is cancelled, fails, or cannot prove current Git/session identity, the original unresolved provider envelope is returned unchanged. If that envelope reaches the parent, localize and present its original two choices losslessly; never append the host action to the decoded provider contract. Decline creates no lineage, authority, standing grant, latch, or pending authorization; the next candidate asks again. Old Pi clone latch files are inert, and Pi writes no asked latch.
|
|
24
26
|
|
|
25
27
|
Reviewer, refuter, and validator verdicts are admitted natively, never Pi-authored. `finalize` follows the provider's negotiated `next_transition` and supplies only the negotiated collection answers: a lens `review.capture-result` collect input rendered with `--agent=pi --materialize=true` is satisfied by the gentle-pi host relay, which prints the exact Go-materialized opaque prompt, launches a fresh locked-down print-mode `pi` subprocess in an empty scratch directory with every discovery surface disabled, and submits the untouched raw output bytes through the provider-owned submission form. The adversarial roles do not go through that relay: `review.capture-refuter` and `review.capture-validation` collect inputs render as self-contained authority-advancing vectors (binding tokens plus `--agent=pi --execute=true`, no submission descriptor); executing the exact rendered invocation makes Go materialize the role prompt, spawn its own locked-down `pi` process, and admit the raw verdict. Native Go owns validation, canonicalization, missing lens/finding ID assignment, persistence, and hashing, and performs only the legal transition from the current compact state. The five states are `reviewing`, `correction_required`, `validating`, `approved`, and `escalated`.
|
|
26
28
|
|
|
29
|
+
### Concurrent Reviewer Group (MANDATORY)
|
|
30
|
+
|
|
31
|
+
When one fresh `collect.inputs` set contains multiple distinct independent `review.capture-result` reviewer slots, call `gentle_review_capture_group` once with the complete ordered provider bindings and its forecast acknowledgement. Before any materialization it validates the whole current group, its common binding fields, unique slot identities, and every provider submission descriptor; then it starts all reviewers before waiting. For canonical 4R, preserve `review-risk`, `review-resilience`, `review-readability`, `review-reliability` order.
|
|
32
|
+
|
|
33
|
+
Each grouped launch runs only its own provider-issued `review.capture-result` binding, and admission remains in provider order. A typed terminal or nonterminal closure returns directly; a later stop after earlier admission reports bounded partial progress and never claims no mutation. If every submission returns without closure, reconcile fresh bound STATUS and return its declared action rather than inferring group success. On `correction_required`, continue only through exact bound STATUS and the provider-issued `review.capture-correction-plan` binding.
|
|
34
|
+
|
|
27
35
|
`validate` is informational and runs with zero actors. It never mutates compact authority or controls delivery.
|
|
28
36
|
|
|
29
37
|
## Causal findings
|
|
@@ -4,18 +4,14 @@ description: "Create and triage GitHub issues from repository evidence. Trigger:
|
|
|
4
4
|
license: Apache-2.0
|
|
5
5
|
metadata:
|
|
6
6
|
author: gentleman-programming
|
|
7
|
-
version: "1.
|
|
7
|
+
version: "1.3"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# Issue Creation
|
|
11
11
|
|
|
12
|
-
## When To Use
|
|
13
|
-
|
|
14
|
-
Use this skill when creating, drafting, triaging, or approving an issue in the current GitHub repository.
|
|
15
|
-
|
|
16
12
|
## Core Rule
|
|
17
13
|
|
|
18
|
-
Discover the repository's
|
|
14
|
+
Discover the target repository's contribution workflow before proposing or publishing. YAML Issue Forms are the format authority for the default automated path: materialize reviewed answers into a private `BODY_FILE` and publish with `--body-file`.
|
|
19
15
|
|
|
20
16
|
## Safe Discovery
|
|
21
17
|
|
|
@@ -27,123 +23,87 @@ REPO="$(gh repo view --json nameWithOwner -q .nameWithOwner)"
|
|
|
27
23
|
REPO_URL="$(gh repo view --json url -q .url)"
|
|
28
24
|
HOST="${REPO_URL#*://}"
|
|
29
25
|
HOST="${HOST%%/*}"
|
|
26
|
+
TARGET="$HOST/$REPO"
|
|
30
27
|
gh repo view --json nameWithOwner,url,hasDiscussionsEnabled,hasIssuesEnabled,isBlankIssuesEnabled
|
|
31
|
-
git ls-files CONTRIBUTING.md CONTRIBUTING.* .github/CONTRIBUTING.md .github/ISSUE_TEMPLATE
|
|
28
|
+
git ls-files README.md CONTRIBUTING.md CONTRIBUTING.* .github/CONTRIBUTING.md .github/ISSUE_TEMPLATE .github/ISSUE_TEMPLATE/config.yml
|
|
32
29
|
gh api --hostname "$HOST" --paginate "repos/$REPO/labels?per_page=100" --jq '.[].name'
|
|
33
30
|
```
|
|
34
31
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
- repository instructions such as `CONTRIBUTING.md` and `README.md`;
|
|
38
|
-
- files under `.github/ISSUE_TEMPLATE`;
|
|
39
|
-
- `.github/ISSUE_TEMPLATE/config.yml` when present;
|
|
40
|
-
- issue forms, required fields, and labels declared by each template;
|
|
41
|
-
- existing open and closed issues for duplicates and established wording.
|
|
32
|
+
Inspect `README.md`, contribution instructions, `.github/ISSUE_TEMPLATE/config.yml` contact links, forms, labels, and open and closed issues. For questions/support, follow repository-prescribed Discussions/contact routing when available; otherwise ask or stop. Complete target verification for `REPO`, `HOST`, and `TARGET`. Fail closed before mutation when authentication, target verification, issue availability, policy, form selection, or required metadata is missing or ambiguous. A blank fallback is allowed only when `isBlankIssuesEnabled` is explicitly true.
|
|
42
33
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
A no-template fallback is allowed only when isBlankIssuesEnabled is explicitly true. Otherwise follow discovered contact links or stop and ask; never publish.
|
|
46
|
-
|
|
47
|
-
After discovery and review, build optional label arguments using only labels that exist and repository policy permits the actor to apply:
|
|
34
|
+
Build `LABEL_ARGS` only from reviewed labels that exist and policy permits the actor to apply:
|
|
48
35
|
|
|
49
36
|
```bash
|
|
50
37
|
LABEL_ARGS=()
|
|
51
|
-
# Repeat for each
|
|
52
|
-
LABEL_ARGS+=(--label "$LABEL")
|
|
38
|
+
LABEL_ARGS+=(--label "$LABEL") # Repeat only for each permitted discovered label.
|
|
53
39
|
```
|
|
54
40
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
## Workflow
|
|
41
|
+
## Duplicate And Form Decision
|
|
58
42
|
|
|
59
|
-
1. Describe the
|
|
60
|
-
2. Search open and closed issues:
|
|
43
|
+
1. Describe the report in one sentence, derive `QUERY`, then complete one duplicate search across open and closed issues:
|
|
61
44
|
|
|
62
45
|
```bash
|
|
63
|
-
gh issue list --repo "$
|
|
46
|
+
gh issue list --repo "$TARGET" --state all --search "$QUERY" --limit 1000
|
|
64
47
|
```
|
|
65
48
|
|
|
66
|
-
If
|
|
67
|
-
|
|
68
|
-
3.
|
|
69
|
-
4. Choose a repository-provided template only when its purpose matches the report.
|
|
70
|
-
5. Fill every required template field from known evidence. Ask for missing facts rather than inventing them.
|
|
71
|
-
6. Apply labels only when they exist and repository guidance establishes who should apply them.
|
|
72
|
-
7. Publish only after the title, body, target repository, and selected template or fallback have been reviewed, and the pre-submission privacy review below has passed.
|
|
73
|
-
|
|
74
|
-
## Pre-submission Privacy Review
|
|
75
|
-
|
|
76
|
-
Pre-submission privacy review is mandatory. Scan every issue body immediately before `gh issue create`. The scan replaces — never deletes — environment-specific data with explicit placeholders so the reproduction still teaches:
|
|
77
|
-
|
|
78
|
-
| Category | Replace with | Example (before → after) |
|
|
79
|
-
|----------|---------------|---------------------------|
|
|
80
|
-
| Private project names | `<project-name>` | `my-private-project-b` → `<project-name>` |
|
|
81
|
-
| Usernames | `<user>` | `C:\Users\my-real-username\go\bin` → `C:\Users\<user>\go\bin` |
|
|
82
|
-
| Hostnames | `<hostname>` | `devbox-macbook.local` → `<hostname>` |
|
|
83
|
-
| Home paths | `/home/<user>` or `C:\Users\<user>` | (covered above) |
|
|
84
|
-
| API keys, tokens, passwords | `<token>` / `<password>` | `ghp_abc123...` → `<token>` |
|
|
85
|
-
| Internal ports / hostnames | `<host>:<port>` | `10.0.0.42:5432` → `<host>:<port>` |
|
|
86
|
-
|
|
87
|
-
Do NOT redact intentionally public identifiers: tool names (`gentle-ai`, `engram`, `go`, `node`, `python`), package names, public documentation URLs, generic example domains (`example.com`, `localhost`). Keep reproduction structure with placeholders — never redact an example into nothingness.
|
|
88
|
-
|
|
89
|
-
**Rule of thumb:** if the reader can run the reproduction step after you replace every identifier with its placeholder, the sanitization is correct. If a step becomes impossible (because the placeholder consumed a needed value), that step needs the value — and you should mark it `<value-required>` and explain in the body what the user should fill in.
|
|
49
|
+
If results are saturated or completeness is uncertain, narrow the read-only search or stop. Comment on a confirmed duplicate instead of creating one. Before commenting on a confirmed duplicate, perform the same privacy scan/redaction on the exact comment body as for publication.
|
|
50
|
+
2. Select one repository-provided form only when its declared purpose matches. If multiple forms match and policy does not distinguish them, stop and request that decision.
|
|
51
|
+
3. For a YAML form, read its schema and establish controls in declared order. Support only `input`, `textarea`, `dropdown`, and `checkboxes`. Markdown controls are non-answer guidance: honor their visible instructions when collecting and materializing adjacent answers, but do not render them as response sections. Fail closed before mutation on malformed, unsupported, missing, or ambiguous required structure or answers. A malformed schema, or missing or ambiguous required answers, fail closed: do not open a browser or mutate. A browser handoff is available only when the user explicitly requests browser completion or a syntactically valid selected form cannot safely/faithfully be represented by the automated path; otherwise report why automation is unsafe and stop.
|
|
90
52
|
|
|
91
|
-
|
|
53
|
+
| Control | Required handling |
|
|
54
|
+
| --- | --- |
|
|
55
|
+
| `input` / `textarea` | Preserve the visible label. Require an answer when `validations.required` is true; otherwise render `_No response_`. |
|
|
56
|
+
| `dropdown` | Preserve visible labels and options. Require exact selected option text; single-select has one selection, and multi-select preserves selections in declared options order. A required dropdown needs at least one valid selection; an optional dropdown with no selection renders `_No response_`. |
|
|
57
|
+
| `checkboxes` | Preserve the visible label and every option as `- [x]` or `- [ ]` in declared order. Enforce individually required checkboxes and require explicit first-person affirmation for first-person option text. |
|
|
92
58
|
|
|
93
|
-
|
|
59
|
+
For each answer, render `### <visible label>` followed by its materialized value. For `textarea.attributes.render`, fence the answer with the declared language and a fence long enough for its content. Never invent answers, selections, confirmations, or labels.
|
|
94
60
|
|
|
95
|
-
|
|
61
|
+
A Markdown template may be completed only from known evidence into the same private `BODY_FILE`. If no matching template exists, use the reviewed structured blank fallback only when blank issues are explicitly enabled; otherwise stop without publishing.
|
|
96
62
|
|
|
97
|
-
|
|
98
|
-
gh issue create --repo "$HOST/$REPO" --web "${LABEL_ARGS[@]}"
|
|
99
|
-
```
|
|
63
|
+
## Review And Publication
|
|
100
64
|
|
|
101
|
-
|
|
65
|
+
Before the single create attempt, review the target, title, selected form or permitted fallback, exact body, and permitted labels. Perform a privacy scan immediately before publication: replace private project names, usernames, hostnames, home paths, credentials, and private network addresses with useful placeholders without removing reproduction structure.
|
|
102
66
|
|
|
103
|
-
|
|
104
|
-
gh issue create --repo "$HOST/$REPO" --title "$TITLE" --body-file "$BODY_FILE" "${LABEL_ARGS[@]}"
|
|
105
|
-
```
|
|
106
|
-
|
|
107
|
-
## No-Template Fallback
|
|
108
|
-
|
|
109
|
-
When the repository permits issue creation, provides no matching template, and isBlankIssuesEnabled is explicitly true, prepare a structured body with these sections:
|
|
110
|
-
|
|
111
|
-
- problem or requested outcome;
|
|
112
|
-
- reproduction or motivating example;
|
|
113
|
-
- expected behavior;
|
|
114
|
-
- actual behavior or current limitation;
|
|
115
|
-
- environment and relevant evidence;
|
|
116
|
-
- alternatives or workarounds, when applicable.
|
|
117
|
-
|
|
118
|
-
Publish the reviewed fallback explicitly:
|
|
67
|
+
Create one owner-only temporary directory outside the repository for both private files; restrict it to the current user and clean up both files on every exit/outcome:
|
|
119
68
|
|
|
120
69
|
```bash
|
|
121
|
-
|
|
70
|
+
umask 077
|
|
71
|
+
REPO_ROOT="$(git rev-parse --show-toplevel)" || exit 1
|
|
72
|
+
REPO_ROOT="$(cd "$REPO_ROOT" && pwd -P)" || exit 1
|
|
73
|
+
if [ "$REPO_ROOT" = "/" ]; then
|
|
74
|
+
printf '%s\n' "Temporary directory is inside the repository" >&2; exit 1
|
|
75
|
+
fi
|
|
76
|
+
TMP_DIR="$(TMPDIR=/tmp mktemp -d /tmp/gentle-ai-issue.XXXXXXXX)" || exit 1
|
|
77
|
+
trap 'rm -rf -- "$TMP_DIR"' EXIT
|
|
78
|
+
TMP_DIR_REAL="$(cd "$TMP_DIR" && pwd -P)" || exit 1
|
|
79
|
+
case "$TMP_DIR_REAL/" in
|
|
80
|
+
"$REPO_ROOT/"*) printf '%s\n' "Temporary directory is inside the repository" >&2; exit 1 ;;
|
|
81
|
+
esac
|
|
82
|
+
chmod 700 "$TMP_DIR_REAL"
|
|
83
|
+
BODY_FILE="$TMP_DIR_REAL/body.md"
|
|
84
|
+
READBACK_FILE="$TMP_DIR_REAL/readback.json"
|
|
122
85
|
```
|
|
123
86
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
## Labels And Approval
|
|
87
|
+
Make one mutation attempt through the automated path and publish exactly once:
|
|
127
88
|
|
|
128
|
-
|
|
89
|
+
```bash
|
|
90
|
+
gh issue create --repo "$TARGET" --title "$TITLE" --body-file "$BODY_FILE" "${LABEL_ARGS[@]}"
|
|
91
|
+
```
|
|
129
92
|
|
|
130
|
-
|
|
131
|
-
- follow contribution guidance for who may apply each label;
|
|
132
|
-
- wait when repository policy requires maintainer approval before implementation;
|
|
133
|
-
- do not invent a status or priority taxonomy when none is documented.
|
|
93
|
+
When browser completion is available under the form decision above, an optional, separate browser handoff may open the repository form. It is never proof of publication and is never a response to malformed schemas or missing/ambiguous required answers:
|
|
134
94
|
|
|
135
|
-
|
|
95
|
+
```bash
|
|
96
|
+
gh issue create --repo "$TARGET" --web
|
|
97
|
+
```
|
|
136
98
|
|
|
137
|
-
|
|
99
|
+
Do not retry a timeout, network failure, missing identity, or other uncertain result. Capture the returned issue number, then read it back from the verified target host before reporting success:
|
|
138
100
|
|
|
139
|
-
|
|
101
|
+
```bash
|
|
102
|
+
gh issue view "$NUMBER" --repo "$TARGET" --json number,url,title,body,state,labels >"$READBACK_FILE"
|
|
103
|
+
```
|
|
140
104
|
|
|
141
|
-
|
|
105
|
+
Confirm that read-back identifies the target-host issue and that title and body match after only CRLF-to-LF and trailing-final-newline normalization. Report `confirmed` only after this target-host read-back. Otherwise report `no_write` when an authoritative rejection proves no issue was created, or `unknown` and stop all later mutations.
|
|
142
106
|
|
|
143
|
-
|
|
144
|
-
- it is not a duplicate;
|
|
145
|
-
- the report contains enough evidence for an implementation decision;
|
|
146
|
-
- the requested behavior is in repository scope;
|
|
147
|
-
- labels and status changes follow the current repository's policy.
|
|
107
|
+
## Triage
|
|
148
108
|
|
|
149
|
-
If any point is uncertain,
|
|
109
|
+
Before approving or closing an issue, verify it is concrete, non-duplicate, sufficiently evidenced, in scope, and consistent with repository label/status policy. If any point is uncertain, retain the repository review state and request the smallest missing evidence.
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import test, { after } from "node:test";
|
|
6
|
+
import {
|
|
7
|
+
AGENT_MODE,
|
|
8
|
+
agentDirectories,
|
|
9
|
+
discoverAgents,
|
|
10
|
+
loadAgentsConfig,
|
|
11
|
+
parseAgentDefinition,
|
|
12
|
+
parseAgentsConfig,
|
|
13
|
+
parseFrontmatter,
|
|
14
|
+
parseModelRef,
|
|
15
|
+
resolveAgentProfile,
|
|
16
|
+
} from "../lib/agents-config.ts";
|
|
17
|
+
|
|
18
|
+
// Gentle Agents configuration: markdown agent definitions (the same files
|
|
19
|
+
// gentle-ai installs) and subagents.json, both parsed without touching pi.
|
|
20
|
+
|
|
21
|
+
const root = mkdtempSync(join(tmpdir(), "gentle-agents-config-"));
|
|
22
|
+
after(() => rmSync(root, { recursive: true, force: true }));
|
|
23
|
+
|
|
24
|
+
const EXPLORER = `---
|
|
25
|
+
name: gentle-ai-explore
|
|
26
|
+
description: Read-only exploration and mapping.
|
|
27
|
+
model: openai-codex/gpt-5.6-terra
|
|
28
|
+
thinking: high
|
|
29
|
+
tools:
|
|
30
|
+
- read
|
|
31
|
+
- grep
|
|
32
|
+
- codegraph
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
You are the read-only explorer.
|
|
36
|
+
Map files and return a compressed handoff.
|
|
37
|
+
`;
|
|
38
|
+
|
|
39
|
+
test("parseFrontmatter reads scalars, inline lists, and block lists, and keeps the body", () => {
|
|
40
|
+
const { data, body } = parseFrontmatter("---\nname: a\ntools: [read, grep]\nlist:\n - one\n - two\nquoted: \"x: y\"\n---\nBody here\n");
|
|
41
|
+
assert.deepEqual(data, { name: "a", tools: ["read", "grep"], list: ["one", "two"], quoted: "x: y" });
|
|
42
|
+
assert.equal(body, "Body here");
|
|
43
|
+
assert.deepEqual(parseFrontmatter("no frontmatter"), { data: {}, body: "no frontmatter" });
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("parseModelRef splits provider/id and accepts a bare id", () => {
|
|
47
|
+
assert.deepEqual(parseModelRef("openai-codex/gpt-5.6-terra"), { provider: "openai-codex", id: "gpt-5.6-terra" });
|
|
48
|
+
assert.deepEqual(parseModelRef("sonnet"), { provider: undefined, id: "sonnet" });
|
|
49
|
+
assert.equal(parseModelRef(" "), undefined);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("parseAgentDefinition builds a definition from the gentle-ai agent format", () => {
|
|
53
|
+
const agent = parseAgentDefinition(EXPLORER, "/home/x/.pi/agent/agents/gentle-ai-explore.md", "global");
|
|
54
|
+
assert.ok(!("error" in agent));
|
|
55
|
+
assert.equal(agent.name, "gentle-ai-explore");
|
|
56
|
+
assert.equal(agent.description, "Read-only exploration and mapping.");
|
|
57
|
+
assert.deepEqual(agent.model, { provider: "openai-codex", id: "gpt-5.6-terra" });
|
|
58
|
+
assert.equal(agent.thinking, "high");
|
|
59
|
+
assert.deepEqual(agent.tools, ["read", "grep", "codegraph"]);
|
|
60
|
+
assert.equal(agent.mode, undefined);
|
|
61
|
+
assert.equal(agent.scope, "global");
|
|
62
|
+
assert.match(agent.instructions, /^You are the read-only explorer\./);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("parseAgentDefinition accepts effort and subagent_mode aliases, csv tools, and names from the file", () => {
|
|
66
|
+
const agent = parseAgentDefinition("---\ndescription: d\neffort: low\nsubagent_mode: background\ntools: read, bash\n---\nbody", "/p/.pi/agents/worker.md", "project");
|
|
67
|
+
assert.ok(!("error" in agent));
|
|
68
|
+
assert.equal(agent.name, "worker");
|
|
69
|
+
assert.equal(agent.thinking, "low");
|
|
70
|
+
assert.equal(agent.mode, AGENT_MODE.BACKGROUND);
|
|
71
|
+
assert.deepEqual(agent.tools, ["read", "bash"]);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("parseAgentDefinition rejects unknown thinking levels, modes, and empty bodies", () => {
|
|
75
|
+
assert.match((parseAgentDefinition("---\nname: a\nthinking: extreme\n---\nbody", "/a.md", "global") as { error: string }).error, /thinking "extreme"/);
|
|
76
|
+
assert.match((parseAgentDefinition("---\nname: a\nsubagent_mode: forever\n---\nbody", "/a.md", "global") as { error: string }).error, /mode "forever"/);
|
|
77
|
+
assert.match((parseAgentDefinition("---\nname: a\n---\n \n", "/a.md", "global") as { error: string }).error, /no instructions/);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("discoverAgents merges the four directories with project over global and subagents over agents", () => {
|
|
81
|
+
const home = join(root, "home");
|
|
82
|
+
const cwd = join(root, "project");
|
|
83
|
+
for (const dir of [".pi/agent/agents", ".pi/agent/subagents"]) mkdirSync(join(home, dir), { recursive: true });
|
|
84
|
+
for (const dir of [".pi/agents", ".pi/subagents"]) mkdirSync(join(cwd, dir), { recursive: true });
|
|
85
|
+
writeFileSync(join(home, ".pi/agent/agents/explore.md"), EXPLORER);
|
|
86
|
+
writeFileSync(join(home, ".pi/agent/agents/shared.md"), "---\ndescription: global agents\n---\nglobal");
|
|
87
|
+
writeFileSync(join(home, ".pi/agent/subagents/shared.md"), "---\ndescription: global subagents\n---\nglobal sub");
|
|
88
|
+
writeFileSync(join(cwd, ".pi/agents/shared.md"), "---\ndescription: project agents\n---\nproject");
|
|
89
|
+
writeFileSync(join(cwd, ".pi/agents/broken.md"), "---\nthinking: nope\n---\nx");
|
|
90
|
+
writeFileSync(join(cwd, ".pi/agents/notes.txt"), "ignored");
|
|
91
|
+
const { agents, errors } = discoverAgents({ cwd, home });
|
|
92
|
+
assert.deepEqual(agents.map((agent) => `${agent.name}:${agent.description}:${agent.scope}`), ["gentle-ai-explore:Read-only exploration and mapping.:global", "shared:project agents:project"]);
|
|
93
|
+
assert.equal(errors.length, 1);
|
|
94
|
+
assert.match(errors[0], /broken\.md/);
|
|
95
|
+
assert.deepEqual(discoverAgents({ cwd: join(root, "empty"), home: join(root, "nohome") }), { agents: [], errors: [] });
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("profile agent roots isolate global definitions and subagents.json while explicit homes keep their fallback", () => {
|
|
99
|
+
const cwd = join(root, "profile-project");
|
|
100
|
+
const principal = join(root, "pi-principal", "agent");
|
|
101
|
+
const lab = join(root, "pi-lab", "agent");
|
|
102
|
+
for (const [agentHome, name, model] of [[principal, "principal", "openai/principal"], [lab, "lab", "openai/lab"]] as const) {
|
|
103
|
+
mkdirSync(join(agentHome, "agents"), { recursive: true });
|
|
104
|
+
writeFileSync(join(agentHome, "agents", `${name}.md`), `---\ndescription: ${name}\n---\n${name}`);
|
|
105
|
+
writeFileSync(join(agentHome, "subagents.json"), JSON.stringify({ default_model: model }));
|
|
106
|
+
}
|
|
107
|
+
assert.deepEqual(agentDirectories({ cwd, home: join(root, "legacy-home"), agentHome: principal }).slice(0, 2).map(({ dir }) => dir), [join(principal, "agents"), join(principal, "subagents")]);
|
|
108
|
+
assert.deepEqual(discoverAgents({ cwd, home: join(root, "legacy-home"), agentHome: principal }).agents.map((agent) => agent.name), ["principal"]);
|
|
109
|
+
assert.deepEqual(discoverAgents({ cwd, home: join(root, "legacy-home"), agentHome: lab }).agents.map((agent) => agent.name), ["lab"]);
|
|
110
|
+
assert.equal(loadAgentsConfig({ cwd, home: join(root, "legacy-home"), agentHome: principal }).defaultModel?.id, "principal");
|
|
111
|
+
assert.equal(loadAgentsConfig({ cwd, home: join(root, "legacy-home"), agentHome: lab }).defaultModel?.id, "lab");
|
|
112
|
+
assert.equal(agentDirectories({ cwd, home: join(root, "legacy-home") })[0].dir, join(root, "legacy-home", ".pi", "agent", "agents"));
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("parseAgentsConfig applies defaults, validates values, and silently ignores the retired total-timeout key", () => {
|
|
116
|
+
const config = parseAgentsConfig({ default_model: "openai-codex/gpt-6-astra", default_effort: "medium", max_concurrency: 3, timeout_ms: 1000, stall_timeout_ms: 12_000, model_profiles: { explore: { model: "openai-codex/gpt-5.6-terra", effort: "high" } } }, { max_concurrency: 2, timeout_ms: 500, model_profiles: { explore: { effort: "low" }, worker: { model: "anthropic/claude-sonnet-5" } } });
|
|
117
|
+
assert.deepEqual(config.defaultModel, { provider: "openai-codex", id: "gpt-6-astra" });
|
|
118
|
+
assert.equal(config.defaultThinking, "medium");
|
|
119
|
+
assert.equal(config.maxConcurrency, 2);
|
|
120
|
+
assert.equal(config.stallTimeoutMs, 12_000);
|
|
121
|
+
assert.equal("timeoutMs" in config, false, "legacy timeout_ms must not become an active runtime setting");
|
|
122
|
+
assert.deepEqual(config.modelProfiles.explore, { model: { provider: "openai-codex", id: "gpt-5.6-terra" }, thinking: "low" });
|
|
123
|
+
assert.deepEqual(config.modelProfiles.worker, { model: { provider: "anthropic", id: "claude-sonnet-5" }, thinking: undefined });
|
|
124
|
+
const defaults = parseAgentsConfig(undefined, undefined);
|
|
125
|
+
assert.equal(defaults.maxConcurrency, 5);
|
|
126
|
+
assert.equal("timeoutMs" in defaults, false);
|
|
127
|
+
assert.equal(defaults.stallTimeoutMs, 4 * 60_000);
|
|
128
|
+
assert.equal(defaults.defaultMode, AGENT_MODE.TASK);
|
|
129
|
+
assert.equal(defaults.historyMaxTasks, 200);
|
|
130
|
+
assert.equal(parseAgentsConfig({ max_concurrency: "many", default_effort: "wild", default_mode: "background" }, undefined).maxConcurrency, 5);
|
|
131
|
+
assert.equal(parseAgentsConfig({ default_mode: "background" }, undefined).defaultMode, AGENT_MODE.BACKGROUND);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("resolveAgentProfile prefers the profile, then the definition, then the defaults", () => {
|
|
135
|
+
const config = parseAgentsConfig({ default_model: "openai-codex/gpt-6-astra", default_effort: "medium", model_profiles: { "gentle-ai-explore": { effort: "high" } } }, undefined);
|
|
136
|
+
const explore = parseAgentDefinition(EXPLORER, "/x/explore.md", "global");
|
|
137
|
+
assert.ok(!("error" in explore));
|
|
138
|
+
assert.deepEqual(resolveAgentProfile(explore, config), { model: { provider: "openai-codex", id: "gpt-5.6-terra" }, thinking: "high", source: { model: "definition", thinking: "profile" } });
|
|
139
|
+
const bare = parseAgentDefinition("---\nname: bare\n---\nbody", "/x/bare.md", "global");
|
|
140
|
+
assert.ok(!("error" in bare));
|
|
141
|
+
assert.deepEqual(resolveAgentProfile(bare, config), { model: { provider: "openai-codex", id: "gpt-6-astra" }, thinking: "medium", source: { model: "default", thinking: "default" } });
|
|
142
|
+
assert.deepEqual(resolveAgentProfile(bare, parseAgentsConfig(undefined, undefined)).source, { model: "unresolved", thinking: "unresolved" });
|
|
143
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
import { PassThrough } from "node:stream";
|
|
3
|
+
import type { ChildLike } from "../lib/agents-runner.ts";
|
|
4
|
+
|
|
5
|
+
// A fake `pi --mode rpc` child: answers every command with a success
|
|
6
|
+
// response, records what the host wrote, and lets tests emit events.
|
|
7
|
+
|
|
8
|
+
export interface FakeChild {
|
|
9
|
+
child: ChildLike;
|
|
10
|
+
written: Array<Record<string, unknown>>;
|
|
11
|
+
emit(event: Record<string, unknown>): void;
|
|
12
|
+
exit(code: number): void;
|
|
13
|
+
fail(message: string): void;
|
|
14
|
+
killed: string[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function fakeChild(options: { exitOnKill?: boolean; pid?: number } = {}): FakeChild {
|
|
18
|
+
const emitter = new EventEmitter();
|
|
19
|
+
const stdin = new PassThrough();
|
|
20
|
+
const stdout = new PassThrough();
|
|
21
|
+
const written: Array<Record<string, unknown>> = [];
|
|
22
|
+
const killed: string[] = [];
|
|
23
|
+
let buffer = "";
|
|
24
|
+
stdin.on("data", (chunk: Buffer) => {
|
|
25
|
+
buffer += chunk.toString();
|
|
26
|
+
const lines = buffer.split("\n");
|
|
27
|
+
buffer = lines.pop() ?? "";
|
|
28
|
+
for (const line of lines) {
|
|
29
|
+
const command = JSON.parse(line) as Record<string, unknown>;
|
|
30
|
+
written.push(command);
|
|
31
|
+
if (command.type === "extension_ui_response") continue;
|
|
32
|
+
const data = command.type === "get_state" ? { sessionFile: "/sessions/child.jsonl" } : undefined;
|
|
33
|
+
stdout.write(`${JSON.stringify({ type: "response", id: command.id, command: command.type, success: true, data })}\n`);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
const child: ChildLike = {
|
|
37
|
+
pid: options.pid,
|
|
38
|
+
stdin,
|
|
39
|
+
stdout,
|
|
40
|
+
stderr: new PassThrough(),
|
|
41
|
+
kill: (signal) => {
|
|
42
|
+
killed.push(String(signal ?? "SIGTERM"));
|
|
43
|
+
if (options.exitOnKill !== false) queueMicrotask(() => emitter.emit("exit", 0, signal ?? "SIGTERM"));
|
|
44
|
+
return true;
|
|
45
|
+
},
|
|
46
|
+
on: (event, listener) => {
|
|
47
|
+
emitter.on(event, listener);
|
|
48
|
+
return child;
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
return { child, written, killed, emit: (event) => stdout.write(`${JSON.stringify(event)}\n`), exit: (code) => emitter.emit("exit", code, null), fail: (message) => emitter.emit("error", new Error(message)) };
|
|
52
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdtempSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import test, { after } from "node:test";
|
|
6
|
+
import { historyDir, loadHistory, loadStoredTask, pruneHistory, saveTask } from "../lib/agents-history.ts";
|
|
7
|
+
import { applyTaskEvent, emptyThread, TASK_EVENT, TASK_STATUS, TaskStore, type TaskRecord } from "../lib/agents-protocol.ts";
|
|
8
|
+
|
|
9
|
+
// Gentle Agents history: JSON per task, async, lazy, pruned by count.
|
|
10
|
+
|
|
11
|
+
const root = mkdtempSync(join(tmpdir(), "gentle-agents-history-"));
|
|
12
|
+
after(() => rmSync(root, { recursive: true, force: true }));
|
|
13
|
+
const dir = join(root, "tasks");
|
|
14
|
+
|
|
15
|
+
function task(id: string, createdAt: number): TaskRecord {
|
|
16
|
+
return { id, agent: "explore", mode: "task", prompt: "p", label: "p", cwd: "/r", parentSessionId: "s", status: TASK_STATUS.COMPLETED, createdAt, startedAt: createdAt, endedAt: createdAt + 5, model: "m", thinking: undefined, sessionPath: null, error: null, result: "ok", lastStep: "done", lastActivityAt: createdAt, turns: 1, toolCalls: 0, tokens: 10, cost: 0.01 };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
test("historyDir follows an isolated agent profile while explicit homes retain the default fallback", () => {
|
|
20
|
+
assert.equal(historyDir("/home/x", "/profiles/pi-principal/agent"), join("/profiles/pi-principal/agent", "gentle-agents", "tasks"));
|
|
21
|
+
assert.equal(historyDir("/home/x", "/profiles/pi-lab/agent"), join("/profiles/pi-lab/agent", "gentle-agents", "tasks"));
|
|
22
|
+
assert.equal(historyDir("/home/x"), join("/home/x", ".pi", "agent", "gentle-agents", "tasks"));
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("saveTask writes a task with its thread and loadStoredTask reads it back", async () => {
|
|
26
|
+
const thread = applyTaskEvent(emptyThread(), { type: TASK_EVENT.TEXT, text: "hello" });
|
|
27
|
+
await saveTask(dir, task("a1", 1000), thread);
|
|
28
|
+
const stored = await loadStoredTask(dir, "a1");
|
|
29
|
+
assert.equal(stored?.task.result, "ok");
|
|
30
|
+
assert.deepEqual(stored?.thread.items, [{ kind: "text", text: "hello" }]);
|
|
31
|
+
assert.equal(await loadStoredTask(dir, "missing"), undefined);
|
|
32
|
+
assert.equal(await loadStoredTask(dir, "../etc/passwd"), undefined);
|
|
33
|
+
assert.deepEqual(readdirSync(dir), ["a1.json"], "no temp file is left behind");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("loadHistory skips broken files, sorts newest first, and pruneHistory keeps the newest N", async () => {
|
|
37
|
+
await saveTask(dir, task("b2", 3000), emptyThread());
|
|
38
|
+
await saveTask(dir, task("c3", 2000), emptyThread());
|
|
39
|
+
writeFileSync(join(dir, "junk.json"), "{not json");
|
|
40
|
+
writeFileSync(join(dir, "shape.json"), JSON.stringify({ task: { id: 1 } }));
|
|
41
|
+
assert.deepEqual((await loadHistory(dir)).map((entry) => entry.task.id), ["b2", "c3", "a1"]);
|
|
42
|
+
assert.equal(await pruneHistory(dir, 2), 1);
|
|
43
|
+
assert.deepEqual((await loadHistory(dir)).map((entry) => entry.task.id), ["b2", "c3"]);
|
|
44
|
+
assert.deepEqual(await loadHistory(join(root, "nowhere")), []);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("TaskStore.restore adds a stored task without clobbering a live one", () => {
|
|
48
|
+
const store = new TaskStore();
|
|
49
|
+
const thread = applyTaskEvent(emptyThread(), { type: TASK_EVENT.NOTE, text: "restored" });
|
|
50
|
+
assert.equal(store.restore(task("r1", 1000), thread), true);
|
|
51
|
+
assert.equal(store.thread("r1").items.length, 1);
|
|
52
|
+
assert.equal(store.restore({ ...task("r1", 1000), result: "other" }, emptyThread()), false);
|
|
53
|
+
assert.equal(store.get("r1")?.result, "ok");
|
|
54
|
+
});
|