gentle-pi 3.1.0 → 3.2.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/assets/orchestrator-delegation.md +6 -4
- 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 +43 -18
- package/extensions/gentle-agents.ts +22 -1
- package/extensions/gentle-ai.ts +33 -138
- package/extensions/pi-pretty.ts +13 -1
- package/lib/background-subagents-policy.ts +148 -0
- package/lib/native-review-cli.ts +9 -0
- package/lib/review-candidate-view.ts +27 -6
- package/lib/sdd-preflight.ts +5 -19
- package/package.json +1 -1
- package/runtime/native-review-cli.mjs +9 -0
- package/scripts/gentle-ai-installer.mjs +10 -10
- package/scripts/verify-package-files.mjs +2 -2
- package/skills/chained-pr/SKILL.md +2 -1
- package/skills/work-unit-commits/SKILL.md +9 -0
- package/tests/background-subagents-default-mode.test.ts +105 -0
- package/tests/gentle-agents.test.ts +14 -1
- package/tests/gentle-ai-binary.test.ts +1 -1
- package/tests/gentle-ai-installer.test.ts +47 -47
- package/tests/gentle-theme.test.ts +2 -0
- package/tests/native-review-capability-contract.test.ts +14 -1
- package/tests/odd-routing-contract.test.ts +49 -2
- package/tests/package-manifest.test.ts +6 -6
- package/tests/pi-pretty.test.ts +5 -0
- package/tests/review-base-ref-hint.test.ts +39 -0
- package/tests/review-candidate-view.test.ts +55 -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
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { gentlePiConfigHome } from "./agent-home.ts";
|
|
4
|
+
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// Background subagents policy — project > global > env > default off
|
|
7
|
+
//
|
|
8
|
+
// Pure resolver, extracted from extensions/gentle-ai.ts so the runtime side
|
|
9
|
+
// (extensions/gentle-agents.ts) can read the effective policy without
|
|
10
|
+
// importing the pi extension surface. No pi imports belong in this file.
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
|
|
13
|
+
export type BackgroundSubagentsPolicy = "on" | "off";
|
|
14
|
+
|
|
15
|
+
/** Which of the four sources decided the effective policy. */
|
|
16
|
+
export type BackgroundSubagentsSource =
|
|
17
|
+
| "project_file"
|
|
18
|
+
| "global_file"
|
|
19
|
+
| "environment"
|
|
20
|
+
| "default";
|
|
21
|
+
|
|
22
|
+
export interface BackgroundSubagentsResolution {
|
|
23
|
+
policy: BackgroundSubagentsPolicy;
|
|
24
|
+
source: BackgroundSubagentsSource;
|
|
25
|
+
/** The deciding file was present but failed the strict decode. */
|
|
26
|
+
malformed: boolean;
|
|
27
|
+
projectFile: string;
|
|
28
|
+
globalFile: string;
|
|
29
|
+
projectFileExists: boolean;
|
|
30
|
+
globalFileExists: boolean;
|
|
31
|
+
/** The raw env value, reported even when it is unrecognized and inert. */
|
|
32
|
+
envValue: string | undefined;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface LoadBackgroundSubagentsOptions {
|
|
36
|
+
/** Override the config home directory (used in tests to avoid touching ~/.pi). */
|
|
37
|
+
gentlePiConfigHome?: string;
|
|
38
|
+
/** Override the environment lookup (used in tests). */
|
|
39
|
+
env?: Record<string, string | undefined>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const BACKGROUND_SUBAGENTS_SCHEMA = "gentle-pi.background-subagents/v1";
|
|
43
|
+
export const BACKGROUND_SUBAGENTS_FILE = "background-subagents.json";
|
|
44
|
+
|
|
45
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
46
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Strict decode of {"schema":"gentle-pi.background-subagents/v1","policy":"on"|"off"}.
|
|
51
|
+
* Any malformed shape (bad JSON, wrong schema, unknown keys, invalid policy)
|
|
52
|
+
* returns undefined so the caller fails closed to "off".
|
|
53
|
+
*/
|
|
54
|
+
export function parseBackgroundSubagentsPolicyFile(
|
|
55
|
+
raw: string,
|
|
56
|
+
): BackgroundSubagentsPolicy | undefined {
|
|
57
|
+
let parsed: unknown;
|
|
58
|
+
try {
|
|
59
|
+
parsed = JSON.parse(raw);
|
|
60
|
+
} catch {
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
if (!isRecord(parsed)) return undefined;
|
|
64
|
+
if (parsed.schema !== BACKGROUND_SUBAGENTS_SCHEMA) return undefined;
|
|
65
|
+
if (parsed.policy !== "on" && parsed.policy !== "off") return undefined;
|
|
66
|
+
if (Object.keys(parsed).length !== 2) return undefined;
|
|
67
|
+
return parsed.policy;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Resolve the background-subagents policy AND the source that decided it.
|
|
72
|
+
*
|
|
73
|
+
* Resolution order (first hit wins, mirroring loadRuntimeGuardrailsConfig):
|
|
74
|
+
* 1. Project file `${cwd}/.pi/gentle-ai/background-subagents.json`
|
|
75
|
+
* 2. Global file `${configHome}/background-subagents.json`
|
|
76
|
+
* (configHome honors GENTLE_PI_CONFIG_HOME, default ~/.pi/gentle-ai)
|
|
77
|
+
* 3. Env var GENTLE_PI_BACKGROUND_SUBAGENTS ("on" | "off")
|
|
78
|
+
* 4. Default "off"
|
|
79
|
+
*
|
|
80
|
+
* A present-but-malformed file fails closed to "off" instead of falling
|
|
81
|
+
* through to a lower-priority source, and it stays attributed to that file:
|
|
82
|
+
* "off decided by a broken project file" and "off by default" are different
|
|
83
|
+
* situations, and only the first one is a mistake to fix.
|
|
84
|
+
*
|
|
85
|
+
* Four sources with first-hit-wins is exactly the shape that makes an edit
|
|
86
|
+
* look like it did nothing, so the deciding source is part of the result
|
|
87
|
+
* rather than something a caller has to re-derive.
|
|
88
|
+
*/
|
|
89
|
+
export function resolveBackgroundSubagentsPolicy(
|
|
90
|
+
cwd: string,
|
|
91
|
+
options: LoadBackgroundSubagentsOptions = {},
|
|
92
|
+
): BackgroundSubagentsResolution {
|
|
93
|
+
const env = options.env ?? process.env;
|
|
94
|
+
const envValue = env.GENTLE_PI_BACKGROUND_SUBAGENTS;
|
|
95
|
+
let projectFile = "";
|
|
96
|
+
let globalFile = "";
|
|
97
|
+
try {
|
|
98
|
+
const configHome = options.gentlePiConfigHome ?? gentlePiConfigHome();
|
|
99
|
+
projectFile = join(cwd, ".pi", "gentle-ai", BACKGROUND_SUBAGENTS_FILE);
|
|
100
|
+
globalFile = join(configHome, BACKGROUND_SUBAGENTS_FILE);
|
|
101
|
+
const projectFileExists = existsSync(projectFile);
|
|
102
|
+
const globalFileExists = existsSync(globalFile);
|
|
103
|
+
const locations = { projectFile, globalFile, projectFileExists, globalFileExists, envValue };
|
|
104
|
+
for (const [source, path, present] of [
|
|
105
|
+
["project_file", projectFile, projectFileExists],
|
|
106
|
+
["global_file", globalFile, globalFileExists],
|
|
107
|
+
] as const) {
|
|
108
|
+
if (!present) continue;
|
|
109
|
+
let decoded: BackgroundSubagentsPolicy | undefined;
|
|
110
|
+
try {
|
|
111
|
+
decoded = parseBackgroundSubagentsPolicyFile(readFileSync(path, "utf8"));
|
|
112
|
+
} catch {
|
|
113
|
+
// Unreadable is indistinguishable from unusable at this layer, and
|
|
114
|
+
// both must fail closed on the file that claimed the decision.
|
|
115
|
+
decoded = undefined;
|
|
116
|
+
}
|
|
117
|
+
return decoded === undefined
|
|
118
|
+
? { policy: "off", source, malformed: true, ...locations }
|
|
119
|
+
: { policy: decoded, source, malformed: false, ...locations };
|
|
120
|
+
}
|
|
121
|
+
if (envValue === "on" || envValue === "off") {
|
|
122
|
+
return { policy: envValue, source: "environment", malformed: false, ...locations };
|
|
123
|
+
}
|
|
124
|
+
return { policy: "off", source: "default", malformed: false, ...locations };
|
|
125
|
+
} catch {
|
|
126
|
+
return {
|
|
127
|
+
policy: "off",
|
|
128
|
+
source: "default",
|
|
129
|
+
malformed: false,
|
|
130
|
+
projectFile,
|
|
131
|
+
globalFile,
|
|
132
|
+
projectFileExists: false,
|
|
133
|
+
globalFileExists: false,
|
|
134
|
+
envValue,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* The effective policy alone, for callers that do not report a source.
|
|
141
|
+
* It delegates so the loader and the resolver can never disagree.
|
|
142
|
+
*/
|
|
143
|
+
export function loadBackgroundSubagentsPolicy(
|
|
144
|
+
cwd: string,
|
|
145
|
+
options: LoadBackgroundSubagentsOptions = {},
|
|
146
|
+
): BackgroundSubagentsPolicy {
|
|
147
|
+
return resolveBackgroundSubagentsPolicy(cwd, options).policy;
|
|
148
|
+
}
|
package/lib/native-review-cli.ts
CHANGED
|
@@ -986,6 +986,15 @@ export const NATIVE_CLI_CONTRACTS = Object.freeze({
|
|
|
986
986
|
// and hint remain dark because neither is proven to reach the negotiated
|
|
987
987
|
// START path Pi consumes.
|
|
988
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 }),
|
|
989
|
+
// v3.1.0 changed the ODD orchestrator contract only (gentle-ai #4714).
|
|
990
|
+
// Ground-truthed by diffing contracts/review-integration/v2 and
|
|
991
|
+
// contracts/review-provider-contract between the v3.0.2 and v3.1.0 tags
|
|
992
|
+
// in the gentle-ai source tree: zero bytes changed (provider contract
|
|
993
|
+
// stays 1.2.0). Neither change touches the closed START/STATUS fields
|
|
994
|
+
// this row negotiates, so it repeats 3.0.1 exactly. riskEvidence and hint
|
|
995
|
+
// remain dark because neither is proven to reach the negotiated START
|
|
996
|
+
// path Pi consumes.
|
|
997
|
+
"3.1.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 }),
|
|
989
998
|
});
|
|
990
999
|
|
|
991
1000
|
export interface NativeReviewProcessDiagnostics {
|
|
@@ -659,6 +659,8 @@ function isFullCommitId(selector: string): boolean {
|
|
|
659
659
|
return /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/i.test(selector);
|
|
660
660
|
}
|
|
661
661
|
|
|
662
|
+
export const BASE_REF_ACCEPTED_FORMS = "HEAD, a full 40- or 64-character commit id, or a ref name (branch, tag, remote, or refs/...); abbreviated commit ids are not accepted";
|
|
663
|
+
|
|
662
664
|
function explicitBaseRefCandidates(cwd: string, selector: string, env: NodeJS.ProcessEnv, executor: CandidateGitExecutor): string[] {
|
|
663
665
|
if (selector === "HEAD" || isFullCommitId(selector)) return [selector];
|
|
664
666
|
const refs = new Set(git(cwd, ["for-each-ref", "--format=%(refname)"], env, executor).split("\n").filter((ref) => ref.length > 0));
|
|
@@ -719,8 +721,27 @@ function resolveEmptyTree(cwd: string, env: NodeJS.ProcessEnv, executor: Candida
|
|
|
719
721
|
return git(cwd, ["mktree"], env, executor);
|
|
720
722
|
}
|
|
721
723
|
|
|
724
|
+
// Resolves `<selector>^{commit}` to a commit id. For an explicit (caller-
|
|
725
|
+
// supplied) baseRef, a nonzero exit is reported as base-ref-unresolvable with
|
|
726
|
+
// the accepted forms rather than as a generic git failure: the selector may
|
|
727
|
+
// be well-formed (a full commit id, a resolvable ref) and still not name a
|
|
728
|
+
// commit, e.g. a full hex id that names a tree object. Timeouts and output-
|
|
729
|
+
// limit failures still propagate as real git failures through
|
|
730
|
+
// probeCandidateGit, the same way isUnbornSymbolicHead handles them. The
|
|
731
|
+
// implicit default (HEAD, no baseRef) keeps the original fail-closed
|
|
732
|
+
// git-failure behavior, since HEAD not resolving signals repository
|
|
733
|
+
// corruption rather than a caller mistake.
|
|
734
|
+
function resolveCommitSelector(cwd: string, selector: string, env: NodeJS.ProcessEnv, executor: CandidateGitExecutor, explicit: boolean): string {
|
|
735
|
+
const arguments_ = ["rev-parse", "--verify", "--end-of-options", `${selector}^{commit}`];
|
|
736
|
+
if (!explicit) return git(cwd, arguments_, env, executor);
|
|
737
|
+
const probe = probeCandidateGit(cwd, arguments_, env, executor);
|
|
738
|
+
if (probe.status !== 0) throw new CandidateViewError(`candidate base reference is unresolvable; it does not name a commit; accepted forms: ${BASE_REF_ACCEPTED_FORMS}`, "base-ref-unresolvable");
|
|
739
|
+
return probe.stdout;
|
|
740
|
+
}
|
|
741
|
+
|
|
722
742
|
function resolveCandidateBase(cwd: string, baseRef: string | undefined, env: NodeJS.ProcessEnv, executor: CandidateGitExecutor): ResolvedCandidateBase {
|
|
723
743
|
const selector = baseRef ?? "HEAD";
|
|
744
|
+
const explicit = baseRef !== undefined;
|
|
724
745
|
// An unborn repository has a symbolic HEAD pointing at a branch with no
|
|
725
746
|
// commits yet. Its review base is Git's repository-native empty tree, not a
|
|
726
747
|
// missing or malformed commit. Only the default/HEAD selector is entitled to
|
|
@@ -729,21 +750,21 @@ function resolveCandidateBase(cwd: string, baseRef: string | undefined, env: Nod
|
|
|
729
750
|
return { commit: "HEAD", tree: resolveEmptyTree(cwd, env, executor) };
|
|
730
751
|
}
|
|
731
752
|
try {
|
|
732
|
-
if (
|
|
753
|
+
if (explicit) {
|
|
733
754
|
const candidates = explicitBaseRefCandidates(cwd, selector, env, executor);
|
|
734
|
-
if (candidates.length > 1) throw new CandidateViewError(
|
|
735
|
-
if (candidates.length === 0) throw new CandidateViewError(
|
|
755
|
+
if (candidates.length > 1) throw new CandidateViewError(`candidate base reference is ambiguous (matches more than one ref); accepted forms: ${BASE_REF_ACCEPTED_FORMS}`, "base-ref-ambiguous");
|
|
756
|
+
if (candidates.length === 0) throw new CandidateViewError(`candidate base reference is unresolvable; accepted forms: ${BASE_REF_ACCEPTED_FORMS}`, "base-ref-unresolvable");
|
|
736
757
|
}
|
|
737
|
-
const firstCommit =
|
|
758
|
+
const firstCommit = resolveCommitSelector(cwd, selector, env, executor, explicit);
|
|
738
759
|
const tree = git(cwd, ["rev-parse", "--verify", "--end-of-options", `${firstCommit}^{tree}`], env, executor);
|
|
739
|
-
const confirmedCommit =
|
|
760
|
+
const confirmedCommit = resolveCommitSelector(cwd, selector, env, executor, explicit);
|
|
740
761
|
if (firstCommit !== confirmedCommit) throw new CandidateViewError("candidate base reference moved during resolution", "base-ref-moved");
|
|
741
762
|
const confirmedTree = git(cwd, ["rev-parse", "--verify", "--end-of-options", `${confirmedCommit}^{tree}`], env, executor);
|
|
742
763
|
if (tree !== confirmedTree) throw new CandidateViewError("candidate base tree changed during resolution", "base-ref-moved");
|
|
743
764
|
return { commit: confirmedCommit, tree: confirmedTree };
|
|
744
765
|
} catch (error) {
|
|
745
766
|
if (error instanceof CandidateViewError && (error.diagnostics !== undefined || error.reason === "base-ref-ambiguous" || error.reason === "base-ref-moved" || error.reason === "base-ref-unresolvable")) throw error;
|
|
746
|
-
throw new CandidateViewError(
|
|
767
|
+
throw new CandidateViewError(`candidate base reference is unresolvable; accepted forms: ${BASE_REF_ACCEPTED_FORMS}`, "base-ref-unresolvable");
|
|
747
768
|
}
|
|
748
769
|
}
|
|
749
770
|
|
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.2.0",
|
|
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",
|
|
@@ -987,6 +987,15 @@ export const NATIVE_CLI_CONTRACTS = Object.freeze({
|
|
|
987
987
|
// and hint remain dark because neither is proven to reach the negotiated
|
|
988
988
|
// START path Pi consumes.
|
|
989
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 }),
|
|
990
|
+
// v3.1.0 changed the ODD orchestrator contract only (gentle-ai #4714).
|
|
991
|
+
// Ground-truthed by diffing contracts/review-integration/v2 and
|
|
992
|
+
// contracts/review-provider-contract between the v3.0.2 and v3.1.0 tags
|
|
993
|
+
// in the gentle-ai source tree: zero bytes changed (provider contract
|
|
994
|
+
// stays 1.2.0). Neither change touches the closed START/STATUS fields
|
|
995
|
+
// this row negotiates, so it repeats 3.0.1 exactly. riskEvidence and hint
|
|
996
|
+
// remain dark because neither is proven to reach the negotiated START
|
|
997
|
+
// path Pi consumes.
|
|
998
|
+
"3.1.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 }),
|
|
990
999
|
});
|
|
991
1000
|
|
|
992
1001
|
|
|
@@ -36,7 +36,7 @@ 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 = "3.0
|
|
39
|
+
export const INSTALLER_VERSION = "3.1.0";
|
|
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",
|
|
@@ -45,10 +45,10 @@ export const GENTLE_AI_INSTALL_METHOD = Object.freeze({
|
|
|
45
45
|
export const GENTLE_AI_WINDOWS_SOURCE_PACKAGE_PATH = "github.com/gentleman-programming/gentle-ai/v3/cmd/gentle-ai";
|
|
46
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/v3@v3.0
|
|
48
|
+
// `go mod download -json github.com/gentleman-programming/gentle-ai/v3@v3.1.0`
|
|
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 cfc415ce, the published v3.1.0 release head.
|
|
51
|
+
export const GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM = "h1:CrZlui5N8/RSmxvn/7usKe6q1EvRt7y6dyfEwjstmJw=";
|
|
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 v3.0
|
|
70
|
+
// still holds it. The v3.1.0 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 v3.0
|
|
112
|
+
// distributing unsigned Windows builds in c4b764d0, so v3.1.0 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-ai_3.
|
|
118
|
-
"darwin/arm64": asset("gentle-ai_3.
|
|
119
|
-
"linux/amd64": asset("gentle-ai_3.
|
|
120
|
-
"linux/arm64": asset("gentle-ai_3.
|
|
117
|
+
"darwin/amd64": asset("gentle-ai_3.1.0_darwin_amd64.tar.gz", "613f0e11adeebb421daae4c68cb9f207f55c559a70595549ff25f988559226e4", "98340df0102825072431a2c0373ea4d9db1bacafced234f51fb58661ed3d731f", "gentle-ai"),
|
|
118
|
+
"darwin/arm64": asset("gentle-ai_3.1.0_darwin_arm64.tar.gz", "bfcbf8df2682fcf1535b26c604e8dbb445df0ca00a651de204fdc2d013dfe472", "3cdc9689ea0d71186b896341b4181e2df13a82b64d236a26a3273171150d802f", "gentle-ai"),
|
|
119
|
+
"linux/amd64": asset("gentle-ai_3.1.0_linux_amd64.tar.gz", "dc55c44a2eb46212a38eca0dfd4d778481ec37e765f40d5a0752d03c28e1ee49", "70e335d25809a0d358c12f48b2f0d1da00741e725584ceeb8c1318c60d0a6e9e", "gentle-ai"),
|
|
120
|
+
"linux/arm64": asset("gentle-ai_3.1.0_linux_arm64.tar.gz", "3a89d5f5a549004cc2b01949014ed59f9c28e1ff0c9958531bb539504286e407", "6cf9f20fc390b13e2b9b427ca1c2df404d1bbb9248201f9a592c7f0a37ef5416", "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 v3.0
|
|
342
|
+
console.error("gentle-pi packaged review-integration/v1 and review-integration/v2 contract bytes drifted from the pinned v3.1.0 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 v3.0
|
|
387
|
+
console.log(`gentle-pi package resource check passed (${requiredPaths.length} files; ${Object.keys(contractHashes).length} exact byte-pinned contract artifacts for the v3.1.0 runtime).`);
|
|
388
388
|
}
|
|
389
389
|
|
|
390
390
|
const isMainModule = process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href;
|
|
@@ -9,7 +9,7 @@ metadata:
|
|
|
9
9
|
|
|
10
10
|
## Activation Contract
|
|
11
11
|
|
|
12
|
-
Load this skill when a planned PR may exceed **400 changed lines**, SDD forecasts `400-line budget risk: High` or `Chained PRs recommended: Yes`, or the user asks for chained/stacked PRs, review slices, or reviewer-load control.
|
|
12
|
+
Load this skill when a planned PR may exceed **400 changed lines**, SDD forecasts `400-line budget risk: High` or `Chained PRs recommended: Yes`, an ODD feature's forecast or running authored changed-line count from work-unit commits exceeds about 400, or the user asks for chained/stacked PRs, review slices, or reviewer-load control.
|
|
13
13
|
|
|
14
14
|
## Hard Rules
|
|
15
15
|
|
|
@@ -34,6 +34,7 @@ Load this skill when a planned PR may exceed **400 changed lines**, SDD forecast
|
|
|
34
34
|
| Generated/vendor/migration diff cannot split cleanly | Ask maintainer for `size:exception`. |
|
|
35
35
|
| No cohesive split fits the budget after one slicing pass | Stop; deliver the best split, report the overage and why it cannot shrink further, and recommend `size:exception`. |
|
|
36
36
|
| SDD provides `delivery_strategy` | Follow it before apply/PR creation. |
|
|
37
|
+
| ODD provides `delivery_strategy` and `chain_strategy` | Follow them before the next work-unit commit or PR creation. |
|
|
37
38
|
|
|
38
39
|
## Execution Steps
|
|
39
40
|
|
|
@@ -18,6 +18,7 @@ Use it for:
|
|
|
18
18
|
- Turning a large change into chained or stacked PRs.
|
|
19
19
|
- Keeping reviewer cognitive load healthy.
|
|
20
20
|
- Applying SDD tasks without accidentally producing a PR above 400 changed lines.
|
|
21
|
+
- Closing an ODD task with a work-unit commit.
|
|
21
22
|
|
|
22
23
|
## Critical Rules
|
|
23
24
|
|
|
@@ -76,6 +77,14 @@ Each SDD work unit should map cleanly to a commit or PR with:
|
|
|
76
77
|
- verification in the same unit,
|
|
77
78
|
- rollback that does not remove unrelated work.
|
|
78
79
|
|
|
80
|
+
## ODD Relationship
|
|
81
|
+
|
|
82
|
+
Every ODD task closes with at least one work-unit commit:
|
|
83
|
+
|
|
84
|
+
- The native review candidate is that commit, or the PR slice it belongs to when review is deferred, evaluated against the previous reviewed boundary.
|
|
85
|
+
- The running authored line count from work-unit commits feeds the same delivery-strategy vocabulary as SDD.
|
|
86
|
+
- The feature document records the commit identity and, once a delivery strategy applies, the slice boundaries.
|
|
87
|
+
|
|
79
88
|
## Commands
|
|
80
89
|
|
|
81
90
|
```bash
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { AGENT_MODE } from "../lib/agents-config.ts";
|
|
4
|
+
import { resolveDefaultSubagentMode } from "../extensions/gentle-agents.ts";
|
|
5
|
+
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// resolveDefaultSubagentMode: the runtime default for subagent_run when
|
|
8
|
+
// neither an explicit params.mode nor an agent-defined mode was given.
|
|
9
|
+
//
|
|
10
|
+
// Background is a runtime default only when the background-subagents policy
|
|
11
|
+
// is "on" AND the parent can receive background results. Print mode exits
|
|
12
|
+
// before a parent session exists to deliver a background result to, so it
|
|
13
|
+
// must keep the configured default even when the policy is on (see the
|
|
14
|
+
// `ctx.mode === "print"` guard in gentle-agents.ts `launch`).
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
test("policy on + interactive parent -> background", () => {
|
|
18
|
+
assert.equal(
|
|
19
|
+
resolveDefaultSubagentMode({
|
|
20
|
+
configuredDefault: AGENT_MODE.TASK,
|
|
21
|
+
policy: "on",
|
|
22
|
+
parentMode: "interactive",
|
|
23
|
+
}),
|
|
24
|
+
AGENT_MODE.BACKGROUND,
|
|
25
|
+
);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("policy on + rpc parent -> background", () => {
|
|
29
|
+
assert.equal(
|
|
30
|
+
resolveDefaultSubagentMode({
|
|
31
|
+
configuredDefault: AGENT_MODE.TASK,
|
|
32
|
+
policy: "on",
|
|
33
|
+
parentMode: "rpc",
|
|
34
|
+
}),
|
|
35
|
+
AGENT_MODE.BACKGROUND,
|
|
36
|
+
);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("policy on + print parent -> configured default (task), never background", () => {
|
|
40
|
+
assert.equal(
|
|
41
|
+
resolveDefaultSubagentMode({
|
|
42
|
+
configuredDefault: AGENT_MODE.TASK,
|
|
43
|
+
policy: "on",
|
|
44
|
+
parentMode: "print",
|
|
45
|
+
}),
|
|
46
|
+
AGENT_MODE.TASK,
|
|
47
|
+
);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("policy off -> configured default regardless of parent mode", () => {
|
|
51
|
+
for (const parentMode of ["interactive", "rpc", "print", undefined]) {
|
|
52
|
+
assert.equal(
|
|
53
|
+
resolveDefaultSubagentMode({
|
|
54
|
+
configuredDefault: AGENT_MODE.TASK,
|
|
55
|
+
policy: "off",
|
|
56
|
+
parentMode,
|
|
57
|
+
}),
|
|
58
|
+
AGENT_MODE.TASK,
|
|
59
|
+
`parentMode ${String(parentMode)} must not change an off policy`,
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("configured default background + policy off -> background (the configured default wins when the policy is off)", () => {
|
|
65
|
+
assert.equal(
|
|
66
|
+
resolveDefaultSubagentMode({
|
|
67
|
+
configuredDefault: AGENT_MODE.BACKGROUND,
|
|
68
|
+
policy: "off",
|
|
69
|
+
parentMode: "interactive",
|
|
70
|
+
}),
|
|
71
|
+
AGENT_MODE.BACKGROUND,
|
|
72
|
+
);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("configured default background + policy on + interactive -> background (both agree)", () => {
|
|
76
|
+
assert.equal(
|
|
77
|
+
resolveDefaultSubagentMode({
|
|
78
|
+
configuredDefault: AGENT_MODE.BACKGROUND,
|
|
79
|
+
policy: "on",
|
|
80
|
+
parentMode: "interactive",
|
|
81
|
+
}),
|
|
82
|
+
AGENT_MODE.BACKGROUND,
|
|
83
|
+
);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("parentMode undefined with policy on is treated as not print -> background", () => {
|
|
87
|
+
assert.equal(
|
|
88
|
+
resolveDefaultSubagentMode({
|
|
89
|
+
configuredDefault: AGENT_MODE.TASK,
|
|
90
|
+
policy: "on",
|
|
91
|
+
parentMode: undefined,
|
|
92
|
+
}),
|
|
93
|
+
AGENT_MODE.BACKGROUND,
|
|
94
|
+
);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// Explicit request mode always wins over the resolved default: this is
|
|
98
|
+
// asserted at the wiring site (extensions/gentle-agents.ts `run` and
|
|
99
|
+
// `continue` tools), not inside this pure helper, which has no notion of
|
|
100
|
+
// "explicit" at all — it is called only when params.mode and agent.mode are
|
|
101
|
+
// both absent. tests/agents-integration.test.ts does not currently exercise
|
|
102
|
+
// subagent_run with a fake child process, so that wiring-level assertion is
|
|
103
|
+
// not covered by an automated test in this change; it is covered by reading
|
|
104
|
+
// the call site, where `params.mode ?? agent.mode ?? resolveDefaultSubagentMode(...)`
|
|
105
|
+
// short-circuits on any explicit value before this helper is ever invoked.
|
|
@@ -83,9 +83,22 @@ const root = realpathSync(mkdtempSync(join(tmpdir(), "gentle-agents-ext-")));
|
|
|
83
83
|
const activeSessionTeardowns = new Set<() => Promise<void>>();
|
|
84
84
|
const stopActiveSessions = () => Promise.all([...activeSessionTeardowns].map((shutdown) => shutdown()));
|
|
85
85
|
afterEach(stopActiveSessions);
|
|
86
|
+
// subagent_run's default mode now reads the background-subagents policy
|
|
87
|
+
// in-process (gentle-pi#background-subagents-default-mode), which falls
|
|
88
|
+
// back to the real ~/.pi/gentle-ai/background-subagents.json when
|
|
89
|
+
// GENTLE_PI_CONFIG_HOME is unset. Point it at an empty scratch directory so
|
|
90
|
+
// this file's expectations never depend on the developer's own global
|
|
91
|
+
// policy file (a real "on" file on the runner's machine would otherwise
|
|
92
|
+
// flip every unrelated fixture's default mode to background).
|
|
93
|
+
const previousGentlePiConfigHome = process.env.GENTLE_PI_CONFIG_HOME;
|
|
94
|
+
process.env.GENTLE_PI_CONFIG_HOME = join(root, "gentle-ai-config-home");
|
|
86
95
|
after(async () => {
|
|
87
96
|
try { await stopActiveSessions(); }
|
|
88
|
-
finally {
|
|
97
|
+
finally {
|
|
98
|
+
if (previousGentlePiConfigHome === undefined) delete process.env.GENTLE_PI_CONFIG_HOME;
|
|
99
|
+
else process.env.GENTLE_PI_CONFIG_HOME = previousGentlePiConfigHome;
|
|
100
|
+
rmSync(root, { recursive: true, force: true });
|
|
101
|
+
}
|
|
89
102
|
});
|
|
90
103
|
const home = join(root, "home");
|
|
91
104
|
const cwd = join(root, "project");
|
|
@@ -97,7 +97,7 @@ async function writeWindowsSourceBinary(packageRoot: string): Promise<{ binaryPa
|
|
|
97
97
|
method: "go-sumdb-source-build",
|
|
98
98
|
package: "github.com/gentleman-programming/gentle-ai/v3/cmd/gentle-ai",
|
|
99
99
|
module: "github.com/gentleman-programming/gentle-ai/v3",
|
|
100
|
-
tag: "v3.0
|
|
100
|
+
tag: "v3.1.0",
|
|
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,
|