@brainjar/cli 0.1.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.
@@ -0,0 +1,207 @@
1
+ import { Cli, z, Errors } from 'incur'
2
+
3
+ const { IncurError } = Errors
4
+ import { readdir, readFile, writeFile, access } from 'node:fs/promises'
5
+ import { join, basename } from 'node:path'
6
+ import { paths } from '../paths.js'
7
+ import { readState, writeState, withStateLock, readLocalState, writeLocalState, withLocalStateLock, readEnvState, mergeState, requireBrainjarDir, stripFrontmatter, normalizeSlug } from '../state.js'
8
+ import { sync } from '../sync.js'
9
+
10
+ export const soul = Cli.create('soul', {
11
+ description: 'Manage soul — personality and values for the agent',
12
+ })
13
+ .command('create', {
14
+ description: 'Create a new soul',
15
+ args: z.object({
16
+ name: z.string().describe('Soul name (will be used as filename)'),
17
+ }),
18
+ options: z.object({
19
+ description: z.string().optional().describe('One-line description of the soul'),
20
+ }),
21
+ async run(c) {
22
+ await requireBrainjarDir()
23
+ const name = normalizeSlug(c.args.name, 'soul name')
24
+ const dest = join(paths.souls, `${name}.md`)
25
+
26
+ try {
27
+ await access(dest)
28
+ throw new IncurError({
29
+ code: 'SOUL_EXISTS',
30
+ message: `Soul "${name}" already exists.`,
31
+ hint: 'Choose a different name or edit the existing file.',
32
+ })
33
+ } catch (e) {
34
+ if (e instanceof IncurError) throw e
35
+ }
36
+
37
+ const lines: string[] = []
38
+ lines.push(`# ${name}`)
39
+ lines.push('')
40
+ if (c.options.description) {
41
+ lines.push(c.options.description)
42
+ lines.push('')
43
+ }
44
+
45
+ const content = lines.join('\n')
46
+ await writeFile(dest, content)
47
+
48
+ if (c.agent || c.formatExplicit) {
49
+ return { created: dest, name, template: content }
50
+ }
51
+
52
+ return {
53
+ created: dest,
54
+ name,
55
+ template: `\n${content}`,
56
+ next: `Edit ${dest} to flesh out your soul, then run \`brainjar soul use ${name}\` to activate.`,
57
+ }
58
+ },
59
+ })
60
+ .command('list', {
61
+ description: 'List available souls',
62
+ async run() {
63
+ await requireBrainjarDir()
64
+ const entries = await readdir(paths.souls).catch(() => [])
65
+ const souls = entries.filter(f => f.endsWith('.md')).map(f => basename(f, '.md'))
66
+ return { souls }
67
+ },
68
+ })
69
+ .command('show', {
70
+ description: 'Show a soul by name, or the active soul if no name given',
71
+ args: z.object({
72
+ name: z.string().optional().describe('Soul name to show (defaults to active soul)'),
73
+ }),
74
+ options: z.object({
75
+ local: z.boolean().default(false).describe('Show local soul override (if any)'),
76
+ short: z.boolean().default(false).describe('Print only the active soul name'),
77
+ }),
78
+ async run(c) {
79
+ await requireBrainjarDir()
80
+
81
+ if (c.options.short) {
82
+ if (c.args.name) return c.args.name
83
+ const global = await readState()
84
+ const local = await readLocalState()
85
+ const env = readEnvState()
86
+ const effective = mergeState(global, local, env)
87
+ return effective.soul.value ?? 'none'
88
+ }
89
+
90
+ // If a specific name was given, show that soul directly
91
+ if (c.args.name) {
92
+ const name = normalizeSlug(c.args.name, 'soul name')
93
+ try {
94
+ const raw = await readFile(join(paths.souls, `${name}.md`), 'utf-8')
95
+ const content = stripFrontmatter(raw)
96
+ const title = content.split('\n').find(l => l.startsWith('# '))?.replace('# ', '') ?? null
97
+ return { name, title, content }
98
+ } catch {
99
+ throw new IncurError({
100
+ code: 'SOUL_NOT_FOUND',
101
+ message: `Soul "${name}" not found.`,
102
+ hint: 'Run `brainjar soul list` to see available souls.',
103
+ })
104
+ }
105
+ }
106
+
107
+ if (c.options.local) {
108
+ const local = await readLocalState()
109
+ if (!('soul' in local)) return { active: false, scope: 'local', note: 'No local soul override (cascades from global)' }
110
+ if (local.soul === null) return { active: false, scope: 'local', name: null, note: 'Explicitly unset at local scope' }
111
+ try {
112
+ const raw = await readFile(join(paths.souls, `${local.soul}.md`), 'utf-8')
113
+ const content = stripFrontmatter(raw)
114
+ const title = content.split('\n').find(l => l.startsWith('# '))?.replace('# ', '') ?? null
115
+ return { active: true, scope: 'local', name: local.soul, title, content }
116
+ } catch {
117
+ return { active: false, scope: 'local', name: local.soul, error: 'File not found' }
118
+ }
119
+ }
120
+
121
+ const global = await readState()
122
+ const local = await readLocalState()
123
+ const env = readEnvState()
124
+ const effective = mergeState(global, local, env)
125
+ if (!effective.soul.value) return { active: false }
126
+ try {
127
+ const raw = await readFile(join(paths.souls, `${effective.soul.value}.md`), 'utf-8')
128
+ const content = stripFrontmatter(raw)
129
+ const title = content.split('\n').find(l => l.startsWith('# '))?.replace('# ', '') ?? null
130
+ return { active: true, name: effective.soul.value, scope: effective.soul.scope, title, content }
131
+ } catch {
132
+ return { active: false, name: effective.soul.value, error: 'File not found' }
133
+ }
134
+ },
135
+ })
136
+ .command('use', {
137
+ description: 'Activate a soul',
138
+ args: z.object({
139
+ name: z.string().describe('Soul name (filename without .md in ~/.brainjar/souls/)'),
140
+ }),
141
+ options: z.object({
142
+ local: z.boolean().default(false).describe('Write to local .claude/CLAUDE.md instead of global'),
143
+ }),
144
+ async run(c) {
145
+ await requireBrainjarDir()
146
+ const name = normalizeSlug(c.args.name, 'soul name')
147
+ const source = join(paths.souls, `${name}.md`)
148
+ try {
149
+ await readFile(source, 'utf-8')
150
+ } catch {
151
+ throw new IncurError({
152
+ code: 'SOUL_NOT_FOUND',
153
+ message: `Soul "${name}" not found.`,
154
+ hint: 'Run `brainjar soul list` to see available souls.',
155
+ })
156
+ }
157
+
158
+ if (c.options.local) {
159
+ await withLocalStateLock(async () => {
160
+ const local = await readLocalState()
161
+ local.soul = name
162
+ await writeLocalState(local)
163
+ await sync({ local: true })
164
+ })
165
+ } else {
166
+ await withStateLock(async () => {
167
+ const state = await readState()
168
+ state.soul = name
169
+ await writeState(state)
170
+ await sync()
171
+ })
172
+ }
173
+
174
+ return { activated: name, local: c.options.local }
175
+ },
176
+ })
177
+ .command('drop', {
178
+ description: 'Deactivate the current soul',
179
+ options: z.object({
180
+ local: z.boolean().default(false).describe('Remove local soul override or deactivate global soul'),
181
+ }),
182
+ async run(c) {
183
+ await requireBrainjarDir()
184
+ if (c.options.local) {
185
+ await withLocalStateLock(async () => {
186
+ const local = await readLocalState()
187
+ delete local.soul
188
+ await writeLocalState(local)
189
+ await sync({ local: true })
190
+ })
191
+ } else {
192
+ await withStateLock(async () => {
193
+ const state = await readState()
194
+ if (!state.soul) {
195
+ throw new IncurError({
196
+ code: 'NO_ACTIVE_SOUL',
197
+ message: 'No active soul to deactivate.',
198
+ })
199
+ }
200
+ state.soul = null
201
+ await writeState(state)
202
+ await sync()
203
+ })
204
+ }
205
+ return { deactivated: true, local: c.options.local }
206
+ },
207
+ })
@@ -0,0 +1,131 @@
1
+ import { Cli, z } from 'incur'
2
+ import { readState, readLocalState, readEnvState, mergeState, loadIdentity, requireBrainjarDir } from '../state.js'
3
+ import { sync } from '../sync.js'
4
+
5
+ export const status = Cli.create('status', {
6
+ description: 'Show active brain configuration',
7
+ options: z.object({
8
+ sync: z.boolean().default(false).describe('Regenerate config file from active layers'),
9
+ global: z.boolean().default(false).describe('Show only global state'),
10
+ local: z.boolean().default(false).describe('Show only local overrides'),
11
+ short: z.boolean().default(false).describe('One-line output: soul | persona | identity'),
12
+ }),
13
+ async run(c) {
14
+ await requireBrainjarDir()
15
+
16
+ // --short: compact one-liner for scripts/statuslines
17
+ if (c.options.short) {
18
+ const global = await readState()
19
+ const local = await readLocalState()
20
+ const env = readEnvState()
21
+ const effective = mergeState(global, local, env)
22
+ const slug = effective.identity.value
23
+ ? (await loadIdentity(effective.identity.value).catch(() => null))?.slug ?? effective.identity.value
24
+ : null
25
+ const parts = [
26
+ `soul: ${effective.soul.value ?? 'none'}`,
27
+ `persona: ${effective.persona.value ?? 'none'}`,
28
+ `identity: ${slug ?? 'none'}`,
29
+ ]
30
+ return parts.join(' | ')
31
+ }
32
+
33
+ // Sync if requested
34
+ let synced: Record<string, unknown> | undefined
35
+ if (c.options.sync) {
36
+ const syncResult = await sync()
37
+ synced = { written: syncResult.written, warnings: syncResult.warnings }
38
+ }
39
+
40
+ // --global: show only global state (v0.1 behavior)
41
+ if (c.options.global) {
42
+ const state = await readState()
43
+ let identityFull: Record<string, unknown> | null = null
44
+ if (state.identity) {
45
+ try {
46
+ const { content: _, ...id } = await loadIdentity(state.identity)
47
+ identityFull = id
48
+ } catch {
49
+ identityFull = { slug: state.identity, error: 'File not found' }
50
+ }
51
+ }
52
+ const result: Record<string, unknown> = {
53
+ soul: state.soul ?? null,
54
+ persona: state.persona ?? null,
55
+ rules: state.rules,
56
+ identity: identityFull,
57
+ }
58
+ if (synced) result.synced = synced
59
+ return result
60
+ }
61
+
62
+ // --local: show only local overrides
63
+ if (c.options.local) {
64
+ const local = await readLocalState()
65
+ const result: Record<string, unknown> = {}
66
+ if ('soul' in local) result.soul = local.soul
67
+ if ('persona' in local) result.persona = local.persona
68
+ if (local.rules) result.rules = local.rules
69
+ if ('identity' in local) result.identity = local.identity
70
+ if (Object.keys(result).length === 0) result.note = 'No local overrides'
71
+ if (synced) result.synced = synced
72
+ return result
73
+ }
74
+
75
+ // Default: effective state with scope annotations
76
+ const global = await readState()
77
+ const local = await readLocalState()
78
+ const env = readEnvState()
79
+ const effective = mergeState(global, local, env)
80
+
81
+ // Resolve identity details
82
+ let identityFull: Record<string, unknown> | null = null
83
+ if (effective.identity.value) {
84
+ try {
85
+ const { content: _, ...id } = await loadIdentity(effective.identity.value)
86
+ identityFull = { ...id, scope: effective.identity.scope }
87
+ } catch {
88
+ identityFull = { slug: effective.identity.value, scope: effective.identity.scope, error: 'File not found' }
89
+ }
90
+ }
91
+
92
+ // Agents and explicit --format get full structured data
93
+ if (c.agent || c.formatExplicit) {
94
+ const result: Record<string, unknown> = {
95
+ soul: effective.soul,
96
+ persona: effective.persona,
97
+ rules: effective.rules,
98
+ identity: identityFull,
99
+ }
100
+ if (synced) result.synced = synced
101
+ return result
102
+ }
103
+
104
+ // Humans get a compact view with scope annotations
105
+ const fmtScope = (scope: string) => `(${scope})`
106
+
107
+ const identityLabel = identityFull
108
+ ? identityFull.error
109
+ ? `${effective.identity.value} (not found)`
110
+ : identityFull.engine
111
+ ? `${identityFull.slug} ${fmtScope(effective.identity.scope)} (${identityFull.engine})`
112
+ : `${identityFull.slug} ${fmtScope(effective.identity.scope)}`
113
+ : null
114
+
115
+ const rulesLabel = effective.rules.length
116
+ ? effective.rules
117
+ .filter(r => !r.scope.startsWith('-'))
118
+ .map(r => `${r.value} ${fmtScope(r.scope)}`)
119
+ .join(', ')
120
+ : null
121
+
122
+ const result: Record<string, unknown> = {
123
+ soul: effective.soul.value ? `${effective.soul.value} ${fmtScope(effective.soul.scope)}` : null,
124
+ persona: effective.persona.value ? `${effective.persona.value} ${fmtScope(effective.persona.scope)}` : null,
125
+ rules: rulesLabel,
126
+ identity: identityLabel,
127
+ }
128
+ if (synced) result.synced = synced
129
+ return result
130
+ },
131
+ })
@@ -0,0 +1,105 @@
1
+ import { $ } from 'bun'
2
+ import { readFile } from 'node:fs/promises'
3
+ import { paths } from '../paths.js'
4
+ import type { CredentialEngine, EngineStatus } from './types.js'
5
+
6
+ export async function loadSession(): Promise<string | null> {
7
+ // Env var takes precedence, then session file
8
+ if (process.env.BW_SESSION) return process.env.BW_SESSION
9
+ try {
10
+ const session = (await readFile(paths.session, 'utf-8')).trim()
11
+ return session || null
12
+ } catch {
13
+ return null
14
+ }
15
+ }
16
+
17
+ /** Thin shell wrapper — extracted so tests can replace it via spyOn. */
18
+ export const bw = {
19
+ async whichBw(): Promise<void> {
20
+ await $`which bw`.quiet()
21
+ },
22
+
23
+ async status(session: string | null): Promise<any> {
24
+ return session
25
+ ? $`bw status`.env({ ...process.env, BW_SESSION: session }).json()
26
+ : $`bw status`.json()
27
+ },
28
+
29
+ async getItem(item: string, session: string): Promise<any> {
30
+ return $`bw get item ${item}`.env({ ...process.env, BW_SESSION: session }).json()
31
+ },
32
+
33
+ async lock(): Promise<void> {
34
+ await $`bw lock`.quiet()
35
+ },
36
+ }
37
+
38
+ export const bitwarden: CredentialEngine = {
39
+ name: 'bitwarden',
40
+
41
+ async status(): Promise<EngineStatus> {
42
+ try {
43
+ await bw.whichBw()
44
+ } catch {
45
+ return { state: 'not_installed', install: 'npm install -g @bitwarden/cli' }
46
+ }
47
+
48
+ try {
49
+ const session = await loadSession()
50
+ const result = await bw.status(session)
51
+
52
+ if (result.status === 'unauthenticated') {
53
+ return {
54
+ state: 'unauthenticated',
55
+ operator_action: `bw login ${result.userEmail ?? '<email>'}`,
56
+ }
57
+ }
58
+
59
+ if (result.status === 'unlocked' && session) {
60
+ return { state: 'unlocked', session }
61
+ }
62
+
63
+ return {
64
+ state: 'locked',
65
+ operator_action: 'Run `bw unlock` and then `brainjar identity unlock <session>`',
66
+ }
67
+ } catch {
68
+ return {
69
+ state: 'locked',
70
+ operator_action: 'Could not determine vault status. Run `bw unlock` and then `brainjar identity unlock <session>`',
71
+ }
72
+ }
73
+ },
74
+
75
+ async get(item: string, session: string) {
76
+ if (!item || item.length > 256 || /[\x00-\x1f]/.test(item)) {
77
+ return { error: `Invalid item name: "${item}"` }
78
+ }
79
+ try {
80
+ const result = await bw.getItem(item, session)
81
+
82
+ if (result.login?.password) {
83
+ return { value: result.login.password }
84
+ }
85
+ if (result.notes) {
86
+ return { value: result.notes }
87
+ }
88
+
89
+ return { error: `Item "${item}" found but has no password or notes.` }
90
+ } catch (e) {
91
+ const stderr = (e as any)?.stderr?.toString?.()?.trim?.()
92
+ const message = stderr || (e as Error).message || 'unknown error'
93
+ return { error: `Could not retrieve "${item}": ${message}` }
94
+ }
95
+ },
96
+
97
+ async lock() {
98
+ try {
99
+ await bw.lock()
100
+ } catch (e) {
101
+ const stderr = (e as any)?.stderr?.toString?.()?.trim?.()
102
+ throw new Error(`Failed to lock vault: ${stderr || (e as Error).message}`)
103
+ }
104
+ },
105
+ }
@@ -0,0 +1,12 @@
1
+ import type { CredentialEngine } from './types.js'
2
+ import { bitwarden } from './bitwarden.js'
3
+
4
+ const engines: Record<string, CredentialEngine> = {
5
+ bitwarden,
6
+ }
7
+
8
+ export function getEngine(name: string): CredentialEngine | null {
9
+ return engines[name] ?? null
10
+ }
11
+
12
+ export type { CredentialEngine }
@@ -0,0 +1,12 @@
1
+ export type EngineStatus =
2
+ | { state: 'not_installed'; install: string }
3
+ | { state: 'unauthenticated'; operator_action: string }
4
+ | { state: 'locked'; operator_action: string }
5
+ | { state: 'unlocked'; session: string }
6
+
7
+ export interface CredentialEngine {
8
+ name: string
9
+ status(): Promise<EngineStatus>
10
+ get(item: string, session: string): Promise<{ value: string } | { error: string }>
11
+ lock(): Promise<void>
12
+ }
package/src/paths.ts ADDED
@@ -0,0 +1,48 @@
1
+ import { homedir } from 'node:os'
2
+ import { join } from 'node:path'
3
+
4
+ /** Resolved lazily so tests can override HOME env var. */
5
+ export function getHome() {
6
+ return process.env.BRAINJAR_TEST_HOME ?? homedir()
7
+ }
8
+
9
+ export function getBrainjarDir() {
10
+ return process.env.BRAINJAR_HOME ?? join(getHome(), '.brainjar')
11
+ }
12
+
13
+ export type Backend = 'claude' | 'codex'
14
+
15
+ const BACKEND_CONFIG_FILES: Record<Backend, string> = {
16
+ claude: 'CLAUDE.md',
17
+ codex: 'AGENTS.md',
18
+ }
19
+
20
+ export function getBackendConfig(backend: Backend, options?: { local?: boolean }) {
21
+ const configFileName = BACKEND_CONFIG_FILES[backend]
22
+ const dir = options?.local
23
+ ? join(process.cwd(), backend === 'claude' ? '.claude' : '.codex')
24
+ : join(getHome(), backend === 'claude' ? '.claude' : '.codex')
25
+ return {
26
+ dir,
27
+ configFile: join(dir, configFileName),
28
+ configFileName,
29
+ backupFile: join(dir, `${configFileName}.pre-brainjar`),
30
+ }
31
+ }
32
+
33
+ /** Local brainjar dir for per-repo state. */
34
+ export function getLocalDir() {
35
+ return process.env.BRAINJAR_LOCAL_DIR ?? join(process.cwd(), '.brainjar')
36
+ }
37
+
38
+ export const paths = {
39
+ get root() { return getBrainjarDir() },
40
+ get souls() { return join(getBrainjarDir(), 'souls') },
41
+ get personas() { return join(getBrainjarDir(), 'personas') },
42
+ get rules() { return join(getBrainjarDir(), 'rules') },
43
+ get brains() { return join(getBrainjarDir(), 'brains') },
44
+ get identities() { return join(getBrainjarDir(), 'identities') },
45
+ get session() { return join(getBrainjarDir(), '.session') },
46
+ get state() { return join(getBrainjarDir(), 'state.yaml') },
47
+ get localState() { return join(getLocalDir(), 'state.yaml') },
48
+ }
@@ -0,0 +1,26 @@
1
+ ---
2
+ rules:
3
+ - default
4
+ - git-discipline
5
+ ---
6
+
7
+ # Engineer
8
+
9
+ Build what's asked. Build it well.
10
+
11
+ ## Direct mode
12
+ - Clarify ambiguous requirements before writing code. One targeted question beats three assumptions.
13
+ - Show your plan briefly, then execute. Don't ask for permission on obvious steps.
14
+
15
+ ## Subagent mode
16
+ - You will be given a specific task by the orchestrating agent. Deliver production-quality code.
17
+ - Return the implementation with a brief note on any decisions you made.
18
+ - If the task spec is incomplete, flag what's missing — don't fill in gaps silently.
19
+
20
+ ## Always
21
+ - Read existing code before writing new code. Match the project's patterns and conventions.
22
+ - Prefer the simplest solution that fully meets the requirement.
23
+ - Handle errors. Happy path only is not done.
24
+ - Run tests before declaring a task complete.
25
+ - One logical change at a time. Don't mix unrelated fixes.
26
+ - Respect the codebase. It's the user's house — don't rearrange the furniture.
@@ -0,0 +1,24 @@
1
+ ---
2
+ rules:
3
+ - default
4
+ ---
5
+
6
+ # Planner
7
+
8
+ Think first, build second.
9
+
10
+ ## Direct mode
11
+ - Start with "what problem are we solving?" Clarify intent and constraints before proposing solutions.
12
+ - Present options with tradeoffs, then recommend one. Don't just list — decide.
13
+
14
+ ## Subagent mode
15
+ - You will be given a design or analysis task. Return structured, actionable output — not vague suggestions.
16
+ - Include file paths, interfaces, and concrete steps. A plan someone else can execute.
17
+ - Surface risks and dependencies explicitly.
18
+
19
+ ## Always
20
+ - Break large problems into phases. Name what's in each phase and what's deferred.
21
+ - Consider how pieces fit together. Local changes have system-wide effects.
22
+ - Challenge assumptions. "Do we actually need this?" is a valid design question.
23
+ - Keep the long game in mind, but don't gold-plate. Design for the next 3 changes, not the next 30.
24
+ - Write it down. A plan in prose beats a plan in memory.
@@ -0,0 +1,27 @@
1
+ ---
2
+ rules:
3
+ - default
4
+ - security
5
+ ---
6
+
7
+ # Reviewer
8
+
9
+ Find problems before they ship.
10
+
11
+ ## Direct mode
12
+ - Review code the user points you to. Ask for context if the intent isn't clear.
13
+ - Prioritize: correctness first, then security, then maintainability. Style last.
14
+
15
+ ## Subagent mode
16
+ - You will be given specific files or changes to review. Read every file mentioned. Return a structured verdict:
17
+ - **Pass**: implementation is correct, secure, and meets the stated goal.
18
+ - **Issues found**: list each issue with file path, line number, severity (blocker/warning), and suggested fix.
19
+ - Do not make changes yourself — report findings.
20
+
21
+ ## Always
22
+ - Be skeptical by default. Assume there's a bug until proven otherwise.
23
+ - Read error paths as carefully as happy paths. Most bugs live in error handling.
24
+ - Look for edge cases, null/undefined, and off-by-one errors.
25
+ - Flag security issues immediately: injection, auth gaps, leaked secrets.
26
+ - Be honest, not harsh. Point out issues clearly without being condescending.
27
+ - Don't nitpick style when there are real problems to fix.
@@ -0,0 +1,25 @@
1
+ # Boundaries
2
+
3
+ This rule prevents scope creep and unwanted changes.
4
+
5
+ ## Scope Control
6
+
7
+ - Only modify files directly related to the current task.
8
+ - Don't refactor code that isn't broken and isn't part of the task.
9
+ - Don't "improve" code you happen to read while working on something else.
10
+ - One task at a time. Finish the current task before suggesting new ones.
11
+
12
+ ## Ask Before
13
+
14
+ - Adding or removing dependencies.
15
+ - Changing configuration files (CI, linters, formatters, build configs).
16
+ - Modifying git workflow (hooks, branch strategies).
17
+ - Changing project structure or moving files.
18
+ - Altering APIs or interfaces used by other parts of the codebase.
19
+
20
+ ## File Discipline
21
+
22
+ - Don't create files that weren't requested (docs, configs, helpers "for later").
23
+ - Don't delete files without confirming they're unused.
24
+ - Don't rename files or variables for style preferences.
25
+ - Keep changes minimal and reviewable.
@@ -0,0 +1,17 @@
1
+ # Context Recovery
2
+
3
+ This rule governs behavior after context compaction, session resume, or any situation where prior conversation may be lost.
4
+
5
+ ## After Context Loss
6
+
7
+ 1. **Re-read the task plan.** Check todo lists, plan files, and any task-tracking artifacts.
8
+ 2. **Check recent changes.** Run `git diff` and `git log --oneline -10` to see what's been done.
9
+ 3. **Re-read active files.** Open files you're currently modifying — don't rely on memory of their contents.
10
+ 4. **Summarize state.** Briefly state what's done, what's in progress, and what's next before continuing work.
11
+
12
+ ## Rules
13
+
14
+ - Never continue from memory alone after compaction. Always re-ground in artifacts.
15
+ - If you can't determine the current state, ask the user rather than guessing.
16
+ - Treat every post-compaction turn as if you're a new engineer picking up someone else's work.
17
+ - The task isn't "remember what we were doing" — it's "figure out what needs doing next."
@@ -0,0 +1,31 @@
1
+ # Task Completion
2
+
3
+ This rule defines what "done" means. Premature completion is worse than slow completion.
4
+
5
+ ## Completion Criteria
6
+
7
+ A task is done only when ALL of the following are true:
8
+
9
+ 1. **No stubs.** No TODOs, no placeholders, no "implement this later" comments.
10
+ 2. **Code works.** It compiles/parses without errors. If there are tests, they pass.
11
+ 3. **Requirement met.** Re-read the original request. Does the implementation fully satisfy it?
12
+ 4. **Self-review passed.** Read through your changes as if reviewing someone else's PR.
13
+
14
+ ## Before Declaring Done
15
+
16
+ - Re-read the user's original request word by word.
17
+ - Diff your changes against what was asked for. Look for gaps.
18
+ - If the task involved multiple steps, verify each one individually.
19
+ - Run any relevant verification commands (build, test, lint).
20
+
21
+ ## When You Can't Complete
22
+
23
+ - Say so explicitly. "I can't complete this because X."
24
+ - Don't deliver partial work dressed up as complete work.
25
+ - Suggest concrete next steps the user can take.
26
+
27
+ ## Anti-Patterns
28
+
29
+ - Saying "done" then listing caveats that mean it's not actually done.
30
+ - Implementing 90% and leaving the hard part as a TODO.
31
+ - Skipping verification because "it should work."