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
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Claim: codex-loop-bash-retirement-allowlist-integrity-20260526

claimed-at: 2026-05-26T23:34:00Z
agent: Codex
session: codex/launchd-loop
surface: codex-background-service
origin: codex-launchd-loop
run-id: 20260526T232949Z
branch: claim/codex-loop-bash-retirement-allowlist-integrity-20260526
worktree: /Users/acehack/.local/share/zeta-codex-loop/Zeta-worktrees/codex-loop-bash-retirement-allowlist-integrity-20260526

## Scope

Trajectory: TypeScript / Bun migration.

Bounded step: harden the bash-retirement inventory guard so the retained shell
allowlist is itself checked for duplicate or unsorted entries before it is used
to classify repo `.sh` drift.

## Paths

- tools/hygiene/check-bash-retirement-inventory.ts
- tools/hygiene/check-bash-retirement-inventory.test.ts
- docs/claims/codex-loop-bash-retirement-allowlist-integrity-20260526.md
Comment on lines +3 to +24

## Non-Scope

- No shell-script porting or deletion.
- No changes to the retained shell allowlist membership.
- No edits in the contested root checkout.

## Acceptance Check

- `bun test tools/hygiene/check-bash-retirement-inventory.test.ts`
- `bun run hygiene:check-bash-retirement-inventory`
- `node_modules/.bin/tsc --noEmit -p tsconfig.json`
42 changes: 42 additions & 0 deletions tools/hygiene/check-bash-retirement-inventory.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,21 @@ function splitExpectedRetained(): readonly [string, readonly string[]] {
return [missing, rest];
}

function firstTwoExpectedRetained(): readonly [string, string, readonly string[]] {
const [first, second, ...rest] = EXPECTED_RETAINED_SHELL;
if (first === undefined || second === undefined) {
throw new Error("expected retained shell allowlist must contain at least two entries");
}
return [first, second, rest];
}

