feat: finish Pi and workspace management updates
This commit is contained in:
@@ -6,8 +6,8 @@
|
|||||||
"apiKey": "$ZAI_API_KEY",
|
"apiKey": "$ZAI_API_KEY",
|
||||||
"models": [
|
"models": [
|
||||||
{
|
{
|
||||||
"id": "glm-5.2",
|
"id": "glm-5.3",
|
||||||
"name": "GLM-5.2",
|
"name": "GLM-5.3",
|
||||||
"reasoning": true,
|
"reasoning": true,
|
||||||
"contextWindow": 200000,
|
"contextWindow": 200000,
|
||||||
"maxTokens": 131072
|
"maxTokens": 131072
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"defaultProjectTrust": "always",
|
"defaultProjectTrust": "always",
|
||||||
"enabledModels": [
|
"enabledModels": [
|
||||||
"zai/glm-5.2",
|
"zai/glm-5.3",
|
||||||
"deepseek/deepseek-v4-flash",
|
"deepseek/deepseek-v4-flash",
|
||||||
"deepseek/deepseek-v4-pro",
|
"deepseek/deepseek-v4-pro",
|
||||||
"aritmolab/qwen3.6-35b-a3b"
|
"aritmolab/qwen3.6-35b-a3b"
|
||||||
|
|||||||
@@ -6,13 +6,18 @@ does not mount a Docker socket, and Pi is never updated in a running container.
|
|||||||
## Inspection and configuration
|
## Inspection and configuration
|
||||||
|
|
||||||
```text
|
```text
|
||||||
thothctl --installation /absolute/path/thothii-installation.yaml pi status
|
thothctl pi status
|
||||||
thothctl --installation /absolute/path/thothii-installation.yaml pi doctor
|
thothctl pi doctor
|
||||||
thothctl --installation /absolute/path/thothii-installation.yaml pi test
|
thothctl pi test
|
||||||
thothctl --installation /absolute/path/thothii-installation.yaml pi logs
|
thothctl pi logs
|
||||||
thothctl --installation /absolute/path/thothii-installation.yaml pi configure
|
thothctl pi configure
|
||||||
```
|
```
|
||||||
|
|
||||||
|
When `--installation` is omitted, `thothctl` first uses `THOTHII_INSTALLATION` and otherwise
|
||||||
|
discovers one valid `thothii-installation.yaml` in the current project tree, including an immediate
|
||||||
|
`deploy/*` directory. Use `--installation /absolute/path/thothii-installation.yaml` as an explicit
|
||||||
|
override when the descriptor is outside that tree or more than one installation is available.
|
||||||
|
|
||||||
`status` executes the image-bundled `pi --version`. `doctor` compares that value with both the
|
`status` executes the image-bundled `pi --version`. `doctor` compares that value with both the
|
||||||
container's `PI_VERSION` contract and the `io.thothii.pi.version` image label; a merely nonempty
|
container's `PI_VERSION` contract and the `io.thothii.pi.version` image label; a merely nonempty
|
||||||
version is not sufficient. `doctor` and `test` also require a healthy core, a successful Pi smoke,
|
version is not sufficient. `doctor` and `test` also require a healthy core, a successful Pi smoke,
|
||||||
@@ -25,7 +30,7 @@ models come from the backend's closed model list, and the model choices are rest
|
|||||||
selected provider. In non-interactive use, all choices must be explicit:
|
selected provider. In non-interactive use, all choices must be explicit:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
thothctl --installation /absolute/path/thothii-installation.yaml pi configure \
|
thothctl pi configure \
|
||||||
--provider zai --model glm-5.2 --thinking medium
|
--provider zai --model glm-5.2 --thinking medium
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -70,7 +75,7 @@ secret overrides must never bypass that preflight wrapper.
|
|||||||
Configuration reload is a separate lifecycle operation from an image update:
|
Configuration reload is a separate lifecycle operation from an image update:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
thothctl --installation /absolute/path/thothii-installation.yaml pi restart --yes [--drain]
|
thothctl pi restart --yes [--drain]
|
||||||
```
|
```
|
||||||
|
|
||||||
`--yes` is required after reviewing the planned core recreation. Restart activates the durable
|
`--yes` is required after reviewing the planned core recreation. Restart activates the durable
|
||||||
@@ -104,13 +109,25 @@ recovery rather than deleting recovery material.
|
|||||||
|
|
||||||
## Updating Pi
|
## Updating Pi
|
||||||
|
|
||||||
Every update requires a pinned version, an explicit source, and confirmation:
|
The normal update uses the repository's pinned version and build source automatically:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
thothctl --installation /absolute/path/thothii-installation.yaml pi update \
|
thothctl pi update
|
||||||
--version 0.81.0 --source build --yes
|
thothctl pi update --version 0.81.0
|
||||||
|
```
|
||||||
|
|
||||||
thothctl --installation /absolute/path/thothii-installation.yaml pi update \
|
With no `--version`, the command reads the single default `ARG PI_VERSION=<version>` from
|
||||||
|
`docker/core.Dockerfile` in the selected project. The normal path confirms the explicit update
|
||||||
|
command, drains active sessions without terminating them, builds the candidate, recreates only
|
||||||
|
`core`, verifies it, and promotes it transactionally.
|
||||||
|
|
||||||
|
Advanced registry updates remain available and require an immutable digest:
|
||||||
|
|
||||||
|
```text
|
||||||
|
thothctl pi update \
|
||||||
|
--version 0.81.0 --source build --yes --drain
|
||||||
|
|
||||||
|
thothctl pi update \
|
||||||
--version 0.81.0 --source pull \
|
--version 0.81.0 --source pull \
|
||||||
--image registry.example.invalid/thothii-core@sha256:<64-lowercase-hex-digits> --yes
|
--image registry.example.invalid/thothii-core@sha256:<64-lowercase-hex-digits> --yes
|
||||||
```
|
```
|
||||||
@@ -178,7 +195,7 @@ gate can open.
|
|||||||
For a failed update with `update-state.json`, first run:
|
For a failed update with `update-state.json`, first run:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
thothctl --installation /absolute/path/thothii-installation.yaml pi rollback --yes
|
thothctl pi rollback --yes
|
||||||
```
|
```
|
||||||
|
|
||||||
Rollback restores the image recorded in update state, but it checks restart state before making any
|
Rollback restores the image recorded in update state, but it checks restart state before making any
|
||||||
@@ -189,8 +206,8 @@ reported problem, then use maintenance recovery.
|
|||||||
Inspect and clean a stale durable gate with:
|
Inspect and clean a stale durable gate with:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
thothctl --installation /absolute/path/thothii-installation.yaml pi maintenance status
|
thothctl pi maintenance status
|
||||||
thothctl --installation /absolute/path/thothii-installation.yaml pi maintenance recover --yes
|
thothctl pi maintenance recover --yes
|
||||||
```
|
```
|
||||||
|
|
||||||
`maintenance recover` restores the captured restart image pin and lifecycle override when needed,
|
`maintenance recover` restores the captured restart image pin and lifecycle override when needed,
|
||||||
|
|||||||
@@ -4,14 +4,21 @@ ThothII bundles Pi in the `core` image. Operators use the Pi Management page for
|
|||||||
defaults and the host-side `thothctl` CLI for lifecycle work. A local Pi installation is not
|
defaults and the host-side `thothctl` CLI for lifecycle work. A local Pi installation is not
|
||||||
required.
|
required.
|
||||||
|
|
||||||
In the commands below, replace `/absolute/path/to/thothii-installation.yaml` with the protected
|
Run these commands from the root of the current ThothII checkout or worktree. `thothctl` discovers
|
||||||
installation descriptor created by the [local installation guide](local.md).
|
the valid installation descriptor in that project tree, so it uses the `deploy/` files belonging to
|
||||||
|
the checkout from which you run it. Do not use `~/bin`: `~` is the user home directory, not the
|
||||||
|
project root.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
THTCTL=/absolute/path/to/thothctl
|
mkdir -p bin
|
||||||
INSTALLATION=/absolute/path/to/thothii-installation.yaml
|
go -C tools/thothctl build -o ../../bin/thothctl ./cmd/thothctl
|
||||||
|
THTCTL=./bin/thothctl
|
||||||
```
|
```
|
||||||
|
|
||||||
|
If `thothctl` is already on `PATH`, you may use `THTCTL=thothctl` instead. For an installation
|
||||||
|
stored elsewhere, set `THOTHII_INSTALLATION` or pass
|
||||||
|
`--installation <absolute-path>/thothii-installation.yaml` explicitly.
|
||||||
|
|
||||||
## Choose application defaults
|
## Choose application defaults
|
||||||
|
|
||||||
Use the **Pi Management** page to select the supported provider, model, and reasoning default, then
|
Use the **Pi Management** page to select the supported provider, model, and reasoning default, then
|
||||||
@@ -21,8 +28,8 @@ diagnostics; it never accepts or displays a credential, opens a terminal, or upd
|
|||||||
Alternatively, use the CLI from an administrator terminal:
|
Alternatively, use the CLI from an administrator terminal:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
"$THTCTL" --installation "$INSTALLATION" pi configure
|
"$THTCTL" pi configure
|
||||||
"$THTCTL" --installation "$INSTALLATION" pi configure --provider zai --model glm-5.2 --thinking medium
|
"$THTCTL" pi configure --provider zai --model glm-5.2 --thinking medium
|
||||||
```
|
```
|
||||||
|
|
||||||
Use GUI Save defaults or CLI `pi configure`, not both for the same change. The CLI's interactive
|
Use GUI Save defaults or CLI `pi configure`, not both for the same change. The CLI's interactive
|
||||||
@@ -32,11 +39,11 @@ methods store application defaults in backend installation settings, not in the
|
|||||||
Useful read-only checks are:
|
Useful read-only checks are:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
"$THTCTL" --installation "$INSTALLATION" pi status
|
"$THTCTL" pi status
|
||||||
"$THTCTL" --installation "$INSTALLATION" pi doctor
|
"$THTCTL" pi doctor
|
||||||
"$THTCTL" --installation "$INSTALLATION" pi test
|
"$THTCTL" pi test
|
||||||
"$THTCTL" --installation "$INSTALLATION" pi check
|
"$THTCTL" pi check
|
||||||
"$THTCTL" --installation "$INSTALLATION" pi logs
|
"$THTCTL" pi logs
|
||||||
```
|
```
|
||||||
|
|
||||||
`pi check` is an alias for `pi test`; logs are a sanitized, bounded snapshot with no follow mode.
|
`pi check` is an alias for `pi test`; logs are a sanitized, bounded snapshot with no follow mode.
|
||||||
@@ -72,7 +79,7 @@ After changing the provider catalog, enabled-model policy, or selected credentia
|
|||||||
running application with one confirmed restart:
|
running application with one confirmed restart:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
"$THTCTL" --installation "$INSTALLATION" pi restart --yes --drain
|
"$THTCTL" pi restart --yes --drain
|
||||||
```
|
```
|
||||||
|
|
||||||
`--yes` confirms that core will be recreated. Without `--drain`, restart refuses active sessions;
|
`--yes` confirms that core will be recreated. Without `--drain`, restart refuses active sessions;
|
||||||
@@ -89,18 +96,25 @@ and `thothctl start` or raw Compose commands for this reload workflow.
|
|||||||
|
|
||||||
## Update the bundled Pi version
|
## Update the bundled Pi version
|
||||||
|
|
||||||
`pi update` is for a new bundled Pi version; it is not a configuration reload. Finish or drain
|
`pi update` is for a new bundled Pi version; it is not a configuration reload. The simple command
|
||||||
active work, then choose an explicit source and version. A build update uses this checkout:
|
uses the single `ARG PI_VERSION=...` pin in `docker/core.Dockerfile`, builds that version, waits for
|
||||||
|
active sessions to finish, and recreates only `core`:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
"$THTCTL" --installation "$INSTALLATION" pi update \
|
"$THTCTL" pi update
|
||||||
--version 0.81.0 --source build --yes --drain
|
```
|
||||||
|
|
||||||
|
To build a specific version, pass `--version`; source, confirmation, and drain are automatic for
|
||||||
|
this normal build path:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
"$THTCTL" pi update --version 0.81.0
|
||||||
```
|
```
|
||||||
|
|
||||||
A registry update must use an immutable digest, never a mutable tag:
|
A registry update must use an immutable digest, never a mutable tag:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
"$THTCTL" --installation "$INSTALLATION" pi update \
|
"$THTCTL" pi update \
|
||||||
--version 0.81.0 --source pull \
|
--version 0.81.0 --source pull \
|
||||||
--image registry.example.invalid/thothii-core@sha256:<64-lowercase-hex-digits> \
|
--image registry.example.invalid/thothii-core@sha256:<64-lowercase-hex-digits> \
|
||||||
--yes --drain
|
--yes --drain
|
||||||
@@ -117,24 +131,24 @@ reported recovery state and transaction override. Do not delete `.thothctl`, sta
|
|||||||
containers, or volumes. Inspect status and sanitized logs:
|
containers, or volumes. Inspect status and sanitized logs:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
"$THTCTL" --installation "$INSTALLATION" pi maintenance status
|
"$THTCTL" pi maintenance status
|
||||||
"$THTCTL" --installation "$INSTALLATION" pi status
|
"$THTCTL" pi status
|
||||||
"$THTCTL" --installation "$INSTALLATION" pi logs
|
"$THTCTL" pi logs
|
||||||
```
|
```
|
||||||
|
|
||||||
For a failed update, restore its prior image:
|
For a failed update, restore its prior image:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
"$THTCTL" --installation "$INSTALLATION" pi rollback --yes
|
"$THTCTL" pi rollback --yes
|
||||||
```
|
```
|
||||||
|
|
||||||
For a failed restart, use maintenance recovery instead of rollback. After repairing the reported
|
For a failed restart, use maintenance recovery instead of rollback. After repairing the reported
|
||||||
Docker, disk, or configuration problem, use the same command to complete either safe recovery path:
|
Docker, disk, or configuration problem, use the same command to complete either safe recovery path:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
"$THTCTL" --installation "$INSTALLATION" pi maintenance recover --yes
|
"$THTCTL" pi maintenance recover --yes
|
||||||
"$THTCTL" --installation "$INSTALLATION" pi doctor
|
"$THTCTL" pi doctor
|
||||||
"$THTCTL" --installation "$INSTALLATION" pi test
|
"$THTCTL" pi test
|
||||||
```
|
```
|
||||||
|
|
||||||
`pi rollback --yes` restores the prior update image. `pi maintenance recover --yes` checks both
|
`pi rollback --yes` restores the prior update image. `pi maintenance recover --yes` checks both
|
||||||
|
|||||||
@@ -0,0 +1,171 @@
|
|||||||
|
# Audit critico dei comandi `tht`
|
||||||
|
|
||||||
|
Data: 2026-08-15
|
||||||
|
|
||||||
|
## Scopo
|
||||||
|
|
||||||
|
Questo audit valuta tutti i comandi terminali registrati dall'attuale CLI Python `tht` prima di
|
||||||
|
unificare la CLI di ThothII sotto un solo eseguibile pubblico. La valutazione incrocia:
|
||||||
|
|
||||||
|
- il contratto del workflow Pi in `harness/.pi/skills/tht-sessione/SKILL.md`;
|
||||||
|
- le invocazioni reali del gate in `harness/.pi/extensions/tht-gate.js`;
|
||||||
|
- le invocazioni del backend in `backend/src/tht/tht-runner.ts`;
|
||||||
|
- i job operatore in `backend/src/workspaces/preprocessing-service.ts`;
|
||||||
|
- il migratore in `docker/session-migrate.sh`;
|
||||||
|
- test e documentazione esistenti.
|
||||||
|
|
||||||
|
L'inventario autorevole contiene 76 comandi Typer più il comando callback `doctor`: 77 comandi
|
||||||
|
terminali complessivi.
|
||||||
|
|
||||||
|
## Legenda
|
||||||
|
|
||||||
|
- **WF — intoccabile workflow**: chiamato da Pi, dal gate o dal contratto delle otto fasi. Va
|
||||||
|
conservato con semantica, output JSON ed exit code compatibili. Non deve necessariamente apparire
|
||||||
|
nell'help ordinario dell'utente.
|
||||||
|
- **PL — intoccabile piattaforma**: chiamato dal backend, dai job workspace o dal deployment. Anche
|
||||||
|
questo è un contratto interno, non necessariamente un comando da mostrare all'utente.
|
||||||
|
- **ADV — mantenere avanzato**: non è nel flusso automatico, ma offre una capacità amministrativa o
|
||||||
|
di recupero che sarebbe imprudente perdere. Va nascosto dall'help base.
|
||||||
|
- **ACCORPA**: la capacità serve, ma non merita un comando autonomo.
|
||||||
|
- **RIMUOVI**: il comando non ha chiamanti reali ed è duplicato, superato, pericoloso o incompleto.
|
||||||
|
L'eventuale logica riutilizzata da altri flussi resta una libreria interna.
|
||||||
|
|
||||||
|
## Risultato sintetico
|
||||||
|
|
||||||
|
| Esito | Numero | Conseguenza |
|
||||||
|
|---|---:|---|
|
||||||
|
| WF o PL, intoccabili | 55 | Conservare il contratto; nascondere i primitivi tecnici dall'help base |
|
||||||
|
| ADV o ACCORPA | 8 | Conservare la capacità riducendo la superficie UX |
|
||||||
|
| RIMUOVI | 14 | Eliminare il comando dalla nuova CLI |
|
||||||
|
| **Totale** | **77** | Una sola CLI pubblica molto più semplice, senza riscrivere il workflow vivo |
|
||||||
|
|
||||||
|
## Matrice completa
|
||||||
|
|
||||||
|
### Diagnostica, configurazione e dipendenze
|
||||||
|
|
||||||
|
| Comando | Valutazione |
|
||||||
|
|---|---|
|
||||||
|
| `config check` | **ACCORPA** in `tht doctor`: la validazione della configurazione serve, ma due preflight distinti confondono l'utente. |
|
||||||
|
| `doctor` | **ACCORPA/MANTIENI pubblico** come unico `tht doctor`, includendo controlli host, Compose, storage e configurazione runtime. |
|
||||||
|
| `db ping` | **PL**: il backend lo usa per rifiutare correttamente una nuova sessione quando il DWH non è raggiungibile o non è read-only. Interno. |
|
||||||
|
| `db fetch-ca` | **ACCORPA** in `tht setup` o nella configurazione workspace: utile per TLS, ma non giustifica un comando isolato. |
|
||||||
|
| `ollama ensure` | **PL**: preflight automatico dell'embedder usato dal backend. Interno. |
|
||||||
|
|
||||||
|
### Fasi e decision ledger
|
||||||
|
|
||||||
|
| Comando | Valutazione |
|
||||||
|
|---|---|
|
||||||
|
| `phase advance` | **WF**: il gate lo usa per avanzare solo dopo la decisione umana. Primitivo anti-bypass, quindi interno. |
|
||||||
|
| `phase meta` | **WF**: fornisce al gate la definizione data-driven delle fasi e dei tipi di decisione. Interno. |
|
||||||
|
| `phase reopen` | **WF**: è il percorso canonico per tornare a una fase precedente e invalidare deterministicamente gli artefatti successivi. |
|
||||||
|
| `phase show` | **WF**: il gate lo usa per calcolare la fase corrente. Interno. |
|
||||||
|
| `decision add` | **WF**: persistenza fondamentale delle decisioni del reviewer. Solo gate, non shell utente. |
|
||||||
|
| `decision add-batch` | **WF**: scrittura atomica delle decisioni multiple. Evita ledger parziali. |
|
||||||
|
| `decision add-join-set` | **WF**: sostituzione atomica dell'intero insieme di join. |
|
||||||
|
| `decision list` | **RIMUOVI**: nessun chiamante; `session show --json` contiene già il ledger necessario. |
|
||||||
|
| `decision retract` | **RIMUOVI** dalla CLI: nessun flusso vivo lo invoca e `phase reopen` è il percorso di correzione supportato. La semantica tombstone può restare nel dominio finché utile. |
|
||||||
|
|
||||||
|
### Sessioni
|
||||||
|
|
||||||
|
| Comando | Valutazione |
|
||||||
|
|---|---|
|
||||||
|
| `session archive` | **PL**: usato dalla gestione sessioni del backend. |
|
||||||
|
| `session check` | **WF**: gate oggettivo della fase 5; verifica decisioni e schema linking. |
|
||||||
|
| `session close` | **PL**: usato dal backend. |
|
||||||
|
| `session delete` | **PL**: usato dal backend con i relativi controlli applicativi. |
|
||||||
|
| `session documents` | **WF/PL**: ricostruisce il contesto persistito e alimenta sia Pi sia la GUI. |
|
||||||
|
| `session fail` | **PL**: usato dal backend per rappresentare il fallimento terminale. |
|
||||||
|
| `session finalize` | **WF**: chiusura deterministica della fase finale e indicizzazione della domanda risolta. |
|
||||||
|
| `session list` | **PL**: alimenta la lista sessioni della GUI. |
|
||||||
|
| `session migrate` | **PL**: eseguito dal servizio one-shot di migrazione server; resta interno dietro `tht sessions migrate`. |
|
||||||
|
| `session new` | **WF/PL**: crea la persistenza iniziale della domanda; il backend dipende dal JSON restituito. |
|
||||||
|
| `session preferences get` | **PL**: lettura delle preferenze applicative. Interno. |
|
||||||
|
| `session preferences set` | **PL**: scrittura delle preferenze applicative. Interno. |
|
||||||
|
| `session reopen` | **PL**: riapertura dello stato terminale esposta dalla gestione sessioni. |
|
||||||
|
| `session retrieval-pack` | **WF**: legge il retrieval pack già persistito per il kickoff di Pi. Distinto da `search pack`, che lo costruisce. |
|
||||||
|
| `session set-group` | **PL**: rinomina il raggruppamento dalla GUI. |
|
||||||
|
| `session set-name` | **PL**: rinomina la sessione dalla GUI. |
|
||||||
|
| `session set-question` | **WF**: persiste deterministicamente domanda riscritta e assunzioni. Solo gate. |
|
||||||
|
| `session set-schema-linking` | **WF**: valida e scrive `schema_linking.json`. Solo gate. |
|
||||||
|
| `session show` | **WF/PL**: fonte compatta dello stato persistito per resume, gate e backend. |
|
||||||
|
| `session sync-schema-linking` | **WF**: riproietta deterministicamente il ledger nello schema linking. |
|
||||||
|
| `session unarchive` | **PL**: usato dalla gestione sessioni del backend. |
|
||||||
|
|
||||||
|
### Schema e retrieval
|
||||||
|
|
||||||
|
| Comando | Valutazione |
|
||||||
|
|---|---|
|
||||||
|
| `schema check` | **PL**: validazione delle annotazioni curate nel workflow workspace. |
|
||||||
|
| `schema columns` | **WF**: il gate usa il catalogo colonne per validare e correggere il linking. |
|
||||||
|
| `schema introspect` | **WF**: fallback previsto dal contratto quando manca lo schema fisico; la modalità refresh resta manutenzione. |
|
||||||
|
| `schema render` | **WF**: produce il contesto mschema usato dal modello. |
|
||||||
|
| `schema suggest-fks` | **PL**: comando del flusso operatore per le annotazioni FK curate. |
|
||||||
|
| `search find` | **WF**: ricerca mirata di evidence, valori e formule durante le fasi. |
|
||||||
|
| `search pack` | **WF/PL**: costruisce e persiste il contesto iniziale F1; usato anche dal backend. |
|
||||||
|
|
||||||
|
### CTE, SQL e datamart
|
||||||
|
|
||||||
|
| Comando | Valutazione |
|
||||||
|
|---|---|
|
||||||
|
| `cte info` | **WF**: restituisce SQL persistito, posizione nel piano e ultimo test. |
|
||||||
|
| `cte list` | **RIMUOVI**: nessun chiamante o test; `cte plan`, `cte info` e `session documents` coprono il bisogno. |
|
||||||
|
| `cte next` | **WF**: il gate determina il prossimo CTE da revisionare. |
|
||||||
|
| `cte plan` | **WF**: persiste l'ordine completo dei CTE. |
|
||||||
|
| `cte save` | **WF**: tool deterministico di scrittura usato dal gate. |
|
||||||
|
| `cte test` | **WF**: verifica read-only dei CTE prevista esplicitamente dal contratto. |
|
||||||
|
| `sql validate` | **WF**: validazione strutturale e read-only prima dell'esecuzione. |
|
||||||
|
| `sql preview` | **WF/PL**: preview controllata usata dal modello e dalla GUI. |
|
||||||
|
| `sql set-final` | **WF**: unica scrittura canonica di `sql_final.sql` attraverso il repository di sessione. |
|
||||||
|
| `sql export` | **PL**: esportazione richiesta dalla GUI. |
|
||||||
|
| `sql explain` | **RIMUOVI**: nessun chiamante, test o requisito nel workflow corrente. Si reintroduce solo con un vero passo di analisi del piano. |
|
||||||
|
| `sql save` | **RIMUOVI**: duplica `set-final` ed `export` e permette un percorso di scrittura non usato. |
|
||||||
|
| `datamart generate` | **WF**: fase 8 del workflow. |
|
||||||
|
|
||||||
|
### Memory
|
||||||
|
|
||||||
|
| Comando | Valutazione |
|
||||||
|
|---|---|
|
||||||
|
| `memory promote` | **WF**: preview dei candidati di promozione usata dal gate. |
|
||||||
|
| `memory save-one` | **WF**: persistenza atomica della singola memory approvata. |
|
||||||
|
| `memory search` | **WF**: recupero delle memory riutilizzabili nella fase 2. |
|
||||||
|
| `memory solved-index` | **WF**: recupero manuale previsto se l'indicizzazione al finalize fallisce. |
|
||||||
|
| `memory solved-search` | **WF**: recupero di domande risolte simili nelle fasi successive. |
|
||||||
|
| `memory list` | **ADV**: mantenere per amministrare record errati, ma fuori dall'help base. |
|
||||||
|
| `memory show` | **ADV**: mantenere insieme a `list` per ispezione puntuale. |
|
||||||
|
| `memory update` | **ADV**: mantenere per correggere il merito di una memory senza alterarne la provenienza. |
|
||||||
|
| `memory delete` | **ADV**: mantenere come rimedio selettivo; richiede conferma esplicita nella nuova CLI. |
|
||||||
|
| `memory index` | **ADV**: utile come riparazione/full-resync, ma va presentato come manutenzione e non come uso normale. |
|
||||||
|
| `memory clear` | **RIMUOVI**: distruzione globale non usata; confligge con una UX sicura di backup/ripristino. |
|
||||||
|
| `memory migrate` | **RIMUOVI**: migrazione legacy una tantum senza dati di produzione da preservare. |
|
||||||
|
|
||||||
|
### Preprocessing, evidence e indici
|
||||||
|
|
||||||
|
| Comando | Valutazione |
|
||||||
|
|---|---|
|
||||||
|
| `preprocess dwh` | **PL**: pipeline canonica usata da `tht workspace preprocess dwh/run`. |
|
||||||
|
| `preprocess evidence` | **PL**: pipeline canonica usata da `tht workspace preprocess evidence/run`. |
|
||||||
|
| `vector index-schema` | **PL**: indicizzazione schema usata dal workflow workspace. |
|
||||||
|
| `evidence extract` | **RIMUOVI**: primitivo superato dalla pipeline versionata `preprocess evidence`. Conservare soltanto la logica riusata. |
|
||||||
|
| `evidence index` | **RIMUOVI**: primitivo superato dalla stessa pipeline versionata. |
|
||||||
|
| `lsh build` | **RIMUOVI** come comando: è già uno step di `preprocess dwh`; il builder resta interno. |
|
||||||
|
| `lsh query` | **RIMUOVI**: probe visuale senza chiamanti, test o documentazione operativa. La ricerca applicativa passa da `search find`. |
|
||||||
|
| `vector init` | **RIMUOVI**: il controllo di Qdrant/embedder è ormai coperto dal reconciler di collezione, da `ollama ensure` e dal nuovo `tht doctor`. |
|
||||||
|
|
||||||
|
### Formule di concetto
|
||||||
|
|
||||||
|
| Comando | Valutazione |
|
||||||
|
|---|---|
|
||||||
|
| `formula save` | **RIMUOVI** dalla CLI corrente: nessun chiamante, test o flusso di approvazione lo usa. Conservare il formato/store e la lettura tramite `search find --kind formula`. |
|
||||||
|
| `formula list` | **RIMUOVI**: stesso sottosistema incompleto. Un futuro flusso di curation dovrà progettare insieme creazione, approvazione, elenco e modifica. |
|
||||||
|
|
||||||
|
## Conseguenza per la nuova CLI unica
|
||||||
|
|
||||||
|
La semplificazione migliore non consiste nel rinominare tutti i 55 contratti vivi o nel mostrarli
|
||||||
|
all'utente. Consiste nel mantenere un unico eseguibile `tht` con due livelli di visibilità:
|
||||||
|
|
||||||
|
1. l'help ordinario mostra soltanto setup, lifecycle, backup/restore, Pi e workspace;
|
||||||
|
2. i contratti WF/PL restano invocabili dallo stesso eseguibile, ma sono interni/nascosti e usati da
|
||||||
|
backend, gate e job one-shot.
|
||||||
|
|
||||||
|
In questo modo l'utente vede una CLI piccola, mentre il workflow non subisce una riscrittura inutile
|
||||||
|
e rischiosa. Non serve un secondo eseguibile né un alias `thothctl`.
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
# Proposta maintain-erase-enhance per i comandi `tht`
|
||||||
|
|
||||||
|
Data: 2026-08-15
|
||||||
|
|
||||||
|
## Criterio
|
||||||
|
|
||||||
|
- **MAINTAIN**: il comando resta disponibile senza modifiche sostanziali. Come richiesto, non viene
|
||||||
|
aggiunta una motivazione.
|
||||||
|
- **ERASE**: il comando viene eliminato dalla nuova CLI; la motivazione indica la duplicazione, il
|
||||||
|
superamento o l'assenza di un utilizzo reale.
|
||||||
|
- **ENHANCE**: la capacità viene mantenuta, ma il comando viene migliorato, accorpato o reso più
|
||||||
|
sicuro. La proposta indica l'intervento.
|
||||||
|
|
||||||
|
La proposta copre tutti i 77 comandi terminali dell'attuale CLI Python.
|
||||||
|
|
||||||
|
## Sintesi
|
||||||
|
|
||||||
|
| Proposta | Numero |
|
||||||
|
|---|---:|
|
||||||
|
| MAINTAIN | 55 |
|
||||||
|
| ENHANCE | 8 |
|
||||||
|
| ERASE | 14 |
|
||||||
|
| **Totale** | **77** |
|
||||||
|
|
||||||
|
## Lista completa
|
||||||
|
|
||||||
|
### Diagnostica, configurazione e dipendenze
|
||||||
|
|
||||||
|
| Comando | Proposta |
|
||||||
|
|---|---|
|
||||||
|
| `config check` | **ENHANCE** — incorporare la validazione nel comando pubblico `tht doctor`, mantenendo una funzione interna riutilizzabile e l'output strutturato. Evita due preflight sovrapposti. |
|
||||||
|
| `doctor` | **ENHANCE** — farne l'unica diagnostica multilivello: installazione, descriptor, Compose, storage, configurazione runtime, DWH, Pi, Qdrant ed embedder. Deve offrire output umano e `--json`, senza mutare lo stato. |
|
||||||
|
| `db ping` | **MAINTAIN** |
|
||||||
|
| `db fetch-ca` | **ENHANCE** — integrarlo nel setup guidato del workspace, mostrando endpoint e fingerprint prima della conferma. Può restare disponibile come operazione TLS avanzata, ma non come passaggio manuale obbligatorio. |
|
||||||
|
| `ollama ensure` | **MAINTAIN** |
|
||||||
|
|
||||||
|
### Fasi e decision ledger
|
||||||
|
|
||||||
|
| Comando | Proposta |
|
||||||
|
|---|---|
|
||||||
|
| `phase advance` | **MAINTAIN** |
|
||||||
|
| `phase meta` | **MAINTAIN** |
|
||||||
|
| `phase reopen` | **MAINTAIN** |
|
||||||
|
| `phase show` | **MAINTAIN** |
|
||||||
|
| `decision add` | **MAINTAIN** |
|
||||||
|
| `decision add-batch` | **MAINTAIN** |
|
||||||
|
| `decision add-join-set` | **MAINTAIN** |
|
||||||
|
| `decision list` | **ERASE** — non ha chiamanti reali e duplica il ledger già restituito da `session show --json`. |
|
||||||
|
| `decision retract` | **ERASE** — non è invocato dal workflow corrente; `phase reopen` è il percorso supportato per correggere e invalidare deterministicamente le decisioni. La semantica tombstone può restare nel dominio. |
|
||||||
|
|
||||||
|
### Sessioni
|
||||||
|
|
||||||
|
| Comando | Proposta |
|
||||||
|
|---|---|
|
||||||
|
| `session archive` | **MAINTAIN** |
|
||||||
|
| `session check` | **MAINTAIN** |
|
||||||
|
| `session close` | **MAINTAIN** |
|
||||||
|
| `session delete` | **MAINTAIN** |
|
||||||
|
| `session documents` | **MAINTAIN** |
|
||||||
|
| `session fail` | **MAINTAIN** |
|
||||||
|
| `session finalize` | **MAINTAIN** |
|
||||||
|
| `session list` | **MAINTAIN** |
|
||||||
|
| `session migrate` | **MAINTAIN** |
|
||||||
|
| `session new` | **MAINTAIN** |
|
||||||
|
| `session preferences get` | **MAINTAIN** |
|
||||||
|
| `session preferences set` | **MAINTAIN** |
|
||||||
|
| `session reopen` | **MAINTAIN** |
|
||||||
|
| `session retrieval-pack` | **MAINTAIN** |
|
||||||
|
| `session set-group` | **MAINTAIN** |
|
||||||
|
| `session set-name` | **MAINTAIN** |
|
||||||
|
| `session set-question` | **MAINTAIN** |
|
||||||
|
| `session set-schema-linking` | **MAINTAIN** |
|
||||||
|
| `session show` | **MAINTAIN** |
|
||||||
|
| `session sync-schema-linking` | **MAINTAIN** |
|
||||||
|
| `session unarchive` | **MAINTAIN** |
|
||||||
|
|
||||||
|
### Schema e retrieval
|
||||||
|
|
||||||
|
| Comando | Proposta |
|
||||||
|
|---|---|
|
||||||
|
| `schema check` | **MAINTAIN** |
|
||||||
|
| `schema columns` | **MAINTAIN** |
|
||||||
|
| `schema introspect` | **MAINTAIN** |
|
||||||
|
| `schema render` | **MAINTAIN** |
|
||||||
|
| `schema suggest-fks` | **MAINTAIN** |
|
||||||
|
| `search find` | **MAINTAIN** |
|
||||||
|
| `search pack` | **MAINTAIN** |
|
||||||
|
|
||||||
|
### CTE, SQL e datamart
|
||||||
|
|
||||||
|
| Comando | Proposta |
|
||||||
|
|---|---|
|
||||||
|
| `cte info` | **MAINTAIN** |
|
||||||
|
| `cte list` | **ERASE** — non ha chiamanti o test e sovrappone informazioni già disponibili con `cte plan`, `cte info` e `session documents`. |
|
||||||
|
| `cte next` | **MAINTAIN** |
|
||||||
|
| `cte plan` | **MAINTAIN** |
|
||||||
|
| `cte save` | **MAINTAIN** |
|
||||||
|
| `cte test` | **MAINTAIN** |
|
||||||
|
| `sql validate` | **MAINTAIN** |
|
||||||
|
| `sql preview` | **MAINTAIN** |
|
||||||
|
| `sql set-final` | **MAINTAIN** |
|
||||||
|
| `sql export` | **MAINTAIN** |
|
||||||
|
| `sql explain` | **ERASE** — non è usato né testato dal workflow attuale. Va reintrodotto soltanto se l'analisi del piano diventa un passo esplicito del processo. |
|
||||||
|
| `sql save` | **ERASE** — duplica `sql set-final` e `sql export` e introduce un percorso di scrittura non utilizzato. |
|
||||||
|
| `datamart generate` | **MAINTAIN** |
|
||||||
|
|
||||||
|
### Memory
|
||||||
|
|
||||||
|
| Comando | Proposta |
|
||||||
|
|---|---|
|
||||||
|
| `memory promote` | **MAINTAIN** |
|
||||||
|
| `memory save-one` | **MAINTAIN** |
|
||||||
|
| `memory search` | **MAINTAIN** |
|
||||||
|
| `memory solved-index` | **MAINTAIN** |
|
||||||
|
| `memory solved-search` | **MAINTAIN** |
|
||||||
|
| `memory list` | **ENHANCE** — trasformarlo in una vista amministrativa paginata, con filtri, provenienza, stato e output `--json`; non mostrarlo nell'help base. |
|
||||||
|
| `memory show` | **ENHANCE** — mostrare provenienza immutabile, decisione sorgente, stato dell'indice e riferimenti necessari a una correzione consapevole. |
|
||||||
|
| `memory update` | **ENHANCE** — limitare l'aggiornamento ai campi modificabili, mostrare un diff prima della conferma e impedire modifiche alla provenienza. |
|
||||||
|
| `memory delete` | **ENHANCE** — richiedere identificatore esatto e conferma esplicita, mostrare l'impatto e verificare la rimozione coerente da registro e indice. |
|
||||||
|
| `memory index` | **ENHANCE** — riposizionarlo come comando di repair: prima rileva il drift, poi ricostruisce soltanto con conferma e verifica finale. Non deve sembrare un'operazione ordinaria. |
|
||||||
|
| `memory clear` | **ERASE** — cancellazione globale non usata e troppo facile da eseguire per errore; backup/ripristino e cancellazione selettiva sono percorsi più sicuri. |
|
||||||
|
| `memory migrate` | **ERASE** — migrazione legacy una tantum; non esistono dati di produzione da preservare e la nuova architettura può partire direttamente dal formato corrente. |
|
||||||
|
|
||||||
|
### Preprocessing, evidence e indici
|
||||||
|
|
||||||
|
| Comando | Proposta |
|
||||||
|
|---|---|
|
||||||
|
| `preprocess dwh` | **MAINTAIN** |
|
||||||
|
| `preprocess evidence` | **MAINTAIN** |
|
||||||
|
| `vector index-schema` | **MAINTAIN** |
|
||||||
|
| `evidence extract` | **ERASE** — è un primitivo superato dalla pipeline versionata `preprocess evidence`; l'eventuale logica condivisa resta interna. |
|
||||||
|
| `evidence index` | **ERASE** — è un secondo primitivo superato dalla stessa pipeline, che già gestisce materializzazione, indicizzazione, versionamento e resume. |
|
||||||
|
| `lsh build` | **ERASE** — la costruzione LSH è già uno step di `preprocess dwh`; mantenere due ingressi permette esecuzioni parziali incoerenti. |
|
||||||
|
| `lsh query` | **ERASE** — probe visuale senza chiamanti, test o documentazione operativa; il workflow usa `search find`. |
|
||||||
|
| `vector init` | **ERASE** — il controllo di Qdrant ed embedder è già coperto dal reconciler della collezione, da `ollama ensure` e dal nuovo `tht doctor`. |
|
||||||
|
|
||||||
|
### Formule di concetto
|
||||||
|
|
||||||
|
| Comando | Proposta |
|
||||||
|
|---|---|
|
||||||
|
| `formula save` | **ERASE** — non ha chiamanti, test o un flusso di approvazione completo. Il formato e lo store possono restare disponibili alla ricerca finché non viene progettata una vera curation. |
|
||||||
|
| `formula list` | **ERASE** — appartiene allo stesso sottosistema incompleto; un futuro flusso deve progettare insieme creazione, approvazione, elenco, modifica e cancellazione. |
|
||||||
|
|
||||||
|
## Impatto sulla UX
|
||||||
|
|
||||||
|
I 55 comandi `MAINTAIN` comprendono molti contratti macchina intoccabili. Mantenerli non implica
|
||||||
|
mostrarli tutti nell'help principale. La futura CLI unica può conservare gli stessi percorsi per
|
||||||
|
backend, gate e job, mostrando all'utente soltanto i gruppi operativi di primo livello.
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
# Simplified `thothctl` installation selection and Pi update Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** Allow all existing `thothctl` commands to discover the installation descriptor automatically and make `thothctl pi update` use the checkout's pinned Pi version by default.
|
||||||
|
|
||||||
|
**Architecture:** Add a small config-level resolver that chooses one validated installation descriptor from an explicit flag, environment variable, or bounded upward search from the working directory. Keep the existing Pi lifecycle transaction intact; resolve only the requested version/source at the CLI boundary so the update engine retains its safety and recovery guarantees.
|
||||||
|
|
||||||
|
**Tech Stack:** Go 1.26, Docker Compose v2, existing `tools/thothctl` config and Pi lifecycle packages, Go tests.
|
||||||
|
|
||||||
|
**Spec:** `docs/superpowers/specs/2026-08-14-thothctl-discovery-and-pi-update-design.md`
|
||||||
|
|
||||||
|
## Global Constraints
|
||||||
|
|
||||||
|
- Preserve every existing `pi` subcommand, alias, safety check, and explicit invocation form.
|
||||||
|
- `--installation` remains an explicit override and accepts only an absolute descriptor path named `thothii-installation.yaml`.
|
||||||
|
- Automatic discovery must not recursively scan `.artifacts`, home directories, or unrelated descendants.
|
||||||
|
- The default Pi version is the single `ARG PI_VERSION=<version>` in the selected project's `docker/core.Dockerfile`.
|
||||||
|
- The default Pi update uses the existing transactional build path and must not install an arbitrary network “latest”.
|
||||||
|
- All failures remain sanitized and must not reveal secret values.
|
||||||
|
|
||||||
|
## File Map
|
||||||
|
|
||||||
|
- Create `tools/thothctl/internal/config/discovery.go` and `discovery_test.go` for bounded descriptor resolution and safe diagnostics.
|
||||||
|
- Modify `tools/thothctl/cmd/thothctl/main.go` and `main_test.go` for optional global selection, `pi update` defaults, help text, and dispatch.
|
||||||
|
- Create `tools/thothctl/internal/pi/version.go` and `version_test.go` for reading the project Pi pin.
|
||||||
|
- Modify `tools/thothctl/internal/pi/update.go` and `update_test.go` only if the default request needs a typed source/confirmation adjustment; keep lifecycle internals unchanged otherwise.
|
||||||
|
- Modify `docs/contracts/thothctl-pi.md`, `docs/install/pi-management.md`, and relevant command-contract verification scripts.
|
||||||
|
|
||||||
|
### Task 1: Add bounded installation descriptor discovery
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `tools/thothctl/internal/config/discovery.go`
|
||||||
|
- Test: `tools/thothctl/internal/config/discovery_test.go`
|
||||||
|
|
||||||
|
**Interface:** `func Resolve(explicit string, environment func(string) string, workingDirectory string) (string, error)`.
|
||||||
|
|
||||||
|
- [x] Write failing tests for explicit-path precedence, `THOTHII_INSTALLATION`, `deploy/*/thothii-installation.yaml` discovery, parent discovery, `.artifacts` exclusion, invalid candidates, ambiguity, and no-candidate errors.
|
||||||
|
- [x] Run `cd tools/thothctl && go test ./internal/config -run 'TestResolve' -count=1`; confirm RED because `Resolve` is absent.
|
||||||
|
- [x] Implement a bounded upward walk. At each level inspect only the exact descriptor and immediate `deploy/*/thothii-installation.yaml` entries; skip `.artifacts`; require regular files; validate candidates through `config.Load`; deduplicate canonical paths; fail clearly on zero or multiple valid candidates.
|
||||||
|
- [x] Re-run the focused tests and confirm GREEN.
|
||||||
|
- [x] Refactor only after green, keeping path collection separate from candidate validation.
|
||||||
|
|
||||||
|
### Task 2: Make the global installation option optional
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `tools/thothctl/cmd/thothctl/main.go`
|
||||||
|
- Test: `tools/thothctl/cmd/thothctl/main_test.go`
|
||||||
|
|
||||||
|
- [x] Add failing CLI tests proving `thothctl pi status` works from a project tree, the environment variable is used, an explicit flag wins, ambiguity fails before Docker, and all existing commands retain their dispatch.
|
||||||
|
- [x] Run the focused CLI tests and confirm RED because the current parser requires `--installation`.
|
||||||
|
- [x] Parse optional `--installation`, call `config.Resolve` with `THOTHII_INSTALLATION` and the process working directory, and update help to `thothctl [--installation PATH] <command>`.
|
||||||
|
- [x] Run `cd tools/thothctl && go test ./cmd/thothctl -count=1`; confirm GREEN.
|
||||||
|
|
||||||
|
### Task 3: Default `pi update` to the repository Pi pin
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `tools/thothctl/internal/pi/version.go`
|
||||||
|
- Test: `tools/thothctl/internal/pi/version_test.go`
|
||||||
|
- Modify: `tools/thothctl/cmd/thothctl/main.go`
|
||||||
|
- Test: `tools/thothctl/cmd/thothctl/main_test.go`
|
||||||
|
|
||||||
|
**Interface:** `func ReadPinnedVersion(projectDirectory string) (string, error)`.
|
||||||
|
|
||||||
|
- [x] Add failing tests for one valid Dockerfile pin, missing Dockerfile, duplicate default pins, malformed versions, and `pi update` without `--version`; retain explicit version and advanced pull tests.
|
||||||
|
- [x] Run `cd tools/thothctl && go test ./internal/pi ./cmd/thothctl -run 'Test(ReadPinnedVersion|ParsePiUpdate|RunPiUpdate)' -count=1`; confirm RED.
|
||||||
|
- [x] Read only `docker/core.Dockerfile`, require one default `ARG PI_VERSION=...`, validate it with the existing version grammar, and make the short request select build mode while preserving the lifecycle transaction.
|
||||||
|
- [x] Re-run focused tests and confirm GREEN.
|
||||||
|
|
||||||
|
### Task 4: Update contracts without removing commands
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `docs/contracts/thothctl-pi.md`
|
||||||
|
- Modify: `docs/install/pi-management.md`
|
||||||
|
- Modify: the documentation verification script that asserts the old mandatory update invocation.
|
||||||
|
|
||||||
|
- [x] Document automatic descriptor discovery, the explicit override, `thothctl pi update` as the normal path, `--version` as an explicit pin, and the advanced pull/digest form.
|
||||||
|
- [x] Keep status, doctor, test/check, configure, restart, rollback, maintenance, and logs documented.
|
||||||
|
- [x] Run the targeted documentation checks and `git diff --check`.
|
||||||
|
|
||||||
|
### Task 5: Full verification
|
||||||
|
|
||||||
|
- [x] Run `cd tools/thothctl && go test ./... -count=1`.
|
||||||
|
- [x] Run `cd tools/thothctl && go build ./cmd/thothctl`.
|
||||||
|
- [x] Run the relevant documentation contract script and inspect `git status --short`.
|
||||||
|
- [x] Verify the help text contains the optional form and all existing commands; do not mutate the live Docker installation unless separately requested.
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,44 @@
|
|||||||
|
# Simplified `thothctl` installation selection and Pi update design
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Keep every existing `thothctl pi` subcommand. Make the installation descriptor optional on the command line and resolve it automatically when the operator runs from the project tree. Keep `--installation <path>` as an explicit override for non-standard locations or multiple installations.
|
||||||
|
|
||||||
|
The normal Pi update becomes:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
thothctl pi update
|
||||||
|
```
|
||||||
|
|
||||||
|
When `--version` is omitted, `thothctl` reads the single default `ARG PI_VERSION=<version>` from `docker/core.Dockerfile` in the selected installation's project directory and uses that pinned version with the existing transactional build/update path. An explicit `--version <version>` remains supported. The command does not fetch an arbitrary npm “latest”; the repository pin, lockfile, image labels, and executable must remain consistent.
|
||||||
|
|
||||||
|
## Installation resolution
|
||||||
|
|
||||||
|
Resolution order is:
|
||||||
|
|
||||||
|
1. an explicit `--installation <absolute-path>/thothii-installation.yaml`;
|
||||||
|
2. `THOTHII_INSTALLATION`, when set to an absolute descriptor path;
|
||||||
|
3. automatic discovery from the current working directory and its parents.
|
||||||
|
|
||||||
|
Automatic discovery examines only the exact descriptor at each directory level and the immediate `deploy/*/thothii-installation.yaml` locations. It never recursively scans `.artifacts`, home directories, or unrelated descendants. A candidate must be a regular file and must pass `config.Load`. One valid candidate is selected. No candidates or multiple valid candidates produce an actionable error that names the expected locations and explains how to use `--installation`.
|
||||||
|
|
||||||
|
The resolver is shared by all existing top-level commands, not only `pi`, so `thothctl status`, `start`, `stop`, `doctor`, `workspace`, and the Pi commands have the same invocation rules. Existing explicit invocations remain valid.
|
||||||
|
|
||||||
|
## Safety and compatibility
|
||||||
|
|
||||||
|
- No existing Pi subcommand is removed or renamed.
|
||||||
|
- Existing advanced `pi update --source ... --image ... --yes --drain` syntax remains accepted for compatibility.
|
||||||
|
- The short update path selects build mode and preserves the existing lifecycle lock, maintenance gate, session handling, candidate verification, image selector promotion, and rollback/recovery behavior.
|
||||||
|
- The default update uses the current checkout's declared Pi pin; changing to a newer Pi release still requires updating the repository pin and lockfile in the normal source-update workflow.
|
||||||
|
- Errors and automatic-discovery diagnostics never expose secret file contents.
|
||||||
|
|
||||||
|
## User-facing examples
|
||||||
|
|
||||||
|
```sh
|
||||||
|
thothctl pi update
|
||||||
|
thothctl pi update --version 0.81.0
|
||||||
|
thothctl pi status
|
||||||
|
thothctl pi restart --yes --drain
|
||||||
|
thothctl --installation ~/operator/thothii-installation.yaml pi update
|
||||||
|
```
|
||||||
|
|
||||||
@@ -0,0 +1,235 @@
|
|||||||
|
# Unified `tht` CLI and Product Setup Design
|
||||||
|
|
||||||
|
Date: 2026-08-15
|
||||||
|
|
||||||
|
## Objective
|
||||||
|
|
||||||
|
Turn the repository into a product that can be cloned, bootstrapped once, and then operated with a
|
||||||
|
single normal system command named `tht`. The operator must not build Go manually, create a `bin`
|
||||||
|
directory, remember an installation descriptor path, or use raw Docker commands for normal
|
||||||
|
installation, lifecycle, Pi updates, backup, or restore.
|
||||||
|
|
||||||
|
## Confirmed decisions
|
||||||
|
|
||||||
|
1. The only product command name is `tht`.
|
||||||
|
2. `thothctl` is removed completely. There is no compatibility alias, wrapper, deprecation period,
|
||||||
|
or second installed command.
|
||||||
|
3. The host operator implementation remains a native Go binary so macOS, Linux, and Windows hosts
|
||||||
|
do not require Go, Python, or a virtualenv.
|
||||||
|
4. The existing Python workflow CLI remains inside `core` under the same command name `tht`.
|
||||||
|
Backend and Pi continue to use it there. It is not installed on the host and is not presented in
|
||||||
|
operator documentation. No `tht-runtime` command or namespace is introduced.
|
||||||
|
5. The command audit is binding: 55 commands are `MAINTAIN`, 8 are `ENHANCE`, and 14 are `ERASE`.
|
||||||
|
The detailed decision matrix is in
|
||||||
|
`docs/reports/2026-08-15-tht-command-maintain-erase-enhance.md`.
|
||||||
|
6. Workflow commands called by Pi, the gate, the backend, workspace maintenance, or the session
|
||||||
|
migrator are machine contracts. Their semantics, pristine JSON output, stdin behavior, and exit
|
||||||
|
codes are not changed merely to simplify the operator help.
|
||||||
|
7. The public operator help stays small. Internal workflow commands do not appear in host help.
|
||||||
|
8. `--installation` remains available as an optional override. Normal commands discover the
|
||||||
|
installation from the current project root or worktree.
|
||||||
|
9. `backup` and `restore` are introduced as first-class product commands.
|
||||||
|
10. The final implementation is installed on this Mac and used to rebuild/restart the live stack on
|
||||||
|
port 8080.
|
||||||
|
|
||||||
|
## Command boundary
|
||||||
|
|
||||||
|
### Host operator CLI
|
||||||
|
|
||||||
|
The native host command exposes:
|
||||||
|
|
||||||
|
```text
|
||||||
|
tht setup
|
||||||
|
tht version
|
||||||
|
tht start [--build]
|
||||||
|
tht stop
|
||||||
|
tht status
|
||||||
|
tht doctor [--json]
|
||||||
|
tht logs
|
||||||
|
tht update [--check-only] [--yes] [--drain]
|
||||||
|
tht backup [--output PATH] [--include-secrets --yes] [--drain]
|
||||||
|
tht restore ARCHIVE --yes [--drain]
|
||||||
|
tht sessions migrate --yes
|
||||||
|
tht remove [--yes ID...]
|
||||||
|
tht pi ...
|
||||||
|
tht workspace ...
|
||||||
|
```
|
||||||
|
|
||||||
|
`tht pi` preserves `status`, `doctor`, `test`/`check`, `configure`, `restart`, `update`,
|
||||||
|
`rollback`, `maintenance`, and `logs`. `tht workspace` preserves every currently implemented
|
||||||
|
workspace operation, including the two vector operations missing from the current help.
|
||||||
|
|
||||||
|
### Container workflow CLI
|
||||||
|
|
||||||
|
The Python command tree remains the deterministic protocol used by Pi and the backend. The host
|
||||||
|
installation does not expose these commands as operator shortcuts. This prevents a human operator
|
||||||
|
from bypassing reviewer gates while avoiding a risky rewrite of the live workflow.
|
||||||
|
|
||||||
|
The `MAINTAIN`, `ENHANCE`, and `ERASE` decisions apply exactly as recorded in the command audit.
|
||||||
|
`MAINTAIN` commands retain their current path. `ENHANCE` commands receive the approved safety or
|
||||||
|
diagnostic improvements. `ERASE` commands disappear from Typer registration and active docs; domain
|
||||||
|
functions still used by canonical pipelines remain internal libraries.
|
||||||
|
|
||||||
|
## Bootstrap and PATH installation
|
||||||
|
|
||||||
|
A freshly cloned repository necessarily needs one bootstrap action before `tht` exists:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash scripts/install-tht.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Windows uses:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
powershell -ExecutionPolicy Bypass -File scripts/install-tht.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
The scripts require Docker, build the correct native binary using the repository-pinned Docker
|
||||||
|
builder, verify it, and install it atomically:
|
||||||
|
|
||||||
|
- macOS and Linux: `/usr/local/bin/tht`, requesting elevation only for the final atomic install;
|
||||||
|
- Windows: `%LOCALAPPDATA%\ThothII\bin\tht.exe`, adding that directory to the user PATH when needed.
|
||||||
|
|
||||||
|
The user never selects a platform binary, runs Go, creates a `bin` directory, or invokes a
|
||||||
|
project-relative executable. Re-running the installer upgrades the installed command idempotently.
|
||||||
|
|
||||||
|
## Project and worktree discovery
|
||||||
|
|
||||||
|
Every host command starts from the current working directory and walks parents until it finds the
|
||||||
|
ThothII root contract (`compose.yaml`, `deploy/`, and the repository marker). A Git worktree root is
|
||||||
|
treated exactly like the main checkout.
|
||||||
|
|
||||||
|
Installation resolution order is:
|
||||||
|
|
||||||
|
1. explicit `--installation PATH`;
|
||||||
|
2. `THOTHII_INSTALLATION`;
|
||||||
|
3. one valid `thothii-installation.yaml` in the current directory or immediate `deploy/*`;
|
||||||
|
4. the same search while walking parent directories.
|
||||||
|
|
||||||
|
Zero candidates produce a setup-oriented error. Multiple candidates produce a bounded list and
|
||||||
|
require `--installation`; no arbitrary recursive search is allowed.
|
||||||
|
|
||||||
|
## `tht setup`
|
||||||
|
|
||||||
|
`tht setup` is idempotent and defaults to the local profile on macOS, Windows, and workstation
|
||||||
|
Linux. `--profile server` selects server behavior. Generated installation files live under
|
||||||
|
`deploy/<installation-id>/`, where `deploy` is explicitly documented as a directory in the project
|
||||||
|
or worktree root.
|
||||||
|
|
||||||
|
The setup flow:
|
||||||
|
|
||||||
|
1. verifies root/worktree identity, Docker, Compose, supported architecture, and line endings;
|
||||||
|
2. creates or validates the installation descriptor and non-secret environment files;
|
||||||
|
3. asks plain-language questions and stores secret file paths, never secret values in the
|
||||||
|
descriptor;
|
||||||
|
4. creates protected secret-file templates only after explicit confirmation and never overwrites an
|
||||||
|
existing file;
|
||||||
|
5. renders and validates Compose configuration;
|
||||||
|
6. builds the ThothII images from the current checkout;
|
||||||
|
7. starts the stack with `docker compose up --detach --remove-orphans`;
|
||||||
|
8. waits for bounded health checks;
|
||||||
|
9. runs installation diagnostics and Pi diagnostics;
|
||||||
|
10. prints the URL and the exact descriptor selected.
|
||||||
|
|
||||||
|
`tht setup --configure-only` stops after validated configuration. `tht setup` never silently
|
||||||
|
replaces a descriptor, environment file, secret file, or generated state belonging to another
|
||||||
|
installation.
|
||||||
|
|
||||||
|
## Lifecycle and product update
|
||||||
|
|
||||||
|
- `tht start` starts the selected installation without rebuilding.
|
||||||
|
- `tht start --build` builds current-checkout images before startup.
|
||||||
|
- `tht stop` stops the installation while preserving state.
|
||||||
|
- `tht update --check-only` retains its current non-mutating validation behavior.
|
||||||
|
- `tht update` becomes the complete product update: lifecycle lock, active-session check/drain,
|
||||||
|
backup checkpoint, image build from the current checkout, controlled recreation, health checks,
|
||||||
|
diagnostics, and rollback to the recorded images when verification fails.
|
||||||
|
|
||||||
|
Product update and Pi update remain separate. `tht update` updates ThothII. `tht pi update` updates
|
||||||
|
only Pi in `core`.
|
||||||
|
|
||||||
|
## Pi management
|
||||||
|
|
||||||
|
`tht pi update [--version VERSION]` makes the version optional. Without `--version`, it queries the
|
||||||
|
latest stable version of the pinned Pi package from the authoritative package registry. A lookup
|
||||||
|
failure stops before mutation and tells the operator to retry or supply `--version`; it never
|
||||||
|
silently substitutes an older pin.
|
||||||
|
|
||||||
|
The command builds or pulls a candidate, verifies the Pi executable, `PI_VERSION`, and image label,
|
||||||
|
recreates only `core`, checks health, runs the smoke test, preserves volumes, and rolls back on
|
||||||
|
failure. Interactive terminals receive one clear confirmation; non-interactive execution requires
|
||||||
|
`--yes`. Model/provider selection remains the responsibility of `tht pi configure` and is not a
|
||||||
|
required argument to Pi update.
|
||||||
|
|
||||||
|
The project release pin in `docker/core.Dockerfile` remains the clean-build default. Installation
|
||||||
|
update state records the selected newer image/version so ordinary restart does not revert it.
|
||||||
|
|
||||||
|
## Backup and restore
|
||||||
|
|
||||||
|
`tht backup` creates a versioned manifest and checksummed archive in
|
||||||
|
`~/.thothii/backups/<installation-id>/` unless `--output` is supplied. It acquires the lifecycle
|
||||||
|
lock, refuses active work unless `--drain` is accepted, obtains a consistent stopped snapshot, and
|
||||||
|
restarts/verifies a previously running installation.
|
||||||
|
|
||||||
|
The backup includes:
|
||||||
|
|
||||||
|
- installation descriptor, non-secret environment/configuration, generated overrides, and source
|
||||||
|
revision metadata;
|
||||||
|
- installation-owned settings, Pi state, workspace registry, workspace secrets volume, sessions,
|
||||||
|
Qdrant data, and embedding-model volume;
|
||||||
|
- server bind roots returned by the installation preservation contract;
|
||||||
|
- a manifest of external secret-file paths and digests.
|
||||||
|
|
||||||
|
Secret-file contents are excluded by default. `--include-secrets --yes` includes them and marks the
|
||||||
|
archive sensitive; the file is created with owner-only permissions. A backup without secrets is
|
||||||
|
restorable only when all referenced secret files still exist and match preflight requirements.
|
||||||
|
|
||||||
|
`tht restore ARCHIVE --yes` validates schema version, checksums, installation identity, target
|
||||||
|
ownership, secret prerequisites, disk space, and stopped/quiescent state before mutation. It creates
|
||||||
|
a rollback checkpoint, restores only manifest-listed paths/volumes, starts the stack when it was
|
||||||
|
previously running, and runs health, `doctor`, `pi doctor`, and workspace inspection. Failure keeps
|
||||||
|
the target in a recoverable stopped state and prints the checkpoint path.
|
||||||
|
|
||||||
|
## Pi Management frontend
|
||||||
|
|
||||||
|
The frontend uses only Docker-based instructions and only the command `tht`. The section:
|
||||||
|
|
||||||
|
- begins fully collapsed;
|
||||||
|
- uses separate Linux, macOS, and Windows environment panels, with none open initially;
|
||||||
|
- is shorter than the current panel and has a working vertical scrollbar;
|
||||||
|
- begins with the exact concise wording `Using the host terminal`;
|
||||||
|
- explains that `deploy` is a directory in the project/worktree root beside `compose.yaml`;
|
||||||
|
- explains that `deploy/pi/models.json` and `deploy/pi/settings.json` are host files mounted
|
||||||
|
read-only into `core`, so the operator edits the host files, not files inside the container;
|
||||||
|
- explains credential-file concepts in plain language rather than presenting environment-variable
|
||||||
|
names without context;
|
||||||
|
- shows direct commands such as `tht pi configure`, `tht pi restart`, `tht pi update`,
|
||||||
|
`tht pi status`, `tht pi doctor`, and `tht pi test`;
|
||||||
|
- contains no Go build, `~/bin`, `./bin`, `./tht`, `thothctl`, or mandatory descriptor path.
|
||||||
|
|
||||||
|
The sanitized-log control must either display the bounded, sanitized `core` log response or fail
|
||||||
|
with a visible error. The live model selector must reflect the mounted Pi configuration, including
|
||||||
|
the existing GLM 5.3 change after `core` is recreated.
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
Active user, installation, architecture, CLI-contract, testing, and README documentation is
|
||||||
|
rewritten around the bootstrap-plus-setup flow and the direct `tht` command. Historical
|
||||||
|
`docs/superpowers` plans/specs remain historical records; the new spec supersedes them.
|
||||||
|
|
||||||
|
Documentation examples run from the project/worktree root, refer to the home directory as `~`, and
|
||||||
|
do not teach manual Go builds or manually constructed installation paths for normal use. Advanced
|
||||||
|
sections may document optional `--installation` and non-interactive flags.
|
||||||
|
|
||||||
|
## Safety and acceptance
|
||||||
|
|
||||||
|
- Existing user changes to `deploy/pi/models.json` and `deploy/pi/settings.json` are preserved.
|
||||||
|
- Unrelated dirty-worktree files are not overwritten or committed accidentally.
|
||||||
|
- JSON contracts remain pristine and secrets are sanitized from stdout, stderr, logs, archives, and
|
||||||
|
failure messages.
|
||||||
|
- Tests cover macOS/Linux shell installation, Windows PowerShell installation, root/worktree
|
||||||
|
discovery, setup idempotency, lifecycle rollback, Pi latest-version lookup, command audit,
|
||||||
|
backup/restore, frontend layout/copy, and active-document command examples.
|
||||||
|
- Final acceptance installs `tht` on this Mac, verifies `command -v tht`, confirms `thothctl` is
|
||||||
|
absent, updates the live stack, checks healthy services, opens port 8080, verifies Pi Management,
|
||||||
|
and confirms GLM 5.3 is selectable.
|
||||||
@@ -238,25 +238,30 @@ test("shows a seven-step host-terminal workflow in scrollable platform tabs", as
|
|||||||
"Open the project root",
|
"Open the project root",
|
||||||
"Edit the provider catalog",
|
"Edit the provider catalog",
|
||||||
"Enable the model",
|
"Enable the model",
|
||||||
"Set the provider credential",
|
"Check the provider credential",
|
||||||
"Reload Pi configuration",
|
"Reload Pi configuration",
|
||||||
"Update the Pi version",
|
"Update the Pi version",
|
||||||
"Recover a failed update",
|
"Recover a failed update",
|
||||||
]);
|
]);
|
||||||
expect(linux).toHaveTextContent("The deploy directory is in the ThothII project root, beside compose.yaml");
|
expect(linux).toHaveTextContent("The deploy directory is beside compose.yaml");
|
||||||
|
expect(linux).toHaveTextContent("All commands below start in this project root");
|
||||||
|
expect(linux).toHaveTextContent("mkdir -p bin");
|
||||||
|
expect(linux).toHaveTextContent("go -C tools/thothctl build -o ../../bin/thothctl ./cmd/thothctl");
|
||||||
expect(linux).toHaveTextContent("deploy/pi/models.json");
|
expect(linux).toHaveTextContent("deploy/pi/models.json");
|
||||||
expect(linux).toHaveTextContent("deploy/pi/settings.json");
|
expect(linux).toHaveTextContent("deploy/pi/settings.json");
|
||||||
expect(linux).toHaveTextContent("baseUrl is the provider API endpoint");
|
expect(linux).toHaveTextContent("baseUrl is the provider API endpoint");
|
||||||
expect(linux).toHaveTextContent("enabledModels uses provider/model identifiers");
|
expect(linux).toHaveTextContent("enabledModels uses provider/model identifiers");
|
||||||
expect(linux).toHaveTextContent("PI_AUTH_FILE is a setting in the installation environment file");
|
expect(linux).toHaveTextContent("Pi reads the provider API key from a protected file on the host");
|
||||||
expect(linux).toHaveTextContent("~/bin/thothctl --installation ~/thothii-installation.yaml pi restart --yes --drain");
|
expect(linux).toHaveTextContent("Do not put the key in models.json or settings.json");
|
||||||
expect(linux).toHaveTextContent("~/bin/thothctl --installation ~/thothii-installation.yaml pi update --version <VERSION> --source build --yes --drain");
|
expect(linux).toHaveTextContent("./bin/thothctl pi restart --yes --drain");
|
||||||
expect(linux).toHaveTextContent("~/bin/thothctl --installation ~/thothii-installation.yaml pi update --version <VERSION> --source pull --image <IMAGE>@sha256:<DIGEST> --yes --drain");
|
expect(linux).toHaveTextContent("./bin/thothctl pi update");
|
||||||
expect(linux).toHaveTextContent("<VERSION> is a placeholder. Replace it with the Pi release/version you want to install.");
|
expect(linux).toHaveTextContent("./bin/thothctl pi update --version <VERSION> --source pull --image <IMAGE>@sha256:<DIGEST> --yes --drain");
|
||||||
expect(linux).toHaveTextContent("~/bin/thothctl --installation ~/thothii-installation.yaml pi maintenance status");
|
expect(linux).toHaveTextContent("The command installs the Pi version pinned in docker/core.Dockerfile");
|
||||||
expect(linux).toHaveTextContent("~/bin/thothctl --installation ~/thothii-installation.yaml pi logs");
|
expect(linux).toHaveTextContent("./bin/thothctl pi maintenance status");
|
||||||
expect(linux).toHaveTextContent("~/bin/thothctl --installation ~/thothii-installation.yaml pi rollback --yes");
|
expect(linux).toHaveTextContent("./bin/thothctl pi logs");
|
||||||
expect(linux).toHaveTextContent("~/bin/thothctl --installation ~/thothii-installation.yaml pi maintenance recover --yes");
|
expect(linux).toHaveTextContent("./bin/thothctl pi rollback --yes");
|
||||||
|
expect(linux).toHaveTextContent("./bin/thothctl pi maintenance recover --yes");
|
||||||
|
expect(linux).not.toHaveTextContent("~/bin/");
|
||||||
expect(linux).not.toHaveTextContent("~/.pi/agent/");
|
expect(linux).not.toHaveTextContent("~/.pi/agent/");
|
||||||
|
|
||||||
await user.click(macosTab);
|
await user.click(macosTab);
|
||||||
@@ -266,22 +271,24 @@ test("shows a seven-step host-terminal workflow in scrollable platform tabs", as
|
|||||||
"Open the project root",
|
"Open the project root",
|
||||||
"Edit the provider catalog",
|
"Edit the provider catalog",
|
||||||
"Enable the model",
|
"Enable the model",
|
||||||
"Set the provider credential",
|
"Check the provider credential",
|
||||||
"Reload Pi configuration",
|
"Reload Pi configuration",
|
||||||
"Update the Pi version",
|
"Update the Pi version",
|
||||||
"Recover a failed update",
|
"Recover a failed update",
|
||||||
]);
|
]);
|
||||||
expect(macos).toHaveTextContent("The deploy directory is in the ThothII project root, beside compose.yaml");
|
expect(macos).toHaveTextContent("The deploy directory is beside compose.yaml");
|
||||||
|
expect(macos).toHaveTextContent("All commands below start in this project root");
|
||||||
|
expect(macos).toHaveTextContent("go -C tools/thothctl build -o ../../bin/thothctl ./cmd/thothctl");
|
||||||
expect(macos).toHaveTextContent("deploy/pi/models.json");
|
expect(macos).toHaveTextContent("deploy/pi/models.json");
|
||||||
expect(macos).toHaveTextContent("deploy/pi/settings.json");
|
expect(macos).toHaveTextContent("deploy/pi/settings.json");
|
||||||
expect(macos).toHaveTextContent("~/bin/thothctl --installation ~/thothii-installation.yaml pi restart --yes --drain");
|
expect(macos).toHaveTextContent("./bin/thothctl pi restart --yes --drain");
|
||||||
expect(macos).toHaveTextContent("~/bin/thothctl --installation ~/thothii-installation.yaml pi update --version <VERSION> --source build --yes --drain");
|
expect(macos).toHaveTextContent("./bin/thothctl pi update");
|
||||||
expect(macos).toHaveTextContent("~/bin/thothctl --installation ~/thothii-installation.yaml pi update --version <VERSION> --source pull --image <IMAGE>@sha256:<DIGEST> --yes --drain");
|
expect(macos).toHaveTextContent("./bin/thothctl pi update --version <VERSION> --source pull --image <IMAGE>@sha256:<DIGEST> --yes --drain");
|
||||||
expect(macos).toHaveTextContent("<VERSION> is a placeholder. Replace it with the Pi release/version you want to install.");
|
expect(macos).toHaveTextContent("The command installs the Pi version pinned in docker/core.Dockerfile");
|
||||||
expect(macos).toHaveTextContent("~/bin/thothctl --installation ~/thothii-installation.yaml pi maintenance status");
|
expect(macos).toHaveTextContent("./bin/thothctl pi maintenance status");
|
||||||
expect(macos).toHaveTextContent("~/bin/thothctl --installation ~/thothii-installation.yaml pi logs");
|
expect(macos).toHaveTextContent("./bin/thothctl pi logs");
|
||||||
expect(macos).toHaveTextContent("~/bin/thothctl --installation ~/thothii-installation.yaml pi rollback --yes");
|
expect(macos).toHaveTextContent("./bin/thothctl pi rollback --yes");
|
||||||
expect(macos).toHaveTextContent("~/bin/thothctl --installation ~/thothii-installation.yaml pi maintenance recover --yes");
|
expect(macos).toHaveTextContent("./bin/thothctl pi maintenance recover --yes");
|
||||||
|
|
||||||
await user.click(windowsTab);
|
await user.click(windowsTab);
|
||||||
const windows = screen.getByRole("tabpanel", { name: "Windows" });
|
const windows = screen.getByRole("tabpanel", { name: "Windows" });
|
||||||
@@ -290,22 +297,26 @@ test("shows a seven-step host-terminal workflow in scrollable platform tabs", as
|
|||||||
"Open the project root",
|
"Open the project root",
|
||||||
"Edit the provider catalog",
|
"Edit the provider catalog",
|
||||||
"Enable the model",
|
"Enable the model",
|
||||||
"Set the provider credential",
|
"Check the provider credential",
|
||||||
"Reload Pi configuration",
|
"Reload Pi configuration",
|
||||||
"Update the Pi version",
|
"Update the Pi version",
|
||||||
"Recover a failed update",
|
"Recover a failed update",
|
||||||
]);
|
]);
|
||||||
expect(windows).toHaveTextContent("The deploy directory is in the ThothII project root, beside compose.yaml");
|
expect(windows).toHaveTextContent("The deploy directory is beside compose.yaml");
|
||||||
|
expect(windows).toHaveTextContent("All commands below start in this project root");
|
||||||
expect(windows).toHaveTextContent("deploy\\pi\\models.json");
|
expect(windows).toHaveTextContent("deploy\\pi\\models.json");
|
||||||
expect(windows).toHaveTextContent("deploy\\pi\\settings.json");
|
expect(windows).toHaveTextContent("deploy\\pi\\settings.json");
|
||||||
expect(windows).toHaveTextContent('& (Resolve-Path "~\\bin\\thothctl-windows-amd64.exe") --installation (Resolve-Path "~\\thothii-installation.yaml") pi restart --yes --drain');
|
expect(windows).toHaveTextContent("New-Item -ItemType Directory -Force bin");
|
||||||
expect(windows).toHaveTextContent('& (Resolve-Path "~\\bin\\thothctl-windows-amd64.exe") --installation (Resolve-Path "~\\thothii-installation.yaml") pi update --version <VERSION> --source build --yes --drain');
|
expect(windows).toHaveTextContent("go -C tools/thothctl build -o ../../bin/thothctl.exe ./cmd/thothctl");
|
||||||
expect(windows).toHaveTextContent('& (Resolve-Path "~\\bin\\thothctl-windows-amd64.exe") --installation (Resolve-Path "~\\thothii-installation.yaml") pi update --version <VERSION> --source pull --image <IMAGE>@sha256:<DIGEST> --yes --drain');
|
expect(windows).toHaveTextContent('.\\bin\\thothctl.exe pi restart --yes --drain');
|
||||||
expect(windows).toHaveTextContent("<VERSION> is a placeholder. Replace it with the Pi release/version you want to install.");
|
expect(windows).toHaveTextContent('.\\bin\\thothctl.exe pi update');
|
||||||
expect(windows).toHaveTextContent('& (Resolve-Path "~\\bin\\thothctl-windows-amd64.exe") --installation (Resolve-Path "~\\thothii-installation.yaml") pi maintenance status');
|
expect(windows).toHaveTextContent('.\\bin\\thothctl.exe pi update --version <VERSION> --source pull --image <IMAGE>@sha256:<DIGEST> --yes --drain');
|
||||||
expect(windows).toHaveTextContent('& (Resolve-Path "~\\bin\\thothctl-windows-amd64.exe") --installation (Resolve-Path "~\\thothii-installation.yaml") pi logs');
|
expect(windows).toHaveTextContent("The command installs the Pi version pinned in docker/core.Dockerfile");
|
||||||
expect(windows).toHaveTextContent('& (Resolve-Path "~\\bin\\thothctl-windows-amd64.exe") --installation (Resolve-Path "~\\thothii-installation.yaml") pi rollback --yes');
|
expect(windows).toHaveTextContent('.\\bin\\thothctl.exe pi maintenance status');
|
||||||
expect(windows).toHaveTextContent('& (Resolve-Path "~\\bin\\thothctl-windows-amd64.exe") --installation (Resolve-Path "~\\thothii-installation.yaml") pi maintenance recover --yes');
|
expect(windows).toHaveTextContent('.\\bin\\thothctl.exe pi logs');
|
||||||
|
expect(windows).toHaveTextContent('.\\bin\\thothctl.exe pi rollback --yes');
|
||||||
|
expect(windows).toHaveTextContent('.\\bin\\thothctl.exe pi maintenance recover --yes');
|
||||||
|
expect(windows).not.toHaveTextContent("~\\bin\\");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("keeps the required PI_AUTH_FILE guidance in one static text node", async () => {
|
test("keeps the required PI_AUTH_FILE guidance in one static text node", async () => {
|
||||||
@@ -314,13 +325,13 @@ test("keeps the required PI_AUTH_FILE guidance in one static text node", async (
|
|||||||
|
|
||||||
const tablist = await screen.findByRole("tablist", { name: "Pi host platform" });
|
const tablist = await screen.findByRole("tablist", { name: "Pi host platform" });
|
||||||
await user.click(within(tablist).getByRole("tab", { name: "Linux" }));
|
await user.click(within(tablist).getByRole("tab", { name: "Linux" }));
|
||||||
const credentialStep = screen.getByRole("heading", { name: "Set the provider credential" }).closest("li");
|
const credentialStep = screen.getByRole("heading", { name: "Check the provider credential" }).closest("li");
|
||||||
const guidance = credentialStep?.querySelector("p");
|
const guidance = credentialStep?.querySelector("p");
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
Array.from(guidance?.childNodes ?? []).some(
|
Array.from(guidance?.childNodes ?? []).some(
|
||||||
(node) => node.nodeType === Node.TEXT_NODE
|
(node) => node.nodeType === Node.TEXT_NODE
|
||||||
&& node.textContent?.includes("PI_AUTH_FILE is a setting in the installation environment file"),
|
&& node.textContent?.includes("Pi reads the provider API key from a protected file on the host"),
|
||||||
),
|
),
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -108,6 +108,7 @@ type PiPlatformDetails = {
|
|||||||
settingsPath: string;
|
settingsPath: string;
|
||||||
terminal: string;
|
terminal: string;
|
||||||
credentialProtection: string;
|
credentialProtection: string;
|
||||||
|
buildCommand: string;
|
||||||
restartCommand: string;
|
restartCommand: string;
|
||||||
updateCommand: string;
|
updateCommand: string;
|
||||||
pullCommand: string;
|
pullCommand: string;
|
||||||
@@ -123,10 +124,11 @@ const piPlatforms: Array<{ id: PiPlatform; label: string; details: PiPlatformDet
|
|||||||
settingsPath: "deploy/pi/settings.json",
|
settingsPath: "deploy/pi/settings.json",
|
||||||
terminal: "a terminal",
|
terminal: "a terminal",
|
||||||
credentialProtection: "a protected host file with mode 0600",
|
credentialProtection: "a protected host file with mode 0600",
|
||||||
restartCommand: "~/bin/thothctl --installation ~/thothii-installation.yaml pi restart --yes --drain",
|
buildCommand: "mkdir -p bin\ngo -C tools/thothctl build -o ../../bin/thothctl ./cmd/thothctl",
|
||||||
updateCommand: "~/bin/thothctl --installation ~/thothii-installation.yaml pi update --version <VERSION> --source build --yes --drain",
|
restartCommand: "./bin/thothctl pi restart --yes --drain",
|
||||||
pullCommand: "~/bin/thothctl --installation ~/thothii-installation.yaml pi update --version <VERSION> --source pull --image <IMAGE>@sha256:<DIGEST> --yes --drain",
|
updateCommand: "./bin/thothctl pi update",
|
||||||
recoveryCommands: "~/bin/thothctl --installation ~/thothii-installation.yaml pi maintenance status\n~/bin/thothctl --installation ~/thothii-installation.yaml pi logs\n~/bin/thothctl --installation ~/thothii-installation.yaml pi rollback --yes\n~/bin/thothctl --installation ~/thothii-installation.yaml pi maintenance recover --yes",
|
pullCommand: "./bin/thothctl pi update --version <VERSION> --source pull --image <IMAGE>@sha256:<DIGEST> --yes --drain",
|
||||||
|
recoveryCommands: "./bin/thothctl pi maintenance status\n./bin/thothctl pi logs\n./bin/thothctl pi rollback --yes\n./bin/thothctl pi maintenance recover --yes",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -137,10 +139,11 @@ const piPlatforms: Array<{ id: PiPlatform; label: string; details: PiPlatformDet
|
|||||||
settingsPath: "deploy/pi/settings.json",
|
settingsPath: "deploy/pi/settings.json",
|
||||||
terminal: "Terminal",
|
terminal: "Terminal",
|
||||||
credentialProtection: "a protected host file with mode 0600",
|
credentialProtection: "a protected host file with mode 0600",
|
||||||
restartCommand: "~/bin/thothctl --installation ~/thothii-installation.yaml pi restart --yes --drain",
|
buildCommand: "mkdir -p bin\ngo -C tools/thothctl build -o ../../bin/thothctl ./cmd/thothctl",
|
||||||
updateCommand: "~/bin/thothctl --installation ~/thothii-installation.yaml pi update --version <VERSION> --source build --yes --drain",
|
restartCommand: "./bin/thothctl pi restart --yes --drain",
|
||||||
pullCommand: "~/bin/thothctl --installation ~/thothii-installation.yaml pi update --version <VERSION> --source pull --image <IMAGE>@sha256:<DIGEST> --yes --drain",
|
updateCommand: "./bin/thothctl pi update",
|
||||||
recoveryCommands: "~/bin/thothctl --installation ~/thothii-installation.yaml pi maintenance status\n~/bin/thothctl --installation ~/thothii-installation.yaml pi logs\n~/bin/thothctl --installation ~/thothii-installation.yaml pi rollback --yes\n~/bin/thothctl --installation ~/thothii-installation.yaml pi maintenance recover --yes",
|
pullCommand: "./bin/thothctl pi update --version <VERSION> --source pull --image <IMAGE>@sha256:<DIGEST> --yes --drain",
|
||||||
|
recoveryCommands: "./bin/thothctl pi maintenance status\n./bin/thothctl pi logs\n./bin/thothctl pi rollback --yes\n./bin/thothctl pi maintenance recover --yes",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -151,10 +154,11 @@ const piPlatforms: Array<{ id: PiPlatform; label: string; details: PiPlatformDet
|
|||||||
settingsPath: "deploy\\pi\\settings.json",
|
settingsPath: "deploy\\pi\\settings.json",
|
||||||
terminal: "PowerShell",
|
terminal: "PowerShell",
|
||||||
credentialProtection: "a protected host file with a user-only ACL",
|
credentialProtection: "a protected host file with a user-only ACL",
|
||||||
restartCommand: '& (Resolve-Path "~\\bin\\thothctl-windows-amd64.exe") --installation (Resolve-Path "~\\thothii-installation.yaml") pi restart --yes --drain',
|
buildCommand: "New-Item -ItemType Directory -Force bin | Out-Null\ngo -C tools/thothctl build -o ../../bin/thothctl.exe ./cmd/thothctl",
|
||||||
updateCommand: '& (Resolve-Path "~\\bin\\thothctl-windows-amd64.exe") --installation (Resolve-Path "~\\thothii-installation.yaml") pi update --version <VERSION> --source build --yes --drain',
|
restartCommand: ".\\bin\\thothctl.exe pi restart --yes --drain",
|
||||||
pullCommand: '& (Resolve-Path "~\\bin\\thothctl-windows-amd64.exe") --installation (Resolve-Path "~\\thothii-installation.yaml") pi update --version <VERSION> --source pull --image <IMAGE>@sha256:<DIGEST> --yes --drain',
|
updateCommand: ".\\bin\\thothctl.exe pi update",
|
||||||
recoveryCommands: '& (Resolve-Path "~\\bin\\thothctl-windows-amd64.exe") --installation (Resolve-Path "~\\thothii-installation.yaml") pi maintenance status\n& (Resolve-Path "~\\bin\\thothctl-windows-amd64.exe") --installation (Resolve-Path "~\\thothii-installation.yaml") pi logs\n& (Resolve-Path "~\\bin\\thothctl-windows-amd64.exe") --installation (Resolve-Path "~\\thothii-installation.yaml") pi rollback --yes\n& (Resolve-Path "~\\bin\\thothctl-windows-amd64.exe") --installation (Resolve-Path "~\\thothii-installation.yaml") pi maintenance recover --yes',
|
pullCommand: ".\\bin\\thothctl.exe pi update --version <VERSION> --source pull --image <IMAGE>@sha256:<DIGEST> --yes --drain",
|
||||||
|
recoveryCommands: ".\\bin\\thothctl.exe pi maintenance status\n.\\bin\\thothctl.exe pi logs\n.\\bin\\thothctl.exe pi rollback --yes\n.\\bin\\thothctl.exe pi maintenance recover --yes",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -167,7 +171,9 @@ function PiInstructionSteps({ details }: { details: PiPlatformDetails }) {
|
|||||||
return <ol className="grid gap-4 pl-5 marker:font-semibold marker:text-muted-foreground">
|
return <ol className="grid gap-4 pl-5 marker:font-semibold marker:text-muted-foreground">
|
||||||
<li>
|
<li>
|
||||||
<h4 className="font-semibold text-foreground">Open the project root</h4>
|
<h4 className="font-semibold text-foreground">Open the project root</h4>
|
||||||
<p className="mt-1 text-muted-foreground">Open {details.terminal} in the ThothII project root. The deploy directory is in the ThothII project root, beside <code>compose.yaml</code>.</p>
|
<p className="mt-1 text-muted-foreground">Using {details.terminal}, open the current ThothII checkout or worktree root. All commands below start in this project root. The deploy directory is beside <code>compose.yaml</code>. If this checkout has no <code>bin</code> directory, create it and build the local CLI once:</p>
|
||||||
|
<PiCodeBlock className="mt-2">{details.buildCommand}</PiCodeBlock>
|
||||||
|
<p className="mt-2 text-muted-foreground">The commands below use the executable built in this checkout, so they cannot accidentally target another worktree.</p>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<h4 className="font-semibold text-foreground">Edit the provider catalog</h4>
|
<h4 className="font-semibold text-foreground">Edit the provider catalog</h4>
|
||||||
@@ -188,8 +194,8 @@ function PiInstructionSteps({ details }: { details: PiPlatformDetails }) {
|
|||||||
</dl>
|
</dl>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<h4 className="font-semibold text-foreground">Set the provider credential</h4>
|
<h4 className="font-semibold text-foreground">Check the provider credential</h4>
|
||||||
<p className="mt-1 text-muted-foreground">PI_AUTH_FILE is a setting in the installation environment file. It selects {details.credentialProtection}; Docker mounts the selected host credential file read-only for Pi.</p>
|
<p className="mt-1 text-muted-foreground">Pi reads the provider API key from a protected file on the host. <code>PI_AUTH_FILE</code> tells this installation which file to use; Docker mounts it read-only into the core container. Do not put the key in <code>models.json</code> or <code>settings.json</code>. Keep {details.credentialProtection}.</p>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<h4 className="font-semibold text-foreground">Reload Pi configuration</h4>
|
<h4 className="font-semibold text-foreground">Reload Pi configuration</h4>
|
||||||
@@ -198,8 +204,7 @@ function PiInstructionSteps({ details }: { details: PiPlatformDetails }) {
|
|||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<h4 className="font-semibold text-foreground">Update the Pi version</h4>
|
<h4 className="font-semibold text-foreground">Update the Pi version</h4>
|
||||||
<p className="mt-1 text-muted-foreground">Use a build update only when changing the bundled Pi version.</p>
|
<p className="mt-1 text-muted-foreground">The command installs the Pi version pinned in <code>docker/core.Dockerfile</code>. Use <code>--version <VERSION></code> only when you deliberately want another version.</p>
|
||||||
<p className="mt-1 text-muted-foreground">{"<VERSION> is a placeholder. Replace it with the Pi release/version you want to install."}</p>
|
|
||||||
<PiCodeBlock className="mt-2">{details.updateCommand}</PiCodeBlock>
|
<PiCodeBlock className="mt-2">{details.updateCommand}</PiCodeBlock>
|
||||||
<p className="mt-2 text-[11px] text-muted-foreground">Advanced: pull an immutable, digest-pinned image.</p>
|
<p className="mt-2 text-[11px] text-muted-foreground">Advanced: pull an immutable, digest-pinned image.</p>
|
||||||
<PiCodeBlock className="mt-1 text-[11px] text-muted-foreground">{details.pullCommand}</PiCodeBlock>
|
<PiCodeBlock className="mt-1 text-[11px] text-muted-foreground">{details.pullCommand}</PiCodeBlock>
|
||||||
|
|||||||
@@ -114,7 +114,13 @@ test("level one explains the read-only Git sequence and the repository update bu
|
|||||||
const overview = screen.getByTestId("workspace-overview");
|
const overview = screen.getByTestId("workspace-overview");
|
||||||
expect(within(overview).getByText(/Git server such as GitHub, GitLab, or Gitea/i)).toBeVisible();
|
expect(within(overview).getByText(/Git server such as GitHub, GitLab, or Gitea/i)).toBeVisible();
|
||||||
expect(within(overview).getByText(/configured during ThothII installation/i)).toBeVisible();
|
expect(within(overview).getByText(/configured during ThothII installation/i)).toBeVisible();
|
||||||
expect(within(overview).getAllByRole("listitem")[0]).toHaveTextContent(/choosing any local directory you prefer/i);
|
const repositoryStep = within(overview).getAllByRole("listitem")[0];
|
||||||
|
expect(repositoryStep).toHaveTextContent(/create a workspace repository/i);
|
||||||
|
expect(repositoryStep).toHaveTextContent(/one directory for each workspace/i);
|
||||||
|
expect(repositoryStep).toHaveTextContent(/thoth-workspaces\.yaml/i);
|
||||||
|
expect(repositoryStep).toHaveTextContent(/database connection/i);
|
||||||
|
expect(repositoryStep).toHaveTextContent(/Evidence sources/i);
|
||||||
|
expect(repositoryStep).toHaveTextContent(/vector-database collection/i);
|
||||||
expect(within(overview).getByRole("link", { name: /workspace authoring instructions on GitHub/i })).toHaveAttribute(
|
expect(within(overview).getByRole("link", { name: /workspace authoring instructions on GitHub/i })).toHaveAttribute(
|
||||||
"href",
|
"href",
|
||||||
"https://github.com/mptyl/ThothII/blob/main/docs/install/local-workspace-registry.md#prepare-and-publish-a-workspace-source",
|
"https://github.com/mptyl/ThothII/blob/main/docs/install/local-workspace-registry.md#prepare-and-publish-a-workspace-source",
|
||||||
@@ -126,7 +132,7 @@ test("level one explains the read-only Git sequence and the repository update bu
|
|||||||
|
|
||||||
await user.click(screen.getByRole("button", { name: "Update workspace repository" }));
|
await user.click(screen.getByRole("button", { name: "Update workspace repository" }));
|
||||||
expect(await screen.findByText("Workspace repository updated and validated.")).toBeVisible();
|
expect(await screen.findByText("Workspace repository updated and validated.")).toBeVisible();
|
||||||
expect(screen.getByText(/create a local workspace/i)).toBeInTheDocument();
|
expect(screen.getByText(/create a workspace repository/i)).toBeInTheDocument();
|
||||||
expect(screen.queryByText(/import|export|bundle/i)).not.toBeInTheDocument();
|
expect(screen.queryByText(/import|export|bundle/i)).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -141,10 +147,61 @@ test("workspace-specific commands remain isolated until a workspace is selected"
|
|||||||
expect(screen.getByText(/reads this revision without modifying or publishing it/i)).toBeVisible();
|
expect(screen.getByText(/reads this revision without modifying or publishing it/i)).toBeVisible();
|
||||||
expect(screen.getByText(/checks workspace.yaml and the required workspace directories/i)).toBeVisible();
|
expect(screen.getByText(/checks workspace.yaml and the required workspace directories/i)).toBeVisible();
|
||||||
expect(screen.getByText(/temporary decrypted credentials/i)).toBeVisible();
|
expect(screen.getByText(/temporary decrypted credentials/i)).toBeVisible();
|
||||||
|
const databaseField = screen.getByText("Database").parentElement;
|
||||||
|
expect(databaseField).not.toBeNull();
|
||||||
|
expect(databaseField).toHaveTextContent("engine: postgres");
|
||||||
|
expect(databaseField).toHaveTextContent("database: database");
|
||||||
|
expect(databaseField).toHaveTextContent("schema: public");
|
||||||
expect(screen.getByRole("button", { name: "Validate workspace source" })).toBeVisible();
|
expect(screen.getByRole("button", { name: "Validate workspace source" })).toBeVisible();
|
||||||
expect(screen.getByRole("button", { name: "Test workspace connections" })).toBeVisible();
|
expect(screen.getByRole("button", { name: "Test workspace connections" })).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("keeps validation and connection results inside their respective action cards", async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
server.use(
|
||||||
|
http.post("/api/workspaces/validate", () => HttpResponse.json({ workspace, contract: {} })),
|
||||||
|
http.post("/api/workspaces/psd-clinical/test", () => HttpResponse.json({
|
||||||
|
activatable: false,
|
||||||
|
diagnostics: [{ level: "error", code: "connector_unavailable", message: "Connector diagnostic failed." }],
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
renderManager();
|
||||||
|
await user.click(await screen.findByRole("button", { name: "PSD Clinical" }));
|
||||||
|
|
||||||
|
const validationCard = screen.getByTestId("workspace-validation-card");
|
||||||
|
const connectionCard = screen.getByTestId("workspace-connection-card");
|
||||||
|
await user.click(within(validationCard).getByRole("button", { name: "Validate workspace source" }));
|
||||||
|
const validationStatus = await within(validationCard).findByRole("status");
|
||||||
|
expect(validationStatus).toHaveTextContent("Workspace source is valid.");
|
||||||
|
expect(validationStatus).toHaveClass("text-emerald-700");
|
||||||
|
expect(within(connectionCard).queryByText("Workspace source is valid.")).not.toBeInTheDocument();
|
||||||
|
|
||||||
|
await user.click(within(connectionCard).getByRole("button", { name: "Test workspace connections" }));
|
||||||
|
expect(await within(connectionCard).findByRole("alert")).toHaveTextContent(
|
||||||
|
"connector_unavailable: Connector diagnostic failed.",
|
||||||
|
);
|
||||||
|
expect(within(validationCard).queryByText("connector_unavailable: Connector diagnostic failed.")).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("renders binding_ok as a green connection success", async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
server.use(
|
||||||
|
http.post("/api/workspaces/psd-clinical/test", () => HttpResponse.json({
|
||||||
|
activatable: true,
|
||||||
|
diagnostics: [{ level: "info", code: "binding_ok", message: "Installation bindings and diagnostics succeeded." }],
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
renderManager();
|
||||||
|
await user.click(await screen.findByRole("button", { name: "PSD Clinical" }));
|
||||||
|
|
||||||
|
const connectionCard = screen.getByTestId("workspace-connection-card");
|
||||||
|
await user.click(within(connectionCard).getByRole("button", { name: "Test workspace connections" }));
|
||||||
|
const connectionStatus = await within(connectionCard).findByRole("status");
|
||||||
|
expect(connectionStatus).toHaveTextContent("binding_ok: Installation bindings and diagnostics succeeded.");
|
||||||
|
expect(connectionStatus).toHaveClass("text-emerald-700");
|
||||||
|
expect(within(connectionCard).queryByRole("alert")).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
test("secret fields are write-only, clear after blind save, and may be forgotten", async () => {
|
test("secret fields are write-only, clear after blind save, and may be forgotten", async () => {
|
||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
let savedBody: unknown;
|
let savedBody: unknown;
|
||||||
|
|||||||
@@ -66,6 +66,10 @@ export function WorkspaceManager({ open, onClose }: { open: boolean; onClose: ()
|
|||||||
const [secretValues, setSecretValues] = useState<Record<string, string>>({});
|
const [secretValues, setSecretValues] = useState<Record<string, string>>({});
|
||||||
const [notice, setNotice] = useState<string>();
|
const [notice, setNotice] = useState<string>();
|
||||||
const [diagnostics, setDiagnostics] = useState<string[]>([]);
|
const [diagnostics, setDiagnostics] = useState<string[]>([]);
|
||||||
|
const [validationNotice, setValidationNotice] = useState<string>();
|
||||||
|
const [validationDiagnostics, setValidationDiagnostics] = useState<string[]>([]);
|
||||||
|
const [connectionNotice, setConnectionNotice] = useState<string>();
|
||||||
|
const [connectionDiagnostics, setConnectionDiagnostics] = useState<string[]>([]);
|
||||||
const [busyAction, setBusyAction] = useState<string>();
|
const [busyAction, setBusyAction] = useState<string>();
|
||||||
|
|
||||||
const statusQuery = useQuery({
|
const statusQuery = useQuery({
|
||||||
@@ -97,6 +101,15 @@ export function WorkspaceManager({ open, onClose }: { open: boolean; onClose: ()
|
|||||||
const clearMessages = () => {
|
const clearMessages = () => {
|
||||||
setNotice(undefined);
|
setNotice(undefined);
|
||||||
setDiagnostics([]);
|
setDiagnostics([]);
|
||||||
|
setValidationNotice(undefined);
|
||||||
|
setValidationDiagnostics([]);
|
||||||
|
setConnectionNotice(undefined);
|
||||||
|
setConnectionDiagnostics([]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearGlobalMessages = () => {
|
||||||
|
setNotice(undefined);
|
||||||
|
setDiagnostics([]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const close = () => {
|
const close = () => {
|
||||||
@@ -139,12 +152,14 @@ export function WorkspaceManager({ open, onClose }: { open: boolean; onClose: ()
|
|||||||
async function validateSource() {
|
async function validateSource() {
|
||||||
if (!detailQuery.data) return;
|
if (!detailQuery.data) return;
|
||||||
setBusyAction("validate");
|
setBusyAction("validate");
|
||||||
clearMessages();
|
clearGlobalMessages();
|
||||||
|
setValidationNotice(undefined);
|
||||||
|
setValidationDiagnostics([]);
|
||||||
try {
|
try {
|
||||||
await validateWorkspace(detailQuery.data.workspace);
|
await validateWorkspace(detailQuery.data.workspace);
|
||||||
setNotice("Workspace source is valid.");
|
setValidationNotice("Workspace source is valid.");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setDiagnostics([publicError(error, "workspace_invalid: Workspace validation could not be completed")]);
|
setValidationDiagnostics([publicError(error, "workspace_invalid: Workspace validation could not be completed")]);
|
||||||
} finally {
|
} finally {
|
||||||
setBusyAction(undefined);
|
setBusyAction(undefined);
|
||||||
}
|
}
|
||||||
@@ -153,17 +168,23 @@ export function WorkspaceManager({ open, onClose }: { open: boolean; onClose: ()
|
|||||||
async function testConnections() {
|
async function testConnections() {
|
||||||
if (!selectedId) return;
|
if (!selectedId) return;
|
||||||
setBusyAction("test");
|
setBusyAction("test");
|
||||||
clearMessages();
|
clearGlobalMessages();
|
||||||
|
setConnectionNotice(undefined);
|
||||||
|
setConnectionDiagnostics([]);
|
||||||
try {
|
try {
|
||||||
const result = await testWorkspace(selectedId);
|
const result = await testWorkspace(selectedId);
|
||||||
setDiagnostics(result.diagnostics.map(({ code, message }) => `${code}: ${message}`));
|
const issues = result.diagnostics.filter(({ level }) => level !== "info");
|
||||||
if (result.diagnostics.length === 0) {
|
const informational = result.diagnostics.find(({ level }) => level === "info");
|
||||||
setNotice(result.activatable
|
setConnectionDiagnostics(issues.map(({ code, message }) => `${code}: ${message}`));
|
||||||
? "Workspace connections are valid."
|
if (issues.length === 0) {
|
||||||
|
setConnectionNotice(result.activatable
|
||||||
|
? informational
|
||||||
|
? `${informational.code}: ${informational.message}`
|
||||||
|
: "Workspace connections are valid."
|
||||||
: "Workspace connection test completed.");
|
: "Workspace connection test completed.");
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setDiagnostics([publicError(error, "connector_unavailable: Workspace connections could not be tested")]);
|
setConnectionDiagnostics([publicError(error, "connector_unavailable: Workspace connections could not be tested")]);
|
||||||
} finally {
|
} finally {
|
||||||
setBusyAction(undefined);
|
setBusyAction(undefined);
|
||||||
}
|
}
|
||||||
@@ -293,7 +314,7 @@ export function WorkspaceManager({ open, onClose }: { open: boolean; onClose: ()
|
|||||||
<h2 className="font-heading text-xl font-semibold">How workspaces reach ThothII</h2>
|
<h2 className="font-heading text-xl font-semibold">How workspaces reach ThothII</h2>
|
||||||
</div>
|
</div>
|
||||||
<ol className="grid list-decimal gap-3 pl-5 text-sm leading-6 text-muted-foreground">
|
<ol className="grid list-decimal gap-3 pl-5 text-sm leading-6 text-muted-foreground">
|
||||||
<li><span className="font-medium text-foreground">Create a local workspace</span> by choosing any local directory you prefer and setting up the workspace there. It must contain <code>workspace.yaml</code> and every required subdirectory, including any versioned Evidence files.</li>
|
<li><span className="font-medium text-foreground">Create a workspace repository</span> in any local directory you choose. Add one directory for each workspace you want ThothII to manage. At the repository root, <code>thoth-workspaces.yaml</code> lists those workspaces; each workspace directory contains its own <code>workspace.yaml</code>, which declares the database connection, the Evidence sources, and the ThothII vector-database collection used during the process.</li>
|
||||||
<li>Publish that source by committing and pushing it to a repository hosted by a Git server such as GitHub, GitLab, or Gitea.</li>
|
<li>Publish that source by committing and pushing it to a repository hosted by a Git server such as GitHub, GitLab, or Gitea.</li>
|
||||||
<li>The repository address, branch, and read-only Git credentials are configured during ThothII installation. This installation reads <span className="font-medium text-foreground">{repositoryLabel}</span> on branch <span className="font-mono text-foreground">{statusQuery.data?.branch ?? "main"}</span>.</li>
|
<li>The repository address, branch, and read-only Git credentials are configured during ThothII installation. This installation reads <span className="font-medium text-foreground">{repositoryLabel}</span> on branch <span className="font-mono text-foreground">{statusQuery.data?.branch ?? "main"}</span>.</li>
|
||||||
<li>ThothII fetches the configured branch into its managed read-only checkout, validates the complete candidate revision, and activates it only when validation succeeds. It never edits, commits, pushes, or publishes workspace source.</li>
|
<li>ThothII fetches the configured branch into its managed read-only checkout, validates the complete candidate revision, and activates it only when validation succeeds. It never edits, commits, pushes, or publishes workspace source.</li>
|
||||||
@@ -337,21 +358,56 @@ export function WorkspaceManager({ open, onClose }: { open: boolean; onClose: ()
|
|||||||
<dl className="grid gap-3 rounded-lg border border-border bg-muted/20 p-4 text-sm sm:grid-cols-2">
|
<dl className="grid gap-3 rounded-lg border border-border bg-muted/20 p-4 text-sm sm:grid-cols-2">
|
||||||
<div><dt className="text-xs text-muted-foreground">Source file</dt><dd className="font-mono">{selectedSummary.file}</dd></div>
|
<div><dt className="text-xs text-muted-foreground">Source file</dt><dd className="font-mono">{selectedSummary.file}</dd></div>
|
||||||
<div><dt className="text-xs text-muted-foreground">Active revision</dt><dd className="truncate font-mono">{detailQuery.data.revision.commit}</dd></div>
|
<div><dt className="text-xs text-muted-foreground">Active revision</dt><dd className="truncate font-mono">{detailQuery.data.revision.commit}</dd></div>
|
||||||
<div><dt className="text-xs text-muted-foreground">Data warehouse</dt><dd>{detailQuery.data.workspace.dwh.engine} · {detailQuery.data.workspace.dwh.database}/{detailQuery.data.workspace.dwh.schema}</dd></div>
|
<div>
|
||||||
|
<dt className="text-xs text-muted-foreground">Database</dt>
|
||||||
|
<dd className="grid gap-0.5 font-mono text-xs">
|
||||||
|
<span>engine: {detailQuery.data.workspace.dwh.engine}</span>
|
||||||
|
<span>database: {detailQuery.data.workspace.dwh.database}</span>
|
||||||
|
<span>schema: {detailQuery.data.workspace.dwh.schema}</span>
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
<div><dt className="text-xs text-muted-foreground">Runtime status</dt><dd>{stateLabel(runtime.configurationState)}</dd></div>
|
<div><dt className="text-xs text-muted-foreground">Runtime status</dt><dd>{stateLabel(runtime.configurationState)}</dd></div>
|
||||||
</dl>
|
</dl>
|
||||||
|
|
||||||
<div className="grid gap-3 lg:grid-cols-2">
|
<div className="grid gap-3 lg:grid-cols-2">
|
||||||
<div className="rounded-lg border border-border p-4">
|
<div data-testid="workspace-validation-card" className="rounded-lg border border-border p-4">
|
||||||
<h4 className="font-heading font-semibold">Validate workspace source</h4>
|
<h4 className="font-heading font-semibold">Validate workspace source</h4>
|
||||||
<p className="mt-1 min-h-12 text-sm leading-5 text-muted-foreground">Checks workspace.yaml and the required workspace directories against the supported workspace schema. No source file is changed.</p>
|
<p className="mt-1 min-h-12 text-sm leading-5 text-muted-foreground">Checks workspace.yaml and the required workspace directories against the supported workspace schema. No source file is changed.</p>
|
||||||
|
{validationNotice && (
|
||||||
|
<p role="status" className="mt-3 rounded-md border border-emerald-500/30 bg-emerald-500/10 px-3 py-2 text-sm text-emerald-700">
|
||||||
|
<CheckCircle2 className="mr-1 inline size-4 text-emerald-600" />{validationNotice}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{validationDiagnostics.length > 0 && (
|
||||||
|
<div role="alert" aria-live="polite" className="mt-3 grid gap-1 rounded-md border border-amber-500/30 bg-amber-500/10 px-3 py-2 text-sm">
|
||||||
|
{validationDiagnostics.map((diagnostic) => (
|
||||||
|
<p key={diagnostic} className="flex items-start gap-2">
|
||||||
|
<AlertCircle className="mt-0.5 size-4 shrink-0 text-amber-700" />{diagnostic}
|
||||||
|
</p>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<Button className="mt-3" size="sm" variant="outline" disabled={busyAction === "validate"} onClick={() => { void validateSource(); }}>
|
<Button className="mt-3" size="sm" variant="outline" disabled={busyAction === "validate"} onClick={() => { void validateSource(); }}>
|
||||||
<ClipboardCheck />Validate workspace source
|
<ClipboardCheck />Validate workspace source
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-lg border border-border p-4">
|
<div data-testid="workspace-connection-card" className="rounded-lg border border-border p-4">
|
||||||
<h4 className="font-heading font-semibold">Test workspace connections</h4>
|
<h4 className="font-heading font-semibold">Test workspace connections</h4>
|
||||||
<p className="mt-1 min-h-12 text-sm leading-5 text-muted-foreground">Uses temporary decrypted credentials to verify the configured data warehouse and Evidence source. Temporary files are deleted after the test.</p>
|
<p className="mt-1 min-h-12 text-sm leading-5 text-muted-foreground">Uses temporary decrypted credentials to verify the configured data warehouse and Evidence source. Temporary files are deleted after the test.</p>
|
||||||
|
{connectionNotice && (
|
||||||
|
<p role="status" className="mt-3 rounded-md border border-emerald-500/30 bg-emerald-500/10 px-3 py-2 text-sm text-emerald-700">
|
||||||
|
<CheckCircle2 className="mr-1 inline size-4 text-emerald-600" />{connectionNotice}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{connectionDiagnostics.length > 0 && (
|
||||||
|
<div role="alert" aria-live="polite" className="mt-3 grid gap-1 rounded-md border border-amber-500/30 bg-amber-500/10 px-3 py-2 text-sm">
|
||||||
|
{connectionDiagnostics.map((diagnostic) => (
|
||||||
|
<p key={diagnostic} className="flex items-start gap-2">
|
||||||
|
<AlertCircle className="mt-0.5 size-4 shrink-0 text-amber-700" />{diagnostic}
|
||||||
|
</p>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<Button className="mt-3" size="sm" variant="outline" disabled={busyAction === "test"} onClick={() => { void testConnections(); }}>
|
<Button className="mt-3" size="sm" variant="outline" disabled={busyAction === "test"} onClick={() => { void testConnections(); }}>
|
||||||
<FlaskConical />Test workspace connections
|
<FlaskConical />Test workspace connections
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -122,6 +122,12 @@ func extractSecretValues(contents []byte) ([]string, error) {
|
|||||||
if whole != "" {
|
if whole != "" {
|
||||||
values = append(values, whole)
|
values = append(values, whole)
|
||||||
}
|
}
|
||||||
|
// PEM files (including OpenSSH private keys) can contain base64 lines that look
|
||||||
|
// like dotenv assignments. Keep the complete document opaque instead of trying
|
||||||
|
// to parse it as a dotenv bundle.
|
||||||
|
if bytes.HasPrefix(trimmed, []byte("-----BEGIN ")) {
|
||||||
|
return values, nil
|
||||||
|
}
|
||||||
|
|
||||||
if trimmed[0] == '{' || trimmed[0] == '[' {
|
if trimmed[0] == '{' || trimmed[0] == '[' {
|
||||||
var document any
|
var document any
|
||||||
|
|||||||
@@ -105,6 +105,26 @@ func TestSecretValuesFromFilesRedactsEveryDotenvBundleValue(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSecretValuesFromFilesAcceptsOpenSSHPrivateKey(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
secretFile := filepath.Join(physicalTempDir(t), "git-ssh-key")
|
||||||
|
contents := "-----BEGIN OPENSSH PRIVATE KEY-----\n" +
|
||||||
|
"ZmFrZS1rZXktcGF5bG9hZA==\n" +
|
||||||
|
"-----END OPENSSH PRIVATE KEY-----\n"
|
||||||
|
if err := os.WriteFile(secretFile, []byte(contents), 0o600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
secrets, err := SecretValuesFromFiles([]string{secretFile})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("SecretValuesFromFiles() error = %v, want OpenSSH key accepted", err)
|
||||||
|
}
|
||||||
|
if len(secrets) == 0 {
|
||||||
|
t.Fatal("SecretValuesFromFiles() returned no values for OpenSSH key")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestSanitizeRecognizesQuotedCredentialKeys(t *testing.T) {
|
func TestSanitizeRecognizesQuotedCredentialKeys(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package pi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var defaultPiArgPattern = regexp.MustCompile(`^ARG[[:space:]]+PI_VERSION[[:space:]]*=(.*)$`)
|
||||||
|
|
||||||
|
// ReadPinnedVersion reads the repository's declared Pi runtime version. It deliberately reads
|
||||||
|
// only the default ARG, not the later ARG PI_VERSION declarations used by build stages.
|
||||||
|
func ReadPinnedVersion(projectDirectory string) (string, error) {
|
||||||
|
path := filepath.Join(projectDirectory, "docker", "core.Dockerfile")
|
||||||
|
file, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("read Pi version pin: %w", err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
var version string
|
||||||
|
count := 0
|
||||||
|
scanner := bufio.NewScanner(file)
|
||||||
|
for scanner.Scan() {
|
||||||
|
match := defaultPiArgPattern.FindStringSubmatch(strings.TrimSuffix(scanner.Text(), "\r"))
|
||||||
|
if len(match) != 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
count++
|
||||||
|
version = strings.TrimSpace(match[1])
|
||||||
|
}
|
||||||
|
if err := scanner.Err(); err != nil {
|
||||||
|
return "", fmt.Errorf("read Pi version pin: %w", err)
|
||||||
|
}
|
||||||
|
if count != 1 {
|
||||||
|
return "", errors.New("docker/core.Dockerfile must contain exactly one default PI_VERSION")
|
||||||
|
}
|
||||||
|
if _, err := parseSemanticVersion(version); err != nil {
|
||||||
|
return "", errors.New("docker/core.Dockerfile contains an invalid Pi version")
|
||||||
|
}
|
||||||
|
return version, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package pi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestReadPinnedVersionReadsTheSingleDefaultPiArg(t *testing.T) {
|
||||||
|
root := t.TempDir()
|
||||||
|
writeCoreDockerfile(t, root, "ARG PI_VERSION=0.81.0\nARG PI_VERSION\n")
|
||||||
|
|
||||||
|
got, err := ReadPinnedVersion(root)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if got != "0.81.0" {
|
||||||
|
t.Fatalf("ReadPinnedVersion() = %q, want 0.81.0", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReadPinnedVersionRejectsMissingDuplicateAndMalformedPins(t *testing.T) {
|
||||||
|
for _, test := range []struct {
|
||||||
|
name string
|
||||||
|
file string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{name: "missing", file: "# no default\n", want: "one default PI_VERSION"},
|
||||||
|
{name: "duplicate", file: "ARG PI_VERSION=0.80.3\nARG PI_VERSION=0.81.0\n", want: "one default PI_VERSION"},
|
||||||
|
{name: "malformed", file: "ARG PI_VERSION=latest\n", want: "invalid Pi version"},
|
||||||
|
} {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
root := t.TempDir()
|
||||||
|
writeCoreDockerfile(t, root, test.file)
|
||||||
|
_, err := ReadPinnedVersion(root)
|
||||||
|
if err == nil || !strings.Contains(err.Error(), test.want) {
|
||||||
|
t.Fatalf("ReadPinnedVersion() error = %v, want %q", err, test.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeCoreDockerfile(t *testing.T, root, contents string) {
|
||||||
|
t.Helper()
|
||||||
|
if err := os.MkdirAll(filepath.Join(root, "docker"), 0o755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(filepath.Join(root, "docker", "core.Dockerfile"), []byte(contents), 0o600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user