@decidio/sdk 0.6.4 → 0.8.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.
@@ -1,19 +1,70 @@
1
1
  import { interrupt, Command } from "@langchain/langgraph";
2
- import { DecidioBlockedError } from "../index.js";
3
- import { gateAuthorize, gateConfirm, verdictFrom, assertVerdict, checkEngineVersion } from "./_gate.js";
2
+ import * as langgraph from "@langchain/langgraph";
3
+ import { DecidioBlockedError, approvalRefusal, bindingMaterial, requestCommitment, idempotencyKey, decisionIdProblem } from "../index.js";
4
+ import { gateAuthorize, gateConfirm, gateCheckApproval, adapterRequest, checkEngineVersion } from "./_gate.js";
5
+ const gatesOpened = new WeakMap();
6
+ function runIdentity() {
7
+ let conf;
8
+ try {
9
+ const getConfig = langgraph.getConfig;
10
+ conf = typeof getConfig === "function" ? getConfig()?.configurable : undefined;
11
+ }
12
+ catch {
13
+ return undefined;
14
+ }
15
+ const thread = conf?.["thread_id"], ns = conf?.["checkpoint_ns"];
16
+ if ((typeof thread !== "string" && typeof thread !== "number") || String(thread) === "")
17
+ return undefined;
18
+ if (typeof ns !== "string" || ns === "")
19
+ return undefined;
20
+ const scratch = conf?.["__pregel_scratchpad"];
21
+ if (!scratch || typeof scratch !== "object")
22
+ return undefined;
23
+ const call = gatesOpened.get(scratch) ?? 0;
24
+ gatesOpened.set(scratch, call + 1);
25
+ return { thread: String(thread), ns, call };
26
+ }
4
27
  export async function decidioGate(config, ctx, run, opts) {
5
- const auth = await gateAuthorize(config, ctx);
28
+ const material = bindingMaterial(config);
29
+ if (!material) {
30
+ throw new Error("[decidio] the LangGraph adapter binds a routed approval to the exact request with a key only this agent holds, and none is configured - set DECIDIO_AGENT_KEY + DECIDIO_AGENT_DID (npx @decidio/sdk init) or DECIDIO_API_TOKEN. Nothing was routed or executed.");
31
+ }
32
+ const request = adapterRequest(ctx);
33
+ const self = runIdentity();
34
+ const key = self ? idempotencyKey(material, { thread: self.thread, ns: self.ns, call: self.call, commitment: requestCommitment(material, request) }) : undefined;
35
+ const auth = await gateAuthorize(config, ctx, key ? { idempotencyKey: key } : {});
6
36
  if (auth.decision === "block")
7
37
  throw new DecidioBlockedError(auth.reason ?? "blocked by policy", auth.decisionId);
8
38
  if (auth.decision === "route") {
9
- const resumed = interrupt({ decidio: { decisionId: auth.decisionId, action: ctx.action, amount: ctx.amount, scope: ctx.scope } });
10
- assertVerdict(verdictFrom(resumed), auth.decisionId, resumed?.reason);
39
+ if (!self) {
40
+ throw new DecidioBlockedError(`this LangGraph run exposes no thread/task identity (a checkpointer with a thread_id, @langchain/langgraph's getConfig() and the node execution's scratchpad are required), so a resumed node could not recognise decision ${auth.decisionId} as its own. Nothing was executed; ${auth.decisionId} is pending in Decidio - reject it (npx @decidio/sdk approvals reject ${auth.decisionId}).`, auth.decisionId);
41
+ }
42
+ const payload = { decidio: { decisionId: auth.decisionId, action: ctx.action, amount: ctx.amount, scope: ctx.scope } };
43
+ let resumed = interrupt(payload);
44
+ for (;;) {
45
+ const named = resumed && typeof resumed === "object" ? resumed.decisionId : undefined;
46
+ if (typeof named !== "string" || named === "") {
47
+ throw new DecidioBlockedError(`the LangGraph resume value names no decision (${JSON.stringify(resumed ?? null).slice(0, 120)}) - refusing to execute. Build it with decidioResumeCommand(webhookBody), which carries the decisionId: an approval that does not say which decision it approves cannot be applied to this one.`, auth.decisionId);
48
+ }
49
+ if (named === auth.decisionId) {
50
+ const check = await gateCheckApproval(config, auth.decisionId, ctx);
51
+ if (check.ok)
52
+ break;
53
+ if (check.code !== "pending")
54
+ throw approvalRefusal(check);
55
+ }
56
+ resumed = interrupt(payload);
57
+ }
11
58
  }
12
59
  const result = await run();
13
60
  const confirmation = await gateConfirm(config, auth.decisionId, result, opts?.evidence);
14
61
  return { value: result, decisionId: auth.decisionId, confirmation };
15
62
  }
