Skip to content

Latest commit

 

History

History
483 lines (387 loc) · 25.9 KB

CHANGELOG.md

File metadata and controls

483 lines (387 loc) · 25.9 KB

Changelog

All notable changes to this project are documented in this file.

Format follows Keep a Changelog.


[UNRELEASED]

[1.2.0] - 2026-09-17

Added

Bounded multi-agent tool-call dispatch. orchestrator.dispatch_agent exposes a single dispatch_agent(task, domain_hint) OpenAI-style tool to the architecture (Phase A) and implementation (Phase B) calls, capability-gated per candidate via models.yaml's new supports_tools/last_verified fields (config.RoleCandidate) -- a model resolved to a candidate without supports_tools: true never receives the tool, and behaves exactly as before. When called, the tool resolves a real specialist agent from claude-global-library via kgf.select.Selector (through engine/knowledge.py's existing resolve()), builds its budgeted persona, and makes one nested call_role() consultation -- capped at nesting_depth 1 (a dispatched specialist never receives the tool itself), a per-root-task visited-agent set (no re-dispatching the same specialist), and a hard dispatch_budget_tokens ceiling (models.yaml's new dispatch: section, config.DispatchConfig). Failure modes (NO_MATCH, dispatch timeout, malformed tool-call JSON, a failed nested call) all degrade to an error-shaped tool result rather than raising, so a weaker open model's tool-call fumbles never crash the parent call. engine/calling.py's call_role() gained the generic tools/tool_handlers round-trip loop this and any future tool sits on top of; engine/agent_runtime.py's spawned-process dispatcher forwards tool schemas across the coordinator boundary so Phase A's own spawned-OS-process architecture call gets the same capability without exposing live Router/LLMClient objects to the child process. Design reviewed by solution-architect and gated APPROVED by consensus-agent (both personas from claude-global-library) before implementation began.

[1.1.0] - 2026-09-17

Added

Opt-in end-to-end SDLC lifecycle chaining (Gap A). sdlc.run_full_lifecycle composes generate_srs -> generate_hld -> run_task (self-heal) -> create_sprint_plan -> push_and_create_pr in one call, each stage gated by its own do_srs/do_hld/do_sprint/ do_push_pr flag defaulting to False -- calling it with no flags is exactly today's run_task-only behavior. Exposed as the new vishwakarma sdlc full TASK command; the existing run, sdlc srs, sdlc hld, sdlc jira, and push/PR commands are unchanged. There is deliberately no standalone commit-only flag, since push_and_create_pr already composes commit+push+PR internally. jira_sprint.link_pr_to_epic closes the loop by attaching the created PR's URL back to the Jira Epic once both a sprint and a PR exist in the same run, following jira_link_to_epic's existing non-fatal contract (a link failure is logged, never raised).

Opt-in adaptive routing (Gap B). router.pick_first_candidate maps classify_complexity's existing "simple"/"complex" signal to a starting candidate index for primary_coder/reasoner -- complex tasks start at index 0 (the role's strongest candidate), simple tasks start one position in -- falling back to a single capped router_fast call only when the signal is genuinely outside that two-value enum. Router.seed_active_index applies the chosen index directly, distinct from handle_unavailable and restore_active_index, and is bounds-checked but does not itself re-verify reachability (a bad seed is corrected by the very next call_role failure, exactly like any other candidate fault). The whole mechanism is wired in engine/orchestrator.py::run_task, right after the real complexity signal exists, and is a complete no-op unless VISHWAKARMA_ADAPTIVE_ROUTING is set -- default OFF, preserving ADR-0006 and ADR-0010 exactly as before.

A config-gated openai candidate on primary_coder (last position). models.yaml role candidates now support an optional enable_env: key; config.py excludes a gated candidate from the role's list entirely unless that named environment variable is truthy, so a dormant provider can be tried without a code change. openai/gpt-4o-mini is wired behind VISHWAKARMA_ENABLE_OPENAI, defaulting OFF. Not live-tested from this account -- OPENAI_API_KEY is absent from .env here, and the same is true of ANTHROPIC_API_KEY and XKIRO_API_KEY for anthropic-compatible and xkiro, so none of the three genuinely dormant providers could be verified this round; wire either the same way once its key lands.


[1.0.8] - 2026-09-14

Added

