akm-cli 0.9.4 → 0.9.6
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 +265 -0
- package/dist/commands/env/env-cli.js +1 -2
- package/dist/commands/env/secret-cli.js +1 -5
- package/dist/commands/feedback-cli.js +0 -4
- package/dist/commands/health/checks.js +0 -32
- package/dist/commands/health/surfaces.js +2 -2
- package/dist/commands/health.js +6 -15
- package/dist/commands/improve/anti-collapse.js +4 -91
- package/dist/commands/improve/autonomy-gate.js +1 -1
- package/dist/commands/improve/consolidate.js +25 -26
- package/dist/commands/improve/distill.js +2 -2
- package/dist/commands/improve/extract.js +8 -1
- package/dist/commands/improve/improve.js +3 -5
- package/dist/commands/improve/locks.js +13 -5
- package/dist/commands/improve/loop-stages.js +8 -9
- package/dist/commands/improve/memory/memory-contradiction-detect.js +1 -19
- package/dist/commands/improve/outcome-loop.js +0 -6
- package/dist/commands/improve/reflect.js +20 -9
- package/dist/commands/lint/index.js +4 -6
- package/dist/commands/proposal/validators/proposal-quality-validators.js +0 -7
- package/dist/commands/proposal/validators/proposal-validators.js +12 -0
- package/dist/commands/read/search.js +14 -24
- package/dist/commands/sources/bundle-config-ops.js +9 -4
- package/dist/commands/sources/info.js +14 -10
- package/dist/commands/tasks/tasks-cli.js +56 -3
- package/dist/commands/tasks/tasks.js +101 -18
- package/dist/commands/workflow-cli.js +2 -2
- package/dist/core/adapter/adapters/akm-adapter.js +2 -0
- package/dist/core/common.js +61 -24
- package/dist/core/config/config-io.js +2 -2
- package/dist/core/config/config-sources.js +32 -2
- package/dist/core/config/config-version-shim.js +101 -0
- package/dist/core/config/config.js +7 -7
- package/dist/core/config/schema/engines.js +9 -7
- package/dist/core/config/schema/primitives.js +0 -5
- package/dist/core/config/schema/search.js +2 -1
- package/dist/core/file-lock.js +2 -1
- package/dist/core/improve-result.js +35 -14
- package/dist/core/maintenance-barrier.js +2 -14
- package/dist/core/paths.js +0 -3
- package/dist/core/redaction.js +2 -2
- package/dist/core/spawn-env.js +8 -12
- package/dist/core/state/migrations.js +1 -12
- package/dist/core/state-db.js +9 -27
- package/dist/core/write-source.js +12 -19
- package/dist/execution/directory-identity.js +36 -10
- package/dist/execution/guarded-source.js +0 -10
- package/dist/indexer/graph/graph-boost.js +0 -4
- package/dist/indexer/index-writer-lock.js +43 -24
- package/dist/indexer/index-written-assets.js +5 -6
- package/dist/indexer/indexer.js +2 -39
- package/dist/indexer/materialize-embeddings.js +85 -41
- package/dist/indexer/passes/metadata.js +12 -4
- package/dist/indexer/scan/doc-to-entry.js +2 -0
- package/dist/indexer/search/db-search.js +15 -48
- package/dist/indexer/search/ranking-contributors.js +0 -25
- package/dist/indexer/search/ranking.js +3 -13
- package/dist/indexer/search/search-fields.js +16 -1
- package/dist/integrations/agent/builder-shared.js +0 -25
- package/dist/integrations/agent/model-map.js +2 -60
- package/dist/integrations/harnesses/aider/agent-builder.js +1 -3
- package/dist/integrations/harnesses/amazonq/agent-builder.js +2 -7
- package/dist/integrations/harnesses/claude/agent-builder.js +1 -4
- package/dist/integrations/harnesses/codex/agent-builder.js +1 -4
- package/dist/integrations/harnesses/copilot/agent-builder.js +2 -6
- package/dist/integrations/harnesses/gemini/agent-builder.js +2 -7
- package/dist/integrations/harnesses/opencode/agent-builder.js +1 -4
- package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +2 -16
- package/dist/integrations/harnesses/openhands/agent-builder.js +1 -3
- package/dist/integrations/harnesses/pi/agent-builder.js +2 -4
- package/dist/integrations/session-logs/index.js +0 -9
- package/dist/llm/client.js +75 -42
- package/dist/llm/embedder.js +7 -3
- package/dist/llm/embedders/remote.js +141 -42
- package/dist/output/shapes/passthrough.js +17 -5
- package/dist/registry/network.js +5 -37
- package/dist/runtime.js +2 -10
- package/dist/scripts/akm-migrate-node.js +180 -238
- package/dist/scripts/akm-migrate.js +180 -238
- package/dist/setup/engine-config.js +2 -5
- package/dist/setup/registry-stash-loader.js +0 -8
- package/dist/setup/setup.js +9 -46
- package/dist/setup/steps/connection-shared.js +10 -13
- package/dist/sources/providers/git-install.js +1 -1
- package/dist/storage/engines/sqlite-migrations.js +20 -1
- package/dist/storage/repositories/index-entries-repository.js +0 -15
- package/dist/storage/repositories/proposals-repository.js +32 -6
- package/dist/tasks/backends/launchd.js +15 -20
- package/dist/tasks/backends/schtasks.js +18 -8
- package/dist/tasks/run/run-native-task.js +8 -6
- package/dist/tasks/scheduler-binding.js +15 -5
- package/dist/tasks/scheduler-sync-preview.js +4 -2
- package/dist/tasks/scheduler-sync.js +77 -42
- package/dist/tasks/source/bounded-document.js +2 -4
- package/dist/tasks/source/task-source-v3-frozen.js +5 -7
- package/dist/tasks/source/task-source-v4.js +5 -10
- package/dist/tasks/source/task-to-v3.js +29 -22
- package/dist/tasks/source/task-to-v4.js +1 -14
- package/dist/tasks/source-v3.js +6 -6
- package/dist/workflows/exec/native-executor.js +21 -31
- package/dist/workflows/exec/run-workflow.js +5 -6
- package/dist/workflows/exec/scheduler.js +3 -19
- package/dist/workflows/exec/step-work.js +1 -4
- package/dist/workflows/exec/unit-dispatch.js +2 -2
- package/dist/workflows/exec/worktree.js +1 -13
- package/dist/workflows/freeze/targets/child-workflow.js +2 -10
- package/dist/workflows/ir/plan-hash.js +4 -6
- package/dist/workflows/ir/schema-v4.js +0 -12
- package/dist/workflows/ir/schema.js +20 -31
- package/dist/workflows/parser.js +11 -52
- package/dist/workflows/renderer.js +2 -3
- package/dist/workflows/resource-limits.js +11 -41
- package/dist/workflows/runtime/runs.js +3 -4
- package/dist/workflows/source-ir/schema.js +14 -30
- package/dist/workflows/validator.js +1 -7
- package/docs/reference/cli.md +25 -3
- package/docs/reference/configuration.md +28 -7
- package/docs/reference/tasks.md +10 -0
- package/package.json +1 -1
- package/schemas/akm-config.json +4 -23
- package/schemas/akm-task.json +1 -2
- package/schemas/akm-workflow.json +1 -13
- package/dist/indexer/search/semantic-status.js +0 -142
|
@@ -73,7 +73,7 @@ import { stripFrontmatterBody as stripBodyForFidelity } from "./content-hash.js"
|
|
|
73
73
|
import { autoRepairLessonFrontmatter, autoSwapDescriptionWhenToUse, collectLessonQualityFindings, repairLessonDescriptionTruncation, } from "./distill/content-repair.js";
|
|
74
74
|
import { memoryKnowledgePromotionRequiresDispatch, planMemoryKnowledgePromotion, promoteMemoryToKnowledge, } from "./distill/promote-memory.js";
|
|
75
75
|
import { fetchTopSimilarLessons, persistOutputEncodingSalience, runLessonQualityJudge, writeQualityRejection, } from "./distill/quality-gate.js";
|
|
76
|
-
import { buildClsContext, checkDistillFidelity } from "./distill-guards.js";
|
|
76
|
+
import { buildClsContext, checkDistillFidelity, DEFAULT_CLS_ADJACENT_COUNT } from "./distill-guards.js";
|
|
77
77
|
import { deriveKnowledgeRef } from "./distill-promotion-policy.js";
|
|
78
78
|
import { buildRefVocabulary, scoreEncodingSalience } from "./encoding-salience.js";
|
|
79
79
|
import { resolveImproveLlmExecution } from "./execution.js";
|
|
@@ -1239,7 +1239,7 @@ async function buildDistillMessages(args) {
|
|
|
1239
1239
|
let clsContext = "";
|
|
1240
1240
|
if (clsConfig.enabled) {
|
|
1241
1241
|
try {
|
|
1242
|
-
const adjacentCount = clsConfig.adjacentCount ??
|
|
1242
|
+
const adjacentCount = clsConfig.adjacentCount ?? DEFAULT_CLS_ADJACENT_COUNT;
|
|
1243
1243
|
// Use the asset content or input ref as the query for adjacent retrieval.
|
|
1244
1244
|
const clsQuery = assetContent ? assetContent.slice(0, 500) : inputRef;
|
|
1245
1245
|
const adjacentItems = await fetchSimilarLessonsFn(clsQuery, adjacentCount);
|
|
@@ -44,6 +44,7 @@ import { indexWrittenAssets } from "../../indexer/index-written-assets.js";
|
|
|
44
44
|
import { disposeLoweredExecutionDispatchLease, } from "../../integrations/agent/execution-lowering.js";
|
|
45
45
|
import { getAvailableHarnesses } from "../../integrations/session-logs/index.js";
|
|
46
46
|
import { preFilterSession } from "../../integrations/session-logs/pre-filter.js";
|
|
47
|
+
import { isJsonSchemaKnownUnsupported } from "../../llm/client.js";
|
|
47
48
|
import { callStructured, preflightStructuredLlmRunner } from "../../llm/structured-call.js";
|
|
48
49
|
import { sha256Hex } from "../../runtime.js";
|
|
49
50
|
import { getExtractedSessionsMap, getLastExtractRunAt, shouldSkipAlreadyExtractedSession, upsertExtractedSession, } from "../../storage/repositories/extract-sessions-repository.js";
|
|
@@ -563,7 +564,13 @@ async function runSessionExtractionLlmCall(args) {
|
|
|
563
564
|
return payload.parseFailure ? undefined : payload;
|
|
564
565
|
},
|
|
565
566
|
validate: (payload) => ({ ok: true, value: payload }),
|
|
566
|
-
|
|
567
|
+
// One attempt when structured output is expected to work (not explicitly
|
|
568
|
+
// disabled, and this connection hasn't already proven otherwise this
|
|
569
|
+
// process — see `isJsonSchemaKnownUnsupported`); two when it's known
|
|
570
|
+
// unsupported and extraction is relying on looser prompt-contract JSON.
|
|
571
|
+
maxAttempts: llmRunner.connection.supportsJsonSchema !== false && !isJsonSchemaKnownUnsupported(llmRunner.connection)
|
|
572
|
+
? 1
|
|
573
|
+
: 2,
|
|
567
574
|
buildFeedback: () => "Your previous response did not contain a valid extraction payload. Respond with ONLY a JSON object matching the requested schema, with a candidates array and no prose or code fences.",
|
|
568
575
|
});
|
|
569
576
|
if (result.ok)
|
|
@@ -35,7 +35,7 @@ import { akmDistill } from "./distill.js";
|
|
|
35
35
|
import { buildLatestProposalTsMap, collectEligibleRefs, collectEligibleRefsReadOnly, memoryCleanupParentRef, resolveImproveScope, shouldAnalyzeMemoryCleanup, } from "./eligibility.js";
|
|
36
36
|
import { countEvalCases } from "./eval-cases.js";
|
|
37
37
|
import { resolveImprovePlan, resolveImproveStrategy } from "./improve-strategies.js";
|
|
38
|
-
import { improveLockPath,
|
|
38
|
+
import { improveLockPath, releaseImproveLock, tryAcquireImproveLock } from "./locks.js";
|
|
39
39
|
// The cycle loop / post-loop / maintenance stages live in ./loop-stages.
|
|
40
40
|
import { runImproveLoopStage, runImprovePostLoopStage } from "./loop-stages.js";
|
|
41
41
|
import { analyzeMemoryCleanup } from "./memory/memory-improve.js";
|
|
@@ -115,7 +115,7 @@ function createRunWriteJournal() {
|
|
|
115
115
|
export async function akmImprove(options = {}) {
|
|
116
116
|
const setup = resolveImproveRunSetup(options);
|
|
117
117
|
options = setup.options;
|
|
118
|
-
const { budgetMs, budgetAbortController, scope, selectedStrategy, syncRepoDir, resolvedStateDbPath, resolvedLockPath,
|
|
118
|
+
const { budgetMs, budgetAbortController, scope, selectedStrategy, syncRepoDir, resolvedStateDbPath, resolvedLockPath, } = setup;
|
|
119
119
|
let clearBudgetTimer = () => { };
|
|
120
120
|
let initialGitPaths = new Set();
|
|
121
121
|
const runJournal = createRunWriteJournal();
|
|
@@ -164,7 +164,7 @@ export async function akmImprove(options = {}) {
|
|
|
164
164
|
}
|
|
165
165
|
try {
|
|
166
166
|
if (!options.dryRun) {
|
|
167
|
-
const acquisition = tryAcquireImproveLock(resolvedLockPath,
|
|
167
|
+
const acquisition = tryAcquireImproveLock(resolvedLockPath, options.skipIfLocked, {
|
|
168
168
|
// R25: C2 boundary-pinned path — the long-lived handle doesn't exist yet.
|
|
169
169
|
dbPath: resolvedStateDbPath,
|
|
170
170
|
});
|
|
@@ -484,7 +484,6 @@ function resolveImproveRunSetup(options) {
|
|
|
484
484
|
// triage, indexing, proposal work, maintenance, and final stash sync.
|
|
485
485
|
const lockBaseDir = primaryStashDir ? path.join(primaryStashDir, ".akm") : path.join(options.stashDir ?? ".", ".akm");
|
|
486
486
|
const resolvedLockPath = improveLockPath(lockBaseDir);
|
|
487
|
-
const lockStaleAfterMs = Math.max(MIN_IMPROVE_LOCK_STALE_MS, budgetMs + 10 * 60 * 1000);
|
|
488
487
|
const effectiveSync = { ...improveProfile.sync, ...options.sync };
|
|
489
488
|
return {
|
|
490
489
|
startMs,
|
|
@@ -514,7 +513,6 @@ function resolveImproveRunSetup(options) {
|
|
|
514
513
|
syncRepoDir,
|
|
515
514
|
resolvedStateDbPath,
|
|
516
515
|
resolvedLockPath,
|
|
517
|
-
lockStaleAfterMs,
|
|
518
516
|
effectiveSync,
|
|
519
517
|
};
|
|
520
518
|
}
|
|
@@ -9,13 +9,12 @@ import { createLockPayload, probeLock, reclaimStaleLock, releaseLock, tryAcquire
|
|
|
9
9
|
import { tryWithMaintenanceStartBarrier, withMaintenanceStartBarrier } from "../../core/maintenance-barrier.js";
|
|
10
10
|
import { describeInaccessiblePath } from "../../core/path-access.js";
|
|
11
11
|
import { warn } from "../../core/warn.js";
|
|
12
|
-
export const MIN_IMPROVE_LOCK_STALE_MS = 4 * 60 * 60 * 1000;
|
|
13
12
|
export function improveLockPath(lockBaseDir) {
|
|
14
13
|
return path.join(lockBaseDir, "improve.lock");
|
|
15
14
|
}
|
|
16
|
-
export function tryAcquireImproveLock(lockPath,
|
|
15
|
+
export function tryAcquireImproveLock(lockPath, skipIfLocked, eventsCtx) {
|
|
17
16
|
let recoveryEvent;
|
|
18
|
-
const acquire = () => tryAcquireImproveLockUnlocked(lockPath,
|
|
17
|
+
const acquire = () => tryAcquireImproveLockUnlocked(lockPath, skipIfLocked, (event) => {
|
|
19
18
|
recoveryEvent = event;
|
|
20
19
|
});
|
|
21
20
|
const result = skipIfLocked ? tryWithMaintenanceStartBarrier(acquire) : withMaintenanceStartBarrier(acquire);
|
|
@@ -37,14 +36,23 @@ export function tryAcquireImproveLock(lockPath, staleAfterMs, skipIfLocked, even
|
|
|
37
36
|
}
|
|
38
37
|
return result;
|
|
39
38
|
}
|
|
40
|
-
function tryAcquireImproveLockUnlocked(lockPath,
|
|
39
|
+
function tryAcquireImproveLockUnlocked(lockPath, skipIfLocked, onRecovered) {
|
|
41
40
|
fs.mkdirSync(path.dirname(lockPath), { recursive: true });
|
|
42
41
|
const lockPayload = () => createLockPayload({ startedAt: new Date().toISOString() });
|
|
43
42
|
let ownership = tryAcquireLockSync(lockPath, lockPayload());
|
|
44
43
|
if (ownership) {
|
|
45
44
|
return { state: "acquired", ownership };
|
|
46
45
|
}
|
|
47
|
-
|
|
46
|
+
// No `staleAfterMs`: only a verifiably dead holder is ever reclaimed. A
|
|
47
|
+
// wedged-but-alive `akm improve` (SQLite WAL + busy_timeout + BEGIN
|
|
48
|
+
// IMMEDIATE already serialize concurrent writes to state.db at the
|
|
49
|
+
// correctness layer, so this lock only avoids duplicate LOGICAL work) must
|
|
50
|
+
// not have its lease silently taken away purely because a clock elapsed —
|
|
51
|
+
// that was the #872-shaped hazard here: a live holder passed the
|
|
52
|
+
// PID-liveness check forever, so only a multi-hour age window could ever
|
|
53
|
+
// free it, stranding every `akm improve --skip-if-locked` invocation for
|
|
54
|
+
// up to that long while reporting success.
|
|
55
|
+
const probe = probeLock(lockPath);
|
|
48
56
|
// Race: the holder released the lock between our failed `tryAcquireLockSync`
|
|
49
57
|
// and this probe, so the probe sees no file (`absent`). Retry acquisition once
|
|
50
58
|
// rather than falling through to the contended skip/throw below — otherwise we
|
|
@@ -13,7 +13,6 @@ import { getDbPath } from "../../core/paths.js";
|
|
|
13
13
|
import { withStateDb } from "../../core/state-db.js";
|
|
14
14
|
import { info } from "../../core/warn.js";
|
|
15
15
|
import { DEFAULT_GRAPH_EXTRACTION_INCLUDE_TYPES, runGraphExtractionPass, } from "../../indexer/graph/graph-extraction.js";
|
|
16
|
-
import { withIndexWriterLease } from "../../indexer/index-writer-lock.js";
|
|
17
16
|
import { deriveWritableBundleIds } from "../../indexer/installations.js";
|
|
18
17
|
import { collectPendingMemories, runMemoryInferencePass, } from "../../indexer/passes/memory-inference.js";
|
|
19
18
|
import { resolveSourceEntries } from "../../indexer/search/search-source.js";
|
|
@@ -679,13 +678,13 @@ export async function runImproveMaintenancePasses(args) {
|
|
|
679
678
|
graphExtractionFn,
|
|
680
679
|
reindexWithIndexDbReleased,
|
|
681
680
|
};
|
|
682
|
-
const collected = await
|
|
681
|
+
const collected = await runMaintenancePassesUnderLease(ctx, dbCell, {
|
|
683
682
|
actionableRefs: args.actionableRefs,
|
|
684
683
|
memoryRefsForInference,
|
|
685
684
|
consolidationRan: args.consolidationRan,
|
|
686
685
|
allWarnings,
|
|
687
686
|
openIndexDb,
|
|
688
|
-
})
|
|
687
|
+
});
|
|
689
688
|
return {
|
|
690
689
|
...(collected.memoryInference ? { memoryInference: collected.memoryInference } : {}),
|
|
691
690
|
...(collected.graphExtraction ? { graphExtraction: collected.graphExtraction } : {}),
|
|
@@ -697,12 +696,12 @@ export async function runImproveMaintenancePasses(args) {
|
|
|
697
696
|
};
|
|
698
697
|
}
|
|
699
698
|
/**
|
|
700
|
-
* The maintenance sequence
|
|
701
|
-
*
|
|
702
|
-
* reindex-after-inference → graph extraction →
|
|
703
|
-
* expiration) → retention purges. Each pass
|
|
704
|
-
* this orchestrator folds warnings into the
|
|
705
|
-
* same points the inline code pushed them.
|
|
699
|
+
* The maintenance sequence (formerly the ~389-line anonymous
|
|
700
|
+
* `withIndexWriterLease` callback, before #872 removed the index-rebuild
|
|
701
|
+
* lease): memory inference → reindex-after-inference → graph extraction →
|
|
702
|
+
* proposal hygiene (orphan purge, expiration) → retention purges. Each pass
|
|
703
|
+
* returns its results and warnings; this orchestrator folds warnings into the
|
|
704
|
+
* caller's `allWarnings` sink at the same points the inline code pushed them.
|
|
706
705
|
*/
|
|
707
706
|
async function runMaintenancePassesUnderLease(ctx, dbCell, args) {
|
|
708
707
|
const { allWarnings } = args;
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* # Algorithm
|
|
15
15
|
*
|
|
16
16
|
* 1. Collect all derived memories grouped by `parentRef` family.
|
|
17
|
-
* 2. For each family, enumerate candidate pairs
|
|
17
|
+
* 2. For each family, enumerate candidate pairs.
|
|
18
18
|
* 3. For each pair, call the LLM to judge whether the two memories are in
|
|
19
19
|
* direct factual conflict.
|
|
20
20
|
* 4. For confirmed contradictions, write `contradictedBy` edges directly to
|
|
@@ -43,16 +43,6 @@ import { callStructured, preflightStructuredLlmRunner } from "../../../llm/struc
|
|
|
43
43
|
import { resolveImproveLlmExecution } from "../execution.js";
|
|
44
44
|
import { isDerivedMemory, memoryIdentityRef, resolveParentRef } from "./derived-ref.js";
|
|
45
45
|
// ── Constants ────────────────────────────────────────────────────────────────
|
|
46
|
-
/**
|
|
47
|
-
* Maximum family size for pairwise contradiction checking. Families larger
|
|
48
|
-
* than this are skipped to bound the LLM call count (O(n²) pairs).
|
|
49
|
-
*/
|
|
50
|
-
const MAX_FAMILY_SIZE = 8;
|
|
51
|
-
/**
|
|
52
|
-
* Maximum number of contradiction pairs to check per improve run, across all
|
|
53
|
-
* families. Prevents runaway LLM usage on stashes with many memories.
|
|
54
|
-
*/
|
|
55
|
-
const MAX_PAIRS_PER_RUN = 20;
|
|
56
46
|
/**
|
|
57
47
|
* Minimum confidence required to write a contradiction edge. Below this
|
|
58
48
|
* threshold the LLM may be flagging topic-overlap rather than genuine logical
|
|
@@ -218,15 +208,9 @@ export async function detectAndWriteContradictions(stashDir, config, chat, strat
|
|
|
218
208
|
for (const [, family] of byParent) {
|
|
219
209
|
if (family.length < 2)
|
|
220
210
|
continue;
|
|
221
|
-
if (family.length > MAX_FAMILY_SIZE) {
|
|
222
|
-
result.warnings.push(`Skipping contradiction check for family of ${family.length} members (exceeds MAX_FAMILY_SIZE=${MAX_FAMILY_SIZE})`);
|
|
223
|
-
continue;
|
|
224
|
-
}
|
|
225
211
|
result.familiesExamined++;
|
|
226
212
|
for (let i = 0; i < family.length - 1; i++) {
|
|
227
213
|
for (let j = i + 1; j < family.length; j++) {
|
|
228
|
-
if (candidatePairs.length >= MAX_PAIRS_PER_RUN)
|
|
229
|
-
break;
|
|
230
214
|
const a = family[i];
|
|
231
215
|
const b = family[j];
|
|
232
216
|
if (!a || !b)
|
|
@@ -245,8 +229,6 @@ export async function detectAndWriteContradictions(stashDir, config, chat, strat
|
|
|
245
229
|
continue;
|
|
246
230
|
candidatePairs.push({ a, b, loser, winnerRef });
|
|
247
231
|
}
|
|
248
|
-
if (candidatePairs.length >= MAX_PAIRS_PER_RUN)
|
|
249
|
-
break;
|
|
250
232
|
}
|
|
251
233
|
}
|
|
252
234
|
if (candidatePairs.length === 0) {
|
|
@@ -9,12 +9,6 @@ import { getAllAssetOutcomes, getAssetOutcome, getOutcomeScoresByRef, upsertAsse
|
|
|
9
9
|
* At α = 0.3 the window is ≈ 3 cycles.
|
|
10
10
|
*/
|
|
11
11
|
export const OUTCOME_EMA_ALPHA = 0.3;
|
|
12
|
-
/**
|
|
13
|
-
* Maximum K improve cycles for the eligibility-trace window. Retrievals older
|
|
14
|
-
* than K cycles contribute via the EMA decay naturally (they are already baked
|
|
15
|
-
* into `expected_retrieval_rate`).
|
|
16
|
-
*/
|
|
17
|
-
export const OUTCOME_TRACE_CYCLES = 5;
|
|
18
12
|
/**
|
|
19
13
|
* Warm-start cap: the maximum `outcome_score` a brand-new row can be seeded with
|
|
20
14
|
* (from the utility EMA). Prevents a `[0,1]`-range utility value from generating
|
|
@@ -44,7 +44,7 @@ import { prepareInlineExecution, prepareInlineExecutionWithRunner } from "../../
|
|
|
44
44
|
import { buildReflectOutputRepairPrompt, buildReflectPrompt, extractDraftConfidence, parseAgentProposalPayload, } from "../../integrations/agent/prompts.js";
|
|
45
45
|
import { runnerIsLlm, runnerSupportsFileWrite } from "../../integrations/agent/runner.js";
|
|
46
46
|
import { collectDispatchSensitiveValues } from "../../integrations/agent/runner-dispatch.js";
|
|
47
|
-
import { LlmCallError } from "../../llm/client.js";
|
|
47
|
+
import { isJsonSchemaKnownUnsupported, LlmCallError } from "../../llm/client.js";
|
|
48
48
|
import { callStructured } from "../../llm/structured-call.js";
|
|
49
49
|
import { baseFailureFields, enoentHintMessage, isEnoentFailure } from "../agent/agent-support.js";
|
|
50
50
|
import { isProposalSkipped, listProposalsReadOnly, proposalContent, } from "../proposal/repository.js";
|
|
@@ -578,8 +578,9 @@ export function sanitizeReflectPayload(payload, sourceContent, targetRef) {
|
|
|
578
578
|
}
|
|
579
579
|
/**
|
|
580
580
|
* JSON Schema for structured reflect output. Passed to `chatCompletion` when
|
|
581
|
-
*
|
|
582
|
-
* JSON object containing only the target-scoped
|
|
581
|
+
* {@link wantsJsonSchemaOutput} selects `outputMode: "json_schema"`, so the
|
|
582
|
+
* model returns a strict JSON object containing only the target-scoped
|
|
583
|
+
* fields AKM cannot derive.
|
|
583
584
|
*/
|
|
584
585
|
const REFLECT_FRONTMATTER_PATCH_JSON_SCHEMA = {
|
|
585
586
|
type: "object",
|
|
@@ -626,6 +627,17 @@ const REFLECT_UNSCOPED_JSON_SCHEMA = {
|
|
|
626
627
|
frontmatterPatch: REFLECT_FRONTMATTER_PATCH_JSON_SCHEMA,
|
|
627
628
|
},
|
|
628
629
|
};
|
|
630
|
+
/**
|
|
631
|
+
* Whether to frame the reflect prompt for structured JSON output on this
|
|
632
|
+
* connection. Optimistic by default — `chatCompletion` attempts
|
|
633
|
+
* `response_format: json_schema` fresh on every call and falls back once on
|
|
634
|
+
* a 4xx, so there is no persisted verdict to consult here. `false` only when
|
|
635
|
+
* a human/workflow explicitly disabled it, or a real call already proved
|
|
636
|
+
* this connection rejects it earlier in the same process.
|
|
637
|
+
*/
|
|
638
|
+
function wantsJsonSchemaOutput(connection) {
|
|
639
|
+
return connection.supportsJsonSchema !== false && !isJsonSchemaKnownUnsupported(connection);
|
|
640
|
+
}
|
|
629
641
|
/** Critique prompt injected between prior draft and refinement request (Self-Refine loop). */
|
|
630
642
|
const REFLECT_CRITIQUE_PROMPT = "Your previous proposal is shown above. Review it critically and provide an improved version that is more specific, actionable, and avoids any issues with the previous attempt. Return only the improved response using the output contract from the original prompt.";
|
|
631
643
|
/**
|
|
@@ -1354,21 +1366,20 @@ async function resolveReflectSource(options, stash, emitReflectFailed) {
|
|
|
1354
1366
|
}
|
|
1355
1367
|
/**
|
|
1356
1368
|
* Run the agent with the optional Self-Refine loop (R-1 / #372): up to
|
|
1357
|
-
*
|
|
1369
|
+
* `maxRefineIters` invocations, each injecting the prior draft as self-critique
|
|
1358
1370
|
* context and exiting early on a no-op refinement. Synthesizes per-iteration
|
|
1359
1371
|
* draft paths into `draftPathsToCleanup` (mutated) and returns the final agent
|
|
1360
1372
|
* result + last draft path. Extracted verbatim from `akmReflect`.
|
|
1361
1373
|
*/
|
|
1362
1374
|
async function runReflectRefineIterations(args) {
|
|
1363
1375
|
const { options, parsedRef, assetContent, feedback, schemaHints, relatedLessons, rejectedProposals, standardsContext, runnerSpec, lease, agentEnv, draftPathsToCleanup, onNotices, } = args;
|
|
1364
|
-
const
|
|
1365
|
-
const maxRefineIters = Math.min(Math.max(1, options.maxRefineIters ?? 1), MAX_REFINE_ITERS);
|
|
1376
|
+
const maxRefineIters = Math.max(1, options.maxRefineIters ?? 1);
|
|
1366
1377
|
// Determine whether this dispatch can honour the file-write contract.
|
|
1367
1378
|
// Agent CLI + OpenCode SDK runners both have filesystem access; the direct
|
|
1368
1379
|
// LLM HTTP runner does NOT.
|
|
1369
1380
|
const canRunnerWriteFile = runnerSupportsFileWrite(runnerSpec);
|
|
1370
1381
|
const outputMode = runnerIsLlm(runnerSpec)
|
|
1371
|
-
? runnerSpec.connection
|
|
1382
|
+
? wantsJsonSchemaOutput(runnerSpec.connection)
|
|
1372
1383
|
? "json_schema"
|
|
1373
1384
|
: "framed_markdown"
|
|
1374
1385
|
: undefined;
|
|
@@ -1421,10 +1432,10 @@ async function runReflectRefineIterations(args) {
|
|
|
1421
1432
|
...(options.signal ? { signal: options.signal } : {}),
|
|
1422
1433
|
priorDraft,
|
|
1423
1434
|
iteration: iter,
|
|
1424
|
-
...(
|
|
1435
|
+
...(outputMode === "json_schema"
|
|
1425
1436
|
? { responseSchema: options.ref ? REFLECT_JSON_SCHEMA : REFLECT_UNSCOPED_JSON_SCHEMA }
|
|
1426
1437
|
: {}),
|
|
1427
|
-
outputMode:
|
|
1438
|
+
outputMode: outputMode ?? "framed_markdown",
|
|
1428
1439
|
...(options.ref ? { targetRef: options.ref } : {}),
|
|
1429
1440
|
allowRepair: repairAttempts === 0,
|
|
1430
1441
|
...(options.chat ? { chat: options.chat } : {}),
|
|
@@ -92,7 +92,7 @@ function collectWorkflowFiles(dir) {
|
|
|
92
92
|
continue;
|
|
93
93
|
const full = path.join(dir, entry.name);
|
|
94
94
|
if (entry.isDirectory()) {
|
|
95
|
-
if (
|
|
95
|
+
if (isAkmRegistryCachePath(full))
|
|
96
96
|
continue;
|
|
97
97
|
results.push(...collectWorkflowFiles(full));
|
|
98
98
|
continue;
|
|
@@ -185,11 +185,9 @@ function collectAdapterFiles(root, extensions) {
|
|
|
185
185
|
walk(full);
|
|
186
186
|
}
|
|
187
187
|
else if (entry.isFile() && matchesAdapterExtension(entry.name, extensions)) {
|
|
188
|
-
//
|
|
189
|
-
//
|
|
190
|
-
|
|
191
|
-
const segments = path.relative(root, full).split(/[\\/]/);
|
|
192
|
-
if (segments.includes(".cache") || segments.includes("registry"))
|
|
188
|
+
// Skip akm's OWN resolved registry-cache copies, not any user
|
|
189
|
+
// directory that happens to be named ".cache" or "registry".
|
|
190
|
+
if (isAkmRegistryCachePath(full))
|
|
193
191
|
continue;
|
|
194
192
|
results.push(full);
|
|
195
193
|
}
|
|
@@ -196,13 +196,6 @@ export function hasHotCaptureMode(frontmatter) {
|
|
|
196
196
|
* with 80-90% overlap legitimately compress to well under 50% of the larger.
|
|
197
197
|
*/
|
|
198
198
|
export const MERGE_SHRINK_RATIO_MIN = 0.3;
|
|
199
|
-
/**
|
|
200
|
-
* Absolute floor (chars) for merged body. When sources are short (<~333 chars),
|
|
201
|
-
* `MERGE_SHRINK_RATIO_MIN × largerBodyLen` falls below this and the absolute
|
|
202
|
-
* floor dominates — prevents false positives on very terse memory pairs.
|
|
203
|
-
* Matches the existing `promote_source_too_small` floor of 100 chars.
|
|
204
|
-
*/
|
|
205
|
-
export const MERGE_ABSOLUTE_FLOOR_CHARS = 100;
|
|
206
199
|
// ── Reflect size gate ────────────────────────────────────────────────────────
|
|
207
200
|
/** Ratio lower-bound: proposed body must be at least this fraction of source. */
|
|
208
201
|
export const REFLECT_SHRINK_RATIO_MIN = 0.5;
|
|
@@ -67,6 +67,18 @@ const canonicalProposalValidators = {
|
|
|
67
67
|
const content = proposalContent(proposal);
|
|
68
68
|
if (!content.trim())
|
|
69
69
|
return [];
|
|
70
|
+
// #859: proposedTarget is absent on legacy archived rows, but this
|
|
71
|
+
// validator only ever runs on a proposal about to be minted or promoted
|
|
72
|
+
// (both always carry proposedTarget — see the Proposal.proposedTarget
|
|
73
|
+
// doc comment) — so hitting this is a genuine defect, not a legacy gap.
|
|
74
|
+
if (!proposal.proposedTarget) {
|
|
75
|
+
return [
|
|
76
|
+
{
|
|
77
|
+
kind: "invalid-workflow-structure",
|
|
78
|
+
message: `Workflow proposal ${proposal.id} (${proposal.ref}) is missing proposedTarget and cannot be validated.`,
|
|
79
|
+
},
|
|
80
|
+
];
|
|
81
|
+
}
|
|
70
82
|
const sourcePath = proposal.changes[0]?.path || proposal.ref;
|
|
71
83
|
const result = compileWorkflowSource(content, {
|
|
72
84
|
path: sourcePath,
|
|
@@ -14,13 +14,10 @@
|
|
|
14
14
|
import { getSources, loadConfig } from "../../core/config/config.js";
|
|
15
15
|
import { rethrowIfTestIsolationError, UsageError } from "../../core/errors.js";
|
|
16
16
|
import { appendEvent } from "../../core/events.js";
|
|
17
|
-
import { isTransientStashPath } from "../../core/paths.js";
|
|
18
17
|
import { resolveReadSources } from "../../indexer/read-preflight.js";
|
|
19
18
|
import { searchLocal } from "../../indexer/search/db-search.js";
|
|
20
19
|
import { getSearchHitAttribution, usageEventAttributionMetadata, } from "../../indexer/search/search-attribution.js";
|
|
21
20
|
import { getEntryIdByFilePath, getItemRefById } from "../../storage/repositories/index-entries-repository.js";
|
|
22
|
-
import { bumpUtilityScoresBatch } from "../../storage/repositories/index-utility-repository.js";
|
|
23
|
-
import { getCurrentWorkflowScopeKey } from "../../workflows/authoring/scope-key.js";
|
|
24
21
|
// Eagerly import source providers to trigger self-registration before the
|
|
25
22
|
// indexer or path-resolution code runs.
|
|
26
23
|
import "../../sources/providers/index.js";
|
|
@@ -188,7 +185,7 @@ function usageSearchMode(mode) {
|
|
|
188
185
|
function maybeLogSearchEvent(input, query, response, mode) {
|
|
189
186
|
if (input.skipLogging)
|
|
190
187
|
return;
|
|
191
|
-
logSearchEvent(query, response, mode, input.eventSource, input.
|
|
188
|
+
logSearchEvent(query, response, mode, input.eventSource, input.attributionProjection);
|
|
192
189
|
}
|
|
193
190
|
/**
|
|
194
191
|
* Resolve entry IDs by file_path lookup (exact match, not LIKE).
|
|
@@ -229,7 +226,7 @@ function resolveEntryIds(db, hits) {
|
|
|
229
226
|
* Per-entry events are recorded only for stash hits because registry hits
|
|
230
227
|
* have no local entry_id to reference.
|
|
231
228
|
*/
|
|
232
|
-
function logSearchEvent(query, response, mode = "keyword", eventSource = "user",
|
|
229
|
+
function logSearchEvent(query, response, mode = "keyword", eventSource = "user", attributionProjection = "full") {
|
|
233
230
|
// Emit a structured event to events.jsonl so workflow-trace consumers
|
|
234
231
|
// detect akm search invocations without relying on stdout scraping.
|
|
235
232
|
const stashHits = response.hits.filter((h) => h.type !== "registry");
|
|
@@ -276,25 +273,18 @@ function logSearchEvent(query, response, mode = "keyword", eventSource = "user",
|
|
|
276
273
|
source: eventSource,
|
|
277
274
|
});
|
|
278
275
|
}, TELEMETRY_BUSY_TIMEOUT_MS);
|
|
279
|
-
//
|
|
280
|
-
//
|
|
281
|
-
//
|
|
282
|
-
//
|
|
283
|
-
//
|
|
284
|
-
//
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
scopeKey = disabled ? undefined : getCurrentWorkflowScopeKey();
|
|
292
|
-
}
|
|
293
|
-
catch {
|
|
294
|
-
// Non-fatal — fall back to global-only bumps on any error.
|
|
295
|
-
}
|
|
296
|
-
bumpUtilityScoresBatch(db, resolvedIds, 1.0, 0.1, scopeKey);
|
|
297
|
-
}
|
|
276
|
+
// No live utility_scores/utility_scores_scoped write here (#862): a
|
|
277
|
+
// search result is an impression, not a signal that the asset was
|
|
278
|
+
// useful. Rewarding every returned hit created a feedback loop where
|
|
279
|
+
// merely appearing in results inflated future ranking — assets
|
|
280
|
+
// surfaced because they'd surfaced before, not because a user acted
|
|
281
|
+
// on them. Retrieval counts are still recorded above via
|
|
282
|
+
// insertUsageEvent (search_count) and rolled into utility_scores by
|
|
283
|
+
// the offline `recomputeUtilityScores` pass (`akm index`), which uses
|
|
284
|
+
// the show/search *select rate* — a ratio that requires an actual
|
|
285
|
+
// `show`/select event, not raw impressions. Explicit signal comes
|
|
286
|
+
// from `akm feedback` (applyFeedbackToUtilityScore) and from
|
|
287
|
+
// selection (recordShowUsage / the `select` event derived from it).
|
|
298
288
|
}, { busyTimeoutMs: TELEMETRY_BUSY_TIMEOUT_MS });
|
|
299
289
|
}
|
|
300
290
|
catch (err) {
|
|
@@ -13,20 +13,25 @@
|
|
|
13
13
|
* keying rules across call sites.
|
|
14
14
|
*/
|
|
15
15
|
import path from "node:path";
|
|
16
|
-
import { primaryBundlePath } from "../../core/config/config.js";
|
|
16
|
+
import { bundleKeyForContentRoot, primaryBundlePath } from "../../core/config/config.js";
|
|
17
17
|
import { deriveBundleId } from "../../indexer/installations.js";
|
|
18
18
|
export { primaryBundlePath };
|
|
19
19
|
/**
|
|
20
20
|
* Upsert the primary filesystem bundle (`{ path, writable: true }`) and point
|
|
21
21
|
* `defaultBundle` at it. Reuses the current default key when it already names a
|
|
22
|
-
* filesystem bundle (so re-pointing the primary keeps a stable id)
|
|
23
|
-
*
|
|
22
|
+
* filesystem bundle (so re-pointing the primary keeps a stable id). Otherwise,
|
|
23
|
+
* reuses whichever configured bundle ALREADY resolves to `stashDir`'s content
|
|
24
|
+
* root (issue #870 — `AKM_BUNDLE_DIR`/`--dir` pointed at a directory already
|
|
25
|
+
* configured under a different id must not mint a second bundle for it); only
|
|
26
|
+
* when no bundle owns that root yet is a fresh slug-legal key derived.
|
|
24
27
|
*/
|
|
25
28
|
export function withPrimaryBundle(config, stashDir) {
|
|
26
29
|
const bundles = { ...(config.bundles ?? {}) };
|
|
27
30
|
let key = config.defaultBundle;
|
|
28
31
|
if (!key || !(key in bundles) || typeof bundles[key]?.path !== "string") {
|
|
29
|
-
key =
|
|
32
|
+
key =
|
|
33
|
+
bundleKeyForContentRoot(config, path.resolve(stashDir)) ??
|
|
34
|
+
deriveBundleId(undefined, stashDir, new Set(Object.keys(bundles)));
|
|
30
35
|
}
|
|
31
36
|
bundles[key] = {
|
|
32
37
|
...bundles[key],
|
|
@@ -8,7 +8,6 @@ import { classifyPathAccess, describeInaccessiblePath } from "../../core/path-ac
|
|
|
8
8
|
import { getDbPath } from "../../core/paths.js";
|
|
9
9
|
import { formatRegistryUrl } from "../../core/registry-url.js";
|
|
10
10
|
import { error } from "../../core/warn.js";
|
|
11
|
-
import { getEffectiveSemanticStatus, readSemanticStatus } from "../../indexer/search/semantic-status.js";
|
|
12
11
|
import { closeDatabase, openExistingDatabase } from "../../storage/repositories/index-connection.js";
|
|
13
12
|
import { getEntryCount, getEntryCountByType } from "../../storage/repositories/index-entries-repository.js";
|
|
14
13
|
import { getMeta } from "../../storage/repositories/index-meta-repository.js";
|
|
@@ -29,13 +28,6 @@ export function assembleInfo(options) {
|
|
|
29
28
|
const defaultBundle = config.defaultBundle ?? null;
|
|
30
29
|
// Asset types (copy into a mutable array — `placementTypes()` returns readonly)
|
|
31
30
|
const assetTypes = [...placementTypes()];
|
|
32
|
-
const semanticRuntime = readSemanticStatus();
|
|
33
|
-
const semanticStatus = getEffectiveSemanticStatus(config, semanticRuntime);
|
|
34
|
-
// Search modes
|
|
35
|
-
const searchModes = ["fts"];
|
|
36
|
-
if (semanticStatus === "ready-js" || semanticStatus === "ready-vec") {
|
|
37
|
-
searchModes.push("semantic", "hybrid");
|
|
38
|
-
}
|
|
39
31
|
// Registries (strip sensitive fields like apiKey from options)
|
|
40
32
|
const registries = (config.registries ?? []).map((r) => ({
|
|
41
33
|
url: formatRegistryUrl(r.url),
|
|
@@ -58,6 +50,20 @@ export function assembleInfo(options) {
|
|
|
58
50
|
// health and search use, so info reads the same database they do.
|
|
59
51
|
const resolvedDbPath = options?.dbPath ?? getDbPath();
|
|
60
52
|
const indexStats = readIndexStats(resolvedDbPath);
|
|
53
|
+
// Semantic status is read live from the index's own state, not a cached
|
|
54
|
+
// verdict — a failed embed attempt at search time falls back to FTS and
|
|
55
|
+
// reports that in the search response, it never disables the mode here.
|
|
56
|
+
const semanticStatus = config.semanticSearchMode === "off"
|
|
57
|
+
? "disabled"
|
|
58
|
+
: !indexStats.hasEmbeddings
|
|
59
|
+
? "pending"
|
|
60
|
+
: indexStats.vecAvailable
|
|
61
|
+
? "ready-vec"
|
|
62
|
+
: "ready-js";
|
|
63
|
+
const searchModes = ["fts"];
|
|
64
|
+
if (semanticStatus === "ready-js" || semanticStatus === "ready-vec") {
|
|
65
|
+
searchModes.push("semantic", "hybrid");
|
|
66
|
+
}
|
|
61
67
|
return {
|
|
62
68
|
schemaVersion: 1,
|
|
63
69
|
version: pkgVersion,
|
|
@@ -68,8 +74,6 @@ export function assembleInfo(options) {
|
|
|
68
74
|
semanticSearch: {
|
|
69
75
|
mode: config.semanticSearchMode,
|
|
70
76
|
status: semanticStatus,
|
|
71
|
-
...(semanticRuntime?.reason ? { reason: semanticRuntime.reason } : {}),
|
|
72
|
-
...(semanticRuntime?.message ? { message: semanticRuntime.message } : {}),
|
|
73
77
|
},
|
|
74
78
|
registries,
|
|
75
79
|
sourceProviders,
|
|
@@ -31,7 +31,7 @@ import { defineGroupCommand, defineJsonCommand, EXIT_CODES, GLOBAL_OUTPUT_ARGS,
|
|
|
31
31
|
import { UsageError } from "../../core/errors.js";
|
|
32
32
|
import { TASK_RUN_BOOLEAN_FLAGS, TASK_RUN_VALUE_FLAGS } from "../../tasks/task-run-reserved-flags.js";
|
|
33
33
|
import { akmTaskExplain } from "./explain.js";
|
|
34
|
-
import { akmTasksAdd, akmTasksDoctor, akmTasksHistory, akmTasksRun, akmTasksSync, akmTasksSyncPlan } from "./tasks.js";
|
|
34
|
+
import { akmTasksAdd, akmTasksDoctor, akmTasksHistory, akmTasksPrune, akmTasksRun, akmTasksSync, akmTasksSyncPlan, } from "./tasks.js";
|
|
35
35
|
/** Shared `--bundle <bundle>` arg wired onto every task subcommand. */
|
|
36
36
|
const bundleArg = {
|
|
37
37
|
bundle: {
|
|
@@ -307,10 +307,13 @@ const tasksHistoryCommand = defineJsonCommand({
|
|
|
307
307
|
* #849: `task sync --dry-run`'s exit-code contract, "non-zero when the plan
|
|
308
308
|
* contains removals" — factored out as a pure function (rather than left
|
|
309
309
|
* inline in the command's `run()`) so it's directly unit-testable without
|
|
310
|
-
* driving the whole CLI through a real scheduler backend.
|
|
310
|
+
* driving the whole CLI through a real scheduler backend. #867: also
|
|
311
|
+
* non-zero when any source failed to parse/prepare — sync degrades (still
|
|
312
|
+
* reconciles the tasks/workflows that DID parse) rather than rejecting the
|
|
313
|
+
* whole set, but a dropped source must still surface as a failing exit.
|
|
311
314
|
*/
|
|
312
315
|
export function taskSyncDryRunExitCode(preview) {
|
|
313
|
-
return preview.hasRemovals ? EXIT_CODES.GENERAL : undefined;
|
|
316
|
+
return preview.hasRemovals || (preview.failures?.length ?? 0) > 0 ? EXIT_CODES.GENERAL : undefined;
|
|
314
317
|
}
|
|
315
318
|
const tasksSyncCommand = defineJsonCommand({
|
|
316
319
|
meta: {
|
|
@@ -345,6 +348,12 @@ const tasksSyncCommand = defineJsonCommand({
|
|
|
345
348
|
}
|
|
346
349
|
const result = await akmTasksSync({}, args.bundle, { rebind });
|
|
347
350
|
output("task-sync", result);
|
|
351
|
+
// #867: sync degrades — sources that failed to parse/prepare are
|
|
352
|
+
// excluded from reconciliation and reported in `result.failed` rather
|
|
353
|
+
// than poisoning the whole sync, but their presence must still fail
|
|
354
|
+
// the command's exit code so the breakage stays visible.
|
|
355
|
+
if (result.failed.length > 0)
|
|
356
|
+
process.exitCode = EXIT_CODES.GENERAL;
|
|
348
357
|
},
|
|
349
358
|
});
|
|
350
359
|
// ── `akm task explain` — read-only introspection (P2b Lane B, spec
|
|
@@ -389,6 +398,49 @@ const tasksDoctorCommand = defineJsonCommand({
|
|
|
389
398
|
output("task-doctor", result);
|
|
390
399
|
},
|
|
391
400
|
});
|
|
401
|
+
/**
|
|
402
|
+
* #851: `akm task prune`'s exit-code contract mirrors `task sync --dry-run`'s
|
|
403
|
+
* (`taskSyncDryRunExitCode` above) — non-zero whenever the preview lists
|
|
404
|
+
* removals the invocation didn't (or couldn't, without `--yes`) execute, so
|
|
405
|
+
* `akm task prune` is usable as a CI/health check the same way `sync
|
|
406
|
+
* --dry-run` is.
|
|
407
|
+
*/
|
|
408
|
+
export function taskPruneExitCode(result) {
|
|
409
|
+
return result.dryRun && result.preview.hasRemovals ? EXIT_CODES.GENERAL : undefined;
|
|
410
|
+
}
|
|
411
|
+
const tasksPruneCommand = defineJsonCommand({
|
|
412
|
+
meta: {
|
|
413
|
+
name: "prune",
|
|
414
|
+
description: "Remove installed scheduler entries `sync` can never reclaim because their own descriptor no longer " +
|
|
415
|
+
"resolves to a live bundle (corrupt/missing --scheduler-context, or the owning bundle directory is " +
|
|
416
|
+
"gone). Defaults to a dry-run preview — zero scheduler writes. Requires --yes to remove anything; " +
|
|
417
|
+
"--id narrows removal to specific binding ids (comma-separated).",
|
|
418
|
+
},
|
|
419
|
+
args: {
|
|
420
|
+
yes: {
|
|
421
|
+
type: "boolean",
|
|
422
|
+
description: "Execute removal of every currently-computed orphan (the plan is still printed first)",
|
|
423
|
+
default: false,
|
|
424
|
+
},
|
|
425
|
+
id: {
|
|
426
|
+
type: "string",
|
|
427
|
+
description: "Comma-separated scheduler binding id(s) to limit pruning to (must already be orphan candidates)",
|
|
428
|
+
},
|
|
429
|
+
},
|
|
430
|
+
async run({ args }) {
|
|
431
|
+
const id = args.id
|
|
432
|
+
? args.id
|
|
433
|
+
.split(",")
|
|
434
|
+
.map((value) => value.trim())
|
|
435
|
+
.filter(Boolean)
|
|
436
|
+
: undefined;
|
|
437
|
+
const result = await akmTasksPrune({}, { yes: args.yes === true, id });
|
|
438
|
+
output("task-prune", result);
|
|
439
|
+
const exitCode = taskPruneExitCode(result);
|
|
440
|
+
if (exitCode !== undefined)
|
|
441
|
+
process.exitCode = exitCode;
|
|
442
|
+
},
|
|
443
|
+
});
|
|
392
444
|
export const taskCommand = defineGroupCommand({
|
|
393
445
|
meta: {
|
|
394
446
|
name: "task",
|
|
@@ -400,6 +452,7 @@ export const taskCommand = defineGroupCommand({
|
|
|
400
452
|
explain: tasksExplainCommand,
|
|
401
453
|
history: tasksHistoryCommand,
|
|
402
454
|
sync: tasksSyncCommand,
|
|
455
|
+
prune: tasksPruneCommand,
|
|
403
456
|
doctor: tasksDoctorCommand,
|
|
404
457
|
},
|
|
405
458
|
// Bare `akm task` reports scheduler diagnostics. Inspection of individual
|