@appchy/jarvis 0.1.37 → 0.1.39

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.
Files changed (108) hide show
  1. package/bin/config-change.mjs +1 -1
  2. package/bin/jarvis.mjs +8 -12
  3. package/bin/pre-tool-use.mjs +2 -2
  4. package/bin/session-start.mjs +1 -1
  5. package/bin/stop.mjs +1 -1
  6. package/bin/user-prompt-submit.mjs +1 -1
  7. package/dist/bin.js +10168 -37341
  8. package/dist/bin.js.map +1 -1
  9. package/dist/data/backends.mjs +469 -0
  10. package/dist/data/chunk-7REP35VA.mjs +462 -0
  11. package/dist/data/chunk-AKQQC5IT.mjs +1224 -0
  12. package/dist/data/chunk-AYOJSS2F.mjs +72 -0
  13. package/dist/data/chunk-RRJ6KKYL.mjs +42 -0
  14. package/dist/data/chunk-YWSWQEJN.mjs +138 -0
  15. package/dist/data/embedders.mjs +80 -0
  16. package/dist/data/finders.mjs +35 -0
  17. package/dist/data/index.mjs +137 -0
  18. package/dist/data/linkers.mjs +83 -0
  19. package/dist/data/mcp.mjs +2624 -0
  20. package/dist/data/persistences.mjs +8 -0
  21. package/dist/data/rerankers.mjs +52 -0
  22. package/dist/data/stores.mjs +38 -0
  23. package/dist/hooks/config-change.js +7 -29
  24. package/dist/hooks/config-change.js.map +1 -1
  25. package/dist/hooks/pre-tool-use.js +129 -32
  26. package/dist/hooks/pre-tool-use.js.map +1 -1
  27. package/dist/hooks/session-start.js +72 -37
  28. package/dist/hooks/session-start.js.map +1 -1
  29. package/dist/hooks/stop.js +140 -32
  30. package/dist/hooks/stop.js.map +1 -1
  31. package/dist/hooks/user-prompt-submit.js +7 -29
  32. package/dist/hooks/user-prompt-submit.js.map +1 -1
  33. package/harness/assets/architecture-template.md +53 -0
  34. package/harness/assets/design-template.md +89 -0
  35. package/harness/assets/domains/architecture.md +43 -0
  36. package/harness/assets/domains/commercial.md +42 -0
  37. package/harness/assets/domains/design.md +42 -0
  38. package/harness/assets/domains/legal.md +43 -0
  39. package/harness/assets/domains/operations.md +39 -0
  40. package/harness/assets/domains/product.md +43 -0
  41. package/harness/assets/domains/quality.md +43 -0
  42. package/harness/assets/domains/security.md +44 -0
  43. package/harness/assets/domains/support.md +45 -0
  44. package/harness/assets/epic-template.md +85 -0
  45. package/harness/assets/feature-template.md +86 -0
  46. package/harness/assets/handoff-template.md +26 -0
  47. package/harness/assets/task-template.md +38 -0
  48. package/harness/assets/version-template.md +43 -0
  49. package/harness/harness/__init__.py +7 -0
  50. package/harness/harness/align.py +582 -0
  51. package/harness/harness/architecture.py +254 -0
  52. package/harness/harness/autonomy.py +374 -0
  53. package/harness/harness/branches.py +408 -0
  54. package/harness/harness/config.py +1482 -0
  55. package/harness/harness/coverage.py +199 -0
  56. package/harness/harness/epic.py +220 -0
  57. package/harness/harness/events.py +153 -0
  58. package/harness/harness/extend.py +99 -0
  59. package/harness/harness/frontmatter.py +218 -0
  60. package/harness/harness/gate.py +591 -0
  61. package/harness/harness/generate.py +267 -0
  62. package/harness/harness/git.py +775 -0
  63. package/harness/harness/ids.py +140 -0
  64. package/harness/harness/kickoff.py +231 -0
  65. package/harness/harness/lint.py +505 -0
  66. package/harness/harness/model.py +364 -0
  67. package/harness/harness/peers.py +187 -0
  68. package/harness/harness/product.py +29 -0
  69. package/harness/harness/registry.py +382 -0
  70. package/harness/harness/report.py +227 -0
  71. package/harness/harness/safety.py +387 -0
  72. package/harness/harness/scaffold.py +129 -0
  73. package/harness/harness/shard.py +63 -0
  74. package/harness/harness/shift.py +348 -0
  75. package/harness/harness/task.py +507 -0
  76. package/harness/harness/tree.py +258 -0
  77. package/harness/harness/version.py +305 -0
  78. package/harness/harness/wrap.py +217 -0
  79. package/harness/hooks/guard.py +259 -0
  80. package/harness/presets/appchy/PRESET.md +717 -0
  81. package/harness/presets/appchy/references/artifacts.md +539 -0
  82. package/harness/presets/appchy/references/graph.md +154 -0
  83. package/harness/presets/appchy/references/operations.md +444 -0
  84. package/harness/presets/appchy/references/research.md +216 -0
  85. package/harness/schema/work.config.schema.json +401 -0
  86. package/harness/test_work.py +5002 -0
  87. package/harness/work.py +534 -0
  88. package/package.json +34 -40
  89. package/bin/config-change.dev.mjs +0 -17
  90. package/bin/jarvis-dev.mjs +0 -30
  91. package/bin/pre-tool-use.dev.mjs +0 -23
  92. package/bin/session-start.dev.mjs +0 -17
  93. package/bin/stop.dev.mjs +0 -17
  94. package/bin/user-prompt-submit.dev.mjs +0 -17
  95. package/dev/bin.js +0 -38821
  96. package/dev/bin.js.map +0 -1
  97. package/dev/env.json +0 -1
  98. package/dev/hooks/config-change.js +0 -110
  99. package/dev/hooks/config-change.js.map +0 -1
  100. package/dev/hooks/pre-tool-use.js +0 -120
  101. package/dev/hooks/pre-tool-use.js.map +0 -1
  102. package/dev/hooks/session-start.js +0 -115
  103. package/dev/hooks/session-start.js.map +0 -1
  104. package/dev/hooks/stop.js +0 -112
  105. package/dev/hooks/stop.js.map +0 -1
  106. package/dev/hooks/user-prompt-submit.js +0 -111
  107. package/dev/hooks/user-prompt-submit.js.map +0 -1
  108. package/dist/env.json +0 -1
@@ -5,33 +5,11 @@ import { appendFile, mkdir } from "fs/promises";
5
5
  import { join } from "path";
6
6
 
7
7
  // src/config.ts
8
- import fs from "fs";
9
8
  import path from "path";
10
9
  import os from "os";
11
- import { fileURLToPath } from "url";
12
- var __filename = fileURLToPath(import.meta.url);
13
- var __dirname = path.dirname(__filename);
14
- function isDev() {
15
- const candidates = [
16
- path.join(__dirname, "env.json"),
17
- path.join(__dirname, "..", "env.json")
18
- ];
19
- for (const envFile of candidates) {
20
- try {
21
- const data = JSON.parse(fs.readFileSync(envFile, "utf-8"));
22
- return data.env === "development";
23
- } catch {
24
- }
25
- }
26
- return false;
27
- }
28
- function resolveConfigDir() {
29
- if (process.env.JARVIS_CONFIG_DIR) return process.env.JARVIS_CONFIG_DIR;
30
- const base = path.join(os.homedir(), ".jarvis");
31
- return isDev() ? path.join(base, "dev") : base;
32
- }
33
10
  function configDir() {
34
- return resolveConfigDir();
11
+ if (process.env.JARVIS_CONFIG_DIR) return process.env.JARVIS_CONFIG_DIR;
12
+ return path.join(os.homedir(), ".jarvis");
35
13
  }
