akm-cli 0.7.5 → 0.8.0-rc.6
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/{.github/CHANGELOG.md → CHANGELOG.md} +113 -2
- package/README.md +20 -4
- package/SECURITY.md +93 -0
- package/dist/cli/config-migrate.js +144 -0
- package/dist/cli/config-validate.js +39 -0
- package/dist/cli/confirm.js +73 -0
- package/dist/cli/parse-args.js +133 -0
- package/dist/cli.js +1995 -551
- package/dist/commands/agent-dispatch.js +110 -0
- package/dist/commands/agent-support.js +68 -0
- package/dist/commands/completions.js +3 -0
- package/dist/commands/config-cli.js +130 -534
- package/dist/commands/consolidate.js +1531 -0
- package/dist/commands/curate.js +44 -3
- package/dist/commands/db-cli.js +23 -0
- package/dist/commands/distill-promotion-policy.js +660 -0
- package/dist/commands/distill.js +990 -75
- package/dist/commands/eval-cases.js +43 -0
- package/dist/commands/events.js +5 -23
- package/dist/commands/graph.js +477 -0
- package/dist/commands/health.js +400 -0
- package/dist/commands/help/help-accept.md +9 -0
- package/dist/commands/help/help-improve.md +77 -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 +54 -46
- package/dist/commands/improve-profiles.js +146 -0
- package/dist/commands/improve-result-file.js +103 -0
- package/dist/commands/improve.js +2175 -0
- package/dist/commands/info.js +5 -2
- package/dist/commands/init.js +50 -2
- package/dist/commands/installed-stashes.js +102 -139
- package/dist/commands/knowledge.js +136 -0
- package/dist/commands/lint/agent-linter.js +49 -0
- package/dist/commands/lint/base-linter.js +479 -0
- package/dist/commands/lint/command-linter.js +49 -0
- package/dist/commands/lint/default-linter.js +16 -0
- package/dist/commands/lint/index.js +183 -0
- package/dist/commands/lint/knowledge-linter.js +16 -0
- package/dist/commands/lint/markdown-insertion.js +343 -0
- package/dist/commands/lint/memory-linter.js +61 -0
- package/dist/commands/lint/registry.js +36 -0
- package/dist/commands/lint/skill-linter.js +45 -0
- package/dist/commands/lint/task-linter.js +50 -0
- package/dist/commands/lint/types.js +4 -0
- package/dist/commands/lint/vault-key-rules.js +139 -0
- package/dist/commands/lint/workflow-linter.js +56 -0
- package/dist/commands/lint.js +4 -0
- package/dist/commands/migration-help.js +5 -2
- package/dist/commands/proposal.js +66 -12
- package/dist/commands/propose.js +86 -31
- package/dist/commands/reflect.js +1119 -73
- package/dist/commands/registry-search.js +5 -2
- package/dist/commands/remember.js +69 -6
- package/dist/commands/schema-repair.js +203 -0
- package/dist/commands/search.js +115 -14
- package/dist/commands/self-update.js +3 -0
- package/dist/commands/show.js +144 -25
- package/dist/commands/source-add.js +17 -45
- package/dist/commands/source-clone.js +3 -0
- package/dist/commands/source-manage.js +14 -19
- package/dist/commands/tasks.js +438 -0
- package/dist/commands/url-checker.js +42 -0
- package/dist/commands/vault.js +130 -77
- package/dist/core/action-contributors.js +28 -0
- package/dist/core/asset-ref.js +7 -0
- package/dist/core/asset-registry.js +7 -16
- package/dist/core/asset-serialize.js +88 -0
- package/dist/core/asset-spec.js +22 -0
- package/dist/core/common.js +157 -0
- package/dist/core/concurrent.js +25 -0
- package/dist/core/config-io.js +347 -0
- package/dist/core/config-migration.js +625 -0
- package/dist/core/config-schema.js +501 -0
- package/dist/core/config-sources.js +108 -0
- package/dist/core/config-types.js +4 -0
- package/dist/core/config-walker.js +337 -0
- package/dist/core/config.js +327 -987
- package/dist/core/errors.js +40 -19
- package/dist/core/events.js +91 -138
- package/dist/core/file-lock.js +104 -0
- package/dist/core/frontmatter.js +3 -6
- package/dist/core/lesson-lint.js +3 -0
- package/dist/core/markdown.js +20 -0
- package/dist/core/memory-belief.js +62 -0
- package/dist/core/memory-contradiction-detect.js +274 -0
- package/dist/core/memory-improve.js +806 -0
- package/dist/core/parse.js +158 -0
- package/dist/core/paths.js +326 -14
- package/dist/core/proposal-quality-validators.js +364 -0
- package/dist/core/proposal-validators.js +69 -0
- package/dist/core/proposals.js +498 -42
- package/dist/core/state-db.js +927 -0
- package/dist/core/text-truncation.js +107 -0
- package/dist/core/time.js +54 -0
- package/dist/core/warn.js +62 -1
- package/dist/core/write-source.js +3 -0
- package/dist/indexer/db-backup.js +391 -0
- package/dist/indexer/db-search.js +152 -253
- package/dist/indexer/db.js +933 -103
- package/dist/indexer/ensure-index.js +64 -0
- package/dist/indexer/file-context.js +3 -0
- package/dist/indexer/graph-boost.js +376 -101
- package/dist/indexer/graph-db.js +391 -0
- package/dist/indexer/graph-dedup.js +95 -0
- package/dist/indexer/graph-extraction.js +550 -124
- package/dist/indexer/index-context.js +4 -0
- package/dist/indexer/indexer.js +506 -291
- package/dist/indexer/llm-cache.js +47 -0
- package/dist/indexer/manifest.js +3 -0
- package/dist/indexer/matchers.js +148 -160
- package/dist/indexer/memory-inference.js +99 -74
- package/dist/indexer/metadata-contributors.js +29 -0
- package/dist/indexer/metadata.js +255 -196
- package/dist/indexer/path-resolver.js +92 -0
- package/dist/indexer/project-context.js +192 -0
- package/dist/indexer/ranking-contributors.js +331 -0
- package/dist/indexer/ranking.js +81 -0
- package/dist/indexer/search-fields.js +5 -9
- package/dist/indexer/search-hit-enrichers.js +111 -0
- package/dist/indexer/search-source.js +44 -10
- package/dist/indexer/semantic-status.js +5 -16
- package/dist/indexer/staleness-detect.js +447 -0
- package/dist/indexer/usage-events.js +12 -9
- package/dist/indexer/walker.js +28 -0
- package/dist/integrations/agent/builders.js +135 -0
- package/dist/integrations/agent/config.js +122 -230
- package/dist/integrations/agent/detect.js +3 -0
- package/dist/integrations/agent/index.js +7 -13
- package/dist/integrations/agent/model-aliases.js +55 -0
- package/dist/integrations/agent/profiles.js +70 -5
- package/dist/integrations/agent/prompts.js +150 -74
- package/dist/integrations/agent/runner.js +151 -0
- package/dist/integrations/agent/sdk-runner.js +126 -0
- package/dist/integrations/agent/spawn.js +118 -23
- package/dist/integrations/github.js +3 -0
- package/dist/integrations/lockfile.js +32 -69
- package/dist/integrations/session-logs/index.js +68 -0
- package/dist/integrations/session-logs/providers/claude-code.js +59 -0
- package/dist/integrations/session-logs/providers/opencode.js +55 -0
- package/dist/integrations/session-logs/types.js +4 -0
- package/dist/llm/call-ai.js +62 -0
- package/dist/llm/client.js +72 -124
- package/dist/llm/embedder.js +3 -19
- package/dist/llm/embedders/cache.js +3 -7
- package/dist/llm/embedders/local.js +3 -0
- package/dist/llm/embedders/remote.js +20 -8
- package/dist/llm/embedders/types.js +3 -7
- package/dist/llm/feature-gate.js +89 -48
- package/dist/llm/graph-extract.js +676 -70
- package/dist/llm/index-passes.js +9 -23
- package/dist/llm/memory-infer.js +52 -71
- package/dist/llm/metadata-enhance.js +42 -29
- package/dist/llm/prompts/graph-extract-user-prompt.md +35 -0
- package/dist/output/cli-hints-full.md +281 -0
- package/dist/output/cli-hints-short.md +65 -0
- package/dist/output/cli-hints.js +5 -318
- package/dist/output/context.js +3 -0
- package/dist/output/renderers.js +223 -256
- package/dist/output/shapes.js +150 -105
- package/dist/output/text.js +318 -30
- package/dist/registry/build-index.js +3 -0
- package/dist/registry/create-provider-registry.js +3 -0
- package/dist/registry/factory.js +3 -0
- package/dist/registry/origin-resolve.js +3 -0
- package/dist/registry/providers/index.js +3 -0
- package/dist/registry/providers/skills-sh.js +70 -49
- package/dist/registry/providers/static-index.js +53 -48
- package/dist/registry/providers/types.js +3 -24
- package/dist/registry/resolve.js +11 -16
- package/dist/registry/types.js +3 -0
- package/dist/scripts/migrate-storage.js +17307 -0
- package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +8900 -0
- package/dist/scripts/migrations/v16-to-v17.js +141 -0
- package/dist/setup/detect.js +3 -0
- package/dist/setup/ripgrep-install.js +3 -0
- package/dist/setup/ripgrep-resolve.js +3 -0
- package/dist/setup/setup.js +775 -37
- package/dist/setup/steps.js +3 -15
- package/dist/sources/include.js +3 -0
- package/dist/sources/provider-factory.js +5 -12
- package/dist/sources/provider.js +3 -20
- package/dist/sources/providers/filesystem.js +19 -23
- package/dist/sources/providers/git.js +7 -5
- package/dist/sources/providers/index.js +3 -0
- package/dist/sources/providers/install-types.js +3 -13
- package/dist/sources/providers/npm.js +3 -4
- package/dist/sources/providers/provider-utils.js +3 -0
- package/dist/sources/providers/sync-from-ref.js +3 -11
- package/dist/sources/providers/tar-utils.js +3 -0
- package/dist/sources/providers/website.js +18 -22
- package/dist/sources/resolve.js +3 -0
- package/dist/sources/types.js +3 -0
- package/dist/sources/website-ingest.js +7 -0
- package/dist/tasks/backends/cron.js +203 -0
- package/dist/tasks/backends/exec-utils.js +28 -0
- package/dist/tasks/backends/index.js +24 -0
- package/dist/tasks/backends/launchd-template.xml +19 -0
- package/dist/tasks/backends/launchd.js +187 -0
- package/dist/tasks/backends/schtasks-template.xml +29 -0
- package/dist/tasks/backends/schtasks.js +215 -0
- package/dist/tasks/parser.js +211 -0
- package/dist/tasks/resolveAkmBin.js +87 -0
- package/dist/tasks/runner.js +458 -0
- package/dist/tasks/schedule.js +211 -0
- package/dist/tasks/schema.js +15 -0
- package/dist/tasks/validator.js +62 -0
- package/dist/version.js +3 -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 +15 -0
- package/dist/wiki/wiki.js +13 -61
- package/dist/workflows/authoring.js +8 -25
- package/dist/workflows/cli.js +3 -0
- package/dist/workflows/db.js +140 -10
- package/dist/workflows/document-cache.js +3 -10
- package/dist/workflows/parser.js +3 -0
- package/dist/workflows/renderer.js +11 -3
- package/dist/workflows/runs.js +62 -91
- package/dist/workflows/schema.js +3 -0
- package/dist/workflows/scope-key.js +3 -0
- package/dist/workflows/validator.js +4 -8
- package/dist/workflows/workflow-template.md +24 -0
- package/docs/README.md +9 -2
- package/docs/data-and-telemetry.md +225 -0
- package/docs/migration/release-notes/0.7.0.md +1 -1
- package/docs/migration/release-notes/0.7.5.md +2 -2
- package/docs/migration/release-notes/0.8.0.md +48 -0
- package/docs/migration/v0.7-to-v0.8.md +1307 -0
- package/package.json +20 -8
- package/.github/LICENSE +0 -374
- package/dist/commands/install-audit.js +0 -381
- package/dist/templates/wiki-templates.js +0 -100
package/dist/workflows/db.js
CHANGED
|
@@ -1,7 +1,44 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
1
4
|
import { Database } from "bun:sqlite";
|
|
2
5
|
import fs from "node:fs";
|
|
3
6
|
import path from "node:path";
|
|
4
7
|
import { getWorkflowDbPath } from "../core/paths";
|
|
8
|
+
/**
|
|
9
|
+
* workflow.db — Durable SQLite database for workflow run state.
|
|
10
|
+
*
|
|
11
|
+
* Owns the `workflow_runs` and `workflow_run_steps` tables that track active /
|
|
12
|
+
* completed workflow executions. Like `state.db` (and unlike `index.db`), the
|
|
13
|
+
* rows here are NON-REGENERABLE — losing them is data loss. Schema must evolve
|
|
14
|
+
* via incremental, additive migrations recorded in `schema_migrations`.
|
|
15
|
+
*
|
|
16
|
+
* ## Migration-safety contract
|
|
17
|
+
*
|
|
18
|
+
* The `schema_migrations` table records every applied migration by a stable
|
|
19
|
+
* string ID. `runMigrations(db)` is idempotent: new installs run every
|
|
20
|
+
* migration in order; upgrades run only the ones not yet applied. The
|
|
21
|
+
* migration framework here intentionally mirrors `src/core/state-db.ts` so
|
|
22
|
+
* future schema evolution follows a single proven pattern.
|
|
23
|
+
*
|
|
24
|
+
* Permitted schema evolution operations (always migration-safe in SQLite):
|
|
25
|
+
* - ALTER TABLE … ADD COLUMN <name> <type> DEFAULT <value>
|
|
26
|
+
* - CREATE INDEX IF NOT EXISTS …
|
|
27
|
+
* - CREATE TABLE IF NOT EXISTS … (additive new tables)
|
|
28
|
+
*
|
|
29
|
+
* ## Bootstrapping pre-versioning databases
|
|
30
|
+
*
|
|
31
|
+
* Workflow databases created before this file gained `schema_migrations`
|
|
32
|
+
* already have the `workflow_runs.scope_key` column applied by the previous
|
|
33
|
+
* ad-hoc `PRAGMA table_info` + `ALTER TABLE` code. To avoid re-running the
|
|
34
|
+
* migration (which would no-op but still wastes work and clutters logs), the
|
|
35
|
+
* runner detects this state and back-fills the `schema_migrations` row for
|
|
36
|
+
* the scope-key migration before evaluating the migration list. See
|
|
37
|
+
* `bootstrapPreVersioningDb()`.
|
|
38
|
+
*
|
|
39
|
+
* @module workflows/db
|
|
40
|
+
*/
|
|
41
|
+
// ── Public API ───────────────────────────────────────────────────────────────
|
|
5
42
|
export function openWorkflowDatabase(dbPath = getWorkflowDbPath()) {
|
|
6
43
|
const dir = path.dirname(dbPath);
|
|
7
44
|
if (!fs.existsSync(dir)) {
|
|
@@ -10,18 +47,29 @@ export function openWorkflowDatabase(dbPath = getWorkflowDbPath()) {
|
|
|
10
47
|
const db = new Database(dbPath);
|
|
11
48
|
db.exec("PRAGMA journal_mode = WAL");
|
|
12
49
|
db.exec("PRAGMA foreign_keys = ON");
|
|
13
|
-
|
|
50
|
+
ensureBaseSchema(db);
|
|
51
|
+
runMigrations(db);
|
|
14
52
|
return db;
|
|
15
53
|
}
|
|
16
54
|
export function closeWorkflowDatabase(db) {
|
|
17
55
|
db.close();
|
|
18
56
|
}
|
|
19
|
-
|
|
57
|
+
// ── Base schema ──────────────────────────────────────────────────────────────
|
|
58
|
+
/**
|
|
59
|
+
* Create the baseline `workflow_runs` and `workflow_run_steps` tables if they
|
|
60
|
+
* do not exist. These statements are idempotent: existing databases keep their
|
|
61
|
+
* current schema, and migrations evolve them further.
|
|
62
|
+
*
|
|
63
|
+
* NOTE: the `scope_key` column on `workflow_runs` is intentionally NOT declared
|
|
64
|
+
* here. It is added by migration `001-add-scope-key`. Fresh databases will run
|
|
65
|
+
* the migration immediately on first open; pre-versioning databases that
|
|
66
|
+
* already have the column are bootstrapped — see {@link runMigrations}.
|
|
67
|
+
*/
|
|
68
|
+
function ensureBaseSchema(db) {
|
|
20
69
|
db.exec(`
|
|
21
70
|
CREATE TABLE IF NOT EXISTS workflow_runs (
|
|
22
71
|
id TEXT PRIMARY KEY,
|
|
23
72
|
workflow_ref TEXT NOT NULL,
|
|
24
|
-
scope_key TEXT,
|
|
25
73
|
workflow_entry_id INTEGER,
|
|
26
74
|
workflow_title TEXT NOT NULL,
|
|
27
75
|
status TEXT NOT NULL CHECK (status IN ('active', 'completed', 'blocked', 'failed')),
|
|
@@ -53,12 +101,94 @@ function ensureWorkflowSchema(db) {
|
|
|
53
101
|
CREATE INDEX IF NOT EXISTS idx_workflow_run_steps_run_sequence
|
|
54
102
|
ON workflow_run_steps(run_id, sequence_index);
|
|
55
103
|
`);
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* All workflow.db migrations in application order. New migrations are
|
|
107
|
+
* APPENDED — never inserted in the middle or reordered.
|
|
108
|
+
*/
|
|
109
|
+
const MIGRATIONS = [
|
|
110
|
+
// ── Migration 001 — add scope_key column ────────────────────────────────────
|
|
111
|
+
//
|
|
112
|
+
// Adds the `scope_key` column to `workflow_runs` so runs can be partitioned
|
|
113
|
+
// per stash/scope. Pre-versioning databases that already have this column
|
|
114
|
+
// are bootstrapped before this migration runs — see runMigrations().
|
|
115
|
+
{
|
|
116
|
+
id: "001-add-scope-key",
|
|
117
|
+
up: `
|
|
118
|
+
ALTER TABLE workflow_runs ADD COLUMN scope_key TEXT;
|
|
119
|
+
|
|
120
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_runs_scope_ref_status
|
|
121
|
+
ON workflow_runs(scope_key, workflow_ref, status);
|
|
122
|
+
`,
|
|
123
|
+
},
|
|
124
|
+
];
|
|
125
|
+
/**
|
|
126
|
+
* Stable id of the scope_key migration. Exported for bootstrap detection and
|
|
127
|
+
* tests.
|
|
128
|
+
*/
|
|
129
|
+
const SCOPE_KEY_MIGRATION_ID = "001-add-scope-key";
|
|
130
|
+
/**
|
|
131
|
+
* Create the migrations table if it does not exist. Called unconditionally on
|
|
132
|
+
* every open so a fresh database bootstraps correctly.
|
|
133
|
+
*/
|
|
134
|
+
function ensureMigrationsTable(db) {
|
|
135
|
+
db.exec(`
|
|
136
|
+
CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
137
|
+
id TEXT PRIMARY KEY,
|
|
138
|
+
applied_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
139
|
+
);
|
|
140
|
+
`);
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Detect whether a column exists on a given table.
|
|
144
|
+
*/
|
|
145
|
+
function hasColumn(db, table, column) {
|
|
146
|
+
const rows = db.query(`PRAGMA table_info(${table})`).all();
|
|
147
|
+
return rows.some((r) => r.name === column);
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Back-fill `schema_migrations` rows for any schema state that existed before
|
|
151
|
+
* this file gained migration tracking.
|
|
152
|
+
*
|
|
153
|
+
* The pre-versioning code added the `scope_key` column on `workflow_runs` via
|
|
154
|
+
* an ad-hoc PRAGMA / ALTER TABLE pair. Those databases must not re-run the
|
|
155
|
+
* scope_key migration (the ALTER would fail with "duplicate column name"
|
|
156
|
+
* since the migration body does not use IF NOT EXISTS — SQLite does not
|
|
157
|
+
* support that clause on ALTER TABLE ADD COLUMN). Instead, we mark the
|
|
158
|
+
* migration as already applied.
|
|
159
|
+
*
|
|
160
|
+
* This function is a no-op on fresh databases: the `scope_key` column does
|
|
161
|
+
* not exist, so the migration runs normally and records itself.
|
|
162
|
+
*/
|
|
163
|
+
function bootstrapPreVersioningDb(db) {
|
|
164
|
+
const alreadyRecorded = db.prepare("SELECT 1 FROM schema_migrations WHERE id = ?").get(SCOPE_KEY_MIGRATION_ID);
|
|
165
|
+
if (alreadyRecorded)
|
|
166
|
+
return;
|
|
167
|
+
if (hasColumn(db, "workflow_runs", "scope_key")) {
|
|
168
|
+
db.prepare("INSERT INTO schema_migrations (id) VALUES (?)").run(SCOPE_KEY_MIGRATION_ID);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Apply every pending migration in a single transaction per migration.
|
|
173
|
+
*
|
|
174
|
+
* Each migration is applied in its own transaction so a failure in migration N
|
|
175
|
+
* does not roll back already-applied migrations 1..N-1. The migration row is
|
|
176
|
+
* inserted AFTER the DDL succeeds — a crash mid-migration leaves no row and
|
|
177
|
+
* the migration is retried on next open.
|
|
178
|
+
*
|
|
179
|
+
* Called automatically by {@link openWorkflowDatabase}.
|
|
180
|
+
*/
|
|
181
|
+
export function runMigrations(db) {
|
|
182
|
+
ensureMigrationsTable(db);
|
|
183
|
+
bootstrapPreVersioningDb(db);
|
|
184
|
+
const appliedRows = db.prepare("SELECT id FROM schema_migrations").all();
|
|
185
|
+
const applied = new Set(appliedRows.map((r) => r.id));
|
|
186
|
+
for (const migration of MIGRATIONS) {
|
|
187
|
+
if (applied.has(migration.id))
|
|
188
|
+
continue;
|
|
189
|
+
db.transaction(() => {
|
|
190
|
+
db.exec(migration.up);
|
|
191
|
+
db.prepare("INSERT INTO schema_migrations (id) VALUES (?)").run(migration.id);
|
|
192
|
+
})();
|
|
62
193
|
}
|
|
63
|
-
db.exec("CREATE INDEX IF NOT EXISTS idx_workflow_runs_scope_ref_status ON workflow_runs(scope_key, workflow_ref, status)");
|
|
64
194
|
}
|
|
@@ -1,13 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
* `entry_json` column or widening `StashEntry` with a workflow-shaped field.
|
|
5
|
-
*
|
|
6
|
-
* The renderer is called during metadata generation; the indexer writes the
|
|
7
|
-
* document to `workflow_documents` after `upsertEntry` returns the row id.
|
|
8
|
-
* A WeakMap keyed by the entry object preserves the parse work between the
|
|
9
|
-
* two phases without leaking memory if the entry is dropped.
|
|
10
|
-
*/
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
11
4
|
const cache = new WeakMap();
|
|
12
5
|
export function cacheWorkflowDocument(entry, doc) {
|
|
13
6
|
cache.set(entry, doc);
|
package/dist/workflows/parser.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
1
4
|
/**
|
|
2
5
|
* Show + indexing renderer for workflow assets.
|
|
3
6
|
*
|
|
@@ -8,6 +11,7 @@
|
|
|
8
11
|
*/
|
|
9
12
|
import { makeAssetRef } from "../core/asset-ref";
|
|
10
13
|
import { UsageError } from "../core/errors";
|
|
14
|
+
import { registerMetadataContributor } from "../indexer/metadata-contributors";
|
|
11
15
|
import { cacheWorkflowDocument } from "./document-cache";
|
|
12
16
|
import { parseWorkflow } from "./parser";
|
|
13
17
|
function shellQuote(value) {
|
|
@@ -54,8 +58,12 @@ export const workflowMdRenderer = {
|
|
|
54
58
|
})),
|
|
55
59
|
};
|
|
56
60
|
},
|
|
57
|
-
|
|
58
|
-
|
|
61
|
+
};
|
|
62
|
+
registerMetadataContributor({
|
|
63
|
+
name: "workflow-document-metadata",
|
|
64
|
+
appliesTo: ({ rendererName }) => rendererName === "workflow-md",
|
|
65
|
+
contribute(entry, { renderContext }) {
|
|
66
|
+
const doc = loadDocument(renderContext);
|
|
59
67
|
const hints = new Set(entry.searchHints ?? []);
|
|
60
68
|
hints.add(doc.title);
|
|
61
69
|
for (const step of doc.steps) {
|
|
@@ -75,4 +83,4 @@ export const workflowMdRenderer = {
|
|
|
75
83
|
}
|
|
76
84
|
cacheWorkflowDocument(entry, doc);
|
|
77
85
|
},
|
|
78
|
-
};
|
|
86
|
+
});
|
package/dist/workflows/runs.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
1
4
|
import { randomUUID } from "node:crypto";
|
|
2
5
|
import fs from "node:fs";
|
|
3
6
|
import { parseAssetRef } from "../core/asset-ref";
|
|
@@ -13,64 +16,58 @@ import { formatWorkflowErrors } from "./authoring";
|
|
|
13
16
|
import { closeWorkflowDatabase, openWorkflowDatabase } from "./db";
|
|
14
17
|
import { parseWorkflow } from "./parser";
|
|
15
18
|
import { getCurrentWorkflowScopeKey } from "./scope-key";
|
|
19
|
+
async function withWorkflowDb(fn) {
|
|
20
|
+
const db = openWorkflowDatabase();
|
|
21
|
+
try {
|
|
22
|
+
return await Promise.resolve(fn(db));
|
|
23
|
+
}
|
|
24
|
+
finally {
|
|
25
|
+
closeWorkflowDatabase(db);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
16
28
|
export async function startWorkflowRun(ref, params = {}) {
|
|
17
29
|
const asset = await loadWorkflowAsset(ref);
|
|
18
|
-
|
|
19
|
-
try {
|
|
30
|
+
return withWorkflowDb(async (db) => {
|
|
20
31
|
const now = new Date().toISOString();
|
|
21
32
|
const runId = randomUUID();
|
|
22
33
|
const scopeKey = getCurrentWorkflowScopeKey();
|
|
23
34
|
const currentStepId = asset.steps[0]?.id ?? null;
|
|
24
35
|
const workflowEntryId = resolveWorkflowEntryId(asset.sourcePath, asset.ref);
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
.prepare(`INSERT INTO workflow_runs (
|
|
36
|
+
db.transaction(() => {
|
|
37
|
+
db.prepare(`INSERT INTO workflow_runs (
|
|
28
38
|
id, workflow_ref, scope_key, workflow_entry_id, workflow_title, status, params_json, current_step_id, created_at, updated_at
|
|
29
|
-
) VALUES (?, ?, ?, ?, ?, 'active', ?, ?, ?, ?)`)
|
|
30
|
-
|
|
31
|
-
const insertStep = workflowDb.prepare(`INSERT INTO workflow_run_steps (
|
|
39
|
+
) VALUES (?, ?, ?, ?, ?, 'active', ?, ?, ?, ?)`).run(runId, asset.ref, scopeKey, workflowEntryId, asset.title, JSON.stringify(params), currentStepId, now, now);
|
|
40
|
+
const insertStep = db.prepare(`INSERT INTO workflow_run_steps (
|
|
32
41
|
run_id, step_id, step_title, instructions, completion_json, sequence_index, status
|
|
33
42
|
) VALUES (?, ?, ?, ?, ?, ?, 'pending')`);
|
|
34
43
|
for (const step of asset.steps) {
|
|
35
44
|
insertStep.run(runId, step.id, step.title, step.instructions, step.completionCriteria ? JSON.stringify(step.completionCriteria) : null, step.sequenceIndex ?? 0);
|
|
36
45
|
}
|
|
37
46
|
})();
|
|
38
|
-
const result = getWorkflowStatus(runId);
|
|
47
|
+
const result = await getWorkflowStatus(runId);
|
|
39
48
|
appendEvent({
|
|
40
49
|
eventType: "workflow_started",
|
|
41
50
|
ref: ref,
|
|
42
51
|
metadata: { runId: result.run.id, title: result.run.workflowTitle },
|
|
43
52
|
});
|
|
44
53
|
return result;
|
|
45
|
-
}
|
|
46
|
-
finally {
|
|
47
|
-
closeWorkflowDatabase(workflowDb);
|
|
48
|
-
}
|
|
54
|
+
});
|
|
49
55
|
}
|
|
50
|
-
export function getWorkflowStatus(runId) {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const
|
|
54
|
-
const steps = readWorkflowRunSteps(workflowDb, run.id);
|
|
56
|
+
export async function getWorkflowStatus(runId) {
|
|
57
|
+
return withWorkflowDb((db) => {
|
|
58
|
+
const run = readWorkflowRun(db, runId);
|
|
59
|
+
const steps = readWorkflowRunSteps(db, run.id);
|
|
55
60
|
return buildWorkflowRunDetail(run, steps);
|
|
56
|
-
}
|
|
57
|
-
finally {
|
|
58
|
-
closeWorkflowDatabase(workflowDb);
|
|
59
|
-
}
|
|
61
|
+
});
|
|
60
62
|
}
|
|
61
|
-
export function hasWorkflowRun(runId) {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const row = workflowDb.prepare("SELECT 1 FROM workflow_runs WHERE id = ? LIMIT 1").get(runId);
|
|
63
|
+
export async function hasWorkflowRun(runId) {
|
|
64
|
+
return withWorkflowDb((db) => {
|
|
65
|
+
const row = db.prepare("SELECT 1 FROM workflow_runs WHERE id = ? LIMIT 1").get(runId);
|
|
65
66
|
return !!row;
|
|
66
|
-
}
|
|
67
|
-
finally {
|
|
68
|
-
closeWorkflowDatabase(workflowDb);
|
|
69
|
-
}
|
|
67
|
+
});
|
|
70
68
|
}
|
|
71
|
-
export function listWorkflowRuns(input) {
|
|
72
|
-
|
|
73
|
-
try {
|
|
69
|
+
export async function listWorkflowRuns(input) {
|
|
70
|
+
return withWorkflowDb((db) => {
|
|
74
71
|
const filters = [];
|
|
75
72
|
const params = [];
|
|
76
73
|
const scopeKey = getCurrentWorkflowScopeKey();
|
|
@@ -88,20 +85,16 @@ export function listWorkflowRuns(input) {
|
|
|
88
85
|
filters.push("status IN ('active', 'blocked')");
|
|
89
86
|
}
|
|
90
87
|
const where = filters.length > 0 ? `WHERE ${filters.join(" AND ")}` : "";
|
|
91
|
-
const rows =
|
|
88
|
+
const rows = db
|
|
92
89
|
.prepare(`SELECT * FROM workflow_runs ${where} ORDER BY updated_at DESC, created_at DESC`)
|
|
93
90
|
.all(...params);
|
|
94
91
|
return { runs: rows.map(toWorkflowRunSummary) };
|
|
95
|
-
}
|
|
96
|
-
finally {
|
|
97
|
-
closeWorkflowDatabase(workflowDb);
|
|
98
|
-
}
|
|
92
|
+
});
|
|
99
93
|
}
|
|
100
94
|
export async function getNextWorkflowStep(specifier, params) {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const
|
|
104
|
-
const steps = readWorkflowRunSteps(workflowDb, run.id);
|
|
95
|
+
return withWorkflowDb(async (db) => {
|
|
96
|
+
const { run, autoStarted } = await resolveRunSpecifier(db, specifier, params);
|
|
97
|
+
const steps = readWorkflowRunSteps(db, run.id);
|
|
105
98
|
const currentStep = resolveCurrentStep(run, steps);
|
|
106
99
|
const done = run.status === "completed" ? true : undefined;
|
|
107
100
|
return {
|
|
@@ -115,54 +108,44 @@ export async function getNextWorkflowStep(specifier, params) {
|
|
|
115
108
|
...(done ? { done } : {}),
|
|
116
109
|
...(autoStarted ? { autoStarted } : {}),
|
|
117
110
|
};
|
|
118
|
-
}
|
|
119
|
-
finally {
|
|
120
|
-
closeWorkflowDatabase(workflowDb);
|
|
121
|
-
}
|
|
111
|
+
});
|
|
122
112
|
}
|
|
123
|
-
export function resumeWorkflowRun(runId) {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
const run = readWorkflowRun(workflowDb, runId);
|
|
113
|
+
export async function resumeWorkflowRun(runId) {
|
|
114
|
+
return withWorkflowDb((db) => {
|
|
115
|
+
const run = readWorkflowRun(db, runId);
|
|
127
116
|
if (run.status === "completed") {
|
|
128
117
|
throw new UsageError(`Workflow run ${run.id} is already completed and cannot be resumed.`);
|
|
129
118
|
}
|
|
130
119
|
if (run.status === "active") {
|
|
131
|
-
const steps = readWorkflowRunSteps(
|
|
120
|
+
const steps = readWorkflowRunSteps(db, run.id);
|
|
132
121
|
return buildWorkflowRunDetail(run, steps);
|
|
133
122
|
}
|
|
134
123
|
// blocked or failed → flip back to active and re-open the current step so
|
|
135
124
|
// it can be reclassified (completed, failed, skipped) after resuming.
|
|
136
125
|
const now = new Date().toISOString();
|
|
137
|
-
|
|
126
|
+
db.transaction(() => {
|
|
138
127
|
if (run.current_step_id) {
|
|
139
|
-
|
|
140
|
-
.prepare(`UPDATE workflow_run_steps
|
|
128
|
+
db.prepare(`UPDATE workflow_run_steps
|
|
141
129
|
SET status = 'pending', notes = NULL, evidence_json = NULL, completed_at = NULL
|
|
142
|
-
WHERE run_id = ? AND step_id = ? AND status IN ('blocked', 'failed')`)
|
|
143
|
-
.run(run.id, run.current_step_id);
|
|
130
|
+
WHERE run_id = ? AND step_id = ? AND status IN ('blocked', 'failed')`).run(run.id, run.current_step_id);
|
|
144
131
|
}
|
|
145
|
-
|
|
132
|
+
db.prepare("UPDATE workflow_runs SET status = 'active', updated_at = ? WHERE id = ?").run(now, run.id);
|
|
146
133
|
})();
|
|
147
134
|
const updated = { ...run, status: "active", updated_at: now };
|
|
148
|
-
const steps = readWorkflowRunSteps(
|
|
135
|
+
const steps = readWorkflowRunSteps(db, run.id);
|
|
149
136
|
return buildWorkflowRunDetail(updated, steps);
|
|
150
|
-
}
|
|
151
|
-
finally {
|
|
152
|
-
closeWorkflowDatabase(workflowDb);
|
|
153
|
-
}
|
|
137
|
+
});
|
|
154
138
|
}
|
|
155
|
-
export function completeWorkflowStep(input) {
|
|
156
|
-
|
|
157
|
-
try {
|
|
139
|
+
export async function completeWorkflowStep(input) {
|
|
140
|
+
return withWorkflowDb((db) => {
|
|
158
141
|
let updatedRun;
|
|
159
142
|
let refreshedSteps = [];
|
|
160
|
-
|
|
161
|
-
const run = readWorkflowRun(
|
|
143
|
+
db.transaction(() => {
|
|
144
|
+
const run = readWorkflowRun(db, input.runId);
|
|
162
145
|
if (run.status !== "active") {
|
|
163
146
|
throw new UsageError(`Workflow run ${run.id} is ${run.status} and cannot be updated.`);
|
|
164
147
|
}
|
|
165
|
-
const existing =
|
|
148
|
+
const existing = db
|
|
166
149
|
.prepare("SELECT * FROM workflow_run_steps WHERE run_id = ? AND step_id = ?")
|
|
167
150
|
.get(run.id, input.stepId);
|
|
168
151
|
if (!existing) {
|
|
@@ -175,18 +158,14 @@ export function completeWorkflowStep(input) {
|
|
|
175
158
|
throw new UsageError(`Step "${input.stepId}" is not the current step for workflow run ${run.id}. Complete "${run.current_step_id}" first.`);
|
|
176
159
|
}
|
|
177
160
|
const completedAt = new Date().toISOString();
|
|
178
|
-
|
|
179
|
-
.prepare(`UPDATE workflow_run_steps
|
|
161
|
+
db.prepare(`UPDATE workflow_run_steps
|
|
180
162
|
SET status = ?, notes = ?, evidence_json = ?, completed_at = ?
|
|
181
|
-
WHERE run_id = ? AND step_id = ?`)
|
|
182
|
-
|
|
183
|
-
refreshedSteps = readWorkflowRunSteps(workflowDb, run.id);
|
|
163
|
+
WHERE run_id = ? AND step_id = ?`).run(input.status, input.notes?.trim() || null, input.evidence ? JSON.stringify(input.evidence) : null, completedAt, run.id, input.stepId);
|
|
164
|
+
refreshedSteps = readWorkflowRunSteps(db, run.id);
|
|
184
165
|
const state = deriveRunState(refreshedSteps);
|
|
185
|
-
|
|
186
|
-
.prepare(`UPDATE workflow_runs
|
|
166
|
+
db.prepare(`UPDATE workflow_runs
|
|
187
167
|
SET status = ?, current_step_id = ?, updated_at = ?, completed_at = ?
|
|
188
|
-
WHERE id = ?`)
|
|
189
|
-
.run(state.status, state.currentStepId, completedAt, state.completedAt, run.id);
|
|
168
|
+
WHERE id = ?`).run(state.status, state.currentStepId, completedAt, state.completedAt, run.id);
|
|
190
169
|
updatedRun = {
|
|
191
170
|
...run,
|
|
192
171
|
status: state.status,
|
|
@@ -205,10 +184,7 @@ export function completeWorkflowStep(input) {
|
|
|
205
184
|
appendEvent({ eventType: "workflow_finished", ref: detail.run.workflowRef, metadata: { runId: input.runId } });
|
|
206
185
|
}
|
|
207
186
|
return detail;
|
|
208
|
-
}
|
|
209
|
-
finally {
|
|
210
|
-
closeWorkflowDatabase(workflowDb);
|
|
211
|
-
}
|
|
187
|
+
});
|
|
212
188
|
}
|
|
213
189
|
async function resolveRunSpecifier(db, specifier, params) {
|
|
214
190
|
const explicitRun = db.prepare("SELECT * FROM workflow_runs WHERE id = ?").get(specifier);
|
|
@@ -262,7 +238,7 @@ async function loadWorkflowAsset(ref) {
|
|
|
262
238
|
if (!assetPath) {
|
|
263
239
|
throw new NotFoundError(`Workflow not found for ref: workflow:${parsed.name}`);
|
|
264
240
|
}
|
|
265
|
-
const resolvedSourcePath = sourcePath ??
|
|
241
|
+
const resolvedSourcePath = sourcePath ?? config.stashDir ?? assetPath;
|
|
266
242
|
const fullRef = `${parsed.origin ? `${parsed.origin}//` : ""}workflow:${parsed.name}`;
|
|
267
243
|
const cached = readWorkflowDocumentFromIndex(resolvedSourcePath, fullRef);
|
|
268
244
|
const document = cached ?? loadWorkflowDocumentFromDisk(assetPath);
|
|
@@ -452,18 +428,13 @@ function parseJsonArray(value) {
|
|
|
452
428
|
}
|
|
453
429
|
return undefined;
|
|
454
430
|
}
|
|
455
|
-
export function getActiveWorkflowRun(scopeKey = getCurrentWorkflowScopeKey()) {
|
|
456
|
-
|
|
457
|
-
const
|
|
458
|
-
const row = workflowDb
|
|
431
|
+
export async function getActiveWorkflowRun(scopeKey = getCurrentWorkflowScopeKey()) {
|
|
432
|
+
return withWorkflowDb((db) => {
|
|
433
|
+
const row = db
|
|
459
434
|
.query("SELECT id, current_step_id, workflow_ref FROM workflow_runs WHERE scope_key = ? AND status IN ('active', 'blocked') ORDER BY updated_at DESC LIMIT 1")
|
|
460
435
|
.get(scopeKey);
|
|
461
|
-
closeWorkflowDatabase(workflowDb);
|
|
462
436
|
if (!row)
|
|
463
437
|
return null;
|
|
464
438
|
return { runId: row.id, stepId: row.current_step_id, workflowRef: row.workflow_ref };
|
|
465
|
-
}
|
|
466
|
-
catch {
|
|
467
|
-
return null; // fail-open: never crash show output due to DB error
|
|
468
|
-
}
|
|
439
|
+
}).catch(() => null); // fail-open: never crash show output due to DB error
|
|
469
440
|
}
|
package/dist/workflows/schema.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
1
4
|
import { createHash } from "node:crypto";
|
|
2
5
|
import fs from "node:fs";
|
|
3
6
|
import path from "node:path";
|
|
@@ -1,12 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
* The parser handles per-line shape checks; this module runs rules that need
|
|
5
|
-
* the whole document or the raw frontmatter at once: duplicate step IDs,
|
|
6
|
-
* step-id format, and the frontmatter key whitelist.
|
|
7
|
-
*/
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
8
4
|
const STEP_ID_REGEX = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
9
|
-
const ALLOWED_FRONTMATTER_KEYS = new Set(["description", "tags", "params"]);
|
|
5
|
+
const ALLOWED_FRONTMATTER_KEYS = new Set(["description", "tags", "params", "name", "updated"]);
|
|
10
6
|
export function runSemanticChecks(draft, frontmatterData, frontmatterEndLine, errors) {
|
|
11
7
|
checkFrontmatterKeys(frontmatterData, frontmatterEndLine, errors);
|
|
12
8
|
checkStepIdFormat(draft, errors);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Describe what this workflow accomplishes
|
|
3
|
+
tags:
|
|
4
|
+
- example
|
|
5
|
+
params:
|
|
6
|
+
example_param: Explain this parameter
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Workflow: {{TITLE}}
|
|
10
|
+
|
|
11
|
+
## Step: {{FIRST_STEP_TITLE}}
|
|
12
|
+
Step ID: {{FIRST_STEP_ID}}
|
|
13
|
+
|
|
14
|
+
### Instructions
|
|
15
|
+
Describe what to do in this step.
|
|
16
|
+
|
|
17
|
+
### Completion Criteria
|
|
18
|
+
- Confirm the first step is complete
|
|
19
|
+
|
|
20
|
+
## Step: Second Step
|
|
21
|
+
Step ID: second-step
|
|
22
|
+
|
|
23
|
+
### Instructions
|
|
24
|
+
Describe what happens next.
|
package/docs/README.md
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
## Upgrading
|
|
12
12
|
|
|
13
13
|
- [v1 migration guide](migration/v1.md) -- The path from 0.x to v1.0, including the `.stash.json` removal scheduled for v0.8.0
|
|
14
|
-
- [Release notes (latest: 0.
|
|
14
|
+
- [Release notes (latest: 0.8.0)](migration/release-notes/0.8.0.md) -- Per-release notes drop into `migration/release-notes/`, including current pre-release removals
|
|
15
15
|
- [v0.5 → v0.6 migration guide](migration/v0.5-to-v0.6.md) -- Every breaking change with before/after code, publisher checklist, and troubleshooting
|
|
16
16
|
|
|
17
17
|
## Reference
|
|
@@ -28,17 +28,24 @@
|
|
|
28
28
|
- [itlackey/akm-plugins](https://github.com/itlackey/akm-plugins) -- optional integrations for tools like OpenCode
|
|
29
29
|
- [itlackey/akm-bench](https://github.com/itlackey/akm-bench) -- the standalone benchmark and evaluation repo for akm
|
|
30
30
|
|
|
31
|
+
## Operations
|
|
32
|
+
|
|
33
|
+
- [Improve Stats](improve-stats.md) -- Analyze `akm improve` run logs with the `scripts/improve-stats/` toolkit (runs-trend, run-show, actions-breakdown, lint-current)
|
|
34
|
+
|
|
31
35
|
## Internals
|
|
32
36
|
|
|
33
37
|
- [Search](technical/search.md) -- Hybrid search architecture and scoring
|
|
34
38
|
- [Indexing](technical/indexing.md) -- How the search index is built
|
|
35
39
|
- [Classification](technical/classification.md) -- Matcher and renderer behavior
|
|
40
|
+
- [Functional Contract Patterns](technical/functional-contract-patterns.md) -- Quick reference for contributor pipelines and small process contracts
|
|
41
|
+
- [Implementation Plan: Functional Contract Refactor](technical/implementation-plan-functional-contract-refactor.md) -- Phased plan to move behavior from type-centric switchboards to process-local contributors
|
|
42
|
+
- [Architecture Cleanup Checklist](technical/architecture-cleanup-checklist.md) -- Living checklist for executing the cleanup plan with parity gates, reviews, and git hygiene
|
|
36
43
|
- [Show Response](technical/show-response.md) -- `akm show` output fields by asset type
|
|
37
44
|
- [Testing Workflow](technical/testing-workflow.md) -- End-to-end, Docker, deployment, and upgrade validation
|
|
38
45
|
- [Ref Format](technical/ref.md) -- Wire format for asset references
|
|
39
46
|
- [Test Coverage Guide](technical/test-coverage-guide.md) -- High-value testing areas
|
|
40
47
|
- [Core Principles](technical/akm-core-principles.md) -- Design principles and constraints
|
|
41
|
-
-
|
|
48
|
+
- `technical/benchmark.md` (planned) -- Search-quality benchmark suite
|
|
42
49
|
|
|
43
50
|
## Posts
|
|
44
51
|
|