gentle-pi 2.5.0 → 2.6.1

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.
Files changed (188) hide show
  1. package/README.md +139 -30
  2. package/assets/agents/gentle-ai-worker.md +4 -0
  3. package/assets/agents/jd-fix-agent.md +18 -0
  4. package/assets/agents/jd-judge-a.md +1 -1
  5. package/assets/agents/jd-judge-b.md +1 -1
  6. package/assets/agents/sdd-apply.md +7 -5
  7. package/assets/agents/sdd-archive.md +5 -3
  8. package/assets/agents/sdd-design.md +4 -0
  9. package/assets/agents/sdd-explore.md +4 -0
  10. package/assets/agents/sdd-init.md +4 -0
  11. package/assets/agents/sdd-onboard.md +4 -0
  12. package/assets/agents/sdd-proposal.md +4 -0
  13. package/assets/agents/sdd-remediate.md +37 -0
  14. package/assets/agents/sdd-research.md +26 -3
  15. package/assets/agents/sdd-spec.md +4 -0
  16. package/assets/agents/sdd-status.md +9 -75
  17. package/assets/agents/sdd-sync.md +4 -0
  18. package/assets/agents/sdd-tasks.md +4 -0
  19. package/assets/agents/sdd-verify.md +5 -3
  20. package/assets/chains/sdd-full.chain.md +4 -0
  21. package/assets/chains/sdd-plan.chain.md +4 -0
  22. package/assets/chains/sdd-verify.chain.md +4 -0
  23. package/assets/migrations/managed-assets-v2.5.0.json +7 -0
  24. package/assets/orchestrator-delegation.md +21 -3
  25. package/assets/sdd-orchestrator-workflow.md +54 -21
  26. package/assets/support/sdd-status-contract.md +34 -90
  27. package/contracts/telemetry/runtime-aggregate-v1.schema.json +67 -0
  28. package/docs/telemetry.md +57 -1
  29. package/docs/windows-startup-console-visibility.md +18 -0
  30. package/extensions/ask-user-choice.ts +143 -15
  31. package/extensions/codegraph-tools.ts +1 -0
  32. package/extensions/gentle-agents.ts +795 -50
  33. package/extensions/gentle-ai.ts +2033 -322
  34. package/extensions/gentle-shell.ts +145 -42
  35. package/extensions/gentle-todo.ts +47 -12
  36. package/extensions/quiet-tools.ts +1 -0
  37. package/extensions/runtime-metrics.ts +121 -0
  38. package/extensions/sdd-init.ts +2 -2
  39. package/extensions/startup-banner.ts +52 -75
  40. package/lib/agent-profiles.ts +550 -0
  41. package/lib/agents-completion-delivery.ts +72 -0
  42. package/lib/agents-config.ts +7 -10
  43. package/lib/agents-history.ts +9 -1
  44. package/lib/agents-messaging.ts +187 -0
  45. package/lib/agents-protocol.ts +77 -5
  46. package/lib/agents-runner.ts +548 -26
  47. package/lib/agents-thread-view.ts +57 -0
  48. package/lib/agents-view-layout.ts +40 -0
  49. package/lib/agents-view.ts +548 -191
  50. package/lib/agents-widget.ts +33 -14
  51. package/lib/gentle-ai-binary.ts +3 -1
  52. package/lib/gentle-ai-renderer.ts +8 -6
  53. package/lib/native-review-cli.ts +288 -1
  54. package/lib/orchestrator-presence.ts +337 -0
  55. package/lib/profiles-orchestrator.ts +203 -0
  56. package/lib/review-candidate-view-owner.ts +296 -46
  57. package/lib/review-candidate-view.ts +30 -20
  58. package/lib/review-consent-component.ts +247 -0
  59. package/lib/review-consent-ui.ts +53 -8
  60. package/lib/review-host-relay.ts +28 -0
  61. package/lib/review-integration-v2.ts +187 -5
  62. package/lib/review-last-event-controller.ts +7 -4
  63. package/lib/review-reminder-receipt.ts +74 -0
  64. package/lib/review-session-standing-permission.ts +27 -6
  65. package/lib/runtime-metrics-children.ts +197 -0
  66. package/lib/runtime-metrics-delivery.ts +68 -0
  67. package/lib/runtime-metrics-native.ts +174 -0
  68. package/lib/runtime-metrics-policy.ts +51 -0
  69. package/lib/runtime-metrics.ts +308 -0
  70. package/lib/sdd-preflight.ts +362 -81
  71. package/lib/sdd-research-capabilities.ts +228 -0
  72. package/lib/sdd-status.ts +29 -7
  73. package/lib/session-worktree-registry.ts +118 -0
  74. package/lib/shell-bar.ts +47 -1
  75. package/lib/shell-card.ts +1 -4
  76. package/lib/shell-changes-view.ts +362 -37
  77. package/lib/shell-changes.ts +81 -1
  78. package/lib/shell-prompt.ts +11 -15
  79. package/lib/shell-sidebar-banner.ts +11 -0
  80. package/lib/shell-sidebar-layout.ts +213 -0
  81. package/lib/shell-sidebar.ts +41 -0
  82. package/lib/shell-todo.ts +28 -11
  83. package/lib/telemetry-trigger.ts +2 -0
  84. package/package.json +6 -3
  85. package/runtime/gentle-ai-binary.mjs +3 -1
  86. package/runtime/native-review-cli.mjs +288 -1
  87. package/runtime/review-integration-v2.mjs +187 -5
  88. package/runtime/telemetry-trigger.mjs +2 -0
  89. package/scripts/build-runtime-modules.mjs +9 -1
  90. package/scripts/check-types.mjs +125 -0
  91. package/scripts/gentle-ai-installer.mjs +10 -10
  92. package/scripts/install-gentle-ai.mjs +12 -0
  93. package/scripts/install-tui-mode-setting.mjs +114 -0
  94. package/scripts/test-packed-runner.mjs +16 -2
  95. package/scripts/types-baseline.json +95 -0
  96. package/scripts/verify-package-files.mjs +4 -2
  97. package/skills/_shared/review-ledger-contract.md +17 -1
  98. package/skills/issue-creation/SKILL.md +3 -3
  99. package/skills/judgment-day/SKILL.md +17 -3
  100. package/skills/judgment-day/references/prompts-and-formats.md +14 -3
  101. package/tests/agent-profiles.test.ts +722 -0
  102. package/tests/agents-completion-delivery.test.ts +94 -0
  103. package/tests/agents-config.test.ts +62 -0
  104. package/tests/agents-fake-child.ts +15 -1
  105. package/tests/agents-grouping.test.ts +179 -0
  106. package/tests/agents-integration.test.ts +100 -0
  107. package/tests/agents-messaging.test.ts +94 -0
  108. package/tests/agents-protocol.test.ts +45 -0
  109. package/tests/agents-queries.test.ts +190 -0
  110. package/tests/agents-responsive.test.ts +43 -0
  111. package/tests/agents-runner.test.ts +571 -14
  112. package/tests/agents-thread-view.test.ts +45 -0
  113. package/tests/agents-view.test.ts +476 -65
  114. package/tests/agents-widget.test.ts +31 -1
  115. package/tests/artifact-language.test.ts +25 -2
  116. package/tests/ask-user-choice.test.ts +169 -3
  117. package/tests/asset-installation-runtime.test.ts +108 -0
  118. package/tests/autonomous-guard.test.ts +116 -1
  119. package/tests/codegraph-tools.test.ts +2 -1
  120. package/tests/delegated-key-learnings-contract.test.ts +1 -1
  121. package/tests/devbinary/native-review-parity.devtest.ts +2 -0
  122. package/tests/feature-request-form.test.ts +67 -0
  123. package/tests/fixtures/agents-messaging-child.mjs +5 -0
  124. package/tests/fixtures/runtime-metrics-native-batches.json +6 -0
  125. package/tests/gentle-agents.test.ts +1454 -33
  126. package/tests/gentle-ai-binary.test.ts +7 -2
  127. package/tests/gentle-ai-installer.test.ts +47 -47
  128. package/tests/gentle-ai-renderer.test.ts +38 -0
  129. package/tests/gentle-ai.test.ts +944 -4
  130. package/tests/gentle-shell.test.ts +303 -12
  131. package/tests/gentle-todo.test.ts +54 -10
  132. package/tests/install-tui-mode-setting.test.ts +324 -0
  133. package/tests/issue-creation-skill.test.ts +22 -0
  134. package/tests/model-routing-authority.test.ts +12 -0
  135. package/tests/native-review-capability-contract.test.ts +23 -1
  136. package/tests/native-review-cli.test.ts +277 -3
  137. package/tests/native-review-parity.test.ts +14 -7
  138. package/tests/native-sdd-attempt-authority.test.ts +7 -2
  139. package/tests/orchestrator-presence.test.ts +389 -0
  140. package/tests/package-manifest.test.ts +232 -7
  141. package/tests/profiles-orchestrator.test.ts +208 -0
  142. package/tests/quiet-tool-rendering.test.ts +1 -0
  143. package/tests/rdd-aware-verification-contract.test.ts +10 -0
  144. package/tests/review-agent-end-preflight.test.ts +332 -24
  145. package/tests/review-candidate-view.test.ts +304 -6
  146. package/tests/review-consent-ui.test.ts +352 -0
  147. package/tests/review-contract-prompt.test.ts +14 -0
  148. package/tests/review-controller-native-routing.test.ts +563 -3
  149. package/tests/review-controller.test.ts +1 -1
  150. package/tests/review-host-relay-restart-parity.test.ts +142 -1
  151. package/tests/review-host-relay-routing.test.ts +364 -4
  152. package/tests/review-host-relay.test.ts +29 -0
  153. package/tests/review-integration-v2-forward.test.ts +44 -0
  154. package/tests/review-integration-v2.test.ts +164 -0
  155. package/tests/review-last-event-closure.test.ts +105 -1
  156. package/tests/review-ledger-contract.test.ts +61 -6
  157. package/tests/review-reminder-receipt.test.ts +62 -0
  158. package/tests/review-session-standing-permission-controller.test.ts +52 -4
  159. package/tests/review-session-standing-permission.test.ts +30 -0
  160. package/tests/runtime-harness.mjs +447 -39
  161. package/tests/runtime-metrics-children.test.ts +207 -0
  162. package/tests/runtime-metrics-delivery.test.ts +85 -0
  163. package/tests/runtime-metrics-extension.test.ts +196 -0
  164. package/tests/runtime-metrics-model.test.ts +76 -0
  165. package/tests/runtime-metrics-native.test.ts +275 -0
  166. package/tests/runtime-metrics-policy.test.ts +62 -0
  167. package/tests/runtime-metrics.test.ts +187 -0
  168. package/tests/sdd-agent-tools.test.ts +10 -1
  169. package/tests/sdd-execution-routing-contract.test.ts +28 -0
  170. package/tests/sdd-managed-runtime-settlement.test.ts +331 -0
  171. package/tests/sdd-native-managed-uptake.test.ts +253 -0
  172. package/tests/sdd-planning-routing-contract.test.ts +45 -0
  173. package/tests/sdd-preflight.test.ts +252 -8
  174. package/tests/sdd-research-capabilities.test.ts +256 -0
  175. package/tests/sdd-research-live.test.ts +241 -0
  176. package/tests/sdd-selection-transport.test.ts +504 -0
  177. package/tests/sdd-status.test.ts +51 -0
  178. package/tests/session-worktree-registry.test.ts +135 -0
  179. package/tests/shell-card.test.ts +24 -3
  180. package/tests/shell-changes-view.test.ts +471 -8
  181. package/tests/shell-changes.test.ts +168 -0
  182. package/tests/shell-prompt.test.ts +28 -6
  183. package/tests/shell-sidebar-banner.test.ts +23 -0
  184. package/tests/shell-sidebar-layout.test.ts +387 -0
  185. package/tests/shell-sidebar.test.ts +50 -0
  186. package/tests/shell-todo.test.ts +100 -11
  187. package/tests/startup-banner.test.ts +126 -0
  188. package/tests/telemetry-trigger.test.ts +3 -1
