akm-cli 0.9.2-alpha.3 → 0.9.2-alpha.5
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 +664 -0
- package/STABILITY.md +23 -5
- package/dist/assets/hints/cli-hints-full.md +12 -7
- package/dist/assets/tasks/core/extract.yml +3 -5
- package/dist/assets/tasks/core/improve.yml +3 -5
- package/dist/assets/tasks/core/index-refresh.yml +3 -5
- package/dist/assets/tasks/core/sync.yml +3 -5
- package/dist/assets/tasks/core/version-check.yml +3 -5
- package/dist/assets/tasks/improve/akm-graph-refresh-weekly.yml +3 -5
- package/dist/assets/tasks/improve/akm-improve-catchup.yml +6 -6
- package/dist/assets/tasks/improve/akm-improve-consolidate.yml +3 -5
- package/dist/assets/tasks/improve/akm-improve-frequent.yml +3 -5
- package/dist/assets/tasks/improve/akm-improve-nightly.yml +3 -5
- package/dist/cli/unknown-flags.js +12 -1
- package/dist/cli.js +8 -1
- package/dist/commands/command/command-execution.js +23 -2
- package/dist/commands/health/improve-metrics.js +38 -0
- package/dist/commands/health/plugin-staleness.js +219 -0
- package/dist/commands/health/type-directory-check.js +167 -0
- package/dist/commands/health/windows.js +8 -4
- package/dist/commands/health.js +71 -9
- package/dist/commands/improve/extract.js +22 -1
- package/dist/commands/lint/index.js +1 -1
- package/dist/commands/migrate-cli.js +130 -24
- package/dist/commands/proposal/validators/proposal-validators.js +7 -2
- package/dist/commands/read/remember-cli.js +6 -1
- package/dist/commands/remember.js +70 -0
- package/dist/commands/tasks/explain.js +304 -0
- package/dist/commands/tasks/tasks-cli.js +185 -3
- package/dist/commands/tasks/tasks.js +233 -45
- package/dist/commands/workflow/plan.js +159 -0
- package/dist/commands/workflow-cli.js +94 -2
- package/dist/core/activation-policy.js +2 -12
- package/dist/core/adapter/adapters/akm-lint.js +7 -4
- package/dist/core/adapter/adapters/akm-metadata.js +26 -14
- package/dist/core/adapter/adapters/akm-task-adapter.js +13 -10
- package/dist/core/errors.js +45 -0
- package/dist/core/json-schema.js +15 -5
- package/dist/core/state/migrations.js +57 -0
- package/dist/core/state-db.js +16 -14
- package/dist/core/subprocess.js +47 -13
- package/dist/execution/guarded-source.js +44 -0
- package/dist/execution/input-contract.js +250 -0
- package/dist/execution/target-ref.js +63 -0
- package/dist/indexer/usage/usage-events.js +14 -3
- package/dist/integrations/agent/execution-lowering.js +12 -1
- package/dist/integrations/harnesses/claude/session-log.js +85 -25
- package/dist/integrations/session-logs/pre-filter.js +152 -2
- package/dist/output/shapes/passthrough.js +2 -0
- package/dist/output/text/helpers.js +1 -1
- package/dist/output/text/migrate.js +12 -3
- package/dist/output/text/workflow-format.js +192 -10
- package/dist/output/text/workflow.js +2 -1
- package/dist/registry/semver.js +4 -0
- package/dist/runtime.js +1 -0
- package/dist/scripts/akm-migrate-node.js +11838 -10118
- package/dist/scripts/akm-migrate.js +11828 -10117
- package/dist/setup/steps/tasks.js +34 -17
- package/dist/storage/repositories/task-history-repository.js +5 -1
- package/dist/storage/repositories/workflow-runs-repository.js +144 -6
- package/dist/tasks/backends/launchd.js +31 -84
- package/dist/tasks/embedded.js +13 -7
- package/dist/tasks/model/invocation.js +4 -0
- package/dist/tasks/prepare/prepare-script-target.js +9 -0
- package/dist/tasks/prepare/prepare-support.js +154 -0
- package/dist/tasks/prepare/prepare.js +117 -0
- package/dist/tasks/prepare/prepared-execution.js +4 -0
- package/dist/tasks/prepare/script-capture.js +80 -0
- package/dist/tasks/run/attempt-lifecycle.js +165 -0
- package/dist/tasks/run/load-task.js +117 -0
- package/dist/tasks/run/provenance.js +20 -0
- package/dist/tasks/run/run-command-task.js +92 -0
- package/dist/tasks/run/run-native-task.js +222 -0
- package/dist/tasks/run/run-task.js +99 -0
- package/dist/tasks/run/run-workflow-task.js +222 -0
- package/dist/tasks/run/task-history.js +134 -0
- package/dist/tasks/run/task-log.js +179 -0
- package/dist/tasks/run/task-result.js +19 -0
- package/dist/tasks/scheduler-binding.js +66 -2
- package/dist/tasks/scheduler-invocation.js +63 -3
- package/dist/tasks/scheduler-sync.js +55 -12
- package/dist/tasks/source/bounded-document.js +455 -0
- package/dist/tasks/source/parse-task-source.js +59 -0
- package/dist/tasks/source/project-v4.js +62 -0
- package/dist/tasks/source/task-input-diagnostics.js +36 -0
- package/dist/tasks/source/task-source-v4.js +626 -0
- package/dist/tasks/source-v3.js +10 -733
- package/dist/tasks/task-run-reserved-flags.js +79 -0
- package/dist/workflows/authoring/authoring.js +17 -8
- package/dist/workflows/exec/child-invocation.js +34 -0
- package/dist/workflows/exec/child-workflow.js +370 -0
- package/dist/workflows/exec/exec-unit.js +50 -170
- package/dist/workflows/exec/frozen-judge.js +19 -2
- package/dist/workflows/exec/native-executor.js +49 -27
- package/dist/workflows/exec/param-secrets.js +12 -0
- package/dist/workflows/exec/run-workflow.js +48 -59
- package/dist/workflows/exec/step-work.js +222 -80
- package/dist/workflows/exec/unit-dispatch.js +72 -0
- package/dist/workflows/freeze/child-output-references.js +94 -0
- package/dist/workflows/freeze/environment.js +174 -0
- package/dist/workflows/freeze/identity.js +22 -0
- package/dist/workflows/freeze/resolve-steps.js +78 -0
- package/dist/workflows/freeze/source-freeze.js +57 -0
- package/dist/workflows/freeze/step-values.js +68 -0
- package/dist/workflows/freeze/targets/child-workflow.js +206 -0
- package/dist/workflows/freeze/targets/command.js +81 -0
- package/dist/workflows/freeze/targets/script.js +57 -0
- package/dist/workflows/freeze/targets/shell.js +31 -0
- package/dist/workflows/freeze/targets/task.js +179 -0
- package/dist/workflows/freeze/task-bindings.js +180 -0
- package/dist/workflows/ir/compile.js +59 -11
- package/dist/workflows/ir/environment-v4.js +3 -3
- package/dist/workflows/ir/freeze-v4.js +41 -7
- package/dist/workflows/ir/params.js +58 -131
- package/dist/workflows/ir/plan-hash.js +3 -3
- package/dist/workflows/ir/schema-v4.js +246 -17
- package/dist/workflows/parser.js +74 -2
- package/dist/workflows/program/schema.js +5 -2
- package/dist/workflows/resource-limits.js +20 -0
- package/dist/workflows/runtime/plan-classifier.js +19 -5
- package/dist/workflows/runtime/run-outputs.js +103 -0
- package/dist/workflows/runtime/runs.js +114 -9
- package/dist/workflows/runtime/workflow-asset-loader.js +14 -6
- package/dist/workflows/source-files.js +5 -5
- package/dist/workflows/source-ir/compare.js +17 -0
- package/dist/workflows/source-ir/compile.js +7 -3
- package/dist/workflows/source-ir/github-yaml.js +64 -17
- package/dist/workflows/source-ir/schema.js +69 -21
- package/dist/workflows/source-ir/semantics.js +7 -25
- package/dist/workflows/source-ir/triggers.js +79 -0
- package/dist/workflows/source-ir/uses.js +33 -7
- package/docs/migration/README.md +1 -1
- package/docs/migration/release-notes/0.9.2.md +87 -11
- package/docs/migration/release-notes/README.md +2 -2
- package/docs/migration/v0.8-to-v0.9.md +9 -7
- package/docs/migration/v0.9.0-troubleshooting.md +14 -7
- package/docs/migration/v0.9.1-to-v0.9.2.md +598 -49
- package/docs/reference/README.md +1 -1
- package/docs/reference/cli.md +140 -46
- package/docs/reference/configuration.md +3 -3
- package/docs/reference/supported-formats.md +9 -5
- package/docs/reference/tasks.md +338 -75
- package/docs/reference/workflow-schema.md +281 -8
- package/docs/reference/workflows.md +57 -7
- package/package.json +1 -1
- package/schemas/akm-task.json +173 -118
- package/schemas/akm-workflow.json +28 -0
- package/dist/tasks/runner.js +0 -941
- package/dist/tasks/runtime-v3.js +0 -281
- package/dist/workflows/ir/source-freeze-v4.js +0 -506
- package/dist/workflows/source-ir/ordering.js +0 -38
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
/**
|
|
5
|
+
* `type-directory-disagreement` advisory for `akm health` (#831).
|
|
6
|
+
*
|
|
7
|
+
* The invariant "a file's directory declares its type" is load-bearing for
|
|
8
|
+
* refs, namespace listings, and `akm show` paths (see #824: three files
|
|
9
|
+
* written to `memories/` were indexed as `type: command`, moved their refs
|
|
10
|
+
* under `commands/memories/<slug>`, and silently vanished from the
|
|
11
|
+
* `memories/` namespace — nothing on any normal surface said so). This
|
|
12
|
+
* advisory re-checks that invariant against every currently indexed entry.
|
|
13
|
+
*
|
|
14
|
+
* Legitimate disagreements exist by design — a `knowledge/` file containing
|
|
15
|
+
* `$ARGUMENTS` is deliberately a `command`, and an `agents/` file with an
|
|
16
|
+
* `agent:` frontmatter key is deliberately a `command` (both asserted in
|
|
17
|
+
* `tests/integration/commands/show.test.ts`). So this is never a hard
|
|
18
|
+
* failure: every disagreement is reported with a `winner` naming which
|
|
19
|
+
* classifier signal produced the resolved type, so a deliberate override
|
|
20
|
+
* reads differently from an unexplained one.
|
|
21
|
+
*/
|
|
22
|
+
import fs from "node:fs";
|
|
23
|
+
import path from "node:path";
|
|
24
|
+
import { parseFrontmatter } from "../../core/asset/frontmatter.js";
|
|
25
|
+
/**
|
|
26
|
+
* Directory → declared-type map, mirroring `DIR_TYPE_MAP` in
|
|
27
|
+
* `src/indexer/walk/matchers.ts` minus its per-directory extension test —
|
|
28
|
+
* this check only needs "which type does this directory declare", not which
|
|
29
|
+
* extensions it accepts. Keep in sync if `DIR_TYPE_MAP` gains, renames, or
|
|
30
|
+
* removes a directory.
|
|
31
|
+
*/
|
|
32
|
+
const DECLARED_DIR_TYPES = {
|
|
33
|
+
memories: "memory",
|
|
34
|
+
knowledge: "knowledge",
|
|
35
|
+
commands: "command",
|
|
36
|
+
agents: "agent",
|
|
37
|
+
workflows: "workflow",
|
|
38
|
+
facts: "fact",
|
|
39
|
+
lessons: "lesson",
|
|
40
|
+
sessions: "session",
|
|
41
|
+
instructions: "instruction",
|
|
42
|
+
scripts: "script",
|
|
43
|
+
env: "env",
|
|
44
|
+
secrets: "secret",
|
|
45
|
+
tasks: "task",
|
|
46
|
+
};
|
|
47
|
+
const realReadFile = (absPath) => {
|
|
48
|
+
try {
|
|
49
|
+
return fs.readFileSync(absPath, "utf8");
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* The type a file's directory declares, mirroring the `classifyByDirectory` /
|
|
57
|
+
* `classifyByParentDirHint` precedence in matchers.ts: the immediate parent
|
|
58
|
+
* directory wins when it is itself typed (parentDirHint, specificity 15);
|
|
59
|
+
* otherwise the outermost typed ancestor wins (directoryMatcher, specificity
|
|
60
|
+
* 10, which walks root-to-leaf and returns on the first hit).
|
|
61
|
+
*/
|
|
62
|
+
function declaredTypeForPath(absPath) {
|
|
63
|
+
const segments = path
|
|
64
|
+
.dirname(absPath)
|
|
65
|
+
.split(path.sep)
|
|
66
|
+
.filter((seg) => seg.length > 0);
|
|
67
|
+
const immediateParent = segments.at(-1);
|
|
68
|
+
if (immediateParent) {
|
|
69
|
+
const parentType = DECLARED_DIR_TYPES[immediateParent];
|
|
70
|
+
if (parentType)
|
|
71
|
+
return { dir: immediateParent, type: parentType };
|
|
72
|
+
}
|
|
73
|
+
for (const seg of segments) {
|
|
74
|
+
const type = DECLARED_DIR_TYPES[seg];
|
|
75
|
+
if (type)
|
|
76
|
+
return { dir: seg, type };
|
|
77
|
+
}
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Best-effort explanation for why `classifyBySmartMd` (matchers.ts) would
|
|
82
|
+
* have produced `resolvedType` for this content, in the SAME precedence
|
|
83
|
+
* order the real function checks them. Returns `undefined` when no known
|
|
84
|
+
* override signal is found — that absence is itself the accident signal:
|
|
85
|
+
* nothing in the file explains why its type disagrees with its directory.
|
|
86
|
+
*
|
|
87
|
+
* The numeric-placeholder branch is flagged `knownGoodOverride: false` on
|
|
88
|
+
* purpose: since #826, that heuristic is guarded to never fire when the file
|
|
89
|
+
* sits under a declared-type directory, so seeing it win here would mean the
|
|
90
|
+
* guard regressed, not that this is a sanctioned override.
|
|
91
|
+
*/
|
|
92
|
+
function explainOverride(resolvedType, content) {
|
|
93
|
+
const fm = parseFrontmatter(content).data;
|
|
94
|
+
if (fm.type === "workflow" && resolvedType === "workflow") {
|
|
95
|
+
return { winner: "smart-md:workflow-frontmatter", knownGoodOverride: true };
|
|
96
|
+
}
|
|
97
|
+
if ("tools" in fm && resolvedType === "agent") {
|
|
98
|
+
return { winner: "smart-md:tools-frontmatter", knownGoodOverride: true };
|
|
99
|
+
}
|
|
100
|
+
if ("agent" in fm && resolvedType === "command") {
|
|
101
|
+
return { winner: "smart-md:agent-frontmatter", knownGoodOverride: true };
|
|
102
|
+
}
|
|
103
|
+
if (resolvedType === "command" && content.includes("$ARGUMENTS")) {
|
|
104
|
+
return { winner: "smart-md:$ARGUMENTS", knownGoodOverride: true };
|
|
105
|
+
}
|
|
106
|
+
if (resolvedType === "command" && /\$[123](?!\d|[.,]\d)/.test(content)) {
|
|
107
|
+
return { winner: "smart-md:numeric-placeholder", knownGoodOverride: false };
|
|
108
|
+
}
|
|
109
|
+
if ("model" in fm && resolvedType === "agent") {
|
|
110
|
+
return { winner: "smart-md:model-frontmatter", knownGoodOverride: true };
|
|
111
|
+
}
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Compare every indexed entry's resolved type against the type its
|
|
116
|
+
* directory declares (see {@link DECLARED_DIR_TYPES}), and return one
|
|
117
|
+
* {@link TypeDirectoryDisagreement} per mismatch, sorted by path. Entries
|
|
118
|
+
* outside any declared-type directory are not checked — this is only the
|
|
119
|
+
* "directory declares type" invariant.
|
|
120
|
+
*/
|
|
121
|
+
export function collectTypeDirectoryDisagreements(entries, readFile = realReadFile) {
|
|
122
|
+
const disagreements = [];
|
|
123
|
+
for (const entry of entries) {
|
|
124
|
+
const declared = declaredTypeForPath(entry.filePath);
|
|
125
|
+
if (!declared || declared.type === entry.type)
|
|
126
|
+
continue;
|
|
127
|
+
const content = readFile(entry.filePath);
|
|
128
|
+
const explanation = content === undefined ? undefined : explainOverride(entry.type, content);
|
|
129
|
+
disagreements.push({
|
|
130
|
+
path: entry.filePath,
|
|
131
|
+
resolved: entry.type,
|
|
132
|
+
expected: declared.type,
|
|
133
|
+
winner: explanation?.winner ?? "unknown",
|
|
134
|
+
knownGoodOverride: explanation?.knownGoodOverride ?? false,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
return disagreements.sort((a, b) => a.path.localeCompare(b.path));
|
|
138
|
+
}
|
|
139
|
+
const MAX_DETAIL_LINES = 10;
|
|
140
|
+
/**
|
|
141
|
+
* Build the `type-directory-disagreement` advisory, or `undefined` when
|
|
142
|
+
* every indexed entry agrees with its directory. Always `status: "warn"`
|
|
143
|
+
* (never `"fail"`) — a deliberate override is still a disagreement worth
|
|
144
|
+
* seeing, just not a gate.
|
|
145
|
+
*/
|
|
146
|
+
export function buildTypeDirectoryAdvisory(entries, readFile = realReadFile, displayPath = (p) => p) {
|
|
147
|
+
const disagreements = collectTypeDirectoryDisagreements(entries, readFile);
|
|
148
|
+
if (disagreements.length === 0)
|
|
149
|
+
return undefined;
|
|
150
|
+
const lines = disagreements.slice(0, MAX_DETAIL_LINES).map((d) => {
|
|
151
|
+
const note = d.knownGoodOverride ? " (known-good override)" : "";
|
|
152
|
+
return `${displayPath(d.path)} resolved=${d.resolved} expected=${d.expected} winner=${d.winner}${note}`;
|
|
153
|
+
});
|
|
154
|
+
if (disagreements.length > MAX_DETAIL_LINES) {
|
|
155
|
+
lines.push(`+${disagreements.length - MAX_DETAIL_LINES} more`);
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
name: "type-directory-disagreement",
|
|
159
|
+
kind: "deterministic",
|
|
160
|
+
status: "warn",
|
|
161
|
+
confidence: "high",
|
|
162
|
+
message: `${disagreements.length} indexed asset(s) have a resolved type that disagrees with the type their directory declares: ${lines.join("; ")}`,
|
|
163
|
+
evidence: {
|
|
164
|
+
disagreements: disagreements.map((d) => ({ ...d, path: displayPath(d.path) })),
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
}
|
|
@@ -11,7 +11,7 @@ import { readEvents } from "../../core/events.js";
|
|
|
11
11
|
import { buildTaskRunId, getLoggedRunIds } from "../../core/logs-db.js";
|
|
12
12
|
import { DURATION_UNITS, parseDuration } from "../../core/time.js";
|
|
13
13
|
import { queryTaskHistory } from "../../storage/repositories/task-history-repository.js";
|
|
14
|
-
import { buildImproveSkipSummary, computeWallTimeStats, parseTaskMetadata, roundRate, summarizeImproveCompleted, summarizeImproveRuns, } from "./improve-metrics.js";
|
|
14
|
+
import { buildImproveSkipSummary, computeWallTimeStats, isAgentTaskHistoryRow, parseTaskMetadata, roundRate, summarizeImproveCompleted, summarizeImproveRuns, } from "./improve-metrics.js";
|
|
15
15
|
import { readLlmUsageAggregate } from "./llm-usage.js";
|
|
16
16
|
import { computeDegradationMetrics, computeDenominatorFixedCoverage } from "./metrics.js";
|
|
17
17
|
import { buildPerRunSummaries } from "./task-runs.js";
|
|
@@ -148,14 +148,18 @@ export function buildWindowMetrics(db, stateDbPath, since, until, now = () => Da
|
|
|
148
148
|
const failedTaskRows = taskRows.filter((row) => row.status === "failed");
|
|
149
149
|
const activeRows = taskRows.filter((row) => row.status === "active" && row.completed_at === null);
|
|
150
150
|
const stuckActiveRuns = activeRows.filter((row) => now() - new Date(row.started_at).getTime() > ACTIVE_RUN_WARN_MS).length;
|
|
151
|
-
|
|
152
|
-
|
|
151
|
+
// D8 (spec §5.3): a marked "command" row or a legacy (unmarked) "prompt"
|
|
152
|
+
// row is the agent/LLM arm; an unmarked "command" row is the legacy
|
|
153
|
+
// native shell/script arm and must not be counted here (see
|
|
154
|
+
// isAgentTaskHistoryRow's header comment for the full mapping).
|
|
155
|
+
const agentRows = taskRows.filter((row) => isAgentTaskHistoryRow(row));
|
|
156
|
+
const agentFailures = agentRows.filter((row) => {
|
|
153
157
|
const detail = parseTaskMetadata(row).detail;
|
|
154
158
|
return typeof detail?.reason === "string" && detail.reason.length > 0;
|
|
155
159
|
});
|
|
156
160
|
const logBackingRate = taskRowsWithLogs.length === 0 ? 1 : existingLogRows.length / taskRowsWithLogs.length;
|
|
157
161
|
const taskFailRate = taskRows.length === 0 ? 0 : failedTaskRows.length / taskRows.length;
|
|
158
|
-
const agentFailureRate =
|
|
162
|
+
const agentFailureRate = agentRows.length === 0 ? 0 : agentFailures.length / agentRows.length;
|
|
159
163
|
const improveInvoked = readEvents({ since, type: "improve_invoked" }, { dbPath: stateDbPath }).events.filter((event) => new Date(event.ts ?? since).getTime() < new Date(until).getTime()).length;
|
|
160
164
|
const improveCompletedEvents = readEvents({ since, type: IMPROVE_COMPLETED_EVENT }, { dbPath: stateDbPath }).events.filter((event) => new Date(event.ts ?? since).getTime() < new Date(until).getTime());
|
|
161
165
|
const improveSkippedEvents = readEvents({ since, type: "improve_skipped" }, { dbPath: stateDbPath }).events.filter((event) => new Date(event.ts ?? since).getTime() < new Date(until).getTime());
|
package/dist/commands/health.js
CHANGED
|
@@ -15,15 +15,19 @@ import { listExistingTableNames, openStateDatabase } from "../core/state-db.js";
|
|
|
15
15
|
import { DURATION_UNITS, parseDuration, parseSinceToIso } from "../core/time.js";
|
|
16
16
|
import { readSemanticStatus } from "../indexer/search/semantic-status.js";
|
|
17
17
|
import { closeDatabase, openReadonlyExistingDatabase } from "../storage/repositories/index-connection.js";
|
|
18
|
+
import { getAllEntries } from "../storage/repositories/index-entries-repository.js";
|
|
18
19
|
import { queryTaskHistory } from "../storage/repositories/task-history-repository.js";
|
|
20
|
+
import { pkgVersion } from "../version.js";
|
|
19
21
|
import { collectImproveAdvisories } from "./health/advisories.js";
|
|
20
22
|
import { HEALTH_CHECKS, runHealthEngineProbes } from "./health/checks.js";
|
|
21
|
-
import { buildImproveSkipSummary, computeWallTimeStats, parseTaskMetadata, roundRate, summarizeImproveCompleted, summarizeImproveRuns, } from "./health/improve-metrics.js";
|
|
23
|
+
import { buildImproveSkipSummary, computeWallTimeStats, isAgentTaskHistoryRow, parseTaskMetadata, roundRate, summarizeImproveCompleted, summarizeImproveRuns, } from "./health/improve-metrics.js";
|
|
22
24
|
import { emptyLlmUsageAggregate, readLlmUsageAggregate } from "./health/llm-usage.js";
|
|
23
25
|
import { computeDegradationMetrics, computeDenominatorFixedCoverage, computeEnrichmentMintingRollup, probeStateDbRoundTrip, } from "./health/metrics.js";
|
|
26
|
+
import { collectPluginStalenessAdvisories } from "./health/plugin-staleness.js";
|
|
24
27
|
import { collectStashExposureAdvisory } from "./health/stash-exposure.js";
|
|
25
28
|
import { collectSurfacesAdvisories } from "./health/surfaces.js";
|
|
26
29
|
import { buildPerRunSummaries } from "./health/task-runs.js";
|
|
30
|
+
import { buildTypeDirectoryAdvisory } from "./health/type-directory-check.js";
|
|
27
31
|
import { ACTIVE_RUN_WARN_MS, IMPROVE_COMPLETED_EVENT, MIN_ROWS_FOR_WORST_TASK_FAIL_RATE, } from "./health/types.js";
|
|
28
32
|
import { buildWindowMetrics, computeDeltas, partitionLogBackedRows, resolveWindowCompare } from "./health/windows.js";
|
|
29
33
|
const DEFAULT_SINCE_MS = 24 * 60 * 60 * 1000;
|
|
@@ -120,14 +124,18 @@ function gatherTaskHistoryPhase(db, logsDb, since, stateDbPath, now) {
|
|
|
120
124
|
const failedTaskRows = taskRows.filter((row) => row.status === "failed");
|
|
121
125
|
const activeRows = taskRows.filter((row) => row.status === "active" && row.completed_at === null);
|
|
122
126
|
const stuckActiveRows = activeRows.filter((row) => now() - new Date(row.started_at).getTime() > ACTIVE_RUN_WARN_MS);
|
|
123
|
-
|
|
124
|
-
|
|
127
|
+
// D8 (spec §5.3): a marked "command" row or a legacy (unmarked) "prompt"
|
|
128
|
+
// row is the agent/LLM arm; an unmarked "command" row is the legacy
|
|
129
|
+
// native shell/script arm and must not be counted here (see
|
|
130
|
+
// isAgentTaskHistoryRow's header comment for the full mapping).
|
|
131
|
+
const agentRows = taskRows.filter((row) => isAgentTaskHistoryRow(row));
|
|
132
|
+
const agentFailures = agentRows.filter((row) => {
|
|
125
133
|
const detail = parseTaskMetadata(row).detail;
|
|
126
134
|
return typeof detail?.reason === "string" && detail.reason.length > 0;
|
|
127
135
|
});
|
|
128
136
|
const logBackingRate = taskRowsWithLogs.length === 0 ? 1 : existingLogRows.length / taskRowsWithLogs.length;
|
|
129
137
|
const taskFailRate = taskRows.length === 0 ? 0 : failedTaskRows.length / taskRows.length;
|
|
130
|
-
const agentFailureRate =
|
|
138
|
+
const agentFailureRate = agentRows.length === 0 ? 0 : agentFailures.length / agentRows.length;
|
|
131
139
|
return {
|
|
132
140
|
tableNames,
|
|
133
141
|
missingTables,
|
|
@@ -227,11 +235,12 @@ function gatherImproveSummaryPhase(db, stateDbPath, since, now) {
|
|
|
227
235
|
return { improveSummary, perRunSummaries };
|
|
228
236
|
}
|
|
229
237
|
/**
|
|
230
|
-
* The
|
|
231
|
-
* improve advisories, the `stash-git-exposure` probe,
|
|
232
|
-
*
|
|
233
|
-
* the returned array. A probe/filesystem
|
|
234
|
-
* abort the health report — each group
|
|
238
|
+
* The four best-effort advisory groups beyond the health-check registry:
|
|
239
|
+
* improve advisories, the `stash-git-exposure` probe, the 08 surfaces group
|
|
240
|
+
* (binary-config-skew, egress-endpoints), and `plugin-version` (itlackey/akm#832).
|
|
241
|
+
* Order matches emission order in the returned array. A probe/filesystem
|
|
242
|
+
* failure in any try/catch must not abort the health report — each group
|
|
243
|
+
* degrades to "no advisory" independently.
|
|
235
244
|
*/
|
|
236
245
|
function gatherAncillaryAdvisories(db, stateDbPath, since, improveSummary, options, egressConfigView) {
|
|
237
246
|
const advisories = [...collectImproveAdvisories(db, stateDbPath, since, improveSummary)];
|
|
@@ -269,8 +278,61 @@ function gatherAncillaryAdvisories(db, stateDbPath, since, improveSummary, optio
|
|
|
269
278
|
catch {
|
|
270
279
|
// Non-fatal.
|
|
271
280
|
}
|
|
281
|
+
// #831: flag indexed assets whose resolved type disagrees with the type
|
|
282
|
+
// their containing directory declares (see health/type-directory-check.ts).
|
|
283
|
+
// Best-effort — an unreadable index must not abort the health report.
|
|
284
|
+
try {
|
|
285
|
+
const typeDirMismatch = detectTypeDirectoryDisagreements(options.stashDir ?? resolveStashDir());
|
|
286
|
+
if (typeDirMismatch)
|
|
287
|
+
advisories.push(typeDirMismatch);
|
|
288
|
+
}
|
|
289
|
+
catch {
|
|
290
|
+
// Non-fatal.
|
|
291
|
+
}
|
|
292
|
+
// itlackey/akm#832: report installed Claude Code harness plugin version(s)
|
|
293
|
+
// and warn when stale or when the plugin's own akm-cli version range no
|
|
294
|
+
// longer admits this CLI. Best-effort — no plugin installed, an unreadable
|
|
295
|
+
// manifest, or a network failure while checking the newest tag must not
|
|
296
|
+
// abort the health report.
|
|
297
|
+
try {
|
|
298
|
+
advisories.push(...collectPluginStalenessAdvisories({ cliVersion: pkgVersion }));
|
|
299
|
+
}
|
|
300
|
+
catch {
|
|
301
|
+
// Non-fatal.
|
|
302
|
+
}
|
|
272
303
|
return advisories;
|
|
273
304
|
}
|
|
305
|
+
/**
|
|
306
|
+
* Open index.db read-only, project every entry to `{ filePath, type }`, and
|
|
307
|
+
* build the `type-directory-disagreement` advisory. `stashRoot` is used only
|
|
308
|
+
* to shorten displayed paths (relative to the stash) when it's an ancestor of
|
|
309
|
+
* the entry's path; falls back to the absolute path otherwise. Returns
|
|
310
|
+
* `undefined` when the index is absent/unreadable or nothing disagrees —
|
|
311
|
+
* mirrors {@link detectIndexStateGenerationMismatch}'s best-effort shape.
|
|
312
|
+
*/
|
|
313
|
+
function detectTypeDirectoryDisagreements(stashRoot) {
|
|
314
|
+
let indexDb;
|
|
315
|
+
try {
|
|
316
|
+
indexDb = openReadonlyExistingDatabase(getDbPath());
|
|
317
|
+
if (!indexDb)
|
|
318
|
+
return undefined;
|
|
319
|
+
const entries = getAllEntries(indexDb).map((entry) => ({ filePath: entry.filePath, type: entry.type }));
|
|
320
|
+
return buildTypeDirectoryAdvisory(entries, undefined, (absPath) => absPath.startsWith(stashRoot) ? path.relative(stashRoot, absPath) : absPath);
|
|
321
|
+
}
|
|
322
|
+
catch {
|
|
323
|
+
return undefined;
|
|
324
|
+
}
|
|
325
|
+
finally {
|
|
326
|
+
if (indexDb) {
|
|
327
|
+
try {
|
|
328
|
+
closeDatabase(indexDb);
|
|
329
|
+
}
|
|
330
|
+
catch {
|
|
331
|
+
// Best-effort advisory: a close failure must not abort health.
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
274
336
|
/**
|
|
275
337
|
* Detect the durable signature of an interrupted cross-database update.
|
|
276
338
|
*
|
|
@@ -377,7 +377,20 @@ function runPreLlmSessionGates(args) {
|
|
|
377
377
|
if (!force && shouldSkipAlreadyExtractedSession(prior, contentHash)) {
|
|
378
378
|
return { skip: alreadyExtractedResult(harness.name, sessionRef.sessionId, prior, contentHash) };
|
|
379
379
|
}
|
|
380
|
-
|
|
380
|
+
// #840 — harvest-without-prompting hybrid: the LLM prompt is built only from
|
|
381
|
+
// parent-origin events (folding stays as infrastructure for hashing above
|
|
382
|
+
// and inline-ref harvesting on `data.inlineRefs`, both of which still see
|
|
383
|
+
// the FULL folded stream). Subagent-origin events never reach
|
|
384
|
+
// `preFilterSession`, so #839's `dedupeTaskNotifications` naturally becomes
|
|
385
|
+
// a no-op on this path — a subagent's own event can no longer be in the
|
|
386
|
+
// kept set for a notification to be deduped against, leaving the parent's
|
|
387
|
+
// `<task-notification>` (the only surviving trace of that delegated work)
|
|
388
|
+
// untouched. See docs/plans/subagent-extraction-design.md §6.
|
|
389
|
+
const parentOriginData = {
|
|
390
|
+
...data,
|
|
391
|
+
events: data.events.filter((e) => e.filePath === data.ref.filePath),
|
|
392
|
+
};
|
|
393
|
+
const filtered = preFilterSession(parentOriginData, {
|
|
381
394
|
...(typeof maxTotalChars === "number" ? { maxTotalChars } : {}),
|
|
382
395
|
});
|
|
383
396
|
// #595/#596 — minContentChars gate: skip the LLM call for sessions whose RAW
|
|
@@ -388,6 +401,14 @@ function runPreLlmSessionGates(args) {
|
|
|
388
401
|
// fix gated on `filtered.stats.inputCount`, which is an EVENT count, not a
|
|
389
402
|
// char count — this port measures actual raw chars so the threshold matches
|
|
390
403
|
// the config key's documented unit.
|
|
404
|
+
// #840 — deliberately measured on the FULL folded `data.events` (parent +
|
|
405
|
+
// subagents), not the parent-origin view above: narrowing this to
|
|
406
|
+
// parent-origin chars would newly skip delegation-heavy sessions with a
|
|
407
|
+
// thin parent transcript before extraction runs at all, even though their
|
|
408
|
+
// subagent work is still fully harvested via `data.inlineRefs` above. The
|
|
409
|
+
// full-stream measurement is today's unchanged behavior, so the worst case
|
|
410
|
+
// this preserves is an LLM call over a small parent-only prompt, not a
|
|
411
|
+
// missed extraction.
|
|
391
412
|
const rawContentChars = data.events.reduce((sum, event) => sum + event.text.length, 0);
|
|
392
413
|
if (minContentChars > 0 && rawContentChars < minContentChars) {
|
|
393
414
|
return {
|
|
@@ -18,7 +18,7 @@ import { warn } from "../../core/warn.js";
|
|
|
18
18
|
import { resolveSourceEntries } from "../../indexer/search/search-source.js";
|
|
19
19
|
import { TASK_EXTENSION, TASK_NEAR_MISS_EXTENSION, taskExtensionDetail } from "../../tasks/source-v3.js";
|
|
20
20
|
import { resolveWorkflowSourceDomains } from "../../workflows/source-files.js";
|
|
21
|
-
import { compareWorkflowSourceCodePoints } from "../../workflows/source-ir/
|
|
21
|
+
import { compareWorkflowSourceCodePoints } from "../../workflows/source-ir/compare.js";
|
|
22
22
|
import { runBaseChecks } from "./base-linter.js";
|
|
23
23
|
import { checkEnvForDangerousKeys } from "./env-key-rules.js";
|
|
24
24
|
import { isAdvisoryLintIssue } from "./types.js";
|
|
@@ -1,44 +1,149 @@
|
|
|
1
1
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
2
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
3
|
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
-
import { defineGroupCommand, defineJsonCommand, output } from "../cli/shared.js";
|
|
4
|
+
import { defineGroupCommand, defineJsonCommand, EXIT_CODES, output } from "../cli/shared.js";
|
|
5
5
|
import { runMigrationTool } from "./migration-tool.js";
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
* output pipeline.
|
|
9
|
-
*/
|
|
10
|
-
async function runMigrateSubcommand(command, args) {
|
|
11
|
-
const result = await runMigrationTool(args);
|
|
6
|
+
async function callMigrateTool(args, runTool) {
|
|
7
|
+
const result = await runTool(args);
|
|
12
8
|
if (result.stderr)
|
|
13
9
|
process.stderr.write(result.stderr);
|
|
14
10
|
const resultLine = result.stdout.trim();
|
|
15
|
-
if (resultLine)
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
11
|
+
if (!resultLine)
|
|
12
|
+
return { status: result.status };
|
|
13
|
+
try {
|
|
14
|
+
return { status: result.status, plan: JSON.parse(resultLine) };
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
console.log(resultLine);
|
|
18
|
+
return { status: result.status };
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function worstStatus(left, right) {
|
|
22
|
+
if (left === "blocked" || right === "blocked")
|
|
23
|
+
return "blocked";
|
|
24
|
+
if (left === "ready" || right === "ready")
|
|
25
|
+
return "ready";
|
|
26
|
+
return "current";
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Resolve one generation's contribution to the combined status — fail
|
|
30
|
+
* CLOSED, never open (code-review finding: this tool advertises itself as
|
|
31
|
+
* "blocked-not-guessed").
|
|
32
|
+
*
|
|
33
|
+
* A generation that exited SUCCESS with no plan on stdout legitimately means
|
|
34
|
+
* "nothing to report" and defaults to `"current"`. A generation that exited
|
|
35
|
+
* NON-SUCCESS (by the caller's own guard, this can only be `EXIT_CODES.
|
|
36
|
+
* GENERAL` — the "blocked" code) with a parsed `plan.status` reports that
|
|
37
|
+
* status verbatim, same as before.
|
|
38
|
+
*
|
|
39
|
+
* The gap this closes: NON-SUCCESS with NO parseable plan at all —
|
|
40
|
+
* `runMigrationTool` coerces a `spawnSync` `status` of `null` (the child was
|
|
41
|
+
* killed by a signal — OOM, a timeout, a manual kill — never scheduled to
|
|
42
|
+
* exit) to `1`, indistinguishable from the migrator's own legitimate
|
|
43
|
+
* "blocked" exit code, and truncated/malformed stdout hits the same
|
|
44
|
+
* `JSON.parse` catch in `callMigrateTool`. Previously `?? "current"` silently
|
|
45
|
+
* read a crashed generation as "nothing to migrate"; this reports it as
|
|
46
|
+
* `"blocked"` with an explanatory blocker instead, so the combined exit code
|
|
47
|
+
* (`EXIT_CODES.GENERAL` below) actually reflects that the generation's real
|
|
48
|
+
* state is unknown, rather than reporting success at exit 0.
|
|
49
|
+
*/
|
|
50
|
+
export function resolveGenerationStatus(call, label) {
|
|
51
|
+
const planStatus = call.plan?.status;
|
|
52
|
+
if (planStatus !== undefined)
|
|
53
|
+
return { status: planStatus };
|
|
54
|
+
if (call.status !== EXIT_CODES.SUCCESS) {
|
|
55
|
+
return {
|
|
56
|
+
status: "blocked",
|
|
57
|
+
error: `${label}: the child process exited without printing a plan (exit status ${call.status}) — its real migration state is unknown.`,
|
|
58
|
+
};
|
|
22
59
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
60
|
+
return { status: "current" };
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Run BOTH migration generations — task-v2-to-v3, then task-v3-to-task-
|
|
64
|
+
* source-v4 — and print one combined plan (spec
|
|
65
|
+
* docs/plans/specs/p4-deletions-closeout.md §3.2.5, rows B-31/B-32).
|
|
66
|
+
*
|
|
67
|
+
* Each generation is its OWN subprocess call into the standalone migrator
|
|
68
|
+
* (`scripts/akm-migrate.ts`'s `status`/`apply` and `task-v4-status`/
|
|
69
|
+
* `task-v4-apply` verbs, UNCHANGED — row B-33), so each keeps its own
|
|
70
|
+
* `withConfigLock` + `O_EXCL` backup root + prevalidate + TOCTOU recheck +
|
|
71
|
+
* atomic replace + reverse rollback + convergence check, and the two are
|
|
72
|
+
* NEVER interleaved. The two calls are unconditional and independent of
|
|
73
|
+
* each other's outcome: a blocked (or otherwise incomplete) generation-1
|
|
74
|
+
* result does not stop generation 2 from running against whatever is
|
|
75
|
+
* already task source v4 — exactly `akm-migrate status`/`task-v4-status`
|
|
76
|
+
* (or `apply`/`task-v4-apply`) run back to back by hand. Only a genuine
|
|
77
|
+
* hard failure (a status neither SUCCESS nor the "blocked" GENERAL code —
|
|
78
|
+
* a config error, a crash) aborts the second call, since generation 1 never
|
|
79
|
+
* got to look at a stable tree in that case.
|
|
80
|
+
*/
|
|
81
|
+
export async function runMigrateSubcommand(command, genOneArgs, genTwoArgs, runTool = runMigrationTool) {
|
|
82
|
+
const first = await callMigrateTool(genOneArgs, runTool);
|
|
83
|
+
if (first.status !== EXIT_CODES.SUCCESS && first.status !== EXIT_CODES.GENERAL) {
|
|
84
|
+
process.exitCode = first.status;
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const second = await callMigrateTool(genTwoArgs, runTool);
|
|
88
|
+
if (second.status !== EXIT_CODES.SUCCESS && second.status !== EXIT_CODES.GENERAL) {
|
|
89
|
+
process.exitCode = second.status;
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (!first.plan && !second.plan) {
|
|
93
|
+
if (first.status !== EXIT_CODES.SUCCESS)
|
|
94
|
+
process.exitCode = first.status;
|
|
28
95
|
return;
|
|
29
96
|
}
|
|
97
|
+
const combined = combineMigrationPlans(first, second);
|
|
98
|
+
output(command, combined);
|
|
99
|
+
if (combined.status === "blocked")
|
|
100
|
+
process.exitCode = EXIT_CODES.GENERAL;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Merge both generations' plans into the one combined envelope the command
|
|
104
|
+
* prints. Deliberately PURE — every rule the combined plan encodes (the
|
|
105
|
+
* {@link worstStatus} rollup, the fail-closed
|
|
106
|
+
* {@link resolveGenerationStatus} contribution, and the blockers merge, which
|
|
107
|
+
* orders generation 1's own blockers after its resolution error and before
|
|
108
|
+
* generation 2's) is decided here from two plain values, so it is provable
|
|
109
|
+
* without a subprocess, a CLI dispatch, or an output-mode singleton. The
|
|
110
|
+
* caller keeps the only two effectful decisions: whether generation 2 runs at
|
|
111
|
+
* all, and the process exit code.
|
|
112
|
+
*/
|
|
113
|
+
export function combineMigrationPlans(first, second) {
|
|
114
|
+
const firstResolved = resolveGenerationStatus(first, "task-v2-to-v3");
|
|
115
|
+
const secondResolved = resolveGenerationStatus(second, "task-v3-to-task-source-v4");
|
|
116
|
+
return {
|
|
117
|
+
schemaVersion: 1,
|
|
118
|
+
status: worstStatus(firstResolved.status, secondResolved.status),
|
|
119
|
+
blockers: [
|
|
120
|
+
...(firstResolved.error ? [firstResolved.error] : []),
|
|
121
|
+
...(first.plan?.blockers ?? []),
|
|
122
|
+
...(secondResolved.error ? [secondResolved.error] : []),
|
|
123
|
+
...(second.plan?.blockers ?? []),
|
|
124
|
+
],
|
|
125
|
+
taskV3Migration: first.plan?.taskV3Migration,
|
|
126
|
+
taskV4Migration: second.plan?.taskV4Migration,
|
|
127
|
+
...(first.plan?.backupPath !== undefined ? { backupPath: first.plan.backupPath } : {}),
|
|
128
|
+
...(first.plan?.applied !== undefined ? { applied: first.plan.applied } : {}),
|
|
129
|
+
...(second.plan?.backupPath !== undefined ? { taskV4BackupPath: second.plan.backupPath } : {}),
|
|
130
|
+
...(second.plan?.applied !== undefined ? { taskV4Applied: second.plan.applied } : {}),
|
|
131
|
+
};
|
|
30
132
|
}
|
|
31
133
|
export const migrateCommand = defineGroupCommand({
|
|
32
|
-
meta: { name: "migrate", description: "Inspect or apply task-v2
|
|
134
|
+
meta: { name: "migrate", description: "Inspect or apply task-v2 and task-v3 sources to task source v4" },
|
|
33
135
|
subCommands: {
|
|
34
136
|
status: defineJsonCommand({
|
|
35
|
-
meta: { name: "status", description: "Read-only task-v2 migration check" },
|
|
137
|
+
meta: { name: "status", description: "Read-only task-v2 and task-v3 migration check" },
|
|
36
138
|
run() {
|
|
37
|
-
return runMigrateSubcommand("migrate-status", ["status"]);
|
|
139
|
+
return runMigrateSubcommand("migrate-status", ["status"], ["task-v4-status"]);
|
|
38
140
|
},
|
|
39
141
|
}),
|
|
40
142
|
apply: defineJsonCommand({
|
|
41
|
-
meta: {
|
|
143
|
+
meta: {
|
|
144
|
+
name: "apply",
|
|
145
|
+
description: "Back up and atomically convert task-v2 and task-v3 files to task source v4",
|
|
146
|
+
},
|
|
42
147
|
args: {
|
|
43
148
|
"dry-run": {
|
|
44
149
|
type: "boolean",
|
|
@@ -47,7 +152,8 @@ export const migrateCommand = defineGroupCommand({
|
|
|
47
152
|
},
|
|
48
153
|
},
|
|
49
154
|
run({ args }) {
|
|
50
|
-
|
|
155
|
+
const dryRunFlag = args.dryRun ? ["--dry-run"] : [];
|
|
156
|
+
return runMigrateSubcommand("migrate-apply", ["apply", ...dryRunFlag], ["task-v4-apply", ...dryRunFlag]);
|
|
51
157
|
},
|
|
52
158
|
}),
|
|
53
159
|
},
|
|
@@ -5,7 +5,7 @@ import { parseFrontmatter } from "../../../core/asset/frontmatter.js";
|
|
|
5
5
|
import { parseRefInput } from "../../../core/asset/resolve-ref.js";
|
|
6
6
|
import { proposalContent } from "../../../core/file-change.js";
|
|
7
7
|
import { lintLessonContent } from "../../../core/lesson-lint.js";
|
|
8
|
-
import {
|
|
8
|
+
import { parseTaskSource } from "../../../tasks/source/parse-task-source.js";
|
|
9
9
|
import { compileWorkflowSource } from "../../../workflows/source-ir/compile.js";
|
|
10
10
|
import { defaultProposalQualityValidators } from "./proposal-quality-validators.js";
|
|
11
11
|
const genericProposalValidator = {
|
|
@@ -52,7 +52,12 @@ const canonicalProposalValidators = {
|
|
|
52
52
|
const name = ctx.parsedRef?.name;
|
|
53
53
|
if (!name)
|
|
54
54
|
return [];
|
|
55
|
-
|
|
55
|
+
// Version-routing seam (spec docs/plans/specs/p2a-task-source-v4.md
|
|
56
|
+
// §3.6): a proposal body is validated by parsing alone — neither arm's
|
|
57
|
+
// parsed document is inspected further, so routing through the union is
|
|
58
|
+
// a pure swap; any parse failure (either version) is turned into an
|
|
59
|
+
// `invalid-task-structure` finding by the try/catch this call sits in.
|
|
60
|
+
parseTaskSource({
|
|
56
61
|
yaml: proposalContent(proposal),
|
|
57
62
|
filePath: proposal.changes[0]?.path || proposal.ref,
|
|
58
63
|
});
|
|
@@ -7,7 +7,7 @@ import { defineJsonCommand, output, parseAllFlagValues } from "../../cli/shared.
|
|
|
7
7
|
import { UsageError } from "../../core/errors.js";
|
|
8
8
|
import { appendEvent } from "../../core/events.js";
|
|
9
9
|
import { resolveUsageEventSource } from "../../indexer/usage/usage-events.js";
|
|
10
|
-
import { buildMemoryFrontmatter, parseDuration, readMemoryContent, runAutoHeuristics, runLlmEnrich } from "../remember.js";
|
|
10
|
+
import { buildMemoryFrontmatter, parseDuration, readMemoryContent, runAutoHeuristics, runLlmEnrich, synthesizeMemoryDescription, } from "../remember.js";
|
|
11
11
|
import { assertFlatAssetName, inferAssetName, resolveSupersedesForWrite, resolveSupersedesWriteTarget, resolveXrefsForWrite, writeMarkdownAsset, } from "./knowledge.js";
|
|
12
12
|
import { akmSearch } from "./search.js";
|
|
13
13
|
// ── Helper: similar memory search ────────────────────────────────────────────
|
|
@@ -189,7 +189,9 @@ export const rememberCommand = defineJsonCommand({
|
|
|
189
189
|
// Phase 1B / Rec 7: even the zero-flag hot-path emits
|
|
190
190
|
// `captureMode: hot` + `beliefState: asserted` so user-supplied
|
|
191
191
|
// memories outrank background-derived ones during ranking.
|
|
192
|
+
// #834: `description` is synthesized deterministically (see synthesizeMemoryDescription) so the memory is indexable.
|
|
192
193
|
const frontmatterBlock = buildMemoryFrontmatter({
|
|
194
|
+
description: synthesizeMemoryDescription(body),
|
|
193
195
|
captureMode: "hot",
|
|
194
196
|
beliefState: "asserted",
|
|
195
197
|
});
|
|
@@ -264,6 +266,9 @@ export const rememberCommand = defineJsonCommand({
|
|
|
264
266
|
observed_at = enriched.observed_at;
|
|
265
267
|
executionNotices = enriched.notices;
|
|
266
268
|
}
|
|
269
|
+
// #834: no --description and no --enrich-derived one — synthesize deterministically (see zero-flag path above).
|
|
270
|
+
if (!description)
|
|
271
|
+
description = synthesizeMemoryDescription(body);
|
|
267
272
|
// ── Required-field check (before any write) ───────────────────────────
|
|
268
273
|
// Tags remain required when the user explicitly asked for tag-bearing
|
|
269
274
|
// metadata (--tag / --enrich / --description / --source / --expires).
|