The architecture decision records the code has been citing all along — closes #51. Source comments justify their design with "per ADR-2", "ADR-4's no-cache decision", "ADR-7's replay promise". Measured: 35 citations across seven ADRs, pointing at documents that existed in no file a recipient could open.

docs/adr/ now holds twelve: the nine original decisions, plus three this codebase has come to depend on just as heavily — the failover taxonomy (0010), the best-attempt ratchet (0011), and the rule that absent evidence is not success (0012).

Each states the accepted cost, not only the choice. That is the half worth reading: several were decided against a reviewer's recommendation, and most record a claim that measurement falsified — the 35s load that was really 76ms, the model: field that marks math masters rather than capability tiers, the SSRF defences urllib cannot deliver as first specified.

Fixed

A bare pytest no longer reports on the tool's own output. From the repo root it collected 740 tests where pytest tests/ collects 700, sweeping in vishwakarma-output/ — so the most obvious command ran a suite whose contents depended on whatever was last generated, and whose pass rate reflected a model's output rather than this codebase. testpaths is pinned.

[1.0.7] - 2026-09-14

Fixed

A stalled provider is now detected in 30s, not 120 — part of #56. The timeout is httpx's inter-chunk read timeout, so 120s meant a provider emitting one token every 119 seconds read as perfectly healthy. Since the router advances only on exceptions and a slow stream raises none, such a call had no timeout, no error and no failover. 30s is still an order of magnitude above any observed inter-chunk gap on a working provider.

Recorded while fixing it: this does bound time-to-first-byte. Measured against a socket that accepts and then stays silent, httpx.ReadTimeout fires at 4.6s against a 4s configured read timeout — so a server that accepts and sends nothing is killed by the transport, and only the "alive but crawling" case needs the throughput bound still to come.

A dead provider is probed once, not once per role — closes #57. validate_startup memoised catalogue successes only; every failure arm returned without recording anything, so an unreachable or mis-keyed provider was re-probed for each role naming it. Measured at 13.8s per probe, so four roles spent ~55s of startup learning the same fact four times.

Two measured wins came with it: the local base_urls use 127.0.0.1 rather than localhost (which resolves to both ::1 and 127.0.0.1, doubling every connect attempt), and the startup catalogue probe no longer uses the SDK's default retries — it asks "is the provider there", and the answer does not change on retry. Together 13.8s → 2.0s.

Zero cost today, because only Groq is wired and reachable. It becomes live the moment a second provider joins more than one role's candidate list.

strip_reasoning_trace takes the last closing tag, as its docstring always said — closes #58. The implementation used .search(), which finds the first, leaving an entire second trace inside what it returned as "the answer". Fixed in the direction the contract already specified rather than by rewriting the contract to match the code.

It matters because this now guards both task classifiers, and both parse by substring: a surviving trace containing the word "complex" makes classify_complexity return "complex" regardless of the model's conclusion — the same inversion #53 removed, reachable again through a repeated trace.

Accepted cost, checked rather than assumed and pinned by a test: an answer legitimately containing the literal </think> is truncated there. generate.py does not import this function, so the coder's JSON path is untouched.

[1.0.6] - 2026-09-14

Fixed

Two regressions in the fixes that shipped hours earlier — closes #66. Both have the same shape: a value meaning "cannot tell" was consumed as if it meant "fine", in both cases against an adapter docstring that said explicitly it must not be.

#59 came back through a second door. executed_test_count returns None when the runner printed no count at all, and the guard tested executed == 0 — which None fails — so control fell through to passed = returncode == 0. Reachable exactly as the original was: a *Test.java outside src/test/java satisfies the file-presence check, Maven never compiles it, surefire prints no Tests run: line, and the tool reported "All tests passed" over zero executed tests. The guard now covers both zero and absent, and says which it saw, because they need different remedies.

#64's ratchet switched itself off. One attempt whose output carried no countable signal — a dependency failure, a broken pom.xml — set best_count = None, which discarded the known-good champion and made every later iteration take the same branch, disabling rollback for the rest of the loop and shipping the unmeasurable state as "best". An unscoreable attempt is now skipped while the champion is kept, and surfaces as heal_attempt_unscored.

The bootstrap half is the subtle one and is pinned by its own test: the branch keys on best_count is None, not attempt_count is None, so a run whose starting state was unscoreable still lets the first scoreable attempt establish the baseline. Keyed the other way, the champion would never initialise.

