@damiangilgonzalez/harness 1.3.18 → 1.3.20
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 +57 -5
- package/manifest.json +1 -1
- package/package.json +1 -1
- package/scripts/install-apply.js +9 -0
- package/scripts/rsc.js +27 -1
- package/skills/suggest/SKILL.md +12 -8
- package/skills/suggest/evals/cases.yaml +6 -0
- package/targets/clone-bootstrap.mjs +1 -1
- package/targets/knowledge-sync.mjs +759 -0
- package/targets/knowledge-wiring.js +196 -0
- package/targets/memory.js +1 -0
- package/targets/opencode-memory-plugin.js +67 -22
- package/targets/opt-outs.js +4 -0
- package/targets/session-memory-adapter.mjs +47 -5
- package/targets/session-memory-core.mjs +244 -29
- package/targets/trunk-policy.mjs +86 -0
package/README.md
CHANGED
|
@@ -169,11 +169,6 @@ Un `git pull` no escribe nada en tu máquina; `rsc doctor` te dice qué ya no cu
|
|
|
169
169
|
**Skills propias.** Las que escribe tu equipo viven en el repo y funcionan al clonar.
|
|
170
170
|
rsc nunca las instala, actualiza ni sobrescribe: su versión es el commit.
|
|
171
171
|
|
|
172
|
-

|
|
173
|
-
|
|
174
|
-
Al cerrar, guarda rama, ficheros y commits; al abrir, te dice dónde lo dejaste.
|
|
175
|
-
Nunca guarda prompts, respuestas ni secretos, y no sale de tu máquina. `rsc memory off` lo apaga.
|
|
176
|
-
|
|
177
172
|

|
|
178
173
|
|
|
179
174
|
Cada rama en su worktree, cortada siempre desde `origin`.
|
|
@@ -195,6 +190,63 @@ npx @damiangilgonzalez/harness@latest repair --dry-run
|
|
|
195
190
|
|
|
196
191
|
---
|
|
197
192
|
|
|
193
|
+
## La memoria
|
|
194
|
+
|
|
195
|
+

|
|
196
|
+
|
|
197
|
+
El agente empieza cada conversación en blanco. Lo que recuerda está en cuatro sitios, y cada uno
|
|
198
|
+
responde a una pregunta distinta:
|
|
199
|
+
|
|
200
|
+
| Nivel | Qué guarda | Dónde | Quién lo escribe |
|
|
201
|
+
|---|---|---|---|
|
|
202
|
+
| **1. Wiki del proyecto** | Lo que el proyecto *sabe*: decisiones, stack, hechos del negocio | `docs/wiki/` (en git) | El agente cuando dices «recuerda que…» |
|
|
203
|
+
| **2. Diario de sesión** | *Dónde lo dejaste*: rama, ficheros tocados, commits | `.rsc/memory/` (solo tu máquina, 30 días) | Los hooks, solos |
|
|
204
|
+
| **3. Lecciones aprobadas** | Reglas cortas para cada sesión de este proyecto | `.rsc/memory/lessons/` (solo tu máquina) | Tú, con `rsc memory learn … --approve` |
|
|
205
|
+
| **4. Memoria de Claude Code** | *Cómo te gusta trabajar a ti* | `~/.claude/projects/` (fuera del repo) | Claude, cuando le hablas de ti |
|
|
206
|
+
|
|
207
|
+

