@shawaze/agentspace 0.3.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Shawaze Ahmer
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,106 @@
1
+ # agentspace
2
+
3
+ [![CI](https://github.com/irucsS-9/agentspace/actions/workflows/ci.yml/badge.svg)](https://github.com/irucsS-9/agentspace/actions/workflows/ci.yml)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](./LICENSE)
5
+ [![npm](https://img.shields.io/npm/v/@shawaze/agentspace.svg)](https://www.npmjs.com/package/@shawaze/agentspace)
6
+
7
+ **Scaffold an agent-native multi-repo workspace** — a coordination layer that sits
8
+ above your sibling repositories and keeps them coherent for AI coding agents.
9
+
10
+ ```bash
11
+ npx @shawaze/agentspace init # interactive wizard → scaffold the workspace
12
+ npx @shawaze/agentspace doctor # mechanical health checks on a workspace
13
+ ```
14
+
15
+ > Status: **v0.3.** Workspace reconstruction, the memory-bank wiki, the
16
+ > enforcement pack, and the cross-repo contract layer all work today.
17
+
18
+ ---
19
+
20
+ ## Why this exists
21
+
22
+ If you run a product as **several separate repositories** (a backend, a web app, a
23
+ mobile client, shared libraries) — a *polyrepo*, not a monorepo — AI coding agents
24
+ have a blind spot: an agent that changes an API in one repo can't see the consumers
25
+ it just broke in another, and every session re-derives the same cross-repo context
26
+ from scratch.
27
+
28
+ `agentspace` is **not** a monorepo tool (Nx, Turborepo, pnpm workspaces). Those
29
+ unify repos under one build. agentspace does the opposite: it leaves your repos
30
+ independent and adds a thin **coordination layer above them** — a declarative
31
+ manifest, an LLM-curated knowledge wiki, cross-repo contracts, and
32
+ boundary-enforced agents — so the *set* of repos stays coherent for an agent
33
+ without a human babysitting drift.
34
+
35
+ It is the generalization of a hand-built workspace methodology into a reusable tool.
36
+
37
+ ## The four pillars
38
+
39
+ | Pillar | What it gives you | Status |
40
+ |---|---|---|
41
+ | **Workspace reconstruction** | A declarative `manifest.yaml` + an idempotent `clone-repos.sh` that rebuilds the whole workspace on any machine. | ✅ v0.1 |
42
+ | **LLM Wiki** (`memory-bank/`) | A [Karpathy-pattern](https://gist.github.com/karpathy/442a6bf555914893e9891c11519de94f) knowledge base the agent curates as it works — citation discipline, staleness/size budgets, ingest/query/lint operations. | ✅ v0.1 (structure) |
43
+ | **Cross-app contracts** (`openspec/`) | A prescriptive contract layer + propose/apply/archive lifecycle that fights contract drift across repos. | ✅ v0.3 |
44
+ | **Agents + enforcement** | Boundary-enforced per-repo agents, a warm-until-warm Stop hook, a read-only cross-app reviewer. | ✅ v0.2 |
45
+
46
+ The point isn't any single pillar — it's the **integrated discipline** where they
47
+ reinforce each other.
48
+
49
+ ## Topology-aware by design
50
+
51
+ `agentspace init` asks your **workspace shape** and only emits artifacts that shape
52
+ warrants. A single repo, four peer microservices, a library + consumers, and a
53
+ one-product/backend+clients workspace all get *different* output:
54
+
55
+ - A **one-product** workspace gets cross-app contract scaffolding and a
56
+ product-scoped wiki.
57
+ - A **single repo** or set of **unrelated repos** does **not** get a cross-app
58
+ contract layer, a cross-app reviewer, or a blocking hook — because none of that
59
+ applies.
60
+
61
+ You never get a pile of cork-shaped scaffolding that doesn't fit your project.
62
+
63
+ ## What `init` generates today (v0.3)
64
+
65
+ - `manifest.yaml` + a resilient `clone-repos.sh`
66
+ - a `.gitignore` (sub-repos are independent git repos, ignored by the workspace)
67
+ - a root `CLAUDE.md` router + `README.md`
68
+ - a shape-aware `memory-bank/` wiki: numbered priority folders, a conventions
69
+ README, and seeded overview/contract stubs appropriate to your shape
70
+ - (enforcement pillar, opt-in) a `.claude/` pack: per-repo boundary-enforced
71
+ agents, `/ingest` `/query` `/lint` commands, a warm-until-warm Stop hook, and
72
+ a cross-app reviewer (contract-linked shapes).
73
+ - (contracts pillar, opt-in) an `openspec/` cross-repo contract layer — a
74
+ shape-aware `project.md` + `specs/`/`changes/`; the `/opsx:*` commands come
75
+ from the external `openspec` CLI (`openspec update`).
76
+
77
+ ## Quick start
78
+
79
+ ```bash
80
+ npx @shawaze/agentspace init
81
+ # answer: workspace name → shape → repos (name, remote, stack, role) → pillars
82
+ ./clone-repos.sh # pull any sub-repos that aren't on disk yet
83
+ npx @shawaze/agentspace doctor # check workspace health (size budgets, staleness, manifest)
84
+ ```
85
+
86
+ ## Roadmap
87
+
88
+ - More tool adapters (Cursor, Windsurf, …) via the same intent seam.
89
+
90
+ ## Contributing
91
+
92
+ See [CONTRIBUTING.md](./CONTRIBUTING.md). The per-stack agent library is designed
93
+ so adding support for a new stack is a single markdown file — issues and PRs welcome.
94
+
95
+ ## Development
96
+
97
+ ```bash
98
+ npm install
99
+ npm test # vitest
100
+ npm run typecheck
101
+ npm run build # tsup → dist/cli.js
102
+ ```
103
+
104
+ ## License
105
+
106
+ [MIT](./LICENSE) © Shawaze Ahmer
@@ -0,0 +1,136 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * agentspace Stop hook — keeps the memory bank current on cross-repo work.
4
+ * Dep-free (runs with bare node). Reads .claude/agentspace-hook.json for config.
5
+ * Pure helpers are exported when required (for tests); the file runs when executed.
6
+ */
7
+ 'use strict';
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+
12
+ const MUTATING_TOOLS = new Set(['Edit', 'Write', 'MultiEdit', 'NotebookEdit']);
13
+ const SEED_PAGES = new Set(['README.md', 'index.md', 'log.md', 'projectOverview.md', 'crossAppContracts.md']);
14
+
15
+ /** Pure decision: allow | warn | block. */
16
+ function decideStop({ mode, warm, crossAppMutation, memoryBankUpdated }) {
17
+ if (!crossAppMutation || memoryBankUpdated) return 'allow';
18
+ if (mode === 'warn') return 'warn';
19
+ if (mode === 'block') return 'block';
20
+ return warm ? 'block' : 'warn'; // auto
21
+ }
22
+
23
+ /** Pure: warm when pages OR sessions cross their thresholds. */
24
+ function isWarm({ pages, sessions, warmPages, warmSessions }) {
25
+ return pages > warmPages || sessions >= warmSessions;
26
+ }
27
+
28
+ /** Count real (non-seed) memory-bank .md pages, recursively. */
29
+ function countRealPages(mbDir) {
30
+ let count = 0;
31
+ function walk(dir) {
32
+ let entries = [];
33
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
34
+ for (const e of entries) {
35
+ const full = path.join(dir, e.name);
36
+ if (e.isDirectory()) walk(full);
37
+ else if (e.name.endsWith('.md') && !SEED_PAGES.has(e.name)) count++;
38
+ }
39
+ }
40
+ walk(mbDir);
41
+ return count;
42
+ }
43
+
44
+ function readState(stateFile) {
45
+ try { return JSON.parse(fs.readFileSync(stateFile, 'utf8')); } catch { return { sessions: 0 }; }
46
+ }
47
+
48
+ function writeState(stateFile, state) {
49
+ try {
50
+ fs.mkdirSync(path.dirname(stateFile), { recursive: true });
51
+ fs.writeFileSync(stateFile, JSON.stringify(state));
52
+ } catch { /* best effort */ }
53
+ }
54
+
55
+ function main() {
56
+ let input = {};
57
+ try { input = JSON.parse(fs.readFileSync(0, 'utf8') || '{}'); } catch { process.exit(0); }
58
+ if (input.stop_hook_active) process.exit(0);
59
+
60
+ const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
61
+ const mbDir = path.join(projectDir, 'memory-bank');
62
+ if (!fs.existsSync(mbDir)) process.exit(0);
63
+
64
+ const configFile = path.join(projectDir, '.claude', 'agentspace-hook.json');
65
+ let cfg;
66
+ try { cfg = JSON.parse(fs.readFileSync(configFile, 'utf8')); } catch { process.exit(0); }
67
+ const subRepos = Array.isArray(cfg.subRepos) ? cfg.subRepos : [];
68
+ const mode = (cfg.mode === 'warn' || cfg.mode === 'block') ? cfg.mode : 'auto';
69
+ const warmPages = typeof cfg.warmPages === 'number' ? cfg.warmPages : 5;
70
+ const warmSessions = typeof cfg.warmSessions === 'number' ? cfg.warmSessions : 10;
71
+
72
+ // Count this session (every Stop on a configured workspace).
73
+ const stateFile = path.join(projectDir, '.agentspace', 'state.json');
74
+ const state = readState(stateFile);
75
+ state.sessions = (state.sessions || 0) + 1;
76
+ writeState(stateFile, state);
77
+
78
+ // Inspect the transcript for mutating tool uses.
79
+ let mutationCount = 0;
80
+ const touched = new Set();
81
+ let memoryBankUpdated = false;
82
+ const tp = input.transcript_path;
83
+ let transcriptLines = [];
84
+ try {
85
+ if (tp && fs.existsSync(tp)) transcriptLines = fs.readFileSync(tp, 'utf8').split('\n');
86
+ } catch { /* best effort — treat as no transcript, fail open */ }
87
+ if (transcriptLines.length) {
88
+ for (const line of transcriptLines) {
89
+ if (!line.trim()) continue;
90
+ let evt; try { evt = JSON.parse(line); } catch { continue; }
91
+ const content = evt && evt.message && evt.message.content;
92
+ if (!Array.isArray(content)) continue;
93
+ for (const block of content) {
94
+ if (!block || block.type !== 'tool_use' || !MUTATING_TOOLS.has(block.name)) continue;
95
+ mutationCount++;
96
+ const raw = (block.input && (block.input.file_path || block.input.notebook_path)) || '';
97
+ const rel = raw.startsWith(projectDir) ? raw.slice(projectDir.length + 1) : raw;
98
+ if (rel.startsWith('memory-bank/')) memoryBankUpdated = true;
99
+ for (const sub of subRepos) { if (rel.startsWith(sub + '/')) { touched.add(sub); break; } }
100
+ }
101
+ }
102
+ }
103
+
104
+ const crossAppMutation = mutationCount > 0 && touched.size >= 2;
105
+ const warm = isWarm({
106
+ pages: countRealPages(mbDir),
107
+ sessions: state.sessions,
108
+ warmPages,
109
+ warmSessions,
110
+ });
111
+ const decision = decideStop({ mode, warm, crossAppMutation, memoryBankUpdated });
112
+
113
+ if (decision === 'allow') process.exit(0);
114
+
115
+ const list = [...touched].sort().join(', ');
116
+ const reason = [
117
+ `Cross-repo activity detected (${list}) — update the memory bank before ending.`,
118
+ '1. Refresh memory-bank/01-active/currentWork.md (date + status).',
119
+ '2. Append one line to memory-bank/log.md: `## [YYYY-MM-DD] <action> | <slug>`.',
120
+ '3. If a cross-repo contract was touched, record it in memory-bank/00-core/crossAppContracts.md (cite `file:line`).',
121
+ ].join('\n');
122
+
123
+ if (decision === 'block') {
124
+ process.stdout.write(JSON.stringify({ decision: 'block', reason }));
125
+ } else {
126
+ // warn: surface a note but allow the stop.
127
+ process.stdout.write(JSON.stringify({ systemMessage: reason }));
128
+ }
129
+ process.exit(0);
130
+ }
131
+
132
+ if (require.main === module) {
133
+ try { main(); } catch { process.exit(0); }
134
+ } else {
135
+ module.exports = { decideStop, isWarm, countRealPages, readState, writeState };
136
+ }