arkaos 4.22.0 → 4.24.0
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/README.md +1 -1
- package/THE-ARKAOS-GUIDE.md +72 -0
- package/VERSION +1 -1
- package/arka/SKILL.md +29 -7
- package/arka/skills/bootstrap-agent/SKILL.md +3 -2
- package/arka/skills/conclave/SKILL.md +11 -0
- package/arka/skills/flow/SKILL.md +3 -2
- package/arka/skills/forge/SKILL.md +3 -2
- package/arka/skills/recipes/SKILL.md +3 -2
- package/arka/skills/refine/SKILL.md +3 -2
- package/arka/skills/research/SKILL.md +3 -2
- package/bin/arka-doctor +8 -0
- package/core/cognition/insights/store.py +15 -4
- package/core/forge/persistence.py +27 -9
- package/core/forge/schema.py +45 -22
- package/core/governance/evidence_checks.py +101 -9
- package/core/governance/kb_cite_check.py +16 -6
- package/core/governance/stop_lint.py +38 -11
- package/core/governance/stop_lint_telemetry.py +11 -2
- package/core/hooks/user_prompt_submit.py +21 -23
- package/core/synapse/engine.py +46 -16
- package/core/synapse/graph_context_layer.py +7 -2
- package/core/synapse/kb_cache.py +63 -16
- package/core/synapse/layers.py +93 -30
- package/core/synapse/layers_base.py +16 -0
- package/core/synapse/layers_kb.py +325 -19
- package/dashboard/app/layouts/default.vue +7 -0
- package/dashboard/app/pages/plan-canvas.vue +630 -0
- package/departments/brand/SKILL.md +3 -2
- package/departments/brand/skills/archetype-finder/SKILL.md +3 -2
- package/departments/brand/skills/colors/SKILL.md +3 -2
- package/departments/brand/skills/design-system/SKILL.md +3 -2
- package/departments/brand/skills/identity-system/SKILL.md +3 -2
- package/departments/brand/skills/logo-brief/SKILL.md +3 -2
- package/departments/brand/skills/mockup-generate/SKILL.md +3 -2
- package/departments/brand/skills/naming-evaluate/SKILL.md +3 -2
- package/departments/brand/skills/positioning-statement/SKILL.md +3 -2
- package/departments/brand/skills/primal-audit/SKILL.md +3 -2
- package/departments/brand/skills/ux-audit/SKILL.md +3 -2
- package/departments/brand/skills/voice-guide/SKILL.md +3 -2
- package/departments/brand/skills/wireframe/SKILL.md +3 -2
- package/departments/community/SKILL.md +3 -2
- package/departments/community/skills/ai-community/SKILL.md +3 -2
- package/departments/community/skills/betting-setup/SKILL.md +3 -2
- package/departments/community/skills/business-model/SKILL.md +3 -2
- package/departments/community/skills/content-calendar/SKILL.md +3 -2
- package/departments/community/skills/events-plan/SKILL.md +3 -2
- package/departments/community/skills/gamification-design/SKILL.md +3 -2
- package/departments/community/skills/growth-plan/SKILL.md +3 -2
- package/departments/community/skills/metrics-track/SKILL.md +3 -2
- package/departments/community/skills/moderation/SKILL.md +3 -2
- package/departments/community/skills/monetize-plan/SKILL.md +3 -2
- package/departments/community/skills/niche-setup/SKILL.md +3 -2
- package/departments/community/skills/onboarding-flow/SKILL.md +3 -2
- package/departments/community/skills/platform-select/SKILL.md +3 -2
- package/departments/content/SKILL.md +3 -2
- package/departments/content/skills/ai-workflow/SKILL.md +3 -2
- package/departments/content/skills/analytics/SKILL.md +3 -2
- package/departments/content/skills/calendar/SKILL.md +3 -2
- package/departments/content/skills/content-system/SKILL.md +3 -2
- package/departments/content/skills/monetization-plan/SKILL.md +3 -2
- package/departments/content/skills/newsletter-write/SKILL.md +3 -2
- package/departments/content/skills/platform-optimize/SKILL.md +3 -2
- package/departments/content/skills/repurpose-plan/SKILL.md +3 -2
- package/departments/content/skills/research-compile/SKILL.md +3 -2
- package/departments/content/skills/script-structure/SKILL.md +3 -2
- package/departments/content/skills/short-form/SKILL.md +3 -2
- package/departments/content/skills/thumbnail-package/SKILL.md +3 -2
- package/departments/content/skills/trend-hunt/SKILL.md +3 -2
- package/departments/content/skills/video-produce/SKILL.md +3 -2
- package/departments/content/skills/video-setup/SKILL.md +3 -2
- package/departments/content/skills/viral-design/SKILL.md +3 -2
- package/departments/content/skills/youtube-strategy/SKILL.md +3 -2
- package/departments/dev/SKILL.md +3 -2
- package/departments/dev/skills/ai-assisted-dev/SKILL.md +3 -2
- package/departments/dev/skills/animated-website/SKILL.md +7 -0
- package/departments/dev/skills/architecture-design/SKILL.md +3 -2
- package/departments/dev/skills/code-review/SKILL.md +3 -2
- package/departments/dev/skills/db-design/SKILL.md +3 -2
- package/departments/dev/skills/ddd-model/SKILL.md +3 -2
- package/departments/dev/skills/demo-gif/SKILL.md +3 -2
- package/departments/dev/skills/deploy/SKILL.md +3 -2
- package/departments/dev/skills/devops-pipeline/SKILL.md +3 -2
- package/departments/dev/skills/docs/SKILL.md +3 -2
- package/departments/dev/skills/evaluator-build-loop/SKILL.md +93 -0
- package/departments/dev/skills/mcp/SKILL.md +3 -2
- package/departments/dev/skills/performance-audit/SKILL.md +3 -2
- package/departments/dev/skills/refactor-plan/SKILL.md +3 -2
- package/departments/dev/skills/research/SKILL.md +3 -2
- package/departments/dev/skills/security-compliance/SKILL.md +3 -2
- package/departments/dev/skills/spec-miner/SKILL.md +86 -0
- package/departments/dev/skills/stack-check/SKILL.md +3 -2
- package/departments/ecom/SKILL.md +3 -2
- package/departments/ecom/skills/analytics/SKILL.md +3 -2
- package/departments/ecom/skills/browse-competitor/SKILL.md +3 -2
- package/departments/ecom/skills/cart-recovery/SKILL.md +3 -2
- package/departments/ecom/skills/cro-optimize/SKILL.md +3 -2
- package/departments/ecom/skills/customer-journey/SKILL.md +3 -2
- package/departments/ecom/skills/fulfillment-plan/SKILL.md +3 -2
- package/departments/ecom/skills/marketplace-manage/SKILL.md +3 -2
- package/departments/ecom/skills/pricing-strategy/SKILL.md +3 -2
- package/departments/ecom/skills/product-launch/SKILL.md +3 -2
- package/departments/ecom/skills/rfm-segment/SKILL.md +3 -2
- package/departments/ecom/skills/social-commerce/SKILL.md +3 -2
- package/departments/ecom/skills/store-audit/SKILL.md +3 -2
- package/departments/ecom/skills/subscription-model/SKILL.md +3 -2
- package/departments/finance/SKILL.md +3 -2
- package/departments/finance/skills/budget-plan/SKILL.md +3 -2
- package/departments/finance/skills/cashflow-forecast/SKILL.md +3 -2
- package/departments/finance/skills/ciso-advisor/SKILL.md +3 -2
- package/departments/finance/skills/financial-model/SKILL.md +3 -2
- package/departments/finance/skills/pitch-deck/SKILL.md +3 -2
- package/departments/finance/skills/scenario-analysis/SKILL.md +3 -2
- package/departments/finance/skills/unit-economics/SKILL.md +3 -2
- package/departments/finance/skills/valuation-model/SKILL.md +3 -2
- package/departments/kb/SKILL.md +3 -2
- package/departments/kb/skills/ai-research/SKILL.md +3 -2
- package/departments/kb/skills/competitive-intel/SKILL.md +3 -2
- package/departments/kb/skills/doc-extraction/SKILL.md +3 -2
- package/departments/kb/skills/doc-redaction/SKILL.md +3 -2
- package/departments/kb/skills/knowledge-ops/SKILL.md +3 -2
- package/departments/kb/skills/knowledge-review/SKILL.md +3 -2
- package/departments/kb/skills/learn-content/SKILL.md +3 -2
- package/departments/kb/skills/moc-create/SKILL.md +3 -2
- package/departments/kb/skills/persona-build/SKILL.md +3 -2
- package/departments/kb/skills/research-plan/SKILL.md +3 -2
- package/departments/kb/skills/search-kb/SKILL.md +3 -2
- package/departments/kb/skills/source-evaluate/SKILL.md +3 -2
- package/departments/kb/skills/taxonomy-manage/SKILL.md +3 -2
- package/departments/kb/skills/write-as-persona/SKILL.md +3 -2
- package/departments/landing/SKILL.md +3 -2
- package/departments/landing/skills/ab-test/SKILL.md +3 -2
- package/departments/landing/skills/affiliate-bridge/SKILL.md +3 -2
- package/departments/landing/skills/awareness-diagnose/SKILL.md +3 -2
- package/departments/landing/skills/email-sequence/SKILL.md +3 -2
- package/departments/landing/skills/funnel-metrics/SKILL.md +3 -2
- package/departments/landing/skills/headline-write/SKILL.md +3 -2
- package/departments/landing/skills/launch-sequence/SKILL.md +3 -2
- package/departments/landing/skills/offer-create/SKILL.md +3 -2
- package/departments/landing/skills/optimize-page/SKILL.md +3 -2
- package/departments/landing/skills/page-architect/SKILL.md +3 -2
- package/departments/landing/skills/persuasion-apply/SKILL.md +3 -2
- package/departments/landing/skills/webinar-funnel/SKILL.md +3 -2
- package/departments/leadership/SKILL.md +3 -2
- package/departments/leadership/skills/change-manage/SKILL.md +3 -2
- package/departments/leadership/skills/conflict-resolve/SKILL.md +3 -2
- package/departments/leadership/skills/culture-audit/SKILL.md +3 -2
- package/departments/leadership/skills/delegation-matrix/SKILL.md +3 -2
- package/departments/leadership/skills/disc-assess/SKILL.md +3 -2
- package/departments/leadership/skills/feedback-give/SKILL.md +3 -2
- package/departments/leadership/skills/hiring-plan/SKILL.md +3 -2
- package/departments/leadership/skills/performance-review/SKILL.md +3 -2
- package/departments/marketing/SKILL.md +3 -2
- package/departments/marketing/skills/ab-test/SKILL.md +3 -2
- package/departments/marketing/skills/analytics-report/SKILL.md +3 -2
- package/departments/marketing/skills/audience-segment/SKILL.md +3 -2
- package/departments/marketing/skills/calendar-plan/SKILL.md +3 -2
- package/departments/marketing/skills/competitor-analysis/SKILL.md +3 -2
- package/departments/marketing/skills/content-audit/SKILL.md +3 -2
- package/departments/marketing/skills/email-sequence/SKILL.md +3 -2
- package/departments/marketing/skills/growth-loop/SKILL.md +3 -2
- package/departments/marketing/skills/marketing-automation/SKILL.md +3 -2
- package/departments/marketing/skills/paid-campaign/SKILL.md +3 -2
- package/departments/marketing/skills/programmatic-seo/SKILL.md +3 -2
- package/departments/marketing/skills/seo-audit/SKILL.md +3 -2
- package/departments/marketing/skills/social-strategy/SKILL.md +3 -2
- package/departments/ops/SKILL.md +3 -2
- package/departments/ops/skills/bottleneck-find/SKILL.md +3 -2
- package/departments/ops/skills/dashboard-build/SKILL.md +3 -2
- package/departments/ops/skills/gdpr-compliance/SKILL.md +3 -2
- package/departments/ops/skills/github-ops/SKILL.md +3 -2
- package/departments/ops/skills/gtd-setup/SKILL.md +3 -2
- package/departments/ops/skills/integration-design/SKILL.md +3 -2
- package/departments/ops/skills/iso27001/SKILL.md +3 -2
- package/departments/ops/skills/lean-audit/SKILL.md +3 -2
- package/departments/ops/skills/metrics-dashboard/SKILL.md +3 -2
- package/departments/ops/skills/n8n-flow/SKILL.md +3 -2
- package/departments/ops/skills/quality-management/SKILL.md +3 -2
- package/departments/ops/skills/risk-management/SKILL.md +3 -2
- package/departments/ops/skills/soc2-compliance/SKILL.md +3 -2
- package/departments/ops/skills/sop-create/SKILL.md +3 -2
- package/departments/ops/skills/terminal-ops/SKILL.md +3 -2
- package/departments/ops/skills/workflow-automate/SKILL.md +3 -2
- package/departments/ops/skills/zapier-flow/SKILL.md +3 -2
- package/departments/org/SKILL.md +3 -2
- package/departments/org/skills/compensation-plan/SKILL.md +3 -2
- package/departments/org/skills/culture-define/SKILL.md +3 -2
- package/departments/org/skills/decision-framework/SKILL.md +3 -2
- package/departments/org/skills/hiring-plan/SKILL.md +3 -2
- package/departments/org/skills/meeting-optimize/SKILL.md +3 -2
- package/departments/org/skills/onboarding-design/SKILL.md +3 -2
- package/departments/org/skills/org-design/SKILL.md +3 -2
- package/departments/org/skills/remote-setup/SKILL.md +3 -2
- package/departments/org/skills/sop-process/SKILL.md +3 -2
- package/departments/org/skills/team-assess/SKILL.md +3 -2
- package/departments/pm/SKILL.md +3 -2
- package/departments/pm/skills/backlog-groom/SKILL.md +3 -2
- package/departments/pm/skills/discovery-plan/SKILL.md +3 -2
- package/departments/pm/skills/estimate-forecast/SKILL.md +3 -2
- package/departments/pm/skills/impact-map/SKILL.md +3 -2
- package/departments/pm/skills/kanban-setup/SKILL.md +3 -2
- package/departments/pm/skills/risk-register/SKILL.md +3 -2
- package/departments/pm/skills/roadmap-build/SKILL.md +3 -2
- package/departments/pm/skills/sprint-plan/SKILL.md +3 -2
- package/departments/pm/skills/stakeholder-map/SKILL.md +3 -2
- package/departments/pm/skills/standup-run/SKILL.md +3 -2
- package/departments/pm/skills/story-write/SKILL.md +3 -2
- package/departments/saas/SKILL.md +3 -2
- package/departments/saas/skills/benchmark-compare/SKILL.md +3 -2
- package/departments/saas/skills/churn-analysis/SKILL.md +3 -2
- package/departments/saas/skills/customer-success/SKILL.md +3 -2
- package/departments/saas/skills/growth-plan/SKILL.md +3 -2
- package/departments/saas/skills/gtm-strategy/SKILL.md +3 -2
- package/departments/saas/skills/launch-execute/SKILL.md +3 -2
- package/departments/saas/skills/metrics-dashboard/SKILL.md +3 -2
- package/departments/saas/skills/micro-saas-stack/SKILL.md +3 -2
- package/departments/saas/skills/mvp-build/SKILL.md +3 -2
- package/departments/saas/skills/niche-evaluate/SKILL.md +3 -2
- package/departments/saas/skills/onboarding-optimize/SKILL.md +3 -2
- package/departments/saas/skills/plg-setup/SKILL.md +3 -2
- package/departments/saas/skills/pricing-strategy/SKILL.md +3 -2
- package/departments/saas/skills/validate-idea/SKILL.md +3 -2
- package/departments/sales/SKILL.md +3 -2
- package/departments/sales/skills/challenger-sell/SKILL.md +3 -2
- package/departments/sales/skills/deal-qualify/SKILL.md +3 -2
- package/departments/sales/skills/discovery-call/SKILL.md +3 -2
- package/departments/sales/skills/forecast-revenue/SKILL.md +3 -2
- package/departments/sales/skills/negotiate-plan/SKILL.md +3 -2
- package/departments/sales/skills/objection-handle/SKILL.md +3 -2
- package/departments/sales/skills/pipeline-manage/SKILL.md +3 -2
- package/departments/sales/skills/pricing-negotiate/SKILL.md +3 -2
- package/departments/strategy/SKILL.md +3 -2
- package/departments/strategy/skills/blue-ocean/SKILL.md +3 -2
- package/departments/strategy/skills/bmc/SKILL.md +3 -2
- package/departments/strategy/skills/board-advisor/SKILL.md +3 -2
- package/departments/strategy/skills/cto-advisor/SKILL.md +3 -2
- package/departments/strategy/skills/extract-data/SKILL.md +3 -2
- package/departments/strategy/skills/five-forces/SKILL.md +3 -2
- package/departments/strategy/skills/growth-strategy/SKILL.md +3 -2
- package/departments/strategy/skills/moat-analysis/SKILL.md +3 -2
- package/departments/strategy/skills/position/SKILL.md +3 -2
- package/departments/strategy/skills/scenario-plan/SKILL.md +3 -2
- package/installer/config-seed.js +81 -25
- package/installer/graphify.js +340 -0
- package/installer/index.js +8 -1
- package/installer/keys.js +18 -3
- package/installer/update.js +8 -1
- package/knowledge/skills-manifest.json +27 -1
- package/mcps/registry.json +9 -9
- package/mcps/scripts/apply-mcps.sh +6 -2
- package/package.json +2 -1
- package/pyproject.toml +1 -1
- package/scripts/dashboard-api.py +336 -124
- package/scripts/guide_gen.py +139 -0
- package/scripts/migrate_skills_kb_first.py +3 -2
- package/scripts/synapse-bridge.py +9 -1
|
@@ -23,6 +23,8 @@ CLI (for hooks/skills)::
|
|
|
23
23
|
from __future__ import annotations
|
|
24
24
|
|
|
25
25
|
import argparse
|
|
26
|
+
import configparser
|
|
27
|
+
import fnmatch
|
|
26
28
|
import importlib.util
|
|
27
29
|
import json
|
|
28
30
|
import re
|
|
@@ -31,7 +33,7 @@ import shutil
|
|
|
31
33
|
import subprocess
|
|
32
34
|
import sys
|
|
33
35
|
import time
|
|
34
|
-
from dataclasses import asdict, dataclass, field
|
|
36
|
+
from dataclasses import asdict, dataclass, field, replace
|
|
35
37
|
from pathlib import Path
|
|
36
38
|
|
|
37
39
|
from core.shared.test_evidence import coverage_percent_from_xml
|
|
@@ -218,18 +220,22 @@ def _labelled(result: CheckResult, label: str) -> CheckResult:
|
|
|
218
220
|
return result
|
|
219
221
|
|
|
220
222
|
|
|
221
|
-
def _tool_cmd(tool: str) -> list[str] | None:
|
|
223
|
+
def _tool_cmd(tool: str, module: str | None = None) -> list[str] | None:
|
|
222
224
|
"""Resolve a Python tool: PATH binary, else the interpreter's module.
|
|
223
225
|
|
|
224
226
|
Operator machines install ruff/pytest into the ArkaOS venv with no
|
|
225
227
|
PATH binary — keying on shutil.which alone silently downgraded
|
|
226
228
|
Python lint to eslint over installer/*.js, a FALSE GREEN on a
|
|
227
229
|
NON-NEGOTIABLE gate (QG findings, F1-B2/F1-C1 reviews).
|
|
230
|
+
|
|
231
|
+
``module`` covers tools whose import name differs from the command
|
|
232
|
+
(codespell ships ``codespell_lib``); without it the module fallback
|
|
233
|
+
misses and the check skips forever on a venv install.
|
|
228
234
|
"""
|
|
229
235
|
if shutil.which(tool):
|
|
230
236
|
return [tool]
|
|
231
|
-
if importlib.util.find_spec(tool) is not None:
|
|
232
|
-
return [sys.executable, "-m", tool]
|
|
237
|
+
if importlib.util.find_spec(module or tool) is not None:
|
|
238
|
+
return [sys.executable, "-m", module or tool]
|
|
233
239
|
return None
|
|
234
240
|
|
|
235
241
|
|
|
@@ -335,15 +341,59 @@ def _check_typecheck(
|
|
|
335
341
|
return _skip("typecheck", "no typecheck configuration detected")
|
|
336
342
|
|
|
337
343
|
|
|
344
|
+
def _project_pytest(project_dir: Path) -> list[str] | None:
|
|
345
|
+
"""pytest belonging to the target project's own environment."""
|
|
346
|
+
for rel in (".venv/bin/pytest", "venv/bin/pytest"):
|
|
347
|
+
candidate = project_dir / rel
|
|
348
|
+
if candidate.is_file():
|
|
349
|
+
return [str(candidate)]
|
|
350
|
+
return None
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def _foreign_pytest_can_collect(
|
|
354
|
+
pytest_cmd: list[str], project_dir: Path, timeout: int,
|
|
355
|
+
) -> bool:
|
|
356
|
+
"""A PATH/venv-foreign pytest must prove it can import the project.
|
|
357
|
+
|
|
358
|
+
Running a foreign-env pytest blind produced false FAILs on projects
|
|
359
|
+
whose dependencies live in their own venv (import errors read as
|
|
360
|
+
test failures — issue #283, a blind gate on mandatory-qa). Exit 5
|
|
361
|
+
("no tests collected") still counts as a working import path.
|
|
362
|
+
"""
|
|
363
|
+
try:
|
|
364
|
+
probe = subprocess.run(
|
|
365
|
+
[*pytest_cmd, "--collect-only", "-q"], cwd=project_dir,
|
|
366
|
+
capture_output=True, text=True, timeout=min(timeout, 60),
|
|
367
|
+
)
|
|
368
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
369
|
+
return False
|
|
370
|
+
return probe.returncode in (0, 5)
|
|
371
|
+
|
|
372
|
+
|
|
338
373
|
def _check_tests(
|
|
339
374
|
project_dir: Path, changed: list[str] | None,
|
|
340
375
|
test_command: str | None, timeout: int,
|
|
341
376
|
) -> CheckResult:
|
|
342
377
|
if test_command:
|
|
343
378
|
return _run("tests", shlex.split(test_command), project_dir, timeout)
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
379
|
+
if _has_python(project_dir, changed):
|
|
380
|
+
local_pytest = _project_pytest(project_dir)
|
|
381
|
+
if local_pytest:
|
|
382
|
+
return _labelled(
|
|
383
|
+
_run("tests", [*local_pytest, "-q"], project_dir, timeout),
|
|
384
|
+
"tests(project-venv)",
|
|
385
|
+
)
|
|
386
|
+
pytest_cmd = _tool_cmd("pytest")
|
|
387
|
+
if pytest_cmd and _foreign_pytest_can_collect(
|
|
388
|
+
pytest_cmd, project_dir, timeout
|
|
389
|
+
):
|
|
390
|
+
return _run("tests", [*pytest_cmd, "-q"], project_dir, timeout)
|
|
391
|
+
if pytest_cmd:
|
|
392
|
+
return _skip(
|
|
393
|
+
"tests",
|
|
394
|
+
"pytest resolved outside the project env cannot import "
|
|
395
|
+
"it — pin --test-command",
|
|
396
|
+
)
|
|
347
397
|
if _package_json_script(project_dir, "test"):
|
|
348
398
|
return _run(
|
|
349
399
|
"tests", ["npm", "test", "--silent"], project_dir, timeout,
|
|
@@ -501,12 +551,54 @@ def _check_spellcheck(
|
|
|
501
551
|
project_dir: Path, changed: list[str] | None,
|
|
502
552
|
test_command: str | None, timeout: int,
|
|
503
553
|
) -> CheckResult:
|
|
504
|
-
|
|
554
|
+
# Resolve like lint/tests do. A bare shutil.which lookup left this check
|
|
555
|
+
# dark on venv installs (the binary is not on the harness PATH), so three
|
|
556
|
+
# consecutive QG rounds shipped with NO machine spellcheck signal.
|
|
557
|
+
cmd = _tool_cmd("codespell", module="codespell_lib")
|
|
558
|
+
if cmd is None:
|
|
505
559
|
return _skip("spellcheck", "codespell not installed")
|
|
506
560
|
md_files = [f for f in changed or [] if f.endswith(".md")]
|
|
507
561
|
if not md_files:
|
|
508
562
|
return _skip("spellcheck", "no changed .md files")
|
|
509
|
-
|
|
563
|
+
result = _run("spellcheck", [*cmd, *md_files], project_dir, timeout)
|
|
564
|
+
# codespell honours .codespellrc `skip` even for explicitly listed files,
|
|
565
|
+
# so a PASS here can cover far fewer files than were handed in. Reporting
|
|
566
|
+
# the raw count as coverage is how a narrowed scope reads as a full green
|
|
567
|
+
# (QG finding: 8 of 220 inspected, reported as 220). State both.
|
|
568
|
+
inspected = _spellcheck_inspected_count(project_dir, md_files)
|
|
569
|
+
scope = f"{inspected} of {len(md_files)} changed .md inspected"
|
|
570
|
+
if inspected < len(md_files):
|
|
571
|
+
scope += f"; {len(md_files) - inspected} excluded by .codespellrc skip"
|
|
572
|
+
summary = f"{scope}\n{result.summary}" if result.summary else scope
|
|
573
|
+
return replace(result, summary=summary)
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
def _spellcheck_inspected_count(project_dir: Path, md_files: list[str]) -> int:
|
|
577
|
+
"""How many of ``md_files`` codespell actually reads after `skip`.
|
|
578
|
+
|
|
579
|
+
Asks codespell itself (``--count`` on a per-file basis is too slow; a
|
|
580
|
+
skipped file simply produces no output for any planted probe), so instead
|
|
581
|
+
we replay its own glob semantics from the config.
|
|
582
|
+
"""
|
|
583
|
+
patterns = _codespell_skip_globs(project_dir)
|
|
584
|
+
if not patterns:
|
|
585
|
+
return len(md_files)
|
|
586
|
+
return sum(
|
|
587
|
+
1 for f in md_files
|
|
588
|
+
if not any(fnmatch.fnmatch(f, p) or fnmatch.fnmatch(f"./{f}", p) for p in patterns)
|
|
589
|
+
)
|
|
590
|
+
|
|
591
|
+
|
|
592
|
+
def _codespell_skip_globs(project_dir: Path) -> list[str]:
|
|
593
|
+
"""`skip` globs from .codespellrc, or [] when unreadable/absent."""
|
|
594
|
+
cfg = project_dir / ".codespellrc"
|
|
595
|
+
try:
|
|
596
|
+
parser = configparser.ConfigParser()
|
|
597
|
+
parser.read(cfg, encoding="utf-8")
|
|
598
|
+
raw = parser.get("codespell", "skip", fallback="")
|
|
599
|
+
except (OSError, configparser.Error):
|
|
600
|
+
return []
|
|
601
|
+
return [p.strip() for p in raw.split(",") if p.strip()]
|
|
510
602
|
|
|
511
603
|
|
|
512
604
|
def _check_ui_screenshot(
|
|
@@ -20,6 +20,15 @@ from dataclasses import dataclass
|
|
|
20
20
|
|
|
21
21
|
_WIKILINK_PATTERN: re.Pattern[str] = re.compile(r"\[\[([^\[\]]+)\]\]")
|
|
22
22
|
_KNOWLEDGE_PATTERN: re.Pattern[str] = re.compile(r"\[knowledge:", re.IGNORECASE)
|
|
23
|
+
# Graphify knowledge-graph citation: the `[graph:` marker ONLY (parallel to
|
|
24
|
+
# `[knowledge:`), which stands for a node actually returned by the graph.
|
|
25
|
+
# The tool NAME is deliberately excluded: naming a tool is narration, not
|
|
26
|
+
# evidence, and the literal `mcp__graphify__query_graph` appears in the
|
|
27
|
+
# KB-first pointer of every migrated SKILL.md — crediting it would let any
|
|
28
|
+
# response that merely echoes the doctrine self-certify. Same asymmetry as
|
|
29
|
+
# `mcp__obsidian`, which is a topic keyword and earns credit only via a real
|
|
30
|
+
# `[[wikilink]]`.
|
|
31
|
+
_GRAPHIFY_PATTERN: re.Pattern[str] = re.compile(r"\[graph:", re.IGNORECASE)
|
|
23
32
|
# File:line — requires at least one path separator. Quantifiers are bounded
|
|
24
33
|
# to prevent catastrophic backtracking on pathological input (PR18 security
|
|
25
34
|
# review: unbounded variant blew past Stop's 5s budget by ~8x on 100KB of
|
|
@@ -41,8 +50,8 @@ _ARKAOS_KEYWORDS: frozenset[str] = frozenset({
|
|
|
41
50
|
"ines", "rafael", "beatriz", "miguel", "rodrigo",
|
|
42
51
|
"non-negotiable", "tier 0", "squad lead",
|
|
43
52
|
"core/governance", "core/synapse", "core/cognition",
|
|
44
|
-
"core/workflow", "mcp__obsidian", "kb-first",
|
|
45
|
-
"departments/", "agent yaml", "obsidian vault",
|
|
53
|
+
"core/workflow", "mcp__obsidian", "mcp__graphify", "kb-first",
|
|
54
|
+
"departments/", "agent yaml", "obsidian vault", "graphify",
|
|
46
55
|
})
|
|
47
56
|
|
|
48
57
|
_BYPASS_DEFAULTS: tuple[str, ...] = ("[arka:trivial]",)
|
|
@@ -50,8 +59,8 @@ _TRIVIAL_WORD_THRESHOLD: int = 15
|
|
|
50
59
|
_TOPIC_THRESHOLD: float = 0.4
|
|
51
60
|
_SUGGESTION_TEXT: str = (
|
|
52
61
|
"KB-first — last response had no citation on ArkaOS topic. "
|
|
53
|
-
"Use @[[note-name]], /kb search, or
|
|
54
|
-
"to ground the next answer."
|
|
62
|
+
"Use @[[note-name]], /kb search, mcp__obsidian__search_notes, or "
|
|
63
|
+
"mcp__graphify__query_graph to ground the next answer."
|
|
55
64
|
)
|
|
56
65
|
|
|
57
66
|
|
|
@@ -111,13 +120,14 @@ def _is_trivial_length(text: str) -> bool:
|
|
|
111
120
|
def _count_citations(text: str) -> int:
|
|
112
121
|
# Truncate to _MAX_SCAN_CHARS first; the file-line regex is the only
|
|
113
122
|
# backtracking-prone pattern and the bounded quantifiers (capped at 8
|
|
114
|
-
# segments
|
|
123
|
+
# segments x 64 chars each) combined with this slice make all three
|
|
115
124
|
# patterns safe to run unconditionally below.
|
|
116
125
|
scan = text if len(text) <= _MAX_SCAN_CHARS else text[:_MAX_SCAN_CHARS]
|
|
117
126
|
wikilinks = len(_WIKILINK_PATTERN.findall(scan))
|
|
118
127
|
knowledge = 1 if _KNOWLEDGE_PATTERN.search(scan) else 0
|
|
128
|
+
graphify = 1 if _GRAPHIFY_PATTERN.search(scan) else 0
|
|
119
129
|
files = len(_FILE_LINE_PATTERN.findall(scan))
|
|
120
|
-
return wikilinks + knowledge + files
|
|
130
|
+
return wikilinks + knowledge + graphify + files
|
|
121
131
|
|
|
122
132
|
|
|
123
133
|
def _compute_topic_score(text: str, keywords: frozenset[str]) -> float:
|
|
@@ -84,6 +84,29 @@ def _git(args: list[str], cwd: Path) -> subprocess.CompletedProcess | None:
|
|
|
84
84
|
return None
|
|
85
85
|
|
|
86
86
|
|
|
87
|
+
def _porcelain_entries(stdout: str) -> list[tuple[str, str]]:
|
|
88
|
+
"""(status, path) pairs from ``git status --porcelain -z`` output.
|
|
89
|
+
|
|
90
|
+
``-z`` terminates entries with NUL and never quotes paths (the
|
|
91
|
+
line-based form octal-escapes non-ASCII and quoted names, which
|
|
92
|
+
broke path matching). Rename/copy entries carry a second
|
|
93
|
+
NUL-separated field — the original path — which is skipped.
|
|
94
|
+
"""
|
|
95
|
+
entries: list[tuple[str, str]] = []
|
|
96
|
+
tokens = stdout.split("\0")
|
|
97
|
+
index = 0
|
|
98
|
+
while index < len(tokens):
|
|
99
|
+
token = tokens[index]
|
|
100
|
+
index += 1
|
|
101
|
+
if len(token) < 4:
|
|
102
|
+
continue
|
|
103
|
+
status, path = token[:2], token[3:]
|
|
104
|
+
if status[0] in "RC":
|
|
105
|
+
index += 1 # skip the original-path field
|
|
106
|
+
entries.append((status, path))
|
|
107
|
+
return entries
|
|
108
|
+
|
|
109
|
+
|
|
87
110
|
def changed_files(project_dir: Path) -> list[str]:
|
|
88
111
|
"""Repo-relative files changed vs the merge-base, plus untracked."""
|
|
89
112
|
try:
|
|
@@ -93,15 +116,15 @@ def changed_files(project_dir: Path) -> list[str]:
|
|
|
93
116
|
if base is None:
|
|
94
117
|
return []
|
|
95
118
|
out: list[str] = []
|
|
96
|
-
diff = _git(["diff", "--name-only", base], project_dir)
|
|
119
|
+
diff = _git(["diff", "--name-only", "-z", base], project_dir)
|
|
97
120
|
if diff is not None and diff.returncode == 0:
|
|
98
|
-
out.extend(f
|
|
99
|
-
status = _git(["status", "--porcelain"], project_dir)
|
|
121
|
+
out.extend(f for f in diff.stdout.split("\0") if f.strip())
|
|
122
|
+
status = _git(["status", "--porcelain", "-z"], project_dir)
|
|
100
123
|
if status is not None and status.returncode == 0:
|
|
101
124
|
out.extend(
|
|
102
|
-
|
|
103
|
-
for
|
|
104
|
-
if
|
|
125
|
+
path
|
|
126
|
+
for entry_status, path in _porcelain_entries(status.stdout)
|
|
127
|
+
if entry_status == "??" and path.strip()
|
|
105
128
|
)
|
|
106
129
|
seen: set[str] = set()
|
|
107
130
|
return [f for f in out if not (f in seen or seen.add(f))]
|
|
@@ -109,7 +132,7 @@ def changed_files(project_dir: Path) -> list[str]:
|
|
|
109
132
|
|
|
110
133
|
def _fingerprint(project_dir: Path) -> str | None:
|
|
111
134
|
head = _git(["rev-parse", "HEAD"], project_dir)
|
|
112
|
-
status = _git(["status", "--porcelain"], project_dir)
|
|
135
|
+
status = _git(["status", "--porcelain", "-z"], project_dir)
|
|
113
136
|
if head is None or head.returncode != 0 or status is None:
|
|
114
137
|
return None
|
|
115
138
|
digest = hashlib.sha1()
|
|
@@ -117,9 +140,9 @@ def _fingerprint(project_dir: Path) -> str | None:
|
|
|
117
140
|
# Porcelain alone only says WHICH files are dirty — editing an
|
|
118
141
|
# already-dirty file leaves it unchanged. Fold in mtime+size so
|
|
119
142
|
# content edits re-arm the worker.
|
|
120
|
-
for
|
|
121
|
-
digest.update(
|
|
122
|
-
digest.update(_stat_token(project_dir /
|
|
143
|
+
for entry_status, path in sorted(_porcelain_entries(status.stdout)):
|
|
144
|
+
digest.update(f"{entry_status} {path}".encode("utf-8", "replace"))
|
|
145
|
+
digest.update(_stat_token(project_dir / path))
|
|
123
146
|
return digest.hexdigest()
|
|
124
147
|
|
|
125
148
|
|
|
@@ -151,9 +174,13 @@ def _remember_fingerprint(state_file: Path, fingerprint: str) -> None:
|
|
|
151
174
|
prev_umask = os.umask(0o077)
|
|
152
175
|
try:
|
|
153
176
|
state_file.parent.mkdir(parents=True, exist_ok=True)
|
|
154
|
-
|
|
177
|
+
# Atomic tmp+replace: a crash mid-write must never leave a
|
|
178
|
+
# truncated state file behind (the reorganizer report pattern).
|
|
179
|
+
tmp_file = state_file.with_suffix(f".tmp-{os.getpid()}")
|
|
180
|
+
tmp_file.write_text(
|
|
155
181
|
json.dumps({"fingerprint": fingerprint}), encoding="utf-8"
|
|
156
182
|
)
|
|
183
|
+
os.replace(tmp_file, state_file)
|
|
157
184
|
except OSError:
|
|
158
185
|
pass
|
|
159
186
|
finally:
|
|
@@ -34,7 +34,9 @@ class StopLintSummary:
|
|
|
34
34
|
runs: int
|
|
35
35
|
skipped_runs: int
|
|
36
36
|
lint_pass_rate: float
|
|
37
|
-
|
|
37
|
+
# None when no run observed the check (typecheck is opt-in) —
|
|
38
|
+
# distinct from 0.0, which means "observed and failing".
|
|
39
|
+
typecheck_pass_rate: float | None
|
|
38
40
|
would_block_rate: float
|
|
39
41
|
corrupt_line_count: int = 0
|
|
40
42
|
|
|
@@ -119,7 +121,7 @@ def _build_summary(
|
|
|
119
121
|
runs=len(rows),
|
|
120
122
|
skipped_runs=len(rows) - len(observed),
|
|
121
123
|
lint_pass_rate=_true_rate(observed, "lint_passed"),
|
|
122
|
-
typecheck_pass_rate=
|
|
124
|
+
typecheck_pass_rate=_true_rate_or_none(observed, "typecheck_passed"),
|
|
123
125
|
would_block_rate=_true_rate(observed, "would_block"),
|
|
124
126
|
corrupt_line_count=corrupt,
|
|
125
127
|
)
|
|
@@ -130,3 +132,10 @@ def _true_rate(rows: list[dict[str, Any]], key: str) -> float:
|
|
|
130
132
|
if not observed:
|
|
131
133
|
return 0.0
|
|
132
134
|
return sum(1 for r in observed if r[key] is True) / len(observed)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _true_rate_or_none(rows: list[dict[str, Any]], key: str) -> float | None:
|
|
138
|
+
"""Like ``_true_rate`` but honest about zero observations."""
|
|
139
|
+
if not any(isinstance(r.get(key), bool) for r in rows):
|
|
140
|
+
return None
|
|
141
|
+
return _true_rate(rows, key)
|
|
@@ -36,7 +36,6 @@ import subprocess
|
|
|
36
36
|
import time
|
|
37
37
|
from pathlib import Path
|
|
38
38
|
|
|
39
|
-
from core.shared.temp_paths import arkaos_temp_dir
|
|
40
39
|
from core.hooks._shared import (
|
|
41
40
|
ensure_root_on_path,
|
|
42
41
|
get_str,
|
|
@@ -45,6 +44,7 @@ from core.hooks._shared import (
|
|
|
45
44
|
resolve_arkaos_root,
|
|
46
45
|
safe_session_id,
|
|
47
46
|
)
|
|
47
|
+
from core.shared.temp_paths import arkaos_temp_dir
|
|
48
48
|
|
|
49
49
|
_CACHE_DIR = arkaos_temp_dir("arkaos-context-cache")
|
|
50
50
|
_CACHE_TTL = 300 # Constitution cache: 5 minutes
|
|
@@ -89,13 +89,14 @@ _WF_VERB_PATTERN = (
|
|
|
89
89
|
)
|
|
90
90
|
_WF_VERB_RE = re.compile(rf"\b{_WF_VERB_PATTERN}\b", re.IGNORECASE)
|
|
91
91
|
|
|
92
|
-
_STOPWORDS = frozenset(
|
|
93
|
-
"the a an and or but if then of for to in on
|
|
94
|
-
"
|
|
95
|
-
"
|
|
96
|
-
"
|
|
97
|
-
"
|
|
98
|
-
|
|
92
|
+
_STOPWORDS = frozenset([
|
|
93
|
+
"the", "a", "an", "and", "or", "but", "if", "then", "of", "for", "to", "in", "on",
|
|
94
|
+
"at", "by", "with", "from", "is", "are", "was", "were", "be", "been", "being", "do",
|
|
95
|
+
"does", "did", "have", "has", "had", "this", "that", "these", "those", "it", "its",
|
|
96
|
+
"as", "i", "you", "we", "they", "he", "she", "them", "my", "your", "our", "their",
|
|
97
|
+
"so", "not", "no", "yes", "can", "will", "would", "could", "should", "may", "might",
|
|
98
|
+
"must", "need", "want", "fix", "make", "use", "get", "set", "add", "remove"
|
|
99
|
+
])
|
|
99
100
|
|
|
100
101
|
_VAGUE_PHRASES = (
|
|
101
102
|
"fix the bug", "that file", "the error",
|
|
@@ -228,7 +229,14 @@ def _run_bridge(root: str, user_input: str, session_id: str, cwd: str = "") -> s
|
|
|
228
229
|
payload["cwd"] = cwd
|
|
229
230
|
output, code = module.run_bridge(payload, Path(root))
|
|
230
231
|
if code == 0:
|
|
231
|
-
|
|
232
|
+
parts = [str(output.get("context_string", ""))]
|
|
233
|
+
# Full-text blocks follow the compact tag line. A tag such as
|
|
234
|
+
# `[kb-context:5 +graph]` announces an injection; without these
|
|
235
|
+
# the announcement was all the model ever received.
|
|
236
|
+
blocks = output.get("content_blocks") or []
|
|
237
|
+
if isinstance(blocks, list):
|
|
238
|
+
parts.extend(str(b) for b in blocks if b)
|
|
239
|
+
return "\n".join(p for p in parts if p)
|
|
232
240
|
except Exception:
|
|
233
241
|
pass
|
|
234
242
|
return ""
|
|
@@ -251,10 +259,6 @@ def _workflow_tag() -> str:
|
|
|
251
259
|
workflow = str(state.get("workflow", "") or "")
|
|
252
260
|
branch = str(state.get("branch", "") or "")
|
|
253
261
|
violations = len(state.get("violations", []) or [])
|
|
254
|
-
total = len(phases)
|
|
255
|
-
completed = sum(
|
|
256
|
-
1 for p in phases.values() if p.get("status") == "completed"
|
|
257
|
-
)
|
|
258
262
|
current = next(
|
|
259
263
|
(k for k, p in phases.items() if p.get("status") == "in_progress"),
|
|
260
264
|
"none",
|
|
@@ -334,10 +338,8 @@ def _l0_constitution() -> str:
|
|
|
334
338
|
return cache_file.read_text(encoding="utf-8")
|
|
335
339
|
except OSError:
|
|
336
340
|
pass
|
|
337
|
-
|
|
341
|
+
with contextlib.suppress(OSError):
|
|
338
342
|
cache_file.write_text(_L0_FALLBACK, encoding="utf-8")
|
|
339
|
-
except OSError:
|
|
340
|
-
pass
|
|
341
343
|
return _L0_FALLBACK
|
|
342
344
|
|
|
343
345
|
|
|
@@ -501,10 +503,8 @@ def _one_shot_nudge(subdir: str, session_id: str) -> str:
|
|
|
501
503
|
nudge = f"[arka:suggest] {suggestion}"
|
|
502
504
|
except (OSError, json.JSONDecodeError, ValueError):
|
|
503
505
|
pass
|
|
504
|
-
|
|
506
|
+
with contextlib.suppress(OSError):
|
|
505
507
|
nudge_file.unlink(missing_ok=True)
|
|
506
|
-
except OSError:
|
|
507
|
-
pass
|
|
508
508
|
return nudge
|
|
509
509
|
|
|
510
510
|
|
|
@@ -548,10 +548,8 @@ def main(stdin_json: dict | None = None, raw: str = "") -> int:
|
|
|
548
548
|
|
|
549
549
|
root = resolve_arkaos_root()
|
|
550
550
|
ensure_root_on_path(root)
|
|
551
|
-
|
|
551
|
+
with contextlib.suppress(OSError):
|
|
552
552
|
_CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
|
553
|
-
except OSError:
|
|
554
|
-
pass
|
|
555
553
|
|
|
556
554
|
# Claude Code sends the user's text under "prompt" (UserPromptSubmit
|
|
557
555
|
# hook schema); "userInput"/"message" cover other runtimes. Missing
|
|
@@ -683,4 +681,4 @@ if __name__ == "__main__":
|
|
|
683
681
|
except Exception:
|
|
684
682
|
# Fail open with the minimal constitution context.
|
|
685
683
|
print(json.dumps({"additionalContext": _L0_FALLBACK}))
|
|
686
|
-
raise SystemExit(0)
|
|
684
|
+
raise SystemExit(0) from None
|
package/core/synapse/engine.py
CHANGED
|
@@ -13,8 +13,8 @@ import time
|
|
|
13
13
|
from dataclasses import dataclass, field
|
|
14
14
|
from typing import Any
|
|
15
15
|
|
|
16
|
-
from core.synapse.layers import Layer, LayerResult, PromptContext
|
|
17
16
|
from core.synapse.cache import LayerCache
|
|
17
|
+
from core.synapse.layers import Layer, LayerResult, PromptContext
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
@dataclass
|
|
@@ -27,6 +27,12 @@ class SynapseResult:
|
|
|
27
27
|
total_tokens_est: int # Estimated total tokens injected
|
|
28
28
|
cache_stats: dict # Cache hit/miss statistics
|
|
29
29
|
layers_skipped: int # Layers that returned empty results
|
|
30
|
+
# Full-text blocks from layers whose content is richer than their tag
|
|
31
|
+
# (L2.5 KB context, graph context). Kept separate from context_string so
|
|
32
|
+
# the compact tag line stays compact; the hook appends these after it.
|
|
33
|
+
# Before this existed, LayerResult.content had NO consumer — every block a
|
|
34
|
+
# layer built was discarded while its tag still advertised the injection.
|
|
35
|
+
content_blocks: list[str] = field(default_factory=list)
|
|
30
36
|
|
|
31
37
|
|
|
32
38
|
class SynapseEngine:
|
|
@@ -44,11 +50,11 @@ class SynapseEngine:
|
|
|
44
50
|
def register_layer(self, layer: Layer) -> None:
|
|
45
51
|
"""Register a context layer. Layers execute in priority order."""
|
|
46
52
|
self._layers.append(layer)
|
|
47
|
-
self._layers.sort(key=lambda
|
|
53
|
+
self._layers.sort(key=lambda x: x.priority)
|
|
48
54
|
|
|
49
55
|
def remove_layer(self, layer_id: str) -> None:
|
|
50
56
|
"""Remove a layer by ID."""
|
|
51
|
-
self._layers = [
|
|
57
|
+
self._layers = [x for x in self._layers if x.id != layer_id]
|
|
52
58
|
|
|
53
59
|
def get_layer(self, layer_id: str) -> Layer | None:
|
|
54
60
|
"""Get a layer by ID."""
|
|
@@ -70,10 +76,13 @@ class SynapseEngine:
|
|
|
70
76
|
results: list[LayerResult] = []
|
|
71
77
|
skipped = 0
|
|
72
78
|
|
|
79
|
+
block_layers: set[str] = set()
|
|
73
80
|
for layer in self._layers:
|
|
74
81
|
result = self._compute_layer(layer, ctx)
|
|
75
82
|
if result.tag or result.content:
|
|
76
83
|
results.append(result)
|
|
84
|
+
if layer.emits_block:
|
|
85
|
+
block_layers.add(layer.id)
|
|
77
86
|
else:
|
|
78
87
|
skipped += 1
|
|
79
88
|
|
|
@@ -81,6 +90,15 @@ class SynapseEngine:
|
|
|
81
90
|
tags = [r.tag for r in results if r.tag]
|
|
82
91
|
context_string = " ".join(tags)
|
|
83
92
|
|
|
93
|
+
# Full-text blocks — OPT-IN via Layer.emits_block. Collecting on
|
|
94
|
+
# `content != tag` instead swept up every layer whose content is its
|
|
95
|
+
# tag's value ("dev", "active", "feat/plan-canvas"), injecting stray
|
|
96
|
+
# unlabeled lines into every prompt for zero added information.
|
|
97
|
+
content_blocks = [
|
|
98
|
+
r.content for r in results
|
|
99
|
+
if r.content and r.layer_id in block_layers
|
|
100
|
+
]
|
|
101
|
+
|
|
84
102
|
total_tokens = sum(r.tokens_est for r in results)
|
|
85
103
|
total_ms = int((time.time() - start) * 1000)
|
|
86
104
|
|
|
@@ -105,6 +123,7 @@ class SynapseEngine:
|
|
|
105
123
|
total_tokens_est=total_tokens,
|
|
106
124
|
cache_stats=self._cache.stats,
|
|
107
125
|
layers_skipped=skipped,
|
|
126
|
+
content_blocks=content_blocks,
|
|
108
127
|
)
|
|
109
128
|
|
|
110
129
|
def _compute_layer(self, layer: Layer, ctx: PromptContext) -> LayerResult:
|
|
@@ -125,15 +144,23 @@ class SynapseEngine:
|
|
|
125
144
|
session_id = (ctx.extra or {}).get("session_id", "default")
|
|
126
145
|
cache_key += f":{session_id}"
|
|
127
146
|
|
|
128
|
-
# Check cache
|
|
147
|
+
# Check cache. The stored value is `tag` alone, or `tag\x00content`
|
|
148
|
+
# whenever content differs from tag: replaying the tag as the content
|
|
149
|
+
# would feed a marker to the model in place of what it advertises.
|
|
150
|
+
# This condition is deliberately WIDER than `emits_block` — the cache
|
|
151
|
+
# round-trips content faithfully for every layer (AgentLayer stores
|
|
152
|
+
# `[agent:x disc:y]\x00x` and is no block at all); only the block
|
|
153
|
+
# channel in inject() is opt-in.
|
|
129
154
|
if layer.cache_ttl > 0:
|
|
130
155
|
cached = self._cache.get(cache_key)
|
|
131
156
|
if cached is not None:
|
|
157
|
+
tag, _, content = cached.partition("\x00")
|
|
158
|
+
content = content or tag
|
|
132
159
|
return LayerResult(
|
|
133
160
|
layer_id=layer.id,
|
|
134
|
-
tag=
|
|
135
|
-
content=
|
|
136
|
-
tokens_est=len(
|
|
161
|
+
tag=tag,
|
|
162
|
+
content=content,
|
|
163
|
+
tokens_est=len(content.split()),
|
|
137
164
|
compute_ms=0,
|
|
138
165
|
cached=True,
|
|
139
166
|
)
|
|
@@ -143,7 +170,10 @@ class SynapseEngine:
|
|
|
143
170
|
|
|
144
171
|
# Cache if TTL > 0 and result is non-empty
|
|
145
172
|
if layer.cache_ttl > 0 and result.tag:
|
|
146
|
-
|
|
173
|
+
stored = result.tag
|
|
174
|
+
if result.content and result.content != result.tag:
|
|
175
|
+
stored = f"{result.tag}\x00{result.content}"
|
|
176
|
+
self._cache.set(cache_key, stored, layer.cache_ttl)
|
|
147
177
|
|
|
148
178
|
return result
|
|
149
179
|
|
|
@@ -188,21 +218,21 @@ def create_default_engine(
|
|
|
188
218
|
Returns:
|
|
189
219
|
Configured SynapseEngine ready to use.
|
|
190
220
|
"""
|
|
221
|
+
from core.synapse.agent_experiences_layer import AgentExperiencesLayer
|
|
222
|
+
from core.synapse.graph_context_layer import GraphContextLayer
|
|
191
223
|
from core.synapse.layers import (
|
|
192
|
-
ConstitutionLayer,
|
|
193
|
-
DepartmentLayer,
|
|
194
224
|
AgentLayer,
|
|
195
|
-
ProjectLayer,
|
|
196
225
|
BranchLayer,
|
|
197
226
|
CommandHintsLayer,
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
KBContextLayer,
|
|
227
|
+
ConstitutionLayer,
|
|
228
|
+
DepartmentLayer,
|
|
201
229
|
ForgeContextLayer,
|
|
230
|
+
KBContextLayer,
|
|
231
|
+
KnowledgeRetrievalLayer,
|
|
232
|
+
ProjectLayer,
|
|
233
|
+
QualityGateLayer,
|
|
202
234
|
SessionContextLayer,
|
|
203
235
|
)
|
|
204
|
-
from core.synapse.agent_experiences_layer import AgentExperiencesLayer
|
|
205
|
-
from core.synapse.graph_context_layer import GraphContextLayer
|
|
206
236
|
from core.synapse.pattern_library_layer import PatternLibraryLayer
|
|
207
237
|
from core.synapse.recipe_layer import RecipeLayer
|
|
208
238
|
from core.synapse.routing_feedback_layer import RoutingFeedbackLayer
|
|
@@ -26,7 +26,7 @@ import json
|
|
|
26
26
|
import os
|
|
27
27
|
import time
|
|
28
28
|
from pathlib import Path
|
|
29
|
-
from typing import Any
|
|
29
|
+
from typing import Any
|
|
30
30
|
|
|
31
31
|
from core.synapse.layers import Layer, LayerResult, PromptContext
|
|
32
32
|
from core.synapse.pattern_library_layer import _extract_keywords
|
|
@@ -58,7 +58,7 @@ def _graph_flag_on() -> bool:
|
|
|
58
58
|
return bool(synapse_cfg.get("graphContext", True))
|
|
59
59
|
|
|
60
60
|
|
|
61
|
-
def _locate_graph(cwd: str) ->
|
|
61
|
+
def _locate_graph(cwd: str) -> Path | None:
|
|
62
62
|
"""Find ``graphify-out/graph.json`` in cwd or up to 3 parent dirs."""
|
|
63
63
|
if not cwd:
|
|
64
64
|
return None
|
|
@@ -149,6 +149,11 @@ class GraphContextLayer(Layer):
|
|
|
149
149
|
def cache_ttl(self) -> int:
|
|
150
150
|
return 30
|
|
151
151
|
|
|
152
|
+
@property
|
|
153
|
+
def emits_block(self) -> bool:
|
|
154
|
+
"""Content is the self-naming [arka:graph-context:N] block."""
|
|
155
|
+
return True
|
|
156
|
+
|
|
152
157
|
@property
|
|
153
158
|
def priority(self) -> int:
|
|
154
159
|
return 26 # right after L2.5 KBContext (25), before ProjectLayer (30)
|