describe("buildInventoryReport", () => {
test("accepts the retained shell allowlist", () => {
const report = buildInventoryReport(EXPECTED_RETAINED_SHELL);

expect(hasDrift(report)).toBe(false);
expect(report.allowlistIntegrity.duplicateEntries).toEqual([]);
expect(report.allowlistIntegrity.orderViolations).toEqual([]);
expect(report.retained).toHaveLength(EXPECTED_RETAINED_SHELL.length);
expect(report.drift.unexpected).toEqual([]);
expect(report.drift.missingRetained).toEqual([]);
Expand Down Expand Up @@ -53,6 +63,28 @@ describe("buildInventoryReport", () => {
expect(report.drift.missingRetained).toEqual([missing]);
});

test("flags duplicate allowlist entries before classifying repo shell drift", () => {
const [duplicate, rest] = splitExpectedRetained();
const report = buildInventoryReport(EXPECTED_RETAINED_SHELL, [duplicate, duplicate, ...rest]);

expect(hasDrift(report)).toBe(true);
expect(report.allowlistIntegrity.duplicateEntries).toEqual([duplicate]);
expect(report.allowlistIntegrity.orderViolations).toEqual([]);
expect(report.drift.unexpected).toEqual([]);
expect(report.drift.missingRetained).toEqual([]);
});

test("flags unsorted allowlist entries before classifying repo shell drift", () => {
const [first, second, rest] = firstTwoExpectedRetained();
const report = buildInventoryReport(EXPECTED_RETAINED_SHELL, [second, first, ...rest]);

expect(hasDrift(report)).toBe(true);
expect(report.allowlistIntegrity.duplicateEntries).toEqual([]);
expect(report.allowlistIntegrity.orderViolations).toEqual([{ index: 1, previous: second, current: first }]);
expect(report.drift.unexpected).toEqual([]);
expect(report.drift.missingRetained).toEqual([]);
});

test("matches the current tracked repo shell inventory", () => {
const report = buildInventoryReport(trackedNonLeanShellFilesFromGit());

Expand All @@ -78,4 +110,14 @@ describe("renderReport", () => {
expect(rendered).toContain(`## Missing retained ${RETAINED_SHELL_SCOPE} files`);
expect(rendered).toContain(missing);
});

test("renders allowlist integrity errors before drift sections", () => {
const [duplicate, rest] = splitExpectedRetained();
const rendered = renderReport(buildInventoryReport(EXPECTED_RETAINED_SHELL, [duplicate, duplicate, ...rest]));

expect(rendered).toContain("## Retained shell allowlist integrity errors");
Comment on lines +114 to +118
expect(rendered).toContain("### Duplicate entries");
expect(rendered).toContain(duplicate);
expect(rendered).not.toContain("## Unexpected non-Lean shell files");
});
});
87 changes: 83 additions & 4 deletions tools/hygiene/check-bash-retirement-inventory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,26 @@ interface InventoryDrift {
readonly missingRetained: readonly string[];
}

interface AllowlistOrderViolation {
readonly index: number;
readonly previous: string;
readonly current: string;
}

interface AllowlistIntegrity {
readonly duplicateEntries: readonly string[];
readonly orderViolations: readonly AllowlistOrderViolation[];
}

export interface InventoryReport {
readonly retained: readonly string[];
readonly expectedRetained: readonly string[];
readonly allowlistIntegrity: AllowlistIntegrity;
readonly drift: InventoryDrift;
}

const SPAWN_MAX_BUFFER = 64 * 1024 * 1024;
export const RETAINED_SHELL_SCOPE =
"repo-wide setup/bootstrap/service-wrapper/installer/dev-cluster allowlist";
export const RETAINED_SHELL_SCOPE = "repo-wide setup/bootstrap/service-wrapper/installer/dev-cluster allowlist";

export const EXPECTED_RETAINED_SHELL: readonly string[] = [
".gemini/service/install-lior-service.sh",
Expand Down Expand Up @@ -119,15 +130,56 @@ export function trackedNonLeanShellFilesFromGit(): readonly string[] {

export const trackedNonLeanBashFilesFromGit = trackedNonLeanShellFilesFromGit;

function inspectAllowlistIntegrity(expectedRetained: readonly string[]): AllowlistIntegrity {
const counts = new Map<string, number>();
const orderViolations: AllowlistOrderViolation[] = [];

for (let index = 0; index < expectedRetained.length; index += 1) {
const current = expectedRetained[index];
if (current === undefined) continue;
counts.set(current, (counts.get(current) ?? 0) + 1);

const previous = expectedRetained[index - 1];
if (previous !== undefined && previous.localeCompare(current) > 0) {
orderViolations.push({ index, previous, current });
}
}

const duplicateEntries = [...counts.entries()]
.filter(([, count]) => count > 1)
.map(([file]) => file)
.sort((a, b) => a.localeCompare(b));

return { duplicateEntries, orderViolations };
}

function hasAllowlistIntegrityDrift(integrity: AllowlistIntegrity): boolean {
return integrity.duplicateEntries.length > 0 || integrity.orderViolations.length > 0;
}

export function buildInventoryReport(
retained: readonly string[],
expectedRetained: readonly string[] = EXPECTED_RETAINED_SHELL,
): InventoryReport {
const allowlistIntegrity = inspectAllowlistIntegrity(expectedRetained);
if (hasAllowlistIntegrityDrift(allowlistIntegrity)) {
return {
retained: [...retained].sort((a, b) => a.localeCompare(b)),
expectedRetained: [...expectedRetained],
allowlistIntegrity,
drift: {
unexpected: [],
missingRetained: [],
},
};
}

const retainedSet = new Set(retained);
const expectedSet = new Set(expectedRetained);
return {
retained: [...retained].sort((a, b) => a.localeCompare(b)),
expectedRetained: [...expectedRetained].sort((a, b) => a.localeCompare(b)),
expectedRetained: [...expectedRetained],
allowlistIntegrity,
drift: {
unexpected: retained.filter((file) => !expectedSet.has(file)).sort((a, b) => a.localeCompare(b)),
missingRetained: expectedRetained.filter((file) => !retainedSet.has(file)).sort((a, b) => a.localeCompare(b)),
Expand All @@ -136,7 +188,11 @@ export function buildInventoryReport(
}

export function hasDrift(report: InventoryReport): boolean {
return report.drift.unexpected.length > 0 || report.drift.missingRetained.length > 0;
return (
hasAllowlistIntegrityDrift(report.allowlistIntegrity) ||
report.drift.unexpected.length > 0 ||
report.drift.missingRetained.length > 0
);
}

export function renderReport(report: InventoryReport): string {
Expand All @@ -145,13 +201,36 @@ export function renderReport(report: InventoryReport): string {
lines.push("");
lines.push(`retained_non_lean_shell: ${String(report.retained.length)}`);
lines.push(`expected_retained: ${String(report.expectedRetained.length)}`);
lines.push(`allowlist_duplicates: ${String(report.allowlistIntegrity.duplicateEntries.length)}`);
lines.push(`allowlist_order_violations: ${String(report.allowlistIntegrity.orderViolations.length)}`);
lines.push(`unexpected: ${String(report.drift.unexpected.length)}`);
lines.push(`missing_retained: ${String(report.drift.missingRetained.length)}`);
lines.push("");
if (!hasDrift(report)) {
lines.push(`OK: retained non-Lean shell surface matches ${RETAINED_SHELL_SCOPE}.`);
return `${lines.join("\n")}\n`;
}
if (hasAllowlistIntegrityDrift(report.allowlistIntegrity)) {
lines.push("## Retained shell allowlist integrity errors");
lines.push("");
lines.push("The retained shell allowlist must be unique and sorted before repo shell drift is classified.");
lines.push("");
if (report.allowlistIntegrity.duplicateEntries.length > 0) {
lines.push("### Duplicate entries");
lines.push("");
for (const file of report.allowlistIntegrity.duplicateEntries) lines.push(`- ${file}`);
lines.push("");
}
if (report.allowlistIntegrity.orderViolations.length > 0) {
lines.push("### Out-of-order entries");
lines.push("");
for (const violation of report.allowlistIntegrity.orderViolations) {
lines.push(`- index ${String(violation.index)}: ${violation.previous} > ${violation.current}`);
}
lines.push("");
}
return `${lines.join("\n")}\n`;
}
if (report.drift.unexpected.length > 0) {
lines.push("## Unexpected non-Lean shell files");
lines.push("");
Expand Down
Loading