@@ -15,94 +15,43 @@ Any phase that selects, continues, applies, verifies, syncs, or archives an SDD
15
15
 
16
16
  ## Native Engine
17
17
 
18
- - For file-backed `openspec` or `both` sessions with an `openspec/` directory, use Gentle Pi's local SDD status engine as the artifact-state authority. It resolves the local artifact graph without consulting RDD authority or receipts.
19
- - For non-authoritative stores (`engram`, `none`, and `both` without an `openspec/` directory), do not treat disk status output as authoritative; follow Engine Authority by Store below.
20
- - Runtime-attempt authority is different from artifact dispatch: normal runtime-bearing OpenSpec and Engram continuations MUST bracket external execution with `gentle-ai sdd-attempt acquire|settle --cwd <repo> --change <change>`. Their bounded result contains only `proceed`, `blocked`, or `complete` plus an opaque continuation token when required, and MAY carry `settle_obligation` on a `proceed`. The Git-common-dir immutable chain remains the sole authority for ordinals, cumulative attempt/line budgets, runtime evidence, and atomic bound remediation.
21
- - A phase actor launched BY a parent that already holds a `proceed`-state acquire for that exact work unit is a distinct call/process, not a fresh continuation: it MUST NOT `acquire` again blind. Colliding with its own parent's active attempt is not a genuine `blocked: active_attempt` (#2291). It authenticates as that SAME attempt by passing the parent's returned token on its own `acquire --token <token>` call: a token matching the ledger's live active attempt returns `proceed` with that same token and zero mutation, while a non-matching token gets the ordinary `blocked: active_attempt` naming the real active token.
22
- - When `blockedReasons` is non-empty, do not proceed to terminal, archive, or apply work. Return or report `blockedReasons` and stop unless `nextRecommended` is `verify`, in which case verification may run only to remediate or refresh evidence for the blockers. When `nextRecommended` is `resolve-blockers`, always report `blockedReasons` and stop. When `nextRecommended` is a planning token (`propose`, `spec`, `design`, or `tasks`), launch the corresponding planning phase — missing planning artifacts are the expected output of those phases, not genuine blockers.
23
- - `nextRecommended` is a bounded machine token for routing, not human prose. Route only by `nextRecommended` and dependency states. Human-readable explanation belongs in `blockedReasons`, not `nextRecommended`.
24
- - If the binary is unavailable, fall back to this prompt contract and the manual status schema below. Manual fallback status MUST stay shape-compatible with the native status JSON even when values are reconstructed manually.
18
+ - `gentle-ai sdd-status --contract gentle-ai.sdd-status/v2` is the sole status authority for every store. It is read-only: inspect its native projection unchanged and never launch a phase, prepare consent, or grant roots while reading it.
19
+ - If native status is unavailable, malformed, or does not select the requested change/workspace, stop and report that failure. Do not construct a local status, infer readiness from artifacts, substitute continuation, or bypass it through Engram.
20
+ - `nextRecommended`, `dependencies`, `blockedReasons`, `actionContext`, and optional `phaseInstructions` are producer facts. Route only by their typed values, never by prose or a local lifecycle graph. A genuine blocker's human-readable explanation belongs in `blockedReasons`; a non-blocking diagnostic belongs in `notes`; neither belongs in `nextRecommended`.
21
+ - Runtime-attempt authority is separate from status: runtime-bearing work uses the provider `sdd-attempt acquire|settle` flow and its `proceed`, `blocked`, or `complete` result.
22
+ - Only an explicitly authorized `gentle-ai sdd-continue` may prepare a missing change-instance marker. `ensureChangeInstanceMarker` has no status caller; its sole production path is `PrepareChangeInstanceConsent` through `sdd-continue`.
23
+
24
+ ## Bounded Planning Routing
25
+
26
+ For authoritative native status, route only by the bounded `nextRecommended` token and dependency states; never infer a route from prose. Keep genuine blockers in `blockedReasons` and non-blocking diagnostics in `notes`, never in `nextRecommended`, and report them without discarding them to enable a route.
27
+
28
+ | `nextRecommended` | Planning route |
29
+ | --- | --- |
30
+ | `propose` | `sdd-proposal` |
31
+ | `spec` | `sdd-spec` |
32
+ | `design` | `sdd-design` |
33
+ | `tasks` | `sdd-tasks` |
34
+
35
+ Native unprefixed tokens are the only automatic planning routes. Prefixed or locally derived status tokens never authorize a phase.
36
+
37
+ These planning routes remain runnable when missing planning artifacts leave `dependencies.apply: blocked`; do not require apply readiness to produce those artifacts. This is a planning-only exception, not permission to run apply or another blocked non-planning phase.
38
+
39
+ Before any planning launch, stop for ambiguous change selection, unresolved session preflight, or unsafe action context. Carry `actionContext` and prove planned writes are within the authoritative workspace or allowed edit roots; workspace-planning without allowed edit roots remains read-only. Planning does not bypass the init guard, pre-proposal gate, or phase approval requirements.
40
+
41
+ ## Bounded Execution Routing
42
+
43
+ | Native `nextRecommended` | Pi executor |
44
+ | --- | --- |
45
+ | `apply` | `sdd-apply` |
46
+ | `verify` | `sdd-verify` |
47
+ | `remediate` | `sdd-remediate` |
48
+ | `archive` | `sdd-archive` |
49
+
50
+ Execute only a native selected action whose dependency and `actionContext` permit it. Unknown, malformed, blocked, or unsupported actions stop before work; no local route, prefixed token, or prose can replace them. `notes` is separate from `blockedReasons` and never gates: report a non-empty `notes` value as informational and proceed when the dependency and `blockedReasons` gates allow. Manual `sdd-sync` remains its intentional local resolver and is never an automatic native-status dispatch.
25
51
 
