@evoclock/pi-agentic-driver 1.1.2 → 2.0.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/.agentic-driver/loop-until-done.defaults.json +5 -0
- package/README.md +302 -41
- package/config/merge-policy.v1.example.json +4 -0
- package/config/relay-authority.v1.example.json +7 -0
- package/config/treehouse.v1.json +4 -0
- package/docs/durable-driver-api.md +23 -0
- package/docs/durable-persistence.md +32 -0
- package/docs/merge-policy.md +45 -0
- package/docs/relay-authority.md +9 -0
- package/extensions/aidr.ts +3 -0
- package/extensions/harvest-lessons.ts +8 -0
- package/extensions/herdr-dispatch.ts +32 -3
- package/extensions/loop-until-done.ts +7 -0
- package/extensions/recovery-evidence.ts +3 -3
- package/extensions/treehouse-recycle.ts +47 -0
- package/janus/README.md +2 -4
- package/janus/janus/config.ts +2 -2
- package/janus/janus/providers.ts +2 -4
- package/janus/tests/providers.test.ts +2 -2
- package/{extensions → lib}/task-store-adapter.ts +1 -5
- package/package.json +158 -4
- package/scripts/enforcement/approval_ledger_pi.js +135 -0
- package/scripts/enforcement/attended_authority_guard.js +113 -0
- package/scripts/enforcement/board_claim_bridge_pi.js +10 -10
- package/scripts/enforcement/durable_assignment_pi.js +488 -0
- package/scripts/enforcement/durable_driver_pi.d.ts +51 -0
- package/scripts/enforcement/durable_driver_pi.js +254 -0
- package/scripts/enforcement/herdr_async_dispatch_pi.js +86 -20
- package/scripts/enforcement/herdr_async_seam_pi.js +90 -3
- package/scripts/enforcement/herdr_communication_pi.js +126 -12
- package/scripts/enforcement/herdr_lifecycle_pi.js +730 -81
- package/scripts/enforcement/linux_microvm_cutover_pi.js +3 -3
- package/scripts/enforcement/linux_microvm_remote_fixture.sh +0 -6
- package/scripts/enforcement/loop_until_done_guide.md +13 -0
- package/scripts/enforcement/loop_until_done_pi.js +214 -0
- package/scripts/enforcement/merge_policy.js +181 -0
- package/scripts/enforcement/outward_effect_pi.js +156 -0
- package/scripts/enforcement/recovery_evidence_review_pi.js +31 -3
- package/scripts/enforcement/storage_owner_lock_pi.js +66 -0
- package/scripts/enforcement/task_board_core_pi.js +13 -100
- package/scripts/enforcement/task_board_hash_pi.js +86 -0
- package/scripts/enforcement/treehouse_recycle_pi.js +145 -0
- package/scripts/harvest_lessons.js +219 -0
- package/scripts/relay/record_approval.mjs +10 -0
- package/scripts/relay/send_request.mjs +26 -0
- package/skills/architect-pi/SKILL.md +44 -0
- package/skills/arena-pi/SKILL.md +46 -0
- package/skills/automate-me-pi/SKILL.md +37 -0
- package/skills/benchmark-checklist-pi/SKILL.md +35 -0
- package/skills/blast-radius-pi/SKILL.md +24 -0
- package/skills/bro-pi/SKILL.md +17 -0
- package/skills/correct-pi/SKILL.md +36 -0
- package/skills/create-verification-skill-pi/LICENSE +21 -0
- package/skills/create-verification-skill-pi/SKILL.md +24 -0
- package/skills/decision-to-spec/SKILL.md +5 -5
- package/skills/figure-it-out-pi/SKILL.md +37 -0
- package/skills/harvest-lessons/SKILL.md +56 -0
- package/skills/harvest-lessons/harvest.example.json +8 -0
- package/skills/how/LICENSE +24 -0
- package/skills/how/WORKFLOW_BOUNDARIES.md +19 -0
- package/skills/how-pi/SKILL.md +33 -0
- package/skills/in-your-own-words-pi/SKILL.md +15 -0
- package/skills/interrogate-pi/SKILL.md +35 -0
- package/skills/ketchup-pi/SKILL.md +21 -0
- package/skills/maintain-verification-skill-pi/SKILL.md +37 -0
- package/skills/make-bot-ui-pi/SKILL.md +33 -0
- package/skills/no-comments-pi/SKILL.md +33 -0
- package/skills/poteto-help-pi/SKILL.md +50 -0
- package/skills/poteto-mode-pi/LICENSE +21 -0
- package/skills/poteto-mode-pi/SKILL.md +41 -0
- package/skills/poteto-mode-pi/playbooks.md +95 -0
- package/skills/principle-attack-the-premise-pi/SKILL.md +17 -0
- package/skills/principle-boundary-discipline-pi/SKILL.md +19 -0
- package/skills/principle-build-the-lever-pi/SKILL.md +19 -0
- package/skills/principle-encode-lessons-in-structure-pi/SKILL.md +17 -0
- package/skills/principle-exhaust-the-design-space-pi/SKILL.md +17 -0
- package/skills/principle-experience-first-pi/SKILL.md +17 -0
- package/skills/principle-explain-the-number-pi/SKILL.md +21 -0
- package/skills/principle-fix-root-causes-pi/SKILL.md +21 -0
- package/skills/principle-foundational-thinking-pi/SKILL.md +21 -0
- package/skills/principle-guard-the-context-window-pi/SKILL.md +19 -0
- package/skills/principle-laziness-protocol-pi/SKILL.md +19 -0
- package/skills/principle-make-operations-idempotent-pi/SKILL.md +21 -0
- package/skills/principle-migrate-callers-then-delete-legacy-apis-pi/SKILL.md +19 -0
- package/skills/principle-minimize-reader-load-pi/SKILL.md +19 -0
- package/skills/principle-model-the-domain-pi/SKILL.md +19 -0
- package/skills/principle-never-block-on-the-human-pi/SKILL.md +19 -0
- package/skills/principle-outcome-oriented-execution-pi/SKILL.md +19 -0
- package/skills/principle-prove-it-works-pi/SKILL.md +19 -0
- package/skills/principle-redesign-from-first-principles-pi/SKILL.md +19 -0
- package/skills/principle-separate-before-serializing-shared-state-pi/SKILL.md +19 -0
- package/skills/principle-sequence-verifiable-units-pi/SKILL.md +19 -0
- package/skills/principle-subtract-before-you-add-pi/SKILL.md +19 -0
- package/skills/principle-test-behavior-not-implementation-pi/SKILL.md +21 -0
- package/skills/principle-type-system-discipline-pi/SKILL.md +19 -0
- package/skills/public-pr-wording/SKILL.md +69 -0
- package/skills/public-pr-wording/pr_wording_config.example.json +22 -0
- package/skills/public-pr-wording/pr_wording_lint.py +249 -0
- package/skills/public-pr-wording/pr_wording_rules.json +60 -0
- package/skills/pulse/SKILL.md +6 -0
- package/skills/recall-pi/SKILL.md +29 -0
- package/skills/reflect-pi/SKILL.md +27 -0
- package/skills/setup-pstack-pi/SKILL.md +23 -0
- package/skills/setup-pstack-pi/assets/pstack-model-roles.v1.example.json +7 -0
- package/skills/sharpen-pi/SKILL.md +19 -0
- package/skills/show-me-your-work-pi/SKILL.md +27 -0
- package/skills/swarm-pi/SKILL.md +27 -0
- package/skills/tasks/SKILL.md +8 -2
- package/skills/tdd-pi/SKILL.md +21 -0
- package/skills/teach-pi/SKILL.md +21 -0
- package/skills/technical-writing-pi/SKILL.md +27 -0
- package/skills/typescript-best-practices-pi/SKILL.md +21 -0
- package/skills/unslop-pi/SKILL.md +27 -0
- package/skills/why-pi/SKILL.md +37 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Relayed authority
|
|
2
|
+
|
|
3
|
+
A relayed approval records the owner's exact answer, the question it answered, channel, source message and time. A record attests that a configured relayer recorded those words; the hash chain does **not** prove the speaker's identity. The coordinator can check `[relay:v1 ledger=<id> relayer=<name>]` with the read-only `agentic_relay_check_request` tool against the profile ledger, never against claims in prompt prose. A covered result is only a preflight; each effect still checks its own approval at its action boundary. Without a valid record, the native confirmation path remains in force.
|
|
4
|
+
|
|
5
|
+
The active profile holds `config/relay-authority.v1.json` and `approvals/{approvals.v1.jsonl,head.v1.json,uses.v1.jsonl}`. The committed `config/relay-authority.v1.example.json` is a template, not a trusted live list. Only the owner edits trusted relayers, channels and `githubOwner`. The example leaves `githubOwner` unset; registration refuses to proceed until the owner configures the real value in the local active profile. The relay CLI takes a JSON input file and profile path; it validates a relayer against the active config before appending. The relaying bot runs the writer CLI outside a guarded Pi session: the Pi shell guard blocks direct approval-ledger access, including this CLI. The writer lock uses exclusive creation and reclaims a lock only after its age exceeds one minute and its PID is demonstrably gone. The head file records count and last hash; a missing final line is detected. A malicious writer able to replace both ledger and head can still rewrite history: no signing key is used.
|
|
6
|
+
|
|
7
|
+
Tier 0 covers reads and drafts. Tier 1 permits approved private review-branch work, registered-repo spawns and tracked-file removals in reviewable feature-branch commits within approved paths. Tier 2 permits registry additions only after automatic clone, origin and private-visibility verification. Tier 3 remains owner-direct for merges unless separately allowed by merge policy, public publication, force-push, credentials, unrecoverable deletion, registry removals and edits to trusted authority files. Standing approvals expire after seven days by default. A covered spawn skips the native dialog even if the owner is present. Tier 1 tracked-file removal is a documented scope rule, not a mechanical `git rm` path check: the current shell guard permits `git rm` without checking the approved paths. The coordinator continues loading the published npm driver, not this checkout; deployment is a separate owner action.
|
|
8
|
+
|
|
9
|
+
An authority failure should include `status: needs-owner`, the tier, missing scope, a relayable phrase and next step. A use reservation precedes the spawn mutation and counts toward `maxUses`; a partial attempt is not automatically retried. The write guard rejects direct agent-shell access to approvals paths (including reads) and invocation of the relay writer CLI; use `agentic_relay_check_request` for read-only authority checks. It is not filesystem isolation: an arbitrary program with profile write access can bypass it. Live headless acceptance remains separate from fixture tests.
|
package/extensions/aidr.ts
CHANGED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 evoclock
|
|
2
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
3
|
+
|
|
4
|
+
import { registerHarvestLessons } from "../scripts/harvest_lessons.js";
|
|
5
|
+
|
|
6
|
+
export default function registerHarvestLessonsExtension(pi: any) {
|
|
7
|
+
return registerHarvestLessons(pi);
|
|
8
|
+
}
|
|
@@ -1,9 +1,36 @@
|
|
|
1
1
|
// SPDX-FileCopyrightText: 2026 Julen Gamboa <j.a.r.gamboa@gmail.com>
|
|
2
2
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
3
3
|
|
|
4
|
-
import { createTaskStore } from "
|
|
4
|
+
import { createTaskStore } from "../lib/task-store-adapter.ts";
|
|
5
|
+
import { mkdirSync, realpathSync } from "node:fs";
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
import { join } from "node:path";
|
|
5
8
|
|
|
6
9
|
async function herdrDispatchCore(pi) {
|
|
10
|
+
const stores = new Map();
|
|
11
|
+
const getJourneyStore = (context) => {
|
|
12
|
+
const sessionId = context?.sessionManager?.getSessionId?.();
|
|
13
|
+
if (typeof sessionId !== "string" || !/^[a-zA-Z0-9_-]{1,128}$/.test(sessionId)) {
|
|
14
|
+
throw new Error("stable Pi session ID required for durable dispatch");
|
|
15
|
+
}
|
|
16
|
+
if (!stores.has(sessionId)) {
|
|
17
|
+
const pending = (async () => {
|
|
18
|
+
const directory = join(realpathSync(homedir()), ".pi", "agentic-driver-dispatch", "sessions", sessionId);
|
|
19
|
+
mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
20
|
+
if (realpathSync(directory) !== directory) throw new Error("canonical dispatch store required");
|
|
21
|
+
const { openDurableSeamStore } = await import(new URL("../scripts/enforcement/durable_assignment_pi.js", import.meta.url).href);
|
|
22
|
+
return openDurableSeamStore({ directory });
|
|
23
|
+
})();
|
|
24
|
+
stores.set(sessionId, pending);
|
|
25
|
+
pending.catch(() => { if (stores.get(sessionId) === pending) stores.delete(sessionId); });
|
|
26
|
+
}
|
|
27
|
+
return stores.get(sessionId);
|
|
28
|
+
};
|
|
29
|
+
pi.on?.("session_shutdown", async () => {
|
|
30
|
+
const opened = [...stores.values()];
|
|
31
|
+
stores.clear();
|
|
32
|
+
await Promise.all(opened.map(async (pending) => { try { await (await pending).close(); } catch { /* Open may already have failed. */ } }));
|
|
33
|
+
});
|
|
7
34
|
const dispatch = await import(new URL("../scripts/enforcement/herdr_async_dispatch_pi.js", import.meta.url).href);
|
|
8
35
|
const lifecycle = await import(new URL("../scripts/enforcement/herdr_lifecycle_pi.js", import.meta.url).href);
|
|
9
36
|
|
|
@@ -27,10 +54,12 @@ async function herdrDispatchCore(pi) {
|
|
|
27
54
|
|
|
28
55
|
// A fresh read-only session view per journey; registration remains keyed
|
|
29
56
|
// on the real Pi API identity for the enforcement module's dedupe guard.
|
|
30
|
-
|
|
57
|
+
const outward = await import(new URL("../scripts/enforcement/outward_effect_pi.js", import.meta.url).href);
|
|
58
|
+
outward.registerOutwardEffectInterface(pi, { getJourneyStore });
|
|
59
|
+
return dispatch.registerWorkerDispatchInterface(pi, { spawnReplacement, taskStoreFactory: createTaskStore, getJourneyStore });
|
|
31
60
|
}
|
|
32
61
|
|
|
33
|
-
// Async transport seam
|
|
62
|
+
// Async transport seam: submit returns a versioned,
|
|
34
63
|
// non-authorizing receipt immediately; poll/observe/read are separate
|
|
35
64
|
// single-attempt operations. No retries, no resends, no authority.
|
|
36
65
|
export default async function herdrDispatchPi(pi) {
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Julen Gamboa <j.a.r.gamboa@gmail.com>
|
|
2
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
3
|
+
|
|
4
|
+
export default async function loopUntilDonePi(pi) {
|
|
5
|
+
const module = await import(new URL("../scripts/enforcement/loop_until_done_pi.js", import.meta.url).href);
|
|
6
|
+
return module.registerLoopUntilDone(pi);
|
|
7
|
+
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// SPDX-FileCopyrightText: 2026 Julen Gamboa <j.a.r.gamboa@gmail.com>
|
|
2
2
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
3
3
|
|
|
4
|
-
// Thin extension wrapper for
|
|
5
|
-
//
|
|
6
|
-
// the
|
|
4
|
+
// Thin extension wrapper for owner-only full-evidence review and a bounded,
|
|
5
|
+
// metadata-only summary tool. Full evidence remains in the native TUI overlay;
|
|
6
|
+
// the model never receives entries, identifiers or retrieval references. The registration is async (the
|
|
7
7
|
// trusted root is resolved and snapshotted); Pi awaits an asynchronous
|
|
8
8
|
// default factory before startup continues (extensions.md), so no return
|
|
9
9
|
// value or extra await is needed here.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { recycleAllPools, TREEHOUSE_ROOT } from "../scripts/enforcement/treehouse_recycle_pi.js";
|
|
4
|
+
|
|
5
|
+
// Scans every pool under the Treehouse root after session start and
|
|
6
|
+
// periodically, so PR closure/merge is detected without touching the dispatch
|
|
7
|
+
// path. No activity occurs at extension load.
|
|
8
|
+
export function createTreehouseRecycleExtension(
|
|
9
|
+
recycle = recycleAllPools,
|
|
10
|
+
rootForSession = async () => TREEHOUSE_ROOT,
|
|
11
|
+
schedule = setInterval,
|
|
12
|
+
cancel = clearInterval,
|
|
13
|
+
) {
|
|
14
|
+
return function (pi: ExtensionAPI) {
|
|
15
|
+
let timer: ReturnType<typeof setInterval> | undefined;
|
|
16
|
+
let busy = false;
|
|
17
|
+
let active = false;
|
|
18
|
+
let generation = 0;
|
|
19
|
+
const check = async () => {
|
|
20
|
+
if (busy || !active) return;
|
|
21
|
+
busy = true;
|
|
22
|
+
const started = generation;
|
|
23
|
+
try {
|
|
24
|
+
const root = await rootForSession();
|
|
25
|
+
if (active && started === generation && root) await recycle({ treehouseRoot: root, shouldContinue: () => active && started === generation });
|
|
26
|
+
} catch (error) {
|
|
27
|
+
console.error("Treehouse recycling held:", error);
|
|
28
|
+
} finally { busy = false; }
|
|
29
|
+
};
|
|
30
|
+
pi.on("session_start", () => {
|
|
31
|
+
if (active) return;
|
|
32
|
+
active = true;
|
|
33
|
+
void check();
|
|
34
|
+
const { pollIntervalMs } = JSON.parse(readFileSync(new URL("../config/treehouse.v1.json", import.meta.url), "utf8"));
|
|
35
|
+
timer = schedule(() => { void check(); }, pollIntervalMs);
|
|
36
|
+
timer.unref?.();
|
|
37
|
+
});
|
|
38
|
+
pi.on("session_shutdown", () => {
|
|
39
|
+
active = false;
|
|
40
|
+
generation++;
|
|
41
|
+
if (timer) cancel(timer);
|
|
42
|
+
timer = undefined;
|
|
43
|
+
});
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export default createTreehouseRecycleExtension();
|
package/janus/README.md
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
# janus — Jev evaluation wrapper service
|
|
2
2
|
|
|
3
3
|
janus is the local wrapper service for the Jev decision layer. It wraps the
|
|
4
|
-
official `@typesafe-ai/sdk` (
|
|
5
|
-
provider; the Vercel AI Gateway and merge gateway were dropped by owner
|
|
6
|
-
decision) and exposes one evaluation endpoint plus health/readiness/version
|
|
4
|
+
official `@typesafe-ai/sdk` (the sole configured direct provider) and exposes one evaluation endpoint plus health/readiness/version
|
|
7
5
|
probes on loopback.
|
|
8
6
|
|
|
9
7
|
## What janus does
|
|
@@ -63,7 +61,7 @@ Dev (from `janus/`):
|
|
|
63
61
|
npx tsx janus/server.ts # listens on 127.0.0.1:8787
|
|
64
62
|
```
|
|
65
63
|
|
|
66
|
-
Config is read from
|
|
64
|
+
Config is read from the application config directory
|
|
67
65
|
(override with `JANUS_CONFIG_PATH`). Defaults work with no file:
|
|
68
66
|
|
|
69
67
|
```json
|
package/janus/janus/config.ts
CHANGED
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
//
|
|
11
11
|
// Provider architecture (authoritative owner decision): direct TypeSafe
|
|
12
12
|
// (`@typesafe-ai/sdk`, official SDK) is the SOLE provider. The Vercel AI
|
|
13
|
-
//
|
|
14
|
-
//
|
|
13
|
+
// No alternative gateway is valid config and there is no fallback transport.
|
|
14
|
+
// When the configured direct provider is unconfigured the
|
|
15
15
|
// service fails closed (model_unauthorized), with no silent degradation.
|
|
16
16
|
|
|
17
17
|
import { existsSync, readFileSync } from "node:fs";
|
package/janus/janus/providers.ts
CHANGED
|
@@ -6,10 +6,8 @@
|
|
|
6
6
|
// 1. typesafe-direct — THE SOLE PROVIDER (owner decision): the official
|
|
7
7
|
// @typesafe-ai/sdk (`TypeSafeClient.systemOne`)
|
|
8
8
|
// with the owner's direct API key from the macOS
|
|
9
|
-
// Keychain
|
|
10
|
-
//
|
|
11
|
-
// merge gateway are DROPPED — there is no fallback
|
|
12
|
-
// transport, and an unconfigured TypeSafe Direct
|
|
9
|
+
// Keychain. No alternative gateway or fallback
|
|
10
|
+
// transport is configured; an unconfigured direct provider
|
|
13
11
|
// fails closed with the existing error taxonomy.
|
|
14
12
|
//
|
|
15
13
|
// Invariants enforced here:
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
// Adapter mapping tests (review fix #2): the transport contract maps onto the
|
|
5
5
|
// sole provider adapter (typesafe-direct) with mocked clients and NO network,
|
|
6
6
|
// NO live credentials. Question composition (review fix #1), the single-retry
|
|
7
|
-
// policy, and failed-attempt usage accumulation are verified here.
|
|
8
|
-
//
|
|
7
|
+
// policy, and failed-attempt usage accumulation are verified here. No
|
|
8
|
+
// alternative gateway is configured or tested.
|
|
9
9
|
|
|
10
10
|
import test from "node:test";
|
|
11
11
|
import assert from "node:assert/strict";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// SPDX-FileCopyrightText: 2026
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 evoclock
|
|
2
2
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
3
3
|
|
|
4
4
|
import {
|
|
@@ -7,10 +7,6 @@ import {
|
|
|
7
7
|
} from "../scripts/enforcement/session_tasks_core_pi.js";
|
|
8
8
|
import { createHash } from "node:crypto";
|
|
9
9
|
|
|
10
|
-
// Pi discovers every packaged extensions/*.ts file. This helper also needs a
|
|
11
|
-
// harmless default factory so discovery can load it without registering tools.
|
|
12
|
-
export default function taskStoreAdapter() {}
|
|
13
|
-
|
|
14
10
|
// Match TaskList's branch-first/high-water-mark observation in tasks.ts.
|
|
15
11
|
// The session mirror is a fallback; neither reader changes either source.
|
|
16
12
|
function observeSessionTasks(ctx) {
|
package/package.json
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@evoclock/pi-agentic-driver",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "Guardrail extensions for Agentic Driver: advisory review, bounded Herdr communication, and guarded worker lifecycle.",
|
|
5
5
|
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "node --test tests/test_*.mjs",
|
|
8
|
+
"test:ci": "node --test tests/test_durable_driver.mjs tests/test_durable_assignment.mjs tests/test_outward_effect.mjs tests/test_herdr_async_dispatch.mjs tests/test_herdr_async_seam.mjs tests/test_herdr_communication.mjs tests/test_fresh_pack_install.mjs tests/test_task_board_dispatch.mjs tests/test_task_store_adapter.mjs tests/test_public_extension_set.mjs"
|
|
9
|
+
},
|
|
6
10
|
"license": "AGPL-3.0-only",
|
|
7
11
|
"engines": {
|
|
8
|
-
"node": ">=22.
|
|
12
|
+
"node": ">=22.19.0"
|
|
9
13
|
},
|
|
10
14
|
"keywords": [
|
|
11
15
|
"pi-package",
|
|
@@ -39,10 +43,19 @@
|
|
|
39
43
|
"files": [
|
|
40
44
|
"extensions/attended-authority-guard.ts",
|
|
41
45
|
"scripts/enforcement/attended_authority_guard.js",
|
|
46
|
+
"scripts/enforcement/approval_ledger_pi.js",
|
|
47
|
+
"scripts/relay/record_approval.mjs",
|
|
48
|
+
"scripts/relay/send_request.mjs",
|
|
49
|
+
"config/relay-authority.v1.example.json",
|
|
50
|
+
"docs/relay-authority.md",
|
|
51
|
+
"scripts/enforcement/merge_policy.js",
|
|
52
|
+
"config/merge-policy.v1.example.json",
|
|
53
|
+
"docs/merge-policy.md",
|
|
42
54
|
"extensions/code-phage.js",
|
|
43
55
|
"extensions/herdr-communication.ts",
|
|
44
56
|
"extensions/herdr-dispatch.ts",
|
|
45
|
-
"extensions/
|
|
57
|
+
"extensions/loop-until-done.ts",
|
|
58
|
+
"lib/task-store-adapter.ts",
|
|
46
59
|
"extensions/task-board.ts",
|
|
47
60
|
"extensions/pulse.ts",
|
|
48
61
|
"lib/adapters/diff-scope.mjs",
|
|
@@ -56,6 +69,15 @@
|
|
|
56
69
|
"scripts/enforcement/herdr_communication_pi.js",
|
|
57
70
|
"scripts/enforcement/herdr_async_dispatch_pi.js",
|
|
58
71
|
"scripts/enforcement/herdr_async_seam_pi.js",
|
|
72
|
+
"scripts/enforcement/loop_until_done_pi.js",
|
|
73
|
+
"scripts/enforcement/loop_until_done_guide.md",
|
|
74
|
+
"scripts/enforcement/durable_assignment_pi.js",
|
|
75
|
+
"scripts/enforcement/storage_owner_lock_pi.js",
|
|
76
|
+
"scripts/enforcement/durable_driver_pi.js",
|
|
77
|
+
"scripts/enforcement/durable_driver_pi.d.ts",
|
|
78
|
+
"docs/durable-driver-api.md",
|
|
79
|
+
"scripts/enforcement/outward_effect_pi.js",
|
|
80
|
+
"docs/durable-persistence.md",
|
|
59
81
|
"scripts/enforcement/board_claim_bridge_pi.js",
|
|
60
82
|
"scripts/enforcement/nudge_classifier_pi.js",
|
|
61
83
|
"scripts/enforcement/nudge_config_pi.js",
|
|
@@ -66,9 +88,13 @@
|
|
|
66
88
|
"scripts/enforcement/nudge_reason_state_pi.js",
|
|
67
89
|
"scripts/enforcement/nudge_validate_pi.js",
|
|
68
90
|
".agentic-driver/nudge.defaults.json",
|
|
91
|
+
".agentic-driver/loop-until-done.defaults.json",
|
|
69
92
|
"README.md",
|
|
70
93
|
"LICENSE",
|
|
71
94
|
"extensions/herdr-lifecycle.ts",
|
|
95
|
+
"extensions/treehouse-recycle.ts",
|
|
96
|
+
"scripts/enforcement/treehouse_recycle_pi.js",
|
|
97
|
+
"config/treehouse.v1.json",
|
|
72
98
|
"scripts/enforcement/herdr_lifecycle_pi.js",
|
|
73
99
|
"config/microvm-target.v1.example.json",
|
|
74
100
|
"config/microvm-target.v1.json",
|
|
@@ -85,6 +111,7 @@
|
|
|
85
111
|
".githooks/pre-commit",
|
|
86
112
|
"templates/AGENTS.md",
|
|
87
113
|
"scripts/enforcement/task_board_core_pi.js",
|
|
114
|
+
"scripts/enforcement/task_board_hash_pi.js",
|
|
88
115
|
"scripts/enforcement/pulse_core_pi.js",
|
|
89
116
|
"scripts/enforcement/pulse_scheduler_pi.js",
|
|
90
117
|
"scripts/enforcement/router_engine_pi.js",
|
|
@@ -97,6 +124,69 @@
|
|
|
97
124
|
"skills/pulse/SKILL.md",
|
|
98
125
|
"skills/tasks/SKILL.md",
|
|
99
126
|
"skills/decision-to-spec/SKILL.md",
|
|
127
|
+
"extensions/harvest-lessons.ts",
|
|
128
|
+
"scripts/harvest_lessons.js",
|
|
129
|
+
"skills/harvest-lessons",
|
|
130
|
+
"skills/public-pr-wording/SKILL.md",
|
|
131
|
+
"skills/public-pr-wording/pr_wording_lint.py",
|
|
132
|
+
"skills/public-pr-wording/pr_wording_rules.json",
|
|
133
|
+
"skills/public-pr-wording/pr_wording_config.example.json",
|
|
134
|
+
"skills/how/LICENSE",
|
|
135
|
+
"skills/how/WORKFLOW_BOUNDARIES.md",
|
|
136
|
+
"skills/architect-pi",
|
|
137
|
+
"skills/arena-pi",
|
|
138
|
+
"skills/automate-me-pi",
|
|
139
|
+
"skills/benchmark-checklist-pi",
|
|
140
|
+
"skills/blast-radius-pi",
|
|
141
|
+
"skills/bro-pi",
|
|
142
|
+
"skills/correct-pi",
|
|
143
|
+
"skills/create-verification-skill-pi",
|
|
144
|
+
"skills/figure-it-out-pi",
|
|
145
|
+
"skills/how-pi",
|
|
146
|
+
"skills/in-your-own-words-pi",
|
|
147
|
+
"skills/interrogate-pi",
|
|
148
|
+
"skills/ketchup-pi",
|
|
149
|
+
"skills/maintain-verification-skill-pi",
|
|
150
|
+
"skills/make-bot-ui-pi",
|
|
151
|
+
"skills/no-comments-pi",
|
|
152
|
+
"skills/poteto-help-pi",
|
|
153
|
+
"skills/poteto-mode-pi",
|
|
154
|
+
"skills/principle-attack-the-premise-pi",
|
|
155
|
+
"skills/principle-boundary-discipline-pi",
|
|
156
|
+
"skills/principle-build-the-lever-pi",
|
|
157
|
+
"skills/principle-encode-lessons-in-structure-pi",
|
|
158
|
+
"skills/principle-exhaust-the-design-space-pi",
|
|
159
|
+
"skills/principle-experience-first-pi",
|
|
160
|
+
"skills/principle-explain-the-number-pi",
|
|
161
|
+
"skills/principle-fix-root-causes-pi",
|
|
162
|
+
"skills/principle-foundational-thinking-pi",
|
|
163
|
+
"skills/principle-guard-the-context-window-pi",
|
|
164
|
+
"skills/principle-laziness-protocol-pi",
|
|
165
|
+
"skills/principle-make-operations-idempotent-pi",
|
|
166
|
+
"skills/principle-migrate-callers-then-delete-legacy-apis-pi",
|
|
167
|
+
"skills/principle-minimize-reader-load-pi",
|
|
168
|
+
"skills/principle-model-the-domain-pi",
|
|
169
|
+
"skills/principle-never-block-on-the-human-pi",
|
|
170
|
+
"skills/principle-outcome-oriented-execution-pi",
|
|
171
|
+
"skills/principle-prove-it-works-pi",
|
|
172
|
+
"skills/principle-redesign-from-first-principles-pi",
|
|
173
|
+
"skills/principle-separate-before-serializing-shared-state-pi",
|
|
174
|
+
"skills/principle-sequence-verifiable-units-pi",
|
|
175
|
+
"skills/principle-subtract-before-you-add-pi",
|
|
176
|
+
"skills/principle-test-behavior-not-implementation-pi",
|
|
177
|
+
"skills/principle-type-system-discipline-pi",
|
|
178
|
+
"skills/recall-pi",
|
|
179
|
+
"skills/reflect-pi",
|
|
180
|
+
"skills/setup-pstack-pi",
|
|
181
|
+
"skills/sharpen-pi",
|
|
182
|
+
"skills/show-me-your-work-pi",
|
|
183
|
+
"skills/swarm-pi",
|
|
184
|
+
"skills/tdd-pi",
|
|
185
|
+
"skills/teach-pi",
|
|
186
|
+
"skills/technical-writing-pi",
|
|
187
|
+
"skills/typescript-best-practices-pi",
|
|
188
|
+
"skills/unslop-pi",
|
|
189
|
+
"skills/why-pi",
|
|
100
190
|
"extensions/tasks.ts",
|
|
101
191
|
"scripts/enforcement/session_tasks_core_pi.js",
|
|
102
192
|
"scripts/enforcement/session_tasks_validate_pi.js",
|
|
@@ -107,14 +197,78 @@
|
|
|
107
197
|
"./extensions"
|
|
108
198
|
],
|
|
109
199
|
"skills": [
|
|
110
|
-
"./skills/decision-to-spec"
|
|
200
|
+
"./skills/decision-to-spec",
|
|
201
|
+
"./skills/pulse",
|
|
202
|
+
"./skills/tasks",
|
|
203
|
+
"./skills/harvest-lessons",
|
|
204
|
+
"./skills/public-pr-wording",
|
|
205
|
+
"./skills/architect-pi",
|
|
206
|
+
"./skills/arena-pi",
|
|
207
|
+
"./skills/automate-me-pi",
|
|
208
|
+
"./skills/benchmark-checklist-pi",
|
|
209
|
+
"./skills/blast-radius-pi",
|
|
210
|
+
"./skills/bro-pi",
|
|
211
|
+
"./skills/correct-pi",
|
|
212
|
+
"./skills/create-verification-skill-pi",
|
|
213
|
+
"./skills/figure-it-out-pi",
|
|
214
|
+
"./skills/how-pi",
|
|
215
|
+
"./skills/in-your-own-words-pi",
|
|
216
|
+
"./skills/interrogate-pi",
|
|
217
|
+
"./skills/ketchup-pi",
|
|
218
|
+
"./skills/maintain-verification-skill-pi",
|
|
219
|
+
"./skills/make-bot-ui-pi",
|
|
220
|
+
"./skills/no-comments-pi",
|
|
221
|
+
"./skills/poteto-help-pi",
|
|
222
|
+
"./skills/poteto-mode-pi",
|
|
223
|
+
"./skills/principle-attack-the-premise-pi",
|
|
224
|
+
"./skills/principle-boundary-discipline-pi",
|
|
225
|
+
"./skills/principle-build-the-lever-pi",
|
|
226
|
+
"./skills/principle-encode-lessons-in-structure-pi",
|
|
227
|
+
"./skills/principle-exhaust-the-design-space-pi",
|
|
228
|
+
"./skills/principle-experience-first-pi",
|
|
229
|
+
"./skills/principle-explain-the-number-pi",
|
|
230
|
+
"./skills/principle-fix-root-causes-pi",
|
|
231
|
+
"./skills/principle-foundational-thinking-pi",
|
|
232
|
+
"./skills/principle-guard-the-context-window-pi",
|
|
233
|
+
"./skills/principle-laziness-protocol-pi",
|
|
234
|
+
"./skills/principle-make-operations-idempotent-pi",
|
|
235
|
+
"./skills/principle-migrate-callers-then-delete-legacy-apis-pi",
|
|
236
|
+
"./skills/principle-minimize-reader-load-pi",
|
|
237
|
+
"./skills/principle-model-the-domain-pi",
|
|
238
|
+
"./skills/principle-never-block-on-the-human-pi",
|
|
239
|
+
"./skills/principle-outcome-oriented-execution-pi",
|
|
240
|
+
"./skills/principle-prove-it-works-pi",
|
|
241
|
+
"./skills/principle-redesign-from-first-principles-pi",
|
|
242
|
+
"./skills/principle-separate-before-serializing-shared-state-pi",
|
|
243
|
+
"./skills/principle-sequence-verifiable-units-pi",
|
|
244
|
+
"./skills/principle-subtract-before-you-add-pi",
|
|
245
|
+
"./skills/principle-test-behavior-not-implementation-pi",
|
|
246
|
+
"./skills/principle-type-system-discipline-pi",
|
|
247
|
+
"./skills/recall-pi",
|
|
248
|
+
"./skills/reflect-pi",
|
|
249
|
+
"./skills/setup-pstack-pi",
|
|
250
|
+
"./skills/sharpen-pi",
|
|
251
|
+
"./skills/show-me-your-work-pi",
|
|
252
|
+
"./skills/swarm-pi",
|
|
253
|
+
"./skills/tdd-pi",
|
|
254
|
+
"./skills/teach-pi",
|
|
255
|
+
"./skills/technical-writing-pi",
|
|
256
|
+
"./skills/typescript-best-practices-pi",
|
|
257
|
+
"./skills/unslop-pi",
|
|
258
|
+
"./skills/why-pi"
|
|
111
259
|
]
|
|
112
260
|
},
|
|
113
261
|
"dependencies": {
|
|
262
|
+
"@earendil-works/chord": "1.0.4",
|
|
263
|
+
"@earendil-works/pi-ai": "1.0.4",
|
|
264
|
+
"@earendil-works/pi-durable": "1.0.4",
|
|
114
265
|
"typescript": "5.9.2"
|
|
115
266
|
},
|
|
116
267
|
"repository": {
|
|
117
268
|
"type": "git",
|
|
118
269
|
"url": "git+https://github.com/evoclock/pi-agentic-driver.git"
|
|
270
|
+
},
|
|
271
|
+
"devDependencies": {
|
|
272
|
+
"@earendil-works/pi-coding-agent": "1.2.0"
|
|
119
273
|
}
|
|
120
274
|
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
// Relayed approval records are attestations by configured relayers, not proof of owner identity.
|
|
3
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
4
|
+
import { appendFileSync, readFileSync, writeFileSync, openSync, closeSync, mkdirSync, statSync, lstatSync, unlinkSync } from "node:fs";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { setTimeout as sleep } from "node:timers/promises";
|
|
7
|
+
|
|
8
|
+
const digest = (value) => createHash("sha256").update(value).digest("hex");
|
|
9
|
+
const validDate = (value) => typeof value === "string" && /(?:Z|[+-]\d\d:\d\d)$/.test(value) && Number.isFinite(Date.parse(value));
|
|
10
|
+
export function approvalPaths(profile) {
|
|
11
|
+
if (!profile) throw new Error("profile directory required");
|
|
12
|
+
return { ledger: join(profile, "approvals/approvals.v1.jsonl"), head: join(profile, "approvals/head.v1.json"), lock: join(profile, "approvals/writer.lock"), uses: join(profile, "approvals/uses.v1.jsonl"), relayers: join(profile, "config/relay-authority.v1.json") };
|
|
13
|
+
}
|
|
14
|
+
export function verifyApprovals(file, headFile = join(file, "../head.v1.json")) {
|
|
15
|
+
for (const candidate of [file, headFile]) {
|
|
16
|
+
try { if (!lstatSync(candidate).isFile()) throw new Error("approval file must be regular"); }
|
|
17
|
+
catch (error) { if (error.code !== "ENOENT") throw error; }
|
|
18
|
+
}
|
|
19
|
+
let raw;
|
|
20
|
+
try { raw = readFileSync(file, "utf8"); } catch (error) {
|
|
21
|
+
if (error.code !== "ENOENT") throw error;
|
|
22
|
+
try { readFileSync(headFile, "utf8"); } catch (headError) { if (headError.code === "ENOENT") return []; throw headError; }
|
|
23
|
+
throw new Error("approval ledger missing but head exists");
|
|
24
|
+
}
|
|
25
|
+
if (raw && !raw.endsWith("\n")) throw new Error("incomplete approval ledger");
|
|
26
|
+
const entries = [];
|
|
27
|
+
let previous = null;
|
|
28
|
+
const ids = new Set();
|
|
29
|
+
for (const line of raw.split("\n").filter(Boolean)) {
|
|
30
|
+
const entry = JSON.parse(line);
|
|
31
|
+
const { hash, ...body } = entry;
|
|
32
|
+
if (entry.schema !== "relay-approval.v1" || entry.prevHash !== previous || hash !== digest(JSON.stringify(body)) || ids.has(entry.id)) throw new Error("approval chain invalid");
|
|
33
|
+
ids.add(entry.id);
|
|
34
|
+
previous = hash;
|
|
35
|
+
entries.push(entry);
|
|
36
|
+
}
|
|
37
|
+
let head;
|
|
38
|
+
try { head = JSON.parse(readFileSync(headFile, "utf8")); } catch (error) { if (error.code !== "ENOENT") throw error; }
|
|
39
|
+
if (head ? head.count !== entries.length || head.lastHash !== previous : entries.length !== 0) throw new Error("approval head mismatch");
|
|
40
|
+
return entries;
|
|
41
|
+
}
|
|
42
|
+
// O_EXCL prevents competing writers. A lock is stale only when both aged and
|
|
43
|
+
// its PID is demonstrably gone; uncertainty fails closed.
|
|
44
|
+
export async function withApprovalLock(profile, operation) {
|
|
45
|
+
const { lock } = approvalPaths(profile);
|
|
46
|
+
mkdirSync(join(profile, "approvals"), { recursive: true });
|
|
47
|
+
for (let attempt = 0; attempt < 50; attempt++) {
|
|
48
|
+
let fd;
|
|
49
|
+
try { fd = openSync(lock, "wx", 0o600); }
|
|
50
|
+
catch (error) {
|
|
51
|
+
if (error.code !== "EEXIST") throw error;
|
|
52
|
+
const snapshot = statSync(lock);
|
|
53
|
+
if (Date.now() - snapshot.mtimeMs > 60000) {
|
|
54
|
+
let owner;
|
|
55
|
+
try { owner = JSON.parse(readFileSync(lock, "utf8")); } catch { /* incomplete lock: fail closed */ }
|
|
56
|
+
if (Number.isSafeInteger(owner?.pid) && owner.pid > 0) {
|
|
57
|
+
try { process.kill(owner.pid, 0); } catch (probe) {
|
|
58
|
+
if (probe.code === "ESRCH" && statSync(lock).ino === snapshot.ino) { unlinkSync(lock); continue; }
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
await sleep(100);
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
writeFileSync(fd, JSON.stringify({ pid: process.pid, at: new Date().toISOString() }));
|
|
67
|
+
return await operation();
|
|
68
|
+
} finally { closeSync(fd); unlinkSync(lock); }
|
|
69
|
+
}
|
|
70
|
+
throw new Error("approval writer lock unavailable");
|
|
71
|
+
}
|
|
72
|
+
export async function recordApproval(profile, input) {
|
|
73
|
+
const paths = approvalPaths(profile);
|
|
74
|
+
const config = JSON.parse(readFileSync(paths.relayers, "utf8"));
|
|
75
|
+
const allowed = new Set(["kind", "relayer", "channel", "sourceMessageId", "quote", "quotedQuestion", "givenAt", "scope", "tier", "expiresAt", "maxUses"]);
|
|
76
|
+
if (!input || Object.keys(input).some((key) => !allowed.has(key))) throw new Error("unexpected approval field");
|
|
77
|
+
if (!Number.isInteger(input.tier) || input.tier < 1 || input.tier > 2 || !config.relayers?.some((r) => r.relayer === input.relayer && r.channel === input.channel && r.maxTier >= input.tier)) throw new Error("untrusted relayer or tier");
|
|
78
|
+
if (!["relay", "direct", "standing"].includes(input.kind) || !input.quote?.trim() || !input.quotedQuestion?.trim() || !input.sourceMessageId?.trim() || !validDate(input.givenAt) || !input.scope?.repo || !Array.isArray(input.scope.actions)) throw new Error("incomplete approval");
|
|
79
|
+
const scope = input.scope;
|
|
80
|
+
if (typeof scope.repo !== "string" || !/^[a-z][a-z0-9-]{0,63}$/.test(scope.repo)
|
|
81
|
+
|| Object.keys(scope).some((key) => !["repo", "actions", "paths", "prs", "branches", "models", "roles"].includes(key))
|
|
82
|
+
|| ["actions", "paths", "prs", "branches", "models", "roles"].some((key) => scope[key] !== undefined && (!Array.isArray(scope[key]) || scope[key].some((value) => typeof value !== "string" || !value.trim())))) throw new Error("invalid approval scope");
|
|
83
|
+
const now = new Date();
|
|
84
|
+
if (Date.parse(input.givenAt) > now.getTime() || (input.expiresAt && (!validDate(input.expiresAt) || Date.parse(input.expiresAt) <= now.getTime()))) throw new Error("invalid approval time");
|
|
85
|
+
const expiresAt = input.expiresAt ?? (input.kind === "standing" ? new Date(now.getTime() + 7 * 86400000).toISOString() : null);
|
|
86
|
+
if (input.maxUses != null && (!Number.isSafeInteger(input.maxUses) || input.maxUses < 1)) throw new Error("invalid use limit");
|
|
87
|
+
return withApprovalLock(profile, () => {
|
|
88
|
+
const entries = verifyApprovals(paths.ledger, paths.head);
|
|
89
|
+
const body = { ...input, scope: { paths: [], prs: [], branches: [], models: [], roles: [], ...input.scope }, schema: "relay-approval.v1", id: randomUUID(), prevHash: entries.at(-1)?.hash ?? null, recordedAt: now.toISOString(), expiresAt };
|
|
90
|
+
const entry = { ...body, hash: digest(JSON.stringify(body)) };
|
|
91
|
+
appendFileSync(paths.ledger, JSON.stringify(entry) + "\n", { flag: "a", mode: 0o600 });
|
|
92
|
+
writeFileSync(paths.head, JSON.stringify({ count: entries.length + 1, lastHash: entry.hash }) + "\n", { mode: 0o600 });
|
|
93
|
+
return entry;
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
export function authorizeApproval(profile, id, { tier, repository, action, model, role, path, pr, branch }, now = new Date()) {
|
|
97
|
+
const paths = approvalPaths(profile);
|
|
98
|
+
const entries = verifyApprovals(paths.ledger);
|
|
99
|
+
const entry = entries.find((item) => item.id === id);
|
|
100
|
+
const config = JSON.parse(readFileSync(paths.relayers, "utf8"));
|
|
101
|
+
const trusted = config.relayers?.some((r) => r.relayer === entry?.relayer && r.channel === entry?.channel && r.maxTier >= tier);
|
|
102
|
+
const scope = entry?.scope;
|
|
103
|
+
const used = (() => { try { return readFileSync(paths.uses, "utf8").split("\n").filter(Boolean).map(JSON.parse).filter((use) => use.ledgerId === id && use.result === "reserved").length; } catch (error) { if (error.code === "ENOENT") return 0; throw error; } })();
|
|
104
|
+
if (!entry || config.revokedIds?.includes(id) || tier > 2 || entry.tier < tier || !trusted || !scope || scope.repo !== repository || (tier === 2 && !entry.quote.includes(repository)) || !scope.actions?.includes(action) || (model && !scope.models?.includes(model)) || (role && !scope.roles?.includes(role)) || (path && !scope.paths?.includes(path)) || (pr && !scope.prs?.includes(pr)) || (branch && !scope.branches?.includes(branch)) || (entry.expiresAt && Date.parse(entry.expiresAt) <= now.getTime()) || (entry.maxUses != null && used >= entry.maxUses)) throw new Error("approval does not cover action");
|
|
105
|
+
return entry;
|
|
106
|
+
}
|
|
107
|
+
// The header names candidate records only. Each requested action is checked
|
|
108
|
+
// against stored scope; prose in the request does not add permissions.
|
|
109
|
+
export function checkRelayRequests(profile, header, requests) {
|
|
110
|
+
const match = /^\[relay:v1 ledger=([a-f0-9-]{36}(?:,[a-f0-9-]{36})*) relayer=([a-z][a-z0-9-]*)\]$/.exec(header ?? "");
|
|
111
|
+
return requests.map((request) => {
|
|
112
|
+
if (match && request.tier < 3) {
|
|
113
|
+
for (const id of match[1].split(",")) {
|
|
114
|
+
try {
|
|
115
|
+
const entry = authorizeApproval(profile, id, request);
|
|
116
|
+
if (entry.relayer === match[2]) return { status: "covered", ledgerId: id, tier: request.tier };
|
|
117
|
+
} catch { /* No record covers this particular action. */ }
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return { status: "needs-owner", tier: request.tier, missing: [request.action], relayablePhrase: `Approve ${request.action} for ${request.repository}`, nextStep: request.tier === 3 ? "Request direct owner approval at the action boundary." : "Relay the scoped approval with its source and record it." };
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
export async function reserveApprovalUse(profile, ledgerId, request, operation) {
|
|
124
|
+
return withApprovalLock(profile, async () => {
|
|
125
|
+
const entry = authorizeApproval(profile, ledgerId, request);
|
|
126
|
+
appendFileSync(approvalPaths(profile).uses, JSON.stringify({ use: randomUUID(), ledgerId, action: request.action, target: request.repository, result: "reserved", at: new Date().toISOString() }) + "\n", { mode: 0o600 });
|
|
127
|
+
return operation ? operation(entry) : entry;
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
export async function recordApprovalUse(profile, ledgerId, action, target, result) {
|
|
131
|
+
return withApprovalLock(profile, () => {
|
|
132
|
+
const path = approvalPaths(profile).uses;
|
|
133
|
+
appendFileSync(path, JSON.stringify({ use: randomUUID(), ledgerId, action, target, result, at: new Date().toISOString() }) + "\n", { mode: 0o600 });
|
|
134
|
+
});
|
|
135
|
+
}
|