16
63
  export function decidioResumeCommand(body) {
17
- return new Command({ resume: { verdict: body.verdict, reason: body.reason } });
64
+ const decisionId = body?.decisionId;
65
+ const problem = decisionIdProblem(decisionId);
66
+ if (problem)
67
+ throw new Error(`[decidio] decidioResumeCommand needs the webhook's decisionId (${problem}) - the node refuses a resume that does not say which decision it approves. Pass the webhook body verbatim.`);
68
+ return new Command({ resume: { decisionId, verdict: body.verdict, reason: body.reason } });
18
69
  }
19
70
  checkEngineVersion("langgraph");
@@ -16,10 +16,27 @@ interface RunStateLike {
16
16
  *
17
17
  * const state = result.state; // or RunState from the run
18
18
  * const { resolved, pending } = await authorizeInterruptions(config, state, describe);
19
- * if (pending.length) { await store.put(decisionId, state.toString()); return; } // suspend
19
+ * if (pending.length) { // suspend: park the state + each {callId, decisionId}
20
+ * await store.put(runId, { state: state.toString(), pending: pending.map(({ callId, decisionId }) => ({ callId, decisionId })) });
21
+ * return;
22
+ * }
20
23
  * result = await run(agent, state); // all resolved → resume
21
- * // on Decidio approval webhook: state = await RunState.fromString(agent, parked);
22
- * // applyResume(state, item, verdict); result = await run(agent, state);
24
+ * // on Decidio's webhook (a wake-up): restore, find EACH parked call in the restored state by its call id,
25
+ * // and let Decidio decide it:
26
+ * // const parked = await store.get(runId); const state = await RunState.fromString(agent, parked.state);
27
+ * // for (const { callId, decisionId } of parked.pending) {
28
+ * // const item = state.getInterruptions().find((i) => i.rawItem?.callId === callId);
29
+ * // await applyVerifiedResume(config, state, item, decisionId, describe);
30
+ * // }
31
+ * // result = await run(agent, state);
32
+ *
33
+ * THE BINDING IS THE TOOL CALL (#4B review W2): a routed approval is bound to the described context AND
34
+ * the call itself - its name, call id and exact argument string (toolCallRequest). An item edited after
35
+ * approval, or a second identical call (another call id), never rides the approval.
36
+ *
37
+ * KNOWN LIMIT (predates 0.7.0): this door has no single-execution claim. The RunState is the host's own
38
+ * storage, so two concurrent resumes of the same parked state can both apply the approval and both run
39
+ * the tool; serialize resumes per run on your side (one worker per run id, or a lock).
23
40
  *
24
41
  * Duck-typed on the state/item (no hard @openai/agents import) so it stays thin.
25
42
  *
@@ -30,9 +47,12 @@ export interface Resolved {
30
47
  verdict: "approved" | "blocked";
31
48
  decisionId: string;
32
49
  }
50
+ /** `callId` (0.7.0) is the tool call the approval is bound to - find the item again by it after
51
+ * RunState.fromString. */
33
52
  export interface Pending {
34
53
  item: ToolApprovalItem;
35
54
  decisionId: string;
55
+ callId: string;
36
56
  }
37
57
  /** THIS DOOR AUTHORIZES; IT DOES NOT CONFIRM. Named `gateInterruptions` it looked like the other
38
58
  * `decidioGate*` functions, which all run the action and report what happened. This one cannot:
@@ -42,7 +62,7 @@ export interface Pending {
42
62
  * and a failed confirmation must not look alike.
43
63
  *
44
64
  * proceed → state.approve(item); block → state.reject(item); route → pending (a human decides via
45
- * Decidio; the host parks state.toString() + resumes via applyResume). */
65
+ * Decidio; the host parks state.toString() + resumes via applyVerifiedResume). */
46
66
  export declare function authorizeInterruptions(config: GuardConfig, state: RunStateLike, describe: (item: ToolApprovalItem) => ApprovalContext): Promise<{
47
67
  resolved: Resolved[];
48
68
  pending: Pending[];
@@ -51,11 +71,21 @@ export declare function authorizeInterruptions(config: GuardConfig, state: RunSt
51
71
  /** @deprecated Renamed to `authorizeInterruptions` in 0.3.0 — the old name read like the other
52
72
  * `decidioGate*` doors, which execute and confirm. This one only ever authorized. */
53
73
  export declare const gateInterruptions: typeof authorizeInterruptions;
54
- /** Apply a Decidio verdict to a resumed RunState (after RunState.fromString).
74
+ /** Apply Decidio's answer for one routed tool call to a resumed RunState (after RunState.fromString)
75
+ * - VERIFIED (0.7.0). It asks Decidio: GET /agent/status/:decisionId, and the approval must be bound
76
+ * to THIS tool call - the commitment Decidio holds is recomputed over the call's name, call id and exact
77
+ * argument string plus `describe(item)` (toolCallRequest), the same binding the call was authorized
78
+ * with. A parked RunState is the host's own storage: an item whose arguments were edited there after a
79
+ * human approved, or another call handed this call's decision id, must not ride that approval.
80
+ *
81
+ * Approved and bound → state.approve(item), returns "approved". Rejected/blocked by Decidio →
82
+ * state.reject(item), returns that verdict. Still pending, or approved for a DIFFERENT request, or not
83
+ * tied to one → throws the typed refusal and the RunState is NOT modified.
55
84
  *
56
- * VALIDATES BEFORE MUTATING. `else state.reject(item)` treated every unrecognised string as a
57
- * rejection, so a garbled payload, a version-skew verdict or an empty string became a rejection
58
- * that a caller could not tell apart from a human deciding no — and the RunState was already
59
- * mutated by the time anyone could look. The recognised set is exactly the one `resume` uses. */
60
- export declare function applyResume(state: RunStateLike, item: ToolApprovalItem, verdict: string): void;
85
+ * REPLACES `applyResume(state, item, verdict)`, REMOVED in 0.7.0. It applied a bare verdict string
86
+ * with no decision attached, so correctness depended entirely on the host routing the right verdict
87
+ * to the right item. It was removed rather than deprecated: a deprecated function that still approves
88
+ * keeps the hole open for every integration that never reads the warning, and one that refused
89
+ * approvals at runtime would fail AFTER a human approved - the removal fails at build time instead. */
90
+ export declare function applyVerifiedResume(config: GuardConfig, state: RunStateLike, item: ToolApprovalItem, decisionId: string, describe: (item: ToolApprovalItem) => ApprovalContext): Promise<"approved" | "rejected" | "blocked">;
61
91
  export {};
@@ -1,10 +1,11 @@
1
- import { gateAuthorize, checkEngineVersion } from "./_gate.js";
2
- import { DecidioBlockedError } from "../index.js";
1
+ import { gateAuthorize, gateCheckApproval, toolCallRequest, checkEngineVersion } from "./_gate.js";
2
+ import { DecidioBlockedError, approvalRefusal, decisionIdProblem } from "../index.js";
3
3
  export async function authorizeInterruptions(config, state, describe) {
4
4
  const resolved = [];
5
5
  const pending = [];
6
- for (const item of state.getInterruptions()) {
7
- const auth = await gateAuthorize(config, describe(item));
6
+ const batch = state.getInterruptions().map((item) => { const ctx = describe(item); return { item, ctx, request: toolCallRequest(item, ctx) }; });
7
+ for (const { item, ctx, request } of batch) {
8
+ const auth = await gateAuthorize(config, ctx, { request });
8
9
  if (auth.decision === "proceed") {
9
10
  state.approve(item);
10
11
  resolved.push({ item, verdict: "approved", decisionId: auth.decisionId });
@@ -14,22 +15,29 @@ export async function authorizeInterruptions(config, state, describe) {
14
15
  resolved.push({ item, verdict: "blocked", decisionId: auth.decisionId });
15
16
  }
16
17
  else if (auth.decision === "route")
17
- pending.push({ item, decisionId: auth.decisionId });
18
+ pending.push({ item, decisionId: auth.decisionId, callId: request.args[0].callId });
18
19
  else
19
20
  throw new DecidioBlockedError(`unhandled authorization verdict ${JSON.stringify(auth.decision)}`, auth.decisionId);
20
21
  }
21
22
  return { resolved, pending, confirmationSupported: false };
22
23
  }
23
24
  export const gateInterruptions = authorizeInterruptions;
24
- export function applyResume(state, item, verdict) {
25
- if (verdict === "approved" || verdict === "auto_approved") {
25
+ export async function applyVerifiedResume(config, state, item, decisionId, describe) {
26
+ const idProblem = decisionIdProblem(decisionId);
27
+ if (idProblem)
28
+ throw new DecidioBlockedError(`applyVerifiedResume needs the decision this item was routed as (${idProblem}) - pass the decisionId authorizeInterruptions returned for it. The RunState was NOT modified.`, typeof decisionId === "string" ? decisionId : "");
29
+ const ctx = describe(item);
30
+ const check = await gateCheckApproval(config, decisionId, ctx, toolCallRequest(item, ctx));
31
+ if (check.ok) {
26
32
  state.approve(item);
27
- return;
33
+ return "approved";
28
34
  }
29
- if (verdict === "rejected" || verdict === "blocked") {
35
+ if (check.code === "rejected" || check.code === "blocked") {
30
36
  state.reject(item);
31
- return;
37
+ return check.code;
32
38
  }
33
- throw new DecidioBlockedError(`unrecognised resume verdict ${JSON.stringify(verdict)} — refusing to apply it. This SDK understands approved / auto_approved / rejected / blocked; anything else is a version skew, and guessing would turn "I do not understand" into a decision. The RunState was NOT modified.`, "");
39
+ const refusal = approvalRefusal(check);
40
+ refusal.message += " The RunState was NOT modified.";
41
+ throw refusal;
34
42
  }
35
43
  checkEngineVersion("openai");
@@ -1,4 +1,4 @@
1
- import { type GuardConfig, type ApprovalContext, type ProtectedExecution, type EvidenceOutcome } from "../index.js";
1
+ import { type GuardConfig, type ApprovalContext, type ProtectedExecution, type EvidenceOutcome, type ApprovalCheck } from "../index.js";
2
2
  import { type GateAuth } from "./_gate.js";
3
3
  /**
4
4
  * @decidio/sdk/temporal — durable suspend via Temporal's native `condition()` + a
@@ -9,20 +9,39 @@ import { type GateAuth } from "./_gate.js";
9
9
  * const verdicts = installDecidioSignal(); // wires the resume signal
10
10
  * const out = await decidioGate(config, ctx, () => createOpp(o), verdicts, {
11
11
  * authorize: () => executeActivity(authorizeActivity, ...), // HTTP belongs in activities
12
+ * verify: (id) => executeActivity(verifyActivity, id), // body: return verifyApprovalActivity(config, id, ctx)
12
13
  * confirm: (id, r) => executeActivity(confirmActivity, ...),
13
14
  * });
14
15
  * Decidio's webhook bridges to: handle.signal(decidioResumeSignal, decisionId, verdict, reason)
15
16
  *
17
+ * THE SIGNAL IS A WAKE-UP (0.7.0). Before 0.7.0 the action ran on the signal alone, so anyone who could
18
+ * signal the workflow with the decisionId approved it. Now the signal only wakes the workflow; `verify`
19
+ * then confirms with Decidio that THIS request was approved (GET /agent/status + the request
20
+ * commitment) before `run()`. Still pending → keep waiting for the next signal; rejected/blocked/changed
21
+ * → the typed refusal. `verify` is HTTP, so it must be an ACTIVITY, never workflow code (determinism) -
22
+ * and it is required whenever `authorize` is.
23
+ *
16
24
  * (This wiring doc ships in the .d.ts — the README points here.)
17
25
  */
18
26
  export declare const decidioResumeSignal: import("@temporalio/workflow").SignalDefinition<[string, string, (string | undefined)?]>;
19
- /** Call inside the workflow to wire the resume signal; returns the verdict store. */
27
+ /** Call inside the workflow to wire the resume signal; returns the signal store. Since 0.7.0 an entry is
28
+ * a WAKE-UP for its decision - the verdict it carries is informational and is consumed when the gate
29
+ * checks the approval with Decidio. */
20
30
  export declare function installDecidioSignal(): Map<string, {
21
31
  verdict: string;
22
32
  reason?: string;
23
33
  }>;
34
+ /** The approval check as an ACTIVITY body (0.7.0) - register an activity that returns this, unchanged,
35
+ * and route `verify` through it. GET /agent/status + the request commitment recomputed over `ctx`, which
36
+ * must be the same context the gate authorized. Never call it from workflow code (it is HTTP). */
37
+ export declare function verifyApprovalActivity(config: GuardConfig, decisionId: string, ctx: ApprovalContext): Promise<ApprovalCheck>;
24
38
  export interface DecidioGateOpts<R = unknown> {
25
39
  authorize?: () => Promise<GateAuth>;
40
+ /** THE APPROVAL CHECK, run after the resume signal and before `run()` (0.7.0). It is HTTP, so route it
41
+ * through an ACTIVITY whose body is `return verifyApprovalActivity(config, decisionId, ctx)` - the same
42
+ * `ctx` this gate authorized - and return that value unchanged. Required whenever `authorize` is
43
+ * supplied; without either it defaults to a direct call (tests only - a workflow must not do I/O). */
44
+ verify?: (decisionId: string) => Promise<ApprovalCheck>;
26
45
  /** Reports through YOUR activity. Return the gate's confirm response body (validated with
27
46
  * `readConfirmResponse`) and it becomes the outcome; return nothing and the outcome says the SDK could
28
47
  * not read back what Decidio recorded (prodsec V035-03: a returned outcome used to be discarded). */
@@ -1,21 +1,34 @@
1
1
  import { condition, defineSignal, setHandler } from "@temporalio/workflow";
2
- import { DecidioBlockedError } from "../index.js";
3
- import { gateAuthorize, gateConfirm, verdictFrom, assertVerdict, checkEngineVersion, assertAuthorizeVerdict } from "./_gate.js";
2
+ import { DecidioBlockedError, approvalRefusal } from "../index.js";
3
+ import { gateAuthorize, gateConfirm, gateCheckApproval, assertApprovalCheckShape, checkEngineVersion, assertAuthorizeVerdict } from "./_gate.js";
4
4
  export const decidioResumeSignal = defineSignal("decidioResume");
5
5
  export function installDecidioSignal() {
6
6
  const verdicts = new Map();
7
7
  setHandler(decidioResumeSignal, (decisionId, verdict, reason) => { verdicts.set(decisionId, { verdict, reason }); });
8
8
  return verdicts;
9
9
  }
10
+ export function verifyApprovalActivity(config, decisionId, ctx) {
11
+ return gateCheckApproval(config, decisionId, ctx);
12
+ }
10
13
  let confirmWarned = false;
11
14
  export async function decidioGate(config, ctx, run, verdicts, opts = {}) {
15
+ if (opts.authorize && !opts.verify) {
16
+ throw new Error("[decidio] temporal: `authorize` runs in your activity, so the approval check must too - pass `verify: (decisionId) => executeActivity(verifyActivity, decisionId)`, where the activity returns verifyApprovalActivity(config, decisionId, ctx) (from @decidio/sdk/temporal) unchanged. Since 0.7.0 a resume signal only wakes the workflow; the approval is confirmed with Decidio before your action runs. Nothing was routed or executed.");
17
+ }
12
18
  const auth = assertAuthorizeVerdict(await (opts.authorize ?? (() => gateAuthorize(config, ctx)))(), "unknown");
13
19
  if (auth.decision === "block")
14
20
  throw new DecidioBlockedError(auth.reason ?? "blocked by policy", auth.decisionId);
15
21
  if (auth.decision === "route") {
16
- await condition(() => verdicts.has(auth.decisionId));
17
- const v = verdicts.get(auth.decisionId);
18
- assertVerdict(verdictFrom(v), auth.decisionId, v.reason);
22
+ const verify = opts.verify ?? ((decisionId) => gateCheckApproval(config, decisionId, ctx));
23
+ for (;;) {
24
+ await condition(() => verdicts.has(auth.decisionId));
25
+ verdicts.delete(auth.decisionId);
26
+ const check = assertApprovalCheckShape(await verify(auth.decisionId), auth.decisionId);
27
+ if (check.ok)
28
+ break;
29
+ if (check.code !== "pending")
30
+ throw approvalRefusal(check);
31
+ }
19
32
  }
20
33
  else if (auth.decision !== "proceed") {
21
34
  throw new DecidioBlockedError(`unhandled authorization verdict ${JSON.stringify(auth.decision)} — refusing to execute`, auth.decisionId);
package/dist/cli.d.ts CHANGED
@@ -1,6 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
  export type ApprovalAnswer = "approve" | "reject" | "again";
3
3
  export declare function approvalAnswer(answer: unknown): ApprovalAnswer;
4
+ /** The issuer store's key for a Decidio address: its origin (scheme + lowercased host + non-default port), computed by a
5
+ * RESTRICTED parser that FAILS CLOSED, identically in both SDKs. It deliberately does NOT reproduce the browser URL's
6
+ * canonicalisation: `new URL("https://%64ecidio-api.onrender.com").origin` is the real host, and keying by that let a
7
+ * planted store entry under a percent-encoded / IDN / numeric-shorthand / backslash spelling be silently trusted AS the
8
+ * real host (the Python twin, on urlsplit, never decoded these, so the two SDKs also disagreed). Now: only a bare
9
+ * http(s)://, an ASCII-label host or a bracketed real IP literal is keyed as an origin; anything the parser is not sure
10
+ * it can canonicalise the same in both languages is keyed as its trimmed lowercased self, which cannot equal a real
11
+ * origin and so is never mistaken for one. Exported for the smokes; the Python twin _issuer_store_key answers the same. */
12
+ export declare function issuerStoreKey(url: string): string;
4
13
  /** The agent id `init` uses when none is given: this folder's name (or the package name), slugified. "my-agent" told a
5
14
  * tester nothing; the folder they are in is the agent they are building (founder ruling 2026-09-06). */
6
15
  export declare function defaultAgentId(dir?: string): string;
@@ -8,3 +17,53 @@ export declare function defaultAgentId(dir?: string): string;
8
17
  * distinct decision ids can never share a file (first-run review of 0.4.1: dots were STRIPPED, `a.b` and `ab`
9
18
  * collided). `~` is outside the allowed set, so the escape is unambiguous. Exported for the smokes. */
10
19
  export declare function receiptFileName(id: string): string;
20
+ /** Scoped packs (GitHub App design §2): a pack's scope in words - the organisation's, a unit's (its type, and its name when
21
+ * the server sent one: names that fail its person check are withheld there), or the reader's own. Null for an older server. */
22
+ export declare function packScopeLabel(scope: any): string | null;
23
+ /** Where `context pull --here` is: a folder's .decidio.json names a unit; else the checkout's GitHub remote names the repository
24
+ * (its binding's unit decides); else nowhere in particular (the organisation's packs and your own). */
25
+ export declare function hereQuery(cwd?: string): {
26
+ unit?: string;
27
+ repo?: string;
28
+ error?: string;
29
+ };
30
+ /** PROJECT FILES ARE TOUCHED ONLY THROUGH PLAIN PATHS (invariant-guard 2026-10-16): a repository can commit `.claude/skills/x`
31
+ * or `.claude/rules` as a SYMLINK to anywhere, so every component from the project's `.claude` down is lstat'ed and a symlink
32
+ * refuses the whole path - nothing is written through it and nothing is removed through it. A missing component is fine. */
33
+ export declare function plainProjectPath(parts: string[]): boolean;
34
+ /** Is this file one `context pull` wrote? A skill file's frontmatter names `decidio-<slug>` and its heading says it is a
35
+ * Decidio context pack; a rules file's heading says it is one to read first. A person's own file with our prefix is not. */
36
+ export declare function isPackFileOf(text: string, slug: string, kind: "skill" | "rules"): boolean;
37
+ /** The skill file for one pack: the trigger IS the description (the host reads it every turn), the rows are the body. */
38
+ export declare function packSkillMarkdown(idx: any): string;
39
+ /** An ALWAYS-pack (stackable packs, 2026-10-15): a Claude Code rules file, loaded whole at launch with the project's own
40
+ * rules - no frontmatter, no trigger; the foundation every other pack can assume was read. */
41
+ export declare function packRulesMarkdown(idx: any): string;
42
+ export type ParsedAdr = {
43
+ title: string;
44
+ status: string | null;
45
+ decided: string;
46
+ rationale: string;
47
+ alternatives: string[];
48
+ };
49
+ /** PURE: one ADR's title, status, decision, context (with its consequences) and the options it considered; null when it has no title. */
50
+ export declare function parseAdr(text: string): ParsedAdr | null;
51
+ export declare const AGENTS_MD_START = "<!-- decidio:start -->";
52
+ export declare const AGENTS_MD_END = "<!-- decidio:end -->";
53
+ /** Decidio's public GitHub Action (github.com/OmniTwin/decidio-actions): one action, step request or verify. It fetches the job's own
54
+ * OIDC token (audience "decidio") - no repository secret - and fails the job in plain words when Decidio refuses. Pinned to the
55
+ * v1.0.0 COMMIT, never a tag (invariant-guard 2026-10-20): the verify step enforces the approval in a production job with
56
+ * id-token: write, so a moved tag must not change it. Bump both pins together, with the release. */
57
+ export declare const DECIDIO_ACTION = "OmniTwin/decidio-actions@e40ba8e75a223f2475038d38a16df5a5885afde4 # v1.0.0";
58
+ export declare const CHECKOUT_ACTION = "actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0";
59
+ export declare function githubRequestWorkflow(api: string, environment: string, branch?: string): string;
60
+ /** The verify step and the checkout of the approved commit: the first steps of any deploy job Decidio starts. */
61
+ export declare function githubVerifySteps(api: string): string;
62
+ export declare function githubDeployWorkflow(api: string, environment: string): string;
63
+ /** The AGENTS.md section: a link to where the decisions live and how an agent checks a plan - never a decision's text (founder
64
+ * 2026-10-17: decisions embedded in a repository leak and go stale). */
65
+ export declare function agentsMdSection(appUrl: string): string;
66
+ /** PURE: AGENTS.md with our section added, or replaced where it already is (idempotent). Ours is recognised only whole - the start
67
+ * marker straight followed by our heading, then the end marker after it - so a stray marker in a person's own text is never
68
+ * read as ours and nothing of theirs is ever removed (invariant-guard 2026-10-17). */
69
+ export declare function withAgentsMdSection(text: string, section: string): string;