26
52
  ## Status Schema
27
53
 
28
- Return status as markdown with these fields, or equivalent JSON when the host supports it:
29
-
30
- ```yaml
31
- schemaName: spec-driven
32
- changeName: <change-name>
33
- artifactStore: openspec | engram | both | none
34
- planningHome:
35
- root: <project-or-openspec-root>
36
- changesDir: <openspec/changes or memory topic prefix>
37
- changeRoot: <openspec/changes/<change> or memory topic prefix>
38
- artifactPaths:
39
- proposal: [<path-or-topic>]
40
- specs: [<path-or-topic>]
41
- design: [<path-or-topic>]
42
- tasks: [<path-or-topic>]
43
- applyProgress: [<path-or-topic>]
44
- verifyReport: [<path-or-topic>]
45
- syncReport: [<path-or-topic>]
46
- contextFiles:
47
- proposal: [<concrete readable files/topics>]
48
- specs: [<concrete readable files/topics>]
49
- design: [<concrete readable files/topics>]
50
- tasks: [<concrete readable files/topics>]
51
- applyProgress: [<concrete readable files/topics>]
52
- verifyReport: [<concrete readable files/topics>]
53
- syncReport: [<concrete readable files/topics>]
54
- artifacts:
55
- proposal: missing | done | partial
56
- specs: missing | done | partial
57
- design: missing | done | partial
58
- tasks: missing | done | partial
59
- applyProgress: missing | done | partial
60
- verifyReport: missing | done | partial
61
- syncReport: missing | done | partial
62
- taskProgress: # implementation-owned plus malformed unresolved rows
63
- total: 0
64
- complete: 0
65
- remaining: 0
66
- unchecked: []
67
- deferredParentActions:
68
- total: 0
69
- complete: 0
70
- remaining: 0
71
- unchecked: []
72
- taskArtifactErrors: []
73
- applyState: blocked | all_done | ready | not_applicable
74
- dependencies:
75
- apply: blocked | ready | all_done | not_applicable
76
- verify: blocked | ready | all_done | not_applicable
77
- sync: blocked | ready | all_done | not_applicable
78
- archive: blocked | ready | all_done | not_applicable
79
- actionContext:
80
- mode: repo-local | workspace-planning
81
- workspaceRoot: <absolute path>
82
- allowedEditRoots: [<absolute paths>]
83
- warnings: []
84
- nextRecommended: <bounded-machine-token>
85
- isNonAuthoritative: false # boolean; true when the native engine is not authoritative for the store
86
- ```
87
-
88
- ## Task Ownership
89
-
90
- New task checkboxes end with the terminal marker `<!-- sdd-owner: implementation -->`. An unmarked legacy checkbox is implementation-owned. Supported legacy non-implementation rows are informational only. Any line containing `sdd-owner` that is unsupported, duplicated, or non-terminal is malformed: add its exact line to `taskArtifactErrors` and `blockedReasons`, and count it as unresolved implementation work even when checked. `taskProgress` reports implementation work.
91
-
92
- ## Apply State
93
-
94
- - `blocked`: required apply artifacts are missing, task selection is ambiguous, malformed ownership markers exist, or action context makes edits unsafe.
95
- - `all_done`: tasks artifact exists and every implementation task is checked `[x]`.
96
- - `ready`: tasks artifact exists, at least one implementation task remains unchecked, and edit scope is safe.
97
- - `not_applicable`: emitted for non-authoritative stores (see Engine Authority by Store). This is NOT a blocker.
98
-
99
- ## Dependency States
100
-
101
- - `apply` is `ready` only when specs, design, and tasks are available and task progress is not all done.
102
- - `verify` is ready after implementation completion when tasks are complete or apply-progress exists. RDD authority and receipts never gate the apply -> verify -> sync -> archive route. Unchecked implementation tasks remain CRITICAL blockers for full archive readiness.
103
- - `sync` is `ready` only when verify-report exists and has no unresolved `FAIL`, `BLOCKED`, `CRITICAL`, or verification blockers. `engram`/`none` modes may mark sync `not_applicable`.
104
- - `archive` is `ready` only when verify-report exists, sync is complete or not applicable, and implementation tasks are complete. CRITICAL verification issues have no override. Explicit recorded exceptions are limited to non-critical partial archives or stale-checkbox reconciliation when apply-progress/verify-report prove completion.
105
- - `not_applicable`: emitted for non-authoritative stores (engram, none, and both when no `openspec/` directory exists) when `nextRecommended: "resolve-via-engram"` is active. `not_applicable` is NOT a gate failure — readiness must be resolved from Engram instead of from these fields.
54
+ Consume the native v2 projection (`schemaName: gentle-ai.sdd-status`, `schemaVersion: 2`) losslessly. Its producer-defined selection, artifact locators, task progress, seven dependencies, `actionContext`, `blockedReasons`, optional execution instructions, remediation state, and `nextRecommended` are status facts, not a Pi schema to recreate.
106
55
 
