gentle-pi 2.6.1 → 2.6.2
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 +183 -943
- package/contracts/review-provider-contract-mirror/provider-contract.lock.json +9 -8
- package/contracts/review-provider-contract-mirror/v1.2.0/bundle/manifest.json +3 -3
- package/contracts/review-provider-contract-mirror/v1.2.0/bundle/orchestration/pi.md +7 -2
- package/contracts/review-provider-contract-mirror/v1.2.0/bundle/schemas/lens.schema.json +2 -2
- package/contracts/review-provider-contract-mirror/v1.2.0/bundle/schemas/targeted-validator.schema.json +1 -1
- package/contracts/review-provider-contract-mirror/v1.2.0/generated/provider-capabilities.baseline.json +1 -1
- package/contracts/review-provider-contract-mirror/v1.2.0/generated/provider-roles.baseline.json +2 -2
- package/docs/assets/brand/gentle-pi-banner.png +0 -0
- package/docs/assets/brand/gentle-pi-banner.svg +33 -0
- package/docs/assets/brand/terminal-divider.svg +17 -0
- package/docs/assets/diagrams/agent-orchestration.svg +19 -0
- package/docs/assets/diagrams/gentleman-workflow.svg +15 -0
- package/docs/assets/diagrams/native-review.svg +16 -0
- package/docs/assets/diagrams/sdd-cycle.svg +14 -0
- package/docs/assets/features/gentle-shell.png +0 -0
- package/docs/gentle-shell.md +151 -0
- package/docs/readme-reference.md +868 -0
- package/extensions/gentle-agents.ts +4 -1
- package/extensions/gentle-ai.ts +65 -22
- package/lib/agents-history.ts +7 -1
- package/lib/native-review-cli.ts +9 -0
- 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/tests/gentle-agents.test.ts +22 -0
- package/tests/gentle-ai-binary.test.ts +1 -1
- package/tests/gentle-ai-installer.test.ts +47 -47
- package/tests/gentle-ai.test.ts +3 -2
- package/tests/native-review-capability-contract.test.ts +13 -1
- package/tests/package-manifest.test.ts +19 -18
- package/tests/review-authority-recovery-docs.test.ts +13 -13
- package/tests/review-controller-native-routing.test.ts +49 -0
- package/tests/review-ledger-contract.test.ts +7 -5
- package/tests/sdd-managed-runtime-settlement.test.ts +37 -0
- package/tests/sdd-selection-transport.test.ts +57 -0
- package/tests/skill-collision-prefixes.test.ts +2 -2
|
@@ -19,6 +19,7 @@ const JD_SKILL = "skills/judgment-day/SKILL.md";
|
|
|
19
19
|
const JD_PROMPTS = "skills/judgment-day/references/prompts-and-formats.md";
|
|
20
20
|
const GENTLE_SKILL = "skills/gentle-ai/SKILL.md";
|
|
21
21
|
const README = "README.md";
|
|
22
|
+
const TECHNICAL_REFERENCE = "docs/readme-reference.md";
|
|
22
23
|
const CHAIN = "assets/chains/4r-review.chain.md";
|
|
23
24
|
const SDD_WORKFLOW = "assets/sdd-orchestrator-workflow.md";
|
|
24
25
|
const RELEASE_SKILL = "skills/release/SKILL.md";
|
|
@@ -132,7 +133,8 @@ test("canonical contract defines compact risk, causal admission, correction, CAS
|
|
|
132
133
|
/untrusted repository content.*malformed inputs.*stale authority.*path drift.*external callers/i,
|
|
133
134
|
...JUDGMENT_DAY_DISCOVERY_PATTERNS,
|
|
134
135
|
]);
|
|
135
|
-
assert.match(read(
|
|
136
|
+
assert.match(read(TECHNICAL_REFERENCE), /Review outcomes and receipt state are informational; commit, push, pull-request, and release delivery follow ordinary repository policy\./);
|
|
137
|
+
assert.match(read(README), /\]\(docs\/readme-reference\.md(?:#[^)]+)?\)/);
|
|
136
138
|
assert.doesNotMatch(read(README), /one one-shot authorization for the exact command/i);
|
|
137
139
|
assert.doesNotMatch(read(README), /review-publication-gate/i);
|
|
138
140
|
});
|
|
@@ -365,14 +367,14 @@ test("Judgment Day fix routing has one canonical shape and never falls back to g
|
|
|
365
367
|
assert.match(read(SDD_WORKFLOW), /\| default\s+\| balanced\s+\| SDD phase fallback; never a Judgment Day role\s+\|/);
|
|
366
368
|
});
|
|
367
369
|
|
|
368
|
-
test("orchestrator, injected skill, and
|
|
370
|
+
test("orchestrator, injected skill, and technical reference defer RDD lifecycle ownership to Gentle AI", () => {
|
|
369
371
|
const boundary = "This package injects the mirrored provider-bundle review execution contract into this session's system prompt at start; Gentle AI writes nothing into the Pi system prompt, and this package owns everything else here. Absent that mirrored contract, this package invents no lifecycle instructions.";
|
|
370
372
|
const orchestrator = union(ORCHESTRATOR);
|
|
371
373
|
assert.ok(orchestrator.includes(boundary), "orchestrator must carry the sole static ownership boundary");
|
|
372
374
|
|
|
373
375
|
for (const [label, content] of [
|
|
374
376
|
[GENTLE_SKILL, read(GENTLE_SKILL)],
|
|
375
|
-
[
|
|
377
|
+
[TECHNICAL_REFERENCE, read(TECHNICAL_REFERENCE)],
|
|
376
378
|
] as const) {
|
|
377
379
|
assertMatches(label, content, [
|
|
378
380
|
/Gentle AI dynamically supplies runtime-specific RDD instructions/i,
|
|
@@ -388,8 +390,8 @@ test("orchestrator, injected skill, and README defer RDD lifecycle ownership to
|
|
|
388
390
|
}
|
|
389
391
|
});
|
|
390
392
|
|
|
391
|
-
test("
|
|
392
|
-
const content = read(
|
|
393
|
+
test("technical reference documents the dynamic runtime authority boundary without an old package route", () => {
|
|
394
|
+
const content = read(TECHNICAL_REFERENCE);
|
|
393
395
|
assert.match(content, /Gentle AI dynamically supplies runtime-specific RDD instructions/i);
|
|
394
396
|
assert.match(content, /does not define an RDD lifecycle/i);
|
|
395
397
|
assert.doesNotMatch(content, /New ordinary review uses compact `gentle_review` `start -> finalize -> validate`\./);
|
|
@@ -185,9 +185,11 @@ test("history pruning retains admitted unsettled and uncertain task payloads", a
|
|
|
185
185
|
const dir = await mkdtemp(join(tmpdir(), "remediation-history-")); t.after(() => rm(dir, { recursive: true, force: true }));
|
|
186
186
|
await saveTask(dir, { id: "retained", agent: "sdd-remediate", status: "failed", createdAt: 1, sddRemediation: { token: "opaque", settlementUncertain: true, settle: { requestId: "exact" } } } as unknown as TaskRecord, emptyThread());
|
|
187
187
|
for (const state of ["blocked", "complete"] as const) await saveTask(dir, { id: state, agent: "sdd-remediate", status: "failed", createdAt: 2, sddRemediation: { acquireResult: { state } } } as unknown as TaskRecord, emptyThread());
|
|
188
|
+
await saveTask(dir, { id: "settled-blocked", agent: "sdd-remediate", status: "failed", createdAt: 3, sddRemediation: { settlement: { state: "blocked", reason: "maintainer_decision" } } } as unknown as TaskRecord, emptyThread());
|
|
188
189
|
await pruneHistory(dir, 0);
|
|
189
190
|
const stored = await loadHistory(dir);
|
|
190
191
|
assert.equal(stored.length, 1); assert.equal(stored[0].task.sddRemediation.settle.requestId, "exact");
|
|
192
|
+
assert.ok(!stored.some(entry => entry.task.id === "settled-blocked"), "a known blocked settlement is prunable like any terminal task");
|
|
191
193
|
});
|
|
192
194
|
|
|
193
195
|
|
|
@@ -214,6 +216,41 @@ test("native refusal or uncertain settlement is not a completed managed correcti
|
|
|
214
216
|
assert.equal(task.status, "failed");
|
|
215
217
|
assert.match(task.error, /settlement/);
|
|
216
218
|
});
|
|
219
|
+
test("a received blocked settlement names the block; a lost settlement reply retains unresolved history", async () => {
|
|
220
|
+
const known = await admissionFixture({ sddAttemptSettle: async () => ({ state: "blocked", reason: "maintainer_decision" }) });
|
|
221
|
+
known.task.status = "completed";
|
|
222
|
+
await known.admitted.finalizeRemediation(known.task, { spawned: true, exited: true, cleanupConfirmed: true });
|
|
223
|
+
assert.equal(known.task.status, "failed");
|
|
224
|
+
assert.match(known.task.error, /blocked\(maintainer_decision\)/);
|
|
225
|
+
assert.doesNotMatch(known.task.error, /unresolved/);
|
|
226
|
+
assert.equal(remediationUnresolved(known.task), false);
|
|
227
|
+
|
|
228
|
+
const unspecified = await admissionFixture({ sddAttemptSettle: async () => ({ state: "blocked" }) });
|
|
229
|
+
unspecified.task.status = "completed";
|
|
230
|
+
await unspecified.admitted.finalizeRemediation(unspecified.task, { spawned: true, exited: true, cleanupConfirmed: true });
|
|
231
|
+
assert.equal(unspecified.task.status, "failed");
|
|
232
|
+
assert.match(unspecified.task.error, /blocked\(unspecified\)/, "a settlement without a reason is distinguishable from one whose reason is literally blocked");
|
|
233
|
+
assert.doesNotMatch(unspecified.task.error, /unresolved/);
|
|
234
|
+
|
|
235
|
+
let attempts = 0;
|
|
236
|
+
const uncertain = await admissionFixture({ sddAttemptSettle: async () => { attempts++; throw new Error("lost reply"); } });
|
|
237
|
+
uncertain.task.status = "completed";
|
|
238
|
+
await uncertain.admitted.finalizeRemediation(uncertain.task, { spawned: true, exited: true, cleanupConfirmed: true });
|
|
239
|
+
assert.equal(attempts, 2);
|
|
240
|
+
assert.equal(uncertain.task.status, "failed");
|
|
241
|
+
assert.match(uncertain.task.error, /unresolved/);
|
|
242
|
+
assert.equal(remediationUnresolved(uncertain.task), true);
|
|
243
|
+
});
|
|
244
|
+
test("remediationUnresolved treats a received settlement as terminal, regardless of state, unless uncertain", () => {
|
|
245
|
+
// A terminal acquire result keeps the final branch false, so the token and
|
|
246
|
+
// actor-claim assertions below discriminate on those fields alone.
|
|
247
|
+
const base = { acquire: {}, acquireResult: { state: "complete" } } as unknown as TaskRecord["sddRemediation"];
|
|
248
|
+
assert.equal(remediationUnresolved({ sddRemediation: { ...base } } as unknown as TaskRecord), false);
|
|
249
|
+
assert.equal(remediationUnresolved({ sddRemediation: { ...base, settlement: { state: "blocked", reason: "maintainer_decision" } } } as unknown as TaskRecord), false);
|
|
250
|
+
assert.equal(remediationUnresolved({ sddRemediation: { ...base, settlement: { state: "blocked", reason: "maintainer_decision" }, settlementUncertain: true } } as unknown as TaskRecord), true);
|
|
251
|
+
assert.equal(remediationUnresolved({ sddRemediation: { ...base, actorClaimed: true } } as unknown as TaskRecord), true);
|
|
252
|
+
assert.equal(remediationUnresolved({ sddRemediation: { ...base, token: "retained" } } as unknown as TaskRecord), true);
|
|
253
|
+
});
|
|
217
254
|
|
|
218
255
|
|
|
219
256
|
test("remediation actor preserves separately authorized memory artifact tools", async () => {
|
|
@@ -218,6 +218,63 @@ test("selected native v2 failures fail closed without consulting the local resol
|
|
|
218
218
|
);
|
|
219
219
|
});
|
|
220
220
|
|
|
221
|
+
function nativeStartup(
|
|
222
|
+
serialized: unknown,
|
|
223
|
+
cwd: string,
|
|
224
|
+
agentName: string,
|
|
225
|
+
native: { sddStatus: (request: unknown) => Promise<unknown> },
|
|
226
|
+
) {
|
|
227
|
+
return (__testing as unknown as {
|
|
228
|
+
resolveSelectedNativeSddChangeStartup(
|
|
229
|
+
serialized: unknown, cwd: string, agentName: string,
|
|
230
|
+
native: { sddStatus?: (request: unknown) => Promise<unknown> },
|
|
231
|
+
): Promise<{ selection: { changeName: string; workspaceRoot: string; phase: string }; status: NativeSddStatusV2 }>;
|
|
232
|
+
}).resolveSelectedNativeSddChangeStartup(serialized, cwd, agentName, native);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function verifyRefreshAuthority(root: string, blockedReasons: readonly string[]) {
|
|
236
|
+
return {
|
|
237
|
+
schemaName: "gentle-ai.sdd-status", schemaVersion: 2, changeName: "alpha", artifactStore: "openspec",
|
|
238
|
+
planningHome: { mode: "repo-local", path: join(root, "openspec") }, changeRoot: join(root, "openspec/changes/alpha"),
|
|
239
|
+
actionContext: { mode: "repo-local", workspaceRoot: root, allowedEditRoots: [root] },
|
|
240
|
+
dependencies: { proposal: "all_done", specs: "all_done", design: "all_done", tasks: "all_done", apply: "all_done", verify: "ready", archive: "blocked" },
|
|
241
|
+
phaseInstructions: { apply: ["done"], verify: ["rerun SDD verification"], remediate: ["failed evidence"], archive: ["blocked"] },
|
|
242
|
+
blockedReasons: [...blockedReasons], nextRecommended: "verify",
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
test("a native verify evidence-refresh route starts under its own blocker while every other phase stays closed", async (t) => {
|
|
247
|
+
const root = workspace(t);
|
|
248
|
+
const refreshReason = "failed verification evidence is incomplete; rerun SDD verification";
|
|
249
|
+
const authority = verifyRefreshAuthority(root, [refreshReason]);
|
|
250
|
+
const startup = await nativeStartup(
|
|
251
|
+
JSON.stringify({ changeName: "alpha", workspaceRoot: root, phase: "verify" }),
|
|
252
|
+
root,
|
|
253
|
+
"sdd-verify",
|
|
254
|
+
{ sddStatus: async () => authority },
|
|
255
|
+
);
|
|
256
|
+
assert.deepEqual(startup.selection, { changeName: "alpha", workspaceRoot: root, phase: "verify" });
|
|
257
|
+
assert.equal(startup.status, authority, "the validated native status is injected whole, blockers included");
|
|
258
|
+
assert.deepEqual(startup.status.blockedReasons, [refreshReason], "the blocking reason is preserved for reporting");
|
|
259
|
+
|
|
260
|
+
for (const phase of ["apply", "archive"] as const) {
|
|
261
|
+
const gated = {
|
|
262
|
+
...verifyRefreshAuthority(root, [refreshReason]),
|
|
263
|
+
nextRecommended: phase,
|
|
264
|
+
dependencies: { ...authority.dependencies, [phase]: "ready", verify: "all_done" },
|
|
265
|
+
};
|
|
266
|
+
await assert.rejects(
|
|
267
|
+
() => nativeStartup(
|
|
268
|
+
JSON.stringify({ changeName: "alpha", workspaceRoot: root, phase }),
|
|
269
|
+
root,
|
|
270
|
+
`sdd-${phase}`,
|
|
271
|
+
{ sddStatus: async () => gated },
|
|
272
|
+
),
|
|
273
|
+
/native status blocks phase/i,
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
|
|
221
278
|
test("a throwing SDD selection flag reader fails closed without resolving an unselected status", (t) => {
|
|
222
279
|
const root = workspace(t);
|
|
223
280
|
assert.equal(__testing.readSddChangeFlag({ getFlag: () => false } as never), undefined);
|
|
@@ -33,8 +33,8 @@ for (const [dir, expectedName] of Object.entries(PREFIXED_NAMES)) {
|
|
|
33
33
|
});
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
test("
|
|
37
|
-
const readme = readFileSync(join(repoRoot, "
|
|
36
|
+
test("technical reference documents legacy skill-name compatibility aliases", () => {
|
|
37
|
+
const readme = readFileSync(join(repoRoot, "docs", "readme-reference.md"), "utf8");
|
|
38
38
|
for (const [legacyName, prefixedName] of [
|
|
39
39
|
["branch-pr", "gentle-ai-branch-pr"],
|
|
40
40
|
["judgment-day", "gentle-ai-judgment-day"],
|