gentle-pi 2.1.2 → 2.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/README.md +70 -12
- package/assets/agents/gentle-ai-worker.md +7 -3
- package/assets/agents/jd-fix-agent.md +1 -1
- package/assets/agents/jd-judge-a.md +3 -1
- package/assets/agents/jd-judge-b.md +3 -1
- package/assets/agents/review-readability.md +4 -1
- package/assets/agents/review-reliability.md +4 -1
- package/assets/agents/review-resilience.md +4 -1
- package/assets/agents/review-risk.md +4 -1
- package/assets/agents/sdd-apply.md +6 -1
- package/assets/agents/sdd-archive.md +6 -1
- package/assets/agents/sdd-design.md +6 -1
- package/assets/agents/sdd-explore.md +6 -2
- package/assets/agents/sdd-init.md +10 -2
- package/assets/agents/sdd-onboard.md +6 -1
- package/assets/agents/sdd-proposal.md +6 -1
- package/assets/agents/sdd-spec.md +6 -1
- package/assets/agents/sdd-status.md +6 -1
- package/assets/agents/sdd-sync.md +6 -1
- package/assets/agents/sdd-tasks.md +6 -1
- package/assets/agents/sdd-verify.md +6 -1
- package/assets/chains/4r-review.chain.md +2 -0
- package/assets/chains/sdd-full.chain.md +1 -1
- package/assets/chains/sdd-plan.chain.md +1 -1
- package/assets/chains/sdd-verify.chain.md +1 -1
- package/assets/orchestrator-delegation.md +246 -67
- package/assets/orchestrator.md +7 -14
- package/assets/sdd-orchestrator-workflow.md +154 -9
- package/assets/support/sdd-status-contract.md +19 -1
- package/contracts/review-integration/v1/fixtures/consent.fixture.json +3 -3
- package/contracts/review-integration/v1/fixtures/start-v2.fixture.json +19 -28
- package/contracts/review-integration/v1/fixtures/start.fixture.json +1 -10
- package/contracts/review-integration/v1/fixtures/status-v2.fixture.json +12 -21
- package/contracts/review-integration/v1/schemas/correction-plan-request.schema.json +49 -0
- package/contracts/review-integration/v1/schemas/operation.schema.json +76 -0
- package/contracts/review-integration/v1/schemas/repair.schema.json +39 -0
- package/contracts/review-integration/v1/schemas/status-v2.schema.json +4 -2
- package/contracts/review-integration/v1/schemas/status.schema.json +4 -2
- package/contracts/review-integration/v2/fixtures/consent.fixture.json +1 -1
- package/contracts/review-integration/v2/fixtures/start.fixture.json +1 -10
- package/contracts/review-integration/v2/fixtures/status.fixture.json +1 -10
- package/contracts/review-integration/v2/schemas/failure.schema.json +5 -1
- package/contracts/review-integration/v2/schemas/operation.schema.json +6 -1
- package/contracts/review-integration/v2/schemas/repair.schema.json +4 -2
- package/contracts/review-integration/v2/schemas/start.schema.json +5 -2
- package/contracts/review-integration/v2/schemas/status.schema.json +4 -2
- package/contracts/review-provider-contract-mirror/provider-contract.lock.json +30 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/README.md +12 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/manifest.json +65 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/schemas/lens.schema.json +16 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/schemas/refuter.schema.json +1 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/schemas/targeted-validator.schema.json +1 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/vectors/lens.json +1 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/vectors/refuter.json +1 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/bundle/vectors/targeted-validator.json +1 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/generated/provider-capabilities.baseline.json +15 -0
- package/contracts/review-provider-contract-mirror/v1.1.0/generated/provider-roles.baseline.json +42 -0
- package/docs/native-authority-architecture.md +5 -5
- package/docs/review-integration.md +22 -2
- package/extensions/gentle-ai.ts +1595 -201
- package/extensions/sdd-init.ts +19 -6
- package/extensions/skill-registry.ts +10 -2
- package/extensions/startup-banner.ts +10 -4
- package/lib/gentle-ai-binary.ts +173 -2
- package/lib/git-commit-transaction.ts +77 -17
- package/lib/native-review-cli.ts +528 -65
- package/lib/provider-contract-bundle.ts +704 -0
- package/lib/review-candidate-view.ts +527 -18
- package/lib/review-compact-contract.ts +59 -248
- package/lib/review-host-relay.ts +436 -0
- package/lib/review-integration-v2.ts +537 -36
- package/lib/review-relay-contract.ts +16 -0
- package/lib/sdd-preflight.ts +53 -1
- package/package.json +5 -2
- package/runtime/gentle-ai-binary.mjs +173 -2
- package/runtime/git-commit-transaction.mjs +75 -15
- package/runtime/native-review-cli.mjs +524 -61
- package/runtime/review-integration-v2.mjs +536 -35
- package/runtime/review-relay-contract.mjs +17 -0
- package/scripts/build-git-commit-transaction-runner.mjs +1 -0
- package/scripts/check-provider-contract.mjs +138 -0
- package/scripts/gentle-ai-installer.mjs +23 -13
- package/scripts/maintainer/provider-relay-matrix.mjs +219 -0
- package/scripts/mirror-provider-contract.mjs +143 -0
- package/scripts/test-packed-runner.mjs +16 -2
- package/scripts/verify-package-files.mjs +110 -33
- package/skills/_shared/review-ledger-contract.md +4 -6
- package/skills/gentle-ai/SKILL.md +4 -4
- package/skills/issue-creation/SKILL.md +94 -168
- package/skills/judgment-day/SKILL.md +7 -1
- package/skills/judgment-day/references/prompts-and-formats.md +2 -0
- package/skills/rdd-defect-workflow/SKILL.md +54 -0
- package/tests/background-subagents.test.ts +771 -0
- package/tests/crosslane/cross-lane.mjs +1169 -0
- package/tests/delegated-key-learnings-contract.test.ts +238 -0
- package/tests/fixtures/devbinary/capabilities-v2.1.derived.json +331 -0
- package/tests/fixtures/devbinary/capabilities-v2.2.captured.json +340 -0
- package/tests/fixtures/devbinary/consent-v3.captured.json +37 -0
- package/tests/fixtures/devbinary/failure-v2-capture-evidence.captured.json +16 -0
- package/tests/fixtures/devbinary/result-artifact-v2-path.captured.json +12 -0
- package/tests/fixtures/devbinary/result-artifact-v2.captured.json +12 -0
- package/tests/fixtures/devbinary/start-v3-consent-declined.captured.json +19 -0
- package/tests/fixtures/devbinary/start-v3-consent-granted.captured.json +109 -0
- package/tests/fixtures/devbinary/status-v5-capture-result-submission.captured.json +184 -0
- package/tests/fixtures/devbinary/status-v5-repository-context.captured.json +138 -0
- package/tests/fixtures/devbinary/status-v5.captured.json +88 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/README.md +12 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/manifest.json +65 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/schemas/lens.schema.json +16 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/schemas/refuter.schema.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/schemas/targeted-validator.schema.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/vectors/lens.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/vectors/refuter.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.1.0/vectors/targeted-validator.json +1 -0
- package/tests/gentle-ai-binary.test.ts +1 -1
- package/tests/gentle-ai-dev-binary-surfacing.test.ts +195 -0
- package/tests/gentle-ai-dev-binary.test.ts +336 -0
- package/tests/gentle-ai-installer.test.ts +46 -46
- package/tests/git-commit-transaction.test.ts +229 -1
- package/tests/maintainer/provider-relay.maintest.ts +265 -0
- package/tests/native-review-capability-contract.test.ts +48 -2
- package/tests/native-review-cli.test.ts +56 -0
- package/tests/native-review-consent.test.ts +164 -3
- package/tests/native-review-parity-runtime.test.ts +37 -0
- package/tests/native-review-parity.test.ts +218 -15
- package/tests/native-sdd-attempt-authority.test.ts +235 -0
- package/tests/orchestrator-budget.test.ts +30 -5
- package/tests/package-manifest.test.ts +98 -72
- package/tests/provider-contract-bundle.test.ts +385 -0
- package/tests/provider-contract-mirror.test.ts +206 -0
- package/tests/provider-defect-handoff.test.ts +355 -0
- package/tests/review-actor-tool-deny.test.ts +12 -13
- package/tests/review-candidate-view.test.ts +489 -9
- package/tests/review-compact-contract.test.ts +52 -119
- package/tests/review-controller-native-recovery.test.ts +643 -47
- package/tests/review-controller-native-routing.test.ts +1667 -222
- package/tests/review-controller-workspace-root.test.ts +17 -2
- package/tests/review-corrected-finalize-binding.test.ts +175 -0
- package/tests/review-dispatch-hydration-gap.test.ts +197 -0
- package/tests/review-host-relay-routing.test.ts +317 -0
- package/tests/review-host-relay.test.ts +520 -0
- package/tests/review-integration-v2-forward.test.ts +631 -0
- package/tests/review-integration-v2.test.ts +114 -0
- package/tests/review-ledger-contract.test.ts +12 -28
- package/tests/review-recovered-lineage-routing.test.ts +246 -0
- package/tests/review-relay-transport-agent.test.ts +249 -0
- package/tests/runtime-harness.mjs +242 -14
- package/tests/sdd-agent-tools.test.ts +18 -33
- package/tests/skill-collision-prefixes.test.ts +1 -0
- package/tests/skill-registry.test.ts +50 -1
- package/tests/verify-package-files.test.ts +62 -0
- package/assets/agents/review-refuter.md +0 -40
- package/assets/agents/review-validator.md +0 -23
- package/lib/native-review-remediation.ts +0 -49
- package/lib/review-compact.ts +0 -947
- package/lib/review-refuter-adapter.ts +0 -129
- package/lib/review-runtime-contract.ts +0 -68
- package/prompts/gcl.md +0 -54
- package/prompts/gis.md +0 -25
- package/prompts/gpr.md +0 -41
- package/prompts/gwr.md +0 -31
- package/tests/fixtures/native-review-cli/v2.1.2/bind-sdd.json +0 -25
- package/tests/fixtures/native-review-cli/v2.1.2/finalize.json +0 -8
- package/tests/fixtures/native-review-cli/v2.1.2/sdd-status-engram.json +0 -139
- package/tests/fixtures/native-review-cli/v2.1.2/sdd-status.json +0 -200
- package/tests/fixtures/native-review-cli/v2.1.2/start.json +0 -12
- package/tests/fixtures/native-review-cli/v2.1.2/validate-allow.json +0 -24
- package/tests/fixtures/native-review-cli/v2.1.2/validate-deny-empty-context.json +0 -20
- package/tests/fixtures/native-review-cli/v2.1.2/validate-deny.json +0 -28
- package/tests/review-compact.test.ts +0 -243
- package/tests/review-refuter-adapter.test.ts +0 -89
|
@@ -89,6 +89,40 @@ interface ReviewGateResult {
|
|
|
89
89
|
context: ReviewGateContext;
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
// Every test in this file drives the real pinned binary through a full review
|
|
93
|
+
// lifecycle, and gentle-ai v2.4.0 made receipt-driven development opt-in: an
|
|
94
|
+
// install whose global mode was never set now resolves to off, and START is
|
|
95
|
+
// rejected before it reaches any of the behavior these tests exist to check.
|
|
96
|
+
//
|
|
97
|
+
// Until that release these tests had a silent dependency on whatever the
|
|
98
|
+
// operator happened to have configured. That is not a new hazard introduced by
|
|
99
|
+
// the pin — it was always there — but the flip is what made it visible: the
|
|
100
|
+
// suite was green on a machine with review enabled and red on a fresh CI
|
|
101
|
+
// runner, from identical bytes. A test whose result depends on unversioned
|
|
102
|
+
// machine state is not evidence either way.
|
|
103
|
+
//
|
|
104
|
+
// So each test owns a sandbox HOME and opts in the same way a user does,
|
|
105
|
+
// exactly as gentle-ai did for its own lifecycle fixtures in the commit that
|
|
106
|
+
// flipped the default. The process-wide HOME is what the extension-registered
|
|
107
|
+
// controller path needs, because it spawns the CLI with the inherited
|
|
108
|
+
// environment rather than an explicit one; it is restored afterwards.
|
|
109
|
+
async function reviewEnabledHome(t: baseTest.TestContext): Promise<string> {
|
|
110
|
+
const home = await mkdtemp(join(tmpdir(), "gentle-pi-review-home-"));
|
|
111
|
+
const previousHome = process.env.HOME;
|
|
112
|
+
process.env.HOME = home;
|
|
113
|
+
t.after(async () => {
|
|
114
|
+
if (previousHome === undefined) delete process.env.HOME;
|
|
115
|
+
else process.env.HOME = previousHome;
|
|
116
|
+
await rm(home, { recursive: true, force: true });
|
|
117
|
+
});
|
|
118
|
+
const enabled = await run(binary, ["review", "mode", "enable", "--scope", "global", "--json"], packageRoot, false, { ...process.env, HOME: home });
|
|
119
|
+
// Assert the opt-in landed rather than assuming it. A silently ineffective
|
|
120
|
+
// enable would put these tests straight back to depending on ambient state,
|
|
121
|
+
// which is the exact failure this helper exists to remove.
|
|
122
|
+
assert.match(enabled.stdout, /"effective": "on"/, "the sandbox HOME must have receipt-driven development explicitly enabled");
|
|
123
|
+
return home;
|
|
124
|
+
}
|
|
125
|
+
|
|
92
126
|
async function run(command: string, arguments_: readonly string[], cwd: string, allowFailure = false, environment?: NodeJS.ProcessEnv): Promise<CommandResult> {
|
|
93
127
|
try {
|
|
94
128
|
const result = await execFileAsync(command, [...arguments_], { cwd, encoding: "utf8", shell: false, env: environment });
|
|
@@ -204,6 +238,7 @@ async function finalizeEmptyReview(repository: string, artifacts: string, starte
|
|
|
204
238
|
}
|
|
205
239
|
|
|
206
240
|
test("official pinned package runtime authorizes an unchanged linked-view candidate and denies a changed staging tree", async (t) => {
|
|
241
|
+
await reviewEnabledHome(t);
|
|
207
242
|
assert.equal(createHash("sha256").update(await readFile(binary)).digest("hex"), OFFICIAL_BINARY_SHA256);
|
|
208
243
|
assert.deepEqual(await run(binary, ["version"], packageRoot), { exitCode: 0, stdout: `gentle-ai ${GENTLE_AI_VERSION}\n`, stderr: "" });
|
|
209
244
|
|
|
@@ -285,6 +320,7 @@ test("official pinned package runtime authorizes an unchanged linked-view candid
|
|
|
285
320
|
});
|
|
286
321
|
|
|
287
322
|
test("official pinned package runtime keeps frozen candidate lineages and receipts isolated across replay and replacement", async (t) => {
|
|
323
|
+
await reviewEnabledHome(t);
|
|
288
324
|
const workspace = await mkdtemp(join(tmpdir(), "gentle-pi-v215-lineage-"));
|
|
289
325
|
const repository = join(workspace, "repository");
|
|
290
326
|
const artifacts = join(workspace, "artifacts");
|
|
@@ -379,6 +415,7 @@ test("official pinned package runtime keeps frozen candidate lineages and receip
|
|
|
379
415
|
});
|
|
380
416
|
|
|
381
417
|
test("registered gentle_review START materializes a safe internal skill symlink and cleans pending consent on session shutdown", async (t) => {
|
|
418
|
+
await reviewEnabledHome(t);
|
|
382
419
|
const workspace = await mkdtemp(join(tmpdir(), "gentle-pi-v215-symlink-candidate-"));
|
|
383
420
|
const repository = join(workspace, "repository");
|
|
384
421
|
t.after(async () => rm(workspace, { recursive: true, force: true }));
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import { execFileSync } from "node:child_process";
|
|
3
|
-
import { mkdtempSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { mkdtempSync, realpathSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
import test from "node:test";
|
|
@@ -8,6 +8,7 @@ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-a
|
|
|
8
8
|
import { __testing, createGentleAiExtension } from "../extensions/gentle-ai.ts";
|
|
9
9
|
import {
|
|
10
10
|
NATIVE_REVIEW_ERROR_CODE,
|
|
11
|
+
REVIEW_CONSENT_NOTICES,
|
|
11
12
|
NativeReviewCliError,
|
|
12
13
|
NativeReviewConsentBindingError,
|
|
13
14
|
NativeReviewConsentRequiredError,
|
|
@@ -21,7 +22,7 @@ import {
|
|
|
21
22
|
type NativeStartRequest,
|
|
22
23
|
} from "../lib/native-review-cli.ts";
|
|
23
24
|
import { CandidateViewRegistry } from "../lib/review-candidate-view.ts";
|
|
24
|
-
import { recordReviewConsentLatch } from "../lib/review-consent-latch.ts";
|
|
25
|
+
import { readReviewConsentLatch, recordReviewConsentLatch } from "../lib/review-consent-latch.ts";
|
|
25
26
|
import type { AuthorityRepairAssessmentV1, ReviewConsentV2, ReviewStatusV3 } from "../lib/review-integration-v2.ts";
|
|
26
27
|
|
|
27
28
|
// Queued-adapter clients never execute a real process; default to a fixed
|
|
@@ -261,6 +262,29 @@ test("START tolerates the exact review-consent-skipped stderr line only when the
|
|
|
261
262
|
);
|
|
262
263
|
});
|
|
263
264
|
|
|
265
|
+
// gentle-ai v2.4.0 (feat(review)!: make receipt-driven development opt-in)
|
|
266
|
+
// DELETED reviewConsentSkippedDefaultProvenance. It existed to admit reviews
|
|
267
|
+
// were on because nobody chose; under opt-in RDD a default-source clone is
|
|
268
|
+
// refused before the consent ceremony runs, so the pinned binary can never
|
|
269
|
+
// emit it. The allowlist is a statement about what the PINNED binary writes
|
|
270
|
+
// while still succeeding, so a line no pinned binary can produce does not
|
|
271
|
+
// belong in it: keeping it would silently tolerate that exact text from
|
|
272
|
+
// anywhere else in a START's stderr.
|
|
273
|
+
test("the tolerated-stderr allowlist carries no line the pinned binary cannot emit", async () => {
|
|
274
|
+
assert.ok(
|
|
275
|
+
!REVIEW_CONSENT_NOTICES.some((notice) => notice.startsWith("Reviews are on by default")),
|
|
276
|
+
"the default-provenance notice was deleted upstream in v2.4.0 and must not stay tolerated",
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
const start = JSON.parse(START.stdout) as Record<string, unknown>;
|
|
280
|
+
const deleted = "Reviews are on by default; this was never explicitly chosen. Run 'gentle-ai review mode enable' to make reviews an explicit choice, or 'gentle-ai review mode disable' to turn them off.";
|
|
281
|
+
const queue = queuedAdapter([CAPABLE_VERSION_LINE, { stdout: JSON.stringify(start), stderr: deleted }]);
|
|
282
|
+
await assert.rejects(
|
|
283
|
+
() => new NativeReviewCliV213(queue.adapter).start({ cwd: "/repo" }),
|
|
284
|
+
(error: unknown) => error instanceof NativeReviewCliError && error.code === NATIVE_REVIEW_ERROR_CODE.UNEXPECTED_STDERR,
|
|
285
|
+
);
|
|
286
|
+
});
|
|
287
|
+
|
|
264
288
|
test("START rejects near-miss, prefixed, or extra-line stderr even when the mode capability is true", async () => {
|
|
265
289
|
const start = JSON.parse(START.stdout) as Record<string, unknown>;
|
|
266
290
|
const notice = "Gentle AI reviewed this change without asking, because this session has no terminal to answer on. Run 'gentle-ai review mode disable' to turn reviews off, or 'gentle-ai review mode status' to see the current setting.";
|
|
@@ -441,12 +465,16 @@ interface RegisteredEventFixture {
|
|
|
441
465
|
(event: unknown, ctx: ExtensionContext): Promise<unknown> | unknown;
|
|
442
466
|
}
|
|
443
467
|
|
|
444
|
-
function runtime(
|
|
468
|
+
function runtime(
|
|
469
|
+
nativeReviewCli: NativeReviewCli | null,
|
|
470
|
+
writeReviewConsentLatch: typeof recordReviewConsentLatch = recordReviewConsentLatch,
|
|
471
|
+
clock?: { now?: () => number; scheduleTimer?: (callback: () => void, delayMs: number) => { unref: () => void } },
|
|
472
|
+
): { controller: RegisteredTool; commands: Map<string, RegisteredCommandFixture>; events: Map<string, RegisteredEventFixture> } {
|
|
445
473
|
const tools = new Map<string, RegisteredTool>();
|
|
446
474
|
const commands = new Map<string, RegisteredCommandFixture>();
|
|
447
475
|
const events = new Map<string, RegisteredEventFixture>();
|
|
448
|
-
const dependencies = { nativeReviewCli, candidateViews: new CandidateViewRegistry() } as unknown as Parameters<typeof createGentleAiExtension>[0];
|
|
449
|
-
createGentleAiExtension(dependencies)({
|
|
476
|
+
const dependencies = { nativeReviewCli, candidateViews: new CandidateViewRegistry(), ...clock } as unknown as Parameters<typeof createGentleAiExtension>[0];
|
|
477
|
+
__testing.createGentleAiExtension(dependencies, writeReviewConsentLatch)({
|
|
450
478
|
on(name: string, handler: RegisteredEventFixture) { events.set(name, handler); },
|
|
451
479
|
registerTool(definition: RegisteredTool & { name: string }) { tools.set(definition.name, definition); },
|
|
452
480
|
registerCommand(name: string, definition: RegisteredCommandFixture) { commands.set(name, definition); },
|
|
@@ -523,15 +551,28 @@ test("kill-switch: a global off names the native global-scope command, never Pi'
|
|
|
523
551
|
assert.ok(!/\/gentle:review-mode enable to turn/.test(String(result.next_action)), "a global off must never be sent to Pi's clone-scope command");
|
|
524
552
|
});
|
|
525
553
|
|
|
526
|
-
// Parity with reviewModeScopeForSource
|
|
527
|
-
//
|
|
528
|
-
|
|
554
|
+
// Parity with reviewModeScopeForSource as of gentle-ai v2.4.0, which made
|
|
555
|
+
// receipt-driven development opt-in. Before that release an all-sources-unset
|
|
556
|
+
// install resolved to ON with source `default`, so `default` could never be
|
|
557
|
+
// what kept reviews off and naming a continuation would have been a guess.
|
|
558
|
+
// v2.4.0 resolves the same install to OFF with source `default`, which makes
|
|
559
|
+
// this the MOST COMMON refusal there is: every install that never opted in.
|
|
560
|
+
// gentle-ai answers `global` for it -- not because default is a global
|
|
561
|
+
// opinion, but because global is the only scope that can turn reviews on at
|
|
562
|
+
// all. Pi must say the same thing, because the alternative is handing the
|
|
563
|
+
// single most common state a dead end with no way forward.
|
|
564
|
+
test("kill-switch: an off with the default source names the only scope that can turn reviews on", async (t) => {
|
|
529
565
|
const cwd = repository(t);
|
|
530
566
|
const { native } = fakeOrganicNative({ reviewModeEffective: "off", reviewModeSource: "default" });
|
|
531
567
|
const { controller } = runtime(native);
|
|
532
568
|
const result = await execStart(controller, "kill-switch-default", headlessContext(cwd));
|
|
533
569
|
assert.equal(result.mode_source, "default");
|
|
534
|
-
assert.equal(result.
|
|
570
|
+
assert.equal(result.reason, "receipt-driven development is disabled: start is skipped because the default mode source keeps it off");
|
|
571
|
+
assert.equal(
|
|
572
|
+
result.next_action,
|
|
573
|
+
"Run `gentle-ai review mode enable --scope=global` to turn reviews on; receipt-driven development is opt-in and nothing here has enabled it yet. /gentle:review-mode enable only sets clone scope, which can never turn reviews on.",
|
|
574
|
+
);
|
|
575
|
+
assert.ok(!/\/gentle:review-mode enable to turn/.test(String(result.next_action)), "a default off must never be sent to Pi's clone-scope command");
|
|
535
576
|
});
|
|
536
577
|
|
|
537
578
|
test("kill-switch: capability-absent (no reviewMode) leaves today's path unchanged", async (t) => {
|
|
@@ -610,8 +651,13 @@ test("consent relay returns the identical complete parent-visible envelope with
|
|
|
610
651
|
});
|
|
611
652
|
|
|
612
653
|
test("explicit consent follow-up grants or declines exactly once", async (t) => {
|
|
613
|
-
const cwd = repository(t);
|
|
614
654
|
for (const answer of ["granted", "declined"] as const) {
|
|
655
|
+
const repositoryCwd = repository(t);
|
|
656
|
+
const cwd = `${repositoryCwd}-alias`;
|
|
657
|
+
symlinkSync(repositoryCwd, cwd, process.platform === "win32" ? "junction" : "dir");
|
|
658
|
+
t.after(() => rmSync(cwd, { force: true }));
|
|
659
|
+
const canonicalCwd = realpathSync(cwd);
|
|
660
|
+
assert.equal(readReviewConsentLatch(cwd), false);
|
|
615
661
|
const { native, answers, startRequests, answerRequests } = relayedConsentNative(cwd);
|
|
616
662
|
const { controller } = runtime(native);
|
|
617
663
|
const blocked = await blockedConsent(controller, `consent-${answer}`, headlessContext(cwd));
|
|
@@ -626,17 +672,42 @@ test("explicit consent follow-up grants or declines exactly once", async (t) =>
|
|
|
626
672
|
assert.equal(answerRequests[0]?.consent.targetIdentity, startRequests[0]?.targetIdentity);
|
|
627
673
|
if (answer === "granted") {
|
|
628
674
|
const actorBinding = result.actor_binding as { workspace_root: string; candidate_root: string };
|
|
629
|
-
assert.equal(actorBinding.workspace_root,
|
|
630
|
-
assert.notEqual(actorBinding.candidate_root,
|
|
675
|
+
assert.equal(actorBinding.workspace_root, canonicalCwd);
|
|
676
|
+
assert.notEqual(actorBinding.candidate_root, canonicalCwd);
|
|
677
|
+
assert.equal(readReviewConsentLatch(cwd), true);
|
|
631
678
|
} else {
|
|
632
679
|
assert.equal(result.outcome, "consent-declined-this-candidate");
|
|
633
680
|
assert.equal(result.lineage_created, false);
|
|
634
|
-
assert.equal(result
|
|
681
|
+
assert.equal("actor_binding" in result, false);
|
|
682
|
+
assert.equal("result" in result, false);
|
|
683
|
+
assert.equal(readReviewConsentLatch(cwd), false);
|
|
635
684
|
}
|
|
636
685
|
await assert.rejects(() => answerConsent(controller, blocked.consent_binding, answer, headlessContext(cwd)), /unknown, expired, or already consumed/);
|
|
637
686
|
}
|
|
638
687
|
});
|
|
639
688
|
|
|
689
|
+
test("granted consent preserves the completed native start when local latch persistence fails", async (t) => {
|
|
690
|
+
const cwd = repository(t);
|
|
691
|
+
const { native, answers } = relayedConsentNative(cwd);
|
|
692
|
+
const notices: Array<{ message: string; type?: string }> = [];
|
|
693
|
+
let latchWrites = 0;
|
|
694
|
+
const { controller } = runtime(native, () => {
|
|
695
|
+
latchWrites += 1;
|
|
696
|
+
throw new Error("injected consent latch failure");
|
|
697
|
+
});
|
|
698
|
+
const blocked = await blockedConsent(controller, "consent-latch-failure", headlessContext(cwd, notices));
|
|
699
|
+
const result = await answerConsent(controller, blocked.consent_binding, "granted", headlessContext(cwd, notices));
|
|
700
|
+
|
|
701
|
+
assert.deepEqual(answers, ["granted"]);
|
|
702
|
+
assert.equal(latchWrites, 1);
|
|
703
|
+
assert.deepEqual(result.result, { lineage_id: "native-lineage", state: "reviewing", risk_tier: "high", selected_lenses: ["review-risk", "review-resilience", "review-readability", "review-reliability"], changed_files: 1, original_changed_lines: 1, correction_budget: 1, action: "created", lenses_required: true });
|
|
704
|
+
assert.equal((result.actor_binding as { workspace_root: string }).workspace_root, realpathSync(cwd));
|
|
705
|
+
assert.equal(readReviewConsentLatch(cwd), false);
|
|
706
|
+
assert.equal(notices.length, 1);
|
|
707
|
+
assert.equal(notices[0]?.type, "warning");
|
|
708
|
+
assert.match(notices[0]?.message ?? "", /native review start completed, but Pi could not record the local consent latch/i);
|
|
709
|
+
});
|
|
710
|
+
|
|
640
711
|
// Issue #247: a local binding mismatch was indistinguishable from a provider
|
|
641
712
|
// outage, so the reporter diagnosed a missing --cwd that Pi does forward.
|
|
642
713
|
test("a consent binding mismatch surfaces as an actionable local failure, not an opaque native operation failure", async (t) => {
|
|
@@ -657,6 +728,7 @@ test("a consent binding mismatch surfaces as an actionable local failure, not an
|
|
|
657
728
|
assert.equal(result.mutation_outcome, "none");
|
|
658
729
|
assert.equal(result.next_action, "resolve-consent-binding");
|
|
659
730
|
assert.deepEqual(answers, []);
|
|
731
|
+
assert.equal(readReviewConsentLatch(cwd), false);
|
|
660
732
|
});
|
|
661
733
|
|
|
662
734
|
test("consent follow-up rejects invalid token, unknown id, changed cwd, and changed target binding", async (t) => {
|
|
@@ -682,6 +754,17 @@ test("consent follow-up rejects invalid token, unknown id, changed cwd, and chan
|
|
|
682
754
|
(consent.choices[0] as { invocation: string }).invocation = consent.choices[0].invocation.replace("native-lineage", "changed-lineage");
|
|
683
755
|
await assert.rejects(() => answerConsent(controller, blocked.consent_binding, "granted", headlessContext(cwd)), /consent envelope binding changed/);
|
|
684
756
|
assert.deepEqual(answers, []);
|
|
757
|
+
assert.equal(readReviewConsentLatch(cwd), false);
|
|
758
|
+
});
|
|
759
|
+
|
|
760
|
+
test("unavailable consent follow-up leaves the clone latch unset", async (t) => {
|
|
761
|
+
const cwd = repository(t);
|
|
762
|
+
const { native } = relayedConsentNative(cwd);
|
|
763
|
+
delete native.answerConsent;
|
|
764
|
+
const { controller } = runtime(native);
|
|
765
|
+
const blocked = await blockedConsent(controller, "consent-unavailable", headlessContext(cwd));
|
|
766
|
+
await assert.rejects(() => answerConsent(controller, blocked.consent_binding, "granted", headlessContext(cwd)), /consent follow-up is unavailable/);
|
|
767
|
+
assert.equal(readReviewConsentLatch(cwd), false);
|
|
685
768
|
});
|
|
686
769
|
|
|
687
770
|
test("ambiguous consent mutation consumes the one-shot binding and requires status instead of blind replay", async (t) => {
|
|
@@ -696,11 +779,12 @@ test("ambiguous consent mutation consumes the one-shot binding and requires stat
|
|
|
696
779
|
await answerConsent(controller, blocked.consent_binding, "granted", headlessContext(cwd));
|
|
697
780
|
assert.equal(statusCalls, 2, "ambiguous consent must reconcile through target status");
|
|
698
781
|
await assert.rejects(() => answerConsent(controller, blocked.consent_binding, "granted", headlessContext(cwd)), /unknown, expired, or already consumed/);
|
|
782
|
+
assert.equal(readReviewConsentLatch(cwd), false);
|
|
699
783
|
});
|
|
700
784
|
|
|
701
785
|
test("session shutdown clears pending candidate consent bindings and is idempotent", async (t) => {
|
|
702
786
|
const cwd = repository(t);
|
|
703
|
-
const { native, answers } = relayedConsentNative(cwd);
|
|
787
|
+
const { native, answers, startRequests } = relayedConsentNative(cwd);
|
|
704
788
|
const { controller, events } = runtime(native);
|
|
705
789
|
const blocked = await blockedConsent(controller, "consent-before-shutdown", headlessContext(cwd));
|
|
706
790
|
const shutdown = events.get("session_shutdown");
|
|
@@ -709,6 +793,18 @@ test("session shutdown clears pending candidate consent bindings and is idempote
|
|
|
709
793
|
await shutdown({}, headlessContext(cwd));
|
|
710
794
|
await assert.rejects(() => answerConsent(controller, blocked.consent_binding, "granted", headlessContext(cwd)), /unknown, expired, or already consumed/);
|
|
711
795
|
assert.deepEqual(answers, []);
|
|
796
|
+
const afterShutdown = await blockedConsent(controller, "consent-after-shutdown", headlessContext(cwd));
|
|
797
|
+
assert.notEqual(afterShutdown.consent_binding, blocked.consent_binding, "the same candidate gets a fresh binding after shutdown cleanup");
|
|
798
|
+
assert.equal(startRequests.length, 2, "shutdown loss requires a fresh native START instead of local replay");
|
|
799
|
+
});
|
|
800
|
+
|
|
801
|
+
test("extension reload gives the same candidate a fresh consent binding instead of replaying lost local state", async (t) => {
|
|
802
|
+
const cwd = repository(t);
|
|
803
|
+
const { native, startRequests } = relayedConsentNative(cwd);
|
|
804
|
+
const beforeReload = await blockedConsent(runtime(native).controller, "consent-before-reload", headlessContext(cwd));
|
|
805
|
+
const afterReload = await blockedConsent(runtime(native).controller, "consent-after-reload", headlessContext(cwd));
|
|
806
|
+
assert.notEqual(afterReload.consent_binding, beforeReload.consent_binding);
|
|
807
|
+
assert.equal(startRequests.length, 2, "reload loss requires a fresh native START instead of local replay");
|
|
712
808
|
});
|
|
713
809
|
|
|
714
810
|
test("successful explicit disable clears pending candidate consent binding even after re-enable", async (t) => {
|
|
@@ -729,6 +825,90 @@ test("successful explicit disable clears pending candidate consent binding even
|
|
|
729
825
|
assert.deepEqual(answers, []);
|
|
730
826
|
});
|
|
731
827
|
|
|
828
|
+
// Issue #264: an unused consent binding and the candidate view retained
|
|
829
|
+
// exclusively for that binding must expire as one lifecycle unit before any
|
|
830
|
+
// later START may reuse the view. Cleanup is synchronous with respect to the
|
|
831
|
+
// observable TTL; the queued cleanup macrotask is a safety net, not the
|
|
832
|
+
// authority, so a fresh-candidate retry after expiry must get a new binding
|
|
833
|
+
// rather than tripping candidate-target-projection-drift on a stale view.
|
|
834
|
+
const REVIEW_CONSENT_TTL_MS = 10 * 60 * 1000;
|
|
835
|
+
|
|
836
|
+
function fakeConsentClock(): { now: () => number; scheduleTimer: (callback: () => void, delayMs: number) => { unref: () => void }; advance: (ms: number) => void; scheduled: Array<() => void> } {
|
|
837
|
+
const start = Date.now();
|
|
838
|
+
let nowMs = start;
|
|
839
|
+
const scheduled: Array<() => void> = [];
|
|
840
|
+
return {
|
|
841
|
+
now: () => nowMs,
|
|
842
|
+
// Never fires the callback: simulates a queued cleanup macrotask that
|
|
843
|
+
// has not yet gotten a turn on the event loop.
|
|
844
|
+
scheduleTimer: (callback) => { scheduled.push(callback); return { unref: () => {} }; },
|
|
845
|
+
advance: (ms) => { nowMs = start + ms; },
|
|
846
|
+
scheduled,
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
test("an expired unused binding prunes synchronously so a fresh-candidate retry gets a new binding, not candidate-target-projection-drift", async (t) => {
|
|
851
|
+
const cwd = repository(t);
|
|
852
|
+
const { native, startRequests } = relayedConsentNative(cwd);
|
|
853
|
+
const clock = fakeConsentClock();
|
|
854
|
+
const { controller } = runtime(native, recordReviewConsentLatch, { now: clock.now, scheduleTimer: clock.scheduleTimer });
|
|
855
|
+
// First START: consent-required, binding T1 retained with a queued cleanup timer.
|
|
856
|
+
const first = await blockedConsent(controller, "expired-fresh-1", headlessContext(cwd));
|
|
857
|
+
const firstBinding = first.consent_binding;
|
|
858
|
+
assert.equal(clock.scheduled.length, 1, "the TTL cleanup macrotask is queued for T1");
|
|
859
|
+
// The candidate changes: a later START on a FRESH candidate must not reuse T1.
|
|
860
|
+
writeFileSync(join(cwd, "app.ts"), "export const value = 3;\n");
|
|
861
|
+
// Advance time to the exact TTL boundary without letting the queued cleanup macrotask fire.
|
|
862
|
+
clock.advance(REVIEW_CONSENT_TTL_MS);
|
|
863
|
+
assert.equal(clock.scheduled.length, 1, "the queued cleanup macrotask has not fired");
|
|
864
|
+
const second = await execStart(controller, "expired-fresh-2", headlessContext(cwd));
|
|
865
|
+
// The fix: a fresh consent-required binding, not candidate-target-projection-drift.
|
|
866
|
+
assert.equal(second.status, "blocked");
|
|
867
|
+
assert.equal(second.outcome, "native-review-consent-required", "the stale view must not trip candidate-target-projection-drift");
|
|
868
|
+
assert.equal(typeof second.consent_binding, "string");
|
|
869
|
+
assert.notEqual(second.consent_binding, firstBinding, "a fresh candidate gets a fresh binding, not the stale one");
|
|
870
|
+
assert.equal(startRequests.length, 2, "native start was attempted for both candidates");
|
|
871
|
+
assert.equal(clock.scheduled.length, 2, "the fresh binding queued its own cleanup macrotask");
|
|
872
|
+
});
|
|
873
|
+
|
|
874
|
+
test("an expired same-candidate consent request creates a fresh binding instead of replaying local state", async (t) => {
|
|
875
|
+
const cwd = repository(t);
|
|
876
|
+
const { native, startRequests } = relayedConsentNative(cwd);
|
|
877
|
+
const clock = fakeConsentClock();
|
|
878
|
+
const { controller } = runtime(native, recordReviewConsentLatch, { now: clock.now, scheduleTimer: clock.scheduleTimer });
|
|
879
|
+
const first = await blockedConsent(controller, "same-candidate-expired-1", headlessContext(cwd));
|
|
880
|
+
clock.advance(REVIEW_CONSENT_TTL_MS);
|
|
881
|
+
const second = await blockedConsent(controller, "same-candidate-expired-2", headlessContext(cwd));
|
|
882
|
+
assert.notEqual(second.consent_binding, first.consent_binding);
|
|
883
|
+
assert.equal(startRequests.length, 2, "expiry requires a fresh native START instead of local replay");
|
|
884
|
+
assert.equal(clock.scheduled.length, 2, "the fresh binding queues its own cleanup macrotask");
|
|
885
|
+
});
|
|
886
|
+
|
|
887
|
+
test("a non-expired same-candidate consent request reuses the same binding instead of creating a new one", async (t) => {
|
|
888
|
+
const cwd = repository(t);
|
|
889
|
+
const { native } = relayedConsentNative(cwd);
|
|
890
|
+
const clock = fakeConsentClock();
|
|
891
|
+
const { controller } = runtime(native, recordReviewConsentLatch, { now: clock.now, scheduleTimer: clock.scheduleTimer });
|
|
892
|
+
const first = await blockedConsent(controller, "dedup-1", headlessContext(cwd));
|
|
893
|
+
// Same candidate, same consent, still within TTL: the existing binding is reused.
|
|
894
|
+
const second = await blockedConsent(controller, "dedup-2", headlessContext(cwd));
|
|
895
|
+
assert.equal(second.consent_binding, first.consent_binding, "a non-expired same-candidate request deduplicates the binding");
|
|
896
|
+
assert.equal(clock.scheduled.length, 1, "no new cleanup macrotask is queued when the binding is reused");
|
|
897
|
+
});
|
|
898
|
+
|
|
899
|
+
test("an expired consent binding is rejected on answer-consent even before the queued cleanup macrotask fires", async (t) => {
|
|
900
|
+
const cwd = repository(t);
|
|
901
|
+
const { native } = relayedConsentNative(cwd);
|
|
902
|
+
const clock = fakeConsentClock();
|
|
903
|
+
const { controller } = runtime(native, recordReviewConsentLatch, { now: clock.now, scheduleTimer: clock.scheduleTimer });
|
|
904
|
+
const blocked = await blockedConsent(controller, "expired-answer-1", headlessContext(cwd));
|
|
905
|
+
// Advance to the exact TTL boundary without firing the queued cleanup macrotask.
|
|
906
|
+
clock.advance(REVIEW_CONSENT_TTL_MS);
|
|
907
|
+
assert.equal(clock.scheduled.length, 1, "the queued cleanup macrotask has not fired");
|
|
908
|
+
await assert.rejects(() => answerConsent(controller, blocked.consent_binding, "granted", headlessContext(cwd)), /unknown, expired, or already consumed/);
|
|
909
|
+
assert.equal(clock.scheduled.length, 1, "rejection was synchronous from the TTL observation, not from a fired macrotask");
|
|
910
|
+
});
|
|
911
|
+
|
|
732
912
|
test("candidate-scoped decline creates no authority and the next candidate asks again", async (t) => {
|
|
733
913
|
const cwd = repository(t);
|
|
734
914
|
const { native, answers } = relayedConsentNative(cwd);
|
|
@@ -738,11 +918,34 @@ test("candidate-scoped decline creates no authority and the next candidate asks
|
|
|
738
918
|
const result = await answerConsent(controller, blocked.consent_binding, "declined", headlessContext(cwd));
|
|
739
919
|
assert.equal(result.outcome, "consent-declined-this-candidate");
|
|
740
920
|
assert.equal(result.lineage_created, false);
|
|
741
|
-
assert.equal(result
|
|
921
|
+
assert.equal("actor_binding" in result, false);
|
|
922
|
+
assert.equal("result" in result, false);
|
|
923
|
+
assert.equal(readReviewConsentLatch(cwd), false);
|
|
742
924
|
}
|
|
743
925
|
assert.deepEqual(answers, ["declined", "declined"]);
|
|
744
926
|
});
|
|
745
927
|
|
|
928
|
+
// Upstream gentle-ai.review-integration.consent/v3 consent is per-candidate:
|
|
929
|
+
// the clone-local latch records only that the one-time question was already
|
|
930
|
+
// put to the user, never that any later candidate is pre-approved. A fresh
|
|
931
|
+
// START with the latch already recorded must still relay the complete
|
|
932
|
+
// blocking envelope, and only an explicit ANSWER_CONSENT may resolve it.
|
|
933
|
+
test("a pre-recorded consent latch never suppresses a later candidate's consent envelope or auto-answers it", async (t) => {
|
|
934
|
+
const cwd = repository(t);
|
|
935
|
+
recordReviewConsentLatch(cwd);
|
|
936
|
+
assert.equal(readReviewConsentLatch(cwd), true);
|
|
937
|
+
const { native, answers, startRequests } = relayedConsentNative(cwd);
|
|
938
|
+
const { controller } = runtime(native);
|
|
939
|
+
const blocked = await blockedConsent(controller, "consent-latched-fresh-start", headlessContext(cwd));
|
|
940
|
+
assert.deepEqual(blocked.consent, candidateConsent(cwd).raw, "the full consent envelope is relayed despite the pre-recorded latch");
|
|
941
|
+
assert.deepEqual(answers, [], "the latch must never auto-answer a later candidate's consent");
|
|
942
|
+
assert.equal(blocked.lineage_created, false);
|
|
943
|
+
assert.equal(startRequests.length, 1);
|
|
944
|
+
const result = await answerConsent(controller, blocked.consent_binding, "granted", headlessContext(cwd));
|
|
945
|
+
assert.deepEqual(answers, ["granted"], "explicit ANSWER_CONSENT is still required to proceed");
|
|
946
|
+
assert.ok(result.result);
|
|
947
|
+
});
|
|
948
|
+
|
|
746
949
|
test("low-risk zero-lens START remains silent", async (t) => {
|
|
747
950
|
const cwd = repository(t);
|
|
748
951
|
const { native } = fakeOrganicNative({ riskEvidence: undefined, lensesRequired: false });
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import test from "node:test";
|
|
5
|
+
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// Native SDD Runtime Attempt Authority contract tests (issue #256 track 3).
|
|
8
|
+
//
|
|
9
|
+
// Locks the Pi-owned lazy-loaded orchestration/status contract that ports the
|
|
10
|
+
// released Gentle AI v2.4.0 compact SDD attempt ledger authority. Pi must NOT
|
|
11
|
+
// implement a local attempt runtime; it must route every runtime-bearing
|
|
12
|
+
// sdd-apply/sdd-verify/remediation launch through the provider compact CLI.
|
|
13
|
+
//
|
|
14
|
+
// Both assets are Markdown contracts consumed by orchestrators; these tests
|
|
15
|
+
// read the real repo files and assert the essential semantics, not merely
|
|
16
|
+
// keyword presence. Negative controls guard against caller-authored
|
|
17
|
+
// counters, OpenSpec/Engram attempt ledgers, automatic reset, and legacy
|
|
18
|
+
// status|begin|finish|reset normal-flow routing.
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
const ROOT = join(import.meta.dirname, "..");
|
|
22
|
+
const WORKFLOW = "assets/sdd-orchestrator-workflow.md";
|
|
23
|
+
const STATUS_CONTRACT = "assets/support/sdd-status-contract.md";
|
|
24
|
+
const SECTION = "Native Runtime Attempt Authority";
|
|
25
|
+
|
|
26
|
+
function read(path: string): string {
|
|
27
|
+
return readFileSync(join(ROOT, path), "utf8");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function readMarkdownSection(source: string, heading: string): string {
|
|
31
|
+
const lines = source.split(/\r?\n/);
|
|
32
|
+
const matches = lines.flatMap((line, index) => {
|
|
33
|
+
const match = line.match(/^(#{1,6})\s+(.+?)\s*$/);
|
|
34
|
+
return match?.[2] === heading ? [{ index, level: match[1].length }] : [];
|
|
35
|
+
});
|
|
36
|
+
assert.equal(
|
|
37
|
+
matches.length,
|
|
38
|
+
1,
|
|
39
|
+
`Markdown must contain exactly one "${heading}" section (found ${matches.length})`,
|
|
40
|
+
);
|
|
41
|
+
const [{ index: start, level }] = matches;
|
|
42
|
+
const relativeEnd = lines.slice(start + 1).findIndex((line) => {
|
|
43
|
+
const match = line.match(/^(#{1,6})\s+/);
|
|
44
|
+
return match !== null && match[1].length <= level;
|
|
45
|
+
});
|
|
46
|
+
const end = relativeEnd === -1 ? lines.length : start + 1 + relativeEnd;
|
|
47
|
+
return lines.slice(start + 1, end).join("\n").trim();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Extracts one command line beginning with `commandPrefix` so payload args bind
|
|
51
|
+
// to the correct command shape; `--cwd`, `--change`, `--request-id` appear in
|
|
52
|
+
// both acquire and settle, so a section-wide match stays green on arg removal.
|
|
53
|
+
function extractCommandLine(section: string, commandPrefix: string): string {
|
|
54
|
+
const escaped = commandPrefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
55
|
+
const re = new RegExp(`^[ \t]*${escaped}.*$`, "gm");
|
|
56
|
+
const matches = section.match(re) ?? [];
|
|
57
|
+
assert.equal(
|
|
58
|
+
matches.length,
|
|
59
|
+
1,
|
|
60
|
+
`Native Runtime Attempt Authority section must contain exactly one command line starting with "${commandPrefix}" (found ${matches.length})`,
|
|
61
|
+
);
|
|
62
|
+
const [line] = matches;
|
|
63
|
+
return line;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
test("extractCommandLine rejects duplicate matching command lines (negative control)", () => {
|
|
67
|
+
for (const prefix of [
|
|
68
|
+
"gentle-ai sdd-attempt acquire ",
|
|
69
|
+
"gentle-ai sdd-attempt settle ",
|
|
70
|
+
]) {
|
|
71
|
+
const duplicate = [
|
|
72
|
+
`${prefix}--cwd repo --change change --request-id id`,
|
|
73
|
+
`${prefix}--cwd repo2 --change change2 --request-id id2`,
|
|
74
|
+
].join("\n");
|
|
75
|
+
assert.throws(
|
|
76
|
+
() => extractCommandLine(duplicate, prefix),
|
|
77
|
+
/exactly one command line/,
|
|
78
|
+
`duplicate ${prefix.trim()} lines must be rejected`,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("workflow asset has exactly one Native Runtime Attempt Authority section", () => {
|
|
84
|
+
const section = readMarkdownSection(read(WORKFLOW), SECTION);
|
|
85
|
+
assert.ok(section.length > 0, "section must be non-empty");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("status contract asset has exactly one Native Runtime Attempt Authority section", () => {
|
|
89
|
+
const section = readMarkdownSection(read(STATUS_CONTRACT), SECTION);
|
|
90
|
+
assert.ok(section.length > 0, "section must be non-empty");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("workflow names acquire and settle as sole provider authority for OpenSpec and Engram", () => {
|
|
94
|
+
const section = readMarkdownSection(read(WORKFLOW), SECTION);
|
|
95
|
+
assert.match(section, /sdd-attempt acquire/);
|
|
96
|
+
assert.match(section, /sdd-attempt settle/);
|
|
97
|
+
assert.match(section, /sole attempt and changed-line budget authority/i);
|
|
98
|
+
assert.match(section, /Git-common-dir/);
|
|
99
|
+
assert.match(section, /OpenSpec and Engram/);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("status contract names acquire and settle and forbids OpenSpec/Engram attempt ledgers", () => {
|
|
103
|
+
const section = readMarkdownSection(read(STATUS_CONTRACT), SECTION);
|
|
104
|
+
assert.match(section, /acquire/);
|
|
105
|
+
assert.match(section, /settle/);
|
|
106
|
+
assert.match(section, /OpenSpec or Engram attempt ledger/i);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("workflow launches only on proceed; blocked and complete stop the launch", () => {
|
|
110
|
+
const section = readMarkdownSection(read(WORKFLOW), SECTION);
|
|
111
|
+
assert.match(section, /proceed/);
|
|
112
|
+
assert.match(section, /blocked/);
|
|
113
|
+
assert.match(section, /complete/);
|
|
114
|
+
assert.match(section, /launch only on `proceed`/i);
|
|
115
|
+
assert.match(section, /blocked.*do not launch/i);
|
|
116
|
+
assert.match(section, /complete.*do not launch/i);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("workflow requires distinct request IDs with own-ID-only idempotent replay", () => {
|
|
120
|
+
const section = readMarkdownSection(read(WORKFLOW), SECTION);
|
|
121
|
+
assert.match(section, /distinct from acquire/i);
|
|
122
|
+
assert.match(section, /own ID only for idempotent replay/i);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("workflow acquire command line binds every mandatory payload argument", () => {
|
|
126
|
+
const section = readMarkdownSection(read(WORKFLOW), SECTION);
|
|
127
|
+
const acquire = extractCommandLine(section, "gentle-ai sdd-attempt acquire ");
|
|
128
|
+
for (const arg of [
|
|
129
|
+
"--cwd <repo>",
|
|
130
|
+
"--change <change>",
|
|
131
|
+
"--request-id <id>",
|
|
132
|
+
"--work-unit <label>",
|
|
133
|
+
"--evidence-goal <goal>",
|
|
134
|
+
"--max-attempts <count>",
|
|
135
|
+
"--max-changed-lines <count>",
|
|
136
|
+
]) {
|
|
137
|
+
assert.ok(acquire.includes(arg), `acquire command is missing ${arg}; command: ${acquire}`);
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("workflow settle command line binds mandatory arguments and routing invariants", () => {
|
|
142
|
+
const section = readMarkdownSection(read(WORKFLOW), SECTION);
|
|
143
|
+
const settle = extractCommandLine(section, "gentle-ai sdd-attempt settle ");
|
|
144
|
+
for (const arg of [
|
|
145
|
+
"--cwd <repo>",
|
|
146
|
+
"--change <change>",
|
|
147
|
+
"--token <token>",
|
|
148
|
+
"--request-id <id>",
|
|
149
|
+
"--outcome <failed|interrupted|passed>",
|
|
150
|
+
"--evidence-revision <sha256:...>",
|
|
151
|
+
"--diagnosis <text>",
|
|
152
|
+
"--harness-disposition <reused|invalidated>",
|
|
153
|
+
"--cleanup-evidence <text>",
|
|
154
|
+
"--process-evidence <text>",
|
|
155
|
+
]) {
|
|
156
|
+
assert.ok(settle.includes(arg), `settle command is missing ${arg}; command: ${settle}`);
|
|
157
|
+
}
|
|
158
|
+
assert.match(section, /never `none`/);
|
|
159
|
+
assert.match(section, /proceed\|blocked\|complete/);
|
|
160
|
+
assert.match(section, /--successor-lineage/);
|
|
161
|
+
assert.match(section, /--remediates-evidence-revision/);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("workflow forbids caller-authored counters and Pi-owned attempt state", () => {
|
|
165
|
+
const section = readMarkdownSection(read(WORKFLOW), SECTION);
|
|
166
|
+
assert.match(section, /never persist caller-authored/i);
|
|
167
|
+
assert.match(section, /OpenSpec artifacts, Engram memory/);
|
|
168
|
+
assert.match(section, /counter|token store|state machine|interception/i);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("workflow states reset is never automatic and names compatibility surfaces", () => {
|
|
172
|
+
const section = readMarkdownSection(read(WORKFLOW), SECTION);
|
|
173
|
+
assert.match(section, /`status`, `begin`, `finish`, and `reset`/);
|
|
174
|
+
assert.match(section, /diagnostic\/compatibility surfaces/);
|
|
175
|
+
assert.match(section, /not the normal runtime route/i);
|
|
176
|
+
assert.match(section, /reset.*never automatic/i);
|
|
177
|
+
assert.match(section, /explicit maintainer scope decision/);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test("workflow gatekeeper rerun is subordinate to a fresh native acquire", () => {
|
|
181
|
+
const section = readMarkdownSection(read(WORKFLOW), SECTION);
|
|
182
|
+
assert.match(section, /rerun never bypasses native attempt authority/i);
|
|
183
|
+
assert.match(section, /fresh compact acquire/i);
|
|
184
|
+
// The gatekeeper quality rule is preserved (not deleted).
|
|
185
|
+
const gatekeeper = readMarkdownSection(read(WORKFLOW), "Automatic Mode Gatekeeper");
|
|
186
|
+
assert.match(gatekeeper, /rerun the same SDD phase once with corrective feedback/);
|
|
187
|
+
assert.match(gatekeeper, /Validate the rerun/);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test("workflow does not present status|begin|finish|reset as the primary route", () => {
|
|
191
|
+
const section = readMarkdownSection(read(WORKFLOW), SECTION);
|
|
192
|
+
assert.match(section, /not the normal runtime route/i);
|
|
193
|
+
assert.doesNotMatch(
|
|
194
|
+
section,
|
|
195
|
+
/primary route.*sdd-attempt (?:status|begin|finish|reset)/i,
|
|
196
|
+
);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test("status contract authority is artifact-store agnostic and excluded from SDD v1 status", () => {
|
|
200
|
+
const section = readMarkdownSection(read(STATUS_CONTRACT), SECTION);
|
|
201
|
+
assert.match(section, /artifact-store agnostic/i);
|
|
202
|
+
assert.match(section, /MUST NOT be embedded in the SDD v1 status/i);
|
|
203
|
+
assert.match(section, /separate from artifact dispatch/i);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test("status contract continuation rule: acquire before launch, settle after run, distinct IDs, provider states route, reset never automatic", () => {
|
|
207
|
+
const section = readMarkdownSection(read(STATUS_CONTRACT), SECTION);
|
|
208
|
+
assert.match(section, /before every runtime-bearing/i);
|
|
209
|
+
assert.match(section, /after the external run completes it MUST settle/i);
|
|
210
|
+
assert.match(section, /distinct/i);
|
|
211
|
+
assert.match(section, /proceed\|blocked\|complete/);
|
|
212
|
+
assert.match(section, /launch only on `proceed`/i);
|
|
213
|
+
assert.match(section, /reset.*never automatic/i);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test("status contract schema is unchanged (no attempt authority payload added)", () => {
|
|
217
|
+
const schema = readMarkdownSection(read(STATUS_CONTRACT), "Status Schema");
|
|
218
|
+
assert.doesNotMatch(
|
|
219
|
+
schema,
|
|
220
|
+
/attemptToken|attempt_token|attemptCount|attempt_counter|sddAttempt|sdd_attempt|nativeAttempt/i,
|
|
221
|
+
"Status Schema must not embed the runtime attempt authority payload",
|
|
222
|
+
);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test("status contract uses a runtime-safe semantic reference, not a repo-source assets/ path", () => {
|
|
226
|
+
const section = readMarkdownSection(read(STATUS_CONTRACT), SECTION);
|
|
227
|
+
assert.doesNotMatch(
|
|
228
|
+
section,
|
|
229
|
+
/See `assets\/sdd-orchestrator-workflow\.md`|see `assets\//i,
|
|
230
|
+
"status contract must not point installed consumers at a repo-source assets/ path; those are package source paths before installation",
|
|
231
|
+
);
|
|
232
|
+
assert.match(section, /lazy-loaded `SDD Orchestrator Workflow`/i);
|
|
233
|
+
assert.match(section, /Native Runtime Attempt Authority/);
|
|
234
|
+
assert.match(section, /Do not look up `assets\/\.\.\.` paths at runtime/i);
|
|
235
|
+
});
|