107
56
  ## Action Context Guard
108
57
 
@@ -112,11 +61,6 @@ The orchestrator MUST carry `actionContext` into any phase launch.
112
61
  - If `allowedEditRoots` is present, only edit or move files within those roots.
113
62
  - If a phase cannot prove a file is inside the authoritative workspace or allowed edit roots, stop and ask for clarification.
114
63
 
115
- ## Engine Authority by Store
116
-
117
- - `openspec` and `both` (when `openspec/` directory exists): the local SDD status engine resolves artifact state from disk and is authoritative. Phase executors must obey it.
118
- - `engram`, `none`, and `both` (when `openspec/` directory does NOT exist): the local engine cannot read Engram artifacts. It returns `nextRecommended: "resolve-via-engram"` and empty `blockedReasons`. This output is **non-authoritative**. The orchestrator must resolve readiness directly from Engram using the Engram memory tools injected by the memory provider on the change topic keys (`sdd/{change-name}/proposal`, `sdd/{change-name}/spec`, etc.) instead of relying on the engine's dependency states. The `artifactStore` field still reflects the real chosen store value (e.g. `"both"`) and must not be rewritten.
119
-
120
64
  ## Native Runtime Attempt Authority
121
65
 
122
66
  The compact SDD runtime attempt authority is separate from artifact dispatch and status. It is artifact-store agnostic: the same acquire/settle discipline applies to `openspec`, `engram`, `both`, and `none` stores. Its payload MUST NOT be embedded in the SDD v1 status schema above; status reports artifact state only, never attempt tokens or attempt counters. No OpenSpec or Engram attempt ledger may be created or mirrored by Pi.