|
|
208
|
+
|
|
209
|
+
**Nada guarda tus conversaciones.** Solo se recuerda lo que alguien escribe en uno de los cuatro
|
|
210
|
+
sitios:
|
|
211
|
+
|
|
212
|
+
| Nivel | ¿Cómo se escribe? | ¿Qué haces tú? | ¿Cuándo lo lee el agente? |
|
|
213
|
+
|---|---|---|---|
|
|
214
|
+
| **1. Wiki** | El agente, cuando se lo pides, o una skill que documenta su trabajo (`harness`, `continuous-learning`…) | Decir «recuerda que…» | Cuando la tarea lo pide: `CLAUDE.md` le dice que existe `docs/wiki/index.md` |
|
|
215
|
+
| **2. Diario** | Automático: los hooks, en cada evento | Nada | Automático, al abrir sesión |
|
|
216
|
+
| **3. Lecciones** | Solo con un comando, y solo con `--approve` | `rsc memory learn --text "…" --approve` | Automático, al abrir sesión |
|
|
217
|
+
| **4. Memoria de Claude** | Claude Code, cuando le cuentas algo de ti | Decir «recuerda que prefiero…» | Automático, al abrir sesión (`MEMORY.md`) |
|
|
218
|
+
|
|
219
|
+
«Recuerda que…» sobre el proyecto va al nivel 1, y el agente te dice la ruta de la página. Si no
|
|
220
|
+
te da una ruta dentro de `docs/wiki/`, no lo ha guardado en el proyecto.
|
|
221
|
+
|
|
222
|
+
**Qué pasa en cada mensaje** (no al abrir rama ni sesión: en cada mensaje):
|
|
223
|
+
|
|
224
|
+
1. Envías un mensaje.
|
|
225
|
+
2. Antes de que el agente lo lea, un hook trae a **tu rama actual** lo nuevo de la rama
|
|
226
|
+
`rsc/knowledge` del remoto (solo `docs/wiki/`, `docs/attachments/` y `01-TOOLS/`) y te avisa
|
|
227
|
+
con una línea `📥 Traídos…`.
|
|
228
|
+
3. El agente responde con la wiki al día.
|
|
229
|
+
4. Al terminar, otro hook sube a `rsc/knowledge` lo que cambió en esas carpetas.
|
|
230
|
+
|
|
231
|
+
`rsc/knowledge` es una rama de intercambio, no una PR: lo que escribe un compañero te llega sin
|
|
232
|
+
revisión, y llega a `main` dentro de vuestras PRs normales. Nunca trae código, nunca pisa un
|
|
233
|
+
fichero que estás editando y nunca comitea en una `main` cerrada. Trabajando solo, sirve para que
|
|
234
|
+
lo que anotas en una rama aparezca en las demás. Lo que se trae es lo que se descargó en el
|
|
235
|
+
mensaje anterior; si la última descarga tiene más de 3 minutos, descarga antes (3 s como mucho).
|
|
236
|
+
|
|
237
|
+

