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
|
@@ -27,20 +27,12 @@ export const ExperimentalConfigSchema = z
|
|
|
27
27
|
*/
|
|
28
28
|
improveAutonomy: z.boolean().optional(),
|
|
29
29
|
/**
|
|
30
|
-
* Allow the
|
|
30
|
+
* Allow the harness-neutral workflow external-driver protocol (Q-05).
|
|
31
31
|
*
|
|
32
|
-
* OFF by default. Gates `akm workflow
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
* Deliberately NOT gated: authoring/linting the unified markdown format
|
|
37
|
-
* and the manual workflow CLI contract (`start`/`next`/`complete`/
|
|
38
|
-
* `status`/`list`/`create`/`resume`/`abandon`) remain stable regardless of
|
|
39
|
-
* this key.
|
|
40
|
-
*
|
|
41
|
-
* Unlike `improveAutonomy`, a gated call here REFUSES outright rather than
|
|
42
|
-
* degrading: a workflow step either executes or it does not, so there is
|
|
43
|
-
* no safe partial-execution fallback to fall back to.
|
|
32
|
+
* OFF by default. Gates only `akm workflow brief`/`report`. Stable native
|
|
33
|
+
* orchestration through `workflow run`, authoring/linting, inspection, and
|
|
34
|
+
* recovery remain available regardless of this key. A gated driver call
|
|
35
|
+
* refuses outright rather than degrading.
|
|
44
36
|
*/
|
|
45
37
|
workflowEngine: z.boolean().optional(),
|
|
46
38
|
})
|
|
@@ -80,6 +80,17 @@ const BundleWebsiteDescriptorSchema = z
|
|
|
80
80
|
refresh: z.string().min(1).optional(),
|
|
81
81
|
maxPages: positiveInt.optional(),
|
|
82
82
|
maxDepth: positiveInt.optional(),
|
|
83
|
+
// Default true: crawl-scoped robots.txt compliance (Disallow/Crawl-delay
|
|
84
|
+
// for the akm/akm-cli product tokens or "*"). See
|
|
85
|
+
// src/sources/snapshot-fetchers/robots.ts. Opt out with `false` to
|
|
86
|
+
// restore pre-P1 behavior exactly (no /robots.txt request at all).
|
|
87
|
+
respectRobots: z.boolean().optional(),
|
|
88
|
+
// Hard wall-clock cap on the whole crawl, in milliseconds. Defaults to
|
|
89
|
+
// 10 minutes. Unlike a between-page check, this aborts work already in
|
|
90
|
+
// flight — including a `Retry-After` sleep, which a server can otherwise
|
|
91
|
+
// make arbitrarily long. Set to 0 to disable the cap entirely for a
|
|
92
|
+
// deliberately long-running crawl.
|
|
93
|
+
crawlTimeoutMs: z.number().int().min(0).optional(),
|
|
83
94
|
})
|
|
84
95
|
.passthrough();
|
|
85
96
|
/** One component of a bundle (spec §10.1). Single-entry, transitional. */
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* `config-schema.ts` monolith — no behavior change.
|
|
7
7
|
*/
|
|
8
8
|
import { z } from "zod";
|
|
9
|
-
import { positiveInt } from "./primitives.js";
|
|
9
|
+
import { engineName, positiveInt } from "./primitives.js";
|
|
10
10
|
// ── Workflow engine ─────────────────────────────────────────────────────────
|
|
11
11
|
/**
|
|
12
12
|
* Workflow-engine settings (`workflow`).
|
|
@@ -25,5 +25,7 @@ import { positiveInt } from "./primitives.js";
|
|
|
25
25
|
export const WorkflowConfigSchema = z
|
|
26
26
|
.object({
|
|
27
27
|
maxConcurrency: positiveInt.optional(),
|
|
28
|
+
/** Named LLM or agent engine frozen into every criteria-bearing gate. */
|
|
29
|
+
judgeEngine: engineName.optional(),
|
|
28
30
|
})
|
|
29
31
|
.passthrough();
|
package/dist/core/errors.js
CHANGED
|
@@ -29,12 +29,17 @@ const USAGE_HINTS = {
|
|
|
29
29
|
TARGET_NOT_UPDATABLE: "Run `akm bundle list` to view your sources, then retry with one of those values.",
|
|
30
30
|
MISSING_REQUIRED_ARGUMENT: "Refs use the form [bundle//]conceptId, e.g. `akm show knowledge/guide.md` or `akm show skills/deploy`.",
|
|
31
31
|
UNKNOWN_COMMAND: "Run `akm --help` to see available commands.",
|
|
32
|
+
UNKNOWN_FLAG: "Run the command with `--help` to see its accepted flags.",
|
|
32
33
|
};
|
|
33
34
|
/** Default hint for each NotFoundError code. */
|
|
34
35
|
const NOT_FOUND_HINTS = {
|
|
35
36
|
ASSET_NOT_FOUND: "Run `akm search <query>` or `akm index` to refresh the index.",
|
|
36
37
|
SOURCE_NOT_FOUND: "Run `akm bundle list` to view your sources, then retry with one of those values.",
|
|
37
38
|
WORKFLOW_NOT_FOUND: "Run `akm workflow list --active` to see runs.",
|
|
39
|
+
// A proposal is addressed by id or ref, never by path — reusing
|
|
40
|
+
// FILE_NOT_FOUND here handed users "check the path exists and is readable"
|
|
41
|
+
// for a mistyped id, which points at the wrong thing entirely.
|
|
42
|
+
PROPOSAL_NOT_FOUND: "Run `akm proposal list` to see pending proposals and their ids.",
|
|
38
43
|
FILE_NOT_FOUND: "Check the path exists and is readable.",
|
|
39
44
|
};
|
|
40
45
|
/**
|
package/dist/core/logs-db.js
CHANGED
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
* @module logs-db
|
|
39
39
|
*/
|
|
40
40
|
import path from "node:path";
|
|
41
|
-
import { runMigrations as runSqliteMigrations } from "../storage/engines/sqlite-migrations.js";
|
|
41
|
+
import { assertMigrationRegistry, runMigrations as runSqliteMigrations, } from "../storage/engines/sqlite-migrations.js";
|
|
42
42
|
import { openManagedDatabase } from "../storage/managed-db.js";
|
|
43
43
|
import { getDataDir } from "./paths.js";
|
|
44
44
|
// ── Path helper ──────────────────────────────────────────────────────────────
|
|
@@ -130,6 +130,7 @@ const MIGRATIONS = [
|
|
|
130
130
|
`,
|
|
131
131
|
},
|
|
132
132
|
];
|
|
133
|
+
assertMigrationRegistry(MIGRATIONS);
|
|
133
134
|
/**
|
|
134
135
|
* Apply every pending migration. Called automatically by
|
|
135
136
|
* {@link openLogsDatabase}; exported for the same test seams state-db exposes.
|
package/dist/core/parse.js
CHANGED
|
@@ -23,7 +23,10 @@
|
|
|
23
23
|
* models). Also strips leading/trailing whitespace.
|
|
24
24
|
*/
|
|
25
25
|
export function stripThinkBlocks(raw) {
|
|
26
|
-
return raw
|
|
26
|
+
return raw
|
|
27
|
+
.replace(/<think>[\s\S]*?<\/think>/gi, "")
|
|
28
|
+
.replace(/^[\s\S]*?<\/think>/i, "")
|
|
29
|
+
.trim();
|
|
27
30
|
}
|
|
28
31
|
/**
|
|
29
32
|
* Strips markdown code fences (``` or ~~~, with optional language tag).
|
|
@@ -1,7 +1,7 @@
|
|
|
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 { runMigrations as runSqliteMigrations } from "../../storage/engines/sqlite-migrations.js";
|
|
4
|
+
import { assertMigrationRegistry, runMigrations as runSqliteMigrations, } from "../../storage/engines/sqlite-migrations.js";
|
|
5
5
|
export const STATE_MIGRATIONS = [
|
|
6
6
|
// ── Migration 001 — initial schema ──────────────────────────────────────────
|
|
7
7
|
{
|
|
@@ -59,7 +59,7 @@ export const STATE_MIGRATIONS = [
|
|
|
59
59
|
-- id TEXT PK — UUID (crypto.randomUUID()); stable directory name.
|
|
60
60
|
-- stash_dir TEXT — absolute stash root; multi-stash installs need
|
|
61
61
|
-- this to partition proposal lists per stash.
|
|
62
|
-
-- ref TEXT — target asset ref (e.g. "
|
|
62
|
+
-- ref TEXT — target asset ref (e.g. "lessons/alpha");
|
|
63
63
|
-- indexed for ref-scoped queue views.
|
|
64
64
|
-- status TEXT — "pending" | "accepted" | "rejected"; indexed
|
|
65
65
|
-- so pending-queue queries are fast.
|
|
@@ -76,7 +76,7 @@ export const STATE_MIGRATIONS = [
|
|
|
76
76
|
-- metadata_json TEXT — JSON object for future proposal fields.
|
|
77
77
|
-- Current fields stored here: sourceRun,
|
|
78
78
|
-- review, confidence, gateDecision (#577),
|
|
79
|
-
-- backupContent
|
|
79
|
+
-- backupContent.
|
|
80
80
|
--
|
|
81
81
|
-- ADD COLUMN extension points (future migrations):
|
|
82
82
|
-- ALTER TABLE proposals ADD COLUMN source_run TEXT DEFAULT NULL;
|
|
@@ -353,9 +353,9 @@ export const STATE_MIGRATIONS = [
|
|
|
353
353
|
// (`scripts/akm-migrate/migrate/legacy/proposal-fs-import.ts`, wired through
|
|
354
354
|
// `akm-migrate apply`),
|
|
355
355
|
// whose idempotency is INSERT OR IGNORE on the proposal UUID plus migrate-apply's
|
|
356
|
-
//
|
|
357
|
-
// stays because
|
|
358
|
-
//
|
|
356
|
+
// incomplete sentinel — it no longer reads or writes this ledger. The CREATE TABLE
|
|
357
|
+
// stays because migration IDs are append-only: removing a released migration
|
|
358
|
+
// would make the schema_migrations ledger of an
|
|
359
359
|
// already-migrated rc database stop being an exact ordered prefix, and the
|
|
360
360
|
// runner would refuse to open it. The empty table is harmless.
|
|
361
361
|
//
|
|
@@ -833,12 +833,11 @@ export const STATE_MIGRATIONS = [
|
|
|
833
833
|
//
|
|
834
834
|
// The state.db half of the three-DB merge (plan §3.2/§8, normative §11.4,
|
|
835
835
|
// chunk-8 cutover design §1). This migration is PURE,
|
|
836
|
-
//
|
|
836
|
+
// IDEMPOTENT DDL ONLY — `CREATE TABLE IF NOT EXISTS` (+ indexes),
|
|
837
837
|
// never a DROP or a data move. The actual data movement (the workflow.db merge,
|
|
838
838
|
// the usage_events rescue from index.db, the full old-ref→item_ref re-key, and
|
|
839
|
-
// the workflow.db delete / index.db quarantine) is
|
|
840
|
-
//
|
|
841
|
-
// `cutover-applied` phase), driven by
|
|
839
|
+
// the workflow.db delete / index.db quarantine) is idempotent migrate-apply
|
|
840
|
+
// code driven by
|
|
842
841
|
// `scripts/akm-migrate/migrate/legacy/three-db-cutover.ts`. See the no-DROP
|
|
843
842
|
// contract carve-out note in `src/core/state-db.ts`.
|
|
844
843
|
//
|
|
@@ -985,6 +984,7 @@ export const STATE_MIGRATIONS = [
|
|
|
985
984
|
`,
|
|
986
985
|
},
|
|
987
986
|
];
|
|
987
|
+
assertMigrationRegistry(STATE_MIGRATIONS);
|
|
988
988
|
/**
|
|
989
989
|
* Apply every pending migration in a single transaction per migration.
|
|
990
990
|
*
|
|
@@ -994,8 +994,5 @@ export const STATE_MIGRATIONS = [
|
|
|
994
994
|
* Called automatically by `openStateDatabase()`.
|
|
995
995
|
*/
|
|
996
996
|
export function runMigrations(db, options) {
|
|
997
|
-
runSqliteMigrations(db, STATE_MIGRATIONS, {
|
|
998
|
-
applyPending: options?.applyPending,
|
|
999
|
-
generationMarker: options?.generationMarker,
|
|
1000
|
-
});
|
|
997
|
+
runSqliteMigrations(db, STATE_MIGRATIONS, { applyPending: options?.applyPending });
|
|
1001
998
|
}
|
package/dist/core/state-db.js
CHANGED
|
@@ -45,12 +45,10 @@
|
|
|
45
45
|
* merge-target tables at their final shape. The one-time, filesystem-derived,
|
|
46
46
|
* fail-closed DATA movement (the workflow.db merge, the usage_events rescue, the
|
|
47
47
|
* full old-ref→item_ref re-key, and the workflow.db unlink / index.db quarantine
|
|
48
|
-
* rename)
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
* contract here is intact — the physical workflow.db deletion happens outside
|
|
53
|
-
* the ledger DDL, under the backup-verified-restorable fail-closed gate.
|
|
48
|
+
* rename) runs as idempotent code in the migrate-apply coordinator
|
|
49
|
+
* (`scripts/akm-migrate/migrate/legacy/three-db-cutover.ts`). So the no-DROP
|
|
50
|
+
* contract here is intact: physical workflow.db deletion happens outside the
|
|
51
|
+
* ledger DDL, after a verified backup and committed data transaction.
|
|
54
52
|
*
|
|
55
53
|
* ## Schema design: indexed columns vs. metadata_json
|
|
56
54
|
*
|
package/dist/core/subprocess.js
CHANGED
|
@@ -148,6 +148,7 @@ export async function readStream(stream, opts) {
|
|
|
148
148
|
}
|
|
149
149
|
}
|
|
150
150
|
const EMPTY_READ = { text: "", timedOut: false };
|
|
151
|
+
const UNBOUNDED_STREAM_READ_SAFETY_MS = 60 * 60 * 1000;
|
|
151
152
|
function toError(err) {
|
|
152
153
|
return err instanceof Error ? err : new Error(String(err));
|
|
153
154
|
}
|
|
@@ -238,10 +239,11 @@ export async function runManagedSubprocess(cmd, opts) {
|
|
|
238
239
|
else
|
|
239
240
|
abortSignal.addEventListener("abort", onAbort, { once: true });
|
|
240
241
|
}
|
|
241
|
-
// Stream-drain timeout: the wall budget plus a 2 s grace, or
|
|
242
|
-
//
|
|
243
|
-
//
|
|
244
|
-
|
|
242
|
+
// Stream-drain timeout: the wall budget plus a 2 s grace, or a one-hour
|
|
243
|
+
// safety bound when execution itself is unbounded. This timer starts with
|
|
244
|
+
// capture, so the null-timeout path must not impose a short hidden deadline
|
|
245
|
+
// on an otherwise healthy long-running process.
|
|
246
|
+
const streamDrainTimeoutMs = timeoutMs !== null ? timeoutMs + 2_000 : UNBOUNDED_STREAM_READ_SAFETY_MS;
|
|
245
247
|
const stdoutPromise = capture
|
|
246
248
|
? readStream(proc.stdout ?? null, {
|
|
247
249
|
timeoutMs: streamDrainTimeoutMs,
|
|
@@ -38,7 +38,7 @@ function shellQuote(value) {
|
|
|
38
38
|
}
|
|
39
39
|
/** Reproduced from `output/renderers.ts#buildWorkflowAction` — the one function-valued `ACTION_BUILDERS.workflow` entry. */
|
|
40
40
|
function buildWorkflowAction(ref) {
|
|
41
|
-
return `
|
|
41
|
+
return `Start or resume execution with \`akm workflow run ${shellQuote(ref)}\`.`;
|
|
42
42
|
}
|
|
43
43
|
/**
|
|
44
44
|
* Exhaustive over {@link KnownType} — a HAND-WRITTEN literal, not derived
|
package/dist/indexer/indexer.js
CHANGED
|
@@ -419,12 +419,27 @@ async function akmIndexReal(options) {
|
|
|
419
419
|
// Load config and resolve all stash sources
|
|
420
420
|
const { loadConfig, mutateConfig } = await import("../core/config/config.js");
|
|
421
421
|
let config = loadConfig();
|
|
422
|
+
// Durable state must be runtime-compatible before source hydration,
|
|
423
|
+
// adapter persistence, or index.db creation can mutate the installation.
|
|
424
|
+
onProgress({ phase: "preflight", message: "Validating durable state." });
|
|
425
|
+
withStateDb(() => undefined);
|
|
422
426
|
// Ensure git stash caches are extracted before resolving stash dirs,
|
|
423
427
|
// so their content directories exist on disk for the walker to discover.
|
|
424
428
|
const sourceCacheStart = Date.now();
|
|
425
429
|
onProgress({ phase: "preflight", message: "Hydrating source caches." });
|
|
426
430
|
const { ensureSourceCaches, resolveSourceEntries } = await import("./search/search-source.js");
|
|
427
|
-
|
|
431
|
+
// Inject the store-backed secret resolver from here — a composition root
|
|
432
|
+
// ABOVE the provider/fetcher import cycle (this module reaches
|
|
433
|
+
// search-source only via dynamic import). This is what lets a website
|
|
434
|
+
// source's X fetcher resolve `secrets/x-bearer-token` during
|
|
435
|
+
// bundle-update / hydrate, not just from the command-layer URL-ingest
|
|
436
|
+
// path. `secret-seam` is imported here, never from inside the cycle.
|
|
437
|
+
const { storeSecretResolver } = await import("../sources/snapshot-fetchers/secret-seam.js");
|
|
438
|
+
await ensureSourceCaches(config, {
|
|
439
|
+
force: full,
|
|
440
|
+
materialize: options.hydrateSources !== false,
|
|
441
|
+
secrets: storeSecretResolver,
|
|
442
|
+
});
|
|
428
443
|
const sourceCacheEnd = Date.now();
|
|
429
444
|
const allSourceEntries = resolveSourceEntries(stashDir, config);
|
|
430
445
|
const detected = detectAndPersistBundleAdapters(allSourceEntries, config, mutateConfig, {
|
|
@@ -304,7 +304,7 @@ export async function ensureSourceCaches(config, options) {
|
|
|
304
304
|
continue;
|
|
305
305
|
}
|
|
306
306
|
try {
|
|
307
|
-
await provider.sync({ force });
|
|
307
|
+
await provider.sync({ force, secrets: options?.secrets });
|
|
308
308
|
}
|
|
309
309
|
catch (err) {
|
|
310
310
|
warn(`Warning: failed to refresh ${provider.kind} source "${provider.name}": ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -188,7 +188,9 @@ function classifyBySmartMd(ctx) {
|
|
|
188
188
|
return { type: "workflow", specificity: 19 };
|
|
189
189
|
}
|
|
190
190
|
if (fm) {
|
|
191
|
-
|
|
191
|
+
// `tools` is the one authoring key for an agent's tool grant. Recognition
|
|
192
|
+
// covers only the key the renderer honors.
|
|
193
|
+
if ("tools" in fm) {
|
|
192
194
|
return { type: "agent", specificity: 20 };
|
|
193
195
|
}
|
|
194
196
|
if ("agent" in fm) {
|
|
@@ -1,7 +1,7 @@
|
|
|
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
|
-
/** Default
|
|
5
|
-
export const DEFAULT_AGENT_TIMEOUT_MS =
|
|
4
|
+
/** Default agent CLI timeout; null means agents run until they finish. */
|
|
5
|
+
export const DEFAULT_AGENT_TIMEOUT_MS = null;
|
|
6
6
|
/** Default hard timeout for direct LLM calls when no engine/use override exists. */
|
|
7
7
|
export const DEFAULT_LLM_TIMEOUT_MS = 600_000;
|
|
@@ -15,26 +15,34 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import fs from "node:fs";
|
|
17
17
|
import path from "node:path";
|
|
18
|
-
import { getBuiltinAgentProfile, listBuiltinAgentProfiles } from "./profiles.js";
|
|
18
|
+
import { getBuiltinAgentProfile, listBuiltinAgentProfiles, OPENCODE_SDK_SERVER_BIN, } from "./profiles.js";
|
|
19
19
|
/**
|
|
20
20
|
* Default PATH lookup. Walks `process.env.PATH` and returns the first
|
|
21
21
|
* existing executable file. Returns `undefined` when the bin is not on
|
|
22
22
|
* PATH or the env is empty.
|
|
23
23
|
*
|
|
24
|
-
* `process.env.PATH` is split on the platform-correct delimiter
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* match
|
|
24
|
+
* `process.env.PATH` is split on the platform-correct delimiter. On Windows
|
|
25
|
+
* agent CLIs install as extension-bearing shims (`claude.cmd`, `q.exe`), so
|
|
26
|
+
* each PATH entry is probed for the bare name AND for `<bin><ext>` over
|
|
27
|
+
* PATHEXT — an exact-match-only probe reports every agent CLI as missing
|
|
28
|
+
* there, which silently hides the "installed CLI agent" option during setup.
|
|
28
29
|
*/
|
|
29
30
|
export function defaultWhich(bin, envSource = process.env) {
|
|
31
|
+
// Computed once per call — not per PATH entry — since it never varies.
|
|
32
|
+
const suffixes = executableSuffixes(envSource);
|
|
30
33
|
if (!bin || bin.includes("/") || bin.includes("\\")) {
|
|
31
34
|
// Absolute / relative paths: caller already specified location.
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
for (const suffix of suffixes) {
|
|
36
|
+
const candidate = bin + suffix;
|
|
37
|
+
try {
|
|
38
|
+
if (fs.statSync(candidate).isFile())
|
|
39
|
+
return candidate;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
/* try the next extension */
|
|
43
|
+
}
|
|
37
44
|
}
|
|
45
|
+
return undefined;
|
|
38
46
|
}
|
|
39
47
|
const pathVar = envSource.PATH ?? envSource.Path ?? envSource.path ?? "";
|
|
40
48
|
if (!pathVar)
|
|
@@ -43,18 +51,40 @@ export function defaultWhich(bin, envSource = process.env) {
|
|
|
43
51
|
for (const dir of pathVar.split(sep)) {
|
|
44
52
|
if (!dir)
|
|
45
53
|
continue;
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
+
const base = path.join(dir, bin);
|
|
55
|
+
for (const suffix of suffixes) {
|
|
56
|
+
const candidate = base + suffix;
|
|
57
|
+
try {
|
|
58
|
+
const st = fs.statSync(candidate);
|
|
59
|
+
if (st.isFile())
|
|
60
|
+
return candidate;
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
/* keep walking */
|
|
64
|
+
}
|
|
54
65
|
}
|
|
55
66
|
}
|
|
56
67
|
return undefined;
|
|
57
68
|
}
|
|
69
|
+
/** Windows PATHEXT default, used when the env carries no explicit list. */
|
|
70
|
+
const DEFAULT_PATHEXT = ".COM;.EXE;.BAT;.CMD";
|
|
71
|
+
/**
|
|
72
|
+
* Executable suffixes to try for each candidate path, bare name (`""`) first
|
|
73
|
+
* so POSIX resolution is byte-for-byte unchanged. Extensions are appended only
|
|
74
|
+
* on win32 or when the env supplies PATHEXT (which is also the seam tests use
|
|
75
|
+
* to cover Windows resolution from a POSIX runner).
|
|
76
|
+
*/
|
|
77
|
+
function executableSuffixes(envSource) {
|
|
78
|
+
const pathext = envSource.PATHEXT ?? envSource.Pathext ?? envSource.pathext;
|
|
79
|
+
if (process.platform !== "win32" && !pathext)
|
|
80
|
+
return [""];
|
|
81
|
+
const exts = (pathext ?? DEFAULT_PATHEXT)
|
|
82
|
+
.split(";")
|
|
83
|
+
.map((ext) => ext.trim())
|
|
84
|
+
.filter(Boolean)
|
|
85
|
+
.map((ext) => (ext.startsWith(".") ? ext : `.${ext}`));
|
|
86
|
+
return ["", ...exts];
|
|
87
|
+
}
|
|
58
88
|
let detectOverrides;
|
|
59
89
|
/** TEST-ONLY. Swap the detection implementations; pass undefined to restore. */
|
|
60
90
|
export function _setAgentDetectForTests(fakes) {
|
|
@@ -79,7 +109,7 @@ export function detectAgentCliProfiles(agent, whichFn = defaultWhich) {
|
|
|
79
109
|
profilesByName.set(name, {
|
|
80
110
|
name,
|
|
81
111
|
platform: engine.platform,
|
|
82
|
-
bin: engine.bin ?? builtin?.bin ?? (engine.platform === "opencode-sdk" ?
|
|
112
|
+
bin: engine.bin ?? builtin?.bin ?? (engine.platform === "opencode-sdk" ? OPENCODE_SDK_SERVER_BIN : engine.platform),
|
|
83
113
|
args: engine.args ?? builtin?.args ?? [],
|
|
84
114
|
stdio: "captured",
|
|
85
115
|
envPassthrough: builtin?.envPassthrough ?? [],
|
|
@@ -91,6 +91,16 @@ const BUILTINS = {
|
|
|
91
91
|
parseOutput: "text",
|
|
92
92
|
},
|
|
93
93
|
};
|
|
94
|
+
/**
|
|
95
|
+
* Binary the `opencode-sdk` harness needs on PATH.
|
|
96
|
+
*
|
|
97
|
+
* The embedded SDK is not self-contained: its runner spawns `opencode serve`
|
|
98
|
+
* and talks HTTP to it (see `harnesses/opencode-sdk/sdk-runner.ts`), so the
|
|
99
|
+
* `opencode` binary gates the SDK path exactly as it gates the CLI path.
|
|
100
|
+
* `opencode-sdk` deliberately has no {@link BUILTINS} entry — it dispatches
|
|
101
|
+
* without argv construction — so this is the one place that pairing lives.
|
|
102
|
+
*/
|
|
103
|
+
export const OPENCODE_SDK_SERVER_BIN = "opencode";
|
|
94
104
|
/** Names of the canonical built-in harness descriptors. Stable, sorted. */
|
|
95
105
|
export const BUILTIN_AGENT_PROFILE_NAMES = Object.freeze(Object.keys(BUILTINS).sort());
|
|
96
106
|
/** Returns the built-in descriptor for a canonical harness id. */
|
|
@@ -21,7 +21,6 @@ import { parseEmbeddedJsonResponse } from "../../core/parse.js";
|
|
|
21
21
|
import { runManagedSubprocess, } from "../../core/subprocess.js";
|
|
22
22
|
import { getCommandBuilder } from "./builders.js";
|
|
23
23
|
import { DEFAULT_AGENT_TIMEOUT_MS } from "./config.js";
|
|
24
|
-
const DEFAULT_TIMEOUT_MS = DEFAULT_AGENT_TIMEOUT_MS;
|
|
25
24
|
/**
|
|
26
25
|
* Supplement `existingPath` with well-known user binary directories when
|
|
27
26
|
* running in a scheduler context (cron/launchd) where PATH is stripped.
|
|
@@ -144,7 +143,7 @@ function streamFailureMessage(profileName, stdout, stderr) {
|
|
|
144
143
|
export async function runAgent(profile, prompt, options = {}) {
|
|
145
144
|
const stdioMode = options.stdio ?? profile.stdio;
|
|
146
145
|
// null = explicitly disabled (no kill timer). undefined = runtime default.
|
|
147
|
-
const timeoutMs = options.timeoutMs !== undefined ? options.timeoutMs :
|
|
146
|
+
const timeoutMs = options.timeoutMs !== undefined ? options.timeoutMs : DEFAULT_AGENT_TIMEOUT_MS;
|
|
148
147
|
const parseOutput = options.parseOutput ?? profile.parseOutput;
|
|
149
148
|
const setTimeoutImpl = options.setTimeoutFn ?? setTimeout;
|
|
150
149
|
const clearTimeoutImpl = options.clearTimeoutFn ?? clearTimeout;
|
|
@@ -4,9 +4,13 @@
|
|
|
4
4
|
/**
|
|
5
5
|
* OpenCode SDK agent runner (migrated from `agent/sdk-runner.ts`, #564).
|
|
6
6
|
*
|
|
7
|
-
* Uses the embedded `@opencode-ai/sdk` instead of `Bun.spawn
|
|
8
|
-
* agent CLI
|
|
9
|
-
*
|
|
7
|
+
* Uses the embedded `@opencode-ai/sdk` instead of `Bun.spawn` for dispatch, so
|
|
8
|
+
* no agent CLI argv is ever constructed — but the SDK is not self-contained:
|
|
9
|
+
* {@link createManagedOpencode} spawns `opencode serve` and talks HTTP to it,
|
|
10
|
+
* so the `opencode` binary must be on PATH (which is why setup's
|
|
11
|
+
* `detectHarness` gates this harness on that probe). The user provides an
|
|
12
|
+
* OpenAI-compatible endpoint (or inherits from the selected fallback LLM
|
|
13
|
+
* engine) for the SDK.
|
|
10
14
|
*
|
|
11
15
|
* This is the runtime surface of the {@link OpencodeSdkHarness} (`id =
|
|
12
16
|
* 'opencode-sdk'`). It is the dispatch path for SDK runner specs; it exposes
|
|
@@ -169,9 +169,7 @@ export async function upsertLockEntry(entry) {
|
|
|
169
169
|
* migrate-apply's config lock + maintenance barrier) whose synchronous body
|
|
170
170
|
* cannot await the sentinel's retry loop. No-op for an empty list.
|
|
171
171
|
*/
|
|
172
|
-
|
|
173
|
-
if (entries.length === 0)
|
|
174
|
-
return;
|
|
172
|
+
function readLockEntriesForMigration() {
|
|
175
173
|
let existing = [];
|
|
176
174
|
const lockfilePath = getLockfilePath();
|
|
177
175
|
if (fs.existsSync(lockfilePath)) {
|
|
@@ -187,6 +185,16 @@ export function mergeLockEntriesSync(entries) {
|
|
|
187
185
|
}
|
|
188
186
|
existing = raw;
|
|
189
187
|
}
|
|
188
|
+
return existing;
|
|
189
|
+
}
|
|
190
|
+
/** Validate the current lockfile before migrate-apply creates its backup or sentinel. */
|
|
191
|
+
export function assertMigrationLockfileReadable() {
|
|
192
|
+
readLockEntriesForMigration();
|
|
193
|
+
}
|
|
194
|
+
export function mergeLockEntriesSync(entries) {
|
|
195
|
+
const existing = readLockEntriesForMigration();
|
|
196
|
+
if (entries.length === 0)
|
|
197
|
+
return;
|
|
190
198
|
// MERGE per id, never replace: the migrator's entries are sparse (id/source/
|
|
191
199
|
// ref/localRoot), while an existing row may carry `resolvedVersion`,
|
|
192
200
|
// `resolvedRevision`, `integrity`, `installedAt`, … from a real install.
|
|
@@ -202,59 +210,6 @@ export function mergeLockEntriesSync(entries) {
|
|
|
202
210
|
const incomingIds = new Set(entries.map((e) => e.id));
|
|
203
211
|
writeLockfileUnlocked([...existing.filter((e) => !incomingIds.has(e.id)), ...merged]);
|
|
204
212
|
}
|
|
205
|
-
/** Freeze exact old/desired lockfile bytes while the migration lifecycle lock is held. */
|
|
206
|
-
export function planLockMergeSync(entries) {
|
|
207
|
-
const lockfilePath = getLockfilePath();
|
|
208
|
-
let oldBytes = null;
|
|
209
|
-
let existing = [];
|
|
210
|
-
try {
|
|
211
|
-
oldBytes = fs.readFileSync(lockfilePath, "utf8");
|
|
212
|
-
}
|
|
213
|
-
catch (error) {
|
|
214
|
-
if (error.code !== "ENOENT")
|
|
215
|
-
throw error;
|
|
216
|
-
}
|
|
217
|
-
if (oldBytes !== null) {
|
|
218
|
-
let parsed;
|
|
219
|
-
try {
|
|
220
|
-
parsed = JSON.parse(oldBytes);
|
|
221
|
-
}
|
|
222
|
-
catch (error) {
|
|
223
|
-
throw new ConfigError(`Cannot plan migration lock entries from unreadable lockfile ${lockfilePath}: ${error instanceof Error ? error.message : String(error)}.`, "INVALID_CONFIG_FILE");
|
|
224
|
-
}
|
|
225
|
-
if (!Array.isArray(parsed) || !parsed.every(isValidLockfileEntry)) {
|
|
226
|
-
throw new ConfigError(`Cannot plan migration lock entries from malformed lockfile ${lockfilePath}.`, "INVALID_CONFIG_FILE");
|
|
227
|
-
}
|
|
228
|
-
existing = parsed;
|
|
229
|
-
}
|
|
230
|
-
if (entries.length === 0)
|
|
231
|
-
return { oldBytes, desiredBytes: oldBytes };
|
|
232
|
-
const byId = new Map(existing.map((entry) => [entry.id, entry]));
|
|
233
|
-
for (const entry of entries)
|
|
234
|
-
byId.set(entry.id, { ...byId.get(entry.id), ...entry });
|
|
235
|
-
return { oldBytes, desiredBytes: `${JSON.stringify([...byId.values()], null, 2)}\n` };
|
|
236
|
-
}
|
|
237
|
-
/** Publish a frozen migration lock generation, accepting only exact old or exact desired bytes. */
|
|
238
|
-
export function publishPlannedLockMergeSync(plan) {
|
|
239
|
-
const lockfilePath = getLockfilePath();
|
|
240
|
-
let current = null;
|
|
241
|
-
try {
|
|
242
|
-
current = fs.readFileSync(lockfilePath, "utf8");
|
|
243
|
-
}
|
|
244
|
-
catch (error) {
|
|
245
|
-
if (error.code !== "ENOENT")
|
|
246
|
-
throw error;
|
|
247
|
-
}
|
|
248
|
-
if (current === plan.desiredBytes)
|
|
249
|
-
return;
|
|
250
|
-
if (current !== plan.oldBytes) {
|
|
251
|
-
throw new ConfigError(`Refusing migration lockfile publication because ${lockfilePath} is a third generation, neither the journaled old nor desired bytes.`, "INVALID_CONFIG_FILE");
|
|
252
|
-
}
|
|
253
|
-
if (plan.desiredBytes === null)
|
|
254
|
-
return;
|
|
255
|
-
fs.mkdirSync(path.dirname(lockfilePath), { recursive: true });
|
|
256
|
-
writeFileAtomic(lockfilePath, plan.desiredBytes, 0o600);
|
|
257
|
-
}
|
|
258
213
|
export async function removeLockEntry(id) {
|
|
259
214
|
if (!fs.existsSync(getDataDir()))
|
|
260
215
|
return;
|
|
@@ -58,15 +58,11 @@ const PASSTHROUGH_COMMANDS = [
|
|
|
58
58
|
"upgrade",
|
|
59
59
|
"workflow-abandon",
|
|
60
60
|
"workflow-brief",
|
|
61
|
-
"workflow-complete",
|
|
62
|
-
"workflow-complete-rejected",
|
|
63
61
|
"workflow-create",
|
|
64
62
|
"workflow-list",
|
|
65
|
-
"workflow-next",
|
|
66
63
|
"workflow-report",
|
|
67
64
|
"workflow-resume",
|
|
68
65
|
"workflow-run",
|
|
69
|
-
"workflow-start",
|
|
70
66
|
"workflow-status",
|
|
71
67
|
];
|
|
72
68
|
export const passthroughShapes = PASSTHROUGH_COMMANDS.map((command) => ({
|
|
@@ -289,12 +289,12 @@ export function formatSearchPlain(r, detail) {
|
|
|
289
289
|
if (hasWorkflowHit) {
|
|
290
290
|
const workflowRef = hits.find((h) => h.type === "workflow");
|
|
291
291
|
const wfRef = workflowRef && typeof workflowRef.ref === "string" ? workflowRef.ref : topRef;
|
|
292
|
-
lines.push(`Next: akm show '${topRef}' | To
|
|
293
|
-
lines.push("
|
|
292
|
+
lines.push(`Next: akm show '${topRef}' | To execute a workflow: akm workflow run '${wfRef}'`);
|
|
293
|
+
lines.push("Inspect the workflow before running it; `workflow run` executes and verifies its steps automatically.");
|
|
294
294
|
}
|
|
295
295
|
else {
|
|
296
296
|
lines.push(`Next: akm show '${topRef}'`);
|
|
297
|
-
lines.push("After reading the asset: check whether a workflow applies before editing — if so, use `akm workflow
|
|
297
|
+
lines.push("After reading the asset: check whether a workflow applies before editing — if so, inspect it and use `akm workflow run`.");
|
|
298
298
|
}
|
|
299
299
|
}
|
|
300
300
|
}
|
|
@@ -18,4 +18,4 @@
|
|
|
18
18
|
export { formatAddPlain, formatBundleShowPlain, formatClonePlain, formatConfigPlain, formatCuratePlain, formatEnvCreatePlain, formatEnvExportPlain, formatEnvListPlain, formatEnvRemovePlain, formatEventLine, formatEventsPlain, formatFeedbackPlain, formatImportPlain, formatIndexPlain, formatInfoPlain, formatInitPlain, formatListPlain, formatRegistryAddPlain, formatRegistryListPlain, formatRegistryRemovePlain, formatRegistrySearchPlain, formatRememberPlain, formatRemovePlain, formatSearchPlain, formatSyncPlain, formatUpdatePlain, formatUpgradePlain, } from "./command-format.js";
|
|
19
19
|
export { formatGateDecisionSummary, formatProposalAcceptPlain, formatProposalDiffPlain, formatProposalDrainPlain, formatProposalListPlain, formatProposalProducerPlain, formatProposalRejectPlain, formatProposalShowPlain, } from "./proposal-format.js";
|
|
20
20
|
export { formatShowPlain } from "./show-format.js";
|
|
21
|
-
export { formatWorkflowBriefPlain,
|
|
21
|
+
export { formatWorkflowBriefPlain, formatWorkflowCreatePlain, formatWorkflowListPlain, formatWorkflowResumePlain, formatWorkflowRunPlain, formatWorkflowStatusPlain, } from "./workflow-format.js";
|
|
@@ -45,9 +45,7 @@ export function appendShowDirectives(lines, r) {
|
|
|
45
45
|
if (assetType === "skill" || assetType === "knowledge") {
|
|
46
46
|
const activeRun = r.activeRun;
|
|
47
47
|
if (activeRun) {
|
|
48
|
-
|
|
49
|
-
lines.unshift(` akm workflow complete '${activeRun.runId}'${activeRun.stepId ? ` --step '${activeRun.stepId}'` : ""}`);
|
|
50
|
-
lines.unshift("Read this schema, then follow your workflow step's instructions to edit the workspace file. When done, mark the step complete:");
|
|
48
|
+
lines.unshift("Read this reference, follow the active unit's instructions, and return the result to the workflow runner. Step completion is automatic.");
|
|
51
49
|
lines.unshift(`WORKFLOW ACTIVE — schema shown as reference (run: ${activeRun.runId})`);
|
|
52
50
|
lines.unshift("---");
|
|
53
51
|
lines.unshift("");
|
|
@@ -88,14 +86,14 @@ export function appendShowDirectives(lines, r) {
|
|
|
88
86
|
const insertIdx = separatorIdx >= 0 ? separatorIdx + 1 : r.type || r.name ? 1 : 0;
|
|
89
87
|
const actionDirective = [
|
|
90
88
|
`ACTION REQUIRED: Do not execute steps manually from this output.`,
|
|
91
|
-
`Run \`akm workflow
|
|
89
|
+
`Run \`akm workflow run '${workflowRef}'\` to execute the workflow through its declared gates.`,
|
|
92
90
|
"---",
|
|
93
91
|
];
|
|
94
92
|
lines.splice(insertIdx, 0, "", ...actionDirective);
|
|
95
93
|
lines.push("");
|
|
96
94
|
lines.push("---");
|
|
97
|
-
lines.push(`NEXT STEP: Run \`akm workflow
|
|
98
|
-
lines.push("
|
|
95
|
+
lines.push(`NEXT STEP: Run \`akm workflow run '${workflowRef}'\` to execute the workflow.`);
|
|
96
|
+
lines.push("Inspect the workflow first; the runner dispatches and completes each step automatically.");
|
|
99
97
|
}
|
|
100
98
|
}
|
|
101
99
|
/**
|