akm-cli 0.7.3 → 0.7.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/{CHANGELOG.md → .github/CHANGELOG.md} +35 -0
- package/.github/LICENSE +374 -0
- package/dist/cli.js +241 -170
- package/dist/commands/curate.js +1 -0
- package/dist/commands/distill.js +14 -4
- package/dist/commands/events.js +10 -1
- package/dist/commands/migration-help.js +2 -2
- package/dist/commands/propose.js +36 -16
- package/dist/commands/reflect.js +40 -14
- package/dist/commands/remember.js +1 -1
- package/dist/commands/show.js +19 -44
- package/dist/commands/vault.js +5 -10
- package/dist/core/asset-registry.js +1 -1
- package/dist/core/asset-spec.js +1 -1
- package/dist/core/config.js +13 -0
- package/dist/core/events.js +19 -2
- package/dist/indexer/db-search.js +35 -235
- package/dist/indexer/db.js +15 -5
- package/dist/indexer/ensure-index.js +72 -0
- package/dist/indexer/graph-extraction.js +10 -0
- package/dist/indexer/indexer.js +38 -22
- package/dist/integrations/agent/prompts.js +95 -15
- package/dist/integrations/agent/spawn.js +65 -12
- package/dist/llm/client.js +40 -2
- package/dist/llm/graph-extract.js +2 -4
- package/dist/llm/memory-infer.js +7 -4
- package/dist/output/cli-hints.js +17 -8
- package/dist/output/renderers.js +6 -1
- package/dist/output/shapes.js +8 -3
- package/dist/output/text.js +18 -19
- package/dist/sources/providers/git.js +43 -1
- package/dist/workflows/db.js +9 -0
- package/dist/workflows/runs.js +25 -8
- package/dist/workflows/scope-key.js +76 -0
- package/docs/migration/release-notes/0.7.3.md +16 -0
- package/docs/migration/release-notes/0.7.4.md +17 -0
- package/docs/migration/release-notes/0.7.5.md +20 -0
- package/package.json +2 -2
|
@@ -47,16 +47,30 @@ function knownTypeList() {
|
|
|
47
47
|
return Object.keys(TYPE_DIRS).sort().join(", ");
|
|
48
48
|
}
|
|
49
49
|
/**
|
|
50
|
-
* Common envelope every prompt asks the agent to honour
|
|
51
|
-
* uses `JSON.parse(stdout)` to extract
|
|
52
|
-
* JSON object will be treated as a parse
|
|
50
|
+
* Common envelope every prompt asks the agent to honour when NO draft file
|
|
51
|
+
* path is available. The wrapper code uses `JSON.parse(stdout)` to extract
|
|
52
|
+
* the payload — anything outside the JSON object will be treated as a parse
|
|
53
|
+
* error.
|
|
53
54
|
*/
|
|
54
|
-
const
|
|
55
|
+
const RESPONSE_CONTRACT_JSON = [
|
|
55
56
|
"Respond ONLY with a single JSON object. No prose before or after.",
|
|
56
57
|
'Shape: {"ref": "<type>:<name>", "content": "<full file contents>", "frontmatter": {...}}',
|
|
57
58
|
"`content` is the full file body that will be written if accepted.",
|
|
58
59
|
"`frontmatter` is optional — include it if `content` starts with `---` so reviewers can sanity-check the keys.",
|
|
59
60
|
].join("\n");
|
|
61
|
+
/**
|
|
62
|
+
* Response contract used when a draft file path is available. Instructs the
|
|
63
|
+
* agent to write the improved asset content directly to the file using its
|
|
64
|
+
* native file-editing tools — no stdout JSON parsing required.
|
|
65
|
+
*/
|
|
66
|
+
function fileWriteContract(draftFilePath) {
|
|
67
|
+
return [
|
|
68
|
+
`Write the complete improved asset content to: ${draftFilePath}`,
|
|
69
|
+
"Use your file-editing tools to create or overwrite that file.",
|
|
70
|
+
"Do NOT output JSON to stdout. Do NOT print the file contents. Just write the file.",
|
|
71
|
+
"When you are done writing the file, output a single line: DRAFT_WRITTEN",
|
|
72
|
+
].join("\n");
|
|
73
|
+
}
|
|
60
74
|
/**
|
|
61
75
|
* Build the prompt for `akm reflect [ref]`. Asks the agent to review an
|
|
62
76
|
* existing asset (plus any negative feedback / lint findings) and propose
|
|
@@ -105,7 +119,7 @@ export function buildReflectPrompt(input) {
|
|
|
105
119
|
sections.push(`- ${line}`);
|
|
106
120
|
}
|
|
107
121
|
sections.push("Produce a single proposal that addresses the feedback and respects the asset-type contract.");
|
|
108
|
-
sections.push(
|
|
122
|
+
sections.push(input.draftFilePath ? fileWriteContract(input.draftFilePath) : RESPONSE_CONTRACT_JSON);
|
|
109
123
|
return sections.join("\n\n");
|
|
110
124
|
}
|
|
111
125
|
/**
|
|
@@ -124,19 +138,35 @@ export function buildProposePrompt(input) {
|
|
|
124
138
|
sections.push(`- ${line}`);
|
|
125
139
|
}
|
|
126
140
|
sections.push("Produce a single proposal that, if accepted, would land as the asset described above.");
|
|
127
|
-
sections.push(
|
|
141
|
+
sections.push(input.draftFilePath ? fileWriteContract(input.draftFilePath) : RESPONSE_CONTRACT_JSON);
|
|
128
142
|
return sections.join("\n\n");
|
|
129
143
|
}
|
|
130
144
|
/**
|
|
131
145
|
* Parse agent stdout into a proposal payload. The agent contract requires a
|
|
132
146
|
* single JSON object; anything else is reported as a parse error so callers
|
|
133
147
|
* can map to {@link AgentFailureReason} `parse_error`.
|
|
148
|
+
*
|
|
149
|
+
* Resilient to two common local-LLM failure modes:
|
|
150
|
+
* 1. `<think>…</think>` blocks emitted before the JSON (stripped by `stripJsonFences`).
|
|
151
|
+
* 2. Prose preamble / postamble around the JSON object (handled by `extractEmbeddedJson`).
|
|
134
152
|
*/
|
|
135
153
|
export function parseAgentProposalPayload(stdout) {
|
|
136
154
|
const trimmed = stripJsonFences(stdout).trim();
|
|
137
155
|
if (!trimmed)
|
|
138
156
|
throw new Error("agent produced empty output");
|
|
139
|
-
|
|
157
|
+
let parsed;
|
|
158
|
+
try {
|
|
159
|
+
parsed = JSON.parse(trimmed);
|
|
160
|
+
}
|
|
161
|
+
catch (directErr) {
|
|
162
|
+
// Agent output contains prose before/after the JSON object (e.g. a local
|
|
163
|
+
// LLM that narrates before responding). Try extracting the first balanced
|
|
164
|
+
// top-level `{…}` from the text rather than failing immediately.
|
|
165
|
+
const embedded = extractEmbeddedJson(trimmed);
|
|
166
|
+
if (!embedded)
|
|
167
|
+
throw directErr;
|
|
168
|
+
parsed = embedded;
|
|
169
|
+
}
|
|
140
170
|
if (typeof parsed.ref !== "string" || !parsed.ref.trim()) {
|
|
141
171
|
throw new Error('agent response missing required string field "ref"');
|
|
142
172
|
}
|
|
@@ -153,15 +183,65 @@ export function parseAgentProposalPayload(stdout) {
|
|
|
153
183
|
return out;
|
|
154
184
|
}
|
|
155
185
|
/**
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
* `agent/`
|
|
159
|
-
* v1 spec §9.7
|
|
186
|
+
* Extract the first balanced top-level `{…}` object from `text`. Used as a
|
|
187
|
+
* fallback when direct `JSON.parse` fails due to surrounding prose. Kept
|
|
188
|
+
* local to `agent/` (mirrors `parseEmbeddedJsonResponse` in `src/llm/client.ts`
|
|
189
|
+
* without importing across the one-way boundary — v1 spec §9.7).
|
|
190
|
+
*/
|
|
191
|
+
function extractEmbeddedJson(text) {
|
|
192
|
+
for (let start = 0; start < text.length; start++) {
|
|
193
|
+
if (text[start] !== "{")
|
|
194
|
+
continue;
|
|
195
|
+
let depth = 0;
|
|
196
|
+
let inString = false;
|
|
197
|
+
let escaped = false;
|
|
198
|
+
for (let i = start; i < text.length; i++) {
|
|
199
|
+
const ch = text[i];
|
|
200
|
+
if (inString) {
|
|
201
|
+
if (escaped) {
|
|
202
|
+
escaped = false;
|
|
203
|
+
}
|
|
204
|
+
else if (ch === "\\") {
|
|
205
|
+
escaped = true;
|
|
206
|
+
}
|
|
207
|
+
else if (ch === '"') {
|
|
208
|
+
inString = false;
|
|
209
|
+
}
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
if (ch === '"') {
|
|
213
|
+
inString = true;
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
if (ch === "{")
|
|
217
|
+
depth++;
|
|
218
|
+
if (ch === "}") {
|
|
219
|
+
depth--;
|
|
220
|
+
if (depth === 0) {
|
|
221
|
+
try {
|
|
222
|
+
return JSON.parse(text.slice(start, i + 1));
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return undefined;
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Strip `\`\`\`json … \`\`\`` fences and `<think>…</think>` reasoning blocks
|
|
235
|
+
* from agent output. Mirrors `client.ts` but kept local to `agent/` per v1
|
|
236
|
+
* spec §9.7 (one-way boundary — `agent/` does not import from `llm/`).
|
|
160
237
|
*/
|
|
161
238
|
export function stripJsonFences(text) {
|
|
162
|
-
const
|
|
163
|
-
|
|
239
|
+
const stripped = text
|
|
240
|
+
.trim()
|
|
241
|
+
.replace(/<think>[\s\S]*?<\/think>/gi, "")
|
|
242
|
+
.trim();
|
|
243
|
+
const fenced = stripped.match(/^```(?:json)?\s*\n([\s\S]*?)\n```$/);
|
|
164
244
|
if (fenced)
|
|
165
|
-
return fenced[1] ??
|
|
166
|
-
return
|
|
245
|
+
return fenced[1] ?? stripped;
|
|
246
|
+
return stripped;
|
|
167
247
|
}
|
|
@@ -252,21 +252,74 @@ export async function runAgent(profile, prompt, options = {}) {
|
|
|
252
252
|
};
|
|
253
253
|
}
|
|
254
254
|
if (parseOutput === "json" && stdioMode === "captured") {
|
|
255
|
+
// Strip <think> blocks and code fences, then try direct parse with
|
|
256
|
+
// embedded-JSON fallback for local LLMs that emit prose around the payload.
|
|
257
|
+
const cleaned = stdout
|
|
258
|
+
.trim()
|
|
259
|
+
.replace(/<think>[\s\S]*?<\/think>/gi, "")
|
|
260
|
+
.trim()
|
|
261
|
+
.replace(/^```(?:json)?\s*\n?/, "")
|
|
262
|
+
.replace(/\n?```\s*$/, "")
|
|
263
|
+
.trim();
|
|
264
|
+
let parsed;
|
|
255
265
|
try {
|
|
256
|
-
|
|
257
|
-
return { ok: true, exitCode, stdout, stderr, durationMs, parsed };
|
|
266
|
+
parsed = JSON.parse(cleaned);
|
|
258
267
|
}
|
|
259
|
-
catch
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
268
|
+
catch {
|
|
269
|
+
// Fallback: extract the first balanced {…} from prose output.
|
|
270
|
+
let found;
|
|
271
|
+
for (let s = 0; s < cleaned.length; s++) {
|
|
272
|
+
if (cleaned[s] !== "{")
|
|
273
|
+
continue;
|
|
274
|
+
let depth = 0, inStr = false, esc = false;
|
|
275
|
+
for (let i = s; i < cleaned.length; i++) {
|
|
276
|
+
const c = cleaned[i];
|
|
277
|
+
if (inStr) {
|
|
278
|
+
if (esc) {
|
|
279
|
+
esc = false;
|
|
280
|
+
}
|
|
281
|
+
else if (c === "\\") {
|
|
282
|
+
esc = true;
|
|
283
|
+
}
|
|
284
|
+
else if (c === '"') {
|
|
285
|
+
inStr = false;
|
|
286
|
+
}
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
if (c === '"') {
|
|
290
|
+
inStr = true;
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
if (c === "{")
|
|
294
|
+
depth++;
|
|
295
|
+
if (c === "}") {
|
|
296
|
+
depth--;
|
|
297
|
+
if (depth === 0) {
|
|
298
|
+
try {
|
|
299
|
+
found = JSON.parse(cleaned.slice(s, i + 1));
|
|
300
|
+
}
|
|
301
|
+
catch { }
|
|
302
|
+
break;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
if (found !== undefined)
|
|
307
|
+
break;
|
|
308
|
+
}
|
|
309
|
+
if (found === undefined) {
|
|
310
|
+
return {
|
|
311
|
+
ok: false,
|
|
312
|
+
exitCode,
|
|
313
|
+
stdout,
|
|
314
|
+
stderr,
|
|
315
|
+
durationMs,
|
|
316
|
+
reason: "parse_error",
|
|
317
|
+
error: "no JSON object found in agent output",
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
parsed = found;
|
|
269
321
|
}
|
|
322
|
+
return { ok: true, exitCode, stdout, stderr, durationMs, parsed };
|
|
270
323
|
}
|
|
271
324
|
return { ok: true, exitCode, stdout, stderr, durationMs };
|
|
272
325
|
}
|
package/dist/llm/client.js
CHANGED
|
@@ -39,6 +39,7 @@ export function redactErrorBody(input) {
|
|
|
39
39
|
return out;
|
|
40
40
|
}
|
|
41
41
|
export async function chatCompletion(config, messages, options) {
|
|
42
|
+
const timeoutMs = options?.timeoutMs ?? config.timeoutMs ?? 120_000;
|
|
42
43
|
const headers = { "Content-Type": "application/json" };
|
|
43
44
|
if (config.apiKey) {
|
|
44
45
|
headers.Authorization = `Bearer ${config.apiKey}`;
|
|
@@ -53,7 +54,7 @@ export async function chatCompletion(config, messages, options) {
|
|
|
53
54
|
max_tokens: options?.maxTokens ?? config.maxTokens ?? 512,
|
|
54
55
|
...config.extraParams,
|
|
55
56
|
}),
|
|
56
|
-
},
|
|
57
|
+
}, timeoutMs, options?.signal);
|
|
57
58
|
if (!response.ok) {
|
|
58
59
|
const rawBody = await response.text().catch(() => "");
|
|
59
60
|
const safeBody = redactErrorBody(rawBody);
|
|
@@ -64,12 +65,49 @@ export async function chatCompletion(config, messages, options) {
|
|
|
64
65
|
}
|
|
65
66
|
/** Strip leading/trailing markdown code fences from an LLM response. */
|
|
66
67
|
export function stripJsonFences(raw) {
|
|
67
|
-
|
|
68
|
+
const repaired = raw
|
|
68
69
|
.trim()
|
|
69
70
|
.replace(/<think>[\s\S]*?<\/think>/gi, "")
|
|
70
71
|
.replace(/^```(?:json)?\s*\n?/i, "")
|
|
71
72
|
.replace(/\n?```\s*$/i, "")
|
|
72
73
|
.trim();
|
|
74
|
+
let out = "";
|
|
75
|
+
let inString = false;
|
|
76
|
+
let escaped = false;
|
|
77
|
+
for (let i = 0; i < repaired.length; i++) {
|
|
78
|
+
const ch = repaired[i];
|
|
79
|
+
if (escaped) {
|
|
80
|
+
out += ch;
|
|
81
|
+
escaped = false;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (ch === "\\" && inString) {
|
|
85
|
+
out += ch;
|
|
86
|
+
escaped = true;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (ch === '"') {
|
|
90
|
+
inString = !inString;
|
|
91
|
+
out += ch;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (inString) {
|
|
95
|
+
if (ch === "\n") {
|
|
96
|
+
out += "\\n";
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (ch === "\r") {
|
|
100
|
+
out += "\\r";
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (ch === "\t") {
|
|
104
|
+
out += "\\t";
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
out += ch;
|
|
109
|
+
}
|
|
110
|
+
return out;
|
|
73
111
|
}
|
|
74
112
|
/** Parse a possibly-fenced JSON response. Returns undefined if invalid. */
|
|
75
113
|
export function parseJsonResponse(raw) {
|
|
@@ -26,8 +26,6 @@ const MAX_BODY_CHARS = 4000;
|
|
|
26
26
|
const MAX_ENTITIES_PER_ASSET = 32;
|
|
27
27
|
/** Hard cap on relations returned per asset. */
|
|
28
28
|
const MAX_RELATIONS_PER_ASSET = 32;
|
|
29
|
-
/** Hard timeout for the LLM call; an `akm index` run must not hang on a misbehaving endpoint. */
|
|
30
|
-
const LLM_TIMEOUT_MS = 30_000;
|
|
31
29
|
const SYSTEM_PROMPT = "You extract a knowledge graph from developer notes. Return only valid JSON. " + "No prose, no markdown fences.";
|
|
32
30
|
const USER_PROMPT_PREFIX = `Extract entities and relations from the asset body below.
|
|
33
31
|
|
|
@@ -61,9 +59,9 @@ export async function extractGraphFromBody(llmConfig, body, signal) {
|
|
|
61
59
|
chatCompletion(llmConfig, [
|
|
62
60
|
{ role: "system", content: SYSTEM_PROMPT },
|
|
63
61
|
{ role: "user", content: userPrompt },
|
|
64
|
-
], { maxTokens: 1024, temperature: 0.1, signal }),
|
|
62
|
+
], { maxTokens: 1024, temperature: 0.1, timeoutMs: llmConfig.timeoutMs ?? 120_000, signal }),
|
|
65
63
|
new Promise((_, reject) => {
|
|
66
|
-
timeoutHandle = setTimeout(() => reject(new Error("graph extraction timed out")),
|
|
64
|
+
timeoutHandle = setTimeout(() => reject(new Error("graph extraction timed out")), llmConfig.timeoutMs ?? 120_000);
|
|
67
65
|
}),
|
|
68
66
|
]);
|
|
69
67
|
if (!raw)
|
package/dist/llm/memory-infer.js
CHANGED
|
@@ -20,8 +20,6 @@ import { warn } from "../core/warn";
|
|
|
20
20
|
import { chatCompletion, parseEmbeddedJsonResponse } from "./client";
|
|
21
21
|
/** Hard cap on body chars sent to the model — pragmatic and matches `runLlmEnrich`. */
|
|
22
22
|
const MAX_BODY_CHARS = 4000;
|
|
23
|
-
/** Hard timeout for the LLM call. The index run must not hang on a misbehaving endpoint. */
|
|
24
|
-
const LLM_TIMEOUT_MS = 30_000;
|
|
25
23
|
const SYSTEM_PROMPT = "You compress a developer memory into one high-signal derived memory for later retrieval. " +
|
|
26
24
|
"Return only valid JSON. No prose outside the JSON object. No markdown fences.";
|
|
27
25
|
const USER_PROMPT_PREFIX = `Compress the memory below into one concise, information-dense derived memory.
|
|
@@ -68,9 +66,14 @@ export async function compressMemoryToDerivedMemory(llmConfig, body, signal) {
|
|
|
68
66
|
chatCompletion(llmConfig, [
|
|
69
67
|
{ role: "system", content: SYSTEM_PROMPT },
|
|
70
68
|
{ role: "user", content: userPrompt },
|
|
71
|
-
], {
|
|
69
|
+
], {
|
|
70
|
+
maxTokens: llmConfig.maxTokens ?? 4096,
|
|
71
|
+
temperature: 0.1,
|
|
72
|
+
timeoutMs: llmConfig.timeoutMs ?? 120_000,
|
|
73
|
+
signal,
|
|
74
|
+
}),
|
|
72
75
|
new Promise((_, reject) => {
|
|
73
|
-
timeoutHandle = setTimeout(() => reject(new Error("memory inference timed out")),
|
|
76
|
+
timeoutHandle = setTimeout(() => reject(new Error("memory inference timed out")), llmConfig.timeoutMs ?? 120_000);
|
|
74
77
|
}),
|
|
75
78
|
]);
|
|
76
79
|
if (!raw)
|
package/dist/output/cli-hints.js
CHANGED
|
@@ -24,6 +24,10 @@ For workflow tasks:
|
|
|
24
24
|
2. Do the step work in your workspace
|
|
25
25
|
3. \`akm workflow complete <run-id> --step <step-id>\` — mark done, get next step
|
|
26
26
|
|
|
27
|
+
Workflow runs are scoped to your current project/worktree/directory. Ref-based
|
|
28
|
+
commands like \`workflow next workflow:<name>\`, \`workflow status workflow:<name>\`,
|
|
29
|
+
and \`workflow list\` operate within the current scope only.
|
|
30
|
+
|
|
27
31
|
## Quick Reference
|
|
28
32
|
|
|
29
33
|
\`\`\`sh
|
|
@@ -55,7 +59,7 @@ akm registry search "<query>" # Search all registries
|
|
|
55
59
|
| knowledge | A reference doc (use \`toc\` or \`section "..."\` to navigate) |
|
|
56
60
|
| workflow | Parsed steps plus workflow-specific execution commands |
|
|
57
61
|
| memory | Recalled context (read the content for background information) |
|
|
58
|
-
| vault | Key names only; use vault
|
|
62
|
+
| vault | Key names only; use \`akm vault path\` or \`akm vault run\` to use values safely |
|
|
59
63
|
| wiki | A page in a multi-wiki knowledge base. For any wiki task, start with \`akm wiki list\`, then \`akm wiki ingest <name>\` for the workflow. Run \`akm wiki -h\` for the full surface. |
|
|
60
64
|
|
|
61
65
|
When an asset meaningfully helps or fails, record that with \`akm feedback\` so
|
|
@@ -186,24 +190,29 @@ akm vault create prod # Create a new vault
|
|
|
186
190
|
akm vault set prod DB_URL postgres://... # Set a key (or KEY=VALUE combined form)
|
|
187
191
|
akm vault set prod DB_URL=postgres://... # Combined KEY=VALUE form also works
|
|
188
192
|
akm vault unset prod DB_URL # Remove a key
|
|
189
|
-
akm vault list
|
|
190
|
-
akm vault
|
|
191
|
-
akm vault
|
|
193
|
+
akm vault list # List all vaults across all stashes with key names
|
|
194
|
+
akm vault path vault:prod # Print the vault file path for shell loading
|
|
195
|
+
akm vault run vault:prod -- env # Run one command with all vault vars injected
|
|
196
|
+
akm vault run vault:prod/DB_URL -- printenv DB_URL # Inject one key for one command
|
|
192
197
|
\`\`\`
|
|
193
198
|
|
|
194
199
|
## Workflows
|
|
195
200
|
|
|
196
201
|
Step-based workflows stored as \`<stashDir>/workflows/<name>.md\`.
|
|
197
202
|
|
|
203
|
+
Ref-based workflow commands are scoped to the current project/worktree/directory,
|
|
204
|
+
so one active run does not block unrelated directories from starting the same
|
|
205
|
+
workflow. Direct run-id commands still target the exact run.
|
|
206
|
+
|
|
198
207
|
\`\`\`sh
|
|
199
208
|
akm workflow template # Print a starter workflow template
|
|
200
209
|
akm workflow create ship-release # Scaffold a new workflow asset
|
|
201
|
-
akm workflow start workflow:ship-release # Start a new run
|
|
202
|
-
akm workflow next workflow:ship-release # Advance to the next step (or auto-start)
|
|
210
|
+
akm workflow start workflow:ship-release # Start a new run in the current scope
|
|
211
|
+
akm workflow next workflow:ship-release # Advance to the next step (or auto-start) in the current scope
|
|
203
212
|
akm workflow complete <run-id> # Mark a step complete and advance
|
|
204
|
-
akm workflow status <run-id> # Show
|
|
213
|
+
akm workflow status <run-id> # Show the exact run by id
|
|
205
214
|
akm workflow resume <run-id> # Resume a blocked or failed run
|
|
206
|
-
akm workflow list # List
|
|
215
|
+
akm workflow list # List workflow runs in the current scope
|
|
207
216
|
\`\`\`
|
|
208
217
|
|
|
209
218
|
## Clone
|
package/dist/output/renderers.js
CHANGED
|
@@ -563,12 +563,17 @@ const vaultEnvRenderer = {
|
|
|
563
563
|
type: "vault",
|
|
564
564
|
name,
|
|
565
565
|
path: ctx.absPath,
|
|
566
|
-
action: 'Vault — keys + comments only. Use `
|
|
566
|
+
action: 'Vault — keys + comments only. Use `source "$(akm vault path <ref>)"` to load values into the current shell, or `akm vault run <ref[/KEY]> -- <command>` to run with injected env. Values stay on disk and are never written to akm\'s stdout.',
|
|
567
567
|
description: comments.length > 0 ? comments.join("\n") : undefined,
|
|
568
568
|
keys,
|
|
569
569
|
comments,
|
|
570
570
|
};
|
|
571
571
|
},
|
|
572
|
+
enrichSearchHit(hit, _stashDir) {
|
|
573
|
+
const { keys } = listVaultKeys(hit.path);
|
|
574
|
+
if (keys.length > 0)
|
|
575
|
+
hit.keys = keys;
|
|
576
|
+
},
|
|
572
577
|
extractMetadata(entry, ctx) {
|
|
573
578
|
// Re-derive from the file directly to guarantee no value ever transits
|
|
574
579
|
// through any other code path. Caller already short-circuits in
|
package/dist/output/shapes.js
CHANGED
|
@@ -407,19 +407,22 @@ export function shapeSearchHit(hit, detail) {
|
|
|
407
407
|
// `ref` is included at `brief` so agents can run `akm show <ref>` without
|
|
408
408
|
// needing --detail full or --for-agent (REC-03).
|
|
409
409
|
if (detail === "brief")
|
|
410
|
-
return pickFields(hit, ["type", "name", "ref", "action", "estimatedTokens"]);
|
|
410
|
+
return pickFields(hit, ["type", "name", "ref", "action", "estimatedTokens", "keys"]);
|
|
411
411
|
if (detail === "normal") {
|
|
412
412
|
// `warnings` is projected at `normal` so non-fatal hit-level issues are
|
|
413
413
|
// visible without forcing callers up to `--detail full`. Optional
|
|
414
414
|
// `quality` (v1 spec §4.2) is also surfaced when present so callers
|
|
415
415
|
// can see why a `proposed` entry showed up under `--include-proposed`.
|
|
416
|
-
|
|
416
|
+
const shaped = capDescription(pickFields(hit, ["type", "name", "description", "action", "score", "estimatedTokens", "warnings", "quality"]), NORMAL_DESCRIPTION_LIMIT);
|
|
417
|
+
if (Array.isArray(hit.keys) && hit.keys.length > 0)
|
|
418
|
+
shaped.keys = hit.keys;
|
|
419
|
+
return shaped;
|
|
417
420
|
}
|
|
418
421
|
return hit;
|
|
419
422
|
}
|
|
420
423
|
/** Agent-optimized search hit: only fields an LLM agent needs to decide and act */
|
|
421
424
|
export function shapeSearchHitForAgent(hit) {
|
|
422
|
-
const picked = pickFields(hit, ["name", "ref", "type", "description", "action", "score", "estimatedTokens"]);
|
|
425
|
+
const picked = pickFields(hit, ["name", "ref", "type", "description", "action", "score", "estimatedTokens", "keys"]);
|
|
423
426
|
return capDescription(picked, NORMAL_DESCRIPTION_LIMIT);
|
|
424
427
|
}
|
|
425
428
|
export function capDescription(hit, limit) {
|
|
@@ -449,6 +452,7 @@ export function shapeShowOutput(result, detail, forAgent = false) {
|
|
|
449
452
|
"run",
|
|
450
453
|
"setup",
|
|
451
454
|
"cwd",
|
|
455
|
+
"activeRun",
|
|
452
456
|
"toolPolicy",
|
|
453
457
|
"modelHint",
|
|
454
458
|
"agent",
|
|
@@ -495,6 +499,7 @@ export function shapeShowOutput(result, detail, forAgent = false) {
|
|
|
495
499
|
"run",
|
|
496
500
|
"setup",
|
|
497
501
|
"cwd",
|
|
502
|
+
"activeRun",
|
|
498
503
|
"keys",
|
|
499
504
|
"comments",
|
|
500
505
|
// path and editable are always projected so JSON consumers can locate and
|
package/dist/output/text.js
CHANGED
|
@@ -404,22 +404,7 @@ export function formatRegistryBuildIndexPlain(r) {
|
|
|
404
404
|
return `Wrote registry index ${version} (${total} kits) → ${outPath}`.replace(/\s+/g, " ").trim();
|
|
405
405
|
}
|
|
406
406
|
export function formatVaultListPlain(r) {
|
|
407
|
-
//
|
|
408
|
-
if (typeof r.ref === "string" && Array.isArray(r.entries)) {
|
|
409
|
-
const ref = r.ref;
|
|
410
|
-
const entries = r.entries;
|
|
411
|
-
if (entries.length === 0) {
|
|
412
|
-
return `No keys in ${ref}. Set one with \`akm vault set ${ref} KEY=VALUE\`.`;
|
|
413
|
-
}
|
|
414
|
-
const lines = [ref];
|
|
415
|
-
for (const e of entries) {
|
|
416
|
-
const key = String(e.key ?? "?");
|
|
417
|
-
const comment = typeof e.comment === "string" && e.comment ? ` # ${e.comment}` : "";
|
|
418
|
-
lines.push(` ${key}${comment}`);
|
|
419
|
-
}
|
|
420
|
-
return lines.join("\n");
|
|
421
|
-
}
|
|
422
|
-
// Multi-vault listing: { vaults: [{ ref, path, keyCount }, ...] }
|
|
407
|
+
// Multi-vault listing: { vaults: [{ ref, path, keys }, ...] }
|
|
423
408
|
const vaults = Array.isArray(r.vaults) ? r.vaults : [];
|
|
424
409
|
if (vaults.length === 0) {
|
|
425
410
|
return "No vaults. Create one with `akm vault create <name>` then `akm vault set vault:<name> KEY=VALUE`.";
|
|
@@ -427,8 +412,17 @@ export function formatVaultListPlain(r) {
|
|
|
427
412
|
const lines = [];
|
|
428
413
|
for (const v of vaults) {
|
|
429
414
|
const ref = String(v.ref ?? "?");
|
|
430
|
-
const
|
|
431
|
-
lines.
|
|
415
|
+
const keys = Array.isArray(v.keys) ? v.keys.map(String) : [];
|
|
416
|
+
if (lines.length > 0)
|
|
417
|
+
lines.push("");
|
|
418
|
+
lines.push(`## ${ref}`);
|
|
419
|
+
if (keys.length === 0) {
|
|
420
|
+
lines.push("- (no keys)");
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
for (const key of keys) {
|
|
424
|
+
lines.push(`- ${key}`);
|
|
425
|
+
}
|
|
432
426
|
}
|
|
433
427
|
return lines.join("\n");
|
|
434
428
|
}
|
|
@@ -795,7 +789,7 @@ function isCommandOutputSkill(lines) {
|
|
|
795
789
|
export function formatWorkflowListPlain(result) {
|
|
796
790
|
const runs = Array.isArray(result.runs) ? result.runs : [];
|
|
797
791
|
if (runs.length === 0) {
|
|
798
|
-
return "No workflow runs. Start one with `akm workflow next workflow:<name>` or author one with `akm workflow create <name>`.";
|
|
792
|
+
return "No workflow runs in the current working scope. Start one with `akm workflow next workflow:<name>` or author one with `akm workflow create <name>`.";
|
|
799
793
|
}
|
|
800
794
|
return runs
|
|
801
795
|
.map((run) => {
|
|
@@ -906,6 +900,8 @@ export function formatSearchPlain(r, detail) {
|
|
|
906
900
|
lines.push(` ref: ${String(hit.ref)}`);
|
|
907
901
|
if (hit.origin !== undefined)
|
|
908
902
|
lines.push(` origin: ${String(hit.origin)}`);
|
|
903
|
+
if (Array.isArray(hit.keys) && hit.keys.length > 0)
|
|
904
|
+
lines.push(` keys: ${hit.keys.join(", ")}`);
|
|
909
905
|
if (hit.size)
|
|
910
906
|
lines.push(` size: ${String(hit.size)}`);
|
|
911
907
|
if (hit.action)
|
|
@@ -1088,6 +1084,9 @@ export function formatCuratePlain(r, detail) {
|
|
|
1088
1084
|
lines.push(` ref: ${String(item.ref)}`);
|
|
1089
1085
|
if (item.id)
|
|
1090
1086
|
lines.push(` id: ${String(item.id)}`);
|
|
1087
|
+
if (Array.isArray(item.keys) && item.keys.length > 0) {
|
|
1088
|
+
lines.push(` keys: ${item.keys.join(", ")}`);
|
|
1089
|
+
}
|
|
1091
1090
|
if (Array.isArray(item.parameters) && item.parameters.length > 0) {
|
|
1092
1091
|
lines.push(` parameters: ${item.parameters.join(", ")}`);
|
|
1093
1092
|
}
|
|
@@ -407,7 +407,7 @@ export function saveGitStash(name, message, writableOverride) {
|
|
|
407
407
|
let writable = false;
|
|
408
408
|
if (name) {
|
|
409
409
|
const config = loadConfig();
|
|
410
|
-
const stash = (config.sources ?? config.stashes ?? []
|
|
410
|
+
const stash = findGitStashByTarget(config.sources ?? config.stashes ?? [], name);
|
|
411
411
|
if (!stash)
|
|
412
412
|
throw new UsageError(`No git stash found with name "${name}"`);
|
|
413
413
|
if (!GIT_STASH_TYPES.has(stash.type)) {
|
|
@@ -468,5 +468,47 @@ export function saveGitStash(name, message, writableOverride) {
|
|
|
468
468
|
output: (commitResult.stdout + pushResult.stdout).trim() || "changes committed and pushed",
|
|
469
469
|
};
|
|
470
470
|
}
|
|
471
|
+
function findGitStashByTarget(stashes, target) {
|
|
472
|
+
return stashes.find((stash) => matchesGitStashTarget(stash, target));
|
|
473
|
+
}
|
|
474
|
+
function matchesGitStashTarget(stash, target) {
|
|
475
|
+
if (!GIT_STASH_TYPES.has(stash.type))
|
|
476
|
+
return false;
|
|
477
|
+
if (stash.name === target || stash.url === target)
|
|
478
|
+
return true;
|
|
479
|
+
if (!stash.url)
|
|
480
|
+
return false;
|
|
481
|
+
try {
|
|
482
|
+
const repo = parseGitRepoUrl(stash.url);
|
|
483
|
+
if (repo.canonicalUrl === target)
|
|
484
|
+
return true;
|
|
485
|
+
return buildGithubTargetAliases(repo.canonicalUrl).has(target);
|
|
486
|
+
}
|
|
487
|
+
catch {
|
|
488
|
+
return false;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
function buildGithubTargetAliases(canonicalUrl) {
|
|
492
|
+
try {
|
|
493
|
+
const parsed = new URL(canonicalUrl);
|
|
494
|
+
if (parsed.hostname !== "github.com")
|
|
495
|
+
return new Set();
|
|
496
|
+
const segments = parsed.pathname.split("/").filter(Boolean);
|
|
497
|
+
if (segments.length < 2)
|
|
498
|
+
return new Set();
|
|
499
|
+
const owner = segments[0];
|
|
500
|
+
const repo = segments[1];
|
|
501
|
+
const aliases = new Set([`${owner}/${repo}`, `github:${owner}/${repo}`]);
|
|
502
|
+
if (segments[2] === "tree" && segments.length >= 4) {
|
|
503
|
+
const ref = segments.slice(3).join("/");
|
|
504
|
+
aliases.add(`${owner}/${repo}#${ref}`);
|
|
505
|
+
aliases.add(`github:${owner}/${repo}#${ref}`);
|
|
506
|
+
}
|
|
507
|
+
return aliases;
|
|
508
|
+
}
|
|
509
|
+
catch {
|
|
510
|
+
return new Set();
|
|
511
|
+
}
|
|
512
|
+
}
|
|
471
513
|
// ── Exports ─────────────────────────────────────────────────────────────────
|
|
472
514
|
export { ensureGitMirror, GitSourceProvider, getCachePaths, parseGitRepoUrl };
|
package/dist/workflows/db.js
CHANGED
|
@@ -21,6 +21,7 @@ function ensureWorkflowSchema(db) {
|
|
|
21
21
|
CREATE TABLE IF NOT EXISTS workflow_runs (
|
|
22
22
|
id TEXT PRIMARY KEY,
|
|
23
23
|
workflow_ref TEXT NOT NULL,
|
|
24
|
+
scope_key TEXT,
|
|
24
25
|
workflow_entry_id INTEGER,
|
|
25
26
|
workflow_title TEXT NOT NULL,
|
|
26
27
|
status TEXT NOT NULL CHECK (status IN ('active', 'completed', 'blocked', 'failed')),
|
|
@@ -52,4 +53,12 @@ function ensureWorkflowSchema(db) {
|
|
|
52
53
|
CREATE INDEX IF NOT EXISTS idx_workflow_run_steps_run_sequence
|
|
53
54
|
ON workflow_run_steps(run_id, sequence_index);
|
|
54
55
|
`);
|
|
56
|
+
const columns = db
|
|
57
|
+
.query("PRAGMA table_info(workflow_runs)")
|
|
58
|
+
.all()
|
|
59
|
+
.map((column) => column.name);
|
|
60
|
+
if (!columns.includes("scope_key")) {
|
|
61
|
+
db.exec("ALTER TABLE workflow_runs ADD COLUMN scope_key TEXT");
|
|
62
|
+
}
|
|
63
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_workflow_runs_scope_ref_status ON workflow_runs(scope_key, workflow_ref, status)");
|
|
55
64
|
}
|