|
|
238
|
+
|
|
239
|
+
El diario (nivel 2) nunca guarda prompts, respuestas ni secretos, y no sale de tu máquina.
|
|
240
|
+
|
|
241
|
+
```bash
|
|
242
|
+
npx @damiangilgonzalez/harness memory resume # lo que recibirá el agente al abrir sesión
|
|
243
|
+
npx @damiangilgonzalez/harness knowledge-sync status # ¿se sincroniza la wiki? y si no, por qué
|
|
244
|
+
npx @damiangilgonzalez/harness memory off # apaga el diario y las lecciones
|
|
245
|
+
npx @damiangilgonzalez/harness knowledge-sync off # apaga la sincronización (todo el proyecto)
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
Guía completa: [la memoria del harness](docs/como-funciona/memoria.md).
|
|
249
|
+
|
|
198
250
|
## Catálogo
|
|
199
251
|
|
|
200
252
|

|
package/manifest.json
CHANGED
package/package.json
CHANGED
package/scripts/install-apply.js
CHANGED
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
import {
|
|
22
22
|
wireMemory, unwireMemory, memoryManagedPaths, memoryModeFor, memoryEnabledForProject, memoryArtifactsPresent,
|
|
23
23
|
} from '../targets/memory.js';
|
|
24
|
+
import { wireKnowledge, unwireKnowledge, knowledgeManagedPaths, knowledgeArtifactsPresent } from '../targets/knowledge-wiring.js';
|
|
24
25
|
|
|
25
26
|
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
26
27
|
const CLI_VERSION = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf8')).version;
|
|
@@ -94,6 +95,7 @@ export function managedPathsForInstall({ skillIds, agentIds = [], target, home,
|
|
|
94
95
|
const out = [paths.stateFile, versionFile(cwd), baseVersionsFile(cwd)];
|
|
95
96
|
if (policy?.context7 === false) out.push(join(cwd, '.rsc', '.no-context7'));
|
|
96
97
|
if (policy?.memory !== false) out.push(...memoryManagedPaths(target, cwd));
|
|
98
|
+
out.push(...knowledgeManagedPaths(target, cwd));
|
|
97
99
|
if (targetHasAgents(target)) {
|
|
98
100
|
const state = readState(paths.stateFile);
|
|
99
101
|
const explicit = [...new Set([...(state.explicitAgents || readManifest(cwd)?.agents || []), ...agentIds])];
|
|
@@ -273,6 +275,10 @@ export async function applyInstall({ skillIds = [], agentIds = [], target, home,
|
|
|
273
275
|
? { mode: 'disabled', reason: 'onboarding-policy', paths: unwireMemory(target, cwd) }
|
|
274
276
|
: wireMemory(target, cwd);
|
|
275
277
|
state.memory = { mode: memoryResult.mode, reason: memoryResult.reason, paths: memoryResult.paths };
|
|
278
|
+
// Knowledge sync is its own decision (`.no-knowledge-sync`, checked at runtime) and never part of
|
|
279
|
+
// the memory, so the memory's policy does not decide it: wired wherever the assistant can run it.
|
|
280
|
+
const knowledgeResult = wireKnowledge(target, cwd);
|
|
281
|
+
state.knowledge = { mode: knowledgeResult.mode, reason: knowledgeResult.reason || null, paths: knowledgeResult.paths };
|
|
276
282
|
const context7OptOut = join(cwd, '.rsc', '.no-context7');
|
|
277
283
|
if (policy?.context7 === false) {
|
|
278
284
|
mkdirSync(dirname(context7OptOut), { recursive: true });
|
|
@@ -587,6 +593,7 @@ export function removeTargetInstall({ target, home, cwd = process.cwd() }) {
|
|
|
587
593
|
}
|
|
588
594
|
unwireHook(target, paths);
|
|
589
595
|
unwireMemory(target, cwd);
|
|
596
|
+
unwireKnowledge(target, cwd);
|
|
590
597
|
rmSync(paths.stateFile, { force: true });
|
|
591
598
|
const gitignore = join(cwd, '.gitignore');
|
|
592
599
|
if (existsSync(gitignore)) {
|
|
@@ -731,8 +738,10 @@ export async function purge({ home, cwd = process.cwd(), withDocs = false, dryRu
|
|
|
731
738
|
if (!dryRun) {
|
|
732
739
|
removed.push(...unwireHook(target, paths));
|
|
733
740
|
removed.push(...unwireMemory(target, cwd));
|
|
741
|
+
removed.push(...unwireKnowledge(target, cwd));
|
|
734
742
|
} else {
|
|
735
743
|
removed.push(...memoryArtifactsPresent(target, cwd));
|
|
744
|
+
removed.push(...knowledgeArtifactsPresent(target, cwd));
|
|
736
745
|
}
|
|
737
746
|
// A lost state file also loses proof of ownership. Leave same-named user files
|
|
738
747
|
// behind rather than guessing from a catalog id and deleting their work.
|
package/scripts/rsc.js
CHANGED
|
@@ -41,7 +41,7 @@ if (['--version', '-v', 'version'].includes(rawArgv[0])) {
|
|
|
41
41
|
const GLOBAL_VALUE_FLAGS = new Set([
|
|
42
42
|
'--target', '--technical-level', '--accompaniment', '--project-kind', '--goal', '--goal-base64', '--software-scope', '--accept-plan',
|
|
43
43
|
]);
|
|
44
|
-
const COMMANDS = new Set(['onboard', 'reassess', 'add', 'install', 'consult', 'catalog', 'audit', 'list', 'doctor', 'memory', 'sync', 'backups', 'restore', 'upgrade', 'registry', 'worktrees', 'capabilities', 'review', 'sello', 'repair', 'uninstall', 'purge']);
|
|
44
|
+
const COMMANDS = new Set(['onboard', 'reassess', 'add', 'install', 'consult', 'catalog', 'audit', 'list', 'doctor', 'memory', 'knowledge-sync', 'sync', 'backups', 'restore', 'upgrade', 'registry', 'worktrees', 'capabilities', 'review', 'sello', 'repair', 'uninstall', 'purge']);
|
|
45
45
|
function positionalTokens(input) {
|
|
46
46
|
const out = [];
|
|
47
47
|
for (let i = 0; i < input.length; i++) {
|
|
@@ -756,6 +756,32 @@ async function main() {
|
|
|
756
756
|
printContextBudget(report.contextBudget);
|
|
757
757
|
return void say(JSON.stringify({ ...report, contextBudget: undefined }, null, 2));
|
|
758
758
|
}
|
|
759
|
+
case 'knowledge-sync': {
|
|
760
|
+
// On by default; the switch is a PROJECT decision (`.no-knowledge-sync`, recorded in `.rsc.json`
|
|
761
|
+
// optOuts by the sync below), because one person off means the team stops seeing their work.
|
|
762
|
+
const sub = argv[1] || 'status';
|
|
763
|
+
const root = process.cwd();
|
|
764
|
+
const { mkdirSync, writeFileSync, rmSync } = await import('node:fs');
|
|
765
|
+
const { join } = await import('node:path');
|
|
766
|
+
const marker = join(root, '.rsc', '.no-knowledge-sync');
|
|
767
|
+
if (sub === 'on' || sub === 'off') {
|
|
768
|
+
mkdirSync(join(root, '.rsc'), { recursive: true });
|
|
769
|
+
if (sub === 'off') writeFileSync(marker, '');
|
|
770
|
+
else rmSync(marker, { force: true });
|
|
771
|
+
for (const t of targets) await syncInstalled({ target: t, cwd: root });
|
|
772
|
+
say(sub === 'off'
|
|
773
|
+
? 'rsc knowledge-sync off: 01-TOOLS/ and docs/ no longer sync on their own in this project. Commit .rsc.json so the team gets the same decision.'
|
|
774
|
+
: 'rsc knowledge-sync on: 01-TOOLS/ and docs/ go up when a turn ends and come down before each message. Commit .rsc.json so the team gets the same decision.');
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
if (sub === 'status') {
|
|
778
|
+
const { knowledgeStatus } = await import('../targets/knowledge-sync.mjs');
|
|
779
|
+
return void say(JSON.stringify(knowledgeStatus(root), null, 2));
|
|
780
|
+
}
|
|
781
|
+
say('Use: npx @damiangilgonzalez/harness@latest knowledge-sync on|off|status');
|
|
782
|
+
process.exitCode = 2;
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
759
785
|
case 'memory': {
|
|
760
786
|
const sub = argv[1] || 'status';
|
|
761
787
|
const root = process.cwd();
|
package/skills/suggest/SKILL.md
CHANGED
|
@@ -71,13 +71,12 @@ name it and offer it. This runs mid-conversation, not only at project start.
|
|
|
71
71
|
1. `npx @damiangilgonzalez/harness catalog --available` lists every not-installed skill as
|
|
72
72
|
`id available short description`.
|
|
73
73
|
2. Pick the single best fit **by meaning**, the way you would match a request to a teammate's
|
|
74
|
-
expertise
|
|
75
|
-
|
|
76
|
-
and move on: a tangential suggestion is worse than none.
|
|
74
|
+
expertise, even when no keyword overlaps. If nothing genuinely fits, say so and move on: a
|
|
75
|
+
tangential suggestion is worse than none.
|
|
77
76
|
3. Ask once, plainly: "Para esto instalaría `<id>`, que aún no tienes. ¿La instalo? (sí/no)".
|
|
78
77
|
4. On yes, run `npx @damiangilgonzalez/harness add <id>`, then continue the original task.
|
|
79
78
|
|
|
80
|
-
|
|
79
|
+
One suggestion at a time,
|
|
81
80
|
and never to interrupt a flow with a nice-to-have. Never recommend something already installed
|
|
82
81
|
(`npx @damiangilgonzalez/harness list`).
|
|
83
82
|
|
|
@@ -95,9 +94,8 @@ Not covered → one line, skill or agent. Rules: `skill-scout`.
|
|
|
95
94
|
|
|
96
95
|
## 3. A harness that is broken fixes itself
|
|
97
96
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
nothing they recognise. When any of these is true, act on it **once** in the session:
|
|
97
|
+
Its owner rarely notices: the symptoms name nothing they know. When any of these
|
|
98
|
+
is true, act on it **once** in the session:
|
|
101
99
|
|
|
102
100
|
- `.rsc.json` exists and what it declares is not what is installed;
|
|
103
101
|
- the same hook seems to run several times;
|
|
@@ -117,7 +115,7 @@ a cause. Then:
|
|
|
117
115
|
teammate added, disarming a gate — **ask first**. A `git pull` never rewrites someone's
|
|
118
116
|
machine.
|
|
119
117
|
|
|
120
|
-
Offer once per session
|
|
118
|
+
Offer once per session; say nothing when the harness is healthy.
|
|
121
119
|
|
|
122
120
|
## 4. First contact
|
|
123
121
|
|
|
@@ -130,6 +128,12 @@ Before handling the first request of a session, check the workspace:
|
|
|
130
128
|
`.rsc/.no-harness`, confirm in one line, and never auto-start `init` in this repo again.
|
|
131
129
|
- Once the profile exists, this gate is inert. Never re-onboard.
|
|
132
130
|
|
|
131
|
+
## 5. «Recuerda esto»
|
|
132
|
+
|
|
133
|
+
A project or business fact goes to its topic page in `docs/wiki/` (link a new page from
|
|
134
|
+
`wiki/index.md`); say the path. Your own memory sits outside the project, unseen by person and
|
|
135
|
+
team: keep there only how this person likes you to work (or all, without `docs/`).
|
|
136
|
+
|
|
133
137
|
## Explain without assuming
|
|
134
138
|
|
|
135
139
|
Define a term at first use; never give a command, flag or path without saying what it does.
|
|
@@ -76,3 +76,9 @@ capability:
|
|
|
76
76
|
- "Names that one missing automation/connector skill"
|
|
77
77
|
- "Asks one short confirmation before installing and does not auto-install"
|
|
78
78
|
- "On confirmation, runs `npx @damiangilgonzalez/harness add <id>` for that one missing skill, then resumes the automation task"
|
|
79
|
+
|
|
80
|
+
- scenario: "In a workspace with `docs/wiki/`, the user says 'recuerda que toda la actividad, también la de Andorra, va por una sola sociedad, Nadsan 2006 S.L.'. Show where rsc-suggest puts it."
|
|
81
|
+
must_include:
|
|
82
|
+
- "Writes the fact into the `docs/wiki/` page of its topic (creating it and linking it from `wiki/index.md` if none exists), not only into the assistant's own memory under `~/.claude/projects/`"
|
|
83
|
+
- "Tells the person the path where it was saved"
|
|
84
|
+
- "Does not start the feature chain or ask a lane question for it"
|
|
@@ -43,7 +43,7 @@ const SEMVER = /^\d+\.\d+\.\d+(?:-[0-9a-z.-]+)?$/i;
|
|
|
43
43
|
* nothing checks is just drift with a head start.
|
|
44
44
|
*/
|
|
45
45
|
export const PROJECT_OPT_OUTS = [
|
|
46
|
-
'audit', 'claudemd-check', 'danger-guard', 'feature-gate', 'gitmoji', 'ship-guard', 'worktree-cleanup',
|
|
46
|
+
'audit', 'claudemd-check', 'danger-guard', 'feature-gate', 'gitmoji', 'knowledge-sync', 'ship-guard', 'trunk-guard', 'trunk-open', 'worktree-cleanup', 'worktree-isolation',
|
|
47
47
|
];
|
|
48
48
|
|
|
49
49
|
/**
|