akm-cli 0.9.0-rc.13 → 0.9.0-rc.14
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 +117 -23
- package/dist/akm-migrate +3 -1
- package/dist/assets/hints/cli-hints-full.md +3 -4
- package/dist/assets/hints/cli-hints-short.md +5 -5
- package/dist/assets/workflows/workflow-template.md +4 -3
- package/dist/cli/invocation.js +3 -2
- package/dist/cli/retired-commands.js +3 -0
- package/dist/cli/unknown-flags.js +226 -0
- package/dist/cli.js +19 -35
- package/dist/commands/agent/contribute-cli.js +15 -3
- package/dist/commands/feedback-cli.js +6 -3
- package/dist/commands/improve/collapse-detector.js +2 -3
- package/dist/commands/lint/base-linter.js +4 -16
- package/dist/commands/lint/index.js +13 -13
- package/dist/commands/log.js +6 -1
- package/dist/commands/migration-tool.js +4 -5
- package/dist/commands/observability-cli.js +1 -1
- package/dist/commands/proposal/repository.js +5 -5
- package/dist/commands/read/knowledge.js +2 -0
- package/dist/commands/registry-cli.js +5 -3
- package/dist/commands/sources/add-cli.js +6 -6
- package/dist/commands/sources/self-update.js +30 -7
- package/dist/commands/sources/source-add.js +17 -2
- package/dist/commands/tasks/tasks.js +8 -3
- package/dist/commands/workflow-cli.js +142 -119
- package/dist/core/adapter/adapters/akm-lint.js +17 -13
- package/dist/core/adapter/adapters/akm-task-adapter.js +14 -11
- package/dist/core/asset/akm-markdown.js +41 -8
- package/dist/core/asset/frontmatter.js +22 -0
- package/dist/core/asset/resolve-ref.js +23 -3
- package/dist/core/common.js +45 -2
- package/dist/core/config/config-schema.js +8 -0
- package/dist/core/config/schema/experimental.js +5 -13
- package/dist/core/config/schema/sources-bundles.js +11 -0
- package/dist/core/config/schema/workflow.js +3 -1
- package/dist/core/errors.js +5 -0
- package/dist/core/logs-db.js +2 -1
- package/dist/core/parse.js +4 -1
- package/dist/core/state/migrations.js +11 -14
- package/dist/core/state-db.js +4 -6
- package/dist/core/subprocess.js +6 -4
- package/dist/core/type-presentation.js +1 -1
- package/dist/indexer/indexer.js +16 -1
- package/dist/indexer/search/search-source.js +1 -1
- package/dist/indexer/walk/matchers.js +3 -1
- package/dist/integrations/agent/config.js +2 -2
- package/dist/integrations/agent/detect.js +49 -19
- package/dist/integrations/agent/profiles.js +10 -0
- package/dist/integrations/agent/spawn.js +1 -2
- package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +7 -3
- package/dist/integrations/lockfile.js +11 -56
- package/dist/output/shapes/passthrough.js +0 -4
- package/dist/output/text/command-format.js +3 -3
- package/dist/output/text/helpers.js +1 -1
- package/dist/output/text/show-directives.js +4 -6
- package/dist/output/text/workflow-format.js +7 -62
- package/dist/output/text/workflow.js +1 -5
- package/dist/scripts/akm-migrate-node.js +58773 -0
- package/dist/scripts/akm-migrate.js +33976 -11391
- package/dist/setup/detect.js +40 -15
- package/dist/setup/setup.js +1 -1
- package/dist/sources/providers/git-stash.js +4 -2
- package/dist/sources/providers/website.js +5 -0
- package/dist/sources/snapshot-fetchers/bluesky.js +146 -0
- package/dist/sources/snapshot-fetchers/content-extract.js +370 -0
- package/dist/sources/snapshot-fetchers/fetcher-util.js +40 -0
- package/dist/sources/snapshot-fetchers/host-guard.js +199 -0
- package/dist/sources/snapshot-fetchers/registry.js +10 -1
- package/dist/sources/snapshot-fetchers/robots.js +348 -0
- package/dist/sources/snapshot-fetchers/rss.js +279 -0
- package/dist/sources/snapshot-fetchers/secret-seam.js +42 -0
- package/dist/sources/snapshot-fetchers/website-ingest.js +488 -257
- package/dist/sources/snapshot-fetchers/x.js +193 -0
- package/dist/storage/engines/sqlite-migrations.js +22 -107
- package/dist/tasks/runner.js +19 -16
- package/dist/tasks/schema.js +24 -1
- package/dist/workflows/exec/brief.js +1 -1
- package/dist/workflows/exec/frozen-judge.js +28 -2
- package/dist/workflows/exec/native-executor.js +18 -1
- package/dist/workflows/exec/report.js +11 -4
- package/dist/workflows/exec/run-workflow.js +103 -44
- package/dist/workflows/exec/step-work.js +19 -18
- package/dist/workflows/exec/unit-dispatch.js +4 -0
- package/dist/workflows/exec/workflow-engine-gate.js +11 -13
- package/dist/workflows/ir/compile.js +2 -2
- package/dist/workflows/ir/freeze.js +16 -8
- package/dist/workflows/ir/params.js +134 -10
- package/dist/workflows/ir/plan-hash.js +1 -1
- package/dist/workflows/ir/schema.js +6 -2
- package/dist/workflows/renderer.js +2 -2
- package/dist/workflows/runtime/checkin.js +3 -3
- package/dist/workflows/runtime/runs.js +50 -29
- package/dist/workflows/validate-summary.js +30 -14
- package/docs/migration/release-notes/0.9.0.md +31 -4
- package/docs/migration/v0.8-to-v0.9.md +69 -100
- package/docs/reference/data-and-telemetry.md +5 -5
- package/package.json +6 -2
- package/schemas/akm-config.json +24 -0
- package/schemas/akm-workflow.json +1 -1
- package/dist/workflows/cli.js +0 -33
|
@@ -25,6 +25,7 @@ import { getStringArg, parsePositiveIntFlag } from "../../cli/parse-args.js";
|
|
|
25
25
|
import { EXIT_CODES, GLOBAL_OUTPUT_ARGS, output, runWithJsonErrors } from "../../cli/shared.js";
|
|
26
26
|
import { loadConfig } from "../../core/config/config.js";
|
|
27
27
|
import { resolveUsageEventSource } from "../../indexer/usage/usage-events.js";
|
|
28
|
+
import { getHyphenatedBoolean } from "../../output/context.js";
|
|
28
29
|
import { akmLint } from "../lint/index.js";
|
|
29
30
|
import { akmAgentDispatch } from "./agent-dispatch.js";
|
|
30
31
|
const EXIT_GENERAL = EXIT_CODES.GENERAL;
|
|
@@ -132,8 +133,19 @@ export const lintCommand = defineCommand({
|
|
|
132
133
|
...GLOBAL_OUTPUT_ARGS,
|
|
133
134
|
fix: {
|
|
134
135
|
type: "boolean",
|
|
135
|
-
|
|
136
|
-
|
|
136
|
+
description: "Apply auto-fixes in place",
|
|
137
|
+
default: false,
|
|
138
|
+
},
|
|
139
|
+
// Declared as its own arg rather than `alias: "auto-fix"`: citty's alias
|
|
140
|
+
// handling (node:util parseArgs `short` options) only supports
|
|
141
|
+
// SINGLE-character aliases. A
|
|
142
|
+
// multi-char alias rendered in help as `-auto-fix` (one dash) and parsed
|
|
143
|
+
// as a pile of junk single-char flags, so BOTH advertised spellings —
|
|
144
|
+
// `-auto-fix` and `--auto-fix` — silently ran a plain lint while claiming
|
|
145
|
+
// to fix. Two real boolean args, OR'd at the call site, work and render.
|
|
146
|
+
"auto-fix": {
|
|
147
|
+
type: "boolean",
|
|
148
|
+
description: "Apply auto-fixes in place (same as --fix)",
|
|
137
149
|
default: false,
|
|
138
150
|
},
|
|
139
151
|
dir: { type: "string", description: "Override bundle root directory (default: from config)" },
|
|
@@ -151,7 +163,7 @@ export const lintCommand = defineCommand({
|
|
|
151
163
|
async run({ args }) {
|
|
152
164
|
await runWithJsonErrors(async () => {
|
|
153
165
|
const result = akmLint({
|
|
154
|
-
fix: args.fix
|
|
166
|
+
fix: args.fix === true || getHyphenatedBoolean(args, "auto-fix"),
|
|
155
167
|
dir: getStringArg(args, "dir"),
|
|
156
168
|
typeFilter: getStringArg(args, "type"),
|
|
157
169
|
});
|
|
@@ -9,7 +9,7 @@ import { parseFrontmatter, parseFrontmatterBlock } from "../core/asset/frontmatt
|
|
|
9
9
|
import { conceptIdFromTypeName, parseRefInput } from "../core/asset/resolve-ref.js";
|
|
10
10
|
import { isWithin, writeFileAtomic } from "../core/common.js";
|
|
11
11
|
import { FEEDBACK_FAILURE_MODES, loadConfig } from "../core/config/config.js";
|
|
12
|
-
import { UsageError } from "../core/errors.js";
|
|
12
|
+
import { NotFoundError, UsageError } from "../core/errors.js";
|
|
13
13
|
import { appendEvent } from "../core/events.js";
|
|
14
14
|
import { resolveMutationTarget } from "../core/mutation-target.js";
|
|
15
15
|
import { getDbPath } from "../core/paths.js";
|
|
@@ -311,8 +311,11 @@ export const feedbackCommand = defineJsonCommand({
|
|
|
311
311
|
const lookupRef = makeBundleRef(parsedRef.bundle, parsedRef.conceptId);
|
|
312
312
|
const entryId = findEntryIdByRef(db, lookupRef, requestedSource?.path);
|
|
313
313
|
if (entryId === undefined) {
|
|
314
|
-
|
|
315
|
-
|
|
314
|
+
// NotFoundError (exit 1), not UsageError (exit 2): the flags parsed
|
|
315
|
+
// fine, the asset just isn't there. The documented exit-code table
|
|
316
|
+
// reserves 1 for "requested resource missing", and scripts branch on it.
|
|
317
|
+
throw new NotFoundError(`Ref "${ref}" is not in the index. ` +
|
|
318
|
+
"Run 'akm search' to verify the asset exists, then 'akm index' if it was recently added.", "ASSET_NOT_FOUND");
|
|
316
319
|
}
|
|
317
320
|
// Persist the feedback signal into usage_events. Both positive and
|
|
318
321
|
// negative signals apply the EMA utility update immediately — no
|
|
@@ -15,9 +15,8 @@
|
|
|
15
15
|
* hardcoded 0 from the 0.9.0 confidence-gate deletion onward), so the alert
|
|
16
16
|
* could never fire. See the design doc's status note for the removal
|
|
17
17
|
* rationale. `improve_cycle_metrics.accepted_actions` stays in the schema and
|
|
18
|
-
* is written as 0
|
|
19
|
-
*
|
|
20
|
-
* it would fail those installs closed.
|
|
18
|
+
* is written as 0 because deployed `state.db` files already contain it and
|
|
19
|
+
* removing it would be a destructive schema change.
|
|
21
20
|
*
|
|
22
21
|
* Hard invariants: deterministic only (FTS BM25 + hashing — never an LLM,
|
|
23
22
|
* never an embedding model); bounded storage (< 2 KB per qualifying cycle,
|
|
@@ -35,15 +35,11 @@ import fs from "node:fs";
|
|
|
35
35
|
import path from "node:path";
|
|
36
36
|
import { assetPathForName, stashDirFor } from "../../core/asset/asset-placement.js";
|
|
37
37
|
import { BUNDLE_REF_RE } from "../../core/asset/asset-ref.js";
|
|
38
|
+
import { spliceFrontmatterLine } from "../../core/asset/frontmatter.js";
|
|
38
39
|
import { typeNameFromConceptId } from "../../core/asset/resolve-ref.js";
|
|
40
|
+
import { localDateStamp } from "../../core/common.js";
|
|
39
41
|
import { findFenceRegions } from "./markdown-insertion.js";
|
|
40
42
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
41
|
-
function formatDate(d) {
|
|
42
|
-
const y = d.getFullYear();
|
|
43
|
-
const m = String(d.getMonth() + 1).padStart(2, "0");
|
|
44
|
-
const day = String(d.getDate()).padStart(2, "0");
|
|
45
|
-
return `${y}-${m}-${day}`;
|
|
46
|
-
}
|
|
47
43
|
function checkUnquotedColon(frontmatterText) {
|
|
48
44
|
if (!frontmatterText)
|
|
49
45
|
return null;
|
|
@@ -86,15 +82,7 @@ function checkMissingUpdated(data, frontmatterText) {
|
|
|
86
82
|
return frontmatterText !== null && !("updated" in data);
|
|
87
83
|
}
|
|
88
84
|
function fixMissingUpdated(raw, mtime) {
|
|
89
|
-
|
|
90
|
-
const lines = raw.split(/\r?\n/);
|
|
91
|
-
if (lines[0]?.trim() !== "---")
|
|
92
|
-
return raw;
|
|
93
|
-
const closeIdx = lines.findIndex((l, i) => i > 0 && l.trim() === "---");
|
|
94
|
-
if (closeIdx === -1)
|
|
95
|
-
return raw;
|
|
96
|
-
lines.splice(closeIdx, 0, `updated: ${dateStr}`);
|
|
97
|
-
return lines.join("\n");
|
|
85
|
+
return spliceFrontmatterLine(raw, `updated: ${localDateStamp(mtime)}`) ?? raw;
|
|
98
86
|
}
|
|
99
87
|
// ── stale-path helpers ────────────────────────────────────────────────────────
|
|
100
88
|
function checkStalePath(body) {
|
|
@@ -529,7 +517,7 @@ export function runBaseChecks(ctx) {
|
|
|
529
517
|
issues.push({
|
|
530
518
|
file: ctx.relPath,
|
|
531
519
|
issue: "missing-updated",
|
|
532
|
-
detail: `stamped updated: ${
|
|
520
|
+
detail: `stamped updated: ${localDateStamp(mtime)}`,
|
|
533
521
|
fixed: true,
|
|
534
522
|
});
|
|
535
523
|
}
|
|
@@ -6,7 +6,9 @@ import path from "node:path";
|
|
|
6
6
|
import { parse as parseYaml } from "yaml";
|
|
7
7
|
import { factDiagnostics, matchWorkflowPlaceholder, memoryOrphanStubApplies, nameOrTypeDiagnostics, ORPHANED_STUB_DETAIL, taskDiagnostics, workflowStructureDiagnostics, } from "../../core/adapter/adapters/akm-lint.js";
|
|
8
8
|
import { detectAdapterId } from "../../core/adapter/detect-adapter.js";
|
|
9
|
+
import { stashDirFor } from "../../core/asset/asset-placement.js";
|
|
9
10
|
import { parseFrontmatter, parseFrontmatterBlock } from "../../core/asset/frontmatter.js";
|
|
11
|
+
import { conceptIdForStashFile, displayRefForConceptId } from "../../core/asset/resolve-ref.js";
|
|
10
12
|
import { resolveStashDir } from "../../core/common.js";
|
|
11
13
|
import { loadConfig, primaryBundlePath } from "../../core/config/config.js";
|
|
12
14
|
import { resolveSourceEntries } from "../../indexer/search/search-source.js";
|
|
@@ -321,23 +323,21 @@ export function akmLint(options = {}) {
|
|
|
321
323
|
// keys that are known to enable process-execution hijacking. Warn-only —
|
|
322
324
|
// findings go into `flagged`, never `fixed`.
|
|
323
325
|
const envRoots = [stashRoot, ...extraStashRoots];
|
|
326
|
+
const bundleIdByRoot = new Map(sources.map((source) => [path.resolve(source.path), source.registryId]));
|
|
324
327
|
for (const root of envRoots) {
|
|
325
|
-
|
|
326
|
-
// `
|
|
327
|
-
//
|
|
328
|
-
//
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
const dir = path.join(root, scanSubdir);
|
|
328
|
+
const bundleId = bundleIdByRoot.get(path.resolve(root));
|
|
329
|
+
// `env` assets live under `env/`, whole-file `secret` assets under
|
|
330
|
+
// `secrets/`. `displayRefForConceptId` owns the short-default /
|
|
331
|
+
// qualified-secondary `Ref:` spelling `akm show` emits — the old
|
|
332
|
+
// hand-built `env:<base>` colon grammar is rejected by the 0.9.0 ref
|
|
333
|
+
// parser, which dead-ended a user copying the ref off a security finding.
|
|
334
|
+
for (const assetType of ["env", "secret"]) {
|
|
335
|
+
const dir = path.join(root, stashDirFor(assetType));
|
|
334
336
|
if (!fs.existsSync(dir))
|
|
335
337
|
continue;
|
|
336
338
|
for (const envPath of collectEnvFiles(dir)) {
|
|
337
|
-
const
|
|
338
|
-
|
|
339
|
-
// basename so it doesn't collide with `default.env` → refPrefix:default.
|
|
340
|
-
const ref = baseName === "" ? `${refPrefix}:.env` : `${refPrefix}:${baseName}`;
|
|
339
|
+
const conceptId = conceptIdForStashFile(assetType, root, envPath);
|
|
340
|
+
const ref = displayRefForConceptId(conceptId, bundleId, cfg.defaultBundle);
|
|
341
341
|
const relPath = path.relative(root, envPath);
|
|
342
342
|
for (const issue of checkEnvForDangerousKeys(envPath, relPath, ref)) {
|
|
343
343
|
flagged.push(issue);
|
package/dist/commands/log.js
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
* other caller and was removed with it.
|
|
17
17
|
*/
|
|
18
18
|
import { makeBundleRef, parseBundleRef } from "../core/asset/asset-ref.js";
|
|
19
|
+
import { loadConfig } from "../core/config/config.js";
|
|
19
20
|
import { UsageError } from "../core/errors.js";
|
|
20
21
|
import { readEvents } from "../core/events.js";
|
|
21
22
|
import { parseSinceToIso } from "../core/time.js";
|
|
@@ -49,7 +50,11 @@ function validateRef(ref) {
|
|
|
49
50
|
throw new UsageError("--ref cannot be empty.", "INVALID_FLAG_VALUE");
|
|
50
51
|
}
|
|
51
52
|
const parsed = parseBundleRef(trimmed);
|
|
52
|
-
|
|
53
|
+
// Deliberately shallow qualification: a bare conceptId is assumed to mean
|
|
54
|
+
// the default bundle, without the full priority-walk `resolveRef` performs —
|
|
55
|
+
// event refs are recorded fully qualified, so the default is the only
|
|
56
|
+
// spelling a short --ref can usefully mean here.
|
|
57
|
+
return makeBundleRef(parsed.bundle ?? loadConfig().defaultBundle, parsed.conceptId);
|
|
53
58
|
}
|
|
54
59
|
function validateRunId(run) {
|
|
55
60
|
if (run === undefined)
|
|
@@ -7,8 +7,8 @@ import { fileURLToPath } from "node:url";
|
|
|
7
7
|
import { NotFoundError } from "../core/errors.js";
|
|
8
8
|
function migrationEntryPoint() {
|
|
9
9
|
const candidates = [
|
|
10
|
-
fileURLToPath(new URL("
|
|
11
|
-
fileURLToPath(new URL("
|
|
10
|
+
fileURLToPath(new URL(process.versions.bun ? "../scripts/akm-migrate.js" : "../scripts/akm-migrate-node.js", import.meta.url)),
|
|
11
|
+
...(process.versions.bun ? [fileURLToPath(new URL("../../scripts/akm-migrate.ts", import.meta.url))] : []),
|
|
12
12
|
];
|
|
13
13
|
return candidates.find((candidate) => fs.existsSync(candidate));
|
|
14
14
|
}
|
|
@@ -27,14 +27,13 @@ export async function runMigrationTool(args) {
|
|
|
27
27
|
// `scripts/akm-standalone.ts`, which embeds the migrator and dispatches to
|
|
28
28
|
// it when `AKM_MIGRATE_ENTRY=1` — so re-exec ourselves with the marker.
|
|
29
29
|
// (src must not import scripts/: the dist build's tsc has `rootDir: src`.)
|
|
30
|
-
const
|
|
31
|
-
const result = spawnSync(runtime, entry ? [entry, ...args] : [...args], {
|
|
30
|
+
const result = spawnSync(process.execPath, entry ? [entry, ...args] : [...args], {
|
|
32
31
|
encoding: "utf8",
|
|
33
32
|
env: entry ? process.env : { ...process.env, AKM_MIGRATE_ENTRY: "1" },
|
|
34
33
|
maxBuffer: 16 * 1024 * 1024,
|
|
35
34
|
});
|
|
36
35
|
if (result.error) {
|
|
37
|
-
throw new NotFoundError(`Cannot start the standalone akm-migrate tool: ${result.error.message}`, "FILE_NOT_FOUND", "
|
|
36
|
+
throw new NotFoundError(`Cannot start the standalone akm-migrate tool: ${result.error.message}`, "FILE_NOT_FOUND", "Reinstall akm-cli, or use a runtime-free standalone release binary.");
|
|
38
37
|
}
|
|
39
38
|
if (result.stdout)
|
|
40
39
|
process.stdout.write(result.stdout);
|
|
@@ -72,7 +72,7 @@ export const logCommand = defineJsonCommand({
|
|
|
72
72
|
ref: { type: "string", description: "Filter by asset ref ([bundle//]conceptId)" },
|
|
73
73
|
run: {
|
|
74
74
|
type: "string",
|
|
75
|
-
description: "Filter to a workflow run's events (metadata.runId), e.g. the id from `akm workflow
|
|
75
|
+
description: "Filter to a workflow run's events (metadata.runId), e.g. the id from `akm workflow run`",
|
|
76
76
|
},
|
|
77
77
|
"exclude-tags": {
|
|
78
78
|
type: "string",
|
|
@@ -168,7 +168,7 @@ function persistProposalUpdate(db, proposal, stashDir) {
|
|
|
168
168
|
.prepare("SELECT metadata_json FROM proposals WHERE id = ? AND stash_dir = ?")
|
|
169
169
|
.get(proposal.id, stashDir);
|
|
170
170
|
if (!row)
|
|
171
|
-
throw new NotFoundError(`Proposal "${proposal.id}" not found.`, "
|
|
171
|
+
throw new NotFoundError(`Proposal "${proposal.id}" not found.`, "PROPOSAL_NOT_FOUND");
|
|
172
172
|
const metadata = JSON.parse(row.metadata_json);
|
|
173
173
|
for (const field of [
|
|
174
174
|
"sourceRun",
|
|
@@ -572,7 +572,7 @@ export function getProposal(stashDir, id, ctx) {
|
|
|
572
572
|
function requireProposal(db, stashDir, id) {
|
|
573
573
|
const proposal = getStateProposal(db, id, stashDir);
|
|
574
574
|
if (!proposal) {
|
|
575
|
-
throw new NotFoundError(`Proposal "${id}" not found.`, "
|
|
575
|
+
throw new NotFoundError(`Proposal "${id}" not found.`, "PROPOSAL_NOT_FOUND");
|
|
576
576
|
}
|
|
577
577
|
return proposal;
|
|
578
578
|
}
|
|
@@ -588,7 +588,7 @@ function requireProposal(db, stashDir, id) {
|
|
|
588
588
|
function requireProposalLenient(db, stashDir, id) {
|
|
589
589
|
const proposal = getStateProposalLenient(db, id, stashDir);
|
|
590
590
|
if (!proposal) {
|
|
591
|
-
throw new NotFoundError(`Proposal "${id}" not found.`, "
|
|
591
|
+
throw new NotFoundError(`Proposal "${id}" not found.`, "PROPOSAL_NOT_FOUND");
|
|
592
592
|
}
|
|
593
593
|
return proposal;
|
|
594
594
|
}
|
|
@@ -623,7 +623,7 @@ export function resolveProposalId(stashDir, idOrRef, ctx) {
|
|
|
623
623
|
const archived = byRecency(forConcept());
|
|
624
624
|
if (archived)
|
|
625
625
|
return archived;
|
|
626
|
-
throw new NotFoundError(`No proposal found for ref "${idOrRef}".`, "
|
|
626
|
+
throw new NotFoundError(`No proposal found for ref "${idOrRef}".`, "PROPOSAL_NOT_FOUND");
|
|
627
627
|
}
|
|
628
628
|
// 3. UUID prefix (pending queue only).
|
|
629
629
|
const prefixMatches = listStateProposalIdsByPrefix(db, stashDir, idOrRef);
|
|
@@ -632,7 +632,7 @@ export function resolveProposalId(stashDir, idOrRef, ctx) {
|
|
|
632
632
|
if (prefixMatches.length > 1) {
|
|
633
633
|
throw new UsageError(`Ambiguous prefix "${idOrRef}" — matches: ${prefixMatches.join(", ")}`, "INVALID_FLAG_VALUE");
|
|
634
634
|
}
|
|
635
|
-
throw new NotFoundError(`Proposal "${idOrRef}" not found.`, "
|
|
635
|
+
throw new NotFoundError(`Proposal "${idOrRef}" not found.`, "PROPOSAL_NOT_FOUND");
|
|
636
636
|
});
|
|
637
637
|
}
|
|
638
638
|
/**
|
|
@@ -27,6 +27,7 @@ import { commitWriteTargetBoundary, formatRefForMessage, recordWriteTargetPath,
|
|
|
27
27
|
import { indexWrittenAssets } from "../../indexer/index-written-assets.js";
|
|
28
28
|
import { deriveInstallations, slugForPath } from "../../indexer/installations.js";
|
|
29
29
|
import { resolveSourceEntries } from "../../indexer/search/search-source.js";
|
|
30
|
+
import { storeSecretResolver } from "../../sources/snapshot-fetchers/secret-seam.js";
|
|
30
31
|
import { fetchWebsiteMarkdownSnapshot, shouldAllowPrivateWebsiteUrlForTests, } from "../../sources/snapshot-fetchers/website-ingest.js";
|
|
31
32
|
import { writeSupersededEdge } from "../improve/memory/memory-belief.js";
|
|
32
33
|
import { refToRelPath, resolveRefPathInStash } from "../lint/base-linter.js";
|
|
@@ -119,6 +120,7 @@ export async function readKnowledgeInput(source, options) {
|
|
|
119
120
|
return readKnowledgeContent(source);
|
|
120
121
|
const snapshot = await fetchWebsiteMarkdownSnapshot(source, {
|
|
121
122
|
stashDir: options?.stashDir,
|
|
123
|
+
resolveSecret: storeSecretResolver.resolveSecret,
|
|
122
124
|
allowPrivateHosts: options?.allowPrivateHosts ?? shouldAllowPrivateWebsiteUrlForTests(source),
|
|
123
125
|
});
|
|
124
126
|
return { content: snapshot.content, preferredName: snapshot.preferredName };
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
4
|
import { defineGroupCommand, defineJsonCommand, output } from "../cli/shared.js";
|
|
5
5
|
import { getEffectiveRegistries, loadUserConfig, mutateConfig } from "../core/config/config.js";
|
|
6
|
-
import { UsageError } from "../core/errors.js";
|
|
6
|
+
import { NotFoundError, UsageError } from "../core/errors.js";
|
|
7
7
|
import { warn } from "../core/warn.js";
|
|
8
8
|
export const registryCommand = defineGroupCommand({
|
|
9
9
|
meta: { name: "registry", description: "Manage bundle registries" },
|
|
@@ -82,8 +82,10 @@ export const registryCommand = defineGroupCommand({
|
|
|
82
82
|
const registries = [...(config.registries ?? [])];
|
|
83
83
|
const idx = registries.findIndex((r) => r.url === args.target || r.name === args.target);
|
|
84
84
|
if (idx === -1) {
|
|
85
|
-
|
|
86
|
-
|
|
85
|
+
// Was a success envelope with `removed: false` and exit 0, so
|
|
86
|
+
// `akm registry remove typo && ...` proceeded as if it had removed
|
|
87
|
+
// something. A missing target is exit 1, like every other not-found.
|
|
88
|
+
throw new NotFoundError(`No registry matching "${args.target}" is configured.`, "SOURCE_NOT_FOUND", "Run `akm registry list` to see configured registries.");
|
|
87
89
|
}
|
|
88
90
|
const { confirmDestructive } = await import("../cli/confirm.js");
|
|
89
91
|
const confirmed = await confirmDestructive(`Remove registry "${args.target}"? This cannot be undone.`, {
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import fs from "node:fs";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import * as p from "../../cli/clack.js";
|
|
7
|
+
import { getStringArg, parsePositiveIntFlag } from "../../cli/parse-args.js";
|
|
7
8
|
import { defineJsonCommand, output } from "../../cli/shared.js";
|
|
8
9
|
import { decideDangerousKeyInstall } from "../../core/activation-policy.js";
|
|
9
10
|
import { UsageError } from "../../core/errors.js";
|
|
@@ -14,12 +15,11 @@ import { akmAdd } from "./source-add.js";
|
|
|
14
15
|
import { addStash } from "./source-manage.js";
|
|
15
16
|
// ── Shared website-options helper ──────────
|
|
16
17
|
export function buildWebsiteOptions(args) {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
return websiteOptions;
|
|
18
|
+
// getStringArg maps absent/blank to undefined; parsePositiveIntFlag maps
|
|
19
|
+
// undefined to undefined — so an unsupplied flag simply omits the key.
|
|
20
|
+
const maxPages = parsePositiveIntFlag(getStringArg(args, "max-pages"), "--max-pages");
|
|
21
|
+
const maxDepth = parsePositiveIntFlag(getStringArg(args, "max-depth"), "--max-depth");
|
|
22
|
+
return { ...(maxPages !== undefined ? { maxPages } : {}), ...(maxDepth !== undefined ? { maxDepth } : {}) };
|
|
23
23
|
}
|
|
24
24
|
// ── HTTP safety check ─────────────────────────────────────────────────────────
|
|
25
25
|
export function shouldWarnOnPlainHttp(ref) {
|
|
@@ -5,7 +5,7 @@ import * as childProcess from "node:child_process";
|
|
|
5
5
|
import { createHash } from "node:crypto";
|
|
6
6
|
import fs from "node:fs";
|
|
7
7
|
import path from "node:path";
|
|
8
|
-
import { fetchWithRetry, IS_WINDOWS, ResponseTooLargeError, readBodyWithByteCap } from "../../core/common.js";
|
|
8
|
+
import { fetchWithRetry, IS_WINDOWS, ResponseTooLargeError, readBodyWithByteCap, readChunkWithDeadline, } from "../../core/common.js";
|
|
9
9
|
import { ConfigError } from "../../core/errors.js";
|
|
10
10
|
import { warn } from "../../core/warn.js";
|
|
11
11
|
import { githubHeaders } from "../../integrations/github.js";
|
|
@@ -29,7 +29,19 @@ export function defaultMigrationCommand(preparedConfigPath) {
|
|
|
29
29
|
apply: (akmBin) => runRequiredCommand(akmBin, ["migrate", "apply", ...configArgs], "Migration apply"),
|
|
30
30
|
};
|
|
31
31
|
}
|
|
32
|
-
|
|
32
|
+
/**
|
|
33
|
+
* Bounds on the binary body read. `fetchWithTimeout`'s timer only covers
|
|
34
|
+
* time-to-HEADERS — it is cleared the moment `fetch` resolves — so without
|
|
35
|
+
* these a compromised or misconfigured endpoint could dribble bytes forever
|
|
36
|
+
* and hang `akm upgrade` with no output and no way out but Ctrl-C. Two
|
|
37
|
+
* bounds, because either alone is evadable: a per-chunk STALL deadline (a
|
|
38
|
+
* connection making no progress dies quickly, while a slow-but-progressing
|
|
39
|
+
* one is left alone) and an OVERALL deadline (a trickle that sends one byte
|
|
40
|
+
* per stall-window can't stretch the download indefinitely).
|
|
41
|
+
*/
|
|
42
|
+
const BINARY_STALL_TIMEOUT_MS = 60_000;
|
|
43
|
+
const BINARY_TOTAL_TIMEOUT_MS = 30 * 60_000;
|
|
44
|
+
export async function streamResponseToFile(response, destination, maxBytes, limits) {
|
|
33
45
|
const declaredText = response.headers.get("content-length");
|
|
34
46
|
if (declaredText) {
|
|
35
47
|
const declared = Number(declaredText);
|
|
@@ -40,22 +52,30 @@ async function streamResponseToFile(response, destination, maxBytes) {
|
|
|
40
52
|
}
|
|
41
53
|
if (!response.body)
|
|
42
54
|
throw new Error("Binary download response did not provide a streaming body.");
|
|
55
|
+
const url = response.url || "binary download";
|
|
56
|
+
const stallTimeoutMs = limits?.stallTimeoutMs ?? BINARY_STALL_TIMEOUT_MS;
|
|
57
|
+
const totalTimeoutMs = limits?.totalTimeoutMs ?? BINARY_TOTAL_TIMEOUT_MS;
|
|
58
|
+
const overallDeadlineAt = Date.now() + totalTimeoutMs;
|
|
43
59
|
const fd = fs.openSync(destination, "w", 0o600);
|
|
44
60
|
const reader = response.body.getReader();
|
|
45
61
|
const hash = createHash("sha256");
|
|
46
62
|
let byteSize = 0;
|
|
47
63
|
try {
|
|
48
64
|
while (true) {
|
|
49
|
-
|
|
65
|
+
// Whichever bound bites first wins, and the error reports THAT bound —
|
|
66
|
+
// "no data for 60s" and "download exceeded 30m" are different diagnoses.
|
|
67
|
+
const stallDeadlineAt = Date.now() + stallTimeoutMs;
|
|
68
|
+
const deadlineAt = Math.min(stallDeadlineAt, overallDeadlineAt);
|
|
69
|
+
const reportedTimeoutMs = deadlineAt === overallDeadlineAt ? totalTimeoutMs : stallTimeoutMs;
|
|
70
|
+
const { done, value } = await readChunkWithDeadline(reader, deadlineAt, undefined, url, reportedTimeoutMs);
|
|
50
71
|
if (done)
|
|
51
72
|
break;
|
|
52
73
|
if (!value)
|
|
53
74
|
continue;
|
|
54
75
|
byteSize += value.byteLength;
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
throw new ResponseTooLargeError(
|
|
58
|
-
}
|
|
76
|
+
// The catch block cancels the reader for every throw path, this included.
|
|
77
|
+
if (byteSize > maxBytes)
|
|
78
|
+
throw new ResponseTooLargeError(url, maxBytes, byteSize);
|
|
59
79
|
hash.update(value);
|
|
60
80
|
let written = 0;
|
|
61
81
|
while (written < value.byteLength)
|
|
@@ -65,6 +85,9 @@ async function streamResponseToFile(response, destination, maxBytes) {
|
|
|
65
85
|
return { byteSize, sha256: hash.digest("hex") };
|
|
66
86
|
}
|
|
67
87
|
catch (error) {
|
|
88
|
+
// Cancel so a timed-out (still-open) connection releases its socket rather
|
|
89
|
+
// than lingering until process exit. Already-cancelled readers no-op.
|
|
90
|
+
await reader.cancel().catch(() => undefined);
|
|
68
91
|
removeFileBestEffort(destination);
|
|
69
92
|
throw error;
|
|
70
93
|
}
|
|
@@ -14,6 +14,7 @@ import { readLockfile, removeLockEntry, upsertLockEntry } from "../../integratio
|
|
|
14
14
|
import { parseRegistryRef } from "../../registry/resolve.js";
|
|
15
15
|
import { detectStashRoot } from "../../sources/providers/provider-utils.js";
|
|
16
16
|
import { syncFromRef } from "../../sources/providers/sync-from-ref.js";
|
|
17
|
+
import { storeSecretResolver } from "../../sources/snapshot-fetchers/secret-seam.js";
|
|
17
18
|
import { ensureWebsiteMirror, shouldAllowPrivateWebsiteUrlForTests, validateWebsiteInputUrl, } from "../../sources/snapshot-fetchers/website-ingest.js";
|
|
18
19
|
import { bundleKeyForPath, bundleKeyForUrl, nextBundleKey } from "./bundle-config-ops.js";
|
|
19
20
|
export async function akmAdd(input) {
|
|
@@ -95,13 +96,26 @@ async function addLocalSource(ref, sourcePath, stashDir, explicitName) {
|
|
|
95
96
|
async function addWebsiteSource(ref, stashDir, name, options) {
|
|
96
97
|
const allowPrivateHosts = shouldAllowPrivateWebsiteUrlForTests(ref);
|
|
97
98
|
const normalizedUrl = validateWebsiteInputUrl(ref, { allowPrivateHosts });
|
|
98
|
-
const
|
|
99
|
+
const numberOption = (value) => (typeof value === "number" ? value : undefined);
|
|
100
|
+
const maxPages = numberOption(options?.maxPages);
|
|
101
|
+
const maxDepth = numberOption(options?.maxDepth);
|
|
99
102
|
let entry;
|
|
100
103
|
mutateConfig((config) => {
|
|
101
104
|
const bundles = { ...(config.bundles ?? {}) };
|
|
102
105
|
const existingKey = bundleKeyForUrl(config, normalizedUrl);
|
|
103
106
|
const key = existingKey ?? nextBundleKey(bundles, name ?? toWebsiteName(normalizedUrl), normalizedUrl);
|
|
104
|
-
|
|
107
|
+
// Merge onto the existing descriptor rather than replacing it: re-running
|
|
108
|
+
// `bundle add` for a URL that already has a bundle would otherwise drop
|
|
109
|
+
// respectRobots / refresh, silently restoring default robots enforcement
|
|
110
|
+
// and changing what the next update fetches. Explicitly-passed maxPages /
|
|
111
|
+
// maxDepth still win over the stored values.
|
|
112
|
+
const existingWebsite = existingKey ? (bundles[key]?.website ?? {}) : {};
|
|
113
|
+
const website = {
|
|
114
|
+
...existingWebsite,
|
|
115
|
+
url: normalizedUrl,
|
|
116
|
+
...(maxPages !== undefined ? { maxPages } : {}),
|
|
117
|
+
...(maxDepth !== undefined ? { maxDepth } : {}),
|
|
118
|
+
};
|
|
105
119
|
const nextBundle = {
|
|
106
120
|
...(existingKey ? bundles[key] : {}),
|
|
107
121
|
website,
|
|
@@ -117,6 +131,7 @@ async function addWebsiteSource(ref, stashDir, name, options) {
|
|
|
117
131
|
});
|
|
118
132
|
const cachePaths = await ensureWebsiteMirror(entry, {
|
|
119
133
|
requireStashDir: true,
|
|
134
|
+
resolveSecret: storeSecretResolver.resolveSecret,
|
|
120
135
|
...(allowPrivateHosts ? { allowPrivateHosts: true } : {}),
|
|
121
136
|
});
|
|
122
137
|
const index = await akmIndex({ stashDir });
|
|
@@ -60,7 +60,12 @@ export async function akmTasksAdd(input, deps = {}) {
|
|
|
60
60
|
if (!isWithin(assetPath, typeRoot)) {
|
|
61
61
|
throw new UsageError(`Resolved task path escapes the stash: "${id}".`, "PATH_ESCAPE_VIOLATION");
|
|
62
62
|
}
|
|
63
|
-
|
|
63
|
+
// Pre-0.8.0 tasks were markdown; the 0.8.0 cutover moved them to pure YAML
|
|
64
|
+
// (see the tasks dir rule in src/indexer/walk/matchers.ts). A leftover
|
|
65
|
+
// `<id>.md` still names the same task, so creating `<id>.yml` beside it
|
|
66
|
+
// must collide loudly rather than silently minting a duplicate.
|
|
67
|
+
const legacyAssetPath = path.join(typeRoot, `${id}.md`);
|
|
68
|
+
if ((fs.existsSync(assetPath) || fs.existsSync(legacyAssetPath)) && !input.force) {
|
|
64
69
|
throw new UsageError(`Task "${id}" already exists. Pass --force to overwrite, or delete its file and run \`akm task sync\` first.`, "RESOURCE_ALREADY_EXISTS");
|
|
65
70
|
}
|
|
66
71
|
const yaml = renderTaskYaml({
|
|
@@ -408,8 +413,8 @@ export async function akmTasksDoctor(deps = {}) {
|
|
|
408
413
|
configKey: IMPROVE_AUTONOMY_CONFIG_KEY,
|
|
409
414
|
gatedLanes: allGated.map((entry) => ({ lane: entry.lane, reason: entry.reason })),
|
|
410
415
|
};
|
|
411
|
-
// Q-05 — report the
|
|
412
|
-
//
|
|
416
|
+
// Q-05 — report the external-driver gate alongside the autonomy gate: both
|
|
417
|
+
// are `experimental.*` opt-ins doctor exists to surface.
|
|
413
418
|
const workflowEngine = {
|
|
414
419
|
enabled: isWorkflowEngineEnabled(config),
|
|
415
420
|
configKey: WORKFLOW_ENGINE_CONFIG_KEY,
|