Released alone, with no other change in the same commit range, so git bisect on "did the tool ever lie about test results" stays a one-commit answer.

[1.0.5] - 2026-09-14

Fixed

Self-heal no longer ends worse than a state it already reached — closes #64. The loop had a bound and a success condition and nothing in between: an attempt that increased the error count was carried forward exactly like one that reduced it, and the next diagnosis started from the worse state.

Each fix is locally reasonable and the sequence need not be — an attempt can repair one file while breaking another it changed two attempts ago. Measured live, compile errors went 3 → 1 → 5, the last five caused by a service being rewritten against a constructor an earlier attempt had itself changed.

Language adapters now report a failure_count, and the loop keeps the best state rather than the last, rewriting it to disk so the next attempt diagnoses what was actually kept. Java counts distinct compile-error sites — Maven echoes every error in its own summary, so counting lines would double it — then falls through to surefire's failure totals once the project compiles.

Measured on the project that exposed it:

START errors = 5
  attempt 1                      -> 1     (best)
  attempt 2  regressed: 11 vs 1  -> rolled back
  attempt 3  regressed: 11 vs 1  -> rolled back
END   errors = 1

Before this the same run finished at 11 errors, worse than it started. It now finishes at 1. Rejected attempts are visible as heal_attempt_regressed rather than being rolled back silently.

Still not green on that project; the remaining single error is a genuine defect the loop has not yet fixed. The guarantee added here is narrower and worth stating exactly: the loop can no longer go backwards.

[1.0.4] - 2026-09-14

Fixed

Self-heal was never shown the file it was asked to fix — closes #62. The Java path could not repair a project at any attempt budget, and the reason was not the model.

The diagnosis prompt spends a 6,000-char file budget in generation order, which is effectively alphabetical. On a 16-file Spring project 10 files were omitted, including both files the compiler named — so the reasoner was asked to fix code it had never seen. Two further defects in the same path, each sufficient alone:

  • current_files = artifact.files replaced the file list with whatever the coder chose to rewrite, so the model's view shrank every attempt while the runner kept compiling all 16 from disk;
  • the history block repeated the whole file listing per attempt, putting the prompt at ~7,900 tokens against Groq's 7,000 ITPM ceiling — five 413s, an exhausted candidate chain, and a dead run.

Files the failure output names now come first and are exempt from the per-file cap; matching is on basename or type name, because javac reports location: variable ex of type ...OrderAlreadyExistsException and never the filename. Fixes are merged rather than replacing the file list, and history carries what changed plus the error instead of another copy of the source.

Measured on the project that exposed it: prompt 7,912 → 3,957 tokens, broken files omitted → shown in full, visible lines of the broken file 41 → 103, and 3 of the 4 original compile errors now get fixed where previously none could be.

Not yet green: the loop now converges and then oscillates — an attempt that fixes one file can break another it had already changed. Tracked separately.

[1.0.3] - 2026-09-14

Fixed

A run that wrote no tests reported "All tests passed" — closes #59. Found by running the tool, not reading it: run "add a REST endpoint for creating an order" --lang java produced seven Java sources and zero test files, and reported success, because mvn test prints "No tests to run." and exits 0.

An exit code answers "did anything fail", never "did anything run". So a generation with no tests was indistinguishable from one whose tests all passed — in the direction that looks like success — which falsified the headline claim that this tool writes the code and its own tests.

It also corrupted self-heal: the cheapest way to make a runner exit 0 is to write no tests, so the repair loop was being rewarded for deleting them.