@@ -0,0 +1,67 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "title": "gentle-ai.telemetry-runtime-aggregate/v1",
4
+ "description": "Sanitized stdin for gentle-ai telemetry runtime send --json. Maximum UTF-8 input 16384 bytes. One best-effort send attempt, no client persistence or retry. Rows are independent observations, not a session composition. No caller identity is accepted.",
5
+ "type": "object", "additionalProperties": false,
6
+ "required": ["schema", "registry", "host", "rows"],
7
+ "properties": {
8
+ "schema": {"const": "gentle-ai.telemetry-runtime-aggregate/v1"},
9
+ "registry": {"const": 1},
10
+ "host": {"enum": ["claude-code", "opencode", "codex", "pi"]},
11
+ "rows": {"type": "array", "minItems": 1, "maxItems": 32, "items": {"$ref": "#/$defs/row"}}
12
+ },
13
+ "$defs": {
14
+ "metric": {"oneOf": [{"type": "integer", "minimum": 0, "maximum": 999999999999}, {"type": "null"}, {"const": "unsupported"}]},
15
+ "count": {"type": "integer", "minimum": 0, "maximum": 999999999999},
16
+ "token": {
17
+ "type": "object", "additionalProperties": false,
18
+ "required": ["reported", "unavailable", "unsupported", "sum"],
19
+ "properties": {
20
+ "reported": {"$ref": "#/$defs/count"}, "unavailable": {"$ref": "#/$defs/count"},
21
+ "unsupported": {"$ref": "#/$defs/count"}, "sum": {"$ref": "#/$defs/count"}
22
+ },
23
+ "if": {"properties": {"reported": {"const": 0}}},
24
+ "then": {"properties": {"sum": {"const": 0}}}
25
+ },
26
+ "duration": {
27
+ "type": "object", "additionalProperties": false, "required": ["kind", "measured_count", "sum_ms"],
28
+ "properties": {"kind": {}, "measured_count": {}, "sum_ms": {}},
29
+ "oneOf": [
30
+ {"properties": {"kind": {"const": "unavailable"}, "measured_count": {"const": 0}, "sum_ms": {"type": "null"}}},
31
+ {"properties": {"kind": {"enum": ["request", "message"]}, "measured_count": {"type": "integer", "minimum": 1, "maximum": 999999999999}, "sum_ms": {"type": "number", "minimum": 0, "maximum": 999999999999}}}
32
+ ]
33
+ },
34
+ "effort": {"enum": ["off", "minimal", "low", "medium", "high", "xhigh", "max", "not_selected", "unknown", "custom", "unavailable", "unsupported"]},
35
+ "model": {
36
+ "type": "object", "additionalProperties": false, "required": ["provider", "id"],
37
+ "properties": {
38
+ "provider": {"type": "string", "maxLength": 32},
39
+ "id": {"type": "string", "maxLength": 64}
40
+ },
41
+ "anyOf": [
42
+ {"properties": {"provider": {"pattern": "^[a-z0-9][a-z0-9-]{0,31}$"}, "id": {"pattern": "^(claude|gpt|o[1-9]|codex|gemini|gemma|deepseek|glm|qwen|qwq|kimi|moonshot|llama|codellama|mistral|mixtral|codestral|devstral|magistral|ministral|minimax|grok|phi|nemotron|jamba|hunyuan|doubao|ernie|mimo|granite|olmo|smollm|starcoder|titan)[a-z0-9]*([-_.:][a-z0-9]+){0,8}$"}}},
43
+ {"properties": {"provider": {"const": "unknown"}, "id": {"const": "unknown"}}},
44
+ {"properties": {"provider": {"const": "custom"}, "id": {"const": "custom"}}},
45
+ {"properties": {"provider": {"const": "opencode"}, "id": {"const": "custom"}}}
46
+ ]
47
+ },
48
+ "row": {
49
+ "type": "object", "additionalProperties": false,
50
+ "required": ["model", "model_evidence", "agent_kind", "agent_class", "selected_effort", "effective_effort", "launches", "responses", "input_tokens", "output_tokens", "cache_read_tokens", "cache_creation_tokens", "reasoning_tokens", "total_tokens", "error_category", "duration"],
51
+ "properties": {
52
+ "reasoning_tokens": {"$ref": "#/$defs/token"},
53
+ "total_tokens": {"$ref": "#/$defs/token"},
54
+ "agent_class": {"enum": ["orchestrator", "worker", "explore", "verify", "unknown", "sdd-init", "sdd-explore", "sdd-research", "sdd-propose", "sdd-spec", "sdd-design", "sdd-tasks", "sdd-apply", "sdd-verify", "sdd-archive", "sdd-onboard", "sdd-status", "sdd-sync", "jd-judge-a", "jd-judge-b", "jd-fix-agent", "review-risk", "review-readability", "review-reliability", "review-resilience", "review-refuter", "review-validator"]},
55
+ "error_category": {"enum": ["none", "unknown", "auth", "output_length", "aborted", "api", "rate_limit", "server"]},
56
+ "duration": {"$ref": "#/$defs/duration"},
57
+ "model": {"$ref": "#/$defs/model"},
58
+ "model_evidence": {"enum": ["selected", "response", "unknown"]},
59
+ "agent_kind": {"enum": ["orchestrator", "built_in", "custom", "unknown"]},
60
+ "selected_effort": {"$ref": "#/$defs/effort"}, "effective_effort": {"$ref": "#/$defs/effort"},
61
+ "launches": {"$ref": "#/$defs/metric", "description": "Source-observed event count only; null when not observed. Never reconstructed from sessions."}, "responses": {"$ref": "#/$defs/metric", "description": "Source-observed response occurrence count, independent of token coverage and success."},
62
+ "input_tokens": {"$ref": "#/$defs/token"}, "output_tokens": {"$ref": "#/$defs/token"},
63
+ "cache_read_tokens": {"$ref": "#/$defs/token"}, "cache_creation_tokens": {"$ref": "#/$defs/token"}
64
+ }
65
+ }
66
+ }
67
+ }
package/docs/telemetry.md CHANGED
@@ -1,6 +1,62 @@
1
1
  # Telemetry
