@gobing-ai/spur 0.3.4 → 0.3.6
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.
- package/package.json +1 -1
- package/spur-cli/config/rules/typescript/guarded-happy-dom-register.yaml +64 -0
- package/spur-cli/config/rules/typescript/no-leaky-module-mocks.yaml +8 -9
- package/spur-cli/config/rules/typescript/no-module-scope-import-calls.yaml +61 -0
- package/spur-cli/config/rules/typescript/no-unmocked-module-eval-side-effects.yaml +67 -0
- package/spur-cli/config/templates/AGENTS.md +40 -0
- package/spur-cli/config/templates/docs/00_ADR.md +5 -0
- package/spur-cli/config/templates/docs/99_PROJECT_CONSTITUTION.md +1 -1
- package/spur.js +45 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gobing-ai/spur",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.6",
|
|
4
4
|
"description": "Spur CLI — local-first harness for mainstream coding agents: constraint checking, workflow orchestration, agent health, and history analytics. Bun-native; exposes the `spur` command.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"spur",
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
$schema: "@gobing-ai/spur/schemas/rule-file.schema.json"
|
|
2
|
+
# Require try/catch guard around GlobalRegistrator.register() in web tests.
|
|
3
|
+
#
|
|
4
|
+
# WHY: GlobalRegistrator.register() sets up happy-dom globals (window, document,
|
|
5
|
+
# etc.). In a full bun test suite, multiple test files call it. The first call
|
|
6
|
+
# succeeds; subsequent calls throw "Happy DOM has already been globally
|
|
7
|
+
# registered" and the error kills the test file (4 test files × ~13 tests each
|
|
8
|
+
# = 52 tests lost). This only surfaces when ALL tests run together (CI, or
|
|
9
|
+
# `bun test ./apps/web/`), not when a single file runs in isolation.
|
|
10
|
+
# Real incident: 52 CI-only failures across 4 test files, 2026-07-09.
|
|
11
|
+
#
|
|
12
|
+
# FIX: wrap in try/catch:
|
|
13
|
+
#
|
|
14
|
+
# try { GlobalRegistrator.register(); } catch {} // already registered in suite
|
|
15
|
+
#
|
|
16
|
+
# The teardown helper (apps/web/tests/happy-dom.ts) is the sanctioned unregister
|
|
17
|
+
# caller (see happy-dom-teardown.yaml). This rule guards the register side.
|
|
18
|
+
#
|
|
19
|
+
# Severity warning: the file may pass in isolation but will fail in the full
|
|
20
|
+
# suite; fix before merge.
|
|
21
|
+
include:
|
|
22
|
+
- "apps/web/tests/**/*.test.ts"
|
|
23
|
+
- "apps/web/tests/**/*.test.tsx"
|
|
24
|
+
exclude:
|
|
25
|
+
- "**/node_modules/**"
|
|
26
|
+
- "**/dist/**"
|
|
27
|
+
# Helper that manages happy-dom lifecycle — not a raw register call.
|
|
28
|
+
- "apps/web/tests/happy-dom.ts"
|
|
29
|
+
# These files already wrap register() in try/catch; they are compliant.
|
|
30
|
+
- "apps/web/tests/modules/task-kanban/board.test.tsx"
|
|
31
|
+
- "apps/web/tests/modules/task-kanban/components.test.tsx"
|
|
32
|
+
- "apps/web/tests/modules/task-kanban/task-detail.test.tsx"
|
|
33
|
+
- "apps/web/tests/modules/task-kanban/index.test.tsx"
|
|
34
|
+
- "apps/web/tests/modules/task-kanban/new-task-panel.test.tsx"
|
|
35
|
+
- "apps/web/tests/modules/task-kanban/markdown-body.test.tsx"
|
|
36
|
+
- "apps/web/tests/modules/task-kanban/useTaskParams.test.tsx"
|
|
37
|
+
- "apps/web/tests/modules/task-kanban/task-filters.test.tsx"
|
|
38
|
+
- "apps/web/tests/modules/task-kanban/useTasks.test.ts"
|
|
39
|
+
- "apps/web/tests/modules/features/components.test.tsx"
|
|
40
|
+
- "apps/web/tests/lib/theme.test.ts"
|
|
41
|
+
- "apps/web/tests/components/BoardLayout.test.tsx"
|
|
42
|
+
- "apps/web/tests/modules/observability/components.test.tsx"
|
|
43
|
+
- "apps/web/tests/components/ThemeToggle.test.tsx"
|
|
44
|
+
- "apps/web/tests/components/ResponsiveAndTheme.test.tsx"
|
|
45
|
+
|
|
46
|
+
rules:
|
|
47
|
+
- id: guarded-happy-dom-register
|
|
48
|
+
description: >
|
|
49
|
+
GlobalRegistrator.register() must be wrapped in try/catch — without
|
|
50
|
+
it, the second test file that calls it throws "already been globally
|
|
51
|
+
registered" and all tests in that file are lost (a CI-only failure
|
|
52
|
+
that never reproduces when a single file runs in isolation). Wrap:
|
|
53
|
+
`try { GlobalRegistrator.register(); } catch {}`. 52-real CI incidents,
|
|
54
|
+
2026-07-09.
|
|
55
|
+
severity: warning
|
|
56
|
+
evaluator:
|
|
57
|
+
type: rg
|
|
58
|
+
config:
|
|
59
|
+
# Match bare GlobalRegistrator.register() — NOT inside try/catch
|
|
60
|
+
# Negative lookbehind: line does NOT start with "try" before the call
|
|
61
|
+
pattern: "GlobalRegistrator\\.register\\(\\)"
|
|
62
|
+
# Only flag lines where the call is NOT preceded by try{ on the same line
|
|
63
|
+
# (the fixed version is `try { GlobalRegistrator.register(); } catch {}`)
|
|
64
|
+
# We match the call itself and let the description guide the fix
|
|
@@ -35,21 +35,20 @@ include:
|
|
|
35
35
|
exclude:
|
|
36
36
|
- "**/node_modules/**"
|
|
37
37
|
- "**/dist/**"
|
|
38
|
-
#
|
|
39
|
-
#
|
|
40
|
-
|
|
41
|
-
#
|
|
42
|
-
#
|
|
43
|
-
# Keep this list minimal: only files that mock a first-party module and have no
|
|
44
|
-
# spyOn-able alternative belong here. (markdown-body mocks only third-party modules —
|
|
45
|
-
# mermaid/dompurify — which the narrowed pattern no longer flags, so it is not listed.)
|
|
38
|
+
# Shared full-surface mock helpers are the CORRECT pattern — they ensure every
|
|
39
|
+
# test file registers the same compatible surface, neutralizing "last mock wins."
|
|
40
|
+
- "apps/web/tests/test-helpers/rpc-client-mock.ts"
|
|
41
|
+
# These files use beforeEach restoreMock() atop the shared baseline — the mock.module
|
|
42
|
+
# call is in a runtime function, not at module scope, and always registers the full surface.
|
|
46
43
|
- "apps/web/tests/modules/task-kanban/board.test.tsx"
|
|
47
44
|
- "apps/web/tests/modules/task-kanban/components.test.tsx"
|
|
48
45
|
- "apps/web/tests/modules/task-kanban/new-task-panel.test.tsx"
|
|
49
46
|
- "apps/web/tests/modules/features/components.test.tsx"
|
|
50
47
|
- "apps/web/tests/modules/task-kanban/task-detail.test.tsx"
|
|
51
48
|
- "apps/web/tests/modules/task-kanban/index.test.tsx"
|
|
52
|
-
|
|
49
|
+
# index.test.tsx no longer mocks useTasks/useTaskParams/TaskDetail; useTasks.test.ts
|
|
50
|
+
# uses custom listFn (isolated store), not mock.module for first-party modules.
|
|
51
|
+
- "apps/web/tests/modules/task-kanban/useTasks.test.ts"
|
|
53
52
|
rules:
|
|
54
53
|
- id: no-leaky-mock-module
|
|
55
54
|
description: >
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
$schema: "@gobing-ai/spur/schemas/rule-file.schema.json"
|
|
2
|
+
# Forbid calling imported functions at module-evaluation scope in source files.
|
|
3
|
+
#
|
|
4
|
+
# WHY: calling an imported function at module scope (outside any function/class
|
|
5
|
+
# body) creates an evaluation-time side effect that captures the depencency
|
|
6
|
+
# before any test mock can intercept it. If the first test file that evaluates
|
|
7
|
+
# this module has a mock active, the module caches with that mock's state; in
|
|
8
|
+
# CI (where test-file order differs from local), a different test may evaluate
|
|
9
|
+
# it first with NO mock or a different mock, causing CI-only failures that pass
|
|
10
|
+
# locally every time. Real incidents:
|
|
11
|
+
#
|
|
12
|
+
# 2026-07-09: useTasks.ts:6 — const SSE_URL = `${resolveApiUrl()}/...` (73 failures)
|
|
13
|
+
# 2026-07-09: SystemEventsTab.tsx:42-43 — const HISTORY_URL = `${resolveApiUrl()}/...`;
|
|
14
|
+
# const SSE_URL = `${resolveApiUrl()}/...` (1 failure)
|
|
15
|
+
#
|
|
16
|
+
# The pattern is always the same: a top-level `const` or `let` whose initializer
|
|
17
|
+
# calls a function imported from another module. Defer the call to a function:
|
|
18
|
+
#
|
|
19
|
+
# // BEFORE (broken):
|
|
20
|
+
# const SSE_URL = `${resolveApiUrl()}/events/planning`;
|
|
21
|
+
#
|
|
22
|
+
# // AFTER (fixed):
|
|
23
|
+
# const sseUrl = () => `${resolveApiUrl()}/events/planning`;
|
|
24
|
+
#
|
|
25
|
+
# FIX: wrap the initializer in an arrow function and call it lazily at the
|
|
26
|
+
# point of use. If the value is used as a default parameter or a constant
|
|
27
|
+
# reference, convert it to a getter function or compute it in the
|
|
28
|
+
# constructor/init method.
|
|
29
|
+
#
|
|
30
|
+
# Severity: warning. A hit may pass locally but WILL fail in CI if a mock is
|
|
31
|
+
# involved; justify with a comment or convert to lazy.
|
|
32
|
+
include:
|
|
33
|
+
- "apps/**/src/**/*.ts"
|
|
34
|
+
- "apps/**/src/**/*.tsx"
|
|
35
|
+
- "packages/**/src/**/*.ts"
|
|
36
|
+
- "packages/**/src/**/*.tsx"
|
|
37
|
+
exclude:
|
|
38
|
+
- "**/node_modules/**"
|
|
39
|
+
- "**/dist/**"
|
|
40
|
+
- "**/tests/**"
|
|
41
|
+
- "**/*.test.ts"
|
|
42
|
+
- "**/*.test.tsx"
|
|
43
|
+
|
|
44
|
+
rules:
|
|
45
|
+
- id: no-module-scope-import-calls
|
|
46
|
+
description: >
|
|
47
|
+
Calling an imported function at module scope (outside any function or
|
|
48
|
+
class body) creates an evaluation-time side effect that captures the
|
|
49
|
+
dependency before any test mock can intercept it — a CI-only,
|
|
50
|
+
ordering-dependent failure. Defer the call: wrap the value in an arrow
|
|
51
|
+
function and call it lazily at the point of use. Real incidents:
|
|
52
|
+
useTasks.ts:6 (73 CI failures), SystemEventsTab.tsx:42 (1 CI failure),
|
|
53
|
+
2026-07-09.
|
|
54
|
+
severity: warning
|
|
55
|
+
evaluator:
|
|
56
|
+
type: rg
|
|
57
|
+
config:
|
|
58
|
+
# Match top-level const/let whose initializer is a template literal
|
|
59
|
+
# calling an import (e.g. resolveApiUrl(), api.*, ...).
|
|
60
|
+
# The pattern: line starts with `const`/`let`, contains `${...()}`.
|
|
61
|
+
pattern: "^(const|let)\\s+\\w+\\s*=\\s*`[^`]*\\$\\{[^}]*\\([^)]*\\)[^}]*\\}[^`]*`"
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
$schema: "@gobing-ai/spur/schemas/rule-file.schema.json"
|
|
2
|
+
# Require test files to mock dependencies of modules with module-eval side effects.
|
|
3
|
+
#
|
|
4
|
+
# WHY (the general pattern): some source modules run code at module-evaluation
|
|
5
|
+
# time — creating singletons, computing constants from imports, calling functions
|
|
6
|
+
# from other modules at module scope (not inside a function/class body). When a
|
|
7
|
+
# test file imports such a module, it triggers the side effect. If the dependency
|
|
8
|
+
# modules are not mocked, the side effect captures the real API and that state
|
|
9
|
+
# persists across the entire test suite — a CI-only, file-ordering-dependent
|
|
10
|
+
# failure that never reproduces locally. The sibling rule `no-leaky-module-mocks`
|
|
11
|
+
# handles the mirror problem (mock.module leaking into real-import readers).
|
|
12
|
+
#
|
|
13
|
+
# KNOWN MODULES WITH MODULE-EVAL SIDE EFFECTS:
|
|
14
|
+
#
|
|
15
|
+
# (None currently active — the useTasks.ts singleton side effect was fixed by
|
|
16
|
+
# commit 3937d31 which converted resolveApiUrl() and api.task.list() calls to
|
|
17
|
+
# lazy dynamic imports. The singleton _sharedStore = new TaskStore() no longer
|
|
18
|
+
# triggers an API call at module-eval time. When a new side-effect module is
|
|
19
|
+
# discovered, add it here with the pattern shown below.)
|
|
20
|
+
#
|
|
21
|
+
# RESOLVED — src/modules/task-kanban/useTasks.ts (fixed 2026-07-08)
|
|
22
|
+
# - Line 6: was const SSE_URL = `${resolveApiUrl()}/events/planning`;
|
|
23
|
+
# → now: const sseUrl = async () => { const { resolveApiUrl } = await import(...); return ...; };
|
|
24
|
+
# - Line 159: was const sharedStore = new TaskStore(); (calls defaultListTasks
|
|
25
|
+
# which statically imported api.task.list) → now: defaultListTasks uses
|
|
26
|
+
# dynamic await import('../../lib/rpc-client'), so construction is lazy.
|
|
27
|
+
# - 73 CI failures, 2026-07-08. Fix: commit 3937d31.
|
|
28
|
+
#
|
|
29
|
+
# To ADD a new module to this list when another CI-only failure is traced to a
|
|
30
|
+
# module-eval side effect: append its import pattern to `rules[].evaluator.config.pattern`
|
|
31
|
+
# (alternation) and add the corresponding entry above.
|
|
32
|
+
include:
|
|
33
|
+
- "apps/**/tests/**/*.test.ts"
|
|
34
|
+
- "apps/**/tests/**/*.test.tsx"
|
|
35
|
+
- "packages/**/tests/**/*.test.ts"
|
|
36
|
+
- "packages/**/tests/**/*.test.tsx"
|
|
37
|
+
exclude:
|
|
38
|
+
- "**/node_modules/**"
|
|
39
|
+
- "**/dist/**"
|
|
40
|
+
# All known side-effect modules are resolved. When a new one is discovered,
|
|
41
|
+
# exclude files that already mock the dependency before importing it.
|
|
42
|
+
|
|
43
|
+
rules:
|
|
44
|
+
- id: no-unmocked-module-eval-side-effects
|
|
45
|
+
description: >
|
|
46
|
+
Test files that import a module with module-evaluation side effects
|
|
47
|
+
(singletons created at module scope, imports called at eval time, etc.)
|
|
48
|
+
MUST mock the dependency modules BEFORE the import. Without the mock,
|
|
49
|
+
the side effect captures the real API and leaks into every subsequent
|
|
50
|
+
test file — a CI-only, ordering-dependent failure. Add mock.module()
|
|
51
|
+
for the listed dependency before any import of the side-effect module.
|
|
52
|
+
See the file header for the list of known side-effect modules and their
|
|
53
|
+
dependencies. 73-real CI incidents (useTasks → rpc-client), 2026-07-09.
|
|
54
|
+
severity: warning
|
|
55
|
+
evaluator:
|
|
56
|
+
type: rg
|
|
57
|
+
config:
|
|
58
|
+
# Match imports from modules with KNOWN, ACTIVE module-eval side effects.
|
|
59
|
+
# IMPORTANT: this rule is INERT unless a pattern is present. When a new
|
|
60
|
+
# side-effect module is discovered (CI-only ordering failure traced to
|
|
61
|
+
# an import-time call), add its import pattern here as a string. Remove
|
|
62
|
+
# the pattern once the source module is fixed (side effects made lazy).
|
|
63
|
+
#
|
|
64
|
+
# RESOLVED modules (patterns removed after fix — kept as history):
|
|
65
|
+
# - task-kanban/useTasks — fixed 2026-07-08, commit 3937d31
|
|
66
|
+
# (resolveApiUrl/api.task.list converted to lazy dynamic imports)
|
|
67
|
+
pattern: "__no_active_side_effect_modules__"
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# AGENTS.md
|
|
2
|
+
|
|
3
|
+
Guidance for AI coding agents working in this repository.
|
|
4
|
+
|
|
5
|
+
## Project
|
|
6
|
+
|
|
7
|
+
<!-- Replace the line below with your project name and a one-line description. -->
|
|
8
|
+
**{project-name}** — (one-line description).
|
|
9
|
+
|
|
10
|
+
## Commands
|
|
11
|
+
|
|
12
|
+
<!-- Fill in the build/test/lint commands for this project. Examples: -->
|
|
13
|
+
```bash
|
|
14
|
+
# build
|
|
15
|
+
# test
|
|
16
|
+
# lint
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Conventions & boundaries
|
|
20
|
+
|
|
21
|
+
- Conventional Commits required (`feat:`, `fix:`, `docs:`, `chore:`, ...). Breaking changes in a `BREAKING CHANGE:` footer.
|
|
22
|
+
- Never commit secrets, `.env*`, or credentials.
|
|
23
|
+
- Surgical changes only: touch what the task needs; no drive-by refactors, no speculative abstractions,
|
|
24
|
+
no comments that restate what the code already says.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Indexed context
|
|
29
|
+
|
|
30
|
+
Project context lives in `.spur/context/` (gitignored) and is surfaced by the `sp:indexed-context` skill.
|
|
31
|
+
Check it before re-reading files you may already have indexed:
|
|
32
|
+
|
|
33
|
+
1. `.spur/context/anatomy.md` — one-line description + token estimate per file. Read before opening a file.
|
|
34
|
+
2. `.spur/context/learnings.md` — project conventions, decisions, preferences. Read before generating code.
|
|
35
|
+
3. `.spur/context/pitfalls.md` — dated "do-not-repeat" entries. Read before generating code.
|
|
36
|
+
4. `.spur/context/buglog.md` — historical bug log. Read before fixing a bug.
|
|
37
|
+
5. `.spur/context/memory.md` — session log. Append one line per significant action.
|
|
38
|
+
6. `.spur/context/token-ledger.jsonl` — auto-tracked by hooks; never hand-edit.
|
|
39
|
+
|
|
40
|
+
If `.spur/context/` is absent, proceed normally. Never block work on its absence.
|
|
@@ -12,6 +12,9 @@ updated_at: 1970-01-01T00:00:00.000Z
|
|
|
12
12
|
|
|
13
13
|
> Authoritative on **decisions**. Lower number wins — this doc overrides all others on decisions.
|
|
14
14
|
> Each entry is append-only; supersession is by a new dated entry, never by editing an old one.
|
|
15
|
+
> Only real cross-cutting decisions belong here — not implementation notes, not feature status, not
|
|
16
|
+
> how-to guidance. Entries that grow past decision + reason are carrying mechanism that belongs in
|
|
17
|
+
> `03`/`04`; link it instead of inlining it.
|
|
15
18
|
|
|
16
19
|
## ADR-001 — (example) Adopt this doc structure
|
|
17
20
|
|
|
@@ -24,4 +27,6 @@ updated_at: 1970-01-01T00:00:00.000Z
|
|
|
24
27
|
<!--
|
|
25
28
|
Add new ADRs here. Copy the entry shape above (Date, Status, Context, Decision, Reason).
|
|
26
29
|
A decision that reverses a prior ADR adds a new entry that says "supersedes ADR-NNN".
|
|
30
|
+
An Amendment records the decision delta + one-line reason — not the mechanism. Implementation
|
|
31
|
+
paths, detailed semantics, and multi-paragraph rationale belong in 03/04, not in the amendment.
|
|
27
32
|
-->
|
|
@@ -53,7 +53,7 @@ When a change touches one of these, the listed doc MUST be updated in the **same
|
|
|
53
53
|
|
|
54
54
|
| ID | Trigger | Doc(s) |
|
|
55
55
|
|----|---------|--------|
|
|
56
|
-
| T1 | New cross-cutting decision | `00` first, then `03` mechanism |
|
|
56
|
+
| T1 | New cross-cutting decision (entry = decision + one-line reason; amendment = decision delta only — mechanism goes in `03`/`04`, not the amendment) | `00` first, then `03` mechanism |
|
|
57
57
|
| T3 | Command/flag/config/schema/DTO added/changed | `04` + `AGENTS.md` |
|
|
58
58
|
| T4 | Feature ships or changes state | `05` row |
|
|
59
59
|
| T6 | Scope added / cut / deferred | `01` |
|
package/spur.js
CHANGED
|
@@ -70181,7 +70181,7 @@ import { join as join13, resolve as resolve5 } from "path";
|
|
|
70181
70181
|
var CLI_CONFIG = {
|
|
70182
70182
|
binaryName: "spur",
|
|
70183
70183
|
binaryLabel: "spur",
|
|
70184
|
-
binaryVersion: "0.3.
|
|
70184
|
+
binaryVersion: "0.3.6",
|
|
70185
70185
|
configDir: ".spur",
|
|
70186
70186
|
configFile: ".spur/config.yaml",
|
|
70187
70187
|
databaseFile: ".spur/spur.db"
|
|
@@ -70227,7 +70227,8 @@ var SCAFFOLD_MANIFEST = [
|
|
|
70227
70227
|
{ source: "templates/docs/02_ROADMAP.md", target: "templates/docs/02_ROADMAP.md" },
|
|
70228
70228
|
{ source: "templates/docs/03_ARCHITECTURE.md", target: "templates/docs/03_ARCHITECTURE.md" },
|
|
70229
70229
|
{ source: "templates/docs/04_DESIGN.md", target: "templates/docs/04_DESIGN.md" },
|
|
70230
|
-
{ source: "templates/docs/05_FEATURES.md", target: "templates/docs/05_FEATURES.md" }
|
|
70230
|
+
{ source: "templates/docs/05_FEATURES.md", target: "templates/docs/05_FEATURES.md" },
|
|
70231
|
+
{ source: "templates/AGENTS.md", target: "AGENTS.md", root: true, preserve: true }
|
|
70231
70232
|
];
|
|
70232
70233
|
|
|
70233
70234
|
// src/commands/init.ts
|
|
@@ -70235,6 +70236,24 @@ var GLOBAL_CONFIG_DIR2 = join13(".config", "spur");
|
|
|
70235
70236
|
var GLOBAL_RULES_DIR2 = join13(GLOBAL_CONFIG_DIR2, "rules");
|
|
70236
70237
|
var GLOBAL_CONFIG_EXAMPLE = "config.example.yaml";
|
|
70237
70238
|
var GLOBAL_CONFIG_FILE2 = "config.yaml";
|
|
70239
|
+
var INDEXED_CONTEXT_MARKER = "## Indexed context";
|
|
70240
|
+
var INDEXED_CONTEXT_BLOCK = `
|
|
70241
|
+
---
|
|
70242
|
+
|
|
70243
|
+
## Indexed context
|
|
70244
|
+
|
|
70245
|
+
Project context lives in \`.spur/context/\` (gitignored) and is surfaced by the \`sp:indexed-context\` skill.
|
|
70246
|
+
Check it before re-reading files you may already have indexed:
|
|
70247
|
+
|
|
70248
|
+
1. \`.spur/context/anatomy.md\` \u2014 one-line description + token estimate per file. Read before opening a file.
|
|
70249
|
+
2. \`.spur/context/learnings.md\` \u2014 project conventions, decisions, preferences. Read before generating code.
|
|
70250
|
+
3. \`.spur/context/pitfalls.md\` \u2014 dated "do-not-repeat" entries. Read before generating code.
|
|
70251
|
+
4. \`.spur/context/buglog.md\` \u2014 historical bug log. Read before fixing a bug.
|
|
70252
|
+
5. \`.spur/context/memory.md\` \u2014 session log. Append one line per significant action.
|
|
70253
|
+
6. \`.spur/context/token-ledger.jsonl\` \u2014 auto-tracked by hooks; never hand-edit.
|
|
70254
|
+
|
|
70255
|
+
If \`.spur/context/\` is absent, proceed normally. Never block work on its absence.
|
|
70256
|
+
`;
|
|
70238
70257
|
function globalRulesRoot(context3) {
|
|
70239
70258
|
const override = context3.env.SPUR_GLOBAL_RULES_DIR;
|
|
70240
70259
|
return override !== undefined && override.length > 0 ? resolve5(context3.cwd, override) : join13(homedir5(), GLOBAL_RULES_DIR2);
|
|
@@ -70336,6 +70355,22 @@ function registerInitCommand(program2, context3) {
|
|
|
70336
70355
|
const agentsDir = join13(context3.cwd, CLI_CONFIG.configDir, "agents");
|
|
70337
70356
|
await context3.fs.ensureDir(agentsDir);
|
|
70338
70357
|
await writeIfNew(context3, join13(agentsDir, ".gitkeep"), "", force, result);
|
|
70358
|
+
const gitignorePath = join13(context3.cwd, ".gitignore");
|
|
70359
|
+
const contextEntry = ".spur/context/";
|
|
70360
|
+
if (await context3.fs.exists(gitignorePath)) {
|
|
70361
|
+
const existing = await context3.fs.readFile(gitignorePath);
|
|
70362
|
+
if (!existing.includes(contextEntry)) {
|
|
70363
|
+
await context3.fs.writeFile(gitignorePath, `${existing.trimEnd()}
|
|
70364
|
+
|
|
70365
|
+
# Spur indexed-context (machine-generated)
|
|
70366
|
+
${contextEntry}
|
|
70367
|
+
`);
|
|
70368
|
+
}
|
|
70369
|
+
} else {
|
|
70370
|
+
await context3.fs.writeFile(gitignorePath, `# Spur indexed-context (machine-generated)
|
|
70371
|
+
${contextEntry}
|
|
70372
|
+
`);
|
|
70373
|
+
}
|
|
70339
70374
|
if (!minimal) {
|
|
70340
70375
|
const configRoot = bundledConfigRoot();
|
|
70341
70376
|
if (configRoot !== null) {
|
|
@@ -70351,6 +70386,14 @@ function registerInitCommand(program2, context3) {
|
|
|
70351
70386
|
}
|
|
70352
70387
|
}
|
|
70353
70388
|
}
|
|
70389
|
+
const agentsMdPath = join13(context3.cwd, "AGENTS.md");
|
|
70390
|
+
if (await context3.fs.exists(agentsMdPath)) {
|
|
70391
|
+
const existing = await context3.fs.readFile(agentsMdPath);
|
|
70392
|
+
if (!existing.includes(INDEXED_CONTEXT_MARKER)) {
|
|
70393
|
+
await context3.fs.writeFile(agentsMdPath, `${existing.trimEnd()}${INDEXED_CONTEXT_BLOCK}`);
|
|
70394
|
+
result.created.push(agentsMdPath);
|
|
70395
|
+
}
|
|
70396
|
+
}
|
|
70354
70397
|
const db2 = await context3.getDb();
|
|
70355
70398
|
await new ArtifactDao(db2).record({ path: configPath, kind: "config" });
|
|
70356
70399
|
const rulesSeeded = await seedGlobalRules(context3);
|