Skip to content

feat(compile): add a caller system policy, as a prompt modifier - #195

Merged
ethanj merged 1 commit into
mainfrom
feat/compile-system-policy
Aug 25, 2026
Merged

feat(compile): add a caller system policy, as a prompt modifier#195
ethanj merged 1 commit into
mainfrom
feat/compile-system-policy

Conversation

@ethanj

@ethanj ethanj commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Picks up #170, rebased onto current main and finished against the review. @TigerOfCountryYao is credited as co-author. Partly answers #144.

What it does

compile({ systemPolicy }) appends deployment-specific editorial or publication guidance to the built-in compile prompts, for an SDK host that needs it without forking the prompts. Additive rather than a replacement, placed before the source material, and blank or omitted leaves the prompt byte-identical.

It is advisory. A policy makes a model more likely to follow a rule; it cannot make it obey one, and nothing downstream verifies that it did. Anything that must hold belongs in a lint rule or a trust gate, and the docs say so rather than letting the word "policy" imply a boundary.

The substantive change from #170

The policy is registered as a prompt modifier. It alters what the prompt asks for without changing a source byte, so without this a settled project reports "Nothing to compile" and keeps content generated under a policy the operator has already replaced. A pending review candidate has the same problem.

Registering it in activePromptModifiers means it inherits the whole mechanism from #188: invalidation on change, candidate-aware deduplication, the scoped-refresh guard, and per-page provenance. Bumping PROMPT_VERSION alone does not achieve this, since two different policies share a version.

The digest enters the modifier set, never the text. That set is hashed into state.json, stamped onto every page's frontmatter, and published through the JSON export, so carrying the prose would put an operator's editorial instructions into three artifacts that outlive the run.

Smaller than the original

The prompt builders read the policy from run state rather than taking it as a parameter, so their signatures are unchanged and the option is not threaded through extraction, page rendering, the review pipeline and seed pages. withRunSystemPolicy restores the previous value rather than clearing, so overlapping SDK callers on different roots cannot strand a policy they did not set.

Isolation

The policy lives in AsyncLocalStorage, matching quietScope and verboseScope in utils/output.ts. The SDK documents that concurrent calls are fully isolated with no global state, and the compile lock is per root, so two callers compiling different projects genuinely overlap in one process.

A module variable that saves and restores handles nesting and fails on interleaving. Measured on an earlier revision of this branch, two overlapping runs observed ["Policy B", undefined] where they should observe ["Policy A", "Policy B"] - one project reading another's policy, and the other losing its own. That reaches past prompt text, since the policy feeds the digest written to state.json, candidates and page provenance.

Two tests pin it, one on the policy and one on the digest, and both yield before reading: the broken implementation only fails once the runs actually interleave.

Tests

Thirteen unit cases plus five that drive the real pipeline twice over byte-identical sources: policy A to B, policy cleared, policy unchanged, blank treated as absent, and a pending candidate produced under a different policy. Only the pipeline cases can detect the wiring.

Every control is mutation-tested: removing the policy from the modifier set turns 7 red, reverting PROMPT_VERSION turns 1 red, and removing it from the prompts turns 3 red.

On #144

This is the SDK primitive underneath that request, not a closure of it. #144 asks for a SOUL.md-style file, and a file or CLI surface on top of this is a reasonable next step.

Not included

#169's embeddings flag. The feature has no dependency on it, only a shared test file, so this is unstacked.

@ethanj
ethanj force-pushed the feat/compile-system-policy branch from 9743bd4 to 2038a9c Compare August 25, 2026 01:26
Based on #170 by @TigerOfCountryYao, rebased onto current main and finished
against the review. Partly answers #144.

His design is what ships: the additive framing, its wording verbatim
("Additional system policy (follow in addition to every built-in instruction
above):"), the placement after the built-in instructions and before the source
material, blank treated as omitted so the default prompt stays byte-identical,
and the PROMPT_VERSION bump. The invalidation mechanism, the isolation and the
tests below are not his.

`compile({ systemPolicy })` appends deployment-specific editorial or
publication guidance to the built-in compile prompts, for an SDK host that
needs it without forking the prompts. Additive rather than a replacement, and
placed before the source material so the whole instruction block still precedes
the content it describes. Blank or omitted leaves the prompt byte-identical.

It is ADVISORY. A policy makes a model more likely to follow a rule; it cannot
make it obey one, and nothing downstream verifies that it did. Anything that
must hold belongs in a lint rule or a trust gate, and the docs say so rather
than letting "policy" imply a boundary.

The policy is registered as a PROMPT MODIFIER, which is the substantive change
from #170. A policy alters what the prompt asks for without changing a source
byte, so without this a settled project reports "Nothing to compile" and keeps
content generated under a policy the operator has already replaced. Registering
it in activePromptModifiers means it inherits the whole mechanism from #188:
invalidation on change, candidate-aware deduplication so a pending review
candidate produced under one policy does not satisfy a run asking for another,
the scoped-refresh guard, and per-page provenance. Bumping PROMPT_VERSION alone
does not achieve this, because two policies share a version.

The policy lives in AsyncLocalStorage, matching quietScope and verboseScope in
utils/output.ts. The SDK documents that concurrent calls are fully isolated
with no global state, and the compile lock is per ROOT, so two callers
compiling different projects genuinely overlap in one process. A module
variable that saves and restores handles nesting and fails on interleaving:
measured, two overlapping runs observed ["Policy B", undefined] where they
should observe ["Policy A", "Policy B"] — one project reading another's policy
and the other losing its own. That reaches further than prompt text, since the
policy feeds the digest written to state.json, candidates and page provenance.

The DIGEST enters the modifier set, never the text, because that set is hashed
into state.json, stamped onto every page's frontmatter, and published through
the JSON export.

The prompt builders read the policy from run state rather than taking it as a
parameter, so their signatures are unchanged and the option is not threaded
through extraction, page rendering, the review pipeline and seed pages, none of
which otherwise need to know it exists.

PROMPT_VERSION advances to v2, and is now pinned by a test: nothing in the repo
asserted its value before, so a silent revert would mislabel every page
compiled afterwards.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MUdoRq1DJq23aJuhK9QK7X
@ethanj
ethanj force-pushed the feat/compile-system-policy branch from 2038a9c to fd29023 Compare August 25, 2026 02:48
@ethanj
ethanj merged commit b22d6eb into main Aug 25, 2026
3 checks passed
@ethanj
ethanj deleted the feat/compile-system-policy branch August 25, 2026 05:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant