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
16 changes: 15 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,13 +157,19 @@ Local data is stored in `~/.hasna/configs/` (unchanged, for fleet continuity).

`instructions session plan` and `instructions session apply` render
OpenIdentities and instruction sources into provider-native files for Claude,
Codex, Cursor, OpenCode, Codewith, aicopilot, and Google Antigravity.
Codex, Cursor, OpenCode, Codewith, Qwen, aicopilot, and Google Antigravity.
The old Google agent target is removed; Antigravity is the only Google coding
agent render target. Antigravity workspace rules are rendered to
`.agents/rules/*.md`; its current global rules and MCP files use Google's
legacy-named `~/.gemini/GEMINI.md` and `~/.gemini/config/mcp_config.json`
paths but remain owned by the `antigravity` target.

Qwen Code session rendering writes `QWEN.md` instructional context with
`QWEN_HOME` pointing at the rendered profile home. Known config sync also
tracks Qwen Code `QWEN.md` and `settings.json` files at `~/.qwen/...` and
project `QWEN.md` / `.qwen/settings.json`, so native hook settings can be
managed without claiming session-rendered context is hard enforcement.

```bash
instructions session plan \
--tool codewith \
Expand Down Expand Up @@ -193,6 +199,14 @@ push safety, no brittle hardcoding when source-of-truth or reusable abstractions
exist, autonomous repair, Hasna CLI source-of-truth usage, conversation surface
routing, and unbudgeted Codewith goals unless a user asks for budgets.

They also seed `dangerous-operation-guard-standard`, the managed station01 guard
source for risky shell commands, edits, git operations, package installs, and
secret-adjacent access. The guard excludes Gemini CLI, requires Codewith/Codex
`PreToolUse` to hard-deny or inject context rather than ask for approval, uses
`PermissionRequest` for Codewith/Codex approvals, records that Qwen `QWEN.md`
is policy context only, and records native hook or wrapper/plugin fallback
expectations for Claude, Qwen, OpenCode, Cursor, and Antigravity.

## Machine-aware Profiles

`instructions init` seeds two platform profiles:
Expand Down
2 changes: 1 addition & 1 deletion bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

31 changes: 26 additions & 5 deletions docs/managed-prompt-hierarchy.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ Render sources are composed in this order:
| Layer | Purpose |
| --- | --- |
| `global` | Non-overridable Hasna rules and default agent behavior. |
| `tool` | Tool/provider-specific behavior such as Codewith, Claude, Codex, Cursor, OpenCode, Antigravity, or aicopilot. |
| `tool` | Tool/provider-specific behavior such as Codewith, Claude, Codex, Qwen, Cursor, OpenCode, Antigravity, or aicopilot. |
| `account` | Account or auth-profile overlays such as `live-codewith`. |
| `machine` | Host-specific rules for machines such as `spark01`, Apple laptops, or fleet nodes. |
| `division` | Area-level rules for repo families such as `opensource`, `hasnastudio`, `hasnatools`, or `infra`. |
Expand All @@ -43,6 +43,7 @@ continue to map to `repo`; `machine-overlay` maps to `machine`; and
| `cursor` | Project-owned `.cursor/rules/*.mdc`. |
| `opencode` | `AGENTS.md`, `opencode.json`, and `.hasna/instructions` fragments. |
| `aicopilot` | `AICOPILOT.md`, with optional `aicopilot.json` instructions in a later pass. |
| `qwen` | Profile-scoped `QWEN.md` session render/apply output; known sync also tracks `~/.qwen/QWEN.md`, `~/.qwen/settings.json`, project `QWEN.md`, and `.qwen/settings.json`. Native hooks should be represented in settings when verified. |
| `antigravity` | Project-owned `.agents/rules/*.md`, workspace MCP at `.agents/mcp_config.json`, and Google's current legacy-named global Antigravity files at `~/.gemini/GEMINI.md` and `~/.gemini/config/mcp_config.json`. |

The retired Google coding-agent target has no active render path. Do not create
Expand Down Expand Up @@ -96,16 +97,36 @@ The managed global prompt must include these rules:
10. Never expose secrets in prompts, tasks, memories, conversations, manifests,
reports, logs, or PR text. Reference vault item names and grants only.

## Dangerous Operation Guard

`instructions init` and `bun run seed` also seed
`dangerous-operation-guard-standard`, the managed rollout source for risky
operation policy on sustained station01 coding agents.

The guard covers Codewith, Codex, Claude Code, Qwen Code, OpenCode, Cursor, and
Google Antigravity. Gemini CLI remains excluded. Codewith and Codex must use
managed `PreToolUse` as a hard-deny/context-injection surface and
`PermissionRequest` for approvals; `PreToolUse` must not return ask-style
approval decisions. Qwen `QWEN.md` rendering is policy context only; hard
enforcement for Qwen requires native hook settings or a managed wrapper/plugin.
Claude and Qwen should use native hook settings where available. OpenCode,
Cursor, and Antigravity must use a verified native plugin/config path or an
explicitly managed wrapper/plugin fallback before hard enforcement is claimed.

## Implementation Notes

The implementation is incremental:

1. Extend the session render layer model and aliases.
2. Add active Antigravity support and remove the retired Google agent from active config sync.
3. Keep Codewith behavior compatible with existing flattened renders and the
3. Add Qwen Code `QWEN.md` session rendering and track Qwen settings as an
active config owner without adding a retired Gemini target.
4. Keep Codewith behavior compatible with existing flattened renders and the
`HASNA_CONFIGS_CODEWITH_NATIVE_IMPORTS` gate.
4. Seed the managed `global-agent-rules-standard` reference so canonical global
5. Seed the managed `global-agent-rules-standard` reference so canonical global
prompt content includes the required operating rules.
5. Add tests that prove layer ordering, Antigravity output paths, the
6. Seed the managed `dangerous-operation-guard-standard` reference so station01
guard requirements can roll out through instructions-managed config.
7. Add tests that prove layer ordering, Antigravity output paths, the
Antigravity 12,000-character rule-file limit, active agent target coverage,
and the seeded global rules content.
and the seeded global rules and dangerous-operation guard content.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@hasna/instructions",
"version": "0.4.6",
"version": "0.4.7",
"description": "AI coding agent instruction & configuration manager \u2014 store, version, apply, and share all your AI coding configs. CLI + MCP + HTTP API (instructions-serve) + generated SDK + Dashboard.",
"type": "module",
"main": "dist/index.js",
Expand Down
4 changes: 4 additions & 0 deletions scripts/seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { LocalConfigStore } from "../src/data/config-store";
import { ensurePlatformProfiles } from "../src/lib/platform-profiles";
import { ensureProjectDashboardStandardConfig } from "../src/lib/project-dashboard-standard";
import { ensureGlobalAgentRulesStandardConfig } from "../src/lib/global-agent-rules-standard";
import { ensureDangerousOperationGuardStandardConfig } from "../src/lib/dangerous-operation-guard-standard";

const db = getDatabase();
const store = new LocalConfigStore(db);
Expand Down Expand Up @@ -87,6 +88,9 @@ for (const ref of refs) {
const globalAgentRulesStandard = await ensureGlobalAgentRulesStandardConfig(store);
console.log(` = ${globalAgentRulesStandard.slug}`);

const dangerousOperationGuardStandard = await ensureDangerousOperationGuardStandardConfig(store);
console.log(` = ${dangerousOperationGuardStandard.slug}`);

const projectDashboardStandard = await ensureProjectDashboardStandardConfig(store);
console.log(` = ${projectDashboardStandard.slug}`);

Expand Down
2 changes: 1 addition & 1 deletion sdk/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export interface Config {
slug: string;
kind: "file" | "reference";
category: "agent" | "rules" | "mcp" | "shell" | "secrets_schema" | "workspace" | "git" | "tools";
agent: "claude" | "codex" | "antigravity" | "opencode" | "cursor" | "codewith" | "aicopilot" | "zsh" | "git" | "npm" | "global";
agent: "claude" | "codex" | "antigravity" | "opencode" | "cursor" | "codewith" | "aicopilot" | "qwen" | "zsh" | "git" | "npm" | "global";
target_path: string | null;
outputs: ConfigOutput[];
format: "text" | "json" | "toml" | "yaml" | "markdown" | "ini";
Expand Down
6 changes: 4 additions & 2 deletions src/cli/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { planSessionRender, resolveSessionPath, sourceFromConfig, sourceFromFile
import { ensurePlatformProfiles } from "../lib/platform-profiles.js";
import { ensureProjectDashboardStandardConfig } from "../lib/project-dashboard-standard.js";
import { ensureGlobalAgentRulesStandardConfig } from "../lib/global-agent-rules-standard.js";
import { ensureDangerousOperationGuardStandardConfig } from "../lib/dangerous-operation-guard-standard.js";
import { getConfigsStatus } from "../status.js";
import { resolveConfigStore, isCloudMode, type ConfigStore } from "../data/config-store.js";
import { DEFAULT_LIST_LIMIT, paginate, parseLimit, truncateMiddle, truncateText } from "../lib/compact-output.js";
Expand Down Expand Up @@ -425,8 +426,8 @@ program
// ── sync ─────────────────────────────────────────────────────────────────────
program
.command("sync")
.description("Sync known AI coding configs from disk into DB (claude, codex, opencode, cursor, codewith, aicopilot, antigravity, zsh, git, npm)")
.option("-a, --agent <agent>", "only sync configs for this agent (claude|codex|opencode|cursor|codewith|aicopilot|antigravity|zsh|git|npm)")
.description("Sync known AI coding configs from disk into DB (claude, codex, opencode, cursor, codewith, aicopilot, antigravity, qwen, zsh, git, npm)")
.option("-a, --agent <agent>", "only sync configs for this agent (claude|codex|opencode|cursor|codewith|aicopilot|antigravity|qwen|zsh|git|npm)")
.option("-c, --category <cat>", "only sync configs in this category")
.option("-p, --project [dir]", "sync project-scoped configs (CLAUDE.md, .mcp.json, etc.) from a project dir")
.option("--all", "with --project: scan all subdirs for projects to sync")
Expand Down Expand Up @@ -1163,6 +1164,7 @@ program
}
}
await ensureGlobalAgentRulesStandardConfig(store);
await ensureDangerousOperationGuardStandardConfig(store);
await ensureProjectDashboardStandardConfig(store);

// Create default profile
Expand Down
5 changes: 5 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,11 @@ export {
GLOBAL_AGENT_RULES_STANDARD_SLUG,
ensureGlobalAgentRulesStandardConfig,
} from "./lib/global-agent-rules-standard.js";
export {
DANGEROUS_OPERATION_GUARD_STANDARD_CONTENT,
DANGEROUS_OPERATION_GUARD_STANDARD_SLUG,
ensureDangerousOperationGuardStandardConfig,
} from "./lib/dangerous-operation-guard-standard.js";

// Lib — sync
export { syncKnown, syncToDisk, syncProject, diffConfig, detectCategory, detectAgent, detectFormat, KNOWN_CONFIGS, PROJECT_CONFIG_FILES } from "./lib/sync.js";
Expand Down
154 changes: 154 additions & 0 deletions src/lib/dangerous-operation-guard-standard.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import { beforeEach, describe, expect, test } from "bun:test";
import type { Database } from "bun:sqlite";
import { LocalConfigStore } from "../data/config-store";
import { createConfig, getConfig } from "../db/configs";
import { getDatabase, resetDatabase } from "../db/database";
import { getProfileConfigs } from "../db/profiles";
import { ensurePlatformProfiles } from "./platform-profiles";
import { planSessionRender, sourceFromConfig } from "./session-render";
import {
DANGEROUS_OPERATION_GUARD_STANDARD_CONTENT,
DANGEROUS_OPERATION_GUARD_STANDARD_SLUG,
ensureDangerousOperationGuardStandardConfig,
} from "./dangerous-operation-guard-standard";

let db: Database;

beforeEach(() => {
resetDatabase();
process.env["HASNA_INSTRUCTIONS_DB_PATH"] = ":memory:";
db = getDatabase();
});

describe("dangerous operation guard standard", () => {
test("seeds managed dangerous-operation guard rules with runtime-specific clauses", async () => {
const config = await ensureDangerousOperationGuardStandardConfig(new LocalConfigStore(db));

expect(config.slug).toBe(DANGEROUS_OPERATION_GUARD_STANDARD_SLUG);
expect(config.kind).toBe("reference");
expect(config.category).toBe("rules");
expect(config.agent).toBe("global");
expect(config.tags).toEqual(expect.arrayContaining(["dangerous-operation-guard", "hooks", "station01"]));

const content = config.content;
expect(content).toContain("sustained Hasna\ncoding agents on station01");
expect(content).toContain("Apply this guard to sustained Codewith, Codex, Claude Code, Qwen Code");
expect(content).toContain("OpenCode, Cursor, and Google Antigravity");
expect(content).toContain("Do not target Gemini CLI");
expect(content).toContain("Keep guard policy in managed instructions/config");
expect(content).toContain("allow_managed_hooks_only = true");
expect(content).toContain("`PreToolUse` is a hard-deny and context-injection surface");
expect(content).toContain("Approval decisions belong in `PermissionRequest` hooks");
expect(content).toContain("do not return `permissionDecision: \"ask\"` from\n `PreToolUse`");
expect(content).toContain("Session rendering writes Qwen Code `QWEN.md`");
expect(content).toContain("policy context only");
expect(content).toContain("Qwen Code native `settings.json` hooks");
expect(content).toContain("`~/.qwen/settings.json`");
expect(content).toContain("`.qwen/settings.json`");
expect(content).toContain("Cursor rule files are advisory prompt context");
expect(content).toContain("managed wrapper/plugin fallback for enforcement");
expect(content).toContain("must never reintroduce an active\n `gemini` target");
});

test("updates stale seeded guard instead of creating a duplicate", async () => {
createConfig({
name: "Dangerous Operation Guard Standard",
category: "rules",
agent: "global",
format: "markdown",
kind: "reference",
content: "old content",
}, db);

const config = await ensureDangerousOperationGuardStandardConfig(new LocalConfigStore(db));
const stored = getConfig(DANGEROUS_OPERATION_GUARD_STANDARD_SLUG, db);

expect(config.id).toBe(stored.id);
expect(stored.content).toBe(DANGEROUS_OPERATION_GUARD_STANDARD_CONTENT);
expect(stored.version).toBe(2);
});

test("renders through managed guard outputs for all claimed render targets", async () => {
const config = await ensureDangerousOperationGuardStandardConfig(new LocalConfigStore(db));
const source = sourceFromConfig(config);

const codewith = planSessionRender({
tool: "codewith",
profile: "account999",
targetHome: "/tmp/codewith-account999",
sources: [source],
});
expect(codewith.files[0]?.relativePath).toBe("CODEWITH.md");
expect(codewith.files[0]?.content).toContain("Dangerous Operation Guard Standard");
expect(codewith.files[0]?.content).toContain("PermissionRequest");

const codex = planSessionRender({
tool: "codex",
profile: "account999",
targetHome: "/tmp/codex-account999",
sources: [source],
});
expect(codex.files[0]?.relativePath).toBe("AGENTS.md");
expect(codex.files[0]?.content).toContain("PreToolUse");

const claude = planSessionRender({
tool: "claude",
profile: "account999",
targetHome: "/tmp/claude-account999",
sources: [source],
});
expect(claude.files[0]?.relativePath).toBe("CLAUDE.md");
expect(claude.files[0]?.content).toContain("@./.hasna/instructions/01-dangerous-operation-guard-standard.md");
expect(claude.files[1]?.content).toContain("Claude Code native hooks");

const opencode = planSessionRender({
tool: "opencode",
profile: "account999",
targetHome: "/tmp/opencode-account999",
sources: [source],
});
expect(opencode.files[0]?.relativePath).toBe("AGENTS.md");
expect(opencode.files[1]?.relativePath).toBe("opencode.json");
expect(opencode.files[0]?.content).toContain("OpenCode");
expect(JSON.parse(opencode.files[1]!.content)).toMatchObject({
instructions: [".hasna/instructions/01-dangerous-operation-guard-standard.md"],
});

const cursor = planSessionRender({
tool: "cursor",
profile: "account999",
projectRoot: "/tmp/repo",
sources: [source],
});
expect(cursor.files[0]?.relativePath).toBe(".cursor/rules/01-dangerous-operation-guard-standard.mdc");
expect(cursor.files[0]?.content).toContain("Cursor rule files are advisory prompt context");

const qwen = planSessionRender({
tool: "qwen",
profile: "account999",
targetHome: "/tmp/qwen-account999",
sources: [source],
});
expect(qwen.files[0]?.relativePath).toBe("QWEN.md");
expect(qwen.files[0]?.content).toContain("Qwen Code");
expect(qwen.files[0]?.content).toContain("policy context only");

const antigravity = planSessionRender({
tool: "antigravity",
profile: "account999",
projectRoot: "/tmp/repo",
sources: [source],
});
expect(antigravity.files[0]?.relativePath).toBe(".agents/rules/01-dangerous-operation-guard-standard.md");
expect(antigravity.files[0]?.content).toContain("wrapper/plugin fallback");
});

test("platform profiles link the guard standard when present", async () => {
const standard = await ensureDangerousOperationGuardStandardConfig(new LocalConfigStore(db));
const profiles = await ensurePlatformProfiles(new LocalConfigStore(db));

for (const profile of profiles) {
expect(getProfileConfigs(profile.id, db).map((config) => config.id)).toContain(standard.id);
}
});
});
Loading
Loading