algo-cli-runtime 0.14.0__py3-none-any.whl
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.
- algo_cli/__init__.py +3 -0
- algo_cli/__main__.py +7 -0
- algo_cli/_internal/__init__.py +12 -0
- algo_cli/_internal/policy_chain.py +259 -0
- algo_cli/action_registry.py +1047 -0
- algo_cli/agent_blocks.py +550 -0
- algo_cli/agent_pipeline.py +1457 -0
- algo_cli/agent_threads.py +308 -0
- algo_cli/animations.py +316 -0
- algo_cli/cache_admission.py +209 -0
- algo_cli/capability_mask.py +66 -0
- algo_cli/chat_protocol.py +116 -0
- algo_cli/chatgpt_auth.py +510 -0
- algo_cli/chatgpt_client.py +657 -0
- algo_cli/code_rag.py +479 -0
- algo_cli/config.py +651 -0
- algo_cli/context_budget.py +679 -0
- algo_cli/credential_helpers.py +315 -0
- algo_cli/deliberation.py +29 -0
- algo_cli/display.py +1470 -0
- algo_cli/evals/__init__.py +21 -0
- algo_cli/evals/algorithm_effectiveness.py +560 -0
- algo_cli/evals/competitive_harness_rating.py +702 -0
- algo_cli/evals/cot_quality.py +220 -0
- algo_cli/evals/harness_retrieval_benchmark.py +401 -0
- algo_cli/evals/performance_regression.py +136 -0
- algo_cli/evals/scorecard_grading.py +308 -0
- algo_cli/evals/session_distribution.py +84 -0
- algo_cli/execution_guardrails.py +806 -0
- algo_cli/extensions_manifest.py +84 -0
- algo_cli/git_evidence.py +227 -0
- algo_cli/google_workspace.py +407 -0
- algo_cli/google_workspace_auth.py +523 -0
- algo_cli/harness.py +2587 -0
- algo_cli/identity.py +557 -0
- algo_cli/index_compute_lab.py +228 -0
- algo_cli/inference_harness.py +70 -0
- algo_cli/intelligence/__init__.py +1103 -0
- algo_cli/intelligence/acrobat_config.py +307 -0
- algo_cli/intelligence/acrobat_manifests.py +338 -0
- algo_cli/intelligence/acrobat_models.py +195 -0
- algo_cli/intelligence/acrobat_pipeline.py +295 -0
- algo_cli/intelligence/acrobat_runtime.py +302 -0
- algo_cli/intelligence/acrobat_security.py +261 -0
- algo_cli/intelligence/acrobat_workflows.py +226 -0
- algo_cli/intelligence/actionability.py +165 -0
- algo_cli/intelligence/adversarial_audit.py +136 -0
- algo_cli/intelligence/agent_arena.py +92 -0
- algo_cli/intelligence/agent_benchmark.py +236 -0
- algo_cli/intelligence/agent_runtime.py +171 -0
- algo_cli/intelligence/agents_as_tools.py +70 -0
- algo_cli/intelligence/artifact_binding.py +80 -0
- algo_cli/intelligence/autonomous_engineer.py +1976 -0
- algo_cli/intelligence/backpressure.py +99 -0
- algo_cli/intelligence/bloom_filter.py +186 -0
- algo_cli/intelligence/bonferroni.py +66 -0
- algo_cli/intelligence/boundary_compaction.py +98 -0
- algo_cli/intelligence/catalog_verifier.py +172 -0
- algo_cli/intelligence/cavecrew.py +118 -0
- algo_cli/intelligence/changelog.py +176 -0
- algo_cli/intelligence/checkpoint_resume.py +92 -0
- algo_cli/intelligence/circuit_breaker.py +88 -0
- algo_cli/intelligence/clarification_gate.py +101 -0
- algo_cli/intelligence/code_graph.py +180 -0
- algo_cli/intelligence/coderank.py +97 -0
- algo_cli/intelligence/consistent_hash.py +150 -0
- algo_cli/intelligence/consortium_synthesis.py +139 -0
- algo_cli/intelligence/construction/__init__.py +241 -0
- algo_cli/intelligence/construction/common.py +273 -0
- algo_cli/intelligence/construction/documents.py +496 -0
- algo_cli/intelligence/construction/labor_units.py +1395 -0
- algo_cli/intelligence/construction/payments.py +470 -0
- algo_cli/intelligence/construction/risk.py +784 -0
- algo_cli/intelligence/content_extractor.py +132 -0
- algo_cli/intelligence/context_adaptive.py +102 -0
- algo_cli/intelligence/context_ops.py +95 -0
- algo_cli/intelligence/count_min.py +145 -0
- algo_cli/intelligence/cow_state.py +103 -0
- algo_cli/intelligence/critic_loop.py +119 -0
- algo_cli/intelligence/cross_source.py +113 -0
- algo_cli/intelligence/daemon_mode.py +99 -0
- algo_cli/intelligence/dag_orchestration.py +151 -0
- algo_cli/intelligence/deep_research.py +155 -0
- algo_cli/intelligence/degenerate_detector.py +78 -0
- algo_cli/intelligence/delta_report.py +92 -0
- algo_cli/intelligence/discovery_event_log.py +92 -0
- algo_cli/intelligence/document_ingest.py +298 -0
- algo_cli/intelligence/dual_layer_validate.py +151 -0
- algo_cli/intelligence/echo_fidelity.py +73 -0
- algo_cli/intelligence/ema_tuning.py +104 -0
- algo_cli/intelligence/event_log.py +92 -0
- algo_cli/intelligence/evidence_graph.py +114 -0
- algo_cli/intelligence/extension_host.py +162 -0
- algo_cli/intelligence/extension_manifest.py +115 -0
- algo_cli/intelligence/falsification_suite.py +178 -0
- algo_cli/intelligence/finance/__init__.py +169 -0
- algo_cli/intelligence/finance/anomalies.py +135 -0
- algo_cli/intelligence/finance/ap_ar.py +351 -0
- algo_cli/intelligence/finance/cash.py +162 -0
- algo_cli/intelligence/finance/close.py +332 -0
- algo_cli/intelligence/finance/common.py +244 -0
- algo_cli/intelligence/finance/construction.py +135 -0
- algo_cli/intelligence/finance/controls.py +172 -0
- algo_cli/intelligence/finance/evidence.py +119 -0
- algo_cli/intelligence/finance/exceptions.py +157 -0
- algo_cli/intelligence/finance/reconciliations.py +254 -0
- algo_cli/intelligence/finance/revenue.py +109 -0
- algo_cli/intelligence/finance/tax.py +74 -0
- algo_cli/intelligence/finance/workpapers.py +111 -0
- algo_cli/intelligence/finding_record.py +120 -0
- algo_cli/intelligence/flow_dag.py +267 -0
- algo_cli/intelligence/gatherer.py +223 -0
- algo_cli/intelligence/golden_master.py +98 -0
- algo_cli/intelligence/graph_rag.py +195 -0
- algo_cli/intelligence/group_chat.py +143 -0
- algo_cli/intelligence/hash_dedup.py +145 -0
- algo_cli/intelligence/hyperloglog.py +128 -0
- algo_cli/intelligence/incremental_index.py +316 -0
- algo_cli/intelligence/index_store.py +16 -0
- algo_cli/intelligence/iteration_plan.py +133 -0
- algo_cli/intelligence/kernel_plugins.py +167 -0
- algo_cli/intelligence/lesson_catalog.py +135 -0
- algo_cli/intelligence/llm_fallback.py +169 -0
- algo_cli/intelligence/log2_histogram.py +267 -0
- algo_cli/intelligence/lsp_integration.py +147 -0
- algo_cli/intelligence/memory_evolution.py +117 -0
- algo_cli/intelligence/minhash_lsh.py +182 -0
- algo_cli/intelligence/multi_model_score.py +174 -0
- algo_cli/intelligence/multi_tier_grade.py +211 -0
- algo_cli/intelligence/negative_controls.py +113 -0
- algo_cli/intelligence/numeric_clamp.py +63 -0
- algo_cli/intelligence/occ_editor.py +66 -0
- algo_cli/intelligence/output_normalize.py +112 -0
- algo_cli/intelligence/parallel_delegation.py +98 -0
- algo_cli/intelligence/parallel_fanout.py +104 -0
- algo_cli/intelligence/permission_modes.py +105 -0
- algo_cli/intelligence/pre_push_gate.py +68 -0
- algo_cli/intelligence/prefetch.py +171 -0
- algo_cli/intelligence/process_framework.py +217 -0
- algo_cli/intelligence/project_graph.py +387 -0
- algo_cli/intelligence/query_expansion.py +146 -0
- algo_cli/intelligence/ralph_loop.py +117 -0
- algo_cli/intelligence/rate_limiter.py +153 -0
- algo_cli/intelligence/refactor_transaction.py +94 -0
- algo_cli/intelligence/research_workspace.py +108 -0
- algo_cli/intelligence/retraction_ledger.py +72 -0
- algo_cli/intelligence/saga_pattern.py +88 -0
- algo_cli/intelligence/session_fork.py +100 -0
- algo_cli/intelligence/shadow_editor.py +67 -0
- algo_cli/intelligence/shell_session.py +213 -0
- algo_cli/intelligence/source_registry.py +143 -0
- algo_cli/intelligence/spawn_scales.py +99 -0
- algo_cli/intelligence/stat_stability.py +104 -0
- algo_cli/intelligence/structural_validator.py +148 -0
- algo_cli/intelligence/subagent_spawner.py +111 -0
- algo_cli/intelligence/symmetric_verify.py +70 -0
- algo_cli/intelligence/task_classifier.py +129 -0
- algo_cli/intelligence/team_execution.py +122 -0
- algo_cli/intelligence/tiered_access.py +121 -0
- algo_cli/intelligence/utility_registry.py +159 -0
- algo_cli/intuition_engine.py +560 -0
- algo_cli/intuition_injector.py +82 -0
- algo_cli/kernels/__init__.py +5 -0
- algo_cli/kernels/manifest.py +763 -0
- algo_cli/main.py +3903 -0
- algo_cli/memory_candidates.py +541 -0
- algo_cli/memory_echo_veil.py +394 -0
- algo_cli/memory_runtime.py +112 -0
- algo_cli/model_info.py +548 -0
- algo_cli/model_profile.py +160 -0
- algo_cli/model_routing.py +74 -0
- algo_cli/oneshot.py +331 -0
- algo_cli/perf_telemetry.py +389 -0
- algo_cli/plugins.py +245 -0
- algo_cli/private_event_store.py +654 -0
- algo_cli/quantization/__init__.py +24 -0
- algo_cli/quantization/lloyd_max.py +98 -0
- algo_cli/quantization/turbo_quant.py +308 -0
- algo_cli/reasoning/__init__.py +46 -0
- algo_cli/reasoning/combinatorial.py +356 -0
- algo_cli/reasoning/graph_of_thought.py +297 -0
- algo_cli/reasoning/mcts.py +220 -0
- algo_cli/reasoning/neuro_symbolic.py +250 -0
- algo_cli/reasoning/react.py +246 -0
- algo_cli/reasoning/reflexion.py +225 -0
- algo_cli/reasoning/tree_of_thought.py +241 -0
- algo_cli/reasoning_bridge.py +150 -0
- algo_cli/reconciliation.py +284 -0
- algo_cli/reflex.py +385 -0
- algo_cli/resources/docs/ALGO.md +13958 -0
- algo_cli/resources/docs/algo-cli-algorithm-evidence-contract.md +60 -0
- algo_cli/resources/docs/algo-cli-execution-verification-contract.md +59 -0
- algo_cli/resources/docs/algo-cli-memory-lifecycle-contract.md +72 -0
- algo_cli/resources/docs/harness-extension-cleanup-recommendation.md +41 -0
- algo_cli/resources/docs/index-compute-lab-integration.md +32 -0
- algo_cli/resources/docs/inference-harness-loop-blueprint-2026-06.md +55 -0
- algo_cli/resources/docs/main-split-map.md +35 -0
- algo_cli/resources/docs/privacy-and-context.md +48 -0
- algo_cli/resources/docs/reflex-loop-v0.2.md +354 -0
- algo_cli/resources/skills/README.md +26 -0
- algo_cli/resources/skills/algo-cli.md +59 -0
- algo_cli/resources/skills/edit-file-precision.md +49 -0
- algo_cli/resources/skills/harness-search-first.md +47 -0
- algo_cli/resources/skills/memory-recall-ritual.md +51 -0
- algo_cli/resources/skills/qol-algorithms.md +224 -0
- algo_cli/resources/skills/smart-error-recovery.md +56 -0
- algo_cli/resources/skills/tool-selection-cheatsheet.md +65 -0
- algo_cli/retrieval_algorithms.py +127 -0
- algo_cli/runtime_qos.py +236 -0
- algo_cli/runtime_services.py +320 -0
- algo_cli/session_commands.py +95 -0
- algo_cli/session_mode.py +113 -0
- algo_cli/skills.py +430 -0
- algo_cli/slash_dispatch.py +1265 -0
- algo_cli/small_context.py +206 -0
- algo_cli/spawn_budget.py +89 -0
- algo_cli/task_ledger.py +84 -0
- algo_cli/task_router.py +197 -0
- algo_cli/tool_context.py +94 -0
- algo_cli/tool_contract.py +99 -0
- algo_cli/tool_policy.py +357 -0
- algo_cli/tool_runtime.py +647 -0
- algo_cli/tools.py +3056 -0
- algo_cli/url_scheme.py +174 -0
- algo_cli/verify.py +154 -0
- algo_cli/version_manifest.py +178 -0
- algo_cli/vision_screenshot_verify.py +76 -0
- algo_cli/workspace_resolver.py +68 -0
- algo_cli/x_account.py +209 -0
- algo_cli/xai_auth.py +374 -0
- algo_cli/xai_client.py +600 -0
- algo_cli_runtime-0.14.0.dist-info/METADATA +369 -0
- algo_cli_runtime-0.14.0.dist-info/RECORD +237 -0
- algo_cli_runtime-0.14.0.dist-info/WHEEL +4 -0
- algo_cli_runtime-0.14.0.dist-info/entry_points.txt +3 -0
- algo_cli_runtime-0.14.0.dist-info/licenses/LICENSE +21 -0
- ollama_cli/__init__.py +67 -0
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
"""Incremental Index — mtime-watermark based change tracking.
|
|
2
|
+
|
|
3
|
+
Borrowed from Windows Search USN Journal change tracking
|
|
4
|
+
(C:\\ProgramData\\Microsoft\\Search\\Data\\Applications\\Windows\\Windows-usn.db):
|
|
5
|
+
Windows Search uses the NTFS USN (Update Sequence Number) journal to detect
|
|
6
|
+
file changes in O(1) instead of scanning the entire filesystem. The
|
|
7
|
+
ChangeTracking table tracks moves, deletes, and byte-range changes with
|
|
8
|
+
batch-based processing.
|
|
9
|
+
|
|
10
|
+
Since Python cannot access the raw NTFS USN journal without ctypes bindings,
|
|
11
|
+
this module uses file mtime + size as a watermark — the same principle but
|
|
12
|
+
portable across filesystems.
|
|
13
|
+
|
|
14
|
+
Pattern: B27 in ALGO.md.
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import json
|
|
19
|
+
import os
|
|
20
|
+
import time
|
|
21
|
+
from dataclasses import dataclass, field
|
|
22
|
+
from enum import IntFlag
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import Any, Iterable
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ChangeType(IntFlag):
|
|
28
|
+
NONE = 0
|
|
29
|
+
CREATED = 1
|
|
30
|
+
MODIFIED = 2
|
|
31
|
+
DELETED = 4
|
|
32
|
+
MOVED = 8
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class FileWatermark:
|
|
37
|
+
"""Per-file watermark — mtime + size + inode."""
|
|
38
|
+
path: str
|
|
39
|
+
mtime: float
|
|
40
|
+
size: int
|
|
41
|
+
inode: int = 0 # for move detection on platforms that support it
|
|
42
|
+
|
|
43
|
+
def matches(self, stat: os.stat_result) -> bool:
|
|
44
|
+
return (self.mtime == stat.st_mtime
|
|
45
|
+
and self.size == stat.st_size)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass
|
|
49
|
+
class ChangeRecord:
|
|
50
|
+
"""A single detected change."""
|
|
51
|
+
path: str
|
|
52
|
+
change_type: ChangeType
|
|
53
|
+
old_path: str | None = None # for moves
|
|
54
|
+
watermark: FileWatermark | None = None
|
|
55
|
+
timestamp: float = field(default_factory=time.time)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass
|
|
59
|
+
class IncrementalIndex:
|
|
60
|
+
"""Mtime-watermark based incremental change tracker.
|
|
61
|
+
|
|
62
|
+
Usage:
|
|
63
|
+
tracker = IncrementalIndex.load(watermark_file)
|
|
64
|
+
changes = tracker.scan(directory, glob="*.py")
|
|
65
|
+
for change in changes:
|
|
66
|
+
if change.change_type & ChangeType.MODIFIED:
|
|
67
|
+
re_embed(change.path)
|
|
68
|
+
tracker.save(watermark_file)
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
watermarks: dict[str, FileWatermark] = field(default_factory=dict)
|
|
72
|
+
last_scan: float = 0.0
|
|
73
|
+
scan_count: int = 0
|
|
74
|
+
|
|
75
|
+
# --- scan -------------------------------------------------------------
|
|
76
|
+
|
|
77
|
+
def scan_paths(self, paths: Iterable[Path]) -> list[ChangeRecord]:
|
|
78
|
+
"""Scan an explicit iterable of files and return changes.
|
|
79
|
+
|
|
80
|
+
This is the safer companion to ``scan(directory)`` for callers that
|
|
81
|
+
already have a filtered source list (for example, harness roots with
|
|
82
|
+
include/exclude patterns). It preserves the same watermark semantics
|
|
83
|
+
while avoiding a broad recursive walk.
|
|
84
|
+
"""
|
|
85
|
+
changes: list[ChangeRecord] = []
|
|
86
|
+
current_files: dict[str, FileWatermark] = {}
|
|
87
|
+
current_inodes: set[int] = set()
|
|
88
|
+
|
|
89
|
+
for filepath in paths:
|
|
90
|
+
if not filepath.is_file():
|
|
91
|
+
continue
|
|
92
|
+
path_str = str(filepath)
|
|
93
|
+
try:
|
|
94
|
+
stat = filepath.stat()
|
|
95
|
+
except OSError:
|
|
96
|
+
continue
|
|
97
|
+
|
|
98
|
+
wm = FileWatermark(
|
|
99
|
+
path=path_str,
|
|
100
|
+
mtime=stat.st_mtime,
|
|
101
|
+
size=stat.st_size,
|
|
102
|
+
inode=stat.st_ino,
|
|
103
|
+
)
|
|
104
|
+
current_files[path_str] = wm
|
|
105
|
+
if stat.st_ino:
|
|
106
|
+
current_inodes.add(stat.st_ino)
|
|
107
|
+
|
|
108
|
+
old_wm = self.watermarks.get(path_str)
|
|
109
|
+
if old_wm is None:
|
|
110
|
+
moved_from = self._find_by_inode(stat.st_ino, path_str)
|
|
111
|
+
if moved_from:
|
|
112
|
+
changes.append(ChangeRecord(
|
|
113
|
+
path=path_str,
|
|
114
|
+
change_type=ChangeType.MOVED,
|
|
115
|
+
old_path=moved_from,
|
|
116
|
+
watermark=wm,
|
|
117
|
+
))
|
|
118
|
+
else:
|
|
119
|
+
changes.append(ChangeRecord(
|
|
120
|
+
path=path_str,
|
|
121
|
+
change_type=ChangeType.CREATED,
|
|
122
|
+
watermark=wm,
|
|
123
|
+
))
|
|
124
|
+
elif not old_wm.matches(stat):
|
|
125
|
+
changes.append(ChangeRecord(
|
|
126
|
+
path=path_str,
|
|
127
|
+
change_type=ChangeType.MODIFIED,
|
|
128
|
+
watermark=wm,
|
|
129
|
+
))
|
|
130
|
+
|
|
131
|
+
for old_path, old_wm in self.watermarks.items():
|
|
132
|
+
if old_path not in current_files:
|
|
133
|
+
if old_wm.inode and old_wm.inode in current_inodes:
|
|
134
|
+
continue
|
|
135
|
+
changes.append(ChangeRecord(
|
|
136
|
+
path=old_path,
|
|
137
|
+
change_type=ChangeType.DELETED,
|
|
138
|
+
))
|
|
139
|
+
|
|
140
|
+
self.watermarks = current_files
|
|
141
|
+
self.last_scan = time.time()
|
|
142
|
+
self.scan_count += 1
|
|
143
|
+
return changes
|
|
144
|
+
|
|
145
|
+
def scan(
|
|
146
|
+
self,
|
|
147
|
+
directory: Path,
|
|
148
|
+
*,
|
|
149
|
+
glob: str = "**/*",
|
|
150
|
+
exclude_dirs: set[str] | None = None,
|
|
151
|
+
) -> list[ChangeRecord]:
|
|
152
|
+
"""Scan directory and return changes since last scan.
|
|
153
|
+
|
|
154
|
+
Detects: created, modified, deleted, moved.
|
|
155
|
+
"""
|
|
156
|
+
if exclude_dirs is None:
|
|
157
|
+
exclude_dirs = {".git", "__pycache__", ".venv", "node_modules",
|
|
158
|
+
".mypy_cache", ".pytest_cache", ".ruff_cache"}
|
|
159
|
+
|
|
160
|
+
changes: list[ChangeRecord] = []
|
|
161
|
+
current_files: dict[str, FileWatermark] = {}
|
|
162
|
+
|
|
163
|
+
# Walk the directory
|
|
164
|
+
for filepath in directory.glob(glob):
|
|
165
|
+
if not filepath.is_file():
|
|
166
|
+
continue
|
|
167
|
+
# Skip excluded directories
|
|
168
|
+
rel_parts = filepath.relative_to(directory).parts
|
|
169
|
+
if any(part in exclude_dirs for part in rel_parts):
|
|
170
|
+
continue
|
|
171
|
+
|
|
172
|
+
path_str = str(filepath)
|
|
173
|
+
try:
|
|
174
|
+
stat = filepath.stat()
|
|
175
|
+
except OSError:
|
|
176
|
+
continue
|
|
177
|
+
|
|
178
|
+
wm = FileWatermark(
|
|
179
|
+
path=path_str,
|
|
180
|
+
mtime=stat.st_mtime,
|
|
181
|
+
size=stat.st_size,
|
|
182
|
+
inode=stat.st_ino,
|
|
183
|
+
)
|
|
184
|
+
current_files[path_str] = wm
|
|
185
|
+
|
|
186
|
+
old_wm = self.watermarks.get(path_str)
|
|
187
|
+
if old_wm is None:
|
|
188
|
+
# Check if this file was moved (same inode, different path)
|
|
189
|
+
moved_from = self._find_by_inode(stat.st_ino, path_str)
|
|
190
|
+
if moved_from:
|
|
191
|
+
changes.append(ChangeRecord(
|
|
192
|
+
path=path_str,
|
|
193
|
+
change_type=ChangeType.MOVED,
|
|
194
|
+
old_path=moved_from,
|
|
195
|
+
watermark=wm,
|
|
196
|
+
))
|
|
197
|
+
else:
|
|
198
|
+
changes.append(ChangeRecord(
|
|
199
|
+
path=path_str,
|
|
200
|
+
change_type=ChangeType.CREATED,
|
|
201
|
+
watermark=wm,
|
|
202
|
+
))
|
|
203
|
+
elif not old_wm.matches(stat):
|
|
204
|
+
changes.append(ChangeRecord(
|
|
205
|
+
path=path_str,
|
|
206
|
+
change_type=ChangeType.MODIFIED,
|
|
207
|
+
watermark=wm,
|
|
208
|
+
))
|
|
209
|
+
|
|
210
|
+
# Detect deletions (files in watermarks but not in current_files)
|
|
211
|
+
for old_path, old_wm in self.watermarks.items():
|
|
212
|
+
if old_path not in current_files:
|
|
213
|
+
# Check if it was moved (inode exists under new path)
|
|
214
|
+
if not self._inode_exists_elsewhere(old_wm.inode, old_path):
|
|
215
|
+
changes.append(ChangeRecord(
|
|
216
|
+
path=old_path,
|
|
217
|
+
change_type=ChangeType.DELETED,
|
|
218
|
+
))
|
|
219
|
+
|
|
220
|
+
# Update watermarks
|
|
221
|
+
self.watermarks = current_files
|
|
222
|
+
self.last_scan = time.time()
|
|
223
|
+
self.scan_count += 1
|
|
224
|
+
|
|
225
|
+
return changes
|
|
226
|
+
|
|
227
|
+
def _find_by_inode(self, inode: int, exclude_path: str) -> str | None:
|
|
228
|
+
"""Find a watermark with the same inode (for move detection)."""
|
|
229
|
+
if inode == 0:
|
|
230
|
+
return None
|
|
231
|
+
for path, wm in self.watermarks.items():
|
|
232
|
+
if path != exclude_path and wm.inode == inode:
|
|
233
|
+
return path
|
|
234
|
+
return None
|
|
235
|
+
|
|
236
|
+
def _inode_exists_elsewhere(self, inode: int, exclude_path: str) -> bool:
|
|
237
|
+
"""Check if inode exists in current scan under a different path."""
|
|
238
|
+
return self._find_by_inode(inode, exclude_path) is not None
|
|
239
|
+
|
|
240
|
+
# --- query ------------------------------------------------------------
|
|
241
|
+
|
|
242
|
+
def needs_update(self, path: str) -> bool:
|
|
243
|
+
"""Check if a specific file has changed since last scan."""
|
|
244
|
+
filepath = Path(path)
|
|
245
|
+
if not filepath.exists():
|
|
246
|
+
return path in self.watermarks # was deleted
|
|
247
|
+
try:
|
|
248
|
+
stat = filepath.stat()
|
|
249
|
+
except OSError:
|
|
250
|
+
return False
|
|
251
|
+
wm = self.watermarks.get(path)
|
|
252
|
+
if wm is None:
|
|
253
|
+
return True # new file
|
|
254
|
+
return not wm.matches(stat)
|
|
255
|
+
|
|
256
|
+
def get_changed_files(self, directory: Path, **kwargs: Any) -> list[str]:
|
|
257
|
+
"""Return only the paths of changed files (convenience)."""
|
|
258
|
+
changes = self.scan(directory, **kwargs)
|
|
259
|
+
return [c.path for c in changes if c.change_type != ChangeType.NONE]
|
|
260
|
+
|
|
261
|
+
# --- serialization ----------------------------------------------------
|
|
262
|
+
|
|
263
|
+
def to_dict(self) -> dict[str, Any]:
|
|
264
|
+
return {
|
|
265
|
+
"watermarks": {
|
|
266
|
+
path: {"mtime": wm.mtime, "size": wm.size, "inode": wm.inode}
|
|
267
|
+
for path, wm in self.watermarks.items()
|
|
268
|
+
},
|
|
269
|
+
"last_scan": self.last_scan,
|
|
270
|
+
"scan_count": self.scan_count,
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
@classmethod
|
|
274
|
+
def from_dict(cls, data: dict[str, Any]) -> IncrementalIndex:
|
|
275
|
+
tracker = cls()
|
|
276
|
+
for path, wm_data in data.get("watermarks", {}).items():
|
|
277
|
+
tracker.watermarks[path] = FileWatermark(
|
|
278
|
+
path=path,
|
|
279
|
+
mtime=wm_data["mtime"],
|
|
280
|
+
size=wm_data["size"],
|
|
281
|
+
inode=wm_data.get("inode", 0),
|
|
282
|
+
)
|
|
283
|
+
tracker.last_scan = data.get("last_scan", 0.0)
|
|
284
|
+
tracker.scan_count = data.get("scan_count", 0)
|
|
285
|
+
return tracker
|
|
286
|
+
|
|
287
|
+
def save(self, path: Path) -> None:
|
|
288
|
+
"""Save watermarks to a JSON file."""
|
|
289
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
290
|
+
path.write_text(
|
|
291
|
+
json.dumps(self.to_dict(), indent=2),
|
|
292
|
+
encoding="utf-8",
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
@classmethod
|
|
296
|
+
def load(cls, path: Path) -> IncrementalIndex:
|
|
297
|
+
"""Load watermarks from a JSON file.
|
|
298
|
+
|
|
299
|
+
Returns empty tracker if file doesn't exist (first scan).
|
|
300
|
+
"""
|
|
301
|
+
if not path.exists():
|
|
302
|
+
return cls()
|
|
303
|
+
try:
|
|
304
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
305
|
+
except (json.JSONDecodeError, OSError):
|
|
306
|
+
return cls()
|
|
307
|
+
return cls.from_dict(data)
|
|
308
|
+
|
|
309
|
+
# --- stats ------------------------------------------------------------
|
|
310
|
+
|
|
311
|
+
def stats(self) -> dict[str, Any]:
|
|
312
|
+
return {
|
|
313
|
+
"tracked_files": len(self.watermarks),
|
|
314
|
+
"last_scan": self.last_scan,
|
|
315
|
+
"scan_count": self.scan_count,
|
|
316
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Small persistence helpers for project intelligence indexes."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from .project_graph import ProjectGraph, build_project_graph, load_project_graph
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def ensure_project_graph(root: Path | str, *, rebuild: bool = False) -> ProjectGraph:
|
|
11
|
+
root_path = Path(root).resolve()
|
|
12
|
+
if not rebuild:
|
|
13
|
+
graph = load_project_graph(root_path)
|
|
14
|
+
if graph is not None:
|
|
15
|
+
return graph
|
|
16
|
+
return build_project_graph(root_path, persist=True)
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""B39. Iteration Plan + Endgame Checklist Cadence (VS Code Project Pattern).
|
|
2
|
+
|
|
3
|
+
Manages monthly iteration plans and release endgame checklists so
|
|
4
|
+
development stays disciplined as features accumulate.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
from enum import Enum
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class CheckState(Enum):
|
|
16
|
+
UNCHECKED = "unchecked"
|
|
17
|
+
PASSED = "passed"
|
|
18
|
+
FAILED = "failed"
|
|
19
|
+
BLOCKED = "blocked"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class ChecklistItem:
|
|
24
|
+
name: str
|
|
25
|
+
description: str = ""
|
|
26
|
+
state: CheckState = CheckState.UNCHECKED
|
|
27
|
+
blocker: bool = False # if True, failed state blocks release
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class IterationPlan:
|
|
32
|
+
month: str # "YYYY-MM"
|
|
33
|
+
goals: list[str] = field(default_factory=list)
|
|
34
|
+
work_items: list[dict[str, str]] = field(default_factory=list) # {label, kind, status}
|
|
35
|
+
created_at: str = field(default_factory=lambda: datetime.now().isoformat())
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class EndgameChecklist:
|
|
40
|
+
release_version: str
|
|
41
|
+
items: list[ChecklistItem] = field(default_factory=list)
|
|
42
|
+
created_at: str = field(default_factory=lambda: datetime.now().isoformat())
|
|
43
|
+
|
|
44
|
+
def add_item(self, name: str, description: str = "", blocker: bool = False) -> None:
|
|
45
|
+
self.items.append(ChecklistItem(name=name, description=description, blocker=blocker))
|
|
46
|
+
|
|
47
|
+
def mark(self, name: str, state: CheckState) -> bool:
|
|
48
|
+
for item in self.items:
|
|
49
|
+
if item.name == name:
|
|
50
|
+
item.state = state
|
|
51
|
+
return True
|
|
52
|
+
return False
|
|
53
|
+
|
|
54
|
+
def release_ready(self) -> bool:
|
|
55
|
+
"""True if no blocker item is failed or unchecked."""
|
|
56
|
+
for item in self.items:
|
|
57
|
+
if item.blocker and item.state != CheckState.PASSED:
|
|
58
|
+
return False
|
|
59
|
+
return True
|
|
60
|
+
|
|
61
|
+
def blockers(self) -> list[ChecklistItem]:
|
|
62
|
+
"""Return unresolved blocker items."""
|
|
63
|
+
return [i for i in self.items if i.blocker and i.state != CheckState.PASSED]
|
|
64
|
+
|
|
65
|
+
def summary(self) -> dict[str, Any]:
|
|
66
|
+
total = len(self.items)
|
|
67
|
+
passed = sum(1 for i in self.items if i.state == CheckState.PASSED)
|
|
68
|
+
failed = sum(1 for i in self.items if i.state == CheckState.FAILED)
|
|
69
|
+
unchecked = sum(1 for i in self.items if i.state == CheckState.UNCHECKED)
|
|
70
|
+
blocked = sum(1 for i in self.items if i.state == CheckState.BLOCKED)
|
|
71
|
+
return {
|
|
72
|
+
"release": self.release_version,
|
|
73
|
+
"total": total,
|
|
74
|
+
"passed": passed,
|
|
75
|
+
"failed": failed,
|
|
76
|
+
"unchecked": unchecked,
|
|
77
|
+
"blocked": blocked,
|
|
78
|
+
"release_ready": self.release_ready(),
|
|
79
|
+
"blockers": [b.name for b in self.blockers()],
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def default_endgame_checklist(version: str) -> EndgameChecklist:
|
|
84
|
+
"""Create a standard release endgame checklist."""
|
|
85
|
+
cl = EndgameChecklist(release_version=version)
|
|
86
|
+
cl.add_item("tests_pass", "All tests pass (pytest -q)", blocker=True)
|
|
87
|
+
cl.add_item("no_skip_markers", "No new pytest.skip markers without justification", blocker=False)
|
|
88
|
+
cl.add_item("help_updated", "/help text updated for new commands", blocker=True)
|
|
89
|
+
cl.add_item("docs_updated", "ALGO.md and README updated", blocker=False)
|
|
90
|
+
cl.add_item("migration_notes", "Migration notes for breaking changes", blocker=True)
|
|
91
|
+
cl.add_item("safety_gates", "Safety gates verified (external send, delete, payments)", blocker=True)
|
|
92
|
+
cl.add_item("rollback_plan", "Rollback plan documented", blocker=False)
|
|
93
|
+
cl.add_item("changelog", "Changelog/changefiles written", blocker=False)
|
|
94
|
+
return cl
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class IterationPlanManager:
|
|
98
|
+
"""Manages iteration plans and endgame checklists."""
|
|
99
|
+
|
|
100
|
+
def __init__(self) -> None:
|
|
101
|
+
self.plans: dict[str, IterationPlan] = {}
|
|
102
|
+
self.checklists: dict[str, EndgameChecklist] = {}
|
|
103
|
+
|
|
104
|
+
def create_plan(self, month: str, goals: list[str] | None = None) -> IterationPlan:
|
|
105
|
+
plan = IterationPlan(month=month, goals=goals or [])
|
|
106
|
+
self.plans[month] = plan
|
|
107
|
+
return plan
|
|
108
|
+
|
|
109
|
+
def add_work_item(self, month: str, label: str, kind: str, status: str = "open") -> bool:
|
|
110
|
+
plan = self.plans.get(month)
|
|
111
|
+
if not plan:
|
|
112
|
+
return False
|
|
113
|
+
plan.work_items.append({"label": label, "kind": kind, "status": status})
|
|
114
|
+
return True
|
|
115
|
+
|
|
116
|
+
def create_checklist(self, version: str) -> EndgameChecklist:
|
|
117
|
+
cl = default_endgame_checklist(version)
|
|
118
|
+
self.checklists[version] = cl
|
|
119
|
+
return cl
|
|
120
|
+
|
|
121
|
+
def current_plan(self) -> IterationPlan | None:
|
|
122
|
+
if not self.plans:
|
|
123
|
+
return None
|
|
124
|
+
return self.plans[max(self.plans.keys())]
|
|
125
|
+
|
|
126
|
+
def roadmap_summary(self) -> dict[str, Any]:
|
|
127
|
+
current = self.current_plan()
|
|
128
|
+
return {
|
|
129
|
+
"current_month": current.month if current else None,
|
|
130
|
+
"goals": current.goals if current else [],
|
|
131
|
+
"open_items": sum(1 for w in (current.work_items if current else []) if w.get("status") == "open"),
|
|
132
|
+
"checklists": {v: cl.summary() for v, cl in self.checklists.items()},
|
|
133
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""B45. Kernel Plugin Architecture with Typed Annotations (Semantic Kernel Pattern).
|
|
2
|
+
|
|
3
|
+
Tools are plain Python methods decorated with @kernel_function and typed
|
|
4
|
+
with Annotated[T, "description"]. The kernel introspects the function
|
|
5
|
+
signature and auto-generates the tool schema — no manual JSON schema
|
|
6
|
+
authoring. Structured outputs use Pydantic BaseModel with response_format.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import inspect
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from typing import Any, Callable, get_type_hints
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class ToolParam:
|
|
18
|
+
name: str
|
|
19
|
+
type: str
|
|
20
|
+
description: str
|
|
21
|
+
required: bool
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class ToolSchema:
|
|
26
|
+
name: str
|
|
27
|
+
description: str
|
|
28
|
+
params: list[ToolParam] = field(default_factory=list)
|
|
29
|
+
return_type: str = "string"
|
|
30
|
+
return_description: str = ""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def kernel_function(description: str = "") -> Callable:
|
|
34
|
+
"""Decorator that marks a method as a kernel-callable tool."""
|
|
35
|
+
def decorator(fn: Callable) -> Callable:
|
|
36
|
+
fn._kernel_description = description or fn.__doc__ or ""
|
|
37
|
+
fn._kernel_function = True
|
|
38
|
+
return fn
|
|
39
|
+
return decorator
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def introspect_tool(fn: Callable) -> ToolSchema:
|
|
43
|
+
"""Auto-generate tool schema from function signature + Annotated hints."""
|
|
44
|
+
sig = inspect.signature(fn)
|
|
45
|
+
try:
|
|
46
|
+
hints = get_type_hints(fn, include_extras=True)
|
|
47
|
+
except Exception:
|
|
48
|
+
hints = {}
|
|
49
|
+
|
|
50
|
+
params: list[ToolParam] = []
|
|
51
|
+
for name, param in sig.parameters.items():
|
|
52
|
+
if name == "self":
|
|
53
|
+
continue
|
|
54
|
+
hint = hints.get(name, str)
|
|
55
|
+
desc = ""
|
|
56
|
+
type_name = "string"
|
|
57
|
+
|
|
58
|
+
# Extract Annotated[T, "description"]
|
|
59
|
+
if hasattr(hint, "__metadata__"):
|
|
60
|
+
origin = getattr(hint, "__origin__", str)
|
|
61
|
+
type_name = getattr(origin, "__name__", "string")
|
|
62
|
+
if hint.__metadata__:
|
|
63
|
+
desc = str(hint.__metadata__[0])
|
|
64
|
+
else:
|
|
65
|
+
type_name = getattr(hint, "__name__", "string")
|
|
66
|
+
|
|
67
|
+
params.append(ToolParam(
|
|
68
|
+
name=name,
|
|
69
|
+
type=type_name,
|
|
70
|
+
description=desc,
|
|
71
|
+
required=param.default is inspect.Parameter.empty,
|
|
72
|
+
))
|
|
73
|
+
|
|
74
|
+
# Return type
|
|
75
|
+
ret_hint = hints.get("return", type(None))
|
|
76
|
+
ret_desc = ""
|
|
77
|
+
if hasattr(ret_hint, "__metadata__"):
|
|
78
|
+
ret_desc = str(ret_hint.__metadata__[0]) if ret_hint.__metadata__ else ""
|
|
79
|
+
ret_origin = getattr(ret_hint, "__origin__", ret_hint)
|
|
80
|
+
ret_type = getattr(ret_origin, "__name__", "void")
|
|
81
|
+
|
|
82
|
+
return ToolSchema(
|
|
83
|
+
name=fn.__name__,
|
|
84
|
+
description=getattr(fn, "_kernel_description", fn.__doc__ or ""),
|
|
85
|
+
params=params,
|
|
86
|
+
return_type=ret_type,
|
|
87
|
+
return_description=ret_desc,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class Kernel:
|
|
92
|
+
"""Central kernel that manages plugins and invokes tools."""
|
|
93
|
+
|
|
94
|
+
def __init__(self) -> None:
|
|
95
|
+
self._plugins: dict[str, Any] = {}
|
|
96
|
+
self._schemas: dict[str, ToolSchema] = {}
|
|
97
|
+
self._handlers: dict[str, Callable] = {}
|
|
98
|
+
|
|
99
|
+
def add_plugin(self, plugin: Any, name: str | None = None) -> None:
|
|
100
|
+
"""Register a plugin class — all @kernel_function methods become tools."""
|
|
101
|
+
plugin_name = name or plugin.__class__.__name__
|
|
102
|
+
self._plugins[plugin_name] = plugin
|
|
103
|
+
for attr_name in dir(plugin):
|
|
104
|
+
attr = getattr(plugin, attr_name, None)
|
|
105
|
+
if callable(attr) and getattr(attr, "_kernel_function", False):
|
|
106
|
+
schema = introspect_tool(attr)
|
|
107
|
+
self._schemas[schema.name] = schema
|
|
108
|
+
self._handlers[schema.name] = attr
|
|
109
|
+
|
|
110
|
+
def list_tools(self) -> list[ToolSchema]:
|
|
111
|
+
return list(self._schemas.values())
|
|
112
|
+
|
|
113
|
+
def get_tool_schema(self, name: str) -> ToolSchema | None:
|
|
114
|
+
return self._schemas.get(name)
|
|
115
|
+
|
|
116
|
+
def invoke(self, tool_name: str, **kwargs: Any) -> str:
|
|
117
|
+
"""Invoke a registered tool by name."""
|
|
118
|
+
handler = self._handlers.get(tool_name)
|
|
119
|
+
if not handler:
|
|
120
|
+
return f"Tool '{tool_name}' not found"
|
|
121
|
+
try:
|
|
122
|
+
result = handler(**kwargs)
|
|
123
|
+
return str(result)
|
|
124
|
+
except Exception as e:
|
|
125
|
+
return f"Tool '{tool_name}' error: {e}"
|
|
126
|
+
|
|
127
|
+
def tool_descriptions_for_prompt(self) -> str:
|
|
128
|
+
"""Generate tool description text for inclusion in system prompt."""
|
|
129
|
+
lines: list[str] = []
|
|
130
|
+
for schema in self._schemas.values():
|
|
131
|
+
param_str = ", ".join(
|
|
132
|
+
f"{p.name}: {p.type}" + (f" — {p.description}" if p.description else "")
|
|
133
|
+
for p in schema.params
|
|
134
|
+
)
|
|
135
|
+
lines.append(f"- {schema.name}({param_str}): {schema.description}")
|
|
136
|
+
return "\n".join(lines)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
# ── structured output support ─────────────────────────────────────────
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def validate_structured_output(data: dict, model_class: type) -> dict:
|
|
143
|
+
"""Validate LLM output against a Pydantic-like BaseModel.
|
|
144
|
+
|
|
145
|
+
Works with any class that has __annotations__ (dataclass or Pydantic).
|
|
146
|
+
"""
|
|
147
|
+
try:
|
|
148
|
+
from pydantic import BaseModel
|
|
149
|
+
if issubclass(model_class, BaseModel):
|
|
150
|
+
obj = model_class(**data)
|
|
151
|
+
return {"valid": True, "data": obj.model_dump()}
|
|
152
|
+
except ImportError:
|
|
153
|
+
pass
|
|
154
|
+
except Exception as e:
|
|
155
|
+
return {"valid": False, "error": str(e)}
|
|
156
|
+
|
|
157
|
+
# Fallback: simple annotation-based validation
|
|
158
|
+
annotations = getattr(model_class, "__annotations__", {})
|
|
159
|
+
errors: list[str] = []
|
|
160
|
+
for field_name, field_type in annotations.items():
|
|
161
|
+
if field_name not in data:
|
|
162
|
+
errors.append(f"missing field: {field_name}")
|
|
163
|
+
elif not isinstance(data[field_name], field_type):
|
|
164
|
+
errors.append(f"{field_name}: expected {field_type.__name__}, got {type(data[field_name]).__name__}")
|
|
165
|
+
if errors:
|
|
166
|
+
return {"valid": False, "error": "; ".join(errors)}
|
|
167
|
+
return {"valid": True, "data": data}
|