gentle-pi 3.0.0 → 3.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/orchestrator-memory.md +1 -1
- package/assets/sdd-orchestrator-workflow.md +1 -1
- package/docs/gentle-shell.md +1 -1
- package/docs/readme-reference.md +6 -6
- package/extensions/gentle-ai.ts +2 -2
- package/extensions/pi-pretty.ts +13 -1
- package/lib/native-review-cli.ts +18 -0
- package/lib/sdd-preflight.ts +5 -19
- package/package.json +1 -1
- package/runtime/native-review-cli.mjs +18 -0
- package/scripts/gentle-ai-installer.mjs +12 -12
- package/scripts/verify-package-files.mjs +2 -2
- package/tests/gentle-ai-binary.test.ts +3 -3
- package/tests/gentle-ai-installer.test.ts +51 -51
- package/tests/gentle-theme.test.ts +2 -0
- package/tests/native-review-capability-contract.test.ts +30 -1
- package/tests/odd-routing-contract.test.ts +5 -0
- package/tests/package-manifest.test.ts +6 -6
- package/tests/pi-pretty.test.ts +5 -0
- package/tests/runtime-harness.mjs +12 -10
- package/tests/sdd-managed-runtime-settlement.test.ts +2 -2
- package/tests/sdd-preflight-rpc-input.test.ts +29 -29
- package/tests/sdd-preflight.test.ts +12 -7
- package/tests/windows-hidden-processes.test.ts +2 -2
- package/themes/Gentleman-Cute.json +1 -1
|
@@ -14,7 +14,7 @@ On resume, use `mem_context`, then project/feature-scoped `mem_search`, and `mem
|
|
|
14
14
|
|
|
15
15
|
Before implementation or resume, the parent reads both the actual file and full observation, reconciles them, and passes the locator and relevant context; workers read the document before edits. Small work without a document still receives its authorized scope and checks.
|
|
16
16
|
|
|
17
|
-
The existing `todo` tool is
|
|
17
|
+
The existing `todo` tool is the required session/UI projection for substantial ODD, not a third authority. After reconciling and writing the durable file and Engram copy, create or rebuild the visible `todo` list from the same feature tasks before the first source write; after every task transition and material plan change, update both durable copies and the visible projection in the same turn; its replay or completed-list clearing must not delete or replace the durable file or Engram copy. If the projection is unavailable, record that limitation without pretending it is synchronized. Small/read-only work does not acquire an ODD artifact or todo list merely because the UI can display tasks.
|
|
18
18
|
|
|
19
19
|
### SDD phases
|
|
20
20
|
|
|
@@ -71,7 +71,7 @@ Do not guess the active change. If change selection is ambiguous, ask the user a
|
|
|
71
71
|
|
|
72
72
|
## Lazy SDD Preflight
|
|
73
73
|
|
|
74
|
-
Do not ask SDD setup questions on session start. The first time the user initiates an SDD process in a Pi session, run the SDD preflight once and keep those choices for the rest of that session. Runtime
|
|
74
|
+
Do not ask SDD setup questions on session start. The first time the user initiates an SDD process in a Pi session, run the SDD preflight once and keep those choices for the rest of that session. Runtime input detection is intentionally syntax-only: slash SDD flows and `/gentle-sdd-init` run preflight automatically; ordinary natural-language text never triggers preflight by itself. For natural-language requests, the parent/orchestrator decides semantically whether the user explicitly selected SDD. Only after that selection, when the parent is about to dispatch the first SDD operation or agent, the execution gate runs or reuses `/gentle:sdd-preflight` before allowing SDD work to start. Merely mentioning, asking about, explaining, comparing, auditing, or referencing SDD — including questions ("what is SDD?"), negatives ("don't use SDD for this"), conditionals ("could we use SDD?"), bug reports about SDD itself, and passing references — stays in the normal read-only flow without preflight, the init guard, or any SDD phase. When a message is ambiguous between discussing SDD and requesting it, ask one clarification and remain read-only until answered.
|
|
75
75
|
|
|
76
76
|
**Hard gate:** `openspec/config.yaml`, existing SDD changes, installed `.pi`/global SDD assets, or a todo named "preflight" are not session preflight. They are project context only. Do not mark SDD preflight complete, start `sdd-init`, launch SDD subagents/chains, or move to explore/proposal/spec/design/tasks until this session has an injected `## SDD Session Preflight` block or an equivalent resolution from the canonical authority order below.
|
|
77
77
|
|
package/docs/gentle-shell.md
CHANGED
|
@@ -15,7 +15,7 @@ The [v2.6.0 release](https://github.com/Gentleman-Programming/gentle-pi/releases
|
|
|
15
15
|
- The Agents List and Details views preserve the orchestrator/session hierarchy and completion, abort, and lost-exit history. Parent-child queries and notifications have an explicit handoff path, while model, effort, and usage stay observable per task.
|
|
16
16
|
- Named `/gentle:profiles` atomically route the orchestrator separately from packaged and review roles; see the [technical reference](readme-reference.md#agent-model-profiles) for the profile model.
|
|
17
17
|
|
|
18
|
-
The source checkout currently prepares `gentle-pi` `3.
|
|
18
|
+
The source checkout currently prepares `gentle-pi` `3.1.0` with a package-local Gentle AI `v3.0.1` pin; this is not a claim that `3.1.0` is published.
|
|
19
19
|
|
|
20
20
|
## Shell interactions and runtime behavior
|
|
21
21
|
|
package/docs/readme-reference.md
CHANGED
|
@@ -99,7 +99,7 @@ This is guidance through existing tools, not a new CLI, phase, state engine, or
|
|
|
99
99
|
| **Skill creation workflow** | Provides the `gentle-ai-skill-creator`/`gentle-ai-skill-improver` skills, `/skill-creation` prompt, and packaged style guide for LLM-first skills. |
|
|
100
100
|
| **Delivery skills** | Includes issue-first PRs, chained PRs, work-unit commits, cognitive docs, comment writing, and Judgment Day review. |
|
|
101
101
|
| **Bounded native review** | Freezes one candidate, dispatches only controller-selected lenses, and records native authority. Review outcomes are informational; delivery follows ordinary repository policy. |
|
|
102
|
-
| **Verified native runtime** | The current source checkout provisions the exact package-local Gentle AI
|
|
102
|
+
| **Verified native runtime** | The current source checkout provisions the exact package-local Gentle AI v3.0.1 runtime: signed, SHA-256-pinned release archives on Darwin/Linux and a Go SumDB-verified source build on Windows x64/arm64. It validates package-local integrity and rejects PATH, global, sibling, symlink, and mode fallbacks. |
|
|
103
103
|
| **Runtime safety** | Blocks destructive shell commands, asks for confirmation for sensitive operations, and blocks direct read/write/edit access to sensitive paths. |
|
|
104
104
|
|
|
105
105
|
## Native pointer regions
|
|
@@ -136,7 +136,7 @@ The stable release is [`v2.6.0`](https://github.com/Gentleman-Programming/gentle
|
|
|
136
136
|
|
|
137
137
|
### Source checkout
|
|
138
138
|
|
|
139
|
-
This checkout prepares `gentle-pi` `3.
|
|
139
|
+
This checkout prepares `gentle-pi` `3.1.0`; it is source state, not a published release. Its package-local native runtime pin is Gentle AI `v3.0.1`, distinct from the published `v2.6.0` pairing.
|
|
140
140
|
|
|
141
141
|
The native SDD status consumer accepts both the pinned producer's legacy
|
|
142
142
|
`apply`/`verify`/`remediate`/`archive` instruction record and the classical
|
|
@@ -147,7 +147,7 @@ Unknown or incomplete instruction records still fail closed.
|
|
|
147
147
|
The Pi runtime now uses native status exclusively for SDD and retires standalone
|
|
148
148
|
sync. The full chain follows completed apply to archive, where applicable delta
|
|
149
149
|
specs are composed; verification remains explicitly invokable. With the current
|
|
150
|
-
|
|
150
|
+
3.0.1 pin, native still requires verification and its emitted evidence requirements;
|
|
151
151
|
a plain practical PASS report does not satisfy that legacy native gate. Pi forwards
|
|
152
152
|
those exact instructions without overriding readiness or inventing legacy evidence.
|
|
153
153
|
Classical direct-archive behavior is compatibility-tested with an identified
|
|
@@ -181,7 +181,7 @@ pi install npm:gentle-pi@2.6.0
|
|
|
181
181
|
|
|
182
182
|
RDD remains opt-in. Enable it only through an explicit user decision with `/gentle:review-mode enable`; `status` lets you inspect the mode without changing it.
|
|
183
183
|
|
|
184
|
-
The source checkout's RDD integration installs Gentle AI only into its private `.gentle-ai/` directory. Darwin and Linux use pinned release assets with asset and executable SHA-256 verification (signed archives for source pin `
|
|
184
|
+
The source checkout's RDD integration installs Gentle AI only into its private `.gentle-ai/` directory. Darwin and Linux use pinned release assets with asset and executable SHA-256 verification (signed archives for source pin `v3.0.1`; raw prerelease binaries only under a prerelease pin). Windows x64 and arm64 build the exact `v3.0.1` source tag with a local Go 1.25.10+ toolchain, a sealed Go environment, `GOTOOLCHAIN=local`, and `GOSUMDB=sum.golang.org`; it does not download Go automatically. Windows provenance is Go-toolchain plus SumDB evidence and postinstall tamper detection, **not** Authenticode or protection against a malicious joint binary-and-manifest replacement. Package-private locks coordinate cooperative concurrent or crashed installers; their tombstones fail closed. A malicious same-user process with write access to package-private `node_modules` is outside that protocol because it can already replace package code, binary, or manifest, and portable Node has no pathname-delete CAS. It never uses `PATH` or a global `gentle-ai` installation. For development or offline installs only, set `GENTLE_PI_SKIP_GENTLE_AI_INSTALL=1`; native review operations then fail closed with an actionable `package-local-binary-missing` error. To recover explicitly, if `GENTLE_PI_SKIP_GENTLE_AI_INSTALL` is set, remove or unset it before changing to the installed `gentle-pi` package directory. Then run `node scripts/install-gentle-ai.mjs`. This invokes the package-owned installer without relying on a global binary or npm configuration change. A missing binary can result from skipped lifecycle scripts, but does not prove that lifecycle scripts were disabled.
|
|
185
185
|
|
|
186
186
|
Recommended companion packages:
|
|
187
187
|
|
|
@@ -338,13 +338,13 @@ flowchart TD
|
|
|
338
338
|
|
|
339
339
|
VALIDATE is informational. Commit, push, PR, and release commands follow ordinary repository policy; RDD never authorizes, rewrites, consumes review state for, or blocks them. Dangerous-command safety and destructive-review consent remain independent.
|
|
340
340
|
|
|
341
|
-
For the source checkout, native contract pairing is exact: this adapter resolves only the integrity-verified package-local Gentle AI
|
|
341
|
+
For the source checkout, native contract pairing is exact: this adapter resolves only the integrity-verified package-local Gentle AI v3.0.1 executable, independently hashes it, then negotiates `gentle-ai.review-integration/v2` outside the repository. Capabilities are cached by that executable digest. Every START, target status, FINALIZE, validate, and BIND-SDD request passes the same contract identifier. Negotiated envelopes decode exactly against the vendored schemas; `recover` routes only the provider-selected `action_disposition`, and optional additions require a future compatible schema/minor that the provider explicitly advertises and the consumer negotiates.
|
|
342
342
|
|
|
343
343
|
Contract `/v2` replaces the Base64 `candidate_diff` reviewer transport of `/v1` with immutable `base_tree`/`candidate_tree` plus an ordered `changed_path_manifest` and never an inline patch. `gentle-pi` negotiates `/v2` only, with no dual-lane fallback; the cutover landed as one atomic commit against gentle-ai v2.2.2 (tracked by the `migrate-review-integration-v2` change), and the `/v1` schemas stay packaged because the `/v2` schemas `$ref` into their fragments. This provider contract version is unrelated to Pi's own internal "compact-v2" review-authority naming used below — the shared digit is coincidental, not a version pairing.
|
|
344
344
|
|
|
345
345
|
Target status owns `current_target`, `unrelated`, `ambiguous`, and `corrupted` applicability and returns one native action. Pi does not reconstruct ordinary authority from provider-private files or choose a lineage from repository-wide history. Restart recovery rebuilds only the derived candidate view from the native Git/content projection, including intended-untracked paths, symlinks, and immutable gitlink identities. Native failure envelopes retain their exact mutation outcome, replayability, required inputs, request digest, and next action. After an unknown or lost mutating result, Pi calls target status before any replay decision and returns only the provider-declared action.
|
|
346
346
|
|
|
347
|
-
Once the source checkout's pinned gentle-ai runtime (currently
|
|
347
|
+
Once the source checkout's pinned gentle-ai runtime (currently v3.0.1) has written review authority, rollback MUST preserve every native store and receipt and MUST NOT run a downgraded binary against that repository. Disable the Pi route or roll forward to a compatible authority-aware release instead; deleting authority data or reinstalling an older binary is not a rollback path.
|
|
348
348
|
|
|
349
349
|
### FINALIZE wrapper input
|
|
350
350
|
|
package/extensions/gentle-ai.ts
CHANGED
|
@@ -1388,8 +1388,8 @@ Organic Driven Development (ODD) is the predefined workflow of this orchestrator
|
|
|
1388
1388
|
2. **Explore.** Explore existing code and requirements first, proportionately to the request, before proposing or writing anything.
|
|
1389
1389
|
3. **Resolve uncertainty.** Recommend optional research only for a named uncertainty; ask one focused user question only for a real unresolved product decision, then stop and wait; use at most one scoped read-only assumption challenge for a high-consequence unproven premise.
|
|
1390
1390
|
4. **Classify.** The work is substantial when exploration yields two or more meaningful implementation steps, or progress worth recovering after an interruption. Small, understood work stays small and creates no durable task artifacts.
|
|
1391
|
-
5. **Track before the first write.** For substantial authorized implementation, create \`odd/tasks/<feature-name>.md\` and its Engram mirror \`odd/<feature-name>/tasks\` automatically, before the first source write
|
|
1392
|
-
6. **Implement task by task.** Route each task through the orchestrator's Work Routing Ladder, with the configured TDD mode and applicable checks. Check an item off only after its outcome and checks were observed; update the file and
|
|
1391
|
+
5. **Track before the first write.** For substantial authorized implementation, create \`odd/tasks/<feature-name>.md\` and its Engram mirror \`odd/<feature-name>/tasks\` automatically, then create or rebuild the visible \`todo\` list from the reconciled feature tasks, all before the first source write and without asking permission for tasks or storage. Tell the user in one line which feature document was created and how many tasks it holds.
|
|
1392
|
+
6. **Implement task by task.** Route each task through the orchestrator's Work Routing Ladder, with the configured TDD mode and applicable checks. Check an item off only after its outcome and checks were observed; update the file, mirror, and visible \`todo\` projection after every task transition and material plan change.
|
|
1393
1393
|
7. **Close.** Report the verified outcome, every failed, skipped, or pending check, and the next step. Native review applies only at the deliverable boundary and only under the user-owned RDD switch.
|
|
1394
1394
|
Resume an interrupted feature with \`mem_context\`, then project- and feature-scoped \`mem_search\`, then \`mem_get_observation\` for the full document, then the task file itself; reconcile before continuing the next unfinished task. Detail for steps 3–7: \`orchestrator-delegation.md\` and \`orchestrator-memory.md\`.
|
|
1395
1395
|
|
package/extensions/pi-pretty.ts
CHANGED
|
@@ -61,7 +61,19 @@ export default async function gentlePiPrettyExtension(
|
|
|
61
61
|
};
|
|
62
62
|
},
|
|
63
63
|
});
|
|
64
|
-
|
|
64
|
+
// pi-pretty's hidden-thinking API is global. Its best-effort private per-row
|
|
65
|
+
// patch can miss the host's actual component class and then animate every
|
|
66
|
+
// historical Thinking row. Gentle Shell chooses the robust presentation:
|
|
67
|
+
// keep collapsed labels static while preserving the separate working frame.
|
|
68
|
+
const previousThinkingIndicator = process.env.PRETTY_THINKING_INDICATOR;
|
|
69
|
+
process.env.PRETTY_THINKING_INDICATOR = "off";
|
|
70
|
+
let result: unknown;
|
|
71
|
+
try {
|
|
72
|
+
result = await extension(delegated, deps);
|
|
73
|
+
} finally {
|
|
74
|
+
if (previousThinkingIndicator === undefined) delete process.env.PRETTY_THINKING_INDICATOR;
|
|
75
|
+
else process.env.PRETTY_THINKING_INDICATOR = previousThinkingIndicator;
|
|
76
|
+
}
|
|
65
77
|
// GentlePromptEditor already carries the live working state in its frame.
|
|
66
78
|
// Hide Pi's separate loader row to avoid repeating Thinking above the input;
|
|
67
79
|
// transcript thinking blocks remain untouched as historical reasoning markers.
|
package/lib/native-review-cli.ts
CHANGED
|
@@ -968,6 +968,24 @@ export const NATIVE_CLI_CONTRACTS = Object.freeze({
|
|
|
968
968
|
// exactly. riskEvidence and hint remain dark because neither is proven to
|
|
969
969
|
// reach the negotiated START path Pi consumes.
|
|
970
970
|
"2.9.1": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: true, status: true, inventory: true, reclaim: true, recover: true, abandon: true, quarantineLegacy: true, reconcileAuthority: true, repairLegacyAlias: true, mode: true, riskEvidence: false, hint: false, delivery: true }),
|
|
971
|
+
// v3.0.0 shipped ODD as the orchestrator's mandatory default protocol and
|
|
972
|
+
// integrated the simplified SDD workflow into it (gentle-ai #4642, #4644),
|
|
973
|
+
// with the provider contract byte-frozen at 1.2.0. Ground-truthed by
|
|
974
|
+
// diffing contracts/review-integration/v2 and
|
|
975
|
+
// contracts/review-provider-contract between the v2.9.1 and v3.0.0 tags
|
|
976
|
+
// in the gentle-ai source tree: zero bytes changed. Neither change touches
|
|
977
|
+
// the closed START/STATUS fields this row negotiates, so it repeats 2.9.1
|
|
978
|
+
// exactly. riskEvidence and hint remain dark because neither is proven to
|
|
979
|
+
// reach the negotiated START path Pi consumes.
|
|
980
|
+
"3.0.0": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: true, status: true, inventory: true, reclaim: true, recover: true, abandon: true, quarantineLegacy: true, reconcileAuthority: true, repairLegacyAlias: true, mode: true, riskEvidence: false, hint: false, delivery: true }),
|
|
981
|
+
// v3.0.1 moved the Go module path to github.com/gentleman-programming/gentle-ai/v3
|
|
982
|
+
// with no contract change (gentle-ai #4683). Ground-truthed by diffing
|
|
983
|
+
// contracts/review-integration/v2 and contracts/review-provider-contract
|
|
984
|
+
// between the v3.0.0 and v3.0.1 tags in the gentle-ai source tree: zero
|
|
985
|
+
// bytes changed. This row repeats 3.0.0 (and 2.9.1) exactly. riskEvidence
|
|
986
|
+
// and hint remain dark because neither is proven to reach the negotiated
|
|
987
|
+
// START path Pi consumes.
|
|
988
|
+
"3.0.1": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: true, status: true, inventory: true, reclaim: true, recover: true, abandon: true, quarantineLegacy: true, reconcileAuthority: true, repairLegacyAlias: true, mode: true, riskEvidence: false, hint: false, delivery: true }),
|
|
971
989
|
});
|
|
972
990
|
|
|
973
991
|
export interface NativeReviewProcessDiagnostics {
|
package/lib/sdd-preflight.ts
CHANGED
|
@@ -835,26 +835,12 @@ export function installPackageAssets(
|
|
|
835
835
|
}, lockOptions);
|
|
836
836
|
}
|
|
837
837
|
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
// "I use SDD sometimes" remains ordinary conversation.
|
|
843
|
-
if (!/\bsdd\b/i.test(text)) return false;
|
|
844
|
-
return /(?:\b(?:please|por\s+favor)\b|\b(?:want|need|would\s+like|let'?s|quiero|queremos|necesito|quisiera|me\s+gustar[ií]a|vamos|vayamos|hagamos|usemos)\b|^(?:use|run|start|build|create|implement|handle|make|usa|usá|corre|corré|arranca|arrancá|inicia|iniciá|empeza|empezá|hacelo|hazlo|hacerlo)\b)/i.test(text);
|
|
845
|
-
}
|
|
846
|
-
|
|
838
|
+
// Input interception is intentionally syntax-only. Natural-language SDD intent
|
|
839
|
+
// belongs to the parent/orchestrator; dispatch and before_agent_start gates run
|
|
840
|
+
// or reuse preflight when an SDD action is actually attempted. This keeps mere
|
|
841
|
+
// mentions side-effect free without trying to encode language in a regex.
|
|
847
842
|
export function isSddPreflightTrigger(text: string): boolean {
|
|
848
|
-
|
|
849
|
-
if (/^\/(?:gentle-)?sdd(?:[-:][^\s]*)?(?:\s|$)/i.test(trimmed)) return true;
|
|
850
|
-
if (/[??]\s*$/.test(trimmed)) return false;
|
|
851
|
-
if (
|
|
852
|
-
/(?:\b(?:don't|do\s+not|never)\b|\bnot\s+(?:want|need|plan(?:ning)?|intend|use|using)\b)[^.!?\n]{0,80}\bsdd\b/i.test(trimmed) ||
|
|
853
|
-
/\b(?:sin\s+usar|no\s+(?:quiero|queremos|necesito|necesitamos|quisiera|quisiéramos|vamos\s+a|pienso|planeo|usar))\b[^.!?\n]{0,80}\bsdd\b/i.test(trimmed)
|
|
854
|
-
) {
|
|
855
|
-
return false;
|
|
856
|
-
}
|
|
857
|
-
return hasAffirmativeSddIntent(trimmed);
|
|
843
|
+
return /^\/(?:gentle-)?sdd(?:[-:][^\s]*)?(?:\s|$)/i.test(text.trim());
|
|
858
844
|
}
|
|
859
845
|
|
|
860
846
|
export function sddPreflightSessionKey(ctx: ExtensionContext): string {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gentle-pi",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.1.1",
|
|
4
4
|
"description": "Turn Pi into el Gentleman: a senior-architect development harness with SDD/OpenSpec, subagents, strict TDD evidence, review guardrails, and skill discovery.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -969,6 +969,24 @@ export const NATIVE_CLI_CONTRACTS = Object.freeze({
|
|
|
969
969
|
// exactly. riskEvidence and hint remain dark because neither is proven to
|
|
970
970
|
// reach the negotiated START path Pi consumes.
|
|
971
971
|
"2.9.1": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: true, status: true, inventory: true, reclaim: true, recover: true, abandon: true, quarantineLegacy: true, reconcileAuthority: true, repairLegacyAlias: true, mode: true, riskEvidence: false, hint: false, delivery: true }),
|
|
972
|
+
// v3.0.0 shipped ODD as the orchestrator's mandatory default protocol and
|
|
973
|
+
// integrated the simplified SDD workflow into it (gentle-ai #4642, #4644),
|
|
974
|
+
// with the provider contract byte-frozen at 1.2.0. Ground-truthed by
|
|
975
|
+
// diffing contracts/review-integration/v2 and
|
|
976
|
+
// contracts/review-provider-contract between the v2.9.1 and v3.0.0 tags
|
|
977
|
+
// in the gentle-ai source tree: zero bytes changed. Neither change touches
|
|
978
|
+
// the closed START/STATUS fields this row negotiates, so it repeats 2.9.1
|
|
979
|
+
// exactly. riskEvidence and hint remain dark because neither is proven to
|
|
980
|
+
// reach the negotiated START path Pi consumes.
|
|
981
|
+
"3.0.0": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: true, status: true, inventory: true, reclaim: true, recover: true, abandon: true, quarantineLegacy: true, reconcileAuthority: true, repairLegacyAlias: true, mode: true, riskEvidence: false, hint: false, delivery: true }),
|
|
982
|
+
// v3.0.1 moved the Go module path to github.com/gentleman-programming/gentle-ai/v3
|
|
983
|
+
// with no contract change (gentle-ai #4683). Ground-truthed by diffing
|
|
984
|
+
// contracts/review-integration/v2 and contracts/review-provider-contract
|
|
985
|
+
// between the v3.0.0 and v3.0.1 tags in the gentle-ai source tree: zero
|
|
986
|
+
// bytes changed. This row repeats 3.0.0 (and 2.9.1) exactly. riskEvidence
|
|
987
|
+
// and hint remain dark because neither is proven to reach the negotiated
|
|
988
|
+
// START path Pi consumes.
|
|
989
|
+
"3.0.1": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: true, status: true, inventory: true, reclaim: true, recover: true, abandon: true, quarantineLegacy: true, reconcileAuthority: true, repairLegacyAlias: true, mode: true, riskEvidence: false, hint: false, delivery: true }),
|
|
972
990
|
});
|
|
973
991
|
|
|
974
992
|
|
|
@@ -36,19 +36,19 @@ const WINDOWS_SYSTEM_ROOT = "C:\\Windows";
|
|
|
36
36
|
// version check below) derives from this constant instead of repeating the
|
|
37
37
|
// literal, so a pin bump cannot leave a stale copy behind. See
|
|
38
38
|
// scripts/install-gentle-ai.mjs for the incident that motivated this.
|
|
39
|
-
export const INSTALLER_VERSION = "
|
|
39
|
+
export const INSTALLER_VERSION = "3.0.1";
|
|
40
40
|
export const RELEASE_BASE_URL = `https://github.com/Gentleman-Programming/gentle-ai/releases/download/v${INSTALLER_VERSION}/`;
|
|
41
41
|
export const GENTLE_AI_INSTALL_METHOD = Object.freeze({
|
|
42
42
|
SIGNED_RELEASE_ASSET: "signed-release-asset",
|
|
43
43
|
GO_SUMDB_SOURCE_BUILD: "go-sumdb-source-build",
|
|
44
44
|
});
|
|
45
|
-
export const GENTLE_AI_WINDOWS_SOURCE_PACKAGE_PATH = "github.com/gentleman-programming/gentle-ai/
|
|
46
|
-
export const GENTLE_AI_WINDOWS_SOURCE_MODULE = "github.com/gentleman-programming/gentle-ai/
|
|
45
|
+
export const GENTLE_AI_WINDOWS_SOURCE_PACKAGE_PATH = "github.com/gentleman-programming/gentle-ai/v3/cmd/gentle-ai";
|
|
46
|
+
export const GENTLE_AI_WINDOWS_SOURCE_MODULE = "github.com/gentleman-programming/gentle-ai/v3";
|
|
47
47
|
export const GENTLE_AI_WINDOWS_SOURCE_TAG = `v${INSTALLER_VERSION}`;
|
|
48
|
-
// `go mod download -json github.com/gentleman-programming/gentle-ai/
|
|
48
|
+
// `go mod download -json github.com/gentleman-programming/gentle-ai/v3@v3.0.1`
|
|
49
49
|
// with GOSUMDB=sum.golang.org reports this exact module SumDB checksum, and the
|
|
50
|
-
// tag resolves to commit
|
|
51
|
-
export const GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM = "h1:
|
|
50
|
+
// tag resolves to commit 2594581e, the published v3.0.1 release head.
|
|
51
|
+
export const GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM = "h1:r2z8yf0CdL0ZPPkMEKI3km6SitjOIC/DQDvtipc1Igs=";
|
|
52
52
|
export const GENTLE_AI_WINDOWS_SOURCE_PACKAGE = `${GENTLE_AI_WINDOWS_SOURCE_PACKAGE_PATH}@${GENTLE_AI_WINDOWS_SOURCE_TAG}`;
|
|
53
53
|
export const GENTLE_AI_WINDOWS_MINIMUM_GO_VERSION = "1.25.10";
|
|
54
54
|
export const GENTLE_AI_GO_TOOLCHAIN_UNAVAILABLE_CODE = "GENTLE_AI_GO_TOOLCHAIN_UNAVAILABLE";
|
|
@@ -67,7 +67,7 @@ export class GentleAiInstallerError extends Error {
|
|
|
67
67
|
// Sentinel used while a re-pinned gentle-ai release is not yet published. A
|
|
68
68
|
// sentinel digest can never match a real SHA-256, so installation fails closed,
|
|
69
69
|
// and verify-package-files.mjs refuses to pack/publish while any digest below
|
|
70
|
-
// still holds it. The
|
|
70
|
+
// still holds it. The v3.0.1 digests are pinned from the published release:
|
|
71
71
|
// archive sha256 values verified against the minisign-signed checksums.txt and
|
|
72
72
|
// freshly computed hashes; binary sha256 values computed from the extracted
|
|
73
73
|
// executables.
|
|
@@ -109,15 +109,15 @@ async function downloadPinnedGentleAiAsset(asset, destination, options) {
|
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
// Windows is absent from signed release archives on purpose. gentle-ai stopped
|
|
112
|
-
// distributing unsigned Windows builds in c4b764d0, so
|
|
112
|
+
// distributing unsigned Windows builds in c4b764d0, so v3.0.1 publishes signed
|
|
113
113
|
// Darwin/Linux archives only. Windows x64/arm64 uses the separately verified
|
|
114
114
|
// exact-tag Go SumDB source-build path below; restore archive rows only when
|
|
115
115
|
// upstream ships signed Windows assets.
|
|
116
116
|
export const GENTLE_AI_RELEASE_ASSETS = Object.freeze({
|
|
117
|
-
"darwin/amd64": asset("gentle-
|
|
118
|
-
"darwin/arm64": asset("gentle-
|
|
119
|
-
"linux/amd64": asset("gentle-
|
|
120
|
-
"linux/arm64": asset("gentle-
|
|
117
|
+
"darwin/amd64": asset("gentle-ai_3.0.1_darwin_amd64.tar.gz", "fce899fbdada3781624cc03aa6adaf9b21041915240ef092ff89f222151c9efd", "01dec1aea73a0e70d44432d021bdafc9309382f597ce247f58e6c523caed1345", "gentle-ai"),
|
|
118
|
+
"darwin/arm64": asset("gentle-ai_3.0.1_darwin_arm64.tar.gz", "17f12008a056e43042daa6d72b37923a2a071f0d52cca80606410eb096caa9c2", "74c0f14c9806a786ae2cd78989d8ca95e0c01e6c7cc3b80c738f284c1064212e", "gentle-ai"),
|
|
119
|
+
"linux/amd64": asset("gentle-ai_3.0.1_linux_amd64.tar.gz", "920ad99189a655452006e96a7b25f1e04d745973e36b75130a3b4a16c53deda8", "201b79d4e6e7551cdfde3ad44e56ad0638173b1093f87fc1156a24652fa24448", "gentle-ai"),
|
|
120
|
+
"linux/arm64": asset("gentle-ai_3.0.1_linux_arm64.tar.gz", "e54a588c4ce5adfd9ded4c59c6cc5c1eb369fd88f8c54fa892f2cceb7fdbac8d", "9dbd481f5854c568411a8030fdd2192a23ff011270f8994ae181655786dca638", "gentle-ai"),
|
|
121
121
|
});
|
|
122
122
|
|
|
123
123
|
// A pinned asset is either a signed archive or, for a prerelease pin only,
|
|
@@ -339,7 +339,7 @@ async function main() {
|
|
|
339
339
|
});
|
|
340
340
|
|
|
341
341
|
if (driftedContracts.length > 0) {
|
|
342
|
-
console.error("gentle-pi packaged review-integration/v1 and review-integration/v2 contract bytes drifted from the pinned
|
|
342
|
+
console.error("gentle-pi packaged review-integration/v1 and review-integration/v2 contract bytes drifted from the pinned v3.0.1 runtime's vendored Gentle AI contract artifacts:");
|
|
343
343
|
for (const drift of driftedContracts) console.error(`- ${drift.relativePath}: expected ${drift.expected}, got ${drift.actual}`);
|
|
344
344
|
process.exit(1);
|
|
345
345
|
}
|
|
@@ -384,7 +384,7 @@ async function main() {
|
|
|
384
384
|
process.exit(1);
|
|
385
385
|
}
|
|
386
386
|
|
|
387
|
-
console.log(`gentle-pi package resource check passed (${requiredPaths.length} files; ${Object.keys(contractHashes).length} exact byte-pinned contract artifacts for the
|
|
387
|
+
console.log(`gentle-pi package resource check passed (${requiredPaths.length} files; ${Object.keys(contractHashes).length} exact byte-pinned contract artifacts for the v3.0.1 runtime).`);
|
|
388
388
|
}
|
|
389
389
|
|
|
390
390
|
const isMainModule = process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href;
|
|
@@ -95,9 +95,9 @@ async function writeWindowsSourceBinary(packageRoot: string): Promise<{ binaryPa
|
|
|
95
95
|
await writeFile(manifestPath, `${JSON.stringify({
|
|
96
96
|
version: GENTLE_AI_VERSION,
|
|
97
97
|
method: "go-sumdb-source-build",
|
|
98
|
-
package: "github.com/gentleman-programming/gentle-ai/
|
|
99
|
-
module: "github.com/gentleman-programming/gentle-ai/
|
|
100
|
-
tag: "
|
|
98
|
+
package: "github.com/gentleman-programming/gentle-ai/v3/cmd/gentle-ai",
|
|
99
|
+
module: "github.com/gentleman-programming/gentle-ai/v3",
|
|
100
|
+
tag: "v3.0.1",
|
|
101
101
|
architecture: process.arch === "x64" ? "x64" : "arm64",
|
|
102
102
|
binarySha256: createHash("sha256").update(binary).digest("hex"),
|
|
103
103
|
moduleChecksum: GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM,
|
|
@@ -20,14 +20,14 @@ import {
|
|
|
20
20
|
trustedSystemExtractor,
|
|
21
21
|
} from "../scripts/gentle-ai-installer.mjs";
|
|
22
22
|
|
|
23
|
-
//
|
|
23
|
+
// v3.0.1 digests pinned from the published release: archive digests verified
|
|
24
24
|
// against the minisign-signed checksums.txt and freshly computed hashes; binary
|
|
25
25
|
// digests computed from the extracted executables.
|
|
26
26
|
const EXPECTED_ASSETS = {
|
|
27
|
-
"darwin/amd64": { name: "gentle-
|
|
28
|
-
"darwin/arm64": { name: "gentle-
|
|
29
|
-
"linux/amd64": { name: "gentle-
|
|
30
|
-
"linux/arm64": { name: "gentle-
|
|
27
|
+
"darwin/amd64": { name: "gentle-ai_3.0.1_darwin_amd64.tar.gz", sha256: "fce899fbdada3781624cc03aa6adaf9b21041915240ef092ff89f222151c9efd", binarySha256: "01dec1aea73a0e70d44432d021bdafc9309382f597ce247f58e6c523caed1345" },
|
|
28
|
+
"darwin/arm64": { name: "gentle-ai_3.0.1_darwin_arm64.tar.gz", sha256: "17f12008a056e43042daa6d72b37923a2a071f0d52cca80606410eb096caa9c2", binarySha256: "74c0f14c9806a786ae2cd78989d8ca95e0c01e6c7cc3b80c738f284c1064212e" },
|
|
29
|
+
"linux/amd64": { name: "gentle-ai_3.0.1_linux_amd64.tar.gz", sha256: "920ad99189a655452006e96a7b25f1e04d745973e36b75130a3b4a16c53deda8", binarySha256: "201b79d4e6e7551cdfde3ad44e56ad0638173b1093f87fc1156a24652fa24448" },
|
|
30
|
+
"linux/arm64": { name: "gentle-ai_3.0.1_linux_arm64.tar.gz", sha256: "e54a588c4ce5adfd9ded4c59c6cc5c1eb369fd88f8c54fa892f2cceb7fdbac8d", binarySha256: "9dbd481f5854c568411a8030fdd2192a23ff011270f8994ae181655786dca638" },
|
|
31
31
|
} as const;
|
|
32
32
|
|
|
33
33
|
test("default installer package root is the package containing scripts, not its parent", () => {
|
|
@@ -38,15 +38,15 @@ test("default installer package root is the package containing scripts, not its
|
|
|
38
38
|
assert.notEqual(resolveGentleAiInstallerPackageRoot(), dirname(expectedPackageRoot));
|
|
39
39
|
});
|
|
40
40
|
|
|
41
|
-
test("release mapping selects only the supported official
|
|
41
|
+
test("release mapping selects only the supported official v3.0.1 assets and pinned digests", () => {
|
|
42
42
|
assert.deepEqual(
|
|
43
43
|
Object.fromEntries(Object.entries(GENTLE_AI_RELEASE_ASSETS).map(([key, asset]) => [key, { name: asset.name, sha256: asset.sha256, binarySha256: asset.binarySha256 }])),
|
|
44
44
|
EXPECTED_ASSETS,
|
|
45
45
|
);
|
|
46
|
-
assert.equal(resolveGentleAiReleaseAsset("linux", "x64").name, "gentle-
|
|
47
|
-
assert.equal(resolveGentleAiReleaseAsset("darwin", "arm64").name, "gentle-
|
|
46
|
+
assert.equal(resolveGentleAiReleaseAsset("linux", "x64").name, "gentle-ai_3.0.1_linux_amd64.tar.gz");
|
|
47
|
+
assert.equal(resolveGentleAiReleaseAsset("darwin", "arm64").name, "gentle-ai_3.0.1_darwin_arm64.tar.gz");
|
|
48
48
|
for (const asset of Object.values(GENTLE_AI_RELEASE_ASSETS)) {
|
|
49
|
-
assert.match(asset.url, /^https:\/\/github\.com\/Gentleman-Programming\/gentle-ai\/releases\/download\/
|
|
49
|
+
assert.match(asset.url, /^https:\/\/github\.com\/Gentleman-Programming\/gentle-ai\/releases\/download\/v3\.0\.1\//);
|
|
50
50
|
}
|
|
51
51
|
});
|
|
52
52
|
|
|
@@ -56,7 +56,7 @@ test("raw release assets are admitted only under a prerelease pin", () => {
|
|
|
56
56
|
assert.equal(gentleAiAssetForm("gentle-ai_2.5.0-rc.3_windows_amd64.exe", "2.5.0-rc.3"), "raw-binary");
|
|
57
57
|
// A raw binary under a stable pin means the pin itself is wrong: stable
|
|
58
58
|
// releases publish signed archives only, so this fails closed pre-download.
|
|
59
|
-
assert.throws(() => gentleAiAssetForm("gentle-
|
|
59
|
+
assert.throws(() => gentleAiAssetForm("gentle-ai_3.0.1_linux_amd64", "3.0.1"), /only admitted for a prerelease pin/);
|
|
60
60
|
assert.throws(() => gentleAiAssetForm("gentle-ai.dmg", "2.5.0-rc.3"), /unsupported Gentle AI release asset form/);
|
|
61
61
|
// The current stable pin admits every pinned asset row through the same
|
|
62
62
|
// gate the installer uses at download time (default installerVersion
|
|
@@ -83,7 +83,7 @@ test("release digests are all-or-none and install fails closed while any digest
|
|
|
83
83
|
}),
|
|
84
84
|
/checksum mismatch/,
|
|
85
85
|
);
|
|
86
|
-
assert.equal(existsSync(join(packageRoot, ".gentle-ai", "
|
|
86
|
+
assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v3.0.1", "gentle-ai")), false);
|
|
87
87
|
}
|
|
88
88
|
});
|
|
89
89
|
|
|
@@ -111,8 +111,8 @@ function windowsGoFixture(fixtureOptions: WindowsGoFixtureOptions = {}) {
|
|
|
111
111
|
let goExecutable = "go";
|
|
112
112
|
const metadata = [
|
|
113
113
|
"gentle-ai.exe: go1.25.10",
|
|
114
|
-
"\tpath\tgithub.com/gentleman-programming/gentle-ai/
|
|
115
|
-
`\tmod\tgithub.com/gentleman-programming/gentle-ai/
|
|
114
|
+
"\tpath\tgithub.com/gentleman-programming/gentle-ai/v3/cmd/gentle-ai",
|
|
115
|
+
`\tmod\tgithub.com/gentleman-programming/gentle-ai/v3\tv3.0.1\t${GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM}`,
|
|
116
116
|
"\tbuild\t-buildmode=exe", "\tbuild\t-compiler=gc", "\tbuild\tCGO_ENABLED=0", `\tbuild\tGOARCH=${fixtureOptions.goArchitecture ?? "amd64"}`, "\tbuild\tGOOS=windows",
|
|
117
117
|
].join("\n");
|
|
118
118
|
const run = async (file: string, arguments_: string[], options: WindowsGoCall["options"]) => {
|
|
@@ -130,7 +130,7 @@ function windowsGoFixture(fixtureOptions: WindowsGoFixtureOptions = {}) {
|
|
|
130
130
|
return { stdout: "", stderr: "" };
|
|
131
131
|
}
|
|
132
132
|
if (file === goExecutable && arguments_[0] === "version" && arguments_[1] === "-m") return { stdout: metadata, stderr: "" };
|
|
133
|
-
if (arguments_.length === 1 && arguments_[0] === "version") return { stdout: fixtureOptions.reportedVersion ?? "gentle-ai
|
|
133
|
+
if (arguments_.length === 1 && arguments_[0] === "version") return { stdout: fixtureOptions.reportedVersion ?? "gentle-ai 3.0.1\n", stderr: "" };
|
|
134
134
|
throw new Error(`unexpected command: ${file} ${arguments_.join(" ")}`);
|
|
135
135
|
};
|
|
136
136
|
return { calls, run, setGoExecutable: (path: string) => { goExecutable = path; } };
|
|
@@ -146,7 +146,7 @@ test("win32 x64 and arm64 install the exact Go SumDB source tag without archive
|
|
|
146
146
|
const result = await installGentleAi({ packageRoot, platform: "win32", arch, execFile: fixture.run, resolveGoExecutable: async () => goPath });
|
|
147
147
|
assert.equal(result.installed, true);
|
|
148
148
|
assert.deepEqual(fixture.calls.filter((call) => call.file === goPath).map((call) => call.arguments_.slice(0, 2)), [
|
|
149
|
-
["version"], ["install", "github.com/gentleman-programming/gentle-ai/
|
|
149
|
+
["version"], ["install", "github.com/gentleman-programming/gentle-ai/v3/cmd/gentle-ai@v3.0.1"], ["version", "-m"],
|
|
150
150
|
]);
|
|
151
151
|
}
|
|
152
152
|
});
|
|
@@ -165,7 +165,7 @@ test("Windows source install reports missing or too-old Go without publishing a
|
|
|
165
165
|
() => installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: async () => goPath }),
|
|
166
166
|
(error: unknown) => error instanceof Error && "code" in error && error.code === fixtureOptions.expectedCode,
|
|
167
167
|
);
|
|
168
|
-
assert.equal(existsSync(join(packageRoot, ".gentle-ai", "
|
|
168
|
+
assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v3.0.1", "gentle-ai.exe")), false);
|
|
169
169
|
assert.deepEqual((await readdir(packageRoot)).filter((entry) => entry.includes("install-")), []);
|
|
170
170
|
}
|
|
171
171
|
});
|
|
@@ -184,7 +184,7 @@ test("Windows source install cleans staging after Go failure or wrong built vers
|
|
|
184
184
|
() => installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: async () => goPath }),
|
|
185
185
|
(error: unknown) => error instanceof Error && "code" in error && error.code === fixtureOptions.expectedCode,
|
|
186
186
|
);
|
|
187
|
-
const runtimeDirectory = join(packageRoot, ".gentle-ai", "
|
|
187
|
+
const runtimeDirectory = join(packageRoot, ".gentle-ai", "v3.0.1");
|
|
188
188
|
assert.ok(fixture.calls.some((call) => call.arguments_[0] === "install"));
|
|
189
189
|
assert.equal(existsSync(join(runtimeDirectory, "gentle-ai.exe")), false);
|
|
190
190
|
assert.equal(existsSync(runtimeDirectory) && (await readdir(runtimeDirectory)).some((entry) => entry.startsWith(".go-install-") || entry.endsWith(".tmp")), false);
|
|
@@ -205,7 +205,7 @@ test("Windows source installs reuse only a fully verified package-local binary",
|
|
|
205
205
|
assert.equal(reused.installed, false);
|
|
206
206
|
assert.ok(reuse.calls.every((call) => call.file !== "gentle-ai"), "the installer must never fall back to ambient gentle-ai on PATH");
|
|
207
207
|
|
|
208
|
-
await writeFile(join(packageRoot, ".gentle-ai", "
|
|
208
|
+
await writeFile(join(packageRoot, ".gentle-ai", "v3.0.1", "integrity.json"), "{}\n");
|
|
209
209
|
const repaired = windowsGoFixture();
|
|
210
210
|
repaired.setGoExecutable(goPath);
|
|
211
211
|
assert.equal((await installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: repaired.run, resolveGoExecutable: async () => goPath })).installed, true);
|
|
@@ -246,8 +246,8 @@ async function hardenedWindowsGoFixture(packageRoot: string, fixtureOptions: Har
|
|
|
246
246
|
: undefined;
|
|
247
247
|
const metadata = fixtureOptions.metadataOverride ?? [
|
|
248
248
|
"gentle-ai.exe: go1.25.10",
|
|
249
|
-
"\tpath\tgithub.com/gentleman-programming/gentle-ai/
|
|
250
|
-
`\tmod\tgithub.com/gentleman-programming/gentle-ai/
|
|
249
|
+
"\tpath\tgithub.com/gentleman-programming/gentle-ai/v3/cmd/gentle-ai",
|
|
250
|
+
`\tmod\tgithub.com/gentleman-programming/gentle-ai/v3\tv3.0.1\t${GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM}`,
|
|
251
251
|
"\tbuild\t-buildmode=exe",
|
|
252
252
|
"\tbuild\t-compiler=gc",
|
|
253
253
|
"\tbuild\tCGO_ENABLED=0",
|
|
@@ -267,7 +267,7 @@ async function hardenedWindowsGoFixture(packageRoot: string, fixtureOptions: Har
|
|
|
267
267
|
return { stdout: "", stderr: "" };
|
|
268
268
|
}
|
|
269
269
|
if (file === goPath && arguments_[0] === "version" && arguments_[1] === "-m") return { stdout: metadata, stderr: "" };
|
|
270
|
-
if (arguments_.length === 1 && arguments_[0] === "version") return { stdout: "gentle-ai
|
|
270
|
+
if (arguments_.length === 1 && arguments_[0] === "version") return { stdout: "gentle-ai 3.0.1\n", stderr: "" };
|
|
271
271
|
throw new Error(`unexpected command: ${file} ${arguments_.join(" ")}`);
|
|
272
272
|
};
|
|
273
273
|
return {
|
|
@@ -309,7 +309,7 @@ test("Windows source installation resolves one validated Go executable and seals
|
|
|
309
309
|
const goCalls = fixture.calls.filter((call) => call.file === fixture.goPath);
|
|
310
310
|
assert.deepEqual(goCalls.map((call) => call.arguments_.slice(0, 2)), [
|
|
311
311
|
["version"],
|
|
312
|
-
["install", "github.com/gentleman-programming/gentle-ai/
|
|
312
|
+
["install", "github.com/gentleman-programming/gentle-ai/v3/cmd/gentle-ai@v3.0.1"],
|
|
313
313
|
["version", "-m"],
|
|
314
314
|
]);
|
|
315
315
|
for (const call of goCalls) {
|
|
@@ -335,14 +335,14 @@ test("Windows source manifest binds verified Go metadata and architecture", asyn
|
|
|
335
335
|
const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-provenance-"));
|
|
336
336
|
const fixture = await hardenedWindowsGoFixture(packageRoot, { architecture: "arm64" });
|
|
337
337
|
const result = await installGentleAi({ packageRoot, platform: "win32", arch: "arm64", execFile: fixture.run, resolveGoExecutable: fixture.resolveGoExecutable });
|
|
338
|
-
const manifest = JSON.parse(await readFile(join(packageRoot, ".gentle-ai", "
|
|
338
|
+
const manifest = JSON.parse(await readFile(join(packageRoot, ".gentle-ai", "v3.0.1", "integrity.json"), "utf8")) as Record<string, string>;
|
|
339
339
|
assert.equal(result.installed, true);
|
|
340
340
|
assert.deepEqual(manifest, {
|
|
341
|
-
version: "
|
|
341
|
+
version: "3.0.1",
|
|
342
342
|
method: "go-sumdb-source-build",
|
|
343
|
-
package: "github.com/gentleman-programming/gentle-ai/
|
|
344
|
-
module: "github.com/gentleman-programming/gentle-ai/
|
|
345
|
-
tag: "
|
|
343
|
+
package: "github.com/gentleman-programming/gentle-ai/v3/cmd/gentle-ai",
|
|
344
|
+
module: "github.com/gentleman-programming/gentle-ai/v3",
|
|
345
|
+
tag: "v3.0.1",
|
|
346
346
|
architecture: "arm64",
|
|
347
347
|
binarySha256: createHash("sha256").update("trusted Windows source build").digest("hex"),
|
|
348
348
|
moduleChecksum: GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM,
|
|
@@ -362,12 +362,12 @@ test("Windows source installation rejects Go metadata for a different architectu
|
|
|
362
362
|
() => installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: fixture.resolveGoExecutable }),
|
|
363
363
|
(error: unknown) => error instanceof Error && "code" in error && error.code === "GENTLE_AI_GO_INSTALL_FAILED",
|
|
364
364
|
);
|
|
365
|
-
assert.equal(existsSync(join(packageRoot, ".gentle-ai", "
|
|
365
|
+
assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v3.0.1", "gentle-ai.exe")), false);
|
|
366
366
|
});
|
|
367
367
|
|
|
368
368
|
test("Windows source installation treats a fresh ownerless lock as active", async () => {
|
|
369
369
|
const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-ownerless-lock-"));
|
|
370
|
-
const lockPath = join(packageRoot, ".gentle-ai", ".
|
|
370
|
+
const lockPath = join(packageRoot, ".gentle-ai", ".v3.0.1.install.lock");
|
|
371
371
|
await mkdir(lockPath, { recursive: true });
|
|
372
372
|
const fixture = await hardenedWindowsGoFixture(packageRoot);
|
|
373
373
|
await assertManualLockRecoveryRequired(packageRoot, fixture);
|
|
@@ -375,7 +375,7 @@ test("Windows source installation treats a fresh ownerless lock as active", asyn
|
|
|
375
375
|
|
|
376
376
|
test("Windows source installation preserves a lock whose owner nonce changed before release", async () => {
|
|
377
377
|
const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-owner-lock-"));
|
|
378
|
-
const lockOwnerPath = join(packageRoot, ".gentle-ai", ".
|
|
378
|
+
const lockOwnerPath = join(packageRoot, ".gentle-ai", ".v3.0.1.install.lock", "owner.json");
|
|
379
379
|
const fixture = await hardenedWindowsGoFixture(packageRoot);
|
|
380
380
|
let replacedOwner = false;
|
|
381
381
|
const run = async (...arguments_: Parameters<typeof fixture.run>) => {
|
|
@@ -392,7 +392,7 @@ test("Windows source installation preserves a lock whose owner nonce changed bef
|
|
|
392
392
|
});
|
|
393
393
|
|
|
394
394
|
async function assertManualLockRecoveryRequired(packageRoot: string, fixture: Awaited<ReturnType<typeof hardenedWindowsGoFixture>>, options: Record<string, unknown> = {}) {
|
|
395
|
-
const lockPath = join(packageRoot, ".gentle-ai", ".
|
|
395
|
+
const lockPath = join(packageRoot, ".gentle-ai", ".v3.0.1.install.lock");
|
|
396
396
|
await assert.rejects(
|
|
397
397
|
() => installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: fixture.resolveGoExecutable, ...options }),
|
|
398
398
|
(error: unknown) => error instanceof Error && error.message.includes(lockPath) && /confirm no installer is active.*remove.*manually/i.test(error.message),
|
|
@@ -402,18 +402,18 @@ async function assertManualLockRecoveryRequired(packageRoot: string, fixture: Aw
|
|
|
402
402
|
}
|
|
403
403
|
|
|
404
404
|
function tombstonePath(packageRoot: string, nonce: string): string {
|
|
405
|
-
return join(packageRoot, ".gentle-ai", `.
|
|
405
|
+
return join(packageRoot, ".gentle-ai", `.v3.0.1.install.tombstone-${nonce}`);
|
|
406
406
|
}
|
|
407
407
|
|
|
408
408
|
async function tombstones(packageRoot: string): Promise<string[]> {
|
|
409
409
|
const runtimeRoot = join(packageRoot, ".gentle-ai");
|
|
410
410
|
return existsSync(runtimeRoot)
|
|
411
|
-
? (await readdir(runtimeRoot)).filter((entry) => entry.startsWith(".
|
|
411
|
+
? (await readdir(runtimeRoot)).filter((entry) => entry.startsWith(".v3.0.1.install.tombstone-"))
|
|
412
412
|
: [];
|
|
413
413
|
}
|
|
414
414
|
|
|
415
415
|
function backupBundlePath(packageRoot: string, nonce: string): string {
|
|
416
|
-
return join(packageRoot, ".gentle-ai", `.
|
|
416
|
+
return join(packageRoot, ".gentle-ai", `.v3.0.1.backup-${nonce}`);
|
|
417
417
|
}
|
|
418
418
|
|
|
419
419
|
async function copyWindowsBundle(source: string, destination: string): Promise<void> {
|
|
@@ -424,7 +424,7 @@ async function copyWindowsBundle(source: string, destination: string): Promise<v
|
|
|
424
424
|
|
|
425
425
|
test("Windows source installation fails closed for an ownerless lock even after the stale threshold", async () => {
|
|
426
426
|
const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-ownerless-stale-lock-"));
|
|
427
|
-
const lockPath = join(packageRoot, ".gentle-ai", ".
|
|
427
|
+
const lockPath = join(packageRoot, ".gentle-ai", ".v3.0.1.install.lock");
|
|
428
428
|
await mkdir(lockPath, { recursive: true });
|
|
429
429
|
const fixture = await hardenedWindowsGoFixture(packageRoot);
|
|
430
430
|
await assertManualLockRecoveryRequired(packageRoot, fixture, { now: () => Date.now() + 10 * 60_000 });
|
|
@@ -432,7 +432,7 @@ test("Windows source installation fails closed for an ownerless lock even after
|
|
|
432
432
|
|
|
433
433
|
test("Windows source installation fails closed for a stale owner lock", async () => {
|
|
434
434
|
const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-stale-lock-"));
|
|
435
|
-
const lockPath = join(packageRoot, ".gentle-ai", ".
|
|
435
|
+
const lockPath = join(packageRoot, ".gentle-ai", ".v3.0.1.install.lock");
|
|
436
436
|
await mkdir(lockPath, { recursive: true });
|
|
437
437
|
await writeFile(join(lockPath, "owner.json"), `${JSON.stringify({ createdAt: 0, nonce: "0".repeat(64) })}\n`);
|
|
438
438
|
const fixture = await hardenedWindowsGoFixture(packageRoot);
|
|
@@ -484,7 +484,7 @@ test("Windows source acquisition fails closed when a tombstone appears after its
|
|
|
484
484
|
(error: unknown) => error instanceof Error && error.message.includes(foreignTombstone),
|
|
485
485
|
);
|
|
486
486
|
assert.equal(existsSync(foreignTombstone), true);
|
|
487
|
-
assert.equal(existsSync(join(packageRoot, ".gentle-ai", ".
|
|
487
|
+
assert.equal(existsSync(join(packageRoot, ".gentle-ai", ".v3.0.1.install.lock")), false);
|
|
488
488
|
assert.equal(fixture.calls.some((call) => call.arguments_[0] === "install"), false);
|
|
489
489
|
});
|
|
490
490
|
|
|
@@ -492,7 +492,7 @@ test("Windows source release deletes its matching tombstone and allows reuse", a
|
|
|
492
492
|
const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-owned-lock-release-"));
|
|
493
493
|
const fixture = await hardenedWindowsGoFixture(packageRoot);
|
|
494
494
|
await installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: fixture.resolveGoExecutable });
|
|
495
|
-
assert.equal(existsSync(join(packageRoot, ".gentle-ai", ".
|
|
495
|
+
assert.equal(existsSync(join(packageRoot, ".gentle-ai", ".v3.0.1.install.lock")), false);
|
|
496
496
|
assert.deepEqual(await tombstones(packageRoot), []);
|
|
497
497
|
const reuse = await hardenedWindowsGoFixture(packageRoot);
|
|
498
498
|
assert.equal((await installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: reuse.run, resolveGoExecutable: reuse.resolveGoExecutable })).installed, false);
|
|
@@ -502,7 +502,7 @@ test("Windows source recovers a valid backup after a crash between publication r
|
|
|
502
502
|
const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-backup-crash-"));
|
|
503
503
|
const initial = await hardenedWindowsGoFixture(packageRoot);
|
|
504
504
|
await installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: initial.run, resolveGoExecutable: initial.resolveGoExecutable });
|
|
505
|
-
const live = join(packageRoot, ".gentle-ai", "
|
|
505
|
+
const live = join(packageRoot, ".gentle-ai", "v3.0.1");
|
|
506
506
|
const backup = backupBundlePath(packageRoot, "crash");
|
|
507
507
|
await rename(live, backup);
|
|
508
508
|
const recovery = await hardenedWindowsGoFixture(packageRoot);
|
|
@@ -538,7 +538,7 @@ test("Windows source cleans one validated backup only when the live bundle is va
|
|
|
538
538
|
const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-backup-valid-live-"));
|
|
539
539
|
const initial = await hardenedWindowsGoFixture(packageRoot);
|
|
540
540
|
await installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: initial.run, resolveGoExecutable: initial.resolveGoExecutable });
|
|
541
|
-
const live = join(packageRoot, ".gentle-ai", "
|
|
541
|
+
const live = join(packageRoot, ".gentle-ai", "v3.0.1");
|
|
542
542
|
const backup = backupBundlePath(packageRoot, "valid");
|
|
543
543
|
await copyWindowsBundle(live, backup);
|
|
544
544
|
const reuse = await hardenedWindowsGoFixture(packageRoot);
|
|
@@ -551,7 +551,7 @@ test("Windows source preserves a valid backup when the live bundle is invalid",
|
|
|
551
551
|
const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-backup-invalid-live-"));
|
|
552
552
|
const initial = await hardenedWindowsGoFixture(packageRoot);
|
|
553
553
|
await installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: initial.run, resolveGoExecutable: initial.resolveGoExecutable });
|
|
554
|
-
const live = join(packageRoot, ".gentle-ai", "
|
|
554
|
+
const live = join(packageRoot, ".gentle-ai", "v3.0.1");
|
|
555
555
|
const backup = backupBundlePath(packageRoot, "valid");
|
|
556
556
|
await copyWindowsBundle(live, backup);
|
|
557
557
|
await writeFile(join(live, "integrity.json"), "{}\n");
|
|
@@ -569,7 +569,7 @@ test("Windows concurrent installs fail closed until normal release, then reuse t
|
|
|
569
569
|
const fixture = await hardenedWindowsGoFixture(packageRoot, { blockInstall: true });
|
|
570
570
|
const first = installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: fixture.resolveGoExecutable });
|
|
571
571
|
await fixture.waitForInstall();
|
|
572
|
-
const lockPath = join(packageRoot, ".gentle-ai", ".
|
|
572
|
+
const lockPath = join(packageRoot, ".gentle-ai", ".v3.0.1.install.lock");
|
|
573
573
|
await assert.rejects(
|
|
574
574
|
() => installGentleAi({ packageRoot, platform: "win32", arch: "x64", execFile: fixture.run, resolveGoExecutable: fixture.resolveGoExecutable }),
|
|
575
575
|
(error: unknown) => error instanceof Error && error.message.includes(lockPath),
|
|
@@ -583,7 +583,7 @@ test("Windows concurrent installs fail closed until normal release, then reuse t
|
|
|
583
583
|
|
|
584
584
|
test("Windows source publication rolls back a prior bundle when final directory swap fails", async () => {
|
|
585
585
|
const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-installer-rollback-"));
|
|
586
|
-
const versionDirectory = join(packageRoot, ".gentle-ai", "
|
|
586
|
+
const versionDirectory = join(packageRoot, ".gentle-ai", "v3.0.1");
|
|
587
587
|
await mkdir(versionDirectory, { recursive: true });
|
|
588
588
|
await writeFile(join(versionDirectory, "old.txt"), "previous bundle");
|
|
589
589
|
const fixture = await hardenedWindowsGoFixture(packageRoot);
|
|
@@ -620,7 +620,7 @@ test("Darwin/Linux signed bundles retain their four-field manifest and reusable
|
|
|
620
620
|
},
|
|
621
621
|
};
|
|
622
622
|
await installGentleAi(options);
|
|
623
|
-
const manifestPath = join(packageRoot, ".gentle-ai", "
|
|
623
|
+
const manifestPath = join(packageRoot, ".gentle-ai", "v3.0.1", "integrity.json");
|
|
624
624
|
const manifest = JSON.parse(await readFile(manifestPath, "utf8")) as Record<string, string>;
|
|
625
625
|
assert.deepEqual(Object.keys(manifest), ["version", "asset", "assetSha256", "binarySha256"]);
|
|
626
626
|
assert.equal((await installGentleAi({ ...options, download: async () => { throw new Error("signed bundle must be reused"); } })).installed, false);
|
|
@@ -633,7 +633,7 @@ test("a pinned asset falls back to its gentle-pi mirror only on download failure
|
|
|
633
633
|
// under the current stable pin, whose form gate refuses raw binaries.
|
|
634
634
|
const payload = Buffer.from("signed archive fixture");
|
|
635
635
|
const binary = "mirrored binary";
|
|
636
|
-
const baseAsset = { name: "gentle-
|
|
636
|
+
const baseAsset = { name: "gentle-ai_3.0.1_linux_amd64.tar.gz", sha256: createHash("sha256").update(payload).digest("hex"), binarySha256: createHash("sha256").update(binary).digest("hex"), url: "https://example.invalid/upstream", mirrorUrl: "https://example.invalid/mirror", executable: "gentle-ai" };
|
|
637
637
|
const extractArchive = async (_archive: string, destination: string) => {
|
|
638
638
|
await mkdir(destination, { recursive: true });
|
|
639
639
|
await writeFile(join(destination, "gentle-ai"), binary);
|
|
@@ -751,7 +751,7 @@ test("checksum mismatch cleans temporary state without promoting a binary", asyn
|
|
|
751
751
|
}),
|
|
752
752
|
/checksum mismatch/,
|
|
753
753
|
);
|
|
754
|
-
assert.equal(existsSync(join(packageRoot, ".gentle-ai", "
|
|
754
|
+
assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v3.0.1", "gentle-ai")), false);
|
|
755
755
|
assert.deepEqual((await readdir(packageRoot)).filter((entry) => entry.startsWith(".gentle-ai-install-")), []);
|
|
756
756
|
});
|
|
757
757
|
|
|
@@ -773,7 +773,7 @@ test("installer promotes only the expected regular executable with executable PO
|
|
|
773
773
|
await chmod(extracted, 0o700);
|
|
774
774
|
},
|
|
775
775
|
});
|
|
776
|
-
const binary = join(packageRoot, ".gentle-ai", "
|
|
776
|
+
const binary = join(packageRoot, ".gentle-ai", "v3.0.1", "gentle-ai");
|
|
777
777
|
assert.equal(existsSync(binary), true);
|
|
778
778
|
assert.equal(await readFile(binary, "utf8"), "native executable");
|
|
779
779
|
assert.ok(((await stat(binary)).mode & 0o111) !== 0);
|
|
@@ -804,7 +804,7 @@ test("installer rejects an extracted binary that differs from its pinned digest"
|
|
|
804
804
|
}),
|
|
805
805
|
/binary checksum mismatch/,
|
|
806
806
|
);
|
|
807
|
-
assert.equal(existsSync(join(packageRoot, ".gentle-ai", "
|
|
807
|
+
assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v3.0.1", "gentle-ai")), false);
|
|
808
808
|
});
|
|
809
809
|
|
|
810
810
|
test("installer repairs a valid non-executable POSIX binary instead of reusing it", async (t) => {
|
|
@@ -827,7 +827,7 @@ test("installer repairs a valid non-executable POSIX binary instead of reusing i
|
|
|
827
827
|
},
|
|
828
828
|
};
|
|
829
829
|
await installGentleAi(options);
|
|
830
|
-
const binary = join(packageRoot, ".gentle-ai", "
|
|
830
|
+
const binary = join(packageRoot, ".gentle-ai", "v3.0.1", "gentle-ai");
|
|
831
831
|
await chmod(binary, 0o600);
|
|
832
832
|
const repaired = await installGentleAi(options);
|
|
833
833
|
assert.equal(repaired.installed, true);
|
|
@@ -864,7 +864,7 @@ test("installer rejects archives with multiple expected executable entries", asy
|
|
|
864
864
|
}),
|
|
865
865
|
/exactly one regular gentle-ai/,
|
|
866
866
|
);
|
|
867
|
-
assert.equal(existsSync(join(packageRoot, ".gentle-ai", "
|
|
867
|
+
assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v3.0.1", "gentle-ai")), false);
|
|
868
868
|
});
|
|
869
869
|
|
|
870
870
|
test("installer rejects an archive without the expected regular executable", async () => {
|
|
@@ -882,7 +882,7 @@ test("installer rejects an archive without the expected regular executable", asy
|
|
|
882
882
|
}),
|
|
883
883
|
/non-regular gentle-ai/,
|
|
884
884
|
);
|
|
885
|
-
assert.equal(existsSync(join(packageRoot, ".gentle-ai", "
|
|
885
|
+
assert.equal(existsSync(join(packageRoot, ".gentle-ai", "v3.0.1", "gentle-ai")), false);
|
|
886
886
|
});
|
|
887
887
|
|
|
888
888
|
// #400: a clean Windows source build measured 232 s on a cold module cache,
|
|
@@ -219,6 +219,8 @@ test("bundled Gentleman-Cute Pi theme defines complete brand and restrained colo
|
|
|
219
219
|
assertResolvedThemeColor(theme, "error", "#FF718F");
|
|
220
220
|
assertResolvedThemeColor(theme, "muted", "#A78E9B");
|
|
221
221
|
assertResolvedThemeColor(theme, "dim", "#76616B");
|
|
222
|
+
assert.equal(colors.thinkingText, "softRose");
|
|
223
|
+
assertResolvedThemeColor(theme, "thinkingText", "#D7A0B8");
|
|
222
224
|
assertResolvedThemeColor(theme, "toolPendingBg", "#100A0F");
|
|
223
225
|
assertResolvedThemeColor(theme, "toolSuccessBg", "#151316");
|
|
224
226
|
assertResolvedThemeColor(theme, "toolErrorBg", "#261019");
|
|
@@ -246,11 +246,40 @@ test("2.9.1 repeats 2.9.0 because the negotiated lane Pi consumes is unchanged",
|
|
|
246
246
|
assert.deepEqual(contract, NATIVE_CLI_CONTRACTS["2.9.0"] as Record<string, boolean>);
|
|
247
247
|
});
|
|
248
248
|
|
|
249
|
+
test("3.0.0 repeats 2.9.1 because the negotiated lane Pi consumes is unchanged", () => {
|
|
250
|
+
// v3.0.0 shipped ODD as the orchestrator's mandatory default protocol and
|
|
251
|
+
// integrated the simplified SDD workflow into it (gentle-ai #4642, #4644),
|
|
252
|
+
// with the provider contract byte-frozen at 1.2.0. Diffing
|
|
253
|
+
// contracts/review-integration/v2 and contracts/review-provider-contract
|
|
254
|
+
// between the v2.9.1 and v3.0.0 tags in the gentle-ai source tree showed
|
|
255
|
+
// zero byte changes, so this row repeats 2.9.1. riskEvidence and hint
|
|
256
|
+
// remain dark because neither is proven to reach Pi's negotiated START
|
|
257
|
+
// path.
|
|
258
|
+
const contract = NATIVE_CLI_CONTRACTS["3.0.0"] as Record<string, boolean>;
|
|
259
|
+
assert.equal(contract.riskEvidence, false);
|
|
260
|
+
assert.equal(contract.hint, false);
|
|
261
|
+
assert.deepEqual(contract, NATIVE_CLI_CONTRACTS["2.9.1"] as Record<string, boolean>);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
test("3.0.1 repeats 3.0.0 because the negotiated lane Pi consumes is unchanged", () => {
|
|
265
|
+
// v3.0.1 moved the Go module path to
|
|
266
|
+
// github.com/gentleman-programming/gentle-ai/v3 with no contract change
|
|
267
|
+
// (gentle-ai #4683). Diffing contracts/review-integration/v2 and
|
|
268
|
+
// contracts/review-provider-contract between the v3.0.0 and v3.0.1 tags
|
|
269
|
+
// in the gentle-ai source tree showed zero byte changes, so this row
|
|
270
|
+
// repeats 3.0.0. riskEvidence and hint remain dark because neither is
|
|
271
|
+
// proven to reach Pi's negotiated START path.
|
|
272
|
+
const contract = NATIVE_CLI_CONTRACTS["3.0.1"] as Record<string, boolean>;
|
|
273
|
+
assert.equal(contract.riskEvidence, false);
|
|
274
|
+
assert.equal(contract.hint, false);
|
|
275
|
+
assert.deepEqual(contract, NATIVE_CLI_CONTRACTS["3.0.0"] as Record<string, boolean>);
|
|
276
|
+
});
|
|
277
|
+
|
|
249
278
|
test("no shipped version key was added beyond the pin bump", () => {
|
|
250
279
|
// Rows are promises to consumers, so a new key only ever appears in a
|
|
251
280
|
// dedicated commit alongside a pin bump, never as a side effect. v2.2.4 and
|
|
252
281
|
// v2.3.0 shipped upstream while Pi stayed on 2.2.3 and were never pinned,
|
|
253
282
|
// so they get no row: a row asserts ground truth measured against a binary
|
|
254
283
|
// Pi actually ran, and the table only has to be ascending, not gapless.
|
|
255
|
-
assert.deepEqual(Object.keys(NATIVE_CLI_CONTRACTS), [...DARK_VERSIONS, "2.2.0", "2.2.1", "2.2.2", "2.2.3", "2.4.0", "2.5.0-rc.3", "2.5.0", "2.6.0", "2.7.0", "2.8.0", "2.8.1", "2.8.2", "2.9.0", "2.9.1"]);
|
|
284
|
+
assert.deepEqual(Object.keys(NATIVE_CLI_CONTRACTS), [...DARK_VERSIONS, "2.2.0", "2.2.1", "2.2.2", "2.2.3", "2.4.0", "2.5.0-rc.3", "2.5.0", "2.6.0", "2.7.0", "2.8.0", "2.8.1", "2.8.2", "2.9.0", "2.9.1", "3.0.0", "3.0.1"]);
|
|
256
285
|
});
|
|
@@ -81,7 +81,11 @@ test("organic progress preserves both complete feature copies and reconciles act
|
|
|
81
81
|
"mem_get_observation",
|
|
82
82
|
"read the actual task file",
|
|
83
83
|
"not a third authority",
|
|
84
|
+
"create or rebuild the visible `todo` list",
|
|
85
|
+
"before the first source write",
|
|
86
|
+
"after every task transition and material plan change",
|
|
84
87
|
]);
|
|
88
|
+
assert.doesNotMatch(memory, /`todo` tool is an optional session\/UI projection/);
|
|
85
89
|
});
|
|
86
90
|
|
|
87
91
|
test("assumption challenge and task checks do not activate or duplicate native review", () => {
|
|
@@ -170,6 +174,7 @@ test("ODD protocol is always-on in the rendered system prompt and runs by defaul
|
|
|
170
174
|
"4. **Classify.**",
|
|
171
175
|
"two or more meaningful implementation steps",
|
|
172
176
|
"5. **Track before the first write.**",
|
|
177
|
+
"create or rebuild the visible `todo` list from the reconciled feature tasks",
|
|
173
178
|
"Tell the user in one line which feature document was created and how many tasks it holds",
|
|
174
179
|
"6. **Implement task by task.**",
|
|
175
180
|
"7. **Close.**",
|
|
@@ -309,20 +309,20 @@ test("package manifest installs pi-pretty through a wrapper without bundling nat
|
|
|
309
309
|
);
|
|
310
310
|
});
|
|
311
311
|
|
|
312
|
-
test("package verification binds the published Gentle AI
|
|
312
|
+
test("package verification binds the published Gentle AI v3.0.1 runtime pin", () => {
|
|
313
313
|
const installer = readFileSync(join(PACKAGE_ROOT, "scripts", "gentle-ai-installer.mjs"), "utf8");
|
|
314
314
|
const binary = readFileSync(join(PACKAGE_ROOT, "lib", "gentle-ai-binary.ts"), "utf8");
|
|
315
315
|
const verifier = readFileSync(join(PACKAGE_ROOT, "scripts", "verify-package-files.mjs"), "utf8");
|
|
316
316
|
|
|
317
|
-
assert.match(installer, /INSTALLER_VERSION = "
|
|
317
|
+
assert.match(installer, /INSTALLER_VERSION = "3\.0\.1"/);
|
|
318
318
|
assert.match(installer, /GENTLE_AI_WINDOWS_SOURCE_PACKAGE.*GENTLE_AI_WINDOWS_SOURCE_MODULE/);
|
|
319
|
-
assert.match(installer, /GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM = "h1:
|
|
319
|
+
assert.match(installer, /GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM = "h1:r2z8yf0CdL0ZPPkMEKI3km6SitjOIC\/DQDvtipc1Igs="/);
|
|
320
320
|
assert.match(installer, /GOTOOLCHAIN: "local"/);
|
|
321
321
|
assert.match(installer, /GOSUMDB: "sum\.golang\.org"/);
|
|
322
322
|
assert.match(binary, /GENTLE_AI_VERSION = INSTALLER_VERSION/);
|
|
323
323
|
assert.match(binary, /GO_SUMDB_SOURCE_BUILD/);
|
|
324
324
|
assert.match(binary, /GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM/);
|
|
325
|
-
assert.match(verifier, /
|
|
325
|
+
assert.match(verifier, /v3\.0\.1/);
|
|
326
326
|
});
|
|
327
327
|
|
|
328
328
|
|
|
@@ -1574,9 +1574,9 @@ test("pi-pretty wrapper uses cached ESM loading for compiled and pnpm symlink in
|
|
|
1574
1574
|
assert.match(wrapper, /quietToolsEnabled/);
|
|
1575
1575
|
});
|
|
1576
1576
|
|
|
1577
|
-
test("v3.
|
|
1577
|
+
test("Gentle Shell v3.1.1 package and runtime stop before publication", () => {
|
|
1578
1578
|
const packageJson = readPackageJson();
|
|
1579
|
-
assert.equal(packageJson.version, "3.
|
|
1579
|
+
assert.equal(packageJson.version, "3.1.1", "the release manifest must remain explicitly pinned to v3.1.1");
|
|
1580
1580
|
assert.equal(
|
|
1581
1581
|
packageJson.scripts?.test,
|
|
1582
1582
|
"node --experimental-strip-types --test tests/*.test.ts && pnpm run check:provider-contract && pnpm run test:harness",
|
package/tests/pi-pretty.test.ts
CHANGED
|
@@ -18,13 +18,18 @@ test("bundled pretty cannot replace or restore an editor owned by the host", asy
|
|
|
18
18
|
setWorkingVisible(value: boolean) { visible = value; },
|
|
19
19
|
theme: { fg: (role: string, text: string) => `<${role}>${text}</${role}>`, bold: (text: string) => text },
|
|
20
20
|
} };
|
|
21
|
+
const previousThinkingIndicator = process.env.PRETTY_THINKING_INDICATOR;
|
|
22
|
+
let thinkingIndicatorDuringActivation: string | undefined;
|
|
21
23
|
await pretty(pi, undefined, async (api: any) => {
|
|
24
|
+
thinkingIndicatorDuringActivation = process.env.PRETTY_THINKING_INDICATOR;
|
|
22
25
|
api.on("session_start", (_event: unknown, context: any) => {
|
|
23
26
|
context.ui.setEditorComponent(() => "pretty");
|
|
24
27
|
context.ui.setWorkingIndicator({ frames: ["foreign"] });
|
|
25
28
|
});
|
|
26
29
|
api.on("session_shutdown", (_event: unknown, context: any) => context.ui.setEditorComponent(undefined));
|
|
27
30
|
}, {});
|
|
31
|
+
assert.equal(thinkingIndicatorDuringActivation, "off", "Gentle Shell disables pi-pretty's global Thinking shimmer");
|
|
32
|
+
assert.equal(process.env.PRETTY_THINKING_INDICATOR, previousThinkingIndicator, "activation must restore the caller's environment");
|
|
28
33
|
for (const handler of handlers.get("session_start") ?? []) await handler({}, ctx);
|
|
29
34
|
assert.equal(editor, owner);
|
|
30
35
|
assert.equal(visible, false, "the prompt frame owns the live working state without a duplicate loader row");
|
|
@@ -1250,24 +1250,26 @@ async function run() {
|
|
|
1250
1250
|
await inputHook({ text: "vamos con sdd", source: "interactive" }, ctx),
|
|
1251
1251
|
{ action: "continue" },
|
|
1252
1252
|
);
|
|
1253
|
-
assert.equal(existsSync(join(lazySddCwd, ".pi", "agents", "sdd-apply.md")), false);
|
|
1254
|
-
assert.equal(existsSync(join(lazySddCwd, ".pi", "chains", "sdd-full.chain.md")), false);
|
|
1255
|
-
assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-apply.md")), true);
|
|
1256
|
-
assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-status.md")), true);
|
|
1257
|
-
assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-sync.md")), false);
|
|
1258
|
-
assert.equal(existsSync(join(globalAgentHome, "gentle-ai", "support", "sdd-status-contract.md")), true);
|
|
1259
|
-
assert.equal(existsSync(join(globalAgentHome, "chains", "sdd-full.chain.md")), true);
|
|
1260
|
-
assert.equal(ctx.ui.selections.length, 1, "first interactive SDD trigger confirms session suggestions");
|
|
1261
|
-
assert.match(ctx.ui.notifications.at(-1).message, /Preference source: explicit session choice/);
|
|
1262
1253
|
assert.deepEqual(
|
|
1263
1254
|
await inputHook({ text: "please use sdd for this change", source: "interactive" }, ctx),
|
|
1264
1255
|
{ action: "continue" },
|
|
1265
1256
|
);
|
|
1266
|
-
assert.equal(
|
|
1257
|
+
assert.equal(existsSync(join(lazySddCwd, ".pi", "agents", "sdd-apply.md")), false);
|
|
1258
|
+
assert.equal(existsSync(join(lazySddCwd, ".pi", "chains", "sdd-full.chain.md")), false);
|
|
1259
|
+
assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-apply.md")), false);
|
|
1260
|
+
assert.equal(ctx.ui.selections.length, 0, "natural-language SDD text has no input-hook side effect");
|
|
1261
|
+
|
|
1267
1262
|
assert.deepEqual(
|
|
1268
1263
|
await inputHook({ text: "/sdd", source: "interactive" }, ctx),
|
|
1269
1264
|
{ action: "continue" },
|
|
1270
1265
|
);
|
|
1266
|
+
assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-apply.md")), true);
|
|
1267
|
+
assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-status.md")), true);
|
|
1268
|
+
assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-sync.md")), false);
|
|
1269
|
+
assert.equal(existsSync(join(globalAgentHome, "gentle-ai", "support", "sdd-status-contract.md")), true);
|
|
1270
|
+
assert.equal(existsSync(join(globalAgentHome, "chains", "sdd-full.chain.md")), true);
|
|
1271
|
+
assert.equal(ctx.ui.selections.length, 1, "first slash SDD trigger confirms session suggestions");
|
|
1272
|
+
assert.match(ctx.ui.notifications.at(-1).message, /Preference source: explicit session choice/);
|
|
1271
1273
|
assert.deepEqual(
|
|
1272
1274
|
await inputHook({ text: "/sdd plan", source: "interactive" }, ctx),
|
|
1273
1275
|
{ action: "continue" },
|
|
@@ -75,9 +75,9 @@ test("remediation actor preserves separately authorized memory artifact tools",
|
|
|
75
75
|
|
|
76
76
|
test("R1 confirms exact canonical paths and commands; data, denial and symlinks grant nothing", async t => {
|
|
77
77
|
const { confirmRemediationScope, remediationToolAllowed } = await import("../extensions/gentle-agents.ts");
|
|
78
|
-
const { mkdtempSync, writeFileSync, symlinkSync, rmSync } = await import("node:fs");
|
|
78
|
+
const { mkdtempSync, realpathSync, writeFileSync, symlinkSync, rmSync } = await import("node:fs");
|
|
79
79
|
const { tmpdir } = await import("node:os"); const { join } = await import("node:path");
|
|
80
|
-
const cwd = mkdtempSync(join(tmpdir(), "remediation-scope-")); t.after(() => rmSync(cwd, { recursive: true, force: true }));
|
|
80
|
+
const cwd = realpathSync(mkdtempSync(join(tmpdir(), "remediation-scope-"))); t.after(() => rmSync(cwd, { recursive: true, force: true }));
|
|
81
81
|
const target = join(cwd, "allowed.ts"); writeFileSync(target, "original"); symlinkSync(target, join(cwd, "alias.ts"));
|
|
82
82
|
const candidate = { ...plan, cwd, editPaths: [target] }, native = { mode: "repo-local", workspaceRoot: cwd, allowedEditRoots: [cwd] };
|
|
83
83
|
let shown = "", confirmations = 0;
|
|
@@ -8,17 +8,12 @@ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-a
|
|
|
8
8
|
import { createGentleAiExtension } from "../extensions/gentle-ai.ts";
|
|
9
9
|
import { isSddPreflightTrigger, sddPreflightDiskPath } from "../lib/sdd-preflight.ts";
|
|
10
10
|
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
// calls, and a stall that only surfaced as the runner's inactivity timeout.
|
|
18
|
-
//
|
|
19
|
-
// An RPC child consumes the parent-rendered preflight block transported in its
|
|
20
|
-
// task context. It must never originate preflight, and the interceptor must
|
|
21
|
-
// never consume a delegated prompt on its behalf.
|
|
11
|
+
// The `input` hook is syntax-only: slash SDD commands may originate preflight
|
|
12
|
+
// in an interactive parent, while ordinary natural-language text always reaches
|
|
13
|
+
// the model. Natural-language SDD selection belongs to the parent/orchestrator;
|
|
14
|
+
// dispatch and before_agent_start gates enforce preflight at the action boundary.
|
|
15
|
+
// RPC children consume the parent-rendered preflight block transported in task
|
|
16
|
+
// context and never originate preflight.
|
|
22
17
|
|
|
23
18
|
type InputResult = { action: "continue" | "handled" };
|
|
24
19
|
type InputHandler = (event: { text?: unknown }, ctx: ExtensionContext) => Promise<InputResult>;
|
|
@@ -52,13 +47,11 @@ function ctx(overrides: Record<string, unknown>): ExtensionContext {
|
|
|
52
47
|
} as unknown as ExtensionContext;
|
|
53
48
|
}
|
|
54
49
|
|
|
55
|
-
test("
|
|
56
|
-
|
|
57
|
-
// this text they would pass for the wrong reason.
|
|
58
|
-
assert.equal(isSddPreflightTrigger(DELEGATED_SDD_TASK), true);
|
|
50
|
+
test("natural-language SDD task text bypasses the input preflight interceptor", () => {
|
|
51
|
+
assert.equal(isSddPreflightTrigger(DELEGATED_SDD_TASK), false);
|
|
59
52
|
});
|
|
60
53
|
|
|
61
|
-
test("an RPC child's
|
|
54
|
+
test("an RPC child's natural-language SDD prompt is not consumed by the input hook", async () => {
|
|
62
55
|
const input = inputHook();
|
|
63
56
|
const cwd = await mkdtemp(join(tmpdir(), "gentle-pi-rpc-input-"));
|
|
64
57
|
const notifications: string[] = [];
|
|
@@ -95,27 +88,34 @@ test("an RPC child's ordinary prompt still continues", async () => {
|
|
|
95
88
|
}
|
|
96
89
|
});
|
|
97
90
|
|
|
98
|
-
test("an interactive parent
|
|
91
|
+
test("an interactive parent's natural-language SDD request has no input-hook side effect", async () => {
|
|
99
92
|
const input = inputHook();
|
|
100
|
-
const cwd = await mkdtemp(join(tmpdir(), "gentle-pi-parent-input-"));
|
|
93
|
+
const cwd = await mkdtemp(join(tmpdir(), "gentle-pi-parent-natural-input-"));
|
|
94
|
+
try {
|
|
95
|
+
const result = await input(
|
|
96
|
+
{ text: DELEGATED_SDD_TASK },
|
|
97
|
+
ctx({ cwd, hasUI: false, sessionManager: { getSessionId: () => "sdd-preflight-natural-parent" } }),
|
|
98
|
+
);
|
|
99
|
+
assert.deepEqual(result, { action: "continue" });
|
|
100
|
+
assert.equal(existsSync(sddPreflightDiskPath(cwd)), false, "text alone must not persist preflight");
|
|
101
|
+
} finally {
|
|
102
|
+
await rm(cwd, { recursive: true, force: true });
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("an interactive parent still resolves preflight for an explicit slash SDD command", async () => {
|
|
107
|
+
const input = inputHook();
|
|
108
|
+
const cwd = await mkdtemp(join(tmpdir(), "gentle-pi-parent-slash-input-"));
|
|
101
109
|
const agentHome = await mkdtemp(join(tmpdir(), "gentle-pi-parent-agent-home-"));
|
|
102
110
|
const previousAgentHome = process.env.GENTLE_PI_AGENT_HOME;
|
|
103
111
|
process.env.GENTLE_PI_AGENT_HOME = agentHome;
|
|
104
112
|
try {
|
|
105
113
|
const result = await input(
|
|
106
|
-
{ text:
|
|
107
|
-
ctx({
|
|
108
|
-
cwd,
|
|
109
|
-
hasUI: false,
|
|
110
|
-
sessionManager: { getSessionId: () => "sdd-preflight-interactive-parent" },
|
|
111
|
-
}),
|
|
114
|
+
{ text: "/sdd-new feature" },
|
|
115
|
+
ctx({ cwd, hasUI: false, sessionManager: { getSessionId: () => "sdd-preflight-slash-parent" } }),
|
|
112
116
|
);
|
|
113
117
|
assert.deepEqual(result, { action: "continue" });
|
|
114
|
-
assert.equal(
|
|
115
|
-
existsSync(sddPreflightDiskPath(cwd)),
|
|
116
|
-
true,
|
|
117
|
-
"the parent remains the only actor that resolves and persists preflight",
|
|
118
|
-
);
|
|
118
|
+
assert.equal(existsSync(sddPreflightDiskPath(cwd)), true, "slash SDD commands still resolve preflight");
|
|
119
119
|
} finally {
|
|
120
120
|
if (previousAgentHome === undefined) delete process.env.GENTLE_PI_AGENT_HOME;
|
|
121
121
|
else process.env.GENTLE_PI_AGENT_HOME = previousAgentHome;
|
|
@@ -507,20 +507,25 @@ test("only a structurally valid parent-rendered preflight block can reach an SDD
|
|
|
507
507
|
assert.equal(isParentConfirmedSddPreflightContext("## SDD Session Preflight\ncaller-authored defaults"), false);
|
|
508
508
|
});
|
|
509
509
|
|
|
510
|
-
test("
|
|
510
|
+
test("natural-language SDD text never triggers input preflight", () => {
|
|
511
|
+
// Natural-language intent belongs to the parent/orchestrator. The input hook
|
|
512
|
+
// is syntax-only; execution gates enforce preflight when an SDD action is
|
|
513
|
+
// actually attempted.
|
|
511
514
|
for (const text of [
|
|
512
515
|
"quiero hacer un proyecto con SDD",
|
|
513
516
|
"I want to build this with SDD",
|
|
514
517
|
"por favor usemos SDD para este cambio",
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
for (const text of [
|
|
518
|
+
"do SDD for this change",
|
|
519
|
+
"haz SDD para este cambio",
|
|
520
|
+
"continue the SDD change",
|
|
519
521
|
"Should we use SDD?",
|
|
520
522
|
"no quiero usar SDD por ahora",
|
|
521
|
-
"no necesito usar SDD",
|
|
522
|
-
"I don't want to use SDD",
|
|
523
523
|
"I use SDD sometimes",
|
|
524
|
+
"necesito reportar un bug del preflight de SDD",
|
|
525
|
+
"I need to report a bug in the SDD preflight",
|
|
526
|
+
"quiero comparar SDD con ODD",
|
|
527
|
+
"I want to review the SDD proposal",
|
|
528
|
+
"necesito una explicación de SDD",
|
|
524
529
|
]) {
|
|
525
530
|
assert.equal(isSddPreflightTrigger(text), false, text);
|
|
526
531
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import { createRequire, syncBuiltinESMExports } from "node:module";
|
|
3
|
-
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
import { promisify } from "node:util";
|
|
@@ -102,7 +102,7 @@ async function captureOwnedRoutes(t: TestContext): Promise<{ captures: Capture[]
|
|
|
102
102
|
const cp = childProcess();
|
|
103
103
|
const original = { execFileSync: cp.execFileSync, execFile: cp.execFile, spawnSync: cp.spawnSync };
|
|
104
104
|
const captures: Capture[] = [];
|
|
105
|
-
const cwd = mkdtempSync(join(tmpdir(), "gentle-pi-windows-hidden-workspace-"));
|
|
105
|
+
const cwd = realpathSync(mkdtempSync(join(tmpdir(), "gentle-pi-windows-hidden-workspace-")));
|
|
106
106
|
t.after(() => rmSync(cwd, { recursive: true, force: true }));
|
|
107
107
|
mkdirSync(join(cwd, ".git"), { recursive: true });
|
|
108
108
|
const fakeSync = ((command: string, args: readonly string[], options: Options = {}) => {
|