wendkeep 0.3.0 → 0.6.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/CHANGELOG.md +129 -0
- package/LICENSE +21 -0
- package/README.md +23 -3
- package/bin/wendkeep.mjs +6 -0
- package/hooks/brain-inject.mjs +3 -1
- package/hooks/change-core.mjs +73 -8
- package/hooks/harness-doctor.mjs +77 -0
- package/hooks/lessons-core.mjs +43 -0
- package/hooks/sensors-core.mjs +34 -4
- package/hooks/spec-core.mjs +114 -0
- package/package.json +12 -3
- package/src/change.mjs +17 -5
- package/src/doctor.mjs +22 -10
- package/src/init.mjs +7 -1
- package/src/lessons.mjs +35 -0
- package/src/skills-seed.mjs +85 -22
- package/src/taxonomy.mjs +3 -0
- package/src/verify.mjs +45 -5
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to **wendkeep** are documented here. Format based on
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this project follows
|
|
5
|
+
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## [0.6.0] — 2026-07-05
|
|
8
|
+
|
|
9
|
+
Enforcement layer (Wave B of the TLC-parity program) — closes TLC parity.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- **Discrimination sensor (`type: mutation`):** delegates to the project's mutation tool and
|
|
13
|
+
parses its mutation-testing-elements report; surviving mutants become fix tasks in the active
|
|
14
|
+
change (`- [ ] M.n mata mutante file:line`), bounded to 3 rounds before escalating.
|
|
15
|
+
- **Harness self-check:** `wendkeep doctor` now validates the a2 state — an invalid
|
|
16
|
+
`wendkeep.sensors.json`, a broken `CURRENT_CHANGE` pointer, changes without a `proposta.md`,
|
|
17
|
+
an orphan `[req:]` (unknown requirement), and stale verdicts.
|
|
18
|
+
- **Lessons loop:** `wendkeep lesson add "<trigger>" "<lesson>"` records a project-local lesson in
|
|
19
|
+
`.brain/lessons/`; `brain-inject` surfaces the recent ones as a `<lessons>` block at SessionStart.
|
|
20
|
+
- **Auto-sizing:** `wendkeep change new <slug> --simple` scaffolds only proposta + tarefas
|
|
21
|
+
(no design / spec-delta) for trivial changes.
|
|
22
|
+
- **Harness contract v1.1** (`docs/14-harness-contract.md`): the mutation + lesson formats.
|
|
23
|
+
|
|
24
|
+
## [0.5.0] — 2026-07-05
|
|
25
|
+
|
|
26
|
+
Verification & credibility layer (Wave A of the TLC-parity program). The gate stops being
|
|
27
|
+
"green sensors" alone and starts requiring an independent verdict for changes that touch a spec.
|
|
28
|
+
|
|
29
|
+
### Added
|
|
30
|
+
- **Requirement IDs + traceability:** living-spec requirements carry a stable ID
|
|
31
|
+
(`### Requisito: GATE-1 — nome`); tasks reference them with `[req:<ID>]`; the archive ADR
|
|
32
|
+
lists the requirements it satisfied. Rastro req → task → verdict → ADR.
|
|
33
|
+
- **`wendkeep verify --deep`:** assembles a verification package (`verificacao.json`) for an
|
|
34
|
+
independent pass. A trivial change (no `[req:]`, sensors green) gets an auto verdict.
|
|
35
|
+
- **Independent verdict gate:** `change archive` now also requires `verdict.json` (`ok`, covering
|
|
36
|
+
every declared `[req:]`) for requirement-bearing changes.
|
|
37
|
+
- **TLC-grade process skills:** rewrote `wk-tdd` (spec-derived assertions, non-shallow litmus,
|
|
38
|
+
test adequacy, test-learning) and `wk-brainstorming` (closure gate + out-of-scope); new
|
|
39
|
+
`wk-verify` (fresh read-only verifier, author≠verifier).
|
|
40
|
+
- **Harness contract v1** (`docs/14-harness-contract.md`): the extension-point formats.
|
|
41
|
+
|
|
42
|
+
### Changed
|
|
43
|
+
- Requirement-less changes are unaffected — the sensor gate remains their proof. The verdict
|
|
44
|
+
requirement applies only when a change declares `[req:]` tasks. Specs from 0.4.0 (headings
|
|
45
|
+
without an ID) stay valid.
|
|
46
|
+
|
|
47
|
+
## [0.4.0] — 2026-07-05
|
|
48
|
+
|
|
49
|
+
Spec promotion (the living contract) + harness fixes.
|
|
50
|
+
|
|
51
|
+
### Added
|
|
52
|
+
- **Spec promotion** — `wendkeep change archive` merges each capability's spec delta
|
|
53
|
+
(`## ADDED` / `## MODIFIED` / `## REMOVED Requirements`) into the living
|
|
54
|
+
`07-Specs/<capability>.md`. Multi-capability per change via `specs: [slugs]` in the
|
|
55
|
+
proposta; the living spec footer wikilinks the archived change (`hooks/spec-core.mjs`).
|
|
56
|
+
- Change scaffold now seeds an example spec delta at `specs/exemplo/spec.md`.
|
|
57
|
+
- `wendkeep change archive` prints promoted capabilities and surfaces delta warnings
|
|
58
|
+
(ADDED-already-exists / MODIFIED-missing) without blocking.
|
|
59
|
+
|
|
60
|
+
### Changed
|
|
61
|
+
- `wendkeep init` now runs `sync-defs` itself — process skills and agents are delivered
|
|
62
|
+
to `.claude/skills` / `.codex/agents` immediately (no manual step).
|
|
63
|
+
- The archive gate and `wendkeep verify` share one rule: only **critical** (or missing)
|
|
64
|
+
sensors block; a red `warning` sensor is advisory. Evidence records each sensor's severity.
|
|
65
|
+
- README documents the change/verify/skills commands and the a2 loop.
|
|
66
|
+
|
|
67
|
+
## [0.3.0] — 2026-07-05
|
|
68
|
+
|
|
69
|
+
The a2 native harness — a zero-dependency spec→change→proof loop on the vault memory
|
|
70
|
+
core (recreates the best of OpenSpec + dotcontext + superpowers, natively).
|
|
71
|
+
|
|
72
|
+
### Added
|
|
73
|
+
- **Pilar B — change lifecycle:** `wendkeep change new|list|show|archive`. Scaffolds
|
|
74
|
+
`08-Mudanças/<slug>/` (proposta/design/tarefas); the active change is injected at the
|
|
75
|
+
next `SessionStart`; archive moves the change to `_arquivo/` and mints an ADR in
|
|
76
|
+
`04-Decisões/`. New vault folders `07-Specs/`, `08-Mudanças/` (`hooks/change-core.mjs`).
|
|
77
|
+
- **Pilar C — verify + gate:** `wendkeep verify` runs a change's task-declared sensors
|
|
78
|
+
(`[sensor:<id>]` hints) from a native `wendkeep.sensors.json`, records `evidencia.json`;
|
|
79
|
+
`change archive` gates on the evidence (`hooks/sensors-core.mjs`).
|
|
80
|
+
- **Pilar A — process skills:** native `wk-workflow` / `wk-tdd` / `wk-debugging` /
|
|
81
|
+
`wk-brainstorming` / `wk-planning` seeded into `.brain/skills` (`src/skills-seed.mjs`).
|
|
82
|
+
|
|
83
|
+
## [0.2.7] — 2026-06-30
|
|
84
|
+
|
|
85
|
+
### Added
|
|
86
|
+
- **Definitions layer:** `.brain/agents/` + `.brain/skills/` as versioned source of truth,
|
|
87
|
+
copied into the project with `wendkeep sync-defs`.
|
|
88
|
+
- **dotcontext seed:** a starter `.context/config/sensors.json` (a `validate-memory` sensor
|
|
89
|
+
plus one per detected `package.json` script) when the dotcontext companion is selected.
|
|
90
|
+
|
|
91
|
+
## [0.2.1] – [0.2.6] — 2026-06-29
|
|
92
|
+
|
|
93
|
+
Rapid iteration on the companion + memory layers (same day):
|
|
94
|
+
- **Companions** wired the most agent-agnostic way (context-mode / dotcontext as MCP,
|
|
95
|
+
understand-anything via a domain-graph SessionStart injector, caveman via installer).
|
|
96
|
+
- **Obsidian color system** — a mode-agnostic CSS snippet (note-type accents) + graph
|
|
97
|
+
color groups, merged non-destructively into `.obsidian/`.
|
|
98
|
+
- **Curated memory protocol** — `.brain/CORE.md` + `COMPACTION_PROTOCOL.md` and
|
|
99
|
+
`wendkeep validate-memory` (cap 25 lines, 3 sections, no secrets/PII).
|
|
100
|
+
- Cross-platform caveman installer fix (npx non-interactive; Gemini excluded).
|
|
101
|
+
- Derived notes grouped by month under the year.
|
|
102
|
+
|
|
103
|
+
## [0.2.0] — 2026-06-29
|
|
104
|
+
|
|
105
|
+
### Added
|
|
106
|
+
- Companion plugins/MCP selection in `wendkeep init` (context-mode, understand-anything,
|
|
107
|
+
caveman) with idempotent settings/`.mcp.json` merging.
|
|
108
|
+
|
|
109
|
+
## [0.1.0] — 2026-06-29
|
|
110
|
+
|
|
111
|
+
Initial release — the capture engine, extracted from a system in daily production use.
|
|
112
|
+
|
|
113
|
+
### Added
|
|
114
|
+
- Automatic session capture (`SessionStart` / `UserPromptSubmit` / `Stop` hooks) into
|
|
115
|
+
`02-Sessões/` as turn-by-turn Markdown.
|
|
116
|
+
- Multi-agent provider detection (Claude Code, Codex, Copilot).
|
|
117
|
+
- Token & cost tracking (cache-aware `pricing.json`).
|
|
118
|
+
- Auto-extracted derived notes (decisions / bugs / learnings), backlinked to the session.
|
|
119
|
+
- Curated memory (`.brain/` cold index + `CORE` + `DIGEST` injected at `SessionStart`).
|
|
120
|
+
- `wendkeep init` (cross-platform installer) + optional `@bitbonsai/mcpvault` MCP server.
|
|
121
|
+
|
|
122
|
+
<!-- Only v0.4.0+ is tagged in git (history starts here); older versions link to npm. -->
|
|
123
|
+
[0.6.0]: https://github.com/rogersialves/wendkeep/releases/tag/v0.6.0
|
|
124
|
+
[0.5.0]: https://github.com/rogersialves/wendkeep/releases/tag/v0.5.0
|
|
125
|
+
[0.4.0]: https://github.com/rogersialves/wendkeep/releases/tag/v0.4.0
|
|
126
|
+
[0.3.0]: https://www.npmjs.com/package/wendkeep/v/0.3.0
|
|
127
|
+
[0.2.7]: https://www.npmjs.com/package/wendkeep/v/0.2.7
|
|
128
|
+
[0.2.0]: https://www.npmjs.com/package/wendkeep/v/0.2.0
|
|
129
|
+
[0.1.0]: https://www.npmjs.com/package/wendkeep/v/0.1.0
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Roger Alves
|
|
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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# wendkeep
|
|
2
2
|
|
|
3
|
-
**
|
|
3
|
+
**A persistent‑memory harness for AI coding agents, built on your Obsidian vault.** Every Claude Code / Codex session is captured turn‑by‑turn into local Markdown — with token/cost tracking, auto‑extracted decisions, bugs and learnings, and a curated memory layer injected back at the start of the next session. On top of that memory core sits a native, zero‑dependency **change lifecycle** (spec → change → TDD → sensor‑gated archive) that keeps intent, work and proof wikilinked in one graph. 100% local, open‑core.
|
|
4
4
|
|
|
5
5
|
> Status: `v0.1` — extracted from a system in daily production use. The capture engine, cost tracking and graph wiring are battle‑tested; the cross‑platform installer (`wendkeep init`) is the new part. See [`docs/`](docs/) for the project's strategy and decision log.
|
|
6
6
|
|
|
@@ -41,7 +41,8 @@ npx wendkeep init
|
|
|
41
41
|
4. Offer to pin **companion** plugins/MCP (multi-choice; `context-mode` + `dotcontext` pre-checked). Each is wired through the most agent-agnostic path it supports — `context-mode` and `dotcontext` as `.mcp.json` MCP servers (any agent), `understand-anything` via a `understand-inject` SessionStart hook that injects its domain graph when generated, and the Claude Code plugin layer (`extraKnownMarketplaces` + `enabledPlugins`) as a bonus. **`dotcontext`** (execution/PREVC harness) also gets its lifecycle hooks wired (SessionStart-last + Stop + PostToolUse, pinned CLI) and a starter `.context/config/sensors.json` seeded with a `wendkeep validate-memory` sensor plus one per detected `package.json` script (test/typecheck/lint/build) — add the `.context/runtime|cache|logs|plans|agents|skills/` paths to `.gitignore` (wendkeep doesn't touch git). If dotcontext is already configured globally (`~/.claude.json`), the project MCP entry is **skipped automatically** to avoid a duplicate; tune with `--dotcontext-mcp <auto|project|none>` and `--dotcontext-hooks <full|light|none>` (light drops PostToolUse). Control selection with `--companions <csv>` or `--no-companions`. (`caveman` additionally runs its own cross-agent installer on non-Claude agents.)
|
|
42
42
|
5. Install a **color system** into the vault's `.obsidian/`: a CSS snippet that accents notes by type (session/decision/bug/learning, via the `cssclasses` the hooks emit) plus graph color groups by folder. Non-destructive merge into `appearance.json`/`graph.json`; skip with `--no-colors`.
|
|
43
43
|
6. Seed the **curated memory layer**: `.brain/CORE.md` (the hand-curated hot layer, with the 3 required sections) and `.brain/COMPACTION_PROTOCOL.md` (the protocol guide). The auto layers (`DIGEST.md`, `index.jsonl`) are generated by the hooks. Validate the curated layer with `wendkeep validate-memory` (cap 25 lines, 3 sections, no secrets/PII).
|
|
44
|
-
7. Seed the **definitions layer**: `.brain/agents/` + `.brain/skills/` (versioned source of truth
|
|
44
|
+
7. Seed the **definitions + skills layer**: `.brain/agents/` + `.brain/skills/` (versioned source of truth), including the native process skills `wk-workflow` / `wk-tdd` / `wk-debugging` / `wk-brainstorming` / `wk-planning`. `init` runs `wendkeep sync-defs` for you, delivering them to `.codex/agents/` + `.claude/skills/` (re‑run `sync-defs` after editing `.brain`).
|
|
45
|
+
8. Seed the **change/spec lifecycle**: the `07-Specs/` + `08-Mudanças/` folders and a native `wendkeep.sensors.json` (a `validate-memory` sensor plus one per detected `package.json` script). Drives `wendkeep change` / `wendkeep verify` — see **Change lifecycle** below.
|
|
45
46
|
|
|
46
47
|
```bash
|
|
47
48
|
npx wendkeep init --vault "~/vaults/work" --project . --yes # non-interactive (companions: context-mode,dotcontext)
|
|
@@ -65,11 +66,30 @@ No re‑copying, no snapshot to re‑sync — the package is the single source o
|
|
|
65
66
|
|
|
66
67
|
| Command | What it does |
|
|
67
68
|
|---|---|
|
|
68
|
-
| `wendkeep init` | Set up wendkeep in a project (vault taxonomy + settings + MCP). |
|
|
69
|
+
| `wendkeep init` | Set up wendkeep in a project (vault taxonomy + settings + MCP + skills). |
|
|
69
70
|
| `wendkeep hook <name>` | Run a session hook; invoked by `settings.json` (reads agent JSON on stdin). |
|
|
71
|
+
| `wendkeep change <sub>` | Change lifecycle: `new <slug>` / `list` / `show <slug>` / `archive <slug>`. |
|
|
72
|
+
| `wendkeep verify [--change s]` | Run the active (or named) change's task sensors; record evidence (the gate). |
|
|
73
|
+
| `wendkeep sync-defs` | Copy `.brain/agents\|skills` into the project (`.codex/agents`, `.claude/skills`). |
|
|
74
|
+
| `wendkeep validate-memory [path]` | Validate `.brain/CORE.md` (cap 25, 3 sections, no secrets/PII). |
|
|
70
75
|
| `wendkeep doctor [--vault P]` | Run a vault health check (integrity of sessions, registry, links). |
|
|
71
76
|
| `wendkeep --version` / `--help` | Version / usage. |
|
|
72
77
|
|
|
78
|
+
## Change lifecycle — the a2 loop (spec‑driven, native)
|
|
79
|
+
|
|
80
|
+
Beyond capturing sessions, wendkeep is a **harness**: a native, zero‑dependency loop that keeps *intent* (specs), *work* (changes) and *proof* (sensors) together in the vault, wikilinked into the Obsidian graph.
|
|
81
|
+
|
|
82
|
+
```
|
|
83
|
+
explore → propose → apply (TDD) → verify → archive
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
- **Propose** — `wendkeep change new <slug>` scaffolds `08-Mudanças/<slug>/` (`proposta.md`, `design.md`, `tarefas.md`, and a `specs/` delta). The change becomes *active* and is injected at the next `SessionStart`, so the agent resumes work‑in‑progress.
|
|
87
|
+
- **Apply** — implement each `tarefas.md` task. Tag a task that needs machine proof with `[sensor:<id>]`.
|
|
88
|
+
- **Verify** — `wendkeep verify` runs the sensors your tasks declared (from `wendkeep.sensors.json` at the project root) and records `evidencia.json`. A critical red fails the gate; a `warning` red is advisory.
|
|
89
|
+
- **Archive** — `wendkeep change archive <slug>` **gates** on the evidence (blocks unless every declared critical sensor is green), promotes each capability's spec delta (`ADDED`/`MODIFIED`/`REMOVED`) into the living `07-Specs/<capability>.md`, moves the change to `_arquivo/`, and mints an ADR in `04-Decisões/`.
|
|
90
|
+
|
|
91
|
+
`wendkeep init` also seeds **native process skills** (`wk-workflow`, `wk-tdd`, `wk-debugging`, `wk-brainstorming`, `wk-planning`) into `.brain/skills` and delivers them to `.claude/skills` — the *how* layer, zero‑dep. Optional companions (`context-mode`, `dotcontext`, `understand-anything`, `caveman`) remain an opt‑in extra layer.
|
|
92
|
+
|
|
73
93
|
## How it works
|
|
74
94
|
|
|
75
95
|
```
|
package/bin/wendkeep.mjs
CHANGED
|
@@ -45,6 +45,7 @@ Usage:
|
|
|
45
45
|
wendkeep doctor [--vault P] Run a vault health check.
|
|
46
46
|
wendkeep change <sub> Change lifecycle: new <slug> | list | show <slug> | archive <slug>.
|
|
47
47
|
wendkeep verify [--change s] Run a change's task sensors + record evidence (the gate).
|
|
48
|
+
wendkeep lesson add "t" "l" Record a project-local lesson (injected at SessionStart).
|
|
48
49
|
wendkeep validate-memory [path] Validate .brain/CORE.md against the compaction
|
|
49
50
|
protocol (cap 25, 3 sections, no secrets/PII). Uses
|
|
50
51
|
--vault <path> or OBSIDIAN_VAULT_PATH if no path given.
|
|
@@ -111,6 +112,11 @@ async function main() {
|
|
|
111
112
|
runVerify(rest);
|
|
112
113
|
break;
|
|
113
114
|
}
|
|
115
|
+
case 'lesson': {
|
|
116
|
+
const { runLesson } = await import('../src/lessons.mjs');
|
|
117
|
+
runLesson(rest);
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
114
120
|
case '--version':
|
|
115
121
|
case '-v':
|
|
116
122
|
process.stdout.write(`${version()}\n`);
|
package/hooks/brain-inject.mjs
CHANGED
|
@@ -8,6 +8,7 @@ import { pathToFileURL } from 'node:url';
|
|
|
8
8
|
import { getVaultBase, readHookInput, writeHookOutput } from './obsidian-common.mjs';
|
|
9
9
|
import { brainDir } from './brain-core.mjs';
|
|
10
10
|
import { buildActiveChangeInjection } from './change-core.mjs';
|
|
11
|
+
import { buildLessonsInjection } from './lessons-core.mjs';
|
|
11
12
|
|
|
12
13
|
const MAX_LINES = 45; // CORE ≤25 + DIGEST ≤15 + folga; salvaguarda se o CORE crescer à mão
|
|
13
14
|
|
|
@@ -26,7 +27,8 @@ export function buildInjection(vaultBase) {
|
|
|
26
27
|
}
|
|
27
28
|
const brain = ['<brain_memory>', ...lines, pointer, '</brain_memory>'].join('\n');
|
|
28
29
|
const change = buildActiveChangeInjection(vaultBase);
|
|
29
|
-
|
|
30
|
+
const lessons = buildLessonsInjection(vaultBase);
|
|
31
|
+
return [brain, change, lessons].filter(Boolean).join('\n');
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
package/hooks/change-core.mjs
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, writeFileSync } from 'node:fs';
|
|
5
5
|
import { join } from 'node:path';
|
|
6
6
|
import { ensureDir, wikilinkFromRel } from './obsidian-common.mjs';
|
|
7
|
+
import { parseSpecsList, promoteSpecs } from './spec-core.mjs';
|
|
7
8
|
|
|
8
9
|
export const CHANGES_DIR = '08-Mudanças';
|
|
9
10
|
export const SPECS_DIR = '07-Specs';
|
|
@@ -48,7 +49,15 @@ specs: []
|
|
|
48
49
|
|
|
49
50
|
- [ ] 1.1 (primeira tarefa)
|
|
50
51
|
`;
|
|
51
|
-
|
|
52
|
+
const specDelta = `## ADDED Requirements
|
|
53
|
+
### Requisito: (nome)
|
|
54
|
+
(comportamento / cenários)
|
|
55
|
+
|
|
56
|
+
## MODIFIED Requirements
|
|
57
|
+
|
|
58
|
+
## REMOVED Requirements
|
|
59
|
+
`;
|
|
60
|
+
return { proposta, design, tarefas, specDelta };
|
|
52
61
|
}
|
|
53
62
|
|
|
54
63
|
export function activeChange(vaultBase) {
|
|
@@ -70,7 +79,7 @@ export function clearActiveChange(vaultBase) {
|
|
|
70
79
|
if (existsSync(p)) writeFileSync(p, 'change:\n', 'utf8');
|
|
71
80
|
}
|
|
72
81
|
|
|
73
|
-
export function newChange(vaultBase, slug, { sessionRel = '', dateStr }) {
|
|
82
|
+
export function newChange(vaultBase, slug, { sessionRel = '', dateStr, simple = false }) {
|
|
74
83
|
const dir = join(vaultBase, CHANGES_DIR, slug);
|
|
75
84
|
const existed = existsSync(join(dir, 'proposta.md'));
|
|
76
85
|
mkdirSync(dir, { recursive: true });
|
|
@@ -80,8 +89,16 @@ export function newChange(vaultBase, slug, { sessionRel = '', dateStr }) {
|
|
|
80
89
|
if (!existsSync(f)) writeFileSync(f, content, 'utf8');
|
|
81
90
|
};
|
|
82
91
|
write('proposta.md', files.proposta);
|
|
83
|
-
write('design.md', files.design);
|
|
84
92
|
write('tarefas.md', files.tarefas);
|
|
93
|
+
// Auto-sizing (Wave B): a --simple change skips the design + spec-delta scaffold.
|
|
94
|
+
if (!simple) {
|
|
95
|
+
write('design.md', files.design);
|
|
96
|
+
const exampleDelta = join(dir, 'specs', 'exemplo', 'spec.md');
|
|
97
|
+
if (!existsSync(exampleDelta)) {
|
|
98
|
+
mkdirSync(join(dir, 'specs', 'exemplo'), { recursive: true });
|
|
99
|
+
writeFileSync(exampleDelta, files.specDelta, 'utf8');
|
|
100
|
+
}
|
|
101
|
+
}
|
|
85
102
|
setActiveChange(vaultBase, slug);
|
|
86
103
|
return { rel: changeDirRel(slug), created: !existed };
|
|
87
104
|
}
|
|
@@ -90,13 +107,18 @@ export function parseTasks(md) {
|
|
|
90
107
|
const tasks = [];
|
|
91
108
|
const re = /^-\s+\[( |x)\]\s+(\S+)\s+(.*)$/gm;
|
|
92
109
|
const sensorRe = /\[sensor:\s*([\w.-]+)\]/;
|
|
110
|
+
const reqRe = /\[req:\s*([A-Z][A-Z0-9]*-\d+)\]/;
|
|
93
111
|
let m;
|
|
94
112
|
while ((m = re.exec(String(md))) !== null) {
|
|
95
113
|
let text = m[3].trim();
|
|
96
114
|
const sm = text.match(sensorRe);
|
|
115
|
+
const rm = text.match(reqRe);
|
|
97
116
|
const sensor = sm ? sm[1] : undefined;
|
|
98
|
-
|
|
99
|
-
|
|
117
|
+
const req = rm ? rm[1] : undefined;
|
|
118
|
+
if (sm) text = text.replace(sensorRe, '');
|
|
119
|
+
if (rm) text = text.replace(reqRe, '');
|
|
120
|
+
text = text.replace(/\s+/g, ' ').trim();
|
|
121
|
+
tasks.push({ id: m[2], text, done: m[1] === 'x', ...(sensor ? { sensor } : {}), ...(req ? { req } : {}) });
|
|
100
122
|
}
|
|
101
123
|
return tasks;
|
|
102
124
|
}
|
|
@@ -136,6 +158,29 @@ export function activeChangeLink(vaultBase) {
|
|
|
136
158
|
return slug ? `Change ativa: [[${CHANGES_DIR}/${slug}/proposta]]` : '';
|
|
137
159
|
}
|
|
138
160
|
|
|
161
|
+
// Append fix tasks for surviving mutants to a change's tarefas.md (Wave B). Deduped by
|
|
162
|
+
// file:line, numbered M.<n> continuing from any existing fix tasks. Returns count added.
|
|
163
|
+
export function appendFixTasks(changeDir, mutants, sensorId) {
|
|
164
|
+
const path = join(changeDir, 'tarefas.md');
|
|
165
|
+
let md = '';
|
|
166
|
+
try { md = readFileSync(path, 'utf8'); } catch { /* nova */ }
|
|
167
|
+
const existing = new Set([...md.matchAll(/mata mutante (\S+):(\d+)/g)].map((m) => `${m[1]}:${m[2]}`));
|
|
168
|
+
const nums = [...md.matchAll(/^-\s+\[[ x]\]\s+M\.(\d+)\b/gm)].map((m) => Number(m[1]));
|
|
169
|
+
let n = nums.length ? Math.max(...nums) : 0;
|
|
170
|
+
const lines = [];
|
|
171
|
+
for (const mut of mutants || []) {
|
|
172
|
+
const key = `${mut.file}:${mut.line}`;
|
|
173
|
+
if (existing.has(key)) continue;
|
|
174
|
+
existing.add(key);
|
|
175
|
+
n += 1;
|
|
176
|
+
lines.push(`- [ ] M.${n} mata mutante ${mut.file}:${mut.line} (${mut.mutator}) [sensor:${sensorId}]`);
|
|
177
|
+
}
|
|
178
|
+
if (!lines.length) return 0;
|
|
179
|
+
const sep = md === '' || md.endsWith('\n') ? '' : '\n';
|
|
180
|
+
writeFileSync(path, `${md}${sep}${lines.join('\n')}\n`, 'utf8');
|
|
181
|
+
return lines.length;
|
|
182
|
+
}
|
|
183
|
+
|
|
139
184
|
// Gate seam (Pilar B stub; Pilar C replaces with real sensor evidence).
|
|
140
185
|
export function gateGreen() {
|
|
141
186
|
return { ok: true, failing: [] };
|
|
@@ -147,6 +192,23 @@ export function archiveChange(vaultBase, slug, { gate = gateGreen, dateStr, adrN
|
|
|
147
192
|
if (!verdict.ok) return { ok: false, failing: verdict.failing || [] };
|
|
148
193
|
|
|
149
194
|
const destRel = join(CHANGES_DIR, ARCHIVE_DIR, `${dateStr}-${slug}`);
|
|
195
|
+
const changeWikilink = wikilinkFromRel(join(destRel, 'proposta'));
|
|
196
|
+
|
|
197
|
+
// Promote spec deltas into the living 07-Specs BEFORE moving (deltas live in src).
|
|
198
|
+
let promoted = [];
|
|
199
|
+
let specWarnings = [];
|
|
200
|
+
try {
|
|
201
|
+
const specs = parseSpecsList(readFileSync(join(src, 'proposta.md'), 'utf8'));
|
|
202
|
+
if (specs.length) {
|
|
203
|
+
const res = promoteSpecs(vaultBase, src, specs, { changeWikilink, dateStr });
|
|
204
|
+
promoted = res.promoted;
|
|
205
|
+
specWarnings = res.warnings;
|
|
206
|
+
}
|
|
207
|
+
} catch { /* proposta ilegível — segue só com ADR */ }
|
|
208
|
+
|
|
209
|
+
let reqIds = [];
|
|
210
|
+
try { reqIds = [...new Set(parseTasks(readFileSync(join(src, 'tarefas.md'), 'utf8')).map((t) => t.req).filter(Boolean))]; } catch { /* sem tarefas */ }
|
|
211
|
+
|
|
150
212
|
ensureDir(join(vaultBase, CHANGES_DIR, ARCHIVE_DIR));
|
|
151
213
|
renameSync(src, join(vaultBase, destRel));
|
|
152
214
|
|
|
@@ -155,7 +217,10 @@ export function archiveChange(vaultBase, slug, { gate = gateGreen, dateStr, adrN
|
|
|
155
217
|
ensureDir(join(vaultBase, adrDirRel));
|
|
156
218
|
const num = String(adrNum).padStart(3, '0');
|
|
157
219
|
const adrRel = join(adrDirRel, `ADR-${num}-${slug}.md`);
|
|
158
|
-
const
|
|
220
|
+
const capLine = promoted.length
|
|
221
|
+
? `\n\nCapabilities: ${promoted.map((c) => wikilinkFromRel(join(SPECS_DIR, c))).join(', ')}.`
|
|
222
|
+
: '';
|
|
223
|
+
const reqLine = reqIds.length ? `\n\nRequisitos: ${reqIds.join(', ')}.` : '';
|
|
159
224
|
writeFileSync(join(vaultBase, adrRel), `---
|
|
160
225
|
type: decision
|
|
161
226
|
status: accepted
|
|
@@ -170,9 +235,9 @@ tags:
|
|
|
170
235
|
|
|
171
236
|
## Decisão
|
|
172
237
|
|
|
173
|
-
Mudança ${changeWikilink} concluída e arquivada
|
|
238
|
+
Mudança ${changeWikilink} concluída e arquivada.${capLine}${reqLine}
|
|
174
239
|
`, 'utf8');
|
|
175
240
|
|
|
176
241
|
clearActiveChange(vaultBase);
|
|
177
|
-
return { ok: true, failing: [], archivedRel: destRel, adrRel };
|
|
242
|
+
return { ok: true, failing: [], archivedRel: destRel, adrRel, promoted, specWarnings };
|
|
178
243
|
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// hooks/harness-doctor.mjs — integrity checks for the a2 harness state (Wave B).
|
|
2
|
+
// Pure-ish (fs reads only). `wendkeep doctor` reports errors (exit 1) + warnings.
|
|
3
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { activeChange, parseTasks } from './change-core.mjs';
|
|
6
|
+
import { parseRequirements, parseSpecsList, parseDelta, evaluateVerdict } from './spec-core.mjs';
|
|
7
|
+
|
|
8
|
+
const CHANGES_DIR = '08-Mudanças';
|
|
9
|
+
const SPECS_DIR = '07-Specs';
|
|
10
|
+
|
|
11
|
+
export function checkHarness(vaultBase, projectRoot) {
|
|
12
|
+
const errors = [];
|
|
13
|
+
const warnings = [];
|
|
14
|
+
|
|
15
|
+
// 1. wendkeep.sensors.json well-formed.
|
|
16
|
+
const sensorsPath = join(projectRoot, 'wendkeep.sensors.json');
|
|
17
|
+
if (existsSync(sensorsPath)) {
|
|
18
|
+
try {
|
|
19
|
+
const data = JSON.parse(readFileSync(sensorsPath, 'utf8'));
|
|
20
|
+
if (!Array.isArray(data.sensors)) errors.push('wendkeep.sensors.json: "sensors" não é lista');
|
|
21
|
+
else for (const s of data.sensors) if (!s.id || !s.command) errors.push(`sensor sem id/command: ${JSON.stringify(s.id || '?')}`);
|
|
22
|
+
} catch { errors.push('wendkeep.sensors.json: JSON inválido'); }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Known requirement ids across the living specs (+ spec-without-source warning).
|
|
26
|
+
const knownReqs = new Set();
|
|
27
|
+
try {
|
|
28
|
+
for (const f of readdirSync(join(vaultBase, SPECS_DIR))) {
|
|
29
|
+
if (!f.endsWith('.md') || f === 'README.md') continue;
|
|
30
|
+
const md = readFileSync(join(vaultBase, SPECS_DIR, f), 'utf8');
|
|
31
|
+
for (const r of parseRequirements(md)) if (r.id) knownReqs.add(r.id);
|
|
32
|
+
if (!/^>\s+Atualizado por/m.test(md)) warnings.push(`spec sem origem: ${SPECS_DIR}/${f}`);
|
|
33
|
+
}
|
|
34
|
+
} catch { /* sem 07-Specs */ }
|
|
35
|
+
|
|
36
|
+
// 2/3. Changes: malformed dirs; the active change's deltas add to knownReqs.
|
|
37
|
+
const active = activeChange(vaultBase);
|
|
38
|
+
let names = [];
|
|
39
|
+
try { names = readdirSync(join(vaultBase, CHANGES_DIR)).filter((n) => n !== '_arquivo'); } catch { /* none */ }
|
|
40
|
+
for (const name of names) {
|
|
41
|
+
const dir = join(vaultBase, CHANGES_DIR, name);
|
|
42
|
+
let entries;
|
|
43
|
+
try { entries = readdirSync(dir); } catch { continue; } // a file, not a change dir
|
|
44
|
+
if (!entries.includes('proposta.md')) { errors.push(`change sem proposta.md: ${name}`); continue; }
|
|
45
|
+
if (name === active) {
|
|
46
|
+
try {
|
|
47
|
+
for (const cap of parseSpecsList(readFileSync(join(dir, 'proposta.md'), 'utf8'))) {
|
|
48
|
+
try {
|
|
49
|
+
const d = parseDelta(readFileSync(join(dir, 'specs', cap, 'spec.md'), 'utf8'));
|
|
50
|
+
for (const r of [...d.added, ...d.modified]) if (r.id) knownReqs.add(r.id);
|
|
51
|
+
} catch { /* sem delta pra essa cap */ }
|
|
52
|
+
}
|
|
53
|
+
} catch { /* proposta ilegível */ }
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// 4/5. Active-change pointer resolves; [req:] orphans; stale verdict.
|
|
58
|
+
if (active) {
|
|
59
|
+
const dir = join(vaultBase, CHANGES_DIR, active);
|
|
60
|
+
if (!existsSync(join(dir, 'proposta.md'))) {
|
|
61
|
+
errors.push(`ponteiro CURRENT_CHANGE aponta pra change inexistente: ${active}`);
|
|
62
|
+
} else {
|
|
63
|
+
let tasks = [];
|
|
64
|
+
try { tasks = parseTasks(readFileSync(join(dir, 'tarefas.md'), 'utf8')); } catch { /* sem tarefas */ }
|
|
65
|
+
const reqIds = [...new Set(tasks.map((t) => t.req).filter(Boolean))];
|
|
66
|
+
for (const rid of reqIds) if (!knownReqs.has(rid)) errors.push(`req órfão em ${active}: [req:${rid}] não existe em 07-Specs nem no delta`);
|
|
67
|
+
let verdict = null;
|
|
68
|
+
try { verdict = JSON.parse(readFileSync(join(dir, 'verdict.json'), 'utf8')); } catch { /* sem verdict */ }
|
|
69
|
+
if (verdict && reqIds.length) {
|
|
70
|
+
const v = evaluateVerdict(verdict, reqIds);
|
|
71
|
+
if (!v.ok) warnings.push(`verdict stale em ${active}: falta cobrir ${v.missing.join(', ')}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return { errors, warnings };
|
|
77
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// hooks/lessons-core.mjs — project-local lessons from verification failures (Wave B).
|
|
2
|
+
// A failure distilled into a terse lesson; brain-inject surfaces the recent ones at
|
|
3
|
+
// SessionStart so the framework gets sharper on your codebase. No external deps.
|
|
4
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
|
|
7
|
+
function slugify(s) {
|
|
8
|
+
return String(s)
|
|
9
|
+
.toLowerCase()
|
|
10
|
+
.normalize('NFD').replace(/[̀-ͯ]/g, '')
|
|
11
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
12
|
+
.replace(/^-+|-+$/g, '')
|
|
13
|
+
.slice(0, 50) || 'licao';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function addLesson(vaultBase, { trigger, lesson, sourceChange = '', dateStr = '' }) {
|
|
17
|
+
const dir = join(vaultBase, '.brain', 'lessons');
|
|
18
|
+
mkdirSync(dir, { recursive: true });
|
|
19
|
+
const path = join(dir, `${dateStr ? `${dateStr}-` : ''}${slugify(trigger)}.md`);
|
|
20
|
+
writeFileSync(
|
|
21
|
+
path,
|
|
22
|
+
`---\ntype: lesson\ntrigger: ${JSON.stringify(String(trigger))}\nsource: ${sourceChange}\ndate: ${dateStr}\n---\n\n${lesson}\n`,
|
|
23
|
+
'utf8',
|
|
24
|
+
);
|
|
25
|
+
return path;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Compact <lessons> block of the most recent lessons (filename-sorted desc; a date prefix
|
|
29
|
+
// makes that chronological). Budget-capped by `max`. '' when there are none.
|
|
30
|
+
export function buildLessonsInjection(vaultBase, { max = 5 } = {}) {
|
|
31
|
+
const dir = join(vaultBase, '.brain', 'lessons');
|
|
32
|
+
let files;
|
|
33
|
+
try { files = readdirSync(dir).filter((f) => f.endsWith('.md')).sort().reverse().slice(0, max); }
|
|
34
|
+
catch { return ''; }
|
|
35
|
+
const lines = [];
|
|
36
|
+
for (const f of files) {
|
|
37
|
+
try {
|
|
38
|
+
const body = readFileSync(join(dir, f), 'utf8').replace(/^---[\s\S]*?---\n/, '').trim().split('\n')[0];
|
|
39
|
+
if (body) lines.push(`- ${body}`);
|
|
40
|
+
} catch { /* skip */ }
|
|
41
|
+
}
|
|
42
|
+
return lines.length ? `<lessons>\nLições do projeto (de falhas anteriores):\n${lines.join('\n')}\n</lessons>` : '';
|
|
43
|
+
}
|
package/hooks/sensors-core.mjs
CHANGED
|
@@ -24,15 +24,45 @@ export function runSensors(sensors, ids, { spawn = spawnSync, cwd, now } = {}) {
|
|
|
24
24
|
const evidence = [];
|
|
25
25
|
for (const id of ids) {
|
|
26
26
|
const s = byId[id];
|
|
27
|
-
if (!s) { evidence.push({ id, status: 'red', ts, note: 'sensor não definido' }); continue; }
|
|
27
|
+
if (!s) { evidence.push({ id, status: 'red', ts, severity: 'critical', note: 'sensor não definido' }); continue; }
|
|
28
28
|
const r = spawn(s.command, [], { cwd, shell: true, stdio: 'ignore' });
|
|
29
|
-
|
|
29
|
+
const entry = { id, status: (r.status ?? 1) === 0 ? 'green' : 'red', ts, severity: s.severity || 'critical' };
|
|
30
|
+
if (s.type === 'mutation' && s.report) {
|
|
31
|
+
// Delegated mutation (Wave B): read the tool's mutation-testing-elements report and
|
|
32
|
+
// attach surviving mutants so verify can turn them into fix tasks.
|
|
33
|
+
try { entry.survivors = parseMutationReport(JSON.parse(readFileSync(join(cwd || '.', s.report), 'utf8'))); }
|
|
34
|
+
catch { /* report ausente/ilegível — segue só com o exit code */ }
|
|
35
|
+
}
|
|
36
|
+
evidence.push(entry);
|
|
30
37
|
}
|
|
31
38
|
return evidence;
|
|
32
39
|
}
|
|
33
40
|
|
|
41
|
+
// Parse a mutation-testing-elements report (Stryker et al.): return surviving mutants
|
|
42
|
+
// (Survived | NoCoverage) as {file, line, mutator}.
|
|
43
|
+
export function parseMutationReport(json) {
|
|
44
|
+
const out = [];
|
|
45
|
+
const files = json && json.files ? json.files : {};
|
|
46
|
+
for (const [file, data] of Object.entries(files)) {
|
|
47
|
+
for (const m of (data && data.mutants) || []) {
|
|
48
|
+
if (m.status === 'Survived' || m.status === 'NoCoverage') {
|
|
49
|
+
out.push({ file, line: m.location && m.location.start ? m.location.start.line : null, mutator: m.mutatorName || 'unknown' });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// A required sensor blocks the gate when it is missing (never verified) or red at a
|
|
57
|
+
// non-warning severity. Warnings are advisory: a red warning does not block archive.
|
|
58
|
+
// Severity comes from the evidence entry (written by runSensors); absent -> critical.
|
|
34
59
|
export function evaluateGate(evidence, requiredIds) {
|
|
35
|
-
const byId = Object.fromEntries((evidence || []).map((e) => [e.id, e
|
|
36
|
-
const failing = (requiredIds || []).filter((id) =>
|
|
60
|
+
const byId = Object.fromEntries((evidence || []).map((e) => [e.id, e]));
|
|
61
|
+
const failing = (requiredIds || []).filter((id) => {
|
|
62
|
+
const e = byId[id];
|
|
63
|
+
if (!e) return true; // never verified
|
|
64
|
+
if (e.status === 'green') return false;
|
|
65
|
+
return (e.severity || 'critical') !== 'warning';
|
|
66
|
+
});
|
|
37
67
|
return { ok: failing.length === 0, failing };
|
|
38
68
|
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// hooks/spec-core.mjs — living spec (07-Specs) + change delta merge (OpenSpec native).
|
|
2
|
+
// Pure parsing/merge + promoteSpecs (fs). No import from change-core (avoids a cycle).
|
|
3
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { ensureDir } from './obsidian-common.mjs';
|
|
6
|
+
|
|
7
|
+
const SPECS_DIR = '07-Specs';
|
|
8
|
+
const REQ_RE = /^### Requisito:\s*(.+)$/gm;
|
|
9
|
+
|
|
10
|
+
export function parseRequirements(md) {
|
|
11
|
+
const text = String(md);
|
|
12
|
+
const matches = [...text.matchAll(REQ_RE)];
|
|
13
|
+
const reqs = [];
|
|
14
|
+
for (let i = 0; i < matches.length; i += 1) {
|
|
15
|
+
const raw = matches[i][1].trim();
|
|
16
|
+
// Identity is the ID (e.g. GATE-1) when the heading is "<ID> — <nome>"; else the whole text.
|
|
17
|
+
const idM = raw.match(/^([A-Z][A-Z0-9]*-\d+)\s*—\s*(.+)$/);
|
|
18
|
+
const id = idM ? idM[1] : null;
|
|
19
|
+
const name = idM ? idM[2].trim() : raw;
|
|
20
|
+
const start = matches[i].index + matches[i][0].length;
|
|
21
|
+
const end = i + 1 < matches.length ? matches[i + 1].index : text.length;
|
|
22
|
+
const body = text.slice(start, end).replace(/(\n>[^\n]*)+\s*$/, '').trim();
|
|
23
|
+
reqs.push({ id, name, body });
|
|
24
|
+
}
|
|
25
|
+
return reqs;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function parseDelta(md) {
|
|
29
|
+
const text = String(md);
|
|
30
|
+
const grab = (label) => {
|
|
31
|
+
const m = text.match(new RegExp(`^##\\s+${label} Requirements\\s*$`, 'm'));
|
|
32
|
+
if (!m) return '';
|
|
33
|
+
const rest = text.slice(m.index + m[0].length);
|
|
34
|
+
const next = rest.search(/^##\s+\w+ Requirements\s*$/m);
|
|
35
|
+
return next === -1 ? rest : rest.slice(0, next);
|
|
36
|
+
};
|
|
37
|
+
return {
|
|
38
|
+
added: parseRequirements(grab('ADDED')),
|
|
39
|
+
modified: parseRequirements(grab('MODIFIED')),
|
|
40
|
+
removed: parseRequirements(grab('REMOVED')).map((r) => r.id || r.name),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function applyDelta(reqs, delta) {
|
|
45
|
+
const keyOf = (r) => r.id || r.name; // identity = ID when present, else the name
|
|
46
|
+
const order = reqs.map(keyOf);
|
|
47
|
+
const map = new Map(reqs.map((r) => [keyOf(r), r])); // key -> full {id,name,body}
|
|
48
|
+
const warnings = [];
|
|
49
|
+
for (const r of delta.added || []) {
|
|
50
|
+
const k = keyOf(r);
|
|
51
|
+
if (map.has(k)) warnings.push(`ADDED já existe: ${k}`); else order.push(k);
|
|
52
|
+
map.set(k, r);
|
|
53
|
+
}
|
|
54
|
+
for (const r of delta.modified || []) {
|
|
55
|
+
const k = keyOf(r);
|
|
56
|
+
if (!map.has(k)) { warnings.push(`MODIFIED inexistente: ${k}`); order.push(k); }
|
|
57
|
+
map.set(k, r);
|
|
58
|
+
}
|
|
59
|
+
for (const k of delta.removed || []) {
|
|
60
|
+
if (!map.has(k)) warnings.push(`REMOVED inexistente: ${k}`);
|
|
61
|
+
map.delete(k);
|
|
62
|
+
}
|
|
63
|
+
const seen = new Set();
|
|
64
|
+
const out = order.filter((k) => map.has(k) && !seen.has(k) && seen.add(k)).map((k) => map.get(k));
|
|
65
|
+
return { reqs: out, warnings };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function renderSpec(capability, reqs, { footer } = {}) {
|
|
69
|
+
const blocks = reqs.map((r) => `### Requisito: ${r.id ? `${r.id} — ${r.name}` : r.name}\n${r.body}`).join('\n\n');
|
|
70
|
+
const foot = footer ? `\n\n> ${footer}\n` : '\n';
|
|
71
|
+
return `---\ntype: spec\ncssclasses:\n - topic-spec\ntags:\n - spec\n---\n\n# ${capability}\n\n## Requisitos\n\n${blocks}${foot}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function parseSpecsList(propostaMd) {
|
|
75
|
+
const text = String(propostaMd);
|
|
76
|
+
const inline = text.match(/^specs:\s*\[(.*?)\]\s*$/m);
|
|
77
|
+
if (inline) return inline[1].split(',').map((s) => s.trim().replace(/['"]/g, '')).filter(Boolean);
|
|
78
|
+
const block = text.match(/^specs:\s*\n((?:[ \t]+-[ \t]*.+\n?)+)/m);
|
|
79
|
+
if (block) return block[1].split('\n').map((l) => l.replace(/^[ \t]*-[ \t]*/, '').trim().replace(/['"]/g, '')).filter(Boolean);
|
|
80
|
+
return [];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Merge each capability's delta (in the change) into the living spec in 07-Specs.
|
|
84
|
+
export function promoteSpecs(vaultBase, changeDir, specs, { changeWikilink, dateStr } = {}) {
|
|
85
|
+
const promoted = [];
|
|
86
|
+
const warnings = [];
|
|
87
|
+
for (const cap of specs) {
|
|
88
|
+
let delta;
|
|
89
|
+
try { delta = parseDelta(readFileSync(join(changeDir, 'specs', cap, 'spec.md'), 'utf8')); }
|
|
90
|
+
catch { warnings.push(`sem delta para ${cap}`); continue; }
|
|
91
|
+
const livePath = join(vaultBase, SPECS_DIR, `${cap}.md`);
|
|
92
|
+
let current = [];
|
|
93
|
+
try { current = parseRequirements(readFileSync(livePath, 'utf8')); } catch { /* nova capability */ }
|
|
94
|
+
const applied = applyDelta(current, delta);
|
|
95
|
+
warnings.push(...applied.warnings.map((w) => `${cap}: ${w}`));
|
|
96
|
+
ensureDir(join(vaultBase, SPECS_DIR));
|
|
97
|
+
const footer = changeWikilink ? `Atualizado por ${changeWikilink} em ${dateStr}.` : '';
|
|
98
|
+
writeFileSync(livePath, renderSpec(cap, applied.reqs, { footer }), 'utf8');
|
|
99
|
+
promoted.push(cap);
|
|
100
|
+
}
|
|
101
|
+
return { promoted, warnings };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Gate check for the independent verdict (Wave A). A requirement-bearing change must have
|
|
105
|
+
// a verdict that is ok and covers every declared req id. A requirement-less change passes:
|
|
106
|
+
// nothing for an independent verifier to check — the sensor gate is already the proof.
|
|
107
|
+
export function evaluateVerdict(verdict, reqIds) {
|
|
108
|
+
const ids = reqIds || [];
|
|
109
|
+
if (ids.length === 0) return { ok: true, missing: [] };
|
|
110
|
+
if (!verdict || verdict.ok !== true) return { ok: false, missing: [] };
|
|
111
|
+
const covered = new Set((verdict.coverage || []).filter((c) => c.covered).map((c) => c.req));
|
|
112
|
+
const missing = ids.filter((r) => !covered.has(r));
|
|
113
|
+
return { ok: missing.length === 0, missing };
|
|
114
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wendkeep",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Automatically capture AI coding agent sessions (Claude Code, Codex) as local Markdown in your Obsidian vault — turn-by-turn history, cost/token tracking, auto-extracted decisions/bugs/learnings, rendered in the graph. Local-first.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
"bin",
|
|
12
12
|
"src",
|
|
13
13
|
"hooks",
|
|
14
|
-
"README.md"
|
|
14
|
+
"README.md",
|
|
15
|
+
"CHANGELOG.md"
|
|
15
16
|
],
|
|
16
17
|
"engines": {
|
|
17
18
|
"node": ">=18"
|
|
@@ -32,5 +33,13 @@
|
|
|
32
33
|
"token-cost"
|
|
33
34
|
],
|
|
34
35
|
"author": "Roger Alves",
|
|
35
|
-
"license": "MIT"
|
|
36
|
+
"license": "MIT",
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "git+https://github.com/rogersialves/wendkeep.git"
|
|
40
|
+
},
|
|
41
|
+
"homepage": "https://github.com/rogersialves/wendkeep#readme",
|
|
42
|
+
"bugs": {
|
|
43
|
+
"url": "https://github.com/rogersialves/wendkeep/issues"
|
|
44
|
+
}
|
|
36
45
|
}
|
package/src/change.mjs
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
archiveChange,
|
|
10
10
|
} from '../hooks/change-core.mjs';
|
|
11
11
|
import { evaluateGate, requiredSensors } from '../hooks/sensors-core.mjs';
|
|
12
|
+
import { evaluateVerdict } from '../hooks/spec-core.mjs';
|
|
12
13
|
import { getNextAdrNumber } from '../hooks/obsidian-common.mjs';
|
|
13
14
|
|
|
14
15
|
function resolveVault(argv) {
|
|
@@ -39,7 +40,7 @@ export function runChange(argv) {
|
|
|
39
40
|
if (sub === 'new') {
|
|
40
41
|
const slug = slugArg();
|
|
41
42
|
if (!slug) { process.stderr.write('wendkeep change new: missing <slug>\n'); process.exit(2); }
|
|
42
|
-
const r = newChange(vaultBase, slug, { dateStr: today() });
|
|
43
|
+
const r = newChange(vaultBase, slug, { dateStr: today(), simple: rest.includes('--simple') });
|
|
43
44
|
process.stdout.write(`change ${r.created ? 'created' : 'exists'}: ${r.rel} (active)\n`);
|
|
44
45
|
process.exit(0);
|
|
45
46
|
}
|
|
@@ -70,18 +71,29 @@ export function runChange(argv) {
|
|
|
70
71
|
if (!slug) { process.stderr.write('wendkeep change archive: missing <slug> and no active change\n'); process.exit(2); }
|
|
71
72
|
// Real gate (Pilar C): every sensor a task declared must be green in evidencia.json.
|
|
72
73
|
const gate = (dir) => {
|
|
73
|
-
let
|
|
74
|
-
try {
|
|
74
|
+
let tasks = [];
|
|
75
|
+
try { tasks = parseTasks(readFileSync(join(dir, 'tarefas.md'), 'utf8')); } catch { /* no tasks */ }
|
|
76
|
+
const required = requiredSensors(tasks);
|
|
77
|
+
const reqIds = [...new Set(tasks.map((t) => t.req).filter(Boolean))];
|
|
75
78
|
let evidence = [];
|
|
76
79
|
try { evidence = JSON.parse(readFileSync(join(dir, 'evidencia.json'), 'utf8')); } catch { /* no evidence */ }
|
|
77
|
-
|
|
80
|
+
const s = evaluateGate(evidence, required);
|
|
81
|
+
if (!s.ok) return s;
|
|
82
|
+
// Independent verdict (Wave A): required only when the change declares [req:] tasks.
|
|
83
|
+
let verdict = null;
|
|
84
|
+
try { verdict = JSON.parse(readFileSync(join(dir, 'verdict.json'), 'utf8')); } catch { /* none */ }
|
|
85
|
+
const v = evaluateVerdict(verdict, reqIds);
|
|
86
|
+
if (!v.ok) return { ok: false, failing: verdict ? [`verdict incompleto: falta ${v.missing.join(', ')}`] : ['sem verdict — rode `wendkeep verify --deep` + skill wk-verify'] };
|
|
87
|
+
return { ok: true, failing: [] };
|
|
78
88
|
};
|
|
79
89
|
const r = archiveChange(vaultBase, slug, { dateStr: today(), adrNum: getNextAdrNumber(vaultBase), gate });
|
|
80
90
|
if (!r.ok) {
|
|
81
|
-
process.stderr.write(`change archive BLOCKED (gate):
|
|
91
|
+
process.stderr.write(`change archive BLOCKED (gate): ${r.failing.join('; ')}\n`);
|
|
82
92
|
process.exit(1);
|
|
83
93
|
}
|
|
84
94
|
process.stdout.write(`archived: ${r.archivedRel}; ADR: ${r.adrRel}\n`);
|
|
95
|
+
if (r.promoted && r.promoted.length) process.stdout.write(`specs promovidas: ${r.promoted.join(', ')}\n`);
|
|
96
|
+
if (r.specWarnings && r.specWarnings.length) for (const w of r.specWarnings) process.stderr.write(` aviso spec: ${w}\n`);
|
|
85
97
|
process.exit(0);
|
|
86
98
|
}
|
|
87
99
|
|
package/src/doctor.mjs
CHANGED
|
@@ -1,36 +1,48 @@
|
|
|
1
|
-
// `wendkeep doctor` —
|
|
2
|
-
//
|
|
3
|
-
// hooks/vault-health.mjs (which reads getVaultBase()).
|
|
1
|
+
// `wendkeep doctor` — vault/session integrity (hooks/vault-health.mjs) PLUS the a2
|
|
2
|
+
// harness integrity check (hooks/harness-doctor.mjs). Exits 1 on any error.
|
|
4
3
|
import { spawnSync } from 'node:child_process';
|
|
5
4
|
import { existsSync } from 'node:fs';
|
|
6
5
|
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
7
6
|
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { checkHarness } from '../hooks/harness-doctor.mjs';
|
|
8
8
|
|
|
9
9
|
export function runDoctor(argv) {
|
|
10
10
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
11
11
|
const hookFile = join(here, '..', 'hooks', 'vault-health.mjs');
|
|
12
|
-
if (!existsSync(hookFile)) {
|
|
13
|
-
process.stderr.write(`wendkeep doctor: vault-health.mjs not found at ${hookFile}\n`);
|
|
14
|
-
process.exit(2);
|
|
15
|
-
}
|
|
16
12
|
|
|
17
13
|
let vault;
|
|
14
|
+
let project;
|
|
18
15
|
const passthrough = [];
|
|
19
16
|
for (let i = 0; i < argv.length; i += 1) {
|
|
20
17
|
const a = argv[i];
|
|
21
18
|
if (a === '--vault') vault = argv[++i];
|
|
22
19
|
else if (a.startsWith('--vault=')) vault = a.slice(8);
|
|
20
|
+
else if (a === '--project') project = argv[++i];
|
|
21
|
+
else if (a.startsWith('--project=')) project = a.slice(10);
|
|
23
22
|
else passthrough.push(a);
|
|
24
23
|
}
|
|
25
24
|
|
|
26
25
|
const env = { ...process.env };
|
|
27
26
|
if (vault) env.OBSIDIAN_VAULT_PATH = isAbsolute(vault) ? vault : resolve(process.cwd(), vault);
|
|
28
|
-
|
|
29
27
|
if (!env.OBSIDIAN_VAULT_PATH) {
|
|
30
28
|
process.stderr.write('wendkeep doctor: no vault. Pass --vault <path> or set OBSIDIAN_VAULT_PATH.\n');
|
|
31
29
|
process.exit(2);
|
|
32
30
|
}
|
|
31
|
+
const vaultBase = env.OBSIDIAN_VAULT_PATH;
|
|
32
|
+
const projectRoot = resolve(project || process.cwd());
|
|
33
|
+
|
|
34
|
+
// 1. Session/vault integrity (existing check).
|
|
35
|
+
let healthStatus = 0;
|
|
36
|
+
if (existsSync(hookFile)) {
|
|
37
|
+
const r = spawnSync(process.execPath, [hookFile, ...passthrough], { stdio: 'inherit', env });
|
|
38
|
+
healthStatus = r.status ?? 0;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// 2. Harness integrity (Wave B).
|
|
42
|
+
const { errors, warnings } = checkHarness(vaultBase, projectRoot);
|
|
43
|
+
process.stdout.write(`\n[harness] ${errors.length} erro(s), ${warnings.length} aviso(s)\n`);
|
|
44
|
+
for (const e of errors) process.stdout.write(` ✗ ${e}\n`);
|
|
45
|
+
for (const w of warnings) process.stdout.write(` ! ${w}\n`);
|
|
33
46
|
|
|
34
|
-
|
|
35
|
-
process.exit(r.status ?? 0);
|
|
47
|
+
process.exit(healthStatus !== 0 || errors.length ? 1 : 0);
|
|
36
48
|
}
|
package/src/init.mjs
CHANGED
|
@@ -31,7 +31,7 @@ import {
|
|
|
31
31
|
mergeGraphColorGroups,
|
|
32
32
|
} from './vault-theme.mjs';
|
|
33
33
|
import { renderCoreSkeleton, renderCompactionProtocol } from './validate-core.mjs';
|
|
34
|
-
import { seedDefinitions } from './sync-defs.mjs';
|
|
34
|
+
import { seedDefinitions, syncDefs } from './sync-defs.mjs';
|
|
35
35
|
import { seedWkSkills } from './skills-seed.mjs';
|
|
36
36
|
import { seedDotcontext, globalHasDotcontext, resolveDotcontextSkipMcp, renderSensorsJson } from './dotcontext-seed.mjs';
|
|
37
37
|
|
|
@@ -295,6 +295,12 @@ export async function runInit(argv) {
|
|
|
295
295
|
writeFileSync(sensorsFile, renderSensorsJson(scripts), 'utf8');
|
|
296
296
|
}
|
|
297
297
|
log(` [1/4] vault taxonomy: ${VAULT_FOLDERS.length} folders (${created} created)${readmeNote}, .brain + change/spec + sensors seeded`);
|
|
298
|
+
// Deliver the seeded defs (agents + wk process skills) to the project so they're
|
|
299
|
+
// usable immediately — no separate `wendkeep sync-defs` step needed.
|
|
300
|
+
const synced = syncDefs(vaultPath, projectPath);
|
|
301
|
+
if (synced.skills.length || synced.agents.length) {
|
|
302
|
+
log(` defs delivered: ${synced.skills.length} skill(s) -> .claude/skills, ${synced.agents.length} agent(s) -> .codex/agents`);
|
|
303
|
+
}
|
|
298
304
|
|
|
299
305
|
// 2. .claude/settings.json --------------------------------------------------
|
|
300
306
|
const settingsPath = join(projectPath, '.claude', 'settings.json');
|
package/src/lessons.mjs
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// `wendkeep lesson add "<trigger>" "<lesson>"` — record a project-local lesson (Wave B).
|
|
2
|
+
// The wk-verify / wk-debugging skills call this when a verification fails.
|
|
3
|
+
import { isAbsolute, resolve } from 'node:path';
|
|
4
|
+
import { addLesson } from '../hooks/lessons-core.mjs';
|
|
5
|
+
|
|
6
|
+
function opt(argv, name) {
|
|
7
|
+
const i = argv.indexOf(name);
|
|
8
|
+
if (i >= 0) return argv[i + 1];
|
|
9
|
+
const eq = argv.find((a) => a.startsWith(`${name}=`));
|
|
10
|
+
return eq ? eq.slice(name.length + 1) : undefined;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function today() {
|
|
14
|
+
const d = new Date();
|
|
15
|
+
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function runLesson(argv) {
|
|
19
|
+
const [sub, ...rest] = argv;
|
|
20
|
+
if (sub !== 'add') {
|
|
21
|
+
process.stderr.write(`wendkeep lesson: unknown "${sub}". Use: lesson add "<trigger>" "<lesson>"\n`);
|
|
22
|
+
process.exit(2);
|
|
23
|
+
}
|
|
24
|
+
const vaultRaw = opt(rest, '--vault') || process.env.OBSIDIAN_VAULT_PATH;
|
|
25
|
+
if (!vaultRaw) { process.stderr.write('wendkeep lesson: no vault (--vault or OBSIDIAN_VAULT_PATH).\n'); process.exit(2); }
|
|
26
|
+
const vaultBase = isAbsolute(vaultRaw) ? vaultRaw : resolve(process.cwd(), vaultRaw);
|
|
27
|
+
|
|
28
|
+
const positional = rest.filter((a, i) => !a.startsWith('--') && rest[i - 1] !== '--vault' && rest[i - 1] !== '--change');
|
|
29
|
+
const [trigger, lesson] = positional;
|
|
30
|
+
if (!trigger || !lesson) { process.stderr.write('wendkeep lesson add: precisa "<trigger>" "<lesson>"\n'); process.exit(2); }
|
|
31
|
+
|
|
32
|
+
const p = addLesson(vaultBase, { trigger, lesson, sourceChange: opt(rest, '--change') || '', dateStr: today() });
|
|
33
|
+
process.stdout.write(`lesson: ${p}\n`);
|
|
34
|
+
process.exit(0);
|
|
35
|
+
}
|
package/src/skills-seed.mjs
CHANGED
|
@@ -23,14 +23,19 @@ Use ao começar qualquer mudança não-trivial. O loop mantém memória (vault)
|
|
|
23
23
|
A mudança vira a *ativa* (ponteiro \`.brain/CURRENT_CHANGE.md\`) e é injetada no
|
|
24
24
|
próximo SessionStart, então você retoma o trabalho em curso automaticamente.
|
|
25
25
|
3. **Apply** — implemente cada tarefa de \`tarefas.md\` com disciplina **wk-tdd**
|
|
26
|
-
(teste vermelho antes do código). Marque \`- [x]\` ao concluir
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
26
|
+
(teste vermelho antes do código). Marque \`- [x]\` ao concluir. Declare nas tarefas:
|
|
27
|
+
- \`[sensor:<id>]\` — a prova automatizada (roda no verify).
|
|
28
|
+
- \`[req:<ID>]\` — o requisito do spec que a tarefa satisfaz (ex.: \`[req:GATE-1]\`),
|
|
29
|
+
quando a change mexe numa capability de \`07-Specs\`.
|
|
30
|
+
Ex.: \`- [ ] 2.1 valida CORE [req:MEM-1] [sensor:memory-validation]\`.
|
|
31
|
+
4. **Verify** — \`wendkeep verify\` roda os sensores → \`evidencia.json\`. Depois
|
|
32
|
+
\`wendkeep verify --deep\` monta o *pacote de verificação* pro passe independente.
|
|
33
|
+
5. **Verify deep** — a skill **wk-verify** (passe fresco, autor≠verificador) lê o pacote,
|
|
34
|
+
re-deriva a cobertura do \`07-Specs\` e grava \`verdict.json\`. Change trivial (sem
|
|
35
|
+
\`[req:]\`) recebe verdict automático — pula este passe.
|
|
36
|
+
6. **Archive** — \`wendkeep change archive <slug>\`. O *gate* exige sensores verdes **E**
|
|
37
|
+
\`verdict.json\` cobrindo os \`[req:]\`. Passando, promove os deltas pro \`07-Specs\`,
|
|
38
|
+
move a change pro \`_arquivo\` e gera um ADR em \`04-Decisões/\`.
|
|
34
39
|
|
|
35
40
|
## Regras
|
|
36
41
|
|
|
@@ -41,25 +46,46 @@ Use ao começar qualquer mudança não-trivial. O loop mantém memória (vault)
|
|
|
41
46
|
o grafo do Obsidian mostra plano↔sessão↔decisão.
|
|
42
47
|
`;
|
|
43
48
|
|
|
44
|
-
const TDD = `# TDD — Red, Green, Refactor
|
|
49
|
+
const TDD = `# TDD — Red, Green, Refactor (testes que discriminam)
|
|
45
50
|
|
|
46
|
-
Use ao implementar qualquer comportamento. Nunca escreva código de produção sem um
|
|
47
|
-
|
|
51
|
+
Use ao implementar qualquer comportamento. Nunca escreva código de produção sem um teste
|
|
52
|
+
vermelho pedindo por ele — e nunca escreva um teste que passaria sob a implementação errada.
|
|
48
53
|
|
|
49
54
|
## O ciclo
|
|
50
55
|
|
|
51
|
-
1. **Red** —
|
|
52
|
-
2. **Rode e veja falhar** —
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
+
1. **Red** — o menor teste que falha por faltar o comportamento.
|
|
57
|
+
2. **Rode e veja falhar** — pelo motivo certo (não import/typo). Teste que nunca falhou não prova nada.
|
|
58
|
+
3. **Green** — o código mínimo pra passar.
|
|
59
|
+
4. **Refactor** — limpe com os verdes te protegendo.
|
|
60
|
+
|
|
61
|
+
## Derive do spec, não do código
|
|
62
|
+
|
|
63
|
+
Escreva a asserção a partir do *critério de aceite* do requisito (\`07-Specs\`), não lendo a
|
|
64
|
+
implementação. Cada asserção mira o resultado que o spec definiu. Escrever o teste lendo o
|
|
65
|
+
código = ele só confirma o que o código já faz, bugs inclusos.
|
|
66
|
+
|
|
67
|
+
## Litmus não-raso
|
|
68
|
+
|
|
69
|
+
Rejeite asserção que passaria sob uma implementação errada. Afirme **valor ou estado
|
|
70
|
+
persistido**, nunca "o mock foi chamado". "Testado depois" é rejeitado — o teste nasce
|
|
71
|
+
junto com o código da tarefa.
|
|
72
|
+
|
|
73
|
+
## Adequação (necessário e suficiente)
|
|
74
|
+
|
|
75
|
+
- Todo critério de aceite tem uma asserção — com evidência \`arquivo:linha\`.
|
|
76
|
+
- Todo teste rastreia um requisito (\`[req:ID]\`). Teste sem requisito = escopo à toa.
|
|
77
|
+
- Nem raso, nem inflado: cobre o que o spec pede, nada além.
|
|
78
|
+
|
|
79
|
+
## Aprende o projeto
|
|
80
|
+
|
|
81
|
+
Antes de escrever, amostre 5–10 testes existentes: estilo, localização, framework,
|
|
82
|
+
profundidade por camada — trate essa profundidade como *piso*, nunca teto. Leia
|
|
83
|
+
\`AGENTS.md\` / \`.cursor/rules\` / configs de CI pros padrões declarados.
|
|
56
84
|
|
|
57
85
|
## Regras
|
|
58
86
|
|
|
59
|
-
- Um comportamento por teste
|
|
60
|
-
-
|
|
61
|
-
- Não teste detalhe interno que você vai refatorar — teste a interface observável.
|
|
62
|
-
- Bug encontrado = primeiro um teste que o reproduz (vermelho), depois a correção (verde).
|
|
87
|
+
- Um comportamento por teste; o nome descreve o comportamento, não a implementação.
|
|
88
|
+
- Bug encontrado = primeiro o teste que o reproduz (vermelho), depois a correção.
|
|
63
89
|
`;
|
|
64
90
|
|
|
65
91
|
const DEBUGGING = `# Depuração sistemática
|
|
@@ -99,6 +125,17 @@ qualquer código.
|
|
|
99
125
|
3. **Proponha 2–3 abordagens** — com trade-offs e sua recomendação primeiro.
|
|
100
126
|
4. **Apresente o design em seções** — escale ao tamanho do problema; confirme cada seção.
|
|
101
127
|
|
|
128
|
+
## Closure gate — nada de ambiguidade solta
|
|
129
|
+
|
|
130
|
+
Antes de fechar o design, resolva cada zona cinza: decide com o usuário, ou registra como
|
|
131
|
+
**assumption assinada** ("assumo X porque Y — corrija se errado"). Cinza declinado pelo
|
|
132
|
+
usuário é *registrado*, não descartado no silêncio. Nada sai do design silenciosamente ambíguo.
|
|
133
|
+
|
|
134
|
+
## Tabela out-of-scope
|
|
135
|
+
|
|
136
|
+
Liste explicitamente o que a mudança **não** faz. Escopo não declarado vira creep; a tabela
|
|
137
|
+
é o contrato do que ficou de fora — e base pra próxima change.
|
|
138
|
+
|
|
102
139
|
## Gate rígido
|
|
103
140
|
|
|
104
141
|
NÃO escreva código, scaffold nem tome ação de implementação até apresentar um design e o
|
|
@@ -139,12 +176,38 @@ de 2–5 min:
|
|
|
139
176
|
- Nomes/assinaturas consistentes entre tarefas (uma função é \`x()\` em toda parte).
|
|
140
177
|
`;
|
|
141
178
|
|
|
179
|
+
const VERIFY = `# Verificação independente — o passe fresco
|
|
180
|
+
|
|
181
|
+
Use no passo *verify deep* do loop, depois de \`wendkeep verify --deep\`. Você é o
|
|
182
|
+
**verificador**, não o autor — mesmo tendo escrito o código, entre neste passe como se
|
|
183
|
+
nunca tivesse visto a implementação. Contexto fresco, read-only.
|
|
184
|
+
|
|
185
|
+
## O que fazer
|
|
186
|
+
|
|
187
|
+
1. Leia o pacote \`08-Mudanças/<slug>/verificacao.json\` (requisitos, tarefas, evidência).
|
|
188
|
+
2. **Re-derive a cobertura do \`07-Specs\`** — pra cada \`[req:ID]\` da change, cheque se o
|
|
189
|
+
comportamento definido no requisito está coberto por um teste que discrimina (não
|
|
190
|
+
passaria sob impl errada). Evidência \`arquivo:linha\`.
|
|
191
|
+
3. Outcome check ancorado no spec: o resultado observável bate com o critério de aceite?
|
|
192
|
+
4. Grave \`08-Mudanças/<slug>/verdict.json\`:
|
|
193
|
+
\`{ "slug": "...", "ok": true, "coverage": [{ "req": "GATE-1", "covered": true, "evidence": "arquivo:linha" }], "notes": [] }\`.
|
|
194
|
+
|
|
195
|
+
## Regras
|
|
196
|
+
|
|
197
|
+
- **Autor ≠ verificador.** No Claude, spawn um sub-agente read-only pra este passe
|
|
198
|
+
(isolamento real). Nos outros, entre num contexto limpo e re-derive do spec, não da memória.
|
|
199
|
+
- \`ok: false\` se algum requisito não tem cobertura que discrimina. Gap não é "quase lá" — é vermelho.
|
|
200
|
+
- Não conserte aqui. Gap vira tarefa de correção na change; re-verifica depois.
|
|
201
|
+
- O gate do \`archive\` **exige** \`verdict.json\` com \`ok\` cobrindo todo \`[req:]\`. Sem isso, não arquiva.
|
|
202
|
+
`;
|
|
203
|
+
|
|
142
204
|
export const WK_SKILLS = [
|
|
143
205
|
skill('wk-workflow', 'Use ao começar qualquer mudança não-trivial — orquestra o loop a2 (explore, propose, apply, verify, archive) nos comandos wendkeep.', WORKFLOW),
|
|
144
|
-
skill('wk-tdd', 'Use ao implementar qualquer comportamento —
|
|
206
|
+
skill('wk-tdd', 'Use ao implementar qualquer comportamento — Red/Green/Refactor com testes que discriminam (derivados do spec, litmus não-raso, adequação).', TDD),
|
|
145
207
|
skill('wk-debugging', 'Use quando algo falha ou quebra — depuração sistemática por hipótese antes de corrigir.', DEBUGGING),
|
|
146
|
-
skill('wk-brainstorming', 'Use quando a ideia ainda é vaga —
|
|
208
|
+
skill('wk-brainstorming', 'Use quando a ideia ainda é vaga — vira design aprovado, com closure gate e tabela out-of-scope, antes de código.', BRAINSTORMING),
|
|
147
209
|
skill('wk-planning', 'Use após um design aprovado — decompõe em plano de tarefas TDD bite-sized.', PLANNING),
|
|
210
|
+
skill('wk-verify', 'Use no verify deep — passe independente read-only (autor≠verificador) que re-deriva a cobertura do spec e grava verdict.json.', VERIFY),
|
|
148
211
|
];
|
|
149
212
|
|
|
150
213
|
// Seed each skill into <brainDir>/skills/<name>/SKILL.md if absent (non-destructive).
|
package/src/taxonomy.mjs
CHANGED
package/src/verify.mjs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// the VAULT. Writes 08-Mudanças/<slug>/evidencia.json; exit 1 if a critical sensor is red.
|
|
4
4
|
import { readFileSync, writeFileSync } from 'node:fs';
|
|
5
5
|
import { isAbsolute, join, resolve } from 'node:path';
|
|
6
|
-
import { parseTasks, activeChange } from '../hooks/change-core.mjs';
|
|
6
|
+
import { parseTasks, activeChange, appendFixTasks } from '../hooks/change-core.mjs';
|
|
7
7
|
import { loadSensors, requiredSensors, runSensors, evaluateGate } from '../hooks/sensors-core.mjs';
|
|
8
8
|
|
|
9
9
|
function opt(argv, name) {
|
|
@@ -31,11 +31,51 @@ export function runVerify(argv) {
|
|
|
31
31
|
const evidence = runSensors(sensors, ids, { cwd: projectRoot });
|
|
32
32
|
writeFileSync(join(changeDir, 'evidencia.json'), `${JSON.stringify(evidence, null, 2)}\n`, 'utf8');
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
34
|
+
// Mutation survivors -> fix tasks (Wave B), bounded at 3 rounds then escalate.
|
|
35
|
+
const withSurvivors = evidence.filter((e) => e.survivors && e.survivors.length);
|
|
36
|
+
if (withSurvivors.length) {
|
|
37
|
+
const roundFile = join(changeDir, '.mutation-round');
|
|
38
|
+
let round = 0;
|
|
39
|
+
try { round = Number(readFileSync(roundFile, 'utf8').trim()) || 0; } catch { /* first round */ }
|
|
40
|
+
if (round >= 3) {
|
|
41
|
+
process.stderr.write('verify: mutantes ainda sobrevivem após 3 rodadas — revise os testes à mão.\n');
|
|
42
|
+
} else {
|
|
43
|
+
let added = 0;
|
|
44
|
+
for (const e of withSurvivors) added += appendFixTasks(changeDir, e.survivors, e.id);
|
|
45
|
+
writeFileSync(roundFile, String(round + 1), 'utf8');
|
|
46
|
+
process.stdout.write(`verify: ${added} fix-task(s) de mutação (rodada ${round + 1}/3)\n`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Same rule as the archive gate: evidence carries severity, so evaluateGate blocks
|
|
51
|
+
// only on critical/missing — a red warning is advisory and passes verify.
|
|
52
|
+
const { ok, failing } = evaluateGate(evidence, ids);
|
|
53
|
+
for (const e of evidence) {
|
|
54
|
+
const mark = e.status === 'green' ? '✓' : (e.severity === 'warning' ? '!' : '✗');
|
|
55
|
+
process.stdout.write(` ${mark} ${e.id}${e.severity === 'warning' && e.status === 'red' ? ' (warning)' : ''}\n`);
|
|
56
|
+
}
|
|
38
57
|
if (!ok) { process.stderr.write(`verify: critical sensors red: ${failing.join(', ')}\n`); process.exit(1); }
|
|
58
|
+
|
|
59
|
+
// --deep (Q2=B): assemble the verification package the wk-verify skill judges. A trivial
|
|
60
|
+
// change (no [req:] tasks, sensors green) gets an auto verdict — no agent pass needed.
|
|
61
|
+
if (argv.includes('--deep')) {
|
|
62
|
+
const tasks = parseTasks(tarefas);
|
|
63
|
+
const reqIds = [...new Set(tasks.map((t) => t.req).filter(Boolean))];
|
|
64
|
+
const pkg = {
|
|
65
|
+
slug,
|
|
66
|
+
requirements: reqIds.map((id) => ({ id })),
|
|
67
|
+
tasks: tasks.map((t) => ({ id: t.id, text: t.text, req: t.req || null, done: t.done })),
|
|
68
|
+
sensors: evidence,
|
|
69
|
+
};
|
|
70
|
+
writeFileSync(join(changeDir, 'verificacao.json'), `${JSON.stringify(pkg, null, 2)}\n`, 'utf8');
|
|
71
|
+
if (reqIds.length === 0) {
|
|
72
|
+
writeFileSync(join(changeDir, 'verdict.json'), `${JSON.stringify({ slug, ok: true, coverage: [], notes: ['trivial: sem requisito'] }, null, 2)}\n`, 'utf8');
|
|
73
|
+
process.stdout.write('verify --deep: pacote + verdict trivial escritos\n');
|
|
74
|
+
} else {
|
|
75
|
+
process.stdout.write('verify --deep: pacote escrito — rode a skill wk-verify pra gravar verdict.json\n');
|
|
76
|
+
}
|
|
77
|
+
process.exit(0);
|
|
78
|
+
}
|
|
39
79
|
process.stdout.write(`verify OK (${ids.length} sensor(s))\n`);
|
|
40
80
|
process.exit(0);
|
|
41
81
|
}
|