gentle-pi 3.7.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -6
- package/assets/agents/gentle-ai-explore.md +4 -4
- package/assets/agents/gentle-ai-verify.md +6 -4
- package/assets/agents/gentle-ai-worker.md +7 -9
- package/assets/orchestrator-delegation.md +40 -41
- package/assets/orchestrator-memory.md +1 -22
- package/assets/orchestrator-skills.md +1 -1
- package/assets/orchestrator.md +12 -24
- package/assets/support/strict-tdd-verify.md +4 -266
- package/assets/support/strict-tdd.md +8 -360
- package/bin/gentle-shell.mjs +254 -29
- package/docs/delegated-verification.md +26 -1
- package/docs/gentle-agents-activity.md +24 -0
- package/docs/gentle-shell.md +92 -28
- package/docs/native-authority-architecture.md +2 -2
- package/docs/prompt-history.md +280 -0
- package/docs/readme-reference.md +112 -206
- package/docs/telemetry.md +1 -1
- package/docs/yolo-mode.md +86 -0
- package/extensions/child-context.ts +26 -0
- package/extensions/child-safety.ts +23 -0
- package/extensions/gentle-agents.ts +429 -262
- package/extensions/gentle-ai.ts +781 -683
- package/extensions/gentle-shell.ts +1375 -88
- package/extensions/gentle-stats.ts +101 -0
- package/extensions/gentle-todo.ts +18 -7
- package/extensions/history/atomic-write.ts +38 -0
- package/extensions/history/hide-prompts.ts +183 -0
- package/extensions/history/index.ts +1419 -0
- package/extensions/history/load-shared-history.ts +39 -0
- package/extensions/history/selector-helpers.ts +538 -0
- package/extensions/history/session-scan.ts +233 -0
- package/extensions/history/store.ts +1119 -0
- package/extensions/nan-provider.ts +6 -0
- package/extensions/quiet-tools.ts +179 -87
- package/extensions/resume-hint.ts +60 -0
- package/extensions/skill-registry.ts +16 -12
- package/extensions/startup-banner.ts +60 -31
- package/lib/agent-assets.ts +604 -0
- package/lib/agent-profile-pin.ts +12 -0
- package/lib/agents-message-delivery.ts +181 -0
- package/lib/agents-protocol.ts +60 -0
- package/lib/agents-runner.ts +96 -98
- package/lib/agents-view.ts +26 -3
- package/lib/agents-widget.ts +16 -9
- package/lib/append-system-prompt.ts +21 -0
- package/lib/bounded-writer-admission.ts +147 -0
- package/lib/card-style-policy.ts +60 -0
- package/lib/child-context-files.ts +166 -0
- package/lib/codemode-renderer.ts +185 -0
- package/lib/command-palette-catalog.ts +3 -9
- package/lib/command-palette.ts +25 -14
- package/lib/destructive-command-guard.ts +144 -0
- package/lib/gentle-ai-elapsed-store.ts +87 -0
- package/lib/gentle-ai-renderer.ts +196 -39
- package/lib/gentle-shell-launcher.ts +24 -13
- package/lib/gentle-shell-resume-hint.ts +176 -0
- package/lib/history-capture-policy.ts +95 -0
- package/lib/model-routing-authority.ts +5 -1
- package/lib/nan-provider.ts +227 -0
- package/lib/native-review-cli.ts +49 -108
- package/lib/odd-phase-inference.ts +231 -0
- package/lib/odd-phase.ts +141 -0
- package/lib/overlay-repaint.ts +26 -0
- package/lib/pi-tui-keys.ts +53 -0
- package/lib/review-candidate-view-owner.ts +67 -17
- package/lib/review-candidate-view.ts +112 -25
- package/lib/review-reminder-receipt.ts +48 -8
- package/lib/review-risk-assessment.ts +156 -11
- package/lib/review-sidebar-state.ts +223 -0
- package/lib/selection-engine.ts +515 -0
- package/lib/session-messaging-grants.ts +135 -0
- package/lib/session-worktree-registry.ts +14 -2
- package/lib/shell-bar.ts +179 -24
- package/lib/shell-card.ts +287 -18
- package/lib/shell-changes-view.ts +2 -1
- package/lib/shell-prompt.ts +102 -6
- package/lib/shell-sidebar-layout.ts +78 -14
- package/lib/shell-sidebar.ts +15 -1
- package/lib/shell-todo.ts +23 -13
- package/lib/shell-usage-view.ts +9 -4
- package/lib/shell-usage.ts +66 -10
- package/lib/stats-collector.ts +381 -0
- package/lib/stats-view.ts +431 -0
- package/lib/theme-customization.ts +52 -0
- package/lib/vim-editor-adapter.ts +379 -0
- package/lib/vim-normal-engine.ts +154 -0
- package/lib/vim-operator-engine.ts +416 -0
- package/lib/vim-policy.ts +49 -0
- package/lib/vim-visual-engine.ts +107 -0
- package/lib/visual-customization-policy.ts +108 -0
- package/lib/visual-customize-view.ts +330 -0
- package/lib/visual-profiles.ts +228 -0
- package/lib/yolo-session-policy.ts +240 -0
- package/package.json +20 -8
- package/runtime/gentle-shell-launcher.mjs +23 -12
- package/runtime/gentle-shell-resume-hint.mjs +177 -0
- package/runtime/native-review-cli.mjs +49 -108
- package/runtime/review-risk-assessment.mjs +154 -9
- package/scripts/build-runtime-modules.mjs +1 -0
- package/scripts/gentle-ai-installer.mjs +14 -13
- package/scripts/mirror-odd-routing.mjs +2 -2
- package/scripts/run-test-suite.mjs +76 -0
- package/scripts/test-packed-runner.mjs +31 -14
- package/scripts/verify-package-files.mjs +11 -22
- package/skills/branch-pr/SKILL.md +24 -52
- package/skills/chained-pr/SKILL.md +31 -15
- package/skills/chained-pr/references/chaining-details.md +31 -20
- package/skills/gentle-ai/SKILL.md +9 -15
- package/skills/issue-creation/SKILL.md +8 -2
- package/skills/issue-creation/references/delegated-workflow-actions.md +19 -0
- package/skills/work-unit-commits/SKILL.md +4 -3
- package/tests/agent-profiles.test.ts +18 -0
- package/tests/agents-fake-child.ts +2 -2
- package/tests/agents-message-delivery.test.ts +106 -0
- package/tests/agents-protocol.test.ts +40 -0
- package/tests/agents-runner.test.ts +378 -89
- package/tests/agents-view-thread-identity.test.ts +169 -0
- package/tests/agents-view.test.ts +8 -2
- package/tests/agents-widget.test.ts +154 -15
- package/tests/append-system-prompt-route.test.ts +160 -0
- package/tests/append-system-prompt.test.ts +46 -0
- package/tests/artifact-language.test.ts +19 -213
- package/tests/ask-user-question.test.ts +44 -1
- package/tests/asset-installation-runtime.test.ts +5 -16
- package/tests/autonomous-guard.test.ts +69 -1
- package/tests/bounded-writer-admission.test.ts +95 -0
- package/tests/branch-pr-skill.test.ts +43 -0
- package/tests/card-style-policy.test.ts +55 -0
- package/tests/chained-pr-skill.test.ts +124 -0
- package/tests/child-context-files.test.ts +255 -0
- package/tests/child-safety.test.ts +82 -0
- package/tests/codemode-rendering.test.ts +491 -0
- package/tests/command-palette.test.ts +39 -3
- package/tests/delegated-key-learnings-contract.test.ts +0 -76
- package/tests/destructive-command-guard.test.ts +84 -0
- package/tests/devbinary/native-review-parity.devtest.ts +170 -2
- package/tests/devbinary/non-git-subagent-bootstrap.devtest.ts +193 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-28T10-00-00-000Z_aaa.jsonl +7 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-29T23-00-00-000Z_bbb.jsonl +3 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-30T08-00-00-000Z_ddd.jsonl +2 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-30T09-00-00-000Z_eee.jsonl +3 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/run-1/session.jsonl +2 -0
- package/tests/fixtures/stats/sessions/--work-beta--/2026-09-01T12-00-00-000Z_ccc.jsonl +2 -0
- package/tests/fixtures/stats/user-pi/sessions/--work-alpha--/2026-09-28T10-00-00-000Z_aaa.jsonl +2 -0
- package/tests/fixtures/stats/user-pi/sessions/--work-alpha--/2026-09-29T23-00-00-000Z_bbb.jsonl +4 -0
- package/tests/fixtures/stats/user-pi/sessions/--work-gamma--/2026-09-20T09-00-00-000Z_fff.jsonl +3 -0
- package/tests/generic-agent-tools.test.ts +54 -0
- package/tests/gentle-agents.test.ts +1047 -252
- package/tests/gentle-ai-binary.test.ts +3 -3
- package/tests/gentle-ai-elapsed-store.test.ts +68 -0
- package/tests/gentle-ai-installer.test.ts +68 -54
- package/tests/gentle-ai-renderer.test.ts +487 -8
- package/tests/gentle-ai.test.ts +288 -65
- package/tests/gentle-card-text.ts +2 -1
- package/tests/gentle-shell-bin.test.ts +651 -114
- package/tests/gentle-shell-launcher.test.ts +99 -52
- package/tests/gentle-shell-resume-hint.test.ts +270 -0
- package/tests/gentle-shell.test.ts +3839 -187
- package/tests/gentle-stats.test.ts +152 -0
- package/tests/gentle-theme.test.ts +4 -1
- package/tests/gentle-todo.test.ts +80 -8
- package/tests/history-atomic-write.test.ts +57 -0
- package/tests/history-capture-policy.test.ts +102 -0
- package/tests/history-command-registration.test.ts +164 -0
- package/tests/history-dedupe-entries.test.ts +123 -0
- package/tests/history-delete-backfill.test.ts +190 -0
- package/tests/history-delete-confirm.test.ts +460 -0
- package/tests/history-dispatch.test.ts +180 -0
- package/tests/history-drain-hidden.test.ts +110 -0
- package/tests/history-drain-order.test.ts +98 -0
- package/tests/history-expanded-globals.test.ts +62 -0
- package/tests/history-gc.test.ts +832 -0
- package/tests/history-header-layout.test.ts +265 -0
- package/tests/history-hide-prompts.test.ts +275 -0
- package/tests/history-lazy-windowing.test.ts +508 -0
- package/tests/history-legacy-migrate-v2.test.ts +297 -0
- package/tests/history-load-shared-history.test.ts +53 -0
- package/tests/history-max-results-cap.test.ts +76 -0
- package/tests/history-multi-reader.test.ts +203 -0
- package/tests/history-off-path.test.ts +170 -0
- package/tests/history-openflow-integration.test.ts +173 -0
- package/tests/history-overlay-margin.test.ts +326 -0
- package/tests/history-preview-layout.test.ts +93 -0
- package/tests/history-registry.test.ts +143 -0
- package/tests/history-scope-delete.test.ts +411 -0
- package/tests/history-search-caret-keys.test.ts +142 -0
- package/tests/history-seed-bootstrap.test.ts +170 -0
- package/tests/history-seed-regen.test.ts +129 -0
- package/tests/history-selector-windowing.test.ts +94 -0
- package/tests/history-session-scan-directory.test.ts +87 -0
- package/tests/history-session-scan-extract.test.ts +583 -0
- package/tests/history-session-writer.test.ts +351 -0
- package/tests/history-store-paths.test.ts +79 -0
- package/tests/history-tombstone-exact.test.ts +139 -0
- package/tests/history-wheel-mouse.test.ts +242 -0
- package/tests/inprocess-reviewer.test.ts +29 -19
- package/tests/issue-creation-skill.test.ts +61 -0
- package/tests/model-routing-authority.test.ts +16 -0
- package/tests/nan-provider.test.ts +471 -0
- package/tests/native-review-capability-contract.test.ts +7 -1
- package/tests/native-review-cli.test.ts +6 -120
- package/tests/native-review-parity-runtime.test.ts +100 -3
- package/tests/odd-integration.test.ts +67 -0
- package/tests/odd-phase-inference.test.ts +213 -0
- package/tests/odd-phase-loader.test.ts +253 -0
- package/tests/odd-phase.test.ts +307 -0
- package/tests/odd-routing-canonical-ratchet.test.ts +11 -6
- package/tests/odd-routing-contract.test.ts +86 -35
- package/tests/orchestrator-budget.test.ts +14 -39
- package/tests/orchestrator-rdd-ownership.test.ts +3 -3
- package/tests/overlay-repaint.test.ts +74 -0
- package/tests/package-manifest.test.ts +252 -115
- package/tests/packed-runner-owned-path.test.ts +46 -0
- package/tests/persona-single-channel.test.ts +6 -6
- package/tests/provider-defect-handoff.test.ts +3 -11
- package/tests/quiet-bash-runtime.test.ts +76 -0
- package/tests/quiet-tool-rendering.test.ts +409 -184
- package/tests/rdd-aware-verification-contract.test.ts +76 -1
- package/tests/rdd-status-line.test.ts +9 -4
- package/tests/resume-hint-extension.test.ts +122 -0
- package/tests/review-agent-end-preflight.test.ts +176 -12
- package/tests/review-candidate-owner-retry.test.ts +22 -1
- package/tests/review-candidate-view.test.ts +298 -0
- package/tests/review-contract-prompt.test.ts +108 -43
- package/tests/review-controller-lock-status.test.ts +0 -1
- package/tests/review-controller-native-routing.test.ts +611 -5
- package/tests/review-controller-workspace-root.test.ts +163 -4
- package/tests/review-host-relay-routing.test.ts +338 -2
- package/tests/review-integration-v2-forward.test.ts +200 -0
- package/tests/review-ledger-contract.test.ts +10 -34
- package/tests/review-reminder-receipt.test.ts +47 -1
- package/tests/review-risk-assessment.test.ts +498 -6
- package/tests/review-sidebar-state.test.ts +402 -0
- package/tests/run-test-suite.test.ts +124 -0
- package/tests/runtime-harness.mjs +145 -786
- package/tests/runtime-metrics-children.test.ts +16 -23
- package/tests/selection-engine.test.ts +421 -0
- package/tests/session-messaging-grants.test.ts +255 -0
- package/tests/session-worktree-registry.test.ts +77 -0
- package/tests/shell-bar.test.ts +382 -1
- package/tests/shell-card.test.ts +353 -1
- package/tests/shell-changes-view.test.ts +52 -0
- package/tests/shell-prompt.test.ts +94 -2
- package/tests/shell-sidebar-layout.test.ts +325 -21
- package/tests/shell-sidebar-scroll-benchmark.test.ts +255 -0
- package/tests/shell-todo.test.ts +87 -1
- package/tests/shell-usage-view.test.ts +27 -0
- package/tests/shell-usage.test.ts +73 -0
- package/tests/skill-registry.test.ts +50 -1
- package/tests/startup-banner.test.ts +130 -2
- package/tests/stats-collector.test.ts +195 -0
- package/tests/stats-view.test.ts +202 -0
- package/tests/telemetry-trigger.test.ts +81 -20
- package/tests/theme-customization.test.ts +72 -0
- package/tests/vim-editor-adapter-host-resolution.test.ts +37 -0
- package/tests/vim-editor-adapter.test.ts +804 -0
- package/tests/vim-normal-engine.test.ts +101 -0
- package/tests/vim-operator-engine.test.ts +215 -0
- package/tests/vim-policy.test.ts +19 -0
- package/tests/vim-visual-engine.test.ts +52 -0
- package/tests/visual-customization-policy.test.ts +110 -0
- package/tests/visual-customize-view.test.ts +418 -0
- package/tests/visual-profiles.test.ts +87 -0
- package/tests/yolo-customize.test.ts +256 -0
- package/tests/yolo-mode-runtime.test.ts +161 -0
- package/tests/yolo-mode.test.ts +261 -0
- package/tests/yolo-session-policy.test.ts +59 -0
- package/themes/Gentle.json +2 -1
- package/themes/Gentleman-Cute.json +2 -1
- package/themes/Gentleman-Sexy.json +2 -1
- package/assets/agents/sdd-apply.md +0 -159
- package/assets/agents/sdd-archive.md +0 -228
- package/assets/agents/sdd-design.md +0 -49
- package/assets/agents/sdd-explore.md +0 -48
- package/assets/agents/sdd-init.md +0 -56
- package/assets/agents/sdd-onboard.md +0 -52
- package/assets/agents/sdd-proposal.md +0 -64
- package/assets/agents/sdd-remediate.md +0 -37
- package/assets/agents/sdd-research.md +0 -49
- package/assets/agents/sdd-spec.md +0 -192
- package/assets/agents/sdd-status.md +0 -54
- package/assets/agents/sdd-tasks.md +0 -108
- package/assets/agents/sdd-verify.md +0 -124
- package/assets/chains/sdd-full.chain.md +0 -83
- package/assets/chains/sdd-plan.chain.md +0 -56
- package/assets/chains/sdd-verify.chain.md +0 -43
- package/assets/sdd-orchestrator-workflow.md +0 -319
- package/assets/support/sdd-status-contract.md +0 -77
- package/docs/assets/diagrams/sdd-cycle.svg +0 -14
- package/extensions/sdd-init.ts +0 -816
- package/lib/openspec-deltas.ts +0 -156
- package/lib/sdd-preflight.ts +0 -1066
- package/lib/sdd-research-capabilities.ts +0 -94
- package/lib/sdd-status.ts +0 -26
- package/tests/fixtures/legacy/sdd-research-v2.5.0.md +0 -54
- package/tests/fixtures/native-review-cli/v2.1.3/bind-sdd.json +0 -25
- package/tests/fixtures/v0.10.7/assets/agents/sdd-apply.md +0 -132
- package/tests/openspec-deltas.test.ts +0 -209
- package/tests/sdd-agent-tools.test.ts +0 -156
- package/tests/sdd-archive-replay.test.ts +0 -82
- package/tests/sdd-classical-continuation.test.ts +0 -74
- package/tests/sdd-execution-routing-contract.test.ts +0 -44
- package/tests/sdd-managed-runtime-settlement.test.ts +0 -155
- package/tests/sdd-native-managed-uptake.test.ts +0 -243
- package/tests/sdd-no-attempts-contract.test.ts +0 -15
- package/tests/sdd-odd-integration.test.ts +0 -33
- package/tests/sdd-optional-research.test.ts +0 -124
- package/tests/sdd-planning-routing-contract.test.ts +0 -45
- package/tests/sdd-preflight-rpc-input.test.ts +0 -125
- package/tests/sdd-preflight.test.ts +0 -541
- package/tests/sdd-research-capabilities.test.ts +0 -114
- package/tests/sdd-research-live.test.ts +0 -241
- package/tests/sdd-selection-transport.test.ts +0 -653
- package/tests/sdd-status.test.ts +0 -9
- package/tests/sdd-task-truth.test.ts +0 -43
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { gentlePiConfigHome } from "./agent-home.ts";
|
|
5
|
+
|
|
6
|
+
// Prompt history capture is default-off. An explicit GENTLE_PI_HISTORY_CAPTURE
|
|
7
|
+
// value wins, then the Gentle → Customize preference, then off. Any missing,
|
|
8
|
+
// malformed or unreadable preference fails closed. Turning capture off only
|
|
9
|
+
// stops new captures; stored history is never deleted here.
|
|
10
|
+
export const HISTORY_CAPTURE_POLICY = { ON: "on", OFF: "off" } as const;
|
|
11
|
+
export type HistoryCapturePolicy = (typeof HISTORY_CAPTURE_POLICY)[keyof typeof HISTORY_CAPTURE_POLICY];
|
|
12
|
+
export const HISTORY_CAPTURE_SCHEMA = "gentle-pi.history-capture/v1";
|
|
13
|
+
export const HISTORY_CAPTURE_ENV = "GENTLE_PI_HISTORY_CAPTURE";
|
|
14
|
+
interface HistoryCaptureOptions { gentlePiConfigHome?: string }
|
|
15
|
+
export interface HistoryCapturePolicyResolution {
|
|
16
|
+
policy: HistoryCapturePolicy;
|
|
17
|
+
source: "global_file" | "default";
|
|
18
|
+
malformed: boolean;
|
|
19
|
+
globalFile: string;
|
|
20
|
+
}
|
|
21
|
+
export interface HistoryCaptureResolution {
|
|
22
|
+
enabled: boolean;
|
|
23
|
+
source: "env" | "global_file" | "default";
|
|
24
|
+
/** The persisted Customize preference, reported even while the env overrides it. */
|
|
25
|
+
preference: HistoryCapturePolicy;
|
|
26
|
+
envOverride: HistoryCapturePolicy | undefined;
|
|
27
|
+
malformed: boolean;
|
|
28
|
+
globalFile: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Only explicit on/off values override; anything else defers to the preference. */
|
|
32
|
+
export function historyCaptureEnvOverride(env: NodeJS.ProcessEnv = process.env): HistoryCapturePolicy | undefined {
|
|
33
|
+
const value = env[HISTORY_CAPTURE_ENV]?.trim().toLowerCase();
|
|
34
|
+
if (value === "1" || value === "true" || value === "on") return "on";
|
|
35
|
+
if (value === "0" || value === "false" || value === "off") return "off";
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function parseHistoryCaptureFile(raw: string): HistoryCapturePolicy | undefined {
|
|
40
|
+
try {
|
|
41
|
+
const value: unknown = JSON.parse(raw);
|
|
42
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return undefined;
|
|
43
|
+
if (Object.keys(value).length !== 2 || !("schema" in value) || value.schema !== HISTORY_CAPTURE_SCHEMA || !("policy" in value)) return undefined;
|
|
44
|
+
return value.policy === "on" || value.policy === "off" ? value.policy : undefined;
|
|
45
|
+
} catch { return undefined; }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function resolveHistoryCapturePolicy(options: HistoryCaptureOptions = {}): HistoryCapturePolicyResolution {
|
|
49
|
+
const globalFile = join(options.gentlePiConfigHome ?? gentlePiConfigHome(), "history-capture.json");
|
|
50
|
+
try {
|
|
51
|
+
const policy = parseHistoryCaptureFile(readFileSync(globalFile, "utf8"));
|
|
52
|
+
return { policy: policy ?? "off", source: "global_file", malformed: policy === undefined, globalFile };
|
|
53
|
+
} catch (error) {
|
|
54
|
+
const missing = typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
55
|
+
return { policy: "off", source: missing ? "default" : "global_file", malformed: !missing, globalFile };
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Full resolution for status surfaces; always reads the preference. */
|
|
60
|
+
export function resolveHistoryCapture(options: { env?: NodeJS.ProcessEnv; gentlePiConfigHome?: string } = {}): HistoryCaptureResolution {
|
|
61
|
+
const env = options.env ?? process.env;
|
|
62
|
+
const saved = resolveHistoryCapturePolicy({ gentlePiConfigHome: options.gentlePiConfigHome ?? gentlePiConfigHome(env) });
|
|
63
|
+
const envOverride = historyCaptureEnvOverride(env);
|
|
64
|
+
return {
|
|
65
|
+
enabled: (envOverride ?? saved.policy) === "on",
|
|
66
|
+
source: envOverride ? "env" : saved.source,
|
|
67
|
+
preference: saved.policy,
|
|
68
|
+
envOverride,
|
|
69
|
+
malformed: saved.malformed,
|
|
70
|
+
globalFile: saved.globalFile,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Per-prompt gate: an explicit env value never reads the preference file. */
|
|
75
|
+
export function historyCaptureEnabled(options: { env?: NodeJS.ProcessEnv; gentlePiConfigHome?: string } = {}): boolean {
|
|
76
|
+
const env = options.env ?? process.env;
|
|
77
|
+
const override = historyCaptureEnvOverride(env);
|
|
78
|
+
if (override) return override === "on";
|
|
79
|
+
return resolveHistoryCapturePolicy({ gentlePiConfigHome: options.gentlePiConfigHome ?? gentlePiConfigHome(env) }).policy === "on";
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function writeHistoryCapturePolicy(policy: HistoryCapturePolicy, options: HistoryCaptureOptions = {}): string {
|
|
83
|
+
if (policy !== "on" && policy !== "off") throw new TypeError("Invalid history capture policy");
|
|
84
|
+
const home = options.gentlePiConfigHome ?? gentlePiConfigHome();
|
|
85
|
+
const path = join(home, "history-capture.json");
|
|
86
|
+
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
87
|
+
mkdirSync(home, { recursive: true });
|
|
88
|
+
try {
|
|
89
|
+
writeFileSync(temporary, `${JSON.stringify({ schema: HISTORY_CAPTURE_SCHEMA, policy })}\n`, { flag: "wx", mode: 0o600 });
|
|
90
|
+
renameSync(temporary, path);
|
|
91
|
+
} finally {
|
|
92
|
+
try { unlinkSync(temporary); } catch { /* Rename consumed the temporary file. */ }
|
|
93
|
+
}
|
|
94
|
+
return path;
|
|
95
|
+
}
|
|
@@ -63,7 +63,11 @@ export function normalizeRoutingEntry(value: unknown): AgentRoutingEntry | undef
|
|
|
63
63
|
}
|
|
64
64
|
if (!isRecord(value)) return undefined;
|
|
65
65
|
const model = normalizeModelId(value.model);
|
|
66
|
-
const thinking = isThinkingLevel(value.thinking)
|
|
66
|
+
const thinking = isThinkingLevel(value.thinking)
|
|
67
|
+
? value.thinking
|
|
68
|
+
: isThinkingLevel(value.effort)
|
|
69
|
+
? value.effort
|
|
70
|
+
: undefined;
|
|
67
71
|
if (!model && !thinking) {
|
|
68
72
|
return Object.keys(value).length === 0 ? {} : undefined;
|
|
69
73
|
}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import * as piAi from "@earendil-works/pi-ai";
|
|
2
|
+
import type { Provider, ProviderStreams, RefreshModelsContext } from "@earendil-works/pi-ai";
|
|
3
|
+
import type { ProviderModelConfig } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
|
|
5
|
+
export const NAN_PROVIDER_ID = "nan";
|
|
6
|
+
export const NAN_PROVIDER_BASE_URL = "https://api.nan.builders/v1";
|
|
7
|
+
export const NAN_MODELS_TIMEOUT_MS = 3_000;
|
|
8
|
+
|
|
9
|
+
export interface NanProviderOptions {
|
|
10
|
+
fetchImpl?: typeof fetch;
|
|
11
|
+
timeoutMs?: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
type NanChatModelConfig = Extract<ProviderModelConfig, { type?: "chat" }>;
|
|
15
|
+
|
|
16
|
+
// Pi requires numeric rates; NaN access is quota-based, so zero avoids inventing per-token pricing.
|
|
17
|
+
const ZERO_COST = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
18
|
+
|
|
19
|
+
// Maintained chat subset from https://nan.builders/docs/models.
|
|
20
|
+
// Decimal bounds conservatively interpret the documented 1M/262K/131K labels.
|
|
21
|
+
// Pi models text/image inputs only; documented audio/video inputs are not advertised.
|
|
22
|
+
// Reasoning shares max_tokens with the answer: 65,536 leaves answer room after a
|
|
23
|
+
// 32,768 reasoning budget; DeepSeek uses NaN's 16,384 floor. Qwen 3.8 retains its
|
|
24
|
+
// published 131K output maximum. The remaining 32,768 caps are configured, not NaN-published limits.
|
|
25
|
+
// Pi requires explicit max mappings to offer that level; missing ordinary levels pass through.
|
|
26
|
+
const FIXED_THINKING_LEVEL_MAP: NanChatModelConfig["thinkingLevelMap"] = {
|
|
27
|
+
off: null, minimal: null, low: null, high: null, xhigh: null, max: null,
|
|
28
|
+
}; // Only medium remains usable: depth is fixed and reasoning cannot be disabled.
|
|
29
|
+
|
|
30
|
+
const CHAT_MODELS: NanChatModelConfig[] = ([
|
|
31
|
+
{
|
|
32
|
+
id: "glm5.3", name: "GLM 5.3", input: ["text", "image"], contextWindow: 1_000_000,
|
|
33
|
+
thinkingLevelMap: { off: null, minimal: "low", xhigh: "max", max: "max" },
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
id: "deepseek-v4-flash", name: "DeepSeek V4 Flash", input: ["text", "image"], contextWindow: 1_000_000,
|
|
37
|
+
thinkingLevelMap: FIXED_THINKING_LEVEL_MAP, maxTokens: 16_384,
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
id: "glm5.3-flash", name: "GLM 5.3 Flash", input: ["text", "image"], contextWindow: 1_000_000,
|
|
41
|
+
thinkingLevelMap: { off: null, minimal: "low", xhigh: "max", max: "max" },
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
id: "qwen3.8-flash", name: "Qwen 3.8 Flash", input: ["text", "image"], contextWindow: 1_048_576,
|
|
45
|
+
thinkingLevelMap: FIXED_THINKING_LEVEL_MAP, maxTokens: 131_000,
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
id: "mimo-v2.6-flash", name: "MiMo V2.6 Flash", input: ["text", "image"], contextWindow: 1_000_000,
|
|
49
|
+
thinkingLevelMap: FIXED_THINKING_LEVEL_MAP,
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
id: "gemma4", name: "Gemma 4", input: ["text", "image"], contextWindow: 262_000,
|
|
53
|
+
thinkingLevelMap: { off: "none", xhigh: "max", max: "max" }, maxTokens: 65_536,
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
id: "qwen3.6", name: "Qwen 3.6", input: ["text", "image"], contextWindow: 262_000,
|
|
57
|
+
thinkingLevelMap: { off: "none", xhigh: "max", max: "max" }, maxTokens: 65_536,
|
|
58
|
+
},
|
|
59
|
+
] satisfies Partial<NanChatModelConfig>[]).map((model) => ({
|
|
60
|
+
...model,
|
|
61
|
+
api: "openai-completions",
|
|
62
|
+
reasoning: true,
|
|
63
|
+
cost: ZERO_COST,
|
|
64
|
+
maxTokens: model.maxTokens ?? 32_768,
|
|
65
|
+
}));
|
|
66
|
+
|
|
67
|
+
// The cold/offline baseline declares documented chat support, not key entitlement.
|
|
68
|
+
// A successful live catalog remains authoritative for the credential that fetched it.
|
|
69
|
+
const OFFLINE_MODELS = CHAT_MODELS;
|
|
70
|
+
|
|
71
|
+
function cloneModel(model: NanChatModelConfig): NanChatModelConfig {
|
|
72
|
+
return {
|
|
73
|
+
...model,
|
|
74
|
+
input: [...model.input],
|
|
75
|
+
cost: { ...model.cost },
|
|
76
|
+
thinkingLevelMap: model.thinkingLevelMap ? { ...model.thinkingLevelMap } : undefined,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function knownChatModels(ids: readonly string[]): NanChatModelConfig[] {
|
|
81
|
+
return ids.flatMap((id) => {
|
|
82
|
+
const known = CHAT_MODELS.find((model) => model.id === id);
|
|
83
|
+
return known ? [cloneModel(known)] : [];
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
88
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Returns undefined on an unusable response; an empty data array is authoritative. */
|
|
92
|
+
async function fetchLiveModelIds(options: {
|
|
93
|
+
apiKey?: string;
|
|
94
|
+
signal: AbortSignal;
|
|
95
|
+
fetchImpl: typeof fetch;
|
|
96
|
+
timeoutMs: number;
|
|
97
|
+
}): Promise<string[] | undefined> {
|
|
98
|
+
const { apiKey, signal, fetchImpl, timeoutMs } = options;
|
|
99
|
+
if (signal.aborted) return undefined;
|
|
100
|
+
|
|
101
|
+
const controller = new AbortController();
|
|
102
|
+
const abort = () => controller.abort(signal.reason);
|
|
103
|
+
if (signal.aborted) abort();
|
|
104
|
+
else signal.addEventListener("abort", abort, { once: true });
|
|
105
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
const headers: Record<string, string> = { Accept: "application/json" };
|
|
109
|
+
if (apiKey) headers.Authorization = `Bearer ${apiKey}`;
|
|
110
|
+
const response = await fetchImpl(`${NAN_PROVIDER_BASE_URL}/models`, {
|
|
111
|
+
method: "GET",
|
|
112
|
+
headers,
|
|
113
|
+
signal: controller.signal,
|
|
114
|
+
redirect: "error",
|
|
115
|
+
cache: "no-store",
|
|
116
|
+
});
|
|
117
|
+
if (!response.ok) return undefined;
|
|
118
|
+
|
|
119
|
+
const payload: unknown = await response.json();
|
|
120
|
+
if (!isRecord(payload) || !Array.isArray(payload.data)) return undefined;
|
|
121
|
+
if (payload.data.length === 0) return [];
|
|
122
|
+
|
|
123
|
+
const ids = new Set<string>();
|
|
124
|
+
for (const row of payload.data) {
|
|
125
|
+
if (!isRecord(row) || typeof row.id !== "string") continue;
|
|
126
|
+
const id = row.id.trim();
|
|
127
|
+
if (id) ids.add(id);
|
|
128
|
+
}
|
|
129
|
+
return ids.size > 0 ? [...ids] : undefined;
|
|
130
|
+
} catch {
|
|
131
|
+
// Discovery is best-effort. Never log request or response data: it may contain credentials.
|
|
132
|
+
return undefined;
|
|
133
|
+
} finally {
|
|
134
|
+
clearTimeout(timeout);
|
|
135
|
+
signal.removeEventListener("abort", abort);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function cloneCatalog(models: readonly NanChatModelConfig[]): NanChatModelConfig[] {
|
|
140
|
+
return models.map(cloneModel);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Native auth belongs to the provider; Pi persists only a successful login result. */
|
|
144
|
+
export function createNanProviderConfig(options: NanProviderOptions = {}): Provider<"openai-completions"> {
|
|
145
|
+
const config = createCatalogConfig(options);
|
|
146
|
+
// Pi's extension loader aliases the bare root to compat, which exposes this
|
|
147
|
+
// host-owned lazy API factory. Do not import an SDK implementation subpath.
|
|
148
|
+
const api = piAi.lazyApi(async () => {
|
|
149
|
+
const runtime = piAi as typeof piAi & { openAICompletionsApi?: () => ProviderStreams };
|
|
150
|
+
if (!runtime.openAICompletionsApi) throw new Error("NaN requires Pi's OpenAI completions API factory");
|
|
151
|
+
return runtime.openAICompletionsApi();
|
|
152
|
+
});
|
|
153
|
+
return {
|
|
154
|
+
id: NAN_PROVIDER_ID,
|
|
155
|
+
name: "NaN",
|
|
156
|
+
baseUrl: NAN_PROVIDER_BASE_URL,
|
|
157
|
+
auth: { apiKey: {
|
|
158
|
+
name: "NaN API key",
|
|
159
|
+
async login(interaction) {
|
|
160
|
+
interaction.signal.throwIfAborted();
|
|
161
|
+
const entered = await interaction.prompt({
|
|
162
|
+
type: "secret", message: "Enter API key", signal: interaction.signal,
|
|
163
|
+
});
|
|
164
|
+
interaction.signal.throwIfAborted();
|
|
165
|
+
const key = entered.trim();
|
|
166
|
+
if (!key) throw new Error("NaN requires a non-empty API key");
|
|
167
|
+
return { type: "api_key", key };
|
|
168
|
+
},
|
|
169
|
+
async resolve({ ctx, credential, signal }) {
|
|
170
|
+
signal.throwIfAborted();
|
|
171
|
+
const stored = credential?.key?.trim();
|
|
172
|
+
const key = stored || (await ctx.env("NAN_API_KEY"))?.trim();
|
|
173
|
+
signal.throwIfAborted();
|
|
174
|
+
return key ? { auth: { apiKey: key }, source: stored ? "API key" : "NAN_API_KEY" } : undefined;
|
|
175
|
+
},
|
|
176
|
+
} },
|
|
177
|
+
getModels: () => config.getModels().map((model) => ({
|
|
178
|
+
...cloneModel(model), provider: NAN_PROVIDER_ID,
|
|
179
|
+
baseUrl: NAN_PROVIDER_BASE_URL, api: "openai-completions" as const,
|
|
180
|
+
})),
|
|
181
|
+
refreshModels: (context) => config.refreshModels(context),
|
|
182
|
+
stream: api.stream,
|
|
183
|
+
streamSimple: api.streamSimple,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function createCatalogConfig(options: NanProviderOptions = {}): {
|
|
188
|
+
getModels(): NanChatModelConfig[];
|
|
189
|
+
refreshModels(context: RefreshModelsContext): Promise<void>;
|
|
190
|
+
} {
|
|
191
|
+
let catalog = cloneCatalog(OFFLINE_MODELS);
|
|
192
|
+
let catalogKey: string | undefined;
|
|
193
|
+
let credentialRevision = 0;
|
|
194
|
+
const fetchImpl = options.fetchImpl ?? globalThis.fetch;
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
// One source of truth lets Pi snapshot the new catalog inside publish.update.
|
|
198
|
+
getModels: () => cloneCatalog(catalog),
|
|
199
|
+
refreshModels: async (context: RefreshModelsContext) => {
|
|
200
|
+
const apiKey = context.credential?.type === "api_key" ? context.credential.key : undefined;
|
|
201
|
+
if (apiKey !== catalogKey) {
|
|
202
|
+
// A live catalog is authoritative only for the credential that discovered it.
|
|
203
|
+
catalogKey = apiKey;
|
|
204
|
+
credentialRevision++;
|
|
205
|
+
catalog = cloneCatalog(OFFLINE_MODELS);
|
|
206
|
+
}
|
|
207
|
+
const revision = credentialRevision;
|
|
208
|
+
if (!context.allowNetwork || context.signal.aborted || typeof fetchImpl !== "function") {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const ids = await fetchLiveModelIds({
|
|
213
|
+
apiKey,
|
|
214
|
+
signal: context.signal,
|
|
215
|
+
fetchImpl,
|
|
216
|
+
timeoutMs: options.timeoutMs ?? NAN_MODELS_TIMEOUT_MS,
|
|
217
|
+
});
|
|
218
|
+
if (revision !== credentialRevision || ids === undefined || context.signal.aborted) {
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
await context.publish({ update: () => {
|
|
223
|
+
if (revision === credentialRevision && !context.signal.aborted) catalog = knownChatModels(ids);
|
|
224
|
+
} });
|
|
225
|
+
},
|
|
226
|
+
};
|
|
227
|
+
}
|
package/lib/native-review-cli.ts
CHANGED
|
@@ -67,8 +67,6 @@ export const NATIVE_REVIEW_OPERATION = {
|
|
|
67
67
|
CAPTURE_PROVIDER_ROLE: "review/capture-provider-role",
|
|
68
68
|
CAPTURE_UNACHIEVABLE: "review/capture-unachievable",
|
|
69
69
|
ACKNOWLEDGE_APPROVED: "review/acknowledge-approved",
|
|
70
|
-
SDD_STATUS: "sdd-status",
|
|
71
|
-
SDD_CONTINUE: "sdd-continue",
|
|
72
70
|
} as const;
|
|
73
71
|
export type NativeReviewOperation = (typeof NATIVE_REVIEW_OPERATION)[keyof typeof NATIVE_REVIEW_OPERATION];
|
|
74
72
|
|
|
@@ -121,11 +119,6 @@ export interface NativeReviewCli {
|
|
|
121
119
|
// an older binary without the verb, or any other process/decode failure,
|
|
122
120
|
// rejects the returned promise -- callers fail closed to `high` risk.
|
|
123
121
|
assess?(request: NativeReviewAssessRequest): Promise<ReviewAssessmentV1>;
|
|
124
|
-
// Exact native SDD readiness authority for an explicitly selected phase.
|
|
125
|
-
// This is intentionally separate from review/RDD gates and carries no
|
|
126
|
-
// delivery or review-transaction authority.
|
|
127
|
-
sddStatus?(request: NativeSddStatusRequest): Promise<NativeSddStatusV2>;
|
|
128
|
-
sddContinue?(request: NativeSddStatusRequest): Promise<NativeSddStatusV2>;
|
|
129
122
|
}
|
|
130
123
|
|
|
131
124
|
export const NATIVE_REVIEW_MODE_OPERATION = {
|
|
@@ -162,35 +155,6 @@ export const NATIVE_REVIEW_MODE_SCOPE = {
|
|
|
162
155
|
} as const;
|
|
163
156
|
export type NativeReviewModeScope = (typeof NATIVE_REVIEW_MODE_SCOPE)[keyof typeof NATIVE_REVIEW_MODE_SCOPE];
|
|
164
157
|
|
|
165
|
-
export interface NativeSddStatusRequest {
|
|
166
|
-
changeName?: string;
|
|
167
|
-
workspaceRoot: string;
|
|
168
|
-
signal?: AbortSignal;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
export type NativeSddPhase = "apply" | "verify" | "archive";
|
|
172
|
-
export type NativeSddDependencyState = "blocked" | "ready" | "all_done";
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* The native CLI owns this complete v2 record. The decoder validates the fields
|
|
176
|
-
* Pi relies on and returns the original object without adding, omitting, or
|
|
177
|
-
* reconciling local SDD state.
|
|
178
|
-
*/
|
|
179
|
-
export interface NativeSddStatusV2 extends Readonly<Record<string, unknown>> {
|
|
180
|
-
schemaName: "gentle-ai.sdd-status";
|
|
181
|
-
schemaVersion: 2;
|
|
182
|
-
changeName: string | null;
|
|
183
|
-
artifactStore: "openspec" | "engram" | "hybrid" | "none";
|
|
184
|
-
planningHome: Readonly<Record<string, unknown>> & { mode: "repo-local"; path: string };
|
|
185
|
-
changeRoot: string | null;
|
|
186
|
-
actionContext: Readonly<Record<string, unknown>> & { mode: "repo-local"; workspaceRoot: string; allowedEditRoots: readonly string[] };
|
|
187
|
-
dependencies: Readonly<Record<(typeof NATIVE_SDD_DEPENDENCIES)[number], NativeSddDependencyState>>;
|
|
188
|
-
phaseInstructions?: Readonly<Record<(typeof NATIVE_SDD_INSTRUCTION_PHASES)[number], readonly string[]> & { remediate?: readonly string[] }>;
|
|
189
|
-
blockedReasons: readonly string[];
|
|
190
|
-
nextRecommended: string;
|
|
191
|
-
remediationState?: { required: boolean; complete: boolean; failedEvidenceRevision: string };
|
|
192
|
-
}
|
|
193
|
-
|
|
194
158
|
export interface NativeReviewModeRequest {
|
|
195
159
|
cwd: string;
|
|
196
160
|
operation: NativeReviewModeOperation;
|
|
@@ -747,6 +711,21 @@ function nativeUntrackedSelectionArguments(selection: NativeUntrackedSelection):
|
|
|
747
711
|
];
|
|
748
712
|
}
|
|
749
713
|
|
|
714
|
+
// Fail closed on malformed or duplicate provider selectors before invoking STATUS.
|
|
715
|
+
function providerFlagValue(tokens: readonly string[], flag: string): string | undefined {
|
|
716
|
+
let found: string | undefined;
|
|
717
|
+
for (let index = 0; index < tokens.length; index++) {
|
|
718
|
+
const token = tokens[index]!;
|
|
719
|
+
if (token !== `--${flag}` && !token.startsWith(`--${flag}=`)) continue;
|
|
720
|
+
if (found !== undefined) throw new TypeError(`Native intended-untracked selection repeats --${flag}`);
|
|
721
|
+
const value = token === `--${flag}` ? tokens[index + 1] : token.slice(`--${flag}=`.length);
|
|
722
|
+
if (value === undefined || value === "" || value.startsWith("--")) throw new TypeError(`Native intended-untracked selection has an invalid --${flag} value`);
|
|
723
|
+
found = value;
|
|
724
|
+
if (token === `--${flag}`) index++;
|
|
725
|
+
}
|
|
726
|
+
return found;
|
|
727
|
+
}
|
|
728
|
+
|
|
750
729
|
const NATIVE_RISK_LEVEL = ["low", "medium", "high"] as const;
|
|
751
730
|
|
|
752
731
|
// gentle-ai's negotiated `start/v2` envelope is a closed schema
|
|
@@ -1048,6 +1027,13 @@ export const NATIVE_CLI_CONTRACTS = Object.freeze({
|
|
|
1048
1027
|
// 547b68e172cc87aa297309d61624e5fc2c24d407a494b53eeb5a2b053904352c
|
|
1049
1028
|
// at contract 1.2.0. No new negotiated capability is asserted.
|
|
1050
1029
|
"3.7.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 }),
|
|
1030
|
+
// v4.0.0 repeats 3.7.0: the published provider-contract tar remains SHA-256
|
|
1031
|
+
// 547b68e172cc87aa297309d61624e5fc2c24d407a494b53eeb5a2b053904352c
|
|
1032
|
+
// at contract 1.2.0, and the published binary still advertises
|
|
1033
|
+
// capabilities/v2.6 under review-integration/v2. The Go module path moved
|
|
1034
|
+
// to /v4 without a review-integration/v2 change. No new negotiated
|
|
1035
|
+
// capability is asserted.
|
|
1036
|
+
"4.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 }),
|
|
1051
1037
|
});
|
|
1052
1038
|
|
|
1053
1039
|
export interface NativeReviewProcessDiagnostics {
|
|
@@ -1515,52 +1501,6 @@ interface NativeJsonExecution {
|
|
|
1515
1501
|
process: ExecFileResult;
|
|
1516
1502
|
}
|
|
1517
1503
|
|
|
1518
|
-
const NATIVE_SDD_DEPENDENCIES = ["proposal", "specs", "design", "tasks", "apply", "verify", "archive"] as const;
|
|
1519
|
-
const NATIVE_SDD_INSTRUCTION_PHASES = ["apply", "verify", "archive"] as const;
|
|
1520
|
-
const NATIVE_SDD_NEXT_RECOMMENDATIONS = ["apply", "verify", "remediate", "archive", "archived", "resolve-blockers", "sdd-new", "select-change", "propose", "spec", "design", "tasks"] as const;
|
|
1521
|
-
const NATIVE_SDD_DEPENDENCY_STATES = ["blocked", "ready", "all_done"] as const;
|
|
1522
|
-
|
|
1523
|
-
/** Strictly validates the native v2 contract while preserving its whole record. */
|
|
1524
|
-
export function decodeNativeSddStatusV2(value: unknown, request: Pick<NativeSddStatusRequest, "changeName" | "workspaceRoot">): NativeSddStatusV2 {
|
|
1525
|
-
const status = object(value);
|
|
1526
|
-
if (status.schemaName !== "gentle-ai.sdd-status" || status.schemaVersion !== 2) throw new Error("wrong native SDD status schema");
|
|
1527
|
-
if ((request.changeName !== undefined && status.changeName !== request.changeName) || (status.changeName !== null && !isCanonicalProcessString(status.changeName))) throw new Error("native SDD status change identity mismatch");
|
|
1528
|
-
const artifactStore = enumString(status.artifactStore, ["openspec", "engram", "hybrid", "none"]);
|
|
1529
|
-
const planningHome = object(status.planningHome);
|
|
1530
|
-
if (planningHome.mode !== "repo-local" || !isCanonicalProcessString(planningHome.path)) throw new Error("invalid native SDD planning home");
|
|
1531
|
-
const expectedOpenSpecHome = join(request.workspaceRoot, "openspec");
|
|
1532
|
-
if (planningHome.path !== expectedOpenSpecHome && !((artifactStore === "engram" || artifactStore === "hybrid") && planningHome.path === "engram:sdd")) throw new Error("native SDD planning home escaped its workspace");
|
|
1533
|
-
if (status.changeRoot !== null && !isCanonicalProcessString(status.changeRoot)) throw new Error("invalid native SDD change root");
|
|
1534
|
-
const actionContext = object(status.actionContext);
|
|
1535
|
-
if (actionContext.mode !== "repo-local" || actionContext.workspaceRoot !== request.workspaceRoot || !isCanonicalProcessString(actionContext.workspaceRoot)) throw new Error("native SDD status workspace root mismatch");
|
|
1536
|
-
const allowedEditRoots = stringArray(actionContext.allowedEditRoots);
|
|
1537
|
-
if (!allowedEditRoots.includes(request.workspaceRoot) || allowedEditRoots.some((root) => !isAbsolute(root) || root !== join(root))) throw new Error("invalid native SDD allowed edit roots");
|
|
1538
|
-
const dependencies = object(status.dependencies);
|
|
1539
|
-
for (const phase of NATIVE_SDD_DEPENDENCIES) {
|
|
1540
|
-
if (enumString(dependencies[phase], NATIVE_SDD_DEPENDENCY_STATES) !== dependencies[phase]) throw new Error("invalid native SDD dependency");
|
|
1541
|
-
}
|
|
1542
|
-
if (Object.keys(dependencies).length !== NATIVE_SDD_DEPENDENCIES.length) throw new Error("native SDD dependencies have an unsupported shape");
|
|
1543
|
-
if (status.instructions !== undefined) throw new Error("native SDD status uses phaseInstructions, not instructions");
|
|
1544
|
-
if (status.phaseInstructions !== undefined) {
|
|
1545
|
-
const instructions = object(status.phaseInstructions);
|
|
1546
|
-
for (const phase of NATIVE_SDD_INSTRUCTION_PHASES) stringArray(instructions[phase]);
|
|
1547
|
-
// Classical SDD no longer emits a remediation phase. Keep the published
|
|
1548
|
-
// producer's optional legacy instructions intact without inventing them
|
|
1549
|
-
// for a newer producer or accepting unknown phase keys.
|
|
1550
|
-
const hasRemediation = Object.hasOwn(instructions, "remediate");
|
|
1551
|
-
if (hasRemediation) stringArray(instructions.remediate);
|
|
1552
|
-
if (Object.keys(instructions).length !== NATIVE_SDD_INSTRUCTION_PHASES.length + Number(hasRemediation)) throw new Error("native SDD instructions have an unsupported shape");
|
|
1553
|
-
if (status.nextRecommended === "remediate" && !hasRemediation) throw new Error("native SDD remediation instructions are missing");
|
|
1554
|
-
}
|
|
1555
|
-
if (status.nextRecommended === "remediate" || status.remediationState !== undefined) {
|
|
1556
|
-
const remediation = object(status.remediationState);
|
|
1557
|
-
if (typeof remediation.required !== "boolean" || typeof remediation.complete !== "boolean" || typeof remediation.failedEvidenceRevision !== "string" || (remediation.failedEvidenceRevision !== "" && !/^sha256:[0-9a-f]{64}$/.test(remediation.failedEvidenceRevision)) || (status.nextRecommended === "remediate" && (!remediation.required || remediation.complete || !remediation.failedEvidenceRevision))) throw new Error("Invalid native remediation state");
|
|
1558
|
-
}
|
|
1559
|
-
stringArray(status.blockedReasons);
|
|
1560
|
-
enumString(status.nextRecommended, NATIVE_SDD_NEXT_RECOMMENDATIONS);
|
|
1561
|
-
return status as NativeSddStatusV2;
|
|
1562
|
-
}
|
|
1563
|
-
|
|
1564
1504
|
class NativeReviewPlainCli {
|
|
1565
1505
|
private readonly adapter: ExecFileAdapter;
|
|
1566
1506
|
private readonly executable: string | (() => string);
|
|
@@ -2199,30 +2139,6 @@ export class NativeReviewCliV216 implements NativeReviewCli {
|
|
|
2199
2139
|
return this.invoke(operation, cwd, arguments_, mutating, signal, this.executablePath(operation, mutating), toleratedStderr);
|
|
2200
2140
|
}
|
|
2201
2141
|
|
|
2202
|
-
async sddStatus(request: NativeSddStatusRequest): Promise<NativeSddStatusV2> {
|
|
2203
|
-
return this.sddProjection(request, false);
|
|
2204
|
-
}
|
|
2205
|
-
|
|
2206
|
-
async sddContinue(request: NativeSddStatusRequest): Promise<NativeSddStatusV2> {
|
|
2207
|
-
if (!isCanonicalProcessString(request.changeName)) throw new TypeError("Native SDD continuation requires an exact selected change");
|
|
2208
|
-
return this.sddProjection(request, true);
|
|
2209
|
-
}
|
|
2210
|
-
|
|
2211
|
-
private async sddProjection(request: NativeSddStatusRequest, mutating: boolean): Promise<NativeSddStatusV2> {
|
|
2212
|
-
if ((request.changeName !== undefined && !isCanonicalProcessString(request.changeName)) || !isCanonicalProcessString(request.workspaceRoot) || !isAbsolute(request.workspaceRoot)) {
|
|
2213
|
-
throw new TypeError("Native SDD status requires a canonical change and absolute workspace root");
|
|
2214
|
-
}
|
|
2215
|
-
const operation = mutating ? NATIVE_REVIEW_OPERATION.SDD_CONTINUE : NATIVE_REVIEW_OPERATION.SDD_STATUS;
|
|
2216
|
-
const execution = await this.negotiated(
|
|
2217
|
-
operation,
|
|
2218
|
-
request.workspaceRoot,
|
|
2219
|
-
[operation, ...(request.changeName === undefined ? [] : [request.changeName]), "--cwd", request.workspaceRoot, "--json", "--instructions"],
|
|
2220
|
-
mutating,
|
|
2221
|
-
request.signal,
|
|
2222
|
-
);
|
|
2223
|
-
return decode(operation, mutating, () => decodeNativeSddStatusV2(execution.body, request));
|
|
2224
|
-
}
|
|
2225
|
-
|
|
2226
2142
|
async start(request: NativeStartRequest): Promise<NativeStartResult> {
|
|
2227
2143
|
if (request.baseRef !== undefined && !isCanonicalProcessString(request.baseRef)) throw new TypeError("Native START baseRef must be a non-empty, trimmed, NUL-free string");
|
|
2228
2144
|
if (request.baseRef !== undefined && request.committedOnly !== true) throw new TypeError("Native START baseRef requires explicit committedOnly acknowledgement");
|
|
@@ -2345,7 +2261,32 @@ export class NativeReviewCliV216 implements NativeReviewCli {
|
|
|
2345
2261
|
const selection = nativeUntrackedSelection(request);
|
|
2346
2262
|
const submitted = request.intendedUntrackedSelection;
|
|
2347
2263
|
if (submitted !== undefined && submitted.argumentTokens.reduce((count, token) => count + token.split("{{value}}").length - 1, 0) !== 1) throw new TypeError("Native intended-untracked selection requires exactly one provider-issued {{value}} token");
|
|
2348
|
-
const
|
|
2264
|
+
const submittedTokens = submitted === undefined ? undefined : submitted.argumentTokens.map((token) => token.replaceAll("{{value}}", submitted.value));
|
|
2265
|
+
// gentle-ai 3.7.1 submission argument_tokens never carry --base-ref /
|
|
2266
|
+
// --committed-only / --lineage, so a forwarded selector must be appended
|
|
2267
|
+
// after them; only bail if the provider already staked out a conflicting
|
|
2268
|
+
// value for the same flag. providerFlagValue also fails closed on a
|
|
2269
|
+
// dangling `--flag` (last token, no value) or an empty `--flag=`.
|
|
2270
|
+
const submittedBaseRefValue = submittedTokens === undefined ? undefined : providerFlagValue(submittedTokens, "base-ref");
|
|
2271
|
+
if (request.baseRef !== undefined && submittedBaseRefValue !== undefined && submittedBaseRefValue !== request.baseRef) throw new TypeError("Native intended-untracked selection already carries a conflicting base-ref");
|
|
2272
|
+
const submittedLineageValue = submittedTokens === undefined ? undefined : providerFlagValue(submittedTokens, "lineage");
|
|
2273
|
+
if (request.lineageId !== undefined && submittedLineageValue !== undefined && submittedLineageValue !== request.lineageId) throw new TypeError("Native intended-untracked selection already carries a conflicting lineage");
|
|
2274
|
+
let submittedHasCommittedOnly = false;
|
|
2275
|
+
for (const token of submittedTokens ?? []) {
|
|
2276
|
+
if (token !== "--committed-only" && !token.startsWith("--committed-only=")) continue;
|
|
2277
|
+
if (token !== "--committed-only" && token !== "--committed-only=true") throw new TypeError("Native intended-untracked selection has an invalid --committed-only value");
|
|
2278
|
+
if (submittedHasCommittedOnly) throw new TypeError("Native intended-untracked selection repeats --committed-only");
|
|
2279
|
+
submittedHasCommittedOnly = true;
|
|
2280
|
+
}
|
|
2281
|
+
// Append only the half of the base-ref/committed-only pair the provider
|
|
2282
|
+
// did not already carry (a present base-ref already equals
|
|
2283
|
+
// request.baseRef; the throw above ruled out the conflicting case).
|
|
2284
|
+
const forwardedBaseRef = request.baseRef === undefined ? [] : [
|
|
2285
|
+
...(submittedBaseRefValue === undefined ? ["--base-ref", request.baseRef] : []),
|
|
2286
|
+
...(submittedHasCommittedOnly ? [] : ["--committed-only"]),
|
|
2287
|
+
];
|
|
2288
|
+
const forwardedLineage = request.lineageId === undefined || submittedLineageValue !== undefined ? [] : ["--lineage", request.lineageId];
|
|
2289
|
+
const statusArguments = submittedTokens === undefined ? [
|
|
2349
2290
|
"review", "status", "--contract", REVIEW_INTEGRATION_CONTRACT, "--cwd", request.cwd,
|
|
2350
2291
|
"--projection", request.projection ?? "workspace",
|
|
2351
2292
|
...nativeUntrackedSelectionArguments(selection),
|
|
@@ -2353,7 +2294,7 @@ export class NativeReviewCliV216 implements NativeReviewCli {
|
|
|
2353
2294
|
...(request.lineageId === undefined ? [] : ["--lineage", request.lineageId]),
|
|
2354
2295
|
...(request.agent === undefined ? [] : ["--agent", request.agent]),
|
|
2355
2296
|
"--next-transition",
|
|
2356
|
-
] : ["review", "status", "--cwd", request.cwd, ...
|
|
2297
|
+
] : ["review", "status", "--cwd", request.cwd, ...submittedTokens, ...forwardedBaseRef, ...forwardedLineage];
|
|
2357
2298
|
const execution = await this.negotiated(NATIVE_REVIEW_OPERATION.STATUS, request.cwd, statusArguments, false, request.signal);
|
|
2358
2299
|
assertSupportedNextTransitionOperation(execution.body);
|
|
2359
2300
|
return decode(NATIVE_REVIEW_OPERATION.STATUS, false, () => decodeReviewStatusV3(execution.body));
|