36
14
  function getHooksDir() {
37
15
  return path.join(configDir(), "hooks");
@@ -39,9 +17,9 @@ function getHooksDir() {
39
17
 
40
18
  // src/hooks/drop.ts
41
19
  async function appendDrop(req) {
42
- if (!req.sessionId || !req.kind || !req.uniqId) {
20
+ if (!req.sessionId || !req.type || !req.uniqId) {
43
21
  process.stderr.write(
44
- `[hook] skip ${req.kind ?? "?"} missing field sessionId=${!!req.sessionId} uniqId=${!!req.uniqId}
22
+ `[hook] skip ${req.type ?? "?"} missing field sessionId=${!!req.sessionId} uniqId=${!!req.uniqId}
45
23
  `
46
24
  );
47
25
  return;
@@ -52,7 +30,7 @@ async function appendDrop(req) {
52
30
  `);
53
31
  });
54
32
  const envelope = {
55
- kind: req.kind,
33
+ type: req.type,
56
34
  sessionId: req.sessionId,
57
35
  ts: (/* @__PURE__ */ new Date()).toISOString(),
58
36
  uniqId: req.uniqId,
@@ -62,7 +40,7 @@ async function appendDrop(req) {
62
40
  try {
63
41
  await appendFile(file, JSON.stringify(envelope) + "\n");
64
42
  process.stderr.write(
65
- `[hook] ${req.kind} appended sessionId=${req.sessionId} uniqId=${req.uniqId}
43
+ `[hook] ${req.type} appended sessionId=${req.sessionId} uniqId=${req.uniqId}
66
44
  `
67
45
  );
68
46
  } catch (err) {
@@ -101,7 +79,7 @@ async function main() {
101
79
  ...p.cwd ? { cwd: p.cwd } : {}
102
80
  };
103
81
  await appendDrop({
104
- kind: "user-prompt-submit",
82
+ type: "user-prompt-submit",
105
83
  sessionId,
106
84
  uniqId: `user-prompt-submit-${ts}`,
107
85
  data
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/hooks/drop.ts","../../src/config.ts","../../src/hooks/user-prompt-submit.ts"],"sourcesContent":["/**\n * Shared helpers for CC hook scripts.\n *\n * Each hook script (pre-tool-use, session-start, stop, …) is a tiny\n * stdin → append-line → exit binary. They all write to the same per-\n * session JSONL at `<hooksDir>/<sessionId>.jsonl`. This module\n * concentrates the envelope shape and the atomic append so the per-\n * hook scripts stay <30 lines each.\n *\n * Hot-path constraints: NEVER block, NEVER network, NEVER read user\n * input. Pure stdin parse + filesystem append + exit. Worst-case one\n * `mkdir` + one `appendFile` per fire.\n */\n\nimport { appendFile, mkdir } from \"node:fs/promises\";\nimport { join } from \"node:path\";\n\nimport { getHooksDir } from \"../config\";\n\n/** Wire-format envelope written one-per-line to the hook JSONL.\n * The watcher in `sessions.ts` parses this exact shape — keep in sync. */\nexport interface HookDropEnvelope<TData = unknown> {\n kind: string;\n sessionId: string;\n ts: string;\n /** Idempotency key. Lets the watcher's replay-on-restart skip drops\n * whose effects were already emitted (see `HookCursor.applied`). */\n uniqId: string;\n data: TData;\n}\n\nexport interface AppendDropRequest<TData> {\n kind: string;\n sessionId: string;\n uniqId: string;\n data: TData;\n}\n\n/** Append one envelope line to the per-session hook JSONL. Atomic between\n * hook processes via POSIX `O_APPEND` (Node's `fs.appendFile`) — concurrent\n * appends from different CC sessions / hook kinds can't interleave bytes\n * within a line as long as the line stays < `PIPE_BUF` (4KB). Drops are\n * <1KB in practice, so this is safe without locks.\n *\n * Stderr logs are written so `tail -f ~/.jarvis[/dev]/hooks/hook.log` (when\n * CC pipes hook stderr there, which it does by default) shows what fired.\n * Stderr never breaks CC — it only watches stdout for decision JSON. */\nexport async function appendDrop<TData>(req: AppendDropRequest<TData>): Promise<void> {\n if (!req.sessionId || !req.kind || !req.uniqId) {\n process.stderr.write(\n `[hook] skip ${req.kind ?? \"?\"} missing field ` +\n `sessionId=${!!req.sessionId} uniqId=${!!req.uniqId}\\n`,\n );\n return;\n }\n const dir = getHooksDir();\n await mkdir(dir, { recursive: true }).catch((err) => {\n process.stderr.write(`[hook] mkdir failed ${dir}: ${err}\\n`);\n });\n const envelope: HookDropEnvelope<TData> = {\n kind: req.kind,\n sessionId: req.sessionId,\n ts: new Date().toISOString(),\n uniqId: req.uniqId,\n data: req.data,\n };\n const file = join(dir, `${req.sessionId}.jsonl`);\n try {\n await appendFile(file, JSON.stringify(envelope) + \"\\n\");\n process.stderr.write(\n `[hook] ${req.kind} appended sessionId=${req.sessionId} uniqId=${req.uniqId}\\n`,\n );\n } catch (err) {\n process.stderr.write(`[hook] append failed ${file}: ${err}\\n`);\n }\n}\n\n/** Read CC's hook payload from stdin. Returns `null` on parse failure\n * (never throws — the hook must never block CC). */\nexport async function readHookStdin<T = Record<string, unknown>>(): Promise<T | null> {\n try {\n const chunks: Buffer[] = [];\n for await (const chunk of process.stdin) {\n chunks.push(typeof chunk === \"string\" ? Buffer.from(chunk) : chunk);\n }\n const raw = Buffer.concat(chunks).toString(\"utf-8\");\n if (!raw) return null;\n return JSON.parse(raw) as T;\n } catch {\n return null;\n }\n}\n\n/** Hard cap any hook so an exotic stdin stall can't pile up forever\n * before CC's terminal prompt fires. We exit 0 with no output → CC\n * proceeds with its normal permission flow. */\nexport function installHookTimeout(ms = 750): void {\n const timer = setTimeout(() => process.exit(0), ms);\n timer.unref();\n}\n","/**\n * Agent Config\n *\n * Manages ~/.jarvis/config.json — saved by `jarvis connect <token>`,\n * read on `jarvis start`.\n */\n\nimport fs from \"fs\";\nimport path from \"path\";\nimport os from \"os\";\nimport { fileURLToPath } from \"url\";\n\n// =============================================================================\n// Types\n// =============================================================================\n\nexport interface AgentConfig {\n /** Cloud WS API URL (from connect token) */\n apiUrl?: string;\n /** JWT auth token (from connect token) */\n token?: string;\n /** Long-lived refresh token for obtaining new access tokens */\n refreshToken?: string;\n /** User ID */\n userId: string;\n /** Environment ID */\n envId?: string;\n /** Workspace root path for repo operations */\n workspacePath?: string;\n /** Web app URL for browser-based auth */\n appUrl?: string;\n /** Anthropic API key (for local-only use without cloud) */\n anthropicApiKey?: string;\n /** Use Anthropic subscription instead of API key */\n useSubscription?: boolean;\n /** When the config was last updated */\n connectedAt?: string;\n}\n\nexport interface ConnectToken {\n apiUrl: string;\n jwt: string;\n refreshToken: string;\n userId: string;\n envId: string;\n}\n\n// =============================================================================\n// Paths\n// =============================================================================\n\n// =============================================================================\n// Environment (single source of truth)\n// =============================================================================\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\n\n/** Single source of truth for dev vs prod.\n * Two locations, picked by which file is shipped:\n * Dev: apps/cli/env.json (committed, `{ \"env\": \"development\" }`)\n * Prod: apps/cli/dist/env.json (written by tsup build, `{ \"env\": \"production\" }`)\n * We try the prod-style sibling first (next to bin.js when bundled), then fall\n * back to the dev-style parent path (one up from src/config.ts). The prod bundle\n * has no `..` parent env.json, and the dev source tree has no sibling env.json,\n * so the two never collide. Safe default if neither exists: prod. */\nexport function isDev(): boolean {\n const candidates = [\n path.join(__dirname, \"env.json\"),\n path.join(__dirname, \"..\", \"env.json\"),\n ];\n for (const envFile of candidates) {\n try {\n const data = JSON.parse(fs.readFileSync(envFile, \"utf-8\"));\n return data.env === \"development\";\n } catch {\n /* try next candidate */\n }\n }\n return false;\n}\n\nconst PROD_APP_URL = \"https://jarvis.appchy.com\";\nconst DEV_APP_URL = \"http://localhost:3000\";\n\nconst PROD_WS_URL = \"wss://jarvis.appchy.com/ws\";\nconst DEV_WS_URL = \"ws://localhost:7863\";\n\n/** App URL for the current environment. */\nexport function getDefaultAppUrl(): string {\n return isDev() ? (process.env.APP_URL || DEV_APP_URL) : PROD_APP_URL;\n}\n\n/** WS hub URL for the current environment. Hardcoded per build so the prod\n * binary can never subscribe to the dev hub (or vice versa) regardless of\n * what's stored in `config.apiUrl`. Dev permits a `JARVIS_UPSTREAM_URL`\n * override for tests/CI on a non-default port; prod has no escape hatch. */\nexport function getDefaultWsUrl(): string {\n return isDev() ? (process.env.JARVIS_UPSTREAM_URL || DEV_WS_URL) : PROD_WS_URL;\n}\n\n// =============================================================================\n// Config directory\n// =============================================================================\n\n/** Determine config directory.\n * - JARVIS_CONFIG_DIR env var takes priority (escape hatch for CI/testing)\n * - dev → ~/.jarvis/dev/\n * - prod → ~/.jarvis/\n *\n * Called fresh on every read so test environments that set\n * `JARVIS_CONFIG_DIR` AFTER module load still see the override — caching\n * the value at module init would lock tests onto the host's real\n * `~/.jarvis/dev/` and leak production state into the in-memory infra. */\nfunction resolveConfigDir(): string {\n if (process.env.JARVIS_CONFIG_DIR) return process.env.JARVIS_CONFIG_DIR;\n const base = path.join(os.homedir(), \".jarvis\");\n return isDev() ? path.join(base, \"dev\") : base;\n}\n\nfunction configDir(): string {\n return resolveConfigDir();\n}\n\nfunction configFile(): string {\n return path.join(configDir(), \"config.json\");\n}\n\n/** Absolute path to the per-daemon config directory.\n * `prod` → `~/.jarvis/`, `dev` → `~/.jarvis/dev/`. */\nexport function getConfigDir(): string {\n return configDir();\n}\n\n/** Per-daemon directory for PreToolUse hook drop files. The CC hook script\n * writes `<sessionId>/<toolUseId>.json` here; the sessions watcher consumes\n * them. Prod and dev daemons each own their own subtree so a hook installed\n * for one never feeds the other. */\nexport function getHooksDir(): string {\n return path.join(configDir(), \"hooks\");\n}\n\n// =============================================================================\n// Operations\n// =============================================================================\n\nexport function loadConfig(): AgentConfig | null {\n try {\n const raw = fs.readFileSync(configFile(), \"utf-8\");\n return JSON.parse(raw) as AgentConfig;\n } catch {\n return null;\n }\n}\n\nexport function saveConfig(config: AgentConfig): void {\n fs.mkdirSync(configDir(), { recursive: true });\n fs.writeFileSync(configFile(), JSON.stringify(config, null, 2) + \"\\n\");\n}\n\nexport function clearConfig(): void {\n try {\n fs.unlinkSync(configFile());\n } catch {}\n}\n\nexport function parseConnectToken(token: string): ConnectToken {\n try {\n const decoded = Buffer.from(token, \"base64\").toString(\"utf-8\");\n const parsed = JSON.parse(decoded);\n if (!parsed.apiUrl || !parsed.jwt || !parsed.userId) {\n throw new Error(\"Invalid token: missing required fields (apiUrl, jwt, userId)\");\n }\n return parsed as ConnectToken;\n } catch (err) {\n if (err instanceof SyntaxError) {\n throw new Error(\"Invalid token: not valid base64-encoded JSON\");\n }\n throw err;\n }\n}\n\nexport function getConfigPath(): string {\n return configFile();\n}\n","/**\n * UserPromptSubmit hook — drops an envelope each time a terminal `claude`\n * user submits a prompt. The daemon SDK runs with `settingSources: []`\n * so its own `query()` calls never trigger this hook — every drop\n * observed here is unambiguously a terminal-typed turn.\n *\n * Used to flip `SessionCacheEntry.controller` to `\"claudeCode\"` on the\n * watcher side, surfacing the controller change in the web UI's pill.\n */\n\nimport { appendDrop, installHookTimeout, readHookStdin } from \"./drop\";\n\ninterface CcUserPromptSubmitInput {\n session_id?: string;\n transcript_path?: string;\n cwd?: string;\n prompt?: string;\n}\n\ninterface UserPromptSubmitDropData {\n transcriptPath?: string;\n cwd?: string;\n}\n\ninstallHookTimeout();\n\nasync function main(): Promise<void> {\n const p = await readHookStdin<CcUserPromptSubmitInput>();\n if (!p) return;\n const sessionId = p.session_id;\n if (!sessionId) return;\n\n const ts = new Date().toISOString();\n const data: UserPromptSubmitDropData = {\n ...(p.transcript_path ? { transcriptPath: p.transcript_path } : {}),\n ...(p.cwd ? { cwd: p.cwd } : {}),\n };\n\n await appendDrop<UserPromptSubmitDropData>({\n kind: \"user-prompt-submit\",\n sessionId,\n uniqId: `user-prompt-submit-${ts}`,\n data,\n });\n}\n\nvoid main().finally(() => process.exit(0));\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/hooks/drop.ts","../../src/config.ts","../../src/hooks/user-prompt-submit.ts"],"sourcesContent":["/**\n * Shared helpers for CC hook scripts.\n *\n * Each hook script (pre-tool-use, session-start, stop, …) is a tiny\n * stdin → append-line → exit binary. They all write to the same per-\n * session JSONL at `<hooksDir>/<sessionId>.jsonl`. This module\n * concentrates the envelope shape and the atomic append so the per-\n * hook scripts stay <30 lines each.\n *\n * Hot-path constraints: NEVER block, NEVER network, NEVER read user\n * input. Pure stdin parse + filesystem append + exit. Worst-case one\n * `mkdir` + one `appendFile` per fire.\n */\n\nimport { appendFile, mkdir } from \"node:fs/promises\";\nimport { join } from \"node:path\";\n\nimport { getHooksDir } from \"../config\";\n\n/** Wire-format envelope written one-per-line to the hook JSONL.\n * The watcher in `sessions.ts` parses this exact shape — keep in sync. */\nexport interface HookDropEnvelope<TData = unknown> {\n type: string;\n sessionId: string;\n ts: string;\n /** Idempotency key. Lets the watcher's replay-on-restart skip drops\n * whose effects were already emitted (see `HookCursor.applied`). */\n uniqId: string;\n data: TData;\n}\n\nexport interface AppendDropRequest<TData> {\n type: string;\n sessionId: string;\n uniqId: string;\n data: TData;\n}\n\n/** Append one envelope line to the per-session hook JSONL. Atomic between\n * hook processes via POSIX `O_APPEND` (Node's `fs.appendFile`) — concurrent\n * appends from different CC sessions / hook types can't interleave bytes\n * within a line as long as the line stays < `PIPE_BUF` (4KB). Drops are\n * <1KB in practice, so this is safe without locks.\n *\n * Stderr logs are written so `tail -f ~/.jarvis/hooks/hook.log` (when\n * CC pipes hook stderr there, which it does by default) shows what fired.\n * Stderr never breaks CC — it only watches stdout for decision JSON. */\nexport async function appendDrop<TData>(req: AppendDropRequest<TData>): Promise<void> {\n if (!req.sessionId || !req.type || !req.uniqId) {\n process.stderr.write(\n `[hook] skip ${req.type ?? \"?\"} missing field ` +\n `sessionId=${!!req.sessionId} uniqId=${!!req.uniqId}\\n`,\n );\n return;\n }\n const dir = getHooksDir();\n await mkdir(dir, { recursive: true }).catch((err) => {\n process.stderr.write(`[hook] mkdir failed ${dir}: ${err}\\n`);\n });\n const envelope: HookDropEnvelope<TData> = {\n type: req.type,\n sessionId: req.sessionId,\n ts: new Date().toISOString(),\n uniqId: req.uniqId,\n data: req.data,\n };\n const file = join(dir, `${req.sessionId}.jsonl`);\n try {\n await appendFile(file, JSON.stringify(envelope) + \"\\n\");\n process.stderr.write(\n `[hook] ${req.type} appended sessionId=${req.sessionId} uniqId=${req.uniqId}\\n`,\n );\n } catch (err) {\n process.stderr.write(`[hook] append failed ${file}: ${err}\\n`);\n }\n}\n\n/** Read CC's hook payload from stdin. Returns `null` on parse failure\n * (never throws — the hook must never block CC). */\nexport async function readHookStdin<T = Record<string, unknown>>(): Promise<T | null> {\n try {\n const chunks: Buffer[] = [];\n for await (const chunk of process.stdin) {\n chunks.push(typeof chunk === \"string\" ? Buffer.from(chunk) : chunk);\n }\n const raw = Buffer.concat(chunks).toString(\"utf-8\");\n if (!raw) return null;\n return JSON.parse(raw) as T;\n } catch {\n return null;\n }\n}\n\n/** Hard cap any hook so an exotic stdin stall can't pile up forever\n * before CC's terminal prompt fires. We exit 0 with no output → CC\n * proceeds with its normal permission flow. */\nexport function installHookTimeout(ms = 750): void {\n const timer = setTimeout(() => process.exit(0), ms);\n timer.unref();\n}\n","/**\n * Agent Config\n *\n * Manages ~/.jarvis/config.json — saved by `jarvis connect <token>`,\n * read on `jarvis start`.\n */\n\nimport fs from \"fs\";\nimport path from \"path\";\nimport os from \"os\";\n\n// =============================================================================\n// Types\n// =============================================================================\n\nexport interface AgentConfig {\n /** The jarvis this machine joined — the address a person opens in a browser,\n * and what pairing talks to. Ours by default; `jarvis init --url <url>` joins\n * someone else's. */\n url?: string;\n /** WS hub URL this machine dials. Written once, by pairing, from the hub the\n * joined instance named — and never from a flag, an environment variable or\n * any later command. Re-pointing a machine means unpairing it first. */\n hubUrl?: string;\n /** JWT auth token (from connect token) */\n token?: string;\n /** Long-lived refresh token for obtaining new access tokens */\n refreshToken?: string;\n /** User ID */\n userId: string;\n /** Environment ID */\n envId?: string;\n /** Workspace root path for repo operations */\n workspacePath?: string;\n /** Anthropic API key (for local-only use without cloud) */\n anthropicApiKey?: string;\n /** Use Anthropic subscription instead of API key */\n useSubscription?: boolean;\n /** When the config was last updated */\n connectedAt?: string;\n}\n\nexport interface ConnectToken {\n hubUrl: string;\n jwt: string;\n refreshToken: string;\n userId: string;\n envId: string;\n}\n\n// =============================================================================\n// Environment\n// =============================================================================\n\n/** The jarvis a machine pairs with when nothing else is named. Only ever read\n * by pairing: `jarvis init --url <url>` joins someone else's instead, and the\n * hub URL is whatever that instance hands back. */\nexport const DEFAULT_URL = \"https://jarvis.appchy.com\";\n\n// =============================================================================\n// Config directory\n// =============================================================================\n\n/** The config dir is the machine's identity: what it paired as, and therefore\n * which hub it dials. One daemon per dir, so a second identity on the same\n * machine — a storage rig, a localhost instance — is a second dir, named by\n * `JARVIS_CONFIG_DIR`, never a second binary.\n *\n * Called fresh on every read so a test that sets `JARVIS_CONFIG_DIR` AFTER\n * module load still sees the override — caching at module init would lock it\n * onto the host's real `~/.jarvis/` and leak production state into the\n * in-memory infra. */\nfunction configDir(): string {\n if (process.env.JARVIS_CONFIG_DIR) return process.env.JARVIS_CONFIG_DIR;\n return path.join(os.homedir(), \".jarvis\");\n}\n\nfunction configFile(): string {\n return path.join(configDir(), \"config.json\");\n}\n\n/** Absolute path to the per-daemon config directory — `~/.jarvis/` unless\n * `JARVIS_CONFIG_DIR` names another. */\nexport function getConfigDir(): string {\n return configDir();\n}\n\n/** Per-daemon directory for PreToolUse hook drop files. The CC hook script\n * writes `<sessionId>/<toolUseId>.json` here; the sessions watcher consumes\n * them. Each daemon owns the subtree under its own config dir, so a hook\n * installed for one never feeds another. */\nexport function getHooksDir(): string {\n return path.join(configDir(), \"hooks\");\n}\n\n// =============================================================================\n// Operations\n// =============================================================================\n\n/** Read the machine's config, accepting the names these two fields used to\n * carry. `appUrl`/`apiUrl` were renamed on 2026-08-15 — `apiUrl` never held an\n * API URL, it held the hub's. A config written before that still loads, and\n * `saveConfig` only ever writes the current names, so a machine heals itself on\n * its next write with nothing to run. Delete this tolerance once no config\n * predating the rename is in use. */\nexport function loadConfig(): AgentConfig | null {\n try {\n const stored = JSON.parse(fs.readFileSync(configFile(), \"utf-8\")) as AgentConfig &\n Partial<{ appUrl: string; apiUrl: string }>;\n const { appUrl, apiUrl, ...config } = stored;\n const url = config.url ?? appUrl;\n const hubUrl = config.hubUrl ?? apiUrl;\n return {\n ...config,\n ...(url !== undefined ? { url } : {}),\n ...(hubUrl !== undefined ? { hubUrl } : {}),\n };\n } catch {\n return null;\n }\n}\n\n/** Persist the machine's config.\n *\n * Refuses to move an already-paired machine to a different hub. A machine that\n * joins the wrong control plane hands it credentials, so re-pointing one is a\n * deliberate act — `jarvis unpair` (which clears the config) and then pair\n * again — rather than something a stale token or a mistyped command can do on\n * its way past. Re-pairing to the same hub is unaffected. */\nexport function saveConfig(config: AgentConfig): void {\n const paired = loadConfig();\n if (paired?.hubUrl && config.hubUrl && config.hubUrl !== paired.hubUrl) {\n throw new Error(\n `This machine is paired with ${paired.hubUrl} and cannot be moved to ${config.hubUrl}.\\n` +\n ` Run 'jarvis unpair' first if you mean to join a different jarvis.`,\n );\n }\n fs.mkdirSync(configDir(), { recursive: true });\n fs.writeFileSync(configFile(), JSON.stringify(config, null, 2) + \"\\n\");\n}\n\nexport function clearConfig(): void {\n try {\n fs.unlinkSync(configFile());\n } catch {}\n}\n\nexport function parseConnectToken(token: string): ConnectToken {\n try {\n const decoded = Buffer.from(token, \"base64\").toString(\"utf-8\");\n const parsed = JSON.parse(decoded);\n if (!parsed.hubUrl || !parsed.jwt || !parsed.userId) {\n throw new Error(\"Invalid token: missing required fields (hubUrl, jwt, userId)\");\n }\n return parsed as ConnectToken;\n } catch (err) {\n if (err instanceof SyntaxError) {\n throw new Error(\"Invalid token: not valid base64-encoded JSON\");\n }\n throw err;\n }\n}\n\nexport function getConfigPath(): string {\n return configFile();\n}\n","/**\n * UserPromptSubmit hook — drops an envelope each time a `claude` user submits\n * a prompt. Jarvis starts no sessions, so every drop observed here is a\n * person's turn.\n *\n * Used to flip `SessionCacheEntry.controller` to `\"claudeCode\"` on the\n * watcher side, surfacing the controller change in the web UI's pill.\n */\n\nimport { appendDrop, installHookTimeout, readHookStdin } from \"./drop\";\n\ninterface CcUserPromptSubmitInput {\n session_id?: string;\n transcript_path?: string;\n cwd?: string;\n prompt?: string;\n}\n\ninterface UserPromptSubmitDropData {\n transcriptPath?: string;\n cwd?: string;\n}\n\ninstallHookTimeout();\n\nasync function main(): Promise<void> {\n const p = await readHookStdin<CcUserPromptSubmitInput>();\n if (!p) return;\n const sessionId = p.session_id;\n if (!sessionId) return;\n\n const ts = new Date().toISOString();\n const data: UserPromptSubmitDropData = {\n ...(p.transcript_path ? { transcriptPath: p.transcript_path } : {}),\n ...(p.cwd ? { cwd: p.cwd } : {}),\n };\n\n await appendDrop<UserPromptSubmitDropData>({\n type: \"user-prompt-submit\",\n sessionId,\n uniqId: `user-prompt-submit-${ts}`,\n data,\n });\n}\n\nvoid main().finally(() => process.exit(0));\n"],"mappings":";;;AAcA,SAAS,YAAY,aAAa;AAClC,SAAS,YAAY;;;ACPrB,OAAO,UAAU;AACjB,OAAO,QAAQ;AA+Df,SAAS,YAAoB;AAC3B,MAAI,QAAQ,IAAI,kBAAmB,QAAO,QAAQ,IAAI;AACtD,SAAO,KAAK,KAAK,GAAG,QAAQ,GAAG,SAAS;AAC1C;AAgBO,SAAS,cAAsB;AACpC,SAAO,KAAK,KAAK,UAAU,GAAG,OAAO;AACvC;;;AD9CA,eAAsB,WAAkB,KAA8C;AACpF,MAAI,CAAC,IAAI,aAAa,CAAC,IAAI,QAAQ,CAAC,IAAI,QAAQ;AAC9C,YAAQ,OAAO;AAAA,MACb,eAAe,IAAI,QAAQ,GAAG,4BACf,CAAC,CAAC,IAAI,SAAS,WAAW,CAAC,CAAC,IAAI,MAAM;AAAA;AAAA,IACvD;AACA;AAAA,EACF;AACA,QAAM,MAAM,YAAY;AACxB,QAAM,MAAM,KAAK,EAAE,WAAW,KAAK,CAAC,EAAE,MAAM,CAAC,QAAQ;AACnD,YAAQ,OAAO,MAAM,uBAAuB,GAAG,KAAK,GAAG;AAAA,CAAI;AAAA,EAC7D,CAAC;AACD,QAAM,WAAoC;AAAA,IACxC,MAAM,IAAI;AAAA,IACV,WAAW,IAAI;AAAA,IACf,KAAI,oBAAI,KAAK,GAAE,YAAY;AAAA,IAC3B,QAAQ,IAAI;AAAA,IACZ,MAAM,IAAI;AAAA,EACZ;AACA,QAAM,OAAO,KAAK,KAAK,GAAG,IAAI,SAAS,QAAQ;AAC/C,MAAI;AACF,UAAM,WAAW,MAAM,KAAK,UAAU,QAAQ,IAAI,IAAI;AACtD,YAAQ,OAAO;AAAA,MACb,UAAU,IAAI,IAAI,uBAAuB,IAAI,SAAS,WAAW,IAAI,MAAM;AAAA;AAAA,IAC7E;AAAA,EACF,SAAS,KAAK;AACZ,YAAQ,OAAO,MAAM,wBAAwB,IAAI,KAAK,GAAG;AAAA,CAAI;AAAA,EAC/D;AACF;AAIA,eAAsB,gBAAgE;AACpF,MAAI;AACF,UAAM,SAAmB,CAAC;AAC1B,qBAAiB,SAAS,QAAQ,OAAO;AACvC,aAAO,KAAK,OAAO,UAAU,WAAW,OAAO,KAAK,KAAK,IAAI,KAAK;AAAA,IACpE;AACA,UAAM,MAAM,OAAO,OAAO,MAAM,EAAE,SAAS,OAAO;AAClD,QAAI,CAAC,IAAK,QAAO;AACjB,WAAO,KAAK,MAAM,GAAG;AAAA,EACvB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAKO,SAAS,mBAAmB,KAAK,KAAW;AACjD,QAAM,QAAQ,WAAW,MAAM,QAAQ,KAAK,CAAC,GAAG,EAAE;AAClD,QAAM,MAAM;AACd;;;AE5EA,mBAAmB;AAEnB,eAAe,OAAsB;AACnC,QAAM,IAAI,MAAM,cAAuC;AACvD,MAAI,CAAC,EAAG;AACR,QAAM,YAAY,EAAE;AACpB,MAAI,CAAC,UAAW;AAEhB,QAAM,MAAK,oBAAI,KAAK,GAAE,YAAY;AAClC,QAAM,OAAiC;AAAA,IACrC,GAAI,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,gBAAgB,IAAI,CAAC;AAAA,IACjE,GAAI,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,IAAI,CAAC;AAAA,EAChC;AAEA,QAAM,WAAqC;AAAA,IACzC,MAAM;AAAA,IACN;AAAA,IACA,QAAQ,sBAAsB,EAAE;AAAA,IAChC;AAAA,EACF,CAAC;AACH;AAEA,KAAK,KAAK,EAAE,QAAQ,MAAM,QAAQ,KAAK,CAAC,CAAC;","names":[]}
@@ -0,0 +1,53 @@
1
+ # {{TITLE}} — technical architecture + phased plan
2
+
3
+ <!-- NOT a claude.ai/design UI brief (see design.md for that). This is the
4
+ version-level TECHNICAL brief — the cross-task "plan FROM this" doc: the
5
+ shared seams, the migration/build slices, the rules that must not
6
+ regress. Owner of record for the cross-task design; each task's own
7
+ `plan.md` still owns its own As-found + Architecture at pickup. -->
8
+
9
+ > Direction → <!-- rule id(s) -->
10
+ <!-- Cite the governing rule ids (+ any narrowing/superseding ones) and, if this
11
+ version has one, the research report it draws from. -->
12
+
13
+ ## As-found
14
+
15
+ <!-- Raw, factual account of how the in-scope code works today — file:line,
16
+ confirmed by the blast-radius sweep. No opinions. What exists, what's
17
+ missing, where the
18
+ seams already are. -->
19
+
20
+ ## Target architecture
21
+
22
+ <!-- Lead with a diagram, then the rationale (why this shape, alternatives
23
+ weighed). The shared seams/contracts every task in this version builds
24
+ against. -->
25
+
26
+ ## Code map
27
+
28
+ <!-- Per `code` region this version touches — a MAP of seams/decisions/links,
29
+ NEVER a code dump. Where Pass-1 topology + the architect's foundation
30
+ reasoning land (SKILL.md §Two-pass decomposition). One block per region;
31
+ omit a region until the cut actually touches it. -->
32
+
33
+ ### <code-region>
34
+
35
+ - **Seam** — <the shared contract/boundary here; link `path:line`, don't paste>
36
+ - **Known-next constraints** — <what later same-region tasks will need this seam
37
+ to support, so it isn't built too narrow>
38
+ - **Consumers** — <the features/tasks that build on this seam (≥2 for a real
39
+ foundation)>
40
+ - **Foundation task** — <the task that lands this seam first, if any; else "—">
41
+
42
+ ## Phases
43
+
44
+ <!-- Each phase independently shippable (own acceptance, own commit) —
45
+ never a partial, unshippable slice. One subsection per phase:
46
+ name, what it delivers, what it depends on. -->
47
+
48
+ 1. **<phase-name>** — <what ships, dependencies>
49
+
50
+ ## Risks
51
+
52
+ <!-- Open items / risks to resolve per-phase, measure-first. What could
53
+ invalidate the target architecture; what to watch for. -->
@@ -0,0 +1,89 @@
1
+ # {{TITLE}} — claude.ai/design brief
2
+
3
+ <!-- Paste this into https://claude.ai/design and link this repo. Claude Design
4
+ already has the full design context — the prototype in /wireframes/, the
5
+ codebase's components, architecture and styling.
6
+
7
+ Claude Design owns the "how" (layout, controls, copy, states). This brief
8
+ is the "what" — the version goal, the user stories, the audience. Don't
9
+ pre-spec screens or content shapes; let Claude Design propose them. -->
10
+
11
+ ## Goal
12
+
13
+ <!-- One paragraph. The capability this version delivers in USER terms — what
14
+ can the user do after this ships that they cannot do today? Why does it
15
+ matter? No technical detail, no implementation hints. -->
16
+
17
+ ## User stories
18
+
19
+ <!-- Lift from the feature stories of the features in this cut
20
+ (work/product/**.md §Stories — the durable owner; version stories.md
21
+ is retired). Each story in:
22
+ "As a <role>, I want <capability>, so that <outcome>."
23
+ followed by a short list of observable / verifiable behaviours it implies
24
+ (behaviours, not screens) — reference the feature's B-/AC- ids. -->
25
+
26
+ ## Audience
27
+
28
+ <!-- Who the user is + what they already know — Claude Design needs this to
29
+ pitch affordances at the right level. One short paragraph. -->
30
+
31
+ ## Workflow
32
+
33
+ Work in **two passes**, then clean up for export. Don't skip ahead — Pass 2
34
+ implements what Pass 1 defines.
35
+
36
+ ### Pass 1 — Wireframes + behaviour
37
+
38
+ Sketch the screens at low fidelity (layout, structure, key controls) and
39
+ **define every behaviour alongside**. As you go, write the per-page spec to
40
+ `ui.md` in this exact format:
41
+
42
+ ```
43
+ # ui.md — {{TITLE}}
44
+
45
+ ## Page: <name>
46
+
47
+ ### Layout
48
+ <ASCII sketch: before> <ASCII sketch: after>
49
+
50
+ ### Changed elements
51
+ #### <element>
52
+ - Does — <behaviour: what happens on interaction, what it reflects>
53
+ - Looks — <visual states + variants, where it sits>
54
+ - Relates to — <other elements/states it depends on or drives>
55
+ - Blast radius — <sibling components, broker/protocol, persisted state>
56
+
57
+ ## Backend changes deduced
58
+ - <what the client behaviour implies the broker / protocol / event model
59
+ must provide — the bridge into the implementation phase>
60
+ ```
61
+
62
+ One section per page that changes; scope tightly (only changed elements
63
+ appear, never untouched ones). Skip the "before" sketch for a brand-new page;
64
+ skip the whole Layout block when the frame is unchanged and only inner
65
+ elements moved. The Backend changes section at the end is mandatory — it is
66
+ how the implementation phase finds its inputs.
67
+
68
+ Iterate Pass 1 until every page's behaviour is settled in `ui.md`. Don't
69
+ start the visual polish before this point.
70
+
71
+ ### Pass 2 — Live mock
72
+
73
+ Implement what Pass 1 defined. Build the **polished HTML/CSS/JS prototype**,
74
+ realising the behaviours from `ui.md` with the final visual style. The mock
75
+ is the behavioural + visual source of truth.
76
+
77
+ ### Cleanup for export
78
+
79
+ When the prototype is final:
80
+
81
+ - **Delete old screenshots** captured during iteration.
82
+ - **Re-take fresh screenshots** of every page in the final mock.
83
+ - Re-check `ui.md` against the prototype (behaviour may have refined during
84
+ the polish pass) and update it if it drifted.
85
+ - The project is **export-ready** — no dead files, no half-built states.
86
+
87
+ The exported bundle (`project/` + `ui.md` + screenshots) drops into
88
+ `/wireframes/` in the repo. The work skill takes it from there to decompose
89
+ into tasks.
@@ -0,0 +1,43 @@
1
+ ---
2
+ type: domain
3
+ id: architecture
4
+ ---
5
+
6
+ # Architecture — how it is built
7
+
8
+ ## What belongs here
9
+
10
+ Durable calls about **how the software is built** — the systems, their seams, the
11
+ dependency rules, and the constraints a plan must not regress.
12
+
13
+ Rules here live in `architecture/<system>.md`, not in this README: a system is a
14
+ capability boundary that owns `code:` regions and hosts the rules governing them.
15
+ This README is the domain's landing doc and carries the derived system index and
16
+ dependency map. A region belongs to exactly ONE system.
17
+
18
+ Its neighbours: [`product/`](../product/README.md) owns what the software must do —
19
+ this domain owns the shape that delivers it.
20
+ [`operations/`](../operations/README.md) owns getting it out and keeping it running;
21
+ a build-time rule is this domain's, a deploy-time one is theirs.
22
+ [`security/`](../security/README.md) owns what must stay safe while it runs.
23
+
24
+ ## Non-goals
25
+
26
+ - **What the software does.** `product/`.
27
+ - **How it ships and runs.** `operations/`.
28
+ - **A rule about a kind of call rather than a system.** It belongs to the domain that owns that kind — filing it here because it was the only place that fit is the mis-filing the nine domains exist to end.
29
+ - **Package inventory.** Derivable from the file tree; a system is not one-per-package.
30
+
31
+ ## Rules
32
+
33
+ <!-- A durable rule lives in the domain that owns its KIND of call. Shape:
34
+
35
+ ### <id> — the rule, in one line
36
+ enforced_by: the check that catches a violation — a command, a test, a lint
37
+
38
+ `enforced_by: judgement` is legal where a person really must decide, and is
39
+ counted separately rather than treated as enforcement. An empty one is a wish.
40
+
41
+ This section is EMPTY ON PURPOSE in a fresh repo. Do not seed it with invented
42
+ rules to make the domain look occupied — an empty domain is a signpost saying
43
+ where a call will go when you make one. -->
@@ -0,0 +1,42 @@
1
+ ---
2
+ type: domain
3
+ id: commercial
4
+ ---
5
+
6
+ # Commercial — how we reach people and earn
7
+
8
+ ## What belongs here
9
+
10
+ Durable calls about **reach and revenue** — pricing shape, packaging and tiers,
11
+ positioning, distribution and partners, and how growth is measured.
12
+
13
+ Expect many rules here to be `enforced_by: judgement`, and that is honest rather
14
+ than lazy: a positioning call is not something a check can catch. They are still
15
+ written down and still counted, because the alternative is that they live only in
16
+ somebody's head and get silently re-argued.
17
+
18
+ Its neighbours: [`product/`](../product/README.md) owns what the thing does — this
19
+ domain owns who it is for and what it costs. [`legal/`](../legal/README.md) owns
20
+ what a commitment binds us to. [`design/`](../design/README.md) owns the surface;
21
+ brand *reach* is this domain's, brand *rendering* is theirs.
22
+
23
+ ## Non-goals
24
+
25
+ - **What the product does.** `product/`.
26
+ - **What a contract obliges.** `legal/`.
27
+ - **How it is delivered technically.** `operations/`.
28
+ - **A campaign.** That is work; the pricing rule behind it is durable.
29
+
30
+ ## Rules
31
+
32
+ <!-- A durable rule lives in the domain that owns its KIND of call. Shape:
33
+
34
+ ### <id> — the rule, in one line
35
+ enforced_by: the check that catches a violation — a command, a test, a lint
36
+
37
+ `enforced_by: judgement` is legal where a person really must decide, and is
38
+ counted separately rather than treated as enforcement. An empty one is a wish.
39
+
40
+ This section is EMPTY ON PURPOSE in a fresh repo. Do not seed it with invented
41
+ rules to make the domain look occupied — an empty domain is a signpost saying
42
+ where a call will go when you make one. -->
@@ -0,0 +1,42 @@
1
+ ---
2
+ type: domain
3
+ id: design
4
+ ---
5
+
6
+ # Design — how it looks and reads
7
+
8
+ ## What belongs here
9
+
10
+ Durable calls about the **visual and interaction surface** — layout, type, colour,
11
+ motion, component behaviour, tone of the words on screen, and accessibility.
12
+
13
+ A note on the word, because it costs sessions: **"design" unqualified means this
14
+ domain**. A technical plan is "the plan", never "the design", however architectural
15
+ it is. A repo that lets one word mean both ends up with two documents claiming to
16
+ be the design of the same change.
17
+
18
+ Its neighbours: [`product/`](../product/README.md) owns what a user must be able to
19
+ do — this domain owns what that looks and feels like.
20
+ [`support/`](../support/README.md) owns getting a stuck user moving; where a design
21
+ choice is the thing that stuck them, the fix is this domain's.
22
+
23
+ ## Non-goals
24
+
25
+ - **What the feature must do.** `product/`.
26
+ - **The technical plan.** `architecture/`, and it is called the plan.
27
+ - **Brand reach and positioning.** `commercial/`.
28
+ - **One screen's spec.** That is a feature's `<feature>.md`, or a task's UI brief.
29
+
30
+ ## Rules
31
+
32
+ <!-- A durable rule lives in the domain that owns its KIND of call. Shape:
33
+
34
+ ### <id> — the rule, in one line
35
+ enforced_by: the check that catches a violation — a command, a test, a lint
36
+
37
+ `enforced_by: judgement` is legal where a person really must decide, and is
38
+ counted separately rather than treated as enforcement. An empty one is a wish.
39
+
40
+ This section is EMPTY ON PURPOSE in a fresh repo. Do not seed it with invented
41
+ rules to make the domain look occupied — an empty domain is a signpost saying
42
+ where a call will go when you make one. -->
@@ -0,0 +1,43 @@
1
+ ---
2
+ type: domain
3
+ id: legal
4
+ ---
5
+
6
+ # Legal — what binds us
7
+
8
+ ## What belongs here
9
+
10
+ Durable calls about **obligation** — licensing of what we ship and what we depend
11
+ on, attribution, terms and privacy commitments, regulatory constraints, and what a
12
+ published artifact must and must not claim.
13
+
14
+ Its neighbours: [`security/`](../security/README.md) owns technical safety — a
15
+ credential rule is theirs, a data-retention *commitment* is this domain's.
16
+ [`commercial/`](../commercial/README.md) owns the deal; this domain owns what
17
+ agreeing to it binds us to.
18
+
19
+ **Often empty in a young repo, and it ships anyway** — see the note in
20
+ [`security/`](../security/README.md). The first rule that lands here is usually a
21
+ dependency licence that turned out to constrain distribution, and it is much better
22
+ found by a charter that already exists than by a lawyer.
23
+
24
+ ## Non-goals
25
+
26
+ - **Technical safety measures.** `security/`.
27
+ - **Pricing and packaging.** `commercial/`.
28
+ - **How we run it.** `operations/`.
29
+ - **One licence review.** That is work; the rule it establishes is durable.
30
+
31
+ ## Rules
32
+
33
+ <!-- A durable rule lives in the domain that owns its KIND of call. Shape:
34
+
35
+ ### <id> — the rule, in one line
36
+ enforced_by: the check that catches a violation — a command, a test, a lint
37
+
38
+ `enforced_by: judgement` is legal where a person really must decide, and is
39
+ counted separately rather than treated as enforcement. An empty one is a wish.
40
+
41
+ This section is EMPTY ON PURPOSE in a fresh repo. Do not seed it with invented
42
+ rules to make the domain look occupied — an empty domain is a signpost saying
43
+ where a call will go when you make one. -->
@@ -0,0 +1,39 @@
1
+ ---
2
+ type: domain
3
+ id: operations
4
+ ---
5
+
6
+ # Operations — how we ship it and run it
7
+
8
+ ## What belongs here
9
+
10
+ Durable calls about **getting it out and keeping it running** — how something is
11
+ published, how a consumer installs and updates, what a version means, how the
12
+ release sequence works, and how the repo's own tooling runs.
13
+
14
+ Its neighbours: [`security/`](../security/README.md) owns what must stay safe while
15
+ we do it — a scan of a published artifact is a security rule, the publish path
16
+ itself is this domain's. [`support/`](../support/README.md) owns keeping the *user*
17
+ unblocked; this domain owns keeping the *system* shipping.
18
+ [`architecture/`](../architecture/README.md) owns build-time structure.
19
+
20
+ ## Non-goals
21
+
22
+ - **What the software does.** `product/`.
23
+ - **How it is structured internally.** `architecture/`.
24
+ - **Pricing and reach.** `commercial/`.
25
+ - **One incident.** A postmortem is a document; the rule it produces belongs here.
26
+
27
+ ## Rules
28
+
29
+ <!-- A durable rule lives in the domain that owns its KIND of call. Shape:
30
+
31
+ ### <id> — the rule, in one line
32
+ enforced_by: the check that catches a violation — a command, a test, a lint
33
+
34
+ `enforced_by: judgement` is legal where a person really must decide, and is
35
+ counted separately rather than treated as enforcement. An empty one is a wish.
36
+
37
+ This section is EMPTY ON PURPOSE in a fresh repo. Do not seed it with invented
38
+ rules to make the domain look occupied — an empty domain is a signpost saying
39
+ where a call will go when you make one. -->
@@ -0,0 +1,43 @@
1
+ ---
2
+ type: domain
3
+ id: product
4
+ ---
5
+
6
+ # Product — what it does
7
+
8
+ ## What belongs here
9
+
10
+ Durable calls about **what the thing does** — the behaviour a user can rely on, and
11
+ what the product *is*.
12
+
13
+ This domain is the one that holds two kinds of file. The `<feature>.md` behaviour
14
+ specs sit beside this charter and own stories, behaviour and `AC-nn`. This README
15
+ also hosts `## Rules` like any other domain, because those are different facts: a
16
+ feature spec says what the app does *today* and can be retired, while a product rule
17
+ says what the product *is* and outlives any feature that expresses it.
18
+
19
+ Its neighbours: [`design/`](../design/README.md) owns how it looks and reads —
20
+ this domain owns what it must do. [`architecture/`](../architecture/README.md) owns
21
+ how it is built. [`quality/`](../quality/README.md) owns how we know it works; a
22
+ criterion's *text* is a feature's, the *standard of proof* is quality's.
23
+
24
+ ## Non-goals
25
+
26
+ - **How it is built.** `architecture/`.
27
+ - **How it looks.** `design/`.
28
+ - **Whether it works.** `quality/` owns the standard of evidence; the `AC-nn` text stays in `<feature>.md`.
29
+ - **Task-level scope.** A task brief is not durable direction.
30
+
31
+ ## Rules
32
+
33
+ <!-- A durable rule lives in the domain that owns its KIND of call. Shape:
34
+
35
+ ### <id> — the rule, in one line
36
+ enforced_by: the check that catches a violation — a command, a test, a lint
37
+
38
+ `enforced_by: judgement` is legal where a person really must decide, and is
39
+ counted separately rather than treated as enforcement. An empty one is a wish.
40
+
41
+ This section is EMPTY ON PURPOSE in a fresh repo. Do not seed it with invented
42
+ rules to make the domain look occupied — an empty domain is a signpost saying
43
+ where a call will go when you make one. -->
@@ -0,0 +1,43 @@
1
+ ---
2
+ type: domain
3
+ id: quality
4
+ ---
5
+
6
+ # Quality — how we know it is good
7
+
8
+ ## What belongs here
9
+
10
+ Durable calls about **evidence** — what counts as proof that something works, how a
11
+ test binds to the criterion it proves, what the levels of proof are, and what makes
12
+ a green run trustworthy.
13
+
14
+ The distinction that earns this domain its own home: *what the app should do* is
15
+ product's, *whether it does* is this domain's. A rule that says "a criterion is
16
+ proven by a tag-bound passing test, never by a comment" is not a build rule and not
17
+ a behaviour spec — and before the domains split, it had nowhere to live.
18
+
19
+ Its neighbours: [`architecture/`](../architecture/README.md) owns how code is
20
+ built; this domain owns how we know the result is right.
21
+ [`product/`](../product/README.md) owns the `AC-nn` text this domain sets the
22
+ standard of proof for.
23
+
24
+ ## Non-goals
25
+
26
+ - **The behaviour being proven.** `product/`.
27
+ - **How the code is structured.** `architecture/`.
28
+ - **Whether the deploy succeeded.** `operations/`.
29
+ - **A single test's assertions.** Code, not governance.
30
+
31
+ ## Rules
32
+
33
+ <!-- A durable rule lives in the domain that owns its KIND of call. Shape:
34
+
35
+ ### <id> — the rule, in one line
36
+ enforced_by: the check that catches a violation — a command, a test, a lint
37
+
38
+ `enforced_by: judgement` is legal where a person really must decide, and is
39
+ counted separately rather than treated as enforcement. An empty one is a wish.
40
+
41
+ This section is EMPTY ON PURPOSE in a fresh repo. Do not seed it with invented
42
+ rules to make the domain look occupied — an empty domain is a signpost saying
43
+ where a call will go when you make one. -->
@@ -0,0 +1,44 @@
1
+ ---
2
+ type: domain
3
+ id: security
4
+ ---
5
+
6
+ # Security — what keeps it safe
7
+
8
+ ## What belongs here
9
+
10
+ Durable calls about **what must stay safe** — secrets and how they are held,
11
+ authentication and authorization boundaries, what untrusted input is and where it is
12
+ trusted, data handling, and what must never enter a published artifact.
13
+
14
+ Its neighbours: [`operations/`](../operations/README.md) owns the publish path and
15
+ keeping the system running — this domain owns what must remain true while it does.
16
+ [`legal/`](../legal/README.md) owns what binds us contractually; a rule about
17
+ technical safety is this domain's, one about obligation is theirs.
18
+ [`architecture/`](../architecture/README.md) owns the seams; where a seam exists
19
+ *because* of a trust boundary, cite this domain's rule from it.
20
+
21
+ **This domain is often empty in a young repo, and that is fine.** It ships anyway,
22
+ because the cost of an empty charter is one file and the cost of a missing domain is
23
+ a security rule mis-filed under whichever system happened to be nearby.
24
+
25
+ ## Non-goals
26
+
27
+ - **How it is deployed.** `operations/`.
28
+ - **Contractual and regulatory obligation.** `legal/`.
29
+ - **Whether the feature works.** `quality/`.
30
+ - **A specific vulnerability.** That is a task, not a durable rule — though the rule that prevents its class belongs here.
31
+
32
+ ## Rules
33
+
34
+ <!-- A durable rule lives in the domain that owns its KIND of call. Shape:
35
+
36
+ ### <id> — the rule, in one line
37
+ enforced_by: the check that catches a violation — a command, a test, a lint
38
+
39
+ `enforced_by: judgement` is legal where a person really must decide, and is
40
+ counted separately rather than treated as enforcement. An empty one is a wish.
41
+
42
+ This section is EMPTY ON PURPOSE in a fresh repo. Do not seed it with invented
43
+ rules to make the domain look occupied — an empty domain is a signpost saying
44
+ where a call will go when you make one. -->