akm-cli 0.9.0-beta.34 → 0.9.0-beta.36
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/prompts/distill-lesson-system.md +1 -1
- package/dist/assets/prompts/extract-session.md +1 -1
- package/dist/commands/improve/consolidate.js +12 -2
- package/dist/commands/improve/distill.js +18 -0
- package/dist/commands/improve/extract-prompt.js +6 -0
- package/dist/commands/improve/extract.js +151 -6
- package/dist/commands/improve/improve.js +18 -2
- package/dist/commands/improve/procedural.js +12 -3
- package/dist/commands/improve/recombine.js +11 -2
- package/dist/commands/improve/reflect.js +5 -0
- package/dist/commands/proposal/drain.js +7 -0
- package/dist/commands/proposal/propose.js +5 -0
- package/dist/commands/proposal/validators/proposal-quality-validators.js +9 -8
- package/dist/commands/proposal/validators/proposals.js +116 -3
- package/dist/commands/sources/schema-repair.js +13 -1
- package/dist/core/authoring-rules.js +83 -0
- package/dist/core/config/config-schema.js +5 -0
- package/dist/core/standards/resolve-standards-context.js +56 -0
- package/dist/core/standards/resolve-stash-standards.js +87 -0
- package/dist/core/state-db.js +17 -0
- package/dist/integrations/agent/prompts.js +32 -0
- package/dist/scripts/migrate-storage.js +8 -0
- package/dist/wiki/wiki.js +37 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,69 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.9.0-beta.36] — 2026-06-22
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **Stash standards + wiki schemas are surfaced to authoring agents at write
|
|
14
|
+
time.** When an agent edits a page under `wikis/<name>/`, that wiki's
|
|
15
|
+
`schema.md` body is injected into the prompt; when it creates/edits a non-wiki
|
|
16
|
+
asset, the bodies of `category: convention`/`meta` `fact` assets are injected.
|
|
17
|
+
Two mutually-exclusive features selected by target type, sharing one
|
|
18
|
+
`standardsContext` prompt seam. Wired into reflect, propose, and every
|
|
19
|
+
improve authoring pass (distill, consolidate, recombine, procedural, extract,
|
|
20
|
+
schema-repair). (#642)
|
|
21
|
+
- **Unified, validator-sourced authoring-rules seam.** A new
|
|
22
|
+
`authoringRulesForType(type)` injects the hard authoring rules (no
|
|
23
|
+
pseudo-frontmatter in body, exactly two `---` fences, description/`when_to_use`
|
|
24
|
+
length + shape) into every authoring prompt. The numeric bounds live in one
|
|
25
|
+
module that the validators import, so the prompt can no longer drift from what
|
|
26
|
+
the gate enforces. (#645)
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
|
|
30
|
+
- **High-salience reflect lane now reflects each asset at most once.** The
|
|
31
|
+
`#608` admission gate lacked the cooldown its sibling high-retrieval gate has,
|
|
32
|
+
so zero-feedback assets were re-selected on every run (auto-accept emits
|
|
33
|
+
`promoted`, not `feedback`), burning LLM calls and churning assets. (#643)
|
|
34
|
+
- **Stuck validation-failing proposals no longer dead-end.** The triage drain no
|
|
35
|
+
longer overwrites an `auto-rejected` gate stamp with a misleading
|
|
36
|
+
`auto-accepted` (the failure stays truthful and visible). A bounded,
|
|
37
|
+
content-preserving auto-repair (strip pseudo-frontmatter / stray `---`, repair
|
|
38
|
+
truncated descriptions) runs at the promote boundary and re-validates — fixable
|
|
39
|
+
proposals promote; genuinely unrepairable ones stay `pending` for manual
|
|
40
|
+
review, with nothing fabricated and validation never bypassed. (#645)
|
|
41
|
+
- Corrected a prompt/validator drift where the distill system prompt asked for an
|
|
42
|
+
80–200 char description while the gate enforced 20–400. (#645)
|
|
43
|
+
|
|
44
|
+
## [0.9.0-beta.35] — 2026-06-21
|
|
45
|
+
|
|
46
|
+
### Fixed
|
|
47
|
+
|
|
48
|
+
- **Default extract discovery window is now "since the last run" (floored at 48h),
|
|
49
|
+
not a fixed 24h.** An intermittently-online host that was off for longer than
|
|
50
|
+
the old 24h window could permanently miss sessions that ended during the gap.
|
|
51
|
+
Discovery now looks back to the last recorded extract run for the harness, never
|
|
52
|
+
less than 48h. Widening is free of redundant LLM cost — the content-hash ledger
|
|
53
|
+
skips unchanged sessions with zero LLM calls. An explicit `--since`/`defaultSince`
|
|
54
|
+
still wins.
|
|
55
|
+
- **Per-session lock prevents concurrent double-extraction.** A session-end hook
|
|
56
|
+
firing `extract --session-id` while the periodic `akm improve` extract pass runs
|
|
57
|
+
discovery could both LLM-process the SAME session (duplicate spend + near-dup
|
|
58
|
+
proposals). A per-(harness, session) advisory lock (co-located with state.db,
|
|
59
|
+
PID + age staleness recovery) now makes the second run skip without any LLM call.
|
|
60
|
+
- **`minNewSessions` is read from the ACTIVE improve profile, not always `default`.**
|
|
61
|
+
A non-default profile (e.g. `frequent`) setting `minNewSessions` was silently
|
|
62
|
+
ignored because the gate (and its candidate-count discovery window) read
|
|
63
|
+
`profiles.improve.default`. They now read the resolved active profile, matching
|
|
64
|
+
how `extract.enabled` already resolves.
|
|
65
|
+
|
|
66
|
+
### Docs
|
|
67
|
+
|
|
68
|
+
- Documented that `processes.extract.indexSessions` (default on) makes a second
|
|
69
|
+
LLM call per processed session (the session summary); set it to `false` to halve
|
|
70
|
+
per-session extract cost. Unchanged/skipped sessions still cost zero.
|
|
71
|
+
|
|
9
72
|
## [0.9.0-beta.34] — 2026-06-21
|
|
10
73
|
|
|
11
74
|
### Fixed
|
|
@@ -15,7 +15,7 @@ when_to_use: <one complete sentence describing the concrete trigger condition>
|
|
|
15
15
|
<lesson body — plain markdown, 1–3 short paragraphs of practical guidance>
|
|
16
16
|
|
|
17
17
|
## description field (MANDATORY)
|
|
18
|
-
- A single complete sentence in present tense,
|
|
18
|
+
- A single complete sentence in present tense, 20–400 chars, NO markdown.
|
|
19
19
|
- Self-contained: a reviewer must understand the lesson from this field alone.
|
|
20
20
|
- DO NOT start with "When ", "If ", or a connector word — that belongs in when_to_use.
|
|
21
21
|
- DO NOT copy a section heading ("Key takeaways", "For example", "Key pitfalls").
|
|
@@ -28,7 +28,7 @@ Things NOT to extract:
|
|
|
28
28
|
- Started: {{STARTED_AT}}
|
|
29
29
|
- Ended: {{ENDED_AT}}
|
|
30
30
|
- Project hint: {{PROJECT_HINT}}
|
|
31
|
-
|
|
31
|
+
{{STANDARDS}}
|
|
32
32
|
## Filtered session transcript
|
|
33
33
|
|
|
34
34
|
The transcript below has already had read-only `akm` meta-ops and platform boilerplate stripped. Only content that might carry signal remains.
|
|
@@ -15,6 +15,7 @@ import { getDefaultLlmConfig, loadConfig } from "../../core/config/config.js";
|
|
|
15
15
|
import { ConfigError } from "../../core/errors.js";
|
|
16
16
|
// Note: appendEvent import removed (WS-3a: archive TTL machinery retired)
|
|
17
17
|
import { parseEmbeddedJsonResponse } from "../../core/parse.js";
|
|
18
|
+
import { resolveStashStandards } from "../../core/standards/resolve-stash-standards.js";
|
|
18
19
|
import { detectTruncatedDescription } from "../../core/text-truncation.js";
|
|
19
20
|
import { hasHotCaptureMode, hasSupersededStatus, MERGE_ABSOLUTE_FLOOR_CHARS, MERGE_SHRINK_RATIO_MIN, validateProposalFrontmatter, } from "../proposal/validators/proposal-quality-validators.js";
|
|
20
21
|
import { createProposal, isProposalSkipped, listProposals } from "../proposal/validators/proposals.js";
|
|
@@ -377,7 +378,7 @@ async function clusterMemoriesBySimilarity(memories, config, stateDb) {
|
|
|
377
378
|
* is precomputed once per run by `loadPendingConsolidateProposalHashes`
|
|
378
379
|
* so the cost stays O(memories) inside the chunk loop.
|
|
379
380
|
*/
|
|
380
|
-
export function buildChunkPrompt(sourceName, memories, chunkIndex, totalChunks, bodyTruncation, pendingProposalBodyHashes = new Set()) {
|
|
381
|
+
export function buildChunkPrompt(sourceName, memories, chunkIndex, totalChunks, bodyTruncation, pendingProposalBodyHashes = new Set(), standardsContext = "") {
|
|
381
382
|
const start = memories[0] ? `memory:${memories[0].name}` : "";
|
|
382
383
|
const end = memories[memories.length - 1] ? `memory:${memories[memories.length - 1].name}` : "";
|
|
383
384
|
const annotationsByIndex = [];
|
|
@@ -405,6 +406,11 @@ export function buildChunkPrompt(sourceName, memories, chunkIndex, totalChunks,
|
|
|
405
406
|
`Chunk ${chunkIndex + 1} of ${totalChunks}, memories ${start}–${end}:`,
|
|
406
407
|
"",
|
|
407
408
|
];
|
|
409
|
+
if (standardsContext.trim()) {
|
|
410
|
+
lines.push("Standards to follow (the rulebook for this target):");
|
|
411
|
+
lines.push(standardsContext.trim());
|
|
412
|
+
lines.push("");
|
|
413
|
+
}
|
|
408
414
|
// Top-of-prompt protection block for hot refs. Neutral phrasing — avoid
|
|
409
415
|
// op-words like "promote", "merge", "contradict" so the model doesn't
|
|
410
416
|
// accidentally treat the warning as a hint to use that op elsewhere
|
|
@@ -1246,6 +1252,10 @@ async function akmConsolidateInner(opts, config, stashDir, startMs, sourceRun, w
|
|
|
1246
1252
|
}
|
|
1247
1253
|
warn(`[consolidate] ${memories.length} memories / ${chunks.length} chunk(s) / chunk_size=${chunkSize}` +
|
|
1248
1254
|
` / pending-proposal hashes: ${pendingProposalBodyHashes.size}`);
|
|
1255
|
+
// Consolidate output merges memories (non-wiki) → stash authoring standards.
|
|
1256
|
+
// Resolved ONCE per run and passed to each chunk prompt (facts not re-read
|
|
1257
|
+
// per chunk).
|
|
1258
|
+
const standardsContext = resolveStashStandards(stashDir);
|
|
1249
1259
|
const chunkOpsArrays = [];
|
|
1250
1260
|
// Structured skip-reason histogram (2026-05-26): every deterministic
|
|
1251
1261
|
// post-LLM op rejection site below also calls `pushSkipReason` so the
|
|
@@ -1363,7 +1373,7 @@ async function akmConsolidateInner(opts, config, stashDir, startMs, sourceRun, w
|
|
|
1363
1373
|
continue;
|
|
1364
1374
|
}
|
|
1365
1375
|
warn(`[consolidate] chunk ${chunkIdx + 1}/${chunks.length} (${chunk.length} memories) …`);
|
|
1366
|
-
const userPrompt = buildChunkPrompt(sourceName, chunk, chunkIdx, chunks.length, bodyTruncation, pendingProposalBodyHashes);
|
|
1376
|
+
const userPrompt = buildChunkPrompt(sourceName, chunk, chunkIdx, chunks.length, bodyTruncation, pendingProposalBodyHashes, standardsContext);
|
|
1367
1377
|
let raw = await tryLlmFeature("memory_consolidation", config, async () => {
|
|
1368
1378
|
if (!llmConfig)
|
|
1369
1379
|
return { ok: false, error: "No LLM configured for consolidation" };
|
|
@@ -58,12 +58,14 @@ import { parseAssetRef } from "../../core/asset/asset-ref.js";
|
|
|
58
58
|
import { assembleAssetFromString } from "../../core/asset/asset-serialize.js";
|
|
59
59
|
import { parseFrontmatter, writeSalienceToFrontmatter } from "../../core/asset/frontmatter.js";
|
|
60
60
|
import { stripMarkdownFences } from "../../core/asset/markdown.js";
|
|
61
|
+
import { authoringRulesForType } from "../../core/authoring-rules.js";
|
|
61
62
|
import { resolveStashDir, timestampForFilename } from "../../core/common.js";
|
|
62
63
|
import { getDefaultLlmConfig, loadConfig } from "../../core/config/config.js";
|
|
63
64
|
import { ConfigError, UsageError } from "../../core/errors.js";
|
|
64
65
|
import { appendEvent, readEvents } from "../../core/events.js";
|
|
65
66
|
import { lintLessonContent } from "../../core/lesson-lint.js";
|
|
66
67
|
import { getDbPath } from "../../core/paths.js";
|
|
68
|
+
import { resolveStashStandards } from "../../core/standards/resolve-stash-standards.js";
|
|
67
69
|
import { openStateDatabase } from "../../core/state-db.js";
|
|
68
70
|
import { warnVerbose } from "../../core/warn.js";
|
|
69
71
|
import { closeDatabase, getAllEntries, openDatabase } from "../../indexer/db/db.js";
|
|
@@ -307,6 +309,18 @@ export function buildDistillPrompt(input) {
|
|
|
307
309
|
const lines = [];
|
|
308
310
|
lines.push(`Asset ref: ${input.inputRef}`);
|
|
309
311
|
lines.push("");
|
|
312
|
+
if (input.standardsContext?.trim()) {
|
|
313
|
+
lines.push("Standards to follow (the rulebook for this target):");
|
|
314
|
+
lines.push(input.standardsContext.trim());
|
|
315
|
+
lines.push("");
|
|
316
|
+
}
|
|
317
|
+
{
|
|
318
|
+
const authoringRules = authoringRulesForType(input.proposalKind ?? "lesson");
|
|
319
|
+
if (authoringRules) {
|
|
320
|
+
lines.push(authoringRules);
|
|
321
|
+
lines.push("");
|
|
322
|
+
}
|
|
323
|
+
}
|
|
310
324
|
lines.push("Asset content:");
|
|
311
325
|
if (input.assetContent) {
|
|
312
326
|
const body = input.assetContent.trim().slice(0, 3000);
|
|
@@ -939,12 +953,16 @@ export async function akmDistill(options) {
|
|
|
939
953
|
// Fail open — CLS is supplemental, never required.
|
|
940
954
|
}
|
|
941
955
|
}
|
|
956
|
+
// Distill output is a lesson/knowledge (non-wiki) → stash authoring
|
|
957
|
+
// standards. Resolved once for this single call.
|
|
958
|
+
const standardsContext = resolveStashStandards(stash);
|
|
942
959
|
const baseUserPrompt = buildDistillPrompt({
|
|
943
960
|
inputRef,
|
|
944
961
|
assetContent,
|
|
945
962
|
feedback,
|
|
946
963
|
proposalKind: effectiveProposalKind,
|
|
947
964
|
...(rejectedForRef.length > 0 ? { rejectedProposals: rejectedForRef } : {}),
|
|
965
|
+
...(standardsContext.trim() ? { standardsContext } : {}),
|
|
948
966
|
});
|
|
949
967
|
const userPrompt = clsContext ? `${baseUserPrompt}${clsContext}` : baseUserPrompt;
|
|
950
968
|
const messages = [
|
|
@@ -149,6 +149,11 @@ export function buildExtractPrompt(input) {
|
|
|
149
149
|
const ref = input.data.ref;
|
|
150
150
|
const startedAt = ref.startedAt ? new Date(ref.startedAt).toISOString() : "unknown";
|
|
151
151
|
const endedAt = ref.endedAt ? new Date(ref.endedAt).toISOString() : "unknown";
|
|
152
|
+
// Optional standards block — rendered to the lead-in + body when present,
|
|
153
|
+
// or an empty string (no section) when absent. Gated on non-empty.
|
|
154
|
+
const standards = input.standardsContext?.trim()
|
|
155
|
+
? `\n## Standards to follow (the rulebook for this target)\n\n${input.standardsContext.trim()}\n`
|
|
156
|
+
: "";
|
|
152
157
|
return promptTemplate
|
|
153
158
|
.replace("{{HARNESS}}", ref.harness)
|
|
154
159
|
.replace("{{TITLE}}", ref.title ?? "(no title)")
|
|
@@ -156,6 +161,7 @@ export function buildExtractPrompt(input) {
|
|
|
156
161
|
.replace("{{ENDED_AT}}", endedAt)
|
|
157
162
|
.replace("{{PROJECT_HINT}}", ref.projectHint ?? "(no project hint)")
|
|
158
163
|
.replace("{{ALREADY_PRESERVED}}", formatAlreadyPreserved(input.inlineRefs))
|
|
164
|
+
.replace("{{STANDARDS}}", standards)
|
|
159
165
|
.replace("{{TRANSCRIPT}}", formatTranscript(input.events));
|
|
160
166
|
}
|
|
161
167
|
/**
|
|
@@ -23,12 +23,16 @@
|
|
|
23
23
|
* descriptionQualityValidator passes — same pattern as the
|
|
24
24
|
* consolidate-writer fix.
|
|
25
25
|
*/
|
|
26
|
+
import fs from "node:fs";
|
|
27
|
+
import path from "node:path";
|
|
26
28
|
import { assembleAsset } from "../../core/asset/asset-serialize.js";
|
|
27
29
|
import { resolveStashDir, timestampForFilename } from "../../core/common.js";
|
|
28
30
|
import { getDefaultLlmConfig, loadConfig } from "../../core/config/config.js";
|
|
29
31
|
import { ConfigError, UsageError } from "../../core/errors.js";
|
|
30
32
|
import { appendEvent } from "../../core/events.js";
|
|
31
|
-
import {
|
|
33
|
+
import { probeLock, releaseLock, tryAcquireLockSync } from "../../core/file-lock.js";
|
|
34
|
+
import { resolveStashStandards } from "../../core/standards/resolve-stash-standards.js";
|
|
35
|
+
import { getExtractedSessionsMap, getLastExtractRunAt, getStateDbPath, openStateDatabase, shouldSkipAlreadyExtractedSession, upsertExtractedSession, } from "../../core/state-db.js";
|
|
32
36
|
import { repairTruncatedDescription } from "../../core/text-truncation.js";
|
|
33
37
|
import { warn } from "../../core/warn.js";
|
|
34
38
|
import { resolveImproveProcessRunnerFromProfile, runnerIsLlm } from "../../integrations/agent/runner.js";
|
|
@@ -63,6 +67,88 @@ const DEFAULT_MIN_CONTENT_CHARS = 10;
|
|
|
63
67
|
* processed by subsequent runs, so coverage is preserved — just spread out.
|
|
64
68
|
*/
|
|
65
69
|
const DEFAULT_MAX_SESSIONS_PER_RUN = 25;
|
|
70
|
+
/**
|
|
71
|
+
* Floor for the default discovery window (48h). When no explicit `--since` /
|
|
72
|
+
* `defaultSince` is configured, discovery looks back to the LAST recorded
|
|
73
|
+
* extract run for the harness (so an intermittently-online host that was off for
|
|
74
|
+
* days still rediscovers sessions that ended during the gap), but never LESS
|
|
75
|
+
* than this — looking back less than the prior window could drop a session that
|
|
76
|
+
* a previous run deferred via `maxSessionsPerRun`. Widening is free of redundant
|
|
77
|
+
* LLM cost: the content-hash ledger skips unchanged sessions with zero LLM calls.
|
|
78
|
+
*/
|
|
79
|
+
const DEFAULT_SINCE_FLOOR_MS = 48 * 60 * 60 * 1000;
|
|
80
|
+
/**
|
|
81
|
+
* Staleness window for the per-session extract lock. A single session's
|
|
82
|
+
* processing is bounded by the per-session LLM timeout (default 60s) plus the
|
|
83
|
+
* session-summary call, so a lock older than this must belong to a crashed
|
|
84
|
+
* holder and is safe to reclaim.
|
|
85
|
+
*/
|
|
86
|
+
const EXTRACT_SESSION_LOCK_STALE_MS = 5 * 60 * 1000;
|
|
87
|
+
/**
|
|
88
|
+
* Resolve the discovery `sinceMs` cutoff when no explicit `since`/`defaultSince`
|
|
89
|
+
* is set: the later of (last recorded extract run for this harness) and
|
|
90
|
+
* (now − 48h). See {@link DEFAULT_SINCE_FLOOR_MS}. Best-effort — any state.db
|
|
91
|
+
* error falls back to the 48h floor.
|
|
92
|
+
*/
|
|
93
|
+
function resolveDefaultSinceMs(harnessName, now, opts) {
|
|
94
|
+
const floor = now - DEFAULT_SINCE_FLOOR_MS;
|
|
95
|
+
if (opts.skipTracking)
|
|
96
|
+
return floor;
|
|
97
|
+
let db = opts.stateDb;
|
|
98
|
+
let opened = false;
|
|
99
|
+
try {
|
|
100
|
+
if (!db) {
|
|
101
|
+
db = openStateDatabase(opts.stateDbPath);
|
|
102
|
+
opened = true;
|
|
103
|
+
}
|
|
104
|
+
const lastRun = getLastExtractRunAt(db, harnessName);
|
|
105
|
+
return lastRun != null ? Math.min(lastRun, floor) : floor;
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return floor;
|
|
109
|
+
}
|
|
110
|
+
finally {
|
|
111
|
+
if (opened && db) {
|
|
112
|
+
try {
|
|
113
|
+
db.close();
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
// best-effort close
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/** Filesystem-safe per-session lock path, co-located with the state.db. */
|
|
122
|
+
function getExtractSessionLockPath(harness, sessionId, stateDbPath) {
|
|
123
|
+
const safe = `${harness}-${sessionId}`.replace(/[^A-Za-z0-9._-]/g, "_");
|
|
124
|
+
return path.join(path.dirname(stateDbPath), "extract-locks", `extract-${safe}.lock`);
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Try to claim the per-session extract lock so a concurrent extract (e.g. a
|
|
128
|
+
* session-end hook firing `--session-id` while the hourly improve pass runs
|
|
129
|
+
* discovery) cannot double-process the SAME session — duplicate LLM spend and
|
|
130
|
+
* near-duplicate proposals. Reclaims a stale lock (dead holder PID or age past
|
|
131
|
+
* {@link EXTRACT_SESSION_LOCK_STALE_MS}). Returns false when another LIVE run
|
|
132
|
+
* holds it — the caller then skips the session without any LLM call. Best-effort:
|
|
133
|
+
* any filesystem error resolves to `true` (proceed) so locking never blocks
|
|
134
|
+
* extraction outright.
|
|
135
|
+
*/
|
|
136
|
+
function acquireExtractSessionLock(lockPath) {
|
|
137
|
+
try {
|
|
138
|
+
fs.mkdirSync(path.dirname(lockPath), { recursive: true });
|
|
139
|
+
if (tryAcquireLockSync(lockPath, String(process.pid)))
|
|
140
|
+
return true;
|
|
141
|
+
const probe = probeLock(lockPath, { staleAfterMs: EXTRACT_SESSION_LOCK_STALE_MS });
|
|
142
|
+
if (probe.state === "held")
|
|
143
|
+
return false;
|
|
144
|
+
// absent (released between attempt + probe) or stale → reclaim and retry once.
|
|
145
|
+
releaseLock(lockPath);
|
|
146
|
+
return tryAcquireLockSync(lockPath, String(process.pid));
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
66
152
|
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
67
153
|
/**
|
|
68
154
|
* Parse a since-string into an absolute ms-epoch cutoff. Accepts:
|
|
@@ -180,7 +266,11 @@ triage, sessionIndexing, schemaSimilarityCtx,
|
|
|
180
266
|
// prior row + bypass flags are threaded in from the caller. Skipping here still
|
|
181
267
|
// costs ZERO LLM calls (the expensive resource #602 protects); only the cheap
|
|
182
268
|
// file read is incurred.
|
|
183
|
-
prior, force
|
|
269
|
+
prior, force,
|
|
270
|
+
// Stash authoring standards (convention/meta fact bodies) for non-wiki
|
|
271
|
+
// output. Resolved ONCE per run by the caller and threaded in so facts are
|
|
272
|
+
// not re-read per session. Empty string when none exist.
|
|
273
|
+
standardsContext) {
|
|
184
274
|
const warnings = [];
|
|
185
275
|
let data;
|
|
186
276
|
try {
|
|
@@ -275,7 +365,12 @@ prior, force) {
|
|
|
275
365
|
};
|
|
276
366
|
}
|
|
277
367
|
}
|
|
278
|
-
const prompt = buildExtractPrompt({
|
|
368
|
+
const prompt = buildExtractPrompt({
|
|
369
|
+
data,
|
|
370
|
+
events: filtered.events,
|
|
371
|
+
inlineRefs: data.inlineRefs,
|
|
372
|
+
...(standardsContext.trim() ? { standardsContext } : {}),
|
|
373
|
+
});
|
|
279
374
|
// #561 — ADDITIVE session indexing. Generate + write the session asset
|
|
280
375
|
// (`sessions/<harness>/<id>.md`). FAIL-OPEN: any failure only records a
|
|
281
376
|
// warning; it NEVER changes the proposal/skip outcome of extract. Returns the
|
|
@@ -625,7 +720,16 @@ export async function akmExtract(options) {
|
|
|
625
720
|
candidates = [target];
|
|
626
721
|
}
|
|
627
722
|
else {
|
|
628
|
-
|
|
723
|
+
// No explicit `--since`/`defaultSince` → default to "since the last run"
|
|
724
|
+
// (floored at 48h) so an intermittently-online host doesn't lose sessions
|
|
725
|
+
// that ended while it was off. See {@link resolveDefaultSinceMs}.
|
|
726
|
+
const sinceMs = effectiveSince
|
|
727
|
+
? parseSinceArg(effectiveSince)
|
|
728
|
+
: resolveDefaultSinceMs(harness.name, startMs, {
|
|
729
|
+
...(options.stateDb ? { stateDb: options.stateDb } : {}),
|
|
730
|
+
...(options.stateDbPath ? { stateDbPath: options.stateDbPath } : {}),
|
|
731
|
+
...(options.skipTracking ? { skipTracking: options.skipTracking } : {}),
|
|
732
|
+
});
|
|
629
733
|
candidates = harness.listSessions({
|
|
630
734
|
sinceMs,
|
|
631
735
|
...(options.location ? { location: options.location } : {}),
|
|
@@ -677,6 +781,10 @@ export async function akmExtract(options) {
|
|
|
677
781
|
embedFn: options.schemaSimilarityEmbedFn,
|
|
678
782
|
};
|
|
679
783
|
}
|
|
784
|
+
// Stash authoring standards (convention/meta fact bodies) for non-wiki
|
|
785
|
+
// extract output. Resolved ONCE per run and threaded into each session's
|
|
786
|
+
// prompt so facts are not re-read per session.
|
|
787
|
+
const extractStandardsContext = resolveStashStandards(stashDir);
|
|
680
788
|
for (const summary of candidates) {
|
|
681
789
|
// #602 — the already-extracted skip moved INTO processSession (the content
|
|
682
790
|
// hash needs the session body, only available after readSession). The prior
|
|
@@ -690,8 +798,33 @@ export async function akmExtract(options) {
|
|
|
690
798
|
topLevelWarnings.push(`Reached maxSessionsPerRun=${maxSessionsPerRun}; ${candidates.length - processedCount - skippedCount} session(s) deferred to a later run.`);
|
|
691
799
|
break;
|
|
692
800
|
}
|
|
801
|
+
// Q5 — per-session lock so two concurrent extracts (e.g. a session-end hook
|
|
802
|
+
// firing `--session-id` while the hourly improve discovery pass runs) can't
|
|
803
|
+
// both LLM-process the SAME session. The holder records the outcome; a
|
|
804
|
+
// second run skips without any LLM call. Engaged only for real cross-process
|
|
805
|
+
// runs (those that open their own state.db): dry-run is read-only, an
|
|
806
|
+
// injected `stateDb` handle is an in-process/test scenario with no cross-
|
|
807
|
+
// process race, and skip-tracking-off opts out entirely.
|
|
808
|
+
let sessionLockPath;
|
|
809
|
+
if (trackingEnabled && !dryRun && !options.stateDb) {
|
|
810
|
+
sessionLockPath = getExtractSessionLockPath(harness.name, summary.sessionId, options.stateDbPath ?? getStateDbPath());
|
|
811
|
+
if (!acquireExtractSessionLock(sessionLockPath)) {
|
|
812
|
+
sessions.push({
|
|
813
|
+
sessionId: summary.sessionId,
|
|
814
|
+
harness: harness.name,
|
|
815
|
+
candidateCount: 0,
|
|
816
|
+
proposalIds: [],
|
|
817
|
+
preFilter: { inputCount: 0, outputCount: 0, truncatedCount: 0 },
|
|
818
|
+
warnings: ["concurrent extract holds this session's lock — skipped (handled by the other run)"],
|
|
819
|
+
skipped: true,
|
|
820
|
+
skipReason: "locked_concurrent",
|
|
821
|
+
});
|
|
822
|
+
skippedCount += 1;
|
|
823
|
+
continue;
|
|
824
|
+
}
|
|
825
|
+
}
|
|
693
826
|
try {
|
|
694
|
-
const result = await processSession(harness, summary, stashDir, config, llmConfig, chat, options.ctx, sourceRun, dryRun, timeoutMs, maxTotalChars, minContentChars, triage, sessionIndexing, schemaSimilarityCtx, prior, options.force === true);
|
|
827
|
+
const result = await processSession(harness, summary, stashDir, config, llmConfig, chat, options.ctx, sourceRun, dryRun, timeoutMs, maxTotalChars, minContentChars, triage, sessionIndexing, schemaSimilarityCtx, prior, options.force === true, extractStandardsContext);
|
|
695
828
|
sessions.push(result);
|
|
696
829
|
// #626 — triage aggregation. A session reached the triage gate only when it
|
|
697
830
|
// was NOT already preempted by an earlier skip (read_failed / too_short /
|
|
@@ -779,6 +912,10 @@ export async function akmExtract(options) {
|
|
|
779
912
|
});
|
|
780
913
|
skippedCount += 1;
|
|
781
914
|
}
|
|
915
|
+
finally {
|
|
916
|
+
if (sessionLockPath)
|
|
917
|
+
releaseLock(sessionLockPath);
|
|
918
|
+
}
|
|
782
919
|
}
|
|
783
920
|
// Close the state.db connection we opened. Callers that injected stateDb
|
|
784
921
|
// via the test seam own its lifecycle.
|
|
@@ -838,13 +975,21 @@ export async function akmExtract(options) {
|
|
|
838
975
|
export function countNewExtractCandidates(config, options = {}) {
|
|
839
976
|
const extractProcess = config.profiles?.improve?.default?.processes?.extract;
|
|
840
977
|
const effectiveSince = options.since ?? extractProcess?.defaultSince;
|
|
841
|
-
|
|
978
|
+
// Mirror akmExtract: when no explicit window is set, default per-harness to
|
|
979
|
+
// "since the last run" (floored at 48h) instead of a fixed 24h. Keeps this
|
|
980
|
+
// gate's discovery window identical to what akmExtract will actually scan.
|
|
981
|
+
const explicitSinceMs = effectiveSince ? parseSinceArg(effectiveSince) : undefined;
|
|
842
982
|
const harnesses = (options.harnesses ?? getAvailableHarnesses()).filter((h) => h.isAvailable());
|
|
843
983
|
let stateDb = options.stateDb;
|
|
844
984
|
let openedStateDb = false;
|
|
845
985
|
let total = 0;
|
|
846
986
|
try {
|
|
847
987
|
for (const harness of harnesses) {
|
|
988
|
+
const sinceMs = explicitSinceMs ??
|
|
989
|
+
resolveDefaultSinceMs(harness.name, Date.now(), {
|
|
990
|
+
...(options.stateDb ? { stateDb: options.stateDb } : {}),
|
|
991
|
+
...(options.stateDbPath ? { stateDbPath: options.stateDbPath } : {}),
|
|
992
|
+
});
|
|
848
993
|
const candidates = harness.listSessions({ sinceMs });
|
|
849
994
|
if (candidates.length === 0)
|
|
850
995
|
continue;
|
|
@@ -1887,7 +1887,10 @@ async function runImprovePreparationStage(args) {
|
|
|
1887
1887
|
// memory mtimes, so a skipped extract never flags work for the NEXT run's
|
|
1888
1888
|
// consolidation mtime-gate (the downstream trigger #554 asks us to suppress).
|
|
1889
1889
|
const EXTRACT_DEFAULT_MIN_NEW_SESSIONS = 0;
|
|
1890
|
-
|
|
1890
|
+
// Read from the ACTIVE resolved profile (not always `default`), matching how
|
|
1891
|
+
// `extract.enabled` resolves — otherwise a non-default profile (e.g.
|
|
1892
|
+
// `frequent`) setting `minNewSessions` was silently ignored.
|
|
1893
|
+
const configuredMinNewSessions = improveProfile.processes?.extract?.minNewSessions;
|
|
1891
1894
|
const minNewSessions = typeof configuredMinNewSessions === "number" ? configuredMinNewSessions : EXTRACT_DEFAULT_MIN_NEW_SESSIONS;
|
|
1892
1895
|
// #593/#594: the ACTIVE resolved improve profile is the single source of
|
|
1893
1896
|
// truth for whether extract runs. (Previously this also ANDed in the legacy
|
|
@@ -1904,6 +1907,11 @@ async function runImprovePreparationStage(args) {
|
|
|
1904
1907
|
const countFn = options.extractCandidateCountFn ?? countNewExtractCandidates;
|
|
1905
1908
|
const newCandidateCount = countFn(extractConfig, {
|
|
1906
1909
|
...(options.extractHarnesses ? { harnesses: options.extractHarnesses } : {}),
|
|
1910
|
+
// Use the ACTIVE profile's discovery window so the gate counts over the
|
|
1911
|
+
// same window akmExtract will scan (not always `default`).
|
|
1912
|
+
...(improveProfile.processes?.extract?.defaultSince
|
|
1913
|
+
? { since: improveProfile.processes.extract.defaultSince }
|
|
1914
|
+
: {}),
|
|
1907
1915
|
// C2: pin the candidate-count state.db open to the boundary-resolved path.
|
|
1908
1916
|
...(eventsCtx?.dbPath ? { stateDbPath: eventsCtx.dbPath } : {}),
|
|
1909
1917
|
});
|
|
@@ -2455,6 +2463,14 @@ async function runImprovePreparationStage(args) {
|
|
|
2455
2463
|
// Cap: at most 10% of the effective run limit so the lane cannot crowd out
|
|
2456
2464
|
// reactive feedback. Requires state.db to have an asset_salience row — refs
|
|
2457
2465
|
// without a row (pre-#608 assets still on the type-weight stub) are skipped.
|
|
2466
|
+
//
|
|
2467
|
+
// Cooldown: a ref qualifies at most once — when no prior reflect proposal
|
|
2468
|
+
// exists for it (`!lastReflectProposalTs.has`). Without this guard the lane
|
|
2469
|
+
// re-selects the same high-salience refs on EVERY run (auto-accept emits a
|
|
2470
|
+
// `promoted` event, not `feedback`, so the ref never leaves
|
|
2471
|
+
// noFeedbackCandidates), burning LLM calls and churning the asset. This
|
|
2472
|
+
// mirrors the P0-A high-retrieval gate's `!lastReflectProposalTs.has(r.ref)`
|
|
2473
|
+
// guard above so all "rescue" lanes share the same once-per-asset semantics.
|
|
2458
2474
|
const highSalienceRefs = [];
|
|
2459
2475
|
const salienceCfg = (options.config ?? loadConfig()).improve?.salience;
|
|
2460
2476
|
const salienceThreshold = salienceCfg?.salienceThreshold ?? 0.75;
|
|
@@ -2470,7 +2486,7 @@ async function runImprovePreparationStage(args) {
|
|
|
2470
2486
|
if (highSalienceRefs.length >= highSalienceCap)
|
|
2471
2487
|
break;
|
|
2472
2488
|
const row = getAssetSalience(dbForHighSalience, r.ref);
|
|
2473
|
-
if (row && row.encoding_salience >= salienceThreshold) {
|
|
2489
|
+
if (row && row.encoding_salience >= salienceThreshold && !lastReflectProposalTs.has(r.ref)) {
|
|
2474
2490
|
highSalienceRefs.push(r);
|
|
2475
2491
|
}
|
|
2476
2492
|
}
|
|
@@ -27,6 +27,7 @@ import { resolveStashDir } from "../../core/common.js";
|
|
|
27
27
|
import { getDefaultLlmConfig, loadConfig } from "../../core/config/config.js";
|
|
28
28
|
import { appendEvent } from "../../core/events.js";
|
|
29
29
|
import { parseEmbeddedJsonResponse } from "../../core/parse.js";
|
|
30
|
+
import { resolveStashStandards } from "../../core/standards/resolve-stash-standards.js";
|
|
30
31
|
import { warn } from "../../core/warn.js";
|
|
31
32
|
import { closeDatabase, getAllEntries, openExistingDatabase } from "../../indexer/db/db.js";
|
|
32
33
|
import { resolveImproveProcessRunnerFromProfile, runnerIsLlm } from "../../integrations/agent/runner.js";
|
|
@@ -144,12 +145,17 @@ export function deriveProceduralWorkflowRef(cluster) {
|
|
|
144
145
|
}
|
|
145
146
|
// ── Prompt + parse ──────────────────────────────────────────────────────────────
|
|
146
147
|
/** Assemble the per-sequence user prompt fed to the procedural LLM. */
|
|
147
|
-
export function buildProceduralPrompt(cluster) {
|
|
148
|
+
export function buildProceduralPrompt(cluster, standardsContext = "") {
|
|
148
149
|
const lines = [
|
|
149
150
|
`A recurring successful action sequence observed across ${cluster.members.length} sessions.`,
|
|
150
151
|
"",
|
|
151
|
-
"Ordered actions (turn EACH into exactly one step, in this order):",
|
|
152
152
|
];
|
|
153
|
+
if (standardsContext.trim()) {
|
|
154
|
+
lines.push("Standards to follow (the rulebook for this target):");
|
|
155
|
+
lines.push(standardsContext.trim());
|
|
156
|
+
lines.push("");
|
|
157
|
+
}
|
|
158
|
+
lines.push("Ordered actions (turn EACH into exactly one step, in this order):");
|
|
153
159
|
cluster.normalized.forEach((step, i) => {
|
|
154
160
|
lines.push(`${i + 1}. ${step}`);
|
|
155
161
|
});
|
|
@@ -312,6 +318,9 @@ export async function akmProcedural(opts) {
|
|
|
312
318
|
warnings.push("procedural: no LLM configured — skipping");
|
|
313
319
|
return finish({ sequencesScanned, clustersFormed: 0 });
|
|
314
320
|
}
|
|
321
|
+
// Procedural output is a workflow (non-wiki) → stash authoring standards.
|
|
322
|
+
// Resolved ONCE per run and passed to each sequence prompt.
|
|
323
|
+
const standardsContext = resolveStashStandards(stashDir);
|
|
315
324
|
for (const cluster of clusters) {
|
|
316
325
|
if (opts.signal?.aborted) {
|
|
317
326
|
warnings.push("aborted-mid-run");
|
|
@@ -319,7 +328,7 @@ export async function akmProcedural(opts) {
|
|
|
319
328
|
}
|
|
320
329
|
clustersFormed += 1;
|
|
321
330
|
const workflowRef = deriveProceduralWorkflowRef(cluster);
|
|
322
|
-
const prompt = buildProceduralPrompt(cluster);
|
|
331
|
+
const prompt = buildProceduralPrompt(cluster, standardsContext);
|
|
323
332
|
const raw = await llmFn(prompt);
|
|
324
333
|
const doc = parseProceduralWorkflow(raw, cluster.normalized.length);
|
|
325
334
|
if (!doc) {
|
|
@@ -45,6 +45,7 @@ import { resolveStashDir } from "../../core/common.js";
|
|
|
45
45
|
import { getDefaultLlmConfig, loadConfig } from "../../core/config/config.js";
|
|
46
46
|
import { appendEvent } from "../../core/events.js";
|
|
47
47
|
import { parseEmbeddedJsonResponse } from "../../core/parse.js";
|
|
48
|
+
import { resolveStashStandards } from "../../core/standards/resolve-stash-standards.js";
|
|
48
49
|
import { decayUnseenRecombineHypotheses, findMatchingRecombineHypothesis, getRecombineHypothesis, getStateDbPath, markRecombineHypothesisPromoted, openStateDatabase, recordRecombineInduction, } from "../../core/state-db.js";
|
|
49
50
|
import { warn } from "../../core/warn.js";
|
|
50
51
|
import { closeDatabase, getAllEntries, getEntitiesByEntryIds, openExistingDatabase, } from "../../indexer/db/db.js";
|
|
@@ -228,12 +229,17 @@ function readBody(entry) {
|
|
|
228
229
|
}
|
|
229
230
|
}
|
|
230
231
|
/** Assemble the per-cluster user prompt fed to the recombine LLM. */
|
|
231
|
-
export function buildClusterPrompt(cluster) {
|
|
232
|
+
export function buildClusterPrompt(cluster, standardsContext = "") {
|
|
232
233
|
const lines = [
|
|
233
234
|
`Shared signal: ${cluster.signature}`,
|
|
234
235
|
`Cluster of ${cluster.members.length} related memories:`,
|
|
235
236
|
"",
|
|
236
237
|
];
|
|
238
|
+
if (standardsContext.trim()) {
|
|
239
|
+
lines.push("Standards to follow (the rulebook for this target):");
|
|
240
|
+
lines.push(standardsContext.trim());
|
|
241
|
+
lines.push("");
|
|
242
|
+
}
|
|
237
243
|
for (const m of cluster.members) {
|
|
238
244
|
lines.push(`[memory:${m.entry.name}]`);
|
|
239
245
|
if (m.entry.description)
|
|
@@ -399,6 +405,9 @@ export async function akmRecombine(opts) {
|
|
|
399
405
|
// Refs re-induced (defensible generalization passed the quality gate) THIS
|
|
400
406
|
// run — everything else is decayed after the loop.
|
|
401
407
|
const seenThisRun = new Set();
|
|
408
|
+
// Recombine output is knowledge/lesson (non-wiki) → stash authoring
|
|
409
|
+
// standards. Resolved ONCE per run and passed to each cluster prompt.
|
|
410
|
+
const standardsContext = resolveStashStandards(stashDir);
|
|
402
411
|
try {
|
|
403
412
|
for (const cluster of clusters) {
|
|
404
413
|
if (opts.signal?.aborted) {
|
|
@@ -406,7 +415,7 @@ export async function akmRecombine(opts) {
|
|
|
406
415
|
break;
|
|
407
416
|
}
|
|
408
417
|
clustersFormed += 1;
|
|
409
|
-
const prompt = buildClusterPrompt(cluster);
|
|
418
|
+
const prompt = buildClusterPrompt(cluster, standardsContext);
|
|
410
419
|
const raw = await llmFn(prompt);
|
|
411
420
|
const generalization = parseGeneralization(raw);
|
|
412
421
|
if (!generalization) {
|
|
@@ -34,6 +34,7 @@ import { loadConfig } from "../../core/config/config.js";
|
|
|
34
34
|
import { ConfigError, UsageError } from "../../core/errors.js";
|
|
35
35
|
import { appendEvent, readEvents } from "../../core/events.js";
|
|
36
36
|
import { lintLessonContent } from "../../core/lesson-lint.js";
|
|
37
|
+
import { resolveStandardsContext } from "../../core/standards/resolve-standards-context.js";
|
|
37
38
|
import { lookup } from "../../indexer/indexer.js";
|
|
38
39
|
import { runAgent, } from "../../integrations/agent/index.js";
|
|
39
40
|
import { resolveProcessAgentProfile } from "../../integrations/agent/config.js";
|
|
@@ -745,6 +746,9 @@ export async function akmReflect(options = {}) {
|
|
|
745
746
|
// Reflexion-style verbal-RL: inject rejected proposals so the agent avoids
|
|
746
747
|
// reproducing proposals that have already been reviewed and refused.
|
|
747
748
|
const rejectedProposals = readRejectedProposals(stash, options.ref);
|
|
749
|
+
// Standards "rulebook" for this target — wiki schema (wiki page) or stash
|
|
750
|
+
// convention/meta facts (non-wiki asset); empty when neither fires.
|
|
751
|
+
const standardsContext = resolveStandardsContext(options.ref, stash);
|
|
748
752
|
// 5. Spawn the agent — with optional Self-Refine loop (R-1 / #372).
|
|
749
753
|
//
|
|
750
754
|
// maxRefineIters controls how many agent invocations are made:
|
|
@@ -813,6 +817,7 @@ export async function akmReflect(options = {}) {
|
|
|
813
817
|
...(schemaHints.length > 0 ? { schemaHints } : {}),
|
|
814
818
|
...(relatedLessons.length > 0 ? { relatedLessons } : {}),
|
|
815
819
|
...(options.task ? { task: options.task } : {}),
|
|
820
|
+
...(standardsContext.trim() ? { standardsContext } : {}),
|
|
816
821
|
...(options.avoidPatterns && options.avoidPatterns.length > 0 ? { avoidPatterns: options.avoidPatterns } : {}),
|
|
817
822
|
...(rejectedProposals.length > 0 ? { rejectedProposals } : {}),
|
|
818
823
|
// R-1: inject prior draft as self-critique target on iterations > 0
|
|
@@ -370,6 +370,13 @@ export async function drainProposals(opts, promoteFn = akmProposalAccept, reject
|
|
|
370
370
|
// — these are re-stamped `no-judge-configured` when no runner resolves them.
|
|
371
371
|
const needsJudge = new Set();
|
|
372
372
|
for (const proposal of pending) {
|
|
373
|
+
// Do NOT reclassify a proposal that was already conclusively stamped
|
|
374
|
+
// `auto-rejected` by a prior gate run (e.g. the improve confidence gate).
|
|
375
|
+
// Overwriting an authoritative rejection with `auto-accepted` would corrupt
|
|
376
|
+
// the audit trail and silently promote content the gate explicitly rejected.
|
|
377
|
+
// Such proposals remain pending for manual review (or TTL expiry).
|
|
378
|
+
if (proposal.gateDecision?.outcome === "auto-rejected")
|
|
379
|
+
continue;
|
|
373
380
|
const decision = classifyProposal(proposal, opts.policy, opts.maxDiffLines);
|
|
374
381
|
if (decision === null)
|
|
375
382
|
continue;
|
|
@@ -18,6 +18,7 @@ import { TYPE_DIRS } from "../../core/asset/asset-spec.js";
|
|
|
18
18
|
import { resolveStashDir } from "../../core/common.js";
|
|
19
19
|
import { ConfigError, UsageError } from "../../core/errors.js";
|
|
20
20
|
import { appendEvent } from "../../core/events.js";
|
|
21
|
+
import { resolveStandardsContext } from "../../core/standards/resolve-standards-context.js";
|
|
21
22
|
import { runAgent, } from "../../integrations/agent/index.js";
|
|
22
23
|
import { resolveProcessAgentProfile } from "../../integrations/agent/config.js";
|
|
23
24
|
import { buildProposePrompt, parseAgentProposalPayload } from "../../integrations/agent/prompts.js";
|
|
@@ -93,10 +94,14 @@ export async function akmPropose(options) {
|
|
|
93
94
|
// directly using its file tools rather than returning JSON via stdout.
|
|
94
95
|
const draftFilePath = import("node:os").then((os) => import("node:path").then((path) => path.join(os.tmpdir(), `akm-propose-${options.type}-${options.name.replace(/[^a-z0-9_-]/gi, "_")}-${Date.now()}.md`)));
|
|
95
96
|
const resolvedDraftPath = await draftFilePath;
|
|
97
|
+
// Standards "rulebook" for this target — wiki schema (wiki page) or stash
|
|
98
|
+
// convention/meta facts (non-wiki asset); empty when neither fires.
|
|
99
|
+
const standardsContext = resolveStandardsContext(`${options.type}:${options.name}`, stash);
|
|
96
100
|
const prompt = buildProposePrompt({
|
|
97
101
|
type: options.type,
|
|
98
102
|
name: options.name,
|
|
99
103
|
task: options.task,
|
|
104
|
+
...(standardsContext.trim() ? { standardsContext } : {}),
|
|
100
105
|
draftFilePath: resolvedDraftPath,
|
|
101
106
|
});
|
|
102
107
|
// 4. Spawn the agent.
|