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.
Files changed (38) hide show
  1. package/README.md +183 -943
  2. package/contracts/review-provider-contract-mirror/provider-contract.lock.json +9 -8
  3. package/contracts/review-provider-contract-mirror/v1.2.0/bundle/manifest.json +3 -3
  4. package/contracts/review-provider-contract-mirror/v1.2.0/bundle/orchestration/pi.md +7 -2
  5. package/contracts/review-provider-contract-mirror/v1.2.0/bundle/schemas/lens.schema.json +2 -2
  6. package/contracts/review-provider-contract-mirror/v1.2.0/bundle/schemas/targeted-validator.schema.json +1 -1
  7. package/contracts/review-provider-contract-mirror/v1.2.0/generated/provider-capabilities.baseline.json +1 -1
  8. package/contracts/review-provider-contract-mirror/v1.2.0/generated/provider-roles.baseline.json +2 -2
  9. package/docs/assets/brand/gentle-pi-banner.png +0 -0
  10. package/docs/assets/brand/gentle-pi-banner.svg +33 -0
  11. package/docs/assets/brand/terminal-divider.svg +17 -0
  12. package/docs/assets/diagrams/agent-orchestration.svg +19 -0
  13. package/docs/assets/diagrams/gentleman-workflow.svg +15 -0
  14. package/docs/assets/diagrams/native-review.svg +16 -0
  15. package/docs/assets/diagrams/sdd-cycle.svg +14 -0
  16. package/docs/assets/features/gentle-shell.png +0 -0
  17. package/docs/gentle-shell.md +151 -0
  18. package/docs/readme-reference.md +868 -0
  19. package/extensions/gentle-agents.ts +4 -1
  20. package/extensions/gentle-ai.ts +65 -22
  21. package/lib/agents-history.ts +7 -1
  22. package/lib/native-review-cli.ts +9 -0
  23. package/package.json +1 -1
  24. package/runtime/native-review-cli.mjs +9 -0
  25. package/scripts/gentle-ai-installer.mjs +10 -10
  26. package/scripts/verify-package-files.mjs +2 -2
  27. package/tests/gentle-agents.test.ts +22 -0
  28. package/tests/gentle-ai-binary.test.ts +1 -1
  29. package/tests/gentle-ai-installer.test.ts +47 -47
  30. package/tests/gentle-ai.test.ts +3 -2
  31. package/tests/native-review-capability-contract.test.ts +13 -1
  32. package/tests/package-manifest.test.ts +19 -18
  33. package/tests/review-authority-recovery-docs.test.ts +13 -13
  34. package/tests/review-controller-native-routing.test.ts +49 -0
  35. package/tests/review-ledger-contract.test.ts +7 -5
  36. package/tests/sdd-managed-runtime-settlement.test.ts +37 -0
  37. package/tests/sdd-selection-transport.test.ts +57 -0
  38. 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(README), /Review outcomes and receipt state are informational; commit, push, pull-request, and release delivery follow ordinary repository policy\./);
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 README defer RDD lifecycle ownership to Gentle AI", () => {
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
- [README, read(README)],
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("README documents the dynamic runtime authority boundary without an old package route", () => {
392
- const content = read(README);
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("README documents legacy skill-name compatibility aliases", () => {
37
- const readme = readFileSync(join(repoRoot, "README.md"), "utf8");
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"],