Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions assets/sdd-orchestrator-workflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,40 @@ On gate pass, continue automatically to the next phase. On gate fail, rerun the

The gatekeeper is additive: it does not relax the Review Workload Guard, Strict TDD Forwarding, native status dependency checks, or mandatory delegation rules. It never creates a post-SDD review pass.

## Native Runtime Attempt Authority

The package-local Gentle AI runtime owns the Git-common-dir compact SDD attempt ledger. It is the sole attempt and changed-line budget authority for both OpenSpec and Engram flows on Pi. Pi must not implement a local attempt mirror, counter, token store, state machine, or extension interception layer; such code would duplicate provider authority and could not truthfully settle all runs.

Before every runtime-bearing `sdd-apply`, `sdd-verify`, or remediation actor/harness launch, the orchestrator MUST call the compact acquire:

```text
gentle-ai sdd-attempt acquire --cwd <repo> --change <change> --request-id <id> --work-unit <label> --evidence-goal <goal> --max-attempts <count> --max-changed-lines <count>
```

Pass `--token` only to continue an active attempt; pass `--remediates-evidence-revision` only for an unmanaged remediation. Do not invent continuation or remediation state the provider has not returned.

The provider returns exactly one routing state from `proceed|blocked|complete`:

- `proceed`: launch only on `proceed`; retain the opaque token for settle.
- `blocked`: do not launch; stop and report.
- `complete`: do not launch; the objective is settled.

Never persist caller-authored attempt counters, tokens, or state in OpenSpec artifacts, Engram memory, prompts, or any Pi-owned state.

After the external run completes, call the compact settle with a request ID distinct from acquire, reusing an operation's own ID only for idempotent replay of that exact operation:

```text
gentle-ai sdd-attempt settle --cwd <repo> --change <change> --token <token> --request-id <id> --outcome <failed|interrupted|passed> --evidence-revision <sha256:...> --diagnosis <text> --harness-disposition <reused|invalidated> --cleanup-evidence <text> --process-evidence <text>
```

Every settle field is required: `cwd`, `change`, `token`, `request-id`, `outcome`, `evidence-revision`, `diagnosis`, `harness-disposition`, `cleanup-evidence`, and `process-evidence`. `evidence-revision` is never `none`. Pass `--successor-lineage` only for a distinct approved successor; the current/bound lineage remains itself otherwise. Pass `--remediates-evidence-revision` only when repairing a specific failed evidence revision. Settle derives binding and remediation inputs; the orchestrator never invents them.

`status`, `begin`, `finish`, and `reset` are diagnostic/compatibility surfaces, not the normal runtime route. Route continuation only from the provider-returned `proceed|blocked|complete`. `reset` is never automatic and requires an explicit maintainer scope decision.

### Gatekeeper Reconciliation

The Automatic Mode Gatekeeper one-rerun rule above is a quality gate, not a launch authorization. A rerun never bypasses native attempt authority: every rerun still requires a fresh compact acquire, and the rerun must stop immediately if the provider returns `blocked` or `complete`. The gatekeeper quality rule is preserved and remains subordinate to this authority.

## SDD Phase Delegation Mode

Launch SDD phase subagents with `subagent_run` `mode: "task"` when the parent needs the phase result to route the next step. Do not use `mode: "background"` for SDD phases that must feed continuation; background completion is a notification/history mechanism, not an orchestration resume guarantee.
Expand Down
8 changes: 8 additions & 0 deletions assets/support/sdd-status-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,14 @@ The orchestrator MUST carry `actionContext` into any phase launch.
- `openspec` and `both` (when `openspec/` directory exists): the native status engine resolves artifact state from disk and is authoritative. Phase executors must obey it.
- `engram`, `none`, and `both` (when `openspec/` directory does NOT exist): the native status engine cannot read Engram artifacts. It returns `nextRecommended: "resolve-via-engram"` and empty `blockedReasons`. This output is **non-authoritative**. The orchestrator must resolve readiness directly from Engram using the Engram memory tools injected by the memory provider on the change topic keys (`sdd/{change-name}/proposal`, `sdd/{change-name}/spec`, etc.) instead of relying on the engine's dependency states. The `artifactStore` field still reflects the real chosen store value (e.g. `"both"`) and must not be rewritten.