2
2
 
3
- `gentle-pi` does not collect anything itself. [`gentle-ai`](https://github.com/Gentleman-Programming/gentle-ai) (issue [#4309](https://github.com/Gentleman-Programming/gentle-ai/issues/4309)) owns anonymous usage telemetry end to end: install and heartbeat events, the exact fields sent, rate limiting, and every opt-out. See gentle-ai's own README/docs for that contract. Gentle Pi's only involvement is a best-effort nudge that asks the local binary to act.
3
+ Runtime usage telemetry is best effort: an available usage event gets at most one
4
+ asynchronous attempt through `gentle-ai telemetry runtime send --json`. Busy,
5
+ failed, disabled, or cancelled attempts are discarded silently. There is no
6
+ metrics disk storage, outbox, retry, backoff, cooldown, daemon, or session reconstruction.
7
+
8
+ The production encoder uses the byte-identical [native schema mirror](../contracts/telemetry/runtime-aggregate-v1.schema.json).
9
+ [The synthetic fixture](../tests/fixtures/runtime-metrics-native-batches.json) pins its
10
+ SHA-256 and exact one-shot stdin bytes. No old intake fallback is used. These are
11
+ fake-subprocess tests, not a live collector or deployment verification.
12
+
13
+ ## Runtime usage flow
14
+
15
+ 1. A finalized primary assistant message or child completion supplies available usage.
16
+ Child-host extensions do not independently consume primary usage.
17
+ 2. Pi builds event-local sanitized rows, not cumulative session totals. An occupied
18
+ attempt slot discards the event; it never queues it for later.
19
+ 3. One cancellable immediate defers binary verification and subprocess launch beyond
20
+ the provider callback. The native command owns fresh policy and exactly one POST.
21
+ 4. Only `stored`, `duplicate`, `discarded`, or `disabled` results are recognized.
22
+ All are terminal. `stored` and `duplicate` reflect collector acknowledgement,
23
+ not client persistence; Pi retains nothing and never retries.
24
+
25
+ There is no policy or capability subprocess before send, and no ingest or flush call.
26
+ The packaged binary is verified; development overrides are refused for runtime usage.
27
+ Missing binaries and unsupported commands discard without installation or fallback.
28
+
29
+ Replacement and shutdown cancel an unstarted attempt or request termination of its
30
+ child immediately, without a wait loop or final send. The process slot remains busy
31
+ until actual close, preventing overlap even if a cancelled process is slow to exit.
32
+ A one-second process timeout requests termination; it is not a retry timer or a hard
33
+ bound on synchronous binary verification or event-loop stalls.
34
+
35
+ ## Data and source limits
36
+
37
+ Wire fields are the schema/registry, host, public model with evidence, available
38
+ selected/effective effort, orchestrator or known built-in subagent class, source
39
+ launch/response occurrence coverage, six token coverages, explicitly reported typed
40
+ duration, and a sanitized error category. Occurrences never represent sessions.
41
+ Prompts, responses, code, paths, private names, source IDs, and raw errors never enter
42
+ the payload. There is no `batch_id`; native creates the remote `delivery_id`.
43
+
44
+ One event produces 1–32 rows within 16 KiB. Oversized or invalid events discard
45
+ whole rather than splitting into multiple sends. Child launch selection is a separate
46
+ row with zero response-token coverage, never substituted for per-response evidence.
47
+
48
+ - Token coverage distinguishes reported, unavailable, and unsupported values. Pi's
49
+ SDK-positive counters are usable; zero defaults and absence do not prove reported zero.
50
+ - Selected model/effort is captured at the request hook, separately from response
51
+ model and effective-effort evidence. Ambiguous request sequences discard selection.
52
+ - Pi hooks do not correlate requests/retries reliably, so this adapter does not infer duration.
53
+ - Child configuration classification uses packaged built-in definitions, not agent names.
54
+ Launch configuration is not proof of the model or selected effort of each child response.
55
+ - Bounded live child observations remain in RAM until completion. No completed child
56
+ event is retained for forwarding. A completion without usage creates no usage send.
57
+ - Primary object deduplication uses weak tombstones; copied objects are distinct.
58
+ Child completion tombstones are capped at 256 per extension session. Source IDs
59
+ stay local and are never exported. No session history is read or reconstructed.
4
60
 
5
61
  ## What Gentle Pi does
6
62
 
@@ -0,0 +1,18 @@
1
+ # Windows Startup Console Capture
2
+
3
+ Use this protocol to validate the startup Git child processes on a real Windows desktop. Linux CI can verify spawn configuration but cannot observe Windows console windows.
4
+
5
+ ## Capture
6
+
7
+ 1. Start a Windows window-event trace before launching Pi. Capture `EVENT_OBJECT_SHOW`, its timestamp, HWND, owner PID, and owner session.
8
+ 2. Deliberately launch a known visible, short-lived console as a positive control. If its show event is absent, the capture is **INCONCLUSIVE**.
9
+ 3. Build or install the candidate Gentle Pi package and start `pi` from a repository whose path contains spaces. Leave it idle for at least 10 seconds so the startup identity lookups (`git rev-parse --show-toplevel` and `git rev-parse --git-common-dir`), branch lookup, initial shell scan, and repeated poll run.
10
+ 4. Correlate each show event with `GetWindowThreadProcessId`, then use its timestamp, host PID/session, and Procmon process-creation ancestry and command line to associate it with a Pi-launched Git invocation. The HWND can be owned by `conhost.exe`, OpenConsole, or Windows Terminal rather than `git.exe`.
11
+
12
+ ## Expected Result
13
+
14
+ No `WS_VISIBLE` show event is associated with the Git invocation for the startup identity lookups, banner branch lookup, initial shell scan, or repeated shell polling. A visible show event proves window visibility, not that it was unobscured on screen. Process start alone is not visible-window evidence. If event-to-invocation association cannot be established, report **INCONCLUSIVE**, not pass.
15
+
16
+ ## Scope
17
+
18
+ This protocol does not cover user-configured external editors. Their inherited-stdio launch is intentional and may open a visible window.
@@ -1,6 +1,6 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import { DynamicBorder } from "@earendil-works/pi-coding-agent";
3
- import { Text } from "@earendil-works/pi-tui";
3
+ import { Container, Input, isKeyRelease, matchesKey, Text, type KeybindingsManager, type TuiMouseEvent } from "@earendil-works/pi-tui";
4
4
  import { type Static, Type } from "typebox";
5
5
  import { NativeChoiceList } from "../lib/native-choice-list.ts";
6
6
  import { createNativeFullscreenInteraction } from "../lib/native-fullscreen-interaction.ts";
@@ -25,6 +25,9 @@ const ChoiceParamsSchema = Type.Object(
25
25
  maxItems: 4,
26
26
  description: "Two to four ordered closed options",
27
27
  }),
