akm-cli 0.7.4 → 0.8.0-rc.3
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 → .github/CHANGELOG.md} +34 -1
- package/.github/LICENSE +374 -0
- package/dist/cli/parse-args.js +86 -0
- package/dist/cli.js +1223 -650
- package/dist/commands/agent-dispatch.js +107 -0
- package/dist/commands/agent-support.js +62 -0
- package/dist/commands/config-cli.js +68 -84
- package/dist/commands/consolidate.js +812 -0
- package/dist/commands/curate.js +1 -0
- package/dist/commands/distill-promotion-policy.js +658 -0
- package/dist/commands/distill.js +224 -39
- package/dist/commands/eval-cases.js +40 -0
- package/dist/commands/events.js +12 -24
- package/dist/commands/graph.js +222 -0
- package/dist/commands/health.js +376 -0
- package/dist/commands/help/help-accept.md +9 -0
- package/dist/commands/help/help-improve.md +53 -0
- package/dist/commands/help/help-proposals.md +15 -0
- package/dist/commands/help/help-propose.md +17 -0
- package/dist/commands/help/help-reject.md +8 -0
- package/dist/commands/history.js +3 -30
- package/dist/commands/improve.js +1161 -0
- package/dist/commands/info.js +2 -2
- package/dist/commands/init.js +2 -2
- package/dist/commands/install-audit.js +5 -1
- package/dist/commands/installed-stashes.js +118 -138
- package/dist/commands/knowledge.js +133 -0
- package/dist/commands/lint/agent-linter.js +46 -0
- package/dist/commands/lint/base-linter.js +291 -0
- package/dist/commands/lint/command-linter.js +46 -0
- package/dist/commands/lint/default-linter.js +13 -0
- package/dist/commands/lint/index.js +145 -0
- package/dist/commands/lint/knowledge-linter.js +13 -0
- package/dist/commands/lint/memory-linter.js +58 -0
- package/dist/commands/lint/registry.js +33 -0
- package/dist/commands/lint/skill-linter.js +42 -0
- package/dist/commands/lint/task-linter.js +47 -0
- package/dist/commands/lint/types.js +1 -0
- package/dist/commands/lint/vault-key-rules.js +67 -0
- package/dist/commands/lint/workflow-linter.js +53 -0
- package/dist/commands/lint.js +1 -0
- package/dist/commands/migration-help.js +2 -2
- package/dist/commands/proposal.js +8 -7
- package/dist/commands/propose.js +106 -43
- package/dist/commands/reflect.js +167 -41
- package/dist/commands/registry-search.js +2 -2
- package/dist/commands/remember.js +55 -1
- package/dist/commands/schema-repair.js +130 -0
- package/dist/commands/search.js +21 -5
- package/dist/commands/show.js +135 -55
- package/dist/commands/source-add.js +10 -10
- package/dist/commands/source-manage.js +11 -19
- package/dist/commands/tasks.js +385 -0
- package/dist/commands/url-checker.js +39 -0
- package/dist/commands/vault.js +173 -87
- package/dist/core/action-contributors.js +25 -0
- package/dist/core/asset-ref.js +4 -0
- package/dist/core/asset-registry.js +5 -17
- package/dist/core/asset-spec.js +11 -1
- package/dist/core/common.js +100 -0
- package/dist/core/concurrent.js +22 -0
- package/dist/core/config.js +240 -127
- package/dist/core/events.js +87 -123
- package/dist/core/frontmatter.js +0 -6
- package/dist/core/markdown.js +17 -0
- package/dist/core/memory-improve.js +678 -0
- package/dist/core/parse.js +155 -0
- package/dist/core/paths.js +101 -3
- package/dist/core/proposal-validators.js +61 -0
- package/dist/core/proposals.js +49 -38
- package/dist/core/state-db.js +731 -0
- package/dist/core/time.js +51 -0
- package/dist/core/warn.js +59 -1
- package/dist/indexer/db-search.js +86 -472
- package/dist/indexer/db.js +418 -59
- package/dist/indexer/ensure-index.js +133 -0
- package/dist/indexer/graph-boost.js +247 -94
- package/dist/indexer/graph-db.js +201 -0
- package/dist/indexer/graph-dedup.js +99 -0
- package/dist/indexer/graph-extraction.js +417 -74
- package/dist/indexer/index-context.js +10 -0
- package/dist/indexer/indexer.js +480 -298
- package/dist/indexer/llm-cache.js +47 -0
- package/dist/indexer/matchers.js +124 -160
- package/dist/indexer/memory-inference.js +63 -29
- package/dist/indexer/metadata-contributors.js +26 -0
- package/dist/indexer/metadata.js +196 -197
- package/dist/indexer/path-resolver.js +89 -0
- package/dist/indexer/ranking-contributors.js +204 -0
- package/dist/indexer/ranking.js +74 -0
- package/dist/indexer/search-hit-enrichers.js +22 -0
- package/dist/indexer/search-source.js +24 -9
- package/dist/indexer/semantic-status.js +2 -16
- package/dist/indexer/walker.js +25 -0
- package/dist/integrations/agent/builders.js +109 -0
- package/dist/integrations/agent/config.js +203 -3
- package/dist/integrations/agent/index.js +5 -2
- package/dist/integrations/agent/model-aliases.js +63 -0
- package/dist/integrations/agent/profiles.js +67 -5
- package/dist/integrations/agent/prompts.js +114 -29
- package/dist/integrations/agent/sdk-runner.js +120 -0
- package/dist/integrations/agent/spawn.js +158 -34
- package/dist/integrations/lockfile.js +10 -18
- package/dist/integrations/session-logs/index.js +65 -0
- package/dist/integrations/session-logs/providers/claude-code.js +56 -0
- package/dist/integrations/session-logs/providers/opencode.js +52 -0
- package/dist/integrations/session-logs/types.js +1 -0
- package/dist/llm/call-ai.js +74 -0
- package/dist/llm/client.js +63 -86
- package/dist/llm/feature-gate.js +27 -16
- package/dist/llm/graph-extract.js +297 -64
- package/dist/llm/memory-infer.js +52 -71
- package/dist/llm/metadata-enhance.js +39 -22
- package/dist/llm/prompts/graph-extract-user-prompt.md +12 -0
- package/dist/output/cli-hints-full.md +277 -0
- package/dist/output/cli-hints-short.md +65 -0
- package/dist/output/cli-hints.js +2 -309
- package/dist/output/renderers.js +226 -257
- package/dist/output/shapes.js +109 -96
- package/dist/output/text.js +274 -36
- package/dist/registry/providers/skills-sh.js +61 -49
- package/dist/registry/providers/static-index.js +44 -48
- package/dist/registry/resolve.js +8 -16
- package/dist/setup/setup.js +510 -11
- package/dist/sources/provider-factory.js +2 -1
- package/dist/sources/providers/filesystem.js +16 -23
- package/dist/sources/providers/git.js +45 -4
- package/dist/sources/providers/website.js +15 -22
- package/dist/sources/website-ingest.js +4 -0
- package/dist/tasks/backends/cron.js +200 -0
- package/dist/tasks/backends/exec-utils.js +25 -0
- package/dist/tasks/backends/index.js +32 -0
- package/dist/tasks/backends/launchd-template.xml +19 -0
- package/dist/tasks/backends/launchd.js +184 -0
- package/dist/tasks/backends/schtasks-template.xml +29 -0
- package/dist/tasks/backends/schtasks.js +212 -0
- package/dist/tasks/parser.js +198 -0
- package/dist/tasks/resolveAkmBin.js +84 -0
- package/dist/tasks/runner.js +432 -0
- package/dist/tasks/schedule.js +208 -0
- package/dist/tasks/schema.js +13 -0
- package/dist/tasks/validator.js +59 -0
- package/dist/wiki/index-template.md +12 -0
- package/dist/wiki/ingest-workflow-template.md +54 -0
- package/dist/wiki/log-template.md +8 -0
- package/dist/wiki/schema-template.md +61 -0
- package/dist/wiki/wiki-templates.js +12 -0
- package/dist/wiki/wiki.js +10 -61
- package/dist/workflows/authoring.js +5 -25
- package/dist/workflows/db.js +9 -0
- package/dist/workflows/renderer.js +8 -3
- package/dist/workflows/runs.js +73 -88
- package/dist/workflows/scope-key.js +76 -0
- package/dist/workflows/validator.js +1 -1
- package/dist/workflows/workflow-template.md +24 -0
- package/docs/README.md +5 -2
- package/docs/migration/release-notes/0.7.0.md +1 -1
- package/docs/migration/release-notes/0.7.4.md +1 -1
- package/docs/migration/release-notes/0.7.5.md +20 -0
- package/docs/migration/release-notes/0.8.0.md +43 -0
- package/package.json +4 -3
- package/dist/templates/wiki-templates.js +0 -100
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { BaseLinter } from "./base-linter";
|
|
2
|
+
/**
|
|
3
|
+
* Linter for `tasks/` assets.
|
|
4
|
+
*
|
|
5
|
+
* Tasks are `.md` files with YAML frontmatter. In addition to the base checks
|
|
6
|
+
* this linter validates the required task fields:
|
|
7
|
+
*
|
|
8
|
+
* - `schedule` (string, non-empty) — cron expression or `@`-alias
|
|
9
|
+
* - `enabled` (boolean)
|
|
10
|
+
* - At least one of: `prompt` or `workflow` field present
|
|
11
|
+
*
|
|
12
|
+
* All issues are reported as `invalid-task-frontmatter` and are **not**
|
|
13
|
+
* auto-fixable. Cron expression syntax validation is intentionally out of
|
|
14
|
+
* scope (that belongs to `parseSchedule()`).
|
|
15
|
+
*/
|
|
16
|
+
export class TaskLinter extends BaseLinter {
|
|
17
|
+
types = ["tasks"];
|
|
18
|
+
lint(ctx) {
|
|
19
|
+
const issues = this.runBaseChecks(ctx);
|
|
20
|
+
// Only validate frontmatter fields when frontmatter is present.
|
|
21
|
+
if (ctx.frontmatter === null)
|
|
22
|
+
return issues;
|
|
23
|
+
const missing = [];
|
|
24
|
+
// schedule: must be present and non-empty
|
|
25
|
+
if (!("schedule" in ctx.data) || typeof ctx.data.schedule !== "string" || ctx.data.schedule.trim() === "") {
|
|
26
|
+
missing.push("schedule");
|
|
27
|
+
}
|
|
28
|
+
// enabled: must be present (boolean — value of false is valid)
|
|
29
|
+
if (!("enabled" in ctx.data)) {
|
|
30
|
+
missing.push("enabled");
|
|
31
|
+
}
|
|
32
|
+
// At least one of: prompt or workflow
|
|
33
|
+
const hasTarget = "prompt" in ctx.data || "workflow" in ctx.data;
|
|
34
|
+
if (!hasTarget) {
|
|
35
|
+
missing.push("prompt or workflow");
|
|
36
|
+
}
|
|
37
|
+
if (missing.length > 0) {
|
|
38
|
+
issues.push({
|
|
39
|
+
file: ctx.relPath,
|
|
40
|
+
issue: "invalid-task-frontmatter",
|
|
41
|
+
detail: `missing required fields: ${missing.join(", ")}`,
|
|
42
|
+
fixed: false,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
return issues;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vault security lint rules — flags known-dangerous environment variable names.
|
|
3
|
+
*
|
|
4
|
+
* These env var names, when present as vault keys, indicate the vault can be
|
|
5
|
+
* used to hijack process execution via loader injection, path override, or
|
|
6
|
+
* shell/runtime startup hooks. The lint pass emits a warning-level finding;
|
|
7
|
+
* it does NOT block vault load or `akm add` installation.
|
|
8
|
+
*/
|
|
9
|
+
import { listKeys } from "../vault";
|
|
10
|
+
// ── Dangerous key set ─────────────────────────────────────────────────────────
|
|
11
|
+
export const DANGEROUS_VAULT_KEYS = new Set([
|
|
12
|
+
// Dynamic linker hijacking (Linux)
|
|
13
|
+
"LD_PRELOAD",
|
|
14
|
+
"LD_LIBRARY_PATH",
|
|
15
|
+
"LD_AUDIT",
|
|
16
|
+
"LD_DEBUG",
|
|
17
|
+
// Dynamic linker hijacking (macOS)
|
|
18
|
+
"DYLD_INSERT_LIBRARIES",
|
|
19
|
+
"DYLD_LIBRARY_PATH",
|
|
20
|
+
"DYLD_FRAMEWORK_PATH",
|
|
21
|
+
// Shell and command resolution
|
|
22
|
+
"PATH",
|
|
23
|
+
"BASH_ENV",
|
|
24
|
+
"ENV",
|
|
25
|
+
"PROMPT_COMMAND",
|
|
26
|
+
"PS1",
|
|
27
|
+
"PS2",
|
|
28
|
+
// Language runtime hijacking
|
|
29
|
+
"NODE_OPTIONS",
|
|
30
|
+
"NODE_PATH",
|
|
31
|
+
"PYTHONSTARTUP",
|
|
32
|
+
"PYTHONPATH",
|
|
33
|
+
"PYTHONINSPECT",
|
|
34
|
+
"RUBYLIB",
|
|
35
|
+
"RUBYOPT",
|
|
36
|
+
"PERL5LIB",
|
|
37
|
+
"PERL5OPT",
|
|
38
|
+
"JAVA_TOOL_OPTIONS",
|
|
39
|
+
"JDK_JAVA_OPTIONS",
|
|
40
|
+
"_JAVA_OPTIONS",
|
|
41
|
+
]);
|
|
42
|
+
// ── Checker ───────────────────────────────────────────────────────────────────
|
|
43
|
+
/**
|
|
44
|
+
* Inspect a vault `.env` file and return a lint finding for every key whose
|
|
45
|
+
* name appears in `DANGEROUS_VAULT_KEYS`.
|
|
46
|
+
*
|
|
47
|
+
* @param vaultPath Absolute path to the `.env` file.
|
|
48
|
+
* @param relPath Stash-relative path used as the `file` field in findings
|
|
49
|
+
* (e.g. `"vaults/prod.env"`).
|
|
50
|
+
* @param vaultRef Human-readable vault ref (e.g. `"vault:prod"`) shown in
|
|
51
|
+
* the finding message.
|
|
52
|
+
*/
|
|
53
|
+
export function checkVaultForDangerousKeys(vaultPath, relPath, vaultRef) {
|
|
54
|
+
const { keys } = listKeys(vaultPath);
|
|
55
|
+
const issues = [];
|
|
56
|
+
for (const key of keys) {
|
|
57
|
+
if (!DANGEROUS_VAULT_KEYS.has(key))
|
|
58
|
+
continue;
|
|
59
|
+
issues.push({
|
|
60
|
+
file: relPath,
|
|
61
|
+
issue: "dangerous-vault-key",
|
|
62
|
+
detail: `Vault key \`${key}\` can be used to hijack process execution when injected via \`akm vault run\`. Vault ref: ${vaultRef}. Review this vault file before running \`akm vault run\` commands against untrusted stashes.`,
|
|
63
|
+
fixed: false,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
return issues;
|
|
67
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { BaseLinter } from "./base-linter";
|
|
3
|
+
const PLACEHOLDER_STRINGS = ["Describe what this workflow accomplishes", "Example Workflow"];
|
|
4
|
+
/**
|
|
5
|
+
* Linter for `workflows/` assets.
|
|
6
|
+
*
|
|
7
|
+
* Extra check beyond base:
|
|
8
|
+
* - `placeholder-stub`: body contains a known placeholder string.
|
|
9
|
+
* Fix: delete the file.
|
|
10
|
+
*/
|
|
11
|
+
export class WorkflowLinter extends BaseLinter {
|
|
12
|
+
types = ["workflows"];
|
|
13
|
+
lint(ctx) {
|
|
14
|
+
const issues = this.runBaseChecks(ctx);
|
|
15
|
+
const placeholderMatch = this.#checkPlaceholderStub(ctx.body);
|
|
16
|
+
if (placeholderMatch) {
|
|
17
|
+
if (ctx.fix) {
|
|
18
|
+
try {
|
|
19
|
+
fs.unlinkSync(ctx.filePath);
|
|
20
|
+
issues.push({
|
|
21
|
+
file: ctx.relPath,
|
|
22
|
+
issue: "placeholder-stub",
|
|
23
|
+
detail: `deleted: found "${placeholderMatch}"`,
|
|
24
|
+
fixed: true,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
catch (e) {
|
|
28
|
+
issues.push({
|
|
29
|
+
file: ctx.relPath,
|
|
30
|
+
issue: "placeholder-stub",
|
|
31
|
+
detail: `could not delete: ${e instanceof Error ? e.message : String(e)}`,
|
|
32
|
+
fixed: false,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
return issues;
|
|
36
|
+
}
|
|
37
|
+
issues.push({
|
|
38
|
+
file: ctx.relPath,
|
|
39
|
+
issue: "placeholder-stub",
|
|
40
|
+
detail: `placeholder text: "${placeholderMatch}"`,
|
|
41
|
+
fixed: false,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
return issues;
|
|
45
|
+
}
|
|
46
|
+
#checkPlaceholderStub(body) {
|
|
47
|
+
for (const placeholder of PLACEHOLDER_STRINGS) {
|
|
48
|
+
if (body.includes(placeholder))
|
|
49
|
+
return placeholder;
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { akmLint } from "./lint/index";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
const CHANGELOG_URL = "https://github.com/itlackey/akm/blob/main/CHANGELOG.md";
|
|
3
|
+
const CHANGELOG_URL = "https://github.com/itlackey/akm/blob/main/.github/CHANGELOG.md";
|
|
4
4
|
const MIGRATION_DOC_URL = "https://github.com/itlackey/akm/blob/main/docs/migration/v0.5-to-v0.6.md";
|
|
5
5
|
/**
|
|
6
6
|
* Directory containing per-version release notes. Resolved relative to
|
|
@@ -14,7 +14,7 @@ function releaseNotesDir() {
|
|
|
14
14
|
}
|
|
15
15
|
function loadChangelog() {
|
|
16
16
|
try {
|
|
17
|
-
const changelogPath = path.resolve(import.meta.dir, "
|
|
17
|
+
const changelogPath = path.resolve(import.meta.dir, "../../.github/CHANGELOG.md");
|
|
18
18
|
if (fs.existsSync(changelogPath)) {
|
|
19
19
|
return fs.readFileSync(changelogPath, "utf8");
|
|
20
20
|
}
|
|
@@ -12,7 +12,7 @@ import { resolveStashDir } from "../core/common";
|
|
|
12
12
|
import { loadConfig } from "../core/config";
|
|
13
13
|
import { UsageError } from "../core/errors";
|
|
14
14
|
import { appendEvent } from "../core/events";
|
|
15
|
-
import { archiveProposal, createProposal, diffProposal, getProposal, listProposals, promoteProposal, validateProposal, } from "../core/proposals";
|
|
15
|
+
import { archiveProposal, createProposal, diffProposal, getProposal, listProposals, promoteProposal, resolveProposalId, validateProposal, } from "../core/proposals";
|
|
16
16
|
// ── Shared helpers ──────────────────────────────────────────────────────────
|
|
17
17
|
function resolveStash(stashDir) {
|
|
18
18
|
if (stashDir)
|
|
@@ -43,7 +43,8 @@ export function akmProposalShow(options) {
|
|
|
43
43
|
export async function akmProposalAccept(options) {
|
|
44
44
|
const stash = resolveStash(options.stashDir);
|
|
45
45
|
const config = options.config ?? loadConfig();
|
|
46
|
-
const
|
|
46
|
+
const resolvedId = resolveProposalId(stash, options.id).id;
|
|
47
|
+
const result = await promoteProposal(stash, config, resolvedId, { target: options.target }, options.ctx);
|
|
47
48
|
// Emit `promoted` to the events stream so observers (audit, dashboards,
|
|
48
49
|
// sync) see the accept happen. Only emit on the happy path — promotion
|
|
49
50
|
// throws on validation failure, so reaching this point means the asset
|
|
@@ -69,11 +70,11 @@ export async function akmProposalAccept(options) {
|
|
|
69
70
|
}
|
|
70
71
|
export function akmProposalReject(options) {
|
|
71
72
|
const stash = resolveStash(options.stashDir);
|
|
72
|
-
const existing =
|
|
73
|
+
const existing = resolveProposalId(stash, options.id);
|
|
73
74
|
if (existing.status !== "pending") {
|
|
74
|
-
throw new UsageError(`Proposal ${
|
|
75
|
+
throw new UsageError(`Proposal ${existing.id} is not pending (current status: ${existing.status}). Only pending proposals can be rejected.`, "INVALID_FLAG_VALUE");
|
|
75
76
|
}
|
|
76
|
-
const updated = archiveProposal(stash,
|
|
77
|
+
const updated = archiveProposal(stash, existing.id, "rejected", options.reason, options.ctx);
|
|
77
78
|
appendEvent({
|
|
78
79
|
eventType: "rejected",
|
|
79
80
|
ref: updated.ref,
|
|
@@ -96,8 +97,8 @@ export function akmProposalReject(options) {
|
|
|
96
97
|
export function akmProposalDiff(options) {
|
|
97
98
|
const stash = resolveStash(options.stashDir);
|
|
98
99
|
const config = options.config ?? loadConfig();
|
|
99
|
-
const proposal =
|
|
100
|
-
const diff = diffProposal(stash, config,
|
|
100
|
+
const proposal = resolveProposalId(stash, options.id);
|
|
101
|
+
const diff = diffProposal(stash, config, proposal.id, { target: options.target });
|
|
101
102
|
return {
|
|
102
103
|
schemaVersion: 1,
|
|
103
104
|
id: proposal.id,
|
package/dist/commands/propose.js
CHANGED
|
@@ -12,34 +12,19 @@
|
|
|
12
12
|
import { parseAssetRef } from "../core/asset-ref";
|
|
13
13
|
import { TYPE_DIRS } from "../core/asset-spec";
|
|
14
14
|
import { resolveStashDir } from "../core/common";
|
|
15
|
-
import { loadConfig } from "../core/config";
|
|
16
15
|
import { ConfigError, UsageError } from "../core/errors";
|
|
17
16
|
import { appendEvent } from "../core/events";
|
|
18
17
|
import { createProposal } from "../core/proposals";
|
|
19
|
-
import {
|
|
18
|
+
import { runAgent, } from "../integrations/agent";
|
|
19
|
+
import { resolveProcessAgentProfile } from "../integrations/agent/config";
|
|
20
20
|
import { buildProposePrompt, parseAgentProposalPayload } from "../integrations/agent/prompts";
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
return parseAgentConfig(config.agent);
|
|
24
|
-
}
|
|
25
|
-
function resolveProfile(options) {
|
|
26
|
-
if (options.agentProfile)
|
|
27
|
-
return options.agentProfile;
|
|
28
|
-
const agent = options.agentConfig ?? loadAgentConfigFromDisk();
|
|
29
|
-
return requireAgentProfile(agent, options.profile);
|
|
30
|
-
}
|
|
21
|
+
import { runAgentSdk } from "../integrations/agent/sdk-runner";
|
|
22
|
+
import { baseFailureFields, enoentHintMessage, isEnoentFailure, loadAgentConfigFromDisk, resolveAgentProfile, } from "./agent-support";
|
|
31
23
|
function failureEnvelope(result, type, name, fallbackReason = "non_zero_exit") {
|
|
32
|
-
const reason = result.reason ?? fallbackReason;
|
|
33
24
|
return {
|
|
34
|
-
|
|
35
|
-
ok: false,
|
|
36
|
-
reason,
|
|
37
|
-
error: result.error ?? `agent failure (${reason})`,
|
|
25
|
+
...baseFailureFields(result, fallbackReason),
|
|
38
26
|
type,
|
|
39
27
|
name,
|
|
40
|
-
exitCode: result.exitCode,
|
|
41
|
-
...(result.stdout ? { stdout: result.stdout } : {}),
|
|
42
|
-
...(result.stderr ? { stderr: result.stderr } : {}),
|
|
43
28
|
};
|
|
44
29
|
}
|
|
45
30
|
export async function akmPropose(options) {
|
|
@@ -68,9 +53,31 @@ export async function akmPropose(options) {
|
|
|
68
53
|
},
|
|
69
54
|
});
|
|
70
55
|
// 2. Resolve profile.
|
|
56
|
+
// When an explicit --profile flag is given, honour it directly (existing
|
|
57
|
+
// behaviour). Otherwise use resolveProcessAgentProfile so that per-process
|
|
58
|
+
// agent config (agent.processes["propose"]) is picked up automatically.
|
|
71
59
|
let profile;
|
|
60
|
+
let resolvedTimeoutMs = options.timeoutMs;
|
|
72
61
|
try {
|
|
73
|
-
|
|
62
|
+
if (options.agentProfile) {
|
|
63
|
+
// Test seam: injected profile bypasses all config.
|
|
64
|
+
profile = options.agentProfile;
|
|
65
|
+
}
|
|
66
|
+
else if (options.profile) {
|
|
67
|
+
// Explicit --profile flag wins over process config.
|
|
68
|
+
profile = resolveAgentProfile(options);
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
// Use per-process config resolution (falls back to agent.default).
|
|
72
|
+
const agent = options.agentConfig ?? loadAgentConfigFromDisk();
|
|
73
|
+
const processName = options.agentProcess ?? "propose";
|
|
74
|
+
const resolved = resolveProcessAgentProfile(processName, agent);
|
|
75
|
+
profile = resolved.profile;
|
|
76
|
+
// Only apply process-resolved timeoutMs when caller didn't supply one.
|
|
77
|
+
if (resolvedTimeoutMs === undefined) {
|
|
78
|
+
resolvedTimeoutMs = resolved.timeoutMs;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
74
81
|
}
|
|
75
82
|
catch (err) {
|
|
76
83
|
if (err instanceof ConfigError || err instanceof UsageError)
|
|
@@ -78,40 +85,96 @@ export async function akmPropose(options) {
|
|
|
78
85
|
throw err;
|
|
79
86
|
}
|
|
80
87
|
// 3. Build prompt.
|
|
88
|
+
// Synthesize a temp draft path so opencode can write the asset content
|
|
89
|
+
// directly using its file tools rather than returning JSON via stdout.
|
|
90
|
+
const draftFilePath = import("node:os").then((os) => import("node:path").then((path) => path.join(os.tmpdir(), `akm-propose-${options.type}-${options.name.replace(/[^a-z0-9_-]/gi, "_")}-${Date.now()}.md`)));
|
|
91
|
+
const resolvedDraftPath = await draftFilePath;
|
|
81
92
|
const prompt = buildProposePrompt({
|
|
82
93
|
type: options.type,
|
|
83
94
|
name: options.name,
|
|
84
95
|
task: options.task,
|
|
96
|
+
draftFilePath: resolvedDraftPath,
|
|
85
97
|
});
|
|
86
98
|
// 4. Spawn the agent.
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
99
|
+
// Real agent runs use interactive mode so file tools can write the draft.
|
|
100
|
+
// Injected/custom spawns still need captured stdout for JSON payload tests.
|
|
101
|
+
// Use callAi for the unified AI dispatch path (agent CLI preferred, LLM HTTP fallback).
|
|
102
|
+
const useCustomSpawn = Boolean(options.runAgentOptions?.spawn);
|
|
103
|
+
let result;
|
|
104
|
+
if (useCustomSpawn) {
|
|
105
|
+
// Test seam: use raw runAgent with injected spawn so tests remain deterministic.
|
|
106
|
+
const runOptions = {
|
|
107
|
+
stdio: "captured",
|
|
108
|
+
parseOutput: "text",
|
|
109
|
+
...(resolvedTimeoutMs !== undefined ? { timeoutMs: resolvedTimeoutMs } : {}),
|
|
110
|
+
...(options.runAgentOptions ?? {}),
|
|
111
|
+
};
|
|
112
|
+
result = await runAgent(profile, prompt, runOptions);
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
// Production path: dispatch directly to the appropriate runner.
|
|
116
|
+
const runOptions = {
|
|
117
|
+
stdio: resolvedDraftPath ? "interactive" : "captured",
|
|
118
|
+
parseOutput: "text",
|
|
119
|
+
...(resolvedTimeoutMs !== undefined ? { timeoutMs: resolvedTimeoutMs } : {}),
|
|
120
|
+
};
|
|
121
|
+
result = profile.sdkMode
|
|
122
|
+
? await runAgentSdk(profile, prompt ?? "", runOptions)
|
|
123
|
+
: await runAgent(profile, prompt, runOptions);
|
|
124
|
+
}
|
|
94
125
|
if (!result.ok) {
|
|
126
|
+
// B3: ENOENT / not-found gives an actionable hint.
|
|
127
|
+
if (isEnoentFailure(result)) {
|
|
128
|
+
return { ...failureEnvelope(result, options.type, options.name), error: enoentHintMessage(profile.bin) };
|
|
129
|
+
}
|
|
95
130
|
return failureEnvelope(result, options.type, options.name);
|
|
96
131
|
}
|
|
97
|
-
// 5.
|
|
132
|
+
// 5. Resolve the proposal content.
|
|
133
|
+
// Path A: opencode wrote the draft file — read it directly (no stdout parse).
|
|
134
|
+
// Path B: fallback to stdout JSON parse for non-file-writing agents.
|
|
135
|
+
const fs = await import("node:fs");
|
|
98
136
|
let payload;
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
ok: false,
|
|
106
|
-
reason: "parse_error",
|
|
107
|
-
error: err instanceof Error ? err.message : String(err),
|
|
108
|
-
type: options.type,
|
|
109
|
-
name: options.name,
|
|
110
|
-
exitCode: result.exitCode,
|
|
111
|
-
stdout: result.stdout,
|
|
112
|
-
...(result.stderr ? { stderr: result.stderr } : {}),
|
|
137
|
+
if (fs.existsSync(resolvedDraftPath)) {
|
|
138
|
+
const draftContent = fs.readFileSync(resolvedDraftPath, "utf8");
|
|
139
|
+
fs.unlinkSync(resolvedDraftPath);
|
|
140
|
+
payload = {
|
|
141
|
+
ref: `${options.type}:${options.name}`,
|
|
142
|
+
content: draftContent,
|
|
113
143
|
};
|
|
114
144
|
}
|
|
145
|
+
else {
|
|
146
|
+
// B1: When interactive mode was used and stdout is empty, the agent did not
|
|
147
|
+
// write the draft file and stdout was not captured — surface an actionable error.
|
|
148
|
+
const stdioWasInteractive = !useCustomSpawn;
|
|
149
|
+
if (stdioWasInteractive && (result.stdout ?? "") === "") {
|
|
150
|
+
return {
|
|
151
|
+
schemaVersion: 1,
|
|
152
|
+
ok: false,
|
|
153
|
+
reason: "parse_error",
|
|
154
|
+
error: "Agent did not write draft file and stdout was not captured (interactive mode). Check that the agent CLI understood the file-write instruction, or configure a headless profile with stdio: 'captured'.",
|
|
155
|
+
type: options.type,
|
|
156
|
+
name: options.name,
|
|
157
|
+
exitCode: result.exitCode,
|
|
158
|
+
...(result.stderr ? { stderr: result.stderr } : {}),
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
try {
|
|
162
|
+
payload = parseAgentProposalPayload(result.stdout ?? "");
|
|
163
|
+
}
|
|
164
|
+
catch (err) {
|
|
165
|
+
return {
|
|
166
|
+
schemaVersion: 1,
|
|
167
|
+
ok: false,
|
|
168
|
+
reason: "parse_error",
|
|
169
|
+
error: err instanceof Error ? err.message : String(err),
|
|
170
|
+
type: options.type,
|
|
171
|
+
name: options.name,
|
|
172
|
+
exitCode: result.exitCode,
|
|
173
|
+
stdout: result.stdout,
|
|
174
|
+
...(result.stderr ? { stderr: result.stderr } : {}),
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
}
|
|
115
178
|
// 6. Insert the proposal. Note: we allow the agent's `ref` to normalise the
|
|
116
179
|
// asset name (e.g. path-cleanup), but only after validating that the ref is
|
|
117
180
|
// well-formed and the type still matches the requested type.
|