## Native Runtime Attempt Authority

The compact SDD runtime attempt authority is separate from artifact dispatch and status. It is artifact-store agnostic: the same acquire/settle discipline applies to `openspec`, `engram`, `both`, and `none` stores. Its payload MUST NOT be embedded in the SDD v1 status schema above; status reports artifact state only, never attempt tokens or attempt counters. No OpenSpec or Engram attempt ledger may be created or mirrored by Pi.

Before every runtime-bearing `sdd-apply`, `sdd-verify`, or remediation launch, the orchestrator MUST acquire a bounded attempt from the provider compact CLI; after the external run completes it MUST settle. The acquire and settle request IDs are distinct; an operation's own request ID is reused only for idempotent replay of that exact operation. Continuation routes only from the provider-returned `proceed|blocked|complete` — launch only on `proceed`, stop on `blocked` or `complete`. `reset` is never automatic and requires an explicit maintainer scope decision.

For the exact compact acquire/settle shapes and the full field semantics, see the `Native Runtime Attempt Authority` section of the lazy-loaded `SDD Orchestrator Workflow` contract. Do not look up `assets/...` paths at runtime; those are package source paths before installation.

## Status Output

Every command or agent that acts on a change MUST show or consume status before doing phase work:
Expand Down
235 changes: 235 additions & 0 deletions tests/native-sdd-attempt-authority.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { join } from "node:path";
import test from "node:test";

// ---------------------------------------------------------------------------
// Native SDD Runtime Attempt Authority contract tests (issue #256 track 3).
//
// Locks the Pi-owned lazy-loaded orchestration/status contract that ports the
// released Gentle AI v2.2.3 compact SDD attempt ledger authority. Pi must NOT
// implement a local attempt runtime; it must route every runtime-bearing
// sdd-apply/sdd-verify/remediation launch through the provider compact CLI.
//
// Both assets are Markdown contracts consumed by orchestrators; these tests
// read the real repo files and assert the essential semantics, not merely
// keyword presence. Negative controls guard against caller-authored
// counters, OpenSpec/Engram attempt ledgers, automatic reset, and legacy
// status|begin|finish|reset normal-flow routing.
// ---------------------------------------------------------------------------

const ROOT = join(import.meta.dirname, "..");
const WORKFLOW = "assets/sdd-orchestrator-workflow.md";
const STATUS_CONTRACT = "assets/support/sdd-status-contract.md";
const SECTION = "Native Runtime Attempt Authority";

function read(path: string): string {
return readFileSync(join(ROOT, path), "utf8");
}

