@vib795/agent-memory 0.1.14 → 0.2.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/HOWTO.md +15 -0
- package/README.md +62 -2
- package/package.json +1 -1
- package/src/cli.js +162 -2
- package/src/config.js +69 -2
package/HOWTO.md
CHANGED
|
@@ -170,6 +170,21 @@ You can also point it at something specific:
|
|
|
170
170
|
**Don't bother remembering** anything the code already says. If someone can find it by
|
|
171
171
|
opening a file, it does not need a note. Notes are for what lives in people's heads.
|
|
172
172
|
|
|
173
|
+
**You will also see it happen without you asking.** When a decision settles or a
|
|
174
|
+
constraint turns up mid-conversation, the assistant writes it down and tells you in
|
|
175
|
+
one line:
|
|
176
|
+
|
|
177
|
+
```
|
|
178
|
+
captured 2 notes [decision, constraint]
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Then it carries on with whatever you were actually asking about. This is on purpose:
|
|
182
|
+
the fact worth keeping is usually the one nobody stops to write down. It costs you
|
|
183
|
+
nothing — your allowance is charged per message you send, not per thing the assistant
|
|
184
|
+
does while answering — and you can see everything it captured with `/recall` or
|
|
185
|
+
`agent-memory tree`. If it ever writes down something you did not want, the notes are
|
|
186
|
+
plain markdown files you can edit or delete.
|
|
187
|
+
|
|
173
188
|
### `/handoff` — for moving work between windows
|
|
174
189
|
|
|
175
190
|
This one is not a chat summary. Summaries read nicely and still leave the next
|
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ Three user-level Agent Skills over one local store:
|
|
|
16
16
|
| Skill | What it does |
|
|
17
17
|
|---|---|
|
|
18
18
|
| `/handoff` | Writes a portable working-state file so another window can pick up this thread |
|
|
19
|
-
| `/remember` | Captures durable knowledge into a cross-repo graph |
|
|
19
|
+
| `/remember` | Captures durable knowledge into a cross-repo graph — and fires on its own when a juncture passes |
|
|
20
20
|
| `/recall` | Answers from that graph before deriving anything again |
|
|
21
21
|
|
|
22
22
|
**New here? Read the [HOW-TO](HOWTO.md)** — install, first five minutes, and the
|
|
@@ -78,6 +78,14 @@ recursive CTE *is* the graph engine, and `UNION` plus a depth bound is what keep
|
|
|
78
78
|
- **Staleness is visible at the moment of use.** Every note records the repo HEAD it
|
|
79
79
|
was captured at. On recall, `get` prints `captured 47 commits ago — verify before
|
|
80
80
|
trusting`. A note that quietly rots is worse than no note.
|
|
81
|
+
- **An empty graph says so.** Staleness cannot tell you a repo has nothing in it —
|
|
82
|
+
a repo nobody captured in has no stale notes either, so it reads as fully covered.
|
|
83
|
+
`tree` and `doctor` print `47 commits since anything was captured here` instead.
|
|
84
|
+
Silence has to mean covered, never empty.
|
|
85
|
+
- **Capture does not wait to be asked.** The agent invokes `/remember` itself when a
|
|
86
|
+
decision settles, a constraint surfaces or a root cause is found, and says so in one
|
|
87
|
+
line. Judged by the model in the turn where the context still exists — never on a
|
|
88
|
+
timer or a tool count, because volume is what makes a graph useless.
|
|
81
89
|
- **Redaction is fail-closed.** Keys, tokens, connection strings, private keys,
|
|
82
90
|
session cookies, internal hosts and foreign email addresses are replaced before any
|
|
83
91
|
byte reaches disk — not to a note, not to a temp file, not to the index.
|
|
@@ -86,6 +94,44 @@ recursive CTE *is* the graph engine, and `UNION` plus a depth bound is what keep
|
|
|
86
94
|
- **Constraints are never dropped.** At any cap, in either tier. A constraint is what
|
|
87
95
|
stops an agent burning a retry loop on an approach that was never going to ship.
|
|
88
96
|
|
|
97
|
+
## Engagements
|
|
98
|
+
|
|
99
|
+
If you work for more than one client on one machine, knowledge from one of them is
|
|
100
|
+
not the next one's to see. An engagement is a **separate store**, and switching is a
|
|
101
|
+
path change rather than a filter:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
agent-memory engagement # which one is active, and why
|
|
105
|
+
agent-memory engagement list # all of them, with note counts
|
|
106
|
+
agent-memory engagement use acme # the fallback for every window
|
|
107
|
+
agent-memory engagement purge acme --yes # delete that client's store entirely
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Pin a client's whole tree instead of remembering to switch. One file at the top of
|
|
111
|
+
it, and every repository beneath is in that engagement:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
echo acme > ~/clients/acme/.agent-memory-engagement
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Resolution order is `AGENT_MEMORY_ENGAGEMENT`, then the nearest marker file walking
|
|
118
|
+
up from where you are, then the machine-wide pointer, then `default`. `doctor` and
|
|
119
|
+
`engagement` both print which one decided it, because the failure worth preventing is
|
|
120
|
+
believing you are in one client's store while writing to another.
|
|
121
|
+
|
|
122
|
+
**The isolation is structural, not a filter.** Each engagement is its own directory,
|
|
123
|
+
its own markdown and its own index, so `search`, `get` and `tree` cannot reach across
|
|
124
|
+
even by exact id — there is no query to forget. Filtering would have to be remembered
|
|
125
|
+
at fourteen separate read paths, and two of them were already missed before this
|
|
126
|
+
existed.
|
|
127
|
+
|
|
128
|
+
That also makes removal something you can evidence rather than assert: `purge` deletes
|
|
129
|
+
a directory and reports the count, and refuses without `--yes` after telling you
|
|
130
|
+
exactly what would go.
|
|
131
|
+
|
|
132
|
+
Everything already captured stays in `default`, at the same path as before. Nothing
|
|
133
|
+
migrates and nothing changes until you create a second engagement.
|
|
134
|
+
|
|
89
135
|
## CLI
|
|
90
136
|
|
|
91
137
|
```
|
|
@@ -336,7 +382,21 @@ Mid-session, when something worth keeping has been established:
|
|
|
336
382
|
```
|
|
337
383
|
|
|
338
384
|
Bare, it selects the durable knowledge itself. With an argument, it writes that and
|
|
339
|
-
nothing else.
|
|
385
|
+
nothing else.
|
|
386
|
+
|
|
387
|
+
You will also see it fire without being asked, when a decision settles, a constraint
|
|
388
|
+
surfaces, a root cause is found or a convention is agreed. It reports one line at the
|
|
389
|
+
end of whatever it was already answering and carries on:
|
|
390
|
+
|
|
391
|
+
```
|
|
392
|
+
captured 2 notes [decision, constraint]
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
That costs nothing — a request is charged per prompt, not per tool call, so capture
|
|
396
|
+
inside a turn you already paid for is free. Only typing `/remember` yourself spends
|
|
397
|
+
one.
|
|
398
|
+
|
|
399
|
+
Either way it makes one terminal call, and `write` reports each id:
|
|
340
400
|
|
|
341
401
|
```
|
|
342
402
|
created auth-service [system]
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vib795/agent-memory",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Durable cross-repo knowledge graph for GitHub Copilot and Claude Code. Markdown source of truth, disposable SQLite index, zero runtime dependencies.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"github-copilot",
|
package/src/cli.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { readFileSync, existsSync } from 'node:fs';
|
|
2
|
+
import { readFileSync, existsSync, mkdirSync, rmSync, readdirSync } from 'node:fs';
|
|
3
3
|
import { execFileSync } from 'node:child_process';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
loadConfig, saveConfig, paths, NOTE_TYPES, ENGAGEMENT, ENGAGEMENT_RE, ENGAGEMENT_POINTER,
|
|
6
|
+
ENGAGEMENT_MARKER, DEFAULT_ENGAGEMENT, STORE_BASE, engagementRoot,
|
|
7
|
+
} from './config.js';
|
|
5
8
|
import { ensureStore, writeNote, normalizeTitle, listNotes } from './store.js';
|
|
6
9
|
import {
|
|
7
10
|
openDb, reindex, searchNodes, getNodeRow, markAccessed, nodeCount, hasFts,
|
|
@@ -13,6 +16,7 @@ import { staleness, currentRepo, reviewCandidates, captureGap } from './stalenes
|
|
|
13
16
|
import { setup as runSetup, unlinkSkills, danglingSkillLinks, SKILLS } from './setup.js';
|
|
14
17
|
import { detectTargets, installableTargets } from './targets.js';
|
|
15
18
|
import { join } from 'node:path';
|
|
19
|
+
import { atomicWrite } from './atomic.js';
|
|
16
20
|
|
|
17
21
|
/**
|
|
18
22
|
* One process, one answer.
|
|
@@ -82,8 +86,11 @@ const USAGE = `agent-memory — durable cross-repo knowledge for coding agents
|
|
|
82
86
|
[--source <name>] [--repo <name>]
|
|
83
87
|
compact dedup, decay, reindex, regenerate
|
|
84
88
|
doctor preflight and health report
|
|
89
|
+
engagement [show|list|use <name>] which client store this window writes to
|
|
90
|
+
[purge <name> --yes] delete one engagement's store entirely
|
|
85
91
|
|
|
86
92
|
Add --json to any command for machine-readable output.
|
|
93
|
+
Engagement: ${ENGAGEMENT.name} (${ENGAGEMENT.source})
|
|
87
94
|
Store: ${paths.root}`;
|
|
88
95
|
|
|
89
96
|
// --- commands ---------------------------------------------------------------
|
|
@@ -422,6 +429,10 @@ function cmdDoctor() {
|
|
|
422
429
|
const checks = [];
|
|
423
430
|
const add = (name, ok, detail) => checks.push({ name, ok, detail });
|
|
424
431
|
|
|
432
|
+
// First, because everything below it is a fact about one engagement's store and
|
|
433
|
+
// reading it against the wrong client is the mistake this is here to prevent.
|
|
434
|
+
add('engagement', true, `${ENGAGEMENT.name} (${ENGAGEMENT.source})`);
|
|
435
|
+
|
|
425
436
|
add('node version', nodeVersionOk(), `${process.versions.node} (need >= ${MIN_NODE.join('.')})`);
|
|
426
437
|
if (!nodeVersionOk()) {
|
|
427
438
|
return {
|
|
@@ -550,6 +561,154 @@ function cmdDoctor() {
|
|
|
550
561
|
|
|
551
562
|
// --- dispatch ---------------------------------------------------------------
|
|
552
563
|
|
|
564
|
+
/**
|
|
565
|
+
* Show, switch and purge engagements.
|
|
566
|
+
*
|
|
567
|
+
* Each engagement is a whole store on disk, so switching is a path change and
|
|
568
|
+
* purging is a directory removal. Nothing here filters anything: the isolation is
|
|
569
|
+
* structural, and a subcommand that got it wrong could not leak across the boundary
|
|
570
|
+
* even if it tried.
|
|
571
|
+
*/
|
|
572
|
+
function cmdEngagement(opts) {
|
|
573
|
+
const sub = opts._[0] ?? 'show';
|
|
574
|
+
|
|
575
|
+
if (sub === 'show') {
|
|
576
|
+
return {
|
|
577
|
+
ok: true,
|
|
578
|
+
engagement: ENGAGEMENT.name,
|
|
579
|
+
source: ENGAGEMENT.source,
|
|
580
|
+
store: paths.root,
|
|
581
|
+
text: [
|
|
582
|
+
`engagement: ${ENGAGEMENT.name}`,
|
|
583
|
+
`decided by: ${ENGAGEMENT.source}`,
|
|
584
|
+
`store: ${paths.root}`,
|
|
585
|
+
].join('\n'),
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
if (sub === 'list') {
|
|
590
|
+
const found = listEngagements();
|
|
591
|
+
return {
|
|
592
|
+
ok: true,
|
|
593
|
+
engagements: found,
|
|
594
|
+
text: found
|
|
595
|
+
.map((e) => `${e.name === ENGAGEMENT.name ? '*' : ' '} ${e.name.padEnd(24)} ${e.notes} notes`)
|
|
596
|
+
.join('\n'),
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
if (sub === 'use') {
|
|
601
|
+
const name = opts._[1];
|
|
602
|
+
if (!name || !ENGAGEMENT_RE.test(name)) return badEngagementName(name);
|
|
603
|
+
mkdirSync(STORE_BASE, { recursive: true });
|
|
604
|
+
atomicWrite(ENGAGEMENT_POINTER, `${name}\n`);
|
|
605
|
+
return {
|
|
606
|
+
ok: true,
|
|
607
|
+
engagement: name,
|
|
608
|
+
store: engagementRoot(name),
|
|
609
|
+
text: [
|
|
610
|
+
`engagement: ${name}`,
|
|
611
|
+
`store: ${engagementRoot(name)}`,
|
|
612
|
+
'',
|
|
613
|
+
'This is the fallback for every window on this machine. To pin one client tree',
|
|
614
|
+
`instead, put the name in a ${ENGAGEMENT_MARKER} file at the top of it.`,
|
|
615
|
+
].join('\n'),
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
if (sub === 'purge') {
|
|
620
|
+
const name = opts._[1];
|
|
621
|
+
if (!name || !ENGAGEMENT_RE.test(name)) return badEngagementName(name);
|
|
622
|
+
if (name === DEFAULT_ENGAGEMENT) {
|
|
623
|
+
return {
|
|
624
|
+
ok: false,
|
|
625
|
+
error: 'the default engagement cannot be purged',
|
|
626
|
+
text: 'The default engagement cannot be purged. Delete individual notes instead.',
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
const dir = engagementRoot(name);
|
|
630
|
+
if (!existsSync(dir)) {
|
|
631
|
+
return { ok: false, error: `no engagement named ${name}`, text: `No engagement named ${name}.` };
|
|
632
|
+
}
|
|
633
|
+
const notes = countNotes(dir);
|
|
634
|
+
if (!opts.yes) {
|
|
635
|
+
// Deleting a client's captured knowledge is not something to do because a name
|
|
636
|
+
// was typed. Say exactly what goes, and make the caller ask again.
|
|
637
|
+
return {
|
|
638
|
+
ok: false,
|
|
639
|
+
error: 'purge requires --yes',
|
|
640
|
+
engagement: name,
|
|
641
|
+
notes,
|
|
642
|
+
text: [
|
|
643
|
+
`This deletes ${notes} note${notes === 1 ? '' : 's'} and the whole store at:`,
|
|
644
|
+
` ${dir}`,
|
|
645
|
+
'',
|
|
646
|
+
'Nothing else is touched, and this cannot be undone. To go ahead:',
|
|
647
|
+
` agent-memory engagement purge ${name} --yes`,
|
|
648
|
+
].join('\n'),
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
rmSync(dir, { recursive: true, force: true });
|
|
652
|
+
return {
|
|
653
|
+
ok: true,
|
|
654
|
+
engagement: name,
|
|
655
|
+
notes,
|
|
656
|
+
removed: dir,
|
|
657
|
+
// Stated as a fact someone can check, because "we deleted it" is a claim that
|
|
658
|
+
// eventually has to be evidenced rather than asserted.
|
|
659
|
+
text: [
|
|
660
|
+
`Purged engagement ${name}: ${notes} note${notes === 1 ? '' : 's'} removed.`,
|
|
661
|
+
`${dir} no longer exists.`,
|
|
662
|
+
].join('\n'),
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
return {
|
|
667
|
+
ok: false,
|
|
668
|
+
error: `unknown subcommand ${sub}`,
|
|
669
|
+
text: 'Usage: agent-memory engagement [show|list|use <name>|purge <name> --yes]',
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
function badEngagementName(name) {
|
|
674
|
+
return {
|
|
675
|
+
ok: false,
|
|
676
|
+
error: 'invalid engagement name',
|
|
677
|
+
text: [
|
|
678
|
+
name ? `"${name}" is not a usable engagement name.` : 'An engagement name is required.',
|
|
679
|
+
'Names become directory names: lower case, digits and hyphens, up to 64 characters.',
|
|
680
|
+
].join('\n'),
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
/** Markdown files under a store, counted without opening its index. */
|
|
685
|
+
function countNotes(root) {
|
|
686
|
+
const dir = join(root, 'notes');
|
|
687
|
+
if (!existsSync(dir)) return 0;
|
|
688
|
+
let n = 0;
|
|
689
|
+
const walk = (d) => {
|
|
690
|
+
for (const entry of readdirSync(d, { withFileTypes: true })) {
|
|
691
|
+
if (entry.isDirectory()) walk(join(d, entry.name));
|
|
692
|
+
else if (entry.name.endsWith('.md')) n += 1;
|
|
693
|
+
}
|
|
694
|
+
};
|
|
695
|
+
walk(dir);
|
|
696
|
+
return n;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
function listEngagements() {
|
|
700
|
+
const out = [{ name: DEFAULT_ENGAGEMENT, notes: countNotes(engagementRoot(DEFAULT_ENGAGEMENT)) }];
|
|
701
|
+
const dir = join(STORE_BASE, 'engagements');
|
|
702
|
+
if (existsSync(dir)) {
|
|
703
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
704
|
+
if (entry.isDirectory()) out.push({ name: entry.name, notes: countNotes(join(dir, entry.name)) });
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
// The active one may not exist on disk yet, and omitting it would read as absent.
|
|
708
|
+
if (!out.some((e) => e.name === ENGAGEMENT.name)) out.push({ name: ENGAGEMENT.name, notes: 0 });
|
|
709
|
+
return out.sort((a, b) => a.name.localeCompare(b.name));
|
|
710
|
+
}
|
|
711
|
+
|
|
553
712
|
const COMMANDS = {
|
|
554
713
|
setup: cmdSetup,
|
|
555
714
|
uninstall: cmdUninstall,
|
|
@@ -561,6 +720,7 @@ const COMMANDS = {
|
|
|
561
720
|
write: cmdWrite,
|
|
562
721
|
compact: cmdCompact,
|
|
563
722
|
doctor: cmdDoctor,
|
|
723
|
+
engagement: cmdEngagement,
|
|
564
724
|
};
|
|
565
725
|
|
|
566
726
|
function main(argv) {
|
package/src/config.js
CHANGED
|
@@ -1,14 +1,81 @@
|
|
|
1
1
|
import { readFileSync, existsSync } from 'node:fs';
|
|
2
|
-
import { join } from 'node:path';
|
|
2
|
+
import { join, dirname, parse as parsePath } from 'node:path';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { atomicWrite } from './atomic.js';
|
|
5
5
|
|
|
6
6
|
// The store lives outside every repository on purpose. That is what makes a note
|
|
7
7
|
// written in one window readable from a window opened on a different project.
|
|
8
|
-
export const
|
|
8
|
+
export const STORE_BASE =
|
|
9
9
|
process.env.AGENT_MEMORY_HOME ||
|
|
10
10
|
join(process.env.USERPROFILE || homedir(), '.agents', 'memory');
|
|
11
11
|
|
|
12
|
+
export const DEFAULT_ENGAGEMENT = 'default';
|
|
13
|
+
|
|
14
|
+
/** Where the pointer to the active engagement lives, outside every engagement. */
|
|
15
|
+
export const ENGAGEMENT_POINTER = join(STORE_BASE, '.engagement');
|
|
16
|
+
|
|
17
|
+
/** Engagement names become directory names, so they have to be safe as one. */
|
|
18
|
+
export const ENGAGEMENT_RE = /^[a-z0-9][a-z0-9-]{0,63}$/;
|
|
19
|
+
|
|
20
|
+
/** The file a client tree carries to pin itself to an engagement. */
|
|
21
|
+
export const ENGAGEMENT_MARKER = '.agent-memory-engagement';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Which engagement this process is working in, and how that was decided.
|
|
25
|
+
*
|
|
26
|
+
* A consultant runs several clients through one machine, and knowledge from one is
|
|
27
|
+
* not the next one's to see. The boundary is a separate store per engagement rather
|
|
28
|
+
* than a column to filter on, because filtering has to be remembered at every read
|
|
29
|
+
* and there are fourteen of them; two were already missed. A separate directory
|
|
30
|
+
* cannot be forgotten, and purging one is a directory removal that can be shown to
|
|
31
|
+
* have happened.
|
|
32
|
+
*
|
|
33
|
+
* The marker file is what makes this survive real use: drop one at the root of a
|
|
34
|
+
* client's tree and every repository beneath it is in that engagement, with nothing
|
|
35
|
+
* to remember when switching windows.
|
|
36
|
+
*/
|
|
37
|
+
export function resolveEngagement(cwd = process.cwd()) {
|
|
38
|
+
const env = process.env.AGENT_MEMORY_ENGAGEMENT?.trim();
|
|
39
|
+
if (env) return { name: env, source: 'AGENT_MEMORY_ENGAGEMENT' };
|
|
40
|
+
|
|
41
|
+
let dir = cwd;
|
|
42
|
+
for (;;) {
|
|
43
|
+
const marker = join(dir, ENGAGEMENT_MARKER);
|
|
44
|
+
if (existsSync(marker)) {
|
|
45
|
+
try {
|
|
46
|
+
const name = readFileSync(marker, 'utf8').trim().split('\n')[0].trim();
|
|
47
|
+
if (name) return { name, source: marker };
|
|
48
|
+
} catch {
|
|
49
|
+
// An unreadable marker is not a reason to fall back silently to another
|
|
50
|
+
// client's store. Treat it as unset and let the pointer or default decide.
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const parent = dirname(dir);
|
|
54
|
+
if (parent === dir || dir === parsePath(dir).root) break;
|
|
55
|
+
dir = parent;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (existsSync(ENGAGEMENT_POINTER)) {
|
|
59
|
+
try {
|
|
60
|
+
const name = readFileSync(ENGAGEMENT_POINTER, 'utf8').trim();
|
|
61
|
+
if (name) return { name, source: ENGAGEMENT_POINTER };
|
|
62
|
+
} catch {
|
|
63
|
+
/* fall through to the default */
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return { name: DEFAULT_ENGAGEMENT, source: 'default' };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** The store directory for an engagement. The default one is the base itself. */
|
|
70
|
+
export function engagementRoot(name) {
|
|
71
|
+
return name === DEFAULT_ENGAGEMENT ? STORE_BASE : join(STORE_BASE, 'engagements', name);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export const ENGAGEMENT = resolveEngagement();
|
|
75
|
+
|
|
76
|
+
// Resolved once per process, like everything else here. One process, one answer.
|
|
77
|
+
export const STORE_ROOT = engagementRoot(ENGAGEMENT.name);
|
|
78
|
+
|
|
12
79
|
export const NOTE_TYPES = ['system', 'decision', 'convention', 'constraint'];
|
|
13
80
|
|
|
14
81
|
export const paths = {
|