Three independent checks now, because no single one covers every runner: test files must exist before the runner is invoked; a runner that documents an empty collection is believed (pytest's exit 5); and where the runner prints a count, the count decides — surefire's Tests run: N and pytest's summary are parsed, and exit-0-with-zero-executed is a failure. BUILD SUCCESS is not evidence that anything was verified. Maven accordingly loses -q, which was hiding that very line.

Measured live after the fix, same task: 3 test files in the correct Maven layout instead of none, and an honest FAILED on a real compile error instead of a false pass.

[1.0.2] - 2026-09-14

Fixed

A reasoning trace could invert the task classifier's answer — closes #53. strip_reasoning_trace is applied at 17 call sites across the engine and was absent from exactly two: detect_language and classify_complexity, the pair whose parsing is most fragile. Both match by substring and both fall back silently rather than raising, so a mis-parse was invisible at runtime.

Measured against the pre-fix parse, <think>This is not complex at all, just one function.</think>simple made classify_complexity return "complex" — the opposite of the model's own conclusion, because the word sat in the trace being discarded. That answer decides which phases run and whether generation parallelises, so the failure was a silently different pipeline, not a cosmetic one.

Neither function had any test; tests/test_orchestrator.py stubs both wholesale to reach run_task's branching. Eight added, calling the real functions. Both trace tests are verified negative controls — the language case deliberately names an option sorting earlier than the true answer, because a later one passes against the old code too, by luck of iteration order.

The two classification call sites no longer hardcode reasoning_effort — closes #54. They passed "low" as a literal, reaching past _light_reasoning_effort_for, which exists because each Groq reasoning family defines that parameter differently and the wrong family's value is a hard 400. Behaviour-preserving today only by coincidence: the adapter independently returns "low" for the current model. It stops being a coincidence the moment models.yaml names a different one — a config edit — where the cost is a BadRequestError retried five times (31–39s) and reported as a withdrawn model.

[1.0.1] - 2026-09-14

Fixed

pip install . labelled the package 0.1.0 — closes #50. pyproject.toml carried its own version = "0.1.0" while VERSION and this changelog said 1.0.0, so a recipient installing the project received metadata announcing a pre-release on the day 1.0.0 shipped. VERSION is now the single source of truth and pyproject.toml reads it ([tool.setuptools.dynamic]) rather than keeping a copy, which removes the drift class instead of guarding against it.

Found because the push gate — inert for this repo's entire life until 1.0.0 added VERSION — did its job and blocked this branch for having no version bump.

The warm-load budget test no longer fails on a busy machine — closes #49. It took a single wall-clock sample and compared it to a fixed 300ms, and had gone red three times under full-suite runs (~508ms) while passing every time in isolation. The budget was never the problem: idle, a warm load is 73–85ms, so the assertion had ~4x headroom and still failed. A single sample on a machine that is not dedicated cannot be made reliable by any choice of ceiling.

It now asserts on the minimum of five samples — contention can only make a sample slower, so the minimum estimates the uncontended cost — and prints every sample on failure, because min=412 samples=[412,455,430,501,447] and min=310 samples=[310,315,312,318,311] are different bugs.

Reproduced on demand rather than assumed: 18 concurrent graph-building processes produced [384, 231, 242, 559, 807], three of five over budget, so the old assertion had a 60% chance of failing that window and passed only on the luck of its draw. Pure CPU spin did not reproduce it — the contention that matters is allocator and memory pressure from concurrent processes, not busy loops.

Recorded rather than oversold: this reduces the flake, it does not eliminate it. min-of-N fails with p^N, and at the oversubscription that produced 508ms no practical N helps. If it fires a fourth time the answer is a -m perf marker, not a larger N or a larger budget.

Changed

The load-cost figures were re-measured and given one canonical home. Seven prose copies across four files had drifted to roughly twice the truth — a cold process is ~152ms, not 0.28s, and a four-server MCP compose ~0.6s, not 1.1s — and the warm figure was a cold measurement mislabelled, which understated ADR-4's own no-cache case by ~2.4x. kgf/loader.py's module docstring is now the single place they are stated, with the cache condition recorded. The MCP surface is also correctly described as 15 tools, not 17.


[1.0.0] - 2026-09-14

First versioned release. The project had no VERSION file before this, which meant push-gate reported "tracks no VERSION file, so the version rule does not apply" on every push — the safety gate was silently inert (#47).

Fixed

A transient provider fault no longer spends a candidate — closes #45. Found by running the tool end to end, not by reading code. A real run died with ConfigError: every configured candidate is unavailable ... Add more candidates to models.yaml while the configuration was fine: the very next run of the same command wrote 25 files with tests passing. Two transient 5xx/timeouts inside one run walked router_fast off its 2-candidate chain.

Two defects, the second outliving the run: the diagnosis was false (it told the operator to edit working configuration in response to a provider blip), and the demotion was permanent — _active_index has no reset, so even a surviving run stayed pinned to its fallback model for the whole session. These errors arrive already retried (5 attempts, 1→30s backoff), so the fix is not more retrying but not billing a candidate for a fault that was never its own. call_role now restores the index and raises ProviderTroubleError; a genuinely withdrawn model still raises ConfigError naming models.yaml.

Startup survives a provider it cannot verify — closes #41. validate_startup probes each candidate with a live GET /v1/models and caught only one error class, so a key that was set but wrong, or an unreachable host, killed the process with a message naming neither the role nor the provider. Now classified three ways: a rejected credential skips at WARNING naming the env var, an unreachable host skips at INFO, and a 429 does not skip at all — a throttled catalogue listing says nothing about whether the model works. The except ordering is load-bearing and verified: those types are all APIStatusError subclasses.

Groq's real token ceiling is 8000, not the 6000 assumed — closes #43. Measured from live response headers on all three configured models. A quarter of the granted budget was unused, and every coder call logged "exceeds the whole ceiling; throttled across minutes". rpm_budget was deliberately left at 30: the same headers report 1000 requests but Groq returns both per-minute and per-day ceilings and the observed reset window matches neither, and this configuration's history is two providers added on published figures that did not survive a real call.

The confidence floor is calibrated on the text production actually ranks — closes #16. It was measured on raw task text and applied to LLM-engineered prompts. Engineered confidence bottoms out at 0.4758, so the old 0.45 floor admitted 33 of 33 held-out cases and reported every match as selected, including all 15 whose domain was wrong — a threshold whose whole job is withholding a weak match had never once fired. Now 0.59, the measured optimum.

Verified against the live pipeline: for add a REST endpoint for creating an order, the personas the old floor would have applied were as_built_doc_generator and reverse_engineering_analyst — a reverse- engineering documentation persona steering Spring Boot code. The new floor correctly returns low_confidence and uses the generic prompt instead.

Recorded rather than oversold: the Clopper-Pearson 95% interval on the new precision is [0.476, 0.927], whose lower bound sits below the admit-everything base rate, so at n=33 the improvement is not statistically established. What is established is that 0.45 did nothing.

Other correctness fixes. The TPM concurrency cap is now enforced by slicing in run_dag rather than computed, logged and ignored (#36). Bash no longer inherits the caller's stdin, which hung every command when kgf runs as an MCP server and would have let a granted child read the client's own protocol traffic (#24). Tests that asserted the library was broken were converted to synthetic fixtures so repairing the data could not silently retire them (#27, #29, #34, #38).

Added

kgf's own MCP surface (#23) — four stdio servers over the knowledge graph, 15 tools, with posture read from ~/.kgf/mcp.json at launch. Bash stays denied under the default flags, denied when a call argument asks for it, and denied when the server is spawned with the environment variable set.

Google Gemini and provider groundwork (#31) — declared but wired to no role, so it costs nothing at startup and becomes live the moment GEMINI_API_KEY exists. Adding it required no code change, which is the "any LLM is just configuration" claim actually holding.

Changed

claude-global-library was repaired at source and rebuilt to 29.98.0: 31 documents that would not load now do (1562/1562, up from 1544), and that library's own VERSION was five releases behind its changelog. kgf's pinned figures were re-measured against the rebuild rather than adjusted until green, and a pin-freshness test now fails loudly instead of letting a stale pin silently disarm three measured assertions.

Known limitations

  • Selection quality is the ceiling. Top-3 domain accuracy is 66.7% on the frozen held-out set. spring-boot-microservices exists in the library and is not in the top 3 for a mainstream REST task, so many runs correctly fall to low_confidence and use the generic prompt. --agent <name> forces a specific persona when you know which one you want.
  • The coder call is paced across minutes. MAX_CODER_TOKENS (8000) plus prompt exceeds even the true 8000 TPM ceiling, so one generation cannot fit in a single minute's budget at any context size.
  • No SRS.md. Required by rules/44 at first Step-13; deliberately not invented to satisfy a checklist.
  • test_warm_load_is_within_budget is timing-flaky under a full-suite run (~508ms against a 300ms budget) while passing in isolation. Fixed in 1.0.1 — see above. The two figures first quoted here were not comparable: 508ms was the assertion's own measurement, while the 0.31–0.33s cited beside it was pytest's reported duration for the whole test, which includes an untimed warm-up load.