akm-cli 0.9.0-beta.9 → 0.9.0-rc.1
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 +715 -0
- package/README.md +12 -4
- package/dist/akm +38 -0
- package/dist/akm-migrate-storage +38 -0
- package/dist/assets/help/help-improve.md +9 -6
- package/dist/assets/hints/cli-hints-full.md +6 -5
- package/dist/assets/profiles/default.json +9 -4
- package/dist/assets/profiles/frequent.json +1 -1
- package/dist/assets/profiles/memory-focus.json +1 -1
- package/dist/assets/profiles/proactive-maintenance.json +25 -0
- package/dist/assets/profiles/quick.json +1 -1
- package/dist/assets/profiles/recombine-only.json +21 -0
- package/dist/assets/profiles/reflect-distill.json +30 -0
- package/dist/assets/profiles/synthesize.json +15 -0
- package/dist/assets/profiles/thorough.json +1 -1
- package/dist/assets/prompts/consolidate-system.md +23 -0
- package/dist/assets/prompts/contradiction-judge.md +33 -0
- package/dist/assets/prompts/distill-knowledge-system.md +22 -0
- package/dist/assets/prompts/distill-lesson-system.md +36 -0
- package/dist/assets/prompts/extract-session.md +11 -3
- package/dist/assets/prompts/graph-extract-system.md +1 -0
- package/dist/assets/prompts/graph-extract-user-prompt.md +1 -1
- package/dist/assets/prompts/memory-infer-system.md +1 -0
- package/dist/assets/prompts/memory-infer-user.md +5 -0
- package/dist/assets/prompts/metadata-enhance-system.md +1 -0
- package/dist/assets/prompts/procedural-system.md +44 -0
- package/dist/assets/prompts/recombine-system.md +40 -0
- package/dist/assets/prompts/staleness-detect-system.md +6 -0
- package/dist/assets/prompts/validate-summary-judge.md +1 -0
- package/dist/assets/prompts/workflow-unit-preamble.md +26 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/agent.md +38 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/command.md +38 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/fact.md +39 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/knowledge.md +40 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/lesson.md +43 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/memory.md +38 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/script.md +43 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/skill.md +40 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/workflow.md +43 -0
- package/dist/assets/templates/html/health.html +281 -111
- package/dist/assets/wiki/ingest-workflow-template.md +45 -16
- package/dist/assets/wiki/schema-template.md +4 -4
- package/dist/cli/clack.js +56 -0
- package/dist/cli/config-migrate.js +7 -1
- package/dist/cli/confirm.js +1 -1
- package/dist/cli/parse-args.js +46 -1
- package/dist/cli/shared.js +28 -0
- package/dist/cli.js +25 -21
- package/dist/commands/agent/agent-dispatch.js +3 -2
- package/dist/commands/agent/agent-support.js +0 -7
- package/dist/commands/agent/contribute-cli.js +26 -7
- package/dist/commands/config-cli.js +26 -13
- package/dist/commands/env/child-env.js +47 -0
- package/dist/commands/env/env-binding.js +95 -0
- package/dist/commands/env/env-cli.js +228 -292
- package/dist/commands/env/env.js +14 -67
- package/dist/commands/env/secret-cli.js +140 -138
- package/dist/commands/feedback-cli.js +156 -155
- package/dist/commands/graph/graph-cli.js +5 -13
- package/dist/commands/graph/graph.js +3 -3
- package/dist/commands/health/advisories.js +151 -0
- package/dist/commands/health/checks.js +103 -16
- package/dist/commands/health/html-report.js +447 -81
- package/dist/commands/health/improve-metrics.js +771 -0
- package/dist/commands/health/llm-usage.js +65 -0
- package/dist/commands/health/md-report.js +103 -0
- package/dist/commands/health/metrics.js +278 -0
- package/dist/commands/health/stash-exposure.js +46 -0
- package/dist/commands/health/surfaces.js +216 -0
- package/dist/commands/health/task-runs.js +135 -0
- package/dist/commands/health/types.js +26 -0
- package/dist/commands/health/windows.js +195 -0
- package/dist/commands/health.js +91 -1091
- package/dist/commands/improve/anti-collapse.js +170 -0
- package/dist/commands/improve/calibration.js +161 -0
- package/dist/commands/improve/collapse-detector.js +421 -0
- package/dist/commands/improve/consolidate/chunking.js +141 -0
- package/dist/commands/improve/consolidate/eligibility.js +64 -0
- package/dist/commands/improve/consolidate/merge.js +145 -0
- package/dist/commands/improve/consolidate/sanitize.js +231 -0
- package/dist/commands/{lint.js → improve/consolidate/types.js} +1 -1
- package/dist/commands/improve/consolidate.js +1295 -1277
- package/dist/commands/improve/dedup.js +482 -0
- package/dist/commands/improve/distill/content-repair.js +202 -0
- package/dist/commands/improve/distill/promote-memory.js +229 -0
- package/dist/commands/improve/distill/quality-gate.js +236 -0
- package/dist/commands/improve/distill-guards.js +127 -0
- package/dist/commands/improve/distill-promotion-policy.js +826 -167
- package/dist/commands/improve/distill.js +228 -605
- package/dist/commands/improve/eligibility.js +434 -0
- package/dist/commands/improve/encoding-salience.js +205 -0
- package/dist/commands/improve/extract-cli.js +179 -59
- package/dist/commands/improve/extract-prompt.js +54 -3
- package/dist/commands/improve/extract-watch.js +140 -0
- package/dist/commands/improve/extract.js +409 -43
- package/dist/commands/improve/feedback-valence.js +54 -0
- package/dist/commands/improve/hot-probation.js +45 -0
- package/dist/commands/improve/improve-auto-accept.js +157 -10
- package/dist/commands/improve/improve-cli.js +115 -73
- package/dist/commands/improve/improve-profiles.js +28 -8
- package/dist/commands/improve/improve-result-file.js +15 -25
- package/dist/commands/improve/improve-session.js +58 -0
- package/dist/commands/improve/improve.js +485 -2764
- package/dist/commands/improve/locks.js +154 -0
- package/dist/commands/improve/loop-stages.js +1100 -0
- package/dist/commands/improve/memory/memory-belief.js +14 -15
- package/dist/commands/improve/memory/memory-contradiction-detect.js +83 -60
- package/dist/commands/improve/memory/memory-improve.js +27 -27
- package/dist/commands/improve/outcome-loop.js +270 -0
- package/dist/commands/improve/preparation.js +2002 -0
- package/dist/commands/improve/proactive-maintenance.js +37 -35
- package/dist/commands/improve/procedural.js +398 -0
- package/dist/commands/improve/recombine.js +818 -0
- package/dist/commands/improve/reflect-noise.js +0 -0
- package/dist/commands/improve/reflect.js +206 -45
- package/dist/commands/improve/salience.js +455 -0
- package/dist/commands/improve/schema-similarity-gate.js +168 -0
- package/dist/commands/improve/shared.js +51 -0
- package/dist/commands/improve/triage.js +93 -0
- package/dist/commands/lint/agent-linter.js +19 -24
- package/dist/commands/lint/base-linter.js +173 -60
- package/dist/commands/lint/command-linter.js +19 -24
- package/dist/commands/lint/env-key-rules.js +38 -1
- package/dist/commands/lint/fact-linter.js +39 -0
- package/dist/commands/lint/index.js +31 -13
- package/dist/commands/lint/memory-linter.js +1 -1
- package/dist/commands/lint/registry.js +7 -2
- package/dist/commands/lint/task-linter.js +3 -3
- package/dist/commands/lint/workflow-linter.js +26 -1
- package/dist/commands/observability-cli.js +4 -4
- package/dist/commands/proposal/drain-policies.js +13 -4
- package/dist/commands/proposal/drain.js +45 -51
- package/dist/commands/proposal/legacy-import.js +115 -0
- package/dist/commands/proposal/proposal-cli.js +24 -34
- package/dist/commands/proposal/proposal.js +2 -1
- package/dist/commands/proposal/propose.js +8 -3
- package/dist/commands/proposal/repository.js +829 -0
- package/dist/commands/proposal/validators/proposal-quality-validators.js +9 -8
- package/dist/commands/proposal/validators/proposals.js +93 -895
- package/dist/commands/read/curate.js +410 -111
- package/dist/commands/read/knowledge.js +10 -3
- package/dist/commands/read/remember-cli.js +133 -138
- package/dist/commands/read/search-cli.js +15 -8
- package/dist/commands/read/search.js +22 -11
- package/dist/commands/read/show.js +106 -14
- package/dist/commands/registry-cli.js +76 -87
- package/dist/commands/remember.js +11 -12
- package/dist/commands/sources/add-cli.js +91 -95
- package/dist/commands/sources/history.js +1 -1
- package/dist/commands/sources/init.js +66 -18
- package/dist/commands/sources/installed-stashes.js +11 -3
- package/dist/commands/sources/migration-help.js +7 -4
- package/dist/commands/sources/schema-repair.js +44 -46
- package/dist/commands/sources/self-update.js +2 -2
- package/dist/commands/sources/source-add.js +7 -3
- package/dist/commands/sources/sources-cli.js +3 -3
- package/dist/commands/sources/stash-cli.js +19 -39
- package/dist/commands/sources/stash-skeleton.js +57 -8
- package/dist/commands/tasks/default-tasks.js +15 -2
- package/dist/commands/tasks/tasks-cli.js +20 -29
- package/dist/commands/tasks/tasks.js +39 -11
- package/dist/commands/wiki-cli.js +23 -38
- package/dist/commands/workflow-cli.js +291 -13
- package/dist/core/asset/asset-registry.js +3 -1
- package/dist/core/asset/asset-spec.js +79 -5
- package/dist/core/asset/frontmatter.js +188 -167
- package/dist/core/asset/markdown.js +8 -0
- package/dist/core/authoring-rules.js +92 -0
- package/dist/core/common.js +4 -23
- package/dist/core/concurrent.js +10 -1
- package/dist/core/config/config-io.js +10 -1
- package/dist/core/config/config-migration.js +18 -40
- package/dist/core/config/config-schema.js +403 -62
- package/dist/core/config/config-types.js +3 -3
- package/dist/core/config/config.js +67 -22
- package/dist/core/deep-merge.js +38 -0
- package/dist/core/errors.js +1 -0
- package/dist/core/eval/rank-metrics.js +113 -0
- package/dist/core/events.js +4 -7
- package/dist/core/improve-types.js +47 -8
- package/dist/core/json-schema.js +142 -0
- package/dist/core/logs-db.js +14 -75
- package/dist/core/parse.js +36 -16
- package/dist/core/paths.js +18 -18
- package/dist/core/standards/resolve-standards-context.js +87 -0
- package/dist/core/standards/resolve-stash-standards.js +99 -0
- package/dist/core/standards/resolve-type-conventions.js +66 -0
- package/dist/core/state/migrations.js +770 -0
- package/dist/core/state-db.js +132 -1126
- package/dist/core/structured.js +69 -0
- package/dist/core/time.js +53 -0
- package/dist/core/warn.js +21 -0
- package/dist/core/write-source.js +37 -0
- package/dist/indexer/db/db.js +261 -770
- package/dist/indexer/db/entry-mapper.js +41 -0
- package/dist/indexer/db/graph-db.js +129 -86
- package/dist/indexer/db/llm-cache.js +2 -2
- package/dist/indexer/db/schema.js +516 -0
- package/dist/indexer/ensure-index.js +36 -92
- package/dist/indexer/feedback/utility-policy.js +75 -0
- package/dist/indexer/graph/graph-boost.js +51 -41
- package/dist/indexer/graph/graph-extraction.js +207 -4
- package/dist/indexer/index-writer-lock.js +18 -11
- package/dist/indexer/index-written-assets.js +105 -0
- package/dist/indexer/indexer.js +182 -204
- package/dist/indexer/passes/dir-staleness.js +114 -0
- package/dist/indexer/passes/memory-inference.js +13 -5
- package/dist/indexer/passes/metadata.js +20 -0
- package/dist/indexer/read-preflight.js +23 -0
- package/dist/indexer/search/db-search.js +89 -13
- package/dist/indexer/search/fts-query.js +51 -0
- package/dist/indexer/search/ranking-contributors.js +95 -9
- package/dist/indexer/search/ranking.js +79 -3
- package/dist/indexer/search/search-fields.js +6 -0
- package/dist/indexer/search/search-source.js +32 -21
- package/dist/indexer/search/semantic-status.js +4 -0
- package/dist/indexer/walk/matchers.js +48 -0
- package/dist/indexer/walk/walker.js +21 -13
- package/dist/integrations/agent/builders.js +41 -13
- package/dist/integrations/agent/config.js +20 -59
- package/dist/integrations/agent/detect.js +9 -0
- package/dist/integrations/agent/index.js +3 -19
- package/dist/integrations/agent/model-aliases.js +16 -2
- package/dist/integrations/agent/profiles.js +79 -6
- package/dist/integrations/agent/prompts.js +75 -9
- package/dist/integrations/agent/runner-dispatch.js +83 -0
- package/dist/integrations/agent/runner.js +13 -9
- package/dist/integrations/agent/spawn.js +206 -81
- package/dist/integrations/harnesses/aider/agent-builder.js +113 -0
- package/dist/integrations/harnesses/aider/index.js +58 -0
- package/dist/integrations/harnesses/aider/result-extractor.js +53 -0
- package/dist/integrations/harnesses/amazonq/agent-builder.js +153 -0
- package/dist/integrations/harnesses/amazonq/index.js +59 -0
- package/dist/integrations/harnesses/amazonq/result-extractor.js +48 -0
- package/dist/integrations/harnesses/claude/agent-builder.js +46 -7
- package/dist/integrations/harnesses/claude/index.js +27 -23
- package/dist/integrations/harnesses/claude/result-extractor.js +52 -0
- package/dist/integrations/harnesses/claude/session-log.js +10 -0
- package/dist/integrations/harnesses/codex/agent-builder.js +137 -0
- package/dist/integrations/harnesses/codex/index.js +63 -0
- package/dist/integrations/harnesses/codex/result-extractor.js +73 -0
- package/dist/integrations/harnesses/copilot/agent-builder.js +122 -0
- package/dist/integrations/harnesses/copilot/index.js +60 -0
- package/dist/integrations/harnesses/copilot/result-extractor.js +151 -0
- package/dist/integrations/harnesses/gemini/agent-builder.js +121 -0
- package/dist/integrations/harnesses/gemini/index.js +60 -0
- package/dist/integrations/harnesses/gemini/result-extractor.js +121 -0
- package/dist/integrations/harnesses/index.js +28 -7
- package/dist/integrations/harnesses/opencode/agent-builder.js +1 -1
- package/dist/integrations/harnesses/opencode/index.js +17 -16
- package/dist/integrations/harnesses/opencode/session-log.js +173 -3
- package/dist/integrations/harnesses/opencode-sdk/harness.js +65 -0
- package/dist/integrations/harnesses/opencode-sdk/index.js +10 -34
- package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +642 -71
- package/dist/integrations/harnesses/openhands/agent-builder.js +126 -0
- package/dist/integrations/harnesses/openhands/index.js +58 -0
- package/dist/integrations/harnesses/openhands/result-extractor.js +103 -0
- package/dist/integrations/harnesses/pi/agent-builder.js +104 -0
- package/dist/integrations/harnesses/pi/index.js +58 -0
- package/dist/integrations/harnesses/pi/result-extractor.js +135 -0
- package/dist/integrations/harnesses/types.js +8 -0
- package/dist/integrations/session-logs/index.js +40 -11
- package/dist/llm/call-ai.js +2 -2
- package/dist/llm/client.js +34 -11
- package/dist/llm/embedder.js +67 -4
- package/dist/llm/embedders/cache.js +3 -1
- package/dist/llm/embedders/deterministic.js +66 -0
- package/dist/llm/embedders/local.js +73 -3
- package/dist/llm/feature-gate.js +16 -15
- package/dist/llm/graph-extract.js +67 -44
- package/dist/llm/memory-infer-impl.js +138 -0
- package/dist/llm/memory-infer.js +1 -127
- package/dist/llm/metadata-enhance.js +44 -31
- package/dist/llm/structured-call.js +49 -0
- package/dist/migrate-storage-node.mjs +8 -0
- package/dist/output/context.js +5 -5
- package/dist/output/renderers.js +87 -15
- package/dist/output/shapes/curate.js +14 -2
- package/dist/output/shapes/helpers.js +0 -3
- package/dist/output/shapes/passthrough.js +6 -1
- package/dist/output/text/helpers.js +241 -2
- package/dist/output/text/workflow.js +4 -1
- package/dist/registry/providers/skills-sh.js +21 -147
- package/dist/registry/providers/static-index.js +15 -157
- package/dist/registry/resolve.js +27 -9
- package/dist/runtime.js +25 -1
- package/dist/schemas/akm-config.json +14225 -0
- package/dist/schemas/akm-workflow.json +328 -0
- package/dist/scripts/migrate-storage.js +2743 -8390
- package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +1652 -607
- package/dist/setup/detect.js +9 -0
- package/dist/setup/legacy-config.js +106 -0
- package/dist/setup/prompt.js +57 -0
- package/dist/setup/providers.js +14 -0
- package/dist/setup/registry-stash-loader.js +12 -0
- package/dist/setup/semantic-assets.js +124 -0
- package/dist/setup/setup.js +52 -1614
- package/dist/setup/steps/connection.js +734 -0
- package/dist/setup/steps/output.js +31 -0
- package/dist/setup/steps/platforms.js +124 -0
- package/dist/setup/steps/semantic.js +27 -0
- package/dist/setup/steps/sources.js +222 -0
- package/dist/setup/steps/stashdir.js +42 -0
- package/dist/setup/steps/tasks.js +152 -0
- package/dist/sources/include.js +6 -2
- package/dist/sources/providers/filesystem.js +0 -1
- package/dist/sources/providers/git-install.js +210 -0
- package/dist/sources/providers/git-provider.js +234 -0
- package/dist/sources/providers/git-stash.js +248 -0
- package/dist/sources/providers/git.js +10 -661
- package/dist/sources/providers/npm.js +2 -6
- package/dist/sources/providers/provider-utils.js +13 -7
- package/dist/sources/providers/sync-from-ref.js +9 -1
- package/dist/sources/providers/website.js +9 -5
- package/dist/sources/website-ingest.js +187 -29
- package/dist/sources/wiki-fetchers/registry.js +53 -0
- package/dist/sources/wiki-fetchers/youtube.js +239 -0
- package/dist/storage/database.js +45 -10
- package/dist/storage/managed-db.js +82 -0
- package/dist/storage/repositories/canaries-repository.js +107 -0
- package/dist/storage/repositories/consolidation-repository.js +38 -0
- package/dist/storage/repositories/embeddings-repository.js +72 -0
- package/dist/storage/repositories/events-repository.js +187 -0
- package/dist/storage/repositories/extract-sessions-repository.js +96 -0
- package/dist/storage/repositories/improve-runs-repository.js +146 -0
- package/dist/storage/repositories/index-db.js +14 -8
- package/dist/storage/repositories/proposals-repository.js +220 -0
- package/dist/storage/repositories/recombine-repository.js +213 -0
- package/dist/storage/repositories/registry-cache.js +93 -0
- package/dist/storage/repositories/registry-index-cache-repository.js +46 -0
- package/dist/storage/repositories/task-history-repository.js +93 -0
- package/dist/storage/repositories/workflow-runs-repository.js +189 -1
- package/dist/storage/sqlite-pragmas.js +146 -0
- package/dist/tasks/backends/cron.js +1 -1
- package/dist/tasks/backends/index.js +9 -0
- package/dist/tasks/backends/launchd.js +1 -1
- package/dist/tasks/backends/schtasks.js +1 -1
- package/dist/tasks/{resolveAkmBin.js → resolve-akm-bin.js} +2 -2
- package/dist/tasks/runner.js +15 -13
- package/dist/text-import-hook.mjs +1 -1
- package/dist/wiki/wiki.js +52 -11
- package/dist/workflows/authoring/authoring.js +123 -10
- package/dist/workflows/authoring/workflow-program-template.yaml +31 -0
- package/dist/workflows/cli.js +5 -0
- package/dist/workflows/db.js +138 -4
- package/dist/workflows/exec/brief.js +484 -0
- package/dist/workflows/exec/native-executor.js +975 -0
- package/dist/workflows/exec/param-secrets.js +115 -0
- package/dist/workflows/exec/report.js +1295 -0
- package/dist/workflows/exec/run-workflow.js +596 -0
- package/dist/workflows/exec/scheduler.js +100 -0
- package/dist/workflows/exec/step-work.js +1156 -0
- package/dist/workflows/exec/unit-writer.js +23 -0
- package/dist/workflows/exec/watch.js +116 -0
- package/dist/workflows/exec/worktree.js +171 -0
- package/dist/workflows/ir/compile.js +388 -0
- package/dist/workflows/ir/params.js +54 -0
- package/dist/workflows/ir/plan-hash.js +33 -0
- package/dist/workflows/ir/schema.js +4 -0
- package/dist/workflows/parser.js +3 -1
- package/dist/workflows/program/expressions.js +369 -0
- package/dist/workflows/program/parser.js +760 -0
- package/dist/workflows/program/project.js +105 -0
- package/dist/workflows/program/schema.js +54 -0
- package/dist/workflows/renderer.js +82 -5
- package/dist/workflows/runtime/agent-identity.js +59 -14
- package/dist/workflows/runtime/runs.js +248 -153
- package/dist/workflows/runtime/unit-checkin.js +45 -0
- package/dist/workflows/runtime/workflow-asset-loader.js +188 -0
- package/dist/workflows/validate-summary.js +26 -10
- package/dist/workflows/validator.js +1 -1
- package/docs/README.md +69 -18
- package/docs/data-and-telemetry.md +7 -5
- package/docs/migration/release-notes/0.7.0.md +1 -1
- package/docs/migration/release-notes/0.9.0-beta.60.md +19 -0
- package/docs/migration/release-notes/0.9.0.md +39 -0
- package/package.json +10 -10
- package/dist/assets/tasks/core/update-stashes.yml +0 -4
- package/dist/commands/db-cli.js +0 -23
- package/dist/indexer/db/db-backup.js +0 -376
- package/dist/indexer/passes/staleness-detect.js +0 -488
|
@@ -14,12 +14,15 @@
|
|
|
14
14
|
* Output is the AkmExtractResult JSON envelope (or an aggregated one when
|
|
15
15
|
* `--auto` runs multiple harnesses).
|
|
16
16
|
*/
|
|
17
|
-
import
|
|
18
|
-
import
|
|
17
|
+
import fs from "node:fs";
|
|
18
|
+
import path from "node:path";
|
|
19
|
+
import { getStringArg } from "../../cli/parse-args.js";
|
|
20
|
+
import { defineJsonCommand, EXIT_CODES, output } from "../../cli/shared.js";
|
|
19
21
|
import { UsageError } from "../../core/errors.js";
|
|
20
|
-
import { getAvailableHarnesses } from "../../integrations/session-logs/index.js";
|
|
22
|
+
import { getAvailableHarnesses, getWatchTargets } from "../../integrations/session-logs/index.js";
|
|
21
23
|
import { akmExtract } from "./extract.js";
|
|
22
|
-
|
|
24
|
+
import { akmExtractWatch } from "./extract-watch.js";
|
|
25
|
+
export const extractCommand = defineJsonCommand({
|
|
23
26
|
meta: {
|
|
24
27
|
name: "extract",
|
|
25
28
|
description: "Extract durable insights from native session files (claude-code, opencode) and queue them as proposals. Replaces the legacy session-checkpoint hook.",
|
|
@@ -60,68 +63,185 @@ export const extractCommand = defineCommand({
|
|
|
60
63
|
type: "string",
|
|
61
64
|
description: "Per-session LLM timeout in ms (default 60000).",
|
|
62
65
|
},
|
|
66
|
+
watch: {
|
|
67
|
+
type: "boolean",
|
|
68
|
+
description: "Watch harness session-log directories and run extract on change (debounced). Stays alive until SIGINT/SIGTERM. Falls back to the */30 cron when off.",
|
|
69
|
+
default: false,
|
|
70
|
+
},
|
|
71
|
+
"debounce-ms": {
|
|
72
|
+
type: "string",
|
|
73
|
+
description: "Debounce window in ms for --watch (default 2000).",
|
|
74
|
+
},
|
|
63
75
|
},
|
|
64
76
|
async run({ args }) {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
for (const h of harnesses) {
|
|
107
|
-
const result = await akmExtract({ type: h.name, ...commonOptions });
|
|
108
|
-
results.push(result);
|
|
109
|
-
}
|
|
110
|
-
const ok = results.every((r) => r.ok);
|
|
111
|
-
const totalProposals = results.reduce((sum, r) => sum + r.proposals.length, 0);
|
|
77
|
+
const type = getStringArg(args, "type") ?? "";
|
|
78
|
+
const sessionId = getStringArg(args, "session-id") ?? "";
|
|
79
|
+
const location = getStringArg(args, "location") ?? "";
|
|
80
|
+
const since = getStringArg(args, "since") ?? "";
|
|
81
|
+
const auto = args.auto === true;
|
|
82
|
+
const dryRun = args["dry-run"] === true;
|
|
83
|
+
const force = args.force === true;
|
|
84
|
+
const timeoutMs = typeof args["timeout-ms"] === "string" && args["timeout-ms"] !== ""
|
|
85
|
+
? Number.parseInt(args["timeout-ms"], 10)
|
|
86
|
+
: undefined;
|
|
87
|
+
if (timeoutMs !== undefined && (!Number.isFinite(timeoutMs) || timeoutMs <= 0)) {
|
|
88
|
+
throw new UsageError(`--timeout-ms must be a positive integer (got "${args["timeout-ms"]}").`, "INVALID_FLAG_VALUE");
|
|
89
|
+
}
|
|
90
|
+
const watch = args.watch === true;
|
|
91
|
+
const debounceMs = typeof args["debounce-ms"] === "string" && args["debounce-ms"] !== ""
|
|
92
|
+
? Number.parseInt(args["debounce-ms"], 10)
|
|
93
|
+
: 2000;
|
|
94
|
+
if (watch && (!Number.isFinite(debounceMs) || debounceMs <= 0)) {
|
|
95
|
+
throw new UsageError(`--debounce-ms must be a positive integer (got "${args["debounce-ms"]}").`, "INVALID_FLAG_VALUE");
|
|
96
|
+
}
|
|
97
|
+
if (watch) {
|
|
98
|
+
await runWatchMode({ debounceMs, dryRun, force, ...(since ? { since } : {}) });
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (auto && type) {
|
|
102
|
+
throw new UsageError("--auto and --type are mutually exclusive. Pick one.", "INVALID_FLAG_VALUE");
|
|
103
|
+
}
|
|
104
|
+
if (!auto && !type) {
|
|
105
|
+
throw new UsageError("--type is required (or pass --auto to try every available harness).", "MISSING_REQUIRED_ARGUMENT");
|
|
106
|
+
}
|
|
107
|
+
const commonOptions = {
|
|
108
|
+
...(sessionId ? { sessionId } : {}),
|
|
109
|
+
...(location ? { location } : {}),
|
|
110
|
+
...(since ? { since } : {}),
|
|
111
|
+
dryRun,
|
|
112
|
+
force,
|
|
113
|
+
...(timeoutMs !== undefined ? { timeoutMs } : {}),
|
|
114
|
+
};
|
|
115
|
+
if (auto) {
|
|
116
|
+
const harnesses = getAvailableHarnesses();
|
|
117
|
+
if (harnesses.length === 0) {
|
|
112
118
|
output("extract", {
|
|
113
119
|
schemaVersion: 1,
|
|
114
|
-
ok,
|
|
120
|
+
ok: false,
|
|
115
121
|
shape: "extract-auto-result",
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
totalProposals,
|
|
119
|
-
results,
|
|
122
|
+
warnings: ["no available harnesses found on this machine"],
|
|
123
|
+
results: [],
|
|
120
124
|
});
|
|
121
125
|
return;
|
|
122
126
|
}
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
127
|
+
const results = [];
|
|
128
|
+
for (const h of harnesses) {
|
|
129
|
+
const result = await akmExtract({ type: h.name, ...commonOptions });
|
|
130
|
+
results.push(result);
|
|
131
|
+
}
|
|
132
|
+
const ok = results.every((r) => r.ok);
|
|
133
|
+
const totalProposals = results.reduce((sum, r) => sum + r.proposals.length, 0);
|
|
134
|
+
output("extract", {
|
|
135
|
+
schemaVersion: 1,
|
|
136
|
+
ok,
|
|
137
|
+
shape: "extract-auto-result",
|
|
138
|
+
dryRun,
|
|
139
|
+
harnessesProcessed: results.length,
|
|
140
|
+
totalProposals,
|
|
141
|
+
results,
|
|
142
|
+
});
|
|
143
|
+
// Signal failure to callers/cron when every harness failed. output()
|
|
144
|
+
// only renders; without this the */30 cron exits 0 on a total failure
|
|
145
|
+
// and the breakage is invisible to exit-code monitoring. process.exitCode
|
|
146
|
+
// (not process.exit) lets stdout flush and the watcher/timers settle.
|
|
147
|
+
if (!ok)
|
|
148
|
+
process.exitCode = EXIT_CODES.GENERAL;
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
const result = await akmExtract({ type, ...commonOptions });
|
|
152
|
+
output("extract", result);
|
|
153
|
+
if (!result.ok)
|
|
154
|
+
process.exitCode = EXIT_CODES.GENERAL;
|
|
126
155
|
},
|
|
127
156
|
});
|
|
157
|
+
/**
|
|
158
|
+
* A thin {@link WatchEventSource} over `fs.watch` for each configured root.
|
|
159
|
+
* This adapter is the ONLY place a real `fs.watch` is created (the core stays
|
|
160
|
+
* injectable + fully unit-tested); it is intentionally not unit-covered.
|
|
161
|
+
*
|
|
162
|
+
* `fs.watch(dir, { recursive: true })` is unreliable for recursive mode on some
|
|
163
|
+
* Node/Bun/Linux combinations — that is acceptable here: missed events degrade
|
|
164
|
+
* to the unchanged scheduled cron fallback, never worse than today.
|
|
165
|
+
*/
|
|
166
|
+
function createFsWatchEventSource(roots) {
|
|
167
|
+
return {
|
|
168
|
+
subscribe(listener) {
|
|
169
|
+
const watchers = [];
|
|
170
|
+
for (const root of roots) {
|
|
171
|
+
try {
|
|
172
|
+
const watcher = fs.watch(root, { recursive: true }, (_event, filename) => {
|
|
173
|
+
if (!filename)
|
|
174
|
+
return;
|
|
175
|
+
const filePath = path.isAbsolute(filename.toString())
|
|
176
|
+
? filename.toString()
|
|
177
|
+
: path.join(root, filename.toString());
|
|
178
|
+
listener({ path: filePath });
|
|
179
|
+
});
|
|
180
|
+
watchers.push(watcher);
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
// A root that can't be watched is skipped; the cron fallback covers it.
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return () => {
|
|
187
|
+
for (const w of watchers) {
|
|
188
|
+
try {
|
|
189
|
+
w.close();
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
// best-effort teardown
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Run `akm extract --watch`: watch every available harness's session-log
|
|
201
|
+
* roots and run extract (debounced, per-harness) on change. Stays alive until
|
|
202
|
+
* SIGINT/SIGTERM, then stops cleanly. PROCESS-HYGIENE: stop() removes every
|
|
203
|
+
* watcher + pending timer before the process exits.
|
|
204
|
+
*/
|
|
205
|
+
async function runWatchMode(opts) {
|
|
206
|
+
const targets = getWatchTargets();
|
|
207
|
+
if (targets.length === 0) {
|
|
208
|
+
output("extract", {
|
|
209
|
+
schemaVersion: 1,
|
|
210
|
+
ok: false,
|
|
211
|
+
shape: "extract-watch-started",
|
|
212
|
+
warnings: ["no watchable harness session-log directories found on this machine"],
|
|
213
|
+
watching: [],
|
|
214
|
+
});
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
const allRoots = targets.flatMap((t) => t.roots);
|
|
218
|
+
const eventSource = createFsWatchEventSource(allRoots);
|
|
219
|
+
const handle = akmExtractWatch({
|
|
220
|
+
roots: targets,
|
|
221
|
+
eventSource,
|
|
222
|
+
debounceMs: opts.debounceMs,
|
|
223
|
+
onTrigger: async (harnessName) => {
|
|
224
|
+
await akmExtract({
|
|
225
|
+
type: harnessName,
|
|
226
|
+
dryRun: opts.dryRun,
|
|
227
|
+
force: opts.force,
|
|
228
|
+
...(opts.since ? { since: opts.since } : {}),
|
|
229
|
+
});
|
|
230
|
+
},
|
|
231
|
+
});
|
|
232
|
+
output("extract", {
|
|
233
|
+
schemaVersion: 1,
|
|
234
|
+
ok: true,
|
|
235
|
+
shape: "extract-watch-started",
|
|
236
|
+
debounceMs: opts.debounceMs,
|
|
237
|
+
watching: allRoots,
|
|
238
|
+
});
|
|
239
|
+
await new Promise((resolve) => {
|
|
240
|
+
const shutdown = () => {
|
|
241
|
+
handle.stop();
|
|
242
|
+
resolve();
|
|
243
|
+
};
|
|
244
|
+
process.once("SIGINT", shutdown);
|
|
245
|
+
process.once("SIGTERM", shutdown);
|
|
246
|
+
});
|
|
247
|
+
}
|
|
@@ -21,12 +21,18 @@ import promptTemplate from "../../assets/prompts/extract-session.md" with { type
|
|
|
21
21
|
*
|
|
22
22
|
* Shape:
|
|
23
23
|
* {
|
|
24
|
-
* "candidates": [{type, name, description, when_to_use?, body, confidence, evidence
|
|
24
|
+
* "candidates": [{type, name, description, when_to_use?, body, confidence, evidence,
|
|
25
|
+
* orderedActions?, outcomeData?}, ...],
|
|
25
26
|
* "rationale_if_empty"?: string
|
|
26
27
|
* }
|
|
27
28
|
*
|
|
28
29
|
* `additionalProperties: false` at each level so any hallucinated keys are
|
|
29
30
|
* dropped before parsing.
|
|
31
|
+
*
|
|
32
|
+
* `orderedActions` and `outcomeData` are additive fields for #615 procedural
|
|
33
|
+
* compilation (WS-0 data-capture hook). Source transcripts are external logs
|
|
34
|
+
* not guaranteed re-extractable; we capture the ordered-action sequence and
|
|
35
|
+
* outcome now even though the detection/compilation feature is deferred to 0.10+.
|
|
30
36
|
*/
|
|
31
37
|
export const EXTRACT_JSON_SCHEMA = {
|
|
32
38
|
type: "object",
|
|
@@ -79,6 +85,17 @@ export const EXTRACT_JSON_SCHEMA = {
|
|
|
79
85
|
minLength: 5,
|
|
80
86
|
description: "One-line pointer to the moment in the session that supports this candidate.",
|
|
81
87
|
},
|
|
88
|
+
orderedActions: {
|
|
89
|
+
type: "array",
|
|
90
|
+
description: "OPTIONAL. Ordered list of discrete actions taken during this episode, as brief imperative phrases (e.g. 'run deploy.sh', 'check VPN status', 'retry with --force'). Capture only when the candidate represents a recurring action sequence that could become a skill or workflow. Omit when the candidate is a standalone fact or observation.",
|
|
91
|
+
items: { type: "string", minLength: 3 },
|
|
92
|
+
maxItems: 20,
|
|
93
|
+
},
|
|
94
|
+
outcomeData: {
|
|
95
|
+
type: "string",
|
|
96
|
+
description: "OPTIONAL. One-sentence description of the outcome when the ordered action sequence completed (e.g. 'deploy succeeded after VPN reconnect', 'build failed with error X'). Required when orderedActions is present; omit otherwise.",
|
|
97
|
+
maxLength: 400,
|
|
98
|
+
},
|
|
82
99
|
},
|
|
83
100
|
},
|
|
84
101
|
},
|
|
@@ -106,22 +123,36 @@ function formatAlreadyPreserved(inlineRefs) {
|
|
|
106
123
|
})
|
|
107
124
|
.join("\n");
|
|
108
125
|
}
|
|
126
|
+
/**
|
|
127
|
+
* Delimiters that fence the untrusted session transcript in the extract prompt.
|
|
128
|
+
* Mirrors the `=== ASSET N ===` convention (`graph-extract.ts`): everything
|
|
129
|
+
* between the markers is DATA to analyze, never instructions to obey. The
|
|
130
|
+
* transcript is external, attacker-influenceable content, so an explicit,
|
|
131
|
+
* greppable boundary defuses prompt-injection that tries to pose as a command.
|
|
132
|
+
*/
|
|
133
|
+
export const TRANSCRIPT_FENCE_BEGIN = "=== BEGIN UNTRUSTED SESSION TRANSCRIPT ===";
|
|
134
|
+
export const TRANSCRIPT_FENCE_END = "=== END UNTRUSTED SESSION TRANSCRIPT ===";
|
|
109
135
|
/**
|
|
110
136
|
* Format pre-filtered events as a transcript snippet. Each event becomes:
|
|
111
137
|
* [<role> @ <iso>] <text>
|
|
112
138
|
* Events are already truncated/cleaned by the pre-filter; this is purely
|
|
113
139
|
* a render step.
|
|
140
|
+
*
|
|
141
|
+
* Anti-spoof: any occurrence of the fence markers inside the transcript text is
|
|
142
|
+
* neutralised so a crafted session cannot forge the boundary and "escape" the
|
|
143
|
+
* fence to inject trusted-looking instructions.
|
|
114
144
|
*/
|
|
115
145
|
function formatTranscript(events) {
|
|
116
146
|
if (events.length === 0)
|
|
117
147
|
return "(empty — pre-filter removed all events as noise)";
|
|
118
|
-
|
|
148
|
+
const body = events
|
|
119
149
|
.map((e) => {
|
|
120
150
|
const tsLabel = e.ts ? new Date(e.ts).toISOString() : "unknown-ts";
|
|
121
151
|
const roleLabel = e.role ?? "unknown";
|
|
122
152
|
return `[${roleLabel} @ ${tsLabel}] ${e.text}`;
|
|
123
153
|
})
|
|
124
154
|
.join("\n\n");
|
|
155
|
+
return body.split(TRANSCRIPT_FENCE_BEGIN).join("=== (fence) ===").split(TRANSCRIPT_FENCE_END).join("=== (fence) ===");
|
|
125
156
|
}
|
|
126
157
|
/**
|
|
127
158
|
* Build the user-prompt body for the extract LLM call by interpolating
|
|
@@ -132,6 +163,11 @@ export function buildExtractPrompt(input) {
|
|
|
132
163
|
const ref = input.data.ref;
|
|
133
164
|
const startedAt = ref.startedAt ? new Date(ref.startedAt).toISOString() : "unknown";
|
|
134
165
|
const endedAt = ref.endedAt ? new Date(ref.endedAt).toISOString() : "unknown";
|
|
166
|
+
// Optional standards block — rendered to the lead-in + body when present,
|
|
167
|
+
// or an empty string (no section) when absent. Gated on non-empty.
|
|
168
|
+
const standards = input.standardsContext?.trim()
|
|
169
|
+
? `\n## Standards to follow (the rulebook for this target)\n\n${input.standardsContext.trim()}\n`
|
|
170
|
+
: "";
|
|
135
171
|
return promptTemplate
|
|
136
172
|
.replace("{{HARNESS}}", ref.harness)
|
|
137
173
|
.replace("{{TITLE}}", ref.title ?? "(no title)")
|
|
@@ -139,7 +175,8 @@ export function buildExtractPrompt(input) {
|
|
|
139
175
|
.replace("{{ENDED_AT}}", endedAt)
|
|
140
176
|
.replace("{{PROJECT_HINT}}", ref.projectHint ?? "(no project hint)")
|
|
141
177
|
.replace("{{ALREADY_PRESERVED}}", formatAlreadyPreserved(input.inlineRefs))
|
|
142
|
-
.replace("{{
|
|
178
|
+
.replace("{{STANDARDS}}", standards)
|
|
179
|
+
.replace("{{TRANSCRIPT}}", `${TRANSCRIPT_FENCE_BEGIN}\n${formatTranscript(input.events)}\n${TRANSCRIPT_FENCE_END}`);
|
|
143
180
|
}
|
|
144
181
|
/**
|
|
145
182
|
* Parse the LLM's JSON response into a structured {@link ExtractPayload}.
|
|
@@ -207,6 +244,20 @@ export function parseExtractPayload(stdout) {
|
|
|
207
244
|
};
|
|
208
245
|
if (typeof c.when_to_use === "string")
|
|
209
246
|
candidate.when_to_use = c.when_to_use.trim();
|
|
247
|
+
// #615 WS-0: parse optional ordered-action + outcome fields.
|
|
248
|
+
// Defensive: silently drop malformed entries rather than rejecting the whole candidate.
|
|
249
|
+
if (Array.isArray(c.orderedActions) && c.orderedActions.length > 0) {
|
|
250
|
+
const actions = c.orderedActions
|
|
251
|
+
.filter((a) => typeof a === "string" && a.trim().length >= 3)
|
|
252
|
+
.map((a) => a.trim())
|
|
253
|
+
.slice(0, 20);
|
|
254
|
+
if (actions.length > 0) {
|
|
255
|
+
candidate.orderedActions = actions;
|
|
256
|
+
if (typeof c.outcomeData === "string" && c.outcomeData.trim().length > 0) {
|
|
257
|
+
candidate.outcomeData = c.outcomeData.trim().slice(0, 400);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
210
261
|
candidates.push(candidate);
|
|
211
262
|
}
|
|
212
263
|
const result = { candidates };
|
|
@@ -0,0 +1,140 @@
|
|
|
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/.
|
|
4
|
+
import { akmExtract } from "./extract.js";
|
|
5
|
+
const DEFAULT_DEBOUNCE_MS = 2000;
|
|
6
|
+
/** Small look-back window for the default trigger's `akmExtract` call. */
|
|
7
|
+
const DEFAULT_TRIGGER_SINCE = "10m";
|
|
8
|
+
/**
|
|
9
|
+
* Does `filePath` look like a real session file under one of the configured
|
|
10
|
+
* roots? True only when the path is under a root AND matches that harness's
|
|
11
|
+
* on-disk session-file shape:
|
|
12
|
+
* - claude-code: `<root>/<project>/<id>.jsonl`
|
|
13
|
+
* - opencode: `<root>/<project>/<id>.json`
|
|
14
|
+
* Anything out-of-root, or in-root with the wrong extension, returns false.
|
|
15
|
+
*/
|
|
16
|
+
export function isSessionFile(filePath, roots) {
|
|
17
|
+
return matchHarness(filePath, roots) !== undefined;
|
|
18
|
+
}
|
|
19
|
+
/** Normalize a path for prefix comparison (ensure a trailing separator). */
|
|
20
|
+
function withSep(dir) {
|
|
21
|
+
return dir.endsWith("/") ? dir : `${dir}/`;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Resolve which harness a path belongs to by longest-prefix match against the
|
|
25
|
+
* configured roots, then validate the file shape for that harness. Returns the
|
|
26
|
+
* harness name, or `undefined` when the path is out-of-root or not a session
|
|
27
|
+
* file. Longest-prefix match guards against the (in practice impossible) case
|
|
28
|
+
* of nested roots.
|
|
29
|
+
*/
|
|
30
|
+
export function matchHarness(filePath, roots) {
|
|
31
|
+
let best;
|
|
32
|
+
let bestLen = -1;
|
|
33
|
+
for (const target of roots) {
|
|
34
|
+
for (const root of target.roots) {
|
|
35
|
+
const prefix = withSep(root);
|
|
36
|
+
if (filePath.startsWith(prefix) && prefix.length > bestLen) {
|
|
37
|
+
best = { harnessName: target.harnessName };
|
|
38
|
+
bestLen = prefix.length;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (!best)
|
|
43
|
+
return undefined;
|
|
44
|
+
// Validate the file shape for the resolved harness.
|
|
45
|
+
if (best.harnessName === "claude-code") {
|
|
46
|
+
return filePath.endsWith(".jsonl") ? best.harnessName : undefined;
|
|
47
|
+
}
|
|
48
|
+
if (best.harnessName === "opencode") {
|
|
49
|
+
return filePath.endsWith(".json") ? best.harnessName : undefined;
|
|
50
|
+
}
|
|
51
|
+
// Unknown harness: accept any file under its root.
|
|
52
|
+
return best.harnessName;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Start the watch-mode core. Returns a handle whose `stop()` tears everything
|
|
56
|
+
* down. The event source is injected, so this never touches the real
|
|
57
|
+
* filesystem itself.
|
|
58
|
+
*/
|
|
59
|
+
export function akmExtractWatch(options) {
|
|
60
|
+
const { roots, eventSource, debounceMs = DEFAULT_DEBOUNCE_MS, setTimeoutFn = setTimeout, clearTimeoutFn = clearTimeout, isSessionFile: isSessionFileFn = isSessionFile, } = options;
|
|
61
|
+
const onTrigger = options.onTrigger ??
|
|
62
|
+
(async (harnessName) => {
|
|
63
|
+
await akmExtract({ type: harnessName, since: DEFAULT_TRIGGER_SINCE, force: false });
|
|
64
|
+
});
|
|
65
|
+
let stopped = false;
|
|
66
|
+
// Pending debounce timers, keyed by harness name.
|
|
67
|
+
const pendingTimers = new Map();
|
|
68
|
+
// Harnesses with an in-flight trigger, and whether a re-run is queued.
|
|
69
|
+
const inFlight = new Set();
|
|
70
|
+
const rerunPending = new Set();
|
|
71
|
+
function runTrigger(harnessName) {
|
|
72
|
+
if (stopped)
|
|
73
|
+
return;
|
|
74
|
+
if (inFlight.has(harnessName)) {
|
|
75
|
+
// A trigger is already running for this harness; coalesce into a single
|
|
76
|
+
// re-run after it resolves rather than launching a concurrent extract.
|
|
77
|
+
rerunPending.add(harnessName);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
inFlight.add(harnessName);
|
|
81
|
+
// Invoke synchronously so a synchronous `onTrigger` observes its effect
|
|
82
|
+
// before control returns (tests advance the clock then assert inline).
|
|
83
|
+
// Wrap the (possibly async) result in a promise to drive the in-flight
|
|
84
|
+
// guard's release without forcing the trigger itself into a microtask.
|
|
85
|
+
let result;
|
|
86
|
+
try {
|
|
87
|
+
result = onTrigger(harnessName);
|
|
88
|
+
}
|
|
89
|
+
catch (err) {
|
|
90
|
+
// One failed extract must never kill the watcher (mirrors the non-fatal
|
|
91
|
+
// handling in collectSessionEvents).
|
|
92
|
+
console.error(`[akm] extract --watch trigger for "${harnessName}" failed:`, err);
|
|
93
|
+
inFlight.delete(harnessName);
|
|
94
|
+
if (rerunPending.delete(harnessName) && !stopped)
|
|
95
|
+
runTrigger(harnessName);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
Promise.resolve(result)
|
|
99
|
+
.catch((err) => {
|
|
100
|
+
console.error(`[akm] extract --watch trigger for "${harnessName}" failed:`, err);
|
|
101
|
+
})
|
|
102
|
+
.finally(() => {
|
|
103
|
+
inFlight.delete(harnessName);
|
|
104
|
+
if (rerunPending.delete(harnessName) && !stopped) {
|
|
105
|
+
runTrigger(harnessName);
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
function onEvent(e) {
|
|
110
|
+
if (stopped)
|
|
111
|
+
return;
|
|
112
|
+
if (!isSessionFileFn(e.path, roots))
|
|
113
|
+
return;
|
|
114
|
+
const harnessName = matchHarness(e.path, roots);
|
|
115
|
+
if (!harnessName)
|
|
116
|
+
return;
|
|
117
|
+
const existing = pendingTimers.get(harnessName);
|
|
118
|
+
if (existing !== undefined)
|
|
119
|
+
clearTimeoutFn(existing);
|
|
120
|
+
const timer = setTimeoutFn(() => {
|
|
121
|
+
pendingTimers.delete(harnessName);
|
|
122
|
+
if (!stopped)
|
|
123
|
+
runTrigger(harnessName);
|
|
124
|
+
}, debounceMs);
|
|
125
|
+
pendingTimers.set(harnessName, timer);
|
|
126
|
+
}
|
|
127
|
+
const unsubscribe = eventSource.subscribe(onEvent);
|
|
128
|
+
return {
|
|
129
|
+
stop() {
|
|
130
|
+
if (stopped)
|
|
131
|
+
return;
|
|
132
|
+
stopped = true;
|
|
133
|
+
unsubscribe();
|
|
134
|
+
for (const timer of pendingTimers.values())
|
|
135
|
+
clearTimeoutFn(timer);
|
|
136
|
+
pendingTimers.clear();
|
|
137
|
+
rerunPending.clear();
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
}
|