28
+ allowCustomResponse: Type.Optional(Type.Boolean({
29
+ description: "Opt in to an Other… free-text response. Never enable for provider-owned consent prompts, maintenance authorizations, or any exact opaque-token decision.",
30
+ })),
28
31
  },
29
32
  { additionalProperties: false },
30
33
  );
@@ -42,9 +45,108 @@ interface ChoiceDetails {
42
45
  question: string;
43
46
  options: ChoiceOption[];
44
47
  selection?: ChoiceSelection;
48
+ customResponse?: string;
45
49
  cancelled?: true;
46
50
  }
47
51
 
52
+ type ChoiceResult = ChoiceSelection | { customResponse: string };
53
+
54
+ class CustomResponseEditor extends Container {
55
+ private readonly input = new Input({ prompt: "> ", placeholder: "Type your response" });
56
+ private readonly validationText = new Text("", 1, 0);
57
+ private readonly keybindings: KeybindingsManager | undefined;
58
+ private readonly onSubmit: (value: string) => void;
59
+ private readonly onCancel: () => void;
60
+
61
+ constructor(
62
+ keybindings: KeybindingsManager | undefined,
63
+ onSubmit: (value: string) => void,
64
+ onCancel: () => void,
65
+ ) {
66
+ super();
67
+ this.keybindings = keybindings;
68
+ this.onSubmit = onSubmit;
69
+ this.onCancel = onCancel;
70
+ this.input.focused = true;
71
+ this.addChild(new Text("Custom response", 1, 0));
72
+ this.addChild(this.input);
73
+ this.addChild(this.validationText);
74
+ this.addChild(new Text("Submit to continue • Cancel to return to choices", 1, 0));
75
+ }
76
+
77
+ handleInput(data: string): void {
78
+ if (isKeyRelease(data)) return;
79
+ if (this.matches(data, "tui.select.cancel")) {
80
+ this.onCancel();
81
+ return;
82
+ }
83
+ if (this.matches(data, "tui.input.submit")) {
84
+ const value = this.input.getValue();
85
+ if (value.trim().length > 0) this.onSubmit(value);
86
+ else this.validationText.setText("Response cannot be empty.");
87
+ this.invalidate();
88
+ return;
89
+ }
90
+ if (this.matches(data, "tui.editor.deleteCharBackward")) {
91
+ this.input.handleInput(data);
92
+ this.validationText.setText("");
93
+ this.invalidate();
94
+ return;
95
+ }
96
+ this.input.handleInput(data);
97
+ this.validationText.setText("");
98
+ this.invalidate();
99
+ }
100
+
101
+ private matches(
102
+ data: string,
103
+ binding: "tui.select.cancel" | "tui.input.submit" | "tui.editor.deleteCharBackward",
104
+ ): boolean {
105
+ if (this.keybindings?.matches) return this.keybindings.matches(data, binding);
106
+ const key = binding === "tui.select.cancel" ? "escape"
107
+ : binding === "tui.input.submit" ? "enter" : "backspace";
108
+ return matchesKey(data, key);
109
+ }
110
+ }
111
+
112
+ class ChoiceModeView extends Container {
113
+ private editing = false;
114
+ private readonly list: NativeChoiceList<{ id: string; label: string; description: string }>;
115
+ private readonly editor: CustomResponseEditor;
116
+
117
+ constructor(
118
+ list: NativeChoiceList<{ id: string; label: string; description: string }>,
119
+ editor: CustomResponseEditor,
120
+ ) {
121
+ super();
122
+ this.list = list;
123
+ this.editor = editor;
124
+ }
125
+
126
+ showEditor(): void {
127
+ this.editing = true;
128
+ this.invalidate();
129
+ }
130
+
131
+ showList(): void {
132
+ this.editing = false;
133
+ this.invalidate();
134
+ }
135
+
136
+ handleInput(data: string): void {
137
+ if (this.editing) this.editor.handleInput(data);
138
+ else this.list.handleInput(data);
139
+ }
140
+
141
+ override handleMouse(event: TuiMouseEvent) {
142
+ return this.editing ? undefined : this.list.handleMouse(event);
143
+ }
144
+
145
+ override render(width: number): string[] {
146
+ return (this.editing ? this.editor : this.list).render(width);
147
+ }
148
+ }
149
+
48
150
  function reconcileToolAvailability(pi: ExtensionAPI, interactiveTui: boolean): void {
49
151
  const active = pi.getActiveTools();
50
152
  const isActive = active.includes(CHOICE_TOOL_NAME);
@@ -62,10 +164,12 @@ function resultDetails(params: ChoiceParams): ChoiceDetails {
62
164
  export default function askUserChoice(pi: ExtensionAPI): void {
63
165
  pi.registerTool({
64
166
  name: CHOICE_TOOL_NAME,
167
+ renderShell: "self",
65
168
  label: "Ask User Choice",
66
- description: "Ask one strictly closed single-select question with two to four ordered options. It never accepts free-text or multiple selections.",
169
+ description: "Ask one single-select question with two to four ordered options. A custom response is available only when allowCustomResponse is explicitly enabled.",
67
170
  promptGuidelines: [
68
- "Use ask_user_choice only for one exactly representable closed single-select question with 2-4 ordered options; never use it for free-text or multi-select input.",
171
+ "Use ask_user_choice only for one exactly representable single-select question with 2-4 ordered options. Enable allowCustomResponse only when free text is safe and intended.",
172
+ "Never enable allowCustomResponse for provider-owned consent prompts, maintenance authorizations, or any decision that requires an exact opaque token.",
69
173
  ],
70
174
  parameters: ChoiceParamsSchema,
71
175
  executionMode: "sequential",
@@ -79,37 +183,54 @@ export default function askUserChoice(pi: ExtensionAPI): void {
79
183
  label: option.label,
80
184
  description: option.description,
81
185
  }));
82
- let selection: ChoiceSelection | undefined;
186
+ const customItemId = "choice-custom-response";
187
+ if (params.allowCustomResponse === true) {
188
+ items.push({ id: customItemId, label: "Other…", description: "Provide a custom response" });
189
+ }
190
+ let selection: ChoiceResult | undefined;
83
191
  try {
84
192
  pi.events.emit(ASK_USER_CHOICE_BLOCKED_EVENT, { active: true });
85
- selection = await ctx.ui.custom<ChoiceSelection | undefined>((tui, theme, keybindings, done) => {
193
+ selection = await ctx.ui.custom<ChoiceResult | undefined>((tui, theme, keybindings, done) => {
86
194
  const list = new NativeChoiceList(items, {
87
195
  selectedPrefix: (text) => theme.fg("accent", text),
88
196
  selectedText: (text) => theme.fg("accent", text),
89
197
  description: (text) => theme.fg("muted", text),
90
198
  hoverBackground: (text) => theme.bg("toolPendingBg", text),
91
199
  }, keybindings);
92
- const container = createNativeFullscreenInteraction({
93
- keyboardTarget: list,
94
- requestRender: () => tui.requestRender(),
95
- mouseObserver: list.createMouseObserver(() => tui.requestRender()),
96
- });
97
200
  let completed = false;
98
- const finish = (result: ChoiceSelection | undefined) => {
201
+ const finish = (result: ChoiceResult | undefined) => {
99
202
  if (completed) return;
100
203
  completed = true;
101
204
  list.setDisabled(true);
102
205
  done(result);
103
206
  };
207
+ let view: ChoiceModeView;
208
+ const editor = new CustomResponseEditor(
209
+ keybindings,
210
+ (value) => finish({ customResponse: value }),
211
+ () => view.showList(),
212
+ );
213
+ view = new ChoiceModeView(list, editor);
214
+ const container = createNativeFullscreenInteraction({
215
+ keyboardTarget: view,
216
+ requestRender: () => tui.requestRender(),
217
+ mouseObserver: list.createMouseObserver(() => tui.requestRender()),
218
+ });
104
219
  list.onSelect = (item) => {
105
- const index = items.indexOf(item);
106
- const option = params.options[index];
107
- if (option) finish({ value: option.value, label: option.label, index: index + 1 });
220
+ if (item.id === customItemId) {
221
+ view.showEditor();
222
+ tui.requestRender();
223
+ }
224
+ else {
225
+ const index = items.indexOf(item);
226
+ const option = params.options[index];
227
+ if (option) finish({ value: option.value, label: option.label, index: index + 1 });
228
+ }
108
229
  };
109
230
  list.onCancel = () => finish(undefined);
110
231
  container.addChild(new DynamicBorder((text: string) => theme.fg("accent", text)));
111
232
  container.addChild(new Text(theme.fg("accent", theme.bold(params.question)), 1, 0));
112
- container.addChild(list);
233
+ container.addChild(view);
113
234
  container.addChild(new Text(theme.fg("dim", "↑↓ navigate • Enter select • Esc cancel"), 1, 0));
114
235
  container.addChild(new DynamicBorder((text: string) => theme.fg("accent", text)));
115
236
  return container;
@@ -124,6 +245,12 @@ export default function askUserChoice(pi: ExtensionAPI): void {
124
245
  details: { ...resultDetails(params), cancelled: true },
125
246
  };
126
247
  }
248
+ if ("customResponse" in selection) {
249
+ return {
250
+ content: [{ type: "text", text: `User responded: ${selection.customResponse}` }],
251
+ details: { ...resultDetails(params), customResponse: selection.customResponse },
252
+ };
253
+ }
127
254
  return {
128
255
  content: [{ type: "text", text: `User selected: ${selection.index}. ${selection.label} (value: ${selection.value})` }],
129
256
  details: { ...resultDetails(params), selection },
@@ -147,6 +274,7 @@ export default function askUserChoice(pi: ExtensionAPI): void {
147
274
  if (details?.selection) {
148
275
  return new Text(theme.fg("success", `✓ ${details.selection.index}. ${details.selection.label}`), 0, 0);
149
276
  }
277
+ if (details?.customResponse !== undefined) return new Text(theme.fg("success", "✓ Custom response"), 0, 0);
150
278
  return new Text(theme.fg("warning", "Cancelled"), 0, 0);
151
279
  },
152
280
  });
@@ -278,6 +278,7 @@ const runCodeGraphCommand: CodeGraphRunner = async (args, options) => {
278
278
  export function createCodeGraphTool(runner: CodeGraphRunner = runCodeGraphCommand) {
279
279
  return {
280
280
  name: "codegraph",
281
+ renderShell: "self" as const,
281
282
  label: "CodeGraph",
282
283
  description:
283
284
  "Initialize, search, or explore the CodeGraph index for the current Pi workspace only. This tool never accepts a project path or shell command.",