akm-cli 0.9.0-beta.41 → 0.9.0-beta.43
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 +63 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/agent.md +26 -10
- package/dist/assets/stash-skeleton/facts/conventions/assets/command.md +26 -10
- package/dist/assets/stash-skeleton/facts/conventions/assets/fact.md +27 -12
- package/dist/assets/stash-skeleton/facts/conventions/assets/knowledge.md +28 -10
- package/dist/assets/stash-skeleton/facts/conventions/assets/lesson.md +31 -13
- package/dist/assets/stash-skeleton/facts/conventions/assets/memory.md +26 -9
- package/dist/assets/stash-skeleton/facts/conventions/assets/script.md +31 -9
- package/dist/assets/stash-skeleton/facts/conventions/assets/skill.md +28 -11
- package/dist/assets/stash-skeleton/facts/conventions/assets/workflow.md +31 -10
- package/dist/commands/improve/improve.js +106 -52
- package/dist/commands/improve/recombine.js +43 -7
- package/dist/commands/improve/reflect.js +107 -2
- package/dist/core/authoring-rules.js +9 -0
- package/dist/core/state-db.js +81 -4
- package/dist/integrations/agent/prompts.js +42 -1
- package/dist/scripts/migrate-storage.js +40 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,69 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
8
8
|
|
|
9
9
|
### Fixed
|
|
10
10
|
|
|
11
|
+
- **improve/recombine: cap-aware decay — the `maxClustersPerRun` cap no longer
|
|
12
|
+
traps recurring hypotheses below `confirmThreshold` (#658).** Recombine is a
|
|
13
|
+
two-pass design: a cluster must be re-induced on `confirmThreshold` (=2) runs
|
|
14
|
+
before its `type:hypothesis` proposal promotes to an auto-accepted
|
|
15
|
+
`type:lesson`. But only the top-`maxClustersPerRun` (=5) clusters are
|
|
16
|
+
processed per run, and `decayUnseenRecombineHypotheses` hard-reset the
|
|
17
|
+
confirmation streak of every hypothesis not processed that run. A cluster that
|
|
18
|
+
genuinely re-forms every run but is displaced out of the top-5 (slots are tied
|
|
19
|
+
on member-count and broken by an arbitrary alphabetical tiebreak) had its
|
|
20
|
+
streak zeroed — it could never win two consecutive slots, so its proposal sat
|
|
21
|
+
pending forever (6 such proposals were stuck in one production stash).
|
|
22
|
+
`decayUnseenRecombineHypotheses` is now **cap-aware**: `recombine.ts` passes
|
|
23
|
+
the FULL pre-cap cluster set, and a hypothesis is spared from reset when its
|
|
24
|
+
cluster still Jaccard-matches a present cluster (same signature, overlap ≥ 0.7
|
|
25
|
+
— the same rule used for re-induction). Only hypotheses with no matching
|
|
26
|
+
current cluster (the corpus stopped supporting them) decay. This does **not**
|
|
27
|
+
lower the recurrence bar: the confirmation count is still advanced only by
|
|
28
|
+
genuine re-induction in the processed slice (`recordRecombineInduction`);
|
|
29
|
+
sparing merely avoids an artificial reset, so a genuinely non-recurring
|
|
30
|
+
hypothesis still decays to 0 and never confirms (no new bland-hypothesis churn,
|
|
31
|
+
cf. #632/#633). No schema change. The cap now lives in a new `capClusters`
|
|
32
|
+
helper split out of `buildRelatednessClusters` so the full ranked set stays
|
|
33
|
+
available for the decay sweep.
|
|
34
|
+
- **`improve` reflect no longer emits proposals doomed to fail the
|
|
35
|
+
`invalid-description` gate when the source asset has no frontmatter
|
|
36
|
+
`description` (#636).** Reflect echoed the source frontmatter, so for assets
|
|
37
|
+
that carry other keys but no `description` (notably scraped docs:
|
|
38
|
+
`source`/`title`/`scraped`) the proposal inherited the missing/empty
|
|
39
|
+
description and the promote-time validator (`isValidDescription`, 20–400
|
|
40
|
+
chars) rejected it — observed as ~14/16 rejects in one triage pass, blocking
|
|
41
|
+
the whole scraped-doc/knowledge cluster from reflect improvement. The fix is
|
|
42
|
+
**generation-time only**: (1) `buildReflectPrompt` now injects an explicit
|
|
43
|
+
"synthesize a `description`" instruction whenever the source lacks a non-empty
|
|
44
|
+
`description` and the asset type requires one (per `authoring-rules.ts`
|
|
45
|
+
`DESCRIPTION_TYPES`), telling the model it MUST author a valid 20–400-char
|
|
46
|
+
plain-prose description from the asset's `title:`/first `# Heading`/opening
|
|
47
|
+
body; and (2) a deterministic reflect-side belt-and-suspenders in
|
|
48
|
+
`sanitizeReflectPayload` — if a source that already had frontmatter still ends
|
|
49
|
+
up with a missing/empty description after generation, reflect derives one
|
|
50
|
+
deterministically from `title:`/first heading (validated against
|
|
51
|
+
`isValidDescription`, never free-form invention) **before** the proposal is
|
|
52
|
+
created. The validator, `authoring-rules.ts` bounds, `repairProposalContent`,
|
|
53
|
+
and the drain are unchanged — nothing in the validator/promote path fabricates
|
|
54
|
+
content to pass itself.
|
|
55
|
+
- **The high-salience improve admission lane (#608) now requires a
|
|
56
|
+
content-derived encoding score, not the per-type weight stub (#655,
|
|
57
|
+
#608/#644 follow-up).** The lane previously admitted any zero-feedback ref
|
|
58
|
+
whose `asset_salience.encoding_salience >= salienceThreshold` (default 0.75).
|
|
59
|
+
But for assets distill has not content-scored, `encoding_salience` is just the
|
|
60
|
+
per-type WEIGHT STUB (skill/agent 0.9, command/workflow 0.8, lesson 0.75), so
|
|
61
|
+
"high-salience" degenerated into "is a skill/agent/command/lesson" — which
|
|
62
|
+
selected the type-stub `lore-writer` agent on every run (prod: 1 content-scored
|
|
63
|
+
/ 37 type-stub / 1826 NULL-legacy rows). The gate now also requires
|
|
64
|
+
`isContentEncodingRow(row, parseAssetRef(ref).type)` (the #644 provenance
|
|
65
|
+
helper), so only genuinely content-scored assets qualify. This preserves
|
|
66
|
+
#608's intent — distilled assets, the lane's real targets, keep their real
|
|
67
|
+
content score and still qualify — while cutting the type-stub waste; type-stub
|
|
68
|
+
rows must earn retrieval/feedback signal via the other lanes. NULL-legacy rows
|
|
69
|
+
follow `isContentEncodingRow`'s differs-from-stub heuristic. An aggregated log
|
|
70
|
+
line now reports how many refs the lane admitted so lane composition is
|
|
71
|
+
observable. The threshold, type-weight table, 10% cap, and `isContentEncodingRow`
|
|
72
|
+
are unchanged.
|
|
73
|
+
|
|
11
74
|
- **Auto-sync no longer refuses to commit akm's own changes when unrelated
|
|
12
75
|
non-akm files are present in the stash working tree.** When a stash root is
|
|
13
76
|
shared with a project repo, stray files written into the stash root (e.g. a
|
|
@@ -1,22 +1,38 @@
|
|
|
1
1
|
---
|
|
2
2
|
category: convention
|
|
3
|
-
description:
|
|
3
|
+
description: Soft authoring conventions for agent assets using scoped role, tool, and maintenance rules.
|
|
4
4
|
when_to_use: Surfaced to authoring agents when they write or revise an agent asset.
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
<!--
|
|
8
8
|
SOFT guidance only — advice, not a contract. Nothing here is enforced by the
|
|
9
|
-
proposal gate;
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
proposal gate; validator-rejecting HARD rules live in src/core/authoring-rules.ts
|
|
10
|
+
and remain the sole enforced source. Editing or deleting this file cannot weaken
|
|
11
|
+
the gate. Tune the guidance below to match how your stash wants this asset type
|
|
12
|
+
maintained.
|
|
12
13
|
-->
|
|
13
14
|
|
|
14
15
|
# Agent authoring conventions
|
|
15
16
|
|
|
16
|
-
An agent
|
|
17
|
+
An agent asset defines a reusable role. Treat it like a disciplined maintainer, not a generic personality. Its job is to know its scope, read the right rulebooks, use the right tools, and leave the stash in better shape.
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
## Purpose
|
|
20
|
+
|
|
21
|
+
Use an agent when a recurring task benefits from a specialized role, bounded responsibilities, and explicit tool behavior.
|
|
22
|
+
|
|
23
|
+
## Authoring strategy
|
|
24
|
+
|
|
25
|
+
- Write the description so a dispatcher knows exactly when to delegate to this agent.
|
|
26
|
+
- Define the agent’s domain, authority, boundaries, and expected output.
|
|
27
|
+
- Specify what the agent must read first: relevant stash standards, type conventions, reference docs, source files, or prior lessons.
|
|
28
|
+
- State tool expectations plainly: what tools it may use, what it should avoid, and when it must ask for human review.
|
|
29
|
+
- Give the agent maintenance duties when appropriate: update cross-references, append logs, preserve provenance, and surface contradictions.
|
|
30
|
+
- Prefer a narrow role that does one thing reliably over a broad do-everything persona.
|
|
31
|
+
- Include handoff behavior: what the agent should return when it cannot complete the task safely.
|
|
32
|
+
|
|
33
|
+
## Maintenance strategy
|
|
34
|
+
|
|
35
|
+
- Refine the agent when repeated sessions show the same delegation failure.
|
|
36
|
+
- Add explicit negative guidance when the agent overreaches.
|
|
37
|
+
- Keep role instructions stable and concise; move large background material into knowledge assets.
|
|
38
|
+
- Use lessons to capture operational improvements, then promote stable ones into the agent when they become part of the role.
|
|
@@ -1,22 +1,38 @@
|
|
|
1
1
|
---
|
|
2
2
|
category: convention
|
|
3
|
-
description:
|
|
3
|
+
description: Soft authoring conventions for command assets using repeatable LLM operation patterns.
|
|
4
4
|
when_to_use: Surfaced to authoring agents when they write or revise a command asset.
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
<!--
|
|
8
8
|
SOFT guidance only — advice, not a contract. Nothing here is enforced by the
|
|
9
|
-
proposal gate;
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
proposal gate; validator-rejecting HARD rules live in src/core/authoring-rules.ts
|
|
10
|
+
and remain the sole enforced source. Editing or deleting this file cannot weaken
|
|
11
|
+
the gate. Tune the guidance below to match how your stash wants this asset type
|
|
12
|
+
maintained.
|
|
12
13
|
-->
|
|
13
14
|
|
|
14
15
|
# Command authoring conventions
|
|
15
16
|
|
|
16
|
-
A command is a markdown prompt template the
|
|
17
|
+
A command is a reusable markdown prompt template invoked by name. Treat it like an operation in the stash: every vague instruction will compound into repeated vague output.
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
## Purpose
|
|
20
|
+
|
|
21
|
+
Use a command when the user or agent needs to perform the same prompt-shaped task repeatedly with different arguments or context.
|
|
22
|
+
|
|
23
|
+
## Authoring strategy
|
|
24
|
+
|
|
25
|
+
- Put the task, inputs, constraints, and expected output shape near the top.
|
|
26
|
+
- Make argument placeholders obvious and describe what each one should contain.
|
|
27
|
+
- Tell the model what to inspect before acting, especially relevant stash assets, facts, standards, or reference docs.
|
|
28
|
+
- State the decision boundary: what the command should do directly, what it should only propose, and what it should refuse or defer.
|
|
29
|
+
- Include output requirements that are stable across runs.
|
|
30
|
+
- Keep the prompt tight. A command should be easy to invoke and hard to misinterpret.
|
|
31
|
+
- Avoid embedding one-time project details unless the command is intentionally project-specific.
|
|
32
|
+
|
|
33
|
+
## Maintenance strategy
|
|
34
|
+
|
|
35
|
+
- If users repeatedly clarify the same missing detail, add that detail to the command.
|
|
36
|
+
- If command output regularly becomes useful durable knowledge, instruct the agent to file the result into the right asset type.
|
|
37
|
+
- If the command starts handling multiple unrelated tasks, split it into smaller commands.
|
|
38
|
+
- Preserve a clear invocation contract so future agents can call the command safely.
|
|
@@ -1,24 +1,39 @@
|
|
|
1
1
|
---
|
|
2
2
|
category: convention
|
|
3
|
-
description:
|
|
3
|
+
description: Soft authoring conventions for fact assets using pinned-core and just-in-time context principles.
|
|
4
4
|
when_to_use: Surfaced to authoring agents when they write or revise a fact asset.
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
<!--
|
|
8
8
|
SOFT guidance only — advice, not a contract. Nothing here is enforced by the
|
|
9
|
-
proposal gate;
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
proposal gate; validator-rejecting HARD rules live in src/core/authoring-rules.ts
|
|
10
|
+
and remain the sole enforced source. Editing or deleting this file cannot weaken
|
|
11
|
+
the gate. Tune the guidance below to match how your stash wants this asset type
|
|
12
|
+
maintained.
|
|
12
13
|
-->
|
|
13
14
|
|
|
14
15
|
# Fact authoring conventions
|
|
15
16
|
|
|
16
|
-
A fact is durable stash-level context
|
|
17
|
-
coding conventions, or stash-meta.
|
|
17
|
+
A fact is durable stash-level context: personal, team, project, convention, or meta knowledge. Treat facts as the stash’s semantic layer — selectively loaded context that should guide future work without bloating every prompt.
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
19
|
+
## Purpose
|
|
20
|
+
|
|
21
|
+
Use a fact for stable information that future agents should treat as true or normative: user preferences, project identity, team stack, architecture principles, naming conventions, tag vocabulary, or stash organization.
|
|
22
|
+
|
|
23
|
+
## Authoring strategy
|
|
24
|
+
|
|
25
|
+
- Write each fact as a standing declaration that can survive across sessions.
|
|
26
|
+
- Keep it short, high-signal, and self-contained.
|
|
27
|
+
- Choose the narrowest useful category: personal, team, project, convention, or meta.
|
|
28
|
+
- Use `pinned: true` only for the small core that should be available constantly.
|
|
29
|
+
- Leave most facts unpinned so they can be retrieved just-in-time.
|
|
30
|
+
- Include scope and provenance when a fact is project-specific, inferred, or subject to change.
|
|
31
|
+
- Separate facts from memories: memories preserve observations; facts state durable truth or durable policy.
|
|
32
|
+
- Separate facts from knowledge: knowledge explains a topic; facts declare compact context.
|
|
33
|
+
|
|
34
|
+
## Maintenance strategy
|
|
35
|
+
|
|
36
|
+
- Revise or supersede facts when the durable truth changes.
|
|
37
|
+
- Do not allow contradictory facts to remain equally active.
|
|
38
|
+
- Promote repeated memories or lessons into facts only when they become stable context.
|
|
39
|
+
- Keep convention and meta facts especially clear, because they steer future asset creation.
|
|
@@ -1,22 +1,40 @@
|
|
|
1
1
|
---
|
|
2
2
|
category: convention
|
|
3
|
-
description:
|
|
3
|
+
description: Soft authoring conventions for knowledge assets as compiled, on-demand reference documents.
|
|
4
4
|
when_to_use: Surfaced to authoring agents when they write or revise a knowledge asset.
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
<!--
|
|
8
8
|
SOFT guidance only — advice, not a contract. Nothing here is enforced by the
|
|
9
|
-
proposal gate;
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
proposal gate; validator-rejecting HARD rules live in src/core/authoring-rules.ts
|
|
10
|
+
and remain the sole enforced source. Editing or deleting this file cannot weaken
|
|
11
|
+
the gate. Tune the guidance below to match how your stash wants this asset type
|
|
12
|
+
maintained.
|
|
12
13
|
-->
|
|
13
14
|
|
|
14
15
|
# Knowledge authoring conventions
|
|
15
16
|
|
|
16
|
-
A knowledge asset is a reference document meant to be read on demand.
|
|
17
|
+
A knowledge asset is a compiled reference document meant to be read on demand. Treat it as the synthesized layer above raw material: not source files, not chat residue, but integrated, navigable understanding that saves future agents from rediscovering the same material.
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
## Purpose
|
|
20
|
+
|
|
21
|
+
Use a knowledge asset for durable reference material, synthesized explanations, design notes, comparisons, and project context that is broader than a single memory but less procedural than a skill.
|
|
22
|
+
|
|
23
|
+
## Authoring strategy
|
|
24
|
+
|
|
25
|
+
- Open with a plain top-level title that names the subject.
|
|
26
|
+
- Add a concise orientation paragraph: what this document covers and when it should be read.
|
|
27
|
+
- For a long reference, add a table of contents near the top so the full scope is visible even on a partial read.
|
|
28
|
+
- Organize by stable concepts, decisions, entities, or questions — roughly one page per concept.
|
|
29
|
+
- Cross-reference related assets instead of duplicating them, so a navigable graph forms over time.
|
|
30
|
+
- Preserve provenance where it matters: cite source files, raw notes, session logs, or issues by path/ref.
|
|
31
|
+
- Call out contradictions, uncertainty, stale claims, and open questions explicitly.
|
|
32
|
+
- Prefer accurate synthesis over exhaustive dumping. Raw material belongs elsewhere; this file is the compiled layer.
|
|
33
|
+
- Use tables or checklists when they make retrieval and comparison easier.
|
|
34
|
+
|
|
35
|
+
## Maintenance strategy
|
|
36
|
+
|
|
37
|
+
- Update the existing page when new information changes the same topic; append a dated note rather than silently rewriting when provenance matters.
|
|
38
|
+
- Create a new page when the concept deserves its own durable entry.
|
|
39
|
+
- Add links both ways when a new relationship matters.
|
|
40
|
+
- Periodically scan for orphaned, stale, or overlapping knowledge docs and consolidate them.
|
|
@@ -1,25 +1,43 @@
|
|
|
1
1
|
---
|
|
2
2
|
category: convention
|
|
3
|
-
description:
|
|
3
|
+
description: Soft authoring conventions for lesson assets that capture compounding, hard-won judgment.
|
|
4
4
|
when_to_use: Surfaced to authoring agents when they write or revise a lesson asset.
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
<!--
|
|
8
8
|
SOFT guidance only — advice, not a contract. Nothing here is enforced by the
|
|
9
|
-
proposal gate;
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
proposal gate; validator-rejecting HARD rules live in src/core/authoring-rules.ts
|
|
10
|
+
and remain the sole enforced source. Editing or deleting this file cannot weaken
|
|
11
|
+
the gate. Tune the guidance below to match how your stash wants this asset type
|
|
12
|
+
maintained.
|
|
13
13
|
-->
|
|
14
14
|
|
|
15
15
|
# Lesson authoring conventions
|
|
16
16
|
|
|
17
|
-
A lesson captures durable, hard-won
|
|
18
|
-
and *what goes wrong without it* — it is not a restatement of the source asset.
|
|
17
|
+
A lesson captures durable, hard-won judgment that should compound across future agent sessions. Treat it as distilled judgment about how to act: it should preserve the extracted meaning of what real use revealed, not merely recount an incident or summarize another asset.
|
|
19
18
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
-
|
|
19
|
+
## Purpose
|
|
20
|
+
|
|
21
|
+
Use a lesson to record:
|
|
22
|
+
|
|
23
|
+
- when to reach for a pattern, asset, or decision;
|
|
24
|
+
- what tends to go wrong without it;
|
|
25
|
+
- what evidence, feedback, or repeated experience made the lesson worth keeping;
|
|
26
|
+
- how a future agent should act differently because this lesson exists.
|
|
27
|
+
|
|
28
|
+
## Authoring strategy
|
|
29
|
+
|
|
30
|
+
- Lead with the trigger: the concrete situation where this lesson should be loaded.
|
|
31
|
+
- Follow with the failure mode: what mistake, omission, or confusion this prevents.
|
|
32
|
+
- End with the reusable judgment: the practical rule a future agent can apply.
|
|
33
|
+
- Keep the scope narrow. A lesson should teach one durable behavior.
|
|
34
|
+
- Prefer observed evidence over generic advice. Mention the kind of signal that produced the lesson, such as rejected proposals, repeated lint findings, user feedback, or session outcomes.
|
|
35
|
+
- Do not restate the source asset. Lessons are compiled judgment, not copied documentation.
|
|
36
|
+
- Write for a future agent mid-task: direct, practical, and easy to apply.
|
|
37
|
+
|
|
38
|
+
## Maintenance strategy
|
|
39
|
+
|
|
40
|
+
- Update an existing lesson when new feedback sharpens the same judgment.
|
|
41
|
+
- Create a new lesson only when the trigger or failure mode is meaningfully different.
|
|
42
|
+
- Deprecate or revise stale lessons instead of allowing contradictory guidance to accumulate.
|
|
43
|
+
- When a lesson becomes broadly normative, consider promoting the stable rule into a `fact:conventions/...` asset.
|
|
@@ -1,21 +1,38 @@
|
|
|
1
1
|
---
|
|
2
2
|
category: convention
|
|
3
|
-
description:
|
|
3
|
+
description: Soft authoring conventions for memory assets using durable-context and provenance discipline.
|
|
4
4
|
when_to_use: Surfaced to authoring agents when they write or revise a memory asset.
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
<!--
|
|
8
8
|
SOFT guidance only — advice, not a contract. Nothing here is enforced by the
|
|
9
|
-
proposal gate;
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
proposal gate; validator-rejecting HARD rules live in src/core/authoring-rules.ts
|
|
10
|
+
and remain the sole enforced source. Editing or deleting this file cannot weaken
|
|
11
|
+
the gate. Tune the guidance below to match how your stash wants this asset type
|
|
12
|
+
maintained.
|
|
12
13
|
-->
|
|
13
14
|
|
|
14
15
|
# Memory authoring conventions
|
|
15
16
|
|
|
16
|
-
A memory is a short
|
|
17
|
+
A memory is a short, durable note that should survive beyond the current session. Treat it as a small compiled fact or decision, not a transcript fragment.
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
## Purpose
|
|
20
|
+
|
|
21
|
+
Use a memory when a future agent would make a better decision by knowing a specific user preference, project decision, environmental fact, constraint, or observed outcome.
|
|
22
|
+
|
|
23
|
+
## Authoring strategy
|
|
24
|
+
|
|
25
|
+
- Record one durable fact, decision, or constraint per memory.
|
|
26
|
+
- Write it so it stands alone without the original conversation.
|
|
27
|
+
- Include enough context to prevent misapplication: subject, scope, and when it matters.
|
|
28
|
+
- Prefer stable, reusable information over step-by-step session play-by-play.
|
|
29
|
+
- Mark uncertainty or subjectivity clearly when the memory is not a settled fact.
|
|
30
|
+
- Preserve source/provenance in frontmatter or body when the memory came from a session, log, user statement, or derived inference.
|
|
31
|
+
- Avoid storing secrets, private tokens, or volatile temporary state as memory.
|
|
32
|
+
|
|
33
|
+
## Maintenance strategy
|
|
34
|
+
|
|
35
|
+
- Update or supersede memories when newer evidence changes the truth.
|
|
36
|
+
- Consolidate repeated memories into a clearer fact or knowledge asset.
|
|
37
|
+
- Convert broad, stable conventions into `fact` assets.
|
|
38
|
+
- Archive memories that are no longer current rather than letting stale context keep influencing agents.
|
|
@@ -1,21 +1,43 @@
|
|
|
1
1
|
---
|
|
2
2
|
category: convention
|
|
3
|
-
description:
|
|
3
|
+
description: Soft authoring conventions for script assets using agent-safe CLI helper principles.
|
|
4
4
|
when_to_use: Surfaced to authoring agents when they write or revise a script asset.
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
<!--
|
|
8
8
|
SOFT guidance only — advice, not a contract. Nothing here is enforced by the
|
|
9
|
-
proposal gate;
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
proposal gate; validator-rejecting HARD rules live in src/core/authoring-rules.ts
|
|
10
|
+
and remain the sole enforced source. Editing or deleting this file cannot weaken
|
|
11
|
+
the gate. Tune the guidance below to match how your stash wants this asset type
|
|
12
|
+
maintained.
|
|
12
13
|
-->
|
|
13
14
|
|
|
14
15
|
# Script authoring conventions
|
|
15
16
|
|
|
16
|
-
A script is an executable
|
|
17
|
+
A script is an executable helper that an agent or human can run on demand. Treat it like a small, deterministic tool that reduces manual bookkeeping and makes repeatable operations safer.
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
## Purpose
|
|
20
|
+
|
|
21
|
+
Use a script when a task is mechanical, repeatable, and better handled by a deterministic program than by free-form agent edits.
|
|
22
|
+
|
|
23
|
+
## Authoring strategy
|
|
24
|
+
|
|
25
|
+
- Start with the appropriate interpreter line and a short usage comment.
|
|
26
|
+
- State what the script does, expected inputs, outputs, side effects, and failure behavior.
|
|
27
|
+
- Keep one script focused on one job.
|
|
28
|
+
- Validate inputs before mutation.
|
|
29
|
+
- Fail loudly and early on unsafe or ambiguous input.
|
|
30
|
+
- Declare required dependencies and assumptions explicitly rather than assuming a tool is installed.
|
|
31
|
+
- Justify any non-obvious constant (timeout, retry count, limit) in a comment so a future reader can adjust it safely.
|
|
32
|
+
- Prefer idempotent behavior where practical.
|
|
33
|
+
- Avoid hidden network calls, destructive defaults, or silent writes.
|
|
34
|
+
- Never print secrets or sensitive values.
|
|
35
|
+
- Write output that is easy for both humans and agents to parse.
|
|
36
|
+
- Favor clear names, straightforward control flow, and comments at decision points.
|
|
37
|
+
|
|
38
|
+
## Maintenance strategy
|
|
39
|
+
|
|
40
|
+
- Add examples when agents or users repeatedly invoke the script incorrectly.
|
|
41
|
+
- Keep dangerous actions behind explicit flags.
|
|
42
|
+
- When a script becomes a core operation, add or update a workflow that explains when to run it.
|
|
43
|
+
- If the script encodes a convention, also document that convention in a fact or knowledge asset.
|
|
@@ -1,23 +1,40 @@
|
|
|
1
1
|
---
|
|
2
2
|
category: convention
|
|
3
|
-
description:
|
|
3
|
+
description: Soft authoring conventions for skill assets as reusable, just-in-time procedural rulebooks.
|
|
4
4
|
when_to_use: Surfaced to authoring agents when they write or revise a skill asset.
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
<!--
|
|
8
8
|
SOFT guidance only — advice, not a contract. Nothing here is enforced by the
|
|
9
|
-
proposal gate;
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
proposal gate; validator-rejecting HARD rules live in src/core/authoring-rules.ts
|
|
10
|
+
and remain the sole enforced source. Editing or deleting this file cannot weaken
|
|
11
|
+
the gate. Tune the guidance below to match how your stash wants this asset type
|
|
12
|
+
maintained.
|
|
12
13
|
-->
|
|
13
14
|
|
|
14
15
|
# Skill authoring conventions
|
|
15
16
|
|
|
16
|
-
A skill is a reusable, self-contained capability stored as `skills/<name>/SKILL.md`.
|
|
17
|
+
A skill is a reusable, self-contained capability stored as `skills/<name>/SKILL.md`. Treat it like a compact operating manual that an agent can load just-in-time, follow without rediscovering the process, and improve when repeated use exposes gaps.
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
19
|
+
## Purpose
|
|
20
|
+
|
|
21
|
+
Use a skill when the stash needs reusable procedural guidance for a recurring class of work. A good skill reduces repeated reasoning cost: future agents should not have to reconstruct the same method from raw context.
|
|
22
|
+
|
|
23
|
+
## Authoring strategy
|
|
24
|
+
|
|
25
|
+
- Make the dispatch signal clear. The description should let a dispatcher decide whether to load the skill without reading the whole body.
|
|
26
|
+
- Open with the outcome the skill helps produce.
|
|
27
|
+
- State when to use it, when not to use it, and what inputs the agent should gather before acting.
|
|
28
|
+
- Structure the body as a rulebook: principles first, then procedure, then checks.
|
|
29
|
+
- Use short sections and ordered steps where sequence matters.
|
|
30
|
+
- Match the level of detail to how fragile the task is: open-ended work gets high-level heuristics and room to reason, while fragile or consistency-critical steps get exact, unambiguous instructions.
|
|
31
|
+
- Keep the body lean and move bulky background into companion knowledge docs referenced one level deep, so the skill loads cheaply and stays scannable.
|
|
32
|
+
- Include failure modes and verification steps. A skill should tell the agent how to know the work is complete.
|
|
33
|
+
- Keep one skill focused on one capability. Split unrelated concerns into separate skills and cross-reference them.
|
|
34
|
+
|
|
35
|
+
## Maintenance strategy
|
|
36
|
+
|
|
37
|
+
- Update the skill when session logs, feedback, or rejected proposals reveal repeatable confusion.
|
|
38
|
+
- Add companion knowledge docs when the skill needs background material that would bloat the main procedure.
|
|
39
|
+
- Promote durable recurring corrections into the skill; leave one-off observations in memories or lessons.
|
|
40
|
+
- Prefer small edits that preserve the skill’s operational shape over broad rewrites that erase tested guidance.
|
|
@@ -1,22 +1,43 @@
|
|
|
1
1
|
---
|
|
2
2
|
category: convention
|
|
3
|
-
description:
|
|
3
|
+
description: Soft authoring conventions for workflow assets using explicit operations, logging, and lintable steps.
|
|
4
4
|
when_to_use: Surfaced to authoring agents when they write or revise a workflow asset.
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
<!--
|
|
8
8
|
SOFT guidance only — advice, not a contract. Nothing here is enforced by the
|
|
9
|
-
proposal gate;
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
proposal gate; validator-rejecting HARD rules live in src/core/authoring-rules.ts
|
|
10
|
+
and remain the sole enforced source. Editing or deleting this file cannot weaken
|
|
11
|
+
the gate. Tune the guidance below to match how your stash wants this asset type
|
|
12
|
+
maintained.
|
|
12
13
|
-->
|
|
13
14
|
|
|
14
15
|
# Workflow authoring conventions
|
|
15
16
|
|
|
16
|
-
A workflow describes
|
|
17
|
+
A workflow describes an ordered process an agent or human can follow. Treat it as the operation layer of a maintained stash: clear steps, clear state, clear completion criteria, and enough bookkeeping to resume safely.
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
## Purpose
|
|
20
|
+
|
|
21
|
+
Use a workflow when the task requires multiple steps, branching decisions, repeated checks, or durable progress tracking.
|
|
22
|
+
|
|
23
|
+
## Authoring strategy
|
|
24
|
+
|
|
25
|
+
- Open with the outcome the workflow produces.
|
|
26
|
+
- State prerequisites, required inputs, and tools before the steps.
|
|
27
|
+
- Use ordered step sections when sequence matters.
|
|
28
|
+
- For each step, specify:
|
|
29
|
+
- what to do;
|
|
30
|
+
- what evidence or input it depends on;
|
|
31
|
+
- what output it produces;
|
|
32
|
+
- how to know the step is done.
|
|
33
|
+
- Make branch points explicit. Do not bury conditional behavior in prose.
|
|
34
|
+
- Include validation, lint, or review steps near the end.
|
|
35
|
+
- Include rollback or recovery notes when the workflow mutates files, state, repos, or external systems.
|
|
36
|
+
- Keep steps atomic and resumable so an interrupted run can continue without guessing.
|
|
37
|
+
|
|
38
|
+
## Maintenance strategy
|
|
39
|
+
|
|
40
|
+
- Update the workflow when repeated execution reveals missing checks or unclear handoffs.
|
|
41
|
+
- Add logging expectations when the workflow creates durable state.
|
|
42
|
+
- Extract reusable sub-procedures into skills or scripts when the workflow grows too broad.
|
|
43
|
+
- Record recurring mistakes as lessons, then fold stable corrections back into the workflow.
|
|
@@ -964,6 +964,63 @@ export async function akmImprove(options = {}) {
|
|
|
964
964
|
// #576: clears the per-run LLM usage sink. Defaults to a no-op until the sink
|
|
965
965
|
// is installed inside the try; the `finally` always calls it.
|
|
966
966
|
let disposeLlmUsageSink = () => { };
|
|
967
|
+
// ── Crash-safe / incremental stash sync (#662) ──────────────────────────────
|
|
968
|
+
// The primary stash writes as a filesystem source DURING the run
|
|
969
|
+
// (write-source.ts case-3); those writes become a git commit only when this
|
|
970
|
+
// closure runs. Historically the only call site was a single BATCH commit at
|
|
971
|
+
// the very end of the happy path, so a run interrupted AFTER writing but
|
|
972
|
+
// BEFORE finishing — a mid-cycle crash, a budget abort, or an external
|
|
973
|
+
// SIGTERM/`process.exit` — left every write uncommitted until some LATER run
|
|
974
|
+
// happened to finish cleanly and swept the whole backlog up. We now call this
|
|
975
|
+
// from THREE places: between cycles (bank each completed cycle), at end-of-run
|
|
976
|
+
// (the converged commit), and from the catch path (commit what was written
|
|
977
|
+
// before the crash). That shrinks the worst-case loss from "the entire run" to
|
|
978
|
+
// "the in-flight cycle".
|
|
979
|
+
//
|
|
980
|
+
// Declared in the OUTER scope (not inside the try) so the catch block can reach
|
|
981
|
+
// it. Idempotent + NON-FATAL: `saveGitStash` short-circuits a clean working
|
|
982
|
+
// tree ("nothing to commit") and a thrown sync error is swallowed here, so a
|
|
983
|
+
// repeat call after a no-op cycle is cheap and a failed push never fails the
|
|
984
|
+
// run. Gated identically to the original end-of-run block (git-backed primary
|
|
985
|
+
// stash, sync not disabled). `eventsCtx` is captured by reference, so calls
|
|
986
|
+
// after the db-backed context is installed inside the try use the live handle.
|
|
987
|
+
const effectiveSync = { ...improveProfile.sync, ...options.sync };
|
|
988
|
+
const commitStashBatch = (messageContext) => {
|
|
989
|
+
if (!primaryStashDir || effectiveSync.enabled === false || !isGitBackedStash(primaryStashDir)) {
|
|
990
|
+
return undefined;
|
|
991
|
+
}
|
|
992
|
+
const saveGitStashFn = options.saveGitStashFn ?? saveGitStash;
|
|
993
|
+
const writableOverride = resolveWritableOverride(_earlyConfig);
|
|
994
|
+
const push = effectiveSync.push !== false;
|
|
995
|
+
const message = renderSyncCommitMessage(effectiveSync.message ?? "akm improve auto-sync", messageContext, Date.now());
|
|
996
|
+
try {
|
|
997
|
+
const syncResult = saveGitStashFn(undefined, message, writableOverride, { push, repoDir: primaryStashDir });
|
|
998
|
+
appendEvent({
|
|
999
|
+
eventType: "stash_synced",
|
|
1000
|
+
metadata: {
|
|
1001
|
+
committed: syncResult.committed,
|
|
1002
|
+
pushed: syncResult.pushed,
|
|
1003
|
+
skipped: syncResult.skipped,
|
|
1004
|
+
reason: syncResult.reason ?? null,
|
|
1005
|
+
},
|
|
1006
|
+
}, eventsCtx);
|
|
1007
|
+
return {
|
|
1008
|
+
committed: syncResult.committed,
|
|
1009
|
+
pushed: syncResult.pushed,
|
|
1010
|
+
skipped: syncResult.skipped,
|
|
1011
|
+
...(syncResult.reason !== undefined ? { reason: syncResult.reason } : {}),
|
|
1012
|
+
};
|
|
1013
|
+
}
|
|
1014
|
+
catch (syncErr) {
|
|
1015
|
+
const reason = syncErr instanceof Error ? syncErr.message : String(syncErr);
|
|
1016
|
+
warn(`improve: stash sync failed (non-fatal): ${reason}`);
|
|
1017
|
+
appendEvent({
|
|
1018
|
+
eventType: "stash_synced",
|
|
1019
|
+
metadata: { committed: false, pushed: false, skipped: true, reason },
|
|
1020
|
+
}, eventsCtx);
|
|
1021
|
+
return { committed: false, pushed: false, skipped: true, reason };
|
|
1022
|
+
}
|
|
1023
|
+
};
|
|
967
1024
|
try {
|
|
968
1025
|
// H7 (#566): arm the budget watchdog. `armBudgetWatchdog` captures both the
|
|
969
1026
|
// budget timer and the hard-kill timer it schedules on exhaustion, returning
|
|
@@ -1053,6 +1110,16 @@ export async function akmImprove(options = {}) {
|
|
|
1053
1110
|
break;
|
|
1054
1111
|
}
|
|
1055
1112
|
}
|
|
1113
|
+
// #662 incremental sync: bank the PREVIOUS cycle's writes before starting a
|
|
1114
|
+
// new one, so a crash/abort/timeout mid-run loses at most the in-flight
|
|
1115
|
+
// cycle rather than the whole run. Guarded on `cycleIndex > 0`, so the
|
|
1116
|
+
// common maxCycles:1 path never calls this — its single end-of-run commit
|
|
1117
|
+
// below stays the only sync and the serialized envelope is byte-identical
|
|
1118
|
+
// to pre-#662. `saveGitStash` no-ops a clean tree, so a cycle that wrote
|
|
1119
|
+
// nothing costs only a `git status`.
|
|
1120
|
+
if (cycleIndex > 0) {
|
|
1121
|
+
commitStashBatch({ scope, plannedRefs, runId: options.runId });
|
|
1122
|
+
}
|
|
1056
1123
|
// Re-run ensureIndex + collectEligibleRefs + memory-cleanup recompute for
|
|
1057
1124
|
// cycles 2+ (cycle 1 already ran them in the first try above). This makes
|
|
1058
1125
|
// cycle N's gate-promoted proposals visible to this cycle's ref selection.
|
|
@@ -1309,57 +1376,18 @@ export async function akmImprove(options = {}) {
|
|
|
1309
1376
|
warningCount: allWarnings.length,
|
|
1310
1377
|
orphansPurged: orphansPurged ?? 0,
|
|
1311
1378
|
}, eventsCtx);
|
|
1312
|
-
// End-of-run BATCH auto-sync
|
|
1313
|
-
// path (see write-source.ts case-3): the primary
|
|
1314
|
-
// filesystem source during the run, then is committed
|
|
1315
|
-
// the same `saveGitStash` that `akm sync` calls.
|
|
1316
|
-
//
|
|
1317
|
-
//
|
|
1318
|
-
//
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
// `akm sync` via resolveWritableOverride().
|
|
1325
|
-
const writableOverride = resolveWritableOverride(_earlyConfig);
|
|
1326
|
-
const push = effectiveSync.push !== false;
|
|
1327
|
-
// `sync.message` may contain `{token}` placeholders (timestamp/date/time/
|
|
1328
|
-
// scope/refs/accepted) expanded against this run's results; the default
|
|
1329
|
-
// template has no tokens so it renders verbatim.
|
|
1330
|
-
const message = renderSyncCommitMessage(effectiveSync.message ?? "akm improve auto-sync", result, Date.now());
|
|
1331
|
-
try {
|
|
1332
|
-
// Pass primaryStashDir as the explicit commit target so the gate above
|
|
1333
|
-
// (which validated primaryStashDir via isGitBackedStash) and the commit
|
|
1334
|
-
// operate on the SAME directory — avoids divergence when a caller passes
|
|
1335
|
-
// a non-default options.stashDir (FIX 9).
|
|
1336
|
-
const syncResult = saveGitStashFn(undefined, message, writableOverride, { push, repoDir: primaryStashDir });
|
|
1337
|
-
result.sync = {
|
|
1338
|
-
committed: syncResult.committed,
|
|
1339
|
-
pushed: syncResult.pushed,
|
|
1340
|
-
skipped: syncResult.skipped,
|
|
1341
|
-
...(syncResult.reason !== undefined ? { reason: syncResult.reason } : {}),
|
|
1342
|
-
};
|
|
1343
|
-
appendEvent({
|
|
1344
|
-
eventType: "stash_synced",
|
|
1345
|
-
metadata: {
|
|
1346
|
-
committed: syncResult.committed,
|
|
1347
|
-
pushed: syncResult.pushed,
|
|
1348
|
-
skipped: syncResult.skipped,
|
|
1349
|
-
reason: syncResult.reason ?? null,
|
|
1350
|
-
},
|
|
1351
|
-
}, eventsCtx);
|
|
1352
|
-
}
|
|
1353
|
-
catch (syncErr) {
|
|
1354
|
-
const reason = syncErr instanceof Error ? syncErr.message : String(syncErr);
|
|
1355
|
-
warn(`improve: end-of-run stash sync failed (non-fatal): ${reason}`);
|
|
1356
|
-
result.sync = { committed: false, pushed: false, skipped: true, reason };
|
|
1357
|
-
appendEvent({
|
|
1358
|
-
eventType: "stash_synced",
|
|
1359
|
-
metadata: { committed: false, pushed: false, skipped: true, reason },
|
|
1360
|
-
}, eventsCtx);
|
|
1361
|
-
}
|
|
1362
|
-
}
|
|
1379
|
+
// End-of-run BATCH auto-sync — the converged commit. Recognition is
|
|
1380
|
+
// decoupled from the per-write path (see write-source.ts case-3): the primary
|
|
1381
|
+
// stash writes as a filesystem source during the run, then is committed via
|
|
1382
|
+
// the same `saveGitStash` that `akm sync` calls. The gating (git-backed
|
|
1383
|
+
// primary stash, sync not disabled) and the NON-FATAL guarantee now live in
|
|
1384
|
+
// `commitStashBatch` (#662); the inter-cycle and catch-path calls reuse it.
|
|
1385
|
+
// dry-run already returned above, so this always runs on a completed live
|
|
1386
|
+
// run. `result.sync` reflects this final commit (for a one-cycle run it is
|
|
1387
|
+
// the only commit; for a multi-cycle run the earlier cycles were banked by
|
|
1388
|
+
// the inter-cycle calls and this records the last batch). `result` carries
|
|
1389
|
+
// the full `{accepted}`/`{refs}`/`{triage_*}` token data for the message.
|
|
1390
|
+
result.sync = commitStashBatch(result);
|
|
1363
1391
|
return result;
|
|
1364
1392
|
}
|
|
1365
1393
|
catch (err) {
|
|
@@ -1372,6 +1400,13 @@ export async function akmImprove(options = {}) {
|
|
|
1372
1400
|
durationMs: Date.now() - startMs,
|
|
1373
1401
|
},
|
|
1374
1402
|
}, eventsCtx);
|
|
1403
|
+
// #662 crash/abort safety net: commit whatever this run already wrote to the
|
|
1404
|
+
// primary stash BEFORE rethrowing, so an interrupted run (mid-cycle crash or
|
|
1405
|
+
// a cooperative budget abort that surfaces as a throw) does not leave its
|
|
1406
|
+
// writes uncommitted until a later clean run sweeps them up. Best-effort —
|
|
1407
|
+
// `commitStashBatch` swallows its own errors and no-ops a clean tree, so this
|
|
1408
|
+
// never masks or supersedes the original failure being rethrown below.
|
|
1409
|
+
commitStashBatch({ scope, plannedRefs, runId: options.runId });
|
|
1375
1410
|
throw err;
|
|
1376
1411
|
}
|
|
1377
1412
|
finally {
|
|
@@ -2471,6 +2506,18 @@ async function runImprovePreparationStage(args) {
|
|
|
2471
2506
|
// noFeedbackCandidates), burning LLM calls and churning the asset. This
|
|
2472
2507
|
// mirrors the P0-A high-retrieval gate's `!lastReflectProposalTs.has(r.ref)`
|
|
2473
2508
|
// guard above so all "rescue" lanes share the same once-per-asset semantics.
|
|
2509
|
+
//
|
|
2510
|
+
// Content-provenance gate (#644 follow-up): the row must ALSO carry a genuine
|
|
2511
|
+
// content-derived encoding score (`isContentEncodingRow`). Otherwise the lane
|
|
2512
|
+
// admits the per-type WEIGHT STUB (skill/agent 0.9, command/workflow 0.8,
|
|
2513
|
+
// lesson 0.75 from DEFAULT_TYPE_ENCODING_WEIGHTS) for every distill-unscored
|
|
2514
|
+
// asset — i.e. "high-salience" degenerates into "is a skill/agent/command/
|
|
2515
|
+
// lesson", which selected the lore-writer type-stub agent on every run. Only
|
|
2516
|
+
// content-scored assets earn the high-salience rescue; type-stub rows must
|
|
2517
|
+
// earn retrieval/feedback signal via the other lanes. This PRESERVES #608's
|
|
2518
|
+
// intent — distilled assets (the lane's real targets) keep their real content
|
|
2519
|
+
// score and still qualify — while cutting the type-stub waste. See §5 F1 of
|
|
2520
|
+
// docs/design/improve-salience-working-reference.md and #608/#644.
|
|
2474
2521
|
const highSalienceRefs = [];
|
|
2475
2522
|
const salienceCfg = (options.config ?? loadConfig()).improve?.salience;
|
|
2476
2523
|
const salienceThreshold = salienceCfg?.salienceThreshold ?? 0.75;
|
|
@@ -2486,7 +2533,10 @@ async function runImprovePreparationStage(args) {
|
|
|
2486
2533
|
if (highSalienceRefs.length >= highSalienceCap)
|
|
2487
2534
|
break;
|
|
2488
2535
|
const row = getAssetSalience(dbForHighSalience, r.ref);
|
|
2489
|
-
if (row &&
|
|
2536
|
+
if (row &&
|
|
2537
|
+
isContentEncodingRow(row, parseAssetRef(r.ref).type) &&
|
|
2538
|
+
row.encoding_salience >= salienceThreshold &&
|
|
2539
|
+
!lastReflectProposalTs.has(r.ref)) {
|
|
2490
2540
|
highSalienceRefs.push(r);
|
|
2491
2541
|
}
|
|
2492
2542
|
}
|
|
@@ -2499,6 +2549,10 @@ async function runImprovePreparationStage(args) {
|
|
|
2499
2549
|
if (dbForHighSalience)
|
|
2500
2550
|
dbForHighSalience.close();
|
|
2501
2551
|
}
|
|
2552
|
+
if (highSalienceRefs.length > 0) {
|
|
2553
|
+
info(`[improve] high-salience lane admitted ${highSalienceRefs.length} content-scored ref(s) ` +
|
|
2554
|
+
`(threshold=${salienceThreshold}, requires content-derived encoding_source)`);
|
|
2555
|
+
}
|
|
2502
2556
|
}
|
|
2503
2557
|
// Record an in-memory skip action for every zero-feedback ref that the
|
|
2504
2558
|
// partition loop deferred to P0-A but P0-A then declined (retrievalCount below
|
|
@@ -144,8 +144,11 @@ export function isJunkTag(tag) {
|
|
|
144
144
|
* - `"both"` — union of the tag and entity grouping keys.
|
|
145
145
|
*
|
|
146
146
|
* A cluster is a signal whose member set is >= `minClusterSize`. Overlapping
|
|
147
|
-
* clusters are de-duplicated by member-set identity, and the result is
|
|
148
|
-
*
|
|
147
|
+
* clusters are de-duplicated by member-set identity, and the result is RANKED
|
|
148
|
+
* by member-count descending (deterministic alphabetical tiebreak). The
|
|
149
|
+
* `maxClustersPerRun` cap is NOT applied here — call {@link capClusters} on the
|
|
150
|
+
* result for the processed slice; the full ranked list is retained so the
|
|
151
|
+
* cap-aware decay sweep can tell cap-displacement from corpus absence (#658).
|
|
149
152
|
*/
|
|
150
153
|
export function buildRelatednessClusters(entries, opts) {
|
|
151
154
|
// Only consolidation-eligible memories participate (exclude `.derived`).
|
|
@@ -213,9 +216,24 @@ export function buildRelatednessClusters(entries, opts) {
|
|
|
213
216
|
seenMemberKeys.add(memberKey);
|
|
214
217
|
return true;
|
|
215
218
|
});
|
|
216
|
-
//
|
|
219
|
+
// Rank largest-first (deterministic alphabetical tiebreak). The cap is applied
|
|
220
|
+
// by the caller via {@link capClusters}, NOT here, so the FULL formed set
|
|
221
|
+
// (every cluster that genuinely re-forms this run) stays available for the
|
|
222
|
+
// cap-aware decay sweep — a cluster displaced by the cap must not be confused
|
|
223
|
+
// with a cluster that vanished from the corpus (#658).
|
|
217
224
|
clusters.sort((a, b) => b.members.length - a.members.length || a.signature.localeCompare(b.signature));
|
|
218
|
-
return clusters
|
|
225
|
+
return clusters;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* #658 — apply the `maxClustersPerRun` cap to a largest-first ranked cluster
|
|
229
|
+
* list. Split out from {@link buildRelatednessClusters} so callers retain the
|
|
230
|
+
* full pre-cap set: the clusters BELOW the cap still re-formed this run and must
|
|
231
|
+
* spare their hypotheses from decay (cap-displacement is a SCHEDULING miss, not
|
|
232
|
+
* a substance miss). Callers that only need the processed slice call this; the
|
|
233
|
+
* full ranked list feeds {@link decayUnseenRecombineHypotheses}.
|
|
234
|
+
*/
|
|
235
|
+
export function capClusters(ranked, maxClustersPerRun) {
|
|
236
|
+
return ranked.slice(0, Math.max(0, maxClustersPerRun));
|
|
219
237
|
}
|
|
220
238
|
// ── Prompt + ref derivation ───────────────────────────────────────────────────
|
|
221
239
|
/** Read a memory body (frontmatter stripped) for the cluster prompt. */
|
|
@@ -380,14 +398,18 @@ export async function akmRecombine(opts) {
|
|
|
380
398
|
if (db)
|
|
381
399
|
closeDatabase(db);
|
|
382
400
|
}
|
|
383
|
-
|
|
401
|
+
// #658 — `rankedClusters` is the FULL set that re-formed this run (ranked,
|
|
402
|
+
// pre-cap); `clusters` is the processed top-`maxClustersPerRun` slice. The
|
|
403
|
+
// decay sweep below uses the full set so a cap-displaced (but present)
|
|
404
|
+
// cluster spares its hypothesis from reset.
|
|
405
|
+
const rankedClusters = buildRelatednessClusters(entries, {
|
|
384
406
|
minClusterSize,
|
|
385
|
-
maxClustersPerRun,
|
|
386
407
|
relatednessSource,
|
|
387
408
|
...(entityByEntryId ? { entityByEntryId } : {}),
|
|
388
409
|
...(opts.maxClusterSize != null ? { maxClusterSize: opts.maxClusterSize } : {}),
|
|
389
410
|
...(opts.excludeTags ? { excludeTags: opts.excludeTags } : {}),
|
|
390
411
|
});
|
|
412
|
+
const clusters = capClusters(rankedClusters, maxClustersPerRun);
|
|
391
413
|
let clustersFormed = 0;
|
|
392
414
|
let proposalsEmitted = 0;
|
|
393
415
|
let lessonsPromoted = 0;
|
|
@@ -570,8 +592,22 @@ export async function akmRecombine(opts) {
|
|
|
570
592
|
}
|
|
571
593
|
// #625 — decay hypotheses NOT re-induced this run (reset their consecutive
|
|
572
594
|
// streak) so confirmation is per-consecutive-run and conservative (AC4).
|
|
595
|
+
// #658 — but a hypothesis whose cluster genuinely re-formed this run and was
|
|
596
|
+
// merely cap-displaced (outside the top-`maxClustersPerRun` slice) must NOT
|
|
597
|
+
// be decayed — that is a scheduling miss, not a substance miss. We pass
|
|
598
|
+
// EVERY cluster that formed this run (the full pre-cap `rankedClusters`) as
|
|
599
|
+
// `presentClusters`; decay spares any row that Jaccard-matches a present
|
|
600
|
+
// cluster under the SAME overlap rule used for re-induction. Only rows with
|
|
601
|
+
// no matching current cluster (the corpus stopped supporting them) decay.
|
|
573
602
|
if (stateDb) {
|
|
574
|
-
const
|
|
603
|
+
const presentClusters = rankedClusters.map((c) => ({
|
|
604
|
+
signature: c.signature,
|
|
605
|
+
memberKey: recombineMemberKey(c),
|
|
606
|
+
}));
|
|
607
|
+
const decayedCount = decayUnseenRecombineHypotheses(stateDb, sourceRun, [...seenThisRun], {
|
|
608
|
+
presentClusters,
|
|
609
|
+
minOverlap: DEFAULT_RECOMBINE_OVERLAP,
|
|
610
|
+
});
|
|
575
611
|
if (decayedCount > 0) {
|
|
576
612
|
appendEvent({
|
|
577
613
|
eventType: "recombine_invoked",
|
|
@@ -29,6 +29,7 @@ import { parseAssetRef } from "../../core/asset/asset-ref.js";
|
|
|
29
29
|
import { assembleAssetFromString, serializeFrontmatter } from "../../core/asset/asset-serialize.js";
|
|
30
30
|
import { parseFrontmatter } from "../../core/asset/frontmatter.js";
|
|
31
31
|
import { stripMarkdownFences } from "../../core/asset/markdown.js";
|
|
32
|
+
import { DESCRIPTION_MAX_CHARS, requiresDescription } from "../../core/authoring-rules.js";
|
|
32
33
|
import { resolveStashDir } from "../../core/common.js";
|
|
33
34
|
import { loadConfig } from "../../core/config/config.js";
|
|
34
35
|
import { ConfigError, UsageError } from "../../core/errors.js";
|
|
@@ -44,7 +45,7 @@ import { runOpencodeSdk } from "../../integrations/harnesses/opencode-sdk/index.
|
|
|
44
45
|
import { chatCompletion } from "../../llm/client.js";
|
|
45
46
|
import { isLlmFeatureEnabled } from "../../llm/feature-gate.js";
|
|
46
47
|
import { baseFailureFields, enoentHintMessage, isEnoentFailure, loadAgentConfigFromDisk, resolveAgentProfile, } from "../agent/agent-support.js";
|
|
47
|
-
import { checkReflectSize } from "../proposal/validators/proposal-quality-validators.js";
|
|
48
|
+
import { checkReflectSize, isValidDescription } from "../proposal/validators/proposal-quality-validators.js";
|
|
48
49
|
import { createProposal, isProposalSkipped, listProposals, } from "../proposal/validators/proposals.js";
|
|
49
50
|
import { deriveLessonRef, runLessonQualityJudge } from "./distill.js";
|
|
50
51
|
import { classifyReflectChange } from "./reflect-noise.js";
|
|
@@ -403,6 +404,82 @@ function stripAppendedFrontmatter(body) {
|
|
|
403
404
|
return body;
|
|
404
405
|
return body.slice(0, body.indexOf(match[0])).replace(/\s+$/, "");
|
|
405
406
|
}
|
|
407
|
+
/**
|
|
408
|
+
* #636 — deterministically derive a valid `description` from an asset's existing
|
|
409
|
+
* metadata when one is missing. Sources, in priority order: the `title:`
|
|
410
|
+
* frontmatter field, the first `# Heading` in the (proposed or source) body, and
|
|
411
|
+
* the first sentence of the opening body paragraph. The candidate is normalized
|
|
412
|
+
* (whitespace collapsed, trailing punctuation/markdown stripped, clamped to the
|
|
413
|
+
* description max) and only returned if it PASSES `isValidDescription` — so this
|
|
414
|
+
* never produces a heading-fragment, truncated, or otherwise gate-failing value.
|
|
415
|
+
* Returns `undefined` when nothing usable can be derived (caller leaves the
|
|
416
|
+
* proposal as-is rather than fabricating prose).
|
|
417
|
+
*
|
|
418
|
+
* This is intentionally deterministic and lives in the reflect proposal-build
|
|
419
|
+
* path — it does NOT touch the validators or the promote-time repair.
|
|
420
|
+
*/
|
|
421
|
+
function deriveDescriptionFromAsset(title, proposedBody, sourceBody, targetRef) {
|
|
422
|
+
// Each candidate is tagged with its kind. A title or `# Heading` is a bare
|
|
423
|
+
// fragment ("Paged.js — Named Page") that reads poorly as a description even
|
|
424
|
+
// when it is long enough to pass the length gate, so for those we prefer the
|
|
425
|
+
// padded sentence form. A prose sentence is already a sentence, so it is used
|
|
426
|
+
// as-is (padding it would double-wrap an already-complete sentence).
|
|
427
|
+
const candidates = [];
|
|
428
|
+
// 1. title: frontmatter
|
|
429
|
+
if (typeof title === "string" && title.trim())
|
|
430
|
+
candidates.push({ text: title.trim(), kind: "fragment" });
|
|
431
|
+
// 2. first `# Heading` (proposed body first, then source body)
|
|
432
|
+
for (const body of [proposedBody, sourceBody]) {
|
|
433
|
+
const headingMatch = body.match(/^#{1,6}\s+(.+?)\s*$/m);
|
|
434
|
+
if (headingMatch?.[1])
|
|
435
|
+
candidates.push({ text: headingMatch[1].trim(), kind: "fragment" });
|
|
436
|
+
}
|
|
437
|
+
// 3. first sentence of the opening prose paragraph (skip headings, fences,
|
|
438
|
+
// list markers, blockquotes — those are not prose).
|
|
439
|
+
for (const body of [proposedBody, sourceBody]) {
|
|
440
|
+
const firstSentence = firstProseSentence(body);
|
|
441
|
+
if (firstSentence)
|
|
442
|
+
candidates.push({ text: firstSentence, kind: "prose" });
|
|
443
|
+
}
|
|
444
|
+
for (const { text, kind } of candidates) {
|
|
445
|
+
const normalized = normalizeDescriptionCandidate(text);
|
|
446
|
+
if (!normalized)
|
|
447
|
+
continue;
|
|
448
|
+
// For a title/heading fragment, try the padded sentence form FIRST so the
|
|
449
|
+
// result reads as a sentence rather than a bare fragment — a short but valid
|
|
450
|
+
// title like "Paged.js — Named Page" (21 chars) would otherwise be returned
|
|
451
|
+
// verbatim. Fall back to the bare form only if the padded form fails the
|
|
452
|
+
// gate. A prose candidate is already a sentence, so it is used as-is.
|
|
453
|
+
const variants = kind === "fragment" ? [`Reference notes on ${normalized}.`, normalized] : [normalized];
|
|
454
|
+
for (const v of variants) {
|
|
455
|
+
const clamped = v.length > DESCRIPTION_MAX_CHARS ? v.slice(0, DESCRIPTION_MAX_CHARS).trimEnd() : v;
|
|
456
|
+
if (isValidDescription(clamped, targetRef, { skipRefTailCheck: true }).ok)
|
|
457
|
+
return clamped;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
return undefined;
|
|
461
|
+
}
|
|
462
|
+
/** Extract the first prose sentence from a markdown body, or `""` if none. */
|
|
463
|
+
function firstProseSentence(body) {
|
|
464
|
+
for (const rawLine of body.split(/\r?\n/)) {
|
|
465
|
+
const line = rawLine.trim();
|
|
466
|
+
if (!line)
|
|
467
|
+
continue;
|
|
468
|
+
if (/^(#{1,6}\s|```|~~~|[-*+]\s|\d+\.\s|>|\||<!--)/.test(line))
|
|
469
|
+
continue;
|
|
470
|
+
const sentenceMatch = line.match(/^(.+?[.!?])(\s|$)/);
|
|
471
|
+
return (sentenceMatch?.[1] ?? line).trim();
|
|
472
|
+
}
|
|
473
|
+
return "";
|
|
474
|
+
}
|
|
475
|
+
/** Normalize a description candidate: strip markdown markers, collapse space. */
|
|
476
|
+
function normalizeDescriptionCandidate(raw) {
|
|
477
|
+
return raw
|
|
478
|
+
.replace(/`/g, "")
|
|
479
|
+
.replace(/^[#>*\-\s]+/, "")
|
|
480
|
+
.replace(/\s+/g, " ")
|
|
481
|
+
.trim();
|
|
482
|
+
}
|
|
406
483
|
/**
|
|
407
484
|
* Reflect post-processor — enforces the safety rails described at the top of
|
|
408
485
|
* this file:
|
|
@@ -428,7 +505,7 @@ function stripAppendedFrontmatter(body) {
|
|
|
428
505
|
* from `payload.frontmatter` so identity fields can be enforced. Size guard
|
|
429
506
|
* is skipped because there is no source to compare against.
|
|
430
507
|
*/
|
|
431
|
-
function sanitizeReflectPayload(payload, sourceContent, targetRef) {
|
|
508
|
+
export function sanitizeReflectPayload(payload, sourceContent, targetRef) {
|
|
432
509
|
const warnings = [];
|
|
433
510
|
const { fmText: sourceFmText, body: sourceBody } = sourceContent
|
|
434
511
|
? splitFrontmatter(sourceContent)
|
|
@@ -470,6 +547,34 @@ function sanitizeReflectPayload(payload, sourceContent, targetRef) {
|
|
|
470
547
|
}
|
|
471
548
|
}
|
|
472
549
|
const cleanedBody = stripAppendedFrontmatter(rawLlmBody.replace(/^\s+/, ""));
|
|
550
|
+
// #636 — deterministic description fallback (reflect-side belt-and-suspenders).
|
|
551
|
+
// If the type requires a `description` and the merged frontmatter is still
|
|
552
|
+
// MISSING one (source had none AND the model didn't author one), derive a
|
|
553
|
+
// description DETERMINISTICALLY from the existing `title:` frontmatter or the
|
|
554
|
+
// first `# Heading` / opening body sentence — never free-form invention. This
|
|
555
|
+
// runs in the reflect proposal-build path, BEFORE the proposal is created, so
|
|
556
|
+
// the validator/promote path is left untouched (no gate fabricates content).
|
|
557
|
+
//
|
|
558
|
+
// Scope is the issue's target: a source asset that ALREADY carries frontmatter
|
|
559
|
+
// (e.g. scraped docs: `source`/`title`/`scraped`) but has a MISSING/empty
|
|
560
|
+
// `description`. We deliberately do NOT fire when:
|
|
561
|
+
// - the source has no frontmatter block at all (injecting one would be a
|
|
562
|
+
// structural change and would defeat the #580 no-op/cosmetic noise gate
|
|
563
|
+
// for a pure body echo), or
|
|
564
|
+
// - a present-but-otherwise-invalid description exists (too short, a heading
|
|
565
|
+
// fragment) — overwriting authored content is out of scope; the prompt
|
|
566
|
+
// instruction handles improving it instead.
|
|
567
|
+
const refType = targetRef.includes(":") ? (targetRef.split(":")[0] ?? "") : "";
|
|
568
|
+
const mergedDesc = mergedFm.description;
|
|
569
|
+
const descIsMissing = typeof mergedDesc !== "string" || mergedDesc.trim().length === 0;
|
|
570
|
+
const sourceHadFrontmatter = sourceFmText !== null && Object.keys(sourceFm).length > 0;
|
|
571
|
+
if (refType && requiresDescription(refType) && descIsMissing && sourceHadFrontmatter) {
|
|
572
|
+
const derived = deriveDescriptionFromAsset(mergedFm.title, cleanedBody, sourceBody, targetRef);
|
|
573
|
+
if (derived) {
|
|
574
|
+
mergedFm.description = derived;
|
|
575
|
+
warnings.push("Synthesized a deterministic `description` from title/heading (#636) — source and proposal lacked one.");
|
|
576
|
+
}
|
|
577
|
+
}
|
|
473
578
|
// Size guard — only when source body is meaningfully large. The pure
|
|
474
579
|
// predicate lives in `core/proposal-quality-validators` so the same check
|
|
475
580
|
// also runs inside `runProposalValidators` on `proposal accept`.
|
|
@@ -70,6 +70,15 @@ const WHEN_TO_USE_TYPES = new Set(["lesson"]);
|
|
|
70
70
|
* Inject this VERBATIM into every improve/authoring prompt that creates or edits
|
|
71
71
|
* an asset of `type`, so the agent is told exactly what the gate will reject.
|
|
72
72
|
*/
|
|
73
|
+
/**
|
|
74
|
+
* Whether an asset of `type` carries a `description` that the validator
|
|
75
|
+
* (`isValidDescription` / `validateProposalFrontmatter`) treats as required.
|
|
76
|
+
* Reflect uses this to decide when a description-synthesis instruction (and the
|
|
77
|
+
* deterministic reflect-side fallback) must fire for a description-less source.
|
|
78
|
+
*/
|
|
79
|
+
export function requiresDescription(type) {
|
|
80
|
+
return DESCRIPTION_TYPES.has(type);
|
|
81
|
+
}
|
|
73
82
|
export function authoringRulesForType(type) {
|
|
74
83
|
const rules = [...FRONTMATTER_BODY_RULES];
|
|
75
84
|
if (DESCRIPTION_TYPES.has(type))
|
package/dist/core/state-db.js
CHANGED
|
@@ -1708,6 +1708,35 @@ export function getRecombineHypothesis(db, hypothesisRef) {
|
|
|
1708
1708
|
export function markRecombineHypothesisPromoted(db, hypothesisRef, promotedAt) {
|
|
1709
1709
|
db.prepare("UPDATE recombine_hypotheses SET promoted_at = ?, consecutive_count = 0 WHERE hypothesis_ref = ?").run(promotedAt, hypothesisRef);
|
|
1710
1710
|
}
|
|
1711
|
+
/**
|
|
1712
|
+
* #658 — does any current-run cluster match this hypothesis row under the SAME
|
|
1713
|
+
* signature + Jaccard-overlap rule used for re-induction? A match means the
|
|
1714
|
+
* cluster genuinely re-formed this run (it was merely cap-displaced out of the
|
|
1715
|
+
* processed top-`maxClustersPerRun` slice), so its streak must NOT be reset.
|
|
1716
|
+
*/
|
|
1717
|
+
function hypothesisMatchesAnyPresentCluster(row, presentClusters, minOverlap) {
|
|
1718
|
+
const rowMembers = row.member_key.split("|").filter((m) => m.length > 0);
|
|
1719
|
+
if (rowMembers.length === 0)
|
|
1720
|
+
return false;
|
|
1721
|
+
const rowSet = new Set(rowMembers);
|
|
1722
|
+
for (const cluster of presentClusters) {
|
|
1723
|
+
if (cluster.signature !== row.signature)
|
|
1724
|
+
continue;
|
|
1725
|
+
const clusterMembers = cluster.memberKey.split("|").filter((m) => m.length > 0);
|
|
1726
|
+
if (clusterMembers.length === 0)
|
|
1727
|
+
continue;
|
|
1728
|
+
let intersection = 0;
|
|
1729
|
+
for (const m of clusterMembers) {
|
|
1730
|
+
if (rowSet.has(m))
|
|
1731
|
+
intersection += 1;
|
|
1732
|
+
}
|
|
1733
|
+
const union = rowSet.size + clusterMembers.length - intersection;
|
|
1734
|
+
const overlap = union === 0 ? 0 : intersection / union;
|
|
1735
|
+
if (overlap >= minOverlap)
|
|
1736
|
+
return true;
|
|
1737
|
+
}
|
|
1738
|
+
return false;
|
|
1739
|
+
}
|
|
1711
1740
|
/**
|
|
1712
1741
|
* Decay-to-zero every NON-promoted hypothesis NOT re-induced in the current run.
|
|
1713
1742
|
*
|
|
@@ -1718,9 +1747,50 @@ export function markRecombineHypothesisPromoted(db, hypothesisRef, promotedAt) {
|
|
|
1718
1747
|
*
|
|
1719
1748
|
* Only rows whose `hypothesis_ref` is NOT in `seenRefs` AND whose `last_run` is
|
|
1720
1749
|
* NOT the current run are decayed. Already-promoted rows are left alone.
|
|
1750
|
+
*
|
|
1751
|
+
* #658 — CAP-AWARE decay. The recombine pass only re-inducts (and thus marks
|
|
1752
|
+
* `seen`) the top-`maxClustersPerRun` clusters, but a cluster genuinely
|
|
1753
|
+
* re-forms every run even when it is displaced below that cap. Resetting such a
|
|
1754
|
+
* row treats a SCHEDULING miss as a SUBSTANCE miss and traps the hypothesis
|
|
1755
|
+
* below `confirmThreshold` forever. When `opts.presentClusters` is supplied, a
|
|
1756
|
+
* row is SPARED from decay if it Jaccard-matches any present cluster (same
|
|
1757
|
+
* signature, overlap >= `opts.minOverlap`) — i.e. its cluster re-formed this run
|
|
1758
|
+
* but was cap-displaced. This does NOT advance the streak (only re-induction in
|
|
1759
|
+
* the processed slice does that, via {@link recordRecombineInduction}), so the
|
|
1760
|
+
* recurrence bar for promotion is unchanged; it only stops the cap from
|
|
1761
|
+
* manufacturing artificial misses. Omitting `presentClusters` preserves the
|
|
1762
|
+
* pre-#658 hard-reset-after-one-miss behaviour exactly.
|
|
1763
|
+
*
|
|
1721
1764
|
* Returns the number of rows reset.
|
|
1722
1765
|
*/
|
|
1723
|
-
export function decayUnseenRecombineHypotheses(db, currentRun, seenRefs) {
|
|
1766
|
+
export function decayUnseenRecombineHypotheses(db, currentRun, seenRefs, opts) {
|
|
1767
|
+
// #658 — when cap-aware sparing is requested, fold the cap-displaced rows into
|
|
1768
|
+
// the "seen" exclusion set: the underlying reset SQL already protects every
|
|
1769
|
+
// ref it is given, so sparing == treating a spared row exactly like a seen
|
|
1770
|
+
// row for this sweep (its count is left untouched, never advanced).
|
|
1771
|
+
let effectiveSeen = seenRefs;
|
|
1772
|
+
if (opts && opts.presentClusters.length > 0) {
|
|
1773
|
+
const candidates = db
|
|
1774
|
+
.prepare("SELECT hypothesis_ref, signature, member_key FROM recombine_hypotheses WHERE promoted_at IS NULL AND (last_run IS NULL OR last_run != ?) AND consecutive_count != 0")
|
|
1775
|
+
.all(currentRun);
|
|
1776
|
+
const seenSet = new Set(seenRefs);
|
|
1777
|
+
for (const row of candidates) {
|
|
1778
|
+
if (seenSet.has(row.hypothesis_ref))
|
|
1779
|
+
continue;
|
|
1780
|
+
if (hypothesisMatchesAnyPresentCluster(row, opts.presentClusters, opts.minOverlap)) {
|
|
1781
|
+
seenSet.add(row.hypothesis_ref);
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
effectiveSeen = [...seenSet];
|
|
1785
|
+
}
|
|
1786
|
+
return decayUnseenRecombineHypothesesInner(db, currentRun, effectiveSeen);
|
|
1787
|
+
}
|
|
1788
|
+
/**
|
|
1789
|
+
* The raw reset sweep shared by the cap-aware wrapper above. Resets every
|
|
1790
|
+
* non-promoted row from a prior run whose ref is NOT in `seenRefs`. Kept private
|
|
1791
|
+
* so the param-ceiling chunking logic lives in one place.
|
|
1792
|
+
*/
|
|
1793
|
+
function decayUnseenRecombineHypothesesInner(db, currentRun, seenRefs) {
|
|
1724
1794
|
// Reset every eligible row, then exclude the seen refs in chunks to respect
|
|
1725
1795
|
// the ~999 SQLite param ceiling. With no seen refs we reset all non-promoted
|
|
1726
1796
|
// rows from prior runs in a single statement.
|
|
@@ -1732,9 +1802,16 @@ export function decayUnseenRecombineHypotheses(db, currentRun, seenRefs) {
|
|
|
1732
1802
|
}
|
|
1733
1803
|
// A single NOT IN keeps the exclusion atomic (a chunked NOT IN would let a ref
|
|
1734
1804
|
// excluded by one chunk still be reset by another chunk's statement). The
|
|
1735
|
-
// recombine pass caps
|
|
1736
|
-
//
|
|
1737
|
-
//
|
|
1805
|
+
// recombine pass caps RE-INDUCED clusters at `maxClustersPerRun` (a handful) —
|
|
1806
|
+
// but with #658 cap-aware sparing the caller folds every cap-displaced
|
|
1807
|
+
// (present-but-unprocessed) hypothesis into `effectiveSeen` too, so on a large
|
|
1808
|
+
// stash `seenRefs` here can carry MANY spared refs, not just the handful that
|
|
1809
|
+
// were processed. We cap defensively at ~900 (under SQLite's ~999 param
|
|
1810
|
+
// ceiling): if `effectiveSeen` somehow exceeds it we fall back to resetting all
|
|
1811
|
+
// eligible rows — which re-introduces the cap-displacement trap for THAT run
|
|
1812
|
+
// (spared rows get decayed because the NOT IN protection is dropped). That is a
|
|
1813
|
+
// rare, bounded degradation; a stash with >900 simultaneously-spared
|
|
1814
|
+
// hypotheses is far beyond current scale.
|
|
1738
1815
|
if (seenRefs.length > 900) {
|
|
1739
1816
|
const res = db
|
|
1740
1817
|
.prepare("UPDATE recombine_hypotheses SET consecutive_count = 0 WHERE promoted_at IS NULL AND (last_run IS NULL OR last_run != ?) AND consecutive_count != 0")
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
* during validation. We carry it through if the agent supplies it.
|
|
27
27
|
*/
|
|
28
28
|
import { TYPE_DIRS } from "../../core/asset/asset-spec.js";
|
|
29
|
-
import { authoringRulesForType } from "../../core/authoring-rules.js";
|
|
29
|
+
import { authoringRulesForType, DESCRIPTION_MAX_CHARS, DESCRIPTION_MIN_CHARS, requiresDescription, } from "../../core/authoring-rules.js";
|
|
30
30
|
import { parseEmbeddedJsonResponse, stripCodeFences, stripThinkBlocks } from "../../core/parse.js";
|
|
31
31
|
/**
|
|
32
32
|
* Per-asset-type frontmatter / authoring hints surfaced in the prompt so
|
|
@@ -111,6 +111,30 @@ export function extractDraftConfidence(stdout) {
|
|
|
111
111
|
return undefined;
|
|
112
112
|
return value;
|
|
113
113
|
}
|
|
114
|
+
/**
|
|
115
|
+
* Whether the source asset content has a non-empty `description:` key in its
|
|
116
|
+
* YAML frontmatter. Used by {@link buildReflectPrompt} (#636) to decide whether
|
|
117
|
+
* to inject the synthesize-a-description instruction. Uses an inline regex to
|
|
118
|
+
* avoid pulling the full YAML parser into the prompt module (mirrors the
|
|
119
|
+
* existing inline frontmatter handling here).
|
|
120
|
+
*/
|
|
121
|
+
function sourceHasNonEmptyDescription(assetContent) {
|
|
122
|
+
if (!assetContent)
|
|
123
|
+
return false;
|
|
124
|
+
const fmMatch = assetContent.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
|
|
125
|
+
if (!fmMatch)
|
|
126
|
+
return false;
|
|
127
|
+
const fmBlock = fmMatch[1] ?? "";
|
|
128
|
+
// Match a top-level `description:` line and capture its inline value.
|
|
129
|
+
const descMatch = fmBlock.match(/^description\s*:\s*(.*)$/m);
|
|
130
|
+
if (!descMatch)
|
|
131
|
+
return false;
|
|
132
|
+
const value = (descMatch[1] ?? "")
|
|
133
|
+
.trim()
|
|
134
|
+
.replace(/^['"]|['"]$/g, "")
|
|
135
|
+
.trim();
|
|
136
|
+
return value.length > 0;
|
|
137
|
+
}
|
|
114
138
|
/**
|
|
115
139
|
* Build the prompt for `akm reflect [ref]`. Asks the agent to review an
|
|
116
140
|
* existing asset (plus any negative feedback / lint findings) and propose
|
|
@@ -167,6 +191,23 @@ export function buildReflectPrompt(input) {
|
|
|
167
191
|
if (authoringRules) {
|
|
168
192
|
sections.push(authoringRules);
|
|
169
193
|
}
|
|
194
|
+
// #636 — synthesize-a-description instruction. Many source assets (notably
|
|
195
|
+
// scraped docs: `source`/`title`/`scraped`) carry frontmatter but NO
|
|
196
|
+
// `description`. Reflect echoes the source frontmatter, so the proposal
|
|
197
|
+
// inherits the missing description and the promote-time validator
|
|
198
|
+
// (isValidDescription, 20–400 chars) rejects it. The fix is at GENERATION
|
|
199
|
+
// time: when the source lacks a non-empty `description` and the type
|
|
200
|
+
// requires one, tell the model — unmissably — that it MUST author a valid
|
|
201
|
+
// `description`. (The validator/promote path is NOT changed: it must never
|
|
202
|
+
// fabricate content to pass itself.)
|
|
203
|
+
if (resolvedType && requiresDescription(resolvedType) && !sourceHasNonEmptyDescription(input.assetContent)) {
|
|
204
|
+
sections.push([
|
|
205
|
+
"REQUIRED — synthesize a `description` (the source asset has none):",
|
|
206
|
+
`- The source frontmatter does NOT include a non-empty \`description\`, but a ${resolvedType} asset REQUIRES one or the proposal will be rejected at promote time.`,
|
|
207
|
+
`- You MUST author a valid \`description\` in the proposal frontmatter: ${DESCRIPTION_MIN_CHARS}–${DESCRIPTION_MAX_CHARS} characters of plain-prose sentence summarizing what this asset is about.`,
|
|
208
|
+
'- Synthesize it from the asset\'s `title:` frontmatter, its first `# Heading`, or the opening body sentence. Do NOT copy a bare heading fragment (e.g. "Overview", "Named Page", "Key Insight") and do NOT emit a truncated phrase that ends on `:`/`;`/`,` or a hanging connector word.',
|
|
209
|
+
].join("\n"));
|
|
210
|
+
}
|
|
170
211
|
}
|
|
171
212
|
if (input.assetContent?.trim()) {
|
|
172
213
|
// Cap at 12 000 chars to stay well under OS ARG_MAX when the prompt is
|
|
@@ -9520,7 +9520,46 @@ function getRecombineHypothesis(db, hypothesisRef) {
|
|
|
9520
9520
|
function markRecombineHypothesisPromoted(db, hypothesisRef, promotedAt) {
|
|
9521
9521
|
db.prepare("UPDATE recombine_hypotheses SET promoted_at = ?, consecutive_count = 0 WHERE hypothesis_ref = ?").run(promotedAt, hypothesisRef);
|
|
9522
9522
|
}
|
|
9523
|
-
function
|
|
9523
|
+
function hypothesisMatchesAnyPresentCluster(row, presentClusters, minOverlap) {
|
|
9524
|
+
const rowMembers = row.member_key.split("|").filter((m) => m.length > 0);
|
|
9525
|
+
if (rowMembers.length === 0)
|
|
9526
|
+
return false;
|
|
9527
|
+
const rowSet = new Set(rowMembers);
|
|
9528
|
+
for (const cluster of presentClusters) {
|
|
9529
|
+
if (cluster.signature !== row.signature)
|
|
9530
|
+
continue;
|
|
9531
|
+
const clusterMembers = cluster.memberKey.split("|").filter((m) => m.length > 0);
|
|
9532
|
+
if (clusterMembers.length === 0)
|
|
9533
|
+
continue;
|
|
9534
|
+
let intersection = 0;
|
|
9535
|
+
for (const m of clusterMembers) {
|
|
9536
|
+
if (rowSet.has(m))
|
|
9537
|
+
intersection += 1;
|
|
9538
|
+
}
|
|
9539
|
+
const union = rowSet.size + clusterMembers.length - intersection;
|
|
9540
|
+
const overlap = union === 0 ? 0 : intersection / union;
|
|
9541
|
+
if (overlap >= minOverlap)
|
|
9542
|
+
return true;
|
|
9543
|
+
}
|
|
9544
|
+
return false;
|
|
9545
|
+
}
|
|
9546
|
+
function decayUnseenRecombineHypotheses(db, currentRun, seenRefs, opts) {
|
|
9547
|
+
let effectiveSeen = seenRefs;
|
|
9548
|
+
if (opts && opts.presentClusters.length > 0) {
|
|
9549
|
+
const candidates = db.prepare("SELECT hypothesis_ref, signature, member_key FROM recombine_hypotheses WHERE promoted_at IS NULL AND (last_run IS NULL OR last_run != ?) AND consecutive_count != 0").all(currentRun);
|
|
9550
|
+
const seenSet = new Set(seenRefs);
|
|
9551
|
+
for (const row of candidates) {
|
|
9552
|
+
if (seenSet.has(row.hypothesis_ref))
|
|
9553
|
+
continue;
|
|
9554
|
+
if (hypothesisMatchesAnyPresentCluster(row, opts.presentClusters, opts.minOverlap)) {
|
|
9555
|
+
seenSet.add(row.hypothesis_ref);
|
|
9556
|
+
}
|
|
9557
|
+
}
|
|
9558
|
+
effectiveSeen = [...seenSet];
|
|
9559
|
+
}
|
|
9560
|
+
return decayUnseenRecombineHypothesesInner(db, currentRun, effectiveSeen);
|
|
9561
|
+
}
|
|
9562
|
+
function decayUnseenRecombineHypothesesInner(db, currentRun, seenRefs) {
|
|
9524
9563
|
if (seenRefs.length === 0) {
|
|
9525
9564
|
const res2 = db.prepare("UPDATE recombine_hypotheses SET consecutive_count = 0 WHERE promoted_at IS NULL AND (last_run IS NULL OR last_run != ?) AND consecutive_count != 0").run(currentRun);
|
|
9526
9565
|
return Number(res2.changes);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akm-cli",
|
|
3
|
-
"version": "0.9.0-beta.
|
|
3
|
+
"version": "0.9.0-beta.43",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "akm (Agent Knowledge Management) — A package manager for AI agent skills, commands, tools, and knowledge. Works with Claude Code, OpenCode, Cursor, and any AI coding assistant.",
|
|
6
6
|
"keywords": [
|