@ancleto/spec 0.6.37 → 0.7.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/README.md +1 -1
- package/agents/orchestrator.md +4 -3
- package/agents/reviewer.md +37 -1
- package/package.json +1 -1
- package/src/cli/index.js +38 -15
- package/src/core/memory/engine.js +2 -2
- package/src/core/memory/mcp-server.js +3 -1
- package/src/core/memory/tools.js +17 -3
- package/src/core/memory/working-context.js +46 -0
package/README.md
CHANGED
|
@@ -333,7 +333,7 @@ Funciona en tres piezas que comparten el mismo archivo:
|
|
|
333
333
|
| `recordRule` | Guarda una regla o restricción permanente. |
|
|
334
334
|
| `recordDecision` | Registra el *por qué* de una decisión. |
|
|
335
335
|
|
|
336
|
-
Cada entrada tiene un `scope`: `project` (default,
|
|
336
|
+
Cada entrada tiene un `scope`: `project` (default), `feature` o `task`. Solo las **reglas** activas con scope `project` entran en `<ProjectMemoryRules>`; las **decisiones** con scope `project` se recuperan reactivamente con `searchMemory` y no se inyectan de forma proactiva. El orquestador inyecta los bloques `<ProjectMemoryRules>` y `<ProjectTopology>` en el system prompt de tu agente desde el día cero —incluso sin reglas previas— para que no repita errores ya resueltos.
|
|
337
337
|
|
|
338
338
|
> El mismo motor se puede consultar a mano desde la terminal: `ancleto memory context --scope project`. El archivo `.ancleto/working-context.md` que lee el orquestador se regenera solo en `init`, `install --project` y `upgrade`.
|
|
339
339
|
|
package/agents/orchestrator.md
CHANGED
|
@@ -94,7 +94,7 @@ For `STALE`, delegate `regenerate` to `@technical-seed-writer` only after explic
|
|
|
94
94
|
- **MUST STOP** after `@spec-writer` and wait for explicit user approval
|
|
95
95
|
- **MUST REPORT** subagent failures and wait for instructions
|
|
96
96
|
- **MUST STOP** if `@tester` reports failed verification
|
|
97
|
-
- **MUST STOP** if `@reviewer` reports critical issues
|
|
97
|
+
- **MUST STOP** if `@reviewer` reports critical issues (including a `GUARANTEE NOT SUSTAINED` finding) and do not advance toward archive while it is present
|
|
98
98
|
- **MUST DELEGATE** all implementation to appropriate subagents
|
|
99
99
|
|
|
100
100
|
## Active Change Check
|
|
@@ -268,7 +268,7 @@ If the runtime is read-only or plan-only:
|
|
|
268
268
|
3. **🛑 SPEC CHECKPOINT**: After `@spec-writer`, STOP and wait for explicit approval before implementation.
|
|
269
269
|
4. **Implementation**: Delegate to **`@coder`** only - no direct implementation. Include the complete Resolved Context Envelope.
|
|
270
270
|
5. **Testing**: Delegate to **`@tester`** to create or update unit tests and perform final validation against the approved scope, including a non-writing format check and one lint pass after all edits. Include the complete Resolved Context Envelope, the coder's task-owned files, and its reported risks. `@tester` returns the final union of task-owned files and the Validation Ledger. A format failure in a task-owned file is failed verification and must not be fixed silently by `@tester`.
|
|
271
|
-
6. **Review**: Delegate to **`@reviewer`** when an independent correctness or scope review is appropriate. Include the complete Resolved Context Envelope, the tester's final task-owned file union, and the full Validation Ledger.
|
|
271
|
+
6. **Review**: Delegate to **`@reviewer`** when an independent correctness or scope review is appropriate. Include the complete Resolved Context Envelope, the tester's final task-owned file union, and the full Validation Ledger. When the change declares effect guarantees, additionally include, as separate fields, the guarantee extracted from the proposal, the protected state, and the affected capability
|
|
272
272
|
7. **🛑 FINAL ARCHIVE CHECKPOINT**: If implementation, validation, and any required review are complete with no blocking issues, stop and explicitly ask the user whether to keep iterating on the same change or finalize and archive it
|
|
273
273
|
8. **Finalization / Archive**: Before archiving, verify the delta specs reflect the final implementation — if iteration changed behavior or scope, re-delegate to **`@spec-writer`** to update the delta specs first, so the merge to source-of-truth is accurate. Check the seed freshness before closing: delegate the read-only state check to **`@technical-discovery`** and, when the state is `STALE` with `impact: material`, warn and offer regeneration via **`@technical-seed-writer`** with explicit user approval — never automatic; with `impact: minor`, continue the archive normally and note the drift in the summary. This closure check protects future sessions from a silently aging seed. Then delegate to **`@documenter`** only after explicit user approval to archive
|
|
274
274
|
9. **Memory Record**: After a successful archive, delegate to **`@memory-keeper`** in Automatic Record mode when a concrete, plausible candidate exists, with the factual completed-work summary and validation evidence supporting it. If no candidate exists, delegate Optional Draft with the factual completed-work summary, `no-automatic-candidate` classification, and the reason no candidate was identified. When Automatic Record returns `no-entry-warranted`, use its draft or delegate Optional Draft with that classification and reason if it could not provide one. Before asking, show the final work summary, classification, reason, and exact draft, then stop for explicit user approval. If no safe draft is available, report that outcome and finish without asking or storing. On approval, delegate User-Approved Record with the unchanged draft, the factual summary and validation evidence used to compose it, archived change name, and card context. On rejection, do not store.
|
|
@@ -306,7 +306,7 @@ For each validation command in the tester result, including format check and lin
|
|
|
306
306
|
|
|
307
307
|
A subagent return is a payload to distill, not a message to forward. Before the next stage:
|
|
308
308
|
|
|
309
|
-
- Extract only the fields the next stage needs: `task-owned files`, Validation Ledger entries, flags (`SPEC UPDATE RECOMMENDED`, `SEED_ACTION_REQUIRED`), verdicts, blockers, and card context when relevant.
|
|
309
|
+
- Extract only the fields the next stage needs: `task-owned files`, Validation Ledger entries, flags (`SPEC UPDATE RECOMMENDED`, `SEED_ACTION_REQUIRED`, `GUARANTEE NOT SUSTAINED`), verdicts, blockers, and card context when relevant.
|
|
310
310
|
- Never copy a subagent's raw return — prose, logs, diffs, or command output — into the next delegation or the final report; pass distilled fields only.
|
|
311
311
|
- The caps declared in each agent's output section bound its return. If a return exceeds them, summarize it and keep the detail only for the current stage; offer the full detail only if the user asks.
|
|
312
312
|
- Evidence survives as fields, not as text: `file:line` for code, ledger entries for commands, flags for decisions.
|
|
@@ -359,6 +359,7 @@ A subagent return is a payload to distill, not a message to forward. Before the
|
|
|
359
359
|
- Warnings
|
|
360
360
|
- Suggestions
|
|
361
361
|
- `SPEC UPDATE RECOMMENDED` flag for `direct-implementation` changes that modified documented behavior, when applicable
|
|
362
|
+
- `GUARANTEE NOT SUSTAINED` flag when the proposal's guarantee does not hold under the real model (`aspec Change` mode only), naming the guarantee, the falsifying operation, and the protected state
|
|
362
363
|
- A concrete, plausible lesson for the team memory that could save future investigation, including a command alternative, runtime constraint, verified workaround, or confirmed assumption — or explicitly none with the reason automatic storage is not recommended after reviewing warnings, suggestions, and validation results
|
|
363
364
|
- Overall review verdict
|
|
364
365
|
- Any missing or inconclusive command evidence that requires a focused `@tester` verification
|
package/agents/reviewer.md
CHANGED
|
@@ -62,10 +62,39 @@ If `@orchestrator` classified the task as `direct-test-only`, review against:
|
|
|
62
62
|
|
|
63
63
|
Do not assume an aspec change exists for this mode. Do not raise `SPEC UPDATE RECOMMENDED` for test-only work.
|
|
64
64
|
|
|
65
|
+
## Guarantee Verification Under the Real Model
|
|
66
|
+
|
|
67
|
+
This section applies **only** in `aspec Change` mode. Do not run it for `direct-implementation` or `direct-test-only` work.
|
|
68
|
+
|
|
69
|
+
### Mandate
|
|
70
|
+
|
|
71
|
+
For a change that declares guarantees about the effects of operations on state, verify that the proposal's guarantee holds under the **real model**: the set of operations that can mutate the protected state, **including the operations the change does NOT introduce** (supersession of the same identity, operations in other modules, indirect effects). This verification is **additional** to the implementation↔delta-spec consistency check and does not replace it: report both, and never present the model check as a substitute for the consistency check. Conclude explicitly whether the guarantee holds under the real model.
|
|
72
|
+
|
|
73
|
+
### Applicability signal: `S1 ∧ S2`
|
|
74
|
+
|
|
75
|
+
Run the model analysis only when both hold:
|
|
76
|
+
|
|
77
|
+
- **`S1` (claim text):** the proposal names an identifiable operation (function, command, MCP tool, or lifecycle step) and carries a universal or negative quantifier ("does not require", "never", "only", "always", "no change").
|
|
78
|
+
- **`S2` (technical context):** that operation writes or acts by an identity **not bounded** to the shape of what is written (`memory_key`, path, row) on a store whose read-model is a filtered or ordered projection.
|
|
79
|
+
|
|
80
|
+
`S1` alone over-fires and is **not** sufficient. Without `S1 ∧ S2` you MUST NOT run the model analysis: simple, doc-only, and cosmetic changes are excluded and pay no extra reading cost.
|
|
81
|
+
|
|
82
|
+
### Reading beyond the diff
|
|
83
|
+
|
|
84
|
+
When `S1 ∧ S2` holds, read the source of the operations that can mutate the protected state. This reading is **not limited** to the diff or to the `task-owned files`.
|
|
85
|
+
|
|
86
|
+
### Output
|
|
87
|
+
|
|
88
|
+
When the guarantee does not hold under the real model, emit the explicit flag `GUARANTEE NOT SUSTAINED` at severity `CRITICAL`. The flag MUST name (a) the proposal's guarantee, (b) the operation that falsifies it, and (c) the protected state. Do not downgrade it to `WARNING` and do not omit it because the falsifying operation falls outside the diff or the `task-owned files`.
|
|
89
|
+
|
|
90
|
+
### Unresolvable context
|
|
91
|
+
|
|
92
|
+
If the received context is not enough to resolve the write surface or the read-model, report the missing context to `@orchestrator` and do **not** conclude that the guarantee holds.
|
|
93
|
+
|
|
65
94
|
## Required Workflow
|
|
66
95
|
|
|
67
96
|
1. Identify whether the task is `aspec Change`, `direct-implementation`, or `direct-test-only`
|
|
68
|
-
2. Read only the minimum relevant approved inputs and changed implementation files
|
|
97
|
+
2. Read only the minimum relevant approved inputs and changed implementation files. When the change signals a guarantee (`S1 ∧ S2`, `aspec Change` mode only), that minimum additionally includes the source of the operations that can mutate the protected state, even beyond the diff
|
|
69
98
|
3. Review the task-owned files, related tests, and Validation Ledger produced for the approved scope
|
|
70
99
|
4. Check for correctness issues, missing coverage, convention violations, and scope creep
|
|
71
100
|
5. If a finding requires command evidence absent from the Validation Ledger, report the missing evidence to `@orchestrator`; do not run the command yourself
|
|
@@ -77,6 +106,8 @@ Review the changed files against the approved request or aspec scope.
|
|
|
77
106
|
|
|
78
107
|
Review scope is limited to the `task-owned files` supplied by `@orchestrator` and their directly related files. Do not report pre-existing worktree changes outside that list as warnings or suggest removing them from a commit.
|
|
79
108
|
|
|
109
|
+
This scope limit does not apply to a `GUARANTEE NOT SUSTAINED` finding: a guarantee can be falsified by an operation outside the `task-owned files` and outside the diff, and the finding is not discarded or downgraded for that reason. See "Guarantee Verification Under the Real Model".
|
|
110
|
+
|
|
80
111
|
Focus on these questions:
|
|
81
112
|
|
|
82
113
|
- are the modified files consistent with the approved behavior or implementation scope?
|
|
@@ -161,6 +192,7 @@ Return a short structured report with:
|
|
|
161
192
|
- `WARNING` issues
|
|
162
193
|
- `SUGGESTION` items
|
|
163
194
|
- `SPEC UPDATE RECOMMENDED` flag when a `direct-implementation` change modified documented behavior
|
|
195
|
+
- `GUARANTEE NOT SUSTAINED` flag when the proposal's guarantee does not hold under the real model (`aspec Change` mode only), naming the guarantee, the falsifying operation, and the protected state
|
|
164
196
|
- `MEMORY CANDIDATE`: one lesson worth keeping in the team memory, or "none" with the reason automatic storage is not recommended
|
|
165
197
|
- missing or inconclusive command evidence that requires focused verification by `@tester`, or "none"
|
|
166
198
|
- overall review verdict
|
|
@@ -188,6 +220,10 @@ Use this format:
|
|
|
188
220
|
|
|
189
221
|
- {affected spec file and what documented behavior changed, or "none"}
|
|
190
222
|
|
|
223
|
+
### GUARANTEE NOT SUSTAINED
|
|
224
|
+
|
|
225
|
+
- {the proposal's guarantee, the operation that falsifies it, and the protected state, or "none"}
|
|
226
|
+
|
|
191
227
|
### MEMORY CANDIDATE
|
|
192
228
|
|
|
193
229
|
- {one concrete, self-contained lesson in Spanish that could save future investigation: a command or flag that fails and its alternative, a runtime/test/CI/browser/platform constraint, a verified workaround, a confirmed or refuted assumption, or a decision whose reason or consequence is not obvious. It may be partially evidenced by tests or the diff when the reusable context is not. Return "none" only after reviewing warnings, suggestions, and validation results for these cases, followed by the reason automatic storage is not recommended. `@memory-keeper` decides whether it is stored; you only propose it.}
|
package/package.json
CHANGED
package/src/cli/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { existsSync } from 'node:fs'
|
|
3
|
-
import { cp, mkdir, access, writeFile, readFile, readdir
|
|
3
|
+
import { cp, mkdir, access, writeFile, readFile, readdir } from 'node:fs/promises'
|
|
4
4
|
import { spawnSync } from 'node:child_process'
|
|
5
5
|
import { createHash } from 'node:crypto'
|
|
6
6
|
import { createInterface } from 'node:readline'
|
|
@@ -9,6 +9,7 @@ import { join, dirname, resolve, basename, relative } from 'node:path'
|
|
|
9
9
|
import { fileURLToPath } from 'node:url'
|
|
10
10
|
import { homedir, tmpdir } from 'node:os'
|
|
11
11
|
import { createMemoryEngine, createReadonlyMemoryEngine, defaultMemoryDbPath } from '../core/memory/engine.js'
|
|
12
|
+
import { writeWorkingContext } from '../core/memory/working-context.js'
|
|
12
13
|
import { memoryDoctor } from '../core/memory/doctor.js'
|
|
13
14
|
import { serveMemoryMcp } from '../core/memory/mcp-server.js'
|
|
14
15
|
import { writeDiscoveryMap } from '../core/discovery.js'
|
|
@@ -775,6 +776,39 @@ async function scaffoldAspec(projectDir) {
|
|
|
775
776
|
}
|
|
776
777
|
}
|
|
777
778
|
|
|
779
|
+
const LEGACY_OPENSPEC_DIR = 'openspec'
|
|
780
|
+
const MIGRATION_MARKER = '.migrated-from-openspec'
|
|
781
|
+
|
|
782
|
+
// "Contenido real" = al menos una entrada bajo changes/ o specs/. Un config.yaml
|
|
783
|
+
// solo (o el scaffold vacio) no cuenta como real: no bloquea la migracion.
|
|
784
|
+
async function hasRealContent(aspecDir) {
|
|
785
|
+
for (const sub of ['changes', 'specs']) {
|
|
786
|
+
try {
|
|
787
|
+
if ((await readdir(join(aspecDir, sub))).length > 0) return true
|
|
788
|
+
} catch {
|
|
789
|
+
// subcarpeta inexistente o ilegible: no aporta contenido real
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
return false
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// Migra la carpeta legacy openspec/ a aspec/ por copia recursiva (no destructiva).
|
|
796
|
+
// Unico dueno del invariante: la invocan init y upgrade antes del scaffold.
|
|
797
|
+
async function migrateLegacyOpenspec(projectDir) {
|
|
798
|
+
const legacyDir = join(projectDir, LEGACY_OPENSPEC_DIR)
|
|
799
|
+
if (!(await exists(legacyDir))) return // sin legacy: no-op silencioso
|
|
800
|
+
const aspecDir = join(projectDir, CHANGES_ROOT)
|
|
801
|
+
const marker = join(aspecDir, MIGRATION_MARKER)
|
|
802
|
+
if (await exists(marker)) return // ya migrada: no-op silencioso
|
|
803
|
+
if (await hasRealContent(aspecDir)) {
|
|
804
|
+
console.warn('ancleto: aspec/ ya tiene contenido real — no se migro openspec/ (revisar manualmente; openspec/ se conserva)')
|
|
805
|
+
return
|
|
806
|
+
}
|
|
807
|
+
await cp(legacyDir, aspecDir, { recursive: true, force: false })
|
|
808
|
+
await writeFile(marker, '# Generado por ancleto: esta aspec/ ya fue migrada desde openspec/\n')
|
|
809
|
+
console.log('ancleto: contenido migrado de openspec/ a aspec/ (openspec/ se conserva como backup)')
|
|
810
|
+
}
|
|
811
|
+
|
|
778
812
|
function extractLockedBlocks(content) {
|
|
779
813
|
const blocks = new Map()
|
|
780
814
|
const re = /<!--\s*LOCKED:\s*([\w-]+)\s*-->([\s\S]*?)<!--\s*\/LOCKED:\s*\1\s*-->/g
|
|
@@ -982,14 +1016,7 @@ async function upgradeCmd(args) {
|
|
|
982
1016
|
console.error("Error: No se encontro .ancletorc. Ejecuta 'ancleto init' primero.")
|
|
983
1017
|
process.exit(1)
|
|
984
1018
|
}
|
|
985
|
-
|
|
986
|
-
const changesRoot = join(projectDir, CHANGES_ROOT)
|
|
987
|
-
if ((await exists(legacyChanges)) && !(await exists(changesRoot))) {
|
|
988
|
-
await rename(legacyChanges, changesRoot)
|
|
989
|
-
console.log('ancleto: changes migrados de openspec/ a aspec/')
|
|
990
|
-
} else if ((await exists(legacyChanges)) && (await exists(changesRoot))) {
|
|
991
|
-
console.warn('ancleto: existen openspec/ y aspec/ — no se migro nada (revisar manualmente)')
|
|
992
|
-
}
|
|
1019
|
+
await migrateLegacyOpenspec(projectDir)
|
|
993
1020
|
const agent = await resolveAgent(args, rc.agent)
|
|
994
1021
|
await copyTemplates(projectDir)
|
|
995
1022
|
const agentSkillsDir = await installAgentSkills(projectDir, agent)
|
|
@@ -1083,6 +1110,7 @@ async function initProject(args) {
|
|
|
1083
1110
|
if (tier) {
|
|
1084
1111
|
await applyTier(join(projectDir, '.opencode', 'agents'), tier, tierModels(tier, TIERS, gratisModelChoice))
|
|
1085
1112
|
}
|
|
1113
|
+
await migrateLegacyOpenspec(projectDir)
|
|
1086
1114
|
await scaffoldAspec(projectDir)
|
|
1087
1115
|
await refreshWorkingContext(projectDir)
|
|
1088
1116
|
await registerProject(projectDir, { agent, tier: tier || null })
|
|
@@ -1419,16 +1447,11 @@ async function refreshWorkingContext(projectDir, scope = 'project') {
|
|
|
1419
1447
|
const dbPath = defaultMemoryDbPath(projectDir)
|
|
1420
1448
|
if (!(await exists(dbPath))) return null
|
|
1421
1449
|
const engine = createMemoryEngine(dbPath)
|
|
1422
|
-
let block
|
|
1423
1450
|
try {
|
|
1424
|
-
|
|
1451
|
+
return writeWorkingContext(engine, projectDir, scope)
|
|
1425
1452
|
} finally {
|
|
1426
1453
|
engine.close()
|
|
1427
1454
|
}
|
|
1428
|
-
const out = join(projectDir, '.ancleto', 'working-context.md')
|
|
1429
|
-
await mkdir(dirname(out), { recursive: true })
|
|
1430
|
-
await writeFile(out, block === null ? '' : block + '\n')
|
|
1431
|
-
return block === null ? null : out
|
|
1432
1455
|
}
|
|
1433
1456
|
|
|
1434
1457
|
async function memoryContext(flags) {
|
|
@@ -130,8 +130,8 @@ function buildEngine(db) {
|
|
|
130
130
|
WHERE memory_fts MATCH ? AND n.status = 'active'`
|
|
131
131
|
const run = (match, loose) => {
|
|
132
132
|
const sql = type
|
|
133
|
-
? db.prepare(`${base} AND n.type = ? ORDER BY rank LIMIT ?`)
|
|
134
|
-
: db.prepare(`${base} ORDER BY rank LIMIT ?`)
|
|
133
|
+
? db.prepare(`${base} AND n.type = ? ORDER BY rank, n.rowid LIMIT ?`)
|
|
134
|
+
: db.prepare(`${base} ORDER BY rank, n.rowid LIMIT ?`)
|
|
135
135
|
// FTS5 no acepta parametros en ORDER BY rank; el prefijo '*' va dentro del MATCH.
|
|
136
136
|
try {
|
|
137
137
|
return type ? sql.all(match, type, lim) : sql.all(match, lim)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createMemoryEngine, defaultMemoryDbPath } from './engine.js'
|
|
2
2
|
import { memoryTools, createMemoryToolHandlers } from './tools.js'
|
|
3
|
+
import { resolveMemoryProjectRoot } from './working-context.js'
|
|
3
4
|
|
|
4
5
|
const PROTOCOL_VERSION = '2024-11-05'
|
|
5
6
|
const SERVER_INFO = { name: 'ancleto-memory', version: '1.0.0' }
|
|
@@ -7,11 +8,12 @@ const SERVER_INFO = { name: 'ancleto-memory', version: '1.0.0' }
|
|
|
7
8
|
export function createMemoryServer({ dbPath = defaultMemoryDbPath() } = {}) {
|
|
8
9
|
let engine = null
|
|
9
10
|
let handlers = null
|
|
11
|
+
const projectRoot = resolveMemoryProjectRoot(dbPath)
|
|
10
12
|
|
|
11
13
|
function toolkit() {
|
|
12
14
|
if (!engine) {
|
|
13
15
|
engine = createMemoryEngine(dbPath)
|
|
14
|
-
handlers = createMemoryToolHandlers(engine, { source: 'mcp:ancleto-memory' })
|
|
16
|
+
handlers = createMemoryToolHandlers(engine, { source: 'mcp:ancleto-memory', projectRoot })
|
|
15
17
|
}
|
|
16
18
|
return handlers
|
|
17
19
|
}
|
package/src/core/memory/tools.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { createMemoryEngine } from './engine.js'
|
|
2
|
+
import { shouldRefreshWorkingContext, writeWorkingContext } from './working-context.js'
|
|
2
3
|
|
|
3
4
|
const KEY_DESCRIPTION = 'Clave conceptual estable (ej. "api-error-format"). Reusala para actualizar: la nueva version supersede automaticamente la anterior.'
|
|
4
5
|
|
|
5
6
|
const SCOPE_SCHEMA = {
|
|
6
7
|
type: 'string',
|
|
7
8
|
enum: ['project', 'feature', 'task'],
|
|
8
|
-
description: 'Ambito: project (default
|
|
9
|
+
description: 'Ambito: project (default; las reglas con scope project entran en <ProjectMemoryRules>, las decisiones se recuperan reactivamente con searchMemory), feature o task para algo mas acotado.'
|
|
9
10
|
}
|
|
10
11
|
|
|
11
12
|
const SEARCH_SCHEMA = {
|
|
@@ -62,10 +63,23 @@ export const memoryTools = [
|
|
|
62
63
|
]
|
|
63
64
|
|
|
64
65
|
export function createMemoryToolHandlers(engine, runtimeContext = {}) {
|
|
66
|
+
function record(args, type, defaultSource) {
|
|
67
|
+
const result = engine.recordNode({ ...args, type }, { ...runtimeContext, source: runtimeContext.source || defaultSource })
|
|
68
|
+
if (runtimeContext.projectRoot && shouldRefreshWorkingContext(result)) {
|
|
69
|
+
try {
|
|
70
|
+
writeWorkingContext(engine, runtimeContext.projectRoot)
|
|
71
|
+
} catch (err) {
|
|
72
|
+
// stderr: stdout es el canal JSON-RPC del server MCP.
|
|
73
|
+
console.warn(`ancleto: no se pudo refrescar el working-context: ${err.message}`)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return result
|
|
77
|
+
}
|
|
78
|
+
|
|
65
79
|
return {
|
|
66
80
|
searchMemory: (args) => engine.searchMemory(args),
|
|
67
|
-
recordRule: (args) =>
|
|
68
|
-
recordDecision: (args) =>
|
|
81
|
+
recordRule: (args) => record(args, 'rule', 'tool:recordRule'),
|
|
82
|
+
recordDecision: (args) => record(args, 'decision', 'tool:recordDecision')
|
|
69
83
|
}
|
|
70
84
|
}
|
|
71
85
|
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import { dirname, join } from 'node:path'
|
|
3
|
+
|
|
4
|
+
// Invariante "memoria activa -> working-context.md": render desde el engine vivo
|
|
5
|
+
// (una sola conexion, sin reabrir la DB) y persistencia del artefacto derivado.
|
|
6
|
+
|
|
7
|
+
export function workingContextPath(projectRoot) {
|
|
8
|
+
return join(projectRoot, '.ancleto', 'working-context.md')
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Trigger del refresh sobre el resultado completo de `recordNode` `{ node, superseded }`:
|
|
12
|
+
// (a) contribucion directa: el nodo entrante es rule + project (alimenta el bloque);
|
|
13
|
+
// (b) efecto por supersesion: `memory_key` es identidad transversal (supersede sin filtrar
|
|
14
|
+
// type/scope), asi que cualquier supersesion pudo retirar del bloque una rule de scope project.
|
|
15
|
+
// El over-refresh (supersesion que no altera el bloque) es intencional: el engine solo expone
|
|
16
|
+
// `{ node, superseded }` y el unico consumidor lee el archivo una vez por sesion.
|
|
17
|
+
export function shouldRefreshWorkingContext(result) {
|
|
18
|
+
return !!result && ((result.node.type === 'rule' && result.node.scope === 'project') || result.superseded > 0)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Deriva la raiz del proyecto desde la ubicacion de la DB (no desde el cwd del proceso).
|
|
22
|
+
// Prioriza el primer ancestro con `.ancletorc`; si no hay, usa el candidato derivado de
|
|
23
|
+
// `dbPath` mientras exista el layout `.ancleto/`; si no, no hay raiz (refresh best-effort).
|
|
24
|
+
export function resolveMemoryProjectRoot(dbPath) {
|
|
25
|
+
if (!dbPath) return null
|
|
26
|
+
const candidate = dirname(dirname(dbPath))
|
|
27
|
+
for (let dir = candidate; ; dir = dirname(dir)) {
|
|
28
|
+
if (existsSync(join(dir, '.ancletorc'))) return dir
|
|
29
|
+
const parent = dirname(dir)
|
|
30
|
+
if (parent === dir) break
|
|
31
|
+
}
|
|
32
|
+
if (existsSync(join(candidate, '.ancleto'))) return candidate
|
|
33
|
+
return null
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Render desde el engine vivo con la raiz explicita (topologia + salida) y escritura
|
|
37
|
+
// sincrona del archivo: paridad con `block + '\n'` y `''` cuando el bloque es null.
|
|
38
|
+
// Retorna el path escrito o null si no hay bloque/raiz.
|
|
39
|
+
export function writeWorkingContext(engine, projectRoot, scope = 'project') {
|
|
40
|
+
if (!projectRoot) return null
|
|
41
|
+
const block = engine.buildWorkingContext(scope, undefined, projectRoot)
|
|
42
|
+
const out = workingContextPath(projectRoot)
|
|
43
|
+
mkdirSync(dirname(out), { recursive: true })
|
|
44
|
+
writeFileSync(out, block === null ? '' : block + '\n')
|
|
45
|
+
return block === null ? null : out
|
|
46
|
+
}
|