function readMarkdownSection(source: string, heading: string): string {
const lines = source.split(/\r?\n/);
const matches = lines.flatMap((line, index) => {
const match = line.match(/^(#{1,6})\s+(.+?)\s*$/);
return match?.[2] === heading ? [{ index, level: match[1].length }] : [];
});
assert.equal(
matches.length,
1,
`Markdown must contain exactly one "${heading}" section (found ${matches.length})`,
);
const [{ index: start, level }] = matches;
const relativeEnd = lines.slice(start + 1).findIndex((line) => {
const match = line.match(/^(#{1,6})\s+/);
return match !== null && match[1].length <= level;
});
const end = relativeEnd === -1 ? lines.length : start + 1 + relativeEnd;
return lines.slice(start + 1, end).join("\n").trim();
}

// Extracts one command line beginning with `commandPrefix` so payload args bind
// to the correct command shape; `--cwd`, `--change`, `--request-id` appear in
// both acquire and settle, so a section-wide match stays green on arg removal.
function extractCommandLine(section: string, commandPrefix: string): string {
const escaped = commandPrefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const re = new RegExp(`^[ \t]*${escaped}.*$`, "gm");
const matches = section.match(re) ?? [];
assert.equal(
matches.length,
1,
`Native Runtime Attempt Authority section must contain exactly one command line starting with "${commandPrefix}" (found ${matches.length})`,
);
const [line] = matches;
return line;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

test("extractCommandLine rejects duplicate matching command lines (negative control)", () => {
for (const prefix of [
"gentle-ai sdd-attempt acquire ",
"gentle-ai sdd-attempt settle ",
]) {
const duplicate = [
`${prefix}--cwd repo --change change --request-id id`,
`${prefix}--cwd repo2 --change change2 --request-id id2`,
].join("\n");
assert.throws(
() => extractCommandLine(duplicate, prefix),
/exactly one command line/,
`duplicate ${prefix.trim()} lines must be rejected`,
);
}
});

test("workflow asset has exactly one Native Runtime Attempt Authority section", () => {
const section = readMarkdownSection(read(WORKFLOW), SECTION);
assert.ok(section.length > 0, "section must be non-empty");
});

test("status contract asset has exactly one Native Runtime Attempt Authority section", () => {
const section = readMarkdownSection(read(STATUS_CONTRACT), SECTION);
assert.ok(section.length > 0, "section must be non-empty");
});

test("workflow names acquire and settle as sole provider authority for OpenSpec and Engram", () => {
const section = readMarkdownSection(read(WORKFLOW), SECTION);
assert.match(section, /sdd-attempt acquire/);
assert.match(section, /sdd-attempt settle/);
assert.match(section, /sole attempt and changed-line budget authority/i);
assert.match(section, /Git-common-dir/);
assert.match(section, /OpenSpec and Engram/);
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.

test("status contract names acquire and settle and forbids OpenSpec/Engram attempt ledgers", () => {
const section = readMarkdownSection(read(STATUS_CONTRACT), SECTION);
assert.match(section, /acquire/);
assert.match(section, /settle/);
assert.match(section, /OpenSpec or Engram attempt ledger/i);
});

test("workflow launches only on proceed; blocked and complete stop the launch", () => {
const section = readMarkdownSection(read(WORKFLOW), SECTION);
assert.match(section, /proceed/);
assert.match(section, /blocked/);
assert.match(section, /complete/);
assert.match(section, /launch only on `proceed`/i);
assert.match(section, /blocked.*do not launch/i);
assert.match(section, /complete.*do not launch/i);
});

test("workflow requires distinct request IDs with own-ID-only idempotent replay", () => {
const section = readMarkdownSection(read(WORKFLOW), SECTION);
assert.match(section, /distinct from acquire/i);
assert.match(section, /own ID only for idempotent replay/i);
});

test("workflow acquire command line binds every mandatory payload argument", () => {
const section = readMarkdownSection(read(WORKFLOW), SECTION);
const acquire = extractCommandLine(section, "gentle-ai sdd-attempt acquire ");
for (const arg of [
"--cwd <repo>",
"--change <change>",
"--request-id <id>",
"--work-unit <label>",
"--evidence-goal <goal>",
"--max-attempts <count>",
"--max-changed-lines <count>",
]) {
assert.ok(acquire.includes(arg), `acquire command is missing ${arg}; command: ${acquire}`);
}
});

test("workflow settle command line binds mandatory arguments and routing invariants", () => {
const section = readMarkdownSection(read(WORKFLOW), SECTION);
const settle = extractCommandLine(section, "gentle-ai sdd-attempt settle ");
for (const arg of [
"--cwd <repo>",
"--change <change>",
"--token <token>",
"--request-id <id>",
"--outcome <failed|interrupted|passed>",
"--evidence-revision <sha256:...>",
"--diagnosis <text>",
"--harness-disposition <reused|invalidated>",
"--cleanup-evidence <text>",
"--process-evidence <text>",
]) {
assert.ok(settle.includes(arg), `settle command is missing ${arg}; command: ${settle}`);
}
assert.match(section, /never `none`/);
assert.match(section, /proceed\|blocked\|complete/);
assert.match(section, /--successor-lineage/);
assert.match(section, /--remediates-evidence-revision/);
});

test("workflow forbids caller-authored counters and Pi-owned attempt state", () => {
const section = readMarkdownSection(read(WORKFLOW), SECTION);
assert.match(section, /never persist caller-authored/i);
assert.match(section, /OpenSpec artifacts, Engram memory/);
assert.match(section, /counter|token store|state machine|interception/i);
});

test("workflow states reset is never automatic and names compatibility surfaces", () => {
const section = readMarkdownSection(read(WORKFLOW), SECTION);
assert.match(section, /`status`, `begin`, `finish`, and `reset`/);
assert.match(section, /diagnostic\/compatibility surfaces/);
assert.match(section, /not the normal runtime route/i);
assert.match(section, /reset.*never automatic/i);
assert.match(section, /explicit maintainer scope decision/);
});

test("workflow gatekeeper rerun is subordinate to a fresh native acquire", () => {
const section = readMarkdownSection(read(WORKFLOW), SECTION);
assert.match(section, /rerun never bypasses native attempt authority/i);
assert.match(section, /fresh compact acquire/i);
// The gatekeeper quality rule is preserved (not deleted).
const gatekeeper = readMarkdownSection(read(WORKFLOW), "Automatic Mode Gatekeeper");
assert.match(gatekeeper, /rerun the same SDD phase once with corrective feedback/);
assert.match(gatekeeper, /Validate the rerun/);
});

test("workflow does not present status|begin|finish|reset as the primary route", () => {
const section = readMarkdownSection(read(WORKFLOW), SECTION);
assert.match(section, /not the normal runtime route/i);
assert.doesNotMatch(
section,
/primary route.*sdd-attempt (?:status|begin|finish|reset)/i,
);
});

test("status contract authority is artifact-store agnostic and excluded from SDD v1 status", () => {
const section = readMarkdownSection(read(STATUS_CONTRACT), SECTION);
assert.match(section, /artifact-store agnostic/i);
assert.match(section, /MUST NOT be embedded in the SDD v1 status/i);
assert.match(section, /separate from artifact dispatch/i);
});

test("status contract continuation rule: acquire before launch, settle after run, distinct IDs, provider states route, reset never automatic", () => {
const section = readMarkdownSection(read(STATUS_CONTRACT), SECTION);
assert.match(section, /before every runtime-bearing/i);
assert.match(section, /after the external run completes it MUST settle/i);
assert.match(section, /distinct/i);
assert.match(section, /proceed\|blocked\|complete/);
assert.match(section, /launch only on `proceed`/i);
assert.match(section, /reset.*never automatic/i);
});

test("status contract schema is unchanged (no attempt authority payload added)", () => {
const schema = readMarkdownSection(read(STATUS_CONTRACT), "Status Schema");
assert.doesNotMatch(
schema,
/attemptToken|attempt_token|attemptCount|attempt_counter|sddAttempt|sdd_attempt|nativeAttempt/i,
"Status Schema must not embed the runtime attempt authority payload",
);
});

test("status contract uses a runtime-safe semantic reference, not a repo-source assets/ path", () => {
const section = readMarkdownSection(read(STATUS_CONTRACT), SECTION);
assert.doesNotMatch(
section,
/See `assets\/sdd-orchestrator-workflow\.md`|see `assets\//i,
"status contract must not point installed consumers at a repo-source assets/ path; those are package source paths before installation",
);
assert.match(section, /lazy-loaded `SDD Orchestrator Workflow`/i);
assert.match(section, /Native Runtime Attempt Authority/);
assert.match(section, /Do not look up `assets\/\.\.\.` paths at runtime/i);
});
Loading