akm-cli 0.9.17-alpha.6 → 0.9.17-alpha.7
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 +146 -0
- package/STABILITY.md +2 -2
- package/dist/akm +7 -7
- package/dist/assets/prompts/retrieval-relevance-judge.md +6 -0
- package/dist/commands/improve/consolidate.js +11 -0
- package/dist/commands/improve/improve-cli.js +27 -7
- package/dist/commands/improve/ledger.js +7 -3
- package/dist/commands/improve/preparation.js +40 -10
- package/dist/commands/improve/reflect.js +46 -22
- package/dist/commands/improve/retrieval-gate.js +127 -0
- package/dist/commands/improve/retrieval-scope.js +77 -0
- package/dist/commands/read/curate.js +1 -17
- package/dist/commands/tasks/tasks-cli.js +10 -12
- package/dist/commands/tasks/tasks.js +57 -56
- package/dist/commands/tasks/validate.js +27 -46
- package/dist/core/adapter/adapters/akm-task-adapter.js +29 -8
- package/dist/core/improve-result.js +4 -1
- package/dist/core/non-task-input.js +20 -0
- package/dist/core/paths.js +0 -4
- package/dist/indexer/indexer.js +1 -3
- package/dist/indexer/usage/usage-events.js +34 -0
- package/dist/scripts/akm-migrate-node.js +5822 -5833
- package/dist/scripts/akm-migrate.js +6301 -6312
- package/dist/storage/repositories/proposals-repository.js +4 -0
- package/dist/tasks/backends/cron.js +80 -43
- package/dist/tasks/backends/launchd.js +28 -15
- package/dist/tasks/backends/schtasks.js +25 -10
- package/dist/tasks/run/load-task.js +1 -1
- package/dist/tasks/scheduler-binding.js +4 -2
- package/dist/tasks/scheduler-invocation.js +127 -235
- package/dist/tasks/scheduler-sync.js +13 -8
- package/dist/tasks/source/parse-task-source.js +22 -126
- package/dist/tasks/source/task-to-v3.js +1 -55
- package/dist/tasks/source/task-to-v4.js +1 -13
- package/docs/migration/v0.9.1-to-v0.9.2.md +7 -3
- package/docs/reference/cli.md +4 -3
- package/docs/reference/tasks.md +58 -40
- package/package.json +1 -1
|
@@ -0,0 +1,127 @@
|
|
|
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
|
+
* The retrieval regression gate (#722): a reflect rewrite of an existing asset
|
|
6
|
+
* must not grade lower on the queries that actually retrieved it.
|
|
7
|
+
*
|
|
8
|
+
* Measured before it was built: of 60 accepted reflect rewrites judged against
|
|
9
|
+
* their own queries, 14 graded lower (23%, 95% CI 14–35%) and 12 higher. The
|
|
10
|
+
* old and new content are graded one query at a time, blind, with the
|
|
11
|
+
* retrieval-eval judge's prompt (kappa 0.83 against human grades) and its
|
|
12
|
+
* document shape: type, ref, name, description and the first 1,500 characters
|
|
13
|
+
* of the body.
|
|
14
|
+
*/
|
|
15
|
+
import relevanceJudgePrompt from "../../assets/prompts/retrieval-relevance-judge.md" with { type: "text" };
|
|
16
|
+
import { parseFrontmatter } from "../../core/asset/frontmatter.js";
|
|
17
|
+
import { parseRefInput } from "../../core/asset/resolve-ref.js";
|
|
18
|
+
import { nonTaskInput } from "../../core/non-task-input.js";
|
|
19
|
+
import { parseEmbeddedJsonResponse } from "../../core/parse.js";
|
|
20
|
+
import { listRetrievalQueries } from "../../indexer/usage/usage-events.js";
|
|
21
|
+
import { readLedgerDb, stripBundle } from "./ledger.js";
|
|
22
|
+
import { callStage } from "./stage.js";
|
|
23
|
+
/** Queries graded per rewrite, as measured. */
|
|
24
|
+
const MAX_QUERIES = 5;
|
|
25
|
+
/** The retrieval suite treats a longer input as a paste, not a query; the measurement did the same. */
|
|
26
|
+
const MAX_QUERY_CHARS = 2000;
|
|
27
|
+
/** The judge sees this much of the body, as in the retrieval eval. */
|
|
28
|
+
const MAX_DOC_CHARS = 1500;
|
|
29
|
+
const GRADE_SCHEMA = {
|
|
30
|
+
type: "object",
|
|
31
|
+
required: ["grade", "reason"],
|
|
32
|
+
additionalProperties: false,
|
|
33
|
+
properties: { grade: { type: "integer", minimum: 0, maximum: 3 }, reason: { type: "string" } },
|
|
34
|
+
};
|
|
35
|
+
/** Up to five distinct task queries, in the given order, whitespace collapsed. */
|
|
36
|
+
export function usableRetrievalQueries(raw) {
|
|
37
|
+
const out = [];
|
|
38
|
+
for (const text of raw) {
|
|
39
|
+
const query = text.replace(/\s+/g, " ").trim();
|
|
40
|
+
if (!query || query.length > MAX_QUERY_CHARS || nonTaskInput(query) || out.includes(query))
|
|
41
|
+
continue;
|
|
42
|
+
out.push(query);
|
|
43
|
+
if (out.length === MAX_QUERIES)
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
48
|
+
/** The asset's own retrieval queries from the usage log (none when state.db cannot be read). */
|
|
49
|
+
export function loadRetrievalQueries(access, ref) {
|
|
50
|
+
try {
|
|
51
|
+
return usableRetrievalQueries(readLedgerDb(access, (db) => listRetrievalQueries(db, stripBundle(ref))) ?? []);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function judgeDocument(ref, raw) {
|
|
58
|
+
const conceptId = stripBundle(ref);
|
|
59
|
+
const parsed = parseRefInput(conceptId);
|
|
60
|
+
const { data, content } = parseFrontmatter(raw);
|
|
61
|
+
const text = content
|
|
62
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
63
|
+
.trim()
|
|
64
|
+
.slice(0, MAX_DOC_CHARS);
|
|
65
|
+
const name = typeof data.name === "string" && data.name ? data.name : parsed.name;
|
|
66
|
+
const description = typeof data.description === "string" ? data.description : "";
|
|
67
|
+
return [
|
|
68
|
+
"Candidate asset:",
|
|
69
|
+
`Type: ${parsed.type}`,
|
|
70
|
+
`Ref: ${conceptId}`,
|
|
71
|
+
`Name: ${name}`,
|
|
72
|
+
`Description: ${description}`,
|
|
73
|
+
"",
|
|
74
|
+
"Content:",
|
|
75
|
+
text,
|
|
76
|
+
].join("\n");
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Grade `before` and `after` on each query; refuse the rewrite when the new
|
|
80
|
+
* content's mean grade is lower. Fails closed, like the quality judge: a grade
|
|
81
|
+
* that cannot be obtained refuses the rewrite.
|
|
82
|
+
*/
|
|
83
|
+
export async function runRetrievalRegressionGate(args) {
|
|
84
|
+
if (args.queries.length === 0)
|
|
85
|
+
return { pass: true, queries: 0, reason: "no retrieval queries to compare on" };
|
|
86
|
+
const documents = { old: judgeDocument(args.ref, args.before), new: judgeDocument(args.ref, args.after) };
|
|
87
|
+
const totals = { old: 0, new: 0 };
|
|
88
|
+
for (const query of args.queries) {
|
|
89
|
+
for (const version of ["old", "new"]) {
|
|
90
|
+
const outcome = await callStage({
|
|
91
|
+
feature: "proposal_quality_gate",
|
|
92
|
+
runner: args.runner,
|
|
93
|
+
system: relevanceJudgePrompt.trim(),
|
|
94
|
+
prompt: `Query: ${query}\n\n${documents[version]}`,
|
|
95
|
+
request: {
|
|
96
|
+
enableThinking: false,
|
|
97
|
+
temperature: 0,
|
|
98
|
+
responseSchema: GRADE_SCHEMA,
|
|
99
|
+
...(args.timeoutMs !== undefined ? { timeoutMs: args.timeoutMs } : {}),
|
|
100
|
+
...(args.signal ? { signal: args.signal } : {}),
|
|
101
|
+
...(args.chat ? { chat: args.chat } : {}),
|
|
102
|
+
},
|
|
103
|
+
...(args.onNotices ? { onNotices: args.onNotices } : {}),
|
|
104
|
+
});
|
|
105
|
+
const grade = outcome.ok ? parseEmbeddedJsonResponse(outcome.raw)?.grade : undefined;
|
|
106
|
+
if (typeof grade !== "number" || !Number.isInteger(grade) || grade < 0 || grade > 3) {
|
|
107
|
+
return {
|
|
108
|
+
pass: false,
|
|
109
|
+
queries: args.queries.length,
|
|
110
|
+
reason: `retrieval check could not grade the ${version} content${outcome.ok ? "" : ` (${outcome.reason})`}`,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
totals[version] += grade;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const oldMean = totals.old / args.queries.length;
|
|
117
|
+
const newMean = totals.new / args.queries.length;
|
|
118
|
+
const pass = newMean >= oldMean;
|
|
119
|
+
const grades = `${oldMean.toFixed(2)} -> ${newMean.toFixed(2)} over ${args.queries.length} retrieval ${args.queries.length === 1 ? "query" : "queries"}`;
|
|
120
|
+
return {
|
|
121
|
+
pass,
|
|
122
|
+
queries: args.queries.length,
|
|
123
|
+
oldMean,
|
|
124
|
+
newMean,
|
|
125
|
+
reason: pass ? `retrieval grade ${grades}` : `retrieval regression: grade ${grades}`,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
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
|
+
* What improve may rework (#986): an asset that retrieval returned inside the
|
|
6
|
+
* usage window, or newly captured material no improve stage has processed.
|
|
7
|
+
*
|
|
8
|
+
* Fresh feedback and an explicit `--scope <ref>` are usage evidence of their
|
|
9
|
+
* own, so the signal-delta and scope lanes need no check. The fallback lanes
|
|
10
|
+
* (proactive maintenance, high salience, forgetting safety) and consolidation
|
|
11
|
+
* pick assets without such evidence, so they pick only inside this scope.
|
|
12
|
+
*/
|
|
13
|
+
import fs from "node:fs";
|
|
14
|
+
import { daysToMs } from "../../core/common.js";
|
|
15
|
+
import { warn } from "../../core/warn.js";
|
|
16
|
+
import { listUsedEntryRefs, USAGE_EVENT_RETENTION_DAYS } from "../../indexer/usage/usage-events.js";
|
|
17
|
+
import { listImproveLedgerRows } from "../../storage/repositories/improve-ledger-repository.js";
|
|
18
|
+
import { listProposalRefSources } from "../../storage/repositories/proposals-repository.js";
|
|
19
|
+
import { readLedgerDb, stripBundle } from "./ledger.js";
|
|
20
|
+
/**
|
|
21
|
+
* Ledger and proposal sources that bring material in. Every other source is an
|
|
22
|
+
* improve stage reworking an asset (reflect, distill, consolidate, schema repair).
|
|
23
|
+
*/
|
|
24
|
+
const CAPTURE_SOURCES = new Set(["extract", "propose", "remember", "import"]);
|
|
25
|
+
/**
|
|
26
|
+
* Load the scope from state.db. The window is the usage log's retention: the
|
|
27
|
+
* log keeps nothing older, and anything shorter would drop assets read less
|
|
28
|
+
* often than the window. A `.derived` hit counts for its parent memory, whose
|
|
29
|
+
* facts it carries. `undefined` (every asset eligible, as before #986) only
|
|
30
|
+
* when state.db cannot be read.
|
|
31
|
+
*/
|
|
32
|
+
export function loadRetrievalScope(access, stashDir) {
|
|
33
|
+
const sinceMs = Date.now() - daysToMs(USAGE_EVENT_RETENTION_DAYS);
|
|
34
|
+
const used = new Set();
|
|
35
|
+
const processed = new Set();
|
|
36
|
+
try {
|
|
37
|
+
readLedgerDb(access, (db) => {
|
|
38
|
+
for (const entryRef of listUsedEntryRefs(db, new Date(sinceMs).toISOString())) {
|
|
39
|
+
const conceptId = stripBundle(entryRef);
|
|
40
|
+
used.add(conceptId);
|
|
41
|
+
if (conceptId.endsWith(".derived"))
|
|
42
|
+
used.add(conceptId.slice(0, -".derived".length));
|
|
43
|
+
}
|
|
44
|
+
if (!stashDir)
|
|
45
|
+
return;
|
|
46
|
+
for (const row of [...listImproveLedgerRows(db, stashDir), ...listProposalRefSources(db, stashDir)]) {
|
|
47
|
+
if (!CAPTURE_SOURCES.has(row.source))
|
|
48
|
+
processed.add(stripBundle(row.ref));
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
warn(`[improve] usage history unreadable, so every asset stays eligible: ${error instanceof Error ? error.message : String(error)}`);
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
return { used, processed, sinceMs };
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Whether improve may rework `ref` (whose file is `filePath`) under `scope`. An
|
|
60
|
+
* unprocessed asset whose file cannot be read is left to the disk check that
|
|
61
|
+
* follows, which reports it as missing.
|
|
62
|
+
*/
|
|
63
|
+
export function isInRetrievalScope(scope, ref, filePath) {
|
|
64
|
+
if (!scope)
|
|
65
|
+
return true;
|
|
66
|
+
const conceptId = stripBundle(ref);
|
|
67
|
+
if (scope.used.has(conceptId))
|
|
68
|
+
return true;
|
|
69
|
+
if (scope.processed.has(conceptId))
|
|
70
|
+
return false;
|
|
71
|
+
try {
|
|
72
|
+
return filePath === undefined || fs.statSync(filePath).mtimeMs >= scope.sinceMs;
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
import { loadConfig } from "../../core/config/config.js";
|
|
19
19
|
import { rethrowIfTestIsolationError, UsageError } from "../../core/errors.js";
|
|
20
20
|
import { appendEvent } from "../../core/events.js";
|
|
21
|
+
import { nonTaskInput } from "../../core/non-task-input.js";
|
|
21
22
|
import { redactCredentialPatterns } from "../../core/redaction.js";
|
|
22
23
|
import { withStateDbTelemetry } from "../../core/state-db.js";
|
|
23
24
|
import { searchHitContent } from "../../indexer/search/db-search.js";
|
|
@@ -33,8 +34,6 @@ import { akmSearch, parseSearchSource } from "./search.js";
|
|
|
33
34
|
import { akmShowUnified } from "./show.js";
|
|
34
35
|
const DEFAULT_CURATE_LIMIT = 4;
|
|
35
36
|
const MAX_CURATE_SUPPORT_REFS = 2;
|
|
36
|
-
/** The line of `src/assets/stash-skeleton/README.md` that reaches curate verbatim as a query. */
|
|
37
|
-
const STASH_README_LINE = "This is an **AKM stash** — a structured knowledge repository that stores reusable";
|
|
38
37
|
/** Fused candidates the reranker reorders when `search.curateRerank.topN` is unset. */
|
|
39
38
|
const DEFAULT_CURATE_RERANK_TOP_N = 30;
|
|
40
39
|
/** Characters of name, description and content sent to the reranker per candidate. */
|
|
@@ -131,21 +130,6 @@ export async function akmCurate(options) {
|
|
|
131
130
|
}
|
|
132
131
|
return result;
|
|
133
132
|
}
|
|
134
|
-
/**
|
|
135
|
-
* What the (trimmed) curate input is when it is not a task, else undefined.
|
|
136
|
-
* Harness and tool envelopes (`<task-notification>…`, `<system-reminder>…`,
|
|
137
|
-
* `<cross-session-message …>…`) start with a tag and close one, and the stash
|
|
138
|
-
* README line arrives verbatim; on the retrieval suite neither shape occurs in
|
|
139
|
-
* a real query. Length is not a signal: prompts over 2,000 characters found
|
|
140
|
-
* relevant assets at about the rate of shorter long prompts.
|
|
141
|
-
*/
|
|
142
|
-
function nonTaskInput(query) {
|
|
143
|
-
if (query.startsWith("<") && query.includes("</"))
|
|
144
|
-
return "a harness or tool envelope";
|
|
145
|
-
if (query === STASH_README_LINE)
|
|
146
|
-
return "the akm stash README boilerplate";
|
|
147
|
-
return undefined;
|
|
148
|
-
}
|
|
149
133
|
export async function curateSearchResults(query, result, limit, selectedType, eventSource) {
|
|
150
134
|
const allStashHits = result.hits.filter((hit) => hit.type !== "registry");
|
|
151
135
|
const registryHits = result.registryHits ?? [];
|
|
@@ -435,17 +435,15 @@ const tasksDoctorCommand = defineJsonCommand({
|
|
|
435
435
|
},
|
|
436
436
|
});
|
|
437
437
|
/**
|
|
438
|
-
* #907: `akm task validate`'s exit-code contract — `valid`
|
|
439
|
-
*
|
|
440
|
-
*
|
|
441
|
-
*
|
|
442
|
-
*
|
|
443
|
-
*
|
|
444
|
-
* throws a `UsageError` for those, which `defineJsonCommand`'s wrapping
|
|
445
|
-
* already maps to exit 2.
|
|
438
|
+
* #907: `akm task validate`'s exit-code contract — `valid` succeeds (exit
|
|
439
|
+
* 0); `blocked`/`invalid`/`not-a-task` are diagnosed defects the caller must
|
|
440
|
+
* act on (exit 1, mirroring `task sync`'s own `failures.length > 0 ->
|
|
441
|
+
* EXIT_CODES.GENERAL`). A missing path or an unreadable file never reaches
|
|
442
|
+
* this function at all — `akmTaskValidate` throws a `UsageError` for those,
|
|
443
|
+
* which `defineJsonCommand`'s wrapping already maps to exit 2.
|
|
446
444
|
*/
|
|
447
445
|
export function taskValidateExitCode(result) {
|
|
448
|
-
return result.outcome === "valid"
|
|
446
|
+
return result.outcome === "valid" ? undefined : EXIT_CODES.GENERAL;
|
|
449
447
|
}
|
|
450
448
|
const tasksValidateCommand = defineJsonCommand({
|
|
451
449
|
meta: {
|
|
@@ -478,9 +476,9 @@ export function taskPruneExitCode(result) {
|
|
|
478
476
|
const tasksPruneCommand = defineJsonCommand({
|
|
479
477
|
meta: {
|
|
480
478
|
name: "prune",
|
|
481
|
-
description: "Remove installed scheduler entries `sync` can never reclaim because
|
|
482
|
-
"
|
|
483
|
-
"
|
|
479
|
+
description: "Remove installed scheduler entries `sync` can never reclaim because they no longer resolve to a live " +
|
|
480
|
+
"bundle (the bundle directory a row names is gone, or an older row's --scheduler-context descriptor " +
|
|
481
|
+
"cannot be read). Defaults to a dry-run preview — zero scheduler writes. Requires --yes to remove anything; " +
|
|
484
482
|
"--id narrows removal to specific binding ids (comma-separated).",
|
|
485
483
|
},
|
|
486
484
|
args: {
|
|
@@ -41,7 +41,7 @@ import { readTaskHistory } from "../../tasks/run/task-history.js";
|
|
|
41
41
|
import { exitCodeForStatus } from "../../tasks/run/task-result.js";
|
|
42
42
|
import { parseSchedule, SCHEDULE_SUPPORTED_SUBSET_HINT } from "../../tasks/schedule.js";
|
|
43
43
|
import { compileTaskSchedulerBindings, schedulerBindingNativeId, } from "../../tasks/scheduler-binding.js";
|
|
44
|
-
import {
|
|
44
|
+
import { readLegacySchedulerContext, scheduledRowEnvironment } from "../../tasks/scheduler-invocation.js";
|
|
45
45
|
import { withSchedulerLock } from "../../tasks/scheduler-lock.js";
|
|
46
46
|
import { compileSchedulerSources, installedRowNativeId, installedRowOwner, installedRowScope, planSchedulerSync, renderSchedulerPlanPreview, scheduledInvocationBundle, } from "../../tasks/scheduler-sync.js";
|
|
47
47
|
import { parseTaskSource } from "../../tasks/source/parse-task-source.js";
|
|
@@ -301,9 +301,7 @@ export async function akmTasksHistory(input) {
|
|
|
301
301
|
*/
|
|
302
302
|
export async function akmTasksSync(deps = {}, bundleTarget, options = {}) {
|
|
303
303
|
return withSchedulerLock(async () => {
|
|
304
|
-
const { sched, plan,
|
|
305
|
-
if (publish && plan.operations.some((operation) => operation.kind !== "remove"))
|
|
306
|
-
publish();
|
|
304
|
+
const { sched, plan, warnings } = await buildSchedulerSyncPlan(deps, bundleTarget, options);
|
|
307
305
|
const failed = new Set();
|
|
308
306
|
const failures = [...plan.failures];
|
|
309
307
|
for (const operation of plan.operations) {
|
|
@@ -343,7 +341,7 @@ export async function akmTasksSync(deps = {}, bundleTarget, options = {}) {
|
|
|
343
341
|
}
|
|
344
342
|
/**
|
|
345
343
|
* `akm task sync --dry-run` (#849): the exact plan `akmTasksSync` would
|
|
346
|
-
* apply, previewed. It writes nothing: no config, no
|
|
344
|
+
* apply, previewed. It writes nothing: no config, no row.
|
|
347
345
|
*/
|
|
348
346
|
export async function akmTasksSyncPlan(deps = {}, bundleTarget, options = {}) {
|
|
349
347
|
const { sched, plan } = await buildSchedulerSyncPlan(deps, bundleTarget, { ...options, dryRun: true });
|
|
@@ -440,7 +438,7 @@ async function buildSchedulerSyncPlan(deps, bundleTarget, options) {
|
|
|
440
438
|
installed,
|
|
441
439
|
scopes,
|
|
442
440
|
...(sched.expectedSignature ? { expectedSignature: sched.expectedSignature.bind(sched) } : {}),
|
|
443
|
-
|
|
441
|
+
installOptions: runtime.options,
|
|
444
442
|
rebind: options.rebind === true,
|
|
445
443
|
extraRemovals,
|
|
446
444
|
keepRefs,
|
|
@@ -450,53 +448,45 @@ async function buildSchedulerSyncPlan(deps, bundleTarget, options) {
|
|
|
450
448
|
? plan.operations.some((operation) => operation.kind !== "remove")
|
|
451
449
|
: plan.installed.length > 0;
|
|
452
450
|
if (runtime.via === "checkout" && writesLauncher) {
|
|
453
|
-
warnings.push(`Scheduled tasks now run akm from a source checkout (${runtime.options
|
|
451
|
+
warnings.push(`Scheduled tasks now run akm from a source checkout (${runtime.options.binding?.join(" ")}); they run whatever the checkout holds when they fire. Install akm with \`npm install --global akm-cli\` or a standalone release, then run \`akm task sync --rebind\`.`);
|
|
454
452
|
}
|
|
455
|
-
return {
|
|
456
|
-
sched,
|
|
457
|
-
plan: { ...plan, failures: [...failures, ...plan.failures] },
|
|
458
|
-
...(runtime.publish ? { publish: runtime.publish } : {}),
|
|
459
|
-
warnings,
|
|
460
|
-
};
|
|
453
|
+
return { sched, plan: { ...plan, failures: [...failures, ...plan.failures] }, warnings };
|
|
461
454
|
}
|
|
462
455
|
/**
|
|
463
|
-
* The launcher and
|
|
464
|
-
* the current policy on every sync; a row that is already installed
|
|
465
|
-
* launcher unless `--rebind` (see `installOptionsFor`). An injected
|
|
466
|
-
* (tests) renders with its own
|
|
456
|
+
* The launcher and environment rows are written with. The environment
|
|
457
|
+
* follows the current policy on every sync; a row that is already installed
|
|
458
|
+
* keeps its launcher unless `--rebind` (see `installOptionsFor`). An injected
|
|
459
|
+
* backend (tests) renders with its own launcher.
|
|
467
460
|
*/
|
|
468
461
|
function prepareSchedulerRuntime(deps) {
|
|
462
|
+
const environment = scheduledRowEnvironment();
|
|
469
463
|
if (deps.schedulerRuntime) {
|
|
470
464
|
const runtime = deps.schedulerRuntime();
|
|
471
|
-
return {
|
|
472
|
-
options: { binding: runtime.binding, contextPath: runtime.contextPath },
|
|
473
|
-
...(runtime.via ? { via: runtime.via } : {}),
|
|
474
|
-
};
|
|
465
|
+
return { options: { binding: runtime.binding, environment }, ...(runtime.via ? { via: runtime.via } : {}) };
|
|
475
466
|
}
|
|
476
467
|
if (deps.backend)
|
|
477
|
-
return {};
|
|
478
|
-
const descriptor = schedulerContextDescriptor();
|
|
468
|
+
return { options: { environment } };
|
|
479
469
|
const invocation = resolveAkmInvocation();
|
|
480
|
-
return {
|
|
481
|
-
options: { binding: invocation.argv, contextPath: schedulerContextPath(descriptor) },
|
|
482
|
-
// The content-addressed descriptor is written once, before the first row that references it.
|
|
483
|
-
publish: () => {
|
|
484
|
-
writeSchedulerContextDescriptor(descriptor);
|
|
485
|
-
},
|
|
486
|
-
via: invocation.via,
|
|
487
|
-
};
|
|
470
|
+
return { options: { binding: invocation.argv, environment }, via: invocation.via };
|
|
488
471
|
}
|
|
489
|
-
/** One read of the akm-owned rows, each attributed to the bundle path
|
|
472
|
+
/** One read of the akm-owned rows, each attributed to the bundle path it names (#846). */
|
|
490
473
|
async function listInstalledRows(sched) {
|
|
491
474
|
return (await sched.list()).map((row) => {
|
|
492
|
-
const ownerBundlePath =
|
|
475
|
+
const ownerBundlePath = installedRowBundleDir(row);
|
|
493
476
|
return ownerBundlePath !== undefined ? { ...row, ownerBundlePath } : row;
|
|
494
477
|
});
|
|
495
478
|
}
|
|
496
|
-
/**
|
|
497
|
-
|
|
479
|
+
/**
|
|
480
|
+
* The `AKM_BUNDLE_DIR` an installed row names: its inline one, or the one in
|
|
481
|
+
* the descriptor a row written before 0.9.17-alpha.7 references. Undefined
|
|
482
|
+
* when it names none, or its descriptor cannot be read — which never means
|
|
483
|
+
* "mine".
|
|
484
|
+
*/
|
|
485
|
+
function installedRowBundleDir(row) {
|
|
486
|
+
if (row.contextPath === undefined)
|
|
487
|
+
return row.environment?.AKM_BUNDLE_DIR;
|
|
498
488
|
try {
|
|
499
|
-
return
|
|
489
|
+
return readLegacySchedulerContext(row.contextPath).AKM_BUNDLE_DIR;
|
|
500
490
|
}
|
|
501
491
|
catch {
|
|
502
492
|
return undefined;
|
|
@@ -515,26 +505,34 @@ function installedSchedulerBundle(config, row) {
|
|
|
515
505
|
return bundleKeyForContentRoot(config, row.ownerBundlePath);
|
|
516
506
|
}
|
|
517
507
|
/**
|
|
518
|
-
* Why `akm task prune` (#851) would remove an installed row:
|
|
519
|
-
* descriptor
|
|
520
|
-
* that is gone (`dead-bundle-path`). A row
|
|
521
|
-
* bundle is never a candidate.
|
|
508
|
+
* Why `akm task prune` (#851) would remove an installed row: a row written
|
|
509
|
+
* before 0.9.17-alpha.7 whose descriptor cannot be read (`invalid-context`),
|
|
510
|
+
* or one naming a bundle directory that is gone (`dead-bundle-path`). A row
|
|
511
|
+
* that still resolves to a live bundle is never a candidate.
|
|
522
512
|
*/
|
|
523
513
|
function classifyPruneCandidate(entry) {
|
|
524
|
-
let
|
|
525
|
-
|
|
526
|
-
|
|
514
|
+
let bundleDir;
|
|
515
|
+
if (entry.contextPath !== undefined) {
|
|
516
|
+
try {
|
|
517
|
+
bundleDir = readLegacySchedulerContext(entry.contextPath).AKM_BUNDLE_DIR;
|
|
518
|
+
}
|
|
519
|
+
catch {
|
|
520
|
+
return "invalid-context";
|
|
521
|
+
}
|
|
522
|
+
// Every descriptor akm wrote names the bundle; one that does not cannot be attributed.
|
|
523
|
+
if (bundleDir === undefined)
|
|
524
|
+
return "invalid-context";
|
|
527
525
|
}
|
|
528
|
-
|
|
529
|
-
|
|
526
|
+
else {
|
|
527
|
+
bundleDir = entry.environment?.AKM_BUNDLE_DIR;
|
|
530
528
|
}
|
|
531
|
-
if (
|
|
529
|
+
if (bundleDir !== undefined && !fs.existsSync(bundleDir))
|
|
532
530
|
return "dead-bundle-path";
|
|
533
531
|
return undefined;
|
|
534
532
|
}
|
|
535
533
|
/**
|
|
536
534
|
* `akm task prune` (#851): remove installed rows `sync` can never reclaim
|
|
537
|
-
* because
|
|
535
|
+
* because they no longer resolve to a live bundle. It scans
|
|
538
536
|
* every installed row, not one bundle's. Without `--yes` it only previews and
|
|
539
537
|
* writes nothing; `--id` narrows it to named candidates.
|
|
540
538
|
*/
|
|
@@ -655,7 +653,7 @@ function groupInstalledBindings(entries) {
|
|
|
655
653
|
}
|
|
656
654
|
groups.set(key, {
|
|
657
655
|
argv,
|
|
658
|
-
contextPath: entry.contextPath,
|
|
656
|
+
...(entry.contextPath !== undefined ? { contextPath: entry.contextPath } : {}),
|
|
659
657
|
taskIds: [entry.id],
|
|
660
658
|
status,
|
|
661
659
|
});
|
|
@@ -669,13 +667,15 @@ function inspectInstalledBinding(entry) {
|
|
|
669
667
|
status.push("checkout");
|
|
670
668
|
if (binding.some((part) => part === "akm" || part === "bun" || part === "node"))
|
|
671
669
|
status.push("path-selected");
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
670
|
+
if (entry.contextPath !== undefined) {
|
|
671
|
+
try {
|
|
672
|
+
readLegacySchedulerContext(entry.contextPath);
|
|
673
|
+
}
|
|
674
|
+
catch {
|
|
675
|
+
status.push("invalid-context");
|
|
676
|
+
}
|
|
677
677
|
}
|
|
678
|
-
const absolutePaths =
|
|
678
|
+
const absolutePaths = binding.filter((part) => path.isAbsolute(part));
|
|
679
679
|
if (absolutePaths.some((part) => !fs.existsSync(part)))
|
|
680
680
|
status.push("missing-path");
|
|
681
681
|
if (status.length === 0)
|
|
@@ -734,8 +734,9 @@ export function resolveTaskReadBundle(refBundle, flagBundle) {
|
|
|
734
734
|
* New scheduler bindings always carry a canonical `--bundle <owner>` token,
|
|
735
735
|
* including an env-selected working stash. That stash need not be persisted in
|
|
736
736
|
* config (CI, one-shot tools, and fresh installs commonly use only
|
|
737
|
-
* AKM_BUNDLE_DIR), so its scheduled child
|
|
738
|
-
*
|
|
737
|
+
* AKM_BUNDLE_DIR), so its scheduled child — every row sets AKM_BUNDLE_DIR to
|
|
738
|
+
* the working stash it was synced from — must accept precisely its derived
|
|
739
|
+
* owner name.
|
|
739
740
|
*
|
|
740
741
|
* This is intentionally narrower than an unknown-bundle fallback: a configured
|
|
741
742
|
* source always wins, and an unconfigured selector is accepted only when it is
|
|
@@ -9,23 +9,20 @@
|
|
|
9
9
|
* a bundle/adapter/concept id at all — it reads exactly the path it was
|
|
10
10
|
* given and classifies it.
|
|
11
11
|
*
|
|
12
|
-
* Reuses the exact version
|
|
13
|
-
* (`src/tasks/source/parse-task-source.ts`)
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* the report, because `parseTaskSource`'s own `ParsedTaskSource.version` is
|
|
20
|
-
* always `4` post-shim — it cannot answer "was this a v2/v3/v4 file?" on
|
|
21
|
-
* its own once a v2/v3 source has been converted in memory.
|
|
12
|
+
* Reuses the exact version router `parseTaskSource`
|
|
13
|
+
* (`src/tasks/source/parse-task-source.ts`) every other task-source reader
|
|
14
|
+
* uses (`akm task sync`'s `compileTaskSources` included) — this module never
|
|
15
|
+
* forks a second parser. `readBoundedTaskSourceYaml` /
|
|
16
|
+
* `peekTaskSourceVersion` / `own` are the SAME front-end helpers the router
|
|
17
|
+
* calls first; they are used here only to recover the file's DECLARED schema
|
|
18
|
+
* version for the report and to tell `blocked` from `invalid`.
|
|
22
19
|
*
|
|
23
20
|
* Beyond parsing, this module also runs the SAME two per-source gates
|
|
24
21
|
* `akm task sync`'s `compileTaskSources` runs before it ever installs a
|
|
25
22
|
* schedule — `assertTaskScheduleInputsSatisfyContract` and
|
|
26
23
|
* `assertTaskScheduleCronValid` (both extracted from `scheduler-sync.ts` for
|
|
27
24
|
* exactly this reuse) — so a file `sync` would reject can never
|
|
28
|
-
* be reported `valid
|
|
25
|
+
* be reported `valid` here. Cron dialect is checked against
|
|
29
26
|
* `backendNameForPlatform()`, the same platform default `sync` falls back
|
|
30
27
|
* to whenever it has no injected/native-inspected backend to hand (see
|
|
31
28
|
* `akmTasksAdd`, `src/commands/tasks/tasks.ts`); a bare file was never
|
|
@@ -46,27 +43,18 @@
|
|
|
46
43
|
*
|
|
47
44
|
* Outcome classification (mirrors `parse-task-source.ts`'s own routing
|
|
48
45
|
* table in its header, extended for the two gates above):
|
|
49
|
-
* - `valid` — parses as task source v4
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
* `schedule[].enabled` key also reports `converts`
|
|
56
|
-
* (`sourceVersion` is still `4`) — it read through the
|
|
57
|
-
* same in-memory shim, not the direct v4 parse path.
|
|
58
|
-
* - `blocked` — declared `version: 2` or `3`; the migrator itself
|
|
59
|
-
* could not convert it (an ambiguous/unmigratable
|
|
60
|
-
* shape) — the ONLY way `parseTaskSource` ever throws
|
|
61
|
-
* for those two version numbers, so no message-text
|
|
62
|
-
* sniffing is needed to tell this apart from `invalid`.
|
|
46
|
+
* - `valid` — parses as task source v4 and passes both sync gates.
|
|
47
|
+
* - `blocked` — declared `version: 2` or `3`, or a `version: 4`
|
|
48
|
+
* document still carrying the retired
|
|
49
|
+
* `schedule[].enabled`: what `akm migrate apply`
|
|
50
|
+
* rewrites. The runtime reads only v4 (#987), so the
|
|
51
|
+
* reason names that command.
|
|
63
52
|
* - `invalid` — the document declares SOME version (`4`, or anything
|
|
64
53
|
* other than 2/3/4) but fails to parse/validate, OR it
|
|
65
|
-
* parsed
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
* shape) — reported with the parser's own reason.
|
|
54
|
+
* parsed but fails one of the two sync gates above, OR
|
|
55
|
+
* the YAML itself does not parse at all (a genuine
|
|
56
|
+
* syntax error, not merely a non-task shape) — reported
|
|
57
|
+
* with the parser's own reason.
|
|
70
58
|
* - `not-a-task` — the document parses as YAML but never declares a
|
|
71
59
|
* `version:` field at all (or isn't a YAML mapping) —
|
|
72
60
|
* the strongest signal available that the file was
|
|
@@ -142,10 +130,11 @@ export async function akmTaskValidate(filePath) {
|
|
|
142
130
|
if (!(cause instanceof UsageError))
|
|
143
131
|
throw cause;
|
|
144
132
|
const reason = cause.message;
|
|
145
|
-
// `
|
|
146
|
-
//
|
|
147
|
-
|
|
148
|
-
|
|
133
|
+
// What `akm migrate apply` rewrites is the only refusal for these
|
|
134
|
+
// shapes (`parse-task-source.ts`), so no message-text sniffing is needed.
|
|
135
|
+
if (declaredVersion === 2 ||
|
|
136
|
+
declaredVersion === 3 ||
|
|
137
|
+
(declaredVersion === 4 && v4ScheduleHasRetiredEnabledKey(root))) {
|
|
149
138
|
return { ok: false, path: resolvedPath, sourceVersion: declaredVersion, outcome: "blocked", reason };
|
|
150
139
|
}
|
|
151
140
|
if (peekFailed) {
|
|
@@ -163,13 +152,10 @@ export async function akmTaskValidate(filePath) {
|
|
|
163
152
|
};
|
|
164
153
|
}
|
|
165
154
|
// Success is unreachable from any path that leaves `declaredVersion`
|
|
166
|
-
// undefined — the router requires a numeric
|
|
155
|
+
// undefined — the router requires a numeric version 4 to reach here.
|
|
167
156
|
const sourceVersion = declaredVersion ?? 4;
|
|
168
|
-
// The document itself parsed
|
|
169
|
-
//
|
|
170
|
-
// accepting it. A violation here is `invalid`, never `blocked`: the
|
|
171
|
-
// migrator already succeeded, so this is the same kind of defect a
|
|
172
|
-
// native v4 document with the identical schedule would have.
|
|
157
|
+
// The document itself parsed — now the two gates `compileTaskSources`
|
|
158
|
+
// runs before accepting it. A violation here is `invalid`.
|
|
173
159
|
try {
|
|
174
160
|
assertTaskScheduleInputsSatisfyContract(parsed.v4, resolvedPath);
|
|
175
161
|
assertTaskScheduleCronValid(parsed.v4, backend);
|
|
@@ -179,17 +165,12 @@ export async function akmTaskValidate(filePath) {
|
|
|
179
165
|
throw cause;
|
|
180
166
|
return { ok: false, path: resolvedPath, sourceVersion, outcome: "invalid", reason: cause.message };
|
|
181
167
|
}
|
|
182
|
-
// A declared `version: 4` document with a retired `schedule[].enabled`
|
|
183
|
-
// key parsed through `parseTaskSource`'s in-memory shim, not the direct
|
|
184
|
-
// v4 path, even though `sourceVersion` reads `4` — report it the same
|
|
185
|
-
// way a converted v2/v3 document is reported.
|
|
186
|
-
const convertedFromRetiredScheduleEnabled = sourceVersion === 4 && !peekFailed && v4ScheduleHasRetiredEnabledKey(root);
|
|
187
168
|
const id = path.parse(resolvedPath).name;
|
|
188
169
|
return {
|
|
189
170
|
ok: true,
|
|
190
171
|
path: resolvedPath,
|
|
191
172
|
sourceVersion,
|
|
192
|
-
outcome:
|
|
173
|
+
outcome: "valid",
|
|
193
174
|
resolved: buildResolved(id, parsed.v4),
|
|
194
175
|
};
|
|
195
176
|
}
|